@opengsd/gsd-core 1.7.0-rc.3 → 1.7.0-rc.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +20 -0
- package/README.ja-JP.md +4 -3
- package/README.ko-KR.md +4 -3
- package/README.md +4 -3
- package/README.pt-BR.md +4 -3
- package/README.zh-CN.md +4 -3
- package/agents/gsd-doc-classifier.md +105 -0
- package/agents/gsd-doc-synthesizer.md +61 -0
- package/agents/gsd-ui-checker.md +28 -0
- package/bin/install.js +652 -466
- package/commands/gsd/map-codebase.md +4 -4
- package/commands/gsd/ns-project.md +2 -1
- package/commands/gsd/onboard.md +46 -0
- package/gsd-core/bin/gsd-tools.cjs +87 -3
- package/gsd-core/bin/lib/api-coverage.cjs +466 -0
- package/gsd-core/bin/lib/audit.cjs +6 -3
- package/gsd-core/bin/lib/capability-loader.cjs +11 -9
- package/gsd-core/bin/lib/capability-registry.cjs +741 -62
- package/gsd-core/bin/lib/capability-state.cjs +117 -18
- package/gsd-core/bin/lib/capability-validator.cjs +1 -1
- package/gsd-core/bin/lib/capability-writer.cjs +21 -2
- package/gsd-core/bin/lib/check-command-router.cjs +242 -3
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +136 -0
- package/gsd-core/bin/lib/claude-orchestration.cjs +404 -0
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/command-aliases.cjs +8 -0
- package/gsd-core/bin/lib/commands.cjs +7 -5
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +96 -0
- package/gsd-core/bin/lib/core-utils.cjs +4 -1
- package/gsd-core/bin/lib/host-integration-adapters/cline-sdk-binding.cjs +234 -0
- package/gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs +145 -0
- package/gsd-core/bin/lib/host-integration.cjs +16 -0
- package/gsd-core/bin/lib/init-command-router.cjs +4 -0
- package/gsd-core/bin/lib/init.cjs +99 -99
- package/gsd-core/bin/lib/install-effort-resolver.cjs +213 -0
- package/gsd-core/bin/lib/install-engine.cjs +159 -16
- package/gsd-core/bin/lib/install-profiles.cjs +2 -0
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +75 -18
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +50 -11
- package/gsd-core/bin/lib/milestone.cjs +3 -3
- package/gsd-core/bin/lib/model-resolver.cjs +69 -4
- package/gsd-core/bin/lib/normalize-test-command.cjs +187 -0
- package/gsd-core/bin/lib/onboard-projection.cjs +309 -0
- package/gsd-core/bin/lib/phase-id.cjs +132 -3
- package/gsd-core/bin/lib/phase.cjs +78 -16
- package/gsd-core/bin/lib/planning-workspace.cjs +17 -0
- package/gsd-core/bin/lib/roadmap-command-router.cjs +5 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +21 -30
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +9 -9
- package/gsd-core/bin/lib/roadmap.cjs +42 -56
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +13 -6
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +17 -10
- package/gsd-core/bin/lib/runtime-homes.cjs +8 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +33 -25
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/spec-section.cjs +111 -0
- package/gsd-core/bin/lib/state-transition.cjs +1 -1
- package/gsd-core/bin/lib/state.cjs +24 -24
- package/gsd-core/bin/lib/surface.cjs +81 -30
- package/gsd-core/bin/lib/uat.cjs +4 -1
- package/gsd-core/bin/lib/validate.cjs +15 -6
- package/gsd-core/bin/lib/verify.cjs +33 -37
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/bin/shared/model-catalog.json +11 -6
- package/gsd-core/references/api-coverage.md +104 -0
- package/gsd-core/references/gsd-run-resolver.md +8 -0
- package/gsd-core/references/model-profiles.md +2 -2
- package/gsd-core/references/planning-config.md +2 -0
- package/gsd-core/references/specless-probe-fallback.md +172 -0
- package/gsd-core/templates/config.json +2 -1
- package/gsd-core/templates/project.md +1 -1
- package/gsd-core/workflows/audit-fix.md +9 -1
- package/gsd-core/workflows/code-review-fix.md +7 -3
- package/gsd-core/workflows/code-review.md +4 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -1
- package/gsd-core/workflows/do.md +4 -3
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +8 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +42 -0
- package/gsd-core/workflows/execute-phase.md +1 -25
- package/gsd-core/workflows/help/modes/brief.md +2 -1
- package/gsd-core/workflows/help/modes/default.md +2 -1
- package/gsd-core/workflows/help/modes/full.md +11 -1
- package/gsd-core/workflows/help/modes/topic.md +1 -1
- package/gsd-core/workflows/onboard.md +277 -0
- package/gsd-core/workflows/plan-phase.md +31 -2
- package/gsd-core/workflows/quick.md +22 -2
- package/gsd-core/workflows/review.md +59 -13
- package/gsd-core/workflows/settings-advanced.md +5 -5
- package/gsd-core/workflows/settings.md +2 -2
- package/gsd-core/workflows/verify-phase.md +3 -2
- package/gsd-core/workflows/verify-work.md +38 -0
- package/hooks/dist/gsd-cursor-pre-tool.js +76 -0
- package/hooks/dist/gsd-cursor-stop.js +48 -0
- package/hooks/dist/gsd-cursor-subagent-start.js +50 -0
- package/hooks/dist/gsd-cursor-subagent-stop.js +40 -0
- package/hooks/dist/managed-hooks-registry.cjs +4 -0
- package/hooks/gsd-cursor-pre-tool.js +76 -0
- package/hooks/gsd-cursor-stop.js +48 -0
- package/hooks/gsd-cursor-subagent-start.js +50 -0
- package/hooks/gsd-cursor-subagent-stop.js +40 -0
- package/hooks/managed-hooks-registry.cjs +4 -0
- package/package.json +2 -1
- package/scripts/build-hooks.js +5 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +77 -0
- package/scripts/lint-phase-id-drift.cjs +150 -0
- package/scripts/run-tests.cjs +26 -4
- package/scripts/sync-runtime-launcher.cjs +20 -2
- package/skills/gsd-map-codebase/SKILL.md +3 -3
- package/skills/gsd-ns-project/SKILL.md +1 -0
- package/skills/gsd-onboard/SKILL.md +46 -0
|
@@ -45,7 +45,7 @@ const configLoaderMod = require("./config-loader.cjs");
|
|
|
45
45
|
const { loadConfig } = configLoaderMod;
|
|
46
46
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
47
47
|
const installProfilesMod = require("./install-profiles.cjs");
|
|
48
|
-
const { readActiveProfile, loadSkillsManifest, resolveProfile, parseRequires } = installProfilesMod;
|
|
48
|
+
const { readActiveProfile, loadSkillsManifest, resolveProfile, parseRequires, parseCallsAgents } = installProfilesMod;
|
|
49
49
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
50
|
const surfaceMod = require("./surface.cjs");
|
|
51
51
|
const { resolveSurface } = surfaceMod;
|
|
@@ -282,22 +282,90 @@ function _loadInstalledSkillsManifest(configDir) {
|
|
|
282
282
|
}
|
|
283
283
|
return manifest;
|
|
284
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* #1858 — Build a skill dependency manifest from a FLAT commands/gsd-<stem>.md
|
|
287
|
+
* source layout (the Claude local project install shape, where the `gsd-`
|
|
288
|
+
* prefix is baked into each filename at the commands/ level and there is no
|
|
289
|
+
* commands/gsd/ subdir). Strips the `gsd-` prefix so stems match the nested
|
|
290
|
+
* loader's output (gsd-validate-phase.md → validate-phase, same as nested
|
|
291
|
+
* validate-phase.md).
|
|
292
|
+
*
|
|
293
|
+
* Map shape is identical to loadSkillsManifest: each stem maps to its
|
|
294
|
+
* `requires` deps (parsed via the same shared parseRequires) and carries a
|
|
295
|
+
* companion `_calls_agents_<stem>` key (parsed via parseCallsAgents) so the
|
|
296
|
+
* flat and nested paths cannot drift.
|
|
297
|
+
*
|
|
298
|
+
* Returns an empty Map when the parent directory does not exist or contains
|
|
299
|
+
* no gsd-*.md files (so _resolveManifest can use size>0 as the "flat layout
|
|
300
|
+
* present" signal and fall through to the installed-skills branch otherwise).
|
|
301
|
+
*/
|
|
302
|
+
function _loadFlatCommandsGsdManifest(commandsParentDir) {
|
|
303
|
+
const manifest = new Map();
|
|
304
|
+
let entries;
|
|
305
|
+
try {
|
|
306
|
+
entries = node_fs_1.default.readdirSync(commandsParentDir, { withFileTypes: true });
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return manifest;
|
|
310
|
+
}
|
|
311
|
+
for (const entry of entries) {
|
|
312
|
+
if (!entry.isFile())
|
|
313
|
+
continue;
|
|
314
|
+
if (!entry.name.startsWith('gsd-'))
|
|
315
|
+
continue;
|
|
316
|
+
if (!entry.name.endsWith('.md'))
|
|
317
|
+
continue;
|
|
318
|
+
// Strip 'gsd-' prefix (4 chars) and '.md' suffix (3 chars) → stem.
|
|
319
|
+
const stem = entry.name.slice(4, -3);
|
|
320
|
+
if (!stem)
|
|
321
|
+
continue;
|
|
322
|
+
// Mirror loadSkillsManifest's try/catch structure exactly: wrap read +
|
|
323
|
+
// parse + set together so an unreadable file OR a thrown parser degrades
|
|
324
|
+
// both keys to [] (parity; closes the latent catch-scope drift a reviewer
|
|
325
|
+
// flagged — both parsers are non-throwing today, but the structural
|
|
326
|
+
// match future-proofs the "identical Map shape" contract).
|
|
327
|
+
try {
|
|
328
|
+
const content = node_fs_1.default.readFileSync(node_path_1.default.join(commandsParentDir, entry.name), 'utf8');
|
|
329
|
+
manifest.set(stem, parseRequires(content));
|
|
330
|
+
manifest.set(`_calls_agents_${stem}`, parseCallsAgents(content));
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
manifest.set(stem, []);
|
|
334
|
+
manifest.set(`_calls_agents_${stem}`, []);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return manifest;
|
|
338
|
+
}
|
|
285
339
|
/**
|
|
286
340
|
* Resolve the skill dependency manifest for capability-state resolution.
|
|
287
341
|
*
|
|
288
|
-
* Resolution order
|
|
289
|
-
* 1. If commandsGsdDir exists, load from source
|
|
290
|
-
*
|
|
342
|
+
* Resolution order:
|
|
343
|
+
* 1. If commandsGsdDir exists, load from the nested source layout
|
|
344
|
+
* (repo-checkout behavior: <repo>/commands/gsd/*.md).
|
|
345
|
+
* 2. #1858 — otherwise, if the flat source layout is present (gsd-<stem>.md
|
|
346
|
+
* files in dirname(commandsGsdDir)), load from there. This is the Claude
|
|
347
|
+
* local project install shape where commands/gsd/ does not exist but
|
|
348
|
+
* commands/gsd-<stem>.md files do.
|
|
349
|
+
* 3. #1160 — otherwise, fall back to installed skills at
|
|
350
|
+
* configDir/skills/gsd-[stem]/SKILL.md.
|
|
291
351
|
*
|
|
292
|
-
* In an installed runtime
|
|
293
|
-
*
|
|
294
|
-
* materialise the full-sentinel to an empty Set, making every
|
|
295
|
-
* unsurfaced even when the skill was physically installed
|
|
352
|
+
* In an installed runtime both source trees are absent; only the skills/
|
|
353
|
+
* layout exists. Returning an empty manifest caused resolveSurface to
|
|
354
|
+
* materialise the full-sentinel to an empty Set, making every skill-bearing
|
|
355
|
+
* capability appear unsurfaced even when the skill was physically installed
|
|
356
|
+
* (#1160) or authored as a flat command file (#1858).
|
|
296
357
|
*/
|
|
297
358
|
function _resolveManifest(commandsGsdDir, configDir) {
|
|
298
359
|
if (node_fs_1.default.existsSync(commandsGsdDir)) {
|
|
299
360
|
return loadSkillsManifest(commandsGsdDir);
|
|
300
361
|
}
|
|
362
|
+
// #1858: flat source layout — gsd-<stem>.md files at dirname(commandsGsdDir).
|
|
363
|
+
// Only claim the flat branch when it actually has gsd-*.md files; otherwise
|
|
364
|
+
// fall through to the installed-skills branch (a commands/ dir with no gsd
|
|
365
|
+
// files must not shadow an installed skills/ tree).
|
|
366
|
+
const flat = _loadFlatCommandsGsdManifest(node_path_1.default.dirname(commandsGsdDir));
|
|
367
|
+
if (flat.size > 0)
|
|
368
|
+
return flat;
|
|
301
369
|
return _loadInstalledSkillsManifest(configDir);
|
|
302
370
|
}
|
|
303
371
|
/**
|
|
@@ -334,7 +402,7 @@ function _resolveManifest(commandsGsdDir, configDir) {
|
|
|
334
402
|
* @param raw Whether to emit raw JSON (io.output raw mode)
|
|
335
403
|
* @param _options Reserved for future use
|
|
336
404
|
*/
|
|
337
|
-
function resolveCapabilityRuntimeState(cwd, runtimeConfigDir, configOverride) {
|
|
405
|
+
function resolveCapabilityRuntimeState(cwd, runtimeConfigDir, configOverride, runtimeOverride) {
|
|
338
406
|
const warnings = [];
|
|
339
407
|
// Resolve runtimeConfigDir using the canonical runtime-homes resolver.
|
|
340
408
|
// When not provided, the active runtime is detected via the canonical
|
|
@@ -350,13 +418,40 @@ function resolveCapabilityRuntimeState(cwd, runtimeConfigDir, configOverride) {
|
|
|
350
418
|
try {
|
|
351
419
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
352
420
|
const runtimeHomes = require('./runtime-homes.cjs');
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
421
|
+
// #2003: an explicit --runtime override bypasses the persisted-runtime
|
|
422
|
+
// fallback (GSD_RUNTIME → config.runtime → 'claude') so, e.g., a repo with
|
|
423
|
+
// persisted runtime:"codex" resolves the Claude config dir when the operator
|
|
424
|
+
// is driving from Claude Code. Canonicalize via runtime-name-policy (handles
|
|
425
|
+
// aliases like codex-app → codex); if canonicalization yields nothing, fall
|
|
426
|
+
// through to the persisted-runtime resolution below. Mirrors the update-
|
|
427
|
+
// context / effort sync precedent (read/diagnostic paths accepting both
|
|
428
|
+
// --config-dir and --runtime).
|
|
429
|
+
if (typeof runtimeOverride === 'string' && runtimeOverride.trim() !== '') {
|
|
430
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
431
|
+
const runtimeNamePolicy = require('./runtime-name-policy.cjs');
|
|
432
|
+
const canonical = runtimeNamePolicy.canonicalizeRuntimeName(runtimeOverride);
|
|
433
|
+
if (canonical) {
|
|
434
|
+
resolvedConfigDir = runtimeHomes.getGlobalConfigDir(canonical);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
// #2003: unknown runtime override — warn (don't silently ignore the
|
|
438
|
+
// explicit input) and fall through to persisted-runtime resolution.
|
|
439
|
+
// Avoids a silent-wrong-result on this diagnostic command for typos
|
|
440
|
+
// (e.g. "cluade") or runtimes known to runtime-homes but not yet to
|
|
441
|
+
// the alias manifest (e.g. "grok"). The warning surfaces via the
|
|
442
|
+
// `warnings[]` channel consumed by cmdCapabilityState/cmdLoopRenderHooks.
|
|
443
|
+
warnings.push(`--runtime "${runtimeOverride}" is not a known runtime; falling back to auto-detected/persisted runtime resolution`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (!resolvedConfigDir) {
|
|
447
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
448
|
+
const runtimeSlash = require('./runtime-slash.cjs');
|
|
449
|
+
// Detect the active runtime via GSD_RUNTIME → config.runtime → 'claude'.
|
|
450
|
+
// resolveRuntime reads config.json directly (no side effects) and returns
|
|
451
|
+
// a lowercased canonical runtime name.
|
|
452
|
+
const detectedRuntime = runtimeSlash.resolveRuntime(cwd);
|
|
453
|
+
resolvedConfigDir = runtimeHomes.getGlobalConfigDir(detectedRuntime);
|
|
454
|
+
}
|
|
360
455
|
}
|
|
361
456
|
catch {
|
|
362
457
|
// Defensive fallback: use ~/.claude if the canonical resolver throws.
|
|
@@ -450,8 +545,11 @@ function resolveCapabilityRuntimeState(cwd, runtimeConfigDir, configOverride) {
|
|
|
450
545
|
capabilities: result.capabilities,
|
|
451
546
|
};
|
|
452
547
|
}
|
|
453
|
-
function cmdCapabilityState(cwd, runtimeConfigDir, raw,
|
|
454
|
-
|
|
548
|
+
function cmdCapabilityState(cwd, runtimeConfigDir, raw, options = {}) {
|
|
549
|
+
// #2003: thread an explicit --runtime override so the config-dir resolution
|
|
550
|
+
// bypasses the persisted-runtime fallback (GSD_RUNTIME → config.runtime).
|
|
551
|
+
const runtimeOverride = typeof options['runtime'] === 'string' ? options['runtime'] : undefined;
|
|
552
|
+
const result = resolveCapabilityRuntimeState(cwd, runtimeConfigDir, undefined, runtimeOverride);
|
|
455
553
|
for (const warning of result.warnings) {
|
|
456
554
|
coreError(`capability state: ${warning}`);
|
|
457
555
|
}
|
|
@@ -491,6 +589,7 @@ module.exports = {
|
|
|
491
589
|
// Exported for tests
|
|
492
590
|
_resolveCommandsGsdDir,
|
|
493
591
|
_loadInstalledSkillsManifest,
|
|
592
|
+
_loadFlatCommandsGsdManifest,
|
|
494
593
|
_resolveManifest,
|
|
495
594
|
_isSafePropKey,
|
|
496
595
|
};
|
|
@@ -709,7 +709,7 @@ const VALID_HOOK_EVENTS = new Set(['claude', 'gemini']);
|
|
|
709
709
|
// DISTINCT from hookEvents (managed-hook dialect): extensionEvents describes the
|
|
710
710
|
// plugin-owned event subset imperative hosts expose (opencode / pi); 'none' = the
|
|
711
711
|
// host exposes no extension surface (engine owns the bus, e.g. VS Code).
|
|
712
|
-
const VALID_EXTENSION_EVENTS = new Set(['opencode', 'pi', 'none']);
|
|
712
|
+
const VALID_EXTENSION_EVENTS = new Set(['opencode', 'pi', 'hermes', 'none']);
|
|
713
713
|
const VALID_SANDBOX_TIERS = new Set(['none', 'codex-agent-sandbox']);
|
|
714
714
|
const VALID_ARTIFACT_KIND_NAMES = new Set(['commands', 'agents', 'skills', 'kimi-agents']);
|
|
715
715
|
const VALID_ARTIFACT_NESTINGS = new Set(['flat', 'nested']);
|
|
@@ -77,8 +77,17 @@ function setCapabilityState(cwd, runtimeConfigDir, desired, opts) {
|
|
|
77
77
|
const before = resolveCapabilityRuntimeState(cwd, runtimeConfigDir);
|
|
78
78
|
const resolvedConfigDir = before.runtimeConfigDir;
|
|
79
79
|
// ── Load registry ─────────────────────────────────────────────────────────
|
|
80
|
+
// Issue #2045 (DEFECT 2): validate against the COMPOSED overlay-aware registry
|
|
81
|
+
// (first-party ∪ accepted overlays), mirroring capability-state.cts:547-551.
|
|
82
|
+
// The frozen capability-registry.cjs only knows first-party ids, so a third-
|
|
83
|
+
// party cap failed the membership check below → "unknown capability" even
|
|
84
|
+
// though resolveCapabilityRuntimeState (the `before` snapshot, line 150) already
|
|
85
|
+
// knew about it. loadRegistry is non-throwing and first-party-wins, so a
|
|
86
|
+
// malformed overlay is skipped (never crashes the writer); a truly-unknown id
|
|
87
|
+
// is STILL rejected because it is absent from the composed capabilities map.
|
|
80
88
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
81
|
-
const
|
|
89
|
+
const { loadRegistry } = require('./capability-loader.cjs');
|
|
90
|
+
const registry = loadRegistry({ includeInstalled: true, cwd, gsdHome: process.env['GSD_HOME'] });
|
|
82
91
|
const capabilitiesMap = (registry['capabilities'] && typeof registry['capabilities'] === 'object' && !Array.isArray(registry['capabilities'])
|
|
83
92
|
? registry['capabilities']
|
|
84
93
|
: {});
|
|
@@ -250,8 +259,18 @@ function setCapabilityState(cwd, runtimeConfigDir, desired, opts) {
|
|
|
250
259
|
const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, resolvedConfigDir, scope);
|
|
251
260
|
const commandsGsdDir = _resolveCommandsGsdDir();
|
|
252
261
|
const manifest = _resolveManifest(commandsGsdDir, resolvedConfigDir);
|
|
262
|
+
// #1575: applySurface now accepts opts.resolveAttribution so surface-path
|
|
263
|
+
// agents get the same Co-Authored-By trailer as the install path. The
|
|
264
|
+
// resolver is not threaded here yet — the CLI command handler does not have
|
|
265
|
+
// access to getCommitAttribution (which lives in bin/install.js). Until that
|
|
266
|
+
// is refactored into a shared module, surface-path agents for descriptor-
|
|
267
|
+
// driven runtimes will lack the Co-Authored-By trailer that install adds.
|
|
268
|
+
// Parity is proven when resolveAttribution IS provided (see
|
|
269
|
+
// tests/issue-1575-agent-descriptor-parity.test.cjs).
|
|
253
270
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
254
|
-
applySurface(resolvedConfigDir, layout, manifest, undefined, registry
|
|
271
|
+
applySurface(resolvedConfigDir, layout, manifest, undefined, registry, opts?.materialize?.resolveAttribution
|
|
272
|
+
? { resolveAttribution: opts.materialize.resolveAttribution }
|
|
273
|
+
: undefined);
|
|
255
274
|
}
|
|
256
275
|
catch (err) {
|
|
257
276
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -37,6 +37,9 @@ const prohibition_enforcement_cjs_1 = require("./prohibition-enforcement.cjs");
|
|
|
37
37
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
38
38
|
const gatePredicateEval = require("./gate-predicate-evaluator.cjs");
|
|
39
39
|
const { evaluatePredicate } = gatePredicateEval;
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
|
+
const apiCoverageMod = require("./api-coverage.cjs");
|
|
42
|
+
const { detectApiIntegration, validateCoverageMatrix } = apiCoverageMod;
|
|
40
43
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
41
44
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
42
45
|
function normalizePhrase(text) {
|
|
@@ -131,10 +134,13 @@ function loadPlanContents(phaseDir) {
|
|
|
131
134
|
}
|
|
132
135
|
}
|
|
133
136
|
const DESIGNATED_HEADINGS_RE = /^#{1,6}\s+(?:must[_ ]haves?|truths?|tasks?|objective)\b/i;
|
|
134
|
-
const XML_DECISION_TAGS_RE = /<(?:objective|tasks?|action)(?:\s[^>]
|
|
137
|
+
const XML_DECISION_TAGS_RE = /<(?:objective|tasks?|action)(?:\s[^>]{0,1000})?>((?:(?!<(?:objective|tasks?|action)[\s>])[\s\S])*?)<\/(?:objective|tasks?|action)>/gi;
|
|
135
138
|
function stripCommentsAndFences(text) {
|
|
136
139
|
// HTML-comment stripping stays caller-side (the seam does not strip HTML comments).
|
|
137
|
-
|
|
140
|
+
// Stop-at-next-open body (ReDoS-safe, #2128); an UNCLOSED `<!--` does not match,
|
|
141
|
+
// so downstream tags are preserved (unlike a `(?:-->|$)` fallback, which would
|
|
142
|
+
// wipe to EOF and fail-close the decision-coverage gate).
|
|
143
|
+
const htmlStripped = text.replace(/<!--(?:(?!<!--)[\s\S])*?-->/g, ' ');
|
|
138
144
|
// Fenced-code stripping: delegate to the canonical CommonMark-correct seam.
|
|
139
145
|
// replaces the prior independent regex copy (```` ``` ``` ```` + `~~~ ~~~`).
|
|
140
146
|
return (0, markdown_sectionizer_cjs_1.stripFencedCode)(htmlStripped).text;
|
|
@@ -876,6 +882,233 @@ function cmdCheckPredicate(projectDir, args, raw) {
|
|
|
876
882
|
}
|
|
877
883
|
output(result, raw, undefined);
|
|
878
884
|
}
|
|
885
|
+
// ─── api-coverage-verify-pre ──────────────────────────────────────────────────
|
|
886
|
+
/**
|
|
887
|
+
* api-coverage.verify-pre: BLOCKING seal-time gate for the ai-integration
|
|
888
|
+
* capability (#1562). Enforces "Full API Coverage by Default — Opt Out, Never
|
|
889
|
+
* Opt In." A phase that integrates an external API/SDK/service may not seal
|
|
890
|
+
* until a COVERAGE.md matrix enumerates the surface and every non-integrated
|
|
891
|
+
* capability is an explicit, reasoned opt-out.
|
|
892
|
+
*
|
|
893
|
+
* Contract (two touch points composed into one check):
|
|
894
|
+
* 1. If COVERAGE.md exists in the phase dir → validate it (acceptance #2).
|
|
895
|
+
* Block on any validation error (empty matrix, OPT-OUT without reason,
|
|
896
|
+
* duplicate/empty capability).
|
|
897
|
+
* 2. If COVERAGE.md is absent → run detectApiIntegration over the phase scope
|
|
898
|
+
* (PLAN.md body, then ROADMAP phase section as fallback). If a strong
|
|
899
|
+
* external-API-integration signal is detected → BLOCK ("integration
|
|
900
|
+
* detected without coverage matrix"). If no signal → PASS (treat as a
|
|
901
|
+
* non-API phase; acceptance #4 — low false positives).
|
|
902
|
+
*
|
|
903
|
+
* The detector is the FALLBACK for the "nobody decided / forgot the matrix"
|
|
904
|
+
* case; the primary path is the plan:pre contribution prompting COVERAGE.md.
|
|
905
|
+
*
|
|
906
|
+
* Args: check api-coverage.verify-pre <phase-dir>
|
|
907
|
+
* Emits the uniform gate contract: { block, passed, message, ...details }.
|
|
908
|
+
*/
|
|
909
|
+
function cmdApiCoverageVerifyPre(projectDir, args, raw) {
|
|
910
|
+
const phaseArg = typeof args[2] === 'string' ? args[2] : '';
|
|
911
|
+
if (!phaseArg) {
|
|
912
|
+
error('api-coverage.verify-pre requires a phase argument: check api-coverage.verify-pre <phase-dir-or-token>', ERROR_REASON.SDK_MISSING_ARG);
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const pDir = planningDir(projectDir);
|
|
916
|
+
const phasesRoot = node_path_1.default.join(pDir, 'phases');
|
|
917
|
+
// SECURITY (path traversal): the phase argument is taken ONLY as a phase
|
|
918
|
+
// token — its basename — and resolved by findPhaseInternal strictly under
|
|
919
|
+
// .planning/phases/ (or a milestone archive). The raw arg is never used as a
|
|
920
|
+
// path, so `..`, absolute paths, and arbitrary directories cannot reach a
|
|
921
|
+
// file read. Mirrors cmdVerifySchemaDrift's token-match approach.
|
|
922
|
+
let token = phaseArg.replace(/\\/g, '/').split('/').filter(Boolean).pop() || '';
|
|
923
|
+
// A token like ".." or "." carries no phase identity → unresolvable.
|
|
924
|
+
if (token === '.' || token === '..')
|
|
925
|
+
token = '';
|
|
926
|
+
// Not a GSD project (no phases tree at all) → fail-open: nothing to gate.
|
|
927
|
+
if (!node_fs_1.default.existsSync(phasesRoot)) {
|
|
928
|
+
output({
|
|
929
|
+
block: false,
|
|
930
|
+
passed: true,
|
|
931
|
+
coverage_present: false,
|
|
932
|
+
detected: false,
|
|
933
|
+
message: 'api-coverage: no .planning/phases directory; gate skipped (not a GSD project layout)',
|
|
934
|
+
}, raw, undefined);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
// Resolve the phase dir under the contained phases root.
|
|
938
|
+
let resolvedDir = null;
|
|
939
|
+
let phaseNumber = '';
|
|
940
|
+
if (token) {
|
|
941
|
+
const found = findPhaseInternal(projectDir, token);
|
|
942
|
+
if (found && found.directory) {
|
|
943
|
+
resolvedDir = found.directory;
|
|
944
|
+
phaseNumber = found.phase_number || '';
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (!resolvedDir) {
|
|
948
|
+
// The phases tree EXISTS but THIS phase could not be resolved. For a
|
|
949
|
+
// BLOCKING gate, fail-closed: a missing phase dir must not silently bypass
|
|
950
|
+
// the coverage requirement. (Distinguished from "no .planning at all"
|
|
951
|
+
// above, which is a genuine non-GSD-project → pass.)
|
|
952
|
+
output({
|
|
953
|
+
block: true,
|
|
954
|
+
passed: false,
|
|
955
|
+
coverage_present: false,
|
|
956
|
+
detected: false,
|
|
957
|
+
phase_lookup_failed: true,
|
|
958
|
+
message: `api-coverage: could not resolve phase "${phaseArg}" under .planning/phases/. ` +
|
|
959
|
+
'Resolve the phase directory (or produce COVERAGE.md) before sealing.',
|
|
960
|
+
}, raw, undefined);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
// Defense-in-depth: the resolved dir must be inside the phases root (or a
|
|
964
|
+
// milestone archive under .planning/milestones).
|
|
965
|
+
const milestonesRoot = node_path_1.default.join(pDir, 'milestones');
|
|
966
|
+
if (!isInsideRoot(resolvedDir, phasesRoot) && !isInsideRoot(resolvedDir, milestonesRoot)) {
|
|
967
|
+
output({
|
|
968
|
+
block: true,
|
|
969
|
+
passed: false,
|
|
970
|
+
coverage_present: false,
|
|
971
|
+
detected: false,
|
|
972
|
+
message: 'api-coverage: resolved phase dir escapes .planning/ — refusing to evaluate',
|
|
973
|
+
}, raw, undefined);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
// (1) locate COVERAGE.md — prefer the exact name, then a single *-COVERAGE.md.
|
|
977
|
+
let coverageFile = '';
|
|
978
|
+
let suffixed = [];
|
|
979
|
+
try {
|
|
980
|
+
const entries = node_fs_1.default.readdirSync(resolvedDir, { withFileTypes: true });
|
|
981
|
+
const files = entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
982
|
+
const exact = files.find((f) => /^COVERAGE\.md$/i.test(f));
|
|
983
|
+
if (exact) {
|
|
984
|
+
coverageFile = exact;
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
suffixed = files.filter((f) => /-COVERAGE\.md$/i.test(f)).sort();
|
|
988
|
+
if (suffixed.length === 1)
|
|
989
|
+
coverageFile = suffixed[0];
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
catch {
|
|
993
|
+
// readdir failure → treat as no matrix readable; fall through to detection.
|
|
994
|
+
}
|
|
995
|
+
if (coverageFile) {
|
|
996
|
+
let matrixText;
|
|
997
|
+
try {
|
|
998
|
+
matrixText = node_fs_1.default.readFileSync(node_path_1.default.join(resolvedDir, coverageFile), 'utf8');
|
|
999
|
+
}
|
|
1000
|
+
catch {
|
|
1001
|
+
// COVERAGE.md exists but is unreadable (EACCES/EIO/encoding). Fail-closed
|
|
1002
|
+
// with a useful message rather than a raw throw.
|
|
1003
|
+
output({
|
|
1004
|
+
block: true,
|
|
1005
|
+
passed: false,
|
|
1006
|
+
coverage_present: true,
|
|
1007
|
+
message: `api-coverage: COVERAGE.md exists but is unreadable — fix file permissions/encoding before sealing`,
|
|
1008
|
+
}, raw, undefined);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const v = validateCoverageMatrix(matrixText);
|
|
1012
|
+
if (v.valid) {
|
|
1013
|
+
output({
|
|
1014
|
+
block: false,
|
|
1015
|
+
passed: true,
|
|
1016
|
+
coverage_present: true,
|
|
1017
|
+
matrix: coverageFile,
|
|
1018
|
+
counts: v.counts,
|
|
1019
|
+
message: `api-coverage: matrix present (${v.counts.surface} capabilities, ${v.counts.optout} opt-out)`,
|
|
1020
|
+
}, raw, undefined);
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
// Fixed-template message (no raw cell content echoed into the LLM-facing
|
|
1024
|
+
// message). The structured `errors` array is safe (row-indexed, no cell
|
|
1025
|
+
// values) and travels as data for tooling that wants detail.
|
|
1026
|
+
output({
|
|
1027
|
+
block: true,
|
|
1028
|
+
passed: false,
|
|
1029
|
+
coverage_present: true,
|
|
1030
|
+
matrix: coverageFile,
|
|
1031
|
+
error_count: v.errors.length,
|
|
1032
|
+
errors: v.errors,
|
|
1033
|
+
message: `api-coverage: COVERAGE.md has ${v.errors.length} problem(s) — fix the matrix (every capability INTEGRATE or OPT-OUT with a reason) before sealing`,
|
|
1034
|
+
}, raw, undefined);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (suffixed.length > 1) {
|
|
1038
|
+
output({
|
|
1039
|
+
block: true,
|
|
1040
|
+
passed: false,
|
|
1041
|
+
coverage_present: false,
|
|
1042
|
+
message: `api-coverage: multiple *-COVERAGE.md files found (${suffixed.length}) — consolidate into one COVERAGE.md before sealing`,
|
|
1043
|
+
}, raw, undefined);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
// (2) no matrix — detect whether this phase integrates an external API.
|
|
1047
|
+
const scopeText = readPhaseScope(projectDir, resolvedDir, phaseNumber);
|
|
1048
|
+
const detection = detectApiIntegration(scopeText);
|
|
1049
|
+
if (detection.detected) {
|
|
1050
|
+
// Surface only verb/noun (typed, bounded) — NOT raw prose snippets — so the
|
|
1051
|
+
// gate output cannot relay injected PLAN.md instructions to the orchestrator.
|
|
1052
|
+
const signals = detection.signals.map((s) => ({ verb: s.verb, noun: s.noun }));
|
|
1053
|
+
output({
|
|
1054
|
+
block: true,
|
|
1055
|
+
passed: false,
|
|
1056
|
+
coverage_present: false,
|
|
1057
|
+
detected: true,
|
|
1058
|
+
signals,
|
|
1059
|
+
message: 'api-coverage: external-API integration detected without a coverage matrix. ' +
|
|
1060
|
+
'Produce COVERAGE.md enumerating the API surface (every capability INTEGRATE or ' +
|
|
1061
|
+
'OPT-OUT with a reason) before sealing. Full coverage is the default.',
|
|
1062
|
+
}, raw, undefined);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
output({
|
|
1066
|
+
block: false,
|
|
1067
|
+
passed: true,
|
|
1068
|
+
coverage_present: false,
|
|
1069
|
+
detected: false,
|
|
1070
|
+
message: 'api-coverage: no external-API integration detected; coverage matrix not required',
|
|
1071
|
+
}, raw, undefined);
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Read the phase-scope text used for API-integration detection. Uses the
|
|
1075
|
+
* resolved plan files (PLAN.md bodies — the planner's own words about what the
|
|
1076
|
+
* phase does) and, as a fallback, ONLY THIS PHASE'S ROADMAP section (not the
|
|
1077
|
+
* whole roadmap, which would cross-contaminate sibling phases). Strips nothing
|
|
1078
|
+
* here — detectApiIntegration strips fenced code itself.
|
|
1079
|
+
*/
|
|
1080
|
+
function readPhaseScope(projectDir, phaseDir, phaseNumber) {
|
|
1081
|
+
const chunks = [];
|
|
1082
|
+
try {
|
|
1083
|
+
const entries = node_fs_1.default.readdirSync(phaseDir, { withFileTypes: true });
|
|
1084
|
+
const plans = entries
|
|
1085
|
+
.filter((e) => e.isFile() && /-PLAN\.md$/i.test(e.name))
|
|
1086
|
+
.map((e) => e.name)
|
|
1087
|
+
.sort();
|
|
1088
|
+
for (const p of plans) {
|
|
1089
|
+
chunks.push(node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, p), 'utf8'));
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
catch {
|
|
1093
|
+
// ignore — fall through to roadmap
|
|
1094
|
+
}
|
|
1095
|
+
if (chunks.join('').trim().length > 0)
|
|
1096
|
+
return chunks.join('\n\n');
|
|
1097
|
+
// Fallback: ONLY this phase's ROADMAP section (not the whole file, which
|
|
1098
|
+
// would pollute detection with sibling-phase prose). Best-effort; absence or
|
|
1099
|
+
// an unresolvable section is non-fatal (detector returns not-detected).
|
|
1100
|
+
if (phaseNumber) {
|
|
1101
|
+
try {
|
|
1102
|
+
const section = getRoadmapPhaseWithFallback(projectDir, phaseNumber);
|
|
1103
|
+
if (section)
|
|
1104
|
+
return section;
|
|
1105
|
+
}
|
|
1106
|
+
catch {
|
|
1107
|
+
// ignore
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return '';
|
|
1111
|
+
}
|
|
879
1112
|
function routeCheckCommand({ args, cwd, raw }) {
|
|
880
1113
|
// Normalize dots to hyphens in the subcommand so both forms are accepted.
|
|
881
1114
|
// This makes `check.query = "ui.plan-gate"` (dotted form in capability.json gates)
|
|
@@ -905,6 +1138,12 @@ function routeCheckCommand({ args, cwd, raw }) {
|
|
|
905
1138
|
cmdGapAnalysisPlanPost(cwd, args, raw);
|
|
906
1139
|
return;
|
|
907
1140
|
}
|
|
1141
|
+
if (subcommand === 'api-coverage-verify-pre') {
|
|
1142
|
+
// ai-integration capability blocking gate at verify:pre (#1562). Dot-to-
|
|
1143
|
+
// hyphen normalization means query "api-coverage.verify-pre" routes here.
|
|
1144
|
+
cmdApiCoverageVerifyPre(cwd, args, raw);
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
908
1147
|
if (subcommand === 'tdd-review-checkpoint') {
|
|
909
1148
|
cmdTddReviewCheckpoint(cwd, args, raw);
|
|
910
1149
|
return;
|
|
@@ -945,7 +1184,7 @@ function routeCheckCommand({ args, cwd, raw }) {
|
|
|
945
1184
|
(0, prohibition_enforcement_cjs_1.routeProhibitionEnforcement)(args, raw);
|
|
946
1185
|
return;
|
|
947
1186
|
}
|
|
948
|
-
error('Unknown check subcommand. Available: auto-mode, decision-coverage-plan, decision-coverage-verify, gap-analysis-plan-post, predicate, prohibition-enforcement, tdd-review-checkpoint, ui-plan-gate, ui-safety-gate, verify-schema-drift, verify-codebase-drift', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1187
|
+
error('Unknown check subcommand. Available: api-coverage-verify-pre, auto-mode, decision-coverage-plan, decision-coverage-verify, gap-analysis-plan-post, predicate, prohibition-enforcement, tdd-review-checkpoint, ui-plan-gate, ui-safety-gate, verify-schema-drift, verify-codebase-drift', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
949
1188
|
}
|
|
950
1189
|
module.exports = {
|
|
951
1190
|
routeCheckCommand,
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Claude orchestration command router — CLI dispatcher for
|
|
4
|
+
* `gsd-tools claude-orchestration <subcommand>`.
|
|
5
|
+
*
|
|
6
|
+
* #1143 — thin CLI adapter over the pure `claude-orchestration.cjs` module.
|
|
7
|
+
* Lets execute-phase (or any orchestrator) invoke the Workflow-backend
|
|
8
|
+
* detection and the Workflow-script emitter through the standard capability
|
|
9
|
+
* command surface (ADR-959) instead of a bare `require()`.
|
|
10
|
+
*
|
|
11
|
+
* Router signature: { args, cwd, raw, error } — identical to the other host
|
|
12
|
+
* routers; discovered by dispatchCapabilityCommand via the registry's
|
|
13
|
+
* commandFamilies index.
|
|
14
|
+
*
|
|
15
|
+
* Subcommands:
|
|
16
|
+
* detect-backend [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch]
|
|
17
|
+
* Resolves whether the Workflow backend should activate. `--runtime`
|
|
18
|
+
* defaults to the GSD_RUNTIME env var (or 'unknown'). Reads the
|
|
19
|
+
* `claude_orchestration.*` keys from .planning/config.json. Emits
|
|
20
|
+
* { available, backend, reason }.
|
|
21
|
+
*
|
|
22
|
+
* emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>]
|
|
23
|
+
* Reads a wave/plan manifest JSON file and emits the generated Workflow
|
|
24
|
+
* script + summary. The manifest shape matches emitWorkflowScript's input:
|
|
25
|
+
* { waves: [{ id, plans: [{ id, brief, files_modified: string[] }] }] }.
|
|
26
|
+
*/
|
|
27
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
28
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
29
|
+
};
|
|
30
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
31
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
33
|
+
const io = require("./io.cjs");
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
|
+
const core = require("./claude-orchestration.cjs");
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
|
+
const configLoader = require("./config-loader.cjs");
|
|
38
|
+
const { output } = io;
|
|
39
|
+
const { detectWorkflowBackend, emitWorkflowScript } = core;
|
|
40
|
+
const CAPABLE_HOST = { dispatch: { nested: true, background: true } };
|
|
41
|
+
function usage(error) {
|
|
42
|
+
error('Usage: gsd-tools claude-orchestration <detect-backend|emit-workflow> [...]\n' +
|
|
43
|
+
' detect-backend [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch]\n' +
|
|
44
|
+
' emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>]');
|
|
45
|
+
}
|
|
46
|
+
function argValue(args, flag) {
|
|
47
|
+
const i = args.indexOf(flag);
|
|
48
|
+
return i !== -1 && i + 1 < args.length ? args[i + 1] : undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Detect whether the Workflow backend should activate for the current/given
|
|
52
|
+
* runtime. Reads `claude_orchestration.*` from the project config; runtime and
|
|
53
|
+
* SDK version come from flags (the orchestrator already knows these) or env.
|
|
54
|
+
*/
|
|
55
|
+
function cmdDetectBackend(args, cwd, raw) {
|
|
56
|
+
const runtimeId = argValue(args, '--runtime') || process.env['GSD_RUNTIME'] || 'unknown';
|
|
57
|
+
const agentSdkVersion = argValue(args, '--agent-sdk-version');
|
|
58
|
+
const noNested = args.includes('--no-nested-dispatch');
|
|
59
|
+
const hostIntegration = noNested ? { dispatch: { nested: false, background: true } } : CAPABLE_HOST;
|
|
60
|
+
// Resolve the claude_orchestration.* slice from the project config (federated
|
|
61
|
+
// keys are merged by loadConfig as a nested object). A config read failure
|
|
62
|
+
// degrades to inline — it must not break the core loop.
|
|
63
|
+
let claudeSlice = {};
|
|
64
|
+
try {
|
|
65
|
+
const loaded = configLoader.loadConfig(cwd);
|
|
66
|
+
const slice = loaded['claude_orchestration'];
|
|
67
|
+
if (slice && typeof slice === 'object' && !Array.isArray(slice)) {
|
|
68
|
+
claudeSlice = slice;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
claudeSlice = {};
|
|
73
|
+
}
|
|
74
|
+
// Flatten the nested slice into the dotted-key shape detectWorkflowBackend expects.
|
|
75
|
+
const flatConfig = {};
|
|
76
|
+
for (const k of Object.keys(claudeSlice)) {
|
|
77
|
+
flatConfig['claude_orchestration.' + k] = claudeSlice[k];
|
|
78
|
+
}
|
|
79
|
+
const result = detectWorkflowBackend({ runtimeId, hostIntegration, config: flatConfig, agentSdkVersion });
|
|
80
|
+
output(result, raw);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Emit a Workflow script from a wave/plan manifest file.
|
|
84
|
+
*/
|
|
85
|
+
function cmdEmitWorkflow(args, _cwd, raw, error) {
|
|
86
|
+
const wavesPath = argValue(args, '--waves');
|
|
87
|
+
const runId = argValue(args, '--run-id');
|
|
88
|
+
const phaseDir = argValue(args, '--phase-dir') || '.planning/phases/current';
|
|
89
|
+
const budgetRaw = argValue(args, '--budget');
|
|
90
|
+
if (!wavesPath) {
|
|
91
|
+
error('emit-workflow requires --waves <path>');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!runId) {
|
|
95
|
+
error('emit-workflow requires --run-id <id>');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
let waves;
|
|
99
|
+
try {
|
|
100
|
+
const content = node_fs_1.default.readFileSync(node_path_1.default.resolve(wavesPath), 'utf8');
|
|
101
|
+
const parsed = JSON.parse(content);
|
|
102
|
+
waves = parsed['waves'];
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
error('emit-workflow: could not read/parse --waves file "' + wavesPath + '": ' + (e instanceof Error ? e.message : String(e)));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const budgetTokens = budgetRaw !== undefined ? parseInt(budgetRaw, 10) : undefined;
|
|
109
|
+
const budget = (typeof budgetTokens === 'number' && !Number.isNaN(budgetTokens)) ? budgetTokens : undefined;
|
|
110
|
+
const result = emitWorkflowScript({
|
|
111
|
+
phaseDir,
|
|
112
|
+
runId,
|
|
113
|
+
waves: waves,
|
|
114
|
+
budgetTokens: budget,
|
|
115
|
+
});
|
|
116
|
+
if (!result.ok) {
|
|
117
|
+
error('emit-workflow: ' + result.reason);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
output({ script: result.script, summary: result.summary }, raw);
|
|
121
|
+
}
|
|
122
|
+
function routeClaudeOrchestrationCommand(opts) {
|
|
123
|
+
const { args, cwd, raw, error } = opts;
|
|
124
|
+
// args[0] is the family ('claude-orchestration'); the subcommand is args[1].
|
|
125
|
+
const subcommand = args[1];
|
|
126
|
+
if (subcommand === 'detect-backend') {
|
|
127
|
+
cmdDetectBackend(args, cwd, raw);
|
|
128
|
+
}
|
|
129
|
+
else if (subcommand === 'emit-workflow') {
|
|
130
|
+
cmdEmitWorkflow(args, cwd, raw, error);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
usage(error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
module.exports = { routeClaudeOrchestrationCommand };
|