pan-wizard 3.33.0 → 3.35.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 +29 -26
- package/agents/pan-conductor.md +18 -17
- package/agents/pan-counterfactual.md +2 -2
- package/agents/pan-debugger.md +30 -9
- package/agents/pan-design-checker.md +2 -1
- package/agents/pan-designer.md +2 -2
- package/agents/pan-distiller.md +3 -3
- package/agents/pan-document_code.md +14 -14
- package/agents/pan-executor.md +30 -9
- package/agents/pan-experiment-runner.md +9 -8
- package/agents/pan-hardener.md +1 -1
- package/agents/pan-integration-checker.md +2 -2
- package/agents/pan-knowledge.md +1 -1
- package/agents/pan-meta-reviewer.md +4 -4
- package/agents/pan-optimizer.md +39 -43
- package/agents/pan-phase-researcher.md +10 -10
- package/agents/pan-plan-checker.md +37 -34
- package/agents/pan-planner.md +57 -44
- package/agents/pan-previewer.md +1 -1
- package/agents/pan-project-researcher.md +12 -12
- package/agents/pan-release.md +1 -1
- package/agents/pan-research-synthesizer.md +3 -3
- package/agents/pan-reviewer.md +26 -3
- package/agents/pan-roadmapper.md +15 -14
- package/agents/pan-verifier.md +33 -25
- package/bin/install-lib.cjs +176 -38
- package/bin/install.js +81 -63
- package/commands/pan/army.md +15 -11
- package/commands/pan/assumptions.md +2 -2
- package/commands/pan/audit-deployment.md +11 -12
- package/commands/pan/cost.md +8 -8
- package/commands/pan/dashboard.md +0 -3
- package/commands/pan/debug.md +10 -7
- package/commands/pan/design-phase.md +5 -5
- package/commands/pan/discuss-phase.md +2 -0
- package/commands/pan/exec-phase.md +21 -37
- package/commands/pan/experiment.md +9 -8
- package/commands/pan/focus-auto.md +22 -20
- package/commands/pan/focus-design.md +7 -6
- package/commands/pan/focus-doc-audit.md +4 -3
- package/commands/pan/focus-drift-walking.md +4 -3
- package/commands/pan/focus-exec.md +6 -6
- package/commands/pan/focus-plan.md +12 -11
- package/commands/pan/focus-scan.md +5 -3
- package/commands/pan/focus-sync.md +3 -2
- package/commands/pan/git.md +8 -8
- package/commands/pan/health.md +2 -2
- package/commands/pan/hud.md +4 -7
- package/commands/pan/hygiene.md +14 -7
- package/commands/pan/knowledge.md +9 -10
- package/commands/pan/learn.md +10 -7
- package/commands/pan/links.md +4 -1
- package/commands/pan/map-codebase.md +1 -1
- package/commands/pan/mcp-bridge.md +19 -21
- package/commands/pan/milestone-done.md +6 -3
- package/commands/pan/milestone-new.md +1 -1
- package/commands/pan/new-project.md +10 -10
- package/commands/pan/optimize.md +18 -14
- package/commands/pan/patches.md +2 -6
- package/commands/pan/pause.md +1 -1
- package/commands/pan/phase-tests.md +3 -2
- package/commands/pan/plan-phase.md +9 -19
- package/commands/pan/preview.md +2 -3
- package/commands/pan/profile.md +2 -2
- package/commands/pan/quick.md +1 -1
- package/commands/pan/remove-phase.md +1 -1
- package/commands/pan/report.md +3 -7
- package/commands/pan/research-phase.md +16 -15
- package/commands/pan/resume.md +9 -11
- package/commands/pan/retro.md +5 -3
- package/commands/pan/review-deep.md +7 -9
- package/commands/pan/settings.md +1 -1
- package/commands/pan/verify-phase.md +0 -11
- package/commands/pan/what-if.md +3 -4
- package/hooks/dist/pan-context-monitor.js +352 -44
- package/hooks/dist/pan-cost-logger.js +30 -0
- package/hooks/dist/pan-guard.js +244 -0
- package/hooks/dist/pan-statusline.js +73 -24
- package/hooks/dist/pan-stop-guard.js +7 -2
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/agents-md.cjs +44 -6
- package/pan-wizard-core/bin/lib/bridge.cjs +6 -5
- package/pan-wizard-core/bin/lib/campaign.cjs +4 -2
- package/pan-wizard-core/bin/lib/commands-learnings.cjs +19 -2
- package/pan-wizard-core/bin/lib/commands.cjs +57 -6
- package/pan-wizard-core/bin/lib/config.cjs +7 -1
- package/pan-wizard-core/bin/lib/constants.cjs +18 -6
- package/pan-wizard-core/bin/lib/context-budget.cjs +12 -9
- package/pan-wizard-core/bin/lib/core.cjs +53 -19
- package/pan-wizard-core/bin/lib/cost-rebuild.cjs +13 -1
- package/pan-wizard-core/bin/lib/cost.cjs +142 -13
- package/pan-wizard-core/bin/lib/distill.cjs +6 -3
- package/pan-wizard-core/bin/lib/doc-lint.cjs +30 -11
- package/pan-wizard-core/bin/lib/findings.cjs +135 -21
- package/pan-wizard-core/bin/lib/focus.cjs +13 -4
- package/pan-wizard-core/bin/lib/frontmatter.cjs +105 -26
- package/pan-wizard-core/bin/lib/hygiene.cjs +184 -17
- package/pan-wizard-core/bin/lib/init.cjs +13 -4
- package/pan-wizard-core/bin/lib/knowledge.cjs +17 -1
- package/pan-wizard-core/bin/lib/learn-index.cjs +111 -10
- package/pan-wizard-core/bin/lib/learn-lint.cjs +30 -3
- package/pan-wizard-core/bin/lib/memory-optimize.cjs +8 -4
- package/pan-wizard-core/bin/lib/memory.cjs +508 -54
- package/pan-wizard-core/bin/lib/milestone.cjs +8 -3
- package/pan-wizard-core/bin/lib/optimize.cjs +97 -17
- package/pan-wizard-core/bin/lib/phase-gate.cjs +174 -0
- package/pan-wizard-core/bin/lib/phase-remove.cjs +85 -21
- package/pan-wizard-core/bin/lib/phase-report.cjs +8 -2
- package/pan-wizard-core/bin/lib/phase.cjs +39 -13
- package/pan-wizard-core/bin/lib/preview.cjs +34 -31
- package/pan-wizard-core/bin/lib/roadmap-compact.cjs +267 -0
- package/pan-wizard-core/bin/lib/roadmap.cjs +243 -9
- package/pan-wizard-core/bin/lib/runner.cjs +2 -1
- package/pan-wizard-core/bin/lib/state-compact.cjs +11 -6
- package/pan-wizard-core/bin/lib/state.cjs +43 -28
- package/pan-wizard-core/bin/lib/template.cjs +11 -4
- package/pan-wizard-core/bin/lib/verdict.cjs +140 -10
- package/pan-wizard-core/bin/lib/verify-integrity.cjs +181 -0
- package/pan-wizard-core/bin/lib/verify-plan-set.cjs +230 -0
- package/pan-wizard-core/bin/lib/verify-preflight.cjs +4 -13
- package/pan-wizard-core/bin/lib/verify-retro.cjs +5 -3
- package/pan-wizard-core/bin/lib/verify-scope.cjs +11 -7
- package/pan-wizard-core/bin/lib/verify.cjs +30 -28
- package/pan-wizard-core/bin/lib/whatif.cjs +53 -9
- package/pan-wizard-core/bin/pan-tools.cjs +114 -19
- package/pan-wizard-core/learnings/index.json +5 -5
- package/pan-wizard-core/mcp/merge-gate.cjs +14 -3
- package/pan-wizard-core/mcp/native-tools.cjs +3 -3
- package/pan-wizard-core/opencode/pan-wizard.js +139 -0
- package/pan-wizard-core/references/checkpoints.md +3 -3
- package/pan-wizard-core/references/continuation-format.md +3 -3
- package/pan-wizard-core/references/decimal-phase-calculation.md +2 -3
- package/pan-wizard-core/references/design-methodology.md +1 -1
- package/pan-wizard-core/references/git-integration.md +4 -6
- package/pan-wizard-core/references/git-planning-commit.md +2 -2
- package/pan-wizard-core/references/guardrails.md +2 -2
- package/pan-wizard-core/references/handoff-decisions.md +3 -3
- package/pan-wizard-core/references/model-profile-resolution.md +5 -5
- package/pan-wizard-core/references/model-profiles.md +15 -10
- package/pan-wizard-core/references/phase-argument-parsing.md +7 -7
- package/pan-wizard-core/references/planning-config.md +7 -7
- package/pan-wizard-core/references/tdd.md +10 -1
- package/pan-wizard-core/references/verdict-contract.md +6 -1
- package/pan-wizard-core/references/verification-patterns.md +5 -5
- package/pan-wizard-core/templates/codebase/architecture.md +2 -2
- package/pan-wizard-core/templates/codebase/best-practices.md +2 -2
- package/pan-wizard-core/templates/codebase/concerns.md +3 -3
- package/pan-wizard-core/templates/codebase/integrations.md +6 -6
- package/pan-wizard-core/templates/codebase/relationships.md +2 -2
- package/pan-wizard-core/templates/codebase/stack.md +2 -2
- package/pan-wizard-core/templates/codebase/structure.md +4 -4
- package/pan-wizard-core/templates/codebase/testing.md +4 -4
- package/pan-wizard-core/templates/config.json +2 -6
- package/pan-wizard-core/templates/continue-here.md +1 -1
- package/pan-wizard-core/templates/debug-subagent-prompt.md +5 -3
- package/pan-wizard-core/templates/debug.md +15 -1
- package/pan-wizard-core/templates/milestone-archive.md +6 -6
- package/pan-wizard-core/templates/milestone.md +1 -1
- package/pan-wizard-core/templates/phase-prompt.md +31 -17
- package/pan-wizard-core/templates/planner-subagent-prompt.md +5 -5
- package/pan-wizard-core/templates/preview-report.md +1 -1
- package/pan-wizard-core/templates/research-project/architecture.md +3 -3
- package/pan-wizard-core/templates/research-project/features.md +1 -1
- package/pan-wizard-core/templates/research-project/pitfalls.md +1 -1
- package/pan-wizard-core/templates/research-project/stack.md +3 -3
- package/pan-wizard-core/templates/research.md +5 -5
- package/pan-wizard-core/templates/retrospective.md +1 -1
- package/pan-wizard-core/templates/roadmap.md +15 -15
- package/pan-wizard-core/templates/state.md +4 -4
- package/pan-wizard-core/templates/summary-complex.md +1 -0
- package/pan-wizard-core/templates/summary-minimal.md +1 -0
- package/pan-wizard-core/templates/summary-standard.md +2 -1
- package/pan-wizard-core/templates/summary.md +18 -2
- package/pan-wizard-core/templates/uat.md +3 -3
- package/pan-wizard-core/templates/user-setup.md +8 -8
- package/pan-wizard-core/templates/verification-report.md +53 -8
- package/pan-wizard-core/workflows/add-phase.md +1 -1
- package/pan-wizard-core/workflows/assumptions.md +1 -1
- package/pan-wizard-core/workflows/diagnose-issues.md +4 -4
- package/pan-wizard-core/workflows/discuss-phase.md +10 -8
- package/pan-wizard-core/workflows/exec-phase.md +75 -95
- package/pan-wizard-core/workflows/execute-plan.md +8 -7
- package/pan-wizard-core/workflows/health.md +13 -12
- package/pan-wizard-core/workflows/help.md +8 -9
- package/pan-wizard-core/workflows/learn.md +13 -11
- package/pan-wizard-core/workflows/map-codebase.md +10 -10
- package/pan-wizard-core/workflows/milestone-audit.md +10 -10
- package/pan-wizard-core/workflows/milestone-gaps.md +18 -25
- package/pan-wizard-core/workflows/milestone-new.md +7 -6
- package/pan-wizard-core/workflows/new-project.md +17 -25
- package/pan-wizard-core/workflows/optimize.md +11 -9
- package/pan-wizard-core/workflows/pause.md +6 -1
- package/pan-wizard-core/workflows/phase-tests.md +4 -4
- package/pan-wizard-core/workflows/plan-phase.md +53 -42
- package/pan-wizard-core/workflows/profile.md +2 -2
- package/pan-wizard-core/workflows/progress.md +6 -8
- package/pan-wizard-core/workflows/quick.md +7 -5
- package/pan-wizard-core/workflows/remove-phase.md +1 -1
- package/pan-wizard-core/workflows/research-phase.md +9 -5
- package/pan-wizard-core/workflows/resume-project.md +4 -6
- package/pan-wizard-core/workflows/retro.md +3 -1
- package/pan-wizard-core/workflows/settings.md +16 -24
- package/pan-wizard-core/workflows/todo-check.md +4 -3
- package/pan-wizard-core/workflows/transition.md +16 -12
- package/pan-wizard-core/workflows/update.md +25 -17
- package/pan-wizard-core/workflows/verify-phase.md +22 -20
- package/pan-zcode/KNOWN-BETA-RISKS.md +2 -2
- package/pan-zcode/README.md +7 -7
- package/pan-zcode/bin/install-zcode.js +1 -1
- package/scripts/build-hooks.js +1 -0
- package/scripts/build-plugin.js +3 -1
- package/scripts/generate-skills-docs.py +15 -6
- package/scripts/prompt-lint.cjs +167 -0
- package/scripts/test-surface.cjs +1 -0
|
@@ -55,6 +55,12 @@ const TOKENS_FILE = 'tokens.jsonl';
|
|
|
55
55
|
* write at `cache_write` (market-ideas queue M13). OpenAI and Google report no
|
|
56
56
|
* lifetime split, so their rows carry none.
|
|
57
57
|
*/
|
|
58
|
+
// The prompt size above which a long-prompt rate applies (Haiku 5.5's tier). The
|
|
59
|
+
// cost-logger hook and `cost rebuild` record the tokens of every request over it as a
|
|
60
|
+
// row's `long_prompt`, with this figure as its `above`; hooks/pan-cost-logger.js keeps
|
|
61
|
+
// its own copy (hooks require nothing from lib), and a test pins the two equal.
|
|
62
|
+
const LONG_PROMPT_THRESHOLD = 100000;
|
|
63
|
+
|
|
58
64
|
const DEFAULT_RATES = {
|
|
59
65
|
// Anthropic — platform.claude.com/docs/en/about-claude/pricing, every row read
|
|
60
66
|
// 2026-09-23. Opus 4.6+/Opus 5 are $5/$25 (the old $15/$75 Opus pricing ended with
|
|
@@ -91,16 +97,24 @@ const DEFAULT_RATES = {
|
|
|
91
97
|
// 2026-08-31 was made permanent and the scheduled rise to $3/$15 cancelled.
|
|
92
98
|
// Lesson: never write down a pre-announced price — this row carried the future
|
|
93
99
|
// rate for a month and over-billed by half.
|
|
94
|
-
// Sonnet 5.5 (claude-sonnet-5-5):
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
'claude-sonnet-5-5': { input: 2.0, output: 10.0, cache_read: 0.
|
|
100
|
+
// Sonnet 5.5 (claude-sonnet-5-5): $2/$10, 5-minute writes $2.50, 1-hour $4, and
|
|
101
|
+
// hits at 0.05x input, $0.10 (pricing page, read 2026-10-10; Claude Code 2.1.296
|
|
102
|
+
// corrected its own figure from $0.20 the same week). Claude Code 2.1.284 points the
|
|
103
|
+
// `sonnet` alias at it on the Anthropic API (model-config, read 2026-10-03). Its own
|
|
104
|
+
// row is what keeps it off Sonnet 5's $0.20 hits through the family prefix.
|
|
105
|
+
'claude-sonnet-5-5': { input: 2.0, output: 10.0, cache_read: 0.10, cache_write: 2.50, cache_write_1h: 4.0 },
|
|
100
106
|
'claude-sonnet-5': { input: 2.0, output: 10.0, cache_read: 0.20, cache_write: 2.50, cache_write_1h: 4.0 },
|
|
101
107
|
'claude-sonnet-4-6': { input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75, cache_write_1h: 6.0 },
|
|
102
108
|
// Sonnet 4.5 (dated id claude-sonnet-4-5-20250929): $3/$15/$0.30/$3.75 (R7).
|
|
103
109
|
'claude-sonnet-4-5': { input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75, cache_write_1h: 6.0 },
|
|
110
|
+
// Haiku 5.5 (claude-haiku-5-5): $0.10/$0.50, 5-minute writes $0.125, 1-hour $0.20,
|
|
111
|
+
// hits $0.01 — and every rate 5x for a request whose prompt is over 100,000 tokens,
|
|
112
|
+
// counting cache reads and writes (pricing page "Long context pricing", read
|
|
113
|
+
// 2026-10-10). Claude Code 2.1.293 makes it the `haiku` alias on the Anthropic API,
|
|
114
|
+
// which is PAN's fast tier there. `long` prices the part of a row that came from
|
|
115
|
+
// such requests (`long_prompt`, see computeCost).
|
|
116
|
+
'claude-haiku-5-5': { input: 0.10, output: 0.50, cache_read: 0.01, cache_write: 0.125, cache_write_1h: 0.20,
|
|
117
|
+
long: { above: LONG_PROMPT_THRESHOLD, input: 0.50, output: 2.50, cache_read: 0.05, cache_write: 0.625, cache_write_1h: 1.0 } },
|
|
104
118
|
// Haiku 4.5: API id claude-haiku-4-5-20251001, alias claude-haiku-4-5.
|
|
105
119
|
'claude-haiku-4-5': { input: 1.0, output: 5.0, cache_read: 0.1, cache_write: 1.25, cache_write_1h: 2.0 },
|
|
106
120
|
|
|
@@ -242,6 +256,7 @@ const scaleRate = (rate, m) => ({
|
|
|
242
256
|
cache_read: round6(rate.cache_read * m),
|
|
243
257
|
cache_write: round6(rate.cache_write * m),
|
|
244
258
|
...(typeof rate.cache_write_1h === 'number' ? { cache_write_1h: round6(rate.cache_write_1h * m) } : {}),
|
|
259
|
+
...(rate.long && typeof rate.long === 'object' ? { long: { above: rate.long.above, ...scaleRate(rate.long, m) } } : {}),
|
|
245
260
|
});
|
|
246
261
|
|
|
247
262
|
/**
|
|
@@ -388,12 +403,35 @@ function computeCost(rec, configRates) {
|
|
|
388
403
|
// bills at the one-hour rate; the rest — and every write on a row or rate without
|
|
389
404
|
// the split — at `cache_write`.
|
|
390
405
|
const write1h = Math.min(cacheWrite, Math.max(0, Number(rec.cache_write_1h_tokens) || 0));
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
406
|
+
const tokens = { input, output, cacheRead, cacheWrite, write1h };
|
|
407
|
+
// A long-prompt tier (Haiku 5.5): the row's `long_prompt` holds the tokens of the
|
|
408
|
+
// requests whose prompt was over the threshold, and those bill at the long rates.
|
|
409
|
+
// Measured at the same threshold only — a row split at another figure cannot be
|
|
410
|
+
// re-cut here, so it prices at the base rates as before.
|
|
411
|
+
const lp = rec.long_prompt;
|
|
412
|
+
let usd = 0;
|
|
413
|
+
if (rate.long && lp && typeof lp === 'object' && Number(lp.above) === Number(rate.long.above)) {
|
|
414
|
+
const part = {
|
|
415
|
+
input: Math.min(input, Number(lp.input_tokens) || 0),
|
|
416
|
+
output: Math.min(output, Number(lp.output_tokens) || 0),
|
|
417
|
+
cacheRead: Math.min(cacheRead, Number(lp.cache_read_tokens) || 0),
|
|
418
|
+
cacheWrite: Math.min(cacheWrite, Number(lp.cache_write_tokens) || 0),
|
|
419
|
+
};
|
|
420
|
+
part.write1h = Math.min(part.cacheWrite, write1h, Number(lp.cache_write_1h_tokens) || 0);
|
|
421
|
+
usd += priceTokens(part, rate.long);
|
|
422
|
+
for (const k of Object.keys(part)) tokens[k] -= part[k];
|
|
423
|
+
}
|
|
424
|
+
usd += priceTokens(tokens, rate);
|
|
394
425
|
return Math.round(usd * 10000) / 10000;
|
|
395
426
|
}
|
|
396
427
|
|
|
428
|
+
/** USD for disjoint token counts at one rate row (1-hour writes at their own rate). */
|
|
429
|
+
function priceTokens(t, rate) {
|
|
430
|
+
const rate1h = typeof rate.cache_write_1h === 'number' ? rate.cache_write_1h : rate.cache_write;
|
|
431
|
+
return (t.input * rate.input + t.output * rate.output + t.cacheRead * rate.cache_read
|
|
432
|
+
+ (t.cacheWrite - t.write1h) * rate.cache_write + t.write1h * rate1h) / 1_000_000;
|
|
433
|
+
}
|
|
434
|
+
|
|
397
435
|
/**
|
|
398
436
|
* Append a cost record. Non-blocking — errors are swallowed so instrumentation
|
|
399
437
|
* never breaks the caller.
|
|
@@ -532,7 +570,11 @@ function aggregate(cwd, opts) {
|
|
|
532
570
|
const records = readRecords(cwd);
|
|
533
571
|
const malformedSkipped = _lastReadMalformed; // captured before any later read
|
|
534
572
|
const since = opts?.since ? new Date(opts.since).getTime() : null;
|
|
535
|
-
|
|
573
|
+
// A bare date is that day's first instant, so as an upper bound it left out the
|
|
574
|
+
// whole day the docs call inclusive: read it as the day's last millisecond.
|
|
575
|
+
const until = opts?.until
|
|
576
|
+
? new Date(/^\d{4}-\d{2}-\d{2}$/.test(String(opts.until)) ? `${opts.until}T23:59:59.999Z` : opts.until).getTime()
|
|
577
|
+
: null;
|
|
536
578
|
const config = loadConfig(cwd);
|
|
537
579
|
const configRates = effectiveRates(config);
|
|
538
580
|
|
|
@@ -699,10 +741,12 @@ function renderChart(agg) {
|
|
|
699
741
|
function cmdCostReport(cwd, opts, raw) {
|
|
700
742
|
const format = opts?.format || 'json';
|
|
701
743
|
const agg = aggregate(cwd, opts);
|
|
744
|
+
// `--format table|chart` asks for text, so it prints text with or without
|
|
745
|
+
// `--raw`; without `--raw` it used to print the JSON report unchanged.
|
|
702
746
|
if (format === 'table') {
|
|
703
|
-
output(agg,
|
|
747
|
+
output(agg, true, renderTable(agg));
|
|
704
748
|
} else if (format === 'chart') {
|
|
705
|
-
output(agg,
|
|
749
|
+
output(agg, true, renderChart(agg));
|
|
706
750
|
} else {
|
|
707
751
|
output(agg, raw);
|
|
708
752
|
}
|
|
@@ -722,6 +766,86 @@ function cmdCostClear(cwd, raw) {
|
|
|
722
766
|
}
|
|
723
767
|
}
|
|
724
768
|
|
|
769
|
+
// ─── Host usage limits (Claude Code subscriptions) ───────────────────────────
|
|
770
|
+
//
|
|
771
|
+
// Claude Code's status-line payload carries `rate_limits.five_hour` and
|
|
772
|
+
// `rate_limits.seven_day` (`used_percentage`, `resets_at` in epoch seconds;
|
|
773
|
+
// code.claude.com/docs/en/statusline, read 2026-10-10). On a subscription those
|
|
774
|
+
// windows, not dollars, are what a long campaign runs out of. PAN's status line
|
|
775
|
+
// writes them, account-wide, to claude-limits.json in the per-user pan-hooks
|
|
776
|
+
// directory; campaigns read them here. Headless `claude -p` runs no status line, so
|
|
777
|
+
// there the file goes stale and nothing stops on it.
|
|
778
|
+
|
|
779
|
+
const HOST_LIMITS_FILE = 'claude-limits.json';
|
|
780
|
+
const HOST_LIMITS_FRESH_SECONDS = 15 * 60;
|
|
781
|
+
const DEFAULT_WEEKLY_LIMIT_STOP_PCT = 90;
|
|
782
|
+
|
|
783
|
+
/** The per-user pan-hooks directory when it is safe to read (ours, not a symlink, owner-only), else null. */
|
|
784
|
+
function hooksDirIfSafe(tmpDir) {
|
|
785
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : (process.env.USERNAME || 'win');
|
|
786
|
+
const dir = path.join(tmpDir || require('os').tmpdir(), `pan-hooks-${uid}`);
|
|
787
|
+
try {
|
|
788
|
+
const st = fs.lstatSync(dir);
|
|
789
|
+
if (st.isSymbolicLink() || !st.isDirectory()) return null;
|
|
790
|
+
if (typeof process.getuid === 'function' && (st.uid !== process.getuid() || (st.mode & 0o077) !== 0)) return null;
|
|
791
|
+
return dir;
|
|
792
|
+
} catch { return null; }
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* The host's usage-limit windows as the status line last saw them, or null when no
|
|
797
|
+
* status line has written them. A window whose `resets_at` has passed reads as reset
|
|
798
|
+
* (used 0); the whole reading is `stale` past HOST_LIMITS_FRESH_SECONDS.
|
|
799
|
+
* @param {{tmpDir?: string, now?: number}} [opts] - now in epoch seconds
|
|
800
|
+
*/
|
|
801
|
+
function readHostLimits(opts = {}) {
|
|
802
|
+
const dir = hooksDirIfSafe(opts.tmpDir);
|
|
803
|
+
if (!dir) return null;
|
|
804
|
+
let raw;
|
|
805
|
+
try { raw = JSON.parse(fs.readFileSync(path.join(dir, HOST_LIMITS_FILE), 'utf8')); } catch { return null; }
|
|
806
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
807
|
+
const now = Number.isFinite(opts.now) ? opts.now : Math.floor(Date.now() / 1000);
|
|
808
|
+
const win = (w) => {
|
|
809
|
+
if (!w || typeof w !== 'object' || !Number.isFinite(Number(w.used_percentage))) return null;
|
|
810
|
+
const resetsAt = Number.isFinite(Number(w.resets_at)) ? Number(w.resets_at) : null;
|
|
811
|
+
const reset = resetsAt !== null && resetsAt <= now;
|
|
812
|
+
return { used_pct: reset ? 0 : Math.max(0, Math.min(100, Number(w.used_percentage))), resets_at: resetsAt, reset_since_read: reset };
|
|
813
|
+
};
|
|
814
|
+
const age = Number.isFinite(Number(raw.timestamp)) ? now - Number(raw.timestamp) : null;
|
|
815
|
+
return {
|
|
816
|
+
five_hour: win(raw.five_hour),
|
|
817
|
+
seven_day: win(raw.seven_day),
|
|
818
|
+
age_seconds: age,
|
|
819
|
+
stale: age === null || age > HOST_LIMITS_FRESH_SECONDS,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Whether a campaign should stop on the 7-day window: a fresh reading at or past
|
|
825
|
+
* `cost.weekly_limit_stop_pct` (default 90; 0 or false turns it off).
|
|
826
|
+
*/
|
|
827
|
+
function weeklyLimitStop(cwd, limits = readHostLimits()) {
|
|
828
|
+
let pct = DEFAULT_WEEKLY_LIMIT_STOP_PCT;
|
|
829
|
+
try {
|
|
830
|
+
const v = loadConfig(cwd)?.cost?.weekly_limit_stop_pct;
|
|
831
|
+
if (v === false || v === 0) pct = 0;
|
|
832
|
+
else if (Number.isFinite(Number(v)) && Number(v) > 0 && Number(v) <= 100) pct = Number(v);
|
|
833
|
+
} catch { /* defaults */ }
|
|
834
|
+
const used = limits && !limits.stale && limits.seven_day ? limits.seven_day.used_pct : null;
|
|
835
|
+
return { stop: pct > 0 && used !== null && used >= pct, threshold_pct: pct, seven_day_used_pct: used };
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function cmdCostLimits(cwd, raw) {
|
|
839
|
+
const limits = readHostLimits();
|
|
840
|
+
const stop = weeklyLimitStop(cwd, limits);
|
|
841
|
+
output({
|
|
842
|
+
available: !!limits,
|
|
843
|
+
...(limits || { five_hour: null, seven_day: null, age_seconds: null, stale: true }),
|
|
844
|
+
weekly_limit_stop_pct: stop.threshold_pct,
|
|
845
|
+
weekly_limit_reached: stop.stop,
|
|
846
|
+
}, raw, stop.stop ? 'weekly_limit_reached' : (limits ? 'ok' : 'no_reading'));
|
|
847
|
+
}
|
|
848
|
+
|
|
725
849
|
// ─── Rate-table staleness ───────────────────────────────────────────────────
|
|
726
850
|
|
|
727
851
|
// Date DEFAULT_RATES was last verified against published provider pricing — ALL
|
|
@@ -738,7 +862,7 @@ function cmdCostClear(cwd, raw) {
|
|
|
738
862
|
// a default-model change inside the window — that is the job of the
|
|
739
863
|
// documented-default-ids fixture test (tests/fixtures/documented-default-models.json,
|
|
740
864
|
// R31), which fails the suite the day a documented default id has no exact row.
|
|
741
|
-
const RATES_VERIFIED_AT = '2026-
|
|
865
|
+
const RATES_VERIFIED_AT = '2026-10-10';
|
|
742
866
|
const RATES_STALE_AFTER_DAYS = 60;
|
|
743
867
|
const RATE_TIERS = ['reasoning', 'mid', 'fast'];
|
|
744
868
|
|
|
@@ -784,6 +908,11 @@ module.exports = {
|
|
|
784
908
|
renderTable,
|
|
785
909
|
renderChart,
|
|
786
910
|
resolveRate,
|
|
911
|
+
LONG_PROMPT_THRESHOLD,
|
|
912
|
+
readHostLimits,
|
|
913
|
+
weeklyLimitStop,
|
|
914
|
+
cmdCostLimits,
|
|
915
|
+
HOST_LIMITS_FILE,
|
|
787
916
|
ratesFromModelPricing,
|
|
788
917
|
managedSettingsDir,
|
|
789
918
|
loadManagedModelPricing,
|
|
@@ -21,8 +21,10 @@ const SCANNABLE_EXTS = ['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.py'];
|
|
|
21
21
|
|
|
22
22
|
// ─── Pass 1: Deterministic static analysis ───────────────────────────────────
|
|
23
23
|
|
|
24
|
-
// Calls
|
|
25
|
-
// try/catch wrapping only such a call is
|
|
24
|
+
// Calls the matcher treats as non-throwing: none of them throws on a string or a
|
|
25
|
+
// number, so a try/catch wrapping only such a call is usually phantom. Number, String,
|
|
26
|
+
// parseInt and parseFloat do throw on an object whose toString throws (all but String
|
|
27
|
+
// on a Symbol too), which is why findPhantomTryCatch tiers it review_required.
|
|
26
28
|
// Deliberately EXCLUDES throwing calls — JSON.parse (SyntaxError on bad input),
|
|
27
29
|
// JSON.stringify (TypeError on circular refs / BigInt), new RegExp, fs.*Sync,
|
|
28
30
|
// etc. — because their try/catch is load-bearing and must not be flagged for
|
|
@@ -431,7 +433,7 @@ function writePatternsMemory(cwd, findings, opts) {
|
|
|
431
433
|
const lines = [
|
|
432
434
|
'---',
|
|
433
435
|
'name: distill-patterns',
|
|
434
|
-
'description: Cross-session memory of AI-bloat patterns detected
|
|
436
|
+
'description: Cross-session memory of AI-bloat patterns detected (it records detections, not fixes)',
|
|
435
437
|
'type: project',
|
|
436
438
|
'---',
|
|
437
439
|
'',
|
|
@@ -520,6 +522,7 @@ module.exports = {
|
|
|
520
522
|
findUnreferencedExports,
|
|
521
523
|
SAFETY_TIERS,
|
|
522
524
|
DEFAULT_BLOAT_THRESHOLD,
|
|
525
|
+
PATTERNS_FILE,
|
|
523
526
|
MAX_FUNCTION_LOC,
|
|
524
527
|
MAX_PARAM_COUNT,
|
|
525
528
|
MAX_NESTING_DEPTH,
|
|
@@ -44,11 +44,21 @@ const DEFAULT_SCHEMA_PATH = path.join(
|
|
|
44
44
|
* calls it must pass its verdict as output()'s 4th argument; a bare
|
|
45
45
|
* process.exit() after an output() call is unreachable.
|
|
46
46
|
*/
|
|
47
|
+
/**
|
|
48
|
+
* A missing path or a file where a directory is expected ends in error(), not in the
|
|
49
|
+
* stack trace walkMarkdownFiles' throw used to print (`doc-lint counts README.md`).
|
|
50
|
+
* @param {string} targetDir
|
|
51
|
+
*/
|
|
52
|
+
function requireDirectory(targetDir) {
|
|
53
|
+
let stat = null;
|
|
54
|
+
try { stat = fs.statSync(targetDir); } catch { /* absent */ }
|
|
55
|
+
if (!stat) error(`directory not found: ${targetDir}`);
|
|
56
|
+
if (!stat.isDirectory()) error(`not a directory: ${targetDir} — doc-lint scans a directory of markdown files`);
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
function cmdDocLint(cwd, dir, opts = {}) {
|
|
48
60
|
const targetDir = path.isAbsolute(dir) ? dir : path.join(cwd, dir);
|
|
49
|
-
|
|
50
|
-
error(`directory not found: ${targetDir}`);
|
|
51
|
-
}
|
|
61
|
+
requireDirectory(targetDir);
|
|
52
62
|
|
|
53
63
|
const schemaPath = opts.schema
|
|
54
64
|
? (path.isAbsolute(opts.schema) ? opts.schema : path.join(cwd, opts.schema))
|
|
@@ -209,7 +219,6 @@ function isCountAllowed(relativePath) {
|
|
|
209
219
|
// Things that LOOK like counts but are stable identities (allowed everywhere):
|
|
210
220
|
const STABLE_IDENTITIES = [
|
|
211
221
|
/\b5\s+(target\s+)?runtimes\b/i, // 5 target runtimes
|
|
212
|
-
/\b5\s+hooks\b/i, // 5 hooks (named individually)
|
|
213
222
|
/\bLAYER\s+\d+\b/, // architecture layer labels
|
|
214
223
|
/\b5\s+(parallel\s+)?(researchers?|research\s+)/i, // 5 parallel researchers
|
|
215
224
|
/\b6\s+(parallel\s+)?agents\b/i, // 6 parallel agents (codebase mapper)
|
|
@@ -218,9 +227,17 @@ const STABLE_IDENTITIES = [
|
|
|
218
227
|
/\bthree\s+phases\b|\bfour\s+phases\b/i, // generic phase counts in narrative
|
|
219
228
|
];
|
|
220
229
|
|
|
221
|
-
function isStableIdentity(matchText, surrounding) {
|
|
230
|
+
function isStableIdentity(matchText, surrounding, matchIndex = 0) {
|
|
222
231
|
for (const re of STABLE_IDENTITIES) {
|
|
223
|
-
if (re.test(matchText)
|
|
232
|
+
if (re.test(matchText)) return true;
|
|
233
|
+
// Only an identity that overlaps the count excuses it ("LAYER 6 modules"). One
|
|
234
|
+
// elsewhere on the line excused every count there: "52 commands on all 5 runtimes".
|
|
235
|
+
const all = new RegExp(re.source, re.flags.includes('g') ? re.flags : `${re.flags}g`);
|
|
236
|
+
let s;
|
|
237
|
+
while ((s = all.exec(surrounding)) !== null) {
|
|
238
|
+
if (s.index < matchIndex + matchText.length && matchIndex < s.index + s[0].length) return true;
|
|
239
|
+
if (s[0].length === 0) all.lastIndex++;
|
|
240
|
+
}
|
|
224
241
|
}
|
|
225
242
|
return false;
|
|
226
243
|
}
|
|
@@ -233,9 +250,7 @@ function isStableIdentity(matchText, surrounding) {
|
|
|
233
250
|
*/
|
|
234
251
|
function cmdDocLintCounts(cwd, dir, opts = {}) {
|
|
235
252
|
const targetDir = path.isAbsolute(dir) ? dir : path.join(cwd, dir);
|
|
236
|
-
|
|
237
|
-
error(`directory not found: ${targetDir}`);
|
|
238
|
-
}
|
|
253
|
+
requireDirectory(targetDir);
|
|
239
254
|
|
|
240
255
|
const exclude = opts.exclude || [];
|
|
241
256
|
const files = walkMarkdownFiles(targetDir, { exclude });
|
|
@@ -260,8 +275,8 @@ function cmdDocLintCounts(cwd, dir, opts = {}) {
|
|
|
260
275
|
re.lastIndex = 0; // reset for /g
|
|
261
276
|
let m;
|
|
262
277
|
while ((m = re.exec(line)) !== null) {
|
|
263
|
-
// Skip
|
|
264
|
-
if (isStableIdentity(m[0], line)) continue;
|
|
278
|
+
// Skip a count that is part of a stable identity
|
|
279
|
+
if (isStableIdentity(m[0], line, m.index)) continue;
|
|
265
280
|
violations.push({
|
|
266
281
|
file: file.relativePath,
|
|
267
282
|
line: i + 1,
|
|
@@ -363,6 +378,10 @@ function scanDocFlags(cwd, opts = {}) {
|
|
|
363
378
|
}
|
|
364
379
|
|
|
365
380
|
function cmdDocLintFlags(cwd, opts = {}, raw) {
|
|
381
|
+
for (const d of opts.docDirs || []) {
|
|
382
|
+
const abs = path.isAbsolute(d) ? d : path.join(cwd, d);
|
|
383
|
+
if (fs.existsSync(abs)) requireDirectory(abs);
|
|
384
|
+
}
|
|
366
385
|
const r = scanDocFlags(cwd, opts);
|
|
367
386
|
if (raw) {
|
|
368
387
|
if (r.violation_count === 0) {
|
|
@@ -16,11 +16,14 @@
|
|
|
16
16
|
* again (a regression);
|
|
17
17
|
* - with no disposition, a finding is `open`.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
* -
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* Rules applied when a verdict is recorded:
|
|
20
|
+
* - EVIDENCE CLOSES, SILENCE DOES NOT (V6): an earlier open finding closes only when
|
|
21
|
+
* the judge answers it `resolved` with evidence (a `resolutions` entry, or the
|
|
22
|
+
* verifier's `re_verification.gaps_closed`), or, for a `code-fact:<fact>` finding,
|
|
23
|
+
* when that fact passes. One the new verdict neither reports nor answers stays
|
|
24
|
+
* open and is listed as `unaddressed`. `still_present` counts as a re-report.
|
|
25
|
+
* - IDENTITY: a reworded report of an earlier finding (same class family, same
|
|
26
|
+
* file, same truth or a similar summary) keeps that finding's id.
|
|
24
27
|
* - IDEMPOTENCY: recording the same artifact twice is a no-op that returns the
|
|
25
28
|
* first verdict (record_sig). That is why exec-phase and verify-phase may both
|
|
26
29
|
* record one verification.md.
|
|
@@ -122,6 +125,69 @@ function samePhase(a, b) {
|
|
|
122
125
|
return V.comparablePhase(a) === V.comparablePhase(b);
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
// ─── Finding identity across reports (V6) ───────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/** The file a `where` names, without a line or column suffix. */
|
|
131
|
+
function fileOf(where) {
|
|
132
|
+
return where ? String(where).replace(/\\/g, '/').replace(/(:\d+){1,2}$/, '').replace(/#L?\d+.*$/, '') : '';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function tokens(s) {
|
|
136
|
+
return new Set(String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').split(' ').filter((w) => w.length > 2));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Token overlap (Jaccard) of two summaries at or above one half. */
|
|
140
|
+
function similar(a, b) {
|
|
141
|
+
const A = tokens(a);
|
|
142
|
+
const B = tokens(b);
|
|
143
|
+
if (!A.size || !B.size) return false;
|
|
144
|
+
let both = 0;
|
|
145
|
+
for (const t of A) if (B.has(t)) both++;
|
|
146
|
+
return both / (A.size + B.size - both) >= 0.5;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The part of a summary before " — " (a verifier gap's truth), lower-cased. */
|
|
150
|
+
const headOf = (s) => String(s || '').split(' — ')[0].trim().toLowerCase();
|
|
151
|
+
/** A verifier gap moves between missing and partial as it is worked on: one family. */
|
|
152
|
+
const familyOf = (c) => (c === 'partial' ? 'missing' : c);
|
|
153
|
+
|
|
154
|
+
/** Same class family, same file, and the same truth or a similar summary. */
|
|
155
|
+
function sameFinding(e, f) {
|
|
156
|
+
return familyOf(e.class) === familyOf(f.class) && fileOf(e.where) === fileOf(f.where)
|
|
157
|
+
&& (headOf(e.summary) === headOf(f.summary) || similar(e.summary, f.summary));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The earlier finding a judge's resolution answers: by id, else by its truth or summary. */
|
|
161
|
+
function resolutionTarget(mine, r) {
|
|
162
|
+
if (r.id) return mine.find((f) => f.id === r.id) || null;
|
|
163
|
+
const head = headOf(r.summary);
|
|
164
|
+
return mine.find((f) => headOf(f.summary) === head) || mine.find((f) => similar(f.summary, r.summary)) || null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function truncateReason(s) {
|
|
168
|
+
return String(s).replace(/\s+/g, ' ').trim().slice(0, REASON_MAX);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* How stable a judge's findings are across its reports on one phase (V6): between
|
|
173
|
+
* consecutive verdicts, how many ids appeared and how many disappeared. A judge whose
|
|
174
|
+
* findings churn while the code does not change is not measuring the code.
|
|
175
|
+
*/
|
|
176
|
+
function findingsChurn(rows, phase, agent) {
|
|
177
|
+
const seq = foldFindings(rows).verdicts.filter((v) => v.agent === agent && samePhase(v.phase, phase));
|
|
178
|
+
let added = 0;
|
|
179
|
+
let dropped = 0;
|
|
180
|
+
let union = 0;
|
|
181
|
+
for (let i = 1; i < seq.length; i++) {
|
|
182
|
+
const a = new Set(seq[i - 1].finding_ids || []);
|
|
183
|
+
const b = new Set(seq[i].finding_ids || []);
|
|
184
|
+
for (const id of b) if (!a.has(id)) added++;
|
|
185
|
+
for (const id of a) if (!b.has(id)) dropped++;
|
|
186
|
+
union += new Set([...a, ...b]).size;
|
|
187
|
+
}
|
|
188
|
+
return { reports: seq.length, transitions: Math.max(0, seq.length - 1), added, dropped, rate: union ? Math.round(((added + dropped) / union) * 1000) / 1000 : 0 };
|
|
189
|
+
}
|
|
190
|
+
|
|
125
191
|
/** `.planning/` must exist: the ledger fills a planning tree, it never creates one. */
|
|
126
192
|
function requirePlanningTree(cwd) {
|
|
127
193
|
try { return fs.statSync(planningPath(cwd)).isDirectory(); } catch { return false; }
|
|
@@ -163,11 +229,15 @@ function recordVerdict(cwd, input) {
|
|
|
163
229
|
// `not_checked` (M23): what the judge could not run. A pass with entries here is a
|
|
164
230
|
// pass over less than the whole phase, and the record says so.
|
|
165
231
|
const notChecked = verdict.not_checked || [];
|
|
166
|
-
|
|
232
|
+
// Two-key verdict (V9): when a code fact lowered the judge's word, the record keeps
|
|
233
|
+
// both, and `verdict` is the lowered word the workflow branches on.
|
|
234
|
+
const lowered = Array.isArray(parsed.lowered_by) && parsed.lowered_by.length
|
|
235
|
+
? { agent_verdict: verdict.agent_verdict, lowered_by: parsed.lowered_by } : {};
|
|
236
|
+
const base = { contract: V.VERDICT_CONTRACT, agent, verdict: verdict.verdict, outcome: verdict.outcome, ...lowered, not_checked: notChecked, source_kind: parsed.source_kind, warnings: parsed.warnings };
|
|
167
237
|
|
|
168
238
|
const dup = fold.verdicts.find((r) => r.record_sig === recordSig && r.agent === agent && samePhase(r.phase, phase));
|
|
169
239
|
if (dup) {
|
|
170
|
-
return { ...base, recorded: false, duplicate: true, verdict_id: dup.verdict_id, attempt: dup.attempt, phase, findings: (dup.finding_ids || []).length, new: 0,
|
|
240
|
+
return { ...base, recorded: false, duplicate: true, verdict_id: dup.verdict_id, attempt: dup.attempt, phase, findings: (dup.finding_ids || []).length, new: 0, rematched: 0, resolved: 0, unaddressed: [], reopened: 0 };
|
|
171
241
|
}
|
|
172
242
|
|
|
173
243
|
const prior = fold.verdicts.filter((r) => r.agent === agent && samePhase(r.phase, phase));
|
|
@@ -180,21 +250,52 @@ function recordVerdict(cwd, input) {
|
|
|
180
250
|
const ids = [];
|
|
181
251
|
const newRows = [];
|
|
182
252
|
const findingRows = [];
|
|
253
|
+
const mine = [...fold.findings.values()].filter((e) => e.agent === agent && samePhase(e.phase, phase));
|
|
254
|
+
let rematched = 0;
|
|
183
255
|
for (const f of verdict.findings) {
|
|
184
|
-
|
|
256
|
+
let id = V.findingId(phase, agent, f);
|
|
257
|
+
if (!fold.findings.has(id)) {
|
|
258
|
+
// A reworded report of an earlier finding keeps that finding's id (V6), so a
|
|
259
|
+
// rewording is a re-report, not a fix plus a new finding.
|
|
260
|
+
const earlier = mine.filter((e) => !ids.includes(e.id) && sameFinding(e, f)).sort((a, b) => (a.status === 'open' ? 0 : 1) - (b.status === 'open' ? 0 : 1))[0];
|
|
261
|
+
if (earlier) { id = earlier.id; rematched++; }
|
|
262
|
+
}
|
|
185
263
|
if (ids.includes(id)) continue; // the same finding twice in one report is one finding
|
|
186
264
|
ids.push(id);
|
|
187
265
|
if (!fold.findings.has(id)) {
|
|
188
266
|
findingRows.push({ v: LEDGER_V, kind: 'finding', ts, id, verdict_id: verdictId, phase, milestone, agent, class: f.class, severity: f.severity, where: f.where, summary: f.summary });
|
|
189
267
|
}
|
|
190
268
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
269
|
+
|
|
270
|
+
// Findings close only on evidence (V6). Silence is not evidence: an earlier open
|
|
271
|
+
// finding the new verdict neither reports nor answers stays open, and the record
|
|
272
|
+
// lists it as unaddressed. What closes one: the judge's `resolved` answer with its
|
|
273
|
+
// evidence, or, for a code-fact finding, the fact passing.
|
|
274
|
+
const resolveRows = [];
|
|
275
|
+
const closing = new Set();
|
|
276
|
+
const close = (f, reason) => {
|
|
277
|
+
if (closing.has(f.id) || ids.includes(f.id) || f.status !== 'open') return;
|
|
278
|
+
closing.add(f.id);
|
|
279
|
+
resolveRows.push({ v: LEDGER_V, kind: 'disposition', ts, id: f.id, disposition: 'fixed', reason: truncateReason(reason), auto: true, verdict_id: verdictId });
|
|
280
|
+
};
|
|
281
|
+
let contradicted = 0;
|
|
282
|
+
for (const r of verdict.resolutions || []) {
|
|
283
|
+
const target = resolutionTarget(mine, r);
|
|
284
|
+
if (!target) { parsed.warnings.push(`resolution_unmatched: ${r.id || r.summary}`); continue; }
|
|
285
|
+
if (r.status === 'resolved') {
|
|
286
|
+
if (ids.includes(target.id)) { contradicted++; continue; }
|
|
287
|
+
close(target, `resolved by ${agent} at attempt ${attempt}: ${r.evidence}`);
|
|
288
|
+
} else if (r.status === 'still_present' && !ids.includes(target.id)) {
|
|
289
|
+
ids.push(target.id); // a re-report: reopens a fixed finding, keeps an open one open
|
|
290
|
+
}
|
|
197
291
|
}
|
|
292
|
+
if (contradicted) parsed.warnings.push(`resolution_contradicted: ${contradicted} finding(s) answered resolved but reported again; they stay open`);
|
|
293
|
+
for (const fact of parsed.facts_ok || []) {
|
|
294
|
+
for (const f of mine) if (f.where === `code-fact:${fact}`) close(f, `${fact} passed at attempt ${attempt}`);
|
|
295
|
+
}
|
|
296
|
+
const reopened = ids.filter((id) => fold.findings.has(id) && fold.findings.get(id).status === 'fixed').length;
|
|
297
|
+
const unaddressed = mine.filter((f) => f.status === 'open' && !ids.includes(f.id) && !closing.has(f.id)).map((f) => f.id);
|
|
298
|
+
if (unaddressed.length) parsed.warnings.push(`open_findings_unaddressed: ${unaddressed.length} earlier finding(s) neither reported nor resolved; they stay open`);
|
|
198
299
|
|
|
199
300
|
const bySeverity = countBy(verdict.findings, 'severity');
|
|
200
301
|
const byClass = countBy(verdict.findings, 'class');
|
|
@@ -205,11 +306,14 @@ function recordVerdict(cwd, input) {
|
|
|
205
306
|
}
|
|
206
307
|
newRows.push({
|
|
207
308
|
v: LEDGER_V, kind: 'verdict', ts, verdict_id: verdictId, record_sig: recordSig, phase, milestone, agent,
|
|
208
|
-
verdict: verdict.verdict, outcome: verdict.outcome, attempt, source, finding_ids: ids,
|
|
309
|
+
verdict: verdict.verdict, outcome: verdict.outcome, ...lowered, attempt, source, finding_ids: ids,
|
|
209
310
|
by_severity: bySeverity, by_class: byClass, warnings: parsed.warnings,
|
|
210
311
|
...(notChecked.length ? { not_checked: notChecked } : {}),
|
|
312
|
+
// A plan check is bound to the plans it checked (V7): exec-phase compares this
|
|
313
|
+
// hash with the plans it is about to run.
|
|
314
|
+
...(agent === 'pan-plan-checker' ? { plans_sha: require('./verify-plan-set.cjs').plansShaFor(cwd, phase) } : {}),
|
|
211
315
|
});
|
|
212
|
-
appendRows(cwd, [...findingRows, ...newRows, ...
|
|
316
|
+
appendRows(cwd, [...findingRows, ...newRows, ...resolveRows]);
|
|
213
317
|
|
|
214
318
|
const category = { pass: 'verdict_passed', fail: 'verdict_failed', needs_human: 'verdict_needs_human' }[verdict.outcome];
|
|
215
319
|
const serious = (bySeverity.critical || 0) + (bySeverity.high || 0) > 0;
|
|
@@ -228,13 +332,13 @@ function recordVerdict(cwd, input) {
|
|
|
228
332
|
category: 'verdict_retry',
|
|
229
333
|
agent,
|
|
230
334
|
phase,
|
|
231
|
-
description: `${agent} judged phase ${phase} again (attempt ${attempt}): ${previous.outcome} → ${verdict.outcome}, ${
|
|
335
|
+
description: `${agent} judged phase ${phase} again (attempt ${attempt}): ${previous.outcome} → ${verdict.outcome}, ${resolveRows.length} earlier finding(s) resolved, ${unaddressed.length} unaddressed`,
|
|
232
336
|
impact: 'minor',
|
|
233
|
-
context: { agent, phase, attempt, previous_outcome: previous.outcome, outcome: verdict.outcome,
|
|
337
|
+
context: { agent, phase, attempt, previous_outcome: previous.outcome, outcome: verdict.outcome, resolved: resolveRows.length, unaddressed: unaddressed.length, reopened, verdict_id: verdictId },
|
|
234
338
|
});
|
|
235
339
|
}
|
|
236
340
|
|
|
237
|
-
return { ...base, recorded: true, duplicate: false, verdict_id: verdictId, attempt, phase, findings: ids.length, new: findingRows.length,
|
|
341
|
+
return { ...base, recorded: true, duplicate: false, verdict_id: verdictId, attempt, phase, findings: ids.length, new: findingRows.length, rematched, resolved: resolveRows.length, unaddressed, reopened, ledger: planningRel(FINDINGS_FILE) };
|
|
238
342
|
}
|
|
239
343
|
|
|
240
344
|
/**
|
|
@@ -276,6 +380,13 @@ function disposeFindings(cwd, input) {
|
|
|
276
380
|
return { contract: V.VERDICT_CONTRACT, disposition: as, disposed, skipped };
|
|
277
381
|
}
|
|
278
382
|
|
|
383
|
+
/**
|
|
384
|
+
* Milestone versions compare with or without the leading `v`: the ledger stores
|
|
385
|
+
* `getMilestoneInfo`'s `v1.0`, while milestone-audit names its version `1.0` (its
|
|
386
|
+
* report is `v{version}-milestone-audit.md`), so `--milestone 1.0` found no debt.
|
|
387
|
+
*/
|
|
388
|
+
const sameMilestone = (a, b) => String(a || '').trim().replace(/^v/i, '') === String(b || '').trim().replace(/^v/i, '');
|
|
389
|
+
|
|
279
390
|
/** Findings with their folded status, filtered. */
|
|
280
391
|
function listFindings(cwd, filters = {}) {
|
|
281
392
|
const { rows, malformed } = readLedger(cwd);
|
|
@@ -285,13 +396,14 @@ function listFindings(cwd, filters = {}) {
|
|
|
285
396
|
if (filters.agent) list = list.filter((f) => f.agent === filters.agent);
|
|
286
397
|
if (filters.status) list = list.filter((f) => f.status === filters.status);
|
|
287
398
|
if (filters.class) list = list.filter((f) => f.class === filters.class);
|
|
288
|
-
if (filters.milestone) list = list.filter((f) => f.milestone
|
|
399
|
+
if (filters.milestone) list = list.filter((f) => sameMilestone(f.milestone, filters.milestone));
|
|
289
400
|
const findings = list.map(publicFinding);
|
|
290
401
|
return {
|
|
291
402
|
contract: V.VERDICT_CONTRACT,
|
|
292
403
|
findings,
|
|
293
404
|
counts: { total: findings.length, by_status: countBy(findings, 'status'), by_class: countBy(findings, 'class'), by_severity: countBy(findings, 'severity') },
|
|
294
405
|
verdicts: fold.verdicts.length,
|
|
406
|
+
...(filters.phase && filters.agent ? { churn: findingsChurn(rows, filters.phase, filters.agent) } : {}),
|
|
295
407
|
malformed_rows: malformed,
|
|
296
408
|
};
|
|
297
409
|
}
|
|
@@ -306,7 +418,7 @@ function findingsDebt(cwd, opts = {}) {
|
|
|
306
418
|
const fold = foldFindings(rows);
|
|
307
419
|
const byPhase = new Map();
|
|
308
420
|
for (const f of fold.findings.values()) {
|
|
309
|
-
if (milestone && f.milestone
|
|
421
|
+
if (milestone && !sameMilestone(f.milestone, milestone)) continue;
|
|
310
422
|
if (f.status !== 'deferred' && f.status !== 'open') continue;
|
|
311
423
|
const key = V.comparablePhase(f.phase);
|
|
312
424
|
if (!byPhase.has(key)) byPhase.set(key, { phase: f.phase, deferred: [], open: [] });
|
|
@@ -377,6 +489,8 @@ module.exports = {
|
|
|
377
489
|
disposeFindings,
|
|
378
490
|
listFindings,
|
|
379
491
|
findingsDebt,
|
|
492
|
+
findingsChurn,
|
|
493
|
+
sameFinding,
|
|
380
494
|
findingIdsIn,
|
|
381
495
|
cmdFindingsRecord,
|
|
382
496
|
cmdFindingsDispose,
|
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
const { extractFrontmatter, extractPriorityEffort } = require('./frontmatter.cjs');
|
|
23
23
|
const { enumerateRoadmapPhases } = require('./roadmap.cjs');
|
|
24
24
|
const { readErrorPatterns } = require('./commands.cjs');
|
|
25
|
+
const { weeklyLimitStop } = require('./cost.cjs');
|
|
25
26
|
const { planningPath, listPhaseDirs, classifyPhaseStatus, filterPlanFiles, filterSummaryFiles, planningRel } = require('./utils.cjs');
|
|
26
27
|
|
|
27
28
|
// ─── Scan helpers ───────────────────────────────────────────────────────────
|
|
@@ -302,10 +303,13 @@ function allocateBudget(items, budget, mode) {
|
|
|
302
303
|
} else if (mode === 'features') {
|
|
303
304
|
const { picked: mPicked, used: mUsed } = allocatePass(items, budget, { maxPriority: 0, minPriority: 0, track: 'mandatory' });
|
|
304
305
|
addToBatch(mPicked);
|
|
305
|
-
|
|
306
|
+
// The feature/stability split shares what the P0 pass left: splitting the
|
|
307
|
+
// whole budget again let a batch reach nearly twice the budget.
|
|
308
|
+
const rest = budget - mUsed;
|
|
309
|
+
const featureBudget = Math.floor(rest * FEATURE_RATIO);
|
|
306
310
|
const { picked: fPicked, used: fUsed } = allocatePass(items, featureBudget, { minPriority: 3, maxPriority: 5, track: 'feature', exclude: batchIds });
|
|
307
311
|
addToBatch(fPicked);
|
|
308
|
-
const { picked: sPicked, used: sUsed } = allocatePass(items,
|
|
312
|
+
const { picked: sPicked, used: sUsed } = allocatePass(items, rest - featureBudget, { minPriority: 1, maxPriority: 2, track: 'stability', exclude: batchIds });
|
|
309
313
|
addToBatch(sPicked);
|
|
310
314
|
allocated = mUsed + fUsed + sUsed;
|
|
311
315
|
} else {
|
|
@@ -920,9 +924,14 @@ function focusAutoUpdate(cwd, raw, getVal) {
|
|
|
920
924
|
run.totals.tests_current = cycle.tests_after;
|
|
921
925
|
run.status = AUTORUN_STATUSES.IN_PROGRESS;
|
|
922
926
|
|
|
923
|
-
|
|
927
|
+
// The host's 7-day usage window (Claude Code subscriptions, read from what PAN's
|
|
928
|
+
// status line last saw): past cost.weekly_limit_stop_pct the run stops resumable,
|
|
929
|
+
// so the rest of the window stays the user's. No fresh reading never stops it.
|
|
930
|
+
const weekly = weeklyLimitStop(cwd);
|
|
931
|
+
cycle.weekly_limit_used_pct = weekly.seven_day_used_pct;
|
|
932
|
+
const stopReason = determineStopReason(cycle, run) || (weekly.stop ? 'weekly_limit' : null);
|
|
924
933
|
if (stopReason) {
|
|
925
|
-
run.status = stopReason === 'regression' ? AUTORUN_STATUSES.STOPPED : AUTORUN_STATUSES.COMPLETED;
|
|
934
|
+
run.status = stopReason === 'regression' || stopReason === 'weekly_limit' ? AUTORUN_STATUSES.STOPPED : AUTORUN_STATUSES.COMPLETED;
|
|
926
935
|
run.stop_reason = stopReason;
|
|
927
936
|
}
|
|
928
937
|
|