gcf-aif-ext 0.8.0 → 0.10.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/lib/domain.js ADDED
@@ -0,0 +1,370 @@
1
+ // The GCF domain of the board read (Board-PRD decision 16): what a board shows and offers for a
2
+ // GCF project, as data. Its columns, its blocks (phase, WIP, playtest, inbox…), the fields of an
3
+ // item, every move's form with the `ai-factory` words it runs, the terms of its Help. A board
4
+ // draws these and builds each command from `args` and `fields`; it hard-codes none of them.
5
+ import { ITEM_MOVES, STORED } from './transitions.js';
6
+ import { DEFAULT_GATE, GATES, ITEM_TYPES, SEVERITIES, UNIT_ITEM_TYPES } from './validate-item.js';
7
+ import { SOURCES } from './inbox.js';
8
+
9
+ // The status a run starts from; the order of its column is the run order.
10
+ export const RUNS_FROM = 'ready';
11
+
12
+ // The board's words for each stored status: a column's label and tooltip.
13
+ const COLUMN_TEXT = {
14
+ ready: ['ready', 'Waits for a run; the order here is the run order'],
15
+ blocked: ['blocked', 'Stopped: waits for an answer, an asset or a fix of a failed check'],
16
+ 'awaiting-verdict': ['awaiting verdict', 'Built; waits for a playtest and a keep, tune or kill'],
17
+ 'awaiting-decision': ['awaiting decision', 'Waits for the operator to decide: go, no, close…'],
18
+ unplayed: ['unplayed', 'Closed without a playtest; waits for one'],
19
+ parked: ['parked', 'A bug kept as a known issue; unblock takes it back to ready'],
20
+ };
21
+
22
+ const GATE_HELP = {
23
+ verdict: 'Closed by a playtest: keep, tune or kill',
24
+ auto: 'Closed by the run itself when its checks are green; no playtest',
25
+ decision: 'Closed by the operator: go, no, close or continue',
26
+ };
27
+
28
+ // The form of each move for a board: the `ai-factory` words with `{id}` for its target and
29
+ // `{ids}` for a list of items, and the fields the operator fills. A field goes as
30
+ // `--<flag or key>=<value>`; a list repeats it; `ids` and `tags` go as one comma list;
31
+ // `remove` is `--remove` / `--no-remove`, or the command's own rule when left empty; `slug` is
32
+ // the target id the operator types (`{id}`); a field with `when` goes only while another field
33
+ // holds one of those values.
34
+ const REASON = { key: 'reason', label: 'Reason', kind: 'longtext', required: true };
35
+ const OPTIONAL_REASON = { ...REASON, required: false };
36
+ const REMOVE = { key: 'remove', label: 'Removal item for its code', kind: 'remove' };
37
+ const CONSTRAINT = { key: 'constraint', label: 'Constraint lines', kind: 'list' };
38
+ // close and submit need a green gcf verify of the code as it is now; the operator may say why
39
+ // they go without one, and the reason goes into the move's commit.
40
+ const UNVERIFIED = {
41
+ key: 'unverified',
42
+ label: 'No green gcf verify? Why it goes anyway',
43
+ kind: 'text',
44
+ };
45
+
46
+ const verdict = (v, help, fields, danger) => ({
47
+ label: v,
48
+ help,
49
+ args: ['gcf', 'verdict', '{id}', v],
50
+ fields,
51
+ ...(danger && { danger }),
52
+ });
53
+ const decide = (choice, help, fields = [], danger) => ({
54
+ label: choice,
55
+ help,
56
+ args: ['gcf', 'decide', '{id}', choice],
57
+ fields,
58
+ ...(danger && { danger }),
59
+ });
60
+ const text = (key, label, extra = {}) => ({ key, label, kind: 'text', ...extra });
61
+
62
+ // An item's moves (§4.1), offered by `items[].moves`.
63
+ const ITEM_SPECS = {
64
+ submit: {
65
+ label: 'submit',
66
+ help: 'Built: on to awaiting verdict or awaiting decision, by its gate',
67
+ args: ['gcf', 'submit', '{id}'],
68
+ fields: [UNVERIFIED],
69
+ rare: true,
70
+ },
71
+ close: {
72
+ label: 'close',
73
+ help: 'Done, gate auto: to history; a mechanic or a resource waits unplayed',
74
+ args: ['gcf', 'close', '{id}'],
75
+ fields: [CONSTRAINT, UNVERIFIED],
76
+ rare: true,
77
+ },
78
+ 'verdict:keep': verdict('keep', 'Played and kept: it goes to history', [
79
+ { key: 'note', label: 'Note', kind: 'text' },
80
+ CONSTRAINT,
81
+ ]),
82
+ 'verdict:tune': verdict('tune', 'Played; needs a change: back to ready with the note', [
83
+ { key: 'note', label: 'What to change', kind: 'longtext', required: true },
84
+ ]),
85
+ 'verdict:kill': verdict(
86
+ 'kill',
87
+ 'Played and rejected: it goes to the graveyard',
88
+ [{ key: 'note', label: 'Why', kind: 'longtext' }, REMOVE],
89
+ true,
90
+ ),
91
+ 'decide:go': decide('go', 'Approved: it goes to history as decided'),
92
+ 'decide:no': decide(
93
+ 'no',
94
+ 'Not doing it: it goes to the graveyard',
95
+ [OPTIONAL_REASON, REMOVE],
96
+ true,
97
+ ),
98
+ 'decide:wontfix': decide(
99
+ 'wontfix',
100
+ 'A bug left as it is: it goes to the graveyard',
101
+ [OPTIONAL_REASON, REMOVE],
102
+ true,
103
+ ),
104
+ 'decide:known': decide('known', 'A bug kept as a known issue: it goes to parked'),
105
+ 'decide:close': decide('close', 'The feature is done: it goes to history'),
106
+ 'decide:continue': decide('continue', 'The feature goes on: back to ready for more children'),
107
+ 'decide:kill': decide(
108
+ 'kill',
109
+ 'The feature is dropped: it goes to the graveyard with its live children',
110
+ [OPTIONAL_REASON],
111
+ true,
112
+ ),
113
+ block: {
114
+ label: 'block',
115
+ help: 'Stop it until the operator answers',
116
+ args: ['gcf', 'block', '{id}'],
117
+ fields: [REASON],
118
+ },
119
+ unblock: {
120
+ label: 'unblock',
121
+ help: 'The answer is given: back to ready (a feature with a dead child: to a decision)',
122
+ args: ['gcf', 'unblock', '{id}'],
123
+ fields: [{ key: 'note', label: 'Answer or "file: <path>"', kind: 'longtext' }],
124
+ drag: RUNS_FROM,
125
+ },
126
+ drop: {
127
+ label: 'drop',
128
+ help: 'Off the board into the graveyard, from any status',
129
+ args: ['gcf', 'drop', '{id}'],
130
+ fields: [REASON, { key: 'tags', label: 'Graveyard tags', kind: 'text' }, REMOVE],
131
+ danger: true,
132
+ rare: true,
133
+ drag: 'graveyard',
134
+ },
135
+ depends: {
136
+ label: 'depends',
137
+ help: 'The items it waits for: a run starts only after they are played',
138
+ args: ['gcf', 'depends', '{id}'],
139
+ fields: [
140
+ { key: 'add', label: 'Add: ids it waits for', kind: 'ids' },
141
+ { key: 'remove', label: 'Remove: ids it no longer waits for', kind: 'ids' },
142
+ ],
143
+ rare: true,
144
+ },
145
+ };
146
+
147
+ // The agent's move: a session of the gcf-run skill builds the item, driven by the harness.
148
+ const RUN = {
149
+ kind: 'agent',
150
+ label: 'run',
151
+ help: 'An agent session builds it: plan, code, the checks, then submit or close',
152
+ skill: 'gcf-run',
153
+ args: [],
154
+ fields: [],
155
+ };
156
+
157
+ // The new item form (`gcf item new`): work that is not a design unit; a design unit enters work
158
+ // through the gcf-card skill and take.
159
+ const NEW_ITEM = {
160
+ label: 'create',
161
+ help: 'A new work item, committed; the core refuses a type the phase does not take',
162
+ args: ['gcf', 'item', 'new', '{id}', '--commit'],
163
+ fields: [
164
+ { key: 'id', label: 'Id (lowercase-hyphen slug)', kind: 'slug', required: true },
165
+ {
166
+ key: 'type',
167
+ label: 'Type',
168
+ kind: 'choice',
169
+ required: true,
170
+ default: 'tech',
171
+ options: ITEM_TYPES.filter((t) => !UNIT_ITEM_TYPES.includes(t)).map((t) => ({
172
+ value: t,
173
+ label: t,
174
+ help: `gate ${DEFAULT_GATE[t]} by default`,
175
+ })),
176
+ },
177
+ {
178
+ key: 'gate',
179
+ label: 'Gate',
180
+ kind: 'choice',
181
+ options: [
182
+ { value: '', label: 'by type' },
183
+ ...GATES.map((g) => ({ value: g, label: g, help: GATE_HELP[g] })),
184
+ ],
185
+ },
186
+ {
187
+ key: 'severity',
188
+ label: 'Severity',
189
+ kind: 'choice',
190
+ required: true,
191
+ options: SEVERITIES.map((s) => ({ value: s, label: s })),
192
+ when: { key: 'type', is: ['bug'] },
193
+ },
194
+ text('release', 'Version', { required: true, when: { key: 'type', is: ['release'] } }),
195
+ text('channel', 'Channel', { required: true, when: { key: 'type', is: ['release'] } }),
196
+ text('title', 'Title: one line, what it gives'),
197
+ text('system', 'Subsystems, comma-separated'),
198
+ text('depends', 'Depends on ids, comma-separated'),
199
+ text('parent', 'Parent feature id'),
200
+ text('milestone', 'Milestone'),
201
+ text('blocked', 'Start blocked: why (e.g. "file needed")'),
202
+ text('model', 'Model of its run (e.g. opus)'),
203
+ text('effort', 'Effort of its run (e.g. high)'),
204
+ // The inbox entry it comes from: the form fills it, the operator does not.
205
+ text('from', 'From inbox entry', { hidden: true }),
206
+ ],
207
+ };
208
+
209
+ // The moves of the domain's blocks: the phase, the inbox, the questions, the run order.
210
+ const BLOCK_SPECS = {
211
+ 'item-new': NEW_ITEM,
212
+ 'phase-next': {
213
+ label: 'move on',
214
+ help: 'On to the next phase; against an unmet criterion only with a reason',
215
+ args: ['gcf', 'phase', 'next'],
216
+ fields: [
217
+ { key: 'reason', label: 'Reason (needed when a criterion is not met)', kind: 'longtext' },
218
+ ],
219
+ },
220
+ 'inbox-add': {
221
+ label: 'add',
222
+ help: 'A raw note for triage: an idea, a playtest note, a CI finding',
223
+ args: ['gcf', 'inbox', 'add', '--commit'],
224
+ fields: [
225
+ { key: 'text', label: 'Text', kind: 'text', required: true },
226
+ {
227
+ key: 'source',
228
+ label: 'Source',
229
+ kind: 'choice',
230
+ default: 'operator',
231
+ options: SOURCES.map((s) => ({ value: s, label: s })),
232
+ },
233
+ { key: 'tags', label: 'Tags', kind: 'tags' },
234
+ ],
235
+ },
236
+ 'inbox-drop': {
237
+ label: 'erase',
238
+ help: 'Noise: the entry is erased, nothing is made of it',
239
+ args: ['gcf', 'inbox', 'drop', '{id}', '--commit'],
240
+ fields: [],
241
+ danger: true,
242
+ },
243
+ 'inbox-tag': {
244
+ label: 'tag',
245
+ help: 'Sort the entry: tags added and removed',
246
+ args: ['gcf', 'inbox', 'tag', '{id}', '--commit'],
247
+ fields: [
248
+ { key: 'add', label: 'Add tags', kind: 'tags' },
249
+ { key: 'remove', label: 'Remove tags', kind: 'tags' },
250
+ ],
251
+ },
252
+ 'question-answer': {
253
+ label: 'answer',
254
+ help: 'The answer goes into the record; the next agent on its units puts it in place',
255
+ args: ['gcf', 'question', 'answer', '{id}'],
256
+ fields: [{ key: 'text', label: 'Answer', kind: 'text', required: true }],
257
+ },
258
+ prioritize: {
259
+ label: 'prioritize',
260
+ help: 'The run order: ranks 1, 2, 3… in one commit',
261
+ args: ['gcf', 'prioritize', '{ids}'],
262
+ fields: [],
263
+ },
264
+ };
265
+
266
+ // The item fields a board shows: on the card (`card`), in the drawer (all that have a value).
267
+ const GATE_FIELD_OPTIONS = GATES.map((g) => ({ value: g, label: g, help: GATE_HELP[g] }));
268
+ const FIELDS = [
269
+ { key: 'priority', label: 'priority', kind: 'rank', help: 'Rank: lower runs sooner', card: true },
270
+ { key: 'type', label: 'type', kind: 'text', card: true, badge: true },
271
+ {
272
+ key: 'gate',
273
+ label: 'gate',
274
+ kind: 'choice',
275
+ options: GATE_FIELD_OPTIONS,
276
+ help: 'Who closes it',
277
+ card: true,
278
+ badge: true,
279
+ },
280
+ { key: 'severity', label: 'severity', kind: 'text', card: true, badge: true },
281
+ { key: 'model', label: 'model', kind: 'text', card: true },
282
+ { key: 'effort', label: 'effort', kind: 'text' },
283
+ { key: 'tune_count', label: 'tune', kind: 'text', help: 'Tune verdicts so far', card: true },
284
+ {
285
+ key: 'waits_for',
286
+ label: 'waits for',
287
+ kind: 'ids',
288
+ help: 'The run gate refuses it until these are played',
289
+ card: true,
290
+ },
291
+ { key: 'depends', label: 'depends', kind: 'ids' },
292
+ { key: 'needed_by', label: 'needed by', kind: 'ids', help: 'The items that wait for this one' },
293
+ { key: 'system', label: 'system', kind: 'list' },
294
+ { key: 'parent', label: 'parent', kind: 'text' },
295
+ { key: 'children', label: 'children', kind: 'ids' },
296
+ { key: 'milestone', label: 'milestone', kind: 'text' },
297
+ { key: 'blocked', label: 'blocked', kind: 'longtext', help: 'Why it stopped', card: true },
298
+ ];
299
+
300
+ // The words of its Help (Board-PRD decision 17): what the board's terms mean in GCF.
301
+ const TERMS = [
302
+ ['item', 'One piece of work in gcf/work/: a file with its type, gate and status'],
303
+ ['gate', 'Who closes an item: a verdict (playtest), auto (its checks) or a decision'],
304
+ ...GATES.map((g) => [`gate: ${g}`, GATE_HELP[g]]),
305
+ [
306
+ 'feature',
307
+ 'A parent item: when its children are done, the operator decides: close, continue or kill',
308
+ ],
309
+ ['design unit', 'A mechanic, resource, rule, constraint or entity of gcf/design/'],
310
+ ['phase', 'The stage of the project (prototype, vertical slice…): the types it takes'],
311
+ ['WIP', 'Items built and waiting for a verdict; at the limit new verdict work stops'],
312
+ ['verify', 'The DoD: the project test and build commands, green on the code as it is'],
313
+ ['inbox', 'Raw input waiting for triage: it becomes an item or is erased'],
314
+ ['lesson', 'An inbox entry an agent session left: what got in its way'],
315
+ ['D1', 'A question only the operator answers; it blocks the units it names'],
316
+ ['D2', 'An assumption the agent made and works with; it comes back when its trigger fires'],
317
+ ['unplayed', 'Closed by its checks without a playtest; it waits for one'],
318
+ ['history', 'What was accepted: history.md'],
319
+ ['graveyard', 'What was killed or dropped, with the reason: graveyard.md'],
320
+ ].map(([term, help]) => ({ term, help }));
321
+
322
+ const SECTIONS = {
323
+ phase: { next: 'phase-next' },
324
+ release: {},
325
+ wip: { help: 'Items awaiting a verdict / wip_built_limit' },
326
+ inbox: { add: 'inbox-add', drop: 'inbox-drop', tag: 'inbox-tag' },
327
+ questions: { answer: 'question-answer' },
328
+ lessons: {},
329
+ playtest: {
330
+ waits: ['awaiting-verdict', 'unplayed'],
331
+ verdicts: ['verdict:keep', 'verdict:tune', 'verdict:kill'],
332
+ },
333
+ history: { label: 'Accepted' },
334
+ graveyard: { label: 'Graveyard' },
335
+ };
336
+
337
+ // The extension whose CLI drives agent runs (run, next, stop, metrics with --json).
338
+ const HARNESS = { extension: 'gcf-harness-aif-ext', cli: 'gcf-harness' };
339
+
340
+ /** Whether the project's AI Factory extensions include the harness (`extensions` of the read). */
341
+ export const hasHarness = (extensions) => Object.hasOwn(extensions ?? {}, HARNESS.extension);
342
+
343
+ /**
344
+ * The GCF domain of the board read. `extensions` are the project's AI Factory extensions: the
345
+ * agent's move, and the runner that drives it, only with the harness.
346
+ */
347
+ export function domainOf(extensions = {}) {
348
+ const harness = hasHarness(extensions);
349
+
350
+ return {
351
+ kind: 'gcf',
352
+ runs_from: RUNS_FROM,
353
+ harness: harness ? HARNESS.cli : null,
354
+ columns: STORED.map((status) => ({
355
+ status,
356
+ label: COLUMN_TEXT[status][0],
357
+ help: COLUMN_TEXT[status][1],
358
+ })),
359
+ moves: {
360
+ ...(harness && { run: RUN }),
361
+ ...Object.fromEntries(ITEM_MOVES.map((m) => [m, ITEM_SPECS[m]])),
362
+ ...BLOCK_SPECS,
363
+ },
364
+ fields: FIELDS,
365
+ terms: TERMS,
366
+ sections: SECTIONS,
367
+ new_item: 'item-new',
368
+ rank: 'prioritize',
369
+ };
370
+ }
@@ -1,13 +1,14 @@
1
1
  // Methodology §4. A work item FILE carries one of the stored statuses; `running` and `verify` are
