@opengsd/gsd-core 1.7.0-rc.2 → 1.7.0-rc.4

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 (184) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/GEMINI.md +25 -23
  4. package/README.ja-JP.md +4 -3
  5. package/README.ko-KR.md +4 -3
  6. package/README.md +7 -6
  7. package/README.pt-BR.md +4 -3
  8. package/README.zh-CN.md +4 -3
  9. package/agents/gsd-advisor-researcher.md +1 -1
  10. package/agents/gsd-ai-researcher.md +1 -1
  11. package/agents/gsd-code-fixer.md +1 -1
  12. package/agents/gsd-debug-session-manager.md +1 -1
  13. package/agents/gsd-debugger.md +1 -1
  14. package/agents/gsd-domain-researcher.md +1 -1
  15. package/agents/gsd-eval-auditor.md +1 -1
  16. package/agents/gsd-executor.md +4 -4
  17. package/agents/gsd-intel-updater.md +1 -1
  18. package/agents/gsd-mempalace-curator.md +1 -1
  19. package/agents/gsd-phase-researcher.md +2 -2
  20. package/agents/gsd-plan-checker.md +1 -1
  21. package/agents/gsd-planner.md +2 -2
  22. package/agents/gsd-project-researcher.md +2 -2
  23. package/agents/gsd-research-synthesizer.md +1 -1
  24. package/agents/gsd-ui-researcher.md +2 -2
  25. package/agents/gsd-verifier.md +2 -2
  26. package/bin/install.js +132 -464
  27. package/commands/gsd/map-codebase.md +4 -4
  28. package/commands/gsd/mempalace-capture.md +1 -1
  29. package/commands/gsd/mempalace-recall.md +9 -5
  30. package/commands/gsd/next.md +29 -0
  31. package/commands/gsd/ns-project.md +2 -1
  32. package/commands/gsd/ns-workflow.md +5 -2
  33. package/commands/gsd/onboard.md +46 -0
  34. package/gsd-core/bin/ensure-runtime-build.cjs +246 -0
  35. package/gsd-core/bin/gsd-tools.cjs +72 -3
  36. package/gsd-core/bin/lib/capability-registry.cjs +684 -264
  37. package/gsd-core/bin/lib/capability-state.cjs +117 -18
  38. package/gsd-core/bin/lib/capability-writer.cjs +11 -1
  39. package/gsd-core/bin/lib/check-command-router.cjs +111 -1
  40. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +136 -0
  41. package/gsd-core/bin/lib/claude-orchestration.cjs +404 -0
  42. package/gsd-core/bin/lib/clusters.cjs +2 -0
  43. package/gsd-core/bin/lib/command-aliases.cjs +8 -0
  44. package/gsd-core/bin/lib/config.cjs +19 -1
  45. package/gsd-core/bin/lib/core-utils.cjs +41 -0
  46. package/gsd-core/bin/lib/external-job.cjs +287 -0
  47. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +151 -0
  48. package/gsd-core/bin/lib/graphify.cjs +41 -9
  49. package/gsd-core/bin/lib/init-command-router.cjs +4 -0
  50. package/gsd-core/bin/lib/init.cjs +25 -69
  51. package/gsd-core/bin/lib/install-engine.cjs +8 -5
  52. package/gsd-core/bin/lib/install-profiles.cjs +2 -0
  53. package/gsd-core/bin/lib/loop-resolver.cjs +7 -1
  54. package/gsd-core/bin/lib/milestone.cjs +7 -1
  55. package/gsd-core/bin/lib/model-resolver.cjs +69 -4
  56. package/gsd-core/bin/lib/onboard-projection.cjs +309 -0
  57. package/gsd-core/bin/lib/phase.cjs +14 -2
  58. package/gsd-core/bin/lib/plan-scan.cjs +9 -1
  59. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  60. package/gsd-core/bin/lib/prohibition-enforcement.cjs +21 -19
  61. package/gsd-core/bin/lib/roadmap.cjs +17 -2
  62. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +18 -13
  63. package/gsd-core/bin/lib/runtime-name-policy.cjs +11 -11
  64. package/gsd-core/bin/lib/shell-command-projection.cjs +12 -8
  65. package/gsd-core/bin/lib/smart-entry.cjs +521 -0
  66. package/gsd-core/bin/lib/surface.cjs +63 -33
  67. package/gsd-core/bin/lib/update-context.cjs +0 -5
  68. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  69. package/gsd-core/bin/shared/model-catalog.json +5 -5
  70. package/gsd-core/bin/shared/runtime-aliases.manifest.json +0 -5
  71. package/gsd-core/references/gates.md +1 -1
  72. package/gsd-core/references/gsd-run-resolver.md +8 -0
  73. package/gsd-core/references/planning-config.md +1 -1
  74. package/gsd-core/templates/project.md +1 -1
  75. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  76. package/gsd-core/workflows/add-backlog.md +1 -1
  77. package/gsd-core/workflows/add-phase.md +1 -1
  78. package/gsd-core/workflows/add-tests.md +1 -1
  79. package/gsd-core/workflows/add-todo.md +1 -1
  80. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  81. package/gsd-core/workflows/audit-fix.md +1 -1
  82. package/gsd-core/workflows/audit-milestone.md +1 -1
  83. package/gsd-core/workflows/audit-uat.md +1 -1
  84. package/gsd-core/workflows/autonomous.md +1 -1
  85. package/gsd-core/workflows/check-todos.md +1 -1
  86. package/gsd-core/workflows/cleanup.md +1 -1
  87. package/gsd-core/workflows/code-review-fix.md +1 -1
  88. package/gsd-core/workflows/code-review.md +1 -1
  89. package/gsd-core/workflows/complete-milestone.md +1 -1
  90. package/gsd-core/workflows/debug.md +1 -1
  91. package/gsd-core/workflows/diagnose-issues.md +1 -1
  92. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  93. package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
  94. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  95. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  96. package/gsd-core/workflows/discuss-phase.md +2 -2
  97. package/gsd-core/workflows/do.md +5 -4
  98. package/gsd-core/workflows/docs-update.md +1 -1
  99. package/gsd-core/workflows/edit-phase.md +1 -1
  100. package/gsd-core/workflows/eval-review.md +1 -1
  101. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  102. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  103. package/gsd-core/workflows/execute-phase.md +4 -5
  104. package/gsd-core/workflows/execute-plan.md +1 -1
  105. package/gsd-core/workflows/explore.md +1 -1
  106. package/gsd-core/workflows/extract-learnings.md +1 -1
  107. package/gsd-core/workflows/forensics.md +1 -1
  108. package/gsd-core/workflows/graduation.md +1 -1
  109. package/gsd-core/workflows/health.md +1 -1
  110. package/gsd-core/workflows/help/modes/brief.md +2 -1
  111. package/gsd-core/workflows/help/modes/default.md +2 -1
  112. package/gsd-core/workflows/help/modes/full.md +25 -1
  113. package/gsd-core/workflows/help/modes/topic.md +2 -1
  114. package/gsd-core/workflows/import.md +1 -1
  115. package/gsd-core/workflows/ingest-docs.md +1 -1
  116. package/gsd-core/workflows/insert-phase.md +1 -1
  117. package/gsd-core/workflows/list-seeds.md +1 -1
  118. package/gsd-core/workflows/list-workspaces.md +1 -1
  119. package/gsd-core/workflows/manager.md +1 -1
  120. package/gsd-core/workflows/map-codebase.md +1 -1
  121. package/gsd-core/workflows/milestone-summary.md +1 -1
  122. package/gsd-core/workflows/mvp-phase.md +1 -1
  123. package/gsd-core/workflows/new-milestone.md +1 -1
  124. package/gsd-core/workflows/new-project.md +1 -1
  125. package/gsd-core/workflows/new-workspace.md +1 -1
  126. package/gsd-core/workflows/next.md +1 -1
  127. package/gsd-core/workflows/onboard.md +277 -0
  128. package/gsd-core/workflows/pause-work.md +1 -1
  129. package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
  130. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  131. package/gsd-core/workflows/plan-phase.md +9 -2
  132. package/gsd-core/workflows/plan-review-convergence.md +1 -1
  133. package/gsd-core/workflows/plant-seed.md +1 -1
  134. package/gsd-core/workflows/pr-branch.md +1 -1
  135. package/gsd-core/workflows/profile-user.md +1 -1
  136. package/gsd-core/workflows/progress.md +1 -1
  137. package/gsd-core/workflows/quick.md +21 -1
  138. package/gsd-core/workflows/reapply-patches.md +1 -1
  139. package/gsd-core/workflows/remove-phase.md +1 -1
  140. package/gsd-core/workflows/remove-workspace.md +1 -1
  141. package/gsd-core/workflows/resume-project.md +1 -1
  142. package/gsd-core/workflows/review.md +43 -7
  143. package/gsd-core/workflows/scan.md +1 -1
  144. package/gsd-core/workflows/secure-phase.md +1 -1
  145. package/gsd-core/workflows/settings-advanced.md +3 -1
  146. package/gsd-core/workflows/settings-integrations.md +1 -1
  147. package/gsd-core/workflows/settings.md +1 -1
  148. package/gsd-core/workflows/ship.md +1 -1
  149. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  150. package/gsd-core/workflows/sketch.md +1 -1
  151. package/gsd-core/workflows/smart-entry.md +120 -0
  152. package/gsd-core/workflows/spec-phase.md +8 -6
  153. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  154. package/gsd-core/workflows/spike.md +1 -1
  155. package/gsd-core/workflows/stats.md +1 -1
  156. package/gsd-core/workflows/thread.md +1 -1
  157. package/gsd-core/workflows/transition.md +1 -1
  158. package/gsd-core/workflows/ui-phase.md +1 -1
  159. package/gsd-core/workflows/ui-review.md +1 -1
  160. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  161. package/gsd-core/workflows/update.md +3 -6
  162. package/gsd-core/workflows/validate-phase.md +1 -1
  163. package/gsd-core/workflows/verify-phase.md +1 -1
  164. package/gsd-core/workflows/verify-work.md +62 -4
  165. package/package.json +3 -5
  166. package/scripts/ci-test-scope.cjs +19 -1
  167. package/scripts/gen-golden-install-parity-zcode.cjs +77 -0
  168. package/scripts/generate-package-identity.cjs +27 -2
  169. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -148
  170. package/scripts/lint-regression-test-names.allowlist.json +1 -273
  171. package/scripts/lint-test-file-count.allowlist.json +7 -91
  172. package/scripts/research-profiles.cjs +6 -6
  173. package/scripts/run-tests.cjs +18 -3
  174. package/scripts/slurm-adapter.cjs +269 -0
  175. package/scripts/sync-manifest-versions.cjs +3 -2
  176. package/scripts/sync-runtime-launcher.cjs +20 -2
  177. package/skills/gsd-map-codebase/SKILL.md +3 -3
  178. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  179. package/skills/gsd-mempalace-recall/SKILL.md +9 -5
  180. package/skills/gsd-next/SKILL.md +30 -0
  181. package/skills/gsd-ns-project/SKILL.md +1 -0
  182. package/skills/gsd-ns-workflow/SKILL.md +4 -1
  183. package/skills/gsd-onboard/SKILL.md +46 -0
  184. package/gemini-extension.json +0 -6
