Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"name": "cogos-harness",
"source": "./plugins/cogos-harness",
"description": "Portable Claude Code membrane for a CogOS node: session-lifecycle hooks, kernel-vitals proprioception, a compaction bypass path, a threads registry for open-ended waits, and the cogos-kernel MCP server. Degrades to a no-op wherever the kernel or a cog workspace is absent.",
"version": "0.3.0",
"version": "0.4.0",
"category": "development",
"keywords": ["cogos", "kernel", "mcp", "hooks", "proprioception", "session-continuity", "threads"],
"homepage": "https://github.com/myrgic/plugins"
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Commit-message convention: [Conventional Commits](https://www.conventionalcommit

### Added

- `cogos-harness` 0.4.0: mods skeleton. One function-hooks module (`hooks/index.mjs`) composes five stub mods (`cog-redact`, `cog-guard`, `cog-context`, `cog-status`, `cog-receipt`) over a shared `lib/` (config loader, typed order, vitals cache, ledger, kernel fetch). All mods are off unless `~/.cog/conf/harness-mods.json` enables them. The existing shell hooks are unchanged. Tests run with `claude plugin test`; vendored 2.1.292 types allow `tsc`.
- New `cogos-architecture` plugin with the `corpus-cross-reference` skill — automatically cross-checks a proposed RFC or ADR against the existing substrate corpus (composition gaps, conflicts, prior art, frontmatter validation, numbering). Handles both corpus numbering schemes (cog workspace 3-digit, cogos repo 4-digit). Includes `resources/canonical-frontmatter-shapes.md`, `resources/refs-vocabulary.md`, `resources/cross-check-heuristics.md`, and `tools/list_corpus.sh`.
- New `cogos-harness` plugin — the first plugin in this marketplace of the hooks/MCP-server kind rather than skills-only. Bundles `SessionStart`/`SessionEnd` presence hooks, a compaction bypass path, ambient kernel-vitals + context-window proprioception on `UserPromptSubmit`, the `cogos-kernel` MCP server, and the `btw`/`consolidate`/`handoff` skills. Every hook degrades to a silent no-op wherever the kernel or a cog workspace is absent.
- `cogos-architecture`'s `.claude-plugin/plugin.json` — the package was declared in `marketplace.json` but shipped no manifest of its own, so installing it specifically would misbehave.
Expand Down
16 changes: 13 additions & 3 deletions plugins/cogos-harness/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "cogos-harness",
"description": "Portable Claude Code membrane for a CogOS node: session-lifecycle hooks, kernel-vitals proprioception, a compaction bypass path, a threads registry for open-ended waits, and the cogos-kernel MCP server. Degrades to a no-op wherever the kernel or a cog workspace is absent.",
"version": "0.3.0",
"description": "Portable Claude Code membrane for a CogOS node: session-lifecycle hooks, kernel-vitals proprioception, a compaction bypass path, a threads registry for open-ended waits, the cogos-kernel MCP server, and (0.4.0) one function-hooks module composing five mods (cog-redact, cog-guard, cog-context, cog-status, cog-receipt), all off unless ~/.cog/conf/harness-mods.json turns them on. Degrades to a no-op wherever the kernel or a cog workspace is absent.",
"version": "0.4.0",
"author": {
"name": "myrgic"
},
"homepage": "https://github.com/myrgic/plugins",
"repository": "https://github.com/myrgic/plugins",
"license": "MIT",
"keywords": ["cogos", "kernel", "mcp", "hooks", "proprioception", "session-continuity", "threads"]
"keywords": [
"cogos",
"kernel",
"mcp",
"hooks",
"proprioception",
"session-continuity",
"threads",
"mods"
],
"types": "./types/index.d.ts"
}
73 changes: 73 additions & 0 deletions plugins/cogos-harness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,79 @@ a good predicate, how to close). `pull-context-dispatch` is **not**
duplicated here — it already ships in this marketplace's `cogos-workflow`
package; install that alongside this plugin if you want it too.

## Mods (0.4.0): one function-hooks module, five mods

Alongside the shell hooks above (which all keep running unchanged: `hooks.json`
carries both `"hooks"` and `"modules"`), 0.4.0 adds Claude Code **mods**
(function hooks, Claude Code >= 2.1.287). In 0.4.0 they are **stubs**: they
load, register, and do nothing.

| Mod | File | Hooks (stub) | Brief |
|---|---|---|---|
| cog-redact | `mods/cog-redact.mjs` | `tool.call`, `prompt.submit` | secrets out before the model reads them |
| cog-guard | `mods/cog-guard.mjs` | `tool.call` | one permission mod for every gate |
| cog-context | `mods/cog-context.mjs` | `prompt.context` | one in-process context injector |
| cog-status | `mods/cog-status.mjs` | `session.measure` | status band + the vitals-cache owner |
| cog-receipt | `mods/cog-receipt.mjs` | `tool.call`, `turn.complete` | turn receipts to the stage and the bus |

**Off by default.** Each mod reads its mode from `~/.cog/conf/harness-mods.json`
(override path: `COGOS_HARNESS_MODS_CONF`):

