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,235 @@
1
+ /**
2
+ * ── ⭐⭐ THE PER-RUN HALF OF WRITE APPROVAL ─────────────────────────────────
3
+ *
4
+ * `diff-preview.mjs` is complete and was reachable from nothing. Its own running
5
+ * list entry says exactly what was missing, and it is only this:
6
+ *
7
+ * > `executeToolCall` "is a pure switch over a single call and has no memory of
8
+ * > the round before it", and `approvalDecision` needs two facts that span
9
+ * > rounds — `createdThisRun` (so the agent revising its own draft is never
10
+ * > queried) and `sessionApproved` (the "all remaining" answer).
11
+ *
12
+ * So this file is the memory, and nothing else. All the judgement — what counts
13
+ * as risky, what a diff looks like, what the prompt says — already lives in
14
+ * `diff-preview.mjs` and is not duplicated here.
15
+ *
16
+ * ── ⚠️ THE GATE IS ON THE MODEL'S DOOR, NOT IN THE EXECUTOR ─────────────────
17
+ * `tools.mjs` states the rule for the credential guard and it applies verbatim:
18
+ * *"THE GUARD IS HERE, ON THE MODEL'S DOOR, NOT IN THE EXECUTOR … a model-facing
19
+ * refusal pushed down into shared plumbing broke seven tests of legitimate
20
+ * internal work."* Approval is a model-facing refusal. Hooking
21
+ * `workspace.writeFile` would gate `--doctor`, checkpoint restores and the
22
+ * acceptance harness too — none of which is a model asking to change your files.
23
+ *
24
+ * ── ⚠️⚠️ AND IT NEVER BLOCKS AN UNATTENDED RUN ─────────────────────────────
25
+ * `diff-preview.mjs`'s header settles this and the reasoning is inherited whole:
26
+ * a gate that breaks CI, `--parallel` and the fleet gets globally disabled with
27
+ * `ACUVO_APPROVE=never` in a shell profile, and then it protects nobody on the
28
+ * laptop either. With no terminal there is nobody to ask, so the write proceeds
29
+ * and the decision says so. **This is a review affordance, not a fifth lock** —
30
+ * the write is already confined by `resolveInWorkspace`, `WRITE_FORBIDDEN_ROOTS`,
31
+ * `MAX_WRITE_BYTES` and `policy.requireDryRun` before it ever reaches here.
32
+ */
33
+ import {
34
+ approvalDecision, approvalMode, interpretAnswer,
35
+ diffUnified, renderApproval, approvalPrompt, previewLineCap,
36
+ } from './diff-preview.mjs';
37
+
38
+ /**
39
+ * Build the per-run approver. One per turn; the closure IS the memory.
40
+ *
41
+ * @param {object} opts
42
+ * @param {(question: string) => Promise<string>|string} [opts.ask] the existing
43
+ * asker seam. Absent = unattended, and every write settles unreviewed.
44
+ * @param {boolean} [opts.isInteractive]
45
+ * @param {NodeJS.ProcessEnv} [opts.env]
46
+ * @param {string|null} [opts.flag] `--yes` / `--approve=<mode>` if given.
47
+ * @returns {{ approve: (write: object) => Promise<object>, noteCreated: (path: string) => void, state: object }}
48
+ */
49
+ /**
50
+ * ── ⚠️⚠️⚠️ PEOPLE WERE APPROVING CHANGES THEY COULD NOT SEE ─────────────────
51
+ *
52
+ * Measured 2026-08-19: all 1,044 lines of `diff-preview.mjs`'s renderer —
53
+ * `renderDiff`, `renderApproval`, `approvalPrompt` — had **zero production
54
+ * callers**. The prompt a user actually got was the bare fallback
55
+ * `Apply changes to <path>?`, with no diff and no mention of `a` or `q`, even
56
+ * though `interpretAnswer` accepts all four.
57
+ *
58
+ * ⭐ THE DIFF IS THE PRODUCT HERE. Claude Code's primary output is the diff;
59
+ * "approve this" without showing what "this" is trains people to type `y`
60
+ * reflexively, which is the same as having no gate at all — worse, because it
61
+ * looks like one.
62
+ */
63
+ export function createWriteApprover({
64
+ ask = null, isInteractive = false, env = process.env, flag = null,
65
+ /**
66
+ * Where the diff is printed. STDERR by default, deliberately: stdout is the
67
+ * `--json` contract, and a review that corrupted a machine-readable document
68
+ * would be a worse bug than the one this fixes.
69
+ */
70
+ show = (text) => process.stderr.write(`${text}
71
+ `),
72
+ paint = null,
73
+ } = {}) {
74
+ /**
75
+ * ⚠️ `approvalMode` RETURNS `{ ok, mode, from }`, NOT A STRING — and passing the
76
+ * object straight through silently disabled the feature: `approvalDecision`
77
+ * compares `mode === 'never'`, an object never matches, so `ACUVO_APPROVE=never`
78
+ * kept prompting. Caught by its own test, which is the only reason it is not
79
+ * still doing that.
80
+ */
81
+ const { mode } = approvalMode({ env, flag });
82
+ /** Files this run brought into existence. Revising your own draft is not a change to REVIEW. */
83
+ const createdThisRun = new Set();
84
+ const state = { sessionApproved: false, asked: 0, approved: 0, refused: 0 };
85
+
86
+ /**
87
+ * ⚠️ A PATH IS RECORDED AS "OURS" ONLY WHEN IT DID NOT EXIST BEFORE. Recording
88
+ * every written path would mean the second edit of a file the user wrote is
89
+ * treated as our own draft and never queried — which is precisely the review
90
+ * this exists to perform.
91
+ */
92
+ const noteCreated = (path) => {
93
+ const p = String(path ?? '').trim();
94
+ if (p) createdThisRun.add(p);
95
+ };
96
+
97
+ const approve = async (write) => {
98
+ const decision = approvalDecision(write, {
99
+ mode,
100
+ interactive: Boolean(isInteractive && ask),
101
+ createdThisRun,
102
+ sessionApproved: state.sessionApproved,
103
+ });
104
+
105
+ // Nothing to ask, or nobody to ask. `diff-preview` has already encoded which.
106
+ if (!decision.required || !decision.satisfiable || typeof ask !== 'function') {
107
+ return { ...decision, allowed: true };
108
+ }
109
+
110
+ state.asked += 1;
111
+
112
+ /**
113
+ * ⭐⭐ SHOW THE CHANGE BEFORE ASKING ABOUT IT. `diffUnified` refuses to
114
+ * render something enormous and says so rather than printing 40,000 lines
115
+ * into a terminal, so the cap is its problem, not ours.
116
+ *
117
+ * ⚠️ NEVER FAILS THE WRITE. A renderer that threw would turn "we could not
118
+ * draw this" into "your change was rejected", which is the worst possible
119
+ * trade for a display feature.
120
+ */
121
+ const kind = write?.kind
122
+ ?? (write?.after === null || write?.after === undefined ? 'deleted'
123
+ : (write?.before === null || write?.before === undefined ? 'created' : 'replaced'));
124
+ /**
125
+ * ⭐ `renderApproval` HANDS BACK BOTH HALVES — `{ body, prompt }` — so the
126
+ * diff and the question it belongs to cannot drift apart. Composing the
127
+ * prompt separately here is how a change ends up shown beside the wrong
128
+ * verb: "Delete" printed over a diff that creates.
129
+ */
130
+ let question = null;
131
+ try {
132
+ const diff = diffUnified(write?.before ?? '', write?.after ?? '', write?.path ?? '', {});
133
+ const rendered = renderApproval(diff, decision, {
134
+ cap: previewLineCap({ env }),
135
+ path: write?.path ?? '',
136
+ ...(paint ? { paint } : {}),
137
+ });
138
+ if (rendered?.body) show(rendered.body);
139
+ question = rendered?.prompt ?? null;
140
+ } catch { /* a diff we cannot draw is not a reason to refuse a write */ }
141
+
142
+ let answer;
143
+ try {
144
+ answer = await ask(decision.prompt ?? question ?? approvalPrompt(write?.path ?? '', kind));
145
+ } catch {
146
+ /**
147
+ * ⚠️ AN ASKER THAT THROWS MEANS NOBODY ANSWERED, NOT "NO". A closed pipe
148
+ * mid-run must not turn into a refusal that looks like the user vetoing
149
+ * the work — that is the least legible failure available, because the
150
+ * agent then reports being blocked by a person who was never there.
151
+ */
152
+ return { ...decision, allowed: true, reviewed: false, reason: 'nobody could be asked, so the write proceeded' };
153
+ }
154
+
155
+ /**
156
+ * ⚠️ FOUR OUTCOMES, NOT TWO, and `diff-preview.mjs` owns the vocabulary:
157
+ * `approve` · `approve-all` · `reject` · `abort`. Collapsing them to a
158
+ * boolean here would lose the two that matter most — `approve-all` is what
159
+ * stops the prompt firing forty times, and `abort` means STOP THE RUN rather
160
+ * than "skip this file", which the agent must not treat as a retryable no.
161
+ */
162
+ const verdict = interpretAnswer(answer);
163
+ if (verdict.decision === 'approve-all') state.sessionApproved = true;
164
+ const allowed = verdict.decision === 'approve' || verdict.decision === 'approve-all';
165
+ if (allowed) state.approved += 1;
166
+ else state.refused += 1;
167
+ return {
168
+ ...decision,
169
+ allowed,
170
+ reviewed: true,
171
+ aborted: verdict.decision === 'abort',
172
+ answer: verdict,
173
+ reason: verdict.reason,
174
+ };
175
+ };
176
+
177
+ /**
178
+ * ── ⭐ THE BULK CASE: ONE QUESTION FOR THE WHOLE BATCH ─────────────────────
179
+ *
180
+ * ⚠️ Asking per file turns one intent — "apply this refactor" — into forty
181
+ * prompts, and a prompt answered forty times is answered without reading by
182
+ * the third.
183
+ *
184
+ * ⚠️ AND IT ASKS IF **ANY** FILE NEEDS IT, not if all of them do. A batch of
185
+ * thirty-nine scratch files plus one rewrite of `server.js` is exactly the
186
+ * shape a review exists to catch, and an "all" rule would wave it through.
187
+ * The answer then governs the whole batch, which is what the person was shown.
188
+ */
189
+ const approveMany = async (writes) => {
190
+ const list = Array.isArray(writes) ? writes : [];
191
+ if (list.length === 0) return { allowed: true, required: false, reason: 'nothing to write' };
192
+
193
+ const decisions = list.map((w) => approvalDecision(w, {
194
+ mode,
195
+ interactive: Boolean(isInteractive && ask),
196
+ createdThisRun,
197
+ sessionApproved: state.sessionApproved,
198
+ }));
199
+ const riskiest = decisions.find((d) => d.required && d.satisfiable);
200
+ if (!riskiest || typeof ask !== 'function') {
201
+ return { allowed: true, required: false, reason: 'no file in this batch needed review' };
202
+ }
203
+
204
+ state.asked += 1;
205
+ let answer;
206
+ try {
207
+ answer = await ask(
208
+ `${riskiest.prompt ?? 'Apply these changes?'} (${list.length} file${list.length === 1 ? '' : 's'} in this batch)`,
209
+ );
210
+ } catch {
211
+ return { allowed: true, reviewed: false, reason: 'nobody could be asked, so the batch proceeded' };
212
+ }
213
+ const verdict = interpretAnswer(answer);
214
+ if (verdict.decision === 'approve-all') state.sessionApproved = true;
215
+ const allowed = verdict.decision === 'approve' || verdict.decision === 'approve-all';
216
+ if (allowed) state.approved += 1;
217
+ else state.refused += 1;
218
+ return { allowed, reviewed: true, aborted: verdict.decision === 'abort', reason: verdict.reason };
219
+ };
220
+
221
+ return { approve, approveMany, noteCreated, state };
222
+ }
223
+
224
+ /**
225
+ * ⭐ THE REFUSAL THE MODEL SEES. It has to read as a decision a person made, not
226
+ * as a tool failure — otherwise the agent's next move is to retry the write,
227
+ * which is the worst possible response to "no".
228
+ */
229
+ export function refusedWriteResult(path) {
230
+ return {
231
+ ok: false,
232
+ refused: true,
233
+ error: `the change to ${path} was declined by the person running this. Do not retry the same write — ask what they want different, or move on to another part of the task.`,
234
+ };
235
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * ── ⭐⭐ THE BULK EDIT WAS ALREADY HAPPENING — JUST NOT WHERE ANYTHING COULD SEE IT ─
3
+ *
4
+ * Measured 2026-08-13. Given "add a parameter to every one of these 45 modules",
5
+ * the agent did not call `write_file` 45 times across 45 rounds. It wrote a
6
+ * loop inside `evaluate` and did all 45 in ONE call, correctly, in six rounds.
7
+ * That is the right instinct and it is how a person would do it.
8
+ *
9
+ * ⚠️ But `evaluate` starts a process, so every accounting mechanism this package
10
+ * has was blind to it:
11
+ *
12
+ * · no `mutated` flag → the run reported "No files changed" after 45 writes
13
+ * · no lease claim → another terminal's file could be overwritten silently
14
+ * · no `mutatedPath` → `parallel.mjs`'s collision detection saw nothing
15
+ * · no credential gate → the write path's refusals never ran
16
+ *
17
+ * ⭐ THE ANSWER IS NOT TO FORBID `evaluate`. It is to make the operation the
18
+ * model actually wants a FIRST-CLASS one, so the effective technique and the
19
+ * governed technique are the same technique. Every write here goes through
20
+ * `executor.writeFile`, which means leases, refusals, dry-run and the change
21
+ * count all work exactly as they do for a single file — because it IS the
22
+ * single-file path, called in a loop.
23
+ *
24
+ * ── ⚠️ PARTIAL SUCCESS IS REPORTED, NOT ROLLED BACK ─────────────────────────
25
+ *
26
+ * The obvious instinct is all-or-nothing. It is wrong here, for a reason worth
27
+ * writing down: a rollback needs to restore the previous contents of files this
28
+ * tool may have just created, in a workspace another terminal may be writing to
29
+ * concurrently — a second, more dangerous write path, invented to tidy the
30
+ * failure of the first. Reporting exactly which files landed and which were
31
+ * refused is honest, is what the model needs in order to retry the remainder,
32
+ * and cannot itself corrupt anything.
33
+ */
34
+
35
+ /** More than this in one call is a migration, and a migration wants review. */
36
+ export const MAX_FILES = 60;
37
+ /** The sum of all contents. `write_file`'s own per-file ceiling still applies. */
38
+ export const MAX_TOTAL_BYTES = 2_000_000;
39
+
40
+ export function writeManyToolSchemas() {
41
+ return [
42
+ {
43
+ type: 'function',
44
+ function: {
45
+ name: 'write_files',
46
+ description: [
47
+ 'Write SEVERAL files in one call. Use this instead of many separate write_file calls',
48
+ 'when a change touches more than two or three files — a rename across a package, adding',
49
+ 'a parameter to every module, generating a set of tests.',
50
+ 'DO NOT reach for a script inside `evaluate` to do bulk edits: writes made that way are',
51
+ 'invisible to the file leases that stop two terminals colliding, and to the "files changed"',
52
+ 'count the person reads at the end.',
53
+ `At most ${MAX_FILES} files per call. Each file replaces its whole contents, exactly like write_file.`,
54
+ 'If some are refused, the rest still land and you are told which did what.',
55
+ ].join(' '),
56
+ parameters: {
57
+ type: 'object',
58
+ properties: {
59
+ files: {
60
+ type: 'array',
61
+ description: 'The files to write. Each replaces the whole file.',
62
+ items: {
63
+ type: 'object',
64
+ properties: {
65
+ path: { type: 'string', description: 'Workspace-relative path.' },
66
+ content: { type: 'string', description: 'The complete new contents.' },
67
+ },
68
+ required: ['path', 'content'],
69
+ },
70
+ },
71
+ },
72
+ required: ['files'],
73
+ },
74
+ },
75
+ },
76
+ ];
77
+ }
78
+
79
+ /**
80
+ * @param {{ writeFile: (path: string, content: string) => any }} executor
81
+ * @param {{ files?: unknown }} args
82
+ */
83
+ export function writeMany(executor, { files } = {}) {
84
+ if (!Array.isArray(files) || files.length === 0) {
85
+ return { ok: false, error: 'write_files needs a non-empty `files` array — each entry is { path, content }' };
86
+ }
87
+ if (files.length > MAX_FILES) {
88
+ return {
89
+ ok: false,
90
+ error: `${files.length} files in one call is over the ${MAX_FILES} limit. Split it — a change that large is one somebody will want to read in pieces.`,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * ⚠️ VALIDATED BEFORE ANYTHING IS WRITTEN. A malformed entry halfway down the
96
+ * list would otherwise leave the first half applied and the call refused,
97
+ * which is the worst of both answers: work done, and an error saying it was
98
+ * not.
99
+ */
100
+ let total = 0;
101
+ const seen = new Set();
102
+ for (const [i, f] of files.entries()) {
103
+ if (!f || typeof f !== 'object') return { ok: false, error: `files[${i}] is not an object — each entry is { path, content }` };
104
+ if (typeof f.path !== 'string' || !f.path.trim()) return { ok: false, error: `files[${i}] has no path` };
105
+ if (typeof f.content !== 'string') return { ok: false, error: `files[${i}] (${f.path}) has no content string` };
106
+ /**
107
+ * ⚠️ THE SAME PATH TWICE IS A MISTAKE, NOT AN INSTRUCTION. One of the two
108
+ * contents would silently win, and which one is an implementation detail of
109
+ * iteration order — exactly the kind of thing that looks fine in testing
110
+ * and loses somebody's work once.
111
+ */
112
+ const key = f.path.trim();
113
+ if (seen.has(key)) return { ok: false, error: `${key} appears twice in one call — one of the two would silently win` };
114
+ seen.add(key);
115
+ total += Buffer.byteLength(f.content, 'utf8');
116
+ }
117
+ if (total > MAX_TOTAL_BYTES) {
118
+ return { ok: false, error: `refusing to write ${total} bytes in one call (limit ${MAX_TOTAL_BYTES})` };
119
+ }
120
+
121
+ const written = [];
122
+ const refused = [];
123
+ for (const f of files) {
124
+ // ⭐ THE SINGLE-FILE PATH, in a loop. Every guard it has — leases,
125
+ // credential refusals, `.acuvo/` protection, dry-run — applies unchanged,
126
+ // because this is not a second way to write a file.
127
+ const r = executor.writeFile(f.path, f.content);
128
+ if (r?.ok) written.push({ path: r.path ?? f.path, bytes: r.bytes ?? Buffer.byteLength(f.content, 'utf8'), created: r.created === true, dryRun: r.dryRun === true });
129
+ else refused.push({ path: f.path, error: r?.error ?? 'the write was refused' });
130
+ }
131
+
132
+ return {
133
+ /**
134
+ * ⚠️ `ok` IS FALSE WHEN NOTHING LANDED, and true when some did. A call that
135
+ * wrote 44 of 45 files did real work the model must not repeat blindly, and
136
+ * one that wrote none is a failure it should react to. Collapsing the two
137
+ * into "partial" would leave the model guessing which it had.
138
+ */
139
+ ok: written.length > 0,
140
+ written,
141
+ refused,
142
+ error: written.length === 0
143
+ ? `no file was written. ${refused.length === 1 ? refused[0].error : `${refused.length} paths were refused — the first was: ${refused[0]?.error ?? 'unknown'}`}`
144
+ : undefined,
145
+ };
146
+ }
147
+
148
+ /** What the model reads back. Names every refusal, because it has to retry them. */
149
+ export function formatWriteMany(result) {
150
+ if (!result) return 'write_files returned nothing';
151
+ if (result.ok !== true && (result.written?.length ?? 0) === 0) return `write_files: ${result.error}`;
152
+ const lines = [];
153
+ const w = result.written ?? [];
154
+ const dry = w.some((f) => f.dryRun);
155
+ lines.push(`${w.length} file${w.length === 1 ? '' : 's'} ${dry ? 'WOULD be written (dry run — nothing was)' : 'written'}:`);
156
+ for (const f of w.slice(0, 60)) lines.push(` ${f.created ? 'created ' : 'replaced'} ${f.path} (${f.bytes} bytes)`);
157
+ if ((result.refused?.length ?? 0) > 0) {
158
+ lines.push(`${result.refused.length} refused — these still need doing:`);
159
+ for (const f of result.refused.slice(0, 20)) lines.push(` ${f.path}: ${f.error}`);
160
+ }
161
+ return lines.join('\n');
162
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "acuvo-code",
3
+ "version": "0.2.0",
4
+ "description": "Acuvo Code — the terminal client for the Acuvo capability registry. Zero dependencies, by design.",
5
+ "type": "module",
6
+ "bin": {
7
+ "acuvo": "bin/acuvo.mjs",
8
+ "acuvo-mcp": "bin/acuvo-mcp.mjs"
9
+ },
10
+ "engines": {
11
+ "node": ">=20"
12
+ },
13
+ "exports": {
14
+ ".": "./lib/turn.mjs",
15
+ "./turn": "./lib/turn.mjs",
16
+ "./tools": "./lib/tools.mjs",
17
+ "./workspace": "./lib/workspace.mjs",
18
+ "./memory-workspace": "./lib/memory-workspace.mjs",
19
+ "./budget": "./lib/budget.mjs",
20
+ "./policy": "./lib/policy.mjs",
21
+ "./lib/*": "./lib/*.mjs",
22
+ "./package.json": "./package.json"
23
+ },
24
+ "scripts": {
25
+ "test": "node scripts/test.mjs",
26
+ "test:raw": "node --test --test-timeout=180000 test/",
27
+ "bundle": "node scripts/bundle.mjs --out dist/acuvo.mjs",
28
+ "bundle:mcp": "node scripts/bundle.mjs --entry bin/acuvo-mcp.mjs --out dist/acuvo-mcp.mjs",
29
+ "machine": "node scripts/machine.mjs status",
30
+ "machine:stop": "node scripts/machine.mjs stop"
31
+ },
32
+ "dependencies": {},
33
+ "devDependencies": {},
34
+ "license": "FSL-1.1-ALv2",
35
+ "files": [
36
+ "bin/",
37
+ "lib/",
38
+ "skills/",
39
+ "scripts/",
40
+ "README.md",
41
+ "LICENSE",
42
+ "CHANGELOG.md",
43
+ "ENTERPRISE.md",
44
+ "ROADMAP.md"
45
+ ],
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/xxiautomate-star/acuvo-code.git"
49
+ },
50
+ "homepage": "https://acuvo.xxiautomate.com",
51
+ "bugs": {
52
+ "email": "roman@xxiautomate.com"
53
+ },
54
+ "author": "XXIautomate (Roman Puglielli)",
55
+ "keywords": [
56
+ "ai",
57
+ "cli",
58
+ "coding-agent",
59
+ "agent",
60
+ "terminal"
61
+ ]
62
+ }