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,168 @@
1
+ /**
2
+ * ── ⚠️⚠️ A CLONED REPOSITORY COULD RUN CODE, AND IT WAS REPORTED AS A ✔ ──────
3
+ *
4
+ * PROVEN 2026-08-13 in a scratch workspace, not argued:
5
+ *
6
+ * .acuvo/acceptance.json → {"criteria":[{"command":"node payload.js"}]}
7
+ * payload.js → writeFileSync('PWNED.txt', …)
8
+ *
9
+ * $ acuvo "say hello, change nothing"
10
+ * ✔ MET — `node payload.js` exited 0
11
+ * $ cat PWNED.txt
12
+ * a cloned repo executed this without being asked
13
+ *
14
+ * No prompt. No record. And the CLI printed the payload's execution as a
15
+ * PASSING CHECK — the most reassuring possible rendering of an attack.
16
+ *
17
+ * ⭐ This is the SAME hole `mcp-consent.mjs` was written to close, one file
18
+ * over. Its header says it exactly: *"Cloning an untrusted repository and typing
19
+ * `acuvo` used to execute a binary that repository chose — no prompt, no record,
20
+ * full environment. Consent, not a blocklist: a committed `.mcp.json` is a
21
+ * legitimate thing to want. What was missing is that nobody ever agreed to it."*
22
+ * Every word applies here, and `acceptance.json` was simply missed.
23
+ *
24
+ * ⚠️ THE ALLOWLIST IS NOT THE ANSWER, THOUGH IT LOOKS LIKE ONE. `command.mjs`
25
+ * only permits vetted binaries, so this cannot run `curl`. It can run `node`,
26
+ * and `node <a file in the repo>` is arbitrary code — the allowlist bounds the
27
+ * BINARY, never the program. The probe above used nothing but `node`.
28
+ *
29
+ * ── ⭐ CONSENT ON AUTHORSHIP, WHICH IS WHY THIS DOES NOT BECOME A NAG ────────
30
+ *
31
+ * A prompt everybody clicks through is worse than no prompt, because it
32
+ * launders the decision. So the ordinary path must never ask:
33
+ *
34
+ * · the agent calls `declare_acceptance` during YOUR run → that is your own
35
+ * session authoring the file, so trust is recorded at the moment of writing
36
+ * and the next run recognises it silently.
37
+ * · a file that arrived any other way — a clone, a pull, a colleague — is
38
+ * unrecognised, and THAT is the only case that asks.
39
+ *
40
+ * The result: a person who uses this feature normally never sees a question,
41
+ * and the question they do see means something specific.
42
+ *
43
+ * ── ⚠️ AND WHEN IT IS NOT TRUSTED, THE CRITERIA ARE IGNORED, NOT FATAL ──────
44
+ *
45
+ * Refusing to run at all would make a stray file a denial of service, and would
46
+ * teach people to delete the safety feature. Unrecognised criteria are simply
47
+ * not loaded: the run proceeds exactly as it would in a workspace with no
48
+ * acceptance file, and says so once. Nothing untrusted is executed, and nothing
49
+ * legitimate is blocked.
50
+ */
51
+
52
+ import { fingerprint as hashOf, loadTrust, isTrusted, recordTrust, trustStorePath } from './mcp-consent.mjs';
53
+
54
+ /** `ACUVO_TRUST_ACCEPTANCE=1` — the non-interactive escape, mirroring MCP's. */
55
+ export const TRUST_ENV = 'ACUVO_TRUST_ACCEPTANCE';
56
+
57
+ /**
58
+ * What is being approved: the COMMANDS, not the file.
59
+ *
60
+ * ⚠️ Reusing `mcp-consent.mjs`'s hash by shaping criteria into the same
61
+ * `{name, command, args}` form it canonicalises. Reformatting the JSON or
62
+ * editing a `why` must not re-prompt — a nag people learn to click through is
63
+ * the failure mode — while changing a COMMAND must. The `kind` prefix keeps an
64
+ * acceptance fingerprint from ever colliding with an MCP one in the shared
65
+ * store, so approving a server can never silently approve a criterion.
66
+ */
67
+ export function acceptanceFingerprint(criteria) {
68
+ const shaped = (criteria ?? [])
69
+ .map((c) => ({ name: 'acceptance', command: String(c?.command ?? ''), args: [] }))
70
+ .sort((a, b) => a.command.localeCompare(b.command));
71
+ return `acceptance:${hashOf(shaped)}`;
72
+ }
73
+
74
+ /** The text a person reads before deciding. Exported so a test can assert it names the command. */
75
+ export function describeCriteria(criteria, { root = '' } = {}) {
76
+ const lines = [
77
+ '',
78
+ ` ⚠️ ${root || 'this workspace'} ships ${criteria.length} acceptance criterion${criteria.length === 1 ? '' : 's'}`,
79
+ ' that nobody in this workspace has approved. Each one is a COMMAND that will be',
80
+ ' run on your machine, and a passing run is reported as a ✔.',
81
+ '',
82
+ ];
83
+ for (const c of criteria) {
84
+ lines.push(` $ ${c?.command ?? '(no command)'}`);
85
+ if (c?.why) lines.push(` (${c.why})`);
86
+ }
87
+ lines.push('');
88
+ return lines.join('\n');
89
+ }
90
+
91
+ /**
92
+ * May these criteria be executed?
93
+ *
94
+ * @param {Array<{command?: string, why?: string}>} criteria
95
+ * @param {object} opts
96
+ * @param {string} [opts.root]
97
+ * @param {null | ((q: string) => Promise<string|null>)} [opts.ask]
98
+ * @param {boolean} [opts.isInteractive]
99
+ * @returns {Promise<{allowed: boolean, reason: string, fingerprint: string, remember?: boolean}>}
100
+ */
101
+ export async function checkAcceptanceConsent(criteria, {
102
+ root = '',
103
+ ask = null,
104
+ isInteractive = false,
105
+ env = process.env,
106
+ home = undefined,
107
+ } = {}) {
108
+ const list = Array.isArray(criteria) ? criteria.filter((c) => c && typeof c.command === 'string' && c.command.trim()) : [];
109
+ const fp = acceptanceFingerprint(list);
110
+ const base = { fingerprint: fp };
111
+
112
+ // Nothing to run is nothing to approve — and must not print a question.
113
+ if (list.length === 0) return { ...base, allowed: true, reason: 'no criteria' };
114
+
115
+ if (String(env[TRUST_ENV] ?? '').trim() === '1') {
116
+ return { ...base, allowed: true, reason: `${TRUST_ENV}=1 was set, so the criteria were accepted without asking` };
117
+ }
118
+
119
+ const store = loadTrust({ env, ...(home === undefined ? {} : { home }) });
120
+ if (isTrusted(fp, store)) {
121
+ return { ...base, allowed: true, reason: 'these exact criteria were approved in this workspace before' };
122
+ }
123
+
124
+ /**
125
+ * ⚠️ FAIL CLOSED. In CI, a pipe or a task runner there is nobody to ask, and
126
+ * "nobody objected" is not consent. The run continues with no criteria rather
127
+ * than executing commands nobody agreed to — see the header for why this is
128
+ * not fatal.
129
+ */
130
+ if (!isInteractive || typeof ask !== 'function') {
131
+ return {
132
+ ...base,
133
+ allowed: false,
134
+ reason: 'this workspace ships acceptance criteria that have not been approved, and there is no terminal here to ask.\n'
135
+ + ` They were NOT run. Approve them interactively once, or set ${TRUST_ENV}=1 if you have read them and this is your own repository.`,
136
+ };
137
+ }
138
+
139
+ const answer = String(await ask(' Run these commands? [y/N] ') ?? '').trim().toLowerCase();
140
+ if (answer !== 'y' && answer !== 'yes') {
141
+ return { ...base, allowed: false, reason: 'declined — the criteria were not run' };
142
+ }
143
+ return { ...base, allowed: true, remember: true, reason: 'approved for this workspace' };
144
+ }
145
+
146
+ /**
147
+ * Record consent because THIS session authored the criteria.
148
+ *
149
+ * ⭐ Called when `declare_acceptance` writes the file. The user's own run
150
+ * created it, so asking about it afterwards would be asking them to approve
151
+ * their own instruction — the definition of a prompt people learn to ignore.
152
+ *
153
+ * ⚠️ Never fatal, exactly like `recordTrust`: failing to remember means asking
154
+ * once next time, which is annoying and safe.
155
+ */
156
+ export function trustAuthoredCriteria(criteria, { root = '', env = process.env, home = undefined } = {}) {
157
+ const list = Array.isArray(criteria) ? criteria.filter((c) => c && typeof c.command === 'string' && c.command.trim()) : [];
158
+ if (list.length === 0) return { ok: true, already: true };
159
+ const fp = acceptanceFingerprint(list);
160
+ return recordTrust(fp, {
161
+ root,
162
+ servers: list.map((c) => ({ name: 'acceptance', command: c.command, args: [] })),
163
+ env,
164
+ ...(home === undefined ? {} : { home }),
165
+ });
166
+ }
167
+
168
+ export { trustStorePath };