@opengsd/gsd-core 1.7.0 → 1.9.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 (261) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +45 -1
  4. package/README.md +2 -0
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-codebase-mapper.md +1 -1
  7. package/agents/gsd-debug-session-manager.md +78 -4
  8. package/agents/gsd-debugger.md +87 -29
  9. package/agents/gsd-executor.md +49 -9
  10. package/agents/gsd-intel-updater.md +3 -3
  11. package/agents/gsd-phase-researcher.md +4 -2
  12. package/agents/gsd-plan-checker.md +20 -0
  13. package/agents/gsd-planner.md +44 -59
  14. package/agents/gsd-project-researcher.md +2 -2
  15. package/agents/gsd-ui-auditor.md +0 -40
  16. package/agents/gsd-verifier.md +2 -2
  17. package/bin/install.js +1338 -135
  18. package/commands/gsd/ai-integration-phase.md +1 -1
  19. package/commands/gsd/mempalace-capture.md +9 -5
  20. package/commands/gsd/new-milestone.md +1 -1
  21. package/commands/gsd/plan-phase.md +5 -3
  22. package/commands/gsd/plan-review-convergence.md +7 -2
  23. package/gsd-core/bin/gsd-tools.cjs +2690 -2472
  24. package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
  25. package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
  26. package/gsd-core/bin/lib/api-coverage.cjs +360 -53
  27. package/gsd-core/bin/lib/audit.cjs +8 -8
  28. package/gsd-core/bin/lib/broken-windows.cjs +716 -0
  29. package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
  30. package/gsd-core/bin/lib/capability-consent.cjs +40 -1
  31. package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
  32. package/gsd-core/bin/lib/capability-loader.cjs +23 -1
  33. package/gsd-core/bin/lib/capability-registry.cjs +1450 -160
  34. package/gsd-core/bin/lib/capability-trust.cjs +468 -33
  35. package/gsd-core/bin/lib/capability-validator.cjs +882 -6
  36. package/gsd-core/bin/lib/capability-writer.cjs +6 -1
  37. package/gsd-core/bin/lib/check-command-router.cjs +140 -27
  38. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
  39. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +209 -31
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +203 -25
  41. package/gsd-core/bin/lib/command-aliases.cjs +14 -0
  42. package/gsd-core/bin/lib/commands.cjs +326 -21
  43. package/gsd-core/bin/lib/config-loader.cjs +214 -30
  44. package/gsd-core/bin/lib/config.cjs +158 -22
  45. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  46. package/gsd-core/bin/lib/decisions.cjs +32 -8
  47. package/gsd-core/bin/lib/docs.cjs +6 -0
  48. package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
  49. package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
  50. package/gsd-core/bin/lib/frontmatter.cjs +125 -15
  51. package/gsd-core/bin/lib/gap-checker.cjs +17 -2
  52. package/gsd-core/bin/lib/host-integration.cjs +215 -8
  53. package/gsd-core/bin/lib/init.cjs +155 -66
  54. package/gsd-core/bin/lib/install-engine.cjs +299 -23
  55. package/gsd-core/bin/lib/install-profiles.cjs +239 -1
  56. package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
  57. package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
  58. package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
  59. package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
  60. package/gsd-core/bin/lib/milestone.cjs +248 -14
  61. package/gsd-core/bin/lib/model-catalog.cjs +69 -4
  62. package/gsd-core/bin/lib/model-resolver.cjs +189 -7
  63. package/gsd-core/bin/lib/observability/logger.cjs +7 -2
  64. package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
  65. package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
  66. package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
  67. package/gsd-core/bin/lib/phase-id.cjs +304 -9
  68. package/gsd-core/bin/lib/phase.cjs +258 -17
  69. package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
  70. package/gsd-core/bin/lib/plan-scan.cjs +70 -2
  71. package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
  72. package/gsd-core/bin/lib/profile-output.cjs +34 -8
  73. package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
  74. package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
  75. package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
  76. package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
  77. package/gsd-core/bin/lib/roadmap-parser.cjs +61 -10
  78. package/gsd-core/bin/lib/roadmap.cjs +23 -7
  79. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +38 -5
  80. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +23 -9
  81. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +156 -0
  82. package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
  83. package/gsd-core/bin/lib/smart-entry.cjs +70 -5
  84. package/gsd-core/bin/lib/state-document.cjs +171 -24
  85. package/gsd-core/bin/lib/state-transition.cjs +50 -11
  86. package/gsd-core/bin/lib/state.cjs +206 -32
  87. package/gsd-core/bin/lib/surface.cjs +51 -9
  88. package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
  89. package/gsd-core/bin/lib/uat.cjs +428 -11
  90. package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
  91. package/gsd-core/bin/lib/unusable-input.cjs +216 -0
  92. package/gsd-core/bin/lib/validate.cjs +44 -8
  93. package/gsd-core/bin/lib/verification.cjs +163 -31
  94. package/gsd-core/bin/lib/verify.cjs +348 -42
  95. package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
  96. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  97. package/gsd-core/bin/shared/config-schema.manifest.json +4 -15
  98. package/gsd-core/bin/shared/model-catalog.json +5 -0
  99. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
  100. package/gsd-core/references/api-coverage.md +37 -7
  101. package/gsd-core/references/checkpoints.md +1 -1
  102. package/gsd-core/references/common-bug-patterns.md +13 -0
  103. package/gsd-core/references/context-budget.md +40 -0
  104. package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
  105. package/gsd-core/references/debugger-fix-acceptance.md +157 -0
  106. package/gsd-core/references/debugger-philosophy.md +1 -0
  107. package/gsd-core/references/debugger-prevention.md +98 -0
  108. package/gsd-core/references/debugger-rca-branching.md +98 -0
  109. package/gsd-core/references/debugger-repro-hardening.md +130 -0
  110. package/gsd-core/references/debugger-sbfl.md +110 -0
  111. package/gsd-core/references/debugger-semantic-recall.md +81 -0
  112. package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
  113. package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
  114. package/gsd-core/references/execute-phase-response-language.md +7 -0
  115. package/gsd-core/references/gate-prompts.md +6 -3
  116. package/gsd-core/references/model-profile-resolution.md +64 -13
  117. package/gsd-core/references/offer-next.md +88 -0
  118. package/gsd-core/references/planner-antipatterns.md +6 -0
  119. package/gsd-core/references/planner-mvp-mode.md +12 -13
  120. package/gsd-core/references/planner-preconditions.md +156 -0
  121. package/gsd-core/references/planner-reversibility.md +132 -0
  122. package/gsd-core/references/planning-config.md +2 -1
  123. package/gsd-core/references/reviewer-instances.md +28 -19
  124. package/gsd-core/references/runtime-aware-dispatch.md +42 -0
  125. package/gsd-core/references/skeleton-template.md +1 -1
  126. package/gsd-core/references/thinking-models-planning.md +3 -1
  127. package/gsd-core/references/ui-consideration-probe.md +2 -2
  128. package/gsd-core/references/worktree-branch-check.md +4 -4
  129. package/gsd-core/templates/DEBUG.md +5 -3
  130. package/gsd-core/templates/summary-minimal.md +4 -0
  131. package/gsd-core/templates/summary-standard.md +4 -0
  132. package/gsd-core/templates/summary.md +7 -0
  133. package/gsd-core/workflows/add-phase.md +2 -0
  134. package/gsd-core/workflows/add-tests.md +3 -1
  135. package/gsd-core/workflows/add-todo.md +32 -1
  136. package/gsd-core/workflows/ai-integration-phase.md +8 -6
  137. package/gsd-core/workflows/audit-fix.md +6 -2
  138. package/gsd-core/workflows/audit-milestone.md +8 -0
  139. package/gsd-core/workflows/autonomous.md +19 -15
  140. package/gsd-core/workflows/check-todos.md +5 -3
  141. package/gsd-core/workflows/cleanup.md +7 -1
  142. package/gsd-core/workflows/code-review-fix.md +14 -6
  143. package/gsd-core/workflows/code-review.md +93 -24
  144. package/gsd-core/workflows/complete-milestone.md +3 -0
  145. package/gsd-core/workflows/debug.md +35 -7
  146. package/gsd-core/workflows/diagnose-issues.md +5 -1
  147. package/gsd-core/workflows/discovery-phase.md +7 -0
  148. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
  149. package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
  150. package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
  151. package/gsd-core/workflows/discuss-phase-assumptions.md +18 -9
  152. package/gsd-core/workflows/discuss-phase.md +2 -2
  153. package/gsd-core/workflows/do.md +7 -1
  154. package/gsd-core/workflows/docs-update.md +9 -0
  155. package/gsd-core/workflows/eval-review.md +4 -1
  156. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
  157. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
  158. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
  159. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
  160. package/gsd-core/workflows/execute-phase.md +110 -149
  161. package/gsd-core/workflows/execute-plan.md +20 -8
  162. package/gsd-core/workflows/explore.md +4 -0
  163. package/gsd-core/workflows/extract-learnings.md +21 -0
  164. package/gsd-core/workflows/graduation.md +3 -0
  165. package/gsd-core/workflows/health.md +7 -1
  166. package/gsd-core/workflows/help/modes/full.md +9 -5
  167. package/gsd-core/workflows/import.md +11 -2
  168. package/gsd-core/workflows/inbox.md +7 -0
  169. package/gsd-core/workflows/ingest-docs.md +19 -10
  170. package/gsd-core/workflows/manager.md +3 -1
  171. package/gsd-core/workflows/map-codebase.md +17 -10
  172. package/gsd-core/workflows/mvp-phase.md +3 -0
  173. package/gsd-core/workflows/new-milestone.md +79 -23
  174. package/gsd-core/workflows/new-project.md +28 -19
  175. package/gsd-core/workflows/new-workspace.md +3 -1
  176. package/gsd-core/workflows/next.md +5 -2
  177. package/gsd-core/workflows/onboard.md +3 -0
  178. package/gsd-core/workflows/plan-phase.md +56 -51
  179. package/gsd-core/workflows/plan-review-convergence.md +61 -12
  180. package/gsd-core/workflows/plant-seed.md +3 -0
  181. package/gsd-core/workflows/profile-user.md +7 -1
  182. package/gsd-core/workflows/progress.md +31 -3
  183. package/gsd-core/workflows/quick.md +33 -10
  184. package/gsd-core/workflows/remove-workspace.md +3 -0
  185. package/gsd-core/workflows/review.md +172 -585
  186. package/gsd-core/workflows/scan.md +10 -2
  187. package/gsd-core/workflows/secure-phase.md +13 -2
  188. package/gsd-core/workflows/settings-integrations.md +3 -0
  189. package/gsd-core/workflows/settings.md +3 -0
  190. package/gsd-core/workflows/ship.md +88 -11
  191. package/gsd-core/workflows/sketch.md +3 -0
  192. package/gsd-core/workflows/smart-entry.md +4 -1
  193. package/gsd-core/workflows/spike.md +7 -1
  194. package/gsd-core/workflows/ui-phase.md +11 -2
  195. package/gsd-core/workflows/ui-review.md +11 -1
  196. package/gsd-core/workflows/undo.md +7 -0
  197. package/gsd-core/workflows/update.md +106 -5
  198. package/gsd-core/workflows/validate-phase.md +13 -2
  199. package/gsd-core/workflows/verify-phase.md +2 -2
  200. package/gsd-core/workflows/verify-work.md +15 -4
  201. package/hooks/dist/gsd-context-monitor.js +27 -9
  202. package/hooks/dist/gsd-cursor-session-start.js +6 -2
  203. package/hooks/dist/gsd-cursor-stop.js +6 -2
  204. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  205. package/hooks/dist/gsd-graphify-update.sh +9 -0
  206. package/hooks/dist/gsd-phase-boundary.sh +14 -2
  207. package/hooks/dist/gsd-prompt-guard.js +101 -2
  208. package/hooks/dist/gsd-read-guard.js +100 -2
  209. package/hooks/dist/gsd-read-injection-scanner.js +109 -2
  210. package/hooks/dist/gsd-statusline.js +97 -9
  211. package/hooks/dist/gsd-workflow-guard.js +110 -6
  212. package/hooks/dist/gsd-worktree-path-guard.js +132 -8
  213. package/hooks/dist/lib/cursor-workspace.js +74 -0
  214. package/hooks/gsd-context-monitor.js +27 -9
  215. package/hooks/gsd-cursor-session-start.js +6 -2
  216. package/hooks/gsd-cursor-stop.js +6 -2
  217. package/hooks/gsd-cursor-subagent-start.js +6 -2
  218. package/hooks/gsd-graphify-update.sh +9 -0
  219. package/hooks/gsd-phase-boundary.sh +14 -2
  220. package/hooks/gsd-prompt-guard.js +101 -2
  221. package/hooks/gsd-read-guard.js +100 -2
  222. package/hooks/gsd-read-injection-scanner.js +109 -2
  223. package/hooks/gsd-statusline.js +97 -9
  224. package/hooks/gsd-workflow-guard.js +110 -6
  225. package/hooks/gsd-worktree-path-guard.js +132 -8
  226. package/hooks/lib/cursor-workspace.js +74 -0
  227. package/package.json +10 -8
  228. package/pi/gsd.cjs +34 -3
  229. package/scripts/changeset/lint.cjs +1 -0
  230. package/scripts/changeset/parse.cjs +26 -0
  231. package/scripts/check-coverage-gate.cjs +51 -0
  232. package/scripts/check-glossary-refs.cjs +244 -0
  233. package/scripts/ci-rebase-check.cjs +48 -4
  234. package/scripts/ci-test-scope.cjs +67 -17
  235. package/scripts/gen-adr-index.cjs +528 -0
  236. package/scripts/gen-capability-matrix.cjs +26 -2
  237. package/scripts/gen-capability-registry.cjs +132 -34
  238. package/scripts/gen-emitted-baseline.cjs +145 -0
  239. package/scripts/gen-test-timings.cjs +201 -0
  240. package/scripts/lint-compiled-artifact-sync.cjs +146 -0
  241. package/scripts/lint-emitted-drift-ack.cjs +149 -0
  242. package/scripts/lint-fix-has-regression-test.cjs +131 -0
  243. package/scripts/lint-portable-timeout.cjs +140 -0
  244. package/scripts/lint-resolution-provenance.cjs +9 -0
  245. package/scripts/lint-test-file-count.allowlist.json +1 -0
  246. package/scripts/mutation-matrix.cjs +4 -0
  247. package/scripts/prompt-injection-scan.sh +6 -0
  248. package/scripts/registry-schema.cjs +57 -8
  249. package/scripts/release-notes/conventional-title.cjs +19 -1
  250. package/scripts/release-notes/format-github-release-notes.cjs +7 -3
  251. package/scripts/release-tarball-smoke.cjs +18 -11
  252. package/scripts/run-tests.cjs +420 -58
  253. package/scripts/workflow-size.cjs +16 -8
  254. package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
  255. package/skills/gsd-mempalace-capture/SKILL.md +9 -5
  256. package/skills/gsd-new-milestone/SKILL.md +1 -1
  257. package/skills/gsd-plan-phase/SKILL.md +5 -3
  258. package/skills/gsd-plan-review-convergence/SKILL.md +7 -2
  259. package/vscode/package.json +1 -1
  260. package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
  261. package/scripts/update-size-baseline.cjs +0 -68
