@opengsd/gsd-core 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +31 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +36 -0
- package/agents/gsd-executor.md +20 -7
- package/agents/gsd-intel-updater.md +3 -3
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +20 -0
- package/agents/gsd-planner.md +15 -23
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/bin/install.js +186 -55
- package/commands/gsd/plan-review-convergence.md +5 -1
- package/gsd-core/bin/gsd-tools.cjs +849 -2
- package/gsd-core/bin/lib/api-coverage.cjs +22 -8
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/capability-consent.cjs +40 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
- package/gsd-core/bin/lib/capability-loader.cjs +23 -1
- package/gsd-core/bin/lib/capability-registry.cjs +1353 -132
- package/gsd-core/bin/lib/capability-trust.cjs +468 -33
- package/gsd-core/bin/lib/capability-validator.cjs +882 -6
- package/gsd-core/bin/lib/check-command-router.cjs +12 -2
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
- package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
- package/gsd-core/bin/lib/commands.cjs +246 -18
- package/gsd-core/bin/lib/config-loader.cjs +200 -28
- package/gsd-core/bin/lib/config.cjs +90 -5
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +44 -19
- package/gsd-core/bin/lib/install-engine.cjs +1 -0
- package/gsd-core/bin/lib/milestone.cjs +5 -5
- package/gsd-core/bin/lib/model-catalog.cjs +51 -1
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
- package/gsd-core/bin/lib/phase-id.cjs +278 -5
- package/gsd-core/bin/lib/phase.cjs +57 -5
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +1 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
- package/gsd-core/bin/lib/profile-output.cjs +34 -8
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
- package/gsd-core/bin/lib/roadmap-parser.cjs +54 -6
- package/gsd-core/bin/lib/roadmap.cjs +10 -4
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +1 -1
- package/gsd-core/bin/lib/state-document.cjs +164 -20
- package/gsd-core/bin/lib/state-transition.cjs +28 -10
- package/gsd-core/bin/lib/state.cjs +141 -21
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +9 -7
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
- package/gsd-core/bin/lib/unusable-input.cjs +216 -0
- package/gsd-core/bin/lib/validate.cjs +32 -0
- package/gsd-core/bin/lib/verification.cjs +51 -14
- package/gsd-core/bin/lib/verify.cjs +128 -20
- package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -13
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/gate-prompts.md +6 -3
- package/gsd-core/references/model-profile-resolution.md +64 -13
- package/gsd-core/references/offer-next.md +88 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -21
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/summary-minimal.md +4 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +7 -0
- package/gsd-core/workflows/ai-integration-phase.md +4 -4
- package/gsd-core/workflows/audit-fix.md +4 -0
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +76 -19
- package/gsd-core/workflows/debug.md +10 -2
- package/gsd-core/workflows/diagnose-issues.md +4 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
- package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
- package/gsd-core/workflows/discuss-phase-assumptions.md +15 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/docs-update.md +8 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
- package/gsd-core/workflows/execute-phase.md +85 -115
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/help/modes/full.md +3 -3
- package/gsd-core/workflows/import.md +4 -1
- package/gsd-core/workflows/ingest-docs.md +4 -0
- package/gsd-core/workflows/map-codebase.md +13 -6
- package/gsd-core/workflows/new-milestone.md +10 -2
- package/gsd-core/workflows/new-project.md +11 -4
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/plan-phase.md +42 -46
- package/gsd-core/workflows/plan-review-convergence.md +18 -14
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +14 -3
- package/gsd-core/workflows/review.md +146 -575
- package/gsd-core/workflows/scan.md +9 -1
- package/gsd-core/workflows/secure-phase.md +10 -2
- package/gsd-core/workflows/ship.md +41 -11
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/ui-phase.md +8 -1
- package/gsd-core/workflows/ui-review.md +8 -1
- package/gsd-core/workflows/update.md +104 -5
- package/gsd-core/workflows/validate-phase.md +10 -2
- package/gsd-core/workflows/verify-work.md +8 -1
- package/hooks/dist/gsd-cursor-session-start.js +6 -2
- package/hooks/dist/gsd-cursor-stop.js +6 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +9 -0
- package/hooks/dist/gsd-phase-boundary.sh +14 -2
- package/hooks/dist/gsd-prompt-guard.js +101 -2
- package/hooks/dist/gsd-read-guard.js +100 -2
- package/hooks/dist/gsd-read-injection-scanner.js +109 -2
- package/hooks/dist/gsd-statusline.js +9 -6
- package/hooks/dist/gsd-workflow-guard.js +110 -6
- package/hooks/dist/gsd-worktree-path-guard.js +132 -8
- package/hooks/dist/lib/cursor-workspace.js +74 -0
- package/hooks/gsd-cursor-session-start.js +6 -2
- package/hooks/gsd-cursor-stop.js +6 -2
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +9 -0
- package/hooks/gsd-phase-boundary.sh +14 -2
- package/hooks/gsd-prompt-guard.js +101 -2
- package/hooks/gsd-read-guard.js +100 -2
- package/hooks/gsd-read-injection-scanner.js +109 -2
- package/hooks/gsd-statusline.js +9 -6
- package/hooks/gsd-workflow-guard.js +110 -6
- package/hooks/gsd-worktree-path-guard.js +132 -8
- package/hooks/lib/cursor-workspace.js +74 -0
- package/package.json +7 -7
- package/pi/gsd.cjs +26 -1
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +24 -0
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +6 -4
- package/scripts/gen-capability-matrix.cjs +26 -2
- package/scripts/gen-capability-registry.cjs +132 -34
- package/scripts/gen-emitted-baseline.cjs +145 -0
- package/scripts/lint-compiled-artifact-sync.cjs +146 -0
- package/scripts/lint-emitted-drift-ack.cjs +149 -0
- package/scripts/lint-fix-has-regression-test.cjs +131 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +57 -8
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -16,41 +16,108 @@
|
|
|
16
16
|
* detected. The instance→cli mapping lives HERE (single source; see the parity
|
|
17
17
|
* test in tests/review-reviewer-instances.test.cjs — DEFECT.GENERATIVE-FIX).
|
|
18
18
|
*
|
|
19
|
-
* KNOWN_REVIEWER_SLUGS (
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
19
|
+
* KNOWN_REVIEWER_SLUGS (ADR-2782 D9, Phase 5a #2798): derived from declared
|
|
20
|
+
* `reviewer` bodies in the capability registry, not a hand-maintained tail.
|
|
21
|
+
* A capability of EITHER `role: "runtime"` (the six dual-purpose hosts —
|
|
22
|
+
* antigravity/claude/codex/cursor/opencode/qwen) or the lane-only
|
|
23
|
+
* `role: "reviewer"` (gemini/coderabbit/ollama/lm_studio/llama_cpp) may carry
|
|
24
|
+
* a `reviewer` body, and it is the body's `reviewer.slug` — NOT the capability
|
|
25
|
+
* id — that becomes the roster entry: the two differ for `lm-studio` (id) /
|
|
26
|
+
* `lm_studio` (slug) and `llama-cpp` (id) / `llama_cpp` (slug), ADR-2782's
|
|
27
|
+
* three-namespace trap (`id` must be kebab; `slug` keeps the shipped roster's
|
|
28
|
+
* snake form).
|
|
29
|
+
*
|
|
30
|
+
* `runtime.hostBehaviors.reviewerCli: true` survives as a DERIVED LEGACY ALIAS
|
|
31
|
+
* for one release (D9): a capability that only sets the flag (no `reviewer`
|
|
32
|
+
* body yet) still contributes its capability id as a slug, exactly as before
|
|
33
|
+
* this phase. Where a capability carries BOTH a declared body and the alias,
|
|
34
|
+
* the BODY WINS — that capability contributes only the body's slug, never
|
|
35
|
+
* both. Alias removal is a named later phase (#2801), not done here.
|
|
36
|
+
*
|
|
37
|
+
* Before this phase the five non-runtime reviewers (`gemini`, `coderabbit`,
|
|
38
|
+
* `ollama`, `lm_studio`, `llama_cpp`) had no `capabilities/<id>/` descriptor at
|
|
39
|
+
* all and were a hardcoded `NON_RUNTIME_REVIEWER_SLUGS` tail. Phase 5a gives
|
|
40
|
+
* each one a lane-only `role: "reviewer"` capability (ADR-2782 D3), so the
|
|
41
|
+
* tail is deleted outright. See
|
|
42
|
+
* `docs/adr/2782-reviewer-lane-capability-surface.md`.
|
|
31
43
|
*/
|
|
32
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
45
|
exports.INSTANCE_NAME_PATTERN = exports.KNOWN_REVIEWER_SLUGS = void 0;
|
|
46
|
+
exports.deriveReviewerSlugs = deriveReviewerSlugs;
|
|
34
47
|
exports.normalizeConfiguredDefaultReviewers = normalizeConfiguredDefaultReviewers;
|
|
35
48
|
exports.normalizeReviewerInstances = normalizeReviewerInstances;
|
|
36
49
|
exports.resolveReviewerSelection = resolveReviewerSelection;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Derive the reviewer-slug roster from a capability registry.
|
|
52
|
+
*
|
|
53
|
+
* Exported (rather than a require()-time-only side effect) so tests can
|
|
54
|
+
* exercise the derivation directly against a synthetic registry — the real
|
|
55
|
+
* roster below is a single call against the generated
|
|
56
|
+
* `capability-registry.cjs`, and `tests/review-lane-descriptor.test.cjs`'s
|
|
57
|
+
* `checkReviewerLaneParity` separately guards that the real roster still
|
|
58
|
+
* agrees with `src/review-lane-descriptor.cts` and the workflow.
|
|
59
|
+
*
|
|
60
|
+
* Reads `registry.capabilities` — every declared capability regardless of
|
|
61
|
+
* role — because a `role: "reviewer"` lane-only capability is never stored in
|
|
62
|
+
* `registry.runtimes` (that map is role:"runtime" only). Per capability: a
|
|
63
|
+
* declared `reviewer.slug` wins outright (D9) and the capability contributes
|
|
64
|
+
* ONLY that slug; only when there is no body does the legacy
|
|
65
|
+
* `hostBehaviors.reviewerCli` alias contribute the capability id instead. The
|
|
66
|
+
* result is collected into a Set (so a slug can never appear twice, even if
|
|
67
|
+
* two distinct capabilities somehow named the same one) and returned as a
|
|
68
|
+
* SORTED array, so the roster's order never depends on `Object.entries()`
|
|
69
|
+
* iteration / registry build order.
|
|
70
|
+
*/
|
|
71
|
+
function deriveReviewerSlugs(registry) {
|
|
72
|
+
const capabilities = registry.capabilities || {};
|
|
73
|
+
const slugs = new Set();
|
|
74
|
+
for (const [capId, cap] of Object.entries(capabilities)) {
|
|
75
|
+
// Trim before the emptiness test: `length > 0` alone admits a whitespace-only
|
|
76
|
+
// slug (" ") verbatim into the roster, where it can never match a real lane
|
|
77
|
+
// but still occupies a roster entry. Unreachable through the checked-in
|
|
78
|
+
// registry today, but this function is EXPORTED for reuse and carries no
|
|
79
|
+
// other validation, so it should not depend on its caller's hygiene.
|
|
80
|
+
const rawSlug = cap?.reviewer?.slug;
|
|
81
|
+
const declaredSlug = typeof rawSlug === 'string' ? rawSlug.trim() : '';
|
|
82
|
+
if (declaredSlug.length > 0) {
|
|
83
|
+
slugs.add(declaredSlug);
|
|
84
|
+
continue; // the body wins — never ALSO add this capability's alias below.
|
|
85
|
+
}
|
|
86
|
+
if (cap?.runtime?.hostBehaviors?.reviewerCli === true) {
|
|
87
|
+
slugs.add(capId);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return [...slugs].sort();
|
|
49
91
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
92
|
+
/**
|
|
93
|
+
* The roster, derived once at module load.
|
|
94
|
+
*
|
|
95
|
+
* GUARDED, because this runs at `require()` time: an uncaught throw here does not
|
|
96
|
+
* degrade reviewer selection, it breaks `require()` of this module for EVERY
|
|
97
|
+
* consumer. The sibling `capability-trust.cjs` already holds this line — its
|
|
98
|
+
* `discloseExecutableSurfaces` is documented "TOTAL: never throws, for any
|
|
99
|
+
* manifest shape" and wrapped accordingly — and this module handles the same
|
|
100
|
+
* class of registry-derived input, so it should not be the asymmetric one.
|
|
101
|
+
*
|
|
102
|
+
* A malformed registry yields an EMPTY roster rather than a crash. That is a
|
|
103
|
+
* visible degradation, not a silent one: under ADR-2782 D4 an explicitly
|
|
104
|
+
* requested reviewer that is unavailable is an ERROR, so `/gsd:review --claude`
|
|
105
|
+
* against an empty roster fails loudly instead of quietly reviewing with nobody.
|
|
106
|
+
*
|
|
107
|
+
* Unreachable through the checked-in registry today (it is generated, JSON-sourced
|
|
108
|
+
* and code-reviewed, so it cannot carry a getter or a Proxy). Guarded anyway —
|
|
109
|
+
* reachability analysis is not a contract, and the next caller should not have to
|
|
110
|
+
* redo it.
|
|
111
|
+
*/
|
|
112
|
+
exports.KNOWN_REVIEWER_SLUGS = (() => {
|
|
113
|
+
try {
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
115
|
+
return deriveReviewerSlugs(require('./capability-registry.cjs'));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
54
121
|
/** Instance names are lowercase slugs that must not shadow a built-in slug. */
|
|
55
122
|
exports.INSTANCE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
56
123
|
function normalizeConfiguredDefaultReviewers(rawValue) {
|
|
@@ -164,12 +231,27 @@ function resolveReviewerSelection(input) {
|
|
|
164
231
|
let selected = [];
|
|
165
232
|
if (explicitFlags.size > 0) {
|
|
166
233
|
source = 'explicit_flags';
|
|
234
|
+
// ADR-2782 D4: absent-safe governs DISCOVERY, never explicit selection.
|
|
235
|
+
// Not finding a lane nobody asked for is normal; failing to run a lane
|
|
236
|
+
// somebody asked for is an error. Every miss used to be an `info`, so a
|
|
237
|
+
// PARTIAL miss (`--gemini --qwen` with qwen absent) ran the review with a
|
|
238
|
+
// thinner reviewer set while present_results reported success — "a cross-AI
|
|
239
|
+
// review that silently drops a lane is blind in one eye" (review.md).
|
|
240
|
+
// A total miss already errored, but only as a side effect of the selected
|
|
241
|
+
// set being empty; the partial case had no signal at all.
|
|
242
|
+
const priorErrorCount = errors.length;
|
|
167
243
|
selected = [...explicitFlags].filter((slug) => detected.has(slug));
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
244
|
+
// Sorted so the error order does not depend on flag order on the command line.
|
|
245
|
+
const missing = [...explicitFlags].filter((slug) => !detected.has(slug)).sort();
|
|
246
|
+
for (const slug of missing) {
|
|
247
|
+
errors.push(`explicit reviewer '${slug}' is not available on this host`);
|
|
171
248
|
}
|
|
172
|
-
|
|
249
|
+
// Guarded on the error count as it stood BEFORE this branch, so the
|
|
250
|
+
// aggregate message fires under exactly the conditions it did previously.
|
|
251
|
+
// Guarding on `errors.length` would let the per-slug errors just pushed
|
|
252
|
+
// suppress it, silently dropping a message this change did not intend to
|
|
253
|
+
// remove.
|
|
254
|
+
if (selected.length === 0 && priorErrorCount === 0) {
|
|
173
255
|
errors.push('no selected reviewers are available for explicit flags');
|
|
174
256
|
}
|
|
175
257
|
}
|
|
@@ -31,6 +31,9 @@ roadmapPhaseLookupSources, } = phaseIdModule;
|
|
|
31
31
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
32
32
|
const { planningDir } = planningWorkspace;
|
|
33
33
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
|
+
const unusableInputMod = require("./unusable-input.cjs");
|
|
36
|
+
const { UNUSABLE_REASON, warnUnusableInput } = unusableInputMod;
|
|
34
37
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
35
38
|
// ─── Roadmap milestone scoping ───────────────────────────────────────────────
|
|
36
39
|
/**
|
|
@@ -271,10 +274,18 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
|
|
|
271
274
|
const normalizedPhase = stripProjectCodePrefix(phaseNum);
|
|
272
275
|
if (/^999(?:\.|$)/.test(normalizedPhase))
|
|
273
276
|
return null;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
+
// Resolved INSIDE the try for the same reason as getMilestoneInfo below: planningDir
|
|
278
|
+
// throws a plain Error for an invalid GSD_WORKSTREAM/GSD_PROJECT segment, and resolving
|
|
279
|
+
// it outside let that escape uncaught, crashing every caller for a malformed workstream
|
|
280
|
+
// name. ADR-227 is explicit that throwing breaks pipeline continuity, and this read path
|
|
281
|
+
// has no reason to be the exception -- it already degrades to null for every other
|
|
282
|
+
// failure. Absence still returns null before any diagnostic, and when the path never
|
|
283
|
+
// resolved there is nothing to name.
|
|
284
|
+
let roadmapPath;
|
|
277
285
|
try {
|
|
286
|
+
roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
287
|
+
if (!node_fs_1.default.existsSync(roadmapPath))
|
|
288
|
+
return null;
|
|
278
289
|
const roadmapRaw = (0, shell_command_projection_cjs_1.platformReadSync)(roadmapPath);
|
|
279
290
|
if (roadmapRaw === null)
|
|
280
291
|
throw new Error('missing');
|
|
@@ -301,10 +312,33 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
|
|
|
301
312
|
}
|
|
302
313
|
return null;
|
|
303
314
|
}
|
|
304
|
-
catch {
|
|
315
|
+
catch (err) {
|
|
316
|
+
// Absence already returned above via existsSync; anything caught here is a read fault
|
|
317
|
+
// or the synthetic missing-marker. The null is preserved exactly either way.
|
|
318
|
+
if (roadmapPath !== undefined)
|
|
319
|
+
reportUnreadableRoadmap(err, roadmapPath);
|
|
305
320
|
return null;
|
|
306
321
|
}
|
|
307
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Report a ROADMAP.md that exists but could not be read (#1881, ADR-1411).
|
|
325
|
+
*
|
|
326
|
+
* The discriminator is the errno, and it matters in the SILENT direction.
|
|
327
|
+
* platformReadSync returns null for ENOENT and both callers convert that null into a
|
|
328
|
+
* synthetic Error carrying no code, which lands in the same catch as a real EACCES.
|
|
329
|
+
* Reporting unconditionally here would flag every project that has no ROADMAP.md yet --
|
|
330
|
+
* every brand-new project -- as corrupt. A genuine read fault always carries an errno;
|
|
331
|
+
* absence never does.
|
|
332
|
+
*
|
|
333
|
+
* The parse itself is regex over text and cannot throw, so anything reaching a catch is
|
|
334
|
+
* either a read fault or that synthetic absence marker. Nothing else gets here.
|
|
335
|
+
*/
|
|
336
|
+
function reportUnreadableRoadmap(err, roadmapPath) {
|
|
337
|
+
const code = err?.code;
|
|
338
|
+
if (typeof code !== 'string')
|
|
339
|
+
return;
|
|
340
|
+
warnUnusableInput({ reason: UNUSABLE_REASON.ROADMAP_UNREADABLE, source: roadmapPath });
|
|
341
|
+
}
|
|
308
342
|
/**
|
|
309
343
|
* Strip a leading delimiter run (whitespace, em/en-dash, colon, hyphen) from a
|
|
310
344
|
* milestone-name capture. Markdown headings commonly take the shape
|
|
@@ -318,8 +352,16 @@ function stripLeadingDelimiter(s) {
|
|
|
318
352
|
return s.replace(/^[\s—–:-]+/, '').trim();
|
|
319
353
|
}
|
|
320
354
|
function getMilestoneInfo(cwd) {
|
|
355
|
+
// Declared here but RESOLVED INSIDE the try, so the catch can name the file without
|
|
356
|
+
// moving planningDir() out of the protected region. planningDir throws a plain Error
|
|
357
|
+
// for an invalid GSD_WORKSTREAM/GSD_PROJECT segment, and hoisting the call let that
|
|
358
|
+
// escape uncaught — breaking the invariant #2245 relies on, that this function never
|
|
359
|
+
// throws. When the path never resolved there is nothing to name, so the diagnostic is
|
|
360
|
+
// skipped and the default is returned exactly as before.
|
|
361
|
+
let roadmapPath;
|
|
321
362
|
try {
|
|
322
|
-
|
|
363
|
+
roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
364
|
+
const roadmap = (0, shell_command_projection_cjs_1.platformReadSync)(roadmapPath);
|
|
323
365
|
if (roadmap === null)
|
|
324
366
|
throw new Error('missing');
|
|
325
367
|
let stateVersion = null;
|
|
@@ -388,7 +430,13 @@ function getMilestoneInfo(cwd) {
|
|
|
388
430
|
name: 'milestone',
|
|
389
431
|
};
|
|
390
432
|
}
|
|
391
|
-
catch {
|
|
433
|
+
catch (err) {
|
|
434
|
+
// This function has no existsSync guard, so an absent ROADMAP arrives here too, as a
|
|
435
|
+
// synthetic Error with no errno. Only a real read fault is reported; the populated
|
|
436
|
+
// default is returned unchanged either way, and a plausible-looking default needs the
|
|
437
|
+
// diagnostic more than an empty sentinel does, not less (ADR-1411).
|
|
438
|
+
if (roadmapPath !== undefined)
|
|
439
|
+
reportUnreadableRoadmap(err, roadmapPath);
|
|
392
440
|
return { version: 'v1.0', name: 'milestone' };
|
|
393
441
|
}
|
|
394
442
|
}
|
|
@@ -137,10 +137,16 @@ function searchPhaseInContent(content, escapedPhase, phaseNum) {
|
|
|
137
137
|
// comparison; unrecognized values are preserved verbatim for forward-compat.
|
|
138
138
|
const modeMatch = section.match(/\*\*Mode(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
|
139
139
|
const mode = modeMatch ? modeMatch[1].trim().toLowerCase() : null;
|
|
140
|
-
// Extract success criteria as structured array
|
|
141
|
-
|
|
140
|
+
// Extract success criteria as structured array. A criterion may wrap onto extra
|
|
141
|
+
// indented lines (no `N.` prefix); those continuations must fold INTO their
|
|
142
|
+
// criterion, not end the run (#2522 — the old `(?:\s*\d+\.\s*[^\n]+)+` broke on a
|
|
143
|
+
// wrapped line, truncating it and silently dropping every criterion below it).
|
|
144
|
+
// `\n*` before each numbered line keeps blank-line-separated criteria working.
|
|
145
|
+
const criteriaMatch = section.match(/\*\*Success Criteria\*\*[^\n]*:\s*\n((?:\n*[ \t]*\d+\.[^\n]*\n?(?:[ \t]+(?!\d+\.)[^\n]*\n?)*)+)/i);
|
|
142
146
|
const success_criteria = criteriaMatch
|
|
143
|
-
? criteriaMatch[1].trim().split(
|
|
147
|
+
? criteriaMatch[1].trim().split(/\n+(?=[ \t]*\d+\.)/)
|
|
148
|
+
.map(entry => entry.replace(/^\s*\d+\.\s*/, '').replace(/\s*\n\s*/g, ' ').trim())
|
|
149
|
+
.filter(Boolean)
|
|
144
150
|
: [];
|
|
145
151
|
return {
|
|
146
152
|
found: true,
|
|
@@ -658,7 +664,7 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
|
|
|
658
664
|
const planPath = node_path_1.default.join(node_path_1.default.resolve(cwd, phaseInfo.directory), planFile);
|
|
659
665
|
try {
|
|
660
666
|
const content = node_fs_1.default.readFileSync(planPath, 'utf-8');
|
|
661
|
-
const fm = extractFrontmatter(content);
|
|
667
|
+
const fm = extractFrontmatter(content, planPath);
|
|
662
668
|
const wave = parseInt(fm.wave, 10) || 1;
|
|
663
669
|
const planId = planFile.replace(/-PLAN\.md$/i, '').replace(/PLAN\.md$/i, '');
|
|
664
670
|
const truths = parseMustHavesBlock(content, 'truths') || [];
|
|
@@ -518,6 +518,28 @@ function convertClaudeCommandToKimiSkill(content, skillName, _runtime = null, cm
|
|
|
518
518
|
const normalizedBody = convertGsdCommandReferencesToKimiSkillInvocations(frontmatter ? body : content, names);
|
|
519
519
|
return `---\nname: ${kimiSkillName}\ndescription: ${yamlQuote(toSingleLine(description))}\n---\nInvoke this Kimi skill with \`/skill:${kimiSkillName}\`.\n\n${normalizedBody}`;
|
|
520
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* Convert a Claude command-markdown source into a Kimi Code Agent Skill.
|
|
523
|
+
*
|
|
524
|
+
* Kimi Code (Moonshot's Node CLI) uses the standard Agent Skills format —
|
|
525
|
+
* a directory containing SKILL.md with frontmatter (name/description) and
|
|
526
|
+
* body — auto-discovered from `~/.kimi-code/skills/` (per Kimi Code docs:
|
|
527
|
+
* "Agent Skills is an open format for adding specialized knowledge and
|
|
528
|
+
* workflows to AI agents"). The invocation prefix is `/skill:<name>`,
|
|
529
|
+
* identical to Python kimi-cli.
|
|
530
|
+
*
|
|
531
|
+
* Today the output is byte-identical to `convertClaudeCommandToKimiSkill`
|
|
532
|
+
* (the Python kimi-cli converter) because both products consume the same
|
|
533
|
+
* Agent Skills format + `/skill:` invocation. The distinct function name
|
|
534
|
+
* lets a future divergence land cleanly if Kimi Code's skill format evolves
|
|
535
|
+
* independently of Python kimi-cli.
|
|
536
|
+
*
|
|
537
|
+
* Registered as `convertClaudeCommandToKimiCodeSkill` in the capabilities/
|
|
538
|
+
* kimi-code/capability.json `artifactLayout` `converter` field (#2454 PR 2).
|
|
539
|
+
*/
|
|
540
|
+
function convertClaudeCommandToKimiCodeSkill(content, skillName, _runtime = null, cmdNames = null) {
|
|
541
|
+
return convertClaudeCommandToKimiSkill(content, skillName, _runtime, cmdNames);
|
|
542
|
+
}
|
|
521
543
|
const KIMI_CANONICAL_GSD_AGENT_RE = /^gsd-[a-z0-9-]+$/;
|
|
522
544
|
function parseKimiAgentSource(source) {
|
|
523
545
|
if (typeof source === 'string') {
|
|
@@ -1391,10 +1413,14 @@ Typed mapping (agent_type-capable schema only):
|
|
|
1391
1413
|
inherited, or unsupported values; do not invent one-off effort literals in
|
|
1392
1414
|
workflow prose.
|
|
1393
1415
|
- \`fork_context: false\` by default — GSD agents load their own context via \`<files_to_read>\` blocks
|
|
1394
|
-
- \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct
|
|
1395
|
-
Codex \`
|
|
1396
|
-
|
|
1397
|
-
|
|
1416
|
+
- \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct \`spawn_agent\` mapping,
|
|
1417
|
+
but Codex declares \`dispatch.isolation: orchestrator-worktree\` (#2584). Codex
|
|
1418
|
+
\`spawn_agent\` still does not create or bind a git worktree; instead GSD itself
|
|
1419
|
+
creates the worktree and process-spawns the executor into it with
|
|
1420
|
+
\`codex exec --cd <dir>\`, performing every git operation on the executor's behalf
|
|
1421
|
+
(its \`workspace-write\` sandbox makes \`.git\` read-only). Workflows must therefore
|
|
1422
|
+
never fabricate a manual worktree protocol — route through the negotiated
|
|
1423
|
+
isolation adapter, which still fails closed for hosts declaring \`none\` (#3360).
|
|
1398
1424
|
|
|
1399
1425
|
Generic-agent workaround (multi_agent_v1 schema — NO agent_type field):
|
|
1400
1426
|
When only the generic \`multi_agent_v1\` schema is available, typed GSD agent dispatch
|
|
@@ -2603,6 +2629,7 @@ module.exports = {
|
|
|
2603
2629
|
convertClaudeCommandToAntigravitySkill,
|
|
2604
2630
|
convertClaudeCommandToClaudeSkill,
|
|
2605
2631
|
convertClaudeCommandToKimiSkill,
|
|
2632
|
+
convertClaudeCommandToKimiCodeSkill,
|
|
2606
2633
|
buildKimiAgentArtifacts,
|
|
2607
2634
|
convertClaudeToCursorMarkdown,
|
|
2608
2635
|
convertClaudeCommandToCursorSkill,
|
|
@@ -316,7 +316,7 @@ function dispatchKindEntry(entry, runtime, configDir, scope, capabilityRegistry)
|
|
|
316
316
|
default:
|
|
317
317
|
throw new TypeError(`resolveRuntimeArtifactLayout: unknown kind '${kind}' in descriptor for runtime '${runtime}'`);
|
|
318
318
|
}
|
|
319
|
-
if (typeof entry.home === 'string' && entry.home !== '') {
|
|
319
|
+
if (scope === 'global' && typeof entry.home === 'string' && entry.home !== '') {
|
|
320
320
|
result.home = node_path_1.default.join(node_os_1.default.homedir(), entry.home);
|
|
321
321
|
}
|
|
322
322
|
return result;
|
|
@@ -149,6 +149,81 @@ function atomicWriteFileSync(target, data, options) {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
// ---------------------------------------------------------------------------
|
|
152
|
+
// CommonJS package.json marker for staged .js hook scripts (#2717)
|
|
153
|
+
//
|
|
154
|
+
// Node resolves the nearest package.json walking up from a .js file. When a
|
|
155
|
+
// runtime's config root (e.g. ~/.cursor, ~/.codeium/windsurf, ~/.codex) — or any
|
|
156
|
+
// parent — declares {"type":"module"}, Node loads GSD's staged CommonJS hook
|
|
157
|
+
// scripts as ESM and every require() fails with "require is not defined",
|
|
158
|
+
// silently disabling that runtime's lifecycle hooks.
|
|
159
|
+
//
|
|
160
|
+
// installSharedHooksBundle writes this marker for the 12 runtimes that go
|
|
161
|
+
// through the shared hooks bundle, but cursor/windsurf (skipSharedHooksInstall)
|
|
162
|
+
// and codex (the !isCodex gate) stage their .js hooks via the dedicated paths
|
|
163
|
+
// below and never reached it. These helpers decouple the marker write from the
|
|
164
|
+
// shared bundle so any code path that stages .js hooks can ensure the marker
|
|
165
|
+
// lands in the SAME directory as the scripts (#2717).
|
|
166
|
+
//
|
|
167
|
+
// The marker content is byte-identical to installSharedHooksBundle's
|
|
168
|
+
// (bin/install.js installSharedHooksBundle): {"type":"commonjs"}\n.
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/** The exact marker content GSD writes, matching installSharedHooksBundle. */
|
|
171
|
+
const COMMONJS_MARKER_CONTENT = '{"type":"commonjs"}\n';
|
|
172
|
+
/**
|
|
173
|
+
* Ensure a `package.json` forcing CommonJS mode exists in `dir` (the directory
|
|
174
|
+
* holding GSD-staged `.js` hook scripts). Idempotent: a no-op if the marker is
|
|
175
|
+
* already present with GSD's content. Overwrites only when the file is absent
|
|
176
|
+
* or already carries GSD's exact marker — it never clobbers a distinct
|
|
177
|
+
* user-authored package.json (it leaves such a file in place; the user owns it).
|
|
178
|
+
*
|
|
179
|
+
* @param dir - absolute path to the directory holding the staged .js hooks
|
|
180
|
+
* @returns `true` if the marker is present after the call (written or already there)
|
|
181
|
+
*/
|
|
182
|
+
function ensureCommonJsMarker(dir) {
|
|
183
|
+
const markerPath = node_path_1.default.join(dir, 'package.json');
|
|
184
|
+
try {
|
|
185
|
+
if (node_fs_1.default.existsSync(markerPath)) {
|
|
186
|
+
const existing = node_fs_1.default.readFileSync(markerPath, 'utf8');
|
|
187
|
+
// Already GSD's marker (tolerant of trailing-whitespace variants) — done.
|
|
188
|
+
if (existing.trim() === '{"type":"commonjs"}')
|
|
189
|
+
return true;
|
|
190
|
+
// A distinct package.json the user owns — do NOT clobber. The hook will
|
|
191
|
+
// load as whatever type the user declared; that is the user's choice.
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
node_fs_1.default.writeFileSync(markerPath, COMMONJS_MARKER_CONTENT);
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Best-effort: a marker write failure must not fail the whole install.
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Remove the CommonJS marker from `dir` on uninstall — but ONLY if it carries
|
|
204
|
+
* GSD's exact marker content. A user-authored package.json is never deleted.
|
|
205
|
+
* Mirrors the kimi uninstall guard in bin/install.js.
|
|
206
|
+
*
|
|
207
|
+
* @param dir - absolute path to the directory that held the staged .js hooks
|
|
208
|
+
* @returns `true` if a GSD-owned marker was removed
|
|
209
|
+
*/
|
|
210
|
+
function removeCommonJsMarkerIfGsdOwned(dir) {
|
|
211
|
+
const markerPath = node_path_1.default.join(dir, 'package.json');
|
|
212
|
+
try {
|
|
213
|
+
if (!node_fs_1.default.existsSync(markerPath))
|
|
214
|
+
return false;
|
|
215
|
+
const content = node_fs_1.default.readFileSync(markerPath, 'utf8').trim();
|
|
216
|
+
if (content === '{"type":"commonjs"}') {
|
|
217
|
+
node_fs_1.default.unlinkSync(markerPath);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
152
227
|
// parseTomlValue + findMultilineBasicStringClose
|
|
153
228
|
// (needed by rewriteLegacyCodexHookBlock — pure TOML helpers, no state)
|
|
154
229
|
// ---------------------------------------------------------------------------
|
|
@@ -956,6 +1031,48 @@ function writeCursorHooksJson(targetDir, src, opts) {
|
|
|
956
1031
|
installedScripts.add(script);
|
|
957
1032
|
}
|
|
958
1033
|
}
|
|
1034
|
+
// Stage the hooks/lib/ helpers the staged scripts require (#2587). Cursor sets
|
|
1035
|
+
// hostBehaviors.skipSharedHooksInstall, so it never reaches the installer's
|
|
1036
|
+
// bulk hooks/lib copy — without this, a script requiring './lib/…' would throw
|
|
1037
|
+
// MODULE_NOT_FOUND at load, BEFORE its own try/catch, and wedge every Cursor
|
|
1038
|
+
// session on the one runtime these hooks exist for. Driven off what the staged
|
|
1039
|
+
// scripts actually require so a future helper cannot be silently omitted.
|
|
1040
|
+
const requiredLibFiles = new Set();
|
|
1041
|
+
for (const script of installedScripts) {
|
|
1042
|
+
const staged = node_fs_1.default.readFileSync(node_path_1.default.join(hooksDir, script), 'utf8');
|
|
1043
|
+
// Tolerant of interior whitespace and either quote style: a hook author
|
|
1044
|
+
// writing `require( "./lib/x.js" )` must still get its helper staged, since
|
|
1045
|
+
// a miss here surfaces as MODULE_NOT_FOUND at hook load, not at install.
|
|
1046
|
+
const re = /require\(\s*['"]\.\/lib\/([A-Za-z0-9._-]+)['"]\s*\)/g;
|
|
1047
|
+
let m;
|
|
1048
|
+
while ((m = re.exec(staged)) !== null)
|
|
1049
|
+
requiredLibFiles.add(m[1]);
|
|
1050
|
+
}
|
|
1051
|
+
if (requiredLibFiles.size > 0) {
|
|
1052
|
+
const srcLibDir = node_path_1.default.join(srcHooksDir, 'lib');
|
|
1053
|
+
const destLibDir = node_path_1.default.join(hooksDir, 'lib');
|
|
1054
|
+
node_fs_1.default.mkdirSync(destLibDir, { recursive: true });
|
|
1055
|
+
for (const libFile of requiredLibFiles) {
|
|
1056
|
+
const libSrc = node_path_1.default.join(srcLibDir, libFile);
|
|
1057
|
+
if (!node_fs_1.default.existsSync(libSrc)) {
|
|
1058
|
+
// FAIL LOUD. Skipping here would ship hook scripts whose top-level
|
|
1059
|
+
// require() throws before their own try/catch, wedging every session —
|
|
1060
|
+
// and the install would still exit 0, so nobody would know until a user
|
|
1061
|
+
// hit it. A missing helper source is a packaging bug; surface it.
|
|
1062
|
+
throw new Error(`hooks/lib/${libFile} is required by a staged Cursor hook but is missing from ${srcLibDir}. `
|
|
1063
|
+
+ 'Installing would ship a hook that throws MODULE_NOT_FOUND at load.');
|
|
1064
|
+
}
|
|
1065
|
+
let libContent = node_fs_1.default.readFileSync(libSrc, 'utf8');
|
|
1066
|
+
libContent = libContent.replace(/gsd:/gi, 'gsd-');
|
|
1067
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(destLibDir, libFile), libContent);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
// #2717: write the CommonJS marker into hooks/ alongside the staged .js
|
|
1071
|
+
// scripts. Cursor sets skipSharedHooksInstall, so it never reaches
|
|
1072
|
+
// installSharedHooksBundle (the only other writer of this marker); without
|
|
1073
|
+
// it, a ~/.cursor/package.json declaring {"type":"module"} makes Node load
|
|
1074
|
+
// these require()-using scripts as ESM and every Cursor hook fails silently.
|
|
1075
|
+
ensureCommonJsMarker(hooksDir);
|
|
959
1076
|
const hookOpts = { runtime: 'cursor', platform: opts.platform || process.platform };
|
|
960
1077
|
const commands = {};
|
|
961
1078
|
for (const ev of events) {
|
|
@@ -987,6 +1104,14 @@ function removeCursorHooksJson(targetDir) {
|
|
|
987
1104
|
const hasAnyEvents = Object.keys(hookTable).some((k) => Array.isArray(hookTable[k]) && hookTable[k].length > 0);
|
|
988
1105
|
if (!hasAnyEvents) {
|
|
989
1106
|
node_fs_1.default.unlinkSync(hooksJsonPath);
|
|
1107
|
+
// #2717: also remove the CommonJS marker GSD wrote into hooks/ — but
|
|
1108
|
+
// only if it still carries GSD's exact content (a user-authored
|
|
1109
|
+
// package.json is never deleted). Best-effort: a failure here must not
|
|
1110
|
+
// mask the hooks.json removal above.
|
|
1111
|
+
try {
|
|
1112
|
+
removeCommonJsMarkerIfGsdOwned(node_path_1.default.join(targetDir, 'hooks'));
|
|
1113
|
+
}
|
|
1114
|
+
catch { /* leave it */ }
|
|
990
1115
|
return { changed: true };
|
|
991
1116
|
}
|
|
992
1117
|
}
|
|
@@ -1136,6 +1261,12 @@ function writeWindsurfHooksJson(targetDir, src, opts) {
|
|
|
1136
1261
|
installedScripts.add(script);
|
|
1137
1262
|
}
|
|
1138
1263
|
}
|
|
1264
|
+
// #2717: write the CommonJS marker into hooks/ alongside the staged .js
|
|
1265
|
+
// scripts. Windsurf sets skipSharedHooksInstall, so it never reaches
|
|
1266
|
+
// installSharedHooksBundle (the only other writer of this marker); without
|
|
1267
|
+
// it, a config-root package.json declaring {"type":"module"} makes Node load
|
|
1268
|
+
// these require()-using scripts as ESM and the Windsurf hooks fail silently.
|
|
1269
|
+
ensureCommonJsMarker(hooksDir);
|
|
1139
1270
|
const hookOpts = { runtime: 'windsurf', platform: opts.platform || process.platform };
|
|
1140
1271
|
const commands = {};
|
|
1141
1272
|
for (const ev of WINDSURF_HOOK_EVENTS) {
|
|
@@ -1174,6 +1305,13 @@ function removeWindsurfHooksJson(targetDir) {
|
|
|
1174
1305
|
const hasAnyEvents = Object.keys(hookTable).some((k) => Array.isArray(hookTable[k]) && hookTable[k].length > 0);
|
|
1175
1306
|
if (!hasAnyEvents) {
|
|
1176
1307
|
node_fs_1.default.unlinkSync(hooksJsonPath);
|
|
1308
|
+
// #2717: also remove the CommonJS marker GSD wrote into hooks/ — but
|
|
1309
|
+
// only if it still carries GSD's exact content (a user-authored
|
|
1310
|
+
// package.json is never deleted). Best-effort.
|
|
1311
|
+
try {
|
|
1312
|
+
removeCommonJsMarkerIfGsdOwned(node_path_1.default.join(targetDir, 'hooks'));
|
|
1313
|
+
}
|
|
1314
|
+
catch { /* leave it */ }
|
|
1177
1315
|
return { changed: true };
|
|
1178
1316
|
}
|
|
1179
1317
|
}
|
|
@@ -1963,6 +2101,8 @@ module.exports = {
|
|
|
1963
2101
|
GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT,
|
|
1964
2102
|
GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT,
|
|
1965
2103
|
GSD_WINDSURF_HOOK_SCRIPTS,
|
|
2104
|
+
ensureCommonJsMarker,
|
|
2105
|
+
removeCommonJsMarkerIfGsdOwned,
|
|
1966
2106
|
GSD_WINDSURF_HOOK_MARKER,
|
|
1967
2107
|
// Copilot
|
|
1968
2108
|
buildCopilotHookConfig,
|
|
@@ -40,6 +40,7 @@ const FALLBACK_ALIASES = {
|
|
|
40
40
|
qwen: ['qwen', 'qwen-code', 'qwen-cli'],
|
|
41
41
|
hermes: ['hermes', 'hermes-agent', 'hermes-cli'],
|
|
42
42
|
kimi: ['kimi'],
|
|
43
|
+
'kimi-code': ['kimi-code', 'kimicode', 'kimi_code'],
|
|
43
44
|
codebuddy: ['codebuddy', 'codebuddy-cli'],
|
|
44
45
|
cline: ['cline', 'cline-cli'],
|
|
45
46
|
};
|
|
@@ -248,6 +249,7 @@ const RUNTIME_LABELS = {
|
|
|
248
249
|
qwen: 'Qwen Code',
|
|
249
250
|
hermes: 'Hermes Agent',
|
|
250
251
|
kimi: 'Kimi CLI',
|
|
252
|
+
'kimi-code': 'Kimi Code',
|
|
251
253
|
codebuddy: 'CodeBuddy',
|
|
252
254
|
cline: 'Cline',
|
|
253
255
|
zcode: 'ZCode',
|
|
@@ -303,6 +305,7 @@ const GLOBAL_CONFIG_HOME_FRAGMENTS = {
|
|
|
303
305
|
codebuddy: "'.codebuddy'",
|
|
304
306
|
cline: "'.cline'",
|
|
305
307
|
kimi: "'.config', 'agents'",
|
|
308
|
+
'kimi-code': "'.kimi-code'",
|
|
306
309
|
zcode: "'.zcode'",
|
|
307
310
|
// pi's global config home is ~/.pi/agent (configHome: dot-home-nested,
|
|
308
311
|
// parent '.pi', name 'agent' — capabilities/pi/capability.json), matching
|
|
@@ -337,8 +340,18 @@ function getGlobalConfigHomeFragment(runtime) {
|
|
|
337
340
|
// folds the shared-hooks-install skip).
|
|
338
341
|
const RUNTIME_FLAG_IDS = Object.freeze([
|
|
339
342
|
'opencode', 'kilo', 'codex', 'copilot', 'antigravity', 'cursor',
|
|
340
|
-
'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'kimi', 'zcode', 'pi',
|
|
343
|
+
'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'kimi', 'kimi-code', 'zcode', 'pi',
|
|
341
344
|
]);
|
|
345
|
+
/**
|
|
346
|
+
* Convert a runtime id (kebab-case, e.g. 'kimi-code') to its `is<Foo>` flag
|
|
347
|
+
* name in PascalCase (e.g. 'isKimiCode'). The first letter is capitalised and
|
|
348
|
+
* every `-[a-z]` boundary is folded to its uppercase twin. Single-word ids
|
|
349
|
+
* (the prior 16: opencode, kilo, codex, …) are unaffected — only hyphenated
|
|
350
|
+
* ids like 'kimi-code' (#2454) hit the folding branch.
|
|
351
|
+
*/
|
|
352
|
+
function runtimeIdToFlagName(id) {
|
|
353
|
+
return 'is' + id.charAt(0).toUpperCase() + id.slice(1).replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
354
|
+
}
|
|
342
355
|
/**
|
|
343
356
|
* Return a frozen map of `is<Runtime>` boolean predicates for the given runtime
|
|
344
357
|
* id (e.g. `flags.isOpencode`). Collapses the four duplicated `const isX =
|
|
@@ -349,7 +362,7 @@ const RUNTIME_FLAG_IDS = Object.freeze([
|
|
|
349
362
|
function runtimeFlags(runtime) {
|
|
350
363
|
const flags = {};
|
|
351
364
|
for (const id of RUNTIME_FLAG_IDS) {
|
|
352
|
-
flags[
|
|
365
|
+
flags[runtimeIdToFlagName(id)] = runtime === id;
|
|
353
366
|
}
|
|
354
367
|
return Object.freeze(flags);
|
|
355
368
|
}
|
|
@@ -224,7 +224,7 @@ function readStateFile(statePath) {
|
|
|
224
224
|
catch {
|
|
225
225
|
return null;
|
|
226
226
|
}
|
|
227
|
-
const fm = extractFrontmatter(content);
|
|
227
|
+
const fm = extractFrontmatter(content, statePath);
|
|
228
228
|
const body = content.replace(/^---[\s\S]*?---\s*/, '');
|
|
229
229
|
return { fm, body };
|
|
230
230
|
}
|