@opengsd/gsd-core 1.6.1 → 1.7.0-rc.2

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 (119) hide show
  1. package/.claude-plugin/marketplace.json +20 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +711 -0
  4. package/agents/gsd-advisor-researcher.md +2 -0
  5. package/agents/gsd-ai-researcher.md +1 -1
  6. package/agents/gsd-assumptions-analyzer.md +2 -0
  7. package/agents/gsd-code-fixer.md +2 -0
  8. package/agents/gsd-code-reviewer.md +2 -0
  9. package/agents/gsd-codebase-mapper.md +2 -0
  10. package/agents/gsd-debugger.md +2 -0
  11. package/agents/gsd-doc-writer.md +2 -0
  12. package/agents/gsd-eval-auditor.md +2 -0
  13. package/agents/gsd-executor.md +9 -6
  14. package/agents/gsd-integration-checker.md +2 -0
  15. package/agents/gsd-nyquist-auditor.md +2 -0
  16. package/agents/gsd-phase-researcher.md +2 -0
  17. package/agents/gsd-plan-checker.md +2 -0
  18. package/agents/gsd-planner.md +2 -0
  19. package/agents/gsd-project-researcher.md +2 -0
  20. package/agents/gsd-research-synthesizer.md +2 -0
  21. package/agents/gsd-roadmapper.md +2 -0
  22. package/agents/gsd-security-auditor.md +2 -0
  23. package/agents/gsd-ui-auditor.md +2 -0
  24. package/agents/gsd-ui-checker.md +2 -0
  25. package/agents/gsd-ui-researcher.md +2 -0
  26. package/agents/gsd-verifier.md +5 -2
  27. package/bin/gsd-mcp-server.js +31 -0
  28. package/bin/install.js +411 -1146
  29. package/commands/gsd/review.md +6 -0
  30. package/gemini-extension.json +1 -1
  31. package/gsd-core/bin/gsd-tools.cjs +134 -8
  32. package/gsd-core/bin/lib/adapter-declarative.cjs +35 -0
  33. package/gsd-core/bin/lib/adapter-imperative.cjs +52 -0
  34. package/gsd-core/bin/lib/assumption-delta.cjs +231 -0
  35. package/gsd-core/bin/lib/capability-lifecycle.cjs +7 -7
  36. package/gsd-core/bin/lib/capability-loader.cjs +45 -9
  37. package/gsd-core/bin/lib/capability-lock.cjs +2 -2
  38. package/gsd-core/bin/lib/capability-registry.cjs +891 -82
  39. package/gsd-core/bin/lib/capability-source.cjs +26 -11
  40. package/gsd-core/bin/lib/capability-validator.cjs +222 -2
  41. package/gsd-core/bin/lib/cli-skew-check.cjs +44 -0
  42. package/gsd-core/bin/lib/command-aliases.cjs +8 -0
  43. package/gsd-core/bin/lib/commands.cjs +2 -1
  44. package/gsd-core/bin/lib/config.cjs +27 -0
  45. package/gsd-core/bin/lib/embedding-adapter.cjs +27 -0
  46. package/gsd-core/bin/lib/external-descriptor-trust.cjs +70 -0
  47. package/gsd-core/bin/lib/frontmatter.cjs +53 -6
  48. package/gsd-core/bin/lib/handshake-serialized.cjs +70 -0
  49. package/gsd-core/bin/lib/hook-bus.cjs +81 -0
  50. package/gsd-core/bin/lib/host-integration-sdk.cjs +53 -0
  51. package/gsd-core/bin/lib/host-integration.cjs +469 -0
  52. package/gsd-core/bin/lib/init.cjs +35 -7
  53. package/gsd-core/bin/lib/install-engine.cjs +755 -0
  54. package/gsd-core/bin/lib/install-profiles.cjs +35 -4
  55. package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
  56. package/gsd-core/bin/lib/mcp-server.cjs +194 -0
  57. package/gsd-core/bin/lib/milestone.cjs +68 -40
  58. package/gsd-core/bin/lib/model-adapter.cjs +50 -0
  59. package/gsd-core/bin/lib/phase-id.cjs +18 -0
  60. package/gsd-core/bin/lib/phase.cjs +57 -90
  61. package/gsd-core/bin/lib/phases-command-router.cjs +4 -3
  62. package/gsd-core/bin/lib/planning-workspace.cjs +1 -1
  63. package/gsd-core/bin/lib/probe-core.cjs +132 -2
  64. package/gsd-core/bin/lib/review-reviewer-selection.cjs +129 -13
  65. package/gsd-core/bin/lib/roadmap-command-router.cjs +3 -2
  66. package/gsd-core/bin/lib/roadmap-parser.cjs +21 -11
  67. package/gsd-core/bin/lib/roadmap-upgrade.cjs +3 -2
  68. package/gsd-core/bin/lib/roadmap.cjs +33 -22
  69. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +65 -9
  70. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +54 -4
  71. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +5 -2
  72. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1 -1
  73. package/gsd-core/bin/lib/runtime-name-policy.cjs +160 -30
  74. package/gsd-core/bin/lib/shell-command-projection.cjs +16 -0
  75. package/gsd-core/bin/lib/stale-bake-guard.cjs +254 -0
  76. package/gsd-core/bin/lib/state-command-router.cjs +4 -0
  77. package/gsd-core/bin/lib/state-io.cjs +55 -0
  78. package/gsd-core/bin/lib/state-transition.cjs +1603 -0
  79. package/gsd-core/bin/lib/state.cjs +327 -683
  80. package/gsd-core/bin/lib/surface.cjs +4 -1
  81. package/gsd-core/bin/lib/validate.cjs +2 -1
  82. package/gsd-core/bin/lib/verify.cjs +6 -4
  83. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +12 -2
  84. package/gsd-core/bin/lib/workstream-inventory.cjs +28 -0
  85. package/gsd-core/bin/lib/workstream.cjs +4 -4
  86. package/gsd-core/bin/shared/config-schema.manifest.json +9 -0
  87. package/gsd-core/references/agent-skills-bootstrap.md +60 -0
  88. package/gsd-core/references/honest-verifier.md +105 -0
  89. package/gsd-core/references/model-profiles.md +27 -0
  90. package/gsd-core/references/reviewer-instances.md +99 -0
  91. package/gsd-core/workflows/autonomous.md +30 -32
  92. package/gsd-core/workflows/complete-milestone.md +6 -10
  93. package/gsd-core/workflows/execute-phase.md +1 -1
  94. package/gsd-core/workflows/forensics.md +3 -3
  95. package/gsd-core/workflows/help/modes/full.md +1 -1
  96. package/gsd-core/workflows/manager.md +15 -15
  97. package/gsd-core/workflows/milestone-summary.md +3 -3
  98. package/gsd-core/workflows/new-milestone.md +6 -0
  99. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +42 -0
  100. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +102 -0
  101. package/gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md +23 -0
  102. package/gsd-core/workflows/plan-phase.md +4 -159
  103. package/gsd-core/workflows/review.md +33 -2
  104. package/gsd-core/workflows/thread.md +4 -4
  105. package/gsd-core/workflows/verify-phase.md +11 -4
  106. package/gsd-core/workflows/verify-work.md +1 -2
  107. package/hooks/dist/gsd-graphify-update.sh +7 -1
  108. package/hooks/gsd-graphify-update.sh +7 -1
  109. package/package.json +6 -4
  110. package/scripts/ci-test-scope.cjs +38 -9
  111. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
  112. package/scripts/lint-regression-test-names.allowlist.json +3 -0
  113. package/scripts/lint-test-file-count.allowlist.json +19 -5
  114. package/scripts/mutation-matrix.cjs +45 -3
  115. package/scripts/prompt-injection-scan.sh +8 -0
  116. package/scripts/run-tests.cjs +51 -1
  117. package/scripts/sync-manifest-versions.cjs +66 -14
  118. package/skills/gsd-review/SKILL.md +6 -0
  119. package/scripts/lint-windows-test-portability.cjs +0 -178
