@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
|
@@ -28,9 +28,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
28
28
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
29
29
|
const node_path_1 = __importDefault(require("node:path"));
|
|
30
30
|
const node_os_1 = __importDefault(require("node:os"));
|
|
31
|
+
const imperative_hook_bus_cjs_1 = require("./host-integration-adapters/imperative-hook-bus.cjs");
|
|
31
32
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
33
|
const shellCmdProjection = require("./shell-command-projection.cjs");
|
|
33
|
-
const { isManagedHookBasename, isManagedHookCommand, projectLegacySettingsHookCommand, projectManagedHookCommand, projectPortableHookBaseDir, projectCodexHookTomlCommand, shellHookOmitsBashRunner, } = shellCmdProjection;
|
|
34
|
+
const { isManagedHookBasename, isManagedHookCommand, projectLegacySettingsHookCommand, projectManagedHookCommand, projectPortableHookBaseDir, projectCodexHookTomlCommand, shellHookOmitsBashRunner, escapeTomlDoubleQuotedString, } = shellCmdProjection;
|
|
34
35
|
// ---------------------------------------------------------------------------
|
|
35
36
|
// Terminal color constants (mirrors install.js for console output parity)
|
|
36
37
|
// ---------------------------------------------------------------------------
|
|
@@ -52,18 +53,68 @@ const GSD_COPILOT_SESSION_HOOK_BASH = 'if [ -f .planning/STATE.md ]; then ' +
|
|
|
52
53
|
const GSD_COPILOT_SESSION_HOOK_PWSH = 'if (Test-Path .planning/STATE.md) ' +
|
|
53
54
|
`{ '{"additionalContext":"${GSD_COPILOT_SESSION_MSG_PRESENT}"}' } ` +
|
|
54
55
|
`else { '{"additionalContext":"${GSD_COPILOT_SESSION_MSG_ABSENT}"}' }`;
|
|
56
|
+
// #2099 UPGRADE 1: multi-event hook bus. Each additional event is a static,
|
|
57
|
+
// deterministic advisory (no branching/no-op-style, matching sessionStart's
|
|
58
|
+
// tone) so the emitted hooks/gsd-session.json stays golden-trackable — no
|
|
59
|
+
// node-runner invocation, no filesystem probing beyond what sessionStart
|
|
60
|
+
// already does.
|
|
61
|
+
const GSD_COPILOT_PRE_TOOL_MSG = 'GSD: confirm this tool use is in scope for the active phase before proceeding.';
|
|
62
|
+
const GSD_COPILOT_PRE_TOOL_HOOK_BASH = `printf '%s' '{"additionalContext":"${GSD_COPILOT_PRE_TOOL_MSG}"}'`;
|
|
63
|
+
const GSD_COPILOT_PRE_TOOL_HOOK_PWSH = `'{"additionalContext":"${GSD_COPILOT_PRE_TOOL_MSG}"}'`;
|
|
64
|
+
const GSD_COPILOT_POST_TOOL_MSG = 'GSD: review the tool result against the active phase before continuing.';
|
|
65
|
+
const GSD_COPILOT_POST_TOOL_HOOK_BASH = `printf '%s' '{"additionalContext":"${GSD_COPILOT_POST_TOOL_MSG}"}'`;
|
|
66
|
+
const GSD_COPILOT_POST_TOOL_HOOK_PWSH = `'{"additionalContext":"${GSD_COPILOT_POST_TOOL_MSG}"}'`;
|
|
67
|
+
const GSD_COPILOT_PROMPT_SUBMIT_MSG = 'GSD: check this request against .planning/STATE.md scope before acting.';
|
|
68
|
+
const GSD_COPILOT_PROMPT_SUBMIT_HOOK_BASH = `printf '%s' '{"additionalContext":"${GSD_COPILOT_PROMPT_SUBMIT_MSG}"}'`;
|
|
69
|
+
const GSD_COPILOT_PROMPT_SUBMIT_HOOK_PWSH = `'{"additionalContext":"${GSD_COPILOT_PROMPT_SUBMIT_MSG}"}'`;
|
|
70
|
+
const GSD_COPILOT_SESSION_END_MSG = 'GSD: update .planning/STATE.md with the session outcome before ending.';
|
|
71
|
+
const GSD_COPILOT_SESSION_END_HOOK_BASH = `printf '%s' '{"additionalContext":"${GSD_COPILOT_SESSION_END_MSG}"}'`;
|
|
72
|
+
const GSD_COPILOT_SESSION_END_HOOK_PWSH = `'{"additionalContext":"${GSD_COPILOT_SESSION_END_MSG}"}'`;
|
|
55
73
|
// ---------------------------------------------------------------------------
|
|
56
74
|
// Cursor hook constants
|
|
57
75
|
// ---------------------------------------------------------------------------
|
|
58
76
|
const GSD_CURSOR_SESSION_HOOK_SCRIPT = 'gsd-cursor-session-start.js';
|
|
59
77
|
const GSD_CURSOR_POST_TOOL_HOOK_SCRIPT = 'gsd-cursor-post-tool.js';
|
|
78
|
+
const GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT = 'gsd-cursor-pre-tool.js';
|
|
79
|
+
const GSD_CURSOR_STOP_HOOK_SCRIPT = 'gsd-cursor-stop.js';
|
|
80
|
+
const GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT = 'gsd-cursor-subagent-start.js';
|
|
81
|
+
const GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT = 'gsd-cursor-subagent-stop.js';
|
|
60
82
|
const GSD_CURSOR_HOOK_MARKER = 'gsd-managed';
|
|
83
|
+
// The full set of Cursor hook events GSD manages — sourced from the adapter
|
|
84
|
+
// (src/host-integration-adapters/imperative-hook-bus.cts) so the vocabulary
|
|
85
|
+
// stays closed and first-party. Used by reconcileCursorHooksJson (the
|
|
86
|
+
// reconciliation scope is always the full set). The install path
|
|
87
|
+
// (writeCursorHooksJson) resolves a descriptor-driven subset via
|
|
88
|
+
// resolveManagedHookEvents(opts.managedHookEvents).
|
|
89
|
+
const CURSOR_MANAGED_EVENTS = imperative_hook_bus_cjs_1.CURSOR_HOOK_EVENTS;
|
|
61
90
|
// ---------------------------------------------------------------------------
|
|
62
91
|
// Cline / AGENTS.md constants
|
|
63
92
|
// ---------------------------------------------------------------------------
|
|
64
93
|
const GSD_AGENTS_MD_MARKER = '<!-- GSD Configuration — managed by gsd-core installer -->';
|
|
65
94
|
const GSD_AGENTS_MD_CLOSE_MARKER = '<!-- End GSD Configuration -->';
|
|
66
95
|
// ---------------------------------------------------------------------------
|
|
96
|
+
// Descriptor-driven runtime title lookup (ADR-1239 / #2092)
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
/**
|
|
99
|
+
* Console-log label for a runtime, sourced from the capability registry's
|
|
100
|
+
* `title` field (capabilities/<runtime>/capability.json). Folded from a
|
|
101
|
+
* hardcoded `runtime === 'qwen' ? 'Qwen Code' : runtime === 'claude' ?
|
|
102
|
+
* 'Claude Code' : runtime` ternary — cosmetic (log text) only, but resolves
|
|
103
|
+
* to the same 'Qwen Code' / 'Claude Code' values for those two runtimes.
|
|
104
|
+
* Falls back to the raw runtime id if the registry can't be loaded or the
|
|
105
|
+
* runtime has no title.
|
|
106
|
+
*/
|
|
107
|
+
function _capabilityTitle(runtime) {
|
|
108
|
+
try {
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
110
|
+
const reg = require('./capability-registry.cjs');
|
|
111
|
+
return reg?.runtimes?.[runtime]?.title || runtime;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return runtime;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
67
118
|
// atomicWriteFileSync — shared canonical implementation.
|
|
68
119
|
//
|
|
69
120
|
// __atomicWrittenTmps is exported so bin/install.js can merge it into its
|
|
@@ -819,9 +870,8 @@ function reconcileCursorHooksJson(hooksJsonPath, managedEntries) {
|
|
|
819
870
|
parsed = {};
|
|
820
871
|
const hasNestedHooksObject = parsed['hooks'] && typeof parsed['hooks'] === 'object' && !Array.isArray(parsed['hooks']);
|
|
821
872
|
if (!hasNestedHooksObject) {
|
|
822
|
-
const eventKeys = ['sessionStart', 'postToolUse'];
|
|
823
873
|
const lifted = {};
|
|
824
|
-
for (const k of
|
|
874
|
+
for (const k of CURSOR_MANAGED_EVENTS) {
|
|
825
875
|
if (Array.isArray(parsed[k])) {
|
|
826
876
|
lifted[k] = parsed[k];
|
|
827
877
|
delete parsed[k];
|
|
@@ -832,9 +882,8 @@ function reconcileCursorHooksJson(hooksJsonPath, managedEntries) {
|
|
|
832
882
|
if (!parsed['version'])
|
|
833
883
|
parsed['version'] = 1;
|
|
834
884
|
const hookTable = parsed['hooks'];
|
|
835
|
-
const MANAGED_EVENTS = ['sessionStart', 'postToolUse'];
|
|
836
885
|
const entries = managedEntries || {};
|
|
837
|
-
for (const event of
|
|
886
|
+
for (const event of CURSOR_MANAGED_EVENTS) {
|
|
838
887
|
const existing = Array.isArray(hookTable[event]) ? hookTable[event] : [];
|
|
839
888
|
const userOwned = existing.filter((e) => !isManagedCursorHookEntry(e));
|
|
840
889
|
const newEntry = entries[event] || null;
|
|
@@ -862,7 +911,11 @@ function writeCursorHooksJson(targetDir, src, opts) {
|
|
|
862
911
|
opts = opts || {};
|
|
863
912
|
const hooksDir = node_path_1.default.join(targetDir, 'hooks');
|
|
864
913
|
node_fs_1.default.mkdirSync(hooksDir, { recursive: true });
|
|
865
|
-
|
|
914
|
+
// Descriptor-driven event resolution (#2089): the managed event set comes
|
|
915
|
+
// from the host descriptor's hostBehaviors.managedHookEvents via the pure
|
|
916
|
+
// adapter (resolveManagedHookEvents), NOT a hardcoded constant.
|
|
917
|
+
const events = (0, imperative_hook_bus_cjs_1.resolveManagedHookEvents)(opts.managedHookEvents);
|
|
918
|
+
const hookScripts = (0, imperative_hook_bus_cjs_1.resolveHookScripts)(events);
|
|
866
919
|
const srcHooksDir = node_path_1.default.join(src, 'hooks');
|
|
867
920
|
const installedScripts = new Set();
|
|
868
921
|
for (const script of hookScripts) {
|
|
@@ -880,27 +933,17 @@ function writeCursorHooksJson(targetDir, src, opts) {
|
|
|
880
933
|
}
|
|
881
934
|
}
|
|
882
935
|
const hookOpts = { runtime: 'cursor', platform: opts.platform || process.platform };
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
type: 'command',
|
|
893
|
-
command: sessionStartCmd,
|
|
894
|
-
[GSD_CURSOR_HOOK_MARKER]: true,
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
if (postToolCmd) {
|
|
898
|
-
managedEntries['postToolUse'] = {
|
|
899
|
-
type: 'command',
|
|
900
|
-
command: postToolCmd,
|
|
901
|
-
[GSD_CURSOR_HOOK_MARKER]: true,
|
|
902
|
-
};
|
|
936
|
+
const commands = {};
|
|
937
|
+
for (const ev of events) {
|
|
938
|
+
const script = imperative_hook_bus_cjs_1.CURSOR_EVENT_SCRIPT_MAP[ev];
|
|
939
|
+
if (script && installedScripts.has(script)) {
|
|
940
|
+
commands[ev] = buildHookCommand(targetDir, script, hookOpts);
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
commands[ev] = null;
|
|
944
|
+
}
|
|
903
945
|
}
|
|
946
|
+
const managedEntries = (0, imperative_hook_bus_cjs_1.buildHookBusEntries)(events, commands);
|
|
904
947
|
const hooksJsonPath = node_path_1.default.join(targetDir, 'hooks.json');
|
|
905
948
|
const result = reconcileCursorHooksJson(hooksJsonPath, managedEntries);
|
|
906
949
|
return { hooksJsonPath, changed: result.changed };
|
|
@@ -928,6 +971,193 @@ function removeCursorHooksJson(targetDir) {
|
|
|
928
971
|
return { changed: result.changed };
|
|
929
972
|
}
|
|
930
973
|
// ---------------------------------------------------------------------------
|
|
974
|
+
// Windsurf/Cascade hook functions (ADR-1239 / #2100 Stage 2 — HOOK-BRIDGE)
|
|
975
|
+
//
|
|
976
|
+
// Cascade (Windsurf's agent) hooks.json format is DISTINCT from Cursor's:
|
|
977
|
+
// { "hooks": { "<event>": [ { "command": "<shell cmd>", ... } ] } }
|
|
978
|
+
// Each entry carries a bare `command` STRING (a shell command line) — not
|
|
979
|
+
// Cursor's `{ type: 'command', command: <cmd> }` wrapper — and there is no
|
|
980
|
+
// top-level `version` field. Docs (reference): https://docs.windsurf.com/llms-full.txt ,
|
|
981
|
+
// https://docs.devin.ai/desktop/cascade/hooks
|
|
982
|
+
//
|
|
983
|
+
// Cascade blocks via EXIT CODE 2 (+ a stderr reason), not Cursor's stdout-JSON
|
|
984
|
+
// `{ block: true, reason }` form — so the two hook scripts installed here
|
|
985
|
+
// (hooks/gsd-windsurf-pre-write.js, hooks/gsd-windsurf-pre-command.js) speak a
|
|
986
|
+
// different protocol than the Cursor scripts, even though the surrounding
|
|
987
|
+
// install/reconcile infra mirrors writeCursorHooksJson/removeCursorHooksJson.
|
|
988
|
+
//
|
|
989
|
+
// Only 2 of GSD's 6 Cursor-parity hook events have a Cascade counterpart with
|
|
990
|
+
// BLOCKING semantics: pre_write_code and pre_run_command. Cascade has no
|
|
991
|
+
// context-injection channel (no `additional_context`-style advisory
|
|
992
|
+
// response), so the 4 advisory events GSD registers on Cursor (sessionStart,
|
|
993
|
+
// postToolUse, stop, subagentStart/subagentStop) are deliberately NOT ported.
|
|
994
|
+
// ---------------------------------------------------------------------------
|
|
995
|
+
const GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT = 'gsd-windsurf-pre-write.js';
|
|
996
|
+
const GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT = 'gsd-windsurf-pre-command.js';
|
|
997
|
+
const GSD_WINDSURF_HOOK_MARKER = 'gsd-managed';
|
|
998
|
+
/** The 2 Cascade hook events GSD wires with blocking (exit-code-2) guards. */
|
|
999
|
+
const WINDSURF_HOOK_EVENTS = Object.freeze(['pre_write_code', 'pre_run_command']);
|
|
1000
|
+
/** Event → hook-script mapping (mirrors CURSOR_EVENT_SCRIPT_MAP's convention). */
|
|
1001
|
+
const WINDSURF_EVENT_SCRIPT_MAP = Object.freeze({
|
|
1002
|
+
pre_write_code: GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT,
|
|
1003
|
+
pre_run_command: GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT,
|
|
1004
|
+
});
|
|
1005
|
+
/** All GSD-managed Windsurf hook scripts (used by uninstall cleanup). */
|
|
1006
|
+
const GSD_WINDSURF_HOOK_SCRIPTS = [
|
|
1007
|
+
GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT,
|
|
1008
|
+
GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT,
|
|
1009
|
+
];
|
|
1010
|
+
/**
|
|
1011
|
+
* Build a single Cascade hooks.json managed entry. Cascade's entry shape has
|
|
1012
|
+
* no `type` field (unlike Cursor's `{ type: 'command', command }`) — just a
|
|
1013
|
+
* bare `command` shell string plus the GSD marker.
|
|
1014
|
+
*/
|
|
1015
|
+
function buildWindsurfHookEntry(command) {
|
|
1016
|
+
return {
|
|
1017
|
+
command,
|
|
1018
|
+
[GSD_WINDSURF_HOOK_MARKER]: true,
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
function isManagedWindsurfHookEntry(entry) {
|
|
1022
|
+
return Boolean(entry && typeof entry === 'object' && entry[GSD_WINDSURF_HOOK_MARKER]);
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Reconcile GSD's managed Cascade hook entries into `<targetDir>/hooks.json`,
|
|
1026
|
+
* preserving any user-owned entries. Mirrors reconcileCursorHooksJson's
|
|
1027
|
+
* merge/no-write-when-unchanged semantics, adapted to Cascade's flatter
|
|
1028
|
+
* `{ hooks: { <event>: [...] } }` shape (no `version` field, no legacy
|
|
1029
|
+
* top-level-array lift — Cascade's hooks.json is a brand-new surface with no
|
|
1030
|
+
* prior shape to migrate from).
|
|
1031
|
+
*/
|
|
1032
|
+
function reconcileWindsurfHooksJson(hooksJsonPath, managedEntries) {
|
|
1033
|
+
let parsed = {};
|
|
1034
|
+
let currentContent = null;
|
|
1035
|
+
if (node_fs_1.default.existsSync(hooksJsonPath)) {
|
|
1036
|
+
const raw = node_fs_1.default.readFileSync(hooksJsonPath, 'utf8');
|
|
1037
|
+
currentContent = raw;
|
|
1038
|
+
if (raw.trim()) {
|
|
1039
|
+
try {
|
|
1040
|
+
parsed = JSON.parse(raw);
|
|
1041
|
+
}
|
|
1042
|
+
catch (err) {
|
|
1043
|
+
throw new Error(`Windsurf hooks.json parse failed: ${err && err.message ? err.message : String(err)}`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
1048
|
+
parsed = {};
|
|
1049
|
+
const hasNestedHooksObject = parsed['hooks'] && typeof parsed['hooks'] === 'object' && !Array.isArray(parsed['hooks']);
|
|
1050
|
+
if (!hasNestedHooksObject)
|
|
1051
|
+
parsed['hooks'] = {};
|
|
1052
|
+
const hookTable = parsed['hooks'];
|
|
1053
|
+
const entries = managedEntries || {};
|
|
1054
|
+
for (const event of WINDSURF_HOOK_EVENTS) {
|
|
1055
|
+
const existing = Array.isArray(hookTable[event]) ? hookTable[event] : [];
|
|
1056
|
+
const userOwned = existing.filter((e) => !isManagedWindsurfHookEntry(e));
|
|
1057
|
+
const newEntry = entries[event] || null;
|
|
1058
|
+
if (newEntry) {
|
|
1059
|
+
hookTable[event] = [...userOwned, newEntry];
|
|
1060
|
+
}
|
|
1061
|
+
else if (userOwned.length > 0) {
|
|
1062
|
+
hookTable[event] = userOwned;
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
delete hookTable[event];
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
// Avoid writing an empty `{ "hooks": {} }` artifact.
|
|
1069
|
+
if (Object.keys(hookTable).length === 0)
|
|
1070
|
+
delete parsed['hooks'];
|
|
1071
|
+
const nextContent = `${JSON.stringify(parsed, null, 2)}\n`;
|
|
1072
|
+
const changed = currentContent !== nextContent;
|
|
1073
|
+
const shouldWrite = changed && (currentContent !== null || Object.keys(parsed).length > 0);
|
|
1074
|
+
if (shouldWrite) {
|
|
1075
|
+
atomicWriteFileSync(hooksJsonPath, nextContent, 'utf8');
|
|
1076
|
+
}
|
|
1077
|
+
return { changed: changed, wrote: shouldWrite, path: hooksJsonPath };
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Write GSD-managed Cascade lifecycle hooks into `<targetDir>/hooks.json`.
|
|
1081
|
+
* Both managed hook scripts (gsd-windsurf-pre-write.js,
|
|
1082
|
+
* gsd-windsurf-pre-command.js) are copied from the GSD hooks/ source to
|
|
1083
|
+
* `<targetDir>/hooks/` first, so the hooks.json entries never reference a
|
|
1084
|
+
* script that wasn't installed. Mirrors writeCursorHooksJson's structure;
|
|
1085
|
+
* `buildHookCommand` is runtime-agnostic (it already returns a plain shell
|
|
1086
|
+
* command string), so it is reused as-is with `runtime: 'windsurf'` — only
|
|
1087
|
+
* the hooks.json ENTRY shape (buildWindsurfHookEntry) and the reconcile
|
|
1088
|
+
* function differ from Cursor's.
|
|
1089
|
+
*
|
|
1090
|
+
* @param targetDir - The Windsurf config dir (global: ~/.codeium/windsurf; local: .windsurf)
|
|
1091
|
+
* @param src - The GSD install source root (for copying hook scripts)
|
|
1092
|
+
* @param opts - `{ platform? }`
|
|
1093
|
+
* @returns `{ hooksJsonPath, changed }`
|
|
1094
|
+
*/
|
|
1095
|
+
function writeWindsurfHooksJson(targetDir, src, opts) {
|
|
1096
|
+
opts = opts || {};
|
|
1097
|
+
const hooksDir = node_path_1.default.join(targetDir, 'hooks');
|
|
1098
|
+
node_fs_1.default.mkdirSync(hooksDir, { recursive: true });
|
|
1099
|
+
const srcHooksDir = node_path_1.default.join(src, 'hooks');
|
|
1100
|
+
const installedScripts = new Set();
|
|
1101
|
+
for (const script of GSD_WINDSURF_HOOK_SCRIPTS) {
|
|
1102
|
+
const srcPath = node_path_1.default.join(srcHooksDir, script);
|
|
1103
|
+
const destPath = node_path_1.default.join(hooksDir, script);
|
|
1104
|
+
if (node_fs_1.default.existsSync(srcPath)) {
|
|
1105
|
+
let content = node_fs_1.default.readFileSync(srcPath, 'utf8');
|
|
1106
|
+
content = content.replace(/gsd:/gi, 'gsd-');
|
|
1107
|
+
node_fs_1.default.writeFileSync(destPath, content);
|
|
1108
|
+
try {
|
|
1109
|
+
node_fs_1.default.chmodSync(destPath, 0o755);
|
|
1110
|
+
}
|
|
1111
|
+
catch { /* Windows: ignore chmod */ }
|
|
1112
|
+
installedScripts.add(script);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
const hookOpts = { runtime: 'windsurf', platform: opts.platform || process.platform };
|
|
1116
|
+
const commands = {};
|
|
1117
|
+
for (const ev of WINDSURF_HOOK_EVENTS) {
|
|
1118
|
+
const script = WINDSURF_EVENT_SCRIPT_MAP[ev];
|
|
1119
|
+
commands[ev] = (script && installedScripts.has(script)) ? buildHookCommand(targetDir, script, hookOpts) : null;
|
|
1120
|
+
}
|
|
1121
|
+
const managedEntries = {};
|
|
1122
|
+
for (const ev of WINDSURF_HOOK_EVENTS) {
|
|
1123
|
+
const cmd = commands[ev];
|
|
1124
|
+
if (cmd)
|
|
1125
|
+
managedEntries[ev] = buildWindsurfHookEntry(cmd);
|
|
1126
|
+
}
|
|
1127
|
+
const hooksJsonPath = node_path_1.default.join(targetDir, 'hooks.json');
|
|
1128
|
+
const result = reconcileWindsurfHooksJson(hooksJsonPath, managedEntries);
|
|
1129
|
+
return { hooksJsonPath, changed: result.changed };
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Remove all GSD-managed Cascade hook entries from hooks.json. User-owned
|
|
1133
|
+
* entries are preserved. If the file becomes empty, it is removed.
|
|
1134
|
+
*
|
|
1135
|
+
* @param targetDir - The Windsurf config dir
|
|
1136
|
+
* @returns `{ changed }`
|
|
1137
|
+
*/
|
|
1138
|
+
function removeWindsurfHooksJson(targetDir) {
|
|
1139
|
+
const hooksJsonPath = node_path_1.default.join(targetDir, 'hooks.json');
|
|
1140
|
+
if (!node_fs_1.default.existsSync(hooksJsonPath))
|
|
1141
|
+
return { changed: false };
|
|
1142
|
+
const result = reconcileWindsurfHooksJson(hooksJsonPath, null);
|
|
1143
|
+
if (result.changed) {
|
|
1144
|
+
try {
|
|
1145
|
+
const contentRaw = node_fs_1.default.readFileSync(hooksJsonPath, 'utf8');
|
|
1146
|
+
const parsed = JSON.parse(contentRaw);
|
|
1147
|
+
const hookTable = (parsed['hooks'] && typeof parsed['hooks'] === 'object' && !Array.isArray(parsed['hooks']))
|
|
1148
|
+
? parsed['hooks']
|
|
1149
|
+
: {};
|
|
1150
|
+
const hasAnyEvents = Object.keys(hookTable).some((k) => Array.isArray(hookTable[k]) && hookTable[k].length > 0);
|
|
1151
|
+
if (!hasAnyEvents) {
|
|
1152
|
+
node_fs_1.default.unlinkSync(hooksJsonPath);
|
|
1153
|
+
return { changed: true };
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
catch { /* best-effort: leave the file */ }
|
|
1157
|
+
}
|
|
1158
|
+
return { changed: result.changed };
|
|
1159
|
+
}
|
|
1160
|
+
// ---------------------------------------------------------------------------
|
|
931
1161
|
// Copilot hook functions
|
|
932
1162
|
// ---------------------------------------------------------------------------
|
|
933
1163
|
function buildCopilotHookConfig() {
|
|
@@ -942,6 +1172,41 @@ function buildCopilotHookConfig() {
|
|
|
942
1172
|
timeoutSec: 10,
|
|
943
1173
|
},
|
|
944
1174
|
],
|
|
1175
|
+
// #2099 UPGRADE 1: multi-event hook bus — preToolUse (worktree/read-safety
|
|
1176
|
+
// advisory), postToolUse (context-monitor advisory), userPromptSubmitted
|
|
1177
|
+
// (prompt-guard advisory), sessionEnd (session-finalize advisory).
|
|
1178
|
+
preToolUse: [
|
|
1179
|
+
{
|
|
1180
|
+
type: 'command',
|
|
1181
|
+
bash: GSD_COPILOT_PRE_TOOL_HOOK_BASH,
|
|
1182
|
+
powershell: GSD_COPILOT_PRE_TOOL_HOOK_PWSH,
|
|
1183
|
+
timeoutSec: 10,
|
|
1184
|
+
},
|
|
1185
|
+
],
|
|
1186
|
+
postToolUse: [
|
|
1187
|
+
{
|
|
1188
|
+
type: 'command',
|
|
1189
|
+
bash: GSD_COPILOT_POST_TOOL_HOOK_BASH,
|
|
1190
|
+
powershell: GSD_COPILOT_POST_TOOL_HOOK_PWSH,
|
|
1191
|
+
timeoutSec: 10,
|
|
1192
|
+
},
|
|
1193
|
+
],
|
|
1194
|
+
userPromptSubmitted: [
|
|
1195
|
+
{
|
|
1196
|
+
type: 'command',
|
|
1197
|
+
bash: GSD_COPILOT_PROMPT_SUBMIT_HOOK_BASH,
|
|
1198
|
+
powershell: GSD_COPILOT_PROMPT_SUBMIT_HOOK_PWSH,
|
|
1199
|
+
timeoutSec: 10,
|
|
1200
|
+
},
|
|
1201
|
+
],
|
|
1202
|
+
sessionEnd: [
|
|
1203
|
+
{
|
|
1204
|
+
type: 'command',
|
|
1205
|
+
bash: GSD_COPILOT_SESSION_END_HOOK_BASH,
|
|
1206
|
+
powershell: GSD_COPILOT_SESSION_END_HOOK_PWSH,
|
|
1207
|
+
timeoutSec: 10,
|
|
1208
|
+
},
|
|
1209
|
+
],
|
|
945
1210
|
},
|
|
946
1211
|
};
|
|
947
1212
|
}
|
|
@@ -966,7 +1231,13 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
966
1231
|
// register settings.json hooks; runtimes with hooksSurface === 'none'
|
|
967
1232
|
// (opencode, kilo) are skipped. Equivalence: hooksSurface !== 'none' iff
|
|
968
1233
|
// the old !isOpencode && !isKilo check.
|
|
969
|
-
|
|
1234
|
+
// #2095: kimi's hooksSurface is 'kimi-hooks-toml' — it registers hooks into
|
|
1235
|
+
// its own native config.toml via writeKimiHooksToml, not settings.json (kimi
|
|
1236
|
+
// never writes settings.json at all: writesSharedSettings stays false). This
|
|
1237
|
+
// guard must also skip kimi's surface so applySettingsJsonHooks doesn't log
|
|
1238
|
+
// misleading "Configured ..." console messages for a settings object that
|
|
1239
|
+
// finishInstall() will never persist for kimi.
|
|
1240
|
+
if (hooksSurface !== 'none' && hooksSurface !== 'kimi-hooks-toml') {
|
|
970
1241
|
if (!settings.hooks) {
|
|
971
1242
|
settings.hooks = {};
|
|
972
1243
|
}
|
|
@@ -1268,12 +1539,18 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1268
1539
|
else if (!hasPhaseBoundaryHook && !phaseBoundaryCommand) {
|
|
1269
1540
|
console.warn(` ${yellow}⚠${reset} Skipped phase boundary hook — Bash executable path unavailable (#3393)`);
|
|
1270
1541
|
}
|
|
1271
|
-
// ── Extended hook events: SubagentStop / Stop / PreCompact
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1275
|
-
//
|
|
1542
|
+
// ── Extended hook events: SubagentStop / Stop / PreCompact / SubagentStart
|
|
1543
|
+
// (#788 + #770 + #2092) ────────────────────────────────────────────────
|
|
1544
|
+
// Claude Code (since #770) and Qwen Code (since #788) both support the
|
|
1545
|
+
// SubagentStop / Stop / PreCompact lifecycle events. Qwen Code additionally
|
|
1546
|
+
// supports SubagentStart (#2092 Phase B, Upgrade 2). Wire gsd-context-
|
|
1547
|
+
// monitor so agents get context-headroom warnings at subagent start,
|
|
1548
|
+
// subagent completion, model stop, and pre-compaction (the most critical
|
|
1549
|
+
// moment to surface headroom info).
|
|
1276
1550
|
//
|
|
1551
|
+
// SubagentStart — subagent lifecycle start (context headroom tracking;
|
|
1552
|
+
// qwen-only today — no other runtime declares it in
|
|
1553
|
+
// extendedHookEvents)
|
|
1277
1554
|
// SubagentStop — subagent lifecycle completion (context headroom tracking)
|
|
1278
1555
|
// Stop — model stop / final-response moment (context headroom)
|
|
1279
1556
|
// PreCompact — fires before conversation compaction (most critical
|
|
@@ -1283,11 +1560,15 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1283
1560
|
// user prompt text, not a tool invocation, so gsd-prompt-guard (which
|
|
1284
1561
|
// exits unless tool_name is Write/Edit) would be a silent no-op. A
|
|
1285
1562
|
// dedicated handler for UserPromptSubmit is deferred to a follow-on issue.
|
|
1286
|
-
// SubagentStop, Stop, PreCompact — route through the context monitor.
|
|
1287
|
-
// Guard is
|
|
1563
|
+
// SubagentStart, SubagentStop, Stop, PreCompact — route through the context monitor.
|
|
1564
|
+
// Guard is descriptor-driven: only events present in extendedEvents are wired,
|
|
1565
|
+
// so this loop is a no-op for every runtime that doesn't list SubagentStart.
|
|
1288
1566
|
{
|
|
1289
|
-
|
|
1290
|
-
|
|
1567
|
+
// Descriptor-driven (ADR-1239 / #2092): folded from a hardcoded
|
|
1568
|
+
// `runtime === 'qwen' ? ... : ...` ternary into a capability-title
|
|
1569
|
+
// lookup (see _capabilityTitle above).
|
|
1570
|
+
const runtimeLabel = _capabilityTitle(runtime);
|
|
1571
|
+
for (const event of ['SubagentStop', 'Stop', 'PreCompact', 'SubagentStart']) {
|
|
1291
1572
|
if (!extendedEvents.includes(event))
|
|
1292
1573
|
continue;
|
|
1293
1574
|
if (!settings.hooks[event]) {
|
|
@@ -1311,7 +1592,7 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1311
1592
|
}
|
|
1312
1593
|
}
|
|
1313
1594
|
}
|
|
1314
|
-
// ── end SubagentStop / Stop / PreCompact events
|
|
1595
|
+
// ── end SubagentStop / Stop / PreCompact / SubagentStart events ────────────
|
|
1315
1596
|
// ── Extended hook events (#776; Gemini runtime removed #1928) ──────────────
|
|
1316
1597
|
// The Gemini-3-backend dialect exposes several hook events beyond
|
|
1317
1598
|
// BeforeTool/AfterTool. These were added for the now-removed Gemini CLI
|
|
@@ -1408,6 +1689,199 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1408
1689
|
@typescript-eslint/no-unsafe-assignment */
|
|
1409
1690
|
}
|
|
1410
1691
|
// ---------------------------------------------------------------------------
|
|
1692
|
+
// Kimi hooks.toml (#2095 EoS/kimi Upgrade 1 — native hook bus)
|
|
1693
|
+
//
|
|
1694
|
+
// Kimi CLI reads lifecycle hooks from a flat `[[hooks]]` array in its own
|
|
1695
|
+
// config.toml (moonshotai.github.io/kimi-cli/en/customization/hooks.html),
|
|
1696
|
+
// not from settings.json. Unlike every other hooksSurface writer above, this
|
|
1697
|
+
// file lives OUTSIDE the runtime's GSD configDir: kimi's configDir is the
|
|
1698
|
+
// generic Agent-Skills root (~/.config/agents by default), while config.toml
|
|
1699
|
+
// is a sibling at ~/.kimi (KIMI_SHARE_DIR override), resolved by
|
|
1700
|
+
// resolveKimiHooksTomlDir in runtime-homes.cts. Callers resolve that path and
|
|
1701
|
+
// pass it in explicitly — this module never reaches into runtime-homes.cjs
|
|
1702
|
+
// itself, keeping the same configDir/targetDir-passed-in shape every other
|
|
1703
|
+
// writer in this file uses.
|
|
1704
|
+
//
|
|
1705
|
+
// GSD-owned [[hooks]] entries are wrapped in marker comments so a reinstall
|
|
1706
|
+
// can find-and-replace only GSD's own block, leaving any user-authored
|
|
1707
|
+
// [[hooks]] entries elsewhere in the file untouched — mirrors the marker
|
|
1708
|
+
// approach stripStaleGsdHookBlocks uses for Codex's config.toml, simplified
|
|
1709
|
+
// to plain string slicing since this block is a flat, self-contained span
|
|
1710
|
+
// (no nested per-key structural TOML parsing is needed).
|
|
1711
|
+
// ---------------------------------------------------------------------------
|
|
1712
|
+
const KIMI_HOOKS_TOML_MARKER_BEGIN = '# GSD Hooks BEGIN — managed by GSD, do not edit between these markers';
|
|
1713
|
+
const KIMI_HOOKS_TOML_MARKER_END = '# GSD Hooks END';
|
|
1714
|
+
function buildKimiHookEntryToml(spec) {
|
|
1715
|
+
if (!spec.command)
|
|
1716
|
+
return null;
|
|
1717
|
+
const lines = ['[[hooks]]', `event = "${spec.event}"`];
|
|
1718
|
+
if (spec.matcher) {
|
|
1719
|
+
lines.push(`matcher = "${escapeTomlDoubleQuotedString(spec.matcher)}"`);
|
|
1720
|
+
}
|
|
1721
|
+
lines.push(`command = "${escapeTomlDoubleQuotedString(spec.command)}"`);
|
|
1722
|
+
if (typeof spec.timeout === 'number') {
|
|
1723
|
+
lines.push(`timeout = ${spec.timeout}`);
|
|
1724
|
+
}
|
|
1725
|
+
return lines.join('\n');
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Build the full marker-delimited GSD [[hooks]] block for kimi's config.toml,
|
|
1729
|
+
* or null when no GSD hook resolved to a usable command (hooks/ missing, or
|
|
1730
|
+
* the node/bash runner could not be resolved — mirrors the #1754/#3002
|
|
1731
|
+
* defensive guards applySettingsJsonHooks applies per-hook above).
|
|
1732
|
+
*
|
|
1733
|
+
* Event -> hook mapping mirrors applySettingsJsonHooks' settings.json wiring
|
|
1734
|
+
* 1:1 by GSD hook script (update check, session-state, phase-boundary,
|
|
1735
|
+
* graphify, context monitor, prompt/read/workflow/worktree guards, commit
|
|
1736
|
+
* validation). Kimi's 13 lifecycle events include exact-name equivalents for
|
|
1737
|
+
* every Claude-dialect event GSD currently wires (SessionStart, PreToolUse,
|
|
1738
|
+
* PostToolUse, Stop, PreCompact, SubagentStart, SubagentStop) — see
|
|
1739
|
+
* moonshotai.github.io/kimi-cli/en/customization/hooks.html.
|
|
1740
|
+
*
|
|
1741
|
+
* Matcher translation (best-effort — Kimi's tool-name vocabulary is
|
|
1742
|
+
* confirmed distinct from Claude's by the upstream hooks doc's own examples):
|
|
1743
|
+
* Bash -> Shell, Write -> WriteFile, Edit/MultiEdit -> StrReplaceFile.
|
|
1744
|
+
* Read -> ReadFile follows the same WriteFile/StrReplaceFile naming
|
|
1745
|
+
* convention but is not independently doc-confirmed. Claude's Agent|Task
|
|
1746
|
+
* (subagent-dispatch) matcher segment has no confirmed Kimi tool name and is
|
|
1747
|
+
* dropped rather than guessed — gsd-context-monitor's PostToolUse entry runs
|
|
1748
|
+
* unmatched (all tools) instead, which only widens when it fires, it never
|
|
1749
|
+
* narrows incorrectly.
|
|
1750
|
+
*/
|
|
1751
|
+
function buildKimiHooksTomlBlock(targetDir, opts) {
|
|
1752
|
+
const { hookOpts } = opts;
|
|
1753
|
+
const cmd = (hookName) => {
|
|
1754
|
+
if (!node_fs_1.default.existsSync(node_path_1.default.join(targetDir, 'hooks', hookName)))
|
|
1755
|
+
return null;
|
|
1756
|
+
return buildHookCommand(targetDir, hookName, hookOpts);
|
|
1757
|
+
};
|
|
1758
|
+
const specs = [
|
|
1759
|
+
// SessionStart — unmatched (session-level; no tool_name to filter on).
|
|
1760
|
+
{ event: 'SessionStart', command: cmd('gsd-check-update.js') },
|
|
1761
|
+
{ event: 'SessionStart', command: cmd('gsd-session-state.sh') },
|
|
1762
|
+
// PreToolUse
|
|
1763
|
+
{ event: 'PreToolUse', command: cmd('gsd-prompt-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout: 5 },
|
|
1764
|
+
{ event: 'PreToolUse', command: cmd('gsd-read-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout: 5 },
|
|
1765
|
+
{ event: 'PreToolUse', command: cmd('gsd-worktree-path-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout: 5 },
|
|
1766
|
+
{ event: 'PreToolUse', command: cmd('gsd-workflow-guard.js'), matcher: 'Shell|WriteFile|StrReplaceFile', timeout: 5 },
|
|
1767
|
+
{ event: 'PreToolUse', command: cmd('gsd-validate-commit.sh'), matcher: 'Shell', timeout: 5 },
|
|
1768
|
+
// PostToolUse
|
|
1769
|
+
{ event: 'PostToolUse', command: cmd('gsd-context-monitor.js'), timeout: 10 },
|
|
1770
|
+
{ event: 'PostToolUse', command: cmd('gsd-phase-boundary.sh'), matcher: 'WriteFile|StrReplaceFile', timeout: 5 },
|
|
1771
|
+
{ event: 'PostToolUse', command: cmd('gsd-read-injection-scanner.js'), matcher: 'ReadFile', timeout: 5 },
|
|
1772
|
+
{ event: 'PostToolUse', command: cmd('gsd-graphify-update.sh'), matcher: 'Shell', timeout: 5 },
|
|
1773
|
+
// Extended lifecycle events — context-headroom tracking (unmatched).
|
|
1774
|
+
{ event: 'Stop', command: cmd('gsd-context-monitor.js'), timeout: 10 },
|
|
1775
|
+
{ event: 'PreCompact', command: cmd('gsd-context-monitor.js'), timeout: 10 },
|
|
1776
|
+
{ event: 'SubagentStart', command: cmd('gsd-context-monitor.js'), timeout: 10 },
|
|
1777
|
+
{ event: 'SubagentStop', command: cmd('gsd-context-monitor.js'), timeout: 10 },
|
|
1778
|
+
];
|
|
1779
|
+
const entries = specs
|
|
1780
|
+
.map(buildKimiHookEntryToml)
|
|
1781
|
+
.filter((entry) => entry !== null);
|
|
1782
|
+
if (entries.length === 0)
|
|
1783
|
+
return null;
|
|
1784
|
+
return [KIMI_HOOKS_TOML_MARKER_BEGIN, '', entries.join('\n\n'), '', KIMI_HOOKS_TOML_MARKER_END].join('\n');
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Strip a previously-written GSD [[hooks]] block from kimi's config.toml
|
|
1788
|
+
* content. Pure string function (no fs access) so install, uninstall, and
|
|
1789
|
+
* tests share one strip implementation. Returns null when stripping leaves
|
|
1790
|
+
* nothing but whitespace (the file was GSD-only), so the caller can unlink
|
|
1791
|
+
* it instead of writing an empty file.
|
|
1792
|
+
*/
|
|
1793
|
+
function stripKimiHooksTomlBlock(content) {
|
|
1794
|
+
const beginIdx = content.indexOf(KIMI_HOOKS_TOML_MARKER_BEGIN);
|
|
1795
|
+
if (beginIdx === -1) {
|
|
1796
|
+
return content.trim() === '' ? null : content;
|
|
1797
|
+
}
|
|
1798
|
+
const endMarkerIdx = content.indexOf(KIMI_HOOKS_TOML_MARKER_END, beginIdx);
|
|
1799
|
+
if (endMarkerIdx === -1) {
|
|
1800
|
+
// Malformed marker pair — BEGIN present but no END after it (missing END,
|
|
1801
|
+
// or an END that only appears earlier in the file, before BEGIN). Never
|
|
1802
|
+
// fall back to content.length here: that would slice to EOF and destroy
|
|
1803
|
+
// every user section that follows. Leave the content untouched instead;
|
|
1804
|
+
// a subsequent writeKimiHooksToml call will append a fresh, well-formed
|
|
1805
|
+
// block rather than silently deleting user data.
|
|
1806
|
+
return content;
|
|
1807
|
+
}
|
|
1808
|
+
const endIdx = endMarkerIdx + KIMI_HOOKS_TOML_MARKER_END.length;
|
|
1809
|
+
// Swallow blank lines immediately surrounding the block so repeated
|
|
1810
|
+
// strip+rewrite cycles never accumulate blank lines.
|
|
1811
|
+
let sliceStart = beginIdx;
|
|
1812
|
+
while (sliceStart > 0 && (content[sliceStart - 1] === '\n' || content[sliceStart - 1] === '\r'))
|
|
1813
|
+
sliceStart -= 1;
|
|
1814
|
+
let sliceEnd = endIdx;
|
|
1815
|
+
while (sliceEnd < content.length && (content[sliceEnd] === '\n' || content[sliceEnd] === '\r'))
|
|
1816
|
+
sliceEnd += 1;
|
|
1817
|
+
const before = content.slice(0, sliceStart);
|
|
1818
|
+
const after = content.slice(sliceEnd);
|
|
1819
|
+
// Blank-line swallowing above consumes every newline flanking the block,
|
|
1820
|
+
// including the one required to keep the surrounding user sections on
|
|
1821
|
+
// separate lines. If the block sat BETWEEN two user sections (content
|
|
1822
|
+
// survives on both sides), concatenating `before` + `after` directly would
|
|
1823
|
+
// glue the last line of the earlier section onto the first line of the
|
|
1824
|
+
// later one. Reinsert a blank-line separator in that case; when only one
|
|
1825
|
+
// side has content (block at file start or EOF), no separator is needed —
|
|
1826
|
+
// that matches the pre-existing idempotent behavior for those shapes.
|
|
1827
|
+
const result = before.trim() !== '' && after.trim() !== ''
|
|
1828
|
+
? `${before}\n\n${after}`
|
|
1829
|
+
: before + after;
|
|
1830
|
+
return result.trim() === '' ? null : result;
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Idempotently (re)write kimi's GSD-owned [[hooks]] block into its native
|
|
1834
|
+
* config.toml at `configPath` (resolved by the caller via
|
|
1835
|
+
* resolveKimiHooksTomlDir). No-ops (`{changed:false}`) when the computed
|
|
1836
|
+
* block is byte-identical to what's already on disk, so reinstalls don't
|
|
1837
|
+
* touch the file's mtime for no reason.
|
|
1838
|
+
*/
|
|
1839
|
+
function writeKimiHooksToml(configPath, targetDir, opts) {
|
|
1840
|
+
const existing = node_fs_1.default.existsSync(configPath) ? node_fs_1.default.readFileSync(configPath, 'utf8') : '';
|
|
1841
|
+
const stripped = stripKimiHooksTomlBlock(existing) ?? '';
|
|
1842
|
+
const block = buildKimiHooksTomlBlock(targetDir, opts);
|
|
1843
|
+
const entryCount = block ? (block.match(/\[\[hooks\]\]/g) || []).length : 0;
|
|
1844
|
+
if (!block) {
|
|
1845
|
+
if (stripped === existing)
|
|
1846
|
+
return { changed: false, path: configPath, entryCount: 0 };
|
|
1847
|
+
if (stripped.trim() === '') {
|
|
1848
|
+
if (node_fs_1.default.existsSync(configPath))
|
|
1849
|
+
node_fs_1.default.unlinkSync(configPath);
|
|
1850
|
+
}
|
|
1851
|
+
else {
|
|
1852
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(configPath), { recursive: true });
|
|
1853
|
+
atomicWriteFileSync(configPath, stripped, 'utf8');
|
|
1854
|
+
}
|
|
1855
|
+
return { changed: true, path: configPath, entryCount: 0 };
|
|
1856
|
+
}
|
|
1857
|
+
const separator = stripped.trim() === '' ? '' : (stripped.endsWith('\n') ? '\n' : '\n\n');
|
|
1858
|
+
const next = stripped.trim() === '' ? `${block}\n` : `${stripped}${separator}${block}\n`;
|
|
1859
|
+
if (next === existing)
|
|
1860
|
+
return { changed: false, path: configPath, entryCount };
|
|
1861
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(configPath), { recursive: true });
|
|
1862
|
+
atomicWriteFileSync(configPath, next, 'utf8');
|
|
1863
|
+
return { changed: true, path: configPath, entryCount };
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Uninstall-time counterpart to writeKimiHooksToml: strips the GSD block and
|
|
1867
|
+
* deletes the file if nothing but GSD's own block was ever in it.
|
|
1868
|
+
*/
|
|
1869
|
+
function removeKimiHooksToml(configPath) {
|
|
1870
|
+
if (!node_fs_1.default.existsSync(configPath))
|
|
1871
|
+
return { changed: false };
|
|
1872
|
+
const existing = node_fs_1.default.readFileSync(configPath, 'utf8');
|
|
1873
|
+
const stripped = stripKimiHooksTomlBlock(existing);
|
|
1874
|
+
if (stripped === existing)
|
|
1875
|
+
return { changed: false };
|
|
1876
|
+
if (stripped === null || stripped.trim() === '') {
|
|
1877
|
+
node_fs_1.default.unlinkSync(configPath);
|
|
1878
|
+
}
|
|
1879
|
+
else {
|
|
1880
|
+
atomicWriteFileSync(configPath, stripped, 'utf8');
|
|
1881
|
+
}
|
|
1882
|
+
return { changed: true };
|
|
1883
|
+
}
|
|
1884
|
+
// ---------------------------------------------------------------------------
|
|
1411
1885
|
// referencesHook
|
|
1412
1886
|
//
|
|
1413
1887
|
// Pure predicate — checks whether a hook entry object references a managed
|
|
@@ -1449,7 +1923,23 @@ module.exports = {
|
|
|
1449
1923
|
removeCursorHooksJson,
|
|
1450
1924
|
GSD_CURSOR_SESSION_HOOK_SCRIPT,
|
|
1451
1925
|
GSD_CURSOR_POST_TOOL_HOOK_SCRIPT,
|
|
1926
|
+
GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT,
|
|
1927
|
+
GSD_CURSOR_STOP_HOOK_SCRIPT,
|
|
1928
|
+
GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT,
|
|
1929
|
+
GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT,
|
|
1452
1930
|
GSD_CURSOR_HOOK_MARKER,
|
|
1931
|
+
// Windsurf/Cascade
|
|
1932
|
+
buildWindsurfHookEntry,
|
|
1933
|
+
isManagedWindsurfHookEntry,
|
|
1934
|
+
reconcileWindsurfHooksJson,
|
|
1935
|
+
writeWindsurfHooksJson,
|
|
1936
|
+
removeWindsurfHooksJson,
|
|
1937
|
+
WINDSURF_HOOK_EVENTS,
|
|
1938
|
+
WINDSURF_EVENT_SCRIPT_MAP,
|
|
1939
|
+
GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT,
|
|
1940
|
+
GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT,
|
|
1941
|
+
GSD_WINDSURF_HOOK_SCRIPTS,
|
|
1942
|
+
GSD_WINDSURF_HOOK_MARKER,
|
|
1453
1943
|
// Copilot
|
|
1454
1944
|
buildCopilotHookConfig,
|
|
1455
1945
|
writeCopilotHookConfig,
|
|
@@ -1465,6 +1955,13 @@ module.exports = {
|
|
|
1465
1955
|
// Codex TOML
|
|
1466
1956
|
buildCodexHookBlock,
|
|
1467
1957
|
rewriteLegacyCodexHookBlock,
|
|
1958
|
+
// Kimi hooks.toml
|
|
1959
|
+
buildKimiHooksTomlBlock,
|
|
1960
|
+
stripKimiHooksTomlBlock,
|
|
1961
|
+
writeKimiHooksToml,
|
|
1962
|
+
removeKimiHooksToml,
|
|
1963
|
+
KIMI_HOOKS_TOML_MARKER_BEGIN,
|
|
1964
|
+
KIMI_HOOKS_TOML_MARKER_END,
|
|
1468
1965
|
// Shared
|
|
1469
1966
|
buildHookCommand,
|
|
1470
1967
|
applySettingsJsonHooks,
|