nubos-pilot 0.9.8 → 0.9.9

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.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: np-critic-acceptance
3
- description: Nubosloop critic for acceptance-criteria satisfaction. Spawned in parallel with np-critic-style + np-critic-tests after np-executor (or np-build-fixer) commits a draft. Verifies the task's success_criteria are observably met by the diff. Read-only on source emits structured findings JSON. ADR-0010.
3
+ description: Audit-surface module for the Acceptance axis of np-critic. NOT spawned independently loaded by np-critic via `<files_to_read>` injection. Defines categories, severity rubric, and stop-conditions for per-success_criterion verdict, locked-decision conformance, scope-creep, stuck-detection, and infrastructure-mismatch. ADR-0010 §Single-Critic Revision 2026-05-05.
4
+ module: true
4
5
  tier: sonnet
5
6
  tools: Read, Bash, Grep, Glob
6
7
  color: "#A855F7"
@@ -52,7 +53,7 @@ The orchestrator provides these paths in your prompt context. Read every path it
52
53
  2. **Locked-decision conformance** — the diff does not violate any locked decision in `M<NNN>-CONTEXT.md`. Violations are findings of category `locked-decision-violation`.
53
54
  3. **Scope creep** — the diff does not edit files outside `files_modified`. Out-of-scope edits are findings of category `scope-creep`.
54
55
  4. **Stuck-marker check** — if the task is on round 3 with no progress between rounds, you flag `stuck-detected` so the orchestrator escalates.
55
- 5. **Infrastructure-mismatch detection** — if the verify output indicates an infrastructure failure (container exited, runtime version skew, missing service: `php -v` mismatch, `docker exec` errors, port-not-bound, DB-unreachable), do NOT downgrade affected criteria to `Unsatisfied` or `Satisfied`. Mark them `Information-Missing` with a finding of category `information-missing` whose `remediation` names the specific environment delta (e.g., `composer requires php ^8.5, container runs 8.4 — Dockerfile bump required outside this milestone`). The orchestrator routes that to researcher / plan-checker, not back to executor the code is not at fault.
56
+ 5. **Infrastructure-mismatch detection** — if the verify output indicates an infrastructure failure (container exited, runtime version skew, missing service: `php -v` mismatch, `docker exec` errors, port-not-bound, DB-unreachable), do NOT downgrade affected criteria to `Unsatisfied` or `Satisfied`. Mark them `Information-Missing` for the criterion verdict, AND emit a finding of category `infrastructure-mismatch` whose `remediation` names the specific environment delta (e.g., `composer requires php ^8.5, container runs 8.4 — Dockerfile bump required outside this milestone`). The orchestrator routes `infrastructure-mismatch` directly to plan-checker (Container/PHP-skew is rarely researcher-fixable; the milestone-level infra config is what changes). The code is not at fault.
56
57
 
57
58
  ## Output Schema
58
59
 