@@ -0,0 +1,404 @@
1
+ "use strict";
2
+ /**
3
+ * Claude Orchestration Capability — Workflow-tool backend detection + emitter
4
+ *
5
+ * #1143 — adopts Claude Code's Workflow tool (the engine behind `/effort ultracode`)
6
+ * as an optional, runtime-gated parallel-execution backend for the GSD loop.
7
+ *
8
+ * This module is the pure, testable core of the capability. It owns two seams:
9
+ *
10
+ * detectWorkflowBackend({ runtimeId, hostIntegration, config, agentSdkVersion })
11
+ * → { available: boolean, backend: 'workflow'|'inline', reason: string }
12
+ * Fail-closed: every miss degrades to `inline` (today's behaviour), so the
13
+ * core loop is byte-identical unless every gate opens. This is criteria 3 + 6.
14
+ *
15
+ * emitWorkflowScript({ phaseDir, waves, runId, budgetTokens? })
16
+ * → { ok:true, script, summary } | { ok:false, reason }
17
+ * Maps GSD's wave/plan model 1:1 onto Workflow primitives:
18
+ * wave → sequential `parallel()` stage barriers,
19
+ * plan → `agent(brief, { agentType:'gsd-executor', isolation:'worktree' })`,
20
+ * files_modified overlap → forces plans into separate sequential stages
21
+ * (the same overlap rule execute-phase already applies inline),
22
+ * resumeFromRunId → wired to the phase run id,
23
+ * budgetTokens → a shared token pool.
24
+ * The emitted script composes the SAME gsd-executor agent and worktree
25
+ * isolation the inline path uses, so it produces the same artifacts/commits
26
+ * (criterion 2). It is a generated string consumed by the orchestrator; this
27
+ * module never invokes the Workflow tool itself.
28
+ *
29
+ * Design laws:
30
+ * - Gall's Law: ship a small working slice that composes existing primitives
31
+ * (gsd-executor + worktree isolation) rather than reinventing them.
32
+ * - Greenspun's Tenth Rule (cited in #1143): adopt the Workflow tool's
33
+ * barrier/pipeline/budget/resume semantics instead of hand-rolling them.
34
+ * - Postel's Law: liberal in input (missing fields → inline), conservative in
35
+ * output (workflow only when every gate opens).
36
+ * - Fail-closed: an unknown version, a missing descriptor, or a disabled
37
+ * toggle all resolve to `inline`, never to `workflow`.
38
+ *
39
+ * Zero external dependencies. Pure functions. Never throws on bad input.
40
+ */
41
+ // ─── Constants ────────────────────────────────────────────────────────────────
42
+ /**
43
+ * The Agent SDK version that introduced the Workflow tool (#1143 prior art).
44
+ * Used as the default floor when config does not override it. A runtime reporting
45
+ * an agentSdkVersion below this cannot host the Workflow backend.
46
+ */
47
+ const WORKFLOW_TOOL_FLOOR_VERSION = '0.3.149';
48
+ /** Closed enum for the `claude_orchestration.execution_backend` config key. */
49
+ const BACKEND_VALUES = new Set(['auto', 'workflow', 'inline']);
50
+ /** Only this runtime can host the Workflow tool (Claude Code / Agent SDK). */
51
+ const WORKFLOW_RUNTIME = 'claude';
52
+ // ─── Semver helpers ───────────────────────────────────────────────────────────
53
+ /** Official-ish strict SemVer 2.0.0 numeric triple (+ optional pre/build). */
54
+ const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
55
+ /** True for a syntactically valid semver string. */
56
+ function isValidSemver(s) {
57
+ return typeof s === 'string' && SEMVER_RE.test(s);
58
+ }
59
+ /**
60
+ * Compare two semver strings.
61
+ * Returns -1/0/1 in the usual sense. Garbage in either position → -1 (fail-closed:
62
+ * an unparseable version is treated as "less than" any real floor, so detection
63
+ * never accidentally enables the preview backend on an unknown SDK).
64
+ *
65
+ * Pre-release/build metadata are ignored for the comparison — only the numeric
66
+ * major.minor.patch triple participates, matching how the Workflow-tool floor is
67
+ * specified (a plain "0.3.149").
68
+ */
69
+ function compareSemver(a, b) {
70
+ if (!isValidSemver(a) || !isValidSemver(b))
71
+ return -1;
72
+ // Split numeric triple from pre-release/build metadata.
73
+ const parseTriple = (s) => {
74
+ const core = s.split('-')[0].split('+')[0].split('.');
75
+ return [parseInt(core[0], 10), parseInt(core[1], 10), parseInt(core[2], 10)];
76
+ };
77
+ const hasPre = (s) => s.indexOf('-') !== -1;
78
+ const preIdentifiers = (s) => (s.split('-')[1] || '').split('+')[0].split('.').filter((x) => x.length > 0);
79
+ const am = parseTriple(a);
80
+ const bm = parseTriple(b);
81
+ for (let i = 0; i < 3; i++) {
82
+ if (am[i] < bm[i])
83
+ return -1;
84
+ if (am[i] > bm[i])
85
+ return 1;
86
+ }
87
+ // Numeric triple is equal. SemVer 2.0.0 §11 precedence:
88
+ // - a version WITH a pre-release tag is LOWER than the same triple WITHOUT one
89
+ // (keeps the floor fail-closed for pre-release builds of the GA floor);
90
+ // - two pre-releases of the same triple are ordered by their dot-separated
91
+ // identifiers (numeric < alphanumeric; numeric compared numerically,
92
+ // alphanumeric lexically; fewer identifiers < more).
93
+ const aPre = hasPre(a);
94
+ const bPre = hasPre(b);
95
+ if (aPre && !bPre)
96
+ return -1;
97
+ if (!aPre && bPre)
98
+ return 1;
99
+ if (aPre && bPre) {
100
+ const ai = preIdentifiers(a);
101
+ const bi = preIdentifiers(b);
102
+ const len = Math.min(ai.length, bi.length);
103
+ for (let i = 0; i < len; i++) {
104
+ const ax = ai[i];
105
+ const bx = bi[i];
106
+ const aNum = /^\d+$/.test(ax);
107
+ const bNum = /^\d+$/.test(bx);
108
+ if (aNum && bNum) {
109
+ const an = parseInt(ax, 10);
110
+ const bn = parseInt(bx, 10);
111
+ if (an < bn)
112
+ return -1;
113
+ if (an > bn)
114
+ return 1;
115
+ }
116
+ else if (aNum && !bNum) {
117
+ return -1; // numeric identifiers always lower than alphanumeric
118
+ }
119
+ else if (!aNum && bNum) {
120
+ return 1;
121
+ }
122
+ else {
123
+ if (ax < bx)
124
+ return -1;
125
+ if (ax > bx)
126
+ return 1;
127
+ }
128
+ }
129
+ if (ai.length < bi.length)
130
+ return -1;
131
+ if (ai.length > bi.length)
132
+ return 1;
133
+ }
134
+ return 0;
135
+ }
136
+ /** Inline result shorthand. */
137
+ function inline(reason, available = false) {
138
+ return { available, backend: 'inline', reason };
139
+ }
140
+ /**
141
+ * Resolve whether the Workflow-tool backend should activate.
142
+ *
143
+ * Gate ladder (all must pass for `workflow`; first miss wins, fail-closed):
144
+ * 1. capability enabled (claude_orchestration.enabled truthy)
145
+ * 2. runtime is Claude (the only runtime that exposes the Workflow tool)
146
+ * 3. execution_backend !== 'inline'
147
+ * 4. host descriptor signals nested+background dispatch (Workflow-tool capable)
148
+ * 5. agentSdkVersion is a known, valid semver
149
+ * 6. agentSdkVersion >= the configured floor (default WORKFLOW_TOOL_FLOOR_VERSION)
150
+ * 7. execution_backend === 'workflow' OR 'auto' (both reach here; 'inline' exited at 3)
151
+ *
152
+ * Never throws. Destructures defensively.
153
+ */
154
+ function detectWorkflowBackend(input) {
155
+ if (input === null || input === undefined || typeof input !== 'object') {
156
+ return inline('capability_disabled');
157
+ }
158
+ const cfg = (input.config !== null && input.config !== undefined && typeof input.config === 'object')
159
+ ? input.config
160
+ : {};
161
+ // 1. capability must be opted in (default-off — ships disabled).
162
+ if (!cfg['claude_orchestration.enabled']) {
163
+ return inline('capability_disabled');
164
+ }
165
+ // 2. only Claude can host the Workflow tool.
166
+ if (input.runtimeId !== WORKFLOW_RUNTIME) {
167
+ return inline('runtime_not_claude');
168
+ }
169
+ // 3. explicit inline opt-out short-circuits.
170
+ let backendRaw = cfg['claude_orchestration.execution_backend'];
171
+ if (typeof backendRaw !== 'string' || !BACKEND_VALUES.has(backendRaw)) {
172
+ backendRaw = 'auto';
173
+ }
174
+ if (backendRaw === 'inline') {
175
+ return inline('backend_inline');
176
+ }
177
+ // 4. the host dispatch descriptor must be the nesting-capable Claude-Code shape
178
+ // (a proxy for Workflow-tool presence). This is Claude-specific and already
179
+ // gated at step 2; `background:true` alone is true on several non-Claude hosts,
180
+ // so the proxy is only meaningful after the runtime check above. Note: this is
181
+ // NOT the canonical `shouldFlattenDispatch` rule (which keys on
182
+ // `backgroundDispatch`); the Workflow backend works precisely because a single
183
+ // tool-call orchestrates internally, sidestepping the backgroundDispatch:false
184
+ // limitation. Missing/false/foreign descriptor → fail-closed.
185
+ const hi = input.hostIntegration;
186
+ if (hi === null || hi === undefined || typeof hi !== 'object' || Array.isArray(hi)) {
187
+ return inline('workflow_tool_unavailable');
188
+ }
189
+ const dispatch = hi.dispatch;
190
+ if (typeof dispatch !== 'object' || dispatch === null || Array.isArray(dispatch)) {
191
+ return inline('workflow_tool_unavailable');
192
+ }
193
+ const nested = dispatch['nested'];
194
+ const background = dispatch['background'];
195
+ if (nested !== true || background !== true) {
196
+ return inline('workflow_tool_unavailable');
197
+ }
198
+ // 5. an unknown agentSdkVersion cannot be trusted to meet the floor.
199
+ if (!isValidSemver(input.agentSdkVersion)) {
200
+ return inline('agent_sdk_version_unknown');
201
+ }
202
+ // 6. version floor (config override > default constant).
203
+ const floorRaw = cfg['claude_orchestration.min_agent_sdk_version'];
204
+ const floor = typeof floorRaw === 'string' && isValidSemver(floorRaw) ? floorRaw : WORKFLOW_TOOL_FLOOR_VERSION;
205
+ if (compareSemver(input.agentSdkVersion, floor) < 0) {
206
+ return inline('agent_sdk_version_below_floor');
207
+ }
208
+ // 7. auto/workflow both reach the workflow backend once every gate passes.
209
+ return { available: true, backend: 'workflow', reason: 'workflow_backend_active' };
210
+ }
211
+ /**
212
+ * Partition a wave's plans into a near-minimal number of sequential stages (via
213
+ * greedy first-fit — not guaranteed optimal for arbitrary overlap graphs, but
214
+ * correct: no two plans sharing a file ever cohabit a stage) such that no two
215
+ * plans in the same stage share a modified file. Each plan goes into the earliest
216
+ * stage where it does not overlap any plan already there.
217
+ *
218
+ * A plan with an EMPTY files_modified set declares no files; it overlaps nothing
219
+ * and coalesces into stage 0 (same behavior as the inline path, which also cannot
220
+ * guard against undeclared concurrent writes — declare filesModified accurately).
221
+ *
222
+ * This is the same overlap rule execute-phase applies inline — the only difference
223
+ * is the execution vehicle (Workflow `parallel()` vs one-agent-per-message).
224
+ */
225
+ function partitionStages(plans) {
226
+ const stages = [];
227
+ for (const plan of plans) {
228
+ const fileSet = new Set(plan.files_modified);
229
+ let placed = false;
230
+ for (const stage of stages) {
231
+ let overlap = false;
232
+ for (const f of fileSet) {
233
+ if (stage.files.has(f)) {
234
+ overlap = true;
235
+ break;
236
+ }
237
+ }
238
+ if (!overlap) {
239
+ stage.plans.push(plan);
240
+ for (const f of fileSet)
241
+ stage.files.add(f);
242
+ placed = true;
243
+ break;
244
+ }
245
+ }
246
+ if (!placed) {
247
+ stages.push({ plans: [plan], files: new Set(fileSet) });
248
+ }
249
+ }
250
+ return stages.map((s) => s.plans.map((p) => p.id));
251
+ }
252
+ /**
253
+ * Quote a free-text value for safe embedding as a JavaScript/Workflow double-quoted
254
+ * string literal. Uses JSON.stringify so every JS-relevant escape (backslash, quote,
255
+ * newline, tab, NUL, U+2028/U+2029, all control chars) is handled by the language
256
+ * itself — there is no hand-rolled escape table to drift. Returns the value already
257
+ * wrapped in its surrounding quotes.
258
+ */
259
+ function quoteString(s) {
260
+ return JSON.stringify(s);
261
+ }
262
+ /**
263
+ * True if `s` is a safe identifier/path token to interpolate into the generated
264
+ * script WITHOUT requiring a string-literal context — i.e. it contains no
265
+ * character that could terminate a comment line (`\n`/`\r`), break out of a
266
+ * string literal (`"` / `\`), or smuggle a NUL/control sequence. Used for
267
+ * `phaseDir`, `runId`, `wave.id`, and `plan.id`, which are identifiers/paths and
268
+ * must never legitimately contain such characters. Rejecting them at validation
269
+ * (rather than silently flattening) keeps the emitted script faithful to input.
270
+ */
271
+ const UNSCRIPTABLE_CHAR_RE = /[\r\n"\\\x00-\x1f\x7f\u2028\u2029]/;
272
+ function isScriptableIdentifier(s) {
273
+ if (typeof s !== 'string' || s.length === 0)
274
+ return false;
275
+ return !UNSCRIPTABLE_CHAR_RE.test(s);
276
+ }
277
+ /**
278
+ * Emit a Workflow script mapping the phase's wave/plan model onto Workflow
279
+ * primitives. Pure and deterministic: identical input yields an identical string.
280
+ *
281
+ * Returns ok:false (never throws) on invalid input — empty waves, missing runId,
282
+ * a wave with no plans, etc.
283
+ */
284
+ function emitWorkflowScript(input) {
285
+ if (input === null || input === undefined || typeof input !== 'object') {
286
+ return { ok: false, reason: 'invalid_input' };
287
+ }
288
+ const { phaseDir, waves, runId } = input;
289
+ // Identifiers/paths interpolated into the generated script must be free of any
290
+ // character that could terminate a comment, break out of a string literal, or
291
+ // smuggle control bytes — reject up front (security: #1143 review Finding 1).
292
+ if (!isScriptableIdentifier(phaseDir)) {
293
+ return { ok: false, reason: 'phaseDir must be a non-empty string without newlines/quotes/backslash/control chars' };
294
+ }
295
+ if (!isScriptableIdentifier(runId)) {
296
+ return { ok: false, reason: 'runId must be a non-empty string without newlines/quotes/backslash/control chars' };
297
+ }
298
+ if (!Array.isArray(waves) || waves.length === 0) {
299
+ return { ok: false, reason: 'waves must be a non-empty array' };
300
+ }
301
+ for (let i = 0; i < waves.length; i++) {
302
+ const w = waves[i];
303
+ if (w === null || typeof w !== 'object' || typeof w.id !== 'string') {
304
+ return { ok: false, reason: 'waves[' + i + '] must be { id, plans: non-empty[] }' };
305
+ }
306
+ if (!isScriptableIdentifier(w.id)) {
307
+ return { ok: false, reason: 'waves[' + i + '].id must not contain newlines/quotes/backslash/control chars' };
308
+ }
309
+ if (!Array.isArray(w.plans) || w.plans.length === 0) {
310
+ return { ok: false, reason: 'waves[' + i + '] must have a non-empty plans array' };
311
+ }
312
+ const seenIds = new Set();
313
+ for (let j = 0; j < w.plans.length; j++) {
314
+ const p = w.plans[j];
315
+ if (p === null || typeof p !== 'object' || typeof p.id !== 'string' || typeof p.brief !== 'string' || !Array.isArray(p.files_modified)) {
316
+ return { ok: false, reason: 'waves[' + i + '].plans[' + j + '] must be { id, brief, files_modified[] }' };
317
+ }
318
+ if (!isScriptableIdentifier(p.id)) {
319
+ return { ok: false, reason: 'waves[' + i + '].plans[' + j + '].id must not contain newlines/quotes/backslash/control chars' };
320
+ }
321
+ if (seenIds.has(p.id)) {
322
+ return { ok: false, reason: 'waves[' + i + '] has duplicate plan id "' + p.id + '"' };
323
+ }
324
+ seenIds.add(p.id);
325
+ for (const f of p.files_modified) {
326
+ if (typeof f !== 'string' || f.length === 0) {
327
+ return { ok: false, reason: 'waves[' + i + '].plans[' + j + '].files_modified entries must be non-empty strings' };
328
+ }
329
+ }
330
+ }
331
+ }
332
+ const budgetTokens = (typeof input.budgetTokens === 'number' && Number.isFinite(input.budgetTokens) && input.budgetTokens > 0)
333
+ ? Math.floor(input.budgetTokens)
334
+ : null;
335
+ const lines = [];
336
+ lines.push('// GSD Workflow script — generated by the claude-orchestration capability (#1143)');
337
+ lines.push('// phase: ' + phaseDir);
338
+ lines.push('// BETA: preview-grade; on any failure the orchestrator falls back to inline dispatch.');
339
+ lines.push('// Composes the SAME gsd-executor agent + worktree isolation as the inline path,');
340
+ lines.push('// so artifacts (SUMMARY.md) and commits are produced identically.');
341
+ lines.push('resumeFromRunId(' + quoteString(runId) + ')');
342
+ if (budgetTokens !== null) {
343
+ lines.push('budget(' + budgetTokens + ')');
344
+ }
345
+ lines.push('');
346
+ const stagesByWave = [];
347
+ let totalPlans = 0;
348
+ for (let wi = 0; wi < waves.length; wi++) {
349
+ const wave = waves[wi];
350
+ const stages = partitionStages(wave.plans);
351
+ stagesByWave.push(stages);
352
+ totalPlans += wave.plans.length;
353
+ lines.push('// Wave ' + wave.id);
354
+ for (let si = 0; si < stages.length; si++) {
355
+ const stagePlanIds = stages[si];
356
+ // Resolve back to plan objects for briefs (ids are unique within a wave — validated above).
357
+ const stagePlans = stagePlanIds.map((id) => wave.plans.find((p) => p.id === id));
358
+ if (stages.length > 1) {
359
+ lines.push('// Stage ' + si + (si > 0 ? ' (sequential — files_modified overlap)' : ''));
360
+ }
361
+ if (stagePlans.length === 1) {
362
+ const p = stagePlans[0];
363
+ lines.push('parallel(');
364
+ lines.push(' agent(' + quoteString(p.brief) + ', { agentType: "gsd-executor", isolation: "worktree" })');
365
+ lines.push(')');
366
+ }
367
+ else {
368
+ lines.push('parallel(');
369
+ for (const p of stagePlans) {
370
+ lines.push(' agent(' + quoteString(p.brief) + ', { agentType: "gsd-executor", isolation: "worktree" }),');
371
+ }
372
+ // Replace trailing comma on the last agent line with nothing.
373
+ const lastIdx = lines.length - 1;
374
+ lines[lastIdx] = lines[lastIdx].replace(/,$/, '');
375
+ lines.push(')');
376
+ }
377
+ }
378
+ if (wi < waves.length - 1)
379
+ lines.push('');
380
+ }
381
+ lines.push('// Each agent writes SUMMARY.md on its worktree branch; commits land there');
382
+ lines.push('// and are merged by the orchestrator exactly as in inline wave dispatch.');
383
+ const script = lines.join('\n');
384
+ return {
385
+ ok: true,
386
+ script,
387
+ summary: {
388
+ waves: waves.length,
389
+ plans: totalPlans,
390
+ stagesByWave,
391
+ resumeRunId: runId,
392
+ budgetTokens,
393
+ },
394
+ };
395
+ }
396
+ module.exports = {
397
+ detectWorkflowBackend,
398
+ emitWorkflowScript,
399
+ compareSemver,
400
+ isValidSemver,
401
+ WORKFLOW_TOOL_FLOOR_VERSION,
402
+ BACKEND_VALUES,
403
+ WORKFLOW_RUNTIME,
404
+ };
@@ -20,7 +20,9 @@ exports.CLUSTERS = void 0;
20
20
  exports.allClusteredSkills = allClusteredSkills;
21
21
  exports.CLUSTERS = Object.freeze({
22
22
  core_loop: Object.freeze([
23
+ 'next',
23
24
  'new-project',
25
+ 'onboard',
24
26
  'discuss-phase',
25
27
  'plan-phase',
26
28
  'execute-phase',
@@ -283,6 +283,14 @@ exports.INIT_COMMAND_ALIASES = [
283
283
  "subcommand": "new-milestone",
284
284
  "mutation": false
285
285
  },
286
+ {
287
+ "canonical": "init.onboard",
288
+ "aliases": [
289
+ "init onboard"
290
+ ],
291
+ "subcommand": "onboard",
292
+ "mutation": false
293
+ },
286
294
  {
287
295
  "canonical": "init.quick",
288
296
  "aliases": [
@@ -531,7 +531,12 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
531
531
  parsedValue = true;
532
532
  else if (val === 'false')
533
533
  parsedValue = false;
534
- else if (!isNaN(Number(val)) && val !== '')
534
+ // #1581: Number.isFinite (not !isNaN) so 'Infinity'/'-Infinity' are NOT
535
+ // coerced to non-finite numbers that JSON.stringify later renders as `null`
536
+ // (disk=null while the CLI echoed 'Infinity'). They fall through to the
537
+ // JSON branch (which rejects them) and stay strings, then per-key validators
538
+ // reject them with a non-zero exit.
539
+ else if (Number.isFinite(Number(val)) && val !== '')
535
540
  parsedValue = Number(val);
536
541
  else if (typeof val === 'string' && (val.startsWith('[') || val.startsWith('{'))) {
537
542
  try {
@@ -539,6 +544,11 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
539
544
  }
540
545
  catch { /* keep as string */ }
541
546
  }
547
+ // #1581: project_code is an identifier string — never number-coerce it. A
548
+ // leading-zero code like '007' must persist verbatim (not collapse to 7).
549
+ if (kp === 'project_code') {
550
+ parsedValue = val;
551
+ }
542
552
  const VALID_CONTEXT_VALUES = ['dev', 'research', 'review'];
543
553
  if (kp === 'context')
544
554
  assertEnumValue(parsedValue, val, VALID_CONTEXT_VALUES, 'context value');
@@ -551,6 +561,14 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
551
561
  error(`Invalid workflow.drift_threshold '${val}'. Must be a positive integer.`);
552
562
  }
553
563
  }
564
+ // #1581: context_window must be a finite positive integer. 'Infinity' is no
565
+ // longer number-coerced (see the parse block above) so it reaches here as a
566
+ // string and is rejected; '0', negatives, and non-integers are also rejected.
567
+ if (kp === 'context_window') {
568
+ if (typeof parsedValue !== 'number' || !Number.isFinite(parsedValue) || !Number.isInteger(parsedValue) || parsedValue < 1) {
569
+ error(`Invalid context_window '${val}'. Must be a positive integer (token count).`, ERROR_REASON.USAGE);
570
+ }
571
+ }
554
572
  // Post-planning gap checker (#2493)
555
573
  if (kp === 'workflow.post_planning_gaps') {
556
574
  if (typeof parsedValue !== 'boolean') {
@@ -183,6 +183,46 @@ function extractCanonicalPlanId(filename) {
183
183
  }
184
184
  return base;
185
185
  }
186
+ /**
187
+ * Count summaries that correspond to a real plan (#1988).
188
+ *
189
+ * A summary counts toward phase completion iff it pairs with an existing plan
190
+ * file. This excludes stray non-plan summaries — e.g. `30-FIX-CR02-SUMMARY.md`,
191
+ * `30-GAPCLOSURE-SUMMARY.md` — that inflate the raw `*-SUMMARY.md` count and
192
+ * silently flip a phase to Complete when plans are actually missing summaries.
193
+ *
194
+ * Pairing is layout-agnostic. For each plan, up to three candidate summary
195
+ * filenames are generated and any match suffices:
196
+ * 1. marker swap `PLAN`→`SUMMARY` on the basename — root padded
197
+ * (`30-01-PLAN.md`↔`30-01-SUMMARY.md`), nested (`PLAN-01.md`↔
198
+ * `SUMMARY-01.md`, incl. a `plans/` prefix), and bare (`PLAN.md`↔
199
+ * `SUMMARY.md`);
200
+ * 2. `<stem>-SUMMARY.md` — bare (`PLAN.md`↔`PLAN-SUMMARY.md`) and legacy
201
+ * (`14-PLAN-01.md`↔`14-PLAN-01-SUMMARY.md`);
202
+ * 3. extended `<n>-PLAN-<m>…`→`<n>-<m>-SUMMARY.md`
203
+ * (`3-PLAN-01-setup.md`↔`3-01-SUMMARY.md`).
204
+ * The swap is applied to the basename only so a lowercase `plans/` dir prefix
205
+ * isn't corrupted to `SUMMARYs/…`.
206
+ */
207
+ function countMatchedSummaries(planFiles, summaryFiles) {
208
+ const summarySet = new Set(summaryFiles);
209
+ let matched = 0;
210
+ for (const plan of planFiles) {
211
+ const slashIdx = plan.lastIndexOf('/');
212
+ const dir = slashIdx >= 0 ? plan.slice(0, slashIdx + 1) : '';
213
+ const base = (dir ? plan.slice(dir.length) : plan).replace(/\.md$/i, '');
214
+ const candidates = [
215
+ dir + base.replace(/PLAN/i, 'SUMMARY') + '.md',
216
+ dir + base + '-SUMMARY.md',
217
+ ];
218
+ const extended = base.match(/^(\d+)-PLAN-(\d+)/i);
219
+ if (extended)
220
+ candidates.push(dir + extended[1] + '-' + extended[2] + '-SUMMARY.md');
221
+ if (candidates.some((c) => summarySet.has(c)))
222
+ matched++;
223
+ }
224
+ return matched;
225
+ }
186
226
  module.exports = {
187
227
  toPosixPath,
188
228
  detectSubRepos,
@@ -195,4 +235,5 @@ module.exports = {
195
235
  readSubdirectories,
196
236
  timeAgo,
197
237
  extractCanonicalPlanId,
238
+ countMatchedSummaries,
198
239
  };