@@ -32,7 +32,7 @@ const coreUtilsMod = require("./core-utils.cjs");
32
32
  const { toPosixPath, generateSlugInternal, readSubdirectories } = coreUtilsMod;
33
33
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
34
34
  const phaseIdMod = require("./phase-id.cjs");
35
- const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, phaseTokenMatches, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, } = phaseIdMod;
35
+ const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, phaseTokenMatches, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, OPTIONAL_PHASE_TAG_SOURCE, } = phaseIdMod;
36
36
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
37
37
  const phaseLocatorMod = require("./phase-locator.cjs");
38
38
  const { findPhaseInternal, getArchivedPhaseDirs } = phaseLocatorMod;
@@ -47,8 +47,8 @@ const frontmatterMod = require("./frontmatter.cjs");
47
47
  const stateMod = require("./state.cjs");
48
48
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
49
49
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
50
- const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
51
50
  const clock_cjs_1 = require("./clock.cjs");
51
+ const state_transition_cjs_1 = require("./state-transition.cjs");
52
52
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- uat-predicate.cjs is an export= CommonJS module
53
53
  const uatPredicate = require("./uat-predicate.cjs");
54
54
  const { evaluateUatPassed } = uatPredicate;
@@ -57,7 +57,7 @@ const verificationMod = require("./verification.cjs");
57
57
  const { readVerificationStatus } = verificationMod;
58
58
  const { planningDir, withPlanningLock } = planningWorkspace;
59
59
  const { extractFrontmatter } = frontmatterMod;
60
- const { readModifyWriteStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, syncStateFrontmatter, withStateLock, updatePerformanceMetricsSection, } = stateMod;
60
+ const { readModifyWriteStateMd, stateExtractField, stateReplaceField, syncStateFrontmatter, withStateLock, updatePerformanceMetricsSection, } = stateMod;
61
61
  // #2893 — strict canonical filter: `{padded_phase}-{NN}-PLAN.md` or `PLAN.md`.
62
62
  const isCanonicalPlanFile = (f) => f.endsWith('-PLAN.md') || f === 'PLAN.md';
63
63
  // Any .md file with PLAN anywhere in the basename — diagnostic net
