@planu/cli 4.10.5 → 4.10.7
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 -0
- package/dist/engine/compact/tool-list-compactor.d.ts +1 -1
- package/dist/engine/evidence-gates/lifecycle-gate.js +17 -1
- package/dist/engine/evidence-index/done-drift.js +4 -1
- package/dist/engine/local-first/tool-classification.d.ts +7 -0
- package/dist/engine/local-first/tool-classification.js +313 -0
- package/dist/engine/qa-gate.js +13 -1
- package/dist/engine/scope-boundaries/scope-validator.js +14 -2
- package/dist/engine/validator/validation-report-writer.js +3 -4
- package/dist/tools/audit.js +1 -1
- package/dist/tools/challenge-spec.js +52 -2
- package/dist/tools/check-readiness.js +15 -8
- package/dist/tools/data-governance/audit-handler.js +21 -6
- package/dist/tools/data-governance/detect-handler.js +32 -15
- package/dist/tools/define-ui-contract.js +1 -4
- package/dist/tools/design-schema.js +1 -4
- package/dist/tools/detect-drift.js +48 -28
- package/dist/tools/flag-spec-gap.js +47 -12
- package/dist/tools/generate-sub-agent.js +2 -4
- package/dist/tools/list-specs.js +15 -59
- package/dist/tools/orchestrate-locking.js +15 -4
- package/dist/tools/output-compressor.d.ts +14 -1
- package/dist/tools/output-compressor.js +121 -2
- package/dist/tools/package-handoff.js +136 -12
- package/dist/tools/reverse-engineer/handler.js +9 -10
- package/dist/tools/rollback-release.js +20 -2
- package/dist/tools/spec-diff-handler.js +3 -3
- package/dist/tools/status-handler.js +6 -1
- package/dist/tools/suggest-mcp-server.js +2 -4
- package/dist/tools/token-recording.d.ts +2 -1
- package/dist/tools/token-recording.js +21 -2
- package/dist/tools/update-status/dod-gates.d.ts +2 -2
- package/dist/tools/update-status/dod-gates.js +47 -52
- package/dist/tools/update-status/evidence-gate.js +6 -4
- package/dist/tools/update-status/index.js +10 -3
- package/dist/tools/update-status/qa-gate.d.ts +5 -0
- package/dist/tools/update-status/qa-gate.js +50 -0
- package/dist/tools/update-status/response-builder.js +96 -2
- package/dist/tools/update-status/transition-guard.js +33 -11
- package/dist/tools/update-status-convention-gate.js +22 -6
- package/dist/tools/validate-team-results.js +23 -1
- package/dist/tools/validate.js +137 -16
- package/dist/transports/http-transport.js +13 -0
- package/dist/transports/transport-factory.js +13 -0
- package/dist/types/qa-gate.d.ts +3 -0
- package/dist/types/tool-classification.d.ts +8 -0
- package/dist/types/tool-classification.js +2 -0
- package/package.json +10 -10
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
|
@@ -9,6 +9,14 @@ const MIN_COMPRESS_LENGTH = 800;
|
|
|
9
9
|
const HARD_CAP_CHARS = 2400;
|
|
10
10
|
/** Max lines kept from a long markdown text block */
|
|
11
11
|
const MAX_MARKDOWN_LINES = 40;
|
|
12
|
+
/** Default number of structured array items to keep in MCP responses. */
|
|
13
|
+
const MAX_STRUCTURED_ARRAY_ITEMS = 20;
|
|
14
|
+
/** Default max string length inside structuredContent. */
|
|
15
|
+
const MAX_STRUCTURED_STRING_CHARS = 2000;
|
|
16
|
+
/** Default object key budget inside structuredContent. */
|
|
17
|
+
const MAX_STRUCTURED_OBJECT_KEYS = 60;
|
|
18
|
+
/** Default recursion depth for structuredContent compaction. */
|
|
19
|
+
const MAX_STRUCTURED_DEPTH = 8;
|
|
12
20
|
/**
|
|
13
21
|
* Detects if a string is a JSON dump (starts with { or [).
|
|
14
22
|
* Quick heuristic — avoids parsing large strings unnecessarily.
|
|
@@ -133,10 +141,114 @@ function compressTextBlock(text) {
|
|
|
133
141
|
}
|
|
134
142
|
return hardCap(truncateMarkdown(text));
|
|
135
143
|
}
|
|
144
|
+
function pushArrayMeta(meta, value) {
|
|
145
|
+
meta.arrays.push(value);
|
|
146
|
+
}
|
|
147
|
+
function pushStringMeta(meta, value) {
|
|
148
|
+
meta.strings.push(value);
|
|
149
|
+
}
|
|
150
|
+
function pushObjectMeta(meta, value) {
|
|
151
|
+
meta.objects.push(value);
|
|
152
|
+
}
|
|
153
|
+
function pushDepthMeta(meta, path) {
|
|
154
|
+
meta.depthLimited.push(path);
|
|
155
|
+
}
|
|
156
|
+
function compactStructuredValue(value, path, depth, options, meta) {
|
|
157
|
+
if (typeof value === 'string') {
|
|
158
|
+
if (value.length <= options.maxStringChars) {
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
pushStringMeta(meta, {
|
|
162
|
+
path,
|
|
163
|
+
originalChars: value.length,
|
|
164
|
+
shownChars: options.maxStringChars,
|
|
165
|
+
});
|
|
166
|
+
return `${value.slice(0, options.maxStringChars)}\n... (${String(value.length - options.maxStringChars)} chars omitted)`;
|
|
167
|
+
}
|
|
168
|
+
if (value === null || typeof value !== 'object') {
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
if (depth >= options.maxDepth) {
|
|
172
|
+
pushDepthMeta(meta, path);
|
|
173
|
+
return '[structuredContent depth limit reached]';
|
|
174
|
+
}
|
|
175
|
+
if (Array.isArray(value)) {
|
|
176
|
+
const shown = value.slice(0, options.maxArrayItems);
|
|
177
|
+
if (value.length > shown.length) {
|
|
178
|
+
pushArrayMeta(meta, {
|
|
179
|
+
path,
|
|
180
|
+
total: value.length,
|
|
181
|
+
shown: shown.length,
|
|
182
|
+
omitted: value.length - shown.length,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return shown.map((item, index) => compactStructuredValue(item, `${path}[${String(index)}]`, depth + 1, options, meta));
|
|
186
|
+
}
|
|
187
|
+
const entries = Object.entries(value);
|
|
188
|
+
const keptEntries = entries.slice(0, options.maxObjectKeys);
|
|
189
|
+
if (entries.length > keptEntries.length) {
|
|
190
|
+
pushObjectMeta(meta, {
|
|
191
|
+
path,
|
|
192
|
+
totalKeys: entries.length,
|
|
193
|
+
shownKeys: keptEntries.length,
|
|
194
|
+
omitted: entries.length - keptEntries.length,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const out = {};
|
|
198
|
+
for (const [key, entryValue] of keptEntries) {
|
|
199
|
+
out[key] = compactStructuredValue(entryValue, path === '$' ? `$.${key}` : `${path}.${key}`, depth + 1, options, meta);
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Compacts structuredContent without changing its broad contract shape.
|
|
205
|
+
* Arrays remain arrays, objects remain objects, and compaction metadata is added
|
|
206
|
+
* at top level when a successful response would otherwise return a large payload.
|
|
207
|
+
*/
|
|
208
|
+
export function compactStructuredContent(structuredContent, options = {}) {
|
|
209
|
+
const resolved = {
|
|
210
|
+
maxArrayItems: options.maxArrayItems ?? MAX_STRUCTURED_ARRAY_ITEMS,
|
|
211
|
+
maxStringChars: options.maxStringChars ?? MAX_STRUCTURED_STRING_CHARS,
|
|
212
|
+
maxObjectKeys: options.maxObjectKeys ?? MAX_STRUCTURED_OBJECT_KEYS,
|
|
213
|
+
maxDepth: options.maxDepth ?? MAX_STRUCTURED_DEPTH,
|
|
214
|
+
};
|
|
215
|
+
const meta = {
|
|
216
|
+
structuredContentCompacted: true,
|
|
217
|
+
arrays: [],
|
|
218
|
+
strings: [],
|
|
219
|
+
objects: [],
|
|
220
|
+
depthLimited: [],
|
|
221
|
+
};
|
|
222
|
+
const compacted = compactStructuredValue(structuredContent, '$', 0, resolved, meta);
|
|
223
|
+
if (meta.arrays.length === 0 &&
|
|
224
|
+
meta.strings.length === 0 &&
|
|
225
|
+
meta.objects.length === 0 &&
|
|
226
|
+
meta.depthLimited.length === 0) {
|
|
227
|
+
return structuredContent;
|
|
228
|
+
}
|
|
229
|
+
if (compacted !== null && typeof compacted === 'object' && !Array.isArray(compacted)) {
|
|
230
|
+
const obj = compacted;
|
|
231
|
+
const existingCompaction = obj.compaction !== null && typeof obj.compaction === 'object'
|
|
232
|
+
? obj.compaction
|
|
233
|
+
: {};
|
|
234
|
+
return {
|
|
235
|
+
...obj,
|
|
236
|
+
compaction: {
|
|
237
|
+
...existingCompaction,
|
|
238
|
+
...meta,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
value: compacted,
|
|
244
|
+
compaction: meta,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
136
247
|
/**
|
|
137
248
|
* Post-processes a ToolResult to compress verbose content blocks.
|
|
138
249
|
* Applied to ALL tool outputs via safeWithTelemetry.
|
|
139
|
-
*
|
|
250
|
+
* Also bounds structuredContent so token recording reflects the actual visible
|
|
251
|
+
* payload instead of unbounded internal data.
|
|
140
252
|
*/
|
|
141
253
|
export function compressToolOutput(result) {
|
|
142
254
|
// Don't compress error results — they need full context for debugging
|
|
@@ -151,6 +263,13 @@ export function compressToolOutput(result) {
|
|
|
151
263
|
...block,
|
|
152
264
|
text: compressTextBlock(block.text),
|
|
153
265
|
}));
|
|
154
|
-
|
|
266
|
+
const structuredContent = result.structuredContent !== undefined
|
|
267
|
+
? compactStructuredContent(result.structuredContent)
|
|
268
|
+
: undefined;
|
|
269
|
+
return {
|
|
270
|
+
...result,
|
|
271
|
+
content: compressed,
|
|
272
|
+
...(structuredContent !== undefined ? { structuredContent } : {}),
|
|
273
|
+
};
|
|
155
274
|
}
|
|
156
275
|
//# sourceMappingURL=output-compressor.js.map
|
|
@@ -3,7 +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,
|
|
6
|
+
import { analyzeContextPreflight, buildTokenWasteReport, loadTokenWastePolicy, recommendRelevantTools, toolsFromPolicyGroups, } from '../engine/token-optimizer/index.js';
|
|
7
7
|
import { appendTransitionEvent } from '../storage/transition-log.js';
|
|
8
8
|
import { formatProjectGraphContext, queryProjectGraphSlice, } from '../engine/project-graph/index.js';
|
|
9
9
|
import { analyzeMinimalImplementation, loadMinimalImplementationPolicy, } from '../engine/minimality/index.js';
|
|
@@ -137,6 +137,98 @@ function formatHandoff(pkg) {
|
|
|
137
137
|
}
|
|
138
138
|
return lines.join('\n');
|
|
139
139
|
}
|
|
140
|
+
function topItems(items, limit) {
|
|
141
|
+
return items.slice(0, limit);
|
|
142
|
+
}
|
|
143
|
+
function buildHandoffSummaryText(args) {
|
|
144
|
+
const lines = [];
|
|
145
|
+
lines.push(`# Implementation Handoff Package - ${args.pkg.specId}`);
|
|
146
|
+
lines.push('');
|
|
147
|
+
lines.push(`Readiness Score: ${String(args.pkg.readinessScore)}/100`);
|
|
148
|
+
lines.push(`Blocked: ${args.pkg.blocked ? 'yes' : 'no'}`);
|
|
149
|
+
if (args.pkg.handoffPath) {
|
|
150
|
+
lines.push(`Handoff Path: ${args.pkg.handoffPath}`);
|
|
151
|
+
}
|
|
152
|
+
if (args.pkg.contextHash) {
|
|
153
|
+
lines.push(`Context Hash: ${args.pkg.contextHash}`);
|
|
154
|
+
}
|
|
155
|
+
lines.push(`Counts: ${String(args.pkg.criteria.length)} criteria, ${String(args.pkg.filesToModify.length)} files to modify, ${String(args.pkg.filesToCreate.length)} files to create, ${String(args.pkg.testPlan.length)} tests, ${String(args.pkg.risks.length)} risks.`);
|
|
156
|
+
if (args.topBlockers.length > 0) {
|
|
157
|
+
lines.push(`Top blockers: ${args.topBlockers.join('; ')}`);
|
|
158
|
+
}
|
|
159
|
+
if (args.topRisks.length > 0) {
|
|
160
|
+
lines.push(`Top risks: ${args.topRisks.join('; ')}`);
|
|
161
|
+
}
|
|
162
|
+
if (args.minimalityBlocked !== null) {
|
|
163
|
+
lines.push(`Minimality: ${args.minimalityBlocked ? 'blocked' : 'pass'}`);
|
|
164
|
+
}
|
|
165
|
+
if (args.tokenWasteRiskCount !== null) {
|
|
166
|
+
lines.push(`Token Waste Autopilot risks: ${String(args.tokenWasteRiskCount)}`);
|
|
167
|
+
}
|
|
168
|
+
lines.push(`Next Action: ${args.pkg.nextAction}`);
|
|
169
|
+
lines.push('');
|
|
170
|
+
lines.push('Full handoff is available via the handoffPath artifact; it is not embedded by default.');
|
|
171
|
+
return lines.join('\n');
|
|
172
|
+
}
|
|
173
|
+
function summarizeMinimality(report) {
|
|
174
|
+
if (report === null) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
const blockingFindings = report.findings.filter((finding) => finding.blocksDone);
|
|
178
|
+
return {
|
|
179
|
+
enabled: report.enabled,
|
|
180
|
+
blocked: report.blocked,
|
|
181
|
+
findingCount: report.findings.length,
|
|
182
|
+
blockingFindingCount: blockingFindings.length,
|
|
183
|
+
blockersTop3: topItems(blockingFindings, 3).map((finding) => ({
|
|
184
|
+
ruleId: finding.ruleId,
|
|
185
|
+
target: finding.target,
|
|
186
|
+
evidence: finding.evidence,
|
|
187
|
+
})),
|
|
188
|
+
safetyExceptionCount: report.safetyExceptions.length,
|
|
189
|
+
debtEvidenceCount: report.debtEvidence.length,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function summarizeTokenWaste(report) {
|
|
193
|
+
if (report === null) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
actionsTakenCount: report.actionsTaken.length,
|
|
198
|
+
recommendationCount: report.recommendations.length,
|
|
199
|
+
riskCount: report.risks.length,
|
|
200
|
+
risksTop3: topItems(report.risks, 3).map((risk) => ({
|
|
201
|
+
decision: risk.decision,
|
|
202
|
+
target: risk.target,
|
|
203
|
+
reason: risk.reason,
|
|
204
|
+
confidence: risk.confidence,
|
|
205
|
+
})),
|
|
206
|
+
modelEffort: {
|
|
207
|
+
effort: report.modelEffort.effort,
|
|
208
|
+
reason: report.modelEffort.reason,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function summarizeGraphContext(graphContext) {
|
|
213
|
+
if (graphContext === null || typeof graphContext !== 'object') {
|
|
214
|
+
return { available: false };
|
|
215
|
+
}
|
|
216
|
+
const graph = graphContext;
|
|
217
|
+
return {
|
|
218
|
+
available: true,
|
|
219
|
+
nodeCount: Array.isArray(graph.nodes) ? graph.nodes.length : 0,
|
|
220
|
+
edgeCount: Array.isArray(graph.edges) ? graph.edges.length : 0,
|
|
221
|
+
summary: graph.summary,
|
|
222
|
+
tokenSavings: graph.tokenSavings,
|
|
223
|
+
freshness: graph.freshness
|
|
224
|
+
? {
|
|
225
|
+
graphPath: graph.freshness.graphPath,
|
|
226
|
+
stale: graph.freshness.stale,
|
|
227
|
+
reason: graph.freshness.reason,
|
|
228
|
+
}
|
|
229
|
+
: undefined,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
140
232
|
async function buildHandoffTokenWasteReport(pkg, knowledge, spec) {
|
|
141
233
|
try {
|
|
142
234
|
const { policy } = await loadTokenWastePolicy(knowledge.projectPath);
|
|
@@ -277,12 +369,6 @@ export async function handlePackageHandoff(args) {
|
|
|
277
369
|
? `${formatHandoff(pkgWithScore)}\n${graphContext.text}`
|
|
278
370
|
: formatHandoff(pkgWithScore);
|
|
279
371
|
const minimalityReport = await buildMinimalImplementationReport(pkgWithScore, knowledge, spec, baseHandoffText);
|
|
280
|
-
const handoffText = minimalityReport !== null && minimalityReport.markdown.length > 0
|
|
281
|
-
? `${baseHandoffText}\n${minimalityReport.markdown}`
|
|
282
|
-
: baseHandoffText;
|
|
283
|
-
const formatted = tokenWasteReport !== null
|
|
284
|
-
? `${handoffText}\n${formatTokenWasteReport(tokenWasteReport)}`
|
|
285
|
-
: handoffText;
|
|
286
372
|
void appendTransitionEvent({
|
|
287
373
|
projectId,
|
|
288
374
|
specId,
|
|
@@ -296,16 +382,54 @@ export async function handlePackageHandoff(args) {
|
|
|
296
382
|
}).catch(() => {
|
|
297
383
|
/* best-effort — handoff rendering should not fail because telemetry failed */
|
|
298
384
|
});
|
|
385
|
+
const topBlockers = topItems(pkgWithScore.blockers, 3);
|
|
386
|
+
const topRisks = topItems(pkgWithScore.risks, 5);
|
|
387
|
+
const minimalitySummary = summarizeMinimality(minimalityReport);
|
|
388
|
+
const tokenWasteSummary = summarizeTokenWaste(tokenWasteReport);
|
|
389
|
+
const graphSummary = graphContext !== null ? summarizeGraphContext(graphContext.structured) : undefined;
|
|
299
390
|
return {
|
|
300
|
-
content: [
|
|
391
|
+
content: [
|
|
392
|
+
{
|
|
393
|
+
type: 'text',
|
|
394
|
+
text: buildHandoffSummaryText({
|
|
395
|
+
pkg: pkgWithScore,
|
|
396
|
+
topBlockers,
|
|
397
|
+
topRisks,
|
|
398
|
+
minimalityBlocked: minimalityReport?.blocked ?? null,
|
|
399
|
+
tokenWasteRiskCount: tokenWasteReport?.risks.length ?? null,
|
|
400
|
+
}),
|
|
401
|
+
},
|
|
402
|
+
],
|
|
301
403
|
structuredContent: {
|
|
404
|
+
specId,
|
|
302
405
|
blocked: pkgWithScore.blocked,
|
|
303
|
-
|
|
406
|
+
readinessScore: pkgWithScore.readinessScore,
|
|
407
|
+
blockersCount: pkgWithScore.blockers.length,
|
|
408
|
+
blockersTop3: topBlockers,
|
|
409
|
+
risksTop5: topRisks,
|
|
410
|
+
criteriaTop5: topItems(pkgWithScore.criteria, 5),
|
|
411
|
+
constraintsTop5: topItems(pkgWithScore.constraints, 5),
|
|
412
|
+
warningsTop5: topItems(pkgWithScore.warnings, 5),
|
|
304
413
|
handoffPath: pkgWithScore.handoffPath,
|
|
305
414
|
contextHash: pkgWithScore.contextHash,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
415
|
+
artifactRefs: {
|
|
416
|
+
handoff: pkgWithScore.handoffPath,
|
|
417
|
+
},
|
|
418
|
+
counts: {
|
|
419
|
+
criteria: pkgWithScore.criteria.length,
|
|
420
|
+
filesToModify: pkgWithScore.filesToModify.length,
|
|
421
|
+
filesToCreate: pkgWithScore.filesToCreate.length,
|
|
422
|
+
ownership: pkgWithScore.ownership.length,
|
|
423
|
+
testPlan: pkgWithScore.testPlan.length,
|
|
424
|
+
risks: pkgWithScore.risks.length,
|
|
425
|
+
constraints: pkgWithScore.constraints.length,
|
|
426
|
+
warnings: pkgWithScore.warnings.length,
|
|
427
|
+
outOfScope: pkgWithScore.outOfScope.length,
|
|
428
|
+
},
|
|
429
|
+
nextAction: pkgWithScore.nextAction,
|
|
430
|
+
...(minimalitySummary !== undefined ? { minimality: minimalitySummary } : {}),
|
|
431
|
+
...(tokenWasteSummary !== undefined ? { tokenWaste: tokenWasteSummary } : {}),
|
|
432
|
+
...(graphSummary !== undefined ? { graphContext: graphSummary } : {}),
|
|
309
433
|
},
|
|
310
434
|
};
|
|
311
435
|
}
|
|
@@ -278,15 +278,6 @@ export async function handleReverseEngineer(args) {
|
|
|
278
278
|
type: 'text',
|
|
279
279
|
text: ti('reverseEngineer.completed', { count: String(ca.limit) }),
|
|
280
280
|
},
|
|
281
|
-
{
|
|
282
|
-
type: 'text',
|
|
283
|
-
text: JSON.stringify({
|
|
284
|
-
specId,
|
|
285
|
-
title: spec.title,
|
|
286
|
-
status: 'draft',
|
|
287
|
-
analysis,
|
|
288
|
-
}, null, 2),
|
|
289
|
-
},
|
|
290
281
|
];
|
|
291
282
|
// AC-10: Add human-readable summary for deep-v2
|
|
292
283
|
if (deepV2Summary) {
|
|
@@ -295,6 +286,14 @@ export async function handleReverseEngineer(args) {
|
|
|
295
286
|
text: deepV2Summary,
|
|
296
287
|
});
|
|
297
288
|
}
|
|
298
|
-
return {
|
|
289
|
+
return {
|
|
290
|
+
content,
|
|
291
|
+
structuredContent: {
|
|
292
|
+
specId,
|
|
293
|
+
title: spec.title,
|
|
294
|
+
status: 'draft',
|
|
295
|
+
analysis,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
299
298
|
}
|
|
300
299
|
//# sourceMappingURL=handler.js.map
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { runRollback } from '../engine/release/rollback-runner.js';
|
|
3
3
|
import { writePostmortem } from '../engine/release/postmortem-generator.js';
|
|
4
4
|
import { appendTransitionEvent } from '../storage/transition-log.js';
|
|
5
|
+
import { formatKeyValue } from './output-formatter.js';
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Age guard (30 days)
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
@@ -93,7 +94,7 @@ export async function handleRollbackRelease(input) {
|
|
|
93
94
|
content: [
|
|
94
95
|
{
|
|
95
96
|
type: 'text',
|
|
96
|
-
text:
|
|
97
|
+
text: formatKeyValue({
|
|
97
98
|
error: 'rollback_age_exceeded',
|
|
98
99
|
code: 422,
|
|
99
100
|
fixHint: `Versions older than ${MAX_AGE_DAYS} days require manual operator approval. Re-run with allowAged: true.`,
|
|
@@ -102,6 +103,13 @@ export async function handleRollbackRelease(input) {
|
|
|
102
103
|
},
|
|
103
104
|
],
|
|
104
105
|
isError: true,
|
|
106
|
+
structuredContent: {
|
|
107
|
+
error: 'rollback_age_exceeded',
|
|
108
|
+
code: 422,
|
|
109
|
+
fixHint: `Versions older than ${MAX_AGE_DAYS} days require manual operator approval. Re-run with allowAged: true.`,
|
|
110
|
+
ageDays: Math.round(ageDays),
|
|
111
|
+
maxAgeDays: MAX_AGE_DAYS,
|
|
112
|
+
},
|
|
105
113
|
};
|
|
106
114
|
}
|
|
107
115
|
}
|
|
@@ -178,10 +186,20 @@ export async function handleRollbackRelease(input) {
|
|
|
178
186
|
content: [
|
|
179
187
|
{
|
|
180
188
|
type: 'text',
|
|
181
|
-
text:
|
|
189
|
+
text: formatKeyValue({
|
|
190
|
+
ok: output.ok,
|
|
191
|
+
steps: output.steps.length,
|
|
192
|
+
failedSteps: output.steps.filter((step) => step.status === 'failed').length,
|
|
193
|
+
skippedSteps: output.steps.filter((step) => step.status === 'skipped').length,
|
|
194
|
+
postmortemPath: output.postmortemPath,
|
|
195
|
+
smokeTestOk: output.smokeTest?.ok,
|
|
196
|
+
hints: additionalHints.length,
|
|
197
|
+
firstHint: additionalHints[0],
|
|
198
|
+
}),
|
|
182
199
|
},
|
|
183
200
|
],
|
|
184
201
|
isError: !output.ok,
|
|
202
|
+
structuredContent: { ...output, hints: additionalHints },
|
|
185
203
|
};
|
|
186
204
|
}
|
|
187
205
|
//# sourceMappingURL=rollback-release.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// tools/spec-diff-handler.ts — Handlers for diff_spec_versions and spec_history (SPEC-284)
|
|
2
|
-
import { compactResult } from './output-formatter.js';
|
|
2
|
+
import { compactJson, compactResult } from './output-formatter.js';
|
|
3
3
|
import { projectDataDir } from '../storage/index.js';
|
|
4
4
|
import { computeSpecVersionDiff, formatDiffAsText, listSpecVersionHistory, } from '../engine/spec-differ.js';
|
|
5
5
|
/** Handler for the diff_spec_versions tool. */
|
|
@@ -21,7 +21,7 @@ export async function handleDiffSpecVersions(args) {
|
|
|
21
21
|
content: [
|
|
22
22
|
{
|
|
23
23
|
type: 'text',
|
|
24
|
-
text:
|
|
24
|
+
text: compactJson({
|
|
25
25
|
specId: diff.specId,
|
|
26
26
|
fromTag: diff.fromTag,
|
|
27
27
|
toTag: diff.toTag,
|
|
@@ -30,7 +30,7 @@ export async function handleDiffSpecVersions(args) {
|
|
|
30
30
|
linesRemoved: diff.removed.length,
|
|
31
31
|
criteriaChanges: diff.criteriaChanges,
|
|
32
32
|
sectionChanges: diff.sectionChanges,
|
|
33
|
-
}
|
|
33
|
+
}),
|
|
34
34
|
},
|
|
35
35
|
],
|
|
36
36
|
};
|
|
@@ -308,7 +308,12 @@ export async function handlePlanStatus(args) {
|
|
|
308
308
|
...(sessionTip !== undefined ? { sessionTip } : {}),
|
|
309
309
|
...(tokenWasteHint !== undefined ? { tokenWasteHint } : {}),
|
|
310
310
|
...(graphFreshnessHint !== undefined ? { graphFreshnessHint } : {}),
|
|
311
|
-
...(btwHint !== undefined
|
|
311
|
+
...(btwHint !== undefined
|
|
312
|
+
? {
|
|
313
|
+
btwHintAvailable: true,
|
|
314
|
+
btwHintReason: 'Use /btw for side questions when the current implementation context is large.',
|
|
315
|
+
}
|
|
316
|
+
: {}),
|
|
312
317
|
...(updateBanner !== undefined ? { update_banner: updateBanner } : {}),
|
|
313
318
|
// SPEC-751: pending cleanup counters
|
|
314
319
|
pending_cleanup: pendingCleanup,
|
|
@@ -80,10 +80,8 @@ export async function handleSuggestMcpServer(args) {
|
|
|
80
80
|
`Estimated effort: ${hours}h`,
|
|
81
81
|
].join('\n');
|
|
82
82
|
return {
|
|
83
|
-
content: [
|
|
84
|
-
|
|
85
|
-
{ type: 'text', text: JSON.stringify(recommendation, null, 2) },
|
|
86
|
-
],
|
|
83
|
+
content: [{ type: 'text', text: summary }],
|
|
84
|
+
structuredContent: recommendation,
|
|
87
85
|
};
|
|
88
86
|
}
|
|
89
87
|
//# sourceMappingURL=suggest-mcp-server.js.map
|
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
export declare function recordToolTokens(toolName: string, projectPath: string, inputArgs: unknown, outputText: string, sessionId: string): void;
|
|
6
6
|
/**
|
|
7
7
|
* Extract output text from a ToolResult for token counting.
|
|
8
|
-
* Concatenates
|
|
8
|
+
* Concatenates visible text and compacted structuredContent into a single string.
|
|
9
9
|
*/
|
|
10
10
|
export declare function extractOutputText(result: {
|
|
11
11
|
content: {
|
|
12
12
|
type: string;
|
|
13
13
|
text?: string;
|
|
14
14
|
}[];
|
|
15
|
+
structuredContent?: Record<string, unknown>;
|
|
15
16
|
}): string;
|
|
16
17
|
//# sourceMappingURL=token-recording.d.ts.map
|
|
@@ -4,6 +4,20 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { countTokens, estimateCost } from '../engine/token-optimizer/counter.js';
|
|
5
5
|
import { recordEntry } from '../storage/token-ledger-store.js';
|
|
6
6
|
import { hashProjectPath } from '../storage/base-store.js';
|
|
7
|
+
const SECRET_KEY_PATTERN = /(api[-_]?key|auth|bearer|credential|password|secret|token)/i;
|
|
8
|
+
function stringifyForTokenCounting(value) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.stringify(value, (key, entryValue) => {
|
|
11
|
+
if (SECRET_KEY_PATTERN.test(key)) {
|
|
12
|
+
return '[redacted]';
|
|
13
|
+
}
|
|
14
|
+
return entryValue;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return '[unserializable structuredContent]';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
7
21
|
/**
|
|
8
22
|
* Record a tool call's token usage to the persistent ledger.
|
|
9
23
|
* Always fire-and-forget — never throws, never blocks the response.
|
|
@@ -39,11 +53,16 @@ export function recordToolTokens(toolName, projectPath, inputArgs, outputText, s
|
|
|
39
53
|
}
|
|
40
54
|
/**
|
|
41
55
|
* Extract output text from a ToolResult for token counting.
|
|
42
|
-
* Concatenates
|
|
56
|
+
* Concatenates visible text and compacted structuredContent into a single string.
|
|
43
57
|
*/
|
|
44
58
|
export function extractOutputText(result) {
|
|
45
|
-
|
|
59
|
+
const text = result.content
|
|
46
60
|
.flatMap((item) => (item.type === 'text' && typeof item.text === 'string' ? [item.text] : []))
|
|
47
61
|
.join('\n');
|
|
62
|
+
if (result.structuredContent === undefined) {
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
const structuredText = stringifyForTokenCounting(result.structuredContent);
|
|
66
|
+
return text.length > 0 ? `${text}\n${structuredText}` : structuredText;
|
|
48
67
|
}
|
|
49
68
|
//# sourceMappingURL=token-recording.js.map
|
|
@@ -28,6 +28,7 @@ export type ValidateGateResult = {
|
|
|
28
28
|
score: number | null;
|
|
29
29
|
forced: true;
|
|
30
30
|
forcedReason: string;
|
|
31
|
+
scoreSource: 'forced-best-effort-validateSpec';
|
|
31
32
|
};
|
|
32
33
|
/**
|
|
33
34
|
* SPEC-721 / SPEC-222 Trigger 1: Run validate engine before marking done.
|
|
@@ -99,8 +100,7 @@ export interface ComplianceGateResult {
|
|
|
99
100
|
* Never blocks the transition if the compliance engine is unavailable or fails.
|
|
100
101
|
*/
|
|
101
102
|
export declare function checkComplianceGate(specId: string, projectId: string, projectPath: string | undefined): Promise<ComplianceGateResult>;
|
|
102
|
-
|
|
103
|
-
export declare function checkQaGate(specId: string, projectPath: string | undefined, force: boolean): Promise<ToolResult | null>;
|
|
103
|
+
export { checkQaGate } from './qa-gate.js';
|
|
104
104
|
/**
|
|
105
105
|
* SPEC-716 / SPEC-780: Format gate for transition to 'approved'.
|
|
106
106
|
*
|