gcf-aif-ext 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +63 -11
  2. package/commands/gcf.js +321 -43
  3. package/extension.json +22 -5
  4. package/hooks/pre-commit.js +60 -31
  5. package/injections/commit-status.md +7 -5
  6. package/injections/plan-context.md +8 -5
  7. package/injections/review-pillars.md +4 -3
  8. package/lib/agents-block.js +66 -0
  9. package/lib/anchors.js +27 -4
  10. package/lib/audit-core.js +135 -48
  11. package/lib/bundle.js +7 -4
  12. package/lib/context.js +33 -25
  13. package/lib/design-check.js +149 -40
  14. package/lib/design.js +2 -2
  15. package/lib/gcf-paths.js +13 -2
  16. package/lib/id.js +2 -2
  17. package/lib/inbox.js +54 -0
  18. package/lib/index-gen.js +23 -8
  19. package/lib/ledger.js +19 -17
  20. package/lib/linter.js +2 -1
  21. package/lib/live.js +191 -0
  22. package/lib/pillars.js +113 -0
  23. package/lib/plan-check.js +31 -0
  24. package/lib/playtest-list.js +8 -4
  25. package/lib/systems-doc.js +16 -1
  26. package/lib/transitions.js +36 -21
  27. package/lib/unit-ops.js +125 -97
  28. package/lib/validate-item.js +244 -0
  29. package/lib/verdict-fs.js +67 -128
  30. package/lib/work-ops.js +759 -0
  31. package/package.json +4 -3
  32. package/profiles/pillars.md +1 -1
  33. package/profiles/system.md +1 -1
  34. package/profiles/unit-constraint.md +10 -0
  35. package/profiles/unit-entity.md +10 -0
  36. package/profiles/unit-mechanic.md +6 -1
  37. package/profiles/unit-resource.md +6 -1
  38. package/profiles/unit-rule.md +10 -0
  39. package/schemas/item.schema.json +105 -0
  40. package/skills/gcf-adopt/SKILL.md +2 -2
  41. package/skills/gcf-audit/SKILL.md +3 -3
  42. package/skills/gcf-card/SKILL.md +27 -20
  43. package/skills/gcf-ingest/SKILL.md +3 -3
  44. package/skills/gcf-init/SKILL.md +8 -5
  45. package/skills/gcf-triage/SKILL.md +35 -0
  46. package/skills/gcf-verdict/SKILL.md +19 -16
  47. package/templates/agents-block.md +28 -0
  48. package/templates/config.yml +7 -0
  49. package/templates/{card.md → item.md} +10 -3
  50. package/lib/validate-card.js +0 -93
  51. package/schemas/card.schema.json +0 -46
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  The Game Context Framework as an [AI Factory](https://github.com/lee-to/ai-factory) extension: a documentary layer for building games with an AI agent, where the documents hold **state instead of history**.
4
4
 
5
- A Design Model: the design lives in `gcf/design/<system>.md` as units with typed slots — what the GDD says, quoted and anchored, and `TBD` for everything it does not. A soft unit taken into work becomes a feature card with the same body; the card collapses into its subsystem's document on a human playtest verdict and then ceases to exist. A hard unit leaves through its tests. A graveyard keeps the reasons things were killed. Read rules keep the token cost of a task flat as the project ages.
5
+ A Design Model: the design lives in `gcf/design/<system>.md` as units with typed slots — what the GDD says, quoted and anchored, and `TBD` for everything it does not. Every piece of work — a mechanic, a bug, tech, an asset, a spike, a release — is a work item `gcf/work/<id>.md` with a `type` and a `gate`: who closes it, the human after play (`verdict`), a check (`auto`), or the human without a build (`decision`). A unit taken into work moves into its item with the same body; a soft unit collapses into its subsystem's document on a human playtest verdict and the item ceases to exist. `gcf/LIVE.md` is a short snapshot of what the game is now — capabilities and known issues. `gcf/inbox.md` holds raw input waiting for triage. A history and a graveyard keep how each id ended and why things were killed. Read rules keep the token cost of a task flat as the project ages.
6
+
7
+ The methodology is [Methodology.md](https://github.com/VirtualMaestro/GCF/blob/main/docs/Methodology.md) (RU).
6
8
 
7
9
  Runtime-agnostic — everything here works on any AI Factory runtime. The Claude-Code-specific orchestration lives in [`gcf-harness-aif-ext`](../gcf-harness-aif-ext), which is optional.
8
10
 
@@ -22,20 +24,20 @@ ai-factory extension add gcf-aif-ext # npm
22
24
  # ai-factory extension add ../GCF/packages/gcf-aif-ext # local checkout
23
25
  ```
24
26
 
25
- This installs the 8 skills into each configured runtime, registers the `gcf` command and appends the injections to `aif-plan`, `aif-review` and `aif-commit`. Then set up the project — two steps, run once:
27
+ This installs the 9 skills into each configured runtime, registers the `gcf` command and appends the injections to `aif-plan`, `aif-review` and `aif-commit`. Then set up the project — two steps, run once:
26
28
 
27
29
  ```bash
28
- ai-factory gcf init # seeds gcf/ and cnlp.config.json (PILLARS.md is written by /gcf-init)
30
+ ai-factory gcf init # seeds gcf/, cnlp.config.json and .gitattributes (PILLARS.md is written by /gcf-init)
29
31
  ai-factory gcf hooks-install # arms the pre-commit gate
30
32
  ```
31
33
 
32
- `gcf init` is idempotent: it creates what is missing and overwrites nothing.
34
+ `gcf init` is idempotent: it creates what is missing and overwrites nothing. Among what it seeds: `gcf/work/` for the work items, `gcf/inbox.md` for raw input, `gcf/LIVE.md` for the project snapshot, `gcf/history.md` and `gcf/graveyard.md` — the two files that record how an id's life ended — and three lines in `.gitattributes` (`gcf/history.md merge=union`, `gcf/graveyard.md merge=union`, `gcf/inbox.md merge=union`) so that two branches appending different lines to those files never conflict there.
33
35
 
34
36
  It then offers the optional [harness](../gcf-harness-aif-ext) — orchestration and a readset guard for **Claude Code only** — and on yes runs `ai-factory extension add gcf-harness-aif-ext` and `ai-factory gcf-harness init`. It asks only in a terminal and only when `.ai-factory.json` lists the Claude Code agent; an agent or CI run is never asked and installs nothing. `--harness` installs it without asking, `--no-harness` skips the question.
35
37
 
36
38
  `hooks-install` is a separate step because an AI Factory manifest can declare skills, commands and injections but cannot install a git hook — and because arming a gate that can break your commits is your call, not the extension's. It needs a git repository, and it will not overwrite a `pre-commit` it did not write: if husky or lefthook already owns the hook, it prints the one line to chain from it instead. Without the hook GCF still works; nothing enforces it at commit time.
37
39
 
38
- Then `/gcf-vision` for a brand-new game, `/gcf-adopt` for a project that already has code, `/gcf-ingest <gdd.md>` for an existing design doc, or `/gcf-card` to start a feature by hand.
40
+ Then `/gcf-vision` for a brand-new game, `/gcf-adopt` for a project that already has code, `/gcf-ingest <gdd.md>` for an existing design doc, or `/gcf-card` to start work by hand.
39
41
 
40
42
  ## Upgrade
41
43
 
@@ -44,7 +46,50 @@ ai-factory extension update gcf-aif-ext # pulls the new version
44
46
  ai-factory extension update --force # refresh even if the version is unchanged
45
47
  ```
46
48
 
47
- Updating preserves `gcf/`, `cnlp.config.json` and the pre-commit hook.
49
+ Updating preserves `gcf/`, `cnlp.config.json` and the pre-commit hook. Run `ai-factory gcf init` again after an update: it adds only what a newer version seeds and the project lacks.
50
+
51
+ On Windows, run `extension add` and `extension update` from PowerShell or cmd, not Git Bash: the installer unpacks with `tar`, and Git Bash's GNU tar reads `C:\…` as a remote host (`Cannot connect to C: resolve failed`). The Windows `tar.exe` handles it.
52
+
53
+ ### 0.4.1
54
+
55
+ A fix release over 0.4.0; update in place. What you may notice:
56
+
57
+ - An unplayed item's `frozen:` now covers its `live:` delta as well as its body. Items closed by 0.4.0 keep working. `tune`, `kill` and `drop` refuse an unplayed item edited since its close: restore the file first.
58
+ - With `PILLARS.md` present, a mechanic or resource with `pillars: TBD` is not ready to `take`. `gcf pillar rename` and `gcf pillar retire` revise a pillar where units and items cite it.
59
+ - `design check` blocks a `depends:` cycle and an id with two terminal lines (a merge of branches).
60
+ - The commit gate refuses a `LIVE.md` ref into `gcf/` that names no file.
61
+ - `gcf init` writes an agent instructions block into `AGENTS.md` (and into a `CLAUDE.md` that does not import it), between `<!-- gcf:start -->` and `<!-- gcf:end -->`. Any agent session then knows where work lives and which commands own it. Run `gcf init` again after an update to refresh it.
62
+
63
+ ### Upgrading from 0.3.x
64
+
65
+ 0.4.0 replaces the cards with work items and does **not migrate** them. A schema 2 card in `gcf/cards/` is no longer read: while any is left there, the pre-commit gate refuses every commit that touches `gcf/`, and `gcf audit` reports it first. In order:
66
+
67
+ 1. Update both extensions together — `gcf-harness-aif-ext` 0.5.0 requires this core — and run `ai-factory gcf init`: it seeds `gcf/work/`, `gcf/inbox.md`, `gcf/LIVE.md` and the `gcf/inbox.md merge=union` line. Your `config.yml` is kept; the new keys `unplayed_warn_after`, `plan_passes` and `dod_attempts` take their defaults, and `dod.test` / `dod.build` (the project's test and build commands) are yours to fill in.
68
+ 2. Re-create each card. A 0.3 card is a mechanic or a resource, which `item new` does not create: put its body back as a `## <id>` section in `gcf/design/<system>.md` with a `- <id>` line in `backlog.md`, commit, and run `ai-factory gcf take <id>`. A card that was `built` then goes `ai-factory gcf submit <id>` (its code is in main already); `tune_count` starts at 0 unless you carry it over by hand.
69
+ 3. Delete `gcf/cards/` and commit it with the new items.
70
+
71
+ What else changes: statuses are `ready`, `awaiting-verdict`, `awaiting-decision`, `blocked`, `parked` and `unplayed` (`proposed` and `tuning` are `ready`, `built` is `awaiting-verdict`); `built_at` is `since`; `submit` replaces the hand flip to `built`; `reopens:` names an ended id and rework gets a new id; a kill or drop of code already in main creates a `remove-<id>` item; graveyard lines carry the type (0.3 lines without it still read); history gains the `decided` event; every item needs a `live:` delta before it ends.
72
+
73
+ ### Upgrading from 0.3.0
74
+
75
+ 0.3.1 is a patch release with no migration step. It changes behavior in these ways:
76
+
77
+ - the pre-commit gate ignores `gcf/.metrics/`: a `gcf audit` or `gcf context` run no longer makes the next verdict, `take`, `done` or `drop` fail with `gcf/ has changes outside the index`
78
+ - `ai-factory gcf verdict <id> tune` refuses a card at `tune_limit` (`gcf/config.yml`, 3 by default): kill it, or redesign the envelope and reset `tune_count`
79
+ - `gcf audit` reports open questions older than `question_stale_after_commits` (`stale_questions`) and a `PILLARS.md` with more than 7 pillars
80
+ - a card with an unquoted numeric `id:` (YAML reads `id: 2024` as a number) is refused: quote the slug
81
+ - `built_at` is recorded in a follow-up commit after the implementation commit, with that commit's hash
82
+ - the form check is `npx --yes cnlp-kit check`; `npx cnlp check` fails on a machine without a global cnlp-kit, because no npm package is named `cnlp`
83
+ - a GDD with `#` lines inside closed code fences reports a one-time delta on its next re-import: those lines were wrongly read as headings before
84
+
85
+ ### Upgrading from 0.2.x
86
+
87
+ 0.3.0 replaces the candidate backlog with the Design Model: a card is schema 2 (`cards/<id>.md` with a slug id, `type`, `pillars`, no `slug` and no `route`; the gate names the steps for a schema 1 card), and the design lives in `gcf/design/`. In order:
88
+
89
+ 1. `ai-factory extension update` and `ai-factory gcf init` (adds `history.md` and the `.gitattributes` lines).
90
+ 2. `ai-factory gcf design migrate` — deterministic, no commit. Each `cand-NNN | …` backlog line becomes a unit: the quote goes to provenance, every slot the line did not carry reads `TBD`, every gap note survives as a `TBD`, the backlog keeps its order, and `cand-NNN` references in `open-questions.md` and `graveyard.md` are rewritten. The keeps and dones of old commit messages are copied into `gcf/history.md` once.
91
+ 3. Candidates you already turned into units by hand: `--skip cand-012=tile-placement` drops the line and sends dependencies on it to that unit; `--skip cand-014=take-back+undo-and-clear` does the same for a candidate you split in two. The command prints the dependencies it could not map, the units that look like duplicates of hand-written ones, and the gap notes of skipped candidates to check in their units.
92
+ 4. `ai-factory gcf design check`, fix what it reports — typically a digit in a description — and commit.
48
93
 
49
94
  To remove the extension:
50
95
 
@@ -56,15 +101,22 @@ Removal leaves `gcf/` and `cnlp.config.json` in place. It also leaves the pre-co
56
101
 
57
102
  ## What it adds
58
103
 
59
- **Skills** — `gcf-init`, `gcf-adopt`, `gcf-card`, `gcf-verdict`, `gcf-audit`, `gcf-vision`, `gcf-debate`, `gcf-ingest`.
104
+ **Skills** — `gcf-init`, `gcf-adopt`, `gcf-card` (put work into `gcf/work/`: take units, group them under a feature, or create a non-design item), `gcf-verdict` (verdicts and decisions), `gcf-triage` (inbox entries into items, tunes, units or nothing, with the human approving each), `gcf-audit`, `gcf-vision`, `gcf-debate`, `gcf-ingest`.
105
+
106
+ **A deterministic core** behind `ai-factory gcf …`:
107
+
108
+ - work items — `take <ids...> [--feature <id>]`, `item new <id> --type <type>`, `submit`, `close`, `verdict <id> keep|tune|kill`, `decide <id> go|no|wontfix|known|close|continue|kill`, `block`, `unblock`, `drop`, `done`, `where`
109
+ - inbox and snapshot — `inbox [add <text> | drop <in-id>]`, `live`, `live check`
110
+ - runs and play — `context`, `plan check <id>`, `playtest`, `audit`
111
+ - design and sources — `design check`, `design export`, `design import`, `design migrate`, `anchor`, `section`, `quotes`, `lint`
112
+ - setup — `init`, `index`, `hooks-install`
60
113
 
61
- **A deterministic core** behind `ai-factory gcf …` — `init`, `index`, `context`, `audit`, `playtest`, `verdict`, `anchor`, `section`, `quotes`, `lint`, `design check`, `where`, `take`, `done`, `drop`, `hooks-install`. These are plain Node scripts with no LLM in the loop: the same functions the skills call, the pre-commit gate runs, and the harness gates against.
114
+ Every move of an item makes one commit `gcf(<move>): <id>` with every file it touched — the item, history or graveyard, `LIVE.md`, the systems documents, the parent feature, a removal item, the archived plan. `take`, `item new` and `inbox` do not commit. These are plain Node scripts with no LLM in the loop: the same functions the skills call, the pre-commit gate runs, and the harness gates against.
62
115
 
63
- **A pre-commit gate** that breaks the commit on a terminal status stored in a card file, a malformed `[until:]` annotation, a card built on an unplayed dependency, a dependency that names no known id, a П2 linter finding in a subsystem document, or a blocking design-check finding: a malformed unit or card, a digit in prose, a forbidden synonym, a quote that is not verbatim in its GDD section, an id living in two places, a backlog out of step with `gcf/design/`. An unfinished unit (`TBD`) never breaks a commit. The gate regenerates `gcf/INDEX.md` and stages it for you.
116
+ **A pre-commit gate** that breaks the commit on a schema 2 card left in `gcf/cards/`, an invalid item (an unknown field, a status that is not stored or does not fit its gate or type, a file name that is not its id), a malformed `[until:]` annotation, an edited body of an `unplayed` item, a malformed `gcf/LIVE.md`, a П2 linter finding in a subsystem document, or a blocking design-check finding: a malformed unit or item, a digit in prose, a forbidden synonym, a quote that is not verbatim in its GDD section, a dependency that names no known id, an id living in two places, a backlog out of step with `gcf/design/`. An unfinished unit (`TBD`) never breaks a commit. A unit or item that leaves `gcf/design/` or `gcf/work/` without a move only draws a warning — its reason is lost, but an item still depending on it fails in that same commit. The gate regenerates `gcf/INDEX.md` and stages it for you.
64
117
 
65
- **CNLP profiles and a card schema** for the documents the agent reads — one profile per unit type, shared by a unit and its card — plus injections into `aif-plan`, `aif-review` and `aif-commit`.
118
+ **CNLP profiles and a work item schema** for the documents the agent reads — one profile per unit type, shared by a unit and its item — plus injections into `aif-plan`, `aif-review` and `aif-commit`.
66
119
 
67
- **Upgrading from 0.2.x:** a card is schema 2 now — `cards/<id>.md` with a slug id, `type`, `pillars`, no `slug` and no `route`; the gate names the steps for a schema 1 card. Backlog candidate lines (`cand-NNN | …`) become units in `gcf/design/` with `ai-factory gcf design migrate` (deterministic, no commit): it keeps the backlog order, writes the quote as provenance and every slot the line did not carry as `TBD`, rewrites `cand-NNN` references in `open-questions.md` and `graveyard.md`, and copies the keeps and dones of old commit messages into `gcf/history.md` once — the ledger reads that file, not the log. Then run `ai-factory gcf design check`, fix what it reports — typically a digit in a description — and commit.
68
120
 
69
121
  ## Docs
70
122
 
package/commands/gcf.js CHANGED
@@ -70,12 +70,17 @@ function aif(args) {
70
70
 
71
71
  const today = () => new Date().toISOString().slice(0, 10);
72
72
 
73
- const UNION = ['gcf/history.md merge=union', 'gcf/graveyard.md merge=union'];
73
+ const UNION = [
74
+ 'gcf/history.md merge=union',
75
+ 'gcf/graveyard.md merge=union',
76
+ 'gcf/inbox.md merge=union',
77
+ ];
74
78
 
75
79
  /**
76
- * Two branches that end different lives may add their lines at the same spot of history.md or
77
- * graveyard.md: a union merge keeps both instead of a conflict whose "ours" loses one. Adds the
78
- * missing rules to `<dir>/.gitattributes`; a project that ran an older init gets them on re-run.
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.
79
84
  */
80
85
  export async function unionAttributes(dir) {
81
86
  const file = path.join(dir, '.gitattributes');
@@ -87,7 +92,7 @@ export async function unionAttributes(dir) {
87
92
  }
88
93
 
89
94
  async function seedSkeleton(root) {
90
- for (const dir of ['design', 'cards', 'systems', 'source', '.metrics']) {
95
+ for (const dir of ['design', 'work', 'systems', 'source', '.metrics']) {
91
96
  await mkdir(path.join(root, dir), { recursive: true });
92
97
  }
93
98
 
@@ -102,14 +107,14 @@ async function seedSkeleton(root) {
102
107
  if (!existsSync(target)) await copyFile(path.join(EXT_ROOT, 'templates', tpl), target);
103
108
  }
104
109
 
105
- const { HISTORY_HEAD } = await lib('verdict-fs');
110
+ const { HISTORY_HEAD, GRAVEYARD_HEAD } = await lib('verdict-fs');
111
+ const { INBOX_HEAD } = await lib('inbox');
112
+ const { serializeLive } = await lib('live');
106
113
  for (const [name, header] of [
107
114
  ['history.md', HISTORY_HEAD],
108
- [
109
- 'graveyard.md',
110
- '# Graveyard — `- <id> | <reason> | tags: <tags>`, one line per idea killed by a verdict ' +
111
- 'or dropped before work\n',
112
- ],
115
+ ['graveyard.md', GRAVEYARD_HEAD],
116
+ ['inbox.md', INBOX_HEAD],
117
+ ['LIVE.md', serializeLive({ capabilities: [], knownIssues: [] })],
113
118
  [
114
119
  'open-questions.md',
115
120
  '# Open questions — records with id/lens/sections/sys/class/question/proposed, ' +
@@ -146,7 +151,8 @@ function registerInit(gcf, program) {
146
151
  gcf
147
152
  .command('init')
148
153
  .description(
149
- 'Create the gcf/ skeleton from templates (idempotent, never overwrites); offers the harness',
154
+ 'Create the gcf/ skeleton from templates (idempotent, never overwrites) and the agent ' +
155
+ 'instructions block in AGENTS.md (replaced in place); offers the harness',
150
156
  )
151
157
  .option('--harness', `also install ${HARNESS} and run gcf-harness init (Claude Code only)`)
152
158
  .option('--no-harness', 'do not offer the harness')
@@ -155,6 +161,10 @@ function registerInit(gcf, program) {
155
161
  await seedSkeleton(root);
156
162
  const { writeIndex } = await lib('index-gen');
157
163
  await writeIndex(root);
164
+ // Re-run on every update: the block is replaced in place, so it follows the extension.
165
+ const block = await readFile(path.join(EXT_ROOT, 'templates', 'agents-block.md'), 'utf8');
166
+ const agents = await (await lib('agents-block')).writeAgentsBlock(process.cwd(), block);
167
+ if (agents.length) console.log(`agent instructions written: ${agents.join(', ')}`);
158
168
  console.log(
159
169
  'gcf/ ready. Write PILLARS.md through /gcf-init, then import a GDD with /gcf-ingest ' +
160
170
  'or start the design with /gcf-vision.',
@@ -177,19 +187,19 @@ function registerIndex(gcf) {
177
187
 
178
188
  function registerContext(gcf) {
179
189
  gcf
180
- .command('context <card-id>')
181
- .description('Assemble the readset for a card (GCF-PRD §9.1)')
190
+ .command('context <item-id>')
191
+ .description('Assemble the readset for a work item (GCF-PRD §9.1)')
182
192
  .option('--json', 'machine output')
183
- .action(async (cardId, opts) => {
193
+ .action(async (itemId, opts) => {
184
194
  const { buildContext } = await lib('context');
185
- const ctx = await buildContext(process.cwd(), cardId);
195
+ const ctx = await buildContext(process.cwd(), itemId);
186
196
  // Metric 1 (GCF-PRD §14): readset size over time, one csv line per call.
187
197
  const { appendFile, mkdir } = await import('node:fs/promises');
188
198
  const metricsDir = path.join(process.cwd(), 'gcf', '.metrics');
189
199
  await mkdir(metricsDir, { recursive: true });
190
200
  await appendFile(
191
201
  path.join(metricsDir, 'context-log.csv'),
192
- `${today()},${cardId},${ctx.files.length},${ctx.total_tokens_est}\n`,
202
+ `${today()},${itemId},${ctx.files.length},${ctx.total_tokens_est}\n`,
193
203
  'utf8',
194
204
  );
195
205
  out(opts.json, ctx, (c) =>
@@ -213,9 +223,10 @@ function registerAudit(gcf) {
213
223
  const report = await audit(process.cwd());
214
224
  // Metric 2 (GCF-PRD §14): findings per audit run.
215
225
  const findings =
216
- report.stale_built.length +
226
+ report.stale_waiting.length +
217
227
  report.tune_limit.length +
218
228
  report.until.filter((u) => u.status === 'stale').length +
229
+ report.stale_questions.length +
219
230
  Object.keys(report.linter).length;
220
231
  const { appendFile, mkdir } = await import('node:fs/promises');
221
232
  const metricsDir = path.join(process.cwd(), 'gcf', '.metrics');
@@ -236,7 +247,7 @@ function registerAudit(gcf) {
236
247
  function registerPlaytest(gcf) {
237
248
  gcf
238
249
  .command('playtest')
239
- .description('List built cards with their miss-criterion questions')
250
+ .description('List items awaiting a verdict and unplayed ones, with their questions')
240
251
  .option('--json', 'machine output')
241
252
  .action(async (opts) => {
242
253
  const { gcfRoot } = await lib('gcf-paths');
@@ -244,29 +255,60 @@ function registerPlaytest(gcf) {
244
255
  const list = await playtestList(gcfRoot());
245
256
  out(opts.json, list, (l) =>
246
257
  l.length
247
- ? l.map((c) => `${c.id} [${c.system.join(',')}]: ${c.question}`).join('\n')
248
- : 'nothing in built — nothing to playtest',
258
+ ? l.map((c) => `${c.id} (${c.status}) [${c.system.join(',')}]: ${c.question}`).join('\n')
259
+ : 'nothing waits for a playtest',
249
260
  );
250
261
  });
251
262
  }
252
263
 
264
+ const shortHash = (h) => (h ? ` (${h.slice(0, 7)})` : '');
265
+ const removalLine = (r) => (r.removal ? `\nremoval item: gcf/work/${r.removal}.md` : '');
266
+
267
+ /** `--remove` / `--no-remove` / neither (the move's default) for a move that may bury code. */
268
+ const removeOptions = (cmd) =>
269
+ cmd
270
+ .option('--remove', 'also create a removal item: the code is in main')
271
+ .option('--no-remove', 'no removal item: the code never reached main');
272
+
253
273
  function registerVerdict(gcf) {
254
- gcf
255
- .command('verdict <card-id> <verdict>')
256
- .description('Apply a human verdict: keep | tune | kill (GCF-PRD §8)')
257
- .option('--note <text>', 'tune remark or kill reason')
274
+ removeOptions(
275
+ gcf
276
+ .command('verdict <item-id> <verdict>')
277
+ .description('Apply a human verdict: keep | tune | kill (Methodology §4.1)')
278
+ .option('--note <text>', 'tune remark or kill reason')
279
+ .option('--tags <list>', 'graveyard tags on kill, comma-separated (default: its systems)')
280
+ .option(
281
+ '--constraint <line>',
282
+ 'keep of a hard unit: a line every reader of the subsystem must remember',
283
+ (v, p) => [...p, v],
284
+ [],
285
+ ),
286
+ )
258
287
  .option('--no-commit', 'skip the verdict commit (tests only)')
259
- .action(async (cardId, verdict, opts) => {
260
- const { applyVerdict } = await lib('verdict-fs');
261
- const res = await applyVerdict(process.cwd(), cardId, verdict, {
288
+ .action(async (itemId, verdict, opts) => {
289
+ const { verdictItem } = await lib('work-ops');
290
+ const res = await verdictItem(process.cwd(), itemId, verdict, {
262
291
  note: opts.note ?? '',
292
+ tags: splitList(opts.tags),
293
+ remove: opts.remove,
294
+ constraints: opts.constraint,
263
295
  commit: opts.commit,
264
296
  });
265
- const hash = res.hash ? ` (${res.hash.slice(0, 7)})` : '';
266
- console.log(`${res.verdict} ${res.card} → ${res.systems.join(',')}${hash}`);
297
+ console.log(
298
+ `${res.verdict} ${res.id} → ${res.systems.join(',')}${shortHash(res.hash)}` +
299
+ removalLine(res),
300
+ );
267
301
  });
268
302
  }
269
303
 
304
+ const splitList = (v) =>
305
+ v
306
+ ? String(v)
307
+ .split(',')
308
+ .map((s) => s.trim())
309
+ .filter(Boolean)
310
+ : [];
311
+
270
312
  function anchorDeltaLine(r) {
271
313
  if (r.delta) {
272
314
  const { added, changed, removed, renamed } = r.delta;
@@ -368,7 +410,9 @@ function registerDesign(gcf) {
368
410
  const design = gcf.command('design').description('Design Model: units in gcf/design/');
369
411
  design
370
412
  .command('check')
371
- .description('Mechanical checks over gcf/design/ and the cards; exit 1 on a blocking finding')
413
+ .description(
414
+ 'Mechanical checks over gcf/design/ and the work items; exit 1 on a blocking finding',
415
+ )
372
416
  .option('--json', 'machine output')
373
417
  .action(async (opts) => {
374
418
  const { designCheck } = await lib('design-check');
@@ -438,7 +482,9 @@ const placeLines = (x) => [...x.live.map((p) => `${p.state} gcf/${p.file}`), ...
438
482
  function registerWhere(gcf) {
439
483
  gcf
440
484
  .command('where [id]')
441
- .description('Where an id lives: a design section, a card, a terminal event; no id lists all')
485
+ .description(
486
+ 'Where an id lives: a design section, a work item, a terminal event; no id lists all',
487
+ )
442
488
  .option('--json', 'machine output')
443
489
  .action(async (id, opts) => {
444
490
  const { buildLedger } = await lib('ledger');
@@ -458,9 +504,13 @@ function registerWhere(gcf) {
458
504
  function registerMoves(gcf) {
459
505
  const ops = () => lib('unit-ops');
460
506
  gcf
461
- .command('take <id>')
462
- .description('Move a ready soft unit from gcf/design/ into cards/<id>.md (no commit)')
463
- .action(async (id) => console.log((await (await ops()).takeUnit(process.cwd(), id)).card));
507
+ .command('take <ids...>')
508
+ .description('Move ready units from gcf/design/ into work items gcf/work/<id>.md (no commit)')
509
+ .option('--feature <id>', 'group them under this feature item, new or live')
510
+ .action(async (ids, o) => {
511
+ const r = await (await ops()).takeUnits(process.cwd(), ids, { feature: o.feature ?? null });
512
+ console.log(r.items.join('\n'));
513
+ });
464
514
  gcf
465
515
  .command('done <id>')
466
516
  .description('A hard unit is built and tested: it leaves gcf/design/ (commit gcf(done))')
@@ -477,20 +527,214 @@ function registerMoves(gcf) {
477
527
  ).doneUnit(process.cwd(), id, { constraints: o.constraint, commit: o.commit });
478
528
  console.log(`done ${r.id}${r.hash ? ` (${r.hash.slice(0, 7)})` : ''}`);
479
529
  });
530
+ removeOptions(
531
+ gcf
532
+ .command('drop <id>')
533
+ .description(
534
+ 'Drop a unit or a work item: a graveyard line keeps the reason (commit gcf(drop))',
535
+ )
536
+ .requiredOption('--reason <text>', 'why it dies')
537
+ .option('--tags <list>', 'graveyard tags, comma-separated (default: its systems)'),
538
+ )
539
+ .option('--no-commit', 'skip the commit (tests only)')
540
+ .action(async (id, o) => {
541
+ const tags = splitList(o.tags);
542
+ const { buildLedger } = await lib('ledger');
543
+ const isItem = (await buildLedger(process.cwd())).item(id);
544
+ const r = isItem
545
+ ? await (
546
+ await lib('work-ops')
547
+ ).dropItem(process.cwd(), id, {
548
+ reason: o.reason,
549
+ tags,
550
+ remove: o.remove,
551
+ commit: o.commit,
552
+ })
553
+ : await (
554
+ await ops()
555
+ ).dropUnit(process.cwd(), id, { reason: o.reason, tags, commit: o.commit });
556
+ console.log(`dropped ${r.id}${shortHash(r.hash)}${removalLine(r)}`);
557
+ });
558
+ }
559
+
560
+ function registerItem(gcf) {
561
+ const item = gcf.command('item').description('Work items in gcf/work/ (Methodology §3)');
562
+ item
563
+ .command('new <id>')
564
+ .description('Create a work item in ready (or blocked), no commit; a design unit uses take')
565
+ .requiredOption(
566
+ '--type <type>',
567
+ 'feature | tech | bug | spike | asset | content | perf | release',
568
+ )
569
+ .option('--gate <gate>', 'verdict | auto | decision (default: by type)')
570
+ .option('--title <text>', 'one line: what the item gives')
571
+ .option('--parent <feature>', 'a live feature item this one belongs to')
572
+ .option('--system <list>', 'subsystems it touches, comma-separated')
573
+ .option('--depends <list>', 'ids it waits for, comma-separated')
574
+ .option('--severity <level>', 'a bug: critical | high | medium | low')
575
+ .option('--blocked <why>', 'start blocked, e.g. "file needed" for an asset')
576
+ .option('--from <in-id>', 'the inbox entry it comes from: erased')
577
+ .option('--reopens <id>', 'the ended id this item reworks')
578
+ .action(async (id, o) => {
579
+ const { newItem } = await lib('work-ops');
580
+ const r = await newItem(process.cwd(), {
581
+ id,
582
+ type: o.type,
583
+ gate: o.gate,
584
+ title: o.title,
585
+ parent: o.parent,
586
+ system: splitList(o.system),
587
+ depends: splitList(o.depends),
588
+ severity: o.severity,
589
+ blocked: o.blocked,
590
+ from: o.from,
591
+ reopens: o.reopens,
592
+ });
593
+ console.log(r.item);
594
+ });
595
+ }
596
+
597
+ function registerItemMoves(gcf) {
598
+ const ops = () => lib('work-ops');
480
599
  gcf
481
- .command('drop <id>')
482
- .description(
483
- 'Drop a unit before any work: a graveyard line keeps the reason (commit gcf(drop))',
600
+ .command('submit <id>')
601
+ .description('ready → awaiting-verdict / awaiting-decision (commit gcf(submit))')
602
+ .option('--no-commit', 'skip the commit (tests only)')
603
+ .action(async (id, o) => {
604
+ const r = await (await ops()).submitItem(process.cwd(), id, { commit: o.commit });
605
+ console.log(`${r.id} → ${r.status}${shortHash(r.hash)}`);
606
+ });
607
+ gcf
608
+ .command('close <id>')
609
+ .description('A gate-auto item passed its DoD: it ends, or a soft unit goes unplayed')
610
+ .option(
611
+ '--constraint <line>',
612
+ 'a hard unit: a line every reader of the subsystem must remember',
613
+ (v, p) => [...p, v],
614
+ [],
484
615
  )
485
- .requiredOption('--reason <text>', 'why the idea dies')
486
- .option('--tags <list>', 'graveyard tags, comma-separated (default: its systems)')
487
616
  .option('--no-commit', 'skip the commit (tests only)')
488
617
  .action(async (id, o) => {
489
- const tags = o.tags ? o.tags.split(',').map((t) => t.trim()) : [];
490
618
  const r = await (
491
619
  await ops()
492
- ).dropUnit(process.cwd(), id, { reason: o.reason, tags, commit: o.commit });
493
- console.log(`dropped ${r.id}${r.hash ? ` (${r.hash.slice(0, 7)})` : ''}`);
620
+ ).closeItem(process.cwd(), id, { constraints: o.constraint, commit: o.commit });
621
+ console.log(`closed ${r.id}${r.unplayed ? ' → unplayed' : ''}${shortHash(r.hash)}`);
622
+ });
623
+ }
624
+
625
+ function registerDecide(gcf) {
626
+ const ops = () => lib('work-ops');
627
+ removeOptions(
628
+ gcf
629
+ .command('decide <id> <choice>')
630
+ .description('go | no | wontfix | known | close | continue | kill (Methodology §4.1)')
631
+ .option('--reason <text>', 'why: the graveyard keeps it'),
632
+ )
633
+ .option('--no-commit', 'skip the commit (tests only)')
634
+ .action(async (id, choice, o) => {
635
+ const r = await (
636
+ await ops()
637
+ ).decideItem(process.cwd(), id, choice, {
638
+ reason: o.reason ?? '',
639
+ remove: o.remove,
640
+ commit: o.commit,
641
+ });
642
+ console.log(`${r.choice} ${r.id}${shortHash(r.hash)}${removalLine(r)}`);
643
+ });
644
+ gcf
645
+ .command('block <id>')
646
+ .description('ready → blocked, with the reason or the question (commit gcf(block))')
647
+ .requiredOption('--reason <text>', 'why it cannot go on, or the question for you')
648
+ .option('--no-commit', 'skip the commit (tests only)')
649
+ .action(async (id, o) => {
650
+ const r = await (
651
+ await ops()
652
+ ).blockItem(process.cwd(), id, { reason: o.reason, commit: o.commit });
653
+ console.log(`blocked ${r.id}${shortHash(r.hash)}`);
654
+ });
655
+ gcf
656
+ .command('unblock <id>')
657
+ .description('blocked | parked → ready (commit gcf(unblock))')
658
+ .option('--note <text>', 'the answer, or "file: <path>" for an asset')
659
+ .option('--no-commit', 'skip the commit (tests only)')
660
+ .action(async (id, o) => {
661
+ const r = await (
662
+ await ops()
663
+ ).unblockItem(process.cwd(), id, { note: o.note ?? '', commit: o.commit });
664
+ console.log(`unblocked ${r.id}${shortHash(r.hash)}`);
665
+ });
666
+ }
667
+
668
+ function registerInbox(gcf) {
669
+ const inbox = gcf
670
+ .command('inbox')
671
+ .description('Raw input waiting for triage (gcf/inbox.md); no commit')
672
+ .option('--json', 'machine output')
673
+ .action(async (o) => {
674
+ const { gcfRoot } = await lib('gcf-paths');
675
+ const list = await (await lib('inbox')).listInbox(gcfRoot());
676
+ out(o.json, list, (l) =>
677
+ l.length ? l.map((e) => `${e.id} [${e.source}] ${e.text}`).join('\n') : 'inbox empty',
678
+ );
679
+ });
680
+ inbox
681
+ .command('add <text>')
682
+ .description('Append an entry')
683
+ .option('--source <source>', 'playtest | ci | agent | operator', 'operator')
684
+ .action(async (text, o) => {
685
+ const { gcfRoot } = await lib('gcf-paths');
686
+ const e = await (await lib('inbox')).addInbox(gcfRoot(), text, o.source);
687
+ console.log(e.id);
688
+ });
689
+ inbox
690
+ .command('drop <in-id>')
691
+ .description('Erase an entry: triaged as noise, a duplicate, or turned into an item')
692
+ .action(async (id) => {
693
+ const { gcfRoot } = await lib('gcf-paths');
694
+ await (await lib('inbox')).dropInbox(gcfRoot(), id);
695
+ console.log(`erased ${id}`);
696
+ });
697
+ }
698
+
699
+ function registerLive(gcf) {
700
+ const live = gcf
701
+ .command('live')
702
+ .description('What the game is now: gcf/LIVE.md')
703
+ .option('--json', 'machine output')
704
+ .action(async (o) => {
705
+ const { gcfRoot } = await lib('gcf-paths');
706
+ const l = await (await lib('live')).readLive(gcfRoot());
707
+ const rows = (list) => list.map((r) => ` ${r.id}: ${r.summary}`);
708
+ out(o.json, l, (x) =>
709
+ ['capabilities:', ...rows(x.capabilities), 'known-issues:', ...rows(x.knownIssues)].join(
710
+ '\n',
711
+ ),
712
+ );
713
+ });
714
+ live
715
+ .command('check')
716
+ .description('Record form, refs that name no file, known issues against parked bugs')
717
+ .option('--json', 'machine output')
718
+ .action(async (o) => {
719
+ const r = await (await lib('live')).checkLive(process.cwd());
720
+ out(o.json, r.issues, (l) => (l.length ? l.join('\n') : 'clean'));
721
+ if (r.issues.length) process.exitCode = 1;
722
+ });
723
+ }
724
+
725
+ function registerPlan(gcf) {
726
+ gcf
727
+ .command('plan')
728
+ .description('The plan of a work item')
729
+ .command('check <id>')
730
+ .description('Deterministic plan check: it exists, has task checkboxes, readset in budget')
731
+ .option('--json', 'machine output')
732
+ .action(async (id, o) => {
733
+ const r = await (await lib('plan-check')).planCheck(process.cwd(), id);
734
+ out(o.json, r, (x) =>
735
+ x.ok ? `${x.plan}: ${x.done}/${x.tasks} tasks done` : x.problems.join('\n'),
736
+ );
737
+ if (!r.ok) process.exitCode = 1;
494
738
  });
495
739
  }
496
740
 
@@ -541,8 +785,36 @@ function registerLint(gcf) {
541
785
  });
542
786
  }
543
787
 
788
+ function registerPillar(gcf) {
789
+ const pillar = gcf
790
+ .command('pillar')
791
+ .description('Revise PILLARS.md where units and items cite it (the human decides)');
792
+ pillar
793
+ .command('rename <from> <to>')
794
+ .description('Rename a pillar id in PILLARS.md, units and items (commit gcf(pillar))')
795
+ .option('--no-commit', 'skip the commit (tests only)')
796
+ .action(async (from, to, o) => {
797
+ const r = await (await lib('pillars')).renamePillar(process.cwd(), from, to, o);
798
+ console.log(`renamed ${from} → ${to}${shortHash(r.hash)}`);
799
+ });
800
+ pillar
801
+ .command('retire <id>')
802
+ .description('Take a pillar out; units left with none read pillars: TBD (commit gcf(pillar))')
803
+ .requiredOption('--reason <text>', 'why the game no longer holds it')
804
+ .option('--no-commit', 'skip the commit (tests only)')
805
+ .action(async (id, o) => {
806
+ const r = await (await lib('pillars')).retirePillar(process.cwd(), id, o);
807
+ console.log(`retired ${id}${shortHash(r.hash)}`);
808
+ if (r.bare.length)
809
+ console.log(
810
+ `items now citing no pillar — give each one or accept it: ${r.bare.join(', ')}`,
811
+ );
812
+ });
813
+ }
814
+
544
815
  export function register(program) {
545
816
  const gcf = program.command('gcf').description('Game Context Framework core');
817
+ registerPillar(gcf);
546
818
  registerInit(gcf, program);
547
819
  registerIndex(gcf);
548
820
  registerContext(gcf);
@@ -555,6 +827,12 @@ export function register(program) {
555
827
  registerDesign(gcf);
556
828
  registerWhere(gcf);
557
829
  registerMoves(gcf);
830
+ registerItem(gcf);
831
+ registerItemMoves(gcf);
832
+ registerDecide(gcf);
833
+ registerInbox(gcf);
834
+ registerLive(gcf);
835
+ registerPlan(gcf);
558
836
  registerHooksInstall(gcf);
559
837
  registerLint(gcf);
560
838
  }