@staix/agent-mem 0.12.0 → 0.14.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 (43) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +70 -44
  3. package/package.json +5 -2
  4. package/src/cli-agent-reads.ts +105 -0
  5. package/src/cli.ts +219 -56
  6. package/src/commands-table.ts +10 -9
  7. package/src/index.ts +1 -0
  8. package/src/ingest.ts +21 -4
  9. package/src/mcp.ts +8 -8
  10. package/src/memory/candidates.ts +15 -13
  11. package/src/memory/capacity-split.ts +4 -1
  12. package/src/memory/capacity.ts +48 -4
  13. package/src/memory/catalog.ts +4 -4
  14. package/src/memory/cli-attribution.ts +23 -0
  15. package/src/memory/commands.ts +63 -11
  16. package/src/memory/consolidate.ts +3 -2
  17. package/src/memory/decay-review.ts +4 -4
  18. package/src/memory/doctor.ts +36 -7
  19. package/src/memory/generation-index.ts +56 -11
  20. package/src/memory/generation.ts +73 -7
  21. package/src/memory/metrics.ts +20 -8
  22. package/src/memory/migrate.ts +31 -0
  23. package/src/memory/mutation.ts +24 -4
  24. package/src/memory/note-add.ts +4 -4
  25. package/src/memory/prune-publisher-lineage.ts +139 -0
  26. package/src/memory/prune.ts +28 -1
  27. package/src/memory/public-prune-activation.ts +260 -0
  28. package/src/memory/public-prune-advance.ts +149 -0
  29. package/src/memory/public-prune-input-chain.ts +73 -0
  30. package/src/memory/public-prune-inputs.ts +455 -0
  31. package/src/memory/public-prune-view.ts +139 -0
  32. package/src/memory/public-prune-witness.ts +168 -0
  33. package/src/memory/reader-lease.ts +46 -34
  34. package/src/memory/shard-migration.ts +42 -3
  35. package/src/memory/shard-sync-format.ts +40 -2
  36. package/src/memory/shard-sync-graph.ts +87 -0
  37. package/src/memory/shard-sync.ts +341 -53
  38. package/src/memory/shard.ts +50 -9
  39. package/src/memory/store.ts +51 -12
  40. package/src/memory/style.ts +3 -0
  41. package/src/memory/sync.ts +141 -5
  42. package/src/memory/vault.ts +31 -0
  43. package/src/setup.ts +59 -31
package/src/setup.ts CHANGED
@@ -1,22 +1,28 @@
1
+ import { cliAttribution } from "./memory/cli-attribution.ts";
2
+ import { configuredPolicy } from "./memory/config.ts";
1
3
  import { SETUP_CONFIG_BYTES, updateTomlArray } from "./setup-toml.ts";
2
4
  import { managedAfter, mcpRecord, putMcpRecord, restoreOwned, sameImage, type ConfigImage } from "./setup-owned.ts";
3
5
  import { spawnSync } from "node:child_process";
4
6
  import { createHash, randomUUID } from "node:crypto";
5
7
  import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, renameSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
6
8
  import { homedir, tmpdir } from "node:os";
7
- import { createInterface } from "node:readline/promises";
8
9
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
9
10
  import { fileURLToPath } from "node:url";
10
11
  import { isDeepStrictEqual } from "node:util";
11
12
  import { HOOK_EVENTS } from "./hook-contract.ts";
12
13
  import { memoryHome } from "./memory/home.ts";
13
- import { label, PLAIN, type Style } from "./memory/style.ts";
14
+ import { label, PLAIN, singleLine, type Style } from "./memory/style.ts";
14
15
  import { canonicalPath, memoryGitEnv, memoryGitSync, processLiveness, processSignature, realPath } from "./memory/platform.ts";
15
16
 
16
17
  export const SETUP_TARGETS = ["claude", "codex", "kimi", "pi", "qwen", "jcode", "agy"] as const;
