@opengsd/gsd-core 1.4.4 → 1.5.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 (197) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +3 -3
  3. package/agents/gsd-code-fixer.md +3 -2
  4. package/agents/gsd-debug-session-manager.md +2 -1
  5. package/agents/gsd-debugger.md +4 -3
  6. package/agents/gsd-executor.md +17 -16
  7. package/agents/gsd-intel-updater.md +38 -41
  8. package/agents/gsd-phase-researcher.md +8 -8
  9. package/agents/gsd-plan-checker.md +23 -13
  10. package/agents/gsd-planner.md +32 -188
  11. package/agents/gsd-project-researcher.md +5 -4
  12. package/agents/gsd-research-synthesizer.md +2 -1
  13. package/agents/gsd-ui-researcher.md +2 -1
  14. package/agents/gsd-verifier.md +12 -11
  15. package/bin/install.js +965 -1486
  16. package/commands/gsd/autonomous.md +5 -1
  17. package/commands/gsd/ns-manage.md +8 -1
  18. package/commands/gsd/ns-project.md +5 -0
  19. package/commands/gsd/ns-review.md +4 -1
  20. package/commands/gsd/ns-workflow.md +7 -1
  21. package/commands/gsd/plan-review-convergence.md +5 -4
  22. package/commands/gsd/surface.md +12 -5
  23. package/gemini-extension.json +1 -1
  24. package/gsd-core/bin/gsd-tools.cjs +198 -101
  25. package/gsd-core/bin/gsd_run +20 -0
  26. package/gsd-core/bin/lib/audit-command-router.cjs +61 -0
  27. package/gsd-core/bin/lib/capability-registry.cjs +2234 -0
  28. package/gsd-core/bin/lib/capability-state.cjs +336 -0
  29. package/gsd-core/bin/lib/check-command-router.cjs +133 -2
  30. package/gsd-core/bin/lib/cli-exit.cjs +22 -3
  31. package/gsd-core/bin/lib/config-loader.cjs +716 -0
  32. package/gsd-core/bin/lib/configuration.cjs +4 -34
  33. package/gsd-core/bin/lib/core-utils.cjs +198 -0
  34. package/gsd-core/bin/lib/core.cjs +107 -1817
  35. package/gsd-core/bin/lib/edge-probe.cjs +173 -0
  36. package/gsd-core/bin/lib/fallow-runner.cjs +63 -25
  37. package/gsd-core/bin/lib/federated-config.cjs +182 -0
  38. package/gsd-core/bin/lib/graphify-command-router.cjs +74 -0
  39. package/gsd-core/bin/lib/init.cjs +58 -12
  40. package/gsd-core/bin/lib/install-profiles.cjs +157 -3
  41. package/gsd-core/bin/lib/intel-command-router.cjs +116 -0
  42. package/gsd-core/bin/lib/intel.cjs +3 -3
  43. package/gsd-core/bin/lib/io.cjs +222 -0
  44. package/gsd-core/bin/lib/loop-host-contract.cjs +105 -0
  45. package/gsd-core/bin/lib/loop-resolver.cjs +460 -0
  46. package/gsd-core/bin/lib/model-resolver.cjs +426 -0
  47. package/gsd-core/bin/lib/phase-command-router.cjs +20 -0
  48. package/gsd-core/bin/lib/phase-id.cjs +215 -0
  49. package/gsd-core/bin/lib/phase-locator.cjs +148 -0
  50. package/gsd-core/bin/lib/phase.cjs +17 -0
  51. package/gsd-core/bin/lib/probe-core.cjs +257 -0
  52. package/gsd-core/bin/lib/profile-pipeline.cjs +2 -2
  53. package/gsd-core/bin/lib/roadmap-parser.cjs +443 -0
  54. package/gsd-core/bin/lib/roadmap.cjs +44 -1
  55. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +92 -95
  56. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +68 -29
  57. package/gsd-core/bin/lib/runtime-homes.cjs +163 -87
  58. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1439 -0
  59. package/gsd-core/bin/lib/runtime-name-policy.cjs +2 -1
  60. package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
  61. package/gsd-core/bin/lib/shell-command-projection.cjs +13 -0
  62. package/gsd-core/bin/lib/state-document.cjs +8 -0
  63. package/gsd-core/bin/lib/state.cjs +114 -2
  64. package/gsd-core/bin/lib/surface.cjs +66 -14
  65. package/gsd-core/bin/lib/uat-predicate.cjs +329 -0
  66. package/gsd-core/bin/lib/update-context.cjs +4 -1
  67. package/gsd-core/bin/lib/verify.cjs +104 -1
  68. package/gsd-core/bin/lib/worktree-base-ref.cjs +33 -8
  69. package/gsd-core/bin/shared/model-catalog.json +11 -6
  70. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -1
  71. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +7 -0
  72. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/requirements.json +1 -0
  73. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +8 -0
  74. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/requirements.json +1 -0
  75. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +7 -0
  76. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/requirements.json +1 -0
  77. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +7 -0
  78. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/requirements.json +1 -0
  79. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +8 -0
  80. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/requirements.json +1 -0
  81. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +8 -0
  82. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/requirements.json +1 -0
  83. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/resolutions.json +4 -0
  84. package/gsd-core/references/edge-probe.md +261 -0
  85. package/gsd-core/references/planner-antipatterns.md +41 -0
  86. package/gsd-core/references/planner-guidance.md +186 -0
  87. package/gsd-core/references/planner-reviews.md +5 -2
  88. package/gsd-core/templates/phase-prompt.md +7 -7
  89. package/gsd-core/templates/project.md +19 -2
  90. package/gsd-core/templates/spec.md +12 -0
  91. package/gsd-core/templates/summary-complex.md +1 -0
  92. package/gsd-core/templates/summary-minimal.md +1 -0
  93. package/gsd-core/templates/summary-standard.md +1 -0
  94. package/gsd-core/templates/summary.md +1 -0
  95. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  96. package/gsd-core/workflows/add-backlog.md +1 -1
  97. package/gsd-core/workflows/add-phase.md +1 -1
  98. package/gsd-core/workflows/add-tests.md +1 -1
  99. package/gsd-core/workflows/add-todo.md +1 -1
  100. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  101. package/gsd-core/workflows/audit-fix.md +1 -1
  102. package/gsd-core/workflows/audit-milestone.md +1 -1
  103. package/gsd-core/workflows/audit-uat.md +1 -1
  104. package/gsd-core/workflows/autonomous.md +111 -51
  105. package/gsd-core/workflows/check-todos.md +1 -1
  106. package/gsd-core/workflows/cleanup.md +1 -1
  107. package/gsd-core/workflows/code-review-fix.md +6 -4
  108. package/gsd-core/workflows/code-review.md +53 -17
  109. package/gsd-core/workflows/complete-milestone.md +11 -5
  110. package/gsd-core/workflows/debug.md +1 -1
  111. package/gsd-core/workflows/diagnose-issues.md +1 -1
  112. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  113. package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
  114. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  115. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  116. package/gsd-core/workflows/discuss-phase.md +8 -1
  117. package/gsd-core/workflows/do.md +1 -1
  118. package/gsd-core/workflows/docs-update.md +1 -1
  119. package/gsd-core/workflows/edit-phase.md +1 -1
  120. package/gsd-core/workflows/eval-review.md +4 -1
  121. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  122. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  123. package/gsd-core/workflows/execute-phase.md +8 -1
  124. package/gsd-core/workflows/execute-plan.md +1 -1
  125. package/gsd-core/workflows/explore.md +1 -1
  126. package/gsd-core/workflows/extract-learnings.md +1 -1
  127. package/gsd-core/workflows/forensics.md +1 -1
  128. package/gsd-core/workflows/graduation.md +1 -1
  129. package/gsd-core/workflows/health.md +1 -1
  130. package/gsd-core/workflows/help/modes/full.md +1 -1
  131. package/gsd-core/workflows/import.md +1 -1
  132. package/gsd-core/workflows/ingest-docs.md +1 -1
  133. package/gsd-core/workflows/insert-phase.md +1 -1
  134. package/gsd-core/workflows/list-workspaces.md +1 -1
  135. package/gsd-core/workflows/manager.md +1 -1
  136. package/gsd-core/workflows/map-codebase.md +1 -1
  137. package/gsd-core/workflows/milestone-summary.md +1 -1
  138. package/gsd-core/workflows/mvp-phase.md +1 -1
  139. package/gsd-core/workflows/new-milestone.md +9 -1
  140. package/gsd-core/workflows/new-project.md +9 -1
  141. package/gsd-core/workflows/new-workspace.md +1 -1
  142. package/gsd-core/workflows/next.md +1 -1
  143. package/gsd-core/workflows/pause-work.md +1 -1
  144. package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
  145. package/gsd-core/workflows/plan-phase.md +65 -28
  146. package/gsd-core/workflows/plan-review-convergence.md +60 -33
  147. package/gsd-core/workflows/plant-seed.md +1 -1
  148. package/gsd-core/workflows/profile-user.md +1 -1
  149. package/gsd-core/workflows/progress.md +1 -1
  150. package/gsd-core/workflows/quick.md +2 -2
  151. package/gsd-core/workflows/remove-phase.md +1 -1
  152. package/gsd-core/workflows/remove-workspace.md +1 -1
  153. package/gsd-core/workflows/resume-project.md +1 -1
  154. package/gsd-core/workflows/review.md +1 -1
  155. package/gsd-core/workflows/scan.md +1 -1
  156. package/gsd-core/workflows/secure-phase.md +1 -1
  157. package/gsd-core/workflows/settings-advanced.md +7 -7
  158. package/gsd-core/workflows/settings-integrations.md +1 -1
  159. package/gsd-core/workflows/settings.md +2 -2
  160. package/gsd-core/workflows/ship.md +8 -1
  161. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  162. package/gsd-core/workflows/sketch.md +1 -1
  163. package/gsd-core/workflows/spec-phase.md +130 -1
  164. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  165. package/gsd-core/workflows/spike.md +1 -1
  166. package/gsd-core/workflows/stats.md +1 -1
  167. package/gsd-core/workflows/thread.md +1 -1
  168. package/gsd-core/workflows/transition.md +1 -1
  169. package/gsd-core/workflows/ui-phase.md +1 -1
  170. package/gsd-core/workflows/ui-review.md +1 -1
  171. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  172. package/gsd-core/workflows/update.md +2 -2
  173. package/gsd-core/workflows/validate-phase.md +1 -1
  174. package/gsd-core/workflows/verify-phase.md +1 -1
  175. package/gsd-core/workflows/verify-work.md +8 -1
  176. package/package.json +11 -3
  177. package/scripts/base64-scan.sh +1 -1
  178. package/scripts/changeset/cli.cjs +8 -1
  179. package/scripts/changeset/lint.cjs +38 -2
  180. package/scripts/ci-test-scope.cjs +21 -10
  181. package/scripts/gen-capability-registry.cjs +2293 -0
  182. package/scripts/gen-loop-host-contract.cjs +471 -0
  183. package/scripts/lib/allowlist-ratchet.cjs +101 -1
  184. package/scripts/lint-regression-test-names.allowlist.json +269 -0
  185. package/scripts/lint-regression-test-names.cjs +117 -0
  186. package/scripts/lint-test-file-count.allowlist.json +25 -4
  187. package/scripts/lint-windows-test-portability.cjs +178 -0
  188. package/scripts/prompt-injection-scan.sh +4 -4
  189. package/scripts/research-profiles.cjs +10 -10
  190. package/scripts/run-tests.cjs +133 -29
  191. package/scripts/secret-scan.sh +3 -3
  192. package/scripts/sync-next-version.cjs +133 -0
  193. package/scripts/sync-runtime-launcher.cjs +21 -5
  194. package/scripts/update-size-baseline.cjs +68 -0
  195. package/scripts/workflow-policy.cjs +42 -9
  196. package/scripts/workflow-size.cjs +90 -0
  197. package/scripts/run-cross-platform-tests.cjs +0 -67
