@really-knows-ai/foundry 2.0.1 → 2.2.1

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/scripts/sort.js CHANGED
@@ -64,7 +64,7 @@ const defaultIO = {
64
64
  // Routing logic
65
65
  // ---------------------------------------------------------------------------
66
66
 
67
- function determineRoute(stages, history, feedback, maxIterations) {
67
+ function determineRoute(stages, history, feedback, maxIterations, opts = {}) {
68
68
  const forgeCount = history.filter(e => baseStage(e.stage || '') === 'forge').length;
69
69
 
70
70
  const nonSortHistory = history.filter(e => baseStage(e.stage || '') !== 'sort');
@@ -83,11 +83,11 @@ function determineRoute(stages, history, feedback, maxIterations) {
83
83
  }
84
84
 
85
85
  if (lastBase === 'appraise') {
86
- return nextAfterAppraise(stages, lastEntry, feedback, forgeCount, maxIterations, nonSortHistory);
86
+ return nextAfterAppraise(stages, lastEntry, feedback, forgeCount, maxIterations, nonSortHistory, opts);
87
87
  }
88
88
 
89
89
  if (lastBase === 'human-appraise') {
90
- return nextAfterAppraise(stages, lastEntry, feedback, forgeCount, maxIterations, nonSortHistory);
90
+ return nextAfterAppraise(stages, lastEntry, feedback, forgeCount, maxIterations, nonSortHistory, opts);
91
91
  }
92
92
 
93
93
  return 'blocked';
@@ -103,16 +103,31 @@ function nextAfterQuench(stages, current, feedback, forgeCount, maxIterations) {
103
103
  return nextInRoute(stages, current) ?? 'done';
104
104
  }
105
105
 
106
- function nextAfterAppraise(stages, current, feedback, forgeCount, maxIterations, history = []) {
107
- // Check for deadlock escalation
108
- const deadlocked = detectDeadlocks(feedback, history);
106
+ function nextAfterAppraise(stages, current, feedback, forgeCount, maxIterations, history = [], opts = {}) {
107
+ const {
108
+ humanAppraise: humanAppraiseEnabled = false,
109
+ deadlockAppraise = true,
110
+ deadlockIterations = 5,
111
+ cycle = null,
112
+ } = opts;
113
+
114
+ // Check for deadlock escalation using configured threshold
115
+ const deadlocked = detectDeadlocks(feedback, history, deadlockIterations);
109
116
  if (deadlocked.length > 0) {
110
- const humanAppraise = findFirst(stages, 'human-appraise');
111
- if (humanAppraise && baseStage(current) !== 'human-appraise') {
112
- return humanAppraise;
117
+ const alreadyInHumanAppraise = baseStage(current) === 'human-appraise';
118
+ if (alreadyInHumanAppraise) {
119
+ // Human-appraise ran and deadlock still present — give up.
120
+ return 'blocked';
113
121
  }
114
- // Human-appraise not available or we're already in it — blocked
115
- if (forgeCount >= maxIterations) return 'blocked';
122
+ if (deadlockAppraise) {
123
+ // Route to human-appraise. Prefer one in `stages`; else synthesize via cycle id.
124
+ const inStages = findFirst(stages, 'human-appraise');
125
+ if (inStages) return inStages;
126
+ if (cycle) return `human-appraise:${cycle}`;
127
+ return 'blocked';
128
+ }
129
+ // deadlock-appraise disabled — block the cycle.
130
+ return 'blocked';
116
131
  }
117
132
 
118
133
  const needsForge = feedback.some(f => f.state === 'open' || f.state === 'rejected');
@@ -205,11 +220,43 @@ function checkModifiedFiles(lastBase, foundryDir, cycleDef, cycle, io = defaultI
205
220
  return { ok: violations.length === 0, violations };
206
221
  }
207
222
 
223
+ // ---------------------------------------------------------------------------
224
+ // Micro-commit enforcement
225
+ // ---------------------------------------------------------------------------
226
+
227
+ /**
228
+ * Return a list of tool-managed files that have uncommitted changes
229
+ * (modified, staged, or untracked) in the working tree.
230
+ *
231
+ * Tool-managed files are WORK.md, WORK.history.yaml, and anything under
232
+ * .foundry/. The sort skill is the sole writer of these between stages,
233
+ * and every stage must end with `foundry_git_commit`. If this function
234
+ * returns a non-empty list at the start of a sort invocation, a prior
235
+ * stage skipped the commit step.
236
+ */
237
+ function getDirtyToolManagedFiles(io = defaultIO) {
238
+ try {
239
+ const output = io.exec('git status --porcelain -- WORK.md WORK.history.yaml .foundry');
240
+ return output
241
+ .split('\n')
242
+ .map(line => line.trim())
243
+ .filter(Boolean)
244
+ .map(line => line.replace(/^[\sMADRCU?!]+/, '').trim())
245
+ .filter(Boolean);
246
+ } catch {
247
+ return [];
248
+ }
249
+ }
250
+
208
251
  // ---------------------------------------------------------------------------
209
252
  // Exported runSort — structured result for programmatic use
210
253
  // ---------------------------------------------------------------------------
211
254
 
212
- export function runSort({ workPath = 'WORK.md', historyPath = 'WORK.history.yaml', foundryDir = 'foundry', cycleDef } = {}, io = defaultIO) {
255
+ function isDispatchableRoute(route) {
256
+ return typeof route === 'string' && /^(forge|quench|appraise|human-appraise):/.test(route);
257
+ }
258
+
259
+ export function runSort({ workPath = 'WORK.md', historyPath = 'WORK.history.yaml', foundryDir = 'foundry', cycleDef, agentsDir = '.opencode/agents', mint, now = Date.now() } = {}, io = defaultIO) {
213
260
  if (!io.exists(workPath)) {
214
261
  return { route: 'blocked', details: 'WORK.md not found' };
215
262
  }
@@ -220,6 +267,9 @@ export function runSort({ workPath = 'WORK.md', historyPath = 'WORK.history.yaml
220
267
  const cycle = frontmatter.cycle;
221
268
  const stages = frontmatter.stages;
222
269
  const maxIterations = frontmatter['max-iterations'] ?? 3;
270
+ const humanAppraiseEnabled = frontmatter['human-appraise'] === true;
271
+ const deadlockAppraise = frontmatter['deadlock-appraise'] !== false; // default true
272
+ const deadlockIterations = frontmatter['deadlock-iterations'] ?? 5;
223
273
 
224
274
  if (!cycle) return { route: 'blocked', details: 'No cycle in WORK.md frontmatter' };
225
275
  if (!stages || !Array.isArray(stages)) return { route: 'blocked', details: 'No stages in WORK.md frontmatter' };
@@ -229,6 +279,20 @@ export function runSort({ workPath = 'WORK.md', historyPath = 'WORK.history.yaml
229
279
  const history = loadHistory(historyPath, cycle, io);
230
280
  const feedback = parseFeedback(workText, cycle, artefacts);
231
281
 
282
+ // Micro-commit enforcement: if any prior stage ran (history non-empty),
283
+ // all tool-managed files must be committed before the next sort call.
284
+ // The first sort of a cycle has empty history — WORK.md may be untracked
285
+ // or dirty at that point, which is fine.
286
+ if (history.length > 0) {
287
+ const dirty = getDirtyToolManagedFiles(io);
288
+ if (dirty.length > 0) {
289
+ return {
290
+ route: 'violation',
291
+ details: `Uncommitted tool-managed files since last sort: ${dirty.join(', ')}. Call foundry_git_commit for the prior stage before invoking sort again.`,
292
+ };
293
+ }
294
+ }
295
+
232
296
  // File modification enforcement
233
297
  const nonSortHistory = history.filter(e => baseStage(e.stage || '') !== 'sort');
234
298
  if (nonSortHistory.length > 0) {
@@ -248,17 +312,36 @@ export function runSort({ workPath = 'WORK.md', historyPath = 'WORK.history.yaml
248
312
  return { route: 'violation', details: `Feedback tag validation failed: ${details}` };
249
313
  }
250
314
 
251
- const route = determineRoute(stages, history, feedback, maxIterations);
315
+ const route = determineRoute(stages, history, feedback, maxIterations, {
316
+ humanAppraise: humanAppraiseEnabled,
317
+ deadlockAppraise,
318
+ deadlockIterations,
319
+ cycle,
320
+ });
252
321
 
253
322
  // Model resolution
254
323
  let model = null;
255
324
  const routeBase = baseStage(route);
256
325
  if (frontmatter.models && frontmatter.models[routeBase]) {
257
326
  const modelId = frontmatter.models[routeBase];
258
- model = `foundry-${modelId.replace(/\//g, '-')}`;
327
+ model = `foundry-${modelId.replace(/[/.]/g, '-')}`;
328
+
329
+ // Fail-fast: required subagent file must exist
330
+ const agentPath = `${agentsDir}/${model}.md`;
331
+ if (!io.exists(agentPath)) {
332
+ return {
333
+ route: 'violation',
334
+ details: `Missing required subagent: ${model}.md is not present in ${agentsDir}/. Run the refresh-agents skill to regenerate agent files, then restart.`,
335
+ };
336
+ }
259
337
  }
260
338
 
261
- return { route, ...(model ? { model } : {}) };
339
+ const result = { route, ...(model ? { model } : {}) };
340
+ if (mint && isDispatchableRoute(route)) {
341
+ const token = mint({ route, cycle, exp: now + 10 * 60 * 1000 });
342
+ if (token) result.token = token;
343
+ }
344
+ return result;
262
345
  }
263
346
 
264
347
  // ---------------------------------------------------------------------------
@@ -281,6 +364,7 @@ export {
281
364
  getModifiedFiles,
282
365
  getAllowedPatterns,
283
366
  checkModifiedFiles,
367
+ getDirtyToolManagedFiles,
284
368
  };
285
369
 
286
370
 
@@ -54,10 +54,15 @@ Only stages with an explicitly specified model are included in the `models` fron
54
54
 
55
55
  Ask the user:
56
56
 
57
- > Do you want a human quality gate on this cycle? If enabled, a human reviewer will check the artefact after LLM appraisers pass, and can break deadlocks between forge and appraisers.
57
+ > Human-appraise has two independent knobs:
58
58
  >
59
- > - Enable human-appraise? (yes/no)
60
- > - If yes, deadlock threshold (default: 3 number of forge/appraise iterations before escalating to human)
59
+ > 1. `human-appraise` should a human review the artefact every iteration? Default: no.
60
+ > 2. `deadlock-appraise` should a human be pulled in only when LLM appraisers deadlock? Default: yes.
61
+ > 3. If either is enabled, `deadlock-iterations` sets the deadlock threshold (default: 5).
62
+ >
63
+ > - human-appraise: yes/no (default no)
64
+ > - deadlock-appraise: yes/no (default yes)
65
+ > - deadlock-iterations: number (default 5)
61
66
 
62
67
  ### 5. Validate artefact types
63
68
 
@@ -108,9 +113,9 @@ inputs:
108
113
  - <artefact-type-id>
109
114
  targets:
110
115
  - <cycle-id>
111
- human-appraise:
112
- enabled: <true|false>
113
- deadlock-threshold: <number>
116
+ human-appraise: <true|false>
117
+ deadlock-appraise: <true|false>
118
+ deadlock-iterations: <number>
114
119
  models:
115
120
  appraise: <model-id>
116
121
  ---
@@ -14,34 +14,48 @@ Before running this skill, verify that the `foundry/` directory exists in the pr
14
14
 
15
15
  > Foundry is not initialized in this project. Run the `init-foundry` skill first to create the foundry/ directory structure.
16
16
 
17
+ ## Stage lifecycle (mandatory)
18
+
19
+ Appraise runs inside an enforced stage. Your **first** and **last** tool calls are fixed:
20
+
21
+ 1. **First:** `foundry_stage_begin({stage, cycle, token})` — copy the token verbatim from the dispatch prompt.
22
+ 2. **Last:** `foundry_stage_end({summary})`.
23
+
24
+ Appraise makes **no disk writes**. All output flows through `foundry_feedback_add`. `foundry_stage_finalize` flags any unexpected writes as a violation.
25
+
17
26
  ## Protocol
18
27
 
19
- 1. Gather context:
20
- - Call `foundry_workfile_get` — identify the artefact to appraise and its type
21
- - Call `foundry_config_laws` — get all applicable laws (global + type-specific)
22
- - Call `foundry_config_artefact_type` with the type ID get the artefact type definition
23
- - Call `foundry_appraisers_select` with the type ID returns selected appraiser personalities with their raw model IDs
28
+ 1. `foundry_stage_begin(...)`.
29
+ 2. Gather context:
30
+ - `foundry_workfile_get` — read the `cycle` from frontmatter
31
+ - `foundry_artefacts_list({cycle: <current-cycle>})` enumerate this cycle's artefacts. Always pass the `cycle` filter; omitting it returns stale rows from prior sessions. Skip rows whose status is `done` or `blocked`.
32
+ - For each remaining row, gather its type-specific context:
33
+ - `foundry_config_laws` with the row's type — applicable laws (global + type-specific)
34
+ - `foundry_config_artefact_type` with the type ID — the artefact type definition
35
+ - `foundry_appraisers_select` with the type ID — selected appraiser personalities with their raw model IDs
24
36
 
25
- 2. Dispatch each appraiser as an independent sub-agent (see Dispatch below)
37
+ 3. Dispatch each appraiser as an independent sub-agent (see Dispatch below). If this cycle produced multiple artefacts, appraisers evaluate each.
26
38
 
27
- 3. Collect results from all appraisers
39
+ 4. Collect results from all appraisers
28
40
 
29
- 4. Consolidate (this is judgment):
41
+ 5. Consolidate (this is judgment):
30
42
  - Union of all issues — if any one appraiser flags it, it's feedback
31
43
  - De-duplicate: merge overlapping observations into a single feedback item
32
44
  - Preserve which appraiser(s) raised each issue (for traceability)
33
45
 
34
- 5. For each consolidated issue: call `foundry_feedback_add` with the artefact file path, the issue description, and tag `law:<law-id>`
46
+ 6. For each consolidated issue: `foundry_feedback_add(file, text, tag: 'law:<law-id>')`. Tag MUST start with `law:` — the tool rejects other tags during appraise. The tool also de-duplicates by text-hash.
47
+
48
+ 7. If no appraiser found any issues, the artefact clears appraisal.
35
49
 
36
- 6. If no appraiser found any issues, the artefact clears appraisal
50
+ 8. `foundry_stage_end({summary})`.
37
51
 
38
52
  ## Reviewing actioned and wont-fix feedback
39
53
 
40
54
  On subsequent passes, review previously actioned and wont-fix items:
41
55
 
42
- 1. Call `foundry_feedback_list` to find `actioned` and `wontfix` items for this artefact
43
- 2. For each item, the appraiser sub-agents evaluate whether the change addresses the issue (actioned) or the justification is sound (wont-fix)
44
- 3. Call `foundry_feedback_resolve` with disposition `"approved"` or `"rejected"` (with reason) for each
56
+ 1. `foundry_feedback_list` find `actioned` and `wont-fix` items for this artefact.
57
+ 2. Appraiser sub-agents evaluate whether the change addresses the issue (`actioned`) or the justification is sound (`wont-fix`).
58
+ 3. `foundry_feedback_resolve(file, index, resolution: 'approved'|'rejected', reason?)`. Appraise is the only stage (other than human-appraise) allowed to resolve `wont-fix` items.
45
59
 
46
60
  ## Dispatch
47
61
 
@@ -91,7 +105,7 @@ If there are no issues, return an empty list.
91
105
 
92
106
  ## History
93
107
 
94
- Do NOT call `foundry_history_append` — the sort skill (your caller) is responsible for writing history. Instead, return a clear summary of what you found (e.g., "3 issues found across 2 appraisers" or "No issues found") so sort can log it.
108
+ Do NOT call `foundry_history_append` or `foundry_git_commit` — the sort skill handles those. Return a summary via `foundry_stage_end` (e.g., "3 issues found across 2 appraisers" or "No issues found").
95
109
 
96
110
  ### Human override awareness
97
111
 
@@ -99,6 +113,8 @@ When reviewing an artefact, check the feedback history for `#human` tagged items
99
113
 
100
114
  ## What you do NOT do
101
115
 
102
- - You do not revise the artefact
103
- - You do not check deterministic rules — that is the quench skill's job
104
- - You do not filter out feedback because only one appraiser raised it — one is enough
116
+ - You do not write files — all output goes through `foundry_feedback_add`.
117
+ - You do not revise the artefact.
118
+ - You do not check deterministic rules that is the quench skill's job.
119
+ - You do not filter out feedback because only one appraiser raised it — one is enough.
120
+ - You do not register artefacts — that happens automatically via `foundry_stage_finalize`.
@@ -22,38 +22,42 @@ Before running this skill, verify that the `foundry/` directory exists in the pr
22
22
  3. Determine the stage route:
23
23
  - Use the cycle definition's `stages` field if present
24
24
  - Otherwise generate defaults: always `forge`, add `quench` if `foundry_config_validation` returns non-null for the type, always `appraise`
25
- - If the cycle definition has `human-appraise.enabled: true`, append `human-appraise` as the final stage
25
+ - If the cycle definition has `human-appraise: true`, append `human-appraise` as the final stage (runs every iteration). If `human-appraise: false` (default), do NOT include it in `stages` — sort will synthesize `human-appraise:<cycle>` on deadlock when needed.
26
26
  - Stages should use `base:alias` format (e.g. `forge:write-haiku`, `quench:check-syllables`). If you pass bare names, the tool will auto-append the cycle ID as the alias.
27
- 4. Call `foundry_workfile_set` to configure the work file:
28
- - `key: "cycle"`, `value: <cycle-id>`
29
- - `key: "stages"`, `value: <determined stages list>`
30
- - `key: "max-iterations"`, `value: <default 3 or from cycle definition>`
31
- - If the cycle definition has a `models` map: `key: "models"`, `value: <models map>`
27
+ 4. Call `foundry_workfile_configure_from_cycle({cycleId, stages})` with the cycle ID and the stages list from step 3. The tool reads the cycle definition and writes `cycle`, `stages`, `max-iterations`, `human-appraise`, `deadlock-appraise`, `deadlock-iterations`, and (if present) `models` into WORK.md in a single call, applying defaults for anything the cycle def omits. Do **not** use `foundry_workfile_set` for this — the configure tool is the authoritative cycle-def → WORK.md translator.
32
28
  5. Invoke the sort skill
33
29
 
34
30
  ## Sort drives everything
35
31
 
36
- Once sort is invoked, it calls `foundry_sort` to determine the next stage, invokes the corresponding skill, then calls sort again. This repeats until sort returns `done` or `blocked`.
32
+ Once sort is invoked, it calls `foundry_sort` to determine the next stage, dispatches the corresponding skill to a fresh subagent with a single-use token, calls `foundry_stage_finalize` to register outputs (or detect file-pattern violations), writes history, and commits. This repeats until sort returns `done`, `blocked`, or `violation`.
37
33
 
38
- The cycle skill does not contain routing logic — sort owns all of that.
34
+ The cycle skill does not contain routing, finalization, history, or commit logic — sort owns all of that. The cycle skill only sets up the work file and reacts to sort's terminal result.
39
35
 
40
36
  ## Completing a foundry cycle
41
37
 
42
38
  When sort returns `done`:
43
- - Call `foundry_artefacts_set_status` with status `"done"`
44
- - Return control to the flow skill
39
+ - Call `foundry_artefacts_set_status(file, 'done')` for the cycle's output artefact.
40
+ - Return control to the flow skill.
45
41
 
46
42
  When sort returns `blocked`:
47
- - Call `foundry_artefacts_set_status` with status `"blocked"`
48
- - Return control to the flow skill (the flow decides how to handle it)
43
+ - The target artefact is usually already marked `blocked` by sort (on violations) or by human-appraise (on explicit abort). If not, call `foundry_artefacts_set_status(file, 'blocked')`.
44
+ - Return control to the flow skill the flow decides how to handle it.
45
+
46
+ When sort returns `violation` (e.g., `stage_finalize` `unexpected_files`, missing subagent, or file-pattern violation):
47
+ - Sort has already marked affected artefacts blocked and returned. Treat as the blocked path.
48
+ - Return control to the flow skill.
49
49
 
50
50
  ## Human Appraise
51
51
 
52
- If the cycle definition has `human-appraise.enabled: true`, the human-appraise stage is included after appraise. Sort will route to it after LLM appraisers pass, or earlier if a deadlock is detected.
52
+ Human-appraise is controlled by two flat cycle-def keys:
53
+
54
+ - `human-appraise: true` — human-appraise runs every iteration as part of the normal stage flow (appended to `stages`).
55
+ - `deadlock-appraise: true` (default) — if LLM appraisers deadlock on the same feedback for `deadlock-iterations` rounds (default 5), sort routes to human-appraise to resolve it, even when it isn't in `stages`.
56
+ - `deadlock-appraise: false` — no human intervention; deadlock → `blocked`.
53
57
 
54
58
  ## Micro commits
55
59
 
56
- Every stage must end with a micro commit. Call `foundry_git_commit` with message format: `[<cycle-id>] <base>:<alias>: <brief description>`
60
+ Every stage ends with a micro commit, written by sort (not cycle, not subagents). The message format is `[<cycle-id>] <base>:<alias>: <brief description>`.
57
61
 
58
62
  Examples:
59
63
  - `[haiku-creation] forge:write-haiku: initial draft`
@@ -74,8 +78,10 @@ Tag types: `validation` (from quench), `law:<law-id>` (from appraise), `human` (
74
78
 
75
79
  ## What you do NOT do
76
80
 
77
- - You do not make routing decisions — sort does that
78
- - You do not change the laws mid-cycle
79
- - You do not decide the artefact is "close enough" it passes or it doesn't
80
- - You do not proceed past a file modification violation
81
- - You do not modify input artefactsthey are read-only
81
+ - You do not make routing decisions — sort does that.
82
+ - You do not register artefacts `foundry_stage_finalize` does that (invoked by sort).
83
+ - You do not write history or commitssort does that.
84
+ - You do not change the laws mid-cycle.
85
+ - You do not decide the artefact is "close enough" it passes or it doesn't.
86
+ - You do not proceed past a file modification violation — honor sort's `violation`/`blocked` return.
87
+ - You do not modify input artefacts — they are read-only.
@@ -23,8 +23,15 @@ Before running this skill, verify that the `foundry/` directory exists in the pr
23
23
  - If only one starting cycle, use it
24
24
  - If multiple starting cycles, check whether the user's request makes the choice obvious (e.g., "write a haiku" clearly maps to `create-haiku`)
25
25
  - If ambiguous, prompt the user to choose
26
- 4. Call `foundry_workfile_create` with the flow ID, chosen cycle ID, and goal
27
- 5. Execute the cycle by invoking the cycle skill
26
+ 4. Pre-check for an existing workfile (prevents silent data loss from an aborted prior session):
27
+ a. Call `foundry_workfile_get`.
28
+ b. If it returns `{error: ...}` (no WORK.md), proceed to step 5.
29
+ c. If it returns an existing workfile, present its `flow`, `cycle`, and `goal` to the user alongside the values just requested, then prompt for one of:
30
+ - **Resume** — keep the existing workfile and skip to step 6. **Only offer resume if the existing `flow` AND `cycle` match what the user just asked for.** If either differs, do not offer resume — running the wrong cycle against stale state corrupts the workflow.
31
+ - **Discard** — call `foundry_workfile_delete`, then proceed to step 5.
32
+ - **Abort** — stop the skill without modifying anything.
33
+ 5. Call `foundry_workfile_create` with **only** the flow ID, chosen cycle ID, and goal — do **not** pass `stages` or `maxIterations`. The `cycle` skill will read the cycle definition and populate those via `foundry_workfile_set` in the next step.
34
+ 6. Execute the cycle by invoking the cycle skill
28
35
 
29
36
  ## Between cycles
30
37
 
@@ -14,33 +14,42 @@ Before running this skill, verify that the `foundry/` directory exists in the pr
14
14
 
15
15
  > Foundry is not initialized in this project. Run the `init-foundry` skill first to create the foundry/ directory structure.
16
16
 
17
+ ## Stage lifecycle (mandatory)
18
+
19
+ Forge runs inside an enforced stage. Your **first** and **last** tool calls are fixed:
20
+
21
+ 1. **First:** `foundry_stage_begin({stage, cycle, token})` — the orchestrator hands you `stage`, `cycle`, and an opaque `token` string in the dispatch prompt. Copy the token verbatim; never invent, edit, or re-sign it. No other tool call is permitted before this one. Any writes before `stage_begin` will be blocked by preconditions.
22
+ 2. **Last:** `foundry_stage_end({summary})` — return control to the orchestrator. After `stage_end`, the orchestrator calls `foundry_stage_finalize` which scans the disk and registers your output artefact. **You do not register artefacts yourself.**
23
+
17
24
  ## Protocol
18
25
 
19
26
  ### First generation (no artefact registered yet)
20
27
 
21
- 1. Call `foundry_workfile_get` understand the goal
22
- 2. Call `foundry_config_cycle` — understand what to produce and what inputs are available
23
- 3. Call `foundry_config_artefact_type` with the output type ID get the artefact type definition
24
- 4. Call `foundry_config_laws` — get all applicable laws (global + type-specific)
25
- 5. If the cycle has inputs, read the input artefacts (read-only context)
26
- 6. Produce the artefact, respecting all applicable laws from the start (this is judgment — use your craft)
27
- 7. Write the artefact file to the location specified in the artefact type definition
28
- 8. Call `foundry_artefacts_add` with the file path, type, and cycle to register it with status `"draft"`
28
+ 1. `foundry_stage_begin(...)` with the token from the dispatch prompt.
29
+ 2. `foundry_workfile_get` — understand the goal.
30
+ 3. `foundry_config_cycle` understand what to produce and what inputs are available.
31
+ 4. `foundry_config_artefact_type` with the output type ID — get the artefact type definition, especially its `file-patterns`.
32
+ 5. `foundry_config_laws` get all applicable laws (global + type-specific).
33
+ 6. If the cycle has inputs, read the input artefacts (read-only context).
34
+ 7. Produce the artefact, respecting all applicable laws from the start.
35
+ 8. Write the artefact file to a location that matches the artefact type's `file-patterns`.
36
+ 9. `foundry_stage_end({summary})`.
29
37
 
30
38
  ### Revision (feedback exists)
31
39
 
32
- 1. Call `foundry_feedback_list` to find unresolved feedback for the artefact
33
- 2. Read the artefact file
34
- 3. If the cycle has inputs, read the input artefacts (read-only context)
35
- 4. For each unresolved feedback item, either:
36
- - Address it and call `foundry_feedback_action` with the item ID (marks as actioned)
37
- - Call `foundry_feedback_wontfix` with the item ID and a justification (appraisal feedback only)
38
- 5. Update the artefact file
39
- 6. Wont-fix is only available for `law:` feedback (subjective appraisal). Validation feedback must be actioned — deterministic rules are not negotiable.
40
+ 1. `foundry_stage_begin(...)`.
41
+ 2. `foundry_feedback_list` — find unresolved feedback for the artefact.
42
+ 3. Read the artefact file.
43
+ 4. If the cycle has inputs, read the input artefacts (read-only context).
44
+ 5. For each unresolved feedback item, either:
45
+ - Address it and call `foundry_feedback_action` (marks item `actioned`), or
46
+ - Call `foundry_feedback_wontfix` with a justification — available only for `law:` / `human` tags (validation feedback must be actioned).
47
+ 6. Update the artefact file.
48
+ 7. `foundry_stage_end({summary})`.
40
49
 
41
- ### After (both paths)
50
+ ## File-pattern hygiene
42
51
 
43
- Do NOT call `foundry_history_append` the sort skill (your caller) is responsible for writing history. Instead, return a clear summary of what you did so sort can log it.
52
+ Writes during forge must match the output artefact type's `file-patterns`. Writing to any other path causes `foundry_stage_finalize` to return `{error: 'unexpected_files'}` and the orchestrator will mark the cycle's target artefact `blocked`. You will not get a retry. Plus `WORK.md` and `WORK.history.yaml` (managed by tools). Nothing else.
44
53
 
45
54
  ## Unresolved feedback
46
55
 
@@ -53,14 +62,17 @@ An item is resolved if it is `approved`.
53
62
  ## #human feedback
54
63
 
55
64
  Feedback tagged `human` (from the human-appraise stage) takes absolute priority:
56
- - You MUST address it — you cannot wont-fix `#human` feedback
57
- - When `#human` feedback contradicts LLM appraiser feedback on the same topic, follow the human's direction
58
- - Acknowledge the human's input in your revision
65
+ - You MUST address it — you cannot wont-fix `#human` feedback.
66
+ - When `#human` feedback contradicts LLM appraiser feedback on the same topic, follow the human's direction.
67
+ - Acknowledge the human's input in your revision.
59
68
 
60
69
  ## What you do NOT do
61
70
 
62
- - You do not evaluate or score the artefact
63
- - You do not add feedback — that is the quench skill's and appraise skill's job
64
- - You do not mark feedback as actioned unless you actually changed the artefact to address it
65
- - You do not wont-fix validation feedback
66
- - You do not modify input artefacts they are read-only
71
+ - You do not add feedback that is the quench and appraise skills' job. (`foundry_feedback_add` is blocked for you at the tool layer.)
72
+ - You do not `foundry_feedback_resolve` — that belongs to quench/appraise/human-appraise.
73
+ - You do not register artefacts `foundry_stage_finalize` handles that automatically.
74
+ - You do not call `foundry_history_append` or `foundry_git_commit` — the sort skill does.
75
+ - You do not evaluate or score the artefact.
76
+ - You do not mark feedback as actioned unless you actually changed the artefact to address it.
77
+ - You do not wont-fix validation feedback.
78
+ - You do not modify input artefacts — they are read-only.
@@ -14,17 +14,27 @@ Before running this skill, verify that the `foundry/` directory exists in the pr
14
14
 
15
15
  > Foundry is not initialized in this project. Run the `init-foundry` skill first to create the foundry/ directory structure.
16
16
 
17
+ ## Stage lifecycle (mandatory)
18
+
19
+ Human-appraise runs inside an enforced stage. Your **first** and **last** tool calls are fixed:
20
+
21
+ 1. **First:** `foundry_stage_begin({stage, cycle, token})` — copy the token verbatim from the dispatch prompt.
22
+ 2. **Last:** `foundry_stage_end({summary})`.
23
+
24
+ Human-appraise makes **no disk writes**. All output flows through `foundry_feedback_add` / `foundry_feedback_resolve` / `foundry_artefacts_set_status`. `foundry_stage_finalize` flags unexpected writes as a violation.
25
+
17
26
  ## Protocol
18
27
 
19
- 1. Gather context by calling:
20
- - `foundry_workfile_get` current state, goal, artefacts
21
- - `foundry_artefacts_list` — current artefact files and status
28
+ 1. `foundry_stage_begin(...)`.
29
+ 2. Gather context by calling:
30
+ - `foundry_workfile_get` — current state, goal, cycle
31
+ - `foundry_artefacts_list({cycle: <current-cycle>})` — this cycle's artefact files and status (always pass the `cycle` filter; omitting it returns stale rows from prior sessions)
22
32
  - `foundry_feedback_list` — all existing feedback
23
33
  - `foundry_history_list` — what has happened so far
24
34
 
25
- 2. Read the artefact file(s) for this cycle.
35
+ 3. Read the artefact file(s) for this cycle.
26
36
 
27
- 3. Present to the human:
37
+ 4. Present to the human:
28
38
  - The current artefact content (full file content or multi-file diff)
29
39
  - A summary of this iteration's feedback (resolved and open)
30
40
  - If this is a deadlock escalation, clearly explain the deadlock:
@@ -33,15 +43,15 @@ Before running this skill, verify that the `foundry/` directory exists in the pr
33
43
  - Forge's wont-fix or revision justification
34
44
  - Ask the human to resolve the disagreement
35
45
 
36
- 4. Wait for the human's response.
46
+ 5. Wait for the human's response.
37
47
 
38
- 5. Act on the response:
39
- - **Approve** — "looks good" / "continue" — no feedback added, sort will advance
40
- - **Provide feedback** — call `foundry_feedback_add` with the human's feedback and tag `human`. Sort will route back to forge.
41
- - **Dismiss deadlocked feedback** — call `foundry_feedback_resolve` with `resolution: "approved"` on the deadlocked item(s). This overrides the appraiser.
42
- - **Abort** — call `foundry_artefacts_set_status` with status `"blocked"`, cycle ends
48
+ 6. Act on the response (tag MUST be `human` on any added feedback — the tool rejects other tags during human-appraise):
49
+ - **Approve** — "looks good" / "continue" — no feedback added, sort will advance.
50
+ - **Provide feedback** — `foundry_feedback_add(file, text, tag: 'human')`. Sort will route back to forge.
51
+ - **Dismiss deadlocked feedback** — `foundry_feedback_resolve(file, index, resolution: 'approved')`. Human-appraise may resolve items in state `actioned` or `wont-fix`. This overrides the appraiser.
52
+ - **Abort** — `foundry_artefacts_set_status(file, 'blocked')`, cycle ends.
43
53
 
44
- 6. Return a clear summary of what the human decided so sort can log it in history.
54
+ 7. `foundry_stage_end({summary})` describe what the human decided so sort can log it.
45
55
 
46
56
  ## #human feedback rules
47
57
 
@@ -51,8 +61,10 @@ Before running this skill, verify that the `foundry/` directory exists in the pr
51
61
 
52
62
  ## What you do NOT do
53
63
 
54
- - You do not make decisions for the human present the state and wait
55
- - You do not modify the artefact
56
- - You do not skip the pause — the human must respond before continuing
57
- - You do not filter or summarise away important details show the full picture
58
- - You do not call `foundry_history_append`sort owns history writing
64
+ - You do not write files all output goes through foundry tools.
65
+ - You do not make decisions for the human — present the state and wait.
66
+ - You do not modify the artefact.
67
+ - You do not skip the pause the human must respond before continuing.
68
+ - You do not filter or summarise away important details show the full picture.
69
+ - You do not call `foundry_history_append` or `foundry_git_commit` — sort owns those.
70
+ - You do not register artefacts — handled by `foundry_stage_finalize`.
@@ -14,33 +14,49 @@ Before running this skill, verify that the `foundry/` directory exists in the pr
14
14
 
15
15
  > Foundry is not initialized in this project. Run the `init-foundry` skill first to create the foundry/ directory structure.
16
16
 
17
+ ## Stage lifecycle (mandatory)
18
+
19
+ Quench runs inside an enforced stage. Your **first** and **last** tool calls are fixed:
20
+
21
+ 1. **First:** `foundry_stage_begin({stage, cycle, token})` — copy the token verbatim from the dispatch prompt. Any other tool call before this will be blocked.
22
+ 2. **Last:** `foundry_stage_end({summary})`.
23
+
24
+ Quench makes **no disk writes**. You produce feedback via `foundry_feedback_add`, never by creating or modifying files. `foundry_stage_finalize` (run by the orchestrator after you return) will flag any unexpected writes as a violation.
25
+
17
26
  ## Protocol
18
27
 
19
- 1. Call `foundry_workfile_get` to identify the artefact and its type.
20
- 2. Call `foundry_config_validation` with the artefact type ID. If it returns null, output SKIP and stop there is no validation for this type.
21
- 3. Call `foundry_validate_run` with the type ID and artefact file path. It executes all validation commands and returns results.
22
- 4. For each failure: call `foundry_feedback_add` with the artefact file path, a description of the failure, and tag `"validation"`.
23
- 5. If all commands pass, add no new feedback.
28
+ 1. `foundry_stage_begin(...)`.
29
+ 2. `foundry_workfile_get` — read the `cycle` from frontmatter.
30
+ 3. `foundry_artefacts_list({cycle: <current-cycle>})` enumerate the artefacts produced by **this** cycle. Always pass the `cycle` filter; omitting it returns rows from prior sessions and validates stale files. Skip rows whose status is `done` or `blocked`.
31
+ 4. For each remaining row:
32
+ a. `foundry_config_validation` with the row's type. If it returns null, skip this row.
33
+ b. `foundry_validate_run` with the type ID and the row's file path — executes all validation commands and returns results.
34
+ c. For each failure: `foundry_feedback_add(file, text, tag: 'validation')`. Tag MUST be `validation` — the tool rejects other tags during quench.
35
+ 5. If every command passes for every row, add no new feedback.
36
+ 6. If the artefact table has no rows for this cycle, `foundry_stage_end({summary: 'SKIP: no artefacts registered for this cycle'})` and stop.
37
+ 7. `foundry_stage_end({summary})`.
24
38
 
25
39
  ## Reviewing actioned feedback
26
40
 
27
41
  On subsequent passes, review previously actioned items:
28
42
 
29
- 1. Call `foundry_feedback_list` to find `actioned` items tagged `validation` for this artefact.
43
+ 1. `foundry_feedback_list` find `actioned` items tagged `validation` for artefacts in this cycle (use the file list from step 3 above).
30
44
  2. Re-run the relevant command via `foundry_validate_run`.
31
- 3. If the check now passes: call `foundry_feedback_resolve` with disposition `"approved"`.
32
- 4. If it still fails: call `foundry_feedback_resolve` with disposition `"rejected"` and a reason.
45
+ 3. If the check now passes: `foundry_feedback_resolve(file, index, resolution: 'approved')`.
46
+ 4. If it still fails: `foundry_feedback_resolve(file, index, resolution: 'rejected', reason)`.
33
47
 
34
- There is no wont-fix for validation feedback. Deterministic rules are not negotiable.
48
+ There is no wont-fix for validation feedback deterministic rules are not negotiable. Quench may only resolve items in state `actioned`; the feedback tool enforces this.
35
49
 
36
50
  ## History
37
51
 
38
- Do NOT call `foundry_history_append` — the sort skill (your caller) is responsible for writing history. Instead, return a clear summary of what you found (e.g., "2 validation issues found" or "Validation passed") so sort can log it.
52
+ Do NOT call `foundry_history_append` or `foundry_git_commit` — the sort skill handles those. Return a clear summary via `foundry_stage_end` (e.g., "2 validation issues found" or "Validation passed").
39
53
 
40
54
  ## What you do NOT do
41
55
 
42
- - You do not make subjective judgments
43
- - You do not revise the artefact
44
- - You do not evaluate laws — that is the appraise skill's job
45
- - You do not invent validation rules you only run commands from the validation config
46
- - You do not duplicate feedback that already exists
56
+ - You do not write files — all output goes through `foundry_feedback_add`.
57
+ - You do not make subjective judgments.
58
+ - You do not revise the artefact (forge's job).
59
+ - You do not evaluate lawsthat is the appraise skill's job.
60
+ - You do not invent validation rules you only run commands from the validation config.
61
+ - You do not duplicate feedback that already exists (the tool de-duplicates by text-hash, but don't rely on it).
62
+ - You do not register artefacts — that happens automatically.