@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/dist/config/token-waste-autopilot.json +48 -0
  3. package/dist/engine/elicitation/answer-extractor.js +53 -1
  4. package/dist/engine/elicitation/decision-gap-detector.d.ts +3 -0
  5. package/dist/engine/elicitation/decision-gap-detector.js +162 -0
  6. package/dist/engine/elicitation/question-grounding-gate.d.ts +3 -0
  7. package/dist/engine/elicitation/question-grounding-gate.js +54 -0
  8. package/dist/engine/token-optimizer/autopilot.d.ts +5 -0
  9. package/dist/engine/token-optimizer/autopilot.js +93 -0
  10. package/dist/engine/token-optimizer/context-preflight.d.ts +3 -0
  11. package/dist/engine/token-optimizer/context-preflight.js +64 -0
  12. package/dist/engine/token-optimizer/index.d.ts +6 -0
  13. package/dist/engine/token-optimizer/index.js +6 -0
  14. package/dist/engine/token-optimizer/loop-detector.d.ts +3 -0
  15. package/dist/engine/token-optimizer/loop-detector.js +32 -0
  16. package/dist/engine/token-optimizer/output-filter.d.ts +3 -0
  17. package/dist/engine/token-optimizer/output-filter.js +67 -0
  18. package/dist/engine/token-optimizer/policy-loader.d.ts +3 -0
  19. package/dist/engine/token-optimizer/policy-loader.js +83 -0
  20. package/dist/engine/token-optimizer/tool-relevance.d.ts +6 -0
  21. package/dist/engine/token-optimizer/tool-relevance.js +57 -0
  22. package/dist/tools/create-spec/post-creation.d.ts +2 -1
  23. package/dist/tools/create-spec/post-creation.js +32 -0
  24. package/dist/tools/create-spec/question-generator.d.ts +1 -1
  25. package/dist/tools/create-spec/question-generator.js +20 -96
  26. package/dist/tools/package-handoff.js +47 -1
  27. package/dist/tools/schemas/token-intelligence.d.ts +1 -0
  28. package/dist/tools/schemas/token-intelligence.js +3 -2
  29. package/dist/tools/status-handler.js +17 -2
  30. package/dist/tools/token-intelligence-handler.js +46 -1
  31. package/dist/types/clarification.d.ts +8 -0
  32. package/dist/types/elicitation.d.ts +21 -0
  33. package/dist/types/index.d.ts +1 -0
  34. package/dist/types/index.js +1 -0
  35. package/dist/types/token-waste-autopilot.d.ts +142 -0
  36. package/dist/types/token-waste-autopilot.js +2 -0
  37. package/package.json +13 -13
  38. package/planu-native.json +29 -8
  39. package/planu-plugin.json +35 -7
package/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## [4.6.0] - 2026-06-11
2
+
3
+ ### Features
4
+ - feat(SPEC-1084): add token waste autopilot
5
+
6
+ ### Chores
7
+ - chore(deps): sync lockfile
8
+ - chore(deps): update patch dependencies
9
+
10
+
11
+ ## [4.5.0] - 2026-06-10
12
+
13
+ ### Features
14
+ - feat(create-spec): generate intent-grounded questions
15
+
16
+ ### Chores
17
+ - chore(deps): update release tooling
18
+
19
+
1
20
  ## [4.4.3] - 2026-06-09
2
21
 
3
22
  ### Features
@@ -4042,4 +4061,4 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
4042
4061
  - Mermaid diagram generation (architecture, sequence, state machine, ER, data flow)
4043
4062
  - Multi-language i18n (EN/ES/PT) for generated specs
4044
4063
  - Clean Architecture (hexagonal) — engine, tools, storage, types layers
