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.
- package/README.md +63 -11
- package/commands/gcf.js +321 -43
- package/extension.json +22 -5
- package/hooks/pre-commit.js +60 -31
- package/injections/commit-status.md +7 -5
- package/injections/plan-context.md +8 -5
- package/injections/review-pillars.md +4 -3
- package/lib/agents-block.js +66 -0
- package/lib/anchors.js +27 -4
- package/lib/audit-core.js +135 -48
- package/lib/bundle.js +7 -4
- package/lib/context.js +33 -25
- package/lib/design-check.js +149 -40
- package/lib/design.js +2 -2
- package/lib/gcf-paths.js +13 -2
- package/lib/id.js +2 -2
- package/lib/inbox.js +54 -0
- package/lib/index-gen.js +23 -8
- package/lib/ledger.js +19 -17
- package/lib/linter.js +2 -1
- package/lib/live.js +191 -0
- package/lib/pillars.js +113 -0
- package/lib/plan-check.js +31 -0
- package/lib/playtest-list.js +8 -4
- package/lib/systems-doc.js +16 -1
- package/lib/transitions.js +36 -21
- package/lib/unit-ops.js +125 -97
- package/lib/validate-item.js +244 -0
- package/lib/verdict-fs.js +67 -128
- package/lib/work-ops.js +759 -0
- package/package.json +4 -3
- package/profiles/pillars.md +1 -1
- package/profiles/system.md +1 -1
- package/profiles/unit-constraint.md +10 -0
- package/profiles/unit-entity.md +10 -0
- package/profiles/unit-mechanic.md +6 -1
- package/profiles/unit-resource.md +6 -1
- package/profiles/unit-rule.md +10 -0
- package/schemas/item.schema.json +105 -0
- package/skills/gcf-adopt/SKILL.md +2 -2
- package/skills/gcf-audit/SKILL.md +3 -3
- package/skills/gcf-card/SKILL.md +27 -20
- package/skills/gcf-ingest/SKILL.md +3 -3
- package/skills/gcf-init/SKILL.md +8 -5
- package/skills/gcf-triage/SKILL.md +35 -0
- package/skills/gcf-verdict/SKILL.md +19 -16
- package/templates/agents-block.md +28 -0
- package/templates/config.yml +7 -0
- package/templates/{card.md → item.md} +10 -3
- package/lib/validate-card.js +0 -93
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 = [
|
|
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
|
|
77
|
-
* graveyard.md: a union merge keeps both instead of a conflict whose "ours" loses
|
|
78
|
-
* missing rules to `<dir>/.gitattributes`; a project that ran an older init gets
|
|
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', '
|
|
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
|
-
|
|
110
|
-
|
|
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)
|
|
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 <
|
|
181
|
-
.description('Assemble the readset for a
|
|
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 (
|
|
193
|
+
.action(async (itemId, opts) => {
|
|
184
194
|
const { buildContext } = await lib('context');
|
|
185
|
-
const ctx = await buildContext(process.cwd(),
|
|
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()},${
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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 (
|
|
260
|
-
const {
|
|
261
|
-
const res = await
|
|
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
|
-
|
|
266
|
-
|
|
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(
|
|
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(
|
|
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 <
|
|
462
|
-
.description('Move
|
|
463
|
-
.
|
|
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('
|
|
482
|
-
.description(
|
|
483
|
-
|
|
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
|
-
).
|
|
493
|
-
console.log(`
|
|
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
|
}
|