@planu/cli 4.4.3 → 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 +20 -1
- package/dist/config/token-waste-autopilot.json +48 -0
- package/dist/engine/elicitation/answer-extractor.js +53 -1
- package/dist/engine/elicitation/decision-gap-detector.d.ts +3 -0
- package/dist/engine/elicitation/decision-gap-detector.js +162 -0
- package/dist/engine/elicitation/question-grounding-gate.d.ts +3 -0
- package/dist/engine/elicitation/question-grounding-gate.js +54 -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/create-spec/question-generator.d.ts +1 -1
- package/dist/tools/create-spec/question-generator.js +20 -96
- 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/clarification.d.ts +8 -0
- package/dist/types/elicitation.d.ts +21 -0
- 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 +13 -13
- package/planu-native.json +29 -8
- package/planu-plugin.json +35 -7
|
@@ -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 });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { InteractiveQuestion, ProjectKnowledge } from '../../types/index.js';
|
|
2
|
-
/** Generate clarification questions based on
|
|
2
|
+
/** Generate clarification questions based on request-specific missing decisions. */
|
|
3
3
|
export declare function generateInteractiveQuestions(description: string, knowledge: ProjectKnowledge | null): InteractiveQuestion[];
|
|
4
4
|
//# sourceMappingURL=question-generator.d.ts.map
|
|
@@ -1,101 +1,25 @@
|
|
|
1
|
-
// tools/create-spec/question-generator.ts — SPEC-
|
|
2
|
-
//
|
|
3
|
-
import { t } from '../../i18n/index.js';
|
|
1
|
+
// tools/create-spec/question-generator.ts — SPEC-1083
|
|
2
|
+
// Dynamic clarification questions grounded in the user's requested work.
|
|
4
3
|
import { extractSignals } from '../../engine/elicitation/answer-extractor.js';
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
const DIMENSIONS = dimensionsConfig;
|
|
9
|
-
/** Generate clarification questions based on description gaps and project context. */
|
|
4
|
+
import { detectDecisionGaps } from '../../engine/elicitation/decision-gap-detector.js';
|
|
5
|
+
import { validateQuestionGrounding } from '../../engine/elicitation/question-grounding-gate.js';
|
|
6
|
+
/** Generate clarification questions based on request-specific missing decisions. */
|
|
10
7
|
export function generateInteractiveQuestions(description, knowledge) {
|
|
11
8
|
const signals = extractSignals(description);
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
return questions;
|
|
28
|
-
}
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Helpers
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
function isSuppressed(key, signals) {
|
|
33
|
-
if (key === 'hasTarget') {
|
|
34
|
-
return signals.hasTarget;
|
|
35
|
-
}
|
|
36
|
-
if (key === 'hasScope') {
|
|
37
|
-
return signals.hasScope;
|
|
38
|
-
}
|
|
39
|
-
if (key === 'namedProvider') {
|
|
40
|
-
return signals.namedProvider !== null;
|
|
41
|
-
}
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
function hasRequiredSignal(key, signals) {
|
|
45
|
-
if (key === 'hasBilling') {
|
|
46
|
-
return signals.hasBilling;
|
|
47
|
-
}
|
|
48
|
-
if (key === 'hasDatabase') {
|
|
49
|
-
return signals.hasDatabase;
|
|
50
|
-
}
|
|
51
|
-
if (key === 'hasUi') {
|
|
52
|
-
return signals.hasUi;
|
|
53
|
-
}
|
|
54
|
-
if (key === 'hasTarget') {
|
|
55
|
-
return signals.hasTarget;
|
|
56
|
-
}
|
|
57
|
-
if (key === 'hasScope') {
|
|
58
|
-
return signals.hasScope;
|
|
59
|
-
}
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
function matchesDNA(required, knowledge) {
|
|
63
|
-
if (!knowledge) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
const database = knowledge.database;
|
|
67
|
-
const stack = knowledge.stack;
|
|
68
|
-
const framework = knowledge.framework ?? '';
|
|
69
|
-
return required.some((r) => database === r || framework === r || stack.includes(r));
|
|
70
|
-
}
|
|
71
|
-
function shouldInclude(dim, signals, knowledge) {
|
|
72
|
-
if (dim.suppressWhen !== undefined && isSuppressed(dim.suppressWhen, signals)) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
if (dim.requiresSignal !== undefined && !hasRequiredSignal(dim.requiresSignal, signals)) {
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
if (dim.requiresDNA !== undefined) {
|
|
79
|
-
return matchesDNA(dim.requiresDNA, knowledge);
|
|
80
|
-
}
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
function buildOptions(optionSet, knowledge) {
|
|
84
|
-
switch (optionSet) {
|
|
85
|
-
case 'target':
|
|
86
|
-
return buildTargetOptions();
|
|
87
|
-
case 'payment_provider':
|
|
88
|
-
return buildPaymentProviderOptions(knowledge);
|
|
89
|
-
case 'billing_model':
|
|
90
|
-
return buildBillingModelOptions();
|
|
91
|
-
case 'scope':
|
|
92
|
-
return buildScopeOptions();
|
|
93
|
-
case 'database':
|
|
94
|
-
return buildDatabaseOptions();
|
|
95
|
-
case 'uiType':
|
|
96
|
-
return buildUiTypeOptions();
|
|
97
|
-
default:
|
|
98
|
-
return [];
|
|
99
|
-
}
|
|
9
|
+
const gaps = detectDecisionGaps(signals, knowledge);
|
|
10
|
+
return gaps.flatMap((gap) => {
|
|
11
|
+
const question = {
|
|
12
|
+
question: gap.question,
|
|
13
|
+
header: gap.header,
|
|
14
|
+
options: gap.options,
|
|
15
|
+
multiSelect: gap.multiSelect,
|
|
16
|
+
};
|
|
17
|
+
return validateQuestionGrounding(question, {
|
|
18
|
+
gap,
|
|
19
|
+
requestText: description,
|
|
20
|
+
}).passed
|
|
21
|
+
? [question]
|
|
22
|
+
: [];
|
|
23
|
+
});
|
|
100
24
|
}
|
|
101
25
|
//# sourceMappingURL=question-generator.js.map
|
|
@@ -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;
|