gcf-harness-aif-ext 0.5.1 → 0.6.1

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
@@ -11,7 +11,7 @@ The harness turns policy-as-text into an executable pipeline. It has **no memory
11
11
 
12
12
  ## Requirements
13
13
 
14
- - [`gcf-aif-ext`](../gcf-aif-ext) `^0.4.1` 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
14
+ - [`gcf-aif-ext`](../gcf-aif-ext) `^0.5.3` 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
15
  - Claude Code 2.1.270+ (hooks, dynamic workflows) — the only supported runtime
16
16
  - AI Factory 2.x, Node.js ≥ 22.14
17
17
 
@@ -47,6 +47,10 @@ Updating keeps the hook entry valid — it points into the extension's install d
47
47
 
48
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
49
 
50
+ 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.
51
+
52
+ 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.
53
+
50
54
  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.
51
55
 
52
56
  To remove the extension:
@@ -59,10 +63,12 @@ Removal does not touch `.claude/settings.json`: delete the `PreToolUse` entry wh
59
63
 
60
64
  ## What it adds
61
65
 
62
- **`/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 the project's `dod.test` and `dod.build` 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.
66
+ **`/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.
63
67
 
64
68
  **`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.
65
69
 
70
+ **`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 "/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. Logs: `.gcf-session/runs/<id>.log`. 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`.
71
+
66
72
  **`/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.
67
73
 
68
74
  **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.
@@ -7,6 +7,7 @@ import { existsSync } from 'node:fs';
7
7
  import { spawnSync } from 'node:child_process';
8
8
  import path from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
+ import { run, cliCore, DEFAULT_AGENT } from './run.js';
10
11
 
11
12
  const EXT_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
12
13
  const MARKER = 'gcf-readset-guard';
@@ -26,6 +27,8 @@ function itemProblem(id, item, report) {
26
27
  return `${id} is ${item.status} — only ready items are driven`;
27
28
  }
28
29
 
30
+ if (item.running) return `${id} is driven by another harness run — wait until it ends`;
31
+
29
32
  // A child of a feature paused after a death does not start: the decision may kill it.
30
33
  const parent = item.parent ? report.items.find((p) => p.id === item.parent) : null;
31
34
  if (parent?.status === 'awaiting-decision') {
@@ -237,4 +240,49 @@ export function register(program) {
237
240
  .description('Gate work items before the paid stages of /gcf-run: exit 1 with the reason')
238
241
  .option('--json', 'machine output: {pass, reason}')
239
242
  .action(runGate);
243
+
244
+ harness
245
+ .command('run <ids...>')
246
+ .description('Run items at once, each in its worktree with its own agent, then integrate')
247
+ .option(
248
+ '--agent <command>',
249
+ 'the agent command; it gets "/gcf-run <id> --worktree"',
250
+ process.env.GCF_RUN_AGENT ?? DEFAULT_AGENT,
251
+ )
252
+ .option('--json', 'machine output')
253
+ .action(runItems);
254
+ }
255
+
256
+ /** The run report a human reads, then the playtest sheet of what now waits for play. */
257
+ function printRun(r) {
258
+ for (const o of r.orphans)
259
+ console.log(`orphan ${o.id}: ${o.move ?? o.conflict ?? o.error ?? o.skipped}`);
260
+ if (r.gate) console.error(`gcf gate: ${r.gate}`);
261
+ for (const i of r.items) {
262
+ const why = i.reason ?? i.conflict ?? i.error ?? i.skipped;
263
+ console.log(`${i.id}: ${i.result}${why ? ` — ${why}` : ''}`);
264
+ }
265
+
266
+ if (!r.gate) console.log(`\n${cliCore(process.cwd())(['playtest', 'session']).out.trim()}`);
267
+ }
268
+
269
+ const failed = (r) =>
270
+ Boolean(r.gate) ||
271
+ r.items.some((i) => ['conflict', 'error'].includes(i.result)) ||
272
+ r.orphans.some((o) => o.conflict || o.error);
273
+
274
+ async function runItems(ids, opts) {
275
+ try {
276
+ const r = await run(process.cwd(), [...new Set(ids)], {
277
+ agent: opts.agent,
278
+ gate: (batch) => gate(auditReport(), batch),
279
+ });
280
+ if (opts.json) console.log(JSON.stringify(r, null, 2));
281
+ else printRun(r);
282
+ if (failed(r)) process.exitCode = 1;
283
+ } catch (e) {
284
+ // A refusal of the core (main not clean, no core) ends the run with its one line.
285
+ console.error(`gcf-harness run: ${e.message}`);
286
+ process.exitCode = 1;
287
+ }
240
288
  }
@@ -0,0 +1,241 @@
1
+ // `ai-factory gcf-harness run <ids…>` (Methodology §7): every item runs in its own worktree with
2
+ // its own agent session, all at once; the integrator ends the run. The first step picks up the
3
+ // verified branches a dead run left; an agent that needs the human leaves `outcome.json` in its
4
+ // worktree, and the block is recorded on main — a worktree never writes gcf/ status.
5
+ import { spawn, spawnSync } from 'node:child_process';
6
+ import { existsSync } from 'node:fs';
7
+ import { mkdir, open, readFile, rm, stat, writeFile } from 'node:fs/promises';
8
+ import path from 'node:path';
9
+
10
+ export const DEFAULT_AGENT = 'claude -p';
11
+
12
+ /** The core CLI: the same ai-factory that runs this command, in the project. */
13
+ export function cliCore(projectDir) {
14
+ return (args) => {
15
+ const res = spawnSync(process.execPath, [process.argv[1], 'gcf', ...args], {
16
+ cwd: projectDir,
17
+ encoding: 'utf8',
18
+ });
19
+
20
+ return { ok: res.status === 0, out: res.stdout ?? '', err: res.stderr ?? '' };
21
+ };
22
+ }
23
+
24
+ function json(res, what) {
25
+ try {
26
+ return JSON.parse(res.out);
27
+ } catch (e) {
28
+ throw new Error(`ai-factory gcf ${what} gave no report: ${(res.err || res.out).trim()}`, {
29
+ cause: e,
30
+ });
31
+ }
32
+ }
33
+
34
+ /** One agent session in the item's worktree; resolves with its exit code. */
35
+ function startAgent(agent, id, dir, fd) {
36
+ return new Promise((resolve) => {
37
+ const child = spawn(`${agent} "/gcf-run ${id} --worktree"`, {
38
+ cwd: dir,
39
+ shell: true,
40
+ stdio: ['ignore', fd, fd],
41
+ });
42
+ child.on('error', () => resolve(-1));
43
+ child.on('exit', (code) => resolve(code ?? -1));
44
+ });
45
+ }
46
+
47
+ /** What the agent left for the main checkout: `{block: <reason>}`, or null. */
48
+ async function outcomeOf(dir) {
49
+ const file = path.join(dir, '.gcf-session', 'outcome.json');
50
+ if (!existsSync(file)) return null;
51
+ try {
52
+ return JSON.parse(await readFile(file, 'utf8'));
53
+ } catch {
54
+ return { block: `the run left an unreadable ${file}` };
55
+ } finally {
56
+ await rm(file, { force: true });
57
+ }
58
+ }
59
+
60
+ /** Whether a pid is a live process; EPERM is alive under another user. */
61
+ function alive(pid) {
62
+ try {
63
+ process.kill(pid, 0);
64
+
65
+ return true;
66
+ } catch (e) {
67
+ return e.code === 'EPERM';
68
+ }
69
+ }
70
+
71
+ /**
72
+ * One harness run per item at a time: `.gcf-session/runs/<id>.lock` holds the pid of the run
73
+ * that drives it, created exclusively. A lock whose process is gone is taken over; the core
74
+ * refuses moves on the item while the lock is live. Returns the release, or null.
75
+ */
76
+ async function claim(logs, id) {
77
+ const file = path.join(logs, `${id}.lock`);
78
+ for (let attempt = 0; attempt < 2; attempt++) {
79
+ if (await create(file)) return () => rm(file, { force: true });
80
+ const text = await readFile(file, 'utf8').catch(() => null);
81
+ if (text === null) continue; // released meanwhile
82
+ const pid = Number(text.trim());
83
+ if (pid === process.pid) return () => rm(file, { force: true });
84
+ if (!(await dead(file, pid))) return null;
85
+ await clearDead(file, text);
86
+ }
87
+
88
+ return null;
89
+ }
90
+
91
+ /** Create the lock with this pid, or false when it exists. */
92
+ async function create(file) {
93
+ try {
94
+ await writeFile(file, `${process.pid}\n`, { flag: 'wx' });
95
+
96
+ return true;
97
+ } catch (e) {
98
+ if (e.code !== 'EEXIST') throw e;
99
+
100
+ return false;
101
+ }
102
+ }
103
+
104
+ /** A lock whose pid is gone, or that never got one and is old: its creator died writing it. */
105
+ async function dead(file, pid) {
106
+ if (Number.isInteger(pid) && pid > 0) return !alive(pid);
107
+ const age = await stat(file).then(
108
+ (s) => Date.now() - s.mtimeMs,
109
+ () => 0,
110
+ );
111
+
112
+ return age > 10000;
113
+ }
114
+
115
+ /**
116
+ * Remove a dead lock under `<lock>.break`, as the core's main lock does: two runs that judged
117
+ * the same lock dead cannot both remove it, so neither deletes the other's fresh lock.
118
+ */
119
+ async function clearDead(file, text) {
120
+ const breaker = `${file}.break`;
121
+ if (!(await create(breaker))) return;
122
+ try {
123
+ if ((await readFile(file, 'utf8').catch(() => null)) === text) await rm(file, { force: true });
124
+ } finally {
125
+ await rm(breaker, { force: true });
126
+ }
127
+ }
128
+
129
+ /** Whether the tip of the worktree's branch carries `GCF-Verified: <id>`. */
130
+ function verified(dir, id) {
131
+ const res = spawnSync('git', ['log', '-1', '--format=%(trailers:key=GCF-Verified,valueonly)'], {
132
+ cwd: dir,
133
+ encoding: 'utf8',
134
+ });
135
+
136
+ return (res.stdout ?? '').split('\n').some((v) => v.trim() === id);
137
+ }
138
+
139
+ /** The worktree of each item, claimed for this run; an item that cannot start is an error. */
140
+ async function prepare(ids, core, logs) {
141
+ const ready = [];
142
+ const failed = [];
143
+ for (const id of ids) {
144
+ const release = await claim(logs, id);
145
+ if (!release) {
146
+ failed.push({ id, result: 'error', error: `${id} is already running in another run` });
147
+ continue;
148
+ }
149
+
150
+ try {
151
+ const wt = json(await core(['worktree', id, '--json']), `worktree ${id}`);
152
+ if (wt.sync) throw new Error(wt.sync);
153
+ ready.push({ id, dir: wt.path, release });
154
+ } catch (e) {
155
+ await release();
156
+ failed.push({ id, result: 'error', error: e.message });
157
+ }
158
+ }
159
+
160
+ return { ready, failed };
161
+ }
162
+
163
+ /** One session per item, its output in `.gcf-session/runs/<id>.log`. */
164
+ async function session(r, agent, logs) {
165
+ let log = null;
166
+ try {
167
+ log = await open(path.join(logs, `${r.id}.log`), 'a');
168
+
169
+ return { ...r, code: await startAgent(agent, r.id, r.dir, log.fd) };
170
+ } catch (e) {
171
+ return { ...r, code: null, error: e.message };
172
+ } finally {
173
+ await log?.close();
174
+ await r.release();
175
+ }
176
+ }
177
+
178
+ /**
179
+ * A run that asked to block is recorded on main. Asked after its branch was verified, the
180
+ * block wins: an empty commit takes the trailer off the tip, so no integrate merges it.
181
+ */
182
+ async function recordBlock(r, outcome, core) {
183
+ const reason = String(outcome.block);
184
+ if (verified(r.dir, r.id))
185
+ spawnSync('git', ['commit', '--allow-empty', '-m', `gcf(unverified): ${r.id}`], { cwd: r.dir });
186
+ const b = await core(['block', r.id, '--reason', reason]);
187
+
188
+ return {
189
+ id: r.id,
190
+ result: b.ok ? 'block' : 'error',
191
+ reason,
192
+ ...(b.ok ? {} : { error: (b.err || b.out).trim() }),
193
+ };
194
+ }
195
+
196
+ /** The result line of an item the integrator handled, or why it has none. */
197
+ function resultOf(run, integrated) {
198
+ const done = integrated.find((x) => x.id === run.id);
199
+ if (!done && run.error) return { id: run.id, result: 'error', error: run.error };
200
+ if (!done)
201
+ return {
202
+ id: run.id,
203
+ result:
204
+ `no result (agent exit ${run.code}): ${run.id} stays ready, a rerun resumes its ` +
205
+ 'worktree',
206
+ };
207
+
208
+ return {
209
+ ...done,
210
+ result: done.move ?? (done.conflict ? 'conflict' : done.error ? 'error' : 'skipped'),
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Pick up orphans, gate the batch, run every item in its worktree at once, record each block
216
+ * on main, integrate the verified branches. `gate(ids)` answers `{pass, reason}`.
217
+ */
218
+ export async function run(
219
+ projectDir,
220
+ ids,
221
+ { core = cliCore(projectDir), agent = DEFAULT_AGENT, gate },
222
+ ) {
223
+ const orphans = json(await core(['integrate', '--json']), 'integrate');
224
+ const passed = gate(ids);
225
+ if (!passed.pass) return { orphans, gate: passed.reason, items: [] };
226
+
227
+ const logs = path.join(path.resolve(projectDir), '.gcf-session', 'runs');
228
+ await mkdir(logs, { recursive: true });
229
+ const { ready, failed } = await prepare(ids, core, logs);
230
+ const runs = await Promise.all(ready.map((r) => session(r, agent, logs)));
231
+ const items = [...failed];
232
+ for (const r of runs) {
233
+ const outcome = await outcomeOf(r.dir);
234
+ if (outcome?.block) items.push(await recordBlock(r, outcome, core));
235
+ }
236
+
237
+ const integrated = json(await core(['integrate', '--json']), 'integrate');
238
+ for (const r of runs) if (!items.some((i) => i.id === r.id)) items.push(resultOf(r, integrated));
239
+
240
+ return { orphans, items };
241
+ }
package/extension.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/lee-to/ai-factory/2.x/schemas/extension.schema.json",
3
3
  "name": "gcf-harness-aif-ext",
4
- "version": "0.5.1",
4
+ "version": "0.6.1",
5
5
  "description": "GCF orchestration harness for Claude Code: one-command card pipeline, readset guard",
6
6
  "skills": [
7
7
  "skills/gcf-run",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gcf-harness-aif-ext",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "GCF orchestration harness for Claude Code: one-command card pipeline, readset guard",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,7 +28,7 @@
28
28
  "prepack": "node -e \"const p=require('./package.json'),e=require('./extension.json');if(p.version!==e.version)throw new Error('version mismatch: package.json '+p.version+' vs extension.json '+e.version)\""
29
29
  },
30
30
  "peerDependencies": {
31
- "gcf-aif-ext": "^0.4.1"
31
+ "gcf-aif-ext": "^0.5.3"
32
32
  },
33
33
  "files": [
34
34
  "extension.json",
@@ -11,6 +11,7 @@ purpose:
11
11
 
12
12
  inputs:
13
13
  - 1 or more work item or design unit ids; a batch shares one build and one playtest list
14
+ - `--worktree` when `ai-factory gcf-harness run` started this session inside the item's worktree
14
15
 
15
16
  preconditions:
16
17
  - `gcf/` exists and `ai-factory gcf audit` runs (the gcf-aif-ext extension provides it)
@@ -24,7 +25,7 @@ forbidden_behaviors:
24
25
  - do not edit `status:` or `gcf/LIVE.md` by hand: `submit`, `close` and `block` own them
25
26
 
26
27
  workflow:
27
- 1. session ritual: delete a `.gcf-session/readset.json` left by a killed run; run `ai-factory gcf audit --json`; read `gcf/INDEX.md`; report the audit first line to the human
28
+ 1. session ritual: delete a `.gcf-session/readset.json` left by a killed run; run `ai-factory gcf integrate` to merge the verified branches a dead run left; run `ai-factory gcf audit --json`; read `gcf/INDEX.md`; report the audit first line
28
29
  2. for a unit still in `gcf/design/`: take it with the gcf-card skill first
29
30
  3. for a feature id: replace it with its `ready` children from the audit report's `items` (`parent` names the feature) that are not in `open_depends`; a feature in `awaiting-decision` starts none of them
30
31
  4. of those children, keep one per subsystem unless every child sharing it says `parallel: ok`; the rest wait for the next run; an empty list: report "nothing to run" and stop
@@ -36,14 +37,18 @@ workflow:
36
37
  10. after each pass run `ai-factory gcf plan check <id>`; a failure blocks the item with its reason; a pass that changes no task ends the loop - the plan converged
37
38
  11. passes <= `plan_passes` from `gcf/config.yml` (5 by default); a plan still changing after the last pass: run `ai-factory gcf block <id> --reason "plan did not converge"` and stop
38
39
  12. a question only the human can answer - a save format, a public API, a new dependency, a pillar conflict: run `ai-factory gcf block <id> --reason "<question>"` and stop
39
- 13. implement with aif-implement, then verify: the project's `dod.test` and `dod.build` commands from `gcf/config.yml`, plus the item's negative constraints; attempts <= `dod_attempts`, each with a diagnosis
40
- 14. review the diff with a fresh subagent; fix what it finds before the commit; DoD still red after the attempts: commit nothing, run `ai-factory gcf block <id> --reason "<diagnosis>"`
40
+ 13. implement with aif-implement, write the item's `live:` delta, then run `ai-factory gcf verify <id>` and check the item's negative constraints; a non-zero exit is a red DoD; attempts <= `dod_attempts`, each with a diagnosis
41
+ 14. review the diff with a fresh subagent; fix what it finds before the commit; DoD still red after the attempts: commit no code, run `ai-factory gcf block <id> --reason "<diagnosis>"` - its commit carries the item file with its delta
41
42
  15. the reviewer also answers the `review_question` of each pillar in `gcf/PILLARS.md` over the diff: an answer that breaks the pillar is a finding
42
- 16. commit the implementation via aif-commit; write the item's `live:` delta
43
+ 16. commit the implementation and the `live:` delta via aif-commit
43
44
  17. gate `verdict` or `decision`: run `ai-factory gcf submit <id>`; gate `auto`: run `ai-factory gcf close <id>` - a soft unit closes unplayed
44
45
  18. a dependency still waiting for its verdict refuses the close: run `ai-factory gcf block <id> --reason "waits for <dep>"`
45
- 19. close the run: delete `.gcf-session/readset.json`, run `ai-factory gcf playtest` and hand the list to the human, with one report row per plan pass: pass, tasks changed, result
46
+ 19. close the run: delete `.gcf-session/readset.json`, run `ai-factory gcf playtest session` and hand its checklist to the human, with one report row per plan pass: pass, tasks changed, result
46
47
  20. after a tune verdict or an unblock, re-enter this pipeline at step 5: the gates re-run because the counters moved
48
+ 21. with `--worktree`: skip the integrate of step 1, steps 2-4 and the playtest of step 19, and change no `gcf/` file but the item's `live:`
49
+ 22. with `--worktree`, every stop - a failed gate, an over budget readset, a failed plan check, steps 11, 12, 14, 18 - writes `{"block": "<reason>"}` to `.gcf-session/outcome.json` and ends the session
50
+ 23. with `--worktree`, run no `ai-factory gcf block`, `submit` or `close`: the core refuses them on a `work/` branch, and the harness records the move on main
51
+ 24. with `--worktree`, step 17 is: commit everything but `gcf/.metrics`, then run `ai-factory gcf verify <id> --mark`; the harness merges the branch
47
52
 
48
53
  invocation:
49
54
  - run when the user names a work item or a unit to drive; batches share steps 1 and 19