gcf-aif-ext 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -11
- package/commands/gcf.js +287 -42
- package/extension.json +22 -5
- package/hooks/pre-commit.js +55 -31
- package/injections/commit-status.md +7 -5
- package/injections/plan-context.md +8 -5
- package/injections/review-pillars.md +4 -3
- package/lib/anchors.js +27 -4
- package/lib/audit-core.js +135 -48
- package/lib/bundle.js +7 -4
- package/lib/context.js +24 -23
- package/lib/design-check.js +78 -27
- 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 +179 -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 +124 -97
- package/lib/validate-item.js +244 -0
- package/lib/verdict-fs.js +61 -128
- package/lib/work-ops.js +727 -0
- package/package.json +1 -1
- 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 +25 -20
- package/skills/gcf-ingest/SKILL.md +3 -3
- package/skills/gcf-init/SKILL.md +2 -2
- package/skills/gcf-triage/SKILL.md +35 -0
- package/skills/gcf-verdict/SKILL.md +19 -16
- 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,40 @@ 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
|
+
### Upgrading from 0.3.x
|
|
54
|
+
|
|
55
|
+
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:
|
|
56
|
+
|
|
57
|
+
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.
|
|
58
|
+
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.
|
|
59
|
+
3. Delete `gcf/cards/` and commit it with the new items.
|
|
60
|
+
|
|
61
|
+
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.
|
|
62
|
+
|
|
63
|
+
### Upgrading from 0.3.0
|
|
64
|
+
|
|
65
|
+
0.3.1 is a patch release with no migration step. It changes behavior in these ways:
|
|
66
|
+
|
|
67
|
+
- 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`
|
|
68
|
+
- `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`
|
|
69
|
+
- `gcf audit` reports open questions older than `question_stale_after_commits` (`stale_questions`) and a `PILLARS.md` with more than 7 pillars
|
|
70
|
+
- a card with an unquoted numeric `id:` (YAML reads `id: 2024` as a number) is refused: quote the slug
|
|
71
|
+
- `built_at` is recorded in a follow-up commit after the implementation commit, with that commit's hash
|
|
72
|
+
- 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`
|
|
73
|
+
- 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
|
|
74
|
+
|
|
75
|
+
### Upgrading from 0.2.x
|
|
76
|
+
|
|
77
|
+
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:
|
|
78
|
+
|
|
79
|
+
1. `ai-factory extension update` and `ai-factory gcf init` (adds `history.md` and the `.gitattributes` lines).
|
|
80
|
+
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.
|
|
81
|
+
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.
|
|
82
|
+
4. `ai-factory gcf design check`, fix what it reports — typically a digit in a description — and commit.
|
|
48
83
|
|
|
49
84
|
To remove the extension:
|
|
50
85
|
|
|
@@ -56,15 +91,22 @@ Removal leaves `gcf/` and `cnlp.config.json` in place. It also leaves the pre-co
|
|
|
56
91
|
|
|
57
92
|
## What it adds
|
|
58
93
|
|
|
59
|
-
**Skills** — `gcf-init`, `gcf-adopt`, `gcf-card
|
|
94
|
+
**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`.
|
|
95
|
+
|
|
96
|
+
**A deterministic core** behind `ai-factory gcf …`:
|
|
97
|
+
|
|
98
|
+
- 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`
|
|
99
|
+
- inbox and snapshot — `inbox [add <text> | drop <in-id>]`, `live`, `live check`
|
|
100
|
+
- runs and play — `context`, `plan check <id>`, `playtest`, `audit`
|
|
101
|
+
- design and sources — `design check`, `design export`, `design import`, `design migrate`, `anchor`, `section`, `quotes`, `lint`
|
|
102
|
+
- setup — `init`, `index`, `hooks-install`
|
|
60
103
|
|
|
61
|
-
|
|
104
|
+
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
105
|
|
|
63
|
-
**A pre-commit gate** that breaks the commit on a
|
|
106
|
+
**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
107
|
|
|
65
|
-
**CNLP profiles and a
|
|
108
|
+
**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
109
|
|
|
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
110
|
|
|
69
111
|
## Docs
|
|
70
112
|
|
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, ' +
|
|
@@ -177,19 +182,19 @@ function registerIndex(gcf) {
|
|
|
177
182
|
|
|
178
183
|
function registerContext(gcf) {
|
|
179
184
|
gcf
|
|
180
|
-
.command('context <
|
|
181
|
-
.description('Assemble the readset for a
|
|
185
|
+
.command('context <item-id>')
|
|
186
|
+
.description('Assemble the readset for a work item (GCF-PRD §9.1)')
|
|
182
187
|
.option('--json', 'machine output')
|
|
183
|
-
.action(async (
|
|
188
|
+
.action(async (itemId, opts) => {
|
|
184
189
|
const { buildContext } = await lib('context');
|
|
185
|
-
const ctx = await buildContext(process.cwd(),
|
|
190
|
+
const ctx = await buildContext(process.cwd(), itemId);
|
|
186
191
|
// Metric 1 (GCF-PRD §14): readset size over time, one csv line per call.
|
|
187
192
|
const { appendFile, mkdir } = await import('node:fs/promises');
|
|
188
193
|
const metricsDir = path.join(process.cwd(), 'gcf', '.metrics');
|
|
189
194
|
await mkdir(metricsDir, { recursive: true });
|
|
190
195
|
await appendFile(
|
|
191
196
|
path.join(metricsDir, 'context-log.csv'),
|
|
192
|
-
`${today()},${
|
|
197
|
+
`${today()},${itemId},${ctx.files.length},${ctx.total_tokens_est}\n`,
|
|
193
198
|
'utf8',
|
|
194
199
|
);
|
|
195
200
|
out(opts.json, ctx, (c) =>
|
|
@@ -213,9 +218,10 @@ function registerAudit(gcf) {
|
|
|
213
218
|
const report = await audit(process.cwd());
|
|
214
219
|
// Metric 2 (GCF-PRD §14): findings per audit run.
|
|
215
220
|
const findings =
|
|
216
|
-
report.
|
|
221
|
+
report.stale_waiting.length +
|
|
217
222
|
report.tune_limit.length +
|
|
218
223
|
report.until.filter((u) => u.status === 'stale').length +
|
|
224
|
+
report.stale_questions.length +
|
|
219
225
|
Object.keys(report.linter).length;
|
|
220
226
|
const { appendFile, mkdir } = await import('node:fs/promises');
|
|
221
227
|
const metricsDir = path.join(process.cwd(), 'gcf', '.metrics');
|
|
@@ -236,7 +242,7 @@ function registerAudit(gcf) {
|
|
|
236
242
|
function registerPlaytest(gcf) {
|
|
237
243
|
gcf
|
|
238
244
|
.command('playtest')
|
|
239
|
-
.description('List
|
|
245
|
+
.description('List items awaiting a verdict and unplayed ones, with their questions')
|
|
240
246
|
.option('--json', 'machine output')
|
|
241
247
|
.action(async (opts) => {
|
|
242
248
|
const { gcfRoot } = await lib('gcf-paths');
|
|
@@ -244,29 +250,60 @@ function registerPlaytest(gcf) {
|
|
|
244
250
|
const list = await playtestList(gcfRoot());
|
|
245
251
|
out(opts.json, list, (l) =>
|
|
246
252
|
l.length
|
|
247
|
-
? l.map((c) => `${c.id} [${c.system.join(',')}]: ${c.question}`).join('\n')
|
|
248
|
-
: 'nothing
|
|
253
|
+
? l.map((c) => `${c.id} (${c.status}) [${c.system.join(',')}]: ${c.question}`).join('\n')
|
|
254
|
+
: 'nothing waits for a playtest',
|
|
249
255
|
);
|
|
250
256
|
});
|
|
251
257
|
}
|
|
252
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
|
+
|
|
253
268
|
function registerVerdict(gcf) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
+
)
|
|
258
282
|
.option('--no-commit', 'skip the verdict commit (tests only)')
|
|
259
|
-
.action(async (
|
|
260
|
-
const {
|
|
261
|
-
const res = await
|
|
283
|
+
.action(async (itemId, verdict, opts) => {
|
|
284
|
+
const { verdictItem } = await lib('work-ops');
|
|
285
|
+
const res = await verdictItem(process.cwd(), itemId, verdict, {
|
|
262
286
|
note: opts.note ?? '',
|
|
287
|
+
tags: splitList(opts.tags),
|
|
288
|
+
remove: opts.remove,
|
|
289
|
+
constraints: opts.constraint,
|
|
263
290
|
commit: opts.commit,
|
|
264
291
|
});
|
|
265
|
-
|
|
266
|
-
|
|
292
|
+
console.log(
|
|
293
|
+
`${res.verdict} ${res.id} → ${res.systems.join(',')}${shortHash(res.hash)}` +
|
|
294
|
+
removalLine(res),
|
|
295
|
+
);
|
|
267
296
|
});
|
|
268
297
|
}
|
|
269
298
|
|
|
299
|
+
const splitList = (v) =>
|
|
300
|
+
v
|
|
301
|
+
? String(v)
|
|
302
|
+
.split(',')
|
|
303
|
+
.map((s) => s.trim())
|
|
304
|
+
.filter(Boolean)
|
|
305
|
+
: [];
|
|
306
|
+
|
|
270
307
|
function anchorDeltaLine(r) {
|
|
271
308
|
if (r.delta) {
|
|
272
309
|
const { added, changed, removed, renamed } = r.delta;
|
|
@@ -368,7 +405,9 @@ function registerDesign(gcf) {
|
|
|
368
405
|
const design = gcf.command('design').description('Design Model: units in gcf/design/');
|
|
369
406
|
design
|
|
370
407
|
.command('check')
|
|
371
|
-
.description(
|
|
408
|
+
.description(
|
|
409
|
+
'Mechanical checks over gcf/design/ and the work items; exit 1 on a blocking finding',
|
|
410
|
+
)
|
|
372
411
|
.option('--json', 'machine output')
|
|
373
412
|
.action(async (opts) => {
|
|
374
413
|
const { designCheck } = await lib('design-check');
|
|
@@ -438,7 +477,9 @@ const placeLines = (x) => [...x.live.map((p) => `${p.state} gcf/${p.file}`), ...
|
|
|
438
477
|
function registerWhere(gcf) {
|
|
439
478
|
gcf
|
|
440
479
|
.command('where [id]')
|
|
441
|
-
.description(
|
|
480
|
+
.description(
|
|
481
|
+
'Where an id lives: a design section, a work item, a terminal event; no id lists all',
|
|
482
|
+
)
|
|
442
483
|
.option('--json', 'machine output')
|
|
443
484
|
.action(async (id, opts) => {
|
|
444
485
|
const { buildLedger } = await lib('ledger');
|
|
@@ -458,9 +499,13 @@ function registerWhere(gcf) {
|
|
|
458
499
|
function registerMoves(gcf) {
|
|
459
500
|
const ops = () => lib('unit-ops');
|
|
460
501
|
gcf
|
|
461
|
-
.command('take <
|
|
462
|
-
.description('Move
|
|
463
|
-
.
|
|
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
|
+
});
|
|
464
509
|
gcf
|
|
465
510
|
.command('done <id>')
|
|
466
511
|
.description('A hard unit is built and tested: it leaves gcf/design/ (commit gcf(done))')
|
|
@@ -477,20 +522,214 @@ function registerMoves(gcf) {
|
|
|
477
522
|
).doneUnit(process.cwd(), id, { constraints: o.constraint, commit: o.commit });
|
|
478
523
|
console.log(`done ${r.id}${r.hash ? ` (${r.hash.slice(0, 7)})` : ''}`);
|
|
479
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
|
+
});
|
|
480
602
|
gcf
|
|
481
|
-
.command('
|
|
482
|
-
.description(
|
|
483
|
-
|
|
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
|
+
[],
|
|
484
610
|
)
|
|
485
|
-
.requiredOption('--reason <text>', 'why the idea dies')
|
|
486
|
-
.option('--tags <list>', 'graveyard tags, comma-separated (default: its systems)')
|
|
487
611
|
.option('--no-commit', 'skip the commit (tests only)')
|
|
488
612
|
.action(async (id, o) => {
|
|
489
|
-
const tags = o.tags ? o.tags.split(',').map((t) => t.trim()) : [];
|
|
490
613
|
const r = await (
|
|
491
614
|
await ops()
|
|
492
|
-
).
|
|
493
|
-
console.log(`
|
|
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;
|
|
494
733
|
});
|
|
495
734
|
}
|
|
496
735
|
|
|
@@ -555,6 +794,12 @@ export function register(program) {
|
|
|
555
794
|
registerDesign(gcf);
|
|
556
795
|
registerWhere(gcf);
|
|
557
796
|
registerMoves(gcf);
|
|
797
|
+
registerItem(gcf);
|
|
798
|
+
registerItemMoves(gcf);
|
|
799
|
+
registerDecide(gcf);
|
|
800
|
+
registerInbox(gcf);
|
|
801
|
+
registerLive(gcf);
|
|
802
|
+
registerPlan(gcf);
|
|
558
803
|
registerHooksInstall(gcf);
|
|
559
804
|
registerLint(gcf);
|
|
560
805
|
}
|
package/extension.json
CHANGED
|
@@ -1,24 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/lee-to/ai-factory/2.x/schemas/extension.schema.json",
|
|
3
3
|
"name": "gcf-aif-ext",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"description": "Game Context Framework for AI Factory",
|
|
6
6
|
"skills": [
|
|
7
7
|
"skills/gcf-init",
|
|
8
8
|
"skills/gcf-adopt",
|
|
9
9
|
"skills/gcf-card",
|
|
10
10
|
"skills/gcf-verdict",
|
|
11
|
+
"skills/gcf-triage",
|
|
11
12
|
"skills/gcf-audit",
|
|
12
13
|
"skills/gcf-vision",
|
|
13
14
|
"skills/gcf-debate",
|
|
14
15
|
"skills/gcf-ingest"
|
|
15
16
|
],
|
|
16
17
|
"commands": [
|
|
17
|
-
{
|
|
18
|
+
{
|
|
19
|
+
"name": "gcf",
|
|
20
|
+
"description": "Game Context Framework core",
|
|
21
|
+
"module": "commands/gcf.js"
|
|
22
|
+
}
|
|
18
23
|
],
|
|
19
24
|
"injections": [
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
{
|
|
26
|
+
"target": "aif-plan",
|
|
27
|
+
"position": "append",
|
|
28
|
+
"file": "./injections/plan-context.md"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"target": "aif-review",
|
|
32
|
+
"position": "append",
|
|
33
|
+
"file": "./injections/review-pillars.md"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"target": "aif-commit",
|
|
37
|
+
"position": "append",
|
|
38
|
+
"file": "./injections/commit-status.md"
|
|
39
|
+
}
|
|
23
40
|
]
|
|
24
41
|
}
|