@really-knows-ai/foundry 3.8.1 → 3.8.2

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.
@@ -8,7 +8,7 @@ import { syncStore } from '../../../scripts/lib/memory/store.js';
8
8
  import { makeIO, makeMemoryIO, branchIoFactory, asyncIoFactory, flowBranchGuard } from './helpers.js';
9
9
  import { markWorkfileFailed, readFailedStatus, clearWorkfileFailed } from '../../../scripts/lib/failed-flow.js';
10
10
  import { guarded, notFailedGuard } from '../../../scripts/lib/guards.js';
11
- import { initForgeCallLog, verifyAndClearForgeCallLog } from '../../../scripts/lib/stage-calls.js';
11
+ import { initForgeCallLog, readForgeCallSet } from '../../../scripts/lib/stage-calls.js';
12
12
  import { openFeedbackStore } from '../../../scripts/lib/feedback-store.js';
13
13
 
14
14
  const FORGE_REQUIRED_TOOLS = [
@@ -18,6 +18,12 @@ const FORGE_REQUIRED_TOOLS = [
18
18
  'foundry_config_laws',
19
19
  ];
20
20
 
21
+ const FORGE_FORBIDDEN_TOOLS = [
22
+ 'foundry_feedback_action',
23
+ 'foundry_feedback_wontfix',
24
+ 'foundry_feedback_resolve',
25
+ ];
26
+
21
27
  function stageBase(stage) { return stage.split(':')[0]; }
22
28
 
23
29
  const gateNotFailed = notFailedGuard(makeIO);
@@ -25,9 +31,16 @@ const gateNotFailed = notFailedGuard(makeIO);
25
31
  // -- Helpers for forge tool call verification --
26
32
 
27
33
  function verifyAndManageForgeTools(io, active) {
28
- const verified = verifyAndClearForgeCallLog(io, FORGE_REQUIRED_TOOLS);
29
- if (!verified.ok) {
30
- postMissingToolsFeedback(io, active, verified.missing);
34
+ const callSet = readForgeCallSet(io);
35
+ const forbidden = FORGE_FORBIDDEN_TOOLS.filter(t => callSet.has(t));
36
+ const missing = FORGE_REQUIRED_TOOLS.filter(t => !callSet.has(t));
37
+ io.unlink('.foundry/.forge-tool-calls.jsonl');
38
+ if (forbidden.length) {
39
+ postForbiddenToolsFeedback(io, active, forbidden);
40
+ return;
41
+ }
42
+ if (missing.length) {
43
+ postMissingToolsFeedback(io, active, missing);
31
44
  return;
32
45
  }
33
46
  resolveSystemFeedback(io, active);
@@ -144,6 +157,19 @@ async function executeStageEnd(args, context) {
144
157
  return JSON.stringify({ ok: true, summary: args.summary });
145
158
  }
146
159
 
160
+ function postForbiddenToolsFeedback(io, active, forbidden) {
161
+ try {
162
+ const store = openFeedbackStore('WORK.feedback.yaml', io);
163
+ store.add({
164
+ file: '(forge)',
165
+ tag: 'system:forbidden-tool-calls',
166
+ text: `Forbidden forge tool calls: ${forbidden.join(', ')}. Forge subagents do not manage feedback — the orchestrator handles transitions.`,
167
+ source: active.stage,
168
+ cycle: active.cycle,
169
+ });
170
+ } catch { /* feedback file not initialised yet; non-critical */ }
171
+ }
172
+
147
173
  function postMissingToolsFeedback(io, active, missing) {
148
174
  try {
149
175
  const store = openFeedbackStore('WORK.feedback.yaml', io);
package/dist/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.8.2] - 2026-05-27
4
+
5
+ ### Changed
6
+
7
+ - Forge subagent protocol simplified to three keywords: `DONE` (first generation), `ACTIONED` (file changed), `WONT-FIX: <justification>` (no changes needed). The `foundry_stage_end` summary must be exactly one of these — no descriptions, no explanations. The forge contract recognises `ACTIONED` even without a version change.
8
+
9
+ - WONT-FIX is now allowed for all feedback source types (quench, appraise, human-appraise), not just appraise. The `WONT-FIX:` keyword in the summary transitions the item regardless of source.
10
+
11
+ ### Fixed
12
+
13
+ - Forge subagents are blocked from calling `foundry_feedback_action`, `foundry_feedback_wontfix`, or `foundry_feedback_resolve` — `foundry_stage_end` checks the call log and posts `system:forbidden-tool-calls` feedback if any were called.
14
+
15
+ - Single-item dispatch prompts clarified: the orchestrator dispatches one feedback item per forge call. The orchestrate skill instructs the LLM to pass the prompt verbatim without injecting extra items from quench output.
16
+
3
17
  ## [3.8.1] - 2026-05-27
4
18
 
5
19
  ### Fixed
@@ -99,7 +99,5 @@ export function hashText(text) {
99
99
  */
100
100
  export function canForgeWontFix(item, callerStageBase) {
101
101
  if (callerStageBase !== 'forge') return false;
102
- if (!item || typeof item.source !== 'string' || !item.source) return false;
103
- const sourceBase = item.source.split(':')[0];
104
- return sourceBase === 'appraise';
102
+ return !!(item && typeof item.source === 'string' && item.source);
105
103
  }
@@ -45,28 +45,18 @@ function handleVersionChanged(item, feedbackStore, cycleId, postVersion) {
45
45
  }
46
46
 
47
47
  function handleWontFixWithReason(item, feedbackStore, cycleId, postVersion, reason) {
48
- const sourceBase = typeof item.source === 'string' ? item.source.split(':')[0] : '';
49
- if (sourceBase === 'appraise') {
50
- const result = feedbackStore.transition({
51
- id: item.id,
52
- target: 'wont-fix',
53
- stage: 'forge:' + cycleId,
54
- cycle: cycleId,
55
- reason,
56
- });
57
- if (!result.ok) {
58
- postSystemFeedback(feedbackStore, cycleId, postVersion, result.error || 'store transition failed');
59
- feedbackStore.forceState(item.id, 'open', cycleId, `forge:${cycleId}`);
60
- }
61
- return { contractPassed: result.ok };
48
+ const result = feedbackStore.transition({
49
+ id: item.id,
50
+ target: 'wont-fix',
51
+ stage: 'forge:' + cycleId,
52
+ cycle: cycleId,
53
+ reason,
54
+ });
55
+ if (!result.ok) {
56
+ postSystemFeedback(feedbackStore, cycleId, postVersion, result.error || 'store transition failed');
57
+ feedbackStore.forceState(item.id, 'open', cycleId, `forge:${cycleId}`);
62
58
  }
63
- // quench or human-appraise — wont-fix not allowed
64
- postSystemFeedback(
65
- feedbackStore, cycleId, postVersion,
66
- `wont-fix not allowed on ${sourceBase}-sourced item; wont-fix is only allowed for appraise-sourced items`,
67
- );
68
- feedbackStore.forceState(item.id, 'open', cycleId, `forge:${cycleId}`);
69
- return { contractPassed: false };
59
+ return { contractPassed: result.ok };
70
60
  }
71
61
 
72
62
  /**
@@ -81,24 +71,23 @@ function handleWontFixWithReason(item, feedbackStore, cycleId, postVersion, reas
81
71
  * @returns {{ contractPassed: boolean }}
82
72
  */
83
73
  export function enforceForgeContract({ item, preVersion, postVersion, summary, feedbackStore, cycleId }) {
84
- // No item means forge had no prior feedback to respond to.
85
74
  if (!item) return { contractPassed: true };
86
75
 
87
- // Version changed → forge fixed the issue
88
- if (preVersion !== postVersion) {
89
- return handleVersionChanged(item, feedbackStore, cycleId, postVersion);
90
- }
91
-
92
- // Version unchanged — check for WONT-FIX justification
93
76
  const wontFixMatch = summary.match(/WONT-FIX:\s*(.+)/);
77
+ const versionChanged = preVersion !== postVersion;
78
+ const actioned = summary.trim() === 'ACTIONED';
79
+
94
80
  if (wontFixMatch) {
95
81
  return handleWontFixWithReason(item, feedbackStore, cycleId, postVersion, wontFixMatch[1]);
96
82
  }
97
83
 
98
- // Version unchanged with no WONT-FIX — neither fix nor justification
84
+ if (versionChanged || actioned) {
85
+ return handleVersionChanged(item, feedbackStore, cycleId, postVersion);
86
+ }
87
+
99
88
  postSystemFeedback(
100
89
  feedbackStore, cycleId, postVersion,
101
- 'forge did not change artefacts and did not provide WONT-FIX justification',
90
+ 'forge did not change artefacts and did not provide ACTIONED or WONT-FIX justification',
102
91
  );
103
92
  feedbackStore.forceState(item.id, 'open', cycleId, `forge:${cycleId}`);
104
93
  return { contractPassed: false };
@@ -35,7 +35,7 @@ function forgeReason(d) {
35
35
  if (d.forgeCount === 0 && d.needingForge === 0) {
36
36
  return `starting cycle — routing to forge (iteration 1 of ${d.maxIt})`;
37
37
  }
38
- return `found ${d.needingForge} unresolved feedback item(s) — routing to forge for revision (iteration ${d.forgeCount + 1} of ${d.maxIt})`;
38
+ return `found ${d.needingForge} unresolved feedback item(s) — dispatching one item at a time to forge (revision ${d.forgeCount + 1} of ${d.maxIt})`;
39
39
  }
40
40
 
41
41
  function appraiseReason(d) {
@@ -29,6 +29,10 @@ function readCallSet(io) {
29
29
  return called;
30
30
  }
31
31
 
32
+ export function readForgeCallSet(io) {
33
+ return readCallSet(io);
34
+ }
35
+
32
36
  export function verifyAndClearForgeCallLog(io, expected) {
33
37
  const called = readCallSet(io);
34
38
  const missing = expected.filter(t => !called.has(t));
@@ -266,15 +266,17 @@ function buildForgePromptLines({ cycle, outputType, forgeItem }) {
266
266
  `File: ${forgeItem.file}`,
267
267
  `Issue: ${forgeItem.text}`,
268
268
  ``,
269
- `You MUST either:`,
270
- ` a) Fix the issue by changing the artefact file. The orchestrator`,
271
- ` will record this as ACTIONED.`,
272
- ` b) If this is an appraise-sourced item (subjective quality`,
273
- ` feedback), you may respond with:`,
274
- ` WONT-FIX: <justification for why you disagree>`,
269
+ `Respond with EXACTLY one of:`,
270
+ ` - ACTIONED — fix the issue by changing the artefact file`,
271
+ ` - WONT-FIX: <justification> the issue is already resolved or does not apply`,
275
272
  ``,
276
- `Quench-sourced items are deterministic validation failures —`,
277
- `you MUST fix them. There is no wont-fix option.`,
273
+ `Write NOTHING else in the stage_end summary — no descriptions, no explanations.`,
274
+ );
275
+ } else {
276
+ lines.push(
277
+ ``,
278
+ `First generation — no feedback to address yet.`,
279
+ `Produce the artefact and call foundry_stage_end({summary: "DONE"}).`,
278
280
  );
279
281
  }
280
282
  return lines;
@@ -300,8 +302,6 @@ export function renderDispatchPrompt({ stage, cycle, token, cwd, filePatterns, o
300
302
  ``,
301
303
  `Your FIRST tool call MUST be foundry_stage_begin({stage, cycle, token}) using the values above.`,
302
304
  `Your LAST tool call MUST be foundry_stage_end({summary}).`,
303
- ``,
304
- `When done, report back a brief summary. Do NOT call foundry_history_append, foundry_git_commit, or foundry_artefacts_add — the orchestrator handles all of those.`
305
305
  );
306
306
  return lines.join('\n');
307
307
  }
@@ -51,43 +51,38 @@ Forge runs inside an enforced stage. Your **first** and **last** tool calls are
51
51
  - Read the selected files for context.
52
52
  7. Produce the artefact, respecting all applicable laws from the start.
53
53
  8. Write the artefact file to a location that matches the artefact type's `file-patterns`.
54
- 9. `foundry_stage_end({summary})`.
54
+ 9. `foundry_stage_end({summary: "DONE"})`.
55
55
 
56
56
  ### Revision (feedback exists)
57
57
 
58
58
  1. `foundry_stage_begin(...)`.
59
59
  2. Read the artefact file.
60
60
  3. If the cycle declares `inputs`, discover them via filesystem scan against each input type's `file-patterns` (same protocol as first-generation step 6). Re-read the relevant files — they may have changed on disk since the previous iteration (nothing in this cycle wrote to them, but the user may have modified them between iterations).
61
- 4. Address the single feedback item from the dispatch prompt following the feedback handling rules below — either fix the artefact, or for appraise-sourced items write a WONT-FIX justification in the summary.
62
- 5. Update the artefact file.
63
- 6. `foundry_stage_end({summary})`.
61
+ 4. Address the single feedback item from the dispatch prompt following the feedback handling rules below.
62
+ 5. Update the artefact file (if fixing), or skip (if WONT-FIX).
63
+ 6. `foundry_stage_end({summary})`. The summary must be EXACTLY one of:
64
+ - `"ACTIONED"` — file was changed to address the feedback
65
+ - `"WONT-FIX: <justification>"` — item already resolved or does not apply
66
+ Write NOTHING else in the summary.
64
67
 
65
68
  ## Feedback handling
66
69
 
67
- The dispatch prompt already contains the single feedback item for this
68
- iteration. Each item has the shape `{ id, file, tag, text, source, state,
69
- depth, reason? }`.
70
+ The dispatch prompt contains one feedback item to address.
70
71
 
71
- Fix the issue by changing the artefact the orchestrator records the item
72
- as actioned when it detects your changes on disk.
72
+ **To fix the issue** change the artefact file and call
73
+ `foundry_stage_end({summary: "ACTIONED"})`.
73
74
 
74
- For items whose `source` stage base is `appraise` only, you may instead
75
- respond with `WONT-FIX: <justification>` in the `foundry_stage_end`
76
- summary. The orchestrator records the item as wont-fix.
75
+ **If the issue is already resolved** call
76
+ `foundry_stage_end({summary: "WONT-FIX: <justification>"})`.
77
+ Do NOT change the file.
77
78
 
78
- Items whose source base is `quench` (objective validation failure) or
79
- `human-appraise` (direct user instruction) are deterministic failures that
80
- **must** be fixed. There is no wont-fix option for these.
79
+ **If the issue does not apply** (appraise judgement you disagree with) — same
80
+ `WONT-FIX:` flow.
81
81
 
82
- `foundry_feedback_add` (if you ever call it forge normally does not)
83
- returns `{ ok, id, deduped }`. `deduped: true` means an existing
84
- non-resolved item with the same `(file, tag, hash(text))` was found and no
85
- new item was written; the returned `id` is the existing item's id.
86
- `deduped: false` means a new item was created.
82
+ The summary is ONLY one of these keywords. No descriptions, no explanations.
87
83
 
88
- You cannot resolve or reject items — only the stage that created the item
89
- (the `source` on each list entry) can do that, with the exception that
90
- human-appraise can override any non-resolved item.
84
+ Do NOT call `foundry_feedback_action`, `foundry_feedback_wontfix`, or
85
+ `foundry_feedback_resolve`. The orchestrator handles transitions automatically.
91
86
 
92
87
  ## Write invariant
93
88
 
@@ -45,11 +45,8 @@ task tool:
45
45
  description: "Run <stage> for <cycle>"
46
46
  prompt: <prompt-from-payload — pass verbatim>
47
47
  ```
48
- task tool:
49
- subagent_type: <subagent_type-from-payload>
50
- description: "Run <stage> for <cycle>"
51
- prompt: <prompt-from-payload — pass verbatim>
52
- ```
48
+
49
+ **Critical for forge dispatch:** The orchestrator dispatches one feedback item per forge subagent call. The `prompt` already contains exactly one `FEEDBACK ITEM TO ADDRESS`. Pass the prompt verbatim — do NOT read quench output, do NOT add additional feedback items, do NOT inject validator results. The orchestrator will dispatch a separate `task()` call for each unresolved item.
53
50
 
54
51
  When the task returns, call `foundry_orchestrate({lastResult: {ok: true}})`. If the task tool itself errored or reported a subagent crash, pass `{ok: false, error: '<message>'}`.
55
52
 
@@ -114,6 +111,7 @@ Report to the user: "Cycle halted (violation): `<details>`. Affected files: `<af
114
111
  - You do NOT mint, modify, or cache tokens. The `prompt` from orchestrate already contains the token verbatim.
115
112
  - `foundry_history_append`, `foundry_git_commit`, `foundry_stage_finalize`, and `foundry_sort` are not registered tools; orchestrate handles them internally via the loop.
116
113
  - You do NOT reorder the protocol. `foundry_orchestrate` returns, you act, you call back. Nothing else between.
114
+ - You do NOT add extra feedback items to the forge dispatch prompt. The orchestrator dispatches one item at a time. Each prompt already contains exactly one `FEEDBACK ITEM TO ADDRESS`. Do not read quench output and inject additional items.
117
115
 
118
116
  ## Feedback visibility
119
117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@really-knows-ai/foundry",
3
- "version": "3.8.1",
3
+ "version": "3.8.2",
4
4
  "description": "A skill-driven framework for governed artefact generation with AI coding tools. Define your own artefact types, laws, and flows — Foundry handles the forge → quench → appraise pipeline with deterministic routing, quality gates, and iterative refinement.",
5
5
  "type": "module",
6
6
  "main": "dist/.opencode/plugins/foundry.js",