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.
package/commands/pan/army.md
CHANGED
|
@@ -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`
|
|
395
|
-
2.
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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 !==
|
|
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
|
|
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
|
|
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
|
|
package/pan-zcode/README.md
CHANGED
|
@@ -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
|