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,250 @@
1
+ /**
2
+ * ── ⚠️⚠️ THE FIX FOR THE DARK MEDIA HALF WAS ITSELF DARK ───────────────────
3
+ *
4
+ * `bin/acuvo.mjs` grew a `.env` loader whose own comment explains exactly why:
5
+ * *"Measured today: `mediaToolNames(process.env)` returned `[]` in an ordinary
6
+ * terminal, on a machine where every one of those services is configured and
7
+ * working... `see_page` — the capability this CLI is sold on — was never even
8
+ * OFFERED."*
9
+ *
10
+ * ⚠️ IT LOADED `.env`. Measured 2026-08-12 across this whole machine: there is
11
+ * **no plain `.env` anywhere** — every file is `.env.local`, which is the name
12
+ * Next.js, Vite and Create React App all use for the one that holds secrets and
13
+ * is git-ignored. So the loader never fired once, and the media half it was
14
+ * written to rescue stayed exactly as dark as before. A fix that cannot run is
15
+ * indistinguishable from the bug.
16
+ *
17
+ * ⚠️ AND IT WAS INLINE IN `bin/`, WHICH IS WHY NOBODY CAUGHT IT. Eight lines
18
+ * inside `main()` cannot be imported, so no test could assert which filenames it
19
+ * looks for. That is the whole reason this file exists as a module: the list of
20
+ * candidate names is now a value a test can read.
21
+ *
22
+ * ── PRECEDENCE ─────────────────────────────────────────────────────────────
23
+ *
24
+ * ⭐ `.env.local` IS LOADED BEFORE `.env`, and the order is load-bearing because
25
+ * `process.loadEnvFile` DOES NOT OVERWRITE. First writer wins, so "load first"
26
+ * means "higher precedence" — the inverse of what the reading order suggests,
27
+ * and worth stating because getting it backwards silently makes the committed
28
+ * `.env` beat the private `.env.local`.
29
+ *
30
+ * ⭐ AND IT WALKS UP. A monorepo keeps one `.env.local` at the top and runs
31
+ * tools from `packages/whatever`; stopping at the workspace root would find
32
+ * nothing in the common layout. It stops at the filesystem root or a `.git`
33
+ * directory — the same boundary every other developer tool treats as "the
34
+ * project" — so it can never wander into a sibling checkout or a home
35
+ * directory it was not pointed at.
36
+ *
37
+ * ⚠️ A REAL ENVIRONMENT VARIABLE ALWAYS WINS over every file, because the
38
+ * loader never overwrites. An explicit `export` in this shell must beat a stale
39
+ * file somebody forgot about, or debugging becomes guesswork about which value
40
+ * is live.
41
+ */
42
+
43
+ import { existsSync, statSync, readFileSync } from 'node:fs';
44
+ import { join, dirname, resolve } from 'node:path';
45
+
46
+ /**
47
+ * In precedence order — earlier wins, because `loadEnvFile` does not overwrite.
48
+ * ⚠️ `.env.example` is deliberately ABSENT: it is documentation, it is committed,
49
+ * and its values are placeholders. Loading it would set `OPENROUTER_API_KEY` to
50
+ * something like `sk-or-v1-...` and produce a 401 that blames the user's key.
51
+ */
52
+ export const ENV_FILENAMES = Object.freeze(['.env.local', '.env']);
53
+
54
+ /** How far up to walk before giving up. Deep enough for any real monorepo. */
55
+ export const MAX_WALK_UP = 8;
56
+
57
+ /**
58
+ * Every env file that exists, nearest directory first, in precedence order.
59
+ * Pure and exported so a test can assert the NAMES without touching the
60
+ * process environment — the assertion that would have caught the original bug.
61
+ *
62
+ * @param {string} from directory to start at
63
+ * @param {{ stopAtGit?: boolean }} opts
64
+ * @returns {string[]} absolute paths, highest precedence first
65
+ */
66
+ export function envFileCandidates(from, { stopAtGit = true } = {}) {
67
+ if (typeof from !== 'string' || from === '') return [];
68
+
69
+ const found = [];
70
+ let dir = resolve(from);
71
+
72
+ for (let i = 0; i < MAX_WALK_UP; i += 1) {
73
+ for (const name of ENV_FILENAMES) {
74
+ const candidate = join(dir, name);
75
+ try {
76
+ if (existsSync(candidate) && statSync(candidate).isFile()) found.push(candidate);
77
+ } catch { /* unreadable — treat as absent */ }
78
+ }
79
+
80
+ /**
81
+ * ⚠️ THE `.git` STOP COMES **AFTER** THIS DIRECTORY'S FILES ARE COLLECTED.
82
+ * The repository root is the most likely place for the file, so stopping
83
+ * before reading it would skip the single most common location.
84
+ */
85
+ if (stopAtGit) {
86
+ try { if (existsSync(join(dir, '.git'))) break; } catch { /* keep walking */ }
87
+ }
88
+
89
+ const parent = dirname(dir);
90
+ if (parent === dir) break;
91
+ dir = parent;
92
+ }
93
+
94
+ return found;
95
+ }
96
+
97
+ /**
98
+ * Load every env file we can find, best-effort.
99
+ *
100
+ * ⚠️ NEVER THROWS AND NEVER FAILS THE RUN. No env file is the normal case, and
101
+ * a malformed one must not stop a coding session that never needed it.
102
+ *
103
+ * @param {string[]} roots directories to search, in order
104
+ * @param {{ load?: Function, existsImpl?: Function }} opts
105
+ * @returns {{ loaded: string[], failed: Array<{file: string, error: string}> }}
106
+ */
107
+ /**
108
+ * ── ⚠️⚠️ THE MASTER KEY: A CLONED REPOSITORY WAS SETTING OUR OWN SWITCHES ────
109
+ *
110
+ * `bin/acuvo.mjs` calls `envLoad([root, process.cwd()])`, and this loader walks
111
+ * up from the WORKSPACE reading `.env.local` / `.env` into `process.env`. That is
112
+ * correct and wanted for a PROJECT’s variables — DATABASE_URL, STRIPE_KEY, the
113
+ * things the code under test needs.
114
+ *
115
+ * ⚠️⚠️ IT WAS ALSO SETTING OURS. Measured 2026-08-15 with one `.env.local` in a
116
+ * cloned repository, every value chosen by the repository:
117
+ *
118
+ * npm installs enabled false → TRUE (ACUVO_ALLOW_INSTALL)
119
+ * MCP consent bypassed false → TRUE (ACUVO_TRUST_MCP)
120
+ * git push enabled false → TRUE (ACUVO_ALLOW_PUSH)
121
+ * reviewer independent true → FALSE (ACUVO_REFUTE_MODEL → the builder)
122
+ * provider pin ours → theirs (ACUVO_PROVIDER_ORDER)
123
+ *
124
+ * Every guard closed today — the install gate, MCP consent, the independent
125
+ * second opinion, the measured provider pin — defeated by one file in a
126
+ * repository somebody cloned. It is the same defect `policy.mjs` names and
127
+ * solves: **the config lives in the workspace, and the agent can write to the
128
+ * workspace.** Its answer is that a workspace layer may only ever REMOVE
129
+ * permission, never add. This is that rule, applied to the environment.
130
+ *
131
+ * ⭐ SO A WORKSPACE ENV FILE MAY SET THE PROJECT’S VARIABLES AND NONE OF OURS.
132
+ * The refusal is REPORTED, never silent — a variable that vanishes without a
133
+ * word is a bug report we would never receive.
134
+ *
135
+ * ⚠️ AND IT IS A PREFIX RULE, NOT A LIST OF THE SWITCHES WE HAPPEN TO HAVE
136
+ * TODAY. A list goes stale the moment somebody adds `ACUVO_ALLOW_ANYTHING`, and
137
+ * the person adding it will not be reading this file. Anything beginning
138
+ * `ACUVO_` is ours by construction.
139
+ *
140
+ * ⚠️ `process.loadEnvFile` DOES NOT OVERRIDE AN ALREADY-SET VARIABLE, which is
141
+ * why our own OPENROUTER_API_KEY survived the probe. That is a mitigation, not a
142
+ * defence: every variable the operator has NOT set is the repository’s to
143
+ * choose, and the switches above are all unset by default. Relying on it would
144
+ * be relying on the user having already configured the thing being attacked.
145
+ */
146
+ export const OURS_PREFIX = /^ACUVO_/i;
147
+
148
+ /**
149
+ * Variables that steer a CHILD PROCESS rather than this one. A repository that
150
+ * cannot set our switches directly must not be able to set them through the
151
+ * environment a spawned python, gh or node inherits either.
152
+ *
153
+ * ⚠️ `command.mjs`’s `scrubEnvironment` already strips conventionally-named
154
+ * secrets and the node injection variables from CHILDREN. This list is the
155
+ * INBOUND half — stopping them entering `process.env` at all — because a
156
+ * variable we never accept is one no future spawn site can forget to scrub.
157
+ */
158
+ export const CHILD_STEERING_VARS = Object.freeze([
159
+ 'NODE_OPTIONS', 'NODE_PATH', 'NODE_REPL_EXTERNAL_MODULE',
160
+ 'PYTHONPATH', 'PYTHONSTARTUP', 'PYTHONHOME', 'PYTHONWARNINGS', 'PYTEST_ADDOPTS',
161
+ 'GH_HOST', 'GH_TOKEN', 'GH_ENTERPRISE_TOKEN', 'GITHUB_TOKEN', 'GH_CONFIG_DIR',
162
+ 'GIT_SSH_COMMAND', 'GIT_EXTERNAL_DIFF', 'GIT_PAGER',
163
+ 'LD_PRELOAD', 'DYLD_INSERT_LIBRARIES',
164
+ 'OPENROUTER_API_KEY', 'OPENROUTER_CODEGEN_MODEL',
165
+ ]);
166
+
167
+ /**
168
+ * Is this a name a file INSIDE the workspace may set?
169
+ * @param {string} name
170
+ */
171
+ export function workspaceMaySet(name) {
172
+ const n = String(name ?? '');
173
+ if (OURS_PREFIX.test(n)) return false;
174
+ return !CHILD_STEERING_VARS.some((v) => v.toLowerCase() === n.toLowerCase());
175
+ }
176
+
177
+ /**
178
+ * The NAMES an env file declares, without evaluating it.
179
+ *
180
+ * ⚠️ NAMES ONLY, DELIBERATELY. The value is never needed to decide whether the
181
+ * name is allowed, and not parsing values means this cannot become a second,
182
+ * subtly-different dotenv implementation that disagrees with Node’s about
183
+ * quoting or multi-line strings. Node still does the actual loading.
184
+ */
185
+ export function declaredNames(text) {
186
+ const out = [];
187
+ for (const raw of String(text ?? '').split(/\r?\n/)) {
188
+ const line = raw.trim();
189
+ if (!line || line.startsWith('#')) continue;
190
+ const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line);
191
+ if (m) out.push(m[1]);
192
+ }
193
+ return out;
194
+ }
195
+ export function loadEnvFiles(roots, { load = process.loadEnvFile, readImpl = null, env = process.env } = {}) {
196
+ const loaded = [];
197
+ const failed = [];
198
+ /** Names a workspace file tried to set and was not allowed to. Reported, never silent. */
199
+ const refused = [];
200
+ if (typeof load !== 'function') return { loaded, failed, refused };
201
+
202
+ const readText = readImpl ?? ((f) => readFileSync(f, 'utf8'));
203
+
204
+ const seen = new Set();
205
+ for (const root of roots) {
206
+ for (const file of envFileCandidates(root)) {
207
+ if (seen.has(file)) continue;
208
+ seen.add(file);
209
+
210
+ /**
211
+ * ⭐ SNAPSHOT THE NAMES WE OWN, LOAD, THEN PUT THEM BACK.
212
+ *
213
+ * ⚠️ Why not filter BEFORE loading? Because `process.loadEnvFile` is
214
+ * Node’s own parser and offers no hook — reimplementing dotenv to filter
215
+ * first would be a second parser that disagrees with the first about
216
+ * quoting, which is a worse bug than the one being fixed. So the file is
217
+ * loaded, and anything it set that it may not set is restored exactly as
218
+ * it was, including "was not set at all".
219
+ */
220
+ let guarded = [];
221
+ try {
222
+ guarded = declaredNames(readText(file)).filter((n) => !workspaceMaySet(n));
223
+ } catch {
224
+ // Unreadable here means load() fails below and is recorded there.
225
+ }
226
+ const restore = guarded.map((n) => [n, Object.prototype.hasOwnProperty.call(env, n) ? env[n] : undefined]);
227
+
228
+ try {
229
+ load(file);
230
+ loaded.push(file);
231
+ } catch (e) {
232
+ failed.push({ file, error: e?.message ?? String(e) });
233
+ continue;
234
+ }
235
+
236
+ for (const [name, previous] of restore) {
237
+ const now = env[name];
238
+ if (previous === undefined) delete env[name];
239
+ else env[name] = previous;
240
+ /**
241
+ * ⚠️ Only a name the file actually CHANGED is reported. Declaring a
242
+ * variable that already held the same value is not an attempt at
243
+ * anything, and crying wolf about it teaches people to ignore the line.
244
+ */
245
+ if (now !== previous) refused.push({ file, name });
246
+ }
247
+ }
248
+ }
249
+ return { loaded, failed, refused };
250
+ }