@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.
- package/LICENSE +201 -0
- package/agents/acceptance.md +141 -0
- package/agents/api-contract.md +89 -0
- package/agents/architect.md +52 -0
- package/agents/challenger-reviewer.md +104 -0
- package/agents/classifier.md +74 -0
- package/agents/code-analyzer.md +43 -0
- package/agents/context-doc-verifier.md +94 -0
- package/agents/dependency-auditor.md +42 -0
- package/agents/implementer.md +135 -0
- package/agents/logic-reviewer.md +132 -0
- package/agents/migration.md +55 -0
- package/agents/performance.md +95 -0
- package/agents/plan-conformance.md +127 -0
- package/agents/plan-grounding-check.md +106 -0
- package/agents/planner.md +143 -0
- package/agents/playwright.md +68 -0
- package/agents/research.md +52 -0
- package/agents/security.md +88 -0
- package/agents/style-reviewer.md +85 -0
- package/agents/test.md +206 -0
- package/agents/ui-consistency.md +75 -0
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js +34 -0
- package/dist/manifest.js.map +1 -0
- package/dist/src/bundle.d.ts +2 -0
- package/dist/src/bundle.js +424 -0
- package/dist/src/bundle.js.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +14 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/invariants.d.ts +10 -0
- package/dist/src/invariants.js +208 -0
- package/dist/src/invariants.js.map +1 -0
- package/dist/src/policy-resolver.d.ts +2 -0
- package/dist/src/policy-resolver.js +65 -0
- package/dist/src/policy-resolver.js.map +1 -0
- package/dist/src/sandbox-rules.d.ts +2 -0
- package/dist/src/sandbox-rules.js +40 -0
- package/dist/src/sandbox-rules.js.map +1 -0
- package/dist/test/bundle.test.d.ts +1 -0
- package/dist/test/bundle.test.js +289 -0
- package/dist/test/bundle.test.js.map +1 -0
- package/dist/test/sandbox-rules.test.d.ts +1 -0
- package/dist/test/sandbox-rules.test.js +73 -0
- package/dist/test/sandbox-rules.test.js.map +1 -0
- package/knowledge/references/api-design.md +188 -0
- package/knowledge/references/arch-patterns.md +106 -0
- package/knowledge/references/caching.md +190 -0
- package/knowledge/references/concurrency.md +195 -0
- package/knowledge/references/db-postgres.md +153 -0
- package/knowledge/references/e2e-flutter.md +56 -0
- package/knowledge/references/e2e-playwright.md +53 -0
- package/knowledge/references/error-handling.md +208 -0
- package/knowledge/references/next-app-router.md +231 -0
- package/knowledge/references/observability.md +169 -0
- package/knowledge/references/optimization-strategy.md +197 -0
- package/knowledge/references/perf-flutter.md +62 -0
- package/knowledge/references/perf-nestjs.md +59 -0
- package/knowledge/references/perf-python.md +50 -0
- package/knowledge/references/perf-react.md +52 -0
- package/knowledge/references/react19.md +176 -0
- package/knowledge/references/redis.md +175 -0
- package/knowledge/references/security-backend.md +219 -0
- package/knowledge/references/test-flutter.md +65 -0
- package/knowledge/references/test-nestjs.md +82 -0
- package/knowledge/references/test-python.md +76 -0
- package/knowledge/references/test-react.md +66 -0
- package/knowledge/references/test-strategy.md +175 -0
- package/knowledge/references/ui-flutter.md +56 -0
- package/knowledge/references/ui-web.md +51 -0
- package/package.json +34 -0
- package/schemas/agent-feedback.schema.json +80 -0
- package/schemas/category-vocab.json +170 -0
- package/schemas/classifier-output.schema.json +53 -0
- package/schemas/finding.schema.json +92 -0
- package/schemas/pipeline-state.schema.json +238 -0
- package/schemas/reviewer-output.schema.json +62 -0
- package/schemas/state-extension.schema.json +53 -0
- package/schemas/validator-output.schema.json +48 -0
- 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":"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"}
|
|
@@ -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,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
|