pan-wizard 3.21.0 → 3.22.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.
@@ -80,6 +80,7 @@ Every cap the conductor enforces applies to the campaign, scaled up:
80
80
  | `--max-cycles` | 5 | Mission items landed before stopping. |
81
81
  | `--total-budget` | 300 | Cumulative point budget. **Advisory by default** — tracked/surfaced but not a hard stop unless `--enforce-budget` / config `budget.enforce: true`. |
82
82
  | `--enforce-budget` | off | Make the point budgets hard stops again (also settable via config `budget.enforce: true`). |
83
+ | `--verify-reserve` | 0.15 | Fraction of `--total-budget` (0–0.5) held back for the final Quality re-review so it can't be starved (config `budget.verify_reserve`). Advisory by default (surfaced as `into_verify_reserve`); under `--enforce-budget` it stops taking on new missions early (`budget_reserve_reached`) and the reserved points fund the closing `--clean-seal` re-verification before the last INTEGRATE. |
83
84
  | `--squads` | all | Restrict to a subset, e.g. `--squads architecture,build,quality`. |
84
85
  | `--no-build-worktrees` | off | Build in the main tree instead of branch-per-agent worktrees (small/serial projects). |
85
86
  | `--push` | off | Push approved merges to origin (still human-gated). |
@@ -143,13 +144,14 @@ PAN is not a daemon — it cannot wake itself while the session is closed. `--sc
143
144
  - **The trigger (you wire one):** a host scheduler (Claude Code routines / cron / scheduled-tasks) or a `/loop` runs `pan-tools campaign due` and, when it reports due, invokes `/pan:army --continue`. On next session open, a due campaign is surfaced as a nudge.
144
145
  - **Resume (`--continue`):** read the schedule + `.planning/orchestration/` + focus-auto state. If `campaign due` is true and the day's `--daily-budget` isn't spent, run the next mission(s), then `campaign record-run` (advances next-due, accrues the day's spend). If not due or budget-spent, report next-due and STOP.
145
146
  - **Bounded spend:** point budgets (`--total-budget`, `--daily-budget`) are **advisory indicators by default** — they're tracked and surfaced, not hard stops, unless `budget.enforce` / `--enforce-budget` is set. The real bounds are `--max-cycles`, the conductor caps, the abort file, and the human merge gate at every integrate. A scheduled campaign runs the backlog down to staged, reviewed, green PRs over days.
147
+ - **Verify reserve:** a fraction of `--total-budget` (`--verify-reserve`, default 0.15) is held back so the closing Quality re-review isn't starved. Surfaced always via `campaign status` / `focus auto --status` (`into_verify_reserve`, `new_work_budget_remaining`); under `--enforce-budget` the run stops taking on new missions early (`budget_reserve_reached`) and spends the reserve on the final `--clean-seal` verification before the last INTEGRATE.
146
148
 
147
149
  Manage it: `pan-tools campaign status` (active/paused, spent today, next-due), `campaign schedule --pause` / `--resume` / `--disable`.
148
150
 
149
151
  ---
150
152
 
151
153
  ## Completion contract
152
- The campaign is complete when ANY holds: `--max-cycles` reached · backlog empty · abort file present · context < 25% · a mission cannot pass Quality and can't be cleanly reverted (HARD STOP — preserve state, report). (Budget exhaustion is advisory by default — a stop condition only when `budget.enforce`/`--enforce-budget` is set.) Always run `--clean-seal` (unless omitted) after the last item.
154
+ The campaign is complete when ANY holds: `--max-cycles` reached · backlog empty · abort file present · context < 25% · a mission cannot pass Quality and can't be cleanly reverted (HARD STOP — preserve state, report). (Budget exhaustion is advisory by default — a stop condition only when `budget.enforce`/`--enforce-budget` is set; under enforcement, crossing into the verify reserve (`budget_reserve_reached`) stops new missions but the reserved points still fund the closing re-verification.) Always run `--clean-seal` (unless omitted) after the last item.
153
155
 
154
156
  ## NEVER DO
155
157
  - Let Mission Control write code, or let a squad agent spawn further agents (depth cap).
@@ -102,6 +102,7 @@ Wait for the user's reply before proceeding. Do not guess or pick a default cate
102
102
  | `--max-cycles` | 10 | Maximum iterations (1-50) |
103
103
  | `--total-budget` | 500 | Cumulative point budget (5-5000). Advisory by default (tracked/surfaced, not a stop). |
104
104
  | `--enforce-budget` | off | Make `--total-budget` a hard stop again (also settable via config `budget.enforce: true`). |
105
+ | `--verify-reserve` | 0.15 | Fraction of `--total-budget` (0–0.5) held back for the final re-verification pass so it can't be starved (config `budget.verify_reserve`). Advisory by default — surfaced as `into_verify_reserve`; under `--enforce-budget` it stops **new work** early (`budget_reserve_reached`) and Phase 3 spends the reserve on the clean re-verification. |
105
106
  | `--continue` | — | Resume stopped/interrupted run |
106
107
  | `--stop` | — | Gracefully stop active run |
107
108
  | `--status` | — | Show current campaign progress |
@@ -344,6 +345,7 @@ Run: `pan-tools focus auto --update --items-completed N --items-failed N --point
344
345
 