17
18
  /** Workspace-scoped Antigravity setup is always selected explicitly. */
18
19
  export const SETUP_ALL_TARGETS = SETUP_TARGETS.filter(target => target !== "agy");
19
20
  export type SetupTarget = typeof SETUP_TARGETS[number];
21
+ // Only recorded layout verification versions belong here; null means no version was recorded.
22
+ const SETUP_VERIFIED_VERSIONS: Readonly<Record<SetupTarget, string | null>> = {
23
+ claude: null, codex: "0.165.0", kimi: null, pi: null, qwen: "0.26.1", jcode: "0.99.1", agy: null,
24
+ };
25
+ interface ClientVersion { output: string | null; verified: string | null; matches: boolean | null }
20
26
  type Env = Readonly<Record<string, string | undefined>>;
21
27
  type Image = ConfigImage;
22
28
  interface PlannedHook { event: string; command: string; timeout: number }
@@ -25,7 +31,7 @@ interface DirectoryIdentity { dev: number; ino: number }
25
31
  interface Receipt { createdDirectory?: DirectoryIdentity; version: 1; target: SetupTarget; intent: string; phase: "prepared" | "applied"; changes: Change[] }
26
32
  export interface SetupContext {
27
33
  env?: Env; bun?: string; entry?: string; stdinTTY?: boolean; cwd?: string;
28
- /** Trusted confirmation seam; ordinary CLI use always asks the person through stdin. */
34
+ /** @deprecated Retained for source compatibility; explicit apply/undo modes do not prompt. */
29
35
  confirm?: (plan: SetupPlan) => Promise<boolean>;
30
36
  /** Host-only race fixture, called after temporary-file fsync and before the final comparison/rename. */
31
37
  beforePublish?: (file: string) => void;
@@ -34,7 +40,7 @@ export interface SetupContext {
34
40
  }
35
41
  export interface SetupPlan {
36
42
  targets: SetupTarget[]; mode: "preview" | "apply" | "undo";
37
- actions: { target: SetupTarget; files: string[]; receipt: string; cwd?: string; registration: string[] | null; lifecycle: string; available: boolean; hooks: PlannedHook[]; mcpChange: "add" | "replace" | "restore" | "unchanged"; commands: string[][]; state: string }[];
43
+ actions: { target: SetupTarget; version?: ClientVersion; files: string[]; receipt: string; cwd?: string; registration: string[] | null; lifecycle: string; available: boolean; hooks: PlannedHook[]; mcpChange: "add" | "replace" | "restore" | "unchanged"; commands: string[][]; state: string }[];
38
44
  notes: string[];
39
45
  }
40
46
  const MAX = SETUP_CONFIG_BYTES, JOURNAL_MAX = 12 * MAX;
@@ -163,7 +169,7 @@ function assertJcodeRuntime(c: ReturnType<typeof context>): void {
163
169
  assertJcodeHome(c.env);
164
170
  if ([c.bun, c.entry].some(value => value.includes("${"))) throw Error("Jcode interpolates environment placeholders in MCP command paths; setup cannot register this runtime exactly");
165
171
  }
166
- function intent(c: ReturnType<typeof context>, target: SetupTarget): string { return digest([1, target, c.paths[target], c.bun, c.entry, ...(target === "agy" ? [c.workspace!.root] : [])]); }
172
+ function intent(c: ReturnType<typeof context>, target: SetupTarget): string { return digest([1, target, c.paths[target], c.bun, c.entry, ...(["codex", "kimi", "qwen"].includes(target) ? ["capture-json-v1"] : []), ...(target === "agy" ? [c.workspace!.root] : [])]); }
167
173
  function journal(c: ReturnType<typeof context>, target: SetupTarget): string { return join(c.state, target === "agy" ? `agy-${digest(c.workspace!.root)}.json` : `${target}.json`); }
168
174
  function receipt(c: ReturnType<typeof context>, target: SetupTarget, requireRuntime = false): Receipt | undefined {
169
175
  const raw = read(journal(c, target), JOURNAL_MAX); if (raw.bytes === null) return;
@@ -186,7 +192,7 @@ function plannedHooks(c: ReturnType<typeof context>, target: SetupTarget): Plann
186
192
  if (target === "jcode" || target === "agy") return [];
187
193
  const launch = `${quote(c.bun)} --smol ${quote(c.entry)}`;
188
194
  return target === "claude" ? HOOK_EVENTS.map(event => ({ event, command: `${launch} hook ${event}`, timeout: 1 }))
189
- : [{ event: target === "pi" ? "session_shutdown" : "SessionEnd", command: `${launch} ingest ${target}`, timeout: target === "pi" ? 2.5 : 3 }];
195
+ : [{ event: target === "pi" ? "session_shutdown" : "SessionEnd", command: `${launch} ingest ${target}${target === "pi" ? "" : " --json"}`, timeout: target === "pi" ? 2.5 : 3 }];
190
196
  }
191
197
  const lifecycle = (target: SetupTarget) => target === "jcode" || target === "agy" ? "MCP only; native capture unavailable" : target === "claude" ? "seven bounded native hooks" : target === "pi" ? "session_shutdown incremental ingest" : "SessionEnd incremental ingest";
192
198
  export function planSetup(target: string, mode: SetupPlan["mode"] = "preview", options: SetupContext = {}): SetupPlan {
@@ -219,8 +225,43 @@ export function planSetup(target: string, mode: SetupPlan["mode"] = "preview", o
219
225
  const hasAmem = target === "jcode" || target === "agy" ? mcpRecord(target, original) !== undefined : Object.hasOwn(record(document[target === "codex" ? "mcp_servers" : "mcpServers"] ?? {}), "amem");
220
226
  const unchanged = !!prior && prior.phase === "applied" && prior.intent === intent(c, target) && prior.changes.every((change, index) => { try { const live = read(change.path); restoreChange(prior, change, index, live); return managedAfter(target, index, change.before, change.after, live); } catch { return false; } });
221
227
  const add = command(target, c.bun, c.entry), remove = [target, "mcp", "remove", ...(target === "claude" || target === "qwen" ? ["--scope", "user"] : []), "amem"];
222
- return { target, available: available(c, target, options), files: c.paths[target], receipt: journal(c, target), ...(target === "agy" ? { cwd: c.workspace!.root } : {}), registration: mode === "undo" ? null : add, lifecycle: mode === "undo" ? "restore original MCP/hook files" : lifecycle(target), hooks: mode === "undo" || unchanged ? [] : plannedHooks(c, target), mcpChange: mode === "undo" ? prior ? "restore" : "unchanged" : unchanged ? "unchanged" : hasAmem ? "replace" : "add", commands: mode === "undo" || prior?.intent === intent(c, target) || !add || !available(c, target, options) ? [] : [...(hasAmem ? [remove] : []), add], state: prior && prior.intent !== intent(c, target) ? "runtime changed; managed entries will update" : prior?.phase ?? "not installed" };
223
- }), notes: [...pluginNotes, ...(/\/[^/]*\d+\.\d+[^/]*\/(?:bin\/)?bun$/.test(c.bun) ? ["this bun path is version-specific; rerun setup --apply after changing Bun"] : []), "Preview only unless --apply or --undo is confirmed in a person's terminal.", "Native registration commands run in a private staging HOME; only the listed configuration files are published.", "Original file bytes restore exactly when untouched; otherwise undo preserves CLI-owned changes and restores only managed entries, refusing edits to those entries.", "No AGENTS.md, CLAUDE.md or rule files are edited.", `A private ${join(c.state, "setup.lock")} serializes mutations; temporary ${join(realPath(tmpdir()), "amem-setup-*")} staging homes are removed.`, "Codex requires its native /hooks review before newly registered hooks run; setup does not grant hook trust. Runtime registrations use the PATH-resolved absolute Bun path without resolving its stable symlink; setup all reports and skips missing clients."] };
228
+ const installed = available(c, target, options), version = installed && target !== "agy" && mode !== "undo" ? probeClientVersion(c, target, options) : undefined;
229
+ if (version?.matches === false) pluginNotes.push(`WARNING: ${target} version ${version.output}; layout verified on ${version.verified}. Apply remains guarded by native configuration validation.`);
230
+ return { target, available: installed, ...(version ? { version } : {}), files: c.paths[target], receipt: journal(c, target), ...(target === "agy" ? { cwd: c.workspace!.root } : {}), registration: mode === "undo" ? null : add, lifecycle: mode === "undo" ? "restore original MCP/hook files" : lifecycle(target), hooks: mode === "undo" || unchanged ? [] : plannedHooks(c, target), mcpChange: mode === "undo" ? prior ? "restore" : "unchanged" : unchanged ? "unchanged" : hasAmem ? "replace" : "add", commands: mode === "undo" || prior?.intent === intent(c, target) || !add || !installed ? [] : [...(hasAmem ? [remove] : []), add], state: prior && prior.intent !== intent(c, target) ? "runtime changed; managed entries will update" : prior?.phase ?? "not installed" };
231
+ }), notes: [...pluginNotes, ...(/\/[^/]*\d+\.\d+[^/]*\/(?:bin\/)?bun$/.test(c.bun) ? ["this bun path is version-specific; rerun setup --apply after changing Bun"] : []), "Preview only unless --apply or --undo is passed; explicit modes apply without a prompt.", "Native registration commands run in a private staging HOME; only the listed configuration files are published.", "Original file bytes restore exactly when untouched; otherwise undo preserves CLI-owned changes and restores only managed entries, refusing edits to those entries.", "No AGENTS.md, CLAUDE.md or rule files are edited.", `A private ${join(c.state, "setup.lock")} serializes mutations; temporary ${join(realPath(tmpdir()), "amem-setup-*")} staging homes are removed.`, "Codex requires its native /hooks review before newly registered hooks run; setup does not grant hook trust. Runtime registrations use the PATH-resolved absolute Bun path without resolving its stable symlink; setup all reports and skips missing clients."] };
232
+ }
233
+ /** Native probes and registration share one private HOME, cwd and configuration layout. */
234
+ function stagingEnvironment(c: ReturnType<typeof context>, target: SetupTarget, options: SetupContext, staging: string) {
235
+ const env: Record<string, string | undefined> = { ...c.env, HOME: staging, AMEM_HOME: join(staging, "memory"), AGENTHUB_HOME: join(staging, "hub"), CODEX_HOME: join(staging, ".codex"), KIMI_CODE_HOME: join(staging, ".kimi-code"), PI_CODING_AGENT_DIR: join(staging, ".pi", "agent"), QWEN_HOME: join(staging, ".qwen"), JCODE_HOME: join(staging, ".jcode") };
236
+ Object.assign(env, { XDG_CONFIG_HOME: join(staging, ".config"), XDG_DATA_HOME: join(staging, ".local", "share"), XDG_CACHE_HOME: join(staging, ".cache"), XDG_STATE_HOME: join(staging, ".local", "state"), TMPDIR: join(staging, "tmp"), TMP: join(staging, "tmp"), TEMP: join(staging, "tmp") });
237
+ env.QWEN_RUNTIME_DIR = join(staging, "qwen-runtime");
238
+ env.QWEN_CODE_SYSTEM_SETTINGS_PATH = join(staging, "qwen-system-settings.json");
239
+ env.QWEN_CODE_SYSTEM_DEFAULTS_PATH = join(staging, "qwen-system-defaults.json");
240
+ mkdirSync(env.TMPDIR!, { mode: 0o700 });
241
+ if (c.env.CLAUDE_CONFIG_DIR?.trim()) env.CLAUDE_CONFIG_DIR = join(staging, ".claude"); else delete env.CLAUDE_CONFIG_DIR;
242
+ // The staging workspace is a trusted private binding, not a Git repository or the real caller cwd.
243
+ const staged = context({ ...options, env, cwd: staging }, target === "agy" ? { root: staging, ignored: true, assertCurrent() {} } : undefined);
244
+ // Codex refuses an explicit CODEX_HOME that does not exist, even for mcp add.
245
+ mkdirSync(dirname(staged.paths[target][0]!), { recursive: true, mode: 0o700 });
246
+ return { env, staged };
247
+ }
248
+ /** Advisory only: never queried by apply/undo or persisted in the ownership receipt. */
249
+ function probeClientVersion(c: ReturnType<typeof context>, target: SetupTarget, options: SetupContext): ClientVersion {
250
+ const verified = SETUP_VERIFIED_VERSIONS[target];
251
+ let output: string | null = null;
252
+ // Like the help probe, trusted native fixtures do not invoke installed executables.
253
+ if (!options.native) {
254
+ let staging: string | undefined;
255
+ try {
256
+ staging = mkdtempSync(join(realPath(tmpdir()), "amem-setup-"));
257
+ const { env } = stagingEnvironment(c, target, options, staging);
258
+ const result = spawnSync(target, ["--version"], { cwd: staging, env: { ...env }, encoding: "utf8", timeout: 1000, maxBuffer: 4096, stdio: ["ignore", "pipe", "pipe"] });
259
+ if (result.status === 0 && !result.error) output = Array.from(singleLine(result.stdout).trim()).slice(0, 256).join("") || null;
260
+ } catch { /* An absent, failed or timed-out native version never prevents setup. */ }
261
+ finally { if (staging) try { rmSync(staging, { recursive: true, force: true }); } catch { output = null; } }
262
+ }
263
+ const observed = output?.match(/(?:^|\s)v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)(?=\s|$)/)?.[1];
264
+ return { output, verified, matches: observed === undefined || verified === null ? null : observed === verified };
224
265
  }
