sphica 0.6.28 → 0.6.30

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.
package/dist/deliver.js CHANGED
@@ -283,11 +283,6 @@ function explain(e, depth) {
283
283
  var MAX_BYTES = 2 * 1024 * 1024;
284
284
  var leaves = (rel) => rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel);
285
285
 
286
- // server/src/capture.ts
287
- import { spawn } from "node:child_process";
288
- import fs5 from "node:fs";
289
- import path6 from "node:path";
290
-
291
286
  // server/node_modules/kysely/dist/util/object-utils.js
292
287
  function isUndefined(obj) {
293
288
  return typeof obj === "undefined" || obj === undefined;
@@ -11062,6 +11057,31 @@ function parseObject(obj, jsonPath, options) {
11062
11057
  return target;
11063
11058
  }
11064
11059
 
11060
+ // server/src/authority.ts
11061
+ var AI_DECIDED = "A record marked decided by an AI was decided by an AI in an earlier session, not by the owner (unmarked delivered decisions are the owner's): with a concrete reason you may depart from it, saying in your reply which record and why. It never relaxes the owner's rules, public contracts, or approval gates.";
11062
+ var ownerAdopted = (unit) => sql`exists (select 1 from unit_adoption a where a.unit_id = ${sql.ref(unit)}
11063
+ and a.route in ('owner_statement', 'explicit') and a.retracted_at is null)`;
11064
+ async function authorityOf(db, ids, asOf) {
11065
+ const out = new Map(ids.map((id) => [id, "none"]));
11066
+ if (!ids.length)
11067
+ return out;
11068
+ const at = asOf ?? "9999";
11069
+ const rows = await db.selectFrom("unit_adoption").select(["unit_id", "route"]).where("unit_id", "in", ids).where("added_at", "<=", at).where((eb) => eb.or([eb("retracted_at", "is", null), eb("retracted_at", ">", at)])).execute();
11070
+ for (const r of rows) {
11071
+ if (r.route === "agent") {
11072
+ if (out.get(r.unit_id) === "none")
11073
+ out.set(r.unit_id, "agent");
11074
+ } else
11075
+ out.set(r.unit_id, "owner");
11076
+ }
11077
+ return out;
11078
+ }
11079
+
11080
+ // server/src/capture.ts
11081
+ import { spawn } from "node:child_process";
11082
+ import fs5 from "node:fs";
11083
+ import path6 from "node:path";
11084
+
11065
11085
  // server/src/kysely-node-sqlite.ts
11066
11086
  var plain = (row) => {
11067
11087
  const out = {};
@@ -11140,7 +11160,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
11140
11160
 
11141
11161
  // server/src/sqlite.ts
11142
11162
  var SCHEMA_GENERATION = 2;
11143
- var SCHEMA_REVISION = 9;
11163
+ var SCHEMA_REVISION = 10;
11144
11164
  var sphicaHome = () => process.env.SPHICA_HOME || path3.join(os.homedir(), ".sphica");
11145
11165
  var dbFile = () => process.env.SPHICA_DB || path3.join(sphicaHome(), "sphica.db");
11146
11166
  function requireRuntime() {
@@ -11304,6 +11324,7 @@ var CAPTURE_VIEWS = new Set([
11304
11324
  "capture_session",
11305
11325
  "capture_message",
11306
11326
  "capture_edit",
11327
+ "capture_tool_call",
11307
11328
  "capture_delivery",
11308
11329
  "capture_delivery_scoped",
11309
11330
  "capture_delivery_prune"
@@ -11312,6 +11333,7 @@ var TRIGGER_WRITES = {
11312
11333
  capture_session_insert: new Set(["session"]),
11313
11334
  capture_message_insert: new Set(["source"]),
11314
11335
  capture_edit_insert: new Set(["edit_observation"]),
11336
+ capture_tool_call_insert: new Set(["tool_call_observation"]),
11315
11337
  capture_delivery_insert: new Set(["delivery", "delivery_unit"]),
11316
11338
  capture_delivery_scoped_insert: new Set(["delivery", "delivery_unit"]),
11317
11339
  source_fts_ai: new Set(["source_fts"])
@@ -11359,6 +11381,7 @@ function captureAuthorizer(own, action, p1, p2, triggerOrView) {
11359
11381
  var writes = (action) => action === C2.SQLITE_INSERT || action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE;
11360
11382
  var INGEST_INSERTS = new Set([
11361
11383
  "project",
11384
+ "record_call",
11362
11385
  "ingest_source",
11363
11386
  "extraction_run",
11364
11387
  "source_processing",
@@ -11368,6 +11391,7 @@ var INGEST_INSERTS = new Set([
11368
11391
  "unit_adoption",
11369
11392
  "unit_link",
11370
11393
  "unit_state",
11394
+ "unit_replacement",
11371
11395
  "unit_anchor",
11372
11396
  "unit_alias",
11373
11397
  "field_def",
@@ -11388,6 +11412,7 @@ var INGEST_UPDATES = {
11388
11412
  unit_link: new Set(["resolved_at", "resolution"]),
11389
11413
  unit_evidence: new Set(RETRACTION),
11390
11414
  unit_adoption: new Set(RETRACTION),
11415
+ unit_replacement: new Set(["ended_at", "end_reason", "end_run_id"]),
11391
11416
  work: new Set(["title", "goal", "current", "next", "status", "branch", "run_id", "updated_at"])
11392
11417
  };
11393
11418
  var INGEST_DELETES = new Set(["artifact_link"]);
@@ -11396,7 +11421,6 @@ var INGEST_TRIGGER_WRITES = {
11396
11421
  source_fts_ai: ["insert source_fts"],
11397
11422
  source_fts_ad: ["delete source_fts"],
11398
11423
  unit_state_apply: ["update unit"],
11399
- unit_state_restore: ["insert unit_state"],
11400
11424
  delivery_ad: ["delete delivery_unit"],
11401
11425
  ...Object.fromEntries([
11402
11426
  "evidence_i",
@@ -11407,6 +11431,8 @@ var INGEST_TRIGGER_WRITES = {
11407
11431
  "adoption_d",
11408
11432
  "link_i",
11409
11433
  "link_u",
11434
+ "replacement_i",
11435
+ "replacement_u",
11410
11436
  "anchor_i",
11411
11437
  "anchor_u",
11412
11438
  "alias_i",
@@ -11441,7 +11467,14 @@ function ingestAuthorizer(action, p1, p2, trigger) {
11441
11467
  return C2.SQLITE_DENY;
11442
11468
  }
11443
11469
  var FORGET_WRITES = {
11444
- [C2.SQLITE_INSERT]: new Set(["forget_batch", "source_forgotten", "unit_state", "source_fts", "unit_fts"]),
11470
+ [C2.SQLITE_INSERT]: new Set([
11471
+ "forget_batch",
11472
+ "source_forgotten",
11473
+ "unit_state",
11474
+ "unit_replacement",
11475
+ "source_fts",
11476
+ "unit_fts"
11477
+ ]),
11445
11478
  [C2.SQLITE_DELETE]: new Set([
11446
11479
  "source",
11447
11480
  "unit_evidence",
@@ -11455,7 +11488,8 @@ var FORGET_WRITES = {
11455
11488
  };
11456
11489
  var FORGET_UPDATES = {
11457
11490
  unit: new Set(["lifecycle", "revision"]),
11458
- unit_state: new Set(["source_id"])
11491
+ unit_state: new Set(["source_id"]),
11492
+ unit_replacement: new Set(["ended_at", "end_reason", "end_forget_id"])
11459
11493
  };
11460
11494
  function forgetAuthorizer(action, p1, p2) {
11461
11495
  if (SHADOW.test(p1 ?? "") && writes(action))
@@ -12296,6 +12330,30 @@ async function readInput(stream) {
12296
12330
  raw += chunk;
12297
12331
  return JSON.parse(raw || "{}");
12298
12332
  }
12333
+ var RECORD_TOOLS = "mcp__plugin_sphica_record__";
12334
+ var OBSERVE_WAIT_MS = 5000;
12335
+ var hostId = (v) => v && /^[\x21-\x7e]{1,200}$/.test(v) ? v : null;
12336
+ async function observeRecordCall(file, input, owner) {
12337
+ const session = hostId(input.session_id);
12338
+ const toolUse = hostId(input.tool_use_id);
12339
+ const tool = hostId(input.tool_name);
12340
+ if (!session || !toolUse || !tool?.startsWith(RECORD_TOOLS))
12341
+ return;
12342
+ const cap = openWriter("capture", file, OBSERVE_WAIT_MS);
12343
+ try {
12344
+ await cap.insertInto("capture_tool_call").values({
12345
+ host: "claude-code",
12346
+ session_external: session,
12347
+ turn_id: hostId(input.prompt_id),
12348
+ tool_use_id: toolUse,
12349
+ tool_name: tool,
12350
+ owner_turn: owner ? 1 : 0,
12351
+ observed_at: iso(Date.now())
12352
+ }).execute();
12353
+ } finally {
12354
+ await cap.destroy().catch(() => {});
12355
+ }
12356
+ }
12299
12357
  async function main() {
12300
12358
  if (process.argv[2] === "--flush") {
12301
12359
  await flush();
@@ -12303,6 +12361,10 @@ async function main() {
12303
12361
  }
12304
12362
  const input = await readInput(process.stdin);
12305
12363
  const host = process.argv[2] === "codex" ? "codex" : "claude-code";
12364
+ if (host === "claude-code" && input.hook_event_name === "PreToolUse") {
12365
+ await observeRecordCall(dbFile(), input, isOwnerTurn(input));
12366
+ return;
12367
+ }
12306
12368
  if (host === "codex" && input.hook_event_name === "Stop")
12307
12369
  process.stdout.write("{}");
12308
12370
  const { flush: send, notice } = onHook(host, input);
@@ -12553,21 +12615,38 @@ function localChange(root, args) {
12553
12615
  // server/src/trace.ts
12554
12616
  var PENDING_DAYS = 14;
12555
12617
  var pendingCutoff = (now) => new Date(now.getTime() - PENDING_DAYS * 86400000).toISOString();
12556
- var untracedOwner = (eb) => eb.selectFrom("source as m").whereRef("m.session_id", "=", "s.id").where("m.author_kind", "=", "owner").where(({ not, exists, selectFrom }) => not(exists(selectFrom("source_processing as p").whereRef("p.source_id", "=", "m.id").select(sql`1`.as("x")))));
12557
- var untracedSessions = (db, projectId2) => db.selectFrom("session as s").where("s.project_id", "=", projectId2).where((eb) => eb.exists(untracedOwner(eb).select(sql`1`.as("x")))).select((eb) => [
12558
- "s.id",
12559
- "s.host",
12560
- "s.started_at",
12561
- "s.branch",
12562
- eb.selectFrom("source as o").whereRef("o.session_id", "=", "s.id").where("o.author_kind", "=", "owner").select((o) => o.fn.max("o.created_at").as("last")).as("last"),
12563
- untracedOwner(eb).select((m) => m.fn.countAll().as("waiting")).as("waiting"),
12564
- untracedOwner(eb).select((m) => m.fn.min("m.id").as("first")).as("first")
12565
- ]);
12618
+ var untraced = (eb, auto) => {
12619
+ const q = eb.selectFrom("source as m").whereRef("m.session_id", "=", "s.id");
12620
+ return (auto ? q : q.where("m.author_kind", "=", "owner")).where(({ not, exists, selectFrom }) => not(exists(selectFrom("source_processing as p").whereRef("p.source_id", "=", "m.id").select(sql`1`.as("x")))));
12621
+ };
12622
+ var untracedSessions = (db, projectId2, o = {}) => {
12623
+ const auto = o.auto ?? false;
12624
+ const skip = o.skip;
12625
+ return db.selectFrom("session as s").where("s.project_id", "=", projectId2).$if(Boolean(skip), (q) => q.where((eb) => eb.not(eb.and([eb("s.host", "=", skip?.host ?? ""), eb("s.external_id", "=", skip?.session ?? "")])))).where((eb) => eb.exists(untraced(eb, auto).select(sql`1`.as("x")))).select((eb) => [
12626
+ "s.id",
12627
+ "s.host",
12628
+ "s.started_at",
12629
+ "s.branch",
12630
+ eb.selectFrom("source as o").whereRef("o.session_id", "=", "s.id").where("o.author_kind", "=", "owner").select((o2) => o2.fn.max("o.created_at").as("last")).as("last"),
12631
+ untraced(eb, auto).select((m) => m.fn.countAll().as("waiting")).as("waiting"),
12632
+ untraced(eb, auto).select((m) => m.fn.min("m.id").as("first")).as("first")
12633
+ ]);
12634
+ };
12635
+ async function pendingSessions(db, projectId2, group, now = new Date, limit = 20, o = {}) {
12636
+ const cutoff = pendingCutoff(now);
12637
+ const base = db.selectFrom(untracedSessions(db, projectId2, o).as("w")).where("w.last", group === "recent" ? ">=" : "<", cutoff);
12638
+ const ordered = o.auto ? base.selectAll("w").orderBy("w.started_at").orderBy("w.id") : base.selectAll("w").orderBy("w.last", "desc");
12639
+ const [rows, total] = await Promise.all([
12640
+ ordered.limit(limit).execute(),
12641
+ base.select((eb) => eb.fn.countAll().as("n")).executeTakeFirst()
12642
+ ]);
12643
+ return { rows, total: Number(total?.n ?? 0) };
12644
+ }
12566
12645
 
12567
12646
  // server/src/status.ts
12568
- async function pendingCount(db, projectId2, now = new Date) {
12647
+ async function pendingCount(db, projectId2, now = new Date, o = {}) {
12569
12648
  const cutoff = pendingCutoff(now);
12570
- const r = await db.selectFrom(untracedSessions(db, projectId2).as("w")).select([
12649
+ const r = await db.selectFrom(untracedSessions(db, projectId2, o).as("w")).select([
12571
12650
  sql`count(case when w.last >= ${cutoff} then 1 end)`.as("recent"),
12572
12651
  sql`count(case when w.last < ${cutoff} then 1 end)`.as("older")
12573
12652
  ]).executeTakeFirst();
@@ -12578,6 +12657,8 @@ async function pendingCount(db, projectId2, now = new Date) {
12578
12657
  var CONFIRM = "If, after checking a record below against the current code and its full text (Sphica's read), what you were asked to do is a change it rejected or rules out, do not make that change yet: tell the user which record and reason it conflicts with, and ask whether to go ahead.";
12579
12658
  var CONFIRM_GOLD = CONFIRM.replace("its full text (Sphica's read)", "the record text given here");
12580
12659
  var ASK = CONFIRM.length + 1;
12660
+ var AI_ROOM = AI_DECIDED.length + 1;
12661
+ var AI_MARK = ", decided by an AI";
12581
12662
  var LIMITS = {
12582
12663
  session_start: { units: 6, chars: 1000 + ASK },
12583
12664
  pre_edit: { units: 5, chars: 1500 + ASK },
@@ -12598,8 +12679,29 @@ var noted = (text, lead, notes) => {
12598
12679
  ${l}`).join("");
12599
12680
  return { text: note ? `${text || lead}${note}` : text, note };
12600
12681
  };
12601
- var deliverable = (db, projectId2) => db.selectFrom("unit as u").where("u.project_id", "=", projectId2).where("u.lifecycle", "=", "active").where("u.extraction", "=", "supported").where("u.unsourced", "=", 0).where(({ not, exists, selectFrom }) => not(exists(selectFrom("unit_link as l").select("l.from_unit").where("l.kind", "=", "conflicts").where("l.resolved_at", "is", null).where((eb) => eb.or([eb("l.from_unit", "=", eb.ref("u.id")), eb("l.to_unit", "=", eb.ref("u.id"))])))));
12602
- var line = (u, extra = "") => `- ${inline(u.key)} (${u.kind}${u.stance ? ` ${u.stance}` : ""}): ${head(inline(u.text), 240)}${extra}`;
12682
+ var deliverable = (db, projectId2) => db.selectFrom("unit as u").where("u.project_id", "=", projectId2).where("u.lifecycle", "=", "active").where("u.extraction", "=", "supported").where("u.unsourced", "=", 0).where(({ not, exists, selectFrom }) => not(exists(selectFrom("unit_link as l").select("l.from_unit").where("l.kind", "=", "conflicts").where("l.resolved_at", "is", null).where((eb) => eb.or([
12683
+ eb.and([
12684
+ eb("l.from_unit", "=", eb.ref("u.id")),
12685
+ eb.or([eb.not(ownerAdopted("u.id")), ownerAdopted("l.to_unit")])
12686
+ ]),
12687
+ eb.and([
12688
+ eb("l.to_unit", "=", eb.ref("u.id")),
12689
+ eb.or([eb.not(ownerAdopted("u.id")), ownerAdopted("l.from_unit")])
12690
+ ])
12691
+ ])))));
12692
+ var line = (u, extra = "", ai = false) => `- ${inline(u.key)} (${u.kind}${u.stance ? ` ${u.stance}` : ""}${ai ? AI_MARK : ""}): ${head(inline(u.text), 240)}${extra}`;
12693
+ async function aiDecided(db, ids) {
12694
+ const whose = await authorityOf(db, ids);
12695
+ return new Set(ids.filter((id) => whose.get(id) === "agent"));
12696
+ }
12697
+ var withAi = (lead, chars, ai) => ai.size ? { lead: `${lead} ${AI_DECIDED}`, chars: chars + AI_ROOM } : { lead, chars };
12698
+ function fitMarked(lines, chars, lead, ai, ids) {
12699
+ const wide = withAi(lead, chars, ai);
12700
+ const f = fit2(lines, wide.chars, wide.lead, ids.map((id) => ai.has(id ?? -1) ? AI_MARK.length : 0));
12701
+ if (wide.lead === lead || f.kept.some((i) => ai.has(ids[i] ?? -1)))
12702
+ return { ...f, lead: wide.lead };
12703
+ return { ...f, text: f.text && `${lead}${f.text.slice(wide.lead.length)}`, lead };
12704
+ }
12603
12705
  var clip = (text, n) => {
12604
12706
  const chars = Array.from(inline(text));
12605
12707
  return chars.length <= n ? chars.join("") : `${chars.slice(0, n - 1).join("")}…`;
@@ -12625,19 +12727,24 @@ async function reasons(db, ids) {
12625
12727
  return out;
12626
12728
  }
12627
12729
  async function recordLines(db, units) {
12628
- const why = await reasons(db, units.map((u) => u.id));
12629
- return units.map((u) => line(u, why.get(u.id)));
12730
+ const ids = units.map((u) => u.id);
12731
+ const [why, ai] = await Promise.all([reasons(db, ids), aiDecided(db, ids)]);
12732
+ return units.map((u) => line(u, why.get(u.id), ai.has(u.id)));
12733
+ }
12734
+ async function leadFor(db, ids, lead) {
12735
+ return (await aiDecided(db, ids)).size ? `${lead} ${AI_DECIDED}` : lead;
12630
12736
  }
12631
- function fit2(lines, chars, lead) {
12737
+ function fit2(lines, chars, lead, free = []) {
12632
12738
  const forms = lines.map((entry) => Array.isArray(entry) ? entry : [entry]);
12739
+ const cost = (i, l) => l.length - (free[i] ?? 0);
12633
12740
  const chosen = new Map;
12634
12741
  let used = lead.length;
12635
12742
  forms.forEach((f, i) => {
12636
12743
  const short = f[f.length - 1] ?? "";
12637
- if (used + short.length + 1 > chars)
12744
+ if (used + cost(i, short) + 1 > chars)
12638
12745
  return;
12639
12746
  chosen.set(i, short);
12640
- used += short.length + 1;
12747
+ used += cost(i, short) + 1;
12641
12748
  });
12642
12749
  for (const [i, short] of chosen) {
12643
12750
  const longer = forms[i]?.find((l) => used - short.length + l.length <= chars);
@@ -12660,11 +12767,11 @@ async function beforeEdit(db, projectId2, rels) {
12660
12767
  const rows = await anchoredTo(db, projectId2, rels).execute();
12661
12768
  const shown = rows.slice(0, LIMITS.pre_edit.units);
12662
12769
  const why = await reasons(db, shown.map((u) => u.id));
12663
- const lead = `Active decisions applying to ${named(rels)} (current code relevance unverified). ${CONFIRM} ${NOTE}:`;
12664
- const f = fit2(shown.map((u) => why.has(u.id) ? [line(u, why.get(u.id)), line(u)] : line(u)), LIMITS.pre_edit.chars, lead);
12770
+ const ai = await aiDecided(db, shown.map((u) => u.id));
12771
+ const f = fitMarked(shown.map((u) => why.has(u.id) ? [line(u, why.get(u.id), ai.has(u.id)), line(u, "", ai.has(u.id))] : line(u, "", ai.has(u.id))), LIMITS.pre_edit.chars, `Active decisions applying to ${named(rels)} (current code relevance unverified). ${CONFIRM} ${NOTE}:`, ai, shown.map((u) => u.id));
12665
12772
  const omitted = rows.length - shown.length + f.omitted;
12666
12773
  return {
12667
- ...noted(f.text, lead, [leftOut(omitted)]),
12774
+ ...noted(f.text, f.lead, [leftOut(omitted)]),
12668
12775
  units: f.kept.flatMap((i) => shown[i]?.id ?? []),
12669
12776
  eligible: rows.length,
12670
12777
  omitted,
@@ -12680,18 +12787,29 @@ async function beforeRead(db, projectId2, rels, session, agent, how) {
12680
12787
  const restart = await db.selectFrom("delivery as d").where("d.session_id", "=", session).where(sameAgent(agent)).where("d.event", "=", "session_start").where("d.reason", "in", RESTARTS).select((eb) => eb.fn.max("d.id").as("id")).executeTakeFirst();
12681
12788
  const since = restart?.id ?? 0;
12682
12789
  const sent = await db.selectFrom("delivery as d").innerJoin("delivery_unit as x", "x.delivery_id", "d.id").where("d.session_id", "=", session).where(sameAgent(agent)).where("d.id", ">", since).where("d.outcome", "=", "emitted").select(["x.unit_id", "d.event"]).execute();
12683
- const spent = await db.selectFrom("delivery as d").where("d.session_id", "=", session).where(sameAgent(agent)).where("d.id", ">", since).where("d.event", "=", "pre_read").where("d.outcome", "=", "emitted").where(({ exists, selectFrom }) => exists(selectFrom("delivery_unit as x").select("x.unit_id").whereRef("x.delivery_id", "=", "d.id"))).select("d.chars").execute();
12790
+ const spent = await db.selectFrom("delivery as d").where("d.session_id", "=", session).where(sameAgent(agent)).where("d.id", ">", since).where("d.event", "=", "pre_read").where("d.outcome", "=", "emitted").where(({ exists, selectFrom }) => exists(selectFrom("delivery_unit as x").select("x.unit_id").whereRef("x.delivery_id", "=", "d.id"))).select(["d.id", "d.at", "d.chars"]).execute();
12791
+ const shownBy = spent.length ? await db.selectFrom("delivery_unit").select(["delivery_id", "unit_id"]).where("delivery_id", "in", spent.map((r) => r.id)).execute() : [];
12792
+ const marked = new Map;
12793
+ for (const r of spent) {
12794
+ const ids = shownBy.filter((x) => x.delivery_id === r.id).map((x) => x.unit_id);
12795
+ const n = [...(await authorityOf(db, ids, r.at)).values()].filter((a) => a === "agent").length;
12796
+ if (n)
12797
+ marked.set(r.id, n);
12798
+ }
12684
12799
  const seen = new Set(sent.map((r) => r.unit_id));
12685
12800
  const readUnits = sent.filter((r) => r.event === "pre_read").length;
12686
12801
  const rows = (await anchoredTo(db, projectId2, rels).where("u.kind", "in", ["decision", "constraint"]).execute()).filter((u) => !seen.has(u.id));
12687
12802
  const room = Math.min(LIMITS.pre_read.units, READ_SESSION.units - readUnits);
12688
12803
  const shown = rows.slice(0, Math.max(room, 0));
12689
12804
  const why = await reasons(db, shown.map((u) => u.id));
12690
- const lead = `Active decisions applying to ${named(rels)}, which ${how === "reading" ? "you are reading" : "this command names"} (current code relevance unverified). ${CONFIRM} ${NOTE}:`;
12691
- const f = fit2(shown.map((u) => why.has(u.id) ? [line(u, why.get(u.id)), line(u)] : line(u)), Math.min(LIMITS.pre_read.chars, READ_SESSION.chars + ASK - spent.reduce((n, r) => n + Math.max(r.chars - ASK, 0), 0)), lead);
12805
+ const ai = await aiDecided(db, shown.map((u) => u.id));
12806
+ const f = fitMarked(shown.map((u) => why.has(u.id) ? [line(u, why.get(u.id), ai.has(u.id)), line(u, "", ai.has(u.id))] : line(u, "", ai.has(u.id))), Math.min(LIMITS.pre_read.chars, READ_SESSION.chars + ASK - spent.reduce((n, r) => {
12807
+ const ai2 = marked.get(r.id) ?? 0;
12808
+ return n + Math.max(r.chars - ASK - (ai2 ? AI_ROOM + ai2 * AI_MARK.length : 0), 0);
12809
+ }, 0)), `Active decisions applying to ${named(rels)}, which ${how === "reading" ? "you are reading" : "this command names"} (current code relevance unverified). ${CONFIRM} ${NOTE}:`, ai, shown.map((u) => u.id));
12692
12810
  const omitted = rows.length - shown.length + f.omitted;
12693
12811
  return {
12694
- ...noted(f.text, lead, [leftOut(omitted)]),
12812
+ ...noted(f.text, f.lead, [leftOut(omitted)]),
12695
12813
  units: f.kept.flatMap((i) => shown[i]?.id ?? []),
12696
12814
  eligible: rows.length,
12697
12815
  omitted,
@@ -12758,18 +12876,22 @@ async function onPrompt(db, projectId2, root, prompt) {
12758
12876
  hits.push({ u, why: ` [names the ${o.outcome} option ${inline(o.text)}]` });
12759
12877
  }
12760
12878
  const shown = hits.slice(0, LIMITS.prompt.units);
12761
- const lines = shown.map((h) => `${NOTE}: ${line(h.u, h.why).slice(2)}`);
12879
+ const ai = await aiDecided(db, shown.map((h) => h.u.id));
12880
+ const wide = withAi(CONFIRM, LIMITS.prompt.chars, ai);
12881
+ const lines = shown.map((h) => `${NOTE}: ${line(h.u, h.why, ai.has(h.u.id)).slice(2)}`);
12762
12882
  const kept = [];
12763
- let used = ASK;
12764
- for (const l of lines) {
12765
- if (used + l.length + 1 > LIMITS.prompt.chars)
12883
+ let used = wide.lead.length + 1;
12884
+ for (const [i, l] of lines.entries()) {
12885
+ const cost = l.length - (ai.has(shown[i]?.u.id ?? -1) ? AI_MARK.length : 0);
12886
+ if (used + cost + 1 > wide.chars)
12766
12887
  break;
12767
12888
  kept.push(l);
12768
- used += l.length + 1;
12889
+ used += cost + 1;
12769
12890
  }
12891
+ const ask = shown.slice(0, kept.length).some((h) => ai.has(h.u.id)) ? wide.lead : CONFIRM;
12770
12892
  return {
12771
- ...noted(kept.length ? [CONFIRM, ...kept].join(`
12772
- `) : "", CONFIRM, [leftOut(hits.length - kept.length)]),
12893
+ ...noted(kept.length ? [ask, ...kept].join(`
12894
+ `) : "", ask, [leftOut(hits.length - kept.length)]),
12773
12895
  units: shown.slice(0, kept.length).map((h) => h.u.id),
12774
12896
  eligible: hits.length,
12775
12897
  omitted: hits.length - kept.length,
@@ -12777,9 +12899,22 @@ async function onPrompt(db, projectId2, root, prompt) {
12777
12899
  reason: null
12778
12900
  };
12779
12901
  }
12902
+ var AUTO_TRACE_OFF = new Set(["off", "0", "false", "no"]);
12903
+ var AUTO_TRACE = (n) => `- ${n} earlier session${n === 1 ? "" : "s"} of this project ${n === 1 ? "waits" : "wait"} to be traced. After the user's request is done, run the trace Skill on your own for the oldest (at most 2), as its "On your own" section says, without asking the user.`;
12780
12904
  async function waiting(db, projectId2, place) {
12781
12905
  if (!place.owner)
12782
12906
  return "";
12907
+ if (place.auto !== null) {
12908
+ const { total } = await pendingSessions(db, projectId2, "recent", new Date, 1, {
12909
+ auto: true,
12910
+ skip: { host: place.host, session: place.auto }
12911
+ });
12912
+ if (!total)
12913
+ return "";
12914
+ if (!markOnce("auto-trace", `${path8.resolve(place.file)}\x00${place.key}\x00${place.host}\x00${place.auto}`))
12915
+ return "";
12916
+ return AUTO_TRACE(total);
12917
+ }
12783
12918
  const n = (await pendingCount(db, projectId2)).recent;
12784
12919
  if (!n)
12785
12920
  return "";
@@ -12798,20 +12933,20 @@ async function atStart(db, projectId2, branch, place) {
12798
12933
  current2.select((eb) => eb.fn.countAll().as("n")).executeTakeFirst(),
12799
12934
  standing.select((eb) => eb.fn.countAll().as("n")).executeTakeFirst()
12800
12935
  ]).then((r) => r.map((x) => Number(x?.n ?? 0)));
12936
+ const ai = await aiDecided(db, broad.map((u) => u.id));
12801
12937
  const lines = [
12802
12938
  ...work.map((w) => {
12803
12939
  const next = w.next[0];
12804
12940
  return `- Work: ${head(inline(w.title), 120)} (${w.status}${w.branch && w.branch === branch ? ", this branch" : ""}): ${head(inline(w.current), 200)}${next ? `; next: ${head(inline(next), 120)}` : ""}`;
12805
12941
  }),
12806
- ...broad.map((u) => line(u))
12942
+ ...broad.map((u) => line(u, "", ai.has(u.id)))
12807
12943
  ];
12808
- const lead = `Sphica: this project's current work and standing constraints. ${CONFIRM} ${NOTE}:`;
12809
- const f = fit2(lines, LIMITS.session_start.chars, lead);
12944
+ const f = fitMarked(lines, LIMITS.session_start.chars, `Sphica: this project's current work and standing constraints. ${CONFIRM} ${NOTE}:`, ai, [...work.map(() => null), ...broad.map((u) => u.id)]);
12810
12945
  const shownUnits = f.kept.flatMap((i) => i >= work.length ? broad[i - work.length]?.id ?? [] : []);
12811
12946
  const workLeft = Math.max((workTotal ?? 0) - f.kept.filter((i) => i < work.length).length, 0);
12812
12947
  const broadLeft = Math.max((broadTotal ?? 0) - shownUnits.length, 0);
12813
12948
  return {
12814
- ...noted(f.text, lead, [
12949
+ ...noted(f.text, f.lead, [
12815
12950
  leftOut(broadLeft),
12816
12951
  workLeftOut(workLeft),
12817
12952
  await waiting(db, projectId2, place),
@@ -12848,11 +12983,11 @@ async function beforeReview(db, projectId2, root, call) {
12848
12983
  if (!rows.length)
12849
12984
  return { ...said(`Sphica ${checked}: no active recorded decision applies.`, null), once };
12850
12985
  const shown = rows.slice(0, LIMITS.review.units);
12851
- const lead = `Sphica: past decisions that apply to this change (${checked}). ${NOTE}; compare the change against each:`;
12852
- const f = fit2(shown.map((u) => line(u, ` [${inline(u.because)}]`)), LIMITS.review.chars, lead);
12986
+ const ai = await aiDecided(db, shown.map((u) => u.id));
12987
+ const f = fitMarked(shown.map((u) => line(u, ` [${inline(u.because)}]`, ai.has(u.id))), LIMITS.review.chars, `Sphica: past decisions that apply to this change (${checked}). ${NOTE}; compare the change against each:`, ai, shown.map((u) => u.id));
12853
12988
  const omitted = rows.length - shown.length + f.omitted;
12854
12989
  return {
12855
- ...noted(f.text, lead, [leftOut(omitted)]),
12990
+ ...noted(f.text, f.lead, [leftOut(omitted)]),
12856
12991
  units: f.kept.flatMap((i) => shown[i]?.id ?? []),
12857
12992
  eligible: rows.length,
12858
12993
  omitted,
@@ -12987,6 +13122,7 @@ async function deliver(input, host = "claude-code", file = dbFile()) {
12987
13122
  key: place.key,
12988
13123
  host,
12989
13124
  subagent: name === "SubagentStart",
13125
+ auto: host === "claude-code" && name === "SessionStart" && input.source === "startup" && process.env.CLAUDE_CODE_ENTRYPOINT === "cli" && !AUTO_TRACE_OFF.has((process.env.SPHICA_AUTO_TRACE ?? "").trim().toLowerCase()) ? session : null,
12990
13126
  owner: name !== "SubagentStart" && isOwnerTurn(input, undefined, undefined, host === "codex" ? process.env.CODEX_THREAD_ID : undefined)
12991
13127
  }),
12992
13128
  reason: name === "SubagentStart" ? "subagent" : input.source && START_SOURCES.has(input.source) ? input.source : null
@@ -13031,8 +13167,10 @@ if (process.argv[1] && /deliver\.(ts|js)$/.test(process.argv[1])) {
13031
13167
  main2().catch(() => {});
13032
13168
  }
13033
13169
  export {
13170
+ AUTO_TRACE,
13034
13171
  CONFIRM,
13035
13172
  CONFIRM_GOLD,
13036
13173
  deliver,
13174
+ leadFor,
13037
13175
  recordLines
13038
13176
  };