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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +11 -5
- package/db/migrations/0010.check.sql +11 -0
- package/db/migrations/0010.sql +931 -0
- package/db/schema.sql +195 -54
- package/dist/capture.js +47 -4
- package/dist/cli.js +277 -4
- package/dist/deliver.js +187 -49
- package/dist/mcp-record.js +859 -308
- package/dist/mcp.js +277 -183
- package/hooks/hooks.json +13 -0
- package/package.json +1 -1
- package/skills/export/SKILL.md +3 -2
- package/skills/glean/SKILL.md +1 -1
- package/skills/review/reviewers/precedent.md +6 -1
- package/skills/rules/SKILL.md +5 -2
- package/skills/trace/SKILL.md +43 -7
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 =
|
|
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([
|
|
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
|
|
12557
|
-
|
|
12558
|
-
"
|
|
12559
|
-
|
|
12560
|
-
|
|
12561
|
-
|
|
12562
|
-
|
|
12563
|
-
|
|
12564
|
-
|
|
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([
|
|
12602
|
-
|
|
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
|
|
12629
|
-
|
|
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
|
|
12744
|
+
if (used + cost(i, short) + 1 > chars)
|
|
12638
12745
|
return;
|
|
12639
12746
|
chosen.set(i, short);
|
|
12640
|
-
used += short
|
|
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
|
|
12664
|
-
const f =
|
|
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
|
|
12691
|
-
const f =
|
|
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
|
|
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 =
|
|
12764
|
-
for (const l of lines) {
|
|
12765
|
-
|
|
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 +=
|
|
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 ? [
|
|
12772
|
-
`) : "",
|
|
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
|
|
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
|
|
12852
|
-
const f =
|
|
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
|
};
|