@planu/cli 4.11.2 → 4.11.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 +7 -0
- package/dist/config/elicitation-questions.json +5 -17
- package/dist/engine/elicitation/question-generator.js +10 -3
- package/dist/engine/implementation-contract/renderer.js +17 -39
- package/dist/engine/lifecycle-hints.d.ts +1 -1
- package/dist/engine/lifecycle-hints.js +13 -29
- package/dist/engine/spec-format/lean-spec-generator.d.ts +2 -2
- package/dist/engine/spec-format/lean-spec-generator.js +9 -10
- package/dist/engine/spec-generator/fallback-generator.js +13 -20
- package/dist/engine/spec-grounding/contract.d.ts +5 -0
- package/dist/engine/spec-grounding/contract.js +10 -6
- package/dist/engine/spec-migrator/lean-migration.js +9 -16
- package/dist/engine/web-fetcher/stack-advisor.d.ts +2 -2
- package/dist/engine/web-fetcher/stack-advisor.js +78 -17
- package/dist/tools/challenge-spec/challenge-report.d.ts +13 -0
- package/dist/tools/challenge-spec/challenge-report.js +55 -0
- package/dist/tools/challenge-spec.js +9 -8
- package/dist/tools/clarify-requirements/multiple-choice.js +1 -1
- package/dist/tools/clarify-requirements/questions-context.js +11 -11
- package/dist/tools/clarify-requirements/questions.js +3 -23
- package/dist/tools/clarify-requirements.js +38 -0
- package/dist/tools/create-spec/autopilot-analyzer.d.ts +1 -1
- package/dist/tools/create-spec/autopilot-analyzer.js +29 -54
- package/dist/tools/create-spec/post-creation.js +23 -4
- package/dist/tools/create-spec.js +47 -144
- package/dist/tools/elicit-requirements-handler.js +20 -12
- package/dist/tools/register-spec-tools/core-spec-tools.js +11 -2
- package/dist/tools/suggest-stack.js +49 -13
- package/dist/tools/update-status/dod-gates.d.ts +8 -0
- package/dist/tools/update-status/dod-gates.js +68 -43
- package/dist/tools/update-status/index.js +109 -74
- package/dist/tools/update-status/qa-gate.js +26 -0
- package/dist/tools/update-status/transition-guard.d.ts +2 -2
- package/dist/tools/update-status/transition-guard.js +10 -7
- package/dist/tools/validate-lint.d.ts +1 -1
- package/dist/tools/validate-lint.js +39 -1
- package/dist/types/spec/core.d.ts +20 -0
- package/dist/types/spec/inputs.d.ts +1 -1
- package/dist/types/stack/index.d.ts +1 -1
- package/dist/types/stack/recommend.d.ts +10 -0
- package/package.json +9 -9
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
- package/dist/engine/elicitation/non-interactive-defaults.d.ts +0 -9
- package/dist/engine/elicitation/non-interactive-defaults.js +0 -28
- package/dist/tools/clarify-requirements/interview-mode.d.ts +0 -42
- package/dist/tools/clarify-requirements/interview-mode.js +0 -342
|
@@ -137,7 +137,23 @@ const TECH_STACK_PATTERNS = [
|
|
|
137
137
|
];
|
|
138
138
|
/** Returns true if description references any known technology or version. */
|
|
139
139
|
function hasTechStackReferences(description) {
|
|
140
|
-
return TECH_STACK_PATTERNS.some((
|
|
140
|
+
return TECH_STACK_PATTERNS.some((pattern) => hasAffirmedPattern(description, pattern));
|
|
141
|
+
}
|
|
142
|
+
const NEGATIVE_CONTEXT_RE = /\b(?:no|not|never|without|exclude|excluding|forbid|forbidden|remove|avoid|must not|should not|do not|does not|don't|doesn't|out[- ]of[- ]scope|unsupported|unrelated|false positive)\b/i;
|
|
143
|
+
const INCIDENTAL_CONTEXT_RE = /\b(?:example|for example|e\.g\.|quoted|regression|bug caused by|incorrectly inferred|must not infer)\b/i;
|
|
144
|
+
function hasAffirmedPattern(description, pattern) {
|
|
145
|
+
const withoutQuotedText = description
|
|
146
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
147
|
+
.replace(/`[^`]+`/g, ' ')
|
|
148
|
+
.replace(/"[^"]+"|'[^']+'/g, ' ');
|
|
149
|
+
return withoutQuotedText.split(/(?:\r?\n|[.;!?]+)/).some((segment) => {
|
|
150
|
+
const match = new RegExp(pattern.source, pattern.flags.replace('g', '')).exec(segment);
|
|
151
|
+
if (!match) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const prefix = segment.slice(0, match.index);
|
|
155
|
+
return !NEGATIVE_CONTEXT_RE.test(prefix) && !INCIDENTAL_CONTEXT_RE.test(prefix);
|
|
156
|
+
});
|
|
141
157
|
}
|
|
142
158
|
// === Serverless platform detection ===
|
|
143
159
|
const SERVERLESS_PLATFORMS = ['vercel', 'netlify', 'lambda', 'cloudflare-workers', 'deno-deploy'];
|
|
@@ -185,12 +201,12 @@ export async function generatePostCreationSuggestions(projectPath, description,
|
|
|
185
201
|
if (isServerlessKnowledge(knowledge)) {
|
|
186
202
|
suggestions.push({
|
|
187
203
|
tool: 'scan_project',
|
|
188
|
-
reason: '[SERVERLESS WARNING] Module-level mutable state
|
|
204
|
+
reason: '[SERVERLESS WARNING] Module-level mutable state may reset on cold starts. Confirm the required state lifetime and storage strategy before implementation.',
|
|
189
205
|
});
|
|
190
206
|
}
|
|
191
|
-
// Item 4: Suggest templates when
|
|
207
|
+
// Item 4: Suggest templates only when the user explicitly requests one.
|
|
192
208
|
for (const { pattern, category } of TEMPLATE_PATTERNS) {
|
|
193
|
-
if (pattern
|
|
209
|
+
if (hasAffirmedPattern(description, pattern) && explicitlyRequestsTemplate(description)) {
|
|
194
210
|
suggestions.push({
|
|
195
211
|
tool: 'apply_template',
|
|
196
212
|
reason: `Description matches "${category}" pattern. Consider using an ${category} template.`,
|
|
@@ -205,6 +221,9 @@ export async function generatePostCreationSuggestions(projectPath, description,
|
|
|
205
221
|
});
|
|
206
222
|
return suggestions;
|
|
207
223
|
}
|
|
224
|
+
function explicitlyRequestsTemplate(description) {
|
|
225
|
+
return /\b(?:use|apply|start from|base (?:this|it) on)\s+(?:an?\s+)?(?:[\w-]+\s+)?template\b/i.test(description);
|
|
226
|
+
}
|
|
208
227
|
/** Formats a structured post-creation suggestion for human-readable next steps. */
|
|
209
228
|
export function formatPostCreationSuggestion(suggestion) {
|
|
210
229
|
const templateSuffix = suggestion.templateCategory !== undefined ? ` (${suggestion.templateCategory})` : '';
|
|
@@ -7,14 +7,12 @@ import { formatSuccess, addNextSteps, toolResult, interactiveResult } from './re
|
|
|
7
7
|
import { writeFile, mkdir, rm, readFile, stat as fsStat, rename, link as hardLink, } from 'node:fs/promises';
|
|
8
8
|
import { createHash, randomUUID } from 'node:crypto';
|
|
9
9
|
import { dirname as pathDirname, isAbsolute as pathIsAbsolute, join as pathJoin, relative as pathRelative, sep as pathSeparator, } from 'node:path';
|
|
10
|
-
import { estimateSpec } from '../engine/estimator.js';
|
|
11
10
|
import { checkSpecReadiness } from '../engine/readiness-checker.js';
|
|
12
11
|
import { buildSpecContext, buildSplitResult } from './create-spec/spec-builder.js';
|
|
13
12
|
import { validateConstitution } from './create-spec/constitution-validator.js';
|
|
14
13
|
import { setupGitBranch, checkContradictions, fireSpecCreatedHook, generatePostCreationSuggestions, formatPostCreationSuggestion, runAutopilotAsync, getAsyncAnalysisPath, } from './create-spec/post-creation.js';
|
|
15
14
|
import { notifyStoreChange } from '../engine/doc-generator/portal/regen-hook.js';
|
|
16
15
|
import { compactObj } from '../engine/compact-obj.js';
|
|
17
|
-
import { buildCreateSpecSummary } from '../engine/human-summary.js';
|
|
18
16
|
import { runAutoPostCreatePipeline } from './create-spec/auto-pipeline.js';
|
|
19
17
|
import { extractCriteria, generateLeanSpecContent, } from '../engine/spec-format/lean-spec-generator.js';
|
|
20
18
|
import { generateLeanTechnicalContent, } from '../engine/spec-format/lean-technical-generator.js';
|
|
@@ -22,7 +20,7 @@ import { extractFilesFromSpecBody } from '../engine/spec-format/technical-md-pop
|
|
|
22
20
|
import { buildUnifiedSpecContent } from '../engine/spec-format/unified-spec-builder.js';
|
|
23
21
|
import { appendImplementationContractIfMissing } from '../engine/implementation-contract/index.js';
|
|
24
22
|
import { validateEnglishOnlySpecText } from '../engine/spec-language/english-only.js';
|
|
25
|
-
import {
|
|
23
|
+
import { FallbackGenerator } from '../engine/spec-generator/index.js';
|
|
26
24
|
import { analyzeProjectForSpec, getEmptyAutopilotResult, } from './create-spec/autopilot-analyzer.js';
|
|
27
25
|
import { AutopilotSummaryCollector } from '../engine/autopilot/summary-collector.js';
|
|
28
26
|
import { trackCost } from '../engine/cost-tracking/operation-tracker.js';
|
|
@@ -33,10 +31,7 @@ import { resolveProjectIdOrAutoDetect } from './resolve-project-id.js';
|
|
|
33
31
|
import { hashProjectPath } from '../storage/base-store.js';
|
|
34
32
|
import { findSimilarSpecs } from '../engine/spec-searcher.js';
|
|
35
33
|
import { scoreSpecQuality } from '../engine/spec-quality-scorer.js';
|
|
36
|
-
import { enrichEstimationWithVelocity } from './create-spec/adapters/velocity-estimation.js';
|
|
37
|
-
import { enrichEstimationWithCalibration } from './create-spec/adapters/calibration-estimation.js';
|
|
38
34
|
import { runPriorDecisionsHint } from './create-spec/adapters/prior-decisions-hint.js';
|
|
39
|
-
import { suggestOutOfScope } from '../engine/scope-boundaries/index.js';
|
|
40
35
|
import { adviseSimilarSpecs } from '../engine/complexity-budget/index.js';
|
|
41
36
|
import { issuePlannerToken } from '../engine/reviewer-tokens/issuer.js';
|
|
42
37
|
import { generateInteractiveQuestions } from './create-spec/question-generator.js';
|
|
@@ -115,10 +110,9 @@ async function resolveTechnicalFiles(input) {
|
|
|
115
110
|
.join('\n\n');
|
|
116
111
|
const extracted = await extractFilesFromSpecBody(generatedSource, input.projectPath);
|
|
117
112
|
if (extracted !== null && hasFileEntries(extracted)) {
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return input.autopilot.suggestedFiles;
|
|
113
|
+
return input.fallbackReason === undefined
|
|
114
|
+
? mergeTechnicalFiles(extracted, input.autopilot.suggestedFiles)
|
|
115
|
+
: extracted;
|
|
122
116
|
}
|
|
123
117
|
return { create: [], modify: [], test: [] };
|
|
124
118
|
}
|
|
@@ -141,8 +135,9 @@ async function groundTechnicalFiles(input) {
|
|
|
141
135
|
const pathInUserInput = userInput.includes(file.path.toLowerCase());
|
|
142
136
|
const exists = await pathExists(pathJoin(input.projectPath, file.path));
|
|
143
137
|
const fromAutopilot = autopilotPaths.has(file.path);
|
|
144
|
-
const derivedTest = section === 'test' && isDerivedFromGroundedSource(file.path,
|
|
145
|
-
|
|
138
|
+
const derivedTest = section === 'test' && isDerivedFromGroundedSource(file.path, grounded.modify);
|
|
139
|
+
const directProjectEvidence = fromAutopilot && exists;
|
|
140
|
+
if (pathInUserInput || directProjectEvidence || derivedTest) {
|
|
146
141
|
grounded[section].push(file);
|
|
147
142
|
records.push({
|
|
148
143
|
path: file.path,
|
|
@@ -150,8 +145,9 @@ async function groundTechnicalFiles(input) {
|
|
|
150
145
|
source: pathInUserInput ? 'user_input' : 'project_evidence',
|
|
151
146
|
evidence: [
|
|
152
147
|
...(pathInUserInput ? ['create_spec.description'] : []),
|
|
153
|
-
...(
|
|
154
|
-
|
|
148
|
+
...(directProjectEvidence
|
|
149
|
+
? [`file:${file.path}`, 'autopilot.semantic-file-match']
|
|
150
|
+
: []),
|
|
155
151
|
...(derivedTest ? ['derived-test-path'] : []),
|
|
156
152
|
],
|
|
157
153
|
confidence: pathInUserInput || exists ? 'high' : 'medium',
|
|
@@ -224,24 +220,9 @@ function buildAdvisoryCompat() {
|
|
|
224
220
|
canonicalField: 'advisorySignals',
|
|
225
221
|
};
|
|
226
222
|
}
|
|
227
|
-
/**
|
|
228
|
-
function resolveOutOfScope(
|
|
229
|
-
|
|
230
|
-
return { items: provided, message: null };
|
|
231
|
-
}
|
|
232
|
-
try {
|
|
233
|
-
const auto = suggestOutOfScope(description).allSuggestions.slice(0, 3);
|
|
234
|
-
if (auto.length > 0) {
|
|
235
|
-
return {
|
|
236
|
-
items: auto,
|
|
237
|
-
message: `Auto-suggested outOfScope items based on description keywords: ${auto.join(', ')}`,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
/* best-effort */
|
|
243
|
-
}
|
|
244
|
-
return { items: [], message: null };
|
|
223
|
+
/** Persist only out-of-scope items supplied explicitly by the caller. */
|
|
224
|
+
function resolveOutOfScope(provided) {
|
|
225
|
+
return provided?.filter((item) => item.trim().length > 0) ?? [];
|
|
245
226
|
}
|
|
246
227
|
/** SPEC-614 AC4: suggest complexity category based on similar historical specs (best-effort). */
|
|
247
228
|
async function runComplexityAdvice(projectId, currentSpecId, tags, targetCriteriaCount) {
|
|
@@ -867,8 +848,6 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
867
848
|
}
|
|
868
849
|
params = clarificationResult.params;
|
|
869
850
|
}
|
|
870
|
-
// Create spec directory and write lean files (SPEC-461)
|
|
871
|
-
await measureStep('mkdir-specDir', () => mkdir(specDir, { recursive: true }));
|
|
872
851
|
const filteredCriteria = filterGroundedCriteria(autopilot.suggestedCriteria);
|
|
873
852
|
const technologyContract = await readTechnologySelectionContract(params.projectPath ?? '');
|
|
874
853
|
const contractNote = technologyContract
|
|
@@ -889,10 +868,7 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
889
868
|
.filter(Boolean)
|
|
890
869
|
.join('\n')
|
|
891
870
|
: '';
|
|
892
|
-
const
|
|
893
|
-
const specGenerator = anthropicKey !== undefined
|
|
894
|
-
? new OpusGenerator({ apiKey: anthropicKey })
|
|
895
|
-
: new FallbackGenerator();
|
|
871
|
+
const specGenerator = new FallbackGenerator();
|
|
896
872
|
const generatedSpec = await measureStep('generateSpecBody', () => specGenerator.generate({
|
|
897
873
|
title: spec.title,
|
|
898
874
|
description: `${description}${contractNote}`,
|
|
@@ -913,18 +889,32 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
913
889
|
const groundingCriteria = buildCriterionGroundingRecords({
|
|
914
890
|
criteria: [...baseCriteria, ...filteredCriteria],
|
|
915
891
|
userInput: description,
|
|
916
|
-
projectEvidence: [
|
|
917
|
-
...autopilot.detectedPatterns.map((pattern) => `detected-pattern:${pattern}`),
|
|
918
|
-
...autopilot.suggestedFiles.modify.map((file) => `file:${file.path}`),
|
|
919
|
-
...autopilot.suggestedFiles.create.map((file) => `file:${file.path}`),
|
|
920
|
-
...autopilot.suggestedFiles.test.map((file) => `file:${file.path}`),
|
|
921
|
-
],
|
|
922
892
|
generatedEvidence: [
|
|
923
893
|
generatedSpec.generation.modelId ?? generatedSpec.generation.method,
|
|
924
894
|
],
|
|
925
895
|
});
|
|
926
896
|
const contractCriteria = getContractCriteria(groundingCriteria);
|
|
927
897
|
const advisoryCriteria = getAdvisoryCriteria(groundingCriteria).map((record) => record.text);
|
|
898
|
+
if (contractCriteria.length === 0) {
|
|
899
|
+
return {
|
|
900
|
+
ok: false,
|
|
901
|
+
earlyReturn: {
|
|
902
|
+
content: [
|
|
903
|
+
{
|
|
904
|
+
type: 'text',
|
|
905
|
+
text: 'create_spec did not persist a spec because no acceptance criterion was grounded in the user request. ' +
|
|
906
|
+
'Needs decision: provide at least one explicit, verifiable criterion as a checkbox or Given/When/Then outcome.',
|
|
907
|
+
},
|
|
908
|
+
],
|
|
909
|
+
isError: true,
|
|
910
|
+
structuredContent: {
|
|
911
|
+
error: 'MISSING_GROUNDED_ACCEPTANCE_CRITERIA',
|
|
912
|
+
missingDecision: 'Provide at least one explicit, verifiable acceptance criterion.',
|
|
913
|
+
persisted: false,
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
};
|
|
917
|
+
}
|
|
928
918
|
const actionableMetrics = calculateActionableSpecMetrics({
|
|
929
919
|
criteria: [...baseCriteria, ...filteredCriteria],
|
|
930
920
|
groundingRecords: groundingCriteria,
|
|
@@ -942,7 +932,7 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
942
932
|
userInput: description,
|
|
943
933
|
autopilot,
|
|
944
934
|
}));
|
|
945
|
-
const
|
|
935
|
+
const outOfScope = resolveOutOfScope(params.outOfScope);
|
|
946
936
|
const scenarioTestPaths = groundedTechnical.files.test.map((file) => file.path);
|
|
947
937
|
const leanSpec = generateLeanSpecContent({
|
|
948
938
|
spec,
|
|
@@ -952,7 +942,7 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
952
942
|
text: record.text,
|
|
953
943
|
done: false,
|
|
954
944
|
})),
|
|
955
|
-
groundingCriteria,
|
|
945
|
+
groundingCriteria: contractCriteria,
|
|
956
946
|
groundingTechnicalReferences: groundedTechnical.records,
|
|
957
947
|
acFormat: params.acFormat,
|
|
958
948
|
scenarioTestPaths,
|
|
@@ -972,15 +962,8 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
972
962
|
description: generatedSpec.specBody,
|
|
973
963
|
criteria: contractCriteria.map((record) => ({ text: record.text, done: false })),
|
|
974
964
|
files: groundedTechnical.files,
|
|
975
|
-
outOfScope
|
|
976
|
-
verificationCommands: [
|
|
977
|
-
...(scenarioTestPaths.length > 0
|
|
978
|
-
? [`pnpm vitest run ${scenarioTestPaths.join(' ')}`]
|
|
979
|
-
: []),
|
|
980
|
-
'pnpm typecheck',
|
|
981
|
-
'pnpm lint',
|
|
982
|
-
'pnpm test',
|
|
983
|
-
],
|
|
965
|
+
outOfScope,
|
|
966
|
+
verificationCommands: [],
|
|
984
967
|
});
|
|
985
968
|
const genericOutputGate = checkGenericSpecOutput(unifiedSpec);
|
|
986
969
|
if (!genericOutputGate.passed) {
|
|
@@ -1008,6 +991,7 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1008
991
|
};
|
|
1009
992
|
}
|
|
1010
993
|
try {
|
|
994
|
+
await measureStep('mkdir-specDir', () => mkdir(specDir, { recursive: true }));
|
|
1011
995
|
// SPEC-713: measure file write — this is the critical persistence step.
|
|
1012
996
|
await measureStep('writeFile-specPath', () => writeFile(specPath, unifiedSpec, 'utf-8'));
|
|
1013
997
|
await measureStep('commit-idempotency-evidence', () => commitIdempotencyEvidence(resolvedPath, idempotencyKey, idempotencyClaim, spec, specPath));
|
|
@@ -1020,11 +1004,10 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1020
1004
|
await rm(specDir, { recursive: true, force: true });
|
|
1021
1005
|
throw writeErr;
|
|
1022
1006
|
}
|
|
1023
|
-
//
|
|
1024
|
-
if (
|
|
1025
|
-
spec.outOfScope =
|
|
1007
|
+
// Persist only explicit out-of-scope input.
|
|
1008
|
+
if (outOfScope.length > 0) {
|
|
1009
|
+
spec.outOfScope = outOfScope;
|
|
1026
1010
|
}
|
|
1027
|
-
const outOfScopeSuggestionMsg = outOfScopeResolved.message;
|
|
1028
1011
|
// Persist spec in storage
|
|
1029
1012
|
// SPEC-713: measure specStore.createSpec — file lock + JSON rewrite
|
|
1030
1013
|
await measureStep('specStore-createSpec', () => specStore.createSpec(projectId, spec));
|
|
@@ -1050,7 +1033,6 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1050
1033
|
...groundedTechnical.advisoryFiles.map((file) => file.path),
|
|
1051
1034
|
],
|
|
1052
1035
|
actionableMetrics,
|
|
1053
|
-
outOfScopeSuggestionMsg,
|
|
1054
1036
|
},
|
|
1055
1037
|
};
|
|
1056
1038
|
}); // end withTotalBudget critical path
|
|
@@ -1081,7 +1063,7 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1081
1063
|
// Destructure critical path results for use in post-creation enrichment
|
|
1082
1064
|
const { spec, specDir: _specDir, specPath,
|
|
1083
1065
|
// SPEC-1010 Bug A: no longer surfaced in the response payload (SSR back-migration).
|
|
1084
|
-
technicalPath: _technicalPath, estimation, splitSuggestion, duplicate, projectId, agentTeamPlan, knowledge, clarificationSession, constitutionCheck, autopilot, filteredCriteria, advisoryCriteria, actionableMetrics,
|
|
1066
|
+
technicalPath: _technicalPath, estimation, splitSuggestion, duplicate, projectId, agentTeamPlan, knowledge, clarificationSession, constitutionCheck, autopilot, filteredCriteria, advisoryCriteria, actionableMetrics, } = criticalResult.value.data;
|
|
1085
1067
|
// -----------------------------------------------------------------------
|
|
1086
1068
|
// Post-creation enrichment (outside 25s ceiling — best-effort, budgeted)
|
|
1087
1069
|
// -----------------------------------------------------------------------
|
|
@@ -1098,21 +1080,6 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1098
1080
|
// SPEC-713: withBudget 2s — scans all specs for contradiction keywords
|
|
1099
1081
|
const contradictionResult = await withBudget('checkContradictions', 2_000, () => measureStep('checkContradictions', () => checkContradictions(projectId, spec.id, description)));
|
|
1100
1082
|
const contradictionHint = unwrapBudget(contradictionResult, undefined);
|
|
1101
|
-
// SPEC-555: Enrich estimation with calendar days (best-effort, non-blocking)
|
|
1102
|
-
// SPEC-713: withBudget 2s
|
|
1103
|
-
const velocityResult = await withBudget('velocityEnrichment', 2_000, () => measureStep('velocityEnrichment', () => enrichEstimationWithVelocity(params.projectPath ?? '', estimation.devHours)));
|
|
1104
|
-
const velocityEnrichment = unwrapBudget(velocityResult, {
|
|
1105
|
-
calendarDays: null,
|
|
1106
|
-
velocityNote: 'velocity data unavailable',
|
|
1107
|
-
});
|
|
1108
|
-
// SPEC-506: Apply persisted calibration multiplier (best-effort, non-blocking)
|
|
1109
|
-
// SPEC-713: withBudget 2s
|
|
1110
|
-
const calibrationResult = await withBudget('calibrationEnrichment', 2_000, () => measureStep('calibrationEnrichment', () => enrichEstimationWithCalibration(params.projectPath ?? '', estimation.devHours, spec.scope, spec.type)));
|
|
1111
|
-
const calibrationEnrichment = unwrapBudget(calibrationResult, {
|
|
1112
|
-
calibratedHours: estimation.devHours,
|
|
1113
|
-
applied: false,
|
|
1114
|
-
calibrationNote: null,
|
|
1115
|
-
});
|
|
1116
1083
|
// Build result (SPEC-461: lean — no progress, HTML, diagrams, scope filters)
|
|
1117
1084
|
const result = {
|
|
1118
1085
|
// SPEC-781: async analysis running in background (external project data)
|
|
@@ -1134,15 +1101,6 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1134
1101
|
// ## Technical; the file is never written and the field would point to
|
|
1135
1102
|
// a non-existent path. Internal Spec record still carries it for
|
|
1136
1103
|
// backwards-compat with stored data — that field is removed in PR-C.
|
|
1137
|
-
estimation: {
|
|
1138
|
-
devHours: calibrationEnrichment.calibratedHours,
|
|
1139
|
-
reviewHours: estimation.reviewHours,
|
|
1140
|
-
totalCostUsd: estimation.totalCostUsd,
|
|
1141
|
-
recommendedModel: estimation.recommendedModel,
|
|
1142
|
-
executionMode: estimation.tokenOptimization.mode,
|
|
1143
|
-
calendarDays: velocityEnrichment.calendarDays,
|
|
1144
|
-
velocityNote: velocityEnrichment.velocityNote,
|
|
1145
|
-
},
|
|
1146
1104
|
duplicateWarning: duplicate
|
|
1147
1105
|
? ti('spec.duplicateTitle', { title: spec.title, slug: spec.slug })
|
|
1148
1106
|
: undefined,
|
|
@@ -1156,7 +1114,7 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1156
1114
|
clarificationAnswersUsed: Object.keys(clarificationSession.answers).length,
|
|
1157
1115
|
}
|
|
1158
1116
|
: {
|
|
1159
|
-
clarificationNote: 'Spec created directly from
|
|
1117
|
+
clarificationNote: 'Spec created directly from the supplied description; no clarification session was linked.',
|
|
1160
1118
|
}),
|
|
1161
1119
|
message: ti('tools.create_spec.success', { id: spec.id, title: spec.title }),
|
|
1162
1120
|
...(contradictionHint ? { contradictionHint } : {}),
|
|
@@ -1181,7 +1139,6 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1181
1139
|
}
|
|
1182
1140
|
const splitResult = buildSplitResult(splitSuggestion, knowledge?.experienceLevel);
|
|
1183
1141
|
if (splitResult) {
|
|
1184
|
-
result.splitSuggestion = splitResult;
|
|
1185
1142
|
advisorySignals.push(makeAdvisorySignal({
|
|
1186
1143
|
key: 'split-suggestion',
|
|
1187
1144
|
kind: 'complexity',
|
|
@@ -1191,7 +1148,6 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1191
1148
|
confidence: 0.5,
|
|
1192
1149
|
surface: 'structuredContent',
|
|
1193
1150
|
value: splitResult,
|
|
1194
|
-
deprecatedAlias: 'splitSuggestion',
|
|
1195
1151
|
}));
|
|
1196
1152
|
}
|
|
1197
1153
|
// Dispatch hook event (fire-and-forget)
|
|
@@ -1200,21 +1156,6 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1200
1156
|
runAutopilotAsync(spec.id, params.projectPath ?? '', description);
|
|
1201
1157
|
// SPEC-713: Track warnings from budgeted post-creation steps
|
|
1202
1158
|
const budgetWarnings = [];
|
|
1203
|
-
// Auto-estimation (best-effort, fire-and-forget)
|
|
1204
|
-
// SPEC-713: withBudget 2s — estimation is sync, specStore.updateSpec is a single JSON write
|
|
1205
|
-
const autoEstimationResult = await withBudget('auto-estimation', 2_000, async () => {
|
|
1206
|
-
const autoEstimation = estimateSpec(spec);
|
|
1207
|
-
await measureStep('specStore-updateSpec', () => specStore.updateSpec(projectId, spec.id, { estimation: autoEstimation.estimation }));
|
|
1208
|
-
return autoEstimation;
|
|
1209
|
-
});
|
|
1210
|
-
if (!autoEstimationResult.exceeded) {
|
|
1211
|
-
const ae = autoEstimationResult.value;
|
|
1212
|
-
result.autoEstimation = {
|
|
1213
|
-
confidence: ae.confidence,
|
|
1214
|
-
reasoning: ae.reasoning,
|
|
1215
|
-
similarSpecs: ae.similarSpecs,
|
|
1216
|
-
};
|
|
1217
|
-
}
|
|
1218
1159
|
// SPEC-461: No per-spec HTML reports — lean format
|
|
1219
1160
|
// SPEC-713: Run post-creation checks in parallel to reduce total wall-clock time.
|
|
1220
1161
|
// Each has an individual budget; results are merged into `result` after settlement.
|
|
@@ -1376,9 +1317,6 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1376
1317
|
`| Target | ${spec.target} |`,
|
|
1377
1318
|
`| Status | ${spec.status} |`,
|
|
1378
1319
|
`| Tags | ${spec.tags.join(', ')} |`,
|
|
1379
|
-
`| Dev hours | ${String(estimation.devHours)}h |`,
|
|
1380
|
-
`| Review hours | ${String(estimation.reviewHours)}h |`,
|
|
1381
|
-
`| Cost | $${String(estimation.totalCostUsd)} |`,
|
|
1382
1320
|
];
|
|
1383
1321
|
if (spec.gitBranch) {
|
|
1384
1322
|
lines.push(`| Branch | \`${spec.gitBranch}\` |`);
|
|
@@ -1406,20 +1344,6 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1406
1344
|
: formatSuccess(ti('tools.create_spec.success', { id: spec.id, title: spec.title }), lines.join('\n'));
|
|
1407
1345
|
// SPEC-469: Build autopilot summary from analyzer results
|
|
1408
1346
|
const collector = new AutopilotSummaryCollector();
|
|
1409
|
-
// SPEC-612: Record outOfScope auto-suggestion in autopilot summary
|
|
1410
|
-
if (outOfScopeSuggestionMsg !== null) {
|
|
1411
|
-
collector.pushOk('scope-boundaries', outOfScopeSuggestionMsg);
|
|
1412
|
-
advisorySignals.push(makeAdvisorySignal({
|
|
1413
|
-
key: 'out-of-scope-suggestions',
|
|
1414
|
-
kind: 'out-of-scope',
|
|
1415
|
-
message: outOfScopeSuggestionMsg,
|
|
1416
|
-
source: 'heuristic',
|
|
1417
|
-
evidence: ['scope-boundaries suggester'],
|
|
1418
|
-
confidence: 0.45,
|
|
1419
|
-
surface: 'structuredContent',
|
|
1420
|
-
value: spec.outOfScope ?? [],
|
|
1421
|
-
}));
|
|
1422
|
-
}
|
|
1423
1347
|
if (autopilot.detectedPatterns.length > 0) {
|
|
1424
1348
|
collector.pushOk('pattern-detection', `Detected patterns: ${autopilot.detectedPatterns.join(', ')}`);
|
|
1425
1349
|
advisorySignals.push(makeAdvisorySignal({
|
|
@@ -1459,17 +1383,13 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1459
1383
|
}));
|
|
1460
1384
|
}
|
|
1461
1385
|
if (filteredCriteria.length > 0) {
|
|
1462
|
-
collector.pushOk('criteria-enrichment', `
|
|
1386
|
+
collector.pushOk('criteria-enrichment', `Evaluated ${String(filteredCriteria.length)} proposed criteria; only directly grounded items entered the contract`);
|
|
1463
1387
|
}
|
|
1464
1388
|
if (pipelineResult.challengeSummary) {
|
|
1465
1389
|
collector.pushOk('challenge', `Challenge analysis complete: ${pipelineResult.challengeSummary}`);
|
|
1466
1390
|
}
|
|
1467
1391
|
if (pipelineResult.readinessScore !== null) {
|
|
1468
|
-
|
|
1469
|
-
const opusHint = spec.difficulty >= 3
|
|
1470
|
-
? ` — difficulty ${String(spec.difficulty)}: use Opus to add exact file paths + function names`
|
|
1471
|
-
: '';
|
|
1472
|
-
collector.pushOk('readiness', `Readiness score: ${String(pipelineResult.readinessScore)}/100${opusHint}`);
|
|
1392
|
+
collector.pushOk('readiness', `Readiness score: ${String(pipelineResult.readinessScore)}/100`);
|
|
1473
1393
|
advisorySignals.push(makeAdvisorySignal({
|
|
1474
1394
|
key: 'readiness-score',
|
|
1475
1395
|
kind: 'readiness',
|
|
@@ -1481,24 +1401,7 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1481
1401
|
value: pipelineResult.readinessScore,
|
|
1482
1402
|
}));
|
|
1483
1403
|
}
|
|
1484
|
-
|
|
1485
|
-
// SPEC-629: always surface the Opus hint for high-difficulty specs
|
|
1486
|
-
collector.pushOk('recommend_model', `Difficulty ${String(spec.difficulty)} spec — use Opus to add exact file paths, function names and anticipated test breaks.`);
|
|
1487
|
-
advisorySignals.push(makeAdvisorySignal({
|
|
1488
|
-
key: 'model-recommendation',
|
|
1489
|
-
kind: 'model',
|
|
1490
|
-
message: 'High-difficulty spec may benefit from a stronger model for review.',
|
|
1491
|
-
source: 'heuristic',
|
|
1492
|
-
evidence: [`difficulty:${String(spec.difficulty)}`],
|
|
1493
|
-
confidence: 0.4,
|
|
1494
|
-
surface: 'structuredContent',
|
|
1495
|
-
value: { recommendedTier: 'max' },
|
|
1496
|
-
}));
|
|
1497
|
-
}
|
|
1498
|
-
if (calibrationEnrichment.calibrationNote) {
|
|
1499
|
-
collector.pushOk('calibration', `📐 ${calibrationEnrichment.calibrationNote}`);
|
|
1500
|
-
}
|
|
1501
|
-
const humanSummary = buildCreateSpecSummary(spec.title, estimation.devHours);
|
|
1404
|
+
const humanSummary = `Plan created for "${spec.title}". Review the grounded criteria before approval.`;
|
|
1502
1405
|
result.advisorySignals = advisorySignals;
|
|
1503
1406
|
result.compat = buildAdvisoryCompat();
|
|
1504
1407
|
const compactResult = compactObj(result);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { generateSpecQuestions, formatQuestionsAsPrompt, parseAnswers, } from '../engine/elicitation/index.js';
|
|
3
|
-
import { buildDefaultAnswers } from '../engine/elicitation/non-interactive-defaults.js';
|
|
4
3
|
/** Build a clarification summary from parsed answers */
|
|
5
4
|
function buildSummary(session, parsedAnswers) {
|
|
6
5
|
const lines = [`## Clarification Summary for "${session.title}"`, ''];
|
|
@@ -34,10 +33,8 @@ export function handleElicitRequirements(input) {
|
|
|
34
33
|
const { title, description, answers, mode } = input;
|
|
35
34
|
// intakeRef is silently acknowledged — used by downstream agents to chain context
|
|
36
35
|
const context = {
|
|
37
|
-
hasDatabase: description
|
|
38
|
-
|
|
39
|
-
description.toLowerCase().includes('database'),
|
|
40
|
-
isApi: description.toLowerCase().includes('api') || description.toLowerCase().includes('endpoint'),
|
|
36
|
+
hasDatabase: hasAffirmedTerm(description, /\b(?:data|store|database)\b/i),
|
|
37
|
+
isApi: hasAffirmedTerm(description, /\b(?:api|endpoint)\b/i),
|
|
41
38
|
};
|
|
42
39
|
const questions = generateSpecQuestions(title, description, context);
|
|
43
40
|
const sessionId = input.sessionId ?? randomUUID();
|
|
@@ -47,17 +44,21 @@ export function handleElicitRequirements(input) {
|
|
|
47
44
|
questions,
|
|
48
45
|
answers: {},
|
|
49
46
|
};
|
|
50
|
-
//
|
|
47
|
+
// Non-interactive mode cannot complete unanswered decisions on the user's behalf.
|
|
51
48
|
if (mode === 'non-interactive' && (answers === undefined || answers.trim().length === 0)) {
|
|
52
|
-
const
|
|
53
|
-
session.answers = defaultAnswers;
|
|
54
|
-
session.completedAt = new Date().toISOString();
|
|
55
|
-
const summary = buildSummary(session, defaultAnswers);
|
|
49
|
+
const missingDecision = 'Needs decision: provide explicit answers before requirements can be treated as confirmed.';
|
|
56
50
|
return Promise.resolve({
|
|
57
|
-
content: [{ type: 'text', text:
|
|
51
|
+
content: [{ type: 'text', text: missingDecision }],
|
|
58
52
|
structuredContent: {
|
|
59
|
-
summary,
|
|
60
53
|
mode: 'non-interactive',
|
|
54
|
+
complete: false,
|
|
55
|
+
missingDecision,
|
|
56
|
+
pendingQuestions: questions.map((question) => ({
|
|
57
|
+
id: question.id,
|
|
58
|
+
question: question.question,
|
|
59
|
+
required: question.required,
|
|
60
|
+
...(question.choices ? { candidates: question.choices } : {}),
|
|
61
|
+
})),
|
|
61
62
|
},
|
|
62
63
|
});
|
|
63
64
|
}
|
|
@@ -81,4 +82,11 @@ export function handleElicitRequirements(input) {
|
|
|
81
82
|
const summary = buildSummary(session, parsedAnswers);
|
|
82
83
|
return Promise.resolve({ content: [{ type: 'text', text: summary }] });
|
|
83
84
|
}
|
|
85
|
+
const NEGATION_RE = /\b(?:no|not|never|without|exclude|excluding|forbid|forbidden|avoid|must not|should not|do not|does not|don't|doesn't|out[- ]of[- ]scope)\b/i;
|
|
86
|
+
function hasAffirmedTerm(description, pattern) {
|
|
87
|
+
return description.split(/(?:\r?\n|[.;!?]+)/).some((segment) => {
|
|
88
|
+
const match = new RegExp(pattern.source, pattern.flags.replace('g', '')).exec(segment);
|
|
89
|
+
return match !== null && !NEGATION_RE.test(segment.slice(0, match.index));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
84
92
|
//# sourceMappingURL=elicit-requirements-handler.js.map
|
|
@@ -167,8 +167,17 @@ export function registerCoreSpecTools(server) {
|
|
|
167
167
|
.number()
|
|
168
168
|
.optional()
|
|
169
169
|
.describe('Maximum number of questions to generate (default: 5)'),
|
|
170
|
+
sessionId: z
|
|
171
|
+
.string()
|
|
172
|
+
.max(500)
|
|
173
|
+
.optional()
|
|
174
|
+
.describe('Existing clarification session to resume with user-confirmed answers.'),
|
|
175
|
+
answers: z
|
|
176
|
+
.record(z.string(), z.string())
|
|
177
|
+
.optional()
|
|
178
|
+
.describe('Explicit user-confirmed answers keyed by clarification question ID.'),
|
|
170
179
|
},
|
|
171
|
-
}, safeTracked('clarify_requirements', async (args) => handleClarifyRequirements(args)));
|
|
180
|
+
}, safeTracked('clarify_requirements', async (args) => handleClarifyRequirements(args, server)));
|
|
172
181
|
// 5. create_spec
|
|
173
182
|
server.registerTool('create_spec', {
|
|
174
183
|
description: t('tools.create_spec.description'),
|
|
@@ -210,7 +219,7 @@ export function registerCoreSpecTools(server) {
|
|
|
210
219
|
outOfScope: z
|
|
211
220
|
.array(z.string())
|
|
212
221
|
.optional()
|
|
213
|
-
.describe('
|
|
222
|
+
.describe('Explicit user-confirmed out-of-scope items for this spec.'),
|
|
214
223
|
specId: z
|
|
215
224
|
.string()
|
|
216
225
|
.optional()
|