howtofixcli 0.7.8 → 0.7.9

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.
Files changed (152) hide show
  1. package/dist/chunk-27TRKD65.js +2 -0
  2. package/dist/chunk-3FYCFLNK.js +1 -0
  3. package/dist/chunk-56SEAGAQ.js +1 -0
  4. package/dist/chunk-63ZCQDXH.js +6 -0
  5. package/dist/chunk-6FUWMLWO.js +1 -0
  6. package/dist/chunk-7CHDNTH6.js +1 -0
  7. package/dist/chunk-ACOMNDUL.js +185 -0
  8. package/dist/chunk-AEVPET7X.js +2 -0
  9. package/dist/chunk-AGGIYXN7.js +2 -0
  10. package/dist/chunk-BHVBO4SC.js +1 -0
  11. package/dist/chunk-FPXVDP32.js +8 -0
  12. package/dist/chunk-GIUYIKH3.js +109 -0
  13. package/dist/chunk-GQCAJ56I.js +5 -0
  14. package/dist/chunk-GXB7RZHH.js +1 -0
  15. package/dist/chunk-IGEY3AWF.js +1 -0
  16. package/dist/chunk-ILDDDLRC.js +1 -0
  17. package/dist/chunk-JFYVRWJI.js +2 -0
  18. package/dist/chunk-MVLCGU3G.js +1 -0
  19. package/dist/chunk-N345KWWK.js +1 -0
  20. package/dist/chunk-NBQFPCRT.js +1 -0
  21. package/dist/chunk-OIPSLIZE.js +5 -0
  22. package/dist/chunk-ORJ2QZBG.js +3 -0
  23. package/dist/chunk-PCALBZOO.js +2 -0
  24. package/dist/chunk-PDNBGUYP.js +2 -0
  25. package/dist/chunk-S656RE4A.js +1 -0
  26. package/dist/chunk-TSEKPGK4.js +1 -0
  27. package/dist/chunk-U6CI6KXN.js +34 -0
  28. package/dist/chunk-UTYFLLSM.js +1 -0
  29. package/dist/chunk-V3R4FYTJ.js +1 -0
  30. package/dist/chunk-V656XVXV.js +8 -0
  31. package/dist/chunk-WKUJ7CFO.js +1 -0
  32. package/dist/chunk-WOT6VMZA.js +1 -0
  33. package/dist/chunk-WUPU6LTD.js +4 -0
  34. package/dist/chunk-WX3V5P7U.js +5 -0
  35. package/dist/chunk-ZKZ6WCI3.js +2 -0
  36. package/dist/main.js +94 -536
  37. package/package.json +15 -62
  38. package/dist/account-credential.js +0 -136
  39. package/dist/account-credential.test.js +0 -38
  40. package/dist/activity.js +0 -124
  41. package/dist/agent-defs.js +0 -237
  42. package/dist/agent-limiter.js +0 -68
  43. package/dist/agents.js +0 -554
  44. package/dist/anim.js +0 -361
  45. package/dist/args.js +0 -228
  46. package/dist/at-file.js +0 -28
  47. package/dist/brand-extension.js +0 -87
  48. package/dist/brand.js +0 -76
  49. package/dist/browser.js +0 -58
  50. package/dist/cache-report.js +0 -139
  51. package/dist/checkpoint.js +0 -437
  52. package/dist/claims.js +0 -167
  53. package/dist/cli-auth.js +0 -142
  54. package/dist/cli-checkin.js +0 -120
  55. package/dist/cli-credentials.js +0 -254
  56. package/dist/clipboard.js +0 -149
  57. package/dist/collapse.js +0 -129
  58. package/dist/commands.js +0 -301
  59. package/dist/compaction-guard.js +0 -81
  60. package/dist/context-extension.js +0 -17
  61. package/dist/dcp/commands.js +0 -172
  62. package/dist/dcp/compress-state.js +0 -139
  63. package/dist/dcp/compress.js +0 -329
  64. package/dist/dcp/dcp-extension.js +0 -288
  65. package/dist/dcp/dedup.js +0 -93
  66. package/dist/dcp/ids.js +0 -189
  67. package/dist/dcp/index.js +0 -25
  68. package/dist/dcp/prompts.js +0 -68
  69. package/dist/dcp/protected-content.js +0 -221
  70. package/dist/dcp/protected.js +0 -161
  71. package/dist/dcp/prune.js +0 -418
  72. package/dist/dcp/state.js +0 -176
  73. package/dist/device-id.js +0 -90
  74. package/dist/device-id.test.js +0 -23
  75. package/dist/errors.js +0 -63
  76. package/dist/extensions.js +0 -48
  77. package/dist/fatal.js +0 -35
  78. package/dist/feed.js +0 -498
  79. package/dist/fs-atomic.js +0 -54
  80. package/dist/gateway-errors.js +0 -90
  81. package/dist/git.js +0 -230
  82. package/dist/goal-extension.js +0 -88
  83. package/dist/goal.js +0 -222
  84. package/dist/hooks-extension.js +0 -176
  85. package/dist/hooks.js +0 -443
  86. package/dist/howtofix-account.js +0 -715
  87. package/dist/hud-line.js +0 -153
  88. package/dist/hwid.js +0 -130
  89. package/dist/interrupt.js +0 -60
  90. package/dist/json-events.js +0 -50
  91. package/dist/level-style.js +0 -112
  92. package/dist/loop-guard.js +0 -95
  93. package/dist/lsp-tools.js +0 -32
  94. package/dist/lsp.js +0 -347
  95. package/dist/mcp-config.js +0 -85
  96. package/dist/mcp-lifecycle.js +0 -147
  97. package/dist/model-catalog.js +0 -217
  98. package/dist/model-resolve.js +0 -65
  99. package/dist/notes.js +0 -78
  100. package/dist/oauth-login.js +0 -568
  101. package/dist/paths.js +0 -61
  102. package/dist/permissions.js +0 -298
  103. package/dist/plan-mode.js +0 -363
  104. package/dist/preset-command.js +0 -227
  105. package/dist/preset-extension.js +0 -35
  106. package/dist/preset-mcp.js +0 -292
  107. package/dist/presets.js +0 -727
  108. package/dist/prompt-files.js +0 -103
  109. package/dist/prompt-text.js +0 -41
  110. package/dist/proxy.js +0 -137
  111. package/dist/quality/ast.js +0 -196
  112. package/dist/quality/cognitive.js +0 -83
  113. package/dist/quality/cyclomatic.js +0 -30
  114. package/dist/quality/halstead.js +0 -83
  115. package/dist/quality/index.js +0 -18
  116. package/dist/quality/length.js +0 -3
  117. package/dist/quality/maintainability.js +0 -15
  118. package/dist/quality/nesting.js +0 -57
  119. package/dist/quality/report.js +0 -803
  120. package/dist/quality/smells.js +0 -67
  121. package/dist/quality-tool.js +0 -63
  122. package/dist/read-only-commands.js +0 -597
  123. package/dist/repo-map.js +0 -292
  124. package/dist/rtk.js +0 -193
  125. package/dist/sandbox.js +0 -105
  126. package/dist/schema.js +0 -214
  127. package/dist/session-lifecycle.js +0 -19
  128. package/dist/session-retention.js +0 -111
  129. package/dist/session.js +0 -446
  130. package/dist/sessions.js +0 -100
  131. package/dist/settings.js +0 -148
  132. package/dist/skills.js +0 -35
  133. package/dist/slop-tool.js +0 -96
  134. package/dist/spend-cap.js +0 -43
  135. package/dist/subagent.js +0 -171
  136. package/dist/tar.js +0 -146
  137. package/dist/theme.js +0 -79
  138. package/dist/thinking.js +0 -45
  139. package/dist/todo.js +0 -114
  140. package/dist/tool-loadout.js +0 -97
  141. package/dist/tool-render.js +0 -769
  142. package/dist/tools.js +0 -176
  143. package/dist/ui.js +0 -4757
  144. package/dist/update.js +0 -533
  145. package/dist/usage-segment.js +0 -69
  146. package/dist/web-tools.js +0 -46
  147. package/dist/web.js +0 -265
  148. package/dist/welcome.js +0 -179
  149. package/dist/workflow-engine.js +0 -246
  150. package/dist/workflow-progress.js +0 -446
  151. package/dist/workflow-view.js +0 -195
  152. package/dist/workflow.js +0 -349