@@ -0,0 +1,329 @@
1
+ "use strict";
2
+ /**
3
+ * UAT Predicate — Pure-computation UAT pass/fail evaluation
4
+ *
5
+ * Evaluates all *-UAT.md and *-VERIFICATION.md files in a phase directory and
6
+ * returns a typed report. Used by `phase uat-passed` to harden against the
7
+ * naive whole-file regex in cmdPhaseComplete which false-matches `result:` lines
8
+ * inside frontmatter, fenced code blocks, blockquotes, and HTML comments.
9
+ *
10
+ * Issue #247 — phase uat-passed predicate
11
+ *
12
+ * ADR-457 build-at-publish: compiled by tsc to gsd-core/bin/lib/uat-predicate.cjs.
13
+ */
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ const node_fs_1 = __importDefault(require("node:fs"));
18
+ const node_path_1 = __importDefault(require("node:path"));
19
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
20
+ const frontmatter = require("./frontmatter.cjs");
21
+ const { extractFrontmatter } = frontmatter;
22
+ // ─── Blocking state sets (documented for maintainability) ─────────────────────
23
+ // UAT file frontmatter `status` values that indicate the file is not fully done
24
+ const BLOCKING_UAT_FM_STATUSES = new Set([
25
+ 'partial', 'diagnosed', 'pending', 'blocked', 'in_progress', 'failed',
26
+ ]);
27
+ // UAT file frontmatter `result` values that indicate failure
28
+ const BLOCKING_UAT_FM_RESULTS = new Set(['pending', 'blocked', 'failed']);
29
+ // VERIFICATION file frontmatter `status` values that indicate passing
30
+ const PASSING_VERIFICATION_STATUSES = new Set([
31
+ 'complete', 'verified', 'passed', 'human_passed',
32
+ ]);
33
+ // VERIFICATION file frontmatter `status` values that explicitly block
34
+ const BLOCKING_VERIFICATION_FM_STATUSES = new Set([
35
+ 'human_needed', 'gaps_found', 'pending', 'blocked', 'partial',
36
+ 'failed', 'in_progress',
37
+ ]);
38
+ // UAT test-item `result` values that count as passing
39
+ const PASSING_RESULTS = new Set(['passed', 'pass']);
40
+ // ─── stripFalsePositiveContexts ───────────────────────────────────────────────
41
+ /**
42
+ * Remove contexts that can contain `result: ...` lines that are NOT real test results:
43
+ * (a) leading frontmatter block at byte 0
44
+ * (b) HTML comments (unterminated comments swallow to EOF — fail-closed)
45
+ * (c) fenced code blocks (backtick and tilde, indented too) via CommonMark state machine
46
+ * (d) blockquote lines
47
+ *
48
+ * Each step is a composable function (Kernighan's Law — independently testable).
49
+ * Returns surviving lines joined by '\n'. Robust to CRLF input.
50
+ */
51
+ function stripFalsePositiveContexts(content) {
52
+ // Step (a): strip leading frontmatter block only at byte 0
53
+ let stripped = content.replace(/^---\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/, '');
54
+ // Step (b): remove HTML comments anywhere; unterminated comment swallows to EOF
55
+ stripped = stripped.replace(/<!--[\s\S]*?(?:-->|$)/g, '');
56
+ // Step (c): remove fenced code blocks via CommonMark-style state machine (handles CRLF + indented fences)
57
+ stripped = _stripFencedBlocks(stripped).text;
58
+ // Step (d): remove blockquote lines
59
+ stripped = stripped
60
+ .split('\n')
61
+ .filter(line => !/^\s*>/.test(line))
62
+ .join('\n');
63
+ return stripped;
64
+ }
65
+ /**
66
+ * CommonMark-style fenced-code-block stripper.
67
+ * Tracks the opening delimiter char and length so that a ~~~ line inside a
68
+ * ``` fence is correctly treated as fence content, not a closing delimiter.
69
+ *
70
+ * Opening rule: first delimiter line with char+len sets openFence.
71
+ * Closing rule: delimiter line with SAME char, run length >= openFence.len,
72
+ * and NO trailing non-whitespace text closes the fence.
73
+ * All delimiter and content lines are dropped; non-fence lines are kept.
74
+ * Returns the kept text plus unterminatedFence:true if EOF inside a fence.
75
+ */
76
+ function _stripFencedBlocks(content) {
77
+ const lines = content.split('\n');
78
+ const kept = [];
79
+ let openFence = null;
80
+ const delimRe = /^(\s*)(`{3,}|~{3,})(.*)$/;
81
+ for (const rawLine of lines) {
82
+ // Tolerate CRLF: strip trailing \r for matching, but we work on split-by-\n lines
83
+ // (the outer caller joined by \n already; we just handle a stray \r in the last char)
84
+ const line = rawLine.replace(/\r$/, '');
85
+ const m = delimRe.exec(line);
86
+ if (m) {
87
+ const char = m[2][0];
88
+ const len = m[2].length;
89
+ const trailing = m[3];
90
+ if (openFence === null) {
91
+ // Opening delimiter — drop this line and record the fence
92
+ openFence = { char, len };
93
+ }
94
+ else if (char === openFence.char && len >= openFence.len && /^\s*$/.test(trailing)) {
95
+ // Closing delimiter (same char, sufficient length, no trailing text) — drop and close
96
+ openFence = null;
97
+ }
98
+ // else: mismatched delimiter inside fence (e.g. ~~~ inside ```) — drop as content
99
+ continue; // delimiter lines are always dropped
100
+ }
101
+ if (openFence === null) {
102
+ kept.push(rawLine);
103
+ }
104
+ // Lines inside fence are dropped
105
+ }
106
+ return { text: kept.join('\n'), unterminatedFence: openFence !== null };
107
+ }
108
+ /**
109
+ * Analyse raw markdown for structural anomalies (unterminated fence / comment).
110
+ * Exported for unit-testability and used by evaluateUatPassed for per-file malformed detection.
111
+ *
112
+ * FIX C: properly balanced comments are stripped before checking for a dangling <!--,
113
+ * so an earlier closed comment does not mask a later unterminated one.
114
+ */
115
+ function analyzeMarkdown(raw) {
116
+ // Detect an unterminated HTML comment via a paired scan: every `<!--` must
117
+ // have a following `-->`. Using indexOf (not a regex .replace of the comment
118
+ // token) avoids the js/incomplete-multi-character-sanitization pattern — and
119
+ // is exact: a closed earlier comment never masks a later unterminated one.
120
+ let unterminatedComment = false;
121
+ for (let i = 0;;) {
122
+ const open = raw.indexOf('<!--', i);
123
+ if (open === -1)
124
+ break;
125
+ const close = raw.indexOf('-->', open + 4);
126
+ if (close === -1) {
127
+ unterminatedComment = true;
128
+ break;
129
+ }
130
+ i = close + 3;
131
+ }
132
+ // Fence state machine gives the accurate unterminated-fence signal.
133
+ const { unterminatedFence } = _stripFencedBlocks(raw);
134
+ return { unterminatedFence, unterminatedComment };
135
+ }
136
+ // ─── parseUatResultItems ──────────────────────────────────────────────────────
137
+ /**
138
+ * HEADING-BLOCK parser: scan the CLEANED body (after stripFalsePositiveContexts)
139
+ * for UAT test blocks.
140
+ *
141
+ * For each ### N. Name heading, the block spans until the next ### heading or EOF.
142
+ * Within each block, find a column-0 anchored result line (rejects indented YAML
143
+ * block-scalar bodies and inline/quoted fakes).
144
+ *
145
+ * - If a heading block has NO column-0 result line → emit result:'missing' (blocker).
146
+ * - Support bracketed [passed] and bare passed (#2273).
147
+ * - Returns ALL items (both passing and non-passing).
148
+ */
149
+ function parseUatResultItems(cleanContent) {
150
+ const items = [];
151
+ // Find all ### N. Name headings (line-anchored)
152
+ const headingPattern = /^###\s*(\d+)\.\s*(.+)$/gm;
153
+ const headings = [];
154
+ let hMatch;
155
+ while ((hMatch = headingPattern.exec(cleanContent)) !== null) {
156
+ headings.push({
157
+ index: hMatch.index + hMatch[0].length,
158
+ test: parseInt(hMatch[1], 10),
159
+ name: hMatch[2].trim(),
160
+ });
161
+ }
162
+ for (let i = 0; i < headings.length; i++) {
163
+ const h = headings[i];
164
+ const blockStart = h.index;
165
+ // More precise: find next heading's position in the original string
166
+ // We'll slice from current heading end to the position just before next heading's "###"
167
+ const nextHeadingMatch = i + 1 < headings.length
168
+ ? cleanContent.lastIndexOf('\n###', headings[i + 1].index)
169
+ : -1;
170
+ const blockContent = nextHeadingMatch >= blockStart
171
+ ? cleanContent.slice(blockStart, nextHeadingMatch)
172
+ : cleanContent.slice(blockStart);
173
+ // Column-0 anchored result line: /^result:[ \t]*\[?([\w-]+)\]?/mi
174
+ // Uses [ \t]* (not \s*) so the captured value must sit on the SAME line as result:.
175
+ // A result: key with the value on a subsequent line yields no match → 'missing' (blocker).
176
+ const resultMatch = /^result:[ \t]*\[?([\w-]+)\]?/mi.exec(blockContent);
177
+ if (resultMatch) {
178
+ items.push({
179
+ test: h.test,
180
+ name: h.name,
181
+ result: resultMatch[1].toLowerCase(),
182
+ });
183
+ }
184
+ else {
185
+ // No column-0 result line → emit 'missing' (a non-passing state)
186
+ items.push({
187
+ test: h.test,
188
+ name: h.name,
189
+ result: 'missing',
190
+ });
191
+ }
192
+ }
193
+ return items;
194
+ }
195
+ // ─── evaluateUatPassed ────────────────────────────────────────────────────────
196
+ /**
197
+ * Evaluate all UAT/VERIFICATION files in a phase directory.
198
+ * Returns a UatPassedReport with the locked, stable shape defined by the interface.
199
+ *
200
+ * FAIL-CLOSED: any absence/ambiguity/malformed input → NOT passed.
201
+ * Pass requires at least one real passing check AND no blockers.
202
+ */
203
+ function evaluateUatPassed(phaseFullDir, opts) {
204
+ const requireVerification = opts?.policy?.requireVerification === true;
205
+ const blockers = [];
206
+ const checks = [];
207
+ const uatFiles = [];
208
+ const verificationFiles = [];
209
+ // Read the directory — if unreadable, treat as no files (fail-closed: no artifacts → not passed)
210
+ let dirEntries = [];
211
+ try {
212
+ dirEntries = node_fs_1.default.readdirSync(phaseFullDir);
213
+ }
214
+ catch {
215
+ // Unreadable dir — no_uat_artifacts:true, passed:false
216
+ const no_uat_artifacts = true;
217
+ if (requireVerification) {
218
+ blockers.push('policy: verification required but no passing *-VERIFICATION.md found');
219
+ }
220
+ return {
221
+ passed: false,
222
+ uat_files: [],
223
+ verification_files: [],
224
+ checks: [],
225
+ blockers,
226
+ no_uat_artifacts,
227
+ policy: { require_verification: requireVerification },
228
+ };
229
+ }
230
+ // Filter UAT and VERIFICATION files using the same filter as cmdPhaseComplete
231
+ const uatFileNames = dirEntries.filter(f => f.includes('-UAT') && f.endsWith('.md'));
232
+ const verFileNames = dirEntries.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md'));
233
+ // ── Process UAT files ──────────────────────────────────────────────────────
234
+ for (const file of uatFileNames) {
235
+ uatFiles.push(file);
236
+ let raw = '';
237
+ try {
238
+ raw = node_fs_1.default.readFileSync(node_path_1.default.join(phaseFullDir, file), 'utf-8');
239
+ }
240
+ catch {
241
+ blockers.push(`${file}: could not read file`);
242
+ continue;
243
+ }
244
+ // ── Per-file malformed markdown guard ──────────────────────────────────
245
+ // FIX D: use accurate signals from analyzeMarkdown instead of heuristics.
246
+ // unterminatedFence: CommonMark state machine detects a genuinely unclosed fence.
247
+ // unterminatedComment: strips balanced comments first, then checks for leftover <!--.
248
+ const { unterminatedFence, unterminatedComment } = analyzeMarkdown(raw);
249
+ if (unterminatedFence || unterminatedComment) {
250
+ blockers.push(`${file}: malformed markdown (unterminated fence or comment)`);
251
+ }
252
+ const fm = extractFrontmatter(raw);
253
+ // File-level frontmatter status check
254
+ if (fm['status'] && BLOCKING_UAT_FM_STATUSES.has(fm['status'])) {
255
+ blockers.push(`${file}: frontmatter status=${fm['status']}`);
256
+ }
257
+ // File-level frontmatter result check
258
+ if (fm['result'] && BLOCKING_UAT_FM_RESULTS.has(fm['result'])) {
259
+ blockers.push(`${file}: frontmatter result=${fm['result']}`);
260
+ }
261
+ // Parse test items from the cleaned body (hardened against false positives)
262
+ const cleanContent = stripFalsePositiveContexts(raw);
263
+ const items = parseUatResultItems(cleanContent);
264
+ for (const item of items) {
265
+ const passing = PASSING_RESULTS.has(item.result);
266
+ checks.push({
267
+ file,
268
+ test: item.test,
269
+ name: item.name,
270
+ result: item.result,
271
+ passing,
272
+ });
273
+ if (!passing) {
274
+ blockers.push(`${file}: test ${item.test} (${item.result})`);
275
+ }
276
+ }
277
+ }
278
+ // ── Process VERIFICATION files ─────────────────────────────────────────────
279
+ let hasPassingVerification = false;
280
+ for (const file of verFileNames) {
281
+ verificationFiles.push(file);
282
+ let raw = '';
283
+ try {
284
+ raw = node_fs_1.default.readFileSync(node_path_1.default.join(phaseFullDir, file), 'utf-8');
285
+ }
286
+ catch {
287
+ blockers.push(`${file}: could not read verification file`);
288
+ continue;
289
+ }
290
+ const vfm = extractFrontmatter(raw);
291
+ const vStatus = vfm['status'];
292
+ if (vStatus && BLOCKING_VERIFICATION_FM_STATUSES.has(vStatus)) {
293
+ blockers.push(`${file}: verification status=${vStatus}`);
294
+ }
295
+ else if (vStatus && PASSING_VERIFICATION_STATUSES.has(vStatus)) {
296
+ // Allowlist: only explicitly-passing statuses count
297
+ hasPassingVerification = true;
298
+ }
299
+ // Missing or unknown status: does NOT count as passing, does NOT push a blocker
300
+ // (handled by the requireVerification policy check below if needed)
301
+ }
302
+ // ── Policy: requireVerification ───────────────────────────────────────────
303
+ if (requireVerification && !hasPassingVerification) {
304
+ blockers.push('policy: verification required but no passing *-VERIFICATION.md found');
305
+ }
306
+ // ── Determine no_uat_artifacts and passed ─────────────────────────────────
307
+ // no_uat_artifacts: true when no real UAT test items were parsed from any file
308
+ const no_uat_artifacts = checks.length === 0;
309
+ // FIX 1: require positive passing evidence; no vacuous pass
310
+ // passed = no blockers AND at least one check AND all checks passing
311
+ const passed = blockers.length === 0 && checks.length > 0 && checks.every(c => c.passing);
312
+ return {
313
+ passed,
314
+ uat_files: uatFiles,
315
+ verification_files: verificationFiles,
316
+ checks,
317
+ blockers,
318
+ no_uat_artifacts,
319
+ policy: {
320
+ require_verification: requireVerification,
321
+ },
322
+ };
323
+ }
324
+ module.exports = {
325
+ stripFalsePositiveContexts,
326
+ parseUatResultItems,
327
+ analyzeMarkdown,
328
+ evaluateUatPassed,
329
+ };
@@ -29,7 +29,10 @@ exports.RUNTIME_DIRS = [
29
29
  ['antigravity', '.gemini/antigravity-ide'],
30
30
  ['antigravity', '.gemini/antigravity-cli'],
31
31
  ['antigravity', '.gemini/antigravity'],
32
- ['antigravity', '.agent'], // local Antigravity install dir (#503; bin/install.js getDirName('antigravity'))
32
+ ['antigravity', '.agents'], // local Antigravity install dir canonical (#791; bin/install.js getDirName('antigravity'))
33
+ ['antigravity', '.agent'], // local Antigravity install dir legacy (#503; backward-compat with pre-#791 installs)
34
+ ['windsurf', '.devin'], // local Windsurf/Devin Desktop install dir canonical (#1085; bin/install.js getDirName('windsurf'))
35
+ ['windsurf', '.windsurf'], // local Windsurf install dir legacy (#1085; backward-compat with pre-#1085 installs)
33
36
  ['gemini', '.gemini'],
34
37
  ['kilo', '.config/kilo'],
35
38
  ['kilo', '.kilo'],
@@ -121,6 +121,98 @@ function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
121
121
  const result = { passed, checks, errors };
122
122
  output(result, raw, passed ? 'passed' : 'failed');
123
123
  }
