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.
- package/README.md +223 -207
- package/commands/gcf.js +107 -4
- package/extension.json +1 -1
- package/injections/commit-status.md +10 -9
- package/lib/board.js +54 -4
- package/lib/config-set.js +5 -0
- package/lib/gcf-paths.js +3 -1
- package/lib/inbox.js +13 -3
- package/lib/integrate.js +23 -12
- package/lib/lessons.js +131 -0
- package/lib/metrics.js +41 -0
- package/lib/playtest-list.js +16 -2
- package/lib/verify-stamp.js +102 -0
- package/lib/verify.js +118 -9
- package/lib/work-ops.js +36 -2
- package/package.json +1 -1
- package/skills/gcf-triage/SKILL.md +8 -7
- package/templates/agents-block.md +42 -40
- package/templates/config.yml +12 -1
package/lib/playtest-list.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
@@ -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.
|
|
27
|
-
4.
|
|
28
|
-
5.
|
|
29
|
-
6. a
|
|
30
|
-
7. an
|
|
31
|
-
8.
|
|
32
|
-
9.
|
|
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
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
- a
|
|
18
|
-
-
|
|
19
|
-
- a
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
- do not
|
|
30
|
-
- do not
|
|
31
|
-
- do not
|
|
32
|
-
- do not
|
|
33
|
-
- do not
|
|
34
|
-
- do not
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
- gcf-
|
|
40
|
-
- gcf-
|
|
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
|
package/templates/config.yml
CHANGED
|
@@ -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:
|
|
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.
|