pan-wizard 2.9.1 → 3.4.1
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 +8 -8
- package/agents/pan-conductor.md +189 -0
- package/agents/pan-counterfactual.md +112 -0
- package/agents/pan-debugger.md +15 -1
- package/agents/pan-document_code.md +21 -0
- package/agents/pan-executor.md +16 -0
- package/agents/pan-hardener.md +113 -0
- package/agents/pan-integration-checker.md +2 -0
- package/agents/pan-knowledge.md +81 -0
- package/agents/pan-meta-reviewer.md +91 -0
- package/agents/pan-plan-checker.md +2 -0
- package/agents/pan-previewer.md +98 -0
- package/agents/pan-project-researcher.md +4 -4
- package/agents/pan-reviewer.md +2 -0
- package/agents/pan-verifier.md +2 -0
- package/bin/install-lib.cjs +197 -0
- package/bin/install.js +1999 -1959
- package/commands/pan/cost.md +132 -0
- package/commands/pan/exec-phase.md +15 -0
- package/commands/pan/focus-auto.md +18 -0
- package/commands/pan/focus-exec.md +10 -1
- package/commands/pan/knowledge.md +129 -0
- package/commands/pan/map-codebase.md +15 -0
- package/commands/pan/mcp-bridge.md +145 -0
- package/commands/pan/plan-phase.md +11 -0
- package/commands/pan/preview.md +114 -0
- package/commands/pan/profile.md +37 -0
- package/commands/pan/review-deep.md +128 -0
- package/commands/pan/verify-phase.md +11 -0
- package/commands/pan/what-if.md +146 -0
- package/hooks/dist/pan-cost-logger.js +102 -0
- package/hooks/dist/pan-statusline.js +154 -108
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/bridge.cjs +269 -0
- package/pan-wizard-core/bin/lib/bus.cjs +251 -0
- package/pan-wizard-core/bin/lib/codebase.cjs +118 -0
- package/pan-wizard-core/bin/lib/constants.cjs +39 -0
- package/pan-wizard-core/bin/lib/context-budget.cjs +27 -0
- package/pan-wizard-core/bin/lib/core.cjs +91 -6
- package/pan-wizard-core/bin/lib/cost.cjs +359 -0
- package/pan-wizard-core/bin/lib/focus.cjs +100 -2
- package/pan-wizard-core/bin/lib/init.cjs +5 -5
- package/pan-wizard-core/bin/lib/knowledge.cjs +331 -0
- package/pan-wizard-core/bin/lib/memory.cjs +252 -0
- package/pan-wizard-core/bin/lib/phase.cjs +40 -13
- package/pan-wizard-core/bin/lib/preview.cjs +480 -0
- package/pan-wizard-core/bin/lib/review-deep.cjs +280 -0
- package/pan-wizard-core/bin/lib/roadmap.cjs +4 -4
- package/pan-wizard-core/bin/lib/state.cjs +2 -2
- package/pan-wizard-core/bin/lib/verify.cjs +34 -1
- package/pan-wizard-core/bin/lib/whatif.cjs +289 -0
- package/pan-wizard-core/bin/pan-tools.cjs +239 -4
- package/pan-wizard-core/templates/playbook.md +53 -0
- package/pan-wizard-core/templates/preview-report.md +93 -0
- package/pan-wizard-core/templates/roadmap.md +24 -24
- package/pan-wizard-core/templates/state.md +12 -9
- package/pan-wizard-core/workflows/plan-phase.md +1 -1
- package/scripts/build-hooks.js +2 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bus — file-backed message channels for agent-to-agent communication
|
|
3
|
+
*
|
|
4
|
+
* Part of Spec B v2 Y-7 infrastructure (v3.0). Enables future hierarchical
|
|
5
|
+
* agent spawning (exec-phase --hierarchical, Wave 5) and inter-agent
|
|
6
|
+
* coordination (review-deep, Wave 3) without committing to an in-process
|
|
7
|
+
* IPC mechanism.
|
|
8
|
+
*
|
|
9
|
+
* Storage model:
|
|
10
|
+
* .planning/bus/<channel>.jsonl — append-only JSON Lines
|
|
11
|
+
* Each line: {ts, source, payload}
|
|
12
|
+
*
|
|
13
|
+
* Channels are created on first publish. Readers use cursor-based drain
|
|
14
|
+
* (read N lines from an offset) or consume-all drain (read + truncate).
|
|
15
|
+
*
|
|
16
|
+
* Concurrent-write safety: each publish opens the file with append flag
|
|
17
|
+
* (`a`) which the OS treats atomically for writes <PIPE_BUF on POSIX and
|
|
18
|
+
* sub-buffer writes on Windows. Entries are single lines. For parallel
|
|
19
|
+
* publishers writing large payloads, see the safety note below.
|
|
20
|
+
*
|
|
21
|
+
* Agent-name / channel-name validation: restricted to
|
|
22
|
+
* `^[a-zA-Z0-9_-]+$` to block path traversal.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const { output, error } = require('./core.cjs');
|
|
28
|
+
const { PLANNING_DIR } = require('./constants.cjs');
|
|
29
|
+
const { planningPath } = require('./utils.cjs');
|
|
30
|
+
|
|
31
|
+
const BUS_DIR = 'bus';
|
|
32
|
+
const NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
33
|
+
const DEFAULT_DRAIN_LIMIT = 1000;
|
|
34
|
+
|
|
35
|
+
function busDir(cwd) {
|
|
36
|
+
return path.join(planningPath(cwd), BUS_DIR);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function channelFile(cwd, channel) {
|
|
40
|
+
return path.join(busDir(cwd), `${channel}.jsonl`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function validateName(name, label) {
|
|
44
|
+
if (typeof name !== 'string' || !NAME_RE.test(name)) {
|
|
45
|
+
return `Invalid ${label}: ${name}. Must match ${NAME_RE}`;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function nowIso() {
|
|
51
|
+
return new Date().toISOString();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Publish a message to a channel. Creates channel file + dir if missing.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} cwd - Project root
|
|
58
|
+
* @param {string} channel - Channel name (validated)
|
|
59
|
+
* @param {*} payload - JSON-serializable payload
|
|
60
|
+
* @param {Object} [opts] - {source: string} — who sent it (agent name, command name)
|
|
61
|
+
* @returns {{published: true, ts: string, file: string, size: number}|{error: string}}
|
|
62
|
+
*/
|
|
63
|
+
function publish(cwd, channel, payload, opts) {
|
|
64
|
+
const chErr = validateName(channel, 'channel');
|
|
65
|
+
if (chErr) return { error: chErr };
|
|
66
|
+
const source = opts?.source;
|
|
67
|
+
// Treat null + undefined + empty string as "no source provided".
|
|
68
|
+
if (source !== undefined && source !== null && source !== '') {
|
|
69
|
+
const sErr = validateName(source, 'source');
|
|
70
|
+
if (sErr) return { error: sErr };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const normalizedSource = (source !== undefined && source !== null && source !== '') ? source : null;
|
|
74
|
+
let line;
|
|
75
|
+
try {
|
|
76
|
+
line = JSON.stringify({ ts: nowIso(), source: normalizedSource, payload }) + '\n';
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return { error: `payload not JSON-serializable: ${e.message}` };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
fs.mkdirSync(busDir(cwd), { recursive: true });
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return { error: `Failed to create bus dir: ${e.message}` };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const file = channelFile(cwd, channel);
|
|
88
|
+
try {
|
|
89
|
+
fs.appendFileSync(file, line, { encoding: 'utf-8' });
|
|
90
|
+
} catch (e) {
|
|
91
|
+
return { error: `Failed to append to channel ${channel}: ${e.message}` };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let size = 0;
|
|
95
|
+
try { size = fs.statSync(file).size; } catch { /* race — ignore */ }
|
|
96
|
+
|
|
97
|
+
return { published: true, ts: JSON.parse(line).ts, file, size };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse a channel file into an array of entries.
|
|
102
|
+
* @param {string} cwd - Project root
|
|
103
|
+
* @param {string} channel - Channel name
|
|
104
|
+
* @param {Object} [opts] - {offset: number, limit: number}
|
|
105
|
+
* @returns {{entries: Array, total: number, offset: number, more: boolean}|{error: string}}
|
|
106
|
+
*/
|
|
107
|
+
function readChannel(cwd, channel, opts) {
|
|
108
|
+
const chErr = validateName(channel, 'channel');
|
|
109
|
+
if (chErr) return { error: chErr };
|
|
110
|
+
const offset = Math.max(0, Number(opts?.offset) || 0);
|
|
111
|
+
const rawLimit = Number(opts?.limit);
|
|
112
|
+
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : DEFAULT_DRAIN_LIMIT;
|
|
113
|
+
|
|
114
|
+
let raw;
|
|
115
|
+
try {
|
|
116
|
+
raw = fs.readFileSync(channelFile(cwd, channel), 'utf-8');
|
|
117
|
+
} catch {
|
|
118
|
+
return { entries: [], total: 0, offset: 0, more: false };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
122
|
+
const total = lines.length;
|
|
123
|
+
const slice = lines.slice(offset, offset + limit);
|
|
124
|
+
const entries = [];
|
|
125
|
+
for (let i = 0; i < slice.length; i++) {
|
|
126
|
+
try {
|
|
127
|
+
entries.push(JSON.parse(slice[i]));
|
|
128
|
+
} catch {
|
|
129
|
+
// Skip malformed lines but don't fail the whole read.
|
|
130
|
+
entries.push({ ts: null, source: null, payload: null, malformed: true, raw: slice[i] });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { entries, total, offset, more: offset + entries.length < total };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Drain (read + optionally truncate) messages from a channel.
|
|
139
|
+
*
|
|
140
|
+
* Three drain modes:
|
|
141
|
+
* - `peek` (default): read entries, leave file untouched
|
|
142
|
+
* - `consume`: read entries, truncate file to zero bytes
|
|
143
|
+
* - `archive`: read entries, rename file to `<channel>-<ts>.archive.jsonl` so
|
|
144
|
+
* historical data is preserved while the channel restarts empty
|
|
145
|
+
*
|
|
146
|
+
* @param {string} cwd - Project root
|
|
147
|
+
* @param {string} channel - Channel name
|
|
148
|
+
* @param {Object} [opts] - {mode: 'peek'|'consume'|'archive', limit, offset}
|
|
149
|
+
* @returns {Object} Drain result
|
|
150
|
+
*/
|
|
151
|
+
function drain(cwd, channel, opts) {
|
|
152
|
+
const mode = opts?.mode || 'peek';
|
|
153
|
+
const read = readChannel(cwd, channel, opts);
|
|
154
|
+
if (read.error) return read;
|
|
155
|
+
|
|
156
|
+
if (mode === 'peek' || read.total === 0) return { ...read, mode };
|
|
157
|
+
|
|
158
|
+
const file = channelFile(cwd, channel);
|
|
159
|
+
if (mode === 'consume') {
|
|
160
|
+
try {
|
|
161
|
+
fs.writeFileSync(file, '', 'utf-8');
|
|
162
|
+
} catch (e) {
|
|
163
|
+
return { ...read, mode, drain_error: e.message };
|
|
164
|
+
}
|
|
165
|
+
} else if (mode === 'archive') {
|
|
166
|
+
const stamp = nowIso().replace(/[:.]/g, '-');
|
|
167
|
+
const archivePath = path.join(busDir(cwd), `${channel}-${stamp}.archive.jsonl`);
|
|
168
|
+
try {
|
|
169
|
+
fs.renameSync(file, archivePath);
|
|
170
|
+
} catch (e) {
|
|
171
|
+
return { ...read, mode, drain_error: e.message };
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
return { error: `unknown drain mode: ${mode}` };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { ...read, mode };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* List channels + message counts + sizes for observability.
|
|
182
|
+
* @param {string} cwd - Project root
|
|
183
|
+
* @returns {{channels: Array<{channel: string, messages: number, bytes: number, archive: boolean}>}}
|
|
184
|
+
*/
|
|
185
|
+
function listChannels(cwd) {
|
|
186
|
+
let files;
|
|
187
|
+
try {
|
|
188
|
+
files = fs.readdirSync(busDir(cwd));
|
|
189
|
+
} catch {
|
|
190
|
+
return { channels: [] };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const channels = [];
|
|
194
|
+
for (const f of files) {
|
|
195
|
+
if (!f.endsWith('.jsonl')) continue;
|
|
196
|
+
const archive = f.includes('.archive.');
|
|
197
|
+
const nameBase = f.replace(/\.jsonl$/, '');
|
|
198
|
+
const channel = archive ? nameBase.replace(/\.archive$/, '') : nameBase;
|
|
199
|
+
const filePath = path.join(busDir(cwd), f);
|
|
200
|
+
let bytes = 0;
|
|
201
|
+
let messages = 0;
|
|
202
|
+
try {
|
|
203
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
204
|
+
bytes = Buffer.byteLength(content, 'utf-8');
|
|
205
|
+
messages = content.split('\n').filter(Boolean).length;
|
|
206
|
+
} catch { /* unreadable — skip */ }
|
|
207
|
+
channels.push({ channel, messages, bytes, archive });
|
|
208
|
+
}
|
|
209
|
+
channels.sort((a, b) => a.channel.localeCompare(b.channel) || (a.archive ? 1 : -1));
|
|
210
|
+
return { channels };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── CLI wrappers ───────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
function cmdBusPublish(cwd, channel, rawPayload, opts, raw) {
|
|
216
|
+
if (!channel || rawPayload === undefined) {
|
|
217
|
+
error('Usage: bus publish <channel> <json-payload> [--source <name>]');
|
|
218
|
+
}
|
|
219
|
+
let payload;
|
|
220
|
+
try {
|
|
221
|
+
payload = JSON.parse(rawPayload);
|
|
222
|
+
} catch {
|
|
223
|
+
// Fall back: treat as plain string if not valid JSON.
|
|
224
|
+
payload = rawPayload;
|
|
225
|
+
}
|
|
226
|
+
const result = publish(cwd, channel, payload, opts);
|
|
227
|
+
output(result, raw);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function cmdBusDrain(cwd, channel, opts, raw) {
|
|
231
|
+
if (!channel) error('Usage: bus drain <channel> [--mode peek|consume|archive] [--limit N] [--offset N]');
|
|
232
|
+
const result = drain(cwd, channel, opts);
|
|
233
|
+
output(result, raw);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function cmdBusList(cwd, raw) {
|
|
237
|
+
output(listChannels(cwd), raw);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = {
|
|
241
|
+
publish,
|
|
242
|
+
readChannel,
|
|
243
|
+
drain,
|
|
244
|
+
listChannels,
|
|
245
|
+
validateName,
|
|
246
|
+
cmdBusPublish,
|
|
247
|
+
cmdBusDrain,
|
|
248
|
+
cmdBusList,
|
|
249
|
+
BUS_DIR,
|
|
250
|
+
DEFAULT_DRAIN_LIMIT,
|
|
251
|
+
};
|
|
@@ -721,6 +721,121 @@ function cmdBestPractices(cwd, raw) {
|
|
|
721
721
|
output(result, raw);
|
|
722
722
|
}
|
|
723
723
|
|
|
724
|
+
// ─── E-2: Repo token-size estimation (Opus 4.7 single-shot map-codebase) ────
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Estimate total token count of source files in a repository.
|
|
728
|
+
*
|
|
729
|
+
* Uses CHARS_PER_TOKEN as a rough approximation (no tokenizer shipped).
|
|
730
|
+
* Walks source files via walkSourceFiles (respects SKIP_DIRS) plus a curated
|
|
731
|
+
* set of "planning" files the map-codebase agent actually reads (top-level
|
|
732
|
+
* README, package.json, CLAUDE.md, docs/ top level).
|
|
733
|
+
*
|
|
734
|
+
* Returns enough info for map-codebase to choose between single-shot mode
|
|
735
|
+
* (all files fit in 1M context) and sharded mode (6-way parallel today).
|
|
736
|
+
*
|
|
737
|
+
* @param {string} cwd - Project root
|
|
738
|
+
* @param {Object} [opts]
|
|
739
|
+
* @param {number} [opts.threshold=700000] - Single-shot cutoff (tokens)
|
|
740
|
+
* @param {boolean} [opts.include_docs=true] - Whether to include docs/*.md top level
|
|
741
|
+
* @returns {{
|
|
742
|
+
* total_bytes: number,
|
|
743
|
+
* total_tokens: number,
|
|
744
|
+
* threshold: number,
|
|
745
|
+
* mode: 'single-shot'|'sharded',
|
|
746
|
+
* file_count: number,
|
|
747
|
+
* languages: Object<string, number>
|
|
748
|
+
* }}
|
|
749
|
+
*/
|
|
750
|
+
function estimateRepoTokenSize(cwd, opts) {
|
|
751
|
+
const { CHARS_PER_TOKEN } = require('./constants.cjs');
|
|
752
|
+
const threshold = (opts && typeof opts.threshold === 'number' && opts.threshold > 0)
|
|
753
|
+
? opts.threshold
|
|
754
|
+
: 700000;
|
|
755
|
+
const includeDocs = !opts || opts.include_docs !== false;
|
|
756
|
+
|
|
757
|
+
let totalBytes = 0;
|
|
758
|
+
let fileCount = 0;
|
|
759
|
+
const languages = {};
|
|
760
|
+
|
|
761
|
+
// Source files via the existing walker — respects gitignore-like skip list.
|
|
762
|
+
const walked = walkSourceFiles(cwd, cwd);
|
|
763
|
+
for (const [lang, files] of Object.entries(walked.files_by_language)) {
|
|
764
|
+
for (const relPath of files) {
|
|
765
|
+
const abs = path.join(cwd, relPath);
|
|
766
|
+
try {
|
|
767
|
+
const stat = fs.statSync(abs);
|
|
768
|
+
totalBytes += stat.size;
|
|
769
|
+
languages[lang] = (languages[lang] || 0) + stat.size;
|
|
770
|
+
fileCount += 1;
|
|
771
|
+
} catch { /* file may have been removed between walk and stat — ignore */ }
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Curated top-level planning files the agent actually reads.
|
|
776
|
+
const planningCandidates = [
|
|
777
|
+
'README.md',
|
|
778
|
+
'CLAUDE.md',
|
|
779
|
+
'AGENTS.md',
|
|
780
|
+
'package.json',
|
|
781
|
+
'pyproject.toml',
|
|
782
|
+
'go.mod',
|
|
783
|
+
'Cargo.toml',
|
|
784
|
+
];
|
|
785
|
+
for (const rel of planningCandidates) {
|
|
786
|
+
try {
|
|
787
|
+
const stat = fs.statSync(path.join(cwd, rel));
|
|
788
|
+
if (stat.isFile()) {
|
|
789
|
+
totalBytes += stat.size;
|
|
790
|
+
languages.docs = (languages.docs || 0) + stat.size;
|
|
791
|
+
fileCount += 1;
|
|
792
|
+
}
|
|
793
|
+
} catch { /* missing — expected */ }
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// docs/*.md at top level only (avoid recursing into specs/decisions/archive).
|
|
797
|
+
if (includeDocs) {
|
|
798
|
+
const docsDir = path.join(cwd, 'docs');
|
|
799
|
+
try {
|
|
800
|
+
const entries = fs.readdirSync(docsDir, { withFileTypes: true });
|
|
801
|
+
for (const e of entries) {
|
|
802
|
+
if (!e.isFile() || !e.name.endsWith('.md')) continue;
|
|
803
|
+
try {
|
|
804
|
+
const stat = fs.statSync(path.join(docsDir, e.name));
|
|
805
|
+
totalBytes += stat.size;
|
|
806
|
+
languages.docs = (languages.docs || 0) + stat.size;
|
|
807
|
+
fileCount += 1;
|
|
808
|
+
} catch { /* ignore */ }
|
|
809
|
+
}
|
|
810
|
+
} catch { /* no docs dir — expected in greenfield */ }
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const totalTokens = Math.ceil(totalBytes / CHARS_PER_TOKEN);
|
|
814
|
+
const mode = totalTokens <= threshold ? 'single-shot' : 'sharded';
|
|
815
|
+
|
|
816
|
+
return {
|
|
817
|
+
total_bytes: totalBytes,
|
|
818
|
+
total_tokens: totalTokens,
|
|
819
|
+
threshold,
|
|
820
|
+
mode,
|
|
821
|
+
file_count: fileCount,
|
|
822
|
+
languages,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function cmdEstimateRepoSize(cwd, raw, args) {
|
|
827
|
+
const thresholdIdx = Array.isArray(args) ? args.indexOf('--threshold') : -1;
|
|
828
|
+
const threshold = thresholdIdx !== -1 && args[thresholdIdx + 1]
|
|
829
|
+
? Number(args[thresholdIdx + 1])
|
|
830
|
+
: undefined;
|
|
831
|
+
const noDocs = Array.isArray(args) && args.includes('--no-docs');
|
|
832
|
+
const opts = {};
|
|
833
|
+
if (threshold && Number.isFinite(threshold) && threshold > 0) opts.threshold = threshold;
|
|
834
|
+
if (noDocs) opts.include_docs = false;
|
|
835
|
+
const result = estimateRepoTokenSize(cwd, opts);
|
|
836
|
+
output(result, raw);
|
|
837
|
+
}
|
|
838
|
+
|
|
724
839
|
// ─── Exports ────────────────────────────────────────────────────────────────
|
|
725
840
|
|
|
726
841
|
module.exports = {
|
|
@@ -743,4 +858,7 @@ module.exports = {
|
|
|
743
858
|
cmdDetectLanguages,
|
|
744
859
|
cmdAnalyzeImports,
|
|
745
860
|
cmdBestPractices,
|
|
861
|
+
cmdEstimateRepoSize,
|
|
862
|
+
// E-2
|
|
863
|
+
estimateRepoTokenSize,
|
|
746
864
|
};
|
|
@@ -33,6 +33,7 @@ const CONTEXT_SUFFIX = '-context.md';
|
|
|
33
33
|
const RESEARCH_SUFFIX = '-research.md';
|
|
34
34
|
const VERIFICATION_SUFFIX = '-verification.md';
|
|
35
35
|
const UAT_SUFFIX = '-uat.md';
|
|
36
|
+
const VALIDATION_SUFFIX = '-validation.md';
|
|
36
37
|
|
|
37
38
|
// ─── File matching helpers ───────────────────────────────────────────────────
|
|
38
39
|
|
|
@@ -595,6 +596,37 @@ const AUTORUN_STATUSES = {
|
|
|
595
596
|
const FILLED_BLOCK = '\u2588';
|
|
596
597
|
const EMPTY_BLOCK = '\u2591';
|
|
597
598
|
|
|
599
|
+
// ─── Opus 4.7 capability thresholds ─────────────────────────────────────────
|
|
600
|
+
// Used by resolveModel to pick tier given cache/thinking/context hints.
|
|
601
|
+
|
|
602
|
+
/** Context estimate (tokens) above which only 1M-context models (reasoning tier) apply */
|
|
603
|
+
const LARGE_CONTEXT_TOKEN_THRESHOLD = 700000;
|
|
604
|
+
/** Context estimate below which fast tier is viable for cached + non-thinking work */
|
|
605
|
+
const SMALL_CONTEXT_TOKEN_THRESHOLD = 50000;
|
|
606
|
+
/** Files whose content is stable across agent calls in a phase — candidates for prompt caching */
|
|
607
|
+
const CACHEABLE_CONTEXT_FILES = [
|
|
608
|
+
'project.md',
|
|
609
|
+
'requirements.md',
|
|
610
|
+
'roadmap.md',
|
|
611
|
+
'state.md',
|
|
612
|
+
'standards.md',
|
|
613
|
+
];
|
|
614
|
+
/** Default thinking budget (tokens) for verification-heavy agents */
|
|
615
|
+
const THINKING_BUDGETS = {
|
|
616
|
+
'pan-plan-checker': 8000,
|
|
617
|
+
'pan-verifier': 6000,
|
|
618
|
+
'pan-integration-checker': 6000,
|
|
619
|
+
'pan-reviewer': 4000,
|
|
620
|
+
'pan-debugger': 8000,
|
|
621
|
+
'pan-roadmapper': 4000,
|
|
622
|
+
default: 2000,
|
|
623
|
+
};
|
|
624
|
+
/** Whether focus-auto should insert a thinking-gated reflection step between cycles */
|
|
625
|
+
const REFLECTION_THRESHOLD = {
|
|
626
|
+
enabled_default: false,
|
|
627
|
+
enable_on_tiers: ['reasoning'],
|
|
628
|
+
};
|
|
629
|
+
|
|
598
630
|
module.exports = {
|
|
599
631
|
// Directories
|
|
600
632
|
PLANNING_DIR,
|
|
@@ -619,6 +651,7 @@ module.exports = {
|
|
|
619
651
|
RESEARCH_SUFFIX,
|
|
620
652
|
VERIFICATION_SUFFIX,
|
|
621
653
|
UAT_SUFFIX,
|
|
654
|
+
VALIDATION_SUFFIX,
|
|
622
655
|
// File matchers
|
|
623
656
|
isPlanFile,
|
|
624
657
|
isSummaryFile,
|
|
@@ -674,6 +707,12 @@ module.exports = {
|
|
|
674
707
|
MAX_SLUG_LENGTH,
|
|
675
708
|
FILLED_BLOCK,
|
|
676
709
|
EMPTY_BLOCK,
|
|
710
|
+
// Opus 4.7 capabilities
|
|
711
|
+
LARGE_CONTEXT_TOKEN_THRESHOLD,
|
|
712
|
+
SMALL_CONTEXT_TOKEN_THRESHOLD,
|
|
713
|
+
CACHEABLE_CONTEXT_FILES,
|
|
714
|
+
THINKING_BUDGETS,
|
|
715
|
+
REFLECTION_THRESHOLD,
|
|
677
716
|
CONTEXT_WINDOW,
|
|
678
717
|
WARNING_THRESHOLD,
|
|
679
718
|
CRITICAL_THRESHOLD,
|
|
@@ -101,6 +101,29 @@ function cmdContextBudget(cwd, raw) {
|
|
|
101
101
|
recommendation = `Within budget. ~${additionalPlans} more plans could fit before degradation.`;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// E-8: cache metrics — surface how much of the total context would be
|
|
105
|
+
// served from prompt cache when Opus 4.7 cache_control is active.
|
|
106
|
+
const { buildCachedContext } = require('./core.cjs');
|
|
107
|
+
let cache = null;
|
|
108
|
+
try {
|
|
109
|
+
const cached = buildCachedContext(cwd);
|
|
110
|
+
const cacheTokens = Math.ceil(cached.total_bytes / 4); // CHARS_PER_TOKEN ~ 4
|
|
111
|
+
const eligiblePct = totalTokens > 0
|
|
112
|
+
? Math.round((cacheTokens / totalTokens) * 1000) / 10
|
|
113
|
+
: 0;
|
|
114
|
+
cache = {
|
|
115
|
+
block_count: cached.blocks.length,
|
|
116
|
+
block_paths: cached.blocks.map(b => b.path),
|
|
117
|
+
total_bytes: cached.total_bytes,
|
|
118
|
+
total_tokens: cacheTokens,
|
|
119
|
+
eligible_pct: eligiblePct,
|
|
120
|
+
sha: cached.sha,
|
|
121
|
+
};
|
|
122
|
+
} catch {
|
|
123
|
+
// buildCachedContext failed — surface as null, not as an error.
|
|
124
|
+
cache = null;
|
|
125
|
+
}
|
|
126
|
+
|
|
104
127
|
const result = {
|
|
105
128
|
status,
|
|
106
129
|
currentPhase: currentPhase || null,
|
|
@@ -117,6 +140,7 @@ function cmdContextBudget(cwd, raw) {
|
|
|
117
140
|
},
|
|
118
141
|
contextWindow: CONTEXT_WINDOW,
|
|
119
142
|
budgetUtilization: Math.round(utilization * 1000) / 1000,
|
|
143
|
+
cache,
|
|
120
144
|
recommendation,
|
|
121
145
|
};
|
|
122
146
|
|
|
@@ -136,6 +160,9 @@ function cmdContextBudget(cwd, raw) {
|
|
|
136
160
|
` Total: ${totalTokens.toLocaleString()} / ${CONTEXT_WINDOW.toLocaleString()}`,
|
|
137
161
|
``,
|
|
138
162
|
`Utilization: ${(utilization * 100).toFixed(1)}%`,
|
|
163
|
+
cache && cache.block_count > 0
|
|
164
|
+
? `Cache: ${cache.block_count} blocks, ${cache.total_tokens.toLocaleString()} tokens (${cache.eligible_pct}% of total)`
|
|
165
|
+
: `Cache: 0 blocks (no cacheable .planning files)`,
|
|
139
166
|
`${recommendation}`,
|
|
140
167
|
];
|
|
141
168
|
return output(result, true, lines.join('\n'));
|
|
@@ -33,10 +33,10 @@ const {
|
|
|
33
33
|
* "inherit" means the host runtime uses its own top-tier model selection.
|
|
34
34
|
*/
|
|
35
35
|
const PROVIDER_MODELS = {
|
|
36
|
-
anthropic: { reasoning: 'inherit', mid: 'sonnet',
|
|
37
|
-
openai: { reasoning: 'inherit', mid: 'mid',
|
|
38
|
-
google: { reasoning: 'inherit', mid: '
|
|
39
|
-
default: { reasoning: 'inherit', mid: 'sonnet',
|
|
36
|
+
anthropic: { reasoning: 'inherit', mid: 'sonnet', fast: 'haiku' },
|
|
37
|
+
openai: { reasoning: 'inherit', mid: 'mid', fast: 'fast' },
|
|
38
|
+
google: { reasoning: 'inherit', mid: 'gemini-2.5-flash', fast: 'gemini-2.5-flash-lite' },
|
|
39
|
+
default: { reasoning: 'inherit', mid: 'sonnet', fast: 'haiku' },
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
/** Maps legacy Anthropic model names to provider-agnostic tier aliases. */
|
|
@@ -493,7 +493,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
|
|
|
493
493
|
const sectionEnd = nextHeaderMatch ? headerIndex + nextHeaderMatch.index : content.length;
|
|
494
494
|
const section = content.slice(headerIndex, sectionEnd).trim();
|
|
495
495
|
|
|
496
|
-
const goalMatch = section.match(
|
|
496
|
+
const goalMatch = section.match(/(?:\*\*Goal:\*\*|\*\*Goal\*\*:)\s*([^\n]+)/i);
|
|
497
497
|
const goal = goalMatch ? goalMatch[1].trim() : null;
|
|
498
498
|
|
|
499
499
|
return {
|
|
@@ -522,12 +522,49 @@ function getPhaseModelTier(cwd, phaseNum) {
|
|
|
522
522
|
return match ? match[1] : null;
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Adjust a resolved tier given Opus 4.7-era capability hints.
|
|
527
|
+
*
|
|
528
|
+
* Rules, in priority order:
|
|
529
|
+
* 1. context_estimate > LARGE_CONTEXT_TOKEN_THRESHOLD → force reasoning (only 1M-ctx tier).
|
|
530
|
+
* 2. needs_thinking → upgrade fast → mid; leave mid/reasoning alone.
|
|
531
|
+
* 3. cache_warm + !needs_thinking + context_estimate < SMALL_CONTEXT_TOKEN_THRESHOLD →
|
|
532
|
+
* allow downgrade mid → fast (cheap, cached, simple tasks don't need mid).
|
|
533
|
+
*
|
|
534
|
+
* @param {string} tier - Baseline tier (reasoning|mid|fast)
|
|
535
|
+
* @param {Object} [opts] - {context_estimate, needs_thinking, cache_warm}
|
|
536
|
+
* @returns {string} Possibly-adjusted tier
|
|
537
|
+
*/
|
|
538
|
+
function adjustTierForCapabilities(tier, opts) {
|
|
539
|
+
if (!opts) return tier;
|
|
540
|
+
const { context_estimate, needs_thinking, cache_warm } = opts;
|
|
541
|
+
const { LARGE_CONTEXT_TOKEN_THRESHOLD, SMALL_CONTEXT_TOKEN_THRESHOLD } = require('./constants.cjs');
|
|
542
|
+
|
|
543
|
+
if (typeof context_estimate === 'number' && context_estimate > LARGE_CONTEXT_TOKEN_THRESHOLD) {
|
|
544
|
+
return 'reasoning';
|
|
545
|
+
}
|
|
546
|
+
if (needs_thinking && tier === 'fast') {
|
|
547
|
+
return 'mid';
|
|
548
|
+
}
|
|
549
|
+
if (
|
|
550
|
+
cache_warm &&
|
|
551
|
+
!needs_thinking &&
|
|
552
|
+
typeof context_estimate === 'number' &&
|
|
553
|
+
context_estimate < SMALL_CONTEXT_TOKEN_THRESHOLD &&
|
|
554
|
+
tier === 'mid'
|
|
555
|
+
) {
|
|
556
|
+
return 'fast';
|
|
557
|
+
}
|
|
558
|
+
return tier;
|
|
559
|
+
}
|
|
560
|
+
|
|
525
561
|
/**
|
|
526
562
|
* Resolve the model for a given agent type based on profile, provider, and routing strategy.
|
|
527
563
|
* Returns "inherit" for reasoning-tier to let the host runtime use its top-tier model.
|
|
528
564
|
* @param {string} cwd - Project root directory
|
|
529
565
|
* @param {string} agentType - Agent name (e.g., "pan-planner", "pan-executor")
|
|
530
|
-
* @param {Object} [taskMetadata] - Optional metadata
|
|
566
|
+
* @param {Object} [taskMetadata] - Optional metadata. Supports complexity fields and
|
|
567
|
+
* Opus 4.7 capability hints: {context_estimate, needs_thinking, cache_warm}.
|
|
531
568
|
* @returns {string} Model identifier: "inherit", "sonnet", "haiku", "mid", "fast", etc.
|
|
532
569
|
*/
|
|
533
570
|
function resolveModelInternal(cwd, agentType, taskMetadata) {
|
|
@@ -562,6 +599,15 @@ function resolveModelInternal(cwd, agentType, taskMetadata) {
|
|
|
562
599
|
tier = resolveComplexityTier(tier, { ...taskMetadata, thresholds });
|
|
563
600
|
}
|
|
564
601
|
|
|
602
|
+
// Opus 4.7 capability adjustment (only when hints are present)
|
|
603
|
+
if (taskMetadata && (
|
|
604
|
+
taskMetadata.context_estimate !== undefined ||
|
|
605
|
+
taskMetadata.needs_thinking !== undefined ||
|
|
606
|
+
taskMetadata.cache_warm !== undefined
|
|
607
|
+
)) {
|
|
608
|
+
tier = adjustTierForCapabilities(tier, taskMetadata);
|
|
609
|
+
}
|
|
610
|
+
|
|
565
611
|
return resolveTierToModel(tier, provider);
|
|
566
612
|
}
|
|
567
613
|
|
|
@@ -731,6 +777,43 @@ function scanPendingTodos(cwd, area) {
|
|
|
731
777
|
* @param {string} cwd - Project root
|
|
732
778
|
* @returns {{ count: number, items: Array<{file: string, line: number, tag: string, text: string}> }}
|
|
733
779
|
*/
|
|
780
|
+
/**
|
|
781
|
+
* Build an ordered list of cacheable context blocks for agent prompts.
|
|
782
|
+
*
|
|
783
|
+
* Reads files from .planning/ that are stable across agent calls within a phase
|
|
784
|
+
* (project.md, requirements.md, roadmap.md, state.md, standards.md). Each block
|
|
785
|
+
* is tagged `cache: true` so the host runtime (or installer) can translate to
|
|
786
|
+
* the appropriate per-runtime caching syntax (Anthropic cache_control, etc.).
|
|
787
|
+
*
|
|
788
|
+
* Files that don't exist are skipped silently. The order matches the file list
|
|
789
|
+
* in constants.cjs to keep prompt prefixes byte-stable across calls (which is
|
|
790
|
+
* what cache key matching requires).
|
|
791
|
+
*
|
|
792
|
+
* @param {string} cwd - Project root
|
|
793
|
+
* @returns {{blocks: Array<{path: string, content: string, cache: true}>, total_bytes: number, sha: string}}
|
|
794
|
+
*/
|
|
795
|
+
function buildCachedContext(cwd) {
|
|
796
|
+
const { PLANNING_DIR, CACHEABLE_CONTEXT_FILES } = require('./constants.cjs');
|
|
797
|
+
const crypto = require('crypto');
|
|
798
|
+
const blocks = [];
|
|
799
|
+
let totalBytes = 0;
|
|
800
|
+
const hasher = crypto.createHash('sha256');
|
|
801
|
+
|
|
802
|
+
for (const file of CACHEABLE_CONTEXT_FILES) {
|
|
803
|
+
const abs = path.join(cwd, PLANNING_DIR, file);
|
|
804
|
+
try {
|
|
805
|
+
const content = fs.readFileSync(abs, 'utf-8');
|
|
806
|
+
blocks.push({ path: toPosix(path.join(PLANNING_DIR, file)), content, cache: true });
|
|
807
|
+
totalBytes += Buffer.byteLength(content, 'utf-8');
|
|
808
|
+
hasher.update(file + '\0' + content + '\0');
|
|
809
|
+
} catch {
|
|
810
|
+
// Missing files are expected (e.g. standards.md in non-regulated projects).
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return { blocks, total_bytes: totalBytes, sha: hasher.digest('hex').slice(0, 16) };
|
|
815
|
+
}
|
|
816
|
+
|
|
734
817
|
function scanSourceTodos(cwd) {
|
|
735
818
|
const items = [];
|
|
736
819
|
const libDir = path.join(cwd, 'pan-wizard-core', 'bin', 'lib');
|
|
@@ -783,6 +866,7 @@ module.exports = {
|
|
|
783
866
|
getArchivedPhaseDirs,
|
|
784
867
|
getRoadmapPhaseInternal,
|
|
785
868
|
resolveModelInternal,
|
|
869
|
+
adjustTierForCapabilities,
|
|
786
870
|
detectProvider,
|
|
787
871
|
resolveTierToModel,
|
|
788
872
|
resolveComplexityTier,
|
|
@@ -792,6 +876,7 @@ module.exports = {
|
|
|
792
876
|
generateSlugInternal,
|
|
793
877
|
getMilestoneInfo,
|
|
794
878
|
toPosix,
|
|
879
|
+
buildCachedContext,
|
|
795
880
|
scanPendingTodos,
|
|
796
881
|
scanSourceTodos,
|
|
797
882
|
};
|