@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.
Files changed (155) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/config/elicitation-questions.json +5 -17
  3. package/dist/engine/cascade-hooks/registry.js +0 -2
  4. package/dist/engine/challenge-scenarios-loader.d.ts +3 -3
  5. package/dist/engine/compliance-injector.d.ts +3 -3
  6. package/dist/engine/dashboard/kanban-logic.d.ts +1 -1
  7. package/dist/engine/doc-generator/portal/risk-matrix-svg.d.ts +1 -1
  8. package/dist/engine/doc-generator/proposal/proposal-generator.d.ts +1 -1
  9. package/dist/engine/elicitation/answer-extractor.d.ts +1 -4
  10. package/dist/engine/elicitation/answer-extractor.js +197 -68
  11. package/dist/engine/elicitation/decision-gap-detector.js +59 -79
  12. package/dist/engine/elicitation/index.d.ts +0 -1
  13. package/dist/engine/elicitation/index.js +0 -1
  14. package/dist/engine/elicitation/question-generator.js +10 -3
  15. package/dist/engine/elicitation/question-grounding-gate.js +83 -34
  16. package/dist/engine/evidence-gates/lifecycle-gate.js +27 -14
  17. package/dist/engine/evidence-index/index-builder.js +7 -2
  18. package/dist/engine/frontmatter-sha/frozen-fields.d.ts +1 -1
  19. package/dist/engine/handoff-artifacts/schemas/red-tests-handoff.d.ts +2 -2
  20. package/dist/engine/handoff-artifacts/schemas.d.ts +22 -22
  21. package/dist/engine/human-summary.js +3 -3
  22. package/dist/engine/implementation-contract/common.d.ts +1 -1
  23. package/dist/engine/implementation-contract/renderer.js +17 -39
  24. package/dist/engine/keyword-matcher/stopwords.d.ts +2 -2
  25. package/dist/engine/lifecycle-hints.d.ts +1 -1
  26. package/dist/engine/lifecycle-hints.js +13 -29
  27. package/dist/engine/provider-adapters/adapters/claude.d.ts +1 -1
  28. package/dist/engine/provider-adapters/adapters/gemini.d.ts +1 -1
  29. package/dist/engine/provider-adapters/adapters/gpt4.d.ts +1 -1
  30. package/dist/engine/provider-adapters/adapters/markdown.d.ts +1 -1
  31. package/dist/engine/safety/atomic-write-file.d.ts +1 -1
  32. package/dist/engine/safety/cross-process-lock.d.ts +1 -1
  33. package/dist/engine/safety/path-sanitizer.d.ts +1 -1
  34. package/dist/engine/security-analyzer/profile-detector.js +48 -16
  35. package/dist/engine/spec-format/copula-blacklist.d.ts +2 -2
  36. package/dist/engine/spec-format/lean-spec-generator.d.ts +2 -2
  37. package/dist/engine/spec-format/lean-spec-generator.js +9 -10
  38. package/dist/engine/spec-format/metadata-value-policy.d.ts +20 -20
  39. package/dist/engine/spec-generator/fallback-generator.js +13 -20
  40. package/dist/engine/spec-grounding/contract.d.ts +5 -0
  41. package/dist/engine/spec-grounding/contract.js +10 -6
  42. package/dist/engine/spec-migrator/lean-migration.js +9 -16
  43. package/dist/engine/text-signal-boundaries.js +45 -12
  44. package/dist/engine/universal-rules/rules/planu-modes.js +0 -2
  45. package/dist/engine/web-fetcher/stack-advisor.d.ts +2 -2
  46. package/dist/engine/web-fetcher/stack-advisor.js +78 -17
  47. package/dist/engine/well-known/manifest-schema.d.ts +33 -33
  48. package/dist/engine/workers/schema.d.ts +3 -3
  49. package/dist/hosts/claude-code/ux/mcp-resources.d.ts +4 -4
  50. package/dist/hosts/codex/rest-shim.d.ts +1 -1
  51. package/dist/native/lightweight-command-catalog.d.ts +30 -30
  52. package/dist/server/openapi-spec.d.ts +132 -132
  53. package/dist/tools/challenge-spec/challenge-report.d.ts +13 -0
  54. package/dist/tools/challenge-spec/challenge-report.js +55 -0
  55. package/dist/tools/challenge-spec/event-challenge-scenarios.js +8 -27
  56. package/dist/tools/challenge-spec/resilience-challenge-scenarios-a.js +16 -32
  57. package/dist/tools/challenge-spec/resilience-challenge-scenarios-b.js +15 -20
  58. package/dist/tools/challenge-spec/scenario-collector.d.ts +13 -0
  59. package/dist/tools/challenge-spec/scenario-collector.js +86 -0
  60. package/dist/tools/challenge-spec/scenarios-data.js +10 -9
  61. package/dist/tools/challenge-spec/scenarios-failure.js +38 -54
  62. package/dist/tools/challenge-spec/scenarios-scale.js +14 -15
  63. package/dist/tools/challenge-spec/scenarios-security.d.ts +1 -1
  64. package/dist/tools/challenge-spec/scenarios-security.js +18 -11
  65. package/dist/tools/challenge-spec/scenarios-utils.d.ts +31 -5
  66. package/dist/tools/challenge-spec/scenarios-utils.js +102 -6
  67. package/dist/tools/challenge-spec/security-challenge-scenarios.d.ts +1 -1
  68. package/dist/tools/challenge-spec/security-challenge-scenarios.js +20 -25
  69. package/dist/tools/challenge-spec.js +46 -124
  70. package/dist/tools/clarify-requirements/multiple-choice.js +1 -1
  71. package/dist/tools/clarify-requirements/questions-context.js +11 -11
  72. package/dist/tools/clarify-requirements/questions.js +3 -23
  73. package/dist/tools/clarify-requirements.js +38 -0
  74. package/dist/tools/compliance-gate-handler.d.ts +1 -1
  75. package/dist/tools/create-spec/autopilot-analyzer.d.ts +1 -1
  76. package/dist/tools/create-spec/autopilot-analyzer.js +29 -54
  77. package/dist/tools/create-spec/post-creation.js +23 -4
  78. package/dist/tools/create-spec.js +51 -168
  79. package/dist/tools/delete-first/suggest-deletions.d.ts +1 -1
  80. package/dist/tools/elicit-requirements-handler.js +20 -12
  81. package/dist/tools/generate-batch-script.d.ts +2 -2
  82. package/dist/tools/init-project/rules-generator.js +0 -1
  83. package/dist/tools/oauth-handler.d.ts +1 -1
  84. package/dist/tools/register-sdd-tools.d.ts +1 -1
  85. package/dist/tools/register-spec-tools/core-spec-tools.js +11 -2
  86. package/dist/tools/render-spec-for-provider.d.ts +2 -2
  87. package/dist/tools/scan-orphan-spec-refs.d.ts +6 -6
  88. package/dist/tools/schemas/agents.d.ts +7 -7
  89. package/dist/tools/schemas/analysis.d.ts +11 -11
  90. package/dist/tools/schemas/delete.d.ts +5 -5
  91. package/dist/tools/schemas/github.d.ts +4 -4
  92. package/dist/tools/schemas/ide-config.d.ts +12 -12
  93. package/dist/tools/schemas/infra.d.ts +9 -9
  94. package/dist/tools/schemas/lifecycle.d.ts +50 -50
  95. package/dist/tools/schemas/llm-provider-schemas.d.ts +7 -7
  96. package/dist/tools/schemas/plugins-schemas.d.ts +12 -12
  97. package/dist/tools/schemas/project-graph.d.ts +6 -6
  98. package/dist/tools/schemas/rbac.d.ts +2 -2
  99. package/dist/tools/schemas/registry.d.ts +3 -3
  100. package/dist/tools/schemas/runtime-security.d.ts +2 -2
  101. package/dist/tools/schemas/session.d.ts +2 -2
  102. package/dist/tools/schemas/spec.d.ts +15 -15
  103. package/dist/tools/schemas/token-intelligence.d.ts +6 -6
  104. package/dist/tools/schemas/token-optimization.d.ts +2 -2
  105. package/dist/tools/schemas/workers-schema.d.ts +4 -4
  106. package/dist/tools/skill-bootstrap-handler.d.ts +7 -7
  107. package/dist/tools/suggest-stack.js +49 -13
  108. package/dist/tools/tool-registry/deprecated-stubs.d.ts +1 -1
  109. package/dist/tools/triage-request.d.ts +3 -3
  110. package/dist/tools/update-status/dod-gates.d.ts +8 -0
  111. package/dist/tools/update-status/dod-gates.js +68 -43
  112. package/dist/tools/update-status/file-sync.d.ts +1 -1
  113. package/dist/tools/update-status/file-sync.js +10 -45
  114. package/dist/tools/update-status/index.js +133 -97
  115. package/dist/tools/update-status/qa-gate.js +26 -0
  116. package/dist/tools/update-status/response-builder.d.ts +1 -1
  117. package/dist/tools/update-status/response-builder.js +11 -19
  118. package/dist/tools/update-status/transition-guard.d.ts +2 -2
  119. package/dist/tools/update-status/transition-guard.js +11 -8
  120. package/dist/tools/update-status-actions.d.ts +0 -1
  121. package/dist/tools/update-status-actions.js +10 -26
  122. package/dist/tools/update-status-convention-gate.d.ts +1 -6
  123. package/dist/tools/update-status-convention-gate.js +9 -33
  124. package/dist/tools/validate-lint.d.ts +1 -1
  125. package/dist/tools/validate-lint.js +39 -1
  126. package/dist/types/docs.d.ts +0 -4
  127. package/dist/types/env.d.ts +0 -4
  128. package/dist/types/estimation.d.ts +0 -2
  129. package/dist/types/multi-teammate-review.d.ts +4 -4
  130. package/dist/types/plugin-install.d.ts +7 -7
  131. package/dist/types/spec/core.d.ts +20 -0
  132. package/dist/types/spec/inputs.d.ts +1 -1
  133. package/dist/types/stack/index.d.ts +1 -1
  134. package/dist/types/stack/recommend.d.ts +10 -0
  135. package/package.json +21 -18
  136. package/planu-native.json +1 -1
  137. package/planu-plugin.json +1 -1
  138. package/src/i18n/messages/en.json +0 -72
  139. package/src/i18n/messages/es.json +0 -72
  140. package/src/i18n/messages/pt.json +0 -72
  141. package/dist/config/elicitation-dimensions.json +0 -51
  142. package/dist/engine/actuals-estimator.d.ts +0 -14
  143. package/dist/engine/actuals-estimator.js +0 -64
  144. package/dist/engine/cascade-hooks/hooks/living-specs.hook.d.ts +0 -3
  145. package/dist/engine/cascade-hooks/hooks/living-specs.hook.js +0 -19
  146. package/dist/engine/elicitation/non-interactive-defaults.d.ts +0 -9
  147. package/dist/engine/elicitation/non-interactive-defaults.js +0 -28
  148. package/dist/engine/elicitation/option-builder.d.ts +0 -9
  149. package/dist/engine/elicitation/option-builder.js +0 -85
  150. package/dist/engine/progress-writer.d.ts +0 -10
  151. package/dist/engine/progress-writer.js +0 -63
  152. package/dist/tools/clarify-requirements/interview-mode.d.ts +0 -42
  153. package/dist/tools/clarify-requirements/interview-mode.js +0 -342
  154. package/dist/tools/update-status/mode-hints.d.ts +0 -4
  155. 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
