@planu/cli 4.11.1 ā 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 +16 -0
- package/dist/config/elicitation-questions.json +5 -17
- package/dist/engine/cascade-hooks/registry.js +0 -2
- package/dist/engine/challenge-scenarios-loader.d.ts +3 -3
- package/dist/engine/compliance-injector.d.ts +3 -3
- package/dist/engine/dashboard/kanban-logic.d.ts +1 -1
- package/dist/engine/doc-generator/portal/risk-matrix-svg.d.ts +1 -1
- package/dist/engine/doc-generator/proposal/proposal-generator.d.ts +1 -1
- package/dist/engine/elicitation/answer-extractor.d.ts +1 -4
- package/dist/engine/elicitation/answer-extractor.js +197 -68
- package/dist/engine/elicitation/decision-gap-detector.js +59 -79
- package/dist/engine/elicitation/index.d.ts +0 -1
- package/dist/engine/elicitation/index.js +0 -1
- package/dist/engine/elicitation/question-generator.js +10 -3
- package/dist/engine/elicitation/question-grounding-gate.js +83 -34
- package/dist/engine/evidence-gates/lifecycle-gate.js +27 -14
- package/dist/engine/evidence-index/index-builder.js +7 -2
- package/dist/engine/frontmatter-sha/frozen-fields.d.ts +1 -1
- package/dist/engine/handoff-artifacts/schemas/red-tests-handoff.d.ts +2 -2
- package/dist/engine/handoff-artifacts/schemas.d.ts +22 -22
- package/dist/engine/human-summary.js +3 -3
- package/dist/engine/implementation-contract/common.d.ts +1 -1
- package/dist/engine/implementation-contract/renderer.js +17 -39
- package/dist/engine/keyword-matcher/stopwords.d.ts +2 -2
- package/dist/engine/lifecycle-hints.d.ts +1 -1
- package/dist/engine/lifecycle-hints.js +13 -29
- package/dist/engine/provider-adapters/adapters/claude.d.ts +1 -1
- package/dist/engine/provider-adapters/adapters/gemini.d.ts +1 -1
- package/dist/engine/provider-adapters/adapters/gpt4.d.ts +1 -1
- package/dist/engine/provider-adapters/adapters/markdown.d.ts +1 -1
- package/dist/engine/safety/atomic-write-file.d.ts +1 -1
- package/dist/engine/safety/cross-process-lock.d.ts +1 -1
- package/dist/engine/safety/path-sanitizer.d.ts +1 -1
- package/dist/engine/security-analyzer/profile-detector.js +48 -16
- package/dist/engine/spec-format/copula-blacklist.d.ts +2 -2
- 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-format/metadata-value-policy.d.ts +20 -20
- 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/text-signal-boundaries.js +45 -12
- package/dist/engine/universal-rules/rules/planu-modes.js +0 -2
- package/dist/engine/web-fetcher/stack-advisor.d.ts +2 -2
- package/dist/engine/web-fetcher/stack-advisor.js +78 -17
- package/dist/engine/well-known/manifest-schema.d.ts +33 -33
- package/dist/engine/workers/schema.d.ts +3 -3
- package/dist/hosts/claude-code/ux/mcp-resources.d.ts +4 -4
- package/dist/hosts/codex/rest-shim.d.ts +1 -1
- package/dist/native/lightweight-command-catalog.d.ts +30 -30
- package/dist/server/openapi-spec.d.ts +132 -132
- 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/event-challenge-scenarios.js +8 -27
- package/dist/tools/challenge-spec/resilience-challenge-scenarios-a.js +16 -32
- package/dist/tools/challenge-spec/resilience-challenge-scenarios-b.js +15 -20
- package/dist/tools/challenge-spec/scenario-collector.d.ts +13 -0
- package/dist/tools/challenge-spec/scenario-collector.js +86 -0
- package/dist/tools/challenge-spec/scenarios-data.js +10 -9
- package/dist/tools/challenge-spec/scenarios-failure.js +38 -54
- package/dist/tools/challenge-spec/scenarios-scale.js +14 -15
- package/dist/tools/challenge-spec/scenarios-security.d.ts +1 -1
- package/dist/tools/challenge-spec/scenarios-security.js +18 -11
- package/dist/tools/challenge-spec/scenarios-utils.d.ts +31 -5
- package/dist/tools/challenge-spec/scenarios-utils.js +102 -6
- package/dist/tools/challenge-spec/security-challenge-scenarios.d.ts +1 -1
- package/dist/tools/challenge-spec/security-challenge-scenarios.js +20 -25
- package/dist/tools/challenge-spec.js +46 -124
- 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/compliance-gate-handler.d.ts +1 -1
- 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 +51 -168
- package/dist/tools/delete-first/suggest-deletions.d.ts +1 -1
- package/dist/tools/elicit-requirements-handler.js +20 -12
- package/dist/tools/generate-batch-script.d.ts +2 -2
- package/dist/tools/init-project/rules-generator.js +0 -1
- package/dist/tools/oauth-handler.d.ts +1 -1
- package/dist/tools/register-sdd-tools.d.ts +1 -1
- package/dist/tools/register-spec-tools/core-spec-tools.js +11 -2
- package/dist/tools/render-spec-for-provider.d.ts +2 -2
- package/dist/tools/scan-orphan-spec-refs.d.ts +6 -6
- package/dist/tools/schemas/agents.d.ts +7 -7
- package/dist/tools/schemas/analysis.d.ts +11 -11
- package/dist/tools/schemas/delete.d.ts +5 -5
- package/dist/tools/schemas/github.d.ts +4 -4
- package/dist/tools/schemas/ide-config.d.ts +12 -12
- package/dist/tools/schemas/infra.d.ts +9 -9
- package/dist/tools/schemas/lifecycle.d.ts +50 -50
- package/dist/tools/schemas/llm-provider-schemas.d.ts +7 -7
- package/dist/tools/schemas/plugins-schemas.d.ts +12 -12
- package/dist/tools/schemas/project-graph.d.ts +6 -6
- package/dist/tools/schemas/rbac.d.ts +2 -2
- package/dist/tools/schemas/registry.d.ts +3 -3
- package/dist/tools/schemas/runtime-security.d.ts +2 -2
- package/dist/tools/schemas/session.d.ts +2 -2
- package/dist/tools/schemas/spec.d.ts +15 -15
- package/dist/tools/schemas/token-intelligence.d.ts +6 -6
- package/dist/tools/schemas/token-optimization.d.ts +2 -2
- package/dist/tools/schemas/workers-schema.d.ts +4 -4
- package/dist/tools/skill-bootstrap-handler.d.ts +7 -7
- package/dist/tools/suggest-stack.js +49 -13
- package/dist/tools/tool-registry/deprecated-stubs.d.ts +1 -1
- package/dist/tools/triage-request.d.ts +3 -3
- 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/file-sync.d.ts +1 -1
- package/dist/tools/update-status/file-sync.js +10 -45
- package/dist/tools/update-status/index.js +133 -97
- package/dist/tools/update-status/qa-gate.js +26 -0
- package/dist/tools/update-status/response-builder.d.ts +1 -1
- package/dist/tools/update-status/response-builder.js +11 -19
- package/dist/tools/update-status/transition-guard.d.ts +2 -2
- package/dist/tools/update-status/transition-guard.js +11 -8
- package/dist/tools/update-status-actions.d.ts +0 -1
- package/dist/tools/update-status-actions.js +10 -26
- package/dist/tools/update-status-convention-gate.d.ts +1 -6
- package/dist/tools/update-status-convention-gate.js +9 -33
- package/dist/tools/validate-lint.d.ts +1 -1
- package/dist/tools/validate-lint.js +39 -1
- package/dist/types/docs.d.ts +0 -4
- package/dist/types/env.d.ts +0 -4
- package/dist/types/estimation.d.ts +0 -2
- package/dist/types/multi-teammate-review.d.ts +4 -4
- package/dist/types/plugin-install.d.ts +7 -7
- 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 +21 -18
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
- package/src/i18n/messages/en.json +0 -72
- package/src/i18n/messages/es.json +0 -72
- package/src/i18n/messages/pt.json +0 -72
- package/dist/config/elicitation-dimensions.json +0 -51
- package/dist/engine/actuals-estimator.d.ts +0 -14
- package/dist/engine/actuals-estimator.js +0 -64
- package/dist/engine/cascade-hooks/hooks/living-specs.hook.d.ts +0 -3
- package/dist/engine/cascade-hooks/hooks/living-specs.hook.js +0 -19
- package/dist/engine/elicitation/non-interactive-defaults.d.ts +0 -9
- package/dist/engine/elicitation/non-interactive-defaults.js +0 -28
- package/dist/engine/elicitation/option-builder.d.ts +0 -9
- package/dist/engine/elicitation/option-builder.js +0 -85
- package/dist/engine/progress-writer.d.ts +0 -10
- package/dist/engine/progress-writer.js +0 -63
- package/dist/tools/clarify-requirements/interview-mode.d.ts +0 -42
- package/dist/tools/clarify-requirements/interview-mode.js +0 -342
- package/dist/tools/update-status/mode-hints.d.ts +0 -4
- package/dist/tools/update-status/mode-hints.js +0 -24
|
@@ -13,11 +13,9 @@ import { checkApprovedDepGate } from '../../engine/dep-guard/index.js';
|
|
|
13
13
|
import { checkApprovalGate } from '../../engine/approval-workflow.js';
|
|
14
14
|
import * as approvalStore from '../../storage/approval-store.js';
|
|
15
15
|
import { isLocked, getLock } from '../../storage/spec-lock-store.js';
|
|
16
|
-
import { runValidateGate, checkDoneGates, checkComplianceGate, checkQaGate, checkApprovedFormatGate, checkValidationReportGate, checkSpecReviewGate, writeSpecReviewArtifact, } from './dod-gates.js';
|
|
16
|
+
import { runValidateGate, checkDoneGates, checkComplianceGate, checkQaGate, checkApprovedFormatGate, checkValidationReportGate, readApprovedValidationReportGate, checkSpecReviewGate, writeSpecReviewArtifact, } from './dod-gates.js';
|
|
17
17
|
import { checkLifecycleEvidenceTransitionGate } from './evidence-gate.js';
|
|
18
18
|
import { buildStatusResponse, buildValidateBlockedResponse, buildDryRunResponse, } from './response-builder.js';
|
|
19
|
-
import { getSuggestedMode } from './mode-hints.js';
|
|
20
|
-
import { autoFillActuals, createDefaultActuals } from '../../engine/actuals-estimator.js';
|
|
21
19
|
import { recordDoneMetrics, syncSpecFiles, tryReconcile, recordTerminalTransitionEvent, } from './file-sync.js';
|
|
22
20
|
import { appendEntry, getLastHash } from '../../storage/audit-trail-store.js';
|
|
23
21
|
import { appendTransitionEvent } from '../../storage/transition-log.js';
|
|
@@ -45,23 +43,12 @@ import { validateStrictLayoutOrError } from '../validate-helpers.js';
|
|
|
45
43
|
* (e.g. draft ā approved) without receiving an error.
|
|
46
44
|
* Returns the list of states executed, or an empty array if none were needed.
|
|
47
45
|
*/
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const stepsExecuted = [];
|
|
55
|
-
for (const intermediateStatus of steps) {
|
|
56
|
-
// SPEC-720: route through transitionSpec ā single entry point for status writes
|
|
57
|
-
await transitionSpec(projectId, specId, intermediateStatus, {
|
|
58
|
-
trigger: 'auto-advance',
|
|
59
|
-
actor: 'system',
|
|
60
|
-
});
|
|
61
|
-
stepsExecuted.push(intermediateStatus);
|
|
62
|
-
updatedCurrentStatus = intermediateStatus;
|
|
63
|
-
}
|
|
64
|
-
return { updatedCurrentStatus, stepsExecuted };
|
|
46
|
+
function buildAutoAdvancePlan(currentStatus, targetStatus) {
|
|
47
|
+
const stepsExecuted = resolveAutoAdvanceSteps(currentStatus, targetStatus) ?? [];
|
|
48
|
+
return {
|
|
49
|
+
plannedStatuses: [...stepsExecuted, targetStatus],
|
|
50
|
+
stepsExecuted,
|
|
51
|
+
};
|
|
65
52
|
}
|
|
66
53
|
/**
|
|
67
54
|
* SPEC-301: Check if spec is locked by another agent.
|
|
@@ -117,18 +104,18 @@ function computeDoneMetrics(newStatus, spec, actuals) {
|
|
|
117
104
|
if (newStatus !== 'done' || actuals === undefined || spec === null) {
|
|
118
105
|
return null;
|
|
119
106
|
}
|
|
107
|
+
const actualHours = actuals.devHours + actuals.reviewHours;
|
|
108
|
+
if (actualHours === 0) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
120
111
|
return {
|
|
121
|
-
accuracyScore: calculateAccuracy(spec.estimation,
|
|
112
|
+
accuracyScore: calculateAccuracy(spec.estimation, actualHours),
|
|
122
113
|
estimatedHours: spec.estimation.devHours + spec.estimation.reviewHours,
|
|
123
|
-
actualHours
|
|
114
|
+
actualHours,
|
|
124
115
|
estimatedCost: spec.estimation.totalCostUsd,
|
|
125
116
|
actualCost: actuals.totalCostUsd,
|
|
126
117
|
};
|
|
127
118
|
}
|
|
128
|
-
/** Extract recordActualHint from doneActions without adding cyclomatic complexity inline. */
|
|
129
|
-
function extractRecordActualHint(doneActions) {
|
|
130
|
-
return doneActions?.recordActualHint;
|
|
131
|
-
}
|
|
132
119
|
/** SPEC-447: Build a blocked-by-compliance response. */
|
|
133
120
|
function buildBlockedByComplianceResponse(_specId, result) {
|
|
134
121
|
const scoreStr = result.score !== null ? String(result.score) : 'N/A';
|
|
@@ -269,6 +256,22 @@ export async function handleUpdateStatus(params, server) {
|
|
|
269
256
|
};
|
|
270
257
|
}
|
|
271
258
|
const originalStatus = spec.status;
|
|
259
|
+
if (newStatus === 'done' && actuals === undefined) {
|
|
260
|
+
return {
|
|
261
|
+
content: [
|
|
262
|
+
{
|
|
263
|
+
type: 'text',
|
|
264
|
+
text: 'actuals_required: provide measured values for done. Use zero only when a metric is explicitly unavailable; Planu does not estimate provider tokens or costs.',
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
isError: true,
|
|
268
|
+
structuredContent: {
|
|
269
|
+
error: 'actuals_required',
|
|
270
|
+
code: 422,
|
|
271
|
+
fixHint: 'Provide actuals with measured values or zero for unavailable metrics. Values are preserved without estimation.',
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
272
275
|
// SPEC-301: Reject status changes when spec is locked by another holder (legacy manual lock)
|
|
273
276
|
const lockError = await checkSpecLockGate(params.projectPath, specId);
|
|
274
277
|
if (lockError) {
|
|
@@ -321,19 +324,28 @@ export async function handleUpdateStatus(params, server) {
|
|
|
321
324
|
}
|
|
322
325
|
}
|
|
323
326
|
try {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
327
|
+
if (spec.status === newStatus) {
|
|
328
|
+
const idempotentResult = checkIdempotentOrTransition(specId, spec.status, newStatus);
|
|
329
|
+
if (idempotentResult) {
|
|
330
|
+
return idempotentResult;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// SPEC-280/SPEC-1122: plan every intermediate state in memory. No status is
|
|
334
|
+
// persisted until all gates for the complete path have passed.
|
|
335
|
+
const { plannedStatuses, stepsExecuted } = buildAutoAdvancePlan(spec.status, newStatus);
|
|
336
|
+
let plannedFromStatus = spec.status;
|
|
337
|
+
for (const plannedStatus of plannedStatuses) {
|
|
338
|
+
const transitionError = checkTransition(plannedFromStatus, plannedStatus);
|
|
339
|
+
if (transitionError) {
|
|
340
|
+
return transitionError;
|
|
341
|
+
}
|
|
342
|
+
plannedFromStatus = plannedStatus;
|
|
331
343
|
}
|
|
332
344
|
// SPEC-733: Detect reverse transition and validate mandatory reason
|
|
333
|
-
const reverseTransition = isReverseTransition(
|
|
345
|
+
const reverseTransition = isReverseTransition(spec.status, newStatus);
|
|
334
346
|
if (reverseTransition) {
|
|
335
347
|
const reverseValidation = validateReverseTransition({
|
|
336
|
-
from:
|
|
348
|
+
from: spec.status,
|
|
337
349
|
to: newStatus,
|
|
338
350
|
reason: params.reason,
|
|
339
351
|
});
|
|
@@ -359,33 +371,35 @@ export async function handleUpdateStatus(params, server) {
|
|
|
359
371
|
}
|
|
360
372
|
}
|
|
361
373
|
// Gate: approval policy must be satisfied before transitioning to 'approved'
|
|
362
|
-
const
|
|
374
|
+
const approvalGateStatus = plannedStatuses.includes('approved') ? 'approved' : newStatus;
|
|
375
|
+
const approvalError = await checkApprovalPolicyGate(projectId, specId, approvalGateStatus);
|
|
363
376
|
if (approvalError) {
|
|
364
377
|
return approvalError;
|
|
365
378
|
}
|
|
366
379
|
// Gate: DoR must pass before transitioning to 'implementing'
|
|
367
|
-
const
|
|
380
|
+
const dorGateStatus = plannedStatuses.includes('implementing') ? 'implementing' : newStatus;
|
|
381
|
+
const dorError = checkDorGate(spec, specId, projectId, dorGateStatus);
|
|
368
382
|
if (dorError) {
|
|
369
383
|
return dorError;
|
|
370
384
|
}
|
|
371
385
|
// SPEC-716/SPEC-780: Format gate ā block 'approved' unless forceApprove bypasses with warnings
|
|
372
|
-
const formatGate = await checkApprovedFormatGate(spec,
|
|
386
|
+
const formatGate = await checkApprovedFormatGate(spec, approvalGateStatus, params.forceApprove);
|
|
373
387
|
if (formatGate.blockResult) {
|
|
374
388
|
return formatGate.blockResult;
|
|
375
389
|
}
|
|
376
390
|
// SPEC-632: Ambiguity gate ā block 'approved' if score < 70
|
|
377
|
-
const ambiguityError = await checkAmbiguityGate(spec,
|
|
391
|
+
const ambiguityError = await checkAmbiguityGate(spec, approvalGateStatus);
|
|
378
392
|
if (ambiguityError) {
|
|
379
393
|
return ambiguityError;
|
|
380
394
|
}
|
|
381
395
|
// SPEC-769: Readiness gate ā block 'approved' if spec has 0 criteria or score < 70
|
|
382
|
-
const readinessGate = await checkReadinessGate(spec,
|
|
396
|
+
const readinessGate = await checkReadinessGate(spec, approvalGateStatus, params.forceApprove);
|
|
383
397
|
if (readinessGate.blockResult) {
|
|
384
398
|
return readinessGate.blockResult;
|
|
385
399
|
}
|
|
386
400
|
// SPEC-728: DepGuard ā block 'approved' if spec participates in a dependency cycle
|
|
387
401
|
let depGuardResult = null;
|
|
388
|
-
if (
|
|
402
|
+
if (plannedStatuses.includes('approved')) {
|
|
389
403
|
const allSpecsForDepGuard = await specStore.listSpecs(projectId);
|
|
390
404
|
depGuardResult = checkApprovedDepGate(spec, allSpecsForDepGuard);
|
|
391
405
|
if (depGuardResult.blocked) {
|
|
@@ -403,17 +417,12 @@ export async function handleUpdateStatus(params, server) {
|
|
|
403
417
|
}
|
|
404
418
|
}
|
|
405
419
|
// SPEC-964: Challenge gate ā block 'review' if challenge_spec was never run
|
|
406
|
-
const
|
|
420
|
+
const challengeGateStatus = plannedStatuses.includes('review') ? 'review' : newStatus;
|
|
421
|
+
const challengeGate = checkChallengeGate(spec, challengeGateStatus);
|
|
407
422
|
if (challengeGate) {
|
|
408
423
|
return challengeGate;
|
|
409
424
|
}
|
|
410
|
-
if (
|
|
411
|
-
const specReviewWriteError = await writeSpecReviewArtifact(spec, specId, projectId);
|
|
412
|
-
if (specReviewWriteError) {
|
|
413
|
-
return specReviewWriteError;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
if (newStatus === 'approved') {
|
|
425
|
+
if (plannedStatuses.includes('approved')) {
|
|
417
426
|
const specReviewError = await checkSpecReviewGate(specId, projectId, params.forceApprove);
|
|
418
427
|
if (specReviewError) {
|
|
419
428
|
return specReviewError;
|
|
@@ -429,34 +438,45 @@ export async function handleUpdateStatus(params, server) {
|
|
|
429
438
|
// Resolve effective project path once ā used across all gates below
|
|
430
439
|
const effectiveGatePath = knowledge?.projectPath ?? params.projectPath;
|
|
431
440
|
// SPEC-1044: SDD model-routing + context continuity hard gate.
|
|
432
|
-
const sddRoutingGate =
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
441
|
+
const sddRoutingGate = {
|
|
442
|
+
blockResult: null,
|
|
443
|
+
gateResults: { sddModelRouting: 'skip' },
|
|
444
|
+
forcedReasons: [],
|
|
445
|
+
};
|
|
446
|
+
if (!shouldSkipSddRoutingGateForLegacyTestHarness()) {
|
|
447
|
+
const routedStatuses = plannedStatuses.filter((status) => status === 'approved' || status === 'implementing' || status === 'done');
|
|
448
|
+
for (const status of routedStatuses) {
|
|
449
|
+
const gate = await checkSddModelRoutingGate({
|
|
450
|
+
params,
|
|
451
|
+
status,
|
|
452
|
+
projectPath: effectiveGatePath,
|
|
453
|
+
});
|
|
454
|
+
if (gate.blockResult) {
|
|
455
|
+
return gate.blockResult;
|
|
456
|
+
}
|
|
457
|
+
Object.assign(sddRoutingGate.gateResults, gate.gateResults);
|
|
458
|
+
sddRoutingGate.forcedReasons.push(...gate.forcedReasons);
|
|
459
|
+
}
|
|
460
|
+
if (sddRoutingGate.forcedReasons.length > 0) {
|
|
461
|
+
sddRoutingGate.gateResults.sddModelRouting = 'forced';
|
|
437
462
|
}
|
|
438
|
-
: await checkSddModelRoutingGate({
|
|
439
|
-
params,
|
|
440
|
-
status: newStatus,
|
|
441
|
-
projectPath: effectiveGatePath,
|
|
442
|
-
});
|
|
443
|
-
if (sddRoutingGate.blockResult) {
|
|
444
|
-
return sddRoutingGate.blockResult;
|
|
445
463
|
}
|
|
446
464
|
// SPEC-1054: BDD/SDD evidence gates. Non-trivial specs must carry
|
|
447
465
|
// Discovery before approval, task-plan before implementation, and
|
|
448
466
|
// traceability/contract evidence before done.
|
|
449
|
-
if (!shouldSkipEvidenceGateForLegacyTestHarness()
|
|
450
|
-
(
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
467
|
+
if (!shouldSkipEvidenceGateForLegacyTestHarness()) {
|
|
468
|
+
const evidenceStatuses = plannedStatuses.filter((status) => status === 'approved' || status === 'implementing' || status === 'done');
|
|
469
|
+
for (const transition of evidenceStatuses) {
|
|
470
|
+
const evidenceGate = await checkLifecycleEvidenceTransitionGate({
|
|
471
|
+
spec,
|
|
472
|
+
specId,
|
|
473
|
+
projectId,
|
|
474
|
+
projectPath: effectiveGatePath,
|
|
475
|
+
transition,
|
|
476
|
+
});
|
|
477
|
+
if (evidenceGate !== null) {
|
|
478
|
+
return evidenceGate;
|
|
479
|
+
}
|
|
460
480
|
}
|
|
461
481
|
}
|
|
462
482
|
// ---------------------------------------------------------------------------
|
|
@@ -477,7 +497,7 @@ export async function handleUpdateStatus(params, server) {
|
|
|
477
497
|
let forcedBypassWarning = null;
|
|
478
498
|
const [codeRealityResult, doneGatesResult] = await Promise.all([
|
|
479
499
|
// Code reality: only relevant for 'implementing'
|
|
480
|
-
newStatus === 'implementing' && effectiveGatePath
|
|
500
|
+
newStatus === 'implementing' && !reverseTransition && effectiveGatePath
|
|
481
501
|
? checkCodeReality(effectiveGatePath, spec.title).catch(() => null)
|
|
482
502
|
: Promise.resolve(null),
|
|
483
503
|
// Done gates: only relevant for 'done'
|
|
@@ -508,21 +528,37 @@ export async function handleUpdateStatus(params, server) {
|
|
|
508
528
|
let crashShieldWarning = null;
|
|
509
529
|
let crashShieldSkipReason = null;
|
|
510
530
|
let complianceGateResult = null;
|
|
531
|
+
let validationReportGate = null;
|
|
532
|
+
if (newStatus === 'done' && !(params.force ?? params.forceStatus ?? false)) {
|
|
533
|
+
validationReportGate = await readApprovedValidationReportGate(specId, projectId, false);
|
|
534
|
+
if (!validationReportGate.ok) {
|
|
535
|
+
return validationReportGate.error;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
511
538
|
// SPEC-628: Rate-limit crash scan ā check before entering the parallel batch
|
|
512
539
|
crashShieldSkipReason = await checkCrashScanRateLimit(newStatus, effectiveGatePath ?? null);
|
|
540
|
+
const approvedValidationReportScore = validationReportGate?.ok === true && validationReportGate.score === 100
|
|
541
|
+
? validationReportGate.score
|
|
542
|
+
: null;
|
|
513
543
|
const [validateGateResult, crashRisksReport, complianceResult] = await Promise.all([
|
|
514
544
|
// Validate: only on 'done'.
|
|
515
545
|
// SPEC-721: timeout lives inside runValidateGate (Promise.race) ā do NOT wrap with
|
|
516
546
|
// withToolTimeout here, which would silently convert timeout into blocked:false (fail-open).
|
|
517
547
|
newStatus === 'done'
|
|
518
|
-
?
|
|
548
|
+
? approvedValidationReportScore !== null
|
|
549
|
+
? Promise.resolve({
|
|
550
|
+
blocked: false,
|
|
551
|
+
score: approvedValidationReportScore,
|
|
552
|
+
forced: false,
|
|
553
|
+
})
|
|
554
|
+
: runValidateGate(spec, effectiveGatePath ?? '', params.forceStatus ?? false, params.forceStatusReason, 9_000, { projectId, specId })
|
|
519
555
|
: Promise.resolve(null),
|
|
520
556
|
// Crash shield: only on 'done', skipped if rate-limited (SPEC-628)
|
|
521
557
|
newStatus === 'done' && effectiveGatePath && !crashShieldSkipReason
|
|
522
558
|
? withToolTimeout(scanCrashRisks(effectiveGatePath).catch(() => null), 9_000, null)
|
|
523
559
|
: Promise.resolve(null),
|
|
524
|
-
//
|
|
525
|
-
|
|
560
|
+
// The heuristic scorer is a review aid. Done relies on the authoritative validate report.
|
|
561
|
+
plannedStatuses.includes('review')
|
|
526
562
|
? withToolTimeout(checkComplianceGate(specId, projectId, effectiveGatePath), 9_000, {
|
|
527
563
|
skipped: true,
|
|
528
564
|
blocked: false,
|
|
@@ -546,10 +582,13 @@ export async function handleUpdateStatus(params, server) {
|
|
|
546
582
|
};
|
|
547
583
|
}
|
|
548
584
|
else {
|
|
549
|
-
validateScoreSource =
|
|
585
|
+
validateScoreSource =
|
|
586
|
+
approvedValidationReportScore !== null ? 'validation-report' : 'validateSpec';
|
|
550
587
|
}
|
|
551
588
|
}
|
|
552
|
-
if (newStatus === 'done' &&
|
|
589
|
+
if (newStatus === 'done' &&
|
|
590
|
+
!(params.force ?? params.forceStatus ?? false) &&
|
|
591
|
+
validationReportGate === null) {
|
|
553
592
|
const validationReportError = await checkValidationReportGate(specId, projectId, false);
|
|
554
593
|
if (validationReportError) {
|
|
555
594
|
return validationReportError;
|
|
@@ -586,14 +625,14 @@ export async function handleUpdateStatus(params, server) {
|
|
|
586
625
|
// Process compliance gate result ā may block on 'review'
|
|
587
626
|
if (complianceResult !== null) {
|
|
588
627
|
complianceGateResult = complianceResult;
|
|
589
|
-
if (
|
|
628
|
+
if (plannedStatuses.includes('review') && complianceResult.blocked) {
|
|
590
629
|
return buildBlockedByComplianceResponse(specId, complianceResult);
|
|
591
630
|
}
|
|
592
631
|
}
|
|
593
632
|
// SPEC-190: Run all compliance gates in parallel (convention + constitution + compile + lint + test, non-blocking)
|
|
594
633
|
// runComplianceGates internally skips heavy commands (compile/lint/test) for non-done transitions,
|
|
595
634
|
// but 'approved' still needs constitutionWarnings ā so run for 'done' and 'approved' only.
|
|
596
|
-
const { conventionWarnings, constitutionWarnings, compileWarnings, lintWarnings, testWarnings, } = newStatus === 'done' ||
|
|
635
|
+
const { conventionWarnings, constitutionWarnings, compileWarnings, lintWarnings, testWarnings, } = newStatus === 'done' || plannedStatuses.includes('approved')
|
|
597
636
|
? await runComplianceGates(projectId, spec.title, spec.tags, newStatus)
|
|
598
637
|
: {
|
|
599
638
|
conventionWarnings: [],
|
|
@@ -631,25 +670,25 @@ export async function handleUpdateStatus(params, server) {
|
|
|
631
670
|
blockingReasons: [],
|
|
632
671
|
});
|
|
633
672
|
}
|
|
634
|
-
//
|
|
635
|
-
|
|
636
|
-
if (newStatus === '
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
}
|
|
641
|
-
else {
|
|
642
|
-
// Agent passed actuals but tokens/costs are zero ā auto-fill
|
|
643
|
-
resolvedActuals = autoFillActuals(resolvedActuals, spec.estimation);
|
|
673
|
+
// Entering review creates reviewer evidence only after every lifecycle gate has passed.
|
|
674
|
+
// Auto-advance to approval must consume pre-existing approved reviewer evidence.
|
|
675
|
+
if (newStatus === 'review') {
|
|
676
|
+
const specReviewWriteError = await writeSpecReviewArtifact(spec, specId, projectId);
|
|
677
|
+
if (specReviewWriteError) {
|
|
678
|
+
return specReviewWriteError;
|
|
644
679
|
}
|
|
645
680
|
}
|
|
681
|
+
// Explicit actuals are preserved verbatim. Provider usage and cost are never inferred.
|
|
682
|
+
const resolvedActuals = actuals;
|
|
646
683
|
// Run transition-specific actions
|
|
647
684
|
const implActions = newStatus === 'implementing' ? await runImplementingActions(projectId, specId) : null;
|
|
648
685
|
const doneActions = newStatus === 'done' ? await runDoneActions(projectId, specId, spec.gitBranch) : null;
|
|
649
686
|
// SPEC-720: Step 1 ā Transition the status through the single authorised path
|
|
650
687
|
const trigger = reverseTransition
|
|
651
688
|
? 'reopen'
|
|
652
|
-
:
|
|
689
|
+
: stepsExecuted.length > 0
|
|
690
|
+
? 'auto-advance'
|
|
691
|
+
: (params.trigger ?? 'user');
|
|
653
692
|
const actor = params.actor ?? 'system';
|
|
654
693
|
const viaSync = params.viaSync ?? false;
|
|
655
694
|
await transitionSpec(projectId, specId, newStatus, {
|
|
@@ -670,7 +709,7 @@ export async function handleUpdateStatus(params, server) {
|
|
|
670
709
|
projectId,
|
|
671
710
|
specId,
|
|
672
711
|
eventType: 'transition',
|
|
673
|
-
from:
|
|
712
|
+
from: originalStatus,
|
|
674
713
|
to: newStatus,
|
|
675
714
|
actor,
|
|
676
715
|
reason: params.reason,
|
|
@@ -687,7 +726,7 @@ export async function handleUpdateStatus(params, server) {
|
|
|
687
726
|
projectId,
|
|
688
727
|
specId,
|
|
689
728
|
eventType: 'reopen',
|
|
690
|
-
from:
|
|
729
|
+
from: originalStatus,
|
|
691
730
|
to: newStatus,
|
|
692
731
|
actor,
|
|
693
732
|
reason: params.reason,
|
|
@@ -1030,7 +1069,6 @@ export async function handleUpdateStatus(params, server) {
|
|
|
1030
1069
|
if (cascadeResult.affectedSpecs.length > 0) {
|
|
1031
1070
|
collector.pushOk('cascade', `Notified ${String(cascadeResult.affectedSpecs.length)} downstream spec(s)`);
|
|
1032
1071
|
}
|
|
1033
|
-
const modeSuggestion = getSuggestedMode(newStatus);
|
|
1034
1072
|
const result = compactObj({
|
|
1035
1073
|
specId,
|
|
1036
1074
|
projectId,
|
|
@@ -1068,8 +1106,6 @@ export async function handleUpdateStatus(params, server) {
|
|
|
1068
1106
|
metrics: doneMetrics,
|
|
1069
1107
|
complianceGateResult,
|
|
1070
1108
|
versionSnapshotTag,
|
|
1071
|
-
suggestedClaudeMode: modeSuggestion?.mode ?? null,
|
|
1072
|
-
modeHint: modeSuggestion?.hint ?? null,
|
|
1073
1109
|
orchestrationPlan,
|
|
1074
1110
|
// SPEC-728: DepGuard result (null when gate was not applicable)
|
|
1075
1111
|
depGuard: depGuardResult,
|
|
@@ -1105,7 +1141,7 @@ export async function handleUpdateStatus(params, server) {
|
|
|
1105
1141
|
const reconciliationMarkdown = await tryReconcile(newStatus, specId, projectId);
|
|
1106
1142
|
// SPEC-754: Append shell-hygiene reminder when transitioning to done on claude-code host
|
|
1107
1143
|
const shellHygieneHint = newStatus === 'done' && detectHost() === 'claude-code' ? shellHygieneReminder() : null;
|
|
1108
|
-
return buildStatusResponse(result, specId,
|
|
1144
|
+
return buildStatusResponse(result, specId, originalStatus, newStatus, reconciliationMarkdown, spec.title, shellHygieneHint);
|
|
1109
1145
|
}
|
|
1110
1146
|
finally {
|
|
1111
1147
|
// SPEC-719: Release cross-process lock in all exit paths (return, throw)
|
|
@@ -9,6 +9,9 @@ export async function checkQaGate(spec, projectPath, force) {
|
|
|
9
9
|
if (state?.lastResult?.passed === true) {
|
|
10
10
|
return null;
|
|
11
11
|
}
|
|
12
|
+
if (await hasPassedExternalQaEvidence(spec, projectPath)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
12
15
|
let effectiveResult = state?.lastResult ?? null;
|
|
13
16
|
if (effectiveResult === null) {
|
|
14
17
|
const { runQaGate } = await import('../../engine/qa-gate.js');
|
|
@@ -47,4 +50,27 @@ export async function checkQaGate(spec, projectPath, force) {
|
|
|
47
50
|
},
|
|
48
51
|
};
|
|
49
52
|
}
|
|
53
|
+
async function hasPassedExternalQaEvidence(spec, projectPath) {
|
|
54
|
+
try {
|
|
55
|
+
const { readEvidenceArtifacts } = await import('../../engine/evidence-gates/artifact-reader.js');
|
|
56
|
+
const artifacts = await readEvidenceArtifacts({
|
|
57
|
+
spec,
|
|
58
|
+
projectId: spec.projectId,
|
|
59
|
+
specId: spec.id,
|
|
60
|
+
projectPath,
|
|
61
|
+
});
|
|
62
|
+
const rows = artifacts.traceabilityMatrix?.rows ?? [];
|
|
63
|
+
return rows.length > 0 && rows.every((row) => isPassedQaEvidence(row.validationEvidence));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function isPassedQaEvidence(value) {
|
|
70
|
+
if (!value) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return (/\b(?:pnpm\s+validate|pnpm\s+test|pnpm\s+typecheck)\b[\s\S]{0,120}\bpass(?:ed|ing)?\b/i.test(value) ||
|
|
74
|
+
/\bpass(?:ed|ing)?\b[\s\S]{0,120}\b(?:pnpm\s+validate|pnpm\s+test|pnpm\s+typecheck)\b/i.test(value));
|
|
75
|
+
}
|
|
50
76
|
//# sourceMappingURL=qa-gate.js.map
|
|
@@ -13,5 +13,5 @@ export declare function buildValidateBlockedResponse(specId: string, result: Ext
|
|
|
13
13
|
*/
|
|
14
14
|
export declare function buildDryRunResponse(dryRunResult: DryRunResult): ToolResult;
|
|
15
15
|
export declare const STATUS_EMOJIS: Record<SpecStatus, string>;
|
|
16
|
-
export declare function buildStatusResponse(result: Record<string, unknown>, specId: string, currentStatus: SpecStatus, newStatus: SpecStatus, reconciliationMarkdown?: string | null,
|
|
16
|
+
export declare function buildStatusResponse(result: Record<string, unknown>, specId: string, currentStatus: SpecStatus, newStatus: SpecStatus, reconciliationMarkdown?: string | null, specTitle?: string, shellHygieneHint?: string | null): ToolResult;
|
|
17
17
|
//# sourceMappingURL=response-builder.d.ts.map
|
|
@@ -79,7 +79,7 @@ export const STATUS_EMOJIS = {
|
|
|
79
79
|
discarded: 'šļø',
|
|
80
80
|
};
|
|
81
81
|
// eslint-disable-next-line complexity -- aggregates many host- and status-specific branches
|
|
82
|
-
export function buildStatusResponse(result, specId, currentStatus, newStatus, reconciliationMarkdown,
|
|
82
|
+
export function buildStatusResponse(result, specId, currentStatus, newStatus, reconciliationMarkdown, specTitle, shellHygieneHint) {
|
|
83
83
|
const fromEmoji = STATUS_EMOJIS[currentStatus];
|
|
84
84
|
const toEmoji = STATUS_EMOJIS[newStatus];
|
|
85
85
|
const r = result;
|
|
@@ -102,18 +102,15 @@ export function buildStatusResponse(result, specId, currentStatus, newStatus, re
|
|
|
102
102
|
lines.push(`\nš ${r.prSuggestion.title}`);
|
|
103
103
|
}
|
|
104
104
|
if (r.metrics) {
|
|
105
|
-
lines.push(`\nš Accuracy: ${Math.round(r.metrics.accuracyScore
|
|
105
|
+
lines.push(`\nš Accuracy: ${Math.round(r.metrics.accuracyScore)}% | ` +
|
|
106
106
|
`Estimated: ${String(r.metrics.estimatedHours)}h ā Actual: ${String(r.metrics.actualHours)}h`);
|
|
107
107
|
}
|
|
108
108
|
if (r.actuals && (r.actuals.tokensOpus > 0 || r.actuals.tokensSonnet > 0)) {
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const sonnet = r.actuals.tokensSonnet > 0
|
|
112
|
-
? `${prefix}${r.actuals.tokensSonnet.toLocaleString()} sonnet`
|
|
113
|
-
: null;
|
|
109
|
+
const opus = r.actuals.tokensOpus > 0 ? `${r.actuals.tokensOpus.toLocaleString()} opus` : null;
|
|
110
|
+
const sonnet = r.actuals.tokensSonnet > 0 ? `${r.actuals.tokensSonnet.toLocaleString()} sonnet` : null;
|
|
114
111
|
const tokenStr = [opus, sonnet].filter(Boolean).join(' + ');
|
|
115
|
-
const costStr = r.actuals.apiCostUsd > 0 ? ` |
|
|
116
|
-
lines.push(`\nš¤ Tokens: ${tokenStr}${costStr}
|
|
112
|
+
const costStr = r.actuals.apiCostUsd > 0 ? ` | $${r.actuals.apiCostUsd.toFixed(3)} API` : '';
|
|
113
|
+
lines.push(`\nš¤ Tokens: ${tokenStr}${costStr}`);
|
|
117
114
|
}
|
|
118
115
|
if (r.validateScore !== null && r.validateScore !== undefined) {
|
|
119
116
|
const sourceSuffix = r.validateScoreSource === 'forced-best-effort-validateSpec'
|
|
@@ -180,22 +177,17 @@ export function buildStatusResponse(result, specId, currentStatus, newStatus, re
|
|
|
180
177
|
const v = r.orchestrationPlan.parallelSafetyVerdict ? 'parallel-safe' : 'sequential-required';
|
|
181
178
|
lines.push(`\nš¤ Orchestration: ${String(r.orchestrationPlan.executionOrder.length)} waves | ${v}`);
|
|
182
179
|
}
|
|
183
|
-
// SPEC-494: Claude Code mode hint
|
|
184
|
-
if (r.suggestedClaudeMode) {
|
|
185
|
-
lines.push(`\nš” **Suggested Claude mode:** \`${r.suggestedClaudeMode}\` ā ${r.modeHint ?? ''}`);
|
|
186
|
-
}
|
|
187
|
-
if (recordActualHint) {
|
|
188
|
-
lines.push(`\nš” ${recordActualHint}`);
|
|
189
|
-
}
|
|
190
180
|
// SPEC-754: Shell hygiene reminder for claude-code host on done transition
|
|
191
181
|
if (shellHygieneHint) {
|
|
192
182
|
lines.push(`\nš **Shell hygiene:** ${shellHygieneHint}`);
|
|
193
183
|
}
|
|
194
184
|
// SPEC-340: nextAction and lifecycleMap
|
|
195
|
-
const nextAction = buildNextAction(newStatus);
|
|
196
185
|
const lifecycleMap = buildLifecycleMap(newStatus);
|
|
186
|
+
const nextAction = lifecycleMap.next === null ? null : buildNextAction(newStatus);
|
|
197
187
|
lines.push(buildLifecycleMapLine(lifecycleMap));
|
|
198
|
-
|
|
188
|
+
if (nextAction) {
|
|
189
|
+
lines.push(`\n**Next step:** ${nextAction.step}\n> ${nextAction.prompt}\n> \`${nextAction.suggestedCommand}\` ā ${nextAction.whenToDoIt}`);
|
|
190
|
+
}
|
|
199
191
|
const allNextSteps = [...(r.implementingSuggestions ?? [])];
|
|
200
192
|
const baseText = formatSuccess(t('tools.response.statusTitle'), lines.join(''));
|
|
201
193
|
const withNextSteps = allNextSteps.length > 0 ? addNextSteps(baseText, allNextSteps) : baseText;
|
|
@@ -284,7 +276,7 @@ export function buildStatusResponse(result, specId, currentStatus, newStatus, re
|
|
|
284
276
|
warningsTop5: warningItems.slice(0, 5),
|
|
285
277
|
affectedSpecsCount: r.cascade.affectedSpecs.length,
|
|
286
278
|
affectedSpecsTop5: r.cascade.affectedSpecs.slice(0, 5),
|
|
287
|
-
nextAction,
|
|
279
|
+
nextAction: nextAction ?? undefined,
|
|
288
280
|
lifecycleMap,
|
|
289
281
|
humanSummary,
|
|
290
282
|
orchestrationPlan: r.orchestrationPlan
|
|
@@ -73,10 +73,10 @@ export declare function checkAmbiguityGate(spec: Spec, newStatus: SpecStatus): P
|
|
|
73
73
|
export declare function checkReadinessGate(spec: Spec, newStatus: SpecStatus, forceApprove: boolean | undefined): Promise<ReadinessGateOutput>;
|
|
74
74
|
/**
|
|
75
75
|
* SPEC-964: Challenge gate ā block 'review' if challenge_spec was never run
|
|
76
|
-
* or
|
|
76
|
+
* or does not contain enough explicit resolution evidence.
|
|
77
77
|
*
|
|
78
78
|
* - Requires challengeReport to exist on the spec
|
|
79
|
-
* - Requires at least 3 scenarios
|
|
79
|
+
* - Requires at least 3 scenarios with explicit mitigation/risk-acceptance evidence
|
|
80
80
|
* - High-risk specs (high/critical) require all 5 focus areas
|
|
81
81
|
*/
|
|
82
82
|
export declare function checkChallengeGate(spec: Spec, newStatus: SpecStatus): ToolResult | null;
|
|
@@ -9,6 +9,7 @@ import { validateEnglishOnlySpecText } from '../../engine/spec-language/english-
|
|
|
9
9
|
import { checkGroundedSpecContract } from '../../engine/spec-grounding/contract.js';
|
|
10
10
|
import { checkGenericSpecOutput } from '../../engine/spec-quality/generic-output-gate.js';
|
|
11
11
|
import { formatKeyValue } from '../output-formatter.js';
|
|
12
|
+
import { getResolvedChallengeEvidence, MINIMUM_RESOLVED_CHALLENGES, } from '../challenge-spec/challenge-report.js';
|
|
12
13
|
/**
|
|
13
14
|
* Valid state transitions for spec lifecycle.
|
|
14
15
|
* draft -> review -> approved -> implementing -> done
|
|
@@ -115,7 +116,7 @@ export function checkTransition(currentStatus, newStatus) {
|
|
|
115
116
|
* Returns an error ToolResult if blocked, null if passed.
|
|
116
117
|
*/
|
|
117
118
|
export function checkDorGate(spec, specId, projectId, newStatus) {
|
|
118
|
-
if (newStatus !== 'implementing') {
|
|
119
|
+
if (newStatus !== 'implementing' || isReverseTransition(spec.status, newStatus)) {
|
|
119
120
|
return null;
|
|
120
121
|
}
|
|
121
122
|
const dorResult = validateDoR(spec);
|
|
@@ -432,10 +433,10 @@ export async function checkReadinessGate(spec, newStatus, forceApprove) {
|
|
|
432
433
|
}
|
|
433
434
|
/**
|
|
434
435
|
* SPEC-964: Challenge gate ā block 'review' if challenge_spec was never run
|
|
435
|
-
* or
|
|
436
|
+
* or does not contain enough explicit resolution evidence.
|
|
436
437
|
*
|
|
437
438
|
* - Requires challengeReport to exist on the spec
|
|
438
|
-
* - Requires at least 3 scenarios
|
|
439
|
+
* - Requires at least 3 scenarios with explicit mitigation/risk-acceptance evidence
|
|
439
440
|
* - High-risk specs (high/critical) require all 5 focus areas
|
|
440
441
|
*/
|
|
441
442
|
export function checkChallengeGate(spec, newStatus) {
|
|
@@ -459,21 +460,23 @@ export function checkChallengeGate(spec, newStatus) {
|
|
|
459
460
|
},
|
|
460
461
|
};
|
|
461
462
|
}
|
|
462
|
-
|
|
463
|
+
const resolutionEvidence = getResolvedChallengeEvidence(report);
|
|
464
|
+
const addressedCount = resolutionEvidence.length;
|
|
465
|
+
if (addressedCount < MINIMUM_RESOLVED_CHALLENGES) {
|
|
463
466
|
return {
|
|
464
467
|
content: [
|
|
465
468
|
{
|
|
466
469
|
type: 'text',
|
|
467
|
-
text: `Challenge gate blocked: only ${String(
|
|
470
|
+
text: `Challenge gate blocked: only ${String(addressedCount)} scenario(s) have explicit resolution evidence (minimum ${String(MINIMUM_RESOLVED_CHALLENGES)} required). Record mitigation or accepted-risk evidence for discovered findings before retrying.`,
|
|
468
471
|
},
|
|
469
472
|
],
|
|
470
473
|
isError: true,
|
|
471
474
|
structuredContent: {
|
|
472
475
|
error: 'CHALLENGE_GATE_BLOCKED',
|
|
473
476
|
code: 'INSUFFICIENT_CHALLENGES',
|
|
474
|
-
addressedCount
|
|
475
|
-
requiredCount:
|
|
476
|
-
fixHint: `
|
|
477
|
+
addressedCount,
|
|
478
|
+
requiredCount: MINIMUM_RESOLVED_CHALLENGES,
|
|
479
|
+
fixHint: `Resolve challenge_spec findings with concrete evidence, then retry update_status(specId="${spec.id}", status="review").`,
|
|
477
480
|
},
|
|
478
481
|
};
|
|
479
482
|
}
|