sphica 0.6.4 → 0.6.5

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "sphica",
4
- "version": "0.6.4",
4
+ "version": "0.6.5",
5
5
  "description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
6
6
  "author": {
7
7
  "name": "iroha924",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sphica",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
5
5
  "author": {
6
6
  "name": "iroha924",
@@ -1,4 +1,4 @@
1
- -- Revision 1 → 2 of generation 2: the owner can forget chosen sources (issue #187).
1
+ -- Revision 1 → 2 of generation 2: the owner can forget chosen sources.
2
2
  -- `sphica init` runs this in one transaction with foreign keys off (set outside the transaction), then checks foreign_key_check before
3
3
  -- committing. Every statement matches db/schema.sql at revision 2; server/test/migrate.test.ts compares a migrated database with a fresh one.
4
4
 
@@ -1,4 +1,4 @@
1
- -- Revision 2 → 3 of generation 2: a rejected option can carry a reconsider condition the owner stated, quoted as `reconsiders` evidence (issue #193).
1
+ -- Revision 2 → 3 of generation 2: a rejected option can carry a reconsider condition the owner stated, quoted as `reconsiders` evidence.
2
2
  -- `sphica init` runs this in one transaction with foreign keys off (set outside the transaction), then checks foreign_key_check before
3
3
  -- committing. Every statement matches db/schema.sql at revision 3; server/test/migrate.test.ts compares a migrated database with a fresh one.
4
4
 
package/db/schema.sql CHANGED
@@ -1,5 +1,5 @@
1
1
  -- The source of truth for Sphica's database (SQLite, `node:sqlite`): the memory of past implementation and decisions for one owner on one machine.
2
- -- Generation 2 (the 0.5.0 rebuild). `sphica_generation` holds the generation; `pragma user_version` is the revision within it.
2
+ -- Generation 2. `sphica_generation` holds the generation; `pragma user_version` is the revision within it.
3
3
  -- A database of another generation is refused without being changed.
4
4
  --
5
5
  -- Four boundaries:
package/dist/deliver.js CHANGED
@@ -31689,10 +31689,28 @@ function localChange(root, args) {
31689
31689
  return { base, files, digest: sha256(read.join("\x00")).toString("hex") };
31690
31690
  }
31691
31691
 
31692
+ // server/src/trace.ts
31693
+ var PENDING_DAYS = 30;
31694
+ var pendingCutoff = (now) => new Date(now.getTime() - PENDING_DAYS * 86400000).toISOString();
31695
+ 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")))));
31696
+ 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) => [
31697
+ "s.id",
31698
+ "s.host",
31699
+ "s.started_at",
31700
+ "s.branch",
31701
+ 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"),
31702
+ untracedOwner(eb).select((m) => m.fn.countAll().as("waiting")).as("waiting"),
31703
+ untracedOwner(eb).select((m) => m.fn.min("m.id").as("first")).as("first")
31704
+ ]);
31705
+
31692
31706
  // server/src/status.ts
31693
- async function pendingCount(db, projectId2) {
31694
- const r = await db.selectFrom("session as s").where("s.project_id", "=", projectId2).where((eb) => eb.exists(eb.selectFrom("source as m").whereRef("m.session_id", "=", "s.id").where("m.author_kind", "=", "owner").where((eb2) => eb2.not(eb2.exists(eb2.selectFrom("source_processing as p").whereRef("p.source_id", "=", "m.id").select(sql`1`.as("x"))))).select(sql`1`.as("x")))).select((eb) => eb.fn.countAll().as("n")).executeTakeFirst();
31695
- return Number(r?.n ?? 0);
31707
+ async function pendingCount(db, projectId2, now = new Date) {
31708
+ const cutoff = pendingCutoff(now);
31709
+ const r = await db.selectFrom(untracedSessions(db, projectId2).as("w")).select([
31710
+ sql`count(case when w.last >= ${cutoff} then 1 end)`.as("recent"),
31711
+ sql`count(case when w.last < ${cutoff} then 1 end)`.as("older")
31712
+ ]).executeTakeFirst();
31713
+ return { recent: Number(r?.recent ?? 0), older: Number(r?.older ?? 0) };
31696
31714
  }
31697
31715
 
31698
31716
  // server/src/deliver.ts
@@ -31887,7 +31905,7 @@ async function onPrompt(db, projectId2, prompt) {
31887
31905
  async function waiting(db, projectId2, place) {
31888
31906
  if (!place.owner)
31889
31907
  return "";
31890
- const n = await pendingCount(db, projectId2);
31908
+ const n = (await pendingCount(db, projectId2)).recent;
31891
31909
  if (!n)
31892
31910
  return "";
31893
31911
  const day = new Date().toLocaleDateString("sv-SE");
@@ -48172,15 +48172,26 @@ ${c.errors.map((e) => ` ${e}`).join(`
48172
48172
  }
48173
48173
 
48174
48174
  // server/src/trace.ts
48175
- async function pendingSessions(db, projectId2, limit = 20) {
48176
- return db.selectFrom("session as s").innerJoin("source as m", "m.session_id", "s.id").where("s.project_id", "=", projectId2).where("m.author_kind", "=", "owner").where((eb) => eb.not(eb.exists(eb.selectFrom("source_processing as p").whereRef("p.source_id", "=", "m.id").select(sql`1`.as("x"))))).groupBy("s.id").select((eb) => [
48177
- "s.id",
48178
- "s.host",
48179
- "s.started_at",
48180
- "s.branch",
48181
- eb.fn.countAll().as("waiting"),
48182
- eb.fn.min("m.id").as("first")
48183
- ]).orderBy("s.started_at", "desc").limit(limit).execute();
48175
+ var PENDING_DAYS = 30;
48176
+ var pendingCutoff = (now) => new Date(now.getTime() - PENDING_DAYS * 86400000).toISOString();
48177
+ 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")))));
48178
+ 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) => [
48179
+ "s.id",
48180
+ "s.host",
48181
+ "s.started_at",
48182
+ "s.branch",
48183
+ 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"),
48184
+ untracedOwner(eb).select((m) => m.fn.countAll().as("waiting")).as("waiting"),
48185
+ untracedOwner(eb).select((m) => m.fn.min("m.id").as("first")).as("first")
48186
+ ]);
48187
+ async function pendingSessions(db, projectId2, group, now = new Date, limit = 20) {
48188
+ const cutoff = pendingCutoff(now);
48189
+ const base = db.selectFrom(untracedSessions(db, projectId2).as("w")).where("w.last", group === "recent" ? ">=" : "<", cutoff);
48190
+ const [rows, total] = await Promise.all([
48191
+ base.selectAll("w").orderBy("w.last", "desc").limit(limit).execute(),
48192
+ base.select((eb) => eb.fn.countAll().as("n")).executeTakeFirst()
48193
+ ]);
48194
+ return { rows, total: Number(total?.n ?? 0) };
48184
48195
  }
48185
48196
  async function openRun(db, v) {
48186
48197
  const r = await db.insertInto("extraction_run").values({
@@ -48219,15 +48230,30 @@ async function liveUnits(db, projectId2, limit = 40) {
48219
48230
 
48220
48231
  // server/src/extract.ts
48221
48232
  var newRunId = () => crypto2.randomBytes(9).toString("base64url");
48222
- async function pendingText(db, projectId2) {
48233
+ async function pendingText(db, projectId2, now = new Date) {
48223
48234
  await flush(undefined, TOOL_FLUSH_BUDGET_MS).catch(() => {});
48224
- const rows = await pendingSessions(db, projectId2);
48225
- if (!rows.length)
48235
+ const [recent, older] = await Promise.all([
48236
+ pendingSessions(db, projectId2, "recent", now),
48237
+ pendingSessions(db, projectId2, "older", now)
48238
+ ]);
48239
+ if (!recent.total && !older.total)
48226
48240
  return "Every captured session has been traced.";
48227
- const firsts = new Map((await db.selectFrom("source").select(["id", "text"]).where("id", "in", rows.map((r) => Number(r.first))).execute()).map((m) => [m.id, m.text]));
48241
+ const ids = [...recent.rows, ...older.rows].map((r) => Number(r.first));
48242
+ const firsts = new Map(ids.length ? (await db.selectFrom("source").select(["id", "text"]).where("id", "in", ids).execute()).map((m) => [
48243
+ m.id,
48244
+ m.text
48245
+ ]) : []);
48246
+ const group = (g) => [
48247
+ ...g.rows.map((r) => `- ${r.id} ${r.host} ${r.started_at}: ${plural2(Number(r.waiting), "message")} waiting, starting "${inline(firsts.get(Number(r.first)) ?? "").slice(0, 100)}"`),
48248
+ ...g.total > g.rows.length ? [`- and ${g.total - g.rows.length} more`] : []
48249
+ ];
48228
48250
  return [
48229
- `${plural2(rows.length, "session")} to trace (pass the id to trace_begin):`,
48230
- ...rows.map((r) => `- ${r.id} ${r.host} ${r.started_at}: ${plural2(Number(r.waiting), "message")} waiting, starting "${inline(firsts.get(Number(r.first)) ?? "").slice(0, 100)}"`)
48251
+ recent.total ? `${plural2(recent.total, "session")} to trace (pass the id to trace_begin):` : "No recent session waits to be traced.",
48252
+ ...group(recent),
48253
+ ...older.total ? [
48254
+ `Older than ${PENDING_DAYS} days (not counted at session start), ${plural2(older.total, "session")}; trace_begin takes these ids too:`,
48255
+ ...group(older)
48256
+ ] : []
48231
48257
  ].join(`
48232
48258
  `);
48233
48259
  }
@@ -48637,7 +48663,7 @@ var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, o
48637
48663
  var RECORD = exports_external.record(exports_external.string(), exports_external.unknown()).describe("The record, as the Skill describes");
48638
48664
  server.registerTool("trace_pending", {
48639
48665
  title: "Sessions not traced yet",
48640
- description: "Lists this project's captured sessions with owner messages no trace has looked at.",
48666
+ description: "Lists this project's captured sessions with owner messages no trace has looked at, then apart those whose last owner message is over 30 days old.",
48641
48667
  inputSchema: { cwd: CWD },
48642
48668
  annotations: READ
48643
48669
  }, async (a, extra) => tool(async () => pendingText(conn(), (await projectOf(a.cwd, extra._meta)).projectId)));
package/dist/mcp.js CHANGED
@@ -46932,19 +46932,37 @@ async function checkFindings(db, projectId2, files, raw) {
46932
46932
  return problems;
46933
46933
  }
46934
46934
 
46935
+ // server/src/trace.ts
46936
+ var PENDING_DAYS = 30;
46937
+ var pendingCutoff = (now) => new Date(now.getTime() - PENDING_DAYS * 86400000).toISOString();
46938
+ 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")))));
46939
+ 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) => [
46940
+ "s.id",
46941
+ "s.host",
46942
+ "s.started_at",
46943
+ "s.branch",
46944
+ 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"),
46945
+ untracedOwner(eb).select((m) => m.fn.countAll().as("waiting")).as("waiting"),
46946
+ untracedOwner(eb).select((m) => m.fn.min("m.id").as("first")).as("first")
46947
+ ]);
46948
+
46935
46949
  // server/src/status.ts
46936
- async function pendingCount(db, projectId2) {
46937
- const r = await db.selectFrom("session as s").where("s.project_id", "=", projectId2).where((eb) => eb.exists(eb.selectFrom("source as m").whereRef("m.session_id", "=", "s.id").where("m.author_kind", "=", "owner").where((eb2) => eb2.not(eb2.exists(eb2.selectFrom("source_processing as p").whereRef("p.source_id", "=", "m.id").select(sql`1`.as("x"))))).select(sql`1`.as("x")))).select((eb) => eb.fn.countAll().as("n")).executeTakeFirst();
46938
- return Number(r?.n ?? 0);
46939
- }
46940
- async function coverage(db, projectId2) {
46950
+ async function pendingCount(db, projectId2, now = new Date) {
46951
+ const cutoff = pendingCutoff(now);
46952
+ const r = await db.selectFrom(untracedSessions(db, projectId2).as("w")).select([
46953
+ sql`count(case when w.last >= ${cutoff} then 1 end)`.as("recent"),
46954
+ sql`count(case when w.last < ${cutoff} then 1 end)`.as("older")
46955
+ ]).executeTakeFirst();
46956
+ return { recent: Number(r?.recent ?? 0), older: Number(r?.older ?? 0) };
46957
+ }
46958
+ async function coverage(db, projectId2, now) {
46941
46959
  const count = async (q) => Number((await q)?.n ?? 0);
46942
46960
  const units = (where) => count(where(unitBase()).select((eb) => eb.fn.countAll().as("n")).executeTakeFirst());
46943
46961
  const unitBase = () => db.selectFrom("unit").where("project_id", "=", projectId2);
46944
46962
  const processed = (outcome) => db.selectFrom("source_processing as p").innerJoin("source as m", "m.id", "p.source_id").where("m.project_id", "=", projectId2).where("p.outcome", "=", outcome).where("m.session_id", "is not", null).select("m.session_id");
46945
- const [sessions, pendingSessions, emptySessions, sources, active, candidates, quarantined, work] = await Promise.all([
46963
+ const [sessions, pending, emptySessions, sources, active, candidates, quarantined, work] = await Promise.all([
46946
46964
  count(db.selectFrom("session").where("project_id", "=", projectId2).select((eb) => eb.fn.countAll().as("n")).executeTakeFirst()),
46947
- pendingCount(db, projectId2),
46965
+ pendingCount(db, projectId2, now),
46948
46966
  count(db.selectFrom("session").where("id", "in", processed("no_unit")).where("id", "not in", processed("units")).select((eb) => eb.fn.countAll().as("n")).executeTakeFirst()),
46949
46967
  count(db.selectFrom("source").where("project_id", "=", projectId2).select((eb) => eb.fn.countAll().as("n")).executeTakeFirst()),
46950
46968
  units((q) => q.where("lifecycle", "=", "active")),
@@ -46952,15 +46970,21 @@ async function coverage(db, projectId2) {
46952
46970
  units((q) => q.where("extraction", "=", "quarantined")),
46953
46971
  db.selectFrom("work").where("project_id", "=", projectId2).where("status", "in", ["active", "blocked", "paused"]).orderBy("updated_at", "desc").select(["title", "current", "status"]).limit(5).execute()
46954
46972
  ]);
46955
- return { sessions, pendingSessions, emptySessions, sources, active, candidates, quarantined, work };
46973
+ return { sessions, pending, emptySessions, sources, active, candidates, quarantined, work };
46956
46974
  }
46957
- async function status(db, projectId2, name) {
46958
- const c = await coverage(db, projectId2);
46975
+ async function status(db, projectId2, name, now = new Date) {
46976
+ const c = await coverage(db, projectId2, now);
46959
46977
  const lines = [
46960
46978
  `${name}`,
46961
46979
  `Captured: ${plural2(c.sessions, "session")}, ${plural2(c.sources, "source")}.`,
46962
46980
  `Extracted: ${plural2(c.active, "active record")}, ${plural2(c.candidates, "candidate")} not active yet (waiting for adoption or evidence), ${plural2(c.quarantined, "quarantined record")}.`,
46963
- c.pendingSessions ? `${plural2(c.pendingSessions, "session")} not traced yet: their decisions exist only as captured text (run /sphica:trace pending).` : "Every captured session has been traced.",
46981
+ ...c.pending.recent ? [
46982
+ `${plural2(c.pending.recent, "session")} not traced yet: their decisions exist only as captured text (run /sphica:trace pending).`
46983
+ ] : [],
46984
+ ...c.pending.older ? [
46985
+ `${c.pending.older} older ${c.pending.older === 1 ? "session" : "sessions"} (last owner message over ${PENDING_DAYS} days ago) not traced; /sphica:trace pending lists them.`
46986
+ ] : [],
46987
+ ...c.pending.recent || c.pending.older ? [] : ["Every captured session has been traced."],
46964
46988
  ...c.emptySessions ? [`${plural2(c.emptySessions, "session")} traced with nothing to record.`] : [],
46965
46989
  c.work.length ? `Work in progress:
46966
46990
  ${framed(c.work.map((w) => `- ${head(inline(w.title), 200)} (${w.status}): ${head(inline(w.current), 500)}`).join(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sphica",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -3,7 +3,7 @@ name: forget
3
3
  description: Removes sources the owner chooses from Sphica (a message, a pull request item, a file excerpt), with their search index entries and the bytes left in the database file, and judges the records that cited them again. The owner confirms in a dialog before anything is removed. Use only when the user explicitly asks to forget or delete something Sphica captured.
4
4
  argument-hint: "<what to forget>"
5
5
  disable-model-invocation: true
6
- allowed-tools: AskUserQuestion, mcp__plugin_sphica_sphica__search, mcp__plugin_sphica_sphica__read, mcp__plugin_sphica_record__forget_preview, mcp__plugin_sphica_record__forget_apply
6
+ allowed-tools: AskUserQuestion, mcp__plugin_sphica_sphica__search, mcp__plugin_sphica_sphica__read, mcp__plugin_sphica_record__forget_preview, mcp__plugin_sphica_record__forget_apply, mcp__plugin_sphica_sphica__status
7
7
  ---
8
8
 
9
9
  # forget — remove what should not have been kept
@@ -3,7 +3,7 @@ name: glean
3
3
  description: Adds evidence and corrections to existing Sphica records, or keeps something the owner remembers, only from sources the owner points to (an issue or pull request, a file in the repository, the owner's own words now). Asks the owner for the source before saving anything. Use only when the user explicitly asks.
4
4
  argument-hint: "<what to add or correct>"
5
5
  disable-model-invocation: true
6
- allowed-tools: AskUserQuestion, mcp__plugin_sphica_record__glean_begin, mcp__plugin_sphica_record__glean_fetch, mcp__plugin_sphica_record__record_context, mcp__plugin_sphica_record__record_check, mcp__plugin_sphica_record__record_save, mcp__plugin_sphica_sphica__search, mcp__plugin_sphica_sphica__read
6
+ allowed-tools: AskUserQuestion, mcp__plugin_sphica_record__glean_begin, mcp__plugin_sphica_record__glean_fetch, mcp__plugin_sphica_record__record_context, mcp__plugin_sphica_record__record_check, mcp__plugin_sphica_record__record_save, mcp__plugin_sphica_sphica__search, mcp__plugin_sphica_sphica__read, mcp__plugin_sphica_sphica__status
7
7
  ---
8
8
 
9
9
  # glean — add what was found later, with its source
@@ -3,7 +3,7 @@ name: harvest
3
3
  description: Reads one GitHub pull request of the current repository (its body, comments, reviews, review comments, commits, the merge, and the issues it closes), keeps them as sources, and extracts what it decided and implemented into records that quote them, in the same form as trace. Pass the PR number. Use only when the user explicitly asks.
4
4
  argument-hint: "<PR number>"
5
5
  disable-model-invocation: true
6
- allowed-tools: mcp__plugin_sphica_record__harvest_begin, mcp__plugin_sphica_record__record_context, mcp__plugin_sphica_record__record_check, mcp__plugin_sphica_record__record_save, mcp__plugin_sphica_sphica__search, mcp__plugin_sphica_sphica__read
6
+ allowed-tools: mcp__plugin_sphica_record__harvest_begin, mcp__plugin_sphica_record__record_context, mcp__plugin_sphica_record__record_check, mcp__plugin_sphica_record__record_save, mcp__plugin_sphica_sphica__search, mcp__plugin_sphica_sphica__read, mcp__plugin_sphica_sphica__status
7
7
  ---
8
8
 
9
9
  # harvest — keep what a pull request decided and implemented
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: review
3
3
  description: Reviews changes. Use it to review your own committed and uncommitted diff, to review someone else's PR, and to sweep for misses before a merge or release. It starts independent reviewers per aspect, and when Codex is available it runs the same aspects on the other model too, to catch defects only one model can see. Findings are ruled on by reproduction before they are returned. It does not handle formatting or naming inconsistencies, design preferences, or future extensibility.
4
+ allowed-tools: mcp__plugin_sphica_sphica__status, mcp__plugin_sphica_sphica__search, mcp__plugin_sphica_sphica__read, mcp__plugin_sphica_sphica__review_select, mcp__plugin_sphica_sphica__review_check
4
5
  ---
5
6
 
6
7
  # review — sweep changes with independent reviewers
@@ -3,7 +3,7 @@ name: rules
3
3
  description: Drafts lines for CLAUDE.md, AGENTS.md, or .claude/rules from recorded constraints and decisions the owner picks, each line ending with a marker holding its record key, so Sphica's overview (view look) can flag the line once the record is replaced or withdrawn. It prints the draft and never edits a file. Use only when the user explicitly asks for rule text from Sphica's records.
4
4
  argument-hint: "<which constraints, or empty to choose from the list>"
5
5
  disable-model-invocation: true
6
- allowed-tools: mcp__plugin_sphica_sphica__overview, mcp__plugin_sphica_sphica__search, mcp__plugin_sphica_sphica__read
6
+ allowed-tools: mcp__plugin_sphica_sphica__overview, mcp__plugin_sphica_sphica__search, mcp__plugin_sphica_sphica__read, mcp__plugin_sphica_sphica__status
7
7
  ---
8
8
 
9
9
  # rules — draft instruction lines from recorded constraints
@@ -3,7 +3,7 @@ name: trace
3
3
  description: Extracts what a coding session decided and implemented (decisions and rejected options, constraints, implementations, findings, dead ends, open questions) into records whose every claim quotes the captured conversation, so a later session can find them. With "pending", lists this project's sessions not traced yet. Use only when the user explicitly asks.
4
4
  argument-hint: "[pending]"
5
5
  disable-model-invocation: true
6
- allowed-tools: mcp__plugin_sphica_record__trace_pending, mcp__plugin_sphica_record__trace_begin, mcp__plugin_sphica_record__record_context, mcp__plugin_sphica_record__record_check, mcp__plugin_sphica_record__record_save, mcp__plugin_sphica_sphica__search, mcp__plugin_sphica_sphica__read
6
+ allowed-tools: AskUserQuestion, mcp__plugin_sphica_record__trace_pending, mcp__plugin_sphica_record__trace_begin, mcp__plugin_sphica_record__record_context, mcp__plugin_sphica_record__record_check, mcp__plugin_sphica_record__record_save, mcp__plugin_sphica_sphica__search, mcp__plugin_sphica_sphica__read, mcp__plugin_sphica_sphica__status
7
7
  ---
8
8
 
9
9
  # trace — keep what a session decided and implemented
@@ -30,7 +30,8 @@ Everything goes through Sphica's `record` MCP server (its tools are `trace_pendi
30
30
 
31
31
  1. **Pick the session.** Without a target, it is this session: its id is `${CLAUDE_SESSION_ID}` in Claude Code; in Codex, read `CODEX_THREAD_ID`
32
32
  from your shell environment. With `pending`, call `trace_pending`, show the owner the list, and ask which to trace (AskUserQuestion in
33
- Claude Code). Trace one session at a time
33
+ Claude Code). Sessions whose last owner message is over 30 days old come last under their own heading: session start does not count
34
+ them, but they can still be traced. Trace one session at a time
34
35
  2. **Begin**: `trace_begin` with that `session`. It returns a `run` id bound to that session and this project; the record never names them
35
36
  3. **Read**: `record_context` with the run. It prints each captured message as `## s<N> owner|assistant <turn> <time>` followed by its text,
36
37
  the edits observed, and the project's live records. `(traced before)` marks messages an earlier trace already looked at.