@opengsd/gsd-core 1.20.0 → 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 (154) 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 +265 -161
  9. package/commands/gsd/autonomous.md +1 -1
  10. package/commands/gsd/new-milestone.md +2 -2
  11. package/commands/gsd/new-project.md +2 -2
  12. package/gsd-core/bin/gsd-tools.cjs +554 -823
  13. package/gsd-core/bin/lib/capability-activation.cjs +8 -2
  14. package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
  15. package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
  16. package/gsd-core/bin/lib/capability-lock.cjs +19 -27
  17. package/gsd-core/bin/lib/capability-registry.cjs +74 -71
  18. package/gsd-core/bin/lib/capability-validator.cjs +1 -0
  19. package/gsd-core/bin/lib/capability-writer.cjs +7 -4
  20. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
  21. package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
  22. package/gsd-core/bin/lib/cli-surface.cjs +545 -0
  23. package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
  24. package/gsd-core/bin/lib/commands.cjs +99 -2
  25. package/gsd-core/bin/lib/config-file-read.cjs +85 -0
  26. package/gsd-core/bin/lib/config-loader.cjs +23 -15
  27. package/gsd-core/bin/lib/config.cjs +139 -101
  28. package/gsd-core/bin/lib/configuration.cjs +41 -2
  29. package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
  30. package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
  31. package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
  32. package/gsd-core/bin/lib/gap-checker.cjs +12 -1
  33. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
  34. package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
  35. package/gsd-core/bin/lib/graphify.cjs +19 -2
  36. package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
  37. package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
  38. package/gsd-core/bin/lib/init.cjs +20 -18
  39. package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
  40. package/gsd-core/bin/lib/install-engine.cjs +20 -28
  41. package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
  42. package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
  43. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
  44. package/gsd-core/bin/lib/install-profiles.cjs +4 -2
  45. package/gsd-core/bin/lib/io.cjs +6 -0
  46. package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
  47. package/gsd-core/bin/lib/markdown-table.cjs +39 -1
  48. package/gsd-core/bin/lib/milestone.cjs +8 -6
  49. package/gsd-core/bin/lib/model-resolver.cjs +8 -4
  50. package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
  51. package/gsd-core/bin/lib/phase-id.cjs +26 -9
  52. package/gsd-core/bin/lib/phase-locator.cjs +8 -3
  53. package/gsd-core/bin/lib/phase.cjs +170 -45
  54. package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
  55. package/gsd-core/bin/lib/planning-snapshot.cjs +10 -6
  56. package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
  57. package/gsd-core/bin/lib/profile-output.cjs +9 -6
  58. package/gsd-core/bin/lib/project-context.cjs +439 -0
  59. package/gsd-core/bin/lib/project-root.cjs +9 -230
  60. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
  61. package/gsd-core/bin/lib/report-parser.cjs +16 -0
  62. package/gsd-core/bin/lib/roadmap-parser.cjs +357 -50
  63. package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
  64. package/gsd-core/bin/lib/roadmap.cjs +2 -2
  65. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +317 -134
  66. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
  67. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
  68. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
  69. package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
  70. package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
  71. package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
  72. package/gsd-core/bin/lib/state-transition.cjs +103 -46
  73. package/gsd-core/bin/lib/state.cjs +448 -63
  74. package/gsd-core/bin/lib/surface.cjs +4 -7
  75. package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
  76. package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
  77. package/gsd-core/bin/lib/verify.cjs +49 -6
  78. package/gsd-core/bin/lib/workstream-inventory.cjs +4 -4
  79. package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
  80. package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
  81. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  82. package/gsd-core/references/autonomous-smart-discuss.md +99 -18
  83. package/gsd-core/references/checkpoints.md +1 -1
  84. package/gsd-core/references/planner-guidance.md +11 -0
  85. package/gsd-core/references/planner-human-verify-mode.md +1 -1
  86. package/gsd-core/references/planner-reversibility.md +2 -0
  87. package/gsd-core/references/planner-verify-command-grounding.md +22 -6
  88. package/gsd-core/workflows/autonomous.md +4 -4
  89. package/gsd-core/workflows/code-review-fix.md +5 -7
  90. package/gsd-core/workflows/execute-phase.md +21 -7
  91. package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
  92. package/gsd-core/workflows/plan-phase.md +18 -19
  93. package/hooks/dist/gsd-agent-isolation-guard.js +4 -3
  94. package/hooks/dist/gsd-config-reload.js +2 -2
  95. package/hooks/dist/gsd-context-monitor.js +2 -1
  96. package/hooks/dist/gsd-cursor-subagent-start.js +9 -6
  97. package/hooks/dist/gsd-read-injection-scanner.js +2 -2
  98. package/hooks/dist/gsd-secret-read-guard.js +141 -6
  99. package/hooks/dist/gsd-statusline.js +3 -2
  100. package/hooks/dist/gsd-workflow-guard.js +2 -1
  101. package/hooks/dist/lib/config-read.js +49 -0
  102. package/hooks/dist/lib/git-cmd.js +3 -1
  103. package/hooks/dist/lib/git-head-tree.js +474 -0
  104. package/hooks/dist/lib/isolation-sentinel.js +168 -13
  105. package/hooks/dist/lib/secret-read-exemptions.js +161 -0
  106. package/hooks/gsd-agent-isolation-guard.js +4 -3
  107. package/hooks/gsd-config-reload.js +2 -2
  108. package/hooks/gsd-context-monitor.js +2 -1
  109. package/hooks/gsd-cursor-subagent-start.js +9 -6
  110. package/hooks/gsd-read-injection-scanner.js +2 -2
  111. package/hooks/gsd-secret-read-guard.js +141 -6
  112. package/hooks/gsd-statusline.js +3 -2
  113. package/hooks/gsd-workflow-guard.js +2 -1
  114. package/hooks/lib/config-read.js +49 -0
  115. package/hooks/lib/git-cmd.js +3 -1
  116. package/hooks/lib/git-head-tree.js +474 -0
  117. package/hooks/lib/isolation-sentinel.js +168 -13
  118. package/hooks/lib/secret-read-exemptions.js +161 -0
  119. package/package.json +7 -2
  120. package/pi/package-agents/gsd-executor.md +1 -1
  121. package/pi/package-agents/gsd-plan-checker.md +1 -1
  122. package/pi/package-agents/gsd-planner.md +3 -3
  123. package/pi/package-agents/gsd-verifier.md +2 -2
  124. package/pi/package-agents/manifest.json +4 -4
  125. package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
  126. package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
  127. package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
  128. package/scripts/changeset/cli.cjs +2 -2
  129. package/scripts/changeset/lint.cjs +28 -3
  130. package/scripts/changeset/serialize.cjs +12 -2
  131. package/scripts/ci-compact-v8-coverage.cjs +204 -0
  132. package/scripts/ci-fix-next-rerun.cjs +149 -0
  133. package/scripts/ci-next-health.cjs +52 -9
  134. package/scripts/ci-test-scope.cjs +77 -71
  135. package/scripts/command-contract-helpers.cjs +56 -7
  136. package/scripts/docs-guard-registry.cjs +3 -0
  137. package/scripts/gen-cli-surface-manifest.cjs +287 -0
  138. package/scripts/gen-platform-conformance-tier.cjs +258 -19
  139. package/scripts/lib/macos-conformance-tier.generated.cjs +59 -47
  140. package/scripts/lib/platform-conformance-tier.generated.cjs +23 -64
  141. package/scripts/lib/shellcheck-fetch.cjs +71 -4
  142. package/scripts/lint-removed-but-needed.cjs +562 -50
  143. package/scripts/merge-commit-pr.cjs +186 -0
  144. package/scripts/release-ancestry.cjs +86 -0
  145. package/scripts/release-notes/changelog-release-notes.cjs +345 -0
  146. package/scripts/release-notes/discord-release-summary.cjs +82 -1
  147. package/scripts/release-pipeline-pr.cjs +92 -0
  148. package/scripts/require-issue-link-policy.cjs +18 -9
  149. package/scripts/sync-next-version.cjs +11 -2
  150. package/scripts/timeout-evidence-from-ci.cjs +964 -0
  151. package/skills/gsd-autonomous/SKILL.md +1 -1
  152. package/skills/gsd-new-milestone/SKILL.md +1 -1
  153. package/skills/gsd-new-project/SKILL.md +1 -1
  154. package/vscode/package.json +1 -1