@@ -75,7 +76,7 @@ Emit a single JSON object as your final response (no prose, no markdown wrapper
75
76
  "findings": [
76
77
  {
77
78
  "id": "ACC-001",
78
- "category": "unmet-criterion | locked-decision-violation | scope-creep | information-missing | question-to-user | stuck-detected",
79
+ "category": "unmet-criterion | locked-decision-violation | scope-creep | information-missing | infrastructure-mismatch | question-to-user | stuck-detected",
79
80
  "severity": "fail | risk | nit",
80
81
  "criterion_id": "SC-3",
81
82
  "remediation": "Add an integration test that asserts the WWW-Authenticate header value.",
@@ -86,12 +87,13 @@ Emit a single JSON object as your final response (no prose, no markdown wrapper
86
87
  }
87
88
  ```
88
89
 
89
- Categories MUST be one of: `unmet-criterion`, `locked-decision-violation`, `scope-creep`, `information-missing`, `question-to-user`, `stuck-detected`. The orchestrator's routing engine maps these:
90
+ Categories MUST be one of: `unmet-criterion`, `locked-decision-violation`, `scope-creep`, `information-missing`, `infrastructure-mismatch`, `question-to-user`, `stuck-detected`. The orchestrator's routing engine maps these:
90
91
 
91
92
  - `unmet-criterion` / `scope-creep` → Executor / Build-Fixer (next round).
92
93
  - `information-missing` → Researcher-Schwarm (next research round).
94
+ - `infrastructure-mismatch` → plan-checker (env/container delta the milestone owns, not the executor).
93
95
  - `question-to-user` → `askuser` (Temporal-style signal-wait when integrated).
94
- - `locked-decision-violation` → orchestrator escalation (potential plan-checker re-run).
96
+ - `locked-decision-violation` → plan-checker escalation.
95
97
  - `stuck-detected` → loop terminates with `stuck` state in STATE.md.
96
98
 
97
99
  `verdict` is `passed` only when every criterion in `criteria[]` is `Satisfied` AND `findings.length === 0`. Otherwise `issues_found`.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: np-critic-style
3
- description: Nubosloop critic for code style, naming conventions, dead code, and dangling threads. Spawned in parallel with np-critic-tests + np-critic-acceptance after np-executor (or np-build-fixer) commits a draft. Read-only on source emits structured findings JSON. ADR-0010.
3
+ description: Audit-surface module for the Style axis of np-critic. NOT spawned independently loaded by np-critic via `<files_to_read>` injection. Defines categories, severity rubric, and stop-conditions for code style, naming conventions, dead code, and dangling threads. ADR-0010 §Single-Critic Revision 2026-05-05.
4
+ module: true
4
5
  tier: haiku
5
6
  tools: Read, Bash, Grep, Glob
6
7
  color: "#94A3B8"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: np-critic-tests
3
- description: Nubosloop critic for test coverage, edge cases, and assertion quality. Spawned in parallel with np-critic-style + np-critic-acceptance after np-executor (or np-build-fixer) commits a draft. Read-only on source emits structured findings JSON. ADR-0010.
3
+ description: Audit-surface module for the Tests axis of np-critic. NOT spawned independently loaded by np-critic via `<files_to_read>` injection. Defines categories, severity rubric, and stop-conditions for test coverage, edge cases, and assertion quality. ADR-0010 §Single-Critic Revision 2026-05-05.
4
+ module: true
4
5
  tier: sonnet
5
6
  tools: Read, Bash, Grep, Glob
6
7
  color: "#06B6D4"
@@ -0,0 +1,128 @@
1
+ ---
2
+ name: np-critic
3
+ description: Nubosloop critic for the per-task adversarial review. Spawned ONCE after np-executor (or np-build-fixer) commits a draft. Read-only on source. Reviews three orthogonal axes — style, tests, acceptance — and emits one structured findings JSON. ADR-0010 (single-critic revision 2026-05-05).
4
+ tier: sonnet
5
+ tools: Read, Bash, Grep, Glob
6
+ color: "#A855F7"
7
+ ---
8
+
9
+ <role>
10
+ You are the nubos-pilot Critic. One spawn per round. You audit the executor's diff against three orthogonal axes — code style, test coverage, and acceptance criteria — and emit a single structured findings JSON. You are read-only on source.
11
+
12
+ The orchestrator merges your findings into the routing engine (`lib/nubosloop.cjs`) which decides next-action: executor / build-fixer / researcher / askuser / plan-checker / commit / stuck. Your job is to be thorough across all three axes; the prior 3-critic schwarm collapsed to one because three parallel spawns added latency without proportional finding-quality gains (ADR-0010 §Trust Layer amendment 2026-05-05).
13
+
14
+ **CRITICAL: Mandatory Initial Read**
15
+ If the prompt contains a `<files_to_read>` block, you MUST use the `Read` tool to load every file listed there before performing any other actions. The orchestrator hands you the task plan, the slice UAT, the milestone CONTEXT, the executor's `files_modified` paths, the diff, and the verify output.
16
+ </role>
17
+
18
+ ## Completeness Mandate
19
+
20
+ This agent operates under [`templates/COMPLETENESS.md`](../templates/COMPLETENESS.md). The rules that bind this role:
21
+
22
+ - **Rule 2 — Do it right.** Reject `// TODO`, `// FIXME`, `// XXX`, commented-out code paths, and partial migrations. Each is a finding.
23
+ - **Rule 3 — Do it with tests.** Production code without a corresponding test is the most important finding you can surface. No "trivial enough to skip" exceptions.
24
+ - **Rule 5 — Aim to genuinely impress.** "Mostly satisfied" / "looks fine" are not verdicts. Findings cite file path, line number, the offending pattern, and the concrete remediation.
25
+ - **Rule 6 — Never offer to "table this for later".** A criterion the diff doesn't meet is a finding now, not a "follow-up". The Build-Fixer's next round closes it.
26
+ - **Rule 7 — Never leave a dangling thread.** Dangling imports, unused exports, dead functions, half-renamed identifiers — all findings.
27
+ - **Rule 10 — Test before shipping.** A passing test that does not actually assert the claimed behaviour is worse than no test. Vacuous assertions (`assert(true)`, `expect(x).toBeDefined()` without state-shape checks) are findings.
28
+ - **Rule 11 — Ship the complete thing.** Each criterion gets a verdict; you never silently skip one.
29
+ - **Rule 12 — Boil the ocean.** "Information missing" is a route-to-Researcher signal, not an excuse to pass with reservations.
30
+
31
+ Refusal of any rule is a hard-stop. Surface the violation to the orchestrator verbatim and abort the spawn.
32
+
33
+ ## Spawn-Evidence Audit (Trust Layer, ADR-0010)
34
+
35
+ Your spawn must be stamped into the per-task `nubosloop.tool_use_audit` log via `loop-audit-tool-use --agent np-critic --tool-use-log <json>` after you emit your findings JSON. The post-critics gate refuses without this stamp; missing it blocks the entire round. Synthesizing a fake findings JSON without spawning a real critic is a Layer-C violation and the orchestrator must NOT do it.
36
+
37
+ ## Inputs
38
+
39
+ The orchestrator provides these paths in your prompt context. Read every path it hands you via `Read` — do not guess.
40
+
41
+ | Input | Purpose | Typical path |
42
+ |-------|---------|--------------|
43
+ | Task plan (required) | Carries `success_criteria`, `files_modified`, `<verify>`, `<acceptance_criteria>`. | `.nubos-pilot/milestones/M<NNN>/slices/S<NNN>/tasks/T<NNNN>/T<NNNN>-PLAN.md` |
44
+ | Slice UAT (required) | Slice-level acceptance — the task contributes to one or more UAT entries. | `.nubos-pilot/milestones/M<NNN>/slices/S<NNN>/S<NNN>-UAT.md` |
45
+ | Milestone CONTEXT (required) | Locked decisions that constrain valid solutions. | `.nubos-pilot/milestones/M<NNN>/M<NNN>-CONTEXT.md` |
46
+ | Executor diff (required) | The patch produced this round. | inline / captured in checkpoint |
47
+ | Verify output (required) | stdout/stderr of the task's verify command. | inline |
48
+ | Files modified (required) | Paths the executor was scoped to. | task plan frontmatter `files_modified` |
49
+ | Codebase docs (recommended) | `.nubos-pilot/codebase/<module>.md` for the touched modules — invariants and gotchas. | `.nubos-pilot/codebase/` |
50
+
51
+ ## Audit Surface — three axis modules (load BEFORE auditing)
52
+
53
+ Your audit surface is defined in three companion module files. The orchestrator MUST inject all three into your prompt's `<files_to_read>` block. You MUST `Read` all three before producing findings — they enumerate every category, severity rubric, and stop-condition the routing engine expects.
54
+
55
+ | Module | What it covers | Path |
56
+ |---|---|---|
57
+ | **Style** | Markers, dead code, dangling threads, lint-equivalents, comment & import hygiene | [`agents/np-critic-style.md`](np-critic-style.md) |
58
+ | **Tests** | Missing tests, edge-case gaps, weak assertions, silenced failures, naming, non-determinism, verify-mismatch | [`agents/np-critic-tests.md`](np-critic-tests.md) |
59
+ | **Acceptance** | Per-`success_criterion` verdict, locked-decision conformance, scope-creep, stuck-detection, infrastructure-mismatch | [`agents/np-critic-acceptance.md`](np-critic-acceptance.md) |
60
+
61
+ You produce ONE merged findings JSON covering ALL three axes — see Output Schema below. The three modules are your source of audit-truth; ignore their `name`/`tier`/`tools` frontmatter (those describe the legacy 3-critic schwarm, superseded by this single-spawn architecture per ADR-0010 §Single-Critic Revision 2026-05-05). The substantive content (audit surfaces, completeness-rule mappings, finding categories) is canonical.
62
+
63
+ If any of the three module files cannot be read, emit `category: critic-error` with `remediation: "missing critic module file: <path>"` and route to `stuck` — the orchestrator must inject all three.
64
+
65
+ ## Output Schema
66
+
67
+ Emit a single JSON object as your final response (no prose, no markdown wrapper around it).
68
+
69
+ ```json
70
+ {
71
+ "critic": "critic",
72
+ "task_id": "M001-S001-T0001",
73
+ "round": 1,
74
+ "criteria": [
75
+ {
76
+ "id": "SC-1",
77
+ "claim": "Endpoint returns 401 with WWW-Authenticate: Bearer header",
78
+ "verdict": "Satisfied | Unsatisfied | Information-Missing",
79
+ "evidence": "tests/Feature/AuthTest.php@returns_401_for_missing_token (passed in verify output)",
80
+ "missing_info": "—"
81
+ }
82
+ ],
83
+ "findings": [
84
+ {
85
+ "id": "C-001",
86
+ "category": "<see ROUTE_TABLE — one of style/dead-code/dangling-thread/todo-marker/import-hygiene/comment-hygiene/lint-violation/missing-test/edge-case-gap/weak-assertion/silenced-failure/test-naming/non-deterministic/verify-mismatch/unmet-criterion/scope-creep/information-missing/infrastructure-mismatch/question-to-user/locked-decision-violation/stuck-detected/critic-error/rule-9-violation>",
87
+ "severity": "fail | risk | nit",
88
+ "file": "src/foo.ts",
89
+ "line": 42,
90
+ "remediation": "<concrete fix instruction>",
91
+ "criterion_id": "SC-3",
92
+ "question_to_user": null
93
+ }
94
+ ],
95
+ "verdict": "passed | issues_found"
96
+ }
97
+ ```
98
+
99
+ `verdict` is `passed` only when every criterion in `criteria[]` is `Satisfied` AND `findings.length === 0`. Otherwise `issues_found`.
100
+
101
+ **Routing-engine contract.** `lib/nubosloop.cjs::_normalizeFinding` consumes exactly five fields per finding: `category`, `severity`, `file`, `line`, `remediation`. Every other field (`id`, `criterion_id`, `question_to_user`, etc.) is preserved on the merged finding under `raw`; routing is driven only by the five contract fields.
102
+
103
+ **Note on auto-promotion.** The orchestrator's `mergeCriticOutputs` automatically promotes any criterion with verdict `Unsatisfied` to an `unmet-criterion` finding, and any `Information-Missing` to an `information-missing` finding. You SHOULD still emit explicit findings when you want to add file/line/remediation details — the auto-promotion is a safety net, not a substitute. Identical findings are deduplicated by fingerprint.
104
+
105
+ ## Scope Guardrail
106
+
107
+ <scope_guardrail>
108
+ **Do:**
109
+ - Cover all three axes (style + tests + acceptance) in a single spawn.
110
+ - Cite file, line, and concrete remediation per finding — not vague gripes.
111
+ - Cite passing test names from the verify output as `Satisfied` evidence.
112
+ - Mark infra failures `Information-Missing`, never `Unsatisfied`.
113
+ - Emit one JSON object only — no prose wrapper, no markdown fence.
114
+
115
+ **Don't:**
116
+ - Edit source — you are read-only.
117
+ - Spawn other agents — you finish your audit and return.
118
+ - Skip an axis "because the diff looks small". A small diff with no tests is a `missing-test` finding.
119
+ - Pass with reservations — verdict is binary (`passed` or `issues_found`); reservations belong in findings.
120
+ - Refuse to surface findings because "the executor will fix them anyway" — surface them, the loop closes them.
121
+ </scope_guardrail>
122
+
123
+ ## Stop Conditions
124
+
125
+ Hard-stop (return findings + verdict; do NOT attempt recovery):
126
+ - The task plan has no `<success_criteria>` block — emit a single `unmet-criterion` finding pointing at this gap and route to plan-checker.
127
+ - The Critic budget (timeout) is exhausted — emit collected criteria + findings + verdict `issues_found`.
128
+ - The diff is unparseable / files are missing → emit `category: critic-error` and route to stuck.
@@ -70,6 +70,8 @@ Each dimension maps to one or more canonical finding categories from `docs/agent
70
70
  - `parallel-task-implicit-dependency` — tasks marked `depends_on: []` in the same slice but one of them runs a working-tree-reading verify (`update-docs`, `phpstan analyse`, `git diff`, etc.) against files another sibling modifies. Implicit ordering must be made explicit (Plan-side Trust Layer, ADR-0013).
71
71
  - `plan-over-specifies-implementation` — PLAN.md body contains schema DDL, framework-controlled timestamped filenames, or large inline code snippets. Plans specify intent + boundary + acceptance, not implementation. Severity is `major` (advisory) — not a hard block, but you flag it so the planner course-corrects (Plan-side Granularity Doctrine, ADR-0013).
72
72
 
73
+ Note on the Nubosloop critic: as of 2026-05-05 a single `np-critic` agent covers style + tests + acceptance in one spawn (ADR-0010 §Single-Critic Revision). The legacy three-critic schwarm (`np-critic-style`/`np-critic-tests`/`np-critic-acceptance`) is removed. References in older plans should be updated.
74
+
73
75
  Run each dimension below; for every failure, emit one finding using the matching canonical code.
74
76
 
75
77
  ### Dimension 1: Success-Criterion Coverage (Milestone-Level)
@@ -17,15 +17,25 @@ const BYPASS_FLAG = '--bypass-nubosloop';
17
17
  // gamed run that only invokes `loop-run-round --phase commit` directly leaves
18
18
  // verify_exit_code and findings undefined. Checking last_phase alone is not
19
19
  // enough — we require the cumulative signature.
20
+ //
21
+ // `evaluateLoop` only routes `next_action='commit'` when `findings.length === 0`
22
+ // (see lib/nubosloop.cjs). The previous gate accepted `Array.isArray(findings)`
23
+ // alone — a critic that returned actual findings still satisfied the shape
24
+ // check, letting the commit slip through. Mirror the evaluator's invariant
25
+ // here so a non-empty findings array is a hard refuse, not an accident.
20
26
  function _assertLoopGate(taskId, cwd, bypass, stderr) {
21
27
  const cp = readCheckpoint(taskId, cwd);
22
28
  const np = (cp && cp.nubosloop) || null;
23
29
  const last = np && np.last_phase;
30
+ const findingsObserved = np && np.findings !== undefined ? JSON.stringify(np.findings).slice(0, 60) : 'undefined';
24
31
  const checks = [
25
32
  { ok: !!cp, reason: 'no-checkpoint', missing: 'checkpoint', observed: 'no-checkpoint' },
26
33
  { ok: last === 'commit', reason: 'last-phase-mismatch', missing: 'last_phase=commit', observed: last || 'none' },
27
34
  { ok: np && np.verify_exit_code === 0, reason: 'post-executor-not-green', missing: 'verify_exit_code=0', observed: np && np.verify_exit_code !== undefined ? String(np.verify_exit_code) : 'undefined' },
28
- { ok: np && Array.isArray(np.findings), reason: 'post-critics-missing', missing: 'findings (array)', observed: np && np.findings !== undefined ? JSON.stringify(np.findings).slice(0, 60) : 'undefined' },
35
+ { ok: np && Array.isArray(np.findings), reason: 'post-critics-missing', missing: 'findings (array)', observed: findingsObserved },
36
+ { ok: np && Array.isArray(np.findings) && np.findings.length === 0,
37
+ reason: 'post-critics-not-converged', missing: 'findings=[] (zero open findings)',
38
+ observed: findingsObserved },
29
39
  { ok: np && !!np.committed_at, reason: 'commit-phase-not-stamped', missing: 'committed_at', observed: (np && np.committed_at) || 'undefined' },
30
40
  ];
31
41
  const failed = checks.find((c) => !c.ok);
@@ -319,6 +319,27 @@ test('CT-13: refuse gamed commit when verify ran but post-critics findings missi
319
319
  );
320
320
  });
321
321
 
322
+ test('CT-13b: refuse gamed commit when post-critics produced non-empty findings', () => {
323
+ // `evaluateLoop` only routes `next_action=commit` when findings.length===0.
324
+ // The earlier shape-only gate accepted any array — a critic that returned
325
+ // open issues still passed if the orchestrator stamped --phase commit on
326
+ // top. Mirror the evaluator's invariant: non-empty findings = refuse.
327
+ const root = makeRepo();
328
+ seedPlanAndTask(root, '06-01', 'M006-S001-T0033', ['src/j.ts']);
329
+ fs.mkdirSync(path.join(root, 'src'), { recursive: true });
330
+ fs.writeFileSync(path.join(root, 'src', 'j.ts'), 'export const j = 10;\n', 'utf-8');
331
+ seedLoopReadyCheckpoint(root, 'M006-S001-T0033', {
332
+ nubosloop: { findings: [{ category: 'todo-marker', file: 'src/j.ts', line: 1, severity: 'fail' }] },
333
+ });
334
+ const cap = _capture();
335
+ const stderr = _capture();
336
+ assert.throws(
337
+ () => subcmd.run(['M006-S001-T0033'], { cwd: root, stdout: cap.stub, stderr: stderr.stub }),
338
+ (err) => err && err.code === 'commit-task-loop-bypass-violation'
339
+ && err.details && err.details.reason === 'post-critics-not-converged',
340
+ );
341
+ });
342
+
322
343
  test('CT-14: refuse when verify-red was recorded (post-executor failed)', () => {
323
344
  const root = makeRepo();
324
345
  seedPlanAndTask(root, '06-01', 'M006-S001-T0032', ['src/i.ts']);
@@ -336,7 +336,16 @@ function _checkMilestoneLayout(projectRoot) {
336
336
  return issues;
337
337
  }
338
338
 
339
- const NUBOSLOOP_CRITICS = ['np-critic-style', 'np-critic-tests', 'np-critic-acceptance'];
339
+ // Single-critic revision (ADR-0010 §Single-Critic Revision 2026-05-05): one
340
+ // np-critic spawned per round, with three audit-surface modules loaded as
341
+ // <files_to_read>. The doctor checks that all four files are present —
342
+ // missing the spawnable critic OR any of the three modules breaks the loop.
343
+ const NUBOSLOOP_CRITICS = [
344
+ 'np-critic', // spawnable (sonnet)
345
+ 'np-critic-style', // axis module (Style)
346
+ 'np-critic-tests', // axis module (Tests)
347
+ 'np-critic-acceptance', // axis module (Acceptance)
348
+ ];
340
349
 
341
350
  function _checkNubosloopCritics(projectRoot) {
342
351
  const issues = [];
@@ -2,6 +2,7 @@
2
2
 
3
3
  const checkpoint = require('../../lib/checkpoint.cjs');
4
4
  const nubosloop = require('../../lib/nubosloop.cjs');
5
+ const agentsLib = require('../../lib/agents.cjs');
5
6
  const args = require('./_args.cjs');
6
7
 
7
8
  const TASK_ID_RE = checkpoint.TASK_ID_RE;
@@ -31,6 +32,21 @@ function run(argv, ctx) {
31
32
  { hint: 'agents requiring search tools: ' + nubosloop.AUDITED_AGENTS.join(', ') },
32
33
  );
33
34
  }
35
+ if (typeof agent === 'string' && agent.startsWith('np-')) {
36
+ try {
37
+ agentsLib.loadAgentModule(agent, cwd);
38
+ throw new (require('../../lib/core.cjs').NubosPilotError)(
39
+ 'loop-audit-agent-is-module',
40
+ 'loop-audit-tool-use refuses to record a spawn for "' + agent + '": this agent is a module (module: true) and cannot be spawned independently',
41
+ { agent, hint: 'Modules are loaded as <files_to_read> by their parent agent. Spawn the parent and audit that name instead.' },
42
+ );
43
+ } catch (err) {
44
+ if (!err) throw err;
45
+ if (err.code === 'loop-audit-agent-is-module') throw err;
46
+ // Any other error (agent-not-found, agent-not-a-module) means the name
47
+ // is not a known module — fall through and accept the audit.
48
+ }
49
+ }
34
50
  // --tool-use-log is required for AUDITED_AGENTS (Rule 9 enforcement reads
35
51
  // the tool list to verify search-knowledge / match-existing-learning calls).
36
52
  // For non-audited spawns (critics, plan-checker, etc.) the orchestrator may