124
+ /**
125
+ * Issue #429 — negative-grep comment-text echo gate.
126
+ * A literal that an acceptance criterion negative-greps for (grep -c 'LIT' file == 0)
127
+ * must not also appear verbatim inside an <action> body, or the executor's commit-time
128
+ * verify gate fails on the comment echo rather than a real regression. Conservative:
129
+ * errors only on a confidently-extracted QUOTED literal; ambiguous (bareword) → warning.
130
+ */
131
+ function scanNegativeGrepCommentEcho(content) {
132
+ const errors = [];
133
+ const warnings = [];
134
+ // Normalize newlines; join backslash line-continuations so a verify command wrapped
135
+ // across lines (grep ... \ <newline> == 0) is still seen as one segment.
136
+ const text = (content || '')
137
+ .replace(/\r\n/g, '\n')
138
+ .replace(/\r/g, '\n')
139
+ .replace(/\\\n/g, ' ');
140
+ // 1. Allowlisted literals: <!-- planner-discipline-allow: LIT -->
141
+ const allow = new Set();
142
+ const allowRe = /<!--\s*planner-discipline-allow:\s*(.+?)\s*-->/g;
143
+ let am;
144
+ while ((am = allowRe.exec(text)) !== null)
145
+ allow.add(am[1]);
146
+ // Zero-equality comparison (the negative grep). The required leading whitespace
147
+ // before the operator distinguishes a shell comparison (`[ $c == 0 ]`, `... == 0`,
148
+ // always spaced) from an assignment (`VAR=0`, never spaced) and naturally excludes
149
+ // `>= 0`, `<= 0`, `!= 0`, `!== 0`, `=== 0`.
150
+ const zeroCmp = (s) => /\s==?\s*0\b/.test(s) || /-eq\s+0\b/.test(s) || /\bequals\s+0\b/.test(s);
151
+ // A grep invocation using a count flag (-c / -cF / -Fc / --count), capturing the
152
+ // search pattern (first quoted token, else first bareword) after a run of options.
153
+ // The options run lets `grep -c -F 'LIT'`, `grep -F -c 'LIT'`, `grep -c -e 'LIT'`
154
+ // and `grep --count 'LIT'` all resolve to the LIT pattern.
155
+ const countGrepRe = /grep((?:\s+-{1,2}[A-Za-z][A-Za-z-]*)+)\s+(?:'([^']*)'|"([^"]*)"|([^\s'"|>&;]+))/g;
156
+ const optsHaveCount = (opts) => /(?:^|\s)-[A-Za-z]*c[A-Za-z]*(?=\s|$)/.test(opts) || /--count\b/.test(opts);
157
+ // `grep -cv 'pat' == 0` counts NON-matching lines, so == 0 there asserts "all lines
158
+ // match" — a POSITIVE gate, not our negative gate. Skip inverted greps.
159
+ const optsHaveInvert = (opts) => /(?:^|\s)-[A-Za-z]*v[A-Za-z]*(?=\s|$)/.test(opts) || /--invert-match\b/.test(opts);
160
+ // Bareword sanity: a real grep target, not a stray operator/number/flag.
161
+ const plausibleBare = (s) => /[A-Za-z0-9_]/.test(s) && !/^[-=!<>0-9]+$/.test(s);
162
+ // 2. <action> text to scan, with negative-grep COMMAND SPANS removed (only the
163
+ // command, not the whole line) so a pasted verify command does not self-flag
164
+ // while a prose echo on the same line is still caught.
165
+ const cmdSpanRe = /grep(?:\s+-{1,2}[A-Za-z][A-Za-z-]*)+\s+(?:'[^']*'|"[^"]*"|[^\s'"|>&;]+)[^\n]*?(?:==|-eq|=)\s*0\b/g;
166
+ const actionZones = [];
167
+ const actionRe = /<action>([\s\S]*?)<\/action>/g;
168
+ let acm;
169
+ while ((acm = actionRe.exec(text)) !== null)
170
+ actionZones.push(acm[1]);
171
+ const scannableActionText = actionZones.map((zone) => zone.replace(cmdSpanRe, ' ')).join('\n');
172
+ // 3. Per shell SEGMENT (split lines on && / ||) extract count-grep literals and
173
+ // check echoes. Per-segment splitting keeps a positive gate (`== 1`) from
174
+ // poisoning a negative gate (`== 0`) sharing the same physical line.
175
+ const seenErr = new Set();
176
+ const seenWarn = new Set();
177
+ const segments = text.split('\n').flatMap((line) => line.split(/\s*(?:&&|\|\|)\s*/));
178
+ for (const seg of segments) {
179
+ if (!/grep(?:\s+-{1,2}[A-Za-z])/.test(seg) || !zeroCmp(seg))
180
+ continue;
181
+ countGrepRe.lastIndex = 0;
182
+ const quotedLits = [];
183
+ const bareLits = [];
184
+ let m;
185
+ while ((m = countGrepRe.exec(seg)) !== null) {
186
+ if (!optsHaveCount(m[1]) || optsHaveInvert(m[1]))
187
+ continue; // need count, not invert (-cv is positive)
188
+ if (m[2] !== undefined)
189
+ quotedLits.push(m[2]);
190
+ else if (m[3] !== undefined)
191
+ quotedLits.push(m[3]);
192
+ else if (m[4] !== undefined && plausibleBare(m[4]))
193
+ bareLits.push(m[4]);
194
+ }
195
+ for (const quoted of quotedLits) {
196
+ if (!quoted || allow.has(quoted) || seenErr.has(quoted))
197
+ continue;
198
+ if (scannableActionText.includes(quoted)) {
199
+ seenErr.add(quoted);
200
+ errors.push(`Plan body contains forbidden literal "${quoted}" in an <action> block, but an acceptance criterion negative-greps for it (grep -c ... == 0). Rephrase the literal by concept, remove it from the plan body, or add <!-- planner-discipline-allow: ${quoted} --> if it must legitimately appear.`);
201
+ }
202
+ }
203
+ if (quotedLits.length === 0) {
204
+ for (const bare of bareLits) {
205
+ if (allow.has(bare) || seenWarn.has(bare))
206
+ continue;
207
+ if (scannableActionText.includes(bare)) {
208
+ seenWarn.add(bare);
209
+ warnings.push(`Possible comment-text echo (#429): negative-grep target "${bare}" is unquoted so its literal could not be extracted unambiguously, but it appears in an <action> block. Quote the grep literal and add an allowlist marker if the echo is intended, or rephrase by concept.`);
210
+ }
211
+ }
212
+ }
213
+ }
214
+ return { errors, warnings };
215
+ }
124
216
  function cmdVerifyPlanStructure(cwd, filePath, raw) {
125
217
  if (!filePath) {
126
218
  error('file path required');
@@ -175,6 +267,9 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
175
267
  if (hasCheckpoints && fm['autonomous'] !== 'false' && String(fm['autonomous']) !== 'false') {
176
268
  errors.push('Has checkpoint tasks but autonomous is not false');
177
269
  }
270
+ const echoScan = scanNegativeGrepCommentEcho(content);
271
+ errors.push(...echoScan.errors);
272
+ warnings.push(...echoScan.warnings);
178
273
  output({
179
274
  valid: errors.length === 0,
180
275
  errors,
@@ -383,7 +478,7 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
383
478
  };
384
479
  const sourceContent = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(cwd, link['from'] || ''));
385
480
  if (!sourceContent) {
386
- check['detail'] = 'Source file not found';
481
+ check['detail'] = 'Source file not found (from: must be a relative file path; describe components/endpoints in via:)';
387
482
  }
388
483
  else if (link['pattern']) {
389
484
  try {
@@ -826,6 +921,12 @@ function cmdValidateHealth(cwd, options, raw) {
826
921
  if ((agentStatus.installed_agents).length === 0) {
827
922
  addIssue('warning', 'W010', `No GSD agents found in ${agentStatus.agents_dir} — Task(subagent_type="gsd-*") will fall back to general-purpose`, `Run the GSD installer: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`);
828
923
  }
924
+ else if ((agentStatus.incomplete_agents).length > 0 && (agentStatus.missing_agents).length === 0) {
925
+ addIssue('warning', 'W010', `Incomplete agent installs (missing generated file): ${(agentStatus.incomplete_agents).join(', ')} — affected workflows may fall back to general-purpose`, `Re-run the GSD installer to complete the install: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`);
926
+ }
927
+ else if ((agentStatus.incomplete_agents).length > 0) {
928
+ addIssue('warning', 'W010', `Missing ${(agentStatus.missing_agents).length} GSD agents: ${(agentStatus.missing_agents).join(', ')}; incomplete agent installs (missing generated file): ${(agentStatus.incomplete_agents).join(', ')} — affected workflows will fall back to general-purpose`, `Run the GSD installer: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`);
929
+ }
829
930
  else {
830
931
  addIssue('warning', 'W010', `Missing ${(agentStatus.missing_agents).length} GSD agents: ${(agentStatus.missing_agents).join(', ')} — affected workflows will fall back to general-purpose`, `Run the GSD installer: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`);
831
932
  }
@@ -1239,6 +1340,7 @@ function cmdValidateAgents(cwd, raw) {
1239
1340
  agents_found: agentStatus.agents_installed,
1240
1341
  installed: agentStatus.installed_agents,
1241
1342
  missing: agentStatus.missing_agents,
1343
+ incomplete: agentStatus.incomplete_agents,
1242
1344
  expected,
1243
1345
  }, raw);
1244
1346
  }
@@ -1423,6 +1525,7 @@ function cmdVerifyCodebaseDrift(cwd, raw) {
1423
1525
  }
1424
1526
  }
1425
1527
  module.exports = {
1528
+ scanNegativeGrepCommentEcho,
1426
1529
  cmdVerifySummary,
1427
1530
  cmdVerifyPlanStructure,
1428
1531
  cmdVerifyPhaseCompleteness,
@@ -24,6 +24,7 @@ exports.evaluateWorktreeBaseDegrade = evaluateWorktreeBaseDegrade;
24
24
  const node_fs_1 = __importDefault(require("node:fs"));
25
25
  const node_path_1 = __importDefault(require("node:path"));
26
26
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
27
+ const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
27
28
  // ─── Internal helpers ─────────────────────────────────────────────────────────
28
29
  /**
29
30
  * Strip JSONC comments (line and block forms) from a string to produce valid JSON.
@@ -153,12 +154,18 @@ function applyWorktreeBaseRef(settings) {
153
154
  return { changed: true, settings, skipped: null, previous: null };
154
155
  }
155
156
  /**
156
- * Reads settings.local.json then settings.json under claudeDir, extracts
157
- * worktree.baseRef from the first file that provides a non-null string value.
157
+ * Reads settings files in a 3-layer cascade and extracts worktree.baseRef from
158
+ * the first layer that provides a non-null string value. Layers (highest to lowest
159
+ * precedence):
160
+ * 1. project local — <claudeDir>/settings.local.json
161
+ * 2. project shared — <claudeDir>/settings.json
162
+ * 3. user/global — <userClaudeDir>/settings.json (only when userClaudeDir is
163
+ * provided AND resolves to a different path than claudeDir)
158
164
  *
159
165
  * deps.readFile(path) must return the file contents or null on any error.
166
+ * userClaudeDir is optional; when absent/null the user/global layer is skipped.
160
167
  */
161
- function resolveEffectiveBaseRef(claudeDir, deps) {
168
+ function resolveEffectiveBaseRef(claudeDir, deps, userClaudeDir) {
162
169
  const readFile = deps?.readFile ?? ((p) => {
163
170
  try {
164
171
  return node_fs_1.default.readFileSync(p, 'utf8');
@@ -181,21 +188,39 @@ function resolveEffectiveBaseRef(claudeDir, deps) {
181
188
  return null;
182
189
  }
183
190
  }
191
+ // Layer 1: project local
184
192
  const localRef = parseBaseRef(localPath);
185
193
  if (localRef !== null)
186
194
  return localRef;
187
- return parseBaseRef(sharedPath);
195
+ // Layer 2: project shared
196
+ const sharedRef = parseBaseRef(sharedPath);
197
+ if (sharedRef !== null)
198
+ return sharedRef;
199
+ // Layer 3: user/global (only when provided and not the same directory as claudeDir)
200
+ if (userClaudeDir && node_path_1.default.resolve(userClaudeDir) !== node_path_1.default.resolve(claudeDir)) {
201
+ const userSharedPath = node_path_1.default.join(userClaudeDir, 'settings.json');
202
+ const userRef = parseBaseRef(userSharedPath);
203
+ if (userRef !== null)
204
+ return userRef;
205
+ }
206
+ return null;
188
207
  }
189
208
  /**
190
209
  * CLI command: check current worktree base-ref degradation status.
191
210
  *
192
- * Reads effective baseRef from <cwd>/.claude settings, runs degradation
193
- * evaluation, writes JSON result to stdout (or injected write), and returns
194
- * the result object.
211
+ * Reads effective baseRef from <cwd>/.claude settings (3-layer cascade:
212
+ * project local → project shared → user/global), runs degradation evaluation,
213
+ * writes JSON result to stdout (or injected write), and returns the result object.
214
+ *
215
+ * deps.userClaudeDir overrides the user/global config directory resolution
216
+ * (default: getGlobalConfigDir('claude'), which honours CLAUDE_CONFIG_DIR).
195
217
  */
196
218
  function cmdWorktreeBaseCheck(cwd, _args, deps) {
197
219
  const claudeDir = node_path_1.default.join(cwd, '.claude');
198
- const effectiveBaseRef = resolveEffectiveBaseRef(claudeDir, deps?.readFile ? { readFile: deps.readFile } : undefined);
220
+ const userClaudeDir = Object.prototype.hasOwnProperty.call(deps ?? {}, 'userClaudeDir')
221
+ ? deps.userClaudeDir
222
+ : (0, runtime_homes_cjs_1.getGlobalConfigDir)('claude');
223
+ const effectiveBaseRef = resolveEffectiveBaseRef(claudeDir, deps?.readFile ? { readFile: deps.readFile } : undefined, userClaudeDir);
199
224
  const result = evaluateWorktreeBaseDegrade({
200
225
  cwd,
201
226
  effectiveBaseRef,
@@ -14,11 +14,11 @@
14
14
  },
15
15
  "codex": {
16
16
  "opus": { "model": "gpt-5.5", "reasoning_effort": "xhigh" },
17
- "sonnet": { "model": "gpt-5.3-codex", "reasoning_effort": "medium" },
17
+ "sonnet": { "model": "gpt-5.4", "reasoning_effort": "medium" },
18
18
  "haiku": { "model": "gpt-5.4-mini", "reasoning_effort": "medium" }
19
19
  },
20
20
  "gemini": {
21
- "opus": { "model": "gemini-3-pro" },
21
+ "opus": { "model": "gemini-3.1-pro-preview" },
22
22
  "sonnet": { "model": "gemini-3-flash" },
23
23
  "haiku": { "model": "gemini-2.5-flash-lite" }
24
24
  },
@@ -52,6 +52,11 @@
52
52
  "sonnet": null,
53
53
  "haiku": null
54
54
  },
55
+ "kimi": {
56
+ "opus": null,
57
+ "sonnet": null,
58
+ "haiku": null
59
+ },
55
60
  "cursor": {
56
61
  "opus": null,
57
62
  "sonnet": null,
@@ -95,12 +100,12 @@
95
100
  "haiku": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-haiku-4-5" }, "high": { "model": "claude-sonnet-4-6" } }
96
101
  },
97
102
  "openai": {
98
- "opus": { "low": { "model": "gpt-5.3-codex", "reasoning_effort": "medium" }, "medium": { "model": "gpt-5.5", "reasoning_effort": "high" }, "high": { "model": "gpt-5.5", "reasoning_effort": "xhigh" } },
99
- "sonnet": { "low": { "model": "gpt-5.4-mini", "reasoning_effort": "low" }, "medium": { "model": "gpt-5.3-codex", "reasoning_effort": "medium" }, "high": { "model": "gpt-5.5", "reasoning_effort": "medium" } },
100
- "haiku": { "low": { "model": "gpt-5.4-mini", "reasoning_effort": "minimal" }, "medium": { "model": "gpt-5.4-mini", "reasoning_effort": "medium" }, "high": { "model": "gpt-5.3-codex", "reasoning_effort": "medium" } }
103
+ "opus": { "low": { "model": "gpt-5.4", "reasoning_effort": "medium" }, "medium": { "model": "gpt-5.5", "reasoning_effort": "high" }, "high": { "model": "gpt-5.5", "reasoning_effort": "xhigh" } },
104
+ "sonnet": { "low": { "model": "gpt-5.4-mini", "reasoning_effort": "low" }, "medium": { "model": "gpt-5.4", "reasoning_effort": "medium" }, "high": { "model": "gpt-5.5", "reasoning_effort": "medium" } },
105
+ "haiku": { "low": { "model": "gpt-5.4-mini", "reasoning_effort": "minimal" }, "medium": { "model": "gpt-5.4-mini", "reasoning_effort": "medium" }, "high": { "model": "gpt-5.4", "reasoning_effort": "medium" } }
101
106
  },
102
107
  "google": {
103
- "opus": { "low": { "model": "gemini-2.5-flash-lite" }, "medium": { "model": "gemini-3-flash" }, "high": { "model": "gemini-3-pro" } },
108
+ "opus": { "low": { "model": "gemini-2.5-flash-lite" }, "medium": { "model": "gemini-3-flash" }, "high": { "model": "gemini-3.1-pro-preview" } },
104
109
  "sonnet": { "low": { "model": "gemini-2.5-flash-lite" }, "medium": { "model": "gemini-3-flash" }, "high": { "model": "gemini-3-flash" } },
105
110
  "haiku": { "low": { "model": "gemini-2.5-flash-lite" }, "medium": { "model": "gemini-2.5-flash-lite" }, "high": { "model": "gemini-3-flash" } }
106
111
  },
@@ -43,7 +43,8 @@
43
43
  "windsurf": [
44
44
  "windsurf",
45
45
  "windsurf-cli",
46
- "windsurf-next"
46
+ "windsurf-next",
47
+ "devin-desktop"
47
48
  ],
48
49
  "augment": [
49
50
  "augment",
@@ -64,6 +65,9 @@
64
65
  "hermes-agent",
65
66
  "hermes-cli"
66
67
  ],
68
+ "kimi": [
69
+ "kimi"
70
+ ],
67
71
  "codebuddy": [
68
72
  "codebuddy",
69
73
  "codebuddy-cli"
@@ -0,0 +1,7 @@
1
+ {
2
+ "items": [
3
+ { "requirement_id": "R1", "category": "boundary", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What happens exactly at each min/max/threshold — and one step either side?" },
4
+ { "requirement_id": "R1", "category": "precision", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "Where can precision loss, overflow, or rounding/tie-breaking occur — and what is the exact contract (e.g. half-up vs half-to-even, ceil/floor/truncate)?" }
5
+ ],
6
+ "coverage": { "applicable": 2, "resolved": 0, "unresolved": 2, "byVerification": { "explicit": 0, "backstop": 0 } }
7
+ }
@@ -0,0 +1 @@
1
+ [{ "id": "R1", "text": "Round a number to N decimal places, rounding half to nearest" }]
@@ -0,0 +1,8 @@
1
+ {
2
+ "items": [
3
+ { "requirement_id": "R1", "category": "adjacency", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "When two things are exactly equal or just touch, do they merge, collide, or separate?" },
4
+ { "requirement_id": "R1", "category": "empty", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "What is the result for empty, single-element, or null input?" },
5
+ { "requirement_id": "R1", "category": "ordering", "status": "unresolved", "verification": null, "resolution": null, "reason": null, "probe": "When elements compare equal, is output order specified and stable?" }
6
+ ],
7
+ "coverage": { "applicable": 3, "resolved": 0, "unresolved": 3, "byVerification": { "explicit": 0, "backstop": 0 } }
8
+ }
@@ -0,0 +1 @@
1
+ [{ "id": "R1", "text": "Merge a list of overlapping intervals into the minimal set" }]