@planu/cli 5.7.5 → 5.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +3 -3
  3. package/dist/.planu-build.json +1 -1
  4. package/dist/cli/commands/package-handoff.js +30 -5
  5. package/dist/config/compliance-profiles.json +36 -3
  6. package/dist/config/environment-schema.json +21 -0
  7. package/dist/config/hook-templates/planu-review-panel-trigger.sh +2 -3
  8. package/dist/config/hook-templates/planu-session-safeguard.sh +22 -15
  9. package/dist/config/official-sdd-tools.d.ts +1 -1
  10. package/dist/config/official-sdd-tools.js +11 -24
  11. package/dist/config/registries/hosts/codex.json +14 -30
  12. package/dist/config/registries/hosts/opencode.json +1 -6
  13. package/dist/config/server-instructions.js +0 -21
  14. package/dist/config/skill-templates/planu-context-assets.md +3 -6
  15. package/dist/config/skill-templates/planu-implement.md +2 -2
  16. package/dist/config/skill-templates/planu-multi-teammate-review.md +8 -12
  17. package/dist/config/skill-templates/planu-release.md +2 -7
  18. package/dist/config/skill-templates/planu-validate.md +3 -3
  19. package/dist/config/spec-templates/crud-rest-api/template.json +5 -1
  20. package/dist/config/spec-templates/file-upload-s3/template.json +5 -1
  21. package/dist/config/spec-templates/stripe-payments/template.json +5 -1
  22. package/dist/config/spec-templates/webhook-system/template.json +5 -1
  23. package/dist/config/subagent-templates/planu-challenger.md +4 -9
  24. package/dist/config/subagent-templates/planu-spec-implementer.md +2 -4
  25. package/dist/config/subagent-templates/planu-validator.md +5 -8
  26. package/dist/config/tool-groups.json +6 -144
  27. package/dist/engine/ambiguity-scorer.js +40 -1
  28. package/dist/engine/autopilot/bootstrap.js +1 -1
  29. package/dist/engine/code-impact-analyzer.js +2 -6
  30. package/dist/engine/context-orchestrator/index.js +2 -2
  31. package/dist/engine/convention-scanner/codebase-scanner.js +2 -2
  32. package/dist/engine/dogfooding/runtime-gap-detector.js +2 -2
  33. package/dist/engine/evidence-gates/artifact-reader.d.ts +1 -0
  34. package/dist/engine/evidence-gates/artifact-reader.js +1 -1
  35. package/dist/engine/evidence-gates/evidence-autofill.d.ts +13 -5
  36. package/dist/engine/evidence-gates/evidence-autofill.js +106 -43
  37. package/dist/engine/evidence-gates/evidence-skeletons.js +3 -1
  38. package/dist/engine/evidence-gates/lifecycle-gate.js +7 -4
  39. package/dist/engine/evidence-index/index-builder.js +21 -4
  40. package/dist/engine/execution/job-runtime.js +1 -1
  41. package/dist/engine/execution/package-handoff-job-executor.d.ts +22 -0
  42. package/dist/engine/execution/package-handoff-job-executor.js +490 -0
  43. package/dist/engine/execution/validate-job-executor.js +20 -8
  44. package/dist/engine/git/exec-git.d.ts +4 -0
  45. package/dist/engine/git/exec-git.js +14 -0
  46. package/dist/engine/handoff-artifacts/implementation-review-reader.js +15 -2
  47. package/dist/engine/handoff-artifacts/schemas.d.ts +2 -0
  48. package/dist/engine/handoff-artifacts/schemas.js +1 -0
  49. package/dist/engine/handoff-format.d.ts +36 -0
  50. package/dist/engine/handoff-format.js +314 -0
  51. package/dist/engine/handoff-packager.d.ts +5 -3
  52. package/dist/engine/handoff-packager.js +89 -32
  53. package/dist/engine/host-tool-filter.js +20 -49
  54. package/dist/engine/human-summary.js +14 -3
  55. package/dist/engine/implementation-contract/common.d.ts +8 -3
  56. package/dist/engine/implementation-contract/common.js +9 -6
  57. package/dist/engine/implementation-contract/evaluator.js +4 -6
  58. package/dist/engine/implementation-contract/renderer.js +18 -23
  59. package/dist/engine/project-health-checker.js +2 -2
  60. package/dist/engine/readiness-checker.js +43 -5
  61. package/dist/engine/reconcile/apply-changes.js +4 -0
  62. package/dist/engine/reconcile/propagate-mirrors.d.ts +3 -0
  63. package/dist/engine/reconcile/propagate-mirrors.js +311 -0
  64. package/dist/engine/self-healing/healer.js +12 -5
  65. package/dist/engine/self-healing/strategies/lint-fix.d.ts +1 -1
  66. package/dist/engine/self-healing/strategies/lint-fix.js +4 -1
  67. package/dist/engine/self-healing/strategies/test-fix.d.ts +4 -3
  68. package/dist/engine/self-healing/strategies/test-fix.js +32 -14
  69. package/dist/engine/self-healing/strategies/typescript-fix.d.ts +1 -1
  70. package/dist/engine/self-healing/strategies/typescript-fix.js +28 -10
  71. package/dist/engine/session/checkpoint-writer.js +8 -4
  72. package/dist/engine/session/session-tracker.d.ts +5 -16
  73. package/dist/engine/session/session-tracker.js +29 -26
  74. package/dist/engine/session-state/writer.js +3 -11
  75. package/dist/engine/skill-generator/workflow-skill-generator.d.ts +4 -2
  76. package/dist/engine/skill-generator/workflow-skill-generator.js +113 -38
  77. package/dist/engine/spec-format/bdd-parser.d.ts +1 -11
  78. package/dist/engine/spec-format/bdd-parser.js +40 -11
  79. package/dist/engine/spec-format/lean-spec-generator.js +32 -12
  80. package/dist/engine/spec-format/retired-scaffold.d.ts +3 -0
  81. package/dist/engine/spec-format/retired-scaffold.js +5 -0
  82. package/dist/engine/spec-format/technical-md-populator.d.ts +1 -0
  83. package/dist/engine/spec-format/technical-md-populator.js +27 -0
  84. package/dist/engine/spec-format/unified-spec-builder.d.ts +1 -0
  85. package/dist/engine/spec-format/unified-spec-builder.js +30 -20
  86. package/dist/engine/spec-grounding/contract.d.ts +3 -3
  87. package/dist/engine/spec-grounding/contract.js +65 -4
  88. package/dist/engine/spec-grounding/grounding-entry-parser.d.ts +5 -0
  89. package/dist/engine/spec-grounding/grounding-entry-parser.js +185 -0
  90. package/dist/engine/spec-quality/generic-output-gate.js +37 -3
  91. package/dist/engine/technical-enricher/index.js +4 -1
  92. package/dist/engine/type-safety-gate.js +3 -11
  93. package/dist/engine/universal-rules/catalog.js +0 -2
  94. package/dist/engine/universal-rules/rules/planu-workflow.js +1 -1
  95. package/dist/engine/validation/durable-validation.d.ts +3 -2
  96. package/dist/engine/validation/durable-validation.js +106 -40
  97. package/dist/engine/validation/validation-freshness.d.ts +3 -2
  98. package/dist/engine/validation/validation-freshness.js +22 -16
  99. package/dist/engine/validation/validation-worktree.js +34 -0
  100. package/dist/hosts/claude-code/ux/mcp-prompts.js +8 -8
  101. package/dist/index.js +3 -17
  102. package/dist/resources/process.js +38 -65
  103. package/dist/storage/session-state-store.js +2 -0
  104. package/dist/storage/spec-store.js +8 -1
  105. package/dist/tools/bump-spec-version.js +5 -5
  106. package/dist/tools/challenge-spec/scenarios-utils.js +2 -2
  107. package/dist/tools/configure-checkpoint-policy.js +3 -3
  108. package/dist/tools/create-spec-helpers.js +2 -5
  109. package/dist/tools/create-spec.js +21 -4
  110. package/dist/tools/facilitate.js +3 -6
  111. package/dist/tools/git/branch-ops.js +16 -4
  112. package/dist/tools/github-release-handler.js +1 -20
  113. package/dist/tools/init-project/agents-md-writer.js +6 -6
  114. package/dist/tools/init-project/claude-md-generator.d.ts +0 -1
  115. package/dist/tools/init-project/claude-md-generator.js +0 -35
  116. package/dist/tools/init-project/handler.js +7 -5
  117. package/dist/tools/init-project/per-client-files-writer.js +5 -5
  118. package/dist/tools/init-project/planu-workflow-generator.js +2 -13
  119. package/dist/tools/init-project/portable-index-reconciler.d.ts +2 -1
  120. package/dist/tools/init-project/portable-index-reconciler.js +7 -2
  121. package/dist/tools/init-project/rules-generator.d.ts +0 -11
  122. package/dist/tools/init-project/rules-generator.js +4 -85
  123. package/dist/tools/init-project/scaffold-writer.d.ts +0 -1
  124. package/dist/tools/init-project/scaffold-writer.js +1 -12
  125. package/dist/tools/init-project/skills-multi-teammate-review-writer.js +1 -1
  126. package/dist/tools/jobs/handlers.d.ts +0 -6
  127. package/dist/tools/jobs/handlers.js +13 -40
  128. package/dist/tools/list-specs.js +6 -21
  129. package/dist/tools/multi-teammate-review.js +1 -14
  130. package/dist/tools/package-handoff.d.ts +3 -2
  131. package/dist/tools/package-handoff.js +153 -427
  132. package/dist/tools/reconcile-session-safeguard-hook.js +22 -15
  133. package/dist/tools/register-platform-tools/design-stack-tools.js +3 -3
  134. package/dist/tools/register-sdd-tools.js +0 -23
  135. package/dist/tools/register-spec-tools/analysis-tools.js +22 -9
  136. package/dist/tools/register-spec-tools/core-spec-tools.js +115 -70
  137. package/dist/tools/schemas/index.d.ts +1 -1
  138. package/dist/tools/schemas/index.js +1 -1
  139. package/dist/tools/schemas/output-schemas.d.ts +1 -18
  140. package/dist/tools/schemas/output-schemas.js +1 -11
  141. package/dist/tools/schemas/package-handoff-output-schema.d.ts +40 -0
  142. package/dist/tools/schemas/package-handoff-output-schema.js +68 -0
  143. package/dist/tools/schemas/validate-output-schema.d.ts +1 -2
  144. package/dist/tools/schemas/validate-output-schema.js +3 -2
  145. package/dist/tools/session-checkpoint.js +2 -2
  146. package/dist/tools/skill-registry/index.d.ts +0 -1
  147. package/dist/tools/skill-registry/index.js +0 -1
  148. package/dist/tools/sync-spec-state-handler.d.ts +2 -2
  149. package/dist/tools/sync-spec-state-handler.js +23 -3
  150. package/dist/tools/tool-registry/core-tools.js +72 -164
  151. package/dist/tools/tool-registry/group-infra.js +2 -39
  152. package/dist/tools/tool-registry/group-quality-compliance.js +20 -15
  153. package/dist/tools/tool-registry-helpers.js +6 -2
  154. package/dist/tools/update-status/batch.js +17 -0
  155. package/dist/tools/update-status/dod-gates.js +14 -1
  156. package/dist/tools/update-status/done-receipt-verifier.d.ts +22 -0
  157. package/dist/tools/update-status/done-receipt-verifier.js +56 -26
  158. package/dist/tools/update-status/evidence-gate.js +10 -21
  159. package/dist/tools/update-status/file-sync.d.ts +1 -0
  160. package/dist/tools/update-status/file-sync.js +6 -0
  161. package/dist/tools/update-status/index.d.ts +6 -0
  162. package/dist/tools/update-status/index.js +39 -44
  163. package/dist/tools/update-status-actions.js +2 -7
  164. package/dist/tools/validate.js +34 -28
  165. package/dist/transports/oauth-validator.js +12 -1
  166. package/dist/transports/transport-factory.d.ts +2 -1
  167. package/dist/transports/transport-factory.js +19 -0
  168. package/dist/types/common/primitives.d.ts +2 -0
  169. package/dist/types/durable-job.d.ts +45 -0
  170. package/dist/types/durable-validation.d.ts +1 -1
  171. package/dist/types/evidence-autofill.d.ts +13 -1
  172. package/dist/types/handoff-artifacts.d.ts +2 -1
  173. package/dist/types/readiness.d.ts +8 -1
  174. package/dist/types/reconcile.d.ts +6 -0
  175. package/dist/types/skill-registry.d.ts +26 -1
  176. package/dist/types/spec/core.d.ts +5 -0
  177. package/dist/types/spec/inputs.d.ts +2 -2
  178. package/dist/types/spec-format.d.ts +11 -1
  179. package/dist/types/spec-grounding.d.ts +26 -0
  180. package/dist/types/transport.d.ts +1 -0
  181. package/dist/types/validation-receipt.d.ts +27 -0
  182. package/package.json +3 -2
  183. package/planu-plugin.json +13 -26
  184. package/src/i18n/messages/en.json +1 -1
  185. package/src/i18n/messages/es.json +1 -1
  186. package/src/i18n/messages/pt.json +1 -1
  187. package/dist/engine/skill-generator/conventions-hasher.d.ts +0 -7
  188. package/dist/engine/skill-generator/conventions-hasher.js +0 -24
  189. package/dist/engine/universal-rules/rules/agent-teams.d.ts +0 -3
  190. package/dist/engine/universal-rules/rules/agent-teams.js +0 -63
  191. package/dist/tools/clarify-requirements/multiple-choice.d.ts +0 -30
  192. package/dist/tools/clarify-requirements/multiple-choice.js +0 -306
  193. package/dist/tools/clarify-requirements/questions-context.d.ts +0 -9
  194. package/dist/tools/clarify-requirements/questions-context.js +0 -74
  195. package/dist/tools/clarify-requirements/questions.d.ts +0 -16
  196. package/dist/tools/clarify-requirements/questions.js +0 -96
  197. package/dist/tools/clarify-requirements.d.ts +0 -4
  198. package/dist/tools/clarify-requirements.js +0 -314
  199. package/dist/tools/code-graph-handler.d.ts +0 -6
  200. package/dist/tools/code-graph-handler.js +0 -72
  201. package/dist/tools/create-rule.d.ts +0 -4
  202. package/dist/tools/create-rule.js +0 -107
  203. package/dist/tools/feedback-handler.d.ts +0 -7
  204. package/dist/tools/feedback-handler.js +0 -150
  205. package/dist/tools/semantic-search-handler.d.ts +0 -7
  206. package/dist/tools/semantic-search-handler.js +0 -71
  207. package/dist/tools/skill-registry/search.d.ts +0 -11
  208. package/dist/tools/skill-registry/search.js +0 -159
  209. package/dist/tools/tool-registry/deprecated-stubs.d.ts +0 -19
  210. package/dist/tools/tool-registry/deprecated-stubs.js +0 -65