345
346
  Check the response for stop conditions:
346
347
  - `regression`: Tests decreased — STOP IMMEDIATELY
348
+ - `budget_reserve_reached`: New-work spend crossed into the reserved headroom (enforced runs) — stop taking on new items and go to Phase 3, which spends the reserve on the final re-verification pass
347
349
  - `budget_cap`: Cumulative budget exceeded — go to Phase 3
348
350
  - `max_cycles`: Maximum iterations reached — go to Phase 3
349
351
  - `zero_completed`: No items completed in this cycle — go to Phase 3
@@ -391,8 +393,9 @@ Then continue immediately to the next cycle (back to Step 2.1).
391
393
 
392
394
  ### Phase 3: Campaign End
393
395
 
394
- 1. Run `pan-tools focus auto --status` to get final state
395
- 2. Display campaign summary:
396
+ 1. **Final re-verification pass (spend the verify reserve).** Run `pan-tools focus auto --status` and read `stop_reason` / `into_verify_reserve` / `verify_reserve`. Run one clean build + full verification directly (commands from `config.json → build`/`verification`) — the same pass `--clean-seal` performs — when ANY holds: the stop reason is `budget_reserve_reached`, `--clean-seal` is set, or `into_verify_reserve` is true. This is what the reserved budget is FOR: the run stopped taking on **new** work early (at `budget_reserve_reached`) precisely so this closing verification has headroom and isn't starved. Run the verification commands directly — do NOT `focus auto --update` here (the run is already `completed`; its status update is refused, and the verification agents' spend is captured by the cost hooks automatically). If verification fails, treat it as a HARD STOP (preserve state, report) rather than sealing green. Skip this step only when none of the three conditions hold (advisory run that never entered the reserve and no `--clean-seal`).
397
+ 2. Run `pan-tools focus auto --status` to get final state
398
+ 3. Display campaign summary:
396
399
 
397
400
  ```
398
401
  ## Campaign Complete
@@ -408,7 +411,7 @@ Then continue immediately to the next cycle (back to Step 2.1).
408
411
  | Stop reason | <reason> |
409
412
  ```
410
413
 
411
- 3. Remove safety tag: `git tag -d focus-auto-baseline 2>/dev/null`
414
+ 4. Remove safety tag: `git tag -d focus-auto-baseline 2>/dev/null`
412
415
 
413
416
  ## 6-Layer Safety Harness
414
417
 
@@ -416,6 +419,7 @@ Then continue immediately to the next cycle (back to Step 2.1).
416
419
  |-------|-----------|--------|
417
420
  | Per-cycle budget | `--budget N` per cycle | Limits single-cycle damage |
418
421
  | Cumulative budget | `--total-budget N` | Prevents runaway spending |
422
+ | Verify reserve | `--verify-reserve F` | Holds back a budget fraction so the final re-verification isn't starved — stops new work early under enforcement (`budget_reserve_reached`) and spends the reserve on the clean re-verify in Phase 3 |
419
423
  | Iteration limit | `--max-cycles N` | Hard stop on loop count |
