acuvo-code 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/ENTERPRISE.md +927 -0
  3. package/LICENSE +120 -0
  4. package/README.md +1245 -0
  5. package/ROADMAP.md +556 -0
  6. package/bin/acuvo-mcp.mjs +208 -0
  7. package/bin/acuvo.mjs +3108 -0
  8. package/lib/acceptance-consent.mjs +168 -0
  9. package/lib/acceptance.mjs +859 -0
  10. package/lib/account.mjs +226 -0
  11. package/lib/acuvo-dir.mjs +72 -0
  12. package/lib/acuvo-models.mjs +141 -0
  13. package/lib/apply-patch.mjs +570 -0
  14. package/lib/ask-user.mjs +173 -0
  15. package/lib/audit.mjs +530 -0
  16. package/lib/auto-lease.mjs +174 -0
  17. package/lib/background.mjs +842 -0
  18. package/lib/best-of.mjs +334 -0
  19. package/lib/board.mjs +232 -0
  20. package/lib/breaker.mjs +93 -0
  21. package/lib/budget.mjs +1375 -0
  22. package/lib/builtin-skills.mjs +135 -0
  23. package/lib/cache-floor.mjs +204 -0
  24. package/lib/chain.mjs +303 -0
  25. package/lib/changed-paths.mjs +84 -0
  26. package/lib/chat.mjs +434 -0
  27. package/lib/checkpoint.mjs +637 -0
  28. package/lib/child-lifetime.mjs +71 -0
  29. package/lib/cli-args.mjs +1255 -0
  30. package/lib/code-review.mjs +1382 -0
  31. package/lib/colour.mjs +82 -0
  32. package/lib/command.mjs +2847 -0
  33. package/lib/compact.mjs +1151 -0
  34. package/lib/completion.mjs +515 -0
  35. package/lib/creative-engines.mjs +779 -0
  36. package/lib/db-inspect.mjs +1624 -0
  37. package/lib/delete.mjs +111 -0
  38. package/lib/design-loop.mjs +570 -0
  39. package/lib/diff-preview.mjs +1044 -0
  40. package/lib/doctor.mjs +2139 -0
  41. package/lib/dropped.mjs +216 -0
  42. package/lib/edit-diagnostics.mjs +277 -0
  43. package/lib/edit.mjs +460 -0
  44. package/lib/env-file.mjs +250 -0
  45. package/lib/escalate.mjs +702 -0
  46. package/lib/evaluate.mjs +284 -0
  47. package/lib/fetch-text.mjs +952 -0
  48. package/lib/fleet-budget.mjs +256 -0
  49. package/lib/gh.mjs +1536 -0
  50. package/lib/git.mjs +1341 -0
  51. package/lib/github.mjs +261 -0
  52. package/lib/h2.mjs +194 -0
  53. package/lib/handoff.mjs +417 -0
  54. package/lib/hooks.mjs +626 -0
  55. package/lib/http-probe.mjs +907 -0
  56. package/lib/image-director.mjs +322 -0
  57. package/lib/image-edit.mjs +522 -0
  58. package/lib/imagegen.mjs +998 -0
  59. package/lib/interrupt.mjs +234 -0
  60. package/lib/learned.mjs +353 -0
  61. package/lib/lease-watch.mjs +115 -0
  62. package/lib/lease.mjs +868 -0
  63. package/lib/localize.mjs +834 -0
  64. package/lib/log-tail.mjs +1052 -0
  65. package/lib/login.mjs +157 -0
  66. package/lib/lsp.mjs +1613 -0
  67. package/lib/mcp-consent.mjs +377 -0
  68. package/lib/mcp-defaults.mjs +780 -0
  69. package/lib/mcp-server.mjs +1343 -0
  70. package/lib/mcp.mjs +1263 -0
  71. package/lib/media.mjs +1283 -0
  72. package/lib/memory-workspace.mjs +179 -0
  73. package/lib/model-json.mjs +132 -0
  74. package/lib/model-tier.mjs +171 -0
  75. package/lib/model.mjs +1445 -0
  76. package/lib/parallel.mjs +144 -0
  77. package/lib/perchance.mjs +210 -0
  78. package/lib/plan-coherence.mjs +1461 -0
  79. package/lib/plan-ledger.mjs +981 -0
  80. package/lib/plan.mjs +461 -0
  81. package/lib/policy.mjs +783 -0
  82. package/lib/prefix-order.mjs +38 -0
  83. package/lib/project-memory.mjs +127 -0
  84. package/lib/prompt.mjs +109 -0
  85. package/lib/python.mjs +862 -0
  86. package/lib/rcfile.mjs +853 -0
  87. package/lib/read-window.mjs +743 -0
  88. package/lib/refute-tools.mjs +34 -0
  89. package/lib/refute.mjs +806 -0
  90. package/lib/repl-driver.mjs +264 -0
  91. package/lib/repl.mjs +324 -0
  92. package/lib/replay.mjs +1218 -0
  93. package/lib/repo-map.mjs +1101 -0
  94. package/lib/report.mjs +419 -0
  95. package/lib/search-rank.mjs +99 -0
  96. package/lib/search.mjs +659 -0
  97. package/lib/secret-paths.mjs +54 -0
  98. package/lib/session.mjs +1017 -0
  99. package/lib/skills.mjs +703 -0
  100. package/lib/slash.mjs +356 -0
  101. package/lib/spawn-argv.mjs +1151 -0
  102. package/lib/spend.mjs +250 -0
  103. package/lib/steer.mjs +280 -0
  104. package/lib/stream.mjs +253 -0
  105. package/lib/stuck.mjs +712 -0
  106. package/lib/subagent.mjs +749 -0
  107. package/lib/terminal-graphics.mjs +171 -0
  108. package/lib/tool-prefix.mjs +226 -0
  109. package/lib/tool-shortlist.mjs +162 -0
  110. package/lib/tools.mjs +2333 -0
  111. package/lib/tsserver.mjs +423 -0
  112. package/lib/turn.mjs +5672 -0
  113. package/lib/untrusted-block.mjs +271 -0
  114. package/lib/verify-claim.mjs +299 -0
  115. package/lib/vision.mjs +330 -0
  116. package/lib/voice-task.mjs +561 -0
  117. package/lib/warm-provider.mjs +255 -0
  118. package/lib/websearch.mjs +401 -0
  119. package/lib/workspace.mjs +928 -0
  120. package/lib/write-approval.mjs +235 -0
  121. package/lib/write-many.mjs +162 -0
  122. package/package.json +62 -0
  123. package/scripts/bundle.mjs +768 -0
  124. package/scripts/cache-floor.mjs +176 -0
  125. package/scripts/machine.mjs +226 -0
  126. package/scripts/test.mjs +139 -0
  127. package/skills/accessibility.md +87 -0
  128. package/skills/acuvo-design-system.md +123 -0
  129. package/skills/animation.md +84 -0
  130. package/skills/api-design.md +82 -0
  131. package/skills/auth-and-sessions.md +78 -0
  132. package/skills/build-with-a-framework.md +101 -0
  133. package/skills/colour-and-contrast.md +112 -0
  134. package/skills/creative-engines.md +81 -0
  135. package/skills/css-layout.md +85 -0
  136. package/skills/data-and-charts.md +77 -0
  137. package/skills/debugging.md +76 -0
  138. package/skills/designing-by-looking.md +84 -0
  139. package/skills/error-handling.md +78 -0
  140. package/skills/forms-and-validation.md +93 -0
  141. package/skills/nextjs-app-router.md +75 -0
  142. package/skills/page-composition.md +103 -0
  143. package/skills/performance.md +77 -0
  144. package/skills/plan-before-building.md +52 -0
  145. package/skills/planning-and-delegating.md +72 -0
  146. package/skills/refactoring.md +70 -0
  147. package/skills/security-basics.md +76 -0
  148. package/skills/state-management.md +73 -0
  149. package/skills/supabase-multitenant.md +72 -0
  150. package/skills/typescript-strict.md +90 -0
  151. package/skills/typography.md +135 -0
  152. package/skills/verify-your-own-work.md +62 -0
  153. package/skills/web-app-quality.md +62 -0
  154. package/skills/working-in-the-background.md +64 -0
