Merge branch 'next' into codex/gsd-onboard
This commit is contained in:
5
.changeset/1143-claude-orchestration-capability.md
Normal file
5
.changeset/1143-claude-orchestration-capability.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Added
|
||||
pr: 2044
|
||||
---
|
||||
**A default-off, BETA, claude-only "Claude orchestration" capability** — adopts Claude Code's Workflow tool (`/effort ultracode`, Agent SDK ≥ v0.3.149) as an optional parallel-execution backend for the GSD loop, restoring the wave parallelism + plan-checker + verifier that the #853 backgrounded-agent nesting limitation forces inline on Claude Code, and folding the existing `gsd-ultraplan-phase` plan-offload under the same runtime gate. When `claude_orchestration.enabled` is on AND the runtime is Claude AND the Workflow tool is detected AND the Agent SDK meets the floor (`claude_orchestration.min_agent_sdk_version`, default `0.3.149`), `execute-phase` emits a generated Workflow script (`waves → parallel() barriers`, `plans → agent({ agentType: 'gsd-executor', isolation: 'worktree' })`, `files_modified overlap → separate sequential stages`, `resumeFromRunId` wired to the phase run id, shared `budget` pool) that composes the SAME executor agent + worktree isolation the inline path uses, so artifacts/commits are produced identically. Detection is pure and fail-closed (any miss → inline), so on any runtime lacking the Workflow tool behaviour is byte-identical to today. Adds a pure module `gsd-core/bin/lib/claude-orchestration.cjs` (`detectWorkflowBackend`, `emitWorkflowScript`), the `capabilities/claude-orchestration/` declaration with two gated loop contributions (`execute:wave:post`, `plan:post`) and a `claude-orchestration` command family (`gsd-tools claude-orchestration detect-backend|emit-workflow`), federated config keys, and an ADR-1143 implementation amendment. (#1143)
|
||||
5
.changeset/1921-verify-work-gap-recovery.md
Normal file
5
.changeset/1921-verify-work-gap-recovery.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2025
|
||||
---
|
||||
**`/gsd:verify-work` preserves verification state across gap-closure execution and no longer auto-promotes deferred follow-ups into blocking gaps** — resuming after `/gsd:execute-phase --gaps-only` used to lose the verification state: the UAT `## Gaps` still read `status: failed` even after their fix plans executed, so verify-work re-diagnosed them as fresh blockers, spawned a new gap plan, and reported only the new plan as verified. A state contract now links each gap to its fix plan: every UAT gap carries a stable `gap_id` (`G-{phase}-{N}`), gap-closure plans tag the ids they address in their frontmatter (`gap_ids: […]`), and a new `reconcile_gaps` step on resume marks a gap `status: resolved` when its plan has a matching `*-SUMMARY.md` — so fixed gaps aren't re-diagnosed and the phase can close. Separately, a deferred-follow-up branch captures future-work ideas (signals like "later", "next version", "out of scope") into a `## Deferred Follow-Ups` section instead of creating a blocking gap/plan. (#1921)
|
||||
6
.changeset/2002-cli-self-healing-runtime-build.md
Normal file
6
.changeset/2002-cli-self-healing-runtime-build.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 2036
|
||||
---
|
||||
|
||||
**The GSD CLI now self-heals a missing runtime build.** The compiled `gsd-core/bin/lib/*.cjs` modules are gitignored build artifacts (ADR-457) that ship prebuilt in the npm tarball but are absent on a Claude Code plugin-marketplace / git-clone install, which never runs `npm run build:lib`. Previously every command died at load with `Cannot find module './lib/cli-exit.cjs'`. The `gsd-tools` entrypoint now detects the missing output and compiles it once, on demand (lock-guarded so parallel invocations don't race), then proceeds — a single no-op check on the already-built npm path. When TypeScript is genuinely unavailable it prints an actionable `npm install && npm run build:lib` message instead of crashing.
|
||||
5
.changeset/2012-phase-complete-progress-row.md
Normal file
5
.changeset/2012-phase-complete-progress-row.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2032
|
||||
---
|
||||
**`phase.complete` now updates the `## Progress` rollup row even when an earlier phase-numbered table precedes it** — the Progress-row writer used a non-global regex that matched *any* table row starting with the phase number, so it bound to the first such row (e.g. a `| Phase | Requirements | Count |` coverage table), no-op'd on the wrong 3-column row, and never reached the real Progress row. The regex is now scoped to the `## Progress` section so it binds to the correct table. The command still returned `roadmap_updated: true` (that field is `fs.existsSync(ROADMAP.md)`), masking the silent failure. (#2012)
|
||||
5
.changeset/2017-context7-plugin-grant-prefix.md
Normal file
5
.changeset/2017-context7-plugin-grant-prefix.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2029
|
||||
---
|
||||
**context7 now works for plugin-marketplace installs (8 agents regained doc lookup)** — the agents granted only `mcp__context7__*`, which matches a standalone context7 MCP server but not the official Claude Code plugin-marketplace install (`context7@claude-plugins-official`), whose tools are named `mcp__plugin_context7_context7__*`. The grant never matched, so advisor/ai/domain/phase/project/ui-researcher + planner + executor silently lost documentation lookup and fell back to WebSearch. All 8 agents now grant both forms, the researcher profile table is updated, and a parity guard asserts no agent grants the standalone form without the plugin form. (#2017)
|
||||
5
.changeset/2018-applysurface-empty-manifest-agents.md
Normal file
5
.changeset/2018-applysurface-empty-manifest-agents.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2031
|
||||
---
|
||||
**`applySurface` no longer deletes every `gsd-*` agent when the skills manifest resolves empty** — the agent-prune loop in `_syncGsdDir` deleted any `gsd-*.md` not in the staged set, and when the manifest was empty/unresolvable (null manifest, no array entries, no `files` key, or an unresolvable install source root), the staged set was empty → every agent was pruned. Skills were guarded by `pruneSkillDirs`'s manifest-membership check (conservative preservation on empty manifest); agents had no equivalent. The agent-prune loop is now skipped when the manifest is empty/absent, so agents are preserved while copy (adding genuinely new agents) still runs. (#2018)
|
||||
5
.changeset/2019-planning-config-learnings-path.md
Normal file
5
.changeset/2019-planning-config-learnings-path.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2026
|
||||
---
|
||||
**`planning-config.md` global-learnings path corrected to `~/.gsd/knowledge/`** — the `features.global_learnings` row directed users to `~/.gsd/learnings/`, but the implementation (`src/learnings.cts`, `execute-phase.md`) stores and reads global learnings from `~/.gsd/knowledge/`. Anyone following the docs to inspect, back up, or seed their global learnings looked in a directory the code never touches. (#2019)
|
||||
5
.changeset/2020-executor-dead-sdk-ref.md
Normal file
5
.changeset/2020-executor-dead-sdk-ref.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2027
|
||||
---
|
||||
**Removed dead SDK file references from runtime-loaded markdown that triggered an infinite `find.exe` storm on Windows** — `agents/gsd-executor.md` pointed at `sdk/src/query/QUERY-HANDLERS.md` and `gsd-core/workflows/reapply-patches.md` at `sdk/dist/cli.js`, both retired with the SDK package (ADR-0174). AI runtimes that resolve doc references by filesystem search ran `find / -iname …`; on Git Bash for Windows `/` maps to the drive root, so `find.exe` traversed the whole disk (14h+, orphaned processes, 4M+ open handles each, unkillable). The references now resolve to live paths, and a new regression guard asserts no `sdk/src|sdk/dist|sdk/handlers` file references remain in agents/workflows/references markdown. (#2020)
|
||||
5
.changeset/2022-roadmap-verify-gate.md
Normal file
5
.changeset/2022-roadmap-verify-gate.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2030
|
||||
---
|
||||
**`roadmap update-plan-progress` no longer checks the phase checkbox without verification** — the command stamped the phase-level ROADMAP checkbox and completion date the moment the last plan summary landed (called routinely after every wave and every plan), with **no verification gate** — unlike `phase.complete` which correctly requires `readVerificationStatus(...).status === 'passed'`. Now `isComplete` requires both all plan summaries AND a passed verification, matching the `cmdPhaseComplete` contract, so the checkbox only fires after `gsd-verifier` has confirmed the phase. (#2022)
|
||||
5
.changeset/gentle-badgers-roar.md
Normal file
5
.changeset/gentle-badgers-roar.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2048
|
||||
---
|
||||
**`model_overrides` Claude model IDs now resolve to Agent-tool aliases on the claude runtime** — a full Claude model ID (e.g. `claude-sonnet-5`) in `model_overrides` was returned verbatim and silently dropped by the Claude Agent tool (whose `model` parameter documents only tier aliases), causing the spawned subagent to inherit the parent session model instead of the configured one. It now maps to the tier alias (`sonnet`/`opus`/`haiku`/`fable`), consistent with the `model_policy` path (#1144). Bare aliases, non-Claude values, and non-Claude runtimes are unchanged; a Claude ID with no alias warns once and falls through to tier resolution. (#2041)
|
||||
5
.changeset/plucky-jays-dart.md
Normal file
5
.changeset/plucky-jays-dart.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 2040
|
||||
---
|
||||
**`/gsd:surface` and `--materialize` now produce byte-identical agent output to a fresh install** — surface-path agents for descriptor-driven runtimes (cursor, windsurf, augment, trae, codebuddy, copilot, antigravity) now receive the same path-prefix rewrite, Co-Authored-By attribution, runtime-specific conversion, and body normalization as the install path. Copilot and Antigravity agents are now installed via the descriptor-driven path (copilot agents get the `.agent.md` filename rename). Cline remains on the inline loop (rules-only local branch). (#1575)
|
||||
5
.changeset/steady-ibex-run.md
Normal file
5
.changeset/steady-ibex-run.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2049
|
||||
---
|
||||
**Skill-bearing capabilities now surface correctly on flat command-layout installs** — on an install using the flat `commands/gsd-<stem>.md` source layout (e.g. a Claude Code local project install with no `commands/gsd/` subdir), every skill-bearing capability (`nyquist`, `code-review`, `security`, `ui`, `mempalace`, `ai-integration`, `profile-pipeline`) was silently reported `surfaced:false`/`enabled:false`/`active:false`, so their loop hooks (`verify:post`, `execute:post`, etc.) never fired even with the corresponding `workflow.*` toggle on. The skill-manifest resolver now detects the flat layout and produces the same stems the nested `commands/gsd/*.md` loader does. (#1858)
|
||||
5
.changeset/zcode-runtime-1925.md
Normal file
5
.changeset/zcode-runtime-1925.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Added
|
||||
pr: 2039
|
||||
---
|
||||
**ZCode (Z.ai) is now an installable runtime** — a desktop Agentic Development Environment for the GLM-5.2 model can now be targeted with `--zcode`, landing GSD skills at `~/.zcode/skills/<name>/SKILL.md` plus slash commands and subagents. ZCode ships as a pure declarative capability descriptor (`capabilities/zcode/capability.json`) with zero hardcoded `runtime === 'zcode'` branches, reusing the Claude skill converter — the de-hardcoded, data-driven runtime path that 1.7.0 (ADR-1016 / ADR-1239) enables. (#1925)
|
||||
@@ -9,7 +9,7 @@
|
||||
{
|
||||
"name": "gsd-core",
|
||||
"description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"source": "./",
|
||||
"author": {
|
||||
"name": "open-gsd",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "gsd-core",
|
||||
"displayName": "GSD Core",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
|
||||
"author": {
|
||||
"name": "open-gsd",
|
||||
|
||||
@@ -220,6 +220,8 @@ ADR-1244 Phase 4 (D5+D6) orchestration seam (`gsd-core/bin/lib/capability-lifecy
|
||||
### Capability Command Dispatch
|
||||
ADR-1244 Phase 5 (D7) registry-driven dispatch of capability command families. First-party families (`graphify`/`intel`/`audit`, shipped in `bin/lib/`) dispatch via `dispatchCapabilityCommand` (`gsd-core/bin/gsd-tools.cjs`) against the FROZEN `capability-registry.cjs` `commandFamilies` (confined to `bin/lib/`) — unchanged. Third-party (installed overlay) families dispatch via `dispatchOverlayCapabilityCommand`: after the first-party path returns false, it calls `loadRegistry({ includeInstalled, cwd })` and dispatches a family iff its `capId` is in `_overlay.commandRoots` — which `capability-loader.cjs` populates ONLY for accepted overlay capabilities that declare `commands` AND pass the loader's activation gate (a **committed** ledger entry, present and non-`_pending`, PLUS — for PROJECT scope — a matching user consent record in the Capability Consent Store; GLOBAL scope needs no consent record). A bundle dropped on disk with no install (no ledger entry) or no on-this-machine consent is NOT command-dispatchable. The router module is `require()`'d FROM the capability's install root via `defaultRequireFromInstallRoot` (bare-`.cjs` basename + `realpath` containment, rejecting `..` traversal and symlink escape); same own-property/function/sync-only guards as the first-party path. Wired into the `runCommand` default arm before "Unknown command". A repo-planted project ledger no longer activates anything on its own (#1459) — see `docs/explanation/capability-trust-model.md` "project-scope trust boundary".
|
||||
|
||||
### Claude Orchestration Capability
|
||||
Default-off, BETA, claude-only Capability (`capabilities/claude-orchestration/`, `role: feature`, `runtimeCompat.supported: ["claude"]`, `tier: full`, `activationKey: claude_orchestration.enabled`) adopting Claude Code's Workflow tool (the engine behind `/effort ultracode`, Agent SDK ≥ v0.3.149) as an optional parallel-execution backend for the GSD loop, and folding the `gsd-ultraplan-phase` plan-offload under the same runtime gate (#1143; ADR-1143). Pure, fail-closed core in `gsd-core/bin/lib/claude-orchestration.cjs` (generated from `src/claude-orchestration.cts`): `detectWorkflowBackend({ runtimeId, hostIntegration, config, agentSdkVersion }) → { available, backend:'workflow'|'inline', reason }` (gate ladder: enabled → Claude → execution_backend ≠ inline → host dispatch nested+background → valid Agent SDK → SDK ≥ floor; every miss degrades to `inline`, never throws); `emitWorkflowScript({ phaseDir, waves, runId, budgetTokens? }) → { ok, script, summary }` mapping waves → `parallel()` stage barriers, plans → `agent({ agentType:'gsd-executor', isolation:'worktree' })`, `files_modified` overlap → separate sequential stages (greedy first-fit), `resumeFromRunId` wired to the run id, shared `budget(tokens)`; all interpolated identifiers validated script-safe (no `"`,`\`,control chars) and briefs JSON-quoted (review anti-injection). Registers two loop contributions at WIRED points only (execute:wave:pre/execute:pre are declared but not rendered, same constraint external-job documents): `execute:wave:post into:executor` (Workflow-backend guidance) and `plan:post into:planner` (ultraplan ownership declaration), both `when: claude_orchestration.enabled`, `onError: skip`. Federated config keys (`claude_orchestration.enabled` default false, `execution_backend` enum auto|workflow|inline default auto, `min_agent_sdk_version` string default "0.3.149") live only in the registry — uninstall removes them cleanly. Pre-release versions of the floor compare below GA (SemVer precedence). Restores the wave parallelism + plan-checker + verifier that #853 forces inline on Claude Code; on any runtime lacking the Workflow tool, behaviour is byte-identical to today. BETA v1 ships detection + emission + declarative ultraplan ownership + a `claude-orchestration` command family (`gsd-tools claude-orchestration detect-backend|emit-workflow`, router `gsd-core/bin/lib/claude-orchestration-command-router.cjs` from `src/claude-orchestration-command-router.cts`); full install-profile migration of the ultraplan skill into `skills[]` is a follow-up (CLUSTERS/profile gate). Test anchors: `tests/claude-orchestration.test.cjs`, `tests/claude-orchestration-command-router.test.cjs`.
|
||||
### Loop Extension Point
|
||||
A named, stable site on a host loop step (per-step `pre`/`post` plus per-wave in Execute; 12 total) where Capabilities register hooks. Three hook kinds: `step` (runs as its own sequenced unit), `contribution` (injects into the core step's prompt/context), and `gate` (checks and optionally blocks via a declared `blocking` flag). Each hook declares the artifacts it produces and consumes; hook order is derived by topological sort of that produces/consumes graph (capability-id tiebreak), which also defines data flow — file-artifact based, surviving `/clear` and fresh executor contexts. Hooks are surfaced by runtime resolution with concrete projection: the workflow calls a query that resolves the active hooks and returns fully-rendered, ordered markdown for the executor. Failure is default-resilient — a non-gate hook that errors is skipped with a warning; a hook may opt into `onError: halt`. Part of the Capability system. ADR-857 phase 3c ships the registry-consuming query layer: `gsd-core/bin/lib/loop-resolver.cjs` exposes `resolveLoopHooks({ point, registry, config })` (pure, no I/O), `renderLoopHooks(resolved)` (pure markdown renderer), and `cmdLoopRenderHooks(cwd, point, raw, opts)` (I/O entry point); activated via `gsd-tools loop render-hooks <point>` which emits `{ point, activeHooks[], rendered }`. Activation is driven by `when` (dotted config key resolved against `loadConfig`), with inline literal `__proto__`/`constructor`/`prototype` prototype-pollution guard. The first phase-6 cutovers wiring workflows to this query have landed — ui-phase at `plan:pre` and ui-review at `verify:post` (in `plan-phase.md`/`autonomous.md`); further per-feature cutovers are ongoing.
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
# Quick Wins: Confirmed-Bug Fixes
|
||||
|
||||
**Status**: Active
|
||||
**Started**: 2026-05-16
|
||||
**Owner**: Current session (Grok + user)
|
||||
**Context**: Follow-up to `/gsd-inbox` triage on 2026-05-16
|
||||
|
||||
## Goal
|
||||
|
||||
Land 6 high-signal, confirmed-bug issues that currently have **zero open pull requests**. These are the cleanest quick-win opportunities available in the public GitHub inbox right now.
|
||||
|
||||
All six issues carry the `confirmed-bug` label, meaning the bug has been verified and a fix is explicitly welcome.
|
||||
|
||||
## The 6 Issues (Prioritized)
|
||||
|
||||
| # | Issue | Short Title | Type | Recommended Flow | Est. Effort | Status | Notes |
|
||||
|---|-------|-------------|------|------------------|-------------|--------|-------|
|
||||
| 1 | [#3583](https://github.com/open-gsd/gsd-core/issues/3583) | Claude skill install leaves `/gsd:<cmd>` in `SKILL.md` body | Installer / Command namespace | PR 3629 (our branch) + competing 3586 | Small (1 file + test) | PR opened / Review | **Leading PR: 3629** (cristianuibar) — reviewed + hardened with CodeRabbit feedback (left-boundary regex + body-scoped guard). Competing PR 3586 has "needs changes" + "ci: failing". Issue still carries `confirmed-bug`. |
|
||||
| 2 | [#3579](https://github.com/open-gsd/gsd-core/issues/3579) | `build-hooks.js` + npm publish omit graphify auto-update hook | Packaging / Build | `/gsd-quick` | Small | Not started | Classic "new feature missed in release artifact". Easy local verification. |
|
||||
| 3 | [#3496](https://github.com/open-gsd/gsd-core/issues/3496) | `/gsd:update` changelog extraction skips intermediate versions | Workflow / Update logic | `/gsd-quick` or lightweight plan | Medium-small | Not started | Needs deterministic version-range helper. |
|
||||
| 4 | [#3588](https://github.com/open-gsd/gsd-core/issues/3588) | Production `npm audit` has 1 high + 5 moderate advisories | Security / Dependencies | Direct + careful review | Medium | Not started | Transitive via `@anthropic-ai/claude-agent-sdk`. May need overrides. |
|
||||
| 5 | [#3584](https://github.com/open-gsd/gsd-core/issues/3584) | Runtime `bin/lib/*.cjs` still emit `/gsd:<cmd>` (larger piece deferred from #3583) | Runtime output / Slash formatter | Short plan first, then execute | Medium-Large | Not started | 16+ files. Design a centralized runtime-aware formatter. Do after #3583. |
|
||||
| 6 | [#3340](https://github.com/open-gsd/gsd-core/issues/3340) | SDK publish lag — agent dir fix never shipped in `@opengsd/gsd-sdk@0.1.0` | Release / SDK publishing | Plan + coordination | Medium (release-focused) | Not started | Oldest. Mostly a publishing/versioning task. |
|
||||
|
||||
## Execution Rules for This Batch
|
||||
|
||||
- **Branch naming**: `fix/NNNN-short-description` (enforced by CI)
|
||||
- **PR template**: Must use `.github/PULL_REQUEST_TEMPLATE/fix.md`
|
||||
- **Linking**: `Fixes #NNNN` (or `Closes`) in the PR body
|
||||
- **Changeset**: Required for all user-facing or security fixes
|
||||
- **Testing**: All existing tests must pass + new coverage where the issue describes a gap
|
||||
- **Clean context windows**: Each fix should preferably be driven from a fresh session using the prepared prompts (see session notes or ask for them)
|
||||
- **GSD self-use**: For the small ones (#3583, #3579, #3496), using `/gsd-quick` (or `/gsd-fast`) inside the fix session is encouraged and appropriate. For #3584, a short planning step is recommended.
|
||||
|
||||
## Status Legend
|
||||
|
||||
- **Not started** — Issue claimed for this batch, no work begun
|
||||
- **In progress** — Active work in a clean window
|
||||
- **PR opened** — Pull request created and linked
|
||||
- **Review** — Awaiting review / CI / merge fixes
|
||||
- **Merged** — Landed on main
|
||||
- **Blocked** — Needs input from maintainers or upstream
|
||||
|
||||
## Current Status
|
||||
|
||||
- [x] #3583 — **PR opened** (3629 leading after CodeRabbit review + hardening push; competing 3586 needs changes + CI failing)
|
||||
- [ ] #3579 — Not started (cleanest next target — 0 PRs)
|
||||
- [ ] #3496 — PR 3497 open (changes requested)
|
||||
- [ ] #3588 — Not started
|
||||
- [ ] #3584 — Not started (larger; deferred runtime cjs colon emissions)
|
||||
- [ ] #3340 — Not started
|
||||
|
||||
**Progress**: 0 / 6 merged (1 in active review)
|
||||
|
||||
## Process Notes
|
||||
|
||||
- These issues were identified during a `/gsd-inbox` run on 2026-05-16.
|
||||
- At the time of creation of this file, zero of the six had open PRs.
|
||||
- 2026-05-16 Grok session: Reviewed PR 3629 (our #3583 fix) for CodeRabbit comments. 1 critical was false-positive (scripts/ *is* published per package.json "files" + npm pack). Applied the 2 valid suggestions (bidirectional word-boundary lookbehind in `buildColonPattern` + body-only scope for the colon-ref regression guard in the test). Tests pass. Pushed hardening commit to the fork branch. Competing PR 3586 exists but is behind on CI/review status.
|
||||
- Work is intended to be done in **parallel clean context windows** (one issue per fresh Claude/Codex/Gemini session) using dedicated prompts.
|
||||
- After each fix is complete in its window, the resulting branch + PR description should be brought back here for final review and opening.
|
||||
- This file serves as the single source of truth for the current batch while execution is in progress. It can be deleted or moved to `docs/archive/` once all six PRs are merged.
|
||||
|
||||
## Related Artifacts
|
||||
|
||||
- Inbox triage report: `/tmp/GSD-INBOX-TRIAGE-2026-05-16.md` (from the `/gsd-inbox` run)
|
||||
- Full issue list with `confirmed-bug` label: `gh issue list --state open --label confirmed-bug`
|
||||
|
||||
---
|
||||
|
||||
**Next action**: #3583 now has active PR(s) under review. Next clean quick win (0 PRs, small packaging effort, high value for recently-landed graphify feature): **#3579**. Validated via GitHub search: no PRs mention 3579. Ready for `/gsd-quick` or direct fix (update `scripts/build-hooks.js` HOOKS_TO_COPY + ensure `hooks/lib/` copy in installer + fix any publish filter).
|
||||
|
||||
This document will be updated as status changes.
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-advisor-researcher
|
||||
description: Researches a single gray area decision and returns a structured comparison table with rationale. Spawned by discuss-phase advisor mode.
|
||||
tools: Read, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*
|
||||
tools: Read, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*
|
||||
color: cyan
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-ai-researcher
|
||||
description: Researches a chosen AI framework's official docs to produce implementation-ready guidance — best practices, syntax, core patterns, and pitfalls distilled for the specific use case. Writes the Framework Quick Reference and Implementation Guidance sections of AI-SPEC.md. Spawned by /gsd:ai-integration-phase orchestrator.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, WebFetch, WebSearch, mcp__context7__*
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, WebFetch, WebSearch, mcp__context7__*, mcp__plugin_context7_context7__*
|
||||
color: green
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-domain-researcher
|
||||
description: Researches the business domain and real-world application context of the AI system being built. Surfaces domain expert evaluation criteria, industry-specific failure modes, regulatory context, and what "good" looks like for practitioners in this field — before the eval-planner turns it into measurable rubrics. Spawned by /gsd:ai-integration-phase orchestrator.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*
|
||||
color: purple
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-executor
|
||||
description: Executes GSD plans with atomic commits, deviation handling, checkpoint protocols, and state management. Spawned by execute-phase orchestrator or execute-plan command.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, Skill, mcp__context7__*
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, Skill, mcp__context7__*, mcp__plugin_context7_context7__*
|
||||
color: yellow
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
@@ -24,7 +24,7 @@ Your job: Execute the plan completely, commit each task, create SUMMARY.md, upda
|
||||
<documentation_lookup>
|
||||
When you need library or framework documentation, check in this order:
|
||||
|
||||
1. If Context7 MCP tools (`mcp__context7__*`) are available in your environment, use them:
|
||||
1. If Context7 MCP tools (`mcp__context7__*, mcp__plugin_context7_context7__*`) are available in your environment, use them:
|
||||
- Resolve library ID: `mcp__context7__resolve-library-id` with `libraryName`
|
||||
- Fetch docs: `mcp__context7__get-library-docs` with `context7CompatibleLibraryId` and `topic`
|
||||
|
||||
@@ -691,7 +691,7 @@ Do NOT skip. Do NOT proceed to state updates if self-check fails.
|
||||
</self_check>
|
||||
|
||||
<state_updates>
|
||||
After SUMMARY.md, update STATE.md using `gsd-tools query` state handlers (named flags; see `sdk/src/query/QUERY-HANDLERS.md`):
|
||||
After SUMMARY.md, update STATE.md using `gsd-tools query` state handlers (named flags):
|
||||
|
||||
```bash
|
||||
# Advance plan counter (handles edge cases automatically)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-phase-researcher
|
||||
description: Researches how to implement a phase before planning. Produces RESEARCH.md consumed by gsd-planner. Spawned by /gsd:plan-phase orchestrator.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*
|
||||
color: cyan
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-planner
|
||||
description: Creates executable phase plans with task breakdown, dependency analysis, and goal-backward verification. Spawned by /gsd:plan-phase orchestrator.
|
||||
tools: Read, Write, Edit, Bash, Glob, Grep, Skill, WebFetch, mcp__context7__*
|
||||
tools: Read, Write, Edit, Bash, Glob, Grep, Skill, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*
|
||||
color: green
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-project-researcher
|
||||
description: Researches domain ecosystem before roadmap creation. Produces files in .planning/research/ consumed during roadmap creation. Spawned by /gsd:new-project or /gsd:new-milestone orchestrators.
|
||||
tools: Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*
|
||||
tools: Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*
|
||||
color: cyan
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-ui-researcher
|
||||
description: Produces UI-SPEC.md design contract for frontend phases. Reads upstream artifacts, detects design system state, asks only unanswered questions. Spawned by /gsd:ui-phase orchestrator.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*
|
||||
color: purple
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "ai-integration",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "AI design contract",
|
||||
"description": "AI-SPEC design contract workflow for phases that build AI systems; owns the AI integration command, agents, and workflow.ai_integration_phase activation key.",
|
||||
"tier": "full",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "antigravity",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Antigravity",
|
||||
"description": "Google Antigravity IDE — nested under ~/.gemini/antigravity; probed across 1.x and 2.x layouts; Gemini hook event dialect; flat skill layout; tier-1 support.",
|
||||
"tier": "core",
|
||||
@@ -35,6 +35,14 @@
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToAntigravitySkill"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeAgentToAntigravityAgent"
|
||||
}
|
||||
],
|
||||
"local": [
|
||||
@@ -45,6 +53,14 @@
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToAntigravitySkill"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeAgentToAntigravityAgent"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "assumption-delta",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Assumption-delta architecture checkpoint",
|
||||
"description": "Rarely-firing advisory checkpoint that triggers when a phase makes something plural, optional, or chosen that used to be singular, required, or derived. Surfaces one identity-model question (promote the new general representation to primary, or add it alongside?) so a silent primary-key drift does not accumulate into a later user-facing bug. Non-blocking; fires only on a detected signal.",
|
||||
"tier": "full",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "audit",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Audit",
|
||||
"description": "Open-artifact audit and UAT-gap audit for milestone close gates; exposes `gsd-tools audit-uat` (cross-phase UAT outstanding items) and `gsd-tools audit-open` (structured open-artifact scan across debug, tasks, threads, todos, seeds, UAT, verification, context-questions).",
|
||||
"tier": "full",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "augment",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Augment Code",
|
||||
"description": "Augment Code CLI — commands + nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
|
||||
"tier": "core",
|
||||
|
||||
85
capabilities/claude-orchestration/capability.json
Normal file
85
capabilities/claude-orchestration/capability.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"id": "claude-orchestration",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Claude orchestration (Workflow backend)",
|
||||
"description": "Default-off, BETA, claude-only capability that adopts Claude Code's Workflow tool (the engine behind /effort ultracode) as an optional parallel-execution backend for the GSD loop. When the runtime exposes the Workflow tool and claude_orchestration.execution_backend resolves to 'workflow', execute-phase emits a generated Workflow script (waves -> parallel() barriers, plans -> agent({ agentType: 'gsd-executor', isolation: 'worktree' }), files_modified overlap -> separate sequential stages, resumeFromRunId wired to the phase run id, shared token budget) that composes the SAME gsd-executor agent and worktree isolation the inline path uses, restoring the wave parallelism the #853 backgrounded-agent nesting limitation forces inline on Claude Code. (The plan-checker and verifier remain inline until separately wired — this capability delivers the parallel-execution backend, not those gates.) Also folds the ultraplan plan-offload under one runtime gate (plan:* surface). On any runtime lacking the Workflow tool, or when the capability is disabled, behaviour is byte-identical to today (inline/manual dispatch). Detection + emission live in gsd-core/bin/lib/claude-orchestration.cjs (pure, fail-closed). Mirrors the existing gsd-ultraplan-phase BETA-isolation posture.",
|
||||
"tier": "full",
|
||||
"requires": [],
|
||||
"engines": {
|
||||
"gsd": ">=1.7.0"
|
||||
},
|
||||
"runtimeCompat": {
|
||||
"supported": [
|
||||
"claude"
|
||||
],
|
||||
"unsupported": []
|
||||
},
|
||||
"skills": [],
|
||||
"agents": [],
|
||||
"hooks": [],
|
||||
"commands": [
|
||||
{
|
||||
"family": "claude-orchestration",
|
||||
"module": "claude-orchestration-command-router.cjs",
|
||||
"router": "routeClaudeOrchestrationCommand",
|
||||
"subcommands": [
|
||||
"detect-backend",
|
||||
"emit-workflow"
|
||||
]
|
||||
}
|
||||
],
|
||||
"activationKey": "claude_orchestration.enabled",
|
||||
"config": {
|
||||
"claude_orchestration.enabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Master toggle for the Claude orchestration capability. Default-off + BETA: the Workflow-tool execution backend and the ultraplan plan-offload surface are inert unless this is true. When false, loop behaviour is byte-identical to a non-Claude runtime (inline/manual dispatch)."
|
||||
},
|
||||
"claude_orchestration.execution_backend": {
|
||||
"type": "enum",
|
||||
"values": [
|
||||
"auto",
|
||||
"workflow",
|
||||
"inline"
|
||||
],
|
||||
"default": "auto",
|
||||
"description": "Which execute-phase dispatch backend to use when the capability is enabled. 'auto' (default) activates the Workflow backend only when the runtime is Claude AND the Workflow tool is detected AND the Agent SDK meets claude_orchestration.min_agent_sdk_version; otherwise it falls back to inline. 'workflow' forces the Workflow backend when the tool is present AND the Agent SDK meets the floor (still fails closed to inline if the tool is absent or the SDK is too old — the floor applies in both modes). 'inline' forces today's manual one-agent-per-message dispatch regardless of tool availability."
|
||||
},
|
||||
"claude_orchestration.min_agent_sdk_version": {
|
||||
"type": "string",
|
||||
"default": "0.3.149",
|
||||
"description": "Minimum Agent SDK version required to activate the Workflow backend under execution_backend='auto'. Defaults to 0.3.149 (the release that introduced the Workflow tool). Raise to pin a higher floor; the detection seam fails closed to inline for any runtime reporting an older or unknown version."
|
||||
}
|
||||
},
|
||||
"steps": [],
|
||||
"contributions": [
|
||||
{
|
||||
"point": "execute:wave:post",
|
||||
"into": "executor",
|
||||
"fragment": {
|
||||
"path": "fragments/execute-wave-post.md"
|
||||
},
|
||||
"produces": [],
|
||||
"consumes": [
|
||||
"PLAN.md"
|
||||
],
|
||||
"when": "claude_orchestration.enabled",
|
||||
"onError": "skip"
|
||||
},
|
||||
{
|
||||
"point": "plan:post",
|
||||
"into": "planner",
|
||||
"fragment": {
|
||||
"path": "fragments/plan-post.md"
|
||||
},
|
||||
"produces": [],
|
||||
"consumes": [
|
||||
"CONTEXT.md"
|
||||
],
|
||||
"when": "claude_orchestration.enabled",
|
||||
"onError": "skip"
|
||||
}
|
||||
],
|
||||
"gates": []
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
# Claude orchestration — Workflow execution backend (BETA)
|
||||
|
||||
> Injected at `execute:wave:post` `into: executor` only when
|
||||
> `claude_orchestration.enabled` is true. Default-off; `onError: skip`.
|
||||
|
||||
## When this contribution is active
|
||||
|
||||
The Claude orchestration capability is **default-off and BETA**. It activates only
|
||||
when ALL of the following hold:
|
||||
|
||||
1. `claude_orchestration.enabled` is `true` in `.planning/config.json`, AND
|
||||
2. the active runtime is **Claude Code** (the Workflow tool is Claude / Agent
|
||||
SDK-specific), AND
|
||||
3. `claude_orchestration.execution_backend` resolves to `workflow` — either
|
||||
explicitly, or via `auto` — **and** the Agent SDK version is
|
||||
`>= claude_orchestration.min_agent_sdk_version` (default `0.3.149`). The SDK
|
||||
floor applies in both `auto` and `workflow` modes (fail-closed: a pre-release
|
||||
or older SDK never activates the preview backend).
|
||||
|
||||
Detection is fail-closed: any miss degrades to **inline, manual, one-agent-per-
|
||||
message dispatch** — exactly today's behaviour. On a non-Claude runtime this
|
||||
contribution is a no-op.
|
||||
|
||||
## What the executor does when the Workflow backend is active
|
||||
|
||||
Instead of the orchestrator fanning out one `Agent(subagent_type=gsd-executor,
|
||||
isolation=worktree, run_in_background=true)` per message (which on Claude Code
|
||||
cannot nest further subagents — #853 — and so degrades to sequential inline
|
||||
execution), execute-phase **emits a generated Workflow script** and lets the main
|
||||
loop orchestrate it:
|
||||
|
||||
- **waves → one or more sequential `parallel()` barriers** — each wave is a
|
||||
barrier group; when plans within a wave share `files_modified`, they are split
|
||||
into separate sequential stages within that wave's barrier (the next wave
|
||||
still waits for the previous wave to complete).
|
||||
- **plans → `agent(brief, { agentType: 'gsd-executor', isolation: 'worktree' })`**
|
||||
— the SAME executor agent and worktree isolation the inline path uses, so the
|
||||
produced `SUMMARY.md` and commits are identical.
|
||||
- **`files_modified` overlap → separate sequential stages** — two plans that
|
||||
touch the same file are placed in different stages within the wave (the same
|
||||
overlap rule execute-phase already applies inline).
|
||||
- **`resumeFromRunId`** — wired to the phase run id, so an interrupted phase
|
||||
resumes without re-running completed plans.
|
||||
- **`budget(tokens)`** — a shared token pool across the whole phase when the
|
||||
orchestrator passes a `budgetTokens` value to `emitWorkflowScript` (it is a
|
||||
function parameter, not a config key; the orchestrator decides the budget).
|
||||
|
||||
The emitter is a pure function exposed through the capability command surface:
|
||||
`gsd-tools claude-orchestration emit-workflow --waves <manifest.json> --run-id <id>
|
||||
[--phase-dir <dir>] [--budget <n>]` (or `require('gsd-core/bin/lib/claude-orchestration.cjs').emitWorkflowScript`
|
||||
directly). It maps the phase's wave/plan manifest to the Workflow script string
|
||||
and never invokes the Workflow tool itself; the orchestrator runs the emitted
|
||||
script. Detection is resolved by the orchestrator calling the pure
|
||||
`detectWorkflowBackend` with the LIVE host descriptor (the CLI
|
||||
`gsd-tools claude-orchestration detect-backend` is a simulation harness that
|
||||
assumes a capable host unless `--no-nested-dispatch` is passed — it does not probe
|
||||
the real runtime; the orchestrator supplies the real descriptor).
|
||||
|
||||
## Fallback contract
|
||||
|
||||
If detection resolves to `inline` (tool absent, SDK too old, runtime not Claude,
|
||||
or the capability disabled), execute-phase MUST proceed with the standard inline
|
||||
wave dispatch. The executor MUST NOT assume parallelism, a shared budget, or
|
||||
resume-from-run-id semantics in that mode.
|
||||
28
capabilities/claude-orchestration/fragments/plan-post.md
Normal file
28
capabilities/claude-orchestration/fragments/plan-post.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Claude orchestration — ultraplan plan-offload ownership (BETA)
|
||||
|
||||
> Injected at `plan:post` `into: planner` only when
|
||||
> `claude_orchestration.enabled` is true. Default-off; `onError: skip`.
|
||||
|
||||
## Ownership declaration
|
||||
|
||||
The `gsd-ultraplan-phase` plan-offload surface (offloading GSD's plan phase to
|
||||
Claude Code's ultraplan cloud) is **owned by this capability**, not by a
|
||||
standalone BETA skill. Both surfaces share one runtime gate
|
||||
(`claude_orchestration.enabled`), one BETA boundary, and one Claude-Code-only
|
||||
detection seam.
|
||||
|
||||
## When the planner should consider ultraplan offload
|
||||
|
||||
When this contribution is active (capability enabled, Claude Code runtime), the
|
||||
planner MAY offer the `/gsd-ultraplan-phase` path as an alternative to local
|
||||
`/gsd-plan-phase` for phases where cloud-assisted planning adds value. This is
|
||||
advisory, not mandatory — the stable local planner remains the default.
|
||||
|
||||
## Fallback contract
|
||||
|
||||
If the capability is disabled, or the runtime is not Claude Code, ultraplan
|
||||
offload is **not surfaced** and the planner proceeds with the standard local
|
||||
`/gsd-plan-phase`. The `gsd-ultraplan-phase` command itself remains installed
|
||||
(its own runtime gate already no-ops on non-Claude runtimes); this contribution
|
||||
only governs whether the capability manifest advertises it as part of the
|
||||
orchestration surface.
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "claude",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Claude Code",
|
||||
"description": "Anthropic Claude Code — primary development runtime; tier-1 support with full hook surface and skills-based global install.",
|
||||
"tier": "core",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "cline",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Cline",
|
||||
"description": "Cline (VS Code extension) — global-only nested-skill layout; cline-rules hook surface (.clinerules); no hook events emitted; tier-2 support.",
|
||||
"tier": "core",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "code-review",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Code review",
|
||||
"description": "Source-file code review and review-fix workflow support for completed execution work.",
|
||||
"tier": "full",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "codebuddy",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "CodeBuddy",
|
||||
"description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
|
||||
"tier": "core",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "codex",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "OpenAI Codex CLI",
|
||||
"description": "OpenAI Codex CLI — shell-var command style; per-agent sandbox tiers; config.toml + hooks.json hook surface; tier-1 support.",
|
||||
"tier": "core",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "copilot",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "GitHub Copilot",
|
||||
"description": "GitHub Copilot (VS Code) — markdown config format; copilot-inline hook surface; no hook events emitted; flat skill nesting (unconfirmed recursive loader); tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -29,6 +29,14 @@
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCopilotSkill"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeAgentToCopilotAgent"
|
||||
}
|
||||
],
|
||||
"local": [
|
||||
@@ -39,6 +47,14 @@
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCopilotSkill"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeAgentToCopilotAgent"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "cursor",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Cursor",
|
||||
"description": "Cursor IDE — skills + converted commands artifact layout; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.",
|
||||
"tier": "core",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "drift",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Drift detection gates",
|
||||
"description": "Drift detection gates for the planning loop. At execute:wave:post: a blocking schema drift gate (detects schema files changed without a database push) and a non-blocking codebase drift gate (detects structural additions not reflected in STRUCTURE.md). At plan:pre: a non-blocking, warn-only codebase drift gate (gated on workflow.plan_drift_precheck) that flags a stale codebase map before planning, so plans are authored against a fresh STRUCTURE.md instead of discovering drift mid-execution.",
|
||||
"tier": "full",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "external-job",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Async external-job scheduler adapter",
|
||||
"description": "Default-off producer of the async external-job manifest (#1164). At execute:wave:post an executor can externalize long-running compute (SLURM first, scheduler-pluggable), commit a .planning/async-jobs/<job>.json manifest, defer SUMMARY.md, and return external_job_waiting. The core loop (#1165) consumes the manifest; this capability is the only thing that writes it. NOTE on contribution point: #1164 specifies execute:wave:pre, but execute-phase.md only dispatches execute:wave:post today (wave:pre is declared in the loop host contract but not rendered); wiring wave:pre dispatch is a core-loop change #1164 explicitly puts out of scope, so this capability registers at wave:post and the executor honors the runtime_budget classification guidance before running any tagged task. The adapter (scripts/slurm-adapter.cjs) reads external_job.submit_timeout_ms / poll_timeout_ms / artifact_dir through the canonical capability-config seam (env override > config > registry default).",
|
||||
"tier": "full",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "gap-analysis",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Post-planning gap analysis",
|
||||
"description": "Proactive, non-blocking post-planning coverage report. After all PLAN.md files are generated, cross-references every REQ-ID and D-ID from REQUIREMENTS.md and CONTEXT.md against plan bodies. Emits a Source | Item | Status table. Does not block phase advancement.",
|
||||
"tier": "standard",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "graphify",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Knowledge graph",
|
||||
"description": "Build, query, and inspect the project knowledge graph in `.planning/graphs/`; exposes graphify CLI subcommands (build, query, status, diff) and the /gsd-graphify skill.",
|
||||
"tier": "full",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "hermes",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Hermes Agent",
|
||||
"description": "Hermes Agent (NousResearch) — skills nest under skills/gsd/ category bucket; nested skill layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
|
||||
"tier": "core",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "intel",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Codebase intelligence",
|
||||
"description": "Code-intelligence store for codebase querying, diff, snapshot, and API-surface extraction; exposes `gsd-tools intel` subcommands (query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface) and backs `/gsd-map-codebase` and `gsd-intel-updater`.",
|
||||
"tier": "full",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "kilo",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Kilo Code",
|
||||
"description": "Kilo Code — XDG-based config dir; global skills at ~/.kilo/skills (separate from XDG config); flat command/ + skills artifact layout; no lifecycle hook registration; tier-2 support.",
|
||||
"tier": "core",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "kimi",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Kimi CLI",
|
||||
"description": "Kimi CLI (Moonshot AI) — generic agents root at ~/.config/agents; skills + kimi-agents artifact layout; no hook surface; no hook events; tier-2 support.",
|
||||
"tier": "core",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "mempalace",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "MemPalace memory",
|
||||
"description": "Cross-session, cross-project memory: deliberate recall before discuss/plan and verbatim capture + temporal-KG sync at phase boundaries, via the MemPalace MCP server and CLI.",
|
||||
"tier": "full",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "nyquist",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Nyquist validation",
|
||||
"description": "Validation coverage audit that maps executed work back to tests and manual-only evidence.",
|
||||
"tier": "full",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "opencode",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "OpenCode",
|
||||
"description": "OpenCode — XDG-based config dir; flat command/ + skills artifact layout; settings-json config format; no lifecycle hook registration; tier-2 support.",
|
||||
"tier": "core",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "pattern-mapper",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Pattern mapping",
|
||||
"description": "Optional codebase-pattern mapping before planning; owns the pattern mapper agent and workflow.pattern_mapper activation key.",
|
||||
"tier": "full",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "profile-pipeline",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Developer profiling pipeline",
|
||||
"description": "Developer behavioral profiling from Claude Code session history; scans session JSONL files, extracts and samples user messages, and generates profile artifacts (USER-PROFILE.md, dev-preferences.md, CLAUDE.md sections). Exposes eight `gsd-tools` commands: scan-sessions, extract-messages, profile-sample (pipeline phase) and write-profile, profile-questionnaire, generate-dev-preferences, generate-claude-profile, generate-claude-md (output phase). Backs the /gsd-profile-user skill and gsd-user-profiler agent.",
|
||||
"tier": "full",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "qwen",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Qwen Code",
|
||||
"description": "Qwen Code (Alibaba) — nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
|
||||
"tier": "core",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "research",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Phase research",
|
||||
"description": "Optional phase research before planning; owns the phase researcher agent and workflow.research activation key.",
|
||||
"tier": "standard",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "schema-gate",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Schema push detection gate",
|
||||
"description": "Detects ORM schema-relevant files in the phase scope during planning and injects a mandatory [BLOCKING] schema push task into the plan. Prevents false-positive verification where build/types pass because TypeScript types come from config, not the live database.",
|
||||
"tier": "full",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "security",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Security enforcement",
|
||||
"description": "Threat mitigation verification and ship-time security blocking for phases with security enforcement enabled.",
|
||||
"tier": "full",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "tdd",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Test-driven development",
|
||||
"description": "Injects TDD heuristics into the planner and enforces RED/GREEN gate compliance on type:tdd plans after execution. Owns workflow.tdd_mode; the --tdd CLI flag is the ephemeral override.",
|
||||
"tier": "full",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "trae",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Trae IDE",
|
||||
"description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.",
|
||||
"tier": "core",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "ui",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "UI design contracts",
|
||||
"description": "UI-SPEC design contract + retrospective UI audit for frontend phases.",
|
||||
"tier": "full",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "windsurf",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Windsurf",
|
||||
"description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.",
|
||||
"tier": "core",
|
||||
|
||||
102
capabilities/zcode/capability.json
Normal file
102
capabilities/zcode/capability.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"id": "zcode",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "ZCode",
|
||||
"description": "ZCode (Z.ai) — desktop Agentic Development Environment for GLM-5.2; Claude-shaped nested skills at ~/.zcode/skills/<name>/SKILL.md, slash commands, named subagents, native MCP; declarative plugin surface; profile-marker install; tier-2 community support.",
|
||||
"tier": "core",
|
||||
"requires": [],
|
||||
"engines": {
|
||||
"gsd": ">=1.6.0"
|
||||
},
|
||||
"runtime": {
|
||||
"configHome": {
|
||||
"kind": "dot-home",
|
||||
"name": ".zcode",
|
||||
"env": [
|
||||
"ZCODE_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".zcode",
|
||||
"configFormat": "none",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
},
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
}
|
||||
],
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
},
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "none",
|
||||
"sandboxTier": "none",
|
||||
"supportTier": 2,
|
||||
"installSurface": "profile-marker-only",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": false,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "electron"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,6 +309,8 @@
|
||||
"capability-writer.cjs",
|
||||
"check-command-router.cjs",
|
||||
"cjs-command-router-adapter.cjs",
|
||||
"claude-orchestration-command-router.cjs",
|
||||
"claude-orchestration.cjs",
|
||||
"cli-exit.cjs",
|
||||
"cli-skew-check.cjs",
|
||||
"clock.cjs",
|
||||
|
||||
@@ -86,3 +86,39 @@ These existing multi-model features (`execute-phase` `cross_ai_delegation`, the
|
||||
- **Neutral:** no effect on non-Claude runtimes by construction; no behavior change until explicitly enabled.
|
||||
|
||||
> **Governance note:** This ADR is a *draft design* accompanying feature request #1143. Per CONTRIBUTING, it is PR'd only after the issue receives `approved-feature`, and the capability is implemented only after #857 is released.
|
||||
|
||||
## Amendment (2026-07-06): BETA v1 implementation landed
|
||||
|
||||
#857 is **released** (CLOSED); the capability infrastructure is live. The BETA v1
|
||||
of this capability has shipped as `capabilities/claude-orchestration/` with the
|
||||
scope agreed in the Decision, refined to the lowest-risk first slice:
|
||||
|
||||
- **Detection + emission** live as pure, fail-closed functions in
|
||||
`gsd-core/bin/lib/claude-orchestration.cjs` (source `src/claude-orchestration.cts`):
|
||||
`detectWorkflowBackend` (gate ladder: enabled → Claude runtime →
|
||||
execution_backend ≠ inline → host dispatch nested+background → valid Agent SDK
|
||||
→ SDK ≥ `claude_orchestration.min_agent_sdk_version`, default `0.3.149`) and
|
||||
`emitWorkflowScript` (waves → `parallel()` stage barriers, plans →
|
||||
`agent({ agentType: 'gsd-executor', isolation: 'worktree' })`, `files_modified`
|
||||
overlap → separate sequential stages, `resumeFromRunId` wired to the phase run
|
||||
id, shared `budget(tokens)` pool). All interpolated values are validated as
|
||||
script-safe identifiers or JSON-quoted (review Finding 1).
|
||||
- **Loop registration** is at the two **wired** points the loop host contract
|
||||
actually renders: `execute:wave:post into:executor` (Workflow-backend guidance)
|
||||
and `plan:post into:planner` (ultraplan ownership declaration). `execute:wave:pre`
|
||||
and `execute:pre` are declared in the contract but **not wired** today, so the
|
||||
capability registers at `wave:post` (the constraint `external-job` also documents).
|
||||
- **Config** is federated (`claude_orchestration.enabled` default false /
|
||||
`activationKey`, `execution_backend` enum `auto|workflow|inline` default `auto`,
|
||||
`min_agent_sdk_version`); the keys live only in the registry, so uninstall
|
||||
removes them cleanly.
|
||||
- **ultraplan ownership** is declared in the manifest (`plan:post` contribution);
|
||||
full install-profile migration of the `gsd-ultraplan-phase` skill into the
|
||||
capability's `skills[]` is deferred to a follow-up (it triggers the CLUSTERS /
|
||||
profile membership gate and is a heavier, install-machinery change).
|
||||
|
||||
Status remains **Proposed** — the BETA is default-off and the end-to-end Workflow
|
||||
execution path (actual orchestration via the Workflow tool inside Claude Code) is
|
||||
not verifiable outside that runtime. The capability is structurally complete and
|
||||
tested at the contract level; flipping to Accepted follows maintainer sign-off on
|
||||
the E2E behaviour once exercised on Claude Code with the Workflow tool present.
|
||||
|
||||
@@ -80,6 +80,14 @@ Cross-cutting steps (a, b, c) are applied by the descriptor pipeline for the app
|
||||
5. **Codex** — fold the `.toml` sidecar into the descriptor (or declare it an explicit companion artifact); the `.md` + `.toml` must both reach parity.
|
||||
6. **Delete the inline loop** once every runtime is green; remove the now-dead `isKimi`/minimal special-casing that referenced it.
|
||||
|
||||
### Cutover progress (#1575)
|
||||
|
||||
- **Step 0 (parity harness):** shipped in `tests/issue-1575-agent-descriptor-parity.test.cjs`. Asserts `applySurface` output is byte-identical to `installRuntimeArtifacts` for all descriptor-driven runtimes. Covers stale-cleanup convergence (pre-existing legacy `.agent.md` pruned correctly).
|
||||
- **Step 1 (trivial converters):** cursor, windsurf, augment, trae, codebuddy — install-path cutover complete (PR #1438); surface-path parity shipped (#1575: `applySurface` now builds `agentCtx` and passes it to `kind.stage()` for agents, applying path-rewrite + attribution + converter + normalize).
|
||||
- **Step 2 (scope-aware):** copilot and antigravity — cutover complete (#1575: declared `agents` kind in `capability.json`, added to `_DESCRIPTOR_AGENTS_RUNTIMES`, copilot `.agent.md` rename handled in both `_copyStaged` and `_syncGsdDir`).
|
||||
- **Cline:** deferred — rules-only local branch + local/global complication not handled by the descriptor-driven path.
|
||||
- **Remaining:** steps 3–6 (config-reading, no-converter, codex, inline-loop deletion).
|
||||
|
||||
## Risks / trade-offs
|
||||
|
||||
- **Silent install regression** across ~15 runtimes is the dominant risk; the byte-for-byte golden gate is the mitigation, and per-runtime sequencing bounds the blast radius of any single step.
|
||||
|
||||
94
docs/explanation/claude-orchestration-capability.md
Normal file
94
docs/explanation/claude-orchestration-capability.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Claude orchestration capability (BETA)
|
||||
|
||||
> **Explanation** — *why this capability exists and how it fits the loop.* For the
|
||||
> step-by-step, see the [capability reference](../reference/capability-matrix.md);
|
||||
> for the design record, see [ADR-1143](../adr/1143-claude-orchestration-capability.md).
|
||||
|
||||
## The problem
|
||||
|
||||
GSD's `execute-phase` is wave-based: plans carry a wave number, waves run
|
||||
sequentially, and plans *within* a wave run in parallel when their
|
||||
`files_modified` sets don't overlap. On most runtimes GSD realizes that by
|
||||
fanning out one backgrounded `gsd-executor` agent (in a worktree) per plan.
|
||||
|
||||
On **Claude Code** that fan-out degrades. Backgrounded agents on Claude Code have
|
||||
no `Agent`/`Task` tool, so they cannot nest subagents ([#853]). The autonomous
|
||||
loop therefore falls back to **inline sequential execution** — and with it
|
||||
silently drops wave parallelism, the plan-checker, and the verifier — on the one
|
||||
runtime most GSD users run.
|
||||
|
||||
Claude Code ships an orchestration primitive that sidesteps exactly this: the
|
||||
**Workflow tool** (the engine behind `/effort ultracode`, Agent SDK ≥ v0.3.149).
|
||||
A Workflow script *is* the orchestrator — it runs from the main loop and spawns
|
||||
subagents itself via `agent()`, `parallel()` (barrier), `pipeline()`, and
|
||||
`phase()`, with `isolation: 'worktree'`, a shared token `budget`, and
|
||||
`resumeFromRunId`.
|
||||
|
||||
## The capability
|
||||
|
||||
`claude-orchestration` is a **default-off, BETA, claude-only** capability that
|
||||
adopts the Workflow tool as an optional, runtime-gated parallel-execution
|
||||
backend, and folds the existing `gsd-ultraplan-phase` plan-offload under the same
|
||||
gate. It is blocked-on-nothing now that the ADR-857 capability system is released.
|
||||
|
||||
- **`role: feature`**, `runtimeCompat.supported: ["claude"]`, `tier: full`.
|
||||
- **`activationKey: claude_orchestration.enabled`** — default `false`. Nothing
|
||||
changes until you opt in.
|
||||
- Registers at two **wired** loop points: `execute:wave:post` (into the executor)
|
||||
and `plan:post` (into the planner). Both are `onError: skip` and gated by the
|
||||
`enabled` key.
|
||||
|
||||
## How it decides whether to activate
|
||||
|
||||
Detection is a pure, **fail-closed** function — `detectWorkflowBackend`. The
|
||||
Workflow backend activates only when *every* gate passes; any miss degrades to
|
||||
`inline` (today's behaviour):
|
||||
|
||||
1. `claude_orchestration.enabled` is true.
|
||||
2. The runtime is Claude (the Workflow tool is Claude / Agent SDK-specific).
|
||||
3. `claude_orchestration.execution_backend` is `auto` or `workflow` (not `inline`).
|
||||
4. The host descriptor advertises `dispatch.nested` **and** `dispatch.background`
|
||||
(the nesting-capable Claude-Code shape — a proxy for Workflow-tool presence,
|
||||
meaningful only after gate 2).
|
||||
5. The Agent SDK reports a valid semver version.
|
||||
6. That version is `>= claude_orchestration.min_agent_sdk_version`
|
||||
(default `0.3.149`). A pre-release of the floor (e.g. `0.3.149-rc.1`) compares
|
||||
*below* the GA release per SemVer, so the preview backend stays off.
|
||||
|
||||
## What the executor runs when the backend is active
|
||||
|
||||
`emitWorkflowScript` maps the phase's wave/plan model onto Workflow primitives:
|
||||
|
||||
| GSD concept | Workflow primitive |
|
||||
|---|---|
|
||||
| Wave | `parallel()` stage barrier |
|
||||
| Plan | `agent(brief, { agentType: 'gsd-executor', isolation: 'worktree' })` |
|
||||
| `files_modified` overlap | forces the plans into separate sequential stages |
|
||||
| Phase run id | `resumeFromRunId("<id>")` |
|
||||
| Phase token cap | `budget(<tokens>)` |
|
||||
|
||||
Because the emitted script composes the **same** `gsd-executor` agent and
|
||||
**worktree isolation** the inline path uses, it produces the same `SUMMARY.md`
|
||||
artifacts and commits — the only difference is the execution vehicle.
|
||||
|
||||
## The fallback contract
|
||||
|
||||
On any runtime lacking the Workflow tool — or when the capability is disabled,
|
||||
the SDK is too old, or detection fails for any reason — execute-phase proceeds
|
||||
with the standard inline wave dispatch. This is a release gate, not a nicety: a
|
||||
regression test asserts the inline fallback on every non-capable combination, so
|
||||
the capability is default-off and low-risk by construction.
|
||||
|
||||
## BETA scope (v1)
|
||||
|
||||
The first slice ships **detection + emission + declarative ultraplan ownership**.
|
||||
The emitter is exercised at the contract level (structure, overlap splitting,
|
||||
resume, budget, anti-injection). End-to-end execution through the Workflow tool
|
||||
is verifiable only inside Claude Code with the tool present. Full install-profile
|
||||
migration of the `gsd-ultraplan-phase` skill into the capability's `skills[]`
|
||||
array is a follow-up (it touches the cluster/profile machinery); for v1 the
|
||||
manifest *declares* ultraplan ownership at `plan:post` and the existing skill's
|
||||
own runtime gate continues to no-op on non-Claude runtimes.
|
||||
|
||||
[#853]: https://github.com/open-gsd/gsd-core/issues/853
|
||||
[#1143]: https://github.com/open-gsd/gsd-core/issues/1143
|
||||
171
docs/how-to/enable-claude-orchestration-workflow-backend.md
Normal file
171
docs/how-to/enable-claude-orchestration-workflow-backend.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# How to enable and use the Claude orchestration backend (BETA)
|
||||
|
||||
Run GSD's execute-phase waves through Claude Code's Workflow tool (`/effort ultracode`, Agent SDK ≥ v0.3.149) instead of the default one-agent-per-message dispatch, and fold the `gsd-ultraplan-phase` plan-offload under the same gate. On Claude Code this restores the wave parallelism that backgrounded-agent nesting (#853) otherwise forces inline.
|
||||
|
||||
> **BETA.** This capability tracks a Claude Code preview surface. It is default-off, fail-closed, and Claude-only. Every detection miss degrades silently to today's inline behaviour — enabling it can never break the loop. See the [explanation doc](../explanation/claude-orchestration-capability.md) for the why, and [ADR-1143](../adr/1143-claude-orchestration-capability.md) for the design.
|
||||
|
||||
**What you need:**
|
||||
- GSD installed with the `full` profile (the capability is `tier: full`).
|
||||
- **Claude Code** with the Workflow tool available (Agent SDK ≥ `0.3.149`). On any other runtime the capability is an explicit no-op — you can flip the switch safely, nothing happens.
|
||||
- A GSD project with at least one planned phase (you need a wave/plan manifest to emit a script for).
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Enable the capability
|
||||
|
||||
The capability ships disabled. Turn on the master switch inside your GSD project:
|
||||
|
||||
```bash
|
||||
gsd-tools query config-set claude_orchestration.enabled true
|
||||
```
|
||||
|
||||
That single key gates everything — both the Workflow-backend hook at `execute:wave:post` and the ultraplan ownership declaration at `plan:post`. All other `claude_orchestration.*` keys are optional refinements.
|
||||
|
||||
Verify it took:
|
||||
|
||||
```bash
|
||||
gsd-tools query config-get claude_orchestration.enabled
|
||||
# → true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Check whether your runtime qualifies
|
||||
|
||||
Detection is fail-closed: the Workflow backend activates only when **every** gate opens. Before relying on it, confirm your runtime reports as capable:
|
||||
|
||||
```bash
|
||||
gsd-tools claude-orchestration detect-backend \
|
||||
--runtime claude \
|
||||
--agent-sdk-version 1.2.0
|
||||
```
|
||||
|
||||
You will get one of two results:
|
||||
|
||||
| `backend` | `available` | Meaning |
|
||||
|-----------|-------------|---------|
|
||||
| `workflow` | `true` | Every gate passed — the emitter will produce a Workflow script the orchestrator can run. |
|
||||
| `inline` | `false` | A gate failed. The `reason` field tells you which: `capability_disabled`, `runtime_not_claude`, `backend_inline`, `workflow_tool_unavailable`, `agent_sdk_version_unknown`, or `agent_sdk_version_below_floor`. |
|
||||
|
||||
> **The CLI is a simulation harness, not a probe.** `detect-backend` assumes a capable host descriptor unless you pass `--no-nested-dispatch`. It exists so you (and the orchestrator) can ask "given these facts, would the backend activate?" The real detection the loop uses is the pure `detectWorkflowBackend` function, called with the live host descriptor.
|
||||
|
||||
### If detection returns `inline`
|
||||
|
||||
Work through the `reason`:
|
||||
|
||||
- **`runtime_not_claude`** — you are on Codex / Cursor / opencode / etc. The Workflow tool is Claude-specific; there is nothing to enable here. Your loop is unchanged.
|
||||
- **`agent_sdk_version_below_floor`** — upgrade Claude Code / the Agent SDK to at least `claude_orchestration.min_agent_sdk_version` (default `0.3.149`). A pre-release of the floor (e.g. `0.3.149-rc.1`) compares *below* the GA release and will not activate.
|
||||
- **`workflow_tool_unavailable`** — your host descriptor does not advertise nested + background dispatch. This is unusual on Claude Code; if you see it, the Workflow tool is not present in this session.
|
||||
- **`agent_sdk_version_unknown`** — the version could not be determined. Supply it explicitly via `--agent-sdk-version`.
|
||||
|
||||
### Pin a higher floor (optional)
|
||||
|
||||
If you want to gate the BETA behind a newer Agent SDK than the default:
|
||||
|
||||
```bash
|
||||
gsd-tools query config-set claude_orchestration.min_agent_sdk_version 1.0.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Choose the execution backend
|
||||
|
||||
`claude_orchestration.execution_backend` controls how aggressively the backend is used once detection passes:
|
||||
|
||||
| Value | Behaviour |
|
||||
|-------|-----------|
|
||||
| `auto` (default) | Use the Workflow backend **if** detection passes; otherwise inline. The safe, recommended value. |
|
||||
| `workflow` | Force the Workflow backend when the tool is present (still fails closed to inline if the tool is absent or the SDK is too old — the floor applies in both modes). |
|
||||
| `inline` | Force today's manual one-agent-per-message dispatch, even on a capable Claude Code runtime. Use this to A/B compare or to temporarily retire the BETA. |
|
||||
|
||||
Switch with:
|
||||
|
||||
```bash
|
||||
gsd-tools query config-set claude_orchestration.execution_backend workflow
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Emit a Workflow script for a phase
|
||||
|
||||
With the capability enabled and detection passing, generate the Workflow script for a phase's wave/plan manifest. The manifest is the wave/plan model execute-phase already builds:
|
||||
|
||||
```json
|
||||
{
|
||||
"waves": [
|
||||
{
|
||||
"id": "w1",
|
||||
"plans": [
|
||||
{ "id": "p1", "brief": "Implement the foo module", "files_modified": ["src/foo.cts"] },
|
||||
{ "id": "p2", "brief": "Wire the bar seam", "files_modified": ["src/bar.cts"] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Emit the script:
|
||||
|
||||
```bash
|
||||
gsd-tools claude-orchestration emit-workflow \
|
||||
--waves .planning/phases/01-foo/waves.json \
|
||||
--run-id phase-01-foo \
|
||||
--phase-dir .planning/phases/01-foo \
|
||||
--budget 500000
|
||||
```
|
||||
|
||||
The output is a generated Workflow script that maps GSD's model 1:1 onto Workflow primitives:
|
||||
|
||||
- **waves → sequential `parallel()` barriers** (split into separate stages within a wave when `files_modified` overlap),
|
||||
- **plans → `agent(brief, { agentType: "gsd-executor", isolation: "worktree" })`** — the **same** executor agent and worktree isolation the inline path uses,
|
||||
- **`resumeFromRunId("<run-id>")`** wired to the phase run id,
|
||||
- **`budget(<tokens>)`** — a shared token pool across the whole phase (omit `--budget` to skip).
|
||||
|
||||
Because the script composes the same `gsd-executor` agent + worktree isolation + `SUMMARY.md` artifact as the inline path, the artifacts and commits it produces are identical — only the execution vehicle differs.
|
||||
|
||||
### Run the emitted script
|
||||
|
||||
Feed the emitted script to Claude Code's Workflow tool (`/effort ultracode`, or an Agent SDK `Workflow` invocation). The orchestrator runs it; each `agent()` call spawns a `gsd-executor` in its own worktree, waves barrier between each other, and `resumeFromRunId` lets an interrupted phase resume without re-running completed plans.
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Ultraplan plan-offload
|
||||
|
||||
Enabling the capability also folds `gsd-ultraplan-phase` under the same runtime gate. When the capability is on, the planner may offer the `/gsd-ultraplan-phase` path (offload plan-phase to Claude Code's ultraplan cloud) as an alternative to local `/gsd-plan-phase`. This is advisory — the stable local planner remains the default.
|
||||
|
||||
If the capability is off, or the runtime is not Claude Code, ultraplan offload is not surfaced and `/gsd-plan-phase` runs as normal.
|
||||
|
||||
---
|
||||
|
||||
## Disabling
|
||||
|
||||
To turn the capability off and return to byte-identical inline behaviour:
|
||||
|
||||
```bash
|
||||
gsd-tools query config-set claude_orchestration.enabled false
|
||||
```
|
||||
|
||||
Or force inline dispatch while leaving the capability otherwise on:
|
||||
|
||||
```bash
|
||||
gsd-tools query config-set claude_orchestration.execution_backend inline
|
||||
```
|
||||
|
||||
Either step is sufficient — no uninstall or resurface needed. The federated config keys live only in the capability registry, so they vanish cleanly if the capability is ever removed.
|
||||
|
||||
---
|
||||
|
||||
## What is and is not wired in BETA v1
|
||||
|
||||
**Working today:**
|
||||
- Detection (`detectWorkflowBackend` / `gsd-tools claude-orchestration detect-backend`) — fail-closed, tested across every gate.
|
||||
- Emission (`emitWorkflowScript` / `gsd-tools claude-orchestration emit-workflow`) — waves→barriers, overlap→stages, resume, budget, anti-injection.
|
||||
- The contribution fragments at `execute:wave:post` and `plan:post` (gated, `onError: skip`).
|
||||
- Inline fallback on every non-capable combination (regression-tested).
|
||||
|
||||
**Not yet wired (follow-ups):**
|
||||
- `execute-phase.md` does not yet auto-branch to emit-and-run the Workflow script. Today you emit the script explicitly (Step 4) and run it via the Workflow tool. Automatic dispatch inside the loop is the next milestone.
|
||||
- The plan-checker and verifier still run inline — this capability delivers the parallel-execution backend, not those gates.
|
||||
- Full install-profile migration of the `gsd-ultraplan-phase` skill into the capability's `skills[]` (it is currently declared in the manifest; the skill's own runtime gate continues to no-op on non-Claude runtimes).
|
||||
|
||||
If a preview-API change breaks detection, the capability degrades to inline; it cannot destabilise the core loop.
|
||||
@@ -102,6 +102,8 @@ The `gsd-tools` binary (installed as part of the `@opengsd/gsd-core` npm package
|
||||
|
||||
Node.js (`node`) must also be available on your `PATH`. The plugin's always-on guard hooks (wired in `hooks/hooks.json`) are invoked as `node "${CLAUDE_PLUGIN_ROOT}/hooks/<script>"`. Some Claude Code distributions ship as a standalone binary and do not expose a `node` executable on `PATH`; in those environments the plugin's hooks will not run. Verify with `node --version` before relying on the plugin hooks.
|
||||
|
||||
**Runtime build (self-healing).** The runtime CLI's compiled modules under `gsd-core/bin/lib/*.cjs` are build artifacts (ADR-457): they are compiled from `src/*.cts` by `npm run build:lib` and shipped prebuilt in the npm tarball. A plugin-marketplace or git-clone install materializes the repository tree directly and never runs that build step, so those files are initially absent. The CLI heals this automatically: the first `gsd-tools` invocation detects the missing output and compiles it once (using the bundled `typescript` devDependency), then proceeds normally. You may see a one-time `gsd: runtime library not built — compiling once…` notice on stderr; subsequent commands are unaffected. If auto-build cannot run (for example `node_modules` was pruned to production-only and `typescript` is unavailable), the CLI prints an actionable message telling you to run `npm install && npm run build:lib` in the plugin directory.
|
||||
|
||||
#### Claude plugin marketplace discovery (ZCODE and compatible runtimes)
|
||||
|
||||
GSD Core also ships a `.claude-plugin/marketplace.json` marketplace manifest (sibling to `plugin.json`). Runtimes that implement the Claude plugin marketplace contract — such as ZCODE — can discover and install GSD Core from a custom marketplace source without a manual clone:
|
||||
@@ -406,6 +408,22 @@ Skills land in `~/.trae/`. GSD installs skills, agents, and rule references.
|
||||
|
||||
---
|
||||
|
||||
### ZCode
|
||||
|
||||
```bash
|
||||
npx @opengsd/gsd-core@latest --zcode --global
|
||||
```
|
||||
|
||||
[ZCode](https://zcode.z.ai/en) is Z.ai's desktop Agentic Development Environment for the GLM-5.2 model. GSD installs skills (nested `SKILL.md` bundles), slash commands, and subagents under `~/.zcode/`:
|
||||
|
||||
- **Skills** → `~/.zcode/skills/gsd-<name>/SKILL.md` (invoke with `$gsd-<name>` in chat)
|
||||
- **Commands** → `~/.zcode/commands/gsd-<name>.md` (invoke with `/gsd-<name>`)
|
||||
- **Subagents** → `~/.zcode/agents/gsd-<name>.md`
|
||||
|
||||
ZCode's skill format is identical to Claude Code's, so no runtime-specific converter is required — GSD lands as a pure declarative descriptor with no hardcoded installer branches. ZCode also natively imports skills and MCP config from `~/.claude`; if you install GSD for **both** Claude and ZCode, you may see duplicate GSD skills inside ZCode, which is expected. To connect ZCode's MCP servers to GSD's companion server, see [how to connect the GSD MCP server](connect-gsd-mcp-server.md).
|
||||
|
||||
---
|
||||
|
||||
## Local vs global install
|
||||
|
||||
All examples above use `--global`, which installs GSD once for your user account. To scope an install to a single project, replace `--global` with `--local`:
|
||||
|
||||
@@ -44,7 +44,7 @@ Core package and are stamped with the package version at release (per
|
||||
ADR-1244 D6). They are not subject to the consent or integrity-pin flow applied
|
||||
to third-party capabilities.
|
||||
|
||||
### Feature capabilities (role: feature) — 18
|
||||
### Feature capabilities (role: feature) — 19
|
||||
|
||||
Feature capabilities extend what the loop does — contributing research,
|
||||
planning, execution, verification, or ship artefacts at the loop extension
|
||||
@@ -55,6 +55,7 @@ points.
|
||||
| `ai-integration` | feature | full | `>=1.6.0` | `plan:pre` | step | first-party |
|
||||
| `assumption-delta` | feature | full | `>=1.6.0` | `plan:pre` | contribution | first-party |
|
||||
| `audit` | feature | full | `>=1.6.0` | — | — | first-party |
|
||||
| `claude-orchestration` | feature | full | `>=1.7.0` | `plan:post`, `execute:wave:post` | contribution | first-party |
|
||||
| `code-review` | feature | full | `>=1.6.0` | `execute:post` | step | first-party |
|
||||
| `drift` | feature | full | `>=1.6.0` | `plan:pre`, `execute:wave:post` | gate | first-party |
|
||||
| `external-job` | feature | full | `>=1.7.0` | `plan:post`, `execute:wave:post` | contribution | first-party |
|
||||
@@ -71,7 +72,7 @@ points.
|
||||
| `tdd` | feature | full | `>=1.6.0` | `plan:pre`, `execute:post` | contribution, gate | first-party |
|
||||
| `ui` | feature | full | `>=1.6.0` | `plan:pre`, `execute:wave:post`, `verify:post` | step, gate | first-party |
|
||||
|
||||
### Runtime capabilities (role: runtime) — 15
|
||||
### Runtime capabilities (role: runtime) — 16
|
||||
|
||||
Runtime capabilities adapt GSD to a specific AI runtime or IDE — emitting
|
||||
skills, agents, hooks configuration, and surface files for that host. They
|
||||
@@ -95,6 +96,7 @@ emission), so their extension-point and hook-kind cells are `—`.
|
||||
| `qwen` | runtime | core | `>=1.6.0` | — | — | first-party |
|
||||
| `trae` | runtime | core | `>=1.6.0` | — | — | first-party |
|
||||
| `windsurf` | runtime | core | `>=1.6.0` | — | — | first-party |
|
||||
| `zcode` | runtime | core | `>=1.6.0` | — | — | first-party |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -548,3 +548,40 @@ Sources consulted:
|
||||
Documentation gaps:
|
||||
- dispatch.subagentToolkit — docs show three built-in subagent types each with different tool subsets (coder=full, explore=read-only, plan=no shell/write); no single 'full' or 'read-only' value covers all types; maintainer should clarify the intended classification.
|
||||
- runtime — CLI core is Python; a Rust Wire implementation also exists; docs do not state a canonical plugin extension runtime.
|
||||
|
||||
---
|
||||
|
||||
## zcode
|
||||
|
||||
> ZCode (Z.ai) is a desktop Agentic Development Environment for the GLM-5.2 model, distributed as an Electron app. It exposes a Claude-Code-shaped extensibility surface (per-user `~/.zcode/skills/<name>/SKILL.md`, slash commands, named subagents, native MCP, and a plugin system). All values below are sourced verbatim from the official ZCode docs.
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | declarative | https://zcode.z.ai/en/docs/plugin | "A single plugin can bundle several capabilities. ZCode detects which components a plugin includes from its directory layout" — plugins/skills/commands/agents are config/markdown files; no in-process programmatic extension API is documented. |
|
||||
| commandSurface | slash-file | https://zcode.z.ai/en/docs/commands | "Custom commands are stored as `.md` files under `~/.zcode/commands` ... invoke the command with `/command-name`" |
|
||||
| modelMode | passive | https://zcode.z.ai/en/docs/configuration | Models are connected by provider config (Z.ai/BigModel/OpenAI-compat/Anthropic-compat base URLs + API keys in Model Settings); no programmatic model request API is documented. |
|
||||
| hookBus | host | https://zcode.z.ai/en/docs/plugin | A plugin's bundled components include a "**Hook** — Automation hooks triggered on specific events" — the host fires the events a plugin subscribes to. |
|
||||
| stateIO | filesystem | https://zcode.z.ai/en/docs/skill | "User-level skills for ZCode Agent: `~/.zcode/skills/<skill-name>/SKILL.md`" — full local filesystem (desktop app). |
|
||||
| transport | mcp | https://zcode.z.ai/en/docs/mcp-services | "MCP (Model Context Protocol) connects external capabilities ... type as `stdio` (SSE and HTTP remote servers are also supported)" — native MCP. |
|
||||
| runtime | electron | https://zcode.z.ai/en/docs/install (download path `cdn-zcode.z.ai/zcode/electron/releases/3.2.5/ZCode-3.2.5-mac-arm64.dmg`) | ZCode is shipped as an Electron desktop application; the release artifact lives under the `electron/releases` path. |
|
||||
| dispatch.namedDispatch | true | https://zcode.z.ai/en/docs/subagents | "you can let the Agent pick the subagent automatically, or reference it with `@` in the chat box" — subagents are invoked by name via the Agent tool. |
|
||||
| dispatch.nested | undocumented | searched: https://zcode.z.ai/en/docs/subagents | The docs do not state whether a subagent can itself spawn further subagents. |
|
||||
| dispatch.maxDepth | undocumented | searched: https://zcode.z.ai/en/docs/subagents | No maximum nesting depth is documented. |
|
||||
| dispatch.background | false | https://zcode.z.ai/en/docs/subagents | "**Foreground execution.** Subagents run in the foreground ... Background execution is not enabled yet." |
|
||||
| dispatch.subagentToolkit | full | https://zcode.z.ai/en/docs/subagents | "**general-purpose** is the default built-in subagent ... It has access to all tools"; custom subagents default to "All permissions by default" (inherits every tool). |
|
||||
| dispatch.backgroundDispatch | false | https://zcode.z.ai/en/docs/subagents | "Background execution is not enabled yet" — background dispatch is therefore impossible. |
|
||||
|
||||
Sources consulted:
|
||||
- https://zcode.z.ai/en/docs/skill
|
||||
- https://zcode.z.ai/en/docs/commands
|
||||
- https://zcode.z.ai/en/docs/subagents
|
||||
- https://zcode.z.ai/en/docs/mcp-services
|
||||
- https://zcode.z.ai/en/docs/plugin
|
||||
- https://zcode.z.ai/en/docs/configuration
|
||||
- https://zcode.z.ai/en/docs/install
|
||||
|
||||
Documentation gaps:
|
||||
- dispatch.nested / dispatch.maxDepth — ZCode's subagent docs do not state whether subagents can spawn further subagents or any depth bound.
|
||||
- configHome — skills/commands/agents homes are documented (`~/.zcode/skills`, `~/.zcode/commands`, `~/.zcode/agents`); the exact settings filename under `~/.zcode` (where MCP server config is stored) is not fully documented at time of writing.
|
||||
- Maintenance note — ZCode is a young, fast-moving app (observed at v3.2.x); these axes may need revision as its on-disk config layout stabilizes. Because ZCode also natively imports skills/MCP from `~/.claude`, installing GSD to BOTH `claude` and `zcode` can surface duplicated skills inside ZCode; this overlap is expected and documented.
|
||||
|
||||
|
||||
@@ -56,6 +56,8 @@ export default tseslint.config(
|
||||
'coverage/**',
|
||||
'**/*.generated.cjs',
|
||||
// ADR-457: tsc-generated runtime artifact — lint the src/*.cts source, not the emitted .cjs.
|
||||
'gsd-core/bin/lib/claude-orchestration.cjs',
|
||||
'gsd-core/bin/lib/claude-orchestration-command-router.cjs',
|
||||
'gsd-core/bin/lib/semver-compare.cjs',
|
||||
'gsd-core/bin/lib/host-integration.cjs',
|
||||
'gsd-core/bin/lib/handshake-serialized.cjs',
|
||||
|
||||
246
gsd-core/bin/ensure-runtime-build.cjs
Normal file
246
gsd-core/bin/ensure-runtime-build.cjs
Normal file
@@ -0,0 +1,246 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Self-healing runtime build (#2002).
|
||||
*
|
||||
* The GSD runtime CLI (gsd-tools.cjs) require()s ~150 compiled modules from
|
||||
* ./lib/*.cjs. Per ADR-457 ("build-at-publish") those are build artifacts:
|
||||
* compiled from src/*.cts by `npm run build:lib` (tsc -p tsconfig.build.json),
|
||||
* gitignored, and shipped prebuilt in the npm tarball via the prepack /
|
||||
* prepublishOnly lifecycle scripts.
|
||||
*
|
||||
* The Claude Code plugin-marketplace channel does NOT go through `npm publish`
|
||||
* or bin/install.js. Claude Code materializes the git tag tree into its plugin
|
||||
* cache and at most runs `npm install --ignore-scripts`, so neither `prepare`
|
||||
* nor `build:lib` ever fires. The compiled ./lib/*.cjs therefore never exist on
|
||||
* that path and every CLI command dies at module load with
|
||||
* `Cannot find module './lib/cli-exit.cjs'`.
|
||||
*
|
||||
* This module heals that: before gsd-tools.cjs require()s ./lib, if the compiled
|
||||
* output is absent it invokes tsc once — lock-guarded so the many parallel
|
||||
* gsd-tools shell-outs a workflow performs do not race the build — then lets the
|
||||
* requires proceed. On the npm path the artifacts already exist, so the common
|
||||
* case is a single fs.existsSync check and a no-op.
|
||||
*
|
||||
* Deliberately depends on nothing under ./lib (that tree is precisely what may
|
||||
* be missing) — only on Node built-ins.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
// The first module gsd-tools.cjs require()s, and a gitignored build artifact
|
||||
// (so it is absent on a raw git-tag checkout even though ~11 other bin/lib/*.cjs
|
||||
// are git-tracked). tsconfig.build.json sets noEmitOnError:true, so tsc is
|
||||
// all-or-nothing: if this file exists, the whole tree was emitted.
|
||||
const SENTINEL = 'cli-exit.cjs';
|
||||
|
||||
/** Directory holding the compiled ./lib/*.cjs, relative to this file. */
|
||||
function defaultLibDir() {
|
||||
return path.join(__dirname, 'lib');
|
||||
}
|
||||
|
||||
/**
|
||||
* Package root (holds tsconfig.build.json + node_modules). This file lives at
|
||||
* <root>/gsd-core/bin/ensure-runtime-build.cjs, so the root is two levels up —
|
||||
* true for both the dev repo layout and the marketplace plugin-cache checkout
|
||||
* (…/<version>/gsd-core/bin/ensure-runtime-build.cjs).
|
||||
*/
|
||||
function defaultPackageRoot() {
|
||||
return path.resolve(__dirname, '..', '..');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the tsc entry SCRIPT (not the .bin/tsc shim). We run it as
|
||||
* `node <tsc.js>` so behaviour is identical on POSIX and Windows and does not
|
||||
* depend on a shell or on the .cmd shim. Returns null when TypeScript is not
|
||||
* installed under packageRoot.
|
||||
*/
|
||||
function resolveTscScript(packageRoot) {
|
||||
try {
|
||||
return require.resolve('typescript/bin/tsc', { paths: [packageRoot] });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isBuilt(libDir) {
|
||||
return fs.existsSync(path.join(libDir, SENTINEL));
|
||||
}
|
||||
|
||||
/** Synchronous sleep with no dependencies, portable across platforms. */
|
||||
function sleepSync(ms) {
|
||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(0, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Raised when the runtime library is missing and cannot be auto-built. The
|
||||
* message is user-facing and actionable; callers print `.message` (not a stack).
|
||||
*/
|
||||
class RuntimeBuildError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'RuntimeBuildError';
|
||||
this.code = 'GSD_RUNTIME_BUILD_FAILED';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the compiled ./lib/*.cjs exist, building them once if absent.
|
||||
*
|
||||
* Idempotent and safe to call concurrently from many processes. When another
|
||||
* process holds the build lock, this one waits for the build to finish rather
|
||||
* than launching a competing tsc.
|
||||
*
|
||||
* @param {object} [opts]
|
||||
* @param {string} [opts.libDir] Override the compiled-output directory.
|
||||
* @param {string} [opts.packageRoot] Override the tsconfig/node_modules root.
|
||||
* @param {string|null} [opts.tscScript] Override tsc resolution (null = "absent").
|
||||
* @param {function} [opts.spawn] Override spawnSync (for tests).
|
||||
* @param {function} [opts.log] Override the progress logger.
|
||||
* @param {number} [opts.waitTimeoutMs] Max wait for a peer build (default 120s).
|
||||
* @param {number} [opts.pollMs] Poll interval while waiting (default 100ms).
|
||||
* @param {function} [opts.onPoll] Test hook invoked before each wait-poll.
|
||||
* @returns {{built: true, healed: boolean, waited?: boolean}}
|
||||
* @throws {RuntimeBuildError} when the library is absent and cannot be built.
|
||||
*/
|
||||
function ensureRuntimeBuild(opts = {}) {
|
||||
const libDir = opts.libDir || defaultLibDir();
|
||||
const packageRoot = opts.packageRoot || defaultPackageRoot();
|
||||
const spawn = opts.spawn || spawnSync;
|
||||
const log = opts.log || ((m) => process.stderr.write(m + '\n'));
|
||||
const waitTimeoutMs = opts.waitTimeoutMs != null ? opts.waitTimeoutMs : 120000;
|
||||
const pollMs = opts.pollMs != null ? opts.pollMs : 100;
|
||||
|
||||
// Fast path: already built (the npm-registry install and every subsequent run).
|
||||
if (isBuilt(libDir)) return { built: true, healed: false };
|
||||
|
||||
const tsconfig = path.join(packageRoot, 'tsconfig.build.json');
|
||||
if (!fs.existsSync(tsconfig)) {
|
||||
throw new RuntimeBuildError(
|
||||
'GSD runtime library is not built and cannot be auto-built: ' +
|
||||
`${tsconfig} not found. Run \`npm run build:lib\` in the gsd-core package.`,
|
||||
);
|
||||
}
|
||||
|
||||
const tscScript =
|
||||
opts.tscScript !== undefined ? opts.tscScript : resolveTscScript(packageRoot);
|
||||
if (!tscScript) {
|
||||
throw new RuntimeBuildError(
|
||||
`GSD runtime library is not built (missing ${path.join(libDir, SENTINEL)}) ` +
|
||||
'and TypeScript is unavailable to build it. Run ' +
|
||||
`\`npm install && npm run build:lib\` in the gsd-core package (${packageRoot}).`,
|
||||
);
|
||||
}
|
||||
|
||||
// libDir may not exist yet on a totally fresh tree; create it so the lock and
|
||||
// tsc output have a home.
|
||||
fs.mkdirSync(libDir, { recursive: true });
|
||||
const lockDir = path.join(libDir, '.build.lock');
|
||||
|
||||
let haveLock = acquireLock(lockDir);
|
||||
if (!haveLock) {
|
||||
// A peer process is building. Wait for the sentinel rather than racing tsc.
|
||||
const deadline = Date.now() + waitTimeoutMs;
|
||||
while (!isBuilt(libDir)) {
|
||||
if (typeof opts.onPoll === 'function') opts.onPoll();
|
||||
if (isBuilt(libDir)) break;
|
||||
if (Date.now() > deadline) break; // peer wedged/crashed — take over below
|
||||
sleepSync(pollMs);
|
||||
}
|
||||
if (isBuilt(libDir)) return { built: true, healed: true, waited: true };
|
||||
// Peer never finished; try to take over the (stale) lock.
|
||||
haveLock = acquireLock(lockDir);
|
||||
if (!haveLock && !isBuilt(libDir)) {
|
||||
throw new RuntimeBuildError(
|
||||
`GSD runtime build did not complete within ${waitTimeoutMs}ms and the ` +
|
||||
`build lock (${lockDir}) is held by another process. Remove it and run ` +
|
||||
'`npm run build:lib` if this persists.',
|
||||
);
|
||||
}
|
||||
if (isBuilt(libDir)) return { built: true, healed: true, waited: true };
|
||||
}
|
||||
|
||||
try {
|
||||
log('gsd: runtime library not built — compiling once (tsc -p tsconfig.build.json)…');
|
||||
// Heal only runs when the build is genuinely absent/broken, so force a full
|
||||
// emit: a stale incremental cache from a partial build would make tsc report
|
||||
// success without re-emitting the (still-missing) sentinel.
|
||||
forceFullEmit(packageRoot, tsconfig);
|
||||
const res = spawn(process.execPath, [tscScript, '-p', tsconfig], {
|
||||
cwd: packageRoot,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
encoding: 'utf8',
|
||||
});
|
||||
if (!res || res.status !== 0) {
|
||||
const detail = ((res && (res.stderr || res.stdout)) || '').toString().trim();
|
||||
throw new RuntimeBuildError(
|
||||
`GSD runtime build failed (tsc exit ${res ? res.status : 'unknown'}). ` +
|
||||
`Run \`npm run build:lib\` in ${packageRoot} to see the error.` +
|
||||
(detail ? `\n${detail}` : ''),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
releaseLock(lockDir);
|
||||
}
|
||||
|
||||
if (!isBuilt(libDir)) {
|
||||
throw new RuntimeBuildError(
|
||||
`GSD runtime build ran but ${path.join(libDir, SENTINEL)} is still missing. ` +
|
||||
`Run \`npm run build:lib\` in ${packageRoot}.`,
|
||||
);
|
||||
}
|
||||
return { built: true, healed: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete tsc's incremental build cache so the heal re-emits every module. The
|
||||
* cache path defaults to tsconfig.build.tsbuildinfo (per tsconfig.build.json)
|
||||
* but is read from the config when overridden. Best-effort: a missing or
|
||||
* unreadable config just falls back to the default and a missing cache is fine.
|
||||
*/
|
||||
function forceFullEmit(packageRoot, tsconfigPath) {
|
||||
let rel = 'tsconfig.build.tsbuildinfo';
|
||||
try {
|
||||
const cfg = JSON.parse(fs.readFileSync(tsconfigPath, 'utf8'));
|
||||
if (cfg && cfg.compilerOptions && cfg.compilerOptions.tsBuildInfoFile) {
|
||||
rel = cfg.compilerOptions.tsBuildInfoFile;
|
||||
}
|
||||
} catch {
|
||||
/* unreadable/JSONC config — use the default cache path */
|
||||
}
|
||||
try {
|
||||
fs.rmSync(path.resolve(packageRoot, rel), { force: true });
|
||||
} catch {
|
||||
/* best effort */
|
||||
}
|
||||
}
|
||||
|
||||
/** Atomically acquire the build lock. Returns true on success. */
|
||||
function acquireLock(lockDir) {
|
||||
try {
|
||||
fs.mkdirSync(lockDir); // atomic — throws EEXIST if a peer holds it
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e && e.code === 'EEXIST') return false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** Best-effort lock release. */
|
||||
function releaseLock(lockDir) {
|
||||
try {
|
||||
fs.rmSync(lockDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* best effort — a leftover lock is recovered by the wait-then-takeover path */
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ensureRuntimeBuild,
|
||||
resolveTscScript,
|
||||
isBuilt,
|
||||
RuntimeBuildError,
|
||||
SENTINEL,
|
||||
};
|
||||
@@ -195,6 +195,24 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// #2002 — self-healing runtime build. The compiled ./lib/*.cjs modules this
|
||||
// entrypoint require()s below are gitignored build artifacts (ADR-457), shipped
|
||||
// prebuilt in the npm tarball. The Claude Code plugin-marketplace channel never
|
||||
// runs `npm run build:lib` or bin/install.js, so on that path they can be
|
||||
// absent and every command dies at module load. Compile them once (lock-guarded,
|
||||
// idempotent, a no-op when already built) before the ./lib requires run.
|
||||
const { ensureRuntimeBuild } = require('./ensure-runtime-build.cjs');
|
||||
try {
|
||||
ensureRuntimeBuild();
|
||||
} catch (bootErr) {
|
||||
process.stderr.write((bootErr && bootErr.message ? bootErr.message : String(bootErr)) + '\n');
|
||||
// Fatal bootstrap failure before the CLI's ExitError/runMain machinery (which
|
||||
// lives in ./lib) is available to load, so a direct exit is the only option.
|
||||
// eslint-disable-next-line n/no-process-exit
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
||||
const io = require('./lib/io.cjs');
|
||||
const { error, ERROR_REASON, setJsonErrorMode, output } = io;
|
||||
|
||||
@@ -10,7 +10,7 @@ const capabilities = {
|
||||
"ai-integration": {
|
||||
"id": "ai-integration",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "AI design contract",
|
||||
"description": "AI-SPEC design contract workflow for phases that build AI systems; owns the AI integration command, agents, and workflow.ai_integration_phase activation key.",
|
||||
"tier": "full",
|
||||
@@ -63,7 +63,7 @@ const capabilities = {
|
||||
"antigravity": {
|
||||
"id": "antigravity",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Antigravity",
|
||||
"description": "Google Antigravity IDE — nested under ~/.gemini/antigravity; probed across 1.x and 2.x layouts; Gemini hook event dialect; flat skill layout; tier-1 support.",
|
||||
"tier": "core",
|
||||
@@ -97,6 +97,14 @@ const capabilities = {
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToAntigravitySkill"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeAgentToAntigravityAgent"
|
||||
}
|
||||
],
|
||||
"local": [
|
||||
@@ -107,6 +115,14 @@ const capabilities = {
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToAntigravitySkill"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeAgentToAntigravityAgent"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -141,7 +157,7 @@ const capabilities = {
|
||||
"assumption-delta": {
|
||||
"id": "assumption-delta",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Assumption-delta architecture checkpoint",
|
||||
"description": "Rarely-firing advisory checkpoint that triggers when a phase makes something plural, optional, or chosen that used to be singular, required, or derived. Surfaces one identity-model question (promote the new general representation to primary, or add it alongside?) so a silent primary-key drift does not accumulate into a later user-facing bug. Non-blocking; fires only on a detected signal.",
|
||||
"tier": "full",
|
||||
@@ -187,7 +203,7 @@ const capabilities = {
|
||||
"audit": {
|
||||
"id": "audit",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Audit",
|
||||
"description": "Open-artifact audit and UAT-gap audit for milestone close gates; exposes `gsd-tools audit-uat` (cross-phase UAT outstanding items) and `gsd-tools audit-open` (structured open-artifact scan across debug, tasks, threads, todos, seeds, UAT, verification, context-questions).",
|
||||
"tier": "full",
|
||||
@@ -224,7 +240,7 @@ const capabilities = {
|
||||
"augment": {
|
||||
"id": "augment",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Augment Code",
|
||||
"description": "Augment Code CLI — commands + nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -327,7 +343,7 @@ const capabilities = {
|
||||
"claude": {
|
||||
"id": "claude",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Claude Code",
|
||||
"description": "Anthropic Claude Code — primary development runtime; tier-1 support with full hook surface and skills-based global install.",
|
||||
"tier": "core",
|
||||
@@ -408,10 +424,97 @@ const capabilities = {
|
||||
}
|
||||
}
|
||||
},
|
||||
"claude-orchestration": {
|
||||
"id": "claude-orchestration",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Claude orchestration (Workflow backend)",
|
||||
"description": "Default-off, BETA, claude-only capability that adopts Claude Code's Workflow tool (the engine behind /effort ultracode) as an optional parallel-execution backend for the GSD loop. When the runtime exposes the Workflow tool and claude_orchestration.execution_backend resolves to 'workflow', execute-phase emits a generated Workflow script (waves -> parallel() barriers, plans -> agent({ agentType: 'gsd-executor', isolation: 'worktree' }), files_modified overlap -> separate sequential stages, resumeFromRunId wired to the phase run id, shared token budget) that composes the SAME gsd-executor agent and worktree isolation the inline path uses, restoring the wave parallelism the #853 backgrounded-agent nesting limitation forces inline on Claude Code. (The plan-checker and verifier remain inline until separately wired — this capability delivers the parallel-execution backend, not those gates.) Also folds the ultraplan plan-offload under one runtime gate (plan:* surface). On any runtime lacking the Workflow tool, or when the capability is disabled, behaviour is byte-identical to today (inline/manual dispatch). Detection + emission live in gsd-core/bin/lib/claude-orchestration.cjs (pure, fail-closed). Mirrors the existing gsd-ultraplan-phase BETA-isolation posture.",
|
||||
"tier": "full",
|
||||
"requires": [],
|
||||
"engines": {
|
||||
"gsd": ">=1.7.0"
|
||||
},
|
||||
"runtimeCompat": {
|
||||
"supported": [
|
||||
"claude"
|
||||
],
|
||||
"unsupported": []
|
||||
},
|
||||
"skills": [],
|
||||
"agents": [],
|
||||
"hooks": [],
|
||||
"commands": [
|
||||
{
|
||||
"family": "claude-orchestration",
|
||||
"module": "claude-orchestration-command-router.cjs",
|
||||
"router": "routeClaudeOrchestrationCommand",
|
||||
"subcommands": [
|
||||
"detect-backend",
|
||||
"emit-workflow"
|
||||
]
|
||||
}
|
||||
],
|
||||
"activationKey": "claude_orchestration.enabled",
|
||||
"config": {
|
||||
"claude_orchestration.enabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Master toggle for the Claude orchestration capability. Default-off + BETA: the Workflow-tool execution backend and the ultraplan plan-offload surface are inert unless this is true. When false, loop behaviour is byte-identical to a non-Claude runtime (inline/manual dispatch)."
|
||||
},
|
||||
"claude_orchestration.execution_backend": {
|
||||
"type": "enum",
|
||||
"values": [
|
||||
"auto",
|
||||
"workflow",
|
||||
"inline"
|
||||
],
|
||||
"default": "auto",
|
||||
"description": "Which execute-phase dispatch backend to use when the capability is enabled. 'auto' (default) activates the Workflow backend only when the runtime is Claude AND the Workflow tool is detected AND the Agent SDK meets claude_orchestration.min_agent_sdk_version; otherwise it falls back to inline. 'workflow' forces the Workflow backend when the tool is present AND the Agent SDK meets the floor (still fails closed to inline if the tool is absent or the SDK is too old — the floor applies in both modes). 'inline' forces today's manual one-agent-per-message dispatch regardless of tool availability."
|
||||
},
|
||||
"claude_orchestration.min_agent_sdk_version": {
|
||||
"type": "string",
|
||||
"default": "0.3.149",
|
||||
"description": "Minimum Agent SDK version required to activate the Workflow backend under execution_backend='auto'. Defaults to 0.3.149 (the release that introduced the Workflow tool). Raise to pin a higher floor; the detection seam fails closed to inline for any runtime reporting an older or unknown version."
|
||||
}
|
||||
},
|
||||
"steps": [],
|
||||
"contributions": [
|
||||
{
|
||||
"point": "execute:wave:post",
|
||||
"into": "executor",
|
||||
"fragment": {
|
||||
"path": "fragments/execute-wave-post.md",
|
||||
"inline": "# Claude orchestration — Workflow execution backend (BETA)\n\n> Injected at `execute:wave:post` `into: executor` only when\n> `claude_orchestration.enabled` is true. Default-off; `onError: skip`.\n\n## When this contribution is active\n\nThe Claude orchestration capability is **default-off and BETA**. It activates only\nwhen ALL of the following hold:\n\n1. `claude_orchestration.enabled` is `true` in `.planning/config.json`, AND\n2. the active runtime is **Claude Code** (the Workflow tool is Claude / Agent\n SDK-specific), AND\n3. `claude_orchestration.execution_backend` resolves to `workflow` — either\n explicitly, or via `auto` — **and** the Agent SDK version is\n `>= claude_orchestration.min_agent_sdk_version` (default `0.3.149`). The SDK\n floor applies in both `auto` and `workflow` modes (fail-closed: a pre-release\n or older SDK never activates the preview backend).\n\nDetection is fail-closed: any miss degrades to **inline, manual, one-agent-per-\nmessage dispatch** — exactly today's behaviour. On a non-Claude runtime this\ncontribution is a no-op.\n\n## What the executor does when the Workflow backend is active\n\nInstead of the orchestrator fanning out one `Agent(subagent_type=gsd-executor,\nisolation=worktree, run_in_background=true)` per message (which on Claude Code\ncannot nest further subagents — #853 — and so degrades to sequential inline\nexecution), execute-phase **emits a generated Workflow script** and lets the main\nloop orchestrate it:\n\n- **waves → one or more sequential `parallel()` barriers** — each wave is a\n barrier group; when plans within a wave share `files_modified`, they are split\n into separate sequential stages within that wave's barrier (the next wave\n still waits for the previous wave to complete).\n- **plans → `agent(brief, { agentType: 'gsd-executor', isolation: 'worktree' })`**\n — the SAME executor agent and worktree isolation the inline path uses, so the\n produced `SUMMARY.md` and commits are identical.\n- **`files_modified` overlap → separate sequential stages** — two plans that\n touch the same file are placed in different stages within the wave (the same\n overlap rule execute-phase already applies inline).\n- **`resumeFromRunId`** — wired to the phase run id, so an interrupted phase\n resumes without re-running completed plans.\n- **`budget(tokens)`** — a shared token pool across the whole phase when the\n orchestrator passes a `budgetTokens` value to `emitWorkflowScript` (it is a\n function parameter, not a config key; the orchestrator decides the budget).\n\nThe emitter is a pure function exposed through the capability command surface:\n`gsd-tools claude-orchestration emit-workflow --waves <manifest.json> --run-id <id>\n[--phase-dir <dir>] [--budget <n>]` (or `require('gsd-core/bin/lib/claude-orchestration.cjs').emitWorkflowScript`\ndirectly). It maps the phase's wave/plan manifest to the Workflow script string\nand never invokes the Workflow tool itself; the orchestrator runs the emitted\nscript. Detection is resolved by the orchestrator calling the pure\n`detectWorkflowBackend` with the LIVE host descriptor (the CLI\n`gsd-tools claude-orchestration detect-backend` is a simulation harness that\nassumes a capable host unless `--no-nested-dispatch` is passed — it does not probe\nthe real runtime; the orchestrator supplies the real descriptor).\n\n## Fallback contract\n\nIf detection resolves to `inline` (tool absent, SDK too old, runtime not Claude,\nor the capability disabled), execute-phase MUST proceed with the standard inline\nwave dispatch. The executor MUST NOT assume parallelism, a shared budget, or\nresume-from-run-id semantics in that mode.\n"
|
||||
},
|
||||
"produces": [],
|
||||
"consumes": [
|
||||
"PLAN.md"
|
||||
],
|
||||
"when": "claude_orchestration.enabled",
|
||||
"onError": "skip"
|
||||
},
|
||||
{
|
||||
"point": "plan:post",
|
||||
"into": "planner",
|
||||
"fragment": {
|
||||
"path": "fragments/plan-post.md",
|
||||
"inline": "# Claude orchestration — ultraplan plan-offload ownership (BETA)\n\n> Injected at `plan:post` `into: planner` only when\n> `claude_orchestration.enabled` is true. Default-off; `onError: skip`.\n\n## Ownership declaration\n\nThe `gsd-ultraplan-phase` plan-offload surface (offloading GSD's plan phase to\nClaude Code's ultraplan cloud) is **owned by this capability**, not by a\nstandalone BETA skill. Both surfaces share one runtime gate\n(`claude_orchestration.enabled`), one BETA boundary, and one Claude-Code-only\ndetection seam.\n\n## When the planner should consider ultraplan offload\n\nWhen this contribution is active (capability enabled, Claude Code runtime), the\nplanner MAY offer the `/gsd-ultraplan-phase` path as an alternative to local\n`/gsd-plan-phase` for phases where cloud-assisted planning adds value. This is\nadvisory, not mandatory — the stable local planner remains the default.\n\n## Fallback contract\n\nIf the capability is disabled, or the runtime is not Claude Code, ultraplan\noffload is **not surfaced** and the planner proceeds with the standard local\n`/gsd-plan-phase`. The `gsd-ultraplan-phase` command itself remains installed\n(its own runtime gate already no-ops on non-Claude runtimes); this contribution\nonly governs whether the capability manifest advertises it as part of the\norchestration surface.\n"
|
||||
},
|
||||
"produces": [],
|
||||
"consumes": [
|
||||
"CONTEXT.md"
|
||||
],
|
||||
"when": "claude_orchestration.enabled",
|
||||
"onError": "skip"
|
||||
}
|
||||
],
|
||||
"gates": []
|
||||
},
|
||||
"cline": {
|
||||
"id": "cline",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Cline",
|
||||
"description": "Cline (VS Code extension) — global-only nested-skill layout; cline-rules hook surface (.clinerules); no hook events emitted; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -472,7 +575,7 @@ const capabilities = {
|
||||
"code-review": {
|
||||
"id": "code-review",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Code review",
|
||||
"description": "Source-file code review and review-fix workflow support for completed execution work.",
|
||||
"tier": "full",
|
||||
@@ -533,7 +636,7 @@ const capabilities = {
|
||||
"codebuddy": {
|
||||
"id": "codebuddy",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "CodeBuddy",
|
||||
"description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -636,7 +739,7 @@ const capabilities = {
|
||||
"codex": {
|
||||
"id": "codex",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "OpenAI Codex CLI",
|
||||
"description": "OpenAI Codex CLI — shell-var command style; per-agent sandbox tiers; config.toml + hooks.json hook surface; tier-1 support.",
|
||||
"tier": "core",
|
||||
@@ -707,7 +810,7 @@ const capabilities = {
|
||||
"copilot": {
|
||||
"id": "copilot",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "GitHub Copilot",
|
||||
"description": "GitHub Copilot (VS Code) — markdown config format; copilot-inline hook surface; no hook events emitted; flat skill nesting (unconfirmed recursive loader); tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -735,6 +838,14 @@ const capabilities = {
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCopilotSkill"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeAgentToCopilotAgent"
|
||||
}
|
||||
],
|
||||
"local": [
|
||||
@@ -745,6 +856,14 @@ const capabilities = {
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCopilotSkill"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeAgentToCopilotAgent"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -778,7 +897,7 @@ const capabilities = {
|
||||
"cursor": {
|
||||
"id": "cursor",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Cursor",
|
||||
"description": "Cursor IDE — skills + converted commands artifact layout; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -881,7 +1000,7 @@ const capabilities = {
|
||||
"drift": {
|
||||
"id": "drift",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Drift detection gates",
|
||||
"description": "Drift detection gates for the planning loop. At execute:wave:post: a blocking schema drift gate (detects schema files changed without a database push) and a non-blocking codebase drift gate (detects structural additions not reflected in STRUCTURE.md). At plan:pre: a non-blocking, warn-only codebase drift gate (gated on workflow.plan_drift_precheck) that flags a stale codebase map before planning, so plans are authored against a fresh STRUCTURE.md instead of discovering drift mid-execution.",
|
||||
"tier": "full",
|
||||
@@ -959,7 +1078,7 @@ const capabilities = {
|
||||
"external-job": {
|
||||
"id": "external-job",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Async external-job scheduler adapter",
|
||||
"description": "Default-off producer of the async external-job manifest (#1164). At execute:wave:post an executor can externalize long-running compute (SLURM first, scheduler-pluggable), commit a .planning/async-jobs/<job>.json manifest, defer SUMMARY.md, and return external_job_waiting. The core loop (#1165) consumes the manifest; this capability is the only thing that writes it. NOTE on contribution point: #1164 specifies execute:wave:pre, but execute-phase.md only dispatches execute:wave:post today (wave:pre is declared in the loop host contract but not rendered); wiring wave:pre dispatch is a core-loop change #1164 explicitly puts out of scope, so this capability registers at wave:post and the executor honors the runtime_budget classification guidance before running any tagged task. The adapter (scripts/slurm-adapter.cjs) reads external_job.submit_timeout_ms / poll_timeout_ms / artifact_dir through the canonical capability-config seam (env override > config > registry default).",
|
||||
"tier": "full",
|
||||
@@ -1042,7 +1161,7 @@ const capabilities = {
|
||||
"gap-analysis": {
|
||||
"id": "gap-analysis",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Post-planning gap analysis",
|
||||
"description": "Proactive, non-blocking post-planning coverage report. After all PLAN.md files are generated, cross-references every REQ-ID and D-ID from REQUIREMENTS.md and CONTEXT.md against plan bodies. Emits a Source | Item | Status table. Does not block phase advancement.",
|
||||
"tier": "standard",
|
||||
@@ -1083,7 +1202,7 @@ const capabilities = {
|
||||
"graphify": {
|
||||
"id": "graphify",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Knowledge graph",
|
||||
"description": "Build, query, and inspect the project knowledge graph in `.planning/graphs/`; exposes graphify CLI subcommands (build, query, status, diff) and the /gsd-graphify skill.",
|
||||
"tier": "full",
|
||||
@@ -1124,7 +1243,7 @@ const capabilities = {
|
||||
"hermes": {
|
||||
"id": "hermes",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Hermes Agent",
|
||||
"description": "Hermes Agent (NousResearch) — skills nest under skills/gsd/ category bucket; nested skill layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -1195,7 +1314,7 @@ const capabilities = {
|
||||
"intel": {
|
||||
"id": "intel",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Codebase intelligence",
|
||||
"description": "Code-intelligence store for codebase querying, diff, snapshot, and API-surface extraction; exposes `gsd-tools intel` subcommands (query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface) and backs `/gsd-map-codebase` and `gsd-intel-updater`.",
|
||||
"tier": "full",
|
||||
@@ -1247,7 +1366,7 @@ const capabilities = {
|
||||
"kilo": {
|
||||
"id": "kilo",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Kilo Code",
|
||||
"description": "Kilo Code — XDG-based config dir; global skills at ~/.kilo/skills (separate from XDG config); flat command/ + skills artifact layout; no lifecycle hook registration; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -1340,7 +1459,7 @@ const capabilities = {
|
||||
"kimi": {
|
||||
"id": "kimi",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Kimi CLI",
|
||||
"description": "Kimi CLI (Moonshot AI) — generic agents root at ~/.config/agents; skills + kimi-agents artifact layout; no hook surface; no hook events; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -1414,7 +1533,7 @@ const capabilities = {
|
||||
"mempalace": {
|
||||
"id": "mempalace",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "MemPalace memory",
|
||||
"description": "Cross-session, cross-project memory: deliberate recall before discuss/plan and verbatim capture + temporal-KG sync at phase boundaries, via the MemPalace MCP server and CLI.",
|
||||
"tier": "full",
|
||||
@@ -1588,7 +1707,7 @@ const capabilities = {
|
||||
"nyquist": {
|
||||
"id": "nyquist",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Nyquist validation",
|
||||
"description": "Validation coverage audit that maps executed work back to tests and manual-only evidence.",
|
||||
"tier": "full",
|
||||
@@ -1638,7 +1757,7 @@ const capabilities = {
|
||||
"opencode": {
|
||||
"id": "opencode",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "OpenCode",
|
||||
"description": "OpenCode — XDG-based config dir; flat command/ + skills artifact layout; settings-json config format; no lifecycle hook registration; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -1727,7 +1846,7 @@ const capabilities = {
|
||||
"pattern-mapper": {
|
||||
"id": "pattern-mapper",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Pattern mapping",
|
||||
"description": "Optional codebase-pattern mapping before planning; owns the pattern mapper agent and workflow.pattern_mapper activation key.",
|
||||
"tier": "full",
|
||||
@@ -1781,7 +1900,7 @@ const capabilities = {
|
||||
"profile-pipeline": {
|
||||
"id": "profile-pipeline",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Developer profiling pipeline",
|
||||
"description": "Developer behavioral profiling from Claude Code session history; scans session JSONL files, extracts and samples user messages, and generates profile artifacts (USER-PROFILE.md, dev-preferences.md, CLAUDE.md sections). Exposes eight `gsd-tools` commands: scan-sessions, extract-messages, profile-sample (pipeline phase) and write-profile, profile-questionnaire, generate-dev-preferences, generate-claude-profile, generate-claude-md (output phase). Backs the /gsd-profile-user skill and gsd-user-profiler agent.",
|
||||
"tier": "full",
|
||||
@@ -1858,7 +1977,7 @@ const capabilities = {
|
||||
"qwen": {
|
||||
"id": "qwen",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Qwen Code",
|
||||
"description": "Qwen Code (Alibaba) — nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -1933,7 +2052,7 @@ const capabilities = {
|
||||
"research": {
|
||||
"id": "research",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Phase research",
|
||||
"description": "Optional phase research before planning; owns the phase researcher agent and workflow.research activation key.",
|
||||
"tier": "standard",
|
||||
@@ -1985,7 +2104,7 @@ const capabilities = {
|
||||
"schema-gate": {
|
||||
"id": "schema-gate",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Schema push detection gate",
|
||||
"description": "Detects ORM schema-relevant files in the phase scope during planning and injects a mandatory [BLOCKING] schema push task into the plan. Prevents false-positive verification where build/types pass because TypeScript types come from config, not the live database.",
|
||||
"tier": "full",
|
||||
@@ -2031,7 +2150,7 @@ const capabilities = {
|
||||
"security": {
|
||||
"id": "security",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Security enforcement",
|
||||
"description": "Threat mitigation verification and ship-time security blocking for phases with security enforcement enabled.",
|
||||
"tier": "full",
|
||||
@@ -2130,7 +2249,7 @@ const capabilities = {
|
||||
"tdd": {
|
||||
"id": "tdd",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Test-driven development",
|
||||
"description": "Injects TDD heuristics into the planner and enforces RED/GREEN gate compliance on type:tdd plans after execution. Owns workflow.tdd_mode; the --tdd CLI flag is the ephemeral override.",
|
||||
"tier": "full",
|
||||
@@ -2183,7 +2302,7 @@ const capabilities = {
|
||||
"trae": {
|
||||
"id": "trae",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Trae IDE",
|
||||
"description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -2269,7 +2388,7 @@ const capabilities = {
|
||||
"ui": {
|
||||
"id": "ui",
|
||||
"role": "feature",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "UI design contracts",
|
||||
"description": "UI-SPEC design contract + retrospective UI audit for frontend phases.",
|
||||
"tier": "full",
|
||||
@@ -2364,7 +2483,7 @@ const capabilities = {
|
||||
"windsurf": {
|
||||
"id": "windsurf",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Windsurf",
|
||||
"description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -2439,6 +2558,108 @@ const capabilities = {
|
||||
"runtime": "undocumented"
|
||||
}
|
||||
}
|
||||
},
|
||||
"zcode": {
|
||||
"id": "zcode",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "ZCode",
|
||||
"description": "ZCode (Z.ai) — desktop Agentic Development Environment for GLM-5.2; Claude-shaped nested skills at ~/.zcode/skills/<name>/SKILL.md, slash commands, named subagents, native MCP; declarative plugin surface; profile-marker install; tier-2 community support.",
|
||||
"tier": "core",
|
||||
"requires": [],
|
||||
"engines": {
|
||||
"gsd": ">=1.6.0"
|
||||
},
|
||||
"runtime": {
|
||||
"configHome": {
|
||||
"kind": "dot-home",
|
||||
"name": ".zcode",
|
||||
"env": [
|
||||
"ZCODE_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".zcode",
|
||||
"configFormat": "none",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
},
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
}
|
||||
],
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
},
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "none",
|
||||
"sandboxTier": "none",
|
||||
"supportTier": 2,
|
||||
"installSurface": "profile-marker-only",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": false,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "electron"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2711,6 +2932,21 @@ const byLoopPoint = {
|
||||
}
|
||||
],
|
||||
"contributions": [
|
||||
{
|
||||
"capId": "claude-orchestration",
|
||||
"point": "plan:post",
|
||||
"into": "planner",
|
||||
"fragment": {
|
||||
"path": "fragments/plan-post.md",
|
||||
"inline": "# Claude orchestration — ultraplan plan-offload ownership (BETA)\n\n> Injected at `plan:post` `into: planner` only when\n> `claude_orchestration.enabled` is true. Default-off; `onError: skip`.\n\n## Ownership declaration\n\nThe `gsd-ultraplan-phase` plan-offload surface (offloading GSD's plan phase to\nClaude Code's ultraplan cloud) is **owned by this capability**, not by a\nstandalone BETA skill. Both surfaces share one runtime gate\n(`claude_orchestration.enabled`), one BETA boundary, and one Claude-Code-only\ndetection seam.\n\n## When the planner should consider ultraplan offload\n\nWhen this contribution is active (capability enabled, Claude Code runtime), the\nplanner MAY offer the `/gsd-ultraplan-phase` path as an alternative to local\n`/gsd-plan-phase` for phases where cloud-assisted planning adds value. This is\nadvisory, not mandatory — the stable local planner remains the default.\n\n## Fallback contract\n\nIf the capability is disabled, or the runtime is not Claude Code, ultraplan\noffload is **not surfaced** and the planner proceeds with the standard local\n`/gsd-plan-phase`. The `gsd-ultraplan-phase` command itself remains installed\n(its own runtime gate already no-ops on non-Claude runtimes); this contribution\nonly governs whether the capability manifest advertises it as part of the\norchestration surface.\n"
|
||||
},
|
||||
"produces": [],
|
||||
"consumes": [
|
||||
"CONTEXT.md"
|
||||
],
|
||||
"when": "claude_orchestration.enabled",
|
||||
"onError": "skip"
|
||||
},
|
||||
{
|
||||
"capId": "external-job",
|
||||
"point": "plan:post",
|
||||
@@ -2751,6 +2987,21 @@ const byLoopPoint = {
|
||||
"execute:wave:post": {
|
||||
"steps": [],
|
||||
"contributions": [
|
||||
{
|
||||
"capId": "claude-orchestration",
|
||||
"point": "execute:wave:post",
|
||||
"into": "executor",
|
||||
"fragment": {
|
||||
"path": "fragments/execute-wave-post.md",
|
||||
"inline": "# Claude orchestration — Workflow execution backend (BETA)\n\n> Injected at `execute:wave:post` `into: executor` only when\n> `claude_orchestration.enabled` is true. Default-off; `onError: skip`.\n\n## When this contribution is active\n\nThe Claude orchestration capability is **default-off and BETA**. It activates only\nwhen ALL of the following hold:\n\n1. `claude_orchestration.enabled` is `true` in `.planning/config.json`, AND\n2. the active runtime is **Claude Code** (the Workflow tool is Claude / Agent\n SDK-specific), AND\n3. `claude_orchestration.execution_backend` resolves to `workflow` — either\n explicitly, or via `auto` — **and** the Agent SDK version is\n `>= claude_orchestration.min_agent_sdk_version` (default `0.3.149`). The SDK\n floor applies in both `auto` and `workflow` modes (fail-closed: a pre-release\n or older SDK never activates the preview backend).\n\nDetection is fail-closed: any miss degrades to **inline, manual, one-agent-per-\nmessage dispatch** — exactly today's behaviour. On a non-Claude runtime this\ncontribution is a no-op.\n\n## What the executor does when the Workflow backend is active\n\nInstead of the orchestrator fanning out one `Agent(subagent_type=gsd-executor,\nisolation=worktree, run_in_background=true)` per message (which on Claude Code\ncannot nest further subagents — #853 — and so degrades to sequential inline\nexecution), execute-phase **emits a generated Workflow script** and lets the main\nloop orchestrate it:\n\n- **waves → one or more sequential `parallel()` barriers** — each wave is a\n barrier group; when plans within a wave share `files_modified`, they are split\n into separate sequential stages within that wave's barrier (the next wave\n still waits for the previous wave to complete).\n- **plans → `agent(brief, { agentType: 'gsd-executor', isolation: 'worktree' })`**\n — the SAME executor agent and worktree isolation the inline path uses, so the\n produced `SUMMARY.md` and commits are identical.\n- **`files_modified` overlap → separate sequential stages** — two plans that\n touch the same file are placed in different stages within the wave (the same\n overlap rule execute-phase already applies inline).\n- **`resumeFromRunId`** — wired to the phase run id, so an interrupted phase\n resumes without re-running completed plans.\n- **`budget(tokens)`** — a shared token pool across the whole phase when the\n orchestrator passes a `budgetTokens` value to `emitWorkflowScript` (it is a\n function parameter, not a config key; the orchestrator decides the budget).\n\nThe emitter is a pure function exposed through the capability command surface:\n`gsd-tools claude-orchestration emit-workflow --waves <manifest.json> --run-id <id>\n[--phase-dir <dir>] [--budget <n>]` (or `require('gsd-core/bin/lib/claude-orchestration.cjs').emitWorkflowScript`\ndirectly). It maps the phase's wave/plan manifest to the Workflow script string\nand never invokes the Workflow tool itself; the orchestrator runs the emitted\nscript. Detection is resolved by the orchestrator calling the pure\n`detectWorkflowBackend` with the LIVE host descriptor (the CLI\n`gsd-tools claude-orchestration detect-backend` is a simulation harness that\nassumes a capable host unless `--no-nested-dispatch` is passed — it does not probe\nthe real runtime; the orchestrator supplies the real descriptor).\n\n## Fallback contract\n\nIf detection resolves to `inline` (tool absent, SDK too old, runtime not Claude,\nor the capability disabled), execute-phase MUST proceed with the standard inline\nwave dispatch. The executor MUST NOT assume parallelism, a shared budget, or\nresume-from-run-id semantics in that mode.\n"
|
||||
},
|
||||
"produces": [],
|
||||
"consumes": [
|
||||
"PLAN.md"
|
||||
],
|
||||
"when": "claude_orchestration.enabled",
|
||||
"onError": "skip"
|
||||
},
|
||||
{
|
||||
"capId": "external-job",
|
||||
"point": "execute:wave:post",
|
||||
@@ -2961,6 +3212,9 @@ const byLoopPoint = {
|
||||
const configKeys = {
|
||||
"workflow.ai_integration_phase": "ai-integration",
|
||||
"workflow.assumption_delta": "assumption-delta",
|
||||
"claude_orchestration.enabled": "claude-orchestration",
|
||||
"claude_orchestration.execution_backend": "claude-orchestration",
|
||||
"claude_orchestration.min_agent_sdk_version": "claude-orchestration",
|
||||
"workflow.code_review": "code-review",
|
||||
"workflow.code_review_depth": "code-review",
|
||||
"workflow.drift_threshold": "drift",
|
||||
@@ -3012,6 +3266,29 @@ const configSchema = {
|
||||
"default": true,
|
||||
"description": "Enable the assumption-delta architecture checkpoint during planning. When a pluralization/optional/chosen signal is detected in the phase scope, the planner is prompted to re-ask whether the primary key / identity model still names the right thing. Advisory (non-blocking)."
|
||||
},
|
||||
"claude_orchestration.enabled": {
|
||||
"owner": "claude-orchestration",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Master toggle for the Claude orchestration capability. Default-off + BETA: the Workflow-tool execution backend and the ultraplan plan-offload surface are inert unless this is true. When false, loop behaviour is byte-identical to a non-Claude runtime (inline/manual dispatch)."
|
||||
},
|
||||
"claude_orchestration.execution_backend": {
|
||||
"owner": "claude-orchestration",
|
||||
"type": "enum",
|
||||
"default": "auto",
|
||||
"description": "Which execute-phase dispatch backend to use when the capability is enabled. 'auto' (default) activates the Workflow backend only when the runtime is Claude AND the Workflow tool is detected AND the Agent SDK meets claude_orchestration.min_agent_sdk_version; otherwise it falls back to inline. 'workflow' forces the Workflow backend when the tool is present AND the Agent SDK meets the floor (still fails closed to inline if the tool is absent or the SDK is too old — the floor applies in both modes). 'inline' forces today's manual one-agent-per-message dispatch regardless of tool availability.",
|
||||
"values": [
|
||||
"auto",
|
||||
"workflow",
|
||||
"inline"
|
||||
]
|
||||
},
|
||||
"claude_orchestration.min_agent_sdk_version": {
|
||||
"owner": "claude-orchestration",
|
||||
"type": "string",
|
||||
"default": "0.3.149",
|
||||
"description": "Minimum Agent SDK version required to activate the Workflow backend under execution_backend='auto'. Defaults to 0.3.149 (the release that introduced the Workflow tool). Raise to pin a higher floor; the detection seam fails closed to inline for any runtime reporting an older or unknown version."
|
||||
},
|
||||
"workflow.code_review": {
|
||||
"owner": "code-review",
|
||||
"type": "boolean",
|
||||
@@ -3258,7 +3535,7 @@ const runtimes = {
|
||||
"antigravity": {
|
||||
"id": "antigravity",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Antigravity",
|
||||
"description": "Google Antigravity IDE — nested under ~/.gemini/antigravity; probed across 1.x and 2.x layouts; Gemini hook event dialect; flat skill layout; tier-1 support.",
|
||||
"tier": "core",
|
||||
@@ -3292,6 +3569,14 @@ const runtimes = {
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToAntigravitySkill"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeAgentToAntigravityAgent"
|
||||
}
|
||||
],
|
||||
"local": [
|
||||
@@ -3302,6 +3587,14 @@ const runtimes = {
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToAntigravitySkill"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeAgentToAntigravityAgent"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3336,7 +3629,7 @@ const runtimes = {
|
||||
"augment": {
|
||||
"id": "augment",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Augment Code",
|
||||
"description": "Augment Code CLI — commands + nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -3439,7 +3732,7 @@ const runtimes = {
|
||||
"claude": {
|
||||
"id": "claude",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Claude Code",
|
||||
"description": "Anthropic Claude Code — primary development runtime; tier-1 support with full hook surface and skills-based global install.",
|
||||
"tier": "core",
|
||||
@@ -3523,7 +3816,7 @@ const runtimes = {
|
||||
"cline": {
|
||||
"id": "cline",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Cline",
|
||||
"description": "Cline (VS Code extension) — global-only nested-skill layout; cline-rules hook surface (.clinerules); no hook events emitted; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -3584,7 +3877,7 @@ const runtimes = {
|
||||
"codebuddy": {
|
||||
"id": "codebuddy",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "CodeBuddy",
|
||||
"description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -3687,7 +3980,7 @@ const runtimes = {
|
||||
"codex": {
|
||||
"id": "codex",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "OpenAI Codex CLI",
|
||||
"description": "OpenAI Codex CLI — shell-var command style; per-agent sandbox tiers; config.toml + hooks.json hook surface; tier-1 support.",
|
||||
"tier": "core",
|
||||
@@ -3758,7 +4051,7 @@ const runtimes = {
|
||||
"copilot": {
|
||||
"id": "copilot",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "GitHub Copilot",
|
||||
"description": "GitHub Copilot (VS Code) — markdown config format; copilot-inline hook surface; no hook events emitted; flat skill nesting (unconfirmed recursive loader); tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -3786,6 +4079,14 @@ const runtimes = {
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCopilotSkill"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeAgentToCopilotAgent"
|
||||
}
|
||||
],
|
||||
"local": [
|
||||
@@ -3796,6 +4097,14 @@ const runtimes = {
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCopilotSkill"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeAgentToCopilotAgent"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3829,7 +4138,7 @@ const runtimes = {
|
||||
"cursor": {
|
||||
"id": "cursor",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Cursor",
|
||||
"description": "Cursor IDE — skills + converted commands artifact layout; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -3932,7 +4241,7 @@ const runtimes = {
|
||||
"hermes": {
|
||||
"id": "hermes",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Hermes Agent",
|
||||
"description": "Hermes Agent (NousResearch) — skills nest under skills/gsd/ category bucket; nested skill layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -4003,7 +4312,7 @@ const runtimes = {
|
||||
"kilo": {
|
||||
"id": "kilo",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Kilo Code",
|
||||
"description": "Kilo Code — XDG-based config dir; global skills at ~/.kilo/skills (separate from XDG config); flat command/ + skills artifact layout; no lifecycle hook registration; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -4096,7 +4405,7 @@ const runtimes = {
|
||||
"kimi": {
|
||||
"id": "kimi",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Kimi CLI",
|
||||
"description": "Kimi CLI (Moonshot AI) — generic agents root at ~/.config/agents; skills + kimi-agents artifact layout; no hook surface; no hook events; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -4170,7 +4479,7 @@ const runtimes = {
|
||||
"opencode": {
|
||||
"id": "opencode",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "OpenCode",
|
||||
"description": "OpenCode — XDG-based config dir; flat command/ + skills artifact layout; settings-json config format; no lifecycle hook registration; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -4259,7 +4568,7 @@ const runtimes = {
|
||||
"qwen": {
|
||||
"id": "qwen",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Qwen Code",
|
||||
"description": "Qwen Code (Alibaba) — nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -4334,7 +4643,7 @@ const runtimes = {
|
||||
"trae": {
|
||||
"id": "trae",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Trae IDE",
|
||||
"description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -4420,7 +4729,7 @@ const runtimes = {
|
||||
"windsurf": {
|
||||
"id": "windsurf",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "Windsurf",
|
||||
"description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.",
|
||||
"tier": "core",
|
||||
@@ -4495,6 +4804,108 @@ const runtimes = {
|
||||
"runtime": "undocumented"
|
||||
}
|
||||
}
|
||||
},
|
||||
"zcode": {
|
||||
"id": "zcode",
|
||||
"role": "runtime",
|
||||
"version": "1.7.0-rc.3",
|
||||
"title": "ZCode",
|
||||
"description": "ZCode (Z.ai) — desktop Agentic Development Environment for GLM-5.2; Claude-shaped nested skills at ~/.zcode/skills/<name>/SKILL.md, slash commands, named subagents, native MCP; declarative plugin surface; profile-marker install; tier-2 community support.",
|
||||
"tier": "core",
|
||||
"requires": [],
|
||||
"engines": {
|
||||
"gsd": ">=1.6.0"
|
||||
},
|
||||
"runtime": {
|
||||
"configHome": {
|
||||
"kind": "dot-home",
|
||||
"name": ".zcode",
|
||||
"env": [
|
||||
"ZCODE_CONFIG_DIR"
|
||||
]
|
||||
},
|
||||
"localConfigDir": ".zcode",
|
||||
"configFormat": "none",
|
||||
"artifactLayout": {
|
||||
"global": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
},
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
}
|
||||
],
|
||||
"local": [
|
||||
{
|
||||
"kind": "skills",
|
||||
"destSubpath": "skills",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "nested",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToClaudeSkill"
|
||||
},
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandStyle": "slash-hyphen",
|
||||
"hooksSurface": "none",
|
||||
"sandboxTier": "none",
|
||||
"supportTier": 2,
|
||||
"installSurface": "profile-marker-only",
|
||||
"writesSharedSettings": false,
|
||||
"permissionWriter": null,
|
||||
"extendedHookEvents": [],
|
||||
"hostIntegration": {
|
||||
"embeddingMode": "declarative",
|
||||
"commandSurface": "slash-file",
|
||||
"dispatch": {
|
||||
"namedDispatch": true,
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": false,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": false
|
||||
},
|
||||
"modelMode": "passive",
|
||||
"hookBus": "host",
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "electron"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4509,6 +4920,11 @@ const commandFamilies = {
|
||||
"module": "audit-command-router.cjs",
|
||||
"router": "routeAuditUat"
|
||||
},
|
||||
"claude-orchestration": {
|
||||
"capId": "claude-orchestration",
|
||||
"module": "claude-orchestration-command-router.cjs",
|
||||
"router": "routeClaudeOrchestrationCommand"
|
||||
},
|
||||
"extract-messages": {
|
||||
"capId": "profile-pipeline",
|
||||
"module": "profile-pipeline-command-router.cjs",
|
||||
@@ -4648,6 +5064,7 @@ const _requiresGraph = {
|
||||
"audit": [],
|
||||
"augment": [],
|
||||
"claude": [],
|
||||
"claude-orchestration": [],
|
||||
"cline": [],
|
||||
"code-review": [],
|
||||
"codebuddy": [],
|
||||
@@ -4676,7 +5093,8 @@ const _requiresGraph = {
|
||||
"tdd": [],
|
||||
"trae": [],
|
||||
"ui": [],
|
||||
"windsurf": []
|
||||
"windsurf": [],
|
||||
"zcode": []
|
||||
};
|
||||
|
||||
function requiresClosure(id) {
|
||||
|
||||
136
gsd-core/bin/lib/claude-orchestration-command-router.cjs
Normal file
136
gsd-core/bin/lib/claude-orchestration-command-router.cjs
Normal file
@@ -0,0 +1,136 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Claude orchestration command router — CLI dispatcher for
|
||||
* `gsd-tools claude-orchestration <subcommand>`.
|
||||
*
|
||||
* #1143 — thin CLI adapter over the pure `claude-orchestration.cjs` module.
|
||||
* Lets execute-phase (or any orchestrator) invoke the Workflow-backend
|
||||
* detection and the Workflow-script emitter through the standard capability
|
||||
* command surface (ADR-959) instead of a bare `require()`.
|
||||
*
|
||||
* Router signature: { args, cwd, raw, error } — identical to the other host
|
||||
* routers; discovered by dispatchCapabilityCommand via the registry's
|
||||
* commandFamilies index.
|
||||
*
|
||||
* Subcommands:
|
||||
* detect-backend [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch]
|
||||
* Resolves whether the Workflow backend should activate. `--runtime`
|
||||
* defaults to the GSD_RUNTIME env var (or 'unknown'). Reads the
|
||||
* `claude_orchestration.*` keys from .planning/config.json. Emits
|
||||
* { available, backend, reason }.
|
||||
*
|
||||
* emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>]
|
||||
* Reads a wave/plan manifest JSON file and emits the generated Workflow
|
||||
* script + summary. The manifest shape matches emitWorkflowScript's input:
|
||||
* { waves: [{ id, plans: [{ id, brief, files_modified: string[] }] }] }.
|
||||
*/
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
const node_fs_1 = __importDefault(require("node:fs"));
|
||||
const node_path_1 = __importDefault(require("node:path"));
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const io = require("./io.cjs");
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const core = require("./claude-orchestration.cjs");
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const configLoader = require("./config-loader.cjs");
|
||||
const { output } = io;
|
||||
const { detectWorkflowBackend, emitWorkflowScript } = core;
|
||||
const CAPABLE_HOST = { dispatch: { nested: true, background: true } };
|
||||
function usage(error) {
|
||||
error('Usage: gsd-tools claude-orchestration <detect-backend|emit-workflow> [...]\n' +
|
||||
' detect-backend [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch]\n' +
|
||||
' emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>]');
|
||||
}
|
||||
function argValue(args, flag) {
|
||||
const i = args.indexOf(flag);
|
||||
return i !== -1 && i + 1 < args.length ? args[i + 1] : undefined;
|
||||
}
|
||||
/**
|
||||
* Detect whether the Workflow backend should activate for the current/given
|
||||
* runtime. Reads `claude_orchestration.*` from the project config; runtime and
|
||||
* SDK version come from flags (the orchestrator already knows these) or env.
|
||||
*/
|
||||
function cmdDetectBackend(args, cwd, raw) {
|
||||
const runtimeId = argValue(args, '--runtime') || process.env['GSD_RUNTIME'] || 'unknown';
|
||||
const agentSdkVersion = argValue(args, '--agent-sdk-version');
|
||||
const noNested = args.includes('--no-nested-dispatch');
|
||||
const hostIntegration = noNested ? { dispatch: { nested: false, background: true } } : CAPABLE_HOST;
|
||||
// Resolve the claude_orchestration.* slice from the project config (federated
|
||||
// keys are merged by loadConfig as a nested object). A config read failure
|
||||
// degrades to inline — it must not break the core loop.
|
||||
let claudeSlice = {};
|
||||
try {
|
||||
const loaded = configLoader.loadConfig(cwd);
|
||||
const slice = loaded['claude_orchestration'];
|
||||
if (slice && typeof slice === 'object' && !Array.isArray(slice)) {
|
||||
claudeSlice = slice;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
claudeSlice = {};
|
||||
}
|
||||
// Flatten the nested slice into the dotted-key shape detectWorkflowBackend expects.
|
||||
const flatConfig = {};
|
||||
for (const k of Object.keys(claudeSlice)) {
|
||||
flatConfig['claude_orchestration.' + k] = claudeSlice[k];
|
||||
}
|
||||
const result = detectWorkflowBackend({ runtimeId, hostIntegration, config: flatConfig, agentSdkVersion });
|
||||
output(result, raw);
|
||||
}
|
||||
/**
|
||||
* Emit a Workflow script from a wave/plan manifest file.
|
||||
*/
|
||||
function cmdEmitWorkflow(args, _cwd, raw, error) {
|
||||
const wavesPath = argValue(args, '--waves');
|
||||
const runId = argValue(args, '--run-id');
|
||||
const phaseDir = argValue(args, '--phase-dir') || '.planning/phases/current';
|
||||
const budgetRaw = argValue(args, '--budget');
|
||||
if (!wavesPath) {
|
||||
error('emit-workflow requires --waves <path>');
|
||||
return;
|
||||
}
|
||||
if (!runId) {
|
||||
error('emit-workflow requires --run-id <id>');
|
||||
return;
|
||||
}
|
||||
let waves;
|
||||
try {
|
||||
const content = node_fs_1.default.readFileSync(node_path_1.default.resolve(wavesPath), 'utf8');
|
||||
const parsed = JSON.parse(content);
|
||||
waves = parsed['waves'];
|
||||
}
|
||||
catch (e) {
|
||||
error('emit-workflow: could not read/parse --waves file "' + wavesPath + '": ' + (e instanceof Error ? e.message : String(e)));
|
||||
return;
|
||||
}
|
||||
const budgetTokens = budgetRaw !== undefined ? parseInt(budgetRaw, 10) : undefined;
|
||||
const budget = (typeof budgetTokens === 'number' && !Number.isNaN(budgetTokens)) ? budgetTokens : undefined;
|
||||
const result = emitWorkflowScript({
|
||||
phaseDir,
|
||||
runId,
|
||||
waves: waves,
|
||||
budgetTokens: budget,
|
||||
});
|
||||
if (!result.ok) {
|
||||
error('emit-workflow: ' + result.reason);
|
||||
return;
|
||||
}
|
||||
output({ script: result.script, summary: result.summary }, raw);
|
||||
}
|
||||
function routeClaudeOrchestrationCommand(opts) {
|
||||
const { args, cwd, raw, error } = opts;
|
||||
// args[0] is the family ('claude-orchestration'); the subcommand is args[1].
|
||||
const subcommand = args[1];
|
||||
if (subcommand === 'detect-backend') {
|
||||
cmdDetectBackend(args, cwd, raw);
|
||||
}
|
||||
else if (subcommand === 'emit-workflow') {
|
||||
cmdEmitWorkflow(args, cwd, raw, error);
|
||||
}
|
||||
else {
|
||||
usage(error);
|
||||
}
|
||||
}
|
||||
module.exports = { routeClaudeOrchestrationCommand };
|
||||
404
gsd-core/bin/lib/claude-orchestration.cjs
Normal file
404
gsd-core/bin/lib/claude-orchestration.cjs
Normal file
@@ -0,0 +1,404 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Claude Orchestration Capability — Workflow-tool backend detection + emitter
|
||||
*
|
||||
* #1143 — adopts Claude Code's Workflow tool (the engine behind `/effort ultracode`)
|
||||
* as an optional, runtime-gated parallel-execution backend for the GSD loop.
|
||||
*
|
||||
* This module is the pure, testable core of the capability. It owns two seams:
|
||||
*
|
||||
* detectWorkflowBackend({ runtimeId, hostIntegration, config, agentSdkVersion })
|
||||
* → { available: boolean, backend: 'workflow'|'inline', reason: string }
|
||||
* Fail-closed: every miss degrades to `inline` (today's behaviour), so the
|
||||
* core loop is byte-identical unless every gate opens. This is criteria 3 + 6.
|
||||
*
|
||||
* emitWorkflowScript({ phaseDir, waves, runId, budgetTokens? })
|
||||
* → { ok:true, script, summary } | { ok:false, reason }
|
||||
* Maps GSD's wave/plan model 1:1 onto Workflow primitives:
|
||||
* wave → sequential `parallel()` stage barriers,
|
||||
* plan → `agent(brief, { agentType:'gsd-executor', isolation:'worktree' })`,
|
||||
* files_modified overlap → forces plans into separate sequential stages
|
||||
* (the same overlap rule execute-phase already applies inline),
|
||||
* resumeFromRunId → wired to the phase run id,
|
||||
* budgetTokens → a shared token pool.
|
||||
* The emitted script composes the SAME gsd-executor agent and worktree
|
||||
* isolation the inline path uses, so it produces the same artifacts/commits
|
||||
* (criterion 2). It is a generated string consumed by the orchestrator; this
|
||||
* module never invokes the Workflow tool itself.
|
||||
*
|
||||
* Design laws:
|
||||
* - Gall's Law: ship a small working slice that composes existing primitives
|
||||
* (gsd-executor + worktree isolation) rather than reinventing them.
|
||||
* - Greenspun's Tenth Rule (cited in #1143): adopt the Workflow tool's
|
||||
* barrier/pipeline/budget/resume semantics instead of hand-rolling them.
|
||||
* - Postel's Law: liberal in input (missing fields → inline), conservative in
|
||||
* output (workflow only when every gate opens).
|
||||
* - Fail-closed: an unknown version, a missing descriptor, or a disabled
|
||||
* toggle all resolve to `inline`, never to `workflow`.
|
||||
*
|
||||
* Zero external dependencies. Pure functions. Never throws on bad input.
|
||||
*/
|
||||
// ─── Constants ────────────────────────────────────────────────────────────────
|
||||
/**
|
||||
* The Agent SDK version that introduced the Workflow tool (#1143 prior art).
|
||||
* Used as the default floor when config does not override it. A runtime reporting
|
||||
* an agentSdkVersion below this cannot host the Workflow backend.
|
||||
*/
|
||||
const WORKFLOW_TOOL_FLOOR_VERSION = '0.3.149';
|
||||
/** Closed enum for the `claude_orchestration.execution_backend` config key. */
|
||||
const BACKEND_VALUES = new Set(['auto', 'workflow', 'inline']);
|
||||
/** Only this runtime can host the Workflow tool (Claude Code / Agent SDK). */
|
||||
const WORKFLOW_RUNTIME = 'claude';
|
||||
// ─── Semver helpers ───────────────────────────────────────────────────────────
|
||||
/** Official-ish strict SemVer 2.0.0 numeric triple (+ optional pre/build). */
|
||||
const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
||||
/** True for a syntactically valid semver string. */
|
||||
function isValidSemver(s) {
|
||||
return typeof s === 'string' && SEMVER_RE.test(s);
|
||||
}
|
||||
/**
|
||||
* Compare two semver strings.
|
||||
* Returns -1/0/1 in the usual sense. Garbage in either position → -1 (fail-closed:
|
||||
* an unparseable version is treated as "less than" any real floor, so detection
|
||||
* never accidentally enables the preview backend on an unknown SDK).
|
||||
*
|
||||
* Pre-release/build metadata are ignored for the comparison — only the numeric
|
||||
* major.minor.patch triple participates, matching how the Workflow-tool floor is
|
||||
* specified (a plain "0.3.149").
|
||||
*/
|
||||
function compareSemver(a, b) {
|
||||
if (!isValidSemver(a) || !isValidSemver(b))
|
||||
return -1;
|
||||
// Split numeric triple from pre-release/build metadata.
|
||||
const parseTriple = (s) => {
|
||||
const core = s.split('-')[0].split('+')[0].split('.');
|
||||
return [parseInt(core[0], 10), parseInt(core[1], 10), parseInt(core[2], 10)];
|
||||
};
|
||||
const hasPre = (s) => s.indexOf('-') !== -1;
|
||||
const preIdentifiers = (s) => (s.split('-')[1] || '').split('+')[0].split('.').filter((x) => x.length > 0);
|
||||
const am = parseTriple(a);
|
||||
const bm = parseTriple(b);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (am[i] < bm[i])
|
||||
return -1;
|
||||
if (am[i] > bm[i])
|
||||
return 1;
|
||||
}
|
||||
// Numeric triple is equal. SemVer 2.0.0 §11 precedence:
|
||||
// - a version WITH a pre-release tag is LOWER than the same triple WITHOUT one
|
||||
// (keeps the floor fail-closed for pre-release builds of the GA floor);
|
||||
// - two pre-releases of the same triple are ordered by their dot-separated
|
||||
// identifiers (numeric < alphanumeric; numeric compared numerically,
|
||||
// alphanumeric lexically; fewer identifiers < more).
|
||||
const aPre = hasPre(a);
|
||||
const bPre = hasPre(b);
|
||||
if (aPre && !bPre)
|
||||
return -1;
|
||||
if (!aPre && bPre)
|
||||
return 1;
|
||||
if (aPre && bPre) {
|
||||
const ai = preIdentifiers(a);
|
||||
const bi = preIdentifiers(b);
|
||||
const len = Math.min(ai.length, bi.length);
|
||||
for (let i = 0; i < len; i++) {
|
||||
const ax = ai[i];
|
||||
const bx = bi[i];
|
||||
const aNum = /^\d+$/.test(ax);
|
||||
const bNum = /^\d+$/.test(bx);
|
||||
if (aNum && bNum) {
|
||||
const an = parseInt(ax, 10);
|
||||
const bn = parseInt(bx, 10);
|
||||
if (an < bn)
|
||||
return -1;
|
||||
if (an > bn)
|
||||
return 1;
|
||||
}
|
||||
else if (aNum && !bNum) {
|
||||
return -1; // numeric identifiers always lower than alphanumeric
|
||||
}
|
||||
else if (!aNum && bNum) {
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
if (ax < bx)
|
||||
return -1;
|
||||
if (ax > bx)
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (ai.length < bi.length)
|
||||
return -1;
|
||||
if (ai.length > bi.length)
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
/** Inline result shorthand. */
|
||||
function inline(reason, available = false) {
|
||||
return { available, backend: 'inline', reason };
|
||||
}
|
||||
/**
|
||||
* Resolve whether the Workflow-tool backend should activate.
|
||||
*
|
||||
* Gate ladder (all must pass for `workflow`; first miss wins, fail-closed):
|
||||
* 1. capability enabled (claude_orchestration.enabled truthy)
|
||||
* 2. runtime is Claude (the only runtime that exposes the Workflow tool)
|
||||
* 3. execution_backend !== 'inline'
|
||||
* 4. host descriptor signals nested+background dispatch (Workflow-tool capable)
|
||||
* 5. agentSdkVersion is a known, valid semver
|
||||
* 6. agentSdkVersion >= the configured floor (default WORKFLOW_TOOL_FLOOR_VERSION)
|
||||
* 7. execution_backend === 'workflow' OR 'auto' (both reach here; 'inline' exited at 3)
|
||||
*
|
||||
* Never throws. Destructures defensively.
|
||||
*/
|
||||
function detectWorkflowBackend(input) {
|
||||
if (input === null || input === undefined || typeof input !== 'object') {
|
||||
return inline('capability_disabled');
|
||||
}
|
||||
const cfg = (input.config !== null && input.config !== undefined && typeof input.config === 'object')
|
||||
? input.config
|
||||
: {};
|
||||
// 1. capability must be opted in (default-off — ships disabled).
|
||||
if (!cfg['claude_orchestration.enabled']) {
|
||||
return inline('capability_disabled');
|
||||
}
|
||||
// 2. only Claude can host the Workflow tool.
|
||||
if (input.runtimeId !== WORKFLOW_RUNTIME) {
|
||||
return inline('runtime_not_claude');
|
||||
}
|
||||
// 3. explicit inline opt-out short-circuits.
|
||||
let backendRaw = cfg['claude_orchestration.execution_backend'];
|
||||
if (typeof backendRaw !== 'string' || !BACKEND_VALUES.has(backendRaw)) {
|
||||
backendRaw = 'auto';
|
||||
}
|
||||
if (backendRaw === 'inline') {
|
||||
return inline('backend_inline');
|
||||
}
|
||||
// 4. the host dispatch descriptor must be the nesting-capable Claude-Code shape
|
||||
// (a proxy for Workflow-tool presence). This is Claude-specific and already
|
||||
// gated at step 2; `background:true` alone is true on several non-Claude hosts,
|
||||
// so the proxy is only meaningful after the runtime check above. Note: this is
|
||||
// NOT the canonical `shouldFlattenDispatch` rule (which keys on
|
||||
// `backgroundDispatch`); the Workflow backend works precisely because a single
|
||||
// tool-call orchestrates internally, sidestepping the backgroundDispatch:false
|
||||
// limitation. Missing/false/foreign descriptor → fail-closed.
|
||||
const hi = input.hostIntegration;
|
||||
if (hi === null || hi === undefined || typeof hi !== 'object' || Array.isArray(hi)) {
|
||||
return inline('workflow_tool_unavailable');
|
||||
}
|
||||
const dispatch = hi.dispatch;
|
||||
if (typeof dispatch !== 'object' || dispatch === null || Array.isArray(dispatch)) {
|
||||
return inline('workflow_tool_unavailable');
|
||||
}
|
||||
const nested = dispatch['nested'];
|
||||
const background = dispatch['background'];
|
||||
if (nested !== true || background !== true) {
|
||||
return inline('workflow_tool_unavailable');
|
||||
}
|
||||
// 5. an unknown agentSdkVersion cannot be trusted to meet the floor.
|
||||
if (!isValidSemver(input.agentSdkVersion)) {
|
||||
return inline('agent_sdk_version_unknown');
|
||||
}
|
||||
// 6. version floor (config override > default constant).
|
||||
const floorRaw = cfg['claude_orchestration.min_agent_sdk_version'];
|
||||
const floor = typeof floorRaw === 'string' && isValidSemver(floorRaw) ? floorRaw : WORKFLOW_TOOL_FLOOR_VERSION;
|
||||
if (compareSemver(input.agentSdkVersion, floor) < 0) {
|
||||
return inline('agent_sdk_version_below_floor');
|
||||
}
|
||||
// 7. auto/workflow both reach the workflow backend once every gate passes.
|
||||
return { available: true, backend: 'workflow', reason: 'workflow_backend_active' };
|
||||
}
|
||||
/**
|
||||
* Partition a wave's plans into a near-minimal number of sequential stages (via
|
||||
* greedy first-fit — not guaranteed optimal for arbitrary overlap graphs, but
|
||||
* correct: no two plans sharing a file ever cohabit a stage) such that no two
|
||||
* plans in the same stage share a modified file. Each plan goes into the earliest
|
||||
* stage where it does not overlap any plan already there.
|
||||
*
|
||||
* A plan with an EMPTY files_modified set declares no files; it overlaps nothing
|
||||
* and coalesces into stage 0 (same behavior as the inline path, which also cannot
|
||||
* guard against undeclared concurrent writes — declare filesModified accurately).
|
||||
*
|
||||
* This is the same overlap rule execute-phase applies inline — the only difference
|
||||
* is the execution vehicle (Workflow `parallel()` vs one-agent-per-message).
|
||||
*/
|
||||
function partitionStages(plans) {
|
||||
const stages = [];
|
||||
for (const plan of plans) {
|
||||
const fileSet = new Set(plan.files_modified);
|
||||
let placed = false;
|
||||
for (const stage of stages) {
|
||||
let overlap = false;
|
||||
for (const f of fileSet) {
|
||||
if (stage.files.has(f)) {
|
||||
overlap = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!overlap) {
|
||||
stage.plans.push(plan);
|
||||
for (const f of fileSet)
|
||||
stage.files.add(f);
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!placed) {
|
||||
stages.push({ plans: [plan], files: new Set(fileSet) });
|
||||
}
|
||||
}
|
||||
return stages.map((s) => s.plans.map((p) => p.id));
|
||||
}
|
||||
/**
|
||||
* Quote a free-text value for safe embedding as a JavaScript/Workflow double-quoted
|
||||
* string literal. Uses JSON.stringify so every JS-relevant escape (backslash, quote,
|
||||
* newline, tab, NUL, U+2028/U+2029, all control chars) is handled by the language
|
||||
* itself — there is no hand-rolled escape table to drift. Returns the value already
|
||||
* wrapped in its surrounding quotes.
|
||||
*/
|
||||
function quoteString(s) {
|
||||
return JSON.stringify(s);
|
||||
}
|
||||
/**
|
||||
* True if `s` is a safe identifier/path token to interpolate into the generated
|
||||
* script WITHOUT requiring a string-literal context — i.e. it contains no
|
||||
* character that could terminate a comment line (`\n`/`\r`), break out of a
|
||||
* string literal (`"` / `\`), or smuggle a NUL/control sequence. Used for
|
||||
* `phaseDir`, `runId`, `wave.id`, and `plan.id`, which are identifiers/paths and
|
||||
* must never legitimately contain such characters. Rejecting them at validation
|
||||
* (rather than silently flattening) keeps the emitted script faithful to input.
|
||||
*/
|
||||
const UNSCRIPTABLE_CHAR_RE = /[\r\n"\\\x00-\x1f\x7f\u2028\u2029]/;
|
||||
function isScriptableIdentifier(s) {
|
||||
if (typeof s !== 'string' || s.length === 0)
|
||||
return false;
|
||||
return !UNSCRIPTABLE_CHAR_RE.test(s);
|
||||
}
|
||||
/**
|
||||
* Emit a Workflow script mapping the phase's wave/plan model onto Workflow
|
||||
* primitives. Pure and deterministic: identical input yields an identical string.
|
||||
*
|
||||
* Returns ok:false (never throws) on invalid input — empty waves, missing runId,
|
||||
* a wave with no plans, etc.
|
||||
*/
|
||||
function emitWorkflowScript(input) {
|
||||
if (input === null || input === undefined || typeof input !== 'object') {
|
||||
return { ok: false, reason: 'invalid_input' };
|
||||
}
|
||||
const { phaseDir, waves, runId } = input;
|
||||
// Identifiers/paths interpolated into the generated script must be free of any
|
||||
// character that could terminate a comment, break out of a string literal, or
|
||||
// smuggle control bytes — reject up front (security: #1143 review Finding 1).
|
||||
if (!isScriptableIdentifier(phaseDir)) {
|
||||
return { ok: false, reason: 'phaseDir must be a non-empty string without newlines/quotes/backslash/control chars' };
|
||||
}
|
||||
if (!isScriptableIdentifier(runId)) {
|
||||
return { ok: false, reason: 'runId must be a non-empty string without newlines/quotes/backslash/control chars' };
|
||||
}
|
||||
if (!Array.isArray(waves) || waves.length === 0) {
|
||||
return { ok: false, reason: 'waves must be a non-empty array' };
|
||||
}
|
||||
for (let i = 0; i < waves.length; i++) {
|
||||
const w = waves[i];
|
||||
if (w === null || typeof w !== 'object' || typeof w.id !== 'string') {
|
||||
return { ok: false, reason: 'waves[' + i + '] must be { id, plans: non-empty[] }' };
|
||||
}
|
||||
if (!isScriptableIdentifier(w.id)) {
|
||||
return { ok: false, reason: 'waves[' + i + '].id must not contain newlines/quotes/backslash/control chars' };
|
||||
}
|
||||
if (!Array.isArray(w.plans) || w.plans.length === 0) {
|
||||
return { ok: false, reason: 'waves[' + i + '] must have a non-empty plans array' };
|
||||
}
|
||||
const seenIds = new Set();
|
||||
for (let j = 0; j < w.plans.length; j++) {
|
||||
const p = w.plans[j];
|
||||
if (p === null || typeof p !== 'object' || typeof p.id !== 'string' || typeof p.brief !== 'string' || !Array.isArray(p.files_modified)) {
|
||||
return { ok: false, reason: 'waves[' + i + '].plans[' + j + '] must be { id, brief, files_modified[] }' };
|
||||
}
|
||||
if (!isScriptableIdentifier(p.id)) {
|
||||
return { ok: false, reason: 'waves[' + i + '].plans[' + j + '].id must not contain newlines/quotes/backslash/control chars' };
|
||||
}
|
||||
if (seenIds.has(p.id)) {
|
||||
return { ok: false, reason: 'waves[' + i + '] has duplicate plan id "' + p.id + '"' };
|
||||
}
|
||||
seenIds.add(p.id);
|
||||
for (const f of p.files_modified) {
|
||||
if (typeof f !== 'string' || f.length === 0) {
|
||||
return { ok: false, reason: 'waves[' + i + '].plans[' + j + '].files_modified entries must be non-empty strings' };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const budgetTokens = (typeof input.budgetTokens === 'number' && Number.isFinite(input.budgetTokens) && input.budgetTokens > 0)
|
||||
? Math.floor(input.budgetTokens)
|
||||
: null;
|
||||
const lines = [];
|
||||
lines.push('// GSD Workflow script — generated by the claude-orchestration capability (#1143)');
|
||||
lines.push('// phase: ' + phaseDir);
|
||||
lines.push('// BETA: preview-grade; on any failure the orchestrator falls back to inline dispatch.');
|
||||
lines.push('// Composes the SAME gsd-executor agent + worktree isolation as the inline path,');
|
||||
lines.push('// so artifacts (SUMMARY.md) and commits are produced identically.');
|
||||
lines.push('resumeFromRunId(' + quoteString(runId) + ')');
|
||||
if (budgetTokens !== null) {
|
||||
lines.push('budget(' + budgetTokens + ')');
|
||||
}
|
||||
lines.push('');
|
||||
const stagesByWave = [];
|
||||
let totalPlans = 0;
|
||||
for (let wi = 0; wi < waves.length; wi++) {
|
||||
const wave = waves[wi];
|
||||
const stages = partitionStages(wave.plans);
|
||||
stagesByWave.push(stages);
|
||||
totalPlans += wave.plans.length;
|
||||
lines.push('// Wave ' + wave.id);
|
||||
for (let si = 0; si < stages.length; si++) {
|
||||
const stagePlanIds = stages[si];
|
||||
// Resolve back to plan objects for briefs (ids are unique within a wave — validated above).
|
||||
const stagePlans = stagePlanIds.map((id) => wave.plans.find((p) => p.id === id));
|
||||
if (stages.length > 1) {
|
||||
lines.push('// Stage ' + si + (si > 0 ? ' (sequential — files_modified overlap)' : ''));
|
||||
}
|
||||
if (stagePlans.length === 1) {
|
||||
const p = stagePlans[0];
|
||||
lines.push('parallel(');
|
||||
lines.push(' agent(' + quoteString(p.brief) + ', { agentType: "gsd-executor", isolation: "worktree" })');
|
||||
lines.push(')');
|
||||
}
|
||||
else {
|
||||
lines.push('parallel(');
|
||||
for (const p of stagePlans) {
|
||||
lines.push(' agent(' + quoteString(p.brief) + ', { agentType: "gsd-executor", isolation: "worktree" }),');
|
||||
}
|
||||
// Replace trailing comma on the last agent line with nothing.
|
||||
const lastIdx = lines.length - 1;
|
||||
lines[lastIdx] = lines[lastIdx].replace(/,$/, '');
|
||||
lines.push(')');
|
||||
}
|
||||
}
|
||||
if (wi < waves.length - 1)
|
||||
lines.push('');
|
||||
}
|
||||
lines.push('// Each agent writes SUMMARY.md on its worktree branch; commits land there');
|
||||
lines.push('// and are merged by the orchestrator exactly as in inline wave dispatch.');
|
||||
const script = lines.join('\n');
|
||||
return {
|
||||
ok: true,
|
||||
script,
|
||||
summary: {
|
||||
waves: waves.length,
|
||||
plans: totalPlans,
|
||||
stagesByWave,
|
||||
resumeRunId: runId,
|
||||
budgetTokens,
|
||||
},
|
||||
};
|
||||
}
|
||||
module.exports = {
|
||||
detectWorkflowBackend,
|
||||
emitWorkflowScript,
|
||||
compareSemver,
|
||||
isValidSemver,
|
||||
WORKFLOW_TOOL_FLOOR_VERSION,
|
||||
BACKEND_VALUES,
|
||||
WORKFLOW_RUNTIME,
|
||||
};
|
||||
@@ -62,6 +62,11 @@
|
||||
"sonnet": null,
|
||||
"haiku": null
|
||||
},
|
||||
"zcode": {
|
||||
"opus": null,
|
||||
"sonnet": null,
|
||||
"haiku": null
|
||||
},
|
||||
"augment": {
|
||||
"opus": null,
|
||||
"sonnet": null,
|
||||
|
||||
@@ -317,7 +317,7 @@ Set via `features.*` namespace (e.g., `"features": { "thinking_partner": true }`
|
||||
| Key | Type | Default | Allowed Values | Description |
|
||||
|-----|------|---------|----------------|-------------|
|
||||
| `features.thinking_partner` | boolean | `false` | `true`, `false` | Enable conditional extended thinking at workflow decision points (used by discuss-phase and plan-phase for architectural tradeoff analysis) |
|
||||
| `features.global_learnings` | boolean | `false` | `true`, `false` | Enable injection of global learnings from `~/.gsd/learnings/` into agent prompts |
|
||||
| `features.global_learnings` | boolean | `false` | `true`, `false` | Enable injection of global learnings from `~/.gsd/knowledge/` into agent prompts |
|
||||
|
||||
### Hook Fields
|
||||
|
||||
|
||||
@@ -275,7 +275,7 @@ Two layered gates. Both must pass before proceeding to cleanup.
|
||||
|
||||
Run the deterministic verifier script. Do NOT rely solely on the free-text `verified: yes/no` Hunk Verification Table from Step 4 — bug #2969 traced repeated false-positive `verified: yes` reports to that table being filled in without an actual content-presence check. The script performs the check structurally and exits non-zero on any miss.
|
||||
|
||||
Run the verifier as a child process (the gsd-tools binary directory is not required — the script ships under `gsd-core/bin/` in the source repo and is installed to `${GSD_HOME}/gsd-core/bin/`; it is also exposed via the SDK at `sdk/dist/cli.js verify-reapply` when present):
|
||||
Run the verifier as a child process (the gsd-tools binary directory is not required — the script ships under `gsd-core/bin/` in the source repo and is installed to `${GSD_HOME}/gsd-core/bin/`):
|
||||
|
||||
```bash
|
||||
PRISTINE_DIR="${CONFIG_DIR}/gsd-pristine"
|
||||
|
||||
@@ -372,6 +372,28 @@ reason: "{verbatim user response}"
|
||||
|
||||
Note: Blocked tests do NOT go into the Gaps section (they aren't code issues — they're prerequisite gates).
|
||||
|
||||
**If response indicates a deferred follow-up (NOT a current-phase blocker):**
|
||||
- "later", "future", "follow-up", "next version", "out of scope", "nice to have", "not now", "defer", "down the road", "separate phase", "phase 2"
|
||||
|
||||
These are future-work ideas, not code issues for the current phase. Capture them WITHOUT creating a gap plan (#1921 — a deferred follow-up must never become a blocking gap or spawn a fix plan):
|
||||
|
||||
Update Tests section:
|
||||
```
|
||||
### {N}. {name}
|
||||
expected: {expected}
|
||||
result: skipped
|
||||
reason: "Deferred follow-up: {verbatim user response}"
|
||||
```
|
||||
|
||||
Append to UAT.md `## Deferred Follow-Ups` (create the section if absent):
|
||||
```yaml
|
||||
- test: {N}
|
||||
idea: "{verbatim user response}"
|
||||
deferred_at: {today}
|
||||
```
|
||||
|
||||
Do NOT append to `## Gaps` — deferred follow-ups are not blocking gaps. Continue to the next test.
|
||||
|
||||
**If response is anything else:**
|
||||
- Treat as issue description
|
||||
|
||||
@@ -393,7 +415,8 @@ severity: {inferred}
|
||||
|
||||
Append to Gaps section (structured YAML for plan-phase --gaps):
|
||||
```yaml
|
||||
- truth: "{expected behavior from test}"
|
||||
- gap_id: G-{phase}-{N} # Stable id (phase + test number) — gap-closure plans tag it in their frontmatter so verify-work can reconcile resolved gaps on resume (#1921).
|
||||
truth: "{expected behavior from test}"
|
||||
status: failed
|
||||
reason: "User reported: {verbatim user response}"
|
||||
severity: {inferred}
|
||||
@@ -411,9 +434,35 @@ If more tests remain → Update Current Test, go to `present_test`
|
||||
If no more tests → Go to `complete_session`
|
||||
</step>
|
||||
|
||||
<step name="reconcile_gaps">
|
||||
**Reconcile diagnosed gaps against completed gap-closure plans (#1921):**
|
||||
|
||||
When verify-work resumes after `/gsd:execute-phase --gaps-only`, the UAT `## Gaps` entries still read `status: failed` even though their fix plans have executed. Without reconciliation verify-work re-diagnoses them as fresh blockers and spawns new gap plans — losing the verification state. This step closes the loop.
|
||||
|
||||
Read the UAT `## Gaps` section and the phase dir `*-PLAN.md` frontmatter. For each gap with `status: failed`:
|
||||
1. Find a `*-PLAN.md` whose frontmatter `gap_ids` includes the gap's `gap_id` (`G-{phase}-{N}`).
|
||||
2. If such a plan exists AND has a matching `*-SUMMARY.md` in the phase dir (the plan was executed by `--gaps-only`), the gap is **resolved** — update its YAML in place:
|
||||
```yaml
|
||||
- gap_id: G-{phase}-{N}
|
||||
status: resolved # was: failed
|
||||
resolved_by: {plan basename}
|
||||
resolved_at: {today}
|
||||
```
|
||||
3. If no plan references the `gap_id`, or the plan has no SUMMARY, leave the gap `status: failed` (still open).
|
||||
|
||||
Read plan frontmatter directly in-context — do not pipe it through a shell parser. After reconciliation, announce:
|
||||
```
|
||||
Reconciled gap-closure state: {resolved_count} gap(s) resolved by executed plans, {open_count} still open.
|
||||
```
|
||||
|
||||
Resolved gaps are NOT re-diagnosed and do NOT spawn new gap plans. If the user later reports the same behavior as still broken, treat it as a new issue (a regression) with a fresh `gap_id`.
|
||||
</step>
|
||||
|
||||
<step name="resume_from_file">
|
||||
**Resume testing from UAT file:**
|
||||
|
||||
**First run `reconcile_gaps`** (above) so gaps already fixed by `/gsd:execute-phase --gaps-only` are marked `resolved` before testing resumes (#1921).
|
||||
|
||||
Read the full UAT file.
|
||||
|
||||
Find first test with `result: [pending]`.
|
||||
@@ -652,7 +701,7 @@ Display:
|
||||
|
||||
Spawn gsd-planner in --gaps mode:
|
||||
|
||||
```
|
||||
````
|
||||
Agent(
|
||||
prompt="""
|
||||
<planning_context>
|
||||
@@ -673,13 +722,22 @@ ${AGENT_SKILLS_PLANNER}
|
||||
<downstream_consumer>
|
||||
Output consumed by /gsd:execute-phase
|
||||
Plans must be executable prompts.
|
||||
|
||||
**Gap linkage (#1921):** each created `*-PLAN.md` MUST list the UAT gap ids it addresses in its frontmatter:
|
||||
```yaml
|
||||
---
|
||||
gap_closure: true
|
||||
gap_ids: [G-{phase}-{N}, ...] # the ## Gaps gap_id values this plan fixes
|
||||
---
|
||||
```
|
||||
This lets `/gsd:verify-work` reconcile resolved gaps on resume (a gap whose plan has a matching `*-SUMMARY.md` is marked `status: resolved`, not re-diagnosed as a fresh blocker).
|
||||
</downstream_consumer>
|
||||
""",
|
||||
subagent_type="gsd-planner",
|
||||
model="{planner_model}",
|
||||
description="Plan gap fixes for Phase {phase}"
|
||||
)
|
||||
```
|
||||
````
|
||||
|
||||
> **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@opengsd/gsd-core",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@opengsd/gsd-core",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.84",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opengsd/gsd-core",
|
||||
"version": "1.7.0-rc.2",
|
||||
"version": "1.7.0-rc.3",
|
||||
"description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
|
||||
"main": ".opencode/plugins/gsd-core.js",
|
||||
"bin": {
|
||||
|
||||
77
scripts/gen-golden-install-parity-zcode.cjs
Normal file
77
scripts/gen-golden-install-parity-zcode.cjs
Normal file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
/**
|
||||
* Standalone golden-fixture generator for tests/golden-install-parity.
|
||||
*
|
||||
* This is a BUILD-TIME generation script — NOT a test run. It replicates the
|
||||
* buildParityManifest logic from tests/golden-install-parity.test.cjs and
|
||||
* captures the zcode fixture so the parity test (which the gsd-test gate runs)
|
||||
* has a committed artifact to compare against. The authoritative test gate
|
||||
* remains `gsd-test run`, never a local `node --test`.
|
||||
*
|
||||
* Usage: node scripts/gen-golden-install-parity-zcode.cjs
|
||||
*/
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const { walk, runMinimalInstall, RUNTIME_META } = require(path.join(ROOT, 'tests', 'helpers', 'install-shared.cjs'));
|
||||
const PKG_VERSION = require(path.join(ROOT, 'package.json')).version;
|
||||
const FIXTURE_DIR = path.join(ROOT, 'tests', 'fixtures', 'golden-install-parity');
|
||||
|
||||
const VOLATILE_FILES = new Set([
|
||||
'gsd-file-manifest.json',
|
||||
'gsd-install-state.json',
|
||||
'.gsd-source',
|
||||
'gsd-core/CHANGELOG.md',
|
||||
]);
|
||||
const HOOK_CONFIG_FILES = new Set(['settings.json', 'hooks.json']);
|
||||
const EXCLUDED_PREFIXES = ['gsd-core/bin/lib/'];
|
||||
|
||||
function buildParityManifest(configDir, root) {
|
||||
const allFiles = walk(configDir);
|
||||
const unsorted = {};
|
||||
for (const full of allFiles) {
|
||||
const rel = path.relative(configDir, full).split(path.sep).join('/');
|
||||
if (VOLATILE_FILES.has(rel)) continue;
|
||||
if (HOOK_CONFIG_FILES.has(path.basename(rel))) continue;
|
||||
if (EXCLUDED_PREFIXES.some((p) => rel.startsWith(p))) continue;
|
||||
const content = fs.readFileSync(full);
|
||||
const normalized = content.toString('utf8').split(root).join('<HOME>').split(PKG_VERSION).join('<VERSION>');
|
||||
const hash = crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 16);
|
||||
unsorted[rel] = hash;
|
||||
}
|
||||
const sorted = {};
|
||||
for (const key of Object.keys(unsorted).sort()) sorted[key] = unsorted[key];
|
||||
return sorted;
|
||||
}
|
||||
|
||||
function cleanup(root) {
|
||||
try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* best effort */ }
|
||||
}
|
||||
|
||||
// Regenerate the fixture for every runtime in RUNTIME_META. Needed when a
|
||||
// SHARED gsd-core payload file (e.g. model-catalog.json, capability-registry)
|
||||
// changes content — its hash appears in every runtime's manifest, so all
|
||||
// fixtures must be recaptured together. Usage:
|
||||
// node scripts/gen-golden-install-parity-zcode.cjs [runtime ...]
|
||||
// With no args, regenerates ALL runtimes. With args, only the named runtimes.
|
||||
const targets = process.argv.slice(2).length > 0 ? process.argv.slice(2) : Object.keys(RUNTIME_META);
|
||||
fs.mkdirSync(FIXTURE_DIR, { recursive: true });
|
||||
for (const runtime of targets) {
|
||||
if (!Object.prototype.hasOwnProperty.call(RUNTIME_META, runtime)) {
|
||||
process.stderr.write(`[gen] unknown runtime '${runtime}' (not in RUNTIME_META) — skipping\n`);
|
||||
continue;
|
||||
}
|
||||
const { configDir, root } = runMinimalInstall({ runtime, scope: 'global' });
|
||||
let actual;
|
||||
try {
|
||||
actual = buildParityManifest(configDir, root);
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
const fixturePath = path.join(FIXTURE_DIR, `${runtime}.json`);
|
||||
fs.writeFileSync(fixturePath, JSON.stringify(actual, null, 2) + '\n', 'utf8');
|
||||
process.stdout.write(`[gen] ${runtime}: wrote ${Object.keys(actual).length} file hashes -> ${fixturePath}\n`);
|
||||
}
|
||||
@@ -24,7 +24,7 @@ const PROFILES = [
|
||||
'Researches domain ecosystem before roadmap creation. Produces files in .planning/research/ consumed during roadmap creation. Spawned by /gsd:new-project or /gsd:new-milestone orchestrators.',
|
||||
color: 'cyan',
|
||||
tools:
|
||||
'Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*',
|
||||
'Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*',
|
||||
requiredIncludes: [
|
||||
'@~/.claude/gsd-core/references/research-documentation-lookup.md',
|
||||
'@~/.claude/gsd-core/references/research-philosophy.md',
|
||||
@@ -46,7 +46,7 @@ const PROFILES = [
|
||||
'Researches how to implement a phase before planning. Produces RESEARCH.md consumed by gsd-planner. Spawned by /gsd:plan-phase orchestrator.',
|
||||
color: 'cyan',
|
||||
tools:
|
||||
'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*',
|
||||
'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*',
|
||||
requiredIncludes: [
|
||||
'@~/.claude/gsd-core/references/research-documentation-lookup.md',
|
||||
'@~/.claude/gsd-core/references/research-philosophy.md',
|
||||
@@ -68,7 +68,7 @@ const PROFILES = [
|
||||
description:
|
||||
'Researches a single gray area decision and returns a structured comparison table with rationale. Spawned by discuss-phase advisor mode.',
|
||||
color: 'cyan',
|
||||
tools: 'Read, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*',
|
||||
tools: 'Read, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*',
|
||||
requiredIncludes: [
|
||||
'@~/.claude/gsd-core/references/research-documentation-lookup.md',
|
||||
],
|
||||
@@ -84,7 +84,7 @@ const PROFILES = [
|
||||
'Researches a chosen AI framework\'s official docs to produce implementation-ready guidance — best practices, syntax, core patterns, and pitfalls distilled for the specific use case. Writes the Framework Quick Reference and Implementation Guidance sections of AI-SPEC.md. Spawned by /gsd:ai-integration-phase orchestrator.',
|
||||
color: 'green',
|
||||
tools:
|
||||
'Read, Write, Edit, Bash, Grep, Glob, WebFetch, WebSearch, mcp__context7__*',
|
||||
'Read, Write, Edit, Bash, Grep, Glob, WebFetch, WebSearch, mcp__context7__*, mcp__plugin_context7_context7__*',
|
||||
requiredIncludes: [
|
||||
'@~/.claude/gsd-core/references/research-documentation-lookup.md',
|
||||
],
|
||||
@@ -101,7 +101,7 @@ const PROFILES = [
|
||||
'Researches the business domain and real-world application context of the AI system being built. Surfaces domain expert evaluation criteria, industry-specific failure modes, regulatory context, and what "good" looks like for practitioners in this field — before the eval-planner turns it into measurable rubrics. Spawned by /gsd:ai-integration-phase orchestrator.',
|
||||
color: 'purple',
|
||||
tools:
|
||||
'Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*',
|
||||
'Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*',
|
||||
requiredIncludes: [
|
||||
'@~/.claude/gsd-core/references/research-documentation-lookup.md',
|
||||
],
|
||||
@@ -117,7 +117,7 @@ const PROFILES = [
|
||||
'Produces UI-SPEC.md design contract for frontend phases. Reads upstream artifacts, detects design system state, asks only unanswered questions. Spawned by /gsd:ui-phase orchestrator.',
|
||||
color: 'purple',
|
||||
tools:
|
||||
'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
|
||||
'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
|
||||
requiredIncludes: [
|
||||
'@~/.claude/gsd-core/references/research-documentation-lookup.md',
|
||||
],
|
||||
|
||||
@@ -570,11 +570,13 @@ function main() {
|
||||
// progress (verified: no leaked handle / hang; --test-force-exit exits leaks
|
||||
// cleanly, so the timeout was pure slowness, NOT the leak the kill message guesses).
|
||||
// The per-chunk timeout is sized for a "healthy chunk (~4-5 min)"; keep chunks at
|
||||
// roughly half a shard so each gets its own fresh 600s budget and a fresh node
|
||||
// process (also relieving per-process memory pressure from 170+ files at once).
|
||||
// roughly a third of a shard so each gets its own fresh 600s budget and a fresh
|
||||
// node process (also relieving per-process memory pressure from 170+ files at once).
|
||||
// Lowered from 90 to 60 after #1575 — macOS Node 22 shard 2/3 chunk 2 (~80 files
|
||||
// including state.test.cjs, perf-*, worktree-cleanup) exceeded 600s with 90.
|
||||
const MAX_FILES_PER_CHUNK = process.env.RUN_TESTS_MAX_FILES_PER_CHUNK
|
||||
? Number(process.env.RUN_TESTS_MAX_FILES_PER_CHUNK)
|
||||
: 90;
|
||||
: 60;
|
||||
|
||||
// node:test does not exit until the event loop drains. A unit test that leaks
|
||||
// an open handle (un-terminated Worker, un-killed child_process, ref'd timer)
|
||||
|
||||
@@ -46,7 +46,7 @@ const { loadConfig } = configLoaderMod;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import installProfilesMod = require('./install-profiles.cjs');
|
||||
const { readActiveProfile, loadSkillsManifest, resolveProfile, parseRequires } = installProfilesMod;
|
||||
const { readActiveProfile, loadSkillsManifest, resolveProfile, parseRequires, parseCallsAgents } = installProfilesMod;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import surfaceMod = require('./surface.cjs');
|
||||
@@ -384,22 +384,84 @@ function _loadInstalledSkillsManifest(configDir: string): Map<string, string[]>
|
||||
return manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* #1858 — Build a skill dependency manifest from a FLAT commands/gsd-<stem>.md
|
||||
* source layout (the Claude local project install shape, where the `gsd-`
|
||||
* prefix is baked into each filename at the commands/ level and there is no
|
||||
* commands/gsd/ subdir). Strips the `gsd-` prefix so stems match the nested
|
||||
* loader's output (gsd-validate-phase.md → validate-phase, same as nested
|
||||
* validate-phase.md).
|
||||
*
|
||||
* Map shape is identical to loadSkillsManifest: each stem maps to its
|
||||
* `requires` deps (parsed via the same shared parseRequires) and carries a
|
||||
* companion `_calls_agents_<stem>` key (parsed via parseCallsAgents) so the
|
||||
* flat and nested paths cannot drift.
|
||||
*
|
||||
* Returns an empty Map when the parent directory does not exist or contains
|
||||
* no gsd-*.md files (so _resolveManifest can use size>0 as the "flat layout
|
||||
* present" signal and fall through to the installed-skills branch otherwise).
|
||||
*/
|
||||
function _loadFlatCommandsGsdManifest(commandsParentDir: string): Map<string, string[]> {
|
||||
const manifest = new Map<string, string[]>();
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(commandsParentDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return manifest;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) continue;
|
||||
if (!entry.name.startsWith('gsd-')) continue;
|
||||
if (!entry.name.endsWith('.md')) continue;
|
||||
// Strip 'gsd-' prefix (4 chars) and '.md' suffix (3 chars) → stem.
|
||||
const stem = entry.name.slice(4, -3);
|
||||
if (!stem) continue;
|
||||
// Mirror loadSkillsManifest's try/catch structure exactly: wrap read +
|
||||
// parse + set together so an unreadable file OR a thrown parser degrades
|
||||
// both keys to [] (parity; closes the latent catch-scope drift a reviewer
|
||||
// flagged — both parsers are non-throwing today, but the structural
|
||||
// match future-proofs the "identical Map shape" contract).
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(commandsParentDir, entry.name), 'utf8');
|
||||
manifest.set(stem, parseRequires(content));
|
||||
manifest.set(`_calls_agents_${stem}`, parseCallsAgents(content));
|
||||
} catch {
|
||||
manifest.set(stem, []);
|
||||
manifest.set(`_calls_agents_${stem}`, []);
|
||||
}
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the skill dependency manifest for capability-state resolution.
|
||||
*
|
||||
* Resolution order (fixes #1160 — installed-runtime capability surface):
|
||||
* 1. If commandsGsdDir exists, load from source (repo-checkout behavior).
|
||||
* 2. Otherwise, fall back to installed skills at configDir/skills/gsd-[stem]/SKILL.md.
|
||||
* Resolution order:
|
||||
* 1. If commandsGsdDir exists, load from the nested source layout
|
||||
* (repo-checkout behavior: <repo>/commands/gsd/*.md).
|
||||
* 2. #1858 — otherwise, if the flat source layout is present (gsd-<stem>.md
|
||||
* files in dirname(commandsGsdDir)), load from there. This is the Claude
|
||||
* local project install shape where commands/gsd/ does not exist but
|
||||
* commands/gsd-<stem>.md files do.
|
||||
* 3. #1160 — otherwise, fall back to installed skills at
|
||||
* configDir/skills/gsd-[stem]/SKILL.md.
|
||||
*
|
||||
* In an installed runtime the commands/gsd source tree is absent; only the
|
||||
* skills/ layout exists. Returning an empty manifest caused resolveSurface to
|
||||
* materialise the full-sentinel to an empty Set, making every capability appear
|
||||
* unsurfaced even when the skill was physically installed.
|
||||
* In an installed runtime both source trees are absent; only the skills/
|
||||
* layout exists. Returning an empty manifest caused resolveSurface to
|
||||
* materialise the full-sentinel to an empty Set, making every skill-bearing
|
||||
* capability appear unsurfaced even when the skill was physically installed
|
||||
* (#1160) or authored as a flat command file (#1858).
|
||||
*/
|
||||
function _resolveManifest(commandsGsdDir: string, configDir: string): Map<string, string[]> {
|
||||
if (fs.existsSync(commandsGsdDir)) {
|
||||
return loadSkillsManifest(commandsGsdDir);
|
||||
}
|
||||
// #1858: flat source layout — gsd-<stem>.md files at dirname(commandsGsdDir).
|
||||
// Only claim the flat branch when it actually has gsd-*.md files; otherwise
|
||||
// fall through to the installed-skills branch (a commands/ dir with no gsd
|
||||
// files must not shadow an installed skills/ tree).
|
||||
const flat = _loadFlatCommandsGsdManifest(path.dirname(commandsGsdDir));
|
||||
if (flat.size > 0) return flat;
|
||||
return _loadInstalledSkillsManifest(configDir);
|
||||
}
|
||||
|
||||
@@ -619,6 +681,7 @@ export = {
|
||||
// Exported for tests
|
||||
_resolveCommandsGsdDir,
|
||||
_loadInstalledSkillsManifest,
|
||||
_loadFlatCommandsGsdManifest,
|
||||
_resolveManifest,
|
||||
_isSafePropKey,
|
||||
};
|
||||
|
||||
@@ -92,7 +92,7 @@ interface DesiredCapability {
|
||||
}
|
||||
|
||||
interface SetCapabilityStateOptions {
|
||||
materialize?: { runtime: string; scope: string };
|
||||
materialize?: { runtime: string; scope: string; resolveAttribution?: (runtime: string) => string | null | undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -352,8 +352,18 @@ function setCapabilityState(
|
||||
const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, resolvedConfigDir, scope);
|
||||
const commandsGsdDir = _resolveCommandsGsdDir();
|
||||
const manifest = _resolveManifest(commandsGsdDir, resolvedConfigDir);
|
||||
// #1575: applySurface now accepts opts.resolveAttribution so surface-path
|
||||
// agents get the same Co-Authored-By trailer as the install path. The
|
||||
// resolver is not threaded here yet — the CLI command handler does not have
|
||||
// access to getCommitAttribution (which lives in bin/install.js). Until that
|
||||
// is refactored into a shared module, surface-path agents for descriptor-
|
||||
// driven runtimes will lack the Co-Authored-By trailer that install adds.
|
||||
// Parity is proven when resolveAttribution IS provided (see
|
||||
// tests/issue-1575-agent-descriptor-parity.test.cjs).
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
applySurface(resolvedConfigDir, layout, manifest, undefined, registry);
|
||||
applySurface(resolvedConfigDir, layout, manifest, undefined, registry, opts?.materialize?.resolveAttribution
|
||||
? { resolveAttribution: opts.materialize.resolveAttribution }
|
||||
: undefined);
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
// Fix C: materialise was explicitly requested — a failure is an error (non-zero exit),
|
||||
|
||||
159
src/claude-orchestration-command-router.cts
Normal file
159
src/claude-orchestration-command-router.cts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Claude orchestration command router — CLI dispatcher for
|
||||
* `gsd-tools claude-orchestration <subcommand>`.
|
||||
*
|
||||
* #1143 — thin CLI adapter over the pure `claude-orchestration.cjs` module.
|
||||
* Lets execute-phase (or any orchestrator) invoke the Workflow-backend
|
||||
* detection and the Workflow-script emitter through the standard capability
|
||||
* command surface (ADR-959) instead of a bare `require()`.
|
||||
*
|
||||
* Router signature: { args, cwd, raw, error } — identical to the other host
|
||||
* routers; discovered by dispatchCapabilityCommand via the registry's
|
||||
* commandFamilies index.
|
||||
*
|
||||
* Subcommands:
|
||||
* detect-backend [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch]
|
||||
* Resolves whether the Workflow backend should activate. `--runtime`
|
||||
* defaults to the GSD_RUNTIME env var (or 'unknown'). Reads the
|
||||
* `claude_orchestration.*` keys from .planning/config.json. Emits
|
||||
* { available, backend, reason }.
|
||||
*
|
||||
* emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>]
|
||||
* Reads a wave/plan manifest JSON file and emits the generated Workflow
|
||||
* script + summary. The manifest shape matches emitWorkflowScript's input:
|
||||
* { waves: [{ id, plans: [{ id, brief, files_modified: string[] }] }] }.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import io = require('./io.cjs');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import core = require('./claude-orchestration.cjs');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import configLoader = require('./config-loader.cjs');
|
||||
|
||||
const { output } = io;
|
||||
const { detectWorkflowBackend, emitWorkflowScript } = core;
|
||||
|
||||
const CAPABLE_HOST = { dispatch: { nested: true, background: true } };
|
||||
|
||||
interface RouterOpts {
|
||||
args: string[];
|
||||
cwd: string;
|
||||
raw: boolean;
|
||||
error: (msg: string, reason?: string) => void;
|
||||
}
|
||||
|
||||
function usage(error: (msg: string, reason?: string) => void): void {
|
||||
error(
|
||||
'Usage: gsd-tools claude-orchestration <detect-backend|emit-workflow> [...]\n' +
|
||||
' detect-backend [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch]\n' +
|
||||
' emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>]',
|
||||
);
|
||||
}
|
||||
|
||||
function argValue(args: string[], flag: string): string | undefined {
|
||||
const i = args.indexOf(flag);
|
||||
return i !== -1 && i + 1 < args.length ? args[i + 1] : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether the Workflow backend should activate for the current/given
|
||||
* runtime. Reads `claude_orchestration.*` from the project config; runtime and
|
||||
* SDK version come from flags (the orchestrator already knows these) or env.
|
||||
*/
|
||||
function cmdDetectBackend(args: string[], cwd: string, raw: boolean): void {
|
||||
const runtimeId = argValue(args, '--runtime') || process.env['GSD_RUNTIME'] || 'unknown';
|
||||
const agentSdkVersion = argValue(args, '--agent-sdk-version');
|
||||
const noNested = args.includes('--no-nested-dispatch');
|
||||
const hostIntegration = noNested ? { dispatch: { nested: false, background: true } } : CAPABLE_HOST;
|
||||
|
||||
// Resolve the claude_orchestration.* slice from the project config (federated
|
||||
// keys are merged by loadConfig as a nested object). A config read failure
|
||||
// degrades to inline — it must not break the core loop.
|
||||
let claudeSlice: Record<string, unknown> = {};
|
||||
try {
|
||||
const loaded = configLoader.loadConfig(cwd);
|
||||
const slice = loaded['claude_orchestration'];
|
||||
if (slice && typeof slice === 'object' && !Array.isArray(slice)) {
|
||||
claudeSlice = slice as Record<string, unknown>;
|
||||
}
|
||||
} catch {
|
||||
claudeSlice = {};
|
||||
}
|
||||
|
||||
// Flatten the nested slice into the dotted-key shape detectWorkflowBackend expects.
|
||||
const flatConfig: Record<string, unknown> = {};
|
||||
for (const k of Object.keys(claudeSlice)) {
|
||||
flatConfig['claude_orchestration.' + k] = claudeSlice[k];
|
||||
}
|
||||
|
||||
const result = detectWorkflowBackend({ runtimeId, hostIntegration, config: flatConfig, agentSdkVersion });
|
||||
output(result, raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a Workflow script from a wave/plan manifest file.
|
||||
*/
|
||||
function cmdEmitWorkflow(args: string[], _cwd: string, raw: boolean, error: (msg: string, reason?: string) => void): void {
|
||||
const wavesPath = argValue(args, '--waves');
|
||||
const runId = argValue(args, '--run-id');
|
||||
const phaseDir = argValue(args, '--phase-dir') || '.planning/phases/current';
|
||||
const budgetRaw = argValue(args, '--budget');
|
||||
|
||||
if (!wavesPath) {
|
||||
error('emit-workflow requires --waves <path>');
|
||||
return;
|
||||
}
|
||||
if (!runId) {
|
||||
error('emit-workflow requires --run-id <id>');
|
||||
return;
|
||||
}
|
||||
|
||||
let waves: unknown;
|
||||
try {
|
||||
const content = fs.readFileSync(path.resolve(wavesPath), 'utf8');
|
||||
const parsed = JSON.parse(content) as Record<string, unknown>;
|
||||
waves = parsed['waves'];
|
||||
} catch (e) {
|
||||
error('emit-workflow: could not read/parse --waves file "' + wavesPath + '": ' + (e instanceof Error ? e.message : String(e)));
|
||||
return;
|
||||
}
|
||||
|
||||
const budgetTokens = budgetRaw !== undefined ? parseInt(budgetRaw, 10) : undefined;
|
||||
const budget = (typeof budgetTokens === 'number' && !Number.isNaN(budgetTokens)) ? budgetTokens : undefined;
|
||||
|
||||
const result = emitWorkflowScript({
|
||||
phaseDir,
|
||||
runId,
|
||||
waves: waves as EmitInput['waves'],
|
||||
budgetTokens: budget,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
error('emit-workflow: ' + result.reason);
|
||||
return;
|
||||
}
|
||||
output({ script: result.script, summary: result.summary }, raw);
|
||||
}
|
||||
|
||||
// Re-declared minimal input type for the cast above (avoids importing private types).
|
||||
interface EmitInput {
|
||||
waves: Array<{ id: string; plans: Array<{ id: string; brief: string; files_modified: string[] }> }>;
|
||||
}
|
||||
|
||||
function routeClaudeOrchestrationCommand(opts: RouterOpts): void {
|
||||
const { args, cwd, raw, error } = opts;
|
||||
// args[0] is the family ('claude-orchestration'); the subcommand is args[1].
|
||||
const subcommand = args[1];
|
||||
if (subcommand === 'detect-backend') {
|
||||
cmdDetectBackend(args, cwd, raw);
|
||||
} else if (subcommand === 'emit-workflow') {
|
||||
cmdEmitWorkflow(args, cwd, raw, error);
|
||||
} else {
|
||||
usage(error);
|
||||
}
|
||||
}
|
||||
|
||||
export = { routeClaudeOrchestrationCommand };
|
||||
485
src/claude-orchestration.cts
Normal file
485
src/claude-orchestration.cts
Normal file
@@ -0,0 +1,485 @@
|
||||
/**
|
||||
* Claude Orchestration Capability — Workflow-tool backend detection + emitter
|
||||
*
|
||||
* #1143 — adopts Claude Code's Workflow tool (the engine behind `/effort ultracode`)
|
||||
* as an optional, runtime-gated parallel-execution backend for the GSD loop.
|
||||
*
|
||||
* This module is the pure, testable core of the capability. It owns two seams:
|
||||
*
|
||||
* detectWorkflowBackend({ runtimeId, hostIntegration, config, agentSdkVersion })
|
||||
* → { available: boolean, backend: 'workflow'|'inline', reason: string }
|
||||
* Fail-closed: every miss degrades to `inline` (today's behaviour), so the
|
||||
* core loop is byte-identical unless every gate opens. This is criteria 3 + 6.
|
||||
*
|
||||
* emitWorkflowScript({ phaseDir, waves, runId, budgetTokens? })
|
||||
* → { ok:true, script, summary } | { ok:false, reason }
|
||||
* Maps GSD's wave/plan model 1:1 onto Workflow primitives:
|
||||
* wave → sequential `parallel()` stage barriers,
|
||||
* plan → `agent(brief, { agentType:'gsd-executor', isolation:'worktree' })`,
|
||||
* files_modified overlap → forces plans into separate sequential stages
|
||||
* (the same overlap rule execute-phase already applies inline),
|
||||
* resumeFromRunId → wired to the phase run id,
|
||||
* budgetTokens → a shared token pool.
|
||||
* The emitted script composes the SAME gsd-executor agent and worktree
|
||||
* isolation the inline path uses, so it produces the same artifacts/commits
|
||||
* (criterion 2). It is a generated string consumed by the orchestrator; this
|
||||
* module never invokes the Workflow tool itself.
|
||||
*
|
||||
* Design laws:
|
||||
* - Gall's Law: ship a small working slice that composes existing primitives
|
||||
* (gsd-executor + worktree isolation) rather than reinventing them.
|
||||
* - Greenspun's Tenth Rule (cited in #1143): adopt the Workflow tool's
|
||||
* barrier/pipeline/budget/resume semantics instead of hand-rolling them.
|
||||
* - Postel's Law: liberal in input (missing fields → inline), conservative in
|
||||
* output (workflow only when every gate opens).
|
||||
* - Fail-closed: an unknown version, a missing descriptor, or a disabled
|
||||
* toggle all resolve to `inline`, never to `workflow`.
|
||||
*
|
||||
* Zero external dependencies. Pure functions. Never throws on bad input.
|
||||
*/
|
||||
|
||||
// ─── Constants ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* The Agent SDK version that introduced the Workflow tool (#1143 prior art).
|
||||
* Used as the default floor when config does not override it. A runtime reporting
|
||||
* an agentSdkVersion below this cannot host the Workflow backend.
|
||||
*/
|
||||
const WORKFLOW_TOOL_FLOOR_VERSION = '0.3.149';
|
||||
|
||||
/** Closed enum for the `claude_orchestration.execution_backend` config key. */
|
||||
const BACKEND_VALUES = new Set<string>(['auto', 'workflow', 'inline']);
|
||||
|
||||
/** Only this runtime can host the Workflow tool (Claude Code / Agent SDK). */
|
||||
const WORKFLOW_RUNTIME = 'claude';
|
||||
|
||||
// ─── Semver helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
/** Official-ish strict SemVer 2.0.0 numeric triple (+ optional pre/build). */
|
||||
const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
||||
|
||||
/** True for a syntactically valid semver string. */
|
||||
function isValidSemver(s: unknown): s is string {
|
||||
return typeof s === 'string' && SEMVER_RE.test(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two semver strings.
|
||||
* Returns -1/0/1 in the usual sense. Garbage in either position → -1 (fail-closed:
|
||||
* an unparseable version is treated as "less than" any real floor, so detection
|
||||
* never accidentally enables the preview backend on an unknown SDK).
|
||||
*
|
||||
* Pre-release/build metadata are ignored for the comparison — only the numeric
|
||||
* major.minor.patch triple participates, matching how the Workflow-tool floor is
|
||||
* specified (a plain "0.3.149").
|
||||
*/
|
||||
function compareSemver(a: string, b: string): number {
|
||||
if (!isValidSemver(a) || !isValidSemver(b)) return -1;
|
||||
// Split numeric triple from pre-release/build metadata.
|
||||
const parseTriple = (s: string): number[] => {
|
||||
const core = s.split('-')[0].split('+')[0].split('.');
|
||||
return [parseInt(core[0], 10), parseInt(core[1], 10), parseInt(core[2], 10)];
|
||||
};
|
||||
const hasPre = (s: string): boolean => s.indexOf('-') !== -1;
|
||||
const preIdentifiers = (s: string): string[] => (s.split('-')[1] || '').split('+')[0].split('.').filter((x) => x.length > 0);
|
||||
const am = parseTriple(a);
|
||||
const bm = parseTriple(b);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (am[i] < bm[i]) return -1;
|
||||
if (am[i] > bm[i]) return 1;
|
||||
}
|
||||
// Numeric triple is equal. SemVer 2.0.0 §11 precedence:
|
||||
// - a version WITH a pre-release tag is LOWER than the same triple WITHOUT one
|
||||
// (keeps the floor fail-closed for pre-release builds of the GA floor);
|
||||
// - two pre-releases of the same triple are ordered by their dot-separated
|
||||
// identifiers (numeric < alphanumeric; numeric compared numerically,
|
||||
// alphanumeric lexically; fewer identifiers < more).
|
||||
const aPre = hasPre(a);
|
||||
const bPre = hasPre(b);
|
||||
if (aPre && !bPre) return -1;
|
||||
if (!aPre && bPre) return 1;
|
||||
if (aPre && bPre) {
|
||||
const ai = preIdentifiers(a);
|
||||
const bi = preIdentifiers(b);
|
||||
const len = Math.min(ai.length, bi.length);
|
||||
for (let i = 0; i < len; i++) {
|
||||
const ax = ai[i];
|
||||
const bx = bi[i];
|
||||
const aNum = /^\d+$/.test(ax);
|
||||
const bNum = /^\d+$/.test(bx);
|
||||
if (aNum && bNum) {
|
||||
const an = parseInt(ax, 10);
|
||||
const bn = parseInt(bx, 10);
|
||||
if (an < bn) return -1;
|
||||
if (an > bn) return 1;
|
||||
} else if (aNum && !bNum) {
|
||||
return -1; // numeric identifiers always lower than alphanumeric
|
||||
} else if (!aNum && bNum) {
|
||||
return 1;
|
||||
} else {
|
||||
if (ax < bx) return -1;
|
||||
if (ax > bx) return 1;
|
||||
}
|
||||
}
|
||||
if (ai.length < bi.length) return -1;
|
||||
if (ai.length > bi.length) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ─── detectWorkflowBackend ────────────────────────────────────────────────────
|
||||
|
||||
interface HostIntegration {
|
||||
dispatch?: {
|
||||
nested?: boolean;
|
||||
background?: boolean;
|
||||
backgroundDispatch?: boolean;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
interface BackendConfig {
|
||||
'claude_orchestration.enabled'?: unknown;
|
||||
'claude_orchestration.execution_backend'?: unknown;
|
||||
'claude_orchestration.min_agent_sdk_version'?: unknown;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
interface DetectInput {
|
||||
runtimeId?: string;
|
||||
hostIntegration?: HostIntegration | null;
|
||||
config?: BackendConfig | null;
|
||||
agentSdkVersion?: string;
|
||||
}
|
||||
|
||||
interface DetectResult {
|
||||
available: boolean;
|
||||
backend: 'workflow' | 'inline';
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/** Inline result shorthand. */
|
||||
function inline(reason: string, available = false): DetectResult {
|
||||
return { available, backend: 'inline', reason };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve whether the Workflow-tool backend should activate.
|
||||
*
|
||||
* Gate ladder (all must pass for `workflow`; first miss wins, fail-closed):
|
||||
* 1. capability enabled (claude_orchestration.enabled truthy)
|
||||
* 2. runtime is Claude (the only runtime that exposes the Workflow tool)
|
||||
* 3. execution_backend !== 'inline'
|
||||
* 4. host descriptor signals nested+background dispatch (Workflow-tool capable)
|
||||
* 5. agentSdkVersion is a known, valid semver
|
||||
* 6. agentSdkVersion >= the configured floor (default WORKFLOW_TOOL_FLOOR_VERSION)
|
||||
* 7. execution_backend === 'workflow' OR 'auto' (both reach here; 'inline' exited at 3)
|
||||
*
|
||||
* Never throws. Destructures defensively.
|
||||
*/
|
||||
function detectWorkflowBackend(input: DetectInput | null | undefined): DetectResult {
|
||||
if (input === null || input === undefined || typeof input !== 'object') {
|
||||
return inline('capability_disabled');
|
||||
}
|
||||
|
||||
const cfg: BackendConfig =
|
||||
(input.config !== null && input.config !== undefined && typeof input.config === 'object')
|
||||
? input.config
|
||||
: {};
|
||||
|
||||
// 1. capability must be opted in (default-off — ships disabled).
|
||||
if (!cfg['claude_orchestration.enabled']) {
|
||||
return inline('capability_disabled');
|
||||
}
|
||||
|
||||
// 2. only Claude can host the Workflow tool.
|
||||
if (input.runtimeId !== WORKFLOW_RUNTIME) {
|
||||
return inline('runtime_not_claude');
|
||||
}
|
||||
|
||||
// 3. explicit inline opt-out short-circuits.
|
||||
let backendRaw = cfg['claude_orchestration.execution_backend'];
|
||||
if (typeof backendRaw !== 'string' || !BACKEND_VALUES.has(backendRaw)) {
|
||||
backendRaw = 'auto';
|
||||
}
|
||||
if (backendRaw === 'inline') {
|
||||
return inline('backend_inline');
|
||||
}
|
||||
|
||||
// 4. the host dispatch descriptor must be the nesting-capable Claude-Code shape
|
||||
// (a proxy for Workflow-tool presence). This is Claude-specific and already
|
||||
// gated at step 2; `background:true` alone is true on several non-Claude hosts,
|
||||
// so the proxy is only meaningful after the runtime check above. Note: this is
|
||||
// NOT the canonical `shouldFlattenDispatch` rule (which keys on
|
||||
// `backgroundDispatch`); the Workflow backend works precisely because a single
|
||||
// tool-call orchestrates internally, sidestepping the backgroundDispatch:false
|
||||
// limitation. Missing/false/foreign descriptor → fail-closed.
|
||||
const hi = input.hostIntegration;
|
||||
if (hi === null || hi === undefined || typeof hi !== 'object' || Array.isArray(hi)) {
|
||||
return inline('workflow_tool_unavailable');
|
||||
}
|
||||
const dispatch = (hi as { dispatch?: Record<string, unknown> }).dispatch;
|
||||
if (typeof dispatch !== 'object' || dispatch === null || Array.isArray(dispatch)) {
|
||||
return inline('workflow_tool_unavailable');
|
||||
}
|
||||
const nested = dispatch['nested'];
|
||||
const background = dispatch['background'];
|
||||
if (nested !== true || background !== true) {
|
||||
return inline('workflow_tool_unavailable');
|
||||
}
|
||||
|
||||
// 5. an unknown agentSdkVersion cannot be trusted to meet the floor.
|
||||
if (!isValidSemver(input.agentSdkVersion)) {
|
||||
return inline('agent_sdk_version_unknown');
|
||||
}
|
||||
|
||||
// 6. version floor (config override > default constant).
|
||||
const floorRaw = cfg['claude_orchestration.min_agent_sdk_version'];
|
||||
const floor = typeof floorRaw === 'string' && isValidSemver(floorRaw) ? floorRaw : WORKFLOW_TOOL_FLOOR_VERSION;
|
||||
if (compareSemver(input.agentSdkVersion, floor) < 0) {
|
||||
return inline('agent_sdk_version_below_floor');
|
||||
}
|
||||
|
||||
// 7. auto/workflow both reach the workflow backend once every gate passes.
|
||||
return { available: true, backend: 'workflow', reason: 'workflow_backend_active' };
|
||||
}
|
||||
|
||||
// ─── emitWorkflowScript ───────────────────────────────────────────────────────
|
||||
|
||||
interface Plan {
|
||||
id: string;
|
||||
brief: string;
|
||||
files_modified: string[];
|
||||
}
|
||||
|
||||
interface Wave {
|
||||
id: string;
|
||||
plans: Plan[];
|
||||
}
|
||||
|
||||
interface EmitInput {
|
||||
phaseDir: string;
|
||||
waves: Wave[];
|
||||
runId: string;
|
||||
budgetTokens?: number;
|
||||
}
|
||||
|
||||
interface EmitOk {
|
||||
ok: true;
|
||||
script: string;
|
||||
summary: {
|
||||
waves: number;
|
||||
plans: number;
|
||||
stagesByWave: string[][][]; // wave → stage → planId[]
|
||||
resumeRunId: string;
|
||||
budgetTokens: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface EmitErr {
|
||||
ok: false;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Partition a wave's plans into a near-minimal number of sequential stages (via
|
||||
* greedy first-fit — not guaranteed optimal for arbitrary overlap graphs, but
|
||||
* correct: no two plans sharing a file ever cohabit a stage) such that no two
|
||||
* plans in the same stage share a modified file. Each plan goes into the earliest
|
||||
* stage where it does not overlap any plan already there.
|
||||
*
|
||||
* A plan with an EMPTY files_modified set declares no files; it overlaps nothing
|
||||
* and coalesces into stage 0 (same behavior as the inline path, which also cannot
|
||||
* guard against undeclared concurrent writes — declare filesModified accurately).
|
||||
*
|
||||
* This is the same overlap rule execute-phase applies inline — the only difference
|
||||
* is the execution vehicle (Workflow `parallel()` vs one-agent-per-message).
|
||||
*/
|
||||
function partitionStages(plans: Plan[]): string[][] {
|
||||
const stages: { plans: Plan[]; files: Set<string> }[] = [];
|
||||
for (const plan of plans) {
|
||||
const fileSet = new Set(plan.files_modified);
|
||||
let placed = false;
|
||||
for (const stage of stages) {
|
||||
let overlap = false;
|
||||
for (const f of fileSet) {
|
||||
if (stage.files.has(f)) { overlap = true; break; }
|
||||
}
|
||||
if (!overlap) {
|
||||
stage.plans.push(plan);
|
||||
for (const f of fileSet) stage.files.add(f);
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!placed) {
|
||||
stages.push({ plans: [plan], files: new Set(fileSet) });
|
||||
}
|
||||
}
|
||||
return stages.map((s) => s.plans.map((p) => p.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote a free-text value for safe embedding as a JavaScript/Workflow double-quoted
|
||||
* string literal. Uses JSON.stringify so every JS-relevant escape (backslash, quote,
|
||||
* newline, tab, NUL, U+2028/U+2029, all control chars) is handled by the language
|
||||
* itself — there is no hand-rolled escape table to drift. Returns the value already
|
||||
* wrapped in its surrounding quotes.
|
||||
*/
|
||||
function quoteString(s: string): string {
|
||||
return JSON.stringify(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if `s` is a safe identifier/path token to interpolate into the generated
|
||||
* script WITHOUT requiring a string-literal context — i.e. it contains no
|
||||
* character that could terminate a comment line (`\n`/`\r`), break out of a
|
||||
* string literal (`"` / `\`), or smuggle a NUL/control sequence. Used for
|
||||
* `phaseDir`, `runId`, `wave.id`, and `plan.id`, which are identifiers/paths and
|
||||
* must never legitimately contain such characters. Rejecting them at validation
|
||||
* (rather than silently flattening) keeps the emitted script faithful to input.
|
||||
*/
|
||||
const UNSCRIPTABLE_CHAR_RE = /[\r\n"\\\x00-\x1f\x7f\u2028\u2029]/;
|
||||
function isScriptableIdentifier(s: unknown): boolean {
|
||||
if (typeof s !== 'string' || s.length === 0) return false;
|
||||
return !UNSCRIPTABLE_CHAR_RE.test(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a Workflow script mapping the phase's wave/plan model onto Workflow
|
||||
* primitives. Pure and deterministic: identical input yields an identical string.
|
||||
*
|
||||
* Returns ok:false (never throws) on invalid input — empty waves, missing runId,
|
||||
* a wave with no plans, etc.
|
||||
*/
|
||||
function emitWorkflowScript(input: EmitInput | null | undefined): EmitOk | EmitErr {
|
||||
if (input === null || input === undefined || typeof input !== 'object') {
|
||||
return { ok: false, reason: 'invalid_input' };
|
||||
}
|
||||
const { phaseDir, waves, runId } = input;
|
||||
// Identifiers/paths interpolated into the generated script must be free of any
|
||||
// character that could terminate a comment, break out of a string literal, or
|
||||
// smuggle control bytes — reject up front (security: #1143 review Finding 1).
|
||||
if (!isScriptableIdentifier(phaseDir)) {
|
||||
return { ok: false, reason: 'phaseDir must be a non-empty string without newlines/quotes/backslash/control chars' };
|
||||
}
|
||||
if (!isScriptableIdentifier(runId)) {
|
||||
return { ok: false, reason: 'runId must be a non-empty string without newlines/quotes/backslash/control chars' };
|
||||
}
|
||||
if (!Array.isArray(waves) || waves.length === 0) {
|
||||
return { ok: false, reason: 'waves must be a non-empty array' };
|
||||
}
|
||||
for (let i = 0; i < waves.length; i++) {
|
||||
const w = waves[i];
|
||||
if (w === null || typeof w !== 'object' || typeof w.id !== 'string') {
|
||||
return { ok: false, reason: 'waves[' + i + '] must be { id, plans: non-empty[] }' };
|
||||
}
|
||||
if (!isScriptableIdentifier(w.id)) {
|
||||
return { ok: false, reason: 'waves[' + i + '].id must not contain newlines/quotes/backslash/control chars' };
|
||||
}
|
||||
if (!Array.isArray(w.plans) || w.plans.length === 0) {
|
||||
return { ok: false, reason: 'waves[' + i + '] must have a non-empty plans array' };
|
||||
}
|
||||
const seenIds = new Set<string>();
|
||||
for (let j = 0; j < w.plans.length; j++) {
|
||||
const p = w.plans[j];
|
||||
if (p === null || typeof p !== 'object' || typeof p.id !== 'string' || typeof p.brief !== 'string' || !Array.isArray(p.files_modified)) {
|
||||
return { ok: false, reason: 'waves[' + i + '].plans[' + j + '] must be { id, brief, files_modified[] }' };
|
||||
}
|
||||
if (!isScriptableIdentifier(p.id)) {
|
||||
return { ok: false, reason: 'waves[' + i + '].plans[' + j + '].id must not contain newlines/quotes/backslash/control chars' };
|
||||
}
|
||||
if (seenIds.has(p.id)) {
|
||||
return { ok: false, reason: 'waves[' + i + '] has duplicate plan id "' + p.id + '"' };
|
||||
}
|
||||
seenIds.add(p.id);
|
||||
for (const f of p.files_modified) {
|
||||
if (typeof f !== 'string' || f.length === 0) {
|
||||
return { ok: false, reason: 'waves[' + i + '].plans[' + j + '].files_modified entries must be non-empty strings' };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const budgetTokens = (typeof input.budgetTokens === 'number' && Number.isFinite(input.budgetTokens) && input.budgetTokens > 0)
|
||||
? Math.floor(input.budgetTokens)
|
||||
: null;
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push('// GSD Workflow script — generated by the claude-orchestration capability (#1143)');
|
||||
lines.push('// phase: ' + phaseDir);
|
||||
lines.push('// BETA: preview-grade; on any failure the orchestrator falls back to inline dispatch.');
|
||||
lines.push('// Composes the SAME gsd-executor agent + worktree isolation as the inline path,');
|
||||
lines.push('// so artifacts (SUMMARY.md) and commits are produced identically.');
|
||||
lines.push('resumeFromRunId(' + quoteString(runId) + ')');
|
||||
if (budgetTokens !== null) {
|
||||
lines.push('budget(' + budgetTokens + ')');
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
const stagesByWave: string[][][] = [];
|
||||
let totalPlans = 0;
|
||||
|
||||
for (let wi = 0; wi < waves.length; wi++) {
|
||||
const wave = waves[wi];
|
||||
const stages = partitionStages(wave.plans);
|
||||
stagesByWave.push(stages);
|
||||
totalPlans += wave.plans.length;
|
||||
|
||||
lines.push('// Wave ' + wave.id);
|
||||
for (let si = 0; si < stages.length; si++) {
|
||||
const stagePlanIds = stages[si];
|
||||
// Resolve back to plan objects for briefs (ids are unique within a wave — validated above).
|
||||
const stagePlans = stagePlanIds.map((id) => wave.plans.find((p) => p.id === id) as Plan);
|
||||
if (stages.length > 1) {
|
||||
lines.push('// Stage ' + si + (si > 0 ? ' (sequential — files_modified overlap)' : ''));
|
||||
}
|
||||
if (stagePlans.length === 1) {
|
||||
const p = stagePlans[0];
|
||||
lines.push('parallel(');
|
||||
lines.push(' agent(' + quoteString(p.brief) + ', { agentType: "gsd-executor", isolation: "worktree" })');
|
||||
lines.push(')');
|
||||
} else {
|
||||
lines.push('parallel(');
|
||||
for (const p of stagePlans) {
|
||||
lines.push(' agent(' + quoteString(p.brief) + ', { agentType: "gsd-executor", isolation: "worktree" }),');
|
||||
}
|
||||
// Replace trailing comma on the last agent line with nothing.
|
||||
const lastIdx = lines.length - 1;
|
||||
lines[lastIdx] = lines[lastIdx].replace(/,$/, '');
|
||||
lines.push(')');
|
||||
}
|
||||
}
|
||||
if (wi < waves.length - 1) lines.push('');
|
||||
}
|
||||
|
||||
lines.push('// Each agent writes SUMMARY.md on its worktree branch; commits land there');
|
||||
lines.push('// and are merged by the orchestrator exactly as in inline wave dispatch.');
|
||||
|
||||
const script = lines.join('\n');
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
script,
|
||||
summary: {
|
||||
waves: waves.length,
|
||||
plans: totalPlans,
|
||||
stagesByWave,
|
||||
resumeRunId: runId,
|
||||
budgetTokens,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Exports ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export = {
|
||||
detectWorkflowBackend,
|
||||
emitWorkflowScript,
|
||||
compareSemver,
|
||||
isValidSemver,
|
||||
WORKFLOW_TOOL_FLOOR_VERSION,
|
||||
BACKEND_VALUES,
|
||||
WORKFLOW_RUNTIME,
|
||||
};
|
||||
@@ -244,7 +244,7 @@ function migrateLegacyDevPreferencesToSkill(targetDir: string, saved: Map<string
|
||||
* - agents: write as-is (files already carry their own `gsd-` prefix).
|
||||
* For kimi-agents kind: recursively copy generated YAML/prompt files.
|
||||
*/
|
||||
function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: string): void {
|
||||
function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: string, runtime?: string): void {
|
||||
// Defense-in-depth: verify destDir is within the install root even if the
|
||||
// upstream assertDestWithinConfigHome check was somehow bypassed. This guards
|
||||
// the actual write site against any future call-site drift.
|
||||
@@ -302,8 +302,11 @@ function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: s
|
||||
|
||||
let destName: string;
|
||||
if (kind.kind === 'agents') {
|
||||
// Agent files already carry the gsd- prefix in the source dir
|
||||
destName = entry.name;
|
||||
// Agent files already carry the gsd- prefix in the source dir.
|
||||
// #1575: copilot agents get .agent.md suffix (mirrors inline loop line ~9118).
|
||||
destName = runtime === 'copilot'
|
||||
? entry.name.replace(/\.md$/, '.agent.md')
|
||||
: entry.name;
|
||||
} else if (namespacedByDir) {
|
||||
// Directory is the namespace; don't double-prefix the filename
|
||||
destName = entry.name;
|
||||
@@ -619,7 +622,7 @@ function installRuntimeArtifacts(
|
||||
}
|
||||
|
||||
_removeGsdEntries(dest, kind);
|
||||
_copyStaged(item.sourceDir, dest, kind, configDir);
|
||||
_copyStaged(item.sourceDir, dest, kind, configDir, runtime);
|
||||
|
||||
// Restore user-owned dirs after the prune+copy
|
||||
for (const [dirName, snap] of toPreserve) {
|
||||
@@ -629,7 +632,7 @@ function installRuntimeArtifacts(
|
||||
// For non-skills kinds (commands, agents): no user content to preserve;
|
||||
// just prune stale gsd-* entries and copy new ones.
|
||||
_removeGsdEntries(dest, kind);
|
||||
_copyStaged(item.sourceDir, dest, kind, configDir);
|
||||
_copyStaged(item.sourceDir, dest, kind, configDir, runtime);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -876,6 +876,7 @@ export = {
|
||||
writeActiveProfile,
|
||||
// Shared internals
|
||||
parseRequires,
|
||||
parseCallsAgents,
|
||||
cleanupStagedSkills,
|
||||
// Back-compat / deprecated
|
||||
MINIMAL_SKILL_ALLOWLIST,
|
||||
|
||||
@@ -105,6 +105,66 @@ function _resetModelPolicyWarningCacheForTests(): void {
|
||||
_modelPolicyUnmappableWarned.clear();
|
||||
}
|
||||
|
||||
// Dedupe stderr warnings for unmappable model_overrides Claude IDs (#2041).
|
||||
const _modelOverrideUnmappableWarned = new Set<string>();
|
||||
function warnModelOverrideUnmappable(agentType: string, overrideValue: string): void {
|
||||
const key = `${agentType}::${overrideValue}`;
|
||||
if (_modelOverrideUnmappableWarned.has(key)) return;
|
||||
_modelOverrideUnmappableWarned.add(key);
|
||||
// Cap emission length so an oversized or secret-shaped value cannot leak in
|
||||
// full to stderr/logs (#2041 security review). MUST go to stderr — resolve-
|
||||
// model's JSON result is parsed from stdout.
|
||||
const safe = overrideValue.length > 64 ? overrideValue.slice(0, 64) + '…' : overrideValue;
|
||||
process.stderr.write(
|
||||
`gsd: warning — model_overrides value "${safe}" for ${agentType} ` +
|
||||
`has no Claude agent alias; falling through to tier resolution.\n`,
|
||||
);
|
||||
}
|
||||
|
||||
// Test-only: reset the model_overrides warn-dedupe cache between cases (#2041).
|
||||
function _resetModelOverrideWarningCacheForTests(): void {
|
||||
_modelOverrideUnmappableWarned.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* #2041 — Map a `model_overrides` value to its Claude Agent-tool alias on the
|
||||
* claude runtime, mirroring the `model_policy` path (#1144). Claude Code's
|
||||
* Agent tool `model` parameter documents only tier aliases (opus/sonnet/haiku/
|
||||
* fable); a full Claude model ID returned verbatim is silently dropped by the
|
||||
* spawner. Returns the value to return verbatim, or null to signal "fall
|
||||
* through to normal tier/dynamic-routing resolution" (used when a Claude full
|
||||
* ID has no alias — matches model_policy's warn-and-fall-through). Non-Claude
|
||||
* runtimes and non-Claude values always pass through verbatim.
|
||||
*
|
||||
* Hardening (code+security review): a `typeof` guard preserves the pre-fix
|
||||
* no-crash behavior if a malformed config surfaces a non-string value, and an
|
||||
* `Object.hasOwn` lookup defeats `__proto__`/`constructor` lookups on the plain
|
||||
* object literal so those reserved keys cannot return a truthy non-string.
|
||||
*/
|
||||
function mapClaudeOverrideForRuntime(
|
||||
override: string,
|
||||
configRuntime: string | null | undefined,
|
||||
agentType: string,
|
||||
): string | null {
|
||||
// Defensive: model_overrides is typed Record<string,string> but a malformed
|
||||
// config could surface a non-string; pass through verbatim (preserving the
|
||||
// pre-fix no-crash behaviour) and let the downstream Agent tool reject it.
|
||||
if (typeof override !== 'string') return override;
|
||||
const onClaude = !configRuntime || configRuntime === 'claude';
|
||||
if (!onClaude) return override;
|
||||
// Object.hasOwn guards against __proto__/constructor returning a truthy
|
||||
// non-string from the plain object literal (#2041 security review).
|
||||
if (Object.hasOwn(CLAUDE_POLICY_ID_TO_ALIAS, override)) {
|
||||
return CLAUDE_POLICY_ID_TO_ALIAS[override];
|
||||
}
|
||||
if (CLAUDE_AGENT_ALIASES.has(override)) return override;
|
||||
if (override.startsWith('claude-')) {
|
||||
warnModelOverrideUnmappable(agentType, override);
|
||||
return null;
|
||||
}
|
||||
return override;
|
||||
}
|
||||
|
||||
/**
|
||||
* #49 — Provider-neutral model policy preset resolution.
|
||||
*/
|
||||
@@ -159,11 +219,15 @@ function resolveModelPolicy(policy: Record<string, unknown> | null | undefined,
|
||||
function resolveModelInternal(cwd: string, agentType: string): string {
|
||||
const config = loadConfig(cwd);
|
||||
|
||||
// 1. Per-agent override
|
||||
// 1. Per-agent override (#2041: map Claude full IDs → Agent-tool aliases on
|
||||
// the claude runtime, mirroring the model_policy path #1144; non-Claude
|
||||
// runtimes and non-Claude values pass through verbatim).
|
||||
const modelOverrides = config['model_overrides'] as Record<string, string> | null | undefined;
|
||||
const override = modelOverrides?.[agentType];
|
||||
if (override) {
|
||||
return override;
|
||||
const mapped = mapClaudeOverrideForRuntime(override, config['runtime'] as string | null | undefined, agentType);
|
||||
if (mapped !== null) return mapped;
|
||||
// Unmappable Claude ID — fall through to tier resolution (matches model_policy).
|
||||
}
|
||||
|
||||
// 2. Compute the tier
|
||||
@@ -287,7 +351,11 @@ function resolveModelForTier(cwd: string, agentType: string, attempt?: number):
|
||||
|
||||
const modelOverrides = config['model_overrides'] as Record<string, string> | null | undefined;
|
||||
const override = modelOverrides?.[agentType];
|
||||
if (override) return override;
|
||||
if (override) {
|
||||
const mapped = mapClaudeOverrideForRuntime(override, config['runtime'] as string | null | undefined, agentType);
|
||||
if (mapped !== null) return mapped;
|
||||
// Unmappable Claude ID — fall through to dynamic_routing / model_policy resolution.
|
||||
}
|
||||
|
||||
if (config['model_policy'] && config['runtime'] && config['runtime'] !== 'claude') {
|
||||
return resolveModelInternal(cwd, agentType);
|
||||
@@ -508,6 +576,7 @@ export = {
|
||||
resolveModelPolicy,
|
||||
resolveModelInternal,
|
||||
_resetModelPolicyWarningCacheForTests,
|
||||
_resetModelOverrideWarningCacheForTests,
|
||||
VALID_GRANULARITIES,
|
||||
resolveGranularityInternal,
|
||||
assertValidGranularityOverride,
|
||||
|
||||
@@ -1460,7 +1460,10 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
|
||||
`^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
|
||||
'im',
|
||||
);
|
||||
roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
|
||||
// Scope the Progress-row search to the ## Progress section so the regex
|
||||
// doesn't bind to an earlier table (e.g. | Phase | Requirements | Count |)
|
||||
// whose rows also start with the phase number. (#2012)
|
||||
const updateProgressRow = (fullRow: string): string => {
|
||||
const cells = fullRow.split('|').slice(1, -1);
|
||||
const dateShape = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (cells.length === 5) {
|
||||
@@ -1477,7 +1480,15 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
|
||||
cells[3] = dateShape.test(existingDate4) ? cells[3] : ` ${today} `;
|
||||
}
|
||||
return '|' + cells.join('|') + '|';
|
||||
});
|
||||
};
|
||||
const progressIdx = roadmapContent.indexOf('## Progress');
|
||||
if (progressIdx >= 0) {
|
||||
const beforeProgress = roadmapContent.slice(0, progressIdx);
|
||||
const progressSection = roadmapContent.slice(progressIdx);
|
||||
roadmapContent = beforeProgress + progressSection.replace(tableRowPattern, updateProgressRow);
|
||||
} else {
|
||||
roadmapContent = roadmapContent.replace(tableRowPattern, updateProgressRow);
|
||||
}
|
||||
|
||||
const planCountPattern = new RegExp(
|
||||
`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
|
||||
|
||||
@@ -34,6 +34,9 @@ const { countMatchedSummaries } = coreUtils;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import frontmatter = require('./frontmatter.cjs');
|
||||
const { extractFrontmatter, parseMustHavesBlock } = frontmatter;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import verificationMod = require('./verification.cjs');
|
||||
const { readVerificationStatus } = verificationMod;
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -504,7 +507,13 @@ function cmdRoadmapUpdatePlanProgress(cwd: string, phaseNum: string | null | und
|
||||
return;
|
||||
}
|
||||
|
||||
const isComplete = summaryCount >= planCount;
|
||||
// Verification gate (#2022): do NOT check the phase checkbox or stamp a
|
||||
// completion date until the phase's verification status is 'passed', matching
|
||||
// cmdPhaseComplete's gate (phase.cts:1436). Previously the checkbox fired the
|
||||
// moment the last plan summary landed — before gsd-verifier had verified.
|
||||
const phaseDir = path.join(cwd, phaseInfo!.directory);
|
||||
const verificationPassed = readVerificationStatus(phaseDir).status === 'passed';
|
||||
const isComplete = summaryCount >= planCount && verificationPassed;
|
||||
const status = isComplete ? 'Complete' : summaryCount > 0 ? 'In Progress' : 'Planned';
|
||||
const today = realClock.today();
|
||||
|
||||
|
||||
@@ -196,6 +196,7 @@ const RUNTIME_LABELS: Readonly<Record<string, string>> = {
|
||||
kimi: 'Kimi CLI',
|
||||
codebuddy: 'CodeBuddy',
|
||||
cline: 'Cline',
|
||||
zcode: 'ZCode',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -243,6 +244,7 @@ const GLOBAL_CONFIG_HOME_FRAGMENTS: Readonly<Record<string, string>> = {
|
||||
codebuddy: "'.codebuddy'",
|
||||
cline: "'.cline'",
|
||||
kimi: "'.config', 'agents'",
|
||||
zcode: "'.zcode'",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -266,7 +268,7 @@ export function getGlobalConfigHomeFragment(runtime: string): string {
|
||||
*/
|
||||
const RUNTIME_FLAG_IDS = Object.freeze([
|
||||
'opencode', 'kilo', 'codex', 'copilot', 'antigravity', 'cursor',
|
||||
'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'kimi',
|
||||
'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'kimi', 'zcode',
|
||||
] as const);
|
||||
|
||||
/**
|
||||
|
||||
105
src/surface.cts
105
src/surface.cts
@@ -29,6 +29,7 @@
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { platformWriteSync } from './shell-command-projection.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
@@ -55,11 +56,17 @@ const SURFACE_FILE_NAME = '.gsd-surface.json';
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface AgentCtx {
|
||||
runtime: string;
|
||||
pathPrefix: string;
|
||||
attribution: string | null | undefined;
|
||||
}
|
||||
|
||||
interface ArtifactKind {
|
||||
kind: string;
|
||||
destSubpath: string;
|
||||
prefix: string;
|
||||
stage: (resolvedProfile: { name: string; skills: Set<string> | '*'; agents: Set<string> }) => string;
|
||||
stage: (resolvedProfile: { name: string; skills: Set<string> | '*'; agents: Set<string> }, agentCtx?: AgentCtx) => string;
|
||||
}
|
||||
|
||||
interface Layout {
|
||||
@@ -69,6 +76,12 @@ interface Layout {
|
||||
kinds: ArtifactKind[];
|
||||
}
|
||||
|
||||
interface ApplySurfaceOptions {
|
||||
resolveAttribution?: (runtime: string) => string | null | undefined;
|
||||
homedir?: () => string;
|
||||
platform?: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State IO
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -301,32 +314,54 @@ function resolveSurface(runtimeConfigDir: string, manifest: Map<string, string[]
|
||||
* Re-stage the active surface using the resolved layout.
|
||||
* Iterates layout.kinds and syncs each artifact kind to its destination.
|
||||
*/
|
||||
function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<string, string[]> | object, clusterMap?: ClusterMap | Record<string, string[]>, registry?: { capabilityClusters?: Record<string, string[]>; profileMembership?: Record<string, { tier: string; profiles: string[] }> }): { name: string; skills: Set<string>; agents: Set<string> } {
|
||||
function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<string, string[]> | object, clusterMap?: ClusterMap | Record<string, string[]>, registry?: { capabilityClusters?: Record<string, string[]>; profileMembership?: Record<string, { tier: string; profiles: string[] }> }, opts?: ApplySurfaceOptions): { name: string; skills: Set<string>; agents: Set<string> } {
|
||||
if (path.resolve(runtimeConfigDir) !== path.resolve(layout.configDir)) {
|
||||
throw new TypeError('applySurface runtimeConfigDir must match layout.configDir');
|
||||
}
|
||||
const skillManifest = normalizeSkillManifest(layout.configDir, manifest);
|
||||
const resolved = resolveSurface(layout.configDir, skillManifest, clusterMap, registry);
|
||||
// Mirror installRuntimeArtifacts: skills kinds get per-runtime path rewrites
|
||||
// so SKILL.md bodies reference the install target (pathPrefix), not the
|
||||
// converter's default ~/.claude paths (#813). Delegated to the conversion
|
||||
// module's deep seam (ADR-1508 / #1511 Phase 2) — no attribution resolver
|
||||
// needed here (proven: Co-Authored-By never appears in staged content; see
|
||||
// brief PROVEN KEY FACT). No getInstallExports() call required.
|
||||
// #1615 adversarial review (PR #1622): commands kind was previously skipped,
|
||||
// leaving raw @~/.claude/... references in Windsurf workflow bodies after a
|
||||
// /gsd-surface profile change. Same gap affected any runtime with commands
|
||||
// kinds (windsurf, opencode, kilo, cursor, augment, codebuddy, gemini).
|
||||
//
|
||||
// Asymmetry note: rewriteStagedSkillBodies mutates in place (returns void),
|
||||
// but rewriteStagedCommandBodies copies to a fresh mkdtemp dir and returns
|
||||
// its path (commands .md files are flat; mutating the staged source would
|
||||
// corrupt the package source on full-profile runs). Caller MUST sync from
|
||||
// the returned dir and clean it up.
|
||||
// #1575: agents kind now mirrors createRuntimeArtifactInstallPlan — build
|
||||
// agentCtx (pathPrefix + attribution) and pass it to kind.stage() so
|
||||
// stageAgentsForRuntimeWithConverter applies the full inline-loop pipeline
|
||||
// (pathRewrites -> attribution -> converter -> normalize). Without this,
|
||||
// surface-path agents lack path-prefix rewrites and Co-Authored-By trailers,
|
||||
// diverging from a fresh install.
|
||||
const _homedirFn: () => string = opts?.homedir ?? (() => os.homedir());
|
||||
const _resolvedTarget = path.resolve(layout.configDir).replace(/\\/g, '/');
|
||||
const _homeDir = _homedirFn().replace(/\\/g, '/');
|
||||
const _isGlobal = (layout.scope ?? 'global') === 'global';
|
||||
const _isOpencode = layout.runtime === 'opencode';
|
||||
const _isWindowsHost = (opts?.platform ?? process.platform) === 'win32';
|
||||
const _pathPrefix = runtimeArtifactConversion._computePathPrefix({ isGlobal: _isGlobal, isOpencode: _isOpencode, isWindowsHost: _isWindowsHost, resolvedTarget: _resolvedTarget, homeDir: _homeDir });
|
||||
const _attribution = opts?.resolveAttribution ? opts.resolveAttribution(layout.runtime) : undefined;
|
||||
const agentCtx: AgentCtx = { runtime: layout.runtime, pathPrefix: _pathPrefix, attribution: _attribution };
|
||||
|
||||
const tempDirsToClean: string[] = [];
|
||||
// #1575: When the surface has no state modifications AND the base profile is
|
||||
// 'full', pass the '*' sentinel for agents staging so ALL agents are staged —
|
||||
// matching the install path which uses { skills: '*' }. Without this, agents
|
||||
// not referenced by any skill's _calls_agents_ manifest entry would be silently
|
||||
// dropped from the surface path. For tiered profiles (core/standard) or when
|
||||
// surface mods exist, pass the resolved set so only the filtered subset stages.
|
||||
const _surfaceState = readSurface(layout.configDir);
|
||||
const _baseProfileName = (_surfaceState && _surfaceState.baseProfile)
|
||||
? _surfaceState.baseProfile
|
||||
: (readActiveProfile(layout.configDir) || 'full');
|
||||
const _hasSurfaceMods = !!_surfaceState && (
|
||||
_surfaceState.disabledClusters.length > 0 ||
|
||||
_surfaceState.explicitAdds.length > 0 ||
|
||||
_surfaceState.explicitRemoves.length > 0
|
||||
);
|
||||
const _isUnmodifiedFull = _baseProfileName === 'full' && !_hasSurfaceMods;
|
||||
try {
|
||||
for (const kind of layout.kinds) {
|
||||
let staged: string = kind.stage(resolved);
|
||||
let staged: string;
|
||||
if (kind.kind === 'agents') {
|
||||
const agentProfile = _isUnmodifiedFull ? { ...resolved, skills: '*' as const } : resolved;
|
||||
staged = kind.stage(agentProfile, agentCtx);
|
||||
} else {
|
||||
staged = kind.stage(resolved);
|
||||
}
|
||||
if (kind.kind === 'skills') {
|
||||
runtimeArtifactConversion.rewriteStagedSkillBodies(staged, {
|
||||
runtime: layout.runtime,
|
||||
@@ -345,7 +380,7 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<st
|
||||
}
|
||||
}
|
||||
const dest = assertDestWithinConfigHome(layout.configDir, kind.destSubpath);
|
||||
_syncGsdDir(staged, dest, kind, skillManifest);
|
||||
_syncGsdDir(staged, dest, kind, skillManifest, layout.runtime);
|
||||
}
|
||||
} finally {
|
||||
for (const dir of tempDirsToClean) {
|
||||
@@ -451,7 +486,7 @@ function pruneSkillDirs(skillsDir: string, retainedNames: Set<string>, prefix: s
|
||||
* user-owned dirs. GSD-owned = stem in manifest; removal targets = in manifest AND
|
||||
* not in staged set. User-owned (not in manifest) are always preserved.
|
||||
*/
|
||||
function _syncGsdDir(stagedDir: string, destDir: string, kind: ArtifactKind | string, manifest?: Map<string, string[]>): void {
|
||||
function _syncGsdDir(stagedDir: string, destDir: string, kind: ArtifactKind | string, manifest?: Map<string, string[]>, runtime?: string): void {
|
||||
if (!fs.existsSync(stagedDir)) return;
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
|
||||
@@ -459,6 +494,11 @@ function _syncGsdDir(stagedDir: string, destDir: string, kind: ArtifactKind | st
|
||||
const kindName = (typeof kind === 'string') ? kind : kind.kind;
|
||||
const kindPrefix = (typeof kind === 'object' && kind !== null) ? kind.prefix : 'gsd-';
|
||||
|
||||
// #1575: copilot agents are renamed .md -> .agent.md at copy time, mirroring
|
||||
// the inline agent loop in bin/install.js (line ~9118). Other runtimes keep
|
||||
// the staged filename verbatim.
|
||||
const isCopilotAgents = runtime === 'copilot' && kindName === 'agents';
|
||||
|
||||
if (kindName === 'skills') {
|
||||
// Skills kind: work with directories, not files.
|
||||
// Each staged entry is a directory named ${prefix}${stem}.
|
||||
@@ -497,23 +537,28 @@ function _syncGsdDir(stagedDir: string, destDir: string, kind: ArtifactKind | st
|
||||
const stagedFiles = fs.readdirSync(stagedDir).filter(f => f.endsWith('.md'));
|
||||
const stagedDestNames = new Set<string>();
|
||||
for (const file of stagedFiles) {
|
||||
const destName = (kindName === 'agents' || namespacedByDir)
|
||||
? file
|
||||
: `${kindPrefix}${file.slice(0, -3)}.md`;
|
||||
const destName = isCopilotAgents
|
||||
? file.replace(/\.md$/, '.agent.md')
|
||||
: (kindName === 'agents' || namespacedByDir)
|
||||
? file
|
||||
: `${kindPrefix}${file.slice(0, -3)}.md`;
|
||||
fs.copyFileSync(path.join(stagedDir, file), path.join(destDir, destName));
|
||||
stagedDestNames.add(destName);
|
||||
}
|
||||
|
||||
// Prune stale GSD-owned files not in the staged set, preserving user-owned files
|
||||
// (mirrors install's prefix-scoped _removeGsdEntries):
|
||||
// - agents: only gsd-* are GSD-owned
|
||||
// - agents: only gsd-* are GSD-owned (copilot: gsd-*.agent.md)
|
||||
// - flat command dirs: only `${kindPrefix}`-prefixed are GSD-owned
|
||||
// - namespaced command dirs: the whole dir is GSD-owned
|
||||
for (const file of fs.readdirSync(destDir).filter(f => f.endsWith('.md'))) {
|
||||
if (kindName === 'agents' && !file.startsWith('gsd-')) continue;
|
||||
if (kindName === 'commands' && !namespacedByDir && kindPrefix && !file.startsWith(kindPrefix)) continue;
|
||||
if (!stagedDestNames.has(file)) {
|
||||
try { fs.unlinkSync(path.join(destDir, file)); } catch { /* ignore */ }
|
||||
const shouldPruneAgents = !(kindName === 'agents' && (!manifest || manifest.size === 0));
|
||||
if (shouldPruneAgents) {
|
||||
for (const file of fs.readdirSync(destDir).filter(f => f.endsWith('.md'))) {
|
||||
if (kindName === 'agents' && !file.startsWith('gsd-')) continue;
|
||||
if (kindName === 'commands' && !namespacedByDir && kindPrefix && !file.startsWith(kindPrefix)) continue;
|
||||
if (!stagedDestNames.has(file)) {
|
||||
try { fs.unlinkSync(path.join(destDir, file)); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
|
||||
|
||||
const RUNTIMES = Object.keys(registry.runtimes);
|
||||
|
||||
test('declarative adapter: kind === "declarative" + runtime echoed, for all 15 runtimes', () => {
|
||||
assert.ok(RUNTIMES.length >= 15, `expected ≥15 runtimes in registry, got ${RUNTIMES.length}`);
|
||||
test('declarative adapter: kind === "declarative" + runtime echoed, for every registry runtime', () => {
|
||||
assert.ok(RUNTIMES.length > 0, `expected at least one runtime in registry, got ${RUNTIMES.length}`);
|
||||
for (const r of RUNTIMES) {
|
||||
const adapter = createDeclarativeAdapter({ runtime: r });
|
||||
assert.strictEqual(adapter.kind, 'declarative', `${r}: kind must be 'declarative'`);
|
||||
|
||||
@@ -25,7 +25,7 @@ const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
|
||||
|
||||
const RUNTIMES = Object.keys(registry.runtimes);
|
||||
|
||||
test('imperative adapter: kind === "imperative" + runtime echoed + registry present, for all 15 runtimes', () => {
|
||||
test('imperative adapter: kind === "imperative" + runtime echoed + registry present, for every registry runtime', () => {
|
||||
for (const r of RUNTIMES) {
|
||||
const adapter = createImperativeAdapter({ runtime: r });
|
||||
assert.strictEqual(adapter.kind, 'imperative', `${r}: kind must be 'imperative'`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"gsd-advisor-researcher.md": 4693,
|
||||
"gsd-ai-researcher.md": 5909,
|
||||
"gsd-advisor-researcher.md": 4727,
|
||||
"gsd-ai-researcher.md": 5943,
|
||||
"gsd-assumptions-analyzer.md": 4646,
|
||||
"gsd-code-fixer.md": 36640,
|
||||
"gsd-code-reviewer.md": 16870,
|
||||
@@ -11,26 +11,26 @@
|
||||
"gsd-doc-synthesizer.md": 9782,
|
||||
"gsd-doc-verifier.md": 12403,
|
||||
"gsd-doc-writer.md": 38924,
|
||||
"gsd-domain-researcher.md": 6998,
|
||||
"gsd-domain-researcher.md": 7032,
|
||||
"gsd-eval-auditor.md": 12496,
|
||||
"gsd-eval-planner.md": 7008,
|
||||
"gsd-executor.md": 43578,
|
||||
"gsd-executor.md": 43607,
|
||||
"gsd-framework-selector.md": 6778,
|
||||
"gsd-integration-checker.md": 15238,
|
||||
"gsd-intel-updater.md": 18166,
|
||||
"gsd-mempalace-curator.md": 4325,
|
||||
"gsd-nyquist-auditor.md": 7345,
|
||||
"gsd-pattern-mapper.md": 12487,
|
||||
"gsd-phase-researcher.md": 40832,
|
||||
"gsd-phase-researcher.md": 40866,
|
||||
"gsd-plan-checker.md": 44780,
|
||||
"gsd-planner.md": 48157,
|
||||
"gsd-project-researcher.md": 22208,
|
||||
"gsd-planner.md": 48191,
|
||||
"gsd-project-researcher.md": 22242,
|
||||
"gsd-research-synthesizer.md": 13847,
|
||||
"gsd-roadmapper.md": 22273,
|
||||
"gsd-security-auditor.md": 8981,
|
||||
"gsd-ui-auditor.md": 17249,
|
||||
"gsd-ui-checker.md": 11178,
|
||||
"gsd-ui-researcher.md": 19466,
|
||||
"gsd-ui-researcher.md": 19500,
|
||||
"gsd-user-profiler.md": 8516,
|
||||
"gsd-verifier.md": 49147
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user