gcf-aif-ext 0.2.7 → 0.4.0
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 +54 -10
- package/commands/gcf.js +452 -30
- package/extension.json +22 -5
- package/hooks/pre-commit.js +69 -41
- package/injections/commit-status.md +7 -4
- package/injections/plan-context.md +8 -4
- package/injections/review-pillars.md +4 -3
- package/lib/anchors.js +60 -35
- package/lib/audit-core.js +172 -55
- package/lib/bundle.js +278 -0
- package/lib/context.js +25 -23
- package/lib/design-check.js +631 -0
- package/lib/design.js +179 -0
- package/lib/gcf-paths.js +13 -2
- package/lib/git.js +63 -0
- package/lib/id.js +7 -15
- package/lib/inbox.js +54 -0
- package/lib/index-gen.js +62 -13
- package/lib/ledger.js +94 -0
- package/lib/linter.js +17 -5
- package/lib/live.js +179 -0
- package/lib/migrate.js +345 -0
- package/lib/plan-check.js +31 -0
- package/lib/playtest-list.js +9 -5
- package/lib/systems-doc.js +85 -0
- package/lib/transitions.js +36 -21
- package/lib/unit-ops.js +297 -0
- package/lib/validate-item.js +244 -0
- package/lib/verdict-fs.js +112 -165
- package/lib/work-ops.js +727 -0
- package/package.json +1 -1
- package/profiles/game.md +68 -0
- package/profiles/glossary.md +37 -0
- package/profiles/pillars.md +3 -2
- package/profiles/system.md +1 -1
- package/profiles/unit-constraint.md +52 -0
- package/profiles/unit-entity.md +48 -0
- package/profiles/unit-mechanic.md +111 -0
- package/profiles/unit-resource.md +91 -0
- package/profiles/unit-rule.md +52 -0
- package/schemas/item.schema.json +105 -0
- package/skills/gcf-adopt/SKILL.md +3 -3
- package/skills/gcf-audit/SKILL.md +7 -6
- package/skills/gcf-card/SKILL.md +31 -17
- package/skills/gcf-debate/SKILL.md +17 -14
- package/skills/gcf-ingest/SKILL.md +39 -23
- package/skills/gcf-init/SKILL.md +5 -5
- package/skills/gcf-triage/SKILL.md +35 -0
- package/skills/gcf-verdict/SKILL.md +19 -15
- package/skills/gcf-vision/SKILL.md +17 -11
- package/templates/PILLARS.md +2 -0
- package/templates/backlog.md +2 -5
- package/templates/cnlp.config.json +0 -1
- package/templates/config.yml +7 -0
- package/templates/design.md +59 -0
- package/templates/game.md +24 -0
- package/templates/glossary.md +11 -0
- package/templates/item.md +58 -0
- package/templates/system.md +1 -1
- package/lib/validate-card.js +0 -82
- package/profiles/card.md +0 -65
- package/schemas/card.schema.json +0 -30
- package/templates/card.md +0 -34
package/commands/gcf.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// CLI wrapper over the deterministic core (GCF-PRD §10.1): harness and human call
|
|
2
2
|
// the same functions. Registered by AIF via extension.json `commands`.
|
|
3
|
-
import { mkdir, copyFile, readFile } from 'node:fs/promises';
|
|
3
|
+
import { mkdir, copyFile, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
6
|
import path from 'node:path';
|
|
@@ -70,8 +70,29 @@ function aif(args) {
|
|
|
70
70
|
|
|
71
71
|
const today = () => new Date().toISOString().slice(0, 10);
|
|
72
72
|
|
|
73
|
+
const UNION = [
|
|
74
|
+
'gcf/history.md merge=union',
|
|
75
|
+
'gcf/graveyard.md merge=union',
|
|
76
|
+
'gcf/inbox.md merge=union',
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Two branches that end different lives may add their lines at the same spot of history.md,
|
|
81
|
+
* graveyard.md or inbox.md: a union merge keeps both instead of a conflict whose "ours" loses
|
|
82
|
+
* one. Adds the missing rules to `<dir>/.gitattributes`; a project that ran an older init gets
|
|
83
|
+
* them on re-run.
|
|
84
|
+
*/
|
|
85
|
+
export async function unionAttributes(dir) {
|
|
86
|
+
const file = path.join(dir, '.gitattributes');
|
|
87
|
+
const text = existsSync(file) ? await readFile(file, 'utf8') : '';
|
|
88
|
+
const missing = UNION.filter((rule) => !text.split(/\r?\n/).includes(rule));
|
|
89
|
+
if (!missing.length) return;
|
|
90
|
+
const sep = text && !text.endsWith('\n') ? '\n' : '';
|
|
91
|
+
await writeFile(file, `${text}${sep}${missing.join('\n')}\n`, 'utf8');
|
|
92
|
+
}
|
|
93
|
+
|
|
73
94
|
async function seedSkeleton(root) {
|
|
74
|
-
for (const dir of ['
|
|
95
|
+
for (const dir of ['design', 'work', 'systems', 'source', '.metrics']) {
|
|
75
96
|
await mkdir(path.join(root, dir), { recursive: true });
|
|
76
97
|
}
|
|
77
98
|
|
|
@@ -86,8 +107,14 @@ async function seedSkeleton(root) {
|
|
|
86
107
|
if (!existsSync(target)) await copyFile(path.join(EXT_ROOT, 'templates', tpl), target);
|
|
87
108
|
}
|
|
88
109
|
|
|
110
|
+
const { HISTORY_HEAD, GRAVEYARD_HEAD } = await lib('verdict-fs');
|
|
111
|
+
const { INBOX_HEAD } = await lib('inbox');
|
|
112
|
+
const { serializeLive } = await lib('live');
|
|
89
113
|
for (const [name, header] of [
|
|
90
|
-
['
|
|
114
|
+
['history.md', HISTORY_HEAD],
|
|
115
|
+
['graveyard.md', GRAVEYARD_HEAD],
|
|
116
|
+
['inbox.md', INBOX_HEAD],
|
|
117
|
+
['LIVE.md', serializeLive({ capabilities: [], knownIssues: [] })],
|
|
91
118
|
[
|
|
92
119
|
'open-questions.md',
|
|
93
120
|
'# Open questions — records with id/lens/sections/sys/class/question/proposed, ' +
|
|
@@ -95,13 +122,13 @@ async function seedSkeleton(root) {
|
|
|
95
122
|
],
|
|
96
123
|
]) {
|
|
97
124
|
const target = path.join(root, name);
|
|
98
|
-
if (!existsSync(target))
|
|
99
|
-
await (await import('node:fs/promises')).writeFile(target, header, 'utf8');
|
|
125
|
+
if (!existsSync(target)) await writeFile(target, header, 'utf8');
|
|
100
126
|
}
|
|
101
127
|
|
|
102
128
|
const cnlpCfg = path.join(process.cwd(), 'cnlp.config.json');
|
|
103
129
|
if (!existsSync(cnlpCfg))
|
|
104
130
|
await copyFile(path.join(EXT_ROOT, 'templates', 'cnlp.config.json'), cnlpCfg);
|
|
131
|
+
await unionAttributes(process.cwd());
|
|
105
132
|
}
|
|
106
133
|
|
|
107
134
|
async function offerHarness(program, opts) {
|
|
@@ -134,8 +161,8 @@ function registerInit(gcf, program) {
|
|
|
134
161
|
const { writeIndex } = await lib('index-gen');
|
|
135
162
|
await writeIndex(root);
|
|
136
163
|
console.log(
|
|
137
|
-
'gcf/ ready. Write PILLARS.md through /gcf-init, then
|
|
138
|
-
'or
|
|
164
|
+
'gcf/ ready. Write PILLARS.md through /gcf-init, then import a GDD with /gcf-ingest ' +
|
|
165
|
+
'or start the design with /gcf-vision.',
|
|
139
166
|
);
|
|
140
167
|
await offerHarness(program, opts);
|
|
141
168
|
});
|
|
@@ -155,19 +182,19 @@ function registerIndex(gcf) {
|
|
|
155
182
|
|
|
156
183
|
function registerContext(gcf) {
|
|
157
184
|
gcf
|
|
158
|
-
.command('context <
|
|
159
|
-
.description('Assemble the readset for a
|
|
185
|
+
.command('context <item-id>')
|
|
186
|
+
.description('Assemble the readset for a work item (GCF-PRD §9.1)')
|
|
160
187
|
.option('--json', 'machine output')
|
|
161
|
-
.action(async (
|
|
188
|
+
.action(async (itemId, opts) => {
|
|
162
189
|
const { buildContext } = await lib('context');
|
|
163
|
-
const ctx = await buildContext(process.cwd(),
|
|
190
|
+
const ctx = await buildContext(process.cwd(), itemId);
|
|
164
191
|
// Metric 1 (GCF-PRD §14): readset size over time, one csv line per call.
|
|
165
192
|
const { appendFile, mkdir } = await import('node:fs/promises');
|
|
166
193
|
const metricsDir = path.join(process.cwd(), 'gcf', '.metrics');
|
|
167
194
|
await mkdir(metricsDir, { recursive: true });
|
|
168
195
|
await appendFile(
|
|
169
196
|
path.join(metricsDir, 'context-log.csv'),
|
|
170
|
-
`${today()},${
|
|
197
|
+
`${today()},${itemId},${ctx.files.length},${ctx.total_tokens_est}\n`,
|
|
171
198
|
'utf8',
|
|
172
199
|
);
|
|
173
200
|
out(opts.json, ctx, (c) =>
|
|
@@ -191,9 +218,10 @@ function registerAudit(gcf) {
|
|
|
191
218
|
const report = await audit(process.cwd());
|
|
192
219
|
// Metric 2 (GCF-PRD §14): findings per audit run.
|
|
193
220
|
const findings =
|
|
194
|
-
report.
|
|
221
|
+
report.stale_waiting.length +
|
|
195
222
|
report.tune_limit.length +
|
|
196
223
|
report.until.filter((u) => u.status === 'stale').length +
|
|
224
|
+
report.stale_questions.length +
|
|
197
225
|
Object.keys(report.linter).length;
|
|
198
226
|
const { appendFile, mkdir } = await import('node:fs/promises');
|
|
199
227
|
const metricsDir = path.join(process.cwd(), 'gcf', '.metrics');
|
|
@@ -214,7 +242,7 @@ function registerAudit(gcf) {
|
|
|
214
242
|
function registerPlaytest(gcf) {
|
|
215
243
|
gcf
|
|
216
244
|
.command('playtest')
|
|
217
|
-
.description('List
|
|
245
|
+
.description('List items awaiting a verdict and unplayed ones, with their questions')
|
|
218
246
|
.option('--json', 'machine output')
|
|
219
247
|
.action(async (opts) => {
|
|
220
248
|
const { gcfRoot } = await lib('gcf-paths');
|
|
@@ -222,29 +250,60 @@ function registerPlaytest(gcf) {
|
|
|
222
250
|
const list = await playtestList(gcfRoot());
|
|
223
251
|
out(opts.json, list, (l) =>
|
|
224
252
|
l.length
|
|
225
|
-
? l.map((c) => `${c.id} ${c.
|
|
226
|
-
: 'nothing
|
|
253
|
+
? l.map((c) => `${c.id} (${c.status}) [${c.system.join(',')}]: ${c.question}`).join('\n')
|
|
254
|
+
: 'nothing waits for a playtest',
|
|
227
255
|
);
|
|
228
256
|
});
|
|
229
257
|
}
|
|
230
258
|
|
|
259
|
+
const shortHash = (h) => (h ? ` (${h.slice(0, 7)})` : '');
|
|
260
|
+
const removalLine = (r) => (r.removal ? `\nremoval item: gcf/work/${r.removal}.md` : '');
|
|
261
|
+
|
|
262
|
+
/** `--remove` / `--no-remove` / neither (the move's default) for a move that may bury code. */
|
|
263
|
+
const removeOptions = (cmd) =>
|
|
264
|
+
cmd
|
|
265
|
+
.option('--remove', 'also create a removal item: the code is in main')
|
|
266
|
+
.option('--no-remove', 'no removal item: the code never reached main');
|
|
267
|
+
|
|
231
268
|
function registerVerdict(gcf) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
269
|
+
removeOptions(
|
|
270
|
+
gcf
|
|
271
|
+
.command('verdict <item-id> <verdict>')
|
|
272
|
+
.description('Apply a human verdict: keep | tune | kill (Methodology §4.1)')
|
|
273
|
+
.option('--note <text>', 'tune remark or kill reason')
|
|
274
|
+
.option('--tags <list>', 'graveyard tags on kill, comma-separated (default: its systems)')
|
|
275
|
+
.option(
|
|
276
|
+
'--constraint <line>',
|
|
277
|
+
'keep of a hard unit: a line every reader of the subsystem must remember',
|
|
278
|
+
(v, p) => [...p, v],
|
|
279
|
+
[],
|
|
280
|
+
),
|
|
281
|
+
)
|
|
236
282
|
.option('--no-commit', 'skip the verdict commit (tests only)')
|
|
237
|
-
.action(async (
|
|
238
|
-
const {
|
|
239
|
-
const res = await
|
|
283
|
+
.action(async (itemId, verdict, opts) => {
|
|
284
|
+
const { verdictItem } = await lib('work-ops');
|
|
285
|
+
const res = await verdictItem(process.cwd(), itemId, verdict, {
|
|
240
286
|
note: opts.note ?? '',
|
|
287
|
+
tags: splitList(opts.tags),
|
|
288
|
+
remove: opts.remove,
|
|
289
|
+
constraints: opts.constraint,
|
|
241
290
|
commit: opts.commit,
|
|
242
291
|
});
|
|
243
|
-
|
|
244
|
-
|
|
292
|
+
console.log(
|
|
293
|
+
`${res.verdict} ${res.id} → ${res.systems.join(',')}${shortHash(res.hash)}` +
|
|
294
|
+
removalLine(res),
|
|
295
|
+
);
|
|
245
296
|
});
|
|
246
297
|
}
|
|
247
298
|
|
|
299
|
+
const splitList = (v) =>
|
|
300
|
+
v
|
|
301
|
+
? String(v)
|
|
302
|
+
.split(',')
|
|
303
|
+
.map((s) => s.trim())
|
|
304
|
+
.filter(Boolean)
|
|
305
|
+
: [];
|
|
306
|
+
|
|
248
307
|
function anchorDeltaLine(r) {
|
|
249
308
|
if (r.delta) {
|
|
250
309
|
const { added, changed, removed, renamed } = r.delta;
|
|
@@ -267,19 +326,23 @@ function registerAnchor(gcf) {
|
|
|
267
326
|
)
|
|
268
327
|
.option('--json', 'machine output')
|
|
269
328
|
.action(async (file, opts) => {
|
|
270
|
-
const
|
|
329
|
+
const anchors = await lib('anchors');
|
|
330
|
+
const { splitSections, importSource, loadAnchorMap, classifyDelta } = anchors;
|
|
271
331
|
const base = path.basename(file);
|
|
272
332
|
const oldMap = await loadAnchorMap(process.cwd(), base);
|
|
273
333
|
const { readFile } = await import('node:fs/promises');
|
|
274
334
|
const sections = splitSections(await readFile(file, 'utf8'));
|
|
275
335
|
const delta = oldMap?.anchors ? classifyDelta(oldMap.anchors, sections) : null;
|
|
276
336
|
await importSource(process.cwd(), file);
|
|
337
|
+
// A renamed heading moves its anchor: provenance and source: follow it.
|
|
338
|
+
const rewritten = delta ? await anchors.rewriteAnchors(process.cwd(), delta.renamed) : [];
|
|
277
339
|
const rebaselined = Boolean(oldMap?.rebaselined);
|
|
278
340
|
const result = {
|
|
279
341
|
file: `gcf/source/${base}`,
|
|
280
342
|
sections: sections.map((s) => ({ anchor: s.anchor, heading: s.heading, lines: s.lines })),
|
|
281
343
|
delta,
|
|
282
344
|
rebaselined,
|
|
345
|
+
rewritten,
|
|
283
346
|
};
|
|
284
347
|
out(opts.json, result, (r) =>
|
|
285
348
|
[
|
|
@@ -287,6 +350,7 @@ function registerAnchor(gcf) {
|
|
|
287
350
|
(s) => `${s.anchor} L${s.lines[0]}-${s.lines[1]} ${s.heading ?? '(preamble)'}`,
|
|
288
351
|
),
|
|
289
352
|
anchorDeltaLine(r),
|
|
353
|
+
...r.rewritten.map((f) => `renamed anchors rewritten in gcf/${f}`),
|
|
290
354
|
].join('\n'),
|
|
291
355
|
);
|
|
292
356
|
});
|
|
@@ -308,18 +372,367 @@ function registerSection(gcf) {
|
|
|
308
372
|
function registerQuotes(gcf) {
|
|
309
373
|
gcf
|
|
310
374
|
.command('quotes')
|
|
311
|
-
.description('
|
|
375
|
+
.description('Provenance only: every quote verbatim in its src: section (design check M9)')
|
|
312
376
|
.option('--json', 'machine output')
|
|
313
377
|
.action(async (opts) => {
|
|
314
|
-
const {
|
|
315
|
-
const problems = await
|
|
378
|
+
const { designCheck } = await lib('design-check');
|
|
379
|
+
const problems = (await designCheck(process.cwd())).issues.filter((i) => i.check === 'M9');
|
|
316
380
|
out(opts.json, problems, (l) =>
|
|
317
|
-
l.length ? l.map((p) => `${p.
|
|
381
|
+
l.length ? l.map((p) => `${p.file}:${p.line} ${p.id}: ${p.message}`).join('\n') : 'clean',
|
|
318
382
|
);
|
|
319
383
|
if (problems.length) process.exitCode = 1;
|
|
320
384
|
});
|
|
321
385
|
}
|
|
322
386
|
|
|
387
|
+
const issueLine = (i) => `${i.severity} ${i.check} ${i.file}:${i.line} ${i.id ?? ''} ${i.message}`;
|
|
388
|
+
|
|
389
|
+
function designSummary(r) {
|
|
390
|
+
const rows = Object.entries(r.systems).map(
|
|
391
|
+
([s, t]) => `${s}: ${t.units} units, ${t.ready} ready, ${t.tbd}/${t.values} TBD`,
|
|
392
|
+
);
|
|
393
|
+
const blocking = r.issues.filter((i) => i.severity === 'block');
|
|
394
|
+
const notes = r.issues.filter((i) => i.severity === 'report');
|
|
395
|
+
|
|
396
|
+
return [
|
|
397
|
+
...blocking.map(issueLine),
|
|
398
|
+
...notes.map(issueLine),
|
|
399
|
+
...rows,
|
|
400
|
+
blocking.length ? `${blocking.length} blocking finding(s)` : 'no blocking findings',
|
|
401
|
+
].join('\n');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function registerDesign(gcf) {
|
|
405
|
+
const design = gcf.command('design').description('Design Model: units in gcf/design/');
|
|
406
|
+
design
|
|
407
|
+
.command('check')
|
|
408
|
+
.description(
|
|
409
|
+
'Mechanical checks over gcf/design/ and the work items; exit 1 on a blocking finding',
|
|
410
|
+
)
|
|
411
|
+
.option('--json', 'machine output')
|
|
412
|
+
.action(async (opts) => {
|
|
413
|
+
const { designCheck } = await lib('design-check');
|
|
414
|
+
const report = await designCheck(process.cwd());
|
|
415
|
+
out(opts.json, report, designSummary);
|
|
416
|
+
if (report.blocking) process.exitCode = 1;
|
|
417
|
+
});
|
|
418
|
+
design
|
|
419
|
+
.command('export')
|
|
420
|
+
.description('Glue game.md, glossary.md and the units into one bundle a designer edits')
|
|
421
|
+
.option('--system <name>', 'only this subsystem')
|
|
422
|
+
.option('--milestone <name>', 'only the units of this milestone')
|
|
423
|
+
.option('--out <file>', 'write the bundle here instead of stdout')
|
|
424
|
+
.action(async (opts) => {
|
|
425
|
+
const { exportBundle } = await lib('bundle');
|
|
426
|
+
const text = await exportBundle(process.cwd(), opts);
|
|
427
|
+
if (!opts.out) return void process.stdout.write(text);
|
|
428
|
+
const { writeFile } = await import('node:fs/promises');
|
|
429
|
+
await writeFile(opts.out, text, 'utf8');
|
|
430
|
+
console.log(`bundle written → ${opts.out}`);
|
|
431
|
+
});
|
|
432
|
+
design
|
|
433
|
+
.command('import <bundle>')
|
|
434
|
+
.description('Bring an edited bundle back: 3-way by base hash, conflicts reported, no commit')
|
|
435
|
+
.option('--json', 'machine output')
|
|
436
|
+
.action(async (bundle, opts) => {
|
|
437
|
+
const { importBundle } = await lib('bundle');
|
|
438
|
+
const r = await importBundle(process.cwd(), await readFile(bundle, 'utf8'));
|
|
439
|
+
out(opts.json, r, (x) =>
|
|
440
|
+
[
|
|
441
|
+
...x.applied.map((a) => `applied ${a}`),
|
|
442
|
+
...x.report.map((p) => `${p.outcome} ${p.part}: ${p.note}`),
|
|
443
|
+
x.applied.length || x.report.length ? '' : 'nothing changed',
|
|
444
|
+
]
|
|
445
|
+
.filter(Boolean)
|
|
446
|
+
.join('\n'),
|
|
447
|
+
);
|
|
448
|
+
if (r.report.some((p) => p.outcome !== 'deleted')) process.exitCode = 1;
|
|
449
|
+
});
|
|
450
|
+
registerMigrate(design);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function registerMigrate(design) {
|
|
454
|
+
design
|
|
455
|
+
.command('migrate')
|
|
456
|
+
.description('One-shot: backlog cand-NNN lines become units in gcf/design/ (no commit)')
|
|
457
|
+
.option(
|
|
458
|
+
'--skip <ids>',
|
|
459
|
+
'converted by hand, comma-separated: cand-NNN, cand-NNN=<id> or cand-NNN=<id>+<id>',
|
|
460
|
+
)
|
|
461
|
+
.action(async (opts) => {
|
|
462
|
+
const { migrateBacklog } = await lib('migrate');
|
|
463
|
+
const skip = opts.skip ? opts.skip.split(',').map((s) => s.trim()) : [];
|
|
464
|
+
const r = await migrateBacklog(process.cwd(), { skip });
|
|
465
|
+
for (const l of r.lost_depends) console.log(`lost depends: ${l} (map it: --skip cand=id)`);
|
|
466
|
+
for (const l of r.maybe_duplicates) console.log(`maybe a duplicate: ${l}`);
|
|
467
|
+
for (const l of r.unmoved_gaps) console.log(`check its unit holds these gaps: ${l}`);
|
|
468
|
+
console.log(
|
|
469
|
+
`${r.units} units written, ${r.skipped} skipped, ${r.history} history lines from git; ` +
|
|
470
|
+
'run ai-factory gcf design check',
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const placeLines = (x) => [...x.live.map((p) => `${p.state} gcf/${p.file}`), ...x.terminal];
|
|
476
|
+
|
|
477
|
+
function registerWhere(gcf) {
|
|
478
|
+
gcf
|
|
479
|
+
.command('where [id]')
|
|
480
|
+
.description(
|
|
481
|
+
'Where an id lives: a design section, a work item, a terminal event; no id lists all',
|
|
482
|
+
)
|
|
483
|
+
.option('--json', 'machine output')
|
|
484
|
+
.action(async (id, opts) => {
|
|
485
|
+
const { buildLedger } = await lib('ledger');
|
|
486
|
+
const ledger = await buildLedger(process.cwd());
|
|
487
|
+
// Without an id: every id the project has used, so an import never mints a taken one.
|
|
488
|
+
if (!id) {
|
|
489
|
+
const all = ledger.ids().map((i) => ledger.where(i));
|
|
490
|
+
out(opts.json, all, (l) => l.map((x) => `${x.id}: ${placeLines(x).join(', ')}`).join('\n'));
|
|
491
|
+
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
out(opts.json, ledger.where(id), (x) => placeLines(x).join('\n') || `${id} is unknown`);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function registerMoves(gcf) {
|
|
500
|
+
const ops = () => lib('unit-ops');
|
|
501
|
+
gcf
|
|
502
|
+
.command('take <ids...>')
|
|
503
|
+
.description('Move ready units from gcf/design/ into work items gcf/work/<id>.md (no commit)')
|
|
504
|
+
.option('--feature <id>', 'group them under this feature item, new or live')
|
|
505
|
+
.action(async (ids, o) => {
|
|
506
|
+
const r = await (await ops()).takeUnits(process.cwd(), ids, { feature: o.feature ?? null });
|
|
507
|
+
console.log(r.items.join('\n'));
|
|
508
|
+
});
|
|
509
|
+
gcf
|
|
510
|
+
.command('done <id>')
|
|
511
|
+
.description('A hard unit is built and tested: it leaves gcf/design/ (commit gcf(done))')
|
|
512
|
+
.option(
|
|
513
|
+
'--constraint <line>',
|
|
514
|
+
'a line every reader of the subsystem must remember',
|
|
515
|
+
(v, p) => [...p, v],
|
|
516
|
+
[],
|
|
517
|
+
)
|
|
518
|
+
.option('--no-commit', 'skip the commit (tests only)')
|
|
519
|
+
.action(async (id, o) => {
|
|
520
|
+
const r = await (
|
|
521
|
+
await ops()
|
|
522
|
+
).doneUnit(process.cwd(), id, { constraints: o.constraint, commit: o.commit });
|
|
523
|
+
console.log(`done ${r.id}${r.hash ? ` (${r.hash.slice(0, 7)})` : ''}`);
|
|
524
|
+
});
|
|
525
|
+
removeOptions(
|
|
526
|
+
gcf
|
|
527
|
+
.command('drop <id>')
|
|
528
|
+
.description(
|
|
529
|
+
'Drop a unit or a work item: a graveyard line keeps the reason (commit gcf(drop))',
|
|
530
|
+
)
|
|
531
|
+
.requiredOption('--reason <text>', 'why it dies')
|
|
532
|
+
.option('--tags <list>', 'graveyard tags, comma-separated (default: its systems)'),
|
|
533
|
+
)
|
|
534
|
+
.option('--no-commit', 'skip the commit (tests only)')
|
|
535
|
+
.action(async (id, o) => {
|
|
536
|
+
const tags = splitList(o.tags);
|
|
537
|
+
const { buildLedger } = await lib('ledger');
|
|
538
|
+
const isItem = (await buildLedger(process.cwd())).item(id);
|
|
539
|
+
const r = isItem
|
|
540
|
+
? await (
|
|
541
|
+
await lib('work-ops')
|
|
542
|
+
).dropItem(process.cwd(), id, {
|
|
543
|
+
reason: o.reason,
|
|
544
|
+
tags,
|
|
545
|
+
remove: o.remove,
|
|
546
|
+
commit: o.commit,
|
|
547
|
+
})
|
|
548
|
+
: await (
|
|
549
|
+
await ops()
|
|
550
|
+
).dropUnit(process.cwd(), id, { reason: o.reason, tags, commit: o.commit });
|
|
551
|
+
console.log(`dropped ${r.id}${shortHash(r.hash)}${removalLine(r)}`);
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function registerItem(gcf) {
|
|
556
|
+
const item = gcf.command('item').description('Work items in gcf/work/ (Methodology §3)');
|
|
557
|
+
item
|
|
558
|
+
.command('new <id>')
|
|
559
|
+
.description('Create a work item in ready (or blocked), no commit; a design unit uses take')
|
|
560
|
+
.requiredOption(
|
|
561
|
+
'--type <type>',
|
|
562
|
+
'feature | tech | bug | spike | asset | content | perf | release',
|
|
563
|
+
)
|
|
564
|
+
.option('--gate <gate>', 'verdict | auto | decision (default: by type)')
|
|
565
|
+
.option('--title <text>', 'one line: what the item gives')
|
|
566
|
+
.option('--parent <feature>', 'a live feature item this one belongs to')
|
|
567
|
+
.option('--system <list>', 'subsystems it touches, comma-separated')
|
|
568
|
+
.option('--depends <list>', 'ids it waits for, comma-separated')
|
|
569
|
+
.option('--severity <level>', 'a bug: critical | high | medium | low')
|
|
570
|
+
.option('--blocked <why>', 'start blocked, e.g. "file needed" for an asset')
|
|
571
|
+
.option('--from <in-id>', 'the inbox entry it comes from: erased')
|
|
572
|
+
.option('--reopens <id>', 'the ended id this item reworks')
|
|
573
|
+
.action(async (id, o) => {
|
|
574
|
+
const { newItem } = await lib('work-ops');
|
|
575
|
+
const r = await newItem(process.cwd(), {
|
|
576
|
+
id,
|
|
577
|
+
type: o.type,
|
|
578
|
+
gate: o.gate,
|
|
579
|
+
title: o.title,
|
|
580
|
+
parent: o.parent,
|
|
581
|
+
system: splitList(o.system),
|
|
582
|
+
depends: splitList(o.depends),
|
|
583
|
+
severity: o.severity,
|
|
584
|
+
blocked: o.blocked,
|
|
585
|
+
from: o.from,
|
|
586
|
+
reopens: o.reopens,
|
|
587
|
+
});
|
|
588
|
+
console.log(r.item);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function registerItemMoves(gcf) {
|
|
593
|
+
const ops = () => lib('work-ops');
|
|
594
|
+
gcf
|
|
595
|
+
.command('submit <id>')
|
|
596
|
+
.description('ready → awaiting-verdict / awaiting-decision (commit gcf(submit))')
|
|
597
|
+
.option('--no-commit', 'skip the commit (tests only)')
|
|
598
|
+
.action(async (id, o) => {
|
|
599
|
+
const r = await (await ops()).submitItem(process.cwd(), id, { commit: o.commit });
|
|
600
|
+
console.log(`${r.id} → ${r.status}${shortHash(r.hash)}`);
|
|
601
|
+
});
|
|
602
|
+
gcf
|
|
603
|
+
.command('close <id>')
|
|
604
|
+
.description('A gate-auto item passed its DoD: it ends, or a soft unit goes unplayed')
|
|
605
|
+
.option(
|
|
606
|
+
'--constraint <line>',
|
|
607
|
+
'a hard unit: a line every reader of the subsystem must remember',
|
|
608
|
+
(v, p) => [...p, v],
|
|
609
|
+
[],
|
|
610
|
+
)
|
|
611
|
+
.option('--no-commit', 'skip the commit (tests only)')
|
|
612
|
+
.action(async (id, o) => {
|
|
613
|
+
const r = await (
|
|
614
|
+
await ops()
|
|
615
|
+
).closeItem(process.cwd(), id, { constraints: o.constraint, commit: o.commit });
|
|
616
|
+
console.log(`closed ${r.id}${r.unplayed ? ' → unplayed' : ''}${shortHash(r.hash)}`);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function registerDecide(gcf) {
|
|
621
|
+
const ops = () => lib('work-ops');
|
|
622
|
+
removeOptions(
|
|
623
|
+
gcf
|
|
624
|
+
.command('decide <id> <choice>')
|
|
625
|
+
.description('go | no | wontfix | known | close | continue | kill (Methodology §4.1)')
|
|
626
|
+
.option('--reason <text>', 'why: the graveyard keeps it'),
|
|
627
|
+
)
|
|
628
|
+
.option('--no-commit', 'skip the commit (tests only)')
|
|
629
|
+
.action(async (id, choice, o) => {
|
|
630
|
+
const r = await (
|
|
631
|
+
await ops()
|
|
632
|
+
).decideItem(process.cwd(), id, choice, {
|
|
633
|
+
reason: o.reason ?? '',
|
|
634
|
+
remove: o.remove,
|
|
635
|
+
commit: o.commit,
|
|
636
|
+
});
|
|
637
|
+
console.log(`${r.choice} ${r.id}${shortHash(r.hash)}${removalLine(r)}`);
|
|
638
|
+
});
|
|
639
|
+
gcf
|
|
640
|
+
.command('block <id>')
|
|
641
|
+
.description('ready → blocked, with the reason or the question (commit gcf(block))')
|
|
642
|
+
.requiredOption('--reason <text>', 'why it cannot go on, or the question for you')
|
|
643
|
+
.option('--no-commit', 'skip the commit (tests only)')
|
|
644
|
+
.action(async (id, o) => {
|
|
645
|
+
const r = await (
|
|
646
|
+
await ops()
|
|
647
|
+
).blockItem(process.cwd(), id, { reason: o.reason, commit: o.commit });
|
|
648
|
+
console.log(`blocked ${r.id}${shortHash(r.hash)}`);
|
|
649
|
+
});
|
|
650
|
+
gcf
|
|
651
|
+
.command('unblock <id>')
|
|
652
|
+
.description('blocked | parked → ready (commit gcf(unblock))')
|
|
653
|
+
.option('--note <text>', 'the answer, or "file: <path>" for an asset')
|
|
654
|
+
.option('--no-commit', 'skip the commit (tests only)')
|
|
655
|
+
.action(async (id, o) => {
|
|
656
|
+
const r = await (
|
|
657
|
+
await ops()
|
|
658
|
+
).unblockItem(process.cwd(), id, { note: o.note ?? '', commit: o.commit });
|
|
659
|
+
console.log(`unblocked ${r.id}${shortHash(r.hash)}`);
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function registerInbox(gcf) {
|
|
664
|
+
const inbox = gcf
|
|
665
|
+
.command('inbox')
|
|
666
|
+
.description('Raw input waiting for triage (gcf/inbox.md); no commit')
|
|
667
|
+
.option('--json', 'machine output')
|
|
668
|
+
.action(async (o) => {
|
|
669
|
+
const { gcfRoot } = await lib('gcf-paths');
|
|
670
|
+
const list = await (await lib('inbox')).listInbox(gcfRoot());
|
|
671
|
+
out(o.json, list, (l) =>
|
|
672
|
+
l.length ? l.map((e) => `${e.id} [${e.source}] ${e.text}`).join('\n') : 'inbox empty',
|
|
673
|
+
);
|
|
674
|
+
});
|
|
675
|
+
inbox
|
|
676
|
+
.command('add <text>')
|
|
677
|
+
.description('Append an entry')
|
|
678
|
+
.option('--source <source>', 'playtest | ci | agent | operator', 'operator')
|
|
679
|
+
.action(async (text, o) => {
|
|
680
|
+
const { gcfRoot } = await lib('gcf-paths');
|
|
681
|
+
const e = await (await lib('inbox')).addInbox(gcfRoot(), text, o.source);
|
|
682
|
+
console.log(e.id);
|
|
683
|
+
});
|
|
684
|
+
inbox
|
|
685
|
+
.command('drop <in-id>')
|
|
686
|
+
.description('Erase an entry: triaged as noise, a duplicate, or turned into an item')
|
|
687
|
+
.action(async (id) => {
|
|
688
|
+
const { gcfRoot } = await lib('gcf-paths');
|
|
689
|
+
await (await lib('inbox')).dropInbox(gcfRoot(), id);
|
|
690
|
+
console.log(`erased ${id}`);
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function registerLive(gcf) {
|
|
695
|
+
const live = gcf
|
|
696
|
+
.command('live')
|
|
697
|
+
.description('What the game is now: gcf/LIVE.md')
|
|
698
|
+
.option('--json', 'machine output')
|
|
699
|
+
.action(async (o) => {
|
|
700
|
+
const { gcfRoot } = await lib('gcf-paths');
|
|
701
|
+
const l = await (await lib('live')).readLive(gcfRoot());
|
|
702
|
+
const rows = (list) => list.map((r) => ` ${r.id}: ${r.summary}`);
|
|
703
|
+
out(o.json, l, (x) =>
|
|
704
|
+
['capabilities:', ...rows(x.capabilities), 'known-issues:', ...rows(x.knownIssues)].join(
|
|
705
|
+
'\n',
|
|
706
|
+
),
|
|
707
|
+
);
|
|
708
|
+
});
|
|
709
|
+
live
|
|
710
|
+
.command('check')
|
|
711
|
+
.description('Record form, refs that name no file, known issues against parked bugs')
|
|
712
|
+
.option('--json', 'machine output')
|
|
713
|
+
.action(async (o) => {
|
|
714
|
+
const r = await (await lib('live')).checkLive(process.cwd());
|
|
715
|
+
out(o.json, r.issues, (l) => (l.length ? l.join('\n') : 'clean'));
|
|
716
|
+
if (r.issues.length) process.exitCode = 1;
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function registerPlan(gcf) {
|
|
721
|
+
gcf
|
|
722
|
+
.command('plan')
|
|
723
|
+
.description('The plan of a work item')
|
|
724
|
+
.command('check <id>')
|
|
725
|
+
.description('Deterministic plan check: it exists, has task checkboxes, readset in budget')
|
|
726
|
+
.option('--json', 'machine output')
|
|
727
|
+
.action(async (id, o) => {
|
|
728
|
+
const r = await (await lib('plan-check')).planCheck(process.cwd(), id);
|
|
729
|
+
out(o.json, r, (x) =>
|
|
730
|
+
x.ok ? `${x.plan}: ${x.done}/${x.tasks} tasks done` : x.problems.join('\n'),
|
|
731
|
+
);
|
|
732
|
+
if (!r.ok) process.exitCode = 1;
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
|
|
323
736
|
function registerHooksInstall(gcf) {
|
|
324
737
|
gcf
|
|
325
738
|
.command('hooks-install')
|
|
@@ -378,6 +791,15 @@ export function register(program) {
|
|
|
378
791
|
registerAnchor(gcf);
|
|
379
792
|
registerSection(gcf);
|
|
380
793
|
registerQuotes(gcf);
|
|
794
|
+
registerDesign(gcf);
|
|
795
|
+
registerWhere(gcf);
|
|
796
|
+
registerMoves(gcf);
|
|
797
|
+
registerItem(gcf);
|
|
798
|
+
registerItemMoves(gcf);
|
|
799
|
+
registerDecide(gcf);
|
|
800
|
+
registerInbox(gcf);
|
|
801
|
+
registerLive(gcf);
|
|
802
|
+
registerPlan(gcf);
|
|
381
803
|
registerHooksInstall(gcf);
|
|
382
804
|
registerLint(gcf);
|
|
383
805
|
}
|
package/extension.json
CHANGED
|
@@ -1,24 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/lee-to/ai-factory/2.x/schemas/extension.schema.json",
|
|
3
3
|
"name": "gcf-aif-ext",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"description": "Game Context Framework for AI Factory",
|
|
6
6
|
"skills": [
|
|
7
7
|
"skills/gcf-init",
|
|
8
8
|
"skills/gcf-adopt",
|
|
9
9
|
"skills/gcf-card",
|
|
10
10
|
"skills/gcf-verdict",
|
|
11
|
+
"skills/gcf-triage",
|
|
11
12
|
"skills/gcf-audit",
|
|
12
13
|
"skills/gcf-vision",
|
|
13
14
|
"skills/gcf-debate",
|
|
14
15
|
"skills/gcf-ingest"
|
|
15
16
|
],
|
|
16
17
|
"commands": [
|
|
17
|
-
{
|
|
18
|
+
{
|
|
19
|
+
"name": "gcf",
|
|
20
|
+
"description": "Game Context Framework core",
|
|
21
|
+
"module": "commands/gcf.js"
|
|
22
|
+
}
|
|
18
23
|
],
|
|
19
24
|
"injections": [
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
{
|
|
26
|
+
"target": "aif-plan",
|
|
27
|
+
"position": "append",
|
|
28
|
+
"file": "./injections/plan-context.md"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"target": "aif-review",
|
|
32
|
+
"position": "append",
|
|
33
|
+
"file": "./injections/review-pillars.md"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"target": "aif-commit",
|
|
37
|
+
"position": "append",
|
|
38
|
+
"file": "./injections/commit-status.md"
|
|
39
|
+
}
|
|
23
40
|
]
|
|
24
41
|
}
|