@@ -1,6 +1,6 @@
1
1
  export declare const IMPLEMENTATION_CONTRACT_SECTION = "Implementation Contract";
2
2
  /** Subsections every rendered contract must include — omitting one is a defect. */
3
- export declare const IMPLEMENTATION_CONTRACT_REQUIRED_SUBSECTIONS: readonly ['User Outcome', 'File-Level Work Plan', 'Acceptance-To-Verification Map', 'Verification Commands'];
3
+ export declare const IMPLEMENTATION_CONTRACT_REQUIRED_SUBSECTIONS: readonly ['User Outcome'];
4
4
  /**
5
5
  * SPEC-1406: subsections that carry no grounded content are omitted from the rendered
6
6
  * body rather than filled with restated-criterion filler. Their absence is not an
@@ -8,10 +8,15 @@ export declare const IMPLEMENTATION_CONTRACT_REQUIRED_SUBSECTIONS: readonly ['Us
8
8
  * already-persisted specs that still carry these sections (with real, non-empty
9
9
  * content) transitionable: `evaluateImplementationContract` only requires them when
10
10
  * the rendered body chose to include the heading.
11
+ *
12
+ * SPEC-1435: extends the same omission precedent to File-Level Work Plan,
13
+ * Acceptance-To-Verification Map, and Verification Commands — the renderer used to
14
+ * fill these with a constant "no evidence was grounded" sentence that its own quality
15
+ * gates could not see. Now the renderer omits the subsection instead.
11
16
  */