@@ -11,6 +11,7 @@
11
11
  *
12
12
  * Shared helpers for command-family adapters to project argv tokens into
13
13
  * typed named values and multi-word segments.
14
+ * `requiredFlags` (ADR-4909 D1, #6087) turns a missing value-flag into InvalidArgs.
14
15
  */
15
16
  Object.defineProperty(exports, "__esModule", { value: true });
16
17
  exports.isFlagToken = isFlagToken;
@@ -45,121 +46,373 @@ function assertValidSpec(spec) {
45
46
  throw new TypeError('parseNamedArgs: spec.positionals must be a non-negative integer or the literal "rest" ' +
46
47
  `— received ${JSON.stringify(positionals)}.`);
47
48
  }
49
+ if (spec.freePositionals !== undefined && (spec.freePositionals !== true || positionals === 'rest')) {
50
+ throw new TypeError('parseNamedArgs: spec.freePositionals must be true and cannot be combined with positionals "rest" ' +
51
+ `— received freePositionals ${JSON.stringify(spec.freePositionals)} with positionals ${JSON.stringify(positionals)}.`);
52
+ }
53
+ if (spec.rejectShortFlags !== undefined && spec.rejectShortFlags !== true) {
54
+ throw new TypeError(`parseNamedArgs: spec.rejectShortFlags must be true — received ${JSON.stringify(spec.rejectShortFlags)}.`);
55
+ }
56
+ if (spec.unknownFlagMessage !== undefined &&
57
+ (typeof spec.unknownFlagMessage !== 'string' || !spec.unknownFlagMessage.includes('{flag}'))) {
58
+ throw new TypeError(`parseNamedArgs: spec.unknownFlagMessage must be a string containing the {flag} placeholder — received ${JSON.stringify(spec.unknownFlagMessage)}.`);
59
+ }
60
+ for (const kind of VALUE_SUBSET_KINDS)
61
+ assertSubsetOf(spec, kind, 'valueFlags');
62
+ assertSubsetOf(spec, 'emptyValueFlags', 'nonEmptyFlags');
63
+ assertSubsetOf(spec, 'emptyEqFlags', 'nonEmptyFlags');
64
+ assertSubsetOf(spec, 'emptyEqFlags', 'eqFlags');
65
+ assertSubsetOf(spec, 'trimEqFlags', 'eqFlags');
66
+ assertSubsetOf(spec, 'blankIsMissingFlags', 'nonEmptyFlags');
67
+ assertEqRejectPrefix(spec);
68
+ if (spec.missingValueMessage !== undefined &&
69
+ (typeof spec.missingValueMessage !== 'string' || !spec.missingValueMessage.includes('{flag}'))) {
70
+ throw new TypeError(`parseNamedArgs: spec.missingValueMessage must be a string containing the {flag} placeholder — received ${JSON.stringify(spec.missingValueMessage)}.`);
71
+ }
72
+ for (const kind of INDEPENDENT_KINDS)
73
+ assertIndependentKind(spec, kind);
74
+ if (spec.passthroughAfter !== undefined && spec.passthroughAfter !== '--') {
75
+ throw new TypeError(`parseNamedArgs: spec.passthroughAfter must be the literal "--" — received ${JSON.stringify(spec.passthroughAfter)}.`);
76
+ }
48
77
  }
