Merge branch 'next' into codex/gsd-onboard

This commit is contained in:
Tom Boucher
2026-07-06 22:43:32 -04:00
committed by GitHub
149 changed files with 6229 additions and 983 deletions

View File

@@ -0,0 +1,246 @@
'use strict';
/**
* Self-healing runtime build (#2002).
*
* The GSD runtime CLI (gsd-tools.cjs) require()s ~150 compiled modules from
* ./lib/*.cjs. Per ADR-457 ("build-at-publish") those are build artifacts:
* compiled from src/*.cts by `npm run build:lib` (tsc -p tsconfig.build.json),
* gitignored, and shipped prebuilt in the npm tarball via the prepack /
* prepublishOnly lifecycle scripts.
*
* The Claude Code plugin-marketplace channel does NOT go through `npm publish`
* or bin/install.js. Claude Code materializes the git tag tree into its plugin
* cache and at most runs `npm install --ignore-scripts`, so neither `prepare`
* nor `build:lib` ever fires. The compiled ./lib/*.cjs therefore never exist on
* that path and every CLI command dies at module load with
* `Cannot find module './lib/cli-exit.cjs'`.
*
* This module heals that: before gsd-tools.cjs require()s ./lib, if the compiled
* output is absent it invokes tsc once — lock-guarded so the many parallel
* gsd-tools shell-outs a workflow performs do not race the build — then lets the
* requires proceed. On the npm path the artifacts already exist, so the common
* case is a single fs.existsSync check and a no-op.
*
* Deliberately depends on nothing under ./lib (that tree is precisely what may
* be missing) — only on Node built-ins.
*/
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
// The first module gsd-tools.cjs require()s, and a gitignored build artifact
// (so it is absent on a raw git-tag checkout even though ~11 other bin/lib/*.cjs
// are git-tracked). tsconfig.build.json sets noEmitOnError:true, so tsc is
// all-or-nothing: if this file exists, the whole tree was emitted.
const SENTINEL = 'cli-exit.cjs';
/** Directory holding the compiled ./lib/*.cjs, relative to this file. */
function defaultLibDir() {
return path.join(__dirname, 'lib');
}
/**
* Package root (holds tsconfig.build.json + node_modules). This file lives at
* <root>/gsd-core/bin/ensure-runtime-build.cjs, so the root is two levels up —
* true for both the dev repo layout and the marketplace plugin-cache checkout
* (…/<version>/gsd-core/bin/ensure-runtime-build.cjs).
*/
function defaultPackageRoot() {
return path.resolve(__dirname, '..', '..');
}
/**
* Resolve the tsc entry SCRIPT (not the .bin/tsc shim). We run it as
* `node <tsc.js>` so behaviour is identical on POSIX and Windows and does not
* depend on a shell or on the .cmd shim. Returns null when TypeScript is not
* installed under packageRoot.
*/
function resolveTscScript(packageRoot) {
try {
return require.resolve('typescript/bin/tsc', { paths: [packageRoot] });
} catch {
return null;
}
}
function isBuilt(libDir) {
return fs.existsSync(path.join(libDir, SENTINEL));
}
/** Synchronous sleep with no dependencies, portable across platforms. */
function sleepSync(ms) {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(0, ms));
}
/**
* Raised when the runtime library is missing and cannot be auto-built. The
* message is user-facing and actionable; callers print `.message` (not a stack).
*/
class RuntimeBuildError extends Error {
constructor(message) {
super(message);
this.name = 'RuntimeBuildError';
this.code = 'GSD_RUNTIME_BUILD_FAILED';
}
}
/**
* Ensure the compiled ./lib/*.cjs exist, building them once if absent.
*
* Idempotent and safe to call concurrently from many processes. When another
* process holds the build lock, this one waits for the build to finish rather
* than launching a competing tsc.
*
* @param {object} [opts]
* @param {string} [opts.libDir] Override the compiled-output directory.
* @param {string} [opts.packageRoot] Override the tsconfig/node_modules root.
* @param {string|null} [opts.tscScript] Override tsc resolution (null = "absent").
* @param {function} [opts.spawn] Override spawnSync (for tests).
* @param {function} [opts.log] Override the progress logger.
* @param {number} [opts.waitTimeoutMs] Max wait for a peer build (default 120s).
* @param {number} [opts.pollMs] Poll interval while waiting (default 100ms).
* @param {function} [opts.onPoll] Test hook invoked before each wait-poll.
* @returns {{built: true, healed: boolean, waited?: boolean}}
* @throws {RuntimeBuildError} when the library is absent and cannot be built.
*/
function ensureRuntimeBuild(opts = {}) {
const libDir = opts.libDir || defaultLibDir();
const packageRoot = opts.packageRoot || defaultPackageRoot();
const spawn = opts.spawn || spawnSync;
const log = opts.log || ((m) => process.stderr.write(m + '\n'));
const waitTimeoutMs = opts.waitTimeoutMs != null ? opts.waitTimeoutMs : 120000;
const pollMs = opts.pollMs != null ? opts.pollMs : 100;
// Fast path: already built (the npm-registry install and every subsequent run).
if (isBuilt(libDir)) return { built: true, healed: false };
const tsconfig = path.join(packageRoot, 'tsconfig.build.json');
if (!fs.existsSync(tsconfig)) {
throw new RuntimeBuildError(
'GSD runtime library is not built and cannot be auto-built: ' +
`${tsconfig} not found. Run \`npm run build:lib\` in the gsd-core package.`,
);
}
const tscScript =
opts.tscScript !== undefined ? opts.tscScript : resolveTscScript(packageRoot);
if (!tscScript) {
throw new RuntimeBuildError(
`GSD runtime library is not built (missing ${path.join(libDir, SENTINEL)}) ` +
'and TypeScript is unavailable to build it. Run ' +
`\`npm install && npm run build:lib\` in the gsd-core package (${packageRoot}).`,
);
}
// libDir may not exist yet on a totally fresh tree; create it so the lock and
// tsc output have a home.
fs.mkdirSync(libDir, { recursive: true });
const lockDir = path.join(libDir, '.build.lock');
let haveLock = acquireLock(lockDir);
if (!haveLock) {
// A peer process is building. Wait for the sentinel rather than racing tsc.
const deadline = Date.now() + waitTimeoutMs;
while (!isBuilt(libDir)) {
if (typeof opts.onPoll === 'function') opts.onPoll();
if (isBuilt(libDir)) break;
if (Date.now() > deadline) break; // peer wedged/crashed — take over below
sleepSync(pollMs);
}
if (isBuilt(libDir)) return { built: true, healed: true, waited: true };
// Peer never finished; try to take over the (stale) lock.
haveLock = acquireLock(lockDir);
if (!haveLock && !isBuilt(libDir)) {
throw new RuntimeBuildError(
`GSD runtime build did not complete within ${waitTimeoutMs}ms and the ` +
`build lock (${lockDir}) is held by another process. Remove it and run ` +
'`npm run build:lib` if this persists.',
);
}
if (isBuilt(libDir)) return { built: true, healed: true, waited: true };
}
try {
log('gsd: runtime library not built — compiling once (tsc -p tsconfig.build.json)…');
// Heal only runs when the build is genuinely absent/broken, so force a full
// emit: a stale incremental cache from a partial build would make tsc report
// success without re-emitting the (still-missing) sentinel.
forceFullEmit(packageRoot, tsconfig);
const res = spawn(process.execPath, [tscScript, '-p', tsconfig], {
cwd: packageRoot,
stdio: ['ignore', 'pipe', 'pipe'],
encoding: 'utf8',
});
if (!res || res.status !== 0) {
const detail = ((res && (res.stderr || res.stdout)) || '').toString().trim();
throw new RuntimeBuildError(
`GSD runtime build failed (tsc exit ${res ? res.status : 'unknown'}). ` +
`Run \`npm run build:lib\` in ${packageRoot} to see the error.` +
(detail ? `\n${detail}` : ''),
);
}
} finally {
releaseLock(lockDir);
}
if (!isBuilt(libDir)) {
throw new RuntimeBuildError(
`GSD runtime build ran but ${path.join(libDir, SENTINEL)} is still missing. ` +
`Run \`npm run build:lib\` in ${packageRoot}.`,
);
}
return { built: true, healed: true };
}
/**
* Delete tsc's incremental build cache so the heal re-emits every module. The
* cache path defaults to tsconfig.build.tsbuildinfo (per tsconfig.build.json)
* but is read from the config when overridden. Best-effort: a missing or
* unreadable config just falls back to the default and a missing cache is fine.
*/
function forceFullEmit(packageRoot, tsconfigPath) {
let rel = 'tsconfig.build.tsbuildinfo';
try {
const cfg = JSON.parse(fs.readFileSync(tsconfigPath, 'utf8'));
if (cfg && cfg.compilerOptions && cfg.compilerOptions.tsBuildInfoFile) {
rel = cfg.compilerOptions.tsBuildInfoFile;
}
} catch {
/* unreadable/JSONC config — use the default cache path */
}
try {
fs.rmSync(path.resolve(packageRoot, rel), { force: true });
} catch {
/* best effort */
}
}
/** Atomically acquire the build lock. Returns true on success. */
function acquireLock(lockDir) {
try {
fs.mkdirSync(lockDir); // atomic — throws EEXIST if a peer holds it
return true;
} catch (e) {
if (e && e.code === 'EEXIST') return false;
throw e;
}
}
/** Best-effort lock release. */
function releaseLock(lockDir) {
try {
fs.rmSync(lockDir, { recursive: true, force: true });
} catch {
/* best effort — a leftover lock is recovered by the wait-then-takeover path */
}
}
module.exports = {
ensureRuntimeBuild,
resolveTscScript,
isBuilt,
RuntimeBuildError,
SENTINEL,
};

