@loomfsm/bundle-code 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +201 -0
  2. package/agents/acceptance.md +141 -0
  3. package/agents/api-contract.md +89 -0
  4. package/agents/architect.md +52 -0
  5. package/agents/challenger-reviewer.md +104 -0
  6. package/agents/classifier.md +74 -0
  7. package/agents/code-analyzer.md +43 -0
  8. package/agents/context-doc-verifier.md +94 -0
  9. package/agents/dependency-auditor.md +42 -0
  10. package/agents/implementer.md +135 -0
  11. package/agents/logic-reviewer.md +132 -0
  12. package/agents/migration.md +55 -0
  13. package/agents/performance.md +95 -0
  14. package/agents/plan-conformance.md +127 -0
  15. package/agents/plan-grounding-check.md +106 -0
  16. package/agents/planner.md +143 -0
  17. package/agents/playwright.md +68 -0
  18. package/agents/research.md +52 -0
  19. package/agents/security.md +88 -0
  20. package/agents/style-reviewer.md +85 -0
  21. package/agents/test.md +206 -0
  22. package/agents/ui-consistency.md +75 -0
  23. package/dist/manifest.d.ts +2 -0
  24. package/dist/manifest.js +34 -0
  25. package/dist/manifest.js.map +1 -0
  26. package/dist/src/bundle.d.ts +2 -0
  27. package/dist/src/bundle.js +424 -0
  28. package/dist/src/bundle.js.map +1 -0
  29. package/dist/src/index.d.ts +5 -0
  30. package/dist/src/index.js +14 -0
  31. package/dist/src/index.js.map +1 -0
  32. package/dist/src/invariants.d.ts +10 -0
  33. package/dist/src/invariants.js +208 -0
  34. package/dist/src/invariants.js.map +1 -0
  35. package/dist/src/policy-resolver.d.ts +2 -0
  36. package/dist/src/policy-resolver.js +65 -0
  37. package/dist/src/policy-resolver.js.map +1 -0
  38. package/dist/src/sandbox-rules.d.ts +2 -0
  39. package/dist/src/sandbox-rules.js +40 -0
  40. package/dist/src/sandbox-rules.js.map +1 -0
  41. package/dist/test/bundle.test.d.ts +1 -0
  42. package/dist/test/bundle.test.js +289 -0
  43. package/dist/test/bundle.test.js.map +1 -0
  44. package/dist/test/sandbox-rules.test.d.ts +1 -0
  45. package/dist/test/sandbox-rules.test.js +73 -0
  46. package/dist/test/sandbox-rules.test.js.map +1 -0
  47. package/knowledge/references/api-design.md +188 -0
  48. package/knowledge/references/arch-patterns.md +106 -0
  49. package/knowledge/references/caching.md +190 -0
  50. package/knowledge/references/concurrency.md +195 -0
  51. package/knowledge/references/db-postgres.md +153 -0
  52. package/knowledge/references/e2e-flutter.md +56 -0
  53. package/knowledge/references/e2e-playwright.md +53 -0
  54. package/knowledge/references/error-handling.md +208 -0
  55. package/knowledge/references/next-app-router.md +231 -0
  56. package/knowledge/references/observability.md +169 -0
  57. package/knowledge/references/optimization-strategy.md +197 -0
  58. package/knowledge/references/perf-flutter.md +62 -0
  59. package/knowledge/references/perf-nestjs.md +59 -0
  60. package/knowledge/references/perf-python.md +50 -0
  61. package/knowledge/references/perf-react.md +52 -0
  62. package/knowledge/references/react19.md +176 -0
  63. package/knowledge/references/redis.md +175 -0
  64. package/knowledge/references/security-backend.md +219 -0
  65. package/knowledge/references/test-flutter.md +65 -0
  66. package/knowledge/references/test-nestjs.md +82 -0
  67. package/knowledge/references/test-python.md +76 -0
  68. package/knowledge/references/test-react.md +66 -0
  69. package/knowledge/references/test-strategy.md +175 -0
  70. package/knowledge/references/ui-flutter.md +56 -0
  71. package/knowledge/references/ui-web.md +51 -0
  72. package/package.json +34 -0
  73. package/schemas/agent-feedback.schema.json +80 -0
  74. package/schemas/category-vocab.json +170 -0
  75. package/schemas/classifier-output.schema.json +53 -0
  76. package/schemas/finding.schema.json +92 -0
  77. package/schemas/pipeline-state.schema.json +238 -0
  78. package/schemas/reviewer-output.schema.json +62 -0
  79. package/schemas/state-extension.schema.json +53 -0
  80. package/schemas/validator-output.schema.json +48 -0
  81. package/stack-candidates.yaml +248 -0
