@opengsd/gsd-core 1.20.1 → 1.21.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 (135) 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 +33 -2
  4. package/agents/gsd-executor.md +1 -1
  5. package/agents/gsd-plan-checker.md +1 -1
  6. package/agents/gsd-planner.md +3 -3
  7. package/agents/gsd-verifier.md +2 -2
  8. package/bin/install.js +234 -126
  9. package/commands/gsd/autonomous.md +1 -1
  10. package/gsd-core/bin/gsd-tools.cjs +450 -753
  11. package/gsd-core/bin/lib/capability-activation.cjs +8 -2
  12. package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
  13. package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
  14. package/gsd-core/bin/lib/capability-lock.cjs +19 -27
  15. package/gsd-core/bin/lib/capability-registry.cjs +74 -71
  16. package/gsd-core/bin/lib/capability-validator.cjs +1 -0
  17. package/gsd-core/bin/lib/capability-writer.cjs +7 -4
  18. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
  19. package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
  20. package/gsd-core/bin/lib/cli-surface.cjs +545 -0
  21. package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
  22. package/gsd-core/bin/lib/commands.cjs +80 -2
  23. package/gsd-core/bin/lib/config-file-read.cjs +85 -0
  24. package/gsd-core/bin/lib/config-loader.cjs +23 -15
  25. package/gsd-core/bin/lib/config.cjs +139 -101
  26. package/gsd-core/bin/lib/configuration.cjs +41 -2
  27. package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
  28. package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
  29. package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
  30. package/gsd-core/bin/lib/gap-checker.cjs +12 -1
  31. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
  32. package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
  33. package/gsd-core/bin/lib/graphify.cjs +19 -2
  34. package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
  35. package/gsd-core/bin/lib/init.cjs +13 -11
  36. package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
  37. package/gsd-core/bin/lib/install-engine.cjs +11 -19
  38. package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
  39. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
  40. package/gsd-core/bin/lib/io.cjs +6 -0
  41. package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
  42. package/gsd-core/bin/lib/markdown-table.cjs +39 -1
  43. package/gsd-core/bin/lib/milestone.cjs +8 -6
  44. package/gsd-core/bin/lib/model-resolver.cjs +8 -4
  45. package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
  46. package/gsd-core/bin/lib/phase-locator.cjs +8 -3
  47. package/gsd-core/bin/lib/phase.cjs +107 -31
  48. package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
  49. package/gsd-core/bin/lib/planning-snapshot.cjs +8 -4
  50. package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
  51. package/gsd-core/bin/lib/profile-output.cjs +9 -6
  52. package/gsd-core/bin/lib/project-context.cjs +439 -0
  53. package/gsd-core/bin/lib/project-root.cjs +9 -230
  54. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
  55. package/gsd-core/bin/lib/report-parser.cjs +16 -0
  56. package/gsd-core/bin/lib/roadmap-parser.cjs +328 -50
  57. package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
  58. package/gsd-core/bin/lib/roadmap.cjs +2 -2
  59. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -4
  60. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
  61. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
  62. package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
  63. package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
  64. package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
  65. package/gsd-core/bin/lib/state-transition.cjs +91 -41
  66. package/gsd-core/bin/lib/state.cjs +446 -61
  67. package/gsd-core/bin/lib/surface.cjs +4 -7
  68. package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
  69. package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
  70. package/gsd-core/bin/lib/verify.cjs +49 -6
  71. package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
  72. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  73. package/gsd-core/references/autonomous-smart-discuss.md +99 -18
  74. package/gsd-core/references/checkpoints.md +1 -1
  75. package/gsd-core/references/planner-guidance.md +11 -0
  76. package/gsd-core/references/planner-human-verify-mode.md +1 -1
  77. package/gsd-core/references/planner-reversibility.md +2 -0
  78. package/gsd-core/references/planner-verify-command-grounding.md +22 -6
  79. package/gsd-core/workflows/autonomous.md +4 -4
  80. package/gsd-core/workflows/code-review-fix.md +5 -7
  81. package/gsd-core/workflows/execute-phase.md +21 -7
  82. package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
  83. package/gsd-core/workflows/plan-phase.md +18 -19
  84. package/hooks/dist/gsd-agent-isolation-guard.js +3 -2
  85. package/hooks/dist/gsd-config-reload.js +2 -2
  86. package/hooks/dist/gsd-context-monitor.js +2 -1
  87. package/hooks/dist/gsd-cursor-subagent-start.js +8 -5
  88. package/hooks/dist/gsd-read-injection-scanner.js +2 -2
  89. package/hooks/dist/gsd-secret-read-guard.js +141 -6
  90. package/hooks/dist/gsd-statusline.js +3 -2
  91. package/hooks/dist/gsd-workflow-guard.js +2 -1
  92. package/hooks/dist/lib/config-read.js +49 -0
  93. package/hooks/dist/lib/git-cmd.js +3 -1
  94. package/hooks/dist/lib/git-head-tree.js +474 -0
  95. package/hooks/dist/lib/isolation-sentinel.js +97 -17
  96. package/hooks/dist/lib/secret-read-exemptions.js +161 -0
  97. package/hooks/gsd-agent-isolation-guard.js +3 -2
  98. package/hooks/gsd-config-reload.js +2 -2
  99. package/hooks/gsd-context-monitor.js +2 -1
  100. package/hooks/gsd-cursor-subagent-start.js +8 -5
  101. package/hooks/gsd-read-injection-scanner.js +2 -2
  102. package/hooks/gsd-secret-read-guard.js +141 -6
  103. package/hooks/gsd-statusline.js +3 -2
  104. package/hooks/gsd-workflow-guard.js +2 -1
  105. package/hooks/lib/config-read.js +49 -0
  106. package/hooks/lib/git-cmd.js +3 -1
  107. package/hooks/lib/git-head-tree.js +474 -0
  108. package/hooks/lib/isolation-sentinel.js +97 -17
  109. package/hooks/lib/secret-read-exemptions.js +161 -0
  110. package/package.json +7 -2
  111. package/pi/package-agents/gsd-executor.md +1 -1
  112. package/pi/package-agents/gsd-plan-checker.md +1 -1
  113. package/pi/package-agents/gsd-planner.md +3 -3
  114. package/pi/package-agents/gsd-verifier.md +2 -2
  115. package/pi/package-agents/manifest.json +4 -4
  116. package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
  117. package/scripts/changeset/cli.cjs +2 -2
  118. package/scripts/changeset/serialize.cjs +12 -2
  119. package/scripts/ci-compact-v8-coverage.cjs +204 -0
  120. package/scripts/ci-fix-next-rerun.cjs +149 -0
  121. package/scripts/ci-next-health.cjs +52 -9
  122. package/scripts/ci-test-scope.cjs +77 -71
  123. package/scripts/command-contract-helpers.cjs +56 -7
  124. package/scripts/docs-guard-registry.cjs +3 -0
  125. package/scripts/gen-cli-surface-manifest.cjs +287 -0
  126. package/scripts/gen-platform-conformance-tier.cjs +258 -19
  127. package/scripts/lib/macos-conformance-tier.generated.cjs +55 -47
  128. package/scripts/lib/platform-conformance-tier.generated.cjs +19 -64
  129. package/scripts/lib/shellcheck-fetch.cjs +71 -4
  130. package/scripts/lint-removed-but-needed.cjs +562 -50
  131. package/scripts/release-notes/changelog-release-notes.cjs +345 -0
  132. package/scripts/release-notes/discord-release-summary.cjs +82 -1
  133. package/scripts/timeout-evidence-from-ci.cjs +964 -0
  134. package/skills/gsd-autonomous/SKILL.md +1 -1
  135. package/vscode/package.json +1 -1