4045
- - 10,857 tests with ≥95% coverage
4064
+ - 10,857 tests with ≥95% coverage
@@ -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
+ }
@@ -5,6 +5,12 @@ const BACKEND_TERMS = /\b(api|endpoint|route|handler|migration|query|rpc|action|
5
5
  const INFRA_TERMS = /\b(deploy|ci|cd|docker|pipeline|config|env)\b/;
6
6
  const BILLING_TERMS = /\b(payment|billing|invoice|subscription|checkout|pricing|pay)\b/;
7
7
  const DATABASE_TERMS = /\b(database|schema|migration|table|query|sql|rls|rpc|persist|persistence|data access)\b/;
8
+ const ACTION_RE = /\b(add|build|create|fix|update|remove|delete|integrate|sync|import|export|approve|reject|upload|download|migrate|configure|validate)\b\s+(?:(?:a|an|the|new)\s+)?([a-z0-9][a-z0-9 -]{1,60})/i;
9
+ const ACTOR_TERMS = /\b(admin|administrator|manager|owner|customer|user|member|team|approver)\b/gi;
10
+ const DATA_OBJECT_TERMS = /\b(account|profile|request|invoice|subscription|payment|order|report|file|avatar|table|policy|record|settings)\b/gi;
11
+ const PERMISSION_TERMS = /\b(approval|approve|reject|admin|permission|role|rls|policy|access|auth|login|oauth|owner|manager)\b/;
12
+ const DESTRUCTIVE_TERMS = /\b(delete|remove|archive|discard|drop|truncate|revoke|reset)\b/;
13
+ const RISK_TERMS = /\b(approval|approve|payment|billing|auth|login|permission|role|delete|remove|migration|sync|webhook|external|api|rls|privacy|security)\b/gi;
8
14
  const PAYMENT_PROVIDERS = [
9
15
  { name: 'Stripe', pattern: /\bstripe\b/ },
10
16
  { name: 'PayPal', pattern: /\bpaypal\b/ },
@@ -33,7 +39,34 @@ export function extractSignals(description) {
33
39
  const hasBilling = BILLING_TERMS.test(lower) || namedProvider !== null;
34
40
  const wordCount = description.trim().split(/\s+/).length;
35
41
  const hasScope = wordCount >= 10;
36
- return { hasTarget, hasBilling, hasDatabase, hasUi, namedProvider, hasScope };
42
+ const actionMatch = ACTION_RE.exec(lower);
43
+ const action = actionMatch?.[1] ?? null;
44
+ const domainObject = cleanDomainObject(actionMatch?.[2] ?? null);
45
+ const actors = uniqueMatches(lower, ACTOR_TERMS);
46
+ const dataObjects = uniqueMatches(lower, DATA_OBJECT_TERMS);
47
+ const riskTerms = uniqueMatches(lower, RISK_TERMS);
48
+ const integrations = [
49
+ ...PAYMENT_PROVIDERS.filter(({ pattern }) => pattern.test(lower)).map(({ name }) => name),
50
+ ...extractExternalIntegrations(lower),
51
+ ];
52
+ const hasPermissionRisk = PERMISSION_TERMS.test(lower);
53
+ const hasDestructiveAction = DESTRUCTIVE_TERMS.test(lower);
54
+ return {
55
+ hasTarget,
56
+ hasBilling,
57
+ hasDatabase,
58
+ hasUi,
59
+ namedProvider,
60
+ hasScope,
61
+ action,
62
+ domainObject,
63
+ actors,
64
+ integrations,
65
+ dataObjects,
66
+ hasPermissionRisk,
67
+ hasDestructiveAction,
68
+ riskTerms,
69
+ };
37
70
  }
38
71
  function stripIncidentalReferences(description) {
39
72
  return description
@@ -41,4 +74,23 @@ function stripIncidentalReferences(description) {
41
74
  .replace(/\b\S+\/\S+\b/g, ' ')
42
75
  .replace(/\b[\w-]+\.(?:ts|tsx|js|jsx|mjs|cjs|json|md|css|scss|py|go|rs|java|rb|php)\b/g, ' ');
43
76
  }
77
+ function cleanDomainObject(value) {
78
+ if (value === null) {
79
+ return null;
80
+ }
81
+ const cleaned = value
82
+ .replace(/\b(that|with|to|from|when|where|and|or|but|using|in)\b.*$/i, '')
83
+ .replace(/\s+/g, ' ')
84
+ .trim();
85
+ return cleaned.length > 0 ? cleaned : null;
86
+ }
87
+ function uniqueMatches(text, pattern) {
88
+ const flags = pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`;
89
+ const globalPattern = new RegExp(pattern.source, flags);
90
+ return Array.from(new Set(Array.from(text.matchAll(globalPattern)).map((match) => match[0])));
91
+ }
92
+ function extractExternalIntegrations(text) {
93
+ const matches = text.match(/\b([a-z][a-z0-9-]+)\s+(?:api|webhook|sdk|integration)\b/gi) ?? [];
94
+ return Array.from(new Set(matches.map((match) => match.replace(/\s+(api|webhook|sdk|integration)$/i, ''))));
95
+ }
44
96
  //# sourceMappingURL=answer-extractor.js.map
@@ -0,0 +1,3 @@
1
+ import type { DecisionGap, ProjectKnowledge, DescriptionSignals } from '../../types/index.js';
2
+ export declare function detectDecisionGaps(signals: DescriptionSignals, _projectContext: ProjectKnowledge | null): DecisionGap[];
3
+ //# sourceMappingURL=decision-gap-detector.d.ts.map
@@ -0,0 +1,162 @@
1
+ const MAX_GAPS = 3;
2
+ export function detectDecisionGaps(signals, _projectContext) {
3
+ const gaps = [
4
+ buildPermissionGap(signals),
5
+ buildProviderGap(signals),
6
+ buildBillingModelGap(signals),
7
+ buildDataGap(signals),
8
+ buildFailureGap(signals),
9
+ ].filter((gap) => gap !== null);
10
+ if (gaps.length === 0 && !signals.hasScope) {
11
+ gaps.push(buildBehaviorGap(signals));
12
+ }
13
+ return gaps.slice(0, MAX_GAPS);
14
+ }
15
+ function buildPermissionGap(signals) {
16
+ if (!signals.hasPermissionRisk || signals.actors.length > 0) {
17
+ return null;
18
+ }
19
+ const subject = formatSubject(signals);
20
+ return makeGap({
21
+ id: 'permission-owner',
22
+ kind: 'permission',
23
+ header: 'Permission',
24
+ question: `For ${subject}, who is allowed to perform this action? This changes permissions, validation, and audit behavior.`,
25
+ evidence: evidenceFrom(signals, ['permission-sensitive wording']),
26
+ impact: 'Changes authorization checks and acceptance criteria.',
27
+ options: options([
28
+ ['Role-based owner (Recommended)', 'Use the existing role or owner model for this action.'],
29
+ ['Admin only', 'Restrict the behavior to administrators.'],
30
+ ['Any signed-in user', 'Allow all authenticated users and keep checks minimal.'],
31
+ ]),
32
+ });
33
+ }
34
+ function buildProviderGap(signals) {
35
+ if (!signals.hasBilling || signals.namedProvider !== null) {
36
+ return null;
37
+ }
38
+ const subject = formatSubject(signals);
39
+ return makeGap({
40
+ id: 'payment-provider',
41
+ kind: 'provider',
42
+ header: 'Provider',
43
+ question: `For ${subject}, which payment provider should own the payment flow? This changes API contracts, webhook handling, and test fixtures.`,
44
+ evidence: evidenceFrom(signals, ['billing/payment wording']),
45
+ impact: 'Changes integration code, webhook behavior, and verification fixtures.',
46
+ options: options([
47
+ ['Stripe (Recommended)', 'Use the most common provider for subscription and checkout flows.'],
48
+ ['PayPal', 'Use PayPal checkout and payment APIs.'],
49
+ [
50
+ 'No provider yet',
51
+ 'Keep provider integration out of scope and define internal contracts only.',
52
+ ],
53
+ ]),
54
+ });
55
+ }
56
+ function buildBillingModelGap(signals) {
57
+ if (!signals.hasBilling) {
58
+ return null;
59
+ }
60
+ const subject = formatSubject(signals);
61
+ return makeGap({
62
+ id: 'billing-model',
63
+ kind: 'billing-model',
64
+ header: 'Billing',
65
+ question: `For ${subject}, is the billing behavior recurring, one-time, or invoice-based? This changes states, events, and acceptance criteria.`,
66
+ evidence: evidenceFrom(signals, ['billing/payment wording']),
67
+ impact: 'Changes lifecycle states and acceptance criteria.',
68
+ options: options([
69
+ ['Recurring subscription (Recommended)', 'Model ongoing subscriptions and renewal states.'],
70
+ ['One-time checkout', 'Model a single payment completion flow.'],
71
+ ['Invoice-based', 'Model invoice creation, payment, and overdue states.'],
72
+ ]),
73
+ });
74
+ }
75
+ function buildDataGap(signals) {
76
+ if (!signals.hasDatabase || signals.dataObjects.length > 0) {
77
+ return null;
78
+ }
79
+ const subject = formatSubject(signals);
80
+ return makeGap({
81
+ id: 'data-shape',
82
+ kind: 'data',
83
+ header: 'Data',
84
+ question: `For ${subject}, what data must be stored or updated? This changes schema, validation, and migration work.`,
85
+ evidence: evidenceFrom(signals, ['database/data wording']),
86
+ impact: 'Changes schema, persistence, and validation requirements.',
87
+ options: options([
88
+ [
89
+ 'Existing records only (Recommended)',
90
+ 'Use existing tables or records without a new schema.',
91
+ ],
92
+ ['New table or field', 'Add persistence changes and migration coverage.'],
93
+ ['Read-only data', 'Do not persist new state for this behavior.'],
94
+ ]),
95
+ });
96
+ }
97
+ function buildFailureGap(signals) {
98
+ if (signals.integrations.length === 0 &&
99
+ !signals.riskTerms.includes('sync') &&
100
+ !signals.riskTerms.includes('api')) {
101
+ return null;
102
+ }
103
+ const subject = formatSubject(signals);
104
+ return makeGap({
105
+ id: 'failure-handling',
106
+ kind: 'failure',
107
+ header: 'Failure',
108
+ question: `For ${subject}, what should happen if ${formatIntegration(signals)} fails? This changes retry, error, and user-visible behavior.`,
109
+ evidence: evidenceFrom(signals, signals.integrations.length > 0 ? signals.integrations : ['integration/API wording']),
110
+ impact: 'Changes error handling, retries, and tests.',
111
+ options: options([
112
+ ['Show recoverable error (Recommended)', 'Return a clear error and preserve current state.'],
113
+ ['Retry automatically', 'Retry transient failures before surfacing an error.'],
114
+ ['Queue for later', 'Persist work for asynchronous retry.'],
115
+ ]),
116
+ });
117
+ }
118
+ function buildBehaviorGap(signals) {
119
+ const subject = formatSubject(signals);
120
+ return makeGap({
121
+ id: 'behavior-outcome',
122
+ kind: 'behavior',
123
+ header: 'Behavior',
124
+ question: `For ${subject}, what observable outcome should prove the work is done? This becomes the primary acceptance criterion.`,
125
+ evidence: evidenceFrom(signals, ['short request']),
126
+ impact: 'Changes the main acceptance criterion and verification command.',
127
+ options: options([
128
+ ['User-visible workflow (Recommended)', 'Define the end-to-end behavior a user can observe.'],
129
+ ['Backend behavior', 'Define API, persistence, job, or integration behavior.'],
130
+ ['Cleanup or fix', 'Define the before/after bug or cleanup result.'],
131
+ ]),
132
+ });
133
+ }
134
+ function makeGap(gap) {
135
+ return { ...gap, multiSelect: gap.multiSelect ?? false, blocking: true };
136
+ }
137
+ function formatSubject(signals) {
138
+ const action = signals.action ?? 'this request';
139
+ const object = signals.domainObject;
140
+ return object === null ? action : `${action} ${object}`;
141
+ }
142
+ function formatIntegration(signals) {
143
+ if (signals.integrations.length > 0) {
144
+ return signals.integrations[0] ?? 'the integration';
145
+ }
146
+ if (signals.riskTerms.includes('sync')) {
147
+ return 'the sync';
148
+ }
149
+ return 'the API';
150
+ }
151
+ function evidenceFrom(signals, extra) {
152
+ return [
153
+ ...(signals.action ? [`action:${signals.action}`] : []),
154
+ ...(signals.domainObject ? [`object:${signals.domainObject}`] : []),
155
+ ...signals.riskTerms.map((term) => `risk:${term}`),
156
+ ...extra,
157
+ ].slice(0, 6);
158
+ }
159
+ function options(items) {
160
+ return items.map(([label, description]) => ({ label, description }));
161
+ }
162
+ //# sourceMappingURL=decision-gap-detector.js.map
@@ -0,0 +1,3 @@
1
+ import type { InteractiveQuestion, QuestionGroundingContext, QuestionGroundingResult } from '../../types/index.js';
2
+ export declare function validateQuestionGrounding(question: InteractiveQuestion, context: QuestionGroundingContext): QuestionGroundingResult;
3
+ //# sourceMappingURL=question-grounding-gate.d.ts.map
@@ -0,0 +1,54 @@
1
+ const GENERIC_QUESTION_PATTERNS = [
2
+ /^who (are|is) the users\??$/i,
3
+ /^what are the edge cases\??$/i,
4
+ /^what should happen\??$/i,
5
+ /^what is the scope\??$/i,
6
+ /^what data is needed\??$/i,
7
+ /^what are the requirements\??$/i,
8
+ ];
9
+ export function validateQuestionGrounding(question, context) {
10
+ const text = question.question.trim();
11
+ if (GENERIC_QUESTION_PATTERNS.some((pattern) => pattern.test(text))) {
12
+ return { passed: false, reason: 'Question is a generic reusable prompt.' };
13
+ }
14
+ const lowerQuestion = text.toLowerCase();
15
+ const lowerEvidence = [context.requestText, ...context.gap.evidence].join(' ').toLowerCase();
16
+ const evidenceTokens = tokenSet(lowerEvidence);
17
+ const hasRequestEvidence = Array.from(evidenceTokens).some((token) => token.length >= 3 && lowerQuestion.includes(token));
18
+ if (!hasRequestEvidence) {
19
+ return {
20
+ passed: false,
21
+ reason: 'Question does not include evidence from the request or detected gap.',
22
+ };
23
+ }
24
+ if (!/\bchanges?\b|\bthis changes\b|\baffects?\b|\bbecause\b|\bbecomes?\b/i.test(text)) {
25
+ return {
26
+ passed: false,
27
+ reason: 'Question does not explain why the answer matters for the spec.',
28
+ };
29
+ }
30
+ return { passed: true };
31
+ }
32
+ function tokenSet(text) {
33
+ return new Set(text
34
+ .replace(/[^a-z0-9 ]/gi, ' ')
35
+ .split(/\s+/)
36
+ .filter((token) => !STOP_WORDS.has(token) && token.length > 2));
37
+ }
38
+ const STOP_WORDS = new Set([
39
+ 'the',
40
+ 'and',
41
+ 'for',
42
+ 'this',
43
+ 'that',
44
+ 'with',
45
+ 'from',
46
+ 'what',
47
+ 'when',
48
+ 'where',
49
+ 'should',
50
+ 'action',
51
+ 'object',
52
+ 'risk',
53
+ ]);
54
+ //# sourceMappingURL=question-grounding-gate.js.map
@@ -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, '&lt;').replace(/>/g, '&gt;');
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,3 @@
1
+ import type { ContextPreflightInput, ContextPreflightResult } from '../../types/token-waste-autopilot.js';
2
+ export declare function analyzeContextPreflight(input: ContextPreflightInput): ContextPreflightResult;
3
+ //# sourceMappingURL=context-preflight.d.ts.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,3 @@
1
+ import type { VerboseOutputInput, VerboseOutputResult } from '../../types/token-waste-autopilot.js';
2
+ export declare function filterVerboseOutput(input: VerboseOutputInput): VerboseOutputResult;
3
+ //# sourceMappingURL=output-filter.d.ts.map