@planu/cli 4.7.2 → 4.7.3
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 -1
- package/dist/config/token-waste-autopilot.json +16 -0
- package/dist/engine/compact/compact-middleware.d.ts +2 -2
- package/dist/engine/compact/compact-middleware.js +68 -7
- package/dist/engine/context-artifacts/index.d.ts +2 -0
- package/dist/engine/context-artifacts/index.js +2 -0
- package/dist/engine/context-artifacts/store.d.ts +5 -0
- package/dist/engine/context-artifacts/store.js +176 -0
- package/dist/engine/token-optimizer/content-aware-compactor.d.ts +4 -0
- package/dist/engine/token-optimizer/content-aware-compactor.js +230 -0
- package/dist/engine/token-optimizer/index.d.ts +1 -0
- package/dist/engine/token-optimizer/index.js +1 -0
- package/dist/engine/token-optimizer/output-filter.js +18 -2
- package/dist/engine/token-optimizer/policy-loader.js +12 -0
- package/dist/engine/token-optimizer/reporter.d.ts +4 -0
- package/dist/engine/token-optimizer/reporter.js +14 -1
- package/dist/engine/web-fetcher/docs-fetcher.js +5 -1
- package/dist/tools/safe-handler.js +4 -1
- package/dist/tools/token-usage-handler.js +5 -3
- package/dist/types/compact/compact-mode.d.ts +5 -0
- package/dist/types/context-artifacts.d.ts +96 -0
- package/dist/types/context-artifacts.js +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/token-optimization.d.ts +2 -0
- package/dist/types/token-waste-autopilot.d.ts +15 -0
- package/package.json +11 -11
- package/planu-native.json +29 -8
- package/planu-plugin.json +35 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## [4.7.3] - 2026-06-19
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
- feat: add reversible context compaction
|
|
5
|
+
|
|
6
|
+
### Chores
|
|
7
|
+
- chore(deps): update patch and minor dependencies
|
|
8
|
+
|
|
9
|
+
|
|
1
10
|
## [4.7.2] - 2026-06-16
|
|
2
11
|
|
|
3
12
|
### Features
|
|
@@ -4094,4 +4103,4 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
|
|
|
4094
4103
|
- Mermaid diagram generation (architecture, sequence, state machine, ER, data flow)
|
|
4095
4104
|
- Multi-language i18n (EN/ES/PT) for generated specs
|
|
4096
4105
|
- Clean Architecture (hexagonal) — engine, tools, storage, types layers
|
|
4097
|
-
- 10,857 tests with ≥95% coverage
|
|
4106
|
+
- 10,857 tests with ≥95% coverage
|
|
@@ -17,6 +17,22 @@
|
|
|
17
17
|
"generic": { "maxLines": 60 }
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
|
+
"contextArtifacts": {
|
|
21
|
+
"enabled": true,
|
|
22
|
+
"ttlMs": 86400000,
|
|
23
|
+
"minTokens": 200
|
|
24
|
+
},
|
|
25
|
+
"contentCompaction": {
|
|
26
|
+
"strategies": {
|
|
27
|
+
"json": { "maxLines": 60, "maxSnippetChars": 240 },
|
|
28
|
+
"test-log": { "maxLines": 80, "keepFailures": true, "maxSnippetChars": 240 },
|
|
29
|
+
"runtime-log": { "maxLines": 60, "keepFailures": true, "uniqueOnly": true, "maxSnippetChars": 240 },
|
|
30
|
+
"search-results": { "maxLines": 80, "uniqueOnly": true, "maxSnippetChars": 180 },
|
|
31
|
+
"code": { "maxLines": 120, "maxSnippetChars": 240 },
|
|
32
|
+
"spec-or-handoff": { "maxLines": 120, "maxSnippetChars": 240 },
|
|
33
|
+
"generic-text": { "maxLines": 60, "maxSnippetChars": 240 }
|
|
34
|
+
}
|
|
35
|
+
},
|
|
20
36
|
"tools": {
|
|
21
37
|
"groups": {
|
|
22
38
|
"spec": ["create_spec", "check_readiness", "challenge_spec", "update_status"],
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ToolResult } from '../../types/index.js';
|
|
2
|
-
import type { CompactDecision } from '../../types/compact/compact-mode.js';
|
|
2
|
+
import type { CompactDecision, CompactModeOptions } from '../../types/compact/compact-mode.js';
|
|
3
3
|
/**
|
|
4
4
|
* Apply compact mode formatting to a ToolResult.
|
|
5
5
|
* - Truncates text content blocks to tokenBudget
|
|
6
6
|
* - Preserves structuredContent (essential data: status, scores, blockers)
|
|
7
7
|
* - Adds _meta.compactMode and _meta.contextUsed
|
|
8
8
|
*/
|
|
9
|
-
export declare function applyCompactMode(result: ToolResult, decision: CompactDecision): ToolResult;
|
|
9
|
+
export declare function applyCompactMode(result: ToolResult, decision: CompactDecision, options?: CompactModeOptions): ToolResult;
|
|
10
10
|
//# sourceMappingURL=compact-middleware.d.ts.map
|
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
// engine/compact/compact-middleware.ts — SPEC-922: Apply compact mode to tool responses
|
|
2
|
+
import { compactContentAware } from '../token-optimizer/content-aware-compactor.js';
|
|
2
3
|
/** Approximate chars per token for English text. */
|
|
3
4
|
const CHARS_PER_TOKEN = 4;
|
|
5
|
+
function defaultCompactPolicy(tokenBudget) {
|
|
6
|
+
return {
|
|
7
|
+
contextArtifacts: {
|
|
8
|
+
enabled: true,
|
|
9
|
+
ttlMs: 24 * 60 * 60 * 1000,
|
|
10
|
+
minTokens: tokenBudget,
|
|
11
|
+
},
|
|
12
|
+
contentCompaction: {
|
|
13
|
+
strategies: {
|
|
14
|
+
json: { maxLines: tokenBudget },
|
|
15
|
+
'test-log': { maxLines: tokenBudget },
|
|
16
|
+
'runtime-log': { maxLines: tokenBudget },
|
|
17
|
+
'search-results': { maxLines: tokenBudget },
|
|
18
|
+
code: { maxLines: tokenBudget },
|
|
19
|
+
'spec-or-handoff': { maxLines: tokenBudget },
|
|
20
|
+
'generic-text': { maxLines: tokenBudget },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
redaction: {
|
|
24
|
+
maxSnippetChars: 240,
|
|
25
|
+
redactPatterns: ['secret', 'token', 'password', 'api_key'],
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function withCompactionMeta(result, compaction) {
|
|
30
|
+
if (compaction === undefined) {
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
...result,
|
|
35
|
+
structuredContent: {
|
|
36
|
+
...(result.structuredContent ?? {}),
|
|
37
|
+
compaction,
|
|
38
|
+
},
|
|
39
|
+
_meta: {
|
|
40
|
+
...(typeof result._meta === 'object' && result._meta !== null ? result._meta : {}),
|
|
41
|
+
compaction,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
4
45
|
/** Truncate a single text block to a token budget. */
|
|
5
46
|
function truncateBlock(text, tokenBudget) {
|
|
6
47
|
const maxChars = tokenBudget * CHARS_PER_TOKEN;
|
|
@@ -17,7 +58,7 @@ function truncateBlock(text, tokenBudget) {
|
|
|
17
58
|
* - Preserves structuredContent (essential data: status, scores, blockers)
|
|
18
59
|
* - Adds _meta.compactMode and _meta.contextUsed
|
|
19
60
|
*/
|
|
20
|
-
export function applyCompactMode(result, decision) {
|
|
61
|
+
export function applyCompactMode(result, decision, options = {}) {
|
|
21
62
|
if (decision.mode !== 'compact') {
|
|
22
63
|
// Still add _meta even in verbose mode if contextUsed was provided
|
|
23
64
|
if (decision.contextUsed !== undefined) {
|
|
@@ -32,11 +73,31 @@ export function applyCompactMode(result, decision) {
|
|
|
32
73
|
}
|
|
33
74
|
return result;
|
|
34
75
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
76
|
+
let firstCompaction;
|
|
77
|
+
const truncatedContent = result.content.map((block) => {
|
|
78
|
+
if (options.projectPath !== undefined) {
|
|
79
|
+
const compacted = compactContentAware({
|
|
80
|
+
text: block.text,
|
|
81
|
+
policy: defaultCompactPolicy(decision.tokenBudget),
|
|
82
|
+
projectPath: options.projectPath,
|
|
83
|
+
sourcePath: options.sourcePath,
|
|
84
|
+
flow: options.flow,
|
|
85
|
+
kind: options.flow,
|
|
86
|
+
});
|
|
87
|
+
firstCompaction ??= compacted.artifact;
|
|
88
|
+
return {
|
|
89
|
+
...block,
|
|
90
|
+
text: compacted.text.length < block.text.length
|
|
91
|
+
? `${compacted.text}\n\nArtifact: ${compacted.artifact?.artifactRef ?? 'not stored'}`
|
|
92
|
+
: truncateBlock(block.text, decision.tokenBudget),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
...block,
|
|
97
|
+
text: truncateBlock(block.text, decision.tokenBudget),
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
return withCompactionMeta({
|
|
40
101
|
...result,
|
|
41
102
|
content: truncatedContent,
|
|
42
103
|
_meta: {
|
|
@@ -45,6 +106,6 @@ export function applyCompactMode(result, decision) {
|
|
|
45
106
|
contextUsed: decision.contextUsed,
|
|
46
107
|
tokenBudget: decision.tokenBudget,
|
|
47
108
|
},
|
|
48
|
-
};
|
|
109
|
+
}, firstCompaction);
|
|
49
110
|
}
|
|
50
111
|
//# sourceMappingURL=compact-middleware.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ContextArtifactStats, RetrieveContextArtifactResult, StoreContextArtifactInput, StoreContextArtifactResult } from '../../types/context-artifacts.js';
|
|
2
|
+
export declare function storeContextArtifact(input: StoreContextArtifactInput): StoreContextArtifactResult;
|
|
3
|
+
export declare function retrieveContextArtifact(projectPath: string, ref: string): RetrieveContextArtifactResult;
|
|
4
|
+
export declare function getContextArtifactStats(projectPath: string): ContextArtifactStats;
|
|
5
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { getSensitivePathRefusal, shouldBypassLeanMode, } from '../context-intelligence/compression-guards.js';
|
|
5
|
+
import { hashProjectPath, projectDataDir } from '../../storage/base-store.js';
|
|
6
|
+
const REF_PREFIX = 'ctx_';
|
|
7
|
+
const REF_PATTERN = /^ctx_[a-f0-9]{32}$/;
|
|
8
|
+
const FORBIDDEN_METADATA_KEY = /(?:secret|token|password|credential|api[_-]?key|private[_-]?key)/i;
|
|
9
|
+
function artifactDir(projectPath) {
|
|
10
|
+
return join(projectDataDir(hashProjectPath(projectPath)), 'context-artifacts');
|
|
11
|
+
}
|
|
12
|
+
function artifactPath(projectPath, ref) {
|
|
13
|
+
if (!REF_PATTERN.test(ref)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return join(artifactDir(projectPath), `${ref}.json`);
|
|
17
|
+
}
|
|
18
|
+
function sha256(value) {
|
|
19
|
+
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
20
|
+
}
|
|
21
|
+
function safeMetadata(metadata) {
|
|
22
|
+
const safe = {};
|
|
23
|
+
for (const [key, value] of Object.entries(metadata ?? {})) {
|
|
24
|
+
if (FORBIDDEN_METADATA_KEY.test(key)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
28
|
+
safe[key] =
|
|
29
|
+
typeof value === 'string' && FORBIDDEN_METADATA_KEY.test(value) ? '[redacted]' : value;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return safe;
|
|
33
|
+
}
|
|
34
|
+
function compactionMetadata(artifact) {
|
|
35
|
+
return {
|
|
36
|
+
artifactRef: artifact.ref,
|
|
37
|
+
originalTokens: artifact.originalTokens,
|
|
38
|
+
compactTokens: artifact.compactTokens,
|
|
39
|
+
tokensSaved: artifact.tokensSaved,
|
|
40
|
+
strategy: artifact.strategy,
|
|
41
|
+
contentType: artifact.contentType,
|
|
42
|
+
expiresAt: artifact.expiresAt,
|
|
43
|
+
retrievalHint: `Use artifactRef ${artifact.ref} to retrieve the full local output before ${artifact.expiresAt}.`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function readArtifactFile(path) {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function writeArtifactFile(path, artifact) {
|
|
55
|
+
writeFileSync(path, JSON.stringify(artifact, null, 2), 'utf-8');
|
|
56
|
+
}
|
|
57
|
+
export function storeContextArtifact(input) {
|
|
58
|
+
if (input.sourcePath) {
|
|
59
|
+
const refusal = getSensitivePathRefusal(input.sourcePath);
|
|
60
|
+
if (refusal !== null) {
|
|
61
|
+
return { stored: false, refusedReason: refusal };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (input.flow && shouldBypassLeanMode(input.flow)) {
|
|
65
|
+
return {
|
|
66
|
+
stored: false,
|
|
67
|
+
refusedReason: `Refusing recoverable artifact for sensitive flow ${input.flow}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const contentHash = sha256(input.originalContent);
|
|
71
|
+
const ref = `${REF_PREFIX}${contentHash.slice(0, 32)}`;
|
|
72
|
+
const path = artifactPath(input.projectPath, ref);
|
|
73
|
+
if (path === null) {
|
|
74
|
+
return { stored: false, refusedReason: 'Invalid generated artifact ref' };
|
|
75
|
+
}
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
const createdAt = new Date(now).toISOString();
|
|
78
|
+
const expiresAt = new Date(now + input.ttlMs).toISOString();
|
|
79
|
+
const tokensSaved = Math.max(0, input.originalTokens - input.compactTokens);
|
|
80
|
+
const previous = existsSync(path) ? readArtifactFile(path) : null;
|
|
81
|
+
const artifact = {
|
|
82
|
+
ref,
|
|
83
|
+
contentHash,
|
|
84
|
+
createdAt: previous?.createdAt ?? createdAt,
|
|
85
|
+
expiresAt,
|
|
86
|
+
ttlMs: input.ttlMs,
|
|
87
|
+
projectId: hashProjectPath(input.projectPath),
|
|
88
|
+
contentType: input.contentType,
|
|
89
|
+
strategy: input.strategy,
|
|
90
|
+
originalTokens: input.originalTokens,
|
|
91
|
+
compactTokens: input.compactTokens,
|
|
92
|
+
tokensSaved,
|
|
93
|
+
retrievalCount: previous?.retrievalCount ?? 0,
|
|
94
|
+
metadata: safeMetadata(input.metadata),
|
|
95
|
+
originalContent: input.originalContent,
|
|
96
|
+
compactContent: input.compactContent,
|
|
97
|
+
};
|
|
98
|
+
mkdirSync(artifactDir(input.projectPath), { recursive: true });
|
|
99
|
+
writeArtifactFile(path, artifact);
|
|
100
|
+
return { stored: true, artifact, metadata: compactionMetadata(artifact) };
|
|
101
|
+
}
|
|
102
|
+
export function retrieveContextArtifact(projectPath, ref) {
|
|
103
|
+
const path = artifactPath(projectPath, ref);
|
|
104
|
+
if (path === null) {
|
|
105
|
+
return {
|
|
106
|
+
found: false,
|
|
107
|
+
reason: 'invalid-ref',
|
|
108
|
+
hint: 'Artifact refs are opaque ctx_<hash> identifiers.',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (!existsSync(path)) {
|
|
112
|
+
return {
|
|
113
|
+
found: false,
|
|
114
|
+
reason: 'missing',
|
|
115
|
+
hint: 'The artifact is missing or belongs to a different project.',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const artifact = readArtifactFile(path);
|
|
119
|
+
if (artifact === null) {
|
|
120
|
+
return {
|
|
121
|
+
found: false,
|
|
122
|
+
reason: 'corrupt',
|
|
123
|
+
hint: 'The artifact payload is corrupt and cannot be recovered.',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (artifact.projectId !== hashProjectPath(projectPath)) {
|
|
127
|
+
return {
|
|
128
|
+
found: false,
|
|
129
|
+
reason: 'unauthorized',
|
|
130
|
+
hint: 'The artifact does not belong to this project scope.',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (Date.parse(artifact.expiresAt) <= Date.now()) {
|
|
134
|
+
return {
|
|
135
|
+
found: false,
|
|
136
|
+
reason: 'expired',
|
|
137
|
+
hint: 'The artifact expired; rerun the source operation if exact output is needed.',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const updated = { ...artifact, retrievalCount: artifact.retrievalCount + 1 };
|
|
141
|
+
writeArtifactFile(path, updated);
|
|
142
|
+
return {
|
|
143
|
+
found: true,
|
|
144
|
+
artifact: updated,
|
|
145
|
+
hint: 'Full artifact recovered from local Planu storage.',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export function getContextArtifactStats(projectPath) {
|
|
149
|
+
const dir = artifactDir(projectPath);
|
|
150
|
+
const stats = {
|
|
151
|
+
artifactCount: 0,
|
|
152
|
+
totalOriginalTokens: 0,
|
|
153
|
+
totalCompactTokens: 0,
|
|
154
|
+
totalTokensSaved: 0,
|
|
155
|
+
retrievalCount: 0,
|
|
156
|
+
};
|
|
157
|
+
if (!existsSync(dir)) {
|
|
158
|
+
return stats;
|
|
159
|
+
}
|
|
160
|
+
for (const entry of readdirSync(dir)) {
|
|
161
|
+
if (!entry.endsWith('.json')) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const artifact = readArtifactFile(join(dir, entry));
|
|
165
|
+
if (artifact === null || Date.parse(artifact.expiresAt) <= Date.now()) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
stats.artifactCount += 1;
|
|
169
|
+
stats.totalOriginalTokens += artifact.originalTokens;
|
|
170
|
+
stats.totalCompactTokens += artifact.compactTokens;
|
|
171
|
+
stats.totalTokensSaved += artifact.tokensSaved;
|
|
172
|
+
stats.retrievalCount += artifact.retrievalCount;
|
|
173
|
+
}
|
|
174
|
+
return stats;
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ContentAwareCompactionInput, ContentAwareCompactionResult, ContextArtifactContentType } from '../../types/context-artifacts.js';
|
|
2
|
+
export declare function classifyContentType(kind: string | undefined, text: string): ContextArtifactContentType;
|
|
3
|
+
export declare function compactContentAware(input: ContentAwareCompactionInput): ContentAwareCompactionResult;
|
|
4
|
+
//# sourceMappingURL=content-aware-compactor.d.ts.map
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { countTokens } from './counter.js';
|
|
2
|
+
import { storeContextArtifact } from '../context-artifacts/store.js';
|
|
3
|
+
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
4
|
+
const DEFAULT_MIN_TOKENS = 200;
|
|
5
|
+
function redact(text, patterns) {
|
|
6
|
+
return patterns.reduce((current, pattern) => {
|
|
7
|
+
const re = new RegExp(pattern, 'gi');
|
|
8
|
+
return current.replace(re, '[redacted]');
|
|
9
|
+
}, text);
|
|
10
|
+
}
|
|
11
|
+
function escapeInert(text) {
|
|
12
|
+
return text.replace(/</g, '<').replace(/>/g, '>');
|
|
13
|
+
}
|
|
14
|
+
function bounded(value, maxChars) {
|
|
15
|
+
return value.length <= maxChars ? value : `${value.slice(0, maxChars)}...`;
|
|
16
|
+
}
|
|
17
|
+
function isLikelyJson(text) {
|
|
18
|
+
const trimmed = text.trim();
|
|
19
|
+
return ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
20
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']')));
|
|
21
|
+
}
|
|
22
|
+
function isSearchResult(text) {
|
|
23
|
+
return text
|
|
24
|
+
.split('\n')
|
|
25
|
+
.some((line) => /^[^:\n]+:\d+(?::\d+)?:/.test(line) || /^[^:\n]+\(\d+,\d+\):/.test(line));
|
|
26
|
+
}
|
|
27
|
+
function isCode(text) {
|
|
28
|
+
const lines = text.split('\n');
|
|
29
|
+
const codeLines = lines.filter((line) => /^\s*(import|export|function|class|interface|type |const |let |var |def |fn |pub |func |struct |enum )/.test(line));
|
|
30
|
+
return lines.length > 0 && codeLines.length / lines.length > 0.12;
|
|
31
|
+
}
|
|
32
|
+
function isSpecOrHandoff(text) {
|
|
33
|
+
return (/\bSPEC-\d+\b/.test(text) ||
|
|
34
|
+
/\b(acceptance criteria|handoff|validation score|next action)\b/i.test(text));
|
|
35
|
+
}
|
|
36
|
+
export function classifyContentType(kind, text) {
|
|
37
|
+
const normalized = kind?.toLowerCase() ?? '';
|
|
38
|
+
if (normalized.includes('json') || isLikelyJson(text)) {
|
|
39
|
+
return 'json';
|
|
40
|
+
}
|
|
41
|
+
if (normalized.includes('test') ||
|
|
42
|
+
/\b(vitest|jest|failed tests?|test files?|assertionerror)\b/i.test(text)) {
|
|
43
|
+
return 'test-log';
|
|
44
|
+
}
|
|
45
|
+
if (normalized.includes('log') ||
|
|
46
|
+
/\b(error|warn|exception|stack trace|exit code)\b/i.test(text)) {
|
|
47
|
+
return 'runtime-log';
|
|
48
|
+
}
|
|
49
|
+
if (normalized.includes('search') || isSearchResult(text)) {
|
|
50
|
+
return 'search-results';
|
|
51
|
+
}
|
|
52
|
+
if (normalized.includes('code') || isCode(text)) {
|
|
53
|
+
return 'code';
|
|
54
|
+
}
|
|
55
|
+
if (normalized.includes('spec') || normalized.includes('handoff') || isSpecOrHandoff(text)) {
|
|
56
|
+
return 'spec-or-handoff';
|
|
57
|
+
}
|
|
58
|
+
return 'generic-text';
|
|
59
|
+
}
|
|
60
|
+
function summarizeJsonValue(value, depth = 0) {
|
|
61
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number') {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === 'string') {
|
|
65
|
+
return value.length <= 80 ? value : `${value.slice(0, 80)}...`;
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
return {
|
|
69
|
+
length: value.length,
|
|
70
|
+
sample: value.slice(0, 3).map((item) => summarizeJsonValue(item, depth + 1)),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === 'object') {
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const [key, child] of Object.entries(value).slice(0, depth > 1 ? 12 : 40)) {
|
|
76
|
+
if (/^(status|state|count|total|id|uuid|name|ok|success|error|errors|warning|warnings)$/i.test(key)) {
|
|
77
|
+
out[key] = summarizeJsonValue(child, depth + 1);
|
|
78
|
+
}
|
|
79
|
+
else if (depth < 2) {
|
|
80
|
+
out[key] = summarizeJsonValue(child, depth + 1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
if (typeof value === 'undefined') {
|
|
86
|
+
return 'undefined';
|
|
87
|
+
}
|
|
88
|
+
if (typeof value === 'bigint') {
|
|
89
|
+
return value.toString();
|
|
90
|
+
}
|
|
91
|
+
if (typeof value === 'symbol') {
|
|
92
|
+
return value.description ?? 'symbol';
|
|
93
|
+
}
|
|
94
|
+
return '[unsupported]';
|
|
95
|
+
}
|
|
96
|
+
function compactJson(text) {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(text);
|
|
99
|
+
return JSON.stringify(summarizeJsonValue(parsed), null, 2);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return compactGenericText(text);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function compactLog(text, maxLines) {
|
|
106
|
+
const lines = text.split('\n');
|
|
107
|
+
const important = lines.filter((line) => /\b(error|fail|failed|failure|exception|stack|trace|warning|warn|duration|total|passed|exit code|exit status)\b/i.test(line));
|
|
108
|
+
const selected = important.length > 0 ? important : lines;
|
|
109
|
+
return [...new Set(selected)].slice(0, maxLines).join('\n');
|
|
110
|
+
}
|
|
111
|
+
function compactSearchResults(text, maxLines, maxSnippetChars) {
|
|
112
|
+
const grouped = new Map();
|
|
113
|
+
for (const line of text.split('\n')) {
|
|
114
|
+
const match = /^([^:\n]+):(\d+)(?::\d+)?:\s?(.*)$/.exec(line);
|
|
115
|
+
if (!match) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const [, path, lineNumber, snippet] = match;
|
|
119
|
+
if (path === undefined || lineNumber === undefined) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const entries = grouped.get(path) ?? [];
|
|
123
|
+
entries.push(` ${lineNumber}: ${bounded(snippet ?? '', maxSnippetChars)}`);
|
|
124
|
+
grouped.set(path, [...new Set(entries)]);
|
|
125
|
+
}
|
|
126
|
+
if (grouped.size === 0) {
|
|
127
|
+
return compactGenericText(text);
|
|
128
|
+
}
|
|
129
|
+
const out = [];
|
|
130
|
+
for (const [path, entries] of grouped.entries()) {
|
|
131
|
+
out.push(path, ...entries.slice(0, maxLines));
|
|
132
|
+
}
|
|
133
|
+
return out.slice(0, maxLines).join('\n');
|
|
134
|
+
}
|
|
135
|
+
function bracesBalanced(text) {
|
|
136
|
+
let balance = 0;
|
|
137
|
+
for (const char of text) {
|
|
138
|
+
if (char === '{') {
|
|
139
|
+
balance += 1;
|
|
140
|
+
}
|
|
141
|
+
else if (char === '}') {
|
|
142
|
+
balance -= 1;
|
|
143
|
+
}
|
|
144
|
+
if (balance < 0) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return balance === 0;
|
|
149
|
+
}
|
|
150
|
+
function compactCode(text) {
|
|
151
|
+
if (!bracesBalanced(text)) {
|
|
152
|
+
return compactGenericText(text);
|
|
153
|
+
}
|
|
154
|
+
const preserved = text
|
|
155
|
+
.split('\n')
|
|
156
|
+
.filter((line) => /^\s*(import|export|interface|type |class |function |async function|const [A-Z0-9_]+|enum |struct |def |fn |pub )/.test(line));
|
|
157
|
+
return preserved.length > 0 ? preserved.join('\n') : compactGenericText(text);
|
|
158
|
+
}
|
|
159
|
+
function compactSpecOrHandoff(text) {
|
|
160
|
+
const lines = text.split('\n');
|
|
161
|
+
const important = lines.filter((line) => /\b(SPEC-\d+|title:|status|acceptance criteria|blocker|blocked|files?|validation score|risk|security|privacy|next action|implementation|handoff)\b/i.test(line) ||
|
|
162
|
+
/^-\s*\[[ xX]\]\s+/.test(line) ||
|
|
163
|
+
/^#{1,3}\s/.test(line));
|
|
164
|
+
return (important.length > 0 ? important : lines).slice(0, 80).join('\n');
|
|
165
|
+
}
|
|
166
|
+
function compactGenericText(text) {
|
|
167
|
+
const lines = text.split('\n').filter((line) => line.trim().length > 0);
|
|
168
|
+
const important = lines.filter((line) => /^#{1,3}\s/.test(line) ||
|
|
169
|
+
/\b(error|failed|blocked|warning|todo|next action|pnpm|npm|git|https?:\/\/|\w+\/[\w./-]+\.\w+)\b/i.test(line));
|
|
170
|
+
return (important.length > 0 ? important : lines).slice(0, 60).join('\n');
|
|
171
|
+
}
|
|
172
|
+
function compactByType(type, text, maxLines, maxSnippetChars) {
|
|
173
|
+
switch (type) {
|
|
174
|
+
case 'json':
|
|
175
|
+
return compactJson(text);
|
|
176
|
+
case 'test-log':
|
|
177
|
+
case 'runtime-log':
|
|
178
|
+
return compactLog(text, maxLines);
|
|
179
|
+
case 'search-results':
|
|
180
|
+
return compactSearchResults(text, maxLines, maxSnippetChars);
|
|
181
|
+
case 'code':
|
|
182
|
+
return compactCode(text);
|
|
183
|
+
case 'spec-or-handoff':
|
|
184
|
+
return compactSpecOrHandoff(text);
|
|
185
|
+
case 'generic-text':
|
|
186
|
+
return compactGenericText(text);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export function compactContentAware(input) {
|
|
190
|
+
const contentType = classifyContentType(input.kind, input.text);
|
|
191
|
+
const strategyConfig = input.policy.contentCompaction?.strategies?.[contentType];
|
|
192
|
+
const maxLines = strategyConfig?.maxLines ?? 60;
|
|
193
|
+
const maxSnippetChars = strategyConfig?.maxSnippetChars ?? input.policy.redaction.maxSnippetChars;
|
|
194
|
+
const redacted = redact(input.text, input.policy.redaction.redactPatterns);
|
|
195
|
+
const compact = escapeInert(compactByType(contentType, redacted, maxLines, maxSnippetChars));
|
|
196
|
+
const originalTokens = countTokens(input.text).tokens;
|
|
197
|
+
const compactTokens = countTokens(compact).tokens;
|
|
198
|
+
const result = {
|
|
199
|
+
text: compact,
|
|
200
|
+
originalTokens,
|
|
201
|
+
compactTokens,
|
|
202
|
+
tokensSaved: Math.max(0, originalTokens - compactTokens),
|
|
203
|
+
strategy: contentType,
|
|
204
|
+
contentType,
|
|
205
|
+
};
|
|
206
|
+
const artifactsEnabled = input.policy.contextArtifacts?.enabled !== false;
|
|
207
|
+
const minTokens = input.policy.contextArtifacts?.minTokens ?? DEFAULT_MIN_TOKENS;
|
|
208
|
+
if (!artifactsEnabled || input.projectPath === undefined || originalTokens < minTokens) {
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
const stored = storeContextArtifact({
|
|
212
|
+
projectPath: input.projectPath,
|
|
213
|
+
originalContent: input.text,
|
|
214
|
+
compactContent: compact,
|
|
215
|
+
contentType,
|
|
216
|
+
strategy: contentType,
|
|
217
|
+
originalTokens,
|
|
218
|
+
compactTokens,
|
|
219
|
+
ttlMs: input.policy.contextArtifacts?.ttlMs ?? DEFAULT_TTL_MS,
|
|
220
|
+
sourcePath: input.sourcePath,
|
|
221
|
+
flow: input.flow,
|
|
222
|
+
metadata: input.metadata,
|
|
223
|
+
});
|
|
224
|
+
return {
|
|
225
|
+
...result,
|
|
226
|
+
...(stored.metadata !== undefined ? { artifact: stored.metadata } : {}),
|
|
227
|
+
...(stored.refusedReason !== undefined ? { refusedReason: stored.refusedReason } : {}),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=content-aware-compactor.js.map
|
|
@@ -9,6 +9,7 @@ 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
11
|
export { loadTokenWastePolicy } from './policy-loader.js';
|
|
12
|
+
export { classifyContentType, compactContentAware } from './content-aware-compactor.js';
|
|
12
13
|
export { analyzeContextPreflight } from './context-preflight.js';
|
|
13
14
|
export { filterVerboseOutput } from './output-filter.js';
|
|
14
15
|
export { recommendRelevantTools, toolsFromPolicyGroups } from './tool-relevance.js';
|
|
@@ -10,6 +10,7 @@ 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
12
|
export { loadTokenWastePolicy } from './policy-loader.js';
|
|
13
|
+
export { classifyContentType, compactContentAware } from './content-aware-compactor.js';
|
|
13
14
|
export { analyzeContextPreflight } from './context-preflight.js';
|
|
14
15
|
export { filterVerboseOutput } from './output-filter.js';
|
|
15
16
|
export { recommendRelevantTools, toolsFromPolicyGroups } from './tool-relevance.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compactContentAware } from './content-aware-compactor.js';
|
|
1
2
|
function escapeInert(text) {
|
|
2
3
|
return text.replace(/</g, '<').replace(/>/g, '>');
|
|
3
4
|
}
|
|
@@ -46,7 +47,21 @@ export function filterVerboseOutput(input) {
|
|
|
46
47
|
const strategy = selectStrategy(input);
|
|
47
48
|
const originalLines = input.text.split('\n').length;
|
|
48
49
|
const lines = compactLines(input, strategy);
|
|
49
|
-
|
|
50
|
+
let compact = redact(escapeInert(lines.join('\n')), input.policy.redaction.redactPatterns);
|
|
51
|
+
let compaction;
|
|
52
|
+
if (input.policy.contextArtifacts?.enabled === true && input.projectPath !== undefined) {
|
|
53
|
+
const contentAware = compactContentAware({
|
|
54
|
+
text: input.text,
|
|
55
|
+
policy: input.policy,
|
|
56
|
+
kind: input.kind,
|
|
57
|
+
projectPath: input.projectPath,
|
|
58
|
+
sourcePath: input.sourcePath,
|
|
59
|
+
flow: input.flow,
|
|
60
|
+
metadata: input.metadata,
|
|
61
|
+
});
|
|
62
|
+
compact = contentAware.text;
|
|
63
|
+
compaction = contentAware.artifact;
|
|
64
|
+
}
|
|
50
65
|
const decisions = [
|
|
51
66
|
{
|
|
52
67
|
decision: originalLines > lines.length ? 'summarize' : 'include',
|
|
@@ -59,8 +74,9 @@ export function filterVerboseOutput(input) {
|
|
|
59
74
|
return {
|
|
60
75
|
text: compact,
|
|
61
76
|
originalLines,
|
|
62
|
-
returnedLines:
|
|
77
|
+
returnedLines: compact.split('\n').length,
|
|
63
78
|
...(input.fullOutputRef !== undefined ? { fullOutputRef: input.fullOutputRef } : {}),
|
|
79
|
+
...(compaction !== undefined ? { compaction } : {}),
|
|
64
80
|
decisions,
|
|
65
81
|
};
|
|
66
82
|
}
|
|
@@ -39,6 +39,18 @@ function validatePolicy(value) {
|
|
|
39
39
|
if (!isRecord(policy.outputs.strategies)) {
|
|
40
40
|
throw new Error('Invalid token waste policy at outputs.strategies: expected object');
|
|
41
41
|
}
|
|
42
|
+
if (policy.contextArtifacts !== undefined) {
|
|
43
|
+
if (!Number.isFinite(policy.contextArtifacts.ttlMs) || policy.contextArtifacts.ttlMs < 1) {
|
|
44
|
+
throw new Error('Invalid token waste policy at contextArtifacts.ttlMs: expected positive number');
|
|
45
|
+
}
|
|
46
|
+
if (!Number.isFinite(policy.contextArtifacts.minTokens) ||
|
|
47
|
+
policy.contextArtifacts.minTokens < 1) {
|
|
48
|
+
throw new Error('Invalid token waste policy at contextArtifacts.minTokens: expected positive number');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (policy.contentCompaction !== undefined && !isRecord(policy.contentCompaction.strategies)) {
|
|
52
|
+
throw new Error('Invalid token waste policy at contentCompaction.strategies: expected object');
|
|
53
|
+
}
|
|
42
54
|
if (!isRecord(policy.tools.groups)) {
|
|
43
55
|
throw new Error('Invalid token waste policy at tools.groups: expected object');
|
|
44
56
|
}
|
|
@@ -18,6 +18,10 @@ export declare class OptimizationReporter {
|
|
|
18
18
|
* Record a cache hit for a tool.
|
|
19
19
|
*/
|
|
20
20
|
recordCacheHit(toolName: string, tokensSaved: number): void;
|
|
21
|
+
/**
|
|
22
|
+
* Record measured savings from reversible compaction.
|
|
23
|
+
*/
|
|
24
|
+
recordCompaction(toolName: string, tokensSaved: number, retrievals?: number): void;
|
|
21
25
|
/**
|
|
22
26
|
* Record a cache miss for a tool.
|
|
23
27
|
*/
|
|
@@ -28,6 +28,16 @@ export class OptimizationReporter {
|
|
|
28
28
|
record.tokensSaved += tokensSaved;
|
|
29
29
|
this.totalSaved += tokensSaved;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Record measured savings from reversible compaction.
|
|
33
|
+
*/
|
|
34
|
+
recordCompaction(toolName, tokensSaved, retrievals = 0) {
|
|
35
|
+
const record = this.getOrCreateRecord(toolName);
|
|
36
|
+
record.compactionTokensSaved = (record.compactionTokensSaved ?? 0) + tokensSaved;
|
|
37
|
+
record.artifactRetrievals = (record.artifactRetrievals ?? 0) + retrievals;
|
|
38
|
+
record.tokensSaved += tokensSaved;
|
|
39
|
+
this.totalSaved += tokensSaved;
|
|
40
|
+
}
|
|
31
41
|
/**
|
|
32
42
|
* Record a cache miss for a tool.
|
|
33
43
|
*/
|
|
@@ -106,7 +116,8 @@ export class OptimizationReporter {
|
|
|
106
116
|
for (const record of toolRecords) {
|
|
107
117
|
lines.push(`- ${record.toolName}: ${String(record.totalTokens)} tokens, ` +
|
|
108
118
|
`${String(record.callCount)} calls, ` +
|
|
109
|
-
`${String(record.tokensSaved)} saved`
|
|
119
|
+
`${String(record.tokensSaved)} saved ` +
|
|
120
|
+
`(compaction ${String(record.compactionTokensSaved ?? 0)}, retrievals ${String(record.artifactRetrievals ?? 0)})`);
|
|
110
121
|
}
|
|
111
122
|
}
|
|
112
123
|
return lines.join('\n');
|
|
@@ -134,6 +145,8 @@ export class OptimizationReporter {
|
|
|
134
145
|
cacheHits: 0,
|
|
135
146
|
cacheMisses: 0,
|
|
136
147
|
tokensSaved: 0,
|
|
148
|
+
compactionTokensSaved: 0,
|
|
149
|
+
artifactRetrievals: 0,
|
|
137
150
|
};
|
|
138
151
|
this.toolRecords.set(toolName, record);
|
|
139
152
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Planu — Web Fetcher: consultDocs orchestration
|
|
2
2
|
import { searchAndRegisterFramework, saveRegistry } from '../registry-updater.js';
|
|
3
3
|
import { loadDocsRegistry, findDocsEntry, setDocsRegistryCache, getStoredRegistryPath, } from './registry-loader.js';
|
|
4
|
+
import { DEFAULT_CONFIG } from './cache.js';
|
|
4
5
|
import { fetchUrl } from './http-client.js';
|
|
5
6
|
import { extractTextContent, extractCodeBlocks, extractBestPractices, buildSummaryFromContent, buildPlaceholderSummary, buildPlaceholderExamples, buildPlaceholderBestPractices, } from './content-extractor.js';
|
|
6
7
|
export async function consultDocsImpl(topic, framework) {
|
|
@@ -25,7 +26,10 @@ export async function consultDocsImpl(topic, framework) {
|
|
|
25
26
|
: `https://www.google.com/search?q=${encodeURIComponent(`${resolvedFramework} ${topic} documentation`)}`;
|
|
26
27
|
const baseUrl = docsEntry?.base ?? '';
|
|
27
28
|
if (baseUrl) {
|
|
28
|
-
const fetchResult = await fetchUrl(docsUrl
|
|
29
|
+
const fetchResult = await fetchUrl(docsUrl, {
|
|
30
|
+
...DEFAULT_CONFIG,
|
|
31
|
+
timeoutMs: 2_500,
|
|
32
|
+
});
|
|
29
33
|
if (fetchResult) {
|
|
30
34
|
const textContent = extractTextContent(fetchResult.content);
|
|
31
35
|
const codeBlocks = extractCodeBlocks(fetchResult.content);
|
|
@@ -268,7 +268,10 @@ function safeWithTelemetry(toolName, handler) {
|
|
|
268
268
|
// SPEC-455: Compress verbose JSON outputs to save LLM tokens
|
|
269
269
|
const compressed = compressToolOutput(result);
|
|
270
270
|
// SPEC-922: Apply compact mode middleware
|
|
271
|
-
const compacted = applyCompactMode(compressed, decision
|
|
271
|
+
const compacted = applyCompactMode(compressed, decision, {
|
|
272
|
+
projectPath,
|
|
273
|
+
flow: toolName,
|
|
274
|
+
});
|
|
272
275
|
// Inject pending drift banner (non-blocking, informational only)
|
|
273
276
|
const driftBanner = projectPath !== undefined ? await checkPendingDriftBanner(projectPath) : null;
|
|
274
277
|
const withDrift = injectDriftBanner(compacted, driftBanner);
|
|
@@ -27,10 +27,12 @@ export function handleTokenUsage(args) {
|
|
|
27
27
|
let details;
|
|
28
28
|
if (groupBy === 'tool' && toolRecords.length > 0) {
|
|
29
29
|
const lines = toolRecords.map((r) => `| ${r.toolName} | ${String(r.totalTokens)} | ${String(r.callCount)} | ` +
|
|
30
|
-
`${String(r.cacheHits)}/${String(r.cacheHits + r.cacheMisses)} |
|
|
30
|
+
`${String(r.cacheHits)}/${String(r.cacheHits + r.cacheMisses)} | ` +
|
|
31
|
+
`${String(r.tokensSaved)} | ${String(r.compactionTokensSaved ?? 0)} | ` +
|
|
32
|
+
`${String(r.artifactRetrievals ?? 0)} |`);
|
|
31
33
|
details = [
|
|
32
|
-
'| Tool | Tokens | Calls | Cache H/T | Saved |',
|
|
33
|
-
'
|
|
34
|
+
'| Tool | Tokens | Calls | Cache H/T | Saved | Compaction Saved | Retrievals |',
|
|
35
|
+
'|------|--------|-------|-----------|-------|------------------|------------|',
|
|
34
36
|
...lines,
|
|
35
37
|
].join('\n');
|
|
36
38
|
}
|
|
@@ -20,6 +20,11 @@ export interface CompactDecision {
|
|
|
20
20
|
contextUsed?: number;
|
|
21
21
|
reason: 'header_above_enter' | 'header_below_exit' | 'hysteresis_keep' | 'no_header';
|
|
22
22
|
}
|
|
23
|
+
export interface CompactModeOptions {
|
|
24
|
+
projectPath?: string;
|
|
25
|
+
sourcePath?: string;
|
|
26
|
+
flow?: string;
|
|
27
|
+
}
|
|
23
28
|
export interface CompactMetrics {
|
|
24
29
|
compactDecisions: number;
|
|
25
30
|
verboseDecisions: number;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export type ContextArtifactContentType = 'json' | 'test-log' | 'runtime-log' | 'search-results' | 'code' | 'spec-or-handoff' | 'generic-text';
|
|
2
|
+
export interface ContextArtifactCompactionMetadata {
|
|
3
|
+
artifactRef: string;
|
|
4
|
+
originalTokens: number;
|
|
5
|
+
compactTokens: number;
|
|
6
|
+
tokensSaved: number;
|
|
7
|
+
strategy: string;
|
|
8
|
+
contentType: ContextArtifactContentType;
|
|
9
|
+
expiresAt: string;
|
|
10
|
+
retrievalHint: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ContextArtifact {
|
|
13
|
+
ref: string;
|
|
14
|
+
contentHash: string;
|
|
15
|
+
createdAt: string;
|
|
16
|
+
expiresAt: string;
|
|
17
|
+
ttlMs: number;
|
|
18
|
+
projectId: string;
|
|
19
|
+
contentType: ContextArtifactContentType;
|
|
20
|
+
strategy: string;
|
|
21
|
+
originalTokens: number;
|
|
22
|
+
compactTokens: number;
|
|
23
|
+
tokensSaved: number;
|
|
24
|
+
retrievalCount: number;
|
|
25
|
+
metadata: Record<string, string | number | boolean>;
|
|
26
|
+
originalContent: string;
|
|
27
|
+
compactContent: string;
|
|
28
|
+
}
|
|
29
|
+
export interface StoreContextArtifactInput {
|
|
30
|
+
projectPath: string;
|
|
31
|
+
originalContent: string;
|
|
32
|
+
compactContent: string;
|
|
33
|
+
contentType: ContextArtifactContentType;
|
|
34
|
+
strategy: string;
|
|
35
|
+
originalTokens: number;
|
|
36
|
+
compactTokens: number;
|
|
37
|
+
ttlMs: number;
|
|
38
|
+
sourcePath?: string;
|
|
39
|
+
flow?: string;
|
|
40
|
+
metadata?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
export interface StoreContextArtifactResult {
|
|
43
|
+
stored: boolean;
|
|
44
|
+
artifact?: ContextArtifact;
|
|
45
|
+
metadata?: ContextArtifactCompactionMetadata;
|
|
46
|
+
refusedReason?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface RetrieveContextArtifactResult {
|
|
49
|
+
found: boolean;
|
|
50
|
+
artifact?: ContextArtifact;
|
|
51
|
+
reason?: 'missing' | 'expired' | 'corrupt' | 'invalid-ref' | 'unauthorized';
|
|
52
|
+
hint: string;
|
|
53
|
+
}
|
|
54
|
+
export interface ContextArtifactStats {
|
|
55
|
+
artifactCount: number;
|
|
56
|
+
totalOriginalTokens: number;
|
|
57
|
+
totalCompactTokens: number;
|
|
58
|
+
totalTokensSaved: number;
|
|
59
|
+
retrievalCount: number;
|
|
60
|
+
}
|
|
61
|
+
export interface ContentAwareCompactionInput {
|
|
62
|
+
text: string;
|
|
63
|
+
policy: {
|
|
64
|
+
contextArtifacts?: {
|
|
65
|
+
enabled?: boolean;
|
|
66
|
+
ttlMs?: number;
|
|
67
|
+
minTokens?: number;
|
|
68
|
+
};
|
|
69
|
+
contentCompaction?: {
|
|
70
|
+
strategies?: Partial<Record<ContextArtifactContentType, {
|
|
71
|
+
maxLines?: number;
|
|
72
|
+
maxSnippetChars?: number;
|
|
73
|
+
}>>;
|
|
74
|
+
};
|
|
75
|
+
redaction: {
|
|
76
|
+
maxSnippetChars: number;
|
|
77
|
+
redactPatterns: string[];
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
projectPath?: string;
|
|
81
|
+
kind?: string;
|
|
82
|
+
sourcePath?: string;
|
|
83
|
+
flow?: string;
|
|
84
|
+
metadata?: Record<string, unknown>;
|
|
85
|
+
}
|
|
86
|
+
export interface ContentAwareCompactionResult {
|
|
87
|
+
text: string;
|
|
88
|
+
originalTokens: number;
|
|
89
|
+
compactTokens: number;
|
|
90
|
+
tokensSaved: number;
|
|
91
|
+
strategy: string;
|
|
92
|
+
contentType: ContextArtifactContentType;
|
|
93
|
+
artifact?: ContextArtifactCompactionMetadata;
|
|
94
|
+
refusedReason?: string;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=context-artifacts.d.ts.map
|
package/dist/types/index.d.ts
CHANGED
|
@@ -80,6 +80,7 @@ export * from './workers.js';
|
|
|
80
80
|
export * from './orchestration-runtime.js';
|
|
81
81
|
export * from './token-optimization.js';
|
|
82
82
|
export * from './token-waste-autopilot.js';
|
|
83
|
+
export * from './context-artifacts.js';
|
|
83
84
|
export * from './minimal-implementation-gate.js';
|
|
84
85
|
export * from './llm-providers.js';
|
|
85
86
|
export * from './plugins.js';
|
package/dist/types/index.js
CHANGED
|
@@ -81,6 +81,7 @@ export * from './workers.js';
|
|
|
81
81
|
export * from './orchestration-runtime.js';
|
|
82
82
|
export * from './token-optimization.js';
|
|
83
83
|
export * from './token-waste-autopilot.js';
|
|
84
|
+
export * from './context-artifacts.js';
|
|
84
85
|
export * from './minimal-implementation-gate.js';
|
|
85
86
|
export * from './llm-providers.js';
|
|
86
87
|
export * from './plugins.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ContextArtifactCompactionMetadata } from './context-artifacts.js';
|
|
1
2
|
export type TokenWasteDecisionKind = 'include' | 'summarize' | 'exclude' | 'recommend' | 'avoid' | 'warn' | 'override';
|
|
2
3
|
export type TokenWasteConfidence = 'high' | 'medium' | 'low';
|
|
3
4
|
export type TokenWasteAction = string;
|
|
@@ -21,6 +22,7 @@ export interface TokenWasteOutputStrategy {
|
|
|
21
22
|
keepFailures?: boolean;
|
|
22
23
|
uniqueOnly?: boolean;
|
|
23
24
|
summarizeKeys?: boolean;
|
|
25
|
+
maxSnippetChars?: number;
|
|
24
26
|
}
|
|
25
27
|
export interface TokenWastePolicy {
|
|
26
28
|
version: 1;
|
|
@@ -35,6 +37,14 @@ export interface TokenWastePolicy {
|
|
|
35
37
|
outputs: {
|
|
36
38
|
strategies: Record<string, TokenWasteOutputStrategy>;
|
|
37
39
|
};
|
|
40
|
+
contextArtifacts?: {
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
ttlMs: number;
|
|
43
|
+
minTokens: number;
|
|
44
|
+
};
|
|
45
|
+
contentCompaction?: {
|
|
46
|
+
strategies: Record<string, TokenWasteOutputStrategy>;
|
|
47
|
+
};
|
|
38
48
|
tools: {
|
|
39
49
|
groups: Record<string, string[]>;
|
|
40
50
|
maxRecommended: number;
|
|
@@ -83,12 +93,17 @@ export interface VerboseOutputInput {
|
|
|
83
93
|
text: string;
|
|
84
94
|
policy: TokenWastePolicy;
|
|
85
95
|
fullOutputRef?: string;
|
|
96
|
+
projectPath?: string;
|
|
97
|
+
sourcePath?: string;
|
|
98
|
+
flow?: string;
|
|
99
|
+
metadata?: Record<string, unknown>;
|
|
86
100
|
}
|
|
87
101
|
export interface VerboseOutputResult {
|
|
88
102
|
text: string;
|
|
89
103
|
originalLines: number;
|
|
90
104
|
returnedLines: number;
|
|
91
105
|
fullOutputRef?: string;
|
|
106
|
+
compaction?: ContextArtifactCompactionMetadata;
|
|
92
107
|
decisions: TokenWasteDecision[];
|
|
93
108
|
}
|
|
94
109
|
export interface ToolRelevanceInput {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.3",
|
|
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.7.
|
|
38
|
-
"@planu/core-darwin-x64": "4.7.
|
|
39
|
-
"@planu/core-linux-arm64-gnu": "4.7.
|
|
40
|
-
"@planu/core-linux-arm64-musl": "4.7.
|
|
41
|
-
"@planu/core-linux-x64-gnu": "4.7.
|
|
42
|
-
"@planu/core-linux-x64-musl": "4.7.
|
|
43
|
-
"@planu/core-win32-arm64-msvc": "4.7.
|
|
44
|
-
"@planu/core-win32-x64-msvc": "4.7.
|
|
37
|
+
"@planu/core-darwin-arm64": "4.7.3",
|
|
38
|
+
"@planu/core-darwin-x64": "4.7.3",
|
|
39
|
+
"@planu/core-linux-arm64-gnu": "4.7.3",
|
|
40
|
+
"@planu/core-linux-arm64-musl": "4.7.3",
|
|
41
|
+
"@planu/core-linux-x64-gnu": "4.7.3",
|
|
42
|
+
"@planu/core-linux-x64-musl": "4.7.3",
|
|
43
|
+
"@planu/core-win32-arm64-msvc": "4.7.3",
|
|
44
|
+
"@planu/core-win32-x64-msvc": "4.7.3"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=24.0.0"
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
],
|
|
130
130
|
"license": "SEE LICENSE IN LICENSE",
|
|
131
131
|
"dependencies": {
|
|
132
|
-
"@anthropic-ai/sdk": "^0.
|
|
132
|
+
"@anthropic-ai/sdk": "^0.105.0",
|
|
133
133
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
134
134
|
"glob": "^13.0.6",
|
|
135
135
|
"yaml": "^2.9.0",
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
"eslint-config-prettier": "^10.1.8",
|
|
192
192
|
"eslint-import-resolver-typescript": "^4.4.5",
|
|
193
193
|
"eslint-plugin-import": "^2.32.0",
|
|
194
|
-
"happy-dom": "^20.10.
|
|
194
|
+
"happy-dom": "^20.10.6",
|
|
195
195
|
"husky": "^9.1.7",
|
|
196
196
|
"javascript-obfuscator": "^5.4.3",
|
|
197
197
|
"knip": "^6.17.1",
|
package/planu-native.json
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dev.planu.native",
|
|
3
3
|
"displayName": "Planu Native Lightweight Surface",
|
|
4
|
-
"version": "4.7.
|
|
4
|
+
"version": "4.7.3",
|
|
5
5
|
"packageName": "@planu/cli",
|
|
6
6
|
"modes": {
|
|
7
7
|
"lightweight": {
|
|
8
8
|
"requiresMcp": false,
|
|
9
9
|
"requiresDaemon": false,
|
|
10
|
-
"hosts": [
|
|
10
|
+
"hosts": [
|
|
11
|
+
"codex",
|
|
12
|
+
"claude-code"
|
|
13
|
+
],
|
|
11
14
|
"commands": [
|
|
12
15
|
{
|
|
13
16
|
"id": "planu.status",
|
|
14
17
|
"title": "Project status",
|
|
15
18
|
"description": "Show the compact Planu project snapshot without loading the MCP tool graph.",
|
|
16
19
|
"invocation": "planu status",
|
|
17
|
-
"hosts": [
|
|
20
|
+
"hosts": [
|
|
21
|
+
"codex",
|
|
22
|
+
"claude-code"
|
|
23
|
+
],
|
|
18
24
|
"requiresMcp": false,
|
|
19
25
|
"requiresDaemon": false,
|
|
20
26
|
"mapsTo": "handlePlanStatus"
|
|
@@ -24,7 +30,10 @@
|
|
|
24
30
|
"title": "Create spec",
|
|
25
31
|
"description": "Create a new spec through the CLI-backed SDD contract.",
|
|
26
32
|
"invocation": "planu spec create \"<title>\"",
|
|
27
|
-
"hosts": [
|
|
33
|
+
"hosts": [
|
|
34
|
+
"codex",
|
|
35
|
+
"claude-code"
|
|
36
|
+
],
|
|
28
37
|
"requiresMcp": false,
|
|
29
38
|
"requiresDaemon": false,
|
|
30
39
|
"mapsTo": "handleCreateSpec"
|
|
@@ -34,7 +43,10 @@
|
|
|
34
43
|
"title": "List specs",
|
|
35
44
|
"description": "List specs in the current project with optional status/type filters.",
|
|
36
45
|
"invocation": "planu spec list",
|
|
37
|
-
"hosts": [
|
|
46
|
+
"hosts": [
|
|
47
|
+
"codex",
|
|
48
|
+
"claude-code"
|
|
49
|
+
],
|
|
38
50
|
"requiresMcp": false,
|
|
39
51
|
"requiresDaemon": false,
|
|
40
52
|
"mapsTo": "handleListSpecs"
|
|
@@ -44,7 +56,10 @@
|
|
|
44
56
|
"title": "Validate spec",
|
|
45
57
|
"description": "Validate a spec against the current codebase from the native CLI surface.",
|
|
46
58
|
"invocation": "planu spec validate SPEC-001",
|
|
47
|
-
"hosts": [
|
|
59
|
+
"hosts": [
|
|
60
|
+
"codex",
|
|
61
|
+
"claude-code"
|
|
62
|
+
],
|
|
48
63
|
"requiresMcp": false,
|
|
49
64
|
"requiresDaemon": false,
|
|
50
65
|
"mapsTo": "handleValidate"
|
|
@@ -54,7 +69,10 @@
|
|
|
54
69
|
"title": "Audit technical debt",
|
|
55
70
|
"description": "Run the read-only project audit path for lightweight debt checks.",
|
|
56
71
|
"invocation": "planu audit debt",
|
|
57
|
-
"hosts": [
|
|
72
|
+
"hosts": [
|
|
73
|
+
"codex",
|
|
74
|
+
"claude-code"
|
|
75
|
+
],
|
|
58
76
|
"requiresMcp": false,
|
|
59
77
|
"requiresDaemon": false,
|
|
60
78
|
"mapsTo": "handleAudit"
|
|
@@ -64,7 +82,10 @@
|
|
|
64
82
|
"title": "Check release readiness",
|
|
65
83
|
"description": "Check local branch cleanliness and main/develop/release sync readiness.",
|
|
66
84
|
"invocation": "planu release check",
|
|
67
|
-
"hosts": [
|
|
85
|
+
"hosts": [
|
|
86
|
+
"codex",
|
|
87
|
+
"claude-code"
|
|
88
|
+
],
|
|
68
89
|
"requiresMcp": false,
|
|
69
90
|
"requiresDaemon": false,
|
|
70
91
|
"mapsTo": "releaseCommand"
|
package/planu-plugin.json
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
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.7.
|
|
5
|
+
"version": "4.7.3",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
|
-
"command": [
|
|
7
|
+
"command": [
|
|
8
|
+
"npx",
|
|
9
|
+
"@planu/cli@latest"
|
|
10
|
+
],
|
|
8
11
|
"packageName": "@planu/cli",
|
|
9
12
|
"capabilities": {
|
|
10
13
|
"tools": [
|
|
@@ -23,17 +26,42 @@
|
|
|
23
26
|
"create_skill",
|
|
24
27
|
"skill_search"
|
|
25
28
|
],
|
|
26
|
-
"resources": [
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
"resources": [
|
|
30
|
+
"planu://specs/list",
|
|
31
|
+
"planu://specs/{id}",
|
|
32
|
+
"planu://project/status",
|
|
33
|
+
"planu://roadmap"
|
|
34
|
+
],
|
|
35
|
+
"prompts": [
|
|
36
|
+
"create-spec-from-idea",
|
|
37
|
+
"review-spec-readiness",
|
|
38
|
+
"generate-implementation-plan"
|
|
39
|
+
],
|
|
40
|
+
"subagents": [
|
|
41
|
+
"sdd-orchestrator",
|
|
42
|
+
"spec-challenger",
|
|
43
|
+
"test-generator"
|
|
44
|
+
]
|
|
29
45
|
},
|
|
30
46
|
"compatibility": {
|
|
31
47
|
"minimumHostVersion": "1.0.0",
|
|
32
|
-
"requiredFeatures": [
|
|
48
|
+
"requiredFeatures": [
|
|
49
|
+
"mcp-tools",
|
|
50
|
+
"file-editing"
|
|
51
|
+
]
|
|
33
52
|
},
|
|
34
53
|
"repository": "https://github.com/planu-dev/planu",
|
|
35
54
|
"author": "Planu",
|
|
36
55
|
"license": "MIT",
|
|
37
56
|
"homepage": "https://planu.dev",
|
|
38
|
-
"keywords": [
|
|
57
|
+
"keywords": [
|
|
58
|
+
"sdd",
|
|
59
|
+
"spec-driven-development",
|
|
60
|
+
"mcp",
|
|
61
|
+
"specs",
|
|
62
|
+
"planning",
|
|
63
|
+
"ai",
|
|
64
|
+
"bdd",
|
|
65
|
+
"tdd"
|
|
66
|
+
]
|
|
39
67
|
}
|