@@ -181,7 +181,7 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
181
181
  if (node_fs_1.default.existsSync(roadmapPath)) {
182
182
  try {
183
183
  const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
184
- const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalized)}\\.(\\d+)\\s*:`, 'gi');
184
+ const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalized)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
185
185
  let pm;
186
186
  while ((pm = phasePattern.exec(roadmapContent)) !== null) {
187
187
  decimalSet.add(parseInt(pm[1], 10));
@@ -221,7 +221,7 @@ function getRoadmapModeForPhase(cwd, phaseNum) {
221
221
  const milestoneContent = extractCurrentMilestone(rawContent, cwd);
222
222
  const fullContent = stripShippedMilestones(rawContent);
223
223
  const escapedPhase = phaseMarkdownRegexSource(phaseNum);
224
- const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}\\s*:`, 'i');
224
+ const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'i');
225
225
  for (const content of [milestoneContent, fullContent]) {
226
226
  const headerMatch = content.match(phaseHeader);
227
227
  if (!headerMatch || headerMatch.index === undefined)
@@ -578,7 +578,8 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
578
578
  // Three sources are scanned so that a phase in ANY representation
579
579
  // (section header, roadmap bullet, or on-disk directory) is counted:
580
580
  // 1) Section headers: ### Phase N: / ## Phase N: / #### Phase N:
581
- const headerPattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
581
+ // #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
582
+ const headerPattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]*\))?:/gi;
582
583
  // 2) Roadmap bullet entries: - [ ] **Phase N: ...** (all checkbox variants)
583
584
  // The lookahead accepts colon, decimal-dot, whitespace, bold-close asterisk,
584
585
  // or end-of-line so titleless forms ("- [ ] **Phase 11**", "- [ ] Phase 11")
@@ -662,7 +663,8 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
662
663
  const content = extractCurrentMilestone(rawContent, cwd);
663
664
  let maxPhase = 0;
664
665
  if (config.phase_naming !== 'custom') {
665
- const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
666
+ // #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
667
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]*\))?:/gi;
666
668
  let m;
667
669
  while ((m = phasePattern.exec(content)) !== null) {
668
670
  const num = parseInt(m[1], 10);
@@ -740,14 +742,14 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
740
742
  const content = extractCurrentMilestone(rawContent, cwd);
741
743
  const normalizedAfter = normalizePhaseName(afterPhase);
742
744
  const afterPhaseEscaped = phaseMarkdownRegexSource(normalizedAfter);
743
- const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+${afterPhaseEscaped}:`, 'i');
745
+ const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:`, 'i');
744
746
  const headingMatch = targetPattern.test(content);
745
- const bulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}[:\\s]`, 'i');
747
+ const bulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
746
748
  const anyHeadingPattern = /#{2,4}\s*Phase\s+\d/i;
747
749
  const roadmapHasHeadingPhases = anyHeadingPattern.test(content);
748
750
  const isBulletStyle = !headingMatch && bulletPattern.test(content) && !roadmapHasHeadingPhases;
749
751
  if (!headingMatch && !isBulletStyle) {
750
- const checklistPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}[:\\s]`, 'i');
752
+ const checklistPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
751
753
  if (checklistPattern.test(content)) {
752
754
  error(`Phase ${afterPhase} exists in roadmap summary but is missing a detail section (### Phase ${afterPhase}: ...).`);
753
755
  }
@@ -769,7 +771,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
769
771
  catch {
770
772
  /* intentionally empty */
771
773
  }
772
- const rmPhasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalizedBase)}\\.(\\d+)\\s*:`, 'gi');
774
+ const rmPhasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalizedBase)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
773
775
  let rmMatch;
774
776
  while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
775
777
  decimalSet.add(parseInt(rmMatch[1], 10));
@@ -785,13 +787,13 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
785
787
  (0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(dirPath, '.gitkeep'), '');
786
788
  let updatedContent;
787
789
  if (isBulletStyle) {
788
- const boldBulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${afterPhaseEscaped}:`, 'i');
790
+ const boldBulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:`, 'i');
789
791
  const useBold = boldBulletPattern.test(content);
790
792
  const phaseLabel = useBold
791
793
  ? `**Phase ${_decimalPhase}: ${description}**`
792
794
  : `Phase ${_decimalPhase}: ${description}`;
793
795
  const bulletEntry = `\n- [ ] ${phaseLabel}`;
794
- const targetBulletPattern = new RegExp(`(-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}[:\\s][^\\n]*)`, 'i');
796
+ const targetBulletPattern = new RegExp(`(-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][^\\n]*)`, 'i');
795
797
  const bulletMatchResult = rawContent.match(targetBulletPattern);
796
798
  if (!bulletMatchResult) {
797
799
  error(`Could not find Phase ${afterPhase} bullet line`);
@@ -811,7 +813,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
811
813
  }
812
814
  else {
813
815
  const phaseEntry = `\n### Phase ${_decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))} ${_decimalPhase} to break down)\n`;
