@planu/cli 4.5.0 → 4.6.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/CHANGELOG.md +10 -0
- package/dist/config/token-waste-autopilot.json +48 -0
- package/dist/engine/token-optimizer/autopilot.d.ts +5 -0
- package/dist/engine/token-optimizer/autopilot.js +93 -0
- package/dist/engine/token-optimizer/context-preflight.d.ts +3 -0
- package/dist/engine/token-optimizer/context-preflight.js +64 -0
- package/dist/engine/token-optimizer/index.d.ts +6 -0
- package/dist/engine/token-optimizer/index.js +6 -0
- package/dist/engine/token-optimizer/loop-detector.d.ts +3 -0
- package/dist/engine/token-optimizer/loop-detector.js +32 -0
- package/dist/engine/token-optimizer/output-filter.d.ts +3 -0
- package/dist/engine/token-optimizer/output-filter.js +67 -0
- package/dist/engine/token-optimizer/policy-loader.d.ts +3 -0
- package/dist/engine/token-optimizer/policy-loader.js +83 -0
- package/dist/engine/token-optimizer/tool-relevance.d.ts +6 -0
- package/dist/engine/token-optimizer/tool-relevance.js +57 -0
- package/dist/tools/create-spec/post-creation.d.ts +2 -1
- package/dist/tools/create-spec/post-creation.js +32 -0
- package/dist/tools/package-handoff.js +47 -1
- package/dist/tools/schemas/token-intelligence.d.ts +1 -0
- package/dist/tools/schemas/token-intelligence.js +3 -2
- package/dist/tools/status-handler.js +17 -2
- package/dist/tools/token-intelligence-handler.js +46 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/token-waste-autopilot.d.ts +142 -0
- package/dist/types/token-waste-autopilot.js +2 -0
- package/package.json +11 -11
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"enabled": true,
|
|
4
|
+
"context": {
|
|
5
|
+
"generatedPatterns": ["dist/**", "coverage/**", ".next/**", "build/**"],
|
|
6
|
+
"stalePatterns": ["*.log", "*.bak", "*.tmp"],
|
|
7
|
+
"summarizePatterns": ["pnpm-lock.yaml", "package-lock.json", "yarn.lock"],
|
|
8
|
+
"staleSessionMinutes": 60,
|
|
9
|
+
"safetyOverrideActions": ["release", "security", "license", "lockfile", "exact-reproduction"]
|
|
10
|
+
},
|
|
11
|
+
"outputs": {
|
|
12
|
+
"strategies": {
|
|
13
|
+
"test": { "maxLines": 40, "keepFailures": true },
|
|
14
|
+
"log": { "maxLines": 30, "uniqueOnly": true },
|
|
15
|
+
"json": { "maxLines": 40, "summarizeKeys": true },
|
|
16
|
+
"dependency": { "maxLines": 50, "keepFailures": true },
|
|
17
|
+
"generic": { "maxLines": 60 }
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"tools": {
|
|
21
|
+
"groups": {
|
|
22
|
+
"spec": ["create_spec", "check_readiness", "challenge_spec", "update_status"],
|
|
23
|
+
"handoff": ["package_handoff", "generate_execution_plan"],
|
|
24
|
+
"status": ["planu_status", "session_checkpoint"],
|
|
25
|
+
"token": ["token_intelligence", "tokens", "context_budget"]
|
|
26
|
+
},
|
|
27
|
+
"maxRecommended": 8
|
|
28
|
+
},
|
|
29
|
+
"loops": {
|
|
30
|
+
"thresholds": {
|
|
31
|
+
"file_read": 3,
|
|
32
|
+
"search": 3,
|
|
33
|
+
"test_run": 2,
|
|
34
|
+
"failure": 2
|
|
35
|
+
},
|
|
36
|
+
"safetyOverrideActions": ["release", "security", "license"]
|
|
37
|
+
},
|
|
38
|
+
"modelEffort": {
|
|
39
|
+
"lowRisk": "low",
|
|
40
|
+
"default": "medium",
|
|
41
|
+
"highRisk": "high",
|
|
42
|
+
"maxRisk": "max"
|
|
43
|
+
},
|
|
44
|
+
"redaction": {
|
|
45
|
+
"maxSnippetChars": 240,
|
|
46
|
+
"redactPatterns": ["secret", "token", "password", "api_key"]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { TokenWasteAutopilotInput, TokenWasteReport } from '../../types/token-waste-autopilot.js';
|
|
2
|
+
export declare function redactSnippet(text: string, patterns: string[], maxChars: number): string;
|
|
3
|
+
export declare function buildTokenWasteReport(input: TokenWasteAutopilotInput): TokenWasteReport;
|
|
4
|
+
export declare function formatTokenWasteReport(report: TokenWasteReport): string;
|
|
5
|
+
//# sourceMappingURL=autopilot.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
function collectEvidence(decisions) {
|
|
2
|
+
return decisions.flatMap((decision) => decision.evidence);
|
|
3
|
+
}
|
|
4
|
+
function modelAdvice(input) {
|
|
5
|
+
const risk = input.spec?.risk ?? 'medium';
|
|
6
|
+
const effort = risk === 'critical'
|
|
7
|
+
? input.policy.modelEffort.maxRisk
|
|
8
|
+
: risk === 'high'
|
|
9
|
+
? input.policy.modelEffort.highRisk
|
|
10
|
+
: risk === 'low'
|
|
11
|
+
? input.policy.modelEffort.lowRisk
|
|
12
|
+
: input.policy.modelEffort.default;
|
|
13
|
+
return {
|
|
14
|
+
effort,
|
|
15
|
+
reason: `Selected ${effort} effort from spec risk ${risk}.`,
|
|
16
|
+
evidence: [
|
|
17
|
+
{ source: 'spec', key: 'risk', value: risk },
|
|
18
|
+
{ source: 'policy', key: 'modelEffort' },
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function escapeInert(text) {
|
|
23
|
+
return text.replace(/</g, '<').replace(/>/g, '>');
|
|
24
|
+
}
|
|
25
|
+
export function redactSnippet(text, patterns, maxChars) {
|
|
26
|
+
const truncated = text.slice(0, maxChars);
|
|
27
|
+
return patterns.reduce((current, pattern) => {
|
|
28
|
+
const re = new RegExp(pattern, 'gi');
|
|
29
|
+
return current.replace(re, '[redacted]');
|
|
30
|
+
}, truncated);
|
|
31
|
+
}
|
|
32
|
+
function sanitizeDecision(decision, patterns, maxChars) {
|
|
33
|
+
return {
|
|
34
|
+
...decision,
|
|
35
|
+
target: redactSnippet(decision.target, patterns, maxChars),
|
|
36
|
+
reason: redactSnippet(decision.reason, patterns, maxChars),
|
|
37
|
+
evidence: decision.evidence.map((item) => ({
|
|
38
|
+
...item,
|
|
39
|
+
...(typeof item.value === 'string'
|
|
40
|
+
? { value: redactSnippet(item.value, patterns, maxChars) }
|
|
41
|
+
: {}),
|
|
42
|
+
})),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function sanitizeDecisions(decisions, input) {
|
|
46
|
+
return decisions.map((decision) => sanitizeDecision(decision, input.policy.redaction.redactPatterns, input.policy.redaction.maxSnippetChars));
|
|
47
|
+
}
|
|
48
|
+
export function buildTokenWasteReport(input) {
|
|
49
|
+
const actionsTaken = sanitizeDecisions([
|
|
50
|
+
...(input.context?.summarize ?? []),
|
|
51
|
+
...(input.context?.exclude ?? []),
|
|
52
|
+
...(input.output?.decisions ?? []),
|
|
53
|
+
], input);
|
|
54
|
+
const recommendations = sanitizeDecisions([...(input.context?.include ?? []), ...(input.tools?.recommended ?? [])], input);
|
|
55
|
+
const risks = sanitizeDecisions([...(input.tools?.avoided ?? []), ...(input.loops?.warnings ?? [])], input);
|
|
56
|
+
const modelEffort = modelAdvice(input);
|
|
57
|
+
return {
|
|
58
|
+
actionsTaken,
|
|
59
|
+
recommendations,
|
|
60
|
+
risks,
|
|
61
|
+
evidence: [
|
|
62
|
+
...collectEvidence(actionsTaken),
|
|
63
|
+
...collectEvidence(recommendations),
|
|
64
|
+
...collectEvidence(risks),
|
|
65
|
+
...modelEffort.evidence,
|
|
66
|
+
],
|
|
67
|
+
modelEffort,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function formatTokenWasteReport(report) {
|
|
71
|
+
const lines = ['## Token Waste Autopilot', ''];
|
|
72
|
+
lines.push(`Model effort: ${escapeInert(report.modelEffort.effort)} — ${escapeInert(report.modelEffort.reason)}`);
|
|
73
|
+
if (report.actionsTaken.length > 0) {
|
|
74
|
+
lines.push('', 'Actions taken:');
|
|
75
|
+
for (const item of report.actionsTaken.slice(0, 6)) {
|
|
76
|
+
lines.push(`- ${escapeInert(item.decision)}: ${escapeInert(item.target)} — ${escapeInert(item.reason)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (report.recommendations.length > 0) {
|
|
80
|
+
lines.push('', 'Recommendations:');
|
|
81
|
+
for (const item of report.recommendations.slice(0, 6)) {
|
|
82
|
+
lines.push(`- ${escapeInert(item.target)}: ${escapeInert(item.reason)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (report.risks.length > 0) {
|
|
86
|
+
lines.push('', 'Risks:');
|
|
87
|
+
for (const item of report.risks.slice(0, 6)) {
|
|
88
|
+
lines.push(`- ${escapeInert(item.target)}: ${escapeInert(item.reason)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=autopilot.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
function globToRegExp(pattern) {
|
|
2
|
+
const escaped = pattern
|
|
3
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
4
|
+
.replace(/\*\*/g, '::DOUBLE_STAR::')
|
|
5
|
+
.replace(/\*/g, '[^/]*')
|
|
6
|
+
.replace(/::DOUBLE_STAR::/g, '.*');
|
|
7
|
+
return new RegExp(`^${escaped}$`);
|
|
8
|
+
}
|
|
9
|
+
function matchesAny(path, patterns) {
|
|
10
|
+
return patterns.find((pattern) => globToRegExp(pattern).test(path));
|
|
11
|
+
}
|
|
12
|
+
function decision(kind, target, reason, key) {
|
|
13
|
+
return {
|
|
14
|
+
decision: kind,
|
|
15
|
+
target,
|
|
16
|
+
reason,
|
|
17
|
+
evidence: [{ source: 'policy', key }],
|
|
18
|
+
confidence: 'high',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function hasSafetyOverride(action, policy) {
|
|
22
|
+
return policy.context.safetyOverrideActions.includes(action);
|
|
23
|
+
}
|
|
24
|
+
function classifyCandidate(candidate, input) {
|
|
25
|
+
if (hasSafetyOverride(input.action, input.policy)) {
|
|
26
|
+
return decision('override', candidate.path, `Action ${input.action} requires exact context according to policy.`, 'context.safetyOverrideActions');
|
|
27
|
+
}
|
|
28
|
+
const generated = matchesAny(candidate.path, input.policy.context.generatedPatterns);
|
|
29
|
+
if (generated !== undefined) {
|
|
30
|
+
return decision('exclude', candidate.path, `Matches generated context pattern ${generated}.`, 'context.generatedPatterns');
|
|
31
|
+
}
|
|
32
|
+
const stale = matchesAny(candidate.path, input.policy.context.stalePatterns);
|
|
33
|
+
if (stale !== undefined) {
|
|
34
|
+
return decision('exclude', candidate.path, `Matches stale context pattern ${stale}.`, 'context.stalePatterns');
|
|
35
|
+
}
|
|
36
|
+
const summarize = matchesAny(candidate.path, input.policy.context.summarizePatterns);
|
|
37
|
+
if (summarize !== undefined) {
|
|
38
|
+
return decision('summarize', candidate.path, `Matches summarizable context pattern ${summarize}.`, 'context.summarizePatterns');
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
decision: 'include',
|
|
42
|
+
target: candidate.path,
|
|
43
|
+
reason: candidate.reason ?? 'Candidate context is relevant and not excluded by policy.',
|
|
44
|
+
evidence: [{ source: 'runtime', key: 'candidate.path', value: candidate.path }],
|
|
45
|
+
confidence: 'medium',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function analyzeContextPreflight(input) {
|
|
49
|
+
const result = { include: [], summarize: [], exclude: [] };
|
|
50
|
+
for (const candidate of input.candidates) {
|
|
51
|
+
const item = classifyCandidate(candidate, input);
|
|
52
|
+
if (item.decision === 'exclude') {
|
|
53
|
+
result.exclude.push(item);
|
|
54
|
+
}
|
|
55
|
+
else if (item.decision === 'summarize') {
|
|
56
|
+
result.summarize.push(item);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
result.include.push(item);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=context-preflight.js.map
|
|
@@ -8,5 +8,11 @@ export { BudgetManager } from './budget.js';
|
|
|
8
8
|
export { OptimizationReporter } from './reporter.js';
|
|
9
9
|
export { TokenOptimizer } from './optimizer.js';
|
|
10
10
|
export { aggregateEntries, computeDailyBreakdown, detectTrend, detectAnomalies, computeBudgetStatus, getTopConsumers, computeOptimizationSavings, } from './analytics.js';
|
|
11
|
+
export { loadTokenWastePolicy } from './policy-loader.js';
|
|
12
|
+
export { analyzeContextPreflight } from './context-preflight.js';
|
|
13
|
+
export { filterVerboseOutput } from './output-filter.js';
|
|
14
|
+
export { recommendRelevantTools, toolsFromPolicyGroups } from './tool-relevance.js';
|
|
15
|
+
export { detectTokenWasteLoops } from './loop-detector.js';
|
|
16
|
+
export { buildTokenWasteReport, formatTokenWasteReport, redactSnippet } from './autopilot.js';
|
|
11
17
|
export type { DailyBreakdown, TrendResult, Anomaly, TopConsumer, OptimizationSavings, } from './analytics.js';
|
|
12
18
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -9,4 +9,10 @@ export { BudgetManager } from './budget.js';
|
|
|
9
9
|
export { OptimizationReporter } from './reporter.js';
|
|
10
10
|
export { TokenOptimizer } from './optimizer.js';
|
|
11
11
|
export { aggregateEntries, computeDailyBreakdown, detectTrend, detectAnomalies, computeBudgetStatus, getTopConsumers, computeOptimizationSavings, } from './analytics.js';
|
|
12
|
+
export { loadTokenWastePolicy } from './policy-loader.js';
|
|
13
|
+
export { analyzeContextPreflight } from './context-preflight.js';
|
|
14
|
+
export { filterVerboseOutput } from './output-filter.js';
|
|
15
|
+
export { recommendRelevantTools, toolsFromPolicyGroups } from './tool-relevance.js';
|
|
16
|
+
export { detectTokenWasteLoops } from './loop-detector.js';
|
|
17
|
+
export { buildTokenWasteReport, formatTokenWasteReport, redactSnippet } from './autopilot.js';
|
|
12
18
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { TokenWasteLoopResult, TokenWastePolicy, TokenWasteRuntimeEvent } from '../../types/token-waste-autopilot.js';
|
|
2
|
+
export declare function detectTokenWasteLoops(events: TokenWasteRuntimeEvent[], policy: TokenWastePolicy, action?: string): TokenWasteLoopResult;
|
|
3
|
+
//# sourceMappingURL=loop-detector.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function detectTokenWasteLoops(events, policy, action = 'default') {
|
|
2
|
+
if (policy.loops.safetyOverrideActions.includes(action)) {
|
|
3
|
+
return { warnings: [] };
|
|
4
|
+
}
|
|
5
|
+
const counts = new Map();
|
|
6
|
+
for (const event of events) {
|
|
7
|
+
if (event.changedSincePrevious === true) {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
const key = `${event.type}:${event.key}`;
|
|
11
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
12
|
+
}
|
|
13
|
+
const warnings = [];
|
|
14
|
+
for (const [key, count] of counts) {
|
|
15
|
+
const [type = 'unknown', target = key] = key.split(':');
|
|
16
|
+
const threshold = policy.loops.thresholds[type] ?? Number.POSITIVE_INFINITY;
|
|
17
|
+
if (count >= threshold) {
|
|
18
|
+
warnings.push({
|
|
19
|
+
decision: 'warn',
|
|
20
|
+
target,
|
|
21
|
+
reason: `Repeated ${type} operation detected ${String(count)} times without an intervening change.`,
|
|
22
|
+
evidence: [
|
|
23
|
+
{ source: 'policy', key: `loops.thresholds.${type}`, value: threshold },
|
|
24
|
+
{ source: 'runtime', key: 'event.count', value: count },
|
|
25
|
+
],
|
|
26
|
+
confidence: 'high',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { warnings };
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=loop-detector.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function escapeInert(text) {
|
|
2
|
+
return text.replace(/</g, '<').replace(/>/g, '>');
|
|
3
|
+
}
|
|
4
|
+
function redact(text, patterns) {
|
|
5
|
+
return patterns.reduce((current, pattern) => {
|
|
6
|
+
const re = new RegExp(pattern, 'gi');
|
|
7
|
+
return current.replace(re, '[redacted]');
|
|
8
|
+
}, text);
|
|
9
|
+
}
|
|
10
|
+
function uniqueLines(lines) {
|
|
11
|
+
return [...new Set(lines)];
|
|
12
|
+
}
|
|
13
|
+
function failureLines(lines) {
|
|
14
|
+
const failures = lines.filter((line) => /\b(error|fail|failed|exception|critical)\b/i.test(line));
|
|
15
|
+
return failures.length > 0 ? failures : lines;
|
|
16
|
+
}
|
|
17
|
+
function jsonSummary(text) {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(text);
|
|
20
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
21
|
+
return [`JSON object keys: ${Object.keys(parsed).join(', ')}`];
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(parsed)) {
|
|
24
|
+
return [`JSON array length: ${String(parsed.length)}`];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Fall through to normal line handling.
|
|
29
|
+
}
|
|
30
|
+
return text.split('\n');
|
|
31
|
+
}
|
|
32
|
+
function selectStrategy(input) {
|
|
33
|
+
const strategy = input.policy.outputs.strategies[input.kind] ?? input.policy.outputs.strategies.generic;
|
|
34
|
+
if (strategy === undefined) {
|
|
35
|
+
throw new Error('Token waste policy must define outputs.strategies.generic');
|
|
36
|
+
}
|
|
37
|
+
return strategy;
|
|
38
|
+
}
|
|
39
|
+
function compactLines(input, strategy) {
|
|
40
|
+
const rawLines = strategy.summarizeKeys === true ? jsonSummary(input.text) : input.text.split('\n');
|
|
41
|
+
const selected = strategy.keepFailures === true ? failureLines(rawLines) : rawLines;
|
|
42
|
+
const unique = strategy.uniqueOnly === true ? uniqueLines(selected) : selected;
|
|
43
|
+
return unique.slice(0, strategy.maxLines);
|
|
44
|
+
}
|
|
45
|
+
export function filterVerboseOutput(input) {
|
|
46
|
+
const strategy = selectStrategy(input);
|
|
47
|
+
const originalLines = input.text.split('\n').length;
|
|
48
|
+
const lines = compactLines(input, strategy);
|
|
49
|
+
const compact = redact(escapeInert(lines.join('\n')), input.policy.redaction.redactPatterns);
|
|
50
|
+
const decisions = [
|
|
51
|
+
{
|
|
52
|
+
decision: originalLines > lines.length ? 'summarize' : 'include',
|
|
53
|
+
target: input.kind,
|
|
54
|
+
reason: `Applied policy output strategy for ${input.kind}.`,
|
|
55
|
+
evidence: [{ source: 'policy', key: `outputs.strategies.${input.kind}` }],
|
|
56
|
+
confidence: input.policy.outputs.strategies[input.kind] !== undefined ? 'high' : 'medium',
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
return {
|
|
60
|
+
text: compact,
|
|
61
|
+
originalLines,
|
|
62
|
+
returnedLines: lines.length,
|
|
63
|
+
...(input.fullOutputRef !== undefined ? { fullOutputRef: input.fullOutputRef } : {}),
|
|
64
|
+
decisions,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=output-filter.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { LoadedTokenWastePolicy, TokenWastePolicyLoadOptions } from '../../types/token-waste-autopilot.js';
|
|
2
|
+
export declare function loadTokenWastePolicy(projectPath?: string, options?: TokenWastePolicyLoadOptions): Promise<LoadedTokenWastePolicy>;
|
|
3
|
+
//# sourceMappingURL=policy-loader.d.ts.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
function configPath() {
|
|
5
|
+
return join(dirname(fileURLToPath(import.meta.url)), '../../config/token-waste-autopilot.json');
|
|
6
|
+
}
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function mergeDeep(base, override) {
|
|
11
|
+
const result = { ...base };
|
|
12
|
+
for (const [key, value] of Object.entries(override)) {
|
|
13
|
+
const current = result[key];
|
|
14
|
+
result[key] = isRecord(current) && isRecord(value) ? mergeDeep(current, value) : value;
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
function assertStringArray(value, path) {
|
|
19
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
|
|
20
|
+
throw new Error(`Invalid token waste policy at ${path}: expected string[]`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function validatePolicy(value) {
|
|
24
|
+
if (!isRecord(value)) {
|
|
25
|
+
throw new Error('Invalid token waste policy: expected object');
|
|
26
|
+
}
|
|
27
|
+
if (value.version !== 1) {
|
|
28
|
+
throw new Error('Invalid token waste policy: version must be 1');
|
|
29
|
+
}
|
|
30
|
+
const policy = value;
|
|
31
|
+
assertStringArray(policy.context.generatedPatterns, 'context.generatedPatterns');
|
|
32
|
+
assertStringArray(policy.context.stalePatterns, 'context.stalePatterns');
|
|
33
|
+
assertStringArray(policy.context.summarizePatterns, 'context.summarizePatterns');
|
|
34
|
+
if (!Number.isFinite(policy.context.staleSessionMinutes) ||
|
|
35
|
+
policy.context.staleSessionMinutes < 1) {
|
|
36
|
+
throw new Error('Invalid token waste policy at context.staleSessionMinutes: expected positive number');
|
|
37
|
+
}
|
|
38
|
+
assertStringArray(policy.context.safetyOverrideActions, 'context.safetyOverrideActions');
|
|
39
|
+
if (!isRecord(policy.outputs.strategies)) {
|
|
40
|
+
throw new Error('Invalid token waste policy at outputs.strategies: expected object');
|
|
41
|
+
}
|
|
42
|
+
if (!isRecord(policy.tools.groups)) {
|
|
43
|
+
throw new Error('Invalid token waste policy at tools.groups: expected object');
|
|
44
|
+
}
|
|
45
|
+
for (const [group, tools] of Object.entries(policy.tools.groups)) {
|
|
46
|
+
assertStringArray(tools, `tools.groups.${group}`);
|
|
47
|
+
}
|
|
48
|
+
if (!Number.isFinite(policy.tools.maxRecommended) || policy.tools.maxRecommended < 1) {
|
|
49
|
+
throw new Error('Invalid token waste policy at tools.maxRecommended: expected positive number');
|
|
50
|
+
}
|
|
51
|
+
if (!isRecord(policy.loops.thresholds)) {
|
|
52
|
+
throw new Error('Invalid token waste policy at loops.thresholds: expected object');
|
|
53
|
+
}
|
|
54
|
+
assertStringArray(policy.loops.safetyOverrideActions, 'loops.safetyOverrideActions');
|
|
55
|
+
assertStringArray(policy.redaction.redactPatterns, 'redaction.redactPatterns');
|
|
56
|
+
return policy;
|
|
57
|
+
}
|
|
58
|
+
async function readJson(path) {
|
|
59
|
+
return JSON.parse(await readFile(path, 'utf-8'));
|
|
60
|
+
}
|
|
61
|
+
export async function loadTokenWastePolicy(projectPath, options = {}) {
|
|
62
|
+
const defaultPolicyPath = options.defaultPolicyPath ?? configPath();
|
|
63
|
+
const defaultPolicy = validatePolicy(await readJson(defaultPolicyPath));
|
|
64
|
+
const evidence = [
|
|
65
|
+
{ source: 'policy', key: defaultPolicyPath },
|
|
66
|
+
];
|
|
67
|
+
let merged = defaultPolicy;
|
|
68
|
+
if (projectPath !== undefined) {
|
|
69
|
+
const planuConfigPath = join(projectPath, 'planu.json');
|
|
70
|
+
try {
|
|
71
|
+
const config = await readJson(planuConfigPath);
|
|
72
|
+
if (isRecord(config) && isRecord(config.tokenWasteAutopilot)) {
|
|
73
|
+
merged = validatePolicy(mergeDeep(defaultPolicy, config.tokenWasteAutopilot));
|
|
74
|
+
evidence.push({ source: 'project-config', key: 'planu.json.tokenWasteAutopilot' });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Missing or malformed project config does not block default policy loading.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { policy: merged, evidence };
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=policy-loader.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TokenWastePolicy, ToolRelevanceInput, ToolRelevanceResult } from '../../types/token-waste-autopilot.js';
|
|
2
|
+
export declare function toolsFromPolicyGroups(policy: TokenWastePolicy): {
|
|
3
|
+
name: string;
|
|
4
|
+
}[];
|
|
5
|
+
export declare function recommendRelevantTools(input: ToolRelevanceInput): ToolRelevanceResult;
|
|
6
|
+
//# sourceMappingURL=tool-relevance.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
function normalized(text) {
|
|
2
|
+
return text.toLowerCase();
|
|
3
|
+
}
|
|
4
|
+
function matchesIntent(tool, signals) {
|
|
5
|
+
const haystack = normalized(`${tool.name} ${tool.description ?? ''}`);
|
|
6
|
+
return signals.some((signal) => haystack.includes(normalized(signal)));
|
|
7
|
+
}
|
|
8
|
+
function groupNamesForTool(toolName, groups) {
|
|
9
|
+
return Object.entries(groups)
|
|
10
|
+
.filter(([, tools]) => tools.includes(toolName))
|
|
11
|
+
.map(([group]) => group);
|
|
12
|
+
}
|
|
13
|
+
export function toolsFromPolicyGroups(policy) {
|
|
14
|
+
return [...new Set(Object.values(policy.tools.groups).flat())].map((name) => ({ name }));
|
|
15
|
+
}
|
|
16
|
+
export function recommendRelevantTools(input) {
|
|
17
|
+
const unsupported = new Set(input.hostUnsupportedTools ?? []);
|
|
18
|
+
const actionSignals = [input.action, ...input.intentSignals].map(normalized);
|
|
19
|
+
const recommended = [];
|
|
20
|
+
const avoided = [];
|
|
21
|
+
for (const tool of input.tools) {
|
|
22
|
+
const groups = groupNamesForTool(tool.name, input.policy.tools.groups);
|
|
23
|
+
const isUnsupported = unsupported.has(tool.name);
|
|
24
|
+
const isRelevant = !isUnsupported &&
|
|
25
|
+
(matchesIntent(tool, actionSignals) ||
|
|
26
|
+
groups.some((group) => actionSignals.includes(normalized(group))));
|
|
27
|
+
if (isRelevant && recommended.length < input.policy.tools.maxRecommended) {
|
|
28
|
+
recommended.push({
|
|
29
|
+
decision: 'recommend',
|
|
30
|
+
target: tool.name,
|
|
31
|
+
reason: 'Tool matches the current action, intent signals, or policy tool group.',
|
|
32
|
+
evidence: [
|
|
33
|
+
{ source: 'policy', key: 'tools.groups', value: groups.join(',') },
|
|
34
|
+
{ source: 'runtime', key: 'intentSignals', value: input.intentSignals.join(',') },
|
|
35
|
+
],
|
|
36
|
+
confidence: groups.length > 0 ? 'high' : 'medium',
|
|
37
|
+
});
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
avoided.push({
|
|
41
|
+
decision: 'avoid',
|
|
42
|
+
target: tool.name,
|
|
43
|
+
reason: isUnsupported
|
|
44
|
+
? 'Host capability marks this tool unsupported.'
|
|
45
|
+
: 'Tool does not match the current action or intent and can add avoidable tool metadata overhead.',
|
|
46
|
+
evidence: [
|
|
47
|
+
{
|
|
48
|
+
source: isUnsupported ? 'host-capability' : 'policy',
|
|
49
|
+
key: isUnsupported ? 'unsupportedTools' : 'tools.groups',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
confidence: isUnsupported ? 'high' : 'medium',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return { recommended, avoided };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=tool-relevance.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { Spec, PostCreationSuggestion, ProjectKnowledge } from '../../types/index.js';
|
|
1
|
+
import type { Spec, PostCreationSuggestion, ProjectKnowledge, TokenWasteReport } from '../../types/index.js';
|
|
2
2
|
export declare function getAsyncAnalysisPath(projectPath: string, specId: string): string;
|
|
3
|
+
export declare function buildInitialTokenWasteMetadata(specId: string, projectPath: string, description: string): Promise<TokenWasteReport | null>;
|
|
3
4
|
/** Auto-setup git branch (non-blocking). Returns branch info or undefined. */
|
|
4
5
|
export declare function setupGitBranch(projectId: string, specId: string): Promise<{
|
|
5
6
|
branch: string;
|
|
@@ -12,11 +12,42 @@ import { writeFile, mkdir } from 'node:fs/promises';
|
|
|
12
12
|
import { join, dirname } from 'node:path';
|
|
13
13
|
import { hashProjectPath, projectDataDir } from '../../storage/base-store.js';
|
|
14
14
|
import { appendAutopilotLogEntry } from '../../storage/autopilot-log-store.js';
|
|
15
|
+
import { analyzeContextPreflight, buildTokenWasteReport, loadTokenWastePolicy, recommendRelevantTools, toolsFromPolicyGroups, } from '../../engine/token-optimizer/index.js';
|
|
15
16
|
const ASYNC_ANALYSIS_HOOK = 'create-spec-async-analysis';
|
|
16
17
|
export function getAsyncAnalysisPath(projectPath, specId) {
|
|
17
18
|
const projectId = hashProjectPath(projectPath);
|
|
18
19
|
return join(projectDataDir(projectId), 'analysis', `${specId}.json`);
|
|
19
20
|
}
|
|
21
|
+
export async function buildInitialTokenWasteMetadata(specId, projectPath, description) {
|
|
22
|
+
try {
|
|
23
|
+
const { policy } = await loadTokenWastePolicy(projectPath);
|
|
24
|
+
if (!policy.enabled) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const context = analyzeContextPreflight({
|
|
28
|
+
action: 'create_spec',
|
|
29
|
+
policy,
|
|
30
|
+
candidates: [{ path: 'planu/specs', reason: 'New spec workspace metadata.' }],
|
|
31
|
+
spec: { id: specId },
|
|
32
|
+
});
|
|
33
|
+
const tools = recommendRelevantTools({
|
|
34
|
+
action: 'create_spec',
|
|
35
|
+
intentSignals: ['spec', ...description.split(/\s+/).slice(0, 12)],
|
|
36
|
+
tools: toolsFromPolicyGroups(policy),
|
|
37
|
+
policy,
|
|
38
|
+
});
|
|
39
|
+
return buildTokenWasteReport({
|
|
40
|
+
action: 'create_spec',
|
|
41
|
+
policy,
|
|
42
|
+
context,
|
|
43
|
+
tools,
|
|
44
|
+
spec: { id: specId },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
20
51
|
/** Auto-setup git branch (non-blocking). Returns branch info or undefined. */
|
|
21
52
|
export async function setupGitBranch(projectId, specId) {
|
|
22
53
|
const result = await tryAutoSetupGit(projectId, specId);
|
|
@@ -182,6 +213,7 @@ export function runAutopilotAsync(specId, projectPath, _description) {
|
|
|
182
213
|
specId,
|
|
183
214
|
completedAt: new Date().toISOString(),
|
|
184
215
|
pendingAnalysis: false,
|
|
216
|
+
tokenWasteAutopilot: await buildInitialTokenWasteMetadata(specId, projectPath, _description),
|
|
185
217
|
};
|
|
186
218
|
const analysisPath = getAsyncAnalysisPath(projectPath, specId);
|
|
187
219
|
await mkdir(dirname(analysisPath), { recursive: true });
|
|
@@ -3,6 +3,7 @@ import { specStore, knowledgeStore } from '../storage/index.js';
|
|
|
3
3
|
import { checkSpecReadiness } from '../engine/readiness-checker.js';
|
|
4
4
|
import { packageHandoff } from '../engine/handoff-packager.js';
|
|
5
5
|
import { detectParadigms } from '../engine/paradigm-detector.js';
|
|
6
|
+
import { analyzeContextPreflight, buildTokenWasteReport, formatTokenWasteReport, loadTokenWastePolicy, recommendRelevantTools, toolsFromPolicyGroups, } from '../engine/token-optimizer/index.js';
|
|
6
7
|
import { appendTransitionEvent } from '../storage/transition-log.js';
|
|
7
8
|
// ── Formatting helpers ───────────────────────────────────────────────────────
|
|
8
9
|
function formatHandoff(pkg) {
|
|
@@ -134,6 +135,47 @@ function formatHandoff(pkg) {
|
|
|
134
135
|
}
|
|
135
136
|
return lines.join('\n');
|
|
136
137
|
}
|
|
138
|
+
async function buildHandoffTokenWasteReport(pkg, knowledge, spec) {
|
|
139
|
+
try {
|
|
140
|
+
const { policy } = await loadTokenWastePolicy(knowledge.projectPath);
|
|
141
|
+
if (!policy.enabled) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const context = analyzeContextPreflight({
|
|
145
|
+
action: 'package_handoff',
|
|
146
|
+
policy,
|
|
147
|
+
candidates: [...pkg.filesToModify, ...pkg.filesToCreate].map((path) => ({
|
|
148
|
+
path,
|
|
149
|
+
reason: 'Referenced by handoff package.',
|
|
150
|
+
})),
|
|
151
|
+
spec: {
|
|
152
|
+
id: spec.id,
|
|
153
|
+
risk: spec.risk,
|
|
154
|
+
target: spec.target,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
const tools = recommendRelevantTools({
|
|
158
|
+
action: 'package_handoff',
|
|
159
|
+
intentSignals: ['handoff', spec.scope ?? '', spec.target ?? ''].filter(Boolean),
|
|
160
|
+
tools: toolsFromPolicyGroups(policy),
|
|
161
|
+
policy,
|
|
162
|
+
});
|
|
163
|
+
return buildTokenWasteReport({
|
|
164
|
+
action: 'package_handoff',
|
|
165
|
+
policy,
|
|
166
|
+
context,
|
|
167
|
+
tools,
|
|
168
|
+
spec: {
|
|
169
|
+
id: spec.id,
|
|
170
|
+
risk: spec.risk,
|
|
171
|
+
scope: spec.scope,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
137
179
|
// ── Handler ──────────────────────────────────────────────────────────────────
|
|
138
180
|
export async function handlePackageHandoff(args) {
|
|
139
181
|
const { projectId, specId } = args;
|
|
@@ -188,7 +230,10 @@ export async function handlePackageHandoff(args) {
|
|
|
188
230
|
readinessScore: readinessReport.score,
|
|
189
231
|
constraints: paradigmConstraints,
|
|
190
232
|
};
|
|
191
|
-
const
|
|
233
|
+
const tokenWasteReport = await buildHandoffTokenWasteReport(pkgWithScore, knowledge, spec);
|
|
234
|
+
const formatted = tokenWasteReport !== null
|
|
235
|
+
? `${formatHandoff(pkgWithScore)}\n${formatTokenWasteReport(tokenWasteReport)}`
|
|
236
|
+
: formatHandoff(pkgWithScore);
|
|
192
237
|
void appendTransitionEvent({
|
|
193
238
|
projectId,
|
|
194
239
|
specId,
|
|
@@ -209,6 +254,7 @@ export async function handlePackageHandoff(args) {
|
|
|
209
254
|
blockers: pkgWithScore.blockers,
|
|
210
255
|
handoffPath: pkgWithScore.handoffPath,
|
|
211
256
|
contextHash: pkgWithScore.contextHash,
|
|
257
|
+
...(tokenWasteReport !== null ? { tokenWaste: tokenWasteReport } : {}),
|
|
212
258
|
},
|
|
213
259
|
};
|
|
214
260
|
}
|
|
@@ -9,10 +9,11 @@ export const TokenIntelligenceGroupByEnum = z
|
|
|
9
9
|
.describe('Group results by: tool (per MCP tool name), model (per AI model), ' +
|
|
10
10
|
'spec (per spec ID), day (daily breakdown)');
|
|
11
11
|
export const TokenIntelligenceViewEnum = z
|
|
12
|
-
.enum(['summary', 'detailed', 'budget', 'reconciliation', 'trends'])
|
|
12
|
+
.enum(['summary', 'detailed', 'budget', 'reconciliation', 'trends', 'autopilot'])
|
|
13
13
|
.describe('Dashboard view: summary (high-level overview with top consumers), ' +
|
|
14
14
|
'detailed (full per-tool and per-model breakdown), ' +
|
|
15
15
|
'budget (spending vs monthly limits with projections), ' +
|
|
16
16
|
'reconciliation (estimated vs actual cost accuracy per spec), ' +
|
|
17
|
-
'trends (usage patterns over time, week-over-week changes, and anomaly alerts)'
|
|
17
|
+
'trends (usage patterns over time, week-over-week changes, and anomaly alerts), ' +
|
|
18
|
+
'autopilot (policy-driven recommendations to reduce wasted context, output, and tool usage)');
|
|
18
19
|
//# sourceMappingURL=token-intelligence.js.map
|
|
@@ -17,6 +17,7 @@ import { calculateSLAStatus } from '../engine/approval-workflow.js';
|
|
|
17
17
|
import { readPlanuConfig } from '../engine/planu-config-writer.js';
|
|
18
18
|
import { computePlanuDriftScore } from '../engine/dashboard/drift-score.js';
|
|
19
19
|
import { findStaleBranches, findStaleWorktrees, findStaleStashes, } from '../engine/housekeeping/index.js';
|
|
20
|
+
import { loadTokenWastePolicy } from '../engine/token-optimizer/index.js';
|
|
20
21
|
// SPEC-1011 Bug G: fire-and-forget status.json reconciliation
|
|
21
22
|
import { reconcileStatusFromDisk } from '../engine/status-reconciler/index.js';
|
|
22
23
|
const execFile = promisify(execFileCb);
|
|
@@ -127,7 +128,7 @@ function buildSuggestion(snapshot) {
|
|
|
127
128
|
// Output builder
|
|
128
129
|
// ---------------------------------------------------------------------------
|
|
129
130
|
function buildOutput(params) {
|
|
130
|
-
const { snapshot, git, ci, slaBreaches, workMode, updateBanner, autoCompleted, sessionTip } = params;
|
|
131
|
+
const { snapshot, git, ci, slaBreaches, workMode, updateBanner, autoCompleted, sessionTip, tokenWasteHint, } = params;
|
|
131
132
|
const lines = ['Planu Status', '━━━━━━━━━━━━━━━'];
|
|
132
133
|
if (snapshot.active !== null) {
|
|
133
134
|
const title = snapshot.active.title.slice(0, 40);
|
|
@@ -162,6 +163,9 @@ function buildOutput(params) {
|
|
|
162
163
|
if (sessionTip !== undefined) {
|
|
163
164
|
lines.push(`SESSION TIP ${sessionTip}`);
|
|
164
165
|
}
|
|
166
|
+
if (tokenWasteHint !== undefined) {
|
|
167
|
+
lines.push(`TOKEN WASTE ${tokenWasteHint}`);
|
|
168
|
+
}
|
|
165
169
|
lines.push(`SUGGEST ${buildSuggestion(snapshot)}`);
|
|
166
170
|
if (workMode !== undefined) {
|
|
167
171
|
lines.push(`WORK MODE ${workMode}`);
|
|
@@ -206,7 +210,7 @@ export async function handlePlanStatus(args) {
|
|
|
206
210
|
/* best-effort */
|
|
207
211
|
});
|
|
208
212
|
}
|
|
209
|
-
const [snapshot, git, ci, slaBreaches, planuConfig, autoCompleted, sessionState, driftScore, pendingCleanup,] = await Promise.all([
|
|
213
|
+
const [snapshot, git, ci, slaBreaches, planuConfig, autoCompleted, sessionState, driftScore, pendingCleanup, tokenWastePolicy,] = await Promise.all([
|
|
210
214
|
getStatusSpecSnapshot(projectId),
|
|
211
215
|
getGitState(args.projectPath),
|
|
212
216
|
getCiState(args.projectPath),
|
|
@@ -237,6 +241,9 @@ export async function handlePlanStatus(args) {
|
|
|
237
241
|
return { branches: 0, worktrees: 0, stashes: 0 };
|
|
238
242
|
}
|
|
239
243
|
})(),
|
|
244
|
+
loadTokenWastePolicy(args.projectPath)
|
|
245
|
+
.then((loaded) => loaded.policy)
|
|
246
|
+
.catch(() => null),
|
|
240
247
|
]);
|
|
241
248
|
// SPEC-1012: Detect stale release status (best-effort, non-blocking)
|
|
242
249
|
let staleStatusWarnings = [];
|
|
@@ -252,6 +259,12 @@ export async function handlePlanStatus(args) {
|
|
|
252
259
|
const sessionTip = sessionAgeMinutes !== null && sessionAgeMinutes > SESSION_TIP_THRESHOLD_MIN
|
|
253
260
|
? `Last checkpoint ${String(sessionAgeMinutes)}min ago — run session_handoff then start a fresh session to cut token costs`
|
|
254
261
|
: undefined;
|
|
262
|
+
const tokenWasteThresholdMin = tokenWastePolicy?.context.staleSessionMinutes;
|
|
263
|
+
const tokenWasteHint = tokenWasteThresholdMin !== undefined &&
|
|
264
|
+
sessionAgeMinutes !== null &&
|
|
265
|
+
sessionAgeMinutes > tokenWasteThresholdMin
|
|
266
|
+
? `checkpoint is stale; package or hand off context before continuing`
|
|
267
|
+
: undefined;
|
|
255
268
|
const workMode = planuConfig?.workMode;
|
|
256
269
|
const updateBanner = consumeUpdateBanner() ?? undefined;
|
|
257
270
|
const text = buildOutput({
|
|
@@ -263,6 +276,7 @@ export async function handlePlanStatus(args) {
|
|
|
263
276
|
updateBanner,
|
|
264
277
|
autoCompleted,
|
|
265
278
|
sessionTip,
|
|
279
|
+
tokenWasteHint,
|
|
266
280
|
});
|
|
267
281
|
// SPEC-256: Always show /btw hint as educational tip (no context percent available from MCP layer)
|
|
268
282
|
const DEMO_CONTEXT_PERCENT = 65;
|
|
@@ -286,6 +300,7 @@ export async function handlePlanStatus(args) {
|
|
|
286
300
|
lastAuditAt: driftScore?.lastAuditAt ?? null,
|
|
287
301
|
...(autoCompleted.length > 0 ? { autoCompleted } : {}),
|
|
288
302
|
...(sessionTip !== undefined ? { sessionTip } : {}),
|
|
303
|
+
...(tokenWasteHint !== undefined ? { tokenWasteHint } : {}),
|
|
289
304
|
...(btwHint !== undefined ? { btw_hint: btwHint } : {}),
|
|
290
305
|
...(updateBanner !== undefined ? { update_banner: updateBanner } : {}),
|
|
291
306
|
// SPEC-751: pending cleanup counters
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getEntries, getAggregation } from '../storage/token-ledger-store.js';
|
|
2
2
|
import { hashProjectPath } from '../storage/base-store.js';
|
|
3
|
+
import { buildTokenWasteReport, detectTokenWasteLoops, formatTokenWasteReport, loadTokenWastePolicy, recommendRelevantTools, } from '../engine/token-optimizer/index.js';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// Constants
|
|
@@ -348,6 +349,47 @@ function renderTrends(entries, period) {
|
|
|
348
349
|
}
|
|
349
350
|
return lines.join('\n');
|
|
350
351
|
}
|
|
352
|
+
async function renderAutopilot(entries, agg, period, projectPath) {
|
|
353
|
+
const { policy } = await loadTokenWastePolicy(projectPath);
|
|
354
|
+
if (!policy.enabled) {
|
|
355
|
+
return [
|
|
356
|
+
'# Token Intelligence Dashboard — Autopilot View',
|
|
357
|
+
'',
|
|
358
|
+
`**Period:** ${periodLabel(period)}`,
|
|
359
|
+
'',
|
|
360
|
+
'Token Waste Autopilot is disabled by policy.',
|
|
361
|
+
'',
|
|
362
|
+
].join('\n');
|
|
363
|
+
}
|
|
364
|
+
const loops = detectTokenWasteLoops(entries.map((entry) => ({
|
|
365
|
+
type: 'tool',
|
|
366
|
+
key: entry.toolName,
|
|
367
|
+
timestamp: entry.timestamp,
|
|
368
|
+
changedSincePrevious: false,
|
|
369
|
+
})), policy, 'token_intelligence');
|
|
370
|
+
const tools = recommendRelevantTools({
|
|
371
|
+
action: 'token_intelligence',
|
|
372
|
+
intentSignals: ['token', 'status', 'budget'],
|
|
373
|
+
tools: Object.keys(agg.tokensByTool).map((name) => ({ name })),
|
|
374
|
+
policy,
|
|
375
|
+
});
|
|
376
|
+
const report = buildTokenWasteReport({
|
|
377
|
+
action: 'token_intelligence',
|
|
378
|
+
policy,
|
|
379
|
+
loops,
|
|
380
|
+
tools,
|
|
381
|
+
});
|
|
382
|
+
return [
|
|
383
|
+
'# Token Intelligence Dashboard — Autopilot View',
|
|
384
|
+
'',
|
|
385
|
+
`**Period:** ${periodLabel(period)}`,
|
|
386
|
+
'',
|
|
387
|
+
`Analyzed ${String(entries.length)} ledger entries across ${String(agg.uniqueTools)} tools.`,
|
|
388
|
+
'',
|
|
389
|
+
formatTokenWasteReport(report),
|
|
390
|
+
'',
|
|
391
|
+
].join('\n');
|
|
392
|
+
}
|
|
351
393
|
// ---------------------------------------------------------------------------
|
|
352
394
|
// Empty state
|
|
353
395
|
// ---------------------------------------------------------------------------
|
|
@@ -387,7 +429,7 @@ export async function handleTokenIntelligence(input) {
|
|
|
387
429
|
getEntries(projectHash, dataDir, filter),
|
|
388
430
|
getAggregation(projectHash, dataDir, filter),
|
|
389
431
|
]);
|
|
390
|
-
if (entries.length === 0 && view !== 'reconciliation') {
|
|
432
|
+
if (entries.length === 0 && view !== 'reconciliation' && view !== 'autopilot') {
|
|
391
433
|
return { content: [{ type: 'text', text: renderEmpty(view, period) }] };
|
|
392
434
|
}
|
|
393
435
|
let text;
|
|
@@ -404,6 +446,9 @@ export async function handleTokenIntelligence(input) {
|
|
|
404
446
|
case 'trends':
|
|
405
447
|
text = renderTrends(entries, period);
|
|
406
448
|
break;
|
|
449
|
+
case 'autopilot':
|
|
450
|
+
text = await renderAutopilot(entries, agg, period, projectPath);
|
|
451
|
+
break;
|
|
407
452
|
default:
|
|
408
453
|
text = renderSummary(agg, entries, period, groupBy);
|
|
409
454
|
break;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -79,6 +79,7 @@ export * from './runtime-security.js';
|
|
|
79
79
|
export * from './workers.js';
|
|
80
80
|
export * from './orchestration-runtime.js';
|
|
81
81
|
export * from './token-optimization.js';
|
|
82
|
+
export * from './token-waste-autopilot.js';
|
|
82
83
|
export * from './llm-providers.js';
|
|
83
84
|
export * from './plugins.js';
|
|
84
85
|
export * from './github.js';
|
package/dist/types/index.js
CHANGED
|
@@ -80,6 +80,7 @@ export * from './runtime-security.js';
|
|
|
80
80
|
export * from './workers.js';
|
|
81
81
|
export * from './orchestration-runtime.js';
|
|
82
82
|
export * from './token-optimization.js';
|
|
83
|
+
export * from './token-waste-autopilot.js';
|
|
83
84
|
export * from './llm-providers.js';
|
|
84
85
|
export * from './plugins.js';
|
|
85
86
|
export * from './github.js';
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export type TokenWasteDecisionKind = 'include' | 'summarize' | 'exclude' | 'recommend' | 'avoid' | 'warn' | 'override';
|
|
2
|
+
export type TokenWasteConfidence = 'high' | 'medium' | 'low';
|
|
3
|
+
export type TokenWasteAction = string;
|
|
4
|
+
export interface TokenWastePolicyLoadOptions {
|
|
5
|
+
defaultPolicyPath?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TokenWasteEvidence {
|
|
8
|
+
source: 'policy' | 'project-config' | 'runtime' | 'spec' | 'tool-registry' | 'host-capability';
|
|
9
|
+
key: string;
|
|
10
|
+
value?: string | number | boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface TokenWasteDecision {
|
|
13
|
+
decision: TokenWasteDecisionKind;
|
|
14
|
+
target: string;
|
|
15
|
+
reason: string;
|
|
16
|
+
evidence: TokenWasteEvidence[];
|
|
17
|
+
confidence: TokenWasteConfidence;
|
|
18
|
+
}
|
|
19
|
+
export interface TokenWasteOutputStrategy {
|
|
20
|
+
maxLines: number;
|
|
21
|
+
keepFailures?: boolean;
|
|
22
|
+
uniqueOnly?: boolean;
|
|
23
|
+
summarizeKeys?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface TokenWastePolicy {
|
|
26
|
+
version: 1;
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
context: {
|
|
29
|
+
generatedPatterns: string[];
|
|
30
|
+
stalePatterns: string[];
|
|
31
|
+
summarizePatterns: string[];
|
|
32
|
+
staleSessionMinutes: number;
|
|
33
|
+
safetyOverrideActions: string[];
|
|
34
|
+
};
|
|
35
|
+
outputs: {
|
|
36
|
+
strategies: Record<string, TokenWasteOutputStrategy>;
|
|
37
|
+
};
|
|
38
|
+
tools: {
|
|
39
|
+
groups: Record<string, string[]>;
|
|
40
|
+
maxRecommended: number;
|
|
41
|
+
};
|
|
42
|
+
loops: {
|
|
43
|
+
thresholds: Record<string, number>;
|
|
44
|
+
safetyOverrideActions: string[];
|
|
45
|
+
};
|
|
46
|
+
modelEffort: {
|
|
47
|
+
lowRisk: string;
|
|
48
|
+
default: string;
|
|
49
|
+
highRisk: string;
|
|
50
|
+
maxRisk: string;
|
|
51
|
+
};
|
|
52
|
+
redaction: {
|
|
53
|
+
maxSnippetChars: number;
|
|
54
|
+
redactPatterns: string[];
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export interface LoadedTokenWastePolicy {
|
|
58
|
+
policy: TokenWastePolicy;
|
|
59
|
+
evidence: TokenWasteEvidence[];
|
|
60
|
+
}
|
|
61
|
+
export interface ContextCandidate {
|
|
62
|
+
path: string;
|
|
63
|
+
reason?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface ContextPreflightInput {
|
|
66
|
+
action: TokenWasteAction;
|
|
67
|
+
policy: TokenWastePolicy;
|
|
68
|
+
candidates: ContextCandidate[];
|
|
69
|
+
spec?: {
|
|
70
|
+
id?: string;
|
|
71
|
+
status?: string;
|
|
72
|
+
risk?: string;
|
|
73
|
+
target?: string;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export interface ContextPreflightResult {
|
|
77
|
+
include: TokenWasteDecision[];
|
|
78
|
+
summarize: TokenWasteDecision[];
|
|
79
|
+
exclude: TokenWasteDecision[];
|
|
80
|
+
}
|
|
81
|
+
export interface VerboseOutputInput {
|
|
82
|
+
kind: string;
|
|
83
|
+
text: string;
|
|
84
|
+
policy: TokenWastePolicy;
|
|
85
|
+
fullOutputRef?: string;
|
|
86
|
+
}
|
|
87
|
+
export interface VerboseOutputResult {
|
|
88
|
+
text: string;
|
|
89
|
+
originalLines: number;
|
|
90
|
+
returnedLines: number;
|
|
91
|
+
fullOutputRef?: string;
|
|
92
|
+
decisions: TokenWasteDecision[];
|
|
93
|
+
}
|
|
94
|
+
export interface ToolRelevanceInput {
|
|
95
|
+
action: TokenWasteAction;
|
|
96
|
+
intentSignals: string[];
|
|
97
|
+
tools: {
|
|
98
|
+
name: string;
|
|
99
|
+
description?: string;
|
|
100
|
+
}[];
|
|
101
|
+
policy: TokenWastePolicy;
|
|
102
|
+
hostUnsupportedTools?: string[];
|
|
103
|
+
}
|
|
104
|
+
export interface ToolRelevanceResult {
|
|
105
|
+
recommended: TokenWasteDecision[];
|
|
106
|
+
avoided: TokenWasteDecision[];
|
|
107
|
+
}
|
|
108
|
+
export interface TokenWasteRuntimeEvent {
|
|
109
|
+
type: string;
|
|
110
|
+
key: string;
|
|
111
|
+
timestamp?: string;
|
|
112
|
+
changedSincePrevious?: boolean;
|
|
113
|
+
}
|
|
114
|
+
export interface TokenWasteLoopResult {
|
|
115
|
+
warnings: TokenWasteDecision[];
|
|
116
|
+
}
|
|
117
|
+
export interface TokenWasteModelAdvice {
|
|
118
|
+
effort: string;
|
|
119
|
+
reason: string;
|
|
120
|
+
evidence: TokenWasteEvidence[];
|
|
121
|
+
}
|
|
122
|
+
export interface TokenWasteAutopilotInput {
|
|
123
|
+
action: TokenWasteAction;
|
|
124
|
+
policy: TokenWastePolicy;
|
|
125
|
+
context?: ContextPreflightResult;
|
|
126
|
+
output?: VerboseOutputResult;
|
|
127
|
+
tools?: ToolRelevanceResult;
|
|
128
|
+
loops?: TokenWasteLoopResult;
|
|
129
|
+
spec?: {
|
|
130
|
+
id?: string;
|
|
131
|
+
risk?: string;
|
|
132
|
+
scope?: string;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export interface TokenWasteReport {
|
|
136
|
+
actionsTaken: TokenWasteDecision[];
|
|
137
|
+
recommendations: TokenWasteDecision[];
|
|
138
|
+
risks: TokenWasteDecision[];
|
|
139
|
+
evidence: TokenWasteEvidence[];
|
|
140
|
+
modelEffort: TokenWasteModelAdvice;
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=token-waste-autopilot.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0",
|
|
4
4
|
"description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
"packageName": "@planu/core"
|
|
35
35
|
},
|
|
36
36
|
"optionalDependencies": {
|
|
37
|
-
"@planu/core-darwin-arm64": "4.
|
|
38
|
-
"@planu/core-darwin-x64": "4.
|
|
39
|
-
"@planu/core-linux-arm64-gnu": "4.
|
|
40
|
-
"@planu/core-linux-arm64-musl": "4.
|
|
41
|
-
"@planu/core-linux-x64-gnu": "4.
|
|
42
|
-
"@planu/core-linux-x64-musl": "4.
|
|
43
|
-
"@planu/core-win32-arm64-msvc": "4.
|
|
44
|
-
"@planu/core-win32-x64-msvc": "4.
|
|
37
|
+
"@planu/core-darwin-arm64": "4.6.0",
|
|
38
|
+
"@planu/core-darwin-x64": "4.6.0",
|
|
39
|
+
"@planu/core-linux-arm64-gnu": "4.6.0",
|
|
40
|
+
"@planu/core-linux-arm64-musl": "4.6.0",
|
|
41
|
+
"@planu/core-linux-x64-gnu": "4.6.0",
|
|
42
|
+
"@planu/core-linux-x64-musl": "4.6.0",
|
|
43
|
+
"@planu/core-win32-arm64-msvc": "4.6.0",
|
|
44
|
+
"@planu/core-win32-x64-msvc": "4.6.0"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=24.0.0"
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
"@stryker-mutator/core": "^9.6.1",
|
|
183
183
|
"@stryker-mutator/vitest-runner": "^9.6.1",
|
|
184
184
|
"@supabase/supabase-js": "^2.108.1",
|
|
185
|
-
"@types/node": "^25.9.
|
|
185
|
+
"@types/node": "^25.9.3",
|
|
186
186
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
187
187
|
"@vitest/coverage-v8": "^4.1.8",
|
|
188
188
|
"@vue/test-utils": "^2.4.11",
|
|
@@ -205,6 +205,6 @@
|
|
|
205
205
|
"typescript-eslint": "^8.61.0",
|
|
206
206
|
"vite": "^8.0.16",
|
|
207
207
|
"vitest": "^4.1.8",
|
|
208
|
-
"vue": "^3.5.
|
|
208
|
+
"vue": "^3.5.38"
|
|
209
209
|
}
|
|
210
210
|
}
|
package/planu-native.json
CHANGED
package/planu-plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dev.planu.cli",
|
|
3
3
|
"displayName": "Planu — Spec Driven Development",
|
|
4
4
|
"description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.6.0",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": [
|
|
8
8
|
"npx",
|