@opengsd/gsd-core 1.7.0 → 1.8.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 +14 -0
- package/README.md +2 -0
- package/agents/gsd-debug-session-manager.md +42 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +29 -2
- package/agents/gsd-planner.md +29 -36
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1152 -80
- 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 +3 -2
- package/gsd-core/bin/gsd-tools.cjs +1878 -2507
- 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 +338 -45
- 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-registry.cjs +155 -86
- package/gsd-core/bin/lib/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +128 -25
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +115 -27
- package/gsd-core/bin/lib/claude-orchestration.cjs +84 -9
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +81 -4
- package/gsd-core/bin/lib/config-loader.cjs +14 -2
- package/gsd-core/bin/lib/config.cjs +69 -18
- 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/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/init.cjs +111 -47
- package/gsd-core/bin/lib/install-engine.cjs +298 -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 +246 -12
- package/gsd-core/bin/lib/model-catalog.cjs +19 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- package/gsd-core/bin/lib/phase-id.cjs +26 -4
- package/gsd-core/bin/lib/phase.cjs +201 -12
- package/gsd-core/bin/lib/plan-scan.cjs +70 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +7 -4
- package/gsd-core/bin/lib/roadmap.cjs +13 -3
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -1
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +22 -8
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +16 -0
- package/gsd-core/bin/lib/smart-entry.cjs +69 -4
- package/gsd-core/bin/lib/state-document.cjs +7 -4
- package/gsd-core/bin/lib/state-transition.cjs +22 -1
- package/gsd-core/bin/lib/state.cjs +65 -11
- package/gsd-core/bin/lib/surface.cjs +51 -9
- package/gsd-core/bin/lib/uat.cjs +420 -5
- package/gsd-core/bin/lib/validate.cjs +12 -8
- package/gsd-core/bin/lib/verification.cjs +112 -17
- package/gsd-core/bin/lib/verify.cjs +220 -22
- package/gsd-core/bin/shared/config-schema.manifest.json +3 -2
- 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/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/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/reviewer-instances.md +9 -7
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/templates/DEBUG.md +5 -3
- 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 +4 -2
- package/gsd-core/workflows/audit-fix.md +2 -2
- package/gsd-core/workflows/check-todos.md +3 -1
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review.md +17 -5
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +25 -5
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discovery-phase.md +7 -0
- package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -0
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +1 -0
- package/gsd-core/workflows/eval-review.md +3 -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 +25 -34
- package/gsd-core/workflows/execute-plan.md +15 -4
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +6 -2
- package/gsd-core/workflows/import.md +8 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +15 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +4 -4
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +69 -21
- package/gsd-core/workflows/new-project.md +17 -15
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +14 -5
- package/gsd-core/workflows/plan-review-convergence.md +48 -3
- 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 +19 -7
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +89 -73
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -0
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +50 -3
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +3 -0
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/ui-phase.md +3 -1
- package/gsd-core/workflows/ui-review.md +3 -0
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +2 -0
- package/gsd-core/workflows/validate-phase.md +3 -0
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +7 -3
- package/hooks/dist/gsd-context-monitor.js +27 -9
- package/hooks/dist/gsd-statusline.js +88 -3
- package/hooks/gsd-context-monitor.js +27 -9
- package/hooks/gsd-statusline.js +88 -3
- package/package.json +6 -4
- package/pi/gsd.cjs +8 -2
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-glossary-refs.cjs +220 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/gen-adr-index.cjs +526 -0
- package/scripts/gen-test-timings.cjs +201 -0
- package/scripts/lint-portable-timeout.cjs +140 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- 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 +3 -2
- package/vscode/package.json +1 -1
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);
|
|
@@ -352,6 +375,7 @@ const {
|
|
|
352
375
|
} = require(path.join(_gsdLibDir, 'model-catalog.cjs'));
|
|
353
376
|
const {
|
|
354
377
|
resolveTierEntry: gsdResolveTierEntry,
|
|
378
|
+
CLAUDE_AGENT_ALIASES,
|
|
355
379
|
} = require(path.join(_gsdLibDir, 'model-resolver.cjs'));
|
|
356
380
|
|
|
357
381
|
// #2071 — install-time effort resolution (readGsdEffectiveEffortConfig /
|
|
@@ -390,6 +414,32 @@ try {
|
|
|
390
414
|
_capabilityRegistry = undefined;
|
|
391
415
|
}
|
|
392
416
|
|
|
417
|
+
// #2322 BLOCKER 2: `_capabilityRegistry` above is the FROZEN first-party registry
|
|
418
|
+
// (capability-registry.cjs, built at publish time) — it never reflects an
|
|
419
|
+
// INSTALLED third-party overlay capability, so a fresh `gsd install` could never
|
|
420
|
+
// stage an installed third-party capability's skill regardless of registration,
|
|
421
|
+
// even on the DEFAULT `--profile full`. `_installedCapabilityRegistry` composes
|
|
422
|
+
// the overlay via capability-loader's `loadRegistry({includeInstalled:true})` —
|
|
423
|
+
// the SAME call capability-writer.cts's `capability set --runtime` path already
|
|
424
|
+
// uses — so a fresh install and a post-install `capability set` agree on
|
|
425
|
+
// third-party skill availability. Used ONLY for skill-profile resolution and
|
|
426
|
+
// runtime-artifact-layout staging below; `_capabilityRegistry` (frozen) remains
|
|
427
|
+
// the source for gsd-core's OWN runtime/host-behavior descriptors (unaffected —
|
|
428
|
+
// those are always first-party). A load failure degrades to the frozen
|
|
429
|
+
// `_capabilityRegistry` (no overlay data -> no third-party skills staged; never
|
|
430
|
+
// a crash and never a scan-and-guess fallback).
|
|
431
|
+
let _installedCapabilityRegistry;
|
|
432
|
+
try {
|
|
433
|
+
const _capabilityLoader = require(path.join(_gsdLibDir, 'capability-loader.cjs'));
|
|
434
|
+
_installedCapabilityRegistry = _capabilityLoader.loadRegistry({
|
|
435
|
+
includeInstalled: true,
|
|
436
|
+
cwd: process.cwd(),
|
|
437
|
+
gsdHome: process.env['GSD_HOME'],
|
|
438
|
+
});
|
|
439
|
+
} catch (_) {
|
|
440
|
+
_installedCapabilityRegistry = _capabilityRegistry;
|
|
441
|
+
}
|
|
442
|
+
|
|
393
443
|
// Fail-safe floor for the reference host's #338-privacy-critical behaviors, used
|
|
394
444
|
// ONLY when the first-party capability registry cannot be loaded (a broken bundle).
|
|
395
445
|
// Without it, a registry-load failure would make `_hostBehaviors('claude')` return
|
|
@@ -441,6 +491,25 @@ function _hostBehaviors(runtime) {
|
|
|
441
491
|
return _resolveHostBehaviors(runtime, _capabilityRegistry);
|
|
442
492
|
}
|
|
443
493
|
|
|
494
|
+
/**
|
|
495
|
+
* Read a runtime's documentation-sourced `hostIntegration.dispatch` axes
|
|
496
|
+
* (ADR-1239 Phase A — `capabilities/<runtime>/capability.json`
|
|
497
|
+
* `runtime.hostIntegration.dispatch`): `{namedDispatch, nested, maxDepth,
|
|
498
|
+
* background, backgroundDispatch, subagentToolkit}`. These are validated,
|
|
499
|
+
* closed-vocabulary FACTS about what the runtime's real dispatch primitive
|
|
500
|
+
* supports (never inferred) — see `docs/reference/host-integration-capability-
|
|
501
|
+
* matrix.md` for citations. Unlike `_hostBehaviors` (install *policy*), this is
|
|
502
|
+
* the negotiated *capability* surface; #2284 is its first content-projection
|
|
503
|
+
* consumer (previously read only by `shouldFlattenDispatch`). Returns `{}` if
|
|
504
|
+
* the registry or the runtime's descriptor is unavailable, so callers must
|
|
505
|
+
* treat every axis as absent/unknown (fail-closed) rather than assume a value.
|
|
506
|
+
*/
|
|
507
|
+
function _hostIntegrationDispatch(runtime) {
|
|
508
|
+
const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime];
|
|
509
|
+
const dispatch = cap && cap.runtime && cap.runtime.hostIntegration && cap.runtime.hostIntegration.dispatch;
|
|
510
|
+
return dispatch || {};
|
|
511
|
+
}
|
|
512
|
+
|
|
444
513
|
/**
|
|
445
514
|
* Resolve the ACTUAL on-disk skills-install directory for a runtime, honoring a
|
|
446
515
|
* skills-kind `home` override (ADR-1239 upgrade 3 / #2088: e.g. Codex skills ->
|
|
@@ -510,6 +579,7 @@ const {
|
|
|
510
579
|
_installNativePluginIfDeclared,
|
|
511
580
|
_copyStaged,
|
|
512
581
|
hasExistingSymlinkBetween,
|
|
582
|
+
isSymlinkedDestOptIn,
|
|
513
583
|
preserveUserArtifacts,
|
|
514
584
|
restoreUserArtifacts,
|
|
515
585
|
migrateLegacyDevPreferencesToSkill,
|
|
@@ -2368,6 +2438,56 @@ function extractFrontmatterField(frontmatter, fieldName) {
|
|
|
2368
2438
|
return match[1].trim().replace(/^['"]|['"]$/g, '');
|
|
2369
2439
|
}
|
|
2370
2440
|
|
|
2441
|
+
// #2284 finding (b): the `<runtime_compatibility>` block appearing in
|
|
2442
|
+
// gsd-core/workflows/{plan-phase,execute-phase}.md is a runtime-COMPARISON
|
|
2443
|
+
// table ("**Claude Code:** Uses `Agent(...)`" / "a backgrounded Claude Code
|
|
2444
|
+
// agent" / "top-level Claude Code") — every "Claude Code" mention inside it
|
|
2445
|
+
// is a COMPARED-RUNTIME LABEL, not a host self-reference. The brand swap
|
|
2446
|
+
// below (`Claude Code` → the installing runtime's own display name) is
|
|
2447
|
+
// meant only for host self-references; applying it inside this block
|
|
2448
|
+
// mislabels the comparison (e.g. Windsurf installs would read "**Windsurf:**
|
|
2449
|
+
// Uses `Agent(...)`" describing what is actually Claude Code's behavior).
|
|
2450
|
+
// This is cross-cutting across every runtime that brand-swaps workflow
|
|
2451
|
+
// content (cursor/windsurf/trae/cline/codebuddy hardcoded; qwen/hermes
|
|
2452
|
+
// descriptor-driven via hostBehaviors.brandingRewrites) — confirmed to
|
|
2453
|
+
// reproduce on unmodified Windsurf, not Hermes-specific.
|
|
2454
|
+
const RUNTIME_COMPATIBILITY_BLOCK_RE = /<runtime_compatibility>[\s\S]*?<\/runtime_compatibility>/g;
|
|
2455
|
+
|
|
2456
|
+
/**
|
|
2457
|
+
* Rewrite bare "Claude Code" self-references in workflow content to
|
|
2458
|
+
* `brandName`, EXCEPT inside `<runtime_compatibility>...</runtime_compatibility>`
|
|
2459
|
+
* blocks, which are left byte-for-byte verbatim. Every other content
|
|
2460
|
+
* transform in a runtime's `.md` converter (tool-name renames, path
|
|
2461
|
+
* rewrites, etc.) is unaffected — only this literal brand-name swap is
|
|
2462
|
+
* protected-region-aware, since only it risks mislabeling a
|
|
2463
|
+
* runtime-comparison table.
|
|
2464
|
+
*
|
|
2465
|
+
* Implementation: SPLIT `content` on the protected-block regex, brand-swap
|
|
2466
|
+
* only the GAP text between (and around) matches, then rejoin gap+block
|
|
2467
|
+
* alternately. No placeholder/sentinel token of any kind is substituted in
|
|
2468
|
+
* — a prior version used a sentinel-token mask/restore, which is exactly the
|
|
2469
|
+
* kind of invisible landmine this rewrite eliminates (a sentinel string, no
|
|
2470
|
+
* matter how obscure, is a theoretical collision risk with real content and
|
|
2471
|
+
* is easy to silently reintroduce in a future edit without it showing in a
|
|
2472
|
+
* diff). Behavior-identical to the removed sentinel-token version — verified
|
|
2473
|
+
* via `npm run gen:golden` producing zero further diff.
|
|
2474
|
+
*/
|
|
2475
|
+
function applyClaudeCodeBrandSwap(content, brandName) {
|
|
2476
|
+
if (!brandName) return content;
|
|
2477
|
+
let result = '';
|
|
2478
|
+
let lastIndex = 0;
|
|
2479
|
+
RUNTIME_COMPATIBILITY_BLOCK_RE.lastIndex = 0; // reset shared global-regex state before each use
|
|
2480
|
+
let m;
|
|
2481
|
+
while ((m = RUNTIME_COMPATIBILITY_BLOCK_RE.exec(content))) {
|
|
2482
|
+
const gap = content.slice(lastIndex, m.index);
|
|
2483
|
+
result += gap.replace(/\bClaude Code\b/g, brandName);
|
|
2484
|
+
result += m[0]; // protected block, verbatim — never brand-swapped
|
|
2485
|
+
lastIndex = m.index + m[0].length;
|
|
2486
|
+
}
|
|
2487
|
+
result += content.slice(lastIndex).replace(/\bClaude Code\b/g, brandName);
|
|
2488
|
+
return result;
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2371
2491
|
// Tool name mapping from Claude Code to Cursor CLI
|
|
2372
2492
|
const claudeToCursorTools = {
|
|
2373
2493
|
Bash: 'Shell',
|
|
@@ -2400,8 +2520,9 @@ function convertClaudeToCursorMarkdown(content) {
|
|
|
2400
2520
|
// Remove Claude Code-specific bug workarounds before brand replacement
|
|
2401
2521
|
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
2402
2522
|
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
|
2403
|
-
// Replace "Claude Code" brand references with "Cursor"
|
|
2404
|
-
|
|
2523
|
+
// Replace "Claude Code" brand references with "Cursor" — #2284(b): skips
|
|
2524
|
+
// <runtime_compatibility> comparison-table content (protected region).
|
|
2525
|
+
converted = applyClaudeCodeBrandSwap(converted, 'Cursor');
|
|
2405
2526
|
return converted;
|
|
2406
2527
|
}
|
|
2407
2528
|
|
|
@@ -2445,7 +2566,13 @@ function convertClaudeCommandToCursorSkill(content, skillName) {
|
|
|
2445
2566
|
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
|
2446
2567
|
const adapter = getCursorSkillAdapterHeader(skillName);
|
|
2447
2568
|
|
|
2448
|
-
|
|
2569
|
+
// #2341: mark user-invocable:false so the skill is NOT shown in Cursor's '/'
|
|
2570
|
+
// menu (it defaults to true). Cursor also writes a commands/ surface (#785),
|
|
2571
|
+
// and surfacing both duplicated every /gsd-* entry. This mirrors the #789
|
|
2572
|
+
// CodeBuddy de-dup: the commands/ surface is the sole '/' entry point; skills
|
|
2573
|
+
// stay model-invocable background knowledge. (user-invocable:false hides from
|
|
2574
|
+
// '/' while keeping model invocation — distinct from disable-model-invocation.)
|
|
2575
|
+
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
|
2449
2576
|
}
|
|
2450
2577
|
|
|
2451
2578
|
/**
|
|
@@ -2533,8 +2660,9 @@ function convertClaudeToWindsurfMarkdown(content) {
|
|
|
2533
2660
|
// Remove Claude Code-specific bug workarounds before brand replacement
|
|
2534
2661
|
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
2535
2662
|
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
|
2536
|
-
// Replace "Claude Code" brand references with "Windsurf"
|
|
2537
|
-
|
|
2663
|
+
// Replace "Claude Code" brand references with "Windsurf" — #2284(b): skips
|
|
2664
|
+
// <runtime_compatibility> comparison-table content (protected region).
|
|
2665
|
+
converted = applyClaudeCodeBrandSwap(converted, 'Windsurf');
|
|
2538
2666
|
return converted;
|
|
2539
2667
|
}
|
|
2540
2668
|
|
|
@@ -2668,7 +2796,8 @@ function convertClaudeToTraeMarkdown(content) {
|
|
|
2668
2796
|
converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'TRAE_CONFIG_DIR');
|
|
2669
2797
|
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
2670
2798
|
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
|
2671
|
-
|
|
2799
|
+
// #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
|
|
2800
|
+
converted = applyClaudeCodeBrandSwap(converted, 'Trae');
|
|
2672
2801
|
return converted;
|
|
2673
2802
|
}
|
|
2674
2803
|
|
|
@@ -2740,7 +2869,8 @@ function convertClaudeToCodebuddyMarkdown(content) {
|
|
|
2740
2869
|
converted = converted.replace(/\.claude\//g, '.codebuddy/');
|
|
2741
2870
|
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
2742
2871
|
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
|
2743
|
-
|
|
2872
|
+
// #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
|
|
2873
|
+
converted = applyClaudeCodeBrandSwap(converted, 'CodeBuddy');
|
|
2744
2874
|
return converted;
|
|
2745
2875
|
}
|
|
2746
2876
|
|
|
@@ -2835,7 +2965,8 @@ function convertClaudeToCliineMarkdown(content) {
|
|
|
2835
2965
|
converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'CLINE_CONFIG_DIR');
|
|
2836
2966
|
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
2837
2967
|
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
|
2838
|
-
|
|
2968
|
+
// #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
|
|
2969
|
+
converted = applyClaudeCodeBrandSwap(converted, 'Cline');
|
|
2839
2970
|
return converted;
|
|
2840
2971
|
}
|
|
2841
2972
|
|
|
@@ -2888,6 +3019,825 @@ function convertClaudeCommandToClineSkill(content, skillName, runtime = null, cm
|
|
|
2888
3019
|
|
|
2889
3020
|
// ── End Cline converters ─────────────────────────────────────────────────────
|
|
2890
3021
|
|
|
3022
|
+
// ── Hermes converters (#2284) ────────────────────────────────────────────────
|
|
3023
|
+
//
|
|
3024
|
+
// Hermes exposes `delegate_task` for subagent dispatch, not the Claude-shaped
|
|
3025
|
+
// `Agent(...)` tool the host-neutral `gsd-core/workflows/*.md` corpus assumes.
|
|
3026
|
+
// Prior to this fix, the hermes `.md` hook (RUNTIME_CONTENT_DISPATCH.hermes)
|
|
3027
|
+
// only brand-swapped "Claude Code" → "Hermes Agent" via
|
|
3028
|
+
// hostBehaviors.brandingRewrites, leaving the false "Agent tool IS available"
|
|
3029
|
+
// assertion and literal `Agent(...)` call syntax installed verbatim.
|
|
3030
|
+
//
|
|
3031
|
+
// `projectNamedDispatchToStructuralDelegate` is GENERIC projection machinery:
|
|
3032
|
+
// it branches ENTIRELY on the runtime's documentation-sourced
|
|
3033
|
+
// `hostIntegration.dispatch` facts (read via `_hostIntegrationDispatch`,
|
|
3034
|
+
// capabilities/<runtime>/capability.json — never hardcoded here) and a
|
|
3035
|
+
// `toolConfig` that supplies only the target primitive's own vocabulary (its
|
|
3036
|
+
// call name + native parameter names — not a capability claim; there is no
|
|
3037
|
+
// `dispatch` axis for "the call's own parameter names", so that vocabulary is
|
|
3038
|
+
// necessarily supplied by the caller, exactly as every other runtime's
|
|
3039
|
+
// converter supplies its own tool-name vocabulary, e.g. Trae's `Shell(`).
|
|
3040
|
+
//
|
|
3041
|
+
// Hermes-specific facts consumed (capabilities/hermes/capability.json,
|
|
3042
|
+
// docs/reference/host-integration-capability-matrix.md:244-249 — UNCHANGED by
|
|
3043
|
+
// this fix):
|
|
3044
|
+
// - dispatch.namedDispatch: false — Hermes's delegate_task has no named-
|
|
3045
|
+
// agent lookup ("Subagents are identified only by role ('leaf' or
|
|
3046
|
+
// 'orchestrator')"). GSD resolves the referenced gsd-* role itself
|
|
3047
|
+
// (fail-closed against the staged agents/ dir) and embeds the loaded
|
|
3048
|
+
// PROMPT CONTENT into the delegate_task payload.
|
|
3049
|
+
// - dispatch.background: true — `delegate_task(background=true)` "returns a
|
|
3050
|
+
// handle immediately"; Claude's `run_in_background=` maps onto Hermes's
|
|
3051
|
+
// own `background=` parameter, preserving the async-handle / no-busy-poll
|
|
3052
|
+
// / resume-on-completion wording already used throughout these workflows.
|
|
3053
|
+
// - dispatch.subagentToolkit: "read-only" / dispatch.maxDepth: 1 — dispatched
|
|
3054
|
+
// roles never themselves further delegate, so no nested-delegation
|
|
3055
|
+
// instruction is ever emitted toward them.
|
|
3056
|
+
|
|
3057
|
+
/**
|
|
3058
|
+
* Resolve the set of gsd-* role-prompt stems actually shipped in this
|
|
3059
|
+
* package's `agents/` directory (the FULL source set, not profile-staged —
|
|
3060
|
+
* `--minimal`/`--profile=core` intentionally excludes many agents from a
|
|
3061
|
+
* given install without those workflows being unreachable, so validating
|
|
3062
|
+
* against the profile-filtered subset would fail every restricted-profile
|
|
3063
|
+
* Hermes install; validating against the shipped source catches genuine
|
|
3064
|
+
* authoring bugs — a stale/typo'd role reference — without that regression).
|
|
3065
|
+
* Returns `null` if the directory cannot be resolved (fail-closed: callers
|
|
3066
|
+
* must refuse to install rather than skip validation).
|
|
3067
|
+
*/
|
|
3068
|
+
function _resolveAvailableGsdRoles() {
|
|
3069
|
+
try {
|
|
3070
|
+
const agentsDir = path.join(__dirname, '..', 'agents');
|
|
3071
|
+
return new Set(
|
|
3072
|
+
fs.readdirSync(agentsDir, { withFileTypes: true })
|
|
3073
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
3074
|
+
.map((e) => e.name.slice(0, -3)),
|
|
3075
|
+
);
|
|
3076
|
+
} catch (_e) {
|
|
3077
|
+
return null;
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
/**
|
|
3082
|
+
* Fail-closed validation (#2284 AC: "Missing role prompts fail closed" /
|
|
3083
|
+
* "never emit a workflow referencing an unresolvable role"). A single literal
|
|
3084
|
+
* `gsd-*` role value must resolve to a real `agents/<role>.md` file — throws
|
|
3085
|
+
* an explicit Error otherwise, aborting the install (the standard
|
|
3086
|
+
* `copyWithPathReplacement` failure path already used for its own
|
|
3087
|
+
* confinement-violation throws). Called per extracted role value from EVERY
|
|
3088
|
+
* call-syntax form (`subagent_type=`, `subagent_type:`, post-rename
|
|
3089
|
+
* `gsd_role=`) — the check operates on the resolved value, independent of
|
|
3090
|
+
* which source syntax produced it. Non-literal / dynamic expressions (e.g.
|
|
3091
|
+
* `research_hook.ref.agent`) are not quoted strings and are never passed
|
|
3092
|
+
* here; they carry their own runtime resolution + fail-closed instruction via
|
|
3093
|
+
* the injected per-call resolution line.
|
|
3094
|
+
*/
|
|
3095
|
+
function _assertRoleResolvable(role, availableRoles, runtime, sourceDescription) {
|
|
3096
|
+
if (!availableRoles) {
|
|
3097
|
+
throw new Error(
|
|
3098
|
+
`${runtime} workflow install: could not resolve the shipped agents/ directory to validate named-role ` +
|
|
3099
|
+
'dispatch references — refusing to install (fail-closed, #2284)',
|
|
3100
|
+
);
|
|
3101
|
+
}
|
|
3102
|
+
if (role.startsWith('gsd-') && !availableRoles.has(role)) {
|
|
3103
|
+
throw new Error(
|
|
3104
|
+
`${runtime} workflow install: dispatch references role "${role}" via ${sourceDescription}, but no ` +
|
|
3105
|
+
`matching agents/${role}.md prompt file is shipped — refusing to install a workflow that dispatches ` +
|
|
3106
|
+
'an unresolvable role (fail-closed, #2284)',
|
|
3107
|
+
);
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
/**
|
|
3112
|
+
* Segment `text` into 'code' and 'string' runs (recognizes `"..."`, `'...'`,
|
|
3113
|
+
* and Python-style `"""..."""`, with backslash-escaping). Required because
|
|
3114
|
+
* the real corpus embeds unescaped parens inside quoted prompt bodies (e.g.
|
|
3115
|
+
* discuss-phase-assumptions.md's `(e.g., "Technical Approach")` inside a
|
|
3116
|
+
* `"""`-quoted prompt) — naive paren/keyword scanning across raw text would
|
|
3117
|
+
* desync on these. Downstream call-span detection and header-token
|
|
3118
|
+
* extraction operate on a same-length MASK derived from this segmentation
|
|
3119
|
+
* (see `maskStringLiterals`) so string content can never be mistaken for
|
|
3120
|
+
* call structure.
|
|
3121
|
+
*/
|
|
3122
|
+
function _segmentCodeAndStrings(text) {
|
|
3123
|
+
const segments = [];
|
|
3124
|
+
let i = 0;
|
|
3125
|
+
let segStart = 0;
|
|
3126
|
+
const flushCode = (end) => { if (end > segStart) segments.push({ type: 'code', start: segStart, end }); };
|
|
3127
|
+
while (i < text.length) {
|
|
3128
|
+
const ch = text[i];
|
|
3129
|
+
if (ch === '"' && text[i + 1] === '"' && text[i + 2] === '"') {
|
|
3130
|
+
flushCode(i);
|
|
3131
|
+
const strStart = i;
|
|
3132
|
+
i += 3;
|
|
3133
|
+
while (i < text.length && !(text[i] === '"' && text[i + 1] === '"' && text[i + 2] === '"')) {
|
|
3134
|
+
i += text[i] === '\\' ? 2 : 1;
|
|
3135
|
+
}
|
|
3136
|
+
i = Math.min(i + 3, text.length);
|
|
3137
|
+
segments.push({ type: 'string', start: strStart, end: i, quoteLen: 3 });
|
|
3138
|
+
segStart = i;
|
|
3139
|
+
continue;
|
|
3140
|
+
}
|
|
3141
|
+
// Only `"` is recognized as a single-char string delimiter — NOT `'`.
|
|
3142
|
+
// The corpus is markdown prose, not code: apostrophes are routine English
|
|
3143
|
+
// contractions/possessives ("install's", "don't") and treating them as
|
|
3144
|
+
// string delimiters would swallow everything up to the next unrelated
|
|
3145
|
+
// apostrophe as "inside a string" (verified against the real corpus —
|
|
3146
|
+
// this was a real, disqualifying bug during development of this fix).
|
|
3147
|
+
// Every real call-argument value in the corpus uses `"`/`"""` only.
|
|
3148
|
+
if (ch === '"') {
|
|
3149
|
+
flushCode(i);
|
|
3150
|
+
const strStart = i;
|
|
3151
|
+
i += 1;
|
|
3152
|
+
while (i < text.length && text[i] !== '"') {
|
|
3153
|
+
i += text[i] === '\\' ? 2 : 1;
|
|
3154
|
+
}
|
|
3155
|
+
i = Math.min(i + 1, text.length);
|
|
3156
|
+
segments.push({ type: 'string', start: strStart, end: i, quoteLen: 1 });
|
|
3157
|
+
segStart = i;
|
|
3158
|
+
continue;
|
|
3159
|
+
}
|
|
3160
|
+
i += 1;
|
|
3161
|
+
}
|
|
3162
|
+
flushCode(text.length);
|
|
3163
|
+
return segments;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
/**
|
|
3167
|
+
* Same-length mask of `text` with the INTERIOR of every string literal
|
|
3168
|
+
* replaced by a space (newlines preserved, so line-based regexes still work).
|
|
3169
|
+
* The delimiting quote character(s) themselves (`"`, `'`, `"""`) are kept
|
|
3170
|
+
* verbatim so a value-extraction regex like `key\s*[=:]\s*"[^"]*"` still
|
|
3171
|
+
* matches correctly against the mask — only the STRING CONTENT is blanked,
|
|
3172
|
+
* never the quote structure. Positions in the mask line up 1:1 with `text`,
|
|
3173
|
+
* so match indices/offsets found against the mask are valid offsets into the
|
|
3174
|
+
* original.
|
|
3175
|
+
*/
|
|
3176
|
+
function maskStringLiterals(text) {
|
|
3177
|
+
let mask = '';
|
|
3178
|
+
for (const seg of _segmentCodeAndStrings(text)) {
|
|
3179
|
+
const slice = text.slice(seg.start, seg.end);
|
|
3180
|
+
if (seg.type === 'code') { mask += slice; continue; }
|
|
3181
|
+
const q = seg.quoteLen;
|
|
3182
|
+
if (slice.length <= q) { mask += slice; continue; } // truncated/unterminated — keep verbatim
|
|
3183
|
+
const closeLen = Math.min(q, slice.length - q);
|
|
3184
|
+
const open = slice.slice(0, q);
|
|
3185
|
+
const close = slice.slice(slice.length - closeLen);
|
|
3186
|
+
const interiorLen = slice.length - q - closeLen;
|
|
3187
|
+
const interior = interiorLen > 0 ? slice.slice(q, q + interiorLen) : '';
|
|
3188
|
+
mask += open + interior.replace(/[^\n]/g, ' ') + close;
|
|
3189
|
+
}
|
|
3190
|
+
return mask;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
/**
|
|
3194
|
+
* Locate every `<headWord>(` / `<headWord>({` call span in `text`.
|
|
3195
|
+
*
|
|
3196
|
+
* #2284 round-2 CRITICAL fix: this MUST NOT rely on whole-document quote
|
|
3197
|
+
* parity. A markdown workflow file mixes prose, ```bash code fences (full of
|
|
3198
|
+
* their own double-quoted strings), and shell quoting — there is no single
|
|
3199
|
+
* document-wide quote grammar, so a `"`-heavy bash `echo` upstream of a real
|
|
3200
|
+
* call (e.g. code-review.md's fenced `echo "..."` block before its
|
|
3201
|
+
* `Agent(subagent_type="gsd-code-reviewer", ...)` call) can desync a
|
|
3202
|
+
* CUMULATIVE quote-state scan, making the scanner believe the real call's
|
|
3203
|
+
* `Agent(` sits "inside a string" and silently skipping it entirely — the
|
|
3204
|
+
* call then survives completely unnormalized. (Reproduced and root-caused
|
|
3205
|
+
* against the real corpus.)
|
|
3206
|
+
*
|
|
3207
|
+
* Fixed shape: find each `<headWord>(` occurrence via a PLAIN literal-text
|
|
3208
|
+
* search (`indexOf`, immune to any prior document content), then run a
|
|
3209
|
+
* balanced paren-matching scan whose quote-tracking state STARTS FRESH AT
|
|
3210
|
+
* THE HEAD — local to this one call, never inherited from (or able to be
|
|
3211
|
+
* corrupted by) anything earlier in the document. Handles all three real
|
|
3212
|
+
* corpus shapes: multi-line one-key-per-line, single-line object-literal
|
|
3213
|
+
* (`Agent({ ... })`), and single-line compact
|
|
3214
|
+
* (`Agent(subagent_type="x", model="y", prompt="...")`) — including prompt
|
|
3215
|
+
* bodies containing their own unescaped `()`/`{}` (skipped via the SAME
|
|
3216
|
+
* span-local quote tracking, e.g. discuss-phase-assumptions.md's
|
|
3217
|
+
* `"""`-quoted parenthetical prose).
|
|
3218
|
+
*
|
|
3219
|
+
* Returns `[{start, end, hasBraceWrapper}]` — `start`/`end` bound the FULL
|
|
3220
|
+
* call INCLUDING the head word and the closing `)`/`})`.
|
|
3221
|
+
*/
|
|
3222
|
+
function findDispatchCallSpans(text, headWord) {
|
|
3223
|
+
const spans = [];
|
|
3224
|
+
const headToken = `${headWord}(`;
|
|
3225
|
+
let searchFrom = 0;
|
|
3226
|
+
for (;;) {
|
|
3227
|
+
const start = text.indexOf(headToken, searchFrom);
|
|
3228
|
+
if (start === -1) break;
|
|
3229
|
+
const prevChar = start > 0 ? text[start - 1] : '';
|
|
3230
|
+
if (/[A-Za-z0-9_]/.test(prevChar)) { searchFrom = start + 1; continue; } // word-boundary guard
|
|
3231
|
+
|
|
3232
|
+
let i = start + headToken.length; // just past the '('
|
|
3233
|
+
let j = i;
|
|
3234
|
+
while (j < text.length && /\s/.test(text[j])) j++;
|
|
3235
|
+
const hasBraceWrapper = text[j] === '{';
|
|
3236
|
+
|
|
3237
|
+
// LOCAL scan — quote/paren state is fresh here, never inherited from
|
|
3238
|
+
// anything before `start` in the document.
|
|
3239
|
+
let parenDepth = 1;
|
|
3240
|
+
let inString = null; // null | '"' | 'triple'
|
|
3241
|
+
let end = -1;
|
|
3242
|
+
for (; i < text.length; i++) {
|
|
3243
|
+
const ch = text[i];
|
|
3244
|
+
if (inString) {
|
|
3245
|
+
if (ch === '\\') { i++; continue; }
|
|
3246
|
+
if (inString === 'triple') {
|
|
3247
|
+
if (ch === '"' && text[i + 1] === '"' && text[i + 2] === '"') { inString = null; i += 2; }
|
|
3248
|
+
continue;
|
|
3249
|
+
}
|
|
3250
|
+
if (ch === inString) inString = null;
|
|
3251
|
+
continue;
|
|
3252
|
+
}
|
|
3253
|
+
if (ch === '"' && text[i + 1] === '"' && text[i + 2] === '"') { inString = 'triple'; i += 2; continue; }
|
|
3254
|
+
if (ch === '"') { inString = '"'; continue; }
|
|
3255
|
+
if (ch === '(') { parenDepth++; continue; }
|
|
3256
|
+
if (ch === ')') {
|
|
3257
|
+
parenDepth--;
|
|
3258
|
+
if (parenDepth === 0) { end = i + 1; break; }
|
|
3259
|
+
continue;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
if (end === -1) { searchFrom = start + 1; continue; } // unterminated — skip past, keep scanning
|
|
3263
|
+
spans.push({ start, end, hasBraceWrapper });
|
|
3264
|
+
searchFrom = end;
|
|
3265
|
+
}
|
|
3266
|
+
return spans;
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
/**
|
|
3270
|
+
* Remove a call argument's `[matchStart, matchEnd)` token from `spanText`,
|
|
3271
|
+
* consuming its surrounding comma/whitespace so no dangling `, ,` or trailing
|
|
3272
|
+
* comment survives. When the argument owns its whole line, the whole line
|
|
3273
|
+
* (including a trailing inline `# comment`) is removed; `consumeLeadingComments`
|
|
3274
|
+
* additionally removes contiguous comment-only lines immediately ABOVE it —
|
|
3275
|
+
* #2284 Finding 5: explanatory prose describing a now-removed conditional
|
|
3276
|
+
* (e.g. execute-phase.md's "# Only include model= when ...") must not survive
|
|
3277
|
+
* describing a branch that no longer exists. Inline (single-line-compact /
|
|
3278
|
+
* object-literal) occurrences instead eat one adjacent comma.
|
|
3279
|
+
*/
|
|
3280
|
+
function _stripCallArgument(spanText, matchStart, matchEnd, { consumeLeadingComments = false } = {}) {
|
|
3281
|
+
let end = matchEnd;
|
|
3282
|
+
const afterRe = /^[ \t]*,?[ \t]*(#[^\n]*)?\r?\n?/;
|
|
3283
|
+
const afterMatch = afterRe.exec(spanText.slice(end));
|
|
3284
|
+
const hadTrailingComma = !!(afterMatch && /,/.test(afterMatch[0]));
|
|
3285
|
+
if (afterMatch) end += afterMatch[0].length;
|
|
3286
|
+
|
|
3287
|
+
let start = matchStart;
|
|
3288
|
+
const lineStart = spanText.lastIndexOf('\n', start - 1) + 1;
|
|
3289
|
+
const ownLine = /^[ \t]*$/.test(spanText.slice(lineStart, start));
|
|
3290
|
+
if (ownLine) {
|
|
3291
|
+
start = lineStart;
|
|
3292
|
+
if (consumeLeadingComments) {
|
|
3293
|
+
for (;;) {
|
|
3294
|
+
const prevLineStart = start > 0 ? spanText.lastIndexOf('\n', start - 2) + 1 : 0;
|
|
3295
|
+
const prevLine = spanText.slice(prevLineStart, start);
|
|
3296
|
+
if (/^[ \t]*#[^\n]*\r?\n$/.test(prevLine)) {
|
|
3297
|
+
start = prevLineStart;
|
|
3298
|
+
if (prevLineStart === 0) break;
|
|
3299
|
+
} else break;
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
} else if (!hadTrailingComma) {
|
|
3303
|
+
// Inline form and this was the LAST arg (no trailing comma) — eat a
|
|
3304
|
+
// leading comma so the previous arg doesn't dangle one.
|
|
3305
|
+
const before = spanText.slice(0, start);
|
|
3306
|
+
const cm = /,[ \t]*$/.exec(before);
|
|
3307
|
+
if (cm) start -= cm[0].length;
|
|
3308
|
+
}
|
|
3309
|
+
return spanText.slice(0, start) + spanText.slice(end);
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
/**
|
|
3313
|
+
* Replace a named-role argument token's `[matchStart, matchEnd)` span
|
|
3314
|
+
* (`subagent_type=`/`subagent_type:` + its value) with the projected
|
|
3315
|
+
* `gsd_role=` / role-prompt-resolution / structural-role argument group.
|
|
3316
|
+
* Preserves the pretty multi-line one-arg-per-line style when the original
|
|
3317
|
+
* token owned its own line; falls back to an inline, comma-joined group for
|
|
3318
|
+
* the single-line-compact and object-literal forms.
|
|
3319
|
+
*/
|
|
3320
|
+
function _projectRoleArgument(spanText, matchStart, matchEnd, roleValueExpr, toolConfig, canOrchestrate) {
|
|
3321
|
+
const { namedRoleParam, promptContentParam, structuralRoleParam, leafRoleValue } = toolConfig;
|
|
3322
|
+
const lineStart = spanText.lastIndexOf('\n', matchStart - 1) + 1;
|
|
3323
|
+
const startsOwnLine = /^[ \t]*$/.test(spanText.slice(lineStart, matchStart));
|
|
3324
|
+
|
|
3325
|
+
// Consume an immediately-following separator comma (+ same-line whitespace/
|
|
3326
|
+
// newline) into `end` — never leave it dangling AFTER an injected trailing
|
|
3327
|
+
// `# comment` (a bare `,` after `#...` would sit on the comment's own line,
|
|
3328
|
+
// outside any real argument list).
|
|
3329
|
+
let end = matchEnd;
|
|
3330
|
+
const afterRe = /^[ \t]*,[ \t]*\r?\n?/;
|
|
3331
|
+
const afterMatch = afterRe.exec(spanText.slice(end));
|
|
3332
|
+
const hadTrailingComma = !!afterMatch;
|
|
3333
|
+
if (afterMatch) end += afterMatch[0].length;
|
|
3334
|
+
const ownLine = startsOwnLine && hadTrailingComma && /\n$/.test(afterMatch[0]);
|
|
3335
|
+
|
|
3336
|
+
const promptContentPhrase =
|
|
3337
|
+
`${promptContentParam}=<resolve ${roleValueExpr} against the active install's gsd-* role prompts and load ` +
|
|
3338
|
+
'its contents; FAIL CLOSED with an explicit error if unresolved — never execute the role inline>';
|
|
3339
|
+
|
|
3340
|
+
let replacement;
|
|
3341
|
+
if (ownLine) {
|
|
3342
|
+
const indent = spanText.slice(lineStart, matchStart);
|
|
3343
|
+
const depthNote = canOrchestrate ? '' : ' # nested delegation is unavailable at this dispatch depth/toolkit';
|
|
3344
|
+
replacement =
|
|
3345
|
+
`${namedRoleParam}=${roleValueExpr},\n` +
|
|
3346
|
+
`${indent}${promptContentPhrase},\n` +
|
|
3347
|
+
`${indent}${structuralRoleParam}="${leafRoleValue}",${depthNote}\n`;
|
|
3348
|
+
} else {
|
|
3349
|
+
// Inline forms never carry a trailing `#` comment mid-argument-list (it
|
|
3350
|
+
// would silently "comment out" the remainder of the call), so the
|
|
3351
|
+
// depth/toolkit caveat is only ever emitted in the pretty own-line form.
|
|
3352
|
+
// Re-emit exactly the separator that originally followed this argument
|
|
3353
|
+
// (a comma if more args follow; nothing if it was the last one).
|
|
3354
|
+
replacement =
|
|
3355
|
+
`${namedRoleParam}=${roleValueExpr}, ${promptContentPhrase}, ${structuralRoleParam}="${leafRoleValue}"` +
|
|
3356
|
+
(hadTrailingComma ? ', ' : '');
|
|
3357
|
+
}
|
|
3358
|
+
return spanText.slice(0, matchStart) + replacement + spanText.slice(end);
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
// Matches a `subagent_type`/`model` argument's key+delimiter+value across all
|
|
3362
|
+
// three corpus forms: quoted-string values ("gsd-planner", "{model}") and
|
|
3363
|
+
// bare dynamic-expression values (ref.agent, research_hook.ref.agent,
|
|
3364
|
+
// executor_model). The captured group is always the value (a suffix of the
|
|
3365
|
+
// whole match), so its start offset is `match.index + match[0].length -
|
|
3366
|
+
// match[1].length` — avoids needing the regex `d` (indices) flag.
|
|
3367
|
+
function _callArgValueRe(key) {
|
|
3368
|
+
return new RegExp(`\\b${key}\\s*[=:]\\s*("(?:[^"\\\\]|\\\\.)*"|[A-Za-z_][\\w.]*)`);
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
/**
|
|
3372
|
+
* Returns the literal role name from a captured role-argument value EXPR
|
|
3373
|
+
* (e.g. `"gsd-planner"`) — or `null` when it is not a genuine static
|
|
3374
|
+
* literal: a bare dynamic expression (`ref.agent`), OR a quoted value that
|
|
3375
|
+
* still contains `{...}` template interpolation (the corpus's own
|
|
3376
|
+
* placeholder convention, e.g. `model="{researcher_model}"` — and,
|
|
3377
|
+
* critically, `subagent_type: "gsd-{agent}"` in
|
|
3378
|
+
* gsd-core/references/universal-anti-patterns.md, a DOCUMENTATION template
|
|
3379
|
+
* illustrating the naming pattern, never a concrete role to resolve).
|
|
3380
|
+
* Fail-closed validation only ever runs on a genuine static literal; a
|
|
3381
|
+
* template/dynamic value still gets the full role-prompt-resolution
|
|
3382
|
+
* projection treatment (the resolve+fail-closed instruction applies equally
|
|
3383
|
+
* once a template is substituted at runtime) — only the STATIC CHECK is
|
|
3384
|
+
* skipped, never the projection itself.
|
|
3385
|
+
*/
|
|
3386
|
+
function _literalRoleValue(roleValueExpr) {
|
|
3387
|
+
const m = /^"([^"]*)"$/.exec(roleValueExpr);
|
|
3388
|
+
if (!m) return null;
|
|
3389
|
+
if (/[{}]/.test(m[1])) return null;
|
|
3390
|
+
return m[1];
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
/**
|
|
3394
|
+
* `maskStringLiterals` PLUS `#`-to-end-of-line comment blanking (comments are
|
|
3395
|
+
* never string literals, so they survive string-masking as literal `#...`
|
|
3396
|
+
* text). Header-token searches (subagent_type/model/run_in_background) must
|
|
3397
|
+
* use THIS mask, not the string-only one — verified necessary against the
|
|
3398
|
+
* real corpus: execute-phase.md's explanatory comment "# Only include
|
|
3399
|
+
* model= when executor_model is..." literally contains the substring
|
|
3400
|
+
* "model= when", which a comment-blind `model` regex mismatches as a real
|
|
3401
|
+
* `model=when` argument, corrupting the comment AND missing the real
|
|
3402
|
+
* `model="{executor_model}"` line beneath it. Scoped to call-span text only
|
|
3403
|
+
* (never the whole document), so markdown `#`/`##` headings elsewhere are
|
|
3404
|
+
* unaffected.
|
|
3405
|
+
*/
|
|
3406
|
+
function _maskStringsAndComments(text) {
|
|
3407
|
+
return maskStringLiterals(text).replace(/#[^\n]*/g, (m) => ' '.repeat(m.length));
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
/**
|
|
3411
|
+
* Normalize ONE `Agent(...)`/`Agent({...})` call span (already isolated by
|
|
3412
|
+
* `findDispatchCallSpans`) onto the target's real dispatch primitive. Every
|
|
3413
|
+
* behavioral branch reads `dispatch` (the runtime's sourced
|
|
3414
|
+
* `hostIntegration.dispatch` facts) — none is hardcoded. Handles all three
|
|
3415
|
+
* corpus call-argument shapes uniformly via string-aware token location
|
|
3416
|
+
* (`maskStringLiterals` recomputed after each structural edit, since prior
|
|
3417
|
+
* edits shift offsets).
|
|
3418
|
+
*/
|
|
3419
|
+
function _normalizeDispatchCallSpan(spanText, hasBraceWrapper, dispatch, toolConfig) {
|
|
3420
|
+
const namedDispatch = dispatch.namedDispatch === true;
|
|
3421
|
+
const backgroundCapable = dispatch.background === true;
|
|
3422
|
+
const canOrchestrate = dispatch.subagentToolkit === 'full'
|
|
3423
|
+
&& (dispatch.maxDepth === -1 || (typeof dispatch.maxDepth === 'number' && dispatch.maxDepth > 1));
|
|
3424
|
+
const { toolName, backgroundParam, supportsPerCallModel, availableRoles, runtime } = toolConfig;
|
|
3425
|
+
|
|
3426
|
+
let text = spanText;
|
|
3427
|
+
|
|
3428
|
+
// 1. Named-role argument (subagent_type= / subagent_type:) — only when the
|
|
3429
|
+
// target has no native named-agent lookup (dispatch.namedDispatch).
|
|
3430
|
+
// Fail-closed validation runs on the extracted value REGARDLESS of
|
|
3431
|
+
// which source syntax produced it (#2284 requirement 2).
|
|
3432
|
+
if (!namedDispatch) {
|
|
3433
|
+
const roleRe = _callArgValueRe('subagent_type');
|
|
3434
|
+
const rm = roleRe.exec(_maskStringsAndComments(text));
|
|
3435
|
+
if (rm) {
|
|
3436
|
+
// Read the VALUE from the original (unmasked) text at the matched
|
|
3437
|
+
// offset — `rm[1]` was captured against the mask, whose string
|
|
3438
|
+
// INTERIOR is blanked, so it must never be used as the real value.
|
|
3439
|
+
const roleValueExpr = text.slice(rm.index + rm[0].length - rm[1].length, rm.index + rm[0].length);
|
|
3440
|
+
const literalRole = _literalRoleValue(roleValueExpr);
|
|
3441
|
+
if (literalRole !== null) {
|
|
3442
|
+
_assertRoleResolvable(literalRole, availableRoles, runtime, 'subagent_type');
|
|
3443
|
+
} else if (!availableRoles) {
|
|
3444
|
+
// No literal value to check, but a null availableRoles still means
|
|
3445
|
+
// the shipped agents/ dir couldn't be resolved at all — fail closed
|
|
3446
|
+
// unconditionally rather than silently install an unverifiable call.
|
|
3447
|
+
_assertRoleResolvable('', availableRoles, runtime, 'subagent_type');
|
|
3448
|
+
}
|
|
3449
|
+
text = _projectRoleArgument(text, rm.index, rm.index + rm[0].length, roleValueExpr, toolConfig, canOrchestrate);
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
// 2. Per-call model argument (model= / model:) — stripped entirely when the
|
|
3454
|
+
// target has no per-call model-selection parameter (there is no
|
|
3455
|
+
// `dispatch` axis for this — it is inherent tool vocabulary, like the
|
|
3456
|
+
// parameter names themselves). Also removes now-dead explanatory
|
|
3457
|
+
// comment lines directly above a `model=` line that owns its own line
|
|
3458
|
+
// (#2284 Finding 5).
|
|
3459
|
+
if (!supportsPerCallModel) {
|
|
3460
|
+
const modelRe = _callArgValueRe('model');
|
|
3461
|
+
const mm = modelRe.exec(_maskStringsAndComments(text));
|
|
3462
|
+
if (mm) {
|
|
3463
|
+
text = _stripCallArgument(text, mm.index, mm.index + mm[0].length, { consumeLeadingComments: true });
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
// 3. Background-dispatch flag (run_in_background= / run_in_background:) —
|
|
3468
|
+
// maps onto the target's own background parameter ONLY when documented
|
|
3469
|
+
// to support it; otherwise stripped rather than forwarding a parameter
|
|
3470
|
+
// the primitive doesn't accept.
|
|
3471
|
+
{
|
|
3472
|
+
const bgRe = /\brun_in_background\s*[=:]\s*(?:true|false)/;
|
|
3473
|
+
const bm = bgRe.exec(_maskStringsAndComments(text));
|
|
3474
|
+
if (bm) {
|
|
3475
|
+
if (backgroundCapable) {
|
|
3476
|
+
const matched = text.slice(bm.index, bm.index + bm[0].length);
|
|
3477
|
+
const replaced = matched.replace(/^run_in_background(\s*[=:]\s*)/, `${backgroundParam}$1`);
|
|
3478
|
+
text = text.slice(0, bm.index) + replaced + text.slice(bm.index + bm[0].length);
|
|
3479
|
+
} else {
|
|
3480
|
+
text = _stripCallArgument(text, bm.index, bm.index + bm[0].length);
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
// 4. Call-syntax head rename + object-literal brace stripping. Hermes's
|
|
3486
|
+
// delegate_task is a flat kwarg call — `Agent({...})`'s wrapper braces
|
|
3487
|
+
// are dropped rather than carried through, so every projected call ends
|
|
3488
|
+
// up in the same flat shape regardless of source syntax.
|
|
3489
|
+
text = text.replace(/^Agent\(/, `${toolName}(`);
|
|
3490
|
+
if (hasBraceWrapper) {
|
|
3491
|
+
const openMask = maskStringLiterals(text);
|
|
3492
|
+
const braceOpenIdx = openMask.indexOf('{');
|
|
3493
|
+
if (braceOpenIdx !== -1) text = text.slice(0, braceOpenIdx) + text.slice(braceOpenIdx + 1);
|
|
3494
|
+
const closeMask = maskStringLiterals(text);
|
|
3495
|
+
const braceCloseIdx = closeMask.lastIndexOf('}');
|
|
3496
|
+
if (braceCloseIdx !== -1) text = text.slice(0, braceCloseIdx) + text.slice(braceCloseIdx + 1);
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
return text;
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
/**
|
|
3503
|
+
* Blank the interior (and delimiters) of every string literal inside
|
|
3504
|
+
* `spanText` to spaces — same length, newlines preserved — using a fresh,
|
|
3505
|
+
* LOCAL quote-tracking scan that starts at `spanText[0]` with NO inherited
|
|
3506
|
+
* state. This is deliberately the SAME state-machine shape as the
|
|
3507
|
+
* `inString`/`\\`/triple-quote handling inside `findDispatchCallSpans`
|
|
3508
|
+
* (double-quoted and `"""`-triple-quoted, backslash-escape aware) — reused
|
|
3509
|
+
* here so a call span's quoted argument VALUES (documentation prose, prompt
|
|
3510
|
+
* bodies) never masquerade as real call syntax, without EVER falling back to
|
|
3511
|
+
* a whole-document cumulative quote-parity mask (the round-2 defect
|
|
3512
|
+
* documented on `findDispatchCallSpans` above).
|
|
3513
|
+
*/
|
|
3514
|
+
function _blankStringLiteralInteriors(spanText) {
|
|
3515
|
+
let out = '';
|
|
3516
|
+
let inString = null; // null | '"' | 'triple'
|
|
3517
|
+
for (let i = 0; i < spanText.length; i++) {
|
|
3518
|
+
const ch = spanText[i];
|
|
3519
|
+
if (inString) {
|
|
3520
|
+
if (ch === '\\') {
|
|
3521
|
+
out += ' ';
|
|
3522
|
+
i++;
|
|
3523
|
+
if (i < spanText.length) out += (spanText[i] === '\n') ? '\n' : ' ';
|
|
3524
|
+
continue;
|
|
3525
|
+
}
|
|
3526
|
+
if (inString === 'triple') {
|
|
3527
|
+
if (ch === '"' && spanText[i + 1] === '"' && spanText[i + 2] === '"') {
|
|
3528
|
+
inString = null;
|
|
3529
|
+
out += ' ';
|
|
3530
|
+
i += 2;
|
|
3531
|
+
continue;
|
|
3532
|
+
}
|
|
3533
|
+
out += (ch === '\n') ? '\n' : ' ';
|
|
3534
|
+
continue;
|
|
3535
|
+
}
|
|
3536
|
+
if (ch === inString) { inString = null; out += ' '; continue; }
|
|
3537
|
+
out += (ch === '\n') ? '\n' : ' ';
|
|
3538
|
+
continue;
|
|
3539
|
+
}
|
|
3540
|
+
if (ch === '"' && spanText[i + 1] === '"' && spanText[i + 2] === '"') {
|
|
3541
|
+
inString = 'triple';
|
|
3542
|
+
out += ' ';
|
|
3543
|
+
i += 2;
|
|
3544
|
+
continue;
|
|
3545
|
+
}
|
|
3546
|
+
if (ch === '"') { inString = '"'; out += ' '; continue; }
|
|
3547
|
+
out += ch;
|
|
3548
|
+
}
|
|
3549
|
+
return out;
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
/**
|
|
3553
|
+
* Quote-aware view of `content` for the completeness checks below: for every
|
|
3554
|
+
* REAL call span located via `findDispatchCallSpans` (once per head word in
|
|
3555
|
+
* `headWords`), the string-literal ARGUMENT VALUES inside that span are
|
|
3556
|
+
* blanked via `_blankStringLiteralInteriors`; the call's own head word and
|
|
3557
|
+
* bare (unquoted) argument tokens are left untouched. `headWords` is
|
|
3558
|
+
* processed in order and each pass re-scans the PROGRESSIVELY-masked string
|
|
3559
|
+
* — `toolName` first, then `'Agent'` — so a spurious `Agent(` that
|
|
3560
|
+
* `findDispatchCallSpans('Agent')` would otherwise "find" purely because it
|
|
3561
|
+
* sits inside an outer call's quoted string (e.g. a `description="...Agent()
|
|
3562
|
+
* ...subagent_type=x"` argument value) has ALREADY been blanked away by the
|
|
3563
|
+
* outer `toolName` pass by the time the `'Agent'` pass runs, so it is never
|
|
3564
|
+
* mistaken for a real, independent call. A genuinely real (unquoted) `Agent(`
|
|
3565
|
+
* — including one nested as a raw, un-renamed argument value — survives every
|
|
3566
|
+
* pass and remains visible to the caller's regex checks.
|
|
3567
|
+
*/
|
|
3568
|
+
function _maskQuotedRegionsWithinCallSpans(content, headWords) {
|
|
3569
|
+
let masked = content;
|
|
3570
|
+
for (const headWord of headWords) {
|
|
3571
|
+
const spans = findDispatchCallSpans(masked, headWord);
|
|
3572
|
+
for (let i = spans.length - 1; i >= 0; i--) {
|
|
3573
|
+
const { start, end } = spans[i];
|
|
3574
|
+
const maskedSpan = _blankStringLiteralInteriors(masked.slice(start, end));
|
|
3575
|
+
masked = masked.slice(0, start) + maskedSpan + masked.slice(end);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
return masked;
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
/**
|
|
3582
|
+
* Post-projection guard (#2284 requirement 3 — belt-and-suspenders): after
|
|
3583
|
+
* projection, assert the corpus form the projection could not anticipate
|
|
3584
|
+
* never silently ships. Throws an explicit install error (fail-LOUD) rather
|
|
3585
|
+
* than let an unprojected/incompletely-projected dispatch call install.
|
|
3586
|
+
*
|
|
3587
|
+
* #2284 round-2 CRITICAL fix: this is an INDEPENDENT check — it does NOT use
|
|
3588
|
+
* `maskStringLiterals` over the whole document (the round-1 primitive whose
|
|
3589
|
+
* cumulative, document-wide quote-parity tracking was the root cause of the
|
|
3590
|
+
* round-2 defect: a `"`-heavy bash fence upstream of a real call desynced
|
|
3591
|
+
* quote state and made `findDispatchCallSpans` blind to that call, shipping
|
|
3592
|
+
* a Frankenstein `Agent(gsd_role="...", model="...")` with no detection).
|
|
3593
|
+
*
|
|
3594
|
+
* #2284 round-3 fix: a BLUNT, mask-free literal check over the whole
|
|
3595
|
+
* document (round-2's fix) over-throws — it cannot tell a real residual
|
|
3596
|
+
* `Agent(`/`subagent_type` call from the SAME text appearing INSIDE a quoted
|
|
3597
|
+
* string (documentation/prompt prose, e.g. `description="...Agent()..."`).
|
|
3598
|
+
* The completeness checks (residual `subagent_type` / literal `Agent(`) now
|
|
3599
|
+
* run against `_maskQuotedRegionsWithinCallSpans` — quote-aware, but scoped
|
|
3600
|
+
* strictly to already-correctly-bounded, per-occurrence-LOCAL call spans
|
|
3601
|
+
* (never a whole-document cumulative mask), so a real Frankenstein call
|
|
3602
|
+
* (unquoted, real call syntax) still fires while a same-text mention genuinely
|
|
3603
|
+
* inside a quoted string does not.
|
|
3604
|
+
*
|
|
3605
|
+
* The completeness checks also only apply when `namedDispatch` is false: when
|
|
3606
|
+
* `dispatch.namedDispatch === true`, `_normalizeDispatchCallSpan` step 1
|
|
3607
|
+
* INTENTIONALLY leaves `subagent_type` unprojected (the target primitive
|
|
3608
|
+
* resolves named agents itself) — a residual `subagent_type` in that case is
|
|
3609
|
+
* the correct, intended output, not a defect. (The call HEAD is still renamed
|
|
3610
|
+
* unconditionally regardless of `namedDispatch` — see step 4 there — so a
|
|
3611
|
+
* literal `Agent(` residual is gated the same way purely for symmetry with
|
|
3612
|
+
* the dispatch-facts-driven contract; it is never actually left unrenamed by
|
|
3613
|
+
* the projection in practice.)
|
|
3614
|
+
*
|
|
3615
|
+
* The model-leak check is unaffected by either fix above — it is orthogonal
|
|
3616
|
+
* to `namedDispatch` (gated only by `supportsPerCallModel`) and already
|
|
3617
|
+
* bounds each real call via the independently-fixed, per-occurrence-local,
|
|
3618
|
+
* non-cumulative `findDispatchCallSpans`, then does a raw substring check
|
|
3619
|
+
* within that bound.
|
|
3620
|
+
*/
|
|
3621
|
+
function _assertProjectionComplete(content, toolConfig, namedDispatch = false) {
|
|
3622
|
+
const { toolName, runtime, supportsPerCallModel } = toolConfig;
|
|
3623
|
+
|
|
3624
|
+
if (!namedDispatch) {
|
|
3625
|
+
const quoteAware = _maskQuotedRegionsWithinCallSpans(content, [toolName, 'Agent']);
|
|
3626
|
+
if (/\bsubagent_type\s*[=:]/.test(quoteAware)) {
|
|
3627
|
+
throw new Error(
|
|
3628
|
+
`${runtime} workflow install: projection left a residual subagent_type reference — refusing to install ` +
|
|
3629
|
+
'(fail-closed post-projection guard, #2284)',
|
|
3630
|
+
);
|
|
3631
|
+
}
|
|
3632
|
+
if (/\bAgent\(/.test(quoteAware)) {
|
|
3633
|
+
throw new Error(
|
|
3634
|
+
`${runtime} workflow install: projection left literal Agent( call syntax — refusing to install ` +
|
|
3635
|
+
'(fail-closed post-projection guard, #2284)',
|
|
3636
|
+
);
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
if (!supportsPerCallModel) {
|
|
3641
|
+
for (const span of findDispatchCallSpans(content, toolName)) {
|
|
3642
|
+
const rawSpanText = content.slice(span.start, span.end);
|
|
3643
|
+
if (/\bmodel\s*[=:]/.test(rawSpanText)) {
|
|
3644
|
+
throw new Error(
|
|
3645
|
+
`${runtime} workflow install: projection left a leaked model= argument inside a ${toolName}(...) call ` +
|
|
3646
|
+
'— refusing to install (fail-closed post-projection guard, #2284)',
|
|
3647
|
+
);
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
/**
|
|
3654
|
+
* Project host-neutral `Agent(...)` named-subagent dispatch prose onto a
|
|
3655
|
+
* target runtime's real dispatch primitive. See the file-header comment above
|
|
3656
|
+
* for the governing rule: every behavioral branch reads `dispatch` (the
|
|
3657
|
+
* runtime's sourced `hostIntegration.dispatch` facts) — none is a hardcoded
|
|
3658
|
+
* assumption about a specific runtime. Handles all three real corpus call
|
|
3659
|
+
* forms (multi-line one-key-per-line, single-line object-literal, single-line
|
|
3660
|
+
* compact) via string-aware call-span detection rather than three independent
|
|
3661
|
+
* line-anchored regexes, and closes with a post-projection guard that fails
|
|
3662
|
+
* loud on any form it did not anticipate (#2284).
|
|
3663
|
+
*
|
|
3664
|
+
* @param {string} content
|
|
3665
|
+
* @param {{namedDispatch?: boolean, nested?: boolean, maxDepth?: number, background?: boolean, backgroundDispatch?: boolean, subagentToolkit?: string}} dispatch
|
|
3666
|
+
* @param {{toolName: string, namedRoleParam: string, promptContentParam: string, structuralRoleParam: string, leafRoleValue: string, backgroundParam: string, supportsPerCallModel: boolean, availableRoles: Set<string>|null, runtime: string}} toolConfig
|
|
3667
|
+
*/
|
|
3668
|
+
function projectNamedDispatchToStructuralDelegate(content, dispatch, toolConfig) {
|
|
3669
|
+
const d = dispatch || {};
|
|
3670
|
+
const namedDispatch = d.namedDispatch === true;
|
|
3671
|
+
const backgroundCapable = d.background === true;
|
|
3672
|
+
const { toolName, promptContentParam } = toolConfig;
|
|
3673
|
+
|
|
3674
|
+
let converted = content;
|
|
3675
|
+
|
|
3676
|
+
// 1. The "Agent tool IS available" contract assertion (currently unique to
|
|
3677
|
+
// plan-phase.md, matched generically in case of future reuse elsewhere).
|
|
3678
|
+
const assertionRe = /The Agent tool IS available in a top-level ([^\n]+?) session\.\s+Always spawn\s+([\s\S]*?)\s+as separate Agent\(\) calls\./;
|
|
3679
|
+
converted = converted.replace(assertionRe, (_m, sessionName, roster) => {
|
|
3680
|
+
const rosterFlat = roster.replace(/\s+/g, ' ').trim();
|
|
3681
|
+
if (namedDispatch) {
|
|
3682
|
+
return `The \`${toolName}\` tool IS available in a top-level ${sessionName} session. Always dispatch ${rosterFlat} as separate \`${toolName}()\` calls.`;
|
|
3683
|
+
}
|
|
3684
|
+
return (
|
|
3685
|
+
`${sessionName} has no \`Agent\` tool. It exposes \`${toolName}\`, which dispatches by structural role — ` +
|
|
3686
|
+
'it has no concept of a named subagent identity. GSD projects each named gsd-* role onto this primitive ' +
|
|
3687
|
+
`itself: resolve the role's prompt file from the active install, load its contents, and embed them in the ` +
|
|
3688
|
+
`\`${toolName}\` payload via \`${promptContentParam}\` as the dispatched task's operating instructions. ` +
|
|
3689
|
+
'FAIL CLOSED — surface an explicit error and stop — if a referenced role prompt cannot be resolved; never ' +
|
|
3690
|
+
`execute the role inline as a substitute. In a top-level ${sessionName} session, always dispatch ` +
|
|
3691
|
+
`${rosterFlat} as separate \`${toolName}\` calls.`
|
|
3692
|
+
);
|
|
3693
|
+
});
|
|
3694
|
+
|
|
3695
|
+
// 1b. Dispatch-depth-availability prose immediately adjacent to a renamed
|
|
3696
|
+
// `Agent()` mention in the SAME sentence (plan-review-convergence.md
|
|
3697
|
+
// ~lines 108, 347, 355) — a bare "Agent" left un-renamed right next to
|
|
3698
|
+
// the projection's own `Agent()`→`${toolName}()` rename produced
|
|
3699
|
+
// self-contradictory installed text (e.g. "...delegate_task(...)...
|
|
3700
|
+
// with Agent available..."). Narrowly scoped to the EXACT known
|
|
3701
|
+
// phrases the projection itself creates the inconsistency beside —
|
|
3702
|
+
// never a broad bare-word `Agent` rename, which would corrupt
|
|
3703
|
+
// legitimate `Agent`-adjacent prose elsewhere in the corpus (role
|
|
3704
|
+
// names, "Agent Brief", agent-file references).
|
|
3705
|
+
converted = converted.replace(
|
|
3706
|
+
/\borchestrator runs at depth 0 with Agent available\b/g,
|
|
3707
|
+
`orchestrator runs at depth 0 with ${toolName} available`,
|
|
3708
|
+
);
|
|
3709
|
+
converted = converted.replace(
|
|
3710
|
+
/\(bug #936: depth-1 Agent has no Agent tool\)/g,
|
|
3711
|
+
`(bug #936: depth-1 ${toolName} has no nested ${toolName})`,
|
|
3712
|
+
);
|
|
3713
|
+
|
|
3714
|
+
// 2. Per-call model-selection prose ("Model resolution:" paragraph,
|
|
3715
|
+
// execute-phase.md) + inline backtick-quoted model-mention prose
|
|
3716
|
+
// examples (not live call sites) — only rewritten when the target
|
|
3717
|
+
// primitive has no per-call model parameter at all.
|
|
3718
|
+
if (!toolConfig.supportsPerCallModel) {
|
|
3719
|
+
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"`\)\./;
|
|
3720
|
+
converted = converted.replace(
|
|
3721
|
+
modelResolutionRe,
|
|
3722
|
+
`**Model resolution:** \`${toolName}\` has no per-call model-selection parameter — every dispatched role ` +
|
|
3723
|
+
`always inherits the host session's active model. Never pass \`model=\` to \`${toolName}\`; drop the ` +
|
|
3724
|
+
'`executor_model` value entirely for this runtime.',
|
|
3725
|
+
);
|
|
3726
|
+
converted = converted.replace(/`model="[^"`\n]*"`,?\s*(?:and\s+)?/g, '');
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
// 3. Background-dispatch PROSE mentions outside any real call span (e.g.
|
|
3730
|
+
// execute-phase.md:595,600 — `run_in_background: true` inline
|
|
3731
|
+
// documentation, not a call argument) — #2284 Finding 3. Only rewritten
|
|
3732
|
+
// when the target is documented to support background dispatch (a
|
|
3733
|
+
// prose mention of an unsupported capability would be equally
|
|
3734
|
+
// misleading as a real leaked argument).
|
|
3735
|
+
if (backgroundCapable) {
|
|
3736
|
+
converted = converted.replace(
|
|
3737
|
+
/\brun_in_background(\s*[=:]\s*(?:true|false))/g,
|
|
3738
|
+
`${toolConfig.backgroundParam}$1`,
|
|
3739
|
+
);
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
// 4. Call-span-based normalization — the core of the fix. Every
|
|
3743
|
+
// `Agent(...)`/`Agent({...})` occurrence (all three corpus forms) is
|
|
3744
|
+
// located via string-aware balanced paren/brace matching, then
|
|
3745
|
+
// normalized as a unit; spans are rebuilt right-to-left so earlier
|
|
3746
|
+
// offsets stay valid while later ones are rewritten.
|
|
3747
|
+
const spans = findDispatchCallSpans(converted, 'Agent');
|
|
3748
|
+
for (let i = spans.length - 1; i >= 0; i--) {
|
|
3749
|
+
const { start, end, hasBraceWrapper } = spans[i];
|
|
3750
|
+
const rebuilt = _normalizeDispatchCallSpan(converted.slice(start, end), hasBraceWrapper, d, toolConfig);
|
|
3751
|
+
converted = converted.slice(0, start) + rebuilt + converted.slice(end);
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3754
|
+
// 5. "Agent tool" capability mentions (conditions gating parallel vs.
|
|
3755
|
+
// sequential dispatch, e.g. map-codebase.md) → the real target primitive
|
|
3756
|
+
// name, which resolves these conditions accurately since it IS a real,
|
|
3757
|
+
// always-available dispatch primitive for this target.
|
|
3758
|
+
converted = converted.replace(/\bAgent tool\b/g, toolName);
|
|
3759
|
+
|
|
3760
|
+
// 5b. Catch-all: a `subagent_type` mention that is NOT part of any real
|
|
3761
|
+
// `Agent(...)` call span (e.g. map-codebase.md's inline documentation
|
|
3762
|
+
// prose ``Use Agent tool with `subagent_type="X"`, ...`` — disconnected
|
|
3763
|
+
// example syntax, not a live call). Renamed for the same accuracy the
|
|
3764
|
+
// real calls get; a literal quoted role value is still fail-closed
|
|
3765
|
+
// validated even though there is no call structure to inject
|
|
3766
|
+
// role-prompt/fail-closed guidance INTO.
|
|
3767
|
+
if (!namedDispatch) {
|
|
3768
|
+
converted = converted.replace(
|
|
3769
|
+
/\bsubagent_type(\s*[=:]\s*"[^"]*")/g,
|
|
3770
|
+
(_m, rest) => {
|
|
3771
|
+
const literalRole = _literalRoleValue(rest.replace(/^\s*[=:]\s*/, ''));
|
|
3772
|
+
if (literalRole !== null) {
|
|
3773
|
+
_assertRoleResolvable(literalRole, toolConfig.availableRoles, toolConfig.runtime, 'subagent_type (prose mention)');
|
|
3774
|
+
}
|
|
3775
|
+
return `${toolConfig.namedRoleParam}${rest}`;
|
|
3776
|
+
},
|
|
3777
|
+
);
|
|
3778
|
+
converted = converted.replace(/\bsubagent_type(\s*[=:])/g, `${toolConfig.namedRoleParam}$1`);
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
// 5c. Safety net (#2284 requirement 2): the PRIMARY mechanism for
|
|
3782
|
+
// eliminating literal `Agent(` syntax is complete span detection (step
|
|
3783
|
+
// 4) — this unconditional final rename exists only so that even a call
|
|
3784
|
+
// span detection somehow misses at least loses its `Agent(` head
|
|
3785
|
+
// rather than shipping the literal Claude-shaped tool name verbatim.
|
|
3786
|
+
// A call caught only by this safety net is still INCOMPLETELY
|
|
3787
|
+
// normalized (no role/model handling) and gets caught by the
|
|
3788
|
+
// independent post-projection guard below via its OTHER invariants
|
|
3789
|
+
// (residual subagent_type / leaked model=), which this safety net does
|
|
3790
|
+
// not touch — the install still fails closed for a missed span.
|
|
3791
|
+
converted = converted.replace(/\bAgent\(/g, `${toolName}(`);
|
|
3792
|
+
|
|
3793
|
+
// 6. Post-projection guard (#2284 requirement 3): fail loud, never ship
|
|
3794
|
+
// silently, on any residual/leaked form the projection above did not
|
|
3795
|
+
// anticipate. `namedDispatch` gates the completeness checks — a
|
|
3796
|
+
// residual subagent_type is INTENTIONAL, not a defect, when the target
|
|
3797
|
+
// resolves named agents itself (see `_assertProjectionComplete`).
|
|
3798
|
+
_assertProjectionComplete(converted, toolConfig, namedDispatch);
|
|
3799
|
+
|
|
3800
|
+
return converted;
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
const HERMES_DISPATCH_TOOL_CONFIG = Object.freeze({
|
|
3804
|
+
toolName: 'delegate_task',
|
|
3805
|
+
namedRoleParam: 'gsd_role',
|
|
3806
|
+
promptContentParam: 'gsd_role_prompt',
|
|
3807
|
+
structuralRoleParam: 'role',
|
|
3808
|
+
leafRoleValue: 'leaf',
|
|
3809
|
+
backgroundParam: 'background',
|
|
3810
|
+
supportsPerCallModel: false,
|
|
3811
|
+
});
|
|
3812
|
+
|
|
3813
|
+
/**
|
|
3814
|
+
* Hermes `.md` content converter (#2284): brand-swap (unchanged behavior,
|
|
3815
|
+
* descriptor-driven per `hostBehaviors.brandingRewrites`) followed by the
|
|
3816
|
+
* generic named-dispatch → `delegate_task` projection above, driven by
|
|
3817
|
+
* `capabilities/hermes/capability.json`'s `hostIntegration.dispatch` (read
|
|
3818
|
+
* via `_hostIntegrationDispatch`, values UNCHANGED by this fix — they are
|
|
3819
|
+
* already documentation-sourced and correct).
|
|
3820
|
+
*/
|
|
3821
|
+
function convertClaudeToHermesMarkdown(content, ctx) {
|
|
3822
|
+
const runtime = (ctx && ctx.runtime) || 'hermes';
|
|
3823
|
+
const b = _hostBehaviors(runtime).brandingRewrites;
|
|
3824
|
+
let converted = content;
|
|
3825
|
+
if (b) {
|
|
3826
|
+
converted = converted.replace(/CLAUDE\.md/g, b['CLAUDE.md']);
|
|
3827
|
+
// #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
|
|
3828
|
+
converted = applyClaudeCodeBrandSwap(converted, b['Claude Code']);
|
|
3829
|
+
converted = converted.replace(/\.claude\//g, b['.claude/']);
|
|
3830
|
+
}
|
|
3831
|
+
const dispatch = _hostIntegrationDispatch(runtime);
|
|
3832
|
+
const toolConfig = Object.assign({}, HERMES_DISPATCH_TOOL_CONFIG, {
|
|
3833
|
+
availableRoles: _resolveAvailableGsdRoles(),
|
|
3834
|
+
runtime,
|
|
3835
|
+
});
|
|
3836
|
+
return projectNamedDispatchToStructuralDelegate(converted, dispatch, toolConfig);
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
// ── End Hermes converters ────────────────────────────────────────────────────
|
|
3840
|
+
|
|
2891
3841
|
function convertSlashCommandsToCodexSkillMentions(content) {
|
|
2892
3842
|
// Colon-style /gsd: never appears as a filesystem path segment, so no boundary guard is needed (unlike the hyphen-style below).
|
|
2893
3843
|
let converted = content.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => {
|
|
@@ -3089,6 +4039,36 @@ purpose: ${toSingleLine(description)}
|
|
|
3089
4039
|
return `${cleanFrontmatter}\n\n${roleHeader}\n${body}`;
|
|
3090
4040
|
}
|
|
3091
4041
|
|
|
4042
|
+
/**
|
|
4043
|
+
* #2310 — True if `model` is an Anthropic-flavored value that must never appear as a
|
|
4044
|
+
* Codex agent `.toml` `model`. Two forms: (a) a bare Claude Agent-tool tier alias
|
|
4045
|
+
* (opus/sonnet/haiku/fable — the canonical CLAUDE_AGENT_ALIASES, imported from
|
|
4046
|
+
* src/model-resolver.cts so it can't diverge); (b) any Claude model id in any provider
|
|
4047
|
+
* namespacing — `claude-*`, `anthropic/claude-*`, `us.anthropic.claude-*` (the forms the
|
|
4048
|
+
* catalog assigns to opencode/hermes/kilo, reachable on a Codex .toml via the runtime-
|
|
4049
|
+
* resolver path). No OpenAI/Codex model id contains "claude", so a case-insensitive
|
|
4050
|
+
* substring test is a safe, exhaustive guard for (b). Codex/ChatGPT rejects all of these.
|
|
4051
|
+
*/
|
|
4052
|
+
function _isAnthropicFlavoredModel(model) {
|
|
4053
|
+
return typeof model === 'string' && (CLAUDE_AGENT_ALIASES.has(model) || model.toLowerCase().includes('claude'));
|
|
4054
|
+
}
|
|
4055
|
+
|
|
4056
|
+
// #2310 — dedupe stderr warnings so repeated agent emits don't spam (mirrors the
|
|
4057
|
+
// #2041/#1133 model-resolver warn-dedupe). Value is length-capped so an oversized
|
|
4058
|
+
// or secret-shaped override cannot leak in full to logs.
|
|
4059
|
+
const _codexModelOverrideDroppedWarned = new Set();
|
|
4060
|
+
function _warnCodexModelOverrideDropped(agentName, value) {
|
|
4061
|
+
const key = `${agentName}::${value}`;
|
|
4062
|
+
if (_codexModelOverrideDroppedWarned.has(key)) return;
|
|
4063
|
+
_codexModelOverrideDroppedWarned.add(key);
|
|
4064
|
+
const safe = String(value).length > 64 ? `${String(value).slice(0, 64)}…` : String(value);
|
|
4065
|
+
process.stderr.write(
|
|
4066
|
+
`gsd: warning — Codex agent "${agentName}" model "${safe}" is not a valid Codex model ` +
|
|
4067
|
+
`(Anthropic alias/id); dropping it so Codex uses a valid default. ` +
|
|
4068
|
+
`Set runtime:"codex" or pin a gpt-* model to route it.\n`,
|
|
4069
|
+
);
|
|
4070
|
+
}
|
|
4071
|
+
|
|
3092
4072
|
/**
|
|
3093
4073
|
* Generate a per-agent .toml config file for Codex.
|
|
3094
4074
|
* Sets required agent metadata, sandbox_mode, and developer_instructions
|
|
@@ -3122,21 +4102,43 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
|
|
|
3122
4102
|
// model_overrides is respected on Codex (which uses static TOML, not inline
|
|
3123
4103
|
// Task() model parameters). See #2256.
|
|
3124
4104
|
// Precedence: per-agent model_overrides > runtime-aware tier resolution (#2517).
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
4105
|
+
// #2310 — a Codex .toml `model` MUST be a real Codex/OpenAI model id. Codex is a
|
|
4106
|
+
// passive/session-only model host (ADR-1239): GSD cannot reliably route per-agent
|
|
4107
|
+
// tiers, and a bare GSD/Claude tier alias (opus/sonnet/haiku/fable) or a claude-*
|
|
4108
|
+
// id 400s on a ChatGPT-account Codex ("The 'sonnet' model is not supported when
|
|
4109
|
+
// using Codex with a ChatGPT account"). So: embed ONLY an explicit real-Codex
|
|
4110
|
+
// model pin from model_overrides; omit anything Anthropic-flavored so the agent
|
|
4111
|
+
// inherits the always-available session model. (Removing the runtime-resolver
|
|
4112
|
+
// per-tier embedding below is the ADR-2310 passive-posture epic.)
|
|
4113
|
+
const rawModelOverride = modelOverrides?.[resolvedName] || modelOverrides?.[agentName];
|
|
4114
|
+
let pinnedModel = null;
|
|
4115
|
+
if (rawModelOverride) {
|
|
4116
|
+
if (typeof rawModelOverride === 'string' && rawModelOverride && !_isAnthropicFlavoredModel(rawModelOverride)) {
|
|
4117
|
+
pinnedModel = rawModelOverride; // explicit real-Codex model pin → embed verbatim (#2256)
|
|
4118
|
+
} else {
|
|
4119
|
+
_warnCodexModelOverrideDropped(resolvedName, rawModelOverride); // alias/claude-* → omit
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
if (!pinnedModel && runtimeResolver) {
|
|
3131
4123
|
// #2517 — runtime-aware tier resolution. Embeds Codex-native model + reasoning_effort
|
|
3132
4124
|
// from RUNTIME_PROFILE_MAP / model_profile_overrides for the configured tier.
|
|
4125
|
+
// (Superseded on the default path by the ADR-2310 passive-posture epic.)
|
|
3133
4126
|
const entry = runtimeResolver.resolve(resolvedName) || runtimeResolver.resolve(agentName);
|
|
3134
|
-
if (entry?.model)
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
4127
|
+
if (entry?.model) pinnedModel = entry.model;
|
|
4128
|
+
}
|
|
4129
|
+
// #2310 — final safety gate: never emit an Anthropic-flavored model into a Codex
|
|
4130
|
+
// .toml, even from the runtime-resolver path (e.g. a defaults.json runtime that
|
|
4131
|
+
// does not match the codex install target).
|
|
4132
|
+
if (pinnedModel && _isAnthropicFlavoredModel(pinnedModel)) {
|
|
4133
|
+
_warnCodexModelOverrideDropped(resolvedName, pinnedModel);
|
|
4134
|
+
pinnedModel = null;
|
|
4135
|
+
}
|
|
4136
|
+
let hasPinnedModel = false;
|
|
4137
|
+
if (pinnedModel) {
|
|
4138
|
+
lines.push(`model = ${JSON.stringify(pinnedModel)}`);
|
|
4139
|
+
hasPinnedModel = true;
|
|
4140
|
+
// model is resolved here; reasoning_effort from catalog tier is REPLACED by the
|
|
4141
|
+
// unified effort resolver below (#443). Do NOT emit entry.reasoning_effort here.
|
|
3140
4142
|
}
|
|
3141
4143
|
|
|
3142
4144
|
// #443 — Unified effort for Codex .toml. Uses the same config-driven precedence chain
|
|
@@ -3364,14 +4366,26 @@ function _resolveMovedSkillsOldDir(runtime, targetDir, scope) {
|
|
|
3364
4366
|
|
|
3365
4367
|
/**
|
|
3366
4368
|
* Generate the GSD config block for Codex config.toml.
|
|
3367
|
-
*
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
4369
|
+
*
|
|
4370
|
+
* #2406 — standalone per-agent TOMLs (written by installCodexConfig to
|
|
4371
|
+
* `$CODEX_HOME/agents/<name>.toml`) are auto-discovered by Codex and are the
|
|
4372
|
+
* SOLE canonical registration source for each role. This block therefore no
|
|
4373
|
+
* longer emits `[agents.<name>]` role tables that point `config_file` back at
|
|
4374
|
+
* those same standalone TOMLs — that was a second, redundant declaration of
|
|
4375
|
+
* the same role in one config layer, and Codex logged "Ignoring malformed
|
|
4376
|
+
* agent role definition: duplicate agent role name" once per agent as a
|
|
4377
|
+
* result. Only the bare `[agents]` dispatch-tuning scalar table is emitted
|
|
4378
|
+
* here; role name/description/model/reasoning-effort/sandbox settings remain
|
|
4379
|
+
* fully discoverable through the standalone TOML alone.
|
|
4380
|
+
* @param {Array<{name: string, description: string}>} _agents unused — kept
|
|
4381
|
+
* in the signature for call-site compatibility (installCodexConfig and
|
|
4382
|
+
* existing tests still pass it positionally); per-agent role tables are no
|
|
4383
|
+
* longer generated from it.
|
|
4384
|
+
* @param {string} [_targetDir] unused — the standalone-TOML `config_file`
|
|
4385
|
+
* path it used to resolve is no longer emitted here; kept for the same
|
|
4386
|
+
* call-site-compatibility reason as `_agents`.
|
|
4387
|
+
*/
|
|
4388
|
+
function generateCodexConfigBlock(_agents, _targetDir) {
|
|
3375
4389
|
const lines = [
|
|
3376
4390
|
GSD_CODEX_MARKER,
|
|
3377
4391
|
'',
|
|
@@ -3380,24 +4394,12 @@ function generateCodexConfigBlock(agents, targetDir) {
|
|
|
3380
4394
|
// ADR-1239 upgrade 2 / #2088 — explicit dispatch tuning. Pin `max_depth` on the
|
|
3381
4395
|
// `[agents]` (AgentsToml) table rather than relying on codex-cli's implicit
|
|
3382
4396
|
// 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.
|
|
4397
|
+
// `[agents]` scalar table is validated by validateCodexConfigSchema, which
|
|
4398
|
+
// permits a known-scalar-only `[agents]`.
|
|
3387
4399
|
lines.push('[agents]');
|
|
3388
4400
|
lines.push(`max_depth = ${GSD_CODEX_AGENTS_MAX_DEPTH}`);
|
|
3389
4401
|
lines.push('');
|
|
3390
4402
|
|
|
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
4403
|
return lines.join('\n');
|
|
3402
4404
|
}
|
|
3403
4405
|
|
|
@@ -5922,12 +6924,14 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
|
|
|
5922
6924
|
// Symlink-escape guard (parity with _copyStaged / copyWithPathReplacement): the
|
|
5923
6925
|
// lexical gate above does not resolve symlinks, so a pre-existing config.toml or
|
|
5924
6926
|
// agents/ symlink could redirect writes outside targetDir. Reject those.
|
|
6927
|
+
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
|
6928
|
+
const symlinkOptIn = isSymlinkedDestOptIn();
|
|
5925
6929
|
if (
|
|
5926
|
-
hasExistingSymlinkBetween(resolvedTargetRoot, configPath) ||
|
|
5927
|
-
hasExistingSymlinkBetween(resolvedTargetRoot, path.resolve(agentsTomlDir))
|
|
6930
|
+
hasExistingSymlinkBetween(resolvedTargetRoot, configPath, { allowOptInFollow: symlinkOptIn }) ||
|
|
6931
|
+
hasExistingSymlinkBetween(resolvedTargetRoot, path.resolve(agentsTomlDir), { allowOptInFollow: symlinkOptIn })
|
|
5928
6932
|
) {
|
|
5929
6933
|
throw new Error(
|
|
5930
|
-
`installCodexConfig: a Codex config path under "${targetDir}" contains a symlink
|
|
6934
|
+
`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
6935
|
);
|
|
5932
6936
|
}
|
|
5933
6937
|
fs.mkdirSync(agentsTomlDir, { recursive: true });
|
|
@@ -5978,9 +6982,9 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
|
|
|
5978
6982
|
// `name` containing path separators must not escape agents/ (which would let
|
|
5979
6983
|
// it clobber config.toml or write elsewhere under the configHome).
|
|
5980
6984
|
const agentTomlPath = assertDestWithinConfigHome(agentsTomlDir, `${name}.toml`);
|
|
5981
|
-
if (hasExistingSymlinkBetween(resolvedTargetRoot, agentTomlPath)) {
|
|
6985
|
+
if (hasExistingSymlinkBetween(resolvedTargetRoot, agentTomlPath, { allowOptInFollow: symlinkOptIn })) {
|
|
5982
6986
|
throw new Error(
|
|
5983
|
-
`installCodexConfig: agent toml path "${agentTomlPath}" contains a symlink
|
|
6987
|
+
`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
6988
|
);
|
|
5985
6989
|
}
|
|
5986
6990
|
fs.writeFileSync(agentTomlPath, tomlContent);
|
|
@@ -6582,7 +7586,8 @@ const RUNTIME_CONTENT_DISPATCH = {
|
|
|
6582
7586
|
const b = _hostBehaviors(ctx.runtime).brandingRewrites;
|
|
6583
7587
|
if (b) {
|
|
6584
7588
|
content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']);
|
|
6585
|
-
|
|
7589
|
+
// #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
|
|
7590
|
+
content = applyClaudeCodeBrandSwap(content, b['Claude Code']);
|
|
6586
7591
|
content = content.replace(/\.claude\//g, b['.claude/']);
|
|
6587
7592
|
}
|
|
6588
7593
|
return content;
|
|
@@ -6599,16 +7604,13 @@ const RUNTIME_CONTENT_DISPATCH = {
|
|
|
6599
7604
|
},
|
|
6600
7605
|
},
|
|
6601
7606
|
hermes: {
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
}
|
|
6610
|
-
return content;
|
|
6611
|
-
},
|
|
7607
|
+
// #2284: brand-swap alone left the false "Agent tool IS available"
|
|
7608
|
+
// assertion + literal `Agent(...)` call syntax installed verbatim — see
|
|
7609
|
+
// convertClaudeToHermesMarkdown / projectNamedDispatchToStructuralDelegate
|
|
7610
|
+
// above (the Hermes converters section) for the full named-dispatch →
|
|
7611
|
+
// `delegate_task` projection, driven by capabilities/hermes/capability.json's
|
|
7612
|
+
// hostIntegration.dispatch facts.
|
|
7613
|
+
md: (content, ctx) => convertClaudeToHermesMarkdown(content, ctx),
|
|
6612
7614
|
js: (content, ctx) => {
|
|
6613
7615
|
const b = _hostBehaviors(ctx.runtime).brandingRewrites;
|
|
6614
7616
|
if (b) {
|
|
@@ -6645,9 +7647,10 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
6645
7647
|
}
|
|
6646
7648
|
const resolvedConfinementRoot = path.resolve(confinementRoot);
|
|
6647
7649
|
const resolvedDestDir = assertDestWithinConfigHome(confinementRoot, destDir);
|
|
6648
|
-
|
|
7650
|
+
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
|
7651
|
+
if (hasExistingSymlinkBetween(resolvedConfinementRoot, resolvedDestDir, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
6649
7652
|
throw new Error(
|
|
6650
|
-
`copyWithPathReplacement: destDir "${destDir}" contains a symlink
|
|
7653
|
+
`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
7654
|
);
|
|
6652
7655
|
}
|
|
6653
7656
|
// Use the validated absolute path for all writes below so the gate validates
|
|
@@ -7591,8 +8594,11 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
7591
8594
|
let permissionsModified = false;
|
|
7592
8595
|
if (Array.isArray(settings.permissions.allow)) {
|
|
7593
8596
|
const before = settings.permissions.allow.length;
|
|
8597
|
+
// #2278 — filter against the union of the current allow-rule forms
|
|
8598
|
+
// AND the retired legacy forms, so uninstall still cleans up
|
|
8599
|
+
// pre-fix installs that still carry the stale `Write(...)` entries.
|
|
7594
8600
|
settings.permissions.allow = settings.permissions.allow.filter(
|
|
7595
|
-
(e) => !GSD_CLAUDE_ALLOW_PERMISSIONS.includes(e)
|
|
8601
|
+
(e) => !GSD_CLAUDE_ALLOW_PERMISSIONS.includes(e) && !GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS.includes(e)
|
|
7596
8602
|
);
|
|
7597
8603
|
if (settings.permissions.allow.length !== before) {
|
|
7598
8604
|
permissionsModified = true;
|
|
@@ -8303,7 +9309,7 @@ function resolveInstallRelativePath(baseDir, relPath) {
|
|
|
8303
9309
|
if (fullPath !== root && !fullPath.startsWith(root + path.sep)) {
|
|
8304
9310
|
return null;
|
|
8305
9311
|
}
|
|
8306
|
-
if (hasExistingSymlinkBetween(root, fullPath)) {
|
|
9312
|
+
if (hasExistingSymlinkBetween(root, fullPath, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
8307
9313
|
return null;
|
|
8308
9314
|
}
|
|
8309
9315
|
return { relPath: normalized, fullPath };
|
|
@@ -8377,9 +9383,14 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
8377
9383
|
}
|
|
8378
9384
|
}
|
|
8379
9385
|
if (_hostBehaviors(runtime).flatCommandDir && fs.existsSync(opencodeCommandDir)) {
|
|
9386
|
+
// #2329: derive the manifest key prefix from the SAME descriptor value used
|
|
9387
|
+
// to compute opencodeCommandDir above, instead of a separately-hardcoded
|
|
9388
|
+
// literal — a divergence here would silently break the manifest even after
|
|
9389
|
+
// the destSubpath descriptor is corrected (Generative Fix Divergence guard).
|
|
9390
|
+
const flatCommandDirPrefix = _hostBehaviors(runtime).flatCommandDir || 'command';
|
|
8380
9391
|
for (const file of fs.readdirSync(opencodeCommandDir)) {
|
|
8381
9392
|
if (file.startsWith('gsd-') && file.endsWith('.md')) {
|
|
8382
|
-
manifest.files['
|
|
9393
|
+
manifest.files[flatCommandDirPrefix + '/' + file] = fileHash(path.join(opencodeCommandDir, file));
|
|
8383
9394
|
}
|
|
8384
9395
|
}
|
|
8385
9396
|
}
|
|
@@ -8974,14 +9985,24 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
8974
9985
|
const _effectiveInstallMode = _isCoreProfileAlias ? 'minimal' : 'full';
|
|
8975
9986
|
// Load the manifest and compute resolved profile for named profiles.
|
|
8976
9987
|
// For --minimal/core: use an empty manifest (core profile has no transitive
|
|
8977
|
-
// deps) to produce a resolvedProfile with the core skill set.
|
|
8978
|
-
//
|
|
9988
|
+
// deps) to produce a resolvedProfile with the core skill set. For core/
|
|
9989
|
+
// standard profiles, resolveProfile's `registry` arg IS consulted (via
|
|
9990
|
+
// _capabilitySkillsForMode) so tier:core/tier:standard capability skills are
|
|
9991
|
+
// unioned in when registered. #2322 correction: for the DEFAULT `full`
|
|
9992
|
+
// profile, resolveProfile short-circuits to the `{skills:'*'}` sentinel
|
|
9993
|
+
// BEFORE ever reading `registry` (there is nothing to union — '*' already
|
|
9994
|
+
// means "everything"), so the registry consultation that matters for `full`
|
|
9995
|
+
// happens LATER, at staging time (stageSkillsForRuntimeAsSkills's '*'
|
|
9996
|
+
// fill-in, resolveRuntimeArtifactLayout's `capabilityRegistry` param below) —
|
|
9997
|
+
// not here. `_installedCapabilityRegistry` (not the frozen `_capabilityRegistry`)
|
|
9998
|
+
// is passed so an INSTALLED third-party capability (not just a first-party
|
|
9999
|
+
// one) is honored on every profile, `full` included (#2322 blocker 2).
|
|
8979
10000
|
const _commandsDir = path.join(src, 'commands', 'gsd');
|
|
8980
10001
|
const _skillsManifest = _isCoreProfileAlias ? new Map() : loadSkillsManifest(_commandsDir);
|
|
8981
10002
|
const _resolvedProfile = resolveProfile({
|
|
8982
10003
|
modes: [_activeProfileName],
|
|
8983
10004
|
manifest: _skillsManifest,
|
|
8984
|
-
registry:
|
|
10005
|
+
registry: _installedCapabilityRegistry,
|
|
8985
10006
|
});
|
|
8986
10007
|
// Unified staging function: all profiles use stageSkillsForProfile with the
|
|
8987
10008
|
// registry-aware _resolvedProfile (ADR-857 phase 4c cutover).
|
|
@@ -9341,7 +10362,10 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9341
10362
|
resolveAttribution: getCommitAttribution,
|
|
9342
10363
|
});
|
|
9343
10364
|
} else {
|
|
9344
|
-
|
|
10365
|
+
// #2322: fallback path (adapter unavailable) — thread the composed
|
|
10366
|
+
// registry too, so this path stages third-party capability skills
|
|
10367
|
+
// identically to the primary adapter path above.
|
|
10368
|
+
installRuntimeArtifacts(runtime, targetDir, scope, _resolvedProfile, getCommitAttribution, _installedCapabilityRegistry);
|
|
9345
10369
|
}
|
|
9346
10370
|
|
|
9347
10371
|
// #1326 — Codex only: remove stale agents/openai.yaml sidecars from managed
|
|
@@ -9500,7 +10524,8 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9500
10524
|
} else if (_hostBehaviors(runtime).pluginOnlyInstall) {
|
|
9501
10525
|
// pi (ADR-1239 / #2102 Stage 1): plugin-only install — pi's /gsd command is
|
|
9502
10526
|
// registered programmatically by the native extension (pi/gsd.cjs →
|
|
9503
|
-
// extensions/gsd.
|
|
10527
|
+
// extensions/gsd.js, staged separately below; the dest suffix must be
|
|
10528
|
+
// .ts/.js or pi's auto-discovery skips it silently — #2470) and dispatches in-process
|
|
9504
10529
|
// through the embedded gsd-core command-routing hub. pi has no host-read
|
|
9505
10530
|
// markdown surface (unlike Claude/OpenCode/etc., which scan commands/ or
|
|
9506
10531
|
// command/ directories), so writing flat gsd-<cmd>.md files here would be
|
|
@@ -9910,6 +10935,22 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9910
10935
|
failures.push('VERSION');
|
|
9911
10936
|
}
|
|
9912
10937
|
|
|
10938
|
+
// #2297: write a per-install runtime marker co-located with VERSION at
|
|
10939
|
+
// <install>/gsd-core/.gsd-runtime. It gives resolveModelInternal a reliable
|
|
10940
|
+
// "which runtime owns THIS install" signal in a no-project session (config.runtime
|
|
10941
|
+
// is null and GSD_RUNTIME is not exported), so the shared ~/.gsd/defaults.json
|
|
10942
|
+
// resolve_model_ids:"omit" policy (written below for non-alias runtimes only)
|
|
10943
|
+
// applies ONLY when a non-alias runtime is actually resolving — a Claude session
|
|
10944
|
+
// reads its own marker and keeps its tier aliases instead of inheriting another
|
|
10945
|
+
// runtime's install-order-dependent "omit". See src/model-resolver.cts.
|
|
10946
|
+
const runtimeMarkerDest = path.join(targetDir, 'gsd-core', '.gsd-runtime');
|
|
10947
|
+
fs.writeFileSync(runtimeMarkerDest, `${runtime}\n`);
|
|
10948
|
+
if (verifyFileInstalled(runtimeMarkerDest, '.gsd-runtime')) {
|
|
10949
|
+
console.log(` ${green}✓${reset} Wrote runtime marker (.gsd-runtime: ${runtime})`);
|
|
10950
|
+
} else {
|
|
10951
|
+
failures.push('.gsd-runtime');
|
|
10952
|
+
}
|
|
10953
|
+
|
|
9913
10954
|
// Reusable: copy hooks/dist/ + hooks/lib/ into destRootDir, writing the
|
|
9914
10955
|
// CommonJS package.json marker alongside them. Used below for the generic
|
|
9915
10956
|
// configDir install path (guarded by hostBehaviors.skipSharedHooksInstall),
|
|
@@ -10023,13 +11064,15 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10023
11064
|
}
|
|
10024
11065
|
|
|
10025
11066
|
// Gate hooks/lib/ install on the same set of runtimes that receive hooks/.
|
|
10026
|
-
// Codex/Copilot/Cursor/Windsurf/Trae/Cline
|
|
11067
|
+
// Codex/Copilot/Cursor/Windsurf/Trae/Cline do not use the shared
|
|
10027
11068
|
// hooks/lib/ helpers (Cursor uses standalone .js hook scripts registered
|
|
10028
11069
|
// 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).
|
|
11070
|
+
// hostBehaviors.skipSharedHooksInstall, #2089; Cline likewise #2090;
|
|
11071
|
+
// Trae likewise #2094; Codex uses hooks.json directly;
|
|
11072
|
+
// the others skip hooks entirely); ZCode also skips hooks entirely
|
|
11073
|
+
// (hooksSurface:'none' with no plugin surface — #1821). Kilo is NOT
|
|
11074
|
+
// excluded since #2305: its native plugin adapter (#2093) spawns the
|
|
11075
|
+
// staged hooks/*.js scripts, same as OpenCode. None of the
|
|
10033
11076
|
// excluded runtimes must receive the hooks/lib/ helpers — otherwise the
|
|
10034
11077
|
// Codex comment downstream ("we deliberately do *not* copy hooks/lib/ for
|
|
10035
11078
|
// Codex") is contradicted in practice. (Gating lives at the call sites
|
|
@@ -10045,18 +11088,21 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10045
11088
|
return hooksOk;
|
|
10046
11089
|
}
|
|
10047
11090
|
|
|
10048
|
-
// #1821:
|
|
10049
|
-
// so the staged hook scripts are dead weight for
|
|
11091
|
+
// #1821: ZCode declares hooksSurface:'none' AND has no plugin surface,
|
|
11092
|
+
// so the staged hook scripts are dead weight for it — excluded here.
|
|
10050
11093
|
// OpenCode also declares hooksSurface:'none' but is deliberately NOT excluded:
|
|
10051
11094
|
// its native plugin adapter (#1914, installed above under plugins/gsd-core.js)
|
|
10052
11095
|
// spawns the staged hooks/*.js scripts via OpenCode's event bus and needs both
|
|
10053
|
-
// them and the CommonJS package.json marker written below.
|
|
11096
|
+
// them and the CommonJS package.json marker written below. Kilo is the same
|
|
11097
|
+
// shape since #2093 (a nativePlugin spawning the staged hooks), so it must
|
|
11098
|
+
// NOT skip either — declaring skipSharedHooksInstall:true alongside a
|
|
11099
|
+
// nativePlugin left every guard the plugin spawns a silent no-op (#2305).
|
|
10054
11100
|
// #2089: Cursor's exclusion is now descriptor-driven via
|
|
10055
11101
|
// hostBehaviors.skipSharedHooksInstall (was hardcoded !isCursor).
|
|
10056
11102
|
// #2090: Cline's exclusion is likewise descriptor-driven (cline declares
|
|
10057
11103
|
// skipSharedHooksInstall:true) — the redundant `&& !isCline` was removed.
|
|
10058
|
-
// #2093: Kilo's exclusion
|
|
10059
|
-
// skipSharedHooksInstall:true)
|
|
11104
|
+
// #2093/#2305: Kilo's former exclusion (descriptor-driven via
|
|
11105
|
+
// skipSharedHooksInstall:true) was removed in #2305 — see above.
|
|
10060
11106
|
// #2094: Trae's exclusion is likewise descriptor-driven (trae declares
|
|
10061
11107
|
// skipSharedHooksInstall:true) — the redundant `&& !isTrae` was removed.
|
|
10062
11108
|
// #2101: ZCode's exclusion is likewise descriptor-driven (zcode declares
|
|
@@ -11221,6 +12267,19 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
11221
12267
|
fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
|
|
11222
12268
|
console.log(` ${green}✓${reset} Set resolve_model_ids: "omit" in ~/.gsd/defaults.json`);
|
|
11223
12269
|
}
|
|
12270
|
+
|
|
12271
|
+
// #2395: also persist `runtime: <runtime>` for non-Claude runtimes, so
|
|
12272
|
+
// resolveRuntime() (precedence: GSD_RUNTIME env > config.runtime > 'claude')
|
|
12273
|
+
// resolves to the install's actual runtime identity out of the box — without
|
|
12274
|
+
// this, agent_runtime and every runtime-branded slash hint falls through to
|
|
12275
|
+
// the hard-coded 'claude' default. Mirrors the resolve_model_ids write above:
|
|
12276
|
+
// honor an explicit pre-existing value (any string), only default-populating
|
|
12277
|
+
// when absent. Claude is the resolveRuntime() fallback, so it needs no write.
|
|
12278
|
+
if (defaults.runtime === undefined || defaults.runtime === null || defaults.runtime === '') {
|
|
12279
|
+
defaults.runtime = runtime;
|
|
12280
|
+
fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
|
|
12281
|
+
console.log(` ${green}✓${reset} Set runtime: "${runtime}" in ~/.gsd/defaults.json`);
|
|
12282
|
+
}
|
|
11224
12283
|
} catch (e) {
|
|
11225
12284
|
console.log(` ${yellow}⚠${reset} Could not write ~/.gsd/defaults.json: ${e.message}`);
|
|
11226
12285
|
}
|
|
@@ -12157,6 +13216,7 @@ module.exports = {
|
|
|
12157
13216
|
// #768 — Claude Code permissions pre-population
|
|
12158
13217
|
mergeClaudePermissions,
|
|
12159
13218
|
GSD_CLAUDE_ALLOW_PERMISSIONS,
|
|
13219
|
+
GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS,
|
|
12160
13220
|
GSD_CLAUDE_DENY_PERMISSIONS,
|
|
12161
13221
|
GSD_CODEX_MARKER,
|
|
12162
13222
|
CODEX_AGENT_SANDBOX,
|
|
@@ -12206,6 +13266,18 @@ module.exports = {
|
|
|
12206
13266
|
convertClaudeToCliineMarkdown,
|
|
12207
13267
|
convertClaudeCommandToClineSkill,
|
|
12208
13268
|
convertClaudeAgentToClineAgent,
|
|
13269
|
+
// #2284(b) — cross-cutting branding protected-region helper
|
|
13270
|
+
applyClaudeCodeBrandSwap,
|
|
13271
|
+
// #2284 — Hermes named-dispatch → delegate_task projection
|
|
13272
|
+
convertClaudeToHermesMarkdown,
|
|
13273
|
+
projectNamedDispatchToStructuralDelegate,
|
|
13274
|
+
_hostIntegrationDispatch,
|
|
13275
|
+
_resolveAvailableGsdRoles,
|
|
13276
|
+
HERMES_DISPATCH_TOOL_CONFIG,
|
|
13277
|
+
maskStringLiterals,
|
|
13278
|
+
findDispatchCallSpans,
|
|
13279
|
+
_assertProjectionComplete,
|
|
13280
|
+
_normalizeDispatchCallSpan,
|
|
12209
13281
|
buildClineRulesBody,
|
|
12210
13282
|
buildClineAgentsMdBody,
|
|
12211
13283
|
buildClinePreToolUseHook,
|