pan-wizard 3.32.0 → 3.34.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 +23 -20
- package/agents/pan-conductor.md +18 -17
- package/agents/pan-counterfactual.md +2 -2
- package/agents/pan-debugger.md +27 -8
- package/agents/pan-design-checker.md +1 -1
- package/agents/pan-designer.md +2 -2
- package/agents/pan-distiller.md +3 -3
- package/agents/pan-document_code.md +16 -16
- package/agents/pan-executor.md +15 -11
- package/agents/pan-experiment-runner.md +15 -17
- package/agents/pan-hardener.md +15 -15
- package/agents/pan-integration-checker.md +2 -2
- package/agents/pan-knowledge.md +2 -2
- package/agents/pan-meta-reviewer.md +5 -5
- package/agents/pan-optimizer.md +40 -44
- package/agents/pan-phase-researcher.md +10 -10
- package/agents/pan-plan-checker.md +36 -30
- package/agents/pan-planner.md +77 -53
- package/agents/pan-previewer.md +1 -1
- package/agents/pan-project-researcher.md +21 -78
- package/agents/pan-release.md +1 -1
- package/agents/pan-research-synthesizer.md +2 -2
- package/agents/pan-reviewer.md +24 -2
- package/agents/pan-roadmapper.md +20 -62
- package/agents/pan-verifier.md +53 -30
- package/bin/install-lib.cjs +228 -38
- package/bin/install.js +86 -15
- package/commands/pan/army.md +16 -12
- package/commands/pan/assumptions.md +2 -2
- package/commands/pan/audit-deployment.md +11 -12
- package/commands/pan/cost.md +8 -4
- package/commands/pan/debug.md +10 -7
- package/commands/pan/design-phase.md +3 -3
- package/commands/pan/discuss-phase.md +2 -0
- package/commands/pan/exec-phase.md +21 -37
- package/commands/pan/experiment.md +11 -10
- package/commands/pan/focus-auto.md +33 -27
- package/commands/pan/focus-design.md +9 -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 +7 -7
- package/commands/pan/focus-plan.md +12 -11
- package/commands/pan/focus-scan.md +7 -10
- 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 +3 -2
- package/commands/pan/hygiene.md +14 -7
- package/commands/pan/knowledge.md +9 -9
- package/commands/pan/learn.md +10 -7
- package/commands/pan/links.md +4 -1
- package/commands/pan/map-codebase.md +5 -5
- package/commands/pan/mcp-bridge.md +18 -18
- package/commands/pan/milestone-done.md +7 -5
- package/commands/pan/milestone-new.md +1 -1
- package/commands/pan/new-project.md +10 -10
- package/commands/pan/optimize.md +16 -14
- package/commands/pan/patches.md +2 -6
- package/commands/pan/pause.md +1 -1
- package/commands/pan/phase-tests.md +4 -3
- package/commands/pan/plan-phase.md +9 -19
- package/commands/pan/preview.md +1 -1
- package/commands/pan/profile.md +4 -4
- package/commands/pan/quick.md +1 -1
- package/commands/pan/remove-phase.md +1 -1
- package/commands/pan/report.md +2 -2
- package/commands/pan/research-phase.md +14 -15
- package/commands/pan/resume.md +9 -11
- package/commands/pan/retro.md +5 -3
- package/commands/pan/review-deep.md +10 -10
- package/commands/pan/settings.md +1 -1
- package/commands/pan/todo-add.md +2 -2
- package/commands/pan/todo-check.md +2 -2
- package/commands/pan/verify-phase.md +0 -11
- package/commands/pan/what-if.md +2 -2
- package/hooks/dist/pan-context-monitor.js +343 -44
- package/hooks/dist/pan-cost-logger.js +30 -0
- package/hooks/dist/pan-state-reinject.js +140 -15
- package/hooks/dist/pan-statusline.js +68 -22
- package/hooks/dist/pan-stop-guard.js +7 -2
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/agents-md.cjs +48 -7
- 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 +126 -5
- package/pan-wizard-core/bin/lib/config.cjs +112 -1
- package/pan-wizard-core/bin/lib/constants.cjs +29 -15
- package/pan-wizard-core/bin/lib/context-budget.cjs +12 -9
- package/pan-wizard-core/bin/lib/core.cjs +77 -23
- package/pan-wizard-core/bin/lib/cost-rebuild.cjs +13 -1
- package/pan-wizard-core/bin/lib/cost.cjs +149 -10
- 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 +15 -4
- 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 +178 -17
- package/pan-wizard-core/bin/lib/init.cjs +12 -5
- 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 +84 -16
- 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 +67 -15
- package/pan-wizard-core/bin/lib/preview.cjs +3 -1
- package/pan-wizard-core/bin/lib/review-deep.cjs +2 -2
- 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 +47 -31
- package/pan-wizard-core/bin/lib/template.cjs +11 -4
- package/pan-wizard-core/bin/lib/verdict.cjs +54 -4
- package/pan-wizard-core/bin/lib/verify-drift.cjs +14 -5
- 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 +74 -4
- package/pan-wizard-core/bin/lib/verify.cjs +117 -35
- package/pan-wizard-core/bin/lib/whatif.cjs +5 -3
- package/pan-wizard-core/bin/pan-tools.cjs +111 -17
- package/pan-wizard-core/learnings/index.json +5 -5
- package/pan-wizard-core/mcp/server.cjs +18 -9
- package/pan-wizard-core/opencode/pan-wizard.js +111 -0
- package/pan-wizard-core/references/checkpoints.md +7 -5
- 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 +14 -9
- package/pan-wizard-core/references/phase-argument-parsing.md +7 -7
- package/pan-wizard-core/references/planning-config.md +4 -4
- package/pan-wizard-core/references/tdd.md +10 -1
- package/pan-wizard-core/references/verdict-contract.md +2 -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/continue-here.md +1 -1
- package/pan-wizard-core/templates/debug-subagent-prompt.md +3 -2
- package/pan-wizard-core/templates/debug.md +15 -1
- package/pan-wizard-core/templates/discovery.md +1 -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 -18
- package/pan-wizard-core/templates/planner-subagent-prompt.md +3 -5
- 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/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 +54 -4
- package/pan-wizard-core/workflows/add-phase.md +1 -1
- package/pan-wizard-core/workflows/assumptions.md +1 -1
- package/pan-wizard-core/workflows/discuss-phase.md +9 -8
- package/pan-wizard-core/workflows/exec-phase.md +34 -60
- package/pan-wizard-core/workflows/execute-plan.md +8 -7
- package/pan-wizard-core/workflows/health.md +13 -11
- 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 +17 -24
- package/pan-wizard-core/workflows/milestone-new.md +7 -6
- package/pan-wizard-core/workflows/new-project.md +10 -10
- 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 +31 -34
- package/pan-wizard-core/workflows/profile.md +2 -2
- package/pan-wizard-core/workflows/progress.md +5 -7
- package/pan-wizard-core/workflows/quick.md +10 -8
- 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 +1 -1
- package/pan-wizard-core/workflows/transition.md +12 -12
- package/pan-wizard-core/workflows/update.md +25 -17
- package/pan-wizard-core/workflows/verify-phase.md +24 -17
- 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-agent-plugin.js +19 -5
- package/scripts/build-plugin.js +3 -1
- package/scripts/generate-skills-docs.py +13 -4
- package/scripts/test-surface.cjs +4 -0
|
@@ -39,8 +39,13 @@ const { planningPath, planningRel } = require('./utils.cjs');
|
|
|
39
39
|
// budget-profile project on Codex or OpenCode was told to spawn a model called
|
|
40
40
|
// "mid" (reality check R28).
|
|
41
41
|
// - openai: Codex's subagent docs (learn.chatgpt.com/docs/agent-configuration/
|
|
42
|
-
// subagents, read 2026-
|
|
43
|
-
//
|
|
42
|
+
// subagents, re-read 2026-10-03) now start demanding agents on gpt-6.1-sol "when
|
|
43
|
+
// your signed-in account or workspace has access", and keep gpt-6-luna as the
|
|
44
|
+
// "faster, lower-cost option". The mid tier stays gpt-6-sol: GPT-6.1 Sol's launch
|
|
45
|
+
// keeps it off on Enterprise and Edu until an administrator enables it
|
|
46
|
+
// (learn.chatgpt.com/docs/models, read 2026-10-03), so pinning it would hand
|
|
47
|
+
// those users a model they cannot run. The inherit tier runs Codex's own
|
|
48
|
+
// default, which is gpt-6.1-sol where available. gpt-6-astra is the flagship.
|
|
44
49
|
// - google: the newest stable Flash and Flash-Lite on ai.google.dev's models page
|
|
45
50
|
// (read 2026-09-23). The 2.5 family these rows used is "not deprecated" but
|
|
46
51
|
// limited to users who have used it before, so a new project may not reach it.
|
|
@@ -58,12 +63,16 @@ const PROVIDER_MODELS = {
|
|
|
58
63
|
// packages/web/src/content/docs/models.mdx and models.dev/api.json, both read
|
|
59
64
|
// 2026-09-26; every id below exists there under that provider). Same tiers as
|
|
60
65
|
// PROVIDER_MODELS, provider-qualified; `default` is Anthropic as it is there.
|
|
61
|
-
// Reality check R41.
|
|
66
|
+
// Reality check R41. The Anthropic mid tier follows the `sonnet` alias, which
|
|
67
|
+
// resolves to Sonnet 5.5 on the Anthropic API from Claude Code 2.1.284;
|
|
68
|
+
// models.dev lists `anthropic/claude-sonnet-5-5` (api.json, read 2026-10-03). The fast
|
|
69
|
+
// tier follows the `haiku` alias, which resolves to Haiku 5.5 on the Anthropic API from
|
|
70
|
+
// Claude Code 2.1.293; models.dev lists `anthropic/claude-haiku-5-5` (read 2026-10-10).
|
|
62
71
|
const OPENCODE_MODELS = {
|
|
63
|
-
anthropic: { reasoning: 'inherit', mid: 'anthropic/claude-sonnet-5', fast: 'anthropic/claude-haiku-
|
|
64
|
-
openai: { reasoning: 'inherit', mid: 'openai/gpt-6-sol',
|
|
65
|
-
google: { reasoning: 'inherit', mid: 'google/gemini-3.8-flash',
|
|
66
|
-
default: { reasoning: 'inherit', mid: 'anthropic/claude-sonnet-5', fast: 'anthropic/claude-haiku-
|
|
72
|
+
anthropic: { reasoning: 'inherit', mid: 'anthropic/claude-sonnet-5-5', fast: 'anthropic/claude-haiku-5-5' },
|
|
73
|
+
openai: { reasoning: 'inherit', mid: 'openai/gpt-6-sol', fast: 'openai/gpt-6-luna' },
|
|
74
|
+
google: { reasoning: 'inherit', mid: 'google/gemini-3.8-flash', fast: 'google/gemini-3.5-flash-lite' },
|
|
75
|
+
default: { reasoning: 'inherit', mid: 'anthropic/claude-sonnet-5-5', fast: 'anthropic/claude-haiku-5-5' },
|
|
67
76
|
};
|
|
68
77
|
|
|
69
78
|
/**
|
|
@@ -382,6 +391,32 @@ function safeReadFile(filePath) {
|
|
|
382
391
|
}
|
|
383
392
|
}
|
|
384
393
|
|
|
394
|
+
// ─── Line endings ───────────────────────────────────────────────────────────
|
|
395
|
+
// A Windows working tree (core.autocrlf=true) holds planning files with CRLF
|
|
396
|
+
// endings, and a parser written for '\n' finds nothing in them without saying so:
|
|
397
|
+
// on 2026-10-04 every Windows field project measured was CRLF throughout, and the
|
|
398
|
+
// memory optimizer, the must-haves parser and `frontmatter set` had all been
|
|
399
|
+
// silently no-op (or worse) there. Parse on LF; write back in the file's own ending.
|
|
400
|
+
|
|
401
|
+
/** LF text: CRLF → LF and a leading BOM dropped. */
|
|
402
|
+
function toLf(text) {
|
|
403
|
+
return String(text == null ? '' : text).replace(/^/, '').replace(/\r\n/g, '\n');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** The ending a text mostly uses: '\r\n' when its CRLF lines outnumber its bare LF ones. */
|
|
407
|
+
function dominantEol(text) {
|
|
408
|
+
const s = String(text == null ? '' : text);
|
|
409
|
+
const crlf = (s.match(/\r\n/g) || []).length;
|
|
410
|
+
const lf = (s.match(/\n/g) || []).length - crlf;
|
|
411
|
+
return crlf > lf ? '\r\n' : '\n';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** LF text given back in the ending `eol` names. */
|
|
415
|
+
function withEol(text, eol) {
|
|
416
|
+
const s = String(text == null ? '' : text);
|
|
417
|
+
return eol === '\r\n' ? s.replace(/\r?\n/g, '\r\n') : s;
|
|
418
|
+
}
|
|
419
|
+
|
|
385
420
|
/**
|
|
386
421
|
* Load project config from .planning/config.json, merging with defaults.
|
|
387
422
|
* Handles nested config sections (planning.*, workflow.*, git.*) and flat keys.
|
|
@@ -455,10 +490,10 @@ function loadConfig(cwd) {
|
|
|
455
490
|
// Cost dashboard config: `cost.rates` per-model overrides (surfaced so the
|
|
456
491
|
// documented override actually reaches cost.cjs — it was dropped before).
|
|
457
492
|
cost: parsed.cost || {},
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
// built-in list is the phase-model spine, so a focus-model
|
|
461
|
-
// an empty block and
|
|
493
|
+
// Cache config: `cache.extra_files` lets a project add its own stable
|
|
494
|
+
// documents to the block `cache prime` and the size checks measure. Needed
|
|
495
|
+
// because the built-in list is the phase-model spine, so a focus-model
|
|
496
|
+
// project had an empty block and nothing measured.
|
|
462
497
|
cache: parsed.cache || {},
|
|
463
498
|
// ADR-0031: project build/verification commands. null = not configured
|
|
464
499
|
// (focus-auto --clean-seal then asks or skips rather than guessing).
|
|
@@ -552,6 +587,18 @@ function escapeRegex(value) {
|
|
|
552
587
|
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
553
588
|
}
|
|
554
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Make free text safe for one markdown table cell: escape backslashes and pipes,
|
|
592
|
+
* and fold every run of line breaks into one space. A raw `|` splits the row into
|
|
593
|
+
* an extra column, and state.md's tables are appended to for the life of a
|
|
594
|
+
* project, so one bad row breaks every later read of the table (market-ideas M21).
|
|
595
|
+
* @param {*} value - Cell text; null and undefined become ''
|
|
596
|
+
* @returns {string}
|
|
597
|
+
*/
|
|
598
|
+
function escapeTableCell(value) {
|
|
599
|
+
return String(value ?? '').replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/[\r\n]+/g, ' ');
|
|
600
|
+
}
|
|
601
|
+
|
|
555
602
|
/**
|
|
556
603
|
* Normalize a phase identifier to zero-padded format.
|
|
557
604
|
* Examples: "1" → "01", "3A" → "03A", "12.1" → "12.1"
|
|
@@ -615,7 +662,7 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
|
615
662
|
try {
|
|
616
663
|
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
617
664
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
618
|
-
const match = dirs.find(d => d.startsWith(normalized));
|
|
665
|
+
const match = dirs.find(d => d === normalized || d.startsWith(normalized + '-'));
|
|
619
666
|
if (!match) return null;
|
|
620
667
|
|
|
621
668
|
const dirMatch = match.match(PHASE_DIR_RE);
|
|
@@ -766,7 +813,11 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
|
|
|
766
813
|
const restOfContent = content.slice(headerIndex);
|
|
767
814
|
const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
|
|
768
815
|
const sectionEnd = nextHeaderMatch ? headerIndex + nextHeaderMatch.index : content.length;
|
|
769
|
-
|
|
816
|
+
let section = content.slice(headerIndex, sectionEnd).trim();
|
|
817
|
+
// A phase moved out by `roadmap compact` leaves a stub; the full section
|
|
818
|
+
// (and any model_tier override with it) is in roadmap-history.md.
|
|
819
|
+
const compacted = require('./roadmap-compact.cjs').compactedPhaseSection(cwd, unpadded, section);
|
|
820
|
+
if (compacted && compacted.section) section = compacted.section;
|
|
770
821
|
|
|
771
822
|
const goalMatch = section.match(/(?:\*\*Goal:\*\*|\*\*Goal\*\*:)\s*([^\n]+)/i);
|
|
772
823
|
const goal = goalMatch ? goalMatch[1].trim() : null;
|
|
@@ -777,6 +828,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
|
|
|
777
828
|
phase_name: phaseName,
|
|
778
829
|
goal,
|
|
779
830
|
section,
|
|
831
|
+
...(compacted ? { compacted: true } : {}),
|
|
780
832
|
};
|
|
781
833
|
} catch {
|
|
782
834
|
return null;
|
|
@@ -1343,12 +1395,12 @@ function scanPendingTodos(cwd, area) {
|
|
|
1343
1395
|
*
|
|
1344
1396
|
* Reads files from .planning/ that are stable across agent calls within a phase
|
|
1345
1397
|
* (project.md, requirements.md, roadmap.md, state.md, standards.md). Each block
|
|
1346
|
-
* is tagged `cache: true
|
|
1347
|
-
*
|
|
1398
|
+
* is tagged `cache: true`. Nothing translates that tag into a cache marker: the
|
|
1399
|
+
* host caches each agent's prompt prefix by itself, so this list is what PAN
|
|
1400
|
+
* measures (context-budget, hygiene's cache-context check), not what it primes.
|
|
1348
1401
|
*
|
|
1349
1402
|
* Files that don't exist are skipped silently. The order matches the file list
|
|
1350
|
-
* in constants.cjs
|
|
1351
|
-
* what cache key matching requires).
|
|
1403
|
+
* in constants.cjs, so the `sha` of an unchanged set stays the same.
|
|
1352
1404
|
*
|
|
1353
1405
|
* @param {string} cwd - Project root
|
|
1354
1406
|
* @returns {{blocks: Array<{path: string, content: string, cache: true}>, total_bytes: number, sha: string}}
|
|
@@ -1361,14 +1413,12 @@ function buildCachedContext(cwd) {
|
|
|
1361
1413
|
const hasher = crypto.createHash('sha256');
|
|
1362
1414
|
|
|
1363
1415
|
// The built-in list is the PHASE-model spine. A focus-model project has none
|
|
1364
|
-
// of those files, so its
|
|
1365
|
-
//
|
|
1366
|
-
//
|
|
1367
|
-
// otherwise define.
|
|
1416
|
+
// of those files, so its measured block came out empty and the size checks had
|
|
1417
|
+
// nothing to read. `cache.extra_files` lets such a project name its own stable
|
|
1418
|
+
// documents rather than PAN inventing a convention it doesn't otherwise define.
|
|
1368
1419
|
//
|
|
1369
1420
|
// Entries are planning-root-relative, must stay inside it, and are appended
|
|
1370
|
-
// after the built-ins so the
|
|
1371
|
-
// nothing — changing the prefix would invalidate every existing cache key.
|
|
1421
|
+
// after the built-ins, so the `sha` of a project that sets nothing does not change.
|
|
1372
1422
|
const extra = [];
|
|
1373
1423
|
try {
|
|
1374
1424
|
const configured = loadConfig(cwd)?.cache?.extra_files;
|
|
@@ -1449,11 +1499,15 @@ module.exports = {
|
|
|
1449
1499
|
error,
|
|
1450
1500
|
verbose,
|
|
1451
1501
|
safeReadFile,
|
|
1502
|
+
toLf,
|
|
1503
|
+
dominantEol,
|
|
1504
|
+
withEol,
|
|
1452
1505
|
loadConfig,
|
|
1453
1506
|
isGitIgnored,
|
|
1454
1507
|
isGitRepo,
|
|
1455
1508
|
execGit,
|
|
1456
1509
|
escapeRegex,
|
|
1510
|
+
escapeTableCell,
|
|
1457
1511
|
normalizePhaseName,
|
|
1458
1512
|
comparePhaseNum,
|
|
1459
1513
|
searchPhaseInDir,
|
|
@@ -39,7 +39,7 @@ const os = require('os');
|
|
|
39
39
|
const path = require('path');
|
|
40
40
|
const { output, error, loadConfig } = require('./core.cjs');
|
|
41
41
|
const { planningPath } = require('./utils.cjs');
|
|
42
|
-
const { readRecords, computeCost, effectiveRates, isSuspectRecord, isEmptyRecord, METRICS_DIR, TOKENS_FILE } = require('./cost.cjs');
|
|
42
|
+
const { readRecords, computeCost, effectiveRates, isSuspectRecord, isEmptyRecord, LONG_PROMPT_THRESHOLD, METRICS_DIR, TOKENS_FILE } = require('./cost.cjs');
|
|
43
43
|
|
|
44
44
|
const SCHEMA_V = 4; // matches hooks/pan-cost-logger.js SCHEMA_V
|
|
45
45
|
const MAIN_THREAD_AGENT = '(main thread)';
|
|
@@ -144,6 +144,17 @@ function sumTranscriptUsage(file) {
|
|
|
144
144
|
totals.cache_write_1h_tokens = (totals.cache_write_1h_tokens || 0) + num(cc, 'ephemeral_1h_input_tokens');
|
|
145
145
|
totals.cache_write_5m_tokens = (totals.cache_write_5m_tokens || 0) + num(cc, 'ephemeral_5m_input_tokens');
|
|
146
146
|
}
|
|
147
|
+
// A request whose prompt is over LONG_PROMPT_THRESHOLD bills at a model's long
|
|
148
|
+
// rate where it has one (Haiku 5.5); the same rule as the cost-logger hook.
|
|
149
|
+
const prompt = num(u, 'input_tokens') + num(u, 'cache_read_input_tokens') + num(u, 'cache_creation_input_tokens');
|
|
150
|
+
if (prompt > LONG_PROMPT_THRESHOLD) {
|
|
151
|
+
const lp = totals.long_prompt || (totals.long_prompt = { above: LONG_PROMPT_THRESHOLD, input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0, cache_write_1h_tokens: 0 });
|
|
152
|
+
lp.input_tokens += num(u, 'input_tokens');
|
|
153
|
+
lp.output_tokens += num(u, 'output_tokens');
|
|
154
|
+
lp.cache_read_tokens += num(u, 'cache_read_input_tokens');
|
|
155
|
+
lp.cache_write_tokens += num(u, 'cache_creation_input_tokens');
|
|
156
|
+
if (cc && typeof cc === 'object') lp.cache_write_1h_tokens += num(cc, 'ephemeral_1h_input_tokens');
|
|
157
|
+
}
|
|
147
158
|
}
|
|
148
159
|
totals.turns = byMessage.size;
|
|
149
160
|
let best = null;
|
|
@@ -304,6 +315,7 @@ function makeRow(fields) {
|
|
|
304
315
|
...(typeof fields.usage.cache_write_1h_tokens === 'number'
|
|
305
316
|
? { cache_write_1h_tokens: fields.usage.cache_write_1h_tokens, cache_write_5m_tokens: fields.usage.cache_write_5m_tokens || 0 }
|
|
306
317
|
: {}),
|
|
318
|
+
...(fields.usage.long_prompt ? { long_prompt: fields.usage.long_prompt } : {}),
|
|
307
319
|
cost_usd: null,
|
|
308
320
|
duration_ms: fields.usage.first_ts && fields.usage.last_ts ? Date.parse(fields.usage.last_ts) - Date.parse(fields.usage.first_ts) : null,
|
|
309
321
|
phase: null,
|
|
@@ -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,10 +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.
|
|
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 },
|
|
94
106
|
'claude-sonnet-5': { input: 2.0, output: 10.0, cache_read: 0.20, cache_write: 2.50, cache_write_1h: 4.0 },
|
|
95
107
|
'claude-sonnet-4-6': { input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75, cache_write_1h: 6.0 },
|
|
96
108
|
// Sonnet 4.5 (dated id claude-sonnet-4-5-20250929): $3/$15/$0.30/$3.75 (R7).
|
|
97
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 } },
|
|
98
118
|
// Haiku 4.5: API id claude-haiku-4-5-20251001, alias claude-haiku-4-5.
|
|
99
119
|
'claude-haiku-4-5': { input: 1.0, output: 5.0, cache_read: 0.1, cache_write: 1.25, cache_write_1h: 2.0 },
|
|
100
120
|
|
|
@@ -105,10 +125,14 @@ const DEFAULT_RATES = {
|
|
|
105
125
|
// as plain input. Until 2026-09-22 this block said "no separate write charge" for
|
|
106
126
|
// every row, which had stopped being true — Terra and Luna writes were a fifth
|
|
107
127
|
// low (R27). Longest-prefix match lets each tier key win over a shorter one.
|
|
108
|
-
// GPT-6: Astra is the flagship; Sol and Luna were released 2026-09-22.
|
|
109
|
-
//
|
|
110
|
-
//
|
|
128
|
+
// GPT-6: Astra is the flagship; Sol and Luna were released 2026-09-22. GPT-6.1 Sol
|
|
129
|
+
// is Codex's default model from 0.159.1 (2026-09-29): `model = "gpt-6.1-sol"` in
|
|
130
|
+
// config-basic, and the subagent docs' starting model for demanding agents, with
|
|
131
|
+
// gpt-6-luna for lower-cost ones (learn.chatgpt.com/docs/config-file/config-basic,
|
|
132
|
+
// /agent-configuration/subagents, both read 2026-10-03). Its row was read from the
|
|
133
|
+
// pricing page on 2026-10-03: GPT-6 Sol's prices, except cached input at 0.05×.
|
|
111
134
|
'gpt-6-astra': { input: 10.0, output: 50.0, cache_read: 1.0, cache_write: 12.5 },
|
|
135
|
+
'gpt-6.1-sol': { input: 2.0, output: 10.0, cache_read: 0.1, cache_write: 2.5 },
|
|
112
136
|
'gpt-6-sol': { input: 2.0, output: 10.0, cache_read: 0.2, cache_write: 2.5 },
|
|
113
137
|
'gpt-6-luna': { input: 0.10, output: 0.50, cache_read: 0.01, cache_write: 0.125 },
|
|
114
138
|
// GPT-5.6. The bare `gpt-5.6` id is an alias that "routes requests to GPT-5.6 Sol"
|
|
@@ -232,6 +256,7 @@ const scaleRate = (rate, m) => ({
|
|
|
232
256
|
cache_read: round6(rate.cache_read * m),
|
|
233
257
|
cache_write: round6(rate.cache_write * m),
|
|
234
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) } } : {}),
|
|
235
260
|
});
|
|
236
261
|
|
|
237
262
|
/**
|
|
@@ -378,12 +403,35 @@ function computeCost(rec, configRates) {
|
|
|
378
403
|
// bills at the one-hour rate; the rest — and every write on a row or rate without
|
|
379
404
|
// the split — at `cache_write`.
|
|
380
405
|
const write1h = Math.min(cacheWrite, Math.max(0, Number(rec.cache_write_1h_tokens) || 0));
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
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);
|
|
384
425
|
return Math.round(usd * 10000) / 10000;
|
|
385
426
|
}
|
|
386
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
|
+
|
|
387
435
|
/**
|
|
388
436
|
* Append a cost record. Non-blocking — errors are swallowed so instrumentation
|
|
389
437
|
* never breaks the caller.
|
|
@@ -522,7 +570,11 @@ function aggregate(cwd, opts) {
|
|
|
522
570
|
const records = readRecords(cwd);
|
|
523
571
|
const malformedSkipped = _lastReadMalformed; // captured before any later read
|
|
524
572
|
const since = opts?.since ? new Date(opts.since).getTime() : null;
|
|
525
|
-
|
|
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;
|
|
526
578
|
const config = loadConfig(cwd);
|
|
527
579
|
const configRates = effectiveRates(config);
|
|
528
580
|
|
|
@@ -689,10 +741,12 @@ function renderChart(agg) {
|
|
|
689
741
|
function cmdCostReport(cwd, opts, raw) {
|
|
690
742
|
const format = opts?.format || 'json';
|
|
691
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.
|
|
692
746
|
if (format === 'table') {
|
|
693
|
-
output(agg,
|
|
747
|
+
output(agg, true, renderTable(agg));
|
|
694
748
|
} else if (format === 'chart') {
|
|
695
|
-
output(agg,
|
|
749
|
+
output(agg, true, renderChart(agg));
|
|
696
750
|
} else {
|
|
697
751
|
output(agg, raw);
|
|
698
752
|
}
|
|
@@ -712,6 +766,86 @@ function cmdCostClear(cwd, raw) {
|
|
|
712
766
|
}
|
|
713
767
|
}
|
|
714
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
|
+
|
|
715
849
|
// ─── Rate-table staleness ───────────────────────────────────────────────────
|
|
716
850
|
|
|
717
851
|
// Date DEFAULT_RATES was last verified against published provider pricing — ALL
|
|
@@ -728,7 +862,7 @@ function cmdCostClear(cwd, raw) {
|
|
|
728
862
|
// a default-model change inside the window — that is the job of the
|
|
729
863
|
// documented-default-ids fixture test (tests/fixtures/documented-default-models.json,
|
|
730
864
|
// R31), which fails the suite the day a documented default id has no exact row.
|
|
731
|
-
const RATES_VERIFIED_AT = '2026-
|
|
865
|
+
const RATES_VERIFIED_AT = '2026-10-10';
|
|
732
866
|
const RATES_STALE_AFTER_DAYS = 60;
|
|
733
867
|
const RATE_TIERS = ['reasoning', 'mid', 'fast'];
|
|
734
868
|
|
|
@@ -774,6 +908,11 @@ module.exports = {
|
|
|
774
908
|
renderTable,
|
|
775
909
|
renderChart,
|
|
776
910
|
resolveRate,
|
|
911
|
+
LONG_PROMPT_THRESHOLD,
|
|
912
|
+
readHostLimits,
|
|
913
|
+
weeklyLimitStop,
|
|
914
|
+
cmdCostLimits,
|
|
915
|
+
HOST_LIMITS_FILE,
|
|
777
916
|
ratesFromModelPricing,
|
|
778
917
|
managedSettingsDir,
|
|
779
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) {
|
|
@@ -160,7 +160,10 @@ function recordVerdict(cwd, input) {
|
|
|
160
160
|
|
|
161
161
|
const { rows } = readLedger(cwd);
|
|
162
162
|
const fold = foldFindings(rows);
|
|
163
|
-
|
|
163
|
+
// `not_checked` (M23): what the judge could not run. A pass with entries here is a
|
|
164
|
+
// pass over less than the whole phase, and the record says so.
|
|
165
|
+
const notChecked = verdict.not_checked || [];
|
|
166
|
+
const base = { contract: V.VERDICT_CONTRACT, agent, verdict: verdict.verdict, outcome: verdict.outcome, not_checked: notChecked, source_kind: parsed.source_kind, warnings: parsed.warnings };
|
|
164
167
|
|
|
165
168
|
const dup = fold.verdicts.find((r) => r.record_sig === recordSig && r.agent === agent && samePhase(r.phase, phase));
|
|
166
169
|
if (dup) {
|
|
@@ -204,6 +207,7 @@ function recordVerdict(cwd, input) {
|
|
|
204
207
|
v: LEDGER_V, kind: 'verdict', ts, verdict_id: verdictId, record_sig: recordSig, phase, milestone, agent,
|
|
205
208
|
verdict: verdict.verdict, outcome: verdict.outcome, attempt, source, finding_ids: ids,
|
|
206
209
|
by_severity: bySeverity, by_class: byClass, warnings: parsed.warnings,
|
|
210
|
+
...(notChecked.length ? { not_checked: notChecked } : {}),
|
|
207
211
|
});
|
|
208
212
|
appendRows(cwd, [...findingRows, ...newRows, ...autoFixRows]);
|
|
209
213
|
|
|
@@ -216,7 +220,7 @@ function recordVerdict(cwd, input) {
|
|
|
216
220
|
phase,
|
|
217
221
|
description: `${agent} ${verdict.verdict || verdict.outcome} on phase ${phase} (attempt ${attempt}, ${ids.length} finding${ids.length === 1 ? '' : 's'})`,
|
|
218
222
|
impact: verdict.outcome === 'fail' ? (serious ? 'major' : 'minor') : 'minor',
|
|
219
|
-
context: { agent, phase, verdict: verdict.verdict, attempt, findings: ids.length, by_severity: bySeverity, by_class: byClass, verdict_id: verdictId },
|
|
223
|
+
context: { agent, phase, verdict: verdict.verdict, attempt, findings: ids.length, by_severity: bySeverity, by_class: byClass, verdict_id: verdictId, not_checked: notChecked.map((n) => n.check) },
|
|
220
224
|
});
|
|
221
225
|
if (previous && previous.outcome === 'fail') {
|
|
222
226
|
traceEvent(cwd, {
|
|
@@ -272,6 +276,13 @@ function disposeFindings(cwd, input) {
|
|
|
272
276
|
return { contract: V.VERDICT_CONTRACT, disposition: as, disposed, skipped };
|
|
273
277
|
}
|
|
274
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Milestone versions compare with or without the leading `v`: the ledger stores
|
|
281
|
+
* `getMilestoneInfo`'s `v1.0`, while milestone-audit names its version `1.0` (its
|
|
282
|
+
* report is `v{version}-milestone-audit.md`), so `--milestone 1.0` found no debt.
|
|
283
|
+
*/
|
|
284
|
+
const sameMilestone = (a, b) => String(a || '').trim().replace(/^v/i, '') === String(b || '').trim().replace(/^v/i, '');
|
|
285
|
+
|
|
275
286
|
/** Findings with their folded status, filtered. */
|
|
276
287
|
function listFindings(cwd, filters = {}) {
|
|
277
288
|
const { rows, malformed } = readLedger(cwd);
|
|
@@ -281,7 +292,7 @@ function listFindings(cwd, filters = {}) {
|
|
|
281
292
|
if (filters.agent) list = list.filter((f) => f.agent === filters.agent);
|
|
282
293
|
if (filters.status) list = list.filter((f) => f.status === filters.status);
|
|
283
294
|
if (filters.class) list = list.filter((f) => f.class === filters.class);
|
|
284
|
-
if (filters.milestone) list = list.filter((f) => f.milestone
|
|
295
|
+
if (filters.milestone) list = list.filter((f) => sameMilestone(f.milestone, filters.milestone));
|
|
285
296
|
const findings = list.map(publicFinding);
|
|
286
297
|
return {
|
|
287
298
|
contract: V.VERDICT_CONTRACT,
|
|
@@ -302,7 +313,7 @@ function findingsDebt(cwd, opts = {}) {
|
|
|
302
313
|
const fold = foldFindings(rows);
|
|
303
314
|
const byPhase = new Map();
|
|
304
315
|
for (const f of fold.findings.values()) {
|
|
305
|
-
if (milestone && f.milestone
|
|
316
|
+
if (milestone && !sameMilestone(f.milestone, milestone)) continue;
|
|
306
317
|
if (f.status !== 'deferred' && f.status !== 'open') continue;
|
|
307
318
|
const key = V.comparablePhase(f.phase);
|
|
308
319
|
if (!byPhase.has(key)) byPhase.set(key, { phase: f.phase, deferred: [], open: [] });
|