@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
@@ -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) {
@@ -14,6 +14,7 @@ const node_path_1 = __importDefault(require("node:path"));
14
14
  const text_lines_cjs_1 = require("./text-lines.cjs");
15
15
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
16
16
  const pattern_cjs_1 = require("./pattern.cjs");
17
+ const phase_entry_grammar_cjs_1 = require("./phase-entry-grammar.cjs");
17
18
  const install_manifest_record_cjs_1 = require("./install-manifest-record.cjs");
18
19
  const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
19
20
  const security_cjs_1 = require("./security.cjs");
@@ -1782,7 +1783,74 @@ function stageDeclaredRemovals(cwd, removedDeclared) {
1782
1783
  }
1783
1784
  return { removedEntries, removedPathspec, failures };
1784
1785
  }
1785
- function cmdCommit(cwd, message, files, raw, amend, noVerify, filesRemoved) {
1786
+ /**
1787
+ * #6043 (ADR-4908 §3): a committing verb commits in the checkout the process
1788
+ * runs in. Returns the typed refusal when it cannot: not a git repository, or
1789
+ * `cwd` / a path to commit resolves outside `ctx.worktreeRoot`.
1790
+ * `cwd` and the worktree root are compared by realpath; a relative path is
1791
+ * resolved lexically against the canonical `cwd`, so a `.planning` symlink
1792
+ * inside the worktree stays inside; an absolute path is canonicalized through
1793
+ * its nearest existing ancestor directory.
1794
+ * A degraded context (git timed out or could not run) makes no claim about the
1795
+ * worktree, so it never refuses.
1796
+ */
1797
+ function commitRefusal(ctx, cwd, files, filesRemoved) {
1798
+ if (ctx.provenance.reason === 'not_git_repo') {
1799
+ return { reason: 'not_git_repo', message: `commit refused: ${cwd} is not inside a git repository` };
1800
+ }
1801
+ if (ctx.provenance.degraded)
1802
+ return null;
1803
+ const realOrSelf = (p) => {
1804
+ try {
1805
+ return node_fs_1.default.realpathSync.native(p);
1806
+ }
1807
+ catch {
1808
+ return p;
1809
+ }
1810
+ };
1811
+ // Nearest existing ancestor directory of an absolute path, realpath'd, with
1812
+ // the not-yet-existing remainder re-appended (macOS /var <-> /private/var,
1813
+ // Windows 8.3 short names like RUNNER~1).
1814
+ const canonicalAbsolute = (abs) => {
1815
+ const rest = [node_path_1.default.basename(abs)];
1816
+ let ancestor = node_path_1.default.dirname(abs);
1817
+ for (;;) {
1818
+ try {
1819
+ return node_path_1.default.join(node_fs_1.default.realpathSync.native(ancestor), ...rest);
1820
+ }
1821
+ catch {
1822
+ const parent = node_path_1.default.dirname(ancestor);
1823
+ if (parent === ancestor)
1824
+ return abs;
1825
+ rest.unshift(node_path_1.default.basename(ancestor));
1826
+ ancestor = parent;
1827
+ }
1828
+ }
1829
+ };
1830
+ const norm = (p) => {
1831
+ const slashed = p.replace(/\\/g, '/');
1832
+ return process.platform === 'win32' ? slashed.toLowerCase() : slashed;
1833
+ };
1834
+ const inside = (target, root) => (0, security_cjs_1.isContainedIn)(norm(target), norm(root), { sep: '/' });
1835
+ const worktreeRoot = realOrSelf(node_path_1.default.resolve(ctx.worktreeRoot));
1836
+ const realCwd = realOrSelf(node_path_1.default.resolve(cwd));
1837
+ const declared = [...(files ?? []), ...(filesRemoved ?? [])];
1838
+ const targets = declared.length > 0 ? declared : ['.planning/'];
1839
+ // Lexical against the canonical cwd: a `.planning` symlink component is not followed.
1840
+ const resolveTarget = (t) => node_path_1.default.isAbsolute(t) ? canonicalAbsolute(node_path_1.default.resolve(t)) : node_path_1.default.resolve(realCwd, t);
1841
+ const escaping = !inside(realCwd, worktreeRoot)
1842
+ ? realCwd
1843
+ : targets.map(resolveTarget).find((abs) => !inside(abs, worktreeRoot));
1844
+ if (escaping === undefined)
1845
+ return null;
1846
+ return {
1847
+ reason: 'outside_worktree',
1848
+ skipped: false,
1849
+ message: `commit refused: ${escaping} is outside the worktree ${worktreeRoot}; ` +
1850
+ 'pass the checkout that owns the path with --project-dir to commit there',
1851
+ };
1852
+ }
1853
+ function cmdCommit(cwd, message, files, raw, amend, noVerify, filesRemoved, ctx) {
1786
1854
  if (!message && !amend) {
1787
1855
  error('commit message required');
1788
1856
  }
@@ -1812,6 +1880,16 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify, filesRemoved) {
1812
1880
  output(result, raw, 'skipped');
1813
1881
  return;
1814
1882
  }
1883
+ // #6043: refuse before branching or staging, and only AFTER the commit_docs
1884
+ // skip above, so a benign skip is never turned into an error.
1885
+ const refusal = ctx ? commitRefusal(ctx, cwd, files, filesRemoved) : null;
1886
+ if (refusal) {
1887
+ error(refusal.message, refusal.reason === 'not_git_repo' ? ERROR_REASON.COMMIT_NOT_GIT_REPO : ERROR_REASON.COMMIT_OUTSIDE_WORKTREE, {
1888
+ committed: false,
1889
+ ...(refusal.skipped === false ? { skipped: false } : {}),
1890
+ hash: null,
1891
+ });
1892
+ }
1815
1893
  // Ensure branching strategy branch exists before first commit (#1278).
1816
1894
  // Pre-execution workflows (discuss, plan, research) commit artifacts but the branch
1817
1895
  // was previously only created during execute-phase — too late.
@@ -3537,7 +3615,7 @@ function cmdStats(cwd, format, raw) {
3537
3615
  phasesByNumber.set(key, {
3538
3616
  number: key,
3539
3617
  ...(displayId ? { display_id: displayId } : {}),
3540
- name: phaseName.replace(/\(INSERTED\)/i, '').trim(),
3618
+ name: (0, phase_entry_grammar_cjs_1.stripInsertedMarker)(phaseName),
3541
3619
  plans: 0,
3542
3620
  summaries: 0,
3543
3621
  phaseStatus: phase_status_cjs_1.PHASE_STATUS.NOT_STARTED,