@@ -0,0 +1,208 @@
1
+ // Code-bundle domain invariants + the deterministic safety floor.
2
+ //
3
+ // These run inside the same `runInvariants(tx)` call as the substrate's
4
+ // own state-shape rules; the substrate does not care whether a violation
5
+ // came from a generic rule or one of these. A non-null return rolls the
6
+ // commit back. Each invariant is a pure function over the narrow state
7
+ // projection plus declared `reads` metadata — no clock, no IO; replay
8
+ // re-runs them against the stored state and must reach the same verdict.
9
+ //
10
+ // Numbering: the substrate owns the low range; bundle rules start at 101.
11
+ // The four `INV_CODE_*` rules encode the code domain (a plan gate implies
12
+ // planning is closed, sacred tests can't be silently rewritten, an
13
+ // acceptance PASS can't coexist with open blocking findings). The three
14
+ // floor rules (`INV_lint_clean` / `INV_tests_pass` / `INV_typecheck_clean`)
15
+ // are the deterministic boundary that makes a fully-autonomous final gate
16
+ // defensible — they READ a status field; the deterministic Step that
17
+ // WRITES it (a shell-out to lint / test / typecheck) is the writer the
18
+ // floor depends on. The floor only engages when the final role's policy
19
+ // is literally `auto`; under the honest baseline (`on-blockers`) the
20
+ // human-or-blocker gate is the boundary and the floor stays dormant.
21
+ // Local typed-identity helper mirroring the substrate's own invariant
22
+ // constructor: bind the `reads` metadata onto a pure verdict function.
23
+ // The substrate does not export its private constructor, so the bundle
24
+ // carries its own one-liner.
25
+ function defineInvariant(reads, fn) {
26
+ return Object.assign(fn, { reads });
27
+ }
28
+ // A gate counts as "approved" whether a human approved it or a policy
29
+ // auto-approved it — both close the checkpoint.
30
+ function isApproved(status) {
31
+ return status === "approved" || status === "auto-approved";
32
+ }
33
+ function phaseStatus(state, name) {
34
+ const row = state.phases.find((p) => p.name === name);
35
+ return row ? row.status : null;
36
+ }
37
+ // Narrow a `bundle_state` sub-object's `status` field to `"ok"`. The
38
+ // column is an opaque JSON blob on the snapshot, so every read is an
39
+ // unknown that must be shape-checked before use.
40
+ function statusField(value) {
41
+ if (typeof value !== "object" || value === null)
42
+ return null;
43
+ const s = value.status;
44
+ return typeof s === "string" ? s : null;
45
+ }
46
+ function bundleStateField(state, key) {
47
+ return state.bundle_state?.[key];
48
+ }
49
+ // ============================================================================
50
+ // Domain rules — INV_CODE_101..104
51
+ // ============================================================================
52
+ // Once the plan gate is approved, the planning phase must be closed.
53
+ export const invCode101 = defineInvariant(["gates", "phases"], (state) => {
54
+ if (!isApproved(state.gates["gate-plan"]?.status))
55
+ return null;
56
+ const planning = phaseStatus(state, "planning");
57
+ if (planning === null)
58
+ return null; // no planning row yet — nothing to assert
59
+ if (planning === "completed" || planning === "skipped")
60
+ return null;
61
+ return {
62
+ code: "INV_CODE_101",
63
+ message: `gate-plan approved but planning phase is '${planning}'`,
64
+ detail: { planning_status: planning },
65
+ };
66
+ });
67
+ // Once the final gate is approved, both the implementation and validation
68
+ // phases must be completed.
69
+ export const invCode102 = defineInvariant(["gates", "phases"], (state) => {
70
+ if (!isApproved(state.gates["gate-final"]?.status))
71
+ return null;
72
+ const impl = phaseStatus(state, "implementation");
73
+ const validation = phaseStatus(state, "validation");
74
+ if (impl === "completed" && validation === "completed")
75
+ return null;
76
+ return {
77
+ code: "INV_CODE_102",
78
+ message: `gate-final approved but implementation='${impl ?? "missing"}' validation='${validation ?? "missing"}'`,
79
+ detail: { implementation_status: impl, validation_status: validation },
80
+ };
81
+ });
82
+ // Sacred tests: if the implementer modified test files, the final gate may
83
+ // only close with explicit HUMAN approval — a policy must not silently
84
+ // auto-approve work that rewrote the tests it is judged against.
85
+ export const invCode103 = defineInvariant(["bundle_state.test_files_modified_by_implementer", "gates"], (state) => {
86
+ const modified = bundleStateField(state, "test_files_modified_by_implementer");
87
+ if (!Array.isArray(modified) || modified.length === 0)
88
+ return null;
89
+ const g = state.gates["gate-final"];
90
+ if (!g || !isApproved(g.status))
91
+ return null;
92
+ if (g.decided_by === "human")
93
+ return null;
94
+ return {
95
+ code: "INV_CODE_103",
96
+ message: `implementer modified ${modified.length} test file(s); final gate must be human-approved, not '${g.decided_by}'`,
97
+ detail: { decided_by: g.decided_by, modified_count: modified.length },
98
+ };
99
+ });
100
+ // An acceptance PASS cannot coexist with open blocking findings from
101
+ // implementation-phase reviewers at the latest review iteration.
102
+ export const invCode104 = defineInvariant(["agent_verdicts"], (state) => {
103
+ const acceptance = state.agent_verdicts.find((v) => v.agent === "acceptance" && v.phase === "validation");
104
+ if (!acceptance)
105
+ return null;
106
+ if (acceptance.verdict !== "PASS" && acceptance.verdict !== "PASS_WITH_WARNINGS") {
107
+ return null;
108
+ }
109
+ const implEntries = state.agent_verdicts.filter((v) => v.phase === "implementation" && v.agent !== "acceptance");
110
+ if (implEntries.length === 0)
111
+ return null;
112
+ const latestIter = implEntries.reduce((m, v) => Math.max(m, v.iteration ?? 1), 0);
113
+ const offenders = implEntries.filter((v) => v.iteration === latestIter && v.blocking_issues > 0);
114
+ if (offenders.length === 0)
115
+ return null;
116
+ const sum = offenders.reduce((s, v) => s + v.blocking_issues, 0);
117
+ return {
118
+ code: "INV_CODE_104",
119
+ message: `acceptance.verdict='${acceptance.verdict}' but ${sum} open blocking finding(s) from impl-phase reviewers at iteration=${latestIter}`,
120
+ detail: {
121
+ offenders: offenders.map((v) => ({
122
+ agent: v.agent,
123
+ iteration: v.iteration,
124
+ blocking_issues: v.blocking_issues,
125
+ })),
126
+ },
127
+ };
128
+ });
129
+ // ============================================================================
130
+ // Safety floor — only engages when the final role's policy is `auto`
131
+ // ============================================================================
132
+ // True only at the moment a fully-autonomous final gate is being approved.
133
+ // Under the `on-blockers` baseline the gate's blocker check is the boundary
134
+ // and the floor stays dormant, so the substrate never has to write the
135
+ // status fields these rules read until a deployment opts into `auto`.
136
+ function atFinalAutoApprove(state) {
137
+ if (state.gate_policies["final"] !== "auto")
138
+ return false;
139
+ return isApproved(state.gates["gate-final"]?.status);
140
+ }
141
+ function floorViolation(code, field, status, value) {
142
+ return {
143
+ code,
144
+ message: `${field} must pass before final auto-approve (status=${status ?? "missing"})`,
145
+ detail: { [field]: value },
146
+ };
147
+ }
148
+ export const invLintClean = defineInvariant(["gate_policies", "gates", "bundle_state.lint_result"], (state) => {
149
+ if (!atFinalAutoApprove(state))
150
+ return null;
151
+ const lint = bundleStateField(state, "lint_result");
152
+ const status = statusField(lint);
153
+ if (status === "ok")
154
+ return null;
155
+ return floorViolation("INV_lint_clean", "lint_result", status, lint);
156
+ });
157
+ export const invTestsPass = defineInvariant(["gate_policies", "gates", "bundle_state.test_run"], (state) => {
158
+ if (!atFinalAutoApprove(state))
159
+ return null;
160
+ const tests = bundleStateField(state, "test_run");
161
+ const status = statusField(tests);
162
+ if (status === "ok")
163
+ return null;
164
+ return floorViolation("INV_tests_pass", "test_run", status, tests);
165
+ });
166
+ export const invTypecheckClean = defineInvariant(["gate_policies", "gates", "bundle_state.typecheck"], (state) => {
167
+ if (!atFinalAutoApprove(state))
168
+ return null;
169
+ const tc = bundleStateField(state, "typecheck");
170
+ const status = statusField(tc);
171
+ if (status === "ok")
172
+ return null;
173
+ return floorViolation("INV_typecheck_clean", "typecheck", status, tc);
174
+ });
175
+ // Bridge to the loader's auto-policy completeness gate. When a role's
176
+ // default (or per-task override) resolves to `auto`, the loader looks for
177
+ // an invariant whose FUNCTION NAME is `INV_safety_floor_<role>` — the
178
+ // single registered floor for that role. This composite is that anchor for
179
+ // the `final` role: it runs the three deterministic checks above and
180
+ // surfaces the first failure, so a deployment can flip `final` to `auto`
181
+ // and load cleanly, with the floor genuinely enforcing on every
182
+ // auto-approve. The function's `name` (not its violation code) is what the
183
+ // loader matches.
184
+ const safetyFloorFinalImpl = defineInvariant([
185
+ "gate_policies",
186
+ "gates",
187
+ "bundle_state.lint_result",
188
+ "bundle_state.test_run",
189
+ "bundle_state.typecheck",
190
+ ], (state, snapshots) => invLintClean(state, snapshots) ??
191
+ invTestsPass(state, snapshots) ??
192
+ invTypecheckClean(state, snapshots));
193
+ Object.defineProperty(safetyFloorFinalImpl, "name", {
194
+ value: "INV_safety_floor_final",
195
+ });
196
+ export const invSafetyFloorFinal = safetyFloorFinalImpl;
197
+ // The full domain + floor set the bundle registers.
198
+ export const codeBundleInvariants = [
199
+ invCode101,
200
+ invCode102,
201
+ invCode103,
202
+ invCode104,
203
+ invLintClean,
204
+ invTestsPass,
205
+ invTypecheckClean,
206
+ invSafetyFloorFinal,
207
+ ];
208
+ //# sourceMappingURL=invariants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invariants.js","sourceRoot":"","sources":["../../src/invariants.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AACxE,uEAAuE;AACvE,sEAAsE;AACtE,yEAAyE;AACzE,EAAE;AACF,0EAA0E;AAC1E,0EAA0E;AAC1E,mEAAmE;AACnE,wEAAwE;AACxE,4EAA4E;AAC5E,0EAA0E;AAC1E,qEAAqE;AACrE,uEAAuE;AACvE,wEAAwE;AACxE,qEAAqE;AACrE,qEAAqE;AASrE,sEAAsE;AACtE,uEAAuE;AACvE,uEAAuE;AACvE,6BAA6B;AAC7B,SAAS,eAAe,CACtB,KAAwB,EACxB,EAA4E;IAE5E,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;AACtC,CAAC;AAED,sEAAsE;AACtE,gDAAgD;AAChD,SAAS,UAAU,CAAC,MAA0B;IAC5C,OAAO,MAAM,KAAK,UAAU,IAAI,MAAM,KAAK,eAAe,CAAC;AAC7D,CAAC;AAED,SAAS,WAAW,CAAC,KAAsB,EAAE,IAAY;IACvD,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IACtD,OAAO,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AACjC,CAAC;AAED,qEAAqE;AACrE,qEAAqE;AACrE,iDAAiD;AACjD,SAAS,WAAW,CAAC,KAAc;IACjC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC7D,MAAM,CAAC,GAAI,KAA8B,CAAC,MAAM,CAAC;IACjD,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1C,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAsB,EAAE,GAAW;IAC3D,OAAO,KAAK,CAAC,YAAY,EAAE,CAAC,GAAG,CAAC,CAAC;AACnC,CAAC;AAED,+EAA+E;AAC/E,mCAAmC;AACnC,+EAA+E;AAE/E,qEAAqE;AACrE,MAAM,CAAC,MAAM,UAAU,GAAc,eAAe,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE;IAClF,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/D,MAAM,QAAQ,GAAG,WAAW,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;IAChD,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC,CAAC,0CAA0C;IAC9E,IAAI,QAAQ,KAAK,WAAW,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACpE,OAAO;QACL,IAAI,EAAE,cAAc;QACpB,OAAO,EAAE,6CAA6C,QAAQ,GAAG;QACjE,MAAM,EAAE,EAAE,eAAe,EAAE,QAAQ,EAAE;KACtC,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,0EAA0E;AAC1E,4BAA4B;AAC5B,MAAM,CAAC,MAAM,UAAU,GAAc,eAAe,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE;IAClF,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAChE,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC;IAClD,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;IACpD,IAAI,IAAI,KAAK,WAAW,IAAI,UAAU,KAAK,WAAW;QAAE,OAAO,IAAI,CAAC;IACpE,OAAO;QACL,IAAI,EAAE,cAAc;QACpB,OAAO,EAAE,2CAA2C,IAAI,IAAI,SAAS,iBAAiB,UAAU,IAAI,SAAS,GAAG;QAChH,MAAM,EAAE,EAAE,qBAAqB,EAAE,IAAI,EAAE,iBAAiB,EAAE,UAAU,EAAE;KACvE,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,2EAA2E;AAC3E,uEAAuE;AACvE,iEAAiE;AACjE,MAAM,CAAC,MAAM,UAAU,GAAc,eAAe,CAClD,CAAC,iDAAiD,EAAE,OAAO,CAAC,EAC5D,CAAC,KAAK,EAAE,EAAE;IACR,MAAM,QAAQ,GAAG,gBAAgB,CAAC,KAAK,EAAE,oCAAoC,CAAC,CAAC;IAC/E,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnE,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IACpC,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7C,IAAI,CAAC,CAAC,UAAU,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1C,OAAO;QACL,IAAI,EAAE,cAAc;QACpB,OAAO,EAAE,wBAAwB,QAAQ,CAAC,MAAM,0DAA0D,CAAC,CAAC,UAAU,GAAG;QACzH,MAAM,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,cAAc,EAAE,QAAQ,CAAC,MAAM,EAAE;KACtE,CAAC;AACJ,CAAC,CACF,CAAC;AAEF,qEAAqE;AACrE,iEAAiE;AACjE,MAAM,CAAC,MAAM,UAAU,GAAc,eAAe,CAAC,CAAC,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE;IACjF,MAAM,UAAU,GAAG,KAAK,CAAC,cAAc,CAAC,IAAI,CAC1C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,YAAY,IAAI,CAAC,CAAC,KAAK,KAAK,YAAY,CAC5D,CAAC;IACF,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAC7B,IAAI,UAAU,CAAC,OAAO,KAAK,MAAM,IAAI,UAAU,CAAC,OAAO,KAAK,oBAAoB,EAAE,CAAC;QACjF,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,WAAW,GAAG,KAAK,CAAC,cAAc,CAAC,MAAM,CAC7C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,gBAAgB,IAAI,CAAC,CAAC,KAAK,KAAK,YAAY,CAChE,CAAC;IACF,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAClF,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,CAClC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,UAAU,IAAI,CAAC,CAAC,eAAe,GAAG,CAAC,CAC3D,CAAC;IACF,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACxC,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC;IACjE,OAAO;QACL,IAAI,EAAE,cAAc;QACpB,OAAO,EAAE,uBAAuB,UAAU,CAAC,OAAO,SAAS,GAAG,oEAAoE,UAAU,EAAE;QAC9I,MAAM,EAAE;YACN,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC/B,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,SAAS,EAAE,CAAC,CAAC,SAAS;gBACtB,eAAe,EAAE,CAAC,CAAC,eAAe;aACnC,CAAC,CAAC;SACJ;KACF,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,+EAA+E;AAC/E,qEAAqE;AACrE,+EAA+E;AAE/E,2EAA2E;AAC3E,4EAA4E;AAC5E,uEAAuE;AACvE,sEAAsE;AACtE,SAAS,kBAAkB,CAAC,KAAsB;IAChD,IAAI,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1D,OAAO,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC,CAAC;AACvD,CAAC;AAED,SAAS,cAAc,CACrB,IAAY,EACZ,KAAa,EACb,MAAqB,EACrB,KAAc;IAEd,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,KAAK,gDAAgD,MAAM,IAAI,SAAS,GAAG;QACvF,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE;KAC3B,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,YAAY,GAAc,eAAe,CACpD,CAAC,eAAe,EAAE,OAAO,EAAE,0BAA0B,CAAC,EACtD,CAAC,KAAK,EAAE,EAAE;IACR,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5C,MAAM,IAAI,GAAG,gBAAgB,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;IACpD,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACjC,OAAO,cAAc,CAAC,gBAAgB,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;AACvE,CAAC,CACF,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAc,eAAe,CACpD,CAAC,eAAe,EAAE,OAAO,EAAE,uBAAuB,CAAC,EACnD,CAAC,KAAK,EAAE,EAAE;IACR,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5C,MAAM,KAAK,GAAG,gBAAgB,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;IAClD,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACjC,OAAO,cAAc,CAAC,gBAAgB,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;AACrE,CAAC,CACF,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAc,eAAe,CACzD,CAAC,eAAe,EAAE,OAAO,EAAE,wBAAwB,CAAC,EACpD,CAAC,KAAK,EAAE,EAAE;IACR,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5C,MAAM,EAAE,GAAG,gBAAgB,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAC/B,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACjC,OAAO,cAAc,CAAC,qBAAqB,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;AACxE,CAAC,CACF,CAAC;AAEF,sEAAsE;AACtE,0EAA0E;AAC1E,sEAAsE;AACtE,2EAA2E;AAC3E,qEAAqE;AACrE,yEAAyE;AACzE,gEAAgE;AAChE,2EAA2E;AAC3E,kBAAkB;AAClB,MAAM,oBAAoB,GAAG,eAAe,CAC1C;IACE,eAAe;IACf,OAAO;IACP,0BAA0B;IAC1B,uBAAuB;IACvB,wBAAwB;CACzB,EACD,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,CACnB,YAAY,CAAC,KAAK,EAAE,SAAS,CAAC;IAC9B,YAAY,CAAC,KAAK,EAAE,SAAS,CAAC;IAC9B,iBAAiB,CAAC,KAAK,EAAE,SAAS,CAAC,CACtC,CAAC;AACF,MAAM,CAAC,cAAc,CAAC,oBAAoB,EAAE,MAAM,EAAE;IAClD,KAAK,EAAE,wBAAwB;CAChC,CAAC,CAAC;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAc,oBAAoB,CAAC;AAEnE,oDAAoD;AACpD,MAAM,CAAC,MAAM,oBAAoB,GAAgB;IAC/C,UAAU;IACV,UAAU;IACV,UAAU;IACV,UAAU;IACV,YAAY;IACZ,YAAY;IACZ,iBAAiB;IACjB,mBAAmB;CACpB,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { GatePolicyResolver } from "@loomfsm/kernel";
2
+ export declare const codePolicyResolver: GatePolicyResolver;
@@ -0,0 +1,65 @@
1
+ // Code-bundle gate-policy resolver.
2
+ //
3
+ // The substrate's `on-blockers` / `auto` factories delegate the domain
4
+ // decision to this function: given a clean-enough state, what does the
5
+ // code domain want a gate to do? It is a pure function over the narrow
6
+ // state projection plus the policy context's pre-materialized accessors —
7
+ // no clock, no LLM call, no network. The substrate relies on that purity
8
+ // to replay a gate decision deterministically.
9
+ //
10
+ // Three roles, three postures:
11
+ // - classify: trust the classifier; auto-approve.
12
+ // - plan: auto-reject (revise) while planning carries open blockers,
13
+ // else auto-approve.
14
+ // - final: auto-reject (revise) if acceptance failed or any blocking
15
+ // finding is still open, else auto-approve.
16
+ //
17
+ // `counts_against_replan_cap` is set on the auto-reject paths so a stuck
18
+ // revise loop is bounded by the substrate's replan budget rather than
19
+ // spinning forever.
20
+ function renderPlanFeedback(blockers) {
21
+ return `Plan has ${blockers} open blocking finding(s). Revise the plan to resolve them before implementation.`;
22
+ }
23
+ function renderFinalFeedback(acceptanceFailed, openBlockers) {
24
+ const parts = [];
25
+ if (acceptanceFailed)
26
+ parts.push("acceptance verdict is not a PASS");
27
+ if (openBlockers > 0)
28
+ parts.push(`${openBlockers} open blocking finding(s)`);
29
+ return `Final checks did not clear: ${parts.join("; ")}. Address these and resubmit.`;
30
+ }
31
+ export const codePolicyResolver = (state, role, ctx) => {
32
+ if (role === "classify") {
33
+ return { type: "auto-approve", reason: "code: classify trust" };
34
+ }
35
+ if (role === "plan") {
36
+ const planBlockers = ctx.findings.countBlocking({ phase: "planning" });
37
+ if (planBlockers > 0) {
38
+ return {
39
+ type: "auto-reject",
40
+ reason: `code: ${planBlockers} blocking finding(s) in planning`,
41
+ reject_intent: "revise",
42
+ feedback: renderPlanFeedback(planBlockers),
43
+ counts_against_replan_cap: true,
44
+ };
45
+ }
46
+ return { type: "auto-approve", reason: "code: plan clean" };
47
+ }
48
+ if (role === "final") {
49
+ const verdict = ctx.latest_verdict(state, "acceptance");
50
+ const acceptanceFailed = verdict?.verdict === "FAIL";
51
+ const openBlockers = ctx.findings.countBlocking({});
52
+ if (acceptanceFailed || openBlockers > 0) {
53
+ return {
54
+ type: "auto-reject",
55
+ reason: "code: final safety floor",
56
+ reject_intent: "revise",
57
+ feedback: renderFinalFeedback(acceptanceFailed, openBlockers),
58
+ counts_against_replan_cap: true,
59
+ };
60
+ }
61
+ return { type: "auto-approve", reason: "code: final clean" };
62
+ }
63
+ return { type: "human-required", reason: `code: unknown role '${role}'` };
64
+ };
65
+ //# sourceMappingURL=policy-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-resolver.js","sourceRoot":"","sources":["../../src/policy-resolver.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,EAAE;AACF,uEAAuE;AACvE,uEAAuE;AACvE,uEAAuE;AACvE,0EAA0E;AAC1E,yEAAyE;AACzE,+CAA+C;AAC/C,EAAE;AACF,+BAA+B;AAC/B,oDAAoD;AACpD,2EAA2E;AAC3E,mCAAmC;AACnC,0EAA0E;AAC1E,0DAA0D;AAC1D,EAAE;AACF,yEAAyE;AACzE,sEAAsE;AACtE,oBAAoB;AAUpB,SAAS,kBAAkB,CAAC,QAAgB;IAC1C,OAAO,YAAY,QAAQ,mFAAmF,CAAC;AACjH,CAAC;AAED,SAAS,mBAAmB,CAC1B,gBAAyB,EACzB,YAAoB;IAEpB,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,gBAAgB;QAAE,KAAK,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;IACrE,IAAI,YAAY,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,YAAY,2BAA2B,CAAC,CAAC;IAC7E,OAAO,+BAA+B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,+BAA+B,CAAC;AACxF,CAAC;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAuB,CACpD,KAAsB,EACtB,IAAc,EACd,GAAkB,EACA,EAAE;IACpB,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QACxB,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;IAClE,CAAC;IAED,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,MAAM,YAAY,GAAG,GAAG,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;QACvE,IAAI,YAAY,GAAG,CAAC,EAAE,CAAC;YACrB,OAAO;gBACL,IAAI,EAAE,aAAa;gBACnB,MAAM,EAAE,SAAS,YAAY,kCAAkC;gBAC/D,aAAa,EAAE,QAAQ;gBACvB,QAAQ,EAAE,kBAAkB,CAAC,YAAY,CAAC;gBAC1C,yBAAyB,EAAE,IAAI;aAChC,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC9D,CAAC;IAED,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACrB,MAAM,OAAO,GAAG,GAAG,CAAC,cAAc,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;QACxD,MAAM,gBAAgB,GAAG,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;QACrD,MAAM,YAAY,GAAG,GAAG,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;QACpD,IAAI,gBAAgB,IAAI,YAAY,GAAG,CAAC,EAAE,CAAC;YACzC,OAAO;gBACL,IAAI,EAAE,aAAa;gBACnB,MAAM,EAAE,0BAA0B;gBAClC,aAAa,EAAE,QAAQ;gBACvB,QAAQ,EAAE,mBAAmB,CAAC,gBAAgB,EAAE,YAAY,CAAC;gBAC7D,yBAAyB,EAAE,IAAI;aAChC,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC/D,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,uBAAuB,IAAI,GAAG,EAAE,CAAC;AAC5E,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { SensitivePathRules } from "@loomfsm/kernel";
2
+ export declare const CODE_BUNDLE_SENSITIVE_PATH_RULES: SensitivePathRules;
@@ -0,0 +1,40 @@
1
+ // Dev-ecosystem sensitive-path rules contributed by the code bundle.
2
+ //
3
+ // The substrate's path-discipline floor is deliberately domain-neutral —
4
+ // it knows about universally-sensitive credential stores (`~/.ssh`,
5
+ // `.env`, cloud credential dirs) but NOT about the secret files a
6
+ // software project carries. Those belong to whoever owns the domain.
7
+ //
8
+ // This ruleset is the code bundle's contribution, merged onto the kernel
9
+ // floor via `mergeSensitivePathRules` when the per-task tool context is
10
+ // assembled. Two rings, same shape the substrate uses:
11
+ // - `dirs` are matched as substrings of the resolved path, so each token
12
+ // is dot/slash-anchored to avoid colliding with an ordinary project
13
+ // folder of the same name.
14
+ // - `filePatterns` are RegExps tested against the resolved path, anchored
15
+ // on a segment boundary so a leading-dot secret name trips but an
16
+ // unrelated longer name does not.
17
+ //
18
+ // Anti-typo guard, not an exfil boundary: a motivated caller can still
19
+ // reach an in-project file whose name dodges every pattern. Process-level
20
+ // isolation is the real boundary; this is the cheap inner ring that stops
21
+ // an LLM-confabulated or injection-supplied path from reading a package
22
+ // registry token or an infra credential by accident.
23
+ export const CODE_BUNDLE_SENSITIVE_PATH_RULES = {
24
+ dirs: [
25
+ "/.kube/", // kubeconfig + cluster client certs
26
+ "/.docker/", // registry auth (config.json holds base64 creds)
27
+ "/.config/gh/", // GitHub CLI host tokens
28
+ ],
29
+ filePatterns: [
30
+ /(^|\/)\.npmrc$/, // npm registry auth token (_authToken)
31
+ /(^|\/)\.pypirc$/, // PyPI upload credentials
32
+ /(^|\/)[^/]*\.tfvars$/, // Terraform variables — terraform.tfvars and friends carry secrets
33
+ /kubectl[/_]?config/, // a kubectl config dropped outside ~/.kube
34
+ // `.pgpass` is also on the kernel floor; carried here too so the code
35
+ // bundle's ruleset reads as a complete dev-ecosystem set on its own.
36
+ // The merge is additive, so the duplicate is a harmless second check.
37
+ /(^|\/)\.pgpass$/,
38
+ ],
39
+ };
40
+ //# sourceMappingURL=sandbox-rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox-rules.js","sourceRoot":"","sources":["../../src/sandbox-rules.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,EAAE;AACF,yEAAyE;AACzE,oEAAoE;AACpE,kEAAkE;AAClE,qEAAqE;AACrE,EAAE;AACF,yEAAyE;AACzE,wEAAwE;AACxE,uDAAuD;AACvD,2EAA2E;AAC3E,wEAAwE;AACxE,+BAA+B;AAC/B,4EAA4E;AAC5E,sEAAsE;AACtE,sCAAsC;AACtC,EAAE;AACF,uEAAuE;AACvE,0EAA0E;AAC1E,0EAA0E;AAC1E,wEAAwE;AACxE,qDAAqD;AAIrD,MAAM,CAAC,MAAM,gCAAgC,GAAuB;IAClE,IAAI,EAAE;QACJ,SAAS,EAAE,oCAAoC;QAC/C,WAAW,EAAE,iDAAiD;QAC9D,cAAc,EAAE,yBAAyB;KAC1C;IACD,YAAY,EAAE;QACZ,gBAAgB,EAAE,uCAAuC;QACzD,iBAAiB,EAAE,0BAA0B;QAC7C,sBAAsB,EAAE,mEAAmE;QAC3F,oBAAoB,EAAE,2CAA2C;QACjE,sEAAsE;QACtE,qEAAqE;QACrE,sEAAsE;QACtE,iBAAiB;KAClB;CACF,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,289 @@
1
+ import assert from "node:assert/strict";
2
+ import { existsSync, mkdtempSync, rmSync, statSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { afterEach, beforeEach, describe, it } from "node:test";
7
+ import { KernelError, buildPrompt, captureNow, closeDb, loadBundle, reconcileExtensions, } from "@loomfsm/kernel";
8
+ import codeBundle from "../src/bundle.js";
9
+ import codeManifest from "../manifest.js";
10
+ // The compiled test lives at dist/test/; the package root (where agents/,
11
+ // schemas/, src/ and manifest.ts sit) is two levels up.
12
+ const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
13
+ function freshProject() {
14
+ return mkdtempSync(join(tmpdir(), "loom-bundle-code-"));
15
+ }
16
+ function cleanup(dir) {
17
+ try {
18
+ closeDb(dir);
19
+ }
20
+ catch {
21
+ /* may already be closed */
22
+ }
23
+ rmSync(dir, { recursive: true, force: true });
24
+ }
25
+ function shuttleStub(name = "claude-code-shuttle") {
26
+ return {
27
+ name,
28
+ capabilities: { execution: "shuttle", idempotent_spawn: true, reports_usage: true },
29
+ async spawn() {
30
+ throw new Error("stub — spawn must not run in loader tests");
31
+ },
32
+ };
33
+ }
34
+ async function installManifest(dir, now) {
35
+ await reconcileExtensions({
36
+ manifests: [{ path: "/fixture/manifest.json", raw: codeManifest }],
37
+ project_dir: dir,
38
+ now,
39
+ });
40
+ }
41
+ // ============================================================================
42
+ // Happy path — the real bundle registers without error
43
+ // ============================================================================
44
+ describe("@loomfsm/bundle-code — loadBundle", () => {
45
+ let projectDir;
46
+ beforeEach(() => {
47
+ projectDir = freshProject();
48
+ });
49
+ afterEach(() => cleanup(projectDir));
50
+ it("registers the migrated manifest + bundle into a populated Registry", async () => {
51
+ const now = captureNow();
52
+ await installManifest(projectDir, now);
53
+ const registry = await loadBundle({
54
+ bundle: codeBundle,
55
+ bundle_source_dir: PKG_ROOT,
56
+ project_dir: projectDir,
57
+ providers: [shuttleStub()],
58
+ now,
59
+ });
60
+ // 21 canonical agents (the source's 24 minus the three CC-harness
61
+ // trigger agents).
62
+ assert.equal(registry.agents.size, 21);
63
+ // Every agent's `.md` is read off disk into the prompt map at load.
64
+ assert.equal(registry.prompts?.size, 21);
65
+ assert.ok((registry.prompts?.get("classifier")?.body.length ?? 0) > 0);
66
+ assert.equal(registry.flows.size, 3);
67
+ assert.deepEqual(registry.flows.get("medium"), [
68
+ "initialize", "classify", "classify-agent", "gate-classify",
69
+ "enrich", "plan", "plan-review", "gate-plan",
70
+ "git-stash", "implement", "git-diff", "pre-review", "review",
71
+ "reconcile", "iterate", "final-checks", "test-verify",
72
+ "gate-final", "finalize",
73
+ ]);
74
+ // Two post-commit observers, eight domain + floor invariants.
75
+ assert.equal(registry.hooks.length, 2);
76
+ assert.equal(registry.invariants.length, 8);
77
+ // Vocabulary merged the bundle's error_classes onto the kernel set.
78
+ assert.ok(registry.vocabularies.error_classes.has("impl-blockers"));
79
+ assert.ok(registry.vocabularies.gate_roles.has("classify"));
80
+ });
81
+ it("materializes the declared spawn-context assets and injects them into the classifier prompt", async () => {
82
+ const now = captureNow();
83
+ await installManifest(projectDir, now);
84
+ const registry = await loadBundle({
85
+ bundle: codeBundle,
86
+ bundle_source_dir: PKG_ROOT,
87
+ project_dir: projectDir,
88
+ providers: [shuttleStub()],
89
+ now,
90
+ });
91
+ // The two declared assets materialize, in declaration order, scoped to
92
+ // the classifier.
93
+ const assets = registry.context_assets ?? [];
94
+ assert.equal(assets.length, 2);
95
+ const refs = assets.find((a) => a.heading === "Refs catalog");
96
+ const stack = assets.find((a) => a.heading === "Stack candidate registry");
97
+ assert.ok(refs !== undefined, "refs catalog asset materialized");
98
+ assert.ok(stack !== undefined, "stack registry asset materialized");
99
+ assert.deepEqual(refs.agents, ["classifier"]);
100
+ // The catalog lists real reference filenames (the field that hallucinated
101
+ // when no catalog was supplied).
102
+ assert.ok(refs.body.includes("FILE: knowledge/references/api-design.md"));
103
+ assert.ok(refs.body.includes("FILE: knowledge/references/security-backend.md"));
104
+ // The stack registry inlines the real candidate file.
105
+ assert.ok(stack.body.includes("```yaml"));
106
+ // End-to-end: the classifier's spawn prompt carries both, under their
107
+ // bundle headings; a non-consuming agent's prompt does not.
108
+ const classifierAgent = registry.agents.get("classifier");
109
+ assert.ok(classifierAgent !== undefined);
110
+ const classifierPrompt = buildPrompt(makeClassifyState(), classifierAgent, registry);
111
+ assert.ok(classifierPrompt.includes("### Refs catalog"));
112
+ assert.ok(classifierPrompt.includes("FILE: knowledge/references/api-design.md"));
113
+ assert.ok(classifierPrompt.includes("### Stack candidate registry"));
114
+ const implementer = registry.agents.get("implementer");
115
+ assert.ok(implementer !== undefined);
116
+ const implPrompt = buildPrompt(makeClassifyState(), implementer, registry);
117
+ assert.ok(!implPrompt.includes("### Refs catalog"));
118
+ });
119
+ });
120
+ // Minimal state for the pure render path. buildPrompt reads task / ids /
121
+ // project / decisions / driver.flow_name; a partial cast suffices (the same
122
+ // shape the kernel renderer's own specs use).
123
+ function makeClassifyState() {
124
+ return {
125
+ task: "fix a typo in the README",
126
+ task_short: "typo-fix",
127
+ task_id: "task-1",
128
+ driver_state_id: "ds-1",
129
+ project_dir: "/work/proj",
130
+ decisions: {},
131
+ driver: { flow_name: "medium" },
132
+ };
133
+ }
134
+ // ============================================================================
135
+ // Every agent in agents[] has a backing template .md on disk
136
+ // ============================================================================
137
+ describe("@loomfsm/bundle-code — agent templates", () => {
138
+ it("every agents[] entry resolves to an existing .md template file", () => {
139
+ for (const agent of codeBundle.agents) {
140
+ const abs = join(PKG_ROOT, agent.template_path);
141
+ assert.ok(existsSync(abs) && statSync(abs).isFile(), `agent '${agent.name}' template missing: ${agent.template_path}`);
142
+ }
143
+ });
144
+ it("declares exactly the 21 canonical agents", () => {
145
+ assert.equal(codeBundle.agents.length, 21);
146
+ const names = codeBundle.agents.map((a) => a.name).sort();
147
+ // The three CC-harness trigger agents are NOT bundle agents.
148
+ for (const excluded of ["fe-test-all-agent", "runtime-debug-agent", "test-all-agent"]) {
149
+ assert.ok(!names.includes(excluded), `${excluded} must not be a bundle agent`);
150
+ }
151
+ });
152
+ it("maps each gate to its intended kernel role", () => {
153
+ // The loader accepts ANY valid role per gate, so a valid-but-wrong swap
154
+ // (e.g. gate-plan -> "final") loads fine yet is a real bug. Pin the
155
+ // intended mapping here, which load does not guarantee.
156
+ assert.deepEqual(codeBundle.gate_roles, {
157
+ "gate-classify": "classify",
158
+ "gate-plan": "plan",
159
+ "gate-final": "final",
160
+ });
161
+ });
162
+ });
163
+ // ============================================================================
164
+ // Refusals — the load test must FAIL when the bundle shape is broken
165
+ // ============================================================================
166
+ describe("@loomfsm/bundle-code — load-time refusals", () => {
167
+ let projectDir;
168
+ beforeEach(() => {
169
+ projectDir = freshProject();
170
+ });
171
+ afterEach(() => cleanup(projectDir));
172
+ it("refuses an orphan stage name in a flow (BUNDLE_FLOW_UNKNOWN_STAGE)", async () => {
173
+ const now = captureNow();
174
+ await installManifest(projectDir, now);
175
+ const broken = {
176
+ ...codeBundle,
177
+ flows: {
178
+ ...codeBundle.flows,
179
+ medium: [...(codeBundle.flows["medium"] ?? []), "ghost-stage"],
180
+ },
181
+ };
182
+ await assert.rejects(loadBundle({
183
+ bundle: broken,
184
+ bundle_source_dir: PKG_ROOT,
185
+ project_dir: projectDir,
186
+ providers: [shuttleStub()],
187
+ now,
188
+ }), (err) => {
189
+ assert.ok(err instanceof KernelError);
190
+ assert.equal(err.code, "BUNDLE_FLOW_UNKNOWN_STAGE");
191
+ assert.equal(err.detail?.["missing_stage"], "ghost-stage");
192
+ return true;
193
+ });
194
+ });
195
+ it("refuses a gate whose role is neither kernel-known nor declared (GATE_ROLE_UNKNOWN)", async () => {
196
+ const now = captureNow();
197
+ await installManifest(projectDir, now);
198
+ const broken = {
199
+ ...codeBundle,
200
+ gate_roles: { ...codeBundle.gate_roles, "gate-final": "bogus-role" },
201
+ };
202
+ await assert.rejects(loadBundle({
203
+ bundle: broken,
204
+ bundle_source_dir: PKG_ROOT,
205
+ project_dir: projectDir,
206
+ providers: [shuttleStub()],
207
+ now,
208
+ }), (err) => {
209
+ assert.ok(err instanceof KernelError);
210
+ assert.equal(err.code, "GATE_ROLE_UNKNOWN");
211
+ assert.equal(err.detail?.["role"], "bogus-role");
212
+ return true;
213
+ });
214
+ });
215
+ it("refuses an agent whose template .md is missing on disk (TEMPLATE_NOT_FOUND)", async () => {
216
+ const now = captureNow();
217
+ await installManifest(projectDir, now);
218
+ const broken = {
219
+ ...codeBundle,
220
+ agents: [
221
+ ...codeBundle.agents,
222
+ { name: "phantom", template_path: "agents/phantom.md", output_kind: "nonreview" },
223
+ ],
224
+ };
225
+ await assert.rejects(loadBundle({
226
+ bundle: broken,
227
+ bundle_source_dir: PKG_ROOT,
228
+ project_dir: projectDir,
229
+ providers: [shuttleStub()],
230
+ now,
231
+ }), (err) => {
232
+ assert.ok(err instanceof KernelError);
233
+ assert.equal(err.code, "TEMPLATE_NOT_FOUND");
234
+ assert.equal(err.detail?.["agent"], "phantom");
235
+ return true;
236
+ });
237
+ });
238
+ it("refuses a spawn stage referencing an undeclared agent (BUNDLE_AGENT_UNKNOWN)", async () => {
239
+ const now = captureNow();
240
+ await installManifest(projectDir, now);
241
+ const broken = {
242
+ ...codeBundle,
243
+ stages: {
244
+ ...codeBundle.stages,
245
+ implement: { kind: "spawn", name: "implement", phase: "implementation", agent: "ghost-agent" },
246
+ },
247
+ };
248
+ await assert.rejects(loadBundle({
249
+ bundle: broken,
250
+ bundle_source_dir: PKG_ROOT,
251
+ project_dir: projectDir,
252
+ providers: [shuttleStub()],
253
+ now,
254
+ }), (err) => {
255
+ assert.equal(err.code, "BUNDLE_AGENT_UNKNOWN");
256
+ assert.equal(err.detail?.["agent"], "ghost-agent");
257
+ return true;
258
+ });
259
+ });
260
+ });
261
+ // ============================================================================
262
+ // Auto-readiness — flipping the final role to `auto` loads cleanly because
263
+ // the resolver + the named safety-floor invariant are both shipped.
264
+ // ============================================================================
265
+ describe("@loomfsm/bundle-code — full-autonomous readiness", () => {
266
+ let projectDir;
267
+ beforeEach(() => {
268
+ projectDir = freshProject();
269
+ });
270
+ afterEach(() => cleanup(projectDir));
271
+ it("loads cleanly when the final role is overridden to auto", async () => {
272
+ const now = captureNow();
273
+ await installManifest(projectDir, now);
274
+ const autoFinal = {
275
+ ...codeBundle,
276
+ default_gate_policies: { ...codeBundle.default_gate_policies, final: "auto" },
277
+ };
278
+ const registry = await loadBundle({
279
+ bundle: autoFinal,
280
+ bundle_source_dir: PKG_ROOT,
281
+ project_dir: projectDir,
282
+ providers: [shuttleStub()],
283
+ now,
284
+ });
285
+ // The resolver + INV_safety_floor_final satisfied the auto-policy gate.
286
+ assert.ok(registry.invariants.some((inv) => inv.name === "INV_safety_floor_final"));
287
+ });
288
+ });
289
+ //# sourceMappingURL=bundle.test.js.map