gcf-aif-ext 0.7.9 → 0.7.13

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.
@@ -41,6 +41,19 @@ export async function playtestList(root) {
41
41
  }));
42
42
  }
43
43
 
44
+ /** Items awaiting a decision: no verdict, but the human may need the build to make it. */
45
+ async function decisionList(root) {
46
+ return (await listItems(root))
47
+ .filter((c) => c.data.status === 'awaiting-decision')
48
+ .map((c) => ({
49
+ id: c.data.id,
50
+ title: c.data.title ?? null,
51
+ system: c.data.system ?? [],
52
+ intent: firstBullet(c.body, 'intent') || null,
53
+ }))
54
+ .sort((a, b) => a.id.localeCompare(b.id));
55
+ }
56
+
44
57
  const git = (projectDir, args) => {
45
58
  const res = spawnSync('git', args, { cwd: projectDir, encoding: 'utf8' });
46
59
 
@@ -73,10 +86,11 @@ export async function playtestSession(
73
86
  commit: git(projectDir, ['rev-parse', '--short', 'HEAD']),
74
87
  // No commit (no repository, or none yet): the build is unknown, not clean.
75
88
  dirty: dirty === null ? null : Boolean(dirty),
76
- build: /** @type {ReturnType<typeof runDod> | null} */ (null),
89
+ build: /** @type {Awaited<ReturnType<typeof runDod>> | null} */ (null),
77
90
  items,
91
+ decisions: await decisionList(root),
78
92
  };
79
- if (build) session.build = runDod(projectDir, await readConfig(root), 'build', stdio);
93
+ if (build) session.build = await runDod(projectDir, await readConfig(root), 'build', stdio);
80
94
 
81
95
  return session;
82
96
  }