View File

@@ -195,6 +195,24 @@
const fs = require('fs');
const path = require('path');
// #2002 — self-healing runtime build. The compiled ./lib/*.cjs modules this
// entrypoint require()s below are gitignored build artifacts (ADR-457), shipped
// prebuilt in the npm tarball. The Claude Code plugin-marketplace channel never
// runs `npm run build:lib` or bin/install.js, so on that path they can be
// absent and every command dies at module load. Compile them once (lock-guarded,
// idempotent, a no-op when already built) before the ./lib requires run.
const { ensureRuntimeBuild } = require('./ensure-runtime-build.cjs');
try {
ensureRuntimeBuild();
} catch (bootErr) {
process.stderr.write((bootErr && bootErr.message ? bootErr.message : String(bootErr)) + '\n');
// Fatal bootstrap failure before the CLI's ExitError/runMain machinery (which
// lives in ./lib) is available to load, so a direct exit is the only option.
// eslint-disable-next-line n/no-process-exit
process.exit(1);
}
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
const io = require('./lib/io.cjs');
const { error, ERROR_REASON, setJsonErrorMode, output } = io;

View File

@@ -10,7 +10,7 @@ const capabilities = {
"ai-integration": {
"id": "ai-integration",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "AI design contract",
"description": "AI-SPEC design contract workflow for phases that build AI systems; owns the AI integration command, agents, and workflow.ai_integration_phase activation key.",
"tier": "full",
@@ -63,7 +63,7 @@ const capabilities = {
"antigravity": {
"id": "antigravity",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Antigravity",
"description": "Google Antigravity IDE — nested under ~/.gemini/antigravity; probed across 1.x and 2.x layouts; Gemini hook event dialect; flat skill layout; tier-1 support.",
"tier": "core",
@@ -97,6 +97,14 @@ const capabilities = {
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeCommandToAntigravitySkill"
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeAgentToAntigravityAgent"
}
],
"local": [
@@ -107,6 +115,14 @@ const capabilities = {
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeCommandToAntigravitySkill"
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeAgentToAntigravityAgent"
}
]
},
@@ -141,7 +157,7 @@ const capabilities = {
"assumption-delta": {
"id": "assumption-delta",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Assumption-delta architecture checkpoint",
"description": "Rarely-firing advisory checkpoint that triggers when a phase makes something plural, optional, or chosen that used to be singular, required, or derived. Surfaces one identity-model question (promote the new general representation to primary, or add it alongside?) so a silent primary-key drift does not accumulate into a later user-facing bug. Non-blocking; fires only on a detected signal.",
"tier": "full",
@@ -187,7 +203,7 @@ const capabilities = {
"audit": {
"id": "audit",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Audit",
"description": "Open-artifact audit and UAT-gap audit for milestone close gates; exposes `gsd-tools audit-uat` (cross-phase UAT outstanding items) and `gsd-tools audit-open` (structured open-artifact scan across debug, tasks, threads, todos, seeds, UAT, verification, context-questions).",
"tier": "full",
@@ -224,7 +240,7 @@ const capabilities = {
"augment": {
"id": "augment",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Augment Code",
"description": "Augment Code CLI — commands + nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",
@@ -327,7 +343,7 @@ const capabilities = {
"claude": {
"id": "claude",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Claude Code",
"description": "Anthropic Claude Code — primary development runtime; tier-1 support with full hook surface and skills-based global install.",
"tier": "core",
@@ -408,10 +424,97 @@ const capabilities = {
}
}
},
"claude-orchestration": {
"id": "claude-orchestration",
"role": "feature",
"version": "1.7.0-rc.3",
"title": "Claude orchestration (Workflow backend)",
"description": "Default-off, BETA, claude-only capability that adopts Claude Code's Workflow tool (the engine behind /effort ultracode) as an optional parallel-execution backend for the GSD loop. When the runtime exposes the Workflow tool and claude_orchestration.execution_backend resolves to 'workflow', execute-phase emits a generated Workflow script (waves -> parallel() barriers, plans -> agent({ agentType: 'gsd-executor', isolation: 'worktree' }), files_modified overlap -> separate sequential stages, resumeFromRunId wired to the phase run id, shared token budget) that composes the SAME gsd-executor agent and worktree isolation the inline path uses, restoring the wave parallelism the #853 backgrounded-agent nesting limitation forces inline on Claude Code. (The plan-checker and verifier remain inline until separately wired — this capability delivers the parallel-execution backend, not those gates.) Also folds the ultraplan plan-offload under one runtime gate (plan:* surface). On any runtime lacking the Workflow tool, or when the capability is disabled, behaviour is byte-identical to today (inline/manual dispatch). Detection + emission live in gsd-core/bin/lib/claude-orchestration.cjs (pure, fail-closed). Mirrors the existing gsd-ultraplan-phase BETA-isolation posture.",
"tier": "full",
"requires": [],
"engines": {
"gsd": ">=1.7.0"
},
"runtimeCompat": {
"supported": [
"claude"
],
"unsupported": []
},
"skills": [],
"agents": [],
"hooks": [],
"commands": [
{
"family": "claude-orchestration",
"module": "claude-orchestration-command-router.cjs",
"router": "routeClaudeOrchestrationCommand",
"subcommands": [
"detect-backend",
"emit-workflow"
]
}
],
"activationKey": "claude_orchestration.enabled",
"config": {
"claude_orchestration.enabled": {
"type": "boolean",
"default": false,
"description": "Master toggle for the Claude orchestration capability. Default-off + BETA: the Workflow-tool execution backend and the ultraplan plan-offload surface are inert unless this is true. When false, loop behaviour is byte-identical to a non-Claude runtime (inline/manual dispatch)."
},
"claude_orchestration.execution_backend": {
"type": "enum",
"values": [
"auto",
"workflow",
"inline"
],
"default": "auto",
"description": "Which execute-phase dispatch backend to use when the capability is enabled. 'auto' (default) activates the Workflow backend only when the runtime is Claude AND the Workflow tool is detected AND the Agent SDK meets claude_orchestration.min_agent_sdk_version; otherwise it falls back to inline. 'workflow' forces the Workflow backend when the tool is present AND the Agent SDK meets the floor (still fails closed to inline if the tool is absent or the SDK is too old — the floor applies in both modes). 'inline' forces today's manual one-agent-per-message dispatch regardless of tool availability."
},
"claude_orchestration.min_agent_sdk_version": {
"type": "string",
"default": "0.3.149",
"description": "Minimum Agent SDK version required to activate the Workflow backend under execution_backend='auto'. Defaults to 0.3.149 (the release that introduced the Workflow tool). Raise to pin a higher floor; the detection seam fails closed to inline for any runtime reporting an older or unknown version."
}
},
"steps": [],
"contributions": [
{
"point": "execute:wave:post",
"into": "executor",
"fragment": {
"path": "fragments/execute-wave-post.md",
"inline": "# Claude orchestration — Workflow execution backend (BETA)\n\n> Injected at `execute:wave:post` `into: executor` only when\n> `claude_orchestration.enabled` is true. Default-off; `onError: skip`.\n\n## When this contribution is active\n\nThe Claude orchestration capability is **default-off and BETA**. It activates only\nwhen ALL of the following hold:\n\n1. `claude_orchestration.enabled` is `true` in `.planning/config.json`, AND\n2. the active runtime is **Claude Code** (the Workflow tool is Claude / Agent\n SDK-specific), AND\n3. `claude_orchestration.execution_backend` resolves to `workflow` — either\n explicitly, or via `auto` — **and** the Agent SDK version is\n `>= claude_orchestration.min_agent_sdk_version` (default `0.3.149`). The SDK\n floor applies in both `auto` and `workflow` modes (fail-closed: a pre-release\n or older SDK never activates the preview backend).\n\nDetection is fail-closed: any miss degrades to **inline, manual, one-agent-per-\nmessage dispatch** — exactly today's behaviour. On a non-Claude runtime this\ncontribution is a no-op.\n\n## What the executor does when the Workflow backend is active\n\nInstead of the orchestrator fanning out one `Agent(subagent_type=gsd-executor,\nisolation=worktree, run_in_background=true)` per message (which on Claude Code\ncannot nest further subagents — #853 — and so degrades to sequential inline\nexecution), execute-phase **emits a generated Workflow script** and lets the main\nloop orchestrate it:\n\n- **waves → one or more sequential `parallel()` barriers** — each wave is a\n barrier group; when plans within a wave share `files_modified`, they are split\n into separate sequential stages within that wave's barrier (the next wave\n still waits for the previous wave to complete).\n- **plans → `agent(brief, { agentType: 'gsd-executor', isolation: 'worktree' })`**\n — the SAME executor agent and worktree isolation the inline path uses, so the\n produced `SUMMARY.md` and commits are identical.\n- **`files_modified` overlap → separate sequential stages** — two plans that\n touch the same file are placed in different stages within the wave (the same\n overlap rule execute-phase already applies inline).\n- **`resumeFromRunId`** — wired to the phase run id, so an interrupted phase\n resumes without re-running completed plans.\n- **`budget(tokens)`** — a shared token pool across the whole phase when the\n orchestrator passes a `budgetTokens` value to `emitWorkflowScript` (it is a\n function parameter, not a config key; the orchestrator decides the budget).\n\nThe emitter is a pure function exposed through the capability command surface:\n`gsd-tools claude-orchestration emit-workflow --waves <manifest.json> --run-id <id>\n[--phase-dir <dir>] [--budget <n>]` (or `require('gsd-core/bin/lib/claude-orchestration.cjs').emitWorkflowScript`\ndirectly). It maps the phase's wave/plan manifest to the Workflow script string\nand never invokes the Workflow tool itself; the orchestrator runs the emitted\nscript. Detection is resolved by the orchestrator calling the pure\n`detectWorkflowBackend` with the LIVE host descriptor (the CLI\n`gsd-tools claude-orchestration detect-backend` is a simulation harness that\nassumes a capable host unless `--no-nested-dispatch` is passed — it does not probe\nthe real runtime; the orchestrator supplies the real descriptor).\n\n## Fallback contract\n\nIf detection resolves to `inline` (tool absent, SDK too old, runtime not Claude,\nor the capability disabled), execute-phase MUST proceed with the standard inline\nwave dispatch. The executor MUST NOT assume parallelism, a shared budget, or\nresume-from-run-id semantics in that mode.\n"
},
"produces": [],
"consumes": [
"PLAN.md"
],
"when": "claude_orchestration.enabled",
"onError": "skip"
},
{
"point": "plan:post",
"into": "planner",
"fragment": {
"path": "fragments/plan-post.md",
"inline": "# Claude orchestration — ultraplan plan-offload ownership (BETA)\n\n> Injected at `plan:post` `into: planner` only when\n> `claude_orchestration.enabled` is true. Default-off; `onError: skip`.\n\n## Ownership declaration\n\nThe `gsd-ultraplan-phase` plan-offload surface (offloading GSD's plan phase to\nClaude Code's ultraplan cloud) is **owned by this capability**, not by a\nstandalone BETA skill. Both surfaces share one runtime gate\n(`claude_orchestration.enabled`), one BETA boundary, and one Claude-Code-only\ndetection seam.\n\n## When the planner should consider ultraplan offload\n\nWhen this contribution is active (capability enabled, Claude Code runtime), the\nplanner MAY offer the `/gsd-ultraplan-phase` path as an alternative to local\n`/gsd-plan-phase` for phases where cloud-assisted planning adds value. This is\nadvisory, not mandatory — the stable local planner remains the default.\n\n## Fallback contract\n\nIf the capability is disabled, or the runtime is not Claude Code, ultraplan\noffload is **not surfaced** and the planner proceeds with the standard local\n`/gsd-plan-phase`. The `gsd-ultraplan-phase` command itself remains installed\n(its own runtime gate already no-ops on non-Claude runtimes); this contribution\nonly governs whether the capability manifest advertises it as part of the\norchestration surface.\n"
},
"produces": [],
"consumes": [
"CONTEXT.md"
],
"when": "claude_orchestration.enabled",
"onError": "skip"
}
],
"gates": []
},
"cline": {
"id": "cline",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Cline",
"description": "Cline (VS Code extension) — global-only nested-skill layout; cline-rules hook surface (.clinerules); no hook events emitted; tier-2 support.",
"tier": "core",
@@ -472,7 +575,7 @@ const capabilities = {
"code-review": {
"id": "code-review",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Code review",
"description": "Source-file code review and review-fix workflow support for completed execution work.",
"tier": "full",
@@ -533,7 +636,7 @@ const capabilities = {
"codebuddy": {
"id": "codebuddy",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "CodeBuddy",
"description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",
@@ -636,7 +739,7 @@ const capabilities = {
"codex": {
"id": "codex",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "OpenAI Codex CLI",
"description": "OpenAI Codex CLI — shell-var command style; per-agent sandbox tiers; config.toml + hooks.json hook surface; tier-1 support.",
"tier": "core",
@@ -707,7 +810,7 @@ const capabilities = {
"copilot": {
"id": "copilot",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "GitHub Copilot",
"description": "GitHub Copilot (VS Code) — markdown config format; copilot-inline hook surface; no hook events emitted; flat skill nesting (unconfirmed recursive loader); tier-2 support.",
"tier": "core",
@@ -735,6 +838,14 @@ const capabilities = {
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeCommandToCopilotSkill"
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeAgentToCopilotAgent"
}
],
"local": [
@@ -745,6 +856,14 @@ const capabilities = {
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeCommandToCopilotSkill"
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeAgentToCopilotAgent"
}
]
},
@@ -778,7 +897,7 @@ const capabilities = {
"cursor": {
"id": "cursor",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Cursor",
"description": "Cursor IDE — skills + converted commands artifact layout; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.",
"tier": "core",
@@ -881,7 +1000,7 @@ const capabilities = {
"drift": {
"id": "drift",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Drift detection gates",
"description": "Drift detection gates for the planning loop. At execute:wave:post: a blocking schema drift gate (detects schema files changed without a database push) and a non-blocking codebase drift gate (detects structural additions not reflected in STRUCTURE.md). At plan:pre: a non-blocking, warn-only codebase drift gate (gated on workflow.plan_drift_precheck) that flags a stale codebase map before planning, so plans are authored against a fresh STRUCTURE.md instead of discovering drift mid-execution.",
"tier": "full",
@@ -959,7 +1078,7 @@ const capabilities = {
"external-job": {
"id": "external-job",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Async external-job scheduler adapter",
"description": "Default-off producer of the async external-job manifest (#1164). At execute:wave:post an executor can externalize long-running compute (SLURM first, scheduler-pluggable), commit a .planning/async-jobs/<job>.json manifest, defer SUMMARY.md, and return external_job_waiting. The core loop (#1165) consumes the manifest; this capability is the only thing that writes it. NOTE on contribution point: #1164 specifies execute:wave:pre, but execute-phase.md only dispatches execute:wave:post today (wave:pre is declared in the loop host contract but not rendered); wiring wave:pre dispatch is a core-loop change #1164 explicitly puts out of scope, so this capability registers at wave:post and the executor honors the runtime_budget classification guidance before running any tagged task. The adapter (scripts/slurm-adapter.cjs) reads external_job.submit_timeout_ms / poll_timeout_ms / artifact_dir through the canonical capability-config seam (env override > config > registry default).",
"tier": "full",
@@ -1042,7 +1161,7 @@ const capabilities = {
"gap-analysis": {
"id": "gap-analysis",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Post-planning gap analysis",
"description": "Proactive, non-blocking post-planning coverage report. After all PLAN.md files are generated, cross-references every REQ-ID and D-ID from REQUIREMENTS.md and CONTEXT.md against plan bodies. Emits a Source | Item | Status table. Does not block phase advancement.",
"tier": "standard",
@@ -1083,7 +1202,7 @@ const capabilities = {
"graphify": {
"id": "graphify",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Knowledge graph",
"description": "Build, query, and inspect the project knowledge graph in `.planning/graphs/`; exposes graphify CLI subcommands (build, query, status, diff) and the /gsd-graphify skill.",
"tier": "full",
@@ -1124,7 +1243,7 @@ const capabilities = {
"hermes": {
"id": "hermes",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Hermes Agent",
"description": "Hermes Agent (NousResearch) — skills nest under skills/gsd/ category bucket; nested skill layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",
@@ -1195,7 +1314,7 @@ const capabilities = {
"intel": {
"id": "intel",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Codebase intelligence",
"description": "Code-intelligence store for codebase querying, diff, snapshot, and API-surface extraction; exposes `gsd-tools intel` subcommands (query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface) and backs `/gsd-map-codebase` and `gsd-intel-updater`.",
"tier": "full",
@@ -1247,7 +1366,7 @@ const capabilities = {
"kilo": {
"id": "kilo",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Kilo Code",
"description": "Kilo Code — XDG-based config dir; global skills at ~/.kilo/skills (separate from XDG config); flat command/ + skills artifact layout; no lifecycle hook registration; tier-2 support.",
"tier": "core",
@@ -1340,7 +1459,7 @@ const capabilities = {
"kimi": {
"id": "kimi",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Kimi CLI",
"description": "Kimi CLI (Moonshot AI) — generic agents root at ~/.config/agents; skills + kimi-agents artifact layout; no hook surface; no hook events; tier-2 support.",
"tier": "core",
@@ -1414,7 +1533,7 @@ const capabilities = {
"mempalace": {
"id": "mempalace",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "MemPalace memory",
"description": "Cross-session, cross-project memory: deliberate recall before discuss/plan and verbatim capture + temporal-KG sync at phase boundaries, via the MemPalace MCP server and CLI.",
"tier": "full",
@@ -1588,7 +1707,7 @@ const capabilities = {
"nyquist": {
"id": "nyquist",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Nyquist validation",
"description": "Validation coverage audit that maps executed work back to tests and manual-only evidence.",
"tier": "full",
@@ -1638,7 +1757,7 @@ const capabilities = {
"opencode": {
"id": "opencode",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "OpenCode",
"description": "OpenCode — XDG-based config dir; flat command/ + skills artifact layout; settings-json config format; no lifecycle hook registration; tier-2 support.",
"tier": "core",
@@ -1727,7 +1846,7 @@ const capabilities = {
"pattern-mapper": {
"id": "pattern-mapper",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Pattern mapping",
"description": "Optional codebase-pattern mapping before planning; owns the pattern mapper agent and workflow.pattern_mapper activation key.",
"tier": "full",
@@ -1781,7 +1900,7 @@ const capabilities = {
"profile-pipeline": {
"id": "profile-pipeline",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Developer profiling pipeline",
"description": "Developer behavioral profiling from Claude Code session history; scans session JSONL files, extracts and samples user messages, and generates profile artifacts (USER-PROFILE.md, dev-preferences.md, CLAUDE.md sections). Exposes eight `gsd-tools` commands: scan-sessions, extract-messages, profile-sample (pipeline phase) and write-profile, profile-questionnaire, generate-dev-preferences, generate-claude-profile, generate-claude-md (output phase). Backs the /gsd-profile-user skill and gsd-user-profiler agent.",
"tier": "full",
@@ -1858,7 +1977,7 @@ const capabilities = {
"qwen": {
"id": "qwen",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Qwen Code",
"description": "Qwen Code (Alibaba) — nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",
@@ -1933,7 +2052,7 @@ const capabilities = {
"research": {
"id": "research",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Phase research",
"description": "Optional phase research before planning; owns the phase researcher agent and workflow.research activation key.",
"tier": "standard",
@@ -1985,7 +2104,7 @@ const capabilities = {
"schema-gate": {
"id": "schema-gate",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Schema push detection gate",
"description": "Detects ORM schema-relevant files in the phase scope during planning and injects a mandatory [BLOCKING] schema push task into the plan. Prevents false-positive verification where build/types pass because TypeScript types come from config, not the live database.",
"tier": "full",
@@ -2031,7 +2150,7 @@ const capabilities = {
"security": {
"id": "security",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Security enforcement",
"description": "Threat mitigation verification and ship-time security blocking for phases with security enforcement enabled.",
"tier": "full",
@@ -2130,7 +2249,7 @@ const capabilities = {
"tdd": {
"id": "tdd",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Test-driven development",
"description": "Injects TDD heuristics into the planner and enforces RED/GREEN gate compliance on type:tdd plans after execution. Owns workflow.tdd_mode; the --tdd CLI flag is the ephemeral override.",
"tier": "full",
@@ -2183,7 +2302,7 @@ const capabilities = {
"trae": {
"id": "trae",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Trae IDE",
"description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.",
"tier": "core",
@@ -2269,7 +2388,7 @@ const capabilities = {
"ui": {
"id": "ui",
"role": "feature",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "UI design contracts",
"description": "UI-SPEC design contract + retrospective UI audit for frontend phases.",
"tier": "full",
@@ -2364,7 +2483,7 @@ const capabilities = {
"windsurf": {
"id": "windsurf",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Windsurf",
"description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.",
"tier": "core",
@@ -2439,6 +2558,108 @@ const capabilities = {
"runtime": "undocumented"
}
}
},
"zcode": {
"id": "zcode",
"role": "runtime",
"version": "1.7.0-rc.3",
"title": "ZCode",
"description": "ZCode (Z.ai) — desktop Agentic Development Environment for GLM-5.2; Claude-shaped nested skills at ~/.zcode/skills/<name>/SKILL.md, slash commands, named subagents, native MCP; declarative plugin surface; profile-marker install; tier-2 community support.",
"tier": "core",
"requires": [],
"engines": {
"gsd": ">=1.6.0"
},
"runtime": {
"configHome": {
"kind": "dot-home",
"name": ".zcode",
"env": [
"ZCODE_CONFIG_DIR"
]
},
"localConfigDir": ".zcode",
"configFormat": "none",
"artifactLayout": {
"global": [
{
"kind": "skills",
"destSubpath": "skills",
"prefix": "gsd-",
"nesting": "nested",
"recursive": false,
"converter": "convertClaudeCommandToClaudeSkill"
},
{
"kind": "commands",
"destSubpath": "commands",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
}
],
"local": [
{
"kind": "skills",
"destSubpath": "skills",
"prefix": "gsd-",
"nesting": "nested",
"recursive": false,
"converter": "convertClaudeCommandToClaudeSkill"
},
{
"kind": "commands",
"destSubpath": "commands",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
}
]
},
"commandStyle": "slash-hyphen",
"hooksSurface": "none",
"sandboxTier": "none",
"supportTier": 2,
"installSurface": "profile-marker-only",
"writesSharedSettings": false,
"permissionWriter": null,
"extendedHookEvents": [],
"hostIntegration": {
"embeddingMode": "declarative",
"commandSurface": "slash-file",
"dispatch": {
"namedDispatch": true,
"nested": "undocumented",
"maxDepth": "undocumented",
"background": false,
"subagentToolkit": "full",
"backgroundDispatch": false
},
"modelMode": "passive",
"hookBus": "host",
"stateIO": "filesystem",
"transport": "mcp",
"runtime": "electron"
}
}
}
};
@@ -2711,6 +2932,21 @@ const byLoopPoint = {
}
],
"contributions": [
{
"capId": "claude-orchestration",
"point": "plan:post",
"into": "planner",
"fragment": {
"path": "fragments/plan-post.md",
"inline": "# Claude orchestration — ultraplan plan-offload ownership (BETA)\n\n> Injected at `plan:post` `into: planner` only when\n> `claude_orchestration.enabled` is true. Default-off; `onError: skip`.\n\n## Ownership declaration\n\nThe `gsd-ultraplan-phase` plan-offload surface (offloading GSD's plan phase to\nClaude Code's ultraplan cloud) is **owned by this capability**, not by a\nstandalone BETA skill. Both surfaces share one runtime gate\n(`claude_orchestration.enabled`), one BETA boundary, and one Claude-Code-only\ndetection seam.\n\n## When the planner should consider ultraplan offload\n\nWhen this contribution is active (capability enabled, Claude Code runtime), the\nplanner MAY offer the `/gsd-ultraplan-phase` path as an alternative to local\n`/gsd-plan-phase` for phases where cloud-assisted planning adds value. This is\nadvisory, not mandatory — the stable local planner remains the default.\n\n## Fallback contract\n\nIf the capability is disabled, or the runtime is not Claude Code, ultraplan\noffload is **not surfaced** and the planner proceeds with the standard local\n`/gsd-plan-phase`. The `gsd-ultraplan-phase` command itself remains installed\n(its own runtime gate already no-ops on non-Claude runtimes); this contribution\nonly governs whether the capability manifest advertises it as part of the\norchestration surface.\n"
},
"produces": [],
"consumes": [
"CONTEXT.md"
],
"when": "claude_orchestration.enabled",
"onError": "skip"
},
{
"capId": "external-job",
"point": "plan:post",
@@ -2751,6 +2987,21 @@ const byLoopPoint = {
"execute:wave:post": {
"steps": [],
"contributions": [
{
"capId": "claude-orchestration",
"point": "execute:wave:post",
"into": "executor",
"fragment": {
"path": "fragments/execute-wave-post.md",
"inline": "# Claude orchestration — Workflow execution backend (BETA)\n\n> Injected at `execute:wave:post` `into: executor` only when\n> `claude_orchestration.enabled` is true. Default-off; `onError: skip`.\n\n## When this contribution is active\n\nThe Claude orchestration capability is **default-off and BETA**. It activates only\nwhen ALL of the following hold:\n\n1. `claude_orchestration.enabled` is `true` in `.planning/config.json`, AND\n2. the active runtime is **Claude Code** (the Workflow tool is Claude / Agent\n SDK-specific), AND\n3. `claude_orchestration.execution_backend` resolves to `workflow` — either\n explicitly, or via `auto` — **and** the Agent SDK version is\n `>= claude_orchestration.min_agent_sdk_version` (default `0.3.149`). The SDK\n floor applies in both `auto` and `workflow` modes (fail-closed: a pre-release\n or older SDK never activates the preview backend).\n\nDetection is fail-closed: any miss degrades to **inline, manual, one-agent-per-\nmessage dispatch** — exactly today's behaviour. On a non-Claude runtime this\ncontribution is a no-op.\n\n## What the executor does when the Workflow backend is active\n\nInstead of the orchestrator fanning out one `Agent(subagent_type=gsd-executor,\nisolation=worktree, run_in_background=true)` per message (which on Claude Code\ncannot nest further subagents — #853 — and so degrades to sequential inline\nexecution), execute-phase **emits a generated Workflow script** and lets the main\nloop orchestrate it:\n\n- **waves → one or more sequential `parallel()` barriers** — each wave is a\n barrier group; when plans within a wave share `files_modified`, they are split\n into separate sequential stages within that wave's barrier (the next wave\n still waits for the previous wave to complete).\n- **plans → `agent(brief, { agentType: 'gsd-executor', isolation: 'worktree' })`**\n — the SAME executor agent and worktree isolation the inline path uses, so the\n produced `SUMMARY.md` and commits are identical.\n- **`files_modified` overlap → separate sequential stages** — two plans that\n touch the same file are placed in different stages within the wave (the same\n overlap rule execute-phase already applies inline).\n- **`resumeFromRunId`** — wired to the phase run id, so an interrupted phase\n resumes without re-running completed plans.\n- **`budget(tokens)`** — a shared token pool across the whole phase when the\n orchestrator passes a `budgetTokens` value to `emitWorkflowScript` (it is a\n function parameter, not a config key; the orchestrator decides the budget).\n\nThe emitter is a pure function exposed through the capability command surface:\n`gsd-tools claude-orchestration emit-workflow --waves <manifest.json> --run-id <id>\n[--phase-dir <dir>] [--budget <n>]` (or `require('gsd-core/bin/lib/claude-orchestration.cjs').emitWorkflowScript`\ndirectly). It maps the phase's wave/plan manifest to the Workflow script string\nand never invokes the Workflow tool itself; the orchestrator runs the emitted\nscript. Detection is resolved by the orchestrator calling the pure\n`detectWorkflowBackend` with the LIVE host descriptor (the CLI\n`gsd-tools claude-orchestration detect-backend` is a simulation harness that\nassumes a capable host unless `--no-nested-dispatch` is passed — it does not probe\nthe real runtime; the orchestrator supplies the real descriptor).\n\n## Fallback contract\n\nIf detection resolves to `inline` (tool absent, SDK too old, runtime not Claude,\nor the capability disabled), execute-phase MUST proceed with the standard inline\nwave dispatch. The executor MUST NOT assume parallelism, a shared budget, or\nresume-from-run-id semantics in that mode.\n"
},
"produces": [],
"consumes": [
"PLAN.md"
],
"when": "claude_orchestration.enabled",
"onError": "skip"
},
{
"capId": "external-job",
"point": "execute:wave:post",
@@ -2961,6 +3212,9 @@ const byLoopPoint = {
const configKeys = {
"workflow.ai_integration_phase": "ai-integration",
"workflow.assumption_delta": "assumption-delta",
"claude_orchestration.enabled": "claude-orchestration",
"claude_orchestration.execution_backend": "claude-orchestration",
"claude_orchestration.min_agent_sdk_version": "claude-orchestration",
"workflow.code_review": "code-review",
"workflow.code_review_depth": "code-review",
"workflow.drift_threshold": "drift",
@@ -3012,6 +3266,29 @@ const configSchema = {
"default": true,
"description": "Enable the assumption-delta architecture checkpoint during planning. When a pluralization/optional/chosen signal is detected in the phase scope, the planner is prompted to re-ask whether the primary key / identity model still names the right thing. Advisory (non-blocking)."
},
"claude_orchestration.enabled": {
"owner": "claude-orchestration",
"type": "boolean",
"default": false,
"description": "Master toggle for the Claude orchestration capability. Default-off + BETA: the Workflow-tool execution backend and the ultraplan plan-offload surface are inert unless this is true. When false, loop behaviour is byte-identical to a non-Claude runtime (inline/manual dispatch)."
},
"claude_orchestration.execution_backend": {
"owner": "claude-orchestration",
"type": "enum",
"default": "auto",
"description": "Which execute-phase dispatch backend to use when the capability is enabled. 'auto' (default) activates the Workflow backend only when the runtime is Claude AND the Workflow tool is detected AND the Agent SDK meets claude_orchestration.min_agent_sdk_version; otherwise it falls back to inline. 'workflow' forces the Workflow backend when the tool is present AND the Agent SDK meets the floor (still fails closed to inline if the tool is absent or the SDK is too old — the floor applies in both modes). 'inline' forces today's manual one-agent-per-message dispatch regardless of tool availability.",
"values": [
"auto",
"workflow",
"inline"
]
},
"claude_orchestration.min_agent_sdk_version": {
"owner": "claude-orchestration",
"type": "string",
"default": "0.3.149",
"description": "Minimum Agent SDK version required to activate the Workflow backend under execution_backend='auto'. Defaults to 0.3.149 (the release that introduced the Workflow tool). Raise to pin a higher floor; the detection seam fails closed to inline for any runtime reporting an older or unknown version."
},
"workflow.code_review": {
"owner": "code-review",
"type": "boolean",
@@ -3258,7 +3535,7 @@ const runtimes = {
"antigravity": {
"id": "antigravity",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Antigravity",
"description": "Google Antigravity IDE — nested under ~/.gemini/antigravity; probed across 1.x and 2.x layouts; Gemini hook event dialect; flat skill layout; tier-1 support.",
"tier": "core",
@@ -3292,6 +3569,14 @@ const runtimes = {
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeCommandToAntigravitySkill"
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeAgentToAntigravityAgent"
}
],
"local": [
@@ -3302,6 +3587,14 @@ const runtimes = {
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeCommandToAntigravitySkill"
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeAgentToAntigravityAgent"
}
]
},
@@ -3336,7 +3629,7 @@ const runtimes = {
"augment": {
"id": "augment",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Augment Code",
"description": "Augment Code CLI — commands + nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",
@@ -3439,7 +3732,7 @@ const runtimes = {
"claude": {
"id": "claude",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Claude Code",
"description": "Anthropic Claude Code — primary development runtime; tier-1 support with full hook surface and skills-based global install.",
"tier": "core",
@@ -3523,7 +3816,7 @@ const runtimes = {
"cline": {
"id": "cline",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Cline",
"description": "Cline (VS Code extension) — global-only nested-skill layout; cline-rules hook surface (.clinerules); no hook events emitted; tier-2 support.",
"tier": "core",
@@ -3584,7 +3877,7 @@ const runtimes = {
"codebuddy": {
"id": "codebuddy",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "CodeBuddy",
"description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",
@@ -3687,7 +3980,7 @@ const runtimes = {
"codex": {
"id": "codex",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "OpenAI Codex CLI",
"description": "OpenAI Codex CLI — shell-var command style; per-agent sandbox tiers; config.toml + hooks.json hook surface; tier-1 support.",
"tier": "core",
@@ -3758,7 +4051,7 @@ const runtimes = {
"copilot": {
"id": "copilot",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "GitHub Copilot",
"description": "GitHub Copilot (VS Code) — markdown config format; copilot-inline hook surface; no hook events emitted; flat skill nesting (unconfirmed recursive loader); tier-2 support.",
"tier": "core",
@@ -3786,6 +4079,14 @@ const runtimes = {
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeCommandToCopilotSkill"
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeAgentToCopilotAgent"
}
],
"local": [
@@ -3796,6 +4097,14 @@ const runtimes = {
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeCommandToCopilotSkill"
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeAgentToCopilotAgent"
}
]
},
@@ -3829,7 +4138,7 @@ const runtimes = {
"cursor": {
"id": "cursor",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Cursor",
"description": "Cursor IDE — skills + converted commands artifact layout; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.",
"tier": "core",
@@ -3932,7 +4241,7 @@ const runtimes = {
"hermes": {
"id": "hermes",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Hermes Agent",
"description": "Hermes Agent (NousResearch) — skills nest under skills/gsd/ category bucket; nested skill layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",
@@ -4003,7 +4312,7 @@ const runtimes = {
"kilo": {
"id": "kilo",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Kilo Code",
"description": "Kilo Code — XDG-based config dir; global skills at ~/.kilo/skills (separate from XDG config); flat command/ + skills artifact layout; no lifecycle hook registration; tier-2 support.",
"tier": "core",
@@ -4096,7 +4405,7 @@ const runtimes = {
"kimi": {
"id": "kimi",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Kimi CLI",
"description": "Kimi CLI (Moonshot AI) — generic agents root at ~/.config/agents; skills + kimi-agents artifact layout; no hook surface; no hook events; tier-2 support.",
"tier": "core",
@@ -4170,7 +4479,7 @@ const runtimes = {
"opencode": {
"id": "opencode",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "OpenCode",
"description": "OpenCode — XDG-based config dir; flat command/ + skills artifact layout; settings-json config format; no lifecycle hook registration; tier-2 support.",
"tier": "core",
@@ -4259,7 +4568,7 @@ const runtimes = {
"qwen": {
"id": "qwen",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Qwen Code",
"description": "Qwen Code (Alibaba) — nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",
@@ -4334,7 +4643,7 @@ const runtimes = {
"trae": {
"id": "trae",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Trae IDE",
"description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.",
"tier": "core",
@@ -4420,7 +4729,7 @@ const runtimes = {
"windsurf": {
"id": "windsurf",
"role": "runtime",
"version": "1.7.0-rc.2",
"version": "1.7.0-rc.3",
"title": "Windsurf",
"description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.",
"tier": "core",
@@ -4495,6 +4804,108 @@ const runtimes = {
"runtime": "undocumented"
}
}
},
"zcode": {
"id": "zcode",
"role": "runtime",
"version": "1.7.0-rc.3",
"title": "ZCode",
"description": "ZCode (Z.ai) — desktop Agentic Development Environment for GLM-5.2; Claude-shaped nested skills at ~/.zcode/skills/<name>/SKILL.md, slash commands, named subagents, native MCP; declarative plugin surface; profile-marker install; tier-2 community support.",
"tier": "core",
"requires": [],
"engines": {
"gsd": ">=1.6.0"
},
"runtime": {
"configHome": {
"kind": "dot-home",
"name": ".zcode",
"env": [
"ZCODE_CONFIG_DIR"
]
},
"localConfigDir": ".zcode",
"configFormat": "none",
"artifactLayout": {
"global": [
{
"kind": "skills",
"destSubpath": "skills",
"prefix": "gsd-",
"nesting": "nested",
"recursive": false,
"converter": "convertClaudeCommandToClaudeSkill"
},
{
"kind": "commands",
"destSubpath": "commands",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
}
],
"local": [
{
"kind": "skills",
"destSubpath": "skills",
"prefix": "gsd-",
"nesting": "nested",
"recursive": false,
"converter": "convertClaudeCommandToClaudeSkill"
},
{
"kind": "commands",
"destSubpath": "commands",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
},
{
"kind": "agents",
"destSubpath": "agents",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": null
}
]
},
"commandStyle": "slash-hyphen",
"hooksSurface": "none",
"sandboxTier": "none",
"supportTier": 2,
"installSurface": "profile-marker-only",
"writesSharedSettings": false,
"permissionWriter": null,
"extendedHookEvents": [],
"hostIntegration": {
"embeddingMode": "declarative",
"commandSurface": "slash-file",
"dispatch": {
"namedDispatch": true,
"nested": "undocumented",
"maxDepth": "undocumented",
"background": false,
"subagentToolkit": "full",
"backgroundDispatch": false
},
"modelMode": "passive",
"hookBus": "host",
"stateIO": "filesystem",
"transport": "mcp",
"runtime": "electron"
}
}
}
};
@@ -4509,6 +4920,11 @@ const commandFamilies = {
"module": "audit-command-router.cjs",
"router": "routeAuditUat"
},
"claude-orchestration": {
"capId": "claude-orchestration",
"module": "claude-orchestration-command-router.cjs",
"router": "routeClaudeOrchestrationCommand"
},
"extract-messages": {
"capId": "profile-pipeline",
"module": "profile-pipeline-command-router.cjs",
@@ -4648,6 +5064,7 @@ const _requiresGraph = {
"audit": [],
"augment": [],
"claude": [],
"claude-orchestration": [],
"cline": [],
"code-review": [],
"codebuddy": [],
@@ -4676,7 +5093,8 @@ const _requiresGraph = {
"tdd": [],
"trae": [],
"ui": [],
"windsurf": []
"windsurf": [],
"zcode": []
};
function requiresClosure(id) {

View File

@@ -0,0 +1,136 @@
"use strict";
/**
* Claude orchestration command router — CLI dispatcher for
* `gsd-tools claude-orchestration <subcommand>`.
*
* #1143 — thin CLI adapter over the pure `claude-orchestration.cjs` module.
* Lets execute-phase (or any orchestrator) invoke the Workflow-backend
* detection and the Workflow-script emitter through the standard capability
* command surface (ADR-959) instead of a bare `require()`.
*
* Router signature: { args, cwd, raw, error } — identical to the other host
* routers; discovered by dispatchCapabilityCommand via the registry's
* commandFamilies index.
*
* Subcommands:
* detect-backend [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch]
* Resolves whether the Workflow backend should activate. `--runtime`
* defaults to the GSD_RUNTIME env var (or 'unknown'). Reads the
* `claude_orchestration.*` keys from .planning/config.json. Emits
* { available, backend, reason }.
*
* emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>]
* Reads a wave/plan manifest JSON file and emits the generated Workflow
* script + summary. The manifest shape matches emitWorkflowScript's input:
* { waves: [{ id, plans: [{ id, brief, files_modified: string[] }] }] }.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
// eslint-disable-next-line @typescript-eslint/no-require-imports
const io = require("./io.cjs");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const core = require("./claude-orchestration.cjs");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const configLoader = require("./config-loader.cjs");
const { output } = io;
const { detectWorkflowBackend, emitWorkflowScript } = core;
const CAPABLE_HOST = { dispatch: { nested: true, background: true } };
function usage(error) {
error('Usage: gsd-tools claude-orchestration <detect-backend|emit-workflow> [...]\n' +
' detect-backend [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch]\n' +
' emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>]');
}
function argValue(args, flag) {
const i = args.indexOf(flag);
return i !== -1 && i + 1 < args.length ? args[i + 1] : undefined;
}
/**
* Detect whether the Workflow backend should activate for the current/given
* runtime. Reads `claude_orchestration.*` from the project config; runtime and
* SDK version come from flags (the orchestrator already knows these) or env.
*/
function cmdDetectBackend(args, cwd, raw) {
const runtimeId = argValue(args, '--runtime') || process.env['GSD_RUNTIME'] || 'unknown';
const agentSdkVersion = argValue(args, '--agent-sdk-version');
const noNested = args.includes('--no-nested-dispatch');
const hostIntegration = noNested ? { dispatch: { nested: false, background: true } } : CAPABLE_HOST;
// Resolve the claude_orchestration.* slice from the project config (federated
// keys are merged by loadConfig as a nested object). A config read failure
// degrades to inline — it must not break the core loop.
let claudeSlice = {};
try {
const loaded = configLoader.loadConfig(cwd);
const slice = loaded['claude_orchestration'];
if (slice && typeof slice === 'object' && !Array.isArray(slice)) {
claudeSlice = slice;
}
}
catch {
claudeSlice = {};
}
// Flatten the nested slice into the dotted-key shape detectWorkflowBackend expects.
const flatConfig = {};
for (const k of Object.keys(claudeSlice)) {
flatConfig['claude_orchestration.' + k] = claudeSlice[k];
}
const result = detectWorkflowBackend({ runtimeId, hostIntegration, config: flatConfig, agentSdkVersion });
output(result, raw);
}
/**
* Emit a Workflow script from a wave/plan manifest file.
*/
function cmdEmitWorkflow(args, _cwd, raw, error) {
const wavesPath = argValue(args, '--waves');
const runId = argValue(args, '--run-id');
const phaseDir = argValue(args, '--phase-dir') || '.planning/phases/current';
const budgetRaw = argValue(args, '--budget');
if (!wavesPath) {
error('emit-workflow requires --waves <path>');
return;
}
if (!runId) {
error('emit-workflow requires --run-id <id>');
return;
}
let waves;
try {
const content = node_fs_1.default.readFileSync(node_path_1.default.resolve(wavesPath), 'utf8');
const parsed = JSON.parse(content);
waves = parsed['waves'];
}
catch (e) {
error('emit-workflow: could not read/parse --waves file "' + wavesPath + '": ' + (e instanceof Error ? e.message : String(e)));
return;
}
const budgetTokens = budgetRaw !== undefined ? parseInt(budgetRaw, 10) : undefined;
const budget = (typeof budgetTokens === 'number' && !Number.isNaN(budgetTokens)) ? budgetTokens : undefined;
const result = emitWorkflowScript({
phaseDir,
runId,
waves: waves,
budgetTokens: budget,
});
if (!result.ok) {
error('emit-workflow: ' + result.reason);
return;
}
output({ script: result.script, summary: result.summary }, raw);
}
function routeClaudeOrchestrationCommand(opts) {
const { args, cwd, raw, error } = opts;
// args[0] is the family ('claude-orchestration'); the subcommand is args[1].
const subcommand = args[1];
if (subcommand === 'detect-backend') {
cmdDetectBackend(args, cwd, raw);
}
else if (subcommand === 'emit-workflow') {
cmdEmitWorkflow(args, cwd, raw, error);
}
else {
usage(error);
}
}
module.exports = { routeClaudeOrchestrationCommand };

View File

@@ -0,0 +1,404 @@
"use strict";
/**
* Claude Orchestration Capability — Workflow-tool backend detection + emitter
*
* #1143 — adopts Claude Code's Workflow tool (the engine behind `/effort ultracode`)
* as an optional, runtime-gated parallel-execution backend for the GSD loop.
*
* This module is the pure, testable core of the capability. It owns two seams:
*
* detectWorkflowBackend({ runtimeId, hostIntegration, config, agentSdkVersion })
* → { available: boolean, backend: 'workflow'|'inline', reason: string }
* Fail-closed: every miss degrades to `inline` (today's behaviour), so the
* core loop is byte-identical unless every gate opens. This is criteria 3 + 6.
*
* emitWorkflowScript({ phaseDir, waves, runId, budgetTokens? })
* → { ok:true, script, summary } | { ok:false, reason }
* Maps GSD's wave/plan model 1:1 onto Workflow primitives:
* wave → sequential `parallel()` stage barriers,
* plan → `agent(brief, { agentType:'gsd-executor', isolation:'worktree' })`,
* files_modified overlap → forces plans into separate sequential stages
* (the same overlap rule execute-phase already applies inline),
* resumeFromRunId → wired to the phase run id,
* budgetTokens → a shared token pool.
* The emitted script composes the SAME gsd-executor agent and worktree
* isolation the inline path uses, so it produces the same artifacts/commits
* (criterion 2). It is a generated string consumed by the orchestrator; this
* module never invokes the Workflow tool itself.
*
* Design laws:
* - Gall's Law: ship a small working slice that composes existing primitives
* (gsd-executor + worktree isolation) rather than reinventing them.
* - Greenspun's Tenth Rule (cited in #1143): adopt the Workflow tool's
* barrier/pipeline/budget/resume semantics instead of hand-rolling them.
* - Postel's Law: liberal in input (missing fields → inline), conservative in
* output (workflow only when every gate opens).
* - Fail-closed: an unknown version, a missing descriptor, or a disabled
* toggle all resolve to `inline`, never to `workflow`.
*
* Zero external dependencies. Pure functions. Never throws on bad input.
*/
// ─── Constants ────────────────────────────────────────────────────────────────
/**
* The Agent SDK version that introduced the Workflow tool (#1143 prior art).
* Used as the default floor when config does not override it. A runtime reporting
* an agentSdkVersion below this cannot host the Workflow backend.
*/
const WORKFLOW_TOOL_FLOOR_VERSION = '0.3.149';
/** Closed enum for the `claude_orchestration.execution_backend` config key. */
const BACKEND_VALUES = new Set(['auto', 'workflow', 'inline']);
/** Only this runtime can host the Workflow tool (Claude Code / Agent SDK). */
const WORKFLOW_RUNTIME = 'claude';
// ─── Semver helpers ───────────────────────────────────────────────────────────
/** Official-ish strict SemVer 2.0.0 numeric triple (+ optional pre/build). */
const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
/** True for a syntactically valid semver string. */
function isValidSemver(s) {
return typeof s === 'string' && SEMVER_RE.test(s);
}
/**
* Compare two semver strings.
* Returns -1/0/1 in the usual sense. Garbage in either position → -1 (fail-closed:
* an unparseable version is treated as "less than" any real floor, so detection
* never accidentally enables the preview backend on an unknown SDK).
*
* Pre-release/build metadata are ignored for the comparison — only the numeric
* major.minor.patch triple participates, matching how the Workflow-tool floor is
* specified (a plain "0.3.149").
*/
function compareSemver(a, b) {
if (!isValidSemver(a) || !isValidSemver(b))
return -1;
// Split numeric triple from pre-release/build metadata.
const parseTriple = (s) => {
const core = s.split('-')[0].split('+')[0].split('.');
return [parseInt(core[0], 10), parseInt(core[1], 10), parseInt(core[2], 10)];
};
const hasPre = (s) => s.indexOf('-') !== -1;
const preIdentifiers = (s) => (s.split('-')[1] || '').split('+')[0].split('.').filter((x) => x.length > 0);
const am = parseTriple(a);
const bm = parseTriple(b);
for (let i = 0; i < 3; i++) {
if (am[i] < bm[i])
return -1;
if (am[i] > bm[i])
return 1;
}
// Numeric triple is equal. SemVer 2.0.0 §11 precedence:
// - a version WITH a pre-release tag is LOWER than the same triple WITHOUT one
// (keeps the floor fail-closed for pre-release builds of the GA floor);
// - two pre-releases of the same triple are ordered by their dot-separated
// identifiers (numeric < alphanumeric; numeric compared numerically,
// alphanumeric lexically; fewer identifiers < more).
const aPre = hasPre(a);
const bPre = hasPre(b);
if (aPre && !bPre)
return -1;
if (!aPre && bPre)
return 1;
if (aPre && bPre) {
const ai = preIdentifiers(a);
const bi = preIdentifiers(b);
const len = Math.min(ai.length, bi.length);
for (let i = 0; i < len; i++) {
const ax = ai[i];
const bx = bi[i];
const aNum = /^\d+$/.test(ax);
const bNum = /^\d+$/.test(bx);
if (aNum && bNum) {
const an = parseInt(ax, 10);
const bn = parseInt(bx, 10);
if (an < bn)
return -1;
if (an > bn)
return 1;
}
else if (aNum && !bNum) {
return -1; // numeric identifiers always lower than alphanumeric
}
else if (!aNum && bNum) {
return 1;
}
else {
if (ax < bx)
return -1;
if (ax > bx)
return 1;
}
}
if (ai.length < bi.length)
return -1;
if (ai.length > bi.length)
return 1;
}
return 0;
}
/** Inline result shorthand. */
function inline(reason, available = false) {
return { available, backend: 'inline', reason };
}
/**
* Resolve whether the Workflow-tool backend should activate.
*
* Gate ladder (all must pass for `workflow`; first miss wins, fail-closed):
* 1. capability enabled (claude_orchestration.enabled truthy)
* 2. runtime is Claude (the only runtime that exposes the Workflow tool)
* 3. execution_backend !== 'inline'
* 4. host descriptor signals nested+background dispatch (Workflow-tool capable)
* 5. agentSdkVersion is a known, valid semver
* 6. agentSdkVersion >= the configured floor (default WORKFLOW_TOOL_FLOOR_VERSION)
* 7. execution_backend === 'workflow' OR 'auto' (both reach here; 'inline' exited at 3)
*
* Never throws. Destructures defensively.
*/
function detectWorkflowBackend(input) {
if (input === null || input === undefined || typeof input !== 'object') {
return inline('capability_disabled');
}
const cfg = (input.config !== null && input.config !== undefined && typeof input.config === 'object')
? input.config
: {};
// 1. capability must be opted in (default-off — ships disabled).
if (!cfg['claude_orchestration.enabled']) {
return inline('capability_disabled');
}
// 2. only Claude can host the Workflow tool.
if (input.runtimeId !== WORKFLOW_RUNTIME) {
return inline('runtime_not_claude');
}
// 3. explicit inline opt-out short-circuits.
let backendRaw = cfg['claude_orchestration.execution_backend'];
if (typeof backendRaw !== 'string' || !BACKEND_VALUES.has(backendRaw)) {
backendRaw = 'auto';
}
if (backendRaw === 'inline') {
return inline('backend_inline');
}
// 4. the host dispatch descriptor must be the nesting-capable Claude-Code shape
// (a proxy for Workflow-tool presence). This is Claude-specific and already
// gated at step 2; `background:true` alone is true on several non-Claude hosts,
// so the proxy is only meaningful after the runtime check above. Note: this is
// NOT the canonical `shouldFlattenDispatch` rule (which keys on
// `backgroundDispatch`); the Workflow backend works precisely because a single
// tool-call orchestrates internally, sidestepping the backgroundDispatch:false
// limitation. Missing/false/foreign descriptor → fail-closed.
const hi = input.hostIntegration;
if (hi === null || hi === undefined || typeof hi !== 'object' || Array.isArray(hi)) {
return inline('workflow_tool_unavailable');
}
const dispatch = hi.dispatch;
if (typeof dispatch !== 'object' || dispatch === null || Array.isArray(dispatch)) {
return inline('workflow_tool_unavailable');
}
const nested = dispatch['nested'];
const background = dispatch['background'];
if (nested !== true || background !== true) {
return inline('workflow_tool_unavailable');
}
// 5. an unknown agentSdkVersion cannot be trusted to meet the floor.
if (!isValidSemver(input.agentSdkVersion)) {
return inline('agent_sdk_version_unknown');
}
// 6. version floor (config override > default constant).
const floorRaw = cfg['claude_orchestration.min_agent_sdk_version'];
const floor = typeof floorRaw === 'string' && isValidSemver(floorRaw) ? floorRaw : WORKFLOW_TOOL_FLOOR_VERSION;
if (compareSemver(input.agentSdkVersion, floor) < 0) {
return inline('agent_sdk_version_below_floor');
}
// 7. auto/workflow both reach the workflow backend once every gate passes.
return { available: true, backend: 'workflow', reason: 'workflow_backend_active' };
}
/**
* Partition a wave's plans into a near-minimal number of sequential stages (via
* greedy first-fit — not guaranteed optimal for arbitrary overlap graphs, but
* correct: no two plans sharing a file ever cohabit a stage) such that no two
* plans in the same stage share a modified file. Each plan goes into the earliest
* stage where it does not overlap any plan already there.
*
* A plan with an EMPTY files_modified set declares no files; it overlaps nothing
* and coalesces into stage 0 (same behavior as the inline path, which also cannot
* guard against undeclared concurrent writes — declare filesModified accurately).
*
* This is the same overlap rule execute-phase applies inline — the only difference
* is the execution vehicle (Workflow `parallel()` vs one-agent-per-message).
*/
function partitionStages(plans) {
const stages = [];
for (const plan of plans) {
const fileSet = new Set(plan.files_modified);
let placed = false;
for (const stage of stages) {
let overlap = false;
for (const f of fileSet) {
if (stage.files.has(f)) {
overlap = true;
break;
}
}
if (!overlap) {
stage.plans.push(plan);
for (const f of fileSet)
stage.files.add(f);
placed = true;
break;
}
}
if (!placed) {
stages.push({ plans: [plan], files: new Set(fileSet) });
}
}
return stages.map((s) => s.plans.map((p) => p.id));
}
/**
* Quote a free-text value for safe embedding as a JavaScript/Workflow double-quoted
* string literal. Uses JSON.stringify so every JS-relevant escape (backslash, quote,
* newline, tab, NUL, U+2028/U+2029, all control chars) is handled by the language
* itself — there is no hand-rolled escape table to drift. Returns the value already
* wrapped in its surrounding quotes.
*/
function quoteString(s) {
return JSON.stringify(s);
}
/**
* True if `s` is a safe identifier/path token to interpolate into the generated
* script WITHOUT requiring a string-literal context — i.e. it contains no
* character that could terminate a comment line (`\n`/`\r`), break out of a
* string literal (`"` / `\`), or smuggle a NUL/control sequence. Used for
* `phaseDir`, `runId`, `wave.id`, and `plan.id`, which are identifiers/paths and
* must never legitimately contain such characters. Rejecting them at validation
* (rather than silently flattening) keeps the emitted script faithful to input.
*/
const UNSCRIPTABLE_CHAR_RE = /[\r\n"\\\x00-\x1f\x7f\u2028\u2029]/;
function isScriptableIdentifier(s) {
if (typeof s !== 'string' || s.length === 0)
return false;
return !UNSCRIPTABLE_CHAR_RE.test(s);
}
/**
* Emit a Workflow script mapping the phase's wave/plan model onto Workflow
* primitives. Pure and deterministic: identical input yields an identical string.
*
* Returns ok:false (never throws) on invalid input — empty waves, missing runId,
* a wave with no plans, etc.
*/
function emitWorkflowScript(input) {
if (input === null || input === undefined || typeof input !== 'object') {
return { ok: false, reason: 'invalid_input' };
}
const { phaseDir, waves, runId } = input;
// Identifiers/paths interpolated into the generated script must be free of any
// character that could terminate a comment, break out of a string literal, or
// smuggle control bytes — reject up front (security: #1143 review Finding 1).
if (!isScriptableIdentifier(phaseDir)) {
return { ok: false, reason: 'phaseDir must be a non-empty string without newlines/quotes/backslash/control chars' };
}
if (!isScriptableIdentifier(runId)) {
return { ok: false, reason: 'runId must be a non-empty string without newlines/quotes/backslash/control chars' };
}
if (!Array.isArray(waves) || waves.length === 0) {
return { ok: false, reason: 'waves must be a non-empty array' };
}
for (let i = 0; i < waves.length; i++) {
const w = waves[i];
if (w === null || typeof w !== 'object' || typeof w.id !== 'string') {
return { ok: false, reason: 'waves[' + i + '] must be { id, plans: non-empty[] }' };
}
if (!isScriptableIdentifier(w.id)) {
return { ok: false, reason: 'waves[' + i + '].id must not contain newlines/quotes/backslash/control chars' };
}
if (!Array.isArray(w.plans) || w.plans.length === 0) {
return { ok: false, reason: 'waves[' + i + '] must have a non-empty plans array' };
}
const seenIds = new Set();
for (let j = 0; j < w.plans.length; j++) {
const p = w.plans[j];
if (p === null || typeof p !== 'object' || typeof p.id !== 'string' || typeof p.brief !== 'string' || !Array.isArray(p.files_modified)) {
return { ok: false, reason: 'waves[' + i + '].plans[' + j + '] must be { id, brief, files_modified[] }' };
}
if (!isScriptableIdentifier(p.id)) {
return { ok: false, reason: 'waves[' + i + '].plans[' + j + '].id must not contain newlines/quotes/backslash/control chars' };
}
if (seenIds.has(p.id)) {
return { ok: false, reason: 'waves[' + i + '] has duplicate plan id "' + p.id + '"' };
}
seenIds.add(p.id);
for (const f of p.files_modified) {
if (typeof f !== 'string' || f.length === 0) {
return { ok: false, reason: 'waves[' + i + '].plans[' + j + '].files_modified entries must be non-empty strings' };
}
}
}
}
const budgetTokens = (typeof input.budgetTokens === 'number' && Number.isFinite(input.budgetTokens) && input.budgetTokens > 0)
? Math.floor(input.budgetTokens)
: null;
const lines = [];
lines.push('// GSD Workflow script — generated by the claude-orchestration capability (#1143)');
lines.push('// phase: ' + phaseDir);
lines.push('// BETA: preview-grade; on any failure the orchestrator falls back to inline dispatch.');
lines.push('// Composes the SAME gsd-executor agent + worktree isolation as the inline path,');
lines.push('// so artifacts (SUMMARY.md) and commits are produced identically.');
lines.push('resumeFromRunId(' + quoteString(runId) + ')');
if (budgetTokens !== null) {
lines.push('budget(' + budgetTokens + ')');
}
lines.push('');
const stagesByWave = [];
let totalPlans = 0;
for (let wi = 0; wi < waves.length; wi++) {
const wave = waves[wi];
const stages = partitionStages(wave.plans);
stagesByWave.push(stages);
totalPlans += wave.plans.length;
lines.push('// Wave ' + wave.id);
for (let si = 0; si < stages.length; si++) {
const stagePlanIds = stages[si];
// Resolve back to plan objects for briefs (ids are unique within a wave — validated above).
const stagePlans = stagePlanIds.map((id) => wave.plans.find((p) => p.id === id));
if (stages.length > 1) {
lines.push('// Stage ' + si + (si > 0 ? ' (sequential — files_modified overlap)' : ''));
}
if (stagePlans.length === 1) {
const p = stagePlans[0];
lines.push('parallel(');
lines.push(' agent(' + quoteString(p.brief) + ', { agentType: "gsd-executor", isolation: "worktree" })');
lines.push(')');
}
else {
lines.push('parallel(');
for (const p of stagePlans) {
lines.push(' agent(' + quoteString(p.brief) + ', { agentType: "gsd-executor", isolation: "worktree" }),');
}
// Replace trailing comma on the last agent line with nothing.
const lastIdx = lines.length - 1;
lines[lastIdx] = lines[lastIdx].replace(/,$/, '');
lines.push(')');
}
}
if (wi < waves.length - 1)
lines.push('');
}
lines.push('// Each agent writes SUMMARY.md on its worktree branch; commits land there');
lines.push('// and are merged by the orchestrator exactly as in inline wave dispatch.');
const script = lines.join('\n');
return {
ok: true,
script,
summary: {
waves: waves.length,
plans: totalPlans,
stagesByWave,
resumeRunId: runId,
budgetTokens,
},
};
}
module.exports = {
detectWorkflowBackend,
emitWorkflowScript,
compareSemver,
isValidSemver,
WORKFLOW_TOOL_FLOOR_VERSION,
BACKEND_VALUES,
WORKFLOW_RUNTIME,
};

View File

@@ -62,6 +62,11 @@
"sonnet": null,
"haiku": null
},
"zcode": {
"opus": null,
"sonnet": null,
"haiku": null
},
"augment": {
"opus": null,
"sonnet": null,

View File

@@ -317,7 +317,7 @@ Set via `features.*` namespace (e.g., `"features": { "thinking_partner": true }`
| Key | Type | Default | Allowed Values | Description |
|-----|------|---------|----------------|-------------|
| `features.thinking_partner` | boolean | `false` | `true`, `false` | Enable conditional extended thinking at workflow decision points (used by discuss-phase and plan-phase for architectural tradeoff analysis) |
| `features.global_learnings` | boolean | `false` | `true`, `false` | Enable injection of global learnings from `~/.gsd/learnings/` into agent prompts |
| `features.global_learnings` | boolean | `false` | `true`, `false` | Enable injection of global learnings from `~/.gsd/knowledge/` into agent prompts |
### Hook Fields

View File

@@ -275,7 +275,7 @@ Two layered gates. Both must pass before proceeding to cleanup.
Run the deterministic verifier script. Do NOT rely solely on the free-text `verified: yes/no` Hunk Verification Table from Step 4 — bug #2969 traced repeated false-positive `verified: yes` reports to that table being filled in without an actual content-presence check. The script performs the check structurally and exits non-zero on any miss.
Run the verifier as a child process (the gsd-tools binary directory is not required — the script ships under `gsd-core/bin/` in the source repo and is installed to `${GSD_HOME}/gsd-core/bin/`; it is also exposed via the SDK at `sdk/dist/cli.js verify-reapply` when present):
Run the verifier as a child process (the gsd-tools binary directory is not required — the script ships under `gsd-core/bin/` in the source repo and is installed to `${GSD_HOME}/gsd-core/bin/`):
```bash
PRISTINE_DIR="${CONFIG_DIR}/gsd-pristine"

View File

@@ -372,6 +372,28 @@ reason: "{verbatim user response}"
Note: Blocked tests do NOT go into the Gaps section (they aren't code issues — they're prerequisite gates).
**If response indicates a deferred follow-up (NOT a current-phase blocker):**
- "later", "future", "follow-up", "next version", "out of scope", "nice to have", "not now", "defer", "down the road", "separate phase", "phase 2"
These are future-work ideas, not code issues for the current phase. Capture them WITHOUT creating a gap plan (#1921 — a deferred follow-up must never become a blocking gap or spawn a fix plan):
Update Tests section:
```
### {N}. {name}
expected: {expected}
result: skipped
reason: "Deferred follow-up: {verbatim user response}"
```
Append to UAT.md `## Deferred Follow-Ups` (create the section if absent):
```yaml
- test: {N}
idea: "{verbatim user response}"
deferred_at: {today}
```
Do NOT append to `## Gaps` — deferred follow-ups are not blocking gaps. Continue to the next test.
**If response is anything else:**
- Treat as issue description
@@ -393,7 +415,8 @@ severity: {inferred}
Append to Gaps section (structured YAML for plan-phase --gaps):
```yaml
- truth: "{expected behavior from test}"
- gap_id: G-{phase}-{N} # Stable id (phase + test number) — gap-closure plans tag it in their frontmatter so verify-work can reconcile resolved gaps on resume (#1921).
truth: "{expected behavior from test}"
status: failed
reason: "User reported: {verbatim user response}"
severity: {inferred}
@@ -411,9 +434,35 @@ If more tests remain → Update Current Test, go to `present_test`
If no more tests → Go to `complete_session`
</step>
<step name="reconcile_gaps">
**Reconcile diagnosed gaps against completed gap-closure plans (#1921):**
When verify-work resumes after `/gsd:execute-phase --gaps-only`, the UAT `## Gaps` entries still read `status: failed` even though their fix plans have executed. Without reconciliation verify-work re-diagnoses them as fresh blockers and spawns new gap plans — losing the verification state. This step closes the loop.
Read the UAT `## Gaps` section and the phase dir `*-PLAN.md` frontmatter. For each gap with `status: failed`:
1. Find a `*-PLAN.md` whose frontmatter `gap_ids` includes the gap's `gap_id` (`G-{phase}-{N}`).
2. If such a plan exists AND has a matching `*-SUMMARY.md` in the phase dir (the plan was executed by `--gaps-only`), the gap is **resolved** — update its YAML in place:
```yaml
- gap_id: G-{phase}-{N}
status: resolved # was: failed
resolved_by: {plan basename}
resolved_at: {today}
```
3. If no plan references the `gap_id`, or the plan has no SUMMARY, leave the gap `status: failed` (still open).
Read plan frontmatter directly in-context — do not pipe it through a shell parser. After reconciliation, announce:
```
Reconciled gap-closure state: {resolved_count} gap(s) resolved by executed plans, {open_count} still open.
```
Resolved gaps are NOT re-diagnosed and do NOT spawn new gap plans. If the user later reports the same behavior as still broken, treat it as a new issue (a regression) with a fresh `gap_id`.
</step>
<step name="resume_from_file">
**Resume testing from UAT file:**
**First run `reconcile_gaps`** (above) so gaps already fixed by `/gsd:execute-phase --gaps-only` are marked `resolved` before testing resumes (#1921).
Read the full UAT file.
Find first test with `result: [pending]`.
@@ -652,7 +701,7 @@ Display:
Spawn gsd-planner in --gaps mode:
```
````
Agent(
prompt="""
<planning_context>
@@ -673,13 +722,22 @@ ${AGENT_SKILLS_PLANNER}
<downstream_consumer>
Output consumed by /gsd:execute-phase
Plans must be executable prompts.
**Gap linkage (#1921):** each created `*-PLAN.md` MUST list the UAT gap ids it addresses in its frontmatter:
```yaml
---
gap_closure: true
gap_ids: [G-{phase}-{N}, ...] # the ## Gaps gap_id values this plan fixes
---
```
This lets `/gsd:verify-work` reconcile resolved gaps on resume (a gap whose plan has a matching `*-SUMMARY.md` is marked `status: resolved`, not re-diagnosed as a fresh blocker).
</downstream_consumer>
""",
subagent_type="gsd-planner",
model="{planner_model}",
description="Plan gap fixes for Phase {phase}"
)
```
````
> **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.