Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to `@fusengine/harness`. Format: [Keep a Changelog](https://

## [Unreleased]

## [0.1.93] - 2026-09-03

### Added

- **Opt-in multi-agent PRD ownership coordination** (`src/prd/`) — coordinates file/task ownership across multiple sub-agents working from a shared PRD, entirely inert by default: requires both `FUSE_PRD=1` and a routing file (`apex/prd.json`) present before any behavior activates.

## [0.1.92] - 2026-09-02

### Fixed
Expand Down
28 changes: 14 additions & 14 deletions MEMORY/LESSON.md

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ don't assume either the old or the "everything in one place" story):
| `harness check` | cli-mode: check staged files in a pre-commit step, exit non-zero on a violation. For harnesses without hooks. |
| `harness doctor` | Print the version + resolved path of the harness *actually executing*, and compare it to npm's latest — the fast way to catch a stale global (see Pinning). |
| `harness --version` | Print the running version (bare, on stdout) and exit. |
| `harness prd status [--json]` | Read-only, works without `FUSE_PRD=1`. Print each task's router status and sub-task progress. |
| `harness prd validate <task> [agent]` | Requires `FUSE_PRD=1`. Cross-check a task PRD against agent reports and promote matching sub-tasks/router entries to `validated`. |
| `harness prd compact <task>` | Requires `FUSE_PRD=1`. Collapse a fully-validated task PRD to its compacted shape. |

Every invocation writes a `@fusengine/harness vX.Y.Z` banner to **stderr** (never
stdout — the hook JSON contract stays clean) so you can see which version ran.
Expand Down Expand Up @@ -313,6 +316,7 @@ non-zero exit is swallowed — a broken or absent player can never break a hook
| `FUSE_LESSONS_THROTTLE_MIN` | Lessons-injection throttle, minutes (default `5`). |
| `FUSE_ENFORCE_GEMINI_MCP` | **Opt-in (default off).** Blocks hand-written Tailwind UI (`.tsx/.jsx/.vue/.svelte`) until a `mcp__gemini-design__*` call is made this session. Read fresh per call (`src/policy/gemini-mcp-gate.ts`). |
| `FUSE_DESIGN_GEMINI` | **Opt-in (default off), a *different* gate from the one above.** Enables the design-pipeline's own Gemini gates (`create_frontend` validation + "generate before hand-writing HTML/CSS") — inert unless a design agent is active (`src/policy/design/gates.ts:58-60`, see [docs/design.md](docs/design.md)). |
| `FUSE_PRD` | **Opt-in (default off).** Set to exactly `1` to activate task/agent PRD ownership coordination — still inert without a `prd.json` router under `<homeSeg>/apex/`. See [PRD coordination](#prd-coordination-opt-in) below and [docs/prd.md](docs/prd.md). |
| `FUSE_MCP_TTL_SEC` | MCP (Context7/Exa) cache freshness, seconds (default 48h, `src/runtime/mcp-key.ts`). |
| `FUSE_WEBFETCH_TTL_SEC` | WebFetch cache freshness, seconds (default 24h — pages stale faster than docs). |
| `FUSE_CONFIRM_SUBAGENT_WINDOW_SEC` | G0 cool-down (seconds, default `300`) for the `CONFIRM <code>` mechanism above — no token can be placed within this window of the last SubagentStart/Stop seen for the session. |
Expand Down Expand Up @@ -358,6 +362,18 @@ registerGuard(({ tool, command }) =>
: null);
```

## PRD coordination (opt-in)

Set `FUSE_PRD=1` and write a router file so a lead can split one task
across several sub-agents, each restricted to writing only its own
report. Full walkthrough, guard behavior, and the CLI: [docs/prd.md](docs/prd.md).

Minimal example — `.claude/apex/prd.json`:

```json
{ "auth-refactor": { "prd": "prd/auth-refactor-prd.json", "status": "assigned" } }
```

## Subpath exports

| Subpath | What |
Expand Down Expand Up @@ -386,6 +402,7 @@ registerGuard(({ tool, command }) =>
| [docs/modules.md](https://github.com/fusengine/harness/blob/main/docs/modules.md) | cache · refs · state · memory · statusline · util |
| [docs/adapters.md](https://github.com/fusengine/harness/blob/main/docs/adapters.md) | adapters, compatibility, `harness init`/`hook` wiring |
| [docs/design.md](https://github.com/fusengine/harness/blob/main/docs/design.md) | design-agent pipeline — state machine, gates, opt-in Gemini |
| [docs/prd.md](https://github.com/fusengine/harness/blob/main/docs/prd.md) | PRD task/agent ownership coordination — opt-in, `FUSE_PRD=1` |
| [CHANGELOG.md](https://github.com/fusengine/harness/blob/main/CHANGELOG.md) | release history |

Run `bun run docs:api` for the generated typedoc API reference.
Expand Down
20 changes: 20 additions & 0 deletions docs/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ assuming a gate that works on Claude Code also works elsewhere.
| **hermes** | `adapters/hermes/index.ts` | `pre_tool_call` proven: reuses the Claude stdin reader, blocks via `{decision:"block",reason}` (lines 12-36) | untested — no lifecycle dispatch wired for Hermes in this repo | `ask`/`inform` degrade to non-blocking `{context}` — Hermes "has no interactive ask state" (lines 27-28). |
| **kimi** | `adapters/kimi/index.ts` | `PreToolUse` denies via the camelCase JSON channel `{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"…"}}` on stdout at **exit 0** (verified live against kimi-code v0.27.0 — exit 2 with stderr = reason also blocks, but is not required) — only `deny` is documented. `ask` is **downgraded to deny** prefixed `[downgraded from ask — Kimi Code has no interactive approval]`, with a `CONFIRM <code>` recourse appended (`runtime/confirm/`, see below); `inform` rides plain stdout text at exit 0, never wrapped in JSON. Blocking events: `UserPromptSubmit`, `PreToolUse`, `Stop`. | Observation only: `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `PermissionResult`, `SessionStart`, `SessionEnd`, `SubagentStart`, `SubagentStop`, `StopFailure`, `Interrupt`, `PreCompact`, `PostCompact`, `Notification` — Kimi delivers them but **ignores any response**, so no verdict can be returned from them. | A **hook** cannot request approval — Kimi's `ask` lives in a parallel, hook-unreachable system (`[[permission.rules]] decision = "ask"` in `config.toml`), hence the ask→deny downgrade. Hooks are configured **only** in the global `~/.kimi-code/config.toml` (no project-local hooks file), so `harness init` writes no kimi wiring — see [Kimi Code — manual wiring](#kimi-code--manual-wiring). **Fail-open by design**: any exit code other than 0/2, a timeout, or a crash lets the call through. Stdin payload is snake_case and carries only `hook_event_name`, `session_id`, `cwd`, `tool_name`, `tool_input.command` and an undocumented `tool_call_id` (unused here) — no `transcript_path`, no `permission_mode`, no `tool_response`. Verified live against kimi-code v0.27.0 for `PreToolUse`/`Bash`. Instructions file is `AGENTS.md`, not `CLAUDE.md`. |

### PRD capabilities

Opt-in (`FUSE_PRD=1`), documented in full at [prd.md](./prd.md). Coverage
splits by **capability**, not by adapter — a single per-adapter verdict
would be misleading here:

| Capability | claude-code | codex | cursor | kimi | cline / gemini-cli / hermes |
|---|---|---|---|---|---|
| Write ownership block | full | full | consultative only (never blocks) | consultative only (never blocks) | full, if the consumer wires `PreToolUse` |
| Bash-under-`prd/` deny | yes | yes | yes | yes | yes |
| `SubagentStart` slice injection | yes | yes | **no** — the slice IS built (identical to claude-code's), but `adapters/cursor/respond.ts`'s `toCursorLifecycleResponse` collapses any non-denied `subagentStart` to bare `{"permission":"allow"}`, dropping it; measured byte-identical output with a matching PRD assignment, with no router at all, and with `FUSE_PRD` unset | delivered, ignored (observation-only) | gemini-cli: yes, delivered — routed through `respond()`'s native "inform" shape, then re-wrapped by `joinContextResponses` into the shared Claude-style `hookSpecificOutput.hookEventName/additionalContext` envelope plus a `[NOTE]` title line (not gemini-cli's own minimal shape, but the text arrives). hermes: yes, delivered — same Claude-style `contextResponse` as claude-code; Hermes's own documented non-blocking shape is `{context}`, not `hookSpecificOutput`, so whether a real Hermes client reads it is unverified. cline: yes, delivered in its own native `{contextModification}` shape — `joinContextResponses` keeps whichever envelope its parts came from instead of assuming the Claude one (measured 476 bytes where an earlier build emitted none; the six other targets stay byte-identical, gemini-cli 529 = 529, the Claude-family four 506 = 506) |
| `SubagentStop` block-once | yes | yes | delivered, response ignored | delivered, response ignored | yes — each in its OWN native block shape via `respond()`: gemini-cli `{"decision":"deny","reason"}`, cline `{"cancel":true,"errorMessage"}`, hermes `{"decision":"block","reason"}` (via `blockResponse`, which happens to match Hermes's own documented block contract). Block-once verified: 1st call blocks, an identical replay is silent (empty stdout) on all three, under both the default journal-based session track and legacy `FUSE_TRACK_JOURNAL=0` |
| Lead `Stop` block-once | yes | yes | not applicable | best effort — Kimi's `Stop` is a documented blocking event, but not verified live | yes — same per-adapter native shapes and block-once behavior as `SubagentStop` above (gemini-cli/cline/hermes), verified under both tracking modes |

Cursor and Kimi never send an agent-identity field on a Write/Edit
payload, and a Cursor sub-agent's write runs under a session ID
unrelated to its own `SubagentStart` event — so per-write ownership
enforcement isn't possible on either target. Both stay consultative
permanently; the PostToolUse cross-check is the only enforcement there.

## `CONFIRM <code>` — recourse for a degraded `ask`

Both harnesses above downgrade `ask` to a hard `deny`: Kimi's binary
Expand Down
1 change: 1 addition & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ float / `<= 0` all fall back to the default.
| `FUSE_HARNESS_MARKETPLACES` | `fusengine-plugins` | marketplaces auto-scanned for `solid-*` skill refs when `FUSE_HARNESS_REFS` is unset |
| `FUSE_ENFORCE_GEMINI_MCP` | _(off)_ | opt-in — blocks hand-written Tailwind UI (`.tsx/.jsx/.vue/.svelte`) until a `mcp__gemini-design__*` call is made this session (`policy/gemini-mcp-gate.ts`) |
| `FUSE_DESIGN_GEMINI` | _(off)_ | opt-in — a **different** gate from the one above: enables the design-pipeline's own Gemini gates (`policy/design/gates.ts`), inert unless a design agent is active — see [design.md](./design.md) |
| `FUSE_PRD` | _(off)_ | opt-in — set to exactly `1` to activate the PRD task/agent ownership module (`policy/prd/prd-enabled.ts`); still inert without a `prd.json` router under `<homeSeg>/apex/` — see [prd.md](./prd.md) |
| `FUSE_MCP_TTL_SEC` | `172800` (48h) | Context7/Exa cache freshness (`runtime/mcp-key.ts`) |
| `FUSE_WEBFETCH_TTL_SEC` | `86400` (24h) | WebFetch cache freshness — pages stale faster than docs |
| `FUSE_CONFIRM_SUBAGENT_WINDOW_SEC` | `300` | G0 cool-down (seconds) for the `CONFIRM <code>` mechanism (`runtime/confirm/confirm-subagent.ts`) — no confirm token can be placed within this window of the last SubagentStart/SubagentStop seen for the session. Reuses `parseEnvInt` directly (not `resolveTtlSec`, whose default is hardcoded to 120 regardless of key) |
Expand Down
32 changes: 32 additions & 0 deletions docs/guards.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,35 @@ their own chain via `evaluateApex(ctx)`:

Each is individually exported and overridable — pass your own array to
`evaluateApex(ctx, gates)`.

## PRD ownership

Opt-in (`FUSE_PRD=1`), documented in full at [prd.md](./prd.md). It adds a
per-file write-ownership check on top of the chain above: only the
coordinator may write the router or a task PRD, and only the named agent
may write its own `prd/agents/<agent>-prd.json` report.

This authorized PRD traffic is what actually needs a carve-out from
`protectedPathGuard` above: that guard already lists `.claude/apex/`
under its blocked fragments, so every PRD file lives inside a path the
chain blocks by default. When PRD is active, a write the ownership check
allows short-circuits past the rest of `evaluate()` — including the
file-size and APEX-freshness gates — the same way `Explore`/`Plan`
agents are already exempt from file-size today. Nothing changes when PRD
is off: `protectedPathGuard` keeps blocking `.claude/apex/**` exactly as
it always has.

**A mixed `apply_patch` envelope never gets this short-circuit, even for
its legitimate file.** `prdPreGate` only allows-through a PURE-PRD
envelope, where every candidate file classifies in-scope
(`inScope.length === files.length`); the moment a single `apply_patch`
call mixes one PRD-scoped file with any other, unrelated file,
`prdPreGate` returns `null` for the whole call and it falls through to
`applyPatchGate`'s ordinary per-file `protectedPathGate` — which
unconditionally blocks the `.claude/apex/` file, same as if PRD were off.
Verified live: an `apply_patch` envelope touching only the agent's own
`prd/agents/<agent>-prd.json` report is allowed; the identical hunk for
that same file, bundled in ONE envelope with an unrelated `Add File:` for
a normal source file, is denied with `[BLOCKED] Protected path` — not
`[BLOCKED] PRD ownership` — even though the PRD file's own ownership
would otherwise have been legitimate.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ for Bun; a built `dist` (`.mjs` + `.d.mts`) serves Node/bundler consumers.
| [modules.md](./modules.md) | cache · freshness · refs · state · memory · statusline · util |
| [adapters.md](./adapters.md) | adapters + `harness init`/`hook` wiring |
| [design.md](./design.md) | design-agent pipeline — state machine, gates, opt-in Gemini |
| [prd.md](./prd.md) | PRD task/agent ownership coordination — opt-in, `FUSE_PRD=1` |

Generated API reference: `bun run docs:api` → `docs/api/`.
See also: [CHANGELOG](../CHANGELOG.md) · [CONTRIBUTING](../CONTRIBUTING.md) · [README](../README.md).
Expand Down
Loading