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,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost — per-call cost aggregation and dashboard (Spec B v2 Y-6, v3.0).
|
|
3
|
+
*
|
|
4
|
+
* Storage: `.planning/metrics/tokens.jsonl` — append-only JSON Lines.
|
|
5
|
+
*
|
|
6
|
+
* Each line is a cost record:
|
|
7
|
+
* {
|
|
8
|
+
* ts: "2026-04-18T12:34:56.789Z",
|
|
9
|
+
* agent: "pan-planner" | null, // agent name, if spawned as agent
|
|
10
|
+
* command: "exec-phase" | null, // command name, if invoked directly
|
|
11
|
+
* model: "claude-opus-4-7" | null, // model id when known
|
|
12
|
+
* tier: "reasoning" | "mid" | "fast" | null,
|
|
13
|
+
* input_tokens: 12345,
|
|
14
|
+
* output_tokens: 678,
|
|
15
|
+
* cache_read_tokens: 0,
|
|
16
|
+
* cache_write_tokens: 0,
|
|
17
|
+
* cost_usd: 0.123, // computed if model+tokens known, else null
|
|
18
|
+
* phase: "07" | null,
|
|
19
|
+
* session: "abc123" | null
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* The appender is deliberately tolerant: if fields are missing the record
|
|
23
|
+
* is still written; aggregation skips null fields gracefully. Non-blocking
|
|
24
|
+
* — failure to write never breaks the caller (cost is observability, not
|
|
25
|
+
* critical path).
|
|
26
|
+
*
|
|
27
|
+
* Aggregation produces:
|
|
28
|
+
* - by agent, by command, by tier, by day
|
|
29
|
+
* - totals: input/output/cache tokens, cost
|
|
30
|
+
* - hit rate: cache_read / (cache_read + input - cache_write) if any cache activity
|
|
31
|
+
*
|
|
32
|
+
* Rate table is approximate — real pricing comes from the provider's API.
|
|
33
|
+
* Rates are US dollars per million tokens, indicative as of 2026-04. Users
|
|
34
|
+
* can override with `.planning/config.json` → `cost.rates`.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const path = require('path');
|
|
39
|
+
const { output, error, safeReadFile, loadConfig } = require('./core.cjs');
|
|
40
|
+
const { PLANNING_DIR } = require('./constants.cjs');
|
|
41
|
+
const { planningPath } = require('./utils.cjs');
|
|
42
|
+
|
|
43
|
+
const METRICS_DIR = 'metrics';
|
|
44
|
+
const TOKENS_FILE = 'tokens.jsonl';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Default rate table ($ per million tokens).
|
|
48
|
+
* Override per-model in config.json → cost.rates.
|
|
49
|
+
*/
|
|
50
|
+
const DEFAULT_RATES = {
|
|
51
|
+
// Anthropic
|
|
52
|
+
'claude-opus-4-7': { input: 15.0, output: 75.0, cache_read: 1.5, cache_write: 18.75 },
|
|
53
|
+
'claude-opus-4-6': { input: 15.0, output: 75.0, cache_read: 1.5, cache_write: 18.75 },
|
|
54
|
+
'claude-sonnet-4-6': { input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75 },
|
|
55
|
+
'claude-haiku-4-5': { input: 1.0, output: 5.0, cache_read: 0.1, cache_write: 1.25 },
|
|
56
|
+
|
|
57
|
+
// Google Gemini — published rates (per million tokens, approximate; users can override via config.json → cost.rates).
|
|
58
|
+
// 2.5 tier uses the <=200K-context tier; long-context calls may be billed at ~2x. Cache rates are Google's context-cache pricing (~25% of input rate).
|
|
59
|
+
'gemini-2.5-pro': { input: 1.25, output: 10.0, cache_read: 0.3125, cache_write: 1.25 },
|
|
60
|
+
'gemini-2.5-flash': { input: 0.30, output: 2.50, cache_read: 0.075, cache_write: 0.30 },
|
|
61
|
+
'gemini-2.5-flash-lite': { input: 0.10, output: 0.40, cache_read: 0.025, cache_write: 0.10 },
|
|
62
|
+
'gemini-1.5-pro': { input: 1.25, output: 5.00, cache_read: 0.3125, cache_write: 1.25 },
|
|
63
|
+
|
|
64
|
+
// Tier fallbacks when model id is unknown
|
|
65
|
+
'reasoning': { input: 15.0, output: 75.0, cache_read: 1.5, cache_write: 18.75 },
|
|
66
|
+
'mid': { input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75 },
|
|
67
|
+
'fast': { input: 1.0, output: 5.0, cache_read: 0.1, cache_write: 1.25 },
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function metricsDir(cwd) {
|
|
71
|
+
return path.join(planningPath(cwd), METRICS_DIR);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function tokensFile(cwd) {
|
|
75
|
+
return path.join(metricsDir(cwd), TOKENS_FILE);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveRate(model, tier, configRates) {
|
|
79
|
+
if (configRates) {
|
|
80
|
+
if (model && configRates[model]) return configRates[model];
|
|
81
|
+
if (tier && configRates[tier]) return configRates[tier];
|
|
82
|
+
}
|
|
83
|
+
if (model && DEFAULT_RATES[model]) return DEFAULT_RATES[model];
|
|
84
|
+
if (tier && DEFAULT_RATES[tier]) return DEFAULT_RATES[tier];
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Compute cost in USD for a single record given known rates.
|
|
90
|
+
* Returns null when rate is unknown.
|
|
91
|
+
* @param {Object} rec - Cost record
|
|
92
|
+
* @param {Object} [configRates] - Optional rate overrides
|
|
93
|
+
* @returns {number|null}
|
|
94
|
+
*/
|
|
95
|
+
function computeCost(rec, configRates) {
|
|
96
|
+
const rate = resolveRate(rec.model, rec.tier, configRates);
|
|
97
|
+
if (!rate) return null;
|
|
98
|
+
const input = rec.input_tokens || 0;
|
|
99
|
+
const output = rec.output_tokens || 0;
|
|
100
|
+
const cacheRead = rec.cache_read_tokens || 0;
|
|
101
|
+
const cacheWrite = rec.cache_write_tokens || 0;
|
|
102
|
+
// Non-cache-hit input tokens = input - cache_read (cache_read already in input on some providers,
|
|
103
|
+
// separate on others; we treat cache_read as a reduction of effective new input).
|
|
104
|
+
const newInput = Math.max(0, input - cacheRead);
|
|
105
|
+
const usd = (newInput * rate.input + output * rate.output
|
|
106
|
+
+ cacheRead * rate.cache_read + cacheWrite * rate.cache_write) / 1_000_000;
|
|
107
|
+
return Math.round(usd * 10000) / 10000;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Append a cost record. Non-blocking — errors are swallowed so instrumentation
|
|
112
|
+
* never breaks the caller.
|
|
113
|
+
* @param {string} cwd - Project root
|
|
114
|
+
* @param {Object} rec - Partial record; missing fields default to null/0.
|
|
115
|
+
* @returns {{appended: boolean, file?: string, error?: string}}
|
|
116
|
+
*/
|
|
117
|
+
function appendRecord(cwd, rec) {
|
|
118
|
+
const normalized = {
|
|
119
|
+
ts: rec.ts || new Date().toISOString(),
|
|
120
|
+
agent: rec.agent || null,
|
|
121
|
+
command: rec.command || null,
|
|
122
|
+
model: rec.model || null,
|
|
123
|
+
tier: rec.tier || null,
|
|
124
|
+
input_tokens: Number(rec.input_tokens) || 0,
|
|
125
|
+
output_tokens: Number(rec.output_tokens) || 0,
|
|
126
|
+
cache_read_tokens: Number(rec.cache_read_tokens) || 0,
|
|
127
|
+
cache_write_tokens: Number(rec.cache_write_tokens) || 0,
|
|
128
|
+
phase: rec.phase || null,
|
|
129
|
+
session: rec.session || null,
|
|
130
|
+
};
|
|
131
|
+
// Allow caller-supplied cost override; otherwise compute.
|
|
132
|
+
normalized.cost_usd = typeof rec.cost_usd === 'number'
|
|
133
|
+
? rec.cost_usd
|
|
134
|
+
: computeCost(normalized);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
fs.mkdirSync(metricsDir(cwd), { recursive: true });
|
|
138
|
+
fs.appendFileSync(tokensFile(cwd), JSON.stringify(normalized) + '\n', 'utf-8');
|
|
139
|
+
return { appended: true, file: tokensFile(cwd) };
|
|
140
|
+
} catch (e) {
|
|
141
|
+
return { appended: false, error: e.message };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Read all cost records from the log.
|
|
147
|
+
* @param {string} cwd
|
|
148
|
+
* @returns {Array<Object>}
|
|
149
|
+
*/
|
|
150
|
+
function readRecords(cwd) {
|
|
151
|
+
const raw = safeReadFile(tokensFile(cwd));
|
|
152
|
+
if (!raw) return [];
|
|
153
|
+
const records = [];
|
|
154
|
+
for (const line of raw.split('\n')) {
|
|
155
|
+
if (!line.trim()) continue;
|
|
156
|
+
try {
|
|
157
|
+
records.push(JSON.parse(line));
|
|
158
|
+
} catch { /* skip malformed line */ }
|
|
159
|
+
}
|
|
160
|
+
return records;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Aggregate records into totals + breakdowns.
|
|
165
|
+
* @param {string} cwd
|
|
166
|
+
* @param {Object} [opts] - {since, until, group_by}
|
|
167
|
+
* @returns {Object} Aggregation
|
|
168
|
+
*/
|
|
169
|
+
function aggregate(cwd, opts) {
|
|
170
|
+
const records = readRecords(cwd);
|
|
171
|
+
const since = opts?.since ? new Date(opts.since).getTime() : null;
|
|
172
|
+
const until = opts?.until ? new Date(opts.until).getTime() : null;
|
|
173
|
+
const config = loadConfig(cwd);
|
|
174
|
+
const configRates = config?.cost?.rates;
|
|
175
|
+
|
|
176
|
+
const filtered = records.filter(r => {
|
|
177
|
+
if (!r.ts) return true;
|
|
178
|
+
const t = new Date(r.ts).getTime();
|
|
179
|
+
if (since !== null && t < since) return false;
|
|
180
|
+
if (until !== null && t > until) return false;
|
|
181
|
+
return true;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const totals = {
|
|
185
|
+
calls: filtered.length,
|
|
186
|
+
input_tokens: 0,
|
|
187
|
+
output_tokens: 0,
|
|
188
|
+
cache_read_tokens: 0,
|
|
189
|
+
cache_write_tokens: 0,
|
|
190
|
+
cost_usd: 0,
|
|
191
|
+
cost_unknown: 0,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const byAgent = {};
|
|
195
|
+
const byCommand = {};
|
|
196
|
+
const byTier = {};
|
|
197
|
+
const byDay = {};
|
|
198
|
+
|
|
199
|
+
function bump(map, key, rec) {
|
|
200
|
+
if (!key) return;
|
|
201
|
+
if (!map[key]) map[key] = { calls: 0, input: 0, output: 0, cache_read: 0, cache_write: 0, cost: 0 };
|
|
202
|
+
map[key].calls += 1;
|
|
203
|
+
map[key].input += rec.input_tokens || 0;
|
|
204
|
+
map[key].output += rec.output_tokens || 0;
|
|
205
|
+
map[key].cache_read += rec.cache_read_tokens || 0;
|
|
206
|
+
map[key].cache_write += rec.cache_write_tokens || 0;
|
|
207
|
+
const cost = typeof rec.cost_usd === 'number' ? rec.cost_usd : computeCost(rec, configRates);
|
|
208
|
+
if (typeof cost === 'number') map[key].cost += cost;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const r of filtered) {
|
|
212
|
+
totals.input_tokens += r.input_tokens || 0;
|
|
213
|
+
totals.output_tokens += r.output_tokens || 0;
|
|
214
|
+
totals.cache_read_tokens += r.cache_read_tokens || 0;
|
|
215
|
+
totals.cache_write_tokens += r.cache_write_tokens || 0;
|
|
216
|
+
const cost = typeof r.cost_usd === 'number' ? r.cost_usd : computeCost(r, configRates);
|
|
217
|
+
if (typeof cost === 'number') totals.cost_usd += cost;
|
|
218
|
+
else totals.cost_unknown += 1;
|
|
219
|
+
|
|
220
|
+
bump(byAgent, r.agent, r);
|
|
221
|
+
bump(byCommand, r.command, r);
|
|
222
|
+
bump(byTier, r.tier, r);
|
|
223
|
+
const day = r.ts ? r.ts.slice(0, 10) : null;
|
|
224
|
+
bump(byDay, day, r);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
totals.cost_usd = Math.round(totals.cost_usd * 10000) / 10000;
|
|
228
|
+
|
|
229
|
+
// Cache hit rate: cache_read / (cache_read + new input tokens billed at full rate)
|
|
230
|
+
const billedInput = Math.max(0, totals.input_tokens - totals.cache_read_tokens);
|
|
231
|
+
const hitDenom = totals.cache_read_tokens + billedInput;
|
|
232
|
+
const cacheHitRatePct = hitDenom > 0
|
|
233
|
+
? Math.round((totals.cache_read_tokens / hitDenom) * 1000) / 10
|
|
234
|
+
: null;
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
totals,
|
|
238
|
+
cache_hit_rate_pct: cacheHitRatePct,
|
|
239
|
+
by_agent: byAgent,
|
|
240
|
+
by_command: byCommand,
|
|
241
|
+
by_tier: byTier,
|
|
242
|
+
by_day: byDay,
|
|
243
|
+
window: {
|
|
244
|
+
since: opts?.since || null,
|
|
245
|
+
until: opts?.until || null,
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Render aggregation as a human-readable table.
|
|
252
|
+
* @param {Object} agg - from aggregate()
|
|
253
|
+
* @returns {string}
|
|
254
|
+
*/
|
|
255
|
+
function renderTable(agg) {
|
|
256
|
+
const lines = [];
|
|
257
|
+
lines.push('=== PAN Wizard Cost Dashboard ===');
|
|
258
|
+
const window = agg.window.since || agg.window.until
|
|
259
|
+
? ` Window: ${agg.window.since || '(any)'} → ${agg.window.until || 'now'}`
|
|
260
|
+
: ' Window: all time';
|
|
261
|
+
lines.push(window);
|
|
262
|
+
lines.push('');
|
|
263
|
+
lines.push('Totals');
|
|
264
|
+
lines.push(` Calls : ${agg.totals.calls}`);
|
|
265
|
+
lines.push(` Input tokens : ${agg.totals.input_tokens.toLocaleString()}`);
|
|
266
|
+
lines.push(` Output tokens : ${agg.totals.output_tokens.toLocaleString()}`);
|
|
267
|
+
lines.push(` Cache read : ${agg.totals.cache_read_tokens.toLocaleString()}`);
|
|
268
|
+
lines.push(` Cache write : ${agg.totals.cache_write_tokens.toLocaleString()}`);
|
|
269
|
+
lines.push(` Estimated cost : $${agg.totals.cost_usd.toFixed(4)}${agg.totals.cost_unknown > 0 ? ` (+${agg.totals.cost_unknown} unknown)` : ''}`);
|
|
270
|
+
lines.push(` Cache hit rate : ${agg.cache_hit_rate_pct == null ? 'n/a' : `${agg.cache_hit_rate_pct}%`}`);
|
|
271
|
+
|
|
272
|
+
function section(title, map) {
|
|
273
|
+
const keys = Object.keys(map).sort((a, b) => (map[b].cost || 0) - (map[a].cost || 0));
|
|
274
|
+
if (keys.length === 0) return;
|
|
275
|
+
lines.push('');
|
|
276
|
+
lines.push(title);
|
|
277
|
+
lines.push(' ' + 'name'.padEnd(28) + 'calls'.padStart(7) + 'input'.padStart(11) + 'output'.padStart(9) + ' cost');
|
|
278
|
+
for (const k of keys) {
|
|
279
|
+
const row = map[k];
|
|
280
|
+
lines.push(' ' + k.slice(0, 28).padEnd(28)
|
|
281
|
+
+ String(row.calls).padStart(7)
|
|
282
|
+
+ row.input.toLocaleString().padStart(11)
|
|
283
|
+
+ row.output.toLocaleString().padStart(9)
|
|
284
|
+
+ ' $' + row.cost.toFixed(4));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
section('By agent', agg.by_agent);
|
|
288
|
+
section('By command', agg.by_command);
|
|
289
|
+
section('By tier', agg.by_tier);
|
|
290
|
+
section('By day', agg.by_day);
|
|
291
|
+
|
|
292
|
+
return lines.join('\n');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Render aggregation as an ASCII bar chart of cost per day.
|
|
297
|
+
* @param {Object} agg
|
|
298
|
+
* @returns {string}
|
|
299
|
+
*/
|
|
300
|
+
function renderChart(agg) {
|
|
301
|
+
const days = Object.keys(agg.by_day).sort();
|
|
302
|
+
if (days.length === 0) return 'No cost data in window.';
|
|
303
|
+
const max = Math.max(...days.map(d => agg.by_day[d].cost || 0), 0.0001);
|
|
304
|
+
const width = 30;
|
|
305
|
+
const lines = ['=== Cost per day ==='];
|
|
306
|
+
for (const day of days) {
|
|
307
|
+
const cost = agg.by_day[day].cost || 0;
|
|
308
|
+
const len = Math.round((cost / max) * width);
|
|
309
|
+
const bar = '█'.repeat(len) + '░'.repeat(width - len);
|
|
310
|
+
lines.push(` ${day} ${bar} $${cost.toFixed(4)}`);
|
|
311
|
+
}
|
|
312
|
+
lines.push('');
|
|
313
|
+
lines.push(` Total window cost: $${agg.totals.cost_usd.toFixed(4)}`);
|
|
314
|
+
return lines.join('\n');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ─── CLI wrappers ───────────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
function cmdCostReport(cwd, opts, raw) {
|
|
320
|
+
const format = opts?.format || 'json';
|
|
321
|
+
const agg = aggregate(cwd, opts);
|
|
322
|
+
if (format === 'table') {
|
|
323
|
+
output(agg, raw, renderTable(agg));
|
|
324
|
+
} else if (format === 'chart') {
|
|
325
|
+
output(agg, raw, renderChart(agg));
|
|
326
|
+
} else {
|
|
327
|
+
output(agg, raw);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function cmdCostAppend(cwd, rec, raw) {
|
|
332
|
+
const result = appendRecord(cwd, rec);
|
|
333
|
+
output(result, raw);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function cmdCostClear(cwd, raw) {
|
|
337
|
+
try {
|
|
338
|
+
fs.unlinkSync(tokensFile(cwd));
|
|
339
|
+
output({ cleared: true, file: tokensFile(cwd) }, raw);
|
|
340
|
+
} catch (e) {
|
|
341
|
+
output({ cleared: false, error: e.message }, raw);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = {
|
|
346
|
+
computeCost,
|
|
347
|
+
appendRecord,
|
|
348
|
+
readRecords,
|
|
349
|
+
aggregate,
|
|
350
|
+
renderTable,
|
|
351
|
+
renderChart,
|
|
352
|
+
resolveRate,
|
|
353
|
+
cmdCostReport,
|
|
354
|
+
cmdCostAppend,
|
|
355
|
+
cmdCostClear,
|
|
356
|
+
METRICS_DIR,
|
|
357
|
+
TOKENS_FILE,
|
|
358
|
+
DEFAULT_RATES,
|
|
359
|
+
};
|
|
@@ -539,7 +539,9 @@ function cmdFocusSync(cwd, raw, ...args) {
|
|
|
539
539
|
// ─── Exec helpers ───────────────────────────────────────────────────────────
|
|
540
540
|
|
|
541
541
|
/**
|
|
542
|
-
* Read the
|
|
542
|
+
* Read the oldest open batch file from .planning/focus/.
|
|
543
|
+
* Batches are named batch-YYYY-MM-DD.json; lexical sort == chronological.
|
|
544
|
+
* Oldest-first ensures older unfinished batches get executed before newer ones.
|
|
543
545
|
* @param {string} cwd - Project root
|
|
544
546
|
* @returns {Object|null} Parsed batch data or null
|
|
545
547
|
*/
|
|
@@ -552,7 +554,7 @@ function readLatestBatch(cwd) {
|
|
|
552
554
|
return null;
|
|
553
555
|
}
|
|
554
556
|
if (files.length === 0) return null;
|
|
555
|
-
files.sort()
|
|
557
|
+
files.sort();
|
|
556
558
|
const content = safeReadFile(path.join(focusDir, files[0]));
|
|
557
559
|
if (!content) return null;
|
|
558
560
|
try {
|
|
@@ -874,6 +876,99 @@ function cmdFocusAuto(cwd, raw, ...args) {
|
|
|
874
876
|
return focusAutoInit(cwd, raw, getVal, hasFlag);
|
|
875
877
|
}
|
|
876
878
|
|
|
879
|
+
// ─── Opus 4.7: Reflection gate for focus-auto ───────────────────────────────
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Emit a reflection prompt the orchestrator shows to a thinking-capable model
|
|
883
|
+
* before committing to the next auto cycle. Returns null when reflection is
|
|
884
|
+
* disabled (non-reasoning tier, or explicitly turned off).
|
|
885
|
+
*
|
|
886
|
+
* @param {Object} run - Auto-run state (reads totals, config, category)
|
|
887
|
+
* @param {Object} cycle - The just-completed cycle's telemetry
|
|
888
|
+
* @param {Array} proposedNextBatch - Items focus-scan would select for cycle N+1
|
|
889
|
+
* @param {Object} [opts]
|
|
890
|
+
* @param {string} [opts.tier] - Resolved model tier ('reasoning'|'mid'|'fast')
|
|
891
|
+
* @returns {{reflect: boolean, prompt: string|null, reason: string}}
|
|
892
|
+
*/
|
|
893
|
+
function determineContinuation(run, cycle, proposedNextBatch, opts) {
|
|
894
|
+
const { REFLECTION_THRESHOLD } = require('./constants.cjs');
|
|
895
|
+
const tier = opts?.tier || 'mid';
|
|
896
|
+
const configFlag = run?.reflection_enabled;
|
|
897
|
+
|
|
898
|
+
const enabled = configFlag !== undefined
|
|
899
|
+
? Boolean(configFlag)
|
|
900
|
+
: REFLECTION_THRESHOLD.enabled_default ||
|
|
901
|
+
REFLECTION_THRESHOLD.enable_on_tiers.includes(tier);
|
|
902
|
+
|
|
903
|
+
if (!enabled) {
|
|
904
|
+
return { reflect: false, prompt: null, reason: 'reflection_disabled' };
|
|
905
|
+
}
|
|
906
|
+
if (!Array.isArray(proposedNextBatch) || proposedNextBatch.length === 0) {
|
|
907
|
+
return { reflect: false, prompt: null, reason: 'no_next_batch' };
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const completed = cycle?.items_completed ?? 0;
|
|
911
|
+
const pointsUsed = cycle?.points_used ?? 0;
|
|
912
|
+
const efficiency = pointsUsed > 0 ? (completed / pointsUsed).toFixed(3) : 'n/a';
|
|
913
|
+
const category = run?.category || 'mixed';
|
|
914
|
+
const cyclesDone = run?.totals?.cycles_completed ?? 0;
|
|
915
|
+
const maxCycles = run?.max_cycles ?? 10;
|
|
916
|
+
|
|
917
|
+
const firstThree = proposedNextBatch.slice(0, 3)
|
|
918
|
+
.map(i => `- ${i.id || '(no id)'}: ${i.description || i.title || '(no description)'}`)
|
|
919
|
+
.join('\n');
|
|
920
|
+
|
|
921
|
+
const prompt = [
|
|
922
|
+
`Reflect before committing to cycle ${cyclesDone + 1} of ${maxCycles} in category "${category}".`,
|
|
923
|
+
'',
|
|
924
|
+
'Just-completed cycle telemetry:',
|
|
925
|
+
` items_completed: ${completed}`,
|
|
926
|
+
` points_used: ${pointsUsed}`,
|
|
927
|
+
` efficiency: ${efficiency} items/point`,
|
|
928
|
+
'',
|
|
929
|
+
`Next batch candidates (top ${Math.min(3, proposedNextBatch.length)} of ${proposedNextBatch.length}):`,
|
|
930
|
+
firstThree,
|
|
931
|
+
'',
|
|
932
|
+
'Think step-by-step: Is running another cycle worthwhile given the telemetry? Would the remaining items cluster better under a different category? Is there a stop signal this data is showing that the automatic rules missed? Answer in JSON: {"continue": true|false, "rationale": "..."}',
|
|
933
|
+
].join('\n');
|
|
934
|
+
|
|
935
|
+
return { reflect: true, prompt, reason: 'ok' };
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// ─── Opus 4.7: Parallel-tool stage dependency DAG for focus-exec ────────────
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Classify focus-exec items into a simple dependency DAG suitable for
|
|
942
|
+
* emitting parallel-tool-use instructions.
|
|
943
|
+
*
|
|
944
|
+
* Today every item is independent (batches come from focus-plan which
|
|
945
|
+
* already resolves dependencies). But items of tier MICRO can run fully
|
|
946
|
+
* parallel, STANDARD can run within-stage parallel, FULL must serialize.
|
|
947
|
+
* This helper expresses that as independent waves the command template
|
|
948
|
+
* can reference when instructing Opus to emit parallel tool calls.
|
|
949
|
+
*
|
|
950
|
+
* @param {Array<{id?: string, tier?: string}>} items - From readLatestBatch(...).batch
|
|
951
|
+
* @returns {{waves: Array<Array<Object>>, parallelism_hint: string}}
|
|
952
|
+
*/
|
|
953
|
+
function classifyStageDependencies(items) {
|
|
954
|
+
const { FOCUS_TIERS } = require('./constants.cjs');
|
|
955
|
+
const safe = Array.isArray(items) ? items : [];
|
|
956
|
+
const micro = safe.filter(i => i && i.tier === FOCUS_TIERS.MICRO);
|
|
957
|
+
const standard = safe.filter(i => i && i.tier === FOCUS_TIERS.STANDARD);
|
|
958
|
+
const full = safe.filter(i => i && i.tier === FOCUS_TIERS.FULL);
|
|
959
|
+
|
|
960
|
+
const waves = [];
|
|
961
|
+
if (micro.length) waves.push(micro);
|
|
962
|
+
if (standard.length) waves.push(standard);
|
|
963
|
+
for (const f of full) waves.push([f]);
|
|
964
|
+
|
|
965
|
+
let hint = 'sequential';
|
|
966
|
+
if (micro.length >= 2) hint = 'emit-micro-in-parallel';
|
|
967
|
+
else if (standard.length >= 2) hint = 'emit-standard-in-parallel';
|
|
968
|
+
|
|
969
|
+
return { waves, parallelism_hint: hint };
|
|
970
|
+
}
|
|
971
|
+
|
|
877
972
|
// ─── Module exports ─────────────────────────────────────────────────────────
|
|
878
973
|
|
|
879
974
|
module.exports = {
|
|
@@ -902,4 +997,7 @@ module.exports = {
|
|
|
902
997
|
writeAutoRun,
|
|
903
998
|
cmdFocusAuto,
|
|
904
999
|
determineStopReason,
|
|
1000
|
+
// Opus 4.7
|
|
1001
|
+
determineContinuation,
|
|
1002
|
+
classifyStageDependencies,
|
|
905
1003
|
};
|
|
@@ -236,13 +236,13 @@ function cmdInitPlanPhase(cwd, phase, raw) {
|
|
|
236
236
|
nyquist_validation_enabled: config.nyquist_validation,
|
|
237
237
|
commit_docs: config.commit_docs,
|
|
238
238
|
|
|
239
|
-
// Phase info
|
|
239
|
+
// Phase info — fall back to roadmap data when directory doesn't exist yet
|
|
240
240
|
phase_found: !!phaseInfo,
|
|
241
241
|
phase_dir: phaseInfo?.directory || null,
|
|
242
|
-
phase_number: phaseInfo?.phase_number || null,
|
|
243
|
-
phase_name: phaseInfo?.phase_name || null,
|
|
244
|
-
phase_slug: phaseInfo?.phase_slug || null,
|
|
245
|
-
padded_phase: phaseInfo?.phase_number?.padStart(2, '0') || null,
|
|
242
|
+
phase_number: phaseInfo?.phase_number || roadmapPhase?.phase_number || null,
|
|
243
|
+
phase_name: phaseInfo?.phase_name || roadmapPhase?.phase_name || null,
|
|
244
|
+
phase_slug: phaseInfo?.phase_slug || (roadmapPhase?.phase_name ? generateSlugInternal(roadmapPhase.phase_name) : null),
|
|
245
|
+
padded_phase: (phaseInfo?.phase_number || roadmapPhase?.phase_number)?.toString().padStart(2, '0') || null,
|
|
246
246
|
phase_req_ids: phaseReqIds,
|
|
247
247
|
|
|
248
248
|
// Existing artifacts
|