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 +1 -1
- package/commands/harness.js +1 -1
- package/extension.json +1 -1
- package/package.json +2 -2
- package/skills/gcf-run/SKILL.md +4 -4
- package/skills/gcf-run-ingest/SKILL.md +12 -10
- package/workflows/ingest.js +77 -35
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.
|
|
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
|
|
package/commands/harness.js
CHANGED
|
@@ -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 —
|
|
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.
|
|
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
|
+
"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.
|
|
31
|
+
"gcf-aif-ext": "^0.3.0"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"extension.json",
|
package/skills/gcf-run/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gcf-run
|
|
3
|
-
description: Drive one or more GCF
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
28
|
-
5.
|
|
29
|
-
6. append
|
|
30
|
-
7.
|
|
31
|
-
8.
|
|
32
|
-
9.
|
|
33
|
-
10.
|
|
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
|
package/workflows/ingest.js
CHANGED
|
@@ -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: '
|
|
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,
|
|
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
|
-
|
|
51
|
-
|
|
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: ['
|
|
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
|
-
/**
|
|
207
|
-
function
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
'
|
|
238
|
-
`
|
|
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
|
-
//
|
|
299
|
-
//
|
|
300
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
419
|
-
|
|
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
|
|
424
|
-
//
|
|
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'),
|