@@ -0,0 +1,380 @@
1
+ "use strict";
2
+ /**
3
+ * Criterion Replay Module (#4310, ADR-4310).
4
+ *
5
+ * Re-runs the command behind each judged `[HEAD=<v> EXPECT<op><v>]` notation
6
+ * and compares the printed value with the recorded HEAD value. Pure; the
7
+ * runner, clock and HEAD sha are injected, so this module does no I/O.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.MAX_REPLAY_STATUSES = exports.REPLAY_LIMITS = exports.REPLAY_STATUS = void 0;
11
+ exports.extractCriterionCommand = extractCriterionCommand;
12
+ exports.tokenizeCriterionCommand = tokenizeCriterionCommand;
13
+ exports.checkReplayQuarantine = checkReplayQuarantine;
14
+ exports.deriveObservedValue = deriveObservedValue;
15
+ exports.replayBaselineCriteria = replayBaselineCriteria;
16
+ const criterion_baseline_cjs_1 = require("./criterion-baseline.cjs");
17
+ exports.REPLAY_STATUS = Object.freeze({
18
+ NO_COMMAND: 'no_command',
19
+ AMBIGUOUS_COMMAND: 'ambiguous_command',
20
+ MALFORMED_COMMAND: 'malformed_command',
21
+ DISALLOWED: 'disallowed',
22
+ CANNOT_RUN: 'cannot_run',
23
+ TIMEOUT: 'timeout',
24
+ OUTPUT_CAP: 'output_cap',
25
+ BUDGET_EXHAUSTED: 'budget_exhausted',
26
+ TRUNCATED: 'truncated',
27
+ REPLAY_ERROR: 'replay_error',
28
+ });
29
+ exports.REPLAY_LIMITS = Object.freeze({
30
+ MAX_COMMANDS: 50,
31
+ PER_COMMAND_TIMEOUT_MS: 10000,
32
+ TOTAL_BUDGET_MS: 60000,
33
+ OUTPUT_CAP_BYTES: 65536,
34
+ MAX_COMMAND_LENGTH: 1000,
35
+ MAX_TOKENS: 64,
36
+ });
37
+ /** Most statuses a replay records; the rest collapse into the one `truncated` entry. */
38
+ exports.MAX_REPLAY_STATUSES = 50;
39
+ const ZONE_TAG_PREFIX = /^\s*(?:<(?:verify|automated|acceptance_criteria)>\s*)*/;
40
+ function extractCriterionCommand(lineText, column) {
41
+ const prefix = lineText.slice(0, column);
42
+ const spans = prefix.match(/`([^`\r\n]*)`/g) ?? [];
43
+ let cmd;
44
+ if (spans.length > 1) {
45
+ return { status: exports.REPLAY_STATUS.AMBIGUOUS_COMMAND, detail: 'more than one backtick span precedes the notation' };
46
+ }
47
+ if (spans.length === 1) {
48
+ cmd = spans[0].slice(1, -1).trim();
49
+ }
50
+ else {
51
+ const trimmed = prefix.replace(/[ \t]+$/, '');
52
+ if (!trimmed.endsWith('#')) {
53
+ return { status: exports.REPLAY_STATUS.NO_COMMAND, detail: 'no backtick span or trailing comment names a command' };
54
+ }
55
+ cmd = trimmed.slice(0, -1).replace(ZONE_TAG_PREFIX, '').trim();
56
+ }
57
+ if (cmd.length === 0)
58
+ return { status: exports.REPLAY_STATUS.NO_COMMAND, detail: 'the command is empty' };
59
+ return { command: (0, criterion_baseline_cjs_1.decodeNotationEntities)(cmd) };
60
+ }
61
+ const UNQUOTED_META = new Set(['|', '&', ';', '<', '>', '(', ')', '$', '`', '*', '?', '[', '{', '}', '~']);
62
+ /** A recognizer for plain words and quotes, not a shell: anything a shell would interpret is refused. */
63
+ function tokenizeCriterionCommand(command) {
64
+ if (command.length > exports.REPLAY_LIMITS.MAX_COMMAND_LENGTH) {
65
+ return { status: exports.REPLAY_STATUS.DISALLOWED, detail: `command longer than ${exports.REPLAY_LIMITS.MAX_COMMAND_LENGTH} characters` };
66
+ }
67
+ if (/[\r\n\u0000]/.test(command)) {
68
+ return { status: exports.REPLAY_STATUS.DISALLOWED, detail: 'command contains a line break or NUL' };
69
+ }
70
+ const argv = [];
71
+ let cur = null;
72
+ let i = 0;
73
+ while (i < command.length) {
74
+ const ch = command[i];
75
+ if (ch === ' ' || ch === '\t') {
76
+ if (cur !== null)
77
+ argv.push(cur);
78
+ cur = null;
79
+ i += 1;
80
+ }
81
+ else if (ch === "'") {
82
+ const close = command.indexOf("'", i + 1);
83
+ if (close === -1)
84
+ return { status: exports.REPLAY_STATUS.MALFORMED_COMMAND, detail: 'unterminated single quote' };
85
+ cur = (cur ?? '') + command.slice(i + 1, close);
86
+ i = close + 1;
87
+ }
88
+ else if (ch === '"') {
89
+ let j = i + 1;
90
+ let buf = '';
91
+ let closed = false;
92
+ while (j < command.length) {
93
+ const c = command[j];
94
+ if (c === '"') {
95
+ closed = true;
96
+ break;
97
+ }
98
+ if (c === '$' || c === '`') {
99
+ return { status: exports.REPLAY_STATUS.DISALLOWED, detail: 'expansion inside double quotes' };
100
+ }
101
+ if (c === '\\' && j + 1 < command.length) {
102
+ const next = command[j + 1];
103
+ buf += next === '"' || next === '\\' || next === '$' || next === '`' ? next : `\\${next}`;
104
+ j += 2;
105
+ continue;
106
+ }
107
+ buf += c;
108
+ j += 1;
109
+ }
110
+ if (!closed)
111
+ return { status: exports.REPLAY_STATUS.MALFORMED_COMMAND, detail: 'unterminated double quote' };
112
+ cur = (cur ?? '') + buf;
113
+ i = j + 1;
114
+ }
115
+ else if (ch === '\\') {
116
+ if (i + 1 >= command.length)
117
+ return { status: exports.REPLAY_STATUS.MALFORMED_COMMAND, detail: 'trailing backslash' };
118
+ cur = (cur ?? '') + command[i + 1];
119
+ i += 2;
120
+ }
121
+ else if (ch === '#' && cur === null) {
122
+ return { status: exports.REPLAY_STATUS.DISALLOWED, detail: 'shell comment' };
123
+ }
124
+ else if (UNQUOTED_META.has(ch)) {
125
+ return { status: exports.REPLAY_STATUS.DISALLOWED, detail: `unquoted shell metacharacter ${ch}` };
126
+ }
127
+ else {
128
+ cur = (cur ?? '') + ch;
129
+ i += 1;
130
+ }
131
+ }
132
+ if (cur !== null)
133
+ argv.push(cur);
134
+ if (argv.length === 0)
135
+ return { status: exports.REPLAY_STATUS.NO_COMMAND, detail: 'the command has no words' };
136
+ if (argv.length > exports.REPLAY_LIMITS.MAX_TOKENS) {
137
+ return { status: exports.REPLAY_STATUS.DISALLOWED, detail: `more than ${exports.REPLAY_LIMITS.MAX_TOKENS} tokens` };
138
+ }
139
+ return { argv };
140
+ }
141
+ const ALLOWED_PROGRAMS = new Set(['grep', 'git']);
142
+ const ALLOWED_GIT_SUBCOMMANDS = new Set(['grep', 'ls-files', 'rev-parse', 'rev-list']);
143
+ const words = (s) => new Set(s.split(' ').filter((w) => w.length > 0));
144
+ const chars = (s) => new Set(s);
145
+ const GREP_SPEC = {
146
+ short: chars('civwxFEGnlLqsrhHoyI'),
147
+ long: words('--count --ignore-case --invert-match --word-regexp --line-regexp --fixed-strings --extended-regexp --basic-regexp --quiet --silent --no-messages --recursive --no-filename --with-filename --files-with-matches --files-without-match --only-matching --line-number'),
148
+ valued: words('--regexp --include --exclude --exclude-dir'),
149
+ valueShort: '-e',
150
+ };
151
+ const GIT_SPECS = Object.freeze({
152
+ grep: {
153
+ short: chars('civwnlLhHFEGPIoq'),
154
+ long: words('--count --ignore-case --invert-match --word-regexp --line-number --files-with-matches --name-only --files-without-match --fixed-strings --extended-regexp --basic-regexp --perl-regexp --quiet --cached --untracked --no-index --full-name --and --or --not --all-match'),
155
+ valued: words(''),
156
+ valueShort: '-e',
157
+ },
158
+ 'ls-files': {
159
+ short: words('c o d m'),
160
+ long: words('--cached --others --deleted --modified --exclude-standard --full-name'),
161
+ valued: words(''),
162
+ valueShort: null,
163
+ },
164
+ 'rev-parse': {
165
+ short: words('q'),
166
+ long: words('--verify --quiet --is-inside-work-tree --show-toplevel --abbrev-ref --show-prefix --short'),
167
+ valued: words('--short'),
168
+ valueShort: null,
169
+ },
170
+ 'rev-list': {
171
+ short: words(''),
172
+ long: words('--count --all --no-merges --merges --first-parent'),
173
+ valued: words('--max-count'),
174
+ valueShort: '-n',
175
+ },
176
+ });
177
+ function isOutsideRoot(candidate) {
178
+ return /^[\\/]/.test(candidate) || /^[A-Za-z]:[\\/]/.test(candidate) || candidate.split(/[\\/]/).includes('..');
179
+ }
180
+ /** Exact-match option allowlist: an option not named here (including any abbreviation of one) is refused. */
181
+ function checkReplayQuarantine(argv, opts = {}) {
182
+ const program = argv[0];
183
+ if (typeof program !== 'string' || !ALLOWED_PROGRAMS.has(program)) {
184
+ return { status: 'disallowed', detail: 'program is not on the allowlist (grep, git)' };
185
+ }
186
+ let spec;
187
+ let tokens;
188
+ if (program === 'git') {
189
+ if (argv.length < 2 || !ALLOWED_GIT_SUBCOMMANDS.has(argv[1])) {
190
+ return { status: 'disallowed', detail: 'git subcommand is not on the allowlist' };
191
+ }
192
+ spec = GIT_SPECS[argv[1]];
193
+ tokens = argv.slice(2);
194
+ }
195
+ else {
196
+ spec = GREP_SPEC;
197
+ tokens = argv.slice(1);
198
+ }
199
+ const disallowed = (detail) => ({ status: 'disallowed', detail });
200
+ const pathProblem = (candidate) => {
201
+ if (isOutsideRoot(candidate))
202
+ return true;
203
+ return opts.isInsideRoot !== undefined && !opts.isInsideRoot(candidate);
204
+ };
205
+ let operandsOnly = false;
206
+ let takesValueNext = false;
207
+ for (const token of tokens) {
208
+ if (takesValueNext) {
209
+ takesValueNext = false;
210
+ if (pathProblem(token))
211
+ return disallowed('path_outside_root');
212
+ continue;
213
+ }
214
+ if (operandsOnly || !token.startsWith('-') || token === '-') {
215
+ if (pathProblem(token))
216
+ return disallowed('path_outside_root');
217
+ continue;
218
+ }
219
+ if (token === '--') {
220
+ operandsOnly = true;
221
+ continue;
222
+ }
223
+ if (token.startsWith('--')) {
224
+ const eq = token.indexOf('=');
225
+ const name = eq === -1 ? token : token.slice(0, eq);
226
+ const value = eq === -1 ? undefined : token.slice(eq + 1);
227
+ if (spec.valued.has(name)) {
228
+ if (value === undefined) {
229
+ if (!spec.long.has(name))
230
+ return disallowed('option ' + name + ' needs =value');
231
+ }
232
+ else if (pathProblem(value)) {
233
+ return disallowed('path_outside_root');
234
+ }
235
+ }
236
+ else if (spec.long.has(name)) {
237
+ if (value !== undefined)
238
+ return disallowed('option ' + name + ' takes no value');
239
+ }
240
+ else {
241
+ return disallowed('option ' + name + ' is not allowed');
242
+ }
243
+ continue;
244
+ }
245
+ if (spec.valueShort !== null && token === spec.valueShort) {
246
+ takesValueNext = true;
247
+ continue;
248
+ }
249
+ for (const ch of token.slice(1)) {
250
+ if (!spec.short.has(ch))
251
+ return disallowed('option ' + token + ' is not allowed');
252
+ }
253
+ }
254
+ if (takesValueNext)
255
+ return disallowed('malformed: option is missing its value');
256
+ return null;
257
+ }
258
+ function deriveObservedValue(r) {
259
+ if (r.outcome === 'timed_out')
260
+ return { status: exports.REPLAY_STATUS.TIMEOUT, detail: 'the command timed out' };
261
+ if (r.outcome === 'overflow')
262
+ return { status: exports.REPLAY_STATUS.OUTPUT_CAP, detail: 'the command printed more than the output cap' };
263
+ if (r.outcome !== 'exited')
264
+ return { status: exports.REPLAY_STATUS.CANNOT_RUN, detail: `the command did not exit (${String(r.outcome)})` };
265
+ const code = r.exitCode;
266
+ if (typeof code !== 'number' || !Number.isInteger(code) || code < 0 || code > 255) {
267
+ return { status: exports.REPLAY_STATUS.CANNOT_RUN, detail: 'the exit code is not in 0..255' };
268
+ }
269
+ if (code >= 2)
270
+ return { value: { kind: 'error', code } };
271
+ const s = String(r.stdout ?? '').trim();
272
+ if (/^-?\d{1,15}$/.test(s))
273
+ return { value: { kind: 'integer', value: Number(s) } };
274
+ return { value: code === 0 ? { kind: 'present' } : { kind: 'absent' } };
275
+ }
276
+ function lineBounds(content, index) {
277
+ const start = Math.max(content.lastIndexOf('\n', index - 1), content.lastIndexOf('\r', index - 1)) + 1;
278
+ const nl = content.indexOf('\n', index);
279
+ const cr = content.indexOf('\r', index);
280
+ const ends = [nl, cr].filter((n) => n !== -1);
281
+ return { start, end: ends.length > 0 ? Math.min(...ends) : content.length };
282
+ }
283
+ function replayBaselineCriteria(content, deps) {
284
+ const headSha = deps.headSha ?? null;
285
+ const report = { head_sha: headSha, results: [], mismatch_count: 0, statuses: [], warnings: [] };
286
+ try {
287
+ if (typeof content !== 'string' || content.length === 0)
288
+ return report;
289
+ const records = (deps.scan ?? criterion_baseline_cjs_1.scanBaselineNotation)(content).notations;
290
+ const start = deps.now();
291
+ let ran = 0;
292
+ let skipped = 0;
293
+ let dropped = 0;
294
+ const addStatus = (code, line, text, detail) => {
295
+ if (report.statuses.length >= exports.MAX_REPLAY_STATUSES) {
296
+ dropped += 1;
297
+ return;
298
+ }
299
+ report.statuses.push({ code, line, text, detail });
300
+ };
301
+ for (const record of records) {
302
+ const { start: lineStart, end: lineEnd } = lineBounds(content, record.index);
303
+ const lineText = content.slice(lineStart, lineEnd);
304
+ const extracted = extractCriterionCommand(lineText, record.index - lineStart);
305
+ if ('status' in extracted) {
306
+ addStatus(extracted.status, record.line, record.text, extracted.detail);
307
+ continue;
308
+ }
309
+ const tokens = tokenizeCriterionCommand(extracted.command);
310
+ if ('status' in tokens) {
311
+ addStatus(tokens.status, record.line, record.text, tokens.detail);
312
+ continue;
313
+ }
314
+ const refused = checkReplayQuarantine(tokens.argv, { isInsideRoot: deps.isInsideRoot });
315
+ if (refused !== null) {
316
+ addStatus(refused.status, record.line, record.text, refused.detail);
317
+ continue;
318
+ }
319
+ if (ran >= exports.REPLAY_LIMITS.MAX_COMMANDS) {
320
+ skipped += 1;
321
+ continue;
322
+ }
323
+ const elapsed = deps.now() - start;
324
+ if (elapsed >= exports.REPLAY_LIMITS.TOTAL_BUDGET_MS) {
325
+ addStatus(exports.REPLAY_STATUS.BUDGET_EXHAUSTED, record.line, record.text, 'the total replay time budget is spent');
326
+ continue;
327
+ }
328
+ const timeoutMs = Math.min(exports.REPLAY_LIMITS.PER_COMMAND_TIMEOUT_MS, exports.REPLAY_LIMITS.TOTAL_BUDGET_MS - elapsed);
329
+ ran += 1;
330
+ let raw;
331
+ try {
332
+ raw = deps.run(tokens.argv, { timeoutMs });
333
+ }
334
+ catch (e) {
335
+ addStatus(exports.REPLAY_STATUS.CANNOT_RUN, record.line, record.text, e instanceof Error ? e.message : String(e));
336
+ continue;
337
+ }
338
+ const derived = deriveObservedValue(raw);
339
+ if ('status' in derived) {
340
+ addStatus(derived.status, record.line, record.text, derived.detail);
341
+ continue;
342
+ }
343
+ const observed = derived.value;
344
+ const recorded = record.notation.head;
345
+ const matched = (0, criterion_baseline_cjs_1.compareBaseline)(recorded, '==', observed) === true;
346
+ const revDrift = record.notation.rev !== null && typeof deps.headSha === 'string' && !deps.headSha.startsWith(record.notation.rev);
347
+ report.results.push({
348
+ line: record.line,
349
+ text: record.text,
350
+ argv: tokens.argv,
351
+ recorded,
352
+ observed,
353
+ outcome: matched ? 'match' : 'mismatch',
354
+ rev_drift: revDrift,
355
+ });
356
+ if (!matched) {
357
+ report.mismatch_count += 1;
358
+ report.warnings.push(`[plan-criteria replay] line ${record.line}: recorded HEAD=${(0, criterion_baseline_cjs_1.renderBaselineValue)(recorded)}, the command printed ${(0, criterion_baseline_cjs_1.renderBaselineValue)(observed)}${revDrift ? ' (baseline pinned to another commit)' : ''}`);
359
+ }
360
+ }
361
+ if (skipped > 0 || dropped > 0) {
362
+ report.statuses.push({
363
+ code: exports.REPLAY_STATUS.TRUNCATED,
364
+ line: 0,
365
+ text: '',
366
+ detail: `${skipped} command(s) not replayed after the first ${exports.REPLAY_LIMITS.MAX_COMMANDS}; ${dropped} further status(es) omitted`,
367
+ });
368
+ }
369
+ return report;
370
+ }
371
+ catch (e) {
372
+ return {
373
+ head_sha: headSha,
374
+ results: [],
375
+ mismatch_count: 0,
376
+ statuses: [{ code: exports.REPLAY_STATUS.REPLAY_ERROR, line: 0, text: '', detail: e instanceof Error ? e.message : String(e) }],
377
+ warnings: [],
378
+ };
379
+ }
380
+ }
@@ -94,7 +94,11 @@ function defaultBudget() {
94
94
  function readSmartZoneBudget(cwd) {
95
95
  try {
96
96
  const configPath = node_path_1.default.join(planningDir(cwd), 'config.json');
97
- const parsed = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
97
+ // Bounded read (#5759): null (absent) and any throw (FIFO, device, oversized) keep the default.
98
+ const raw = configLoader.readConfigFileBounded(configPath);
99
+ if (raw === null)
100
+ return defaultBudget();
101
+ const parsed = JSON.parse(raw);
98
102
  if (parsed !== null && typeof parsed === 'object') {
99
103
  const workflow = parsed['workflow'];
100
104
  if (workflow !== null && typeof workflow === 'object') {
@@ -25,6 +25,9 @@ const io = require("./io.cjs");
25
25
  const { output, error, formatDiagnosticToken } = io;
26
26
  const pattern_cjs_1 = require("./pattern.cjs");
27
27
  const gate_evidence_cjs_1 = require("./gate-evidence.cjs");
28
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- config-file-read.cjs is an export= CommonJS module
29
+ const configFileReadMod = require("./config-file-read.cjs");
30
+ const { readConfigFileBounded } = configFileReadMod;
28
31
  // eslint-disable-next-line @typescript-eslint/no-require-imports
29
32
  const planningWorkspace = require("./planning-workspace.cjs");
30
33
  const { planningPaths, planningDir, findContextMdIn } = planningWorkspace;
@@ -156,7 +159,15 @@ function parseJsonEvidence(text, span) {
156
159
  */
157
160
  function readGate(cwd) {
158
161
  const cfgPath = node_path_1.default.join(planningDir(cwd), 'config.json');
159
- const text = (0, gate_evidence_cjs_1.readTextEvidence)(cfgPath);
162
+ // Bounded read (#5759): a FIFO/device/oversized config is `unreadable` (reported), never waited on.
163
+ let text;
164
+ try {
165
+ const raw = readConfigFileBounded(cfgPath);
166
+ text = raw === null ? (0, gate_evidence_cjs_1.evidenceNone)() : (0, gate_evidence_cjs_1.evidenceFound)(raw);
167
+ }
168
+ catch (err) {
169
+ text = (0, gate_evidence_cjs_1.evidenceFromError)(err, cfgPath);
170
+ }
160
171
  if (text.kind === 'none')
161
172
  return { enabled: true, unreadable: [] };
162
173
  if (text.kind === 'unreadable')