@really-knows-ai/foundry 2.3.2 → 3.0.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.
Files changed (170) hide show
  1. package/README.md +180 -369
  2. package/dist/.opencode/plugins/foundry-tools/appraiser-tools.js +28 -0
  3. package/dist/.opencode/plugins/foundry-tools/artefact-tools.js +58 -0
  4. package/dist/.opencode/plugins/foundry-tools/assay-tools.js +92 -0
  5. package/dist/.opencode/plugins/foundry-tools/attestation-tools.js +191 -0
  6. package/dist/.opencode/plugins/foundry-tools/config-create-tools.js +128 -0
  7. package/dist/.opencode/plugins/foundry-tools/config-law-tools.js +380 -0
  8. package/dist/.opencode/plugins/foundry-tools/config-tools.js +43 -0
  9. package/dist/.opencode/plugins/foundry-tools/feedback-tools.js +234 -0
  10. package/dist/.opencode/plugins/foundry-tools/git-helpers.js +354 -0
  11. package/dist/.opencode/plugins/foundry-tools/git-tools.js +181 -0
  12. package/dist/.opencode/plugins/foundry-tools/helpers.js +340 -0
  13. package/dist/.opencode/plugins/foundry-tools/history-tools.js +20 -0
  14. package/dist/.opencode/plugins/foundry-tools/memory-admin-tools.js +296 -0
  15. package/dist/.opencode/plugins/foundry-tools/memory-helpers.js +104 -0
  16. package/dist/.opencode/plugins/foundry-tools/memory-tools.js +286 -0
  17. package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +159 -0
  18. package/dist/.opencode/plugins/foundry-tools/snapshot-tools.js +104 -0
  19. package/dist/.opencode/plugins/foundry-tools/stage-tools.js +186 -0
  20. package/dist/.opencode/plugins/foundry-tools/validate-tools.js +263 -0
  21. package/dist/.opencode/plugins/foundry-tools/workfile-tools.js +102 -0
  22. package/dist/.opencode/plugins/foundry.js +105 -0
  23. package/dist/CHANGELOG.md +533 -0
  24. package/dist/LICENSE +21 -0
  25. package/dist/README.md +278 -0
  26. package/dist/docs/README.md +59 -0
  27. package/dist/docs/architecture.md +433 -0
  28. package/dist/docs/concepts.md +395 -0
  29. package/dist/docs/getting-started.md +344 -0
  30. package/dist/docs/memory-maintenance.md +176 -0
  31. package/dist/docs/tools.md +1411 -0
  32. package/dist/docs/work-spec.md +283 -0
  33. package/dist/scripts/lib/artefacts.js +151 -0
  34. package/dist/scripts/lib/assay/loader.js +151 -0
  35. package/dist/scripts/lib/assay/parse-jsonl.js +102 -0
  36. package/dist/scripts/lib/assay/permissions.js +52 -0
  37. package/dist/scripts/lib/assay/run.js +219 -0
  38. package/dist/scripts/lib/assay/spawn-with-timeout.js +138 -0
  39. package/dist/scripts/lib/attestation/attest.js +111 -0
  40. package/dist/scripts/lib/attestation/canonical-json.js +109 -0
  41. package/dist/scripts/lib/attestation/hash.js +17 -0
  42. package/dist/scripts/lib/attestation/parse.js +14 -0
  43. package/dist/scripts/lib/attestation/payload.js +106 -0
  44. package/dist/scripts/lib/attestation/render.js +16 -0
  45. package/dist/scripts/lib/attestation/verify.js +15 -0
  46. package/dist/scripts/lib/branch-guard.js +72 -0
  47. package/dist/scripts/lib/config-creators/appraiser.js +9 -0
  48. package/dist/scripts/lib/config-creators/artefact-type.js +9 -0
  49. package/dist/scripts/lib/config-creators/cycle.js +11 -0
  50. package/dist/scripts/lib/config-creators/factory.js +49 -0
  51. package/dist/scripts/lib/config-creators/flow.js +11 -0
  52. package/dist/scripts/lib/config-validators/appraiser.js +49 -0
  53. package/dist/scripts/lib/config-validators/artefact-type.js +38 -0
  54. package/dist/scripts/lib/config-validators/cycle.js +131 -0
  55. package/dist/scripts/lib/config-validators/flow.js +57 -0
  56. package/dist/scripts/lib/config-validators/helpers.js +96 -0
  57. package/dist/scripts/lib/config-validators/law.js +96 -0
  58. package/dist/scripts/lib/config.js +328 -0
  59. package/dist/scripts/lib/failed-flow.js +131 -0
  60. package/dist/scripts/lib/feedback-store.js +249 -0
  61. package/dist/scripts/lib/feedback-transitions.js +105 -0
  62. package/dist/scripts/lib/finalize.js +70 -0
  63. package/dist/scripts/lib/foundational-guards.js +13 -0
  64. package/dist/scripts/lib/git-bridge.js +77 -0
  65. package/dist/scripts/lib/git-finish/work-finish.js +233 -0
  66. package/dist/scripts/lib/git-policy.js +101 -0
  67. package/dist/scripts/lib/guards.js +125 -0
  68. package/dist/scripts/lib/history.js +132 -0
  69. package/dist/scripts/lib/memory/admin/create-edge-type.js +91 -0
  70. package/dist/scripts/lib/memory/admin/create-entity-type.js +43 -0
  71. package/dist/scripts/lib/memory/admin/create-extractor.js +67 -0
  72. package/dist/scripts/lib/memory/admin/drop-edge-type.js +40 -0
  73. package/dist/scripts/lib/memory/admin/drop-entity-type.js +172 -0
  74. package/dist/scripts/lib/memory/admin/dump.js +47 -0
  75. package/dist/scripts/lib/memory/admin/helpers.js +31 -0
  76. package/dist/scripts/lib/memory/admin/init.js +170 -0
  77. package/dist/scripts/lib/memory/admin/live-store.js +76 -0
  78. package/dist/scripts/lib/memory/admin/reembed.js +285 -0
  79. package/dist/scripts/lib/memory/admin/rename-edge-type.js +54 -0
  80. package/dist/scripts/lib/memory/admin/rename-entity-type.js +151 -0
  81. package/dist/scripts/lib/memory/admin/reset.js +24 -0
  82. package/dist/scripts/lib/memory/admin/vacuum.js +9 -0
  83. package/dist/scripts/lib/memory/admin/validate.js +19 -0
  84. package/dist/scripts/lib/memory/config.js +149 -0
  85. package/dist/scripts/lib/memory/cozo.js +136 -0
  86. package/dist/scripts/lib/memory/drift.js +71 -0
  87. package/dist/scripts/lib/memory/embeddings.js +128 -0
  88. package/dist/scripts/lib/memory/frontmatter.js +75 -0
  89. package/dist/scripts/lib/memory/ndjson.js +84 -0
  90. package/dist/scripts/lib/memory/paths.js +25 -0
  91. package/dist/scripts/lib/memory/permissions.js +41 -0
  92. package/dist/scripts/lib/memory/prompt.js +109 -0
  93. package/dist/scripts/lib/memory/query.js +56 -0
  94. package/dist/scripts/lib/memory/reads.js +109 -0
  95. package/dist/scripts/lib/memory/schema.js +64 -0
  96. package/dist/scripts/lib/memory/search.js +73 -0
  97. package/dist/scripts/lib/memory/singleton.js +49 -0
  98. package/dist/scripts/lib/memory/store.js +162 -0
  99. package/dist/scripts/lib/memory/types.js +93 -0
  100. package/dist/scripts/lib/memory/validate.js +58 -0
  101. package/dist/scripts/lib/memory/writes.js +40 -0
  102. package/{scripts → dist/scripts}/lib/pending.js +7 -2
  103. package/dist/scripts/lib/secret.js +59 -0
  104. package/{scripts → dist/scripts}/lib/slug.js +3 -2
  105. package/dist/scripts/lib/snapshot/finish.js +103 -0
  106. package/dist/scripts/lib/snapshot/inspect.js +253 -0
  107. package/dist/scripts/lib/snapshot/render.js +55 -0
  108. package/dist/scripts/lib/sort-fs-check.js +121 -0
  109. package/dist/scripts/lib/sort-routing.js +101 -0
  110. package/{scripts → dist/scripts}/lib/stage-guard.js +12 -6
  111. package/{scripts → dist/scripts}/lib/state.js +4 -0
  112. package/dist/scripts/lib/token.js +57 -0
  113. package/dist/scripts/lib/tracing.js +59 -0
  114. package/dist/scripts/lib/ulid.js +100 -0
  115. package/dist/scripts/lib/validator-jsonl.js +162 -0
  116. package/{scripts → dist/scripts}/lib/workfile.js +38 -20
  117. package/dist/scripts/orchestrate-cycle.js +215 -0
  118. package/dist/scripts/orchestrate-phases.js +314 -0
  119. package/dist/scripts/orchestrate.js +163 -0
  120. package/dist/scripts/sort.js +278 -0
  121. package/{skills → dist/skills}/add-appraiser/SKILL.md +39 -9
  122. package/{skills → dist/skills}/add-artefact-type/SKILL.md +62 -40
  123. package/{skills → dist/skills}/add-cycle/SKILL.md +57 -17
  124. package/dist/skills/add-extractor/SKILL.md +133 -0
  125. package/{skills → dist/skills}/add-flow/SKILL.md +36 -10
  126. package/dist/skills/add-law/SKILL.md +191 -0
  127. package/dist/skills/add-memory-edge-type/SKILL.md +52 -0
  128. package/dist/skills/add-memory-entity-type/SKILL.md +74 -0
  129. package/{skills → dist/skills}/appraise/SKILL.md +62 -13
  130. package/dist/skills/assay/SKILL.md +72 -0
  131. package/dist/skills/change-embedding-model/SKILL.md +58 -0
  132. package/dist/skills/drop-memory-edge-type/SKILL.md +54 -0
  133. package/dist/skills/drop-memory-entity-type/SKILL.md +57 -0
  134. package/dist/skills/dry-run/SKILL.md +116 -0
  135. package/{skills → dist/skills}/flow/SKILL.md +15 -2
  136. package/dist/skills/forge/SKILL.md +121 -0
  137. package/dist/skills/human-appraise/SKILL.md +153 -0
  138. package/{skills → dist/skills}/init-foundry/SKILL.md +23 -4
  139. package/dist/skills/init-memory/SKILL.md +92 -0
  140. package/{skills → dist/skills}/orchestrate/SKILL.md +30 -4
  141. package/dist/skills/quench/SKILL.md +99 -0
  142. package/{skills → dist/skills}/refresh-agents/SKILL.md +1 -1
  143. package/dist/skills/rename-memory-edge-type/SKILL.md +50 -0
  144. package/dist/skills/rename-memory-entity-type/SKILL.md +51 -0
  145. package/dist/skills/reset-memory/SKILL.md +54 -0
  146. package/dist/skills/upgrade-foundry/SKILL.md +191 -0
  147. package/package.json +34 -17
  148. package/.opencode/plugins/foundry.js +0 -761
  149. package/CHANGELOG.md +0 -100
  150. package/docs/concepts.md +0 -122
  151. package/docs/getting-started.md +0 -187
  152. package/docs/work-spec.md +0 -207
  153. package/scripts/lib/artefacts.js +0 -124
  154. package/scripts/lib/config.js +0 -175
  155. package/scripts/lib/feedback-transitions.js +0 -25
  156. package/scripts/lib/feedback.js +0 -440
  157. package/scripts/lib/finalize.js +0 -41
  158. package/scripts/lib/history.js +0 -59
  159. package/scripts/lib/secret.js +0 -23
  160. package/scripts/lib/tags.js +0 -108
  161. package/scripts/lib/token.js +0 -26
  162. package/scripts/orchestrate.js +0 -418
  163. package/scripts/sort.js +0 -370
  164. package/scripts/validate-tags.js +0 -54
  165. package/skills/add-law/SKILL.md +0 -111
  166. package/skills/forge/SKILL.md +0 -88
  167. package/skills/human-appraise/SKILL.md +0 -82
  168. package/skills/quench/SKILL.md +0 -62
  169. package/skills/upgrade-foundry/SKILL.md +0 -216
  170. /package/{skills → dist/skills}/list-agents/SKILL.md +0 -0