814
- const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
816
+ const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:[^\\n]*\\n)`, 'i');
815
817
  const headerMatch = rawContent.match(headerPattern);
816
818
  if (!headerMatch) {
817
819
  error(`Could not find Phase ${afterPhase} header`);
@@ -860,12 +862,12 @@ function renameDecimalPhases(phasesDir, baseInt, removedDecimal) {
860
862
  const oldPhaseId = `${baseInt}.${item.oldDecimal}`;
861
863
  const newPhaseId = `${baseInt}.${newDecimal}`;
862
864
  const newDirName = `${item.prefix}.${newDecimal}-${item.slug}`;
863
- node_fs_1.default.renameSync(node_path_1.default.join(phasesDir, item.dir), node_path_1.default.join(phasesDir, newDirName));
865
+ (0, shell_command_projection_cjs_1.retryRenameSync)(node_path_1.default.join(phasesDir, item.dir), node_path_1.default.join(phasesDir, newDirName));
864
866
  renamedDirs.push({ from: item.dir, to: newDirName });
865
867
  for (const f of node_fs_1.default.readdirSync(node_path_1.default.join(phasesDir, newDirName))) {
866
868
  if (f.includes(oldPhaseId)) {
867
869
  const newFileName = f.replace(oldPhaseId, newPhaseId);
868
- node_fs_1.default.renameSync(node_path_1.default.join(phasesDir, newDirName, f), node_path_1.default.join(phasesDir, newDirName, newFileName));
870
+ (0, shell_command_projection_cjs_1.retryRenameSync)(node_path_1.default.join(phasesDir, newDirName, f), node_path_1.default.join(phasesDir, newDirName, newFileName));
869
871
  renamedFiles.push({ from: f, to: newFileName });
870
872
  }
871
873
  }
@@ -903,12 +905,12 @@ function renameIntegerPhases(phasesDir, removedInt) {
903
905
  const oldPrefix = `${oldPadded}${letterSuffix}${decimalSuffix}`;
904
906
  const newPrefix = `${newPadded}${letterSuffix}${decimalSuffix}`;
905
907
  const newDirName = `${newPrefix}-${item.slug}`;
906
- node_fs_1.default.renameSync(node_path_1.default.join(phasesDir, item.dir), node_path_1.default.join(phasesDir, newDirName));
908
+ (0, shell_command_projection_cjs_1.retryRenameSync)(node_path_1.default.join(phasesDir, item.dir), node_path_1.default.join(phasesDir, newDirName));
907
909
  renamedDirs.push({ from: item.dir, to: newDirName });
908
910
  for (const f of node_fs_1.default.readdirSync(node_path_1.default.join(phasesDir, newDirName))) {
909
911
  if (f.startsWith(oldPrefix)) {
910
912
  const newFileName = newPrefix + f.slice(oldPrefix.length);
911
- node_fs_1.default.renameSync(node_path_1.default.join(phasesDir, newDirName, f), node_path_1.default.join(phasesDir, newDirName, newFileName));
913
+ (0, shell_command_projection_cjs_1.retryRenameSync)(node_path_1.default.join(phasesDir, newDirName, f), node_path_1.default.join(phasesDir, newDirName, newFileName));
912
914
  renamedFiles.push({ from: f, to: newFileName });
913
915
  }
914
916
  }
@@ -940,11 +942,13 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
940
942
  withPlanningLock(cwd, () => {
941
943
  let content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
942
944
  const escaped = escapeRegex(targetPhase);
943
- content = content.replace(new RegExp(`\\n?(?<h>#{2,4})\\s*Phase\\s+${escaped}\\s*:[\\s\\S]*?(?=\\n\\k<h>(?!#)\\s+Phase\\s+[^\\n:]+\\s*:|$)`, 'i'), '');
944
- content = content.replace(new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${escaped}[:\\s][^\\n]*`, 'gi'), '');
945
+ content = content.replace(new RegExp(`\\n?(?<h>#{2,4})\\s*Phase\\s+${escaped}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:[\\s\\S]*?(?=\\n\\k<h>(?!#)\\s+Phase\\s+[^\\n:]+\\s*:|$)`, 'i'), '');
946
+ content = content.replace(new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${escaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][^\\n]*`, 'gi'), '');
945
947
  content = content.replace(new RegExp(`\\n?\\|\\s*${escaped}\\.?\\s[^|]*\\|[^\\n]*`, 'gi'), '');
946
948
  if (!isDecimal) {
947
- content = content.replace(/(#{2,4}\s*Phase\s+)(\d+(?:\.\d+)?)(\s*:)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}${suffix}`);
949
+ // #1729: fold an optional pre-colon ( ) tag into the suffix capture so it
950
+ // is re-emitted verbatim — a tagged later phase still gets renumbered.
951
+ content = content.replace(/(#{2,4}\s*Phase\s+)(\d+(?:\.\d+)?)((?:\s*\([^)\n]*\))?\s*:)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}${suffix}`);
948
952
  content = content.replace(/(-\s*\[[ x]\]\s*.*?Phase\s+)(\d+)(\s*:|\s+)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseNumber(num, removedInt)}${suffix}`);
949
953
  content = content.replace(/(\|\s*)(\d+)(\.\s)/g, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseNumber(num, removedInt)}${suffix}`);
950
954
  content = content.replace(/(?<![0-9-])(\d{2})-(\d{2})(?=(?:(?:-[A-Za-z][A-Za-z0-9-]*)?-(?:PLAN|SUMMARY)\.md)|(?![0-9-]))/g, (_match, phaseNum, planNum) => `${decrementRoadmapPaddedPhaseNumber(phaseNum, removedInt)}-${planNum}`);
@@ -1047,7 +1051,7 @@ function phaseDisplayNameFromRoadmap(roadmapContent, phaseNum) {
1047
1051
  if (!roadmapContent || !phaseNum)
1048
1052
  return null;
1049
1053
  const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
1050
- const heading = roadmapContent.match(new RegExp(`^#{2,4}\\s*Phase\\s+${phaseEscaped}\\s*:\\s*([^\\n]+)`, 'im'));
1054
+ const heading = roadmapContent.match(new RegExp(`^#{2,4}\\s*Phase\\s+${phaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:\\s*([^\\n]+)`, 'im'));
1051
1055
  if (!heading)
1052
1056
  return null;
1053
1057
  const name = heading[1].replace(/\(INSERTED\)/i, '').trim();
@@ -1128,7 +1132,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
1128
1132
  const originalRoadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
1129
1133
  roadmapContent = originalRoadmapContent;
1130
1134
  const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
1131
- const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`, 'i');
1135
+ const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][^\\n]*)`, 'i');
1132
1136
  roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
1133
1137
  const tableRowPattern = new RegExp(`^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`, 'im');
1134
1138
  roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
@@ -1170,7 +1174,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
1170
1174
  if (node_fs_1.default.existsSync(reqPath)) {
1171
1175
  const phaseEsc = phaseMarkdownRegexSource(phaseNum);
1172
1176
  const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd);
1173
- const phaseSectionMatch = currentMilestoneRoadmap.match(new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i'));
1177
+ const phaseSectionMatch = currentMilestoneRoadmap.match(new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i'));
1174
1178
  const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
1175
1179
  const reqMatch = sectionText.match(/\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]+)/i);
1176
1180
  const originalReqContent = node_fs_1.default.readFileSync(reqPath, 'utf-8');
@@ -1299,7 +1303,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
1299
1303
  // phase. Allow optional `**`/`__` emphasis after the marker and stop
1300
1304
  // the name capture at emphasis so bold names slug cleanly; the number
1301
1305
  // capture is unchanged.
1302
- const phasePattern = /(?:#{2,4}|-\s*\[[ xX]\])\s*(?:\*\*|__)?\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n*]+)/gi;
1306
+ // #1729: `(?:\s*\([^)\n]*\))?` after the number tolerates a pre-colon
1307
+ // ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE) so
1308
+ // `### Phase N (Cluster B): X` resolves. Captures are unchanged.
1309
+ const phasePattern = /(?:#{2,4}|-\s*\[[ xX]\])\s*(?:\*\*|__)?\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:\s*([^\n*]+)/gi;
1303
1310
  let pm;
1304
1311
  while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
1305
1312
  if (comparePhaseNum(pm[1], phaseNum) > 0) {
@@ -1321,73 +1328,33 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
1321
1328
  if (node_fs_1.default.existsSync(statePath)) {
1322
1329
  const originalStateContent = (0, shell_command_projection_cjs_1.platformReadSync)(statePath) || '';
1323
1330
  let stateContent = originalStateContent;
1324
- const phaseValue = nextPhaseNum || phaseNum;
1331
+ // ADR-1769 Phase 3: the STATE.md field-update policy (Current Phase
1332
+ // shape/name, Status, Current Plan, Last Activity + Description, and
1333
+ // the Completed/Total Phases + Progress percent block) now dispatches
1334
+ // to the STATE.md Transition Module. The ~90-line inline RMW callback
1335
+ // that lived here is the pure `completePhaseCore` in
1336
+ // src/state-transition.cts, backed by the field-classification table.
1337
+ // `updatePerformanceMetricsSection` + `syncStateFrontmatter` stay in
1338
+ // this adapter: they are section-table / disk-scan concerns, not
1339
+ // classified fields, and `syncStateFrontmatter` is the post-sync this
1340
+ // transaction needs (it does NOT go through readModifyWriteStateMd
1341
+ // because STATE.md is committed atomically with ROADMAP/REQUIREMENTS).
1325
1342
  const nextPhaseDisplayName = phaseDisplayNameFromRoadmap(roadmapContent, nextPhaseNum) ??
1326
1343
  phaseDisplayNameFromSlug(nextPhaseName);
1327
- const existingPhaseField = stateExtractField(stateContent, 'Current Phase') ||
1328
- stateExtractField(stateContent, 'Phase');
1329
- let newPhaseValue = String(phaseValue);
1330
- if (existingPhaseField) {
1331
- const totalMatch = existingPhaseField.match(/of\s+(\d+)/);
1332
- const nameMatch = existingPhaseField.match(/\(([^)]+)\)/);
1333
- if (totalMatch) {
1334
- const total = totalMatch[1];
1335
- const nameStr = nextPhaseDisplayName
1336
- ? ` (${nextPhaseDisplayName})`
1337
- : nameMatch
1338
- ? ` (${nameMatch[1]})`
1339
- : '';
1340
- newPhaseValue = `${phaseValue} of ${total}${nameStr}`;
1341
- }
1342
- else if (nextPhaseDisplayName) {
1343
- newPhaseValue = `${phaseValue} — ${nextPhaseDisplayName}`;
1344
- }
1345
- }
1346
- stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue);
1347
- if (nextPhaseDisplayName) {
1348
- stateContent =
1349
- stateReplaceField(stateContent, 'Current Phase Name', nextPhaseDisplayName) ||
1350
- stateContent;
1351
- }
1352
- stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null, isLastPhase ? 'Milestone complete' : 'Ready to plan');
1353
- stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started');
1354
- const lastActivityDescription = `Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`;
1355
- if (/^Last activity:/m.test(stateContent)) {
1356
- stateContent =
1357
- stateReplaceField(stateContent, 'Last activity', `${today} — ${lastActivityDescription}`) ||
1358
- stateContent;
1359
- }
1360
- else {
1361
- stateContent =
1362
- stateReplaceField(stateContent, 'Last Activity', today) ||
1363
- stateContent;
1364
- }
1365
- stateContent =
1366
- stateReplaceField(stateContent, 'Last Activity Description', lastActivityDescription) ||
1367
- stateContent;
1368
- const completedRaw = stateExtractField(stateContent, 'Completed Phases');
1369
- if (completedRaw !== null) {
1370
- let newCompleted = parseInt(completedRaw, 10);
1371
- let derivedTotalPhases = null;
1372
- if (roadmapContent !== null) {
1373
- const derived = (0, phase_lifecycle_cjs_1.deriveProgressFromRoadmap)(roadmapContent);
1374
- if (derived.completedPhases !== null)
1375
- newCompleted = derived.completedPhases;
1376
- if (derived.totalPhases !== null)
1377
- derivedTotalPhases = derived.totalPhases;
1378
- }
1379
- stateContent =
1380
- stateReplaceField(stateContent, 'Completed Phases', String(newCompleted)) ||
1381
- stateContent;
1382
- const totalRaw = stateExtractField(stateContent, 'Total Phases');
1383
- const totalPhases = derivedTotalPhases || (totalRaw ? parseInt(totalRaw, 10) : null);
1384
- if (totalPhases && totalPhases > 0) {
1385
- const newPercent = (0, phase_lifecycle_cjs_1.clampPercent)(newCompleted, totalPhases);
1386
- stateContent =
1387
- stateReplaceField(stateContent, 'Progress', `${newPercent}%`) || stateContent;
1388
- stateContent = stateContent.replace(/(percent:\s*)\d+/, `$1${newPercent}`);
1389
- }
1390
- }
1344
+ const completeResult = (0, state_transition_cjs_1.transitionCore)(stateContent, {
1345
+ kind: 'completePhase',
1346
+ phaseNum,
1347
+ nextPhaseNum,
1348
+ nextPhaseName: nextPhaseDisplayName,
1349
+ isLastPhase,
1350
+ planCount,
1351
+ summaryCount,
1352
+ }, {
1353
+ clock: clock_cjs_1.realClock,
1354
+ progressProvider: () => null, // completePhase derives progress from the roadmap, not disk
1355
+ roadmapProvider: () => roadmapContent,
1356
+ });
1357
+ stateContent = completeResult.content;
1391
1358
  stateContent = updatePerformanceMetricsSection(stateContent, cwd, phaseNum, planCount, summaryCount);
1392
1359
  stateContent = syncStateFrontmatter(stateContent, cwd);
1393
1360
  writes.push({ filePath: statePath, before: originalStateContent, after: stateContent });
@@ -4,8 +4,8 @@
4
4
  * Keeps gsd-tools.cjs thin while preserving current CJS semantics.
5
5
  *
6
6
  * Unsupported in this router (treated as unknown):
7
- * - archive: `phases archive` is excluded from the subcommands list so it
8
- * falls through to the unknown-subcommand error path.
7
+ * - archive: `phases archive` is excluded (#2684) — it is an internal
8
+ * subcommand that milestone.complete forwards to, not a public surface.
9
9
  *
10
10
  * ADR-457 build-at-publish: the hand-written bin/lib/phases-command-router.cjs
11
11
  * collapsed to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour
@@ -19,7 +19,8 @@ const { routeCjsCommandFamily } = cjsCommandRouterAdapter;
19
19
  function routePhasesCommand({ phase, milestone, args, cwd, raw, error }) {
20
20
  routeCjsCommandFamily({
21
21
  args,
22
- // Exclude 'archive' so it hits the unknownMessage path.
22
+ // #2684: `phases archive` is deliberately excluded — it is an internal
23
+ // subcommand that milestone.complete forwards to, not a public surface.
23
24
  subcommands: command_aliases_cjs_1.PHASES_SUBCOMMANDS.filter((s) => s !== 'archive'),
24
25
  error,
25
26
  unknownMessage: (_subcommand, available) => `Unknown phases subcommand. Available: ${available.join(', ')}`,
@@ -260,7 +260,7 @@ function withPlanningLock(cwd, fn, clock) {
260
260
  const stolen = lockPath + '.stale-' + process.pid + '-' + clock.now() + '-' + (_planningStealSeq++);
261
261
  let renamed = false;
262
262
  try {
263
- node_fs_1.default.renameSync(lockPath, stolen);
263
+ (0, shell_command_projection_cjs_1.retryRenameSync)(lockPath, stolen);
264
264
  renamed = true;
265
265
  }
266
266
  catch { /* another racer won */ }
@@ -30,13 +30,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
30
30
  return (mod && mod.__esModule) ? mod : { "default": mod };
31
31
  };
32
32
  Object.defineProperty(exports, "__esModule", { value: true });
33
- exports.PROHIBITION_VALIDATORS = exports.VALID_STATUS = void 0;
33
+ exports.INSUFFICIENT_SPEC = exports.PROHIBITION_VALIDATORS = exports.VALID_STATUS = void 0;
34
34
  exports.validateRequirement = validateRequirement;
35
35
  exports.validateResolution = validateResolution;
36
36
  exports.analyzeCoverage = analyzeCoverage;
37
37
  exports.validateProhibitionResolution = validateProhibitionResolution;
38
38
  exports.projectProhibitions = projectProhibitions;
39
39
  exports.dispositionForProhibition = dispositionForProhibition;
40
+ exports.truthStatement = truthStatement;
41
+ exports.truthVerification = truthVerification;
42
+ exports.projectTruths = projectTruths;
43
+ exports.dispositionForUnverifiableTruth = dispositionForUnverifiableTruth;
40
44
  exports.runProbeCli = runProbeCli;
41
45
  const node_fs_1 = __importDefault(require("node:fs"));
42
46
  /** The LOCKED set of valid lifecycle statuses (the re-cut: no covered/backstop). */
@@ -44,11 +48,40 @@ exports.VALID_STATUS = ['resolved', 'dismissed', 'unresolved'];
44
48
  function errMessage(e) {
45
49
  return e instanceof Error ? e.message : String(e);
46
50
  }
51
+ /**
52
+ * Structural guard for ONE item of the report `items[]` — enforces the `Item` contract (#1907).
53
+ * `analyzeCoverage` always emits fully-populated, category/status-validated Items, so this never
54
+ * rejects legitimate output; it catches an adapter that bypasses the merge and hands back per-item
55
+ * garbage (e.g. `items:[{}]`) inside a well-shaped envelope — which the container-only guard let
56
+ * sail through as green output despite the fail-closed docstring below.
57
+ */
58
+ function isValidItem(item) {
59
+ if (item == null || typeof item !== 'object')
60
+ return false;
61
+ const i = item;
62
+ if (typeof i.requirement_id !== 'string' || !i.requirement_id.trim())
63
+ return false;
64
+ if (typeof i.category !== 'string' || !i.category.trim())
65
+ return false;
66
+ if (!exports.VALID_STATUS.includes(i.status))
67
+ return false;
68
+ if (typeof i.probe !== 'string')
69
+ return false;
70
+ // The three nullable fields must be a string or null — never some other type.
71
+ if (i.verification !== null && typeof i.verification !== 'string')
72
+ return false;
73
+ if (i.resolution !== null && typeof i.resolution !== 'string')
74
+ return false;
75
+ if (i.reason !== null && typeof i.reason !== 'string')
76
+ return false;
77
+ return true;
78
+ }
47
79
  /**
48
80
  * Structural guard for the report an adapter's `analyze` returns. The scaffold types `analyze`
49
81
  * loosely (it runs over JSON-parsed input the adapter `as`-casts), so a future adapter (#644)
50
82
  * that forgets to validate inside its closure could hand back a malformed object. Rather than
51
- * stringify garbage as green output, `runProbeCli` checks the report shape and fails closed.
83
+ * stringify garbage as green output, `runProbeCli` checks the report shape — container AND every
84
+ * item — and fails closed.
52
85
  */
53
86
  function isValidReport(report) {
54
87
  if (report == null || typeof report !== 'object')
@@ -56,6 +89,8 @@ function isValidReport(report) {
56
89
  const r = report;
57
90
  if (!Array.isArray(r.items))
58
91
  return false;
92
+ if (!r.items.every(isValidItem))
93
+ return false;
59
94
  const c = r.coverage;
60
95
  if (c == null || typeof c !== 'object')
61
96
  return false;
@@ -355,6 +390,101 @@ function dispositionForProhibition(prohibition, context = {}) {
355
390
  reason: 'judgment-tier prohibition routes to judgment review — never a silent green (ADR-550 D4)',
356
391
  };
357
392
  }
393
+ /** Extract a truth's statement text from either the string or the object form (the Hyrum normalizer). */
394
+ function truthStatement(truth) {
395
+ if (typeof truth === 'string')
396
+ return truth;
397
+ if (truth != null && typeof truth === 'object') {
398
+ const s = truth.statement;
399
+ if (typeof s === 'string')
400
+ return s;
401
+ }
402
+ return '';
403
+ }
404
+ /**
405
+ * Extract a truth's verification tier, or `null` when it carries none (a plain string, or an object
406
+ * with no/garbled marker). Failing toward `null` is the Postel-safe direction: an unrecognized marker
407
+ * grades NORMALLY (never a spurious abstention — the over-abstention guard, AC#3).
408
+ *
409
+ * The marker is NOT only machine-emitted: `must_haves` markers can be authored BY HAND (#1820's
410
+ * spec-optional predicate rail), and the frontmatter continuation-KV parser preserves stray
411
+ * surrounding whitespace/quotes on a hand-authored value. So we normalize before comparison
412
+ * (Postel: be liberal in what you accept) — `'backstop '`, `' backstop'`, `'"backstop"'` all
413
+ * recognize as the tier. Without this, a hand-authored non-inferable `backstop` truth with a stray
414
+ * trailing space silently grades green instead of abstaining — the exact #1154 false-pass (#1905).
415
+ * An unrecoverably-corrupted marker (e.g. an embedded quote) stays unrecognized → null → graded
416
+ * normally (AC#3): we cannot know its intent, and abstaining on it would be a spurious abstention.
417
+ */
418
+ function truthVerification(truth) {
419
+ if (truth == null || typeof truth !== 'object')
420
+ return null;
421
+ const raw = truth.verification;
422
+ const v = typeof raw === 'string' ? raw.trim().replace(/^["']|["']$/g, '').trim() : raw;
423
+ return v === 'explicit' || v === 'backstop' ? v : null;
424
+ }
425
+ /**
426
+ * Conservative serializer (Postel: "send well-formed, minimal data") for projecting truths into a
427
+ * `must_haves.truths` block — the truth-axis analogue of `projectProhibitions`. A `backstop` truth is
428
+ * emitted as a flat-scalar object `{ statement, verification: 'backstop' }` (ADR-550 #1278: flat
429
+ * scalars round-trip the existing `parseMustHavesBlock`; a nested object would mangle it). Every other
430
+ * truth collapses to a bare statement string — only the non-inferable tier needs a structured marker,
431
+ * so an `explicit`/inferable truth never carries one (no spurious markers). Empty statements are dropped.
432
+ */
433
+ function projectTruths(items) {
434
+ if (!Array.isArray(items))
435
+ return [];
436
+ const out = [];
437
+ for (const item of items) {
438
+ const statement = truthStatement(item);
439
+ if (!statement)
440
+ continue;
441
+ if (truthVerification(item) === 'backstop') {
442
+ out.push({ statement, verification: 'backstop' });
443
+ }
444
+ else {
445
+ out.push(statement);
446
+ }
447
+ }
448
+ return out;
449
+ }
450
+ /** The stable, distinguishable verdict-reason token for an abstained non-inferable truth (review condition 1). */
451
+ exports.INSUFFICIENT_SPEC = 'insufficient_spec';
452
+ /**
453
+ * Deterministic verify-time disposition for a single truth (ADR-550 D4 truth-axis mirror, #1154).
454
+ * PURE — no LLM judgment (ADR-550 D5); the LLM verifier's only job is to decide whether `evidence`
455
+ * exists, this helper owns the routing once that is known.
456
+ *
457
+ * - A `backstop` (non-inferable) truth with NO explicit evidence → `{ unverified, flagged }`,
458
+ * reason `insufficient_spec`. NEVER green — the verify-time companion to D4's never-silent-pass.
459
+ * - A `backstop` truth WITH explicit evidence (a passing wired held-out/property test) → `green`.
460
+ * Abstention is for the *unconfirmable*, not for every non-inferable check.
461
+ * - Any non-`backstop` truth (explicit, or a plain inferable string) → `green`, never flagged.
462
+ * This is the over-abstention guard (AC#3): abstention fires ONLY on the exogenous backstop tag.
463
+ */
464
+ function dispositionForUnverifiableTruth(truth, context = {}) {
465
+ const tier = truthVerification(truth);
466
+ // Over-abstention guard (AC#3): only a backstop (non-inferable) truth is ever a candidate to abstain.
467
+ if (tier !== 'backstop') {
468
+ return {
469
+ status: 'green',
470
+ flagged: false,
471
+ tier,
472
+ reason: 'inferable truth — verified normally (no abstention; ADR-550 D4 over-abstention guard)',
473
+ };
474
+ }
475
+ const evidence = Array.isArray(context.evidence) ? context.evidence : [];
476
+ if (evidence.length === 0) {
477
+ // ABSTAIN: a non-inferable truth the verifier cannot confirm with explicit evidence. Routes to
478
+ // human_needed with the distinguishable insufficient_spec reason — never a silent pass (ADR-550 D4).
479
+ return { status: 'unverified', flagged: true, tier, reason: exports.INSUFFICIENT_SPEC };
480
+ }
481
+ return {
482
+ status: 'green',
483
+ flagged: false,
484
+ tier,
485
+ reason: 'backstop truth confirmed by explicit evidence (a passing wired held-out/property test or directly-observed behavior)',
486
+ };
487
+ }
358
488
  /**
359
489
  * Read the requirements file (and optional resolutions file), run the adapter's `analyze`,
360
490
  * and write the report as pretty JSON + newline. With no requirements path, writes the usage