```json
{ "version": 1, "mods": { "redact": "off", "guard": "off", "context": "off", "status": "off", "receipt": "off" } }
```

`off` = the hook is `next(e)` after one cached config read; `shadow` = runs and
logs (`hook: "mod/<name>/<event>"`, `shadow: true`) to
`~/.cog/run/hook-health.jsonl` but changes nothing; `on` = in effect. A missing
or malformed file means every mod is off.

**Layout.**

```
hooks/index.mjs the ONE hooks module: calls each mod's register(on, options) in order
mods/cog-*.mjs one file per mod (each owns only its file + tests/<name>.test.ts + its types block)
lib/cfg.mjs shared config loader (off/shadow/on; missing file = all off)
lib/order.mjs the typed order: redact → guard → context → status → receipt
lib/cache.mjs shared vitals cache read/write with age (writer: cog-status)
lib/ledger.mjs hook-health ledger rows (append via process.run, never rewrite)
lib/kernel.mjs kernel fetch with X-Cogos-Grant
lib/stub.mjs the skeleton's no-op body
types/index.d.ts PluginState contract, one block per mod
types/vendor/ Claude Code's own mod types (2.1.292), for tsc
tests/*.test.ts one file per mod + order.test.ts + lib.test.ts
```

**Engine rules the layout follows** (each measured with `claude plugin
validate` on 2.1.292):

- One plugin lists one hooks module, so `hooks/index.mjs` composes the mods.
- One plugin may not register the same event twice without a matcher, so
every mod hook carries one (`{}` matches everything).
- `$` is followed only into functions declared in the same file, never across
an import. So `lib/` functions take an `io` adapter, and each mod builds it
from `$` with its own same-file `ioOf($)`. The base keys are identical in all five.
- Hooks nest in registration order, first outermost. `redact` therefore sees a
tool call before `guard` (plan §3.2), and `tests/order.test.ts` checks this
on the real chain.

### Build and test: one command for every mod agent

From the repo root:

```bash
claude plugin validate plugins/cogos-harness \
&& claude plugin test plugins/cogos-harness \
&& npx -y -p typescript@5 tsc -p plugins/cogos-harness/tsconfig.json --noEmit
```

All three must pass before a mod PR is opened. A mod PR touches only
`mods/<name>.mjs`, `tests/<name>.test.ts` and its own block in
`types/index.d.ts`. `hooks/index.mjs`, `lib/cfg.mjs`, `lib/order.mjs` and the
`ioOf` base keys are shared; change them through a separate one-line PR. The
exceptions: cog-redact owns the order block and `lib/order.mjs`, and
cog-status owns `lib/cache.mjs`.

## What's excluded, and why

- **`dispatch-model-probe.py`** — audited as a "by-reference companion" to
Expand Down
31 changes: 24 additions & 7 deletions plugins/cogos-harness/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"modules": [
"./index.mjs"
],
"hooks": {
"SessionStart": [
{
Expand All @@ -7,13 +10,17 @@
{
"type": "command",
"command": "python3",
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/user-scope-session-start.py"],
"args": [
"${CLAUDE_PLUGIN_ROOT}/hooks/user-scope-session-start.py"
],
"timeout": 10
},
{
"type": "command",
"command": "python3",
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/seat-identity-heal.py"],
"args": [
"${CLAUDE_PLUGIN_ROOT}/hooks/seat-identity-heal.py"
],
"timeout": 5
}
]
Expand All @@ -24,7 +31,9 @@
{
"type": "command",
"command": "python3",
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/compaction-handoff.py"],
"args": [
"${CLAUDE_PLUGIN_ROOT}/hooks/compaction-handoff.py"
],
"timeout": 10
}
]
Expand All @@ -37,7 +46,9 @@
{
"type": "command",
"command": "python3",
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/user-scope-session-end.py"],
"args": [
"${CLAUDE_PLUGIN_ROOT}/hooks/user-scope-session-end.py"
],
"timeout": 10
}
]
Expand All @@ -50,13 +61,17 @@
{
"type": "command",
"command": "python3",
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/user-scope-proprioception.py"],
"args": [
"${CLAUDE_PLUGIN_ROOT}/hooks/user-scope-proprioception.py"
],
"timeout": 5
},
{
"type": "command",
"command": "python3",
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/threads-warn.py"],
"args": [
"${CLAUDE_PLUGIN_ROOT}/hooks/threads-warn.py"
],
"timeout": 8
}
]
Expand All @@ -69,7 +84,9 @@
{
"type": "command",
"command": "python3",
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/threads-gate-pr.py"],
"args": [
"${CLAUDE_PLUGIN_ROOT}/hooks/threads-gate-pr.py"
],
"timeout": 5
}
]
Expand Down
40 changes: 40 additions & 0 deletions plugins/cogos-harness/hooks/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// hooks/index.mjs — THE one hooks module of cogos-harness (plan §2).
//
// hooks.json "modules" takes exactly one entry per plugin, so this file
// composes the five mods by calling each one's register(on, options).
// Hooks nest in registration order, first outermost: the first mod sees an
// event first and its result last. Each mod gates itself at dispatch
// (lib/cfg.mjs); with ~/.cog/conf/harness-mods.json missing every mod is off
// and every hook is next(e): the plugin is inert by default.
//
// Shared file: changes go through a one-line PR the mod agents can see
// (plan §4). cog-redact owns the order block.

