@opengsd/gsd-core 1.7.0 → 1.9.0
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 +45 -1
- package/README.md +2 -0
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +78 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +49 -9
- package/agents/gsd-intel-updater.md +3 -3
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +20 -0
- package/agents/gsd-planner.md +44 -59
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1338 -135
- package/commands/gsd/ai-integration-phase.md +1 -1
- package/commands/gsd/mempalace-capture.md +9 -5
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +5 -3
- package/commands/gsd/plan-review-convergence.md +7 -2
- package/gsd-core/bin/gsd-tools.cjs +2690 -2472
- package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
- package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
- package/gsd-core/bin/lib/api-coverage.cjs +360 -53
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/broken-windows.cjs +716 -0
- package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
- package/gsd-core/bin/lib/capability-consent.cjs +40 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
- package/gsd-core/bin/lib/capability-loader.cjs +23 -1
- package/gsd-core/bin/lib/capability-registry.cjs +1450 -160
- package/gsd-core/bin/lib/capability-trust.cjs +468 -33
- package/gsd-core/bin/lib/capability-validator.cjs +882 -6
- package/gsd-core/bin/lib/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +140 -27
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +209 -31
- package/gsd-core/bin/lib/claude-orchestration.cjs +203 -25
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +326 -21
- package/gsd-core/bin/lib/config-loader.cjs +214 -30
- package/gsd-core/bin/lib/config.cjs +158 -22
- package/gsd-core/bin/lib/core-utils.cjs +6 -1
- package/gsd-core/bin/lib/decisions.cjs +32 -8
- package/gsd-core/bin/lib/docs.cjs +6 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +155 -66
- package/gsd-core/bin/lib/install-engine.cjs +299 -23
- package/gsd-core/bin/lib/install-profiles.cjs +239 -1
- package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
- package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
- package/gsd-core/bin/lib/milestone.cjs +248 -14
- package/gsd-core/bin/lib/model-catalog.cjs +69 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
- package/gsd-core/bin/lib/phase-id.cjs +304 -9
- package/gsd-core/bin/lib/phase.cjs +258 -17
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +70 -2
- package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
- package/gsd-core/bin/lib/profile-output.cjs +34 -8
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
- package/gsd-core/bin/lib/roadmap-parser.cjs +61 -10
- package/gsd-core/bin/lib/roadmap.cjs +23 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +38 -5
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +23 -9
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +156 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +70 -5
- package/gsd-core/bin/lib/state-document.cjs +171 -24
- package/gsd-core/bin/lib/state-transition.cjs +50 -11
- package/gsd-core/bin/lib/state.cjs +206 -32
- package/gsd-core/bin/lib/surface.cjs +51 -9
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +428 -11
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
- package/gsd-core/bin/lib/unusable-input.cjs +216 -0
- package/gsd-core/bin/lib/validate.cjs +44 -8
- package/gsd-core/bin/lib/verification.cjs +163 -31
- package/gsd-core/bin/lib/verify.cjs +348 -42
- package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +4 -15
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
- package/gsd-core/references/api-coverage.md +37 -7
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/common-bug-patterns.md +13 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
- package/gsd-core/references/debugger-fix-acceptance.md +157 -0
- package/gsd-core/references/debugger-philosophy.md +1 -0
- package/gsd-core/references/debugger-prevention.md +98 -0
- package/gsd-core/references/debugger-rca-branching.md +98 -0
- package/gsd-core/references/debugger-repro-hardening.md +130 -0
- package/gsd-core/references/debugger-sbfl.md +110 -0
- package/gsd-core/references/debugger-semantic-recall.md +81 -0
- package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
- package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
- package/gsd-core/references/execute-phase-response-language.md +7 -0
- package/gsd-core/references/gate-prompts.md +6 -3
- package/gsd-core/references/model-profile-resolution.md +64 -13
- package/gsd-core/references/offer-next.md +88 -0
- package/gsd-core/references/planner-antipatterns.md +6 -0
- package/gsd-core/references/planner-mvp-mode.md +12 -13
- package/gsd-core/references/planner-preconditions.md +156 -0
- package/gsd-core/references/planner-reversibility.md +132 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -19
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/DEBUG.md +5 -3
- package/gsd-core/templates/summary-minimal.md +4 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +7 -0
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +3 -1
- package/gsd-core/workflows/add-todo.md +32 -1
- package/gsd-core/workflows/ai-integration-phase.md +8 -6
- package/gsd-core/workflows/audit-fix.md +6 -2
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +5 -3
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +93 -24
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +35 -7
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discovery-phase.md +7 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
- package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
- package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +18 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +9 -0
- package/gsd-core/workflows/eval-review.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
- package/gsd-core/workflows/execute-phase.md +110 -149
- package/gsd-core/workflows/execute-plan.md +20 -8
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +9 -5
- package/gsd-core/workflows/import.md +11 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +19 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +17 -10
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +79 -23
- package/gsd-core/workflows/new-project.md +28 -19
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +56 -51
- package/gsd-core/workflows/plan-review-convergence.md +61 -12
- package/gsd-core/workflows/plant-seed.md +3 -0
- package/gsd-core/workflows/profile-user.md +7 -1
- package/gsd-core/workflows/progress.md +31 -3
- package/gsd-core/workflows/quick.md +33 -10
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +172 -585
- package/gsd-core/workflows/scan.md +10 -2
- package/gsd-core/workflows/secure-phase.md +13 -2
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +88 -11
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +4 -1
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/ui-phase.md +11 -2
- package/gsd-core/workflows/ui-review.md +11 -1
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +106 -5
- package/gsd-core/workflows/validate-phase.md +13 -2
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +15 -4
- package/hooks/dist/gsd-context-monitor.js +27 -9
- package/hooks/dist/gsd-cursor-session-start.js +6 -2
- package/hooks/dist/gsd-cursor-stop.js +6 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +9 -0
- package/hooks/dist/gsd-phase-boundary.sh +14 -2
- package/hooks/dist/gsd-prompt-guard.js +101 -2
- package/hooks/dist/gsd-read-guard.js +100 -2
- package/hooks/dist/gsd-read-injection-scanner.js +109 -2
- package/hooks/dist/gsd-statusline.js +97 -9
- package/hooks/dist/gsd-workflow-guard.js +110 -6
- package/hooks/dist/gsd-worktree-path-guard.js +132 -8
- package/hooks/dist/lib/cursor-workspace.js +74 -0
- package/hooks/gsd-context-monitor.js +27 -9
- package/hooks/gsd-cursor-session-start.js +6 -2
- package/hooks/gsd-cursor-stop.js +6 -2
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +9 -0
- package/hooks/gsd-phase-boundary.sh +14 -2
- package/hooks/gsd-prompt-guard.js +101 -2
- package/hooks/gsd-read-guard.js +100 -2
- package/hooks/gsd-read-injection-scanner.js +109 -2
- package/hooks/gsd-statusline.js +97 -9
- package/hooks/gsd-workflow-guard.js +110 -6
- package/hooks/gsd-worktree-path-guard.js +132 -8
- package/hooks/lib/cursor-workspace.js +74 -0
- package/package.json +10 -8
- package/pi/gsd.cjs +34 -3
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +244 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +528 -0
- package/scripts/gen-capability-matrix.cjs +26 -2
- package/scripts/gen-capability-registry.cjs +132 -34
- package/scripts/gen-emitted-baseline.cjs +145 -0
- package/scripts/gen-test-timings.cjs +201 -0
- package/scripts/lint-compiled-artifact-sync.cjs +146 -0
- package/scripts/lint-emitted-drift-ack.cjs +149 -0
- package/scripts/lint-fix-has-regression-test.cjs +131 -0
- package/scripts/lint-portable-timeout.cjs +140 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +57 -8
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +9 -5
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +5 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +7 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
package/bin/install.js
CHANGED
|
@@ -168,15 +168,28 @@ const DEFAULT_RUNTIME = 'claude';
|
|
|
168
168
|
const GSD_CLAUDE_ALLOW_PERMISSIONS = Object.freeze([
|
|
169
169
|
'Bash(npx gsd-core *)',
|
|
170
170
|
'Read(.planning/*)',
|
|
171
|
-
'
|
|
171
|
+
'Edit(.planning/*)',
|
|
172
172
|
'Read(STATE.md)',
|
|
173
|
-
'
|
|
173
|
+
'Edit(STATE.md)',
|
|
174
174
|
]);
|
|
175
175
|
const GSD_CLAUDE_DENY_PERMISSIONS = Object.freeze([
|
|
176
176
|
'Read(.env)',
|
|
177
177
|
'Read(.env.*)',
|
|
178
178
|
'Read(.secrets)',
|
|
179
179
|
]);
|
|
180
|
+
// #2278 — Stale allow-rule forms from before the fix. Claude Code has no
|
|
181
|
+
// standalone `Write` permission gate: file-editing tools (Write/Edit/
|
|
182
|
+
// NotebookEdit) are gated collectively via `Edit(pattern)`. The original
|
|
183
|
+
// `Write(.planning/*)` / `Write(STATE.md)` entries were therefore silently
|
|
184
|
+
// unmatched (never granted anything) and Claude Code additionally surfaces a
|
|
185
|
+
// session-start warning about unmatched permission rules. This list lets
|
|
186
|
+
// mergeClaudePermissions and uninstall cleanup retire those stale entries on
|
|
187
|
+
// existing installs while the current GSD_CLAUDE_ALLOW_PERMISSIONS above
|
|
188
|
+
// carries the working `Edit(...)` forms.
|
|
189
|
+
const GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS = Object.freeze([
|
|
190
|
+
'Write(.planning/*)',
|
|
191
|
+
'Write(STATE.md)',
|
|
192
|
+
]);
|
|
180
193
|
|
|
181
194
|
/**
|
|
182
195
|
* Merge GSD-owned permission entries into a Claude Code settings object.
|
|
@@ -185,6 +198,12 @@ const GSD_CLAUDE_DENY_PERMISSIONS = Object.freeze([
|
|
|
185
198
|
* entries are appended only if not already present. No other permission sub-keys
|
|
186
199
|
* (ask, disableBypassPermissionsMode, etc.) are touched.
|
|
187
200
|
*
|
|
201
|
+
* Migration (#2278): before adding the current GSD_CLAUDE_ALLOW_PERMISSIONS,
|
|
202
|
+
* any stale GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS entry (e.g. the unmatched
|
|
203
|
+
* `Write(...)` forms from before the fix) is removed from permissions.allow,
|
|
204
|
+
* so existing installs end up with the working `Edit(...)` forms instead of
|
|
205
|
+
* both the dead legacy entry and its replacement sitting side by side.
|
|
206
|
+
*
|
|
188
207
|
* Defensive: if settings is not a plain object, returns immediately without
|
|
189
208
|
* throwing. If permissions.allow / permissions.deny exist but are not arrays
|
|
190
209
|
* (malformed settings), they are replaced with valid arrays.
|
|
@@ -205,6 +224,10 @@ function mergeClaudePermissions(settings) {
|
|
|
205
224
|
settings.permissions.deny = [];
|
|
206
225
|
}
|
|
207
226
|
|
|
227
|
+
settings.permissions.allow = settings.permissions.allow.filter(
|
|
228
|
+
(e) => !GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS.includes(e)
|
|
229
|
+
);
|
|
230
|
+
|
|
208
231
|
for (const entry of GSD_CLAUDE_ALLOW_PERMISSIONS) {
|
|
209
232
|
if (!settings.permissions.allow.includes(entry)) {
|
|
210
233
|
settings.permissions.allow.push(entry);
|
|
@@ -302,7 +325,13 @@ const GSD_WINDSURF_HOOK_SCRIPTS = [
|
|
|
302
325
|
|
|
303
326
|
// GSD-managed files under hooks/lib/ (helpers required by gsd-*.sh hooks).
|
|
304
327
|
// git-cmd.js does not start with "gsd-" (shared classifier for #3129), gsd-graphify-rebuild.sh does.
|
|
305
|
-
|
|
328
|
+
// cursor-workspace.js (#2587) is required by the Cursor lifecycle hooks. Those
|
|
329
|
+
// are staged individually by writeCursorHooksJson (Cursor sets
|
|
330
|
+
// hostBehaviors.skipSharedHooksInstall, so it never reaches the bulk hooks/lib
|
|
331
|
+
// copy below) — that function stages this helper alongside them. Listing it
|
|
332
|
+
// here keeps uninstall and the manifest managing it for every OTHER runtime
|
|
333
|
+
// that does receive hooks/lib.
|
|
334
|
+
const GSD_HOOK_LIB_FILES = ['git-cmd.js', 'gsd-graphify-rebuild.sh', 'cursor-workspace.js'];
|
|
306
335
|
|
|
307
336
|
const CODEX_AGENT_SANDBOX = {
|
|
308
337
|
'gsd-executor': 'workspace-write',
|
|
@@ -352,6 +381,7 @@ const {
|
|
|
352
381
|
} = require(path.join(_gsdLibDir, 'model-catalog.cjs'));
|
|
353
382
|
const {
|
|
354
383
|
resolveTierEntry: gsdResolveTierEntry,
|
|
384
|
+
CLAUDE_AGENT_ALIASES,
|
|
355
385
|
} = require(path.join(_gsdLibDir, 'model-resolver.cjs'));
|
|
356
386
|
|
|
357
387
|
// #2071 — install-time effort resolution (readGsdEffectiveEffortConfig /
|
|
@@ -390,6 +420,32 @@ try {
|
|
|
390
420
|
_capabilityRegistry = undefined;
|
|
391
421
|
}
|
|
392
422
|
|
|
423
|
+
// #2322 BLOCKER 2: `_capabilityRegistry` above is the FROZEN first-party registry
|
|
424
|
+
// (capability-registry.cjs, built at publish time) — it never reflects an
|
|
425
|
+
// INSTALLED third-party overlay capability, so a fresh `gsd install` could never
|
|
426
|
+
// stage an installed third-party capability's skill regardless of registration,
|
|
427
|
+
// even on the DEFAULT `--profile full`. `_installedCapabilityRegistry` composes
|
|
428
|
+
// the overlay via capability-loader's `loadRegistry({includeInstalled:true})` —
|
|
429
|
+
// the SAME call capability-writer.cts's `capability set --runtime` path already
|
|
430
|
+
// uses — so a fresh install and a post-install `capability set` agree on
|
|
431
|
+
// third-party skill availability. Used ONLY for skill-profile resolution and
|
|
432
|
+
// runtime-artifact-layout staging below; `_capabilityRegistry` (frozen) remains
|
|
433
|
+
// the source for gsd-core's OWN runtime/host-behavior descriptors (unaffected —
|
|
434
|
+
// those are always first-party). A load failure degrades to the frozen
|
|
435
|
+
// `_capabilityRegistry` (no overlay data -> no third-party skills staged; never
|
|
436
|
+
// a crash and never a scan-and-guess fallback).
|
|
437
|
+
let _installedCapabilityRegistry;
|
|
438
|
+
try {
|
|
439
|
+
const _capabilityLoader = require(path.join(_gsdLibDir, 'capability-loader.cjs'));
|
|
440
|
+
_installedCapabilityRegistry = _capabilityLoader.loadRegistry({
|
|
441
|
+
includeInstalled: true,
|
|
442
|
+
cwd: process.cwd(),
|
|
443
|
+
gsdHome: process.env['GSD_HOME'],
|
|
444
|
+
});
|
|
445
|
+
} catch (_) {
|
|
446
|
+
_installedCapabilityRegistry = _capabilityRegistry;
|
|
447
|
+
}
|
|
448
|
+
|
|
393
449
|
// Fail-safe floor for the reference host's #338-privacy-critical behaviors, used
|
|
394
450
|
// ONLY when the first-party capability registry cannot be loaded (a broken bundle).
|
|
395
451
|
// Without it, a registry-load failure would make `_hostBehaviors('claude')` return
|
|
@@ -441,6 +497,25 @@ function _hostBehaviors(runtime) {
|
|
|
441
497
|
return _resolveHostBehaviors(runtime, _capabilityRegistry);
|
|
442
498
|
}
|
|
443
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Read a runtime's documentation-sourced `hostIntegration.dispatch` axes
|
|
502
|
+
* (ADR-1239 Phase A — `capabilities/<runtime>/capability.json`
|
|
503
|
+
* `runtime.hostIntegration.dispatch`): `{namedDispatch, nested, maxDepth,
|
|
504
|
+
* background, backgroundDispatch, subagentToolkit}`. These are validated,
|
|
505
|
+
* closed-vocabulary FACTS about what the runtime's real dispatch primitive
|
|
506
|
+
* supports (never inferred) — see `docs/reference/host-integration-capability-
|
|
507
|
+
* matrix.md` for citations. Unlike `_hostBehaviors` (install *policy*), this is
|
|
508
|
+
* the negotiated *capability* surface; #2284 is its first content-projection
|
|
509
|
+
* consumer (previously read only by `shouldFlattenDispatch`). Returns `{}` if
|
|
510
|
+
* the registry or the runtime's descriptor is unavailable, so callers must
|
|
511
|
+
* treat every axis as absent/unknown (fail-closed) rather than assume a value.
|
|
512
|
+
*/
|
|
513
|
+
function _hostIntegrationDispatch(runtime) {
|
|
514
|
+
const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime];
|
|
515
|
+
const dispatch = cap && cap.runtime && cap.runtime.hostIntegration && cap.runtime.hostIntegration.dispatch;
|
|
516
|
+
return dispatch || {};
|
|
517
|
+
}
|
|
518
|
+
|
|
444
519
|
/**
|
|
445
520
|
* Resolve the ACTUAL on-disk skills-install directory for a runtime, honoring a
|
|
446
521
|
* skills-kind `home` override (ADR-1239 upgrade 3 / #2088: e.g. Codex skills ->
|
|
@@ -510,6 +585,7 @@ const {
|
|
|
510
585
|
_installNativePluginIfDeclared,
|
|
511
586
|
_copyStaged,
|
|
512
587
|
hasExistingSymlinkBetween,
|
|
588
|
+
isSymlinkedDestOptIn,
|
|
513
589
|
preserveUserArtifacts,
|
|
514
590
|
restoreUserArtifacts,
|
|
515
591
|
migrateLegacyDevPreferencesToSkill,
|
|
@@ -557,7 +633,7 @@ if (hasMinimal && _profileArgRaw) {
|
|
|
557
633
|
|
|
558
634
|
function selectRuntimesFromArgs(runtimeArgs) {
|
|
559
635
|
if (runtimeArgs.includes('--all')) {
|
|
560
|
-
return ['claude', 'kimi', 'kilo', 'opencode', 'pi', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'zcode'];
|
|
636
|
+
return ['claude', 'kimi', 'kimi-code', 'kilo', 'opencode', 'pi', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'zcode'];
|
|
561
637
|
}
|
|
562
638
|
if (runtimeArgs.includes('--both')) {
|
|
563
639
|
return ['claude', 'opencode'];
|
|
@@ -578,6 +654,7 @@ function selectRuntimesFromArgs(runtimeArgs) {
|
|
|
578
654
|
if (runtimeArgs.includes('--qwen')) selected.push('qwen');
|
|
579
655
|
if (runtimeArgs.includes('--hermes')) selected.push('hermes');
|
|
580
656
|
if (runtimeArgs.includes('--kimi')) selected.push('kimi');
|
|
657
|
+
if (runtimeArgs.includes('--kimi-code')) selected.push('kimi-code');
|
|
581
658
|
if (runtimeArgs.includes('--codebuddy')) selected.push('codebuddy');
|
|
582
659
|
if (runtimeArgs.includes('--cline')) selected.push('cline');
|
|
583
660
|
if (runtimeArgs.includes('--zcode')) selected.push('zcode');
|
|
@@ -587,6 +664,62 @@ function selectRuntimesFromArgs(runtimeArgs) {
|
|
|
587
664
|
// Runtime selection - can be set by flags or interactive prompt
|
|
588
665
|
let selectedRuntimes = selectRuntimesFromArgs(args);
|
|
589
666
|
|
|
667
|
+
// #2505 Phase 5: Kimi variant disambiguation (#2513). Kimi CLI (Python, ~/.kimi/)
|
|
668
|
+
// and Kimi Code (Node, ~/.kimi-code/) are two distinct Moonshot products that
|
|
669
|
+
// share the "kimi" brand. Probe for each product's config.toml and warn when
|
|
670
|
+
// the selected runtime doesn't match the detected install — catches the common
|
|
671
|
+
// "ran --kimi --global but actually on Kimi Code" mistake that produced inert
|
|
672
|
+
// YAMLs and empty agent-skills before the Phase 1 descriptor split.
|
|
673
|
+
function disambiguateKimiVariant(runtimes) {
|
|
674
|
+
const home = os.homedir();
|
|
675
|
+
const hasKimiCli = fs.existsSync(path.join(home, '.kimi', 'config.toml'));
|
|
676
|
+
const hasKimiCode = fs.existsSync(path.join(home, '.kimi-code', 'config.toml'));
|
|
677
|
+
const notices = [];
|
|
678
|
+
if (runtimes.includes('kimi') && hasKimiCode && !hasKimiCli) {
|
|
679
|
+
notices.push({
|
|
680
|
+
kind: 'wrong-variant',
|
|
681
|
+
selected: 'kimi',
|
|
682
|
+
detected: 'kimi-code',
|
|
683
|
+
message: `Detected ~/.kimi-code/config.toml (Kimi Code, Node CLI) but not ~/.kimi/config.toml (Kimi CLI, Python). You selected --kimi but appear to be on Kimi Code. Re-run with --kimi-code for a working install. (Kimi CLI = Python kimi-cli with named subagents; Kimi Code = Node CLI with coder/explore/plan built-ins only.)`,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
if (runtimes.includes('kimi-code') && hasKimiCli && !hasKimiCode) {
|
|
687
|
+
notices.push({
|
|
688
|
+
kind: 'wrong-variant',
|
|
689
|
+
selected: 'kimi-code',
|
|
690
|
+
detected: 'kimi',
|
|
691
|
+
message: `Detected ~/.kimi/config.toml (Kimi CLI, Python) but not ~/.kimi-code/config.toml (Kimi Code, Node CLI). You selected --kimi-code but appear to be on Kimi CLI. Re-run with --kimi for a working install.`,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
// Distinct-entry descriptions when either Kimi variant is selected.
|
|
695
|
+
if (runtimes.includes('kimi')) {
|
|
696
|
+
notices.push({
|
|
697
|
+
kind: 'description',
|
|
698
|
+
runtime: 'kimi',
|
|
699
|
+
message: 'Kimi CLI (Python kimi-cli): named subagents via YAML, config at ~/.kimi/, hooks via ~/.kimi/config.toml [[hooks]].',
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
if (runtimes.includes('kimi-code')) {
|
|
703
|
+
notices.push({
|
|
704
|
+
kind: 'description',
|
|
705
|
+
runtime: 'kimi-code',
|
|
706
|
+
message: 'Kimi Code (Node CLI): three built-in subagents (coder/explore/plan), Agent Skills at ~/.kimi-code/skills/, config at ~/.kimi-code/.',
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
return notices;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (selectedRuntimes.includes('kimi') || selectedRuntimes.includes('kimi-code')) {
|
|
713
|
+
const kimiNotices = disambiguateKimiVariant(selectedRuntimes);
|
|
714
|
+
for (const notice of kimiNotices) {
|
|
715
|
+
if (notice.kind === 'wrong-variant') {
|
|
716
|
+
console.error(`${yellow}⚠ Kimi variant mismatch (${notice.selected} → ${notice.detected}).${reset} ${notice.message}`);
|
|
717
|
+
} else if (notice.kind === 'description') {
|
|
718
|
+
console.log(`${dim} ${notice.runtime}: ${notice.message}${reset}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
590
723
|
// #1928: Google sunset Gemini CLI on 2026-06-18; Antigravity CLI is its
|
|
591
724
|
// official successor. `--gemini` is no longer a valid runtime selector —
|
|
592
725
|
// selectRuntimesFromArgs above no longer recognizes it, so it never lands in
|
|
@@ -2368,6 +2501,56 @@ function extractFrontmatterField(frontmatter, fieldName) {
|
|
|
2368
2501
|
return match[1].trim().replace(/^['"]|['"]$/g, '');
|
|
2369
2502
|
}
|
|
2370
2503
|
|
|
2504
|
+
// #2284 finding (b): the `<runtime_compatibility>` block appearing in
|
|
2505
|
+
// gsd-core/workflows/{plan-phase,execute-phase}.md is a runtime-COMPARISON
|
|
2506
|
+
// table ("**Claude Code:** Uses `Agent(...)`" / "a backgrounded Claude Code
|
|
2507
|
+
// agent" / "top-level Claude Code") — every "Claude Code" mention inside it
|
|
2508
|
+
// is a COMPARED-RUNTIME LABEL, not a host self-reference. The brand swap
|
|
2509
|
+
// below (`Claude Code` → the installing runtime's own display name) is
|
|
2510
|
+
// meant only for host self-references; applying it inside this block
|
|
2511
|
+
// mislabels the comparison (e.g. Windsurf installs would read "**Windsurf:**
|
|
2512
|
+
// Uses `Agent(...)`" describing what is actually Claude Code's behavior).
|
|
2513
|
+
// This is cross-cutting across every runtime that brand-swaps workflow
|
|
2514
|
+
// content (cursor/windsurf/trae/cline/codebuddy hardcoded; qwen/hermes
|
|
2515
|
+
// descriptor-driven via hostBehaviors.brandingRewrites) — confirmed to
|
|
2516
|
+
// reproduce on unmodified Windsurf, not Hermes-specific.
|
|
2517
|
+
const RUNTIME_COMPATIBILITY_BLOCK_RE = /<runtime_compatibility>[\s\S]*?<\/runtime_compatibility>/g;
|
|
2518
|
+
|
|
2519
|
+
/**
|
|
2520
|
+
* Rewrite bare "Claude Code" self-references in workflow content to
|
|
2521
|
+
* `brandName`, EXCEPT inside `<runtime_compatibility>...</runtime_compatibility>`
|
|
2522
|
+
* blocks, which are left byte-for-byte verbatim. Every other content
|
|
2523
|
+
* transform in a runtime's `.md` converter (tool-name renames, path
|
|
2524
|
+
* rewrites, etc.) is unaffected — only this literal brand-name swap is
|
|
2525
|
+
* protected-region-aware, since only it risks mislabeling a
|
|
2526
|
+
* runtime-comparison table.
|
|
2527
|
+
*
|
|
2528
|
+
* Implementation: SPLIT `content` on the protected-block regex, brand-swap
|
|
2529
|
+
* only the GAP text between (and around) matches, then rejoin gap+block
|
|
2530
|
+
* alternately. No placeholder/sentinel token of any kind is substituted in
|
|
2531
|
+
* — a prior version used a sentinel-token mask/restore, which is exactly the
|
|
2532
|
+
* kind of invisible landmine this rewrite eliminates (a sentinel string, no
|
|
2533
|
+
* matter how obscure, is a theoretical collision risk with real content and
|
|
2534
|
+
* is easy to silently reintroduce in a future edit without it showing in a
|
|
2535
|
+
* diff). Behavior-identical to the removed sentinel-token version — verified
|
|
2536
|
+
* via `npm run gen:golden` producing zero further diff.
|
|
2537
|
+
*/
|
|
2538
|
+
function applyClaudeCodeBrandSwap(content, brandName) {
|
|
2539
|
+
if (!brandName) return content;
|
|
2540
|
+
let result = '';
|
|
2541
|
+
let lastIndex = 0;
|
|
2542
|
+
RUNTIME_COMPATIBILITY_BLOCK_RE.lastIndex = 0; // reset shared global-regex state before each use
|
|
2543
|
+
let m;
|
|
2544
|
+
while ((m = RUNTIME_COMPATIBILITY_BLOCK_RE.exec(content))) {
|
|
2545
|
+
const gap = content.slice(lastIndex, m.index);
|
|
2546
|
+
result += gap.replace(/\bClaude Code\b/g, brandName);
|
|
2547
|
+
result += m[0]; // protected block, verbatim — never brand-swapped
|
|
2548
|
+
lastIndex = m.index + m[0].length;
|
|
2549
|
+
}
|
|
2550
|
+
result += content.slice(lastIndex).replace(/\bClaude Code\b/g, brandName);
|
|
2551
|
+
return result;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2371
2554
|
// Tool name mapping from Claude Code to Cursor CLI
|
|
2372
2555
|
const claudeToCursorTools = {
|
|
2373
2556
|
Bash: 'Shell',
|
|
@@ -2400,8 +2583,9 @@ function convertClaudeToCursorMarkdown(content) {
|
|
|
2400
2583
|
// Remove Claude Code-specific bug workarounds before brand replacement
|
|
2401
2584
|
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
2402
2585
|
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
|
2403
|
-
// Replace "Claude Code" brand references with "Cursor"
|
|
2404
|
-
|
|
2586
|
+
// Replace "Claude Code" brand references with "Cursor" — #2284(b): skips
|
|
2587
|
+
// <runtime_compatibility> comparison-table content (protected region).
|
|
2588
|
+
converted = applyClaudeCodeBrandSwap(converted, 'Cursor');
|
|
2405
2589
|
return converted;
|
|
2406
2590
|
}
|
|
2407
2591
|
|
|
@@ -2445,7 +2629,13 @@ function convertClaudeCommandToCursorSkill(content, skillName) {
|
|
|
2445
2629
|
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
|
2446
2630
|
const adapter = getCursorSkillAdapterHeader(skillName);
|
|
2447
2631
|
|
|
2448
|
-
|
|
2632
|
+
// #2341: mark user-invocable:false so the skill is NOT shown in Cursor's '/'
|
|
2633
|
+
// menu (it defaults to true). Cursor also writes a commands/ surface (#785),
|
|
2634
|
+
// and surfacing both duplicated every /gsd-* entry. This mirrors the #789
|
|
2635
|
+
// CodeBuddy de-dup: the commands/ surface is the sole '/' entry point; skills
|
|
2636
|
+
// stay model-invocable background knowledge. (user-invocable:false hides from
|
|
2637
|
+
// '/' while keeping model invocation — distinct from disable-model-invocation.)
|
|
2638
|
+
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
|
2449
2639
|
}
|
|
2450
2640
|
|
|
2451
2641
|
/**
|
|
@@ -2533,8 +2723,9 @@ function convertClaudeToWindsurfMarkdown(content) {
|
|
|
2533
2723
|
// Remove Claude Code-specific bug workarounds before brand replacement
|
|
2534
2724
|
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
2535
2725
|
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
|
2536
|
-
// Replace "Claude Code" brand references with "Windsurf"
|
|
2537
|
-
|
|
2726
|
+
// Replace "Claude Code" brand references with "Windsurf" — #2284(b): skips
|
|
2727
|
+
// <runtime_compatibility> comparison-table content (protected region).
|
|
2728
|
+
converted = applyClaudeCodeBrandSwap(converted, 'Windsurf');
|
|
2538
2729
|
return converted;
|
|
2539
2730
|
}
|
|
2540
2731
|
|
|
@@ -2668,7 +2859,8 @@ function convertClaudeToTraeMarkdown(content) {
|
|
|
2668
2859
|
converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'TRAE_CONFIG_DIR');
|
|
2669
2860
|
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
2670
2861
|
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
|
2671
|
-
|
|
2862
|
+
// #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
|
|
2863
|
+
converted = applyClaudeCodeBrandSwap(converted, 'Trae');
|
|
2672
2864
|
return converted;
|
|
2673
2865
|
}
|
|
2674
2866
|
|
|
@@ -2740,7 +2932,8 @@ function convertClaudeToCodebuddyMarkdown(content) {
|
|
|
2740
2932
|
converted = converted.replace(/\.claude\//g, '.codebuddy/');
|
|
2741
2933
|
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
2742
2934
|
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
|
2743
|
-
|
|
2935
|
+
// #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
|
|
2936
|
+
converted = applyClaudeCodeBrandSwap(converted, 'CodeBuddy');
|
|
2744
2937
|
return converted;
|
|
2745
2938
|
}
|
|
2746
2939
|
|
|
@@ -2835,7 +3028,8 @@ function convertClaudeToCliineMarkdown(content) {
|
|
|
2835
3028
|
converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'CLINE_CONFIG_DIR');
|
|
2836
3029
|
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
2837
3030
|
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
|
2838
|
-
|
|
3031
|
+
// #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
|
|
3032
|
+
converted = applyClaudeCodeBrandSwap(converted, 'Cline');
|
|
2839
3033
|
return converted;
|
|
2840
3034
|
}
|
|
2841
3035
|
|
|
@@ -2888,6 +3082,825 @@ function convertClaudeCommandToClineSkill(content, skillName, runtime = null, cm
|
|
|
2888
3082
|
|
|
2889
3083
|
// ── End Cline converters ─────────────────────────────────────────────────────
|
|
2890
3084
|
|
|
3085
|
+
// ── Hermes converters (#2284) ────────────────────────────────────────────────
|
|
3086
|
+
//
|
|
3087
|
+
// Hermes exposes `delegate_task` for subagent dispatch, not the Claude-shaped
|
|
3088
|
+
// `Agent(...)` tool the host-neutral `gsd-core/workflows/*.md` corpus assumes.
|
|
3089
|
+
// Prior to this fix, the hermes `.md` hook (RUNTIME_CONTENT_DISPATCH.hermes)
|
|
3090
|
+
// only brand-swapped "Claude Code" → "Hermes Agent" via
|
|
3091
|
+
// hostBehaviors.brandingRewrites, leaving the false "Agent tool IS available"
|
|
3092
|
+
// assertion and literal `Agent(...)` call syntax installed verbatim.
|
|
3093
|
+
//
|
|
3094
|
+
// `projectNamedDispatchToStructuralDelegate` is GENERIC projection machinery:
|
|
3095
|
+
// it branches ENTIRELY on the runtime's documentation-sourced
|
|
3096
|
+
// `hostIntegration.dispatch` facts (read via `_hostIntegrationDispatch`,
|
|
3097
|
+
// capabilities/<runtime>/capability.json — never hardcoded here) and a
|
|
3098
|
+
// `toolConfig` that supplies only the target primitive's own vocabulary (its
|
|
3099
|
+
// call name + native parameter names — not a capability claim; there is no
|
|
3100
|
+
// `dispatch` axis for "the call's own parameter names", so that vocabulary is
|
|
3101
|
+
// necessarily supplied by the caller, exactly as every other runtime's
|
|
3102
|
+
// converter supplies its own tool-name vocabulary, e.g. Trae's `Shell(`).
|
|
3103
|
+
//
|
|
3104
|
+
// Hermes-specific facts consumed (capabilities/hermes/capability.json,
|
|
3105
|
+
// docs/reference/host-integration-capability-matrix.md:244-249 — UNCHANGED by
|
|
3106
|
+
// this fix):
|
|
3107
|
+
// - dispatch.namedDispatch: false — Hermes's delegate_task has no named-
|
|
3108
|
+
// agent lookup ("Subagents are identified only by role ('leaf' or
|
|
3109
|
+
// 'orchestrator')"). GSD resolves the referenced gsd-* role itself
|
|
3110
|
+
// (fail-closed against the staged agents/ dir) and embeds the loaded
|
|
3111
|
+
// PROMPT CONTENT into the delegate_task payload.
|
|
3112
|
+
// - dispatch.background: true — `delegate_task(background=true)` "returns a
|
|
3113
|
+
// handle immediately"; Claude's `run_in_background=` maps onto Hermes's
|
|
3114
|
+
// own `background=` parameter, preserving the async-handle / no-busy-poll
|
|
3115
|
+
// / resume-on-completion wording already used throughout these workflows.
|
|
3116
|
+
// - dispatch.subagentToolkit: "read-only" / dispatch.maxDepth: 1 — dispatched
|
|
3117
|
+
// roles never themselves further delegate, so no nested-delegation
|
|
3118
|
+
// instruction is ever emitted toward them.
|
|
3119
|
+
|
|
3120
|
+
/**
|
|
3121
|
+
* Resolve the set of gsd-* role-prompt stems actually shipped in this
|
|
3122
|
+
* package's `agents/` directory (the FULL source set, not profile-staged —
|
|
3123
|
+
* `--minimal`/`--profile=core` intentionally excludes many agents from a
|
|
3124
|
+
* given install without those workflows being unreachable, so validating
|
|
3125
|
+
* against the profile-filtered subset would fail every restricted-profile
|
|
3126
|
+
* Hermes install; validating against the shipped source catches genuine
|
|
3127
|
+
* authoring bugs — a stale/typo'd role reference — without that regression).
|
|
3128
|
+
* Returns `null` if the directory cannot be resolved (fail-closed: callers
|
|
3129
|
+
* must refuse to install rather than skip validation).
|
|
3130
|
+
*/
|
|
3131
|
+
function _resolveAvailableGsdRoles() {
|
|
3132
|
+
try {
|
|
3133
|
+
const agentsDir = path.join(__dirname, '..', 'agents');
|
|
3134
|
+
return new Set(
|
|
3135
|
+
fs.readdirSync(agentsDir, { withFileTypes: true })
|
|
3136
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
3137
|
+
.map((e) => e.name.slice(0, -3)),
|
|
3138
|
+
);
|
|
3139
|
+
} catch (_e) {
|
|
3140
|
+
return null;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
/**
|
|
3145
|
+
* Fail-closed validation (#2284 AC: "Missing role prompts fail closed" /
|
|
3146
|
+
* "never emit a workflow referencing an unresolvable role"). A single literal
|
|
3147
|
+
* `gsd-*` role value must resolve to a real `agents/<role>.md` file — throws
|
|
3148
|
+
* an explicit Error otherwise, aborting the install (the standard
|
|
3149
|
+
* `copyWithPathReplacement` failure path already used for its own
|
|
3150
|
+
* confinement-violation throws). Called per extracted role value from EVERY
|
|
3151
|
+
* call-syntax form (`subagent_type=`, `subagent_type:`, post-rename
|
|
3152
|
+
* `gsd_role=`) — the check operates on the resolved value, independent of
|
|
3153
|
+
* which source syntax produced it. Non-literal / dynamic expressions (e.g.
|
|
3154
|
+
* `research_hook.ref.agent`) are not quoted strings and are never passed
|
|
3155
|
+
* here; they carry their own runtime resolution + fail-closed instruction via
|
|
3156
|
+
* the injected per-call resolution line.
|
|
3157
|
+
*/
|
|
3158
|
+
function _assertRoleResolvable(role, availableRoles, runtime, sourceDescription) {
|
|
3159
|
+
if (!availableRoles) {
|
|
3160
|
+
throw new Error(
|
|
3161
|
+
`${runtime} workflow install: could not resolve the shipped agents/ directory to validate named-role ` +
|
|
3162
|
+
'dispatch references — refusing to install (fail-closed, #2284)',
|
|
3163
|
+
);
|
|
3164
|
+
}
|
|
3165
|
+
if (role.startsWith('gsd-') && !availableRoles.has(role)) {
|
|
3166
|
+
throw new Error(
|
|
3167
|
+
`${runtime} workflow install: dispatch references role "${role}" via ${sourceDescription}, but no ` +
|
|
3168
|
+
`matching agents/${role}.md prompt file is shipped — refusing to install a workflow that dispatches ` +
|
|
3169
|
+
'an unresolvable role (fail-closed, #2284)',
|
|
3170
|
+
);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
/**
|
|
3175
|
+
* Segment `text` into 'code' and 'string' runs (recognizes `"..."`, `'...'`,
|
|
3176
|
+
* and Python-style `"""..."""`, with backslash-escaping). Required because
|
|
3177
|
+
* the real corpus embeds unescaped parens inside quoted prompt bodies (e.g.
|
|
3178
|
+
* discuss-phase-assumptions.md's `(e.g., "Technical Approach")` inside a
|
|
3179
|
+
* `"""`-quoted prompt) — naive paren/keyword scanning across raw text would
|
|
3180
|
+
* desync on these. Downstream call-span detection and header-token
|
|
3181
|
+
* extraction operate on a same-length MASK derived from this segmentation
|
|
3182
|
+
* (see `maskStringLiterals`) so string content can never be mistaken for
|
|
3183
|
+
* call structure.
|
|
3184
|
+
*/
|
|
3185
|
+
function _segmentCodeAndStrings(text) {
|
|
3186
|
+
const segments = [];
|
|
3187
|
+
let i = 0;
|
|
3188
|
+
let segStart = 0;
|
|
3189
|
+
const flushCode = (end) => { if (end > segStart) segments.push({ type: 'code', start: segStart, end }); };
|
|
3190
|
+
while (i < text.length) {
|
|
3191
|
+
const ch = text[i];
|
|
3192
|
+
if (ch === '"' && text[i + 1] === '"' && text[i + 2] === '"') {
|
|
3193
|
+
flushCode(i);
|
|
3194
|
+
const strStart = i;
|
|
3195
|
+
i += 3;
|
|
3196
|
+
while (i < text.length && !(text[i] === '"' && text[i + 1] === '"' && text[i + 2] === '"')) {
|
|
3197
|
+
i += text[i] === '\\' ? 2 : 1;
|
|
3198
|
+
}
|
|
3199
|
+
i = Math.min(i + 3, text.length);
|
|
3200
|
+
segments.push({ type: 'string', start: strStart, end: i, quoteLen: 3 });
|
|
3201
|
+
segStart = i;
|
|
3202
|
+
continue;
|
|
3203
|
+
}
|
|
3204
|
+
// Only `"` is recognized as a single-char string delimiter — NOT `'`.
|
|
3205
|
+
// The corpus is markdown prose, not code: apostrophes are routine English
|
|
3206
|
+
// contractions/possessives ("install's", "don't") and treating them as
|
|
3207
|
+
// string delimiters would swallow everything up to the next unrelated
|
|
3208
|
+
// apostrophe as "inside a string" (verified against the real corpus —
|
|
3209
|
+
// this was a real, disqualifying bug during development of this fix).
|
|
3210
|
+
// Every real call-argument value in the corpus uses `"`/`"""` only.
|
|
3211
|
+
if (ch === '"') {
|
|
3212
|
+
flushCode(i);
|
|
3213
|
+
const strStart = i;
|
|
3214
|
+
i += 1;
|
|
3215
|
+
while (i < text.length && text[i] !== '"') {
|
|
3216
|
+
i += text[i] === '\\' ? 2 : 1;
|
|
3217
|
+
}
|
|
3218
|
+
i = Math.min(i + 1, text.length);
|
|
3219
|
+
segments.push({ type: 'string', start: strStart, end: i, quoteLen: 1 });
|
|
3220
|
+
segStart = i;
|
|
3221
|
+
continue;
|
|
3222
|
+
}
|
|
3223
|
+
i += 1;
|
|
3224
|
+
}
|
|
3225
|
+
flushCode(text.length);
|
|
3226
|
+
return segments;
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
/**
|
|
3230
|
+
* Same-length mask of `text` with the INTERIOR of every string literal
|
|
3231
|
+
* replaced by a space (newlines preserved, so line-based regexes still work).
|
|
3232
|
+
* The delimiting quote character(s) themselves (`"`, `'`, `"""`) are kept
|
|
3233
|
+
* verbatim so a value-extraction regex like `key\s*[=:]\s*"[^"]*"` still
|
|
3234
|
+
* matches correctly against the mask — only the STRING CONTENT is blanked,
|
|
3235
|
+
* never the quote structure. Positions in the mask line up 1:1 with `text`,
|
|
3236
|
+
* so match indices/offsets found against the mask are valid offsets into the
|
|
3237
|
+
* original.
|
|
3238
|
+
*/
|
|
3239
|
+
function maskStringLiterals(text) {
|
|
3240
|
+
let mask = '';
|
|
3241
|
+
for (const seg of _segmentCodeAndStrings(text)) {
|
|
3242
|
+
const slice = text.slice(seg.start, seg.end);
|
|
3243
|
+
if (seg.type === 'code') { mask += slice; continue; }
|
|
3244
|
+
const q = seg.quoteLen;
|
|
3245
|
+
if (slice.length <= q) { mask += slice; continue; } // truncated/unterminated — keep verbatim
|
|
3246
|
+
const closeLen = Math.min(q, slice.length - q);
|
|
3247
|
+
const open = slice.slice(0, q);
|
|
3248
|
+
const close = slice.slice(slice.length - closeLen);
|
|
3249
|
+
const interiorLen = slice.length - q - closeLen;
|
|
3250
|
+
const interior = interiorLen > 0 ? slice.slice(q, q + interiorLen) : '';
|
|
3251
|
+
mask += open + interior.replace(/[^\n]/g, ' ') + close;
|
|
3252
|
+
}
|
|
3253
|
+
return mask;
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
/**
|
|
3257
|
+
* Locate every `<headWord>(` / `<headWord>({` call span in `text`.
|
|
3258
|
+
*
|
|
3259
|
+
* #2284 round-2 CRITICAL fix: this MUST NOT rely on whole-document quote
|
|
3260
|
+
* parity. A markdown workflow file mixes prose, ```bash code fences (full of
|
|
3261
|
+
* their own double-quoted strings), and shell quoting — there is no single
|
|
3262
|
+
* document-wide quote grammar, so a `"`-heavy bash `echo` upstream of a real
|
|
3263
|
+
* call (e.g. code-review.md's fenced `echo "..."` block before its
|
|
3264
|
+
* `Agent(subagent_type="gsd-code-reviewer", ...)` call) can desync a
|
|
3265
|
+
* CUMULATIVE quote-state scan, making the scanner believe the real call's
|
|
3266
|
+
* `Agent(` sits "inside a string" and silently skipping it entirely — the
|
|
3267
|
+
* call then survives completely unnormalized. (Reproduced and root-caused
|
|
3268
|
+
* against the real corpus.)
|
|
3269
|
+
*
|
|
3270
|
+
* Fixed shape: find each `<headWord>(` occurrence via a PLAIN literal-text
|
|
3271
|
+
* search (`indexOf`, immune to any prior document content), then run a
|
|
3272
|
+
* balanced paren-matching scan whose quote-tracking state STARTS FRESH AT
|
|
3273
|
+
* THE HEAD — local to this one call, never inherited from (or able to be
|
|
3274
|
+
* corrupted by) anything earlier in the document. Handles all three real
|
|
3275
|
+
* corpus shapes: multi-line one-key-per-line, single-line object-literal
|
|
3276
|
+
* (`Agent({ ... })`), and single-line compact
|
|
3277
|
+
* (`Agent(subagent_type="x", model="y", prompt="...")`) — including prompt
|
|
3278
|
+
* bodies containing their own unescaped `()`/`{}` (skipped via the SAME
|
|
3279
|
+
* span-local quote tracking, e.g. discuss-phase-assumptions.md's
|
|
3280
|
+
* `"""`-quoted parenthetical prose).
|
|
3281
|
+
*
|
|
3282
|
+
* Returns `[{start, end, hasBraceWrapper}]` — `start`/`end` bound the FULL
|
|
3283
|
+
* call INCLUDING the head word and the closing `)`/`})`.
|
|
3284
|
+
*/
|
|
3285
|
+
function findDispatchCallSpans(text, headWord) {
|
|
3286
|
+
const spans = [];
|
|
3287
|
+
const headToken = `${headWord}(`;
|
|
3288
|
+
let searchFrom = 0;
|
|
3289
|
+
for (;;) {
|
|
3290
|
+
const start = text.indexOf(headToken, searchFrom);
|
|
3291
|
+
if (start === -1) break;
|
|
3292
|
+
const prevChar = start > 0 ? text[start - 1] : '';
|
|
3293
|
+
if (/[A-Za-z0-9_]/.test(prevChar)) { searchFrom = start + 1; continue; } // word-boundary guard
|
|
3294
|
+
|
|
3295
|
+
let i = start + headToken.length; // just past the '('
|
|
3296
|
+
let j = i;
|
|
3297
|
+
while (j < text.length && /\s/.test(text[j])) j++;
|
|
3298
|
+
const hasBraceWrapper = text[j] === '{';
|
|
3299
|
+
|
|
3300
|
+
// LOCAL scan — quote/paren state is fresh here, never inherited from
|
|
3301
|
+
// anything before `start` in the document.
|
|
3302
|
+
let parenDepth = 1;
|
|
3303
|
+
let inString = null; // null | '"' | 'triple'
|
|
3304
|
+
let end = -1;
|
|
3305
|
+
for (; i < text.length; i++) {
|
|
3306
|
+
const ch = text[i];
|
|
3307
|
+
if (inString) {
|
|
3308
|
+
if (ch === '\\') { i++; continue; }
|
|
3309
|
+
if (inString === 'triple') {
|
|
3310
|
+
if (ch === '"' && text[i + 1] === '"' && text[i + 2] === '"') { inString = null; i += 2; }
|
|
3311
|
+
continue;
|
|
3312
|
+
}
|
|
3313
|
+
if (ch === inString) inString = null;
|
|
3314
|
+
continue;
|
|
3315
|
+
}
|
|
3316
|
+
if (ch === '"' && text[i + 1] === '"' && text[i + 2] === '"') { inString = 'triple'; i += 2; continue; }
|
|
3317
|
+
if (ch === '"') { inString = '"'; continue; }
|
|
3318
|
+
if (ch === '(') { parenDepth++; continue; }
|
|
3319
|
+
if (ch === ')') {
|
|
3320
|
+
parenDepth--;
|
|
3321
|
+
if (parenDepth === 0) { end = i + 1; break; }
|
|
3322
|
+
continue;
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
if (end === -1) { searchFrom = start + 1; continue; } // unterminated — skip past, keep scanning
|
|
3326
|
+
spans.push({ start, end, hasBraceWrapper });
|
|
3327
|
+
searchFrom = end;
|
|
3328
|
+
}
|
|
3329
|
+
return spans;
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
/**
|
|
3333
|
+
* Remove a call argument's `[matchStart, matchEnd)` token from `spanText`,
|
|
3334
|
+
* consuming its surrounding comma/whitespace so no dangling `, ,` or trailing
|
|
3335
|
+
* comment survives. When the argument owns its whole line, the whole line
|
|
3336
|
+
* (including a trailing inline `# comment`) is removed; `consumeLeadingComments`
|
|
3337
|
+
* additionally removes contiguous comment-only lines immediately ABOVE it —
|
|
3338
|
+
* #2284 Finding 5: explanatory prose describing a now-removed conditional
|
|
3339
|
+
* (e.g. execute-phase.md's "# Only include model= when ...") must not survive
|
|
3340
|
+
* describing a branch that no longer exists. Inline (single-line-compact /
|
|
3341
|
+
* object-literal) occurrences instead eat one adjacent comma.
|
|
3342
|
+
*/
|
|
3343
|
+
function _stripCallArgument(spanText, matchStart, matchEnd, { consumeLeadingComments = false } = {}) {
|
|
3344
|
+
let end = matchEnd;
|
|
3345
|
+
const afterRe = /^[ \t]*,?[ \t]*(#[^\n]*)?\r?\n?/;
|
|
3346
|
+
const afterMatch = afterRe.exec(spanText.slice(end));
|
|
3347
|
+
const hadTrailingComma = !!(afterMatch && /,/.test(afterMatch[0]));
|
|
3348
|
+
if (afterMatch) end += afterMatch[0].length;
|
|
3349
|
+
|
|
3350
|
+
let start = matchStart;
|
|
3351
|
+
const lineStart = spanText.lastIndexOf('\n', start - 1) + 1;
|
|
3352
|
+
const ownLine = /^[ \t]*$/.test(spanText.slice(lineStart, start));
|
|
3353
|
+
if (ownLine) {
|
|
3354
|
+
start = lineStart;
|
|
3355
|
+
if (consumeLeadingComments) {
|
|
3356
|
+
for (;;) {
|
|
3357
|
+
const prevLineStart = start > 0 ? spanText.lastIndexOf('\n', start - 2) + 1 : 0;
|
|
3358
|
+
const prevLine = spanText.slice(prevLineStart, start);
|
|
3359
|
+
if (/^[ \t]*#[^\n]*\r?\n$/.test(prevLine)) {
|
|
3360
|
+
start = prevLineStart;
|
|
3361
|
+
if (prevLineStart === 0) break;
|
|
3362
|
+
} else break;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
} else if (!hadTrailingComma) {
|
|
3366
|
+
// Inline form and this was the LAST arg (no trailing comma) — eat a
|
|
3367
|
+
// leading comma so the previous arg doesn't dangle one.
|
|
3368
|
+
const before = spanText.slice(0, start);
|
|
3369
|
+
const cm = /,[ \t]*$/.exec(before);
|
|
3370
|
+
if (cm) start -= cm[0].length;
|
|
3371
|
+
}
|
|
3372
|
+
return spanText.slice(0, start) + spanText.slice(end);
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
/**
|
|
3376
|
+
* Replace a named-role argument token's `[matchStart, matchEnd)` span
|
|
3377
|
+
* (`subagent_type=`/`subagent_type:` + its value) with the projected
|
|
3378
|
+
* `gsd_role=` / role-prompt-resolution / structural-role argument group.
|
|
3379
|
+
* Preserves the pretty multi-line one-arg-per-line style when the original
|
|
3380
|
+
* token owned its own line; falls back to an inline, comma-joined group for
|
|
3381
|
+
* the single-line-compact and object-literal forms.
|
|
3382
|
+
*/
|
|
3383
|
+
function _projectRoleArgument(spanText, matchStart, matchEnd, roleValueExpr, toolConfig, canOrchestrate) {
|
|
3384
|
+
const { namedRoleParam, promptContentParam, structuralRoleParam, leafRoleValue } = toolConfig;
|
|
3385
|
+
const lineStart = spanText.lastIndexOf('\n', matchStart - 1) + 1;
|
|
3386
|
+
const startsOwnLine = /^[ \t]*$/.test(spanText.slice(lineStart, matchStart));
|
|
3387
|
+
|
|
3388
|
+
// Consume an immediately-following separator comma (+ same-line whitespace/
|
|
3389
|
+
// newline) into `end` — never leave it dangling AFTER an injected trailing
|
|
3390
|
+
// `# comment` (a bare `,` after `#...` would sit on the comment's own line,
|
|
3391
|
+
// outside any real argument list).
|
|
3392
|
+
let end = matchEnd;
|
|
3393
|
+
const afterRe = /^[ \t]*,[ \t]*\r?\n?/;
|
|
3394
|
+
const afterMatch = afterRe.exec(spanText.slice(end));
|
|
3395
|
+
const hadTrailingComma = !!afterMatch;
|
|
3396
|
+
if (afterMatch) end += afterMatch[0].length;
|
|
3397
|
+
const ownLine = startsOwnLine && hadTrailingComma && /\n$/.test(afterMatch[0]);
|
|
3398
|
+
|
|
3399
|
+
const promptContentPhrase =
|
|
3400
|
+
`${promptContentParam}=<resolve ${roleValueExpr} against the active install's gsd-* role prompts and load ` +
|
|
3401
|
+
'its contents; FAIL CLOSED with an explicit error if unresolved — never execute the role inline>';
|
|
3402
|
+
|
|
3403
|
+
let replacement;
|
|
3404
|
+
if (ownLine) {
|
|
3405
|
+
const indent = spanText.slice(lineStart, matchStart);
|
|
3406
|
+
const depthNote = canOrchestrate ? '' : ' # nested delegation is unavailable at this dispatch depth/toolkit';
|
|
3407
|
+
replacement =
|
|
3408
|
+
`${namedRoleParam}=${roleValueExpr},\n` +
|
|
3409
|
+
`${indent}${promptContentPhrase},\n` +
|
|
3410
|
+
`${indent}${structuralRoleParam}="${leafRoleValue}",${depthNote}\n`;
|
|
3411
|
+
} else {
|
|
3412
|
+
// Inline forms never carry a trailing `#` comment mid-argument-list (it
|
|
3413
|
+
// would silently "comment out" the remainder of the call), so the
|
|
3414
|
+
// depth/toolkit caveat is only ever emitted in the pretty own-line form.
|
|
3415
|
+
// Re-emit exactly the separator that originally followed this argument
|
|
3416
|
+
// (a comma if more args follow; nothing if it was the last one).
|
|
3417
|
+
replacement =
|
|
3418
|
+
`${namedRoleParam}=${roleValueExpr}, ${promptContentPhrase}, ${structuralRoleParam}="${leafRoleValue}"` +
|
|
3419
|
+
(hadTrailingComma ? ', ' : '');
|
|
3420
|
+
}
|
|
3421
|
+
return spanText.slice(0, matchStart) + replacement + spanText.slice(end);
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
// Matches a `subagent_type`/`model` argument's key+delimiter+value across all
|
|
3425
|
+
// three corpus forms: quoted-string values ("gsd-planner", "{model}") and
|
|
3426
|
+
// bare dynamic-expression values (ref.agent, research_hook.ref.agent,
|
|
3427
|
+
// executor_model). The captured group is always the value (a suffix of the
|
|
3428
|
+
// whole match), so its start offset is `match.index + match[0].length -
|
|
3429
|
+
// match[1].length` — avoids needing the regex `d` (indices) flag.
|
|
3430
|
+
function _callArgValueRe(key) {
|
|
3431
|
+
return new RegExp(`\\b${key}\\s*[=:]\\s*("(?:[^"\\\\]|\\\\.)*"|[A-Za-z_][\\w.]*)`);
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
/**
|
|
3435
|
+
* Returns the literal role name from a captured role-argument value EXPR
|
|
3436
|
+
* (e.g. `"gsd-planner"`) — or `null` when it is not a genuine static
|
|
3437
|
+
* literal: a bare dynamic expression (`ref.agent`), OR a quoted value that
|
|
3438
|
+
* still contains `{...}` template interpolation (the corpus's own
|
|
3439
|
+
* placeholder convention, e.g. `model="{researcher_model}"` — and,
|
|
3440
|
+
* critically, `subagent_type: "gsd-{agent}"` in
|
|
3441
|
+
* gsd-core/references/universal-anti-patterns.md, a DOCUMENTATION template
|
|
3442
|
+
* illustrating the naming pattern, never a concrete role to resolve).
|
|
3443
|
+
* Fail-closed validation only ever runs on a genuine static literal; a
|
|
3444
|
+
* template/dynamic value still gets the full role-prompt-resolution
|
|
3445
|
+
* projection treatment (the resolve+fail-closed instruction applies equally
|
|
3446
|
+
* once a template is substituted at runtime) — only the STATIC CHECK is
|
|
3447
|
+
* skipped, never the projection itself.
|
|
3448
|
+
*/
|
|
3449
|
+
function _literalRoleValue(roleValueExpr) {
|
|
3450
|
+
const m = /^"([^"]*)"$/.exec(roleValueExpr);
|
|
3451
|
+
if (!m) return null;
|
|
3452
|
+
if (/[{}]/.test(m[1])) return null;
|
|
3453
|
+
return m[1];
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
/**
|
|
3457
|
+
* `maskStringLiterals` PLUS `#`-to-end-of-line comment blanking (comments are
|
|
3458
|
+
* never string literals, so they survive string-masking as literal `#...`
|
|
3459
|
+
* text). Header-token searches (subagent_type/model/run_in_background) must
|
|
3460
|
+
* use THIS mask, not the string-only one — verified necessary against the
|
|
3461
|
+
* real corpus: execute-phase.md's explanatory comment "# Only include
|
|
3462
|
+
* model= when executor_model is..." literally contains the substring
|
|
3463
|
+
* "model= when", which a comment-blind `model` regex mismatches as a real
|
|
3464
|
+
* `model=when` argument, corrupting the comment AND missing the real
|
|
3465
|
+
* `model="{executor_model}"` line beneath it. Scoped to call-span text only
|
|
3466
|
+
* (never the whole document), so markdown `#`/`##` headings elsewhere are
|
|
3467
|
+
* unaffected.
|
|
3468
|
+
*/
|
|
3469
|
+
function _maskStringsAndComments(text) {
|
|
3470
|
+
return maskStringLiterals(text).replace(/#[^\n]*/g, (m) => ' '.repeat(m.length));
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
/**
|
|
3474
|
+
* Normalize ONE `Agent(...)`/`Agent({...})` call span (already isolated by
|
|
3475
|
+
* `findDispatchCallSpans`) onto the target's real dispatch primitive. Every
|
|
3476
|
+
* behavioral branch reads `dispatch` (the runtime's sourced
|
|
3477
|
+
* `hostIntegration.dispatch` facts) — none is hardcoded. Handles all three
|
|
3478
|
+
* corpus call-argument shapes uniformly via string-aware token location
|
|
3479
|
+
* (`maskStringLiterals` recomputed after each structural edit, since prior
|
|
3480
|
+
* edits shift offsets).
|
|
3481
|
+
*/
|
|
3482
|
+
function _normalizeDispatchCallSpan(spanText, hasBraceWrapper, dispatch, toolConfig) {
|
|
3483
|
+
const namedDispatch = dispatch.namedDispatch === true;
|
|
3484
|
+
const backgroundCapable = dispatch.background === true;
|
|
3485
|
+
const canOrchestrate = dispatch.subagentToolkit === 'full'
|
|
3486
|
+
&& (dispatch.maxDepth === -1 || (typeof dispatch.maxDepth === 'number' && dispatch.maxDepth > 1));
|
|
3487
|
+
const { toolName, backgroundParam, supportsPerCallModel, availableRoles, runtime } = toolConfig;
|
|
3488
|
+
|
|
3489
|
+
let text = spanText;
|
|
3490
|
+
|
|
3491
|
+
// 1. Named-role argument (subagent_type= / subagent_type:) — only when the
|
|
3492
|
+
// target has no native named-agent lookup (dispatch.namedDispatch).
|
|
3493
|
+
// Fail-closed validation runs on the extracted value REGARDLESS of
|
|
3494
|
+
// which source syntax produced it (#2284 requirement 2).
|
|
3495
|
+
if (!namedDispatch) {
|
|
3496
|
+
const roleRe = _callArgValueRe('subagent_type');
|
|
3497
|
+
const rm = roleRe.exec(_maskStringsAndComments(text));
|
|
3498
|
+
if (rm) {
|
|
3499
|
+
// Read the VALUE from the original (unmasked) text at the matched
|
|
3500
|
+
// offset — `rm[1]` was captured against the mask, whose string
|
|
3501
|
+
// INTERIOR is blanked, so it must never be used as the real value.
|
|
3502
|
+
const roleValueExpr = text.slice(rm.index + rm[0].length - rm[1].length, rm.index + rm[0].length);
|
|
3503
|
+
const literalRole = _literalRoleValue(roleValueExpr);
|
|
3504
|
+
if (literalRole !== null) {
|
|
3505
|
+
_assertRoleResolvable(literalRole, availableRoles, runtime, 'subagent_type');
|
|
3506
|
+
} else if (!availableRoles) {
|
|
3507
|
+
// No literal value to check, but a null availableRoles still means
|
|
3508
|
+
// the shipped agents/ dir couldn't be resolved at all — fail closed
|
|
3509
|
+
// unconditionally rather than silently install an unverifiable call.
|
|
3510
|
+
_assertRoleResolvable('', availableRoles, runtime, 'subagent_type');
|
|
3511
|
+
}
|
|
3512
|
+
text = _projectRoleArgument(text, rm.index, rm.index + rm[0].length, roleValueExpr, toolConfig, canOrchestrate);
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
// 2. Per-call model argument (model= / model:) — stripped entirely when the
|
|
3517
|
+
// target has no per-call model-selection parameter (there is no
|
|
3518
|
+
// `dispatch` axis for this — it is inherent tool vocabulary, like the
|
|
3519
|
+
// parameter names themselves). Also removes now-dead explanatory
|
|
3520
|
+
// comment lines directly above a `model=` line that owns its own line
|
|
3521
|
+
// (#2284 Finding 5).
|
|
3522
|
+
if (!supportsPerCallModel) {
|
|
3523
|
+
const modelRe = _callArgValueRe('model');
|
|
3524
|
+
const mm = modelRe.exec(_maskStringsAndComments(text));
|
|
3525
|
+
if (mm) {
|
|
3526
|
+
text = _stripCallArgument(text, mm.index, mm.index + mm[0].length, { consumeLeadingComments: true });
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
// 3. Background-dispatch flag (run_in_background= / run_in_background:) —
|
|
3531
|
+
// maps onto the target's own background parameter ONLY when documented
|
|
3532
|
+
// to support it; otherwise stripped rather than forwarding a parameter
|
|
3533
|
+
// the primitive doesn't accept.
|
|
3534
|
+
{
|
|
3535
|
+
const bgRe = /\brun_in_background\s*[=:]\s*(?:true|false)/;
|
|
3536
|
+
const bm = bgRe.exec(_maskStringsAndComments(text));
|
|
3537
|
+
if (bm) {
|
|
3538
|
+
if (backgroundCapable) {
|
|
3539
|
+
const matched = text.slice(bm.index, bm.index + bm[0].length);
|
|
3540
|
+
const replaced = matched.replace(/^run_in_background(\s*[=:]\s*)/, `${backgroundParam}$1`);
|
|
3541
|
+
text = text.slice(0, bm.index) + replaced + text.slice(bm.index + bm[0].length);
|
|
3542
|
+
} else {
|
|
3543
|
+
text = _stripCallArgument(text, bm.index, bm.index + bm[0].length);
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
// 4. Call-syntax head rename + object-literal brace stripping. Hermes's
|
|
3549
|
+
// delegate_task is a flat kwarg call — `Agent({...})`'s wrapper braces
|
|
3550
|
+
// are dropped rather than carried through, so every projected call ends
|
|
3551
|
+
// up in the same flat shape regardless of source syntax.
|
|
3552
|
+
text = text.replace(/^Agent\(/, `${toolName}(`);
|
|
3553
|
+
if (hasBraceWrapper) {
|
|
3554
|
+
const openMask = maskStringLiterals(text);
|
|
3555
|
+
const braceOpenIdx = openMask.indexOf('{');
|
|
3556
|
+
if (braceOpenIdx !== -1) text = text.slice(0, braceOpenIdx) + text.slice(braceOpenIdx + 1);
|
|
3557
|
+
const closeMask = maskStringLiterals(text);
|
|
3558
|
+
const braceCloseIdx = closeMask.lastIndexOf('}');
|
|
3559
|
+
if (braceCloseIdx !== -1) text = text.slice(0, braceCloseIdx) + text.slice(braceCloseIdx + 1);
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
return text;
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
/**
|
|
3566
|
+
* Blank the interior (and delimiters) of every string literal inside
|
|
3567
|
+
* `spanText` to spaces — same length, newlines preserved — using a fresh,
|
|
3568
|
+
* LOCAL quote-tracking scan that starts at `spanText[0]` with NO inherited
|
|
3569
|
+
* state. This is deliberately the SAME state-machine shape as the
|
|
3570
|
+
* `inString`/`\\`/triple-quote handling inside `findDispatchCallSpans`
|
|
3571
|
+
* (double-quoted and `"""`-triple-quoted, backslash-escape aware) — reused
|
|
3572
|
+
* here so a call span's quoted argument VALUES (documentation prose, prompt
|
|
3573
|
+
* bodies) never masquerade as real call syntax, without EVER falling back to
|
|
3574
|
+
* a whole-document cumulative quote-parity mask (the round-2 defect
|
|
3575
|
+
* documented on `findDispatchCallSpans` above).
|
|
3576
|
+
*/
|
|
3577
|
+
function _blankStringLiteralInteriors(spanText) {
|
|
3578
|
+
let out = '';
|
|
3579
|
+
let inString = null; // null | '"' | 'triple'
|
|
3580
|
+
for (let i = 0; i < spanText.length; i++) {
|
|
3581
|
+
const ch = spanText[i];
|
|
3582
|
+
if (inString) {
|
|
3583
|
+
if (ch === '\\') {
|
|
3584
|
+
out += ' ';
|
|
3585
|
+
i++;
|
|
3586
|
+
if (i < spanText.length) out += (spanText[i] === '\n') ? '\n' : ' ';
|
|
3587
|
+
continue;
|
|
3588
|
+
}
|
|
3589
|
+
if (inString === 'triple') {
|
|
3590
|
+
if (ch === '"' && spanText[i + 1] === '"' && spanText[i + 2] === '"') {
|
|
3591
|
+
inString = null;
|
|
3592
|
+
out += ' ';
|
|
3593
|
+
i += 2;
|
|
3594
|
+
continue;
|
|
3595
|
+
}
|
|
3596
|
+
out += (ch === '\n') ? '\n' : ' ';
|
|
3597
|
+
continue;
|
|
3598
|
+
}
|
|
3599
|
+
if (ch === inString) { inString = null; out += ' '; continue; }
|
|
3600
|
+
out += (ch === '\n') ? '\n' : ' ';
|
|
3601
|
+
continue;
|
|
3602
|
+
}
|
|
3603
|
+
if (ch === '"' && spanText[i + 1] === '"' && spanText[i + 2] === '"') {
|
|
3604
|
+
inString = 'triple';
|
|
3605
|
+
out += ' ';
|
|
3606
|
+
i += 2;
|
|
3607
|
+
continue;
|
|
3608
|
+
}
|
|
3609
|
+
if (ch === '"') { inString = '"'; out += ' '; continue; }
|
|
3610
|
+
out += ch;
|
|
3611
|
+
}
|
|
3612
|
+
return out;
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
/**
|
|
3616
|
+
* Quote-aware view of `content` for the completeness checks below: for every
|
|
3617
|
+
* REAL call span located via `findDispatchCallSpans` (once per head word in
|
|
3618
|
+
* `headWords`), the string-literal ARGUMENT VALUES inside that span are
|
|
3619
|
+
* blanked via `_blankStringLiteralInteriors`; the call's own head word and
|
|
3620
|
+
* bare (unquoted) argument tokens are left untouched. `headWords` is
|
|
3621
|
+
* processed in order and each pass re-scans the PROGRESSIVELY-masked string
|
|
3622
|
+
* — `toolName` first, then `'Agent'` — so a spurious `Agent(` that
|
|
3623
|
+
* `findDispatchCallSpans('Agent')` would otherwise "find" purely because it
|
|
3624
|
+
* sits inside an outer call's quoted string (e.g. a `description="...Agent()
|
|
3625
|
+
* ...subagent_type=x"` argument value) has ALREADY been blanked away by the
|
|
3626
|
+
* outer `toolName` pass by the time the `'Agent'` pass runs, so it is never
|
|
3627
|
+
* mistaken for a real, independent call. A genuinely real (unquoted) `Agent(`
|
|
3628
|
+
* — including one nested as a raw, un-renamed argument value — survives every
|
|
3629
|
+
* pass and remains visible to the caller's regex checks.
|
|
3630
|
+
*/
|
|
3631
|
+
function _maskQuotedRegionsWithinCallSpans(content, headWords) {
|
|
3632
|
+
let masked = content;
|
|
3633
|
+
for (const headWord of headWords) {
|
|
3634
|
+
const spans = findDispatchCallSpans(masked, headWord);
|
|
3635
|
+
for (let i = spans.length - 1; i >= 0; i--) {
|
|
3636
|
+
const { start, end } = spans[i];
|
|
3637
|
+
const maskedSpan = _blankStringLiteralInteriors(masked.slice(start, end));
|
|
3638
|
+
masked = masked.slice(0, start) + maskedSpan + masked.slice(end);
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
return masked;
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
/**
|
|
3645
|
+
* Post-projection guard (#2284 requirement 3 — belt-and-suspenders): after
|
|
3646
|
+
* projection, assert the corpus form the projection could not anticipate
|
|
3647
|
+
* never silently ships. Throws an explicit install error (fail-LOUD) rather
|
|
3648
|
+
* than let an unprojected/incompletely-projected dispatch call install.
|
|
3649
|
+
*
|
|
3650
|
+
* #2284 round-2 CRITICAL fix: this is an INDEPENDENT check — it does NOT use
|
|
3651
|
+
* `maskStringLiterals` over the whole document (the round-1 primitive whose
|
|
3652
|
+
* cumulative, document-wide quote-parity tracking was the root cause of the
|
|
3653
|
+
* round-2 defect: a `"`-heavy bash fence upstream of a real call desynced
|
|
3654
|
+
* quote state and made `findDispatchCallSpans` blind to that call, shipping
|
|
3655
|
+
* a Frankenstein `Agent(gsd_role="...", model="...")` with no detection).
|
|
3656
|
+
*
|
|
3657
|
+
* #2284 round-3 fix: a BLUNT, mask-free literal check over the whole
|
|
3658
|
+
* document (round-2's fix) over-throws — it cannot tell a real residual
|
|
3659
|
+
* `Agent(`/`subagent_type` call from the SAME text appearing INSIDE a quoted
|
|
3660
|
+
* string (documentation/prompt prose, e.g. `description="...Agent()..."`).
|
|
3661
|
+
* The completeness checks (residual `subagent_type` / literal `Agent(`) now
|
|
3662
|
+
* run against `_maskQuotedRegionsWithinCallSpans` — quote-aware, but scoped
|
|
3663
|
+
* strictly to already-correctly-bounded, per-occurrence-LOCAL call spans
|
|
3664
|
+
* (never a whole-document cumulative mask), so a real Frankenstein call
|
|
3665
|
+
* (unquoted, real call syntax) still fires while a same-text mention genuinely
|
|
3666
|
+
* inside a quoted string does not.
|
|
3667
|
+
*
|
|
3668
|
+
* The completeness checks also only apply when `namedDispatch` is false: when
|
|
3669
|
+
* `dispatch.namedDispatch === true`, `_normalizeDispatchCallSpan` step 1
|
|
3670
|
+
* INTENTIONALLY leaves `subagent_type` unprojected (the target primitive
|
|
3671
|
+
* resolves named agents itself) — a residual `subagent_type` in that case is
|
|
3672
|
+
* the correct, intended output, not a defect. (The call HEAD is still renamed
|
|
3673
|
+
* unconditionally regardless of `namedDispatch` — see step 4 there — so a
|
|
3674
|
+
* literal `Agent(` residual is gated the same way purely for symmetry with
|
|
3675
|
+
* the dispatch-facts-driven contract; it is never actually left unrenamed by
|
|
3676
|
+
* the projection in practice.)
|
|
3677
|
+
*
|
|
3678
|
+
* The model-leak check is unaffected by either fix above — it is orthogonal
|
|
3679
|
+
* to `namedDispatch` (gated only by `supportsPerCallModel`) and already
|
|
3680
|
+
* bounds each real call via the independently-fixed, per-occurrence-local,
|
|
3681
|
+
* non-cumulative `findDispatchCallSpans`, then does a raw substring check
|
|
3682
|
+
* within that bound.
|
|
3683
|
+
*/
|
|
3684
|
+
function _assertProjectionComplete(content, toolConfig, namedDispatch = false) {
|
|
3685
|
+
const { toolName, runtime, supportsPerCallModel } = toolConfig;
|
|
3686
|
+
|
|
3687
|
+
if (!namedDispatch) {
|
|
3688
|
+
const quoteAware = _maskQuotedRegionsWithinCallSpans(content, [toolName, 'Agent']);
|
|
3689
|
+
if (/\bsubagent_type\s*[=:]/.test(quoteAware)) {
|
|
3690
|
+
throw new Error(
|
|
3691
|
+
`${runtime} workflow install: projection left a residual subagent_type reference — refusing to install ` +
|
|
3692
|
+
'(fail-closed post-projection guard, #2284)',
|
|
3693
|
+
);
|
|
3694
|
+
}
|
|
3695
|
+
if (/\bAgent\(/.test(quoteAware)) {
|
|
3696
|
+
throw new Error(
|
|
3697
|
+
`${runtime} workflow install: projection left literal Agent( call syntax — refusing to install ` +
|
|
3698
|
+
'(fail-closed post-projection guard, #2284)',
|
|
3699
|
+
);
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
if (!supportsPerCallModel) {
|
|
3704
|
+
for (const span of findDispatchCallSpans(content, toolName)) {
|
|
3705
|
+
const rawSpanText = content.slice(span.start, span.end);
|
|
3706
|
+
if (/\bmodel\s*[=:]/.test(rawSpanText)) {
|
|
3707
|
+
throw new Error(
|
|
3708
|
+
`${runtime} workflow install: projection left a leaked model= argument inside a ${toolName}(...) call ` +
|
|
3709
|
+
'— refusing to install (fail-closed post-projection guard, #2284)',
|
|
3710
|
+
);
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
/**
|
|
3717
|
+
* Project host-neutral `Agent(...)` named-subagent dispatch prose onto a
|
|
3718
|
+
* target runtime's real dispatch primitive. See the file-header comment above
|
|
3719
|
+
* for the governing rule: every behavioral branch reads `dispatch` (the
|
|
3720
|
+
* runtime's sourced `hostIntegration.dispatch` facts) — none is a hardcoded
|
|
3721
|
+
* assumption about a specific runtime. Handles all three real corpus call
|
|
3722
|
+
* forms (multi-line one-key-per-line, single-line object-literal, single-line
|
|
3723
|
+
* compact) via string-aware call-span detection rather than three independent
|
|
3724
|
+
* line-anchored regexes, and closes with a post-projection guard that fails
|
|
3725
|
+
* loud on any form it did not anticipate (#2284).
|
|
3726
|
+
*
|
|
3727
|
+
* @param {string} content
|
|
3728
|
+
* @param {{namedDispatch?: boolean, nested?: boolean, maxDepth?: number, background?: boolean, backgroundDispatch?: boolean, subagentToolkit?: string}} dispatch
|
|
3729
|
+
* @param {{toolName: string, namedRoleParam: string, promptContentParam: string, structuralRoleParam: string, leafRoleValue: string, backgroundParam: string, supportsPerCallModel: boolean, availableRoles: Set<string>|null, runtime: string}} toolConfig
|
|
3730
|
+
*/
|
|
3731
|
+
function projectNamedDispatchToStructuralDelegate(content, dispatch, toolConfig) {
|
|
3732
|
+
const d = dispatch || {};
|
|
3733
|
+
const namedDispatch = d.namedDispatch === true;
|
|
3734
|
+
const backgroundCapable = d.background === true;
|
|
3735
|
+
const { toolName, promptContentParam } = toolConfig;
|
|
3736
|
+
|
|
3737
|
+
let converted = content;
|
|
3738
|
+
|
|
3739
|
+
// 1. The "Agent tool IS available" contract assertion (currently unique to
|
|
3740
|
+
// plan-phase.md, matched generically in case of future reuse elsewhere).
|
|
3741
|
+
const assertionRe = /The Agent tool IS available in a top-level ([^\n]+?) session\.\s+Always spawn\s+([\s\S]*?)\s+as separate Agent\(\) calls\./;
|
|
3742
|
+
converted = converted.replace(assertionRe, (_m, sessionName, roster) => {
|
|
3743
|
+
const rosterFlat = roster.replace(/\s+/g, ' ').trim();
|
|
3744
|
+
if (namedDispatch) {
|
|
3745
|
+
return `The \`${toolName}\` tool IS available in a top-level ${sessionName} session. Always dispatch ${rosterFlat} as separate \`${toolName}()\` calls.`;
|
|
3746
|
+
}
|
|
3747
|
+
return (
|
|
3748
|
+
`${sessionName} has no \`Agent\` tool. It exposes \`${toolName}\`, which dispatches by structural role — ` +
|
|
3749
|
+
'it has no concept of a named subagent identity. GSD projects each named gsd-* role onto this primitive ' +
|
|
3750
|
+
`itself: resolve the role's prompt file from the active install, load its contents, and embed them in the ` +
|
|
3751
|
+
`\`${toolName}\` payload via \`${promptContentParam}\` as the dispatched task's operating instructions. ` +
|
|
3752
|
+
'FAIL CLOSED — surface an explicit error and stop — if a referenced role prompt cannot be resolved; never ' +
|
|
3753
|
+
`execute the role inline as a substitute. In a top-level ${sessionName} session, always dispatch ` +
|
|
3754
|
+
`${rosterFlat} as separate \`${toolName}\` calls.`
|
|
3755
|
+
);
|
|
3756
|
+
});
|
|
3757
|
+
|
|
3758
|
+
// 1b. Dispatch-depth-availability prose immediately adjacent to a renamed
|
|
3759
|
+
// `Agent()` mention in the SAME sentence (plan-review-convergence.md
|
|
3760
|
+
// ~lines 108, 347, 355) — a bare "Agent" left un-renamed right next to
|
|
3761
|
+
// the projection's own `Agent()`→`${toolName}()` rename produced
|
|
3762
|
+
// self-contradictory installed text (e.g. "...delegate_task(...)...
|
|
3763
|
+
// with Agent available..."). Narrowly scoped to the EXACT known
|
|
3764
|
+
// phrases the projection itself creates the inconsistency beside —
|
|
3765
|
+
// never a broad bare-word `Agent` rename, which would corrupt
|
|
3766
|
+
// legitimate `Agent`-adjacent prose elsewhere in the corpus (role
|
|
3767
|
+
// names, "Agent Brief", agent-file references).
|
|
3768
|
+
converted = converted.replace(
|
|
3769
|
+
/\borchestrator runs at depth 0 with Agent available\b/g,
|
|
3770
|
+
`orchestrator runs at depth 0 with ${toolName} available`,
|
|
3771
|
+
);
|
|
3772
|
+
converted = converted.replace(
|
|
3773
|
+
/\(bug #936: depth-1 Agent has no Agent tool\)/g,
|
|
3774
|
+
`(bug #936: depth-1 ${toolName} has no nested ${toolName})`,
|
|
3775
|
+
);
|
|
3776
|
+
|
|
3777
|
+
// 2. Per-call model-selection prose ("Model resolution:" paragraph,
|
|
3778
|
+
// execute-phase.md) + inline backtick-quoted model-mention prose
|
|
3779
|
+
// examples (not live call sites) — only rewritten when the target
|
|
3780
|
+
// primitive has no per-call model parameter at all.
|
|
3781
|
+
if (!toolConfig.supportsPerCallModel) {
|
|
3782
|
+
const modelResolutionRe = /\*\*Model resolution:\*\* If `executor_model` is `"inherit"`, omit the `model=` parameter from all `Agent\(\)` calls — do NOT pass `model="inherit"` to Agent\. Omitting the `model=` parameter causes [^.]+\. Only set `model=` when `executor_model` is an explicit model name \(e\.g\., `"claude-sonnet-5"`, `"claude-opus-4-8"`\)\./;
|
|
3783
|
+
converted = converted.replace(
|
|
3784
|
+
modelResolutionRe,
|
|
3785
|
+
`**Model resolution:** \`${toolName}\` has no per-call model-selection parameter — every dispatched role ` +
|
|
3786
|
+
`always inherits the host session's active model. Never pass \`model=\` to \`${toolName}\`; drop the ` +
|
|
3787
|
+
'`executor_model` value entirely for this runtime.',
|
|
3788
|
+
);
|
|
3789
|
+
converted = converted.replace(/`model="[^"`\n]*"`,?\s*(?:and\s+)?/g, '');
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
// 3. Background-dispatch PROSE mentions outside any real call span (e.g.
|
|
3793
|
+
// execute-phase.md:595,600 — `run_in_background: true` inline
|
|
3794
|
+
// documentation, not a call argument) — #2284 Finding 3. Only rewritten
|
|
3795
|
+
// when the target is documented to support background dispatch (a
|
|
3796
|
+
// prose mention of an unsupported capability would be equally
|
|
3797
|
+
// misleading as a real leaked argument).
|
|
3798
|
+
if (backgroundCapable) {
|
|
3799
|
+
converted = converted.replace(
|
|
3800
|
+
/\brun_in_background(\s*[=:]\s*(?:true|false))/g,
|
|
3801
|
+
`${toolConfig.backgroundParam}$1`,
|
|
3802
|
+
);
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
// 4. Call-span-based normalization — the core of the fix. Every
|
|
3806
|
+
// `Agent(...)`/`Agent({...})` occurrence (all three corpus forms) is
|
|
3807
|
+
// located via string-aware balanced paren/brace matching, then
|
|
3808
|
+
// normalized as a unit; spans are rebuilt right-to-left so earlier
|
|
3809
|
+
// offsets stay valid while later ones are rewritten.
|
|
3810
|
+
const spans = findDispatchCallSpans(converted, 'Agent');
|
|
3811
|
+
for (let i = spans.length - 1; i >= 0; i--) {
|
|
3812
|
+
const { start, end, hasBraceWrapper } = spans[i];
|
|
3813
|
+
const rebuilt = _normalizeDispatchCallSpan(converted.slice(start, end), hasBraceWrapper, d, toolConfig);
|
|
3814
|
+
converted = converted.slice(0, start) + rebuilt + converted.slice(end);
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
// 5. "Agent tool" capability mentions (conditions gating parallel vs.
|
|
3818
|
+
// sequential dispatch, e.g. map-codebase.md) → the real target primitive
|
|
3819
|
+
// name, which resolves these conditions accurately since it IS a real,
|
|
3820
|
+
// always-available dispatch primitive for this target.
|
|
3821
|
+
converted = converted.replace(/\bAgent tool\b/g, toolName);
|
|
3822
|
+
|
|
3823
|
+
// 5b. Catch-all: a `subagent_type` mention that is NOT part of any real
|
|
3824
|
+
// `Agent(...)` call span (e.g. map-codebase.md's inline documentation
|
|
3825
|
+
// prose ``Use Agent tool with `subagent_type="X"`, ...`` — disconnected
|
|
3826
|
+
// example syntax, not a live call). Renamed for the same accuracy the
|
|
3827
|
+
// real calls get; a literal quoted role value is still fail-closed
|
|
3828
|
+
// validated even though there is no call structure to inject
|
|
3829
|
+
// role-prompt/fail-closed guidance INTO.
|
|
3830
|
+
if (!namedDispatch) {
|
|
3831
|
+
converted = converted.replace(
|
|
3832
|
+
/\bsubagent_type(\s*[=:]\s*"[^"]*")/g,
|
|
3833
|
+
(_m, rest) => {
|
|
3834
|
+
const literalRole = _literalRoleValue(rest.replace(/^\s*[=:]\s*/, ''));
|
|
3835
|
+
if (literalRole !== null) {
|
|
3836
|
+
_assertRoleResolvable(literalRole, toolConfig.availableRoles, toolConfig.runtime, 'subagent_type (prose mention)');
|
|
3837
|
+
}
|
|
3838
|
+
return `${toolConfig.namedRoleParam}${rest}`;
|
|
3839
|
+
},
|
|
3840
|
+
);
|
|
3841
|
+
converted = converted.replace(/\bsubagent_type(\s*[=:])/g, `${toolConfig.namedRoleParam}$1`);
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3844
|
+
// 5c. Safety net (#2284 requirement 2): the PRIMARY mechanism for
|
|
3845
|
+
// eliminating literal `Agent(` syntax is complete span detection (step
|
|
3846
|
+
// 4) — this unconditional final rename exists only so that even a call
|
|
3847
|
+
// span detection somehow misses at least loses its `Agent(` head
|
|
3848
|
+
// rather than shipping the literal Claude-shaped tool name verbatim.
|
|
3849
|
+
// A call caught only by this safety net is still INCOMPLETELY
|
|
3850
|
+
// normalized (no role/model handling) and gets caught by the
|
|
3851
|
+
// independent post-projection guard below via its OTHER invariants
|
|
3852
|
+
// (residual subagent_type / leaked model=), which this safety net does
|
|
3853
|
+
// not touch — the install still fails closed for a missed span.
|
|
3854
|
+
converted = converted.replace(/\bAgent\(/g, `${toolName}(`);
|
|
3855
|
+
|
|
3856
|
+
// 6. Post-projection guard (#2284 requirement 3): fail loud, never ship
|
|
3857
|
+
// silently, on any residual/leaked form the projection above did not
|
|
3858
|
+
// anticipate. `namedDispatch` gates the completeness checks — a
|
|
3859
|
+
// residual subagent_type is INTENTIONAL, not a defect, when the target
|
|
3860
|
+
// resolves named agents itself (see `_assertProjectionComplete`).
|
|
3861
|
+
_assertProjectionComplete(converted, toolConfig, namedDispatch);
|
|
3862
|
+
|
|
3863
|
+
return converted;
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
const HERMES_DISPATCH_TOOL_CONFIG = Object.freeze({
|
|
3867
|
+
toolName: 'delegate_task',
|
|
3868
|
+
namedRoleParam: 'gsd_role',
|
|
3869
|
+
promptContentParam: 'gsd_role_prompt',
|
|
3870
|
+
structuralRoleParam: 'role',
|
|
3871
|
+
leafRoleValue: 'leaf',
|
|
3872
|
+
backgroundParam: 'background',
|
|
3873
|
+
supportsPerCallModel: false,
|
|
3874
|
+
});
|
|
3875
|
+
|
|
3876
|
+
/**
|
|
3877
|
+
* Hermes `.md` content converter (#2284): brand-swap (unchanged behavior,
|
|
3878
|
+
* descriptor-driven per `hostBehaviors.brandingRewrites`) followed by the
|
|
3879
|
+
* generic named-dispatch → `delegate_task` projection above, driven by
|
|
3880
|
+
* `capabilities/hermes/capability.json`'s `hostIntegration.dispatch` (read
|
|
3881
|
+
* via `_hostIntegrationDispatch`, values UNCHANGED by this fix — they are
|
|
3882
|
+
* already documentation-sourced and correct).
|
|
3883
|
+
*/
|
|
3884
|
+
function convertClaudeToHermesMarkdown(content, ctx) {
|
|
3885
|
+
const runtime = (ctx && ctx.runtime) || 'hermes';
|
|
3886
|
+
const b = _hostBehaviors(runtime).brandingRewrites;
|
|
3887
|
+
let converted = content;
|
|
3888
|
+
if (b) {
|
|
3889
|
+
converted = converted.replace(/CLAUDE\.md/g, b['CLAUDE.md']);
|
|
3890
|
+
// #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
|
|
3891
|
+
converted = applyClaudeCodeBrandSwap(converted, b['Claude Code']);
|
|
3892
|
+
converted = converted.replace(/\.claude\//g, b['.claude/']);
|
|
3893
|
+
}
|
|
3894
|
+
const dispatch = _hostIntegrationDispatch(runtime);
|
|
3895
|
+
const toolConfig = Object.assign({}, HERMES_DISPATCH_TOOL_CONFIG, {
|
|
3896
|
+
availableRoles: _resolveAvailableGsdRoles(),
|
|
3897
|
+
runtime,
|
|
3898
|
+
});
|
|
3899
|
+
return projectNamedDispatchToStructuralDelegate(converted, dispatch, toolConfig);
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
// ── End Hermes converters ────────────────────────────────────────────────────
|
|
3903
|
+
|
|
2891
3904
|
function convertSlashCommandsToCodexSkillMentions(content) {
|
|
2892
3905
|
// Colon-style /gsd: never appears as a filesystem path segment, so no boundary guard is needed (unlike the hyphen-style below).
|
|
2893
3906
|
let converted = content.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => {
|
|
@@ -3006,10 +4019,14 @@ Typed mapping (agent_type-capable schema only):
|
|
|
3006
4019
|
inherited, or unsupported values; do not invent one-off effort literals in
|
|
3007
4020
|
workflow prose.
|
|
3008
4021
|
- \`fork_context: false\` by default — GSD agents load their own context via \`<files_to_read>\` blocks
|
|
3009
|
-
- \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct
|
|
3010
|
-
Codex \`
|
|
3011
|
-
|
|
3012
|
-
|
|
4022
|
+
- \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct \`spawn_agent\` mapping,
|
|
4023
|
+
but Codex declares \`dispatch.isolation: orchestrator-worktree\` (#2584). Codex
|
|
4024
|
+
\`spawn_agent\` still does not create or bind a git worktree; instead GSD itself
|
|
4025
|
+
creates the worktree and process-spawns the executor into it with
|
|
4026
|
+
\`codex exec --cd <dir>\`, performing every git operation on the executor's behalf
|
|
4027
|
+
(its \`workspace-write\` sandbox makes \`.git\` read-only). Workflows must therefore
|
|
4028
|
+
never fabricate a manual worktree protocol — route through the negotiated
|
|
4029
|
+
isolation adapter, which still fails closed for hosts declaring \`none\` (#3360).
|
|
3013
4030
|
|
|
3014
4031
|
Generic-agent workaround (multi_agent_v1 schema — NO agent_type field):
|
|
3015
4032
|
When only the generic \`multi_agent_v1\` schema is available, typed GSD agent dispatch
|
|
@@ -3089,6 +4106,36 @@ purpose: ${toSingleLine(description)}
|
|
|
3089
4106
|
return `${cleanFrontmatter}\n\n${roleHeader}\n${body}`;
|
|
3090
4107
|
}
|
|
3091
4108
|
|
|
4109
|
+
/**
|
|
4110
|
+
* #2310 — True if `model` is an Anthropic-flavored value that must never appear as a
|
|
4111
|
+
* Codex agent `.toml` `model`. Two forms: (a) a bare Claude Agent-tool tier alias
|
|
4112
|
+
* (opus/sonnet/haiku/fable — the canonical CLAUDE_AGENT_ALIASES, imported from
|
|
4113
|
+
* src/model-resolver.cts so it can't diverge); (b) any Claude model id in any provider
|
|
4114
|
+
* namespacing — `claude-*`, `anthropic/claude-*`, `us.anthropic.claude-*` (the forms the
|
|
4115
|
+
* catalog assigns to opencode/hermes/kilo, reachable on a Codex .toml via the runtime-
|
|
4116
|
+
* resolver path). No OpenAI/Codex model id contains "claude", so a case-insensitive
|
|
4117
|
+
* substring test is a safe, exhaustive guard for (b). Codex/ChatGPT rejects all of these.
|
|
4118
|
+
*/
|
|
4119
|
+
function _isAnthropicFlavoredModel(model) {
|
|
4120
|
+
return typeof model === 'string' && (CLAUDE_AGENT_ALIASES.has(model) || model.toLowerCase().includes('claude'));
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
// #2310 — dedupe stderr warnings so repeated agent emits don't spam (mirrors the
|
|
4124
|
+
// #2041/#1133 model-resolver warn-dedupe). Value is length-capped so an oversized
|
|
4125
|
+
// or secret-shaped override cannot leak in full to logs.
|
|
4126
|
+
const _codexModelOverrideDroppedWarned = new Set();
|
|
4127
|
+
function _warnCodexModelOverrideDropped(agentName, value) {
|
|
4128
|
+
const key = `${agentName}::${value}`;
|
|
4129
|
+
if (_codexModelOverrideDroppedWarned.has(key)) return;
|
|
4130
|
+
_codexModelOverrideDroppedWarned.add(key);
|
|
4131
|
+
const safe = String(value).length > 64 ? `${String(value).slice(0, 64)}…` : String(value);
|
|
4132
|
+
process.stderr.write(
|
|
4133
|
+
`gsd: warning — Codex agent "${agentName}" model "${safe}" is not a valid Codex model ` +
|
|
4134
|
+
`(Anthropic alias/id); dropping it so Codex uses a valid default. ` +
|
|
4135
|
+
`Set runtime:"codex" or pin a gpt-* model to route it.\n`,
|
|
4136
|
+
);
|
|
4137
|
+
}
|
|
4138
|
+
|
|
3092
4139
|
/**
|
|
3093
4140
|
* Generate a per-agent .toml config file for Codex.
|
|
3094
4141
|
* Sets required agent metadata, sandbox_mode, and developer_instructions
|
|
@@ -3122,21 +4169,43 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
|
|
|
3122
4169
|
// model_overrides is respected on Codex (which uses static TOML, not inline
|
|
3123
4170
|
// Task() model parameters). See #2256.
|
|
3124
4171
|
// Precedence: per-agent model_overrides > runtime-aware tier resolution (#2517).
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
4172
|
+
// #2310 — a Codex .toml `model` MUST be a real Codex/OpenAI model id. Codex is a
|
|
4173
|
+
// passive/session-only model host (ADR-1239): GSD cannot reliably route per-agent
|
|
4174
|
+
// tiers, and a bare GSD/Claude tier alias (opus/sonnet/haiku/fable) or a claude-*
|
|
4175
|
+
// id 400s on a ChatGPT-account Codex ("The 'sonnet' model is not supported when
|
|
4176
|
+
// using Codex with a ChatGPT account"). So: embed ONLY an explicit real-Codex
|
|
4177
|
+
// model pin from model_overrides; omit anything Anthropic-flavored so the agent
|
|
4178
|
+
// inherits the always-available session model. (Removing the runtime-resolver
|
|
4179
|
+
// per-tier embedding below is the ADR-2310 passive-posture epic.)
|
|
4180
|
+
const rawModelOverride = modelOverrides?.[resolvedName] || modelOverrides?.[agentName];
|
|
4181
|
+
let pinnedModel = null;
|
|
4182
|
+
if (rawModelOverride) {
|
|
4183
|
+
if (typeof rawModelOverride === 'string' && rawModelOverride && !_isAnthropicFlavoredModel(rawModelOverride)) {
|
|
4184
|
+
pinnedModel = rawModelOverride; // explicit real-Codex model pin → embed verbatim (#2256)
|
|
4185
|
+
} else {
|
|
4186
|
+
_warnCodexModelOverrideDropped(resolvedName, rawModelOverride); // alias/claude-* → omit
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
if (!pinnedModel && runtimeResolver) {
|
|
3131
4190
|
// #2517 — runtime-aware tier resolution. Embeds Codex-native model + reasoning_effort
|
|
3132
4191
|
// from RUNTIME_PROFILE_MAP / model_profile_overrides for the configured tier.
|
|
4192
|
+
// (Superseded on the default path by the ADR-2310 passive-posture epic.)
|
|
3133
4193
|
const entry = runtimeResolver.resolve(resolvedName) || runtimeResolver.resolve(agentName);
|
|
3134
|
-
if (entry?.model)
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
4194
|
+
if (entry?.model) pinnedModel = entry.model;
|
|
4195
|
+
}
|
|
4196
|
+
// #2310 — final safety gate: never emit an Anthropic-flavored model into a Codex
|
|
4197
|
+
// .toml, even from the runtime-resolver path (e.g. a defaults.json runtime that
|
|
4198
|
+
// does not match the codex install target).
|
|
4199
|
+
if (pinnedModel && _isAnthropicFlavoredModel(pinnedModel)) {
|
|
4200
|
+
_warnCodexModelOverrideDropped(resolvedName, pinnedModel);
|
|
4201
|
+
pinnedModel = null;
|
|
4202
|
+
}
|
|
4203
|
+
let hasPinnedModel = false;
|
|
4204
|
+
if (pinnedModel) {
|
|
4205
|
+
lines.push(`model = ${JSON.stringify(pinnedModel)}`);
|
|
4206
|
+
hasPinnedModel = true;
|
|
4207
|
+
// model is resolved here; reasoning_effort from catalog tier is REPLACED by the
|
|
4208
|
+
// unified effort resolver below (#443). Do NOT emit entry.reasoning_effort here.
|
|
3140
4209
|
}
|
|
3141
4210
|
|
|
3142
4211
|
// #443 — Unified effort for Codex .toml. Uses the same config-driven precedence chain
|
|
@@ -3364,14 +4433,26 @@ function _resolveMovedSkillsOldDir(runtime, targetDir, scope) {
|
|
|
3364
4433
|
|
|
3365
4434
|
/**
|
|
3366
4435
|
* Generate the GSD config block for Codex config.toml.
|
|
3367
|
-
*
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
4436
|
+
*
|
|
4437
|
+
* #2406 — standalone per-agent TOMLs (written by installCodexConfig to
|
|
4438
|
+
* `$CODEX_HOME/agents/<name>.toml`) are auto-discovered by Codex and are the
|
|
4439
|
+
* SOLE canonical registration source for each role. This block therefore no
|
|
4440
|
+
* longer emits `[agents.<name>]` role tables that point `config_file` back at
|
|
4441
|
+
* those same standalone TOMLs — that was a second, redundant declaration of
|
|
4442
|
+
* the same role in one config layer, and Codex logged "Ignoring malformed
|
|
4443
|
+
* agent role definition: duplicate agent role name" once per agent as a
|
|
4444
|
+
* result. Only the bare `[agents]` dispatch-tuning scalar table is emitted
|
|
4445
|
+
* here; role name/description/model/reasoning-effort/sandbox settings remain
|
|
4446
|
+
* fully discoverable through the standalone TOML alone.
|
|
4447
|
+
* @param {Array<{name: string, description: string}>} _agents unused — kept
|
|
4448
|
+
* in the signature for call-site compatibility (installCodexConfig and
|
|
4449
|
+
* existing tests still pass it positionally); per-agent role tables are no
|
|
4450
|
+
* longer generated from it.
|
|
4451
|
+
* @param {string} [_targetDir] unused — the standalone-TOML `config_file`
|
|
4452
|
+
* path it used to resolve is no longer emitted here; kept for the same
|
|
4453
|
+
* call-site-compatibility reason as `_agents`.
|
|
4454
|
+
*/
|
|
4455
|
+
function generateCodexConfigBlock(_agents, _targetDir) {
|
|
3375
4456
|
const lines = [
|
|
3376
4457
|
GSD_CODEX_MARKER,
|
|
3377
4458
|
'',
|
|
@@ -3380,24 +4461,12 @@ function generateCodexConfigBlock(agents, targetDir) {
|
|
|
3380
4461
|
// ADR-1239 upgrade 2 / #2088 — explicit dispatch tuning. Pin `max_depth` on the
|
|
3381
4462
|
// `[agents]` (AgentsToml) table rather than relying on codex-cli's implicit
|
|
3382
4463
|
// default, realizing the negotiated `dispatch.maxDepth: 1` axis. This bare
|
|
3383
|
-
// `[agents]` scalar table
|
|
3384
|
-
//
|
|
3385
|
-
// known-scalar-only `[agents]`). Emitted before the role tables so the parent
|
|
3386
|
-
// table is opened first.
|
|
4464
|
+
// `[agents]` scalar table is validated by validateCodexConfigSchema, which
|
|
4465
|
+
// permits a known-scalar-only `[agents]`.
|
|
3387
4466
|
lines.push('[agents]');
|
|
3388
4467
|
lines.push(`max_depth = ${GSD_CODEX_AGENTS_MAX_DEPTH}`);
|
|
3389
4468
|
lines.push('');
|
|
3390
4469
|
|
|
3391
|
-
for (const { name, description } of agents) {
|
|
3392
|
-
// #2727 — Codex 0.124.0 requires [agents.<name>] struct format, not [[agents]] sequence.
|
|
3393
|
-
// [[agents]] (introduced in #2645) is rejected by codex-cli 0.124.0 with
|
|
3394
|
-
// "invalid type: sequence, expected struct AgentsToml in `agents`".
|
|
3395
|
-
lines.push(`[agents.${name}]`);
|
|
3396
|
-
lines.push(`description = ${JSON.stringify(description)}`);
|
|
3397
|
-
lines.push(`config_file = "${agentsPrefix}/${name}.toml"`);
|
|
3398
|
-
lines.push('');
|
|
3399
|
-
}
|
|
3400
|
-
|
|
3401
4470
|
return lines.join('\n');
|
|
3402
4471
|
}
|
|
3403
4472
|
|
|
@@ -5922,12 +6991,14 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
|
|
|
5922
6991
|
// Symlink-escape guard (parity with _copyStaged / copyWithPathReplacement): the
|
|
5923
6992
|
// lexical gate above does not resolve symlinks, so a pre-existing config.toml or
|
|
5924
6993
|
// agents/ symlink could redirect writes outside targetDir. Reject those.
|
|
6994
|
+
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
|
6995
|
+
const symlinkOptIn = isSymlinkedDestOptIn();
|
|
5925
6996
|
if (
|
|
5926
|
-
hasExistingSymlinkBetween(resolvedTargetRoot, configPath) ||
|
|
5927
|
-
hasExistingSymlinkBetween(resolvedTargetRoot, path.resolve(agentsTomlDir))
|
|
6997
|
+
hasExistingSymlinkBetween(resolvedTargetRoot, configPath, { allowOptInFollow: symlinkOptIn }) ||
|
|
6998
|
+
hasExistingSymlinkBetween(resolvedTargetRoot, path.resolve(agentsTomlDir), { allowOptInFollow: symlinkOptIn })
|
|
5928
6999
|
) {
|
|
5929
7000
|
throw new Error(
|
|
5930
|
-
`installCodexConfig: a Codex config path under "${targetDir}" contains a symlink
|
|
7001
|
+
`installCodexConfig: a Codex config path under "${targetDir}" contains a symlink the install root does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
|
|
5931
7002
|
);
|
|
5932
7003
|
}
|
|
5933
7004
|
fs.mkdirSync(agentsTomlDir, { recursive: true });
|
|
@@ -5978,9 +7049,9 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
|
|
|
5978
7049
|
// `name` containing path separators must not escape agents/ (which would let
|
|
5979
7050
|
// it clobber config.toml or write elsewhere under the configHome).
|
|
5980
7051
|
const agentTomlPath = assertDestWithinConfigHome(agentsTomlDir, `${name}.toml`);
|
|
5981
|
-
if (hasExistingSymlinkBetween(resolvedTargetRoot, agentTomlPath)) {
|
|
7052
|
+
if (hasExistingSymlinkBetween(resolvedTargetRoot, agentTomlPath, { allowOptInFollow: symlinkOptIn })) {
|
|
5982
7053
|
throw new Error(
|
|
5983
|
-
`installCodexConfig: agent toml path "${agentTomlPath}" contains a symlink
|
|
7054
|
+
`installCodexConfig: agent toml path "${agentTomlPath}" contains a symlink the install root does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
|
|
5984
7055
|
);
|
|
5985
7056
|
}
|
|
5986
7057
|
fs.writeFileSync(agentTomlPath, tomlContent);
|
|
@@ -6582,7 +7653,8 @@ const RUNTIME_CONTENT_DISPATCH = {
|
|
|
6582
7653
|
const b = _hostBehaviors(ctx.runtime).brandingRewrites;
|
|
6583
7654
|
if (b) {
|
|
6584
7655
|
content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']);
|
|
6585
|
-
|
|
7656
|
+
// #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
|
|
7657
|
+
content = applyClaudeCodeBrandSwap(content, b['Claude Code']);
|
|
6586
7658
|
content = content.replace(/\.claude\//g, b['.claude/']);
|
|
6587
7659
|
}
|
|
6588
7660
|
return content;
|
|
@@ -6599,16 +7671,13 @@ const RUNTIME_CONTENT_DISPATCH = {
|
|
|
6599
7671
|
},
|
|
6600
7672
|
},
|
|
6601
7673
|
hermes: {
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
}
|
|
6610
|
-
return content;
|
|
6611
|
-
},
|
|
7674
|
+
// #2284: brand-swap alone left the false "Agent tool IS available"
|
|
7675
|
+
// assertion + literal `Agent(...)` call syntax installed verbatim — see
|
|
7676
|
+
// convertClaudeToHermesMarkdown / projectNamedDispatchToStructuralDelegate
|
|
7677
|
+
// above (the Hermes converters section) for the full named-dispatch →
|
|
7678
|
+
// `delegate_task` projection, driven by capabilities/hermes/capability.json's
|
|
7679
|
+
// hostIntegration.dispatch facts.
|
|
7680
|
+
md: (content, ctx) => convertClaudeToHermesMarkdown(content, ctx),
|
|
6612
7681
|
js: (content, ctx) => {
|
|
6613
7682
|
const b = _hostBehaviors(ctx.runtime).brandingRewrites;
|
|
6614
7683
|
if (b) {
|
|
@@ -6645,9 +7714,10 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
6645
7714
|
}
|
|
6646
7715
|
const resolvedConfinementRoot = path.resolve(confinementRoot);
|
|
6647
7716
|
const resolvedDestDir = assertDestWithinConfigHome(confinementRoot, destDir);
|
|
6648
|
-
|
|
7717
|
+
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
|
7718
|
+
if (hasExistingSymlinkBetween(resolvedConfinementRoot, resolvedDestDir, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
6649
7719
|
throw new Error(
|
|
6650
|
-
`copyWithPathReplacement: destDir "${destDir}" contains a symlink
|
|
7720
|
+
`copyWithPathReplacement: destDir "${destDir}" contains a symlink the install root "${confinementRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
|
|
6651
7721
|
);
|
|
6652
7722
|
}
|
|
6653
7723
|
// Use the validated absolute path for all writes below so the gate validates
|
|
@@ -7431,6 +8501,17 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
7431
8501
|
console.log(` ${green}✓${reset} Removed ${removedLibFiles} hooks/lib/ helper(s)`);
|
|
7432
8502
|
}
|
|
7433
8503
|
}
|
|
8504
|
+
|
|
8505
|
+
// #2717: remove the CommonJS marker GSD wrote into hooks/ for runtimes that
|
|
8506
|
+
// stage .js hooks via dedicated paths (cursor/windsurf/codex) — but ONLY if
|
|
8507
|
+
// it still carries GSD's exact content (a user-authored package.json is
|
|
8508
|
+
// never deleted). Safe no-op for runtimes whose marker lives at the config
|
|
8509
|
+
// root (the shared-bundle path) or that never received one.
|
|
8510
|
+
try {
|
|
8511
|
+
if (hooksSurface.removeCommonJsMarkerIfGsdOwned(hooksDir)) {
|
|
8512
|
+
console.log(` ${green}✓${reset} Removed GSD hooks/package.json (CommonJS marker)`);
|
|
8513
|
+
}
|
|
8514
|
+
} catch { /* best-effort */ }
|
|
7434
8515
|
}
|
|
7435
8516
|
|
|
7436
8517
|
// 4z. Remove the native plugin adapter (#1914, extended to Kilo by #2093).
|
|
@@ -7591,8 +8672,11 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
7591
8672
|
let permissionsModified = false;
|
|
7592
8673
|
if (Array.isArray(settings.permissions.allow)) {
|
|
7593
8674
|
const before = settings.permissions.allow.length;
|
|
8675
|
+
// #2278 — filter against the union of the current allow-rule forms
|
|
8676
|
+
// AND the retired legacy forms, so uninstall still cleans up
|
|
8677
|
+
// pre-fix installs that still carry the stale `Write(...)` entries.
|
|
7594
8678
|
settings.permissions.allow = settings.permissions.allow.filter(
|
|
7595
|
-
(e) => !GSD_CLAUDE_ALLOW_PERMISSIONS.includes(e)
|
|
8679
|
+
(e) => !GSD_CLAUDE_ALLOW_PERMISSIONS.includes(e) && !GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS.includes(e)
|
|
7596
8680
|
);
|
|
7597
8681
|
if (settings.permissions.allow.length !== before) {
|
|
7598
8682
|
permissionsModified = true;
|
|
@@ -8303,7 +9387,7 @@ function resolveInstallRelativePath(baseDir, relPath) {
|
|
|
8303
9387
|
if (fullPath !== root && !fullPath.startsWith(root + path.sep)) {
|
|
8304
9388
|
return null;
|
|
8305
9389
|
}
|
|
8306
|
-
if (hasExistingSymlinkBetween(root, fullPath)) {
|
|
9390
|
+
if (hasExistingSymlinkBetween(root, fullPath, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
8307
9391
|
return null;
|
|
8308
9392
|
}
|
|
8309
9393
|
return { relPath: normalized, fullPath };
|
|
@@ -8377,9 +9461,14 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
8377
9461
|
}
|
|
8378
9462
|
}
|
|
8379
9463
|
if (_hostBehaviors(runtime).flatCommandDir && fs.existsSync(opencodeCommandDir)) {
|
|
9464
|
+
// #2329: derive the manifest key prefix from the SAME descriptor value used
|
|
9465
|
+
// to compute opencodeCommandDir above, instead of a separately-hardcoded
|
|
9466
|
+
// literal — a divergence here would silently break the manifest even after
|
|
9467
|
+
// the destSubpath descriptor is corrected (Generative Fix Divergence guard).
|
|
9468
|
+
const flatCommandDirPrefix = _hostBehaviors(runtime).flatCommandDir || 'command';
|
|
8380
9469
|
for (const file of fs.readdirSync(opencodeCommandDir)) {
|
|
8381
9470
|
if (file.startsWith('gsd-') && file.endsWith('.md')) {
|
|
8382
|
-
manifest.files['
|
|
9471
|
+
manifest.files[flatCommandDirPrefix + '/' + file] = fileHash(path.join(opencodeCommandDir, file));
|
|
8383
9472
|
}
|
|
8384
9473
|
}
|
|
8385
9474
|
}
|
|
@@ -8974,14 +10063,24 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
8974
10063
|
const _effectiveInstallMode = _isCoreProfileAlias ? 'minimal' : 'full';
|
|
8975
10064
|
// Load the manifest and compute resolved profile for named profiles.
|
|
8976
10065
|
// For --minimal/core: use an empty manifest (core profile has no transitive
|
|
8977
|
-
// deps) to produce a resolvedProfile with the core skill set.
|
|
8978
|
-
//
|
|
10066
|
+
// deps) to produce a resolvedProfile with the core skill set. For core/
|
|
10067
|
+
// standard profiles, resolveProfile's `registry` arg IS consulted (via
|
|
10068
|
+
// _capabilitySkillsForMode) so tier:core/tier:standard capability skills are
|
|
10069
|
+
// unioned in when registered. #2322 correction: for the DEFAULT `full`
|
|
10070
|
+
// profile, resolveProfile short-circuits to the `{skills:'*'}` sentinel
|
|
10071
|
+
// BEFORE ever reading `registry` (there is nothing to union — '*' already
|
|
10072
|
+
// means "everything"), so the registry consultation that matters for `full`
|
|
10073
|
+
// happens LATER, at staging time (stageSkillsForRuntimeAsSkills's '*'
|
|
10074
|
+
// fill-in, resolveRuntimeArtifactLayout's `capabilityRegistry` param below) —
|
|
10075
|
+
// not here. `_installedCapabilityRegistry` (not the frozen `_capabilityRegistry`)
|
|
10076
|
+
// is passed so an INSTALLED third-party capability (not just a first-party
|
|
10077
|
+
// one) is honored on every profile, `full` included (#2322 blocker 2).
|
|
8979
10078
|
const _commandsDir = path.join(src, 'commands', 'gsd');
|
|
8980
10079
|
const _skillsManifest = _isCoreProfileAlias ? new Map() : loadSkillsManifest(_commandsDir);
|
|
8981
10080
|
const _resolvedProfile = resolveProfile({
|
|
8982
10081
|
modes: [_activeProfileName],
|
|
8983
10082
|
manifest: _skillsManifest,
|
|
8984
|
-
registry:
|
|
10083
|
+
registry: _installedCapabilityRegistry,
|
|
8985
10084
|
});
|
|
8986
10085
|
// Unified staging function: all profiles use stageSkillsForProfile with the
|
|
8987
10086
|
// registry-aware _resolvedProfile (ADR-857 phase 4c cutover).
|
|
@@ -9318,6 +10417,45 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9318
10417
|
return Array.isArray(scopeLayout) && scopeLayout.length > 0;
|
|
9319
10418
|
})();
|
|
9320
10419
|
|
|
10420
|
+
// #2624: write the .gsd-source marker. Extracted from its former late position so it can be
|
|
10421
|
+
// called BEFORE staging reads the marker (see the call site below). Scoped to the Claude-global
|
|
10422
|
+
// layout (issue #1477) — the only install path that ships the skills layout without a
|
|
10423
|
+
// commands/gsd source tree, so findInstallSourceRoot's walk-up has nothing to find and
|
|
10424
|
+
// /gsd-surface (list/status) throws without it. Points at the package's own commands/gsd
|
|
10425
|
+
// source. Guarded on source presence so a half-published package never writes a dangling
|
|
10426
|
+
// marker. Write failure is non-fatal (install proceeds; warn so /gsd-surface breakage is
|
|
10427
|
+
// diagnosable) — the same contract the late write had.
|
|
10428
|
+
function _writeGsdSourceMarker(runtime, targetDir, src, isGlobal) {
|
|
10429
|
+
if (_hostBehaviors(runtime).sourceMarkerFile && isGlobal) {
|
|
10430
|
+
const gsdSourceCommands = path.join(src, 'commands', 'gsd');
|
|
10431
|
+
if (fs.existsSync(gsdSourceCommands)) {
|
|
10432
|
+
try {
|
|
10433
|
+
// ADR-1239 Phase B write-confinement: the descriptor-sourced marker filename
|
|
10434
|
+
// must resolve under targetDir (parity with the other descriptor-driven writes).
|
|
10435
|
+
const _markerPath = assertDestWithinConfigHome(targetDir, _hostBehaviors(runtime).sourceMarkerFile);
|
|
10436
|
+
fs.writeFileSync(_markerPath, gsdSourceCommands + '\n', 'utf8');
|
|
10437
|
+
} catch (err) {
|
|
10438
|
+
// Non-fatal: install proceeds. But on the Claude-global layout walk-up
|
|
10439
|
+
// also fails (no commands/gsd source tree), so a silent write failure
|
|
10440
|
+
// still leaves /gsd-surface broken at runtime — warn so it's diagnosable.
|
|
10441
|
+
console.warn(` ${yellow}!${reset} Could not write .gsd-source marker (${err.message}); /gsd-surface list/status may fail`);
|
|
10442
|
+
}
|
|
10443
|
+
}
|
|
10444
|
+
}
|
|
10445
|
+
}
|
|
10446
|
+
|
|
10447
|
+
// #2624: write the .gsd-source marker BEFORE any staging reads it. The marker write
|
|
10448
|
+
// formerly lived AFTER staging; on an upgrade the marker still held the PREVIOUS
|
|
10449
|
+
// install's source path (e.g. an npx per-version cache dir that still exists on disk),
|
|
10450
|
+
// so findInstallSourceRoot(configDir) — called inside installRuntimeArtifacts below —
|
|
10451
|
+
// returned the stale path and every converted skill was generated from the OLD version's
|
|
10452
|
+
// commands/gsd, silently installing prior-version content with a self-consistent manifest
|
|
10453
|
+
// hash. Writing first closes the read-before-write hole for every findInstallSourceRoot
|
|
10454
|
+
// consumer (skills, commands, /gsd-surface, capability-state). Placed here (before the
|
|
10455
|
+
// _isSkillsRuntime branch) so it runs for every Claude-global install, matching the
|
|
10456
|
+
// original write's sourceMarkerFile && isGlobal guard exactly.
|
|
10457
|
+
_writeGsdSourceMarker(runtime, targetDir, src, isGlobal);
|
|
10458
|
+
|
|
9321
10459
|
if (_isSkillsRuntime) {
|
|
9322
10460
|
// Layout-driven install for skills-based runtimes (full and minimal modes)
|
|
9323
10461
|
const scope = isGlobal ? 'global' : 'local';
|
|
@@ -9341,7 +10479,10 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9341
10479
|
resolveAttribution: getCommitAttribution,
|
|
9342
10480
|
});
|
|
9343
10481
|
} else {
|
|
9344
|
-
|
|
10482
|
+
// #2322: fallback path (adapter unavailable) — thread the composed
|
|
10483
|
+
// registry too, so this path stages third-party capability skills
|
|
10484
|
+
// identically to the primary adapter path above.
|
|
10485
|
+
installRuntimeArtifacts(runtime, targetDir, scope, _resolvedProfile, getCommitAttribution, _installedCapabilityRegistry);
|
|
9345
10486
|
}
|
|
9346
10487
|
|
|
9347
10488
|
// #1326 — Codex only: remove stale agents/openai.yaml sidecars from managed
|
|
@@ -9500,7 +10641,8 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9500
10641
|
} else if (_hostBehaviors(runtime).pluginOnlyInstall) {
|
|
9501
10642
|
// pi (ADR-1239 / #2102 Stage 1): plugin-only install — pi's /gsd command is
|
|
9502
10643
|
// registered programmatically by the native extension (pi/gsd.cjs →
|
|
9503
|
-
// extensions/gsd.
|
|
10644
|
+
// extensions/gsd.js, staged separately below; the dest suffix must be
|
|
10645
|
+
// .ts/.js or pi's auto-discovery skips it silently — #2470) and dispatches in-process
|
|
9504
10646
|
// through the embedded gsd-core command-routing hub. pi has no host-read
|
|
9505
10647
|
// markdown surface (unlike Claude/OpenCode/etc., which scan commands/ or
|
|
9506
10648
|
// command/ directories), so writing flat gsd-<cmd>.md files here would be
|
|
@@ -9602,35 +10744,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9602
10744
|
failures.push('gsd-core');
|
|
9603
10745
|
}
|
|
9604
10746
|
|
|
9605
|
-
//
|
|
9606
|
-
//
|
|
9607
|
-
//
|
|
9608
|
-
//
|
|
9609
|
-
//
|
|
9610
|
-
// and /gsd-surface (list/status) throws. This is the writer half of the
|
|
9611
|
-
// marker that runtime-artifact-layout.cjs's finders already read (the reader
|
|
9612
|
-
// landed in #1476). It points at the package's own commands/gsd source.
|
|
9613
|
-
// Scoped to the Claude-global layout (issue #1477) — the only install path
|
|
9614
|
-
// that ships the skills layout without a commands/gsd source tree; every
|
|
9615
|
-
// other runtime/scope deploys commands/gsd, so its walk-up already resolves
|
|
9616
|
-
// and needs no marker. Guarded on source presence so a half-published
|
|
9617
|
-
// package never writes a dangling marker.
|
|
9618
|
-
if (_hostBehaviors(runtime).sourceMarkerFile && isGlobal) {
|
|
9619
|
-
const gsdSourceCommands = path.join(src, 'commands', 'gsd');
|
|
9620
|
-
if (fs.existsSync(gsdSourceCommands)) {
|
|
9621
|
-
try {
|
|
9622
|
-
// ADR-1239 Phase B write-confinement: the descriptor-sourced marker filename
|
|
9623
|
-
// must resolve under targetDir (parity with the other descriptor-driven writes).
|
|
9624
|
-
const _markerPath = assertDestWithinConfigHome(targetDir, _hostBehaviors(runtime).sourceMarkerFile);
|
|
9625
|
-
fs.writeFileSync(_markerPath, gsdSourceCommands + '\n', 'utf8');
|
|
9626
|
-
} catch (err) {
|
|
9627
|
-
// Non-fatal: install proceeds. But on the Claude-global layout walk-up
|
|
9628
|
-
// also fails (no commands/gsd source tree), so a silent write failure
|
|
9629
|
-
// still leaves /gsd-surface broken at runtime — warn so it's diagnosable.
|
|
9630
|
-
console.warn(` ${yellow}!${reset} Could not write .gsd-source marker (${err.message}); /gsd-surface list/status may fail`);
|
|
9631
|
-
}
|
|
9632
|
-
}
|
|
9633
|
-
}
|
|
10747
|
+
// #2624: the .gsd-source marker is now written by _writeGsdSourceMarker()
|
|
10748
|
+
// BEFORE staging reads it (see the early call above the _isSkillsRuntime
|
|
10749
|
+
// block). The former write lived here — AFTER staging — which on an upgrade
|
|
10750
|
+
// let staging read a stale prior-version marker and silently install
|
|
10751
|
+
// old-version skill content. Moved up; this site intentionally left empty.
|
|
9634
10752
|
|
|
9635
10753
|
// #1629 critical fix: Windsurf workflow wrappers (convertClaudeCommandToWindsurfWorkflow)
|
|
9636
10754
|
// delegate to command bodies at <targetDir>/gsd-core/commands/gsd/${stem}.md via a
|
|
@@ -9910,6 +11028,22 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9910
11028
|
failures.push('VERSION');
|
|
9911
11029
|
}
|
|
9912
11030
|
|
|
11031
|
+
// #2297: write a per-install runtime marker co-located with VERSION at
|
|
11032
|
+
// <install>/gsd-core/.gsd-runtime. It gives resolveModelInternal a reliable
|
|
11033
|
+
// "which runtime owns THIS install" signal in a no-project session (config.runtime
|
|
11034
|
+
// is null and GSD_RUNTIME is not exported), so the shared ~/.gsd/defaults.json
|
|
11035
|
+
// resolve_model_ids:"omit" policy (written below for non-alias runtimes only)
|
|
11036
|
+
// applies ONLY when a non-alias runtime is actually resolving — a Claude session
|
|
11037
|
+
// reads its own marker and keeps its tier aliases instead of inheriting another
|
|
11038
|
+
// runtime's install-order-dependent "omit". See src/model-resolver.cts.
|
|
11039
|
+
const runtimeMarkerDest = path.join(targetDir, 'gsd-core', '.gsd-runtime');
|
|
11040
|
+
fs.writeFileSync(runtimeMarkerDest, `${runtime}\n`);
|
|
11041
|
+
if (verifyFileInstalled(runtimeMarkerDest, '.gsd-runtime')) {
|
|
11042
|
+
console.log(` ${green}✓${reset} Wrote runtime marker (.gsd-runtime: ${runtime})`);
|
|
11043
|
+
} else {
|
|
11044
|
+
failures.push('.gsd-runtime');
|
|
11045
|
+
}
|
|
11046
|
+
|
|
9913
11047
|
// Reusable: copy hooks/dist/ + hooks/lib/ into destRootDir, writing the
|
|
9914
11048
|
// CommonJS package.json marker alongside them. Used below for the generic
|
|
9915
11049
|
// configDir install path (guarded by hostBehaviors.skipSharedHooksInstall),
|
|
@@ -10023,13 +11157,15 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10023
11157
|
}
|
|
10024
11158
|
|
|
10025
11159
|
// Gate hooks/lib/ install on the same set of runtimes that receive hooks/.
|
|
10026
|
-
// Codex/Copilot/Cursor/Windsurf/Trae/Cline
|
|
11160
|
+
// Codex/Copilot/Cursor/Windsurf/Trae/Cline do not use the shared
|
|
10027
11161
|
// hooks/lib/ helpers (Cursor uses standalone .js hook scripts registered
|
|
10028
11162
|
// via hooks.json — gated descriptor-driven via
|
|
10029
|
-
// hostBehaviors.skipSharedHooksInstall, #2089; Cline likewise #2090;
|
|
10030
|
-
//
|
|
10031
|
-
// the others skip hooks entirely);
|
|
10032
|
-
// (hooksSurface:'none' with no plugin surface — #1821).
|
|
11163
|
+
// hostBehaviors.skipSharedHooksInstall, #2089; Cline likewise #2090;
|
|
11164
|
+
// Trae likewise #2094; Codex uses hooks.json directly;
|
|
11165
|
+
// the others skip hooks entirely); ZCode also skips hooks entirely
|
|
11166
|
+
// (hooksSurface:'none' with no plugin surface — #1821). Kilo is NOT
|
|
11167
|
+
// excluded since #2305: its native plugin adapter (#2093) spawns the
|
|
11168
|
+
// staged hooks/*.js scripts, same as OpenCode. None of the
|
|
10033
11169
|
// excluded runtimes must receive the hooks/lib/ helpers — otherwise the
|
|
10034
11170
|
// Codex comment downstream ("we deliberately do *not* copy hooks/lib/ for
|
|
10035
11171
|
// Codex") is contradicted in practice. (Gating lives at the call sites
|
|
@@ -10045,18 +11181,21 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10045
11181
|
return hooksOk;
|
|
10046
11182
|
}
|
|
10047
11183
|
|
|
10048
|
-
// #1821:
|
|
10049
|
-
// so the staged hook scripts are dead weight for
|
|
11184
|
+
// #1821: ZCode declares hooksSurface:'none' AND has no plugin surface,
|
|
11185
|
+
// so the staged hook scripts are dead weight for it — excluded here.
|
|
10050
11186
|
// OpenCode also declares hooksSurface:'none' but is deliberately NOT excluded:
|
|
10051
11187
|
// its native plugin adapter (#1914, installed above under plugins/gsd-core.js)
|
|
10052
11188
|
// spawns the staged hooks/*.js scripts via OpenCode's event bus and needs both
|
|
10053
|
-
// them and the CommonJS package.json marker written below.
|
|
11189
|
+
// them and the CommonJS package.json marker written below. Kilo is the same
|
|
11190
|
+
// shape since #2093 (a nativePlugin spawning the staged hooks), so it must
|
|
11191
|
+
// NOT skip either — declaring skipSharedHooksInstall:true alongside a
|
|
11192
|
+
// nativePlugin left every guard the plugin spawns a silent no-op (#2305).
|
|
10054
11193
|
// #2089: Cursor's exclusion is now descriptor-driven via
|
|
10055
11194
|
// hostBehaviors.skipSharedHooksInstall (was hardcoded !isCursor).
|
|
10056
11195
|
// #2090: Cline's exclusion is likewise descriptor-driven (cline declares
|
|
10057
11196
|
// skipSharedHooksInstall:true) — the redundant `&& !isCline` was removed.
|
|
10058
|
-
// #2093: Kilo's exclusion
|
|
10059
|
-
// skipSharedHooksInstall:true)
|
|
11197
|
+
// #2093/#2305: Kilo's former exclusion (descriptor-driven via
|
|
11198
|
+
// skipSharedHooksInstall:true) was removed in #2305 — see above.
|
|
10060
11199
|
// #2094: Trae's exclusion is likewise descriptor-driven (trae declares
|
|
10061
11200
|
// skipSharedHooksInstall:true) — the redundant `&& !isTrae` was removed.
|
|
10062
11201
|
// #2101: ZCode's exclusion is likewise descriptor-driven (zcode declares
|
|
@@ -10275,7 +11414,15 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10275
11414
|
throw _earlyInstallErr;
|
|
10276
11415
|
}
|
|
10277
11416
|
|
|
10278
|
-
|
|
11417
|
+
// #2695: this branch runs for BOTH `core` (minimal) and `full` profiles.
|
|
11418
|
+
// Hooks are lightweight infrastructure (update-check + context monitor), not the
|
|
11419
|
+
// "full agent surface" that `core` deliberately omits. The config.toml / agent
|
|
11420
|
+
// generation below is still gated by its own inner `!isMinimalMode` guard, so
|
|
11421
|
+
// `core` enters the branch to receive the hook-file copy + hooks.json wiring but
|
|
11422
|
+
// does NOT get agent roles generated. Before #2695 the outer `!isMinimalMode`
|
|
11423
|
+
// here skipped the whole branch for `core`, so the registered parent hook pointed
|
|
11424
|
+
// at a worker/registry the same installer never delivered.
|
|
11425
|
+
if (plan.installSurface === 'codex-toml') {
|
|
10279
11426
|
// Capture pre-install snapshots before ANY GSD mutation
|
|
10280
11427
|
// (#2760 fix 3). On post-write schema-validation failure OR any throw
|
|
10281
11428
|
// during the mutation sequence (write failure, merge throw, etc.) we
|
|
@@ -10458,9 +11605,18 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10458
11605
|
|
|
10459
11606
|
// Copy only the hook files that Codex actually registers via its hook configuration (#2153).
|
|
10460
11607
|
// #772: added gsd-context-monitor.js for the new SubagentStart/Stop/PostToolUse events.
|
|
11608
|
+
// #2695: the parent gsd-check-update.js spawn()s gsd-check-update-worker.js, which
|
|
11609
|
+
// require()s managed-hooks-registry.cjs for MANAGED_HOOKS — so all four must be
|
|
11610
|
+
// installed/refreshed together for every profile, or Codex is wired to a dependency
|
|
11611
|
+
// chain the same installer never delivers.
|
|
10461
11612
|
// We deliberately do *not* copy gsd-graphify-update.sh or hooks/lib/ for Codex
|
|
10462
11613
|
// in this change (graphify auto-update support for Codex is out of scope for #3579).
|
|
10463
|
-
const CODEX_HOOKS_TO_COPY = [
|
|
11614
|
+
const CODEX_HOOKS_TO_COPY = [
|
|
11615
|
+
'gsd-check-update.js',
|
|
11616
|
+
'gsd-check-update-worker.js',
|
|
11617
|
+
'managed-hooks-registry.cjs',
|
|
11618
|
+
'gsd-context-monitor.js',
|
|
11619
|
+
];
|
|
10464
11620
|
const codexHooksSrc = path.join(src, 'hooks', 'dist');
|
|
10465
11621
|
if (fs.existsSync(codexHooksSrc)) {
|
|
10466
11622
|
const codexHooksDest = path.join(targetDir, 'hooks');
|
|
@@ -10490,9 +11646,28 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10490
11646
|
content = content.replace(/\{\{GSD_VERSION\}\}/g, pkg.version);
|
|
10491
11647
|
fs.writeFileSync(destFile, content);
|
|
10492
11648
|
try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows */ }
|
|
11649
|
+
} else {
|
|
11650
|
+
// #2695: raw byte-for-byte copy for allowlisted artifacts that carry
|
|
11651
|
+
// no {{GSD_VERSION}} placeholder and no runtime path token (e.g.
|
|
11652
|
+
// managed-hooks-registry.cjs, whose only `.claude` mention is inside a
|
|
11653
|
+
// doc comment). Version/path transforms would be a no-op at best and a
|
|
11654
|
+
// surprise at worst; the issue requires the registry copied verbatim.
|
|
11655
|
+
fs.copyFileSync(srcFile, destFile);
|
|
11656
|
+
try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows */ }
|
|
10493
11657
|
}
|
|
10494
11658
|
}
|
|
10495
11659
|
console.log(` ${green}✓${reset} Installed hooks (Codex)`);
|
|
11660
|
+
// #2717: write the CommonJS marker into hooks/ alongside the staged .js
|
|
11661
|
+
// scripts. Codex is excluded from installSharedHooksBundle by the
|
|
11662
|
+
// !isCodex gate, so it never received the marker the shared-bundle path
|
|
11663
|
+
// writes for the other runtimes. Without it, a ~/.codex/package.json
|
|
11664
|
+
// declaring {"type":"module"} makes Node load gsd-check-update.js /
|
|
11665
|
+
// gsd-context-monitor.js as ESM and their require() calls fail silently.
|
|
11666
|
+
// Reuses the same helper the Cursor/Windsurf writers call so the marker
|
|
11667
|
+
// content + user-file-preservation contract is identical everywhere.
|
|
11668
|
+
if (hooksSurface.ensureCommonJsMarker(codexHooksDest)) {
|
|
11669
|
+
console.log(` ${green}✓${reset} Wrote hooks/package.json (CommonJS mode)`);
|
|
11670
|
+
}
|
|
10496
11671
|
}
|
|
10497
11672
|
|
|
10498
11673
|
// Add Codex hooks (SessionStart for update checking) — requires codex_hooks feature flag
|
|
@@ -11221,6 +12396,19 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
11221
12396
|
fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
|
|
11222
12397
|
console.log(` ${green}✓${reset} Set resolve_model_ids: "omit" in ~/.gsd/defaults.json`);
|
|
11223
12398
|
}
|
|
12399
|
+
|
|
12400
|
+
// #2395: also persist `runtime: <runtime>` for non-Claude runtimes, so
|
|
12401
|
+
// resolveRuntime() (precedence: GSD_RUNTIME env > config.runtime > 'claude')
|
|
12402
|
+
// resolves to the install's actual runtime identity out of the box — without
|
|
12403
|
+
// this, agent_runtime and every runtime-branded slash hint falls through to
|
|
12404
|
+
// the hard-coded 'claude' default. Mirrors the resolve_model_ids write above:
|
|
12405
|
+
// honor an explicit pre-existing value (any string), only default-populating
|
|
12406
|
+
// when absent. Claude is the resolveRuntime() fallback, so it needs no write.
|
|
12407
|
+
if (defaults.runtime === undefined || defaults.runtime === null || defaults.runtime === '') {
|
|
12408
|
+
defaults.runtime = runtime;
|
|
12409
|
+
fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
|
|
12410
|
+
console.log(` ${green}✓${reset} Set runtime: "${runtime}" in ~/.gsd/defaults.json`);
|
|
12411
|
+
}
|
|
11224
12412
|
} catch (e) {
|
|
11225
12413
|
console.log(` ${yellow}⚠${reset} Could not write ~/.gsd/defaults.json: ${e.message}`);
|
|
11226
12414
|
}
|
|
@@ -11331,16 +12519,17 @@ const runtimeMap = {
|
|
|
11331
12519
|
'8': 'cursor',
|
|
11332
12520
|
'9': 'hermes',
|
|
11333
12521
|
'10': 'kimi',
|
|
11334
|
-
'11': '
|
|
11335
|
-
'12': '
|
|
11336
|
-
'13': '
|
|
11337
|
-
'14': '
|
|
11338
|
-
'15': '
|
|
11339
|
-
'16': '
|
|
11340
|
-
'17': '
|
|
12522
|
+
'11': 'kimi-code',
|
|
12523
|
+
'12': 'kilo',
|
|
12524
|
+
'13': 'opencode',
|
|
12525
|
+
'14': 'pi',
|
|
12526
|
+
'15': 'qwen',
|
|
12527
|
+
'16': 'trae',
|
|
12528
|
+
'17': 'windsurf',
|
|
12529
|
+
'18': 'zcode'
|
|
11341
12530
|
};
|
|
11342
|
-
const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'hermes', 'kimi', 'kilo', 'opencode', 'pi', 'qwen', 'trae', 'windsurf', 'zcode'];
|
|
11343
|
-
const ALL_RUNTIMES_OPTION = '
|
|
12531
|
+
const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'hermes', 'kimi', 'kimi-code', 'kilo', 'opencode', 'pi', 'qwen', 'trae', 'windsurf', 'zcode'];
|
|
12532
|
+
const ALL_RUNTIMES_OPTION = '19';
|
|
11344
12533
|
|
|
11345
12534
|
/**
|
|
11346
12535
|
* Build the runtime-selection prompt text shown by the interactive installer.
|
|
@@ -11358,14 +12547,15 @@ function buildRuntimePromptText() {
|
|
|
11358
12547
|
${cyan}8${reset}) Cursor ${dim}(~/.cursor)${reset}
|
|
11359
12548
|
${cyan}9${reset}) Hermes Agent ${dim}(~/.hermes)${reset}
|
|
11360
12549
|
${cyan}10${reset}) Kimi ${dim}(~/.config/agents, then ~/.agents if existing)${reset}
|
|
11361
|
-
${cyan}11${reset})
|
|
11362
|
-
${cyan}12${reset})
|
|
11363
|
-
${cyan}13${reset})
|
|
11364
|
-
${cyan}14${reset})
|
|
11365
|
-
${cyan}15${reset})
|
|
11366
|
-
${cyan}16${reset})
|
|
11367
|
-
${cyan}17${reset})
|
|
11368
|
-
${cyan}18${reset})
|
|
12550
|
+
${cyan}11${reset}) Kimi Code ${dim}(~/.kimi-code)${reset}
|
|
12551
|
+
${cyan}12${reset}) Kilo ${dim}(~/.config/kilo)${reset}
|
|
12552
|
+
${cyan}13${reset}) OpenCode ${dim}(~/.config/opencode)${reset}
|
|
12553
|
+
${cyan}14${reset}) pi ${dim}(~/.pi/agent)${reset}
|
|
12554
|
+
${cyan}15${reset}) Qwen Code ${dim}(~/.qwen)${reset}
|
|
12555
|
+
${cyan}16${reset}) Trae ${dim}(~/.trae)${reset}
|
|
12556
|
+
${cyan}17${reset}) Windsurf ${dim}(~/.codeium/windsurf)${reset}
|
|
12557
|
+
${cyan}18${reset}) ZCode ${dim}(~/.zcode)${reset}
|
|
12558
|
+
${cyan}19${reset}) All
|
|
11369
12559
|
|
|
11370
12560
|
${dim}Select multiple: 1,2,6 or 1 2 6${reset}
|
|
11371
12561
|
`;
|
|
@@ -11376,7 +12566,7 @@ function buildRuntimePromptText() {
|
|
|
11376
12566
|
* Pure function — exported so tests can verify split/dedupe/fallback behavior.
|
|
11377
12567
|
* - Accepts comma- and/or whitespace-separated choices
|
|
11378
12568
|
* - Deduplicates while preserving order
|
|
11379
|
-
* - Maps option
|
|
12569
|
+
* - Maps option 19 ("All") to every runtime
|
|
11380
12570
|
* - Falls back to ['claude'] when nothing valid is selected
|
|
11381
12571
|
*/
|
|
11382
12572
|
function parseRuntimeInput(answer) {
|
|
@@ -12157,6 +13347,7 @@ module.exports = {
|
|
|
12157
13347
|
// #768 — Claude Code permissions pre-population
|
|
12158
13348
|
mergeClaudePermissions,
|
|
12159
13349
|
GSD_CLAUDE_ALLOW_PERMISSIONS,
|
|
13350
|
+
GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS,
|
|
12160
13351
|
GSD_CLAUDE_DENY_PERMISSIONS,
|
|
12161
13352
|
GSD_CODEX_MARKER,
|
|
12162
13353
|
CODEX_AGENT_SANDBOX,
|
|
@@ -12206,6 +13397,18 @@ module.exports = {
|
|
|
12206
13397
|
convertClaudeToCliineMarkdown,
|
|
12207
13398
|
convertClaudeCommandToClineSkill,
|
|
12208
13399
|
convertClaudeAgentToClineAgent,
|
|
13400
|
+
// #2284(b) — cross-cutting branding protected-region helper
|
|
13401
|
+
applyClaudeCodeBrandSwap,
|
|
13402
|
+
// #2284 — Hermes named-dispatch → delegate_task projection
|
|
13403
|
+
convertClaudeToHermesMarkdown,
|
|
13404
|
+
projectNamedDispatchToStructuralDelegate,
|
|
13405
|
+
_hostIntegrationDispatch,
|
|
13406
|
+
_resolveAvailableGsdRoles,
|
|
13407
|
+
HERMES_DISPATCH_TOOL_CONFIG,
|
|
13408
|
+
maskStringLiterals,
|
|
13409
|
+
findDispatchCallSpans,
|
|
13410
|
+
_assertProjectionComplete,
|
|
13411
|
+
_normalizeDispatchCallSpan,
|
|
12209
13412
|
buildClineRulesBody,
|
|
12210
13413
|
buildClineAgentsMdBody,
|
|
12211
13414
|
buildClinePreToolUseHook,
|