@@ -0,0 +1,102 @@
1
+ // The record of a green `gcf verify` on main (Methodology §6): `close` and `submit` need one for
2
+ // the code as it is now, so a DoD run some other way cannot stand in for it. The record holds
3
+ // the hash of the project's code, not of a commit: committing the verified code keeps it valid,
4
+ // and any edit after the verify (a review's fix) asks for a new one. In a worktree the
5
+ // `GCF-Verified` trailer does this job, and the integrator's moves need no record.
6
+ import { spawnSync } from 'node:child_process';
7
+ import { copyFileSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+
11
+ // Not code: the framework's own files, the plans, the run files. A move or a plan tick there
12
+ // after the verify does not change what the DoD proved.
13
+ const NOT_CODE = ['gcf', '.ai-factory', '.gcf-session'];
14
+
15
+ const stampFile = (projectDir, id) =>
16
+ path.join(path.resolve(projectDir), '.gcf-session', 'runs', `${id}.verified`);
17
+
18
+ const git = (projectDir, args, env = {}) =>
19
+ spawnSync('git', args, { cwd: projectDir, encoding: 'utf8', env: { ...process.env, ...env } });
20
+
21
+ /**
22
+ * The git tree of the project's code as it is on disk, committed or not, new files included
23
+ * (ignored ones are not); null outside a repository. A git that fails on the code (a file it
24
+ * cannot read, `core.safecrlf`) throws: a gate that cannot see the code must not let it pass. A
25
+ * copy of the index keeps git's stat cache, so only changed files are hashed again.
26
+ */
27
+ export function codeTree(projectDir) {
28
+ const index = git(projectDir, ['rev-parse', '--git-path', 'index']);
29
+ if (index.status !== 0) return null;
30
+ const real = path.resolve(projectDir, index.stdout.trim());
31
+ const tmp = path.join(os.tmpdir(), `gcf-code-index-${process.pid}-${Date.now()}`);
32
+ const env = { GIT_INDEX_FILE: tmp };
33
+ const step = (args) => {
34
+ const r = git(projectDir, args, env);
35
+ if (r.status !== 0) throw new Error(`git ${args[0]} on the code failed: ${r.stderr.trim()}`);
36
+
37
+ return r.stdout.trim();
38
+ };
39
+
40
+ try {
41
+ if (existsSync(real)) copyFileSync(real, tmp);
42
+ step(['add', '-A', '--', '.', ...NOT_CODE.map((p) => `:(exclude)${p}`)]);
43
+ step(['rm', '-r', '-q', '--cached', '--ignore-unmatch', '--', ...NOT_CODE]);
44
+
45
+ return step(['write-tree']);
46
+ } finally {
47
+ rmSync(tmp, { force: true });
48
+ }
49
+ }
50
+
51
+ /** Record a green verify of the code as it is now. */
52
+ export function stampVerified(projectDir, id) {
53
+ const tree = codeTree(projectDir);
54
+ if (!tree) return;
55
+ const file = stampFile(projectDir, id);
56
+ mkdirSync(path.dirname(file), { recursive: true });
57
+ writeFileSync(file, `${JSON.stringify({ tree, at: new Date().toISOString() })}\n`);
58
+ }
59
+
60
+ /** A move ended the item's work: its record goes with it. */
61
+ export const dropStamp = (projectDir, id) => rmSync(stampFile(projectDir, id), { force: true });
62
+
63
+ /** The files that differ between two trees, the first few, for a refusal to name. */
64
+ function changed(projectDir, from, to) {
65
+ const r = git(projectDir, ['diff-tree', '-r', '--name-only', from, to]);
66
+ const files = r.status === 0 ? r.stdout.split('\n').filter(Boolean) : [];
67
+ const more = files.length > 5 ? ` and ${files.length - 5} more` : '';
68
+
69
+ return files.length ? ` (${files.slice(0, 5).join(', ')}${more})` : '';
70
+ }
71
+
72
+ /**
73
+ * Why `close` or `submit` may not take the item now, or null: no green verify on record, or
74
+ * the code changed since. A feature closes over its children and has no code of its own. The
75
+ * way past it is named as the human's: an agent reads this text.
76
+ */
77
+ export function unverifiedProblem(projectDir, id, type) {
78
+ if (type === 'feature') return null;
79
+ const tree = codeTree(projectDir);
80
+ if (!tree) return null;
81
+ const how =
82
+ `run ai-factory gcf verify ${id} until it is green; only the human may move it without ` +
83
+ 'one (--unverified "<why>")';
84
+ const stamp = readStamp(projectDir, id);
85
+ if (!stamp) return `${id}: no green gcf verify on record — ${how}`;
86
+ if (stamp.tree !== tree) {
87
+ const files = changed(projectDir, stamp.tree, tree);
88
+
89
+ return `${id}: the code changed since its green gcf verify${files} — ${how}`;
90
+ }
91
+
92
+ return null;
93
+ }
94
+
95
+ /** @returns {{tree: string, at: string} | null} the record, or null when there is none. */
96
+ function readStamp(projectDir, id) {
97
+ try {
98
+ return JSON.parse(readFileSync(stampFile(projectDir, id), 'utf8'));
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
package/lib/verify.js CHANGED
@@ -1,15 +1,111 @@
1
1
  // `gcf verify <id>` (Methodology §6): the auto-DoD checks code can run — the GCF checks of the
2
2
  // pre-commit gate, the item's live: delta against LIVE.md, then the project's `dod.build` and
3
3
  // `dod.test`. The second-agent review is the run's job; the attempt count lives in the run.
4
- import { spawnSync } from 'node:child_process';
4
+ import { spawn, spawnSync } from 'node:child_process';
5
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
5
6
  import path from 'node:path';
6
7
  import { readConfig } from './gcf-paths.js';
7
8
  import { readLive, applyDelta } from './live.js';
8
9
  import { gateProblems } from './gate.js';
9
10
  import { unplayedCloseProblem } from './work-ops.js';
10
11
 
12
+ /**
13
+ * Every process's parent, `[pid, ppid]` (POSIX): from /proc on Linux, which a slim image has
14
+ * with no procps; from `ps` elsewhere.
15
+ */
16
+ function parents() {
17
+ if (existsSync('/proc/self/stat'))
18
+ return readdirSync('/proc')
19
+ .filter((d) => /^\d+$/.test(d))
20
+ .flatMap((d) => {
21
+ try {
22
+ // `pid (comm) state ppid …`: comm may hold spaces and parentheses.
23
+ const stat = readFileSync(`/proc/${d}/stat`, 'utf8');
24
+
25
+ return [[Number(d), Number(stat.slice(stat.lastIndexOf(')') + 2).split(' ')[1])]];
26
+ } catch {
27
+ return [];
28
+ }
29
+ });
30
+ const res = spawnSync('ps', ['-A', '-o', 'pid=,ppid='], { encoding: 'utf8' });
31
+
32
+ return (res.stdout ?? '')
33
+ .split('\n')
34
+ .map((l) => l.trim().split(/\s+/).map(Number))
35
+ .filter((p) => p.length === 2 && p.every(Number.isFinite));
36
+ }
37
+
38
+ /** Stop `p` where it is, or kill it; a process gone already is no error. */
39
+ const signal = (p, sig) => {
40
+ try {
41
+ process.kill(p, sig);
42
+ } catch {
43
+ // Gone already.
44
+ }
45
+ };
46
+
47
+ /**
48
+ * End a command with everything it started: an engine the shell spawned outlives the shell. The
49
+ * command stays in its caller's process group, so a harness that kills its session's group
50
+ * kills the command too. On POSIX each process found is stopped first, so none forks a child the
51
+ * walk would miss; then all are killed.
52
+ * shortcut: a grandchild whose parent exited before the walk is init's and is not found; a
53
+ * process group of its own per command would catch it, at the cost of the harness's group kill.
54
+ */
55
+ function killTree(pid) {
56
+ if (process.platform === 'win32') {
57
+ spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' });
58
+
59
+ return;
60
+ }
61
+
62
+ const doomed = new Set([pid]);
63
+ // finally: a walk that throws (an unreadable /proc) must not leave what it stopped frozen.
64
+ try {
65
+ while (stopChildren(doomed));
66
+ } finally {
67
+ for (const p of doomed) signal(p, 'SIGKILL');
68
+ }
69
+ }
70
+
71
+ /** Stop every process of `doomed`, then add their children; true when one was added. */
72
+ function stopChildren(doomed) {
73
+ let grew = false;
74
+ for (const p of doomed) signal(p, 'SIGSTOP');
75
+ for (const [p, parent] of parents())
76
+ if (doomed.has(parent) && !doomed.has(p)) grew = Boolean(doomed.add(p));
77
+
78
+ return grew;
79
+ }
80
+
81
+ /**
82
+ * Run `cmd` in a shell; past `ms` (0: never) it is killed with its whole tree. `exit`, not
83
+ * `close`: a helper the command left running may hold the inherited stdio open.
84
+ */
85
+ function runCommand(cmd, cwd, stdio, ms) {
86
+ return new Promise((resolve) => {
87
+ const child = spawn(cmd, { cwd, shell: true, stdio });
88
+ let timedOut = false;
89
+ const timer =
90
+ ms > 0
91
+ ? setTimeout(() => {
92
+ timedOut = true;
93
+ killTree(child.pid);
94
+ }, ms)
95
+ : null;
96
+ child.on('error', (error) => {
97
+ clearTimeout(timer ?? undefined);
98
+ resolve({ error, timedOut });
99
+ });
100
+ child.on('exit', (status, signal) => {
101
+ clearTimeout(timer ?? undefined);
102
+ resolve({ status, signal, timedOut });
103
+ });
104
+ });
105
+ }
106
+
11
107
  /** Run a `dod.<key>` command in the project; `none` says the project has no such step. */
12
- export function runDod(projectDir, config, key, stdio) {
108
+ export async function runDod(projectDir, config, key, stdio) {
13
109
  if (config.dod !== undefined && (typeof config.dod !== 'object' || Array.isArray(config.dod)))
14
110
  return { name: key, ok: false, problems: ['dod in gcf/config.yml must map test: and build:'] };
15
111
  const cmd = config.dod?.[key];
@@ -20,13 +116,26 @@ export function runDod(projectDir, config, key, stdio) {
20
116
  ok: false,
21
117
  problems: [`dod.${key} is not set in gcf/config.yml — the project's command, or none`],
22
118
  };
23
- // shortcut: no timeout; stdin is closed, but a command that never ends (a watch mode, a
24
- // server) hangs `gcf verify`, and a harness run until its own timeout; add one when it bites.
25
- const res = spawnSync(cmd, { cwd: projectDir, shell: true, stdio });
26
- const ok = res.status === 0;
119
+ // stdin is closed; a command that never ends (a hung engine, a watch mode) meets the timeout.
120
+ const minutes = config.dod_timeout_min ?? 0;
121
+ const problem = failureOf(
122
+ key,
123
+ cmd,
124
+ minutes,
125
+ await runCommand(cmd, projectDir, stdio, minutes * 60000),
126
+ );
127
+
128
+ return { name: key, ok: !problem, problems: problem ? [problem] : [] };
129
+ }
130
+
131
+ /** Why a finished DoD command failed, or null when it passed. */
132
+ function failureOf(key, cmd, minutes, res) {
133
+ if (res.timedOut)
134
+ return `dod.${key} timed out after ${minutes} min (dod_timeout_min), killed: ${cmd}`;
135
+ if (res.status === 0) return null;
27
136
  const why = res.error ? res.error.message : `exit ${res.status ?? res.signal}`;
28
137
 
29
- return { name: key, ok, problems: ok ? [] : [`dod.${key} failed (${why}): ${cmd}`] };
138
+ return `dod.${key} failed (${why}): ${cmd}`;
30
139
  }
31
140
 
32
141
  /**
@@ -65,8 +174,8 @@ export async function verifyItem(projectDir, id, { stdio = ['ignore', 2, 2] } =
65
174
  const checks = [
66
175
  { name: 'gcf', ok: !problems.length, problems },
67
176
  { name: 'live', ok: !live.length, problems: live },
68
- runDod(projectDir, config, 'build', stdio),
69
- runDod(projectDir, config, 'test', stdio),
177
+ await runDod(projectDir, config, 'build', stdio),
178
+ await runDod(projectDir, config, 'test', stdio),
70
179
  ];
71
180
 
72
181
  return { id, checks, ok: checks.every((c) => c.ok) };
package/lib/work-ops.js CHANGED
@@ -39,6 +39,7 @@ import { dropInbox } from './inbox.js';
39
39
  import { refuseByPhase } from './phase.js';
40
40
  import { locked, runHolder } from './main-lock.js';
41
41
  import { dropAgentSession, releaseSession, sessionActive } from './session-mark.js';
42
+ import { dropStamp, unverifiedProblem } from './verify-stamp.js';
42
43
 
43
44
  /**
44
45
  * Options of a committed move; each move reads the ones it needs.
@@ -232,10 +233,38 @@ async function finish(m, move, tail, commit) {
232
233
  // and no rerun resumes it.
233
234
  releaseSession(m.projectDir, String(m.item.data.id));
234
235
  dropAgentSession(m.projectDir, String(m.item.data.id));
236
+ dropStamp(m.projectDir, String(m.item.data.id));
235
237
 
236
238
  return hash;
237
239
  }
238
240
 
241
+ /**
242
+ * `close` and `submit` from a command (Methodology §6): the code as it is now needs a green
243
+ * `gcf verify`, unless the operator says why not (`unverified`, written into the move's
244
+ * commit). The integrator's moves rest on the branch's trailer instead.
245
+ */
246
+ function needVerified(m, opts) {
247
+ if (!opts.requireVerify || opts.unverified) return;
248
+ const id = String(m.item.data.id);
249
+ const problem = verifyGateProblem(m.projectDir, id, m.item.data.type);
250
+ if (problem) throw new Error(problem);
251
+ }
252
+
253
+ /** The verify gate's refusal, or null; a git that cannot hash the code refuses too. */
254
+ function verifyGateProblem(projectDir, id, type) {
255
+ try {
256
+ return unverifiedProblem(projectDir, id, type);
257
+ } catch (e) {
258
+ return (
259
+ `${id}: cannot hash the code for the verify gate (${e.message}) — fix that, or the ` +
260
+ 'human passes --unverified "<why>"'
261
+ );
262
+ }
263
+ }
264
+
265
+ /** The commit's tail of a move made without a green verify. */
266
+ const unverifiedTail = (opts) => (opts.unverified ? ` (unverified: ${opts.unverified})` : '');
267
+
239
268
  const writeItem = (item, data, body = item.body) => atomicWrite(item.file, serialize(data, body));
240
269
 
241
270
  /** `data` without the keys named — a field that no longer applies leaves the frontmatter. */
@@ -481,11 +510,12 @@ async function submitItemUnlocked(projectDir, id, opts = {}) {
481
510
  if (m.item.data.gate === 'auto')
482
511
  throw new Error(`${id} has gate auto: nobody waits for it — close it with gcf close ${id}`);
483
512
  refuseOpenDepends(m, 'submit', { waiting: true });
513
+ needVerified(m, opts);
484
514
  const status = m.item.data.gate === 'verdict' ? 'awaiting-verdict' : 'awaiting-decision';
485
515
  const data = { ...without(m.item.data, 'blocked'), status, since: headHash(projectDir) };
486
516
  m.step(() => writeItem(m.item, data));
487
517
 
488
- return { id, status, hash: await finish(m, 'submit', id, commit) };
518
+ return { id, status, hash: await finish(m, 'submit', `${id}${unverifiedTail(opts)}`, commit) };
489
519
  }
490
520
 
491
521
  /** Why a soft gate-auto item cannot close unplayed, or null: only an `added` comes back out. */
@@ -556,11 +586,13 @@ async function closeItemUnlocked(projectDir, id, opts = {}) {
556
586
  if (m.item.data.gate !== 'auto')
557
587
  throw new Error(`${id} has gate ${m.item.data.gate}: submit it, the operator closes it`);
558
588
  refuseOpenDepends(m, 'close');
589
+ needVerified(m, opts);
559
590
  if (unplayed) closeUnplayed(m);
560
591
  else await closeEnding(m, constraints);
561
592
  }
562
593
 
563
- const hash = await finish(m, 'close', `${id}${unplayed ? ' → unplayed' : ''}`, commit);
594
+ const tail = `${id}${unplayed ? ' → unplayed' : ''}${unverifiedTail(opts)}`;
595
+ const hash = await finish(m, 'close', tail, commit);
564
596
 
565
597
  return { id, hash, unplayed };
566
598
  }
@@ -905,6 +937,8 @@ async function prioritizeItemsUnlocked(projectDir, ids, { commit = true } = {})
905
937
  });
906
938
  const hash = await commitMove(projectDir, `gcf(priority): ${ids.join(', ')}`, {
907
939
  files: items.map((i) => i.file),
940
+ // An item made with `item new` and never committed rides along: all of it is new work.
941
+ inputs: items.map((i) => i.file).filter((f) => !tracked(projectDir, f)),
908
942
  mutate: async () => {
909
943
  for (const [n, i] of items.entries())
910
944
  await atomicWrite(i.file, serialize({ ...i.data, priority: ranks[n] }, i.body));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gcf-aif-ext",
3
- "version": "0.7.9",
3
+ "version": "0.7.13",
4
4
  "description": "Game Context Framework for AI Factory",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,13 +23,14 @@ forbidden_behaviors:
23
23
  workflow:
24
24
  1. run `ai-factory gcf inbox --json`; read `gcf/LIVE.md` and `gcf/INDEX.md` to know what exists and what is in work
25
25
  2. classify each entry: bug, tune of a live or unplayed item, idea for the design, request (an asset file, a question), duplicate, or noise; search `gcf/graveyard.md` for each idea and bug
26
- 3. for each entry propose one call with its fields: a bug gets type `bug`, severity and a repro; a tune names the item and the note; an idea names the subsystem; a duplicate names the item it repeats
27
- 4. present the calls to the human, calls per message <= 5, and apply only the approved ones
28
- 5. a bug or other work: run `ai-factory gcf item new <id> --type <type> [--severity <level>] --from <in-id>`, then write its `intent:` and `repro:`
29
- 6. a tune of an item waiting for its verdict: keep the note for the verdict session and run `ai-factory gcf inbox drop <in-id>`; the note goes in through `/gcf-verdict … tune --note`
30
- 7. an idea for the design: write a unit with `TBD` slots in `gcf/design/<system>.md` and its backlog line, then run `ai-factory gcf inbox drop <in-id>`
31
- 8. a duplicate or noise: run `ai-factory gcf inbox drop <in-id>`
32
- 9. report what each entry became and how many entries remain
26
+ 3. a lesson (`source: lesson`, with its `kind` and the `item` it came from) is what got in an agent's way: propose the fix it asks for - a project rule, a tech item, a note for the framework or the template - or noise
27
+ 4. for each entry propose one call with its fields: a bug gets type `bug`, severity and a repro; a tune names the item and the note; an idea names the subsystem; a duplicate names the item it repeats
28
+ 5. present the calls to the human, calls per message <= 5, and apply only the approved ones
29
+ 6. a bug or other work: run `ai-factory gcf item new <id> --type <type> [--severity <level>] --from <in-id>`, then write its `intent:` and `repro:`
30
+ 7. a tune of an item waiting for its verdict: keep the note for the verdict session and run `ai-factory gcf inbox drop <in-id>`; the note goes in through `/gcf-verdict … tune --note`
31
+ 8. an idea for the design: write a unit with `TBD` slots in `gcf/design/<system>.md` and its backlog line, then run `ai-factory gcf inbox drop <in-id>`
32
+ 9. a duplicate or noise: run `ai-factory gcf inbox drop <in-id>`
33
+ 10. report what each entry became and how many entries remain
33
34
 
34
35
  invocation:
35
36
  - run after a playtest session, after a CI run that added entries, or when the user asks to triage
@@ -1,40 +1,42 @@
1
- gcf_context:
2
- - the game's design and its work live in `gcf/`, and the `ai-factory gcf` commands and the gcf skills change them
3
- - read `gcf/INDEX.md` before anything else under `gcf/`: it lists every subsystem, design unit and work item
4
- - `gcf/LIVE.md` says what the game does now, and `gcf/PILLARS.md` says what it must stay
5
- - `ai-factory gcf board --json` gives the whole state in one read: the phase, the items by status with plan progress, the inbox and the open questions
6
-
7
- gcf_work:
8
- - every piece of work is one work item `gcf/work/<id>.md`: find the item before writing code for a feature, a bug or a tech task, or create it with the gcf-card skill
9
- - a raw idea, a playtest note or a CI finding that is not work yet goes to `ai-factory gcf inbox add "<text>"`, and the gcf-triage skill sorts it later; a text that starts with a hyphen goes as `inbox add --text="<text>"`
10
- - before planning an item, run `ai-factory gcf context <id>` and read only the files it lists under `gcf/`
11
- - drive a ready item with the gcf-run skill when the project has gcf-harness-aif-ext (a batch at once: `ai-factory gcf-harness run <ids...>`), otherwise with aif-plan and aif-implement
12
- - after the code, write the item's `live:` delta and run `ai-factory gcf verify <id>`: fix what it reports and run it again until every check is green
13
- - then end the work on the item with its command: `ai-factory gcf submit <id>` for gate verdict or decision, `ai-factory gcf close <id>` for gate auto
14
- - work that cannot go on: run `ai-factory gcf block <id> --reason "<why or the question>"`
15
- - in a worktree on branch `work/<id>` change no `gcf/` file but the item's `live:`, commit all, then run `ai-factory gcf verify <id> --mark`: `ai-factory gcf integrate` in the main checkout merges it and records the move
16
- - a design question only the human can answer becomes a D1 record in `gcf/open-questions.md`
17
- - a word added to a `not:` of `gcf/glossary.md` leaves `gcf/design/` and every item but an unplayed one in the same commit: `ai-factory gcf design check` names each hit as M4
18
- - the human plays from `ai-factory gcf playtest session`; each free note from that play goes to `ai-factory gcf inbox add --source playtest "<note>"`
19
- - a release item carries `version:` and `channel:` (`item new <id> --type release --release <v> --channel <c>`): its end writes the `release:` block of `gcf/LIVE.md`
20
- - `item new` and `take` refuse a type the current phase does not take: run `ai-factory gcf phase` and bring the refusal to the human
21
- - an item the human wants run on a given model or effort gets `item new <id> --model <m> --effort <e>`: the harness run passes both to the agent
22
- - ready items run in `priority:` order, lower first: rank a set of items with `ai-factory gcf prioritize <id> <id> …`, and put an id in `depends` only when the item uses that one's data or code
23
- - a D1 record that carries the human's `answer:` goes into the slot of each unit it blocks, with `answer: q-NNN`, and then leaves `gcf/open-questions.md`
24
- - a pillar changes only on the human's word: `ai-factory gcf pillar rename <from> <to>`, or `ai-factory gcf pillar retire <id> --reason "<why>"`
25
-
26
- gcf_forbidden:
27
- - do not edit `status:`, `frozen:`, `gcf/LIVE.md`, `gcf/history.md`, `gcf/graveyard.md` or `gcf/INDEX.md` by hand: the commands own them
28
- - do not give a verdict or a decision: `verdict` and `decide` belong to the human, through the gcf-verdict skill
29
- - do not answer a `TBD` of a design unit yourself: ask the human and offer your draft as the proposed answer
30
- - do not run `ai-factory gcf phase next`: the phase moves on the human's word only
31
- - do not change what agent runs may do: `ai-factory gcf permissions` and its deny groups belong to the human
32
- - do not change a setting of `gcf/config.yml`: `ai-factory gcf config set` belongs to the human
33
- - do not reuse an ended id: rework gets a new id with `reopens: <old id>`
34
- - do not commit past the gate with `--no-verify`: a refused commit names what to fix
35
-
36
- gcf_skills:
37
- - gcf-vision starts a game that has no design yet, and gcf-ingest imports a GDD into `gcf/design/`
38
- - gcf-card puts work into `gcf/work/`, and gcf-triage sorts `gcf/inbox.md`
39
- - gcf-verdict records the human's verdict or decision, and gcf-audit reports the health of `gcf/`
40
- - gcf-debate stresses the design, gcf-adopt puts existing code on GCF, and gcf-init sets up `gcf/` and revises the pillars
1
+ gcf_context:
2
+ - the game's design and its work live in `gcf/`, and the `ai-factory gcf` commands and the gcf skills change them
3
+ - read `gcf/INDEX.md` before anything else under `gcf/`: it lists every subsystem, design unit and work item
4
+ - `gcf/LIVE.md` says what the game does now, and `gcf/PILLARS.md` says what it must stay
5
+ - `ai-factory gcf board --json` gives the whole state in one read: the phase, the items by status with plan progress, the inbox and the open questions
6
+
7
+ gcf_work:
8
+ - every piece of work is one work item `gcf/work/<id>.md`: find the item before writing code for a feature, a bug or a tech task, or create it with the gcf-card skill
9
+ - a raw idea, a playtest note or a CI finding that is not work yet goes to `ai-factory gcf inbox add "<text>"`, and the gcf-triage skill sorts it later; a text that starts with a hyphen goes as `inbox add --text="<text>"`
10
+ - before planning an item, run `ai-factory gcf context <id>` and read only the files it lists under `gcf/`
11
+ - drive a ready item with the gcf-run skill when the project has gcf-harness-aif-ext (a batch at once: `ai-factory gcf-harness run <ids...>`), otherwise with aif-plan and aif-implement
12
+ - a question about how the runs go (the share that reached their move, cost, plan checks, DoD attempts, guard firings) is answered by `ai-factory gcf-harness metrics`
13
+ - after the code, write the item's `live:` delta and run `ai-factory gcf verify <id>`: fix what it reports and run it again until every check is green
14
+ - `submit` and `close` refuse code that changed since its last green `gcf verify`: run it again after any fix; `--unverified "<why>"` is the human's call, never the agent's
15
+ - then end the work on the item with its command: `ai-factory gcf submit <id>` for gate verdict or decision, `ai-factory gcf close <id>` for gate auto
16
+ - work that cannot go on: run `ai-factory gcf block <id> --reason "<why or the question>"`
17
+ - in a worktree on branch `work/<id>` change no `gcf/` file but the item's `live:`, commit all, then run `ai-factory gcf verify <id> --mark`: `ai-factory gcf integrate` in the main checkout merges it and records the move
18
+ - a design question only the human can answer becomes a D1 record in `gcf/open-questions.md`
19
+ - a word added to a `not:` of `gcf/glossary.md` leaves `gcf/design/` and every item but an unplayed one in the same commit: `ai-factory gcf design check` names each hit as M4
20
+ - the human plays from `ai-factory gcf playtest session`; each free note from that play goes to `ai-factory gcf inbox add --source playtest "<note>"`
21
+ - a release item carries `version:` and `channel:` (`item new <id> --type release --release <v> --channel <c>`): its end writes the `release:` block of `gcf/LIVE.md`
22
+ - `item new` and `take` refuse a type the current phase does not take: run `ai-factory gcf phase` and bring the refusal to the human
23
+ - an item the human wants run on a given model or effort gets `item new <id> --model <m> --effort <e>`: the harness run passes both to the agent
24
+ - ready items run in `priority:` order, lower first: rank a set of items with `ai-factory gcf prioritize <id> <id> …`, and put an id in `depends` only when the item uses that one's data or code
25
+ - a D1 record that carries the human's `answer:` goes into the slot of each unit it blocks, with `answer: q-NNN`, and then leaves `gcf/open-questions.md`
26
+ - a pillar changes only on the human's word: `ai-factory gcf pillar rename <from> <to>`, or `ai-factory gcf pillar retire <id> --reason "<why>"`
27
+
28
+ gcf_forbidden:
29
+ - do not edit `status:`, `frozen:`, `gcf/LIVE.md`, `gcf/history.md`, `gcf/graveyard.md` or `gcf/INDEX.md` by hand: the commands own them
30
+ - do not give a verdict or a decision: `verdict` and `decide` belong to the human, through the gcf-verdict skill
31
+ - do not answer a `TBD` of a design unit yourself: ask the human and offer your draft as the proposed answer
32
+ - do not run `ai-factory gcf phase next`: the phase moves on the human's word only
33
+ - do not change what agent runs may do: `ai-factory gcf permissions` and its deny groups belong to the human
34
+ - do not change a setting of `gcf/config.yml`: `ai-factory gcf config set` belongs to the human
35
+ - do not reuse an ended id: rework gets a new id with `reopens: <old id>`
36
+ - do not commit past the gate with `--no-verify`: a refused commit names what to fix
37
+
38
+ gcf_skills:
39
+ - gcf-vision starts a game that has no design yet, and gcf-ingest imports a GDD into `gcf/design/`
40
+ - gcf-card puts work into `gcf/work/`, and gcf-triage sorts `gcf/inbox.md`
41
+ - gcf-verdict records the human's verdict or decision, and gcf-audit reports the health of `gcf/`
42
+ - gcf-debate stresses the design, gcf-adopt puts existing code on GCF, and gcf-init sets up `gcf/` and revises the pillars
@@ -7,14 +7,25 @@ context_budget: 30000
7
7
  pillars_budget: 1500
8
8
  envelope_default_spread: 0.3
9
9
  unplayed_warn_after: 5
10
- plan_passes: 5
10
+ plan_passes: 3
11
11
  dod_attempts: 2
12
+ # Minutes a dod.test or dod.build command may take before it is killed with all it started
13
+ # (0: no limit): a hung engine fails the DoD instead of holding the run.
14
+ dod_timeout_min: 60
12
15
  # Agent runs (Harness-PRD §7): the share of the context window at which a run hands its item
13
16
  # to a fresh session (0: never), the window when the model's is unknown (0: the model's), and
14
17
  # the agent's auto-compaction share (0: its default; at least the handoff share + 15).
15
18
  context_handoff_pct: 60
16
19
  context_window: 0
17
20
  auto_compact_pct: 0
21
+ # Lessons (Methodology §5.2.2): at the end of each session on an item its agent says what got in
22
+ # its way, into the inbox. `ai-factory gcf lessons prompt <id>` prints the questions. Uncomment to
23
+ # turn them off, to ask what the project's template should learn, or to add questions:
24
+ # lessons:
25
+ # off: true
26
+ # template: ../my-template
27
+ # questions:
28
+ # - which step of this item took the longest, and why?
18
29
  # What agent runs may not do lives in an `agent:` section that `ai-factory gcf permissions`
19
30
  # and the board write; without it the default deny groups hold.
20
31
  # The project's own commands for the auto DoD (Methodology §6); the engine does not matter.