@opengsd/gsd-core 1.9.1 → 1.10.0

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 (219) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +27 -3
  5. package/agents/gsd-debugger.md +12 -246
  6. package/agents/gsd-executor.md +7 -5
  7. package/agents/gsd-integration-checker.md +3 -0
  8. package/agents/gsd-plan-checker.md +9 -0
  9. package/agents/gsd-planner.md +5 -8
  10. package/agents/gsd-roadmapper.md +21 -3
  11. package/agents/gsd-verifier.md +14 -70
  12. package/bin/install.js +453 -289
  13. package/commands/gsd/mempalace-capture.md +1 -1
  14. package/commands/gsd/new-milestone.md +1 -1
  15. package/commands/gsd/plan-phase.md +1 -1
  16. package/gsd-core/bin/gsd-tools.cjs +579 -66
  17. package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
  18. package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
  19. package/gsd-core/bin/lib/api-coverage.cjs +120 -0
  20. package/gsd-core/bin/lib/audit.cjs +89 -1
  21. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  22. package/gsd-core/bin/lib/capability-registry.cjs +96 -110
  23. package/gsd-core/bin/lib/capability-validator.cjs +12 -2
  24. package/gsd-core/bin/lib/check-command-router.cjs +43 -1
  25. package/gsd-core/bin/lib/command-aliases.cjs +72 -0
  26. package/gsd-core/bin/lib/commands.cjs +26 -25
  27. package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
  28. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  29. package/gsd-core/bin/lib/config.cjs +12 -1
  30. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  31. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  32. package/gsd-core/bin/lib/core-utils.cjs +91 -12
  33. package/gsd-core/bin/lib/docs.cjs +3 -2
  34. package/gsd-core/bin/lib/external-job.cjs +19 -4
  35. package/gsd-core/bin/lib/frontmatter.cjs +84 -12
  36. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  37. package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
  38. package/gsd-core/bin/lib/graphify.cjs +142 -27
  39. package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
  40. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  41. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  42. package/gsd-core/bin/lib/init.cjs +1021 -57
  43. package/gsd-core/bin/lib/install-engine.cjs +64 -10
  44. package/gsd-core/bin/lib/install-profiles.cjs +27 -1
  45. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  46. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  47. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  48. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  49. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  50. package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
  51. package/gsd-core/bin/lib/io.cjs +28 -3
  52. package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
  53. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  54. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  55. package/gsd-core/bin/lib/milestone.cjs +75 -47
  56. package/gsd-core/bin/lib/phase-id.cjs +63 -0
  57. package/gsd-core/bin/lib/phase-locator.cjs +138 -45
  58. package/gsd-core/bin/lib/phase.cjs +260 -28
  59. package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
  60. package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
  61. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  62. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
  63. package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
  64. package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
  65. package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
  66. package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
  67. package/gsd-core/bin/lib/roadmap.cjs +37 -7
  68. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
  69. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
  70. package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
  71. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
  72. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  73. package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
  74. package/gsd-core/bin/lib/smart-entry.cjs +12 -0
  75. package/gsd-core/bin/lib/state-transition.cjs +73 -8
  76. package/gsd-core/bin/lib/state.cjs +151 -62
  77. package/gsd-core/bin/lib/surface.cjs +12 -1
  78. package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
  79. package/gsd-core/bin/lib/uat.cjs +320 -21
  80. package/gsd-core/bin/lib/unusable-input.cjs +9 -0
  81. package/gsd-core/bin/lib/verification.cjs +29 -12
  82. package/gsd-core/bin/lib/verify.cjs +10 -2
  83. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  84. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
  85. package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
  86. package/gsd-core/bin/lib/workstream.cjs +6 -0
  87. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  88. package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
  89. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  90. package/gsd-core/references/artifact-types.md +10 -3
  91. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  92. package/gsd-core/references/debugger-techniques.md +255 -0
  93. package/gsd-core/references/research-documentation-lookup.md +5 -3
  94. package/gsd-core/references/specless-probe-fallback.md +7 -6
  95. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  96. package/gsd-core/references/worktree-branch-check.md +2 -2
  97. package/gsd-core/templates/summary-complex.md +2 -0
  98. package/gsd-core/templates/summary-minimal.md +2 -0
  99. package/gsd-core/templates/summary-standard.md +2 -0
  100. package/gsd-core/templates/summary.md +2 -0
  101. package/gsd-core/workflows/audit-milestone.md +3 -0
  102. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  103. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  104. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  105. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  106. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  107. package/gsd-core/workflows/autonomous.md +32 -69
  108. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  109. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
  110. package/gsd-core/workflows/code-review.md +42 -160
  111. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  112. package/gsd-core/workflows/complete-milestone.md +23 -81
  113. package/gsd-core/workflows/debug.md +9 -12
  114. package/gsd-core/workflows/diagnose-issues.md +22 -0
  115. package/gsd-core/workflows/discovery-phase.md +4 -4
  116. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  117. package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
  118. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  119. package/gsd-core/workflows/docs-update.md +8 -51
  120. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
  121. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  122. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  123. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
  124. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  125. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  126. package/gsd-core/workflows/execute-phase.md +65 -137
  127. package/gsd-core/workflows/execute-plan.md +1 -1
  128. package/gsd-core/workflows/help/modes/full.md +6 -1
  129. package/gsd-core/workflows/ingest-docs.md +2 -1
  130. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  131. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  132. package/gsd-core/workflows/new-milestone.md +21 -38
  133. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  134. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  135. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  136. package/gsd-core/workflows/new-project.md +13 -226
  137. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  138. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  139. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  140. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  141. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  142. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  143. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  144. package/gsd-core/workflows/plan-phase.md +49 -193
  145. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  146. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  147. package/gsd-core/workflows/progress.md +11 -153
  148. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  149. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  150. package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
  151. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  152. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  153. package/gsd-core/workflows/quick.md +20 -390
  154. package/gsd-core/workflows/resume-project.md +3 -0
  155. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  156. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  157. package/gsd-core/workflows/review.md +15 -8
  158. package/gsd-core/workflows/section-manifest.json +219 -0
  159. package/gsd-core/workflows/sketch.md +1 -1
  160. package/gsd-core/workflows/spec-phase.md +17 -14
  161. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  162. package/gsd-core/workflows/spike.md +50 -16
  163. package/gsd-core/workflows/sync-skills.md +49 -11
  164. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  165. package/gsd-core/workflows/transition.md +8 -21
  166. package/gsd-core/workflows/ui-phase.md +8 -7
  167. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  168. package/gsd-core/workflows/update.md +18 -7
  169. package/gsd-core/workflows/verify-phase.md +4 -7
  170. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  171. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  172. package/gsd-core/workflows/verify-work.md +8 -58
  173. package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
  174. package/hooks/dist/gsd-check-update-worker.js +14 -5
  175. package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
  176. package/hooks/dist/gsd-read-injection-scanner.js +7 -0
  177. package/hooks/dist/gsd-statusline.js +72 -6
  178. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  179. package/hooks/dist/gsd-write-guard.js +359 -0
  180. package/hooks/dist/lib/isolation-sentinel.js +268 -0
  181. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  182. package/hooks/gsd-agent-isolation-guard.js +428 -0
  183. package/hooks/gsd-check-update-worker.js +14 -5
  184. package/hooks/gsd-cursor-subagent-start.js +532 -26
  185. package/hooks/gsd-read-injection-scanner.js +7 -0
  186. package/hooks/gsd-statusline.js +72 -6
  187. package/hooks/gsd-worktree-path-guard.js +2 -1
  188. package/hooks/gsd-write-guard.js +359 -0
  189. package/hooks/hooks.json +12 -0
  190. package/hooks/lib/isolation-sentinel.js +268 -0
  191. package/hooks/managed-hooks-registry.cjs +2 -0
  192. package/package.json +14 -5
  193. package/pi/gsd.cjs +57 -12
  194. package/scripts/build-hooks.js +9 -0
  195. package/scripts/changeset/lint.cjs +9 -2
  196. package/scripts/changeset/serialize.cjs +5 -1
  197. package/scripts/gen-capability-matrix.cjs +1 -1
  198. package/scripts/gen-context-index.cjs +448 -0
  199. package/scripts/gen-inventory-manifest.cjs +101 -1
  200. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  201. package/scripts/gen-section-manifest.cjs +638 -0
  202. package/scripts/generate-package-identity.cjs +4 -2
  203. package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
  204. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  205. package/scripts/lint-docs-command-form.cjs +195 -0
  206. package/scripts/lint-docs-required.cjs +9 -1
  207. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  208. package/scripts/lint-example-parser-parity.cjs +395 -0
  209. package/scripts/lint-test-file-count.allowlist.json +27 -1
  210. package/scripts/mutation-matrix.cjs +13 -0
  211. package/scripts/prompt-injection-scan.sh +27 -6
  212. package/scripts/run-tests.cjs +3 -2
  213. package/skills/gsd-autonomous/SKILL.md +1 -1
  214. package/skills/gsd-execute-phase/SKILL.md +1 -1
  215. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  216. package/skills/gsd-new-milestone/SKILL.md +1 -1
  217. package/skills/gsd-plan-phase/SKILL.md +2 -2
  218. package/vscode/package.json +1 -1
  219. package/scripts/gen-emitted-baseline.cjs +0 -145
