gcf-harness-aif-ext 0.9.1 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,105 +1,118 @@
1
- # gcf-harness-aif-ext
2
-
3
- Claude-Code-specific orchestration for the [Game Context Framework](../gcf-aif-ext). Optional: GCF works without it, by hand.
4
-
5
- > **Claude Code only.** Codex and the other AI Factory runtimes are not supported: the harness is built on Claude Code
6
- > dynamic workflows and `PreToolUse` hooks, which they do not have. AI Factory still copies the `gcf-run` skills into
7
- > every runtime the project is configured for, where they do not work — on a project without Claude Code, do not
8
- > install the harness. GCF itself keeps working on any runtime.
9
-
10
- The harness turns policy-as-text into an executable pipeline. It has **no memory of its own** — everything it knows between two agent calls is on disk in `gcf/`, so a run killed at any point restarts and reaches the same state.
11
-
12
- ## Requirements
13
-
14
- - [`gcf-aif-ext`](../gcf-aif-ext) `^0.7.0` installed, with a `gcf/` tree in the project — declared as a peer dependency, and `gcf-harness init` refuses when the `gcf` command is not registered
15
- - Claude Code 2.1.270+ (hooks, dynamic workflows) — the only supported runtime
16
- - AI Factory 2.x, Node.js ≥ 22.14
17
-
18
- ## Install
19
-
20
- The shortest way is from the core: `ai-factory gcf init` asks whether to install the harness too, and on yes runs both steps below. It asks only in a terminal and only when the project has the Claude Code agent; `gcf init --harness` installs it without asking, `--no-harness` skips the question.
21
-
22
- By hand, into a project that already has the core, from npm or a local checkout:
23
-
24
- ```bash
25
- ai-factory extension add gcf-aif-ext # the core, if not yet installed
26
- ai-factory extension add gcf-harness-aif-ext # npm
27
- # ai-factory extension add ../GCF/packages/gcf-harness-aif-ext # local checkout
28
- ```
29
-
30
- This installs the `gcf-run` and `gcf-run-ingest` skills and registers the `gcf-harness` command. Then initialize the harness in the project — like `gcf init`, once per project:
31
-
32
- ```bash
33
- ai-factory gcf-harness init # per project: .claude/settings.json + .gitignore
34
- ```
35
-
36
- It is **per project**, not global — run it again in every game repository: it adds the readset-guard `PreToolUse` entry to the project's `.claude/settings.json` (never `~/.claude`) and `.gcf-session/` to `.gitignore`. It also allows what an unattended `claude -p` run needs at least — `Edit`, `Write`, `Bash(git *)`, `Bash(ai-factory *)`, and the same two as `PowerShell(…)` rules, the tool the agent runs commands with on Windows — because print mode cannot ask and denies the rest; add your engine's tools yourself (for example `mcp__<editor-server>` for an editor driven over MCP). Commit `.claude/settings.json`: the rules apply to every Claude session in the project. It is idempotent — a re-run after moving or cloning the project repoints the hook at the new path — refuses when the `gcf` command is not registered, and refuses to rewrite a `settings.json` it cannot parse. It is a separate step for the same reason as `gcf hooks-install`: an AI Factory manifest cannot install Claude Code hooks.
37
-
38
- ## Upgrade
39
-
40
- ```bash
41
- ai-factory extension update gcf-harness-aif-ext
42
- ```
43
-
44
- Updating keeps the hook entry valid — it points into the extension's install directory, which an update refreshes in place.
45
-
46
- 0.5.0 drives work items (`gcf/work/<id>.md`, schema 3) instead of cards and requires `gcf-aif-ext` 0.4.0 — update both together; against an older core the gate refuses with `gcf-aif-ext >= 0.4.0 is required`. A project still holding schema 2 cards in `gcf/cards/` re-creates them first (see the core's "Upgrading from 0.3.x"). What changes for you: `/gcf-run` takes ids of items in `ready`; it no longer stops at `built` but ends each item with `submit`, `close` or `block`; the plan is improved in passes until it converges; the WIP cap counts only items that end waiting for a verdict.
47
-
48
- 0.5.1 is a fix release. During a run, `gcf/config.yml` is always readable, and a Grep inside `gcf/` reads only readset files. An import breaks a `depends:` cycle the pair checks closed and asks a D1 about it. It cuts a section longer than a segment at paragraphs, and lens and pair prompts carry a window around the quote. `/gcf-run` checks the plan against each pillar's invariant and asks each pillar's review question over the diff. Use it with core 0.4.1, which brings the parent feature into the readset.
49
-
50
- 0.9.1 is a fix release:
51
- - A stopped session reads as `no result (stopped)`, and its kept record as `ended: stopped`: before, the kill's exit code (`agent exit 1` on Windows).
52
- - `gcf-harness stop <id>` never kills the run: a stop before the item's session starts (or between two sessions of a handoff) leaves `.gcf-session/runs/<id>.stop`, and that session is stopped as it starts. Before, a stop then killed the whole run with the other items of its batch. A run hung before its first session (in its worktree setup) is not stopped. On main, a stop marks the item too once the run has reached it. Known limit: a stop in the moment between a worktree run's claim of the item and its clearing of an old mark is lost.
53
- - A stop that comes while the session starts no longer misses it.
54
-
55
- 0.9.0, to update together with core 0.7.6:
56
- - The harness's own agent runs with `--permission-mode bypassPermissions`: it may do everything but the project's deny rules, which hold in every mode. The rules come from the deny groups of `gcf/config.yml` (`ai-factory gcf permissions`, or the board): git history and uncommitted work, recursive delete, publishing, the human's GCF moves and the guards are on by default; secrets, changes outside the project and killing processes are off. `git push` is allowed. Deny rules match the command as written: they catch accidents, not a rewording, and `/gcf-run` makes a refused command a stop with `needs permission: <command>`.
57
- - A run loads the project's settings and MCP servers only (`--setting-sources project,local --strict-mcp-config --mcp-config .mcp.json`): the operator's plugins, hooks and claude.ai connectors stay out. So does `env` or `apiKeyHelper` in `~/.claude/settings.json`: an operator who logs in through them, not `claude login`, names the agent with `--agent`. The deny rules and the handoff guard come in `.gcf-session/runs/agent-settings.json` (`--settings`), so a worktree gets them without a commit.
58
- - Context handoff: past `context_handoff_pct` (60 by default) of the window, or at a compaction, the run flags the session; the handoff guard then denies every call of the main agent but the plan's read and edit with the instruction to write `## Handoff` into the plan and end. The run starts a fresh session with `/gcf-run <id> --handoff`, which goes on from the plan: the ticked tasks stay done. A session that works on past its flag is stopped; after 3 handoffs in a row the item is blocked with `context handoff limit`. The window is `context_window`, else the one a result of the same model reported (`.gcf-session/runs/windows.json`); with neither, the first session of a model runs without a handoff and says so. The sessions of an item share one timeout, and their costs add up; `--json` gives `handoffs`.
59
- - `auto_compact_pct` sets the agent's `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE`, at least 15 points above the handoff share.
60
- - The readset-guard reads the manifest of the directory the session started in (`CLAUDE_PROJECT_DIR`): an agent's `cd` no longer lifts the readset.
61
- - `/gcf-run` writes a plan task's code through aif-implement, so the plan ticks show the item's progress, and answers aif-implement's questions itself: no commit, stash or merge.
62
-
63
- 0.8.0, to update together with core 0.7.0:
64
- - `run_mode: main` in `gcf/config.yml` (or `run --mode main`): items run one after another in the main checkout, for an agent that works through an editor bound to the project folder. The item carries the core's session mark (`gcf claim`), not a run lock, so the agent records its own move; one run works the checkout at a time (`.gcf-session/runs/.main-run.lock`).
65
- - `gcf-harness next [N] --json`: the ready items that can run together now, by priority; each is taken while the batch with it passes the gate.
66
- - `gcf-harness stop <id>`: stops the session that works the item (its run lock names it); the run records no result for it and goes on with the rest. The item stays ready and its work is kept. Known limit: the session is killed at once, so a `gcf` move it was in the middle of may leave `.gcf-session/main.lock` behind (taken over once its pid is gone).
67
- - A run on main refreshes its item's mark every 5 minutes, and the readset-guard hook refreshes live marks on each read: a long build without a gcf command does not expire it.
68
- - `run` refuses an id that is not a slug before it names a lock file or builds the agent's command line.
69
- - The gate lets a session through for the item its own run marked (`GCF_RUN_ITEM`); `/gcf-run` outside a worktree claims each item after its gate and releases one it leaves without a move.
70
-
71
- 0.7.0, to update together with core 0.6.0: an item's `model:` and `effort:` (`gcf item new --model <m> --effort <e>`) go to its agent as `--model <m> --effort <e>`, read from `gcf board --json`; a value that is not one plain word fails that item and starts no session. `--timeout <minutes>` (120 by default) stops a session that runs longer, with every process it started; its item stays ready, and its run lock is freed. The gate counts the items other runs drive now: their places under the WIP limit, and their systems — a board starts one run per click. Requires `gcf-aif-ext` 0.6.
72
-
73
- 0.6.1 is a fix release, to update together with core 0.5.3. The run lock `.gcf-session/runs/<id>.lock` is created exclusively, and a lock left by a dead run is taken over; while it is live, the gate refuses the item to another run and the core refuses any move on it, `/gcf-run` on main included. A session whose log cannot open ends as an error and releases its claim.
74
-
75
- 0.6.0 adds `ai-factory gcf-harness run` (a batch at once, a worktree and an agent session per item, then the integrator) and the `--worktree` mode of `/gcf-run`, whose session ritual now integrates the verified branches a dead run left. It requires `gcf-aif-ext` 0.5.0 (`gcf worktree`, `gcf integrate`, `gcf verify`) — update both together and run `ai-factory gcf init`. The DoD of `/gcf-run` is `ai-factory gcf verify <id>`, and the item's `live:` delta is written before it.
76
-
77
- 0.4.1 is a patch release. `/gcf-run` now commits the implementation first, then flips the card to `built` in a second commit, with `built_at` set to the implementation commit's hash. Update the core to 0.3.1 together with the harness: without it, an audit run before a verdict makes the pre-commit gate refuse that verdict.
78
-
79
- To remove the extension:
80
-
81
- ```bash
82
- ai-factory extension remove gcf-harness-aif-ext
83
- ```
84
-
85
- Removal does not touch `.claude/settings.json`: delete the `PreToolUse` entry whose command ends in `# gcf-readset-guard` by hand, or every Read, Grep and Glob will report a hook error.
86
-
87
- ## What it adds
88
-
89
- **`/gcf-run <item-id…>`** — drives work items through plan, implementation and DoD, with every gate checked *before* the tokens are spent: the audit ritual, the gate below, the context budget. The plan is written by `aif-plan`, then improved in passes, each by a fresh subagent that reads only the plan and the readset and records every default it picks; `ai-factory gcf plan check <id>` runs after each pass, and a pass that changes no task ends the loop. A plan still changing after `plan_passes` passes, or a question only the human can answer (a save format, a public API, a new dependency, a pillar conflict), blocks the item before any code is written. The DoD is `ai-factory gcf verify <id>` — the gate's checks, the item's `live:` delta, and the project's `dod.build` and `dod.test` commands from `gcf/config.yml` — plus a review by a fresh subagent, up to `dod_attempts` attempts. Every item ends in a state the core records: `gcf submit` (gate `verdict` or `decision` — waits for the human), `gcf close` (gate `auto` — done, or a soft unit closed as `unplayed`) or `gcf block` with the reason. The verdict and the decision belong to the human: the pipeline stops at `submit` and prints the playtest list.
90
-
91
- **`ai-factory gcf-harness gate <item-id…>`** — the `/gcf-run` gate as code. It reads one `gcf audit --json` report and exits 1 with a one-line reason when the items may not go: a core older than 0.4.0, a `schema:` other than 1 in `gcf/config.yml`, an id with no work item, a feature (run its children; the feature closes when they are finished), an item that is not `ready`, an unplayed dependency, no WIP room for the batch (items awaiting a verdict + the batch's `gate: verdict` items over `wip_built_limit`), or two items of the batch touching the same system unless both say `parallel: ok`. The tune limit is not a gate rule: `verdict tune` itself refuses at the limit, and an item tuned up to it still gets its last build. The skill runs it and stops on a non-zero exit — no model compares the numbers.
92
-
93
- **`ai-factory gcf-harness run <item-id…> [--agent "<cmd>"]`** — runs a batch at once, each item in its own worktree (`gcf worktree <id>`) with its own agent session: `claude -p --output-format stream-json --verbose "/gcf-run <id> --worktree"` by default, or the command in `--agent` / `GCF_RUN_AGENT`. The sessions run unattended, so set the agent's permissions for print mode first. A session ends with a commit and `gcf verify <id> --mark` (the `GCF-Verified` trailer); a question for the human goes into `.gcf-session/outcome.json` in its worktree, and `run` records the block on main. Then `gcf integrate` merges the verified branches and records each move, and `run` prints the playtest session. Its first step, like `/gcf-run`'s, integrates the verified branches a dead run left. A session that ended with no result leaves its item `ready`; the next `run` resumes its worktree and the agent's own session (`--resume <session_id>`, kept in `.gcf-session/runs/<id>.agent.json`), in either run mode; `--fresh` starts a new session instead. A session cut off by Claude's usage limit reports `paused (usage limit, resets 3am (…))` and `limit_resets`. Logs: `.gcf-session/runs/<id>.log`, written as the session works: a `start:` line, then one stamped line per agent message, tool call (`tool: Edit <file>`, `tool: Bash <command>`) and failed tool result, and at the end the agent's final text and `done in 34m, 212 turns, $3.41 (…tokens)`; stderr goes in as it is, and a non-Claude `--agent` keeps its own lines. With `--json`, each item carries the session's `cost_usd`, `tokens`, `turns` and `duration_ms` when the agent reported them. One run drives an item at a time (`.gcf-session/runs/<id>.lock`), and a resumed worktree first gets main merged in, so the answer from an `unblock` reaches the session. A worktree sees only what is committed: keep `.ai-factory.json`, the installed extensions and `.claude/` (skills, the readset-guard hook) in git, or the session has no `/gcf-run` and no `ai-factory gcf`.
94
-
95
- **`/gcf-run-ingest <gdd.md>`** — imports a design document as a fan-out workflow: parallel aspect extraction, provenance verified by code rather than by an agent (a quote either is a verbatim substring of its section or it is not), completeness lenses per candidate, and an interaction matrix computed from the candidates that write the same state.
96
-
97
- **readset-guard** — a `PreToolUse` hook on Read/Grep/Glob. While a run manifest exists at `.gcf-session/readset.json`, a path inside `gcf/` that is not in the readset is denied, and so is a Grep from the project root (no `path`, or a `path` above `gcf/`) unless its `glob` starts with a literal directory other than `gcf` (`src/**`, not `**/*.md`), because such a content search would read the whole tree past the gate. Glob and Greps scoped to the game code are never restricted. No manifest means no restriction: manual work is never gated. An honest limit — `cat` through Bash still bypasses it. This is a discipline tool, not a security perimeter.
98
-
99
- ## Docs
100
-
101
- [Harness-PRD](https://github.com/VirtualMaestro/GCF/blob/main/docs/Harness-PRD.md) is the spec: pipeline stages, gates, execution surfaces, the guard's contract.
102
-
103
- ## License
104
-
105
- MIT
1
+ # gcf-harness-aif-ext
2
+
3
+ Claude-Code-specific orchestration for the [Game Context Framework](../gcf-aif-ext). Optional: GCF works without it, by hand.
4
+
5
+ > **Claude Code only.** Codex and the other AI Factory runtimes are not supported: the harness is built on Claude Code
6
+ > dynamic workflows and `PreToolUse` hooks, which they do not have. AI Factory still copies the `gcf-run` skills into
7
+ > every runtime the project is configured for, where they do not work — on a project without Claude Code, do not
8
+ > install the harness. GCF itself keeps working on any runtime.
9
+
10
+ The harness turns policy-as-text into an executable pipeline. It has **no memory of its own** — everything it knows between two agent calls is on disk in `gcf/`, so a run killed at any point restarts and reaches the same state.
11
+
12
+ ## Requirements
13
+
14
+ - [`gcf-aif-ext`](../gcf-aif-ext) `^0.7.0` installed, with a `gcf/` tree in the project — declared as a peer dependency, and `gcf-harness init` refuses when the `gcf` command is not registered
15
+ - Claude Code 2.1.270+ (hooks, dynamic workflows) — the only supported runtime
16
+ - AI Factory 2.x, Node.js ≥ 22.14
17
+
18
+ ## Install
19
+
20
+ The shortest way is from the core: `ai-factory gcf init` asks whether to install the harness too, and on yes runs both steps below. It asks only in a terminal and only when the project has the Claude Code agent; `gcf init --harness` installs it without asking, `--no-harness` skips the question.
21
+
22
+ By hand, into a project that already has the core, from npm or a local checkout:
23
+
24
+ ```bash
25
+ ai-factory extension add gcf-aif-ext # the core, if not yet installed
26
+ ai-factory extension add gcf-harness-aif-ext # npm
27
+ # ai-factory extension add ../GCF/packages/gcf-harness-aif-ext # local checkout
28
+ ```
29
+
30
+ This installs the `gcf-run` and `gcf-run-ingest` skills and registers the `gcf-harness` command. Then initialize the harness in the project — like `gcf init`, once per project:
31
+
32
+ ```bash
33
+ ai-factory gcf-harness init # per project: .claude/settings.json + .gitignore
34
+ ```
35
+
36
+ It is **per project**, not global — run it again in every game repository: it adds the readset-guard and implement-guard entries to the project's `.claude/settings.json` (never `~/.claude`) and `.gcf-session/` to `.gitignore`. It also allows what an unattended `claude -p` run needs at least — `Edit`, `Write`, `Bash(git *)`, `Bash(ai-factory *)`, and the same two as `PowerShell(…)` rules, the tool the agent runs commands with on Windows — because print mode cannot ask and denies the rest; add your engine's tools yourself (for example `mcp__<editor-server>` for an editor driven over MCP). Commit `.claude/settings.json`: the rules apply to every Claude session in the project. It is idempotent — a re-run after moving or cloning the project repoints the hook at the new path — refuses when the `gcf` command is not registered, and refuses to rewrite a `settings.json` it cannot parse. It is a separate step for the same reason as `gcf hooks-install`: an AI Factory manifest cannot install Claude Code hooks.
37
+
38
+ ## Upgrade
39
+
40
+ ```bash
41
+ ai-factory extension update gcf-harness-aif-ext
42
+ ```
43
+
44
+ Updating keeps the hook entry valid — it points into the extension's install directory, which an update refreshes in place.
45
+
46
+ 0.5.0 drives work items (`gcf/work/<id>.md`, schema 3) instead of cards and requires `gcf-aif-ext` 0.4.0 — update both together; against an older core the gate refuses with `gcf-aif-ext >= 0.4.0 is required`. A project still holding schema 2 cards in `gcf/cards/` re-creates them first (see the core's "Upgrading from 0.3.x"). What changes for you: `/gcf-run` takes ids of items in `ready`; it no longer stops at `built` but ends each item with `submit`, `close` or `block`; the plan is improved in passes until it converges; the WIP cap counts only items that end waiting for a verdict.
47
+
48
+ 0.5.1 is a fix release. During a run, `gcf/config.yml` is always readable, and a Grep inside `gcf/` reads only readset files. An import breaks a `depends:` cycle the pair checks closed and asks a D1 about it. It cuts a section longer than a segment at paragraphs, and lens and pair prompts carry a window around the quote. `/gcf-run` checks the plan against each pillar's invariant and asks each pillar's review question over the diff. Use it with core 0.4.1, which brings the parent feature into the readset.
49
+
50
+ 0.9.4 keeps run metrics (Harness-PRD §11) and adds `ai-factory gcf-harness metrics`. Use it with core 0.7.13, which logs the plan checks and the DoD attempts, and refuses `close` and `submit` without a green `gcf verify` of the code as it is. `/gcf-run` runs the verify again after a review fix; implement-guard refuses `gcf close` or `gcf submit` with `--unverified` in a `/gcf-run` session, since a move past the DoD gate is the human's call. A main-mode item whose move went past it anyway (the commit says `(unverified: …)`) reads `— moved without a green gcf verify` on its report line, and `gcf-harness metrics` counts such moves.
51
+
52
+ 0.9.2 holds `/gcf-run` to aif-implement. Run `ai-factory gcf-harness init` again after the update: it registers the new hook.
53
+ - The implement guard: in a session that runs `/gcf-run`, a write to a code file is denied until the session invokes the aif-implement skill (gcf-run step 13), and so is a plan tick, which only aif-implement sets. Planning stays free: `gcf/`, `.ai-factory/`, `.gcf-session/` and any markdown file. After aif-implement every write is free, the fixes of a red DoD and of the review included. It works in the harness's runs and in an interactive `/gcf-run` alike, and touches no other session.
54
+ - `/gcf-run` converges a plan when an improve pass finds nothing blocking: a wrong or missing behavior or task, a broken pillar invariant, a rule conflict, a task no check can prove. Polish (wording, splitting tasks, order) is applied, but no longer keeps the loop going. Before, a pass that changed any task asked for one more, and a fresh reviewer almost always changes something. The report row per pass gives the blocking and polish counts.
55
+ - `/gcf-run` answers aif-plan's own questions (tests, logging, docs) itself, from the item, its DoD and the project's rules.
56
+ - The run report marks an item whose session wrote code without aif-implement: `— code written outside aif-implement` on its line, `process` in `--json` and in its kept record. It reads the session's own events, its subagents' included, so it also covers a project without the guard registered. A write through Bash is seen by neither: the review of the diff is the check there.
57
+ - Lessons (with core 0.7.10): after each agent session the run asks the same session, resumed, what got in its way - tools, rules, the framework, the project template - with the questions of `ai-factory gcf lessons prompt`. Its answers go to the inbox on main as lessons (`gcf lessons add`), and the board shows them on the card, in the drawer and in the inbox. The run passes `--no-lessons` to `/gcf-run`, so the agent carries no lessons instruction while it works. The turn follows a result, a handoff (before the fresh session), a timeout or an error; not a usage limit or a stop, and only for a `claude` agent with a session id. Another agent asks its lessons itself at the end of a run on main; in a worktree it records none. It takes up to 10 minutes; its cost goes on the item, and the report line says `, N lessons`. A failed lessons turn is logged and changes nothing else. A project that turns lessons off, or a core before 0.7.10, gets no turn and no flag.
58
+
59
+ 0.9.1 is a fix release:
60
+ - A stopped session reads as `no result (stopped)`, and its kept record as `ended: stopped`: before, the kill's exit code (`agent exit 1` on Windows).
61
+ - `gcf-harness stop <id>` never kills the run: a stop before the item's session starts (or between two sessions of a handoff) leaves `.gcf-session/runs/<id>.stop`, and that session is stopped as it starts. Before, a stop then killed the whole run with the other items of its batch. A run hung before its first session (in its worktree setup) is not stopped. On main, a stop marks the item too once the run has reached it. Known limit: a stop in the moment between a worktree run's claim of the item and its clearing of an old mark is lost.
62
+ - A stop that comes while the session starts no longer misses it.
63
+
64
+ 0.9.0, to update together with core 0.7.6:
65
+ - The harness's own agent runs with `--permission-mode bypassPermissions`: it may do everything but the project's deny rules, which hold in every mode. The rules come from the deny groups of `gcf/config.yml` (`ai-factory gcf permissions`, or the board): git history and uncommitted work, recursive delete, publishing, the human's GCF moves and the guards are on by default; secrets, changes outside the project and killing processes are off. `git push` is allowed. Deny rules match the command as written: they catch accidents, not a rewording, and `/gcf-run` makes a refused command a stop with `needs permission: <command>`.
66
+ - A run loads the project's settings and MCP servers only (`--setting-sources project,local --strict-mcp-config --mcp-config .mcp.json`): the operator's plugins, hooks and claude.ai connectors stay out. So does `env` or `apiKeyHelper` in `~/.claude/settings.json`: an operator who logs in through them, not `claude login`, names the agent with `--agent`. The deny rules and the handoff guard come in `.gcf-session/runs/agent-settings.json` (`--settings`), so a worktree gets them without a commit.
67
+ - Context handoff: past `context_handoff_pct` (60 by default) of the window, or at a compaction, the run flags the session; the handoff guard then denies every call of the main agent but the plan's read and edit with the instruction to write `## Handoff` into the plan and end. The run starts a fresh session with `/gcf-run <id> --handoff`, which goes on from the plan: the ticked tasks stay done. A session that works on past its flag is stopped; after 3 handoffs in a row the item is blocked with `context handoff limit`. The window is `context_window`, else the one a result of the same model reported (`.gcf-session/runs/windows.json`); with neither, the first session of a model runs without a handoff and says so. The sessions of an item share one timeout, and their costs add up; `--json` gives `handoffs`.
68
+ - `auto_compact_pct` sets the agent's `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE`, at least 15 points above the handoff share.
69
+ - The readset-guard reads the manifest of the directory the session started in (`CLAUDE_PROJECT_DIR`): an agent's `cd` no longer lifts the readset.
70
+ - `/gcf-run` writes a plan task's code through aif-implement, so the plan ticks show the item's progress, and answers aif-implement's questions itself: no commit, stash or merge.
71
+
72
+ 0.8.0, to update together with core 0.7.0:
73
+ - `run_mode: main` in `gcf/config.yml` (or `run --mode main`): items run one after another in the main checkout, for an agent that works through an editor bound to the project folder. The item carries the core's session mark (`gcf claim`), not a run lock, so the agent records its own move; one run works the checkout at a time (`.gcf-session/runs/.main-run.lock`).
74
+ - `gcf-harness next [N] --json`: the ready items that can run together now, by priority; each is taken while the batch with it passes the gate.
75
+ - `gcf-harness stop <id>`: stops the session that works the item (its run lock names it); the run records no result for it and goes on with the rest. The item stays ready and its work is kept. Known limit: the session is killed at once, so a `gcf` move it was in the middle of may leave `.gcf-session/main.lock` behind (taken over once its pid is gone).
76
+ - A run on main refreshes its item's mark every 5 minutes, and the readset-guard hook refreshes live marks on each read: a long build without a gcf command does not expire it.
77
+ - `run` refuses an id that is not a slug before it names a lock file or builds the agent's command line.
78
+ - The gate lets a session through for the item its own run marked (`GCF_RUN_ITEM`); `/gcf-run` outside a worktree claims each item after its gate and releases one it leaves without a move.
79
+
80
+ 0.7.0, to update together with core 0.6.0: an item's `model:` and `effort:` (`gcf item new --model <m> --effort <e>`) go to its agent as `--model <m> --effort <e>`, read from `gcf board --json`; a value that is not one plain word fails that item and starts no session. `--timeout <minutes>` (120 by default) stops a session that runs longer, with every process it started; its item stays ready, and its run lock is freed. The gate counts the items other runs drive now: their places under the WIP limit, and their systems — a board starts one run per click. Requires `gcf-aif-ext` 0.6.
81
+
82
+ 0.6.1 is a fix release, to update together with core 0.5.3. The run lock `.gcf-session/runs/<id>.lock` is created exclusively, and a lock left by a dead run is taken over; while it is live, the gate refuses the item to another run and the core refuses any move on it, `/gcf-run` on main included. A session whose log cannot open ends as an error and releases its claim.
83
+
84
+ 0.6.0 adds `ai-factory gcf-harness run` (a batch at once, a worktree and an agent session per item, then the integrator) and the `--worktree` mode of `/gcf-run`, whose session ritual now integrates the verified branches a dead run left. It requires `gcf-aif-ext` 0.5.0 (`gcf worktree`, `gcf integrate`, `gcf verify`) — update both together and run `ai-factory gcf init`. The DoD of `/gcf-run` is `ai-factory gcf verify <id>`, and the item's `live:` delta is written before it.
85
+
86
+ 0.4.1 is a patch release. `/gcf-run` now commits the implementation first, then flips the card to `built` in a second commit, with `built_at` set to the implementation commit's hash. Update the core to 0.3.1 together with the harness: without it, an audit run before a verdict makes the pre-commit gate refuse that verdict.
87
+
88
+ To remove the extension:
89
+
90
+ ```bash
91
+ ai-factory extension remove gcf-harness-aif-ext
92
+ ```
93
+
94
+ Removal does not touch `.claude/settings.json`: delete the entries whose command ends in `# gcf-readset-guard` or `# gcf-implement-guard` (under `PreToolUse` and `UserPromptSubmit`) by hand, or the tools they match will report a hook error.
95
+
96
+ ## What it adds
97
+
98
+ **`/gcf-run <item-id…>`** — drives work items through plan, implementation and DoD, with every gate checked *before* the tokens are spent: the audit ritual, the gate below, the context budget. The plan is written by `aif-plan`, then improved in passes, each by a fresh subagent that reads only the plan and the readset and records every default it picks; `ai-factory gcf plan check <id>` runs after each pass, and a pass with no blocking finding (a wrong or missing behavior or task, a broken pillar invariant, a rule conflict, an unprovable task) ends the loop. A blocking finding left after `plan_passes` passes (3 by default), or a question only the human can answer (a save format, a public API, a new dependency, a pillar conflict), blocks the item before any code is written. The DoD is `ai-factory gcf verify <id>` — the gate's checks, the item's `live:` delta, and the project's `dod.build` and `dod.test` commands from `gcf/config.yml` — plus a review by a fresh subagent, up to `dod_attempts` attempts. Every item ends in a state the core records: `gcf submit` (gate `verdict` or `decision` — waits for the human), `gcf close` (gate `auto` — done, or a soft unit closed as `unplayed`) or `gcf block` with the reason. The verdict and the decision belong to the human: the pipeline stops at `submit` and prints the playtest list.
99
+
100
+ **`ai-factory gcf-harness gate <item-id…>`** — the `/gcf-run` gate as code. It reads one `gcf audit --json` report and exits 1 with a one-line reason when the items may not go: a core older than 0.4.0, a `schema:` other than 1 in `gcf/config.yml`, an id with no work item, a feature (run its children; the feature closes when they are finished), an item that is not `ready`, an unplayed dependency, no WIP room for the batch (items awaiting a verdict + the batch's `gate: verdict` items over `wip_built_limit`), or two items of the batch touching the same system unless both say `parallel: ok`. The tune limit is not a gate rule: `verdict tune` itself refuses at the limit, and an item tuned up to it still gets its last build. The skill runs it and stops on a non-zero exit — no model compares the numbers.
101
+
102
+ **`ai-factory gcf-harness run <item-id…> [--agent "<cmd>"]`** — runs a batch at once, each item in its own worktree (`gcf worktree <id>`) with its own agent session: `claude -p --output-format stream-json --verbose "/gcf-run <id> --worktree"` by default, or the command in `--agent` / `GCF_RUN_AGENT`. The sessions run unattended, so set the agent's permissions for print mode first. A session ends with a commit and `gcf verify <id> --mark` (the `GCF-Verified` trailer); a question for the human goes into `.gcf-session/outcome.json` in its worktree, and `run` records the block on main. Then `gcf integrate` merges the verified branches and records each move, and `run` prints the playtest session. Its first step, like `/gcf-run`'s, integrates the verified branches a dead run left. A session that ended with no result leaves its item `ready`; the next `run` resumes its worktree and the agent's own session (`--resume <session_id>`, kept in `.gcf-session/runs/<id>.agent.json`), in either run mode; `--fresh` starts a new session instead. A session cut off by Claude's usage limit reports `paused (usage limit, resets 3am (…))` and `limit_resets`. Logs: `.gcf-session/runs/<id>.log`, written as the session works: a `start:` line, then one stamped line per agent message, tool call (`tool: Edit <file>`, `tool: Bash <command>`) and failed tool result, and at the end the agent's final text and `done in 34m, 212 turns, $3.41 (…tokens)`; stderr goes in as it is, and a non-Claude `--agent` keeps its own lines. With `--json`, each item carries the session's `cost_usd`, `tokens`, `turns` and `duration_ms` when the agent reported them. One run drives an item at a time (`.gcf-session/runs/<id>.lock`), and a resumed worktree first gets main merged in, so the answer from an `unblock` reaches the session. A worktree sees only what is committed: keep `.ai-factory.json`, the installed extensions and `.claude/` (skills, the readset-guard hook) in git, or the session has no `/gcf-run` and no `ai-factory gcf`.
103
+
104
+ **`ai-factory gcf-harness metrics [--since <date>] [--json]`** — the run metrics of Harness-PRD §11 from `gcf/.metrics`: how many item runs reached `submit` or `close` (a run the usage limit paused or a session cut short does not count as finished), the block reasons, what the runs cost (dollars, minutes, turns, handoffs, code written outside aif-implement), the plan checks per item (the improve passes), the DoD attempts per item and the checks that failed them, and the guard firings by hook and by item. The logs: `run-log.csv` (one line per item of each `run`), `guard-log.csv` (each denial of readset-guard and implement-guard, by hook: `readset`, `implement`, or `verify` for a move past the verify gate), and the core's `plan-log.csv` and `verify-log.csv`. A worktree session's lines go to the main checkout's logs; the next `gcf integrate` commits them.
105
+
106
+ **`/gcf-run-ingest <gdd.md>`** — imports a design document as a fan-out workflow: parallel aspect extraction, provenance verified by code rather than by an agent (a quote either is a verbatim substring of its section or it is not), completeness lenses per candidate, and an interaction matrix computed from the candidates that write the same state.
107
+
108
+ **readset-guard** — a `PreToolUse` hook on Read/Grep/Glob. While a run manifest exists at `.gcf-session/readset.json`, a path inside `gcf/` that is not in the readset is denied, and so is a Grep from the project root (no `path`, or a `path` above `gcf/`) unless its `glob` starts with a literal directory other than `gcf` (`src/**`, not `**/*.md`), because such a content search would read the whole tree past the gate. Glob and Greps scoped to the game code are never restricted. No manifest means no restriction: manual work is never gated. An honest limit — `cat` through Bash still bypasses it. This is a discipline tool, not a security perimeter.
109
+
110
+ **implement-guard** — a `UserPromptSubmit` and `PreToolUse` hook (Skill, the write tools, Bash and PowerShell). A session that starts `/gcf-run` (typed, passed to `claude -p`, or invoked as a skill) gets a state in `.gcf-session/guard/<session_id>.json`; until it invokes the aif-implement skill, Edit, Write, MultiEdit and NotebookEdit are denied outside `gcf/`, `.ai-factory/`, `.gcf-session/` and markdown, and a markdown write that adds a `- [x]` tick is denied too. Any other session is never touched, and an error lets the call through. A `gcf submit` or `gcf close` with `--unverified` is refused in such a session, after its end move too (a close the core refused does not free it for a skip): only the human moves an item past the DoD gate, from a terminal of their own. Such a refusal is logged with the hook name `verify`. The move that ends the item (`gcf block`, `submit` or `close`, through Bash or PowerShell) lets the session go, so the operator's own edits after a blocked run are free again. A plugin's skill (`<plugin>:aif-implement`) counts too. Limits: the flag is per session, so in a batch the first item's aif-implement frees the rest; a write through Bash is not seen, by the run report either.
111
+
112
+ ## Docs
113
+
114
+ [Harness-PRD](https://github.com/VirtualMaestro/GCF/blob/main/docs/Harness-PRD.md) is the spec: pipeline stages, gates, execution surfaces, the guard's contract.
115
+
116
+ ## License
117
+
118
+ MIT
@@ -109,10 +109,13 @@ function limitIn(text, whole = true) {
109
109
  * @param {string} line
110
110
  * The main agent's turns also carry `context`, the tokens its window holds (a subagent's turns,
111
111
  * `parent_tool_use_id` set, fill a window of their own); the init names the `model`, the result
112
- * the `windows` of its models, and a compaction says `compacted`.
112
+ * the `windows` of its models, and a compaction says `compacted`. A turn's tool calls give
113
+ * `implement` (the aif-implement skill) and `writes` (the files written, by call id); a tool
114
+ * result gives `failed`, the calls that wrote nothing.
113
115
  * @returns {{lines: string[], cost?: Cost, session_id?: string, limit?: {resets: string | null},
114
116
  * context?: number, turn?: string, model?: string, windows?: Record<string, number>,
115
- * compacted?: boolean}}
117
+ * compacted?: boolean, implement?: boolean, writes?: {id: string, file: string}[],
118
+ * failed?: string[]}}
116
119
  */
117
120
  export function activity(line, now = new Date()) {
118
121
  let e;
@@ -128,12 +131,18 @@ export function activity(line, now = new Date()) {
128
131
  const content = Array.isArray(e.message?.content) ? e.message.content : [];
129
132
  if (e.type === 'system') return system(e, at);
130
133
  if (e.type === 'assistant') return { ...said(content, at), ...contextOf(e) };
131
- if (e.type === 'user')
134
+ if (e.type === 'user') {
135
+ const errors = content.filter((b) => b.type === 'tool_result' && b.is_error);
136
+
132
137
  return {
133
- lines: content
134
- .filter((b) => b.type === 'tool_result' && b.is_error)
135
- .map((b) => `${at} tool error: ${cut(firstLines(resultText(b.content), 1), 300)}`),
138
+ lines: errors.map(
139
+ (b) => `${at} tool error: ${cut(firstLines(resultText(b.content), 1), 300)}`,
140
+ ),
141
+ // A refused or failed call wrote nothing: its write does not count.
142
+ ...(errors.length ? { failed: errors.map((b) => String(b.tool_use_id)) } : {}),
136
143
  };
144
+ }
145
+
137
146
  if (e.type === 'result') return ended(e, at);
138
147
 
139
148
  return { lines: [] };
@@ -167,12 +176,32 @@ function contextOf(e) {
167
176
  return { context: tokens, ...(e.message.id ? { turn: String(e.message.id) } : {}) };
168
177
  }
169
178
 
179
+ const WRITES = ['Edit', 'Write', 'MultiEdit', 'NotebookEdit'];
180
+
181
+ /**
182
+ * What a turn's tool calls tell of gcf-run step 13: `implement` when one invokes the aif-implement
183
+ * skill, `writes` the files its write tools name. A subagent's turns count too.
184
+ */
185
+ function steps(uses) {
186
+ const writes = uses
187
+ .filter((u) => WRITES.includes(u.name))
188
+ .map((u) => ({ id: String(u.id), file: u.input?.file_path ?? u.input?.notebook_path }))
189
+ .filter((w) => typeof w.file === 'string');
190
+ // A plugin's skill comes as <plugin>:aif-implement.
191
+ const implement = uses.some(
192
+ (u) => u.name === 'Skill' && /(^|:)aif-implement$/.test(String(u.input?.skill ?? '')),
193
+ );
194
+
195
+ return { ...(implement ? { implement } : {}), ...(writes.length ? { writes } : {}) };
196
+ }
197
+
170
198
  /** An assistant turn: its tool calls and its text, which may be the usage-limit message. */
171
199
  function said(content, at) {
172
200
  const texts = content.filter((b) => b.type === 'text').map((b) => b.text);
173
201
 
174
202
  return {
175
203
  ...(texts.map((t) => limitIn(t)).find((l) => l.limit) ?? {}),
204
+ ...steps(content.filter((b) => b.type === 'tool_use')),
176
205
  lines: content.flatMap((b) => {
177
206
  if (b.type === 'tool_use') return [`${at} ${toolLine(b)}`];
178
207
  const text = b.type === 'text' ? cut(firstLines(b.text, 2), 300) : '';
@@ -197,6 +226,8 @@ function ended(e, at) {
197
226
 
198
227
  return {
199
228
  lines: [...text, `${at} ${summary(e, cost, Boolean(limit.limit))}`],
229
+ // The reply itself: a lessons turn answers with its JSON here.
230
+ ...(typeof e.result === 'string' ? { result: e.result } : {}),
200
231
  cost,
201
232
  ...limit,
202
233
  ...(windows.length ? { windows: Object.fromEntries(windows) } : {}),
@@ -1,4 +1,4 @@
1
- // `ai-factory gcf-harness init` — registers the readset-guard PreToolUse hook in the
1
+ // `ai-factory gcf-harness init` — registers the readset-guard and implement-guard hooks in the
2
2
  // project's .claude/settings.json (the AIF manifest cannot install Claude Code hooks —
3
3
  // same decision as gcf hooks-install for git hooks). Idempotent by marker comment. It also
4
4
  // allows what a headless agent run needs (AGENT_ALLOW).
@@ -8,10 +8,10 @@ import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
8
8
  import { spawnSync } from 'node:child_process';
9
9
  import path from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
+ import { metricsCommand } from './metrics.js';
11
12
  import { run, cliCore, DEFAULT_AGENT, DEFAULT_TIMEOUT_MIN, MAIN_RUN, stopOf } from './run.js';
12
13
 
13
14
  const EXT_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
14
- const MARKER = 'gcf-readset-guard';
15
15
  const SCHEMA = 1; // the only gcf/config.yml schema this harness drives
16
16
  const SESSION_RULE = /^(\/|\*\*\/)?\.gcf-session\/?$/;
17
17
 
@@ -185,20 +185,62 @@ async function readSettings(settingsFile) {
185
185
  }
186
186
  }
187
187
 
188
+ // The hooks `init` registers: the file, the event, its matcher (none for a prompt event).
189
+ /** @type {Array<{name: string, event: string, matcher?: string}>} */
190
+ const GUARDS = [
191
+ { name: 'readset-guard', event: 'PreToolUse', matcher: 'Read|Grep|Glob' },
192
+ {
193
+ name: 'implement-guard',
194
+ event: 'PreToolUse',
195
+ // Bash and PowerShell: a `gcf block|submit|close` ends the run, and the guard lets go.
196
+ matcher: 'Skill|Edit|Write|MultiEdit|NotebookEdit|Bash|PowerShell',
197
+ },
198
+ { name: 'implement-guard', event: 'UserPromptSubmit' },
199
+ ];
200
+
201
+ const hooksOf = (e) => (Array.isArray(e?.hooks) ? e.hooks : []);
202
+
203
+ /**
204
+ * A newer version may watch more tools. An entry of ours alone takes the new matcher; one we
205
+ * share with the operator's own hooks keeps theirs, and ours moves to an entry of its own, at
206
+ * most one: a hook copied into several entries, or one already moved, makes no second. Returns
207
+ * how many entries changed.
208
+ */
209
+ function widen(list, matcher, ours, command) {
210
+ const before = JSON.stringify(list);
211
+ const holding = list.filter((x) => hooksOf(x).some((h) => ours.includes(h)));
212
+ const alone = (e) => e.hooks.every((h) => ours.includes(h));
213
+ // Ours alone, the right matcher first; else a new one. It keeps one hook of ours, with the
214
+ // operator's own settings on it (a timeout).
215
+ let own = holding.find((e) => e.matcher === matcher && alone(e)) ?? holding.find(alone);
216
+ if (own) own.hooks = own.hooks.slice(0, 1);
217
+ else list.push((own = { hooks: [{ ...ours[0], command }] }));
218
+ own.matcher = matcher;
219
+ for (const e of holding) if (e !== own) e.hooks = e.hooks.filter((h) => !ours.includes(h));
220
+ // An entry that held only ours is empty now: it goes.
221
+ for (const e of holding) if (e !== own && !e.hooks.length) list.splice(list.indexOf(e), 1);
222
+
223
+ return JSON.stringify(list) === before ? 0 : 1;
224
+ }
225
+
188
226
  /**
189
- * Adds the guard entry, or repoints an existing one: a moved or cloned project keeps the old
190
- * absolute path under the marker, and the registered hook would keep launching a missing file.
191
- * Returns what happened, or null when the settings already hold the right command.
227
+ * Adds the guard entry of `event`, or repoints an existing one: a moved or cloned project keeps
228
+ * the old absolute path under the marker, and the registered hook would keep launching a missing
229
+ * file. Returns what happened, or null when the settings already hold the right command.
230
+ * @param {Record<string, any>} settings
231
+ * @param {{event: string, matcher?: string}} guard
232
+ * @param {string} marker
233
+ * @param {string} command
192
234
  */
193
- function registerGuard(settings, command) {
235
+ function registerGuard(settings, { event, matcher }, marker, command) {
194
236
  settings.hooks ??= {};
195
- settings.hooks.PreToolUse ??= [];
196
- const ours = settings.hooks.PreToolUse.flatMap((e) => e?.hooks ?? []).filter(
197
- (h) => typeof h?.command === 'string' && h.command.includes(MARKER),
198
- );
237
+ settings.hooks[event] ??= [];
238
+ const ours = settings.hooks[event]
239
+ .flatMap(hooksOf)
240
+ .filter((h) => typeof h?.command === 'string' && h.command.includes(marker));
199
241
  if (!ours.length) {
200
- settings.hooks.PreToolUse.push({
201
- matcher: 'Read|Grep|Glob',
242
+ settings.hooks[event].push({
243
+ ...(matcher ? { matcher } : {}),
202
244
  hooks: [{ type: 'command', command }],
203
245
  });
204
246
 
@@ -207,8 +249,9 @@ function registerGuard(settings, command) {
207
249
 
208
250
  const stale = ours.filter((h) => h.command !== command);
209
251
  for (const h of stale) h.command = command;
252
+ const widened = matcher ? widen(settings.hooks[event], matcher, ours, command) : 0;
210
253
 
211
- return stale.length ? 'repointed' : null;
254
+ return stale.length || widened ? 'repointed' : null;
212
255
  }
213
256
 
214
257
  // What a headless `claude -p` run needs at least: it cannot ask, so anything not allowed is
@@ -258,18 +301,22 @@ async function init(program) {
258
301
 
259
302
  const projectDir = process.cwd();
260
303
  const settingsFile = path.join(projectDir, '.claude', 'settings.json');
261
- const guard = path.join(EXT_ROOT, 'hooks', 'readset-guard.js').replaceAll('\\', '/');
262
304
  const settings = await readSettings(settingsFile);
263
- const change = registerGuard(settings, `node "${guard}" # ${MARKER}`);
305
+ const changes = GUARDS.map((g) => {
306
+ const file = path.join(EXT_ROOT, 'hooks', `${g.name}.js`).replaceAll('\\', '/');
307
+ const marker = `gcf-${g.name}`;
308
+ const change = registerGuard(settings, g, marker, `node "${file}" # ${marker}`);
309
+
310
+ return change && `${g.name} (${g.event}) ${change}`;
311
+ }).filter(Boolean);
264
312
  const allowed = allowAgent(settings);
265
- if (change || allowed.length) {
313
+ if (changes.length || allowed.length) {
266
314
  await mkdir(path.dirname(settingsFile), { recursive: true });
267
315
  await writeFile(settingsFile, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
268
316
  }
269
317
 
270
- console.log(
271
- change ? `readset-guard ${change} → ${settingsFile}` : 'readset-guard already registered',
272
- );
318
+ for (const c of changes) console.log(`${c} → ${settingsFile}`);
319
+ if (!changes.length) console.log('readset-guard and implement-guard already registered');
273
320
  if (allowed.length) console.log(`agent runs may now: ${allowed.join(', ')}`);
274
321
 
275
322
  await ignoreSession(projectDir);
@@ -291,7 +338,8 @@ export function register(program) {
291
338
  harness
292
339
  .command('init')
293
340
  .description(
294
- 'Initialize the harness in this project: readset-guard hook, agent permissions (idempotent)',
341
+ 'Initialize the harness in this project: readset and implement guards, agent permissions ' +
342
+ '(idempotent)',
295
343
  )
296
344
  .action(() => init(program));
297
345
 
@@ -327,6 +375,12 @@ export function register(program) {
327
375
  const ids = nextBatch(auditReport(), Math.max(1, Number(count ?? 1) || 1));
328
376
  console.log(o.json ? JSON.stringify(ids) : ids.join('\n') || 'nothing can run now');
329
377
  });
378
+ harness
379
+ .command('metrics')
380
+ .description('Run metrics from gcf/.metrics: reached moves, cost, plan checks, DoD, guards')
381
+ .option('--since <date>', 'only lines at or after this ISO date or time')
382
+ .option('--json', 'machine output')
383
+ .action(metricsCommand);
330
384
  harness
331
385
  .command('stop <id>')
332
386
  .description('Stop the session that works an item; the run goes on, the item stays ready')
@@ -398,15 +452,21 @@ export function stopRun(projectDir, id, core = cliCore(projectDir)) {
398
452
  return lock ? `stopped the run of ${id}; it stays ready` : `no run drives ${id}`;
399
453
  }
400
454
 
455
+ /**
456
+ * An item's line of the run report: its result, why, a skipped step of the process, and the
457
+ * lessons its sessions left in the inbox.
458
+ */
459
+ export const itemLine = (i) =>
460
+ [`${i.id}: ${i.result}`, i.reason ?? i.conflict ?? i.error ?? i.skipped, i.process]
461
+ .filter(Boolean)
462
+ .join(' — ') + (i.lessons ? `, ${i.lessons} lesson${i.lessons > 1 ? 's' : ''}` : '');
463
+
401
464
  /** The run report a human reads, then the playtest sheet of what now waits for play. */
402
465
  function printRun(r) {
403
466
  for (const o of r.orphans)
404
467
  console.log(`orphan ${o.id}: ${o.move ?? o.conflict ?? o.error ?? o.skipped}`);
405
468
  if (r.gate) console.error(`gcf gate: ${r.gate}`);
406
- for (const i of r.items) {
407
- const why = i.reason ?? i.conflict ?? i.error ?? i.skipped;
408
- console.log(`${i.id}: ${i.result}${why ? ` — ${why}` : ''}`);
409
- }
469
+ for (const i of r.items) console.log(itemLine(i));
410
470
 
411
471
  if (!r.gate) console.log(`\n${cliCore(process.cwd())(['playtest', 'session']).out.trim()}`);
412
472
  }