12
- export declare const IMPLEMENTATION_CONTRACT_OPTIONAL_SUBSECTIONS: readonly ['Behavior Contract', 'Edge Cases And Failure Modes', 'Non-Goals And Forbidden Approaches'];
17
+ export declare const IMPLEMENTATION_CONTRACT_OPTIONAL_SUBSECTIONS: readonly ['Behavior Contract', 'Edge Cases And Failure Modes', 'Non-Goals And Forbidden Approaches', 'File-Level Work Plan', 'Acceptance-To-Verification Map', 'Verification Commands'];
13
18
  /** Union of required + optional subsections. Kept for callers that only need the full set. */
14
- export declare const IMPLEMENTATION_CONTRACT_SUBSECTIONS: readonly ["User Outcome", "File-Level Work Plan", "Acceptance-To-Verification Map", "Verification Commands", "Behavior Contract", "Edge Cases And Failure Modes", "Non-Goals And Forbidden Approaches"];
19
+ export declare const IMPLEMENTATION_CONTRACT_SUBSECTIONS: readonly ["User Outcome", "Behavior Contract", "Edge Cases And Failure Modes", "Non-Goals And Forbidden Approaches", "File-Level Work Plan", "Acceptance-To-Verification Map", "Verification Commands"];
15
20
  export declare function hasImplementationContract(specBody: string): boolean;
16
21
  export declare function extractTopLevelSection(body: string, sectionName: string): string;
17
22
  //# sourceMappingURL=common.d.ts.map
@@ -1,12 +1,7 @@
1
1
  import { escapeRegex } from '../../core/shared/strings.js';
2
2
  export const IMPLEMENTATION_CONTRACT_SECTION = 'Implementation Contract';
3
3
  /** Subsections every rendered contract must include — omitting one is a defect. */
4
- export const IMPLEMENTATION_CONTRACT_REQUIRED_SUBSECTIONS = [
5
- 'User Outcome',
6
- 'File-Level Work Plan',
7
- 'Acceptance-To-Verification Map',
8
- 'Verification Commands',
9
- ];
4
+ export const IMPLEMENTATION_CONTRACT_REQUIRED_SUBSECTIONS = ['User Outcome'];
10
5
  /**
11
6
  * SPEC-1406: subsections that carry no grounded content are omitted from the rendered
12
7
  * body rather than filled with restated-criterion filler. Their absence is not an
@@ -14,11 +9,19 @@ export const IMPLEMENTATION_CONTRACT_REQUIRED_SUBSECTIONS = [
14
9
  * already-persisted specs that still carry these sections (with real, non-empty
15
10
  * content) transitionable: `evaluateImplementationContract` only requires them when
16
11
  * the rendered body chose to include the heading.
12
+ *
13
+ * SPEC-1435: extends the same omission precedent to File-Level Work Plan,
14
+ * Acceptance-To-Verification Map, and Verification Commands — the renderer used to
15
+ * fill these with a constant "no evidence was grounded" sentence that its own quality
16
+ * gates could not see. Now the renderer omits the subsection instead.
17
17
  */
18
18
  export const IMPLEMENTATION_CONTRACT_OPTIONAL_SUBSECTIONS = [
19
19
  'Behavior Contract',
20
20
  'Edge Cases And Failure Modes',
21
21
  'Non-Goals And Forbidden Approaches',
22
+ 'File-Level Work Plan',
23
+ 'Acceptance-To-Verification Map',
24
+ 'Verification Commands',
22
25
  ];