225
266
  function verifyNativeMcp(target: SetupTarget, env: Env, cwd: string): void {
226
267
  // In older free-form Pi CLIs, an unknown subcommand can be treated as a model prompt.
@@ -268,17 +309,7 @@ function prepare(c: ReturnType<typeof context>, target: SetupTarget, options: Se
268
309
  const changes: Change[] = c.paths[target].map((path, i) => ({ key: `${target}:${i}`, path, before: restored[i]!, after: restored[i]!, start: expected[i]! }));
269
310
  const staging = mkdtempSync(join(realPath(tmpdir()), "amem-setup-"));
270
311
  try {
271
- const env: Record<string, string | undefined> = { ...c.env, HOME: staging, AMEM_HOME: join(staging, "memory"), AGENTHUB_HOME: join(staging, "hub"), CODEX_HOME: join(staging, ".codex"), KIMI_CODE_HOME: join(staging, ".kimi-code"), PI_CODING_AGENT_DIR: join(staging, ".pi", "agent"), QWEN_HOME: join(staging, ".qwen"), JCODE_HOME: join(staging, ".jcode") };
272
- Object.assign(env, { XDG_CONFIG_HOME: join(staging, ".config"), XDG_DATA_HOME: join(staging, ".local", "share"), XDG_CACHE_HOME: join(staging, ".cache"), XDG_STATE_HOME: join(staging, ".local", "state"), TMPDIR: join(staging, "tmp"), TMP: join(staging, "tmp"), TEMP: join(staging, "tmp") });
273
- env.QWEN_RUNTIME_DIR = join(staging, "qwen-runtime");
274
- env.QWEN_CODE_SYSTEM_SETTINGS_PATH = join(staging, "qwen-system-settings.json");
275
- env.QWEN_CODE_SYSTEM_DEFAULTS_PATH = join(staging, "qwen-system-defaults.json");
276
- mkdirSync(env.TMPDIR!, { mode: 0o700 });
277
- if (c.env.CLAUDE_CONFIG_DIR?.trim()) env.CLAUDE_CONFIG_DIR = join(staging, ".claude"); else delete env.CLAUDE_CONFIG_DIR;
278
- // The staging workspace is a trusted private binding, not a Git repository or the real caller cwd.
279
- const staged = context({ ...options, env, cwd: staging }, target === "agy" ? { root: staging, ignored: true, assertCurrent() {} } : undefined);
280
- // Codex refuses an explicit CODEX_HOME that does not exist, even for mcp add.
281
- mkdirSync(dirname(staged.paths[target][0]!), { recursive: true, mode: 0o700 });
312
+ const { env, staged } = stagingEnvironment(c, target, options, staging);
282
313
  for (let i = 0; i < changes.length; i++) if (expected[i]!.bytes !== null) write(staged.paths[target][i]!, expected[i]!);
283
314
  const argv = command(target, c.bun, c.entry);
284
315
  if (argv && prior?.intent === intent(c, target)) {
@@ -335,7 +366,7 @@ function prepare(c: ReturnType<typeof context>, target: SetupTarget, options: Se
335
366
  servers.amem = { command: c.bun, args: ["--smol", c.entry, "mcp"] }; document.mcpServers = servers;
336
367
  changes[0]!.after = objectImage(document, changes[0]!.before);
337
368
  }
338
- const capture = (kind: string) => `${quote(c.bun)} --smol ${quote(c.entry)} ingest ${kind}`;
369
+ const capture = (kind: string) => `${quote(c.bun)} --smol ${quote(c.entry)} ingest ${kind} --json`;
339
370
  if (target === "claude") changes[1]!.after = hookDocument(changes[1]!.before, HOOK_EVENTS, event => `${quote(c.bun)} --smol ${quote(c.entry)} hook ${event}`);
340
371
  if (target === "codex") changes[1]!.after = hookDocument(changes[1]!.before, ["SessionEnd"], () => capture("codex"), 3);
341
372
  // Qwen's user MCP and hooks share settings.json: publish and journal one combined image.
@@ -391,7 +422,7 @@ function contextForPlan(plan: SetupPlan, options: SetupContext) {
391
422
  function publishGuard(c: ReturnType<typeof context>, target: SetupTarget, options: SetupContext) {
392
423
  return target === "agy" ? (file: string) => { options.beforePublish?.(file); c.workspace!.assertCurrent(); } : options.beforePublish;
393
424
  }
394
- /** Caller enforces person markers/TTY and confirmation; CLI-owned unrelated fields remain live. */
425
+ /** CLI-owned unrelated fields remain live. */
395
426
  function applyUnlocked(plan: SetupPlan, options: SetupContext = {}): SetupApplyResult {
396
427
  const c = contextForPlan(plan, options), skipped: { target: SetupTarget; reason: string }[] = [];
397
428
  const targets = plan.targets.filter(target => { if (available(c, target, options)) return true; if (plan.targets.length === 1) throw Error(`${target} CLI is not installed`); skipped.push({ target, reason: "CLI not installed" }); return false; });
@@ -461,6 +492,10 @@ export function setupPlanText(plan: SetupPlan, s: Style = PLAIN): string {
461
492
  const state = action.mcpChange === "unchanged" ? "ok" : "attention";
462
493
  const what = action.mcpChange === "unchanged" ? plan.mode === "undo" ? "no setup receipt; nothing to restore" : "already set up; no change" : action.mcpChange === "restore" ? `restore original MCP/hook files${action.available ? "" : " (CLI not installed)"}` : `MCP ${action.mcpChange}`;
463
494
  lines.push(`${label(s, state)} ${s.bold(action.target)}: ${what} ${s.dim(`(${action.state})`)}`);
495
+ if (action.version) {
496
+ const reported = action.version.output === null ? "unknown" : action.version.verified && action.version.matches === null ? `unrecognised (${action.version.output})` : action.version.output;
497
+ lines.push(` version ${reported}; ${action.version.verified ? `verified ${action.version.verified}` : "unverified (no verified version recorded)"}`);
498
+ }
464
499
  for (const file of action.files) lines.push(` file ${file}`);
465
500
  if (action.cwd) lines.push(` cwd ${action.cwd}`);
466
501
  lines.push(` receipt ${action.receipt}`);
@@ -473,7 +508,7 @@ export function setupPlanText(plan: SetupPlan, s: Style = PLAIN): string {
473
508
  if (skipped.length) lines.push("", `Skipped clients: ${skipped.map(action => action.target).join(", ")} (not installed)`);
474
509
  if (warnings.length) lines.push("", "Warnings:", ...warnings.map(note => ` - ${s.attention(note)}`));
475
510
  lines.push("", "Notes:", ...notes.map(note => ` - ${note}`));
476
- if (plan.mode === "preview") lines.push("", `Next: amem setup ${plan.targets.length === 1 ? plan.targets[0] : "all"} --apply (asks before writing); --json prints the full plan`);
511
+ if (plan.mode === "preview") lines.push("", `Next: amem setup ${plan.targets.length === 1 ? plan.targets[0] : "all"} --apply writes without a prompt; --json prints the full plan`);
477
512
  return lines.join("\n");
478
513
  }
479
514
 
@@ -483,20 +518,13 @@ function setupResultText(result: SetupApplyResult | { restored: SetupTarget[] },
483
518
  ...(result.skipped ?? []).map(item => `${label(s, "skipped")} ${item.target}: ${item.reason}`)].join("\n");
484
519
  }
485
520
 
486
- /** What the default confirmation prints before its prompt: the raw plan under `--json`, the readable plan otherwise. */
521
+ /** Compatibility formatter for confirmation-style callers: the raw plan under `--json`, the readable plan otherwise. */
487
522
  export function setupConfirmationText(plan: SetupPlan, json: boolean, s: Style = PLAIN): string { return json ? JSON.stringify(plan, null, 2) : setupPlanText(plan, s); }
488
523
 
489
524
  export async function runSetup(target: string, mode: SetupPlan["mode"], options: SetupContext & { json?: boolean; style?: Style } = {}): Promise<string> {
490
525
  const plan = planSetup(target, mode, options), s = options.json ? PLAIN : options.style ?? PLAIN;
526
+ if (cliAttribution()?.actor === "agent" && !configuredPolicy(memoryHome(options.env ?? process.env), options.cwd ?? process.cwd()).screenStandalone(JSON.parse(JSON.stringify(plan)))) throw Error("Memory operation unavailable or refused; no partial result is shown.");
491
527
  if (mode === "preview") return options.json ? JSON.stringify(plan, null, 2) : setupPlanText(plan, s);
492
- if (!(options.stdinTTY ?? process.stdin.isTTY)) throw Error("setup --apply/--undo require a terminal on stdin");
493
- const confirm = options.confirm ?? (async (shown: SetupPlan) => {
494
- process.stdout.write(setupConfirmationText(shown, options.json === true, s) + "\n\n");
495
- const terminal = createInterface({ input: process.stdin, output: process.stdout });
496
- try { return (await terminal.question(`Type yes to ${mode} the listed configuration changes: `)).trim() === "yes"; }
497
- finally { terminal.close(); }
498
- });
499
- if (!await confirm(plan)) return options.json ? JSON.stringify({ cancelled: true }) : "setup cancelled; nothing changed";
500
528
  const result = mode === "apply" ? applySetup(plan, options) : undoSetup(plan, options);
501
529
  return options.json ? JSON.stringify(result, null, 2) : setupResultText(result, s);
502
530
  }