pan-wizard 3.21.1 → 3.24.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/README.md +38 -24
- package/agents/pan-conductor.md +3 -3
- package/agents/pan-design-checker.md +83 -0
- package/agents/pan-designer.md +67 -0
- package/agents/pan-document_code.md +3 -2
- package/agents/pan-executor.md +2 -2
- package/agents/pan-plan-checker.md +19 -6
- package/agents/pan-planner.md +4 -1
- package/agents/pan-previewer.md +1 -1
- package/agents/pan-roadmapper.md +3 -1
- package/agents/pan-verifier.md +10 -10
- package/bin/install-lib.cjs +128 -17
- package/bin/install.js +480 -74
- package/commands/pan/army.md +20 -14
- package/commands/pan/audit-deployment.md +17 -14
- package/commands/pan/cost.md +2 -2
- package/commands/pan/debug.md +1 -1
- package/commands/pan/design-phase.md +77 -0
- package/commands/pan/exec-phase.md +2 -2
- package/commands/pan/experiment.md +18 -14
- package/commands/pan/focus-auto.md +5 -5
- package/commands/pan/focus-design.md +13 -1
- package/commands/pan/focus-exec.md +1 -1
- package/commands/pan/learn.md +1 -1
- package/commands/pan/links.md +3 -1
- package/commands/pan/map-codebase.md +4 -4
- package/commands/pan/mcp-bridge.md +1 -1
- package/commands/pan/milestone-new.md +3 -1
- package/commands/pan/optimize.md +2 -2
- package/commands/pan/patches.md +1 -1
- package/commands/pan/plan-phase.md +3 -1
- package/commands/pan/preview.md +3 -3
- package/commands/pan/profile.md +3 -3
- package/commands/pan/research-phase.md +1 -1
- package/commands/pan/retro.md +4 -1
- package/commands/pan/review-deep.md +1 -1
- package/commands/pan/settings.md +2 -2
- package/commands/pan/what-if.md +1 -1
- package/hooks/dist/pan-check-update.js +149 -38
- package/hooks/dist/pan-context-monitor.js +155 -81
- package/hooks/dist/pan-cost-logger.js +288 -13
- package/hooks/dist/pan-statusline.js +24 -8
- package/hooks/dist/pan-stop-guard.js +160 -0
- package/hooks/dist/pan-trace-logger.js +261 -19
- package/package.json +2 -2
- package/pan-wizard-core/bin/lib/bridge.cjs +1 -1
- package/pan-wizard-core/bin/lib/bus.cjs +19 -4
- package/pan-wizard-core/bin/lib/campaign.cjs +8 -3
- package/pan-wizard-core/bin/lib/codebase.cjs +15 -5
- package/pan-wizard-core/bin/lib/commands.cjs +88 -6
- package/pan-wizard-core/bin/lib/config.cjs +27 -2
- package/pan-wizard-core/bin/lib/constants.cjs +1 -0
- package/pan-wizard-core/bin/lib/core.cjs +113 -8
- package/pan-wizard-core/bin/lib/cost.cjs +61 -24
- package/pan-wizard-core/bin/lib/distill.cjs +22 -4
- package/pan-wizard-core/bin/lib/doc-lint.cjs +25 -7
- package/pan-wizard-core/bin/lib/experiment.cjs +25 -1
- package/pan-wizard-core/bin/lib/focus.cjs +42 -7
- package/pan-wizard-core/bin/lib/frontmatter.cjs +62 -31
- package/pan-wizard-core/bin/lib/git.cjs +51 -16
- package/pan-wizard-core/bin/lib/hud.cjs +17 -2
- package/pan-wizard-core/bin/lib/init.cjs +2 -5
- package/pan-wizard-core/bin/lib/knowledge.cjs +20 -1
- package/pan-wizard-core/bin/lib/learn-lint.cjs +50 -0
- package/pan-wizard-core/bin/lib/links.cjs +9 -6
- package/pan-wizard-core/bin/lib/lock.cjs +23 -4
- package/pan-wizard-core/bin/lib/memory-optimize.cjs +101 -17
- package/pan-wizard-core/bin/lib/memory-rebuild.cjs +26 -2
- package/pan-wizard-core/bin/lib/milestone.cjs +11 -5
- package/pan-wizard-core/bin/lib/optimize.cjs +18 -7
- package/pan-wizard-core/bin/lib/phase-remove.cjs +41 -2
- package/pan-wizard-core/bin/lib/phase.cjs +82 -11
- package/pan-wizard-core/bin/lib/preview.cjs +23 -4
- package/pan-wizard-core/bin/lib/review-deep.cjs +14 -6
- package/pan-wizard-core/bin/lib/roadmap.cjs +6 -2
- package/pan-wizard-core/bin/lib/runner.cjs +17 -7
- package/pan-wizard-core/bin/lib/squads.cjs +43 -11
- package/pan-wizard-core/bin/lib/state.cjs +32 -11
- package/pan-wizard-core/bin/lib/template.cjs +8 -3
- package/pan-wizard-core/bin/lib/verify-deploy.cjs +14 -2
- package/pan-wizard-core/bin/lib/verify-drift.cjs +15 -1
- package/pan-wizard-core/bin/lib/verify-preflight.cjs +9 -3
- package/pan-wizard-core/bin/lib/verify.cjs +84 -39
- package/pan-wizard-core/bin/lib/whatif.cjs +8 -1
- package/pan-wizard-core/bin/pan-tools.cjs +42 -19
- package/pan-wizard-core/learnings/index.json +7 -7
- package/pan-wizard-core/learnings/universal/autonomous-loop.md +5 -3
- package/pan-wizard-core/learnings/universal/concurrency.md +1 -1
- package/pan-wizard-core/references/design-methodology.md +94 -0
- package/pan-wizard-core/references/git-integration.md +9 -9
- package/pan-wizard-core/references/guardrails.md +4 -1
- package/pan-wizard-core/references/model-profile-resolution.md +1 -1
- package/pan-wizard-core/references/model-profiles.md +44 -34
- package/pan-wizard-core/references/planning-config.md +5 -5
- package/pan-wizard-core/references/verification-patterns.md +67 -0
- package/pan-wizard-core/templates/codebase/architecture.md +1 -1
- package/pan-wizard-core/templates/codebase/concerns.md +1 -1
- package/pan-wizard-core/templates/codebase/conventions.md +1 -1
- package/pan-wizard-core/templates/codebase/integrations.md +1 -1
- package/pan-wizard-core/templates/codebase/stack.md +1 -1
- package/pan-wizard-core/templates/codebase/structure.md +1 -1
- package/pan-wizard-core/templates/codebase/testing.md +1 -1
- package/pan-wizard-core/templates/design.md +146 -0
- package/pan-wizard-core/templates/discovery.md +3 -3
- package/pan-wizard-core/templates/milestone-archive.md +2 -2
- package/pan-wizard-core/templates/playbook.md +1 -1
- package/pan-wizard-core/templates/uat.md +1 -1
- package/pan-wizard-core/workflows/diagnose-issues.md +9 -7
- package/pan-wizard-core/workflows/exec-phase.md +28 -8
- package/pan-wizard-core/workflows/execute-plan.md +2 -3
- package/pan-wizard-core/workflows/health.md +24 -1
- package/pan-wizard-core/workflows/help.md +5 -4
- package/pan-wizard-core/workflows/learn.md +5 -5
- package/pan-wizard-core/workflows/map-codebase.md +1 -1
- package/pan-wizard-core/workflows/milestone-audit.md +1 -1
- package/pan-wizard-core/workflows/milestone-new.md +4 -4
- package/pan-wizard-core/workflows/new-project.md +47 -32
- package/pan-wizard-core/workflows/optimize.md +8 -8
- package/pan-wizard-core/workflows/pause.md +1 -1
- package/pan-wizard-core/workflows/phase-tests.md +2 -2
- package/pan-wizard-core/workflows/plan-phase.md +4 -0
- package/pan-wizard-core/workflows/profile.md +13 -10
- package/pan-wizard-core/workflows/research-phase.md +1 -1
- package/pan-wizard-core/workflows/resume-project.md +1 -1
- package/pan-wizard-core/workflows/settings.md +9 -9
- package/pan-wizard-core/workflows/transition.md +24 -8
- package/pan-wizard-core/workflows/update.md +23 -20
- package/pan-wizard-core/workflows/verify-phase.md +7 -2
- package/pan-zcode/KNOWN-BETA-RISKS.md +8 -0
- package/pan-zcode/README.md +14 -2
- package/pan-zcode/bin/install-zcode.js +1 -1
- package/pan-zcode/mcp/native-tools.cjs +1 -1
- package/pan-zcode/mcp/server.cjs +76 -14
- package/scripts/build-hooks.js +2 -1
- package/scripts/generate-skills-docs.py +0 -4
- package/scripts/run-tests.cjs +11 -4
|
@@ -121,7 +121,7 @@ function initTraceSession(cwd, opts = {}) {
|
|
|
121
121
|
|
|
122
122
|
return { session_id: sessionId, started_at: meta.started_at, directory: sessionDir, reused: false };
|
|
123
123
|
} catch (e) {
|
|
124
|
-
return { error: e.message };
|
|
124
|
+
return { error: e.message || 'trace_init_failed' };
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -278,7 +278,7 @@ function endTraceSession(cwd, sessionId) {
|
|
|
278
278
|
ended_at: meta.ended_at,
|
|
279
279
|
};
|
|
280
280
|
} catch (e) {
|
|
281
|
-
return { error: e.message };
|
|
281
|
+
return { error: e.message || 'trace_read_failed' };
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
|
|
@@ -303,7 +303,7 @@ function reconcileTraceSession(cwd, sessionId) {
|
|
|
303
303
|
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
304
304
|
return { session_id: sid, reconciled: true, event_count: counts.event_count, malformed_count: counts.malformed_count };
|
|
305
305
|
} catch (e) {
|
|
306
|
-
return { error: e.message };
|
|
306
|
+
return { error: e.message || 'trace_write_failed' };
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
|
|
@@ -349,7 +349,7 @@ function readTraceSession(cwd, sessionId) {
|
|
|
349
349
|
|
|
350
350
|
return { session_id: sessionId, metadata, events, event_count: events.length, malformed_count: malformed };
|
|
351
351
|
} catch (e) {
|
|
352
|
-
return { error: e.message };
|
|
352
|
+
return { error: e.message || 'trace_end_failed' };
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
|
|
@@ -380,7 +380,7 @@ function listTraceSessions(cwd) {
|
|
|
380
380
|
|
|
381
381
|
return { sessions, count: sessions.length };
|
|
382
382
|
} catch (e) {
|
|
383
|
-
return { error: e.message };
|
|
383
|
+
return { error: e.message || 'trace_reconcile_failed' };
|
|
384
384
|
}
|
|
385
385
|
}
|
|
386
386
|
|
|
@@ -585,7 +585,7 @@ function listOptimizationReports(cwd) {
|
|
|
585
585
|
|
|
586
586
|
return { reports, count: reports.length };
|
|
587
587
|
} catch (e) {
|
|
588
|
-
return { error: e.message };
|
|
588
|
+
return { error: e.message || 'report_write_failed' };
|
|
589
589
|
}
|
|
590
590
|
}
|
|
591
591
|
|
|
@@ -626,8 +626,19 @@ function applyReportRecommendations(cwd, reportPath) {
|
|
|
626
626
|
const applied = [];
|
|
627
627
|
const skipped = [];
|
|
628
628
|
|
|
629
|
+
const resolvedCwd = path.resolve(cwd);
|
|
629
630
|
for (const action of actions) {
|
|
630
631
|
try {
|
|
632
|
+
// Containment: action.path on memory writes is report/agent-authored and
|
|
633
|
+
// must stay inside the project. A `../` (or absolute) path otherwise
|
|
634
|
+
// escapes and writes anywhere the process can (M23, ADR audit 2026-08).
|
|
635
|
+
if (action.type === 'memory' || action.type === 'memory_append') {
|
|
636
|
+
const abs = path.resolve(cwd, action.path || '');
|
|
637
|
+
if (abs !== resolvedCwd && !abs.startsWith(resolvedCwd + path.sep)) {
|
|
638
|
+
skipped.push({ action, reason: 'path escapes project root — skipped' });
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
631
642
|
if (action.type === 'memory') {
|
|
632
643
|
// Write new memory entry (skip if file exists to avoid overwriting manual edits)
|
|
633
644
|
const memPath = path.join(cwd, action.path);
|
|
@@ -751,7 +762,7 @@ function getOptimizeStats(cwd) {
|
|
|
751
762
|
current_session: getCurrentSessionId(cwd),
|
|
752
763
|
};
|
|
753
764
|
} catch (e) {
|
|
754
|
-
return { error: e.message };
|
|
765
|
+
return { error: e.message || 'apply_failed' };
|
|
755
766
|
}
|
|
756
767
|
}
|
|
757
768
|
|
|
@@ -20,7 +20,7 @@ function removePhaseFromDisk(phaseDir) {
|
|
|
20
20
|
try {
|
|
21
21
|
fs.rmSync(phaseDir, { recursive: true, force: true });
|
|
22
22
|
} catch (e) {
|
|
23
|
-
return { removed: false, error: e.message };
|
|
23
|
+
return { removed: false, error: e.message || 'remove_failed' };
|
|
24
24
|
}
|
|
25
25
|
return { removed: true };
|
|
26
26
|
}
|
|
@@ -214,7 +214,12 @@ function updateRoadmapAfterRemoval(cwd, phaseNum, isDecimal, normalized) {
|
|
|
214
214
|
|
|
215
215
|
// Reasonable upper bound for phase numbers
|
|
216
216
|
const maxPhase = 99;
|
|
217
|
-
|
|
217
|
+
// Floor the loop independently of removedInt. cmdPhaseRemove rejects a phase
|
|
218
|
+
// below 1 before we get here, but this function is also reachable directly,
|
|
219
|
+
// and an unbounded lower end is how `phase remove -1234567890` turned into
|
|
220
|
+
// ~1.2 billion regex passes. The loop can now never exceed maxPhase steps.
|
|
221
|
+
const stopAt = Math.max(removedInt, 0);
|
|
222
|
+
for (let oldNum = maxPhase; oldNum > stopAt; oldNum--) {
|
|
218
223
|
const newNum = oldNum - 1;
|
|
219
224
|
const oldStr = String(oldNum);
|
|
220
225
|
const newStr = String(newNum);
|
|
@@ -269,6 +274,23 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
269
274
|
error('phase number required for phase remove');
|
|
270
275
|
}
|
|
271
276
|
|
|
277
|
+
// Validate the identifier BEFORE touching the roadmap or any directory.
|
|
278
|
+
// normalizePhaseName does not validate — it returns its input unchanged when
|
|
279
|
+
// PHASE_NUM_RE does not match — so a bad argument used to reach the renumber
|
|
280
|
+
// logic and do real damage, in both cases reporting success and exiting 0:
|
|
281
|
+
// `phase remove 0` renumbered EVERY roadmap heading down to "Phase 0"
|
|
282
|
+
// (the descending renumber loop runs 99..1 and re-hits
|
|
283
|
+
// its own output, collapsing 3->2->1->0) and renamed
|
|
284
|
+
// every phase directory one lower.
|
|
285
|
+
// `phase remove -1e9` spun that same loop ~1.2 billion times, each pass
|
|
286
|
+
// running several regex replaces over the roadmap —
|
|
287
|
+
// hours of CPU, no output, no way to tell it was stuck.
|
|
288
|
+
// Phases are numbered from 1, so anything below that is a typo, never a target.
|
|
289
|
+
const phaseIdent = String(targetPhase).trim();
|
|
290
|
+
if (!/^\d+[A-Za-z]?(?:\.\d+)*$/.test(phaseIdent) || parseInt(phaseIdent, 10) < 1) {
|
|
291
|
+
error(`invalid phase number: ${targetPhase} (expected a phase numbered from 1, e.g. 3, 3A or 3.1)`);
|
|
292
|
+
}
|
|
293
|
+
|
|
272
294
|
const roadmapPath = path.join(planningPath(cwd), ROADMAP_FILE);
|
|
273
295
|
const phasesDir = phasesPath(cwd);
|
|
274
296
|
const force = options.force || false;
|
|
@@ -291,6 +313,23 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
291
313
|
// Phases directory does not exist; targetDir remains null
|
|
292
314
|
}
|
|
293
315
|
|
|
316
|
+
// Refuse a phase that exists nowhere. A phase CAN legitimately have a roadmap
|
|
317
|
+
// entry and no directory yet (planned, not scaffolded), so only refuse when it
|
|
318
|
+
// is in neither place. Previously this reported {removed: <n>} with exit 0 for
|
|
319
|
+
// a phase that was never there, and claimed roadmap.md and state.md had been
|
|
320
|
+
// rewritten — an orchestrator reads that as "it is gone" and moves on.
|
|
321
|
+
if (!targetDir) {
|
|
322
|
+
let roadmapText = '';
|
|
323
|
+
try {
|
|
324
|
+
roadmapText = fs.readFileSync(roadmapPath, 'utf8');
|
|
325
|
+
} catch { /* unreadable roadmap is reported below as not-found */ }
|
|
326
|
+
const displayNum = escapeRegex(String(parseInt(phaseIdent, 10)) + (phaseIdent.includes('.') ? '.' + phaseIdent.split('.').slice(1).join('.') : ''));
|
|
327
|
+
const inRoadmap = new RegExp(`^#{2,4}\\s*Phase\\s+0*${displayNum}(\\D|$)`, 'im').test(roadmapText);
|
|
328
|
+
if (!inRoadmap) {
|
|
329
|
+
error(`Phase ${targetPhase} not found — no phase directory and no roadmap entry`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
294
333
|
// Check for executed work (summary.md files)
|
|
295
334
|
if (targetDir && !force) {
|
|
296
335
|
const targetPath = path.join(phasesDir, targetDir);
|
|
@@ -41,11 +41,19 @@ function cmdPhasesList(cwd, options, raw) {
|
|
|
41
41
|
// Get all phase directories
|
|
42
42
|
let dirs = entries.filter(entry => entry.isDirectory()).map(entry => entry.name);
|
|
43
43
|
|
|
44
|
-
// Include archived phases if requested
|
|
44
|
+
// Include archived phases if requested. Archived entries are DECORATED
|
|
45
|
+
// display strings ("<name> [<milestone>]") that do not exist under
|
|
46
|
+
// phasesDir — their real location is getArchivedPhaseDirs()'s fullPath
|
|
47
|
+
// (under .planning/milestones/). M24: track that real path so a later
|
|
48
|
+
// --type read opens the archive dir instead of crashing with ENOENT on the
|
|
49
|
+
// decorated name joined to phasesDir.
|
|
50
|
+
const archivedPaths = new Map();
|
|
45
51
|
if (includeArchived) {
|
|
46
52
|
const archived = getArchivedPhaseDirs(cwd);
|
|
47
53
|
for (const arch of archived) {
|
|
48
|
-
|
|
54
|
+
const decorated = `${arch.name} [${arch.milestone}]`;
|
|
55
|
+
dirs.push(decorated);
|
|
56
|
+
archivedPaths.set(decorated, arch.fullPath);
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
|
|
@@ -67,7 +75,7 @@ function cmdPhasesList(cwd, options, raw) {
|
|
|
67
75
|
if (type) {
|
|
68
76
|
const files = [];
|
|
69
77
|
for (const dir of dirs) {
|
|
70
|
-
const dirPath = path.join(phasesDir, dir);
|
|
78
|
+
const dirPath = archivedPaths.get(dir) || path.join(phasesDir, dir);
|
|
71
79
|
const dirFiles = fs.readdirSync(dirPath);
|
|
72
80
|
|
|
73
81
|
let filtered;
|
|
@@ -347,6 +355,32 @@ function buildPlanIndex(phaseDir, planFiles, summaryFiles) {
|
|
|
347
355
|
return { plans, waves, incomplete, hasCheckpoints };
|
|
348
356
|
}
|
|
349
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Add a `- [ ] **Phase N: name**` entry to the roadmap's checklist, after the last
|
|
360
|
+
* existing entry.
|
|
361
|
+
*
|
|
362
|
+
* Returns the content unchanged when the roadmap has no checklist at all — some
|
|
363
|
+
* roadmaps only carry detail sections, and inventing a list there would be a bigger
|
|
364
|
+
* surprise than omitting one entry. Format matches templates/roadmap.md exactly, so
|
|
365
|
+
* the entry is picked up by the same parsers that read the template's own output.
|
|
366
|
+
*
|
|
367
|
+
* @param {string} content - Full roadmap.md content
|
|
368
|
+
* @param {string|number} phaseNum - Phase number as it should appear
|
|
369
|
+
* @param {string} name - Phase name/description
|
|
370
|
+
* @returns {string} Updated content
|
|
371
|
+
*/
|
|
372
|
+
function appendPhaseChecklistEntry(content, phaseNum, name) {
|
|
373
|
+
const lines = content.split('\n');
|
|
374
|
+
const isChecklistLine = (l) => /^\s*- \[[ x]\]\s*(?:\*\*)?Phase\s+\d/i.test(l);
|
|
375
|
+
let lastIdx = -1;
|
|
376
|
+
for (let i = 0; i < lines.length; i++) {
|
|
377
|
+
if (isChecklistLine(lines[i])) lastIdx = i;
|
|
378
|
+
}
|
|
379
|
+
if (lastIdx === -1) return content; // no checklist to extend
|
|
380
|
+
lines.splice(lastIdx + 1, 0, `- [ ] **Phase ${phaseNum}: ${name}**`);
|
|
381
|
+
return lines.join('\n');
|
|
382
|
+
}
|
|
383
|
+
|
|
350
384
|
/**
|
|
351
385
|
* Append a new phase to the end of the roadmap and create its directory.
|
|
352
386
|
* @param {string} cwd - Working directory path
|
|
@@ -402,6 +436,13 @@ function cmdPhaseAdd(cwd, description, raw) {
|
|
|
402
436
|
updatedContent = content + phaseEntry;
|
|
403
437
|
}
|
|
404
438
|
|
|
439
|
+
// Also add the checklist entry. Only the detail section was written before, so a
|
|
440
|
+
// phase created by `phase add` was permanently absent from the roadmap's "## Phases"
|
|
441
|
+
// list — the surface users read, and the one preview/progress consumers parse. Worse,
|
|
442
|
+
// `phase complete` then reported roadmap_updated:true having changed nothing, because
|
|
443
|
+
// it ticks a checklist line that was never there.
|
|
444
|
+
updatedContent = appendPhaseChecklistEntry(updatedContent, newPhaseNum, description);
|
|
445
|
+
|
|
405
446
|
try {
|
|
406
447
|
fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
|
|
407
448
|
} catch (e) {
|
|
@@ -501,7 +542,10 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
501
542
|
insertIdx = content.length;
|
|
502
543
|
}
|
|
503
544
|
|
|
504
|
-
|
|
545
|
+
// Insert the detail section, then the matching checklist entry — same reason as
|
|
546
|
+
// cmdPhaseAdd: without it the inserted phase never appears in the "## Phases" list.
|
|
547
|
+
const withEntry = content.slice(0, insertIdx) + phaseEntry + content.slice(insertIdx);
|
|
548
|
+
const updatedContent = appendPhaseChecklistEntry(withEntry, decimalPhase, description);
|
|
505
549
|
try {
|
|
506
550
|
fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
|
|
507
551
|
} catch (e) {
|
|
@@ -542,20 +586,33 @@ function markPhaseCompleteInRoadmap(cwd, phaseNum, _phaseName, planCount, summar
|
|
|
542
586
|
try {
|
|
543
587
|
roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
544
588
|
} catch {
|
|
545
|
-
return;
|
|
589
|
+
return { roadmap_warning: `${ROADMAP_FILE} unreadable — phase completion was not recorded in the roadmap` };
|
|
546
590
|
}
|
|
547
591
|
|
|
592
|
+
// Match the phase number the way the roadmap spells it, not the way the
|
|
593
|
+
// caller spelled it. Callers pass "01" (the dir/state spelling) as often as
|
|
594
|
+
// "1", while the template checklist says "Phase 1:" — matching on the raw
|
|
595
|
+
// argument made the tick a silent no-op for padded calls: state advanced,
|
|
596
|
+
// checkbox stayed unticked (PanLoop finding 8). `0*` on the unpadded form
|
|
597
|
+
// accepts both spellings; decimals ("1.1") survive the strip.
|
|
598
|
+
const unpadded = String(phaseNum).replace(/^0+(?=\d)/, '');
|
|
599
|
+
const numPat = `0*${escapeRegex(unpadded)}`;
|
|
600
|
+
|
|
601
|
+
// A tick that silently misses is indistinguishable from one that landed, so
|
|
602
|
+
// detect up front whether ANY checklist entry (ticked or not) names this
|
|
603
|
+
// phase — if none does, say so instead of best-effort-ing past it.
|
|
604
|
+
const hasChecklistEntry = new RegExp(`-\\s*\\[[ xX]\\]\\s*.*Phase\\s+${numPat}[:\\s]`, 'i').test(roadmapContent);
|
|
605
|
+
|
|
548
606
|
// Checkbox: - [ ] Phase N: -> - [x] Phase N: (...completed DATE)
|
|
549
607
|
const checkboxPattern = new RegExp(
|
|
550
|
-
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${
|
|
608
|
+
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${numPat}[:\\s][^\\n]*)`,
|
|
551
609
|
'i'
|
|
552
610
|
);
|
|
553
611
|
roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
|
|
554
612
|
|
|
555
613
|
// Progress table: update Plans Complete, Status, and Date columns
|
|
556
|
-
const phaseEscaped = escapeRegex(phaseNum);
|
|
557
614
|
const tablePattern = new RegExp(
|
|
558
|
-
`(\\|\\s*${
|
|
615
|
+
`(\\|\\s*${numPat}\\.?\\s[^|]*\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`,
|
|
559
616
|
'i'
|
|
560
617
|
);
|
|
561
618
|
roadmapContent = roadmapContent.replace(
|
|
@@ -565,7 +622,7 @@ function markPhaseCompleteInRoadmap(cwd, phaseNum, _phaseName, planCount, summar
|
|
|
565
622
|
|
|
566
623
|
// Update plan count in phase section
|
|
567
624
|
const planCountPattern = new RegExp(
|
|
568
|
-
`(#{2,4}\\s*Phase\\s+${
|
|
625
|
+
`(#{2,4}\\s*Phase\\s+${numPat}[\\s\\S]*?(?:\\*\\*Plans:\\*\\*|\\*\\*Plans\\*\\*:)\\s*)[^\\n]+`,
|
|
569
626
|
'i'
|
|
570
627
|
);
|
|
571
628
|
roadmapContent = roadmapContent.replace(
|
|
@@ -573,14 +630,21 @@ function markPhaseCompleteInRoadmap(cwd, phaseNum, _phaseName, planCount, summar
|
|
|
573
630
|
`$1${summaryCount}/${planCount} plans complete`
|
|
574
631
|
);
|
|
575
632
|
|
|
633
|
+
let roadmapWarning = hasChecklistEntry
|
|
634
|
+
? null
|
|
635
|
+
: `no roadmap checklist entry found for Phase ${phaseNum} — checkbox not ticked; check the "- [ ] **Phase N:" line in ${ROADMAP_FILE}`;
|
|
636
|
+
|
|
576
637
|
try {
|
|
577
638
|
fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
|
|
578
639
|
} catch {
|
|
579
|
-
|
|
640
|
+
roadmapWarning = `${ROADMAP_FILE} could not be written — phase completion was not recorded in the roadmap`;
|
|
580
641
|
}
|
|
581
642
|
|
|
582
643
|
const reqResult = markRequirementsCompleteForPhase(cwd, phaseNum, roadmapContent);
|
|
583
|
-
return {
|
|
644
|
+
return {
|
|
645
|
+
requirements_warning: reqResult && reqResult.error ? reqResult.error : null,
|
|
646
|
+
roadmap_warning: roadmapWarning,
|
|
647
|
+
};
|
|
584
648
|
}
|
|
585
649
|
|
|
586
650
|
/**
|
|
@@ -779,6 +843,13 @@ function cmdPhaseComplete(cwd, phaseNum, raw, opts) {
|
|
|
779
843
|
if (roadmapResult && roadmapResult.requirements_warning) {
|
|
780
844
|
result.requirements_warning = roadmapResult.requirements_warning;
|
|
781
845
|
}
|
|
846
|
+
if (roadmapResult && roadmapResult.roadmap_warning) {
|
|
847
|
+
// roadmap_updated previously meant "roadmap.md exists", which reads as
|
|
848
|
+
// "the tick landed" and masked a silently missed checkbox (finding 8).
|
|
849
|
+
// When the tick did not land, say so on both fields.
|
|
850
|
+
result.roadmap_warning = roadmapResult.roadmap_warning;
|
|
851
|
+
result.roadmap_updated = false;
|
|
852
|
+
}
|
|
782
853
|
|
|
783
854
|
// Auto-commit .planning/ metadata unless --no-commit or not a git repo
|
|
784
855
|
const noCommit = opts && opts.noCommit;
|
|
@@ -139,7 +139,7 @@ function buildPhasePreview(cwd, phaseNum) {
|
|
|
139
139
|
|
|
140
140
|
return {
|
|
141
141
|
phase: String(phaseNum),
|
|
142
|
-
phase_name: phaseInfo.
|
|
142
|
+
phase_name: phaseInfo.phase_name || (roadmapPhase && roadmapPhase.phase_name) || null,
|
|
143
143
|
directory: toPosix(phaseInfo.directory),
|
|
144
144
|
status,
|
|
145
145
|
plan_count: planFiles.length,
|
|
@@ -269,11 +269,30 @@ function buildPhaseDependencyGraph(cwd) {
|
|
|
269
269
|
};
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
// Checklist entries come in two shapes. templates/roadmap.md prescribes the bold
|
|
273
|
+
// one WITH a trailing description:
|
|
274
|
+
// - [ ] **Phase 1: Foundation** - Core scaffolding
|
|
275
|
+
// and hand-written roadmaps often use the plain one:
|
|
276
|
+
// - [ ] Phase 1: Foundation
|
|
277
|
+
//
|
|
278
|
+
// A single regex handled these badly. The previous pattern
|
|
279
|
+
// (`([^\n*]+?)(?:\*\*)?\s*$`) required the line to END at the closing `**`, so
|
|
280
|
+
// every template-shaped line — the format PAN itself writes — failed to match and
|
|
281
|
+
// `preview phases` reported a phase_count with an EMPTY phases array, contradicting
|
|
282
|
+
// itself. Widening the name group to allow a trailing description then broke names
|
|
283
|
+
// containing a hyphen, because the lazy match stopped at the first one.
|
|
284
|
+
//
|
|
285
|
+
// Matching bold-first is what resolves it: `[^*\n]+` cannot cross `*`, so it runs
|
|
286
|
+
// to the closing `**` regardless of hyphens inside the name, and anything after it
|
|
287
|
+
// is description we discard.
|
|
288
|
+
const CHECKLIST_BOLD_RE = /^\s*- \[([ x])\]\s*\*\*Phase\s+(\d+(?:\.\d+)?)\s*[:\-—]?\s*([^*\n]+?)\s*\*\*/i;
|
|
289
|
+
const CHECKLIST_PLAIN_RE = /^\s*- \[([ x])\]\s*Phase\s+(\d+(?:\.\d+)?)\s*[:\-—]?\s*([^\n]+?)\s*$/i;
|
|
290
|
+
|
|
272
291
|
function extractPhaseListFromRoadmap(content) {
|
|
273
292
|
const phases = [];
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
293
|
+
for (const line of String(content).split('\n')) {
|
|
294
|
+
const m = line.match(CHECKLIST_BOLD_RE) || line.match(CHECKLIST_PLAIN_RE);
|
|
295
|
+
if (!m) continue;
|
|
277
296
|
phases.push({
|
|
278
297
|
num: m[2],
|
|
279
298
|
name: m[3].trim(),
|
|
@@ -246,11 +246,19 @@ function writeDeepReview(cwd, phaseNum, payload, opts) {
|
|
|
246
246
|
|
|
247
247
|
// ─── CLI wrappers ───────────────────────────────────────────────────────────
|
|
248
248
|
|
|
249
|
+
// M25: input files must resolve against the passed cwd, not process.cwd().
|
|
250
|
+
// A relative --reviewer-file/--hardener-file/--meta-file was read from the
|
|
251
|
+
// process working directory, silently missing the file when --cwd differed.
|
|
252
|
+
function resolveAgainstCwd(cwd, p) {
|
|
253
|
+
if (!p) return p;
|
|
254
|
+
return path.isAbsolute(p) ? p : path.join(cwd, p);
|
|
255
|
+
}
|
|
256
|
+
|
|
249
257
|
function cmdReviewDeepMerge(cwd, phaseNum, opts, raw) {
|
|
250
258
|
if (!phaseNum) error('Usage: review-deep merge <phase> --reviewer-file X --hardener-file Y [--meta-file Z]');
|
|
251
|
-
const reviewerContent = opts.reviewerFile ? safeReadFile(opts.reviewerFile) : '';
|
|
252
|
-
const hardenerContent = opts.hardenerFile ? safeReadFile(opts.hardenerFile) : '';
|
|
253
|
-
const metaContent = opts.metaFile ? safeReadFile(opts.metaFile) : '';
|
|
259
|
+
const reviewerContent = opts.reviewerFile ? safeReadFile(resolveAgainstCwd(cwd, opts.reviewerFile)) : '';
|
|
260
|
+
const hardenerContent = opts.hardenerFile ? safeReadFile(resolveAgainstCwd(cwd, opts.hardenerFile)) : '';
|
|
261
|
+
const metaContent = opts.metaFile ? safeReadFile(resolveAgainstCwd(cwd, opts.metaFile)) : '';
|
|
254
262
|
if (!reviewerContent && !hardenerContent && !metaContent) {
|
|
255
263
|
output({ error: 'No input files provided or readable' }, raw);
|
|
256
264
|
return;
|
|
@@ -264,9 +272,9 @@ function cmdReviewDeepMerge(cwd, phaseNum, opts, raw) {
|
|
|
264
272
|
function cmdReviewDeepAnalyze(cwd, phaseNum, opts, raw) {
|
|
265
273
|
// Returns the merged payload WITHOUT writing a file. Useful for piping.
|
|
266
274
|
if (!phaseNum) error('Usage: review-deep analyze <phase> --reviewer-file X --hardener-file Y [--meta-file Z]');
|
|
267
|
-
const reviewerContent = opts.reviewerFile ? safeReadFile(opts.reviewerFile) : '';
|
|
268
|
-
const hardenerContent = opts.hardenerFile ? safeReadFile(opts.hardenerFile) : '';
|
|
269
|
-
const metaContent = opts.metaFile ? safeReadFile(opts.metaFile) : '';
|
|
275
|
+
const reviewerContent = opts.reviewerFile ? safeReadFile(resolveAgainstCwd(cwd, opts.reviewerFile)) : '';
|
|
276
|
+
const hardenerContent = opts.hardenerFile ? safeReadFile(resolveAgainstCwd(cwd, opts.hardenerFile)) : '';
|
|
277
|
+
const metaContent = opts.metaFile ? safeReadFile(resolveAgainstCwd(cwd, opts.metaFile)) : '';
|
|
270
278
|
output(mergeReviews(reviewerContent, hardenerContent, metaContent), raw);
|
|
271
279
|
}
|
|
272
280
|
|
|
@@ -336,6 +336,8 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
336
336
|
const summaryCount = phaseInfo.summaries.length;
|
|
337
337
|
|
|
338
338
|
if (planCount === 0) {
|
|
339
|
+
// No error key, exit 0: a phase with no plans has nothing to sync. The roadmap
|
|
340
|
+
// already says what it should say.
|
|
339
341
|
output({ updated: false, reason: 'No plans found', plan_count: 0, summary_count: 0 }, raw, 'no plans');
|
|
340
342
|
return;
|
|
341
343
|
}
|
|
@@ -348,7 +350,8 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
348
350
|
try {
|
|
349
351
|
roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
350
352
|
} catch {
|
|
351
|
-
|
|
353
|
+
// error key => exit 1: there are plans to record and nowhere to record them.
|
|
354
|
+
output({ updated: false, reason: 'roadmap.md not found', error: 'roadmap_not_found', plan_count: planCount, summary_count: summaryCount }, raw, 'no roadmap');
|
|
352
355
|
return;
|
|
353
356
|
}
|
|
354
357
|
const phaseEscaped = escapeRegex(phaseNum);
|
|
@@ -400,7 +403,8 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
400
403
|
try {
|
|
401
404
|
fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
|
|
402
405
|
} catch (err) {
|
|
403
|
-
|
|
406
|
+
// error key => exit 1: the write failed, so progress was computed and then lost.
|
|
407
|
+
output({ updated: false, reason: 'Failed to write roadmap.md: ' + err.message, error: err.message || 'roadmap_write_failed' }, raw, 'write error');
|
|
404
408
|
return;
|
|
405
409
|
}
|
|
406
410
|
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* Exports:
|
|
14
14
|
* - runExperiment(slug, opts) — spawn + observe + return result
|
|
15
15
|
* - tailExperimentState(slug, opts) — read run-state.json snapshot
|
|
16
|
-
* - stopExperiment(slug, opts) —
|
|
16
|
+
* - stopExperiment(slug, opts) — reconcile the run-state of an experiment
|
|
17
|
+
* (see its doc for the spawnSync limitation)
|
|
17
18
|
* - RUNTIME_RUNNERS — adapter map (per-runtime headless invocation)
|
|
18
19
|
*/
|
|
19
20
|
|
|
@@ -170,7 +171,7 @@ function appendEvent(state, type, details) {
|
|
|
170
171
|
* @param {string} [opts.root] - experiment root (default PAN_EXPERIMENTS_ROOT_DEFAULT)
|
|
171
172
|
* @param {string} [opts.prompt] - prompt passed to the external runtime; default
|
|
172
173
|
* is `/pan:new-project --auto @.planning/idea.md`
|
|
173
|
-
* @param {number} [opts.timeoutMs] - hard timeout (default
|
|
174
|
+
* @param {number} [opts.timeoutMs] - hard timeout (default 60 min)
|
|
174
175
|
* @param {object} [opts.runtimeOverride] - { bin, buildArgs } to bypass the manifest's
|
|
175
176
|
* runtime adapter (used by tests)
|
|
176
177
|
* @param {function} [opts.onProgress] - callback invoked per line of stdout/stderr
|
|
@@ -415,13 +416,22 @@ function tailExperimentState(slug, opts = {}) {
|
|
|
415
416
|
// ── stopExperiment ──────────────────────────────────────────────────────────
|
|
416
417
|
|
|
417
418
|
/**
|
|
418
|
-
*
|
|
419
|
+
* Reconcile the run-state of an experiment.
|
|
419
420
|
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
421
|
+
* IMPORTANT (M26): this CANNOT terminate a live, in-flight run. runExperiment()
|
|
422
|
+
* uses a synchronous spawnSync (see its comments — a real-time/async runner is
|
|
423
|
+
* deferred to W3), which blocks until the child exits and only learns the child
|
|
424
|
+
* pid FROM the return value. So while a run is in flight run-state.json still has
|
|
425
|
+
* pid=null, and by the time a pid is recorded the process has already exited.
|
|
426
|
+
* There is therefore no live child for this function to signal.
|
|
423
427
|
*
|
|
424
|
-
*
|
|
428
|
+
* Behavior:
|
|
429
|
+
* - status != 'running' → return the current state (already finished; no error).
|
|
430
|
+
* - status == 'running' but no pid recorded → return an error explaining that
|
|
431
|
+
* the synchronous runner does not expose a live pid to signal.
|
|
432
|
+
* - status == 'running' with a (stale) pid → best-effort SIGTERM and mark the
|
|
433
|
+
* run failed/manual. Note the pid is almost certainly already dead under the
|
|
434
|
+
* spawnSync model; live termination lands with the W3 async runner.
|
|
425
435
|
*/
|
|
426
436
|
function stopExperiment(slug, opts = {}) {
|
|
427
437
|
const root = opts.root || PAN_EXPERIMENTS_ROOT_DEFAULT;
|
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Squads — agent groupings for the bot-army model (ADR-0032).
|
|
3
3
|
*
|
|
4
|
-
* A squad is a named
|
|
5
|
-
*
|
|
6
|
-
* resolver only — it modifies no agent
|
|
7
|
-
*
|
|
4
|
+
* A squad is a named grouping of existing PAN agents under the pan-conductor
|
|
5
|
+
* coordinator, labelled with an intended model tier and an intended access
|
|
6
|
+
* contract. This module is a registry + resolver only — it modifies no agent
|
|
7
|
+
* and changes no execution path. `/pan:army` (ADR-0033) and `hud.cjs` read it;
|
|
8
|
+
* `grep -rn "squads.cjs')" pan-wizard-core/` enumerates its consumers.
|
|
9
|
+
*
|
|
10
|
+
* BOTH LABELS BELOW ARE ADVISORY METADATA, NOT ENFORCEMENT. Read this before
|
|
11
|
+
* writing any doc sentence about them, because the live docs now quote this
|
|
12
|
+
* header as the authority for exactly that (`grep -rn 'changes no execution
|
|
13
|
+
* path' docs commands` finds them):
|
|
14
|
+
*
|
|
15
|
+
* - `access` is the intended least-privilege contract the conductor's prompt is
|
|
16
|
+
* told to honour when it delegates. Nothing here strips a tool from an agent
|
|
17
|
+
* or narrows a grant at spawn time. The binding grant is each agent's own
|
|
18
|
+
* `tools:` frontmatter (`grep '^tools:' agents/*.md`), and it can be wider
|
|
19
|
+
* than the label: several members of a `read-only` squad hold `Write` so they
|
|
20
|
+
* can emit planning or verification artifacts, and the coordinator itself
|
|
21
|
+
* holds `Write` and `Bash`. So a squad agent doing more than its label says
|
|
22
|
+
* is the expected case, not a bug in this file.
|
|
23
|
+
* - `tier` is a squad grouping attribute, not the resolver of anybody's model.
|
|
24
|
+
* The active `model_profile` plus any per-agent `model:` pin decides that.
|
|
25
|
+
*
|
|
26
|
+
* The one rail the grants do enforce is delegation depth: only an agent granted
|
|
27
|
+
* `Task` can spawn another (`grep -l '^tools:.*Task' agents/*.md`), so squad
|
|
28
|
+
* members cannot fan out further.
|
|
8
29
|
*/
|
|
9
30
|
|
|
10
31
|
'use strict';
|
|
@@ -14,13 +35,21 @@ const { output, error } = require('./core.cjs');
|
|
|
14
35
|
/**
|
|
15
36
|
* Coordinator + worker/utility agents that are NOT squad members.
|
|
16
37
|
* - coordinator: the top of the hierarchy (Tier 0).
|
|
17
|
-
* - workers:
|
|
38
|
+
* - workers: narrow-job agents (Tier 2) + standalone utility agents
|
|
18
39
|
* invoked directly by commands, not delegated through a squad.
|
|
40
|
+
*
|
|
41
|
+
* "Tier 2" is a hierarchy position, not a model tier. Post-COST-RESET (2026-07)
|
|
42
|
+
* every agent in MODEL_PROFILES resolves to `reasoning` under both quality and
|
|
43
|
+
* balanced (the default); `budget` is the only profile that down-tiers, and it is
|
|
44
|
+
* the profile — never this list — that decides what an agent drops to. Note it
|
|
45
|
+
* down-tiers per agent, not per group: `budget` sends some of the workers below
|
|
46
|
+
* to `fast` and others to `mid`. MODEL_PROFILES in core.cjs is the table; read it
|
|
47
|
+
* rather than assuming one tier covers a row of a doc.
|
|
19
48
|
*/
|
|
20
49
|
const COORDINATOR = 'pan-conductor';
|
|
21
50
|
const WORKERS = Object.freeze([
|
|
22
|
-
'pan-document_code', //
|
|
23
|
-
'pan-distiller', //
|
|
51
|
+
'pan-document_code', // codebase mapper
|
|
52
|
+
'pan-distiller', // code-bloat optimizer
|
|
24
53
|
'pan-optimizer', // optimization loop
|
|
25
54
|
'pan-experiment-runner', // self-improvement loop
|
|
26
55
|
'pan-knowledge', // retrieval/Q&A
|
|
@@ -29,9 +58,12 @@ const WORKERS = Object.freeze([
|
|
|
29
58
|
]);
|
|
30
59
|
|
|
31
60
|
/**
|
|
32
|
-
* The
|
|
33
|
-
* (resolve-model maps
|
|
34
|
-
*
|
|
61
|
+
* The squads, keyed by lifecycle role — `squad list` enumerates them at runtime.
|
|
62
|
+
* `tier` is a PAN model tier label (resolve-model maps a tier to a provider
|
|
63
|
+
* model); `access` is the intended
|
|
64
|
+
* least-privilege contract the conductor is instructed to honour when delegating
|
|
65
|
+
* to the squad. Neither is applied by this module — see the advisory note in the
|
|
66
|
+
* file header before describing either one as a restriction.
|
|
35
67
|
*/
|
|
36
68
|
const SQUADS = Object.freeze({
|
|
37
69
|
architecture: Object.freeze({
|
|
@@ -40,7 +72,7 @@ const SQUADS = Object.freeze({
|
|
|
40
72
|
access: 'read-only',
|
|
41
73
|
summary: 'Designs the system before code — contract-first.',
|
|
42
74
|
agents: Object.freeze([
|
|
43
|
-
'pan-roadmapper', 'pan-planner', 'pan-plan-checker',
|
|
75
|
+
'pan-roadmapper', 'pan-planner', 'pan-designer', 'pan-plan-checker', 'pan-design-checker',
|
|
44
76
|
'pan-project-researcher', 'pan-phase-researcher', 'pan-research-synthesizer',
|
|
45
77
|
]),
|
|
46
78
|
}),
|