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.
Files changed (63) hide show
  1. package/README.md +54 -10
  2. package/commands/gcf.js +452 -30
  3. package/extension.json +22 -5
  4. package/hooks/pre-commit.js +69 -41
  5. package/injections/commit-status.md +7 -4
  6. package/injections/plan-context.md +8 -4
  7. package/injections/review-pillars.md +4 -3
  8. package/lib/anchors.js +60 -35
  9. package/lib/audit-core.js +172 -55
  10. package/lib/bundle.js +278 -0
  11. package/lib/context.js +25 -23
  12. package/lib/design-check.js +631 -0
  13. package/lib/design.js +179 -0
  14. package/lib/gcf-paths.js +13 -2
  15. package/lib/git.js +63 -0
  16. package/lib/id.js +7 -15
  17. package/lib/inbox.js +54 -0
  18. package/lib/index-gen.js +62 -13
  19. package/lib/ledger.js +94 -0
  20. package/lib/linter.js +17 -5
  21. package/lib/live.js +179 -0
  22. package/lib/migrate.js +345 -0
  23. package/lib/plan-check.js +31 -0
  24. package/lib/playtest-list.js +9 -5
  25. package/lib/systems-doc.js +85 -0
  26. package/lib/transitions.js +36 -21
  27. package/lib/unit-ops.js +297 -0
  28. package/lib/validate-item.js +244 -0
  29. package/lib/verdict-fs.js +112 -165
  30. package/lib/work-ops.js +727 -0
  31. package/package.json +1 -1
  32. package/profiles/game.md +68 -0
  33. package/profiles/glossary.md +37 -0
  34. package/profiles/pillars.md +3 -2
  35. package/profiles/system.md +1 -1
  36. package/profiles/unit-constraint.md +52 -0
  37. package/profiles/unit-entity.md +48 -0
  38. package/profiles/unit-mechanic.md +111 -0
  39. package/profiles/unit-resource.md +91 -0
  40. package/profiles/unit-rule.md +52 -0
  41. package/schemas/item.schema.json +105 -0
  42. package/skills/gcf-adopt/SKILL.md +3 -3
  43. package/skills/gcf-audit/SKILL.md +7 -6
  44. package/skills/gcf-card/SKILL.md +31 -17
  45. package/skills/gcf-debate/SKILL.md +17 -14
  46. package/skills/gcf-ingest/SKILL.md +39 -23
  47. package/skills/gcf-init/SKILL.md +5 -5
  48. package/skills/gcf-triage/SKILL.md +35 -0
  49. package/skills/gcf-verdict/SKILL.md +19 -15
  50. package/skills/gcf-vision/SKILL.md +17 -11
  51. package/templates/PILLARS.md +2 -0
  52. package/templates/backlog.md +2 -5
  53. package/templates/cnlp.config.json +0 -1
  54. package/templates/config.yml +7 -0
  55. package/templates/design.md +59 -0
  56. package/templates/game.md +24 -0
  57. package/templates/glossary.md +11 -0
  58. package/templates/item.md +58 -0
  59. package/templates/system.md +1 -1
  60. package/lib/validate-card.js +0 -82
  61. package/profiles/card.md +0 -65
  62. package/schemas/card.schema.json +0 -30
  63. 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 ['cards', 'systems', 'source', '.metrics']) {
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
- ['graveyard.md', '# Graveyard — one line per killed idea, written by gcf verdict kill\n'],
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 add candidates to backlog.md ' +
138
- 'or import a GDD with /gcf-ingest.',
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 <card-id>')
159
- .description('Assemble the readset for a card (GCF-PRD §9.1)')
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 (cardId, opts) => {
188
+ .action(async (itemId, opts) => {
162
189
  const { buildContext } = await lib('context');
163
- const ctx = await buildContext(process.cwd(), cardId);
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()},${cardId},${ctx.files.length},${ctx.total_tokens_est}\n`,
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.stale_built.length +
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 built cards with their miss-criterion questions')
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.slug} [${c.system.join(',')}]: ${c.question}`).join('\n')
226
- : 'nothing in built — nothing to playtest',
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
- gcf
233
- .command('verdict <card-id> <verdict>')
234
- .description('Apply a human verdict: keep | tune | kill (GCF-PRD §8)')
235
- .option('--note <text>', 'tune remark or kill reason')
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 (cardId, verdict, opts) => {
238
- const { applyVerdict } = await lib('verdict-fs');
239
- const res = await applyVerdict(process.cwd(), cardId, verdict, {
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
- const hash = res.hash ? ` (${res.hash.slice(0, 7)})` : '';
244
- console.log(`${res.verdict} ${res.card} → ${res.systems.join(',')}${hash}`);
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 { splitSections, importSource, loadAnchorMap, classifyDelta } = await lib('anchors');
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('Verify each backlog q: quote is verbatim in its src: section (GCF-PRD §11.2)')
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 { checkQuotes } = await lib('anchors');
315
- const problems = await checkQuotes(process.cwd());
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.id} ${p.anchor}: ${p.problem}`).join('\n') : 'clean',
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.2.7",
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
- { "name": "gcf", "description": "Game Context Framework core", "module": "commands/gcf.js" }
18
+ {
19
+ "name": "gcf",
20
+ "description": "Game Context Framework core",
21
+ "module": "commands/gcf.js"
22
+ }
18
23
  ],
19
24
  "injections": [
20
- { "target": "aif-plan", "position": "append", "file": "./injections/plan-context.md" },
21
- { "target": "aif-review", "position": "append", "file": "./injections/review-pillars.md" },
22
- { "target": "aif-commit", "position": "append", "file": "./injections/commit-status.md" }
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
  }