@opengsd/gsd-core 1.7.0-rc.4 → 1.7.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +20 -0
- package/agents/gsd-doc-classifier.md +105 -0
- package/agents/gsd-doc-synthesizer.md +61 -0
- package/agents/gsd-ui-checker.md +30 -0
- package/agents/gsd-ui-researcher.md +1 -0
- package/bin/install.js +1568 -622
- package/gsd-core/bin/gsd-tools.cjs +40 -1
- package/gsd-core/bin/lib/api-coverage.cjs +466 -0
- package/gsd-core/bin/lib/audit.cjs +6 -3
- package/gsd-core/bin/lib/capability-loader.cjs +11 -9
- package/gsd-core/bin/lib/capability-registry.cjs +761 -84
- package/gsd-core/bin/lib/capability-validator.cjs +56 -18
- package/gsd-core/bin/lib/capability-writer.cjs +10 -1
- package/gsd-core/bin/lib/check-command-router.cjs +242 -3
- package/gsd-core/bin/lib/commands.cjs +7 -5
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +96 -0
- package/gsd-core/bin/lib/core-utils.cjs +4 -1
- package/gsd-core/bin/lib/host-integration-adapters/cline-sdk-binding.cjs +234 -0
- package/gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs +145 -0
- package/gsd-core/bin/lib/host-integration.cjs +45 -4
- package/gsd-core/bin/lib/init.cjs +76 -39
- package/gsd-core/bin/lib/install-effort-resolver.cjs +213 -0
- package/gsd-core/bin/lib/install-engine.cjs +228 -18
- package/gsd-core/bin/lib/installer-migration-report.cjs +7 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +68 -17
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +50 -11
- package/gsd-core/bin/lib/mcp-server.cjs +18 -7
- package/gsd-core/bin/lib/milestone.cjs +3 -3
- package/gsd-core/bin/lib/normalize-test-command.cjs +187 -0
- package/gsd-core/bin/lib/phase-id.cjs +132 -3
- package/gsd-core/bin/lib/phase.cjs +78 -16
- package/gsd-core/bin/lib/planning-workspace.cjs +17 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
- package/gsd-core/bin/lib/roadmap-command-router.cjs +5 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +21 -30
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +9 -9
- package/gsd-core/bin/lib/roadmap.cjs +42 -56
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +248 -44
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +39 -23
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +30 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +534 -37
- package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
- package/gsd-core/bin/lib/security.cjs +6 -36
- package/gsd-core/bin/lib/shell-command-projection.cjs +115 -2
- package/gsd-core/bin/lib/spec-section.cjs +111 -0
- package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
- package/gsd-core/bin/lib/state-transition.cjs +1 -1
- package/gsd-core/bin/lib/state.cjs +24 -24
- package/gsd-core/bin/lib/surface.cjs +40 -6
- package/gsd-core/bin/lib/uat.cjs +4 -1
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
- package/gsd-core/bin/lib/validate.cjs +15 -6
- package/gsd-core/bin/lib/verify.cjs +33 -37
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/bin/shared/model-catalog.json +14 -9
- package/gsd-core/references/api-coverage.md +104 -0
- package/gsd-core/references/model-profiles.md +2 -2
- package/gsd-core/references/planning-config.md +2 -0
- package/gsd-core/references/specless-probe-fallback.md +172 -0
- package/gsd-core/references/ui-consideration-probe.md +73 -0
- package/gsd-core/templates/UI-SPEC.md +25 -0
- package/gsd-core/templates/VALIDATION.md +2 -0
- package/gsd-core/templates/config.json +2 -1
- package/gsd-core/workflows/audit-fix.md +9 -1
- package/gsd-core/workflows/audit-milestone.md +7 -4
- package/gsd-core/workflows/code-review-fix.md +7 -3
- package/gsd-core/workflows/code-review.md +4 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +8 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +42 -0
- package/gsd-core/workflows/execute-phase.md +1 -25
- package/gsd-core/workflows/plan-phase.md +37 -2
- package/gsd-core/workflows/quick.md +2 -2
- package/gsd-core/workflows/review.md +59 -13
- package/gsd-core/workflows/settings-advanced.md +12 -9
- package/gsd-core/workflows/settings.md +2 -2
- package/gsd-core/workflows/ui-phase.md +146 -1
- package/gsd-core/workflows/validate-phase.md +2 -2
- package/gsd-core/workflows/verify-phase.md +3 -2
- package/gsd-core/workflows/verify-work.md +38 -0
- package/hooks/dist/gsd-cursor-pre-tool.js +76 -0
- package/hooks/dist/gsd-cursor-stop.js +48 -0
- package/hooks/dist/gsd-cursor-subagent-start.js +50 -0
- package/hooks/dist/gsd-cursor-subagent-stop.js +40 -0
- package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
- package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
- package/hooks/dist/managed-hooks-registry.cjs +6 -0
- package/hooks/gsd-cursor-pre-tool.js +76 -0
- package/hooks/gsd-cursor-stop.js +48 -0
- package/hooks/gsd-cursor-subagent-start.js +50 -0
- package/hooks/gsd-cursor-subagent-stop.js +40 -0
- package/hooks/gsd-windsurf-pre-command.js +275 -0
- package/hooks/gsd-windsurf-pre-write.js +132 -0
- package/hooks/managed-hooks-registry.cjs +6 -0
- package/package.json +9 -4
- package/pi/gsd.cjs +354 -0
- package/scripts/build-hooks.js +8 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +11 -1
- package/scripts/gen-registry.cjs +128 -0
- package/scripts/lint-phase-id-drift.cjs +150 -0
- package/scripts/lint-test-file-count.allowlist.json +2 -1
- package/scripts/registry-schema.cjs +565 -0
- package/scripts/run-tests.cjs +21 -1
- package/scripts/validate-registry.cjs +117 -0
- package/vscode/browser.js +197 -0
- package/vscode/extension.js +383 -0
- package/vscode/host-binding.js +113 -0
- package/vscode/package.json +96 -0
|
@@ -26,6 +26,7 @@ const runtimeArtifactConversion = require("./runtime-artifact-conversion.cjs");
|
|
|
26
26
|
const runtimeArtifactLayout = require("./runtime-artifact-layout.cjs");
|
|
27
27
|
const runtimeArtifactInstallPlan = require("./runtime-artifact-install-plan.cjs");
|
|
28
28
|
const runtimeNamePolicy = require("./runtime-name-policy.cjs");
|
|
29
|
+
const installProfiles = require("./install-profiles.cjs");
|
|
29
30
|
const { processAttribution } = runtimeArtifactConversion;
|
|
30
31
|
// resolveRuntimeArtifactLayout: accessed via module ref (not destructured) so
|
|
31
32
|
// test stubs that monkeypatch the module's exports are seen at call time.
|
|
@@ -51,6 +52,25 @@ const { getDirName } = runtimeNamePolicy;
|
|
|
51
52
|
*/
|
|
52
53
|
const USER_OWNED_ARTIFACTS = ['USER-PROFILE.md'];
|
|
53
54
|
// ---------------------------------------------------------------------------
|
|
55
|
+
// Host-behavior helpers
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
/**
|
|
58
|
+
* Host-specific install behaviors declared on the runtime descriptor
|
|
59
|
+
* (capabilities/<runtime>/capability.json -> runtime.hostBehaviors).
|
|
60
|
+
* Mirrors bin/install.js's `_hostBehaviors` (ADR-1239 / #2086/#2087). Returns
|
|
61
|
+
* {} for runtimes that declare none or if the registry fails to load, so
|
|
62
|
+
* every behavior branch degrades to the generic path by default.
|
|
63
|
+
*/
|
|
64
|
+
function _hostBehaviors(runtime) {
|
|
65
|
+
try {
|
|
66
|
+
const reg = require('./capability-registry.cjs');
|
|
67
|
+
return (reg && reg.runtimes && reg.runtimes[runtime] && reg.runtimes[runtime].runtime && reg.runtimes[runtime].runtime.hostBehaviors) || {};
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
54
74
|
// Conversion helpers
|
|
55
75
|
// ---------------------------------------------------------------------------
|
|
56
76
|
/**
|
|
@@ -82,6 +102,17 @@ function convertClaudeCommandToOpencodeSkill(content, skillName) {
|
|
|
82
102
|
function convertClaudeCommandToKiloSkill(content, skillName) {
|
|
83
103
|
return runtimeArtifactConversion.convertClaudeCommandToKiloSkill(content, skillName);
|
|
84
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Converter-name registry for the OpenCode-family combined skills installer
|
|
107
|
+
* (ADR-1239 / #2093). Maps the `converter` string declared on each runtime's
|
|
108
|
+
* artifactLayout skills-kind descriptor (capabilities/<runtime>/capability.json)
|
|
109
|
+
* to the actual conversion function, so `installOpencodeFamilySkills` dispatches
|
|
110
|
+
* off the descriptor instead of a `frontmatterDialect === 'kilo'` runtime check.
|
|
111
|
+
*/
|
|
112
|
+
const SKILLS_CONVERTER_REGISTRY = {
|
|
113
|
+
convertClaudeCommandToOpencodeSkill,
|
|
114
|
+
convertClaudeCommandToKiloSkill,
|
|
115
|
+
};
|
|
85
116
|
// ---------------------------------------------------------------------------
|
|
86
117
|
// User-artifact preservation helpers
|
|
87
118
|
// ---------------------------------------------------------------------------
|
|
@@ -231,14 +262,20 @@ function _copyStaged(stagedDir, destDir, kind, configDir, runtime) {
|
|
|
231
262
|
if (configDir === undefined) {
|
|
232
263
|
throw new Error('_copyStaged: configDir (install root) is required to confine writes — refusing to write');
|
|
233
264
|
}
|
|
265
|
+
// The install root is normally configDir, but a kind may declare an alternate
|
|
266
|
+
// `home` (ADR-1239 upgrade 3 / #2088, e.g. Codex skills -> $HOME/.agents) — in
|
|
267
|
+
// that case this defense-in-depth check must confine against the resolved
|
|
268
|
+
// alternate root instead, matching the upstream gate's own root selection in
|
|
269
|
+
// createRuntimeArtifactInstallPlan.
|
|
270
|
+
const installRoot = (kind && typeof kind.home === 'string' && kind.home !== '') ? kind.home : configDir;
|
|
234
271
|
// Strict-subpath + NUL containment via the canonical gate (shared with the
|
|
235
272
|
// layout-driven install plan); throws if destDir escapes the install root.
|
|
236
|
-
// destDir here is an absolute path; path.resolve(
|
|
237
|
-
const resolvedDest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(
|
|
238
|
-
// Symlink-escape guard: reject if any path component between
|
|
239
|
-
// destDir is a symlink that would redirect writes outside
|
|
240
|
-
if (hasExistingSymlinkBetween(node_path_1.default.resolve(
|
|
241
|
-
throw new Error(`_copyStaged: destDir "${destDir}" contains a symlink escaping the install root "${
|
|
273
|
+
// destDir here is an absolute path; path.resolve(installRoot, absoluteDest) returns it unchanged, so the gate's strict-subpath check still correctly confines it to installRoot.
|
|
274
|
+
const resolvedDest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, destDir);
|
|
275
|
+
// Symlink-escape guard: reject if any path component between the install root and
|
|
276
|
+
// destDir is a symlink that would redirect writes outside the install root.
|
|
277
|
+
if (hasExistingSymlinkBetween(node_path_1.default.resolve(installRoot), resolvedDest)) {
|
|
278
|
+
throw new Error(`_copyStaged: destDir "${destDir}" contains a symlink escaping the install root "${installRoot}" — refusing to write`);
|
|
242
279
|
}
|
|
243
280
|
// Use the validated absolute path for the actual writes below.
|
|
244
281
|
destDir = resolvedDest;
|
|
@@ -276,9 +313,13 @@ function _copyStaged(stagedDir, destDir, kind, configDir, runtime) {
|
|
|
276
313
|
let destName;
|
|
277
314
|
if (kind.kind === 'agents') {
|
|
278
315
|
// Agent files already carry the gsd- prefix in the source dir.
|
|
279
|
-
// #
|
|
280
|
-
|
|
281
|
-
|
|
316
|
+
// #2099: descriptor-driven via hostBehaviors.agentFileExtension (was
|
|
317
|
+
// hardcoded `runtime === 'copilot'`). copilot declares '.agent.md';
|
|
318
|
+
// every other runtime's descriptor leaves this unset, so destName falls
|
|
319
|
+
// back to entry.name unchanged (byte-parity, #1575 origin comment).
|
|
320
|
+
const _agentExt = runtime ? _hostBehaviors(runtime).agentFileExtension : undefined;
|
|
321
|
+
destName = _agentExt
|
|
322
|
+
? entry.name.replace(/\.md$/, _agentExt)
|
|
282
323
|
: entry.name;
|
|
283
324
|
}
|
|
284
325
|
else if (namespacedByDir) {
|
|
@@ -414,7 +455,7 @@ function _runLegacyInstallMigrations(runtime, configDir, scope = 'global') {
|
|
|
414
455
|
// that for Hermes the flat skills/gsd-*/ removal (below) does not delete the freshly
|
|
415
456
|
// created skills/gsd-dev-preferences/ skill dir.
|
|
416
457
|
let savedLegacyArtifacts = null;
|
|
417
|
-
if (runtime
|
|
458
|
+
if (_hostBehaviors(runtime).legacyCommandsGsdInstallMigration) {
|
|
418
459
|
if (node_fs_1.default.existsSync(legacyCommandsGsd)) {
|
|
419
460
|
savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsGsd, ['dev-preferences.md']);
|
|
420
461
|
node_fs_1.default.rmSync(legacyCommandsGsd, { recursive: true });
|
|
@@ -471,7 +512,8 @@ function _runLegacyUninstallCleanup(runtime, configDir, scope = 'global') {
|
|
|
471
512
|
// commands/gsd/ for claude local, preserving dev-preferences.md by restoring it
|
|
472
513
|
// to the same location (#1423). Using migrateLegacyDevPreferencesToSkill here
|
|
473
514
|
// (which would redirect to skills/) conflicts with the test contract for local installs.
|
|
474
|
-
const
|
|
515
|
+
const _lu = _hostBehaviors(runtime).legacyCommandsGsdUninstall;
|
|
516
|
+
const isLegacyCommandsGsd = _lu === true || (_lu === 'global' && scope === 'global');
|
|
475
517
|
if (isLegacyCommandsGsd) {
|
|
476
518
|
const legacyCommandsGsd = node_path_1.default.join(configDir, 'commands', 'gsd');
|
|
477
519
|
if (node_fs_1.default.existsSync(legacyCommandsGsd)) {
|
|
@@ -519,6 +561,15 @@ function _runLegacyUninstallCleanup(runtime, configDir, scope = 'global') {
|
|
|
519
561
|
* @param resolveAttribution injection: (runtime) => attribution string | undefined
|
|
520
562
|
*/
|
|
521
563
|
function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution = () => undefined) {
|
|
564
|
+
// Combined-family runtimes (OpenCode/Kilo, ADR-1239 / #2087): route through
|
|
565
|
+
// the dedicated combined commands+skills+plugin orchestrator instead of the
|
|
566
|
+
// generic layout-driven loop below, mirroring the bespoke install path that
|
|
567
|
+
// previously lived inline in bin/install.js.
|
|
568
|
+
const behaviors = _hostBehaviors(runtime);
|
|
569
|
+
if (behaviors.combinedFamilyInstall) {
|
|
570
|
+
installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution, behaviors);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
522
573
|
// Legacy cleanup before layout-driven writes
|
|
523
574
|
_runLegacyInstallMigrations(runtime, configDir, scope);
|
|
524
575
|
const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, configDir, scope);
|
|
@@ -543,10 +594,16 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, res
|
|
|
543
594
|
throw new Error(`Install plan returned unknown artifact kind: ${item.kind}`);
|
|
544
595
|
const dest = item.destDir;
|
|
545
596
|
// Symlink-escape guard: reject before mkdir if dest (or any component
|
|
546
|
-
// between
|
|
547
|
-
// mkdirSync follows symlinks, so this must run BEFORE the mkdir
|
|
548
|
-
|
|
549
|
-
|
|
597
|
+
// between the install root and dest) is a symlink pointing outside that
|
|
598
|
+
// root. mkdirSync follows symlinks, so this must run BEFORE the mkdir
|
|
599
|
+
// call. The install root is normally configDir, but a kind may declare
|
|
600
|
+
// an alternate `home` (ADR-1239 upgrade 3 / #2088, e.g. Codex skills ->
|
|
601
|
+
// $HOME/.agents) — in that case the guard must check against the
|
|
602
|
+
// resolved alternate root instead, matching assertDestWithinConfigHome's
|
|
603
|
+
// own root selection in createRuntimeArtifactInstallPlan.
|
|
604
|
+
const installRoot = (kind && typeof kind.home === 'string' && kind.home !== '') ? kind.home : configDir;
|
|
605
|
+
if (hasExistingSymlinkBetween(node_path_1.default.resolve(installRoot), dest)) {
|
|
606
|
+
throw new Error(`installRuntimeArtifacts: destDir "${dest}" contains a symlink escaping the install root "${installRoot}" — refusing to create`);
|
|
550
607
|
}
|
|
551
608
|
node_fs_1.default.mkdirSync(dest, { recursive: true });
|
|
552
609
|
if (kind.kind === 'skills' && node_fs_1.default.existsSync(dest)) {
|
|
@@ -610,6 +667,18 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, res
|
|
|
610
667
|
const nestedGsdDirForCleanup = node_path_1.default.join(configDir, 'skills', 'gsd');
|
|
611
668
|
_removeHermesBareStemDirs(nestedGsdDirForCleanup);
|
|
612
669
|
}
|
|
670
|
+
// Generic-branch nativePlugin staging (ADR-1239 / #2102 Stage 1): runtimes
|
|
671
|
+
// outside the OpenCode/Kilo combined-family install (e.g. pi, whose
|
|
672
|
+
// artifactLayout is empty and which never sets combinedFamilyInstall) still
|
|
673
|
+
// need their declared hostBehaviors.nativePlugin file copied into configDir.
|
|
674
|
+
// findInstallSourceRoot resolves the repo/package root independent of
|
|
675
|
+
// configDir contents (marker check, then a walk-up from __dirname), so this
|
|
676
|
+
// is safe even when configDir has no .gsd-source marker (artifactLayout: []).
|
|
677
|
+
if (behaviors.nativePlugin) {
|
|
678
|
+
const commandsGsdDir = runtimeArtifactLayout.findInstallSourceRoot(configDir);
|
|
679
|
+
const src = node_path_1.default.dirname(node_path_1.default.dirname(commandsGsdDir));
|
|
680
|
+
_installNativePluginIfDeclared(runtime, configDir, behaviors, src);
|
|
681
|
+
}
|
|
613
682
|
}
|
|
614
683
|
// ---------------------------------------------------------------------------
|
|
615
684
|
// installOpencodeFamilySkills
|
|
@@ -638,9 +707,16 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
|
|
|
638
707
|
const rawDir = rawCommandsDir;
|
|
639
708
|
if (!rawDir || !node_fs_1.default.existsSync(rawDir))
|
|
640
709
|
return 0;
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
710
|
+
// #2093: descriptor-driven — dispatch off the skills-kind entry's `converter`
|
|
711
|
+
// string (capabilities/<runtime>/capability.json artifactLayout) via the
|
|
712
|
+
// SKILLS_CONVERTER_REGISTRY, instead of a `frontmatterDialect === 'kilo'`
|
|
713
|
+
// runtime check. Fail loud if the descriptor names an unregistered converter
|
|
714
|
+
// (mirrors the converter=null throw in runtime-artifact-layout.cts).
|
|
715
|
+
const converterName = skillsKindEntry.converter;
|
|
716
|
+
const converter = converterName ? SKILLS_CONVERTER_REGISTRY[converterName] : undefined;
|
|
717
|
+
if (!converter) {
|
|
718
|
+
throw new TypeError(`installOpencodeFamilySkills: unknown skills converter '${String(converterName)}' for runtime '${runtime}'`);
|
|
719
|
+
}
|
|
644
720
|
const dest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(targetDir, skillsKindEntry.destSubpath);
|
|
645
721
|
// Symlink-escape guard: reject if any path component between targetDir and
|
|
646
722
|
// dest is a symlink that would redirect writes outside the config root.
|
|
@@ -686,6 +762,136 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
|
|
|
686
762
|
return count;
|
|
687
763
|
}
|
|
688
764
|
// ---------------------------------------------------------------------------
|
|
765
|
+
// installOpencodeFamilyCommands
|
|
766
|
+
// ---------------------------------------------------------------------------
|
|
767
|
+
/**
|
|
768
|
+
* Install the flattened commands surface for an OpenCode-family runtime
|
|
769
|
+
* (OpenCode/Kilo): commands/gsd/**\/*.md -> command/gsd-<...>.md, with
|
|
770
|
+
* per-runtime frontmatter conversion and path-prefix/attribution rewrites.
|
|
771
|
+
*
|
|
772
|
+
* Mirrors bin/install.js's copyFlattenedCommands VERBATIM (ADR-1239 /
|
|
773
|
+
* #2087), except attribution is resolved via the injected
|
|
774
|
+
* `resolveAttribution` callback instead of a module-level getCommitAttribution.
|
|
775
|
+
*
|
|
776
|
+
* @param runtime - 'opencode' or 'kilo'
|
|
777
|
+
* @param destDir - destination directory for flattened commands (recurses with the same destDir)
|
|
778
|
+
* @param srcDir - source directory to walk (commands/gsd/, recursing into subdirectories)
|
|
779
|
+
* @param pathPrefix - computed config-path prefix for body rewrites
|
|
780
|
+
* @param resolveAttribution - injection: (runtime) => attribution string | undefined
|
|
781
|
+
* @param prefix - filename prefix accumulator (defaults to 'gsd'; grows on recursion)
|
|
782
|
+
*/
|
|
783
|
+
function installOpencodeFamilyCommands(runtime, destDir, srcDir, pathPrefix, resolveAttribution = () => undefined, prefix = 'gsd') {
|
|
784
|
+
if (!node_fs_1.default.existsSync(srcDir))
|
|
785
|
+
return;
|
|
786
|
+
// Remove old gsd-*.md files before copying new ones
|
|
787
|
+
if (node_fs_1.default.existsSync(destDir)) {
|
|
788
|
+
for (const file of node_fs_1.default.readdirSync(destDir)) {
|
|
789
|
+
if (file.startsWith(`${prefix}-`) && file.endsWith('.md'))
|
|
790
|
+
node_fs_1.default.unlinkSync(node_path_1.default.join(destDir, file));
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
node_fs_1.default.mkdirSync(destDir, { recursive: true });
|
|
795
|
+
}
|
|
796
|
+
for (const entry of node_fs_1.default.readdirSync(srcDir, { withFileTypes: true })) {
|
|
797
|
+
const srcPath = node_path_1.default.join(srcDir, entry.name);
|
|
798
|
+
if (entry.isDirectory()) {
|
|
799
|
+
installOpencodeFamilyCommands(runtime, destDir, srcPath, pathPrefix, resolveAttribution, `${prefix}-${entry.name}`);
|
|
800
|
+
}
|
|
801
|
+
else if (entry.name.endsWith('.md')) {
|
|
802
|
+
const baseName = entry.name.replace('.md', '');
|
|
803
|
+
const destName = `${prefix}-${baseName}.md`;
|
|
804
|
+
let content = node_fs_1.default.readFileSync(srcPath, 'utf8');
|
|
805
|
+
content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix);
|
|
806
|
+
content = processAttribution(content, resolveAttribution(runtime));
|
|
807
|
+
// #2093: this commands-kind entry's descriptor `converter` field is
|
|
808
|
+
// intentionally `null` (see capabilities/{kilo,opencode}/capability.json —
|
|
809
|
+
// the flattened-command writer above applies its own path/attribution
|
|
810
|
+
// rewrites and has no per-file converter slot to key on), so there is no
|
|
811
|
+
// descriptor string to dispatch through here. `frontmatterDialect` is the
|
|
812
|
+
// documented, intentional dispatch key for frontmatter-shape selection —
|
|
813
|
+
// it is itself descriptor-driven (not a `runtime === 'kilo'` check), so it
|
|
814
|
+
// already satisfies the fold-to-descriptor requirement. Only the SKILLS
|
|
815
|
+
// converter site above (installOpencodeFamilySkills) has a real
|
|
816
|
+
// `converter` string to key on via SKILLS_CONVERTER_REGISTRY.
|
|
817
|
+
content = _hostBehaviors(runtime).frontmatterDialect === 'kilo'
|
|
818
|
+
? runtimeArtifactConversion.convertClaudeToKiloFrontmatter(content)
|
|
819
|
+
: runtimeArtifactConversion.convertClaudeToOpencodeFrontmatter(content);
|
|
820
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(destDir, destName), content);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
// ---------------------------------------------------------------------------
|
|
825
|
+
// _installNativePluginIfDeclared
|
|
826
|
+
// ---------------------------------------------------------------------------
|
|
827
|
+
/**
|
|
828
|
+
* Copy a runtime's declared native-extension/plugin file (hostBehaviors.nativePlugin)
|
|
829
|
+
* into its resolved config dir, when the runtime descriptor declares one.
|
|
830
|
+
*
|
|
831
|
+
* Extracted (ADR-1239 / #2102 Stage 1) from the body previously inlined in
|
|
832
|
+
* installOpencodeFamilyArtifacts so a runtime that is NOT part of the
|
|
833
|
+
* OpenCode/Kilo combined-family install (e.g. pi, whose artifactLayout is
|
|
834
|
+
* empty and which never sets combinedFamilyInstall) can still get its
|
|
835
|
+
* nativePlugin file staged via the generic installRuntimeArtifacts branch.
|
|
836
|
+
* Behavior for opencode/kilo is unchanged — same source resolution, same
|
|
837
|
+
* mkdir + copyFileSync call, same silent no-op when the source is missing.
|
|
838
|
+
*
|
|
839
|
+
* @param runtime - canonical runtime id (only used for the assertDestWithinConfigHome guard)
|
|
840
|
+
* @param configDir - resolved runtime config directory
|
|
841
|
+
* @param behaviors - the runtime's hostBehaviors descriptor
|
|
842
|
+
* @param src - repo/package root (two levels up from the commands/gsd source dir)
|
|
843
|
+
*/
|
|
844
|
+
function _installNativePluginIfDeclared(runtime, configDir, behaviors, src) {
|
|
845
|
+
const np = behaviors.nativePlugin;
|
|
846
|
+
if (np && np.source) {
|
|
847
|
+
const pluginSrc = node_path_1.default.join(src, np.source);
|
|
848
|
+
if (node_fs_1.default.existsSync(pluginSrc)) {
|
|
849
|
+
const destDir = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, np.dir);
|
|
850
|
+
node_fs_1.default.mkdirSync(destDir, { recursive: true });
|
|
851
|
+
node_fs_1.default.copyFileSync(pluginSrc, node_path_1.default.join(destDir, np.file));
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
// ---------------------------------------------------------------------------
|
|
856
|
+
// installOpencodeFamilyArtifacts
|
|
857
|
+
// ---------------------------------------------------------------------------
|
|
858
|
+
/**
|
|
859
|
+
* Combined-family install orchestrator for OpenCode/Kilo (ADR-1239 / #2087,
|
|
860
|
+
* #2093). Stages the flattened commands surface + skills surface + (any
|
|
861
|
+
* runtime whose hostBehaviors declares `nativePlugin` — OpenCode and, since
|
|
862
|
+
* #2093, Kilo) native plugin adapter, mirroring the bespoke `else if (isOpencode ||
|
|
863
|
+
* isKilo)` block previously inlined in bin/install.js.
|
|
864
|
+
*
|
|
865
|
+
* @param runtime - 'opencode' or 'kilo'
|
|
866
|
+
* @param configDir - resolved runtime config directory
|
|
867
|
+
* @param scope - install scope ('global' | 'local')
|
|
868
|
+
* @param resolvedProfile - from resolveProfile() / resolveEffectiveProfile()
|
|
869
|
+
* @param resolveAttribution - injection: (runtime) => attribution string | undefined
|
|
870
|
+
* @param behaviors - the runtime's hostBehaviors descriptor (already resolved by the caller)
|
|
871
|
+
*/
|
|
872
|
+
function installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution = () => undefined, behaviors = {}) {
|
|
873
|
+
const isGlobal = scope === 'global';
|
|
874
|
+
// findInstallSourceRoot resolves DIRECTLY to the commands/gsd source dir
|
|
875
|
+
// (via the .gsd-source marker or a walk-up from __dirname) — every other
|
|
876
|
+
// call site in runtime-artifact-layout.cts feeds its return value straight
|
|
877
|
+
// into stageSkillsForProfile/stageSkillsForRuntimeAsSkills. The repo/package
|
|
878
|
+
// root (needed below for the native plugin source) is two levels up.
|
|
879
|
+
const commandsGsdDir = runtimeArtifactLayout.findInstallSourceRoot(configDir);
|
|
880
|
+
const src = node_path_1.default.dirname(node_path_1.default.dirname(commandsGsdDir));
|
|
881
|
+
const rawCommandsDir = installProfiles.stageSkillsForProfile(commandsGsdDir, resolvedProfile);
|
|
882
|
+
const pathPrefix = runtimeArtifactConversion._computePathPrefix({
|
|
883
|
+
isGlobal,
|
|
884
|
+
isOpencode: behaviors.skipHomePrefixSubstitution === true,
|
|
885
|
+
isWindowsHost: process.platform === 'win32',
|
|
886
|
+
resolvedTarget: node_path_1.default.resolve(configDir).replace(/\\/g, '/'),
|
|
887
|
+
homeDir: node_os_1.default.homedir().replace(/\\/g, '/'),
|
|
888
|
+
});
|
|
889
|
+
const commandDir = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, 'command');
|
|
890
|
+
installOpencodeFamilyCommands(runtime, commandDir, rawCommandsDir, pathPrefix, resolveAttribution);
|
|
891
|
+
installOpencodeFamilySkills(runtime, configDir, rawCommandsDir, pathPrefix, resolveAttribution);
|
|
892
|
+
_installNativePluginIfDeclared(runtime, configDir, behaviors, src);
|
|
893
|
+
}
|
|
894
|
+
// ---------------------------------------------------------------------------
|
|
689
895
|
// uninstallRuntimeArtifacts
|
|
690
896
|
// ---------------------------------------------------------------------------
|
|
691
897
|
/**
|
|
@@ -740,6 +946,10 @@ module.exports = {
|
|
|
740
946
|
installRuntimeArtifacts,
|
|
741
947
|
uninstallRuntimeArtifacts,
|
|
742
948
|
installOpencodeFamilySkills,
|
|
949
|
+
installOpencodeFamilyCommands,
|
|
950
|
+
installOpencodeFamilyArtifacts,
|
|
951
|
+
_installNativePluginIfDeclared,
|
|
952
|
+
_hostBehaviors,
|
|
743
953
|
_copyStaged,
|
|
744
954
|
hasExistingSymlinkBetween,
|
|
745
955
|
preserveUserArtifacts,
|
|
@@ -37,7 +37,14 @@ exports.BUNDLED_GSD_HOOK_FILES = Object.freeze(new Set([
|
|
|
37
37
|
'hooks/gsd-config-reload.js',
|
|
38
38
|
'hooks/gsd-context-monitor.js',
|
|
39
39
|
'hooks/gsd-cursor-post-tool.js',
|
|
40
|
+
'hooks/gsd-cursor-pre-tool.js',
|
|
40
41
|
'hooks/gsd-cursor-session-start.js',
|
|
42
|
+
'hooks/gsd-cursor-stop.js',
|
|
43
|
+
'hooks/gsd-cursor-subagent-start.js',
|
|
44
|
+
'hooks/gsd-cursor-subagent-stop.js',
|
|
45
|
+
// Windsurf/Cascade blocking hooks — registered by writeWindsurfHooksJson (#2100).
|
|
46
|
+
'hooks/gsd-windsurf-pre-write.js',
|
|
47
|
+
'hooks/gsd-windsurf-pre-command.js',
|
|
41
48
|
'hooks/gsd-ensure-canonical-path.js',
|
|
42
49
|
'hooks/gsd-graphify-update.sh',
|
|
43
50
|
'hooks/gsd-phase-boundary.sh',
|
|
@@ -401,6 +401,25 @@ function renderLoopHooks(resolved) {
|
|
|
401
401
|
* Missing <capId> value → coreError + non-zero exit.
|
|
402
402
|
* Unknown/inactive capId → `false` (not an error).
|
|
403
403
|
*/
|
|
404
|
+
// #2009: a capability id surfaced inside the runnable `gsd capability remove <id>`
|
|
405
|
+
// remediation must match the canonical kebab-case id shape (identical to
|
|
406
|
+
// capability-consent.cts / capability-ledger.cts) before it is embedded — a raw
|
|
407
|
+
// overlay directory name is attacker-controlled and can carry shell/markdown
|
|
408
|
+
// metacharacters (backticks, ';', '|', '$()'). An id that fails this check is
|
|
409
|
+
// withheld and no runnable command is rendered for it.
|
|
410
|
+
const LOAD_FAIL_CAP_ID_RE = /^[a-z][a-z0-9-]*$/;
|
|
411
|
+
// #2009: neutralize control chars, newlines, and backticks from a third-party
|
|
412
|
+
// load-failure reason so a malicious manifest cannot break out of the warning
|
|
413
|
+
// line or inject markdown / prompt content into the surfaced message.
|
|
414
|
+
function sanitizeLoadFailReason(reason) {
|
|
415
|
+
const cleaned = String(reason)
|
|
416
|
+
// Strip C0 control chars, DEL, and backticks; collapse remaining whitespace.
|
|
417
|
+
.replace(/[\x00-\x1F\x7F`]/g, ' ')
|
|
418
|
+
.replace(/\s+/g, " ")
|
|
419
|
+
.trim()
|
|
420
|
+
.slice(0, 300);
|
|
421
|
+
return cleaned || '(no reason given)';
|
|
422
|
+
}
|
|
404
423
|
function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
|
|
405
424
|
if (!point) {
|
|
406
425
|
coreError('loop render-hooks requires a <point> argument. Valid points: ' + CANONICAL_POINTS.join(', '));
|
|
@@ -456,26 +475,54 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
|
|
|
456
475
|
coreError(msg);
|
|
457
476
|
return;
|
|
458
477
|
}
|
|
459
|
-
// ── ADR-1244 D2
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
478
|
+
// ── ADR-1244 D2: load-failed capability gates FAIL OPEN with a loud warning ────
|
|
479
|
+
// Decision (#2009): a capability that failed to LOAD must not block the loop.
|
|
480
|
+
// The prior behavior injected a BLOCKING synthetic gate (blocking:true,
|
|
481
|
+
// onError:'halt') at every point where the skipped cap declared a gate, so a
|
|
482
|
+
// single incompatible capability halted every ship:pre / verify:post
|
|
483
|
+
// project-wide for a load error unrelated to what the gate checked — with no
|
|
484
|
+
// remediation surfaced. We now fail OPEN: no gate is injected (the loop proceeds
|
|
485
|
+
// and `--active-cap <failed-cap>` correctly reports it inactive), and a loud
|
|
486
|
+
// warning is emitted instead — to STDERR (which the operator, or the agent
|
|
487
|
+
// running the command, actually sees regardless of how the host workflow
|
|
488
|
+
// consumes stdout) AND in the envelope's `warnings` channel for structured
|
|
489
|
+
// consumers. The warning names the load reason and the exact
|
|
490
|
+
// `gsd capability remove <id>` remediation so the operator can clear the broken
|
|
491
|
+
// capability. blockedGates is still recorded by the loader; only the consequence
|
|
492
|
+
// changes from block to warn. step/contribution overlays were already skip-open.
|
|
493
|
+
//
|
|
494
|
+
// The gate injection was dropped rather than made non-blocking because no host
|
|
495
|
+
// workflow generically surfaces an arbitrary gate's message at ship:pre /
|
|
496
|
+
// verify:post (consumers dispatch on specific capIds / ref.skills), and the
|
|
497
|
+
// generic gate consumers expect an object-shaped `check`, not a prose string —
|
|
498
|
+
// so an injected advisory gate would be silently dropped or mis-dispatched. A
|
|
499
|
+
// stderr warning is the channel that is actually surfaced. (See #2009 review.)
|
|
464
500
|
const overlayMeta = registry['_overlay'];
|
|
501
|
+
const loadFailWarnings = [];
|
|
465
502
|
if (overlayMeta && Array.isArray(overlayMeta.blockedGates)) {
|
|
466
503
|
for (const blocked of overlayMeta.blockedGates) {
|
|
467
|
-
if (blocked.point
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
504
|
+
if (blocked.point !== point)
|
|
505
|
+
continue;
|
|
506
|
+
// Security (#2009 review): capId/reason come from a third-party manifest or
|
|
507
|
+
// directory name. Validate capId before embedding it in the runnable
|
|
508
|
+
// remediation command; withhold it (no runnable command) if it is not a
|
|
509
|
+
// canonical id. Strip control chars/backticks from reason.
|
|
510
|
+
const idValid = LOAD_FAIL_CAP_ID_RE.test(String(blocked.capId));
|
|
511
|
+
const capLabel = idValid
|
|
512
|
+
? `"${blocked.capId}"`
|
|
513
|
+
: 'with an invalid id (withheld) under .gsd/capabilities/';
|
|
514
|
+
const remediation = idValid
|
|
515
|
+
? `Run \`gsd capability remove ${blocked.capId}\` to remove it, or fix the load error.`
|
|
516
|
+
: 'Remove the offending capability directory under .gsd/capabilities/, or fix the load error.';
|
|
517
|
+
loadFailWarnings.push(`capability ${capLabel} failed to load (${sanitizeLoadFailReason(blocked.reason)}); ` +
|
|
518
|
+
`its gate at ${point} is SKIPPED and NOT enforced (failing open). ${remediation}`);
|
|
477
519
|
}
|
|
478
520
|
}
|
|
521
|
+
// Emit loudly to stderr in EVERY output mode (including --active-cap), so a
|
|
522
|
+
// skipped gate is never silently invisible to the operator/agent.
|
|
523
|
+
for (const w of loadFailWarnings) {
|
|
524
|
+
process.stderr.write(`gsd: warning — ${w}\n`);
|
|
525
|
+
}
|
|
479
526
|
// --active-cap mode: print exactly 'true' or 'false' with no envelope
|
|
480
527
|
if (activeCapId !== undefined) {
|
|
481
528
|
const isActive = resolved.activeHooks.some((h) => h.capId === activeCapId);
|
|
@@ -488,8 +535,12 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
|
|
|
488
535
|
activeHooks: resolved.activeHooks,
|
|
489
536
|
rendered,
|
|
490
537
|
};
|
|
491
|
-
|
|
492
|
-
|
|
538
|
+
// Surface capability-state warnings and the #2009 load-failure fail-open
|
|
539
|
+
// warnings together in the structured `warnings` channel (in addition to the
|
|
540
|
+
// stderr emission above, which is the channel host workflows actually see).
|
|
541
|
+
const combinedWarnings = [...(state.warnings || []), ...loadFailWarnings];
|
|
542
|
+
if (combinedWarnings.length > 0) {
|
|
543
|
+
envelope.warnings = combinedWarnings;
|
|
493
544
|
}
|
|
494
545
|
coreOutput(envelope, raw);
|
|
495
546
|
}
|
|
@@ -17,6 +17,7 @@ exports.collectSections = collectSections;
|
|
|
17
17
|
exports.collectSection = collectSection;
|
|
18
18
|
exports.iterateBullets = iterateBullets;
|
|
19
19
|
exports.extractTaggedBlocks = extractTaggedBlocks;
|
|
20
|
+
exports.stripTaggedBlocks = stripTaggedBlocks;
|
|
20
21
|
exports.replaceSection = replaceSection;
|
|
21
22
|
// ─── stripFencedCode ──────────────────────────────────────────────────────────
|
|
22
23
|
/**
|
|
@@ -407,25 +408,26 @@ function iterateBullets(sectionText) {
|
|
|
407
408
|
* fenced code blocks itself. If a `<tagName>` block appears inside a fenced code
|
|
408
409
|
* block and should be excluded, the caller should apply `stripFencedCode` first.
|
|
409
410
|
*
|
|
410
|
-
* **Nested tags are NOT supported.** The
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
411
|
+
* **Nested tags are NOT supported.** The body scan terminates at the NEXT
|
|
412
|
+
* opening of the same tag (the ReDoS-safe boundary, #2128). Given
|
|
413
|
+
* `<x><x>inner</x></x>`, `extractTaggedBlocks(content, 'x')` returns `['inner']`
|
|
414
|
+
* — the well-formed inner block; the unterminated outer `<x>` is skipped.
|
|
415
|
+
* Callers that need true nesting must use a proper XML/HTML parser.
|
|
416
|
+
*
|
|
417
|
+
* `allowAttributes` (default `false`): when `true`, the opening tag may carry
|
|
418
|
+
* bounded attributes (`<tag foo="x">`) — needed for `<task type="…">` blocks.
|
|
419
|
+
* Leave `false` for tags that must match exactly (e.g. `<decisions>`), and never
|
|
420
|
+
* enable it for a tag where an attributed form is semantically distinct.
|
|
417
421
|
*
|
|
418
422
|
* Generalises `decisions.cts`'s bespoke `matchAll(/<decisions>([\s\S]*?)<\/decisions>/g)`
|
|
419
423
|
* so tier T1 can drop its own copy (tracked duplication until T1 lands).
|
|
420
424
|
*/
|
|
421
|
-
function extractTaggedBlocks(content, tagName) {
|
|
425
|
+
function extractTaggedBlocks(content, tagName, allowAttributes = false) {
|
|
422
426
|
if (typeof content !== 'string' || content.length === 0)
|
|
423
427
|
return [];
|
|
424
428
|
if (typeof tagName !== 'string' || tagName.length === 0)
|
|
425
429
|
return [];
|
|
426
|
-
|
|
427
|
-
const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
428
|
-
const pattern = new RegExp(`<${escapedTag}>([\\s\\S]*?)</${escapedTag}>`, 'g');
|
|
430
|
+
const pattern = taggedBlockPattern(tagName, 'g', allowAttributes);
|
|
429
431
|
const results = [];
|
|
430
432
|
let match;
|
|
431
433
|
while ((match = pattern.exec(content)) !== null) {
|
|
@@ -433,6 +435,43 @@ function extractTaggedBlocks(content, tagName) {
|
|
|
433
435
|
}
|
|
434
436
|
return results;
|
|
435
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* Build the single, ReDoS-safe `<tag>…</tag>` block regex shared by
|
|
440
|
+
* `extractTaggedBlocks` (extract bodies) and `stripTaggedBlocks` (remove blocks).
|
|
441
|
+
*
|
|
442
|
+
* Safety: the body terminates at the NEXT opening of this tag (stop-at-next-open)
|
|
443
|
+
* instead of lazily rescanning the whole remaining document for a `</tag>` that
|
|
444
|
+
* may never appear — so a document full of unclosed `<tag>` openings scans
|
|
445
|
+
* LINEARLY, not quadratically (#2128). Group 1 is the block body.
|
|
446
|
+
*
|
|
447
|
+
* `allowAttributes`: when `true`, the opener accepts bounded attributes
|
|
448
|
+
* (`<tag foo="x">`) and the body boundary is `<tag` followed by a space or `>`.
|
|
449
|
+
* When `false`, the opener is the EXACT `<tag>` and the boundary is exact `<tag>`,
|
|
450
|
+
* so an attributed `<tag foo>` is neither an opener nor a boundary — it is body
|
|
451
|
+
* content. That exact form is load-bearing for `<details>` stripping: `<details
|
|
452
|
+
* open>` marks the ACTIVE milestone and must be preserved, not stripped (#557).
|
|
453
|
+
*/
|
|
454
|
+
function taggedBlockPattern(tagName, flags, allowAttributes) {
|
|
455
|
+
const esc = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
456
|
+
const open = allowAttributes ? `<${esc}(?:\\s[^>]{0,1000})?>` : `<${esc}>`;
|
|
457
|
+
const boundary = allowAttributes ? `<${esc}[\\s>]` : `<${esc}>`;
|
|
458
|
+
return new RegExp(`${open}((?:(?!${boundary})[\\s\\S])*?)</${esc}>`, flags);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Remove every `<tagName>…</tagName>` block (opening tag, body, and closing tag)
|
|
462
|
+
* from `content`. The ReDoS-safe counterpart to `extractTaggedBlocks` — same
|
|
463
|
+
* hardened pattern, `.replace(…, '')` instead of body extraction. `allowAttributes`
|
|
464
|
+
* defaults to `false` so `<details open>` (active milestone) is preserved (#557);
|
|
465
|
+
* case-insensitive by default (matching the `<details>` strip call sites), pass
|
|
466
|
+
* `caseSensitive` to force exact-case matching.
|
|
467
|
+
*/
|
|
468
|
+
function stripTaggedBlocks(content, tagName, allowAttributes = false, caseSensitive = false) {
|
|
469
|
+
if (typeof content !== 'string' || content.length === 0)
|
|
470
|
+
return '';
|
|
471
|
+
if (typeof tagName !== 'string' || tagName.length === 0)
|
|
472
|
+
return content;
|
|
473
|
+
return content.replace(taggedBlockPattern(tagName, caseSensitive ? 'g' : 'gi', allowAttributes), '');
|
|
474
|
+
}
|
|
436
475
|
// ─── replaceSection ───────────────────────────────────────────────────────────
|
|
437
476
|
/**
|
|
438
477
|
* Splice `newBody` in place of a section's body and return the resulting
|
|
@@ -5,8 +5,16 @@
|
|
|
5
5
|
* so any MCP-consuming host (Claude/Codex/OpenCode/VS Code/Gemini/Cursor/Cline/
|
|
6
6
|
* Hermes) can drive GSD with NO bespoke plugin:
|
|
7
7
|
*
|
|
8
|
-
* - point 1 (command): tool `gsd_invoke_command` →
|
|
9
|
-
* (
|
|
8
|
+
* - point 1 (command): tool `gsd_invoke_command` → `dispatchGsdCommand`
|
|
9
|
+
* (src/shell-command-projection.cts), a bounded subprocess-shim to
|
|
10
|
+
* gsd-tools.cjs. #2102 Stage 2: `commandRoutingHub.createHub()` called
|
|
11
|
+
* with no args here always hit `if(!_cjsRegistry) return
|
|
12
|
+
* makeUnknownCommand()` — every dispatch was UnknownCommand. No
|
|
13
|
+
* fully-populated hub factory exists anywhere in gsd-core (every
|
|
14
|
+
* createHub() caller builds a single-family hub for its own narrow
|
|
15
|
+
* purpose), so the fix routes through the SAME shared dispatch helper
|
|
16
|
+
* the pi extension uses (pi/gsd.cjs), mirroring the SUBPROCESS-REUSE
|
|
17
|
+
* precedent already established for the OpenCode/Kilo hook bridge.
|
|
10
18
|
* - point 5 (state IO): tools `gsd_read_state` / `gsd_write_state` → the
|
|
11
19
|
* Phase 3 `stateIO` seam (src/state-io.cts, filesystem default).
|
|
12
20
|
*
|
|
@@ -24,9 +32,10 @@ exports.SERVER_NAME = exports.PROTOCOL_VERSION = void 0;
|
|
|
24
32
|
exports.handleMessage = handleMessage;
|
|
25
33
|
exports.runServer = runServer;
|
|
26
34
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
|
-
const commandRoutingHub = require("./command-routing-hub.cjs");
|
|
28
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
29
35
|
const stateIo = require("./state-io.cjs");
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
|
+
const shellCommandProjection = require("./shell-command-projection.cjs");
|
|
38
|
+
const { dispatchGsdCommand } = shellCommandProjection;
|
|
30
39
|
exports.PROTOCOL_VERSION = '2024-11-05';
|
|
31
40
|
exports.SERVER_NAME = 'gsd-core';
|
|
32
41
|
const SERVER_VERSION = '1.7.0';
|
|
@@ -94,9 +103,11 @@ function callTool(name, args, ctx) {
|
|
|
94
103
|
if (!family || !subcommand) {
|
|
95
104
|
return { isError: true, content: [{ type: 'text', text: 'gsd_invoke_command requires string "family" and "subcommand".' }] };
|
|
96
105
|
}
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
const res = dispatchGsdCommand({ family, subcommand, args: Array.isArray(a.args) ? a.args : [], cwd });
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
return { isError: true, content: [{ type: 'text', text: res.stderr || res.stdout || `dispatch failed (exit ${res.code})` }] };
|
|
109
|
+
}
|
|
110
|
+
return { content: [{ type: 'text', text: res.stdout }] };
|
|
100
111
|
}
|
|
101
112
|
if (name === 'gsd_read_state') {
|
|
102
113
|
const p = asString(a.path);
|
|
@@ -26,7 +26,7 @@ const ioMod = require("./io.cjs");
|
|
|
26
26
|
const { output, error } = ioMod;
|
|
27
27
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
28
|
const phaseIdMod = require("./phase-id.cjs");
|
|
29
|
-
const { escapeRegex, normalizePhaseName, phaseTokenMatches } = phaseIdMod;
|
|
29
|
+
const { escapeRegex, normalizePhaseName, phaseTokenMatches, PHASE_NUMBER_TOKEN_SOURCE } = phaseIdMod;
|
|
30
30
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
31
|
const roadmapParserMod = require("./roadmap-parser.cjs");
|
|
32
32
|
const { getMilestonePhaseFilter, extractCurrentMilestone, getMilestoneInfo } = roadmapParserMod;
|
|
@@ -156,8 +156,8 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
156
156
|
if (stateVersion && stateVersion === version) {
|
|
157
157
|
const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
158
158
|
const scopedContent = extractCurrentMilestone(roadmapContent, cwd);
|
|
159
|
-
// #1729: `(?:\s*\([^)\n]
|
|
160
|
-
const phasePattern =
|
|
159
|
+
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
160
|
+
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
|
|
161
161
|
const noDirectoryPhases = [];
|
|
162
162
|
let pm;
|
|
163
163
|
const phaseDirEntries = (() => {
|