@@ -61,6 +61,78 @@ function sha256Text(value) {
61
61
  * the correct outcome — refusing to proceed beats silently copying referent
62
62
  * bytes.
63
63
  */
64
+ /**
65
+ * Evaluate and, if safe, perform a `remove-empty-dir` action against `fullPath`.
66
+ *
67
+ * This is deliberately WEAKER than a recursive directory-removal primitive
68
+ * (which 003's docblock records as an intentional absence in the ADR-0008
69
+ * design): it only ever calls `fs.rmdirSync` — never `fs.rmSync`, never
70
+ * `{ recursive: true }`, never `{ force: true }` — so a non-empty directory
71
+ * fails the underlying syscall rather than being swept. The emptiness check
72
+ * immediately above the call is what turns that failure mode into a
73
+ * deliberate, non-error "left in place" outcome instead of surfacing ENOTEMPTY.
74
+ *
75
+ * Guards, in order:
76
+ * - lstat (not stat): a symlinked directory is refused outright, never
77
+ * followed. A missing target is reported distinctly so callers can tell
78
+ * "nothing was ever there" from "something was there and is left alone".
79
+ * - must actually be a directory (not a file masquerading under the relPath).
80
+ * - containment: the REALPATH of the target must resolve strictly inside the
81
+ * REALPATH of configDir — never equal to it (removing the config root
82
+ * itself is never in scope) and never escaping it (e.g. via an ancestor
83
+ * symlink the lstat check alone would not catch).
84
+ * - emptiness, re-checked here rather than trusted from planning time: a
85
+ * directory that still holds any entry (managed-but-undeleted, unknown,
86
+ * or created between plan and apply) is left in place. This is reported
87
+ * as 'skipped-not-empty', a successful no-op, not a failure.
88
+ *
89
+ * Any unexpected error along the way (EACCES, EBUSY, a race that removes the
90
+ * target between the lstat and the rmdir, etc.) degrades to 'left-in-place'.
91
+ * This action must never throw out of the executor, matching every sibling
92
+ * action type's failure posture.
93
+ */
94
+ function evaluateRemoveEmptyDir(configDir, fullPath) {
95
+ let stat;
96
+ try {
97
+ stat = node_fs_1.default.lstatSync(fullPath);
98
+ }
99
+ catch {
100
+ return 'missing';
101
+ }
102
+ if (stat.isSymbolicLink())
103
+ return 'left-in-place';
104
+ if (!stat.isDirectory())
105
+ return 'left-in-place';
106
+ let resolvedRoot;
107
+ let resolvedTarget;
108
+ try {
109
+ resolvedRoot = node_fs_1.default.realpathSync(configDir);
110
+ resolvedTarget = node_fs_1.default.realpathSync(fullPath);
111
+ }
112
+ catch {
113
+ return 'left-in-place';
114
+ }
115
+ if (resolvedTarget === resolvedRoot || !resolvedTarget.startsWith(resolvedRoot + node_path_1.default.sep)) {
116
+ // Refuses both "target IS configDir" and "target escaped configDir".
117
+ return 'left-in-place';
118
+ }
119
+ let entries;
120
+ try {
121
+ entries = node_fs_1.default.readdirSync(fullPath);
122
+ }
123
+ catch {
124
+ return 'left-in-place';
125
+ }
126
+ if (entries.length > 0)
127
+ return 'skipped-not-empty';
128
+ try {
129
+ node_fs_1.default.rmdirSync(fullPath);
130
+ return 'removed';
131
+ }
132
+ catch {
133
+ return 'left-in-place';
134
+ }
135
+ }
64
136
  function copyPreservingSymlink(srcPath, destPath) {
65
137
  if (node_fs_1.default.lstatSync(srcPath).isSymbolicLink()) {
66
138
  // symlinkSync fails with EEXIST on an occupied path, so clear it first.
@@ -643,7 +715,8 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
643
715
  action.type !== 'backup-and-remove' &&
644
716
  action.type !== 'rewrite-json' &&
645
717
  action.type !== 'record-baseline' &&
646
- action.type !== 'baseline-preserve-user') {
718
+ action.type !== 'baseline-preserve-user' &&
719
+ action.type !== 'remove-empty-dir') {
647
720
  throw new Error(`unsupported migration action type: ${action.type}`);
648
721
  }
649
722
  const { normalized, fullPath } = ensureInsideConfig(configDir, action.relPath);
@@ -655,6 +728,18 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
655
728
  journal.actions.push(journalAction(action, action.type === 'record-baseline' ? 'recorded' : 'preserved'));
656
729
  continue;
657
730
  }
731
+ if (action.type === 'remove-empty-dir') {
732
+ // Directory actions never enter the file-copy/rollback machinery below:
733
+ // there is nothing to snapshot-and-restore for a directory node itself
734
+ // (its former CONTENTS were already snapshotted by their own file-level
735
+ // actions before this one runs), and rollback of a removed empty
736
+ // directory is simply re-creating it, which the rollback path below
737
+ // does not model. Non-recursive by construction (evaluateRemoveEmptyDir
738
+ // only ever calls fs.rmdirSync), so there is nothing destructive to undo
739
+ // beyond an mkdir the next install/migration run will happily redo.
740
+ journal.actions.push(journalAction(action, evaluateRemoveEmptyDir(configDir, fullPath)));
741
+ continue;
742
+ }
658
743
  const rollbackPath = node_path_1.default.join(rollbackRoot, normalized);
659
744
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(rollbackPath), { recursive: true });
660
745
  copyPreservingSymlink(fullPath, rollbackPath);
