gcf-harness-aif-ext 0.3.4 → 0.5.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 +7 -3
- package/commands/harness.js +43 -25
- package/extension.json +1 -1
- package/hooks/readset-guard.js +1 -1
- package/package.json +2 -2
- package/skills/gcf-run/SKILL.md +27 -15
- 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.4.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
|
|
|
@@ -43,6 +43,10 @@ ai-factory extension update gcf-harness-aif-ext
|
|
|
43
43
|
|
|
44
44
|
Updating keeps the hook entry valid — it points into the extension's install directory, which an update refreshes in place.
|
|
45
45
|
|
|
46
|
+
0.5.0 drives work items (`gcf/work/<id>.md`, schema 3) instead of cards and requires `gcf-aif-ext` 0.4.0 — update both together; against an older core the gate refuses with `gcf-aif-ext >= 0.4.0 is required`. A project still holding schema 2 cards in `gcf/cards/` re-creates them first (see the core's "Upgrading from 0.3.x"). What changes for you: `/gcf-run` takes ids of items in `ready`; it no longer stops at `built` but ends each item with `submit`, `close` or `block`; the plan is improved in passes until it converges; the WIP cap counts only items that end waiting for a verdict.
|
|
47
|
+
|
|
48
|
+
0.4.1 is a patch release. `/gcf-run` now commits the implementation first, then flips the card to `built` in a second commit, with `built_at` set to the implementation commit's hash. Update the core to 0.3.1 together with the harness: without it, an audit run before a verdict makes the pre-commit gate refuse that verdict.
|
|
49
|
+
|
|
46
50
|
To remove the extension:
|
|
47
51
|
|
|
48
52
|
```bash
|
|
@@ -53,9 +57,9 @@ Removal does not touch `.claude/settings.json`: delete the `PreToolUse` entry wh
|
|
|
53
57
|
|
|
54
58
|
## What it adds
|
|
55
59
|
|
|
56
|
-
**`/gcf-run <
|
|
60
|
+
**`/gcf-run <item-id…>`** — drives work items through plan, implementation and DoD, with every gate checked *before* the tokens are spent: the audit ritual, the gate below, the context budget. The plan is written by `aif-plan`, then improved in passes, each by a fresh subagent that reads only the plan and the readset and records every default it picks; `ai-factory gcf plan check <id>` runs after each pass, and a pass that changes no task ends the loop. A plan still changing after `plan_passes` passes, or a question only the human can answer (a save format, a public API, a new dependency, a pillar conflict), blocks the item before any code is written. The DoD is the project's `dod.test` and `dod.build` commands from `gcf/config.yml` plus a review by a fresh subagent, up to `dod_attempts` attempts. Every item ends in a state the core records: `gcf submit` (gate `verdict` or `decision` — waits for the human), `gcf close` (gate `auto` — done, or a soft unit closed as `unplayed`) or `gcf block` with the reason. The verdict and the decision belong to the human: the pipeline stops at `submit` and prints the playtest list.
|
|
57
61
|
|
|
58
|
-
**`ai-factory gcf-harness gate <
|
|
62
|
+
**`ai-factory gcf-harness gate <item-id…>`** — the `/gcf-run` gate as code. It reads one `gcf audit --json` report and exits 1 with a one-line reason when the items may not go: a core older than 0.4.0, a `schema:` other than 1 in `gcf/config.yml`, an id with no work item, a feature (run its children; the feature closes when they are finished), an item that is not `ready`, an unplayed dependency, no WIP room for the batch (items awaiting a verdict + the batch's `gate: verdict` items over `wip_built_limit`), or two items of the batch touching the same system unless both say `parallel: ok`. The tune limit is not a gate rule: `verdict tune` itself refuses at the limit, and an item tuned up to it still gets its last build. The skill runs it and stops on a non-zero exit — no model compares the numbers.
|
|
59
63
|
|
|
60
64
|
**`/gcf-run-ingest <gdd.md>`** — imports a design document as a fan-out workflow: parallel aspect extraction, provenance verified by code rather than by an agent (a quote either is a verbatim substring of its section or it is not), completeness lenses per candidate, and an interaction matrix computed from the candidates that write the same state.
|
|
61
65
|
|
package/commands/harness.js
CHANGED
|
@@ -15,38 +15,56 @@ const SESSION_RULE = /^(\/|\*\*\/)?\.gcf-session\/?$/;
|
|
|
15
15
|
|
|
16
16
|
// ─── gate ────────────────────────────────────────────────────────────────────────────
|
|
17
17
|
|
|
18
|
-
/** Why one
|
|
19
|
-
function
|
|
20
|
-
if (!
|
|
21
|
-
if (
|
|
22
|
-
return `${id} is
|
|
18
|
+
/** Why one work item may not go, or null. */
|
|
19
|
+
function itemProblem(id, item, report) {
|
|
20
|
+
if (!item) return `${id} has no work item — take the unit or create the item first (/gcf-card)`;
|
|
21
|
+
if (item.type === 'feature') {
|
|
22
|
+
return `${id} is a feature — run its children, the feature closes when they are finished`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
if (item.status !== 'ready') {
|
|
26
|
+
return `${id} is ${item.status} — only ready items are driven`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A child of a feature paused after a death does not start: the decision may kill it.
|
|
30
|
+
const parent = item.parent ? report.items.find((p) => p.id === item.parent) : null;
|
|
31
|
+
if (parent?.status === 'awaiting-decision') {
|
|
32
|
+
return `${id}: its feature ${parent.id} awaits a decision — decide it first`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A dependency inside the same batch lands here too: the dependent waits for it.
|
|
26
36
|
const open = report.open_depends.find((o) => o.id === id);
|
|
27
|
-
if (open)
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
if (open) {
|
|
38
|
+
return (
|
|
39
|
+
`${id} depends on unplayed ${open.depends.join(', ')} — playtest them first; ` +
|
|
40
|
+
'a killed one never plays: take it out of depends, or drop'
|
|
41
|
+
);
|
|
30
42
|
}
|
|
31
43
|
|
|
44
|
+
// The tune limit is held by `verdict tune` itself: an item tuned up to the limit is still
|
|
45
|
+
// built once more, and refusing that run would leave it stuck in ready.
|
|
32
46
|
return null;
|
|
33
47
|
}
|
|
34
48
|
|
|
35
49
|
/** Why the batch as a whole may not go, or null. */
|
|
36
|
-
function batchProblem(
|
|
37
|
-
//
|
|
38
|
-
|
|
50
|
+
function batchProblem(items, wip) {
|
|
51
|
+
// Only an item that ends waiting for a verdict takes a WIP place; `wip.over` is false at
|
|
52
|
+
// count === limit, and this run adds every such item of the batch.
|
|
53
|
+
const waiting = items.filter((i) => i.gate === 'verdict').length;
|
|
54
|
+
if (waiting && wip.count + waiting > wip.limit) {
|
|
39
55
|
return (
|
|
40
|
-
`WIP: ${wip.count}
|
|
41
|
-
|
|
56
|
+
`WIP: ${wip.count} awaiting a verdict + ${waiting} in this run exceeds the limit of ` +
|
|
57
|
+
`${wip.limit} — playtest first`
|
|
42
58
|
);
|
|
43
59
|
}
|
|
44
60
|
|
|
45
|
-
for (let i = 0; i <
|
|
46
|
-
for (const other of
|
|
47
|
-
const shared =
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
for (let i = 0; i < items.length; i++) {
|
|
62
|
+
for (const other of items.slice(i + 1)) {
|
|
63
|
+
const shared = items[i].system.filter((s) => other.system.includes(s));
|
|
64
|
+
// `parallel: ok` on both is the operator's explicit permission (Methodology §7).
|
|
65
|
+
const allowed = items[i].parallel === 'ok' && other.parallel === 'ok';
|
|
66
|
+
if (shared.length && !allowed) {
|
|
67
|
+
const pair = `${items[i].id} and ${other.id}`;
|
|
50
68
|
|
|
51
69
|
return `${pair} both touch ${shared.join(', ')} — run them separately`;
|
|
52
70
|
}
|
|
@@ -59,12 +77,12 @@ function batchProblem(cards, wip) {
|
|
|
59
77
|
const verdict = (reason) => ({ pass: !reason, reason: reason ?? '' });
|
|
60
78
|
|
|
61
79
|
/**
|
|
62
|
-
* Decides whether the
|
|
80
|
+
* Decides whether the work items may enter the paid stages, from one `gcf audit --json` report.
|
|
63
81
|
* Pure: the report is the whole input, so every rule has a test and no model compares numbers.
|
|
64
82
|
*/
|
|
65
83
|
export function gate(report, ids) {
|
|
66
|
-
if (!Array.isArray(report?.
|
|
67
|
-
return verdict('the audit report has no
|
|
84
|
+
if (!Array.isArray(report?.items)) {
|
|
85
|
+
return verdict('the audit report has no work item summary — gcf-aif-ext >= 0.4.0 is required');
|
|
68
86
|
}
|
|
69
87
|
|
|
70
88
|
if (report.schema !== SCHEMA) {
|
|
@@ -73,9 +91,9 @@ export function gate(report, ids) {
|
|
|
73
91
|
return verdict(`gcf/config.yml has schema: ${report.schema}; ${drives}`);
|
|
74
92
|
}
|
|
75
93
|
|
|
76
|
-
const byId = new Map(report.
|
|
94
|
+
const byId = new Map(report.items.map((c) => [c.id, c]));
|
|
77
95
|
for (const id of ids) {
|
|
78
|
-
const problem =
|
|
96
|
+
const problem = itemProblem(id, byId.get(id), report);
|
|
79
97
|
if (problem) return verdict(problem);
|
|
80
98
|
}
|
|
81
99
|
|
|
@@ -216,7 +234,7 @@ export function register(program) {
|
|
|
216
234
|
|
|
217
235
|
harness
|
|
218
236
|
.command('gate <ids...>')
|
|
219
|
-
.description('Gate
|
|
237
|
+
.description('Gate work items before the paid stages of /gcf-run: exit 1 with the reason')
|
|
220
238
|
.option('--json', 'machine output: {pass, reason}')
|
|
221
239
|
.action(runGate);
|
|
222
240
|
}
|
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.5.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/hooks/readset-guard.js
CHANGED
|
@@ -72,7 +72,7 @@ function insideVerdict(rel, manifest) {
|
|
|
72
72
|
// agent told about it on every denial will reach for it and turn a leak into a bypass.
|
|
73
73
|
reason:
|
|
74
74
|
`${rel} is outside the current readset (.gcf-session/readset.json). ` +
|
|
75
|
-
'Extend it via `ai-factory gcf context <
|
|
75
|
+
'Extend it via `ai-factory gcf context <item>`, ' +
|
|
76
76
|
'or work from the files already in the readset.',
|
|
77
77
|
};
|
|
78
78
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gcf-harness-aif-ext",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.4.0"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"extension.json",
|
package/skills/gcf-run/SKILL.md
CHANGED
|
@@ -1,35 +1,47 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gcf-run
|
|
3
|
-
description: Drive one or more GCF
|
|
3
|
+
description: Drive one or more GCF work items through plan, implement and DoD in a single run - gates before tokens, a converging plan, readset discipline, then submit, close or block. Use when the user says "прогони задачу" / "прогони карточку" / "run the item".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
mode: pipeline
|
|
7
7
|
|
|
8
8
|
purpose:
|
|
9
|
-
- drive a
|
|
9
|
+
- drive a work item through gate, readset, plan, implement and its DoD with the stop rules checked before each paid stage
|
|
10
|
+
- end every item in a state the core records: `submit` (waits for the human), `close` (gate auto, done) or `block` (needs the human)
|
|
10
11
|
|
|
11
12
|
inputs:
|
|
12
|
-
- 1 or more
|
|
13
|
+
- 1 or more work item or design unit ids; a batch shares one build and one playtest list
|
|
13
14
|
|
|
14
15
|
preconditions:
|
|
15
16
|
- `gcf/` exists and `ai-factory gcf audit` runs (the gcf-aif-ext extension provides it)
|
|
16
17
|
|
|
17
18
|
forbidden_behaviors:
|
|
18
|
-
- do not
|
|
19
|
+
- do not give a verdict or a decision: `verdict` and `decide` belong to the human, this pipeline stops at `submit`
|
|
19
20
|
- do not read a `gcf/` file outside the readset from `ai-factory gcf context`
|
|
20
21
|
- do not continue past a failed gate: report the one-line reason, delete `.gcf-session/readset.json`, and stop - a stop is a normal outcome
|
|
21
|
-
- do not compare the audit numbers yourself: the gate command is the only judge of whether
|
|
22
|
+
- do not compare the audit numbers yourself: the gate command is the only judge of whether an item may go
|
|
23
|
+
- do not ask the human about a reversible choice during planning: pick the safe default and write it into the plan
|
|
24
|
+
- do not edit `status:` or `gcf/LIVE.md` by hand: `submit`, `close` and `block` own them
|
|
22
25
|
|
|
23
26
|
workflow:
|
|
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`;
|
|
25
|
-
2. for a
|
|
26
|
-
3.
|
|
27
|
-
4.
|
|
28
|
-
5.
|
|
29
|
-
6.
|
|
30
|
-
7.
|
|
31
|
-
8.
|
|
32
|
-
9. after
|
|
27
|
+
1. session ritual: delete a `.gcf-session/readset.json` left by a killed run; run `ai-factory gcf audit --json`; read `gcf/INDEX.md`; report the audit first line to the human
|
|
28
|
+
2. for a unit still in `gcf/design/`: take it with the gcf-card skill first
|
|
29
|
+
3. for a feature id: replace it with its `ready` children from the audit report's `items` (`parent` names the feature) that are not in `open_depends`; a feature in `awaiting-decision` starts none of them
|
|
30
|
+
4. of those children, keep one per subsystem unless every child sharing it says `parallel: ok`; the rest wait for the next run; an empty list: report "nothing to run" and stop
|
|
31
|
+
5. gate: run `ai-factory gcf-harness gate <item-ids...>` over the whole batch; a non-zero exit is a failed gate, its stderr line is the reason
|
|
32
|
+
6. readset: run `ai-factory gcf context <item-id> --json`; write `{"item":"<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
|
|
33
|
+
7. plan: run aif-plan full for the item into its `plan:` path, by default `.ai-factory/plans/<id>.md`, reading only the readset files; an existing plan (the item came back after a tune, an unblock or a continue) is kept, not rewritten
|
|
34
|
+
8. improve the plan in passes, each pass a fresh subagent that reads only the plan and the readset and applies aif-improve without questions, writing each default it picks into the plan
|
|
35
|
+
9. after each pass run `ai-factory gcf plan check <id>`; a failure blocks the item with its reason; a pass that changes no task ends the loop - the plan converged
|
|
36
|
+
10. passes <= `plan_passes` from `gcf/config.yml` (5 by default); a plan still changing after the last pass: run `ai-factory gcf block <id> --reason "plan did not converge"` and stop
|
|
37
|
+
11. a question only the human can answer - a save format, a public API, a new dependency, a pillar conflict: run `ai-factory gcf block <id> --reason "<question>"` and stop
|
|
38
|
+
12. implement with aif-implement, then verify: the project's `dod.test` and `dod.build` commands from `gcf/config.yml`, plus the item's negative constraints; attempts <= `dod_attempts`, each with a diagnosis
|
|
39
|
+
13. review the diff with a fresh subagent; fix what it finds before the commit; DoD still red after the attempts: commit nothing, run `ai-factory gcf block <id> --reason "<diagnosis>"`
|
|
40
|
+
14. commit the implementation via aif-commit; write the item's `live:` delta
|
|
41
|
+
15. gate `verdict` or `decision`: run `ai-factory gcf submit <id>`; gate `auto`: run `ai-factory gcf close <id>` - a soft unit closes unplayed
|
|
42
|
+
16. a dependency still waiting for its verdict refuses the close: run `ai-factory gcf block <id> --reason "waits for <dep>"`
|
|
43
|
+
17. close the run: delete `.gcf-session/readset.json`, run `ai-factory gcf playtest` and hand the list to the human, with one report row per plan pass: pass, tasks changed, result
|
|
44
|
+
18. after a tune verdict or an unblock, re-enter this pipeline at step 5: the gates re-run because the counters moved
|
|
33
45
|
|
|
34
46
|
invocation:
|
|
35
|
-
- run when the user names a
|
|
47
|
+
- run when the user names a work item or a unit to drive; batches share steps 1 and 17
|
|
@@ -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
|
|
18
|
+
- do not create work items: the package holds design units, a work item 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'),
|