pan-wizard 3.31.0 → 3.32.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 +28 -24
- package/agents/pan-design-checker.md +18 -0
- package/agents/pan-optimizer.md +5 -1
- package/agents/pan-plan-checker.md +23 -0
- package/agents/pan-reviewer.md +19 -0
- package/agents/pan-verifier.md +23 -0
- package/commands/pan/design-phase.md +8 -0
- package/commands/pan/exec-phase.md +1 -1
- package/commands/pan/git.md +2 -2
- package/commands/pan/learn.md +2 -0
- package/commands/pan/optimize.md +14 -2
- package/commands/pan/review-deep.md +5 -5
- package/hooks/dist/pan-context-monitor.js +1 -1
- package/hooks/dist/pan-cost-logger.js +63 -11
- package/hooks/dist/pan-state-reinject.js +2 -2
- package/hooks/dist/pan-stop-guard.js +2 -2
- package/hooks/dist/pan-trace-logger.js +283 -18
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/commands.cjs +2 -1
- package/pan-wizard-core/bin/lib/constants.cjs +28 -0
- package/pan-wizard-core/bin/lib/cost.cjs +3 -3
- package/pan-wizard-core/bin/lib/findings.cjs +381 -0
- package/pan-wizard-core/bin/lib/focus.cjs +72 -19
- package/pan-wizard-core/bin/lib/foreign-planning.cjs +24 -6
- package/pan-wizard-core/bin/lib/hygiene.cjs +12 -2
- package/pan-wizard-core/bin/lib/init.cjs +4 -1
- package/pan-wizard-core/bin/lib/optimize.cjs +349 -21
- package/pan-wizard-core/bin/lib/review-deep.cjs +36 -0
- package/pan-wizard-core/bin/lib/state.cjs +7 -2
- package/pan-wizard-core/bin/lib/verdict.cjs +431 -0
- package/pan-wizard-core/bin/lib/verify-preflight.cjs +4 -2
- package/pan-wizard-core/bin/lib/verify-scope.cjs +156 -0
- package/pan-wizard-core/bin/lib/verify.cjs +25 -2
- package/pan-wizard-core/bin/pan-tools.cjs +59 -5
- package/pan-wizard-core/learnings/README.md +10 -7
- package/pan-wizard-core/mcp/tool-registry.cjs +2 -0
- package/pan-wizard-core/references/verdict-contract.md +101 -0
- package/pan-wizard-core/templates/verification-report.md +16 -0
- package/pan-wizard-core/workflows/exec-phase.md +65 -47
- package/pan-wizard-core/workflows/health.md +2 -0
- package/pan-wizard-core/workflows/learn.md +4 -2
- package/pan-wizard-core/workflows/milestone-audit.md +15 -3
- package/pan-wizard-core/workflows/optimize.md +14 -1
- package/pan-wizard-core/workflows/plan-phase.md +20 -15
- package/pan-wizard-core/workflows/verify-phase.md +3 -13
- package/pan-zcode/README.md +3 -3
- package/scripts/git-hooks/pre-commit +2 -1
|
@@ -146,9 +146,11 @@ const CURSOR_FILE = '.cost-cursor.json';
|
|
|
146
146
|
// the per-invocation `event_sig` discriminator; v4 added `agent_id` and the
|
|
147
147
|
// `agent-transcript` token source. Kept as a literal in each hook —
|
|
148
148
|
// they are standalone zero-dep scripts that can't import from pan-wizard-core, so
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
149
|
+
// each hook's copy is kept by hand (in lockstep through v4; the trace logger's events
|
|
150
|
+
// have since moved to 5 on their own). cost-rebuild.cjs in
|
|
151
|
+
// pan-wizard-core mirrors it for the rows it rebuilds; the readers there take a row
|
|
152
|
+
// field by field rather than switching on its version, so an added field is additive
|
|
153
|
+
// for them.
|
|
152
154
|
const SCHEMA_V = 4;
|
|
153
155
|
|
|
154
156
|
// Reverse-map a resolved model id to its cost tier so the "By tier" dashboard
|
|
@@ -456,10 +458,13 @@ const MAX_CURSOR_KEYS = 512;
|
|
|
456
458
|
// marker phantom (an over-count) and from N29 (concurrent same-type siblings).
|
|
457
459
|
//
|
|
458
460
|
// Returns null when the payload cannot be serialized — callers then fail OPEN
|
|
459
|
-
// (record, never mark).
|
|
461
|
+
// (record, never mark). SHA-256 cut to the 40 hex characters the SHA-1 it replaced
|
|
462
|
+
// produced, so persisted `event_sig` values keep their shape: a dedup key, not a
|
|
463
|
+
// security boundary, but CodeQL rightly flags SHA-1 over a payload that carries a
|
|
464
|
+
// session id (2026-09-29).
|
|
460
465
|
function eventSignature(data) {
|
|
461
466
|
try {
|
|
462
|
-
return crypto.createHash('
|
|
467
|
+
return crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex').slice(0, 40);
|
|
463
468
|
} catch { return null; }
|
|
464
469
|
}
|
|
465
470
|
|
|
@@ -559,6 +564,27 @@ function resolveAgentTranscript(data) {
|
|
|
559
564
|
}
|
|
560
565
|
}
|
|
561
566
|
|
|
567
|
+
// Host-internal agents (measured 2026-09-28). Claude Code runs helper agents of its own:
|
|
568
|
+
// the compaction summariser, and one at almost every turn end of an interactive session.
|
|
569
|
+
// It fires SubagentStop for them with an `agent_id`, an EMPTY `agent_type`, and an
|
|
570
|
+
// `agent_transcript_path` it never writes. That shape was captured from a real `/compact`
|
|
571
|
+
// on Claude Code 2.1.280 (tests/fixtures/hooks/subagent-stop-internal-claude.json). Both
|
|
572
|
+
// loggers booked each as an `unknown` zero-token spawn: 181 rows in three field projects
|
|
573
|
+
// in eleven days, skewing optimize learn's agent stats and the ledger's spawn counts.
|
|
574
|
+
// The host documents `agent_type` as present whenever a hook fires inside a subagent, so
|
|
575
|
+
// a named instance with no type and no transcript is not a spawn anyone asked for, and
|
|
576
|
+
// nothing is recorded. A NAMED spawn whose file is late or missing keeps its zero-token
|
|
577
|
+
// row (N17/N25); a payload with no agent instance at all keeps the parent-slice path.
|
|
578
|
+
// Identical in pan-cost-logger.js and pan-trace-logger.js (tests/trace-logger.test.cjs
|
|
579
|
+
// pins the copies).
|
|
580
|
+
function isHostInternalAgent(data, agentTranscript) {
|
|
581
|
+
if (!data || typeof data !== 'object' || agentTranscript) return false;
|
|
582
|
+
const typed = [data.agent_type, data.subagent_type].some((v) => typeof v === 'string' && v.trim() !== '');
|
|
583
|
+
if (typed) return false;
|
|
584
|
+
return (typeof data.agent_id === 'string' && AGENT_ID_SAFE.test(data.agent_id))
|
|
585
|
+
|| (typeof data.agent_transcript_path === 'string' && data.agent_transcript_path !== '');
|
|
586
|
+
}
|
|
587
|
+
|
|
562
588
|
// A slice is a SUM over one subagent's conversation, so its ceiling sits above a
|
|
563
589
|
// single call's (PLAUSIBLE_MAX): a long agent legitimately re-reads its cached
|
|
564
590
|
// context on every turn. The cache_read and output ceilings are the absolute
|
|
@@ -576,6 +602,29 @@ function clampSlice(n, max) {
|
|
|
576
602
|
return typeof n === 'number' && n >= 0 && n <= max ? n : 0;
|
|
577
603
|
}
|
|
578
604
|
|
|
605
|
+
// Copilot CLI's camelCase hook payload (docs.github.com/en/copilot/reference/hooks-reference,
|
|
606
|
+
// read 2026-09-28; documented, not yet observed live) names every field differently from
|
|
607
|
+
// Claude's snake_case one, so this hook read no session, transcript or agent from it and
|
|
608
|
+
// booked every Copilot spawn as `unknown`. Its `agentType` is the KIND of agent (built-in
|
|
609
|
+
// or custom); `agentName` is the configured name PAN's agents carry, so the name wins.
|
|
610
|
+
// Claude payloads carry none of these keys and pass through unchanged. Identical in
|
|
611
|
+
// pan-cost-logger.js and pan-trace-logger.js (tests/copilot-payload.test.cjs pins both).
|
|
612
|
+
function normalizeHookPayload(data) {
|
|
613
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return data;
|
|
614
|
+
const pick = (...keys) => {
|
|
615
|
+
for (const k of keys) if (typeof data[k] === 'string' && data[k]) return data[k];
|
|
616
|
+
return undefined;
|
|
617
|
+
};
|
|
618
|
+
const out = { ...data };
|
|
619
|
+
if (out.session_id === undefined && pick('sessionId')) out.session_id = pick('sessionId');
|
|
620
|
+
if (out.transcript_path === undefined && pick('transcriptPath')) out.transcript_path = pick('transcriptPath');
|
|
621
|
+
if (out.agent_id === undefined && pick('agentId')) out.agent_id = pick('agentId');
|
|
622
|
+
const name = pick('agent_name', 'agentName');
|
|
623
|
+
if (name) out.agent_type = name;
|
|
624
|
+
else if (out.agent_type === undefined && pick('agentType')) out.agent_type = pick('agentType');
|
|
625
|
+
return out;
|
|
626
|
+
}
|
|
627
|
+
|
|
579
628
|
/**
|
|
580
629
|
* Extract what we can from the SubagentStop event payload.
|
|
581
630
|
* Pure function — safe to test without stdin.
|
|
@@ -586,6 +635,7 @@ function clampSlice(n, max) {
|
|
|
586
635
|
*/
|
|
587
636
|
function buildCostRecord(data, cwd) {
|
|
588
637
|
if (!data || typeof data !== 'object') return null;
|
|
638
|
+
data = normalizeHookPayload(data);
|
|
589
639
|
|
|
590
640
|
// Only log actual subagent stops; ignore other Stop variants.
|
|
591
641
|
if (data.hook_event_name && data.hook_event_name !== 'SubagentStop') return null;
|
|
@@ -613,6 +663,7 @@ function buildCostRecord(data, cwd) {
|
|
|
613
663
|
// a value dropped to 0 by a plausibility guard so a guarded zero is
|
|
614
664
|
// distinguishable from a genuine zero-token run.
|
|
615
665
|
const agentTranscript = resolveAgentTranscript(data);
|
|
666
|
+
if (isHostInternalAgent(data, agentTranscript)) return null; // the host's own helper, not a spawn
|
|
616
667
|
const parentPath = typeof data.transcript_path === 'string' && data.transcript_path ? data.transcript_path : null;
|
|
617
668
|
// The host NAMED an agent (a bare id, or an explicit agent transcript path) but
|
|
618
669
|
// its file is not there — not flushed yet, or a host that sends ids without
|
|
@@ -731,13 +782,14 @@ function buildCostRecord(data, cwd) {
|
|
|
731
782
|
// Backfill command/phase from the active trace session when the payload omits
|
|
732
783
|
// them (real SubagentStop payloads carry neither); tier is derived from the model.
|
|
733
784
|
const sessionMeta = readActiveSessionMeta(cwd);
|
|
734
|
-
// The trace
|
|
735
|
-
//
|
|
785
|
+
// The trace logger's day-scoped auto-session names no command (only a session opened
|
|
786
|
+
// by `optimize trace init` or focus mode does), so the parent transcript is the
|
|
787
|
+
// fallback that makes command attribution work in ordinary use — see
|
|
736
788
|
// readCommandFromTranscript.
|
|
737
789
|
const command = data.command || sessionMeta.command || readCommandFromTranscript(data.transcript_path) || null;
|
|
738
|
-
// The
|
|
739
|
-
//
|
|
740
|
-
//
|
|
790
|
+
// The day-scoped auto-session names no phase either, so state.md is the fallback
|
|
791
|
+
// that makes phase attribution work in ordinary use, not only in a session that
|
|
792
|
+
// was opened with one.
|
|
741
793
|
const phase = data.phase || sessionMeta.phase || readCurrentPhase(cwd) || null;
|
|
742
794
|
|
|
743
795
|
const record = {
|
|
@@ -1003,4 +1055,4 @@ if (require.main === module) {
|
|
|
1003
1055
|
});
|
|
1004
1056
|
}
|
|
1005
1057
|
|
|
1006
|
-
module.exports = { deferToClaudeRegistration, buildCostRecord, appendRecord, readUsageFromTranscript, addCacheTtlSplit, resolveAgentTranscript, readCursor, writeCursor, isPanProject, hasPlanningTree, readCommandFromTranscript, isSessionStale, PAN_RUNTIME_DIRS, METRICS_DIR, TOKENS_FILE, CURSOR_FILE, SLICE_MAX, MAX_CURSOR_KEYS };
|
|
1058
|
+
module.exports = { deferToClaudeRegistration, normalizeHookPayload, isHostInternalAgent, buildCostRecord, appendRecord, readUsageFromTranscript, addCacheTtlSplit, resolveAgentTranscript, readCursor, writeCursor, isPanProject, hasPlanningTree, readCommandFromTranscript, isSessionStale, PAN_RUNTIME_DIRS, METRICS_DIR, TOKENS_FILE, CURSOR_FILE, SLICE_MAX, MAX_CURSOR_KEYS };
|
|
@@ -37,8 +37,8 @@ const path = require('path');
|
|
|
37
37
|
* Which planning tree this hook acts on.
|
|
38
38
|
*
|
|
39
39
|
* Mirrors pan-wizard-core/bin/lib/planning-root.cjs, which hooks cannot require
|
|
40
|
-
* (they are standalone and run inside the host runtime).
|
|
41
|
-
* identical copy — if the CLI is pointed at a track while a hook still writes to
|
|
40
|
+
* (they are standalone and run inside the host runtime). Every PAN hook that
|
|
41
|
+
* reads the planning tree carries an identical copy — if the CLI is pointed at a track while a hook still writes to
|
|
42
42
|
* `.planning/`, that tree's telemetry lands in the wrong place.
|
|
43
43
|
*
|
|
44
44
|
* Env only — a hook gets no argv. A value that escapes the project root is
|
|
@@ -116,8 +116,8 @@ const crypto = require('crypto');
|
|
|
116
116
|
* Which planning tree this hook acts on.
|
|
117
117
|
*
|
|
118
118
|
* Mirrors pan-wizard-core/bin/lib/planning-root.cjs, which hooks cannot require
|
|
119
|
-
* (they are standalone and run inside the host runtime).
|
|
120
|
-
* identical copy — if the CLI is pointed at a track while a hook still writes to
|
|
119
|
+
* (they are standalone and run inside the host runtime). Every PAN hook that
|
|
120
|
+
* reads the planning tree carries an identical copy — if the CLI is pointed at a track while a hook still writes to
|
|
121
121
|
* `.planning/`, that tree's telemetry lands in the wrong place.
|
|
122
122
|
*
|
|
123
123
|
* Env only — a hook gets no argv. A value that escapes the project root is
|
|
@@ -9,13 +9,16 @@
|
|
|
9
9
|
// trace artifacts in every repo the user opens.
|
|
10
10
|
//
|
|
11
11
|
// Events logged per subagent:
|
|
12
|
-
// - completion: agent finished, tokens used, exit status
|
|
12
|
+
// - completion: agent finished, tokens used, exit status, tool calls/failures
|
|
13
13
|
// - redundancy: detected when the same agent type ran twice in this session
|
|
14
14
|
// with similar token counts (rough heuristic for repeated work)
|
|
15
|
+
// - tool_error: a failed tool call inside the subagent's own transcript
|
|
16
|
+
// (evidence loop, ADR-0049), grouped per spawn, redacted, capped
|
|
15
17
|
//
|
|
16
18
|
// Errors are swallowed — this hook must never block the main agent loop.
|
|
17
19
|
|
|
18
20
|
const fs = require('fs');
|
|
21
|
+
const os = require('os');
|
|
19
22
|
const path = require('path');
|
|
20
23
|
|
|
21
24
|
// ─── R39: one run per hook when Claude and Copilot share a project ───────────
|
|
@@ -61,8 +64,8 @@ const crypto = require('crypto');
|
|
|
61
64
|
* Which planning tree this hook acts on.
|
|
62
65
|
*
|
|
63
66
|
* Mirrors pan-wizard-core/bin/lib/planning-root.cjs, which hooks cannot require
|
|
64
|
-
* (they are standalone and run inside the host runtime).
|
|
65
|
-
* identical copy — if the CLI is pointed at a track while a hook still writes to
|
|
67
|
+
* (they are standalone and run inside the host runtime). Every PAN hook that
|
|
68
|
+
* reads the planning tree carries an identical copy — if the CLI is pointed at a track while a hook still writes to
|
|
66
69
|
* `.planning/`, that tree's telemetry lands in the wrong place.
|
|
67
70
|
*
|
|
68
71
|
* Env only — a hook gets no argv. A value that escapes the project root is
|
|
@@ -144,11 +147,13 @@ const TRACES_DIR = 'traces';
|
|
|
144
147
|
const CURRENT_SESSION_FILE = 'current-session';
|
|
145
148
|
const TRACE_EVENT_FILE = 'trace.jsonl';
|
|
146
149
|
|
|
147
|
-
// Trace event schema version
|
|
148
|
-
//
|
|
149
|
-
// `
|
|
150
|
-
//
|
|
151
|
-
|
|
150
|
+
// Trace event schema version. v3 added the per-invocation `event_sig`
|
|
151
|
+
// discriminator to the completion event's context; v4 added `agent_id` and the
|
|
152
|
+
// `agent-transcript` token source (both in lockstep with pan-cost-logger.js, whose
|
|
153
|
+
// ledger rows gained the same fields). v5 is this hook's alone: `tool_calls` and
|
|
154
|
+
// `tool_errors` on the completion and the `error/tool_error` event. The cost
|
|
155
|
+
// ledger's rows did not change, so pan-cost-logger stays at 4.
|
|
156
|
+
const SCHEMA_V = 5;
|
|
152
157
|
|
|
153
158
|
// YYYYMMDD stamp for a Date (the day-scope of an auto-session id).
|
|
154
159
|
function dayStamp(d) {
|
|
@@ -384,10 +389,11 @@ const MAX_SEEN_TRANSCRIPTS = 16;
|
|
|
384
389
|
// • `model` and `phase` came out null in those recorded rows — the payload
|
|
385
390
|
// carried neither; `usage` is absent entirely in headless mode
|
|
386
391
|
// (docs/HOOKS.md, P-1805).
|
|
387
|
-
// •
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
392
|
+
// • Claude Code 2.1.280 sends `agent_id`, a per-spawn id (captured 2026-09-29 in
|
|
393
|
+
// tests/fixtures/hooks/subagent-stop-claude.json); no other host's payload has
|
|
394
|
+
// been captured with one.
|
|
395
|
+
// So for two CONCURRENT SAME-TYPE siblings a varying payload field is confirmed only
|
|
396
|
+
// on Claude Code: where the host supplies one, both spawns are admitted; where it
|
|
391
397
|
// supplies none the payloads are the same bytes, hence indistinguishable from a
|
|
392
398
|
// re-fire, and the second stays suppressed.
|
|
393
399
|
//
|
|
@@ -435,10 +441,13 @@ const MAX_SEEN_TRANSCRIPTS = 16;
|
|
|
435
441
|
// reinstates the duplicate-record bug. It is the one UNDER-count residual —
|
|
436
442
|
// distinct from the evicted-marker phantom (over-count) and from N29.
|
|
437
443
|
//
|
|
438
|
-
// Null when unserializable → fail open.
|
|
444
|
+
// Null when unserializable → fail open. SHA-256 cut to the 40 hex characters the
|
|
445
|
+
// SHA-1 it replaced produced, so persisted `event_sig` values keep their shape: a
|
|
446
|
+
// dedup key, not a security boundary, but CodeQL rightly flags SHA-1 over a payload
|
|
447
|
+
// that carries a session id (2026-09-29).
|
|
439
448
|
function eventSignature(data) {
|
|
440
449
|
try {
|
|
441
|
-
return crypto.createHash('
|
|
450
|
+
return crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex').slice(0, 40);
|
|
442
451
|
} catch { return null; }
|
|
443
452
|
}
|
|
444
453
|
|
|
@@ -591,6 +600,27 @@ function resolveAgentTranscript(data) {
|
|
|
591
600
|
}
|
|
592
601
|
}
|
|
593
602
|
|
|
603
|
+
// Host-internal agents (measured 2026-09-28). Claude Code runs helper agents of its own:
|
|
604
|
+
// the compaction summariser, and one at almost every turn end of an interactive session.
|
|
605
|
+
// It fires SubagentStop for them with an `agent_id`, an EMPTY `agent_type`, and an
|
|
606
|
+
// `agent_transcript_path` it never writes. That shape was captured from a real `/compact`
|
|
607
|
+
// on Claude Code 2.1.280 (tests/fixtures/hooks/subagent-stop-internal-claude.json). Both
|
|
608
|
+
// loggers booked each as an `unknown` zero-token spawn: 181 rows in three field projects
|
|
609
|
+
// in eleven days, skewing optimize learn's agent stats and the ledger's spawn counts.
|
|
610
|
+
// The host documents `agent_type` as present whenever a hook fires inside a subagent, so
|
|
611
|
+
// a named instance with no type and no transcript is not a spawn anyone asked for, and
|
|
612
|
+
// nothing is recorded. A NAMED spawn whose file is late or missing keeps its zero-token
|
|
613
|
+
// row (N17/N25); a payload with no agent instance at all keeps the parent-slice path.
|
|
614
|
+
// Identical in pan-cost-logger.js and pan-trace-logger.js (tests/trace-logger.test.cjs
|
|
615
|
+
// pins the copies).
|
|
616
|
+
function isHostInternalAgent(data, agentTranscript) {
|
|
617
|
+
if (!data || typeof data !== 'object' || agentTranscript) return false;
|
|
618
|
+
const typed = [data.agent_type, data.subagent_type].some((v) => typeof v === 'string' && v.trim() !== '');
|
|
619
|
+
if (typed) return false;
|
|
620
|
+
return (typeof data.agent_id === 'string' && AGENT_ID_SAFE.test(data.agent_id))
|
|
621
|
+
|| (typeof data.agent_transcript_path === 'string' && data.agent_transcript_path !== '');
|
|
622
|
+
}
|
|
623
|
+
|
|
594
624
|
// Slice ceilings — a sum over one subagent's conversation, so above a single
|
|
595
625
|
// call's PLAUSIBLE_MAX; cache_read and output mirror cost.cjs isSuspectRecord's
|
|
596
626
|
// absolute limits, input is a hook-only sanity ceiling (same values as
|
|
@@ -697,6 +727,191 @@ function readUsageFromTranscript(transcriptPath, sessionId, sinceLine = 0) {
|
|
|
697
727
|
return totals;
|
|
698
728
|
}
|
|
699
729
|
|
|
730
|
+
// ─── Tool-failure capture (evidence loop EL-9, ADR-0049) ─────────────────────
|
|
731
|
+
// The optimisation loop learns from failures, and until this capture the only
|
|
732
|
+
// failures it saw were the ones an agent chose to report through `optimize trace
|
|
733
|
+
// log`. Across the field projects that was none, while the subagents' own
|
|
734
|
+
// transcripts held a failed tool call in roughly one spawn in four (measured
|
|
735
|
+
// 2026-09-28). The hook already reads the subagent's own transcript for tokens, so
|
|
736
|
+
// it records the failed tool results in that same slice. It does so only on the
|
|
737
|
+
// agent-transcript path: a slice of the shared parent transcript would book the
|
|
738
|
+
// session's failures to whichever spawn stopped.
|
|
739
|
+
|
|
740
|
+
const MAX_FAILURE_EVENTS = 10;
|
|
741
|
+
const ERROR_MESSAGE_MAX = 160;
|
|
742
|
+
// First match wins; `exit_code` is decided before these, from the Bash result shape.
|
|
743
|
+
const ERROR_CLASSES = [
|
|
744
|
+
['permission_denied', /\bPermission (to use|for this command)\b|\bwas denied\b|\bdenied by\b/i],
|
|
745
|
+
['edit_precondition', /String to replace not found|has not been read|modified since (it was )?read|must read/i],
|
|
746
|
+
['not_found', /does not exist|no such file|\bENOENT\b|\bnot found\b/i],
|
|
747
|
+
['network_or_timeout', /getaddrinfo|ECONNREFUSED|ECONNRESET|ETIMEDOUT|fetch failed|timed? ?out|socket hang up/i],
|
|
748
|
+
['user_rejected', /\binterrupted\b|\brejected\b|doesn't want to proceed/i],
|
|
749
|
+
['tool_input', /InputValidationError|Invalid tool parameters|invalid input/i],
|
|
750
|
+
];
|
|
751
|
+
const TOKEN_RE = /\b(?:sk-ant-[A-Za-z0-9_-]{10,}|sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|xox[abprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{30,}|eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,})/g;
|
|
752
|
+
const SECRET_KV_RE = /\b(api[_-]?key|access[_-]?token|auth[_-]?token|refresh[_-]?token|client[_-]?secret|token|secret|password|passwd|pwd|authorization)(["']?\s*[:=]\s*)(?!Bearer\s|<redacted>)("[^"]*"|'[^']*'|[^\s,;&]+)/gi;
|
|
753
|
+
|
|
754
|
+
/** Records of a transcript past `sinceLine` (the same record count the token cursor uses). */
|
|
755
|
+
function readTranscriptSlice(transcriptPath, sinceLine = 0) {
|
|
756
|
+
let raw;
|
|
757
|
+
try { raw = fs.readFileSync(transcriptPath, 'utf-8'); } catch { return []; }
|
|
758
|
+
const out = [];
|
|
759
|
+
let seen = 0;
|
|
760
|
+
for (const line of raw.split('\n')) {
|
|
761
|
+
if (!line) continue;
|
|
762
|
+
seen++;
|
|
763
|
+
if (seen <= sinceLine) continue;
|
|
764
|
+
try { out.push(JSON.parse(line)); } catch { /* a torn line is skipped, as in the token read */ }
|
|
765
|
+
}
|
|
766
|
+
return out;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Error text a trace file may hold. Trace files are committed in some projects, so
|
|
771
|
+
* the message is redacted before it is stored:
|
|
772
|
+
* - bearer tokens, key/value secrets and known token prefixes;
|
|
773
|
+
* - long mixed letter-and-digit runs;
|
|
774
|
+
* - URL query strings;
|
|
775
|
+
* - the project directory, which becomes . (portable, and shorter);
|
|
776
|
+
* - the user's home directory, which becomes ~.
|
|
777
|
+
* The message is also capped at ERROR_MESSAGE_MAX characters.
|
|
778
|
+
*/
|
|
779
|
+
function redactErrorText(text, home = os.homedir(), projectDir = null) {
|
|
780
|
+
let t = String(text == null ? '' : text).replace(/\u001b\[[0-9;]*[A-Za-z]/g, '');
|
|
781
|
+
t = t.replace(/\bBearer\s+[A-Za-z0-9._~+\/=-]+/gi, 'Bearer <redacted>');
|
|
782
|
+
t = t.replace(SECRET_KV_RE, '$1$2<redacted>');
|
|
783
|
+
t = t.replace(TOKEN_RE, '<redacted>');
|
|
784
|
+
t = t.replace(/[A-Za-z0-9+=_-]{32,}/g, (run) => (/\d/.test(run) && /[A-Za-z]/.test(run) ? '<redacted>' : run));
|
|
785
|
+
t = t.replace(/(https?:\/\/[^\s?#]+)\?[^\s#]*/gi, '$1?<redacted>');
|
|
786
|
+
if (typeof projectDir === 'string' && projectDir.length > 3) {
|
|
787
|
+
for (const d of new Set([projectDir, projectDir.replace(/\\/g, '/'), projectDir.replace(/\//g, '\\')])) {
|
|
788
|
+
t = t.split(d).join('.');
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if (home && home.length > 3) {
|
|
792
|
+
for (const h of new Set([home, home.replace(/\\/g, '/'), home.replace(/\//g, '\\')])) {
|
|
793
|
+
t = t.split(h).join('~');
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
t = t.replace(/[A-Za-z]:[\\/]+Users[\\/]+[^\\/\s]+/gi, '~').replace(/\/(?:home|Users)\/[^/\s]+/g, '~');
|
|
797
|
+
t = t.replace(/\s+/g, ' ').trim();
|
|
798
|
+
return t.length > ERROR_MESSAGE_MAX ? t.slice(0, ERROR_MESSAGE_MAX - 1) + '…' : t;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Class, exit code and the informative line of a failed tool result. A shell failure
|
|
803
|
+
* (Bash, or PowerShell on Windows) opens with `Exit code <n>`, and a line after it
|
|
804
|
+
* says what failed. npm/yarn/pnpm first echo the script they run (`> pkg@1.0.0 test`,
|
|
805
|
+
* `> node --test`), which says nothing about the failure, so those lines are skipped.
|
|
806
|
+
* The first real run on 2026-09-28 recorded exactly that echo as its message.
|
|
807
|
+
*/
|
|
808
|
+
function classifyToolError(text) {
|
|
809
|
+
const lines = String(text == null ? '' : text).replace(/<\/?tool_use_error>/g, '').split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
810
|
+
const first = lines[0] || '';
|
|
811
|
+
const exit = /^Exit code (\d+)$/.exec(first);
|
|
812
|
+
if (exit) {
|
|
813
|
+
const code = Number(exit[1]);
|
|
814
|
+
const said = lines.slice(1).find((l) => !/^> /.test(l));
|
|
815
|
+
return { errorClass: 'exit_code', exitCode: code, message: said || `exit code ${code}` };
|
|
816
|
+
}
|
|
817
|
+
const probe = lines.slice(0, 3).join(' ');
|
|
818
|
+
for (const [cls, re] of ERROR_CLASSES) if (re.test(probe)) return { errorClass: cls, exitCode: null, message: first };
|
|
819
|
+
return { errorClass: 'other', exitCode: null, message: first || '(no message)' };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* The failed tool calls of one transcript slice, grouped by (tool, class, message).
|
|
824
|
+
* A retry loop repeating one failure is one group with a count, not a flood.
|
|
825
|
+
* @returns {{toolCalls: number, toolErrors: number, groups: Object[], omitted: number}}
|
|
826
|
+
*/
|
|
827
|
+
function extractToolFailures(records, home = os.homedir(), projectDir = null) {
|
|
828
|
+
const names = new Map();
|
|
829
|
+
let toolCalls = 0;
|
|
830
|
+
for (const r of records) {
|
|
831
|
+
const content = r && r.message && r.message.content;
|
|
832
|
+
if (!Array.isArray(content)) continue;
|
|
833
|
+
for (const b of content) {
|
|
834
|
+
if (b && b.type === 'tool_use' && typeof b.id === 'string') {
|
|
835
|
+
names.set(b.id, typeof b.name === 'string' && b.name ? b.name.slice(0, 64) : 'unknown');
|
|
836
|
+
toolCalls++;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
const groups = new Map();
|
|
841
|
+
let toolErrors = 0;
|
|
842
|
+
for (const r of records) {
|
|
843
|
+
const content = r && r.message && r.message.content;
|
|
844
|
+
if (!Array.isArray(content)) continue;
|
|
845
|
+
for (const b of content) {
|
|
846
|
+
if (!b || b.type !== 'tool_result' || b.is_error !== true) continue;
|
|
847
|
+
toolErrors++;
|
|
848
|
+
const text = Array.isArray(b.content)
|
|
849
|
+
? b.content.filter((x) => x && x.type === 'text' && typeof x.text === 'string').map((x) => x.text).join('\n')
|
|
850
|
+
: (typeof b.content === 'string' ? b.content : '');
|
|
851
|
+
const tool = names.get(b.tool_use_id) || 'unknown';
|
|
852
|
+
const { errorClass, exitCode, message } = classifyToolError(text);
|
|
853
|
+
const redacted = redactErrorText(message, home, projectDir);
|
|
854
|
+
const shape = redacted.replace(/\d+/g, 'N');
|
|
855
|
+
const key = `${tool}\u0000${errorClass}\u0000${shape}`;
|
|
856
|
+
const g = groups.get(key);
|
|
857
|
+
if (g) g.count++;
|
|
858
|
+
else groups.set(key, { tool, errorClass, exitCode, message: redacted, messageSig: crypto.createHash('sha256').update(shape).digest('hex').slice(0, 12), count: 1 });
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const all = [...groups.values()];
|
|
862
|
+
return { toolCalls, toolErrors, groups: all.slice(0, MAX_FAILURE_EVENTS), omitted: Math.max(0, all.length - MAX_FAILURE_EVENTS) };
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* `execution.error_pattern_learning: false` in the project's config turns the
|
|
867
|
+
* capture off. The key shipped as "reserved" and was never read; this is its
|
|
868
|
+
* meaning. Default on; an unreadable config does not switch it off.
|
|
869
|
+
*/
|
|
870
|
+
function errorCaptureEnabled(cwd) {
|
|
871
|
+
if (!cwd) return true;
|
|
872
|
+
try {
|
|
873
|
+
const cfg = JSON.parse(fs.readFileSync(planningPath(cwd, 'config.json'), 'utf-8'));
|
|
874
|
+
const nested = cfg && cfg.execution && typeof cfg.execution === 'object' ? cfg.execution.error_pattern_learning : undefined;
|
|
875
|
+
return !(nested === false || (cfg && cfg['execution.error_pattern_learning'] === false));
|
|
876
|
+
} catch {
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/** The agent type the host wrote beside a per-agent transcript (`agent-<id>.meta.json`). */
|
|
882
|
+
function agentTypeFromMeta(agentTranscriptPath) {
|
|
883
|
+
try {
|
|
884
|
+
const meta = JSON.parse(fs.readFileSync(agentTranscriptPath.replace(/\.jsonl$/, '.meta.json'), 'utf-8'));
|
|
885
|
+
const type = meta && typeof meta.agentType === 'string' ? meta.agentType.trim() : '';
|
|
886
|
+
return /^[A-Za-z0-9][A-Za-z0-9:._-]{0,63}$/.test(type) ? type : null;
|
|
887
|
+
} catch {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Copilot CLI's camelCase hook payload (docs.github.com/en/copilot/reference/hooks-reference,
|
|
893
|
+
// read 2026-09-28; documented, not yet observed live) names every field differently from
|
|
894
|
+
// Claude's snake_case one, so this hook read no session, transcript or agent from it and
|
|
895
|
+
// booked every Copilot spawn as `unknown`. Its `agentType` is the KIND of agent (built-in
|
|
896
|
+
// or custom); `agentName` is the configured name PAN's agents carry, so the name wins.
|
|
897
|
+
// Claude payloads carry none of these keys and pass through unchanged. Identical in
|
|
898
|
+
// pan-cost-logger.js and pan-trace-logger.js (tests/copilot-payload.test.cjs pins both).
|
|
899
|
+
function normalizeHookPayload(data) {
|
|
900
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return data;
|
|
901
|
+
const pick = (...keys) => {
|
|
902
|
+
for (const k of keys) if (typeof data[k] === 'string' && data[k]) return data[k];
|
|
903
|
+
return undefined;
|
|
904
|
+
};
|
|
905
|
+
const out = { ...data };
|
|
906
|
+
if (out.session_id === undefined && pick('sessionId')) out.session_id = pick('sessionId');
|
|
907
|
+
if (out.transcript_path === undefined && pick('transcriptPath')) out.transcript_path = pick('transcriptPath');
|
|
908
|
+
if (out.agent_id === undefined && pick('agentId')) out.agent_id = pick('agentId');
|
|
909
|
+
const name = pick('agent_name', 'agentName');
|
|
910
|
+
if (name) out.agent_type = name;
|
|
911
|
+
else if (out.agent_type === undefined && pick('agentType')) out.agent_type = pick('agentType');
|
|
912
|
+
return out;
|
|
913
|
+
}
|
|
914
|
+
|
|
700
915
|
/**
|
|
701
916
|
* Build trace event(s) from a SubagentStop payload.
|
|
702
917
|
*
|
|
@@ -713,10 +928,11 @@ function readUsageFromTranscript(transcriptPath, sessionId, sinceLine = 0) {
|
|
|
713
928
|
*/
|
|
714
929
|
function buildTraceEvents(data, sessionId, cwd) {
|
|
715
930
|
if (!data || typeof data !== 'object') return [];
|
|
931
|
+
data = normalizeHookPayload(data);
|
|
716
932
|
if (data.hook_event_name && data.hook_event_name !== 'SubagentStop') return [];
|
|
717
933
|
|
|
718
934
|
const ts = new Date().toISOString();
|
|
719
|
-
|
|
935
|
+
let agent = data.agent_type || data.subagent_type || 'unknown';
|
|
720
936
|
// This event's per-invocation identity, hashed once and used by BOTH layers:
|
|
721
937
|
// the seen-event marker below and the completion's `event_sig` context field.
|
|
722
938
|
// buildTraceEvents never mutates `data`, so hoisting the hash here yields the
|
|
@@ -737,6 +953,9 @@ function buildTraceEvents(data, sessionId, cwd) {
|
|
|
737
953
|
// `transcript` for a slice of the shared parent transcript (the fallback when
|
|
738
954
|
// the host names no agent), `usage-fallback` for the payload's own counters.
|
|
739
955
|
const agentTranscript = resolveAgentTranscript(data);
|
|
956
|
+
if (isHostInternalAgent(data, agentTranscript)) return []; // the host's own helper, not a spawn
|
|
957
|
+
// A payload without an agent type still names it in the host's meta file.
|
|
958
|
+
if (agent === 'unknown' && agentTranscript) agent = agentTypeFromMeta(agentTranscript) || agent;
|
|
740
959
|
const parentPath = typeof data.transcript_path === 'string' && data.transcript_path ? data.transcript_path : null;
|
|
741
960
|
// A named agent whose file is not there consumes nothing (see pan-cost-logger:
|
|
742
961
|
// the parent slice would be the whole session booked to one spawn).
|
|
@@ -749,6 +968,7 @@ function buildTraceEvents(data, sessionId, cwd) {
|
|
|
749
968
|
: sliceSource ? 'transcript' : 'usage-fallback';
|
|
750
969
|
const agentId = typeof data.agent_id === 'string' && data.agent_id ? data.agent_id : null;
|
|
751
970
|
let clamped = false;
|
|
971
|
+
let failures = null;
|
|
752
972
|
if (sliceSource || agentFileMissing) {
|
|
753
973
|
const keyPath = sliceSource || parentPath;
|
|
754
974
|
const cursor = readTraceCursor(cwd);
|
|
@@ -765,6 +985,11 @@ function buildTraceEvents(data, sessionId, cwd) {
|
|
|
765
985
|
clamped = rawIn > SLICE_MAX.input || rawOut > SLICE_MAX.output || rawCr > SLICE_MAX.cache_read;
|
|
766
986
|
durationMs = durationFromSpan(fromTranscript.first_ts, fromTranscript.last_ts); // as measured
|
|
767
987
|
if (!model) model = fromTranscript.model;
|
|
988
|
+
// Failures come from the SAME slice the tokens did (records past the cursor), so
|
|
989
|
+
// a resumed agent's earlier failures are not counted again.
|
|
990
|
+
if (tokenSource === 'agent-transcript' && fromTranscript.lineCount > since && errorCaptureEnabled(cwd)) {
|
|
991
|
+
failures = extractToolFailures(readTranscriptSlice(sliceSource, since), os.homedir(), cwd ? path.resolve(cwd) : null);
|
|
992
|
+
}
|
|
768
993
|
if (cwd && fromTranscript.lineCount > since) {
|
|
769
994
|
// A real slice. Advance the cursor and remember this event's signature
|
|
770
995
|
// (N17/N25) so a later empty-slice event can tell its re-fire from a
|
|
@@ -817,6 +1042,10 @@ function buildTraceEvents(data, sessionId, cwd) {
|
|
|
817
1042
|
// path otherwise bypasses).
|
|
818
1043
|
const sessionMeta = cwd ? readSessionMetaById(cwd, sessionId) : {};
|
|
819
1044
|
const phase = data.phase || sessionMeta.phase || null;
|
|
1045
|
+
// The day-scoped auto-session names no command (only a session opened by
|
|
1046
|
+
// `optimize trace init` or focus mode does); the parent transcript names it
|
|
1047
|
+
// instead (see readCommandFromTranscript).
|
|
1048
|
+
const command = data.command || sessionMeta.command || readCommandFromTranscript(data.transcript_path) || null;
|
|
820
1049
|
|
|
821
1050
|
const events = [];
|
|
822
1051
|
|
|
@@ -832,9 +1061,7 @@ function buildTraceEvents(data, sessionId, cwd) {
|
|
|
832
1061
|
description: `${agent} completed`,
|
|
833
1062
|
context: {
|
|
834
1063
|
model,
|
|
835
|
-
|
|
836
|
-
// names it instead (see readCommandFromTranscript).
|
|
837
|
-
command: data.command || sessionMeta.command || readCommandFromTranscript(data.transcript_path) || null,
|
|
1064
|
+
command,
|
|
838
1065
|
agent_id: agentId,
|
|
839
1066
|
input_tokens: inputTokens,
|
|
840
1067
|
output_tokens: outputTokens,
|
|
@@ -844,6 +1071,10 @@ function buildTraceEvents(data, sessionId, cwd) {
|
|
|
844
1071
|
exit_code: data.exit_code || 0,
|
|
845
1072
|
token_source: tokenSource,
|
|
846
1073
|
clamped,
|
|
1074
|
+
// Tool calls and failed tool results in this spawn's own transcript slice;
|
|
1075
|
+
// null whenever the slice is not the agent's own (nothing is attributable).
|
|
1076
|
+
tool_calls: failures ? failures.toolCalls : null,
|
|
1077
|
+
tool_errors: failures ? failures.toolErrors : null,
|
|
847
1078
|
// This spawn's per-invocation discriminator: the event signature,
|
|
848
1079
|
// persisted. The signature was already hashed for the seen-event marker
|
|
849
1080
|
// but never written into the event, so two parallel same-type siblings
|
|
@@ -887,6 +1118,27 @@ function buildTraceEvents(data, sessionId, cwd) {
|
|
|
887
1118
|
});
|
|
888
1119
|
}
|
|
889
1120
|
|
|
1121
|
+
// One event per distinct failure (tool, class, message), with its count. The
|
|
1122
|
+
// completion's tool_errors keeps the total when more groups exist than are written.
|
|
1123
|
+
if (failures) {
|
|
1124
|
+
for (const g of failures.groups) {
|
|
1125
|
+
events.push({
|
|
1126
|
+
v: SCHEMA_V,
|
|
1127
|
+
ts,
|
|
1128
|
+
session: sessionId,
|
|
1129
|
+
agent,
|
|
1130
|
+
phase,
|
|
1131
|
+
type: 'error',
|
|
1132
|
+
category: 'tool_error',
|
|
1133
|
+
description: `${agent}: ${g.tool} failed (${g.errorClass})${g.count > 1 ? ` x${g.count}` : ''}`,
|
|
1134
|
+
context: { tool: g.tool, error_class: g.errorClass, exit_code: g.exitCode, message: g.message, message_sig: g.messageSig, count: g.count, command, agent_id: agentId },
|
|
1135
|
+
impact: 'minor',
|
|
1136
|
+
correction: null,
|
|
1137
|
+
tokens_wasted: null,
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
890
1142
|
return events;
|
|
891
1143
|
}
|
|
892
1144
|
|
|
@@ -976,6 +1228,10 @@ if (require.main === module) {
|
|
|
976
1228
|
// we don't create .planning/ optimization + trace artifacts in them. The tree
|
|
977
1229
|
// must already exist — see hasPlanningTree (field sweep 2026-09-17).
|
|
978
1230
|
if (!hasPlanningTree(cwd)) return;
|
|
1231
|
+
// A host-internal helper agent records nothing, so it must not mint the day's
|
|
1232
|
+
// session either (buildTraceEvents repeats the check for direct callers).
|
|
1233
|
+
const normalized = normalizeHookPayload(data);
|
|
1234
|
+
if (isHostInternalAgent(normalized, resolveAgentTranscript(normalized))) return;
|
|
979
1235
|
// In a PAN project, ensure a session exists — creates a day-scoped
|
|
980
1236
|
// auto-session if needed.
|
|
981
1237
|
const sessionId = ensureSessionId(cwd);
|
|
@@ -989,6 +1245,8 @@ if (require.main === module) {
|
|
|
989
1245
|
|
|
990
1246
|
module.exports = {
|
|
991
1247
|
deferToClaudeRegistration,
|
|
1248
|
+
normalizeHookPayload,
|
|
1249
|
+
isHostInternalAgent,
|
|
992
1250
|
buildTraceEvents,
|
|
993
1251
|
appendTraceEvents,
|
|
994
1252
|
resolveAgentTranscript,
|
|
@@ -999,6 +1257,13 @@ module.exports = {
|
|
|
999
1257
|
PAN_RUNTIME_DIRS,
|
|
1000
1258
|
readCommandFromTranscript,
|
|
1001
1259
|
isSessionStale,
|
|
1260
|
+
readTranscriptSlice,
|
|
1261
|
+
extractToolFailures,
|
|
1262
|
+
classifyToolError,
|
|
1263
|
+
redactErrorText,
|
|
1264
|
+
errorCaptureEnabled,
|
|
1265
|
+
agentTypeFromMeta,
|
|
1266
|
+
MAX_FAILURE_EVENTS,
|
|
1002
1267
|
PLANNING_DIR,
|
|
1003
1268
|
OPTIMIZE_DIR,
|
|
1004
1269
|
TRACES_DIR,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pan-wizard",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.32.0",
|
|
4
4
|
"description": "Command a bot army for your codebase: a reasoning-tier Mission Control delegates whole-project goals to specialist squads and ships behind a human merge gate. Five AI CLIs, zero context rot.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"pan-wizard": "bin/install.js"
|
|
@@ -5,7 +5,7 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { safeReadFile, loadConfig, isGitIgnored, isGitRepo, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, resolveModelDetailed, resolveEffortInternal, detectProvider, resolveTierToModel, estimateCostMultiplier, MODEL_PROFILES, output, error, findPhaseInternal, scanPendingTodos, toPosix } = require('./core.cjs');
|
|
7
7
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
8
|
-
const { PHASES_DIR, MILESTONES_DIR, QUICK_DIR, STATE_FILE, ROADMAP_FILE, PROJECT_FILE, PATTERNS_FILE, SESSION_HISTORY_FILE, LEARNINGS_FILE, CONTEXT_SUFFIX, UAT_SUFFIX, VERIFICATION_SUFFIX, isPlanFile, isSummaryFile, ARCHIVE_DIR_RE, PHASE_DIR_RE, CONTEXT_WINDOW, WARNING_THRESHOLD, CRITICAL_THRESHOLD, VALID_COMMIT_TYPES, DEFAULT_SENSITIVE_PATTERNS } = require('./constants.cjs');
|
|
8
|
+
const { PHASES_DIR, MILESTONES_DIR, QUICK_DIR, STATE_FILE, ROADMAP_FILE, PROJECT_FILE, PATTERNS_FILE, SESSION_HISTORY_FILE, LEARNINGS_FILE, CONTEXT_SUFFIX, UAT_SUFFIX, VERIFICATION_SUFFIX, isPlanFile, isSummaryFile, ARCHIVE_DIR_RE, PHASE_DIR_RE, CONTEXT_WINDOW, WARNING_THRESHOLD, CRITICAL_THRESHOLD, VALID_COMMIT_TYPES, DEFAULT_SENSITIVE_PATTERNS, STATE_CONTRACT } = require('./constants.cjs');
|
|
9
9
|
const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles, planningRel } = require('./utils.cjs');
|
|
10
10
|
const { estimateTokens } = require('./context-budget.cjs');
|
|
11
11
|
const { collectPhaseSummaries, readErrorPatterns, appendErrorPattern, appendSessionSummary, parseLearnings, formatLearningEntry, cmdLearningsExtract, cmdLearningsList, cmdLearningsPrune } = require('./commands-learnings.cjs');
|
|
@@ -692,6 +692,7 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
692
692
|
} else {
|
|
693
693
|
// JSON format
|
|
694
694
|
output({
|
|
695
|
+
contract: STATE_CONTRACT,
|
|
695
696
|
milestone_version: milestone.version,
|
|
696
697
|
milestone_name: milestone.name,
|
|
697
698
|
phases,
|