diff --git a/.changeset/1143-claude-orchestration-capability.md b/.changeset/1143-claude-orchestration-capability.md new file mode 100644 index 000000000..d87aae625 --- /dev/null +++ b/.changeset/1143-claude-orchestration-capability.md @@ -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) diff --git a/.changeset/eager-tunas-swim.md b/.changeset/eager-tunas-swim.md new file mode 100644 index 000000000..5a77fc331 --- /dev/null +++ b/.changeset/eager-tunas-swim.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1991 +--- +**`/gsd-quick` no longer halts with a stale-base worktree mismatch** — the worktree executor now degrades to sequential execution when its fork base has diverged from origin/HEAD, instead of spawning a worktree guaranteed to fail the base-mismatch guard. diff --git a/.changeset/gentle-badgers-roar.md b/.changeset/gentle-badgers-roar.md new file mode 100644 index 000000000..a20eb5765 --- /dev/null +++ b/.changeset/gentle-badgers-roar.md @@ -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) diff --git a/.changeset/noble-foxes-purr.md b/.changeset/noble-foxes-purr.md new file mode 100644 index 000000000..e53da8a32 --- /dev/null +++ b/.changeset/noble-foxes-purr.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2051 +--- +**`capability state` and `loop render-hooks` now accept `--runtime` to override the auto-detected runtime** — previously both commands parsed only `--config-dir`, so the runtime config dir was derived from the persisted `.planning/config.json` runtime (precedence `GSD_RUNTIME` → `config.runtime` → `claude`). A repo that persisted `runtime:"codex"` resolved the config dir to `~/.codex`, where the Claude skill isn't installed, so every skill-bearing capability reported `surfaced:false` and `execute:post`/`verify:post` hooks silently no-op'd when the operator drove GSD from Claude Code. `--runtime ` (canonicalized, so aliases like `codex-app` work) now bypasses that fallback so the config dir resolves to the explicitly-named runtime's home. Behavior without the flag is unchanged. (#2003) diff --git a/.changeset/steady-ibex-run.md b/.changeset/steady-ibex-run.md new file mode 100644 index 000000000..bf1b37a3b --- /dev/null +++ b/.changeset/steady-ibex-run.md @@ -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-.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) diff --git a/.changeset/steady-mice-frolic.md b/.changeset/steady-mice-frolic.md new file mode 100644 index 000000000..d5bb4f8d6 --- /dev/null +++ b/.changeset/steady-mice-frolic.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 1994 +--- +**`/gsd:onboard` guides brownfield setup** — existing repos now have a top-level onboarding command that routes through codebase mapping, docs ingest, project initialization, and an onboarding summary without silently overwriting planning files. diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b99a81103..56ac0ddb5 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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.3", + "version": "1.7.0-rc.4", "source": "./", "author": { "name": "open-gsd", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index de21c6772..a90b6c123 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "gsd-core", "displayName": "GSD Core", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.", "author": { "name": "open-gsd", diff --git a/.gitignore b/.gitignore index 8968c2ddb..9d3fae1fb 100644 --- a/.gitignore +++ b/.gitignore @@ -193,6 +193,7 @@ build/ /gsd-core/bin/lib/eval.cjs /gsd-core/bin/lib/eval-command-router.cjs /gsd-core/bin/lib/init-command-router.cjs +/gsd-core/bin/lib/onboard-projection.cjs /gsd-core/bin/lib/agent-command-router.cjs /gsd-core/bin/lib/agent-install-check.cjs /gsd-core/bin/lib/task-command-router.cjs diff --git a/CONTEXT.md b/CONTEXT.md index 98bfec5e1..565add669 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -68,7 +68,7 @@ Canonical command normalization and resolution Interface (`query-command-resolut Module owning command resolution, policy projection (`mutation`, `output_mode`), unknown-command diagnosis, and handler Adapter binding at one seam for query dispatch. ### Init Command Module -Module owning the `init.*` family of query handlers that compose atomic queries into the flat JSON bundles consumed by init workflows (`/gsd-execute-phase`, `/gsd-plan-phase`, `/gsd-verify-work`, `/gsd-new-project`, `/gsd-manager`, `/gsd-progress`, `/gsd-resume`, etc.). Source of truth: `gsd-core/bin/lib/init.cjs` — the basic handlers (plus `withProjectRoot` project-identity injection) and the 3 heavyweight handlers (`initNewProject`, `initProgress`, `initManager`). All handlers return `{ data: }`. Test seams: `tests/init.test.cjs` and `tests/init-manager.test.cjs` (cover withProjectRoot precedence, progress/manager precedence regression #2674, workstream scoping regression #3196, and cross-milestone dependency regression #2267). (The SDK `handlers/init/*.ts` sources and the `init*.test.ts` seams were retired with the SDK package per ADR-0174.) +Module owning the `init.*` family of query handlers that compose atomic queries into the flat JSON bundles consumed by init workflows (`/gsd-execute-phase`, `/gsd-plan-phase`, `/gsd-verify-work`, `/gsd-new-project`, `/gsd-onboard`, `/gsd-manager`, `/gsd-progress`, `/gsd-resume`, etc.). Source of truth: `src/init.cts` and the compiled `gsd-core/bin/lib/init.cjs`; onboarding routing readiness lives in `src/onboard-projection.cts`. The basic handlers (plus `withProjectRoot` project-identity injection) and the heavyweight handlers (`initNewProject`, `initOnboard`, `initProgress`, `initManager`) return `{ data: }`. Test seams: `tests/init.test.cjs`, `tests/onboard-command.test.cjs`, and `tests/init-manager.test.cjs` (cover withProjectRoot precedence, onboarding projection/rendering, progress/manager precedence regression #2674, workstream scoping regression #3196, and cross-milestone dependency regression #2267). (The SDK `handlers/init/*.ts` sources and the `init*.test.ts` seams were retired with the SDK package per ADR-0174.) ### Command Routing Hub Single dispatch seam (`gsd-core/bin/lib/command-routing-hub.cjs`) that centralizes CJS routing, the no-throw pure-result contract, typed error variants, and dispatch-event emission for all command family adapters. Interface: `createHub({ cjsRegistry, manifest, logger }) → hub`; `hub.dispatch({ family, subcommand, args, cwd, raw, parentTraceId? }) → Result` where `Result = { ok: true, data } | { ok: false, kind, ...typedPayload }` and `kind ∈ { UnknownCommand, InvalidArgs, HandlerRefusal, HandlerFailure }`. The `InvalidArgs` variant carries an optional `exitReason?: string` field (amendment #1642 / #1644 Phase 1) holding the `ERROR_REASON` enum value, separate from `reason` (the explanation text); the `makeInvalidArgs(arg, reason, exitReason?)` factory omits the field when the third arg is absent, undefined, or empty — preserving the strict-keys invariant tested at `tests/command-routing-hub.test.cjs:444`. The Hub is single-runtime (no mode selection, no sdkLoader), never prints, never exits, never throws. Adapters call `createHub`, dispatch, then translate the pure Result to `output()`/`error()` calls; when an `InvalidArgs` Result carries `exitReason`, the adapter passes it as the second arg to `error(message, exitReason)` so the JSON-error envelope (`GSD_JSON_ERRORS=1`) preserves the typed reason. Source: `gsd-core/bin/lib/command-routing-hub.cjs`; ADR: `docs/adr/0174-retire-gsd-sdk-package-boundary.md` (§5 amended #1642). @@ -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 ` 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. diff --git a/README.ja-JP.md b/README.ja-JP.md index 449483459..e852c33b0 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -47,13 +47,14 @@ npx @opengsd/gsd-core@latest 別のランタイムをお使いの場合や Node.js がない場合は [ランタイムへのインストール](docs/ja-JP/how-to/install-on-your-runtime.md) を参照してください。 -インストール後、最初のプロジェクトを開始します。 +インストール後、新規プロジェクトを開始するか、既存リポジトリをオンボーディングします。 ```bash -/gsd-new-project +/gsd-new-project # グリーンフィールドプロジェクト +/gsd-onboard # 既存コードベース ``` -初めての方は [はじめてのプロジェクト](docs/ja-JP/tutorials/your-first-project.md) で、インストールから最初のフェーズ出荷までのガイド付きチュートリアルをご覧ください。 +初めての方は [はじめてのプロジェクト](docs/ja-JP/tutorials/your-first-project.md) で、インストールから最初のフェーズ出荷までのガイド付きチュートリアルをご覧ください。既存リポジトリの場合は [既存コードベースのオンボーディング](docs/ja-JP/tutorials/onboarding-an-existing-codebase.md) を参照してください。 --- diff --git a/README.ko-KR.md b/README.ko-KR.md index 3bef4570b..2d90f1a6e 100644 --- a/README.ko-KR.md +++ b/README.ko-KR.md @@ -47,13 +47,14 @@ npx @opengsd/gsd-core@latest 다른 런타임이나 Node.js가 없는 환경은 [런타임에 설치하기](docs/ko-KR/how-to/install-on-your-runtime.md)를 참조하세요. -설치 후 첫 번째 프로젝트를 시작합니다: +설치 후 새 프로젝트를 시작하거나 기존 저장소를 온보딩합니다: ```bash -/gsd-new-project +/gsd-new-project # 그린필드 프로젝트 +/gsd-onboard # 기존 코드베이스 ``` -처음 사용하시나요? [첫 번째 프로젝트](docs/ko-KR/tutorials/your-first-project.md)를 따라 설치부터 첫 단계 출시까지 안내받으세요. +처음 사용하시나요? [첫 번째 프로젝트](docs/ko-KR/tutorials/your-first-project.md)를 따라 설치부터 첫 단계 출시까지 안내받으세요. 기존 저장소라면 [기존 코드베이스 온보딩](docs/ko-KR/tutorials/onboarding-an-existing-codebase.md)을 참고하세요. --- diff --git a/README.md b/README.md index d18deabc7..9f5f08857 100644 --- a/README.md +++ b/README.md @@ -47,13 +47,14 @@ The installer prompts for your runtime (Claude Code, OpenCode, Antigravity CLI, On another runtime or without Node.js? See [Install on your runtime](docs/how-to/install-on-your-runtime.md). -Once installed, start your first project: +Once installed, start a new project or onboard an existing repo: ```bash -/gsd-new-project +/gsd-new-project # greenfield project +/gsd-onboard # existing codebase ``` -New here? Follow [Your first project](docs/tutorials/your-first-project.md) for a guided walkthrough from install to first shipped phase. +New here? Follow [Your first project](docs/tutorials/your-first-project.md) for a guided walkthrough from install to first shipped phase, or [Onboarding an existing codebase](docs/tutorials/onboarding-an-existing-codebase.md) for brownfield setup. --- diff --git a/README.pt-BR.md b/README.pt-BR.md index 91c6de96e..96c0c9ada 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -47,13 +47,14 @@ O instalador solicita seu ambiente de execução (Claude Code, OpenCode, Gemini Em outro runtime ou sem Node.js? Consulte [Instalar no seu runtime](docs/pt-BR/how-to/install-on-your-runtime.md). -Após a instalação, inicie seu primeiro projeto: +Após a instalação, inicie um projeto novo ou integre um repositório existente: ```bash -/gsd-new-project +/gsd-new-project # projeto greenfield +/gsd-onboard # base de código existente ``` -É a primeira vez? Siga [Seu primeiro projeto](docs/pt-BR/tutorials/your-first-project.md) para um passo a passo guiado, desde a instalação até a primeira fase entregue. +É a primeira vez? Siga [Seu primeiro projeto](docs/pt-BR/tutorials/your-first-project.md) para um passo a passo guiado, desde a instalação até a primeira fase entregue. Para um repositório existente, consulte [Integrar uma base de código existente](docs/pt-BR/tutorials/onboarding-an-existing-codebase.md). --- diff --git a/README.zh-CN.md b/README.zh-CN.md index 7e2831c54..993167e57 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -47,13 +47,14 @@ npx @opengsd/gsd-core@latest 使用其他运行时或没有 Node.js?请参阅[在你的运行时上安装](docs/zh-CN/how-to/install-on-your-runtime.md)。 -安装完成后,启动你的第一个项目: +安装完成后,启动一个新项目或接入现有仓库: ```bash -/gsd-new-project +/gsd-new-project # 新建项目 +/gsd-onboard # 现有代码库 ``` -初次使用?请按照[你的第一个项目](docs/zh-CN/tutorials/your-first-project.md)进行引导式操作,从安装到完成第一个交付阶段。 +初次使用?请按照[你的第一个项目](docs/zh-CN/tutorials/your-first-project.md)进行引导式操作,从安装到完成第一个交付阶段。对于现有仓库,请参阅[接入现有代码库](docs/zh-CN/tutorials/onboarding-an-existing-codebase.md)。 --- diff --git a/capabilities/ai-integration/capability.json b/capabilities/ai-integration/capability.json index 679673d94..998933a2d 100644 --- a/capabilities/ai-integration/capability.json +++ b/capabilities/ai-integration/capability.json @@ -1,7 +1,7 @@ { "id": "ai-integration", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/antigravity/capability.json b/capabilities/antigravity/capability.json index 96b69c82f..585346c69 100644 --- a/capabilities/antigravity/capability.json +++ b/capabilities/antigravity/capability.json @@ -1,7 +1,7 @@ { "id": "antigravity", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/assumption-delta/capability.json b/capabilities/assumption-delta/capability.json index 23333f0b7..439a0101e 100644 --- a/capabilities/assumption-delta/capability.json +++ b/capabilities/assumption-delta/capability.json @@ -1,7 +1,7 @@ { "id": "assumption-delta", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/audit/capability.json b/capabilities/audit/capability.json index 40fcdc0dc..b2a563640 100644 --- a/capabilities/audit/capability.json +++ b/capabilities/audit/capability.json @@ -1,7 +1,7 @@ { "id": "audit", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/augment/capability.json b/capabilities/augment/capability.json index e3f498bb3..82c930e0d 100644 --- a/capabilities/augment/capability.json +++ b/capabilities/augment/capability.json @@ -1,7 +1,7 @@ { "id": "augment", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/claude-orchestration/capability.json b/capabilities/claude-orchestration/capability.json new file mode 100644 index 000000000..f93ec0438 --- /dev/null +++ b/capabilities/claude-orchestration/capability.json @@ -0,0 +1,85 @@ +{ + "id": "claude-orchestration", + "role": "feature", + "version": "1.7.0-rc.4", + "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": [] +} diff --git a/capabilities/claude-orchestration/fragments/execute-wave-post.md b/capabilities/claude-orchestration/fragments/execute-wave-post.md new file mode 100644 index 000000000..db0e76d5a --- /dev/null +++ b/capabilities/claude-orchestration/fragments/execute-wave-post.md @@ -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 --run-id +[--phase-dir ] [--budget ]` (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. diff --git a/capabilities/claude-orchestration/fragments/plan-post.md b/capabilities/claude-orchestration/fragments/plan-post.md new file mode 100644 index 000000000..bec9b01fa --- /dev/null +++ b/capabilities/claude-orchestration/fragments/plan-post.md @@ -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. diff --git a/capabilities/claude/capability.json b/capabilities/claude/capability.json index f87679b19..7bdf6bd40 100644 --- a/capabilities/claude/capability.json +++ b/capabilities/claude/capability.json @@ -1,7 +1,7 @@ { "id": "claude", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Claude Code", "description": "Anthropic Claude Code — primary development runtime; tier-1 support with full hook surface and skills-based global install.", "tier": "core", diff --git a/capabilities/cline/capability.json b/capabilities/cline/capability.json index df94b1775..1b6ab6513 100644 --- a/capabilities/cline/capability.json +++ b/capabilities/cline/capability.json @@ -1,7 +1,7 @@ { "id": "cline", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/code-review/capability.json b/capabilities/code-review/capability.json index 9682c230a..3068c5d8c 100644 --- a/capabilities/code-review/capability.json +++ b/capabilities/code-review/capability.json @@ -1,7 +1,7 @@ { "id": "code-review", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Code review", "description": "Source-file code review and review-fix workflow support for completed execution work.", "tier": "full", diff --git a/capabilities/codebuddy/capability.json b/capabilities/codebuddy/capability.json index 5f8d45a6d..94f498e38 100644 --- a/capabilities/codebuddy/capability.json +++ b/capabilities/codebuddy/capability.json @@ -1,7 +1,7 @@ { "id": "codebuddy", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "CodeBuddy", "description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", "tier": "core", diff --git a/capabilities/codex/capability.json b/capabilities/codex/capability.json index 7cc85f78b..db6e68df3 100644 --- a/capabilities/codex/capability.json +++ b/capabilities/codex/capability.json @@ -1,7 +1,7 @@ { "id": "codex", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/copilot/capability.json b/capabilities/copilot/capability.json index b4c5df53a..ce17f48eb 100644 --- a/capabilities/copilot/capability.json +++ b/capabilities/copilot/capability.json @@ -1,7 +1,7 @@ { "id": "copilot", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/cursor/capability.json b/capabilities/cursor/capability.json index 50136585d..b4877e40f 100644 --- a/capabilities/cursor/capability.json +++ b/capabilities/cursor/capability.json @@ -1,7 +1,7 @@ { "id": "cursor", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/drift/capability.json b/capabilities/drift/capability.json index 5ebea9d1a..996fbdaad 100644 --- a/capabilities/drift/capability.json +++ b/capabilities/drift/capability.json @@ -1,7 +1,7 @@ { "id": "drift", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/external-job/capability.json b/capabilities/external-job/capability.json index a75babb08..1e6d0132e 100644 --- a/capabilities/external-job/capability.json +++ b/capabilities/external-job/capability.json @@ -1,7 +1,7 @@ { "id": "external-job", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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/.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", diff --git a/capabilities/gap-analysis/capability.json b/capabilities/gap-analysis/capability.json index a8a2be422..4be113763 100644 --- a/capabilities/gap-analysis/capability.json +++ b/capabilities/gap-analysis/capability.json @@ -1,7 +1,7 @@ { "id": "gap-analysis", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/graphify/capability.json b/capabilities/graphify/capability.json index e9cac6524..9f9d2cd1e 100644 --- a/capabilities/graphify/capability.json +++ b/capabilities/graphify/capability.json @@ -1,7 +1,7 @@ { "id": "graphify", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/hermes/capability.json b/capabilities/hermes/capability.json index e38f13d63..d8764429e 100644 --- a/capabilities/hermes/capability.json +++ b/capabilities/hermes/capability.json @@ -1,7 +1,7 @@ { "id": "hermes", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/intel/capability.json b/capabilities/intel/capability.json index 361c5b8f6..b6700cfdc 100644 --- a/capabilities/intel/capability.json +++ b/capabilities/intel/capability.json @@ -1,7 +1,7 @@ { "id": "intel", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/kilo/capability.json b/capabilities/kilo/capability.json index bc15624fc..f6477ba4a 100644 --- a/capabilities/kilo/capability.json +++ b/capabilities/kilo/capability.json @@ -1,7 +1,7 @@ { "id": "kilo", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/kimi/capability.json b/capabilities/kimi/capability.json index 67ee60eb2..875a62f19 100644 --- a/capabilities/kimi/capability.json +++ b/capabilities/kimi/capability.json @@ -1,7 +1,7 @@ { "id": "kimi", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/mempalace/capability.json b/capabilities/mempalace/capability.json index 69b6a726f..d9173c317 100644 --- a/capabilities/mempalace/capability.json +++ b/capabilities/mempalace/capability.json @@ -1,7 +1,7 @@ { "id": "mempalace", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/nyquist/capability.json b/capabilities/nyquist/capability.json index 02f512966..d5d754a46 100644 --- a/capabilities/nyquist/capability.json +++ b/capabilities/nyquist/capability.json @@ -1,7 +1,7 @@ { "id": "nyquist", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Nyquist validation", "description": "Validation coverage audit that maps executed work back to tests and manual-only evidence.", "tier": "full", diff --git a/capabilities/opencode/capability.json b/capabilities/opencode/capability.json index 43d1d8815..ff98fcd10 100644 --- a/capabilities/opencode/capability.json +++ b/capabilities/opencode/capability.json @@ -1,7 +1,7 @@ { "id": "opencode", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/pattern-mapper/capability.json b/capabilities/pattern-mapper/capability.json index 6603b8728..d8e8c0de0 100644 --- a/capabilities/pattern-mapper/capability.json +++ b/capabilities/pattern-mapper/capability.json @@ -1,7 +1,7 @@ { "id": "pattern-mapper", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Pattern mapping", "description": "Optional codebase-pattern mapping before planning; owns the pattern mapper agent and workflow.pattern_mapper activation key.", "tier": "full", diff --git a/capabilities/profile-pipeline/capability.json b/capabilities/profile-pipeline/capability.json index baf943bbc..30e1993bb 100644 --- a/capabilities/profile-pipeline/capability.json +++ b/capabilities/profile-pipeline/capability.json @@ -1,7 +1,7 @@ { "id": "profile-pipeline", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/qwen/capability.json b/capabilities/qwen/capability.json index f0c622497..5047c4227 100644 --- a/capabilities/qwen/capability.json +++ b/capabilities/qwen/capability.json @@ -1,7 +1,7 @@ { "id": "qwen", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Qwen Code", "description": "Qwen Code (Alibaba) — nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", "tier": "core", diff --git a/capabilities/research/capability.json b/capabilities/research/capability.json index 1e17d45eb..f4f72e49c 100644 --- a/capabilities/research/capability.json +++ b/capabilities/research/capability.json @@ -1,7 +1,7 @@ { "id": "research", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Phase research", "description": "Optional phase research before planning; owns the phase researcher agent and workflow.research activation key.", "tier": "standard", diff --git a/capabilities/schema-gate/capability.json b/capabilities/schema-gate/capability.json index 8977f52f0..7da7a1e03 100644 --- a/capabilities/schema-gate/capability.json +++ b/capabilities/schema-gate/capability.json @@ -1,7 +1,7 @@ { "id": "schema-gate", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/security/capability.json b/capabilities/security/capability.json index 076925bf5..3b350184d 100644 --- a/capabilities/security/capability.json +++ b/capabilities/security/capability.json @@ -1,7 +1,7 @@ { "id": "security", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Security enforcement", "description": "Threat mitigation verification and ship-time security blocking for phases with security enforcement enabled.", "tier": "full", diff --git a/capabilities/tdd/capability.json b/capabilities/tdd/capability.json index 3ba1beb73..1231ae07b 100644 --- a/capabilities/tdd/capability.json +++ b/capabilities/tdd/capability.json @@ -1,7 +1,7 @@ { "id": "tdd", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", diff --git a/capabilities/trae/capability.json b/capabilities/trae/capability.json index 9d6a20a39..c9e6b6fa4 100644 --- a/capabilities/trae/capability.json +++ b/capabilities/trae/capability.json @@ -1,7 +1,7 @@ { "id": "trae", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Trae IDE", "description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.", "tier": "core", diff --git a/capabilities/ui/capability.json b/capabilities/ui/capability.json index 600228e4a..f20406dbe 100644 --- a/capabilities/ui/capability.json +++ b/capabilities/ui/capability.json @@ -1,7 +1,7 @@ { "id": "ui", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "UI design contracts", "description": "UI-SPEC design contract + retrospective UI audit for frontend phases.", "tier": "full", diff --git a/capabilities/windsurf/capability.json b/capabilities/windsurf/capability.json index 7421c4a7a..52ae07d6e 100644 --- a/capabilities/windsurf/capability.json +++ b/capabilities/windsurf/capability.json @@ -1,7 +1,7 @@ { "id": "windsurf", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Windsurf", "description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.", "tier": "core", diff --git a/capabilities/zcode/capability.json b/capabilities/zcode/capability.json index debc29f01..48060579f 100644 --- a/capabilities/zcode/capability.json +++ b/capabilities/zcode/capability.json @@ -1,7 +1,7 @@ { "id": "zcode", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "ZCode", "description": "ZCode (Z.ai) — desktop Agentic Development Environment for GLM-5.2; Claude-shaped nested skills at ~/.zcode/skills//SKILL.md, slash commands, named subagents, native MCP; declarative plugin surface; profile-marker install; tier-2 community support.", "tier": "core", diff --git a/commands/gsd/map-codebase.md b/commands/gsd/map-codebase.md index b48db59a5..c11d56d69 100644 --- a/commands/gsd/map-codebase.md +++ b/commands/gsd/map-codebase.md @@ -9,7 +9,7 @@ allowed-tools: - Grep - Write - Agent -requires: [config, new-project, plan-phase] +requires: [config, new-project, plan-phase, onboard] --- @@ -42,7 +42,7 @@ Parse the first token of $ARGUMENTS: Check for .planning/STATE.md - loads context if project already initialized **This command can run:** -- Before /gsd:new-project (brownfield codebases) - creates codebase map first +- Via /gsd:onboard for first-time brownfield setup - creates codebase map first - After /gsd:new-project (greenfield codebases) - updates codebase map as code evolves - Anytime to refresh codebase understanding @@ -51,7 +51,7 @@ Check for .planning/STATE.md - loads context if project already initialized **Use map-codebase for:** - Brownfield projects before initialization (understand existing code first) - Refreshing codebase map after significant changes -- Onboarding to an unfamiliar codebase +- Refreshing or deepening an onboarded codebase map - Before major refactoring (understand current state) - When STATE.md references outdated codebase info @@ -71,7 +71,7 @@ Check for .planning/STATE.md - loads context if project already initialized 4. Wait for agents to complete, collect confirmations (NOT document contents) 5. Verify all 7 documents exist with line counts 6. Commit codebase map -7. Offer next steps (typically: /gsd:new-project or /gsd:plan-phase) +7. Offer next steps (typically: /gsd:onboard, /gsd:new-project, or /gsd:plan-phase) diff --git a/commands/gsd/ns-project.md b/commands/gsd/ns-project.md index 3deb4943f..ae6e1748a 100644 --- a/commands/gsd/ns-project.md +++ b/commands/gsd/ns-project.md @@ -5,7 +5,7 @@ argument-hint: "" allowed-tools: - Read - Skill -requires: [new-project, new-milestone, complete-milestone, audit-milestone, milestone-summary, import, ingest-docs, profile-user, review-backlog] +requires: [new-project, onboard, new-milestone, complete-milestone, audit-milestone, milestone-summary, import, ingest-docs, profile-user, review-backlog] --- Route to the appropriate project / milestone skill based on the user's intent. @@ -15,6 +15,7 @@ inline as part of `gsd-audit-milestone`'s output. | User wants | Invoke | |---|---| | Start a new project | gsd-new-project | +| Onboard an existing codebase | gsd-onboard | | Create a new milestone | gsd-new-milestone | | Complete the current milestone | gsd-complete-milestone | | Audit a milestone for issues | gsd-audit-milestone | diff --git a/commands/gsd/onboard.md b/commands/gsd/onboard.md new file mode 100644 index 000000000..e51fd9919 --- /dev/null +++ b/commands/gsd/onboard.md @@ -0,0 +1,46 @@ +--- +name: gsd:onboard +description: Guide existing codebase onboarding through mapping, doc ingest, and planning setup +argument-hint: "[--fast] [--text]" +allowed-tools: + - Read + - Bash + - Write + - Glob + - Grep + - Agent + - AskUserQuestion +requires: [config, new-project, map-codebase, ingest-docs, manager] +--- + +**Copilot (VS Code):** Use `vscode_askquestions` wherever this workflow calls `AskUserQuestion`. They are equivalent — `vscode_askquestions` is the VS Code Copilot implementation of the same interactive question API. + + + +Guide brownfield onboarding for an existing codebase by routing through the existing GSD primitives in the safe order: codebase map → docs ingest → project initialization → onboarding summary. + +**Creates or confirms:** +- `.planning/codebase/` — evidence-backed codebase map from `/gsd:map-codebase` +- `.planning/PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, `STATE.md` — project setup from `/gsd:new-project` or `/gsd:ingest-docs` +- `.planning/onboarding/SUMMARY.md` — lightweight index of what was learned and the next command + +**Non-goals:** This command does not execute phases, ship work, or overwrite existing planning artifacts without an explicit gate. + + + +@~/.claude/gsd-core/workflows/onboard.md +@~/.claude/gsd-core/references/ui-brand.md +@~/.claude/gsd-core/references/gate-prompts.md + + + +Arguments: $ARGUMENTS + +Flags: +- `--fast` — prefer `/gsd:map-codebase --fast` for the mapping handoff; the complete map is still required before `/gsd:new-project`. +- `--text` — use plain-text numbered lists instead of TUI menus. + + + +Execute the onboard workflow end-to-end. Preserve all safety gates, text-mode fallbacks, idempotency checks, and top-level handoff rules for nested interactive commands. + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 07e25df3a..760409bcd 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -620,7 +620,8 @@ Equivalent paths for other runtimes: │ ├── FEATURES.md │ ├── ARCHITECTURE.md │ └── PITFALLS.md -├── codebase/ # Brownfield mapping (from /gsd-map-codebase) +├── codebase/ # Brownfield mapping (from /gsd-map-codebase or /gsd-onboard) +├── onboarding/ # Brownfield onboarding summary (from /gsd-onboard) │ ├── STACK.md # YAML frontmatter carries `last_mapped_commit` │ ├── ARCHITECTURE.md # for the post-execute drift gate (#2003) │ ├── CONVENTIONS.md diff --git a/docs/CLI-TOOLS.md b/docs/CLI-TOOLS.md index 392656603..923a1f757 100644 --- a/docs/CLI-TOOLS.md +++ b/docs/CLI-TOOLS.md @@ -421,13 +421,14 @@ node gsd-tools.cjs scaffold phase-dir --phase N --name "phase name" ## Init Commands (Compound Context Loading) -Load all context needed for a specific workflow in one call. Returns JSON with project info, config, state, and workflow-specific data. +Load all context needed for a specific workflow in one call. Returns JSON with project info, config, state, and workflow-specific data. `init onboard [--fast] [--text]` reports brownfield signals, planning-doc candidates, codebase-map completeness, fast-map readiness, text-mode routing, partial planning state, and onboarding summary status for `/gsd-onboard`. ```bash node gsd-tools.cjs init execute-phase node gsd-tools.cjs init plan-phase node gsd-tools.cjs init new-project node gsd-tools.cjs init new-milestone +node gsd-tools.cjs init onboard [--fast] [--text] node gsd-tools.cjs init quick node gsd-tools.cjs init resume node gsd-tools.cjs init verify-work diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index e8f968d1b..22216ff54 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -58,6 +58,25 @@ Initialize a new project with deep context gathering. --- +### `/gsd-onboard` + +Guide an existing codebase through first-time GSD onboarding. The command checks repo state, routes you through codebase mapping, optional docs ingest, project initialization, and creates an onboarding summary once planning exists. + +| Flag | Description | +|------|-------------| +| `--fast` | Prefer the lightweight `/gsd-map-codebase --fast` mapping handoff; a complete map is still required before `/gsd-new-project` | +| `--text` | Use numbered plain-text gates instead of TUI menus | + +**Prerequisites:** Existing repo or planning docs. For empty greenfield projects, use `/gsd-new-project`. +**Produces:** `.planning/codebase/` via map-codebase, `.planning/` via new-project or ingest-docs, and `.planning/onboarding/SUMMARY.md` after project setup. + +```bash +/gsd-onboard # Guided brownfield onboarding +/gsd-onboard --fast # Use lightweight codebase mapping first, then complete the map before project setup +``` + +--- + ### `/gsd-workspace` Manage GSD workspaces — create, list, or remove isolated workspace environments with repo copies and independent `.planning/` directories. @@ -1183,7 +1202,7 @@ gsd capability remove my-cap --scope project # Turn the installed overl ### `/gsd-map-codebase` -Analyze existing codebase with parallel mapper agents. Use `--fast` for a quick single-agent scan, or `--query` to search existing intel. +Analyze existing codebase with parallel mapper agents. Use `--fast` for a quick single-agent scan, or `--query` to search existing intel. First-time brownfield setup should usually start with `/gsd-onboard`, which hands off to this command when a map is missing. | Argument | Required | Description | |----------|----------|-------------| diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 0019ee7dd..1a80bd7c3 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -38,6 +38,7 @@ - [Model Profiles](#26-model-profiles) - [Brownfield Features](#brownfield-features) - [Codebase Mapping](#27-codebase-mapping) + - [Existing Codebase Onboarding](#27b-existing-codebase-onboarding) - [Utility Features](#utility-features) - [Debug System](#28-debug-system) - [Todo Management](#29-todo-management) @@ -789,7 +790,7 @@ **Command:** `/gsd-map-codebase [area]` -**Purpose:** Analyze an existing codebase before starting a new project, so GSD understands what exists. +**Purpose:** Analyze an existing codebase before starting a new project or as the mapping handoff from `/gsd-onboard`, so GSD understands what exists. **Requirements:** - REQ-MAP-01: System MUST spawn parallel mapper agents for each analysis area @@ -817,6 +818,27 @@ only the subtrees the phase actually changed. Each produced document carries `last_mapped_commit` in its YAML frontmatter so drift can be measured against the mapping point, not HEAD. +### 27b. Existing Codebase Onboarding + +**Command:** `/gsd-onboard [--fast] [--text]` + +**Purpose:** Guide first-time setup for an existing repository by checking brownfield state, routing through codebase mapping and docs ingest, then handing off to project initialization without silently overwriting planning artifacts. + +**Requirements:** +- REQ-ONBOARD-01: System MUST detect existing code, package manifests, planning documents, partial `.planning/` state, and complete or missing codebase-map files. +- REQ-ONBOARD-02: System MUST hand off to `/gsd-map-codebase` or `/gsd-map-codebase --fast` when brownfield code lacks the required `.planning/codebase/` map files; fast-map readiness is partial and MUST NOT be treated as sufficient for `/gsd-new-project`. +- REQ-ONBOARD-03: System MUST offer `/gsd-ingest-docs` before `/gsd-new-project` when ADR/PRD/SPEC/RFC candidates exist and no project exists. +- REQ-ONBOARD-04: System MUST refuse to report onboarding complete until `PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, and `STATE.md` all exist. +- REQ-ONBOARD-05: System MUST create or confirm `.planning/onboarding/SUMMARY.md` only after project setup exists. +- REQ-ONBOARD-06: System MUST support `--text` for numbered plain-text gates on runtimes without interactive menus. + +**Produces:** +| Artifact | Description | +|----------|-------------| +| `.planning/codebase/` | Codebase map produced by the `/gsd-map-codebase` handoff | +| `.planning/PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, `STATE.md` | Planning setup produced by `/gsd-new-project` or `/gsd-ingest-docs` | +| `.planning/onboarding/SUMMARY.md` | Onboarding status, artifact index, and next-command summary | + ### 27a. Post-Execute Codebase Drift Detection **Introduced by:** #2003 diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index fd5c6fecc..bb03ddcf2 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -78,6 +78,7 @@ "/gsd-ns-project", "/gsd-ns-review", "/gsd-ns-workflow", + "/gsd-onboard", "/gsd-pause-work", "/gsd-phase", "/gsd-plan-phase", @@ -160,6 +161,7 @@ "next.md", "node-repair.md", "note.md", + "onboard.md", "pause-work.md", "plan-milestone-gaps.md", "plan-phase.md", @@ -225,6 +227,7 @@ "gates.md", "git-integration.md", "git-planning-commit.md", + "gsd-run-resolver.md", "honest-verifier.md", "ios-scaffold.md", "loop-hook-dispatch.md", @@ -307,6 +310,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", @@ -368,6 +373,7 @@ "model-catalog.cjs", "model-profiles.cjs", "model-resolver.cjs", + "onboard-projection.cjs", "package-identity.cjs", "package-legitimacy.cjs", "phase-command-router.cjs", diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md index 54d96e7b9..3e2b8824b 100644 --- a/docs/INVENTORY.md +++ b/docs/INVENTORY.md @@ -79,6 +79,7 @@ These six routers are descriptor-only entries that the model picks first; the bo | Command | Role | Source | |---------|------|--------| | `/gsd-new-project` | Initialize a new project with deep context gathering and PROJECT.md. | [commands/gsd/new-project.md](../commands/gsd/new-project.md) | +| `/gsd-onboard` | Guide existing codebase onboarding through mapping, docs ingest, project setup, and onboarding summary. | [commands/gsd/onboard.md](../commands/gsd/onboard.md) | | `/gsd-workspace` | Manage GSD workspaces — create (`--new`), list (`--list`), or remove (`--remove`) isolated workspace environments. | [commands/gsd/workspace.md](../commands/gsd/workspace.md) | | `/gsd-discuss-phase` | Gather phase context through adaptive questioning before planning. | [commands/gsd/discuss-phase.md](../commands/gsd/discuss-phase.md) | | `/gsd-mvp-phase` | Plan a phase as a vertical MVP slice — user story, SPIDR splitting, then plan-phase. | [commands/gsd/mvp-phase.md](../commands/gsd/mvp-phase.md) | @@ -223,6 +224,7 @@ Full roster at `gsd-core/workflows/*.md`. Workflows are thin orchestrators that | `milestone-summary.md` | Milestone summary synthesis — onboarding and review artifact from milestone artifacts. | `/gsd-milestone-summary` | | `new-milestone.md` | Start a new milestone cycle — load project context, gather goals, update PROJECT.md/STATE.md. | `/gsd-new-milestone` | | `new-project.md` | Unified new-project flow — questioning, research (optional), requirements, roadmap. | `/gsd-new-project` | +| `onboard.md` | Brownfield onboarding orchestration — map codebase, ingest docs, initialize planning, summarize next step. | `/gsd-onboard` | | `new-workspace.md` | Create an isolated workspace with repo worktrees/clones and an independent `.planning/`. | `/gsd-workspace --new` | | `next.md` | Detect current project state and automatically advance to the next logical step. | `/gsd-progress --next` | | `node-repair.md` | Autonomous repair operator for failed task verification; invoked by `execute-plan`. | `execute-plan.md` (recovery) | diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index e06c10d4d..aaaee31af 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -79,7 +79,7 @@ The core GSD loop is: **discuss → plan → execute → verify → ship**, repe See [Your first project](tutorials/your-first-project.md). -For onboarding an existing codebase before starting a new milestone, see [Onboarding an existing codebase](tutorials/onboarding-an-existing-codebase.md). +For onboarding an existing codebase before starting a new milestone, run `/gsd-onboard` or see [Onboarding an existing codebase](tutorials/onboarding-an-existing-codebase.md). **Relevant flags at a glance:** @@ -536,11 +536,13 @@ claude --dangerously-skip-permissions ### Existing Codebase ```bash -/gsd-map-codebase # Analyse what exists (parallel agents) -/gsd-new-project # Questions focus on what you're ADDING +/gsd-onboard # Safely map, ingest docs, and initialize planning +# Follow the printed top-level handoff commands, then rerun /gsd-onboard # (normal phase workflow from here) ``` +`/gsd-onboard` routes through `/gsd-map-codebase`, `/gsd-ingest-docs`, and `/gsd-new-project` without nesting interactive workflows or overwriting existing planning files silently. + **Post-execute drift detection (#2003).** After every `/gsd-execute-phase`, GSD checks whether the phase introduced enough structural change to make `.planning/codebase/STRUCTURE.md` stale. Flip the behavior with: ```bash @@ -986,7 +988,8 @@ To disable parallel execution entirely: `/gsd-settings` → set `parallelization themes/ default.css # Shared CSS variables for all sketches MANIFEST.md # Index of all sketches with winners - codebase/ # Brownfield codebase mapping (from /gsd-map-codebase) + codebase/ # Brownfield codebase mapping (from /gsd-map-codebase or /gsd-onboard) + onboarding/ # Brownfield onboarding summary (from /gsd-onboard) phases/ XX-phase-name/ XX-YY-PLAN.md # Atomic execution plans diff --git a/docs/adr/1143-claude-orchestration-capability.md b/docs/adr/1143-claude-orchestration-capability.md index 128a40909..4541bce43 100644 --- a/docs/adr/1143-claude-orchestration-capability.md +++ b/docs/adr/1143-claude-orchestration-capability.md @@ -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. diff --git a/docs/adr/1990-existing-code-onboarding.md b/docs/adr/1990-existing-code-onboarding.md new file mode 100644 index 000000000..aa9b47c3f --- /dev/null +++ b/docs/adr/1990-existing-code-onboarding.md @@ -0,0 +1,69 @@ +# Existing Code Onboarding Module owns deterministic repo-state detection and onboarding route selection + +- **Status:** Proposed +- **Date:** 2026-07-06 +- **Issue:** #1990 +- **Implementation:** PR #1994 + +## Context + +GSD already ships strong individual primitives for adopting an existing codebase: `/gsd:map-codebase` (parallel codebase analysis), `/gsd:ingest-docs` (classify and consolidate existing ADR/PRD/SPEC/RFC docs), and `/gsd:new-project` (planning initialization). What it lacked was a single guided entry point that inspects a brownfield repository and tells the user *which primitive runs first*. + +Left to prose alone, that ordering is ambiguous and unsafe: a user can initialize planning before a codebase map exists, skip relevant design docs, or overwrite/duplicate `.planning/` context instead of reusing it. The ordering is not a matter of taste — it is a **dependency graph** (a map should exist before planning; existing design docs should be ingested before a fresh `/gsd:new-project`; nothing should clobber an in-progress `.planning/`). A dependency graph that decides the next safe action from filesystem state is a *projection*, not something a workflow's natural-language instructions can evaluate reliably or test. + +GSD already has the seam for this. The **Init Command Module** (`src/init.cts` → `gsd-core/bin/lib/init.cjs`) owns the `init.*` family of query handlers that compose atomic queries into the flat JSON bundles that init workflows consume, alongside the projection-module precedent set by the **Planning Path Projection Module** (ADR-0006) and the **Shell Command Projection Module** (ADR-0009). Adding `/gsd:onboard` as free-form workflow prose that scans the tree inline would put untested, non-deterministic filesystem logic in markdown — precisely the anti-pattern those projection modules exist to prevent. + +## Decision + +Introduce the **Existing Code Onboarding Module** (implemented as the `src/onboard-projection.cts` → `gsd-core/bin/lib/onboard-projection.cjs` projection) as the Seam that owns **deterministic detection of brownfield repository state and the selection of the next onboarding action**. It is a pure, side-effect-free projection consumed by the Init Command Module's `initOnboard` handler and rendered by the `/gsd:onboard` workflow. It never writes; detection and route selection are a function of repository state only. + +**Detected state (inputs):** + +| Signal | Rule / invariant | +|---|---| +| Brownfield code present | Depth-capped recursive scan for source files (`hasCodeFilesInternal`) OR a recognized package manifest (`hasPackageFileInternal`). | +| Generated / vendor exclusion | Scan skips `CODE_SCAN_SKIP_DIRS` (`node_modules`, `dist`, `build`, `.next`, `.nuxt`, `.svelte-kit`, `coverage`, `vendor`, `.venv`, `venv`) so vendored trees never produce a false brownfield positive. | +| Codebase-map completeness | Whether `.planning/codebase/` holds the canonical map artifacts. | +| Existing design docs | Presence of ADR/PRD/SPEC/RFC-style candidates (root, nested, and segment-based). | +| Partial planning state | Whether some but not all of `PROJECT.md` / `REQUIREMENTS.md` / `ROADMAP.md` / `STATE.md` exist. | + +**Route selection (output), ordered by dependency, not by convenience:** + +1. Brownfield code without a complete `.planning/codebase/` map → hand off to `/gsd:map-codebase` (or `/gsd:map-codebase --fast` in fast mode). +2. Design-doc candidates present and no project yet → offer `/gsd:ingest-docs` **before** `/gsd:new-project`. +3. Otherwise → `/gsd:new-project`. + +The gate order is load-bearing: **partial-planning and fast-map-completeness are evaluated before the docs-ingest branch**, so a half-mapped or half-initialized repo is never routed past the step it still owes. Handoff commands are runtime-formatted (`buildHandoffCommands` / `formatGsdSlash`) so the projected next command is correct for the installed runtime's slash syntax. + +**Safety invariants (the reason this is a Module, not a helper):** + +- **Idempotent / no silent overwrite.** Onboarding never mutates existing tracked `.planning/` artifacts; re-running leaves them byte-unchanged. +- **`SUMMARY.md` is a trailing artifact.** `.planning/onboarding/SUMMARY.md` is written only *after* project setup exists, and only if absent. +- **"Complete" is a conjunction.** Onboarding does not report complete until `PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, and `STATE.md` all exist — no single-file short-circuit. +- **Text-mode parity.** `--text` renders the same gate decisions as numbered plain-text prompts, so runtimes without an interactive picker get identical routing. + +## What stays OUTSIDE this Module + +- **The primitives themselves.** `/gsd:map-codebase`, `/gsd:ingest-docs`, and `/gsd:new-project` retain their own behavior; the Module only *chooses and orders* them. It projects the route; it does not re-implement the destinations. +- **Writing planning artifacts.** All `.planning/` writes remain owned by the destination commands and the Installer/planning modules. The projection is read-only. +- **The workflow's rendering.** `gsd-core/workflows/onboard.md` owns menu/gate presentation; the command `commands/gsd/onboard.md` (and its skill mirror) owns delegation. The Module owns only the state→route decision they consume. + +## Consequences + +- Brownfield onboarding becomes a single, testable entry point with deterministic routing, rather than order-of-operations folklore in prose. The projection is unit-tested (`tests/onboard-command.test.cjs`) for brownfield/greenfield detection, vendor-dir exclusion, gate ordering (partial-planning before docs-ingest), idempotency/no-mutation, and runtime-formatted handoffs. +- The Init Command Module gains one more heavyweight handler (`initOnboard`) with the same `{ data: }` contract as its siblings — no new dispatch shape. +- **New maintenance coupling, now explicit.** The Module's completeness checks must track the canonical `.planning/codebase/` artifact list and the routing targets' identities; if `/gsd:map-codebase` / `/gsd:ingest-docs` / `/gsd:new-project` change their entry contracts, this projection must follow. This ADR records that coupling as the known cost of centralizing the routing decision (the alternative — duplicating the decision across each primitive — is worse). +- No new runtime dependencies; no change to existing command semantics (additive). + +## Open questions + +- Should codebase-map completeness be sourced from a single shared predicate (owned by the map module) rather than re-encoded here, so the two cannot drift? +- The `/gsd:onboard` workflow sources its `gsd_run` bootstrap from a shared `references/gsd-run-resolver.md` snippet rather than inlining it. If that delegation pattern is adopted by other workflows, it likely deserves its own short ADR — noting it here so the precedent is visible rather than silently established. + +## References + +- ADR-0006 — Planning Path Projection Module (projection-module precedent for `.planning` path resolution). +- ADR-0009 — Shell Command Projection Module (runtime-aware projection precedent). +- Init Command Module (`src/init.cts` → `gsd-core/bin/lib/init.cjs`) — owner of the `init.*` handler family that consumes this projection via `initOnboard`. +- `CONTEXT.md` § Init Command Module — where the onboarding projection is registered. +- Issue #1990 — feature spec and acceptance criteria. diff --git a/docs/adr/README.md b/docs/adr/README.md index 5922a7422..03038a5c9 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -64,6 +64,7 @@ See **[CONTRIBUTING.md — "Proposing an ADR or PRD"](../../CONTRIBUTING.md#prop | [1769-state-md-transition-module.md](1769-state-md-transition-module.md) | STATE.md Transition Module — intent-based transitions over scattered RMW callbacks | Proposed | | [1817-state-md-rebuild-derivability-contract.md](1817-state-md-rebuild-derivability-contract.md) | STATE.md rebuild — derivability contract (capstone 11th transition) | Accepted | | [2008-command-exit-zero-gate.md](2008-command-exit-zero-gate.md) | Generic gate-predicate evaluator with a `command-exit-zero` kind (#2008) | Accepted | +| [1990-existing-code-onboarding.md](1990-existing-code-onboarding.md) | Existing Code Onboarding Module owns deterministic repo-state detection and onboarding route selection | Proposed | ## Seam map diff --git a/docs/explanation/claude-orchestration-capability.md b/docs/explanation/claude-orchestration-capability.md new file mode 100644 index 000000000..e044a4e1f --- /dev/null +++ b/docs/explanation/claude-orchestration-capability.md @@ -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("")` | +| Phase token cap | `budget()` | + +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 diff --git a/docs/how-to/enable-claude-orchestration-workflow-backend.md b/docs/how-to/enable-claude-orchestration-workflow-backend.md new file mode 100644 index 000000000..a8fea05e1 --- /dev/null +++ b/docs/how-to/enable-claude-orchestration-workflow-backend.md @@ -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("")`** wired to the phase run id, +- **`budget()`** — 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. diff --git a/docs/how-to/fix-worktree-base-mismatch.md b/docs/how-to/fix-worktree-base-mismatch.md index 16b445198..e442d26ff 100644 --- a/docs/how-to/fix-worktree-base-mismatch.md +++ b/docs/how-to/fix-worktree-base-mismatch.md @@ -1,16 +1,16 @@ # How to fix the worktree base-mismatch (exit 42) error -**Goal:** Understand why `/gsd-execute-phase` halts with `FATAL: worktree base mismatch` / exit 42 when your branch is ahead of the default branch, and choose the right fix to restore normal — or parallel — execution. +**Goal:** Understand why `/gsd-execute-phase` or `/gsd-quick` halts with `FATAL: worktree base mismatch` / exit 42 when your branch is ahead of the default branch, and choose the right fix to restore normal — or parallel — execution. -**Prerequisites:** GSD Core is installed and you have an active project. You have run `/gsd-execute-phase` and either seen the exit-42 error or the one-line `⚠ Worktree base mismatch` warning. +**Prerequisites:** GSD Core is installed and you have an active project. You have run `/gsd-execute-phase` or `/gsd-quick` and either seen the exit-42 error or the one-line `⚠ Worktree base mismatch` warning. --- ## What you will see -When you run `/gsd-execute-phase` on a branch that is ahead of the repository's default branch (for example, an unmerged milestone branch, a long-lived feature branch, or a branch with commits not yet in `origin/HEAD`), you may see one of two messages: +When you run `/gsd-execute-phase` or `/gsd-quick` on a branch that is ahead of the repository's default branch (for example, an unmerged milestone branch, a long-lived feature branch, or a branch with commits not yet in `origin/HEAD`), you may see one of two messages: -**Automatic-degrade warning (phase still completes):** +**Automatic-degrade warning (phase or quick task still completes):** ``` ⚠ Worktree base mismatch: HEAD (abc12345) differs from origin/HEAD (def67890). @@ -19,7 +19,7 @@ To keep parallel worktrees, set worktree.baseRef:"head" in .claude/settings.local.json (or run: gsd-tools worktree set-baseref). See #683. ``` -The phase runs to completion sequentially; nothing is blocked. This is the runtime mitigation. +The phase or quick task runs to completion sequentially; nothing is blocked. This is the runtime mitigation (`/gsd-execute-phase`: #683/#1369; `/gsd-quick`: #1941). **Exit-42 halt (older installs or misconfigured environments):** diff --git a/docs/how-to/install-minimal-and-add-skills.md b/docs/how-to/install-minimal-and-add-skills.md index e30537af7..53a6f772d 100644 --- a/docs/how-to/install-minimal-and-add-skills.md +++ b/docs/how-to/install-minimal-and-add-skills.md @@ -45,7 +45,7 @@ If `core` is too small, pick a wider profile instead. Pass it with `--profile= node gsd-tools.cjs init plan-phase node gsd-tools.cjs init new-project node gsd-tools.cjs init new-milestone +node gsd-tools.cjs init onboard [--fast] [--text] node gsd-tools.cjs init quick node gsd-tools.cjs init resume node gsd-tools.cjs init verify-work diff --git a/docs/ja-JP/COMMANDS.md b/docs/ja-JP/COMMANDS.md index d63034e51..9a2bb72f4 100644 --- a/docs/ja-JP/COMMANDS.md +++ b/docs/ja-JP/COMMANDS.md @@ -51,6 +51,25 @@ v1.40 では、最初のステージエントリーポイントとして6つの --- +### `/gsd-onboard` + +既存コードベースの初回 GSD オンボーディングを案内します。リポジトリ状態を確認し、コードベースマッピング、任意のドキュメント取り込み、プロジェクト初期化へ安全にハンドオフし、計画が揃った後にオンボーディング summary を作成します。 + +| フラグ | 説明 | +|------|-------------| +| `--fast` | 軽量な `/gsd-map-codebase --fast` マッピングハンドオフを優先。ただし `/gsd-new-project` 前には完全なマップが必要 | +| `--text` | TUI メニューではなく番号付きプレーンテキストのゲートを使用 | + +**前提条件:** 既存リポジトリまたは計画ドキュメント。空のグリーンフィールドプロジェクトには `/gsd-new-project` を使用します。 +**生成物:** map-codebase による `.planning/codebase/`、new-project または ingest-docs による `.planning/`、セットアップ後の `.planning/onboarding/SUMMARY.md`。 + +```bash +/gsd-onboard # ガイド付き brownfield オンボーディング +/gsd-onboard --fast # 先に軽量マップを使い、その後プロジェクト設定前に完全マップを作成 +``` + +--- + ### `/gsd-workspace` GSD ワークスペースを管理 — リポジトリコピーと独立した `.planning/` ディレクトリを持つ隔離されたワークスペース環境を作成、一覧表示、または削除します。 diff --git a/docs/ja-JP/FEATURES.md b/docs/ja-JP/FEATURES.md index 6f91953c6..31c1da45c 100644 --- a/docs/ja-JP/FEATURES.md +++ b/docs/ja-JP/FEATURES.md @@ -38,6 +38,7 @@ - [モデルプロファイル](#26-モデルプロファイル) - [ブラウンフィールド機能](#ブラウンフィールド機能) - [コードベースマッピング](#27-コードベースマッピング) + - [既存コードベースオンボーディング](#27b-既存コードベースオンボーディング) - [ユーティリティ機能](#ユーティリティ機能) - [デバッグシステム](#28-デバッグシステム) - [Todo 管理](#29-todo-管理) @@ -781,7 +782,7 @@ **コマンド:** `/gsd-map-codebase [area]` -**目的:** 新しいプロジェクトを開始する前に既存のコードベースを分析し、GSD が既存の構成を理解できるようにします。 +**目的:** 新しいプロジェクトを開始する前、または `/gsd-onboard` からのマッピングハンドオフとして既存のコードベースを分析し、GSD が既存の構成を理解できるようにします。 **要件:** - REQ-MAP-01: システムは各分析領域に対して並列マッパーエージェントを起動しなければならない @@ -805,6 +806,27 @@ `--paths ` スコープヒントを受け付けます。指定した場合、ツリー全体をスキャンする代わりに、リストされたリポジトリ相対プレフィックスに探索を制限します。 これはフェーズが実際に変更したサブツリーのみを更新するために、実行後コードベースドリフトゲートが使用するパスウェイです。各生成ドキュメントはその YAML フロントマターに `last_mapped_commit` を持ち、ドリフトを HEAD ではなくマッピング時点と照らし合わせて計測できます。 +### 27b. 既存コードベースオンボーディング + +**コマンド:** `/gsd-onboard [--fast] [--text]` + +**目的:** 既存リポジトリの初回セットアップを案内し、brownfield 状態を確認してコードベースマッピング、docs 取り込み、プロジェクト初期化へ安全にハンドオフします。 + +**要件:** +- REQ-ONBOARD-01: 既存コード、package manifest、計画ドキュメント、部分的な `.planning/`、コードベースマップの不足を検出する。 +- REQ-ONBOARD-02: 必要な `.planning/codebase/` マップファイルがない brownfield では `/gsd-map-codebase` または `/gsd-map-codebase --fast` へハンドオフする。fast マップの readiness は部分的であり、`/gsd-new-project` に十分として扱ってはならない。 +- REQ-ONBOARD-03: ADR/PRD/SPEC/RFC 候補があり project がない場合、`/gsd-new-project` の前に `/gsd-ingest-docs` を提示する。 +- REQ-ONBOARD-04: `PROJECT.md`、`REQUIREMENTS.md`、`ROADMAP.md`、`STATE.md` が揃うまで完了扱いにしない。 +- REQ-ONBOARD-05: project setup 後にのみ `.planning/onboarding/SUMMARY.md` を作成または確認する。 +- REQ-ONBOARD-06: 対話型メニューがない runtime 向けに、`--text` で番号付きプレーンテキスト gate をサポートする。 + +**生成物:** +| Artifact | 説明 | +|----------|-------------| +| `.planning/codebase/` | `/gsd-map-codebase` handoff が生成するコードベースマップ | +| `.planning/PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, `STATE.md` | `/gsd-new-project` または `/gsd-ingest-docs` が生成する planning setup | +| `.planning/onboarding/SUMMARY.md` | Onboarding status、artifact index、next-command summary | + ### 27a. 実行後コードベースドリフト検出 **導入:** #2003 diff --git a/docs/ja-JP/INVENTORY.md b/docs/ja-JP/INVENTORY.md index a1d5a6a70..506e18442 100644 --- a/docs/ja-JP/INVENTORY.md +++ b/docs/ja-JP/INVENTORY.md @@ -78,6 +78,7 @@ | コマンド | 役割 | ソース | |----------|------|--------| | `/gsd-new-project` | 深いコンテキスト収集と PROJECT.md で新しいプロジェクトを初期化。 | [commands/gsd/new-project.md](../../commands/gsd/new-project.md) | +| `/gsd-onboard` | 既存コードベースをマッピング、ドキュメント取り込み、プロジェクト設定、onboarding summary へ案内。 | [commands/gsd/onboard.md](../../commands/gsd/onboard.md) | | `/gsd-workspace` | GSD ワークスペースを管理 — 独立したワークスペース環境を作成(`--new`)、一覧表示(`--list`)、削除(`--remove`)。 | [commands/gsd/workspace.md](../../commands/gsd/workspace.md) | | `/gsd-discuss-phase` | 計画前にアダプティブな質問でフェーズコンテキストを収集。 | [commands/gsd/discuss-phase.md](../../commands/gsd/discuss-phase.md) | | `/gsd-mvp-phase` | フェーズを垂直 MVP スライスとして計画 — ユーザーストーリー、SPIDR 分割、その後 plan-phase。 | [commands/gsd/mvp-phase.md](../../commands/gsd/mvp-phase.md) | @@ -218,6 +219,7 @@ | `milestone-summary.md` | マイルストーンサマリー合成 — マイルストーンアーティファクトからオンボーディングとレビューアーティファクトを作成。 | `/gsd-milestone-summary` | | `new-milestone.md` | 新しいマイルストーンサイクルを開始 — プロジェクトコンテキストを読み込み、目標を収集して PROJECT.md/STATE.md を更新。 | `/gsd-new-milestone` | | `new-project.md` | 統合新プロジェクトフロー — 質問、調査(任意)、要件、ロードマップ。 | `/gsd-new-project` | +| `onboard.md` | Brownfield onboarding orchestration — コードベースをマップし、docs を取り込み、planning を初期化し、次のステップを要約。 | `/gsd-onboard` | | `new-workspace.md` | リポジトリのワークツリー/クローンと独立した `.planning/` を持つ独立したワークスペースを作成。 | `/gsd-workspace --new` | | `next.md` | 現在のプロジェクト状態を検出して次の論理的なステップに自動的に進む。 | `/gsd-progress --next` | | `node-repair.md` | タスク検証が失敗した場合の自律修復オペレーター。`execute-plan` から呼び出し。 | `execute-plan.md` (recovery) | diff --git a/docs/ja-JP/USER-GUIDE.md b/docs/ja-JP/USER-GUIDE.md index 264953822..15c4ee089 100644 --- a/docs/ja-JP/USER-GUIDE.md +++ b/docs/ja-JP/USER-GUIDE.md @@ -474,8 +474,8 @@ claude --dangerously-skip-permissions ### 既存のコードベース ```bash -/gsd-map-codebase # Analyse what exists (parallel agents) -/gsd-new-project # Questions focus on what you're ADDING +/gsd-onboard # Safely map, ingest docs, and initialize planning +# Follow printed handoff commands, then rerun /gsd-onboard # (normal phase workflow from here) ``` @@ -864,7 +864,8 @@ All subagent/executor commits MUST use `--no-verify`. themes/ default.css # Shared CSS variables for all sketches MANIFEST.md # Index of all sketches with winners - codebase/ # Brownfield codebase mapping (from /gsd-map-codebase) + codebase/ # Brownfield codebase mapping (from /gsd-map-codebase or /gsd-onboard) + onboarding/ # Brownfield onboarding summary (from /gsd-onboard) phases/ XX-phase-name/ XX-YY-PLAN.md # Atomic execution plans diff --git a/docs/ja-JP/how-to/install-on-your-runtime.md b/docs/ja-JP/how-to/install-on-your-runtime.md index 1c1900736..bcd26fc82 100644 --- a/docs/ja-JP/how-to/install-on-your-runtime.md +++ b/docs/ja-JP/how-to/install-on-your-runtime.md @@ -273,10 +273,11 @@ npx @opengsd/gsd-core@latest --opencode --global ## インストール後の作業 -新しいコマンドとエージェントを反映するためにランタイムを再起動してください。その後、最初のプロジェクトを開始します。 +新しいコマンドとエージェントを反映するためにランタイムを再起動してください。その後、新規プロジェクトを開始するか既存リポジトリをオンボーディングします。 ```bash -/gsd-new-project +/gsd-new-project # グリーンフィールドプロジェクト +/gsd-onboard # 既存コードベース ``` 再起動後もコマンドが見つからない場合は、インストールディレクトリがランタイムの期待する設定パスと一致しているか確認してください。最もよくある不一致については上記のプレリリースエディションのセクションを参照してください。 diff --git a/docs/ja-JP/reference/planning-artifacts.md b/docs/ja-JP/reference/planning-artifacts.md index 1f51165ca..f231de028 100644 --- a/docs/ja-JP/reference/planning-artifacts.md +++ b/docs/ja-JP/reference/planning-artifacts.md @@ -23,6 +23,8 @@ │ ├── architecture.md │ ├── stack.md │ └── ... +├── onboarding/ # ブラウンフィールドオンボーディング概要(オプション) +│ └── SUMMARY.md ├── intel/ # クエリ可能なシンボルインデックス(オプション、intel.enabled) │ └── API-SURFACE.md └── phases/ @@ -48,7 +50,7 @@ | | | |---|---| | **用途** | プロジェクトの正規アイデンティティ: 概要、対象ユーザー、コアバリュー、要件、制約、主要な意思決定。プロダクトの進化に伴いプロジェクトライフサイクル全体を通じて更新されます。 | -| **生成元** | `/gsd-new-project`(初回作成); 意思決定が検証されると `/gsd-complete-milestone` によって更新されます。 | +| **生成元** | `/gsd-new-project`(初回作成、`/gsd-onboard` のハンドオフを含む); 意思決定が検証されると `/gsd-complete-milestone` によって更新されます。 | | **参照先** | すべてのプランニングワークフロー; `gsd-phase-researcher`、`gsd-planner`(コンテキスト); `discuss-phase`(過去の意思決定); `gsd-plan-checker`(プロジェクト制約)。 | ### `ROADMAP.md` @@ -56,7 +58,7 @@ | | | |---|---| | **用途** | マイルストーンおよびフェーズ一覧。ゴール、要件 ID、成功基準、フェーズごとの正規リファレンスを含みます。プロジェクトが何をどの順序で構築するかに関する唯一の信頼できる情報源です。 | -| **生成元** | `/gsd-new-project`(初回作成); `/gsd-phase --insert` および `/gsd-complete-milestone` によって更新されます。 | +| **生成元** | `/gsd-new-project`(初回作成、`/gsd-onboard` のハンドオフを含む); `/gsd-phase --insert` および `/gsd-complete-milestone` によって更新されます。 | | **参照先** | `/gsd-discuss-phase`、`/gsd-plan-phase`、`/gsd-execute-phase`; フェーズ情報を必要とするすべてのオーケストレーションコマンド; `gsd-planner`、`gsd-plan-checker`、`gsd-phase-researcher`。 | ### `REQUIREMENTS.md` @@ -64,7 +66,7 @@ | | | |---|---| | **用途** | 番号付きのチェック可能な受け入れ基準。各要件はロードマップフェーズにマッピングされる ID(例: `AUTH-01`)を持ちます。フェーズが実行されると要件を完了済みとしてマークします。 | -| **生成元** | `/gsd-new-project`(初回作成); `execute-phase` によって要件が完了済みとしてマークされます。 | +| **生成元** | `/gsd-new-project`(初回作成、`/gsd-onboard` のハンドオフを含む); `execute-phase` によって要件が完了済みとしてマークされます。 | | **参照先** | `gsd-planner`(プランはすべてのフェーズ要件 ID に対処しなければならない); `gsd-plan-checker` ディメンション1(要件カバレッジ); `discuss-phase`(過去の要件)。 | ### `STATE.md` @@ -72,7 +74,7 @@ | | | |---|---| | **用途** | 現在地を追跡するリビングドキュメント — 現在のフェーズとプラン、進捗指標、蓄積された意思決定、セッション継続性ノート。すべてのワークフロー実行の開始時に読み込まれます。重要なアクションのたびに更新されます。 | -| **生成元** | `/gsd-new-project`(初回作成); すべてのフェーズワークフロー、`/gsd-pause-work`、`/gsd-resume-work` によって継続的に更新されます。 | +| **生成元** | `/gsd-new-project`(初回作成、`/gsd-onboard` のハンドオフを含む); すべてのフェーズワークフロー、`/gsd-pause-work`、`/gsd-resume-work` によって継続的に更新されます。 | | **参照先** | すべてのオーケストレーションワークフロー; `/gsd-progress`; `/gsd-quick` 経由のアドホックタスク実行; `gsd-planner` および `gsd-phase-researcher`(プロジェクトの意思決定)。 | 完全なフィールドリファレンスは [STATE.md スキーマ](state-md.md) を参照してください。 @@ -87,6 +89,14 @@ 完全なスキーマは [CONFIGURATION](../../CONFIGURATION.md) を参照してください。 +### `onboarding/SUMMARY.md`(オプション) + +| | | +|---|---| +| **用途** | ブラウンフィールドオンボーディングの索引。アーティファクト状態、コードベースマッピングの完了状況、初回セットアップ後に推奨される次の GSD コマンドを記録します。 | +| **生成元** | `PROJECT.md`、`REQUIREMENTS.md`、`ROADMAP.md`、`STATE.md` がすべて存在した後の `/gsd-onboard`。 | +| **参照先** | 初回セットアップを確認する人間、および既存オンボーディング状態を確認する今後の `/gsd-onboard` 実行。 | + ### `MILESTONES.md`(オプション) | | | diff --git a/docs/ja-JP/tutorials/onboarding-an-existing-codebase.md b/docs/ja-JP/tutorials/onboarding-an-existing-codebase.md index f2a268486..8a730dc98 100644 --- a/docs/ja-JP/tutorials/onboarding-an-existing-codebase.md +++ b/docs/ja-JP/tutorials/onboarding-an-existing-codebase.md @@ -44,15 +44,15 @@ claude --dangerously-skip-permissions --- -## ステップ 3 — コードベースのマッピング +## ステップ 3 — Brownfield オンボーディングの開始 -プロジェクトを作成する前に、GSD Core に既存のコードを学習させてください。これがブラウンフィールドの計画を正確にするステップです。 +プロジェクトを作成する前に、GSD Core にリポジトリ状態を確認させ、安全な次のトップレベルコマンドを表示させます。これにより、コードベースコンテキストの取りこぼしや既存 planning ファイルの上書きを防げます。 ```text -/gsd-map-codebase +/gsd-onboard ``` -GSD Core が4つの並行マッパーサブエージェントを生成します(「Spawning 4 parallel codebase mapper agents…」という通知が表示されます。1〜5分かかりますので中断しないでください)。各エージェントはそれぞれ異なる観点に注目します: +オンボーディングがコードベースマップ不足を示したら、推奨オプションを選び、表示された `/gsd-map-codebase` ハンドオフを実行してから `/gsd-onboard` を再実行します。`/gsd-onboard --fast` は軽量な初回パスには使えますが、`/gsd-new-project` の前には完全なマップが必要です。`/gsd-map-codebase` が4つの並行マッパーサブエージェントを生成します(「Spawning 4 parallel codebase mapper agents…」という通知が表示されます。1〜5分かかりますので中断しないでください)。各エージェントはそれぞれ異なる観点に注目します: | エージェント | 観点 | |-------|-------| @@ -84,7 +84,7 @@ Created .planning/codebase/: --- -## ステップ 4 — コンテキストをクリアしてプロジェクトを作成 +## ステップ 4 — オンボーディングを再実行してプロジェクトを初期化する セッションウィンドウをクリアします: @@ -92,12 +92,14 @@ Created .planning/codebase/: /clear ``` -プロジェクトを作成します。前のステップで GSD Core が既存のコードを見つけているため、これがブラウンフィールドプロジェクトであることをすでに把握しています。`/gsd-new-project` を実行すると、既存のものを再説明するのではなく、*追加する*内容に焦点を当てた質問がされます: +`/gsd-onboard` をもう一度実行します。GSD Core が ADR、PRD、spec、RFC、またはルートレベルの要件ドキュメントを検出した場合は、先に推奨される `/gsd-ingest-docs` ハンドオフを実行し、その後 `/gsd-onboard` を再実行してください。コンテキストが整うと、オンボーディングはプロジェクト初期化のハンドオフを表示します: ```text /gsd-new-project ``` +前のステップで GSD Core が既存のコードを見つけているため、`/gsd-new-project` はこれがブラウンフィールドプロジェクトだと分かっています。質問は既存のものを再説明するのではなく、*追加する*内容に焦点を当てます: + GSD Core が何を作りたいかを尋ねます。コードベース全体の説明ではなく、追加する機能で答えてください: ```text @@ -124,6 +126,8 @@ Proposed Roadmap ロードマップを承認してください。 +プロジェクト設定が完了したら、もう一度 `/gsd-onboard` を実行します。`PROJECT.md`、`REQUIREMENTS.md`、`ROADMAP.md`、`STATE.md` がすべて存在するため、オンボーディングは `.planning/onboarding/SUMMARY.md` を作成または確認します。 + **`.planning/` に作成されるファイル:** ```text @@ -133,6 +137,7 @@ Proposed Roadmap ROADMAP.md ← フェーズ 1、ステータス: pending STATE.md ← セッションメモリ config.json ← ワークフロー設定 + onboarding/SUMMARY.md ← オンボーディング状態と次のコマンド codebase/ ← ステップ 3 の7つのマップファイル ``` @@ -212,6 +217,7 @@ GSD Core があなたの `CONVENTIONS.md` と `ARCHITECTURE.md` を読み込ん ## 学んだこと +- `/gsd-onboard` が対話型コマンドをネストしたり既存 planning ファイルを上書きしたりせずに、ブラウンフィールド設定を安全に順序付ける仕組み。 - `/gsd-map-codebase` が4つの並行エージェントを実行して `.planning/codebase/` に `STACK.md`、`ARCHITECTURE.md`、`CONVENTIONS.md`、`CONCERNS.md`、`STRUCTURE.md`、`TESTING.md`、`INTEGRATIONS.md` を生成する仕組み。 - ブラウンフィールドリポジトリで `/gsd-new-project` を実行すると、*追加する*内容に焦点を当てた質問がされ、既存コードから Validated 要件が自動入力される仕組み。 - コードベースマップが `/gsd-discuss-phase` のすべての質問を形成する方法 — ファイルパス、パターン、規約が実際のコードから導出される。 diff --git a/docs/ko-KR/ARCHITECTURE.md b/docs/ko-KR/ARCHITECTURE.md index 3c8481647..2e737c75c 100644 --- a/docs/ko-KR/ARCHITECTURE.md +++ b/docs/ko-KR/ARCHITECTURE.md @@ -512,7 +512,8 @@ UI-SPEC.md (단계별) ─────────────────── │ ├── FEATURES.md │ ├── ARCHITECTURE.md │ └── PITFALLS.md -├── codebase/ # 브라운필드 매핑 (/gsd-map-codebase에서) +├── codebase/ # 브라운필드 매핑 (/gsd-map-codebase 또는 /gsd-onboard에서) +├── onboarding/ # 브라운필드 온보딩 요약 (/gsd-onboard에서) │ ├── STACK.md # YAML 전문에 `last_mapped_commit` 포함 │ ├── ARCHITECTURE.md # 실행 후 드리프트 게이트를 위한 (#2003) │ ├── CONVENTIONS.md diff --git a/docs/ko-KR/CLI-TOOLS.md b/docs/ko-KR/CLI-TOOLS.md index 80496bf35..7d9f76984 100644 --- a/docs/ko-KR/CLI-TOOLS.md +++ b/docs/ko-KR/CLI-TOOLS.md @@ -288,13 +288,14 @@ node gsd-tools.cjs scaffold phase-dir --phase N --name "phase name" ## Init 명령 (복합 컨텍스트 로딩) -하나의 호출로 특정 워크플로우에 필요한 모든 컨텍스트를 로드합니다. 프로젝트 정보, 설정, 상태, 워크플로우별 데이터가 포함된 JSON을 반환합니다. +하나의 호출로 특정 워크플로우에 필요한 모든 컨텍스트를 로드합니다. 프로젝트 정보, 설정, 상태, 워크플로우별 데이터가 포함된 JSON을 반환합니다. `init onboard [--fast] [--text]`는 `/gsd-onboard`를 위해 brownfield 신호, 계획 문서 후보, 코드베이스 맵 완성도, fast 맵 준비 상태, 텍스트 모드 라우팅, 부분 planning 상태, 온보딩 요약 상태를 반환합니다. ```bash node gsd-tools.cjs init execute-phase node gsd-tools.cjs init plan-phase node gsd-tools.cjs init new-project node gsd-tools.cjs init new-milestone +node gsd-tools.cjs init onboard [--fast] [--text] node gsd-tools.cjs init quick node gsd-tools.cjs init resume node gsd-tools.cjs init verify-work diff --git a/docs/ko-KR/COMMANDS.md b/docs/ko-KR/COMMANDS.md index 56c8f3625..338bb91d9 100644 --- a/docs/ko-KR/COMMANDS.md +++ b/docs/ko-KR/COMMANDS.md @@ -51,6 +51,25 @@ v1.40에서 여섯 개의 네임스페이스 라우터가 1단계 진입점으 --- +### `/gsd-onboard` + +기존 코드베이스의 최초 GSD 온보딩을 안내합니다. 저장소 상태를 확인하고 코드베이스 매핑, 선택적 문서 수집, 프로젝트 초기화로 안전하게 넘긴 뒤 계획 파일이 준비되면 onboarding summary를 만듭니다. + +| 플래그 | 설명 | +|------|-------------| +| `--fast` | 경량 `/gsd-map-codebase --fast` 매핑 handoff 우선 사용. 단, `/gsd-new-project` 전에는 완전한 맵이 필요 | +| `--text` | TUI 메뉴 대신 번호가 있는 plain-text gate 사용 | + +**전제 조건:** 기존 저장소 또는 계획 문서. 빈 greenfield 프로젝트는 `/gsd-new-project`를 사용하세요. +**생성 결과:** map-codebase의 `.planning/codebase/`, new-project 또는 ingest-docs의 `.planning/`, 설정 후 `.planning/onboarding/SUMMARY.md`. + +```bash +/gsd-onboard # 안내형 brownfield 온보딩 +/gsd-onboard --fast # 먼저 경량 맵을 사용하고, 프로젝트 설정 전 완전한 맵 작성 +``` + +--- + ### `/gsd-workspace` GSD 워크스페이스 관리 — 리포지토리 복사본과 독립적인 `.planning/` 디렉토리를 갖는 격리된 워크스페이스 환경을 생성, 나열, 또는 삭제합니다. diff --git a/docs/ko-KR/FEATURES.md b/docs/ko-KR/FEATURES.md index be2f5d268..a6663acd3 100644 --- a/docs/ko-KR/FEATURES.md +++ b/docs/ko-KR/FEATURES.md @@ -38,6 +38,7 @@ - [모델 프로파일](#26-model-profiles) - [브라운필드 기능](#brownfield-features) - [코드베이스 매핑](#27-codebase-mapping) + - [기존 코드베이스 온보딩](#27b-existing-codebase-onboarding) - [유틸리티 기능](#utility-features) - [디버그 시스템](#28-debug-system) - [할 일 관리](#29-todo-management) @@ -716,7 +717,7 @@ **명령어:** `/gsd-map-codebase [area]` -**목적:** 새 프로젝트를 시작하기 전에 기존 코드베이스를 분석하여 GSD가 무엇이 존재하는지 이해하도록 합니다. +**목적:** 새 프로젝트 시작 전 또는 `/gsd-onboard`의 매핑 handoff로 기존 코드베이스를 분석하여 GSD가 무엇이 존재하는지 이해하도록 합니다. **요구사항.** - REQ-MAP-01: 각 분석 영역에 대한 병렬 매퍼 에이전트를 생성해야 합니다. @@ -736,6 +737,27 @@ | `TESTING.md` | 테스트 인프라, 커버리지, 패턴 | | `INTEGRATIONS.md` | 외부 서비스, API, 서드파티 의존성 | +### 27b. Existing Codebase Onboarding + +**명령어:** `/gsd-onboard [--fast] [--text]` + +**목적:** 기존 저장소의 최초 설정을 안내하고 brownfield 상태를 확인해 코드베이스 매핑, docs 수집, 프로젝트 초기화로 안전하게 handoff합니다. + +**요구사항.** +- REQ-ONBOARD-01: 기존 코드, package manifest, planning 문서, 부분 `.planning/` 상태, 코드베이스 맵 누락을 감지해야 합니다. +- REQ-ONBOARD-02: 필요한 `.planning/codebase/` 맵 파일이 없는 brownfield에서는 `/gsd-map-codebase` 또는 `/gsd-map-codebase --fast`로 handoff해야 합니다. fast 맵 readiness는 부분 상태이며 `/gsd-new-project`에 충분한 것으로 취급해서는 안 됩니다. +- REQ-ONBOARD-03: ADR/PRD/SPEC/RFC 후보가 있고 project가 없으면 `/gsd-new-project` 전에 `/gsd-ingest-docs`를 제안해야 합니다. +- REQ-ONBOARD-04: `PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, `STATE.md`가 모두 있을 때까지 완료로 보고하지 않아야 합니다. +- REQ-ONBOARD-05: project setup 후에만 `.planning/onboarding/SUMMARY.md`를 만들거나 확인해야 합니다. +- REQ-ONBOARD-06: 대화형 메뉴가 없는 runtime을 위해 `--text` 번호형 plain-text gate를 지원해야 합니다. + +**생성 산출물.** +| 산출물 | 설명 | +|----------|-------------| +| `.planning/codebase/` | `/gsd-map-codebase` handoff가 생성한 코드베이스 맵 | +| `.planning/PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, `STATE.md` | `/gsd-new-project` 또는 `/gsd-ingest-docs`가 생성한 planning setup | +| `.planning/onboarding/SUMMARY.md` | Onboarding status, artifact index, next-command summary | + --- ## 유틸리티 기능 diff --git a/docs/ko-KR/INVENTORY.md b/docs/ko-KR/INVENTORY.md index a73860461..83b0faf53 100644 --- a/docs/ko-KR/INVENTORY.md +++ b/docs/ko-KR/INVENTORY.md @@ -78,6 +78,7 @@ | 명령어 | 역할 | 소스 | |---------|------|--------| | `/gsd-new-project` | 심층 컨텍스트 수집 및 PROJECT.md로 새 프로젝트 초기화. | [commands/gsd/new-project.md](../../commands/gsd/new-project.md) | +| `/gsd-onboard` | 기존 코드베이스를 매핑, 문서 수집, 프로젝트 설정, onboarding summary로 안내합니다. | [commands/gsd/onboard.md](../../commands/gsd/onboard.md) | | `/gsd-workspace` | GSD 워크스페이스 관리 — 격리된 워크스페이스 환경을 생성(`--new`), 목록(`--list`), 또는 제거(`--remove`). | [commands/gsd/workspace.md](../../commands/gsd/workspace.md) | | `/gsd-discuss-phase` | 계획 전 적응형 질문을 통한 단계 컨텍스트 수집. | [commands/gsd/discuss-phase.md](../../commands/gsd/discuss-phase.md) | | `/gsd-mvp-phase` | 수직 MVP 슬라이스로 단계 계획 — 사용자 스토리, SPIDR 분할, 이후 plan-phase. | [commands/gsd/mvp-phase.md](../../commands/gsd/mvp-phase.md) | @@ -218,6 +219,7 @@ | `milestone-summary.md` | 마일스톤 아티팩트에서 온보딩 및 검토용 마일스톤 요약 합성. | `/gsd-milestone-summary` | | `new-milestone.md` | 새 마일스톤 사이클 시작 — 프로젝트 컨텍스트 로드, 목표 수집, PROJECT.md/STATE.md 업데이트. | `/gsd-new-milestone` | | `new-project.md` | 통합 새 프로젝트 플로우 — 질문, 조사(선택), 요구사항, 로드맵. | `/gsd-new-project` | +| `onboard.md` | Brownfield 온보딩 오케스트레이션 — 코드베이스 매핑, 문서 수집, planning 초기화, 다음 단계 요약. | `/gsd-onboard` | | `new-workspace.md` | 저장소 워크트리/클론과 독립적인 `.planning/`이 포함된 격리된 워크스페이스 생성. | `/gsd-workspace --new` | | `next.md` | 현재 프로젝트 상태를 감지하고 다음 논리적 단계로 자동 진행. | `/gsd-progress --next` | | `node-repair.md` | 실패한 태스크 검증을 위한 자율 수리 오퍼레이터; `execute-plan`에 의해 호출. | `execute-plan.md` (복구) | diff --git a/docs/ko-KR/USER-GUIDE.md b/docs/ko-KR/USER-GUIDE.md index a2f8c01aa..95cb95d9b 100644 --- a/docs/ko-KR/USER-GUIDE.md +++ b/docs/ko-KR/USER-GUIDE.md @@ -474,8 +474,8 @@ claude --dangerously-skip-permissions ### 기존 코드베이스 ```bash -/gsd-map-codebase # Analyse what exists (parallel agents) -/gsd-new-project # Questions focus on what you're ADDING +/gsd-onboard # Safely map, ingest docs, and initialize planning +# Follow printed handoff commands, then rerun /gsd-onboard # (normal phase workflow from here) ``` @@ -864,7 +864,8 @@ All subagent/executor commits MUST use `--no-verify`. themes/ default.css # Shared CSS variables for all sketches MANIFEST.md # Index of all sketches with winners - codebase/ # Brownfield codebase mapping (from /gsd-map-codebase) + codebase/ # Brownfield codebase mapping (from /gsd-map-codebase or /gsd-onboard) + onboarding/ # Brownfield onboarding summary (from /gsd-onboard) phases/ XX-phase-name/ XX-YY-PLAN.md # Atomic execution plans diff --git a/docs/ko-KR/how-to/install-on-your-runtime.md b/docs/ko-KR/how-to/install-on-your-runtime.md index 1a296068f..435e48c81 100644 --- a/docs/ko-KR/how-to/install-on-your-runtime.md +++ b/docs/ko-KR/how-to/install-on-your-runtime.md @@ -273,10 +273,11 @@ npx @opengsd/gsd-core@latest --opencode --global ## 설치 후 -새 명령과 에이전트를 적용하려면 런타임을 재시작하세요. 그런 다음 첫 번째 프로젝트를 시작합니다: +새 명령과 에이전트를 적용하려면 런타임을 재시작하세요. 그런 다음 새 프로젝트를 시작하거나 기존 저장소를 온보딩합니다: ```bash -/gsd-new-project +/gsd-new-project # 그린필드 프로젝트 +/gsd-onboard # 기존 코드베이스 ``` 재시작 후 명령을 찾을 수 없다면 설치 디렉터리가 런타임이 기대하는 설정 경로와 일치하는지 확인하세요. 위의 프리릴리스 에디션 섹션에서 가장 흔한 불일치 사례를 다룹니다. diff --git a/docs/ko-KR/reference/planning-artifacts.md b/docs/ko-KR/reference/planning-artifacts.md index 0447a9392..04c163b7a 100644 --- a/docs/ko-KR/reference/planning-artifacts.md +++ b/docs/ko-KR/reference/planning-artifacts.md @@ -23,6 +23,8 @@ │ ├── architecture.md │ ├── stack.md │ └── ... +├── onboarding/ # 브라운필드 온보딩 요약 (선택) +│ └── SUMMARY.md ├── intel/ # 쿼리 가능한 심볼 인덱스 (선택, intel.enabled) │ └── API-SURFACE.md └── phases/ @@ -48,7 +50,7 @@ | | | |---|---| | **목적** | 표준 프로젝트 아이덴티티: 무엇인지, 누구를 위한 것인지, 핵심 가치, 요구사항, 제약 사항, 주요 결정. 제품이 발전함에 따라 프로젝트 생명주기 전반에 걸쳐 업데이트됩니다. | -| **생성자** | `/gsd-new-project` (최초 생성); 결정이 검증됨에 따라 `/gsd-complete-milestone`에 의해 업데이트됩니다. | +| **생성자** | `/gsd-new-project` (최초 생성, `/gsd-onboard` handoff 포함); 결정이 검증됨에 따라 `/gsd-complete-milestone`에 의해 업데이트됩니다. | | **소비자** | 모든 플래닝 워크플로; `gsd-phase-researcher`, `gsd-planner` (컨텍스트); `discuss-phase` (이전 결정); `gsd-plan-checker` (프로젝트 제약 사항). | ### `ROADMAP.md` @@ -56,7 +58,7 @@ | | | |---|---| | **목적** | 목표, 요구사항 ID, 성공 기준, 페이즈별 표준 참조가 있는 마일스톤 및 페이즈 목록. 프로젝트가 무엇을 빌드하고 어떤 순서로 하는지에 대한 단일 진실의 원천. | -| **생성자** | `/gsd-new-project` (최초 생성); `/gsd-phase --insert`와 `/gsd-complete-milestone`에 의해 업데이트됩니다. | +| **생성자** | `/gsd-new-project` (최초 생성, `/gsd-onboard` handoff 포함); `/gsd-phase --insert`와 `/gsd-complete-milestone`에 의해 업데이트됩니다. | | **소비자** | `/gsd-discuss-phase`, `/gsd-plan-phase`, `/gsd-execute-phase`; 페이즈 정보가 필요한 모든 오케스트레이션 명령; `gsd-planner`, `gsd-plan-checker`, `gsd-phase-researcher`. | ### `REQUIREMENTS.md` @@ -64,7 +66,7 @@ | | | |---|---| | **목적** | 프로젝트의 번호가 매겨진 체크 가능한 인수 기준. 각 요구사항은 로드맵 페이즈에 매핑되는 ID(예: `AUTH-01`)를 가집니다. 페이즈가 실행됨에 따라 요구사항을 완료로 표시합니다. | -| **생성자** | `/gsd-new-project` (최초 생성); `execute-phase`에 의해 요구사항이 완료로 표시됩니다. | +| **생성자** | `/gsd-new-project` (최초 생성, `/gsd-onboard` handoff 포함); `execute-phase`에 의해 요구사항이 완료로 표시됩니다. | | **소비자** | `gsd-planner` (플랜은 모든 페이즈 요구사항 ID를 처리해야 함); `gsd-plan-checker` Dimension 1 (요구사항 커버리지); `discuss-phase` (이전 요구사항). | ### `STATE.md` @@ -72,7 +74,7 @@ | | | |---|---| | **목적** | 살아있는 위치 추적기 — 현재 페이즈와 플랜, 진행 지표, 누적된 결정, 세션 연속성 노트. 모든 워크플로 실행 시작 시 읽힙니다. 중요한 작업 이후 업데이트됩니다. | -| **생성자** | `/gsd-new-project` (최초 생성); 모든 페이즈 워크플로, `/gsd-pause-work`, `/gsd-resume-work`에 의해 지속적으로 업데이트됩니다. | +| **생성자** | `/gsd-new-project` (최초 생성, `/gsd-onboard` handoff 포함); 모든 페이즈 워크플로, `/gsd-pause-work`, `/gsd-resume-work`에 의해 지속적으로 업데이트됩니다. | | **소비자** | 모든 오케스트레이션 워크플로; `/gsd-progress`; `/gsd-quick`을 통한 임시 태스크 실행; `gsd-planner` 및 `gsd-phase-researcher` (프로젝트 결정). | 전체 필드 참조는 [STATE.md 스키마](state-md.md)를 참조하세요. @@ -87,6 +89,14 @@ 전체 스키마는 [CONFIGURATION](../../CONFIGURATION.md)을 참조하세요. +### `onboarding/SUMMARY.md` (선택) + +| | | +|---|---| +| **목적** | 브라운필드 온보딩 인덱스. 아티팩트 상태, 코드베이스 매핑 완료 여부, 초기 설정 후 권장되는 다음 GSD 명령을 기록합니다. | +| **생성자** | `PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, `STATE.md`가 모두 존재한 뒤 `/gsd-onboard`. | +| **소비자** | 초기 설정을 검토하는 사람; 기존 온보딩 상태를 확인하는 향후 `/gsd-onboard` 실행. | + ### `MILESTONES.md` (선택) | | | diff --git a/docs/ko-KR/tutorials/onboarding-an-existing-codebase.md b/docs/ko-KR/tutorials/onboarding-an-existing-codebase.md index 794d62534..6fe4b7c51 100644 --- a/docs/ko-KR/tutorials/onboarding-an-existing-codebase.md +++ b/docs/ko-KR/tutorials/onboarding-an-existing-codebase.md @@ -44,15 +44,15 @@ claude --dangerously-skip-permissions --- -## Step 3 — 코드베이스 매핑 +## Step 3 — 브라운필드 온보딩 시작 -프로젝트를 생성하기 전에 GSD Core가 이미 존재하는 것을 학습하도록 합니다. 이 단계가 브라운필드 계획의 정확도를 높이는 핵심입니다. +프로젝트를 생성하기 전에 GSD Core가 저장소 상태를 검사하고 안전한 다음 top-level 명령을 알려주게 하세요. 이 단계는 코드베이스 컨텍스트를 건너뛰거나 기존 planning 파일을 덮어쓰는 일을 방지합니다. ```text -/gsd-map-codebase +/gsd-onboard ``` -GSD Core가 4개의 병렬 매퍼 서브 에이전트를 생성합니다("Spawning 4 parallel codebase mapper agents…" 메시지가 표시되며, 1–5분 소요됩니다. 중단하지 마세요). 각 에이전트는 서로 다른 관심사에 집중합니다: +온보딩이 코드베이스 맵 누락을 보고하면 권장 옵션을 선택하고 출력된 `/gsd-map-codebase` handoff를 실행한 뒤 `/gsd-onboard`를 다시 실행합니다. `/gsd-onboard --fast`는 가벼운 첫 패스에는 충분하지만, `/gsd-new-project` 전에는 완전한 맵이 필요합니다. `/gsd-map-codebase`가 4개의 병렬 매퍼 서브 에이전트를 생성합니다("Spawning 4 parallel codebase mapper agents…" 메시지가 표시되며, 1–5분 소요됩니다. 중단하지 마세요). 각 에이전트는 서로 다른 관심사에 집중합니다: | 에이전트 | 집중 영역 | |---------|---------| @@ -84,7 +84,7 @@ Created .planning/codebase/: --- -## Step 4 — 컨텍스트 초기화 후 프로젝트 생성 +## Step 4 — 온보딩 재실행 및 프로젝트 초기화 세션 창을 초기화합니다: @@ -92,7 +92,7 @@ Created .planning/codebase/: /clear ``` -이제 프로젝트를 생성합니다. GSD Core가 이전 단계에서 기존 코드를 발견했으므로, 이미 이것이 브라운필드 프로젝트임을 알고 있습니다. `/gsd-new-project`를 실행하면 기존의 것을 재구성하는 것이 아니라 *추가하는* 것에 집중한 질문을 합니다: +이제 `/gsd-onboard`를 다시 실행합니다. GSD Core가 ADR, PRD, spec, RFC 또는 루트 요구사항 문서를 감지하면 먼저 권장되는 `/gsd-ingest-docs` handoff를 실행하고 이후 `/gsd-onboard`를 다시 실행하세요. 컨텍스트가 준비되면 온보딩이 프로젝트 초기화 handoff를 출력합니다: ```text /gsd-new-project @@ -124,6 +124,8 @@ Proposed Roadmap 로드맵을 승인합니다. +프로젝트 설정이 끝난 뒤 `/gsd-onboard`를 한 번 더 실행합니다. 이제 `PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, `STATE.md`가 모두 있으므로 온보딩은 `.planning/onboarding/SUMMARY.md`를 생성하거나 확인합니다. + **`.planning/`에 생성되는 파일:** ```text @@ -133,6 +135,7 @@ Proposed Roadmap ROADMAP.md ← Phase 1, 상태: pending STATE.md ← 세션 메모리 config.json ← 워크플로 설정 + onboarding/SUMMARY.md ← 온보딩 상태와 다음 명령 codebase/ ← Step 3에서 생성된 7개의 맵 파일 ``` @@ -212,6 +215,7 @@ GSD Core가 `CONVENTIONS.md`와 `ARCHITECTURE.md`를 읽었으므로, 질문들 ## 배운 내용 +- `/gsd-onboard`가 인터랙티브 명령을 중첩하거나 기존 planning 파일을 덮어쓰지 않고 브라운필드 설정을 안전하게 순서화하는 방법. - `/gsd-map-codebase`가 4개의 병렬 에이전트를 실행하여 `.planning/codebase/`에 `STACK.md`, `ARCHITECTURE.md`, `CONVENTIONS.md`, `CONCERNS.md`, `STRUCTURE.md`, `TESTING.md`, `INTEGRATIONS.md`를 생성하는 방법. - 브라운필드 저장소에서 `/gsd-new-project`가 *추가하는* 것에 집중한 질문을 하고 기존 코드에서 Validated 요구사항을 채우는 방법. - 코드베이스 맵이 `/gsd-discuss-phase`의 모든 질문을 형성하는 방법 — 파일 경로, 패턴, 컨벤션이 실제 코드에서 옵니다. diff --git a/docs/pt-BR/ARCHITECTURE.md b/docs/pt-BR/ARCHITECTURE.md index 4b2f1cf3f..14ff90570 100644 --- a/docs/pt-BR/ARCHITECTURE.md +++ b/docs/pt-BR/ARCHITECTURE.md @@ -527,7 +527,8 @@ Caminhos equivalentes para outros runtimes: │ ├── FEATURES.md │ ├── ARCHITECTURE.md │ └── PITFALLS.md -├── codebase/ # Mapeamento de brownfield (do /gsd-map-codebase) +├── codebase/ # Mapeamento de brownfield (do /gsd-map-codebase ou /gsd-onboard) +├── onboarding/ # Resumo de onboarding brownfield (do /gsd-onboard) │ ├── STACK.md # Frontmatter YAML carrega `last_mapped_commit` │ ├── ARCHITECTURE.md # para o portão de desvio pós-execução (#2003) │ ├── CONVENTIONS.md diff --git a/docs/pt-BR/CLI-TOOLS.md b/docs/pt-BR/CLI-TOOLS.md index 24441e6bd..107c3259a 100644 --- a/docs/pt-BR/CLI-TOOLS.md +++ b/docs/pt-BR/CLI-TOOLS.md @@ -290,13 +290,14 @@ node gsd-tools.cjs scaffold phase-dir --phase N --name "phase name" ## Comandos Init (Carregamento de Contexto Composto) -Carrega todo o contexto necessário para um fluxo de trabalho específico em uma única chamada. Retorna JSON com informações do projeto, configuração, estado e dados específicos do fluxo de trabalho. +Carrega todo o contexto necessário para um fluxo de trabalho específico em uma única chamada. Retorna JSON com informações do projeto, configuração, estado e dados específicos do fluxo de trabalho. `init onboard [--fast] [--text]` retorna, para `/gsd-onboard`, sinais de brownfield, candidatos a docs de planning, completude do mapa de código, prontidão do mapa fast, roteamento em modo texto, estado parcial de planning e status do resumo de onboarding. ```bash node gsd-tools.cjs init execute-phase node gsd-tools.cjs init plan-phase node gsd-tools.cjs init new-project node gsd-tools.cjs init new-milestone +node gsd-tools.cjs init onboard [--fast] [--text] node gsd-tools.cjs init quick node gsd-tools.cjs init resume node gsd-tools.cjs init verify-work diff --git a/docs/pt-BR/COMMANDS.md b/docs/pt-BR/COMMANDS.md index e8b74bb13..dc7e978e1 100644 --- a/docs/pt-BR/COMMANDS.md +++ b/docs/pt-BR/COMMANDS.md @@ -51,6 +51,25 @@ Inicializa um novo projeto com coleta aprofundada de contexto. --- +### `/gsd-onboard` + +Guia o onboarding inicial de um código existente no GSD. O comando verifica o estado do repositório, encaminha com segurança por mapeamento da base de código, ingestão opcional de documentos, inicialização do projeto e cria um onboarding summary quando o planejamento existe. + +| Flag | Descrição | +|------|-----------| +| `--fast` | Prefere o handoff leve `/gsd-map-codebase --fast`; um mapa completo ainda é necessário antes de `/gsd-new-project` | +| `--text` | Usa gates numerados em texto puro em vez de menus TUI | + +**Pré-requisitos:** Repositório existente ou documentos de planejamento. Para projetos greenfield vazios, use `/gsd-new-project`. +**Produz:** `.planning/codebase/` via map-codebase, `.planning/` via new-project ou ingest-docs, e `.planning/onboarding/SUMMARY.md` após a configuração do projeto. + +```bash +/gsd-onboard # Onboarding brownfield guiado +/gsd-onboard --fast # Usa primeiro o mapa leve e depois completa o mapa antes do setup do projeto +``` + +--- + ### `/gsd-workspace` Gerencia workspaces do GSD — cria, lista ou remove ambientes de workspace isolados com cópias de repositório e diretórios `.planning/` independentes. diff --git a/docs/pt-BR/FEATURES.md b/docs/pt-BR/FEATURES.md index 186d504a8..ade045a95 100644 --- a/docs/pt-BR/FEATURES.md +++ b/docs/pt-BR/FEATURES.md @@ -14,7 +14,7 @@ Para catálogo completo e detalhamento exaustivo, consulte [FEATURES.md em ingl - **Commits atômicos por tarefa** para rastreabilidade e rollback - **Verificação pós-execução** com foco em objetivos da fase - **UAT guiado** via `/gsd-verify-work` -- **Suporte brownfield** com `/gsd-map-codebase` +- **Suporte brownfield** com `/gsd-onboard` e `/gsd-map-codebase` - **Workstreams** para trilhas paralelas sem colisão de estado - **Backlog, seeds e threads** para memória de médio/longo prazo @@ -68,7 +68,7 @@ Para catálogo completo e detalhamento exaustivo, consulte [FEATURES.md em ingl |--------|----------| | Projeto novo | `/gsd-new-project` -> `/gsd-discuss-phase` -> `/gsd-plan-phase` -> `/gsd-execute-phase` | | Correção rápida | `/gsd-quick` | -| Código existente | `/gsd-map-codebase` -> `/gsd-new-project` | +| Código existente | `/gsd-onboard` -> handoffs para `/gsd-map-codebase`, `/gsd-ingest-docs`, `/gsd-new-project` | | Fechamento de release | `/gsd-audit-milestone` -> `/gsd-complete-milestone` | --- diff --git a/docs/pt-BR/INVENTORY.md b/docs/pt-BR/INVENTORY.md index c10cbe76e..7be789824 100644 --- a/docs/pt-BR/INVENTORY.md +++ b/docs/pt-BR/INVENTORY.md @@ -78,6 +78,7 @@ Esses seis roteadores são entradas apenas descritivas que o modelo seleciona pr | Comando | Função | Fonte | |---------|--------|-------| | `/gsd-new-project` | Inicializa um novo projeto com coleta profunda de contexto e PROJECT.md. | [commands/gsd/new-project.md](../../commands/gsd/new-project.md) | +| `/gsd-onboard` | Guia código existente por mapeamento, ingestão de docs, configuração de projeto e onboarding summary. | [commands/gsd/onboard.md](../../commands/gsd/onboard.md) | | `/gsd-workspace` | Gerencia workspaces GSD — criar (`--new`), listar (`--list`) ou remover (`--remove`) ambientes de workspace isolados. | [commands/gsd/workspace.md](../../commands/gsd/workspace.md) | | `/gsd-discuss-phase` | Coleta contexto da fase por meio de perguntas adaptativas antes do planejamento. | [commands/gsd/discuss-phase.md](../../commands/gsd/discuss-phase.md) | | `/gsd-mvp-phase` | Planeja uma fase como uma fatia vertical de MVP — história de usuário, divisão SPIDR, depois plan-phase. | [commands/gsd/mvp-phase.md](../../commands/gsd/mvp-phase.md) | @@ -218,6 +219,7 @@ Registro completo em `get-shit-done/workflows/*.md`. Workflows são orquestrador | `milestone-summary.md` | Síntese do resumo do milestone — artefato de onboarding e revisão a partir dos artefatos do milestone. | `/gsd-milestone-summary` | | `new-milestone.md` | Inicia um novo ciclo de milestone — carregar contexto do projeto, coletar objetivos, atualizar PROJECT.md/STATE.md. | `/gsd-new-milestone` | | `new-project.md` | Fluxo unificado de novo projeto — questionamento, pesquisa (opcional), requisitos, roadmap. | `/gsd-new-project` | +| `onboard.md` | Orquestração de onboarding brownfield — mapear código, ingerir docs, inicializar planning e resumir próximo passo. | `/gsd-onboard` | | `new-workspace.md` | Cria um workspace isolado com worktrees/clones do repositório e um `.planning/` independente. | `/gsd-workspace --new` | | `next.md` | Detecta o estado atual do projeto e avança automaticamente para o próximo passo lógico. | `/gsd-progress --next` | | `node-repair.md` | Operador de reparo autônomo para verificação de tarefa com falha; invocado por `execute-plan`. | `execute-plan.md` (recuperação) | diff --git a/docs/pt-BR/USER-GUIDE.md b/docs/pt-BR/USER-GUIDE.md index e0b269607..c5f0a393b 100644 --- a/docs/pt-BR/USER-GUIDE.md +++ b/docs/pt-BR/USER-GUIDE.md @@ -474,8 +474,8 @@ claude --dangerously-skip-permissions ### Base de código existente ```bash -/gsd-map-codebase # Analyse what exists (parallel agents) -/gsd-new-project # Questions focus on what you're ADDING +/gsd-onboard # Safely map, ingest docs, and initialize planning +# Follow printed handoff commands, then rerun /gsd-onboard # (normal phase workflow from here) ``` @@ -864,7 +864,8 @@ Para desativar a execução paralela completamente: `/gsd-settings` → defina ` themes/ default.css # Shared CSS variables for all sketches MANIFEST.md # Index of all sketches with winners - codebase/ # Brownfield codebase mapping (from /gsd-map-codebase) + codebase/ # Brownfield codebase mapping (from /gsd-map-codebase or /gsd-onboard) + onboarding/ # Brownfield onboarding summary (from /gsd-onboard) phases/ XX-phase-name/ XX-YY-PLAN.md # Atomic execution plans diff --git a/docs/pt-BR/how-to/install-on-your-runtime.md b/docs/pt-BR/how-to/install-on-your-runtime.md index ba0d31ea1..3b70bc4e8 100644 --- a/docs/pt-BR/how-to/install-on-your-runtime.md +++ b/docs/pt-BR/how-to/install-on-your-runtime.md @@ -273,10 +273,11 @@ npx @opengsd/gsd-core@latest --opencode --global ## Após a instalação -Reinicie seu ambiente para carregar os novos comandos e agentes. Em seguida, inicie seu primeiro projeto: +Reinicie seu ambiente para carregar os novos comandos e agentes. Em seguida, inicie um projeto novo ou faça onboarding de um repositório existente: ```bash -/gsd-new-project +/gsd-new-project # projeto greenfield +/gsd-onboard # base de código existente ``` Se o comando não for encontrado após o reinício, verifique se o diretório de instalação corresponde ao caminho de configuração esperado pelo ambiente. A seção de edições de pré-lançamento acima cobre a incompatibilidade mais comum. diff --git a/docs/pt-BR/reference/planning-artifacts.md b/docs/pt-BR/reference/planning-artifacts.md index 7d6859acc..5ddb9ecfa 100644 --- a/docs/pt-BR/reference/planning-artifacts.md +++ b/docs/pt-BR/reference/planning-artifacts.md @@ -23,6 +23,8 @@ O diretório `.planning/` é a memória compartilhada do GSD Core para um projet │ ├── architecture.md │ ├── stack.md │ └── ... +├── onboarding/ # Resumo de onboarding brownfield (opcional) +│ └── SUMMARY.md ├── intel/ # Índice de símbolos consultável (opcional, intel.enabled) │ └── API-SURFACE.md └── phases/ @@ -48,7 +50,7 @@ O diretório `.planning/` é a memória compartilhada do GSD Core para um projet | | | |---|---| | **Finalidade** | Identidade canônica do projeto: o que é, para quem é, valor central, requisitos, restrições e decisões-chave. Atualizado ao longo do ciclo de vida do projeto conforme o produto evolui. | -| **Produzido por** | `/gsd-new-project` (criação inicial); atualizado por `/gsd-complete-milestone` à medida que as decisões são validadas. | +| **Produzido por** | `/gsd-new-project` (criação inicial, incluindo handoff do `/gsd-onboard`); atualizado por `/gsd-complete-milestone` à medida que as decisões são validadas. | | **Consumido por** | Todos os fluxos de trabalho de planejamento; `gsd-phase-researcher`, `gsd-planner` (contexto); `discuss-phase` (decisões anteriores); `gsd-plan-checker` (restrições do projeto). | ### `ROADMAP.md` @@ -56,7 +58,7 @@ O diretório `.planning/` é a memória compartilhada do GSD Core para um projet | | | |---|---| | **Finalidade** | Listagem de marcos e fases com objetivos, IDs de requisitos, critérios de sucesso e referências canônicas por fase. A fonte única de verdade sobre o que o projeto está construindo e em que ordem. | -| **Produzido por** | `/gsd-new-project` (criação inicial); atualizado por `/gsd-phase --insert` e `/gsd-complete-milestone`. | +| **Produzido por** | `/gsd-new-project` (criação inicial, incluindo handoff do `/gsd-onboard`); atualizado por `/gsd-phase --insert` e `/gsd-complete-milestone`. | | **Consumido por** | `/gsd-discuss-phase`, `/gsd-plan-phase`, `/gsd-execute-phase`; todos os comandos de orquestração que precisam de informações de fase; `gsd-planner`, `gsd-plan-checker`, `gsd-phase-researcher`. | ### `REQUIREMENTS.md` @@ -64,7 +66,7 @@ O diretório `.planning/` é a memória compartilhada do GSD Core para um projet | | | |---|---| | **Finalidade** | Critérios de aceitação numerados e verificáveis para o projeto. Cada requisito possui um ID (ex.: `AUTH-01`) que mapeia para as fases do roadmap. Marca os requisitos como concluídos conforme as fases são executadas. | -| **Produzido por** | `/gsd-new-project` (criação inicial); requisitos marcados como concluídos por `execute-phase`. | +| **Produzido por** | `/gsd-new-project` (criação inicial, incluindo handoff do `/gsd-onboard`); requisitos marcados como concluídos por `execute-phase`. | | **Consumido por** | `gsd-planner` (os planos devem contemplar todos os IDs de requisitos da fase); `gsd-plan-checker` Dimensão 1 (cobertura de requisitos); `discuss-phase` (requisitos anteriores). | ### `STATE.md` @@ -72,7 +74,7 @@ O diretório `.planning/` é a memória compartilhada do GSD Core para um projet | | | |---|---| | **Finalidade** | Rastreador de posição em andamento — fase e plano atuais, métricas de progresso, decisões acumuladas, notas de continuidade de sessão. Lido no início de toda execução de fluxo de trabalho. Atualizado após cada ação significativa. | -| **Produzido por** | `/gsd-new-project` (criação inicial); atualizado continuamente por todos os fluxos de fase, `/gsd-pause-work`, `/gsd-resume-work`. | +| **Produzido por** | `/gsd-new-project` (criação inicial, incluindo handoff do `/gsd-onboard`); atualizado continuamente por todos os fluxos de fase, `/gsd-pause-work`, `/gsd-resume-work`. | | **Consumido por** | Todos os fluxos de orquestração; `/gsd-progress`; execução de tarefas avulsas via `/gsd-quick`; `gsd-planner` e `gsd-phase-researcher` (decisões do projeto). | Consulte o [esquema de STATE.md](state-md.md) para a referência completa de campos. @@ -87,6 +89,14 @@ Consulte o [esquema de STATE.md](state-md.md) para a referência completa de cam Consulte [CONFIGURATION](../CONFIGURATION.md) para o esquema completo. +### `onboarding/SUMMARY.md` (opcional) + +| | | +|---|---| +| **Finalidade** | Índice de onboarding brownfield que registra status dos artefatos, se o mapeamento do código-base está completo e o próximo comando GSD recomendado após a configuração inicial. | +| **Produzido por** | `/gsd-onboard` depois que `PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md` e `STATE.md` existem. | +| **Consumido por** | Humanos revisando a configuração inicial; futuras execuções de `/gsd-onboard` ao confirmar o estado de onboarding existente. | + ### `MILESTONES.md` (opcional) | | | diff --git a/docs/pt-BR/tutorials/onboarding-an-existing-codebase.md b/docs/pt-BR/tutorials/onboarding-an-existing-codebase.md index 760cf362f..4e8a99c76 100644 --- a/docs/pt-BR/tutorials/onboarding-an-existing-codebase.md +++ b/docs/pt-BR/tutorials/onboarding-an-existing-codebase.md @@ -44,15 +44,15 @@ claude --dangerously-skip-permissions --- -## Passo 3 — Mapear a base de código +## Passo 3 — Iniciar o onboarding brownfield -Antes de criar um projeto, deixe o GSD Core aprender o que já existe. Este é o passo que torna o planejamento brownfield preciso. +Antes de criar um projeto, deixe o GSD Core inspecionar o estado do repositório e indicar o próximo comando de nível superior seguro. Este passo evita pular contexto de código ou sobrescrever arquivos de planning existentes. ```text -/gsd-map-codebase +/gsd-onboard ``` -O GSD Core cria quatro sub-agentes mapeadores paralelos (você verá "Spawning 4 parallel codebase mapper agents…" — isso leva de 1 a 5 minutos; não interrompa). Cada agente foca em uma preocupação diferente: +Se o onboarding informar que o mapa da base de código está ausente, escolha a opção recomendada e execute o handoff `/gsd-map-codebase` impresso antes de rodar `/gsd-onboard` novamente. `/gsd-onboard --fast` serve para uma primeira passada leve, mas um mapa completo ainda é necessário antes de `/gsd-new-project`. O `/gsd-map-codebase` cria quatro sub-agentes mapeadores paralelos (você verá "Spawning 4 parallel codebase mapper agents…" — isso leva de 1 a 5 minutos; não interrompa). Cada agente foca em uma preocupação diferente: | Agente | Foco | |--------|------| @@ -84,7 +84,7 @@ Abra `.planning/codebase/CONCERNS.md`. Este é o arquivo mais útil para ler ant --- -## Passo 4 — Limpar o contexto e criar o projeto +## Passo 4 — Reexecutar o onboarding e inicializar o projeto Limpe a janela de sessão: @@ -92,12 +92,14 @@ Limpe a janela de sessão: /clear ``` -Agora crie o projeto. Como o GSD Core encontrou código existente no passo anterior, já sabe que se trata de um projeto brownfield. Quando você executa `/gsd-new-project`, as perguntas focam no que você está *adicionando*, e não em reconstruir o que já existe: +Agora execute `/gsd-onboard` novamente. Se o GSD Core detectar ADRs, PRDs, specs, RFCs ou requisitos de nível raiz, aceite o handoff recomendado para `/gsd-ingest-docs` primeiro e depois rode `/gsd-onboard` de novo. Quando o contexto estiver pronto, o onboarding imprimirá o handoff de inicialização: ```text /gsd-new-project ``` +Como o GSD Core encontrou código existente no passo anterior, `/gsd-new-project` sabe que é um projeto brownfield. As perguntas focam no que você está *adicionando*, não em reconstruir o que já existe: + O GSD Core pergunta o que você quer construir. Responda com o recurso que está adicionando, e não com uma descrição de toda a base de código: ```text @@ -124,6 +126,8 @@ Proposed Roadmap Aprove o roteiro. +Execute `/gsd-onboard` mais uma vez depois que a configuração do projeto terminar. Agora que `PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md` e `STATE.md` existem, o onboarding cria ou confirma `.planning/onboarding/SUMMARY.md`. + **O que é criado em `.planning/`:** ```text @@ -133,6 +137,7 @@ Aprove o roteiro. ROADMAP.md ← Fase 1, status: pending STATE.md ← memória de sessão config.json ← configurações do fluxo de trabalho + onboarding/SUMMARY.md ← status do onboarding e próximo comando codebase/ ← os sete arquivos de mapa do Passo 3 ``` @@ -212,6 +217,7 @@ Para cada recurso futuro, execute `/gsd-map-codebase` novamente sempre que a est ## O que você aprendeu +- Como `/gsd-onboard` sequencia com segurança o setup brownfield sem aninhar comandos interativos ou sobrescrever planning existente. - Como `/gsd-map-codebase` executa quatro agentes paralelos para produzir `STACK.md`, `ARCHITECTURE.md`, `CONVENTIONS.md`, `CONCERNS.md`, `STRUCTURE.md`, `TESTING.md` e `INTEGRATIONS.md` em `.planning/codebase/`. - Como `/gsd-new-project` em um repositório brownfield concentra as perguntas no que você está *adicionando* e preenche os requisitos Validated a partir do código existente. - Como o mapa da base de código orienta cada pergunta em `/gsd-discuss-phase` — caminhos de arquivos, padrões e convenções vêm do seu código real. diff --git a/docs/reference/capability-matrix.md b/docs/reference/capability-matrix.md index 4efee626d..c74755eac 100644 --- a/docs/reference/capability-matrix.md +++ b/docs/reference/capability-matrix.md @@ -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 | diff --git a/docs/reference/planning-artifacts.md b/docs/reference/planning-artifacts.md index 5d7dd5e6a..d62c32005 100644 --- a/docs/reference/planning-artifacts.md +++ b/docs/reference/planning-artifacts.md @@ -23,6 +23,8 @@ The `.planning/` directory is GSD Core's shared memory for a project. Every work │ ├── architecture.md │ ├── stack.md │ └── ... +├── onboarding/ # Brownfield onboarding summary (optional) +│ └── SUMMARY.md ├── intel/ # Queryable symbol index (optional, intel.enabled) │ └── API-SURFACE.md └── phases/ @@ -48,7 +50,7 @@ The `.planning/` directory is GSD Core's shared memory for a project. Every work | | | |---|---| | **Purpose** | Canonical project identity: what it is, who it is for, core value, requirements, constraints, and key decisions. Updated throughout the project lifecycle as the product evolves. | -| **Produced by** | `/gsd-new-project` (initial creation); updated by `/gsd-complete-milestone` as decisions are validated. | +| **Produced by** | `/gsd-new-project` (initial creation, including `/gsd-onboard` handoff); updated by `/gsd-complete-milestone` as decisions are validated. | | **Consumed by** | All planning workflows; `gsd-phase-researcher`, `gsd-planner` (context); `discuss-phase` (prior decisions); `gsd-plan-checker` (project constraints). | Includes an optional `## Business Context` section (Customer, Revenue model, Success metric, Strategy notes) for monetized or customer-facing projects — four one-line fields that connect business outcomes to requirement prioritization. It is deleted for internal tools, experiments, or meta workspaces, and reviewed at each milestone by `/gsd-complete-milestone` when present. @@ -58,7 +60,7 @@ Includes an optional `## Business Context` section (Customer, Revenue model, Suc | | | |---|---| | **Purpose** | Milestone and phase listing with goals, requirement IDs, success criteria, and canonical references per phase. The single source of truth for what the project is building and in what order. | -| **Produced by** | `/gsd-new-project` (initial creation); updated by `/gsd-phase --insert` and `/gsd-complete-milestone`. | +| **Produced by** | `/gsd-new-project` (initial creation, including `/gsd-onboard` handoff); updated by `/gsd-phase --insert` and `/gsd-complete-milestone`. | | **Consumed by** | `/gsd-discuss-phase`, `/gsd-plan-phase`, `/gsd-execute-phase`; all orchestration commands that need phase information; `gsd-planner`, `gsd-plan-checker`, `gsd-phase-researcher`. | ### `REQUIREMENTS.md` @@ -66,7 +68,7 @@ Includes an optional `## Business Context` section (Customer, Revenue model, Suc | | | |---|---| | **Purpose** | Numbered, checkable acceptance criteria for the project. Each requirement carries an ID (e.g., `AUTH-01`) that maps to roadmap phases. Marks requirements complete as phases are executed. | -| **Produced by** | `/gsd-new-project` (initial creation); requirements marked complete by `execute-phase`. | +| **Produced by** | `/gsd-new-project` (initial creation, including `/gsd-onboard` handoff); requirements marked complete by `execute-phase`. | | **Consumed by** | `gsd-planner` (plans must address all phase requirement IDs); `gsd-plan-checker` Dimension 1 (requirement coverage); `discuss-phase` (prior requirements). | ### `STATE.md` @@ -74,7 +76,7 @@ Includes an optional `## Business Context` section (Customer, Revenue model, Suc | | | |---|---| | **Purpose** | Living position tracker — current phase and plan, progress metrics, accumulated decisions, session continuity notes. Read at the start of every workflow run. Updated after every significant action. | -| **Produced by** | `/gsd-new-project` (initial creation); updated continuously by all phase workflows, `/gsd-pause-work`, `/gsd-resume-work`. | +| **Produced by** | `/gsd-new-project` (initial creation, including `/gsd-onboard` handoff); updated continuously by all phase workflows, `/gsd-pause-work`, `/gsd-resume-work`. | | **Consumed by** | All orchestration workflows; `/gsd-progress`; ad-hoc task execution via `/gsd-quick`; `gsd-planner` and `gsd-phase-researcher` (project decisions). | See [STATE.md schema](state-md.md) for the full field reference. @@ -89,6 +91,14 @@ See [STATE.md schema](state-md.md) for the full field reference. See [CONFIGURATION](../CONFIGURATION.md) for the complete schema. +### `onboarding/SUMMARY.md` (optional) + +| | | +|---|---| +| **Purpose** | Brownfield onboarding index that records artifact status, whether codebase mapping is complete, and the next recommended GSD command after first-time setup. | +| **Produced by** | `/gsd-onboard` after `PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, and `STATE.md` all exist. | +| **Consumed by** | Humans reviewing first-time setup; future `/gsd-onboard` runs when confirming existing onboarding state. | + ### `MILESTONES.md` (optional) | | | diff --git a/docs/tutorials/onboarding-an-existing-codebase.md b/docs/tutorials/onboarding-an-existing-codebase.md index 22c536f4a..fd6774ee4 100644 --- a/docs/tutorials/onboarding-an-existing-codebase.md +++ b/docs/tutorials/onboarding-an-existing-codebase.md @@ -44,15 +44,21 @@ claude --dangerously-skip-permissions --- -## Step 3 — Map the codebase +## Step 3 — Start brownfield onboarding -Before creating a project, let GSD Core learn what already exists. This is the step that makes brownfield planning accurate. +Before creating a project, let GSD Core inspect the repo state and tell you the safe next top-level command. This is the step that prevents brownfield setup from skipping codebase context or overwriting existing planning files. + +```text +/gsd-onboard +``` + +If code exists and `.planning/codebase/` is missing, GSD Core asks you to map the codebase first. Choose the recommended mapping option, then run the printed handoff command: ```text /gsd-map-codebase ``` -GSD Core spawns four parallel mapper sub-agents (you'll see "Spawning 4 parallel codebase mapper agents…" — this takes 1–5 minutes; do not interrupt). Each agent focuses on a different concern: +Use `/gsd-onboard --fast` if you want the onboarding gate to prefer `/gsd-map-codebase --fast` for a lighter first pass. Fast mode is only enough for lightweight onboarding; `/gsd-onboard` still sends you back to the full mapper before `/gsd-new-project`. The full mapper spawns four parallel mapper sub-agents (you'll see "Spawning 4 parallel codebase mapper agents…" — this takes 1–5 minutes; do not interrupt). Each agent focuses on a different concern: | Agent | Focus | |-------|-------| @@ -84,7 +90,7 @@ Open `.planning/codebase/CONCERNS.md`. This is the most useful file to read befo --- -## Step 4 — Clear context and create the project +## Step 4 — Rerun onboarding and initialize the project Clear the session window: @@ -92,12 +98,20 @@ Clear the session window: /clear ``` -Now create the project. Because GSD Core found existing code in the last step, it already knows this is a brownfield project. When you run `/gsd-new-project`, the questions focus on what you are *adding*, not rebuilding what already exists: +Now rerun onboarding: + +```text +/gsd-onboard +``` + +If GSD Core detects ADRs, PRDs, specs, RFCs, or top-level requirements docs, choose the recommended docs-ingest handoff first and rerun `/gsd-onboard` afterward. Once codebase context and any existing docs are handled, onboarding prints the project-initialization handoff: ```text /gsd-new-project ``` +Because GSD Core found existing code in the previous step, `/gsd-new-project` knows this is a brownfield project. The questions focus on what you are *adding*, not rebuilding what already exists: + GSD Core asks what you want to build. Answer with the feature you are adding, not a description of the whole codebase: ```text @@ -138,6 +152,20 @@ Approve the roadmap. Notice that `.planning/codebase/` is already there from Step 3. GSD Core read those files when writing `PROJECT.md`, which is why it could populate the Validated requirements without you describing them. +Run onboarding one more time after project setup completes: + +```text +/gsd-onboard +``` + +Now that `PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, and `STATE.md` all exist, onboarding creates or confirms: + +```text +.planning/onboarding/SUMMARY.md +``` + +This summary is a lightweight index of the setup artifacts and the next command to run. + --- ## Step 5 — Clear context and discuss Phase 1 @@ -206,12 +234,13 @@ You now have a project with a codebase map, a discuss decision record, and verif /gsd-ship 1 ``` -For every future feature, run `/gsd-map-codebase` again whenever the structure changes significantly, so the codebase map stays fresh. +For every future feature, run `/gsd-map-codebase` again whenever the structure changes significantly, so the codebase map stays fresh. Rerun `/gsd-onboard` only when you want to re-check first-time setup completeness or regenerate the onboarding summary. --- ## What you've learned +- How `/gsd-onboard` safely sequences brownfield setup without nesting interactive commands or overwriting existing planning files. - How `/gsd-map-codebase` runs four parallel agents to produce `STACK.md`, `ARCHITECTURE.md`, `CONVENTIONS.md`, `CONCERNS.md`, `STRUCTURE.md`, `TESTING.md`, and `INTEGRATIONS.md` in `.planning/codebase/`. - How `/gsd-new-project` in a brownfield repo focuses questions on what you are *adding* and populates Validated requirements from existing code. - How the codebase map shapes every question in `/gsd-discuss-phase` — file paths, patterns, and conventions come from your actual code. @@ -222,5 +251,5 @@ For every future feature, run `/gsd-map-codebase` again whenever the structure c ## Related - [Your first project](your-first-project.md) — the full greenfield loop from install to PR -- [Map codebase via Commands](../COMMANDS.md) — all `/gsd-map-codebase` flags and subcommands +- [Commands](../COMMANDS.md) — `/gsd-onboard`, plus all `/gsd-map-codebase` flags and subcommands - [Documentation index](../README.md) diff --git a/docs/zh-CN/ARCHITECTURE.md b/docs/zh-CN/ARCHITECTURE.md index 5a92e72bb..6395b3733 100644 --- a/docs/zh-CN/ARCHITECTURE.md +++ b/docs/zh-CN/ARCHITECTURE.md @@ -512,7 +512,8 @@ UI-SPEC.md (per phase) ─────────────────── │ ├── FEATURES.md │ ├── ARCHITECTURE.md │ └── PITFALLS.md -├── codebase/ # Brownfield mapping (from /gsd-map-codebase) +├── codebase/ # Brownfield mapping (from /gsd-map-codebase or /gsd-onboard) +├── onboarding/ # Brownfield onboarding summary (from /gsd-onboard) │ ├── STACK.md # YAML frontmatter carries `last_mapped_commit` │ ├── ARCHITECTURE.md # for the post-execute drift gate (#2003) │ ├── CONVENTIONS.md diff --git a/docs/zh-CN/CLI-TOOLS.md b/docs/zh-CN/CLI-TOOLS.md index ff86c98e9..b141cb17e 100644 --- a/docs/zh-CN/CLI-TOOLS.md +++ b/docs/zh-CN/CLI-TOOLS.md @@ -288,13 +288,14 @@ node gsd-tools.cjs scaffold phase-dir --phase N --name "phase name" ## Init 命令(复合上下文加载) -通过单次调用加载特定工作流所需的所有上下文。返回包含项目信息、配置、状态和工作流专属数据的 JSON。 +通过单次调用加载特定工作流所需的所有上下文。返回包含项目信息、配置、状态和工作流专属数据的 JSON。`init onboard [--fast] [--text]` 会为 `/gsd-onboard` 返回 brownfield 信号、planning 文档候选、代码库映射完整性、fast 映射就绪状态、文本模式路由、部分 planning 状态和 onboarding summary 状态。 ```bash node gsd-tools.cjs init execute-phase node gsd-tools.cjs init plan-phase node gsd-tools.cjs init new-project node gsd-tools.cjs init new-milestone +node gsd-tools.cjs init onboard [--fast] [--text] node gsd-tools.cjs init quick node gsd-tools.cjs init resume node gsd-tools.cjs init verify-work diff --git a/docs/zh-CN/COMMANDS.md b/docs/zh-CN/COMMANDS.md index 10d808254..3985a0928 100644 --- a/docs/zh-CN/COMMANDS.md +++ b/docs/zh-CN/COMMANDS.md @@ -51,6 +51,25 @@ v1.40 中,六个命名空间路由器作为第一阶段入口点随附发布 --- +### `/gsd-onboard` + +引导现有代码库完成首次 GSD 接入。该命令检查仓库状态,安全地转到代码库映射、可选文档摄取、项目初始化,并在规划文件齐备后创建 onboarding summary。 + +| 标志 | 描述 | +|------|-------------| +| `--fast` | 优先使用轻量 `/gsd-map-codebase --fast` 映射交接;在 `/gsd-new-project` 前仍需要完整映射 | +| `--text` | 使用编号纯文本关卡,而不是 TUI 菜单 | + +**前提条件:** 现有仓库或规划文档。空的绿地项目请使用 `/gsd-new-project`。 +**产出:** 由 map-codebase 生成的 `.planning/codebase/`,由 new-project 或 ingest-docs 生成的 `.planning/`,以及项目设置后的 `.planning/onboarding/SUMMARY.md`。 + +```bash +/gsd-onboard # 引导式 brownfield 接入 +/gsd-onboard --fast # 先使用轻量映射,并在项目设置前补全完整映射 +``` + +--- + ### `/gsd-workspace` 管理 GSD 工作区 — 创建、列出或移除隔离的工作区环境,包含仓库副本和独立的 `.planning/` 目录。 diff --git a/docs/zh-CN/FEATURES.md b/docs/zh-CN/FEATURES.md index abfbea94e..80953d3af 100644 --- a/docs/zh-CN/FEATURES.md +++ b/docs/zh-CN/FEATURES.md @@ -38,6 +38,7 @@ - [模型配置](#26-model-profiles) - [棕地功能](#brownfield-features) - [代码库映射](#27-codebase-mapping) + - [现有代码库接入](#27b-现有代码库接入) - [实用功能](#utility-features) - [调试系统](#28-debug-system) - [待办事项管理](#29-todo-management) @@ -783,7 +784,7 @@ **命令:** `/gsd-map-codebase [area]` -**目的:** 在启动新项目之前分析现有代码库,使 GSD 了解已有内容。 +**目的:** 在启动新项目之前,或作为 `/gsd-onboard` 的映射交接,分析现有代码库,使 GSD 了解已有内容。 **需求:** - REQ-MAP-01:系统必须为每个分析领域派生并行映射智能体 @@ -805,6 +806,27 @@ **增量重映射 — `--paths` (#2003):** 映射器接受可选的 `--paths ` 范围提示。提供时,它将探索限制在列出的仓库相对前缀,而非扫描整个代码树。这是执行后代码库漂移门控用于仅刷新阶段实际修改的子树的路径。每个生成的文档在其 YAML 前置元数据中携带 `last_mapped_commit`,以便相对于映射点(而非 HEAD)来测量漂移。 +### 27b. 现有代码库接入 + +**命令:** `/gsd-onboard [--fast] [--text]` + +**目的:** 引导现有仓库完成首次设置,检查 brownfield 状态,并安全交接到代码库映射、docs 摄取和项目初始化。 + +**需求:** +- REQ-ONBOARD-01:系统必须检测现有代码、package manifest、规划文档、部分 `.planning/` 状态以及缺失的代码库映射文件。 +- REQ-ONBOARD-02:当 brownfield 缺少所需 `.planning/codebase/` 映射文件时,系统必须交接到 `/gsd-map-codebase` 或 `/gsd-map-codebase --fast`。fast 映射 readiness 是部分状态,不得视为足以运行 `/gsd-new-project`。 +- REQ-ONBOARD-03:当存在 ADR/PRD/SPEC/RFC 候选且 project 不存在时,系统必须在 `/gsd-new-project` 前提供 `/gsd-ingest-docs`。 +- REQ-ONBOARD-04:在 `PROJECT.md`、`REQUIREMENTS.md`、`ROADMAP.md`、`STATE.md` 全部存在前,系统不得报告 onboarding 完成。 +- REQ-ONBOARD-05:系统必须仅在 project setup 后创建或确认 `.planning/onboarding/SUMMARY.md`。 +- REQ-ONBOARD-06:系统必须为没有交互式菜单的运行时支持 `--text` 编号纯文本关卡。 + +**产出:** +| Artifact | 说明 | +|----------|-------------| +| `.planning/codebase/` | 由 `/gsd-map-codebase` 交接生成的代码库映射 | +| `.planning/PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, `STATE.md` | 由 `/gsd-new-project` 或 `/gsd-ingest-docs` 生成的规划设置 | +| `.planning/onboarding/SUMMARY.md` | Onboarding status、artifact index 和 next-command summary | + ### 27a. 执行后代码库漂移检测 **引入版本:** #2003 diff --git a/docs/zh-CN/INVENTORY.md b/docs/zh-CN/INVENTORY.md index d6344bfba..e1d295914 100644 --- a/docs/zh-CN/INVENTORY.md +++ b/docs/zh-CN/INVENTORY.md @@ -78,6 +78,7 @@ | 命令 | 角色 | 源文件 | |------|------|--------| | `/gsd-new-project` | 通过深度上下文收集和 PROJECT.md 初始化新项目。 | [commands/gsd/new-project.md](../../commands/gsd/new-project.md) | +| `/gsd-onboard` | 引导现有代码库完成映射、文档摄取、项目设置和 onboarding summary。 | [commands/gsd/onboard.md](../../commands/gsd/onboard.md) | | `/gsd-workspace` | 管理 GSD 工作区 — 创建(`--new`)、列出(`--list`)或移除(`--remove`)隔离的工作区环境。 | [commands/gsd/workspace.md](../../commands/gsd/workspace.md) | | `/gsd-discuss-phase` | 在规划前通过自适应提问收集阶段上下文。 | [commands/gsd/discuss-phase.md](../../commands/gsd/discuss-phase.md) | | `/gsd-mvp-phase` | 将阶段规划为垂直 MVP 切片 — 用户故事、SPIDR 拆分,然后进行阶段规划。 | [commands/gsd/mvp-phase.md](../../commands/gsd/mvp-phase.md) | @@ -218,6 +219,7 @@ | `milestone-summary.md` | 里程碑摘要综合 — 从里程碑产物生成的入职和审查产物。 | `/gsd-milestone-summary` | | `new-milestone.md` | 启动新里程碑周期 — 加载项目上下文、收集目标、更新 PROJECT.md/STATE.md。 | `/gsd-new-milestone` | | `new-project.md` | 统一的新项目流程 — 提问、研究(可选)、需求、路线图。 | `/gsd-new-project` | +| `onboard.md` | Brownfield 接入编排 — 映射代码库、摄取文档、初始化规划并总结下一步。 | `/gsd-onboard` | | `new-workspace.md` | 创建带有仓库 worktree/克隆和独立 `.planning/` 的隔离工作区。 | `/gsd-workspace --new` | | `next.md` | 检测当前项目状态并自动推进到下一个逻辑步骤。 | `/gsd-progress --next` | | `node-repair.md` | 用于失败任务验证的自主修复算子;由 `execute-plan` 调用。 | `execute-plan.md`(恢复) | diff --git a/docs/zh-CN/USER-GUIDE.md b/docs/zh-CN/USER-GUIDE.md index 3c7c0acea..6462395f6 100644 --- a/docs/zh-CN/USER-GUIDE.md +++ b/docs/zh-CN/USER-GUIDE.md @@ -473,8 +473,8 @@ claude --dangerously-skip-permissions ### 现有代码库 ```bash -/gsd-map-codebase # Analyse what exists (parallel agents) -/gsd-new-project # Questions focus on what you're ADDING +/gsd-onboard # Safely map, ingest docs, and initialize planning +# Follow printed handoff commands, then rerun /gsd-onboard # (normal phase workflow from here) ``` @@ -863,7 +863,8 @@ All subagent/executor commits MUST use `--no-verify`. themes/ default.css # Shared CSS variables for all sketches MANIFEST.md # Index of all sketches with winners - codebase/ # Brownfield codebase mapping (from /gsd-map-codebase) + codebase/ # Brownfield codebase mapping (from /gsd-map-codebase or /gsd-onboard) + onboarding/ # Brownfield onboarding summary (from /gsd-onboard) phases/ XX-phase-name/ XX-YY-PLAN.md # Atomic execution plans diff --git a/docs/zh-CN/how-to/install-on-your-runtime.md b/docs/zh-CN/how-to/install-on-your-runtime.md index 63a97cce3..7bf6f97fb 100644 --- a/docs/zh-CN/how-to/install-on-your-runtime.md +++ b/docs/zh-CN/how-to/install-on-your-runtime.md @@ -273,10 +273,11 @@ npx @opengsd/gsd-core@latest --opencode --global ## 安装后 -重启您的运行时以加载新命令和代理。然后启动您的第一个项目: +重启您的运行时以加载新命令和代理。然后启动新项目或接入现有仓库: ```bash -/gsd-new-project +/gsd-new-project # 新建项目 +/gsd-onboard # 现有代码库 ``` 如果重启后找不到该命令,请确认安装目录与运行时预期的配置路径匹配。上方的预发布版章节介绍了最常见的路径不匹配情况。 diff --git a/docs/zh-CN/reference/planning-artifacts.md b/docs/zh-CN/reference/planning-artifacts.md index 7039e8cd5..73c513916 100644 --- a/docs/zh-CN/reference/planning-artifacts.md +++ b/docs/zh-CN/reference/planning-artifacts.md @@ -23,6 +23,8 @@ │ ├── architecture.md │ ├── stack.md │ └── ... +├── onboarding/ # Brownfield onboarding 摘要(可选) +│ └── SUMMARY.md ├── intel/ # 可查询的符号索引(可选,intel.enabled) │ └── API-SURFACE.md └── phases/ @@ -48,7 +50,7 @@ | | | |---|---| | **用途** | 规范的项目标识:项目内容、目标用户、核心价值、需求、约束和关键决策。随项目演进持续更新。 | -| **生成者** | `/gsd-new-project`(初始创建);由 `/gsd-complete-milestone` 在决策验证后更新。 | +| **生成者** | `/gsd-new-project`(初始创建,包含 `/gsd-onboard` 交接);由 `/gsd-complete-milestone` 在决策验证后更新。 | | **消费者** | 所有规划工作流;`gsd-phase-researcher`、`gsd-planner`(上下文);`discuss-phase`(历史决策);`gsd-plan-checker`(项目约束)。 | ### `ROADMAP.md` @@ -56,7 +58,7 @@ | | | |---|---| | **用途** | 里程碑与阶段列表,含目标、需求 ID、成功标准以及每个阶段的规范参考。是项目构建内容和顺序的唯一可信来源。 | -| **生成者** | `/gsd-new-project`(初始创建);由 `/gsd-phase --insert` 和 `/gsd-complete-milestone` 更新。 | +| **生成者** | `/gsd-new-project`(初始创建,包含 `/gsd-onboard` 交接);由 `/gsd-phase --insert` 和 `/gsd-complete-milestone` 更新。 | | **消费者** | `/gsd-discuss-phase`、`/gsd-plan-phase`、`/gsd-execute-phase`;所有需要阶段信息的编排命令;`gsd-planner`、`gsd-plan-checker`、`gsd-phase-researcher`。 | ### `REQUIREMENTS.md` @@ -64,7 +66,7 @@ | | | |---|---| | **用途** | 编号化、可勾选的项目验收标准。每条需求带有 ID(如 `AUTH-01`),映射到路线图阶段。随着阶段执行,逐步标记需求为已完成。 | -| **生成者** | `/gsd-new-project`(初始创建);需求由 `execute-phase` 标记为已完成。 | +| **生成者** | `/gsd-new-project`(初始创建,包含 `/gsd-onboard` 交接);需求由 `execute-phase` 标记为已完成。 | | **消费者** | `gsd-planner`(计划必须覆盖所有阶段需求 ID);`gsd-plan-checker` 维度 1(需求覆盖);`discuss-phase`(历史需求)。 | ### `STATE.md` @@ -72,7 +74,7 @@ | | | |---|---| | **用途** | 实时进度跟踪器——当前阶段与计划、进度指标、积累的决策、会话连续性说明。每次工作流运行时首先读取,每次重要操作后更新。 | -| **生成者** | `/gsd-new-project`(初始创建);由所有阶段工作流、`/gsd-pause-work`、`/gsd-resume-work` 持续更新。 | +| **生成者** | `/gsd-new-project`(初始创建,包含 `/gsd-onboard` 交接);由所有阶段工作流、`/gsd-pause-work`、`/gsd-resume-work` 持续更新。 | | **消费者** | 所有编排工作流;`/gsd-progress`;通过 `/gsd-quick` 执行的临时任务;`gsd-planner` 和 `gsd-phase-researcher`(项目决策)。 | 完整字段参考请参见 [STATE.md 模式](state-md.md)。 @@ -87,6 +89,14 @@ 完整模式请参见 [CONFIGURATION](../CONFIGURATION.md)。 +### `onboarding/SUMMARY.md`(可选) + +| | | +|---|---| +| **用途** | Brownfield onboarding 索引,记录产物状态、代码库映射是否完整,以及初次设置后的下一个推荐 GSD 命令。 | +| **生成者** | `/gsd-onboard`,在 `PROJECT.md`、`REQUIREMENTS.md`、`ROADMAP.md` 和 `STATE.md` 全部存在后生成。 | +| **消费者** | 审查初次设置的人;以后确认现有 onboarding 状态的 `/gsd-onboard` 运行。 | + ### `MILESTONES.md`(可选) | | | diff --git a/docs/zh-CN/tutorials/onboarding-an-existing-codebase.md b/docs/zh-CN/tutorials/onboarding-an-existing-codebase.md index 05576183b..b90522443 100644 --- a/docs/zh-CN/tutorials/onboarding-an-existing-codebase.md +++ b/docs/zh-CN/tutorials/onboarding-an-existing-codebase.md @@ -44,15 +44,15 @@ claude --dangerously-skip-permissions --- -## 第 3 步 — 映射代码库 +## 第 3 步 — 开始 brownfield onboarding -在创建项目之前,先让 GSD Core 了解已有的内容。这是使棕地规划准确的关键步骤。 +在创建项目之前,让 GSD Core 检查仓库状态并告诉您安全的下一个顶层命令。这一步可避免跳过代码库上下文或覆盖现有 planning 文件。 ```text -/gsd-map-codebase +/gsd-onboard ``` -GSD Core 会派生四个并行映射子代理(您将看到"Spawning 4 parallel codebase mapper agents…"——这需要 1–5 分钟;请勿中断)。每个代理专注于不同的关注点: +如果 onboarding 报告缺少代码库映射,请选择推荐选项并运行打印出的 `/gsd-map-codebase` 交接命令,然后重新运行 `/gsd-onboard`。`/gsd-onboard --fast` 只适合轻量首次通过;在 `/gsd-new-project` 前仍需要完整映射。`/gsd-map-codebase` 会派生四个并行映射子代理(您将看到"Spawning 4 parallel codebase mapper agents…"——这需要 1–5 分钟;请勿中断)。每个代理专注于不同的关注点: | 代理 | 关注点 | |-------|-------| @@ -84,7 +84,7 @@ Created .planning/codebase/: --- -## 第 4 步 — 清除上下文并创建项目 +## 第 4 步 — 重新运行 onboarding 并初始化项目 清除会话窗口: @@ -92,12 +92,14 @@ Created .planning/codebase/: /clear ``` -现在创建项目。由于 GSD Core 在上一步中发现了现有代码,它已经知道这是一个棕地项目。当您运行 `/gsd-new-project` 时,问题将聚焦于您所*新增*的内容,而非重新描述已有的内容: +现在再次运行 `/gsd-onboard`。如果 GSD Core 检测到 ADR、PRD、spec、RFC 或根级需求文档,请先接受推荐的 `/gsd-ingest-docs` 交接,然后再运行 `/gsd-onboard`。上下文准备好后,onboarding 会打印项目初始化交接命令: ```text /gsd-new-project ``` +由于 GSD Core 在上一步中发现了现有代码,`/gsd-new-project` 知道这是一个棕地项目。问题将聚焦于您所*新增*的内容,而非重新描述已有的内容: + GSD Core 会询问您想构建什么。请用您正在添加的功能来回答,而不是描述整个代码库: ```text @@ -124,6 +126,8 @@ Proposed Roadmap 批准路线图。 +项目设置完成后,再运行一次 `/gsd-onboard`。现在 `PROJECT.md`、`REQUIREMENTS.md`、`ROADMAP.md` 和 `STATE.md` 都已存在,onboarding 会创建或确认 `.planning/onboarding/SUMMARY.md`。 + **在 `.planning/` 中创建的内容:** ```text @@ -133,7 +137,8 @@ Proposed Roadmap ROADMAP.md ← Phase 1, status: pending STATE.md ← session memory config.json ← workflow settings - codebase/ ← the seven map files from Step 3 + onboarding/SUMMARY.md ← onboarding 状态和下一个命令 + codebase/ ← 第 3 步生成的七个映射文件 ``` 注意 `.planning/codebase/` 已经从第 3 步存在。GSD Core 在编写 `PROJECT.md` 时读取了这些文件,这就是为什么它无需您描述即可填充已验证的需求。 @@ -212,6 +217,7 @@ Proposed Roadmap ## 您学到了什么 +- `/gsd-onboard` 如何安全编排 brownfield 设置,不嵌套交互式命令,也不覆盖现有 planning 文件。 - `/gsd-map-codebase` 如何运行四个并行代理,在 `.planning/codebase/` 中生成 `STACK.md`、`ARCHITECTURE.md`、`CONVENTIONS.md`、`CONCERNS.md`、`STRUCTURE.md`、`TESTING.md` 和 `INTEGRATIONS.md`。 - 在棕地仓库中运行 `/gsd-new-project` 如何将问题聚焦于您所*新增*的内容,并从现有代码中填充已验证的需求。 - 代码库映射如何塑造 `/gsd-discuss-phase` 中的每个问题——文件路径、模式和规范均来自您的实际代码。 diff --git a/eslint.config.mjs b/eslint.config.mjs index c48dca32b..861f86b6d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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', @@ -163,6 +165,7 @@ export default tseslint.config( 'gsd-core/bin/lib/eval.cjs', 'gsd-core/bin/lib/eval-command-router.cjs', 'gsd-core/bin/lib/init-command-router.cjs', + 'gsd-core/bin/lib/onboard-projection.cjs', 'gsd-core/bin/lib/agent-command-router.cjs', 'gsd-core/bin/lib/agent-install-check.cjs', 'gsd-core/bin/lib/task-command-router.cjs', diff --git a/gsd-core/bin/gsd-tools.cjs b/gsd-core/bin/gsd-tools.cjs index c3cec6716..99c806ef3 100755 --- a/gsd-core/bin/gsd-tools.cjs +++ b/gsd-core/bin/gsd-tools.cjs @@ -179,14 +179,23 @@ * * Loop Extension Point Queries (ADR-857 phase 3c): * loop render-hooks Resolve + render active Capability hooks at a loop point + * [--config-dir ] [--runtime ] [--active-cap ] * Returns JSON envelope { point, activeHooks, rendered } * Valid points: discuss:pre/post, plan:pre/post, * execute:pre/wave:pre/wave:post/post, verify:pre/post, ship:pre/post + * --runtime: override the auto-detected runtime (#2003) so the config + * dir resolves to that runtime's home even when + * .planning/config.json persists a different runtime. * * Capability State (ADR-857 phase 4b): - * capability state [--config-dir ] Resolve per-capability install/surface/hook-activation state + * capability state [--config-dir ] [--runtime ] Resolve per-capability install/surface/hook-activation state * Returns JSON envelope { runtimeConfigDir, capabilities[] } * --config-dir: runtime config dir (default: auto-detect current runtime) + * --runtime: override the auto-detected runtime (#2003); bypasses the + * GSD_RUNTIME → config.runtime → 'claude' precedence so a + * repo with a persisted runtime can still resolve another + * runtime's config dir (e.g. driving Claude Code from a + * repo that persists runtime:"codex"). * * GSD-2 Migration: * from-gsd2 [--path ] [--force] [--dry-run] @@ -1598,9 +1607,28 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand } loopActiveCap = value; } + // --runtime (#2003): explicit runtime override so the config-dir + // resolution bypasses the persisted-runtime fallback (GSD_RUNTIME → + // config.runtime). Mirrors the --config-dir dual-form (--runtime X / + // --runtime=X) and the capability-set --runtime precedent. + let loopRuntime = undefined; + const runtimeEqArg = args.find(arg => arg.startsWith('--runtime=')); + const runtimeIdx = args.indexOf('--runtime'); + if (runtimeEqArg) { + const value = runtimeEqArg.slice('--runtime='.length).trim(); + if (!value) error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined); + loopRuntime = value; + } else if (runtimeIdx !== -1) { + const value = args[runtimeIdx + 1]; + if (!value || value.startsWith('--')) { + error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined); + } + loopRuntime = value; + } loopResolver.cmdLoopRenderHooks(cwd, args[2], raw, { configDir: loopConfigDir ? path.resolve(loopConfigDir) : undefined, activeCap: loopActiveCap, + runtime: loopRuntime, }); } else { error( @@ -1752,7 +1780,24 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand configDir = configDirVal; } const resolvedConfigDir = configDir ? path.resolve(configDir) : null; - capabilityState.cmdCapabilityState(cwd, resolvedConfigDir, raw, {}); + // --runtime (#2003): explicit runtime override so the config-dir + // resolution bypasses the persisted-runtime fallback. Dual-form like + // --config-dir (--runtime X / --runtime=X). + let stateRuntime = undefined; + const stateRuntimeEqArg = args.find(arg => arg.startsWith('--runtime=')); + const stateRuntimeIdx = args.indexOf('--runtime'); + if (stateRuntimeEqArg) { + const value = stateRuntimeEqArg.slice('--runtime='.length).trim(); + if (!value) error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined); + stateRuntime = value; + } else if (stateRuntimeIdx !== -1) { + const value = args[stateRuntimeIdx + 1]; + if (!value || value.startsWith('--')) { + error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined); + } + stateRuntime = value; + } + capabilityState.cmdCapabilityState(cwd, resolvedConfigDir, raw, { runtime: stateRuntime }); } else if (capSubcommand === 'set') { // capability set [--on|--off|--enable|--disable] [--gate =]... [--config-dir ] [--runtime ] [--scope ] const capId = args[2]; diff --git a/gsd-core/bin/lib/capability-registry.cjs b/gsd-core/bin/lib/capability-registry.cjs index a4706bfa3..23acba5ba 100644 --- a/gsd-core/bin/lib/capability-registry.cjs +++ b/gsd-core/bin/lib/capability-registry.cjs @@ -10,7 +10,7 @@ const capabilities = { "ai-integration": { "id": "ai-integration", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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.3", + "version": "1.7.0-rc.4", "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", @@ -157,7 +157,7 @@ const capabilities = { "assumption-delta": { "id": "assumption-delta", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -203,7 +203,7 @@ const capabilities = { "audit": { "id": "audit", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -240,7 +240,7 @@ const capabilities = { "augment": { "id": "augment", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -343,7 +343,7 @@ const capabilities = { "claude": { "id": "claude", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Claude Code", "description": "Anthropic Claude Code — primary development runtime; tier-1 support with full hook surface and skills-based global install.", "tier": "core", @@ -424,10 +424,97 @@ const capabilities = { } } }, + "claude-orchestration": { + "id": "claude-orchestration", + "role": "feature", + "version": "1.7.0-rc.4", + "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 --run-id \n[--phase-dir ] [--budget ]` (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.3", + "version": "1.7.0-rc.4", "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", @@ -488,7 +575,7 @@ const capabilities = { "code-review": { "id": "code-review", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Code review", "description": "Source-file code review and review-fix workflow support for completed execution work.", "tier": "full", @@ -549,7 +636,7 @@ const capabilities = { "codebuddy": { "id": "codebuddy", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "CodeBuddy", "description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", "tier": "core", @@ -652,7 +739,7 @@ const capabilities = { "codex": { "id": "codex", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -723,7 +810,7 @@ const capabilities = { "copilot": { "id": "copilot", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -810,7 +897,7 @@ const capabilities = { "cursor": { "id": "cursor", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -913,7 +1000,7 @@ const capabilities = { "drift": { "id": "drift", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -991,7 +1078,7 @@ const capabilities = { "external-job": { "id": "external-job", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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/.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", @@ -1074,7 +1161,7 @@ const capabilities = { "gap-analysis": { "id": "gap-analysis", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -1115,7 +1202,7 @@ const capabilities = { "graphify": { "id": "graphify", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -1156,7 +1243,7 @@ const capabilities = { "hermes": { "id": "hermes", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -1227,7 +1314,7 @@ const capabilities = { "intel": { "id": "intel", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -1279,7 +1366,7 @@ const capabilities = { "kilo": { "id": "kilo", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -1372,7 +1459,7 @@ const capabilities = { "kimi": { "id": "kimi", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -1446,7 +1533,7 @@ const capabilities = { "mempalace": { "id": "mempalace", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -1620,7 +1707,7 @@ const capabilities = { "nyquist": { "id": "nyquist", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Nyquist validation", "description": "Validation coverage audit that maps executed work back to tests and manual-only evidence.", "tier": "full", @@ -1670,7 +1757,7 @@ const capabilities = { "opencode": { "id": "opencode", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -1759,7 +1846,7 @@ const capabilities = { "pattern-mapper": { "id": "pattern-mapper", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Pattern mapping", "description": "Optional codebase-pattern mapping before planning; owns the pattern mapper agent and workflow.pattern_mapper activation key.", "tier": "full", @@ -1813,7 +1900,7 @@ const capabilities = { "profile-pipeline": { "id": "profile-pipeline", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -1890,7 +1977,7 @@ const capabilities = { "qwen": { "id": "qwen", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Qwen Code", "description": "Qwen Code (Alibaba) — nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", "tier": "core", @@ -1965,7 +2052,7 @@ const capabilities = { "research": { "id": "research", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Phase research", "description": "Optional phase research before planning; owns the phase researcher agent and workflow.research activation key.", "tier": "standard", @@ -2017,7 +2104,7 @@ const capabilities = { "schema-gate": { "id": "schema-gate", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -2063,7 +2150,7 @@ const capabilities = { "security": { "id": "security", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Security enforcement", "description": "Threat mitigation verification and ship-time security blocking for phases with security enforcement enabled.", "tier": "full", @@ -2162,7 +2249,7 @@ const capabilities = { "tdd": { "id": "tdd", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -2215,7 +2302,7 @@ const capabilities = { "trae": { "id": "trae", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Trae IDE", "description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.", "tier": "core", @@ -2301,7 +2388,7 @@ const capabilities = { "ui": { "id": "ui", "role": "feature", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "UI design contracts", "description": "UI-SPEC design contract + retrospective UI audit for frontend phases.", "tier": "full", @@ -2396,7 +2483,7 @@ const capabilities = { "windsurf": { "id": "windsurf", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Windsurf", "description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.", "tier": "core", @@ -2475,7 +2562,7 @@ const capabilities = { "zcode": { "id": "zcode", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "ZCode", "description": "ZCode (Z.ai) — desktop Agentic Development Environment for GLM-5.2; Claude-shaped nested skills at ~/.zcode/skills//SKILL.md, slash commands, named subagents, native MCP; declarative plugin surface; profile-marker install; tier-2 community support.", "tier": "core", @@ -2845,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", @@ -2885,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 --run-id \n[--phase-dir ] [--budget ]` (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", @@ -3095,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", @@ -3146,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", @@ -3392,7 +3535,7 @@ const runtimes = { "antigravity": { "id": "antigravity", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -3486,7 +3629,7 @@ const runtimes = { "augment": { "id": "augment", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -3589,7 +3732,7 @@ const runtimes = { "claude": { "id": "claude", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Claude Code", "description": "Anthropic Claude Code — primary development runtime; tier-1 support with full hook surface and skills-based global install.", "tier": "core", @@ -3673,7 +3816,7 @@ const runtimes = { "cline": { "id": "cline", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -3734,7 +3877,7 @@ const runtimes = { "codebuddy": { "id": "codebuddy", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "CodeBuddy", "description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", "tier": "core", @@ -3837,7 +3980,7 @@ const runtimes = { "codex": { "id": "codex", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -3908,7 +4051,7 @@ const runtimes = { "copilot": { "id": "copilot", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -3995,7 +4138,7 @@ const runtimes = { "cursor": { "id": "cursor", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -4098,7 +4241,7 @@ const runtimes = { "hermes": { "id": "hermes", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -4169,7 +4312,7 @@ const runtimes = { "kilo": { "id": "kilo", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -4262,7 +4405,7 @@ const runtimes = { "kimi": { "id": "kimi", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -4336,7 +4479,7 @@ const runtimes = { "opencode": { "id": "opencode", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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", @@ -4425,7 +4568,7 @@ const runtimes = { "qwen": { "id": "qwen", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Qwen Code", "description": "Qwen Code (Alibaba) — nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", "tier": "core", @@ -4500,7 +4643,7 @@ const runtimes = { "trae": { "id": "trae", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Trae IDE", "description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.", "tier": "core", @@ -4586,7 +4729,7 @@ const runtimes = { "windsurf": { "id": "windsurf", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "Windsurf", "description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.", "tier": "core", @@ -4665,7 +4808,7 @@ const runtimes = { "zcode": { "id": "zcode", "role": "runtime", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "title": "ZCode", "description": "ZCode (Z.ai) — desktop Agentic Development Environment for GLM-5.2; Claude-shaped nested skills at ~/.zcode/skills//SKILL.md, slash commands, named subagents, native MCP; declarative plugin surface; profile-marker install; tier-2 community support.", "tier": "core", @@ -4777,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", @@ -4916,6 +5064,7 @@ const _requiresGraph = { "audit": [], "augment": [], "claude": [], + "claude-orchestration": [], "cline": [], "code-review": [], "codebuddy": [], diff --git a/gsd-core/bin/lib/claude-orchestration-command-router.cjs b/gsd-core/bin/lib/claude-orchestration-command-router.cjs new file mode 100644 index 000000000..a07ad64c2 --- /dev/null +++ b/gsd-core/bin/lib/claude-orchestration-command-router.cjs @@ -0,0 +1,136 @@ +"use strict"; +/** + * Claude orchestration command router — CLI dispatcher for + * `gsd-tools claude-orchestration `. + * + * #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 ] [--agent-sdk-version ] [--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 --run-id [--phase-dir ] [--budget ] + * 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 [...]\n' + + ' detect-backend [--runtime ] [--agent-sdk-version ] [--no-nested-dispatch]\n' + + ' emit-workflow --waves --run-id [--phase-dir ] [--budget ]'); +} +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 '); + return; + } + if (!runId) { + error('emit-workflow requires --run-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 }; diff --git a/gsd-core/bin/lib/claude-orchestration.cjs b/gsd-core/bin/lib/claude-orchestration.cjs new file mode 100644 index 000000000..956fdc2ed --- /dev/null +++ b/gsd-core/bin/lib/claude-orchestration.cjs @@ -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, +}; diff --git a/gsd-core/references/gsd-run-resolver.md b/gsd-core/references/gsd-run-resolver.md new file mode 100644 index 000000000..01f6acb6d --- /dev/null +++ b/gsd-core/references/gsd-run-resolver.md @@ -0,0 +1,8 @@ +# Standard `gsd_run` Resolver + +Use this bootstrap block when a workflow must call `gsd-tools` from the active worktree/runtime. +Keep this resolver centralized; workflows should reference this file instead of copying the shell probe. + +```bash +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +``` diff --git a/gsd-core/templates/project.md b/gsd-core/templates/project.md index a78c5c220..d63152fbc 100644 --- a/gsd-core/templates/project.md +++ b/gsd-core/templates/project.md @@ -166,7 +166,7 @@ and implemented by workflows/transition.md and workflows/complete-milestone.md. For existing codebases: -1. **Map codebase first** via `/gsd:map-codebase` +1. **Onboard or map codebase first** via `/gsd:onboard` (recommended first-time path) or `/gsd:map-codebase` 2. **Infer Validated requirements** from existing code: - What does the codebase actually do? diff --git a/gsd-core/workflows/do.md b/gsd-core/workflows/do.md index a7268e950..db0dfa312 100644 --- a/gsd-core/workflows/do.md +++ b/gsd-core/workflows/do.md @@ -40,8 +40,9 @@ Evaluate `$ARGUMENTS` against these routing rules. Apply the **first matching** | If the text describes... | Route to | Why | |--------------------------|----------|-----| -| Starting a new project, "set up", "initialize" | `/gsd:new-project` | Needs full project initialization | -| Mapping or analyzing an existing codebase | `/gsd:map-codebase` | Codebase discovery | +| Starting a new greenfield project, "set up", "initialize" | `/gsd:new-project` | Needs full project initialization | +| First-time setup for an existing codebase, brownfield onboarding | `/gsd:onboard` | Safe map → docs ingest → project setup sequence | +| Mapping or analyzing an existing codebase map | `/gsd:map-codebase` | Codebase discovery or refresh | | A bug, error, crash, failure, or something broken | `/gsd:debug` | Needs systematic investigation | | Spiking, "test if", "will this work", "experiment", "prove this out", validate feasibility | `/gsd:spike` | Throwaway experiment to validate feasibility | | Sketching, "mockup", "what would this look like", "prototype the UI", "design this", explore visual direction | `/gsd:sketch` | Throwaway HTML mockups to explore design | @@ -61,7 +62,7 @@ Evaluate `$ARGUMENTS` against these routing rules. Apply the **first matching** | Completing a milestone, shipping, releasing | `/gsd:complete-milestone` | Milestone lifecycle | | A specific, actionable, small task (add feature, fix typo, update config) | `/gsd:quick` | Self-contained, single executor | -**Requires `.planning/` directory:** All routes except `/gsd:new-project`, `/gsd:map-codebase`, `/gsd:spike`, `/gsd:sketch`, and `/gsd:help`. If the project doesn't exist and the route requires it, suggest `/gsd:new-project` first. +**Requires `.planning/` directory:** All routes except `/gsd:new-project`, `/gsd:onboard`, `/gsd:map-codebase`, `/gsd:spike`, `/gsd:sketch`, and `/gsd:help`. If the project doesn't exist and the route requires it, suggest `/gsd:onboard` for existing codebases or `/gsd:new-project` for greenfield projects. **Ambiguity handling:** If the text could reasonably match multiple routes, ask the user via AskUserQuestion with the top 2-3 options. For example: diff --git a/gsd-core/workflows/help/modes/brief.md b/gsd-core/workflows/help/modes/brief.md index 359c32f1a..46a1cd41f 100644 --- a/gsd-core/workflows/help/modes/brief.md +++ b/gsd-core/workflows/help/modes/brief.md @@ -7,7 +7,8 @@ One-liner refresher for returning users. Output ONLY the `` content b ```text /gsd:new-project Initialize a project (greenfield) -/gsd:map-codebase Map an existing codebase (brownfield) +/gsd:onboard Onboard an existing codebase (brownfield) +/gsd:map-codebase Refresh/map codebase intelligence /gsd:plan-phase Create a phase plan /gsd:execute-phase Execute a phase /gsd:progress Where am I, what's next diff --git a/gsd-core/workflows/help/modes/default.md b/gsd-core/workflows/help/modes/default.md index 866f1d7dd..86274d63b 100644 --- a/gsd-core/workflows/help/modes/default.md +++ b/gsd-core/workflows/help/modes/default.md @@ -11,11 +11,12 @@ Plan-driven development for solo agentic work with Claude Code. GSD Core turns a ```text /gsd:new-project # Greenfield: questioning → research → requirements → roadmap +/gsd:onboard # Existing codebase: map → ingest docs → initialize planning /gsd:plan-phase 1 # Create a detailed plan for phase 1 /gsd:execute-phase 1 # Execute all plans in the phase ``` -Existing codebase? Run `/gsd:map-codebase` first to ground GSD in your code. +Existing codebase? Run `/gsd:onboard` to map the repo, ingest existing docs, and initialize planning safely. ## Common commands diff --git a/gsd-core/workflows/help/modes/full.md b/gsd-core/workflows/help/modes/full.md index a6686bbae..235fae38d 100644 --- a/gsd-core/workflows/help/modes/full.md +++ b/gsd-core/workflows/help/modes/full.md @@ -62,6 +62,16 @@ Creates all `.planning/` artifacts: Usage: `/gsd:new-project` +**`/gsd:onboard [--fast] [--text]`** +Guide first-time onboarding for an existing codebase. + +- Detects brownfield code, existing planning docs, and partial `.planning/` state +- Routes through `/gsd:map-codebase`, `/gsd:ingest-docs`, and `/gsd:new-project` in the safe order +- Creates `.planning/onboarding/SUMMARY.md` after project setup +- Idempotent: confirms existing artifacts and does not overwrite planning silently + +Usage: `/gsd:onboard` + **`/gsd:map-codebase [--fast] [--focus ] [--query ]`** Map an existing codebase for brownfield projects. @@ -72,7 +82,7 @@ Map an existing codebase for brownfield projects. - Analyzes codebase with parallel Explore agents - Creates `.planning/codebase/` with 7 focused documents - Covers stack, architecture, structure, conventions, testing, integrations, concerns -- Use before `/gsd:new-project` on existing codebases +- Usually reached through `/gsd:onboard` for first-time existing-codebase setup; run directly to refresh or focus a map Usage: `/gsd:map-codebase` diff --git a/gsd-core/workflows/help/modes/topic.md b/gsd-core/workflows/help/modes/topic.md index d89229607..89b380263 100644 --- a/gsd-core/workflows/help/modes/topic.md +++ b/gsd-core/workflows/help/modes/topic.md @@ -9,7 +9,7 @@ Emit a section from the full reference for the topic in `$ARGUMENTS`. Read `work |---|---| | `next`, `smart-entry` | `### Smart Entry` | | `workflow`, `core`, `core-workflow` | `## Core Workflow` (entire section through end of `### Quick Mode`) | -| `init`, `new-project` | `### Project Initialization` | +| `init`, `new-project`, `onboard`, `onboarding`, `brownfield` | `### Project Initialization` | | `map`, `map-codebase` | The `/gsd:map-codebase` block under `### Project Initialization` | | `discuss`, `discuss-phase` | The `/gsd:discuss-phase` block under `### Phase Planning` | | `plan`, `planning`, `plan-phase` | `### Phase Planning` | diff --git a/gsd-core/workflows/onboard.md b/gsd-core/workflows/onboard.md new file mode 100644 index 000000000..dbcd7c147 --- /dev/null +++ b/gsd-core/workflows/onboard.md @@ -0,0 +1,277 @@ +# /gsd:onboard Workflow + +One-command onboarding for an existing or unknown repo. This workflow is a thin +renderer around `init onboard`; deterministic routing lives in the CLI projection. + +@~/.claude/gsd-core/references/gsd-run-resolver.md + +## 1. Render the Onboarding Projection + +Parse `$ARGUMENTS`: +- `--fast` passes `--fast` to `init onboard`. Fast mode accepts the fast map for lightweight onboarding only; `next_action` still decides whether complete map work is required before project setup. +- `--text` forces text-mode choices for runtimes without `AskUserQuestion`. + +Run the standard `gsd_run` resolver from the reference above, then run the projection from the runtime root: + +```bash +# If --fast was parsed from $ARGUMENTS: +INIT=$(gsd_run --cwd "$_GSD_RUNTIME_ROOT" init onboard --fast --raw) +# Otherwise: +INIT=$(gsd_run --cwd "$_GSD_RUNTIME_ROOT" init onboard --raw) +if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi +``` + +Parse JSON fields from `INIT`: + +- `next_action.kind`, `next_action.command`, `next_action.reason`, `next_action.missing`, `next_action.summary_path` +- `handoff_commands.ingest_docs`, `handoff_commands.manager`, `handoff_commands.new_project`, `handoff_commands.onboard` +- `map_readiness`, `codebase_map_summary_status`, `codebase_map_final_status` +- `planning_exists`, `project_exists`, `requirements_exists`, `roadmap_exists`, `state_exists` +- `is_brownfield`, `fast_mode`, `has_codebase_map`, `has_fast_codebase_map` +- `missing_codebase_map_files`, `missing_fast_codebase_map_files` +- `has_docs_candidates`, `doc_candidate_count`, `onboarding_summary_exists` +- `commit_docs`, `text_mode`, `has_git`, `git_worktree_root`, `in_nested_subdir` + +Set: +- `TEXT_MODE=true` if `--text` is present or `text_mode` is true. When `TEXT_MODE` is active, replace every `AskUserQuestion` call below with a plain-text numbered list and ask the user to type their choice number — required for non-Claude runtimes (OpenAI Codex, Gemini CLI, etc.) where `AskUserQuestion` is not available. +- `ONBOARDING_ROOT={git_worktree_root || _GSD_RUNTIME_ROOT}`. + +If `has_git` and `in_nested_subdir` are true, warn that onboarding artifacts belong to the outer worktree at `git_worktree_root`. Do not run `git init`. + +## 2. Execute `next_action` + +### `map-codebase` + +If `next_action.kind == "map-codebase"`: + +- If `TEXT_MODE=true`, print: + +```text +{next_action.reason} +Missing map files: {fast_mode ? missing_fast_codebase_map_files : missing_codebase_map_files} + +1. Map codebase first — run {next_action.command} from worktree root {ONBOARDING_ROOT} (Recommended) +2. Skip mapping — continue with weaker onboarding context + +Enter number: +``` + +- Otherwise use AskUserQuestion: + - header: "Codebase" + - question: "{next_action.reason} Map it first?" + - options: + - "Map codebase first" — Run `{next_action.command}` from worktree root `{ONBOARDING_ROOT}` (Recommended) + - "Skip mapping" — Continue with weaker onboarding context + +If the user chooses mapping, do not nest the interactive workflow. Print: + +```text +Run from worktree root {ONBOARDING_ROOT}: + +{next_action.command} + +Then rerun {handoff_commands.onboard} from the same worktree root. +``` + +Exit. If the user skips mapping: + +- If `(project_exists || requirements_exists || roadmap_exists || state_exists) && (!project_exists || !requirements_exists || !roadmap_exists || !state_exists)`, route the skip to the partial planning guard instead: + +```text +Skipping codebase mapping may give downstream steps weaker context, but project planning exists and is incomplete. + +PROJECT.md: {project_exists ? "present" : "missing"} +REQUIREMENTS.md: {requirements_exists ? "present" : "missing"} +ROADMAP.md: {roadmap_exists ? "present" : "missing"} +STATE.md: {state_exists ? "present" : "missing"} + +Run the appropriate lower-level command to fill the missing planning artifact(s), then rerun {handoff_commands.onboard}. +``` + +Exit. + +- If `has_docs_candidates && !project_exists`, route the skip to docs ingest instead: + +```text +Skipping codebase mapping may give downstream steps weaker context, but existing ADR/PRD/SPEC/RFC documents should still be ingested before {handoff_commands.new_project}. + +Run from worktree root {ONBOARDING_ROOT}: + +{handoff_commands.ingest_docs} + +Then rerun {handoff_commands.onboard} from the same worktree root. +``` + +Exit. + +- Otherwise print: + +```text +Skipping codebase mapping may give {handoff_commands.new_project} weaker context. + +Run from worktree root {ONBOARDING_ROOT}: + +{handoff_commands.new_project} + +Then rerun {handoff_commands.onboard} from the same worktree root. +``` + +Exit. + +### `ingest-docs` + +If `next_action.kind == "ingest-docs"`: + +- If `TEXT_MODE=true`, print: + +```text +{next_action.reason} +Detected {doc_candidate_count} possible ADR/PRD/SPEC/RFC document(s). + +1. Ingest docs first — run {next_action.command} from worktree root {ONBOARDING_ROOT} (Recommended) +2. Skip docs ingest — continue to {handoff_commands.new_project} + +Enter number: +``` + +- Otherwise use AskUserQuestion: + - header: "Docs" + - question: "Detected {doc_candidate_count} possible ADR/PRD/SPEC/RFC document(s). Ingest them first?" + - options: + - "Ingest docs first" — Run `{next_action.command}` from worktree root `{ONBOARDING_ROOT}` (Recommended) + - "Skip docs ingest" — Continue to `{handoff_commands.new_project}` + +If the user chooses ingest, print: + +```text +Run from worktree root {ONBOARDING_ROOT}: + +{next_action.command} + +Then rerun {handoff_commands.onboard} from the same worktree root. +``` + +Exit. If the user skips docs ingest, print: + +```text +Skipping docs ingest may omit existing ADR/PRD/SPEC/RFC context from {handoff_commands.new_project}. + +Run from worktree root {ONBOARDING_ROOT}: + +{handoff_commands.new_project} + +Then rerun {handoff_commands.onboard} from the same worktree root. +``` + +Exit. + +### `complete-map-before-new-project` + +If `next_action.kind == "complete-map-before-new-project"`, print: + +```text +{next_action.reason} + +Run from worktree root {ONBOARDING_ROOT}: + +{next_action.command} + +Then rerun {handoff_commands.onboard} from the same worktree root. +``` + +Exit. + +### `new-project` + +If `next_action.kind == "new-project"`, print: + +```text +{next_action.reason} + +Run from worktree root {ONBOARDING_ROOT}: + +{next_action.command} + +Then rerun {handoff_commands.onboard} from the same worktree root. +``` + +Exit. + +### `partial-planning` + +If `next_action.kind == "partial-planning"`, print: + +```text +Project planning exists but is incomplete. + +Missing files: {next_action.missing} +REQUIREMENTS.md: {requirements_exists ? "present" : "missing"} +ROADMAP.md: {roadmap_exists ? "present" : "missing"} +STATE.md: {state_exists ? "present" : "missing"} + +Run the appropriate lower-level command to fill the missing planning artifact(s), then rerun {handoff_commands.onboard}. +``` + +Exit. + +### `ready` + +If `next_action.kind == "ready"`, print the final status section and exit. + +### `write-summary` + +If `next_action.kind == "write-summary"`, continue to summary creation. + +## 3. Create Onboarding Summary + +Create `{ONBOARDING_ROOT}/{next_action.summary_path}`. Do not overwrite an existing summary; the projection should only route here when the summary is missing. + +Summary template: + +```markdown +# Onboarding Summary + +## Project State +- PROJECT.md: {project_exists ? "present" : "missing"} +- REQUIREMENTS.md: {requirements_exists ? "present" : "missing"} +- ROADMAP.md: {roadmap_exists ? "present" : "missing"} +- STATE.md: {state_exists ? "present" : "missing"} + +## Codebase Context +- Brownfield repo: {is_brownfield ? "yes" : "no"} +- Map readiness: {map_readiness} +- Codebase map: {codebase_map_summary_status} +- Fast map available: {has_fast_codebase_map ? "yes" : "no"} + +## Docs Context +- Existing ADR/PRD/SPEC/RFC candidates: {has_docs_candidates ? doc_candidate_count : 0} + +## Recommended Next Step +- {handoff_commands.manager} +``` + +If `commit_docs` is true, commit only the summary path from the onboarding root: + +```bash +gsd_run --cwd "$ONBOARDING_ROOT" query commit "docs: create onboarding summary" --files .planning/onboarding/SUMMARY.md +``` + +Continue to final status. + +## 4. Final Status + +Print: + +```text +Onboarding status: +- PROJECT.md: {project_exists ? "present" : "missing"} +- REQUIREMENTS.md: {requirements_exists ? "present" : "missing"} +- ROADMAP.md: {roadmap_exists ? "present" : "missing"} +- STATE.md: {state_exists ? "present" : "missing"} +- Codebase map: {codebase_map_final_status} +- Onboarding summary: present + +Next recommended command: {handoff_commands.manager} +``` + +Do not run implementation execution or shipping from onboarding. diff --git a/gsd-core/workflows/quick.md b/gsd-core/workflows/quick.md index 40668ff58..fc9f98756 100644 --- a/gsd-core/workflows/quick.md +++ b/gsd-core/workflows/quick.md @@ -671,6 +671,26 @@ fi **Step 6: Spawn executor** +Auto-degrade to sequential if HEAD has diverged from the worktree fork base (#1941, mirrors +execute-phase's #683/#1369 guard). Claude Code's `isolation="worktree"` forks new worktrees from +`origin/HEAD`, not the live local HEAD. If a prior quick task in this session (or the Step 5.6 +pre-dispatch plan commit above) advanced local HEAD without an intervening `git push`, +`origin/HEAD` stays pinned to a stale ancestor and the executor's `worktree_branch_check` guard +halts with a base-mismatch fatal — potentially many commits behind, not just one. Run this check +immediately before capturing `EXPECTED_BASE` so it reflects the most current local state. + +```bash +if [ "$RUNTIME" = "claude" ] && [ "${USE_WORKTREES:-true}" != "false" ]; then + _QUICK_SHOULD_DEGRADE=$(gsd_run query worktree.base-check --pick shouldDegrade 2>/dev/null || true) + if [ "$_QUICK_SHOULD_DEGRADE" = "true" ]; then + _QUICK_DEGRADE_MSG=$(gsd_run query worktree.base-check --pick message 2>/dev/null || true) + [ -n "$_QUICK_DEGRADE_MSG" ] && printf '%s\n' "$_QUICK_DEGRADE_MSG" >&2 + echo "⚠ [#1941] Worktree fork base diverged from orchestrator HEAD — auto-degrading to sequential mode for this quick task to avoid a base-mismatch halt." >&2 + USE_WORKTREES=false + fi +fi +``` + Capture current HEAD before spawning (used for worktree branch check): ```bash EXPECTED_BASE=$(git rev-parse HEAD) diff --git a/package-lock.json b/package-lock.json index 715dae9b9..17ac44d02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@opengsd/gsd-core", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@opengsd/gsd-core", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "license": "MIT", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.84", diff --git a/package.json b/package.json index 3f8d060e4..844d518ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@opengsd/gsd-core", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "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": { diff --git a/scripts/sync-runtime-launcher.cjs b/scripts/sync-runtime-launcher.cjs index 6ce5a6303..dbc38a2b4 100644 --- a/scripts/sync-runtime-launcher.cjs +++ b/scripts/sync-runtime-launcher.cjs @@ -343,9 +343,12 @@ function transformFile(content, preamble) { } } - // Insert preamble into the first gsd_run block only + // Insert preamble into the first gsd_run block only — UNLESS this file + // delegates to the shared resolver reference (@-include). Delegating files keep + // the stripped blocks (any inline preamble removed) but never get one inserted. + const delegates = delegatesToResolverReference(content); const finalBlocks = strippedBlocks.map((stripped, bi) => { - if (bi === firstGsdRunBlockIdx) { + if (!delegates && bi === firstGsdRunBlockIdx) { return insertPreamble(stripped, preamble); } return stripped; @@ -374,6 +377,21 @@ function escapeRegExp(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } +/** + * A file "delegates to the shared resolver" when it pulls the canonical gsd_run + * preamble in from gsd-core/references/gsd-run-resolver.md via an @-include + * instead of inlining the snippet (see onboard.md / issue #1990). + * + * These files must NOT carry an inline preamble: the resolver reference ships the + * one canonical copy, and onboard-command.test.cjs asserts the inline form is + * absent. transformFile still STRIPS any inline preamble from them (so a stray + * copy is removed) but never re-inserts one — mirroring the exemption in + * runtime-launcher-parity.test.cjs (subtest B / B2). + */ +function delegatesToResolverReference(content) { + return content.includes('references/gsd-run-resolver.md'); +} + // Main function main() { const preamble = loadPreamble(); diff --git a/skills/gsd-map-codebase/SKILL.md b/skills/gsd-map-codebase/SKILL.md index 1f8fbb1d5..36c909128 100644 --- a/skills/gsd-map-codebase/SKILL.md +++ b/skills/gsd-map-codebase/SKILL.md @@ -42,7 +42,7 @@ Parse the first token of $ARGUMENTS: Check for .planning/STATE.md - loads context if project already initialized **This command can run:** -- Before /gsd-new-project (brownfield codebases) - creates codebase map first +- Via /gsd-onboard for first-time brownfield setup - creates codebase map first - After /gsd-new-project (greenfield codebases) - updates codebase map as code evolves - Anytime to refresh codebase understanding @@ -51,7 +51,7 @@ Check for .planning/STATE.md - loads context if project already initialized **Use map-codebase for:** - Brownfield projects before initialization (understand existing code first) - Refreshing codebase map after significant changes -- Onboarding to an unfamiliar codebase +- Refreshing or deepening an onboarded codebase map - Before major refactoring (understand current state) - When STATE.md references outdated codebase info @@ -71,7 +71,7 @@ Check for .planning/STATE.md - loads context if project already initialized 4. Wait for agents to complete, collect confirmations (NOT document contents) 5. Verify all 7 documents exist with line counts 6. Commit codebase map -7. Offer next steps (typically: /gsd-new-project or /gsd-plan-phase) +7. Offer next steps (typically: /gsd-onboard, /gsd-new-project, or /gsd-plan-phase) diff --git a/skills/gsd-ns-project/SKILL.md b/skills/gsd-ns-project/SKILL.md index 5aa5a8e8b..96d22ac4d 100644 --- a/skills/gsd-ns-project/SKILL.md +++ b/skills/gsd-ns-project/SKILL.md @@ -14,6 +14,7 @@ inline as part of `gsd-audit-milestone`'s output. | User wants | Invoke | |---|---| | Start a new project | gsd-new-project | +| Onboard an existing codebase | gsd-onboard | | Create a new milestone | gsd-new-milestone | | Complete the current milestone | gsd-complete-milestone | | Audit a milestone for issues | gsd-audit-milestone | diff --git a/skills/gsd-onboard/SKILL.md b/skills/gsd-onboard/SKILL.md new file mode 100644 index 000000000..d3b3946f5 --- /dev/null +++ b/skills/gsd-onboard/SKILL.md @@ -0,0 +1,46 @@ +--- +name: gsd-onboard +description: "Guide existing codebase onboarding through mapping, doc ingest, and planning setup" +argument-hint: "[--fast] [--text]" +allowed-tools: + - Read + - Bash + - Write + - Glob + - Grep + - Agent + - AskUserQuestion +--- + + +**Copilot (VS Code):** Use `vscode_askquestions` wherever this workflow calls `AskUserQuestion`. They are equivalent — `vscode_askquestions` is the VS Code Copilot implementation of the same interactive question API. + + + +Guide brownfield onboarding for an existing codebase by routing through the existing GSD primitives in the safe order: codebase map → docs ingest → project initialization → onboarding summary. + +**Creates or confirms:** +- `.planning/codebase/` — evidence-backed codebase map from `/gsd-map-codebase` +- `.planning/PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, `STATE.md` — project setup from `/gsd-new-project` or `/gsd-ingest-docs` +- `.planning/onboarding/SUMMARY.md` — lightweight index of what was learned and the next command + +**Non-goals:** This command does not execute phases, ship work, or overwrite existing planning artifacts without an explicit gate. + + + +@~/.claude/gsd-core/workflows/onboard.md +@~/.claude/gsd-core/references/ui-brand.md +@~/.claude/gsd-core/references/gate-prompts.md + + + +Arguments: $ARGUMENTS + +Flags: +- `--fast` — prefer `/gsd-map-codebase --fast` for the mapping handoff; the complete map is still required before `/gsd-new-project`. +- `--text` — use plain-text numbered lists instead of TUI menus. + + + +Execute the onboard workflow end-to-end. Preserve all safety gates, text-mode fallbacks, idempotency checks, and top-level handoff rules for nested interactive commands. + diff --git a/src/capability-state.cts b/src/capability-state.cts index a43b16f3b..d148d6db7 100644 --- a/src/capability-state.cts +++ b/src/capability-state.cts @@ -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 return manifest; } +/** + * #1858 — Build a skill dependency manifest from a FLAT commands/gsd-.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_` 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 { + const manifest = new Map(); + 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: /commands/gsd/*.md). + * 2. #1858 — otherwise, if the flat source layout is present (gsd-.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-.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 { if (fs.existsSync(commandsGsdDir)) { return loadSkillsManifest(commandsGsdDir); } + // #1858: flat source layout — gsd-.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); } @@ -441,6 +503,7 @@ function resolveCapabilityRuntimeState( cwd: string, runtimeConfigDir: string | undefined | null, configOverride?: Record, + runtimeOverride?: string, ): ResolveCapabilityRuntimeStateResult { const warnings: string[] = []; @@ -460,15 +523,45 @@ function resolveCapabilityRuntimeState( const runtimeHomes = require('./runtime-homes.cjs') as { getGlobalConfigDir: (runtime: string) => string; }; - // eslint-disable-next-line @typescript-eslint/no-require-imports - const runtimeSlash = require('./runtime-slash.cjs') as { - resolveRuntime: (projectDir: string | null | undefined) => string; - }; - // Detect the active runtime via GSD_RUNTIME → config.runtime → 'claude'. - // resolveRuntime reads config.json directly (no side effects) and returns - // a lowercased canonical runtime name. - const detectedRuntime = runtimeSlash.resolveRuntime(cwd); - resolvedConfigDir = runtimeHomes.getGlobalConfigDir(detectedRuntime); + // #2003: an explicit --runtime override bypasses the persisted-runtime + // fallback (GSD_RUNTIME → config.runtime → 'claude') so, e.g., a repo with + // persisted runtime:"codex" resolves the Claude config dir when the operator + // is driving from Claude Code. Canonicalize via runtime-name-policy (handles + // aliases like codex-app → codex); if canonicalization yields nothing, fall + // through to the persisted-runtime resolution below. Mirrors the update- + // context / effort sync precedent (read/diagnostic paths accepting both + // --config-dir and --runtime). + if (typeof runtimeOverride === 'string' && runtimeOverride.trim() !== '') { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const runtimeNamePolicy = require('./runtime-name-policy.cjs') as { + canonicalizeRuntimeName: (value: unknown) => string | null; + }; + const canonical = runtimeNamePolicy.canonicalizeRuntimeName(runtimeOverride); + if (canonical) { + resolvedConfigDir = runtimeHomes.getGlobalConfigDir(canonical); + } else { + // #2003: unknown runtime override — warn (don't silently ignore the + // explicit input) and fall through to persisted-runtime resolution. + // Avoids a silent-wrong-result on this diagnostic command for typos + // (e.g. "cluade") or runtimes known to runtime-homes but not yet to + // the alias manifest (e.g. "grok"). The warning surfaces via the + // `warnings[]` channel consumed by cmdCapabilityState/cmdLoopRenderHooks. + warnings.push( + `--runtime "${runtimeOverride}" is not a known runtime; falling back to auto-detected/persisted runtime resolution`, + ); + } + } + if (!resolvedConfigDir) { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const runtimeSlash = require('./runtime-slash.cjs') as { + resolveRuntime: (projectDir: string | null | undefined) => string; + }; + // Detect the active runtime via GSD_RUNTIME → config.runtime → 'claude'. + // resolveRuntime reads config.json directly (no side effects) and returns + // a lowercased canonical runtime name. + const detectedRuntime = runtimeSlash.resolveRuntime(cwd); + resolvedConfigDir = runtimeHomes.getGlobalConfigDir(detectedRuntime); + } } catch { // Defensive fallback: use ~/.claude if the canonical resolver throws. // eslint-disable-next-line @typescript-eslint/no-require-imports @@ -569,9 +662,12 @@ function cmdCapabilityState( cwd: string, runtimeConfigDir: string | undefined | null, raw: boolean, - _options: Record = {}, + options: Record = {}, ): void { - const result = resolveCapabilityRuntimeState(cwd, runtimeConfigDir); + // #2003: thread an explicit --runtime override so the config-dir resolution + // bypasses the persisted-runtime fallback (GSD_RUNTIME → config.runtime). + const runtimeOverride = typeof options['runtime'] === 'string' ? options['runtime'] : undefined; + const result = resolveCapabilityRuntimeState(cwd, runtimeConfigDir, undefined, runtimeOverride); for (const warning of result.warnings) { coreError(`capability state: ${warning}`); } @@ -619,6 +715,7 @@ export = { // Exported for tests _resolveCommandsGsdDir, _loadInstalledSkillsManifest, + _loadFlatCommandsGsdManifest, _resolveManifest, _isSafePropKey, }; diff --git a/src/claude-orchestration-command-router.cts b/src/claude-orchestration-command-router.cts new file mode 100644 index 000000000..cabc96435 --- /dev/null +++ b/src/claude-orchestration-command-router.cts @@ -0,0 +1,159 @@ +/** + * Claude orchestration command router — CLI dispatcher for + * `gsd-tools claude-orchestration `. + * + * #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 ] [--agent-sdk-version ] [--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 --run-id [--phase-dir ] [--budget ] + * 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 [...]\n' + + ' detect-backend [--runtime ] [--agent-sdk-version ] [--no-nested-dispatch]\n' + + ' emit-workflow --waves --run-id [--phase-dir ] [--budget ]', + ); +} + +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 = {}; + try { + const loaded = configLoader.loadConfig(cwd); + const slice = loaded['claude_orchestration']; + if (slice && typeof slice === 'object' && !Array.isArray(slice)) { + claudeSlice = slice as Record; + } + } catch { + claudeSlice = {}; + } + + // Flatten the nested slice into the dotted-key shape detectWorkflowBackend expects. + const flatConfig: Record = {}; + 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 '); + return; + } + if (!runId) { + error('emit-workflow requires --run-id '); + return; + } + + let waves: unknown; + try { + const content = fs.readFileSync(path.resolve(wavesPath), 'utf8'); + const parsed = JSON.parse(content) as Record; + 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 }; diff --git a/src/claude-orchestration.cts b/src/claude-orchestration.cts new file mode 100644 index 000000000..8e877edbf --- /dev/null +++ b/src/claude-orchestration.cts @@ -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(['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 }).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 }[] = []; + 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(); + 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, +}; diff --git a/src/clusters.cts b/src/clusters.cts index 2ea92abfd..7ba2ff05b 100644 --- a/src/clusters.cts +++ b/src/clusters.cts @@ -33,6 +33,7 @@ export const CLUSTERS: ClusterMap = Object.freeze({ core_loop: Object.freeze([ 'next', 'new-project', + 'onboard', 'discuss-phase', 'plan-phase', 'execute-phase', diff --git a/src/command-aliases.cts b/src/command-aliases.cts index 9df935e33..19c2a36be 100644 --- a/src/command-aliases.cts +++ b/src/command-aliases.cts @@ -296,6 +296,14 @@ export const INIT_COMMAND_ALIASES: CommandAlias[] = [ "subcommand": "new-milestone", "mutation": false }, + { + "canonical": "init.onboard", + "aliases": [ + "init onboard" + ], + "subcommand": "onboard", + "mutation": false + }, { "canonical": "init.quick", "aliases": [ diff --git a/src/init-command-router.cts b/src/init-command-router.cts index 317ce0059..6ea4b4dac 100644 --- a/src/init-command-router.cts +++ b/src/init-command-router.cts @@ -28,6 +28,7 @@ interface InitModule { cmdInitPlanPhase(cwd: string, phase: string | undefined, raw: boolean, opts: Record): void; cmdInitNewProject(cwd: string, raw: boolean): void; cmdInitNewMilestone(cwd: string, raw: boolean): void; + cmdInitOnboard(cwd: string, raw: boolean, opts?: Record): void; cmdInitQuick(cwd: string, name: string, raw: boolean): void; cmdInitIngestDocs(cwd: string, raw: boolean): void; cmdInitResume(cwd: string, raw: boolean): void; @@ -71,6 +72,10 @@ function routeInitCommand({ init, args, cwd, raw, error }: RouteInitCommandOptio }, 'new-project': () => init.cmdInitNewProject(cwd, raw), 'new-milestone': () => init.cmdInitNewMilestone(cwd, raw), + onboard: () => { + const namedArgs = parseNamedArgs(args, [], ['fast', 'text']); + init.cmdInitOnboard(cwd, raw, { fast: namedArgs['fast'], text: namedArgs['text'] }); + }, quick: () => init.cmdInitQuick(cwd, args.slice(2).join(' '), raw), 'ingest-docs': () => init.cmdInitIngestDocs(cwd, raw), resume: () => init.cmdInitResume(cwd, raw), diff --git a/src/init.cts b/src/init.cts index 1b9de1cb0..19d8e6f1c 100644 --- a/src/init.cts +++ b/src/init.cts @@ -51,6 +51,15 @@ const { checkAgentsInstalled } = agentInstallCheck; import gitBaseBranch = require('./git-base-branch.cjs'); const { gitWorktreeInfoInternal } = gitBaseBranch; import { makeResolution } from './resolution.cjs'; +// eslint-disable-next-line @typescript-eslint/no-require-imports -- onboard-projection.cjs is an export= CommonJS module +import onboardProjection = require('./onboard-projection.cjs'); +const { + REQUIRED_CODEBASE_MAP_FILES, + buildOnboardProjection, + hasCodeFilesInternal, + hasPackageFileInternal, + listCodebaseMapFiles, +} = onboardProjection; const { output, error } = io; const { loadConfig, loadConfigResolved } = configLoader; @@ -625,68 +634,11 @@ function cmdInitNewProject(cwd: string, raw: boolean): void { const exaKeyFile = path.join(homedir, '.gsd', 'exa_api_key'); const hasExaSearch = !!(process.env['EXA_API_KEY'] || fs.existsSync(exaKeyFile)); - let hasCode = false; - let hasPackageFile = false; - try { - const codeExtensions = new Set([ - '.ts', '.js', '.py', '.go', '.rs', '.swift', '.java', - '.kt', '.kts', - '.c', '.cpp', '.h', - '.cs', - '.rb', - '.php', - '.dart', - '.m', '.mm', - '.scala', - '.groovy', - '.lua', - '.r', '.R', - '.zig', - '.ex', '.exs', - '.clj', - ]); - const skipDirs = new Set([ - 'node_modules', '.git', '.planning', '.claude', '.codex', - '__pycache__', 'target', 'dist', 'build', - ]); - function findCodeFiles(dir: string, depth: number): boolean { - if (depth > 3) return false; - let entries: fs.Dirent[]; - try { - entries = fs.readdirSync(dir, { withFileTypes: true }); - } catch { - return false; - } - for (const entry of entries) { - if (entry.isFile() && codeExtensions.has(path.extname(entry.name))) return true; - if (entry.isDirectory() && !skipDirs.has(entry.name)) { - if (findCodeFiles(path.join(dir, entry.name), depth + 1)) return true; - } - } - return false; - } - hasCode = findCodeFiles(cwd, 0); - } catch { - /* intentionally empty — best-effort detection */ - } - - hasPackageFile = - pathExistsInternal(cwd, 'package.json') || - pathExistsInternal(cwd, 'requirements.txt') || - pathExistsInternal(cwd, 'Cargo.toml') || - pathExistsInternal(cwd, 'go.mod') || - pathExistsInternal(cwd, 'Package.swift') || - pathExistsInternal(cwd, 'build.gradle') || - pathExistsInternal(cwd, 'build.gradle.kts') || - pathExistsInternal(cwd, 'pom.xml') || - pathExistsInternal(cwd, 'Gemfile') || - pathExistsInternal(cwd, 'composer.json') || - pathExistsInternal(cwd, 'pubspec.yaml') || - pathExistsInternal(cwd, 'CMakeLists.txt') || - pathExistsInternal(cwd, 'Makefile') || - pathExistsInternal(cwd, 'build.zig') || - pathExistsInternal(cwd, 'mix.exs') || - pathExistsInternal(cwd, 'project.clj'); + const hasCode = hasCodeFilesInternal(cwd); + const hasPackageFile = hasPackageFileInternal(cwd); + const isBrownfield = hasCode || hasPackageFile; + const codebaseMapFiles = listCodebaseMapFiles(cwd); + const hasCodebaseMap = codebaseMapFiles.length === REQUIRED_CODEBASE_MAP_FILES.length; const result: Record = { researcher_model: resolveModelInternal(cwd, 'gsd-project-researcher'), @@ -696,14 +648,13 @@ function cmdInitNewProject(cwd: string, raw: boolean): void { commit_docs: config.commit_docs, project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'), - has_codebase_map: pathExistsInternal(cwd, '.planning/codebase'), + has_codebase_map: hasCodebaseMap, planning_exists: pathExistsInternal(cwd, '.planning'), has_existing_code: hasCode, has_package_file: hasPackageFile, - is_brownfield: hasCode || hasPackageFile, - needs_codebase_map: - (hasCode || hasPackageFile) && !pathExistsInternal(cwd, '.planning/codebase'), + is_brownfield: isBrownfield, + needs_codebase_map: isBrownfield && !hasCodebaseMap, ...getInitGitState(cwd), @@ -840,6 +791,25 @@ function cmdInitIngestDocs(cwd: string, raw: boolean): void { output(withProjectRoot(cwd, result), raw); } +function cmdInitOnboard( + cwd: string, + raw: boolean, + options: Record = {}, +): void { + const config = loadConfig(cwd); + const workflowConfig = (config.workflow ?? {}) as Record; + const result = { + ...buildOnboardProjection(cwd, { + commitDocs: !!config.commit_docs, + fast: options['fast'] === true, + textMode: options['text'] === true || !!config.text_mode || !!workflowConfig['text_mode'], + }), + ...getInitGitState(cwd), + }; + + output(withProjectRoot(cwd, result), raw); +} + function cmdInitResume(cwd: string, raw: boolean): void { const config = loadConfig(cwd); @@ -2606,6 +2576,7 @@ export = { cmdInitNewMilestone, cmdInitQuick, cmdInitIngestDocs, + cmdInitOnboard, cmdInitResume, cmdInitVerifyWork, cmdInitPhaseOp, diff --git a/src/install-profiles.cts b/src/install-profiles.cts index 90674c367..fd6bfba76 100644 --- a/src/install-profiles.cts +++ b/src/install-profiles.cts @@ -51,6 +51,7 @@ const PROFILES = Object.freeze({ standard: Object.freeze([ // Core loop 'new-project', + 'onboard', 'discuss-phase', 'plan-phase', 'execute-phase', @@ -875,6 +876,7 @@ export = { writeActiveProfile, // Shared internals parseRequires, + parseCallsAgents, cleanupStagedSkills, // Back-compat / deprecated MINIMAL_SKILL_ALLOWLIST, diff --git a/src/loop-resolver.cts b/src/loop-resolver.cts index 7df0af4df..1b0d3b1aa 100644 --- a/src/loop-resolver.cts +++ b/src/loop-resolver.cts @@ -475,6 +475,12 @@ function cmdLoopRenderHooks( const runtimeConfigDir = typeof options['configDir'] === 'string' ? options['configDir'] : undefined; + // #2003: thread an explicit --runtime override into the capability-state + // resolver so the config-dir resolution bypasses the persisted-runtime + // fallback (GSD_RUNTIME → config.runtime). Without this, a repo with persisted + // runtime:"codex" resolves the config dir to ~/.codex and execute:post / + // verify:post hooks silently no-op when the operator drives from Claude Code. + const runtimeOverride = typeof options['runtime'] === 'string' ? options['runtime'] : undefined; // Load the config snapshot ONCE and share it with both the capability-state // resolver (via configOverride) and loop-hook resolution, so federated keys // present in loadConfig resolve identically for `active` and for hook when/ @@ -488,7 +494,7 @@ function cmdLoopRenderHooks( } catch { config = {}; } - const state = resolveCapabilityRuntimeState(cwd, runtimeConfigDir, config) as { + const state = resolveCapabilityRuntimeState(cwd, runtimeConfigDir, config, runtimeOverride) as { warnings?: string[]; capabilities: Array<{ id: string; enabled?: boolean; active: boolean }>; }; diff --git a/src/model-resolver.cts b/src/model-resolver.cts index 2fea5d5e1..dc1bc31f1 100644 --- a/src/model-resolver.cts +++ b/src/model-resolver.cts @@ -105,6 +105,66 @@ function _resetModelPolicyWarningCacheForTests(): void { _modelPolicyUnmappableWarned.clear(); } +// Dedupe stderr warnings for unmappable model_overrides Claude IDs (#2041). +const _modelOverrideUnmappableWarned = new Set(); +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 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 | 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 | 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 | 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, diff --git a/src/onboard-projection.cts b/src/onboard-projection.cts new file mode 100644 index 000000000..bcd3b794b --- /dev/null +++ b/src/onboard-projection.cts @@ -0,0 +1,437 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports -- core-utils.cjs is an export= CommonJS module +import coreUtils = require('./core-utils.cjs'); +// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module +import planningWorkspace = require('./planning-workspace.cjs'); +import { formatGsdSlash, resolveRuntime } from './runtime-slash.cjs'; + +const { pathExistsInternal, toPosixPath } = coreUtils; +const { planningDir, planningRoot } = planningWorkspace; + +const CODE_EXTENSIONS = new Set([ + '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.swift', '.java', + '.kt', '.kts', '.c', '.cpp', '.cc', '.h', '.hpp', '.cs', '.rb', '.php', '.dart', + '.m', '.mm', '.scala', '.groovy', '.lua', '.r', '.R', '.zig', '.ex', '.exs', '.clj', +]); + +const CODE_SCAN_SKIP_DIRS = new Set([ + 'node_modules', '.git', '.planning', '.claude', '.codex', '__pycache__', 'target', + 'dist', 'build', '.next', '.nuxt', '.svelte-kit', 'coverage', 'vendor', '.venv', 'venv', +]); + +const PACKAGE_FILES = [ + 'package.json', 'requirements.txt', 'pyproject.toml', 'Cargo.toml', 'go.mod', + 'Package.swift', 'build.gradle', 'build.gradle.kts', 'pom.xml', 'Gemfile', + 'composer.json', 'pubspec.yaml', 'CMakeLists.txt', 'Makefile', 'build.zig', + 'mix.exs', 'project.clj', +]; + +const REQUIRED_CODEBASE_MAP_FILES = [ + 'STACK.md', 'ARCHITECTURE.md', 'STRUCTURE.md', 'CONVENTIONS.md', 'TESTING.md', + 'INTEGRATIONS.md', 'CONCERNS.md', +]; + +const FAST_CODEBASE_MAP_FILES = [ + 'STACK.md', 'INTEGRATIONS.md', 'ARCHITECTURE.md', 'STRUCTURE.md', +]; + +const PLANNING_DOC_SEGMENTS = new Set([ + 'adr', 'adrs', 'prd', 'prds', 'spec', 'specs', 'rfc', 'rfcs', +]); + +type MapReadiness = 'none' | 'fast' | 'complete'; + +interface OnboardHandoffCommands { + ingest_docs: string; + manager: string; + map_codebase: string; + map_codebase_fast: string; + new_project: string; + onboard: string; +} + +type OnboardNextAction = + | { kind: 'map-codebase'; command: string; reason: string } + | { kind: 'ingest-docs'; command: string; reason: string } + | { kind: 'new-project'; command: string; reason: string } + | { kind: 'complete-map-before-new-project'; command: string; reason: string } + | { kind: 'partial-planning'; missing: string[]; reason: string } + | { kind: 'write-summary'; summary_path: string; reason: string } + | { kind: 'ready'; reason: string }; + +interface BuildOnboardProjectionOptions { + commitDocs: boolean; + fast: boolean; + textMode: boolean; +} + +interface OnboardProjection { + commit_docs: boolean; + text_mode: boolean; + + project_exists: boolean; + planning_exists: boolean; + requirements_exists: boolean; + roadmap_exists: boolean; + state_exists: boolean; + config_exists: boolean; + + handoff_commands: OnboardHandoffCommands; + + has_existing_code: boolean; + has_package_file: boolean; + is_brownfield: boolean; + fast_mode: boolean; + map_readiness: MapReadiness; + next_action: OnboardNextAction; + needs_codebase_map: boolean; + needs_fast_codebase_map: boolean; + has_codebase_map: boolean; + has_fast_codebase_map: boolean; + codebase_dir_exists: boolean; + fast_codebase_map_files_required: string[]; + codebase_map_files_present: string[]; + missing_codebase_map_files: string[]; + missing_fast_codebase_map_files: string[]; + codebase_map_summary_status: string; + codebase_map_final_status: string; + + has_docs_candidates: boolean; + doc_candidate_count: number; + doc_candidates: string[]; + + onboarding_summary_exists: boolean; + onboarding_summary_path: string; + + project_path: string; + requirements_path: string; + roadmap_path: string; + state_path: string; + codebase_dir: string; + onboarding_dir: string; +} + +function hasCodeFilesInternal(dir: string, depth = 0): boolean { + if (depth > 3) return false; + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + return false; + } + + for (const entry of entries) { + if (entry.isFile() && CODE_EXTENSIONS.has(path.extname(entry.name))) return true; + if (entry.isDirectory() && !CODE_SCAN_SKIP_DIRS.has(entry.name)) { + if (hasCodeFilesInternal(path.join(dir, entry.name), depth + 1)) return true; + } + } + + return false; +} + +function hasPackageFileInternal(cwd: string): boolean { + return PACKAGE_FILES.some((file) => pathExistsInternal(cwd, file)); +} + +function listPlanningDocCandidates(cwd: string): string[] { + const roots = ['docs', 'adr', 'adrs', 'prd', 'prds', 'spec', 'specs', 'rfc', 'rfcs']; + const candidates = new Set(); + + function isPlanningDocCandidate(rel: string, name: string): boolean { + const upperName = name.toUpperCase(); + const relLower = rel.toLowerCase(); + const pathSegments = relLower.split('/'); + return ( + /(^|[-_ ])(ADR|PRD|SPEC|RFC)([-_ ]|\.)/i.test(name) || + /^\d{4}[-_].+\.md$/i.test(name) || + pathSegments.some((segment) => PLANNING_DOC_SEGMENTS.has(segment)) || + upperName === 'REQUIREMENTS.MD' + ); + } + + function addCandidate(rel: string, name: string): void { + if (name.toLowerCase().endsWith('.md') && isPlanningDocCandidate(rel, name)) { + candidates.add(toPosixPath(rel)); + } + } + + function visit(dir: string, relDir: string, depth: number): void { + if (depth > 3) return; + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + + for (const entry of entries) { + const rel = relDir ? `${relDir}/${entry.name}` : entry.name; + if (entry.isDirectory()) { + if (!CODE_SCAN_SKIP_DIRS.has(entry.name)) { + visit(path.join(dir, entry.name), rel, depth + 1); + } + continue; + } + + if (entry.isFile()) addCandidate(rel, entry.name); + } + } + + let rootEntries: fs.Dirent[] = []; + try { + rootEntries = fs.readdirSync(cwd, { withFileTypes: true }); + } catch { + rootEntries = []; + } + + for (const entry of rootEntries) { + if (entry.isFile()) addCandidate(entry.name, entry.name); + } + + for (const root of roots) { + const full = path.join(cwd, root); + if (fs.existsSync(full)) visit(full, root, 0); + } + + return [...candidates].sort(); +} + +function listCodebaseMapFiles(cwd: string): string[] { + const codebaseDir = path.join(planningRoot(cwd), 'codebase'); + if (!fs.existsSync(codebaseDir)) return []; + return REQUIRED_CODEBASE_MAP_FILES.filter((file) => + fs.existsSync(path.join(codebaseDir, file)), + ); +} + +function getMapReadiness(hasCompleteMap: boolean, hasFastMap: boolean): MapReadiness { + if (hasCompleteMap) return 'complete'; + if (hasFastMap) return 'fast'; + return 'none'; +} + +function mapSummaryStatus(mapReadiness: MapReadiness): string { + if (mapReadiness === 'complete') return '.planning/codebase/ (complete codebase map)'; + if (mapReadiness === 'fast') return '.planning/codebase/ (fast/partial codebase map; complete map still required for project setup)'; + return 'missing'; +} + +function mapFinalStatus(mapReadiness: MapReadiness): string { + if (mapReadiness === 'complete') return 'complete'; + if (mapReadiness === 'fast') return 'fast/partial; complete map still required for project setup'; + return 'missing'; +} + +function planningMissing( + projectExists: boolean, + requirementsExists: boolean, + roadmapExists: boolean, + stateExists: boolean, +): string[] { + const missing: string[] = []; + if (!projectExists) missing.push('PROJECT.md'); + if (!requirementsExists) missing.push('REQUIREMENTS.md'); + if (!roadmapExists) missing.push('ROADMAP.md'); + if (!stateExists) missing.push('STATE.md'); + return missing; +} + +function nextAction(params: { + fastMode: boolean; + isBrownfield: boolean; + needsOnboardCodebaseMap: boolean; + hasDocsCandidates: boolean; + projectExists: boolean; + mapReadiness: MapReadiness; + onboardingSummaryExists: boolean; + onboardingSummaryPath: string; + hasPlanningArtifacts: boolean; + missingPlanningFiles: string[]; + handoffCommands: OnboardHandoffCommands; +}): OnboardNextAction { + if (params.isBrownfield && params.needsOnboardCodebaseMap) { + return { + kind: 'map-codebase', + command: params.fastMode ? params.handoffCommands.map_codebase_fast : params.handoffCommands.map_codebase, + reason: 'Existing code was detected, but the required .planning/codebase/ map is missing.', + }; + } + + if (params.hasPlanningArtifacts && params.missingPlanningFiles.length > 0) { + return { + kind: 'partial-planning', + missing: params.missingPlanningFiles, + reason: 'Project planning exists but required planning files are missing.', + }; + } + + if (params.fastMode && params.mapReadiness === 'fast' && !params.projectExists) { + return { + kind: 'complete-map-before-new-project', + command: params.handoffCommands.map_codebase, + reason: 'The fast map is enough for lightweight onboarding, but project setup still requires the complete codebase map.', + }; + } + + if (params.hasDocsCandidates && !params.projectExists) { + return { + kind: 'ingest-docs', + command: params.handoffCommands.ingest_docs, + reason: 'Detected existing ADR/PRD/SPEC/RFC document(s) before project setup.', + }; + } + + if (!params.isBrownfield && !params.projectExists && !params.hasDocsCandidates) { + return { + kind: 'new-project', + command: params.handoffCommands.new_project, + reason: 'No existing code or planning docs were detected.', + }; + } + + if (!params.projectExists) { + return { + kind: 'new-project', + command: params.handoffCommands.new_project, + reason: 'Codebase context is ready for project initialization.', + }; + } + + if (!params.onboardingSummaryExists) { + return { + kind: 'write-summary', + summary_path: params.onboardingSummaryPath, + reason: 'Onboarding summary is missing.', + }; + } + + return { + kind: 'ready', + reason: 'Onboarding summary already exists.', + }; +} + +function buildHandoffCommands(cwd: string): OnboardHandoffCommands { + const runtime = resolveRuntime(cwd); + return { + ingest_docs: formatGsdSlash('ingest-docs', runtime) as string, + manager: formatGsdSlash('manager', runtime) as string, + map_codebase: formatGsdSlash('map-codebase', runtime) as string, + map_codebase_fast: formatGsdSlash('map-codebase --fast', runtime) as string, + new_project: formatGsdSlash('new-project', runtime) as string, + onboard: formatGsdSlash('onboard', runtime) as string, + }; +} + +function buildOnboardProjection(cwd: string, options: BuildOnboardProjectionOptions): OnboardProjection { + const handoffCommands = buildHandoffCommands(cwd); + const codebaseMapFiles = listCodebaseMapFiles(cwd); + const missingCodebaseMapFiles = REQUIRED_CODEBASE_MAP_FILES.filter( + (file) => !codebaseMapFiles.includes(file), + ); + const missingFastCodebaseMapFiles = FAST_CODEBASE_MAP_FILES.filter( + (file) => !codebaseMapFiles.includes(file), + ); + const docCandidates = listPlanningDocCandidates(cwd); + const hasCode = hasCodeFilesInternal(cwd); + const hasPackageFile = hasPackageFileInternal(cwd); + const isBrownfield = hasCode || hasPackageFile; + const hasCodebaseMap = codebaseMapFiles.length === REQUIRED_CODEBASE_MAP_FILES.length; + const hasFastCodebaseMap = missingFastCodebaseMapFiles.length === 0; + const mapReadinessValue = getMapReadiness(hasCodebaseMap, hasFastCodebaseMap); + const needsCodebaseMap = isBrownfield && !hasCodebaseMap; + const needsFastCodebaseMap = isBrownfield && !hasFastCodebaseMap; + const needsOnboardCodebaseMap = options.fast ? needsFastCodebaseMap : needsCodebaseMap; + const projectRootPath = path.join(planningRoot(cwd), 'PROJECT.md'); + const projectScopedPath = path.join(planningDir(cwd), 'PROJECT.md'); + const projectExists = + fs.existsSync(projectRootPath) || fs.existsSync(projectScopedPath); + const requirementsExists = fs.existsSync(path.join(planningDir(cwd), 'REQUIREMENTS.md')); + const roadmapExists = fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')); + const stateExists = fs.existsSync(path.join(planningDir(cwd), 'STATE.md')); + const onboardingSummaryPath = path.join(planningRoot(cwd), 'onboarding', 'SUMMARY.md'); + const onboardingSummaryExists = fs.existsSync(onboardingSummaryPath); + const hasPlanningArtifacts = projectExists || requirementsExists || roadmapExists || stateExists; + const missingPlanningFiles = planningMissing( + projectExists, + requirementsExists, + roadmapExists, + stateExists, + ); + + return { + commit_docs: options.commitDocs, + text_mode: options.textMode, + + project_exists: projectExists, + planning_exists: fs.existsSync(planningRoot(cwd)), + requirements_exists: requirementsExists, + roadmap_exists: roadmapExists, + state_exists: stateExists, + config_exists: fs.existsSync(path.join(planningDir(cwd), 'config.json')), + + handoff_commands: handoffCommands, + + has_existing_code: hasCode, + has_package_file: hasPackageFile, + is_brownfield: isBrownfield, + fast_mode: options.fast, + map_readiness: mapReadinessValue, + next_action: nextAction({ + fastMode: options.fast, + isBrownfield, + needsOnboardCodebaseMap, + hasDocsCandidates: docCandidates.length > 0, + projectExists, + mapReadiness: mapReadinessValue, + onboardingSummaryExists, + onboardingSummaryPath: toPosixPath(path.relative(cwd, onboardingSummaryPath)), + hasPlanningArtifacts, + missingPlanningFiles, + handoffCommands, + }), + needs_codebase_map: needsCodebaseMap, + needs_fast_codebase_map: needsFastCodebaseMap, + has_codebase_map: hasCodebaseMap, + has_fast_codebase_map: hasFastCodebaseMap, + codebase_dir_exists: fs.existsSync(path.join(planningRoot(cwd), 'codebase')), + fast_codebase_map_files_required: FAST_CODEBASE_MAP_FILES, + codebase_map_files_present: codebaseMapFiles, + missing_codebase_map_files: missingCodebaseMapFiles, + missing_fast_codebase_map_files: missingFastCodebaseMapFiles, + codebase_map_summary_status: mapSummaryStatus(mapReadinessValue), + codebase_map_final_status: mapFinalStatus(mapReadinessValue), + + has_docs_candidates: docCandidates.length > 0, + doc_candidate_count: docCandidates.length, + doc_candidates: docCandidates, + + onboarding_summary_exists: onboardingSummaryExists, + onboarding_summary_path: toPosixPath(path.relative(cwd, onboardingSummaryPath)), + + project_path: toPosixPath(path.relative( + cwd, + fs.existsSync(projectRootPath) ? projectRootPath : projectScopedPath, + )), + requirements_path: toPosixPath( + path.relative(cwd, path.join(planningDir(cwd), 'REQUIREMENTS.md')), + ), + roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))), + state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))), + codebase_dir: toPosixPath(path.relative(cwd, path.join(planningRoot(cwd), 'codebase'))), + onboarding_dir: toPosixPath(path.relative(cwd, path.join(planningRoot(cwd), 'onboarding'))), + }; +} + +export = { + REQUIRED_CODEBASE_MAP_FILES, + FAST_CODEBASE_MAP_FILES, + buildOnboardProjection, + hasCodeFilesInternal, + hasPackageFileInternal, + listCodebaseMapFiles, + listPlanningDocCandidates, +}; diff --git a/tests/capability-state.test.cjs b/tests/capability-state.test.cjs index 66bba4e9c..4ab47fecc 100644 --- a/tests/capability-state.test.cjs +++ b/tests/capability-state.test.cjs @@ -15,13 +15,15 @@ const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); -const { cleanup } = require('./helpers.cjs'); +const { cleanup, runGsdTools } = require('./helpers.cjs'); const { resolveCapabilityState, + resolveCapabilityRuntimeState, isCapabilityActive, _isSafePropKey, _loadInstalledSkillsManifest, + _loadFlatCommandsGsdManifest, _resolveManifest, } = require('../gsd-core/bin/lib/capability-state.cjs'); @@ -823,6 +825,223 @@ describe('cmdCapabilityState — end-to-end via gsd-tools CLI', () => { // FAIL before the fix and PASS after, regardless of whether commands/gsd // happens to exist in the current checkout. +describe('regressions: flat commands/gsd-.md layout (#1858)', () => { + // Flat command layout (Claude local project install shape): skills live at + // /commands/gsd-.md — the gsd- prefix is baked into the filename + // and there is NO commands/gsd/ subdir. _resolveManifest must detect this + // layout, strip the gsd- prefix, and produce the same stems the nested + // loader (commands/gsd/.md) would, or every skill-bearing capability + // is silently reported surfaced:false / enabled:false / active:false. + function makeFlatCommandMd(stem, requires) { + const req = requires ? `requires: [${requires.join(', ')}]` : 'requires: [phase]'; + return [ + '---', + `name: gsd:${stem}`, + `description: ${stem} skill`, + 'argument-hint: "[phase number]"', + 'allowed-tools:', + ' - Read', + req, + '---', + 'Execute end-to-end.', + ].join('\n') + '\n'; + } + + // ── Unit tests for _loadFlatCommandsGsdManifest ───────────────────────────── + + test('_loadFlatCommandsGsdManifest: returns empty map when parent dir absent', () => { + const missing = path.join(os.tmpdir(), 'cap-flat-missing-' + Date.now()); + const manifest = _loadFlatCommandsGsdManifest(missing); + assert.ok(manifest instanceof Map, 'should return a Map'); + assert.strictEqual(manifest.size, 0, 'should be empty when parent dir absent'); + }); + + test('_loadFlatCommandsGsdManifest: scans gsd-.md and strips the gsd- prefix', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-flat-scan-')); + try { + fs.writeFileSync(path.join(tmpDir, 'gsd-validate-phase.md'), makeFlatCommandMd('validate-phase'), 'utf8'); + fs.writeFileSync(path.join(tmpDir, 'gsd-secure-phase.md'), makeFlatCommandMd('secure-phase'), 'utf8'); + // Non-gsd file must be ignored + fs.writeFileSync(path.join(tmpDir, 'random-doc.md'), '# not a skill\n', 'utf8'); + // Non-markdown gsd file must be ignored + fs.writeFileSync(path.join(tmpDir, 'gsd-notskill.txt'), 'nope\n', 'utf8'); + + const manifest = _loadFlatCommandsGsdManifest(tmpDir); + assert.ok(manifest.has('validate-phase'), 'flat gsd-validate-phase.md -> stem validate-phase'); + assert.ok(manifest.has('secure-phase'), 'flat gsd-secure-phase.md -> stem secure-phase'); + assert.ok(!manifest.has('gsd-validate-phase'), 'must NOT keep the gsd- prefix on the stem'); + assert.ok(!manifest.has('random-doc'), 'non-gsd file must be ignored'); + assert.ok(!manifest.has('notskill'), 'non-.md gsd file must be ignored'); + } finally { + cleanup(tmpDir); + } + }); + + test('_loadFlatCommandsGsdManifest: parses requires via shared parseRequires (no drift)', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-flat-req-')); + try { + fs.writeFileSync( + path.join(tmpDir, 'gsd-my-skill.md'), + makeFlatCommandMd('my-skill', ['dep-a', 'dep-b']), + 'utf8', + ); + const manifest = _loadFlatCommandsGsdManifest(tmpDir); + assert.deepStrictEqual(manifest.get('my-skill'), ['dep-a', 'dep-b']); + } finally { + cleanup(tmpDir); + } + }); + + test('_loadFlatCommandsGsdManifest: companion _calls_agents_ key present (parity with nested loader)', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-flat-agents-')); + try { + fs.writeFileSync(path.join(tmpDir, 'gsd-validate-phase.md'), makeFlatCommandMd('validate-phase'), 'utf8'); + const manifest = _loadFlatCommandsGsdManifest(tmpDir); + assert.ok(manifest.has('_calls_agents_validate-phase'), + 'flat loader must emit the companion _calls_agents_ key (same Map shape as loadSkillsManifest)'); + } finally { + cleanup(tmpDir); + } + }); + + // Low-1 (review): boundary — empty stem (gsd-.md) skipped, single-char stem kept. + test('_loadFlatCommandsGsdManifest: skips gsd-.md (empty stem) and keeps single-char stem (slice boundary)', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-flat-edge-')); + try { + fs.writeFileSync(path.join(tmpDir, 'gsd-.md'), makeFlatCommandMd(''), 'utf8'); + fs.writeFileSync(path.join(tmpDir, 'gsd-x.md'), makeFlatCommandMd('x'), 'utf8'); + const manifest = _loadFlatCommandsGsdManifest(tmpDir); + assert.ok(!manifest.has(''), 'gsd-.md must NOT register an empty-string stem'); + assert.ok(!manifest.has('_calls_agents_'), 'no companion key for an empty stem'); + assert.ok(manifest.has('x'), 'gsd-x.md -> single-char stem "x" (slice(4,-3) boundary)'); + } finally { + cleanup(tmpDir); + } + }); + + // Low-2 (review): unreadable file degrades both keys to [] (parity with nested + // loader's catch). POSIX-only AND must not run as root — root bypasses POSIX + // read permission bits, so chmod 0o000 would NOT make the file unreadable and + // the test would assert [] against the real parsed deps (false failure). + // Skip on win32 (DEFECT.WINDOWS-POSIX-MODE-BIT-ASSERT) and when getuid()==0. + const _skipUnreadable = process.platform === 'win32' || (typeof process.getuid === 'function' && process.getuid() === 0); + test('_loadFlatCommandsGsdManifest: unreadable file degrades to empty deps + agents (parity)', { skip: _skipUnreadable }, () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-flat-unread-')); + let madeUnreadable = false; + try { + const skillPath = path.join(tmpDir, 'gsd-secure.md'); + fs.writeFileSync(skillPath, '---\nname: gsd:secure\nrequires: [phase]\n---\nbody\n', { mode: 0o644 }); + fs.chmodSync(skillPath, 0o000); + madeUnreadable = true; + const manifest = _loadFlatCommandsGsdManifest(tmpDir); + assert.deepStrictEqual(manifest.get('secure'), [], 'unreadable file -> empty requires (parity with nested catch)'); + assert.deepStrictEqual(manifest.get('_calls_agents_secure'), [], 'unreadable file -> empty agents (parity with nested catch)'); + } finally { + // Restore writability so cleanup() can rm the tmp tree. + if (madeUnreadable) { + try { fs.chmodSync(path.join(tmpDir, 'gsd-secure.md'), 0o644); } catch { /* best effort */ } + } + cleanup(tmpDir); + } + }); + + // ── _resolveManifest picks the flat branch when nested is absent ──────────── + + test('_resolveManifest: detects flat commands/gsd-.md layout when nested commands/gsd/ is absent (#1858)', () => { + const tmpRepo = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-rm-flat-repo-')); + try { + // Flat source layout: /commands/gsd-.md + // NO commands/gsd/ subdir, NO skills/ dir. + const commandsDir = path.join(tmpRepo, 'commands'); + fs.mkdirSync(commandsDir, { recursive: true }); + fs.writeFileSync(path.join(commandsDir, 'gsd-validate-phase.md'), makeFlatCommandMd('validate-phase'), 'utf8'); + fs.writeFileSync(path.join(commandsDir, 'gsd-secure-phase.md'), makeFlatCommandMd('secure-phase'), 'utf8'); + + // commandsGsdDir = /commands/gsd (nested — does NOT exist). + // dirname(commandsGsdDir) = /commands (where the flat files live). + const commandsGsdDir = path.join(commandsDir, 'gsd'); + // configDir = a separate empty tmp dir (no skills/ → installed fallback empty). + const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-rm-flat-cfg-')); + try { + const manifest = _resolveManifest(commandsGsdDir, configDir); + assert.ok(manifest.has('validate-phase'), + 'flat layout must populate validate-phase stem (was empty pre-fix → all skill caps unsurfaced)'); + assert.ok(manifest.has('secure-phase'), 'flat layout must populate secure-phase stem'); + assert.ok(!manifest.has('gsd-validate-phase'), 'stem must have gsd- prefix stripped'); + } finally { + cleanup(configDir); + } + } finally { + cleanup(tmpRepo); + } + }); + + test('_resolveManifest: flat branch does NOT shadow a populated installed skills dir when no flat files exist', () => { + // Precedence: nested > flat-source > installed. If the flat parent dir has + // NO gsd-*.md files, the flat loader returns an empty Map and _resolveManifest + // must fall through to the installed-skills branch (not return empty). + const tmpRepo = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-rm-precedence-')); + try { + const commandsDir = path.join(tmpRepo, 'commands'); + fs.mkdirSync(commandsDir, { recursive: true }); + // No gsd-*.md files in commands/ — flat loader yields empty. + // Installed skills present under configDir: + const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-rm-precedence-cfg-')); + try { + const secureDir = path.join(configDir, 'skills', 'gsd-secure-phase'); + fs.mkdirSync(secureDir, { recursive: true }); + fs.writeFileSync(path.join(secureDir, 'SKILL.md'), + '---\nname: gsd:secure-phase\nrequires: [phase]\n---\nbody\n', 'utf8'); + + const commandsGsdDir = path.join(commandsDir, 'gsd'); // nested absent + const manifest = _resolveManifest(commandsGsdDir, configDir); + assert.ok(manifest.has('secure-phase'), + 'when flat dir is empty, installed-skills fallback must still work (precedence flat > installed only when flat non-empty)'); + } finally { + cleanup(configDir); + } + } finally { + cleanup(tmpRepo); + } + }); + + // ── Parity: flat loader produces the same stems as the nested loader ──────── + // (DEFECT.GENERATIVE-FIX — guards against silent divergence between the two + // parallel manifest-builders.) + + test('flat loader and nested loader produce identical stems for the same command set (parity)', () => { + const realCommandsGsdDir = path.resolve(__dirname, '..', 'commands', 'gsd'); + if (!fs.existsSync(realCommandsGsdDir)) return; // skip outside a repo checkout + // Build a flat mirror of the real nested commands/gsd/.md as + // commands/gsd-.md in a temp dir, then compare stem sets. + const tmpFlat = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-parity-flat-')); + try { + const nested = require('../gsd-core/bin/lib/install-profiles.cjs').loadSkillsManifest(realCommandsGsdDir); + for (const [stem] of nested) { + if (stem.startsWith('_calls_agents_')) continue; + const src = path.join(realCommandsGsdDir, stem + '.md'); + if (!fs.existsSync(src)) continue; + fs.writeFileSync(path.join(tmpFlat, 'gsd-' + stem + '.md'), fs.readFileSync(src, 'utf8'), 'utf8'); + } + const flat = _loadFlatCommandsGsdManifest(tmpFlat); + const nestedStems = [...nested.keys()].filter((k) => !k.startsWith('_calls_agents_')).sort(); + const flatStems = [...flat.keys()].filter((k) => !k.startsWith('_calls_agents_')).sort(); + assert.deepStrictEqual(flatStems, nestedStems, + 'flat loader stem set must match nested loader stem set for the real command tree'); + // Nit-2 (review): also compare _calls_agents_ VALUES, not just the + // stem set — proves the shared parseCallsAgents output is identical. + for (const stem of flatStems) { + assert.deepStrictEqual(flat.get(`_calls_agents_${stem}`), nested.get(`_calls_agents_${stem}`), + `agent refs for stem "${stem}" must match between flat and nested loaders`); + assert.deepStrictEqual(flat.get(stem), nested.get(stem), + `requires for stem "${stem}" must match between flat and nested loaders`); + } + } finally { + cleanup(tmpFlat); + } + }); +}); + describe('regressions: installed-runtime capability surface (#1160)', () => { // Minimal valid SKILL.md content (frontmatter only — matches what install emits) function makeSkillMd(stem) { @@ -1810,3 +2029,176 @@ describe('#1459 IC-04: capability-state threads gsdHome to the overlay loader', } }); }); + +describe('regressions: --runtime override bypasses persisted runtime (#2003)', () => { + // #2003: `capability state` and `loop render-hooks` parsed only --config-dir, + // never --runtime. resolveCapabilityRuntimeState derived the config dir from + // resolveRuntime(cwd) (GSD_RUNTIME → config.runtime → 'claude'), so a repo + // with persisted runtime:"codex" resolved the config dir to ~/.codex — where + // the Claude skill isn't installed → surfaced:false. Fix: thread an explicit + // --runtime override through both commands into resolveCapabilityRuntimeState + // so it bypasses the persisted-runtime fallback (mirrors the update-context / + // effort sync precedent). + + function writePersistedRuntime(tmpDir, runtime) { + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true }); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'config.json'), + JSON.stringify({ runtime }), + 'utf8', + ); + } + + test('resolveCapabilityRuntimeState: runtimeOverride="claude" bypasses persisted config.runtime:"codex"', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-rt-override-')); + try { + writePersistedRuntime(tmpDir, 'codex'); + const runtimeHomes = require('../gsd-core/bin/lib/runtime-homes.cjs'); + const expectedClaudeDir = runtimeHomes.getGlobalConfigDir('claude'); + const expectedCodexDir = runtimeHomes.getGlobalConfigDir('codex'); + // Persisted runtime is codex; explicit override is claude. The override + // MUST win (resolveRuntime is never consulted when an override is given, + // so GSD_RUNTIME env cannot interfere either). + const result = resolveCapabilityRuntimeState(tmpDir, undefined, undefined, 'claude'); + assert.strictEqual(result.runtimeConfigDir, expectedClaudeDir, + '--runtime claude must override persisted runtime:"codex"'); + assert.notStrictEqual(result.runtimeConfigDir, expectedCodexDir, + 'must NOT resolve to the codex config dir when --runtime claude is explicit'); + } finally { + cleanup(tmpDir); + } + }); + + test('resolveCapabilityRuntimeState: no override still honours persisted config.runtime (unchanged, regression guard)', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-rt-no-override-')); + // Control GSD_RUNTIME so resolveRuntime deterministically reads config.runtime. + const savedGsdRuntime = process.env.GSD_RUNTIME; + delete process.env.GSD_RUNTIME; + try { + writePersistedRuntime(tmpDir, 'codex'); + const runtimeHomes = require('../gsd-core/bin/lib/runtime-homes.cjs'); + const expectedCodexDir = runtimeHomes.getGlobalConfigDir('codex'); + // No override → persisted codex wins (existing behavior preserved). + const result = resolveCapabilityRuntimeState(tmpDir, undefined, undefined, undefined); + assert.strictEqual(result.runtimeConfigDir, expectedCodexDir, + 'without --runtime, persisted config.runtime:"codex" still drives resolution (unchanged)'); + } finally { + if (savedGsdRuntime === undefined) delete process.env.GSD_RUNTIME; + else process.env.GSD_RUNTIME = savedGsdRuntime; + cleanup(tmpDir); + } + }); + + test('resolveCapabilityRuntimeState: runtimeOverride canonicalizes aliases (codex-app -> codex)', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-rt-alias-')); + try { + writePersistedRuntime(tmpDir, 'claude'); + const runtimeHomes = require('../gsd-core/bin/lib/runtime-homes.cjs'); + const expectedCodexDir = runtimeHomes.getGlobalConfigDir('codex'); + // Alias "codex-app" canonicalizes to "codex" via runtime-name-policy. + const result = resolveCapabilityRuntimeState(tmpDir, undefined, undefined, 'codex-app'); + assert.strictEqual(result.runtimeConfigDir, expectedCodexDir, + '--runtime codex-app (alias) must canonicalize to codex'); + } finally { + cleanup(tmpDir); + } + }); + + test('CLI: `capability state --runtime claude` reports the Claude config dir despite persisted runtime:"codex"', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-rt-cli-')); + try { + writePersistedRuntime(tmpDir, 'codex'); + const result = runGsdTools('capability state --runtime claude --raw', tmpDir); + assert.ok(result.success, `capability state --runtime should succeed: ${result.error || ''}`); + const parsed = JSON.parse(result.output); + const runtimeHomes = require('../gsd-core/bin/lib/runtime-homes.cjs'); + assert.strictEqual(parsed.runtimeConfigDir, runtimeHomes.getGlobalConfigDir('claude'), + '`capability state --runtime claude` must resolve to the Claude config dir, not the persisted codex dir'); + } finally { + cleanup(tmpDir); + } + }); + + // L-2 (review): end-to-end CLI test for loop render-hooks --runtime — the exact + // command the bug report calls out as silently no-op'ing. Guards the copy-pasted + // arg parsing in gsd-tools.cjs from diverging from the capability-state branch. + test('CLI: `loop render-hooks verify:post --runtime claude` resolves against the Claude config dir', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-rt-loop-cli-')); + try { + writePersistedRuntime(tmpDir, 'codex'); + const result = runGsdTools('loop render-hooks verify:post --runtime claude --raw', tmpDir); + assert.ok(result.success, `loop render-hooks --runtime should succeed: ${result.error || ''}`); + const parsed = JSON.parse(result.output); + // The envelope carries activeHooks/rendered; the key assertion is that it + // ran without silently no-op'ing against the codex dir. A non-empty + // point + a rendered string (even "_No active hooks..._") proves the + // command executed against the resolved (claude) config dir rather than + // erroring or emitting nothing. + assert.strictEqual(parsed.point, 'verify:post', 'render-hooks must echo the requested point'); + assert.ok(typeof parsed.rendered === 'string', 'render-hooks must produce a rendered string'); + } finally { + cleanup(tmpDir); + } + }); + + // M-1 + NIT-01 (review): unknown / crafted --runtime values are REJECTED by the + // closed-vocabulary canonicalizer, warn, and fall through to the persisted + // runtime (never embedded into a path). This is the security-load-bearing + // contract — pin it so a future refactor can't silently break it. + test('resolveCapabilityRuntimeState: unknown/crafted --runtime is rejected (closed vocabulary), warns, and falls through to persisted runtime', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-rt-reject-')); + const savedGsdRuntime = process.env.GSD_RUNTIME; + delete process.env.GSD_RUNTIME; + try { + writePersistedRuntime(tmpDir, 'codex'); + const runtimeHomes = require('../gsd-core/bin/lib/runtime-homes.cjs'); + const expectedCodexDir = runtimeHomes.getGlobalConfigDir('codex'); + const crafted = ['../../etc/passwd', '__proto__', 'constructor', '--config-dir', 'garbage', 'foo bar', 'cluade']; + for (const bad of crafted) { + const result = resolveCapabilityRuntimeState(tmpDir, undefined, undefined, bad); + assert.strictEqual(result.runtimeConfigDir, expectedCodexDir, + `crafted --runtime "${bad}" must fall through to persisted codex dir, not be embedded in a path`); + assert.ok(result.warnings.some((w) => w.includes('--runtime') && w.includes(bad)), + `crafted --runtime "${bad}" must emit a warning naming the rejected value`); + } + } finally { + if (savedGsdRuntime === undefined) delete process.env.GSD_RUNTIME; + else process.env.GSD_RUNTIME = savedGsdRuntime; + cleanup(tmpDir); + } + }); + + // N-1 (review): CLI arg-parsing boundary — --config-dir wins over --runtime + // (most-explicit input takes precedence), and missing --runtime value errors. + test('CLI: --config-dir takes precedence over --runtime when both are given', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-rt-precedence-')); + const explicitDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-rt-explicit-cfg-')); + try { + writePersistedRuntime(tmpDir, 'codex'); + const result = runGsdTools( + `capability state --config-dir ${explicitDir} --runtime claude --raw`, + tmpDir, + ); + assert.ok(result.success, `capability state with both flags should succeed: ${result.error || ''}`); + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.runtimeConfigDir, explicitDir, + '--config-dir (explicit path) must win over --runtime when both are present'); + } finally { + cleanup(explicitDir); + cleanup(tmpDir); + } + }); + + test('CLI: `capability state --runtime` with no value errors with USAGE', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-rt-missing-')); + try { + writePersistedRuntime(tmpDir, 'codex'); + const result = runGsdTools('capability state --runtime', tmpDir); + assert.ok(!result.success, 'missing --runtime value must produce a non-zero exit'); + assert.ok(/Missing value for --runtime/.test(result.error || ''), + `error must name the missing --runtime value: ${result.error || ''}`); + } finally { + cleanup(tmpDir); + } + }); +}); diff --git a/tests/check-gap-analysis-plan-post-e2e.test.cjs b/tests/check-gap-analysis-plan-post-e2e.test.cjs index 4a4a7930a..62b4bf87a 100644 --- a/tests/check-gap-analysis-plan-post-e2e.test.cjs +++ b/tests/check-gap-analysis-plan-post-e2e.test.cjs @@ -493,7 +493,7 @@ describe('resolveLoopHooks plan:post — pure function against real registry', ( assert.strictEqual(result.activeHooks[0].capId, 'gap-analysis'); }); - test('[happy] real registry byLoopPoint plan:post has 1 step (mempalace), 1 contribution (external-job planner fragment), and 1 gate (gap-analysis)', () => { + test('[happy] real registry byLoopPoint plan:post has 1 step (mempalace), 2 contributions (external-job planner + claude-orchestration ultraplan ownership), and 1 gate (gap-analysis)', () => { const entry = realRegistry.byLoopPoint['plan:post']; assert.ok(entry, 'plan:post must exist in byLoopPoint'); assert.ok(Array.isArray(entry.steps), 'steps must be an array'); @@ -501,8 +501,12 @@ describe('resolveLoopHooks plan:post — pure function against real registry', ( assert.ok(Array.isArray(entry.gates), 'gates must be an array'); assert.strictEqual(entry.steps.length, 1, 'plan:post must have 1 step (mempalace capture)'); assert.strictEqual(entry.steps[0].capId, 'mempalace', 'plan:post step must be from mempalace'); - assert.strictEqual(entry.contributions.length, 1, 'plan:post must have 1 contribution (external-job planner runtime-budget fragment)'); - assert.strictEqual(entry.contributions[0].capId, 'external-job', 'plan:post contribution must be from external-job'); + // #1143: claude-orchestration registers a plan:post contribution declaring + // ultraplan plan-offload ownership under its runtime gate (default-off). + assert.strictEqual(entry.contributions.length, 2, 'plan:post must have 2 contributions (external-job planner + claude-orchestration ultraplan ownership)'); + const capIds = entry.contributions.map(c => c.capId).sort(); + assert.deepStrictEqual(capIds, ['claude-orchestration', 'external-job'], + `plan:post contributions must be external-job + claude-orchestration; got ${capIds.join(',')}`); assert.strictEqual(entry.gates.length, 1, 'plan:post must have exactly one gate'); assert.strictEqual(entry.gates[0].capId, 'gap-analysis'); }); diff --git a/tests/claude-orchestration-command-router.test.cjs b/tests/claude-orchestration-command-router.test.cjs new file mode 100644 index 000000000..e0a408f1b --- /dev/null +++ b/tests/claude-orchestration-command-router.test.cjs @@ -0,0 +1,169 @@ +'use strict'; + +/** + * claude-orchestration-command-router.test.cjs — end-to-end tests for the + * `gsd-tools claude-orchestration` command surface (#1143). + * + * Exercises the full dispatch path: gsd-tools → dispatchCapabilityCommand → + * routeClaudeOrchestrationCommand → the pure detect/emitter functions. + * + * NOTE: runGsdTools() returns { success, output, exitCode, error } — it does + * NOT throw on a non-zero exit (helpers.cjs). Tests assert `.success` and parse + * `.output`, and check `.error` on the failure path. + */ + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); + +// ─── Fixtures ───────────────────────────────────────────────────────────────── + +const WAVES_MANIFEST = { + 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'] }, + ], + }, + ], +}; + +function writeManifest(tmpDir) { + const manifestPath = path.join(tmpDir, 'waves.json'); + fs.writeFileSync(manifestPath, JSON.stringify(WAVES_MANIFEST), 'utf8'); + return manifestPath; +} + +/** Run the command and assert it succeeded, returning the parsed JSON output. */ +function runAndParse(args, cwd) { + const res = runGsdTools(args, cwd); + assert.strictEqual(res.success, true, 'command should succeed; stderr: ' + (res.error || '')); + assert.ok(res.output.length > 0, 'command should emit output'); + return JSON.parse(res.output); +} + +// ─── emit-workflow ──────────────────────────────────────────────────────────── + +describe('claude-orchestration emit-workflow (CLI)', () => { + test('emits a Workflow script with parallel barriers, gsd-executor + worktree, and resumeFromRunId', () => { + const tmp = createTempProject('claw-emit-'); + try { + const manifestPath = writeManifest(tmp); + const parsed = runAndParse([ + 'claude-orchestration', 'emit-workflow', + '--waves', manifestPath, + '--run-id', 'run-cli-1143', + '--phase-dir', '.planning/phases/01-foo', + ], tmp); + assert.ok(typeof parsed.script === 'string' && parsed.script.length > 0); + assert.ok(parsed.script.includes('parallel('), 'parallel() barrier emitted'); + assert.ok(parsed.script.includes('gsd-executor'), 'gsd-executor agentType'); + assert.ok(parsed.script.includes('worktree'), 'worktree isolation'); + assert.ok(parsed.script.includes('resumeFromRunId'), 'resumeFromRunId wired'); + assert.ok(parsed.script.includes('run-cli-1143'), 'carries the run id'); + assert.strictEqual(parsed.summary.resumeRunId, 'run-cli-1143'); + assert.strictEqual(parsed.summary.waves, 1); + assert.strictEqual(parsed.summary.plans, 2); + } finally { + cleanup(tmp); + } + }); + + test('budget flag threads a shared token pool into the script', () => { + const tmp = createTempProject('claw-budget-'); + try { + const manifestPath = writeManifest(tmp); + const parsed = runAndParse([ + 'claude-orchestration', 'emit-workflow', + '--waves', manifestPath, + '--run-id', 'r', + '--budget', '750000', + ], tmp); + assert.ok(parsed.script.includes('budget('), 'budget() pool emitted'); + assert.ok(parsed.script.includes('750000')); + } finally { + cleanup(tmp); + } + }); + + test('missing --waves -> non-zero exit with a diagnostic', () => { + const tmp = createTempProject('claw-noargs-'); + try { + const res = runGsdTools([ + 'claude-orchestration', 'emit-workflow', '--run-id', 'r', + ], tmp); + assert.strictEqual(res.success, false, 'missing --waves must fail'); + assert.ok(res.exitCode !== 0, 'non-zero exit'); + assert.match(res.error || '', /--waves/); + } finally { + cleanup(tmp); + } + }); +}); + +// ─── detect-backend ─────────────────────────────────────────────────────────── + +describe('claude-orchestration detect-backend (CLI)', () => { + test('default config (capability off) -> inline, even on Claude', () => { + const tmp = createTempProject('claw-detect-'); + try { + const parsed = runAndParse([ + 'claude-orchestration', 'detect-backend', + '--runtime', 'claude', + '--agent-sdk-version', '1.0.0', + ], tmp); + assert.strictEqual(parsed.backend, 'inline'); + assert.strictEqual(parsed.available, false); + assert.match(parsed.reason, /disabled/); + } finally { + cleanup(tmp); + } + }); + + test('enabled + claude + capable + new-enough SDK -> workflow', () => { + const tmp = createTempProject('claw-detect-on-'); + try { + fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(tmp, '.planning', 'config.json'), + JSON.stringify({ claude_orchestration: { enabled: true, execution_backend: 'auto' } }), + 'utf8', + ); + const parsed = runAndParse([ + 'claude-orchestration', 'detect-backend', + '--runtime', 'claude', + '--agent-sdk-version', '1.2.0', + ], tmp); + assert.strictEqual(parsed.backend, 'workflow'); + assert.strictEqual(parsed.available, true); + } finally { + cleanup(tmp); + } + }); + + test('non-Claude runtime -> inline (criterion 6)', () => { + const tmp = createTempProject('claw-detect-nonclaude-'); + try { + fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(tmp, '.planning', 'config.json'), + JSON.stringify({ claude_orchestration: { enabled: true } }), + 'utf8', + ); + const parsed = runAndParse([ + 'claude-orchestration', 'detect-backend', + '--runtime', 'codex', + '--agent-sdk-version', '1.0.0', + ], tmp); + assert.strictEqual(parsed.backend, 'inline'); + assert.match(parsed.reason, /claude/i); + } finally { + cleanup(tmp); + } + }); +}); diff --git a/tests/claude-orchestration.test.cjs b/tests/claude-orchestration.test.cjs new file mode 100644 index 000000000..808501e1e --- /dev/null +++ b/tests/claude-orchestration.test.cjs @@ -0,0 +1,615 @@ +'use strict'; + +/** + * claude-orchestration.test.cjs — Behavioral tests for the Claude orchestration + * capability (#1143): Workflow-tool backend detection, Workflow-script emission, + * capability-declaration validation, registry integration, and inline-fallback parity. + * + * The capability is default-off + BETA + claude-only. On any runtime lacking the + * Workflow tool it must be a byte-identical no-op. These tests encode that contract. + */ + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const fc = require('fast-check'); + +const { + detectWorkflowBackend, + emitWorkflowScript, + WORKFLOW_TOOL_FLOOR_VERSION, + BACKEND_VALUES, + compareSemver, +} = require('../gsd-core/bin/lib/claude-orchestration.cjs'); + +const { + validateCapability, + validateAgainstContract, + loadAndValidate, + buildRegistry, + serializeRegistry, + normalizeLineEndings, + stripGeneratedComment, +} = require('../scripts/gen-capability-registry.cjs'); + +const ROOT = path.resolve(__dirname, '..'); +const CAP_PATH = path.join(ROOT, 'capabilities', 'claude-orchestration', 'capability.json'); +const REGISTRY_PATH = path.join(ROOT, 'gsd-core', 'bin', 'lib', 'capability-registry.cjs'); + +// ─── Fixtures ───────────────────────────────────────────────────────────────── + +/** A host-integration descriptor whose dispatch axis signals Workflow-tool capability. */ +const CAPABLE_HOST = { + dispatch: { namedDispatch: true, nested: true, background: true, backgroundDispatch: false }, +}; + +/** Read the real capability declaration (data file — not a source grep). */ +function loadCap() { + return JSON.parse(fs.readFileSync(CAP_PATH, 'utf8')); +} + +/** A minimal single-plan wave manifest. */ +function singleWaveManifest() { + return { + phaseDir: '.planning/phases/01-foo', + runId: 'run-abc-1143', + waves: [ + { + id: 'w1', + plans: [ + { id: 'p1', brief: 'Implement the foo module', files_modified: ['src/foo.cts'] }, + ], + }, + ], + }; +} + +/** Two plans in one wave that DO NOT overlap (parallel-safe in a single stage). */ +function nonOverlappingManifest() { + return { + phaseDir: '.planning/phases/01-foo', + runId: 'run-abc-1143', + waves: [ + { + id: 'w1', + plans: [ + { id: 'p1', brief: 'Plan A', files_modified: ['src/a.cts'] }, + { id: 'p2', brief: 'Plan B', files_modified: ['src/b.cts'] }, + ], + }, + ], + }; +} + +/** Two plans in one wave that DO overlap on files_modified (must split into stages). */ +function overlappingManifest() { + return { + phaseDir: '.planning/phases/01-foo', + runId: 'run-abc-1143', + waves: [ + { + id: 'w1', + plans: [ + { id: 'p1', brief: 'Plan A', files_modified: ['src/shared.cts', 'src/a.cts'] }, + { id: 'p2', brief: 'Plan B', files_modified: ['src/shared.cts', 'src/b.cts'] }, + ], + }, + ], + }; +} + +// ─── 1. detectWorkflowBackend ───────────────────────────────────────────────── + +describe('detectWorkflowBackend', () => { + + test('capability disabled (default-off) -> inline, even on Claude with the tool', () => { + const r = detectWorkflowBackend({ + runtimeId: 'claude', + hostIntegration: CAPABLE_HOST, + agentSdkVersion: '1.0.0', + config: { 'claude_orchestration.enabled': false }, + }); + assert.strictEqual(r.available, false); + assert.strictEqual(r.backend, 'inline'); + assert.match(r.reason, /disabled/); + }); + + test('non-Claude runtime -> inline (criterion 6: no change to non-Claude loop)', () => { + for (const runtimeId of ['codex', 'cursor', 'opencode', 'copilot', ' Windsurf'.trim()]) { + const r = detectWorkflowBackend({ + runtimeId, + hostIntegration: CAPABLE_HOST, + agentSdkVersion: '1.0.0', + config: { 'claude_orchestration.enabled': true, 'claude_orchestration.execution_backend': 'workflow' }, + }); + assert.strictEqual(r.backend, 'inline', runtimeId + ' should be inline'); + assert.strictEqual(r.available, false, runtimeId + ' should be unavailable'); + assert.match(r.reason, /claude/i, runtimeId + ' reason should mention claude'); + } + }); + + test('Claude + auto + capable host + new-enough SDK -> workflow', () => { + const r = detectWorkflowBackend({ + runtimeId: 'claude', + hostIntegration: CAPABLE_HOST, + agentSdkVersion: '1.2.0', + config: { 'claude_orchestration.enabled': true, 'claude_orchestration.execution_backend': 'auto' }, + }); + assert.strictEqual(r.backend, 'workflow'); + assert.strictEqual(r.available, true); + }); + + test('Claude + execution_backend:"workflow" forces workflow when tool is capable', () => { + const r = detectWorkflowBackend({ + runtimeId: 'claude', + hostIntegration: CAPABLE_HOST, + agentSdkVersion: '1.0.0', + config: { 'claude_orchestration.enabled': true, 'claude_orchestration.execution_backend': 'workflow' }, + }); + assert.strictEqual(r.backend, 'workflow'); + assert.strictEqual(r.available, true); + }); + + test('Claude + execution_backend:"inline" -> inline even when tool is capable', () => { + const r = detectWorkflowBackend({ + runtimeId: 'claude', + hostIntegration: CAPABLE_HOST, + agentSdkVersion: '1.0.0', + config: { 'claude_orchestration.enabled': true, 'claude_orchestration.execution_backend': 'inline' }, + }); + assert.strictEqual(r.backend, 'inline'); + assert.match(r.reason, /inline/); + }); + + test('Claude + auto + host lacking nested dispatch -> inline (fail-closed)', () => { + const r = detectWorkflowBackend({ + runtimeId: 'claude', + hostIntegration: { dispatch: { nested: false, background: true } }, + agentSdkVersion: '1.0.0', + config: { 'claude_orchestration.enabled': true, 'claude_orchestration.execution_backend': 'auto' }, + }); + assert.strictEqual(r.backend, 'inline'); + assert.strictEqual(r.available, false); + }); + + test('Claude + unknown agentSdkVersion -> inline fail-closed (criterion 3 fallback)', () => { + const r = detectWorkflowBackend({ + runtimeId: 'claude', + hostIntegration: CAPABLE_HOST, + agentSdkVersion: undefined, + config: { 'claude_orchestration.enabled': true, 'claude_orchestration.execution_backend': 'auto' }, + }); + assert.strictEqual(r.backend, 'inline'); + assert.strictEqual(r.available, false); + assert.match(r.reason, /version|sdk|unknown/i); + }); + + test('agent SDK version boundary: floor-1 -> inline, floor -> workflow, floor+patch -> workflow', () => { + const floor = WORKFLOW_TOOL_FLOOR_VERSION; + const [maj, min, pat] = floor.split('.').map((n) => parseInt(n, 10)); + // Robust "below" derivation with full borrow chain (works for .0.0 floors too). + let below; + if (pat > 0) below = `${maj}.${min}.${pat - 1}`; + else if (min > 0) below = `${maj}.${min - 1}.999`; + else if (maj > 0) below = `${maj - 1}.999.999`; + else { assert.ok(false, 'cannot derive below for 0.0.0 floor'); return; } + const above = `${maj}.${min}.${pat + 1}`; + // Sanity: confirm below really is below per the comparator under test. + assert.ok(compareSemver(below, floor) < 0, below + ' must compare below ' + floor); + + const cfg = { 'claude_orchestration.enabled': true, 'claude_orchestration.execution_backend': 'auto' }; + + const rBelow = detectWorkflowBackend({ runtimeId: 'claude', hostIntegration: CAPABLE_HOST, agentSdkVersion: below, config: cfg }); + assert.strictEqual(rBelow.backend, 'inline', below + ' (floor-1) must be inline'); + + const rAt = detectWorkflowBackend({ runtimeId: 'claude', hostIntegration: CAPABLE_HOST, agentSdkVersion: floor, config: cfg }); + assert.strictEqual(rAt.backend, 'workflow', floor + ' (exact floor) must be workflow'); + + const rAbove = detectWorkflowBackend({ runtimeId: 'claude', hostIntegration: CAPABLE_HOST, agentSdkVersion: above, config: cfg }); + assert.strictEqual(rAbove.backend, 'workflow', above + ' (floor+patch) must be workflow'); + }); + + test('config-level min_agent_sdk_version override raises/lowers the floor', () => { + const cfg = { + 'claude_orchestration.enabled': true, + 'claude_orchestration.execution_backend': 'auto', + 'claude_orchestration.min_agent_sdk_version': '2.0.0', + }; + const r1 = detectWorkflowBackend({ runtimeId: 'claude', hostIntegration: CAPABLE_HOST, agentSdkVersion: '1.9.9', config: cfg }); + assert.strictEqual(r1.backend, 'inline', 'below raised floor -> inline'); + const r2 = detectWorkflowBackend({ runtimeId: 'claude', hostIntegration: CAPABLE_HOST, agentSdkVersion: '2.0.0', config: cfg }); + assert.strictEqual(r2.backend, 'workflow', 'at raised floor -> workflow'); + }); + + test('execution_backend:"workflow" + SDK below floor -> inline (M-1: floor applies in both modes)', () => { + const cfg = { + 'claude_orchestration.enabled': true, + 'claude_orchestration.execution_backend': 'workflow', + }; + const r = detectWorkflowBackend({ runtimeId: 'claude', hostIntegration: CAPABLE_HOST, agentSdkVersion: '0.3.0', config: cfg }); + assert.strictEqual(r.backend, 'inline', 'workflow mode must still honor the SDK floor (fail-closed)'); + assert.strictEqual(r.available, false); + assert.match(r.reason, /floor|version/); + }); + + test('pre-release of the floor (0.3.149-rc.1) -> inline (pre-release < GA per SemVer)', () => { + const cfg = { 'claude_orchestration.enabled': true, 'claude_orchestration.execution_backend': 'auto' }; + // Explicitly assert the precedence rule: a pre-release tag is below the GA release. + assert.ok(compareSemver('0.3.149-rc.1', '0.3.149') < 0, 'pre-release must compare below GA'); + const r = detectWorkflowBackend({ runtimeId: 'claude', hostIntegration: CAPABLE_HOST, agentSdkVersion: '0.3.149-rc.1', config: cfg }); + assert.strictEqual(r.backend, 'inline', 'pre-release of the floor must not activate the BETA backend'); + assert.strictEqual(r.available, false); + }); + + test('two pre-releases of the same triple order by their identifiers (SemVer §11)', () => { + assert.ok(compareSemver('0.3.149-rc.0', '0.3.149-rc.1') < 0, 'rc.0 < rc.1'); + assert.ok(compareSemver('1.0.0-alpha.1', '1.0.0-alpha.2') < 0, 'alpha.1 < alpha.2'); + assert.ok(compareSemver('1.0.0-rc.1', '1.0.0-rc.2') < 0, 'rc.1 < rc.2'); + // numeric < alphanumeric at the same position + assert.ok(compareSemver('1.0.0-1', '1.0.0-alpha') < 0, 'numeric identifier < alphanumeric'); + }); + + test('missing/empty input -> inline, never throws (Postel: liberal-in-input)', () => { + assert.strictEqual(detectWorkflowBackend({}).backend, 'inline'); + assert.strictEqual(detectWorkflowBackend(null).backend, 'inline'); + assert.strictEqual(detectWorkflowBackend(undefined).backend, 'inline'); + assert.strictEqual(detectWorkflowBackend({ runtimeId: 'claude' }).backend, 'inline'); + }); + + test('BACKEND_VALUES exposes the closed enum', () => { + assert.deepStrictEqual([...BACKEND_VALUES].sort(), ['auto', 'inline', 'workflow']); + }); + + test('property: pure & deterministic (same input -> same output)', () => { + fc.assert(fc.property( + fc.record({ + runtimeId: fc.constantFrom('claude', 'codex', 'cursor', 'opencode'), + sdk: fc.option(fc.string({ minLength: 1, maxLength: 8 }).filter((s) => /^\d/.test(s)), { nil: undefined }), + backend: fc.constantFrom('auto', 'workflow', 'inline'), + enabled: fc.boolean(), + }), + (input) => { + const cfg = { + 'claude_orchestration.enabled': input.enabled, + 'claude_orchestration.execution_backend': input.backend, + }; + const a = detectWorkflowBackend({ runtimeId: input.runtimeId, hostIntegration: CAPABLE_HOST, agentSdkVersion: input.sdk, config: cfg }); + const b = detectWorkflowBackend({ runtimeId: input.runtimeId, hostIntegration: CAPABLE_HOST, agentSdkVersion: input.sdk, config: cfg }); + assert.deepStrictEqual(a, b); + assert.ok(['workflow', 'inline'].includes(a.backend)); + }, + )); + }); +}); + +// ─── 2. compareSemver helper ────────────────────────────────────────────────── + +describe('compareSemver', () => { + test('ordering', () => { + assert.ok(compareSemver('1.0.0', '0.9.9') > 0); + assert.ok(compareSemver('1.0.0', '1.0.0') === 0); + assert.ok(compareSemver('1.0.0', '1.0.1') < 0); + assert.ok(compareSemver('2.0.0', '1.9.9') > 0); + }); + test('garbage versions compare as -1 (fail-closed)', () => { + assert.strictEqual(compareSemver('garbage', '1.0.0'), -1); + assert.strictEqual(compareSemver('1.0.0', ''), -1); + }); +}); + +// ─── 3. emitWorkflowScript ──────────────────────────────────────────────────── + +describe('emitWorkflowScript', () => { + + test('single-wave single-plan -> one parallel barrier, one agent, executor+worktree', () => { + const { ok, script, summary } = emitWorkflowScript(singleWaveManifest()); + assert.strictEqual(ok, true); + assert.ok(typeof script === 'string' && script.length > 0); + + const parallelCount = (script.match(/parallel\s*\(/g) || []).length; + assert.ok(parallelCount >= 1, 'at least one parallel() barrier'); + assert.ok(script.includes('agent('), 'agent() call per plan'); + assert.ok(script.includes('gsd-executor'), 'uses gsd-executor agentType'); + assert.ok(script.includes('worktree'), 'uses worktree isolation'); + assert.ok(script.includes('SUMMARY.md'), 'produces SUMMARY.md (same artifact as inline path)'); + + assert.deepStrictEqual(summary.waves, 1); + assert.deepStrictEqual(summary.plans, 1); + }); + + test('multi-wave -> one parallel() barrier per wave (sequential barriers)', () => { + const r = emitWorkflowScript({ + phaseDir: '.planning/phases/01-foo', + runId: 'run-multi', + waves: [ + { id: 'w1', plans: [{ id: 'p1', brief: 'A', files_modified: ['src/a.cts'] }] }, + { id: 'w2', plans: [{ id: 'p2', brief: 'B', files_modified: ['src/b.cts'] }] }, + { id: 'w3', plans: [{ id: 'p3', brief: 'C', files_modified: ['src/c.cts'] }] }, + ], + }); + assert.strictEqual(r.ok, true); + const parallelCount = (r.script.match(/parallel\s*\(/g) || []).length; + assert.strictEqual(parallelCount, 3, 'one parallel() per wave'); + assert.strictEqual(r.summary.waves, 3); + assert.strictEqual(r.summary.plans, 3); + }); + + test('overlapping files_modified -> plans split into separate sequential stages (criterion 2)', () => { + const r = emitWorkflowScript(overlappingManifest()); + assert.strictEqual(r.ok, true); + // Two plans sharing src/shared.cts must NOT be in the same stage. + const stages = r.summary.stagesByWave[0]; // wave w1 + assert.ok(Array.isArray(stages), 'stagesByWave present'); + assert.strictEqual(stages.length, 2, 'overlapping plans split into 2 stages'); + const stagePlanSets = stages.map((s) => s.slice().sort()); + const allPlans = stagePlanSets.flat().sort(); + assert.deepStrictEqual(allPlans, ['p1', 'p2']); + // p1 and p2 must be in different stages + assert.ok(stages[0].length === 1 && stages[1].length === 1, 'one plan per stage when they overlap'); + }); + + test('non-overlapping plans -> coalesced into a single parallel stage', () => { + const r = emitWorkflowScript(nonOverlappingManifest()); + assert.strictEqual(r.ok, true); + const stages = r.summary.stagesByWave[0]; + assert.strictEqual(stages.length, 1, 'non-overlapping plans share one stage'); + assert.deepStrictEqual(stages[0].slice().sort(), ['p1', 'p2']); + }); + + test('resumeFromRunId wired to the provided runId (criterion 4)', () => { + const r = emitWorkflowScript(singleWaveManifest()); + assert.ok(r.script.includes('resumeFromRunId'), 'references resumeFromRunId'); + assert.ok(r.script.includes('run-abc-1143'), 'carries the run id'); + assert.strictEqual(r.summary.resumeRunId, 'run-abc-1143'); + }); + + test('shared budget pool emitted when budgetTokens provided', () => { + const r = emitWorkflowScript({ ...singleWaveManifest(), budgetTokens: 500000 }); + assert.ok(r.script.includes('budget('), 'emits budget() pool'); + assert.ok(r.script.includes('500000')); + }); + + test('no budget() emitted when budgetTokens omitted', () => { + const r = emitWorkflowScript(singleWaveManifest()); + assert.ok(!r.script.includes('budget('), 'no budget() when unset'); + }); + + test('invalid input -> ok:false with a reason, never throws', () => { + const empty = emitWorkflowScript({ phaseDir: '.p', runId: 'r', waves: [] }); + assert.strictEqual(empty.ok, false); + assert.ok(typeof empty.reason === 'string' && empty.reason.length > 0); + + const noRun = emitWorkflowScript({ phaseDir: '.p', runId: '', waves: singleWaveManifest().waves }); + assert.strictEqual(noRun.ok, false); + + const noPhase = emitWorkflowScript({ phaseDir: '', runId: 'r', waves: singleWaveManifest().waves }); + assert.strictEqual(noPhase.ok, false); + + const badWave = emitWorkflowScript({ phaseDir: '.p', runId: 'r', waves: [{ id: 'w1', plans: [] }] }); + assert.strictEqual(badWave.ok, false); + }); + + test('SECURITY: runId/phaseDir/wave.id/plan.id with injection chars -> ok:false (never reach the script)', () => { + // runId is interpolated inside resumeFromRunId("...") — a quote/backslash/newline + // could break out of the call. Identifier validation must reject it. + const injectRun = emitWorkflowScript({ phaseDir: '.p', runId: 'x");evil("y', waves: singleWaveManifest().waves }); + assert.strictEqual(injectRun.ok, false); + assert.match(injectRun.reason, /runId/i); + + const newlineRun = emitWorkflowScript({ phaseDir: '.p', runId: 'r\nbreakout', waves: singleWaveManifest().waves }); + assert.strictEqual(newlineRun.ok, false); + + const injectPhase = emitWorkflowScript({ phaseDir: '.p"; drop table', runId: 'r', waves: singleWaveManifest().waves }); + assert.strictEqual(injectPhase.ok, false); + + const injectWave = emitWorkflowScript({ + phaseDir: '.p', runId: 'r', + waves: [{ id: 'w1\nagent("evil")', plans: [{ id: 'p1', brief: 'b', files_modified: ['a.cts'] }] }], + }); + assert.strictEqual(injectWave.ok, false); + + const injectPlan = emitWorkflowScript({ + phaseDir: '.p', runId: 'r', + waves: [{ id: 'w1', plans: [{ id: 'p1";x("y', brief: 'b', files_modified: ['a.cts'] }] }], + }); + assert.strictEqual(injectPlan.ok, false); + }); + + test('SECURITY: a brief containing quotes/backslash/newlines is neutralised (never breaks the string literal)', () => { + const r = emitWorkflowScript({ + phaseDir: '.p', runId: 'r', + waves: [{ id: 'w1', plans: [{ id: 'p1', brief: 'he said "hi" \\ then \n newline', files_modified: ['a.cts'] }] }], + }); + assert.strictEqual(r.ok, true); + // The emitted script must not contain a raw unescaped quote that closes the + // agent() string literal, nor a raw newline inside the brief. + assert.ok(!r.script.includes('he said "hi" \\\\'), 'no unescaped breakout'); + // The full brief text never appears verbatim with its dangerous chars intact. + assert.ok(!r.script.includes('"hi"'), 'the inner quote must be JSON-escaped, not raw'); + }); + + test('duplicate plan id within a wave -> ok:false (L-5: no silent brief loss)', () => { + const r = emitWorkflowScript({ + phaseDir: '.p', runId: 'r', + waves: [{ id: 'w1', plans: [ + { id: 'p1', brief: 'first', files_modified: ['a.cts'] }, + { id: 'p1', brief: 'second', files_modified: ['b.cts'] }, + ] }], + }); + assert.strictEqual(r.ok, false); + assert.match(r.reason, /duplicate/i); + }); + + test('non-string files_modified entries -> ok:false (L-7: strict element typing)', () => { + const r = emitWorkflowScript({ + phaseDir: '.p', runId: 'r', + waves: [{ id: 'w1', plans: [{ id: 'p1', brief: 'b', files_modified: ['ok.cts', 42, { path: 'x' }] }] }], + }); + assert.strictEqual(r.ok, false); + assert.match(r.reason, /files_modified/); + }); + + test('property: deterministic (same input -> identical script)', () => { + fc.assert(fc.property( + fc.record({ + runId: fc.string({ minLength: 1, maxLength: 12 }).filter((s) => /^[a-zA-Z0-9-]+$/.test(s)), + nPlans: fc.integer({ min: 1, max: 5 }), + }), + ({ runId, nPlans }) => { + const waves = [{ + id: 'w1', + plans: Array.from({ length: nPlans }, (_, i) => ({ + id: 'p' + i, + brief: 'brief ' + i, + files_modified: ['src/file' + i + '.cts'], + })), + }]; + const a = emitWorkflowScript({ phaseDir: '.planning/phases/01-x', runId, waves }); + const b = emitWorkflowScript({ phaseDir: '.planning/phases/01-x', runId, waves }); + assert.strictEqual(a.script, b.script); + assert.deepStrictEqual(a.summary, b.summary); + }, + )); + }); +}); + +// ─── 4. Capability declaration validation ───────────────────────────────────── + +describe('capability declaration (capabilities/claude-orchestration/capability.json)', () => { + + test('file exists and parses', () => { + const cap = loadCap(); + assert.strictEqual(cap.id, 'claude-orchestration'); + }); + + test('passes per-file validateCapability', () => { + const errors = validateCapability(loadCap(), 'claude-orchestration'); + assert.deepEqual(errors, [], 'Expected no validation errors: ' + JSON.stringify(errors)); + }); + + test('passes contract validation (contribution.into roles, when references)', () => { + const errors = validateAgainstContract(loadCap(), 'claude-orchestration'); + assert.deepEqual(errors, [], 'Expected no contract errors: ' + JSON.stringify(errors)); + }); + + test('default-off: activationKey default is false and points at the enabled key', () => { + const cap = loadCap(); + assert.strictEqual(cap.activationKey, 'claude_orchestration.enabled'); + assert.strictEqual(cap.config['claude_orchestration.enabled'].default, false); + assert.strictEqual(cap.config['claude_orchestration.enabled'].type, 'boolean'); + }); + + test('runtimeCompat is claude-only (criterion 6)', () => { + const cap = loadCap(); + assert.deepStrictEqual(cap.runtimeCompat.supported, ['claude']); + assert.deepStrictEqual(cap.runtimeCompat.unsupported, []); + }); + + test('BETA posture: tier full, role feature', () => { + const cap = loadCap(); + assert.strictEqual(cap.role, 'feature'); + assert.strictEqual(cap.tier, 'full'); + }); + + test('execution_backend is an enum with auto|workflow|inline defaulting to auto', () => { + const slice = loadCap().config['claude_orchestration.execution_backend']; + assert.strictEqual(slice.type, 'enum'); + assert.deepStrictEqual(slice.values, ['auto', 'workflow', 'inline']); + assert.strictEqual(slice.default, 'auto'); + }); + + test('registers at WIRED points only (execute:wave:post, plan:post)', () => { + const cap = loadCap(); + const points = cap.contributions.map((c) => c.point); + for (const p of points) { + assert.ok( + ['discuss:pre', 'discuss:post', 'plan:pre', 'plan:post', 'execute:post', 'execute:wave:post', 'verify:post', 'ship:pre', 'ship:post'].includes(p), + 'contribution point ' + p + ' must be a wired point', + ); + } + assert.ok(points.includes('execute:wave:post'), 'registers the execute wave hook'); + assert.ok(points.includes('plan:post'), 'declares plan:* ownership for ultraplan (criterion 5)'); + }); + + test('all contributions gated by the enabled key + onError:skip (default-resilient)', () => { + const cap = loadCap(); + for (const c of cap.contributions) { + assert.strictEqual(c.when, 'claude_orchestration.enabled', 'every contribution gated by enabled'); + assert.strictEqual(c.onError, 'skip', 'every contribution onError:skip'); + } + }); +}); + +// ─── 5. Registry integration ────────────────────────────────────────────────── + +describe('registry integration', () => { + + test('loadAndValidate includes claude-orchestration with no errors', () => { + const { capMap, errors } = loadAndValidate(new Set()); // empty central keys = no collision noise + // Filter errors to only those touching our capability. + const ours = errors.filter((e) => e.includes('claude-orchestration')); + assert.deepEqual(ours, [], 'our capability produced errors: ' + JSON.stringify(ours)); + assert.ok(capMap.has('claude-orchestration'), 'capMap includes claude-orchestration'); + }); + + test('buildRegistry surfaces the federated config keys in configSchema', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + assert.ok(registry.configSchema['claude_orchestration.enabled'], 'enabled key federated'); + assert.ok(registry.configSchema['claude_orchestration.execution_backend'], 'execution_backend key federated'); + assert.strictEqual(registry.configSchema['claude_orchestration.enabled'].owner, 'claude-orchestration'); + assert.strictEqual(registry.configSchema['claude_orchestration.execution_backend'].default, 'auto'); + }); + + test('byLoopPoint[execute:wave:post].contributions includes our capability', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + const contribs = registry.byLoopPoint['execute:wave:post'].contributions; + const ours = contribs.find((c) => c.capId === 'claude-orchestration'); + assert.ok(ours, 'our execute:wave:post contribution is registered'); + assert.strictEqual(ours.into, 'executor'); + }); + + test('committed registry is in sync (gen-capability-registry --check)', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + const live = serializeRegistry(registry, capMap); + const committed = fs.readFileSync(REGISTRY_PATH, 'utf8'); + assert.strictEqual( + normalizeLineEndings(stripGeneratedComment(committed)), + normalizeLineEndings(stripGeneratedComment(live)), + 'registry is stale — run: node scripts/gen-capability-registry.cjs --write', + ); + }); +}); + +// ─── 6. Inline-fallback parity (criterion 3 + 6) ────────────────────────────── + +describe('inline-fallback parity', () => { + + test('default config (capability off) -> inline on every runtime, including Claude', () => { + // The capability ships default-off; with no user opt-in the backend is always inline. + const defaultCfg = {}; // nothing set + for (const runtimeId of ['claude', 'codex', 'cursor', 'opencode']) { + const r = detectWorkflowBackend({ + runtimeId, + hostIntegration: CAPABLE_HOST, + agentSdkVersion: '1.0.0', + config: defaultCfg, + }); + assert.strictEqual(r.backend, 'inline', runtimeId + ' default must be inline'); + assert.strictEqual(r.available, false, runtimeId + ' default must be unavailable'); + } + }); + + test('generated Workflow script preserves the inline-path contract (same agent + isolation + artifact)', () => { + // Criterion 2: the emitted Workflow composes the SAME gsd-executor agent and worktree + // isolation the inline path uses, and produces the same SUMMARY.md artifact. + const r = emitWorkflowScript(singleWaveManifest()); + assert.ok(r.script.includes('gsd-executor'), 'same executor agent as inline dispatch'); + assert.ok(r.script.includes('worktree'), 'same worktree isolation as inline dispatch'); + assert.ok(r.script.includes('SUMMARY.md'), 'same SUMMARY.md artifact as inline dispatch'); + }); +}); diff --git a/tests/execute-wave-post-gate-pipeline-e2e.test.cjs b/tests/execute-wave-post-gate-pipeline-e2e.test.cjs index 99fdbeec9..c01f1903d 100644 --- a/tests/execute-wave-post-gate-pipeline-e2e.test.cjs +++ b/tests/execute-wave-post-gate-pipeline-e2e.test.cjs @@ -629,15 +629,17 @@ describe('F. Real registry execute:wave:post shape — guard against accidental `ui.safety-gate onError must be 'halt'; got ${uiGate.onError}`); }); - test('[happy] real registry: execute:wave:post has no steps and 2 contributions (mempalace capture-problems + external-job executor fragment)', () => { + test('[happy] real registry: execute:wave:post has no steps and 3 contributions (claude-orchestration executor + external-job executor + mempalace capture-problems)', () => { const point = realRegistry.byLoopPoint['execute:wave:post']; assert.strictEqual(point.steps.length, 0, `execute:wave:post steps must be empty; got ${point.steps.length}`); - assert.strictEqual(point.contributions.length, 2, - `execute:wave:post must have 2 contributions (mempalace + external-job); got ${point.contributions.length}`); + // #1143: claude-orchestration registers an execute:wave:post contribution + // providing the Workflow-tool backend guidance (default-off, claude-only). + assert.strictEqual(point.contributions.length, 3, + `execute:wave:post must have 3 contributions (claude-orchestration + external-job + mempalace); got ${point.contributions.length}`); const capIds = point.contributions.map(c => c.capId).sort(); - assert.deepStrictEqual(capIds, ['external-job', 'mempalace'], - `execute:wave:post contributions must be mempalace + external-job; got ${capIds.join(',')}`); + assert.deepStrictEqual(capIds, ['claude-orchestration', 'external-job', 'mempalace'], + `execute:wave:post contributions must be claude-orchestration + external-job + mempalace; got ${capIds.join(',')}`); }); }); diff --git a/tests/fix-1941-quick-worktree-stale-base.test.cjs b/tests/fix-1941-quick-worktree-stale-base.test.cjs new file mode 100644 index 000000000..0b59589dd --- /dev/null +++ b/tests/fix-1941-quick-worktree-stale-base.test.cjs @@ -0,0 +1,76 @@ +// allow-test-rule: source-text-is-the-product #1941 +// Workflow .md files are the installed AI instructions — their text IS what the runtime +// loads. Testing text content tests the deployed contract. Per CONTRIBUTING.md exception matrix. + +/** + * Regression tests for bug #1941: /gsd-quick worktree executor forks from a stale base — + * up to many commits behind, not just the one-commit gap #1265 already covers. + * + * Root cause: Claude Code's isolation="worktree" forks new worktrees from origin/HEAD, not + * the live local HEAD. When prior local commits (e.g. earlier quick tasks in the same + * session, or this task's own Step 5.6 pre-dispatch plan commit) advance local HEAD without + * an intervening `git push`, origin/HEAD stays pinned to a stale ancestor and the executor's + * worktree_branch_check guard halts with a base-mismatch fatal. The fix ports the + * worktree.base-check auto-degrade pattern already used by execute-phase (#683/#1369) into + * quick.md's single-dispatch path. + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +const WORKFLOW_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'quick.md'); + +describe('quick: pre-dispatch worktree base re-check (#1941)', () => { + test('workflow file exists', () => { + assert.ok(fs.existsSync(WORKFLOW_PATH), 'workflows/quick.md should exist'); + }); + + test('Step 6 runs worktree.base-check before capturing EXPECTED_BASE', () => { + const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); + const step6Idx = content.indexOf('**Step 6: Spawn executor**'); + const baseCheckIdx = content.indexOf('worktree.base-check', step6Idx); + const expectedBaseIdx = content.indexOf('EXPECTED_BASE=$(git rev-parse HEAD)', step6Idx); + assert.ok(step6Idx !== -1, '"Step 6: Spawn executor" must exist in quick.md'); + assert.ok(baseCheckIdx !== -1, 'worktree.base-check must be invoked within Step 6'); + assert.ok(expectedBaseIdx !== -1, 'EXPECTED_BASE capture must exist within Step 6'); + assert.ok( + baseCheckIdx < expectedBaseIdx, + 'worktree.base-check must run BEFORE EXPECTED_BASE is captured so the degrade decision reflects the most current local HEAD' + ); + }); + + test('degrade check references #1941 for traceability', () => { + const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); + assert.ok(content.includes('#1941'), 'quick.md must reference #1941'); + }); + + test('degrade check sets USE_WORKTREES=false when shouldDegrade is true', () => { + const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); + const baseCheckIdx = content.indexOf('worktree.base-check'); + const block = content.slice(baseCheckIdx, baseCheckIdx + 600); + assert.ok( + block.includes('shouldDegrade') && block.includes('USE_WORKTREES=false'), + 'degrade check must override USE_WORKTREES=false when shouldDegrade is true' + ); + }); + + test('degrade check guards on RUNTIME=claude (worktree isolation is Claude Code-specific)', () => { + const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); + const baseCheckIdx = content.indexOf('worktree.base-check'); + const block = content.slice(Math.max(0, baseCheckIdx - 200), baseCheckIdx + 200); + assert.ok( + block.includes('RUNTIME') && (block.includes('"claude"') || block.includes("'claude'")), + 'degrade check must guard on RUNTIME=claude' + ); + }); + + test('degrade check names origin/HEAD as the stale fork base', () => { + const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); + const step6Idx = content.indexOf('**Step 6: Spawn executor**'); + const nextSection = content.indexOf('\n---', step6Idx); + const section = content.slice(step6Idx, nextSection === -1 ? undefined : nextSection); + assert.ok(section.includes('origin/HEAD'), 'Step 6 must name origin/HEAD as the stale fork base'); + }); +}); diff --git a/tests/fixtures/golden-install-parity/antigravity.json b/tests/fixtures/golden-install-parity/antigravity.json index eb0aab145..db53e2eb3 100644 --- a/tests/fixtures/golden-install-parity/antigravity.json +++ b/tests/fixtures/golden-install-parity/antigravity.json @@ -38,7 +38,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74", - "gsd-core/bin/gsd-tools.cjs": "747defe620cb8ae6", + "gsd-core/bin/gsd-tools.cjs": "f2dd7a6993dc12f1", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "1318633d27964e7a", @@ -87,6 +87,7 @@ "gsd-core/references/gates.md": "7dc9fd3a3d6217c6", "gsd-core/references/git-integration.md": "9e6076a137f9e156", "gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7", + "gsd-core/references/gsd-run-resolver.md": "c11c191ffad86a9f", "gsd-core/references/honest-verifier.md": "a31a4bf42d82d5e9", "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", @@ -173,7 +174,7 @@ "gsd-core/templates/milestone.md": "74d2f750ae9f4a9c", "gsd-core/templates/phase-prompt.md": "31f627d456ab14b0", "gsd-core/templates/planner-subagent-prompt.md": "6c9f1b23ee3dc05f", - "gsd-core/templates/project.md": "e4a3b331be081e62", + "gsd-core/templates/project.md": "fae14114f2b79b10", "gsd-core/templates/requirements.md": "a44de4c2f146e473", "gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0", "gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f", @@ -226,7 +227,7 @@ "gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb", "gsd-core/workflows/discuss-phase/templates/context.md": "9804674fccada405", "gsd-core/workflows/discuss-phase/templates/discussion-log.md": "2b56ec2868cbddb4", - "gsd-core/workflows/do.md": "22adc4ed8589a015", + "gsd-core/workflows/do.md": "3eddf1cc9e04b685", "gsd-core/workflows/docs-update.md": "8e986e26d0e6e1a0", "gsd-core/workflows/edit-phase.md": "fc932e82ba1f585a", "gsd-core/workflows/eval-review.md": "eb4040eaa5b8497f", @@ -243,10 +244,10 @@ "gsd-core/workflows/graduation.md": "a9d8f15ba81a993f", "gsd-core/workflows/health.md": "1ac4d567ee95acfb", "gsd-core/workflows/help.md": "5d040504b9ab35e3", - "gsd-core/workflows/help/modes/brief.md": "2e923b3ed635d495", - "gsd-core/workflows/help/modes/default.md": "b544113eb9aa54fa", - "gsd-core/workflows/help/modes/full.md": "0e253248c1c2e063", - "gsd-core/workflows/help/modes/topic.md": "19a279fe91922d3b", + "gsd-core/workflows/help/modes/brief.md": "fa2675516b40e2e3", + "gsd-core/workflows/help/modes/default.md": "be05e56b2c5ee2c0", + "gsd-core/workflows/help/modes/full.md": "c8ffd141e28f5111", + "gsd-core/workflows/help/modes/topic.md": "0bf9ab39d7044d69", "gsd-core/workflows/import.md": "41c62ae199209a68", "gsd-core/workflows/inbox.md": "a448220c548f27bc", "gsd-core/workflows/ingest-docs.md": "e2707f28f67d128b", @@ -264,6 +265,7 @@ "gsd-core/workflows/next.md": "8beefb4335dd7c41", "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", "gsd-core/workflows/note.md": "3ce09c0aa0a20599", + "gsd-core/workflows/onboard.md": "69d871aa53a1a859", "gsd-core/workflows/pause-work.md": "564de32981a24337", "gsd-core/workflows/plan-milestone-gaps.md": "dd6a4b3a8b05ab6e", "gsd-core/workflows/plan-phase.md": "f3310f285ab426c6", @@ -275,7 +277,7 @@ "gsd-core/workflows/pr-branch.md": "dc5598ae8accdecd", "gsd-core/workflows/profile-user.md": "355af92ac285567f", "gsd-core/workflows/progress.md": "79a11ce798082054", - "gsd-core/workflows/quick.md": "a6aa362881f4e5da", + "gsd-core/workflows/quick.md": "35582887917ef938", "gsd-core/workflows/reapply-patches.md": "4dcd6117d0a507ca", "gsd-core/workflows/remove-phase.md": "23b9eb0858a2535e", "gsd-core/workflows/remove-workspace.md": "d0bd7e0601138798", @@ -368,7 +370,7 @@ "skills/gsd-inbox/SKILL.md": "4ec0198966ac4f7d", "skills/gsd-ingest-docs/SKILL.md": "f4e43968af317ed8", "skills/gsd-manager/SKILL.md": "6e51785860784d8c", - "skills/gsd-map-codebase/SKILL.md": "e10431a9386c6aec", + "skills/gsd-map-codebase/SKILL.md": "552f914f43b2181c", "skills/gsd-mempalace-capture/SKILL.md": "588ddf4fbf3ebe65", "skills/gsd-mempalace-recall/SKILL.md": "2d95e385fee26dee", "skills/gsd-milestone-summary/SKILL.md": "2955ab3e3a444ca3", @@ -379,9 +381,10 @@ "skills/gsd-ns-context/SKILL.md": "e791bc60e4cf124e", "skills/gsd-ns-ideate/SKILL.md": "b85f270e8415f595", "skills/gsd-ns-manage/SKILL.md": "d55c53b5b0513033", - "skills/gsd-ns-project/SKILL.md": "64844073a6115c45", + "skills/gsd-ns-project/SKILL.md": "905eff81dbb98b38", "skills/gsd-ns-review/SKILL.md": "2e30158e112d173c", "skills/gsd-ns-workflow/SKILL.md": "529e21833a6a8831", + "skills/gsd-onboard/SKILL.md": "1447665c64c8ade8", "skills/gsd-pause-work/SKILL.md": "9d3cc6bd70b03df1", "skills/gsd-phase/SKILL.md": "00676bbea61410bf", "skills/gsd-plan-phase/SKILL.md": "168046ccf9702532", diff --git a/tests/fixtures/golden-install-parity/augment.json b/tests/fixtures/golden-install-parity/augment.json index e84d63c85..8c916be89 100644 --- a/tests/fixtures/golden-install-parity/augment.json +++ b/tests/fixtures/golden-install-parity/augment.json @@ -62,7 +62,7 @@ "commands/gsd-inbox.md": "c218298db8b36e31", "commands/gsd-ingest-docs.md": "6718b04c597a5428", "commands/gsd-manager.md": "8f4ae79774902293", - "commands/gsd-map-codebase.md": "f3a06c4c7503f243", + "commands/gsd-map-codebase.md": "e4e170d4ef2d46c6", "commands/gsd-mempalace-capture.md": "2e610f3ebe2eb5b4", "commands/gsd-mempalace-recall.md": "38716c0983a3ef9c", "commands/gsd-milestone-summary.md": "ff5e11e6b33d4b5e", @@ -73,9 +73,10 @@ "commands/gsd-ns-context.md": "011c44e7aa46e64a", "commands/gsd-ns-ideate.md": "edc5e543512dd48a", "commands/gsd-ns-manage.md": "0409d810e499357f", - "commands/gsd-ns-project.md": "8dfd1b9a2ffe86ac", + "commands/gsd-ns-project.md": "ff67e85bc6f7fc5a", "commands/gsd-ns-review.md": "3766ed10827882a0", "commands/gsd-ns-workflow.md": "c3b3c046a74ec0ee", + "commands/gsd-onboard.md": "1e8acf7be31834be", "commands/gsd-pause-work.md": "4fb032f72238fe33", "commands/gsd-phase.md": "4920d15d779329eb", "commands/gsd-plan-phase.md": "e74f3cb7a10cbb83", @@ -108,7 +109,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "770e0b1c1f08ef47", + "gsd-core/bin/gsd-tools.cjs": "f1a1a58072e7c35d", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "1318633d27964e7a", @@ -157,6 +158,7 @@ "gsd-core/references/gates.md": "bd79c7f90c8cb8d7", "gsd-core/references/git-integration.md": "77bf9dff38b2c9d4", "gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7", + "gsd-core/references/gsd-run-resolver.md": "e71eb728b84641b1", "gsd-core/references/honest-verifier.md": "8815c9fc18c35719", "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", @@ -243,7 +245,7 @@ "gsd-core/templates/milestone.md": "74d2f750ae9f4a9c", "gsd-core/templates/phase-prompt.md": "b811bf951092df2f", "gsd-core/templates/planner-subagent-prompt.md": "ebf29dbb27042370", - "gsd-core/templates/project.md": "ab4af6becca2bb23", + "gsd-core/templates/project.md": "4f311fb1b05b823b", "gsd-core/templates/requirements.md": "a44de4c2f146e473", "gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0", "gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f", @@ -296,7 +298,7 @@ "gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb", "gsd-core/workflows/discuss-phase/templates/context.md": "6cd929e989fe2b0f", "gsd-core/workflows/discuss-phase/templates/discussion-log.md": "1bbd7703f11128e1", - "gsd-core/workflows/do.md": "8f460af710c8f6a4", + "gsd-core/workflows/do.md": "7512dd1892f118a1", "gsd-core/workflows/docs-update.md": "f35922d15b7061c9", "gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04", "gsd-core/workflows/eval-review.md": "f898936e2cfe4130", @@ -313,10 +315,10 @@ "gsd-core/workflows/graduation.md": "47f1594c88c08501", "gsd-core/workflows/health.md": "107e3c72e76d9535", "gsd-core/workflows/help.md": "5d040504b9ab35e3", - "gsd-core/workflows/help/modes/brief.md": "5fe077fbbdb5d9f7", - "gsd-core/workflows/help/modes/default.md": "cc8ef97de634c995", - "gsd-core/workflows/help/modes/full.md": "3ac1c1bb851da7a5", - "gsd-core/workflows/help/modes/topic.md": "6762e41e3291ae24", + "gsd-core/workflows/help/modes/brief.md": "924860e1f07defb0", + "gsd-core/workflows/help/modes/default.md": "08a02976c0c5cc50", + "gsd-core/workflows/help/modes/full.md": "fc307cb77544df1d", + "gsd-core/workflows/help/modes/topic.md": "5c160093f3cbf35d", "gsd-core/workflows/import.md": "3d3fa603ceb8bc9f", "gsd-core/workflows/inbox.md": "437f981ef9ae7b26", "gsd-core/workflows/ingest-docs.md": "c859921c811ac11b", @@ -334,6 +336,7 @@ "gsd-core/workflows/next.md": "13fb800f2472d970", "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", "gsd-core/workflows/note.md": "5a99eb396c744619", + "gsd-core/workflows/onboard.md": "6f9e6c0b484271a9", "gsd-core/workflows/pause-work.md": "f2b33bba5593d422", "gsd-core/workflows/plan-milestone-gaps.md": "852f6d7c0c4299dc", "gsd-core/workflows/plan-phase.md": "b693bf6fd9ec6b08", @@ -345,7 +348,7 @@ "gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b", "gsd-core/workflows/profile-user.md": "14263db831230142", "gsd-core/workflows/progress.md": "893aa3c36983f74b", - "gsd-core/workflows/quick.md": "3bafc0a59cc90d26", + "gsd-core/workflows/quick.md": "5790ceb09aa685be", "gsd-core/workflows/reapply-patches.md": "39050f72601aec89", "gsd-core/workflows/remove-phase.md": "df9a45f0b1880999", "gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c", @@ -415,7 +418,7 @@ "skills/gsd-ns-context/skills/docs-update/SKILL.md": "e7ab14a984f7462c", "skills/gsd-ns-context/skills/extract-learnings/SKILL.md": "48846bf26ccaf0e7", "skills/gsd-ns-context/skills/graphify/SKILL.md": "3d6341614add6ccd", - "skills/gsd-ns-context/skills/map-codebase/SKILL.md": "51d6b970dca3c28e", + "skills/gsd-ns-context/skills/map-codebase/SKILL.md": "2017007dcd69d153", "skills/gsd-ns-context/skills/mempalace-capture/SKILL.md": "a7261b5ead71cc58", "skills/gsd-ns-context/skills/mempalace-recall/SKILL.md": "646e59e76acd548f", "skills/gsd-ns-ideate/SKILL.md": "d70360a5f7e8ac90", @@ -443,7 +446,7 @@ "skills/gsd-ns-manage/skills/update/SKILL.md": "c8165b8085ba106a", "skills/gsd-ns-manage/skills/workspace/SKILL.md": "98cc03aa8c735e9d", "skills/gsd-ns-manage/skills/workstreams/SKILL.md": "94d00b5116523f36", - "skills/gsd-ns-project/SKILL.md": "4d7e3c3870536dc9", + "skills/gsd-ns-project/SKILL.md": "ae47130ec878de8c", "skills/gsd-ns-project/skills/audit-milestone/SKILL.md": "6d4c77390c549fc7", "skills/gsd-ns-project/skills/complete-milestone/SKILL.md": "0af489daf6fd3f16", "skills/gsd-ns-project/skills/import/SKILL.md": "a3cfa7abc5ddd26d", @@ -451,6 +454,7 @@ "skills/gsd-ns-project/skills/milestone-summary/SKILL.md": "18dc0c134cb62657", "skills/gsd-ns-project/skills/new-milestone/SKILL.md": "a94ff34b09e17a98", "skills/gsd-ns-project/skills/new-project/SKILL.md": "81f681f54ca61f90", + "skills/gsd-ns-project/skills/onboard/SKILL.md": "1f5b29c75a91be4f", "skills/gsd-ns-project/skills/profile-user/SKILL.md": "68b015a40d386bff", "skills/gsd-ns-project/skills/review-backlog/SKILL.md": "0f25310aa0cd48f6", "skills/gsd-ns-review/SKILL.md": "d7567fd9a75a5c21", diff --git a/tests/fixtures/golden-install-parity/claude.json b/tests/fixtures/golden-install-parity/claude.json index 6294ef67a..f71bdd663 100644 --- a/tests/fixtures/golden-install-parity/claude.json +++ b/tests/fixtures/golden-install-parity/claude.json @@ -37,7 +37,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "770e0b1c1f08ef47", + "gsd-core/bin/gsd-tools.cjs": "f1a1a58072e7c35d", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "1318633d27964e7a", @@ -86,6 +86,7 @@ "gsd-core/references/gates.md": "7dc9fd3a3d6217c6", "gsd-core/references/git-integration.md": "5c70ef3203b7c9ce", "gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7", + "gsd-core/references/gsd-run-resolver.md": "e71eb728b84641b1", "gsd-core/references/honest-verifier.md": "8815c9fc18c35719", "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", @@ -172,7 +173,7 @@ "gsd-core/templates/milestone.md": "74d2f750ae9f4a9c", "gsd-core/templates/phase-prompt.md": "213ccd947451ff2b", "gsd-core/templates/planner-subagent-prompt.md": "6c9f1b23ee3dc05f", - "gsd-core/templates/project.md": "1f687c7a88381080", + "gsd-core/templates/project.md": "ae1f68db042c2522", "gsd-core/templates/requirements.md": "a44de4c2f146e473", "gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0", "gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f", @@ -225,7 +226,7 @@ "gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb", "gsd-core/workflows/discuss-phase/templates/context.md": "6cd929e989fe2b0f", "gsd-core/workflows/discuss-phase/templates/discussion-log.md": "1bbd7703f11128e1", - "gsd-core/workflows/do.md": "56215b98e26448bc", + "gsd-core/workflows/do.md": "61b92fb4d0bf3975", "gsd-core/workflows/docs-update.md": "63082608d3ae92be", "gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a", "gsd-core/workflows/eval-review.md": "f59e8329dae1e528", @@ -242,10 +243,10 @@ "gsd-core/workflows/graduation.md": "47f1594c88c08501", "gsd-core/workflows/health.md": "f934b1f1e9f3ae72", "gsd-core/workflows/help.md": "5d040504b9ab35e3", - "gsd-core/workflows/help/modes/brief.md": "2e923b3ed635d495", - "gsd-core/workflows/help/modes/default.md": "b544113eb9aa54fa", - "gsd-core/workflows/help/modes/full.md": "93cbdf7e542115a1", - "gsd-core/workflows/help/modes/topic.md": "899d7168a485297f", + "gsd-core/workflows/help/modes/brief.md": "fa2675516b40e2e3", + "gsd-core/workflows/help/modes/default.md": "be05e56b2c5ee2c0", + "gsd-core/workflows/help/modes/full.md": "e2084c6766e32d6c", + "gsd-core/workflows/help/modes/topic.md": "6e42db16f1568be9", "gsd-core/workflows/import.md": "6e8acff8c3918795", "gsd-core/workflows/inbox.md": "91aac6360e1a8672", "gsd-core/workflows/ingest-docs.md": "25832c784e8812e6", @@ -263,6 +264,7 @@ "gsd-core/workflows/next.md": "ef30b829a6d5c5f6", "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", "gsd-core/workflows/note.md": "42b66686b2c102cb", + "gsd-core/workflows/onboard.md": "3c50ed1f1fd07619", "gsd-core/workflows/pause-work.md": "5716362557f44ce4", "gsd-core/workflows/plan-milestone-gaps.md": "1b43d12812f7bc1e", "gsd-core/workflows/plan-phase.md": "823254227fc8e369", @@ -274,7 +276,7 @@ "gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd", "gsd-core/workflows/profile-user.md": "ff3820a27731ceb8", "gsd-core/workflows/progress.md": "bd1ecf9207331bda", - "gsd-core/workflows/quick.md": "bd56a6313d5ed43c", + "gsd-core/workflows/quick.md": "da83b1a15b7f1bf7", "gsd-core/workflows/reapply-patches.md": "ba9406b60f2c4041", "gsd-core/workflows/remove-phase.md": "ada8a0546c686483", "gsd-core/workflows/remove-workspace.md": "f3ab3a88a7e9e1ed", @@ -367,7 +369,7 @@ "skills/gsd-inbox/SKILL.md": "d05a099c0be02c60", "skills/gsd-ingest-docs/SKILL.md": "eb85e654917a503a", "skills/gsd-manager/SKILL.md": "1370d93741e47828", - "skills/gsd-map-codebase/SKILL.md": "5009595dbde69739", + "skills/gsd-map-codebase/SKILL.md": "cf65e61890448285", "skills/gsd-mempalace-capture/SKILL.md": "40a31ad049374297", "skills/gsd-mempalace-recall/SKILL.md": "c3f412d5a64f3921", "skills/gsd-milestone-summary/SKILL.md": "33d51a325d314f3a", @@ -378,9 +380,10 @@ "skills/gsd-ns-context/SKILL.md": "3102e7ad9b60f182", "skills/gsd-ns-ideate/SKILL.md": "c2c129408a046d22", "skills/gsd-ns-manage/SKILL.md": "8d786e01fa28a7bb", - "skills/gsd-ns-project/SKILL.md": "cc0c4bd9feeab147", + "skills/gsd-ns-project/SKILL.md": "76c77b121ad68b6c", "skills/gsd-ns-review/SKILL.md": "4111ea56e820479f", "skills/gsd-ns-workflow/SKILL.md": "4282c40a5e6e8bbe", + "skills/gsd-onboard/SKILL.md": "46460b479b7524bf", "skills/gsd-pause-work/SKILL.md": "35e8a148e44f5361", "skills/gsd-phase/SKILL.md": "00be96e7ae36c6f0", "skills/gsd-plan-phase/SKILL.md": "0c9e87da048acfb7", diff --git a/tests/fixtures/golden-install-parity/cline.json b/tests/fixtures/golden-install-parity/cline.json index f56e0eb88..d113ecf6e 100644 --- a/tests/fixtures/golden-install-parity/cline.json +++ b/tests/fixtures/golden-install-parity/cline.json @@ -41,7 +41,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "476aa24e8c4f03cf", - "gsd-core/bin/gsd-tools.cjs": "13640341aa311ae3", + "gsd-core/bin/gsd-tools.cjs": "c6818560fbd9f8e1", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "1318633d27964e7a", @@ -90,6 +90,7 @@ "gsd-core/references/gates.md": "bd79c7f90c8cb8d7", "gsd-core/references/git-integration.md": "f5403e1470e46e69", "gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7", + "gsd-core/references/gsd-run-resolver.md": "07544d9a957fc2ca", "gsd-core/references/honest-verifier.md": "8815c9fc18c35719", "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", @@ -176,7 +177,7 @@ "gsd-core/templates/milestone.md": "74d2f750ae9f4a9c", "gsd-core/templates/phase-prompt.md": "b811bf951092df2f", "gsd-core/templates/planner-subagent-prompt.md": "ebf29dbb27042370", - "gsd-core/templates/project.md": "ab4af6becca2bb23", + "gsd-core/templates/project.md": "4f311fb1b05b823b", "gsd-core/templates/requirements.md": "a44de4c2f146e473", "gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0", "gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f", @@ -229,7 +230,7 @@ "gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb", "gsd-core/workflows/discuss-phase/templates/context.md": "6cd929e989fe2b0f", "gsd-core/workflows/discuss-phase/templates/discussion-log.md": "1bbd7703f11128e1", - "gsd-core/workflows/do.md": "a35bbd09f381d940", + "gsd-core/workflows/do.md": "045a18f30efb1202", "gsd-core/workflows/docs-update.md": "39f288623a8f6f32", "gsd-core/workflows/edit-phase.md": "9c9fadc047c61d74", "gsd-core/workflows/eval-review.md": "3e1d7829ed2ed494", @@ -246,10 +247,10 @@ "gsd-core/workflows/graduation.md": "16fedecda36769eb", "gsd-core/workflows/health.md": "788fad84fa42eb9f", "gsd-core/workflows/help.md": "5d040504b9ab35e3", - "gsd-core/workflows/help/modes/brief.md": "5fe077fbbdb5d9f7", - "gsd-core/workflows/help/modes/default.md": "77c34f4a77dec02c", - "gsd-core/workflows/help/modes/full.md": "1bf502890828ada8", - "gsd-core/workflows/help/modes/topic.md": "6762e41e3291ae24", + "gsd-core/workflows/help/modes/brief.md": "924860e1f07defb0", + "gsd-core/workflows/help/modes/default.md": "86f7a14ad06e2f6f", + "gsd-core/workflows/help/modes/full.md": "efb266b8dbe60f51", + "gsd-core/workflows/help/modes/topic.md": "5c160093f3cbf35d", "gsd-core/workflows/import.md": "96ec687c5cfe84ac", "gsd-core/workflows/inbox.md": "437f981ef9ae7b26", "gsd-core/workflows/ingest-docs.md": "7d0517356c9f2619", @@ -267,6 +268,7 @@ "gsd-core/workflows/next.md": "3f8b61a35587a021", "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", "gsd-core/workflows/note.md": "5a99eb396c744619", + "gsd-core/workflows/onboard.md": "6f9e6c0b484271a9", "gsd-core/workflows/pause-work.md": "3530607514b0ac00", "gsd-core/workflows/plan-milestone-gaps.md": "bafdc6945cd2bd87", "gsd-core/workflows/plan-phase.md": "a08c5c99792f3086", @@ -278,7 +280,7 @@ "gsd-core/workflows/pr-branch.md": "9923878a4f6a2d91", "gsd-core/workflows/profile-user.md": "26f74db0a7fcd268", "gsd-core/workflows/progress.md": "9f326d63afb4b76b", - "gsd-core/workflows/quick.md": "10051a9b41d5d210", + "gsd-core/workflows/quick.md": "68a9dfcd53f0859f", "gsd-core/workflows/reapply-patches.md": "eb4272145a117904", "gsd-core/workflows/remove-phase.md": "e336350f8113a328", "gsd-core/workflows/remove-workspace.md": "e685dfbd736dfd90", @@ -326,7 +328,7 @@ "skills/gsd-ns-context/skills/docs-update/SKILL.md": "d150afd41a25ec08", "skills/gsd-ns-context/skills/extract-learnings/SKILL.md": "d39269bea995fabc", "skills/gsd-ns-context/skills/graphify/SKILL.md": "030050fcd08d129d", - "skills/gsd-ns-context/skills/map-codebase/SKILL.md": "19aec854da15f77b", + "skills/gsd-ns-context/skills/map-codebase/SKILL.md": "fa6a37402430af7d", "skills/gsd-ns-context/skills/mempalace-capture/SKILL.md": "588ddf4fbf3ebe65", "skills/gsd-ns-context/skills/mempalace-recall/SKILL.md": "2d95e385fee26dee", "skills/gsd-ns-ideate/SKILL.md": "c10342345c01c91f", @@ -354,7 +356,7 @@ "skills/gsd-ns-manage/skills/update/SKILL.md": "2666ab7786b69017", "skills/gsd-ns-manage/skills/workspace/SKILL.md": "c2616b157ec2350b", "skills/gsd-ns-manage/skills/workstreams/SKILL.md": "737841783c7fdd4f", - "skills/gsd-ns-project/SKILL.md": "5668e3a5e1d8896d", + "skills/gsd-ns-project/SKILL.md": "fac77a9e770fd655", "skills/gsd-ns-project/skills/audit-milestone/SKILL.md": "739ed755ad84e58e", "skills/gsd-ns-project/skills/complete-milestone/SKILL.md": "a69d534b385d6aaf", "skills/gsd-ns-project/skills/import/SKILL.md": "bc9b615cb141d26f", @@ -362,6 +364,7 @@ "skills/gsd-ns-project/skills/milestone-summary/SKILL.md": "e3636a5e9bbaab1f", "skills/gsd-ns-project/skills/new-milestone/SKILL.md": "bc8989d97f3161ad", "skills/gsd-ns-project/skills/new-project/SKILL.md": "ff0d57491df30624", + "skills/gsd-ns-project/skills/onboard/SKILL.md": "56c34282575926d4", "skills/gsd-ns-project/skills/profile-user/SKILL.md": "0406eed4dbd9560c", "skills/gsd-ns-project/skills/review-backlog/SKILL.md": "d8a150558cc9326a", "skills/gsd-ns-review/SKILL.md": "c5afe33c212947dd", diff --git a/tests/fixtures/golden-install-parity/codebuddy.json b/tests/fixtures/golden-install-parity/codebuddy.json index ebbd59e4a..02e1d09aa 100644 --- a/tests/fixtures/golden-install-parity/codebuddy.json +++ b/tests/fixtures/golden-install-parity/codebuddy.json @@ -62,7 +62,7 @@ "commands/gsd-inbox.md": "eecfcfff57f89975", "commands/gsd-ingest-docs.md": "ab3e58239c23d61d", "commands/gsd-manager.md": "bfe9e261fa918876", - "commands/gsd-map-codebase.md": "9ed09a9a9ce48ffd", + "commands/gsd-map-codebase.md": "5093093d42240325", "commands/gsd-mempalace-capture.md": "e9e00b0602e1d8b8", "commands/gsd-mempalace-recall.md": "6efef238d8c62569", "commands/gsd-milestone-summary.md": "e3a36fbd695a1833", @@ -73,9 +73,10 @@ "commands/gsd-ns-context.md": "cc8954f405ae7916", "commands/gsd-ns-ideate.md": "fd1dd80d705e6e36", "commands/gsd-ns-manage.md": "5cc012d0be1caabb", - "commands/gsd-ns-project.md": "52e30c8d84bb1ee5", + "commands/gsd-ns-project.md": "76672fae61723e55", "commands/gsd-ns-review.md": "a28dbfd4aba5f74b", "commands/gsd-ns-workflow.md": "a1b874be9cf16caf", + "commands/gsd-onboard.md": "63283b90aa671229", "commands/gsd-pause-work.md": "6caa75a7c2b4dd2d", "commands/gsd-phase.md": "e3ca4958ea20a935", "commands/gsd-plan-phase.md": "6de27d54ac191539", @@ -108,7 +109,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "770e0b1c1f08ef47", + "gsd-core/bin/gsd-tools.cjs": "f1a1a58072e7c35d", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "1318633d27964e7a", @@ -157,6 +158,7 @@ "gsd-core/references/gates.md": "bd79c7f90c8cb8d7", "gsd-core/references/git-integration.md": "77bf9dff38b2c9d4", "gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7", + "gsd-core/references/gsd-run-resolver.md": "e71eb728b84641b1", "gsd-core/references/honest-verifier.md": "8815c9fc18c35719", "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", @@ -243,7 +245,7 @@ "gsd-core/templates/milestone.md": "74d2f750ae9f4a9c", "gsd-core/templates/phase-prompt.md": "b811bf951092df2f", "gsd-core/templates/planner-subagent-prompt.md": "ebf29dbb27042370", - "gsd-core/templates/project.md": "ab4af6becca2bb23", + "gsd-core/templates/project.md": "4f311fb1b05b823b", "gsd-core/templates/requirements.md": "a44de4c2f146e473", "gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0", "gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f", @@ -296,7 +298,7 @@ "gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb", "gsd-core/workflows/discuss-phase/templates/context.md": "6cd929e989fe2b0f", "gsd-core/workflows/discuss-phase/templates/discussion-log.md": "1bbd7703f11128e1", - "gsd-core/workflows/do.md": "8f460af710c8f6a4", + "gsd-core/workflows/do.md": "7512dd1892f118a1", "gsd-core/workflows/docs-update.md": "f35922d15b7061c9", "gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04", "gsd-core/workflows/eval-review.md": "f898936e2cfe4130", @@ -313,10 +315,10 @@ "gsd-core/workflows/graduation.md": "47f1594c88c08501", "gsd-core/workflows/health.md": "107e3c72e76d9535", "gsd-core/workflows/help.md": "5d040504b9ab35e3", - "gsd-core/workflows/help/modes/brief.md": "5fe077fbbdb5d9f7", - "gsd-core/workflows/help/modes/default.md": "cc8ef97de634c995", - "gsd-core/workflows/help/modes/full.md": "4c018bcc9ed4e55c", - "gsd-core/workflows/help/modes/topic.md": "6762e41e3291ae24", + "gsd-core/workflows/help/modes/brief.md": "924860e1f07defb0", + "gsd-core/workflows/help/modes/default.md": "08a02976c0c5cc50", + "gsd-core/workflows/help/modes/full.md": "3a19e96b69bcc33e", + "gsd-core/workflows/help/modes/topic.md": "5c160093f3cbf35d", "gsd-core/workflows/import.md": "3d3fa603ceb8bc9f", "gsd-core/workflows/inbox.md": "437f981ef9ae7b26", "gsd-core/workflows/ingest-docs.md": "c859921c811ac11b", @@ -334,6 +336,7 @@ "gsd-core/workflows/next.md": "13fb800f2472d970", "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", "gsd-core/workflows/note.md": "5a99eb396c744619", + "gsd-core/workflows/onboard.md": "6f9e6c0b484271a9", "gsd-core/workflows/pause-work.md": "f2b33bba5593d422", "gsd-core/workflows/plan-milestone-gaps.md": "852f6d7c0c4299dc", "gsd-core/workflows/plan-phase.md": "7e7e9c1c99b83834", @@ -345,7 +348,7 @@ "gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b", "gsd-core/workflows/profile-user.md": "4fa910d15dea5695", "gsd-core/workflows/progress.md": "893aa3c36983f74b", - "gsd-core/workflows/quick.md": "5e7dcdd864513b87", + "gsd-core/workflows/quick.md": "9033dbe58443af36", "gsd-core/workflows/reapply-patches.md": "39050f72601aec89", "gsd-core/workflows/remove-phase.md": "df9a45f0b1880999", "gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c", @@ -438,7 +441,7 @@ "skills/gsd-inbox/SKILL.md": "0eb81a1b2e1835d3", "skills/gsd-ingest-docs/SKILL.md": "2df55e0ec95d57d9", "skills/gsd-manager/SKILL.md": "816db4d4ed618537", - "skills/gsd-map-codebase/SKILL.md": "00622b489483ba69", + "skills/gsd-map-codebase/SKILL.md": "b304ec25e9a95c93", "skills/gsd-mempalace-capture/SKILL.md": "5f10d368835e062c", "skills/gsd-mempalace-recall/SKILL.md": "70c8491f53f4161b", "skills/gsd-milestone-summary/SKILL.md": "9eb030a1ac307fa1", @@ -449,9 +452,10 @@ "skills/gsd-ns-context/SKILL.md": "305b9b6248e8cc95", "skills/gsd-ns-ideate/SKILL.md": "c63a1aed61888b35", "skills/gsd-ns-manage/SKILL.md": "6a5b711b5103c566", - "skills/gsd-ns-project/SKILL.md": "345ea11ccffd21b0", + "skills/gsd-ns-project/SKILL.md": "3711ce904d20ca66", "skills/gsd-ns-review/SKILL.md": "98f1f3b3caf1a279", "skills/gsd-ns-workflow/SKILL.md": "b77682820700300b", + "skills/gsd-onboard/SKILL.md": "6ad789a03124cd4f", "skills/gsd-pause-work/SKILL.md": "e2de20b7539e78d4", "skills/gsd-phase/SKILL.md": "f87211f779315ced", "skills/gsd-plan-phase/SKILL.md": "6134e837750034ab", diff --git a/tests/fixtures/golden-install-parity/codex.json b/tests/fixtures/golden-install-parity/codex.json index afc1a4693..a956fe3b2 100644 --- a/tests/fixtures/golden-install-parity/codex.json +++ b/tests/fixtures/golden-install-parity/codex.json @@ -73,7 +73,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "770e0b1c1f08ef47", + "gsd-core/bin/gsd-tools.cjs": "f1a1a58072e7c35d", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "1318633d27964e7a", @@ -122,6 +122,7 @@ "gsd-core/references/gates.md": "11fd2bdf27df57a5", "gsd-core/references/git-integration.md": "94715b69448bb818", "gsd-core/references/git-planning-commit.md": "5aad099c51135a0f", + "gsd-core/references/gsd-run-resolver.md": "e71eb728b84641b1", "gsd-core/references/honest-verifier.md": "8815c9fc18c35719", "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", @@ -208,7 +209,7 @@ "gsd-core/templates/milestone.md": "74d2f750ae9f4a9c", "gsd-core/templates/phase-prompt.md": "56c67fdc5d31b556", "gsd-core/templates/planner-subagent-prompt.md": "a48a5a2ebd5a9f47", - "gsd-core/templates/project.md": "034a6501f348c5a5", + "gsd-core/templates/project.md": "c6005bd88a346a3e", "gsd-core/templates/requirements.md": "a44de4c2f146e473", "gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0", "gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f", @@ -261,7 +262,7 @@ "gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb", "gsd-core/workflows/discuss-phase/templates/context.md": "9804674fccada405", "gsd-core/workflows/discuss-phase/templates/discussion-log.md": "2b56ec2868cbddb4", - "gsd-core/workflows/do.md": "8f3e77a9486bc57b", + "gsd-core/workflows/do.md": "feec00355f896a3d", "gsd-core/workflows/docs-update.md": "e255317df939e302", "gsd-core/workflows/edit-phase.md": "e592a4d85ce5380f", "gsd-core/workflows/eval-review.md": "63d0d0670b54c244", @@ -278,10 +279,10 @@ "gsd-core/workflows/graduation.md": "47f1594c88c08501", "gsd-core/workflows/health.md": "5b835fc606fd3e9b", "gsd-core/workflows/help.md": "08e1349950c5602a", - "gsd-core/workflows/help/modes/brief.md": "becceb85b25dca89", - "gsd-core/workflows/help/modes/default.md": "18c7dc50066f5cd6", - "gsd-core/workflows/help/modes/full.md": "8a7854a34df89bd8", - "gsd-core/workflows/help/modes/topic.md": "303613ba8b123b9f", + "gsd-core/workflows/help/modes/brief.md": "da44b130d1afe556", + "gsd-core/workflows/help/modes/default.md": "4bb3082d28026eea", + "gsd-core/workflows/help/modes/full.md": "01a1ec54df54da6e", + "gsd-core/workflows/help/modes/topic.md": "b7c7e4a8800bc3ea", "gsd-core/workflows/import.md": "cb3c8f9d09edb434", "gsd-core/workflows/inbox.md": "61b8b10e7a74b2e9", "gsd-core/workflows/ingest-docs.md": "3cb763e0687d7ffa", @@ -299,6 +300,7 @@ "gsd-core/workflows/next.md": "7097a9607a981086", "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", "gsd-core/workflows/note.md": "664da466ab989f9d", + "gsd-core/workflows/onboard.md": "83c40ba7055b8b24", "gsd-core/workflows/pause-work.md": "a217770ecafcb2e0", "gsd-core/workflows/plan-milestone-gaps.md": "73d46f77c50a0690", "gsd-core/workflows/plan-phase.md": "34c4d4c7ad7fdf36", @@ -310,7 +312,7 @@ "gsd-core/workflows/pr-branch.md": "d13e1cc81de40896", "gsd-core/workflows/profile-user.md": "05828c8cc61ef384", "gsd-core/workflows/progress.md": "7bedc431bb55edb8", - "gsd-core/workflows/quick.md": "85fb48e52f0b3725", + "gsd-core/workflows/quick.md": "3e7686705da2af19", "gsd-core/workflows/reapply-patches.md": "26297b84736e66a4", "gsd-core/workflows/remove-phase.md": "9ee0fddd11a0d9d4", "gsd-core/workflows/remove-workspace.md": "19d7465aaa50cb62", @@ -383,7 +385,7 @@ "skills/gsd-inbox/SKILL.md": "1315033595643485", "skills/gsd-ingest-docs/SKILL.md": "5bd2838bf0b6dc1b", "skills/gsd-manager/SKILL.md": "cb1cf56f5d3f66d6", - "skills/gsd-map-codebase/SKILL.md": "25d0b3adfc8b75dc", + "skills/gsd-map-codebase/SKILL.md": "cd27dc028718426b", "skills/gsd-mempalace-capture/SKILL.md": "1a7fac4d53f607ab", "skills/gsd-mempalace-recall/SKILL.md": "7205b02250e89f25", "skills/gsd-milestone-summary/SKILL.md": "af84ecb400f23556", @@ -394,9 +396,10 @@ "skills/gsd-ns-context/SKILL.md": "9b496da79789b3f9", "skills/gsd-ns-ideate/SKILL.md": "84ca1cde06110981", "skills/gsd-ns-manage/SKILL.md": "909dafa0cd19ba5a", - "skills/gsd-ns-project/SKILL.md": "fb8ad6c2223be3c3", + "skills/gsd-ns-project/SKILL.md": "936b6998d42520ac", "skills/gsd-ns-review/SKILL.md": "022253010db0e072", "skills/gsd-ns-workflow/SKILL.md": "cba075bc819b539e", + "skills/gsd-onboard/SKILL.md": "f42fc2edebeda671", "skills/gsd-pause-work/SKILL.md": "b379469eed78a196", "skills/gsd-phase/SKILL.md": "25477edc97a90c91", "skills/gsd-plan-phase/SKILL.md": "19ead1acb151a868", diff --git a/tests/fixtures/golden-install-parity/copilot.json b/tests/fixtures/golden-install-parity/copilot.json index 3902c96f1..ee8cb176b 100644 --- a/tests/fixtures/golden-install-parity/copilot.json +++ b/tests/fixtures/golden-install-parity/copilot.json @@ -39,7 +39,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74", - "gsd-core/bin/gsd-tools.cjs": "747defe620cb8ae6", + "gsd-core/bin/gsd-tools.cjs": "f2dd7a6993dc12f1", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "1318633d27964e7a", @@ -88,6 +88,7 @@ "gsd-core/references/gates.md": "7dc9fd3a3d6217c6", "gsd-core/references/git-integration.md": "657a93c539c16cab", "gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7", + "gsd-core/references/gsd-run-resolver.md": "fca7e30efb0afe3f", "gsd-core/references/honest-verifier.md": "08b1e82f59c18078", "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", @@ -174,7 +175,7 @@ "gsd-core/templates/milestone.md": "74d2f750ae9f4a9c", "gsd-core/templates/phase-prompt.md": "e2b07376944c84e8", "gsd-core/templates/planner-subagent-prompt.md": "6c9f1b23ee3dc05f", - "gsd-core/templates/project.md": "e4a3b331be081e62", + "gsd-core/templates/project.md": "fae14114f2b79b10", "gsd-core/templates/requirements.md": "a44de4c2f146e473", "gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0", "gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f", @@ -227,7 +228,7 @@ "gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb", "gsd-core/workflows/discuss-phase/templates/context.md": "9804674fccada405", "gsd-core/workflows/discuss-phase/templates/discussion-log.md": "2b56ec2868cbddb4", - "gsd-core/workflows/do.md": "988a20dab90c60c5", + "gsd-core/workflows/do.md": "9b46119803df09a6", "gsd-core/workflows/docs-update.md": "9292cfa3c52c434e", "gsd-core/workflows/edit-phase.md": "8667c28b22b1599f", "gsd-core/workflows/eval-review.md": "81c8e72ba3862856", @@ -244,10 +245,10 @@ "gsd-core/workflows/graduation.md": "f013efc29096faf0", "gsd-core/workflows/health.md": "f343fd32a0e398e4", "gsd-core/workflows/help.md": "5d040504b9ab35e3", - "gsd-core/workflows/help/modes/brief.md": "2e923b3ed635d495", - "gsd-core/workflows/help/modes/default.md": "b544113eb9aa54fa", - "gsd-core/workflows/help/modes/full.md": "0e9381367d6257bd", - "gsd-core/workflows/help/modes/topic.md": "19a279fe91922d3b", + "gsd-core/workflows/help/modes/brief.md": "fa2675516b40e2e3", + "gsd-core/workflows/help/modes/default.md": "be05e56b2c5ee2c0", + "gsd-core/workflows/help/modes/full.md": "ce88d4eb6b301e09", + "gsd-core/workflows/help/modes/topic.md": "0bf9ab39d7044d69", "gsd-core/workflows/import.md": "f4fa65e332b00f7e", "gsd-core/workflows/inbox.md": "a448220c548f27bc", "gsd-core/workflows/ingest-docs.md": "52be14d1a0713731", @@ -265,6 +266,7 @@ "gsd-core/workflows/next.md": "81c08abb34b2da88", "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", "gsd-core/workflows/note.md": "4a5ee74cf2fc1f54", + "gsd-core/workflows/onboard.md": "a62602c6f3fd538a", "gsd-core/workflows/pause-work.md": "9ce66367be6c40db", "gsd-core/workflows/plan-milestone-gaps.md": "5cf589802d08bdf3", "gsd-core/workflows/plan-phase.md": "909457cfb58ce634", @@ -276,7 +278,7 @@ "gsd-core/workflows/pr-branch.md": "2833905f119b5722", "gsd-core/workflows/profile-user.md": "5cc032206c99ef71", "gsd-core/workflows/progress.md": "6b9a84a43dc55af5", - "gsd-core/workflows/quick.md": "89c34e07f1bf6f53", + "gsd-core/workflows/quick.md": "7ddfe17f048541ec", "gsd-core/workflows/reapply-patches.md": "8fd59e24b486f180", "gsd-core/workflows/remove-phase.md": "e262654e319d1bc4", "gsd-core/workflows/remove-workspace.md": "ceddfeef5f2d6754", @@ -348,7 +350,7 @@ "skills/gsd-inbox/SKILL.md": "cb9ed3865b67c04f", "skills/gsd-ingest-docs/SKILL.md": "10500de29525f6cf", "skills/gsd-manager/SKILL.md": "9946975bec15e737", - "skills/gsd-map-codebase/SKILL.md": "75806f8189d8af49", + "skills/gsd-map-codebase/SKILL.md": "39e7fd6bcc561f9d", "skills/gsd-mempalace-capture/SKILL.md": "f4511f69c14befb7", "skills/gsd-mempalace-recall/SKILL.md": "89523db66f834b2f", "skills/gsd-milestone-summary/SKILL.md": "5ca1dabfe0cd85f0", @@ -359,9 +361,10 @@ "skills/gsd-ns-context/SKILL.md": "d61edde87ae53105", "skills/gsd-ns-ideate/SKILL.md": "ca62cdf78ebe3585", "skills/gsd-ns-manage/SKILL.md": "9b1b7f6d9b877fcd", - "skills/gsd-ns-project/SKILL.md": "f1a666fb76527e6b", + "skills/gsd-ns-project/SKILL.md": "597b8dde8702c22d", "skills/gsd-ns-review/SKILL.md": "6405d0b1cf3f245d", "skills/gsd-ns-workflow/SKILL.md": "243287d21663ba87", + "skills/gsd-onboard/SKILL.md": "4d325f3033df2d37", "skills/gsd-pause-work/SKILL.md": "4f0caa008a8001ff", "skills/gsd-phase/SKILL.md": "d38c7f9b1d0d2360", "skills/gsd-plan-phase/SKILL.md": "d257bb9b1786039d", diff --git a/tests/fixtures/golden-install-parity/cursor.json b/tests/fixtures/golden-install-parity/cursor.json index 6aedd1fc9..fda7e51db 100644 --- a/tests/fixtures/golden-install-parity/cursor.json +++ b/tests/fixtures/golden-install-parity/cursor.json @@ -62,7 +62,7 @@ "commands/gsd-inbox.md": "e6bcee3f863d20db", "commands/gsd-ingest-docs.md": "9e7f8757b9408c01", "commands/gsd-manager.md": "f300b708a5487c76", - "commands/gsd-map-codebase.md": "cf50f4a600119c8a", + "commands/gsd-map-codebase.md": "12f705917eb202fb", "commands/gsd-mempalace-capture.md": "b30bce634ac3136c", "commands/gsd-mempalace-recall.md": "08c910285469a394", "commands/gsd-milestone-summary.md": "adfad2cf43fc5aad", @@ -73,9 +73,10 @@ "commands/gsd-ns-context.md": "b551dcc549c6a23f", "commands/gsd-ns-ideate.md": "32f561de74b1bc7b", "commands/gsd-ns-manage.md": "24445a84bd817c5b", - "commands/gsd-ns-project.md": "b39b509ac4161d28", + "commands/gsd-ns-project.md": "525f84d73f5c51e9", "commands/gsd-ns-review.md": "c8550463fbab5e76", "commands/gsd-ns-workflow.md": "2c1f1fde3c46bb40", + "commands/gsd-onboard.md": "d9e52f558fc2b90f", "commands/gsd-pause-work.md": "59630f05f95fff68", "commands/gsd-phase.md": "9a073dcd0f934f90", "commands/gsd-plan-phase.md": "57de92131fcb16b8", @@ -108,7 +109,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "2525f1ae8b086828", - "gsd-core/bin/gsd-tools.cjs": "887e2cb9ebdb609d", + "gsd-core/bin/gsd-tools.cjs": "2493822b0ae27980", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "1318633d27964e7a", @@ -157,6 +158,7 @@ "gsd-core/references/gates.md": "7dc9fd3a3d6217c6", "gsd-core/references/git-integration.md": "9840f521ad6612b5", "gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7", + "gsd-core/references/gsd-run-resolver.md": "e71eb728b84641b1", "gsd-core/references/honest-verifier.md": "8815c9fc18c35719", "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", @@ -243,7 +245,7 @@ "gsd-core/templates/milestone.md": "74d2f750ae9f4a9c", "gsd-core/templates/phase-prompt.md": "213ccd947451ff2b", "gsd-core/templates/planner-subagent-prompt.md": "6c9f1b23ee3dc05f", - "gsd-core/templates/project.md": "1f687c7a88381080", + "gsd-core/templates/project.md": "ae1f68db042c2522", "gsd-core/templates/requirements.md": "a44de4c2f146e473", "gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0", "gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f", @@ -296,7 +298,7 @@ "gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb", "gsd-core/workflows/discuss-phase/templates/context.md": "6cd929e989fe2b0f", "gsd-core/workflows/discuss-phase/templates/discussion-log.md": "cd5d01c9ef84ab73", - "gsd-core/workflows/do.md": "dc0ddd1f8231d26b", + "gsd-core/workflows/do.md": "9da6a73980ef2a71", "gsd-core/workflows/docs-update.md": "e86d7d7e2e3dac6d", "gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a", "gsd-core/workflows/eval-review.md": "a86279dd98dd5c03", @@ -313,10 +315,10 @@ "gsd-core/workflows/graduation.md": "53a4a6fa3b4e6613", "gsd-core/workflows/health.md": "e4d770484b5e7496", "gsd-core/workflows/help.md": "08e1349950c5602a", - "gsd-core/workflows/help/modes/brief.md": "2e923b3ed635d495", - "gsd-core/workflows/help/modes/default.md": "37c5149aae232e61", - "gsd-core/workflows/help/modes/full.md": "ed31d1afcbd3591f", - "gsd-core/workflows/help/modes/topic.md": "177d351f7465787d", + "gsd-core/workflows/help/modes/brief.md": "fa2675516b40e2e3", + "gsd-core/workflows/help/modes/default.md": "b88ff431fffe40ae", + "gsd-core/workflows/help/modes/full.md": "a7a42d4340e8e00b", + "gsd-core/workflows/help/modes/topic.md": "cee80e0adfa3b06c", "gsd-core/workflows/import.md": "5d5fb8e51f6a243a", "gsd-core/workflows/inbox.md": "797c287852eb8957", "gsd-core/workflows/ingest-docs.md": "c6759dbf17e79293", @@ -334,6 +336,7 @@ "gsd-core/workflows/next.md": "174356dcfd7c6c32", "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", "gsd-core/workflows/note.md": "1c1e466c764e3deb", + "gsd-core/workflows/onboard.md": "20c28136423d40ac", "gsd-core/workflows/pause-work.md": "5716362557f44ce4", "gsd-core/workflows/plan-milestone-gaps.md": "1b43d12812f7bc1e", "gsd-core/workflows/plan-phase.md": "d10fdacf2b9d850e", @@ -345,7 +348,7 @@ "gsd-core/workflows/pr-branch.md": "c67d90c65da47168", "gsd-core/workflows/profile-user.md": "8c943983241260b5", "gsd-core/workflows/progress.md": "65aabee5e8a6dd82", - "gsd-core/workflows/quick.md": "db98fdbe79d68ffc", + "gsd-core/workflows/quick.md": "762256cf6d177c06", "gsd-core/workflows/reapply-patches.md": "ba9406b60f2c4041", "gsd-core/workflows/remove-phase.md": "ada8a0546c686483", "gsd-core/workflows/remove-workspace.md": "433affcd1a200826", @@ -418,7 +421,7 @@ "skills/gsd-inbox/SKILL.md": "f9b4ff17883ee7e4", "skills/gsd-ingest-docs/SKILL.md": "01ed9183a00252ee", "skills/gsd-manager/SKILL.md": "62cd9f671a39e8a4", - "skills/gsd-map-codebase/SKILL.md": "f529da27d3a30b09", + "skills/gsd-map-codebase/SKILL.md": "383559aca64201e8", "skills/gsd-mempalace-capture/SKILL.md": "b308823dee547c65", "skills/gsd-mempalace-recall/SKILL.md": "56f3037091a8bd9a", "skills/gsd-milestone-summary/SKILL.md": "c5dbbc8edb97a7f7", @@ -429,9 +432,10 @@ "skills/gsd-ns-context/SKILL.md": "dcf5711653d57354", "skills/gsd-ns-ideate/SKILL.md": "0114f0bc5501c1e3", "skills/gsd-ns-manage/SKILL.md": "f3232d05d5263f5b", - "skills/gsd-ns-project/SKILL.md": "9f737a544e8d3f31", + "skills/gsd-ns-project/SKILL.md": "81d802dd3ee2e1ff", "skills/gsd-ns-review/SKILL.md": "8c686e92f293bfdd", "skills/gsd-ns-workflow/SKILL.md": "c379192bd17d47d7", + "skills/gsd-onboard/SKILL.md": "cae1f9382469fa00", "skills/gsd-pause-work/SKILL.md": "10e7531a2cb392ad", "skills/gsd-phase/SKILL.md": "1a2c9b64c1af7ffc", "skills/gsd-plan-phase/SKILL.md": "0784da05d41dcfe7", diff --git a/tests/fixtures/golden-install-parity/hermes.json b/tests/fixtures/golden-install-parity/hermes.json index efa94f857..6ded77059 100644 --- a/tests/fixtures/golden-install-parity/hermes.json +++ b/tests/fixtures/golden-install-parity/hermes.json @@ -38,7 +38,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "3a3409215044af9f", - "gsd-core/bin/gsd-tools.cjs": "cbdd5da1b973c6ae", + "gsd-core/bin/gsd-tools.cjs": "3d032fe88d2dc09b", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "1318633d27964e7a", @@ -87,6 +87,7 @@ "gsd-core/references/gates.md": "7dc9fd3a3d6217c6", "gsd-core/references/git-integration.md": "12d23fcbaa7fcf06", "gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7", + "gsd-core/references/gsd-run-resolver.md": "769d1472c989cd54", "gsd-core/references/honest-verifier.md": "8815c9fc18c35719", "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", @@ -173,7 +174,7 @@ "gsd-core/templates/milestone.md": "74d2f750ae9f4a9c", "gsd-core/templates/phase-prompt.md": "213ccd947451ff2b", "gsd-core/templates/planner-subagent-prompt.md": "6c9f1b23ee3dc05f", - "gsd-core/templates/project.md": "1f687c7a88381080", + "gsd-core/templates/project.md": "ae1f68db042c2522", "gsd-core/templates/requirements.md": "a44de4c2f146e473", "gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0", "gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f", @@ -226,7 +227,7 @@ "gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb", "gsd-core/workflows/discuss-phase/templates/context.md": "6cd929e989fe2b0f", "gsd-core/workflows/discuss-phase/templates/discussion-log.md": "1bbd7703f11128e1", - "gsd-core/workflows/do.md": "045e70c238db6faf", + "gsd-core/workflows/do.md": "e37540cc874fb782", "gsd-core/workflows/docs-update.md": "4d6c06e611d83b6d", "gsd-core/workflows/edit-phase.md": "7f27003f20e88fb8", "gsd-core/workflows/eval-review.md": "f510e5762212dc6f", @@ -243,10 +244,10 @@ "gsd-core/workflows/graduation.md": "d1fd52bbe41dcf34", "gsd-core/workflows/health.md": "c622a5ad0d347d30", "gsd-core/workflows/help.md": "5d040504b9ab35e3", - "gsd-core/workflows/help/modes/brief.md": "2e923b3ed635d495", - "gsd-core/workflows/help/modes/default.md": "bb6876c976996dbd", - "gsd-core/workflows/help/modes/full.md": "fdab4b3690bdd11d", - "gsd-core/workflows/help/modes/topic.md": "899d7168a485297f", + "gsd-core/workflows/help/modes/brief.md": "fa2675516b40e2e3", + "gsd-core/workflows/help/modes/default.md": "8874dac94eb68ae6", + "gsd-core/workflows/help/modes/full.md": "62779da88565d65a", + "gsd-core/workflows/help/modes/topic.md": "6e42db16f1568be9", "gsd-core/workflows/import.md": "a441445515bc2dd5", "gsd-core/workflows/inbox.md": "91aac6360e1a8672", "gsd-core/workflows/ingest-docs.md": "ad496a4c423f1ab7", @@ -264,6 +265,7 @@ "gsd-core/workflows/next.md": "8e1fa29751d96564", "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", "gsd-core/workflows/note.md": "42b66686b2c102cb", + "gsd-core/workflows/onboard.md": "3c50ed1f1fd07619", "gsd-core/workflows/pause-work.md": "ae2d5789a95f70fe", "gsd-core/workflows/plan-milestone-gaps.md": "c86cdc1964256b98", "gsd-core/workflows/plan-phase.md": "e30dcfb9615f19da", @@ -275,7 +277,7 @@ "gsd-core/workflows/pr-branch.md": "ecabd55e4eabf229", "gsd-core/workflows/profile-user.md": "de5030437226cf2c", "gsd-core/workflows/progress.md": "f18db000584d9cb1", - "gsd-core/workflows/quick.md": "66b26d6fe2bd4d71", + "gsd-core/workflows/quick.md": "5044ce4e7512e174", "gsd-core/workflows/reapply-patches.md": "158083a310859594", "gsd-core/workflows/remove-phase.md": "fce799aae3ab2715", "gsd-core/workflows/remove-workspace.md": "8facde381657dd71", @@ -346,7 +348,7 @@ "skills/gsd/gsd-ns-context/skills/docs-update/SKILL.md": "b059b3045d5daf5c", "skills/gsd/gsd-ns-context/skills/extract-learnings/SKILL.md": "36e59d6e514dcaf0", "skills/gsd/gsd-ns-context/skills/graphify/SKILL.md": "169eefd42c444cde", - "skills/gsd/gsd-ns-context/skills/map-codebase/SKILL.md": "6062ccca1a14b827", + "skills/gsd/gsd-ns-context/skills/map-codebase/SKILL.md": "9d36eee52586bb34", "skills/gsd/gsd-ns-context/skills/mempalace-capture/SKILL.md": "3fa619f73834c436", "skills/gsd/gsd-ns-context/skills/mempalace-recall/SKILL.md": "5d06922e0d7ab7d9", "skills/gsd/gsd-ns-ideate/SKILL.md": "b36bb4700ed6723a", @@ -374,7 +376,7 @@ "skills/gsd/gsd-ns-manage/skills/update/SKILL.md": "433a236998c305b0", "skills/gsd/gsd-ns-manage/skills/workspace/SKILL.md": "d655afc6f16565ed", "skills/gsd/gsd-ns-manage/skills/workstreams/SKILL.md": "6893312eaf14dab0", - "skills/gsd/gsd-ns-project/SKILL.md": "3ff43fdc0e4a6d02", + "skills/gsd/gsd-ns-project/SKILL.md": "6b3af15e7c4441c9", "skills/gsd/gsd-ns-project/skills/audit-milestone/SKILL.md": "de0556f07d899e50", "skills/gsd/gsd-ns-project/skills/complete-milestone/SKILL.md": "180cccc09bf99444", "skills/gsd/gsd-ns-project/skills/import/SKILL.md": "bf781c7a0d83c2cd", @@ -382,6 +384,7 @@ "skills/gsd/gsd-ns-project/skills/milestone-summary/SKILL.md": "c4bda5737f1137f8", "skills/gsd/gsd-ns-project/skills/new-milestone/SKILL.md": "47586c1fc8aa0645", "skills/gsd/gsd-ns-project/skills/new-project/SKILL.md": "8f22ba75717b3c88", + "skills/gsd/gsd-ns-project/skills/onboard/SKILL.md": "1d8d06ac1f27dacd", "skills/gsd/gsd-ns-project/skills/profile-user/SKILL.md": "5a8e01bc7b873a20", "skills/gsd/gsd-ns-project/skills/review-backlog/SKILL.md": "f3ef97da4e9b1d4c", "skills/gsd/gsd-ns-review/SKILL.md": "23fbadeca640e2bd", diff --git a/tests/fixtures/golden-install-parity/kilo.json b/tests/fixtures/golden-install-parity/kilo.json index 4e6123330..c3073c72b 100644 --- a/tests/fixtures/golden-install-parity/kilo.json +++ b/tests/fixtures/golden-install-parity/kilo.json @@ -62,7 +62,7 @@ "command/gsd-inbox.md": "1429fc7f556337c3", "command/gsd-ingest-docs.md": "e655ecf1d4433ba9", "command/gsd-manager.md": "1cdc133079833552", - "command/gsd-map-codebase.md": "186c8f79319abc1c", + "command/gsd-map-codebase.md": "704cb63214f51523", "command/gsd-mempalace-capture.md": "a94ca275cbe7134c", "command/gsd-mempalace-recall.md": "d358cdd89c935877", "command/gsd-milestone-summary.md": "4ab29b777f62f0bb", @@ -73,9 +73,10 @@ "command/gsd-ns-context.md": "9a2b55b64f2e1e4b", "command/gsd-ns-ideate.md": "27e7fcdc69eeeb5f", "command/gsd-ns-manage.md": "eded59052ca4b240", - "command/gsd-ns-project.md": "91417cf7b76581dd", + "command/gsd-ns-project.md": "c56395edb9bc7467", "command/gsd-ns-review.md": "61fe33e28eb1b961", "command/gsd-ns-workflow.md": "13c1ab91080c0cc3", + "command/gsd-onboard.md": "3e87a21c9c04f0d7", "command/gsd-pause-work.md": "04e993b1c9f8322b", "command/gsd-phase.md": "8f0e98dc6c223229", "command/gsd-plan-phase.md": "b37e9a61f946c975", @@ -108,7 +109,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "770e0b1c1f08ef47", + "gsd-core/bin/gsd-tools.cjs": "f1a1a58072e7c35d", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "1318633d27964e7a", @@ -157,6 +158,7 @@ "gsd-core/references/gates.md": "7dc9fd3a3d6217c6", "gsd-core/references/git-integration.md": "fbdf814a3af9c051", "gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7", + "gsd-core/references/gsd-run-resolver.md": "e71eb728b84641b1", "gsd-core/references/honest-verifier.md": "8815c9fc18c35719", "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", @@ -243,7 +245,7 @@ "gsd-core/templates/milestone.md": "74d2f750ae9f4a9c", "gsd-core/templates/phase-prompt.md": "2e6fd96c2cc26470", "gsd-core/templates/planner-subagent-prompt.md": "6c9f1b23ee3dc05f", - "gsd-core/templates/project.md": "e4a3b331be081e62", + "gsd-core/templates/project.md": "fae14114f2b79b10", "gsd-core/templates/requirements.md": "a44de4c2f146e473", "gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0", "gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f", @@ -296,7 +298,7 @@ "gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb", "gsd-core/workflows/discuss-phase/templates/context.md": "9804674fccada405", "gsd-core/workflows/discuss-phase/templates/discussion-log.md": "04e4feee14772275", - "gsd-core/workflows/do.md": "1e6102574016b150", + "gsd-core/workflows/do.md": "d9a889717e390f8d", "gsd-core/workflows/docs-update.md": "79afaaf19fd527cc", "gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a", "gsd-core/workflows/eval-review.md": "926eda8bbee28b23", @@ -313,10 +315,10 @@ "gsd-core/workflows/graduation.md": "5cc8638dabcbfd40", "gsd-core/workflows/health.md": "064b3668d5d9569a", "gsd-core/workflows/help.md": "5d040504b9ab35e3", - "gsd-core/workflows/help/modes/brief.md": "2e923b3ed635d495", - "gsd-core/workflows/help/modes/default.md": "b544113eb9aa54fa", - "gsd-core/workflows/help/modes/full.md": "051f9633f45558b4", - "gsd-core/workflows/help/modes/topic.md": "19a279fe91922d3b", + "gsd-core/workflows/help/modes/brief.md": "fa2675516b40e2e3", + "gsd-core/workflows/help/modes/default.md": "be05e56b2c5ee2c0", + "gsd-core/workflows/help/modes/full.md": "775f766dfafb72f9", + "gsd-core/workflows/help/modes/topic.md": "0bf9ab39d7044d69", "gsd-core/workflows/import.md": "f8bbe6f2c0e08a78", "gsd-core/workflows/inbox.md": "a073396097c89c01", "gsd-core/workflows/ingest-docs.md": "29c1fe28bc93b345", @@ -334,6 +336,7 @@ "gsd-core/workflows/next.md": "888297a0b21888ea", "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", "gsd-core/workflows/note.md": "f8c2842a2217f776", + "gsd-core/workflows/onboard.md": "78b288089305dc46", "gsd-core/workflows/pause-work.md": "a6e5336c409fdc8b", "gsd-core/workflows/plan-milestone-gaps.md": "1b43d12812f7bc1e", "gsd-core/workflows/plan-phase.md": "6f5eacc08e5d9d2a", @@ -345,7 +348,7 @@ "gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd", "gsd-core/workflows/profile-user.md": "203ebe3f8f3876a8", "gsd-core/workflows/progress.md": "9381c59676ccb937", - "gsd-core/workflows/quick.md": "11ba83f6350462fb", + "gsd-core/workflows/quick.md": "fd7461e5a92fe450", "gsd-core/workflows/reapply-patches.md": "becf9728cdb124c4", "gsd-core/workflows/remove-phase.md": "ada8a0546c686483", "gsd-core/workflows/remove-workspace.md": "fc83f362a2d0a1b7", @@ -439,7 +442,7 @@ "skills/gsd-inbox/SKILL.md": "34370c6138415209", "skills/gsd-ingest-docs/SKILL.md": "f049f06a6ec7da43", "skills/gsd-manager/SKILL.md": "a37ecdbe32952262", - "skills/gsd-map-codebase/SKILL.md": "e8c01704e8ada1d2", + "skills/gsd-map-codebase/SKILL.md": "e06d6ac1b7632cbe", "skills/gsd-mempalace-capture/SKILL.md": "f960b7b1a8fdf89b", "skills/gsd-mempalace-recall/SKILL.md": "2091c48e6e986e28", "skills/gsd-milestone-summary/SKILL.md": "da645171ca2299d8", @@ -450,9 +453,10 @@ "skills/gsd-ns-context/SKILL.md": "110fb2227bcde535", "skills/gsd-ns-ideate/SKILL.md": "d093d59a8519decc", "skills/gsd-ns-manage/SKILL.md": "2756981de613022e", - "skills/gsd-ns-project/SKILL.md": "757976d48e8b7a26", + "skills/gsd-ns-project/SKILL.md": "56d3d8a321d267f8", "skills/gsd-ns-review/SKILL.md": "680bdd33c882b049", "skills/gsd-ns-workflow/SKILL.md": "579b7f0dac74e1fb", + "skills/gsd-onboard/SKILL.md": "0e3cc902f44b41b8", "skills/gsd-pause-work/SKILL.md": "34366b18a392a717", "skills/gsd-phase/SKILL.md": "64a241d4f8665aa2", "skills/gsd-plan-phase/SKILL.md": "c73cba04a26f0bfe", diff --git a/tests/fixtures/golden-install-parity/kimi.json b/tests/fixtures/golden-install-parity/kimi.json index 194453727..89be6cc93 100644 --- a/tests/fixtures/golden-install-parity/kimi.json +++ b/tests/fixtures/golden-install-parity/kimi.json @@ -74,7 +74,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "770e0b1c1f08ef47", + "gsd-core/bin/gsd-tools.cjs": "f1a1a58072e7c35d", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "1318633d27964e7a", @@ -123,6 +123,7 @@ "gsd-core/references/gates.md": "bd79c7f90c8cb8d7", "gsd-core/references/git-integration.md": "77bf9dff38b2c9d4", "gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7", + "gsd-core/references/gsd-run-resolver.md": "e71eb728b84641b1", "gsd-core/references/honest-verifier.md": "8815c9fc18c35719", "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", @@ -209,7 +210,7 @@ "gsd-core/templates/milestone.md": "74d2f750ae9f4a9c", "gsd-core/templates/phase-prompt.md": "b811bf951092df2f", "gsd-core/templates/planner-subagent-prompt.md": "ebf29dbb27042370", - "gsd-core/templates/project.md": "ab4af6becca2bb23", + "gsd-core/templates/project.md": "4f311fb1b05b823b", "gsd-core/templates/requirements.md": "a44de4c2f146e473", "gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0", "gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f", @@ -262,7 +263,7 @@ "gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb", "gsd-core/workflows/discuss-phase/templates/context.md": "6cd929e989fe2b0f", "gsd-core/workflows/discuss-phase/templates/discussion-log.md": "1bbd7703f11128e1", - "gsd-core/workflows/do.md": "8f460af710c8f6a4", + "gsd-core/workflows/do.md": "7512dd1892f118a1", "gsd-core/workflows/docs-update.md": "f35922d15b7061c9", "gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04", "gsd-core/workflows/eval-review.md": "f898936e2cfe4130", @@ -279,10 +280,10 @@ "gsd-core/workflows/graduation.md": "47f1594c88c08501", "gsd-core/workflows/health.md": "107e3c72e76d9535", "gsd-core/workflows/help.md": "5d040504b9ab35e3", - "gsd-core/workflows/help/modes/brief.md": "5fe077fbbdb5d9f7", - "gsd-core/workflows/help/modes/default.md": "cc8ef97de634c995", - "gsd-core/workflows/help/modes/full.md": "94c18706768e5a41", - "gsd-core/workflows/help/modes/topic.md": "6762e41e3291ae24", + "gsd-core/workflows/help/modes/brief.md": "924860e1f07defb0", + "gsd-core/workflows/help/modes/default.md": "08a02976c0c5cc50", + "gsd-core/workflows/help/modes/full.md": "ee47582c5d8d94a7", + "gsd-core/workflows/help/modes/topic.md": "5c160093f3cbf35d", "gsd-core/workflows/import.md": "3d3fa603ceb8bc9f", "gsd-core/workflows/inbox.md": "437f981ef9ae7b26", "gsd-core/workflows/ingest-docs.md": "c859921c811ac11b", @@ -300,6 +301,7 @@ "gsd-core/workflows/next.md": "13fb800f2472d970", "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", "gsd-core/workflows/note.md": "5a99eb396c744619", + "gsd-core/workflows/onboard.md": "6f9e6c0b484271a9", "gsd-core/workflows/pause-work.md": "f2b33bba5593d422", "gsd-core/workflows/plan-milestone-gaps.md": "852f6d7c0c4299dc", "gsd-core/workflows/plan-phase.md": "bfd41e5f867bd751", @@ -311,7 +313,7 @@ "gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b", "gsd-core/workflows/profile-user.md": "5abfae83739fa978", "gsd-core/workflows/progress.md": "893aa3c36983f74b", - "gsd-core/workflows/quick.md": "d722803b72217377", + "gsd-core/workflows/quick.md": "e265e01cfe117215", "gsd-core/workflows/reapply-patches.md": "39050f72601aec89", "gsd-core/workflows/remove-phase.md": "df9a45f0b1880999", "gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c", @@ -382,7 +384,7 @@ "skills/gsd-inbox/SKILL.md": "955779a4b1df1c33", "skills/gsd-ingest-docs/SKILL.md": "6366644e623ae9e4", "skills/gsd-manager/SKILL.md": "8a11a1beb8e35a9c", - "skills/gsd-map-codebase/SKILL.md": "55dade835b0f4954", + "skills/gsd-map-codebase/SKILL.md": "73ab0afb2398de2c", "skills/gsd-mempalace-capture/SKILL.md": "64a2ec493092d26e", "skills/gsd-mempalace-recall/SKILL.md": "c6e0195e55cd934c", "skills/gsd-milestone-summary/SKILL.md": "3ea93ccaa0d4d967", @@ -393,9 +395,10 @@ "skills/gsd-ns-context/SKILL.md": "89f5bffe6702c0c5", "skills/gsd-ns-ideate/SKILL.md": "e0c0420f6878906a", "skills/gsd-ns-manage/SKILL.md": "b7fbe29e45e75f54", - "skills/gsd-ns-project/SKILL.md": "1292d4a6a4280ce9", + "skills/gsd-ns-project/SKILL.md": "fcb8a93ade9ecc72", "skills/gsd-ns-review/SKILL.md": "2ff62330379f947f", "skills/gsd-ns-workflow/SKILL.md": "86272fd655c206bc", + "skills/gsd-onboard/SKILL.md": "fd66b3c5b9c6a871", "skills/gsd-pause-work/SKILL.md": "95017c70ae9dca0d", "skills/gsd-phase/SKILL.md": "31578c329cc2583e", "skills/gsd-plan-phase/SKILL.md": "54798eddf30056b5", diff --git a/tests/fixtures/golden-install-parity/opencode.json b/tests/fixtures/golden-install-parity/opencode.json index a646e2987..3546e38df 100644 --- a/tests/fixtures/golden-install-parity/opencode.json +++ b/tests/fixtures/golden-install-parity/opencode.json @@ -62,7 +62,7 @@ "command/gsd-inbox.md": "cc850bab897aed1f", "command/gsd-ingest-docs.md": "b47c812c247e58a4", "command/gsd-manager.md": "86ece154b34b6d1b", - "command/gsd-map-codebase.md": "1fb678f8a17a380e", + "command/gsd-map-codebase.md": "9aba6b4eed565912", "command/gsd-mempalace-capture.md": "a94ca275cbe7134c", "command/gsd-mempalace-recall.md": "d358cdd89c935877", "command/gsd-milestone-summary.md": "86a827271bc647c5", @@ -73,9 +73,10 @@ "command/gsd-ns-context.md": "9a2b55b64f2e1e4b", "command/gsd-ns-ideate.md": "27e7fcdc69eeeb5f", "command/gsd-ns-manage.md": "eded59052ca4b240", - "command/gsd-ns-project.md": "91417cf7b76581dd", + "command/gsd-ns-project.md": "c56395edb9bc7467", "command/gsd-ns-review.md": "61fe33e28eb1b961", "command/gsd-ns-workflow.md": "13c1ab91080c0cc3", + "command/gsd-onboard.md": "3aafaeb5d3941efe", "command/gsd-pause-work.md": "bb5bf91a2e3e480e", "command/gsd-phase.md": "6bcda1539f949d5a", "command/gsd-plan-phase.md": "7ee44086b59862b9", @@ -108,7 +109,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "770e0b1c1f08ef47", + "gsd-core/bin/gsd-tools.cjs": "f1a1a58072e7c35d", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "1318633d27964e7a", @@ -157,6 +158,7 @@ "gsd-core/references/gates.md": "7dc9fd3a3d6217c6", "gsd-core/references/git-integration.md": "fbdf814a3af9c051", "gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7", + "gsd-core/references/gsd-run-resolver.md": "4e5af9905e4276e4", "gsd-core/references/honest-verifier.md": "b67482409f896a10", "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", @@ -243,7 +245,7 @@ "gsd-core/templates/milestone.md": "74d2f750ae9f4a9c", "gsd-core/templates/phase-prompt.md": "ee8880432b4d1566", "gsd-core/templates/planner-subagent-prompt.md": "6c9f1b23ee3dc05f", - "gsd-core/templates/project.md": "e4a3b331be081e62", + "gsd-core/templates/project.md": "fae14114f2b79b10", "gsd-core/templates/requirements.md": "a44de4c2f146e473", "gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0", "gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f", @@ -296,7 +298,7 @@ "gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb", "gsd-core/workflows/discuss-phase/templates/context.md": "9804674fccada405", "gsd-core/workflows/discuss-phase/templates/discussion-log.md": "04e4feee14772275", - "gsd-core/workflows/do.md": "d72e76d9050b53f3", + "gsd-core/workflows/do.md": "85c4c8cb7aa8af87", "gsd-core/workflows/docs-update.md": "850366c2ef8fb780", "gsd-core/workflows/edit-phase.md": "1876c855fb0a0a39", "gsd-core/workflows/eval-review.md": "5394694d29ad7543", @@ -313,10 +315,10 @@ "gsd-core/workflows/graduation.md": "13080b5c24eec27d", "gsd-core/workflows/health.md": "a91e0a8e5d20b2e9", "gsd-core/workflows/help.md": "5d040504b9ab35e3", - "gsd-core/workflows/help/modes/brief.md": "2e923b3ed635d495", - "gsd-core/workflows/help/modes/default.md": "b544113eb9aa54fa", - "gsd-core/workflows/help/modes/full.md": "604993c20f4e4b20", - "gsd-core/workflows/help/modes/topic.md": "19a279fe91922d3b", + "gsd-core/workflows/help/modes/brief.md": "fa2675516b40e2e3", + "gsd-core/workflows/help/modes/default.md": "be05e56b2c5ee2c0", + "gsd-core/workflows/help/modes/full.md": "35598c0ed5f7356f", + "gsd-core/workflows/help/modes/topic.md": "0bf9ab39d7044d69", "gsd-core/workflows/import.md": "67fbf389ab8e57b4", "gsd-core/workflows/inbox.md": "a073396097c89c01", "gsd-core/workflows/ingest-docs.md": "82370f93c45c8015", @@ -334,6 +336,7 @@ "gsd-core/workflows/next.md": "6719a37b8ce2ac4f", "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", "gsd-core/workflows/note.md": "0d1374f2a2257858", + "gsd-core/workflows/onboard.md": "102556e1715c01b9", "gsd-core/workflows/pause-work.md": "70c72beca55c080a", "gsd-core/workflows/plan-milestone-gaps.md": "0a9dacd422cd9533", "gsd-core/workflows/plan-phase.md": "c771c9c99f4d777d", @@ -345,7 +348,7 @@ "gsd-core/workflows/pr-branch.md": "929b7cb0c99c7b9e", "gsd-core/workflows/profile-user.md": "248d59a31948e0ed", "gsd-core/workflows/progress.md": "8fc3404087f50b95", - "gsd-core/workflows/quick.md": "3866a5259e8912d8", + "gsd-core/workflows/quick.md": "10cc8ddd4bc3f0ee", "gsd-core/workflows/reapply-patches.md": "a0e9b53f90abceb2", "gsd-core/workflows/remove-phase.md": "dea4661e8f89596f", "gsd-core/workflows/remove-workspace.md": "446847e71aa52504", @@ -440,7 +443,7 @@ "skills/gsd-inbox/SKILL.md": "3b2dd319d91dbab4", "skills/gsd-ingest-docs/SKILL.md": "8f294b4894f782a5", "skills/gsd-manager/SKILL.md": "cf40f237b4f20aa7", - "skills/gsd-map-codebase/SKILL.md": "6d74ea41e29ff035", + "skills/gsd-map-codebase/SKILL.md": "73f714297815f74c", "skills/gsd-mempalace-capture/SKILL.md": "f960b7b1a8fdf89b", "skills/gsd-mempalace-recall/SKILL.md": "2091c48e6e986e28", "skills/gsd-milestone-summary/SKILL.md": "175366bb500b5e64", @@ -451,9 +454,10 @@ "skills/gsd-ns-context/SKILL.md": "110fb2227bcde535", "skills/gsd-ns-ideate/SKILL.md": "d093d59a8519decc", "skills/gsd-ns-manage/SKILL.md": "2756981de613022e", - "skills/gsd-ns-project/SKILL.md": "757976d48e8b7a26", + "skills/gsd-ns-project/SKILL.md": "56d3d8a321d267f8", "skills/gsd-ns-review/SKILL.md": "680bdd33c882b049", "skills/gsd-ns-workflow/SKILL.md": "579b7f0dac74e1fb", + "skills/gsd-onboard/SKILL.md": "ea0b1217e5ecae23", "skills/gsd-pause-work/SKILL.md": "c7e9ba4f242c4648", "skills/gsd-phase/SKILL.md": "ee78c0c56c814d84", "skills/gsd-plan-phase/SKILL.md": "6abcc3a1568a6bb9", diff --git a/tests/fixtures/golden-install-parity/qwen.json b/tests/fixtures/golden-install-parity/qwen.json index 79148d301..01d4c9511 100644 --- a/tests/fixtures/golden-install-parity/qwen.json +++ b/tests/fixtures/golden-install-parity/qwen.json @@ -38,7 +38,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "6e98d76e955e35a2", - "gsd-core/bin/gsd-tools.cjs": "1118403709188168", + "gsd-core/bin/gsd-tools.cjs": "6e2a3fff91bacb3e", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "1318633d27964e7a", @@ -87,6 +87,7 @@ "gsd-core/references/gates.md": "7dc9fd3a3d6217c6", "gsd-core/references/git-integration.md": "bd249d9024c39c0d", "gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7", + "gsd-core/references/gsd-run-resolver.md": "10575c928ff9230d", "gsd-core/references/honest-verifier.md": "8815c9fc18c35719", "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", @@ -173,7 +174,7 @@ "gsd-core/templates/milestone.md": "74d2f750ae9f4a9c", "gsd-core/templates/phase-prompt.md": "213ccd947451ff2b", "gsd-core/templates/planner-subagent-prompt.md": "6c9f1b23ee3dc05f", - "gsd-core/templates/project.md": "1f687c7a88381080", + "gsd-core/templates/project.md": "ae1f68db042c2522", "gsd-core/templates/requirements.md": "a44de4c2f146e473", "gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0", "gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f", @@ -226,7 +227,7 @@ "gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb", "gsd-core/workflows/discuss-phase/templates/context.md": "6cd929e989fe2b0f", "gsd-core/workflows/discuss-phase/templates/discussion-log.md": "1bbd7703f11128e1", - "gsd-core/workflows/do.md": "35862ba1f7230247", + "gsd-core/workflows/do.md": "68f2607de614157d", "gsd-core/workflows/docs-update.md": "45d2f0d173c84e07", "gsd-core/workflows/edit-phase.md": "0fb5e0123cfc6f36", "gsd-core/workflows/eval-review.md": "6dee8a1e40ececd4", @@ -243,10 +244,10 @@ "gsd-core/workflows/graduation.md": "40401655435beee3", "gsd-core/workflows/health.md": "043de14edb6a9723", "gsd-core/workflows/help.md": "5d040504b9ab35e3", - "gsd-core/workflows/help/modes/brief.md": "2e923b3ed635d495", - "gsd-core/workflows/help/modes/default.md": "cef457f954a9991e", - "gsd-core/workflows/help/modes/full.md": "1e1e826ba1da0bc5", - "gsd-core/workflows/help/modes/topic.md": "899d7168a485297f", + "gsd-core/workflows/help/modes/brief.md": "fa2675516b40e2e3", + "gsd-core/workflows/help/modes/default.md": "7ca77077085452f5", + "gsd-core/workflows/help/modes/full.md": "00c1433a639f6ed8", + "gsd-core/workflows/help/modes/topic.md": "6e42db16f1568be9", "gsd-core/workflows/import.md": "e56c18a984396066", "gsd-core/workflows/inbox.md": "91aac6360e1a8672", "gsd-core/workflows/ingest-docs.md": "f291310a8a89dbd8", @@ -264,6 +265,7 @@ "gsd-core/workflows/next.md": "c13515a80c20d37d", "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", "gsd-core/workflows/note.md": "42b66686b2c102cb", + "gsd-core/workflows/onboard.md": "3c50ed1f1fd07619", "gsd-core/workflows/pause-work.md": "be33f84dc1d4822f", "gsd-core/workflows/plan-milestone-gaps.md": "d98e98486123eb97", "gsd-core/workflows/plan-phase.md": "8ef2986df30b616e", @@ -275,7 +277,7 @@ "gsd-core/workflows/pr-branch.md": "cef0f65b16d500b4", "gsd-core/workflows/profile-user.md": "263c0693563d98da", "gsd-core/workflows/progress.md": "3b1b2142a74af85c", - "gsd-core/workflows/quick.md": "518740e30dcd4da3", + "gsd-core/workflows/quick.md": "0a9a7dacc73f8e53", "gsd-core/workflows/reapply-patches.md": "de0ee8acfe7245b2", "gsd-core/workflows/remove-phase.md": "e8ae4fbbfac700f0", "gsd-core/workflows/remove-workspace.md": "4ac64de862dc650e", @@ -345,7 +347,7 @@ "skills/gsd-ns-context/skills/docs-update/SKILL.md": "3d41e81ec571de60", "skills/gsd-ns-context/skills/extract-learnings/SKILL.md": "8d38e55e5f8b774b", "skills/gsd-ns-context/skills/graphify/SKILL.md": "ac3661e6e576188b", - "skills/gsd-ns-context/skills/map-codebase/SKILL.md": "5009595dbde69739", + "skills/gsd-ns-context/skills/map-codebase/SKILL.md": "cf65e61890448285", "skills/gsd-ns-context/skills/mempalace-capture/SKILL.md": "40a31ad049374297", "skills/gsd-ns-context/skills/mempalace-recall/SKILL.md": "c3f412d5a64f3921", "skills/gsd-ns-ideate/SKILL.md": "4a0d2691054b23e5", @@ -373,7 +375,7 @@ "skills/gsd-ns-manage/skills/update/SKILL.md": "530b4206d729b56d", "skills/gsd-ns-manage/skills/workspace/SKILL.md": "047c3f4247bc869e", "skills/gsd-ns-manage/skills/workstreams/SKILL.md": "f4e54cb9b1ca0442", - "skills/gsd-ns-project/SKILL.md": "a58b3170197968be", + "skills/gsd-ns-project/SKILL.md": "5d7ec93b195555e7", "skills/gsd-ns-project/skills/audit-milestone/SKILL.md": "edb6a0cc6305d724", "skills/gsd-ns-project/skills/complete-milestone/SKILL.md": "f6799a5a713be7bb", "skills/gsd-ns-project/skills/import/SKILL.md": "88f83921a85e536b", @@ -381,6 +383,7 @@ "skills/gsd-ns-project/skills/milestone-summary/SKILL.md": "33d51a325d314f3a", "skills/gsd-ns-project/skills/new-milestone/SKILL.md": "ac99ffb8a966fe9d", "skills/gsd-ns-project/skills/new-project/SKILL.md": "9d5c27ebfa2c4746", + "skills/gsd-ns-project/skills/onboard/SKILL.md": "46460b479b7524bf", "skills/gsd-ns-project/skills/profile-user/SKILL.md": "78ca46045e85223f", "skills/gsd-ns-project/skills/review-backlog/SKILL.md": "1708aab6cb919223", "skills/gsd-ns-review/SKILL.md": "7badceb8627d8154", diff --git a/tests/fixtures/golden-install-parity/trae.json b/tests/fixtures/golden-install-parity/trae.json index be08afc4a..6e591557f 100644 --- a/tests/fixtures/golden-install-parity/trae.json +++ b/tests/fixtures/golden-install-parity/trae.json @@ -38,7 +38,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "de4627dff103d527", - "gsd-core/bin/gsd-tools.cjs": "fa4db2ce1f77ef99", + "gsd-core/bin/gsd-tools.cjs": "2ed2d11d5be017e2", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "1318633d27964e7a", @@ -87,6 +87,7 @@ "gsd-core/references/gates.md": "7dc9fd3a3d6217c6", "gsd-core/references/git-integration.md": "1bd40c3f94d712b1", "gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7", + "gsd-core/references/gsd-run-resolver.md": "0534754c2758352a", "gsd-core/references/honest-verifier.md": "8815c9fc18c35719", "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", @@ -173,7 +174,7 @@ "gsd-core/templates/milestone.md": "74d2f750ae9f4a9c", "gsd-core/templates/phase-prompt.md": "213ccd947451ff2b", "gsd-core/templates/planner-subagent-prompt.md": "6c9f1b23ee3dc05f", - "gsd-core/templates/project.md": "1f687c7a88381080", + "gsd-core/templates/project.md": "ae1f68db042c2522", "gsd-core/templates/requirements.md": "a44de4c2f146e473", "gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0", "gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f", @@ -226,7 +227,7 @@ "gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb", "gsd-core/workflows/discuss-phase/templates/context.md": "6cd929e989fe2b0f", "gsd-core/workflows/discuss-phase/templates/discussion-log.md": "1bbd7703f11128e1", - "gsd-core/workflows/do.md": "f3328dbd4416f11a", + "gsd-core/workflows/do.md": "5131e8796ba9d99b", "gsd-core/workflows/docs-update.md": "f13571f08e083356", "gsd-core/workflows/edit-phase.md": "7facd0faa33c8cad", "gsd-core/workflows/eval-review.md": "37d545d4f0db4927", @@ -243,10 +244,10 @@ "gsd-core/workflows/graduation.md": "1ca877cda258a5de", "gsd-core/workflows/health.md": "1f9fd2deea45896f", "gsd-core/workflows/help.md": "08e1349950c5602a", - "gsd-core/workflows/help/modes/brief.md": "2e923b3ed635d495", - "gsd-core/workflows/help/modes/default.md": "1b3c315342701265", - "gsd-core/workflows/help/modes/full.md": "62c71f57f047999b", - "gsd-core/workflows/help/modes/topic.md": "229e3f3d85bda04b", + "gsd-core/workflows/help/modes/brief.md": "fa2675516b40e2e3", + "gsd-core/workflows/help/modes/default.md": "ed7368e0d1b8644a", + "gsd-core/workflows/help/modes/full.md": "b3287aa383806758", + "gsd-core/workflows/help/modes/topic.md": "8a5344e56fa64ab9", "gsd-core/workflows/import.md": "31af58468db15daf", "gsd-core/workflows/inbox.md": "e9ea37b2d46dc5b4", "gsd-core/workflows/ingest-docs.md": "fba176ad3cc22f5f", @@ -264,6 +265,7 @@ "gsd-core/workflows/next.md": "2163195861877de5", "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", "gsd-core/workflows/note.md": "acc9130fb1e94f0b", + "gsd-core/workflows/onboard.md": "61f111302af0f404", "gsd-core/workflows/pause-work.md": "c20d267e28ce92f0", "gsd-core/workflows/plan-milestone-gaps.md": "26db7b9329b7ddc8", "gsd-core/workflows/plan-phase.md": "dbdcfdb7aeb4a4f0", @@ -275,7 +277,7 @@ "gsd-core/workflows/pr-branch.md": "79fd55b88ea2db9c", "gsd-core/workflows/profile-user.md": "672821e6b1266645", "gsd-core/workflows/progress.md": "94768f835b0b8908", - "gsd-core/workflows/quick.md": "8ac5f3919f754499", + "gsd-core/workflows/quick.md": "9bbf907e39688638", "gsd-core/workflows/reapply-patches.md": "21b38c374f19fd78", "gsd-core/workflows/remove-phase.md": "a46c2fe853bf4e86", "gsd-core/workflows/remove-workspace.md": "ae0e1c6d4438d663", @@ -323,7 +325,7 @@ "skills/gsd-ns-context/skills/docs-update/SKILL.md": "52755343585b987b", "skills/gsd-ns-context/skills/extract-learnings/SKILL.md": "d39269bea995fabc", "skills/gsd-ns-context/skills/graphify/SKILL.md": "e0253aef14693a7e", - "skills/gsd-ns-context/skills/map-codebase/SKILL.md": "0910348701b1fc65", + "skills/gsd-ns-context/skills/map-codebase/SKILL.md": "2982436d8652f172", "skills/gsd-ns-context/skills/mempalace-capture/SKILL.md": "36c486f14abe869a", "skills/gsd-ns-context/skills/mempalace-recall/SKILL.md": "2d95e385fee26dee", "skills/gsd-ns-ideate/SKILL.md": "c10342345c01c91f", @@ -351,7 +353,7 @@ "skills/gsd-ns-manage/skills/update/SKILL.md": "2fc47bf059aa7b38", "skills/gsd-ns-manage/skills/workspace/SKILL.md": "257d17b9d20274d8", "skills/gsd-ns-manage/skills/workstreams/SKILL.md": "737841783c7fdd4f", - "skills/gsd-ns-project/SKILL.md": "5668e3a5e1d8896d", + "skills/gsd-ns-project/SKILL.md": "fac77a9e770fd655", "skills/gsd-ns-project/skills/audit-milestone/SKILL.md": "0ccabc5556d65d56", "skills/gsd-ns-project/skills/complete-milestone/SKILL.md": "a69d534b385d6aaf", "skills/gsd-ns-project/skills/import/SKILL.md": "a11e630bfd8ae4de", @@ -359,6 +361,7 @@ "skills/gsd-ns-project/skills/milestone-summary/SKILL.md": "9d408f56712ba948", "skills/gsd-ns-project/skills/new-milestone/SKILL.md": "7cbc011130e8590c", "skills/gsd-ns-project/skills/new-project/SKILL.md": "ff0d57491df30624", + "skills/gsd-ns-project/skills/onboard/SKILL.md": "ae4c7b799d9ffb09", "skills/gsd-ns-project/skills/profile-user/SKILL.md": "4f526120cda2f6cc", "skills/gsd-ns-project/skills/review-backlog/SKILL.md": "d8a150558cc9326a", "skills/gsd-ns-review/SKILL.md": "c5afe33c212947dd", diff --git a/tests/fixtures/golden-install-parity/windsurf.json b/tests/fixtures/golden-install-parity/windsurf.json index c6612d3c6..b7761e74d 100644 --- a/tests/fixtures/golden-install-parity/windsurf.json +++ b/tests/fixtures/golden-install-parity/windsurf.json @@ -38,7 +38,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "5636ca0b726871b2", - "gsd-core/bin/gsd-tools.cjs": "92e0f7b7eb55af51", + "gsd-core/bin/gsd-tools.cjs": "ba7396b5e12d9b70", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "1318633d27964e7a", @@ -87,6 +87,7 @@ "gsd-core/references/gates.md": "7dc9fd3a3d6217c6", "gsd-core/references/git-integration.md": "6ec36ea6b868655f", "gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7", + "gsd-core/references/gsd-run-resolver.md": "1bb6a410d6ebfb33", "gsd-core/references/honest-verifier.md": "8815c9fc18c35719", "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", @@ -173,7 +174,7 @@ "gsd-core/templates/milestone.md": "74d2f750ae9f4a9c", "gsd-core/templates/phase-prompt.md": "213ccd947451ff2b", "gsd-core/templates/planner-subagent-prompt.md": "6c9f1b23ee3dc05f", - "gsd-core/templates/project.md": "1f687c7a88381080", + "gsd-core/templates/project.md": "ae1f68db042c2522", "gsd-core/templates/requirements.md": "a44de4c2f146e473", "gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0", "gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f", @@ -226,7 +227,7 @@ "gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb", "gsd-core/workflows/discuss-phase/templates/context.md": "6cd929e989fe2b0f", "gsd-core/workflows/discuss-phase/templates/discussion-log.md": "cd5d01c9ef84ab73", - "gsd-core/workflows/do.md": "a7ff800fdab468c7", + "gsd-core/workflows/do.md": "e2b848547e43c7a5", "gsd-core/workflows/docs-update.md": "70f73cc8c27e0ca0", "gsd-core/workflows/edit-phase.md": "c0ae7d0063f3e789", "gsd-core/workflows/eval-review.md": "b28be79ef29f16fd", @@ -243,10 +244,10 @@ "gsd-core/workflows/graduation.md": "a766039ff6ca653f", "gsd-core/workflows/health.md": "7b19d6e2c0357c3e", "gsd-core/workflows/help.md": "08e1349950c5602a", - "gsd-core/workflows/help/modes/brief.md": "2e923b3ed635d495", - "gsd-core/workflows/help/modes/default.md": "91509fe03bad9795", - "gsd-core/workflows/help/modes/full.md": "4c1a43f7095647e1", - "gsd-core/workflows/help/modes/topic.md": "177d351f7465787d", + "gsd-core/workflows/help/modes/brief.md": "fa2675516b40e2e3", + "gsd-core/workflows/help/modes/default.md": "6a253f1756f74e95", + "gsd-core/workflows/help/modes/full.md": "0cd464dcd077bbfc", + "gsd-core/workflows/help/modes/topic.md": "cee80e0adfa3b06c", "gsd-core/workflows/import.md": "029cacf530aca86b", "gsd-core/workflows/inbox.md": "797c287852eb8957", "gsd-core/workflows/ingest-docs.md": "78ba04ca39f28e8d", @@ -264,6 +265,7 @@ "gsd-core/workflows/next.md": "d48cea140a5153cb", "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", "gsd-core/workflows/note.md": "1c1e466c764e3deb", + "gsd-core/workflows/onboard.md": "20c28136423d40ac", "gsd-core/workflows/pause-work.md": "93fcc1c845da6396", "gsd-core/workflows/plan-milestone-gaps.md": "7880866ee1caf923", "gsd-core/workflows/plan-phase.md": "c7c5cac6409937ac", @@ -275,7 +277,7 @@ "gsd-core/workflows/pr-branch.md": "acd59f915d018ad4", "gsd-core/workflows/profile-user.md": "c4313672b81b5bcd", "gsd-core/workflows/progress.md": "18813a345bd2343a", - "gsd-core/workflows/quick.md": "0fcad72e7dbe99c9", + "gsd-core/workflows/quick.md": "3acd391cc0c3813a", "gsd-core/workflows/reapply-patches.md": "d449a23d3acf6379", "gsd-core/workflows/remove-phase.md": "e7a6af429b36e77b", "gsd-core/workflows/remove-workspace.md": "b5e60fbb33b3e33a", diff --git a/tests/fixtures/golden-install-parity/zcode.json b/tests/fixtures/golden-install-parity/zcode.json index ae224cc5b..d394aab6c 100644 --- a/tests/fixtures/golden-install-parity/zcode.json +++ b/tests/fixtures/golden-install-parity/zcode.json @@ -62,7 +62,7 @@ "commands/gsd-inbox.md": "da55c1408a6ee6cc", "commands/gsd-ingest-docs.md": "7a3c21ae5d5d133a", "commands/gsd-manager.md": "9f54653a7c589b00", - "commands/gsd-map-codebase.md": "8b605921868c03a3", + "commands/gsd-map-codebase.md": "709e9572953accc6", "commands/gsd-mempalace-capture.md": "5d4556e444acbb90", "commands/gsd-mempalace-recall.md": "5ac67fa4b7f2b03b", "commands/gsd-milestone-summary.md": "f8fa9c357c08b74b", @@ -73,9 +73,10 @@ "commands/gsd-ns-context.md": "011c44e7aa46e64a", "commands/gsd-ns-ideate.md": "edc5e543512dd48a", "commands/gsd-ns-manage.md": "0409d810e499357f", - "commands/gsd-ns-project.md": "8dfd1b9a2ffe86ac", + "commands/gsd-ns-project.md": "ff67e85bc6f7fc5a", "commands/gsd-ns-review.md": "3766ed10827882a0", "commands/gsd-ns-workflow.md": "c3b3c046a74ec0ee", + "commands/gsd-onboard.md": "a35340ce39334fc7", "commands/gsd-pause-work.md": "40a953fcddbedb5d", "commands/gsd-phase.md": "5dd3d40e3461a973", "commands/gsd-plan-phase.md": "2fb6cc9c4caa6a36", @@ -108,7 +109,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "770e0b1c1f08ef47", + "gsd-core/bin/gsd-tools.cjs": "f1a1a58072e7c35d", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "1318633d27964e7a", @@ -157,6 +158,7 @@ "gsd-core/references/gates.md": "bd79c7f90c8cb8d7", "gsd-core/references/git-integration.md": "77bf9dff38b2c9d4", "gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7", + "gsd-core/references/gsd-run-resolver.md": "e71eb728b84641b1", "gsd-core/references/honest-verifier.md": "8815c9fc18c35719", "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", @@ -243,7 +245,7 @@ "gsd-core/templates/milestone.md": "74d2f750ae9f4a9c", "gsd-core/templates/phase-prompt.md": "b811bf951092df2f", "gsd-core/templates/planner-subagent-prompt.md": "ebf29dbb27042370", - "gsd-core/templates/project.md": "ab4af6becca2bb23", + "gsd-core/templates/project.md": "4f311fb1b05b823b", "gsd-core/templates/requirements.md": "a44de4c2f146e473", "gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0", "gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f", @@ -296,7 +298,7 @@ "gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb", "gsd-core/workflows/discuss-phase/templates/context.md": "6cd929e989fe2b0f", "gsd-core/workflows/discuss-phase/templates/discussion-log.md": "1bbd7703f11128e1", - "gsd-core/workflows/do.md": "8f460af710c8f6a4", + "gsd-core/workflows/do.md": "7512dd1892f118a1", "gsd-core/workflows/docs-update.md": "f35922d15b7061c9", "gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04", "gsd-core/workflows/eval-review.md": "f898936e2cfe4130", @@ -313,10 +315,10 @@ "gsd-core/workflows/graduation.md": "47f1594c88c08501", "gsd-core/workflows/health.md": "107e3c72e76d9535", "gsd-core/workflows/help.md": "5d040504b9ab35e3", - "gsd-core/workflows/help/modes/brief.md": "5fe077fbbdb5d9f7", - "gsd-core/workflows/help/modes/default.md": "cc8ef97de634c995", - "gsd-core/workflows/help/modes/full.md": "96abbac7f3f10e32", - "gsd-core/workflows/help/modes/topic.md": "6762e41e3291ae24", + "gsd-core/workflows/help/modes/brief.md": "924860e1f07defb0", + "gsd-core/workflows/help/modes/default.md": "08a02976c0c5cc50", + "gsd-core/workflows/help/modes/full.md": "895b35f264530d24", + "gsd-core/workflows/help/modes/topic.md": "5c160093f3cbf35d", "gsd-core/workflows/import.md": "3d3fa603ceb8bc9f", "gsd-core/workflows/inbox.md": "437f981ef9ae7b26", "gsd-core/workflows/ingest-docs.md": "c859921c811ac11b", @@ -334,6 +336,7 @@ "gsd-core/workflows/next.md": "13fb800f2472d970", "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", "gsd-core/workflows/note.md": "5a99eb396c744619", + "gsd-core/workflows/onboard.md": "6f9e6c0b484271a9", "gsd-core/workflows/pause-work.md": "f2b33bba5593d422", "gsd-core/workflows/plan-milestone-gaps.md": "852f6d7c0c4299dc", "gsd-core/workflows/plan-phase.md": "24a8ceaa8b032641", @@ -345,7 +348,7 @@ "gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b", "gsd-core/workflows/profile-user.md": "e23bea0a69c0bb4b", "gsd-core/workflows/progress.md": "893aa3c36983f74b", - "gsd-core/workflows/quick.md": "1bc59c4d97fed876", + "gsd-core/workflows/quick.md": "5747c2d8a37affbb", "gsd-core/workflows/reapply-patches.md": "39050f72601aec89", "gsd-core/workflows/remove-phase.md": "df9a45f0b1880999", "gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c", @@ -415,7 +418,7 @@ "skills/gsd-ns-context/skills/docs-update/SKILL.md": "8fb602920a333d73", "skills/gsd-ns-context/skills/extract-learnings/SKILL.md": "6de7eeaaf4939e5d", "skills/gsd-ns-context/skills/graphify/SKILL.md": "d54c2b44e8059299", - "skills/gsd-ns-context/skills/map-codebase/SKILL.md": "ce7f2382e4086b68", + "skills/gsd-ns-context/skills/map-codebase/SKILL.md": "f48d48b43e2cc4bd", "skills/gsd-ns-context/skills/mempalace-capture/SKILL.md": "40a31ad049374297", "skills/gsd-ns-context/skills/mempalace-recall/SKILL.md": "c3f412d5a64f3921", "skills/gsd-ns-ideate/SKILL.md": "4a0d2691054b23e5", @@ -443,7 +446,7 @@ "skills/gsd-ns-manage/skills/update/SKILL.md": "01ca0efbdb8f6efa", "skills/gsd-ns-manage/skills/workspace/SKILL.md": "c6c07625a373e6a3", "skills/gsd-ns-manage/skills/workstreams/SKILL.md": "f4e54cb9b1ca0442", - "skills/gsd-ns-project/SKILL.md": "a58b3170197968be", + "skills/gsd-ns-project/SKILL.md": "5d7ec93b195555e7", "skills/gsd-ns-project/skills/audit-milestone/SKILL.md": "a6eb7edcfd23c7e4", "skills/gsd-ns-project/skills/complete-milestone/SKILL.md": "d7f6202222d1b26e", "skills/gsd-ns-project/skills/import/SKILL.md": "0a734021e0b86481", @@ -451,6 +454,7 @@ "skills/gsd-ns-project/skills/milestone-summary/SKILL.md": "c77f6c8ce7c57db9", "skills/gsd-ns-project/skills/new-milestone/SKILL.md": "3aad804c48d77474", "skills/gsd-ns-project/skills/new-project/SKILL.md": "e3cfd1fdbc2c3406", + "skills/gsd-ns-project/skills/onboard/SKILL.md": "84519552f7396347", "skills/gsd-ns-project/skills/profile-user/SKILL.md": "f57ad5e5c5899100", "skills/gsd-ns-project/skills/review-backlog/SKILL.md": "1708aab6cb919223", "skills/gsd-ns-review/SKILL.md": "7badceb8627d8154", diff --git a/tests/helpers.cjs b/tests/helpers.cjs index 3538a26a5..234bc8f99 100644 --- a/tests/helpers.cjs +++ b/tests/helpers.cjs @@ -423,6 +423,7 @@ function resetRuntimeWarningCaches() { const modelResolver = require('../gsd-core/bin/lib/model-resolver.cjs'); configLoader._resetRuntimeWarningCacheForTests(); modelResolver._resetModelPolicyWarningCacheForTests(); + modelResolver._resetModelOverrideWarningCacheForTests(); } module.exports = { runGsdTools, createTempDir, createTempProject, createTempGitProject, cleanup, parseFrontmatter, isUsageOutput, captureConsole, toPosixPath, runNpm, isolatedNpmEnv, withIsolatedProcessState, delay, waitFor, resetRuntimeWarningCaches, TOOLS_PATH }; diff --git a/tests/init.test.cjs b/tests/init.test.cjs index 964ce48f1..2ede0d0e8 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -1599,6 +1599,9 @@ describe('cmdInitNewProject', () => { test('brownfield with codebase map does not need map', () => { fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"test"}'); fs.mkdirSync(path.join(tmpDir, '.planning', 'codebase'), { recursive: true }); + for (const name of ['STACK', 'ARCHITECTURE', 'STRUCTURE', 'CONVENTIONS', 'TESTING', 'INTEGRATIONS', 'CONCERNS']) { + fs.writeFileSync(path.join(tmpDir, '.planning', 'codebase', `${name}.md`), `# ${name}\n`); + } const result = runGsdTools('init new-project', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); diff --git a/tests/model-resolver.test.cjs b/tests/model-resolver.test.cjs index 9fc7fa30e..3e2d06ddf 100644 --- a/tests/model-resolver.test.cjs +++ b/tests/model-resolver.test.cjs @@ -3770,6 +3770,190 @@ describe('#49 resolveModelPolicy: prototype-pollution guards', () => { }); }); +// ─── #2041: model_overrides Claude full ID → Agent-tool alias on claude runtime ─ +// +// Mirrors the #1133 model_policy alias-mapping tests (above) for the +// model_overrides path. Bug: a full Claude model ID in model_overrides +// (e.g. "claude-sonnet-5") was returned VERBATIM on the claude runtime and +// handed to the Claude Agent tool, whose typed `model` parameter documents only +// tier aliases (opus/sonnet/haiku/fable). The model_policy path already maps +// full IDs → aliases via CLAUDE_POLICY_ID_TO_ALIAS (#1144); model_overrides +// skipped that mapping entirely. The fix mirrors #1144 on the override path. +// Non-Claude runtimes and non-Claude values pass through verbatim (parity). + +describe('#2041 model_overrides: Claude full ID → alias on claude runtime', () => { + let tmpDir; + beforeEach(() => { + tmpDir = makeTmp('2041'); + resetRuntimeWarningCaches(); + }); + afterEach(() => { + rmr(tmpDir); + resetRuntimeWarningCaches(); + }); + + // AC1 + AC2: mappable Claude full IDs resolve to their aliases on claude runtime + test('model_overrides claude-sonnet-5 → "sonnet" on runtime:claude (resolveModelInternal)', () => { + writeConfig(tmpDir, { + runtime: 'claude', + model_overrides: { 'gsd-executor': 'claude-sonnet-5' }, + }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-executor'), 'sonnet'); + }); + + test('model_overrides claude-opus-4-8 → "opus" on runtime:claude', () => { + writeConfig(tmpDir, { + runtime: 'claude', + model_overrides: { 'gsd-planner': 'claude-opus-4-8' }, + }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'opus'); + }); + + test('model_overrides claude-haiku-4-5 → "haiku" on runtime:claude', () => { + writeConfig(tmpDir, { + runtime: 'claude', + model_overrides: { 'gsd-codebase-mapper': 'claude-haiku-4-5' }, + }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-codebase-mapper'), 'haiku'); + }); + + test('model_overrides claude-fable-5 → "fable" on runtime:claude', () => { + writeConfig(tmpDir, { + runtime: 'claude', + model_overrides: { 'gsd-planner': 'claude-fable-5' }, + }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'fable'); + }); + + // AC3: bare aliases pass through verbatim + test('model_overrides bare "sonnet" alias passes through verbatim on runtime:claude', () => { + writeConfig(tmpDir, { + runtime: 'claude', + model_overrides: { 'gsd-executor': 'sonnet' }, + }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-executor'), 'sonnet'); + }); + + test('model_overrides bare "fable" alias passes through verbatim on runtime:claude', () => { + writeConfig(tmpDir, { + runtime: 'claude', + model_overrides: { 'gsd-planner': 'fable' }, + }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'fable'); + }); + + // AC1 (implicit claude): mapping fires when runtime key is absent (defaults to claude) + test('model_overrides claude-sonnet-5 → "sonnet" with implicit claude runtime (no runtime key)', () => { + writeConfig(tmpDir, { + model_overrides: { 'gsd-executor': 'claude-sonnet-5' }, + }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-executor'), 'sonnet'); + }); + + // AC4: non-claude runtimes keep full IDs verbatim (parity with model_policy path) + test('model_overrides claude-sonnet-5 → verbatim ID on non-claude runtime (opencode)', () => { + writeConfig(tmpDir, { + runtime: 'opencode', + model_overrides: { 'gsd-executor': 'claude-sonnet-5' }, + }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-executor'), 'claude-sonnet-5'); + }); + + // AC5: unmappable Claude full ID warns once + falls through to tier alias + test('model_overrides unmappable claude ID (claude-opus-4-5) falls through to tier alias on claude', () => { + resetRuntimeWarningCaches(); + writeConfig(tmpDir, { + runtime: 'claude', + model_profile: 'balanced', + model_overrides: { 'gsd-planner': 'claude-opus-4-5' }, + }); + // gsd-planner balanced → opus tier; claude-opus-4-5 has no alias → warn + fall through → 'opus' + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'opus'); + }); + + test('model_overrides unmappable claude ID emits a stderr warning exactly once (dedupe)', () => { + resetRuntimeWarningCaches(); + writeConfig(tmpDir, { + runtime: 'claude', + model_profile: 'balanced', + model_overrides: { 'gsd-planner': 'claude-opus-4-5' }, + }); + const writes = []; + const original = process.stderr.write.bind(process.stderr); + process.stderr.write = (chunk) => { writes.push(String(chunk)); return true; }; + try { + resolveModelInternal(tmpDir, 'gsd-planner'); + resolveModelInternal(tmpDir, 'gsd-planner'); // second call — dedupe must suppress + } finally { + process.stderr.write = original; + } + const warnings = writes.filter((w) => w.includes('model_overrides') && w.includes('claude-opus-4-5')); + assert.strictEqual(warnings.length, 1, + `expected exactly one override warning, got ${warnings.length}: ${JSON.stringify(writes)}`); + }); + + // AC6: resolveModelForTier (escalation / --attempt path) maps the same way + test('resolveModelForTier maps claude-sonnet-5 → "sonnet" on runtime:claude', () => { + writeConfig(tmpDir, { + runtime: 'claude', + model_overrides: { 'gsd-executor': 'claude-sonnet-5' }, + }); + assert.strictEqual(resolveModelForTier(tmpDir, 'gsd-executor', 0), 'sonnet'); + }); + + test('resolveModelForTier keeps full ID verbatim on non-claude runtime', () => { + writeConfig(tmpDir, { + runtime: 'opencode', + model_overrides: { 'gsd-executor': 'claude-sonnet-5' }, + }); + assert.strictEqual(resolveModelForTier(tmpDir, 'gsd-executor', 0), 'claude-sonnet-5'); + }); + + // MEDIUM-1 (review): exercise the unmappable-override fall-through branch in + // resolveModelForTier (closes the mutation-score gap — a future refactor that + // accidentally returned the verbatim override instead of falling through + // would otherwise survive the suite). + test('resolveModelForTier unmappable claude ID falls through to tier alias on claude', () => { + resetRuntimeWarningCaches(); + writeConfig(tmpDir, { + runtime: 'claude', + model_profile: 'balanced', + model_overrides: { 'gsd-planner': 'claude-opus-4-5' }, + }); + // unmappable override → fall through → no dynamic_routing → resolveModelInternal → 'opus' + assert.strictEqual(resolveModelForTier(tmpDir, 'gsd-planner', 0), 'opus'); + }); + + // LOW-2 (review): pin the case-sensitive contract — a case-variant like + // "Claude-Sonnet-5" is NOT mapped (alias keys are case-sensitive, matching + // the model_policy path and the Claude API). + test('model_overrides case-variant "Claude-Sonnet-5" passes through verbatim (case-sensitive contract)', () => { + writeConfig(tmpDir, { + runtime: 'claude', + model_overrides: { 'gsd-executor': 'Claude-Sonnet-5' }, + }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-executor'), 'Claude-Sonnet-5'); + }); + + // Regression guard: non-Claude custom / vendor values still pass through verbatim + // on the claude runtime (the fix must NOT touch values that aren't Claude IDs). + test('model_overrides non-Claude custom model passes through verbatim on runtime:claude', () => { + writeConfig(tmpDir, { + runtime: 'claude', + model_overrides: { 'gsd-planner': 'my-custom-model' }, + }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'my-custom-model'); + }); + + test('model_overrides non-Claude vendor ID (openai/gpt-5) passes through verbatim on runtime:claude', () => { + writeConfig(tmpDir, { + runtime: 'claude', + model_overrides: { 'gsd-executor': 'openai/gpt-5' }, + }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-executor'), 'openai/gpt-5'); + }); +}); + // ─── resolveModelForTier: model_policy beats dynamic_routing ───────────────── describe('#49 resolveModelForTier: model_policy beats dynamic_routing', () => { diff --git a/tests/onboard-command.test.cjs b/tests/onboard-command.test.cjs new file mode 100644 index 000000000..c60987a60 --- /dev/null +++ b/tests/onboard-command.test.cjs @@ -0,0 +1,483 @@ +// allow-test-rule: source-text-is-the-product (see #1990) +// Command/workflow markdown is deployed runtime product; source-text assertions +// below verify the installed command contract. CLI assertions exercise real +// gsd-tools behavior through the public command boundary. + +const { describe, test, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); +const { runGsdTools, cleanup } = require('./helpers.cjs'); +const { createFixture } = require('./fixtures/index.cjs'); + +const ROOT = path.join(__dirname, '..'); +const CMD_PATH = path.join(ROOT, 'commands', 'gsd', 'onboard.md'); +const WF_PATH = path.join(ROOT, 'gsd-core', 'workflows', 'onboard.md'); + +describe('init onboard public CLI projection', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createFixture({ planning: false, projectDoc: false }); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('reports brownfield code, docs, and missing planning state', () => { + fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, 'src', 'server.ts'), 'export const server = true;\n'); + fs.mkdirSync(path.join(tmpDir, 'docs', 'adr'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, 'docs', 'adr', '0001-runtime.md'), '# ADR: Runtime\n'); + + const result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.planning_exists, false); + assert.strictEqual(parsed.project_exists, false); + assert.strictEqual(parsed.has_existing_code, true); + assert.strictEqual(parsed.has_codebase_map, false); + assert.strictEqual(parsed.has_docs_candidates, true); + assert.strictEqual(parsed.doc_candidate_count, 1); + assert.deepStrictEqual(parsed.codebase_map_files_present, []); + assert.ok(parsed.doc_candidates.includes('docs/adr/0001-runtime.md')); + for (const file of ['STACK.md', 'ARCHITECTURE.md', 'STRUCTURE.md', 'CONVENTIONS.md', 'TESTING.md', 'INTEGRATIONS.md', 'CONCERNS.md']) { + assert.ok(parsed.missing_codebase_map_files.includes(file), `missing map files should include ${file}`); + } + assert.strictEqual(parsed.onboarding_summary_exists, false); + assert.strictEqual(parsed.text_mode, false); + }); + + test('detects planning docs in top-level ADR, PRD, and RFC folders', () => { + fs.mkdirSync(path.join(tmpDir, 'prd'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, 'prd', 'product.md'), '# Product Requirements\n'); + fs.mkdirSync(path.join(tmpDir, 'adr'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, 'adr', 'decision.md'), '# Architecture Decision\n'); + fs.mkdirSync(path.join(tmpDir, 'rfc'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, 'rfc', 'proposal.md'), '# Request for Comments\n'); + + const result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.has_docs_candidates, true); + assert.strictEqual(parsed.doc_candidate_count, 3); + assert.deepStrictEqual(parsed.doc_candidates, [ + 'adr/decision.md', + 'prd/product.md', + 'rfc/proposal.md', + ]); + }); + + test('detects root-level planning docs without broad repo scan', () => { + fs.writeFileSync(path.join(tmpDir, 'PRD.md'), '# Product Requirements\n'); + fs.writeFileSync(path.join(tmpDir, 'SPEC.md'), '# Specification\n'); + fs.writeFileSync(path.join(tmpDir, 'RFC.md'), '# Request for Comments\n'); + fs.writeFileSync(path.join(tmpDir, 'ADR.md'), '# Architecture Decision\n'); + fs.writeFileSync(path.join(tmpDir, 'REQUIREMENTS.md'), '# Requirements\n'); + fs.writeFileSync(path.join(tmpDir, '0001-decision.md'), '# Decision\n'); + fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, 'src', 'PRD.md'), '# Nested Product Requirements\n'); + + const result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.has_docs_candidates, true); + assert.strictEqual(parsed.doc_candidate_count, 6); + assert.deepStrictEqual(parsed.doc_candidates, [ + '0001-decision.md', + 'ADR.md', + 'PRD.md', + 'REQUIREMENTS.md', + 'RFC.md', + 'SPEC.md', + ]); + }); + + test('forwards the --text flag into the public projection', () => { + const result = runGsdTools(['init', 'onboard', '--text', '--raw'], tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.text_mode, true); + }); + + test('reports complete codebase map and onboarding summary in existing planning', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'codebase'), { recursive: true }); + for (const name of ['STACK', 'ARCHITECTURE', 'STRUCTURE', 'CONVENTIONS', 'TESTING', 'INTEGRATIONS', 'CONCERNS']) { + fs.writeFileSync(path.join(tmpDir, '.planning', 'codebase', `${name}.md`), `# ${name}\n`); + } + fs.mkdirSync(path.join(tmpDir, '.planning', 'onboarding'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, '.planning', 'onboarding', 'SUMMARY.md'), '# Onboarding Summary\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), '# Requirements\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({ workflow: { text_mode: true } })); + + const trackedFiles = [ + path.join(tmpDir, '.planning', 'PROJECT.md'), + path.join(tmpDir, '.planning', 'ROADMAP.md'), + path.join(tmpDir, '.planning', 'STATE.md'), + path.join(tmpDir, '.planning', 'onboarding', 'SUMMARY.md'), + ]; + const before = new Map(trackedFiles.map(file => [file, fs.readFileSync(file, 'utf8')])); + + const result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + for (const file of trackedFiles) { + assert.strictEqual(fs.readFileSync(file, 'utf8'), before.get(file), `${path.basename(file)} must not be mutated by init onboard`); + } + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.planning_exists, true); + assert.strictEqual(parsed.project_exists, true); + assert.strictEqual(parsed.requirements_exists, true); + assert.strictEqual(parsed.roadmap_exists, true); + assert.strictEqual(parsed.state_exists, true); + assert.strictEqual(parsed.has_codebase_map, true); + assert.deepStrictEqual(parsed.missing_codebase_map_files, []); + assert.strictEqual(parsed.onboarding_summary_exists, true); + assert.strictEqual(parsed.onboarding_summary_path, '.planning/onboarding/SUMMARY.md'); + assert.strictEqual(parsed.text_mode, true); + }); + + test('reports fast codebase map readiness and a complete-map handoff before new-project', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'codebase'), { recursive: true }); + for (const name of ['STACK', 'INTEGRATIONS', 'ARCHITECTURE', 'STRUCTURE']) { + fs.writeFileSync(path.join(tmpDir, '.planning', 'codebase', `${name}.md`), `# ${name}\n`); + } + fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"fixture"}\n'); + + const result = runGsdTools(['init', 'onboard', '--fast', '--raw'], tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.map_readiness, 'fast'); + assert.strictEqual(parsed.has_codebase_map, false); + assert.strictEqual(parsed.has_fast_codebase_map, true); + assert.strictEqual(parsed.needs_codebase_map, true); + assert.strictEqual(parsed.needs_fast_codebase_map, false); + assert.strictEqual(parsed.next_action.kind, 'complete-map-before-new-project'); + assert.strictEqual(parsed.next_action.command, '/gsd-map-codebase'); + assert.match(parsed.next_action.reason, /complete codebase map/i); + assert.deepStrictEqual(parsed.fast_codebase_map_files_required, [ + 'STACK.md', + 'INTEGRATIONS.md', + 'ARCHITECTURE.md', + 'STRUCTURE.md', + ]); + assert.deepStrictEqual(parsed.missing_fast_codebase_map_files, []); + }); + + test('routes fast mapped repositories with planning docs to complete map before ingest', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'codebase'), { recursive: true }); + for (const name of ['STACK', 'INTEGRATIONS', 'ARCHITECTURE', 'STRUCTURE']) { + fs.writeFileSync(path.join(tmpDir, '.planning', 'codebase', `${name}.md`), `# ${name}\n`); + } + fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"fixture"}\n'); + fs.mkdirSync(path.join(tmpDir, 'docs', 'adr'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, 'docs', 'adr', '0001-runtime.md'), '# ADR: Runtime\n'); + + const result = runGsdTools(['init', 'onboard', '--fast', '--raw'], tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.next_action.kind, 'complete-map-before-new-project'); + assert.strictEqual(parsed.next_action.command, '/gsd-map-codebase'); + }); + + test('routes planning artifacts without PROJECT.md to partial planning', () => { + fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), '# Requirements\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State\n'); + + const result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.next_action.kind, 'partial-planning'); + assert.deepStrictEqual(parsed.next_action.missing, ['PROJECT.md']); + }); + + test('fast mode leaves a fully set-up project at write-summary instead of the complete-map gate (regression #1990: fast map gate misroute)', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'codebase'), { recursive: true }); + for (const name of ['STACK', 'INTEGRATIONS', 'ARCHITECTURE', 'STRUCTURE']) { + fs.writeFileSync(path.join(tmpDir, '.planning', 'codebase', `${name}.md`), `# ${name}\n`); + } + fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"fixture"}\n'); + for (const name of ['PROJECT', 'REQUIREMENTS', 'ROADMAP', 'STATE']) { + fs.writeFileSync(path.join(tmpDir, '.planning', `${name}.md`), `# ${name}\n`); + } + + const result = runGsdTools(['init', 'onboard', '--fast', '--raw'], tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.map_readiness, 'fast'); + assert.strictEqual(parsed.project_exists, true); + // Once project setup is complete, a fast map must not misroute back to the + // pre-new-project complete-map handoff; onboarding advances to the summary. + assert.strictEqual(parsed.next_action.kind, 'write-summary'); + assert.strictEqual(parsed.next_action.summary_path, '.planning/onboarding/SUMMARY.md'); + }); + + test('fast mode routes incomplete planning to partial-planning before the complete-map gate (regression #1990: fast map gate misroute)', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'codebase'), { recursive: true }); + for (const name of ['STACK', 'INTEGRATIONS', 'ARCHITECTURE', 'STRUCTURE']) { + fs.writeFileSync(path.join(tmpDir, '.planning', 'codebase', `${name}.md`), `# ${name}\n`); + } + fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"fixture"}\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State\n'); + + const result = runGsdTools(['init', 'onboard', '--fast', '--raw'], tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.map_readiness, 'fast'); + // Incomplete planning must surface before the fast-map complete-map handoff. + assert.strictEqual(parsed.next_action.kind, 'partial-planning'); + assert.deepStrictEqual(parsed.next_action.missing, ['REQUIREMENTS.md']); + }); + + test('projects the next action for code, docs, greenfield, partial planning, and summary states', () => { + fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, 'src', 'server.ts'), 'export const server = true;\n'); + let result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + assert.deepStrictEqual(JSON.parse(result.output).next_action, { + kind: 'map-codebase', + command: '/gsd-map-codebase', + reason: 'Existing code was detected, but the required .planning/codebase/ map is missing.', + }); + + cleanup(tmpDir); + tmpDir = createFixture({ planning: false, projectDoc: false }); + fs.mkdirSync(path.join(tmpDir, 'docs', 'adr'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, 'docs', 'adr', '0001-runtime.md'), '# ADR: Runtime\n'); + result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + assert.deepStrictEqual(JSON.parse(result.output).next_action, { + kind: 'ingest-docs', + command: '/gsd-ingest-docs', + reason: 'Detected existing ADR/PRD/SPEC/RFC document(s) before project setup.', + }); + + cleanup(tmpDir); + tmpDir = createFixture({ planning: false, projectDoc: false }); + result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + assert.deepStrictEqual(JSON.parse(result.output).next_action, { + kind: 'new-project', + command: '/gsd-new-project', + reason: 'No existing code or planning docs were detected.', + }); + + cleanup(tmpDir); + tmpDir = createFixture({ planning: false, projectDoc: false }); + fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State\n'); + result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + const partial = JSON.parse(result.output); + assert.strictEqual(partial.next_action.kind, 'partial-planning'); + assert.deepStrictEqual(partial.next_action.missing, ['REQUIREMENTS.md']); + + fs.mkdirSync(path.join(tmpDir, '.planning', 'codebase'), { recursive: true }); + for (const name of ['STACK', 'ARCHITECTURE', 'STRUCTURE', 'CONVENTIONS', 'TESTING', 'INTEGRATIONS', 'CONCERNS']) { + fs.writeFileSync(path.join(tmpDir, '.planning', 'codebase', `${name}.md`), `# ${name}\n`); + } + fs.writeFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), '# Requirements\n'); + result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + assert.deepStrictEqual(JSON.parse(result.output).next_action, { + kind: 'write-summary', + summary_path: '.planning/onboarding/SUMMARY.md', + reason: 'Onboarding summary is missing.', + }); + + fs.mkdirSync(path.join(tmpDir, '.planning', 'onboarding'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, '.planning', 'onboarding', 'SUMMARY.md'), '# Onboarding Summary\n'); + result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + assert.deepStrictEqual(JSON.parse(result.output).next_action, { + kind: 'ready', + reason: 'Onboarding summary already exists.', + }); + }); + + test('reports missing requirements in otherwise existing planning', () => { + fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State\n'); + + const result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.project_exists, true); + assert.strictEqual(parsed.requirements_exists, false); + assert.strictEqual(parsed.roadmap_exists, true); + assert.strictEqual(parsed.state_exists, true); + }); + + test('ignores generated and vendor directories when detecting existing code', () => { + fs.mkdirSync(path.join(tmpDir, 'node_modules', 'pkg'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, 'node_modules', 'pkg', 'index.ts'), 'export const ignored = true;\n'); + fs.mkdirSync(path.join(tmpDir, 'dist'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, 'dist', 'bundle.js'), 'console.log("ignored");\n'); + + const result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.has_existing_code, false); + assert.strictEqual(parsed.has_package_file, false); + assert.strictEqual(parsed.is_brownfield, false); + assert.strictEqual(parsed.needs_codebase_map, false); + }); + + test('treats package manifests as brownfield even without source files', () => { + fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"fixture"}\n'); + + const result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.has_existing_code, false); + assert.strictEqual(parsed.has_package_file, true); + assert.strictEqual(parsed.is_brownfield, true); + assert.strictEqual(parsed.needs_codebase_map, true); + }); + + + test('formats onboard handoff commands for the resolved runtime', () => { + fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"fixture"}\n'); + + const result = runGsdTools(['init', 'onboard', '--raw'], tmpDir, { + HOME: tmpDir, + GSD_RUNTIME: 'codex', + }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.next_action.kind, 'map-codebase'); + assert.strictEqual(parsed.next_action.command, '$gsd-map-codebase'); + assert.deepStrictEqual(parsed.handoff_commands, { + map_codebase: '$gsd-map-codebase', + map_codebase_fast: '$gsd-map-codebase --fast', + ingest_docs: '$gsd-ingest-docs', + manager: '$gsd-manager', + new_project: '$gsd-new-project', + onboard: '$gsd-onboard', + }); + }); + + test('dotted query init.onboard matches direct init onboard', () => { + fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"fixture"}\n'); + + const direct = runGsdTools(['init', 'onboard', '--raw'], tmpDir, { HOME: tmpDir }); + const query = runGsdTools(['query', 'init.onboard', '--raw'], tmpDir, { HOME: tmpDir }); + assert.equal(direct.success, true, direct.error || direct.output); + assert.equal(query.success, true, query.error || query.output); + assert.deepStrictEqual(JSON.parse(query.output), JSON.parse(direct.output)); + }); + +}); + +describe('/gsd:onboard command contract', () => { + test('command file declares the onboard command and loads its workflow', () => { + const content = fs.readFileSync(CMD_PATH, 'utf8'); + assert.match(content, /^name:\s*gsd:onboard$/m); + assert.match(content, /^description:\s*.*(?:existing codebase|brownfield|onboard).*$/mi); + assert.match(content, /^\s*- AskUserQuestion$/m); + assert.match(content, /^\s*- Agent$/m); + assert.ok(content.includes('@~/.claude/gsd-core/workflows/onboard.md')); + assert.ok(content.includes('@~/.claude/gsd-core/references/ui-brand.md')); + assert.ok(content.includes('@~/.claude/gsd-core/references/gate-prompts.md')); + }); + + test('workflow renders the init projection without owning the route state machine', () => { + const content = fs.readFileSync(WF_PATH, 'utf8'); + + assert.ok(content.includes('@~/.claude/gsd-core/references/gsd-run-resolver.md')); + assert.ok( + !content.includes('_GSD_SHIM_NAME="gsd-tools.cjs"'), + 'workflow must reference the shared resolver instead of inlining it', + ); + assert.match(content, /init onboard --fast --raw/); + assert.match(content, /init onboard --raw/); + assert.match(content, /next_action\.kind/); + assert.match(content, /map_readiness/); + assert.match(content, /codebase_map_summary_status/); + assert.match(content, /ONBOARDING_ROOT=\{git_worktree_root \|\| _GSD_RUNTIME_ROOT\}/); + + for (const action of [ + 'map-codebase', + 'ingest-docs', + 'complete-map-before-new-project', + 'new-project', + 'partial-planning', + 'write-summary', + 'ready', + ]) { + assert.ok(content.includes(`next_action.kind == "${action}"`), `workflow must render ${action}`); + } + + assert.ok(content.includes('AskUserQuestion'), 'workflow must still support interactive choices'); + assert.ok(content.includes('--text'), 'workflow must document text-mode fallback'); + assert.ok( + content.includes('Exit. If the user skips mapping:'), + 'skip mapping must hand off explicitly instead of falling through to summary creation', + ); + const partialPlanningSkipGuard = + 'If `(project_exists || requirements_exists || roadmap_exists || state_exists) && (!project_exists || !requirements_exists || !roadmap_exists || !state_exists)`, route the skip to the partial planning guard instead:'; + const docsIngestSkipGuard = + 'If `has_docs_candidates && !project_exists`, route the skip to docs ingest instead:'; + assert.ok( + content.includes(partialPlanningSkipGuard), + 'skip mapping must preserve the partial-planning guard when incomplete planning artifacts are present', + ); + assert.ok( + content.includes(docsIngestSkipGuard), + 'skip mapping must preserve the docs-ingest gate when planning docs are present', + ); + assert.ok( + content.indexOf(partialPlanningSkipGuard) < content.indexOf(docsIngestSkipGuard), + 'skip mapping must check partial planning before docs ingest', + ); + assert.ok( + content.includes('{handoff_commands.ingest_docs}'), + 'skip mapping docs-preserving handoff must use the projected docs ingest command', + ); + assert.ok( + content.includes('Skipping codebase mapping may give {handoff_commands.new_project} weaker context.'), + 'skip mapping must warn about weaker context', + ); + assert.ok( + content.includes('Exit. If the user skips docs ingest, print:'), + 'skip docs ingest must hand off explicitly instead of falling through to summary creation', + ); + assert.ok( + content.includes('Skipping docs ingest may omit existing ADR/PRD/SPEC/RFC context from {handoff_commands.new_project}.'), + 'skip docs ingest must warn about omitted docs context', + ); + assert.match(content, /do not overwrite/i, 'workflow must protect existing summary/planning'); + assert.match(content, /query commit "docs: create onboarding summary" --files \.planning\/onboarding\/SUMMARY\.md/); + assert.ok(!content.includes('execute-phase'), 'onboarding must not execute implementation phases'); + assert.ok(!content.includes('gsd:ship'), 'onboarding must not ship work'); + assert.ok(content.includes('Next recommended command: {handoff_commands.manager}')); + assert.ok(!content.includes('/gsd:ingest-docs'), 'skip handoffs must not hard-code legacy ingest command'); + assert.ok(!content.includes('/gsd:new-project'), 'skip handoffs must not hard-code legacy new-project command'); + }); +}); diff --git a/tests/runtime-artifact-layout-install-profiles.test.cjs b/tests/runtime-artifact-layout-install-profiles.test.cjs index ed94af736..6361ec939 100644 --- a/tests/runtime-artifact-layout-install-profiles.test.cjs +++ b/tests/runtime-artifact-layout-install-profiles.test.cjs @@ -380,6 +380,14 @@ describe('resolveProfile', () => { } }); + test('standard profile includes the onboard manager handoff dependency', () => { + const manifest = loadSkillsManifest(REAL_COMMANDS_DIR); + const result = resolveProfile({ modes: ['standard'], manifest }); + assert.ok(result.skills instanceof Set); + assert.ok(result.skills.has('onboard'), 'standard profile should include onboard'); + assert.ok(result.skills.has('manager'), 'onboard handoff must install gsd-manager'); + }); + test('resolves full profile — returns sentinel', () => { const manifest = loadSkillsManifest(REAL_COMMANDS_DIR); const result = resolveProfile({ modes: ['full'], manifest }); diff --git a/tests/runtime-launcher-parity.test.cjs b/tests/runtime-launcher-parity.test.cjs index 8fada64b1..2f2448b7c 100644 --- a/tests/runtime-launcher-parity.test.cjs +++ b/tests/runtime-launcher-parity.test.cjs @@ -136,6 +136,22 @@ function collectAgentFiles() { return results; } +/** + * A workflow/agent file "delegates to the shared resolver" when it pulls the + * canonical gsd_run preamble in from gsd-core/references/gsd-run-resolver.md via + * an @-include instead of inlining the snippet (see onboard.md / issue #1990). + * + * Such files are exempt from the inline-preamble parity checks (B / G / H and the + * runtime-home propagation checks): they intentionally do NOT inline the preamble + * — onboard-command.test.cjs even asserts the absence of the inline form. Their + * resolver correctness is guaranteed transitively by: + * (1) onboard-command.test.cjs asserting the @-include is present, and + * (2) the "resolver reference stays byte-equal to the snippet" guard (B2) below. + */ +function delegatesToResolverReference(content) { + return content.includes('references/gsd-run-resolver.md'); +} + describe('runtime-launcher-parity (#373)', () => { // ─── (A) No retired GSD_SDK token ──────────────────────────────────────── test('(A) no GSD_SDK token in any workflow .md file', () => { @@ -170,6 +186,9 @@ describe('runtime-launcher-parity (#373)', () => { for (const f of files) { const rel = path.relative(WORKFLOWS_DIR, f); const content = fs.readFileSync(f, 'utf8'); + // Files that delegate to the shared resolver reference (@-include) do not + // inline the preamble — exempt them (see delegatesToResolverReference / (B2)). + if (delegatesToResolverReference(content)) continue; const blocks = extractShellBlocks(content); // Collect all block lines in document order for flat analysis @@ -226,6 +245,30 @@ describe('runtime-launcher-parity (#373)', () => { ); }); + // ─── (B2) Shared resolver reference stays byte-equal to the snippet ─────── + // Workflows may delegate to gsd-core/references/gsd-run-resolver.md instead of + // inlining the preamble (see delegatesToResolverReference). That delegation is + // only safe if the reference's bash block is byte-equal to the canonical + // snippet — otherwise a delegating workflow (e.g. onboard.md) would silently + // ship a drifted resolver. This guard replaces the inline-preamble checks for + // those files. + test('(B2) references/gsd-run-resolver.md preamble is byte-equal to the canonical snippet', () => { + const preambleStr = expectedPreamble().join('\n'); + const refPath = path.join(__dirname, '..', 'gsd-core', 'references', 'gsd-run-resolver.md'); + const refContent = fs.readFileSync(refPath, 'utf8'); + const refPreamble = extractShellBlocks(refContent) + .map((b) => b.lines.join('\n')) + .join('\n') + .trim(); + assert.equal( + refPreamble, + preambleStr, + 'gsd-core/references/gsd-run-resolver.md must contain the canonical gsd_run preamble ' + + 'byte-equal to _runtime-launcher.snippet.sh. Re-copy the snippet into the reference so ' + + 'workflows that delegate to it via @-include ship the current resolver.', + ); + }); + // ─── (C) Space-safe behavioral test ────────────────────────────────────── test('(C) gsd_run works with a RUNTIME_DIR path containing spaces', () => { // Create temp dir whose path contains a space @@ -402,6 +445,7 @@ describe('runtime-launcher-parity (#373)', () => { const content = fs.readFileSync(f, 'utf8'); const blocks = extractShellBlocks(content); const allBlockLines = blocks.flatMap((b) => b.lines); + if (delegatesToResolverReference(content)) continue; const fileHasGsdRun = allBlockLines.some((l) => /\bgsd_run\b/.test(l)); if (!fileHasGsdRun) continue; const allContent = allBlockLines.join('\n'); @@ -433,6 +477,7 @@ describe('runtime-launcher-parity (#373)', () => { const content = fs.readFileSync(f, 'utf8'); const blocks = extractShellBlocks(content); const allBlockLines = blocks.flatMap((b) => b.lines); + if (delegatesToResolverReference(content)) continue; const fileHasGsdRun = allBlockLines.some((l) => /\bgsd_run\b/.test(l)); if (!fileHasGsdRun) continue; if (!allBlockLines.join('\n').includes(CODEX_HOME_PROBE)) { @@ -1346,6 +1391,7 @@ describe('bug-891: non-Claude runtime home fallback arms', () => { const content = fs.readFileSync(f, 'utf8'); const blocks = extractShellBlocks(content); const allBlockLines = blocks.flatMap((b) => b.lines); + if (delegatesToResolverReference(content)) continue; const fileHasGsdRun = allBlockLines.some((l) => /\bgsd_run\b/.test(l)); if (!fileHasGsdRun) continue; const allContent = allBlockLines.join('\n'); diff --git a/tests/skill-frontmatter-contract.test.cjs b/tests/skill-frontmatter-contract.test.cjs index 7e8151c87..7aa9a0458 100644 --- a/tests/skill-frontmatter-contract.test.cjs +++ b/tests/skill-frontmatter-contract.test.cjs @@ -476,6 +476,8 @@ const KNOWN_SKILLS = new Set([ 'mvp-phase.md', 'new-milestone.md', 'new-project.md', + // #1990 brownfield onboarding entry command; sibling of new-project, routed under ns-project. + 'onboard.md', // `next.md` was legitimately reclaimed (#1787): the old workflow-advance // command stays absorbed into `progress.md --next`, while the current // `/gsd:next` is a different state-aware smart-entry launcher. diff --git a/tests/workflow-size-baseline.json b/tests/workflow-size-baseline.json index 63ccf264b..58e1158b4 100644 --- a/tests/workflow-size-baseline.json +++ b/tests/workflow-size-baseline.json @@ -20,7 +20,7 @@ "discuss-phase-assumptions.md": 27028, "discuss-phase-power.md": 11273, "discuss-phase.md": 31986, - "do.md": 10112, + "do.md": 10353, "docs-update.md": 55706, "edit-phase.md": 12927, "eval-review.md": 9967, @@ -50,6 +50,7 @@ "next.md": 20138, "node-repair.md": 4173, "note.md": 6563, + "onboard.md": 8590, "pause-work.md": 14441, "plan-milestone-gaps.md": 11809, "plan-phase.md": 92251, @@ -58,7 +59,7 @@ "pr-branch.md": 15963, "profile-user.md": 21246, "progress.md": 30599, - "quick.md": 49183, + "quick.md": 50452, "reapply-patches.md": 20312, "remove-phase.md": 8513, "remove-workspace.md": 7551, diff --git a/vscode/package.json b/vscode/package.json index f441d977d..19e308b9e 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -2,7 +2,7 @@ "name": "gsd-core-vscode", "displayName": "GSD Core", "description": "GSD orchestration engine embedded in VS Code (ADR-1239 IDE profile).", - "version": "1.7.0-rc.3", + "version": "1.7.0-rc.4", "publisher": "opengsd", "engines": { "vscode": "^1.90.0"