gcf-harness-aif-ext 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@ The harness turns policy-as-text into an executable pipeline. It has **no memory
11
11
 
12
12
  ## Requirements
13
13
 
14
- - [`gcf-aif-ext`](../gcf-aif-ext) `^0.2.7` installed, with a `gcf/` tree in the project — declared as a peer dependency, and `gcf-harness init` refuses when the `gcf` command is not registered
14
+ - [`gcf-aif-ext`](../gcf-aif-ext) `^0.3.0` installed, with a `gcf/` tree in the project — declared as a peer dependency, and `gcf-harness init` refuses when the `gcf` command is not registered
15
15
  - Claude Code 2.1.270+ (hooks, dynamic workflows) — the only supported runtime
16
16
  - AI Factory 2.x, Node.js ≥ 22.14
17
17
 
@@ -17,7 +17,7 @@ const SESSION_RULE = /^(\/|\*\*\/)?\.gcf-session\/?$/;
17
17
 
18
18
  /** Why one card may not go, or null. */
19
19
  function cardProblem(id, card, report) {
20
- if (!card) return `${id} has no card file — materialize the candidate first`;
20
+ if (!card) return `${id} has no card file — take the unit into work first (/gcf-card)`;
21
21
  if (!['proposed', 'tuning'].includes(card.status)) {
22
22
  return `${id} is ${card.status} — only proposed and tuning cards are driven`;
23
23
  }
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-harness-aif-ext",
4
- "version": "0.3.4",
4
+ "version": "0.4.0",
5
5
  "description": "GCF orchestration harness for Claude Code: one-command card pipeline, readset guard",
6
6
  "skills": [
7
7
  "skills/gcf-run",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gcf-harness-aif-ext",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "GCF orchestration harness for Claude Code: one-command card pipeline, readset guard",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,7 +28,7 @@
28
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
29
  },
30
30
  "peerDependencies": {
31
- "gcf-aif-ext": "^0.2.7"
31
+ "gcf-aif-ext": "^0.3.0"
32
32
  },
33
33
  "files": [
34
34
  "extension.json",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: gcf-run
3
- description: Drive one or more GCF cards from backlog to built in a single run - gates before tokens, readset discipline, playtest list at the end. Use when the user says "прогони карточку" / "run the card".
3
+ description: Drive one or more GCF units from the backlog to built in a single run - gates before tokens, readset discipline, playtest list at the end. Use when the user says "прогони карточку" / "run the card".
4
4
  ---
5
5
 
6
6
  mode: pipeline
@@ -9,7 +9,7 @@ purpose:
9
9
  - drive a card through gate, readset, plan, implement and build with the stop rules checked before each paid stage
10
10
 
11
11
  inputs:
12
- - 1 or more cand-NNN or card-NNN ids; a batch shares one build and one playtest list
12
+ - 1 or more unit or card ids; a batch shares one build and one playtest list
13
13
 
14
14
  preconditions:
15
15
  - `gcf/` exists and `ai-factory gcf audit` runs (the gcf-aif-ext extension provides it)
@@ -22,7 +22,7 @@ forbidden_behaviors:
22
22
 
23
23
  workflow:
24
24
  1. session ritual: delete a `.gcf-session/readset.json` left by a killed run; run `ai-factory gcf audit --json`; read `gcf/INDEX.md`; stale built cards with live dependents mean stop - print the playtest list instead of building
25
- 2. for a cand-NNN input: materialize it with the gcf-card skill; a hard route leaves the pipeline for the normal AIF cycle here
25
+ 2. for a unit still in `gcf/design/`: take it with the gcf-card skill; a hard unit leaves the pipeline for the normal AIF cycle here
26
26
  3. gate: run `ai-factory gcf-harness gate <card-ids...>` over the whole batch; a non-zero exit is a failed gate, its stderr line is the reason
27
27
  4. readset: run `ai-factory gcf context <card-id> --json`; write `{"card":"<id>","files": ctx.files.map(f => f.path)}` to `.gcf-session/readset.json` - paths, not objects; `over_budget: true` stops the run with a D1 question naming the subsystem
28
28
  5. plan with aif-plan, shaped by the card slots, reading only the readset files
@@ -32,4 +32,4 @@ workflow:
32
32
  9. after a tune verdict, re-enter this pipeline at step 3: the gates re-run because the counters moved
33
33
 
34
34
  invocation:
35
- - run when the user names a card or candidate to drive; batches share steps 1 and 8
35
+ - run when the user names a card or a unit to drive; batches share steps 1 and 8
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: gcf-run-ingest
3
- description: Fan-out GDD import on Claude Code dynamic workflows - parallel extraction and lenses, one output package. Use for a large GDD or when the user says "запусти импорт воркфлоу".
3
+ description: Fan-out GDD import on Claude Code dynamic workflows - parallel extraction and lenses, design units in one package. Use for a large GDD or when the user says "запусти импорт воркфлоу".
4
4
  ---
5
5
 
6
6
  mode: orchestrated import
@@ -15,22 +15,24 @@ preconditions:
15
15
  - `gcf/` exists; the user has opted into multi-agent orchestration (the Workflow tool requires it)
16
16
 
17
17
  forbidden_behaviors:
18
- - do not create card files: the package holds backlog lines, a card materializes only at pull
18
+ - do not create card files: the package holds design units, a card exists only when `/gcf-card` takes one
19
19
  - do not let an agent write gcf files from inside the workflow: the script returns data, this skill writes
20
20
  - do not paste the package into PILLARS.md: pillar candidates go to the human as D1
21
21
  - do not write anything from a failed workflow: a dead agent fails it on purpose, so report its label and stop
22
22
 
23
23
  workflow:
24
24
  1. run `ai-factory gcf anchor <file> --json` and keep the sections list
25
- 2. on a re-import the output carries `delta`: keep only the sections whose anchor is in `delta.added`, skip the workflow when none are left, and apply the other delta classes as step 11 of the gcf-ingest skill does
25
+ 2. on a re-import the output carries `delta`: keep only the sections whose anchor is in `delta.added`, skip the workflow when none are left, and apply the other delta classes as step 17 of the gcf-ingest skill does
26
26
  3. read `envelope_default_spread` from `gcf/config.yml`
27
- 4. read the highest NNN across `gcf/backlog.md` and `gcf/cards/`; invoke the Workflow tool with scriptPath `.ai-factory/extensions/gcf-harness-aif-ext/workflows/ingest.js` and args `{sections, spread, start_num}`, start_num = NNN + 1
28
- 5. write the results: candidate lines appended to `gcf/backlog.md` keeping the minted ids; fold each `exact` number into the description of the line citing its anchor
29
- 6. append the D1 and D2 records to `gcf/open-questions.md` in the record form the gcf-ingest skill states under outputs, a D2 with its `assumed:` and `until:`
30
- 7. run `ai-factory gcf quotes` and move each flagged line to a D1 with lens `provenance`, then run `ai-factory gcf index`
31
- 8. mark a candidate whose nouns match the `purpose` or the `code` paths of an existing systems doc with `impl?`; with `gcf/systems/` empty on a codebase, report the marks as blind
32
- 9. report the package in the GCF order: D1 questions first, pillar candidates, backlog summary with the top crossing candidates, D2 envelope assumptions
33
- 10. verify equivalence on the first real GDD: compare this package against a sequential `/gcf-ingest` pass and report any divergence as a harness bug
27
+ 4. run `ai-factory gcf where --json` and keep every listed id as `taken`: an import never mints an id the project has used
28
+ 5. invoke the Workflow tool with scriptPath `.ai-factory/extensions/gcf-harness-aif-ext/workflows/ingest.js` and args `{sections, spread, taken}`
29
+ 6. append each unit's `section` to `gcf/design/<system>.md`; start a missing file with `# <system>` and `purpose: TBD`; append `- <id>` per unit to `gcf/backlog.md`
30
+ 7. write each envelope into a `feel` record of the unit citing its anchor, with `assumed: q-NNN` beside its quote; write each `exact` number as a closing comparison into that unit
31
+ 8. append the D1 and D2 records to `gcf/open-questions.md` in the record form the gcf-ingest skill states under outputs, a D2 with its `assumed:` and `until:`
32
+ 9. run `ai-factory gcf design check` and fix every blocking finding: a digit in prose becomes a comparison or words, a misquote becomes a D1 with lens `provenance`
33
+ 10. mark a unit whose nouns match the `purpose` or the `code` paths of an existing systems doc with `flags: impl?`; with `gcf/systems/` empty on a codebase, report the marks as blind
34
+ 11. report the package in the GCF order: D1 questions first, pillar candidates, the design summary per subsystem with the top crossing units, D2 envelope assumptions
35
+ 12. verify equivalence on the first real GDD: compare this package against a sequential `/gcf-ingest` pass and report any divergence as a harness bug
34
36
 
35
37
  invocation:
36
38
  - run when the user asks for the workflow import on a GDD
@@ -4,12 +4,12 @@ export const meta = {
4
4
  'Fan-out GDD import: parallel aspect extraction, code-verified provenance, lenses, one package',
5
5
  phases: [
6
6
  { title: 'Extract', detail: 'one agent per aspect per segment of the anchored sections' },
7
- { title: 'Lenses', detail: 'completeness per candidate, coherence over the whole set' },
7
+ { title: 'Lenses', detail: 'route and id per unit, coherence over the whole set' },
8
8
  { title: 'Conflicts', detail: 'pairwise checks where the interaction matrix crosses' },
9
9
  ],
10
10
  };
11
11
 
12
- // args: { sections: [{anchor, heading, body}], spread, start_num, segment_chars? }
12
+ // args: { sections: [{anchor, heading, body}], spread, taken?: string[], segment_chars? }
13
13
  // The main thread runs `ai-factory gcf anchor --json` BEFORE this workflow and writes
14
14
  // the outputs AFTER it — the script has no filesystem, by design (Harness-PRD §6.1).
15
15
  // Layout: schemas, then pure helpers, then the three phases, then package assembly.
@@ -47,8 +47,17 @@ const UNITS = {
47
47
  const CAND_LENS = {
48
48
  type: 'object',
49
49
  properties: {
50
- gaps: strings('missing answers: state, input, feedback, refusal, boundaries, persistence'),
51
- systems: strings('subsystem slugs this candidate touches'),
50
+ id: {
51
+ type: 'string',
52
+ pattern: '^[a-z0-9]+(-[a-z0-9]+)*$',
53
+ description:
54
+ 'the unit id: a short lowercase latin hyphen slug of its name, frozen once written',
55
+ },
56
+ systems: {
57
+ type: 'array',
58
+ items: { type: 'string', pattern: '^[a-z0-9]+(-[a-z0-9]+)*$' },
59
+ description: 'subsystem slugs this unit touches, the one it lives in first',
60
+ },
52
61
  state_written: strings('named state this mechanic writes'),
53
62
  route: {
54
63
  enum: ['soft', 'hard'],
@@ -59,7 +68,7 @@ const CAND_LENS = {
59
68
  },
60
69
  route_why: { type: 'string', description: 'the reason for the route, one short clause' },
61
70
  },
62
- required: ['gaps', 'systems', 'state_written', 'route', 'route_why'],
71
+ required: ['id', 'systems', 'state_written', 'route', 'route_why'],
63
72
  };
64
73
 
65
74
  const FINDING = {
@@ -203,21 +212,56 @@ function crossingPairs(cands) {
203
212
  return pairs;
204
213
  }
205
214
 
206
- /** The backlog line grammar `gcf index` parses (GCF-PRD §5.2). */
207
- function candidateLine(c, deps) {
208
- const fields = [
209
- `- ${c.id}`,
210
- c.name,
211
- `route: ${c.route === 'hard' ? 'hard' : 'soft'} (${c.route_why || 'acceptance is a playtest'})`,
212
- `sys: ${c.systems[0] ?? 'unassigned'}`,
213
- `deps: ${deps.join(', ') || '-'}`,
214
- `src: ${c.anchor}`,
215
- `q: "${c.quote}"`,
216
- ];
217
- if (c.gaps.length) fields.push(`gaps: ${c.gaps.join(', ')}`);
218
- fields.push(c.detail || c.name);
219
-
220
- return fields.join(' | ');
215
+ /** An id no unit has used yet: the agent's slug, suffixed -2, -3… on a collision. */
216
+ function freeId(wanted, taken) {
217
+ const base = /^[a-z0-9]+(-[a-z0-9]+)*$/.test(wanted ?? '') ? wanted : 'unit';
218
+ let id = base;
219
+ for (let n = 2; taken.has(id); n++) id = `${base}-${n}`;
220
+ taken.add(id);
221
+
222
+ return id;
223
+ }
224
+
225
+ // Every slot the GDD did not answer reads TBD: the questions come when /gcf-card takes the unit.
226
+ const SOFT_SLOTS = ['behavior', 'refusal', 'boundaries', 'persistence', 'feel_like', 'feel'];
227
+
228
+ /**
229
+ * The `## <id>` section of a unit in gcf/design/<system>.md (GCF-PRD §5.7): a player verb is
230
+ * a soft mechanic, an acceptance an autotest gives is a hard rule. Only the quote is the GDD's.
231
+ */
232
+ function unitSection(c, depends) {
233
+ const hard = c.route === 'hard';
234
+ const why = c.route_why || (hard ? 'an autotest accepts it' : 'acceptance is a playtest');
235
+ const slot = hard ? 'statement' : 'intent';
236
+ const body = hard
237
+ ? ['statement:', `- ${c.detail || c.name}`, '', 'check: TBD']
238
+ : [
239
+ 'intent:',
240
+ `- ${c.detail || c.name}`,
241
+ ...SOFT_SLOTS.map((s) => `\n${s}: TBD`),
242
+ '\nconstraints: TBD',
243
+ '\nmiss_criterion: TBD',
244
+ ];
245
+
246
+ return [
247
+ `## ${c.id}`,
248
+ '',
249
+ `type: ${hard ? 'rule' : 'mechanic'}`,
250
+ `title: ${c.name}`,
251
+ `route: ${hard ? 'hard' : 'soft'} (${why})`,
252
+ `system: [${(c.systems.length ? c.systems : ['unassigned']).join(', ')}]`,
253
+ 'milestone: TBD',
254
+ 'pillars: TBD',
255
+ `depends: ${depends.length ? `[${depends.join(', ')}]` : 'none'}`,
256
+ `source: ${c.anchor}`,
257
+ '',
258
+ ...body,
259
+ '',
260
+ 'provenance:',
261
+ `- slot: ${slot}`,
262
+ ` src: ${c.anchor}`,
263
+ ` quote: "${c.quote}"`,
264
+ ].join('\n');
221
265
  }
222
266
 
223
267
  // ─── Prompts ─────────────────────────────────────────────────────────────────────────
@@ -234,8 +278,8 @@ function extractPrompt(aspect, segment, part, parts) {
234
278
 
235
279
  function lensPrompt(c, subsystemNames, sectionBody) {
236
280
  return [
237
- 'Completeness lenses over one feature candidate, and its route with the reason.',
238
- `Candidate: ${c.name} (${c.anchor}: "${c.quote}").`,
281
+ 'One feature unit: its route with the reason, its subsystems, the state it writes, its id.',
282
+ `Unit: ${c.name} (${c.anchor}: "${c.quote}").`,
239
283
  `Known subsystems: ${subsystemNames.join(', ') || 'none yet'}.`,
240
284
  'Section:',
241
285
  sectionBody,
@@ -295,10 +339,9 @@ async function ask(prompt, opts) {
295
339
 
296
340
  const sections = args.sections;
297
341
  const spread = args.spread ?? 0.3;
298
- // Candidate ids continue the existing backlog numbering: minting from the array index
299
- // makes every re-ingest emit cand-001 again. Number(): args come from a model-composed
300
- // call, and "5" would concatenate.
301
- const startNum = Number(args.start_num) || 1;
342
+ // Every id the project has used (`ai-factory gcf where --json`): a dropped or done unit's id
343
+ // is never minted again, or the new unit would collide with the old one's history.
344
+ const taken = new Set(Array.isArray(args.taken) ? args.taken : []);
302
345
  const segments = packSegments(sections, Number(args.segment_chars) || 60000);
303
346
  const bodyOf = new Map(sections.map((s) => [s.anchor, s.body]));
304
347
  const multi = segments.length > 1;
@@ -333,10 +376,7 @@ const enriched = await pipeline(candidates, (c) =>
333
376
  schema: CAND_LENS,
334
377
  }).then((r) => ({ ...c, ...r })),
335
378
  );
336
- const cands = enriched.map((c, i) => ({
337
- ...c,
338
- id: `cand-${String(startNum + i).padStart(3, '0')}`,
339
- }));
379
+ const cands = enriched.map((c) => ({ ...c, id: freeId(c.id, taken) }));
340
380
 
341
381
  // No sections at all also takes the digest path: there is no segment to attach.
342
382
  const wholeDoc = segments.length === 1 ? segments[0].texts.join('\n\n') : citedDigest(byAspect);
@@ -394,7 +434,7 @@ const envelopeQuestions = envelopes
394
434
  question: `"${e.name}": "${e.quote}" taken as the envelope ${e.envelope}`,
395
435
  proposed: e.envelope,
396
436
  assumed: `±${Math.round(spread * 100)}% around the cited number`,
397
- until: `manual(materialization of the candidate citing ${e.anchor})`,
437
+ until: `manual(materialization of the unit citing ${e.anchor})`,
398
438
  }));
399
439
  // GCF-PRD §11.2: the GDD's own open questions are the designer's gaps, said out loud.
400
440
  const gddQuestions = (byAspect.questions ?? []).map((u) => ({
@@ -415,13 +455,15 @@ const questions = [
415
455
  const result = {
416
456
  pillar_candidates: byAspect.pillars ?? [],
417
457
  subsystems: subsystemNames,
418
- candidates: cands.map((c) => ({
419
- line: candidateLine(c, deps[c.id]),
458
+ units: cands.map((c) => ({
459
+ id: c.id,
460
+ system: c.systems[0] ?? 'unassigned',
461
+ section: unitSection(c, deps[c.id]),
420
462
  crossings: pairs.filter((p) => p.a === c || p.b === c).length,
421
463
  })),
422
464
  envelopes: envelopes.filter((e) => e.envelope),
423
- // Counts, hard limits and thresholds stay exact: the write step folds each into the
424
- // description of the candidate citing the same anchor.
465
+ // Counts, hard limits and thresholds stay exact: the write step writes each as a closing
466
+ // comparison into the unit citing the same anchor.
425
467
  exact: envelopes.filter((e) => !e.envelope),
426
468
  d1: questions.filter((q) => q.class === 'D1'),
427
469
  d2: questions.filter((q) => q.class === 'D2'),