package/dist/goal.js DELETED
@@ -1,222 +0,0 @@
1
- /**
2
- * A persistent goal for the session, with usage accounting and a continuation
3
- * loop.
4
- *
5
- * Why this exists: opencode's most-reacted session request (#27167) is a native
6
- * goal, and its own author states the reason it is not a command template — it
7
- * "needs persistence, runtime continuation, usage accounting, and client
8
- * sync/status surfaces". A slash command that prints a prompt can do none of
9
- * those. What a goal buys over a plain message is that the harness knows what
10
- * the run is *for*, so it can:
11
- *
12
- * - keep the goal in the prompt every request (a long task survives a
13
- * compaction that drops the original message),
14
- * - continue working when the agent tries to stop with the goal still open,
15
- * - account the cost against it and stop at a budget instead of running away,
16
- * - refuse to let the model declare victory without evidence.
17
- *
18
- * State lives in the session branch (a `custom` entry), so it survives a resume
19
- * and stays correct across a fork — the same reason `todo.ts` keeps its list
20
- * there. A module-level mirror exists only so the HUD can paint the goal without
21
- * a session handle; it is reseeded from the branch on `session_start`.
22
- *
23
- * Pure where it matters: {@link applyGoalOp} and {@link shouldContinue} hold the
24
- * policy, so both are testable without a session or a model.
25
- */
26
- import { StringEnum } from "@netrodev/hxf-ai";
27
- import { defineTool } from "@netrodev/hxf-coding-agent";
28
- import { Type } from "typebox";
29
- /** Session entry type the goal is persisted under. */
30
- export const GOAL_ENTRY = "howtofix.goal";
31
- /** Custom message type used for the automatic continuation. */
32
- export const GOAL_CONTINUE = "howtofix.goal.continue";
33
- /** Longest goal text accepted. */
34
- export const MAX_GOAL = 400;
35
- /**
36
- * Continuations allowed when the goal sets no budget.
37
- *
38
- * A goal that can continue forever is the failure mode this whole feature is
39
- * meant to avoid, so the default is a cap rather than "unlimited": an unattended
40
- * run stops and says so, and the user can resume it.
41
- */
42
- export const DEFAULT_BUDGET = 25;
43
- const MARK = {
44
- active: "●",
45
- paused: "‖",
46
- done: "✓",
47
- cleared: "○",
48
- };
49
- /** One line for the tool result and the HUD. */
50
- export function formatGoal(goal) {
51
- if (!goal || goal.status === "cleared")
52
- return "No goal set.";
53
- const cap = goal.budget ?? DEFAULT_BUDGET;
54
- const parts = [`${MARK[goal.status]} ${goal.status}`, `${goal.used}/${cap} continuations`];
55
- if (goal.note)
56
- parts.push(goal.note);
57
- return `${parts.join(" · ")}\n${goal.text}`;
58
- }
59
- /** The short form painted in the HUD. Empty when there is nothing worth showing. */
60
- export function goalSegment(goal) {
61
- if (!goal || goal.status === "cleared" || goal.status === "done")
62
- return "";
63
- const cap = goal.budget ?? DEFAULT_BUDGET;
64
- const text = goal.text.length > 48 ? `${goal.text.slice(0, 47)}…` : goal.text;
65
- return `goal ${MARK[goal.status]} ${goal.used}/${cap} · ${text}`;
66
- }
67
- /**
68
- * The block injected into the prompt, so the goal survives compaction.
69
- *
70
- * Deliberately free of anything that changes between turns. This is appended to
71
- * the system message, which is the cacheable prefix of every request, so a
72
- * counter here would invalidate the prompt cache on every continuation — the
73
- * same class of mistake that cost Claude Code 10.2% of fleet-wide cache
74
- * creation tokens when a dynamic agent list sat in a tool schema. The spend is
75
- * reported where it belongs: in the continuation message and the tool result,
76
- * both of which are new content and cannot invalidate a cached prefix.
77
- */
78
- export function renderGoalBlock(goal) {
79
- const lines = ["<goal>", goal.text, `Status: ${goal.status}.`];
80
- if (goal.status === "active") {
81
- lines.push("Work toward this goal until it is genuinely met. When it is, call the `goal` tool with", 'op="complete" and a note naming the evidence; a command you ran or a file you checked.', "If you are blocked and cannot proceed, say so plainly instead of repeating an attempt.");
82
- }
83
- lines.push("</goal>");
84
- return lines.join("\n");
85
- }
86
- /** What the agent is told when the harness decides to keep going. */
87
- export function continuationPrompt(goal) {
88
- return [
89
- "[goal] The goal is still open and you stopped without closing it:",
90
- ` ${goal.text}`,
91
- "",
92
- "Continue from where you left off. Do not repeat work you already did. If the goal is met,",
93
- 'call the `goal` tool with op="complete" and a note naming the evidence. Do not call it',
94
- "complete to escape a hard problem: if you cannot make progress, state what is blocking you.",
95
- `Continuations used: ${goal.used}/${goal.budget ?? DEFAULT_BUDGET}.`,
96
- ].join("\n");
97
- }
98
- /** A stable in-memory mirror for surfaces with no session handle (the HUD). */
99
- let mirror;
100
- export function currentGoal() {
101
- return mirror;
102
- }
103
- export function setMirror(goal) {
104
- mirror = goal;
105
- }
106
- export function shouldContinue(goal) {
107
- if (!goal)
108
- return { action: "stop", reason: "no goal", close: false };
109
- if (goal.status === "done")
110
- return { action: "stop", reason: "goal complete", close: false };
111
- if (goal.status === "cleared")
112
- return { action: "stop", reason: "goal cleared", close: false };
113
- if (goal.status === "paused")
114
- return { action: "stop", reason: "goal paused", close: false };
115
- const cap = goal.budget ?? DEFAULT_BUDGET;
116
- if (goal.used >= cap)
117
- return { action: "stop", reason: `budget exhausted (${cap} continuations)`, close: true };
118
- return { action: "continue" };
119
- }
120
- /** Apply one op. Pure, so the policy is testable without a session. */
121
- export function applyGoalOp(goal, op, text, budget, note) {
122
- switch (op) {
123
- case "view":
124
- return { goal };
125
- case "set": {
126
- if (!text?.trim())
127
- return { goal, error: "set needs text" };
128
- if (text.length > MAX_GOAL)
129
- return { goal, error: `text longer than ${MAX_GOAL} characters` };
130
- if (budget !== undefined && (!Number.isInteger(budget) || budget < 1)) {
131
- return { goal, error: "budget must be a positive integer" };
132
- }
133
- // A new goal resets the accounting: the cap belongs to the goal, not to
134
- // the session, so a fresh goal gets its full budget.
135
- return { goal: { text: text.trim(), status: "active", used: 0, ...(budget === undefined ? {} : { budget }) } };
136
- }
137
- case "pause":
138
- case "resume":
139
- case "complete":
140
- case "clear": {
141
- if (!goal || goal.status === "cleared")
142
- return { goal, error: "no goal to change" };
143
- if (op === "resume") {
144
- if (goal.status === "done")
145
- return { goal, error: "goal is complete; set a new one instead" };
146
- return { goal: { ...goal, status: "active", note: undefined } };
147
- }
148
- if (op === "pause")
149
- return { goal: { ...goal, status: "paused" } };
150
- if (op === "clear")
151
- return { goal: { ...goal, status: "cleared" } };
152
- // `complete` requires evidence. The model may only declare victory with a
153
- // note naming what it verified; a bare "done" is what lets a wrong answer
154
- // ship as a finished task.
155
- if (!note?.trim())
156
- return { goal, error: "complete needs a note naming the evidence (a command you ran or a file you checked)" };
157
- return { goal: { ...goal, status: "done", note: note.trim() } };
158
- }
159
- default:
160
- return { goal, error: `unknown op ${JSON.stringify(op)}` };
161
- }
162
- }
163
- /**
164
- * Rebuild the goal by replaying this branch in order.
165
- *
166
- * Two sources, because two things write it: the `goal` tool (whose result
167
- * `details` are persisted in the transcript, exactly like `todo.ts`) and the
168
- * continuation extension, which appends a `custom` entry to record a spend
169
- * before it continues. Whichever is later in the branch wins, so a resume sees
170
- * the true state and the accounting cannot be reset by a restart.
171
- */
172
- export function readGoal(ctx) {
173
- let goal;
174
- for (const entry of ctx.sessionManager.getBranch()) {
175
- if (entry.type === "custom" && entry.customType === GOAL_ENTRY) {
176
- goal = entry.data;
177
- continue;
178
- }
179
- // The slash command persists through `sendCustomMessage`, which writes a
180
- // `custom_message` carrying the goal in `details` rather than `data`.
181
- if (entry.type === "custom_message" && entry.customType === GOAL_ENTRY) {
182
- goal = entry.details ?? goal;
183
- continue;
184
- }
185
- if (entry.type !== "message")
186
- continue;
187
- const message = entry.message;
188
- if (message.role !== "toolResult" || message.toolName !== "goal")
189
- continue;
190
- const details = message.details;
191
- if (details?.goal)
192
- goal = details.goal;
193
- }
194
- return goal;
195
- }
196
- export const goalParams = Type.Object({
197
- op: StringEnum(["set", "view", "pause", "resume", "complete", "clear"], {
198
- description: "set starts a goal; complete closes it with evidence; pause/resume/clear manage it; view reads it",
199
- }),
200
- text: Type.Optional(Type.String({ description: "The goal, in one sentence, as the user would state it (for set)" })),
201
- budget: Type.Optional(Type.Number({ description: "Optional cap on automatic continuations before the run stops" })),
202
- note: Type.Optional(Type.String({ description: "Evidence that the goal is met (required for complete)" })),
203
- });
204
- export const goalTool = defineTool({
205
- name: "goal",
206
- label: "goal",
207
- description: "Track the goal for this session. The goal is already shown in your context every request, so you rarely need `view`. Call `complete` only when the goal is genuinely met, and pass a note naming the evidence. When a goal is active the harness continues the run if you stop without closing it, so closing it is how you stop.",
208
- promptSnippet: "goal: the session's objective, and how to close it",
209
- parameters: goalParams,
210
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
211
- const before = readGoal(ctx);
212
- const { goal, error } = applyGoalOp(before, params.op, params.text, params.budget, params.note);
213
- setMirror(goal);
214
- const body = formatGoal(goal);
215
- const summary = error ? `Error: ${error}\n${body}` : body;
216
- return {
217
- content: [{ type: "text", text: summary }],
218
- details: { op: params.op, goal },
219
- ...(error ? { isError: true } : {}),
220
- };
221
- },
222
- });
@@ -1,176 +0,0 @@
1
- import { readMessageText, writeMessageText } from "./prompt-text.js";
2
- import { HOOKS_FILE, checkToolInput, loadHooks, resetOnceHooks, runHooksFor } from "./hooks.js";
3
- /** Read once per session; see the note above. */
4
- let config = { hooks: {} };
5
- /** Context a `SessionStart` hook contributed, kept for the session's prompts. */
6
- let sessionContext;
7
- export function createHooksExtension() {
8
- return (hxf) => {
9
- // Read the config HERE, in the factory, not in `session_start`.
10
- //
11
- // Measured: `session_start` does not fire in `-p` print mode — one-shot runs
12
- // are exactly where a `PreToolUse` hook matters most (an unattended agent is
13
- // the one you want policed), and loading in the lifecycle event left `config`
14
- // empty there, so every hook silently did nothing. The factory runs in every
15
- // mode. The read is one small synchronous file; it happens once per process.
16
- const loaded = loadHooks(HOOKS_FILE);
17
- config = loaded.config;
18
- sessionContext = undefined;
19
- // A resumed session re-arms its `once` hooks: the ledger is per process, and
20
- // the session that just opened is a new one from the hook's point of view.
21
- resetOnceHooks();
22
- hxf.on("session_start", async (_event, ctx) => {
23
- for (const diagnostic of loaded.diagnostics) {
24
- ctx.ui.notify(`hooks.json: ${diagnostic.message}`, "warning");
25
- }
26
- const { outcome, ran } = await runHooksFor(config, "SessionStart", { hook_event_name: "SessionStart", cwd: ctx.cwd }, { cwd: ctx.cwd });
27
- if (outcome.context)
28
- sessionContext = outcome.context;
29
- if (outcome.error)
30
- ctx.ui.notify(`SessionStart hook: ${outcome.error}`, "warning");
31
- if (ran && sessionContext) {
32
- ctx.ui.notify(`SessionStart hooks added ${sessionContext.length} characters of context`, "info");
33
- }
34
- });
35
- // `UserPromptSubmit` may block a prompt or add to it. The SDK's `input` event
36
- // is the only place a prompt can be replaced before the run starts.
37
- hxf.on("input", async (event, ctx) => {
38
- if (!config.hooks.UserPromptSubmit)
39
- return undefined;
40
- const payload = {
41
- hook_event_name: "UserPromptSubmit",
42
- cwd: ctx.cwd,
43
- prompt: event.text,
44
- };
45
- const { outcome } = await runHooksFor(config, "UserPromptSubmit", payload, { cwd: ctx.cwd });
46
- if (outcome.error)
47
- ctx.ui.notify(`UserPromptSubmit hook: ${outcome.error}`, "warning");
48
- if (outcome.block) {
49
- ctx.ui.notify(outcome.reason ?? "blocked by a hook", "warning");
50
- return { action: "handled" };
51
- }
52
- if (!outcome.context)
53
- return undefined;
54
- return { action: "transform", text: `${event.text}\n\n${outcome.context}` };
55
- });
56
- // `PreToolUse` is the one that can refuse — or rewrite. It runs before the
57
- // approval gate because a hook is the user's own policy and must not be
58
- // overridable by an approval mode.
59
- hxf.on("tool_call", async (event, ctx) => {
60
- if (!config.hooks.PreToolUse)
61
- return undefined;
62
- const payload = {
63
- hook_event_name: "PreToolUse",
64
- cwd: ctx.cwd,
65
- tool_name: event.toolName,
66
- tool_input: event.input,
67
- };
68
- const { outcome } = await runHooksFor(config, "PreToolUse", payload, { cwd: ctx.cwd });
69
- if (outcome.error)
70
- ctx.ui.notify(`PreToolUse hook: ${outcome.error}`, "warning");
71
- if (outcome.block) {
72
- return { block: true, reason: outcome.reason ?? "blocked by a hook" };
73
- }
74
- // A rewrite replaces what the tool will actually do, so it is validated
75
- // against the tool's own schema before it is used — the same schema the
76
- // provider was given, read from the SDK's own registry. A rewrite that
77
- // fails is dropped and the original arguments run: a guard must not be able
78
- // to turn itself into a way to run something the model never asked for.
79
- if (outcome.updatedInput) {
80
- const tools = hxf.getAllTools();
81
- const schema = tools.find((tool) => tool.name === event.toolName)?.parameters;
82
- if (!schema) {
83
- ctx.ui.notify(`PreToolUse hook rewrote \`${event.toolName}\`, but no schema is known for that tool — the rewrite was ignored`, "warning");
84
- }
85
- else {
86
- const problem = checkToolInput(schema, outcome.updatedInput);
87
- if (problem) {
88
- ctx.ui.notify(`PreToolUse hook rewrite ignored — ${problem}`, "warning");
89
- }
90
- else {
91
- // The SDK's documented way to change arguments: mutate in place.
92
- Object.assign(event.input, outcome.updatedInput);
93
- }
94
- }
95
- }
96
- return undefined;
97
- });
98
- // `PostToolUse` adds context to what the tool returned. The event is
99
- // `tool_result`, and the result's `content` is replaced with the hook's text
100
- // appended — `structuredContent` is returned alongside it so it is not
101
- // dropped (the SDK drops it otherwise).
102
- hxf.on("tool_result", async (event, ctx) => {
103
- if (!config.hooks.PostToolUse)
104
- return undefined;
105
- const text = event.content.map((block) => (block.type === "text" ? block.text : "")).join("\n");
106
- const payload = {
107
- hook_event_name: "PostToolUse",
108
- cwd: ctx.cwd,
109
- tool_name: event.toolName,
110
- tool_input: event.input,
111
- tool_result: text,
112
- tool_is_error: event.isError,
113
- };
114
- const { outcome } = await runHooksFor(config, "PostToolUse", payload, { cwd: ctx.cwd });
115
- if (outcome.error)
116
- ctx.ui.notify(`PostToolUse hook: ${outcome.error}`, "warning");
117
- if (!outcome.context)
118
- return undefined;
119
- return {
120
- content: [...event.content, { type: "text", text: `\n[hook] ${outcome.context}` }],
121
- structuredContent: event.structuredContent,
122
- };
123
- });
124
- // `SessionStart` context reaches the model through the same path the shared
125
- // notes use: appended to the leading system message, which must stay at index
126
- // 0 because that is where the provider reads the prompt from.
127
- hxf.on("context_with_system", async (event) => {
128
- if (!sessionContext)
129
- return undefined;
130
- const head = event.messages[0];
131
- if (!head || head.role !== "system")
132
- return undefined;
133
- const combined = `${readMessageText(head.content)}\n\n${sessionContext}`;
134
- const messages = event.messages.map((message) => (message === head ? { ...head, content: writeMessageText(head.content, combined) } : message));
135
- return { messages };
136
- });
137
- // `PreCompact` observes a compaction before it happens — the moment context
138
- // is about to be thrown away. Notification only: the SDK's compaction result
139
- // is not something a shell command should be able to veto, and a hook that
140
- // could would be a way to wedge a session at its most fragile point.
141
- hxf.on("session_before_compact", async (event, ctx) => {
142
- if (!config.hooks.PreCompact)
143
- return;
144
- const { outcome } = await runHooksFor(config, "PreCompact", {
145
- hook_event_name: "PreCompact",
146
- cwd: ctx.cwd,
147
- compact_reason: event.reason,
148
- }, { cwd: ctx.cwd });
149
- if (outcome.error)
150
- ctx.ui.notify(`PreCompact hook: ${outcome.error}`, "warning");
151
- });
152
- // `SessionEnd` for a session that is **replaced** (`/new`, `/resume`, a fork).
153
- //
154
- // Measured: this event does NOT fire when the process exits — `/quit` and
155
- // double Ctrl+C produced zero events, even for a TypeScript extension writing
156
- // synchronously. The quit path is therefore handled in `ui.ts`'s `shutdown()`,
157
- // which runs the same hooks and awaits them. This handler covers the other
158
- // moment the session ends: being swapped for another one.
159
- hxf.on("session_shutdown", async (event, ctx) => {
160
- if (!config.hooks.SessionEnd)
161
- return;
162
- const { outcome } = await runHooksFor(config, "SessionEnd", { hook_event_name: "SessionEnd", cwd: ctx.cwd, reason: event.reason }, { cwd: ctx.cwd });
163
- if (outcome.error)
164
- ctx.ui.notify(`SessionEnd hook: ${outcome.error}`, "warning");
165
- });
166
- // `Stop` is notification-only: it fires when the run will not continue on its
167
- // own, which is the moment an integration wants to know about.
168
- hxf.on("agent_settled", async (_event, ctx) => {
169
- if (!config.hooks.Stop)
170
- return;
171
- const { outcome } = await runHooksFor(config, "Stop", { hook_event_name: "Stop", cwd: ctx.cwd }, { cwd: ctx.cwd });
172
- if (outcome.error)
173
- ctx.ui.notify(`Stop hook: ${outcome.error}`, "warning");
174
- });
175
- };
176
- }