@@ -12,6 +12,8 @@
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.stripFencedCode = stripFencedCode;
15
+ exports.stripInlineCode = stripInlineCode;
16
+ exports.scanInlineCodeSpans = scanInlineCodeSpans;
15
17
  exports.extractFencedBlock = extractFencedBlock;
16
18
  exports.tokenizeHeadings = tokenizeHeadings;
17
19
  exports.collectSections = collectSections;
@@ -84,6 +86,111 @@ function stripFencedCode(content) {
84
86
  }
85
87
  return { text: kept.join('\n'), unterminatedFence: openFence !== null };
86
88
  }
89
+ // ─── stripInlineCode ──────────────────────────────────────────────────────────
90
+ /**
91
+ * Remove CommonMark inline code spans (§6.1) from prose, line by line.
92
+ *
93
+ * A span opens with a run of N backticks and closes at the next run of EXACTLY
94
+ * N backticks on the same line (a longer or shorter run is span content, per
95
+ * CommonMark). The whole span — delimiters and content — is replaced by a
96
+ * single space so the surrounding words do not join. A run with no matching
97
+ * closer is literal text and is kept. Spans never cross line boundaries here:
98
+ * multi-line code in planning prose is fenced-block territory
99
+ * (`stripFencedCode`).
100
+ *
101
+ * Companion to `stripFencedCode` for term-matching callers (#2365): strip
102
+ * fenced blocks first, then inline spans, so a trigger term inside backticks
103
+ * is code, not prose evidence.
104
+ */
105
+ function stripInlineCode(content) {
106
+ if (typeof content !== 'string' || content.length === 0)
107
+ return '';
108
+ return content.split('\n').map(stripInlineCodeLine).join('\n');
109
+ }
110
+ /**
111
+ * Locate every inline code span in `content`, per line (offsets are into the
112
+ * full string; spans never cross a `\n`). Callers that need the span CONTENT
113
+ * (e.g. api-coverage's dependency-evidence scan, #2365) use this; callers that
114
+ * just want spans gone use `stripInlineCode`.
115
+ */
116
+ function scanInlineCodeSpans(content) {
117
+ if (typeof content !== 'string' || content.length === 0)
118
+ return [];
119
+ const out = [];
120
+ let lineStart = 0;
121
+ for (const line of content.split('\n')) {
122
+ for (const s of scanSpansInLine(line)) {
123
+ out.push({ start: lineStart + s.start, end: lineStart + s.end, content: s.content });
124
+ }
125
+ lineStart += line.length + 1;
126
+ }
127
+ return out;
128
+ }
129
+ function scanSpansInLine(line) {
130
+ const spans = [];
131
+ if (line.indexOf('`') === -1)
132
+ return spans;
133
+ // Collect the maximal backtick RUNS once, then match openers to closers using
134
+ // a per-length forward cursor. A naive "search the rest of the line for the
135
+ // closer" loop is O(n²) on a line of many unmatched increasing-length runs
136
+ // (#2365 review 9); precomputing runs makes the whole scan linear while
137
+ // preserving CommonMark semantics (closer = next run of EXACTLY the same len).
138
+ const runs = [];
139
+ for (let i = 0; i < line.length;) {
140
+ if (line[i] === '`') {
141
+ let n = 1;
142
+ while (i + n < line.length && line[i + n] === '`')
143
+ n++;
144
+ runs.push([i, n]);
145
+ i += n;
146
+ }
147
+ else {
148
+ i++;
149
+ }
150
+ }
151
+ const runsByLen = new Map();
152
+ for (let k = 0; k < runs.length; k++) {
153
+ const len = runs[k][1];
154
+ const arr = runsByLen.get(len);
155
+ if (arr)
156
+ arr.push(k);
157
+ else
158
+ runsByLen.set(len, [k]);
159
+ }
160
+ const cursorByLen = new Map();
161
+ let k = 0;
162
+ while (k < runs.length) {
163
+ const [openPos, n] = runs[k];
164
+ const candidates = runsByLen.get(n); // n came from this map, always present
165
+ let ci = cursorByLen.get(n) ?? 0;
166
+ while (ci < candidates.length && candidates[ci] <= k)
167
+ ci++;
168
+ if (ci < candidates.length) {
169
+ const closeK = candidates[ci];
170
+ const closePos = runs[closeK][0];
171
+ spans.push({ start: openPos, end: closePos + n, content: line.slice(openPos + n, closePos) });
172
+ cursorByLen.set(n, ci + 1);
173
+ k = closeK + 1; // resume after the closer — runs inside the span are code
174
+ }
175
+ else {
176
+ cursorByLen.set(n, ci);
177
+ k++; // unmatched run → literal text, next run is a fresh opener
178
+ }
179
+ }
180
+ return spans;
181
+ }
182
+ function stripInlineCodeLine(line) {
183
+ const spans = scanSpansInLine(line);
184
+ if (spans.length === 0)
185
+ return line;
186
+ let out = '';
187
+ let prev = 0;
188
+ for (const s of spans) {
189
+ out += line.slice(prev, s.start) + ' ';
190
+ prev = s.end;
191
+ }
192
+ return out + line.slice(prev);
193
+ }
87
194
  /**
88
195
  * Shared low-level fence-scanning engine. Walks `lines` and returns every
89
196
  * fenced block found, applying the EXACT SAME CommonMark delimiter rules as
@@ -38,6 +38,14 @@ const { extractOneLinerFromBody } = coreUtilsMod;
38
38
  const { planningPaths } = planningWorkspace;
39
39
  const { extractFrontmatter } = frontmatterMod;
40
40
  const { writeStateMd } = stateMod;
41
+ // #2288 security: a milestone version label becomes a filesystem directory
42
+ // component (`milestones/<label>-phases/`) into which phase directories are
43
+ // MOVED. Any label used as a path segment must be a safe version token —
44
+ // letters/digits/'.'/'-'/'_', leading alphanumeric, no path separators and no
45
+ // `..` (the leading-alphanumeric anchor rejects a bare `..`). This gates both
46
+ // the caller-supplied `--archive-version` override and the STATE.md-derived
47
+ // live-read value, so a crafted value cannot escape `.planning/milestones/`.
48
+ const ARCHIVE_VERSION_LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
41
49
  /**
42
50
  * Scope an `updateTableCell` call to the `## Traceability` (or
43
51
  * `## Traceability Status`) heading's own section — up to the next H1/H2
@@ -229,10 +237,179 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
229
237
  write_set_complete: (0, write_set_cjs_1.writeSetComplete)(writeSet),
230
238
  }, raw, `${updated.length}/${reqIds.length} requirements marked complete`);
231
239
  }
240
+ /**
241
+ * #2388: a requirement ID shared by more than one plan in a phase must not be
242
+ * handed to `cmdRequirementsMarkComplete` until every plan declaring it has
243
+ * finished (i.e. has a `*-SUMMARY.md`) — otherwise the ID reads `Complete` in
244
+ * REQUIREMENTS.md ~20 minutes before its sibling plans even run, and long
245
+ * before `verify_phase_goal` has a chance to catch a gap.
246
+ *
247
+ * Pure read-only gate: scans sibling `*-PLAN.md` files in the SAME phase
248
+ * directory as `planPath` (excluding `planPath` itself) and, for each
249
+ * candidate ID, blocks it only when a sibling plan ALSO declares that ID in
250
+ * its own `requirements:` frontmatter AND that sibling has no matching
251
+ * `*-SUMMARY.md` yet. An ID no sibling declares is never blocked — a
252
+ * single-plan (non-shared) ID is always `ready`, preserving immediate
253
+ * marking with no added latency (acceptance criterion 4). Does not read or
254
+ * write REQUIREMENTS.md itself; callers pass the `ready` subset on to
255
+ * `cmdRequirementsMarkComplete` (whose own flip semantics are untouched).
256
+ */
257
+ function cmdRequirementsReadyIds(cwd, args, raw) {
258
+ const planPathArg = args[0];
259
+ if (!planPathArg) {
260
+ error('plan path required. Usage: requirements ready-ids <plan-path> REQ-01,REQ-02');
261
+ }
262
+ const reqIds = args
263
+ .slice(1)
264
+ .join(' ')
265
+ .replace(/[\[\]]/g, '')
266
+ .split(/[,\s]+/)
267
+ .map((r) => r.trim())
268
+ .filter(Boolean);
269
+ if (reqIds.length === 0) {
270
+ output({ ready: [], blocked: [], total: 0 }, raw, 'no requirement IDs provided');
271
+ return;
272
+ }
273
+ const planAbsPath = node_path_1.default.resolve(cwd, planPathArg);
274
+ const phaseDir = node_path_1.default.dirname(planAbsPath);
275
+ const currentBasename = node_path_1.default.basename(planAbsPath);
276
+ let siblingPlanFiles = [];
277
+ try {
278
+ siblingPlanFiles = node_fs_1.default
279
+ .readdirSync(phaseDir)
280
+ .filter((f) => f.endsWith('-PLAN.md') && f !== currentBasename);
281
+ }
282
+ catch {
283
+ siblingPlanFiles = [];
284
+ }
285
+ const parseFrontmatterReqIds = (content, sourcePath) => {
286
+ const fm = extractFrontmatter(content, sourcePath);
287
+ const fmReq = fm.requirements;
288
+ if (Array.isArray(fmReq))
289
+ return fmReq.map((r) => String(r).trim()).filter(Boolean);
290
+ if (typeof fmReq === 'string') {
291
+ return fmReq
292
+ .replace(/[\[\]]/g, '')
293
+ .split(/[,\s]+/)
294
+ .map((r) => r.trim())
295
+ .filter(Boolean);
296
+ }
297
+ return [];
298
+ };
299
+ const ready = [];
300
+ const blocked = [];
301
+ for (const reqId of reqIds) {
302
+ let blockedBySibling = false;
303
+ for (const siblingFile of siblingPlanFiles) {
304
+ const siblingPath = node_path_1.default.join(phaseDir, siblingFile);
305
+ let siblingContent;
306
+ try {
307
+ siblingContent = node_fs_1.default.readFileSync(siblingPath, 'utf-8');
308
+ }
309
+ catch {
310
+ continue;
311
+ }
312
+ const siblingReqIds = parseFrontmatterReqIds(siblingContent, siblingPath);
313
+ const siblingDeclaresId = siblingReqIds.some((id) => id.toLowerCase() === reqId.toLowerCase());
314
+ if (!siblingDeclaresId)
315
+ continue;
316
+ // Sibling declares the SAME ID — it must have finished (produced a
317
+ // SUMMARY) before this ID is ready to mark Complete.
318
+ const siblingSummaryPath = siblingPath.replace(/-PLAN\.md$/, '-SUMMARY.md');
319
+ if (!node_fs_1.default.existsSync(siblingSummaryPath)) {
320
+ blockedBySibling = true;
321
+ break;
322
+ }
323
+ }
324
+ if (blockedBySibling)
325
+ blocked.push(reqId);
326
+ else
327
+ ready.push(reqId);
328
+ }
329
+ output({ ready, blocked, total: reqIds.length }, raw, `${ready.length}/${reqIds.length} requirement(s) ready to mark complete`);
330
+ }
331
+ /**
332
+ * #2388: revert this phase's own requirement IDs out of `Complete` when
333
+ * `verify_phase_goal` returns `gaps_found` — a gap verdict must not leave a
334
+ * premature `Complete` (from a shared ID's first-declaring plan, or from any
335
+ * other early write) sitting in REQUIREMENTS.md indefinitely.
336
+ *
337
+ * Mirrors `cmdRequirementsMarkComplete`'s two write surfaces in reverse:
338
+ * checkbox `[x]` -> `[ ]`, and traceability Status `Complete` -> `Gaps
339
+ * Found`. Phase-scoping is the CALLER's responsibility — this function only
340
+ * ever touches the exact IDs it is given, so a caller passing just this
341
+ * phase's own `phase_req_ids` never touches another phase's `Complete` row.
342
+ * Never call this on the pass path; it is `gaps_found`-only.
343
+ */
344
+ function cmdRequirementsRevertPhase(cwd, reqIdsRaw, raw) {
345
+ const reqIds = (reqIdsRaw || [])
346
+ .join(' ')
347
+ .replace(/[\[\]]/g, '')
348
+ .split(/[,\s]+/)
349
+ .map((r) => r.trim())
350
+ .filter(Boolean);
351
+ if (reqIds.length === 0) {
352
+ output({ reverted: [], unchanged: [], total: 0 }, raw, 'no requirement IDs provided');
353
+ return;
354
+ }
355
+ const reqPath = planningPaths(cwd).requirements;
356
+ if (!node_fs_1.default.existsSync(reqPath)) {
357
+ output({ reverted: [], unchanged: reqIds, total: reqIds.length, reason: 'REQUIREMENTS.md not found' }, raw, 'no requirements file');
358
+ return;
359
+ }
360
+ let reqContent = node_fs_1.default.readFileSync(reqPath, 'utf-8');
361
+ const reverted = [];
362
+ const unchanged = [];
363
+ for (const reqId of reqIds) {
364
+ const reqEscaped = escapeRegex(reqId);
365
+ let idReverted = false;
366
+ // Surface 1 — checkbox: - [x] **REQ-ID** -> - [ ] **REQ-ID**
367
+ const checkboxPattern = new RegExp(`(-\\s*\\[)x(\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi');
368
+ const afterCheckbox = reqContent.replace(checkboxPattern, '$1 $2');
369
+ if (afterCheckbox !== reqContent) {
370
+ reqContent = afterCheckbox;
371
+ idReverted = true;
372
+ }
373
+ // Surface 2 — traceability row: Status Complete -> Gaps Found. Only
374
+ // flips a row currently reading Complete (mirrors mark-complete's own
375
+ // "only flip Pending -> Complete" gate, in reverse).
376
+ const rowMatch = (row) => (Object.values(row)[0] ?? '').trim().toLowerCase() === reqId.toLowerCase();
377
+ let tableHit = false;
378
+ const tableUpdate = updateTraceabilityCell(reqContent, rowMatch, 'Status', (current) => {
379
+ if (/^complete$/i.test(current.trim())) {
380
+ tableHit = true;
381
+ return ' Gaps Found ';
382
+ }
383
+ return current;
384
+ });
385
+ if (tableUpdate.ok) {
386
+ reqContent = tableUpdate.value;
387
+ if (tableHit)
388
+ idReverted = true;
389
+ }
390
+ if (idReverted)
391
+ reverted.push(reqId);
392
+ else
393
+ unchanged.push(reqId);
394
+ }
395
+ if (reverted.length > 0) {
396
+ (0, shell_command_projection_cjs_1.platformWriteSync)(reqPath, reqContent);
397
+ }
398
+ output({ reverted, unchanged, total: reqIds.length }, raw, `${reverted.length}/${reqIds.length} requirement(s) reverted from Complete`);
399
+ }
232
400
  function cmdMilestoneComplete(cwd, version, options, raw) {
233
401
  if (!version) {
234
402
  error('version required for milestone complete (e.g., v1.0)');
235
403
  }
404
+ // #2288 security: `version` is a CLI positional that is interpolated into
405
+ // multiple filesystem sinks below — `path.join(archiveDir, `${version}-ROADMAP.md`)`,
406
+ // `${version}-REQUIREMENTS.md`, `${version}-MILESTONE-AUDIT.md`, and the
407
+ // `${version}-phases` archive directory that phase dirs are MOVED into. Reject
408
+ // path separators / `..` here (same guard as `--archive-version`) so a crafted
409
+ // version cannot write or relocate content outside `.planning/milestones/`.
410
+ if (!ARCHIVE_VERSION_LABEL_RE.test(version)) {
411
+ error(`milestone complete: version "${version}" is invalid — a milestone version label may contain only letters, digits, '.', '-' and '_', and must not contain path separators or "..".`);
412
+ }
236
413
  const roadmapPath = planningPaths(cwd).roadmap;
237
414
  const reqPath = planningPaths(cwd).requirements;
238
415
  const statePath = planningPaths(cwd).state;
@@ -354,7 +531,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
354
531
  for (const s of summaries) {
355
532
  try {
356
533
  const content = node_fs_1.default.readFileSync(node_path_1.default.join(phasesDir, dir, s), 'utf-8');
357
- const fm = extractFrontmatter(content);
534
+ const fm = extractFrontmatter(content, node_path_1.default.join(phasesDir, dir, s));
358
535
  const rawOneLiner = fm['one-liner'];
359
536
  const oneLiner = (typeof rawOneLiner === 'string' ? rawOneLiner : '') || extractOneLinerFromBody(content);
360
537
  if (oneLiner) {
@@ -491,7 +668,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
491
668
  kind: 'milestoneComplete',
492
669
  version,
493
670
  nextMilestoneCommand: (0, runtime_slash_cjs_1.formatGsdSlash)('new-milestone', (0, runtime_slash_cjs_1.resolveRuntime)(cwd)),
494
- }, { clock: clock_cjs_1.realClock, progressProvider: () => null });
671
+ }, { clock: clock_cjs_1.realClock, progressProvider: () => null, sourcePath: statePath });
495
672
  writeStateMd(statePath, result.content, cwd);
496
673
  }
497
674
  // Archive phase directories if requested
@@ -552,6 +729,32 @@ function cmdPhasesClear(cwd, raw, args) {
552
729
  // --force bypasses the uncommitted-changes guard. Only use when the caller
553
730
  // has already archived or explicitly accepts loss of uncommitted work. (#1447)
554
731
  const force = Array.isArray(args) && args.includes('--force');
732
+ // #2288: explicit outgoing-version override for the archive destination.
733
+ // new-milestone.md runs `state.milestone-switch` BEFORE `phases.clear --confirm`,
734
+ // so a live read of STATE.md would already report the NEW milestone version by
735
+ // the time we get here. Callers that know the outgoing version pass it explicitly;
736
+ // absent an override, archivePhaseDirectories falls back to the live read.
737
+ const avIndex = Array.isArray(args) ? args.indexOf('--archive-version') : -1;
738
+ let archiveVersionOverride = null;
739
+ if (avIndex !== -1) {
740
+ const rawArchiveVersion = args[avIndex + 1];
741
+ // Missing / flag-shaped value: fail loud instead of silently dropping the
742
+ // override. A truncated invocation (e.g. a broken template substitution
743
+ // leaving `--archive-version` with no value) must NOT fall through to the
744
+ // live read — that silently re-files the archive under the new milestone,
745
+ // the exact #2288 bug this flag exists to prevent.
746
+ if (typeof rawArchiveVersion !== 'string' || rawArchiveVersion.startsWith('--') || rawArchiveVersion.trim() === '') {
747
+ error('--archive-version requires a value (a milestone version token, e.g. v1.0)');
748
+ }
749
+ const trimmed = rawArchiveVersion.trim();
750
+ // #2288 security: reject path separators / `..` so a crafted value cannot
751
+ // relocate phase history outside `.planning/milestones/` (phase dirs are
752
+ // MOVED into the archive dir — a traversal is data loss, not just an odd name).
753
+ if (!ARCHIVE_VERSION_LABEL_RE.test(trimmed)) {
754
+ error(`--archive-version "${trimmed}" is invalid — a milestone version label may contain only letters, digits, '.', '-' and '_', and must not contain path separators or "..".`);
755
+ }
756
+ archiveVersionOverride = trimmed;
757
+ }
555
758
  let cleared = 0;
556
759
  if (node_fs_1.default.existsSync(phasesDir)) {
557
760
  const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
@@ -598,7 +801,8 @@ function cmdPhasesClear(cwd, raw, args) {
598
801
  }
599
802
  try {
600
803
  // #1871: archive phase directories instead of destroying them (shared helper).
601
- cleared = archivePhaseDirectories(cwd, phasesDir, dirs).archived;
804
+ // #2288: thread the explicit --archive-version override (if any) through.
805
+ cleared = archivePhaseDirectories(cwd, phasesDir, dirs, archiveVersionOverride).archived;
602
806
  }
603
807
  catch (e) {
604
808
  const message = e instanceof Error ? e.message : String(e);
@@ -609,18 +813,46 @@ function cmdPhasesClear(cwd, raw, args) {
609
813
  }
610
814
  /**
611
815
  * #1871: move each non-999 phase directory under `phasesDir` into
612
- * `milestones/<version>-phases/` (collision-safe; version from getMilestoneInfo,
613
- * timestamp fallback). Shared by `phases clear` (archive-then-remove) and the
614
- * internal milestone.complete phase archival so phase history survives a
615
- * milestone switch instead of being hard-deleted.
816
+ * `milestones/<version>-phases/` (collision-safe). Shared by `phases clear`
817
+ * (archive-then-remove) and the internal milestone.complete phase archival so
818
+ * phase history survives a milestone switch instead of being hard-deleted.
819
+ *
820
+ * Archive-version precedence (#2288): an explicit `archiveVersionOverride` wins
821
+ * first, then a live `getMilestoneInfo(cwd)` read (which itself defaults to a
822
+ * version like `v1.0` when ROADMAP/STATE is absent), and only a dated fallback
823
+ * label if no safe version label is resolvable at all. The override is validated
824
+ * by the caller (`cmdPhasesClear`); the live-read value is re-validated here
825
+ * (defense in depth) because `getMilestoneInfo` derives it from STATE.md's
826
+ * unvalidated `milestone:` field. The override exists because `new-milestone.md`
827
+ * runs `state.milestone-switch` BEFORE `phases.clear --confirm` — by the time
828
+ * this runs, a live read of STATE.md would already report the NEW milestone
829
+ * version, so phase history from the OLD milestone would be misfiled under the
830
+ * new version's archive directory. Callers that know the outgoing version must
831
+ * pass it explicitly.
616
832
  */
617
- function archivePhaseDirectories(cwd, phasesDir, dirs) {
618
- let archiveVersion = null;
619
- try {
620
- archiveVersion = getMilestoneInfo(cwd).version ?? null;
621
- }
622
- catch {
623
- /* ROADMAP/STATE unreadable — fall back to a dated label */
833
+ function archivePhaseDirectories(cwd, phasesDir, dirs, archiveVersionOverride = null) {
834
+ // Self-protecting (#2288 security defense in depth): the sole current caller
835
+ // (`cmdPhasesClear`) already validates the override, but re-test it here so a
836
+ // future caller cannot reopen the path-traversal sink at line ~742. An override
837
+ // that fails the safe-label check is discarded (falls through to the live read
838
+ // / dated label) rather than reaching `path.join` unvalidated.
839
+ const safeOverride = archiveVersionOverride && ARCHIVE_VERSION_LABEL_RE.test(archiveVersionOverride.trim())
840
+ ? archiveVersionOverride.trim()
841
+ : null;
842
+ let archiveVersion = safeOverride;
843
+ if (!archiveVersion) {
844
+ try {
845
+ const liveVersion = getMilestoneInfo(cwd).version ?? null;
846
+ // Defense in depth (#2288 security): getMilestoneInfo reads STATE.md's
847
+ // `milestone:` field, which is unvalidated file content. Only accept it
848
+ // as a path component if it is a safe version label; a crafted value
849
+ // (path separators / `..`) falls through to the dated label below rather
850
+ // than escaping `.planning/milestones/`.
851
+ archiveVersion = liveVersion && ARCHIVE_VERSION_LABEL_RE.test(liveVersion) ? liveVersion : null;
852
+ }
853
+ catch {
854
+ /* ROADMAP/STATE unreadable — fall back to a dated label */
855
+ }
624
856
  }
625
857
  if (!archiveVersion) {
626
858
  archiveVersion = `archived-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 8)}`;
@@ -643,6 +875,8 @@ function archivePhaseDirectories(cwd, phasesDir, dirs) {
643
875
  }
644
876
  module.exports = {
645
877
  cmdRequirementsMarkComplete,
878
+ cmdRequirementsReadyIds,
879
+ cmdRequirementsRevertPhase,
646
880
  cmdMilestoneComplete,
647
881
  cmdPhasesClear,
648
882
  };
@@ -10,10 +10,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
10
10
  return (mod && mod.__esModule) ? mod : { "default": mod };
11
11
  };
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.RUNTIMES_WITH_FAST_MODE = exports.EFFORT_RENDERING = exports.KNOWN_PROVIDERS = exports.PROVIDER_PRESETS = exports.RUNTIMES_WITH_REASONING_EFFORT = exports.KNOWN_RUNTIMES = exports.RUNTIME_PROFILE_MAP = exports.MODEL_ALIAS_MAP = exports.AGENT_DEFAULT_TIERS = exports.AGENT_TO_PHASE_TYPE = exports.MODEL_PROFILES = exports.VALID_AGENT_TIERS = exports.VALID_PHASE_TYPES = exports.VALID_PROFILES = exports.catalog = void 0;
13
+ exports.RUNTIMES_WITH_FAST_MODE = exports.EFFORT_ARGV = exports.EFFORT_RENDERING = exports.KNOWN_PROVIDERS = exports.PROVIDER_PRESETS = exports.RUNTIMES_WITH_REASONING_EFFORT = exports.KNOWN_RUNTIMES = exports.RUNTIME_PROFILE_MAP = exports.MODEL_ALIAS_MAP = exports.AGENT_DEFAULT_TIERS = exports.AGENT_TO_PHASE_TYPE = exports.MODEL_PROFILES = exports.ADAPTIVE_TIER_VALUES = exports.VALID_TIERS = exports.VALID_AGENT_TIERS = exports.VALID_PHASE_TYPES = exports.VALID_PROFILES = exports.catalog = void 0;
14
14
  exports.nextTier = nextTier;
15
15
  exports.formatAgentToModelMapAsTable = formatAgentToModelMapAsTable;
16
16
  exports.getAgentToModelMapForProfile = getAgentToModelMapForProfile;
17
+ exports.renderEffortArgv = renderEffortArgv;
17
18
  exports.renderEffortForRuntime = renderEffortForRuntime;
18
19
  const node_path_1 = __importDefault(require("node:path"));
19
20
  // In .cts (CommonJS output) files, `require` is available as a global;
@@ -23,11 +24,19 @@ const _require = require;
23
24
  // works in every layout:
24
25
  //
25
26
  // 1. Co-located install path — gsd-core/bin/shared/model-catalog.json
26
- // 2. Source-repo dev path — sdk/shared/model-catalog.json
27
- // 3. GSD_MODEL_CATALOG env override
27
+ // 2. GSD_MODEL_CATALOG env override
28
+ //
29
+ // A third candidate — `sdk/shared/model-catalog.json`, three levels up — used to
30
+ // sit between them. It was the legacy source-repo path kept as a fallback by the
31
+ // #3288 fix, whose contract was "check the co-located path FIRST, before the
32
+ // legacy source-repo path". ADR-0174 then retired the `@opengsd/gsd-sdk` package
33
+ // boundary and deleted the `sdk/` tree outright, so that candidate can no longer
34
+ // resolve in any layout: in a source repo there is no `sdk/`, and in an install
35
+ // layout it points at `~/.claude/sdk/shared/`, which the installer never writes
36
+ // (the original #3288 bug). It is removed rather than left as dead weight that
37
+ // implies a package boundary this repo no longer has.
28
38
  const _catalogCandidates = [
29
39
  node_path_1.default.resolve(__dirname, '..', 'shared', 'model-catalog.json'),
30
- node_path_1.default.resolve(__dirname, '..', '..', '..', 'sdk', 'shared', 'model-catalog.json'),
31
40
  ...(process.env['GSD_MODEL_CATALOG'] ? [node_path_1.default.resolve(process.env['GSD_MODEL_CATALOG'])] : []),
32
41
  ];
33
42
  let catalog = null;
@@ -54,6 +63,13 @@ exports.catalog = _catalog;
54
63
  exports.VALID_PROFILES = [..._catalog.profiles];
55
64
  exports.VALID_PHASE_TYPES = new Set(_catalog.phaseTypes);
56
65
  exports.VALID_AGENT_TIERS = new Set(Object.keys(_catalog.adaptiveTierMap));
66
+ // Catalog-derived so this can never drift from the resolver's tier gate:
67
+ // Object.values(adaptiveTierMap) === ['opus', 'sonnet', 'haiku'] today, plus 'inherit'.
68
+ exports.VALID_TIERS = new Set([...Object.values(_catalog.adaptiveTierMap), 'inherit']);
69
+ // Same catalog-derived tier values as VALID_TIERS but WITHOUT 'inherit' — used
70
+ // by config-loader's runtime-override validation (model_profile_overrides /
71
+ // model_policy.runtime_tiers), which does not accept 'inherit' as a tier.
72
+ exports.ADAPTIVE_TIER_VALUES = new Set(Object.values(_catalog.adaptiveTierMap));
57
73
  exports.MODEL_PROFILES = Object.fromEntries(Object.entries(_catalog.agents).map(([agent, meta]) => [agent, {
58
74
  quality: meta.golden,
59
75
  balanced: meta.balanced,
@@ -136,6 +152,55 @@ exports.EFFORT_RENDERING = {
136
152
  },
137
153
  },
138
154
  };
155
+ exports.EFFORT_ARGV = {
156
+ // Verified against `claude --help`: `--effort <level>`.
157
+ claude: {
158
+ render: (level) => ['--effort', level],
159
+ supported: new Set(['low', 'medium', 'high', 'xhigh', 'max']),
160
+ clamp: (level) => (level === 'minimal' ? 'low' : level),
161
+ },
162
+ // Verified against `opencode run --help`: `--variant` — "model variant
163
+ // (provider-specific reasoning effort, e.g., high, max, minimal)".
164
+ opencode: {
165
+ render: (level) => ['--variant', level],
166
+ supported: new Set(['minimal', 'low', 'medium', 'high', 'xhigh', 'max']),
167
+ clamp: (level) => level,
168
+ },
169
+ // First-party Codex docs: `model_reasoning_effort` is a config-only key with no
170
+ // dedicated flag, so the generic `-c key=value` override is the only argv route.
171
+ codex: {
172
+ render: (level) => ['-c', `model_reasoning_effort=${level}`],
173
+ supported: new Set(['minimal', 'low', 'medium', 'high', 'xhigh']),
174
+ clamp: (level) => (level === 'max' ? 'xhigh' : level),
175
+ },
176
+ };
177
+ /**
178
+ * Render the invocation-time effort argument for a host.
179
+ *
180
+ * `effortSurface` is the host's negotiated axis value. Only `argv` produces an
181
+ * argument; `none`, `undocumented`, and anything unrecognised produce nothing.
182
+ * Never throws.
183
+ */
184
+ function renderEffortArgv(host, universalEffort, effortSurface) {
185
+ const empty = { argv: [], value: null, host };
186
+ if (effortSurface !== 'argv')
187
+ return empty;
188
+ // Own-property lookup only. A plain `EFFORT_ARGV[host]` resolves `__proto__`
189
+ // (and `constructor`/`toString`) to inherited members, which are truthy but
190
+ // carry no `clamp`/`render` — a hostile host id would throw instead of
191
+ // degrading. The host id reaches here from a descriptor, i.e. untrusted JSON.
192
+ if (typeof host !== 'string' || !Object.prototype.hasOwnProperty.call(exports.EFFORT_ARGV, host))
193
+ return empty;
194
+ const spec = exports.EFFORT_ARGV[host];
195
+ if (!spec || typeof spec.clamp !== 'function' || typeof spec.render !== 'function')
196
+ return empty;
197
+ if (typeof universalEffort !== 'string' || universalEffort.length === 0)
198
+ return empty;
199
+ const clamped = spec.clamp(universalEffort);
200
+ if (!spec.supported.has(clamped))
201
+ return empty;
202
+ return { argv: spec.render(clamped), value: clamped, host };
203
+ }
139
204
  /**
140
205
  * Render a universal effort string for a specific runtime.
141
206
  */