@@ -0,0 +1,283 @@
1
+ # WORK.md Spec
2
+
3
+ WORK.md is created at the start of a foundry flow on a work branch. It is the shared state between all stages in all foundry cycles. It is transient — it exists only for the duration of the foundry flow.
4
+
5
+ ## Branch namespaces
6
+
7
+ WORK.md and its sibling YAML files live on one of two branch kinds:
8
+
9
+ - **`work/<flowId>-<description>`** — the standard flow run. Created
10
+ from `main`. On `foundry_git_finish`, the work branch is preserved
11
+ as `archive/work/<flowId>-<description>-<hash>` and squash-merged to
12
+ the base branch with a signed commit embedding the canonical Foundry
13
+ attestation block.
14
+ - **`dry-run/<parentConfig>/<flowId>-<description>`** — a trial run
15
+ used to test in-progress config edits against a real flow. Created
16
+ from a `config/*` branch. On `foundry_git_finish`, the dry-run
17
+ branch is force-deleted and a forensic snapshot
18
+ (`README.md`, `work/WORK*`, `diff.patch`, `trace.jsonl`) is written
19
+ to `.snapshots/<run-id>/` on the parent `config/*` working tree.
20
+ No merge, no commit.
21
+
22
+ WORK.md is identical on both kinds; the dispatching distinction is
23
+ made entirely by `foundry_git_finish`. The third namespace,
24
+ `config/<description>`, owns schema/config mutation and never carries
25
+ WORK files.
26
+
27
+ ## Frontmatter
28
+
29
+ ```yaml
30
+ ---
31
+ flow: <flow-id>
32
+ cycle: <current-cycle-id>
33
+ stages: [forge:write-haiku, quench:check-syllables, appraise:evaluate-quality]
34
+ max-iterations: 3
35
+ human-appraise: false
36
+ deadlock-appraise: true
37
+ deadlock-iterations: 5
38
+ models:
39
+ forge: anthropic/claude-opus-4.7
40
+ appraise: openai/gpt-5
41
+ assay:
42
+ extractors: [list-routes, list-models]
43
+ ---
44
+ ```
45
+
46
+ Fields:
47
+ - `flow` — the foundry flow being executed.
48
+ - `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.
50
+ - `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 LLM appraisers deadlock (default: `true`).
53
+ - `deadlock-iterations` — deadlock threshold (default: 5).
54
+ - `models` — optional per-stage model overrides; individual appraisers may further override via their own `model` field.
55
+ - `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
+
57
+ The `stages` list is the happy path. Sort follows it, loops back to `forge` when unresolved feedback demands it, and may insert `human-appraise` on deadlock. If `assay` is configured, it runs once at iteration 0 before the route begins.
58
+
59
+ ### Who sets what
60
+
61
+ - `flow`, `cycle` — set by the `flow` skill via `foundry_workfile_create` at flow start and updated as the flow advances between cycles.
62
+ - `goal` — written once by the `flow` skill when `WORK.md` is created.
63
+ - `stages`, `max-iterations`, `human-appraise`, `deadlock-appraise`, `deadlock-iterations`, `models`, `assay` — set by `foundry_orchestrate` on the first call of each cycle (via internal `workfile_configure_from_cycle`, reading the cycle definition).
64
+
65
+ ## Sections
66
+
67
+ ### Goal
68
+
69
+ 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
+
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
+ ## WORK.feedback.yaml
88
+
89
+ The flow run owns one `WORK.feedback.yaml` file alongside `WORK.md` and
90
+ `WORK.history.yaml`. It records every feedback item created during the current run,
91
+ and the full state-transition history of each. Tracked in git, committed
92
+ per-stage on the work branch, deleted by `foundry_git_finish` before the
93
+ squash-merge (same lifecycle as `WORK.history.yaml`).
94
+
95
+ ### Schema
96
+
97
+ Top-level: `{ items: [Item...] }`.
98
+
99
+ Each `Item`:
100
+
101
+ | Field | Type | Required | Mutable? |
102
+ |-------|------|----------|----------|
103
+ | `id` | string (ULID, 26 chars) | yes | no |
104
+ | `file` | string | yes | no |
105
+ | `tag` | string (no leading `#`) | yes | no |
106
+ | `text` | string | yes | no |
107
+ | `source` | string (`base:alias`) | yes | no |
108
+ | `history` | array, length >= 1 | yes | prepend-only |
109
+
110
+ `source` bases include `quench`, `appraise`, and `human-appraise`. Extractor failure marks the workfile failed, so `assay` does not appear as a feedback source.
111
+
112
+ Each history snapshot:
113
+
114
+ | Field | Type | Required | Notes |
115
+ |-------|------|----------|-------|
116
+ | `state` | enum | yes | `open \| actioned \| wont-fix \| rejected \| deadlocked \| resolved` |
117
+ | `stage` | string (`base:alias`) or literal `sort` | yes | Who performed the transition |
118
+ | `cycle` | string | yes | Cycle id at the time of the transition |
119
+ | `timestamp` | ISO-8601 UTC with ms | yes | |
120
+ | `reason` | string | conditional | Required on `rejected`, `wont-fix`, `deadlocked`, `resolved`; forbidden on `open`; optional on `actioned` |
121
+
122
+ `history[0]` is always the current state; new snapshots are prepended.
123
+ `resolved` is terminal.
124
+
125
+ ### State machine
126
+
127
+ Feedback items flow through a six-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), `deadlocked` (sort detected repeated forge/appraise iterations on the same item), 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
+
129
+ The six states and the legal transitions are:
130
+
131
+ | From \ Caller | forge (any source) | source-stage (quench / appraise / human-appraise where stageId === item.source) | sort | human-appraise (override authority, any source) |
132
+ |---|---|---|---|---|
133
+ | `open` | -> `actioned` always; -> `wont-fix` only if `item.source` base is `appraise` | — | -> `deadlocked` (if depth >= threshold) | — |
134
+ | `rejected` | -> `actioned` always; -> `wont-fix` only if `item.source` base is `appraise` | — | -> `deadlocked` (if depth >= threshold) | — |
135
+ | `actioned` | — | -> `{resolved, rejected}` | -> `deadlocked` (if depth >= threshold) | -> `{resolved, rejected}` |
136
+ | `wont-fix` | — | -> `{resolved, rejected}` | -> `deadlocked` (if depth >= threshold) | -> `{resolved, rejected}` |
137
+ | `deadlocked` | — | — | — | -> `{resolved, rejected}` |
138
+ | `resolved` | — | — | — | — (terminal) |
139
+
140
+ Notes:
141
+
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 is the only path that can transition out of `deadlocked`.
143
+ - **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; `validation` / `human` tag-based restrictions are legacy and do not apply.
145
+ - **Reason required on** `rejected`, `wont-fix`, `deadlocked`, `resolved`. **Forbidden on** `open`. **Optional on** `actioned` (the code change is the reason).
146
+ - Sort is the only writer of `state: deadlocked`; it writes these via its internal pass, not through the plugin API.
147
+
148
+ This section is the authoritative specification of the feedback state machine.
149
+
150
+ ### Transitions are made via the plugin API
151
+
152
+ No direct yaml editing. Every state change goes through one of:
153
+
154
+ - `foundry_feedback_add` (creates items from `quench`, `appraise`, and `human-appraise`)
155
+ - `foundry_feedback_action` (forge: open/rejected -> actioned)
156
+ - `foundry_feedback_wontfix` (forge: open/rejected -> wont-fix)
157
+ - `foundry_feedback_resolve` (source stage: actioned/wont-fix -> resolved/rejected; or human-appraise deadlock override)
158
+
159
+ ### Persistence
160
+
161
+ Writes are atomic: `io.writeFile(path + '.tmp', body); io.rename(tmp, path)`.
162
+ A crash between the two steps leaves the live file untouched.
163
+
164
+ ## Who writes what
165
+
166
+ | Section | Written by | Updated by |
167
+ |---------|-----------|------------|
168
+ | 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`, `deadlock-iterations`, `models`) | `foundry_orchestrate` (first call of each cycle, internally) | reset on each new cycle |
170
+ | Goal | `foundry_workfile_create` (flow skill) | nobody |
171
+ | Artefacts | the orchestrator's internal finalize step (after forge closes) | `foundry_artefacts_set_status` (orchestrator → `done`/`blocked`) |
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); sort writes only deadlocked snapshots |
173
+ | `WORK.history.yaml` | `foundry_orchestrate` | `foundry_orchestrate` |
174
+
175
+ Note: `foundry_artefacts_add` no longer exists as a public tool — artefact registration is automatic via the orchestrator's internal finalize step, which scans the git diff and registers files matching the output type's `file-patterns` as `draft`.
176
+
177
+ ## WORK.history.yaml
178
+
179
+ A separate file (`WORK.history.yaml`) alongside WORK.md. Append-only log of every stage execution.
180
+
181
+ ```yaml
182
+ - timestamp: "2026-04-17T14:32:01.000Z"
183
+ cycle: write-petition
184
+ stage: forge:draft-petition
185
+ iteration: 1
186
+ comment: Initial petition draft created
187
+ seq: 0
188
+ open_feedback: 0
189
+
190
+ - timestamp: "2026-04-17T14:32:45.000Z"
191
+ cycle: write-petition
192
+ stage: quench:validate-petition
193
+ iteration: 1
194
+ comment: 2 validation issues found
195
+ seq: 1
196
+ open_feedback: 2
197
+
198
+ - timestamp: "2026-04-17T14:33:12.000Z"
199
+ cycle: write-petition
200
+ stage: forge:draft-petition
201
+ iteration: 2
202
+ comment: Addressed 2 validation issues
203
+ seq: 2
204
+ open_feedback: 2
205
+
206
+ - timestamp: "2026-04-17T14:33:30.000Z"
207
+ cycle: write-petition
208
+ stage: quench:validate-petition
209
+ iteration: 2
210
+ comment: Validation passed
211
+ seq: 3
212
+ open_feedback: 0
213
+
214
+ - timestamp: "2026-04-17T14:34:00.000Z"
215
+ cycle: write-petition
216
+ stage: appraise:review-petition
217
+ iteration: 2
218
+ comment: No issues found, cycle complete
219
+ seq: 4
220
+ open_feedback: 0
221
+ ```
222
+
223
+ ### Fields
224
+
225
+ | Field | Type | Required | Notes |
226
+ |-------|------|----------|-------|
227
+ | `cycle` | string | yes | |
228
+ | `stage` | string or literal `sort` | yes | |
229
+ | `iteration` | integer | yes | Count of completed forge stages for the cycle at the time of write |
230
+ | `comment` | string | yes | |
231
+ | `timestamp` | ISO-8601 UTC with ms | yes | |
232
+ | `seq` | integer | yes on write | Monotonic per file; sort tiebreaker for same-ms entries |
233
+ | `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; deadlocked items are counted |
235
+
236
+ ### Rules
237
+
238
+ - Append-only — never edit or delete entries.
239
+ - Every stage produces an entry when it completes.
240
+ - Sort reads this to determine what has happened in the current cycle.
241
+ - Iteration is derived from counting forge entries for the current cycle.
242
+
243
+ ### Who writes
244
+
245
+ History entries are written by `foundry_orchestrate` after each stage closes (via its internal `foundry_history_append` — the tool is not registered publicly). Sub-agents never append history directly.
246
+
247
+ ### Lifecycle
248
+
249
+ `WORK.history.yaml` is tracked in git and committed per-stage on the work
250
+ branch. `foundry_git_finish` deletes it before the squash-merge so the
251
+ history does not leak into the base branch.
252
+
253
+ If the yaml is malformed on read (parse failure or non-array root), the
254
+ flow is marked failed via `markWorkfileFailed` and the error is re-thrown
255
+ to the caller. Mirrors the P0 #3 failed-flow pattern used by the memory
256
+ sync writer.
257
+
258
+ ## Example
259
+
260
+ A complete WORK.md mid-foundry flow:
261
+
262
+ ```markdown
263
+ ---
264
+ flow: make-haiku
265
+ cycle: haiku-creation
266
+ stages: [forge:write-haiku, quench:check-syllables, appraise:evaluate-quality]
267
+ max-iterations: 3
268
+ human-appraise: false
269
+ deadlock-appraise: true
270
+ deadlock-iterations: 5
271
+ ---
272
+
273
+ # Goal
274
+
275
+ Write a haiku about autumn rain. Should evoke loneliness
276
+ and the sound of rain on leaves.
277
+
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
+ ```
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Artefacts table utilities for WORK.md.
3
+ *
4
+ * Parses, adds rows to, and updates status in the markdown artefacts table.
5
+ */
6
+
7
+ // --- Table line classifiers ---
8
+
9
+ function isTableHeader(line) {
10
+ return line.startsWith('| File');
11
+ }
12
+
13
+ function isTableSeparator(line) {
14
+ return line.startsWith('|---');
15
+ }
16
+
17
+ function isTableRow(line) {
18
+ return line.startsWith('|');
19
+ }
20
+
21
+ function parseTableRow(line) {
22
+ const cols = line.split('|').slice(1, -1).map(c => c.trim());
23
+ return cols.length >= 4 ? cols : null;
24
+ }
25
+
26
+ // --- Status validation ---
27
+
28
+ function validateStatus(newStatus) {
29
+ if (newStatus === 'draft') {
30
+ throw new Error('status draft not permitted; artefacts are registered automatically during orchestration');
31
+ }
32
+ if (!['done', 'blocked'].includes(newStatus)) {
33
+ throw new Error(`invalid status: ${newStatus}`);
34
+ }
35
+ }
36
+
37
+ // --- Table boundary detection ---
38
+
39
+ function findTableHeader(lines) {
40
+ for (let i = 0; i < lines.length; i++) {
41
+ if (isTableHeader(lines[i].trim())) return i;
42
+ }
43
+ return -1;
44
+ }
45
+
46
+ function findTableSeparator(lines, afterIdx) {
47
+ for (let i = afterIdx + 1; i < lines.length; i++) {
48
+ if (isTableSeparator(lines[i].trim())) return i;
49
+ }
50
+ return -1;
51
+ }
52
+
53
+ function getTableBounds(lines) {
54
+ const headerIdx = findTableHeader(lines);
55
+ if (headerIdx < 0) return null;
56
+ const sepIdx = findTableSeparator(lines, headerIdx);
57
+ if (sepIdx < 0) return null;
58
+ return { headerIdx, sepIdx };
59
+ }
60
+
61
+ function findTableEnd(lines, startIdx) {
62
+ for (let i = startIdx; i < lines.length; i++) {
63
+ const stripped = lines[i].trim();
64
+ if (!isTableRow(stripped)) return i;
65
+ }
66
+ return lines.length;
67
+ }
68
+
69
+ function formatTableRow(cols) {
70
+ return '| ' + cols.join(' | ') + ' |';
71
+ }
72
+
73
+ /**
74
+ * Parse the artefacts markdown table from text.
75
+ * @param {string} text
76
+ * @returns {Array<{file: string, type: string, cycle: string, status: string}>}
77
+ */
78
+ export function parseArtefactsTable(text) {
79
+ const lines = text.split('\n');
80
+ const bounds = getTableBounds(lines);
81
+ if (!bounds) return [];
82
+
83
+ const artefacts = [];
84
+ const endIdx = findTableEnd(lines, bounds.sepIdx + 1);
85
+
86
+ for (let i = bounds.sepIdx + 1; i < endIdx; i++) {
87
+ const cols = parseTableRow(lines[i].trim());
88
+ if (cols) {
89
+ artefacts.push({
90
+ file: cols[0],
91
+ type: cols[1],
92
+ cycle: cols[2],
93
+ status: cols[3],
94
+ });
95
+ }
96
+ }
97
+
98
+ return artefacts;
99
+ }
100
+
101
+ /**
102
+ * Add a row to the artefacts table.
103
+ * @param {string} text - Full WORK.md text
104
+ * @param {{file: string, type: string, cycle: string, status: string}} row
105
+ * @returns {string} Updated text
106
+ */
107
+ export function addArtefactRow(text, { file, type, cycle, status }) {
108
+ const lines = text.split('\n');
109
+ const bounds = getTableBounds(lines);
110
+
111
+ if (!bounds) {
112
+ throw new Error('Artefacts table not found');
113
+ }
114
+
115
+ const endIdx = findTableEnd(lines, bounds.sepIdx + 1);
116
+ const insertAt = endIdx > bounds.sepIdx + 1 ? endIdx - 1 : bounds.sepIdx;
117
+ const newRow = `| ${file} | ${type} | ${cycle} | ${status} |`;
118
+ lines.splice(insertAt + 1, 0, newRow);
119
+ return lines.join('\n');
120
+ }
121
+
122
+ /**
123
+ * Update the status column for a specific file in the artefacts table.
124
+ * @param {string} text - Full WORK.md text
125
+ * @param {string} file - File name to match
126
+ * @param {string} newStatus - New status value
127
+ * @returns {string} Updated text
128
+ */
129
+ export function setArtefactStatus(text, file, newStatus) {
130
+ validateStatus(newStatus);
131
+
132
+ const lines = text.split('\n');
133
+ const bounds = getTableBounds(lines);
134
+
135
+ if (!bounds) {
136
+ throw new Error(`File not found in artefacts table: ${file}`);
137
+ }
138
+
139
+ const endIdx = findTableEnd(lines, bounds.sepIdx + 1);
140
+
141
+ for (let i = bounds.sepIdx + 1; i < endIdx; i++) {
142
+ const cols = parseTableRow(lines[i].trim());
143
+ if (cols && cols[0] === file) {
144
+ cols[3] = newStatus;
145
+ lines[i] = formatTableRow(cols);
146
+ return lines.join('\n');
147
+ }
148
+ }
149
+
150
+ throw new Error(`File not found in artefacts table: ${file}`);
151
+ }
@@ -0,0 +1,151 @@
1
+ import yaml from 'js-yaml';
2
+ import { memoryPaths } from '../memory/paths.js';
3
+
4
+ /**
5
+ * Extractor Contract
6
+ * ==================
7
+ *
8
+ * An extractor is a project-authored executable (script, binary, etc.) that
9
+ * emits JSONL (newline-delimited JSON) describing entities and edges to upsert
10
+ * into flow memory.
11
+ *
12
+ * Output Format:
13
+ * - One JSON object per line (JSONL/NDJSON format)
14
+ * - Pretty-printed multi-line JSON is NOT supported
15
+ * - Blank lines and lines starting with '#' are ignored
16
+ * - Each object must have a "kind" field: "entity" or "edge"
17
+ *
18
+ * Entity format:
19
+ * {"kind":"entity","type":"<entity-type>","name":"<id>","value":"<string ≤ 4KB>"}
20
+ *
21
+ * Edge format:
22
+ * {"kind":"edge","from":{"type":"...","name":"..."},"edge":"<edge-type>","to":{"type":"...","name":"..."}}
23
+ *
24
+ * Exit codes:
25
+ * - 0 on success
26
+ * - Non-zero on failure (aborts the assay stage)
27
+ *
28
+ * Environment:
29
+ * - Extractors inherit the agent's full environment, including any API tokens
30
+ * or credentials present in the agent process
31
+ * - Extractors are project-authored, committed code; they are trusted paths
32
+ * - Keep environment variable handling internal to extraction logic
33
+ */
34
+
35
+ const IDENT = /^[a-z][a-z0-9_-]*$/;
36
+ const MAX_TIMEOUT_MS = 600_000;
37
+
38
+ function validateTimeoutMs(ms) {
39
+ if (!Number.isFinite(ms) || ms <= 0) throw new Error(`timeout must be a positive number (ms) or duration string`);
40
+ if (ms > MAX_TIMEOUT_MS) throw new Error(`timeout must not exceed 600000ms (10 minutes)`);
41
+ return ms;
42
+ }
43
+
44
+ function parseNumberTimeout(v) {
45
+ return validateTimeoutMs(v);
46
+ }
47
+
48
+ function unitToMs(unit) {
49
+ if (unit === 'ms') return 1;
50
+ if (unit === 's') return 1_000;
51
+ if (unit === 'm') return 60_000;
52
+ throw new Error(`timeout: impossible unit ${unit}`);
53
+ }
54
+
55
+ function parseStringTimeout(v) {
56
+ const trimmed = v.trim();
57
+ const m = trimmed.match(/^(\d+)(ms|s|m)?$/);
58
+ if (!m) throw new Error(`timeout: unrecognised duration '${trimmed}' (expected e.g. "500ms", "30s", "2m")`);
59
+ const n = Number(m[1]);
60
+ const unit = m[2] ?? 'ms';
61
+ const ms = n * unitToMs(unit);
62
+ return validateTimeoutMs(ms);
63
+ }
64
+
65
+ function parseTimeout(v) {
66
+ if (v === undefined || v === null) return 60_000;
67
+ if (typeof v === 'number') return parseNumberTimeout(v);
68
+ if (typeof v !== 'string') throw new Error(`timeout must be a duration string (e.g. "30s") or a number of ms`);
69
+ return parseStringTimeout(v);
70
+ }
71
+
72
+ function stripBom(text) {
73
+ return text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
74
+ }
75
+
76
+ function findFrontmatterEnd(lines) {
77
+ for (let i = 1; i < lines.length; i++) {
78
+ if (lines[i] === '---') return i;
79
+ }
80
+ return -1;
81
+ }
82
+
83
+ function parseFrontmatterYaml(fmText) {
84
+ const fm = yaml.load(fmText) ?? {};
85
+ if (typeof fm !== 'object' || Array.isArray(fm)) throw new Error(`frontmatter must be a mapping`);
86
+ return fm;
87
+ }
88
+
89
+ function splitFrontmatter(text) {
90
+ const stripped = stripBom(text);
91
+ const lines = stripped.split(/\r?\n/);
92
+ if (lines[0] !== '---') throw new Error(`missing frontmatter: file must start with '---'`);
93
+ const end = findFrontmatterEnd(lines);
94
+ if (end === -1) throw new Error(`missing frontmatter: no closing '---'`);
95
+ const fmText = lines.slice(1, end).join('\n');
96
+ const body = lines.slice(end + 1).join('\n').replace(/^\s+/, '');
97
+ const fm = parseFrontmatterYaml(fmText);
98
+ return { fm, body };
99
+ }
100
+
101
+ function validateCommand(fm, name) {
102
+ if (typeof fm.command !== 'string' || !fm.command.trim()) {
103
+ throw new Error(`extractor '${name}': 'command' is required and must be a non-empty string`);
104
+ }
105
+ }
106
+
107
+ function validateMemoryWrite(fm, name) {
108
+ const writeList = fm?.memory?.write;
109
+ if (!Array.isArray(writeList) || writeList.length === 0) {
110
+ throw new Error(`extractor '${name}': 'memory.write' is required and must be a non-empty array of entity type names`);
111
+ }
112
+ return writeList;
113
+ }
114
+
115
+ function validateEntityTypes(writeList, name) {
116
+ for (const t of writeList) {
117
+ if (typeof t !== 'string' || !IDENT.test(t)) {
118
+ throw new Error(`extractor '${name}': memory.write entry '${t}' is not a valid entity type identifier`);
119
+ }
120
+ }
121
+ }
122
+
123
+ export async function loadExtractor(foundryDir, name, io) {
124
+ if (!IDENT.test(name)) throw new Error(`invalid extractor name '${name}' (expected lowercase identifier)`);
125
+ const p = memoryPaths(foundryDir);
126
+ const path = p.extractorFile(name);
127
+ if (!(await io.exists(path))) throw new Error(`extractor not found: ${name} (expected at ${path})`);
128
+ const text = await io.readFile(path);
129
+ const { fm, body } = splitFrontmatter(text);
130
+ validateCommand(fm, name);
131
+ const writeList = validateMemoryWrite(fm, name);
132
+ validateEntityTypes(writeList, name);
133
+ const timeoutMs = parseTimeout(fm.timeout);
134
+ return {
135
+ name,
136
+ command: fm.command,
137
+ memoryWrite: writeList,
138
+ timeoutMs,
139
+ body: body,
140
+ };
141
+ }
142
+
143
+ export async function listExtractors(foundryDir, io) {
144
+ const p = memoryPaths(foundryDir);
145
+ if (!(await io.exists(p.extractorsDir))) return [];
146
+ const entries = await io.readDir(p.extractorsDir);
147
+ return entries
148
+ .filter((f) => f.endsWith('.md'))
149
+ .map((f) => f.slice(0, -3))
150
+ .sort();
151
+ }
@@ -0,0 +1,102 @@
1
+ import { MAX_VALUE_BYTES } from '../memory/validate.js';
2
+
3
+ const ENTITY_FIELDS = new Set(['kind', 'type', 'name', 'value']);
4
+ const EDGE_FIELDS = new Set(['kind', 'from', 'edge', 'to']);
5
+
6
+ function checkFields(obj, allowed, lineNo, kind) {
7
+ for (const k of Object.keys(obj)) {
8
+ if (!allowed.has(k)) {
9
+ throw new Error(`extractor output line ${lineNo}: unknown field '${k}' on ${kind} row`);
10
+ }
11
+ }
12
+ }
13
+
14
+ function req(obj, key, lineNo, kind) {
15
+ if (obj[key] === undefined || obj[key] === null || obj[key] === '') {
16
+ throw new Error(`extractor output line ${lineNo}: ${kind}.${key} is required`);
17
+ }
18
+ }
19
+
20
+ function parseEntityRow(obj, lineNo) {
21
+ checkFields(obj, ENTITY_FIELDS, lineNo, 'entity');
22
+ req(obj, 'type', lineNo, 'entity');
23
+ req(obj, 'name', lineNo, 'entity');
24
+ if (typeof obj.value !== 'string') {
25
+ throw new Error(`extractor output line ${lineNo}: entity.value is required and must be a string`);
26
+ }
27
+ const bytes = Buffer.byteLength(obj.value, 'utf-8');
28
+ if (bytes > MAX_VALUE_BYTES) {
29
+ throw new Error(`extractor output line ${lineNo}: entity.value is ${bytes} bytes (max ${MAX_VALUE_BYTES}, too large)`);
30
+ }
31
+ return { kind: 'entity', type: obj.type, name: obj.name, value: obj.value };
32
+ }
33
+
34
+ function validateRef(obj, fieldName, lineNo) {
35
+ if (!obj[fieldName] || typeof obj[fieldName] !== 'object') {
36
+ throw new Error(`extractor output line ${lineNo}: edge.${fieldName} is required and must be an object {type,name}`);
37
+ }
38
+ req(obj[fieldName], 'type', lineNo, `edge.${fieldName}`);
39
+ req(obj[fieldName], 'name', lineNo, `edge.${fieldName}`);
40
+ }
41
+
42
+ function checkRefSize(ref, fieldName, lineNo) {
43
+ const bytes = Buffer.byteLength(ref.name, 'utf-8');
44
+ if (bytes > MAX_VALUE_BYTES) {
45
+ throw new Error(`extractor output line ${lineNo}: edge.${fieldName}.name is ${bytes} bytes (max ${MAX_VALUE_BYTES}, too large)`);
46
+ }
47
+ }
48
+
49
+ function parseEdgeRow(obj, lineNo) {
50
+ checkFields(obj, EDGE_FIELDS, lineNo, 'edge');
51
+ validateRef(obj, 'from', lineNo);
52
+ validateRef(obj, 'to', lineNo);
53
+ req(obj, 'edge', lineNo, 'edge');
54
+ checkRefSize(obj.from, 'from', lineNo);
55
+ checkRefSize(obj.to, 'to', lineNo);
56
+ return {
57
+ kind: 'edge',
58
+ edge_type: obj.edge,
59
+ from_type: obj.from.type,
60
+ from_name: obj.from.name,
61
+ to_type: obj.to.type,
62
+ to_name: obj.to.name,
63
+ };
64
+ }
65
+
66
+ function isSkippableLine(trimmed) {
67
+ return trimmed === '' || trimmed.startsWith('#');
68
+ }
69
+
70
+ function parseJsonLine(trimmed, lineNo) {
71
+ try {
72
+ return JSON.parse(trimmed);
73
+ } catch (err) {
74
+ throw new Error(`extractor output line ${lineNo}: invalid JSON (${err.message}). Extractors must output one JSON object per line (JSONL/NDJSON format), not pretty-printed multi-line JSON.`, { cause: err });
75
+ }
76
+ }
77
+
78
+ function validateParsedObject(obj, lineNo) {
79
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
80
+ throw new Error(`extractor output line ${lineNo}: expected a JSON object`);
81
+ }
82
+ }
83
+
84
+ function dispatchKind(obj, lineNo) {
85
+ if (obj.kind === 'entity') return parseEntityRow(obj, lineNo);
86
+ if (obj.kind === 'edge') return parseEdgeRow(obj, lineNo);
87
+ throw new Error(`extractor output: unknown kind '${obj.kind}' at line ${lineNo}`);
88
+ }
89
+
90
+ export function parseExtractorOutput(text) {
91
+ if (!text) return [];
92
+ const lines = text.split(/\r?\n/);
93
+ const out = [];
94
+ for (let i = 0; i < lines.length; i++) {
95
+ const trimmed = lines[i].trim();
96
+ if (isSkippableLine(trimmed)) continue;
97
+ const obj = parseJsonLine(trimmed, i + 1);
98
+ validateParsedObject(obj, i + 1);
99
+ out.push(dispatchKind(obj, i + 1));
100
+ }
101
+ return out;
102
+ }