pan-wizard 3.30.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 +2 -2
- package/bin/install-lib.cjs +129 -33
- package/bin/install.js +16 -9
- package/commands/pan/army.md +4 -4
- 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 +70 -1
- package/hooks/dist/pan-state-reinject.js +158 -0
- package/hooks/dist/pan-stop-guard.js +48 -2
- package/hooks/dist/pan-trace-logger.js +41 -0
- package/package.json +1 -1
- 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 +232 -23
- package/pan-wizard-core/bin/lib/cost-rebuild.cjs +11 -0
- package/pan-wizard-core/bin/lib/cost.cjs +132 -43
- 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 +9 -2
- 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 +19 -2
- package/pan-wizard-core/bin/pan-tools.cjs +10 -1
- package/pan-wizard-core/mcp/tool-registry.cjs +14 -4
- 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/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 +1 -0
|
@@ -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 (_) {
|
|
@@ -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;
|
|
@@ -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
|
```
|
|
@@ -19,7 +19,7 @@ Extract from init JSON: `commit_docs`, `date`, `timestamp`, `todo_count`, `todos
|
|
|
19
19
|
|
|
20
20
|
Ensure directories exist:
|
|
21
21
|
```bash
|
|
22
|
-
mkdir -p .planning/todos/pending .planning/todos/
|
|
22
|
+
mkdir -p .planning/todos/pending .planning/todos/completed
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
Note existing areas from the todos array for consistency in infer_area step.
|
|
@@ -105,7 +105,7 @@ Use AskUserQuestion:
|
|
|
105
105
|
- header: "Action"
|
|
106
106
|
- question: "This todo relates to Phase [N]: [name]. What would you like to do?"
|
|
107
107
|
- options:
|
|
108
|
-
- "Work on it now" —
|
|
108
|
+
- "Work on it now" — mark it completed, start working
|
|
109
109
|
- "Add to phase plan" — include when planning Phase [N]
|
|
110
110
|
- "Brainstorm approach" — think through before deciding
|
|
111
111
|
- "Put it back" — return to list
|
|
@@ -116,7 +116,7 @@ Use AskUserQuestion:
|
|
|
116
116
|
- header: "Action"
|
|
117
117
|
- question: "What would you like to do with this todo?"
|
|
118
118
|
- options:
|
|
119
|
-
- "Work on it now" —
|
|
119
|
+
- "Work on it now" — mark it completed, start working
|
|
120
120
|
- "Create a phase" — /pan:add-phase with this scope
|
|
121
121
|
- "Brainstorm approach" — think through before deciding
|
|
122
122
|
- "Put it back" — return to list
|
|
@@ -125,8 +125,9 @@ Use AskUserQuestion:
|
|
|
125
125
|
<step name="execute_action">
|
|
126
126
|
**Work on it now:**
|
|
127
127
|
```bash
|
|
128
|
-
|
|
128
|
+
node ~/.claude/pan-wizard-core/bin/pan-tools.cjs todo complete "[filename]"
|
|
129
129
|
```
|
|
130
|
+
This moves it to `.planning/todos/completed/` with a `completed:` date — the directory `init todos` and `/pan:progress` count.
|
|
130
131
|
Update state.md todo count. Present problem/solution context. Begin work or ask how to proceed.
|
|
131
132
|
|
|
132
133
|
**Add to phase plan:**
|
|
@@ -150,11 +151,11 @@ Re-run `init todos` to get updated count, then update state.md "### Pending Todo
|
|
|
150
151
|
</step>
|
|
151
152
|
|
|
152
153
|
<step name="git_commit">
|
|
153
|
-
If todo was
|
|
154
|
+
If the todo was completed, commit the change:
|
|
154
155
|
|
|
155
156
|
```bash
|
|
156
157
|
git rm --cached .planning/todos/pending/[filename] 2>/dev/null || true
|
|
157
|
-
node ~/.claude/pan-wizard-core/bin/pan-tools.cjs commit "docs: start work on todo - [title]" --files .planning/todos/
|
|
158
|
+
node ~/.claude/pan-wizard-core/bin/pan-tools.cjs commit "docs: start work on todo - [title]" --files .planning/todos/completed/[filename] .planning/state.md
|
|
158
159
|
```
|
|
159
160
|
|
|
160
161
|
Tool respects `commit_docs` config and gitignore automatically.
|
|
@@ -172,5 +173,5 @@ Confirm: "Committed: docs: start work on todo - [title]"
|
|
|
172
173
|
- [ ] Appropriate actions offered
|
|
173
174
|
- [ ] Selected action executed
|
|
174
175
|
- [ ] state.md updated if todo count changed
|
|
175
|
-
- [ ] Changes committed to git (if todo
|
|
176
|
+
- [ ] Changes committed to git (if the todo was completed)
|
|
176
177
|
</success_criteria>
|
|
@@ -150,6 +150,8 @@ function hookCommands(rootVar) {
|
|
|
150
150
|
contextMonitorCommand: cmd('pan-context-monitor.js'),
|
|
151
151
|
costLoggerCommand: cmd('pan-cost-logger.js'),
|
|
152
152
|
traceLoggerCommand: cmd('pan-trace-logger.js'),
|
|
153
|
+
stopGuardCommand: cmd('pan-stop-guard.js'),
|
|
154
|
+
stateReinjectCommand: cmd('pan-state-reinject.js'),
|
|
153
155
|
};
|
|
154
156
|
}
|
|
155
157
|
|
|
@@ -166,6 +168,8 @@ function emitCopilotNamespace(agentsSrc, nsDir) {
|
|
|
166
168
|
content = lib.rewriteAgentReferenceCopy(content, TOKEN_PREFIX);
|
|
167
169
|
content = lib.stripThinkingFrontmatter(content, 'copilot');
|
|
168
170
|
content = lib.convertClaudeToCopilotAgent(content);
|
|
171
|
+
// The agent carries the root token, so it carries what the token means.
|
|
172
|
+
if (content.includes(TOKEN_PREFIX)) content = content.replace(/\s*$/, '\n\n' + lib.agentPluginSkillAdapterNote('agent') + '\n');
|
|
169
173
|
fs.writeFileSync(path.join(agentsDest, f.replace(/\.md$/, '.agent.md')), content);
|
|
170
174
|
agents++;
|
|
171
175
|
}
|
package/scripts/build-hooks.js
CHANGED
package/scripts/build-plugin.js
CHANGED
|
@@ -169,6 +169,15 @@ function main() {
|
|
|
169
169
|
);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
// 7. Eval suite (market-ideas M4). `claude plugin eval` reads `evals/` at the
|
|
173
|
+
// plugin root; the cases live in harness/plugin-evals/ beside the harness
|
|
174
|
+
// scenarios they come from. harness/ is not in the npm package, so a build from
|
|
175
|
+
// an installed copy has no suite to copy and the plugin ships without one.
|
|
176
|
+
const evalsSrc = path.join(ROOT, 'harness', 'plugin-evals');
|
|
177
|
+
if (fs.existsSync(evalsSrc)) {
|
|
178
|
+
fs.cpSync(evalsSrc, path.join(OUT, 'evals'), { recursive: true });
|
|
179
|
+
}
|
|
180
|
+
|
|
172
181
|
// Sanity report
|
|
173
182
|
const count = (p) => { try { return fs.readdirSync(p).length; } catch { return 0; } };
|
|
174
183
|
console.log('PAN plugin built at', path.relative(ROOT, OUT));
|
|
@@ -176,6 +185,7 @@ function main() {
|
|
|
176
185
|
console.log(' agents:', count(path.join(OUT, 'agents')));
|
|
177
186
|
console.log(' hooks:', count(path.join(OUT, 'hooks')));
|
|
178
187
|
console.log(' workflows:', workflowScripts.length);
|
|
188
|
+
console.log(' eval cases:', count(path.join(OUT, 'evals')) ? fs.readdirSync(path.join(OUT, 'evals'), { withFileTypes: true }).filter(e => e.isDirectory()).length : 0);
|
|
179
189
|
console.log(' version:', pkg.version);
|
|
180
190
|
}
|
|
181
191
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Print the npm dist-tag a version publishes under: `next` for a prerelease
|
|
4
|
+
* (`3.31.0-rc.1`), `latest` otherwise.
|
|
5
|
+
*
|
|
6
|
+
* WHY. The release workflow ran `npm publish` with no `--tag`, and npm's default
|
|
7
|
+
* tag is `latest` — so a release candidate would have become what
|
|
8
|
+
* `npm install pan-wizard` resolves to for every user (market-ideas queue M1,
|
|
9
|
+
* 2026-09-21). Publishing a candidate under `next` keeps it installable by name
|
|
10
|
+
* (`pan-wizard@next`, `pan-wizard@3.31.0-rc.1`) without moving anyone onto it.
|
|
11
|
+
*
|
|
12
|
+
* A version that is not `major.minor.patch[-prerelease]` exits 1 rather than
|
|
13
|
+
* guessing a tag: publishing junk under `latest` is the failure this prevents.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node scripts/npm-dist-tag.js # the tag for package.json's version
|
|
17
|
+
* node scripts/npm-dist-tag.js 3.31.0-rc.1
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { parse } = require('./deprecate-old-versions.js');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} version
|
|
27
|
+
* @returns {'latest'|'next'|null} null when the version does not parse
|
|
28
|
+
*/
|
|
29
|
+
function distTagFor(version) {
|
|
30
|
+
const v = parse(version);
|
|
31
|
+
if (!v) return null;
|
|
32
|
+
return v.pre === null ? 'latest' : 'next';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function main() {
|
|
36
|
+
const version = process.argv[2] || require(path.join(__dirname, '..', 'package.json')).version;
|
|
37
|
+
const tag = distTagFor(version);
|
|
38
|
+
if (!tag) {
|
|
39
|
+
process.stderr.write(`npm-dist-tag: '${version}' is not a semver version — refusing to pick a tag\n`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
process.stdout.write(tag + '\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { distTagFor };
|
|
46
|
+
|
|
47
|
+
if (require.main === module) main();
|
package/scripts/plugin-path.js
CHANGED
|
@@ -49,8 +49,13 @@ function fail(message) {
|
|
|
49
49
|
function build() {
|
|
50
50
|
try {
|
|
51
51
|
// Relay the builder's stdout to stderr so stdout stays single-line.
|
|
52
|
+
// PAN_PLUGIN_OUT redirects the builder; this script prints PLUGIN_DIR, so a value
|
|
53
|
+
// inherited from the caller's environment would build one place and point the
|
|
54
|
+
// marketplace at another (3.28.0 review, LOW). Build where the path says.
|
|
55
|
+
const env = { ...process.env };
|
|
56
|
+
delete env.PAN_PLUGIN_OUT;
|
|
52
57
|
const out = execFileSync(process.execPath, [path.join(ROOT, 'scripts', 'build-plugin.js')], {
|
|
53
|
-
cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
58
|
+
cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env,
|
|
54
59
|
});
|
|
55
60
|
if (out) process.stderr.write(out);
|
|
56
61
|
} catch (err) {
|
package/scripts/test-surface.cjs
CHANGED
|
@@ -68,6 +68,7 @@ const EVENT_HOOKS = Object.freeze({
|
|
|
68
68
|
postToolUse: ['pan-context-monitor.js'],
|
|
69
69
|
subagentStop: ['pan-cost-logger.js', 'pan-trace-logger.js'],
|
|
70
70
|
stop: ['pan-stop-guard.js'],
|
|
71
|
+
compact: ['pan-state-reinject.js'],
|
|
71
72
|
});
|
|
72
73
|
|
|
73
74
|
// ─── Parsers (pure) ─────────────────────────────────────────────────────────
|