pan-wizard 3.14.0 → 3.15.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/LICENSE +21 -21
- package/README.md +1 -1
- package/commands/pan/audit-deployment.md +384 -384
- package/commands/pan/focus-auto.md +683 -683
- package/commands/pan/focus-doc-audit.md +530 -530
- package/commands/pan/focus-drift-walking.md +525 -525
- package/commands/pan/git.md +1 -1
- package/commands/pan/hud.md +3 -2
- package/commands/pan/report.md +70 -0
- package/hooks/dist/pan-check-update.js +62 -62
- package/hooks/dist/pan-context-monitor.js +134 -134
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/frontmatter.cjs +442 -442
- package/pan-wizard-core/bin/lib/hud.cjs +183 -14
- package/pan-wizard-core/bin/lib/phase-report.cjs +723 -0
- package/pan-wizard-core/bin/lib/utils.cjs +171 -171
- package/pan-wizard-core/bin/pan-tools.cjs +1499 -1473
- package/pan-wizard-core/references/checkpoints.md +776 -776
- package/pan-wizard-core/references/continuation-format.md +249 -249
- package/pan-wizard-core/references/questioning.md +145 -145
- package/pan-wizard-core/references/tdd.md +263 -263
- package/pan-wizard-core/references/ui-brand.md +160 -160
- package/pan-wizard-core/templates/config.json +38 -38
- package/scripts/build-hooks.js +51 -51
- package/scripts/git-hooks/pre-commit +0 -0
- package/scripts/release-check.js +27 -36
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
* Graceful degradation: army-only panels (command stack, campaign, harness,
|
|
12
12
|
* worktrees) render only when a campaign is scheduled or army worktrees
|
|
13
13
|
* exist. A plain PAN project still gets mission, roadmap, telemetry,
|
|
14
|
-
* requirements/quality and activity panels.
|
|
14
|
+
* requirements/quality and activity panels. A project with no phase/roadmap
|
|
15
|
+
* layout (focus-auto / autonomous-loop / imported repos) gets a "planning
|
|
16
|
+
* activity" fallback panel summarising its .planning/ markdown instead of a
|
|
17
|
+
* bare page. Telemetry never quotes a dollar figure it can't stand behind: a
|
|
18
|
+
* poisoned or wholly-unpriced ledger degrades to an honest advisory.
|
|
15
19
|
*
|
|
16
20
|
* collectHudData() and renderHud() are pure given their inputs (a `now` Date
|
|
17
21
|
* is injected for testability); cmdHud() is the only side-effecting wrapper.
|
|
@@ -136,6 +140,79 @@ function scanVerification(cwd) {
|
|
|
136
140
|
return found;
|
|
137
141
|
}
|
|
138
142
|
|
|
143
|
+
/** Humanize a file mtime as an age relative to `now` ("3d ago"). */
|
|
144
|
+
function relAge(mtimeMs, now) {
|
|
145
|
+
const ref = now instanceof Date ? now.getTime() : Number(now);
|
|
146
|
+
const diff = ref - Number(mtimeMs);
|
|
147
|
+
if (!isFinite(diff)) return '';
|
|
148
|
+
if (diff < 60000) return 'just now';
|
|
149
|
+
const min = Math.floor(diff / 60000);
|
|
150
|
+
if (min < 60) return `${min}m ago`;
|
|
151
|
+
const h = Math.floor(min / 60);
|
|
152
|
+
if (h < 24) return `${h}h ago`;
|
|
153
|
+
const days = Math.floor(h / 24);
|
|
154
|
+
if (days < 30) return `${days}d ago`;
|
|
155
|
+
const mo = Math.floor(days / 30);
|
|
156
|
+
return `${mo}mo ago`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Fallback signal for projects that don't use the phase/state/roadmap layout
|
|
161
|
+
* (focus-auto / autonomous-loop projects, imported repos, ad-hoc `.planning/`
|
|
162
|
+
* trees): surface whatever markdown lives under `.planning/` so the HUD isn't
|
|
163
|
+
* a dead shell. Only `.md` docs are stat-ed — a focus-auto ledger can hold
|
|
164
|
+
* tens of thousands of JSON artifacts, and we never stat those. The walk is
|
|
165
|
+
* depth- and entry-bounded so a pathological tree can't stall the render.
|
|
166
|
+
* Returns null when there are no docs to show.
|
|
167
|
+
*/
|
|
168
|
+
function scanPlanningActivity(cwd, now) {
|
|
169
|
+
const root = planningPath(cwd);
|
|
170
|
+
let topEntries;
|
|
171
|
+
try { topEntries = fs.readdirSync(root, { withFileTypes: true }); } catch { return null; }
|
|
172
|
+
|
|
173
|
+
const docs = []; // { rel, folder, mtime }
|
|
174
|
+
const byFolder = {}; // folder name -> md count
|
|
175
|
+
let visited = 0;
|
|
176
|
+
const MAX_ENTRIES = 40000; // backstop against pathological trees
|
|
177
|
+
const MAX_DEPTH = 3;
|
|
178
|
+
|
|
179
|
+
function record(fp, folder) {
|
|
180
|
+
let st;
|
|
181
|
+
try { st = fs.statSync(fp); } catch { return; }
|
|
182
|
+
byFolder[folder] = (byFolder[folder] || 0) + 1;
|
|
183
|
+
docs.push({ rel: toPosix(path.relative(root, fp)), folder, mtime: st.mtimeMs });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function walk(dir, folder, depth) {
|
|
187
|
+
if (depth > MAX_DEPTH || visited > MAX_ENTRIES) return;
|
|
188
|
+
let ents = [];
|
|
189
|
+
try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
190
|
+
for (const e of ents) {
|
|
191
|
+
if (visited++ > MAX_ENTRIES) return;
|
|
192
|
+
if (e.isDirectory()) walk(path.join(dir, e.name), folder, depth + 1);
|
|
193
|
+
else if (e.name.endsWith('.md')) record(path.join(dir, e.name), folder);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const e of topEntries) {
|
|
198
|
+
if (visited++ > MAX_ENTRIES) break;
|
|
199
|
+
if (e.isDirectory()) walk(path.join(root, e.name), e.name, 1);
|
|
200
|
+
else if (e.name.endsWith('.md')) record(path.join(root, e.name), '(root)');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!docs.length) return null;
|
|
204
|
+
docs.sort((a, b) => b.mtime - a.mtime);
|
|
205
|
+
const folders = Object.keys(byFolder)
|
|
206
|
+
.map(f => ({ folder: f, count: byFolder[f] }))
|
|
207
|
+
.sort((a, b) => b.count - a.count);
|
|
208
|
+
const recent = docs.slice(0, 8).map(d => ({
|
|
209
|
+
name: d.rel.split('/').pop(),
|
|
210
|
+
folder: d.folder,
|
|
211
|
+
when: relAge(d.mtime, now),
|
|
212
|
+
}));
|
|
213
|
+
return { doc_count: docs.length, folder_count: folders.length, folders, recent };
|
|
214
|
+
}
|
|
215
|
+
|
|
139
216
|
function recentCommits(cwd, limit) {
|
|
140
217
|
if (!isGitRepo(cwd)) return [];
|
|
141
218
|
const r = execGit(cwd, ['log', '-n', String(limit || 8), '--pretty=%h\x1f%s\x1f%cr']);
|
|
@@ -278,6 +355,7 @@ function collectHudData(cwd, opts = {}) {
|
|
|
278
355
|
telemetry,
|
|
279
356
|
requirements: scanRequirements(cwd),
|
|
280
357
|
quality: scanVerification(cwd),
|
|
358
|
+
planning_activity: scanPlanningActivity(cwd, now),
|
|
281
359
|
activity: recentCommits(cwd, 8),
|
|
282
360
|
};
|
|
283
361
|
}
|
|
@@ -343,6 +421,36 @@ function fmtTokens(n) {
|
|
|
343
421
|
return String(v);
|
|
344
422
|
}
|
|
345
423
|
|
|
424
|
+
/**
|
|
425
|
+
* Assess whether a cost ledger is trustworthy enough to show dollar figures.
|
|
426
|
+
* Two failure modes are treated as "don't quote a number":
|
|
427
|
+
* - legacy: more records were quarantined as implausible than survived (the
|
|
428
|
+
* pre-v3.12.4 transcript-oversum bug) — reset advised.
|
|
429
|
+
* - unresolved: every surviving record lacks a resolvable model→rate, so the
|
|
430
|
+
* computed spend is a misleading $0 even though real tokens were spent.
|
|
431
|
+
* Returns { ok:true } when figures are safe to display.
|
|
432
|
+
*/
|
|
433
|
+
function ledgerReliability(totals) {
|
|
434
|
+
const t = totals || {};
|
|
435
|
+
const calls = t.calls || 0;
|
|
436
|
+
const suspect = t.suspect_excluded || 0;
|
|
437
|
+
const unknown = t.cost_unknown || 0;
|
|
438
|
+
if (suspect > calls) {
|
|
439
|
+
const total = suspect + calls;
|
|
440
|
+
return {
|
|
441
|
+
ok: false, kind: 'legacy',
|
|
442
|
+
message: `${suspect} of ${total} cost records are implausible (the pre-v3.12.4 telemetry capture bug). Reset the ledger with <b>pan-tools cost clear</b> — records captured after the fix are accurate.`,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
if (calls > 0 && unknown >= calls) {
|
|
446
|
+
return {
|
|
447
|
+
ok: false, kind: 'unresolved',
|
|
448
|
+
message: `None of ${calls} cost records carry a resolvable model→rate, so spend can't be priced (it would read $0). Set <b>cost.rates</b> in .planning/config.json, or record a model on each call, to price them. Token volume below is still accurate.`,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
return { ok: true };
|
|
452
|
+
}
|
|
453
|
+
|
|
346
454
|
function relDue(nextIso, nowIso) {
|
|
347
455
|
const a = new Date(nextIso).getTime();
|
|
348
456
|
const b = new Date(nowIso).getTime();
|
|
@@ -403,22 +511,29 @@ function renderMission(d) {
|
|
|
403
511
|
const req = d.requirements;
|
|
404
512
|
const tok = d.telemetry.totals.input_tokens + d.telemetry.totals.output_tokens;
|
|
405
513
|
const cache = d.telemetry.cache_hit_rate_pct;
|
|
514
|
+
const rel = ledgerReliability(d.telemetry.totals);
|
|
515
|
+
const spendCard = rel.ok
|
|
516
|
+
? metricCard({
|
|
517
|
+
label: 'Spend', value: fmtUsd(d.telemetry.totals.cost_usd),
|
|
518
|
+
sub: `${fmtTokens(tok)} tok${cache == null ? '' : ' · ' + cache + '% cache'}`,
|
|
519
|
+
})
|
|
520
|
+
: metricCard({
|
|
521
|
+
label: 'Spend', value: '—',
|
|
522
|
+
sub: rel.kind === 'unresolved' ? `${fmtTokens(tok)} tok · cost n/a` : 'ledger unreliable',
|
|
523
|
+
});
|
|
406
524
|
const cards = [
|
|
407
525
|
metricCard({
|
|
408
526
|
label: 'Progress', value: prog.percent == null ? '—' : String(prog.percent), unit: prog.percent == null ? '' : '%',
|
|
409
527
|
barPct: prog.percent, barColor: 'var(--coral)', sub: prog.total ? `${prog.completed} / ${prog.total} phases` : 'no phases',
|
|
410
528
|
}),
|
|
411
529
|
metricCard({
|
|
412
|
-
label: 'Phase', value: st.current_phase || prog.completed
|
|
530
|
+
label: 'Phase', value: st.current_phase || (prog.total ? prog.completed : '—'), unit: prog.total ? ` / ${prog.total}` : '',
|
|
413
531
|
sub: st.current_phase_name || '',
|
|
414
532
|
}),
|
|
415
533
|
req
|
|
416
534
|
? metricCard({ label: 'Requirements', value: req.done, unit: ` / ${req.total}`, barPct: Math.round((req.done / req.total) * 100), barColor: 'var(--indigo)', sub: `${req.total - req.done} open` })
|
|
417
535
|
: metricCard({ label: 'Requirements', value: '—', sub: 'none tracked' }),
|
|
418
|
-
|
|
419
|
-
label: 'Spend', value: fmtUsd(d.telemetry.totals.cost_usd),
|
|
420
|
-
sub: `${fmtTokens(tok)} tok${cache == null ? '' : ' · ' + cache + '% cache'}`,
|
|
421
|
-
}),
|
|
536
|
+
spendCard,
|
|
422
537
|
].join('');
|
|
423
538
|
return `
|
|
424
539
|
<section class="panel mission">
|
|
@@ -567,6 +682,32 @@ function renderRoadmap(d) {
|
|
|
567
682
|
</section>`;
|
|
568
683
|
}
|
|
569
684
|
|
|
685
|
+
// Graceful degradation: projects without a phase/state/roadmap layout still
|
|
686
|
+
// have real signal under .planning/ (focus-auto designs, ADRs, findings). Show
|
|
687
|
+
// it — but only when there's no roadmap, so standard projects are unchanged.
|
|
688
|
+
function renderPlanningActivity(d) {
|
|
689
|
+
const a = d.planning_activity;
|
|
690
|
+
if (!a || d.roadmap.length) return '';
|
|
691
|
+
const maxCount = a.folders.reduce((m, f) => Math.max(m, f.count), 0) || 1;
|
|
692
|
+
const folderBars = a.folders.slice(0, 7).map(f => {
|
|
693
|
+
const pct = Math.round((f.count / maxCount) * 100);
|
|
694
|
+
return `<div class="sqbar"><div class="sqbar-h"><span>${esc(f.folder)}</span><span>${esc(f.count)}</span></div>`
|
|
695
|
+
+ `<div class="bar"><span style="width:${pct}%;background:var(--indigo)"></span></div></div>`;
|
|
696
|
+
}).join('');
|
|
697
|
+
const recent = a.recent.map(r =>
|
|
698
|
+
`<div class="row"><span class="amono"><span class="dot" style="background:var(--coral)"></span>${esc(r.name)}</span>`
|
|
699
|
+
+ `<span class="amono dim">${esc(r.folder)} · ${esc(r.when)}</span></div>`
|
|
700
|
+
).join('');
|
|
701
|
+
return `
|
|
702
|
+
<section class="panel">
|
|
703
|
+
<div class="ph">planning activity</div>
|
|
704
|
+
<div class="row"><span class="rl">Documents</span><span class="amono">${esc(a.doc_count)} across ${esc(a.folder_count)} folder${a.folder_count === 1 ? '' : 's'}</span></div>
|
|
705
|
+
<div class="sqbars" style="margin-top:8px">${folderBars}</div>
|
|
706
|
+
<div class="ph" style="margin-top:18px">recently updated</div>
|
|
707
|
+
${recent}
|
|
708
|
+
</section>`;
|
|
709
|
+
}
|
|
710
|
+
|
|
570
711
|
function renderHarness(d) {
|
|
571
712
|
if (!d.army_active) return '';
|
|
572
713
|
const h = d.harness;
|
|
@@ -586,15 +727,20 @@ function renderHarness(d) {
|
|
|
586
727
|
|
|
587
728
|
function renderTelemetry(d) {
|
|
588
729
|
const t = d.telemetry;
|
|
589
|
-
//
|
|
590
|
-
//
|
|
591
|
-
|
|
592
|
-
|
|
730
|
+
// Don't present salvaged or uncomputable numbers as if trustworthy — a
|
|
731
|
+
// poisoned or unpriced ledger gets an honest advisory instead of a fake $0.
|
|
732
|
+
const rel = ledgerReliability(t.totals);
|
|
733
|
+
if (!rel.ok) {
|
|
734
|
+
const label = rel.kind === 'legacy' ? 'legacy ledger — unreliable' : 'cost unresolved';
|
|
735
|
+
const tokLine = rel.kind === 'unresolved'
|
|
736
|
+
? `<div class="row" style="margin-top:10px"><span class="rl">Tokens processed</span><span class="amono">${fmtTokens(t.totals.input_tokens + t.totals.output_tokens)} · ${t.totals.calls} calls</span></div>`
|
|
737
|
+
: '';
|
|
593
738
|
return `
|
|
594
739
|
<section class="panel">
|
|
595
740
|
<div class="ph">telemetry</div>
|
|
596
|
-
<div class="row noborder"><span class="rl">Status</span>${pill(
|
|
597
|
-
<div class="amono dim" style="margin-top:10px;line-height:1.6;">${
|
|
741
|
+
<div class="row noborder"><span class="rl">Status</span>${pill(label, 'warn')}</div>
|
|
742
|
+
<div class="amono dim" style="margin-top:10px;line-height:1.6;">${rel.message}</div>
|
|
743
|
+
${tokLine}
|
|
598
744
|
</section>`;
|
|
599
745
|
}
|
|
600
746
|
const keys = Object.keys(t.by_squad).sort((a, b) => t.by_squad[b].cost - t.by_squad[a].cost);
|
|
@@ -794,13 +940,16 @@ body{margin:0;background:var(--bg);color:var(--text);font-family:var(--font);fon
|
|
|
794
940
|
function renderHud(d) {
|
|
795
941
|
const leftCol = [renderCampaign(d), renderRoadmap(d)].filter(Boolean).join('');
|
|
796
942
|
const rightCol = [renderHarness(d), renderTelemetry(d)].filter(Boolean).join('');
|
|
797
|
-
|
|
943
|
+
// Two columns only when both have content; otherwise render full-width so a
|
|
944
|
+
// lone panel (e.g. telemetry on a non-army project) doesn't float in half.
|
|
945
|
+
const grid = (leftCol && rightCol)
|
|
798
946
|
? `<div class="grid"><div class="gcol">${leftCol}</div><div class="gcol">${rightCol}</div></div>`
|
|
799
|
-
:
|
|
947
|
+
: (leftCol || rightCol);
|
|
800
948
|
const body = [
|
|
801
949
|
renderTopBar(d),
|
|
802
950
|
renderMission(d),
|
|
803
951
|
renderNowBuilding(d),
|
|
952
|
+
renderPlanningActivity(d),
|
|
804
953
|
renderCommandStack(d),
|
|
805
954
|
grid,
|
|
806
955
|
renderWorktrees(d),
|
|
@@ -883,6 +1032,7 @@ function cmdHud(cwd, opts = {}, raw) {
|
|
|
883
1032
|
const sections = [
|
|
884
1033
|
'mission',
|
|
885
1034
|
data.roadmap.length && 'now-building',
|
|
1035
|
+
(!data.roadmap.length && data.planning_activity) && 'planning-activity',
|
|
886
1036
|
data.army_active && 'command-stack',
|
|
887
1037
|
data.campaign && 'campaign',
|
|
888
1038
|
data.army_active && 'safety-harness',
|
|
@@ -909,7 +1059,26 @@ module.exports = {
|
|
|
909
1059
|
scanPhases,
|
|
910
1060
|
scanRequirements,
|
|
911
1061
|
scanVerification,
|
|
1062
|
+
scanPlanningActivity,
|
|
1063
|
+
ledgerReliability,
|
|
912
1064
|
buildArmy,
|
|
913
1065
|
telemetryBySquad,
|
|
914
1066
|
esc,
|
|
1067
|
+
// shared rendering foundation — reused verbatim by phase-report.cjs so both
|
|
1068
|
+
// surfaces look identical. Exporting these adds NO logic and cannot change
|
|
1069
|
+
// HUD output (a golden snapshot test guards that). openInBrowser is
|
|
1070
|
+
// deliberately NOT shared: its inline allowlist is a CodeQL taint barrier
|
|
1071
|
+
// that must stay a literal, so phase-report.cjs carries its own byte-copy.
|
|
1072
|
+
HUD_CSS,
|
|
1073
|
+
pill,
|
|
1074
|
+
bar,
|
|
1075
|
+
metricCard,
|
|
1076
|
+
fmtUsd,
|
|
1077
|
+
fmtTokens,
|
|
1078
|
+
relAge,
|
|
1079
|
+
pipelineStage,
|
|
1080
|
+
STATUS_KIND,
|
|
1081
|
+
STATUS_DOT,
|
|
1082
|
+
MARK_SVG,
|
|
1083
|
+
CHECK_SVG,
|
|
915
1084
|
};
|