@really-knows-ai/foundry 3.5.7 → 3.5.9
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 +16 -10
- package/dist/.opencode/plugins/foundry-tools/config-create-tools.js +2 -3
- package/dist/.opencode/plugins/foundry.js +11 -1
- package/dist/CHANGELOG.md +23 -0
- package/dist/README.md +16 -10
- package/dist/docs/README.md +6 -6
- package/dist/docs/architecture.md +59 -19
- package/dist/docs/concepts.md +55 -19
- package/dist/docs/getting-started.md +37 -15
- package/dist/docs/memory-maintenance.md +3 -3
- package/dist/docs/tools.md +131 -70
- package/dist/docs/work-spec.md +38 -52
- package/dist/scripts/lib/config-creators/cycle.js +6 -10
- package/dist/scripts/lib/config-validators/cycle.js +1 -9
- package/dist/scripts/lib/feedback-store.js +1 -52
- package/dist/scripts/lib/sort-reason.js +8 -7
- package/dist/scripts/lib/sort-routing.js +106 -28
- package/dist/scripts/lib/tool-paths.js +5 -1
- package/dist/scripts/orchestrate-cycle.js +3 -13
- package/dist/scripts/orchestrate-phases.js +3 -7
- package/dist/scripts/sort.js +16 -53
- package/dist/skills/add-cycle/SKILL.md +4 -4
- package/dist/skills/add-flow/SKILL.md +1 -1
- package/dist/skills/add-law/SKILL.md +1 -1
- package/dist/skills/human-appraise/SKILL.md +12 -40
- package/package.json +1 -1
package/dist/docs/work-spec.md
CHANGED
|
@@ -32,9 +32,8 @@ flow: <flow-id>
|
|
|
32
32
|
cycle: <current-cycle-id>
|
|
33
33
|
stages: [forge:write-haiku, quench:check-syllables, appraise:evaluate-quality]
|
|
34
34
|
max-iterations: 3
|
|
35
|
-
human-appraise: false
|
|
36
|
-
deadlock-appraise: true
|
|
37
|
-
deadlock-iterations: 5
|
|
35
|
+
always-human-appraise: false
|
|
36
|
+
deadlock-human-appraise: true
|
|
38
37
|
models:
|
|
39
38
|
forge: anthropic/claude-opus-4.7
|
|
40
39
|
appraise: openai/gpt-5
|
|
@@ -46,21 +45,21 @@ assay:
|
|
|
46
45
|
Fields:
|
|
47
46
|
- `flow` — the foundry flow being executed.
|
|
48
47
|
- `cycle` — the current cycle id.
|
|
49
|
-
- `stages` — the ordered route for this cycle. Each entry uses `base:alias` format where `base` is the stage type (`forge`, `quench`, `appraise`, `human-appraise`, or `assay`) and `alias` is a human-readable name for what that stage does in this cycle. The list is derived from the cycle and artefact type: `forge` and `appraise` are always included; `quench` is included iff any applicable law declares validators; `human-appraise` is included iff the cycle sets `human-appraise: true`; and `assay` is included iff the cycle declares an `assay.extractors` block.
|
|
48
|
+
- `stages` — the ordered route for this cycle. Each entry uses `base:alias` format where `base` is the stage type (`forge`, `quench`, `appraise`, `human-appraise`, or `assay`) and `alias` is a human-readable name for what that stage does in this cycle. The list is derived from the cycle and artefact type: `forge` and `appraise` are always included; `quench` is included iff any applicable law declares validators; `human-appraise` is included iff the cycle sets `always-human-appraise: true`; and `assay` is included iff the cycle declares an `assay.extractors` block.
|
|
50
49
|
- `max-iterations` — how many forge passes before the cycle is blocked (default: 3).
|
|
51
|
-
- `human-appraise` — run human-appraise every iteration (default: `false`).
|
|
52
|
-
- `deadlock-appraise` — route to human-appraise when
|
|
53
|
-
- `deadlock-iterations` — deadlock threshold (default: 5).
|
|
50
|
+
- `always-human-appraise` — run human-appraise every iteration (default: `false`).
|
|
51
|
+
- `deadlock-human-appraise` — route to human-appraise when the iteration count reaches `max-iterations` (default: `true`).
|
|
54
52
|
- `models` — optional per-stage model overrides; individual appraisers may further override via their own `model` field.
|
|
55
53
|
- `assay.extractors` — optional list of extractor names (defined under `foundry/memory/extractors/`) to run at iteration 0 before the first forge. Requires `foundry/memory/` to be initialized; cycle fails to load otherwise.
|
|
56
54
|
|
|
57
|
-
The `stages` list is the happy path. Sort follows it, loops back to `forge` when unresolved feedback demands it, and
|
|
55
|
+
The `stages` list is the happy path. Sort follows it, loops back to `forge` when unresolved feedback demands it, and routes to `human-appraise` when the iteration count reaches `max-iterations` (provided `deadlock-human-appraise` is `true`). If `assay` is configured, it runs once at iteration 0 before the route begins.
|
|
58
56
|
|
|
59
57
|
### Who sets what
|
|
60
58
|
|
|
61
59
|
- `flow`, `cycle` — set by the `flow` skill via `foundry_workfile_create` at flow start and updated as the flow advances between cycles.
|
|
62
60
|
- `goal` — written once by the `flow` skill when `WORK.md` is created.
|
|
63
|
-
- `stages`, `max-iterations`, `human-appraise`, `deadlock-appraise`, `
|
|
61
|
+
- `stages`, `max-iterations`, `always-human-appraise`, `deadlock-human-appraise`, `models`, `assay` — set by `foundry_orchestrate` on the first call of each cycle (via internal `workfile_configure_from_cycle`, reading the cycle definition).
|
|
62
|
+
- The frontmatter may also contain `status: failed` with a `reason` when the flow enters a failed state, locking all mutation tools.
|
|
64
63
|
|
|
65
64
|
## Sections
|
|
66
65
|
|
|
@@ -68,22 +67,6 @@ The `stages` list is the happy path. Sort follows it, loops back to `forge` when
|
|
|
68
67
|
|
|
69
68
|
Free text describing what the foundry flow is producing and any context the human provided. Written once at foundry flow start, not modified after.
|
|
70
69
|
|
|
71
|
-
### Artefacts
|
|
72
|
-
|
|
73
|
-
A table tracking every artefact produced by the foundry flow. The generator (`createWorkfile` in `src/scripts/lib/workfile.js`) writes the table immediately after the `# Goal` body — there is no `# Artefacts` heading. The orchestrator's internal finalise step appends rows for matching output files; authoring tools should not edit the artefacts table directly.
|
|
74
|
-
|
|
75
|
-
```markdown
|
|
76
|
-
| File | Type | Cycle | Status |
|
|
77
|
-
|------|------|-------|--------|
|
|
78
|
-
| petitions/login-change.md | petition | write-petition | draft |
|
|
79
|
-
| features/login-change.feature | gherkin | petition-to-gherkin | draft |
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Statuses:
|
|
83
|
-
- `draft` — artefact exists but has not cleared all stages
|
|
84
|
-
- `done` — artefact has cleared all stages
|
|
85
|
-
- `blocked` — artefact hit iteration limit or a violation
|
|
86
|
-
|
|
87
70
|
## WORK.feedback.yaml
|
|
88
71
|
|
|
89
72
|
The flow run owns one `WORK.feedback.yaml` file alongside `WORK.md` and
|
|
@@ -113,40 +96,48 @@ Each history snapshot:
|
|
|
113
96
|
|
|
114
97
|
| Field | Type | Required | Notes |
|
|
115
98
|
|-------|------|----------|-------|
|
|
116
|
-
| `state` | enum | yes | `open \| actioned \| wont-fix \| rejected \|
|
|
99
|
+
| `state` | enum | yes | `open \| actioned \| wont-fix \| rejected \| resolved` |
|
|
117
100
|
| `stage` | string (`base:alias`) or literal `sort` | yes | Who performed the transition |
|
|
118
101
|
| `cycle` | string | yes | Cycle id at the time of the transition |
|
|
119
102
|
| `timestamp` | ISO-8601 UTC with ms | yes | |
|
|
120
|
-
| `reason` | string | conditional | Required on `rejected`, `wont-fix
|
|
103
|
+
| `reason` | string | conditional | Required on `rejected`, `wont-fix`; forbidden on `open`; optional on `actioned`, `resolved` |
|
|
121
104
|
|
|
122
105
|
`history[0]` is always the current state; new snapshots are prepended.
|
|
123
106
|
`resolved` is terminal.
|
|
124
107
|
|
|
125
108
|
### State machine
|
|
126
109
|
|
|
127
|
-
Feedback items flow through a
|
|
110
|
+
Feedback items flow through a five-state lifecycle: `open` (newly raised), `actioned` (forge has addressed it), `wont-fix` (forge declined subjective feedback with justification), `rejected` (appraiser or human overruled the wont-fix), and `resolved` (approved by the item's originating stage or human override). Transitions are source-based: the legal moves depend on what stage created the item and who is trying to transition it. The feedback state machine is the engine that routes work between cycles.
|
|
128
111
|
|
|
129
|
-
The
|
|
112
|
+
The five states and the legal transitions are:
|
|
130
113
|
|
|
131
|
-
| From \ Caller | forge (any source) | source-stage (quench / appraise / human-appraise where stageId === item.source) |
|
|
132
|
-
|
|
133
|
-
| `open` | -> `actioned` always; -> `wont-fix` only if `item.source` base is `appraise` | — |
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `resolved` | — | — | — | — (terminal) |
|
|
114
|
+
| From \ Caller | forge (any source) | source-stage (quench / appraise / human-appraise where stageId === item.source) | human-appraise (override authority, any source) |
|
|
115
|
+
|---|---|---|---|
|
|
116
|
+
| `open` | -> `actioned` always; -> `wont-fix` only if `item.source` base is `appraise` | — | — |
|
|
117
|
+
| `actioned` | — | -> `{resolved, rejected}` | -> `{resolved, rejected}` |
|
|
118
|
+
| `wont-fix` | — | -> `{resolved, rejected}` | -> `{resolved, rejected}` |
|
|
119
|
+
| `rejected` | -> `actioned` always; -> `wont-fix` only if `item.source` base is `appraise` | — | — |
|
|
120
|
+
| `resolved` | — | — | — (terminal) |
|
|
139
121
|
|
|
140
122
|
Notes:
|
|
141
123
|
|
|
142
|
-
- `source-stage` column applies when the caller's stage id exactly matches `item.source` (e.g. `appraise:write-check` resolving an item it created). `human-appraise` override authority (last column) applies regardless of `item.source` and
|
|
124
|
+
- `source-stage` column applies when the caller's stage id exactly matches `item.source` (e.g. `appraise:write-check` resolving an item it created). `human-appraise` override authority (last column) applies regardless of `item.source` and operates only on items in `actioned` or `wont-fix` state (or `deadlocked` for backward compatibility with existing feedback files).
|
|
143
125
|
- **Forge `wont-fix` scope.** When `item.source` base is `quench` (objective validation failure) or `human-appraise` (direct user instruction), forge may not `wont-fix` — it must `actioned`. Only `appraise`-sourced items are wont-fix-able by forge. This replaces the earlier tag-based restriction on `validation` / `human` tags.
|
|
144
|
-
- `tag` is categorical and display-only. The state machine consults `source`, not tags
|
|
145
|
-
- **Reason required on** `rejected`, `wont-fix
|
|
146
|
-
- Sort
|
|
126
|
+
- `tag` is categorical and display-only. The state machine consults `source`, not tags.
|
|
127
|
+
- **Reason required on** `rejected`, `wont-fix`. **Forbidden on** `open`. **Optional on** `actioned`, `resolved`.
|
|
128
|
+
- Sort does not write `state: deadlocked`. Deadlock detection in sort uses iteration count comparison against `max-iterations` and `deadlock-human-appraise` to decide whether to route to human-appraise, not per-item deadlock state. The feedback-transition validator still recognises `deadlocked` state for backward compatibility with existing feedback files.
|
|
147
129
|
|
|
148
130
|
This section is the authoritative specification of the feedback state machine.
|
|
149
131
|
|
|
132
|
+
### Feedback tag gates
|
|
133
|
+
|
|
134
|
+
`foundry_feedback_add` enforces per-stage tag rules:
|
|
135
|
+
|
|
136
|
+
- **forge** and **assay** stages cannot add feedback; the tool returns an error.
|
|
137
|
+
- **quench** and **appraise** stages require tags starting with `law:`.
|
|
138
|
+
- **human-appraise** requires the tag `human`.
|
|
139
|
+
- Quench validator feedback uses the tag format `law:<law-id>:<validator-id>`.
|
|
140
|
+
|
|
150
141
|
### Transitions are made via the plugin API
|
|
151
142
|
|
|
152
143
|
No direct yaml editing. Every state change goes through one of:
|
|
@@ -166,10 +157,10 @@ A crash between the two steps leaves the live file untouched.
|
|
|
166
157
|
| Section | Written by | Updated by |
|
|
167
158
|
|---------|-----------|------------|
|
|
168
159
|
| Frontmatter (`flow`, `cycle`) | `foundry_workfile_create` (flow skill) | updated in place as the flow advances between cycles |
|
|
169
|
-
| Frontmatter (`stages`, `max-iterations`, `human-appraise`, `deadlock-appraise`, `
|
|
160
|
+
| Frontmatter (`stages`, `max-iterations`, `always-human-appraise`, `deadlock-human-appraise`, `models`, `assay`) | `foundry_orchestrate` (first call of each cycle, internally) | reset on each new cycle |
|
|
170
161
|
| Goal | `foundry_workfile_create` (flow skill) | nobody |
|
|
171
162
|
| Artefact files | forge stage writes files on disk | git tracks file changes; cycle-level state records completion or failure |
|
|
172
|
-
| `WORK.feedback.yaml` | `foundry_feedback_add` (`quench` / `appraise` / `human-appraise`) | `foundry_feedback_action` / `foundry_feedback_wontfix` (forge), `foundry_feedback_resolve` (source stage / human-appraise override)
|
|
163
|
+
| `WORK.feedback.yaml` | `foundry_feedback_add` (`quench` / `appraise` / `human-appraise`) | `foundry_feedback_action` / `foundry_feedback_wontfix` (forge), `foundry_feedback_resolve` (source stage / human-appraise override) |
|
|
173
164
|
| `WORK.history.yaml` | `foundry_orchestrate` | `foundry_orchestrate` |
|
|
174
165
|
|
|
175
166
|
Artefact files are discovered from branch changes matching the cycle output type's `file-patterns`.
|
|
@@ -231,7 +222,8 @@ A separate file (`WORK.history.yaml`) alongside WORK.md. Append-only log of ever
|
|
|
231
222
|
| `timestamp` | ISO-8601 UTC with ms | yes | |
|
|
232
223
|
| `seq` | integer | yes on write | Monotonic per file; sort tiebreaker for same-ms entries |
|
|
233
224
|
| `route` | string | conditional | Only on `stage: sort` entries; records the route decision. Throws if set on a non-sort entry |
|
|
234
|
-
| `open_feedback` | integer | yes on write | Count of non-resolved items in `WORK.feedback.yaml` at the time of write
|
|
225
|
+
| `open_feedback` | integer | yes on write | Count of non-resolved items in `WORK.feedback.yaml` at the time of write |
|
|
226
|
+
| `changed_files` | array of strings | conditional | Only on stage entries; records the list of files detected as changed by the finalise step for that stage |
|
|
235
227
|
|
|
236
228
|
### Rules
|
|
237
229
|
|
|
@@ -265,9 +257,8 @@ flow: make-haiku
|
|
|
265
257
|
cycle: haiku-creation
|
|
266
258
|
stages: [forge:write-haiku, quench:check-syllables, appraise:evaluate-quality]
|
|
267
259
|
max-iterations: 3
|
|
268
|
-
human-appraise: false
|
|
269
|
-
deadlock-appraise: true
|
|
270
|
-
deadlock-iterations: 5
|
|
260
|
+
always-human-appraise: false
|
|
261
|
+
deadlock-human-appraise: true
|
|
271
262
|
---
|
|
272
263
|
|
|
273
264
|
# Goal
|
|
@@ -275,9 +266,4 @@ deadlock-iterations: 5
|
|
|
275
266
|
Write a haiku about autumn rain. Should evoke loneliness
|
|
276
267
|
and the sound of rain on leaves.
|
|
277
268
|
|
|
278
|
-
| File | Type | Cycle | Status |
|
|
279
|
-
|------|------|-------|--------|
|
|
280
|
-
| petitions/autumn-rain-haiku.md | petition | haiku-ideation | done |
|
|
281
|
-
| haiku/autumn-rain.md | haiku | haiku-creation | draft |
|
|
282
|
-
|
|
283
269
|
```
|
|
@@ -52,14 +52,11 @@ function renderModels(models) {
|
|
|
52
52
|
/** Render boolean and numeric flags (camelCase to kebab-case). */
|
|
53
53
|
function renderFlags(args) {
|
|
54
54
|
let fm = '';
|
|
55
|
-
if (args.
|
|
56
|
-
fm += `human-appraise: ${args.
|
|
55
|
+
if (args.alwaysHumanAppraise !== undefined) {
|
|
56
|
+
fm += `always-human-appraise: ${args.alwaysHumanAppraise}\n`;
|
|
57
57
|
}
|
|
58
|
-
if (args.
|
|
59
|
-
fm += `deadlock-appraise: ${args.
|
|
60
|
-
}
|
|
61
|
-
if (args.deadlockIterations !== undefined) {
|
|
62
|
-
fm += `deadlock-iterations: ${args.deadlockIterations}\n`;
|
|
58
|
+
if (args.deadlockHumanAppraise !== undefined) {
|
|
59
|
+
fm += `deadlock-human-appraise: ${args.deadlockHumanAppraise}\n`;
|
|
63
60
|
}
|
|
64
61
|
if (args.maxIterations !== undefined) {
|
|
65
62
|
fm += `max-iterations: ${args.maxIterations}\n`;
|
|
@@ -76,9 +73,8 @@ function renderFlags(args) {
|
|
|
76
73
|
* @param {string} args.outputType Artefact type ID this cycle produces.
|
|
77
74
|
* @param {{ type: 'any-of'|'all-of', artefacts: string[] }} [args.inputs] Input contract.
|
|
78
75
|
* @param {string[]} [args.targets] Downstream cycle IDs.
|
|
79
|
-
* @param {boolean} [args.
|
|
80
|
-
* @param {boolean} [args.
|
|
81
|
-
* @param {number} [args.deadlockIterations] Iteration threshold for deadlock detection.
|
|
76
|
+
* @param {boolean} [args.alwaysHumanAppraise] Include human-appraise in every iteration.
|
|
77
|
+
* @param {boolean} [args.deadlockHumanAppraise] Route to human-appraise when max-iterations is reached.
|
|
82
78
|
* @param {number} [args.maxIterations] Maximum forge iterations.
|
|
83
79
|
* @param {{ extractors: string[] }} [args.assay] Assay stage config.
|
|
84
80
|
* @param {{ read: string[], write: string[] }} [args.memory] Flow memory permissions.
|
|
@@ -29,7 +29,6 @@ export async function validate({ name, body, io }) {
|
|
|
29
29
|
await checkOutputType(fm, io),
|
|
30
30
|
...await checkInputs(fm, io),
|
|
31
31
|
...await checkTargets(fm, io),
|
|
32
|
-
checkIterationLimits(fm),
|
|
33
32
|
].filter(Boolean);
|
|
34
33
|
|
|
35
34
|
return errors.length ? { ok: false, errors } : { ok: true };
|
|
@@ -131,11 +130,4 @@ async function validateCycleRefs(targets, io) {
|
|
|
131
130
|
return errors;
|
|
132
131
|
}
|
|
133
132
|
|
|
134
|
-
|
|
135
|
-
const maxIt = fm['max-iterations'];
|
|
136
|
-
const dlIt = fm['deadlock-iterations'];
|
|
137
|
-
if (maxIt !== undefined && dlIt !== undefined && dlIt > maxIt) {
|
|
138
|
-
return `deadlock-iterations (${dlIt}) must be <= max-iterations (${maxIt}); deadlock would never trigger before the cycle blocks`;
|
|
139
|
-
}
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
133
|
+
|
|
@@ -96,12 +96,6 @@ export function openFeedbackStore(path, io) {
|
|
|
96
96
|
timestamp: nowIso, persist,
|
|
97
97
|
});
|
|
98
98
|
},
|
|
99
|
-
writeDeadlockedSnapshotForTest(params) {
|
|
100
|
-
return storeWriteDeadlockedSnapshot(params, items, { timestamp: nowIso, persist });
|
|
101
|
-
},
|
|
102
|
-
writeDeadlockedSnapshots(ids, reason, stage, cycle) {
|
|
103
|
-
return storeWriteDeadlockedSnapshots({ ids, reason, stage, cycle }, items, { timestamp: nowIso, persist });
|
|
104
|
-
},
|
|
105
99
|
resolveSystemItems(stage, cycle) {
|
|
106
100
|
resolveSystemItemsImpl({ items, stage, cycle, timestamp: nowIso, persist });
|
|
107
101
|
},
|
|
@@ -153,7 +147,7 @@ function forgeWontFixError(item) {
|
|
|
153
147
|
|
|
154
148
|
function reasonRequiredForTransition(target, current, reason) {
|
|
155
149
|
const REASON_REQUIRED_TARGETS = new Set(['rejected', 'wont-fix']);
|
|
156
|
-
return
|
|
150
|
+
return REASON_REQUIRED_TARGETS.has(target)
|
|
157
151
|
&& (!reason || !reason.trim());
|
|
158
152
|
}
|
|
159
153
|
|
|
@@ -208,49 +202,4 @@ function storeTransition(params, items, deps) {
|
|
|
208
202
|
return { ok: true };
|
|
209
203
|
}
|
|
210
204
|
|
|
211
|
-
function storeWriteDeadlockedSnapshot(params, items, deps) {
|
|
212
|
-
const { id, cycle, reason } = params;
|
|
213
|
-
const { timestamp, persist } = deps;
|
|
214
|
-
|
|
215
|
-
const item = items.find(x => x.id === id);
|
|
216
|
-
if (!item) return { ok: false, error: `feedback item not found: ${id}` };
|
|
217
|
-
if (!reason || !reason.trim()) {
|
|
218
|
-
return { ok: false, error: 'reason is required for deadlocked snapshot' };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const snapshot = { state: 'deadlocked', stage: 'sort', cycle, timestamp: timestamp(), reason };
|
|
222
|
-
persist(items.map(it =>
|
|
223
|
-
it.id === id ? { ...it, history: [snapshot, ...it.history] } : it
|
|
224
|
-
));
|
|
225
|
-
return { ok: true };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function assertDeadlockedSnapshotArgs(ids, reason) {
|
|
229
|
-
if (!Array.isArray(ids)) return { ok: false, error: 'ids must be an array' };
|
|
230
|
-
if (ids.length === 0) return { ok: true };
|
|
231
|
-
if (!reason) return { ok: false, error: 'reason is required for deadlocked snapshot' };
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function storeWriteDeadlockedSnapshots(params, items, deps) {
|
|
236
|
-
const { ids, reason, stage, cycle } = params;
|
|
237
|
-
const { timestamp, persist } = deps;
|
|
238
|
-
|
|
239
|
-
const precheck = assertDeadlockedSnapshotArgs(ids, reason);
|
|
240
|
-
if (precheck) return precheck;
|
|
241
205
|
|
|
242
|
-
const ts = timestamp();
|
|
243
|
-
const idSet = new Set(ids);
|
|
244
|
-
const nextItems = items.map(it => {
|
|
245
|
-
if (!idSet.has(it.id)) return it;
|
|
246
|
-
return { ...it, history: [{ state: 'deadlocked', stage, cycle, timestamp: ts, reason }, ...it.history] };
|
|
247
|
-
});
|
|
248
|
-
for (const id of ids) {
|
|
249
|
-
if (!items.some(it => it.id === id)) {
|
|
250
|
-
return { ok: false, error: `feedback item(s) not found: ${id}` };
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
persist(nextItems);
|
|
255
|
-
return { ok: true };
|
|
256
|
-
}
|
|
@@ -21,15 +21,14 @@ function buildReasonData(route, prep) {
|
|
|
21
21
|
const forgeCount = prep.history.filter(e => baseStage(e.stage || '') === 'forge').length;
|
|
22
22
|
const maxIt = prep.defaults.maxIterations;
|
|
23
23
|
const feedback = prep.feedback || [];
|
|
24
|
-
const openCount = feedback.filter(
|
|
25
|
-
f => f.state !== 'resolved' && f.state !== 'deadlocked',
|
|
26
|
-
).length;
|
|
27
|
-
const dlCount = feedback.filter(f => f.state === 'deadlocked').length;
|
|
24
|
+
const openCount = feedback.filter(f => f.state !== 'resolved').length;
|
|
28
25
|
const needingForge = feedback.filter(
|
|
29
26
|
f => f.state === 'open' || f.state === 'rejected',
|
|
30
27
|
).length;
|
|
28
|
+
const alwaysHumanAppraise = prep.defaults.alwaysHumanAppraise;
|
|
29
|
+
const deadlockHumanAppraise = prep.defaults.deadlockHumanAppraise;
|
|
31
30
|
|
|
32
|
-
return { base, route, forgeCount, maxIt, openCount,
|
|
31
|
+
return { base, route, forgeCount, maxIt, openCount, needingForge, alwaysHumanAppraise, deadlockHumanAppraise };
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
function forgeReason(d) {
|
|
@@ -38,12 +37,14 @@ function forgeReason(d) {
|
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
function appraiseReason(d) {
|
|
41
|
-
if (d.anyDeadlocked) return `${d.dlCount} feedback item(s) deadlocked — routing to appraise for re-evaluation`;
|
|
42
40
|
return `quench passed with ${d.openCount} open feedback item(s) — routing to appraise`;
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
function humanAppraiseReason(d) {
|
|
46
|
-
|
|
44
|
+
if (d.alwaysHumanAppraise) {
|
|
45
|
+
return `always-human-appraise enabled — routing to human after ${d.forgeCount} forge iteration(s)`;
|
|
46
|
+
}
|
|
47
|
+
return `max iterations (${d.maxIt}) reached after ${d.forgeCount} forge iteration(s) — routing to human for review`;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
function blockedReason(d) {
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* configured `max-lines` limit and to lower per-function complexity.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
// '
|
|
11
|
-
//
|
|
9
|
+
// An item is "open" (still in flight) when its head state is not
|
|
10
|
+
// 'resolved' or 'deadlocked'. The deadlocked check is retained for
|
|
11
|
+
// backward compatibility with existing feedback files.
|
|
12
12
|
const isOpenItem = (f) => f.state !== 'resolved' && f.state !== 'deadlocked';
|
|
13
13
|
|
|
14
14
|
export { isOpenItem };
|
|
@@ -40,36 +40,90 @@ function hasItemsPendingApproval(openItems) {
|
|
|
40
40
|
return openItems.some(f => f.state === 'actioned' || f.state === 'wont-fix');
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
return
|
|
43
|
+
function firstForgeOrBlocked(stages) {
|
|
44
|
+
const stage = findFirst(stages, 'forge');
|
|
45
|
+
return stage !== null ? stage : 'blocked';
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function
|
|
48
|
+
function nextRouteOrDone(stages, current) {
|
|
49
|
+
const nextStage = nextInRoute(stages, current);
|
|
50
|
+
return nextStage !== null ? nextStage : 'done';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function firstHumanOrSuffixed(stages, cycle) {
|
|
54
|
+
const stage = findFirst(stages, 'human-appraise');
|
|
55
|
+
return stage !== null ? stage : `human-appraise:${cycle}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isBelowIterationCap(forgeCount, maxIterations, alwaysHumanAppraise) {
|
|
59
|
+
return alwaysHumanAppraise || forgeCount < maxIterations;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function callHandlerOrBlocked(handler) {
|
|
63
|
+
return handler ? handler() : 'blocked';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function routeForgeIfNeeded(stages, forgeCount, maxIterations, opts = {}) {
|
|
67
|
+
const { alwaysHumanAppraise, deadlockHumanAppraise, cycle } = opts;
|
|
68
|
+
if (isBelowIterationCap(forgeCount, maxIterations, alwaysHumanAppraise)) {
|
|
69
|
+
return firstForgeOrBlocked(stages);
|
|
70
|
+
}
|
|
71
|
+
if (!deadlockHumanAppraise) return 'blocked';
|
|
72
|
+
if (!findFirst(stages, 'human-appraise')) return 'blocked';
|
|
73
|
+
return `human-appraise:${cycle}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function appraiseForgeOrApproval(stages, openItems, forgeCount, maxIterations, opts = {}) {
|
|
49
77
|
if (hasItemsNeedingForge(openItems)) {
|
|
50
|
-
return routeForgeIfNeeded(stages, forgeCount, maxIterations);
|
|
78
|
+
return routeForgeIfNeeded(stages, forgeCount, maxIterations, opts);
|
|
51
79
|
}
|
|
52
80
|
if (hasItemsPendingApproval(openItems)) {
|
|
53
|
-
|
|
81
|
+
const stage = findFirst(stages, 'appraise');
|
|
82
|
+
return stage !== null ? stage : 'blocked';
|
|
54
83
|
}
|
|
55
84
|
return null;
|
|
56
85
|
}
|
|
57
86
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
87
|
+
function routeAlwaysHumanAppraise(stages, current, openItems, cycle) {
|
|
88
|
+
if (openItems.length > 0) {
|
|
89
|
+
return firstHumanOrSuffixed(stages, cycle);
|
|
90
|
+
}
|
|
91
|
+
return nextRouteOrDone(stages, current);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function decideAppraiseRoute(opts) {
|
|
95
|
+
const {
|
|
96
|
+
stages, current, openItems, forgeCount, maxIterations,
|
|
97
|
+
alwaysHumanAppraise, deadlockHumanAppraise, cycle,
|
|
98
|
+
} = opts;
|
|
99
|
+
const decided = appraiseForgeOrApproval(
|
|
100
|
+
stages, openItems, forgeCount, maxIterations,
|
|
101
|
+
{ alwaysHumanAppraise, deadlockHumanAppraise, cycle },
|
|
102
|
+
);
|
|
64
103
|
if (decided !== null) return decided;
|
|
65
|
-
return
|
|
104
|
+
return nextRouteOrDone(stages, current);
|
|
66
105
|
}
|
|
67
106
|
|
|
68
|
-
export function
|
|
107
|
+
export function nextAfterAppraise({
|
|
108
|
+
stages, current, feedback, forgeCount, maxIterations,
|
|
109
|
+
alwaysHumanAppraise = false, deadlockHumanAppraise = false, cycle = 'default',
|
|
110
|
+
}) {
|
|
69
111
|
const openItems = feedback.filter(isOpenItem);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
112
|
+
if (alwaysHumanAppraise) {
|
|
113
|
+
return routeAlwaysHumanAppraise(stages, current, openItems, cycle);
|
|
114
|
+
}
|
|
115
|
+
return decideAppraiseRoute({
|
|
116
|
+
stages, current, openItems, forgeCount, maxIterations,
|
|
117
|
+
alwaysHumanAppraise, deadlockHumanAppraise, cycle,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function nextAfterQuench(stages, current, feedback, opts = {}) {
|
|
122
|
+
const { forgeCount = 0, maxIterations = 100 } = opts;
|
|
123
|
+
const openItems = feedback.filter(isOpenItem);
|
|
124
|
+
const needsForge = hasItemsNeedingForge(openItems);
|
|
125
|
+
if (needsForge) return routeForgeIfNeeded(stages, forgeCount, maxIterations, opts);
|
|
126
|
+
return nextRouteOrDone(stages, current);
|
|
73
127
|
}
|
|
74
128
|
|
|
75
129
|
function lastNonSortStage(history) {
|
|
@@ -78,26 +132,50 @@ function lastNonSortStage(history) {
|
|
|
78
132
|
return nonSort[nonSort.length - 1].stage;
|
|
79
133
|
}
|
|
80
134
|
|
|
81
|
-
function buildRouteHandlers({
|
|
135
|
+
function buildRouteHandlers({
|
|
136
|
+
stages, lastEntry, feedback, forgeCount, maxIterations,
|
|
137
|
+
alwaysHumanAppraise, deadlockHumanAppraise, cycle,
|
|
138
|
+
}) {
|
|
82
139
|
const appraiseRoute = () => nextAfterAppraise({
|
|
83
140
|
stages, current: lastEntry, feedback, forgeCount, maxIterations,
|
|
141
|
+
alwaysHumanAppraise, deadlockHumanAppraise, cycle,
|
|
84
142
|
});
|
|
85
143
|
return {
|
|
86
|
-
'assay': () =>
|
|
87
|
-
'forge': () =>
|
|
88
|
-
'quench': () => nextAfterQuench(
|
|
144
|
+
'assay': () => firstForgeOrBlocked(stages),
|
|
145
|
+
'forge': () => nextRouteOrDone(stages, lastEntry),
|
|
146
|
+
'quench': () => nextAfterQuench(
|
|
147
|
+
stages, lastEntry, feedback,
|
|
148
|
+
{ forgeCount, maxIterations, alwaysHumanAppraise, deadlockHumanAppraise, cycle },
|
|
149
|
+
),
|
|
89
150
|
'appraise': appraiseRoute,
|
|
90
151
|
'human-appraise': appraiseRoute,
|
|
91
152
|
};
|
|
92
153
|
}
|
|
93
154
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
155
|
+
function countForgeIterations(history) {
|
|
156
|
+
return history.filter(e => baseStage(e.stage || '') === 'forge').length;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function routeFromLastEntry(opts) {
|
|
160
|
+
const {
|
|
161
|
+
stages, lastEntry, feedback, forgeCount, maxIterations,
|
|
162
|
+
alwaysHumanAppraise, deadlockHumanAppraise, cycle,
|
|
163
|
+
} = opts;
|
|
97
164
|
if (lastEntry === null) return stages[0];
|
|
98
165
|
const handlers = buildRouteHandlers({
|
|
99
166
|
stages, lastEntry, feedback, forgeCount, maxIterations,
|
|
167
|
+
alwaysHumanAppraise, deadlockHumanAppraise, cycle,
|
|
100
168
|
});
|
|
101
169
|
const handler = handlers[baseStage(lastEntry)];
|
|
102
|
-
return handler
|
|
170
|
+
return callHandlerOrBlocked(handler);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function determineRoute(stages, history, feedback, maxIterations, opts = {}) {
|
|
174
|
+
const { alwaysHumanAppraise = false, deadlockHumanAppraise = false, cycle = 'default' } = opts;
|
|
175
|
+
const forgeCount = countForgeIterations(history);
|
|
176
|
+
const lastEntry = lastNonSortStage(history);
|
|
177
|
+
return routeFromLastEntry({
|
|
178
|
+
stages, lastEntry, feedback, forgeCount, maxIterations,
|
|
179
|
+
alwaysHumanAppraise, deadlockHumanAppraise, cycle,
|
|
180
|
+
});
|
|
103
181
|
}
|
|
@@ -18,4 +18,8 @@ function resolveOpenCode() {
|
|
|
18
18
|
return process.env.FOUNDRY_OPENCODE_PATH || resolveFromPath('opencode');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
function resolvePnpm() {
|
|
22
|
+
return process.env.FOUNDRY_PNPM_PATH || resolveFromPath('pnpm');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { resolveGit, resolveOpenCode, resolvePnpm };
|
|
@@ -150,13 +150,13 @@ export function tryCommit(git, message, allowedPatterns, phase) {
|
|
|
150
150
|
// Stage synthesis (pure utility, used by setupWorkfile and exported publicly).
|
|
151
151
|
// ---------------------------------------------------------------------------
|
|
152
152
|
|
|
153
|
-
export function synthesizeStages({ cycleId, hasValidation,
|
|
153
|
+
export function synthesizeStages({ cycleId, hasValidation, alwaysHumanAppraise, assay = false }) {
|
|
154
154
|
const stages = [];
|
|
155
155
|
if (assay) stages.push(`assay:${cycleId}`);
|
|
156
156
|
stages.push(`forge:${cycleId}`);
|
|
157
157
|
if (hasValidation) stages.push(`quench:${cycleId}`);
|
|
158
158
|
stages.push(`appraise:${cycleId}`);
|
|
159
|
-
if (
|
|
159
|
+
if (alwaysHumanAppraise) stages.push(`human-appraise:${cycleId}`);
|
|
160
160
|
return stages;
|
|
161
161
|
}
|
|
162
162
|
|
|
@@ -273,14 +273,4 @@ export function renderDispatchPrompt({ stage, cycle, token, cwd, filePatterns, o
|
|
|
273
273
|
return lines.join('\n');
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
|
|
277
|
-
const maxIt = cfm['max-iterations'];
|
|
278
|
-
const dlIt = cfm['deadlock-iterations'];
|
|
279
|
-
if (maxIt !== undefined && dlIt !== undefined && dlIt > maxIt) {
|
|
280
|
-
return violation(
|
|
281
|
-
`cycle ${cycleId}: deadlock-iterations (${dlIt}) cannot exceed max-iterations (${maxIt})`,
|
|
282
|
-
['WORK.md'],
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
return null;
|
|
286
|
-
}
|
|
276
|
+
|
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
tryCommit,
|
|
22
22
|
synthesizeStages,
|
|
23
23
|
renderDispatchPrompt,
|
|
24
|
-
checkIterationLimits,
|
|
25
24
|
} from './orchestrate-cycle.js';
|
|
26
25
|
import {
|
|
27
26
|
doneAction,
|
|
@@ -157,7 +156,7 @@ function stageTag(s, cycleId) {
|
|
|
157
156
|
|
|
158
157
|
function resolveStages(cfm, cycleId, hasValidation, assayExtractors) {
|
|
159
158
|
if (!Array.isArray(cfm.stages)) {
|
|
160
|
-
return synthesizeStages({ cycleId, hasValidation,
|
|
159
|
+
return synthesizeStages({ cycleId, hasValidation, alwaysHumanAppraise: cfm['always-human-appraise'] === true, assay: !!assayExtractors });
|
|
161
160
|
}
|
|
162
161
|
if (cfm.stages.length === 0) {
|
|
163
162
|
return { error: violation(`cycle ${cycleId} has no stages declared in cycle definition`, ['WORK.md']) };
|
|
@@ -168,9 +167,8 @@ function resolveStages(cfm, cycleId, hasValidation, assayExtractors) {
|
|
|
168
167
|
function applyFmDefaults(newFm, cfm, assayExtractors) {
|
|
169
168
|
const maxIt = cfm['max-iterations'] ?? 3;
|
|
170
169
|
newFm['max-iterations'] = maxIt;
|
|
171
|
-
newFm['human-appraise'] = cfm['human-appraise'] === true;
|
|
172
|
-
newFm['deadlock-appraise'] = cfm['deadlock-appraise'] !== false;
|
|
173
|
-
newFm['deadlock-iterations'] = cfm['deadlock-iterations'] ?? maxIt;
|
|
170
|
+
newFm['always-human-appraise'] = cfm['always-human-appraise'] === true;
|
|
171
|
+
newFm['deadlock-human-appraise'] = cfm['deadlock-human-appraise'] !== false;
|
|
174
172
|
if (cfm.models) newFm.models = cfm.models;
|
|
175
173
|
if (assayExtractors) newFm.assay = { extractors: assayExtractors };
|
|
176
174
|
}
|
|
@@ -226,8 +224,6 @@ async function completeSetup(ctx) {
|
|
|
226
224
|
const hasValidation = ctx.lawsWithValidators && ctx.lawsWithValidators.length > 0;
|
|
227
225
|
const stagesResult = resolveStages(ctx.cfm, ctx.cycleId, hasValidation, ctx.assayResult.extractors);
|
|
228
226
|
if (stagesResult.error) return stagesResult.error;
|
|
229
|
-
const validityErr = checkIterationLimits(ctx.cfm, ctx.cycleId);
|
|
230
|
-
if (validityErr) return validityErr;
|
|
231
227
|
const newWork = buildNewFrontmatter(ctx.workContent, stagesResult, ctx.cfm, ctx.assayResult.extractors);
|
|
232
228
|
ctx.io.writeFile('WORK.md', newWork);
|
|
233
229
|
return trySetupCommit(ctx);
|