49
- // Single predicate reused for both extraction (a value beginning with a
50
- // single `-` is a value, not a flag) and validation (negative space N5).
51
- // Exported for init-command-router's #3865 --phase alias normalization,
52
- // which needs the same flag-shape test before deciding whether args[2] is a
53
- // phase positional or a flag token.
54
- function isFlagToken(tok) {
55
- return tok.startsWith('--');
78
+ const VALUE_SUBSET_KINDS = [
79
+ 'requiredFlags',
80
+ 'eqFlags',
81
+ 'nonEmptyFlags',
82
+ 'bareEmptyFlags',
83
+ 'repeatableFlags',
84
+ ];
85
+ // Kinds that own their tokens: a flag may not also be a plain value, boolean or
86
+ // optional flag, nor belong to the other independent kind.
87
+ const INDEPENDENT_KINDS = ['listFlags', 'multiwordFlags'];
88
+ function assertStringArray(spec, kind) {
89
+ const list = spec[kind];
90
+ if (list === undefined)
91
+ return null;
92
+ if (!Array.isArray(list) || !list.every((f) => typeof f === 'string')) {
93
+ throw new TypeError(`parseNamedArgs: spec.${kind} must be an array of strings.`);
94
+ }
95
+ return list;
96
+ }
97
+ function assertSubsetOf(spec, kind, parent) {
98
+ const list = assertStringArray(spec, kind);
99
+ if (list === null)
100
+ return;
101
+ const declared = Array.isArray(spec[parent]) ? spec[parent] : [];
102
+ const stray = list.filter((f) => !declared.includes(f));
103
+ if (stray.length > 0) {
104
+ throw new TypeError(`parseNamedArgs: spec.${kind} must be a subset of spec.${parent} — ` +
105
+ `not declared in ${parent}: ${stray.join(', ')}`);
106
+ }
107
+ }
108
+ function assertEqRejectPrefix(spec) {
109
+ const map = spec.eqRejectPrefix;
110
+ if (map === undefined)
111
+ return;
112
+ if (!isPlainObject(map) || !Object.values(map).every((p) => typeof p === 'string' && p !== '')) {
113
+ throw new TypeError('parseNamedArgs: spec.eqRejectPrefix must map flag names to non-empty prefix strings.');
114
+ }
115
+ const flags = Object.keys(map);
116
+ for (const parent of ['eqFlags', 'nonEmptyFlags']) {
117
+ const declared = Array.isArray(spec[parent]) ? spec[parent] : [];
118
+ const stray = flags.filter((f) => !declared.includes(f));
119
+ if (stray.length > 0) {
120
+ throw new TypeError(`parseNamedArgs: spec.eqRejectPrefix must be a subset of spec.${parent} — ` +
121
+ `not declared in ${parent}: ${stray.join(', ')}`);
122
+ }
123
+ }
124
+ }
125
+ function assertIndependentKind(spec, kind) {
126
+ const list = assertStringArray(spec, kind);
127
+ if (list === null)
128
+ return;
129
+ const others = ['valueFlags', 'booleanFlags', 'optionalValueFlags', ...INDEPENDENT_KINDS.filter((k) => k !== kind)];
130
+ for (const other of others) {
131
+ const clash = list.filter((f) => Array.isArray(spec[other]) && spec[other].includes(f));
132
+ if (clash.length > 0) {
133
+ throw new TypeError(`parseNamedArgs: spec.${kind} must be disjoint from spec.${other} — ` +
134
+ `declared in both: ${clash.join(', ')}`);
135
+ }
136
+ }
56
137
  }
57
- // ─── parseNamedArgs ───────────────────────────────────────────────────────────
58
138
  /**
59
- * Project argv tokens into typed named values, then strictly validate every
60
- * token past the caller's declared positional boundary.
61
- *
62
- * Extraction (unchanged semantics, #312): first occurrence wins; a value
63
- * flag whose next token is absent or starts with `--` yields `null` (this is
64
- * NOT a validation error — see the value-flag branch below); boolean flags
65
- * and optional-value flags (`optionalValueFlags`, #2932's `--wave` shape) are
66
- * presence tests. Kept as a single first-index Map so the flag loops don't
67
- * each re-scan argv — O(argv + flags) instead of O(flags * argv).
68
- *
69
- * Validation (skipped entirely when `positionals === 'rest'`): a single
70
- * left-to-right cursor walk from `spec.positionals`, per the design doc's
71
- * Kernighan's Law note — debuggable over clever, never a set-difference.
139
+ * First required flag (declaration order) whose extracted value is `null`
140
+ * (absent or flag-shaped) or `''`, as an InvalidArgs result; else null.
72
141
  */
73
- function parseNamedArgs(args, spec) {
74
- assertValidSpec(spec);
75
- const valueFlags = spec.valueFlags ?? [];
76
- const booleanFlags = spec.booleanFlags ?? [];
77
- const optionalValueFlags = spec.optionalValueFlags ?? [];
78
- const firstIndex = new Map();
79
- for (let i = 0; i < args.length; i++) {
80
- if (!firstIndex.has(args[i]))
81
- firstIndex.set(args[i], i);
142
+ function missingRequired(spec, data) {
143
+ for (const flag of spec.requiredFlags ?? []) {
144
+ const value = data[flag];
145
+ if (value === null || value === '' || (Array.isArray(value) && value.length === 0)) {
146
+ return {
147
+ ok: false,
148
+ kind: 'InvalidArgs',
149
+ arg: `--${flag}`,
150
+ reason: `missing required flag --${flag} <value>`,
151
+ exitReason: ERROR_REASON.USAGE,
152
+ };
153
+ }
82
154
  }
83
- const data = {};
84
- for (const flag of valueFlags) {
85
- const idx = firstIndex.has(`--${flag}`) ? firstIndex.get(`--${flag}`) : -1;
86
- data[flag] =
87
- idx !== -1 && args[idx + 1] !== undefined && !isFlagToken(args[idx + 1])
88
- ? args[idx + 1]
89
- : null;
90
- }
91
- for (const flag of booleanFlags) {
92
- data[flag] = firstIndex.has(`--${flag}`);
93
- }
94
- // Optional-value flags (#2932: `--wave`-shaped) — presence-only, exactly
95
- // like booleanFlags. The value (if any) is deliberately not surfaced here;
96
- // see the NamedArgSpec.optionalValueFlags JSDoc.
97
- for (const flag of optionalValueFlags) {
98
- data[flag] = firstIndex.has(`--${flag}`);
99
- }
100
- if (spec.positionals === 'rest') {
101
- return { ok: true, data };
102
- }
103
- const valueFlagSet = new Set(valueFlags);
104
- const booleanFlagSet = new Set(booleanFlags);
105
- const optionalValueFlagSet = new Set(optionalValueFlags);
106
- const flagList = [
107
- ...valueFlags.map((f) => `--${f} <value>`),
108
- ...booleanFlags.map((f) => `--${f}`),
109
- ...optionalValueFlags.map((f) => `--${f} [value]`),
110
- ];
111
- let i = spec.positionals;
112
- while (i < args.length) {
113
- const tok = args[i];
114
- if (isFlagToken(tok)) {
115
- const name = tok.slice(2);
116
- if (valueFlagSet.has(name)) {
117
- // A value flag whose next token is absent or flag-shaped resolves to
118
- // `null` in `data` (see the extraction loop above) — this is NOT an
119
- // error (#3180/behavior-lock: emptyPrdValueIsFalsyAndTreatedAsAbsent,
120
- // section-manifest-init-facts.test.cjs "flag-shaped value"). Advance
121
- // by 1 so the following flag token is validated on its own merits on
122
- // the next iteration.
123
- const next = args[i + 1];
124
- i += next !== undefined && !isFlagToken(next) ? 2 : 1;
125
- continue;
126
- }
127
- if (booleanFlagSet.has(name)) {
128
- i += 1;
129
- continue;
130
- }
131
- if (optionalValueFlagSet.has(name)) {
132
- const next = args[i + 1];
133
- i += next !== undefined && !isFlagToken(next) ? 2 : 1;
155
+ return null;
156
+ }
157
+ /**
158
+ * Whether the occurrence of a `nonEmptyFlags` member carries no value, per the
159
+ * `emptyValueFlags` / `emptyEqFlags` relaxations: no next token or a flag-shaped
160
+ * one always counts as missing; an empty next token and an empty or
161
+ * whitespace-only `--f=` value count unless relaxed.
162
+ */
163
+ function lacksValue(args, spec, flag, occ) {
164
+ const blankMissing = (spec.blankIsMissingFlags ?? []).includes(flag);
165
+ if (occ.inline !== null) {
166
+ const prefix = spec.eqRejectPrefix?.[flag];
167
+ if (prefix !== undefined && occ.inline.startsWith(prefix))
168
+ return true;
169
+ return occ.inline.trim() === '' && (blankMissing || !(spec.emptyEqFlags ?? []).includes(flag));
170
+ }
171
+ const next = args[occ.index + 1];
172
+ if (next === undefined || isFlagToken(next))
173
+ return true;
174
+ if (blankMissing && next.trim() === '')
175
+ return true;
176
+ return next === '' && !(spec.emptyValueFlags ?? []).includes(flag);
177
+ }
178
+ /**
179
+ * First `nonEmptyFlags` member (declaration order) that is present (`--f` or
180
+ * `--f=` occurs) without a value, as an InvalidArgs result with today's wording
181
+ * (`Missing value for --f`); else null. The first occurrence is checked, except
182
+ * for a repeatable flag, whose last occurrence is.
183
+ */
184
+ function missingValue(args, spec, occurrences) {
185
+ for (const flag of spec.nonEmptyFlags ?? []) {
186
+ const occs = occurrences.get(flag);
187
+ if (occs === undefined)
188
+ continue;
189
+ const occ = (spec.repeatableFlags ?? []).includes(flag) ? occs[occs.length - 1] : occs[0];
190
+ if (lacksValue(args, spec, flag, occ)) {
191
+ return {
192
+ ok: false,
193
+ kind: 'InvalidArgs',
194
+ arg: `--${flag}`,
195
+ reason: spec.missingValueMessage !== undefined
196
+ ? spec.missingValueMessage.split('{flag}').join(`--${flag}`)
197
+ : `Missing value for --${flag}`,
198
+ exitReason: ERROR_REASON.USAGE,
199
+ };
200
+ }
201
+ }
202
+ return null;
203
+ }
204
+ /** Single pass over argv: flag name -> its occurrences in argv order. */
205
+ function indexOccurrences(args, eq) {
206
+ const map = new Map();
207
+ args.forEach((tok, index) => {
208
+ if (!isFlagToken(tok))
209
+ return;
210
+ const eqAt = tok.indexOf('=');
211
+ const eqName = eqAt > 2 ? tok.slice(2, eqAt) : null;
212
+ const isEq = eqName !== null && eq.has(eqName);
213
+ const name = isEq ? eqName : tok.slice(2);
214
+ const list = map.get(name) ?? [];
215
+ list.push({ index, inline: isEq ? tok.slice(eqAt + 1) : null });
216
+ map.set(name, list);
217
+ });
218
+ return map;
219
+ }
220
+ /**
221
+ * The value an occurrence carries: `null` when absent or flag-shaped, or when
222
+ * `--f=` is empty (unless `emptyEqFlags` makes it a value).
223
+ */
224
+ function occurrenceValue(args, spec, flag, occ) {
225
+ if (occ.inline !== null) {
226
+ const inline = (spec.trimEqFlags ?? []).includes(flag) ? occ.inline.trim() : occ.inline;
227
+ return inline === '' && !(spec.emptyEqFlags ?? []).includes(flag) ? null : inline;
228
+ }
229
+ const next = args[occ.index + 1];
230
+ return next !== undefined && !isFlagToken(next) ? next : null;
231
+ }
232
+ function extractValueFlag(args, spec, flag, occs) {
233
+ if ((spec.repeatableFlags ?? []).includes(flag)) {
234
+ // An empty or flag-shaped value contributes nothing to a repeatable flag.
235
+ return occs
236
+ .map((o) => occurrenceValue(args, spec, flag, o))
237
+ .filter((v) => v !== null && v !== '');
238
+ }
239
+ if (occs.length === 0)
240
+ return null;
241
+ const value = occurrenceValue(args, spec, flag, occs[0]);
242
+ const bareEmpty = (spec.bareEmptyFlags ?? []).includes(flag) && occs[0].inline === null;
243
+ return value === null && bareEmpty ? '' : value;
244
+ }
245
+ /**
246
+ * Every occurrence of `--flag` starts a run of non-`--` tokens that ends at
247
+ * the next list flag of the spec (gsd-tools.cjs `collectListFlagValues`).
248
+ */
249
+ function collectListValues(args, flag, stops, sets) {
250
+ const values = [];
251
+ args.forEach((a, i) => {
252
+ if (a !== `--${flag}`)
253
+ return;
254
+ let j = i + 1;
255
+ while (j < args.length) {
256
+ const b = args[j];
257
+ if (stops.has(b))
258
+ break;
259
+ if (isFlagToken(b)) {
260
+ // Step over exactly what the walk steps over, so a word the walk reads
261
+ // as a flag's value is never also collected; a bare `--` advances by 1.
262
+ j += advanceOverFlag(args, j, sets) ?? 1;
134
263
  continue;
135
264
  }
136
- const reason = flagList.length > 0
137
- ? `unknown flag ${formatDiagnosticToken(tok)}; accepted: ${flagList.join(', ')}`
138
- : `unknown flag ${formatDiagnosticToken(tok)}; this command accepts no flags`;
139
- return { ok: false, kind: 'InvalidArgs', arg: tok, reason, exitReason: ERROR_REASON.USAGE };
265
+ values.push(b);
266
+ j += 1;
140
267
  }
141
- return {
142
- ok: false,
143
- kind: 'InvalidArgs',
144
- arg: tok,
145
- reason: `unexpected positional argument ${formatDiagnosticToken(tok)}`,
146
- exitReason: ERROR_REASON.USAGE,
147
- };
268
+ });
269
+ return values;
270
+ }
271
+ function extractData(args, spec, occurrences) {
272
+ const data = {};
273
+ for (const flag of spec.valueFlags ?? []) {
274
+ data[flag] = extractValueFlag(args, spec, flag, occurrences.get(flag) ?? []);
148
275
  }
149
- return { ok: true, data };
276
+ // Boolean and optional-value flags (#2932: `--wave`-shaped) are presence
277
+ // tests; the optional value is deliberately not surfaced here.
278
+ for (const flag of [...(spec.booleanFlags ?? []), ...(spec.optionalValueFlags ?? [])]) {
279
+ data[flag] = occurrences.has(flag);
280
+ }
281
+ const listFlags = spec.listFlags ?? [];
282
+ const stops = new Set(listFlags.map((f) => `--${f}`));
283
+ const sets = buildFlagSets(spec);
284
+ for (const flag of listFlags)
285
+ data[flag] = collectListValues(args, flag, stops, sets);
286
+ for (const flag of spec.multiwordFlags ?? [])
287
+ data[flag] = parseMultiwordArg(args, flag);
288
+ return data;
289
+ }
290
+ function buildFlagSets(spec) {
291
+ const value = spec.valueFlags ?? [];
292
+ const bool = spec.booleanFlags ?? [];
293
+ const optional = spec.optionalValueFlags ?? [];
294
+ const list = spec.listFlags ?? [];
295
+ const multiword = spec.multiwordFlags ?? [];
296
+ return {
297
+ value: new Set(value),
298
+ boolean: new Set(bool),
299
+ optional: new Set(optional),
300
+ eq: new Set(spec.eqFlags ?? []),
301
+ list: new Set(list),
302
+ multiword: new Set(multiword),
303
+ accepted: [
304
+ ...value.map((f) => `--${f} <value>`),
305
+ ...bool.map((f) => `--${f}`),
306
+ ...optional.map((f) => `--${f} [value]`),
307
+ ...list.map((f) => `--${f} <values...>`),
308
+ ...multiword.map((f) => `--${f} <words...>`),
309
+ ],
310
+ };
311
+ }
312
+ // A short flag is a dash followed by a letter; `-1` (negative number) and `-` are not flags.
313
+ const SHORT_FLAG_RE = /^-[A-Za-z]/;
314
+ /** Tokens consumed by the flag at `args[i]`, or null when the flag is not declared. */
315
+ function advanceOverFlag(args, i, sets) {
316
+ const tok = args[i];
317
+ const eqAt = tok.indexOf('=');
318
+ if (eqAt > 2 && sets.eq.has(tok.slice(2, eqAt)))
319
+ return 1;
320
+ const name = tok.slice(2);
321
+ const next = args[i + 1];
322
+ const hasValue = next !== undefined && !isFlagToken(next);
323
+ // A value flag whose next token is absent or flag-shaped resolves to `null`
324
+ // in `data` — NOT an error (#3180/behavior-lock). Advance by 1 so the
325
+ // following flag token is validated on its own merits.
326
+ if (sets.value.has(name) || sets.optional.has(name))
327
+ return hasValue ? 2 : 1;
328
+ if (sets.boolean.has(name) || sets.list.has(name))
329
+ return 1;
330
+ if (sets.multiword.has(name)) {
331
+ let n = 1;
332
+ while (args[i + n] !== undefined && !isFlagToken(args[i + n]))
333
+ n++;
334
+ return n;
335
+ }
336
+ return null;
337
+ }
338
+ function unknownFlag(tok, sets, spec) {
339
+ const template = spec.unknownFlagMessage;
340
+ const reason = template !== undefined
341
+ ? template.split('{flag}').join(tok)
342
+ : sets.accepted.length > 0
343
+ ? `unknown flag ${formatDiagnosticToken(tok)}; accepted: ${sets.accepted.join(', ')}`
344
+ : `unknown flag ${formatDiagnosticToken(tok)}; this command accepts no flags`;
345
+ return { ok: false, kind: 'InvalidArgs', arg: tok, reason, exitReason: ERROR_REASON.USAGE };
150
346
  }
151
347
  /**
152
- * Thin projection over `parseNamedArgs`: on `ok:false` it calls
153
- * `fail(result.reason, result.exitReason)` and then throws.
154
- *
155
- * The trailing throw exists for two reasons: (1) TypeScript's control-flow
156
- * analysis needs a `never`-returning path so callers can destructure the
157
- * return value without a null check; (2) it is a fail-closed backstop — the
158
- * `fail` callbacks in this repo are `never`-returning at runtime (`io.error`
159
- * calls `process.exit(1)`) but are typed `void`, so if a caller ever passes a
160
- * `fail` that actually returns, this still halts instead of falling through
161
- * with a half-built `ParsedNamedArgs`.
348
+ * Left-to-right cursor walk from `spec.positionals`. Once a list flag has
349
+ * been seen, non-flag tokens belong to its run (collectListFlagValues
350
+ * semantics: a run is only ended by another list flag, which restarts it) and
351
+ * are not stray positionals; `--` tokens inside a run are still validated.
162
352
  */
353
+ function walkTokens(args, spec) {
354
+ const sets = buildFlagSets(spec);
355
+ let inListRun = false;
356
+ let i = spec.positionals === 'rest' ? args.length : spec.positionals;
357
+ while (i < args.length) {
358
+ const tok = args[i];
359
+ if (spec.rejectShortFlags === true && SHORT_FLAG_RE.test(tok))
360
+ return unknownFlag(tok, sets, spec);
361
+ if (inListRun && tok === '--') {
362
+ // collectListFlagValues skipped every `--`-prefixed token inside a run, so a bare `--` is accepted there.
363
+ i += 1;
364
+ continue;
365
+ }
366
+ if (isFlagToken(tok)) {
367
+ const advance = advanceOverFlag(args, i, sets);
368
+ if (advance === null)
369
+ return unknownFlag(tok, sets, spec);
370
+ if (sets.list.has(tok.slice(2)))
371
+ inListRun = true;
372
+ i += advance;
373
+ continue;
374
+ }
375
+ if (!inListRun && spec.freePositionals !== true) {
376
+ return {
377
+ ok: false,
378
+ kind: 'InvalidArgs',
379
+ arg: tok,
380
+ reason: `unexpected positional argument ${formatDiagnosticToken(tok)}`,
381
+ exitReason: ERROR_REASON.USAGE,
382
+ };
383
+ }
384
+ i += 1;
385
+ }
386
+ return null;
387
+ }
388
+ /** Split argv at the first literal `--` (passthroughAfter) at/after the positional boundary. */
389
+ function splitPassthrough(args, spec) {
390
+ if (spec.passthroughAfter === undefined)
391
+ return { head: args, tail: [] };
392
+ const from = typeof spec.positionals === 'number' ? spec.positionals : 0;
393
+ const at = args.indexOf(spec.passthroughAfter, from);
394
+ return at === -1 ? { head: args, tail: [] } : { head: args.slice(0, at), tail: args.slice(at + 1) };
395
+ }
396
+ // Single predicate reused for both extraction (a value beginning with a
397
+ // single `-` is a value, not a flag) and validation (negative space N5).
398
+ // Exported for init-command-router's #3865 --phase alias normalization,
399
+ // which needs the same flag-shape test before deciding whether args[2] is a
400
+ // phase positional or a flag token.
401
+ function isFlagToken(tok) {
402
+ return tok.startsWith('--');
403
+ }
404
+ function parseNamedArgs(args, spec) {
405
+ assertValidSpec(spec);
406
+ const { head, tail } = splitPassthrough(args, spec);
407
+ const occurrences = indexOccurrences(head, new Set(spec.eqFlags ?? []));
408
+ const data = extractData(head, spec, occurrences);
409
+ if (spec.passthroughAfter !== undefined)
410
+ data['--'] = tail;
411
+ const invalid = (spec.positionals === 'rest' ? null : walkTokens(head, spec)) ??
412
+ missingValue(head, spec, occurrences) ??
413
+ missingRequired(spec, data);
414
+ return invalid ?? { ok: true, data };
415
+ }
163
416
  function parseNamedArgsOrExit(args, spec, fail) {
164
417
  const result = parseNamedArgs(args, spec);
165
418
  if (!result.ok) {