2
2
  // read from the run (worktree + session), never written. A terminal move deletes the file and
3
3
  // writes a history or graveyard line in the same commit, so no terminal status is ever stored.
4
+ // The order is the board's column order.
4
5
  export const STORED = [
5
6
  'ready',
7
+ 'blocked',
6
8
  'awaiting-verdict',
7
9
  'awaiting-decision',
8
- 'blocked',
9
- 'parked',
10
10
  'unplayed',
11
+ 'parked',
11
12
  ];
12
13
 
13
14
  // The status each command takes an item from (§4.1). `drop` leaves from any stored status.
@@ -27,8 +28,13 @@ const FROM = {
27
28
  block: ['ready'],
28
29
  unblock: ['blocked', 'parked'],
29
30
  drop: STORED,
31
+ // What it waits for may change at any status; the core refuses an unknown id and a cycle.
32
+ depends: STORED,
30
33
  };
31
34
 
35
+ /** Every move an item may be offered, in the board's order. */
36
+ export const ITEM_MOVES = Object.keys(FROM);
37
+
32
38
  /** True when `move` (`close`, `verdict:keep`, …) may start from `status`. */
33
39
  export function allowedFrom(move, status) {
34
40
  return (FROM[move] ?? []).includes(status);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gcf-aif-ext",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Game Context Framework for AI Factory",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,7 +34,7 @@ gcf_forbidden:
34
34
  - do not answer a `TBD` of a design unit yourself: ask the human and offer your draft as the proposed answer
35
35
  - do not run `ai-factory gcf phase next`: the phase moves on the human's word only
36
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
37
+ - do not change a setting of `gcf/config.yml` or the `language` of `.ai-factory/config.yaml`: `ai-factory gcf config set` belongs to the human
38
38
  - do not reuse an ended id: rework gets a new id with `reopens: <old id>`
39
39
  - do not commit past the gate with `--no-verify`: a refused commit names what to fix
40
40