23
26
  /** Union of required + optional subsections. Kept for callers that only need the full set. */
24
27
  export const IMPLEMENTATION_CONTRACT_SUBSECTIONS = [
@@ -83,12 +83,10 @@ function evaluateSubsection(section, content) {
83
83
  }
84
84
  const trimmed = content.trim();
85
85
  if (trimmed.length === 0) {
86
- if (!OPTIONAL_SUBSECTIONS.has(section)) {
87
- issues.push({
88
- code: 'contract_subsection_empty',
89
- message: `Implementation Contract subsection "${section}" is empty.`,
90
- });
91
- }
86
+ issues.push({
87
+ code: 'contract_subsection_empty',
88
+ message: `Implementation Contract subsection "${section}" is empty.`,
89
+ });
92
90
  return issues;
93
91
  }
94
92
  for (const pattern of FILLER_PATTERNS) {
@@ -3,32 +3,32 @@ export function buildImplementationContractSection(input) {
3
3
  const criteria = input.criteria;
4
4
  const testFiles = input.files.test.map((file) => file.path);
5
5
  const verificationCommands = input.verificationCommands;
6
+ const filePlan = renderFilePlan(input.files);
6
7
  const verificationRows = renderVerificationRows(criteria, testFiles, verificationCommands);
7
8
  const failureModes = renderFailureModes(criteria);
8
9
  const nonGoals = renderNonGoals(input.outOfScope);
10
+ const verificationCommandLines = verificationCommands.length > 0
11
+ ? verificationCommands.map((command) => `- \`${command}\``)
12
+ : criteria.length > 0
13
+ ? []
14
+ : ['- Needs decision: provide a repository-supported verification command.'];
9
15
  const lines = [
10
16
  `## ${IMPLEMENTATION_CONTRACT_SECTION}`,
11
17
  '### User Outcome',
12
18
  criteria[0]?.text ??
13
19
  'Needs decision: define the exact observable outcome this spec must deliver.',
14
20
  '',
15
- '### File-Level Work Plan',
16
- ...renderFilePlan(input.files, criteria.length > 0),
17
- '',
18
- '### Acceptance-To-Verification Map',
19
- ...verificationRows,
20
- '',
21
- // SPEC-1406 (DEFECT 5): a subsection with no grounded content is omitted rather than
22
- // filled with a restated-criterion placeholder — prefer omission over filler.
21
+ // SPEC-1406 (DEFECT 5) / SPEC-1435: a subsection with no grounded content is omitted
22
+ // rather than filled with a restated-criterion placeholder — prefer omission over filler.
23
+ ...(filePlan.length > 0 ? ['### File-Level Work Plan', ...filePlan, ''] : []),
24
+ ...(verificationRows.length > 0
25
+ ? ['### Acceptance-To-Verification Map', ...verificationRows, '']
26
+ : []),
23
27
  ...(failureModes.length > 0 ? ['### Edge Cases And Failure Modes', ...failureModes, ''] : []),
24
28
  ...(nonGoals.length > 0 ? ['### Non-Goals And Forbidden Approaches', ...nonGoals, ''] : []),
25
- '### Verification Commands',
26
- ...(verificationCommands.length > 0
27
- ? verificationCommands.map((command) => `- \`${command}\``)
28
- : criteria.length > 0
29
- ? ['- No repository-supported verification command was grounded for this candidate.']
30
- : ['- Needs decision: provide a repository-supported verification command.']),
31
- '',
29
+ ...(verificationCommandLines.length > 0
30
+ ? ['### Verification Commands', ...verificationCommandLines, '']
31
+ : []),
32
32
  ];
33
33
  return lines.join('\n');
34
34
  }
@@ -38,7 +38,7 @@ export function appendImplementationContractIfMissing(specBody, input) {
38
38
  }
39
39
  return `${specBody.trimEnd()}\n\n${buildImplementationContractSection(input)}`;
40
40
  }
41
- function renderFilePlan(files, hasGroundedBehavior) {
41
+ function renderFilePlan(files) {
42
42
  const lines = [];
43
43
  for (const [label, entries] of [
44
44
  ['Create', files.create],
@@ -49,12 +49,7 @@ function renderFilePlan(files, hasGroundedBehavior) {
49
49
  lines.push(`- ${label}: \`${entry.path}\` (${entry.status})`);
50
50
  }
51
51
  }
52
- if (lines.length > 0) {
53
- return lines;
54
- }
55
- return hasGroundedBehavior
56
- ? ['- No file ownership was grounded for this behavior.']
57
- : ['- Needs decision: identify expected source and test files.'];
52
+ return lines;
58
53
  }
59
54
  /**
60
55
  * SPEC-1406 (DEFECT 1, round 2): renders the Acceptance-To-Verification Map body.
@@ -80,7 +75,7 @@ function renderVerificationRows(criteria, testFiles, commands) {
80
75
  return rows;
81
76
  }
82
77
  if (testFiles.length === 0) {
83
- return ['- No repository-supported verification evidence was grounded for these criteria.'];
78
+ return [];
84
79
  }
85
80
  return renderSharedVerificationRow(testFiles, commands);
86
81
  }
@@ -2,15 +2,15 @@ import { technologyValue } from './technology-registry.js';
2
2
  import { fileExists } from '../core/shared/fs.js';
3
3
  // @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
4
4
  // engine/project-health-checker.ts — SPEC-180 + SPEC-192 + SPEC-177: Project health check with convention scanning
5
- import { execSync } from 'node:child_process';
6
5
  import { readFile } from 'node:fs/promises';
7
6
  import { join, relative } from 'node:path';
8
7
  import { parseConventions, scanConventions } from './convention-scanner/index.js';
9
8
  import { fastFindPatterns } from './core-bridge.js';
9
+ import { execGitCapture } from './git/exec-git.js';
10
10
  // ── helpers ──────────────────────────────────────────────────────────────────
11
11
  function execSafe(cmd, cwd) {
12
12
  try {
13
- return execSync(cmd, { cwd, stdio: ['pipe', 'pipe', 'pipe'] }).toString();
13
+ return execGitCapture(cmd, cwd);
14
14
  }
15
15
  catch {
16
16
  return '';
@@ -10,11 +10,39 @@ import { findScenariosWithoutTests, parseFrontmatterScenarios, } from './validat
10
10
  import { normalizeExecutableEvidence } from './validator/executable-evidence.js';
11
11
  import { evaluateImplementationContract } from './implementation-contract/index.js';
12
12
  import { extractNormalizedAcceptanceCriteria } from './spec-format/acceptance-criteria.js';
13
+ import { isRetiredScaffoldText } from './spec-format/retired-scaffold.js';
13
14
  import { detectCrossSpecPremiseContradictions } from './contradiction-detector.js';
15
+ import { parseCriterionGroundingRecords, parseTechnicalReferenceGroundingRecords, } from './spec-grounding/contract.js';
14
16
  // ── SPEC-784: Technical section quality constants ─────────────────────────────
15
17
  const TECHNICAL_MIN_CHARS = 500;
18
+ const GROUNDING_FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
16
19
  // Detects "See technical.md", "See spec.md", or "See `<file>` technical.md" patterns
17
20
  const TECHNICAL_PLACEHOLDER_REGEX = /\bSee\s+(?:`?[^`\n]+`?\s+)?(?:technical|spec)\.md/i;
21
+ const BDD_CRITERION_RE = /\bGIVEN\b[\s\S]*\bWHEN\b[\s\S]*\bTHEN\b/i;
22
+ function isExecutableBddLine(line) {
23
+ return !isRetiredScaffoldText(line) && BDD_CRITERION_RE.test(line);
24
+ }
25
+ function groundingParseDiagnosticFindings(huRaw) {
26
+ const frontmatter = GROUNDING_FRONTMATTER_RE.exec(huRaw)?.[1] ?? '';
27
+ const criteriaDiagnostics = parseCriterionGroundingRecords(frontmatter).diagnostics;
28
+ const technicalDiagnostics = parseTechnicalReferenceGroundingRecords(frontmatter).diagnostics;
29
+ return [...criteriaDiagnostics, ...technicalDiagnostics].map((diagnostic) => `GROUNDING_PARSE_DROPPED: "${diagnostic.identifier}" has an invalid ${diagnostic.field} ` +
30
+ `("${diagnostic.value}") — allowed: ${diagnostic.allowed.join(', ') || 'n/a'}`);
31
+ }
32
+ function checkScenariosWithoutTests(huRaw, bddScenarioCount) {
33
+ if (bddScenarioCount === 0) {
34
+ return [];
35
+ }
36
+ return findScenariosWithoutTests(huRaw).map((title) => `scenarios_missing_tests: scenario "${title}" has no tests array — add tests entries as a block list (tests: with one "- path" line per test) or an inline array (tests: ["path"]) (SPEC-732)`);
37
+ }
38
+ function checkZeroBddCriteria(criteriaLines) {
39
+ if (criteriaLines.length === 0 || criteriaLines.some(isExecutableBddLine)) {
40
+ return [];
41
+ }
42
+ return [
43
+ 'NO_BDD_CRITERIA: no acceptance criterion parses as GIVEN/WHEN/THEN — restate at least one criterion in executable form',
44
+ ];
45
+ }
18
46
  // ── Helpers ──────────────────────────────────────────────────────────────────
19
47
  function scoreHuCompleteness(spec) {
20
48
  const blockers = [];
@@ -73,6 +101,15 @@ function countFrontmatterScenarios(raw) {
73
101
  // Each scenario entry starts with " - title:" (2-space indent)
74
102
  return (block.match(/^\s+-\s+title:/gm) ?? []).length;
75
103
  }
104
+ function frontmatterScenarioScavengeWarning(bddScenarioCount, normalizedCriteria) {
105
+ const bodyCriteriaCount = normalizedCriteria.filter((criterion) => criterion.source === 'body').length;
106
+ if (bddScenarioCount === 0 || bddScenarioCount >= bodyCriteriaCount) {
107
+ return null;
108
+ }
109
+ return (`frontmatter_scenarios_below_body_criteria: frontmatter declares ${String(bddScenarioCount)} ` +
110
+ `scenarios but body renders ${String(bodyCriteriaCount)} acceptance criteria — scenarios may ` +
111
+ `have been scavenged from prose`);
112
+ }
76
113
  async function readFichaContent(spec) {
77
114
  return readSpecTechnicalSection(spec);
78
115
  }
@@ -418,6 +455,7 @@ export async function checkSpecReadiness(spec, mode, projectHash) {
418
455
  ...earsBlockers,
419
456
  ];
420
457
  const allWarnings = [...hu.warnings, ...criteria.warnings, ...files.warnings, ...deps.warnings];
458
+ allBlockers.push(...checkZeroBddCriteria(effectiveCriteriaLines));
421
459
  if (mode === 'strict' && spec.scope !== 'trivial' && spec.difficulty >= 3) {
422
460
  for (const issue of evaluateImplementationContract(huContent, canonicalCriteriaLines)) {
423
461
  allBlockers.push(`${issue.code}: ${issue.message}`);
@@ -426,12 +464,12 @@ export async function checkSpecReadiness(spec, mode, projectHash) {
426
464
  // SPEC-732: Executable AC gate — block approved when any scenario lacks a `tests` array.
427
465
  // Only enforce when the spec uses frontmatter scenarios (BDD format).
428
466
  const bddScenarioCount = countFrontmatterScenarios(huRaw);
429
- if (bddScenarioCount > 0) {
430
- const scenariosWithoutTests = findScenariosWithoutTests(huRaw);
431
- for (const title of scenariosWithoutTests) {
432
- allBlockers.push(`scenarios_missing_tests: scenario "${title}" has no tests array — add tests entries as a block list (tests: with one "- path" line per test) or an inline array (tests: ["path"]) (SPEC-732)`);
433
- }
467
+ allBlockers.push(...checkScenariosWithoutTests(huRaw, bddScenarioCount));
468
+ const scavengeWarning = frontmatterScenarioScavengeWarning(bddScenarioCount, normalizedCriteria);
469
+ if (scavengeWarning) {
470
+ allWarnings.push(scavengeWarning);
434
471
  }
472
+ allWarnings.push(...groundingParseDiagnosticFindings(huRaw));
435
473
  // SPEC-1214: BDD-criteria executable-evidence gate — when a spec has no frontmatter
436
474
  // scenarios, its criteria must carry a TEST: marker mapping to an executable test file,
437
475
  // or validation will later score 0 with "No executable scenarios declared".
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import { atomicWriteFile } from '../safety/atomic-write-file.js';
3
3
  import { SECTIONS_WITHOUT_LITERAL_BODY_TEXT, } from '../../types/index.js';
4
4
  import { escapeRegex } from '../../core/shared/strings.js';
5
+ import { propagateMirrors } from './propagate-mirrors.js';
5
6
  function findSectionBounds(content, section) {
6
7
  const headingRe = new RegExp(`^(#{1,6})\\s+${escapeRegex(section)}\\s*$`, 'mi');
7
8
  const match = headingRe.exec(content);
@@ -54,6 +55,7 @@ export async function applyChangesToSpec(specPath, changes) {
54
55
  let modified = content;
55
56
  let applied = 0;
56
57
  const skipped = [];
58
+ const appliedSections = new Set();
57
59
  for (const change of approvedTextChanges) {
58
60
  const bounds = findSectionBounds(modified, change.section);
59
61
  if (bounds === null) {
@@ -92,10 +94,12 @@ export async function applyChangesToSpec(specPath, changes) {
92
94
  change.newValue +
93
95
  modified.slice(absoluteIdx + change.originalValue.length);
94
96
  applied++;
97
+ appliedSections.add(change.section);
95
98
  }
96
99
  if (applied === 0) {
97
100
  return { applied: 0, skipped, fileWritten: false };
98
101
  }
102
+ modified = propagateMirrors(modified, appliedSections);
99
103
  await atomicWriteFile(specPath, modified);
100
104
  return { applied, skipped, fileWritten: true };
101
105
  }
@@ -0,0 +1,3 @@
1
+ import type { AppliedSections } from '../../types/index.js';
2
+ export declare function propagateMirrors(content: string, appliedSections: AppliedSections): string;
3
+ //# sourceMappingURL=propagate-mirrors.d.ts.map
@@ -0,0 +1,311 @@
1
+ import { escapeRegex } from '../../core/shared/strings.js';
2
+ import { normalizeCriterionText } from '../criterion-identity.js';
3
+ import { parseBddScenarios, renderBddScenariosYaml } from '../spec-format/bdd-parser.js';
4
+ import { extractSectionCriteria } from '../spec-format/acceptance-criteria.js';
5
+ import { extractListItems, findMarkdownSectionRange } from '../spec-format/markdown-sections.js';
6
+ import { parseCriterionGroundingRecords, parseTechnicalReferenceGroundingRecords, renderGroundingFrontmatter, renderTechnicalReferenceGroundingFrontmatter, } from '../spec-grounding/contract.js';
7
+ const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n/;
8
+ const ACCEPTANCE_CRITERIA_HEADING = 'Acceptance Criteria';
9
+ const FILES_HEADING = 'Files';
10
+ const TECHNICAL_HEADING = 'Technical';
11
+ const FILE_WORK_PLAN_HEADING = 'File-Level Work Plan';
12
+ const ACCEPTANCE_MAP_HEADING = 'Acceptance-To-Verification Map';
13
+ const VERIFICATION_COMMANDS_HEADING = 'Verification Commands';
14
+ const VERIFICATION_STARTS_WITH_RE = /Verification starts with:\s*`([^`]+)`/i;
15
+ export function propagateMirrors(content, appliedSections) {
16
+ let result = content;
17
+ if (appliedSections.has(ACCEPTANCE_CRITERIA_HEADING)) {
18
+ result = propagateAcceptanceCriteria(result);
19
+ }
20
+ if (appliedSections.has(FILES_HEADING)) {
21
+ result = propagateFiles(result);
22
+ }
23
+ if (appliedSections.has(TECHNICAL_HEADING)) {
24
+ result = propagateTechnicalVerificationCommand(result);
25
+ }
26
+ return result;
27
+ }
28
+ function propagateAcceptanceCriteria(content) {
29
+ const section = findMarkdownSectionRange(content, ACCEPTANCE_CRITERIA_HEADING);
30
+ if (!section) {
31
+ return content;
32
+ }
33
+ const sectionBody = content.slice(section.contentStart, section.end);
34
+ const criteriaTexts = extractSectionCriteria(sectionBody);
35
+ const scenarios = criteriaTexts.map(criterionToScenario);
36
+ const oldRecords = mapWithFrontmatter(content, (fm) => indexCriterionRecords(parseCriterionGroundingRecords(fm).records));
37
+ const oldTestPaths = mapWithFrontmatter(content, (fm) => parseTechnicalReferenceGroundingRecords(fm)
38
+ .records.filter((record) => record.section === 'test')
39
+ .map((record) => record.path));
40
+ const criteriaRecords = criteriaTexts.map((text) => oldRecords.get(normalizeCriterionText(text)) ?? {
41
+ text,
42
+ source: 'user_input',
43
+ evidence: ['reconcile_spec.changes'],
44
+ confidence: 'high',
45
+ });
46
+ let result = transformFrontmatter(content, (fm) => {
47
+ let updated = replaceFrontmatterKeyBlock(fm, 'scenarios', renderBddScenariosYaml(scenarios));
48
+ updated = replaceFrontmatterKeyBlock(updated, 'grounding', groundingBlockLines(criteriaRecords, technicalReferencesFromFrontmatter(fm)));
49
+ return updated;
50
+ });
51
+ result = withSectionBody(result, ACCEPTANCE_MAP_HEADING, (body) => syncAcceptanceMapForCriteria(body, criteriaTexts, oldTestPaths));
52
+ return result;
53
+ }
54
+ function propagateFiles(content) {
55
+ const section = findMarkdownSectionRange(content, FILES_HEADING);
56
+ if (!section) {
57
+ return content;
58
+ }
59
+ const sectionBody = content.slice(section.contentStart, section.end);
60
+ const files = parseFilesSection(sectionBody);
61
+ const oldTestPaths = mapWithFrontmatter(content, (fm) => parseTechnicalReferenceGroundingRecords(fm)
62
+ .records.filter((record) => record.section === 'test')
63
+ .map((record) => record.path));
64
+ const oldTechnicalReferences = mapWithFrontmatter(content, (fm) => indexTechnicalReferenceRecords(parseTechnicalReferenceGroundingRecords(fm).records));
65
+ const technicalReferences = buildTechnicalReferenceRecords(files, oldTechnicalReferences);
66
+ const newTestPaths = files.test;
67
+ let result = transformFrontmatter(content, (fm) => replaceFrontmatterKeyBlock(fm, 'grounding', groundingBlockLines(criteriaRecordsFromFrontmatter(fm), technicalReferences)));
68
+ result = replaceNamedSection(result, FILE_WORK_PLAN_HEADING, renderFileWorkPlan(files));
69
+ result = withSectionBody(result, ACCEPTANCE_MAP_HEADING, (body) => addVerifiedTestPathEntries(body, oldTestPaths, newTestPaths));
70
+ result = withSectionBody(result, VERIFICATION_COMMANDS_HEADING, (body) => syncVerificationCommandLines(body, oldTestPaths, newTestPaths));
71
+ return result;
72
+ }
73
+ function propagateTechnicalVerificationCommand(content) {
74
+ const technicalSection = findMarkdownSectionRange(content, TECHNICAL_HEADING);
75
+ if (!technicalSection) {
76
+ return content;
77
+ }
78
+ const technicalBody = content.slice(technicalSection.contentStart, technicalSection.end);
79
+ const newCommand = VERIFICATION_STARTS_WITH_RE.exec(technicalBody)?.[1]?.trim();
80
+ if (!newCommand) {
81
+ return content;
82
+ }
83
+ const commandsSection = findMarkdownSectionRange(content, VERIFICATION_COMMANDS_HEADING);
84
+ if (!commandsSection) {
85
+ return content;
86
+ }
87
+ const currentBody = content.slice(commandsSection.contentStart, commandsSection.end);
88
+ if (currentBody.includes(newCommand)) {
89
+ return content;
90
+ }
91
+ return replaceNamedSection(content, VERIFICATION_COMMANDS_HEADING, [`- \`${newCommand}\``]);
92
+ }
93
+ function criterionToScenario(text) {
94
+ const gwtLines = text
95
+ .split(/\b(GIVEN|WHEN|THEN|AND)\b/)
96
+ .reduce((lines, part, index, parts) => {
97
+ if (index % 2 === 0) {
98
+ return lines;
99
+ }
100
+ const keyword = part;
101
+ const rest = (parts[index + 1] ?? '').trim();
102
+ lines.push(`${keyword} ${rest}`);
103
+ return lines;
104
+ }, []);
105
+ const [scenario] = parseBddScenarios(gwtLines.join('\n'));
106
+ return scenario ?? { title: text, steps: [{ keyword: 'then', text }], done: false };
107
+ }
108
+ function indexCriterionRecords(records) {
109
+ const index = new Map();
110
+ for (const record of records) {
111
+ index.set(normalizeCriterionText(record.text), record);
112
+ }
113
+ return index;
114
+ }
115
+ function indexTechnicalReferenceRecords(records) {
116
+ const index = new Map();
117
+ for (const record of records) {
118
+ index.set(record.path, record);
119
+ }
120
+ return index;
121
+ }
122
+ function parseFilesSection(sectionBody) {
123
+ return {
124
+ create: parseFilesSubsection(sectionBody, 'Create'),
125
+ modify: parseFilesSubsection(sectionBody, 'Modify'),
126
+ test: parseFilesSubsection(sectionBody, 'Test'),
127
+ };
128
+ }
129
+ function parseFilesSubsection(sectionBody, heading) {
130
+ const range = findMarkdownSectionRange(sectionBody, heading);
131
+ if (!range) {
132
+ return [];
133
+ }
134
+ const body = sectionBody.slice(range.contentStart, range.end);
135
+ return extractListItems(body).map((item) => item.replace(/`/g, '').trim());
136
+ }
137
+ function buildTechnicalReferenceRecords(files, oldRecords) {
138
+ const groups = [
139
+ ['create', files.create],
140
+ ['modify', files.modify],
141
+ ['test', files.test],
142
+ ];
143
+ const records = [];
144
+ for (const [section, paths] of groups) {
145
+ for (const path of paths) {
146
+ const existing = oldRecords.get(path);
147
+ records.push(existing
148
+ ? { ...existing, section }
149
+ : {
150
+ path,
151
+ section,
152
+ source: 'user_input',
153
+ evidence: ['reconcile_spec.changes'],
154
+ confidence: 'high',
155
+ });
156
+ }
157
+ }
158
+ return records;
159
+ }
160
+ function renderFileWorkPlan(files) {
161
+ const groups = [
162
+ ['Create', files.create],
163
+ ['Modify', files.modify],
164
+ ['Test', files.test],
165
+ ];
166
+ return groups.flatMap(([label, paths]) => paths.map((path) => `- ${label}: \`${path}\` (pending)`));
167
+ }
168
+ function isVerifiedByLine(line, path) {
169
+ return /^-\s*Verified by:/i.test(line.trim()) && line.includes(path);
170
+ }
171
+ function addVerifiedTestPathEntries(body, oldPaths, newPaths) {
172
+ const removed = oldPaths.filter((path) => !newPaths.includes(path));
173
+ const added = newPaths.filter((path) => !oldPaths.includes(path));
174
+ if (removed.length === 0 && added.length === 0) {
175
+ return body;
176
+ }
177
+ let updated = removed.length === 0
178
+ ? body
179
+ : body
180
+ .split('\n')
181
+ .filter((line) => !removed.some((path) => isVerifiedByLine(line, path)))
182
+ .join('\n');
183
+ if (added.length > 0) {
184
+ const bulletLines = added.map((path) => `- Verified by: \`${path}\``).join('\n');
185
+ updated = `${updated.trimEnd()}\n${bulletLines}\n`;
186
+ }
187
+ return updated;
188
+ }
189
+ function isTestRunnerCommandLine(line) {
190
+ return /`[^`]*\b(vitest|jest|mocha|pnpm test|npm test)\b[^`]*`/i.test(line);
191
+ }
192
+ function syncVerificationCommandLines(body, oldPaths, newPaths) {
193
+ const removed = oldPaths.filter((path) => !newPaths.includes(path));
194
+ const added = newPaths.filter((path) => !oldPaths.includes(path));
195
+ if (removed.length === 0 && added.length === 0) {
196
+ return body;
197
+ }
198
+ const lines = body.split('\n').map((line) => {
199
+ if (!oldPaths.some((path) => line.includes(path))) {
200
+ return line;
201
+ }
202
+ return substituteTestPathsInLine(line, removed, added);
203
+ });
204
+ const stillMissing = added.filter((path) => !lines.some((line) => line.includes(path)));
205
+ if (stillMissing.length === 0) {
206
+ return lines.join('\n');
207
+ }
208
+ const commandLineIndex = lines.findIndex(isTestRunnerCommandLine);
209
+ if (commandLineIndex !== -1) {
210
+ lines[commandLineIndex] = substituteTestPathsInLine(lines[commandLineIndex] ?? '', [], stillMissing);
211
+ return lines.join('\n');
212
+ }
213
+ while (lines.length > 0 && lines[lines.length - 1]?.trim() === '') {
214
+ lines.pop();
215
+ }
216
+ return `${[...lines, ...stillMissing.map((path) => `- \`${path}\``)].join('\n')}\n`;
217
+ }
218
+ function syncAcceptanceMapForCriteria(body, criteriaTexts, testPaths) {
219
+ const lines = body.split('\n').filter((line) => line.trim().length > 0);
220
+ const kept = lines.filter((line) => criteriaTexts.some((text) => line.includes(text)));
221
+ const covered = new Set(kept.flatMap((line) => criteriaTexts.filter((text) => line.includes(text))));
222
+ const newEntries = criteriaTexts
223
+ .filter((text) => !covered.has(text))
224
+ .map((text) => renderAcceptanceMapEntry(text, testPaths));
225
+ const resultLines = [...kept, ...newEntries];
226
+ if (resultLines.length === 0) {
227
+ return body;
228
+ }
229
+ return `${resultLines.join('\n')}\n`;
230
+ }
231
+ function renderAcceptanceMapEntry(criterionText, testPaths) {
232
+ return testPaths.length > 0
233
+ ? `- ${criterionText}: verified by ${testPaths.map((path) => `\`${path}\``).join(', ')}`
234
+ : `- ${criterionText}: verification pending — no test file listed yet`;
235
+ }
236
+ function substituteTestPathsInLine(line, removed, added) {
237
+ let updated = line;
238
+ for (const path of removed) {
239
+ updated = updated.replace(new RegExp(`\\s*${escapeRegex(path)}`), '');
240
+ }
241
+ if (added.length === 0) {
242
+ return updated;
243
+ }
244
+ const insertion = ` ${added.join(' ')}`;
245
+ if (updated.endsWith('`.')) {
246
+ return `${updated.slice(0, -2)}${insertion}\`.`;
247
+ }
248
+ if (updated.endsWith('`')) {
249
+ return `${updated.slice(0, -1)}${insertion}\``;
250
+ }
251
+ return `${updated}${insertion}`;
252
+ }
253
+ function replaceNamedSection(content, headingName, bodyLines) {
254
+ const range = findMarkdownSectionRange(content, headingName);
255
+ if (!range) {
256
+ return content;
257
+ }
258
+ const isLast = range.end >= content.length;
259
+ const body = bodyLines.join('\n');
260
+ const tail = isLast ? `${body}\n` : `${body}\n\n`;
261
+ return content.slice(0, range.contentStart) + tail + content.slice(range.end);
262
+ }
263
+ function withSectionBody(content, headingName, transform) {
264
+ const range = findMarkdownSectionRange(content, headingName);
265
+ if (!range) {
266
+ return content;
267
+ }
268
+ const body = content.slice(range.contentStart, range.end);
269
+ const updated = transform(body);
270
+ if (updated === body) {
271
+ return content;
272
+ }
273
+ return content.slice(0, range.contentStart) + updated + content.slice(range.end);
274
+ }
275
+ function transformFrontmatter(content, transform) {
276
+ const match = FRONTMATTER_RE.exec(content);
277
+ if (!match?.[1]) {
278
+ return content;
279
+ }
280
+ const updated = transform(match[1]);
281
+ return `---\n${updated}\n---\n${content.slice(match[0].length)}`;
282
+ }
283
+ function mapWithFrontmatter(content, read) {
284
+ const match = FRONTMATTER_RE.exec(content);
285
+ return read(match?.[1] ?? '');
286
+ }
287
+ function technicalReferencesFromFrontmatter(frontmatter) {
288
+ return parseTechnicalReferenceGroundingRecords(frontmatter).records;
289
+ }
290
+ function criteriaRecordsFromFrontmatter(frontmatter) {
291
+ return parseCriterionGroundingRecords(frontmatter).records;
292
+ }
293
+ function groundingBlockLines(criteria, technicalReferences) {
294
+ const criteriaLines = renderGroundingFrontmatter(criteria).slice(2);
295
+ const technicalReferenceLines = technicalReferences.length > 0
296
+ ? renderTechnicalReferenceGroundingFrontmatter(technicalReferences)
297
+ : [];
298
+ return ['grounding:', ...criteriaLines, ...technicalReferenceLines];
299
+ }
300
+ function replaceFrontmatterKeyBlock(frontmatter, key, blockLines) {
301
+ const re = new RegExp(`^${escapeRegex(key)}:.*(?:\\n[ \\t]+.*)*`, 'm');
302
+ const match = re.exec(frontmatter);
303
+ const replacement = blockLines.join('\n');
304
+ if (!match) {
305
+ return `${frontmatter.trimEnd()}\n${replacement}`;
306
+ }
307
+ return (frontmatter.slice(0, match.index) +
308
+ replacement +
309
+ frontmatter.slice(match.index + match[0].length));
310
+ }
311
+ //# sourceMappingURL=propagate-mirrors.js.map
@@ -3,6 +3,7 @@ import { applyTypescriptFix } from './strategies/typescript-fix.js';
3
3
  import { applyLintFix } from './strategies/lint-fix.js';
4
4
  import { applyTestFix } from './strategies/test-fix.js';
5
5
  import { applyCoverageFix } from './strategies/coverage-fix.js';
6
+ import { qaCheckTimeoutMs } from '../qa-gate.js';
6
7
  const MAX_ATTEMPTS = 3;
7
8
  /**
8
9
  * Maps a failure type to the appropriate fix strategy name.
@@ -24,14 +25,14 @@ export function identifyFixStrategy(failureType) {
24
25
  /**
25
26
  * Dispatches to the appropriate strategy based on strategy name.
26
27
  */
27
- function runStrategy(strategyName, projectPath, details) {
28
+ function runStrategy(strategyName, projectPath, details, timeoutMs) {
28
29
  switch (strategyName) {
29
30
  case technologyValue('technology-typescript-969545'):
30
- return applyTypescriptFix(projectPath, details);
31
+ return applyTypescriptFix(projectPath, details, timeoutMs);
31
32
  case 'lint':
32
- return applyLintFix(projectPath, details);
33
+ return applyLintFix(projectPath, details, timeoutMs);
33
34
  case 'tests':
34
- return applyTestFix(projectPath, details);
35
+ return applyTestFix(projectPath, details, timeoutMs);
35
36
  case 'coverage':
36
37
  return applyCoverageFix(projectPath, details);
37
38
  }
@@ -47,8 +48,14 @@ export async function runHealingLoop(specId, projectPath, failureContext) {
47
48
  const fixedIssues = [];
48
49
  const remainingIssues = [];
49
50
  let healed = false;
51
+ const deadline = Date.now() + qaCheckTimeoutMs();
50
52
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
51
- const result = await runStrategy(strategy, projectPath, failureContext.details);
53
+ const remainingMs = deadline - Date.now();
54
+ if (remainingMs <= 0) {
55
+ remainingIssues.push(`Attempt ${attempt}: skipped — healing wall-clock budget exhausted`);
56
+ break;
57
+ }
58
+ const result = await runStrategy(strategy, projectPath, failureContext.details, remainingMs);
52
59
  if (result.fixed) {
53
60
  attempts.push({
54
61
  strategy,
@@ -2,5 +2,5 @@ import type { StrategyResult } from '../../../types/index.js';
2
2
  /**
3
3
  * Runs eslint --fix on the project and reports what was fixed.
4
4
  */
5
- export declare function applyLintFix(projectPath: string, _details: string): Promise<StrategyResult>;
5
+ export declare function applyLintFix(projectPath: string, _details: string, timeoutMs?: number): Promise<StrategyResult>;
6
6
  //# sourceMappingURL=lint-fix.d.ts.map