import { register as redact } from "../mods/cog-redact.mjs";
import { register as guard } from "../mods/cog-guard.mjs";
import { register as context } from "../mods/cog-context.mjs";
import { register as status } from "../mods/cog-status.mjs";
import { register as receipt } from "../mods/cog-receipt.mjs";
import { ORDER } from "../lib/order.mjs";

// ── order block (owner: cog-redact; contract: plan §3.2) ─────────────────
// redact rewrites secrets out of tool input BEFORE guard decides on it;
// guard decides on redacted input; receipt (last) records what ran.
// tests/order.test.ts fails if this order changes. The validator wants `on`
// handed to functions named at the top of the file, so the order is spelled
// as literal calls below, which MUST match lib/order.mjs ORDER (the typed
// contract; tests/order.test.ts checks the real chain against it).
export const MOD_ORDER = ORDER;

/**
* @param {import('claude-code').On} on
* @param {import('claude-code').PluginOptions} options
*/
export function register(on, options) {
redact(on, options);
guard(on, options);
context(on, options);
status(on, options);
receipt(on, options);
}
// ─────────────────────────────────────────────────────────────────────────
113 changes: 113 additions & 0 deletions plugins/cogos-harness/lib/cache.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// lib/cache.mjs — the shared vitals cache (plan §3.3). Owner after the
// skeleton lands: cog-status (the single writer). cog-context only reads.
//
// In-process: $.state cogos-harness / "status.vitals" (survives hot reload)
// Mirror: ~/.cog/run/vitals.json for out-of-process readers (stage,
// Hermes darkstar-vitals, CogBar).
//
// Shape (Vitals): { written_at, kernel, usage, disk, git, prs, hook_health }
// Every reader must show the age once it passes STALE_MS (2 minutes): the
// fix for the 54 h-stale vitals finding. Never hide a stale value.

export const STALE_MS = 2 * 60_000;

/**
* @typedef {{
* written_at: number,
* kernel?: { reachable: boolean, version?: string, state?: string },
* usage?: { context_pct?: number, five_h_pct?: number, week_pct?: number },
* disk?: { free_gb?: number, low?: boolean },
* git?: { branch?: string, dirty?: number, ahead?: number },
* prs?: { open?: number, stale?: number },
* hook_health?: Record<string, unknown>,
* }} Vitals
*/

/**
* Age of a vitals record in ms, or Infinity when absent/unstamped.
* @param {Vitals | undefined | null} v
* @param {number} now
*/
export function ageMs(v, now) {
if (!v || typeof v.written_at !== "number") return Infinity;
return Math.max(0, now - v.written_at);
}

/** @param {number} ms */
export function fmtAge(ms) {
if (!Number.isFinite(ms)) return "never";
const s = Math.round(ms / 1000);
if (s < 90) return `${s}s`;
const m = Math.round(s / 60);
if (m < 90) return `${m}m`;
const h = Math.round(m / 60);
return h < 48 ? `${h}h` : `${Math.round(h / 24)}d`;
}

/**
* The suffix a reader appends: "" when fresh, " (stale 3m)" when older than
* STALE_MS, " (no vitals)" when absent.
* @param {Vitals | undefined | null} v
* @param {number} now
*/
export function staleSuffix(v, now) {
const a = ageMs(v, now);
if (!Number.isFinite(a)) return " (no vitals)";
return a > STALE_MS ? ` (stale ${fmtAge(a)})` : "";
}

/**
* Read the vitals and their age. Never throws.
* @param {any} io needs now, getVitals
* @returns {Promise<{ vitals: Vitals | undefined, age_ms: number, stale: boolean }>}
*/
export async function readVitals(io) {
let now = 0;
try {
now = await io.now();
} catch {
now = 0;
}
/** @type {Vitals | undefined} */
let vitals;
try {
const held = await io.getVitals();
vitals = held && held.value ? held.value : undefined;
} catch {
vitals = undefined;
}
const age_ms = ageMs(vitals, now);
return { vitals, age_ms, stale: age_ms > STALE_MS };
}

/**
* Write the vitals (stamps written_at) to $.state and mirror the JSON file.
* Only cog-status may call this (only its ioOf has setVitals). Never throws.
* @param {any} io needs now, setVitals, home, write
* @param {Omit<Vitals, "written_at">} partial
* @param {{ mirror?: boolean }} [opts]
*/
export async function writeVitals(io, partial, opts = {}) {
let now = 0;
try {
now = await io.now();
} catch {
now = Date.now();
}
/** @type {Vitals} */
const vitals = { ...partial, written_at: now };
try {
await io.setVitals(vitals);
} catch {
/* fail open */
}
if (opts.mirror !== false) {
try {
const home = await io.home();
if (home) await io.write(`${home}/.cog/run/vitals.json`, JSON.stringify(vitals, null, 2) + "\n");
} catch {
/* fail open */
}
}
return vitals;
}
Loading
Loading