pan-wizard 3.29.0 → 3.31.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 +40 -40
- package/bin/install-lib.cjs +186 -83
- package/bin/install.js +213 -195
- package/commands/pan/army.md +4 -4
- package/commands/pan/audit-deployment.md +2 -2
- package/commands/pan/cost.md +5 -5
- package/commands/pan/exec-phase.md +2 -0
- package/commands/pan/focus-auto.md +5 -5
- package/commands/pan/verify-phase.md +8 -9
- package/hooks/dist/pan-check-update.js +53 -2
- package/hooks/dist/pan-context-monitor.js +41 -0
- package/hooks/dist/pan-cost-logger.js +71 -2
- package/hooks/dist/pan-state-reinject.js +158 -0
- package/hooks/dist/pan-stop-guard.js +127 -2
- package/hooks/dist/pan-trace-logger.js +42 -1
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/agents-md.cjs +3 -2
- package/pan-wizard-core/bin/lib/commands.cjs +20 -4
- package/pan-wizard-core/bin/lib/config.cjs +2 -0
- package/pan-wizard-core/bin/lib/constants.cjs +3 -1
- package/pan-wizard-core/bin/lib/context-budget.cjs +27 -2
- package/pan-wizard-core/bin/lib/core.cjs +249 -27
- package/pan-wizard-core/bin/lib/cost-rebuild.cjs +11 -0
- package/pan-wizard-core/bin/lib/cost.cjs +234 -88
- package/pan-wizard-core/bin/lib/frontmatter.cjs +5 -2
- package/pan-wizard-core/bin/lib/memory-optimize.cjs +12 -6
- package/pan-wizard-core/bin/lib/memory-rebuild.cjs +12 -5
- package/pan-wizard-core/bin/lib/phase-report.cjs +4 -1
- package/pan-wizard-core/bin/lib/roadmap.cjs +87 -18
- package/pan-wizard-core/bin/lib/state.cjs +86 -22
- package/pan-wizard-core/bin/lib/suggest.cjs +21 -9
- package/pan-wizard-core/bin/lib/template.cjs +6 -3
- package/pan-wizard-core/bin/lib/verify-deploy.cjs +20 -3
- package/pan-wizard-core/bin/pan-tools.cjs +10 -1
- package/pan-wizard-core/mcp/tool-registry.cjs +14 -4
- package/pan-wizard-core/references/model-profiles.md +4 -4
- package/pan-wizard-core/references/planning-config.md +19 -23
- package/pan-wizard-core/workflows/exec-phase.md +8 -0
- package/pan-wizard-core/workflows/help.md +2 -2
- package/pan-wizard-core/workflows/phase-tests.md +1 -1
- package/pan-wizard-core/workflows/progress.md +1 -1
- package/pan-wizard-core/workflows/settings.md +2 -4
- package/pan-wizard-core/workflows/todo-add.md +1 -1
- package/pan-wizard-core/workflows/todo-check.md +7 -6
- package/scripts/build-agent-plugin.js +4 -0
- package/scripts/build-hooks.js +2 -1
- package/scripts/build-plugin.js +10 -0
- package/scripts/npm-dist-tag.js +47 -0
- package/scripts/plugin-path.js +6 -1
- package/scripts/test-surface.cjs +11 -9
|
@@ -320,6 +320,78 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
320
320
|
* @param {boolean} raw - If true, output raw value instead of JSON
|
|
321
321
|
* @returns {void}
|
|
322
322
|
*/
|
|
323
|
+
// ─── Progress table and Plans line, located by name ─────────────────────────
|
|
324
|
+
//
|
|
325
|
+
// `roadmap update-plan-progress` used to rewrite the phase's table row by column
|
|
326
|
+
// POSITION — cells 2 to 4, whatever the header said. On PAN's own milestone
|
|
327
|
+
// variant (`| Phase | Milestone | Plans Complete | Status | Completed |`) that put
|
|
328
|
+
// the plan count in the Milestone column; on a roadmap with other columns it
|
|
329
|
+
// destroyed the phase's goal and requirement ids. And the `**Plans:**` update was a
|
|
330
|
+
// lazy match from the phase heading onward, so a phase without its own Plans line
|
|
331
|
+
// had the NEXT phase's rewritten. Both are now located by name, and a table without
|
|
332
|
+
// Plans and Status columns is left alone and reported.
|
|
333
|
+
|
|
334
|
+
const splitRow = (line) => {
|
|
335
|
+
const inner = line.trim().replace(/^\|/, '').replace(/\|$/, '');
|
|
336
|
+
return inner.split(/(?<!\\)\|/).map((c) => c.trim());
|
|
337
|
+
};
|
|
338
|
+
const isTableLine = (line) => /^\s*\|.*\|\s*$/.test(line);
|
|
339
|
+
const isSeparator = (line) => /^\s*\|(?:\s*:?-{3,}:?\s*\|)+\s*$/.test(line);
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Set the Plans / Status / Completed cells of the phase's row in any table whose
|
|
343
|
+
* header names Plans and Status columns. Pure.
|
|
344
|
+
* @returns {{content: string, updated: boolean, reason: string|null}}
|
|
345
|
+
*/
|
|
346
|
+
function updateProgressTableRow(content, phaseNum, values) {
|
|
347
|
+
const eol = content.includes('\r\n') ? '\r\n' : '\n';
|
|
348
|
+
const lines = content.split(/\r?\n/);
|
|
349
|
+
const phaseRe = new RegExp(`^(?:phase\\s+)?${escapeRegex(String(phaseNum))}\\.?(?=\\s|:|$)`, 'i');
|
|
350
|
+
let reason = null;
|
|
351
|
+
for (let i = 0; i + 1 < lines.length; i++) {
|
|
352
|
+
if (!isTableLine(lines[i]) || !isSeparator(lines[i + 1])) continue;
|
|
353
|
+
const header = splitRow(lines[i]);
|
|
354
|
+
const col = (re) => header.findIndex((h) => re.test(h));
|
|
355
|
+
const plansIdx = col(/\bplans?\b/i);
|
|
356
|
+
const statusIdx = col(/^status$/i);
|
|
357
|
+
const doneIdx = col(/^completed?(?:\s+on|\s+date)?$/i);
|
|
358
|
+
for (let r = i + 2; r < lines.length && isTableLine(lines[r]); r++) {
|
|
359
|
+
const cells = splitRow(lines[r]);
|
|
360
|
+
if (!phaseRe.test(cells[0] || '')) continue;
|
|
361
|
+
if (plansIdx < 0 || statusIdx < 0) {
|
|
362
|
+
reason = `the table holding phase ${phaseNum} has no Plans and Status columns (${header.join(' | ')}) — left unchanged`;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
while (cells.length < header.length) cells.push('');
|
|
366
|
+
cells[plansIdx] = values.plans;
|
|
367
|
+
cells[statusIdx] = values.status;
|
|
368
|
+
if (doneIdx >= 0) cells[doneIdx] = values.completed || '';
|
|
369
|
+
const indent = lines[r].match(/^\s*/)[0];
|
|
370
|
+
lines[r] = `${indent}| ${cells.join(' | ')} |`;
|
|
371
|
+
return { content: lines.join(eol), updated: true, reason: null };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return { content, updated: false, reason: reason || `no progress-table row for phase ${phaseNum}` };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Rewrite `**Plans:**` inside the phase's own section only. Pure. */
|
|
378
|
+
function updatePhasePlansLine(content, phaseNum, text) {
|
|
379
|
+
const lines = content.split(/(?<=\n)/);
|
|
380
|
+
const headRe = new RegExp(`^(#{2,4})\\s*Phase\\s+${escapeRegex(String(phaseNum))}(?=[\\s:.]|$)`, 'i');
|
|
381
|
+
const start = lines.findIndex((l) => headRe.test(l));
|
|
382
|
+
if (start < 0) return { content, updated: false };
|
|
383
|
+
const level = lines[start].match(headRe)[1].length;
|
|
384
|
+
const nextHead = new RegExp(`^#{1,${level}}\\s`);
|
|
385
|
+
for (let i = start + 1; i < lines.length && !nextHead.test(lines[i]); i++) {
|
|
386
|
+
const m = lines[i].match(/^(.*?(?:\*\*Plans:\*\*|\*\*Plans\*\*:)\s*)[^\r\n]*(\r?\n?)$/);
|
|
387
|
+
if (m) {
|
|
388
|
+
lines[i] = `${m[1]}${text}${m[2]}`;
|
|
389
|
+
return { content: lines.join(''), updated: true };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return { content, updated: false };
|
|
393
|
+
}
|
|
394
|
+
|
|
323
395
|
function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
324
396
|
if (!phaseNum) {
|
|
325
397
|
error('phase number required for roadmap update-plan-progress');
|
|
@@ -356,26 +428,19 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
356
428
|
}
|
|
357
429
|
const phaseEscaped = escapeRegex(phaseNum);
|
|
358
430
|
|
|
359
|
-
// Progress table row:
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
roadmapContent =
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
);
|
|
369
|
-
|
|
370
|
-
// Update plan count in phase detail section
|
|
371
|
-
const planCountPattern = new RegExp(
|
|
372
|
-
`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?(?:\\*\\*Plans:\\*\\*|\\*\\*Plans\\*\\*:)\\s*)[^\\n]+`,
|
|
373
|
-
'i'
|
|
374
|
-
);
|
|
431
|
+
// Progress table row: the Plans, Status and Completed cells, found by header.
|
|
432
|
+
const table = updateProgressTableRow(roadmapContent, phaseNum, {
|
|
433
|
+
plans: `${summaryCount}/${planCount}`,
|
|
434
|
+
status,
|
|
435
|
+
completed: isComplete ? today : '',
|
|
436
|
+
});
|
|
437
|
+
roadmapContent = table.content;
|
|
438
|
+
|
|
439
|
+
// Update plan count in the phase's own detail section
|
|
375
440
|
const planCountText = isComplete
|
|
376
441
|
? `${summaryCount}/${planCount} plans complete`
|
|
377
442
|
: `${summaryCount}/${planCount} plans executed`;
|
|
378
|
-
roadmapContent = roadmapContent
|
|
443
|
+
roadmapContent = updatePhasePlansLine(roadmapContent, phaseNum, planCountText).content;
|
|
379
444
|
|
|
380
445
|
// If complete: check checkbox
|
|
381
446
|
if (isComplete) {
|
|
@@ -402,6 +467,8 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
402
467
|
|
|
403
468
|
try {
|
|
404
469
|
fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
|
|
470
|
+
// Read back: report success only for the file this command produced.
|
|
471
|
+
if (fs.readFileSync(roadmapPath, 'utf-8') !== roadmapContent) throw new Error('roadmap.md did not read back as written');
|
|
405
472
|
} catch (err) {
|
|
406
473
|
// error key => exit 1: the write failed, so progress was computed and then lost.
|
|
407
474
|
output({ updated: false, reason: 'Failed to write roadmap.md: ' + err.message, error: err.message || 'roadmap_write_failed' }, raw, 'write error');
|
|
@@ -415,7 +482,9 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
415
482
|
summary_count: summaryCount,
|
|
416
483
|
status,
|
|
417
484
|
complete: isComplete,
|
|
418
|
-
|
|
485
|
+
table_updated: table.updated,
|
|
486
|
+
...(table.updated ? {} : { table_reason: table.reason }),
|
|
487
|
+
}, raw, `${summaryCount}/${planCount} ${status}${table.updated ? '' : ' (progress table not updated)'}`);
|
|
419
488
|
}
|
|
420
489
|
|
|
421
490
|
/**
|
|
@@ -799,20 +799,32 @@ function calculateProgressBar(completed, total) {
|
|
|
799
799
|
* Orchestrates extractFieldsFromState, scanPhaseProgress,
|
|
800
800
|
* normalizePhaseStatus, and calculateProgressBar.
|
|
801
801
|
*/
|
|
802
|
-
function buildStateFrontmatter(bodyContent, cwd) {
|
|
802
|
+
function buildStateFrontmatter(bodyContent, cwd, existing = {}) {
|
|
803
803
|
const fields = extractFieldsFromState(bodyContent);
|
|
804
|
+
const prior = existing && typeof existing === 'object' ? existing : {};
|
|
804
805
|
|
|
806
|
+
// The milestone comes from the roadmap resolver — unless the resolver only
|
|
807
|
+
// guessed (no milestone heading at all) and state.md already records one. A
|
|
808
|
+
// guess overwriting a recorded value is how a letter-series milestone ("R-2")
|
|
809
|
+
// was rewritten to the resolver's `v1.0` / `milestone` on every state write.
|
|
805
810
|
let milestone = null;
|
|
806
811
|
let milestoneName = null;
|
|
807
812
|
if (cwd) {
|
|
808
813
|
try {
|
|
809
814
|
const info = getMilestoneInfo(cwd);
|
|
810
|
-
|
|
811
|
-
|
|
815
|
+
const guessed = info.basis === 'default' || info.basis === 'no-milestone-heading';
|
|
816
|
+
if (!(guessed && prior.milestone)) {
|
|
817
|
+
milestone = info.version;
|
|
818
|
+
milestoneName = info.name;
|
|
819
|
+
}
|
|
812
820
|
} catch {
|
|
813
821
|
// No milestone configured or milestone file unreadable -- skip milestone fields
|
|
814
822
|
}
|
|
815
823
|
}
|
|
824
|
+
if (milestone == null && prior.milestone) {
|
|
825
|
+
milestone = prior.milestone;
|
|
826
|
+
milestoneName = prior.milestone_name != null ? prior.milestone_name : null;
|
|
827
|
+
}
|
|
816
828
|
|
|
817
829
|
let totalPhases = fields.totalPhasesRaw ? parseInt(fields.totalPhasesRaw, 10) : null;
|
|
818
830
|
let completedPhases = null;
|
|
@@ -837,20 +849,27 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
837
849
|
if (pctMatch) progressPercent = parseInt(pctMatch[1], 10);
|
|
838
850
|
}
|
|
839
851
|
|
|
840
|
-
const normalizedStatus = normalizePhaseStatus(fields.status, fields.pausedAt);
|
|
841
|
-
|
|
842
852
|
const frontmatter = { pan_state_version: '1.0' };
|
|
843
853
|
|
|
844
854
|
if (milestone) frontmatter.milestone = milestone;
|
|
845
855
|
if (milestoneName) frontmatter.milestone_name = milestoneName;
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
856
|
+
// A field the body restates wins; one it does not keeps its recorded value.
|
|
857
|
+
// Rebuilding from the body alone dropped current_phase_name, current_plan and
|
|
858
|
+
// the rest whenever a project's state.md did not repeat them in prose.
|
|
859
|
+
const keep = (key, fromBody) => {
|
|
860
|
+
if (fromBody != null && fromBody !== '') frontmatter[key] = fromBody;
|
|
861
|
+
else if (prior[key] != null) frontmatter[key] = prior[key];
|
|
862
|
+
};
|
|
863
|
+
keep('current_phase', fields.currentPhase);
|
|
864
|
+
keep('current_phase_name', fields.currentPhaseName);
|
|
865
|
+
keep('current_plan', fields.currentPlan);
|
|
866
|
+
frontmatter.status = (fields.status || fields.pausedAt)
|
|
867
|
+
? normalizePhaseStatus(fields.status, fields.pausedAt)
|
|
868
|
+
: (prior.status != null ? prior.status : normalizePhaseStatus(null, null));
|
|
869
|
+
keep('stopped_at', fields.stoppedAt);
|
|
870
|
+
keep('paused_at', fields.pausedAt);
|
|
852
871
|
frontmatter.last_updated = new Date().toISOString();
|
|
853
|
-
|
|
872
|
+
keep('last_activity', fields.lastActivity);
|
|
854
873
|
|
|
855
874
|
const progress = {};
|
|
856
875
|
if (totalPhases !== null) progress.total_phases = totalPhases;
|
|
@@ -859,19 +878,48 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
859
878
|
if (completedPlans !== null) progress.completed_plans = completedPlans;
|
|
860
879
|
if (progressPercent !== null) progress.percent = progressPercent;
|
|
861
880
|
if (Object.keys(progress).length > 0) frontmatter.progress = progress;
|
|
862
|
-
|
|
863
|
-
|
|
881
|
+
else if (prior.progress != null) frontmatter.progress = prior.progress;
|
|
882
|
+
|
|
883
|
+
// Keys PAN does not manage (a project's own notes) survive every write, in the
|
|
884
|
+
// order the file already had them; PAN's keys fill in around them.
|
|
885
|
+
const merged = {};
|
|
886
|
+
for (const key of Object.keys(prior)) merged[key] = key in frontmatter ? frontmatter[key] : prior[key];
|
|
887
|
+
for (const key of Object.keys(frontmatter)) if (!(key in merged)) merged[key] = frontmatter[key];
|
|
888
|
+
return merged;
|
|
864
889
|
}
|
|
865
890
|
|
|
891
|
+
// One front-matter block at the top of state.md: BOM-tolerant, LF or CRLF. The
|
|
892
|
+
// previous `^---\n…\n---\n` matched LF only, so on a CRLF checkout (git
|
|
893
|
+
// core.autocrlf=true) the old block was never stripped and every state write
|
|
894
|
+
// stacked a new, re-derived block on top of it.
|
|
895
|
+
const STATE_FRONTMATTER_RE = /^\uFEFF?---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
896
|
+
|
|
866
897
|
function stripFrontmatter(content) {
|
|
867
|
-
return content.replace(
|
|
898
|
+
return String(content).replace(STATE_FRONTMATTER_RE, '').replace(/^\uFEFF/, '').replace(/^(?:\r?\n)+/, '');
|
|
868
899
|
}
|
|
869
900
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
901
|
+
/**
|
|
902
|
+
* Re-derive state.md's front matter from its body, merged with the front matter
|
|
903
|
+
* already there. Output uses `eol` (default: whatever the content already uses).
|
|
904
|
+
* Throws (code STATE_STACKED_FRONTMATTER) when the content starts with more than
|
|
905
|
+
* one front-matter block — earlier releases stacked them on CRLF files, and
|
|
906
|
+
* choosing between them silently is exactly the guess this function must not make.
|
|
907
|
+
*/
|
|
908
|
+
function syncStateFrontmatter(content, cwd, eol) {
|
|
909
|
+
const text = String(content);
|
|
910
|
+
const lineEnd = eol || (/\r\n/.test(text) ? '\r\n' : '\n');
|
|
911
|
+
const m = text.match(STATE_FRONTMATTER_RE);
|
|
912
|
+
const existing = m ? extractFrontmatter(m[0]) : {};
|
|
913
|
+
const body = stripFrontmatter(text).replace(/\r\n/g, '\n');
|
|
914
|
+
if (STATE_FRONTMATTER_RE.test(body)) {
|
|
915
|
+
const err = new Error('state.md starts with more than one front-matter block (an earlier PAN release stacked them on CRLF files). Keep the block with the right values, delete the other, and run the command again — PAN will not pick one for you.');
|
|
916
|
+
err.code = 'STATE_STACKED_FRONTMATTER';
|
|
917
|
+
throw err;
|
|
918
|
+
}
|
|
919
|
+
const frontmatter = buildStateFrontmatter(body, cwd, existing);
|
|
873
920
|
const yamlStr = reconstructFrontmatter(frontmatter);
|
|
874
|
-
|
|
921
|
+
const out = `---\n${yamlStr}\n---\n\n${body}`;
|
|
922
|
+
return lineEnd === '\r\n' ? out.replace(/\n/g, '\r\n') : out;
|
|
875
923
|
}
|
|
876
924
|
|
|
877
925
|
/**
|
|
@@ -882,17 +930,33 @@ function syncStateFrontmatter(content, cwd) {
|
|
|
882
930
|
* state.md.lock and lands atomically (temp + rename), so concurrent agents
|
|
883
931
|
* cannot tear the file or interleave read-modify-write cycles. Lock
|
|
884
932
|
* acquisition is best-effort — on timeout the write proceeds unlocked,
|
|
885
|
-
*
|
|
933
|
+
* matching the file-lock contract.
|
|
934
|
+
*
|
|
935
|
+
* The file keeps its own line ending, and the write is verified: exactly one
|
|
936
|
+
* front-matter block goes in, and what is read back under the lock must be what
|
|
937
|
+
* was written. A write that cannot meet either stops with an error and leaves
|
|
938
|
+
* the file as it was — it never reports success over a file it did not produce.
|
|
886
939
|
*/
|
|
887
940
|
function writeStateMd(statePath, content, cwd) {
|
|
888
|
-
|
|
941
|
+
let diskEol = null;
|
|
942
|
+
try { diskEol = /\r\n/.test(fs.readFileSync(statePath, 'utf-8')) ? '\r\n' : '\n'; } catch { /* new file */ }
|
|
943
|
+
let synced;
|
|
944
|
+
try {
|
|
945
|
+
synced = syncStateFrontmatter(content, cwd, diskEol);
|
|
946
|
+
} catch (err) {
|
|
947
|
+
if (err && err.code === 'STATE_STACKED_FRONTMATTER') error(err.message);
|
|
948
|
+
throw err;
|
|
949
|
+
}
|
|
950
|
+
let mismatch = false;
|
|
889
951
|
try {
|
|
890
952
|
withFileLock(statePath, () => {
|
|
891
953
|
writeFileAtomic(statePath, synced);
|
|
954
|
+
mismatch = fs.readFileSync(statePath, 'utf-8') !== synced;
|
|
892
955
|
});
|
|
893
956
|
} catch (err) {
|
|
894
957
|
throw new Error('Failed to write state.md: ' + err.message);
|
|
895
958
|
}
|
|
959
|
+
if (mismatch) error('state.md did not read back as written — another writer changed it during the write. Re-run the command.');
|
|
896
960
|
}
|
|
897
961
|
|
|
898
962
|
/**
|
|
@@ -911,7 +975,7 @@ function cmdStateJson(cwd, raw) {
|
|
|
911
975
|
const frontmatter = extractFrontmatter(content);
|
|
912
976
|
|
|
913
977
|
if (!frontmatter || Object.keys(frontmatter).length === 0) {
|
|
914
|
-
const body = stripFrontmatter(content);
|
|
978
|
+
const body = stripFrontmatter(content).replace(/\r\n/g, '\n');
|
|
915
979
|
const built = buildStateFrontmatter(body, cwd);
|
|
916
980
|
output(built, raw, JSON.stringify(built, null, 2));
|
|
917
981
|
return;
|
|
@@ -48,15 +48,27 @@
|
|
|
48
48
|
function buildSubcommandIndex(sourceText) {
|
|
49
49
|
const index = {};
|
|
50
50
|
if (typeof sourceText !== 'string') return index;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
// The dispatcher words these messages three ways, and until 2026-09-26 only the
|
|
52
|
+
// first was read, so git, distill, experiment, init, state and links got no
|
|
53
|
+
// suggestion on a typo:
|
|
54
|
+
// Unknown optimize subcommand. Available: …
|
|
55
|
+
// Unknown state subcommand: ${subcommand}. Available: … (also `unknown …`, and
|
|
56
|
+
// Unknown init workflow: ${workflow}\nAvailable: …)
|
|
57
|
+
// git subcommand required. Available: …
|
|
58
|
+
const patterns = [
|
|
59
|
+
/[Uu]nknown ([a-z][a-z-]*) (?:subcommand|workflow)[^'"`\n.]*(?:\.\s*|\\n)Available: ([^'"`\n\\]+)/g,
|
|
60
|
+
/([a-z][a-z-]*) subcommand required\. Available: ([^'"`\n\\]+)/g,
|
|
61
|
+
];
|
|
62
|
+
for (const re of patterns) {
|
|
63
|
+
let m;
|
|
64
|
+
while ((m = re.exec(sourceText)) !== null) {
|
|
65
|
+
const group = m[1];
|
|
66
|
+
const subs = m[2]
|
|
67
|
+
.split(',')
|
|
68
|
+
.map((s) => s.trim().split(/\s+/)[0]) // drop " [--apply]"-style hints
|
|
69
|
+
.filter((s) => /^[a-z][a-z0-9-]*$/.test(s));
|
|
70
|
+
if (subs.length) index[group] = [...new Set([...(index[group] || []), ...subs])];
|
|
71
|
+
}
|
|
60
72
|
}
|
|
61
73
|
return index;
|
|
62
74
|
}
|
|
@@ -28,9 +28,12 @@ function cmdTemplateSelect(cwd, planPath, raw) {
|
|
|
28
28
|
const fullPath = path.join(cwd, planPath);
|
|
29
29
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
30
30
|
|
|
31
|
-
// Count task
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
// Count tasks: PAN plans write `<task type="...">` blocks (templates/phase-prompt.md,
|
|
32
|
+
// pan-planner); older plans used `### Task N` headings. Counting only the headings
|
|
33
|
+
// gave every current plan a task count of 0.
|
|
34
|
+
const xmlTasks = content.match(/<task[\s>]/g) || [];
|
|
35
|
+
const headingTasks = content.match(/###\s*Task\s*\d+/g) || [];
|
|
36
|
+
const taskCount = xmlTasks.length || headingTasks.length;
|
|
34
37
|
|
|
35
38
|
// Check for decision-related keywords
|
|
36
39
|
const decisionMatch = content.match(/decision/gi) || [];
|
|
@@ -32,6 +32,24 @@ function detectInstalledRuntimes(cwd) {
|
|
|
32
32
|
return found;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Whether `buf` is the file a manifest hash was taken from, allowing for line
|
|
37
|
+
* endings: raw bytes, the all-LF form or the all-CRLF form may match, so a file
|
|
38
|
+
* git re-checked out with `core.autocrlf=true` is not reported as modified.
|
|
39
|
+
* Binary (NUL-bearing) buffers compare raw only. Same rule as
|
|
40
|
+
* `bytesMatchHashIgnoringEol` in bin/install-lib.cjs, which the installed core
|
|
41
|
+
* cannot require; tests/install-eol-hash.test.cjs pins both.
|
|
42
|
+
*/
|
|
43
|
+
function bytesMatchHashIgnoringEol(crypto, buf, expectedHash) {
|
|
44
|
+
if (!expectedHash) return false;
|
|
45
|
+
const sha = b => crypto.createHash('sha256').update(b).digest('hex');
|
|
46
|
+
if (sha(buf) === expectedHash) return true;
|
|
47
|
+
if (buf.includes(0)) return false;
|
|
48
|
+
const lf = buf.toString('latin1').replace(/\r\n/g, '\n');
|
|
49
|
+
if (sha(Buffer.from(lf, 'latin1')) === expectedHash) return true;
|
|
50
|
+
return sha(Buffer.from(lf.replace(/\n/g, '\r\n'), 'latin1')) === expectedHash;
|
|
51
|
+
}
|
|
52
|
+
|
|
35
53
|
/**
|
|
36
54
|
* Validate a single PAN runtime installation.
|
|
37
55
|
* Checks: manifest files exist, hashes match, settings integrity.
|
|
@@ -61,8 +79,7 @@ function validateRuntimeInstall(cwd, configDir, runtime) {
|
|
|
61
79
|
const absPath = path.join(baseDir, relPath);
|
|
62
80
|
try {
|
|
63
81
|
const content = fs.readFileSync(absPath);
|
|
64
|
-
|
|
65
|
-
if (actualHash !== expectedHash) {
|
|
82
|
+
if (!bytesMatchHashIgnoringEol(crypto, content, expectedHash)) {
|
|
66
83
|
modified.push(relPath);
|
|
67
84
|
}
|
|
68
85
|
} catch (_) {
|
|
@@ -107,7 +124,7 @@ function validateRuntimeInstall(cwd, configDir, runtime) {
|
|
|
107
124
|
}
|
|
108
125
|
}
|
|
109
126
|
}
|
|
110
|
-
// Copilot
|
|
127
|
+
// statusLine (Claude, Copilot; PAN writes none for Gemini since 2026-09-23)
|
|
111
128
|
if (settings.statusLine && settings.statusLine.command) {
|
|
112
129
|
hookCommands.push(settings.statusLine.command);
|
|
113
130
|
}
|
|
@@ -420,7 +420,13 @@ async function main() {
|
|
|
420
420
|
case 'resolve-model': {
|
|
421
421
|
const metadataIdx = args.indexOf('--metadata');
|
|
422
422
|
const metadataJson = metadataIdx !== -1 ? args[metadataIdx + 1] : undefined;
|
|
423
|
-
|
|
423
|
+
const attemptRaw = getArgValue(args, '--attempt');
|
|
424
|
+
let attempt;
|
|
425
|
+
if (attemptRaw !== null && attemptRaw !== undefined) {
|
|
426
|
+
if (!/^[1-9]\d*$/.test(String(attemptRaw))) error('--attempt must be a positive integer (1 for a first try)');
|
|
427
|
+
attempt = parseInt(attemptRaw, 10);
|
|
428
|
+
}
|
|
429
|
+
commands.cmdResolveModel(cwd, args[1], raw, metadataJson, attempt);
|
|
424
430
|
break;
|
|
425
431
|
}
|
|
426
432
|
|
|
@@ -1272,6 +1278,9 @@ async function main() {
|
|
|
1272
1278
|
daily_budget: budget != null ? Number(budget) : undefined,
|
|
1273
1279
|
enabled: args.includes('--disable') ? false : undefined,
|
|
1274
1280
|
paused: args.includes('--pause') ? true : (args.includes('--resume') ? false : undefined),
|
|
1281
|
+
// campaign.cjs has always honoured enforce_budget; no flag reached it, so
|
|
1282
|
+
// making the daily budget a hard stop meant editing schedule.json by hand.
|
|
1283
|
+
enforce_budget: args.includes('--enforce-budget') ? true : (args.includes('--advisory-budget') ? false : undefined),
|
|
1275
1284
|
}, raw);
|
|
1276
1285
|
} else if (subcommand === 'status' || !subcommand) {
|
|
1277
1286
|
campaign.cmdCampaignStatus(cwd, raw);
|
|
@@ -75,13 +75,23 @@ const RESOURCES = [
|
|
|
75
75
|
const SPAWN_TOOLS = [
|
|
76
76
|
{
|
|
77
77
|
name: 'pan_resolve_model', title: 'Resolve model for an agent', verb: 'resolve-model',
|
|
78
|
-
description: 'Resolve the model tier/id PAN would use for an agent under the active profile.',
|
|
78
|
+
description: 'Resolve the model tier/id PAN would use for an agent under the active profile. Pass attempt > 1 for a retry after a failure: the tier climbs toward the agent\'s quality tier (routing.max_escalations caps it).',
|
|
79
79
|
readOnly: true, destructive: false,
|
|
80
80
|
inputSchema: {
|
|
81
81
|
type: 'object', additionalProperties: false, required: ['agent'],
|
|
82
|
-
properties: {
|
|
82
|
+
properties: {
|
|
83
|
+
agent: { type: 'string', description: 'Agent type, e.g. pan-planner' },
|
|
84
|
+
attempt: { type: 'integer', minimum: 1, maximum: 99, description: '1 for a first try; 2 for the first retry after a failure' },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
args: (i) => {
|
|
88
|
+
const out = [str('agent', i && i.agent, AGENT_RE, 64)];
|
|
89
|
+
if (i && i.attempt !== undefined) {
|
|
90
|
+
if (!Number.isInteger(i.attempt) || i.attempt < 1 || i.attempt > 99) throw new Error('Invalid "attempt": must be an integer from 1 to 99');
|
|
91
|
+
out.push('--attempt', String(i.attempt));
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
83
94
|
},
|
|
84
|
-
args: (i) => [str('agent', i && i.agent, AGENT_RE, 64)],
|
|
85
95
|
},
|
|
86
96
|
{
|
|
87
97
|
name: 'pan_find_phase', title: 'Find a phase', verb: 'find-phase',
|
|
@@ -155,7 +165,7 @@ const { NATIVE_TOOLS } = require('./native-tools.cjs');
|
|
|
155
165
|
// safety argument of ADR-0041 is that no client input reaches it.
|
|
156
166
|
const PROJECT_CWD_PROPERTY = Object.freeze({
|
|
157
167
|
type: 'string',
|
|
158
|
-
description: 'Absolute path of the PAN project to operate on. Optional: defaults to the directory the server was started in. Pass it when the server was launched from a plugin directory (Agent Plugins clients do this by default), or to address another project.',
|
|
168
|
+
description: 'Absolute path of the PAN project to operate on. Optional: defaults to the directory the server was started in. Pass it when the server was launched from a plugin directory (Agent Plugins clients do this by default), or to address another project. The server acts on any existing directory named here with the permissions of the user who runs it — it trusts its caller as it trusts the session that started it.',
|
|
159
169
|
});
|
|
160
170
|
|
|
161
171
|
const PROJECT_CWD_MAX = 1024;
|
|
@@ -11,8 +11,8 @@ PAN uses three abstract tiers instead of hardcoded model names:
|
|
|
11
11
|
| Tier | Purpose | Anthropic | OpenAI | Google |
|
|
12
12
|
|------|---------|-----------|--------|--------|
|
|
13
13
|
| `reasoning` | Architecture, planning, complex decisions | inherit (your session's top-tier model) | inherit | inherit |
|
|
14
|
-
| `mid` | Execution, research, verification | Sonnet |
|
|
15
|
-
| `fast` | Read-only extraction, budget tasks | Haiku |
|
|
14
|
+
| `mid` | Execution, research, verification | Sonnet | gpt-6-sol | gemini-3.8-flash |
|
|
15
|
+
| `fast` | Read-only extraction, budget tasks | Haiku | gpt-6-luna | gemini-3.5-flash-lite |
|
|
16
16
|
|
|
17
17
|
**Why `inherit` for reasoning?** Host runtimes map "opus" to a specific model version. PAN returns `inherit` for reasoning-tier agents, so they use whatever top-tier model the user has configured. This avoids version conflicts and silent fallbacks.
|
|
18
18
|
|
|
@@ -39,7 +39,7 @@ At install time PAN also runs a **best-effort, advisory** capability check on th
|
|
|
39
39
|
| Class | Example model IDs | Role in PAN | Context | Relative cost | Notes |
|
|
40
40
|
|-------|-------------------|-------------|---------|---------------|-------|
|
|
41
41
|
| Fable / Mythos | `claude-fable-5` | **Recommended flagship** — deepest long-horizon reasoning; best for the bot army's Mission Control + planning | 1M | ~2× Opus | Runs input safety classifiers (see caveat below); requires 30-day data retention |
|
|
42
|
-
| Opus | `claude-opus-5`, `claude-opus-4-8` | **Cost-conscious pick** — same 1M context + thinking, about half the cost, no cyber classifier | 1M | 1× | The safe pick when you want Opus behavior without Fable's refusal surface |
|
|
42
|
+
| Opus | `claude-opus-5-5`, `claude-opus-5`, `claude-opus-4-8` | **Cost-conscious pick** — same 1M context + thinking, about half the cost, no cyber classifier | 1M | 1× | The safe pick when you want Opus behavior without Fable's refusal surface |
|
|
43
43
|
|
|
44
44
|
**Why the Fable class is the recommended flagship.** It is Anthropic's deepest class for demanding, long-horizon agentic work — exactly what PAN's hierarchical bot army (Mission Control → squads → workers) asks of its reasoning tier. Select the current release in that class in your host runtime and `inherit` routes the reasoning-tier agents to it automatically.
|
|
45
45
|
|
|
@@ -117,7 +117,7 @@ PAN auto-detects the LLM provider to map tiers to the right model names:
|
|
|
117
117
|
|
|
118
118
|
1. **Explicit config** — `routing.provider` in config.json (if not `"auto"`)
|
|
119
119
|
2. **Environment variable** — `PAN_PROVIDER` env var
|
|
120
|
-
3. **Runtime directory** — `.claude/` → Anthropic, `.codex/` → OpenAI, `.gemini/` → Google
|
|
120
|
+
3. **Runtime directory** — `.claude/` → Anthropic, `.codex/` → OpenAI, `.gemini/` → Google, `.opencode/` → OpenAI, `.github/` → default (first match wins)
|
|
121
121
|
4. **Fallback** — Default provider map (Anthropic-style names)
|
|
122
122
|
|
|
123
123
|
---
|
|
@@ -4,24 +4,22 @@ Configuration options for `.planning/` directory behavior.
|
|
|
4
4
|
|
|
5
5
|
<config_schema>
|
|
6
6
|
```json
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
},
|
|
11
|
-
"
|
|
12
|
-
"branching_strategy": "none",
|
|
13
|
-
"phase_branch_template": "pan/phase-{phase}-{slug}",
|
|
14
|
-
"milestone_branch_template": "pan/{milestone}-{slug}"
|
|
15
|
-
}
|
|
7
|
+
"commit_docs": true,
|
|
8
|
+
"search_gitignored": false,
|
|
9
|
+
"branching_strategy": "none",
|
|
10
|
+
"phase_branch_template": "pan/phase-{phase}-{slug}",
|
|
11
|
+
"milestone_branch_template": "pan/{milestone}-{slug}"
|
|
16
12
|
```
|
|
17
13
|
|
|
18
14
|
| Option | Default | Description |
|
|
19
15
|
|--------|---------|-------------|
|
|
20
16
|
| `commit_docs` | `true` | Whether to commit planning artifacts to git |
|
|
21
17
|
| `search_gitignored` | `false` | Add `--no-ignore` to broad rg searches |
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
18
|
+
| `branching_strategy` | `"none"` | Git branching approach: `"none"`, `"phase"`, or `"milestone"` |
|
|
19
|
+
| `phase_branch_template` | `"pan/phase-{phase}-{slug}"` | Branch template for phase strategy |
|
|
20
|
+
| `milestone_branch_template` | `"pan/{milestone}-{slug}"` | Branch template for milestone strategy |
|
|
21
|
+
|
|
22
|
+
These keys are top-level. The nested `planning.*` / `git.*` forms older versions wrote are read only when the top-level key is absent. A config `config-ensure-section` creates carries every top-level key, and one `/pan:new-project` writes carries a top-level `commit_docs` — so write the top-level key; a nested one is ignored whenever the top-level key exists.
|
|
25
23
|
</config_schema>
|
|
26
24
|
|
|
27
25
|
<commit_docs_behavior>
|
|
@@ -84,10 +82,8 @@ To use uncommitted mode:
|
|
|
84
82
|
|
|
85
83
|
1. **Set config:**
|
|
86
84
|
```json
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
"search_gitignored": true
|
|
90
|
-
}
|
|
85
|
+
"commit_docs": false,
|
|
86
|
+
"search_gitignored": true
|
|
91
87
|
```
|
|
92
88
|
|
|
93
89
|
2. **Add to .gitignore:**
|
|
@@ -101,7 +97,7 @@ To use uncommitted mode:
|
|
|
101
97
|
git commit -m "chore: stop tracking planning docs"
|
|
102
98
|
```
|
|
103
99
|
|
|
104
|
-
4. **Branch merges:**
|
|
100
|
+
4. **Branch merges:** PAN never merges branches; when you merge a phase or milestone branch yourself with `commit_docs: false`, keep `.planning/` out of the merge commit.
|
|
105
101
|
|
|
106
102
|
</setup_uncommitted_mode>
|
|
107
103
|
|
|
@@ -113,24 +109,24 @@ To use uncommitted mode:
|
|
|
113
109
|
|----------|---------------------|--------------|-------------|
|
|
114
110
|
| `none` | Never | N/A | N/A |
|
|
115
111
|
| `phase` | At `execute-phase` start | Single phase | User merges after phase |
|
|
116
|
-
| `milestone` | At first `execute-phase` of milestone | Entire milestone |
|
|
112
|
+
| `milestone` | At first `execute-phase` of milestone | Entire milestone | User merges after milestone |
|
|
117
113
|
|
|
118
|
-
**When `
|
|
114
|
+
**When `branching_strategy: "none"` (default):**
|
|
119
115
|
- All work commits to current branch
|
|
120
116
|
- Standard PAN behavior
|
|
121
117
|
|
|
122
|
-
**When `
|
|
118
|
+
**When `branching_strategy: "phase"`:**
|
|
123
119
|
- `execute-phase` creates/switches to a branch before execution
|
|
124
120
|
- Branch name from `phase_branch_template` (e.g., `pan/phase-03-authentication`)
|
|
125
121
|
- All plan commits go to that branch
|
|
126
122
|
- User merges branches manually after phase completion
|
|
127
|
-
- `milestone-done`
|
|
123
|
+
- `milestone-done` does not merge — merge the phase branches yourself
|
|
128
124
|
|
|
129
|
-
**When `
|
|
125
|
+
**When `branching_strategy: "milestone"`:**
|
|
130
126
|
- First `execute-phase` of milestone creates the milestone branch
|
|
131
127
|
- Branch name from `milestone_branch_template` (e.g., `pan/v1.0-mvp`)
|
|
132
128
|
- All phases in milestone commit to same branch
|
|
133
|
-
- `milestone-done`
|
|
129
|
+
- `milestone-done` archives and tags but does not merge — merge the milestone branch yourself
|
|
134
130
|
|
|
135
131
|
**Template variables:**
|
|
136
132
|
|
|
@@ -117,6 +117,14 @@ Parse JSON for: `phase`, `plans[]` (each with `id`, `wave`, `autonomous`, `objec
|
|
|
117
117
|
|
|
118
118
|
**Filtering:** Skip plans where `has_summary: true`. If `--gaps-only`: also skip non-gap_closure plans. If all filtered: "No matching incomplete plans" → exit.
|
|
119
119
|
|
|
120
|
+
**If `--gaps-only`:** this run is the fix round after a verification found gaps — a second attempt at work that failed once. Resolve the executor for that attempt and use it in place of `executor_model` for every spawn in this run:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
EXECUTOR_MODEL=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs resolve-model pan-executor --attempt 2 --raw)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
It equals `executor_model` unless the `budget` profile runs the executor below its quality tier; then it is one tier higher (capped by `routing.max_escalations`), so the gaps are not retried on the tier that left them.
|
|
127
|
+
|
|
120
128
|
Report:
|
|
121
129
|
```
|
|
122
130
|
## Execution Plan
|
|
@@ -224,7 +224,7 @@ The Focus workflow is a **scan → plan → exec → sync** pipeline. Each step
|
|
|
224
224
|
| `/pan:plan-phase <N>` | Research + plan a phase with verification |
|
|
225
225
|
| `/pan:exec-phase <N>` | Execute plans with wave-based parallelization |
|
|
226
226
|
| `/pan:research-phase <N>` | Deep ecosystem research for specialized domains |
|
|
227
|
-
| `/pan:verify-phase <N>` |
|
|
227
|
+
| `/pan:verify-phase <N>` | Re-run goal-backward verification with a test-suite gate |
|
|
228
228
|
|
|
229
229
|
### Phase Management
|
|
230
230
|
| Command | Description |
|
|
@@ -348,7 +348,7 @@ The Focus workflow is a **scan → plan → exec → sync** pipeline. Each step
|
|
|
348
348
|
|
|
349
349
|
**`/pan:exec-phase <N>`** — Execute all plans in a phase. Groups by wave, runs waves sequentially, plans within each wave in parallel.
|
|
350
350
|
|
|
351
|
-
**`/pan:verify-phase <N>`** —
|
|
351
|
+
**`/pan:verify-phase <N>`** — Re-run goal-backward verification behind a test-suite gate. Writes the phase's verification report (passed, gaps_found or human_needed); fix gaps with `/pan:plan-phase <N> --gaps`.
|
|
352
352
|
|
|
353
353
|
**`/pan:quick <desc>`** — Small tasks with PAN guarantees but shorter path. Skips researcher, checker, verifier. Lives in `.planning/quick/`.
|
|
354
354
|
|
|
@@ -364,7 +364,7 @@ Present next steps:
|
|
|
364
364
|
|
|
365
365
|
**Also available:**
|
|
366
366
|
- `/pan:phase-tests {next_phase}` — test another phase
|
|
367
|
-
- `/pan:verify-phase {phase_number}` — run
|
|
367
|
+
- `/pan:verify-phase {phase_number}` — re-run goal-backward verification
|
|
368
368
|
|
|
369
369
|
---
|
|
370
370
|
```
|
|
@@ -252,7 +252,7 @@ uat.md exists with gaps (diagnosed issues). User needs to plan fixes.
|
|
|
252
252
|
|
|
253
253
|
**Also available:**
|
|
254
254
|
- `/pan:exec-phase {phase}` — execute phase plans
|
|
255
|
-
- `/pan:verify-phase {phase}` — run
|
|
255
|
+
- `/pan:verify-phase {phase}` — re-run goal-backward verification
|
|
256
256
|
|
|
257
257
|
---
|
|
258
258
|
```
|