@opengsd/gsd-core 1.6.0-rc.2 → 1.6.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/plugin.json +2 -1
- package/agents/gsd-advisor-researcher.md +2 -0
- package/agents/gsd-ai-researcher.md +2 -0
- package/agents/gsd-assumptions-analyzer.md +2 -0
- package/agents/gsd-doc-classifier.md +2 -0
- package/agents/gsd-doc-synthesizer.md +2 -0
- package/agents/gsd-domain-researcher.md +2 -0
- package/agents/gsd-eval-auditor.md +6 -9
- package/agents/gsd-phase-researcher.md +2 -0
- package/agents/gsd-planner.md +8 -57
- package/agents/gsd-project-researcher.md +2 -0
- package/agents/gsd-research-synthesizer.md +2 -0
- package/agents/gsd-security-auditor.md +37 -18
- package/agents/gsd-ui-researcher.md +2 -0
- package/bin/install.js +370 -18
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +46 -4
- package/gsd-core/bin/lib/audit-command-router.cjs +52 -14
- package/gsd-core/bin/lib/capability-lifecycle.cjs +30 -7
- package/gsd-core/bin/lib/capability-registry.cjs +96 -83
- package/gsd-core/bin/lib/capability-validator.cjs +22 -0
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +40 -2
- package/gsd-core/bin/lib/command-aliases.cjs +10 -1
- package/gsd-core/bin/lib/command-routing-hub.cjs +10 -3
- package/gsd-core/bin/lib/config-schema.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +67 -24
- package/gsd-core/bin/lib/coverage.cjs +464 -0
- package/gsd-core/bin/lib/decisions.cjs +27 -0
- package/gsd-core/bin/lib/eval-command-router.cjs +21 -0
- package/gsd-core/bin/lib/eval.cjs +60 -0
- package/gsd-core/bin/lib/frontmatter.cjs +132 -13
- package/gsd-core/bin/lib/graphify-command-router.cjs +53 -36
- package/gsd-core/bin/lib/init.cjs +139 -30
- package/gsd-core/bin/lib/install-profiles.cjs +6 -3
- package/gsd-core/bin/lib/intel-command-router.cjs +79 -60
- package/gsd-core/bin/lib/io.cjs +1 -0
- package/gsd-core/bin/lib/phase.cjs +16 -2
- package/gsd-core/bin/lib/plan-scan.cjs +2 -2
- package/gsd-core/bin/lib/planning-workspace.cjs +157 -13
- package/gsd-core/bin/lib/profile-output.cjs +18 -6
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +53 -16
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +21 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +15 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +47 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -0
- package/gsd-core/bin/lib/state.cjs +398 -60
- package/gsd-core/bin/lib/surface.cjs +42 -10
- package/gsd-core/bin/lib/uat-predicate.cjs +13 -6
- package/gsd-core/bin/lib/update-context.cjs +2 -2
- package/gsd-core/bin/lib/verification.cjs +67 -6
- package/gsd-core/bin/lib/verify.cjs +8 -1
- package/gsd-core/bin/shared/config-defaults.manifest.json +3 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -1
- package/gsd-core/references/planner-guidance.md +66 -0
- package/gsd-core/references/planning-config.md +2 -2
- package/gsd-core/references/security-asvs-levels.md +27 -0
- package/gsd-core/references/untrusted-input-boundary.md +13 -0
- package/gsd-core/templates/SECURITY.md +6 -4
- package/gsd-core/templates/summary-complex.md +4 -0
- package/gsd-core/templates/summary-minimal.md +3 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +41 -0
- package/gsd-core/workflows/autonomous.md +53 -46
- package/gsd-core/workflows/complete-milestone.md +27 -8
- package/gsd-core/workflows/execute-phase.md +1 -1
- package/gsd-core/workflows/execute-plan.md +5 -0
- package/gsd-core/workflows/manager.md +17 -7
- package/gsd-core/workflows/new-project.md +82 -16
- package/gsd-core/workflows/plan-phase.md +15 -0
- package/gsd-core/workflows/profile-user.md +6 -2
- package/gsd-core/workflows/progress.md +37 -4
- package/gsd-core/workflows/quick.md +3 -1
- package/gsd-core/workflows/secure-phase.md +13 -7
- package/gsd-core/workflows/ship.md +3 -1
- package/gsd-core/workflows/spec-phase.md +3 -1
- package/gsd-core/workflows/transition.md +14 -12
- package/gsd-core/workflows/ui-review.md +2 -6
- package/gsd-core/workflows/verify-work.md +74 -1
- package/hooks/dist/gsd-read-injection-scanner.js +49 -25
- package/hooks/gsd-read-injection-scanner.js +49 -25
- package/hooks/hooks.json +1 -1
- package/package.json +4 -2
- package/scripts/check-alias-drift.cjs +5 -0
- package/scripts/gen-plugin-skills.cjs +117 -0
- package/scripts/lint-test-file-count.allowlist.json +2 -1
- package/scripts/prompt-injection-scan.sh +9 -0
- package/scripts/release-notes/conventional-title.cjs +88 -0
- package/scripts/release-notes/format-github-release-notes.cjs +4 -3
- package/skills/gsd-add-tests/SKILL.md +38 -0
- package/skills/gsd-ai-integration-phase/SKILL.md +37 -0
- package/skills/gsd-audit-fix/SKILL.md +33 -0
- package/skills/gsd-audit-milestone/SKILL.md +37 -0
- package/skills/gsd-audit-uat/SKILL.md +25 -0
- package/skills/gsd-autonomous/SKILL.md +51 -0
- package/skills/gsd-capture/SKILL.md +67 -0
- package/skills/gsd-cleanup/SKILL.md +24 -0
- package/skills/gsd-code-review/SKILL.md +59 -0
- package/skills/gsd-complete-milestone/SKILL.md +142 -0
- package/skills/gsd-config/SKILL.md +56 -0
- package/skills/gsd-debug/SKILL.md +53 -0
- package/skills/gsd-discuss-phase/SKILL.md +77 -0
- package/skills/gsd-docs-update/SKILL.md +49 -0
- package/skills/gsd-eval-review/SKILL.md +33 -0
- package/skills/gsd-execute-phase/SKILL.md +65 -0
- package/skills/gsd-explore/SKILL.md +28 -0
- package/skills/gsd-extract-learnings/SKILL.md +22 -0
- package/skills/gsd-fast/SKILL.md +31 -0
- package/skills/gsd-forensics/SKILL.md +56 -0
- package/skills/gsd-graphify/SKILL.md +204 -0
- package/skills/gsd-health/SKILL.md +31 -0
- package/skills/gsd-help/SKILL.md +29 -0
- package/skills/gsd-import/SKILL.md +46 -0
- package/skills/gsd-inbox/SKILL.md +39 -0
- package/skills/gsd-ingest-docs/SKILL.md +43 -0
- package/skills/gsd-manager/SKILL.md +45 -0
- package/skills/gsd-map-codebase/SKILL.md +83 -0
- package/skills/gsd-mempalace-capture/SKILL.md +71 -0
- package/skills/gsd-mempalace-recall/SKILL.md +102 -0
- package/skills/gsd-milestone-summary/SKILL.md +51 -0
- package/skills/gsd-mvp-phase/SKILL.md +45 -0
- package/skills/gsd-new-milestone/SKILL.md +45 -0
- package/skills/gsd-new-project/SKILL.md +47 -0
- package/skills/gsd-ns-context/SKILL.md +24 -0
- package/skills/gsd-ns-ideate/SKILL.md +23 -0
- package/skills/gsd-ns-manage/SKILL.md +35 -0
- package/skills/gsd-ns-project/SKILL.md +26 -0
- package/skills/gsd-ns-review/SKILL.md +28 -0
- package/skills/gsd-ns-workflow/SKILL.md +33 -0
- package/skills/gsd-pause-work/SKILL.md +43 -0
- package/skills/gsd-phase/SKILL.md +57 -0
- package/skills/gsd-plan-phase/SKILL.md +63 -0
- package/skills/gsd-plan-review-convergence/SKILL.md +60 -0
- package/skills/gsd-pr-branch/SKILL.md +26 -0
- package/skills/gsd-profile-user/SKILL.md +47 -0
- package/skills/gsd-progress/SKILL.md +49 -0
- package/skills/gsd-quick/SKILL.md +174 -0
- package/skills/gsd-resume-work/SKILL.md +31 -0
- package/skills/gsd-review/SKILL.md +42 -0
- package/skills/gsd-review-backlog/SKILL.md +63 -0
- package/skills/gsd-secure-phase/SKILL.md +36 -0
- package/skills/gsd-settings/SKILL.md +29 -0
- package/skills/gsd-ship/SKILL.md +24 -0
- package/skills/gsd-sketch/SKILL.md +60 -0
- package/skills/gsd-spec-phase/SKILL.md +63 -0
- package/skills/gsd-spike/SKILL.md +57 -0
- package/skills/gsd-stats/SKILL.md +20 -0
- package/skills/gsd-surface/SKILL.md +162 -0
- package/skills/gsd-thread/SKILL.md +24 -0
- package/skills/gsd-ui-phase/SKILL.md +35 -0
- package/skills/gsd-ui-review/SKILL.md +33 -0
- package/skills/gsd-ultraplan-phase/SKILL.md +34 -0
- package/skills/gsd-undo/SKILL.md +35 -0
- package/skills/gsd-update/SKILL.md +50 -0
- package/skills/gsd-validate-phase/SKILL.md +36 -0
- package/skills/gsd-verify-work/SKILL.md +39 -0
- package/skills/gsd-workspace/SKILL.md +53 -0
- package/skills/gsd-workstreams/SKILL.md +70 -0
|
@@ -43,7 +43,14 @@ const io = require("./io.cjs");
|
|
|
43
43
|
const coreUtils = require("./core-utils.cjs");
|
|
44
44
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
45
45
|
const path = require("path");
|
|
46
|
+
// Phase 2 (#1646): route through the Hub per ADR-959 §III(B) line 75.
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
48
|
+
const commandRoutingHub = require("./command-routing-hub.cjs");
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
|
+
const cjsCommandRouterAdapter = require("./cjs-command-router-adapter.cjs");
|
|
46
51
|
const { ERROR_REASON } = io;
|
|
52
|
+
const { makeInvalidArgs } = commandRoutingHub;
|
|
53
|
+
const { routeHubCommandFamily } = cjsCommandRouterAdapter;
|
|
47
54
|
// Default CoreModule implementation assembled from leaf modules.
|
|
48
55
|
// _core seam overrides this entirely for test injection.
|
|
49
56
|
const _defaultCore = { output: io.output, timeAgo: coreUtils.timeAgo };
|
|
@@ -52,67 +59,79 @@ function routeIntelCommand({ args, cwd, raw, error, _intel, _core }) {
|
|
|
52
59
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
53
60
|
const intel = _intel ?? require('./intel.cjs');
|
|
54
61
|
const c = _core ?? _defaultCore;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
62
|
+
// Phase 2 (#1646): routes through the Command Routing Hub per ADR-959 §III(B)
|
|
63
|
+
// line 75. Validation handlers return `makeInvalidArgs(...)` Results; the
|
|
64
|
+
// Hub → adapter translation preserves ERROR_REASON granularity via the
|
|
65
|
+
// exitReason field (Phase 1, #1644). Success handlers keep direct `c.output()`
|
|
66
|
+
// calls. The timeAgo mutation in non-raw `status` is preserved. Lazy require
|
|
67
|
+
// of intel.cjs inside the function is preserved (loads only when dispatched).
|
|
68
|
+
routeHubCommandFamily({
|
|
69
|
+
family: 'intel',
|
|
70
|
+
args,
|
|
71
|
+
// Alphabetical for stable unknownMessage text; the integration test asserts
|
|
72
|
+
// inclusion of all 9 subcommands, not order.
|
|
73
|
+
subcommands: ['api-surface', 'diff', 'extract-exports', 'patch-meta', 'query', 'snapshot', 'status', 'update', 'validate'],
|
|
74
|
+
handlers: {
|
|
75
|
+
query: () => {
|
|
76
|
+
const term = args[2];
|
|
77
|
+
if (!term) {
|
|
78
|
+
return makeInvalidArgs('term', 'Usage: gsd-tools intel query <term>', ERROR_REASON.USAGE);
|
|
79
|
+
}
|
|
80
|
+
const planningDir = path.join(cwd, '.planning');
|
|
81
|
+
c.output(intel.intelQuery(term, planningDir), raw);
|
|
82
|
+
},
|
|
83
|
+
status: () => {
|
|
84
|
+
const planningDir = path.join(cwd, '.planning');
|
|
85
|
+
const status = intel.intelStatus(planningDir);
|
|
86
|
+
if (!raw && status.files) {
|
|
87
|
+
for (const file of Object.values(status.files)) {
|
|
88
|
+
if (file.updated_at) {
|
|
89
|
+
file.updated_at = c.timeAgo(new Date(file.updated_at));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
c.output(status, raw);
|
|
94
|
+
},
|
|
95
|
+
diff: () => {
|
|
96
|
+
const planningDir = path.join(cwd, '.planning');
|
|
97
|
+
c.output(intel.intelDiff(planningDir), raw);
|
|
98
|
+
},
|
|
99
|
+
snapshot: () => {
|
|
100
|
+
const planningDir = path.join(cwd, '.planning');
|
|
101
|
+
c.output(intel.intelSnapshot(planningDir), raw);
|
|
102
|
+
},
|
|
103
|
+
'patch-meta': () => {
|
|
104
|
+
const filePath = args[2];
|
|
105
|
+
if (!filePath) {
|
|
106
|
+
return makeInvalidArgs('file-path', 'Usage: gsd-tools intel patch-meta <file-path>', ERROR_REASON.USAGE);
|
|
107
|
+
}
|
|
108
|
+
c.output(intel.intelPatchMeta(path.resolve(cwd, filePath)), raw);
|
|
109
|
+
},
|
|
110
|
+
validate: () => {
|
|
111
|
+
const planningDir = path.join(cwd, '.planning');
|
|
112
|
+
c.output(intel.intelValidate(planningDir), raw);
|
|
113
|
+
},
|
|
114
|
+
'extract-exports': () => {
|
|
115
|
+
const filePath = args[2];
|
|
116
|
+
if (!filePath) {
|
|
117
|
+
return makeInvalidArgs('file-path', 'Usage: gsd-tools intel extract-exports <file-path>', ERROR_REASON.USAGE);
|
|
72
118
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
c.output(intel.intelPatchMeta(path.resolve(cwd, filePath)), raw);
|
|
92
|
-
}
|
|
93
|
-
else if (subcommand === 'validate') {
|
|
94
|
-
const planningDir = path.join(cwd, '.planning');
|
|
95
|
-
c.output(intel.intelValidate(planningDir), raw);
|
|
96
|
-
}
|
|
97
|
-
else if (subcommand === 'extract-exports') {
|
|
98
|
-
const filePath = args[2];
|
|
99
|
-
if (!filePath) {
|
|
100
|
-
error('Usage: gsd-tools intel extract-exports <file-path>', ERROR_REASON.USAGE);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
c.output(intel.intelExtractExports(path.resolve(cwd, filePath)), raw);
|
|
104
|
-
}
|
|
105
|
-
else if (subcommand === 'update') {
|
|
106
|
-
const planningDir = path.join(cwd, '.planning');
|
|
107
|
-
c.output(intel.intelUpdate(planningDir), raw);
|
|
108
|
-
}
|
|
109
|
-
else if (subcommand === 'api-surface') {
|
|
110
|
-
const planningDir = path.join(cwd, '.planning');
|
|
111
|
-
c.output(intel.intelApiSurface(planningDir), raw);
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
error('Unknown intel subcommand. Available: query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
115
|
-
}
|
|
119
|
+
c.output(intel.intelExtractExports(path.resolve(cwd, filePath)), raw);
|
|
120
|
+
},
|
|
121
|
+
update: () => {
|
|
122
|
+
const planningDir = path.join(cwd, '.planning');
|
|
123
|
+
c.output(intel.intelUpdate(planningDir), raw);
|
|
124
|
+
},
|
|
125
|
+
'api-surface': () => {
|
|
126
|
+
const planningDir = path.join(cwd, '.planning');
|
|
127
|
+
c.output(intel.intelApiSurface(planningDir), raw);
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
unknownMessage: (subcommand, available) => `Unknown intel subcommand. Available: ${available.join(', ')}`,
|
|
131
|
+
error,
|
|
132
|
+
cwd,
|
|
133
|
+
raw,
|
|
134
|
+
});
|
|
116
135
|
}
|
|
117
136
|
module.exports = {
|
|
118
137
|
routeIntelCommand,
|
package/gsd-core/bin/lib/io.cjs
CHANGED
|
@@ -167,6 +167,7 @@ const ERROR_REASON = Object.freeze({
|
|
|
167
167
|
SDK_MISSING_ARG: 'sdk_missing_arg',
|
|
168
168
|
// workflow / phase
|
|
169
169
|
PHASE_NOT_FOUND: 'phase_not_found',
|
|
170
|
+
PHASE_VERIFICATION_INCOMPLETE: 'phase_verification_incomplete',
|
|
170
171
|
SUMMARY_NO_PLANNING: 'summary_no_planning',
|
|
171
172
|
// graphify
|
|
172
173
|
GRAPHIFY_NO_GRAPH: 'graphify_no_graph',
|
|
@@ -52,6 +52,9 @@ const clock_cjs_1 = require("./clock.cjs");
|
|
|
52
52
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- uat-predicate.cjs is an export= CommonJS module
|
|
53
53
|
const uatPredicate = require("./uat-predicate.cjs");
|
|
54
54
|
const { evaluateUatPassed } = uatPredicate;
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- verification.cjs is an export= CommonJS module
|
|
56
|
+
const verificationMod = require("./verification.cjs");
|
|
57
|
+
const { readVerificationStatus } = verificationMod;
|
|
55
58
|
const { planningDir, withPlanningLock } = planningWorkspace;
|
|
56
59
|
const { extractFrontmatter } = frontmatterMod;
|
|
57
60
|
const { readModifyWriteStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, syncStateFrontmatter, withStateLock, updatePerformanceMetricsSection, } = stateMod;
|
|
@@ -1077,8 +1080,8 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1077
1080
|
: 0;
|
|
1078
1081
|
let requirementsUpdated = false;
|
|
1079
1082
|
const warnings = [];
|
|
1083
|
+
const phaseFullDir = node_path_1.default.join(cwd, phaseInfo['directory']);
|
|
1080
1084
|
try {
|
|
1081
|
-
const phaseFullDir = node_path_1.default.join(cwd, phaseInfo['directory']);
|
|
1082
1085
|
const phaseFiles = node_fs_1.default.readdirSync(phaseFullDir);
|
|
1083
1086
|
for (const file of phaseFiles.filter((f) => f.includes('-UAT') && f.endsWith('.md'))) {
|
|
1084
1087
|
const content = node_fs_1.default.readFileSync(node_path_1.default.join(phaseFullDir, file), 'utf-8');
|
|
@@ -1113,7 +1116,11 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1113
1116
|
let nextPhaseNum = null;
|
|
1114
1117
|
let nextPhaseName = null;
|
|
1115
1118
|
let isLastPhase = true;
|
|
1116
|
-
withPlanningLock(cwd, () => {
|
|
1119
|
+
const verificationBlocked = withPlanningLock(cwd, () => {
|
|
1120
|
+
const verificationStatus = readVerificationStatus(phaseFullDir);
|
|
1121
|
+
if (verificationStatus.status !== 'passed') {
|
|
1122
|
+
return verificationStatus;
|
|
1123
|
+
}
|
|
1117
1124
|
const runPhaseCompleteTransaction = () => {
|
|
1118
1125
|
const writes = [];
|
|
1119
1126
|
let roadmapContent = null;
|
|
@@ -1381,7 +1388,14 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1381
1388
|
else {
|
|
1382
1389
|
runPhaseCompleteTransaction();
|
|
1383
1390
|
}
|
|
1391
|
+
return null;
|
|
1384
1392
|
});
|
|
1393
|
+
if (verificationBlocked) {
|
|
1394
|
+
const nextStep = verificationBlocked.next_command
|
|
1395
|
+
? ` Next: ${verificationBlocked.next_command}`
|
|
1396
|
+
: '';
|
|
1397
|
+
error(`Phase ${phaseNum} verification is incomplete: ${verificationBlocked.next_action}${nextStep}`, ERROR_REASON.PHASE_VERIFICATION_INCOMPLETE);
|
|
1398
|
+
}
|
|
1385
1399
|
let autoPruned = false;
|
|
1386
1400
|
try {
|
|
1387
1401
|
const configPath = node_path_1.default.join(planningDir(cwd), 'config.json');
|
|
@@ -63,8 +63,8 @@ function scanPhasePlans(phaseDir) {
|
|
|
63
63
|
if ((0, node_fs_1.existsSync)(nestedDir)) {
|
|
64
64
|
try {
|
|
65
65
|
const nestedFiles = (0, node_fs_1.readdirSync)(nestedDir);
|
|
66
|
-
nestedPlanFiles = nestedFiles.filter(isNestedPlanFile);
|
|
67
|
-
nestedSummaryFiles = nestedFiles.filter(isNestedSummaryFile);
|
|
66
|
+
nestedPlanFiles = nestedFiles.filter(isNestedPlanFile).map((file) => `plans/${file}`);
|
|
67
|
+
nestedSummaryFiles = nestedFiles.filter(isNestedSummaryFile).map((file) => `plans/${file}`);
|
|
68
68
|
hasNestedPlans = nestedPlanFiles.length > 0;
|
|
69
69
|
}
|
|
70
70
|
catch { /* ignore unreadable nested layout */ }
|
|
@@ -33,6 +33,54 @@ process.on('exit', () => {
|
|
|
33
33
|
catch { /* already gone */ }
|
|
34
34
|
}
|
|
35
35
|
});
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Lock liveness probe (test seam) — audit M1
|
|
38
|
+
//
|
|
39
|
+
// mtime is a leaky proxy for "the holder is alive". The prior withPlanningLock
|
|
40
|
+
// timeout fallback unconditionally unlinked WHATEVER lock existed — even a fresh,
|
|
41
|
+
// live holder's — and re-acquired it, force-stealing a live writer's critical
|
|
42
|
+
// section. We backport capability-lock.cts's pid-liveness gate: a dead holder is
|
|
43
|
+
// stolen promptly inside the polite loop; a live holder is waited on. The
|
|
44
|
+
// indirection lets unit tests inject a deterministic isPidAlive without real pids.
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
/** Is `pid` a live process? process.kill(pid, 0) succeeds for a live (signalable) process. */
|
|
47
|
+
function _realIsPidAlive(pid) {
|
|
48
|
+
try {
|
|
49
|
+
process.kill(pid, 0);
|
|
50
|
+
return true; // signalable → alive
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
// EPERM = process exists but we cannot signal it (still ALIVE). ESRCH = gone.
|
|
54
|
+
return err.code === 'EPERM';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const _planningLockProbes = { isPidAlive: _realIsPidAlive };
|
|
58
|
+
function _planningLockIsPidAlive(pid) {
|
|
59
|
+
return _planningLockProbes.isPidAlive(pid);
|
|
60
|
+
}
|
|
61
|
+
const _planningLockTestHooks = {};
|
|
62
|
+
// Monotonic sequence for unique stale-steal rename targets (no crypto dependency).
|
|
63
|
+
let _planningStealSeq = 0;
|
|
64
|
+
/**
|
|
65
|
+
* Is the holder recorded in the .lock body VERIFIED-LIVE? The body is JSON
|
|
66
|
+
* { pid, cwd, acquired }. Returns true ONLY when the body parses AND the recorded
|
|
67
|
+
* pid signals alive. A garbage / pid-less / unreadable body (or a dead pid) is NOT
|
|
68
|
+
* verified-live, so the lock stays stealable — corrupt locks never block forever,
|
|
69
|
+
* and a live holder is never force-stolen.
|
|
70
|
+
*/
|
|
71
|
+
function _planningHolderVerifiedLive(lockPath) {
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(node_fs_1.default.readFileSync(lockPath, 'utf-8'));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false; // unreadable / unparseable body → cannot verify → not verified-live
|
|
78
|
+
}
|
|
79
|
+
const pid = parsed?.pid;
|
|
80
|
+
if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0)
|
|
81
|
+
return false;
|
|
82
|
+
return _planningLockIsPidAlive(pid);
|
|
83
|
+
}
|
|
36
84
|
// Transient errno codes that indicate a temporary filesystem condition under
|
|
37
85
|
// concurrent O_EXCL races — Docker overlay-fs (ENOENT/EINVAL/EIO), NFS
|
|
38
86
|
// (ESTALE), and OS-level interrupt/retry signals (EAGAIN/EINTR). These are
|
|
@@ -97,6 +145,12 @@ function withPlanningLock(cwd, fn, clock) {
|
|
|
97
145
|
clock = clock_cjs_1.realClock;
|
|
98
146
|
const lockPath = node_path_1.default.join(planningDir(cwd), '.lock');
|
|
99
147
|
const lockTimeout = 10000; // 10 seconds
|
|
148
|
+
// Deadman ceiling (audit M1 / R4-FIX) — set ABOVE lockTimeout so a holder that reads
|
|
149
|
+
// as alive but is actually a pid-reuse alias (the .lock body has no startTime, so
|
|
150
|
+
// liveness alone cannot detect reuse) is still recovered once its lock ages past this
|
|
151
|
+
// absolute ceiling. Without it, a false-alive holder would make withPlanningLock throw
|
|
152
|
+
// on every call with no self-heal. Mirrors acquireStateLock's deadmanCeilingMs.
|
|
153
|
+
const deadmanCeilingMs = 60000;
|
|
100
154
|
const start = clock.now();
|
|
101
155
|
// Ensure .planning/ exists
|
|
102
156
|
try {
|
|
@@ -143,31 +197,104 @@ function withPlanningLock(cwd, fn, clock) {
|
|
|
143
197
|
continue;
|
|
144
198
|
}
|
|
145
199
|
if (nodeErr.code === 'EEXIST') {
|
|
146
|
-
//
|
|
200
|
+
// Liveness-gated steal (audit M1). Steal the lock PROMPTLY only when its
|
|
201
|
+
// recorded holder is NOT verified-live (crashed/dead pid or garbage body).
|
|
202
|
+
// A verified-live holder is waited on — never force-stolen — because nuking
|
|
203
|
+
// a slow-but-live writer's lock corrupts the .planning/ critical section.
|
|
204
|
+
// The steal is an ATOMIC rename-then-recreate guarded by an identity re-confirm
|
|
205
|
+
// so a racer that recreates a fresh lock in the decision→steal gap never has
|
|
206
|
+
// its replacement deleted (audit M2 / PR #1532 review, window b). The body is
|
|
207
|
+
// written atomically (writeFileSync …{flag:'wx'}) so there is no empty-body
|
|
208
|
+
// create window here — only the double-steal needs hardening.
|
|
147
209
|
try {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
210
|
+
const decisionStat = node_fs_1.default.statSync(lockPath);
|
|
211
|
+
// Snapshot the decision-time body too: (dev, ino) alone is defeated by inode
|
|
212
|
+
// REUSE (a racer's unlink+recreate can land on the same inode), so the body
|
|
213
|
+
// content binds the identity as well — mirrors capability-lock.cts's (dev,
|
|
214
|
+
// ino, ts) re-confirm.
|
|
215
|
+
let decisionBody;
|
|
216
|
+
try {
|
|
217
|
+
decisionBody = node_fs_1.default.readFileSync(lockPath, 'utf-8');
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
decisionBody = null;
|
|
221
|
+
}
|
|
222
|
+
let stealable = !_planningHolderVerifiedLive(lockPath);
|
|
223
|
+
if (!stealable) {
|
|
224
|
+
// Verified-live, but recover anyway once the lock crosses the absolute
|
|
225
|
+
// deadman ceiling — defeats a pid-reuse false-alive that would otherwise
|
|
226
|
+
// block forever (R4-FIX; mtime age is from lock creation, not this call).
|
|
227
|
+
const age = clock.now() - decisionStat.mtimeMs;
|
|
228
|
+
stealable = age > deadmanCeilingMs;
|
|
229
|
+
}
|
|
230
|
+
if (stealable) {
|
|
231
|
+
if (_planningLockTestHooks.beforeSteal)
|
|
232
|
+
_planningLockTestHooks.beforeSteal({ lockPath });
|
|
233
|
+
// Identity re-confirm immediately before the steal: a racer that stole +
|
|
234
|
+
// recreated a fresh lock in the decision→steal gap changes (dev, ino) → do
|
|
235
|
+
// NOT delete the replacement; back off and re-evaluate.
|
|
236
|
+
let confirmStat;
|
|
237
|
+
try {
|
|
238
|
+
confirmStat = node_fs_1.default.statSync(lockPath);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
continue; // vanished between decision and steal — retry the create.
|
|
242
|
+
}
|
|
243
|
+
let confirmBody;
|
|
244
|
+
try {
|
|
245
|
+
confirmBody = node_fs_1.default.readFileSync(lockPath, 'utf-8');
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
confirmBody = null;
|
|
249
|
+
}
|
|
250
|
+
const sameInstance = typeof decisionStat.dev === 'number' && typeof decisionStat.ino === 'number' &&
|
|
251
|
+
confirmStat.dev === decisionStat.dev && confirmStat.ino === decisionStat.ino &&
|
|
252
|
+
decisionBody !== null && confirmBody === decisionBody;
|
|
253
|
+
if (!sameInstance) {
|
|
254
|
+
clock.sleep(100); // a racer won the steal + recreated — re-evaluate, don't delete it.
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
// Atomic steal: rename the inode aside, then remove it. Only ONE racer can
|
|
258
|
+
// win the rename; a failed rename means another process already stole it, so
|
|
259
|
+
// we must NOT fall through to a delete — back off and retry the create.
|
|
260
|
+
const stolen = lockPath + '.stale-' + process.pid + '-' + clock.now() + '-' + (_planningStealSeq++);
|
|
261
|
+
let renamed = false;
|
|
262
|
+
try {
|
|
263
|
+
node_fs_1.default.renameSync(lockPath, stolen);
|
|
264
|
+
renamed = true;
|
|
265
|
+
}
|
|
266
|
+
catch { /* another racer won */ }
|
|
267
|
+
if (renamed) {
|
|
268
|
+
try {
|
|
269
|
+
node_fs_1.default.rmSync(stolen, { force: true });
|
|
270
|
+
}
|
|
271
|
+
catch { /* best-effort */ }
|
|
272
|
+
continue; // dead/garbage/expired holder freed — retry immediately to grab it.
|
|
273
|
+
}
|
|
274
|
+
clock.sleep(100); // lost the steal race — back off and retry.
|
|
275
|
+
continue;
|
|
152
276
|
}
|
|
153
277
|
}
|
|
154
278
|
catch {
|
|
155
279
|
continue;
|
|
156
280
|
}
|
|
157
|
-
//
|
|
281
|
+
// Live holder — wait and retry (cross-platform, no shell dependency).
|
|
158
282
|
clock.sleep(100);
|
|
159
283
|
continue;
|
|
160
284
|
}
|
|
161
285
|
throw err;
|
|
162
286
|
}
|
|
163
287
|
}
|
|
164
|
-
// Timeout
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
288
|
+
// Timeout against a holder still present at budget exhaustion. The polite loop
|
|
289
|
+
// already stole any DEAD holder; reaching here means the holder is verified-live
|
|
290
|
+
// (or a pid-reuse alias we must not corrupt). Do NOT force-steal — the prior
|
|
291
|
+
// unconditional `unlinkSync(lockPath); acquireLock()` here (audit M1) robbed live
|
|
292
|
+
// writers, and its re-acquire sat OUTSIDE any try so a concurrent re-create raced
|
|
293
|
+
// a raw EEXIST out of the helper (audit M2). Surface a clear timeout error instead.
|
|
294
|
+
const timeoutErr = new Error('withPlanningLock: ' + lockPath + ' held by a live process for ' +
|
|
295
|
+
(clock.now() - start) + 'ms (exceeded ' + lockTimeout + 'ms budget)');
|
|
296
|
+
timeoutErr.lockTimeout = true;
|
|
297
|
+
throw timeoutErr;
|
|
171
298
|
}
|
|
172
299
|
function createPlanningWorkspace(cwd, opts = {}) {
|
|
173
300
|
return {
|
|
@@ -242,4 +369,21 @@ module.exports = {
|
|
|
242
369
|
getActiveWorkstream,
|
|
243
370
|
setActiveWorkstream,
|
|
244
371
|
findContextMdIn,
|
|
372
|
+
// Test seam (audit M1): inject a deterministic isPidAlive so the liveness-gated
|
|
373
|
+
// steal decision is exercised without real pids. Mirrors capability-lock.cts.
|
|
374
|
+
_setLockProbes(probes) {
|
|
375
|
+
if (typeof probes.isPidAlive === 'function')
|
|
376
|
+
_planningLockProbes.isPidAlive = probes.isPidAlive;
|
|
377
|
+
},
|
|
378
|
+
_resetLockProbes() {
|
|
379
|
+
_planningLockProbes.isPidAlive = _realIsPidAlive;
|
|
380
|
+
},
|
|
381
|
+
// Test seam (PR #1532 review): script the steal decision→steal gap (window b).
|
|
382
|
+
_setPlanningLockTestHooks(hooks) {
|
|
383
|
+
if ('beforeSteal' in hooks)
|
|
384
|
+
_planningLockTestHooks.beforeSteal = hooks.beforeSteal;
|
|
385
|
+
},
|
|
386
|
+
_resetPlanningLockTestHooks() {
|
|
387
|
+
delete _planningLockTestHooks.beforeSteal;
|
|
388
|
+
},
|
|
245
389
|
};
|
|
@@ -1010,19 +1010,31 @@ function cmdGenerateClaudeMd(cwd, options, raw) {
|
|
|
1010
1010
|
// repo-root `CLAUDE.md`, so generated GSD content does not land next to — or
|
|
1011
1011
|
// pollute — a hand-crafted repo-root CLAUDE.md. An explicit `claude_md_path`
|
|
1012
1012
|
// config value or `--output` still wins.
|
|
1013
|
-
let configClaudeMdPath = '
|
|
1013
|
+
let configClaudeMdPath = '.claude/CLAUDE.md';
|
|
1014
1014
|
try {
|
|
1015
1015
|
const config = loadConfig(cwd);
|
|
1016
1016
|
if (config['claude_md_path'])
|
|
1017
1017
|
configClaudeMdPath = config['claude_md_path'];
|
|
1018
1018
|
if (config['claude_md_assembly'])
|
|
1019
1019
|
assemblyConfig = config['claude_md_assembly'];
|
|
1020
|
-
// #
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1020
|
+
// #1529: When no explicit --output is provided, derive the instruction
|
|
1021
|
+
// file from the runtime via the shared `getProjectInstructionFile` policy
|
|
1022
|
+
// (single source of truth in runtime-name-policy.cjs, shared with the
|
|
1023
|
+
// new-project.md bash workflow via `gsd-tools query
|
|
1024
|
+
// project-instruction-file`). Previously this was a codex-only override
|
|
1025
|
+
// (#3163) that left AGENTS-native runtimes (opencode/kilo/kimi) emitting
|
|
1026
|
+
// CLAUDE.md; copilot now resolves to .github/copilot-instructions.md, and
|
|
1027
|
+
// antigravity/gemini to GEMINI.md. GSD_RUNTIME env var takes precedence
|
|
1028
|
+
// over config.runtime, mirroring detectRuntime().
|
|
1029
|
+
//
|
|
1030
|
+
// Non-claude runtimes always win over a stale `claude_md_path` (the #3163
|
|
1031
|
+
// rationale: a Codex/AGENTS-native project must never write to CLAUDE.md
|
|
1032
|
+
// even if a prior Claude setup left a `claude_md_path` behind). For the
|
|
1033
|
+
// claude runtime, `claude_md_path` config is honored — it IS the
|
|
1034
|
+
// Claude-specific output setting (per #1098 and the #3163 non-codex test).
|
|
1023
1035
|
const effectiveRuntime = (0, runtime_name_policy_cjs_1.resolveRuntimeNameFromCandidates)(process.env['GSD_RUNTIME'], config['runtime']);
|
|
1024
|
-
if (!options.output && effectiveRuntime
|
|
1025
|
-
configClaudeMdPath =
|
|
1036
|
+
if (!options.output && effectiveRuntime && effectiveRuntime !== 'claude') {
|
|
1037
|
+
configClaudeMdPath = (0, runtime_name_policy_cjs_1.getProjectInstructionFile)(effectiveRuntime);
|
|
1026
1038
|
}
|
|
1027
1039
|
}
|
|
1028
1040
|
catch { /* use default */ }
|
|
@@ -855,20 +855,18 @@ function convertClaudeToWindsurfMarkdown(content) {
|
|
|
855
855
|
// Replace subagent_type from Claude to Windsurf format
|
|
856
856
|
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
|
|
857
857
|
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
|
858
|
-
// Replace project-level Claude conventions with Windsurf
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
converted = converted.replace(
|
|
862
|
-
converted = converted.replace(
|
|
863
|
-
converted = converted.replace(
|
|
864
|
-
converted = converted.replace(
|
|
865
|
-
converted = converted.replace(/\.claude
|
|
866
|
-
converted = converted.replace(/\.\/\.claude\//g, './.devin/');
|
|
867
|
-
converted = converted.replace(/\.claude\//g, '.devin/');
|
|
858
|
+
// Replace project-level Claude conventions with Windsurf equivalents.
|
|
859
|
+
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules`');
|
|
860
|
+
converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules');
|
|
861
|
+
converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules`');
|
|
862
|
+
converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules');
|
|
863
|
+
converted = converted.replace(/\.claude\/skills\//g, '.windsurf/skills/');
|
|
864
|
+
converted = converted.replace(/\.\/\.claude\//g, './.windsurf/');
|
|
865
|
+
converted = converted.replace(/\.claude\//g, '.windsurf/');
|
|
868
866
|
// Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
|
|
869
867
|
// Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
|
|
870
|
-
converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.
|
|
871
|
-
converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.
|
|
868
|
+
converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.windsurf');
|
|
869
|
+
converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.windsurf');
|
|
872
870
|
// Environment variable name rewrite
|
|
873
871
|
converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR');
|
|
874
872
|
// Remove Claude Code-specific bug workarounds before brand replacement
|
|
@@ -918,6 +916,30 @@ function convertClaudeCommandToWindsurfSkill(content, skillName) {
|
|
|
918
916
|
const adapter = getWindsurfSkillAdapterHeader(skillName);
|
|
919
917
|
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
|
920
918
|
}
|
|
919
|
+
function convertClaudeCommandToWindsurfWorkflow(content, commandName) {
|
|
920
|
+
// #1615 security: commandName flows unsanitized into a markdown body that
|
|
921
|
+
// Windsurf loads as an LLM-readable workflow. Validate at entry to prevent
|
|
922
|
+
// (a) prompt injection via newlines / markdown structure in the filename,
|
|
923
|
+
// (b) path-component injection via .., /, \ in stem → @-reference target.
|
|
924
|
+
// Pattern: optional gsd- prefix + lowercase alphanumeric + dashes; rejects
|
|
925
|
+
// everything else. See DEFECT.PROMPT-INJECTION-SCAN-COLLISION and the
|
|
926
|
+
// PR #1622 security review.
|
|
927
|
+
if (typeof commandName !== 'string' || !/^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(commandName)) {
|
|
928
|
+
const preview = typeof commandName === 'string' ? JSON.stringify(commandName.slice(0, 60)) : String(commandName);
|
|
929
|
+
throw new Error(`convertClaudeCommandToWindsurfWorkflow: rejected commandName ${preview}; ` +
|
|
930
|
+
'must match /^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ (no slashes, backslashes, spaces, dots, trailing dash, or control chars — prevents prompt injection and path-component injection into the workflow body)');
|
|
931
|
+
}
|
|
932
|
+
const converted = convertClaudeToWindsurfMarkdown(content);
|
|
933
|
+
const { frontmatter } = extractFrontmatterAndBody(converted);
|
|
934
|
+
const description = frontmatter ? extractFrontmatterField(frontmatter, 'description') : '';
|
|
935
|
+
const stem = commandName.startsWith('gsd-') ? commandName.slice(4) : commandName;
|
|
936
|
+
const workflow = `# ${commandName}\n\n${toSingleLine(description || `Run ${commandName}.`)}\n\nRead and execute the GSD command at @~/.claude/gsd-core/commands/gsd/${stem}.md end-to-end. Treat the user's message after /${commandName} as the command arguments.`;
|
|
937
|
+
const byteLength = Buffer.byteLength(workflow, 'utf8');
|
|
938
|
+
if (byteLength > 12000) {
|
|
939
|
+
throw new Error(`Windsurf workflow ${commandName} exceeds 12000 bytes (${byteLength}); extract references before installing`);
|
|
940
|
+
}
|
|
941
|
+
return workflow;
|
|
942
|
+
}
|
|
921
943
|
// --- Augment converters ---
|
|
922
944
|
// Augment uses a tool set similar to Cursor/Windsurf.
|
|
923
945
|
// Config lives in .augment/ (local) and ~/.augment/ (global).
|
|
@@ -1706,10 +1728,16 @@ function convertGeminiToolName(claudeTool) {
|
|
|
1706
1728
|
// Task/Agent: exclude — agents are auto-registered as callable tools.
|
|
1707
1729
|
// AskUserQuestion: exclude — Gemini CLI does not expose an ask_user tool;
|
|
1708
1730
|
// emitting it causes frontmatter validation errors (#3362).
|
|
1731
|
+
// Skill/SlashCommand: exclude — Gemini CLI has no 'skill' built-in tool;
|
|
1732
|
+
// the lowercase fallback would emit an invalid 'skill'/'slashcommand' name
|
|
1733
|
+
// that fails frontmatter validation (tools.N: Invalid tool name) and aborts
|
|
1734
|
+
// the entire agent load (#1394).
|
|
1709
1735
|
if (claudeTool === 'Task' ||
|
|
1710
1736
|
claudeTool === 'Agent' ||
|
|
1711
1737
|
claudeTool === 'AskUserQuestion' ||
|
|
1712
|
-
claudeTool === 'ask_user'
|
|
1738
|
+
claudeTool === 'ask_user' ||
|
|
1739
|
+
claudeTool === 'Skill' ||
|
|
1740
|
+
claudeTool === 'SlashCommand') {
|
|
1713
1741
|
return null;
|
|
1714
1742
|
}
|
|
1715
1743
|
// Check for explicit mapping
|
|
@@ -1954,10 +1982,18 @@ function convertClaudeCommandToKiloSkill(content, skillName) {
|
|
|
1954
1982
|
* @private — exported as `_computePathPrefix` for tests.
|
|
1955
1983
|
*/
|
|
1956
1984
|
function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost, resolvedTarget, homeDir }) {
|
|
1957
|
-
|
|
1958
|
-
|
|
1985
|
+
// #1615: normalize Windows backslashes to forward slashes. This prefix is
|
|
1986
|
+
// substituted into markdown @-references (e.g. Windsurf workflow files),
|
|
1987
|
+
// which use POSIX paths universally. Idempotent on POSIX (no backslashes).
|
|
1988
|
+
// Without this, path.join on Windows produces a backslash prefix that
|
|
1989
|
+
// leaks into markdown content and breaks cross-platform substring checks.
|
|
1990
|
+
// See DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT in CONTEXT.md.
|
|
1991
|
+
const posixTarget = String(resolvedTarget).replace(/\\/g, '/');
|
|
1992
|
+
const posixHome = homeDir ? String(homeDir).replace(/\\/g, '/') : homeDir;
|
|
1993
|
+
if (isGlobal && posixTarget.startsWith(posixHome) && !isOpencode) {
|
|
1994
|
+
return '$HOME' + posixTarget.slice(posixHome.length) + '/';
|
|
1959
1995
|
}
|
|
1960
|
-
return `${
|
|
1996
|
+
return `${posixTarget}/`;
|
|
1961
1997
|
}
|
|
1962
1998
|
/**
|
|
1963
1999
|
* Canonical list of every non-Claude runtime that gsd-core emits artifacts for.
|
|
@@ -2319,6 +2355,7 @@ module.exports = {
|
|
|
2319
2355
|
convertClaudeCommandToCursorCommand,
|
|
2320
2356
|
convertClaudeToWindsurfMarkdown,
|
|
2321
2357
|
convertClaudeCommandToWindsurfSkill,
|
|
2358
|
+
convertClaudeCommandToWindsurfWorkflow,
|
|
2322
2359
|
convertClaudeToAugmentMarkdown,
|
|
2323
2360
|
convertClaudeCommandToAugmentSkill,
|
|
2324
2361
|
convertClaudeToTraeMarkdown,
|
|
@@ -130,6 +130,20 @@ function agentsKind(destSubpath, prefix, configDir) {
|
|
|
130
130
|
* Agent filenames are preserved verbatim (the prefix is already embedded in the
|
|
131
131
|
* agent stem — e.g. `gsd-planner.md`).
|
|
132
132
|
*
|
|
133
|
+
* #1173 SCOPE — plumbing only (declarations deferred): this provides the
|
|
134
|
+
* converter dispatch + `isGlobal` scope threading for the descriptor's `agents`
|
|
135
|
+
* kind, but NO runtime currently declares a converted `agents` kind in its
|
|
136
|
+
* `capability.json`. The descriptor declarations for the 8 non-Claude runtimes
|
|
137
|
+
* (copilot/antigravity/cursor/windsurf/augment/trae/codebuddy/cline) are
|
|
138
|
+
* DEFERRED to a follow-up that first ships the ADR-1235 §0 byte-for-byte parity
|
|
139
|
+
* harness, because the second `layout.kinds` consumer — `applySurface` /
|
|
140
|
+
* `/gsd:surface` / `--materialize` (`src/surface.cts`) — does not yet mirror the
|
|
141
|
+
* legacy agent pipeline (Copilot's `.agent.md` filename rename, the cross-cutting
|
|
142
|
+
* path-prefix rewrite + attribution, stale-file cleanup, config-reading steps),
|
|
143
|
+
* so declaring the kind now would regress the surface path. Until then the legacy
|
|
144
|
+
* `bin/install.js` agent loop remains authoritative for the real install, and
|
|
145
|
+
* this `convertedAgentsKind` is exercised only by synthetic-descriptor seam tests.
|
|
146
|
+
*
|
|
133
147
|
* Mirrors the `convertedCommandsKind` pattern (#785).
|
|
134
148
|
*
|
|
135
149
|
* @param destSubpath destination subpath within configDir (e.g. 'agents')
|
|
@@ -137,14 +151,18 @@ function agentsKind(destSubpath, prefix, configDir) {
|
|
|
137
151
|
* @param converterName name of converter function in Runtime Artifact Conversion exports
|
|
138
152
|
* @param configDir runtime config dir (for .gsd-source marker resolution)
|
|
139
153
|
*/
|
|
140
|
-
function convertedAgentsKind(destSubpath, prefix, converterName, configDir) {
|
|
154
|
+
function convertedAgentsKind(destSubpath, prefix, converterName, configDir, scope = 'global') {
|
|
141
155
|
return {
|
|
142
156
|
kind: 'agents',
|
|
143
157
|
destSubpath,
|
|
144
158
|
prefix,
|
|
145
159
|
stage: (resolved) => {
|
|
160
|
+
// isGlobal is threaded so scope-aware agent converters (copilot, antigravity)
|
|
161
|
+
// choose global-home vs workspace-relative paths; converters that only take
|
|
162
|
+
// (content) ignore the extra positional arg. Mirrors skillsKind's scope
|
|
163
|
+
// threading (#1173).
|
|
146
164
|
const converter = conversionExports[converterName];
|
|
147
|
-
return stageAgentsForRuntimeWithConverter(findAgentsSourceRoot(configDir), resolved, converter);
|
|
165
|
+
return stageAgentsForRuntimeWithConverter(findAgentsSourceRoot(configDir), resolved, converter, scope === 'global');
|
|
148
166
|
},
|
|
149
167
|
};
|
|
150
168
|
}
|
|
@@ -267,7 +285,7 @@ function dispatchKindEntry(entry, runtime, configDir, scope) {
|
|
|
267
285
|
if (converter == null) {
|
|
268
286
|
return agentsKind(destSubpath, prefix, configDir);
|
|
269
287
|
}
|
|
270
|
-
return convertedAgentsKind(destSubpath, prefix, converter, configDir);
|
|
288
|
+
return convertedAgentsKind(destSubpath, prefix, converter, configDir, scope);
|
|
271
289
|
case 'skills':
|
|
272
290
|
if (converter == null) {
|
|
273
291
|
throw new TypeError(`resolveRuntimeArtifactLayout: skills entry for '${runtime}' has converter=null (converter is required for skills)`);
|