420
424
  | Regression circuit breaker | tests_after < tests_before | Immediate stop, status=stopped |
421
425
  | Zero-completed guard | 0 items done in a cycle | Stop — further cycles won't help |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pan-wizard",
3
- "version": "3.21.0",
3
+ "version": "3.22.0",
4
4
  "description": "Command a bot army for your codebase: an Opus Mission Control delegates whole-project goals to specialist squads and ships behind a human merge gate. Five AI CLIs, zero context rot.",
5
5
  "bin": {
6
6
  "pan-wizard": "bin/install.js"
@@ -24,12 +24,49 @@ const { readMemory, parseEntries, listMemoryAgents, compactMemory, DEFAULT_MAX_E
24
24
 
25
25
  const DEFAULT_KEEP = 12; // recent bullets kept inline per section
26
26
  const STATE_ARCHIVE_FILE = 'state-archive.md';
27
+ const QUARANTINE_FILE = 'quarantine.md';
27
28
 
28
29
  // Sections whose bullet lists grow unbounded and are safe to reconcile.
29
30
  const APPEND_HEAVY = /\b(decisions|blockers|concerns|pending todos|todos|session continuity|accumulated context|recent activity)\b/i;
30
31
  // A bullet that is just a placeholder — dropped once real entries exist.
31
32
  const PLACEHOLDER = /^-\s*(none(\s+yet)?|n\/a|tbd|todo|—|-)\.?\s*$/i;
32
33
 
34
+ // Memory-injection defense (threat: a subagent writes an instruction/directive
35
+ // into the always-loaded memory that a LATER agent or run reads and OBEYS — a
36
+ // cross-generation prompt injection, exactly the "agent writes self-serving
37
+ // directives into persistent memory for successors" vector in the OpenAI
38
+ // rogue-agent incident, Reuters 2026-07). See ADR-0040. state.md is agent-writable
39
+ // (decisions/blockers/notes), so during reconcile any bullet that reads like a
40
+ // directive AIMED at the agent/system is QUARANTINED out of standing memory (moved
41
+ // to .planning/memory/quarantine.md, reversible) rather than carried forward —
42
+ // nothing agent-authored becomes standing instruction without human review (the
43
+ // merge gate). High-precision, injection-flavored patterns only, to minimize false
44
+ // positives; a legitimate note caught here is recoverable from quarantine.md.
45
+ const DIRECTIVE_PATTERNS = [
46
+ /\bignore\s+(all\s+|any\s+|these\s+)?(previous|prior|earlier|above)\b/i,
47
+ /\bdisregard\s+(the\s+|all\s+|any\s+|your\s+)?(previous|prior|above|earlier|instructions|rules|guidelines|guardrails)\b/i,
48
+ /\byou\s+are\s+now\b/i,
49
+ /\bnew\s+instructions?\s*:/i,
50
+ /\b(the\s+)?system\s+prompt\b/i,
51
+ /\b(as|acting\s+as|being)\s+(an?\s+)?(admin|administrator|root|superuser|developer\s+with)\b/i,
52
+ /\bpre-?authoriz(e|ed|ation)\b/i,
53
+ /\b(do\s+not|don'?t|never)\s+(tell|inform|notify|ask|alert)\s+the\s+(user|human|operator)\b/i,
54
+ /\bwithout\s+(asking|telling|notifying|informing|alerting)\s+the\s+(user|human|operator)\b/i,
55
+ /\b(bypass|skip|disable|override|remove|turn\s+off)\s+(the\s+)?(merge[-\s]?gate|human[-\s]?(approval|gate|review)|approval[-\s]?gate|safety[-\s]?(harness|check|guard|gate)|verification[-\s]?gate|review[-\s]?gate)\b/i,
56
+ /\b(always|automatically)\s+(approve|auto-?approve|accept|merge|confirm|say\s+yes)\b/i,
57
+ /\bauto-?approve\s+(all|any|every|everything)\b/i,
58
+ /\boverride\s+(the\s+)?(human|approval|merge[-\s]?gate|safety)\b/i,
59
+ ];
60
+
61
+ /**
62
+ * True when a memory entry reads like a directive aimed at the agent/system
63
+ * (an injection), as opposed to a descriptive project note. Pure + zero-dep.
64
+ */
65
+ function isSuspiciousDirective(text) {
66
+ if (typeof text !== 'string' || !text) return false;
67
+ return DIRECTIVE_PATTERNS.some((re) => re.test(text));
68
+ }
69
+
33
70
  const isHeading = (l) => /^#{1,6}\s+\S/.test(l);
34
71
  const headingText = (l) => (l.match(/^#{1,6}\s+(.*)$/) || [, ''])[1];
35
72
  const isBullet = (l) => /^-\s+\S/.test(l);
@@ -64,7 +101,7 @@ function joinSections(sections) {
64
101
  * its indented continuation lines, so a bullet is never orphaned from its detail.
65
102
  * Overflow entries are pushed to `archived`. Returns { lines, changed }.
66
103
  */
67
- function reconcileBullets(lines, keepN, archived) {
104
+ function reconcileBullets(lines, keepN, archived, quarantined) {
68
105
  const firstB = lines.findIndex(isBullet);
69
106
  if (firstB === -1) return { lines, changed: false };
70
107
 
@@ -87,9 +124,22 @@ function reconcileBullets(lines, keepN, archived) {
87
124
  }
88
125
  const trailer = rest.slice(i);
89
126
 
127
+ // 0. QUARANTINE injected directives FIRST — a bullet that reads like an
128
+ // instruction aimed at the agent never survives into standing memory; it is
129
+ // moved to the quarantine file for human review (memory-injection defense).
130
+ const safe = [];
131
+ let quarantinedHere = 0;
132
+ for (const e of entries) {
133
+ if (quarantined && isSuspiciousDirective(e.key)) {
134
+ quarantined.push(e.lines.join('\n'));
135
+ quarantinedHere++;
136
+ } else {
137
+ safe.push(e);
138
+ }
139
+ }
90
140
  // 1. dedupe (keep first occurrence)
91
141
  const seen = new Set();
92
- const deduped = entries.filter((e) => (seen.has(e.key) ? false : (seen.add(e.key), true)));
142
+ const deduped = safe.filter((e) => (seen.has(e.key) ? false : (seen.add(e.key), true)));
93
143
  // 2. strip placeholders once real entries exist
94
144
  const real = deduped.filter((e) => !PLACEHOLDER.test(e.key));
95
145
  const kept0 = real.length ? real : deduped;
@@ -102,7 +152,7 @@ function reconcileBullets(lines, keepN, archived) {
102
152
  }
103
153
  for (const d of dropped) archived.push(d.lines.join('\n'));
104
154
 
105
- const changed = deduped.length !== entries.length || kept0.length !== deduped.length || dropped.length > 0;
155
+ const changed = quarantinedHere > 0 || deduped.length !== safe.length || kept0.length !== deduped.length || dropped.length > 0;
106
156
  const newLines = [...pre, ...kept.flatMap((e) => e.lines), ...trailer];
107
157
  // Preserve the section's trailing blank line (the blank that separates it from
108
158
  // the next heading) so reconciling never collapses two sections together.
@@ -119,23 +169,22 @@ function optimizeStateContent(content, opts = {}) {
119
169
  const keepN = Number.isFinite(opts.keep) && opts.keep > 0 ? opts.keep : DEFAULT_KEEP;
120
170
  const sections = parseSections(content);
121
171
  const archived = [];
172
+ const quarantined = [];
122
173
  const sectionsTouched = [];
123
174
  let changed = false;
124
175
 
125
176
  for (const s of sections) {
126
177
  if (s.heading === null) continue;
127
178
  if (!APPEND_HEAVY.test(headingText(s.heading))) continue;
128
- const before = archived.length;
129
- const r = reconcileBullets(s.lines, keepN, archived);
179
+ const r = reconcileBullets(s.lines, keepN, archived, quarantined);
130
180
  if (r.changed) {
131
181
  s.lines = r.lines;
132
182
  changed = true;
133
183
  sectionsTouched.push(headingText(s.heading).trim());
134
184
  }
135
- void before;
136
185
  }
137
186
 
138
- return { content: changed ? joinSections(sections) : content, changed, archived, sectionsTouched };
187
+ return { content: changed ? joinSections(sections) : content, changed, archived, quarantined, sectionsTouched };
139
188
  }
140
189
 
141
190
  // ─── Command ────────────────────────────────────────────────────────────────
@@ -154,6 +203,30 @@ function appendArchive(cwd, entries, now) {
154
203
  fs.appendFileSync(p, block, 'utf-8');
155
204
  }
156
205
 
206
+ function quarantinePath(cwd) {
207
+ return path.join(planningPath(cwd), MEMORY_DIR, QUARANTINE_FILE);
208
+ }
209
+
210
+ /**
211
+ * Append quarantined directive-like entries to a dated, human-review file. These
212
+ * were pulled OUT of standing memory because they read like injected instructions
213
+ * (memory-injection defense). Reversible — a human can review and, if legitimate,
214
+ * restore an entry by hand. The file leads with a warning so it is never loaded
215
+ * as trusted instruction memory.
216
+ */
217
+ function appendQuarantine(cwd, entries, now) {
218
+ if (!entries.length) return;
219
+ const p = quarantinePath(cwd);
220
+ const fresh = !fs.existsSync(p);
221
+ fs.mkdirSync(path.dirname(p), { recursive: true });
222
+ const stamp = now || '(undated)';
223
+ const header = fresh
224
+ ? '# Quarantined memory (DO NOT auto-load as instructions)\n\nEntries below were pulled out of standing memory during reconcile because they read like\ndirectives aimed at the agent (possible cross-generation prompt injection). They are\nNOT trusted instructions. Review each; restore to state.md by hand only if legitimate.\n'
225
+ : '';
226
+ const block = `${header}\n## Quarantined ${stamp}\n\n${entries.join('\n')}\n`;
227
+ fs.appendFileSync(p, block, 'utf-8');
228
+ }
229
+
157
230
  /**
158
231
  * `memory optimize [--apply] [--keep N]` — reconcile state.md + consolidate
159
232
  * over-budget agent logs. Dry-run by default: reports what WOULD change.
@@ -172,12 +245,15 @@ function cmdMemoryOptimize(cwd, opts = {}, raw) {
172
245
  changed: opt.changed,
173
246
  sections_touched: opt.sectionsTouched,
174
247
  archived_entries: opt.archived.length,
248
+ quarantined_entries: opt.quarantined.length,
175
249
  before_bytes: Buffer.byteLength(before),
176
250
  after_bytes: Buffer.byteLength(opt.content),
177
251
  };
178
252
  result.archived = opt.archived.length;
253
+ result.quarantined = opt.quarantined.length;
179
254
  if (apply && opt.changed) {
180
255
  appendArchive(cwd, opt.archived, opts.now);
256
+ appendQuarantine(cwd, opt.quarantined, opts.now);
181
257
  writeStateMd(statePath, opt.content, cwd);
182
258
  }
183
259
  } else {
@@ -199,8 +275,10 @@ function cmdMemoryOptimize(cwd, opts = {}, raw) {
199
275
  }
200
276
  } catch { /* agent sweep is best-effort */ }
201
277
 
278
+ const q = result.quarantined || 0;
279
+ const quarantineNote = q ? `; ${q} directive-like entr${q === 1 ? 'y' : 'ies'} QUARANTINED` : '';
202
280
  const summary = result.state.changed
203
- ? `${apply ? 'optimized' : 'would optimize'} state.md (${result.state.sections_touched.join(', ')}); ${result.archived} entr${result.archived === 1 ? 'y' : 'ies'} archived${result.agents.length ? `; ${result.agents.length} agent log(s)` : ''}`
281
+ ? `${apply ? 'optimized' : 'would optimize'} state.md (${result.state.sections_touched.join(', ')}); ${result.archived} entr${result.archived === 1 ? 'y' : 'ies'} archived${quarantineNote}${result.agents.length ? `; ${result.agents.length} agent log(s)` : ''}`
204
282
  : `state.md already lean${result.agents.length ? `; ${result.agents.length} agent log(s) over budget` : ''} — nothing to do`;
205
283
  output(result, raw, summary);
206
284
  }
@@ -239,8 +317,9 @@ function maybeAutoOptimizeMemory(cwd, opts = {}) {
239
317
  const opt = optimizeStateContent(before, { keep: opts.keep });
240
318
  if (!opt.changed) return { optimized: false, reason: 'clean' };
241
319
  appendArchive(cwd, opt.archived, opts.now);
320
+ appendQuarantine(cwd, opt.quarantined, opts.now);
242
321
  writeStateMd(statePath, opt.content, cwd);
243
- return { optimized: true, sections: opt.sectionsTouched, archived: opt.archived.length };
322
+ return { optimized: true, sections: opt.sectionsTouched, archived: opt.archived.length, quarantined: opt.quarantined.length };
244
323
  } catch {
245
324
  return { optimized: false, reason: 'error' };
246
325
  }
@@ -248,6 +327,6 @@ function maybeAutoOptimizeMemory(cwd, opts = {}) {
248
327
 
249
328
  module.exports = {
250
329
  optimizeStateContent, reconcileBullets, parseSections, joinSections, cmdMemoryOptimize,
251
- maybeAutoOptimizeMemory, autoOptimizeEnabled,
252
- APPEND_HEAVY, PLACEHOLDER, DEFAULT_KEEP, STATE_ARCHIVE_FILE,
330
+ maybeAutoOptimizeMemory, autoOptimizeEnabled, isSuspiciousDirective,
331
+ APPEND_HEAVY, PLACEHOLDER, DIRECTIVE_PATTERNS, DEFAULT_KEEP, STATE_ARCHIVE_FILE, QUARANTINE_FILE,
253
332
  };
@@ -32,6 +32,24 @@ const {
32
32
  upsertAgentsMdSection,
33
33
  ensureClaudeMdImport,
34
34
  } = require('./agents-md.cjs');
35
+ const { isSuspiciousDirective } = require('./memory-optimize.cjs');
36
+
37
+ /**
38
+ * Scan a procedural-memory file (AGENTS.md / CLAUDE.md) for lines that read like
39
+ * directives aimed at the agent — a memory-injection risk in the ALWAYS-loaded
40
+ * instruction files (ADR-0040). rebuild owns only the marker-fenced PAN section
41
+ * (regenerated from a fixed template, so it can't be poisoned); user content is
42
+ * preserved by contract, so here we WARN rather than auto-edit — surfacing
43
+ * suspect lines for human review instead of silently rewriting the user's file.
44
+ */
45
+ function scanForDirectives(file, content, warnings) {
46
+ if (typeof content !== 'string') return;
47
+ content.split('\n').forEach((line, i) => {
48
+ if (isSuspiciousDirective(line)) {
49
+ warnings.push({ file, line: i + 1, text: line.trim().slice(0, 200) });
50
+ }
51
+ });
52
+ }
35
53
 
36
54
  // Source repo root — mirrors experiment.cjs / install.js. __dirname is
37
55
  // .../pan-wizard-core/bin/lib, so three levels up is the repo (or install) root.
@@ -106,6 +124,7 @@ function cmdMemoryRebuild(cwd, opts = {}, raw) {
106
124
 
107
125
  const runtimes = detectRuntimes(cwd);
108
126
  const targets = [];
127
+ const warnings = [];
109
128
 
110
129
  // 1. AGENTS.md — universal PAN section (all runtimes read it natively).
111
130
  {
@@ -113,6 +132,7 @@ function cmdMemoryRebuild(cwd, opts = {}, raw) {
113
132
  const existing = safeReadFile(p);
114
133
  const desired = upsertAgentsMdSection(existing, buildAgentsMdSection());
115
134
  targets.push({ file: 'AGENTS.md', ...rebuildFile(p, existing, desired, apply) });
135
+ scanForDirectives('AGENTS.md', desired, warnings);
116
136
  }
117
137
 
118
138
  // 2. CLAUDE.md — Claude bridge, only when the Claude runtime is installed.
@@ -121,6 +141,7 @@ function cmdMemoryRebuild(cwd, opts = {}, raw) {
121
141
  const existing = safeReadFile(p);
122
142
  const desired = ensureClaudeMdImport(existing);
123
143
  targets.push({ file: 'CLAUDE.md', ...rebuildFile(p, existing, desired, apply) });
144
+ scanForDirectives('CLAUDE.md', desired, warnings);
124
145
  }
125
146
 
126
147
  // 3. state.md — re-derive YAML frontmatter from the body (progress/status).
@@ -139,10 +160,12 @@ function cmdMemoryRebuild(cwd, opts = {}, raw) {
139
160
  runtimes,
140
161
  rebuilt: targets,
141
162
  changed_count: changed.length,
163
+ directive_warnings: warnings,
142
164
  };
143
- const summary = changed.length === 0
165
+ const warnNote = warnings.length ? `; ⚠ ${warnings.length} directive-like line(s) in procedural memory — review (not auto-edited)` : '';
166
+ const summary = (changed.length === 0
144
167
  ? `tools memory already current (${targets.map((t) => t.file).join(', ')}) — nothing to do`
145
- : `${apply ? 'rebuilt' : 'would rebuild'} ${changed.map((t) => `${t.file} (${t.action})`).join(', ')}`;
168
+ : `${apply ? 'rebuilt' : 'would rebuild'} ${changed.map((t) => `${t.file} (${t.action})`).join(', ')}`) + warnNote;
146
169
  output(result, raw, summary);
147
170
  }
148
171
 
@@ -151,6 +174,7 @@ module.exports = {
151
174
  detectRuntimes,
152
175
  isInsideSourceRepo,
153
176
  rebuildFile,
177
+ scanForDirectives,
154
178
  PAN_SOURCE_ROOT,
155
179
  RUNTIME_DIRS,
156
180
  };
@@ -27,6 +27,14 @@ hard-depend on, and the go/no-go facts that can only be settled on a real ZCode
27
27
  - **User-global subagents only.** No repo-scoped rosters or per-project model profiles.
28
28
  - **Genuinely lost:** scheduled self-resuming multi-day campaigns (no headless/daemon),
29
29
  background execution, committable permissions, and custom slash-commands.
30
+ - **Bridge scope is tool/resource exposure ONLY — never agent session state.** The MCP
31
+ server exposes `pan-tools` verbs as `tools/call` + `resources/read`; it deliberately
32
+ does not represent rich agent session state (diffs, streaming, live thread lifecycle).
33
+ This is by design, not an omission: MCP cannot faithfully carry that state — a point
34
+ OpenAI made explicitly when it built the Codex "harness" as a native Rust core rather
35
+ than over MCP (2026). Do NOT extend the bridge to stream diffs or proxy session state
36
+ through it; keep the CLI's JSON contract as the tool contract. (Codex-adapter audit,
37
+ 2026-07-30.)
30
38
 
31
39
  ## Format-drift policy
32
40
 
@@ -21,6 +21,8 @@ pan-zcode/mcp (this subsystem) a thin, zero-dep bridge — verbs → MCP tools
21
21
  pan-wizard-core (reused as-is) the deterministic engine; .planning/ stays the state store
22
22
  ```
23
23
 
24
+ **Scope boundary (by design):** the bridge exposes `pan-tools` verbs as MCP tools/resources and nothing more. It intentionally does **not** carry rich agent *session state* — diffs, streaming, live thread lifecycle — because MCP can't faithfully represent it (the reason OpenAI built the Codex harness as a native Rust core rather than over MCP). Keep the bridge to tool/resource exposure; the CLI's JSON contract is the tool contract. See `KNOWN-BETA-RISKS.md`.
25
+
24
26
  ## Status — M1–M5 built (M0 is the human verify spike)
25
27
 
26
28
  - **M1 — bridge core.** `mcp/tool-registry.cjs` (pure verb→tool/resource map, with a hard