@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +33 -2
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +3 -3
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +234 -126
- package/commands/gsd/autonomous.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +450 -753
- package/gsd-core/bin/lib/capability-activation.cjs +8 -2
- package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
- package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
- package/gsd-core/bin/lib/capability-lock.cjs +19 -27
- package/gsd-core/bin/lib/capability-registry.cjs +74 -71
- package/gsd-core/bin/lib/capability-validator.cjs +1 -0
- package/gsd-core/bin/lib/capability-writer.cjs +7 -4
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
- package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
- package/gsd-core/bin/lib/cli-surface.cjs +545 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
- package/gsd-core/bin/lib/commands.cjs +80 -2
- package/gsd-core/bin/lib/config-file-read.cjs +85 -0
- package/gsd-core/bin/lib/config-loader.cjs +23 -15
- package/gsd-core/bin/lib/config.cjs +139 -101
- package/gsd-core/bin/lib/configuration.cjs +41 -2
- package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
- package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
- package/gsd-core/bin/lib/gap-checker.cjs +12 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
- package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
- package/gsd-core/bin/lib/graphify.cjs +19 -2
- package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
- package/gsd-core/bin/lib/init.cjs +13 -11
- package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
- package/gsd-core/bin/lib/install-engine.cjs +11 -19
- package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
- package/gsd-core/bin/lib/markdown-table.cjs +39 -1
- package/gsd-core/bin/lib/milestone.cjs +8 -6
- package/gsd-core/bin/lib/model-resolver.cjs +8 -4
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
- package/gsd-core/bin/lib/phase-locator.cjs +8 -3
- package/gsd-core/bin/lib/phase.cjs +107 -31
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
- package/gsd-core/bin/lib/planning-snapshot.cjs +8 -4
- package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
- package/gsd-core/bin/lib/profile-output.cjs +9 -6
- package/gsd-core/bin/lib/project-context.cjs +439 -0
- package/gsd-core/bin/lib/project-root.cjs +9 -230
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
- package/gsd-core/bin/lib/report-parser.cjs +16 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +328 -50
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
- package/gsd-core/bin/lib/roadmap.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -4
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
- package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
- package/gsd-core/bin/lib/state-transition.cjs +91 -41
- package/gsd-core/bin/lib/state.cjs +446 -61
- package/gsd-core/bin/lib/surface.cjs +4 -7
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
- package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
- package/gsd-core/bin/lib/verify.cjs +49 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/autonomous-smart-discuss.md +99 -18
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/planner-guidance.md +11 -0
- package/gsd-core/references/planner-human-verify-mode.md +1 -1
- package/gsd-core/references/planner-reversibility.md +2 -0
- package/gsd-core/references/planner-verify-command-grounding.md +22 -6
- package/gsd-core/workflows/autonomous.md +4 -4
- package/gsd-core/workflows/code-review-fix.md +5 -7
- package/gsd-core/workflows/execute-phase.md +21 -7
- package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
- package/gsd-core/workflows/plan-phase.md +18 -19
- package/hooks/dist/gsd-agent-isolation-guard.js +3 -2
- package/hooks/dist/gsd-config-reload.js +2 -2
- package/hooks/dist/gsd-context-monitor.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +8 -5
- package/hooks/dist/gsd-read-injection-scanner.js +2 -2
- package/hooks/dist/gsd-secret-read-guard.js +141 -6
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/config-read.js +49 -0
- package/hooks/dist/lib/git-cmd.js +3 -1
- package/hooks/dist/lib/git-head-tree.js +474 -0
- package/hooks/dist/lib/isolation-sentinel.js +97 -17
- package/hooks/dist/lib/secret-read-exemptions.js +161 -0
- package/hooks/gsd-agent-isolation-guard.js +3 -2
- package/hooks/gsd-config-reload.js +2 -2
- package/hooks/gsd-context-monitor.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +8 -5
- package/hooks/gsd-read-injection-scanner.js +2 -2
- package/hooks/gsd-secret-read-guard.js +141 -6
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/lib/config-read.js +49 -0
- package/hooks/lib/git-cmd.js +3 -1
- package/hooks/lib/git-head-tree.js +474 -0
- package/hooks/lib/isolation-sentinel.js +97 -17
- package/hooks/lib/secret-read-exemptions.js +161 -0
- package/package.json +7 -2
- package/pi/package-agents/gsd-executor.md +1 -1
- package/pi/package-agents/gsd-plan-checker.md +1 -1
- package/pi/package-agents/gsd-planner.md +3 -3
- package/pi/package-agents/gsd-verifier.md +2 -2
- package/pi/package-agents/manifest.json +4 -4
- package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
- package/scripts/changeset/cli.cjs +2 -2
- package/scripts/changeset/serialize.cjs +12 -2
- package/scripts/ci-compact-v8-coverage.cjs +204 -0
- package/scripts/ci-fix-next-rerun.cjs +149 -0
- package/scripts/ci-next-health.cjs +52 -9
- package/scripts/ci-test-scope.cjs +77 -71
- package/scripts/command-contract-helpers.cjs +56 -7
- package/scripts/docs-guard-registry.cjs +3 -0
- package/scripts/gen-cli-surface-manifest.cjs +287 -0
- package/scripts/gen-platform-conformance-tier.cjs +258 -19
- package/scripts/lib/macos-conformance-tier.generated.cjs +55 -47
- package/scripts/lib/platform-conformance-tier.generated.cjs +19 -64
- package/scripts/lib/shellcheck-fetch.cjs +71 -4
- package/scripts/lint-removed-but-needed.cjs +562 -50
- package/scripts/release-notes/changelog-release-notes.cjs +345 -0
- package/scripts/release-notes/discord-release-summary.cjs +82 -1
- package/scripts/timeout-evidence-from-ci.cjs +964 -0
- package/skills/gsd-autonomous/SKILL.md +1 -1
- 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
|
-
|
|
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
|
-
|
|
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')
|