gcf-aif-ext 0.4.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 +10 -0
- package/commands/gcf.js +34 -1
- package/extension.json +1 -1
- package/hooks/pre-commit.js +12 -7
- package/lib/agents-block.js +66 -0
- package/lib/context.js +11 -4
- package/lib/design-check.js +74 -16
- package/lib/live.js +16 -4
- package/lib/pillars.js +113 -0
- package/lib/unit-ops.js +2 -1
- package/lib/verdict-fs.js +8 -2
- package/lib/work-ops.js +37 -5
- package/package.json +4 -3
- package/skills/gcf-card/SKILL.md +8 -6
- package/skills/gcf-init/SKILL.md +6 -3
- package/templates/agents-block.md +28 -0
package/README.md
CHANGED
|
@@ -50,6 +50,16 @@ Updating preserves `gcf/`, `cnlp.config.json` and the pre-commit hook. Run `ai-f
|
|
|
50
50
|
|
|
51
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
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
|
+
|
|
53
63
|
### Upgrading from 0.3.x
|
|
54
64
|
|
|
55
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:
|
package/commands/gcf.js
CHANGED
|
@@ -151,7 +151,8 @@ function registerInit(gcf, program) {
|
|
|
151
151
|
gcf
|
|
152
152
|
.command('init')
|
|
153
153
|
.description(
|
|
154
|
-
'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',
|
|
155
156
|
)
|
|
156
157
|
.option('--harness', `also install ${HARNESS} and run gcf-harness init (Claude Code only)`)
|
|
157
158
|
.option('--no-harness', 'do not offer the harness')
|
|
@@ -160,6 +161,10 @@ function registerInit(gcf, program) {
|
|
|
160
161
|
await seedSkeleton(root);
|
|
161
162
|
const { writeIndex } = await lib('index-gen');
|
|
162
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(', ')}`);
|
|
163
168
|
console.log(
|
|
164
169
|
'gcf/ ready. Write PILLARS.md through /gcf-init, then import a GDD with /gcf-ingest ' +
|
|
165
170
|
'or start the design with /gcf-vision.',
|
|
@@ -780,8 +785,36 @@ function registerLint(gcf) {
|
|
|
780
785
|
});
|
|
781
786
|
}
|
|
782
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
|
+
|
|
783
815
|
export function register(program) {
|
|
784
816
|
const gcf = program.command('gcf').description('Game Context Framework core');
|
|
817
|
+
registerPillar(gcf);
|
|
785
818
|
registerInit(gcf, program);
|
|
786
819
|
registerIndex(gcf);
|
|
787
820
|
registerContext(gcf);
|
package/extension.json
CHANGED
package/hooks/pre-commit.js
CHANGED
|
@@ -13,7 +13,7 @@ import { writeIndex } from '../lib/index-gen.js';
|
|
|
13
13
|
import { lint } from '../lib/linter.js';
|
|
14
14
|
import { buildLedger } from '../lib/ledger.js';
|
|
15
15
|
import { designCheck } from '../lib/design-check.js';
|
|
16
|
-
import {
|
|
16
|
+
import { matchesFrozen } from '../lib/work-ops.js';
|
|
17
17
|
import { checkLive } from '../lib/live.js';
|
|
18
18
|
|
|
19
19
|
const projectDir = process.cwd();
|
|
@@ -87,11 +87,14 @@ for (const s of await listSystems(root)) {
|
|
|
87
87
|
if (errors.length) fail(`systems/${s.name}: ${errors[0]}`);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
// An unplayed item's body
|
|
91
|
-
// its close, a tune or a kill would take the wrong
|
|
90
|
+
// An unplayed item's body and live: are the source of its systems/ lines and its capability
|
|
91
|
+
// (Methodology §5.4): edited after its close, a tune or a kill would take the wrong ones out.
|
|
92
92
|
for (const c of ledger.items.filter((x) => x.data.status === 'unplayed')) {
|
|
93
|
-
if (
|
|
94
|
-
fail(
|
|
93
|
+
if (!matchesFrozen(c))
|
|
94
|
+
fail(
|
|
95
|
+
`work/${c.name}: an unplayed item's body and live: are frozen — restore them, tune it, ` +
|
|
96
|
+
'then edit',
|
|
97
|
+
);
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
// A unit or item leaving without its move loses its reason, not its integrity: one still citing
|
|
@@ -112,9 +115,11 @@ if (first) fail(`${first.file}:${first.line}: ${first.check}: ${first.message}`)
|
|
|
112
115
|
// one, `close` and `keep` a not-yet-kept one. A gate check would refuse the tune or the kill of
|
|
113
116
|
// a dependency whose dependent is already waiting for its verdict — a legal move, blocked.
|
|
114
117
|
|
|
115
|
-
// LIVE.md form: a malformed record would hide from every reader of the snapshot.
|
|
118
|
+
// LIVE.md form: a malformed record would hide from every reader of the snapshot. A broken ref
|
|
119
|
+
// into gcf/ is this commit's own doing (a move deleted the file); one into the code is not
|
|
120
|
+
// refused — a code commit outside gcf/ may have moved it and the gate never saw that.
|
|
116
121
|
const live = await checkLive(projectDir);
|
|
117
|
-
const form = live.problems[0] ?? live.issues.find((i) => /summary|twice
|
|
122
|
+
const form = live.problems[0] ?? live.issues.find((i) => /summary|twice|refs gcf\//.test(i));
|
|
118
123
|
if (form) fail(`LIVE.md: ${form}`);
|
|
119
124
|
|
|
120
125
|
// 5: П2 linter over systems docs.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The project's instructions for any agent (Methodology §1): a session that runs no gcf skill
|
|
2
|
+
// still has to know that work lives in gcf/ and which commands own it. `gcf init` keeps one
|
|
3
|
+
// marked block in AGENTS.md, and in a CLAUDE.md that does not import AGENTS.md already.
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { atomicWrite } from './safe-path.js';
|
|
8
|
+
|
|
9
|
+
export const START = '<!-- gcf:start -->';
|
|
10
|
+
export const END = '<!-- gcf:end -->';
|
|
11
|
+
|
|
12
|
+
/** `text` with the marked block put in place of the old one, or appended. */
|
|
13
|
+
export function withBlock(text, block) {
|
|
14
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
15
|
+
const body = `${START}${eol}${block.trim().replace(/\r?\n/g, eol)}${eol}${END}`;
|
|
16
|
+
const at = text.indexOf(START);
|
|
17
|
+
const end = text.indexOf(END, at);
|
|
18
|
+
if (at >= 0 && end > at) return text.slice(0, at) + body + text.slice(end + END.length);
|
|
19
|
+
// One blank line between the old text and the block, however the text ended.
|
|
20
|
+
const sep = !text || text.endsWith(eol + eol) ? '' : eol.repeat(text.endsWith(eol) ? 1 : 2);
|
|
21
|
+
|
|
22
|
+
return `${text}${sep}${body}${eol}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Whether the AIF project lists Claude Code among its agents (`.ai-factory.json`). */
|
|
26
|
+
async function claudeAgent(projectDir) {
|
|
27
|
+
const file = path.join(projectDir, '.ai-factory.json');
|
|
28
|
+
if (!existsSync(file)) return false;
|
|
29
|
+
try {
|
|
30
|
+
const cfg = JSON.parse(await readFile(file, 'utf8'));
|
|
31
|
+
|
|
32
|
+
return (cfg.agents ?? []).some((a) => a?.id === 'claude');
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Write the block where agents read it. AGENTS.md always gets it (created when absent — /aif
|
|
40
|
+
* writes that file, gcf-init runs after /aif). A CLAUDE.md that imports `@AGENTS.md` sees it
|
|
41
|
+
* there; one that does not gets its own copy. With Claude among the AIF agents and no
|
|
42
|
+
* CLAUDE.md, one is created importing AGENTS.md. Returns the files written, relative.
|
|
43
|
+
*/
|
|
44
|
+
export async function writeAgentsBlock(projectDir, block) {
|
|
45
|
+
const written = [];
|
|
46
|
+
const put = async (name, base) => {
|
|
47
|
+
const file = path.join(projectDir, name);
|
|
48
|
+
const old = existsSync(file) ? await readFile(file, 'utf8') : base;
|
|
49
|
+
const next = withBlock(old, block);
|
|
50
|
+
if (next === old && existsSync(file)) return;
|
|
51
|
+
await atomicWrite(file, next);
|
|
52
|
+
written.push(name);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
await put('AGENTS.md', '');
|
|
56
|
+
const claude = path.join(projectDir, 'CLAUDE.md');
|
|
57
|
+
if (existsSync(claude)) {
|
|
58
|
+
const text = await readFile(claude, 'utf8');
|
|
59
|
+
if (!/^@AGENTS\.md\s*$/m.test(text) || text.includes(START)) await put('CLAUDE.md', text);
|
|
60
|
+
} else if (await claudeAgent(projectDir)) {
|
|
61
|
+
await atomicWrite(claude, '@AGENTS.md\n');
|
|
62
|
+
written.push('CLAUDE.md');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return written;
|
|
66
|
+
}
|
package/lib/context.js
CHANGED
|
@@ -5,7 +5,7 @@ import { listItems, listSystems, readConfig, tokensEst } from './gcf-paths.js';
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Readset assembly — the whole command is deterministic (GCF-PRD §9.1, §10.1):
|
|
8
|
-
* PILLARS + LIVE.md + touched systems + the item + its depends + graveyard tag hits +
|
|
8
|
+
* PILLARS + LIVE.md + touched systems + the item + its parent + its depends + graveyard tag hits +
|
|
9
9
|
* D2 assumptions whose sys overlaps. Never over_budget-throws: the caller gates.
|
|
10
10
|
*/
|
|
11
11
|
export async function buildContext(projectDir, itemId) {
|
|
@@ -36,14 +36,19 @@ export async function buildContext(projectDir, itemId) {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* PILLARS, LIVE.md, touched systems, the item, its parent feature (the goal a child serves),
|
|
41
|
+
* its depends — in order, skipping absent files.
|
|
42
|
+
*/
|
|
40
43
|
async function readsetFiles(root, item, items) {
|
|
41
44
|
const files = [];
|
|
42
45
|
const push = async (rel) => {
|
|
43
46
|
const abs = path.join(root, rel);
|
|
44
|
-
|
|
47
|
+
const at = `gcf/${rel.replaceAll('\\', '/')}`;
|
|
48
|
+
// A parent that is also a dependency is read once and counted once against the budget.
|
|
49
|
+
if (!existsSync(abs) || files.some((f) => f.path === at)) return;
|
|
45
50
|
const text = await readFile(abs, 'utf8');
|
|
46
|
-
files.push({ path:
|
|
51
|
+
files.push({ path: at, tokens_est: tokensEst(text) });
|
|
47
52
|
};
|
|
48
53
|
|
|
49
54
|
await push('PILLARS.md');
|
|
@@ -54,6 +59,8 @@ async function readsetFiles(root, item, items) {
|
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
await push(`work/${item.name}`);
|
|
62
|
+
const parent = items.find((c) => c.data.id === item.data.parent);
|
|
63
|
+
if (parent) await push(`work/${parent.name}`);
|
|
57
64
|
for (const depId of item.data.depends ?? []) {
|
|
58
65
|
const dep = items.find((c) => c.data.id === depId);
|
|
59
66
|
if (dep) await push(`work/${dep.name}`);
|
package/lib/design-check.js
CHANGED
|
@@ -496,6 +496,10 @@ function ledgerIssues(ledger) {
|
|
|
496
496
|
out.push({ check: 'M12', severity: 'block', file, line: 1, id, message });
|
|
497
497
|
if (live.length > 1)
|
|
498
498
|
add(`${id} lives in ${live.length} places: ${live.map((p) => p.file).join(', ')}`);
|
|
499
|
+
// An id ends once; two ends (a merge of branches that each ended it) leave its dependents
|
|
500
|
+
// counting it as played or dead depending on which line wins.
|
|
501
|
+
if (terminal.length > 1)
|
|
502
|
+
add(`${id} ended ${terminal.join(' and ')}: keep one line in history.md or graveyard.md`);
|
|
499
503
|
if (live.length && terminal.length) {
|
|
500
504
|
add(
|
|
501
505
|
`${id} is ${terminal.join(' and ')} already: ids are never reused — give the new one ` +
|
|
@@ -508,6 +512,49 @@ function ledgerIssues(ledger) {
|
|
|
508
512
|
return out;
|
|
509
513
|
}
|
|
510
514
|
|
|
515
|
+
/**
|
|
516
|
+
* M2 over `depends:` of units and items together: a cycle never gets played — every id in it
|
|
517
|
+
* waits for the next one. Each cycle is reported once, from the id DFS meets it at.
|
|
518
|
+
*/
|
|
519
|
+
export function dependsCycles(ledger) {
|
|
520
|
+
/** @type {Map<string, string[]>} */
|
|
521
|
+
const graph = new Map();
|
|
522
|
+
for (const u of ledger.design.flatMap((d) => d.units)) {
|
|
523
|
+
const list = listOf(u.header.depends ?? '');
|
|
524
|
+
graph.set(u.id, Array.isArray(list) ? list : []);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
for (const c of ledger.items) graph.set(String(c.data.id), c.data.depends ?? []);
|
|
528
|
+
/** @type {Issue[]} */
|
|
529
|
+
const out = [];
|
|
530
|
+
const state = new Map(); // id → 'open' while on the DFS path, 'done' after
|
|
531
|
+
const visit = (id, trail) => {
|
|
532
|
+
if (state.get(id) === 'done') return;
|
|
533
|
+
if (state.get(id) === 'open') {
|
|
534
|
+
const cycle = [...trail.slice(trail.indexOf(id)), id];
|
|
535
|
+
const file = ledger.where(id).live[0]?.file ?? 'design';
|
|
536
|
+
out.push({
|
|
537
|
+
check: 'M2',
|
|
538
|
+
severity: 'block',
|
|
539
|
+
file,
|
|
540
|
+
line: 1,
|
|
541
|
+
id,
|
|
542
|
+
message: `depends cycle ${cycle.join(' -> ')}: none of them can be played first`,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
state.set(id, 'open');
|
|
549
|
+
for (const d of graph.get(id) ?? []) visit(d, [...trail, id]);
|
|
550
|
+
state.set(id, 'done');
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
for (const id of graph.keys()) visit(id, []);
|
|
554
|
+
|
|
555
|
+
return out;
|
|
556
|
+
}
|
|
557
|
+
|
|
511
558
|
/** M14: backlog.md is the order of the units in design/, each exactly once. */
|
|
512
559
|
function backlogIssues(text, unitIds) {
|
|
513
560
|
/** @type {Issue[]} */
|
|
@@ -577,6 +624,31 @@ function pillarReport(units, ctx) {
|
|
|
577
624
|
};
|
|
578
625
|
}
|
|
579
626
|
|
|
627
|
+
/** A unit's line in the report: its TBD count and whether it is ready to take. */
|
|
628
|
+
function unitSummary(u, own, values, ctx) {
|
|
629
|
+
const pillars = listOf(u.header.pillars ?? '');
|
|
630
|
+
// Once PILLARS.md exists, a mechanic or resource says which pillar it serves before work:
|
|
631
|
+
// `take` would turn the TBD into an empty list and the unit would serve nothing silently.
|
|
632
|
+
const pillarsTbd =
|
|
633
|
+
pillars === 'TBD' && ctx.pillarIds.length && CARD_TYPES.includes(u.header.type) ? 1 : 0;
|
|
634
|
+
const tbd = values.filter((v) => isTbd(v.value)).length + pillarsTbd;
|
|
635
|
+
const blockedBy = ctx.questions.d1.get(u.id) ?? [];
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
id: u.id,
|
|
639
|
+
file: u.file,
|
|
640
|
+
system: u.system,
|
|
641
|
+
type: u.header.type ?? null,
|
|
642
|
+
milestone: u.header.milestone ?? 'TBD',
|
|
643
|
+
pillars,
|
|
644
|
+
pillars_owed: Boolean(pillarsTbd),
|
|
645
|
+
values: values.length,
|
|
646
|
+
tbd,
|
|
647
|
+
blocked_by: blockedBy,
|
|
648
|
+
ready: tbd === 0 && !blockedBy.length && !own.some((i) => i.severity === 'block'),
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
580
652
|
/**
|
|
581
653
|
* Run every design check over a project. `blocking` counts the issues that refuse a commit;
|
|
582
654
|
* the rest of the report (completeness, readiness, pillars) is a reading, not a gate.
|
|
@@ -591,28 +663,14 @@ export async function designCheck(projectDir) {
|
|
|
591
663
|
for (const u of ledger.design.flatMap((d) => d.units)) {
|
|
592
664
|
const { issues: own, values } = await checkUnit(u, ctx, ledger);
|
|
593
665
|
issues.push(...own);
|
|
594
|
-
|
|
595
|
-
const blockedBy = ctx.questions.d1.get(u.id) ?? [];
|
|
596
|
-
const pillars = listOf(u.header.pillars ?? '');
|
|
597
|
-
units.push({
|
|
598
|
-
id: u.id,
|
|
599
|
-
file: u.file,
|
|
600
|
-
system: u.system,
|
|
601
|
-
type: u.header.type ?? null,
|
|
602
|
-
milestone: u.header.milestone ?? 'TBD',
|
|
603
|
-
pillars,
|
|
604
|
-
values: values.length,
|
|
605
|
-
tbd,
|
|
606
|
-
blocked_by: blockedBy,
|
|
607
|
-
ready: tbd === 0 && !blockedBy.length && !own.some((i) => i.severity === 'block'),
|
|
608
|
-
});
|
|
666
|
+
units.push(unitSummary(u, own, values, ctx));
|
|
609
667
|
}
|
|
610
668
|
|
|
611
669
|
for (const c of ledger.items) issues.push(...(await checkItem(c, ctx, ledger)));
|
|
612
670
|
issues.push(...(await unplayedIssues(root, ledger)));
|
|
613
671
|
for (const doc of ['game', 'glossary']) issues.push(...(await checkDoc(root, doc, ctx)));
|
|
614
672
|
issues.push(...preambleIssues(ledger.design));
|
|
615
|
-
issues.push(...ledgerIssues(ledger), ...questionIssues(ctx, ledger));
|
|
673
|
+
issues.push(...ledgerIssues(ledger), ...dependsCycles(ledger), ...questionIssues(ctx, ledger));
|
|
616
674
|
if (ledger.design.length) {
|
|
617
675
|
const backlog = await readIf(path.join(root, 'backlog.md'));
|
|
618
676
|
issues.push(...backlogIssues(backlog, new Set(units.map((u) => u.id))));
|
package/lib/live.js
CHANGED
|
@@ -117,13 +117,25 @@ export function applyDelta(capabilities, delta, itemId) {
|
|
|
117
117
|
/**
|
|
118
118
|
* The capabilities without what `delta` added: an unplayed mechanic killed, dropped or sent back
|
|
119
119
|
* to tuning leaves the game, so its record leaves LIVE.md with it. `modified` and `removed`
|
|
120
|
-
* cannot be undone from the delta alone and stay as they are.
|
|
120
|
+
* cannot be undone from the delta alone and stay as they are. A record another item modified
|
|
121
|
+
* since is refused: taking it out would erase that item's accepted change.
|
|
121
122
|
*/
|
|
122
|
-
export function revertDelta(capabilities, delta) {
|
|
123
|
+
export function revertDelta(capabilities, delta, itemId) {
|
|
123
124
|
if (!delta || 'none' in delta) return capabilities;
|
|
124
|
-
const added = delta.changes.filter((c) => c.change === 'added')
|
|
125
|
+
const added = delta.changes.filter((c) => c.change === 'added');
|
|
126
|
+
for (const c of added) {
|
|
127
|
+
const now = capabilities.find((r) => r.id === c.id);
|
|
128
|
+
// LIVE.md keeps fields trimmed: compare in that form, or a trailing space reads as an edit.
|
|
129
|
+
const same = (a, b) => String(a ?? '').trim() === String(b ?? '').trim();
|
|
130
|
+
if (now && !(same(now.summary, c.summary) && same(now.refs, c.refs)))
|
|
131
|
+
throw new Error(
|
|
132
|
+
`${itemId}: capability ${c.id} was modified after ${itemId} added it — taking it out ` +
|
|
133
|
+
'would erase that change: remove the record from LIVE.md by hand in its own commit ' +
|
|
134
|
+
'and repeat, or keep the item',
|
|
135
|
+
);
|
|
136
|
+
}
|
|
125
137
|
|
|
126
|
-
return capabilities.filter((r) => !added.
|
|
138
|
+
return capabilities.filter((r) => !added.some((c) => c.id === r.id));
|
|
127
139
|
}
|
|
128
140
|
|
|
129
141
|
/** The known-issue record of a parked bug: its title or first intent bullet, and its file. */
|
package/lib/pillars.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Revising PILLARS.md after units and items cite it: a rename follows the id everywhere it is
|
|
2
|
+
// cited, a retire takes it out and sends every unit left without a pillar back to the human.
|
|
3
|
+
// Pillars are human authorship (gcf-init): these moves only carry out the human's decision.
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { serialize } from './frontmatter.js';
|
|
8
|
+
import { SLUG_RE, listOf, parseDesign } from './design.js';
|
|
9
|
+
import { recordsIn } from './design-check.js';
|
|
10
|
+
import { buildLedger } from './ledger.js';
|
|
11
|
+
import { atomicWrite } from './safe-path.js';
|
|
12
|
+
import { commitMove } from './unit-ops.js';
|
|
13
|
+
|
|
14
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
15
|
+
|
|
16
|
+
/** The `id: <id>` line of a PILLARS.md record; `\r` and the line break stay outside it. */
|
|
17
|
+
const idLine = (id) =>
|
|
18
|
+
new RegExp(`^([ \\t]*(?:- )?id:[ \\t]*)${escapeRe(id)}[ \\t]*(?=\\r?$)`, 'm');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The design file with `edit` applied to each unit's header `pillars:` line — the header only:
|
|
22
|
+
* a `pillars:` line in a body or the preamble is not a citation. TBD stays TBD; a list left
|
|
23
|
+
* empty reads TBD, so the unit is not ready until the human gives it a pillar again.
|
|
24
|
+
*/
|
|
25
|
+
function rewriteHeaders(text, edit) {
|
|
26
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
27
|
+
const lines = text.split(/\r?\n/);
|
|
28
|
+
for (const u of parseDesign(text, '').units) {
|
|
29
|
+
const at = u.headerLines.pillars;
|
|
30
|
+
const list = listOf(u.header.pillars ?? '');
|
|
31
|
+
if (at === undefined || list === 'TBD') continue;
|
|
32
|
+
const next = edit(list);
|
|
33
|
+
if (next.join() !== list.join())
|
|
34
|
+
lines[at - 1] = `pillars: ${next.length ? `[${next.join(', ')}]` : 'TBD'}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return lines.join(eol);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Apply `edit` to every citing list — unit headers in design/, item frontmatter in work/ — and
|
|
42
|
+
* `editPillars` to PILLARS.md, in one commit `gcf(pillar): <tail>`. Returns the items left
|
|
43
|
+
* citing no pillar: an item in work has no TBD to fall back to, so the human hears about it.
|
|
44
|
+
*/
|
|
45
|
+
async function revise(projectDir, id, { edit, editPillars, tail, commit }) {
|
|
46
|
+
const root = path.join(path.resolve(projectDir), 'gcf');
|
|
47
|
+
const pillarsFile = path.join(root, 'PILLARS.md');
|
|
48
|
+
const text = existsSync(pillarsFile) ? await readFile(pillarsFile, 'utf8') : '';
|
|
49
|
+
const ids = recordsIn(text, 'pillars').map((p) => p.id);
|
|
50
|
+
if (!ids.includes(id)) throw new Error(`${id} is not a pillar of gcf/PILLARS.md`);
|
|
51
|
+
const ledger = await buildLedger(projectDir);
|
|
52
|
+
const designFiles = ledger.design.map((d) => path.join(root, d.file));
|
|
53
|
+
const items = ledger.items.filter((c) => (c.data.pillars ?? []).includes(id));
|
|
54
|
+
const files = [pillarsFile, ...designFiles, ...items.map((c) => c.file)];
|
|
55
|
+
const mutate = async () => {
|
|
56
|
+
await atomicWrite(pillarsFile, editPillars(text));
|
|
57
|
+
for (const f of designFiles) {
|
|
58
|
+
const old = await readFile(f, 'utf8');
|
|
59
|
+
const next = rewriteHeaders(old, edit);
|
|
60
|
+
if (next !== old) await atomicWrite(f, next);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const c of items) {
|
|
64
|
+
await atomicWrite(c.file, serialize({ ...c.data, pillars: edit(c.data.pillars) }, c.body));
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
id,
|
|
70
|
+
hash: await commitMove(projectDir, `gcf(pillar): ${tail}`, { files, mutate, commit }),
|
|
71
|
+
bare: items.filter((c) => !edit(c.data.pillars).length).map((c) => String(c.data.id)),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** `pillar rename <from> <to>`: the id changes in PILLARS.md and every unit and item citing it. */
|
|
76
|
+
export async function renamePillar(projectDir, from, to, { commit = true } = {}) {
|
|
77
|
+
if (!SLUG_RE.test(to)) throw new Error(`${to}: a pillar id is a lowercase hyphen slug`);
|
|
78
|
+
const text = await readFile(path.join(projectDir, 'gcf', 'PILLARS.md'), 'utf8').catch(() => '');
|
|
79
|
+
if (recordsIn(text, 'pillars').some((p) => p.id === to))
|
|
80
|
+
throw new Error(`${to} is a pillar already`);
|
|
81
|
+
|
|
82
|
+
return revise(projectDir, from, {
|
|
83
|
+
edit: (list) => list.map((p) => (p === from ? to : p)),
|
|
84
|
+
editPillars: (t) => t.replace(idLine(from), (_, head) => `${head}${to}`),
|
|
85
|
+
tail: `rename ${from} → ${to}`,
|
|
86
|
+
commit,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* `pillar retire <id> --reason`: the record leaves PILLARS.md and the id leaves every list. A
|
|
92
|
+
* unit left citing no pillar reads `pillars: TBD` again — it is not ready until the human gives
|
|
93
|
+
* it one, or decides it is out of scope. The reason is kept in the commit.
|
|
94
|
+
*/
|
|
95
|
+
export async function retirePillar(projectDir, id, { reason, commit = true }) {
|
|
96
|
+
const why = String(reason ?? '')
|
|
97
|
+
.replace(/\s+/g, ' ')
|
|
98
|
+
.trim();
|
|
99
|
+
if (!why) throw new Error('a retire needs a reason: the commit keeps it');
|
|
100
|
+
const own = idLine(id);
|
|
101
|
+
|
|
102
|
+
return revise(projectDir, id, {
|
|
103
|
+
edit: (list) => list.filter((p) => p !== id),
|
|
104
|
+
// A record starts at a `- ` line; the one holding `id: <id>` goes, blank lines and all.
|
|
105
|
+
editPillars: (t) =>
|
|
106
|
+
t
|
|
107
|
+
.split(/\n(?=- )/)
|
|
108
|
+
.filter((rec) => !own.test(rec))
|
|
109
|
+
.join('\n'),
|
|
110
|
+
tail: `retire ${id}: ${why}`,
|
|
111
|
+
commit,
|
|
112
|
+
});
|
|
113
|
+
}
|
package/lib/unit-ops.js
CHANGED
|
@@ -129,7 +129,8 @@ function refuseUnready(units, summaries) {
|
|
|
129
129
|
if (summary?.ready) continue;
|
|
130
130
|
const why = summary?.blocked_by.length
|
|
131
131
|
? `blocked by ${summary.blocked_by.join(', ')}`
|
|
132
|
-
: `${summary?.tbd ?? '?'} TBD
|
|
132
|
+
: `${summary?.tbd ?? '?'} TBD${summary?.pillars_owed ? ', pillars among them' : ''} ` +
|
|
133
|
+
'or a blocking design-check finding';
|
|
133
134
|
throw new Error(`${unit.id} is not ready (${why}): answer it in design/ first`);
|
|
134
135
|
}
|
|
135
136
|
}
|
package/lib/verdict-fs.js
CHANGED
|
@@ -114,8 +114,14 @@ export async function buryLine(root, id, { type, reason, tags }) {
|
|
|
114
114
|
// A hand-edited file may end without a newline: the line would glue onto the last one.
|
|
115
115
|
const eol = existing.match(/\r?\n/)?.[0] ?? '\n';
|
|
116
116
|
const head = existing && !existing.endsWith('\n') ? existing + eol : existing;
|
|
117
|
-
|
|
118
|
-
const
|
|
117
|
+
// One record per line: a line break would split the reason from its tags, a pipe its fields.
|
|
118
|
+
const flat = (s) =>
|
|
119
|
+
String(s)
|
|
120
|
+
.replace(/\s*\|\s*/g, '; ')
|
|
121
|
+
.replace(/\s*\r?\n\s*/g, ' ')
|
|
122
|
+
.trim();
|
|
123
|
+
const why = flat(reason || 'no reason recorded');
|
|
124
|
+
const line = `- ${id} | ${type} | ${why} | tags: ${tags.map(flat).join(', ')}${eol}`;
|
|
119
125
|
await atomicWrite(gyFile, head + line);
|
|
120
126
|
}
|
|
121
127
|
|
package/lib/work-ops.js
CHANGED
|
@@ -90,9 +90,25 @@ export function openDepends(ledger, depends = [], { waiting = false } = {}) {
|
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
/**
|
|
94
|
+
* The hash an unplayed item is frozen at (Methodology §5.4): its body and its `live:` delta —
|
|
95
|
+
* the two things a tune or a kill takes back out of systems/ and LIVE.md.
|
|
96
|
+
*/
|
|
97
|
+
export const frozenHash = (item) =>
|
|
98
|
+
createHash('sha1')
|
|
99
|
+
.update(String(item.body).replace(/\r\n/g, '\n').trim())
|
|
100
|
+
.update(`\nlive: ${JSON.stringify(item.data.live ?? null)}`)
|
|
101
|
+
.digest('hex');
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Whether an unplayed item still matches its `frozen:`. An item closed by 0.4.0 holds the sha1
|
|
105
|
+
* of its body alone: it still guards the body. shortcut: such an item's live: stays unguarded
|
|
106
|
+
* until it is closed again; drop the legacy form once no 0.4.0 unplayed item is left.
|
|
107
|
+
*/
|
|
108
|
+
export const matchesFrozen = (item) =>
|
|
109
|
+
item.data.frozen === frozenHash(item) ||
|
|
110
|
+
item.data.frozen ===
|
|
111
|
+
createHash('sha1').update(String(item.body).replace(/\r\n/g, '\n').trim()).digest('hex');
|
|
96
112
|
|
|
97
113
|
/** Children of a feature that are not finished: live in any status but unplayed and parked. */
|
|
98
114
|
function unfinishedChildren(ledger, feature) {
|
|
@@ -160,6 +176,14 @@ const without = (data, ...keys) =>
|
|
|
160
176
|
* another unplayed item also wrote stays: that item's body still owes it to systems/.
|
|
161
177
|
*/
|
|
162
178
|
function unplay(m, item) {
|
|
179
|
+
// The gate checks `frozen` only on a file that stays unplayed: a tune or a kill of an edited
|
|
180
|
+
// item would take the edited lines out and leave the frozen ones in systems/ for good.
|
|
181
|
+
if (!matchesFrozen(item))
|
|
182
|
+
throw new Error(
|
|
183
|
+
`${item.data.id}: an unplayed item's body and live: are frozen, and this one was edited — ` +
|
|
184
|
+
`save the edit aside, restore the file (git restore gcf/work/${item.data.id}.md ` +
|
|
185
|
+
'discards it), make the move, then apply the edit to the ready item',
|
|
186
|
+
);
|
|
163
187
|
const systems = item.data.system ?? [];
|
|
164
188
|
m.leaving.add(String(item.data.id));
|
|
165
189
|
m.touch(...systemFiles(m.root, systems));
|
|
@@ -170,7 +194,15 @@ function unplay(m, item) {
|
|
|
170
194
|
.flatMap((i) => Object.values(collapseLines(i.body)).flat());
|
|
171
195
|
for (const s of systems) await unmergeFromSystem(m.root, s, item, keep);
|
|
172
196
|
});
|
|
173
|
-
m.live((l) => ({
|
|
197
|
+
m.live((l) => ({
|
|
198
|
+
...l,
|
|
199
|
+
// The delta as the close applied it, refs included, so a later refs-only edit shows.
|
|
200
|
+
capabilities: revertDelta(
|
|
201
|
+
l.capabilities,
|
|
202
|
+
withRefs(item.data.live, detailOf(item, null)),
|
|
203
|
+
item.data.id,
|
|
204
|
+
),
|
|
205
|
+
}));
|
|
174
206
|
}
|
|
175
207
|
|
|
176
208
|
const dropKnownIssue = (m, id) =>
|
|
@@ -380,7 +412,7 @@ function closeUnplayed(m) {
|
|
|
380
412
|
});
|
|
381
413
|
const delta = withRefs(item.data.live, detailOf(item, null));
|
|
382
414
|
m.live((l) => ({ ...l, capabilities: applyDelta(l.capabilities, delta, id) }));
|
|
383
|
-
m.step(() => writeItem(item, { ...item.data, status: 'unplayed', frozen:
|
|
415
|
+
m.step(() => writeItem(item, { ...item.data, status: 'unplayed', frozen: frozenHash(item) }));
|
|
384
416
|
}
|
|
385
417
|
|
|
386
418
|
/** Any other gate-auto item ends in history; a hard unit may leave lines in `constraints:`. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gcf-aif-ext",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Game Context Framework for AI Factory",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
"node": ">=22.14"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"test": "node --test",
|
|
28
|
-
"prepack": "node -e \"const p=require('./package.json'),e=require('./extension.json');if(p.version!==e.version)throw new Error('version mismatch: package.json '+p.version+' vs extension.json '+e.version)\""
|
|
27
|
+
"test": "node --test && npm run test:gate",
|
|
28
|
+
"prepack": "node -e \"const p=require('./package.json'),e=require('./extension.json');if(p.version!==e.version)throw new Error('version mismatch: package.json '+p.version+' vs extension.json '+e.version)\"",
|
|
29
|
+
"test:gate": "node -e \"process.env.GCF_TEST_GATE='1';require('node:child_process').execFileSync(process.execPath,['--test'],{stdio:'inherit'})\""
|
|
29
30
|
},
|
|
30
31
|
"files": [
|
|
31
32
|
"extension.json",
|
package/skills/gcf-card/SKILL.md
CHANGED
|
@@ -39,12 +39,14 @@ workflow:
|
|
|
39
39
|
7. for a unit with `flags: src-changed` re-read each changed anchor with `ai-factory gcf section`, rebuild the slots whose quote moved, and remove the flag
|
|
40
40
|
8. confirm the route: ask "can an autotest accept this without a human playing?"; yes = hard, no or mixed = soft; a mixed unit splits into a soft mechanic and a hard rule before work
|
|
41
41
|
9. ask the human one question per `TBD` slot, each with your proposed answer, questions per message <= 2
|
|
42
|
-
10.
|
|
43
|
-
11.
|
|
44
|
-
12.
|
|
45
|
-
13.
|
|
46
|
-
14.
|
|
47
|
-
15.
|
|
42
|
+
10. for `pillars: TBD` propose the ids from `gcf/PILLARS.md` whose statement the unit serves and whose invariant it keeps
|
|
43
|
+
11. when no pillar fits, ask whether the unit is out of scope or a pillar is missing, and write a D1 question for the answer
|
|
44
|
+
12. write each answer into the unit in place, with its `answer:` provenance record
|
|
45
|
+
13. run `ai-factory gcf design check --json` until the unit's entry in `units` reads `ready: true`, then commit the answered unit
|
|
46
|
+
14. run `ai-factory gcf take <id>` for one unit, or `ai-factory gcf take <id> <id> --feature <feature-id>` to group several under a feature; it writes `gcf/work/<id>.md` and removes the sections and backlog lines in one step
|
|
47
|
+
15. in each design-unit item write `contract:` from the code tree and `interactions:` from the units and items that write the same state; a soft unit whose human wants no playtest gets `gate: auto` - it will close as unplayed
|
|
48
|
+
16. write the item's `live:` delta (a feature's children keep `none: parent`; the feature writes the capability), run `ai-factory gcf design check`, and commit
|
|
49
|
+
17. report the item paths, each item's gate, and for a verdict item the one question the playtest will answer
|
|
48
50
|
|
|
49
51
|
invocation:
|
|
50
52
|
- run when work starts on a unit or a group of units, when non-design work needs an item, or when a fresh idea needs a home
|
package/skills/gcf-init/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gcf-init
|
|
3
|
-
description: Bootstrap the gcf/ documentary layer - skeleton, pillars interview, hooks reminder. Use when a game project adopts GCF
|
|
3
|
+
description: Bootstrap the gcf/ documentary layer - skeleton, pillars interview, hooks reminder - and revise pillars later. Use when a game project adopts GCF, when the user says "set up gcf", or "поменяем столпы" / "revise pillars".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
mode: bootstrap
|
|
@@ -21,13 +21,16 @@ forbidden_behaviors:
|
|
|
21
21
|
|
|
22
22
|
workflow:
|
|
23
23
|
1. when `.ai-factory/DESCRIPTION.md` is absent, tell the human to run `/aif` first: `aif-plan` reads it, and a work item cannot reach a plan without it
|
|
24
|
-
2. run `ai-factory gcf init` and report which files it created, `gcf/design/` among them; it writes no `PILLARS.md
|
|
24
|
+
2. run `ai-factory gcf init` and report which files it created, `gcf/design/` and the agent instructions in `AGENTS.md` among them; it writes no `PILLARS.md`
|
|
25
25
|
3. when the human is about to import a GDD, leave `PILLARS.md` absent: the import proposes pillar candidates, and the interview runs after it
|
|
26
26
|
4. otherwise ask the human for the game's core promise, then for 2..7 pillars: each needs an id (a lowercase hyphen slug the design units cite), a statement, a negative invariant and a review question
|
|
27
27
|
5. write `gcf/PILLARS.md` with the confirmed pillars in the record-list form of `.ai-factory/extensions/gcf-aif-ext/templates/PILLARS.md`
|
|
28
28
|
6. run `npx --yes cnlp-kit check` and fix form issues it reports
|
|
29
29
|
7. ask the human to run `ai-factory gcf hooks-install`: the commit gate is theirs to arm
|
|
30
30
|
8. state the next step: import a GDD with `/gcf-ingest`, or start the design with `/gcf-vision` when there is none
|
|
31
|
+
9. to add one pillar later, ask only for its id, statement, invariant and review question; append it as one record to `gcf/PILLARS.md`, keep the others, keep the count <= 7, then run step 6
|
|
32
|
+
10. to rename a pillar, run `ai-factory gcf pillar rename <from> <to>`: every unit and item citing it follows
|
|
33
|
+
11. to drop a pillar, run `ai-factory gcf pillar retire <id> --reason "<why>"`; a unit it leaves at `pillars: TBD` goes back through the pillars steps of the gcf-card skill
|
|
31
34
|
|
|
32
35
|
invocation:
|
|
33
|
-
- run when the user asks to set up gcf in a project
|
|
36
|
+
- run when the user asks to set up gcf in a project, or to revise its pillars
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
## Game Context Framework (from gcf-aif-ext)
|
|
2
|
+
|
|
3
|
+
gcf_context:
|
|
4
|
+
- the game's design and its work live in `gcf/`, and the `ai-factory gcf` commands and the gcf skills change them
|
|
5
|
+
- read `gcf/INDEX.md` before anything else under `gcf/`: it lists every subsystem, design unit and work item
|
|
6
|
+
- `gcf/LIVE.md` says what the game does now, and `gcf/PILLARS.md` says what it must stay
|
|
7
|
+
|
|
8
|
+
gcf_work:
|
|
9
|
+
- every piece of work is one work item `gcf/work/<id>.md`: find the item before writing code for a feature, a bug or a tech task, or create it with the gcf-card skill
|
|
10
|
+
- a raw idea, a playtest note or a CI finding that is not work yet goes to `ai-factory gcf inbox add "<text>"`, and the gcf-triage skill sorts it later
|
|
11
|
+
- before planning an item, run `ai-factory gcf context <id>` and read only the files it lists under `gcf/`
|
|
12
|
+
- drive a ready item with the gcf-run skill when the project has gcf-harness-aif-ext, otherwise with aif-plan and aif-implement
|
|
13
|
+
- end the work on an item with its command: `ai-factory gcf submit <id>` for gate verdict or decision, `ai-factory gcf close <id>` for gate auto with every check green
|
|
14
|
+
- work that cannot go on: run `ai-factory gcf block <id> --reason "<why or the question>"`
|
|
15
|
+
- a design question only the human can answer becomes a D1 record in `gcf/open-questions.md`
|
|
16
|
+
|
|
17
|
+
gcf_forbidden:
|
|
18
|
+
- do not edit `status:`, `frozen:`, `gcf/LIVE.md`, `gcf/history.md`, `gcf/graveyard.md` or `gcf/INDEX.md` by hand: the commands own them
|
|
19
|
+
- do not give a verdict or a decision: `verdict` and `decide` belong to the human, through the gcf-verdict skill
|
|
20
|
+
- do not answer a `TBD` of a design unit yourself: ask the human and offer your draft as the proposed answer
|
|
21
|
+
- do not reuse an ended id: rework gets a new id with `reopens: <old id>`
|
|
22
|
+
- do not commit past the gate with `--no-verify`: a refused commit names what to fix
|
|
23
|
+
|
|
24
|
+
gcf_skills:
|
|
25
|
+
- gcf-vision starts a game that has no design yet, and gcf-ingest imports a GDD into `gcf/design/`
|
|
26
|
+
- gcf-card puts work into `gcf/work/`, and gcf-triage sorts `gcf/inbox.md`
|
|
27
|
+
- gcf-verdict records the human's verdict or decision, and gcf-audit reports the health of `gcf/`
|
|
28
|
+
- gcf-debate stresses the design, gcf-adopt puts existing code on GCF, and gcf-init sets up `gcf/` and revises the pillars
|