@@ -0,0 +1,284 @@
1
+ /**
2
+ * ── ⭐⭐ THE ROUND TAX, REMOVED STRUCTURALLY ─────────────────────────────────
3
+ *
4
+ * Measured across five consecutive live runs and then again on the bench: the
5
+ * model reaches for `node -e "import('./x.js').then(m => console.log(...))"` to
6
+ * check a value. The double quotes die on `command.mjs`'s character whitelist,
7
+ * the round is spent, the refusal is read, and the NEXT round writes a temp file
8
+ * and runs that — which is what it should have done first.
9
+ *
10
+ * That is a ~20% tax on a five-round budget, and on the `git` bench task it was
11
+ * the difference between committing and running out of rounds.
12
+ *
13
+ * ── ⚠️ I ALREADY TRIED THE PROMPT FIX AND IT DID NOT HOLD ───────────────────
14
+ * An explicit rule was added naming the refusal and saying to write a file
15
+ * instead. It still happened on the very next run. That is the third time in
16
+ * this project a prompt rule has been obeyed narrowly or not at all, and the
17
+ * lesson is consistent: **when a model keeps reaching for something, give it the
18
+ * thing rather than another sentence telling it not to.**
19
+ *
20
+ * ⭐ So `evaluate` is exactly what `node -e` is for — run a snippet, see the
21
+ * value — with the quoting problem removed because the source arrives as a JSON
22
+ * string argument instead of inside a shell-shaped command line.
23
+ *
24
+ * ── ⚠️ WHY THIS DOES NOT WIDEN THE SECURITY BOUNDARY ────────────────────────
25
+ * It cannot do anything `write_file` + `run_command` could not already do in two
26
+ * calls, which is the honest test for any new capability. It writes into the
27
+ * workspace through the SAME path rules, runs through the SAME bounded spawn
28
+ * with the SAME scrubbed environment, and removes the file afterwards.
29
+ *
30
+ * ⚠️ `command.mjs` still refuses `node --eval` and that refusal STAYS. The
31
+ * objection there was never "evaluating is dangerous" — it was that code passed
32
+ * on a command line is never written to disk, so nothing it did can be reviewed
33
+ * afterwards. Here the snippet IS a file for the duration of the run, and it is
34
+ * echoed in the result, so the audit trail the flag protects is intact.
35
+ */
36
+
37
+ import { unlinkSync, readdirSync, lstatSync } from 'node:fs';
38
+ import { join } from 'node:path';
39
+ import { spawnBounded, scrubEnvironment, childEnvironment, clampOutput, DEFAULT_COMMAND_TIMEOUT_MS } from './command.mjs';
40
+ import { resolveInWorkspace } from './workspace.mjs';
41
+
42
+ /** A snippet longer than this is a program, and programs get written properly. */
43
+ export const MAX_SNIPPET_CHARS = 4_000;
44
+
45
+ /**
46
+ * ── ⚠️ THE `finally` BELOW DOES NOT RUN WHEN THE PROCESS IS KILLED ──────────
47
+ *
48
+ * `turn.mjs` installs `process.on('SIGINT', () => { …; process.exit(130); })`
49
+ * so that Ctrl-C during an interactive session exits deliberately. That exit
50
+ * tears the process down mid-await, and a `finally` in a suspended async
51
+ * function is simply never reached. Measured, against the code as it stood:
52
+ *
53
+ * driver exit code : 130
54
+ * reached finally? : false
55
+ * workspace root : [ '.acuvo-eval-25072-1786406877209.mjs' ]
56
+ *
57
+ * The scratch file is at the WORKSPACE ROOT — the user's repo. It dirties
58
+ * `git status` and it is exactly the kind of file that ends up committed.
59
+ *
60
+ * ⭐ `process.exit()` DOES fire the `exit` event, even though a raw signal does
61
+ * not (turn.mjs measured that separately and relies on the same fact). So one
62
+ * `exit` hook over a registry of in-flight snippets closes the interrupt path,
63
+ * and it is the same shape turn.mjs already uses for MCP connections.
64
+ */
65
+ const pendingSnippets = new Set();
66
+ let exitHookInstalled = false;
67
+
68
+ /** Remove every snippet still in flight. Synchronous — an `exit` handler cannot await. */
69
+ export function flushPendingSnippets() {
70
+ for (const abs of [...pendingSnippets]) {
71
+ try { unlinkSync(abs); } catch { /* already gone, or never landed */ }
72
+ pendingSnippets.delete(abs);
73
+ }
74
+ }
75
+
76
+ function installExitHook() {
77
+ // ⚠️ ONCE, not per call. `evaluate` runs many times per session and a
78
+ // listener per call is how you get MaxListenersExceededWarning printed at a
79
+ // user mid-conversation — the exact bug turn.mjs's registry was written for.
80
+ if (exitHookInstalled) return;
81
+ exitHookInstalled = true;
82
+ process.once('exit', flushPendingSnippets);
83
+ }
84
+
85
+ /**
86
+ * ── ⚠️⚠️ REAPING IS DELETING SOMEONE ELSE'S FILES ───────────────────────────
87
+ *
88
+ * `exit` covers an interrupt, but not a SIGKILL, a crashed terminal or a power
89
+ * cut — and those leave the same litter. The next run sweeps it, which means
90
+ * this code deletes files in a repo it did not write. So it is bounded three
91
+ * ways, ALL of which must hold:
92
+ *
93
+ * 1. the name matches the generated pattern EXACTLY — `.acuvo-eval-<digits>-
94
+ * <digits>.mjs` and nothing else. Not `.acuvo-eval-notes.mjs`, not a
95
+ * `.bak`, not the same name without the leading dot;
96
+ * 2. it is a plain FILE in the workspace ROOT. Never a directory, never a
97
+ * symlink, and this never recurses — a match one level down is untouched;
98
+ * 3. it is OLDER than the threshold. A snippet from a live run in another
99
+ * terminal must survive, and since `spawnBounded` is capped at 120s a
100
+ * live snippet can never approach five minutes.
101
+ *
102
+ * It also skips anything this process still has in flight, and every failure is
103
+ * swallowed: a reaper that can break `evaluate` is not worth having.
104
+ */
105
+ export const STALE_SNIPPET_MS = 5 * 60_000;
106
+
107
+ /** The generated name, and only the generated name. */
108
+ export const SNIPPET_NAME_PATTERN = /^\.acuvo-eval-\d+-\d+\.mjs$/;
109
+
110
+ /**
111
+ * @param {string} root workspace root — never recursed into
112
+ * @returns {string[]} the names actually removed
113
+ */
114
+ export function reapStaleSnippets(root, { now = Date.now(), olderThanMs = STALE_SNIPPET_MS } = {}) {
115
+ const reaped = [];
116
+ if (!root || typeof root !== 'string') return reaped;
117
+
118
+ let entries;
119
+ try {
120
+ entries = readdirSync(root, { withFileTypes: true });
121
+ } catch {
122
+ return reaped; // missing or unreadable root is not this function's problem
123
+ }
124
+
125
+ for (const entry of entries) {
126
+ if (!SNIPPET_NAME_PATTERN.test(entry.name)) continue;
127
+ if (!entry.isFile()) continue; // a directory or symlink wearing the name
128
+ const abs = join(root, entry.name);
129
+ if (pendingSnippets.has(abs)) continue; // ours, and still running
130
+ try {
131
+ const st = lstatSync(abs); // belt and braces: never follow a link
132
+ if (!st.isFile()) continue;
133
+ if (now - st.mtimeMs < olderThanMs) continue; // could belong to a live run
134
+ unlinkSync(abs);
135
+ reaped.push(entry.name);
136
+ } catch { /* raced with someone else, or not ours to delete */ }
137
+ }
138
+ return reaped;
139
+ }
140
+
141
+ /**
142
+ * ── ⚠️ AT THE WORKSPACE ROOT, AND THE FIRST VERSION WAS NOT ─────────────────
143
+ *
144
+ * I staged this in `.acuvo/` and the tool description promised "relative
145
+ * imports work". They did not. Node resolves a relative specifier against the
146
+ * IMPORTING FILE, so `./src/math.mjs` from `.acuvo/eval-*.mjs` looked for
147
+ * `.acuvo/src/math.mjs`:
148
+ *
149
+ * Error [ERR_MODULE_NOT_FOUND]: Cannot find module …/.acuvo/src/math.mjs
150
+ *
151
+ * ⭐ Found by RUNNING it, not by a test — the unit tests stubbed the executor
152
+ * and never resolved a real import. The description was the bug: it made a
153
+ * promise about module resolution that the file's location contradicted, and a
154
+ * model reading it would have written correct code and been told it was wrong.
155
+ *
156
+ * So the snippet lives at the ROOT, where `./src/x.mjs` means what the model
157
+ * thinks it means. The leading dot and the pid/timestamp keep it out of the way,
158
+ * and the `finally` below removes it either way.
159
+ */
160
+ function snippetPath() {
161
+ return `.acuvo-eval-${process.pid}-${Date.now()}.mjs`;
162
+ }
163
+
164
+ /**
165
+ * Run a JavaScript snippet in the workspace and return what it printed.
166
+ *
167
+ * @param {{ executor: any, source: unknown, timeoutMs?: number, spawnImpl?: Function }} args
168
+ */
169
+ export async function evaluateSnippet({ executor, source, timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS, spawnImpl }) {
170
+ if (executor.dryRun) {
171
+ return { ok: false, error: 'this is a --dry-run, so nothing is executed (evaluate writes a file and runs it)' };
172
+ }
173
+ if (typeof source !== 'string' || !source.trim()) {
174
+ return { ok: false, error: 'source is required — the JavaScript to run' };
175
+ }
176
+ if (source.length > MAX_SNIPPET_CHARS) {
177
+ return {
178
+ ok: false,
179
+ error: `the snippet is ${source.length} characters, over the ${MAX_SNIPPET_CHARS} limit — write it to a real file and run that instead`,
180
+ };
181
+ }
182
+
183
+ // Sweep anything a previous run was killed too hard to clean up. Bounded and
184
+ // fail-safe (see reapStaleSnippets); it runs AFTER the dry-run guard above,
185
+ // because a dry run must not delete anything either.
186
+ try { reapStaleSnippets(executor.root); } catch { /* never break evaluate over litter */ }
187
+
188
+ const rel = snippetPath();
189
+
190
+ /**
191
+ * ⚠️ RESOLVED BEFORE IT IS WRITTEN. The old order wrote the file first and
192
+ * resolved second, so a resolve failure left a staged file with no path to
193
+ * unlink it by — a leak on the one path that was supposed to be the safe one.
194
+ */
195
+ const target = resolveInWorkspace(executor.root, rel, 'read');
196
+ if (!target.ok) return { ok: false, error: target.reason };
197
+
198
+ // Registered BEFORE the write, so even a crash mid-write is covered.
199
+ pendingSnippets.add(target.absolute);
200
+ installExitHook();
201
+
202
+ const written = executor.writeFile(rel, source);
203
+ if (!written.ok) {
204
+ pendingSnippets.delete(target.absolute);
205
+ try { unlinkSync(target.absolute); } catch { /* never landed */ }
206
+ return { ok: false, error: `could not stage the snippet: ${written.error}` };
207
+ }
208
+
209
+ try {
210
+ const run = await spawnBounded({
211
+ file: process.execPath,
212
+ args: [target.absolute],
213
+ cwd: executor.root,
214
+ timeoutMs: Math.min(Math.max(1_000, timeoutMs), 120_000),
215
+ spawnImpl,
216
+ /**
217
+ * ⚠️ THIS IS THE `node <a file the model wrote>` ROAD IN ITS PUREST FORM —
218
+ * the snippet is staged and node is spawned on it. So it is the first
219
+ * place an install launched from inside model-written code would reach
220
+ * npm with lifecycle scripts ENABLED, while the gated `npm install` road
221
+ * had them off. `childEnvironment` closes that asymmetry; it never
222
+ * closes the road, and `validateInstallSpec`'s paragraph says so plainly.
223
+ */
224
+ env: childEnvironment({ file: process.execPath, args: [target.absolute] }, process.env),
225
+ });
226
+ if (!run.ok) return { ok: false, error: run.error };
227
+
228
+ return {
229
+ ok: true,
230
+ // ⭐ ECHOED BACK. The snippet is gone from disk by the time the model reads
231
+ // this, and a result whose cause cannot be inspected is how a wrong
232
+ // conclusion gets built on. It is also what keeps the --eval audit
233
+ // argument satisfied.
234
+ source: clampOutput(source, 1_500).text,
235
+ exitCode: run.exitCode,
236
+ timedOut: run.timedOut,
237
+ durationMs: run.durationMs,
238
+ stdout: run.stdout,
239
+ stderr: run.stderr,
240
+ /**
241
+ * ⚠️ `ok` MEANS IT RAN, `passed` MEANS IT SUCCEEDED. The same distinction
242
+ * run_command makes, for the same reason: conflating them is exactly how a
243
+ * loop reports success on a throwing snippet.
244
+ */
245
+ passed: run.exitCode === 0 && !run.timedOut,
246
+ };
247
+ } finally {
248
+ /**
249
+ * ⚠️ REMOVED IN A `finally`, INCLUDING ON TIMEOUT. A scratch file left in
250
+ * someone's repo is the thing that ends up committed — this project already
251
+ * watched the agent burn four rounds unable to delete one, and watched
252
+ * `git status` go dirty because of it.
253
+ */
254
+ try { unlinkSync(target.absolute); } catch { /* already gone, or never landed */ }
255
+ pendingSnippets.delete(target.absolute);
256
+ }
257
+ }
258
+
259
+ export function evaluateToolSchema() {
260
+ return {
261
+ type: 'function',
262
+ function: {
263
+ name: 'evaluate',
264
+ description: [
265
+ 'Run a short piece of JavaScript in the workspace and see what it prints.',
266
+ 'USE THIS INSTEAD OF `node -e` — that is refused, because a command line here cannot',
267
+ 'contain quotes. This is the supported way to check a value, import one of your own',
268
+ 'modules and call it, or verify a function returns what you expect.',
269
+ 'The snippet runs as an ES module from the workspace root, so relative imports work',
270
+ '(e.g. `import { sum } from "./src/math.mjs"`). It is deleted afterwards.',
271
+ ].join(' '),
272
+ parameters: {
273
+ type: 'object',
274
+ properties: {
275
+ source: {
276
+ type: 'string',
277
+ description: 'The JavaScript to run. Use console.log to see anything.',
278
+ },
279
+ },
280
+ required: ['source'],
281
+ },
282
+ },
283
+ };
284
+ }