pan-wizard 3.22.0 → 3.25.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 +4 -4
- 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 +34 -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 +11 -6
- 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/lib/worktree.cjs +98 -0
- package/pan-wizard-core/bin/pan-tools.cjs +45 -20
- 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/README.md +12 -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
|
@@ -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
|
}),
|
|
@@ -195,11 +195,16 @@ function cmdStateUpdate(cwd, field, value) {
|
|
|
195
195
|
writeStateMd(statePath, content, cwd);
|
|
196
196
|
output({ updated: true });
|
|
197
197
|
} else {
|
|
198
|
-
|
|
198
|
+
// error key ⇒ exit 1: the caller asked for a field update that did not
|
|
199
|
+
// happen, so state.md does not say what the caller now believes it says.
|
|
200
|
+
output({ updated: false, reason: `Field "${field}" not found in state.md`, error: 'field_not_found' });
|
|
199
201
|
}
|
|
200
202
|
} catch {
|
|
201
203
|
// state.md does not exist or is unreadable -- report gracefully
|
|
202
|
-
|
|
204
|
+
// Same condition `state json` reports as {error:'state.md not found'} at exit 1.
|
|
205
|
+
// Reporting it as a bare `updated: false` was the identical defect wearing a
|
|
206
|
+
// different key — one file, one missing file, two exit codes.
|
|
207
|
+
output({ updated: false, reason: 'state.md not found', error: 'state_not_found' });
|
|
203
208
|
}
|
|
204
209
|
}
|
|
205
210
|
|
|
@@ -256,6 +261,9 @@ function cmdStateAdvancePlan(cwd, raw) {
|
|
|
256
261
|
content = stateReplaceField(content, 'Status', 'Phase complete — ready for verification') || content;
|
|
257
262
|
content = stateReplaceField(content, 'Last Activity', today) || content;
|
|
258
263
|
writeStateMd(statePath, content, cwd);
|
|
264
|
+
// No error key, exit 0: `last_plan` is the NORMAL end-of-phase signal — the
|
|
265
|
+
// phase advanced to "ready for verification" and state.md was written. Gating on
|
|
266
|
+
// it would make every completed phase look like a failure.
|
|
259
267
|
output({ advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' }, raw, 'false');
|
|
260
268
|
} else {
|
|
261
269
|
const newPlan = currentPlan + 1;
|
|
@@ -303,7 +311,7 @@ function cmdStateRecordMetric(cwd, options, raw) {
|
|
|
303
311
|
writeStateMd(statePath, content, cwd);
|
|
304
312
|
output({ recorded: true, phase, plan, duration }, raw, 'true');
|
|
305
313
|
} else {
|
|
306
|
-
output({ recorded: false, reason: 'Performance Metrics section not found in state.md' }, raw, 'false');
|
|
314
|
+
output({ recorded: false, reason: 'Performance Metrics section not found in state.md', error: 'metrics_section_missing' }, raw, 'false');
|
|
307
315
|
}
|
|
308
316
|
}
|
|
309
317
|
|
|
@@ -345,7 +353,7 @@ function cmdStateUpdateProgress(cwd, raw) {
|
|
|
345
353
|
writeStateMd(statePath, content, cwd);
|
|
346
354
|
output({ updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr }, raw, progressStr);
|
|
347
355
|
} else {
|
|
348
|
-
output({ updated: false, reason: 'Progress field not found in state.md' }, raw, 'false');
|
|
356
|
+
output({ updated: false, reason: 'Progress field not found in state.md', error: 'progress_field_missing' }, raw, 'false');
|
|
349
357
|
}
|
|
350
358
|
}
|
|
351
359
|
|
|
@@ -369,7 +377,7 @@ function cmdStateAddDecision(cwd, options, raw) {
|
|
|
369
377
|
summaryText = readTextArgOrFile(cwd, summary, summary_file, 'summary');
|
|
370
378
|
rationaleText = readTextArgOrFile(cwd, rationale || '', rationale_file, 'rationale');
|
|
371
379
|
} catch (err) {
|
|
372
|
-
output({ added: false, reason: err.message }, raw, 'false');
|
|
380
|
+
output({ added: false, reason: err.message, error: err.message || 'decision_read_failed' }, raw, 'false');
|
|
373
381
|
return;
|
|
374
382
|
}
|
|
375
383
|
|
|
@@ -389,7 +397,7 @@ function cmdStateAddDecision(cwd, options, raw) {
|
|
|
389
397
|
writeStateMd(statePath, content, cwd);
|
|
390
398
|
output({ added: true, decision: entry }, raw, 'true');
|
|
391
399
|
} else {
|
|
392
|
-
output({ added: false, reason: 'Decisions section not found in state.md' }, raw, 'false');
|
|
400
|
+
output({ added: false, reason: 'Decisions section not found in state.md', error: 'decisions_section_missing' }, raw, 'false');
|
|
393
401
|
}
|
|
394
402
|
}
|
|
395
403
|
|
|
@@ -410,7 +418,7 @@ function cmdStateAddBlocker(cwd, text, raw) {
|
|
|
410
418
|
try {
|
|
411
419
|
blockerText = readTextArgOrFile(cwd, blockerOptions.text, blockerOptions.text_file, 'blocker');
|
|
412
420
|
} catch (err) {
|
|
413
|
-
output({ added: false, reason: err.message }, raw, 'false');
|
|
421
|
+
output({ added: false, reason: err.message, error: err.message || 'blocker_read_failed' }, raw, 'false');
|
|
414
422
|
return;
|
|
415
423
|
}
|
|
416
424
|
|
|
@@ -428,7 +436,7 @@ function cmdStateAddBlocker(cwd, text, raw) {
|
|
|
428
436
|
writeStateMd(statePath, content, cwd);
|
|
429
437
|
output({ added: true, blocker: blockerText }, raw, 'true');
|
|
430
438
|
} else {
|
|
431
|
-
output({ added: false, reason: 'Blockers section not found in state.md' }, raw, 'false');
|
|
439
|
+
output({ added: false, reason: 'Blockers section not found in state.md', error: 'blockers_section_missing' }, raw, 'false');
|
|
432
440
|
}
|
|
433
441
|
}
|
|
434
442
|
|
|
@@ -456,6 +464,17 @@ function cmdStateResolveBlocker(cwd, text, raw) {
|
|
|
456
464
|
return !line.toLowerCase().includes(text.toLowerCase());
|
|
457
465
|
});
|
|
458
466
|
|
|
467
|
+
// M27: if nothing was removed, no blocker matched the given text. Report
|
|
468
|
+
// resolved:false rather than falsely claiming a resolution (and skip the
|
|
469
|
+
// write entirely — the file is unchanged).
|
|
470
|
+
if (filtered.length === lines.length) {
|
|
471
|
+
// error key ⇒ exit 1. M27 stopped this from falsely claiming a resolution;
|
|
472
|
+
// the exit code finishes the job. The blocker is STILL OPEN and the caller
|
|
473
|
+
// that asked to clear it would otherwise carry on believing it is gone.
|
|
474
|
+
output({ resolved: false, reason: 'no matching blocker', blocker: text, error: 'blocker_not_matched' }, raw, 'false');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
459
478
|
let newBody = filtered.join('\n');
|
|
460
479
|
// If section is now empty, add placeholder
|
|
461
480
|
if (!newBody.trim() || !newBody.includes('- ')) {
|
|
@@ -466,7 +485,7 @@ function cmdStateResolveBlocker(cwd, text, raw) {
|
|
|
466
485
|
writeStateMd(statePath, content, cwd);
|
|
467
486
|
output({ resolved: true, blocker: text }, raw, 'true');
|
|
468
487
|
} else {
|
|
469
|
-
output({ resolved: false, reason: 'Blockers section not found in state.md' }, raw, 'false');
|
|
488
|
+
output({ resolved: false, reason: 'Blockers section not found in state.md', error: 'blockers_section_missing' }, raw, 'false');
|
|
470
489
|
}
|
|
471
490
|
}
|
|
472
491
|
|
|
@@ -510,7 +529,7 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
510
529
|
try { require('./memory-optimize.cjs').maybeAutoOptimizeMemory(cwd); } catch { /* best-effort */ }
|
|
511
530
|
output({ recorded: true, updated }, raw, 'true');
|
|
512
531
|
} else {
|
|
513
|
-
output({ recorded: false, reason: 'No session fields found in state.md' }, raw, 'false');
|
|
532
|
+
output({ recorded: false, reason: 'No session fields found in state.md', error: 'session_fields_missing' }, raw, 'false');
|
|
514
533
|
}
|
|
515
534
|
}
|
|
516
535
|
|
|
@@ -1012,7 +1031,9 @@ function cmdDashboard(cwd, raw) {
|
|
|
1012
1031
|
lines.push(`Blockers: ${blockerCount}`);
|
|
1013
1032
|
if (lastActivity) lines.push(`Last Activity: ${lastActivity}${lastActivityDesc ? ' — ' + lastActivityDesc : ''}`);
|
|
1014
1033
|
if (nextPhase) lines.push(`Next Phase: ${nextPhase.number} — ${nextPhase.name}`);
|
|
1015
|
-
output(
|
|
1034
|
+
// raw=true so output() emits the human summary; passing false discarded it
|
|
1035
|
+
// and printed JSON, making the whole lines[] block dead (M28, ADR audit 2026-08).
|
|
1036
|
+
output(result, true, lines.join('\n'));
|
|
1016
1037
|
return;
|
|
1017
1038
|
}
|
|
1018
1039
|
|
|
@@ -9,7 +9,7 @@ const {
|
|
|
9
9
|
SIMPLE_TASK_THRESHOLD, SIMPLE_FILE_THRESHOLD, COMPLEX_TASK_THRESHOLD, COMPLEX_FILE_THRESHOLD,
|
|
10
10
|
} = require('./constants.cjs');
|
|
11
11
|
const { planningPath, phasesPath } = require('./utils.cjs');
|
|
12
|
-
const { normalizePhaseName, findPhaseInternal, generateSlugInternal, toPosix, output, error } = require('./core.cjs');
|
|
12
|
+
const { normalizePhaseName, findPhaseInternal, generateSlugInternal, toPosix, output, EXIT_OK, error } = require('./core.cjs');
|
|
13
13
|
const { reconstructFrontmatter } = require('./frontmatter.cjs');
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -64,8 +64,13 @@ function cmdTemplateSelect(cwd, planPath, raw) {
|
|
|
64
64
|
const result = { template, type, taskCount, fileCount, hasDecisions };
|
|
65
65
|
output(result, raw, template);
|
|
66
66
|
} catch (err) {
|
|
67
|
-
// Fallback to standard template on any read/parse error
|
|
68
|
-
|
|
67
|
+
// Fallback to standard template on any read/parse error.
|
|
68
|
+
// EXIT_OK: this is a RESULT, not a failure — the command answered the question
|
|
69
|
+
// it was asked ("which summary template?") with the documented default, and
|
|
70
|
+
// `error` here is only the reason detection fell back. Callers use it as
|
|
71
|
+
// `TEMPLATE=$(pan-tools template select … --raw)`; exiting non-zero would
|
|
72
|
+
// report a failure for a command that produced a usable answer.
|
|
73
|
+
output({ template: 'templates/summary-standard.md', type: 'standard', error: err.message }, raw, 'templates/summary-standard.md', EXIT_OK);
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
76
|
|
|
@@ -90,8 +90,20 @@ function validateRuntimeInstall(cwd, configDir, runtime) {
|
|
|
90
90
|
if (hooks && typeof hooks === 'object') {
|
|
91
91
|
for (const hookArr of Object.values(hooks)) {
|
|
92
92
|
if (!Array.isArray(hookArr)) continue;
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
// M29: PAN installs hooks in the nested Claude shape — each event maps to
|
|
94
|
+
// an array of GROUPS, and each group holds the real commands under
|
|
95
|
+
// group.hooks[]: { matcher, hooks: [{ type, command }] }. The old code
|
|
96
|
+
// only read group.command (undefined in that shape), so it validated
|
|
97
|
+
// nothing for PAN's own hooks. Collect both the flat group.command (other
|
|
98
|
+
// runtimes) AND every command nested in group.hooks[].
|
|
99
|
+
for (const group of hookArr) {
|
|
100
|
+
if (!group || typeof group !== 'object') continue;
|
|
101
|
+
if (group.command) hookCommands.push(group.command);
|
|
102
|
+
if (Array.isArray(group.hooks)) {
|
|
103
|
+
for (const h of group.hooks) {
|
|
104
|
+
if (h && h.command) hookCommands.push(h.command);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
109
|
}
|
|
@@ -135,7 +135,7 @@ function calculateDriftScore(violations, filesChecked, rulesCount) {
|
|
|
135
135
|
* @param {string} [sinceRef] - Git ref to diff against (default: HEAD)
|
|
136
136
|
* @returns {string[]} Array of relative file paths
|
|
137
137
|
*/
|
|
138
|
-
function getChangedFiles(cwd, sinceRef) {
|
|
138
|
+
function getChangedFiles(cwd, sinceRef, opts = {}) {
|
|
139
139
|
const ref = sinceRef || 'HEAD';
|
|
140
140
|
// Try staged + unstaged first, then diff against ref
|
|
141
141
|
const result = execGit(cwd, ['diff', '--name-only', ref]);
|
|
@@ -150,6 +150,20 @@ function getChangedFiles(cwd, sinceRef) {
|
|
|
150
150
|
if (line.trim()) allFiles.add(line.trim());
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
|
+
// Untracked files are OPT-IN because the two consumers want different things.
|
|
154
|
+
// Drift detection asks "what changed against the baseline", where an untracked
|
|
155
|
+
// scratch file is noise. The stub gate asks "is any code in this handoff a stub",
|
|
156
|
+
// and a brand-new file is precisely the normal shape of PAN execution — a plan
|
|
157
|
+
// creates files, a later step commits them — so during that window `git diff` lists
|
|
158
|
+
// nothing and a fully stubbed new module sailed through the gate.
|
|
159
|
+
if (opts.includeUntracked) {
|
|
160
|
+
const untracked = execGit(cwd, ['ls-files', '--others', '--exclude-standard']);
|
|
161
|
+
if (untracked.exitCode === 0) {
|
|
162
|
+
for (const line of untracked.stdout.split(/\r?\n/)) {
|
|
163
|
+
if (line.trim()) allFiles.add(line.trim());
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
153
167
|
// Filter out binary extensions and limit
|
|
154
168
|
const filtered = [];
|
|
155
169
|
for (const f of allFiles) {
|