gcf-aif-ext 0.7.14 → 0.8.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/commands/gcf.js +47 -2
- package/extension.json +1 -1
- package/lib/aif-config.js +22 -0
- package/lib/board.js +24 -0
- package/lib/design-check.js +41 -5
- package/lib/inbox.js +105 -15
- package/lib/lessons.js +10 -1
- package/lib/unit-ops.js +16 -1
- package/lib/verify-stamp.js +8 -1
- package/lib/work-ops.js +66 -0
- package/package.json +1 -1
- package/skills/gcf-card/SKILL.md +3 -2
- package/skills/gcf-triage/SKILL.md +3 -2
- package/templates/agents-block.md +45 -42
package/commands/gcf.js
CHANGED
|
@@ -731,6 +731,27 @@ function registerItem(gcf) {
|
|
|
731
731
|
});
|
|
732
732
|
}
|
|
733
733
|
|
|
734
|
+
function registerDepends(gcf) {
|
|
735
|
+
gcf
|
|
736
|
+
.command('depends <id>')
|
|
737
|
+
.description(
|
|
738
|
+
'Change the items it waits for: refuses an unknown or killed id and a cycle (commit)',
|
|
739
|
+
)
|
|
740
|
+
.option('--add <ids>', 'ids to add, comma-separated')
|
|
741
|
+
.option('--remove <ids>', 'ids to remove, comma-separated')
|
|
742
|
+
.option('--no-commit', 'skip the commit (tests only)')
|
|
743
|
+
.action(async (id, o) => {
|
|
744
|
+
const r = await (
|
|
745
|
+
await lib('work-ops')
|
|
746
|
+
).setDepends(process.cwd(), id, {
|
|
747
|
+
add: splitList(o.add),
|
|
748
|
+
remove: splitList(o.remove),
|
|
749
|
+
commit: o.commit,
|
|
750
|
+
});
|
|
751
|
+
console.log(`${id} depends on ${r.depends.join(', ') || 'nothing'}${shortHash(r.hash)}`);
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
734
755
|
function registerItemMoves(gcf) {
|
|
735
756
|
const ops = () => lib('work-ops');
|
|
736
757
|
gcf
|
|
@@ -741,6 +762,7 @@ function registerItemMoves(gcf) {
|
|
|
741
762
|
const r = await (await ops()).prioritizeItems(process.cwd(), ids, { commit: o.commit });
|
|
742
763
|
console.log(`ranked ${r.ids.join(', ')}${shortHash(r.hash)}`);
|
|
743
764
|
});
|
|
765
|
+
registerDepends(gcf);
|
|
744
766
|
const question = gcf.command('question').description('Open questions (gcf/open-questions.md)');
|
|
745
767
|
question
|
|
746
768
|
.command('answer <qid>')
|
|
@@ -861,6 +883,26 @@ async function inboxMove(commit, message, edit) {
|
|
|
861
883
|
});
|
|
862
884
|
}
|
|
863
885
|
|
|
886
|
+
function registerInboxTag(inbox) {
|
|
887
|
+
inbox
|
|
888
|
+
.command('tag <in-id>')
|
|
889
|
+
.description('Add tags to an entry and remove others')
|
|
890
|
+
.option('--add <tags>', 'tags to add, comma-separated')
|
|
891
|
+
.option('--remove <tags>', 'tags to remove, comma-separated')
|
|
892
|
+
.option('--commit', 'commit it (gcf(inbox)): for a caller with no console, e.g. a board')
|
|
893
|
+
.action(async (id, o) => {
|
|
894
|
+
const { tagInbox } = await lib('inbox');
|
|
895
|
+
let tags = [];
|
|
896
|
+
const hash = await inboxMove(o.commit, `gcf(inbox): tag ${id}`, async (root) => {
|
|
897
|
+
({ tags } = await tagInbox(root, id, {
|
|
898
|
+
add: splitList(o.add),
|
|
899
|
+
remove: splitList(o.remove),
|
|
900
|
+
}));
|
|
901
|
+
});
|
|
902
|
+
console.log(`${id} tags: ${tags.join(', ') || 'none'}${shortHash(hash)}`);
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
864
906
|
function registerInbox(gcf) {
|
|
865
907
|
const inbox = gcf
|
|
866
908
|
.command('inbox')
|
|
@@ -874,8 +916,9 @@ function registerInbox(gcf) {
|
|
|
874
916
|
? l
|
|
875
917
|
.map((e) => {
|
|
876
918
|
const source = e.kind ? `lesson:${e.kind}:${e.item}` : e.source;
|
|
919
|
+
const tags = e.tags.length ? ` (tags: ${e.tags.join(', ')})` : '';
|
|
877
920
|
|
|
878
|
-
return `${e.id} [${source}] ${e.text}`;
|
|
921
|
+
return `${e.id} [${source}] ${e.text}${tags}`;
|
|
879
922
|
})
|
|
880
923
|
.join('\n')
|
|
881
924
|
: 'inbox empty',
|
|
@@ -888,6 +931,7 @@ function registerInbox(gcf) {
|
|
|
888
931
|
// A text that starts with a hyphen ("-5 fps on level 3") reads as a flag at every level of
|
|
889
932
|
// ai-factory's commands, `--` included: as `--text=<text>` it stays one value.
|
|
890
933
|
.option('--text <text>', 'the entry, when it starts with a hyphen')
|
|
934
|
+
.option('--tags <tags>', 'its tags, comma-separated: idea, later, ui:hud')
|
|
891
935
|
.option('--commit', 'commit it (gcf(inbox)): for a caller with no console, e.g. a board')
|
|
892
936
|
.action(async (arg, o) => {
|
|
893
937
|
const text = o.text ?? arg;
|
|
@@ -896,10 +940,11 @@ function registerInbox(gcf) {
|
|
|
896
940
|
const { addInbox } = await lib('inbox');
|
|
897
941
|
let id = '';
|
|
898
942
|
const hash = await inboxMove(o.commit, 'gcf(inbox): add', async (root) => {
|
|
899
|
-
id = (await addInbox(root, text, o.source)).id;
|
|
943
|
+
id = (await addInbox(root, text, o.source, { tags: splitList(o.tags) })).id;
|
|
900
944
|
});
|
|
901
945
|
console.log(`${id}${shortHash(hash)}`);
|
|
902
946
|
});
|
|
947
|
+
registerInboxTag(inbox);
|
|
903
948
|
inbox
|
|
904
949
|
.command('drop <in-id>')
|
|
905
950
|
.description('Erase an entry: triaged as noise, a duplicate, or turned into an item')
|
package/extension.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.8.0",
|
|
5
5
|
"description": "Game Context Framework for AI Factory",
|
|
6
6
|
"skills": [
|
|
7
7
|
"skills/gcf-init",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// The language AI Factory sets in `.ai-factory/config.yaml` for what an agent writes to the
|
|
2
|
+
// human: `language.ui`. GCF adds no setting of its own (Methodology §5.2.3); agents read the
|
|
3
|
+
// file themselves, the core reads it only for the prompts it writes.
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import YAML from 'yaml';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The project's `language.ui`, `en` by default. A missing or unreadable file gives `en`: the
|
|
10
|
+
* language is a preference, and a broken one must not stop the work.
|
|
11
|
+
*/
|
|
12
|
+
export async function uiLanguage(projectDir) {
|
|
13
|
+
try {
|
|
14
|
+
const file = path.join(path.resolve(projectDir), '.ai-factory', 'config.yaml');
|
|
15
|
+
const ui = YAML.parse(await readFile(file, 'utf8'))?.language?.ui;
|
|
16
|
+
|
|
17
|
+
// A language name or code, one word: anything else would break the prompt line it goes in.
|
|
18
|
+
return typeof ui === 'string' && /^[\w-]{2,32}$/.test(ui.trim()) ? ui.trim() : 'en';
|
|
19
|
+
} catch {
|
|
20
|
+
return 'en';
|
|
21
|
+
}
|
|
22
|
+
}
|
package/lib/board.js
CHANGED
|
@@ -134,6 +134,26 @@ async function phaseOf(projectDir) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* The AI Factory extensions the project has, name → version, from `.ai-factory.json`: the board
|
|
139
|
+
* shows which framework packages a project runs. `{}` when the file is missing or unreadable.
|
|
140
|
+
*/
|
|
141
|
+
async function extensionsOf(projectDir) {
|
|
142
|
+
try {
|
|
143
|
+
const cfg = JSON.parse(
|
|
144
|
+
await readFile(path.join(path.resolve(projectDir), '.ai-factory.json'), 'utf8'),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return Object.fromEntries(
|
|
148
|
+
(cfg.extensions ?? [])
|
|
149
|
+
.filter((e) => typeof e?.name === 'string')
|
|
150
|
+
.map((e) => [e.name, typeof e.version === 'string' ? e.version : null]),
|
|
151
|
+
);
|
|
152
|
+
} catch {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
137
157
|
/** The release block with every field: one a malformed block lacks is null (live check says so). */
|
|
138
158
|
const releaseOf = (r) =>
|
|
139
159
|
r && {
|
|
@@ -274,6 +294,9 @@ export async function board(projectDir) {
|
|
|
274
294
|
const inbox = await listInbox(root);
|
|
275
295
|
// Where agents struggle: the lessons in the inbox each item's sessions left.
|
|
276
296
|
for (const i of items) i.lessons = inbox.filter((e) => e.item === i.id).length;
|
|
297
|
+
// The reverse of depends: the items on the board that wait for this one.
|
|
298
|
+
for (const i of items)
|
|
299
|
+
i.needed_by = items.filter((o) => o.depends.includes(i.id)).map((o) => o.id);
|
|
277
300
|
|
|
278
301
|
const config = await readConfig(root);
|
|
279
302
|
const live = await readLive(root);
|
|
@@ -291,6 +314,7 @@ export async function board(projectDir) {
|
|
|
291
314
|
compact_pct: config.auto_compact_pct,
|
|
292
315
|
},
|
|
293
316
|
phase: await phaseOf(projectDir),
|
|
317
|
+
extensions: await extensionsOf(projectDir),
|
|
294
318
|
// A malformed block reads with fields missing (live check names it): null, not absent.
|
|
295
319
|
release: releaseOf(live.release),
|
|
296
320
|
columns: Object.fromEntries(
|
package/lib/design-check.js
CHANGED
|
@@ -557,10 +557,10 @@ function ledgerIssues(ledger) {
|
|
|
557
557
|
}
|
|
558
558
|
|
|
559
559
|
/**
|
|
560
|
-
*
|
|
561
|
-
*
|
|
560
|
+
* `depends:` of every unit and item: id → the ids it waits for. A feature also waits for its
|
|
561
|
+
* children: it closes only after them, so a child that depends on it never gets played.
|
|
562
562
|
*/
|
|
563
|
-
export function
|
|
563
|
+
export function dependsGraph(ledger) {
|
|
564
564
|
/** @type {Map<string, string[]>} */
|
|
565
565
|
const graph = new Map();
|
|
566
566
|
for (const u of ledger.design.flatMap((d) => d.units)) {
|
|
@@ -568,7 +568,43 @@ export function dependsCycles(ledger) {
|
|
|
568
568
|
graph.set(u.id, Array.isArray(list) ? list : []);
|
|
569
569
|
}
|
|
570
570
|
|
|
571
|
-
for (const c of ledger.items)
|
|
571
|
+
for (const c of ledger.items) {
|
|
572
|
+
const children = c.data.type === 'feature' ? (c.data.children ?? []) : [];
|
|
573
|
+
graph.set(String(c.data.id), [...(c.data.depends ?? []), ...children]);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return graph;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** The ids from `from` to `to` over `graph`, both included, or null when there is no way. */
|
|
580
|
+
export function pathTo(graph, from, to, seen = new Set()) {
|
|
581
|
+
if (from === to) return [to];
|
|
582
|
+
if (seen.has(from)) return null;
|
|
583
|
+
seen.add(from);
|
|
584
|
+
for (const next of graph.get(from) ?? []) {
|
|
585
|
+
const rest = pathTo(graph, next, to, seen);
|
|
586
|
+
if (rest) return [from, ...rest];
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Why a depends `cycle` (its ids, the first again at the end) never gets played. */
|
|
593
|
+
export function cycleMessage(ledger, cycle) {
|
|
594
|
+
const feature = cycle.some((id) => ledger.item(id)?.data.type === 'feature');
|
|
595
|
+
|
|
596
|
+
return (
|
|
597
|
+
`depends cycle ${cycle.join(' -> ')}: none of them can be played first` +
|
|
598
|
+
(feature ? ' (a feature waits for its children)' : '')
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* M2 over `depends:` of units and items together: a cycle never gets played — every id in it
|
|
604
|
+
* waits for the next one. Each cycle is reported once, from the id DFS meets it at.
|
|
605
|
+
*/
|
|
606
|
+
export function dependsCycles(ledger) {
|
|
607
|
+
const graph = dependsGraph(ledger);
|
|
572
608
|
/** @type {Issue[]} */
|
|
573
609
|
const out = [];
|
|
574
610
|
const state = new Map(); // id → 'open' while on the DFS path, 'done' after
|
|
@@ -583,7 +619,7 @@ export function dependsCycles(ledger) {
|
|
|
583
619
|
file,
|
|
584
620
|
line: 1,
|
|
585
621
|
id,
|
|
586
|
-
message:
|
|
622
|
+
message: cycleMessage(ledger, cycle),
|
|
587
623
|
});
|
|
588
624
|
|
|
589
625
|
return;
|
package/lib/inbox.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// gcf/inbox.md (Methodology §5.1): raw input waiting for triage — playtest notes, CI failures,
|
|
2
|
-
// the agent's ideas and requests. One line per entry,
|
|
3
|
-
//
|
|
2
|
+
// the agent's ideas and requests, the operator's ideas and notes. One line per entry,
|
|
3
|
+
// `- <in-id> | <source> | <text>[ | tags: a, b]`; a triaged line is erased, not marked: the file
|
|
4
|
+
// holds what is still waiting, not what happened to it. Tags sort it; an older core reads the
|
|
5
|
+
// tags field as part of the text, which loses nothing.
|
|
4
6
|
import { readFile } from 'node:fs/promises';
|
|
5
7
|
import { existsSync } from 'node:fs';
|
|
6
8
|
import { createHash } from 'node:crypto';
|
|
@@ -8,48 +10,136 @@ import path from 'node:path';
|
|
|
8
10
|
import { atomicWrite } from './safe-path.js';
|
|
9
11
|
|
|
10
12
|
export const INBOX_HEAD =
|
|
11
|
-
'# Inbox — `- <in-id> | <source> | <text
|
|
12
|
-
'is erased\n';
|
|
13
|
+
'# Inbox — `- <in-id> | <source> | <text>[ | tags: a, b]`, raw input waiting for triage; a ' +
|
|
14
|
+
'triaged line is erased\n';
|
|
13
15
|
export const SOURCES = ['playtest', 'ci', 'agent', 'operator'];
|
|
14
16
|
const LINE = /^- (in-[0-9a-f]+) \| (\S+) \| (.+)$/;
|
|
17
|
+
// The text may hold ` | ` itself: the tags are only the field at the very end.
|
|
18
|
+
const TAGS = / \| tags: ([^|]*)$/;
|
|
19
|
+
const TAG = /^[a-z0-9][a-z0-9:-]{0,39}$/;
|
|
15
20
|
// An agent's lesson (lessons.js): `lesson:<kind>:<item-id>`.
|
|
16
21
|
const LESSON = /^lesson:([a-z]+):([a-z0-9][a-z0-9-]*)$/;
|
|
17
22
|
|
|
18
23
|
const inboxFile = (root) => path.join(root, 'inbox.md');
|
|
19
24
|
|
|
25
|
+
/**
|
|
26
|
+
* The text and the tags of a line's last field. A field that is not a clean tag list (a bad
|
|
27
|
+
* tag, a tag twice, no text before it) stays part of the text: nothing written is lost.
|
|
28
|
+
*/
|
|
29
|
+
function textAndTags(rest) {
|
|
30
|
+
const m = rest.match(TAGS);
|
|
31
|
+
const tags = m ? m[1].split(',').map((t) => t.trim()) : [];
|
|
32
|
+
const text = m ? rest.slice(0, m.index).trim() : '';
|
|
33
|
+
const clean = tags.every((t) => TAG.test(t)) && new Set(tags).size === tags.length;
|
|
34
|
+
if (!m || !clean || !text) return { text: rest.trim(), tags: [] };
|
|
35
|
+
|
|
36
|
+
return { text, tags };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const lineOf = (e) =>
|
|
40
|
+
`- ${e.id} | ${e.source} | ${e.text}${e.tags.length ? ` | tags: ${e.tags.join(', ')}` : ''}`;
|
|
41
|
+
|
|
42
|
+
/** The entry of one line as written, or null. */
|
|
43
|
+
function rawOf(line) {
|
|
44
|
+
const m = line.match(LINE);
|
|
45
|
+
|
|
46
|
+
return m ? { id: m[1], source: m[2], ...textAndTags(m[3]) } : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* One entry per id. A union merge of two branches that tagged an entry differently leaves two
|
|
51
|
+
* lines with its id: they read as one, its first line's text with the tags of both.
|
|
52
|
+
*/
|
|
53
|
+
function mergeById(raws) {
|
|
54
|
+
/** @type {Map<string, {id: string, source: string, text: string, tags: string[]}>} */
|
|
55
|
+
const byId = new Map();
|
|
56
|
+
for (const r of raws) {
|
|
57
|
+
const seen = byId.get(r.id);
|
|
58
|
+
if (seen) seen.tags = [...new Set([...seen.tags, ...r.tags])];
|
|
59
|
+
else byId.set(r.id, { ...r });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return [...byId.values()];
|
|
63
|
+
}
|
|
64
|
+
|
|
20
65
|
/**
|
|
21
66
|
* Every entry; a lesson reads as source `lesson` with its `kind` and the `item` it came from.
|
|
22
|
-
* @typedef {{id: string, source: string, text: string,
|
|
67
|
+
* @typedef {{id: string, source: string, text: string, tags: string[], kind?: string,
|
|
68
|
+
* item?: string}} Entry
|
|
23
69
|
* @returns {Promise<Entry[]>}
|
|
24
70
|
*/
|
|
25
71
|
export async function listInbox(root) {
|
|
26
72
|
const file = inboxFile(root);
|
|
27
73
|
if (!existsSync(file)) return [];
|
|
74
|
+
const raws = (await readFile(file, 'utf8')).split(/\r?\n/).map(rawOf).filter(Boolean);
|
|
28
75
|
|
|
29
|
-
return (
|
|
30
|
-
const
|
|
31
|
-
if (!m) return [];
|
|
32
|
-
const lesson = m[2].match(LESSON);
|
|
33
|
-
const entry = { id: m[1], source: m[2], text: m[3].trim() };
|
|
76
|
+
return mergeById(/** @type {any[]} */ (raws)).map((entry) => {
|
|
77
|
+
const lesson = entry.source.match(LESSON);
|
|
34
78
|
|
|
35
|
-
return
|
|
79
|
+
return lesson ? { ...entry, source: 'lesson', kind: lesson[1], item: lesson[2] } : entry;
|
|
36
80
|
});
|
|
37
81
|
}
|
|
38
82
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
83
|
+
/** Refuse a tag that is not a lowercase word (`idea`, `later`, `ui:hud`). */
|
|
84
|
+
function checkTags(tags) {
|
|
85
|
+
const bad = tags.find((t) => !TAG.test(t));
|
|
86
|
+
if (bad !== undefined)
|
|
87
|
+
throw new Error(`tag "${bad}" must be a lowercase word: letters, digits, - and :`);
|
|
88
|
+
if (new Set(tags).size !== tags.length) throw new Error('a tag is named twice');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Append an entry with its `tags`; its id hashes the text and the moment (`now`), so two
|
|
93
|
+
* branches never collide.
|
|
94
|
+
*/
|
|
95
|
+
export async function addInbox(
|
|
96
|
+
root,
|
|
97
|
+
text,
|
|
98
|
+
source = 'operator',
|
|
99
|
+
{ now = new Date().toISOString(), tags = [] } = {},
|
|
100
|
+
) {
|
|
41
101
|
const clean = String(text).replace(/\s+/g, ' ').trim();
|
|
42
102
|
if (!clean) throw new Error('an inbox entry needs text');
|
|
43
103
|
if (!SOURCES.includes(source) && !LESSON.test(source))
|
|
44
104
|
throw new Error(`source must be ${SOURCES.join(' | ')}`);
|
|
105
|
+
if (textAndTags(clean).tags.length)
|
|
106
|
+
throw new Error('the text ends like a tags field: give tags as tags');
|
|
107
|
+
checkTags(tags);
|
|
45
108
|
const id = `in-${createHash('sha1').update(`${now}\n${clean}`).digest('hex').slice(0, 8)}`;
|
|
46
109
|
const file = inboxFile(root);
|
|
47
110
|
const old = existsSync(file) ? await readFile(file, 'utf8') : INBOX_HEAD;
|
|
48
111
|
const eol = old.includes('\r\n') ? '\r\n' : '\n';
|
|
49
112
|
const head = old.endsWith('\n') ? old : old + eol;
|
|
50
|
-
|
|
113
|
+
const entry = { id, source, text: clean, tags };
|
|
114
|
+
await atomicWrite(file, `${head}${lineOf(entry)}${eol}`);
|
|
115
|
+
|
|
116
|
+
return entry;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Add tags to entry `id` and remove others; refuses one it has, or lacks, already. Lines a
|
|
121
|
+
* union merge doubled become one again, at the first one's place.
|
|
122
|
+
*/
|
|
123
|
+
export async function tagInbox(root, id, { add = [], remove = [] }) {
|
|
124
|
+
if (!add.length && !remove.length) throw new Error('name tags to --add or --remove');
|
|
125
|
+
checkTags([...add, ...remove]);
|
|
126
|
+
const file = inboxFile(root);
|
|
127
|
+
const lines = (existsSync(file) ? await readFile(file, 'utf8') : '').split(/(?<=\n)/);
|
|
128
|
+
const bodyOf = (l) => l.replace(/\r?\n$/, '');
|
|
129
|
+
const mine = lines.flatMap((l, i) => (rawOf(bodyOf(l))?.id === id ? [i] : []));
|
|
130
|
+
if (!mine.length) throw new Error(`${id} is not in gcf/inbox.md`);
|
|
131
|
+
const [entry] = mergeById(mine.map((i) => /** @type {any} */ (rawOf(bodyOf(lines[i])))));
|
|
132
|
+
const had = add.filter((x) => entry.tags.includes(x));
|
|
133
|
+
if (had.length) throw new Error(`${id} is tagged ${had.join(', ')} already`);
|
|
134
|
+
const missing = remove.filter((x) => !entry.tags.includes(x));
|
|
135
|
+
if (missing.length) throw new Error(`${id} has no tag ${missing.join(', ')}`);
|
|
136
|
+
entry.tags = [...entry.tags.filter((x) => !remove.includes(x)), ...add];
|
|
137
|
+
// The line keeps its own ending: a CRLF file stays CRLF.
|
|
138
|
+
const first = lines[mine[0]];
|
|
139
|
+
lines[mine[0]] = `${lineOf(entry)}${first.slice(bodyOf(first).length)}`;
|
|
140
|
+
await atomicWrite(file, lines.filter((_, i) => !mine.slice(1).includes(i)).join(''));
|
|
51
141
|
|
|
52
|
-
return
|
|
142
|
+
return entry;
|
|
53
143
|
}
|
|
54
144
|
|
|
55
145
|
/** Erase entry `id`; refuses an id the inbox does not hold. */
|
package/lib/lessons.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// the session ends: the harness asks them in one more turn, `/gcf-run` asks them at its close.
|
|
6
6
|
import { readFile } from 'node:fs/promises';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
+
import { uiLanguage } from './aif-config.js';
|
|
8
9
|
import { readRawConfig } from './gcf-paths.js';
|
|
9
10
|
import { addInbox } from './inbox.js';
|
|
10
11
|
import { isValidId } from './id.js';
|
|
@@ -66,6 +67,7 @@ function questionsOf(config) {
|
|
|
66
67
|
export async function lessonsPrompt(projectDir, id, { reply = 'cli' } = {}) {
|
|
67
68
|
if (!isValidId(id)) throw new Error(`${id} is not an item id`);
|
|
68
69
|
const config = lessonsConfig(await readRawConfig(path.join(path.resolve(projectDir), 'gcf')));
|
|
70
|
+
const ui = await uiLanguage(projectDir);
|
|
69
71
|
const asked = questionsOf(config)
|
|
70
72
|
.map((q, i) => `${i + 1}. ${q.kind}: ${q.text}`)
|
|
71
73
|
.join('\n');
|
|
@@ -83,6 +85,10 @@ export async function lessonsPrompt(projectDir, id, { reply = 'cli' } = {}) {
|
|
|
83
85
|
'what got in your way, so it can be removed.',
|
|
84
86
|
'Answer from this session only, one finding per entry, in a sentence or two; skip a ' +
|
|
85
87
|
'question with nothing to say, and never invent one.',
|
|
88
|
+
// The operator reads the answers: they go in the project's language.ui (Methodology §5.2.3).
|
|
89
|
+
...(ui === 'en'
|
|
90
|
+
? []
|
|
91
|
+
: [`Write each finding in the language "${ui}"; keep the kinds as they are.`]),
|
|
86
92
|
asked,
|
|
87
93
|
// The harness's turn comes after the item's commit and its verified trailer: an edit then
|
|
88
94
|
// would leave the checkout dirty or move the trailer off the tip.
|
|
@@ -104,7 +110,10 @@ function lessonOf(raw, i) {
|
|
|
104
110
|
if (!text) throw new Error(`lesson ${i + 1} has no text`);
|
|
105
111
|
const where = raw?.where ? String(raw.where).replace(/\s+/g, ' ').trim() : '';
|
|
106
112
|
|
|
107
|
-
|
|
113
|
+
// An agent's free text must not read as an inbox tags field, nor fail the whole batch on it.
|
|
114
|
+
const full = (where ? `${text} (${where})` : text).replace(/ \| tags:/g, ' / tags:');
|
|
115
|
+
|
|
116
|
+
return { kind, text: full };
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
/**
|
package/lib/unit-ops.js
CHANGED
|
@@ -8,7 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import { serialize } from './frontmatter.js';
|
|
9
9
|
import { CARD_TYPES, SLUG_RE, listOf } from './design.js';
|
|
10
10
|
import { buildLedger } from './ledger.js';
|
|
11
|
-
import { designCheck } from './design-check.js';
|
|
11
|
+
import { cycleMessage, designCheck, dependsGraph, pathTo } from './design-check.js';
|
|
12
12
|
import { atomicWrite, withFileRollback } from './safe-path.js';
|
|
13
13
|
import { writeIndex } from './index-gen.js';
|
|
14
14
|
import { operationInProgress, commitPaths } from './git.js';
|
|
@@ -124,6 +124,20 @@ function featureFor(ledger, root, { id, unitIds, systems, today }) {
|
|
|
124
124
|
return { file, text: serialize(data, `\nintent:\n- TBD: what ${id} gives the player\n`) };
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Refuse taking `units` under `feature` when one waits for the feature: the feature closes only
|
|
129
|
+
* after its children, so neither would ever be played.
|
|
130
|
+
*/
|
|
131
|
+
function refuseFeatureCycle(ledger, feature, units) {
|
|
132
|
+
const graph = dependsGraph(ledger);
|
|
133
|
+
graph.set(feature, [...(graph.get(feature) ?? []), ...units.map((u) => u.id)]);
|
|
134
|
+
for (const u of units)
|
|
135
|
+
for (const d of graph.get(u.id) ?? []) {
|
|
136
|
+
const loop = pathTo(graph, d, u.id);
|
|
137
|
+
if (loop) throw new Error(cycleMessage(ledger, [u.id, ...loop]));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
127
141
|
/** A unit goes into work only ready: no TBD, no blocking D1, no blocking finding. */
|
|
128
142
|
function refuseUnready(units, summaries) {
|
|
129
143
|
for (const unit of units) {
|
|
@@ -163,6 +177,7 @@ export async function takeUnits(
|
|
|
163
177
|
if (feature) {
|
|
164
178
|
const systems = units.flatMap((u) => listOf(u.header.system)).filter((s) => s !== 'TBD');
|
|
165
179
|
writes.push(featureFor(ledger, root, { id: feature, unitIds: ids, systems, today }));
|
|
180
|
+
refuseFeatureCycle(ledger, feature, units);
|
|
166
181
|
}
|
|
167
182
|
|
|
168
183
|
const paths = units.map((u) => basePaths(root, u));
|
package/lib/verify-stamp.js
CHANGED
|
@@ -37,9 +37,16 @@ export function codeTree(projectDir) {
|
|
|
37
37
|
return r.stdout.trim();
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
// `git add` fails on an exclude that names an ignored path (`.gcf-session` in .gitignore or
|
|
41
|
+
// info/exclude), tracked or not, and an ignored path stays out of the add anyway.
|
|
42
|
+
const ignored = git(projectDir, ['check-ignore', '--no-index', '--', ...NOT_CODE]).stdout.split(
|
|
43
|
+
/\r?\n/,
|
|
44
|
+
);
|
|
45
|
+
const excludes = NOT_CODE.filter((p) => !ignored.includes(p)).map((p) => `:(exclude)${p}`);
|
|
46
|
+
|
|
40
47
|
try {
|
|
41
48
|
if (existsSync(real)) copyFileSync(real, tmp);
|
|
42
|
-
step(['add', '-A', '--', '.', ...
|
|
49
|
+
step(['add', '-A', '--', '.', ...excludes]);
|
|
43
50
|
step(['rm', '-r', '-q', '--cached', '--ignore-unmatch', '--', ...NOT_CODE]);
|
|
44
51
|
|
|
45
52
|
return step(['write-tree']);
|
package/lib/work-ops.js
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
knownIssueOf,
|
|
37
37
|
} from './live.js';
|
|
38
38
|
import { dropInbox } from './inbox.js';
|
|
39
|
+
import { cycleMessage, dependsGraph, pathTo } from './design-check.js';
|
|
39
40
|
import { refuseByPhase } from './phase.js';
|
|
40
41
|
import { locked, runHolder } from './main-lock.js';
|
|
41
42
|
import { dropAgentSession, releaseSession, sessionActive } from './session-mark.js';
|
|
@@ -830,6 +831,31 @@ function runFields(spec) {
|
|
|
830
831
|
);
|
|
831
832
|
}
|
|
832
833
|
|
|
834
|
+
/**
|
|
835
|
+
* Refuse `depends` of item `id` that could never be played (Methodology §4.1): an `added` id
|
|
836
|
+
* that is the item itself, unknown, or in the graveyard; or a cycle back to the item. A new
|
|
837
|
+
* child is not in its feature's `children` yet: `parent` adds that edge.
|
|
838
|
+
*/
|
|
839
|
+
function checkDepends(ledger, id, depends, { added = depends, parent = null } = {}) {
|
|
840
|
+
if (new Set(added).size !== added.length) throw new Error('a dependency is named twice');
|
|
841
|
+
if (added.includes(id)) throw new Error(`${id} cannot depend on itself`);
|
|
842
|
+
const unknown = added.filter((d) => !ledger.known(d));
|
|
843
|
+
if (unknown.length) throw new Error(`depends on unknown ${unknown.join(', ')}`);
|
|
844
|
+
const dead = added.filter((d) => {
|
|
845
|
+
const { live, terminal } = ledger.where(d);
|
|
846
|
+
|
|
847
|
+
return !live.length && terminal.includes('killed');
|
|
848
|
+
});
|
|
849
|
+
if (dead.length)
|
|
850
|
+
throw new Error(`${dead.join(', ')} ended in the graveyard: it will never be played`);
|
|
851
|
+
const graph = dependsGraph(ledger).set(id, depends);
|
|
852
|
+
if (parent) graph.set(parent, [...(graph.get(parent) ?? []), id]);
|
|
853
|
+
for (const d of added) {
|
|
854
|
+
const loop = pathTo(graph, d, id);
|
|
855
|
+
if (loop) throw new Error(cycleMessage(ledger, [id, ...loop]));
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
833
859
|
/** Refuse a spec `item new` cannot honour; returns the gate the item gets. */
|
|
834
860
|
function checkNew(ledger, spec) {
|
|
835
861
|
const { id, type, gate, severity } = spec;
|
|
@@ -844,6 +870,7 @@ function checkNew(ledger, spec) {
|
|
|
844
870
|
if (type === 'bug' && !SEVERITIES.includes(severity))
|
|
845
871
|
throw new Error(`a bug needs --severity ${SEVERITIES.join('|')}`);
|
|
846
872
|
checkRelease(spec);
|
|
873
|
+
checkDepends(ledger, id, spec.depends ?? [], { parent: spec.parent });
|
|
847
874
|
|
|
848
875
|
return g;
|
|
849
876
|
}
|
|
@@ -949,6 +976,42 @@ async function prioritizeItemsUnlocked(projectDir, ids, { commit = true } = {})
|
|
|
949
976
|
return { ids, hash };
|
|
950
977
|
}
|
|
951
978
|
|
|
979
|
+
/** `before` with `add` added and `remove` taken out; refuses an id named twice or out of place. */
|
|
980
|
+
function changedDepends(id, before, add, remove) {
|
|
981
|
+
const twice = [...add, ...remove].find((d, i, all) => all.indexOf(d) !== i);
|
|
982
|
+
if (twice) throw new Error(`${twice} is named twice`);
|
|
983
|
+
const listed = add.filter((d) => before.includes(d));
|
|
984
|
+
if (listed.length) throw new Error(`${id} already depends on ${listed.join(', ')}`);
|
|
985
|
+
const absent = remove.filter((d) => !before.includes(d));
|
|
986
|
+
if (absent.length) throw new Error(`${id} does not depend on ${absent.join(', ')}`);
|
|
987
|
+
|
|
988
|
+
return [...before.filter((d) => !remove.includes(d)), ...add];
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Add ids to item `id`'s `depends` and remove others. One commit `gcf(depends)`. An added id
|
|
993
|
+
* must be one that can be played and close no cycle; a removed one may be anything listed, an
|
|
994
|
+
* unknown or a killed one included: removing it is how such a dependency gets fixed.
|
|
995
|
+
*/
|
|
996
|
+
async function setDependsUnlocked(projectDir, id, { add = [], remove = [], commit = true } = {}) {
|
|
997
|
+
if (!add.length && !remove.length) throw new Error('name ids to --add or --remove');
|
|
998
|
+
const ledger = await buildLedger(projectDir);
|
|
999
|
+
const item = ledger.item(id);
|
|
1000
|
+
if (!item) throw new Error(`${id} is not a work item`);
|
|
1001
|
+
refuseRunning(projectDir, id);
|
|
1002
|
+
const depends = changedDepends(id, item.data.depends ?? [], add, remove);
|
|
1003
|
+
checkDepends(ledger, id, depends, { added: add });
|
|
1004
|
+
const hash = await commitMove(projectDir, `gcf(depends): ${id}`, {
|
|
1005
|
+
files: [item.file],
|
|
1006
|
+
// The item's own edits ride along, as with the §4.1 moves: a skill sets depends after it.
|
|
1007
|
+
inputs: [item.file],
|
|
1008
|
+
mutate: () => atomicWrite(item.file, serialize({ ...item.data, depends }, item.body)),
|
|
1009
|
+
commit,
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
return { id, depends, hash };
|
|
1013
|
+
}
|
|
1014
|
+
|
|
952
1015
|
/**
|
|
953
1016
|
* A new work item in ready (or blocked), no commit. A design unit enters work through `take`,
|
|
954
1017
|
* not here: its body is the unit's slots. `from` erases the inbox entry it came from.
|
|
@@ -1012,6 +1075,9 @@ export const dropItem = (projectDir, id, opts = {}) =>
|
|
|
1012
1075
|
/** @type {typeof prioritizeItemsUnlocked} */
|
|
1013
1076
|
export const prioritizeItems = (projectDir, ids, opts = {}) =>
|
|
1014
1077
|
locked(projectDir, opts.commit ?? true, () => prioritizeItemsUnlocked(projectDir, ids, opts));
|
|
1078
|
+
/** @type {typeof setDependsUnlocked} */
|
|
1079
|
+
export const setDepends = (projectDir, id, opts = {}) =>
|
|
1080
|
+
locked(projectDir, opts.commit ?? true, () => setDependsUnlocked(projectDir, id, opts));
|
|
1015
1081
|
/** @type {typeof newItemUnlocked} */
|
|
1016
1082
|
export const newItem = (projectDir, spec) =>
|
|
1017
1083
|
locked(projectDir, spec.commit ?? false, () => newItemUnlocked(projectDir, spec));
|
package/package.json
CHANGED
package/skills/gcf-card/SKILL.md
CHANGED
|
@@ -52,8 +52,9 @@ workflow:
|
|
|
52
52
|
17. 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
|
|
53
53
|
18. when the work is several items (a feature's children, or a batch), cut them along subsystem boundaries: each a thin slice that builds and tests alone
|
|
54
54
|
19. two items of one subsystem that change different files get `parallel: ok`, and their `contract:` or `intent:` names those files
|
|
55
|
-
20.
|
|
56
|
-
21.
|
|
55
|
+
20. set each item's dependencies with `ai-factory gcf depends <id> --add <ids>`: it commits the item's edits too, and a feature's child never depends on its feature
|
|
56
|
+
21. rank the items with `ai-factory gcf prioritize <id> <id> …`: dependencies first, then by value
|
|
57
|
+
22. report the item paths, each item's gate, and for a verdict item the one question the playtest will answer
|
|
57
58
|
|
|
58
59
|
invocation:
|
|
59
60
|
- 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
|
|
@@ -18,7 +18,7 @@ forbidden_behaviors:
|
|
|
18
18
|
- do not create an item or erase an entry without the human's yes: the agent proposes, the human approves
|
|
19
19
|
- do not turn a feel complaint into a bug: "jump feels floaty" is a tune of the item that owns the jump, a bug is behaviour that breaks a rule or a test
|
|
20
20
|
- do not open a second item for something `gcf/graveyard.md` already buried: name the line and ask whether the reason still holds
|
|
21
|
-
- do not edit `gcf/inbox.md` by hand: `ai-factory gcf item new --from` and `ai-factory gcf inbox drop` own its lines
|
|
21
|
+
- do not edit `gcf/inbox.md` by hand: `ai-factory gcf item new --from`, `ai-factory gcf inbox tag` and `ai-factory gcf inbox drop` own its lines
|
|
22
22
|
|
|
23
23
|
workflow:
|
|
24
24
|
1. run `ai-factory gcf inbox --json`; read `gcf/LIVE.md` and `gcf/INDEX.md` to know what exists and what is in work
|
|
@@ -30,7 +30,8 @@ workflow:
|
|
|
30
30
|
7. a tune of an item waiting for its verdict or unplayed: leave the entry in the inbox, the note's only copy, until `/gcf-verdict <id> tune --note` records it and drops it; a tune of a kept mechanic is new work with `--reopens <id>`
|
|
31
31
|
8. an idea for the design: write a unit with `TBD` slots in `gcf/design/<system>.md` and its backlog line, then run `ai-factory gcf inbox drop <in-id>`
|
|
32
32
|
9. a duplicate or noise: run `ai-factory gcf inbox drop <in-id>`
|
|
33
|
-
10.
|
|
33
|
+
10. an entry the human keeps for later (an idea, a note): tag it with the tag the human names, `ai-factory gcf inbox tag <in-id> --add <tags>`, and leave it in the inbox
|
|
34
|
+
11. report what each entry became and how many entries remain
|
|
34
35
|
|
|
35
36
|
invocation:
|
|
36
37
|
- run after a playtest session, after a CI run that added entries, or when the user asks to triage
|
|
@@ -1,42 +1,45 @@
|
|
|
1
|
-
gcf_context:
|
|
2
|
-
- the game's design and its work live in `gcf/`, and the `ai-factory gcf` commands and the gcf skills change them
|
|
3
|
-
- read `gcf/INDEX.md` before anything else under `gcf/`: it lists every subsystem, design unit and work item
|
|
4
|
-
- `gcf/LIVE.md` says what the game does now, and `gcf/PILLARS.md` says what it must stay
|
|
5
|
-
- `ai-factory gcf board --json` gives the whole state in one read: the phase, the items by status with plan progress, the inbox and the open questions
|
|
6
|
-
|
|
7
|
-
gcf_work:
|
|
8
|
-
- 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
|
|
9
|
-
- 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; a text that starts with a hyphen goes as `inbox add --text="<text>"`
|
|
10
|
-
- before planning an item, run `ai-factory gcf context <id>` and read only the files it lists under `gcf/`
|
|
11
|
-
- drive a ready item with the gcf-run skill when the project has gcf-harness-aif-ext and the agent is Claude Code (a batch at once: `ai-factory gcf-harness run <ids...>`), otherwise with aif-plan and aif-implement
|
|
12
|
-
- a question about how the runs go (the share that reached their move, cost, plan checks, DoD attempts, guard firings) is answered by `ai-factory gcf-harness metrics`
|
|
13
|
-
- after the code, write the item's `live:` delta and run `ai-factory gcf verify <id>`: fix what it reports and run it again until every check is green, at most `dod_attempts` of `gcf/config.yml` red runs; still red: block the item with the diagnosis
|
|
14
|
-
- `submit` and `close` refuse code that changed since its last green `gcf verify`: run it again after any fix; `--unverified "<why>"` is the human's call, never the agent's
|
|
15
|
-
- then end the work on the item with its command: `ai-factory gcf submit <id>` for gate verdict or decision, `ai-factory gcf close <id>` for gate auto
|
|
16
|
-
- work that cannot go on: run `ai-factory gcf block <id> --reason "<why or the question>"`
|
|
17
|
-
- in a worktree on branch `work/<id>` change no `gcf/` file but the item's `live:`, commit all, then run `ai-factory gcf verify <id> --mark`: `ai-factory gcf integrate` in the main checkout merges it and records the move
|
|
18
|
-
- a design question only the human can answer becomes a D1 record in `gcf/open-questions.md`
|
|
19
|
-
- a word added to a `not:` of `gcf/glossary.md` leaves `gcf/design/` and every item but an unplayed one in the same commit: `ai-factory gcf design check` names each hit as M4
|
|
20
|
-
- the human plays from `ai-factory gcf playtest session`; each free note from that play goes to `ai-factory gcf inbox add --source playtest "<note>"`
|
|
21
|
-
- a release item carries `version:` and `channel:` (`item new <id> --type release --release <v> --channel <c>`): its end writes the `release:` block of `gcf/LIVE.md`
|
|
22
|
-
- `item new` and `take` refuse a type the current phase does not take: run `ai-factory gcf phase` and bring the refusal to the human
|
|
23
|
-
- an item the human wants run on a given model or effort gets `item new <id> --model <m> --effort <e>`: the harness run passes both to the agent
|
|
24
|
-
- ready items run in `priority:` order, lower first: rank a set of items with `ai-factory gcf prioritize <id> <id> …`, and put an id in `depends` only when the item uses that one's data or code
|
|
25
|
-
- a
|
|
26
|
-
- a
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- do not
|
|
33
|
-
- do not
|
|
34
|
-
- do not
|
|
35
|
-
- do not
|
|
36
|
-
- do not
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- gcf-
|
|
1
|
+
gcf_context:
|
|
2
|
+
- the game's design and its work live in `gcf/`, and the `ai-factory gcf` commands and the gcf skills change them
|
|
3
|
+
- read `gcf/INDEX.md` before anything else under `gcf/`: it lists every subsystem, design unit and work item
|
|
4
|
+
- `gcf/LIVE.md` says what the game does now, and `gcf/PILLARS.md` says what it must stay
|
|
5
|
+
- `ai-factory gcf board --json` gives the whole state in one read: the phase, the items by status with plan progress, the inbox and the open questions
|
|
6
|
+
|
|
7
|
+
gcf_work:
|
|
8
|
+
- 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
|
|
9
|
+
- a raw idea, a playtest note or a CI finding that is not work yet goes to `ai-factory gcf inbox add "<text>" [--tags <a,b>]`, and the gcf-triage skill sorts it later; a text that starts with a hyphen goes as `inbox add --text="<text>"`
|
|
10
|
+
- before planning an item, run `ai-factory gcf context <id>` and read only the files it lists under `gcf/`
|
|
11
|
+
- drive a ready item with the gcf-run skill when the project has gcf-harness-aif-ext and the agent is Claude Code (a batch at once: `ai-factory gcf-harness run <ids...>`), otherwise with aif-plan and aif-implement
|
|
12
|
+
- a question about how the runs go (the share that reached their move, cost, plan checks, DoD attempts, guard firings) is answered by `ai-factory gcf-harness metrics`
|
|
13
|
+
- after the code, write the item's `live:` delta and run `ai-factory gcf verify <id>`: fix what it reports and run it again until every check is green, at most `dod_attempts` of `gcf/config.yml` red runs; still red: block the item with the diagnosis
|
|
14
|
+
- `submit` and `close` refuse code that changed since its last green `gcf verify`: run it again after any fix; `--unverified "<why>"` is the human's call, never the agent's
|
|
15
|
+
- then end the work on the item with its command: `ai-factory gcf submit <id>` for gate verdict or decision, `ai-factory gcf close <id>` for gate auto
|
|
16
|
+
- work that cannot go on: run `ai-factory gcf block <id> --reason "<why or the question>"`
|
|
17
|
+
- in a worktree on branch `work/<id>` change no `gcf/` file but the item's `live:`, commit all, then run `ai-factory gcf verify <id> --mark`: `ai-factory gcf integrate` in the main checkout merges it and records the move
|
|
18
|
+
- a design question only the human can answer becomes a D1 record in `gcf/open-questions.md`
|
|
19
|
+
- a word added to a `not:` of `gcf/glossary.md` leaves `gcf/design/` and every item but an unplayed one in the same commit: `ai-factory gcf design check` names each hit as M4
|
|
20
|
+
- the human plays from `ai-factory gcf playtest session`; each free note from that play goes to `ai-factory gcf inbox add --source playtest "<note>"`
|
|
21
|
+
- a release item carries `version:` and `channel:` (`item new <id> --type release --release <v> --channel <c>`): its end writes the `release:` block of `gcf/LIVE.md`
|
|
22
|
+
- `item new` and `take` refuse a type the current phase does not take: run `ai-factory gcf phase` and bring the refusal to the human
|
|
23
|
+
- an item the human wants run on a given model or effort gets `item new <id> --model <m> --effort <e>`: the harness run passes both to the agent
|
|
24
|
+
- ready items run in `priority:` order, lower first: rank a set of items with `ai-factory gcf prioritize <id> <id> …`, and put an id in `depends` only when the item uses that one's data or code: `ai-factory gcf depends <id> --add <ids>` or `--remove <ids>`, never a hand edit
|
|
25
|
+
- write what the human reads (a question, an assumption, a block reason, an inbox entry, a lesson, a report, an item's `title:`) in `language.ui` of `.ai-factory/config.yaml`, and the body of an item that is not a design unit in its `language.artifacts`
|
|
26
|
+
- write in English whatever lands in a checked document: `gcf/design/`, `gcf/systems/`, `gcf/glossary.md`, `gcf/PILLARS.md`, `gcf/LIVE.md`, the body of a design unit's item but its `tuning:` and `notes:`, an item's `live:` summary; an answer that moves into a unit's slot goes there in English
|
|
27
|
+
- never translate keys, statuses, ids, commands, `TBD`, `answer: q-NNN`, `## Handoff`, a `quote:` (it is verbatim from the GDD) or the fixed start of a block reason (`needs permission:`, `plan did not converge:`, `waits for`)
|
|
28
|
+
- a D1 record that carries the human's `answer:` goes into the slot of each unit it blocks, with `answer: q-NNN`, and then leaves `gcf/open-questions.md`
|
|
29
|
+
- a pillar changes only on the human's word: `ai-factory gcf pillar rename <from> <to>`, or `ai-factory gcf pillar retire <id> --reason "<why>"`
|
|
30
|
+
|
|
31
|
+
gcf_forbidden:
|
|
32
|
+
- do not edit `status:`, `frozen:`, `gcf/LIVE.md`, `gcf/history.md`, `gcf/graveyard.md` or `gcf/INDEX.md` by hand: the commands own them
|
|
33
|
+
- do not give a verdict or a decision: `verdict` and `decide` belong to the human, through the gcf-verdict skill
|
|
34
|
+
- do not answer a `TBD` of a design unit yourself: ask the human and offer your draft as the proposed answer
|
|
35
|
+
- do not run `ai-factory gcf phase next`: the phase moves on the human's word only
|
|
36
|
+
- do not change what agent runs may do: `ai-factory gcf permissions` and its deny groups belong to the human
|
|
37
|
+
- do not change a setting of `gcf/config.yml`: `ai-factory gcf config set` belongs to the human
|
|
38
|
+
- do not reuse an ended id: rework gets a new id with `reopens: <old id>`
|
|
39
|
+
- do not commit past the gate with `--no-verify`: a refused commit names what to fix
|
|
40
|
+
|
|
41
|
+
gcf_skills:
|
|
42
|
+
- gcf-vision starts a game that has no design yet, and gcf-ingest imports a GDD into `gcf/design/`
|
|
43
|
+
- gcf-card puts work into `gcf/work/`, and gcf-triage sorts `gcf/inbox.md`
|
|
44
|
+
- gcf-verdict records the human's verdict or decision, and gcf-audit reports the health of `gcf/`
|
|
45
|
+
- gcf-debate stresses the design, gcf-adopt puts existing code on GCF, and gcf-init sets up `gcf/` and revises the pillars
|