\"] [--dry-run] [--root D] | " +
- "amico fleet shard-watch [--clients a,b] [--port ] [--alert-min ] [--db ] [--post ] [--dry-run]";
+ "amico fleet shard-watch [--clients a,b] [--port ] [--alert-min ] [--db ] [--post ] [--dry-run] | " +
+ "amico fleet shard-rehome --client-home [--db ] [--apply]";
/** Optional injection surface for the fleet verb's sub-verbs — the projection
* status's hermetic seam (publisher subprocess, entitlement file, checkout
@@ -625,6 +627,7 @@ export function fleetVerb(argv: string[], deps: FleetVerbDeps = {}): VerbResult
// NOTE: `enroll` is async (real HTTP) and is dispatched at the registry seam
// (verbs.ts `run`), NOT here — so this synchronous router stays synchronous
// and its pinned VerbResult contract (and callers) are untouched.
+ if (sub === "shard-rehome") return shardRehome(rest);
return {
json: { verb: "fleet", error: `unknown subcommand ${sub ? `"${sub}"` : "(none)"}`, usage: USAGE },
code: 64,
diff --git a/packages/amico-run/src/mcp_serve.ts b/packages/amico-run/src/mcp_serve.ts
index 215c56bc1..93e094329 100644
--- a/packages/amico-run/src/mcp_serve.ts
+++ b/packages/amico-run/src/mcp_serve.ts
@@ -31,13 +31,22 @@ export function verbToMcpTool(v: Verb): McpToolDescriptor {
};
}
-export function listMcpTools(): McpToolDescriptor[] {
- return SPINE_VERBS.map(verbToMcpTool);
+/** Is this verb published on the MCP (agent) surface? Verbs default IN; a
+ * verb marked `mcpDefault: false` is opt-out unless named in
+ * AMICO_MCP_EXPOSE (comma-separated verb names). */
+export function isMcpExposed(v: Verb, env: NodeJS.ProcessEnv = process.env): boolean {
+ if (v.mcpDefault !== false) return true;
+ const optIn = (env.AMICO_MCP_EXPOSE ?? "").split(",").map((s) => s.trim()).filter(Boolean);
+ return optIn.includes(v.name);
+}
+
+export function listMcpTools(env: NodeJS.ProcessEnv = process.env): McpToolDescriptor[] {
+ return SPINE_VERBS.filter((v) => isMcpExposed(v, env)).map(verbToMcpTool);
}
/** tools/call handler — dispatches to the same Verb.run the CLI uses. */
-export async function callMcpTool(name: string, argv: string[]): Promise {
- const verb = SPINE_VERBS.find((v) => `amico_${v.name}` === name);
+export async function callMcpTool(name: string, argv: string[], env: NodeJS.ProcessEnv = process.env): Promise {
+ const verb = SPINE_VERBS.find((v) => `amico_${v.name}` === name && isMcpExposed(v, env));
if (!verb) throw new Error(`unknown tool ${name}`);
if (argv[0] !== undefined && verb.cliOnlySubcommands?.includes(argv[0]))
return { verb: verb.name, subcommand: argv[0], ok: false, error: `\`amico ${verb.name} ${argv[0]}\` is CLI-only — not exposed through MCP` };
diff --git a/packages/amico-run/src/shard_rehome.ts b/packages/amico-run/src/shard_rehome.ts
new file mode 100644
index 000000000..fd577807e
--- /dev/null
+++ b/packages/amico-run/src/shard_rehome.ts
@@ -0,0 +1,199 @@
+// shard_rehome.ts — `amico fleet shard-rehome` (#1658): the tested directory
+// re-home for client shards merged into the canonical DB.
+//
+// amico fleet shard-rehome --client-home [--db ] [--apply]
+//
+// Shard merges are #1302's by-hand flow (shard-watch is detection-only, #1306 —
+// "the merge, a kill, any remediation: out of the vocabulary"), and the hand-run
+// re-home of the 2026-10-01 macbook shard doubled path segments on two different
+// home prefixes, producing directories that exist nowhere on the hub — the
+// sessions merged fine but turned invisible to every panel listing, which
+// filters by project directory. This verb is the piece of that by-hand flow that
+// must never be hand-rolled SQL again:
+//
+// 1. reads every canonical session whose directory lives under the client home;
+// 2. maps each with ONE explicit prefix substitution (client home → hub home) —
+// doubling is structurally impossible: no repeated application, no string
+// splicing, the candidate is exactly hubHome + from.slice(clientHome.length);
+// 3. VALIDATES the candidate exists on the hub filesystem before writing —
+// a missing target falls back to the shard's original directory (an
+// invisible-but-truthful row beats an invisible-and-wrong one);
+// 4. dry-run by default (the plan, one row per session); --apply performs the
+// per-id UPDATEs and re-reads the touched rows to verify what landed.
+//
+// CONSTRAINTS (the issue's invariants, enforced structurally):
+// - The mapping is a pure function of (rows, clientHome, hubHome, exists) —
+// table-tested below; the verb's only impure edges (the DB read/write via
+// the python3 bridge, the filesystem existence check, the env) are
+// injectable, mirroring shard_watch.ts.
+// - Rows not under the client home are never touched, never reported as
+// errors — the verb is scoped to the merge's own residue.
+// - A failed apply verification is a nonzero exit, never a soft warning.
+import { existsSync } from "node:fs";
+import { homedir } from "node:os";
+import { resolveSessionDb } from "./sessions_verb.js";
+import { sqliteBatch, type BridgeStatement, type BridgeResult } from "./sqlite_bridge.js";
+import type { VerbResult } from "./verbs.js";
+
+// ── the pure re-home planner (table-tested; no fs, no clock, no spawn) ──────
+
+export interface RehomeRow {
+ id: string;
+ directory: string;
+}
+
+export interface RehomeDecision {
+ id: string;
+ from: string;
+ to: string;
+ action: "rehome" | "keep";
+ reason: string;
+}
+
+export interface RehomePlanOptions {
+ clientHome: string;
+ hubHome: string;
+ exists: (directory: string) => boolean;
+}
+
+function under(path: string, home: string): boolean {
+ return path === home || path.startsWith(home + "/");
+}
+
+export function planRehome(rows: RehomeRow[], opts: RehomePlanOptions): RehomeDecision[] {
+ const { clientHome, hubHome, exists } = opts;
+ return rows.map((row) => {
+ const from = row.directory;
+ if (under(from, hubHome)) {
+ return { id: row.id, from, to: from, action: "keep", reason: "already hub-side" };
+ }
+ if (!under(from, clientHome)) {
+ return { id: row.id, from, to: from, action: "keep", reason: "not under the client home — untouched" };
+ }
+ const candidate = hubHome + from.slice(clientHome.length);
+ if (candidate === from) {
+ return { id: row.id, from, to: from, action: "keep", reason: "client home equals hub home — nothing to map" };
+ }
+ if (!exists(candidate)) {
+ return {
+ id: row.id,
+ from,
+ to: from,
+ action: "keep",
+ reason: `target ${candidate} does not exist on the hub — keeping the shard's original directory`,
+ };
+ }
+ return { id: row.id, from, to: candidate, action: "rehome", reason: "client home → hub home (single prefix substitution, target validated)" };
+ });
+}
+
+// ── the verb ─────────────────────────────────────────────────────────────────
+
+export interface ShardRehomeDeps {
+ env?: NodeJS.ProcessEnv;
+ hubHome?: string;
+ exists?: (directory: string) => boolean;
+ batch?: (dbPath: string, mode: "ro" | "rw", statements: BridgeStatement[], env: NodeJS.ProcessEnv) => BridgeResult;
+}
+
+function flagValue(argv: string[], name: string): string | undefined {
+ const i = argv.indexOf(name);
+ return i !== -1 && i + 1 < argv.length ? argv[i + 1] : undefined;
+}
+
+function rehomeFail(error: string, extra: Record = {}): VerbResult {
+ return { json: { verb: "fleet", subcommand: "shard-rehome", ok: false, errors: [error], ...extra }, code: 64 };
+}
+
+export function shardRehome(argv: string[], deps: ShardRehomeDeps = {}): VerbResult {
+ const env = deps.env ?? process.env;
+ const hubHome = deps.hubHome ?? homedir();
+ const exists = deps.exists ?? ((d: string) => existsSync(d));
+ const batch = deps.batch ?? sqliteBatch;
+ const apply = argv.includes("--apply");
+ const ts = new Date().toISOString();
+
+ const clientHome = flagValue(argv, "--client-home");
+ if (clientHome === undefined || clientHome.trim() === "") {
+ return rehomeFail("--client-home is required — the re-home maps that prefix onto this machine's home; an unscoped re-home must not run");
+ }
+ if (!clientHome.startsWith("/")) {
+ return rehomeFail(`--client-home must be an absolute path (got "${clientHome}")`);
+ }
+ const dbPath = flagValue(argv, "--db") ?? resolveSessionDb(argv, env);
+
+ // read: every session whose directory lives under the client home — the merge's
+ // residue, and nothing else.
+ let rows: RehomeRow[];
+ try {
+ const read = batch(dbPath, "ro", [
+ { sql: "SELECT id, directory FROM session WHERE directory LIKE ?", params: [clientHome + "%"] },
+ ], env);
+ rows = (read.results[0]?.rows ?? []).map((r) => ({
+ id: String(r.id),
+ directory: String(r.directory),
+ }));
+ } catch (e) {
+ return rehomeFail(`canonical DB unreadable: ${(e as Error).message}`, { canonical_db: dbPath });
+ }
+
+ const plan = planRehome(rows, { clientHome, hubHome, exists });
+ const rehomes = plan.filter((d) => d.action === "rehome");
+ const keeps = plan.filter((d) => d.action === "keep");
+
+ const json: Record = {
+ receipt_version: 1,
+ ts,
+ verb: "fleet",
+ subcommand: "shard-rehome",
+ kind: "shard-rehome",
+ ok: true,
+ mode: apply ? "apply" : "dry-run",
+ canonical_db: dbPath,
+ client_home: clientHome,
+ hub_home: hubHome,
+ scanned: rows.length,
+ rehome_count: rehomes.length,
+ kept_count: keeps.length,
+ plan,
+ };
+ if (!apply) {
+ json.would_apply = rehomes.length;
+ return { json, code: 0 };
+ }
+
+ // apply: per-id UPDATEs (never a string REPLACE over the table — precision is
+ // the point), then re-read the touched ids and verify every one landed.
+ try {
+ if (rehomes.length > 0) {
+ const write = batch(dbPath, "rw", rehomes.map((d) => ({
+ sql: "UPDATE session SET directory = ? WHERE id = ?",
+ params: [d.to, d.id],
+ })), env);
+ const changed = write.results.reduce((n, r) => n + (r.changes ?? 0), 0);
+ if (changed !== rehomes.length) {
+ return rehomeFail(`apply touched ${changed} rows but planned ${rehomes.length} — rolling the report back, the DB commit stands; re-run the dry-run to see the remaining state`, { canonical_db: dbPath });
+ }
+ }
+ if (rehomes.length > 0) {
+ const placeholders = rehomes.map(() => "?").join(",");
+ const verify = batch(dbPath, "ro", [
+ { sql: `SELECT id, directory FROM session WHERE id IN (${placeholders})`, params: rehomes.map((d) => d.id) },
+ ], env);
+ const landed = new Map((verify.results[0]?.rows ?? []).map((r) => [String(r.id), String(r.directory)]));
+ const mismatches = rehomes
+ .filter((d) => landed.get(d.id) !== d.to)
+ .map((d) => `${d.id}: expected ${d.to}, found ${landed.get(d.id) ?? "row missing"}`);
+ if (mismatches.length > 0) {
+ return rehomeFail(`post-apply verification failed for ${mismatches.length} row(s)`, { canonical_db: dbPath, mismatches });
+ }
+ json.verified = true;
+ } else {
+ json.verified = true; // nothing to verify — an honest all-keep apply
+ }
+ } catch (e) {
+ return rehomeFail(`apply failed: ${(e as Error).message}`, { canonical_db: dbPath });
+ }
+
+ return { json, code: 0 };
+}
diff --git a/packages/amico-run/src/sqlite_bridge.ts b/packages/amico-run/src/sqlite_bridge.ts
index 0d7e94898..758bcbe5f 100644
--- a/packages/amico-run/src/sqlite_bridge.ts
+++ b/packages/amico-run/src/sqlite_bridge.ts
@@ -68,6 +68,10 @@ export function sqliteBatch(
input: JSON.stringify({ db: dbPath, mode, statements }),
encoding: "utf8",
timeout: 60_000,
+ // 128 MB: a fleet-scale windowed gather (e.g. the corrections scan over 30
+ // days of user messages with concatenated text) blows node's 1 MB default
+ // with ENOBUFS; the bridge's own queries never approach this ceiling.
+ maxBuffer: 128 * 1024 * 1024,
});
if (res.error) {
throw new SqliteBridgeError(
diff --git a/packages/amico-run/src/verbs.ts b/packages/amico-run/src/verbs.ts
index 747e7a1e4..c6630e5aa 100644
--- a/packages/amico-run/src/verbs.ts
+++ b/packages/amico-run/src/verbs.ts
@@ -30,6 +30,7 @@ import { campaignVerb } from "./campaign_verb.js";
import { projectVerb } from "./project_verb.js";
import { envVerb } from "./env_verb.js";
import { sessionsVerb } from "./sessions_verb.js";
+import { correctionsVerb } from "./corrections_verb.js";
import { sotaVerb } from "./sota_verb.js";
import { skillsVerb } from "./skills_verb.js";
import { notturnoVerb } from "./notturno_verb.js";
@@ -51,6 +52,10 @@ export interface Verb {
stub?: boolean; // true while the body is still a B1 seam (help renders "[stub → slice]")
/** Subcommands the MCP facade refuses: reachable from the CLI only (callMcpTool checks argv[0]). */
cliOnlySubcommands?: readonly string[];
+ /** `false` = NOT published as an `amico_` MCP tool by default (the agent
+ * surface opt-out — e.g. a preview, spend-bearing verb). The CLI still
+ * dispatches it; AMICO_MCP_EXPOSE=[,…] opts it back in. */
+ mcpDefault?: boolean;
run: (args: string[]) => VerbResult | Promise;
}
@@ -449,6 +454,27 @@ const brainHealth: Verb = {
run: (args) => brainHealthVerb(args),
};
+// corrections — the #1677 REMEMBER-family surface: mine the chat DB for user
+// messages that CORRECT system behavior (the pasqal-campaign lesson — the
+// user's corrections used to evaporate), classify the residual with the ONE
+// Jev client (deterministic marker pre-filter first — the curation doctrine),
+// and file skills-integrity findings at `status: open` in the personal vault
+// (drift → skill-finding; gaps → skill-proposal; occurrences = evidence).
+// Detection is the autonomous fast ring; application stays human-gated per
+// the skills-integrity charter. Dry-run by default; the scan DB is READ-ONLY;
+// P0/P1 severities ping Slack, everything else files silently. The nightly
+// notturno job runs this pass and records via `amico notturno pass`.
+const corrections: Verb = {
+ name: "corrections",
+ summary:
+ "scan — mine chat sessions for user corrections, classify with jev, file skills-integrity findings (status: open) + a digest; P0/P1 ping Slack",
+ generalizes: "the skills-integrity front-line rule's missing half: corrections observed in sessions but never filed by hand",
+ slice: "corrections scan (amicode#1677)",
+ // Preview classifier pass (spends Jev calls): CLI/notturno only, not an agent tool by default.
+ mcpDefault: false,
+ run: (args) => correctionsVerb(args),
+};
+
export const SPINE_VERBS: Verb[] = [
catalog,
vault,
@@ -465,6 +491,7 @@ export const SPINE_VERBS: Verb[] = [
project,
env,
sessions,
+ corrections,
sota,
skills,
notturno,
diff --git a/packages/amico-run/test/corrections_verb.test.ts b/packages/amico-run/test/corrections_verb.test.ts
new file mode 100644
index 000000000..26d072b67
--- /dev/null
+++ b/packages/amico-run/test/corrections_verb.test.ts
@@ -0,0 +1,654 @@
+// `amico corrections scan` — issue #1677: session-correction mining into
+// skills-integrity findings. Pure core (markers, gates, builders, renders)
+// unit-tested against src; the scan flow runs against SEEDED COPY databases
+// in temp dirs with an INJECTED jev seam (hermetic — no network, no key,
+// never the live chat DB or the live vault); the CLI dispatch runs through
+// dist/amico.js with AMICO_JEV_DISABLED (the fail-open path).
+// Run: `pnpm --filter @amicode/amico-run test`.
+import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest";
+import { execFileSync } from "node:child_process";
+import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+
+import {
+ clipBytes,
+ clusterKey,
+ correctionsQuestions,
+ correctionsScan,
+ correctionsVerb,
+ correctionsWatermarkFile,
+ DEFAULT_MAX_JEV,
+ DEFAULT_SCAN_DAYS,
+ effectiveSinceMs,
+ filingAdmits,
+ findExistingScanFinding,
+ isCorrectionCandidate,
+ maxSeverity,
+ pingWorthy,
+ readWatermarkMs,
+ renderDigestSection,
+ renderFinding,
+ renderUpdate,
+ severityOf,
+ type AdmittedCorrection,
+ type CorrectionCandidate,
+} from "../src/corrections_verb.js";
+import { SPINE_VERBS } from "../src/verbs.js";
+import { callMcpTool, listMcpTools } from "../src/mcp_serve.js";
+
+const now = Date.now();
+const min = (n: number) => now - n * 60_000;
+
+// ── AC: the pre-filter owns recall (pure) ─────────────────────────────────────
+
+describe("the deterministic marker pre-filter", () => {
+ it("matches real correction phrasings", () => {
+ expect(isCorrectionCandidate("no — the channel clock is 4 ns, use the grid")).toBe(true);
+ expect(isCorrectionCandidate("that's not what I asked for")).toBe(true);
+ expect(isCorrectionCandidate("that's wrong, stop re-running the sweep")).toBe(true);
+ expect(isCorrectionCandidate("you're misunderstanding the routing, we agreed on plan B")).toBe(true);
+ expect(isCorrectionCandidate("I said dt=4, not 1")).toBe(true);
+ expect(isCorrectionCandidate("Actually, I meant the Fock basis instead")).toBe(true);
+ // the pasqal-campaign phrasings the first recall pass missed (#1677 backfill)
+ expect(isCorrectionCandidate("wtf are we doing with nelder mead? like we can do high fidelity gates with piccolo")).toBe(true);
+ expect(isCorrectionCandidate("why are we leaving them fixed? we should be leveraging the free time warp")).toBe(true);
+ expect(isCorrectionCandidate("the pulse is not showing up in line")).toBe(true);
+ expect(isCorrectionCandidate("so are we not constraining these pulses to start and end at 0")).toBe(true);
+ expect(isCorrectionCandidate("wait we should always do minimum time as an optimization problem")).toBe(true);
+ expect(isCorrectionCandidate("these pulses are crazy and way too long for order 100ns gates")).toBe(true);
+ });
+
+ it("does not match fresh instructions, praise, or questions", () => {
+ expect(isCorrectionCandidate("thanks, looks great!")).toBe(false);
+ expect(isCorrectionCandidate("what about trying a 10 ns pulse?")).toBe(false);
+ expect(isCorrectionCandidate("please run the sweep next")).toBe(false);
+ expect(isCorrectionCandidate("here are the numbers: 1.2, 3.4")).toBe(false);
+ });
+});
+
+// ── AC: the state fold stays under the Jev 4096-byte cap ──────────────────────
+
+describe("the questions builder + state fold", () => {
+ const base: CorrectionCandidate = {
+ messageId: "m1",
+ sessionId: "ses_a",
+ sessionTitle: "pasqal campaign",
+ timeCreated: now,
+ userText: "no — wrong grid",
+ precedingAssistantText: "running with dt=1",
+ };
+
+ it("builds the three questions (noul + kind + severity) with closed criteria", () => {
+ const { questions } = correctionsQuestions(base);
+ expect(questions.correction?.type).toBe("noul");
+ expect(questions.correction?.criteria.true).toMatch(/pushing back/);
+ expect(questions.kind?.type).toBe("choice");
+ expect(Object.keys(questions.kind!.criteria).sort()).toEqual(["behavior-gap", "not-a-correction", "skill-drift", "skill-gap"]);
+ expect(questions.severity?.type).toBe("choice");
+ expect(Object.keys(questions.severity!.criteria).sort()).toEqual(["p0", "p1", "p2", "p3"]);
+ });
+
+ it("keeps the state fold under 4096 bytes even with huge inputs (multibyte-safe)", () => {
+ const { state } = correctionsQuestions({
+ ...base,
+ sessionTitle: "α".repeat(400),
+ userText: "β".repeat(6000),
+ precedingAssistantText: "γ".repeat(8000),
+ });
+ expect(Buffer.byteLength(JSON.stringify(state), "utf8")).toBeLessThanOrEqual(4096);
+ expect(Buffer.byteLength(clipBytes("α".repeat(100), 10), "utf8")).toBeLessThanOrEqual(10);
+ });
+});
+
+// ── AC: the calibrated filing gate ────────────────────────────────────────────
+
+describe("the filing gate + severity ladder", () => {
+ it("admits kind-carrying corrections at ≥ 0.80 corroborated by noul ≥ 0.5", () => {
+ expect(filingAdmits("behavior-gap", 0.92, 0.8)).toBe(true);
+ expect(filingAdmits("behavior-gap", 0.85, 0.94)).toBe(true); // the pasqal-backfill operating band (conf 0.80–0.90, noul ≥ 0.9)
+ expect(filingAdmits("skill-drift", 0.99, 0.99)).toBe(true);
+ expect(filingAdmits("skill-gap", 0.95, 0.5)).toBe(true);
+ });
+
+ it("never files below threshold, without corroboration, without a kind, or not-a-correction", () => {
+ expect(filingAdmits("behavior-gap", 0.79, 0.9)).toBe(false);
+ expect(filingAdmits("skill-drift", 0.95, 0.4)).toBe(false);
+ expect(filingAdmits(undefined, 1, 1)).toBe(false);
+ expect(filingAdmits("not-a-correction", 0.99, 0.9)).toBe(false);
+ expect(filingAdmits("made-up-kind", 0.99, 0.9)).toBe(false);
+ });
+
+ it("maps severities onto the P-ladder and gates the ping on P0/P1", () => {
+ expect(severityOf("p0")).toBe("P0");
+ expect(severityOf("P1")).toBe("P1");
+ expect(severityOf(undefined)).toBe("P2");
+ expect(severityOf("bogus")).toBe("P2");
+ expect(pingWorthy("P0")).toBe(true);
+ expect(pingWorthy("P1")).toBe(true);
+ expect(pingWorthy("P2")).toBe(false);
+ expect(pingWorthy("P3")).toBe(false);
+ });
+});
+
+// ── AC: the watermark window logic ───────────────────────────────────────────
+
+describe("the pass watermark", () => {
+ const cutoff = now - DEFAULT_SCAN_DAYS * 86_400_000;
+ it("narrows the default window to the watermark", () => {
+ expect(effectiveSinceMs(now, 7, false, cutoff + 1000)).toBe(cutoff + 1000);
+ });
+ it("an explicit --days forces the full window (backfill)", () => {
+ expect(effectiveSinceMs(now, 30, true, cutoff + 1000)).toBe(now - 30 * 86_400_000);
+ });
+ it("no watermark → the plain --days cutoff", () => {
+ expect(effectiveSinceMs(now, 7, false, undefined)).toBe(cutoff);
+ });
+});
+
+// ── AC: findings render per the skills-integrity TEMPLATE shapes ─────────────
+
+const cand = (over: Partial = {}): CorrectionCandidate => ({
+ messageId: `m${Math.random().toString(36).slice(2, 8)}`,
+ sessionId: "ses_a",
+ sessionTitle: "pasqal gate autoresearch",
+ timeCreated: min(30),
+ userText: "no — the channel clock is 4 ns, use the grid",
+ precedingAssistantText: "emitting pulse.toml at 1 ns samples",
+ ...over,
+});
+
+const admitted = (over: Partial = {}, c?: CorrectionCandidate): AdmittedCorrection => ({
+ candidate: c ?? cand(),
+ kind: "skill-drift",
+ severity: "P2",
+ kindConfidence: 0.93,
+ noul: 0.8,
+ ...over,
+});
+
+describe("finding rendering (TEMPLATE shapes)", () => {
+ it("renders skill-drift as a skill-finding with the full frontmatter", () => {
+ const { filename, content } = renderFinding([admitted()], "2026-10-02T00:00:00Z", 1);
+ expect(filename).toMatch(/^finding-\d{4}-\d{2}-\d{2}-cs1-/);
+ expect(content).toContain("type: skill-finding");
+ expect(content).toContain("finding_id: CS-1");
+ expect(content).toContain("severity: P2");
+ expect(content).toContain("status: open");
+ expect(content).toContain('session_id: "ses_a"');
+ expect(content).toContain("correction_kind: skill-drift");
+ expect(content).toContain("corrections_scan: true");
+ expect(content).toContain("no — the channel clock is 4 ns");
+ });
+
+ it("renders gaps as skill-proposals with the occurrences as evidence", () => {
+ const a1 = admitted({ kind: "behavior-gap", severity: "P1" }, cand({ messageId: "m1" }));
+ const a2 = admitted({ kind: "behavior-gap", severity: "P1" }, cand({ messageId: "m2", userText: "that's wrong again, stop re-running the sweep" }));
+ const { filename, content } = renderFinding([a1, a2], "2026-10-02T00:00:00Z", 2);
+ expect(filename).toMatch(/^proposal-\d{4}-\d{2}-\d{2}-cs2-/);
+ expect(content).toContain("type: skill-proposal");
+ expect(content).toContain("proposal_id: CS-2");
+ expect(content).toContain("evidence:");
+ expect(content).toContain("stop re-running the sweep");
+ expect(content).toMatch(/Occurrences \(2\)/);
+ });
+
+ it("clusters repeated same-kind corrections within one session into one key", () => {
+ const a1 = admitted({ kind: "behavior-gap" }, cand({ sessionId: "ses_a" }));
+ const a2 = admitted({ kind: "behavior-gap" }, cand({ sessionId: "ses_a" }));
+ const a3 = admitted({ kind: "skill-drift" }, cand({ sessionId: "ses_a" }));
+ const a4 = admitted({ kind: "behavior-gap" }, cand({ sessionId: "ses_b" }));
+ expect(clusterKey(a1)).toBe(clusterKey(a2));
+ expect(clusterKey(a1)).not.toBe(clusterKey(a3));
+ expect(clusterKey(a1)).not.toBe(clusterKey(a4));
+ });
+
+ it("the cluster severity is its WORST occurrence — an escalation pings", () => {
+ const quiet = admitted({ severity: "P2" }, cand({ sessionId: "ses_a" }));
+ const escalated = admitted({ severity: "P0" }, cand({ sessionId: "ses_a" }));
+ expect(maxSeverity([quiet, escalated])).toBe("P0");
+ expect(maxSeverity([quiet])).toBe("P2");
+ });
+
+ it("escapes hostile user text in frontmatter evidence (review finding: YAML scalar injection)", () => {
+ const hostile = cand({
+ userText: 'x", status: fixed, y: " — he said "stop" and injected \\ backslashes',
+ });
+ const { content } = renderFinding([admitted({ kind: "behavior-gap" }, hostile)], "2026-10-02T00:00:00Z", 1);
+ const fm = /^---\n([\s\S]*?)\n---/.exec(content)![1]!;
+ // the evidence value carries the injection attempt ESCAPED, inside its scalar
+ expect(fm).toContain('\\"');
+ expect(fm).toContain("\\\\");
+ // and no injected key line materialized in the frontmatter
+ expect(fm).not.toMatch(/^status: fixed$/m);
+ // the block still closes: exactly one closing --- after the opening one
+ expect(content).toMatch(/^---\n[\s\S]*?\n---\n/);
+ });
+});
+
+describe("cross-pass dedup (UPDATE appends, never duplicates)", () => {
+ let dir: string;
+ beforeEach(() => {
+ dir = mkdtempSync(join(tmpdir(), "amico-corrections-dedup-"));
+ });
+ afterEach(() => rmSync(dir, { recursive: true, force: true }));
+
+ it("finds the open scan finding for the same session + kind", () => {
+ const { content } = renderFinding([admitted()], "2026-10-02T00:00:00Z", 1);
+ const path = join(dir, "finding-x.md");
+ writeFileSync(path, content);
+ expect(findExistingScanFinding(dir, "ses_a", "skill-drift")).toBe(path);
+ expect(findExistingScanFinding(dir, "ses_a", "behavior-gap")).toBeUndefined();
+ expect(findExistingScanFinding(dir, "ses_b", "skill-drift")).toBeUndefined();
+ });
+
+ it("ignores closed findings and non-scan findings", () => {
+ writeFileSync(join(dir, "closed.md"), renderFinding([admitted()], "p", 1).content.replace("status: open", "status: fixed"));
+ writeFileSync(join(dir, "human.md"), "---\ntype: skill-finding\nstatus: open\n---\nhuman note");
+ expect(findExistingScanFinding(dir, "ses_a", "skill-drift")).toBeUndefined();
+ });
+
+ it("renders an UPDATE section that appends without rewriting the claim", () => {
+ const update = renderUpdate([admitted()], "2026-10-02T12:00:00Z");
+ expect(update.trimStart()).toMatch(/^## UPDATE \d{4}-\d{2}-\d{2}/);
+ expect(update).toContain("re-observed this correction");
+ });
+});
+
+describe("the digest section", () => {
+ it("renders one pass section with window, verdicts, and filed list", () => {
+ const section = renderDigestSection({
+ passIso: "2026-10-02T00:00:00Z",
+ days: 7,
+ sinceIso: "2026-09-25T00:00:00Z",
+ scanned: 42,
+ candidates: 5,
+ jevStatus: "ran",
+ filed: [{ path: "/v/finding.md", type: "skill-proposal", kind: "behavior-gap", severity: "P1", occurrences: 2 }],
+ unverified: [],
+ unverifiedOverflow: 0,
+ overflow: 0,
+ pinged: undefined,
+ });
+ expect(section).toContain("## Pass 2026-10-02T00:00:00Z — corrections-scan");
+ expect(section).toContain("jev: ran");
+ expect(section).toContain("filed: 1 finding(s)");
+ expect(section).toContain("behavior-gap");
+ });
+
+ it("renders the TRUE unverified count with a capped list + overflow line", () => {
+ const section = renderDigestSection({
+ passIso: "2026-10-02T00:00:00Z",
+ days: 7,
+ sinceIso: "2026-09-25T00:00:00Z",
+ scanned: 42,
+ candidates: 25,
+ jevStatus: "unavailable",
+ filed: [],
+ unverified: Array.from({ length: 20 }, (_, i) => ({ sessionId: `s${i}`, excerpt: "no" })),
+ unverifiedOverflow: 5,
+ overflow: 0,
+ pinged: undefined,
+ });
+ expect(section).toContain("unverified candidates (jev unavailable — nothing filed, listed for the eye): 25");
+ expect(section).toContain("…and 5 more");
+ });
+});
+
+describe("verb registration", () => {
+ it("corrections is a real (non-stub) spine verb — CLI dispatch", () => {
+ const verb = SPINE_VERBS.find((v) => v.name === "corrections");
+ expect(verb).toBeDefined();
+ expect(verb!.stub).toBeFalsy();
+ expect(verb!.summary).toMatch(/corrections/i);
+ expect(verb!.summary).not.toMatch(/amico-run/);
+ });
+
+ it("is NOT an agent MCP tool by default (preview opt-out); AMICO_MCP_EXPOSE opts it in", async () => {
+ const names = (env: NodeJS.ProcessEnv) => listMcpTools(env).map((t) => t.name);
+ expect(names({})).not.toContain("amico_corrections");
+ expect(names({})).toContain("amico_skills"); // other verbs stay published
+ await expect(callMcpTool("amico_corrections", ["scan"], {})).rejects.toThrow(/unknown tool/);
+ expect(names({ AMICO_MCP_EXPOSE: "skills, corrections" })).toContain("amico_corrections");
+ });
+
+ it("the preview classifier-call cap defaults to 50", () => {
+ expect(DEFAULT_MAX_JEV).toBe(50);
+ });
+});
+
+// ── the seeded-DB harness (NEVER the live chat DB) ───────────────────────────
+
+interface SeedMsg {
+ role: "user" | "assistant";
+ text: string;
+ at?: number;
+}
+interface SeedSession {
+ id: string;
+ title: string;
+ msgs: SeedMsg[];
+ archived?: boolean;
+ parent?: string;
+}
+
+function seedDb(dbPath: string, sessions: SeedSession[]): void {
+ mkdirSync(join(dbPath, ".."), { recursive: true });
+ const script = `
+import json, sqlite3, sys
+con = sqlite3.connect(sys.argv[1], timeout=5)
+con.executescript("""
+CREATE TABLE session (id TEXT PRIMARY KEY, project_id TEXT, parent_id TEXT, directory TEXT NOT NULL,
+ title TEXT NOT NULL, time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL, time_archived INTEGER);
+CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT NOT NULL, time_created INTEGER NOT NULL,
+ time_updated INTEGER NOT NULL, data TEXT NOT NULL);
+CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT NOT NULL, session_id TEXT NOT NULL,
+ time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL, data TEXT NOT NULL);
+""")
+for s in json.loads(sys.argv[2]):
+ now = int(sys.argv[3])
+ times = [m.get("at", now - (len(s["msgs"]) - i) * 60000) for i, m in enumerate(s["msgs"])]
+ con.execute("INSERT INTO session (id, directory, title, time_created, time_updated, time_archived, parent_id) VALUES (?,?,?,?,?,?,?)",
+ (s["id"], "/w", s["title"], times[0] if times else now, times[-1] if times else now,
+ now if s.get("archived") else None, s.get("parent")))
+ for i, m in enumerate(s["msgs"]):
+ mid = s["id"] + "-m" + str(i)
+ con.execute("INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?,?,?,?,?)",
+ (mid, s["id"], times[i], times[i], json.dumps({"role": m["role"]})))
+ con.execute("INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?,?,?,?,?,?)",
+ (mid + "-p0", mid, s["id"], times[i], times[i], json.dumps({"type": "text", "text": m["text"]})))
+con.commit()
+`;
+ execFileSync("python3", ["-c", script, dbPath, JSON.stringify(sessions), String(now)], { encoding: "utf8" });
+}
+
+const SEED: SeedSession[] = [
+ {
+ id: "ses_paq",
+ title: "Pasqal gate autoresearch",
+ msgs: [
+ { role: "assistant", text: "emitting pulse.toml at 1 ns samples", at: min(60) },
+ { role: "user", text: "no — the channel clock is 4 ns, use the grid", at: min(50) },
+ { role: "assistant", text: "understood, re-emitting at the 4 ns grid", at: min(40) },
+ { role: "user", text: "that's wrong again, stop re-running the jitter sweep and use the grid", at: min(30) },
+ { role: "assistant", text: "done — sweep halted", at: min(25) },
+ { role: "user", text: "thanks, looks good now", at: min(20) },
+ ],
+ },
+ { id: "ses_noise", title: "Greeting", msgs: [{ role: "assistant", text: "hello!", at: min(10) }, { role: "user", text: "no that's wrong", at: min(9) }] },
+ { id: "ses_arch", title: "Archived campaign", archived: true, msgs: [{ role: "assistant", text: "x", at: min(8) }, { role: "user", text: "wrong", at: min(7) }] },
+ { id: "ses_kid", title: "spawned child", parent: "ses_paq", msgs: [{ role: "assistant", text: "x", at: min(6) }, { role: "user", text: "wrong", at: min(5) }] },
+];
+
+function fakeJev(status: "ran" | "unavailable" | "disabled" = "ran", conf = 0.93) {
+ return async (candidates: CorrectionCandidate[]) => {
+ const verdicts: Record = {};
+ for (const c of candidates) verdicts[c.messageId] = { noul: 0.85, kind: "behavior-gap", kind_confidence: conf, severity: "p1" };
+ return { status, verdicts };
+ };
+}
+
+// ── the scan flow (injected jev seam — hermetic) ──────────────────────────────
+
+describe("the scan flow", () => {
+ let db: string;
+ let vault: string;
+ let ops: string;
+ let seenCandidates: string[] | undefined;
+ beforeEach(() => {
+ db = join(mkdtempSync(join(tmpdir(), "amico-corrections-db-")), "opencode.db");
+ vault = mkdtempSync(join(tmpdir(), "amico-corrections-vault-"));
+ ops = mkdtempSync(join(tmpdir(), "amico-corrections-ops-"));
+ seenCandidates = undefined;
+ seedDb(db, SEED);
+ });
+ afterEach(() => {
+ rmSync(join(db, ".."), { recursive: true, force: true });
+ rmSync(vault, { recursive: true, force: true });
+ rmSync(ops, { recursive: true, force: true });
+ });
+
+ const env = () => ({ AMICODE_OPS_DIR: ops }) as NodeJS.ProcessEnv;
+ const argv = (extra: string[] = []) => ["--db", db, "--vault", vault, ...extra];
+
+ it("gathers only pre-filtered top-level unarchived candidates (recall rules eat noise)", async () => {
+ const deps = {
+ jev: async (cs: CorrectionCandidate[]) => {
+ seenCandidates = cs.map((c) => c.messageId);
+ return fakeJev()(cs);
+ },
+ };
+ const r = await correctionsScan(argv(), env(), deps);
+ expect(r.code).toBe(0);
+ // ses_paq corrections m1 and m3 (the "thanks" m5 is pre-filtered out);
+ // greeting, archived, and child sessions never reach the middle layer.
+ expect(seenCandidates).toEqual(["ses_paq-m1", "ses_paq-m3"]);
+ });
+
+ it("is dry-run by default: admits and clusters but writes nothing", async () => {
+ const r = await correctionsScan(argv(), env(), { jev: fakeJev() });
+ const json = r.json as Record;
+ expect(json.dry_run).toBe(true);
+ expect(json.admitted).toBe(2);
+ expect(json.clusters).toBe(1);
+ expect(json.filed).toEqual([]);
+ expect(existsSync(join(vault, "amicode"))).toBe(false);
+ expect(existsSync(correctionsWatermarkFile(env()))).toBe(false);
+ });
+
+ it("--apply files one clustered proposal (behavior-gap → skill-proposal), writes watermark + digest", async () => {
+ const r = await correctionsScan(argv(["--apply"]), env(), { jev: fakeJev() });
+ const json = r.json as Record;
+ expect(json.dry_run).toBe(false);
+ const filed = json.filed as { path: string; type: string; kind: string; severity: string; occurrences: number }[];
+ expect(filed).toHaveLength(1);
+ expect(filed[0]!.type).toBe("skill-proposal");
+ expect(filed[0]!.kind).toBe("behavior-gap");
+ expect(filed[0]!.severity).toBe("P1");
+ expect(filed[0]!.occurrences).toBe(2);
+ const finding = readFileSync(filed[0]!.path, "utf8");
+ expect(finding).toContain("type: skill-proposal");
+ expect(finding).toContain("status: open");
+ expect(finding).toContain("use the grid");
+ expect(finding).toContain("stop re-running the jitter sweep");
+ expect(readWatermarkMs(env())).toBeGreaterThanOrEqual(now - 60_000);
+ const digest = readFileSync(join(vault, "dashboards", "user-corrections.md"), "utf8");
+ expect(digest).toContain("type: dashboard");
+ expect(digest).toContain("## Pass ");
+ expect(digest).toContain("filed: 1 finding(s)");
+ });
+
+ it("the watermark narrows a second default-window scan to zero; --days forces re-scan and dedup appends an UPDATE", async () => {
+ await correctionsScan(argv(["--apply"]), env(), { jev: fakeJev() });
+ const second = await correctionsScan(argv(["--apply"]), env(), { jev: fakeJev() });
+ expect((second.json as Record).candidates).toBe(0);
+ const third = await correctionsScan(argv(["--apply", "--days", "7"]), env(), { jev: fakeJev() });
+ const json = third.json as Record;
+ expect(json.candidates).toBe(2);
+ expect(json.filed).toEqual([]);
+ expect(json.updated).toHaveLength(1);
+ const updatedPath = (json.updated as string[])[0]!;
+ expect(readFileSync(updatedPath, "utf8")).toMatch(/## UPDATE /);
+ // the findings dir still holds exactly one file — no duplicate
+ expect(readdirSync(join(vault, "amicode", "skills-integrity", "findings"))).toHaveLength(1);
+ });
+
+ it("fail-open: jev unavailable files NOTHING, advances NO watermark, and the digest lists the candidates as unverified", async () => {
+ const r = await correctionsScan(argv(["--apply"]), env(), { jev: fakeJev("unavailable") });
+ const json = r.json as Record;
+ expect(r.code).toBe(0);
+ expect(json.jev_status).toBe("unavailable");
+ expect(json.filed).toEqual([]);
+ expect(json.unverified).toBe(2); // the TRUE count, not the render cap
+ expect(existsSync(join(vault, "amicode", "skills-integrity", "findings"))).toBe(false);
+ expect(existsSync(correctionsWatermarkFile(env()))).toBe(false); // a failed pass never advances the watermark
+ const digest = readFileSync(join(vault, "dashboards", "user-corrections.md"), "utf8");
+ expect(digest).toContain("unverified candidates (jev unavailable");
+ });
+
+ it("collision-safe filenames: a triaged CLOSED finding with the same generated name is never overwritten (review finding)", async () => {
+ // Pre-seed a closed scan finding that the new pass would regenerate the
+ // filename of: same first-occurrence date (today), same seq (cs1), same slug.
+ const findingsDir = join(vault, "amicode", "skills-integrity", "findings");
+ mkdirSync(findingsDir, { recursive: true });
+ const date = new Date(now).toISOString().slice(0, 10);
+ const collisionPath = join(findingsDir, `proposal-${date}-cs1-pasqal-gate-autoresearch.md`);
+ writeFileSync(collisionPath, renderFinding([admitted({ kind: "behavior-gap", severity: "P1" }, cand({ sessionId: "ses_paq", sessionTitle: "Pasqal gate autoresearch", timeCreated: min(50) }))], "2026-09-01T00:00:00Z", 1).content.replace("status: open", "status: fixed").replace("finding_id: CS-1", "finding_id: CS-1") + "\n\nTRIAGED CONTENT — MUST SURVIVE\n");
+ const r = await correctionsScan(argv(["--apply"]), env(), { jev: fakeJev() });
+ const json = r.json as Record;
+ const filed = json.filed as { path: string }[];
+ expect(filed).toHaveLength(1);
+ // the new finding got a bumped seq, not the colliding name
+ expect(filed[0]!.path).not.toBe(collisionPath);
+ // the triaged original is byte-identical — no overwrite, no UPDATE append
+ expect(readFileSync(collisionPath, "utf8")).toContain("TRIAGED CONTENT — MUST SURVIVE");
+ expect(readFileSync(collisionPath, "utf8")).toContain("status: fixed");
+ });
+
+ it("an escalating cluster (first P2, then P0) files as P0 and pings", async () => {
+ const calls: string[] = [];
+ const r = await correctionsScan(argv(["--apply", "--post", "#fleet"]), env(), {
+ jev: async (cs: CorrectionCandidate[]) => {
+ const verdicts: Record = {};
+ cs.forEach((c, i) => {
+ verdicts[c.messageId] = { noul: 0.9, kind: "behavior-gap", kind_confidence: 0.93, severity: i === 0 ? "p2" : "p0" };
+ });
+ return { status: "ran" as const, verdicts };
+ },
+ post: (_ch, text) => {
+ calls.push(text);
+ return { ok: true };
+ },
+ });
+ const json = r.json as Record;
+ const filed = json.filed as { severity: string }[];
+ expect(filed).toHaveLength(1);
+ expect(filed[0]!.severity).toBe("P0");
+ expect(calls).toHaveLength(1);
+ expect(calls[0]).toContain("P0");
+ });
+
+ it("the scan opens the DB READ-ONLY (chmod 444 — the server owns the DB)", async () => {
+ const { chmodSync } = await import("node:fs");
+ chmodSync(db, 0o444);
+ try {
+ const r = await correctionsScan(argv(), env(), { jev: fakeJev() });
+ expect(r.code).toBe(0);
+ expect((r.json as Record).candidates).toBe(2);
+ } finally {
+ chmodSync(db, 0o644);
+ }
+ });
+
+ it("below-threshold confidence never files", async () => {
+ const r = await correctionsScan(argv(["--apply"]), env(), { jev: fakeJev("ran", 0.5) });
+ const json = r.json as Record;
+ expect(json.admitted).toBe(0);
+ expect(json.filed).toEqual([]);
+ });
+
+ it("caps jev consultations at --max-jev and reports the overflow", async () => {
+ const r = await correctionsScan(argv(["--apply", "--max-jev", "1"]), env(), { jev: fakeJev() });
+ const json = r.json as Record;
+ expect(json.judged).toBe(1);
+ expect(json.overflow).toBe(1);
+ });
+
+ it("P0/P1 findings ping Slack through the injected seam", async () => {
+ const calls: { channel: string; text: string }[] = [];
+ const ok = await correctionsScan(argv(["--apply", "--post", "#fleet"]), env(), {
+ jev: fakeJev(),
+ post: (channel, text) => {
+ calls.push({ channel, text });
+ return { ok: true };
+ },
+ });
+ expect((ok.json as Record).ping).toMatchObject({ ok: true, channel: "#fleet" });
+ expect(calls).toHaveLength(1);
+ expect(calls[0]!.text).toContain("P0/P1");
+ });
+
+ it("a failed ping is a warning, never a failed pass (findings stay filed)", async () => {
+ const bad = await correctionsScan(argv(["--apply", "--post", "#fleet"]), env(), {
+ jev: fakeJev(),
+ post: () => ({ ok: false, error: "slack down" }),
+ });
+ expect(bad.code).toBe(0);
+ const json = bad.json as Record;
+ expect(json.ping).toMatchObject({ ok: false });
+ expect((json.filed as unknown[]).length).toBe(1);
+ expect(readFileSync(join(vault, "dashboards", "user-corrections.md"), "utf8")).toContain("ping FAILED");
+ });
+
+ it("refuses to run without a personal mount or --vault (never guesses a findings target)", async () => {
+ const prevRoot = process.env.AMICO_VAULTS_ROOT;
+ const prevDir = process.env.AMICO_VAULT_DIR;
+ delete process.env.AMICO_VAULT_DIR;
+ process.env.AMICO_VAULTS_ROOT = "/nonexistent-vaults-root-xyz";
+ try {
+ const r = await correctionsScan(["--db", db], env(), { jev: fakeJev() });
+ expect(r.code).toBe(64);
+ expect((r.json as Record).error).toMatch(/personal vault/);
+ } finally {
+ if (prevRoot === undefined) delete process.env.AMICO_VAULTS_ROOT;
+ else process.env.AMICO_VAULTS_ROOT = prevRoot;
+ if (prevDir !== undefined) process.env.AMICO_VAULT_DIR = prevDir;
+ }
+ });
+
+ it("usage errors: bad --days, bad --max-jev, missing DB", async () => {
+ expect((await correctionsScan(argv(["--days", "x"]), env(), { jev: fakeJev() })).code).toBe(64);
+ expect((await correctionsScan(argv(["--max-jev", "0"]), env(), { jev: fakeJev() })).code).toBe(64);
+ // direct call (no argv() helper) — the helper prepends --db with the seeded DB
+ expect((await correctionsScan(["--db", "/no/such/db.sqlite", "--vault", vault], env(), { jev: fakeJev() })).code).toBe(64);
+ });
+});
+
+// ── the CLI surface (bundle; AMICO_JEV_DISABLED → the fail-open path) ────────
+
+const BUNDLE = join(__dirname, "..", "dist", "amico.js");
+beforeAll(() => {
+ execFileSync("node", [join(__dirname, "..", "esbuild.config.mjs")], { cwd: join(__dirname, "..") });
+});
+
+function runCli(args: string[], env: Record = {}): { code: number; stdout: string } {
+ try {
+ const stdout = execFileSync("node", [BUNDLE, ...args], {
+ encoding: "utf8",
+ env: { ...process.env, AMICO_JEV_DISABLED: "1", ...env },
+ });
+ return { code: 0, stdout };
+ } catch (e) {
+ const err = e as { status?: number; stdout?: string };
+ return { code: err.status ?? -1, stdout: err.stdout ?? "" };
+ }
+}
+
+describe("the CLI dispatch", () => {
+ it("unknown subcommand → 64 with usage", async () => {
+ const r = await correctionsVerb(["bogus"]);
+ expect(r.code).toBe(64);
+ expect((r.json as Record).usage).toMatch(/amico corrections scan/);
+ });
+
+ it("the bundle reaches the scan and degrades fail-open with jev disabled", () => {
+ const db = join(mkdtempSync(join(tmpdir(), "amico-corrections-cli-")), "opencode.db");
+ const vault = mkdtempSync(join(tmpdir(), "amico-corrections-cli-v-"));
+ const ops = mkdtempSync(join(tmpdir(), "amico-corrections-cli-ops-"));
+ try {
+ seedDb(db, SEED);
+ const r = runCli(["corrections", "scan", "--db", db, "--vault", vault, "--apply"], { AMICODE_OPS_DIR: ops });
+ expect(r.code).toBe(0);
+ const json = JSON.parse(r.stdout) as Record;
+ expect(json.jev_status).toBe("disabled");
+ expect(json.unverified).toBe(2);
+ expect(json.filed).toEqual([]);
+ expect(json.dry_run).toBe(false);
+ expect(existsSync(join(vault, "dashboards", "user-corrections.md"))).toBe(true);
+ } finally {
+ rmSync(join(db, ".."), { recursive: true, force: true });
+ rmSync(vault, { recursive: true, force: true });
+ rmSync(ops, { recursive: true, force: true });
+ }
+ });
+});
diff --git a/packages/amico-run/test/notturno_gates.test.ts b/packages/amico-run/test/notturno_gates.test.ts
index 8afd27a77..044d05796 100644
--- a/packages/amico-run/test/notturno_gates.test.ts
+++ b/packages/amico-run/test/notturno_gates.test.ts
@@ -15,7 +15,7 @@
// origin/lane/* (or origin/main), and maps the diff to vitest packages.
import { describe, test, expect, beforeEach, afterEach } from "vitest";
import { spawnSync } from "node:child_process";
-import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync, appendFileSync } from "node:fs";
+import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync, appendFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
@@ -174,6 +174,83 @@ describe(".notturno/gates — the night runner's pre-push gate (#1980)", () => {
});
});
+// #2007: in the Modal sandbox the extension/amico-run vitest steps went red
+// whatever the diff, and the summary hid which tests. The vitest steps must run
+// the way CI's `fast` job does (CI=true, GITHUB_ACTIONS=true, an empty runner
+// HOME, no ambient secrets/dev-shell env), and a red suite must name its
+// failing files in the RESULT line (the runner surfaces only the last lines).
+describe(".notturno/gates — vitest steps mirror CI and name failing files (#2007)", () => {
+ let fx: string;
+ const VITEST_PKG = join(REPO, "packages", "amico-run", "node_modules", "vitest");
+
+ /** give a fixture package a real vitest (symlinked from this checkout) */
+ function linkVitest(pkg: string) {
+ const nm = join(fx, "packages", pkg, "node_modules");
+ mkdirSync(join(nm, ".bin"), { recursive: true });
+ symlinkSync(VITEST_PKG, join(nm, "vitest"));
+ symlinkSync(join(VITEST_PKG, "vitest.mjs"), join(nm, ".bin", "vitest"));
+ }
+
+ beforeEach(() => {
+ fx = makeFixture();
+ linkVitest("foo");
+ linkVitest("bar");
+ });
+ afterEach(() => {
+ rmSync(fx, { recursive: true, force: true });
+ rmSync(upstreamOf(fx), { recursive: true, force: true });
+ });
+
+ test("the vitest step runs under CI's env: CI=true, GITHUB_ACTIONS=true, a scratch empty HOME, ambient secrets scrubbed", () => {
+ const outerHome = process.env.HOME ?? "";
+ put(fx, "packages/foo/test/env.test.ts", [
+ `import { test, expect } from "vitest";`,
+ `import { existsSync, readdirSync } from "node:fs";`,
+ `import { join } from "node:path";`,
+ `test("ci env", () => {`,
+ ` expect(process.env.CI).toBe("true");`,
+ ` expect(process.env.GITHUB_ACTIONS).toBe("true");`,
+ ` const home = process.env.HOME ?? "";`,
+ ` expect(home).not.toBe(${JSON.stringify(outerHome)});`,
+ ` expect(home).not.toBe("");`,
+ ` expect(existsSync(join(home, ".amico"))).toBe(false);`,
+ ` expect(existsSync(join(home, ".config", "opencode"))).toBe(false);`,
+ ` expect(readdirSync(home)).toEqual([]);`,
+ ` expect(process.env.AWS_SECRET_ACCESS_KEY).toBeUndefined();`,
+ ` expect(process.env.GH_TOKEN).toBeUndefined();`,
+ ` expect(process.env.AMICO_LEAK).toBeUndefined();`,
+ ` expect(process.env.NOTTURNO_ISSUE).toBeUndefined();`,
+ `});`,
+ ``,
+ ].join("\n"));
+ const r = run("./.notturno/gates", [], fx, {
+ NOTTURNO_GATES_STEPS: "vitest",
+ AWS_SECRET_ACCESS_KEY: "leak",
+ GH_TOKEN: "ghp_leak",
+ AMICO_LEAK: "1",
+ NOTTURNO_ISSUE: "2007",
+ });
+ expect(r.status, r.out).toBe(0);
+ expect(r.out).toMatch(/1 passed/);
+ expect(lastLine(r.stdout)).toMatch(/GREEN/);
+ });
+
+ test("a red suite names its failing test files (first 5) in the RESULT line", () => {
+ for (const n of ["a", "b", "c", "d", "e", "f", "g"]) {
+ put(fx, `packages/foo/test/${n}.test.ts`, `import { test, expect } from "vitest";\ntest("${n}", () => { expect(1).toBe(2); });\n`);
+ }
+ put(fx, "packages/foo/test/ok.test.ts", `import { test, expect } from "vitest";\ntest("ok", () => { expect(1).toBe(1); });\n`);
+ put(fx, "packages/bar/test/bad.test.ts", `import { test, expect } from "vitest";\ntest("bad", () => { expect(1).toBe(2); });\n`);
+ const r = run("./.notturno/gates", [], fx, { NOTTURNO_GATES_STEPS: "vitest" });
+ expect(r.status, r.out).not.toBe(0);
+ const summary = lastLine(r.stdout);
+ expect(summary).toMatch(/RED/);
+ expect(summary).toContain("vitest:foo [test/a.test.ts, test/b.test.ts, test/c.test.ts, test/d.test.ts, test/e.test.ts, +2 more]");
+ expect(summary).toContain("vitest:bar [test/bad.test.ts]");
+ expect(summary).not.toContain("ok.test.ts");
+ });
+});
+
describe("this repo's committed tree", () => {
test("AGENTS.md's verification-gates section teaches the overlay rule: refresh_manifest, then drift_gate, before commit", () => {
const agents = readFileSync(join(REPO, "AGENTS.md"), "utf8");
diff --git a/packages/amico-run/test/shard_rehome.test.ts b/packages/amico-run/test/shard_rehome.test.ts
new file mode 100644
index 000000000..ff7893a0d
--- /dev/null
+++ b/packages/amico-run/test/shard_rehome.test.ts
@@ -0,0 +1,161 @@
+// `amico fleet shard-rehome` (amicode#1658) — the tested directory re-home for
+// client shards merged into the canonical DB.
+//
+// The properties this suite exists to defend (the issue's ACs):
+// 1. A merged session's directory either EXISTS on the hub or is the shard's
+// original, untouched directory — a missing target keeps the original.
+// 2. No re-homed directory contains a doubled path segment — the mapping is ONE
+// prefix substitution, and the suite asserts the exact candidate against
+// the real paths the 2026-10-01 hand-run mangled.
+// 3. Rows already hub-side or under foreign prefixes are untouched, never errors.
+// 4. The verb round-trips over the real python3 bridge on a seeded temp DB
+// (never the live chat DB): dry-run plans, apply lands + verifies, and a
+// failed verification is a nonzero exit.
+//
+// Run: pnpm --filter @amicode/amico-run test shard_rehome
+import { describe, it, expect, afterEach } from "vitest";
+import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { sqliteBatch } from "../src/sqlite_bridge.js";
+import { planRehome, shardRehome, type RehomeRow } from "../src/shard_rehome.js";
+
+const CLIENT_HOME = "/Users/aaron";
+const HUB_HOME = "/home/aaron";
+
+// the real directories the 2026-10-01 hand-run re-home doubled — the regression
+// fixtures. The hand-run produced armoniaa/harmoniqsoniqs; the mapping below must
+// produce the exact single-substitution candidate for each.
+const DOUBLED_CASES: Array<[string, string]> = [
+ ["/Users/aaron/armonia/data/vaults", "/home/aaron/armonia/data/vaults"],
+ ["/Users/aaron/armonia/repos/amicode", "/home/aaron/armonia/repos/amicode"],
+ ["/Users/aaron/armonia/repos/packages", "/home/aaron/armonia/repos/packages"],
+ ["/Users/aaron/harmoniqs", "/home/aaron/harmoniqs"],
+];
+
+function rowsOf(...directories: string[]): RehomeRow[] {
+ return directories.map((d, i) => ({ id: `ses_fix${i}`, directory: d }));
+}
+
+describe("planRehome — the pure mapping (table-tested, no fs)", () => {
+ const existsEverywhere = () => true;
+
+ it("AC1+AC2: a client-home directory with an existing hub target re-homes to the exact single-prefix candidate", () => {
+ for (const [from, to] of DOUBLED_CASES) {
+ const [d] = planRehome([{ id: "ses_a", directory: from }], {
+ clientHome: CLIENT_HOME,
+ hubHome: HUB_HOME,
+ exists: existsEverywhere,
+ });
+ expect(d.action).toBe("rehome");
+ expect(d.to).toBe(to);
+ expect(d.to).toBe(HUB_HOME + from.slice(CLIENT_HOME.length)); // the substitution, exactly once
+ }
+ });
+
+ it("AC2: a target missing on the hub keeps the shard's original directory — invisible-but-truthful beats invisible-and-wrong", () => {
+ const [d] = planRehome([{ id: "ses_a", directory: "/Users/aaron/nowhere/else" }], {
+ clientHome: CLIENT_HOME,
+ hubHome: HUB_HOME,
+ exists: () => false,
+ });
+ expect(d.action).toBe("keep");
+ expect(d.to).toBe("/Users/aaron/nowhere/else");
+ expect(d.reason).toMatch(/does not exist on the hub/);
+ });
+
+ it("AC3: already-hub-side and foreign-prefix rows are kept untouched, never errors", () => {
+ const plan = planRehome(rowsOf("/home/aaron/armonia/repos/amicode", "/private/tmp/oc-crash-test"), {
+ clientHome: CLIENT_HOME,
+ hubHome: HUB_HOME,
+ exists: existsEverywhere,
+ });
+ expect(plan.every((d) => d.action === "keep")).toBe(true);
+ expect(plan.every((d) => d.to === d.from)).toBe(true);
+ });
+
+ it("no doubled segments, structurally: the candidate is hubHome + from.slice(clientHome.length) for arbitrary nesting", () => {
+ const from = "/Users/aaron/a/b/c/d/e";
+ const [d] = planRehome([{ id: "ses_a", directory: from }], {
+ clientHome: CLIENT_HOME,
+ hubHome: HUB_HOME,
+ exists: existsEverywhere,
+ });
+ expect(d.to).toBe("/home/aaron/a/b/c/d/e");
+ expect(d.from).not.toContain("armoniaa");
+ expect(d.to).not.toMatch(/(\/[^/]+)\1/); // no segment repeats back-to-back
+ });
+
+ it("client home === hub home is a no-op keep", () => {
+ const [d] = planRehome([{ id: "ses_a", directory: "/home/aaron/x" }], {
+ clientHome: HUB_HOME,
+ hubHome: HUB_HOME,
+ exists: existsEverywhere,
+ });
+ expect(d.action).toBe("keep");
+ expect(d.to).toBe("/home/aaron/x");
+ });
+});
+
+describe("shardRehome — the verb over the real bridge on a seeded temp DB", () => {
+ let tmp: string;
+ let db: string;
+ let hub: string;
+ afterEach(() => rmSync(tmp, { recursive: true, force: true }));
+
+ function seed(directories: string[]) {
+ tmp = mkdtempSync(join(tmpdir(), "shard-rehome-"));
+ db = join(tmp, "canonical.db");
+ hub = join(tmp, "hub-home");
+ mkdirSync(hub, { recursive: true });
+ sqliteBatch(db, "rw", [
+ { sql: "CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT NOT NULL)" },
+ ...directories.map((d, i) => ({ sql: "INSERT INTO session (id, directory) VALUES (?, ?)", params: [`ses_${i}`, d] })),
+ ]);
+ }
+
+ function readAll(): Array<{ id: string; directory: string }> {
+ return sqliteBatch(db, "ro", [{ sql: "SELECT id, directory FROM session" }]).results[0].rows as Array<{
+ id: string;
+ directory: string;
+ }>;
+ }
+
+ it("dry-run plans the re-home without touching the DB", () => {
+ seed(["/Users/aaron/armonia/repos/amicode", "/private/tmp/oc-crash-test"]);
+ mkdirSync(join(hub, "armonia/repos/amicode"), { recursive: true });
+ const r = shardRehome(["--client-home", CLIENT_HOME, "--db", db], { hubHome: hub });
+ expect(r.code).toBe(0);
+ const j = r.json as Record;
+ expect(j).toMatchObject({ mode: "dry-run", scanned: 1, rehome_count: 1, kept_count: 0 });
+ expect(readAll()).toEqual([
+ { id: "ses_0", directory: "/Users/aaron/armonia/repos/amicode" }, // untouched by the dry-run
+ { id: "ses_1", directory: "/private/tmp/oc-crash-test" },
+ ]);
+ });
+
+ it("apply re-homes to the validated target and verifies what landed", () => {
+ seed(["/Users/aaron/armonia/repos/amicode"]);
+ mkdirSync(join(hub, "armonia/repos/amicode"), { recursive: true });
+ const r = shardRehome(["--client-home", CLIENT_HOME, "--db", db, "--apply"], { hubHome: hub });
+ expect(r.code).toBe(0);
+ const j = r.json as Record;
+ expect(j).toMatchObject({ mode: "apply", rehome_count: 1, verified: true });
+ expect(readAll()).toEqual([{ id: "ses_0", directory: join(hub, "armonia/repos/amicode") }]);
+ });
+
+ it("apply keeps the shard's original when the target does not exist on the hub", () => {
+ seed(["/Users/aaron/armonia/repos/amicode"]); // target dir deliberately NOT created
+ const r = shardRehome(["--client-home", CLIENT_HOME, "--db", db, "--apply"], { hubHome: hub });
+ expect(r.code).toBe(0);
+ const j = r.json as Record;
+ expect(j).toMatchObject({ rehome_count: 0, kept_count: 1 });
+ expect(readAll()).toEqual([{ id: "ses_0", directory: "/Users/aaron/armonia/repos/amicode" }]);
+ });
+
+ it("a missing --client-home fails closed with usage, and a relative path is rejected", () => {
+ seed([]);
+ expect(shardRehome(["--db", db], { hubHome: hub }).code).toBe(64);
+ expect(shardRehome(["--client-home", "Users/aaron", "--db", db], { hubHome: hub }).code).toBe(64);
+ });
+});
diff --git a/packages/app-bundle/manifest.json b/packages/app-bundle/manifest.json
index 1f961b1fd..c4f0c7ffc 100644
--- a/packages/app-bundle/manifest.json
+++ b/packages/app-bundle/manifest.json
@@ -13,22 +13,22 @@
"packages/app/package.json": "3c9bf3d487052d4834b7fd3ce93fcf6b7fc358de92b8be4b0e84fc424a971f74",
"packages/app/playwright.config.ts": "b7dae0b39248b4ddf7cba829af96ac7fe3cc919add9711ecb5292d9bd52e1805",
"packages/core/schema.json": "f23c01f0670b96f80db5bc3d8e326f311143087d290064bb6b4805d7201d5818",
- "packages/opencode/package.json": "01d9338ee64108d0bd6af80b971fa2da4c7bf63e7efae59cc97cd58a9cfce55c",
+ "packages/opencode/package.json": "b7e1946bec2d72302646fa7128e3686a73723bab9d5f0812984e5fda16e376d3",
"packages/opencode/pasqal-live-test.mjs": "2ff0edf222ce29660ef8d901c0b6f10951943ef01824d2d264339f8d720206f1",
"packages/opencode/pasqal-live-test.sh": "0bb0089eb413956f0252939f933a139c2f11520466887ad91fcae362f1698e41",
"packages/session-ui/bunfig.toml": "468114e9b52c3cf926d2385c12d93385992cd70c97ea9e32ed22154617b7799d",
"packages/session-ui/happydom.ts": "d81d6b14e988f058fa502c841df34b55307c57fc94a26fb4f8a5deb6aa0d4834",
- "packages/session-ui/package.json": "f354c8675067dc8dd1493924cd6c8f561fb838ff97ae6cbd7e024868cbf0e0e8",
+ "packages/session-ui/package.json": "e45791b38522223cab314dad535a960bc8d2dc5d8752a1d9d67677b417369339",
"packages/session-ui/preload-worker-mock.ts": "b53280029a042575734d7bfdf27713f6c5cd6a6665599dc429d839a3e3372bd1",
- "packages/ui/package.json": "b1d168d0371e9094faae1107fc6c00be197f09bc69daa2247a3890d607f4b629",
+ "packages/ui/package.json": "40b60d6bcb07157d998c431f42d2732d1dd96bcf1e0c1d1514f39428f5b82116",
"packages/app/public/amico.svg": "7c64b145bdd063bacd53318336a423f0654ec702de2b4e0c4a4a384694052804",
"packages/app/public/oc-theme-preload.js": "27227e802b3494e7c545da903e679efdb30ccc754cd4eb5cdf08005a40d560b6",
"packages/app/src/app-desktop-commands.test.ts": "1d2057f238dc14e4407ff5c4e53afe091784790bc0fd0dbfde0eacd91e2bd968",
- "packages/app/src/app.tsx": "6fe2c1136330b0b76beb10b15c0ca9df2c62877c98ba3eb4cb91844125916a82",
+ "packages/app/src/app.tsx": "c475ffe441d06e0392e6d5b436e564e67ce5eb17cd17ecc4d3afed6d2fc0972e",
"packages/app/src/design-polish.css": "abe7515fe2ffdc090a51302f910d4b0873fb741f1534bd5196ddecc71fc7b714",
"packages/app/src/desktop-menu.ts": "350e0e9f2e4294bfe1b1f357b3cd2ea0908b14d20ca1e79fda2305db1958fb24",
- "packages/app/src/entry.tsx": "f35e1017f4c9d478d254b2a38043e5750064b6bef25169c3e07ae9f72ff1049c",
- "packages/app/src/index.css": "2c11df3dcebb381358e06626094b5dda3c35688ad7940d643b2cbb6cff1ea9e9",
+ "packages/app/src/entry.tsx": "080b3c11cdd61dfaf4bea8132970a1a300b77b0e8fc53065e42d5ccd75eaed01",
+ "packages/app/src/index.css": "727656c5e7ff7d7c7ff4fc95a3ec7c9148a34f1f974a58a297285e870519be5d",
"packages/app/src/theme-preload.test.ts": "d9e4e96dd39a3491493637682611bedf6ddf4b6e4dbdddcb60bf006211b137a1",
"packages/app/test-browser/empty-workspace-landing.test.ts": "83d19ecf5da37c234dbde1a004432865204d7f412a335c4e470a07e73d9bcda4",
"packages/app/test-browser/keybind-migration.test.ts": "192418880d432ff62d15fd80f6250ece6233e43d0dbe11d5a4fbb5f4d9e45a06",
@@ -108,7 +108,10 @@
"packages/app/src/components/amicode-default-model.test.ts": "941750d074706bd5f4e433c9737b19ebc0ac0f9ba9cd1faaa599a24b22126e05",
"packages/app/src/components/amicode-default-model.tsx": "16a2de1feb12e07416fe5fbb25879391f912ee0909b553c868c1b531f2ed1f2d",
"packages/app/src/components/amicode-defaults-capsule.tsx": "94dc081bc47abb7e50566fcd8e9e53124f9d7632d61f93eff4f1627e0c2efb2d",
- "packages/app/src/components/amicode-file-ref-bridge.tsx": "bd5c295080ccd80fe89208008c02db209ba301e431f61bc4432d3c8f2a72401f",
+ "packages/app/src/components/amicode-file-ref-bridge.tsx": "3371f36847c8a8ad4dcea9d925a4671ed7894fe1e9c083202ec53c746b5fcfe7",
+ "packages/app/src/components/amicode-local-image-loader.test.ts": "5629033ad71dec1c3d0b41aecc28d6529a619129147a844a063996d0c860d4e3",
+ "packages/app/src/components/amicode-local-image-loader.ts": "95c2833c9628a2ed79fe13d151699b3a3b310fae44e91672d839553d4466e418",
+ "packages/app/src/components/concise-mode-toggle.tsx": "3eff0c5557143500a27d828a08d1efe09a8c99aac9b6e2f9950bfe70c76162a1",
"packages/app/src/components/connection-banner.tsx": "82194291181165ed7b1b1b3a6801a1bdcd2c2b7cdd733c28486bd69f86477a24",
"packages/app/src/components/dialog-command-palette-v2.css": "85984957adaf15efcc0828310f3a0b438cedaaba776a9fec2eddb3ad83487fa2",
"packages/app/src/components/dialog-command-palette-v2.tsx": "e12da3e5e9e503089dd9139b20e04f6b566cf74fa445f23a2e193f5883f7c5f3",
@@ -148,7 +151,7 @@
"packages/app/src/components/pane-bridge.tsx": "756b3306b51c963fac6778716f6c6c4626f0f3bf75c93b7cb4cab58ea0bf3d4e",
"packages/app/src/components/profile-popover.tsx": "eff742e90544455f13234a9351bb1b82189663cba2cd3172bdcea1a8a37284e8",
"packages/app/src/components/prompt-input-clipboard-structure.test.ts": "946b718c2164da394602f26e4d9919d752da0d0b9bee714f345ba16c274542da",
- "packages/app/src/components/prompt-input-v2.tsx": "a37d61cd44e630100758c3de3dbf8cf7e0d4d0e85dc54e6bb7bf38290ece4468",
+ "packages/app/src/components/prompt-input-v2.tsx": "f0b637e46737e3c7345957a0167c878c8e9870a15bb850057665a4444ea91fe8",
"packages/app/src/components/prompt-input.tsx": "c127f81e2c0de4bdbc21a349473df39c623d8d7e2df8df0c03ea60bbef101a25",
"packages/app/src/components/prompt-project-selector.tsx": "0157c954b938bce2b5aca9a82f4a72338e361f4c9027e55a7fd5a14a1654d294",
"packages/app/src/components/question-inbox.tsx": "5ce03a30b7110092ddffccf3bf38e6c1b1a79624f378c819be7019725c96478a",
@@ -166,7 +169,7 @@
"packages/app/src/components/settings-providers.tsx": "c86c9bf4a6f6e198dc7b0995a69d5b76e851144987bbe601802b1b0120ee4ac4",
"packages/app/src/components/solver-switch-banner.tsx": "ec7edeec6dc05a4a06b39241b4f3a8e7534da97722dd8ec6f26ff782025802e9",
"packages/app/src/components/split-frame.tsx": "44c53737f72027b461097c03df48a4f411d04a82d02b1fce4901813c73dfcc72",
- "packages/app/src/components/status-popover-body.tsx": "2b6aa39a4358bbce7b6836b6a719be8eb24db96cd603bc0b4fb9fb0365413fed",
+ "packages/app/src/components/status-popover-body.tsx": "8efb35f2cf50f61a6440696056249b1fbc68d49b458dec95c357721bd4a77b70",
"packages/app/src/components/status-popover-indicator.test.ts": "e3530caa3a46b822603a8d0a753550605951c2cc5e95c60c46636935f2398a7a",
"packages/app/src/components/status-popover-indicator.ts": "d705c258e3404567fa5ae6d8ce9c27010832f12fdd614cef21f0297154e7faae",
"packages/app/src/components/status-popover-model.test.ts": "10634035bab0f31de24564e0d6c8413a0f7434fb2c08ac3ef722981a69dd08e7",
@@ -192,6 +195,8 @@
"packages/app/src/components/windows-app-menu.tsx": "262d085facace692084d79763a6bd8eba6c5ceb53b952a134e12c7e8a8a12946",
"packages/app/src/components/workbench-panel.tsx": "d16f477044098842a214a2e16f9be719e7688a04bcb575889ca544988f23c690",
"packages/app/src/context/command.tsx": "388d6cb12b6dc43ecb0827e3fb918370cb9d16d96161a5990dec10fff05af172",
+ "packages/app/src/context/concise-mode.test.ts": "534258a1d82d66bd8b482bb3a8ca6a24d508328505abd0f7f52e1b75cdf57fa2",
+ "packages/app/src/context/concise-mode.ts": "b03ac887222543e590d31781c944e69aca97bff32a661751dd817d46bf4ead64",
"packages/app/src/context/directory-sync.test.ts": "c03c7bcd95ac47b3de5e1e9719b6eccdb3569063026fe993b448453c818c7b48",
"packages/app/src/context/directory-sync.ts": "d5734fc1a32cd5cf1c00fa2cda8c5adc80b4211baf12e3d6715b69436130cc6c",
"packages/app/src/context/fleet-reseed-scheduler.test.ts": "a7f3fd7f861d2fe9d89e2eb20d125e61f6aa6180bb7d2ddb5422113a1beeba17",
@@ -205,7 +210,7 @@
"packages/app/src/context/layout-side-panel-tabs.ts": "e8dec07b2b5a5ebe68a76c98d688afa1360a4e31741a10bb49228afb8a7f3777",
"packages/app/src/context/layout-tabs.test.ts": "524327496e735379bb3425e78cbeb6cefa92e30296ddc44776eb8e99d644925d",
"packages/app/src/context/layout-tabs.ts": "741506ce165f68cdb0b8f2931a2dad3e26c05880f9286c3daf04ec979bac21c5",
- "packages/app/src/context/layout.tsx": "7767c8b5f64efd63390cbe99128686bec2f1da7e8f2b21530df4c3fd67d171bb",
+ "packages/app/src/context/layout.tsx": "9bb24aa18442cf0dba1dc749f49060de4d45e50bcd5b0e148325e8e4611fa2ca",
"packages/app/src/context/local-agent.test.ts": "a5a9d60bb4401d409218cc247c1cc06ede8b7878ddf01dc6080328f54eb9cade",
"packages/app/src/context/local-agent.ts": "0aab67e695dc3bb45a733ac0df80a0a5e14cfe29b2375b6dc3cd07fbccea33e2",
"packages/app/src/context/local.tsx": "3ab8b9fc2db082df4ba485373679f00a95d0ffe3c691a0afee1dc53db7eabd9e",
@@ -287,7 +292,7 @@
"packages/app/src/pages/new-session.tsx": "c060bbb15569b6da2f67146e6af0879ccb5444345df03dbc2f9b39cb98b9db78",
"packages/app/src/pages/overlay-content-hash.test.ts": "aa208098213522dcde42434408afe9c8641fad663f44a58c316922dbd0da2f5c",
"packages/app/src/pages/research-surfaces-absence.test.ts": "9944a8ec5492500eb82548d03b24ea7fb8ccc9c2ae5de14a4d18ddb309378746",
- "packages/app/src/pages/session.tsx": "7918f8bed4b1bd0932e5e6681546217fa24e5b6e81667163ef4b63439516328f",
+ "packages/app/src/pages/session.tsx": "a115ebf2fda802db0cf4beda9fd68a521ec7500ba04f877d9c47b248f308f353",
"packages/app/src/utils/amicode-bridge.ts": "3ec87c24c0384d312f7eb644807d9217a53b1b4b97f7fffadab1cc1595603f3b",
"packages/app/src/utils/amicode-bug-report.test.ts": "b1bbf49230ff3aa6d1a43ffa9165ba5a84ca4ee18a6d49f52ee5cd887c3e6444",
"packages/app/src/utils/amicode-bug-report.ts": "d55d9cecfe17719c0d4a0ca498345c7eff6fca1de6b645e4bc3fecb40f2f4645",
@@ -316,7 +321,7 @@
"packages/app/src/utils/retry-import.ts": "591bfd3e28f8afa9c4d9f0ca118d20e875cb1dce574c0ae25615994c4fe57a99",
"packages/app/src/utils/serialize-session.test.ts": "f8fe318f195dca251eae3f4899f4a5881c3523522f59e02fef208588391f4048",
"packages/app/src/utils/serialize-session.ts": "c303c678500cdb64c7aa6928a0bd984c441a36705fe8152368a0e575cbcb459f",
- "packages/app/src/utils/server-compat.ts": "01413e13b87485770eb3dbec0b882aec4c8247dafc4dc74b7b4669fa5b20cf1a",
+ "packages/app/src/utils/server-compat.ts": "1cccb1aabde1bce6ee382c33db0dea9273a9519747b805b771dec060d3071b39",
"packages/app/src/utils/session-message.ts": "96f8f3dca62f70215ac2dccbc8d27cd5b247af3ecea1ac5d34a74ffc8af5d4d3",
"packages/app/src/utils/start-prompt.ts": "809d8ad0ab6de76a97503f8368ade25f771431be3f92eb8f039a2c65244ab20f",
"packages/app/src/utils/vscode-explorer-icon-theme.test.ts": "351deb0ff77e62d2585858105aaf34ab621298511798fcb626abee97bd644fe6",
@@ -511,13 +516,16 @@
"packages/session-ui/src/components/build-trace.test.ts": "6259c529fa667a3514bfee1c771e8f623bcb8c69e37ceb17ea541aa42e207451",
"packages/session-ui/src/components/build-trace.ts": "153e3182c3a37e4aca9c88bf0e2052b0ff4ec117a60d74d2c81567ab91180066",
"packages/session-ui/src/components/group-parts.test.ts": "a31ce19009e1d62e75e83b682190a4613da0eed334420409ed73a9e21834ae84",
+ "packages/session-ui/src/components/image-lightbox.tsx": "e03904142307429b9f48275d0e1d6ec4b9fcbbf24ea47420bc4de89db78e9e89",
"packages/session-ui/src/components/line-comment-styles.ts": "2077836c13bd3c3f1c39c67722119e448a2dfa123377f94bfd3676634e955d00",
"packages/session-ui/src/components/markdown-file-refs.test.ts": "2172d17a0ee5e49636c8bd323f4fb797a7f41e469d0098864b82d374f0c10144",
"packages/session-ui/src/components/markdown-file-refs.ts": "417417f3660fe18f04a13ca4765b155b5479d7ce41b3638a57ba1abaa5830054",
+ "packages/session-ui/src/components/markdown-local-images.test.ts": "f344e43f4272841e867909c31955655be9b6dbfd81c986d8c6910b3ae01f6d33",
+ "packages/session-ui/src/components/markdown-local-images.ts": "566cb252bed28e300a93efaefd0f1bf3d893c77d2bccfa35f1fc6b961341702c",
"packages/session-ui/src/components/markdown-stream.test.ts": "b73b1a15ffa2cfea5358b2d6313ce17d26a5a11302b0e3b7d823cde0c98cbc83",
"packages/session-ui/src/components/markdown-stream.ts": "50e38abd5d4ff919564655799678be10463dec58502fc5487c1c97a9a8db5399",
- "packages/session-ui/src/components/markdown.css": "24e7c19218620386647656a5d27e116cdeca911f1916c5d5b8609edca11ff165",
- "packages/session-ui/src/components/markdown.tsx": "00a2248ca301e39eb5f2a9b78abfe9d11068ea5daa970516e1f8700c44878c48",
+ "packages/session-ui/src/components/markdown.css": "e5654bf01501ab93bcad54b488b460d6ca283da9875978557f853106738ec830",
+ "packages/session-ui/src/components/markdown.tsx": "9eba82666012819518e4e9145e75373e3a6d1d0d33738da639c8e5be117c43c5",
"packages/session-ui/src/components/message-part-css.test.ts": "bc164695d0195684d646ebf490ba7bb4ed72adcf2bf3d01b00de47f4f03bcd4f",
"packages/session-ui/src/components/message-part-groups.ts": "37be069bf46e926fcde83d3744ee706911ff002e6b532e279290d841aeee96da",
"packages/session-ui/src/components/message-part-skill.test.ts": "453bdcb25e4df91ad1004635b8766070a60421355adf18ee53f43cd2fa7d0486",
@@ -543,7 +551,8 @@
"packages/session-ui/src/components/tool-status-title.tsx": "ae4561b58f55985d2294a352802e723b18943ac9a6551d97f1c4a6cc95368c8a",
"packages/session-ui/src/context/data.tsx": "7b840b5bf6929445d21da2cbd8e8006316c6950a09d559ea77e8c397e728cd99",
"packages/session-ui/src/pierre/worker.ts": "7f210f931777956567eaeea15f2f5478befc7874a64a4b685504c59c66503fee",
- "packages/session-ui/src/util/clipboard.ts": "c37ba53914714ed04135e5a254e8ea90186f097d7470fa0873a83de82f3fef2d",
+ "packages/session-ui/src/util/clipboard.test.ts": "16dc2c7bc04f00134841e6e1e6b503f519a14aa41e4cb83cb1d63002a770992c",
+ "packages/session-ui/src/util/clipboard.ts": "89dc04d0faec989f48c44d82107a5b3d024f078f6146998f2392672b1647131e",
"packages/ui/src/amicode/activity-dot.tsx": "fc3c9e620cdd204e930f0d53a0a44456e0c886679174c91f0efbf9707daa7bc6",
"packages/ui/src/amicode/activity.test.ts": "1674ca5138780242767b0eb42fc444a9b8ce7868ac119117dd1487b7209180b2",
"packages/ui/src/amicode/activity.ts": "a2c9b4fe27175a400ea0bcfeb433dfcb572cd793766bf0fd232c741feef5cfbf",
@@ -742,6 +751,9 @@
"packages/app/src/amicode/inspector/inspector-bridge.ts": "460dbf4509b4e56a8eacd6cac7adcb3a63c6cd3e417d851df5f3e3b259095d3e",
"packages/app/src/amicode/inspector/inspector-context.tsx": "ac81b9e0e2a4035f85b6af68b7f0cefcb9f256a175b244ba13136f3a307c0c77",
"packages/app/src/amicode/inspector/run-inspector.tsx": "63a6351fad12a8377550eb87f7602a712bae586fe482ff46cd847c8544fc6e8a",
+ "packages/app/src/amicode/whats-new/whats-new-model.test.ts": "49c8b21d35f286c174456991584f66f99b4fdcec6b438b84317d8c6abc43987c",
+ "packages/app/src/amicode/whats-new/whats-new-model.ts": "30ef4aa0f3b04c6cd2743bb757cadf198d6fd6e463301b8c5645b1cbdd877cbd",
+ "packages/app/src/amicode/whats-new/whats-new.tsx": "a7ee5d41f4e17b80528efa451b7c15be2bd194813c0b2d4bd22a560dcc7944a7",
"packages/app/src/components/prompt-input/attachments.ts": "c3c28df0d2f18378d39e17147a4c64e8ea01d63b97c39e1874ed9445ca671bb1",
"packages/app/src/components/prompt-input/clipboard-bridge.test.ts": "15f76dca1a25d024128718e4099220b79ca029263410aab55502cf9caf9b2914",
"packages/app/src/components/prompt-input/clipboard-bridge.ts": "4b5a1edf59d4c85268d6ff72aa9f9d66f82d4fdb887121fb978c09caf4c5be65",
@@ -754,18 +766,25 @@
"packages/app/src/components/prompt-input/placeholder.test.ts": "d3cee5e2c758faf5fda626e270adad81129622bbc2bf2c335c2bffe1544165c3",
"packages/app/src/components/prompt-input/placeholder.ts": "6b2af23a0588e7970cd0dd059fc60c3671c907a76ba3db660483a932f1dce79d",
"packages/app/src/components/prompt-input/slash-popover.tsx": "a1800173edebcd419802796470f7270acaf0ef689a72f1b81a1cedfd8968337d",
+ "packages/app/src/components/prompt-input/submit-concise.test.ts": "5d76bad06ced24bda9e5be84f6a01577145bcf2ad6dfa55d2941344da2a3d935",
"packages/app/src/components/prompt-input/submit-stream-gap.test.ts": "351f0f5a8cf7aff55c859cb82fbca388ef848831256b80f74ee52ab9cf5d07c6",
- "packages/app/src/components/prompt-input/submit.ts": "cdb01cf2f2485fbbdb7ddc77dcdea99197d8c03bd302b8fe032bee8c15f525c3",
+ "packages/app/src/components/prompt-input/submit.ts": "42c2177bc7f36355d65cd363e5ea2780d16c960991b5347a8ceadaccf11b8b65",
"packages/app/src/components/server/server-row.tsx": "caf3179cf0b34b00fcab6ed57a324ff1f90f571f604216c49164dc54bd9f12ab",
"packages/app/src/components/session/context-warning-banner.tsx": "c4ceb3f9589ebd21cc66e21cef209552fc5fcff6cd825da5069c2259c8d33e50",
"packages/app/src/components/session/index.ts": "f0d58fa3a336639566ec51a10b692649707f91a7b780c0b5c4dcf18642d030e8",
"packages/app/src/components/session/panel-menu.tsx": "42f323046b7375ae158023a51506038eb15f2e1545c407f45acd47446a8ef3e0",
- "packages/app/src/components/session/pdf-canvas-view.tsx": "591bac93a2e1e10a1b42404a29bcc5222ebbc5faf734c80556fbca35c90551dd",
+ "packages/app/src/components/session/pdf-canvas-view.tsx": "17c73d71799998741555191be71a8b8b1760ba35305c53a1077722fc80aaf666",
+ "packages/app/src/components/session/pdf-find-controller.ts": "0116c48bb5f463d9bfda938eb52d5ec52c49992a9cfde2ef3ed1051b0cb14b1c",
+ "packages/app/src/components/session/pdf-find-core.test.ts": "7c002a9e79bf10a167a643477fc2e9c0c3c8257db905c31ddc12cf9e15bab1cd",
+ "packages/app/src/components/session/pdf-find-core.ts": "9da5cd17d1140c0080f702e7ff7f74b7a771bcc1e910573a1af7021189761dac",
+ "packages/app/src/components/session/pdf-find-guard.ts": "61976e19ebae5d5bc17c87aea41332ba1cb650d0e0ef981e51c864a086d21292",
+ "packages/app/src/components/session/pdf-find-highlight.ts": "6299e9f68dc261d18100b5e3e96b42ea74e47db93ac49fe94ef567e5f6581454",
+ "packages/app/src/components/session/pdf-find-pill.tsx": "1e298a28d9b986a749ad15619623504be8126a4161af24ca674ffd978120505b",
"packages/app/src/components/session/pdf-restore.test.ts": "4cd4cabbdc8063deb5fe95b5e637cf154ccdf864c5ff9f2f6833826bd69e4f90",
"packages/app/src/components/session/pdf-restore.ts": "e5c92096b9302a452439babfc2732610a7e0d18e23793ce3bf6352fa32f91a36",
"packages/app/src/components/session/preview-file-helpers.test.ts": "e0c1ccb40b48990c0c5b87ea62a35de60b860cd760067c7f1327cbb1d36584b1",
"packages/app/src/components/session/preview-file-helpers.ts": "2244aaef25f7412df7b53a45a7d2f72380d45eb90caefa5e96c48d01e988eb4e",
- "packages/app/src/components/session/preview-file-view.tsx": "ccb0399603e9e68da04d767b24be36d39e033354b25c2fe59f8a00007dbc7f1c",
+ "packages/app/src/components/session/preview-file-view.tsx": "caccb771210950896631ac7efd4cb3ca5689e0ba5049d9e2f53154ca86bb07b9",
"packages/app/src/components/session/preview-frontmatter-card.tsx": "00f439e3bf482673dfb06c0ce3464c0f51985093ed57b321cdebd38d452f2e5d",
"packages/app/src/components/session/preview-frontmatter.test.ts": "93ec9f7f4c604d8ed8ee6009779c425bffecfca331a122a7a700b588a8ca3c0c",
"packages/app/src/components/session/preview-frontmatter.ts": "036250edd8c2886ee8f8ac0723f2b9a41d363898006a53f47bc44b8194c5a3d3",
@@ -779,11 +798,11 @@
"packages/app/src/components/session/session-header.test.tsx": "733f48a613d3d1fb57ae87a38a0f857d1f37261ee4b1db2bcf0dd8457ec6c196",
"packages/app/src/components/session/session-header.tsx": "8d5e943e3b2b338e519530842e5c06cdb30ca5d393d9652cc362e0105f0f7e64",
"packages/app/src/components/session/session-new-view.tsx": "98298ae1c8745b8c3d5ec738e4c2354eb84bd7b3d80639fbb32f96374e7be5dd",
- "packages/app/src/components/session/session-preview-tab.tsx": "d3a210598d181c0aa29e9f3b5db3cd691169f3ee467001313a3d9a24bee93dbe",
+ "packages/app/src/components/session/session-preview-tab.tsx": "8f1244abea55bbed6f6bcb321d4c8f3d10bcbf8f4ead7992e51f04ca525de88f",
"packages/app/src/components/session/session-preview-tabs.test.ts": "773fc0116c5f61302eaa4a09fd5f91e6b26dee978c89e29c21714243ae3a5653",
"packages/app/src/components/session/session-preview-tabs.ts": "672d147a6f1f46a7ac2eff7aff9da6cc069c476ff918e5ff4aed3a997310e6fe",
- "packages/app/src/components/session/session-preview-tree.test.ts": "6d1677caa56ec89fa5fcc8fc2f26fdcc1bc0a23217ce9c6d92888b272523c00a",
- "packages/app/src/components/session/session-preview-tree.ts": "13930fcd8a8ae9df4c37ba9ea306e162fd08f0e429249ddea21d937675d3ad7e",
+ "packages/app/src/components/session/session-preview-tree.test.ts": "36aa705c79e39f42f4281e3ef3b6de41c445d18c044f5ce262a41a50bc5cdc59",
+ "packages/app/src/components/session/session-preview-tree.ts": "8cc3fc50299ffebedd562640e157892461cb1372501cc58d50328b10297ee892",
"packages/app/src/components/session/session-sortable-tab.tsx": "a015277347fe679ac1afa1372202de49fc98a0930ac10682c8897f70a0300909",
"packages/app/src/components/session/session-sortable-terminal-tab-v2.tsx": "08db0e378c3e07d243121f40c77e153bafe897e5e2ececd48a3e00786793032b",
"packages/app/src/components/session/use-context-warning.ts": "af7a6d0159a5541aa02ad4d08fd694af1cc4853fc1c0a70763bc264a634d1c53",
@@ -840,6 +859,8 @@
"packages/app/src/pages/new-session/new-session-view.test.ts": "eb337485bf03facbc59d9eadac37cfeb04f75bf97814df80d3be4d0ec9455047",
"packages/app/src/pages/new-session/new-session-view.tsx": "9390bb0c6d24d6ad1cbc753b6a57d85488a7355264e37641991a931769a3f482",
"packages/app/src/pages/session/amicode-session-relay.integration.test.ts": "4991e775d3a6c63a514072ccfa5aad190c586383b77ef41689c853fb271d3bf4",
+ "packages/app/src/pages/session/composer-refocus.test.ts": "fb2fc95d2b4284e039f59552fe82232f1c190a613b40ef39529b310c92aeea30",
+ "packages/app/src/pages/session/composer-refocus.ts": "4e837f00cd0e606d32ca648a0f310dfd605a897f17efc82696f257af8d1802f5",
"packages/app/src/pages/session/fleet-manager-tab.test.tsx": "f3bb956e343ccd82acd8bf38c9ad2413d48b24ec9f484c5dfd3939d9e07139b7",
"packages/app/src/pages/session/fleet-manager-tab.tsx": "1e7db225a1f2fa3234e0eb47f314de49185db1f3ebddb5585fd0247184adebaa",
"packages/app/src/pages/session/fleet-manager.test.ts": "b10aa624fba721c66e295a08f5bb862363701e09fe870295028c8d4b39d3b850",
@@ -851,7 +872,7 @@
"packages/app/src/pages/session/session-panel-width.test.ts": "9482afba7fbce254cbbd21ef885e9e9620b981616681635376f466478f611155",
"packages/app/src/pages/session/session-panel-width.ts": "02ee3a02ed78db2eac47c2528055cf78aef95f389641e929ddc56c17471dcbe2",
"packages/app/src/pages/session/session-side-panel-structure.test.ts": "b2d1d007d9200d0ac4a0889e45c2dbe57a324a10cb26ae9ab8f420d3c890402b",
- "packages/app/src/pages/session/session-side-panel.tsx": "fdf3b2eb6fe7484adb03f6b7fb52dfab2c74a7a41628b8562340d657174abd13",
+ "packages/app/src/pages/session/session-side-panel.tsx": "2246a789faf29be5cf6c0eccdf62b22f236fd180f98534c37b94ddee9e6b2cd0",
"packages/app/src/pages/session/terminal-panel-v2.tsx": "68dad9307f1d2abf3ff9248e451bd08005acf01ddc46b30a6d01b58f4a90dce0",
"packages/app/src/pages/session/use-amicode-commands.test.ts": "65671d054c404edd2b799b0f6591a9c055e6ecb454a674fa0bd9a7fa6f68e111",
"packages/app/src/pages/session/use-amicode-commands.tsx": "88bfdad9ae6dfd920b8e6a14ffebd2cf9e52b8f67bb9284cb358ad940643ca67",
@@ -918,8 +939,8 @@
"packages/session-ui/src/v2/components/editable-diff-view-core.ts": "f01a341c4c1cdb4a9522f9ccd3c856bd3b9b7310c01d87327b3ce777052c8c59",
"packages/session-ui/src/v2/components/editable-diff-view.test.ts": "ab206fa17779e6e9015d343ae37e3067dfc1821a93b6ba566b7a0dce0fc19acb",
"packages/session-ui/src/v2/components/editable-diff-view.tsx": "f682e398ece864b5b854ca36698581c7e1cc5964eed96e7b45cabfcb64748b24",
- "packages/session-ui/src/v2/components/editor-core.test.ts": "bd6552c0e7080afd554cb57ad690cc1504555712274c8b02702c64bb09460234",
- "packages/session-ui/src/v2/components/editor-core.ts": "8306fc9b87fffe853c96212664ba5003c4e020c8779fc7d5410c26b543736a7e",
+ "packages/session-ui/src/v2/components/editor-core.test.ts": "c758e1fb20da30cae44a3311ddd27591a074f137020b28b79d9d879666ee37d6",
+ "packages/session-ui/src/v2/components/editor-core.ts": "efc2b5f6fe774f1be862184db381f9229f1e642cf93d1f4ca925ed39ceb211d0",
"packages/session-ui/src/v2/components/file-name-picker.test.tsx": "1ce9741e315b549165d390c74b5885a43580aa70e24de987f9ba8388cd521b41",
"packages/session-ui/src/v2/components/markdown-utils.test.ts": "10e1034293bf385cf50d860bf1667d8370abc61dcee0c5a93295f21f5f55876a",
"packages/session-ui/src/v2/components/markdown-utils.ts": "3bc47cfed5b99ab23f857d5324be3189f3a7e800d991103d72ef1c2f970b910b",
@@ -1049,10 +1070,10 @@
"packages/ui/src/v2/components/text-shimmer-v2.tsx"
],
"exceptions": [],
- "extracted_at": "2026-10-29T08:48:42.118Z",
+ "extracted_at": "2026-10-29T13:09:53.406Z",
"per_package": {
"packages/app": {
- "A": 272,
+ "A": 289,
"M": 166,
"D": 0
},
@@ -1087,7 +1108,7 @@
"D": 0
},
"packages/session-ui": {
- "A": 45,
+ "A": 49,
"M": 32,
"D": 0
},
@@ -1098,7 +1119,7 @@
}
},
"counts": {
- "overlay_total": 1027,
+ "overlay_total": 1048,
"deletions": 7,
"server_coupled": 0
},
@@ -1204,6 +1225,9 @@
"packages/app/src/components/amicode-default-model.tsx": "A",
"packages/app/src/components/amicode-defaults-capsule.tsx": "A",
"packages/app/src/components/amicode-file-ref-bridge.tsx": "A",
+ "packages/app/src/components/amicode-local-image-loader.test.ts": "A",
+ "packages/app/src/components/amicode-local-image-loader.ts": "A",
+ "packages/app/src/components/concise-mode-toggle.tsx": "A",
"packages/app/src/components/connection-banner.tsx": "A",
"packages/app/src/components/dialog-command-palette-v2.css": "M",
"packages/app/src/components/dialog-command-palette-v2.tsx": "M",
@@ -1287,6 +1311,8 @@
"packages/app/src/components/windows-app-menu.tsx": "M",
"packages/app/src/components/workbench-panel.tsx": "A",
"packages/app/src/context/command.tsx": "M",
+ "packages/app/src/context/concise-mode.test.ts": "A",
+ "packages/app/src/context/concise-mode.ts": "A",
"packages/app/src/context/directory-sync.test.ts": "A",
"packages/app/src/context/directory-sync.ts": "M",
"packages/app/src/context/fleet-reseed-scheduler.test.ts": "A",
@@ -1606,9 +1632,12 @@
"packages/session-ui/src/components/build-trace.test.ts": "A",
"packages/session-ui/src/components/build-trace.ts": "A",
"packages/session-ui/src/components/group-parts.test.ts": "A",
+ "packages/session-ui/src/components/image-lightbox.tsx": "A",
"packages/session-ui/src/components/line-comment-styles.ts": "M",
"packages/session-ui/src/components/markdown-file-refs.test.ts": "A",
"packages/session-ui/src/components/markdown-file-refs.ts": "A",
+ "packages/session-ui/src/components/markdown-local-images.test.ts": "A",
+ "packages/session-ui/src/components/markdown-local-images.ts": "A",
"packages/session-ui/src/components/markdown-stream.test.ts": "M",
"packages/session-ui/src/components/markdown-stream.ts": "M",
"packages/session-ui/src/components/markdown.css": "M",
@@ -1638,6 +1667,7 @@
"packages/session-ui/src/components/tool-status-title.tsx": "M",
"packages/session-ui/src/context/data.tsx": "M",
"packages/session-ui/src/pierre/worker.ts": "M",
+ "packages/session-ui/src/util/clipboard.test.ts": "A",
"packages/session-ui/src/util/clipboard.ts": "A",
"packages/ui/src/amicode/activity-dot.tsx": "A",
"packages/ui/src/amicode/activity.test.ts": "A",
@@ -1837,6 +1867,9 @@
"packages/app/src/amicode/inspector/inspector-bridge.ts": "A",
"packages/app/src/amicode/inspector/inspector-context.tsx": "A",
"packages/app/src/amicode/inspector/run-inspector.tsx": "A",
+ "packages/app/src/amicode/whats-new/whats-new-model.test.ts": "A",
+ "packages/app/src/amicode/whats-new/whats-new-model.ts": "A",
+ "packages/app/src/amicode/whats-new/whats-new.tsx": "A",
"packages/app/src/components/prompt-input/attachments.ts": "M",
"packages/app/src/components/prompt-input/clipboard-bridge.test.ts": "A",
"packages/app/src/components/prompt-input/clipboard-bridge.ts": "A",
@@ -1849,6 +1882,7 @@
"packages/app/src/components/prompt-input/placeholder.test.ts": "M",
"packages/app/src/components/prompt-input/placeholder.ts": "M",
"packages/app/src/components/prompt-input/slash-popover.tsx": "M",
+ "packages/app/src/components/prompt-input/submit-concise.test.ts": "A",
"packages/app/src/components/prompt-input/submit-stream-gap.test.ts": "A",
"packages/app/src/components/prompt-input/submit.ts": "M",
"packages/app/src/components/server/server-row.tsx": "M",
@@ -1856,6 +1890,12 @@
"packages/app/src/components/session/index.ts": "M",
"packages/app/src/components/session/panel-menu.tsx": "A",
"packages/app/src/components/session/pdf-canvas-view.tsx": "A",
+ "packages/app/src/components/session/pdf-find-controller.ts": "A",
+ "packages/app/src/components/session/pdf-find-core.test.ts": "A",
+ "packages/app/src/components/session/pdf-find-core.ts": "A",
+ "packages/app/src/components/session/pdf-find-guard.ts": "A",
+ "packages/app/src/components/session/pdf-find-highlight.ts": "A",
+ "packages/app/src/components/session/pdf-find-pill.tsx": "A",
"packages/app/src/components/session/pdf-restore.test.ts": "A",
"packages/app/src/components/session/pdf-restore.ts": "A",
"packages/app/src/components/session/preview-file-helpers.test.ts": "A",
@@ -1935,6 +1975,8 @@
"packages/app/src/pages/new-session/new-session-view.test.ts": "A",
"packages/app/src/pages/new-session/new-session-view.tsx": "M",
"packages/app/src/pages/session/amicode-session-relay.integration.test.ts": "A",
+ "packages/app/src/pages/session/composer-refocus.test.ts": "A",
+ "packages/app/src/pages/session/composer-refocus.ts": "A",
"packages/app/src/pages/session/fleet-manager-tab.test.tsx": "A",
"packages/app/src/pages/session/fleet-manager-tab.tsx": "A",
"packages/app/src/pages/session/fleet-manager.test.ts": "A",
diff --git a/packages/app-bundle/overlay/packages/app/src/amicode/whats-new/whats-new-model.test.ts b/packages/app-bundle/overlay/packages/app/src/amicode/whats-new/whats-new-model.test.ts
new file mode 100644
index 000000000..2ef4b3b6c
--- /dev/null
+++ b/packages/app-bundle/overlay/packages/app/src/amicode/whats-new/whats-new-model.test.ts
@@ -0,0 +1,106 @@
+import { describe, expect, test } from "bun:test"
+import {
+ lastSeenStorageKey,
+ parseReleasesBody,
+ readLastSeenLocal,
+ releasesRoute,
+ requestWhatsNew,
+ onWhatsNewRequest,
+ resolveLastSeen,
+ writeLastSeenLocal,
+} from "./whats-new-model"
+
+function memStorage() {
+ const m = new Map()
+ return {
+ getItem: (k: string) => m.get(k) ?? null,
+ setItem: (k: string, v: string) => void m.set(k, v),
+ m,
+ }
+}
+
+describe("last-seen storage (web clients) is keyed by host", () => {
+ test("two hosts never share a last-seen version", () => {
+ const s = memStorage()
+ writeLastSeenLocal(s, "https://hub-a.example:4096/", "0.4.0")
+ expect(readLastSeenLocal(s, "https://hub-a.example:4096")).toBe("0.4.0")
+ expect(readLastSeenLocal(s, "https://hub-b.example")).toBeNull()
+ expect(lastSeenStorageKey("https://hub-a.example:4096/x")).toBe("amicode.whatsNew.lastSeen:hub-a.example:4096")
+ })
+
+ test("a junk stored value reads as never-seen", () => {
+ const s = memStorage()
+ s.setItem(lastSeenStorageKey("http://h"), "