- async function runAutoAdvance(projectId, specId, currentStatus, targetStatus) {
49
- const steps = resolveAutoAdvanceSteps(currentStatus, targetStatus);
50
- if (!steps || steps.length === 0) {
51
- return { updatedCurrentStatus: currentStatus, stepsExecuted: [] };
52
- }
53
- let updatedCurrentStatus = currentStatus;
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, actuals.devHours + actuals.reviewHours),
112
+ accuracyScore: calculateAccuracy(spec.estimation, actualHours),
122
113
  estimatedHours: spec.estimation.devHours + spec.estimation.reviewHours,
123
- actualHours: actuals.devHours + actuals.reviewHours,
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
- // SPEC-280: auto-advance silently through intermediate states (e.g. draft → approved)
325
- const { updatedCurrentStatus, stepsExecuted } = await runAutoAdvance(projectId, specId, spec.status, newStatus);
326
- const currentStatus = updatedCurrentStatus;
327
- // Idempotent or invalid transition check — returns early if no change needed or blocked
328
- const transitionError = checkIdempotentOrTransition(specId, currentStatus, newStatus);
329
- if (transitionError) {
330
- return transitionError;
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(currentStatus, newStatus);
345
+ const reverseTransition = isReverseTransition(spec.status, newStatus);
334
346
  if (reverseTransition) {
335
347
  const reverseValidation = validateReverseTransition({
336
- from: currentStatus,
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 approvalError = await checkApprovalPolicyGate(projectId, specId, newStatus);
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 dorError = checkDorGate(spec, specId, projectId, newStatus);
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, newStatus, params.forceApprove);
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, newStatus);
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, newStatus, params.forceApprove);
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 (newStatus === 'approved') {
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 challengeGate = checkChallengeGate(spec, newStatus);
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 (newStatus === 'review') {
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 = shouldSkipSddRoutingGateForLegacyTestHarness()
433
- ? {
434
- blockResult: null,
435
- gateResults: { sddModelRouting: 'skip' },
436
- forcedReasons: [],
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
- (newStatus === 'approved' || newStatus === 'implementing' || newStatus === 'done')) {
451
- const evidenceGate = await checkLifecycleEvidenceTransitionGate({
452
- spec,
453
- specId,
454
- projectId,
455
- projectPath: effectiveGatePath,
456
- transition: newStatus,
457
- });
458
- if (evidenceGate !== null) {
459
- return evidenceGate;
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
- ? runValidateGate(spec, effectiveGatePath ?? '', params.forceStatus ?? false, params.forceStatusReason, 9_000, { projectId, specId })
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
- // Compliance gate: only on 'done' or 'implementing' (score-compliance is heavy) — hard deadline 9s
525
- newStatus === 'done' || newStatus === 'implementing' || newStatus === 'review'
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 = 'validateSpec';
585
+ validateScoreSource =
586
+ approvedValidationReportScore !== null ? 'validation-report' : 'validateSpec';
550
587
  }
551
588
  }
552
- if (newStatus === 'done' && !(params.force ?? params.forceStatus ?? false)) {
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 (newStatus === 'review' && complianceResult.blocked) {
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' || newStatus === 'approved'
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
- // SPEC-495: Auto-generate actuals when not provided; auto-fill zeros when provided
635
- let resolvedActuals = actuals;
636
- if (newStatus === 'done') {
637
- if (!resolvedActuals) {
638
- // Agent did not pass actuals at all — create from spec estimation
639
- resolvedActuals = createDefaultActuals(spec.estimation);
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
- : (params.trigger ?? 'user');
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: currentStatus,
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: currentStatus,
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, currentStatus, newStatus, reconciliationMarkdown, extractRecordActualHint(doneActions), spec.title, shellHygieneHint);
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, recordActualHint?: string | null, specTitle?: string, shellHygieneHint?: string | null): ToolResult;
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, recordActualHint, specTitle, shellHygieneHint) {
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 * 100)}% | ` +
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 prefix = r.actuals.estimated ? '~' : '';
110
- const opus = r.actuals.tokensOpus > 0 ? `${prefix}${r.actuals.tokensOpus.toLocaleString()} opus` : null;
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 ? ` | ${prefix}$${r.actuals.apiCostUsd.toFixed(3)} API` : '';
116
- lines.push(`\nšŸ¤– Tokens: ${tokenStr}${costStr}${r.actuals.estimated ? ' *(auto-estimated)*' : ''}`);
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
- lines.push(`\n**Next step:** ${nextAction.step}\n> ${nextAction.prompt}\n> \`${nextAction.suggestedCommand}\` — ${nextAction.whenToDoIt}`);
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 did not generate enough addressed scenarios.
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 addressed
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 did not generate enough addressed scenarios.
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 addressed
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
- if (report.addressedCount < 3) {
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(report.addressedCount)} scenario(s) addressed (minimum 3 required). Re-run challenge_spec(specId="${spec.id}") with broader focus areas.`,
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: report.addressedCount,
475
- requiredCount: 3,
476
- fixHint: `Run challenge_spec(specId="${spec.id}") to generate more failure scenarios.`,
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
  }
@@ -13,7 +13,6 @@ export declare function runDoneActions(projectId: string, specId: string, gitBra
13
13
  } | null;
14
14
  validationWarning: string | null;
15
15
  uncommittedWarning: string | null;
16
- recordActualHint: string | null;
17
16
  autopilotSummary: string[];
18
17
  }>;
19
18
  /**