howtofixcli 0.7.13 → 0.7.15

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 (161) 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-6FUWMLWO.js +1 -0
  5. package/dist/chunk-7CHDNTH6.js +1 -0
  6. package/dist/chunk-7DTQKDO4.js +2 -0
  7. package/dist/chunk-AGGIYXN7.js +2 -0
  8. package/dist/chunk-AUDMRGWF.js +189 -0
  9. package/dist/chunk-CPJKKHGH.js +1 -0
  10. package/dist/chunk-DAPXGSGD.js +12 -0
  11. package/dist/chunk-DOD7MUGX.js +1 -0
  12. package/dist/chunk-DV2BBBBJ.js +2 -0
  13. package/dist/chunk-EIBOS5KO.js +5 -0
  14. package/dist/chunk-EYWOVCSS.js +6 -0
  15. package/dist/chunk-FPXVDP32.js +8 -0
  16. package/dist/chunk-GXB7RZHH.js +1 -0
  17. package/dist/chunk-HT3MFLAV.js +34 -0
  18. package/dist/chunk-ILDDDLRC.js +1 -0
  19. package/dist/chunk-JFYVRWJI.js +2 -0
  20. package/dist/chunk-JMKK26AT.js +2 -0
  21. package/dist/chunk-LAHVHRCS.js +2 -0
  22. package/dist/chunk-LOOFOBTS.js +1 -0
  23. package/dist/chunk-LSJC73DG.js +1 -0
  24. package/dist/chunk-MOKUBKE7.js +1 -0
  25. package/dist/chunk-N345KWWK.js +1 -0
  26. package/dist/chunk-ONZLNFQ2.js +3 -0
  27. package/dist/chunk-ORJ2QZBG.js +3 -0
  28. package/dist/chunk-PCALBZOO.js +2 -0
  29. package/dist/chunk-SX5VE6XA.js +110 -0
  30. package/dist/chunk-TSEKPGK4.js +1 -0
  31. package/dist/chunk-UNK5HRQR.js +1 -0
  32. package/dist/chunk-UTYFLLSM.js +1 -0
  33. package/dist/chunk-V2RPZMLK.js +1 -0
  34. package/dist/chunk-V3R4FYTJ.js +1 -0
  35. package/dist/chunk-V656XVXV.js +8 -0
  36. package/dist/chunk-VLMQSD25.js +1 -0
  37. package/dist/chunk-WKUJ7CFO.js +1 -0
  38. package/dist/chunk-WOT6VMZA.js +1 -0
  39. package/dist/chunk-WUPU6LTD.js +4 -0
  40. package/dist/chunk-WX3V5P7U.js +5 -0
  41. package/dist/chunk-Y7YM5R75.js +5 -0
  42. package/dist/chunk-YW7NORH3.js +2 -0
  43. package/dist/main.js +99 -540
  44. package/package.json +17 -64
  45. package/dist/account-credential.js +0 -136
  46. package/dist/account-credential.test.js +0 -38
  47. package/dist/activity.js +0 -124
  48. package/dist/agent-defs.js +0 -237
  49. package/dist/agent-limiter.js +0 -68
  50. package/dist/agents.js +0 -554
  51. package/dist/anim.js +0 -361
  52. package/dist/args.js +0 -228
  53. package/dist/at-file.js +0 -28
  54. package/dist/brand-extension.js +0 -87
  55. package/dist/brand.js +0 -76
  56. package/dist/browser.js +0 -58
  57. package/dist/cache-report.js +0 -139
  58. package/dist/checkpoint.js +0 -437
  59. package/dist/claims.js +0 -167
  60. package/dist/cli-auth.js +0 -142
  61. package/dist/cli-checkin.js +0 -120
  62. package/dist/cli-credentials.js +0 -254
  63. package/dist/clipboard.js +0 -149
  64. package/dist/collapse.js +0 -129
  65. package/dist/commands.js +0 -294
  66. package/dist/compaction-guard.js +0 -81
  67. package/dist/context-extension.js +0 -17
  68. package/dist/dcp/commands.js +0 -172
  69. package/dist/dcp/compress-state.js +0 -139
  70. package/dist/dcp/compress.js +0 -329
  71. package/dist/dcp/dcp-extension.js +0 -295
  72. package/dist/dcp/dedup.js +0 -93
  73. package/dist/dcp/ids.js +0 -189
  74. package/dist/dcp/index.js +0 -25
  75. package/dist/dcp/middle-prune.js +0 -76
  76. package/dist/dcp/prompts.js +0 -68
  77. package/dist/dcp/protected-content.js +0 -221
  78. package/dist/dcp/protected.js +0 -161
  79. package/dist/dcp/prune.js +0 -418
  80. package/dist/dcp/state.js +0 -176
  81. package/dist/device-id.js +0 -90
  82. package/dist/device-id.test.js +0 -23
  83. package/dist/errors.js +0 -63
  84. package/dist/extensions.js +0 -48
  85. package/dist/fatal.js +0 -35
  86. package/dist/feed.js +0 -498
  87. package/dist/fs-atomic.js +0 -54
  88. package/dist/gateway-errors.js +0 -90
  89. package/dist/git.js +0 -230
  90. package/dist/goal-extension.js +0 -88
  91. package/dist/goal.js +0 -222
  92. package/dist/hooks-extension.js +0 -176
  93. package/dist/hooks.js +0 -443
  94. package/dist/howtofix-account.js +0 -735
  95. package/dist/hud-line.js +0 -153
  96. package/dist/hwid.js +0 -130
  97. package/dist/interrupt.js +0 -60
  98. package/dist/json-events.js +0 -50
  99. package/dist/level-style.js +0 -112
  100. package/dist/loop-guard.js +0 -95
  101. package/dist/lsp-tools.js +0 -32
  102. package/dist/lsp.js +0 -429
  103. package/dist/mcp-config.js +0 -85
  104. package/dist/mcp-lifecycle.js +0 -147
  105. package/dist/model-catalog.js +0 -217
  106. package/dist/model-facts.js +0 -35
  107. package/dist/model-resolve.js +0 -65
  108. package/dist/notes.js +0 -78
  109. package/dist/oauth-login.js +0 -568
  110. package/dist/paths.js +0 -61
  111. package/dist/permissions.js +0 -298
  112. package/dist/plan-mode.js +0 -363
  113. package/dist/preset-command.js +0 -227
  114. package/dist/preset-extension.js +0 -35
  115. package/dist/preset-mcp.js +0 -292
  116. package/dist/presets.js +0 -727
  117. package/dist/prompt-files.js +0 -103
  118. package/dist/prompt-text.js +0 -41
  119. package/dist/proxy.js +0 -137
  120. package/dist/quality/ast.js +0 -196
  121. package/dist/quality/cognitive.js +0 -83
  122. package/dist/quality/cyclomatic.js +0 -30
  123. package/dist/quality/halstead.js +0 -83
  124. package/dist/quality/index.js +0 -18
  125. package/dist/quality/length.js +0 -3
  126. package/dist/quality/maintainability.js +0 -15
  127. package/dist/quality/nesting.js +0 -57
  128. package/dist/quality/report.js +0 -803
  129. package/dist/quality/smells.js +0 -67
  130. package/dist/quality-tool.js +0 -63
  131. package/dist/read-only-commands.js +0 -597
  132. package/dist/repo-map.js +0 -292
  133. package/dist/rtk.js +0 -193
  134. package/dist/sandbox.js +0 -105
  135. package/dist/schema.js +0 -214
  136. package/dist/session-lifecycle.js +0 -19
  137. package/dist/session-retention.js +0 -111
  138. package/dist/session.js +0 -447
  139. package/dist/sessions.js +0 -100
  140. package/dist/settings.js +0 -148
  141. package/dist/skills.js +0 -35
  142. package/dist/slop-tool.js +0 -96
  143. package/dist/spend-cap.js +0 -43
  144. package/dist/subagent.js +0 -171
  145. package/dist/tar.js +0 -146
  146. package/dist/theme.js +0 -79
  147. package/dist/thinking.js +0 -45
  148. package/dist/todo.js +0 -114
  149. package/dist/tool-loadout.js +0 -97
  150. package/dist/tool-render.js +0 -769
  151. package/dist/tools.js +0 -176
  152. package/dist/ui.js +0 -4778
  153. package/dist/update.js +0 -551
  154. package/dist/usage-segment.js +0 -69
  155. package/dist/web-tools.js +0 -46
  156. package/dist/web.js +0 -265
  157. package/dist/welcome.js +0 -179
  158. package/dist/workflow-engine.js +0 -246
  159. package/dist/workflow-progress.js +0 -446
  160. package/dist/workflow-view.js +0 -195
  161. package/dist/workflow.js +0 -349