@@ -853,6 +938,7 @@ module.exports = {
853
938
  applyInstallerMigrationPlan,
854
939
  classifyArtifact,
855
940
  discoverInstallerMigrations,
941
+ evaluateRemoveEmptyDir,
856
942
  migrationChecksum,
857
943
  planInstallerMigrations,
858
944
  readInstallManifest,
@@ -118,6 +118,22 @@ function writeAllSync(fd, data) {
118
118
  }
119
119
  }
120
120
  }
121
+ /**
122
+ * The wire form of a JSON result: the exact bytes `output()` emits for it.
123
+ *
124
+ * Exported because a caller that has to reason about the size of its own
125
+ * response — graphify's `--budget` accounting (#2738) — must measure the string
126
+ * this function produces rather than a second, privately-maintained
127
+ * serialization that can drift from it. One definition, so estimator and
128
+ * emitter cannot disagree about indentation or shape.
129
+ *
130
+ * The `@file:` redirection in `output()` is a transport detail, not a payload
131
+ * change: the caller still consumes these bytes, so these are the right ones to
132
+ * measure.
133
+ */
134
+ function serializeForOutput(result) {
135
+ return JSON.stringify(result, null, 2);
136
+ }
121
137
  function output(result, raw, rawValue) {
122
138
  let data;
123
139
  if (raw && rawValue !== undefined) {
@@ -125,7 +141,7 @@ function output(result, raw, rawValue) {
125
141
  data = String(rawValue);
126
142
  }
127
143
  else {
128
- const json = JSON.stringify(result, null, 2);
144
+ const json = serializeForOutput(result);
129
145
  // Large payloads exceed Claude Code's Bash tool buffer (~50KB).
130
146
  // Write to tmpfile and output the path prefixed with @file: so callers can detect it.
131
147
  if (json.length > 50000) {
@@ -168,6 +184,7 @@ const ERROR_REASON = Object.freeze({
168
184
  // workflow / phase
169
185
  PHASE_NOT_FOUND: 'phase_not_found',
170
186
  PHASE_VERIFICATION_INCOMPLETE: 'phase_verification_incomplete',
187
+ PHASE_PLAN_COVERAGE_INCOMPLETE: 'phase_plan_coverage_incomplete',
171
188
  SUMMARY_NO_PLANNING: 'summary_no_planning',
172
189
  // graphify
173
190
  GRAPHIFY_NO_GRAPH: 'graphify_no_graph',
@@ -198,10 +215,17 @@ function getJsonErrorMode() { return _jsonErrorMode; }
198
215
  * process is in JSON-error mode, stderr receives `{ ok: false, reason,
199
216
  * message }` so callers can parse it; otherwise stderr keeps the plain
200
217
  * text form for human operators.
218
+ *
219
+ * `extra` (optional) lets a caller attach additional structured fields
220
+ * (e.g. `{ verification_stale_check_indeterminate: true }`) onto the
221
+ * JSON-error-mode payload, spread alongside `ok`/`reason`/`message`, so a
222
+ * test can assert on the value directly instead of regexing `message`'s
223
+ * human-readable text. Ignored entirely in plain-text mode — the human
224
+ * message is the only thing an operator sees there.
201
225
  */
202
- function error(message, reason = ERROR_REASON.UNKNOWN) {
226
+ function error(message, reason = ERROR_REASON.UNKNOWN, extra) {
203
227
  if (_jsonErrorMode) {
204
- const payload = JSON.stringify({ ok: false, reason, message }) + '\n';
228
+ const payload = JSON.stringify({ ok: false, reason, message, ...(extra || {}) }) + '\n';
205
229
  writeAllSync(2, payload);
206
230
  }
207
231
  else {
@@ -214,6 +238,7 @@ module.exports = {
214
238
  ensureGsdTempDir,
215
239
  reapStaleTempFiles,
216
240
  output,
241
+ serializeForOutput,
217
242
  ERROR_REASON,
218
243
  setJsonErrorMode,
219
244
  getJsonErrorMode,
@@ -14,6 +14,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.stripFencedCode = stripFencedCode;
15
15
  exports.stripInlineCode = stripInlineCode;
16
16
  exports.scanInlineCodeSpans = scanInlineCodeSpans;
17
+ exports.scanFencedBlocks = scanFencedBlocks;
17
18
  exports.extractFencedBlock = extractFencedBlock;
18
19
  exports.tokenizeHeadings = tokenizeHeadings;
19
20
  exports.collectSections = collectSections;
@@ -206,6 +207,11 @@ function stripInlineCodeLine(line) {
206
207
  * Tracked duplication (same status as `tokenizeHeadings`'s copy, see its
207
208
  * comment above): this is a second independent copy of the fence state
208
209
  * machine, pending a T-tier consolidation.
210
+ *
211
+ * Exported so `context-predicates.cts` can consume this seam directly for its
212
+ * line-preserving fenced-line skip detection, instead of carrying a third
213
+ * independent copy of the fence state machine (see that module's doc
214
+ * comment).
209
215
  */
210
216
  function scanFencedBlocks(lines) {
211
217
  const delimRe = /^( {0,3})(`{3,}|~{3,})(.*)$/;