package/dist/fs-atomic.js DELETED
@@ -1,54 +0,0 @@
1
- import { closeSync, fsyncSync, openSync, renameSync, rmSync, writeSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- /**
4
- * Write a file so that a reader never sees a half-written one.
5
- *
6
- * A plain `writeFileSync` truncates the target first. If the process dies
7
- * mid-write — a kill, a power loss, an antivirus holding the file open — the
8
- * user is left with a truncated `models.json` or cache, and the next start
9
- * either loses their config or throws while parsing it. Writing to a sibling
10
- * temp file and renaming over the target makes the swap atomic on every
11
- * platform: readers see the old file or the new one, never a fragment.
12
- *
13
- * On Windows a rename onto an open file fails with `EPERM`/`EBUSY` (the file is
14
- * locked by a scanner or an editor), so the target is removed and the rename
15
- * retried once. If that fails too the temp file is cleaned up and the error is
16
- * rethrown — losing the write is bad, but leaving a stray temp file next to the
17
- * user's config is worse.
18
- *
19
- * `options.mode` is applied to the temp file, and the rename makes that mode
20
- * the target's: a credential file written with `0o600` stays private even when
21
- * the previous file on disk was world-readable. The mode only binds the temp
22
- * file's creation, which always happens on a fresh path — unlike a plain
23
- * `writeFileSync` onto an existing file, where mode is silently ignored.
24
- */
25
- export function writeFileAtomicSync(path, data, options) {
26
- const temp = join(dirname(path), `.${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.tmp`);
27
- const fd = openSync(temp, "w", options?.mode);
28
- try {
29
- writeSync(fd, data);
30
- // Flush before the rename, so the rename cannot land ahead of the bytes.
31
- fsyncSync(fd);
32
- }
33
- finally {
34
- closeSync(fd);
35
- }
36
- try {
37
- renameSync(temp, path);
38
- }
39
- catch (cause) {
40
- const code = cause.code;
41
- if (code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") {
42
- rmSync(temp, { force: true });
43
- throw cause;
44
- }
45
- rmSync(path, { force: true });
46
- try {
47
- renameSync(temp, path);
48
- }
49
- catch (retryCause) {
50
- rmSync(temp, { force: true });
51
- throw retryCause;
52
- }
53
- }
54
- }
@@ -1,90 +0,0 @@
1
- /**
2
- * What to do about the sentence the gateway just failed with.
3
- *
4
- * The gateway reports failures in its own words, and the README's
5
- * Troubleshooting section already tells a reader what each one means — but only
6
- * a reader who has the README open. In the TUI the same sentence arrives as a
7
- * feed row titled `failed` and nothing else: the user is left to remember that
8
- * `No available channel for model` means "the channel behind that id is down",
9
- * not "my token is broken". The other documented sentences have the same gap.
10
- *
11
- * So the mapping lives here, as a pure string -> string function: no network,
12
- * no session, no model runtime. The caller decides where the hint is shown
13
- * (under a feed row in the TUI, on stderr in `--print`), which keeps this file
14
- * free of UI so it can be exercised directly by a test.
15
- *
16
- * Every pattern is grounded in a sentence the gateway actually sends — the
17
- * README's Troubleshooting list, the `probeModel` measurement (a listed model
18
- * answering 500, another answering 401), and the `code 11128` refusal documented
19
- * on `repairGatewayModels`. A pattern that matched nothing real would be a hint
20
- * that lies, so nothing speculative is matched: unknown sentences return
21
- * `undefined` and the caller prints the failure untouched.
22
- */
23
- /**
24
- * The one sentence the gateway sends when the channel behind a model id is
25
- * down, read as the id it names.
26
- *
27
- * Shared with `diagnoseGatewayFailure` on purpose: the TUI does not only want
28
- * a hint for this failure, it wants the *id* so it can probe the other
29
- * candidates for a live one — and two copies of this pattern would drift, so
30
- * the caller that opens the model picker would silently stop matching.
31
- */
32
- const NO_LIVE_CHANNEL = /no available channel for model\s+([^\s"',)\]]+)/i;
33
- /** The model the gateway has no live channel for, or `undefined` for any other sentence. */
34
- export function deadChannelModel(message) {
35
- const text = message ?? "";
36
- if (!text.trim())
37
- return undefined;
38
- return NO_LIVE_CHANNEL.exec(text)?.[1];
39
- }
40
- /**
41
- * The fix for a known gateway sentence, or `undefined` for anything else.
42
- *
43
- * Case-insensitive on purpose: the same sentence arrives from the gateway, from
44
- * an SDK wrapper (`Upstream API error (HTTP 400)`) and from a probe, with
45
- * punctuation that differs. The first pattern that matches wins, ordered from
46
- * the most specific sentence to the most generic, so `HTTP 429` inside a rate
47
- * limit is read as rate limiting rather than as an upstream error.
48
- */
49
- export function diagnoseGatewayFailure(message) {
50
- const text = message ?? "";
51
- if (!text.trim())
52
- return undefined;
53
- // The gateway has no live channel behind that id. Not an account problem:
54
- // `/model` lists what answers *now* (README, Troubleshooting). The id is
55
- // read by `deadChannelModel` too, which is what lets the TUI go looking for
56
- // a channel that *is* alive instead of only saying so.
57
- const deadModel = deadChannelModel(text);
58
- if (deadModel) {
59
- return `the gateway has no live channel for ${deadModel} right now — /model lists the models that answer; pick another.`;
60
- }
61
- // `unknown model` means the id is not in models.json at all, which /connect
62
- // fixes by pulling the current list (README, Troubleshooting).
63
- if (/\bunknown model\b/i.test(text)) {
64
- return "/connect pulls the current model list from the gateway.";
65
- }
66
- // The gateway refused a role the request was sent with (`code 11128`,
67
- // "Illegal API invocation") — the compatibility flag on the model entry is
68
- // what decides which roles are sent, and /connect re-reads it.
69
- if (/\bcode 11128\b|\billegal api invocation\b/i.test(text)) {
70
- return "the gateway refused the request shape (code 11128) — /connect re-syncs each model's compatibility flags.";
71
- }
72
- // 402/insufficient balance: the token is fine, the credit is not.
73
- if (/\binsufficient (?:balance|quota|credit)\b|\bexceeded your current quota\b|http\s*402/i.test(text)) {
74
- return "the account is out of credit — /usage shows what is left.";
75
- }
76
- // The gateway's channel failing, not the CLI (README, Troubleshooting).
77
- if (/\bserver error\b|http\s*5\d\d/i.test(text)) {
78
- return "the gateway's channel is failing, not the CLI — /model lists what your account can reach; pick another.";
79
- }
80
- // Rate limiting: waiting is the fix, and the run can simply be resent.
81
- if (/\brate.?limit|\b429\b/i.test(text)) {
82
- return "the gateway is rate-limiting this token — wait a moment, then resend the prompt.";
83
- }
84
- // `Upstream API error (HTTP nnn)` — the gateway refused the request; a model
85
- // entry written before a gateway change carries stale flags (README).
86
- if (/\bupstream api error\b/i.test(text)) {
87
- return "the gateway refused the request — run /connect again to re-sync your models.";
88
- }
89
- return undefined;
90
- }
package/dist/git.js DELETED
@@ -1,230 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { cp, readdir, stat } from "node:fs/promises";
3
- import { promisify } from "node:util";
4
- import { tmpdir } from "node:os";
5
- import { join } from "node:path";
6
- const execFileAsync = promisify(execFile);
7
- /**
8
- * Run git in the project directory. Never throws; failures come back as text.
9
- *
10
- * Async on purpose: every caller is a slash command running inside the workbench,
11
- * and a synchronous spawn blocks the event loop — on Windows a process spawn costs
12
- * far more than on Linux, so `/commit`, `/undo` and `/review` froze the screen for
13
- * the duration. Measured on Linux: 6.5 ms for a `status` spawn alone.
14
- */
15
- export async function git(cwd, args) {
16
- try {
17
- const { stdout } = await execFileAsync("git", args, {
18
- cwd,
19
- encoding: "utf8",
20
- maxBuffer: 8 * 1024 * 1024,
21
- });
22
- return { ok: true, output: stdout.trim() };
23
- }
24
- catch (error) {
25
- const stderr = error && typeof error === "object" && "stderr" in error && typeof error.stderr === "string" ? error.stderr.trim() : "";
26
- const message = error instanceof Error ? error.message : String(error);
27
- return { ok: false, output: stderr || message };
28
- }
29
- }
30
- export async function isGitRepo(cwd) {
31
- return (await git(cwd, ["rev-parse", "--is-inside-work-tree"])).output === "true";
32
- }
33
- export async function hasChanges(cwd) {
34
- return (await git(cwd, ["status", "--porcelain"])).output.length > 0;
35
- }
36
- /** Stage everything, commit, and return the one-line summary. */
37
- export async function commitAll(cwd, message) {
38
- const add = await git(cwd, ["add", "-A"]);
39
- if (!add.ok)
40
- return add;
41
- const commit = await git(cwd, ["commit", "-m", message]);
42
- if (!commit.ok)
43
- return commit;
44
- const head = await git(cwd, ["log", "-1", "--oneline"]);
45
- return { ok: true, output: head.output || commit.output };
46
- }
47
- /** Drop the last commit, keeping its changes staged. The inverse of `/commit`. */
48
- export async function undoLastCommit(cwd) {
49
- const reset = await git(cwd, ["reset", "--soft", "HEAD~1"]);
50
- if (!reset.ok)
51
- return reset;
52
- const head = await git(cwd, ["log", "-1", "--oneline"]);
53
- return { ok: true, output: head.output || "reset to the previous commit" };
54
- }
55
- export async function diffStat(cwd) {
56
- return (await git(cwd, ["diff", "--stat"])).output;
57
- }
58
- export async function fullDiff(cwd) {
59
- const unstaged = (await git(cwd, ["diff"])).output;
60
- const staged = (await git(cwd, ["diff", "--cached"])).output;
61
- return [unstaged, staged].filter(Boolean).join("\n");
62
- }
63
- /**
64
- * Cap a diff for display.
65
- *
66
- * A transcript is for reading, and a full patch of a large change pushes
67
- * everything else off the screen — the note holding it can be hundreds of lines.
68
- * The file summary is kept whole (it is what you scan), and the body is cut with
69
- * a line saying how much is hidden, so nothing is silently dropped.
70
- */
71
- export function clipDiff(diff, maxLines = 60) {
72
- const lines = diff.split("\n");
73
- if (lines.length <= maxLines)
74
- return diff;
75
- const head = lines.slice(0, maxLines);
76
- const hidden = lines.length - maxLines;
77
- return [...head, `… ${hidden} more line${hidden === 1 ? "" : "s"} not shown — /copy puts the full diff on the clipboard`].join("\n");
78
- }
79
- /**
80
- * Directories that are (a) expensive to rebuild and (b) always git-ignored.
81
- *
82
- * Deliberately a fixed list rather than "everything ignored". `git status --ignored`
83
- * also reports `.env`, `*.pem`, editor state and local databases — copying those into a
84
- * tree that a parallel agent shares widens the blast radius of a mistake for no gain.
85
- * A worktree needs what it takes to BUILD; it does not need someone's secrets.
86
- */
87
- const SHARED_IGNORED = ["node_modules", "vendor", "target", ".venv", "venv", "__pycache__", ".pnpm-store", "dist", "build"];
88
- /**
89
- * The weight past which copying is not attempted.
90
- *
91
- * This runs on the critical path of spawning an agent, so it has a budget. A reflink is
92
- * nearly free; a real copy of 200 MB is not, and a worktree that takes four seconds to
93
- * appear is one the user will turn off. Past the cap the directory is skipped and named
94
- * in the result, so the caller can say why rather than leaving the agent to discover it.
95
- */
96
- const SHARE_BUDGET_BYTES = 1024 * 1024 * 1024;
97
- async function directorySize(path) {
98
- let total = 0;
99
- let entries;
100
- try {
101
- entries = await readdir(path, { withFileTypes: true });
102
- }
103
- catch {
104
- return 0;
105
- }
106
- for (const entry of entries) {
107
- const child = join(path, entry.name);
108
- if (entry.isDirectory())
109
- total += await directorySize(child);
110
- else if (entry.isFile()) {
111
- try {
112
- total += (await stat(child)).size;
113
- }
114
- catch {
115
- // A file that vanished mid-walk costs nothing to skip.
116
- }
117
- }
118
- // Stop early once the answer is already over budget: the exact figure is not used.
119
- if (total > SHARE_BUDGET_BYTES)
120
- return total;
121
- }
122
- return total;
123
- }
124
- /**
125
- * Give the worktree the ignored directories it needs to build.
126
- *
127
- * Measured on this host for a 15,585-file / 163 MB `node_modules`:
128
- * `git worktree add` 37 ms, a size walk 383 ms, and then the copy. Node's own
129
- * recursive `fs.cp` took **13.4 s** — per-file async overhead dominates at this file
130
- * count — while spawning `cp -a` took about **1 s**. So the copy is done by `cp` where
131
- * it exists, and `fs.cp` is kept only as the fallback for Windows and for a `cp` that
132
- * fails. Thirteen seconds is not a slower agent, it is a spawn that looks hung.
133
- *
134
- * Where the filesystem supports reflinks `cp -a` uses them; where it does not, the copy
135
- * is real and the budget below is what bounds it. Returns the directories it could not
136
- * share, because silence here would surface later as an agent claiming a pass it never
137
- * ran.
138
- */
139
- export async function shareIgnoredDirectories(from, to) {
140
- const skipped = [];
141
- for (const name of SHARED_IGNORED) {
142
- const source = join(from, name);
143
- const target = join(to, name);
144
- try {
145
- if (!(await stat(source)).isDirectory())
146
- continue;
147
- }
148
- catch {
149
- continue; // Not present: the common case, and not a problem.
150
- }
151
- if ((await directorySize(source)) > SHARE_BUDGET_BYTES) {
152
- skipped.push(name);
153
- continue;
154
- }
155
- if (await copyDirectory(source, target))
156
- continue;
157
- skipped.push(name);
158
- }
159
- return skipped;
160
- }
161
- /**
162
- * Copy one directory tree, preferring the platform's own `cp`.
163
- *
164
- * `-a` keeps symlinks as symlinks and preserves mode and timestamps, which matters for
165
- * an executable in `node_modules/.bin` and for a build cache that decides what to
166
- * rebuild by timestamp.
167
- */
168
- async function copyDirectory(source, target) {
169
- if (process.platform !== "win32") {
170
- try {
171
- await execFileAsync("cp", ["-a", source, target], { maxBuffer: 8 * 1024 * 1024 });
172
- return true;
173
- }
174
- catch {
175
- // Fall through to the portable path: a `cp` that is missing or refused is not a
176
- // reason to leave the agent without the tree it needs.
177
- }
178
- }
179
- try {
180
- await cp(source, target, { recursive: true, force: true, errorOnExist: false, dereference: false });
181
- return true;
182
- }
183
- catch {
184
- return false;
185
- }
186
- }
187
- /**
188
- * Give one agent call its own checkout, for `agent({ isolation: "worktree" })`.
189
- *
190
- * This is what makes parallel writers safe: two agents told to fix different
191
- * files in the same tree still race each other's `git add -A`, each other's
192
- * builds, and any tool that walks the project. A detached worktree at HEAD gives
193
- * each call a private tree, so a sibling's half-finished edit cannot break it.
194
- *
195
- * Returns `null` when the project is not a git repository (or the worktree could
196
- * not be made), because isolation is an optimisation for correctness, not a
197
- * reason to fail a run: the caller falls back to the shared tree.
198
- *
199
- * The worktree is placed under the system temp dir rather than inside the
200
- * project, so it never shows up in the project's own `git status` and is not
201
- * walked by the agents themselves.
202
- *
203
- * **The ignored directories are the point of this function.** A worktree is a
204
- * checkout of tracked files only, so it arrives without `node_modules/`,
205
- * `target/`, `.venv/` — every artifact that is large precisely because it is not
206
- * in git. An agent told to fix a bug and verify the fix therefore cannot run the
207
- * build or the tests at all: it reports success it never checked, or gives up and
208
- * says the tree is broken. Isolation that removes the ability to verify is not
209
- * isolation, it is a broken checkout. We copy them in.
210
- */
211
- export async function createAgentWorktree(cwd, label) {
212
- if (!(await isGitRepo(cwd)))
213
- return null;
214
- const safe = label.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 40) || "agent";
215
- const path = join(tmpdir(), `howtofix-wt-${process.pid}-${Date.now()}-${safe}`);
216
- const created = await git(cwd, ["worktree", "add", "--detach", path, "HEAD"]);
217
- if (!created.ok)
218
- return null;
219
- const skipped = await shareIgnoredDirectories(cwd, path);
220
- return {
221
- path,
222
- skipped,
223
- async remove() {
224
- await git(cwd, ["worktree", "remove", "--force", path]);
225
- // A crash can leave the directory behind; the worktree registration is
226
- // what matters, so prune after removing.
227
- await git(cwd, ["worktree", "prune"]);
228
- },
229
- };
230
- }
@@ -1,88 +0,0 @@
1
- import { readMessageText, writeMessageText } from "./prompt-text.js";
2
- import { GOAL_CONTINUE, GOAL_ENTRY, continuationPrompt, readGoal, renderGoalBlock, setMirror, shouldContinue } from "./goal.js";
3
- /** Reseed the HUD mirror. Exported so the workbench can call it on /new and /resume. */
4
- export function seedGoalMirror(ctx) {
5
- setMirror(readGoal(ctx));
6
- }
7
- /** The note written when the continuation budget is what stopped the run. */
8
- function budgetNote(goal) {
9
- return `budget exhausted (${goal.budget ?? "default"} continuations)`;
10
- }
11
- export function createGoalExtension() {
12
- return (hxf) => {
13
- hxf.on("session_start", async (_event, ctx) => {
14
- seedGoalMirror(ctx);
15
- });
16
- hxf.on("context_with_system", async (event, ctx) => {
17
- const goal = readGoal(ctx);
18
- setMirror(goal);
19
- if (!goal || goal.status !== "active")
20
- return undefined;
21
- const head = event.messages[0];
22
- if (!head || head.role !== "system")
23
- return undefined;
24
- const combined = `${readMessageText(head.content)}\n\n${renderGoalBlock(goal)}`;
25
- const messages = event.messages.map((message) => (message === head ? { ...head, content: writeMessageText(head.content, combined) } : message));
26
- return { messages };
27
- });
28
- hxf.on("agent_before_settle", async (event, ctx) => {
29
- // After a session replacement the SDK still fires settle events for the
30
- // session that was replaced, and any read through that ctx is rejected as
31
- // stale. There is nothing to do for a session that is no longer on screen,
32
- // so a stale ctx ends the handler quietly instead of surfacing an error
33
- // the user can do nothing about.
34
- try {
35
- const goal = readGoal(ctx);
36
- setMirror(goal);
37
- if (shouldContinue(goal).action === "stop" || !goal)
38
- return undefined;
39
- // A run already continuing for another reason (a queued continuation, a
40
- // retry) must not be doubled up, and continuing is pointless when the
41
- // context cannot accept another request.
42
- if (event.continue || !event.context.canContinue)
43
- return undefined;
44
- // Persist the spend before continuing: if the process dies mid-run the
45
- // count is already on disk, so a resume cannot reset the budget.
46
- const spent = { ...goal, used: goal.used + 1 };
47
- hxf.appendEntry(GOAL_ENTRY, spent);
48
- setMirror(spent);
49
- if (shouldContinue(spent).action === "stop") {
50
- // This spend was the last one the budget allowed. Close it here as well
51
- // as on settle, so the state is right even if no settle event follows.
52
- const closed = { ...spent, status: "done", note: budgetNote(spent) };
53
- hxf.appendEntry(GOAL_ENTRY, closed);
54
- setMirror(closed);
55
- return undefined;
56
- }
57
- return {
58
- entries: [{ type: "custom_message", customType: GOAL_CONTINUE, content: continuationPrompt(spent), display: true }],
59
- continue: true,
60
- };
61
- }
62
- catch {
63
- return undefined;
64
- }
65
- });
66
- hxf.on("agent_settled", async (_event, ctx) => {
67
- // The run is over. If the budget was the reason, close the goal so the
68
- // next prompt does not silently resume a runaway. A ctx left over from a
69
- // replaced session throws on read; that session is gone, so there is
70
- // nothing to close.
71
- try {
72
- const goal = readGoal(ctx);
73
- const decision = shouldContinue(goal);
74
- if (goal && decision.action === "stop" && decision.close) {
75
- const closed = { ...goal, status: "done", note: budgetNote(goal) };
76
- hxf.appendEntry(GOAL_ENTRY, closed);
77
- setMirror(closed);
78
- }
79
- else {
80
- setMirror(goal);
81
- }
82
- }
83
- catch {
84
- return;
85
- }
86
- });
87
- };
88
- }