@mrciphersmith/keryx 0.2.55 → 0.2.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +441 -76
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -26111,6 +26111,10 @@ ${markdown.slice(4)}`;
26111
26111
  }
26112
26112
  return markdown;
26113
26113
  }
26114
+ function extractFrontmatterStatus(markdown) {
26115
+ const bodyMatch = /\nStatus:\s*(\S+)/i.exec(markdown) ?? /^Status:\s*(\S+)/im.exec(markdown);
26116
+ return bodyMatch?.[1] ?? "draft";
26117
+ }
26114
26118
  async function mapPool(items, concurrency, worker) {
26115
26119
  const results = new Array(items.length);
26116
26120
  let next = 0;
@@ -26137,7 +26141,6 @@ async function wikiEnrich(input2) {
26137
26141
  const concurrency = Math.max(1, Math.min(MAX_CONCURRENCY, input2.concurrency ?? DEFAULT_CONCURRENCY));
26138
26142
  const maxOutputTokens = input2.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
26139
26143
  const validate = input2.validate !== false;
26140
- const markAccepted = input2.keepStatus === true ? false : input2.markAccepted !== false;
26141
26144
  const wikiConfig = await loadWikiConfig(input2.cwd);
26142
26145
  const result = {
26143
26146
  provider,
@@ -26231,9 +26234,7 @@ async function wikiEnrich(input2) {
26231
26234
  return { path: page.relativePath, action: "failed", reason: `validation: ${structural}` };
26232
26235
  }
26233
26236
  }
26234
- if (markAccepted) {
26235
- enriched = setFrontmatterStatus(enriched, "accepted");
26236
- }
26237
+ enriched = setFrontmatterStatus(enriched, extractFrontmatterStatus(original));
26237
26238
  if (input2.dryRun) {
26238
26239
  onPage({ index, total, path: page.relativePath, status, phase: "done" });
26239
26240
  return {
@@ -26247,7 +26248,7 @@ async function wikiEnrich(input2) {
26247
26248
  await writeFile36(page.absolutePath, `${enriched.endsWith(`
26248
26249
  `) ? enriched : `${enriched}
26249
26250
  `}`, "utf8");
26250
- onPage({ index, total, path: page.relativePath, status: markAccepted ? "accepted" : status, phase: "done" });
26251
+ onPage({ index, total, path: page.relativePath, status, phase: "done" });
26251
26252
  return {
26252
26253
  path: page.relativePath,
26253
26254
  action: "enriched",
@@ -26273,7 +26274,6 @@ async function wikiEnrich(input2) {
26273
26274
  model,
26274
26275
  maxOutputTokens,
26275
26276
  validate,
26276
- markAccepted,
26277
26277
  concurrency,
26278
26278
  total,
26279
26279
  onPage,
@@ -26356,8 +26356,8 @@ function finalizeEnrichedText(original, rawText, options) {
26356
26356
  if (options.validate) {
26357
26357
  structuralError = validateEnrichedMarkdown(original, content);
26358
26358
  }
26359
- if (structuralError === null && options.markAccepted) {
26360
- content = setFrontmatterStatus(content, "accepted");
26359
+ if (structuralError === null) {
26360
+ content = setFrontmatterStatus(content, extractFrontmatterStatus(original));
26361
26361
  }
26362
26362
  return { content, structuralError };
26363
26363
  }
@@ -26384,7 +26384,7 @@ async function finishSuccess(ctx, page, originalRaw, content, keyFiles, extra) {
26384
26384
  index,
26385
26385
  total: ctx.total,
26386
26386
  path: page.relativePath,
26387
- status: ctx.markAccepted ? "accepted" : status,
26387
+ status,
26388
26388
  phase: "done"
26389
26389
  });
26390
26390
  return {
@@ -26428,8 +26428,7 @@ async function runDeepSingle(ctx, item) {
26428
26428
  let content = null;
26429
26429
  if (rawText !== null) {
26430
26430
  const finalized = finalizeEnrichedText(original, rawText, {
26431
- validate: ctx.validate,
26432
- markAccepted: ctx.markAccepted
26431
+ validate: ctx.validate
26433
26432
  });
26434
26433
  if (finalized.structuralError === null) {
26435
26434
  content = finalized.content;
@@ -26614,8 +26613,7 @@ async function runLightBatch(ctx, items) {
26614
26613
  continue;
26615
26614
  }
26616
26615
  const finalized = finalizeEnrichedText(item.original, rawText, {
26617
- validate: ctx.validate,
26618
- markAccepted: ctx.markAccepted
26616
+ validate: ctx.validate
26619
26617
  });
26620
26618
  if (finalized.structuralError !== null) {
26621
26619
  ctx.onPage({ index, total: ctx.total, path: item.page.relativePath, status, phase: "failed" });
@@ -26666,7 +26664,6 @@ async function runRlmPipeline(ctxInput) {
26666
26664
  model: ctxInput.model,
26667
26665
  maxOutputTokens: ctxInput.maxOutputTokens,
26668
26666
  validate: ctxInput.validate,
26669
- markAccepted: ctxInput.markAccepted,
26670
26667
  total: ctxInput.total,
26671
26668
  onPage: ctxInput.onPage,
26672
26669
  input: input2,
@@ -36675,7 +36672,6 @@ async function runEnrich(args2) {
36675
36672
  const resume = args2.includes("--resume");
36676
36673
  const refreshGraph = args2.includes("--refresh-graph");
36677
36674
  const dryRun = args2.includes("--dry-run");
36678
- const keepStatus = args2.includes("--keep-status");
36679
36675
  const noValidate = args2.includes("--no-validate");
36680
36676
  const valueFlags = new Set([
36681
36677
  "--page",
@@ -36744,9 +36740,7 @@ async function runEnrich(args2) {
36744
36740
  resume,
36745
36741
  refreshGraph,
36746
36742
  dryRun,
36747
- keepStatus,
36748
36743
  validate: !noValidate,
36749
- markAccepted: !keepStatus,
36750
36744
  ...prompt ? { prompt } : {},
36751
36745
  ...provider ? { provider } : {},
36752
36746
  ...model ? { model } : {},
@@ -36823,10 +36817,12 @@ Usage:
36823
36817
  keryx wiki validate
36824
36818
  keryx wiki ask "<question>" [--k <n>] [--rerank]
36825
36819
  keryx wiki enrich [<page>|--all] [--force] [--list] [--resume] [--limit N] [--concurrency N]
36826
- [--refresh-graph] [--max-tokens N] [--keep-status] [--no-validate]
36820
+ [--refresh-graph] [--max-tokens N] [--no-validate]
36827
36821
  [--prompt "<i>"] [--provider <p>] [--model <m>] [--dry-run] [--json]
36828
36822
  # defaults: drafts only; provider/model from auth.json; validate on;
36829
- # mark Status: accepted; concurrency 1 (raise for parallel page swarm)
36823
+ # concurrency 1 (raise for parallel page swarm)
36824
+ # rewrites prose only \u2014 Status is always left exactly as it was
36825
+ # before the run; enrich can never itself accept a page (issue #391)
36830
36826
  keryx wiki context
36831
36827
  keryx wiki backlinks <wiki-page-or-code-file>
36832
36828
 
@@ -49941,7 +49937,8 @@ var PREFIX_BANNED = new Set([
49941
49937
  "osascript",
49942
49938
  "open",
49943
49939
  "tee",
49944
- "cd"
49940
+ "cd",
49941
+ "keryx"
49945
49942
  ]);
49946
49943
  var PREFIX_BANNED_READERS = new Set([
49947
49944
  "cat",
@@ -50012,6 +50009,17 @@ function validateShellPattern(pattern) {
50012
50009
  }
50013
50010
  return { ok: true };
50014
50011
  }
50012
+ function getRunningBinaryName() {
50013
+ try {
50014
+ const argv0 = process.argv0;
50015
+ if (!argv0)
50016
+ return "";
50017
+ const name = argv0.split(/[\\/]/).pop() ?? "";
50018
+ return name.toLowerCase();
50019
+ } catch {
50020
+ return "";
50021
+ }
50022
+ }
50015
50023
  function bannedPrefixGrant(pattern, firstToken) {
50016
50024
  const rest = pattern.slice(firstToken.length).trim();
50017
50025
  const wildcardOnly = /^\*+$/.test(rest) || rest.length === 0 && /\*+$/.test(firstToken);
@@ -50024,6 +50032,13 @@ function bannedPrefixGrant(pattern, firstToken) {
50024
50032
  reason: `\`${word} *\` grants arbitrary execution: ${word} is an interpreter or wrapper, so its first token does not constrain what runs`
50025
50033
  };
50026
50034
  }
50035
+ const runningBinary = getRunningBinaryName();
50036
+ if (runningBinary && word === runningBinary) {
50037
+ return {
50038
+ word,
50039
+ reason: `\`${word} *\` grants arbitrary execution: ${word} can run arbitrary subcommands, so remembering this grant would silently approve mutating/destructive operations forever`
50040
+ };
50041
+ }
50027
50042
  if (PREFIX_BANNED_READERS.has(word)) {
50028
50043
  return {
50029
50044
  word,
@@ -53108,8 +53123,10 @@ init_slate();
53108
53123
  init_workspace_service();
53109
53124
  init_workspace_resolve();
53110
53125
  init_service7();
53126
+ init_store2();
53111
53127
  init_fs();
53112
53128
  import path149 from "path";
53129
+ import { readFile as readFile79 } from "fs/promises";
53113
53130
  var POSITIVE_INTEGER = /^[1-9][0-9]*$/;
53114
53131
  var DEFAULT_AUTO_GOAL_ROUNDS = 8;
53115
53132
  function parseGoalArgs(rest) {
@@ -53211,9 +53228,20 @@ async function autoProvisionFlow(cwd, goalText) {
53211
53228
  await service5.start({ cwd, id: result.flow.id });
53212
53229
  return result.flow.id;
53213
53230
  }
53231
+ var ROUND_DONE_MARKER = "GOAL_ROUND_COMPLETE";
53232
+ function continuationRoundClaimsDone(history) {
53233
+ for (let i = history.length - 1;i >= 0; i--) {
53234
+ const message2 = history[i];
53235
+ if (message2?.role === "assistant") {
53236
+ return message2.content.includes(ROUND_DONE_MARKER);
53237
+ }
53238
+ }
53239
+ return false;
53240
+ }
53214
53241
  async function buildContinuationMessage(cwd, slateSession, round4, roundsCap) {
53215
53242
  const totalRounds = roundsCap + 1;
53216
- const generic = `Continue working toward the stated goal (round ${round4} of ${totalRounds}).`;
53243
+ const doneInstruction = `If \u2014 and only if \u2014 the stated goal is now FULLY achieved and there is nothing further to do this round, ` + `end your reply with the exact line ${ROUND_DONE_MARKER} on its own, and nothing else on that line. ` + `Otherwise do not include that line at all.`;
53244
+ const generic = `Continue working toward the stated goal (round ${round4} of ${totalRounds}). ${doneInstruction}`;
53217
53245
  const slate = await readSlate(slateSession.dir).catch(() => {
53218
53246
  return;
53219
53247
  });
@@ -53251,11 +53279,61 @@ function parseVerifierVerdict(output2) {
53251
53279
  return;
53252
53280
  }
53253
53281
  }
53254
- async function runGoalVerifier(deps, goalText) {
53282
+ var MAX_EVIDENCE_SEEDS = 10;
53283
+ function summarizeRecentSeeds(seeds) {
53284
+ return seeds.slice(-MAX_EVIDENCE_SEEDS).map((seed) => `- Seed${seed.kind !== undefined ? ` [${seed.kind}]` : ""}: ${seed.text}`);
53285
+ }
53286
+ function summarizeWorkspaceProposals(history) {
53287
+ const lines = [];
53288
+ for (const message2 of history) {
53289
+ if (message2.role !== "assistant" || message2.toolCalls === undefined) {
53290
+ continue;
53291
+ }
53292
+ for (const call of message2.toolCalls) {
53293
+ if (call.name !== "workspace_propose") {
53294
+ continue;
53295
+ }
53296
+ const resultMessage = history.find((candidate) => candidate.role === "tool" && candidate.toolCallId === call.id);
53297
+ let argSummary = call.arguments;
53298
+ try {
53299
+ const parsedArgs = JSON.parse(call.arguments);
53300
+ const kind2 = typeof parsedArgs.kind === "string" ? parsedArgs.kind : "?";
53301
+ const note2 = typeof parsedArgs.note === "string" ? parsedArgs.note : undefined;
53302
+ argSummary = `kind=${kind2}${note2 !== undefined ? `, note=${note2}` : ""}`;
53303
+ } catch {}
53304
+ const outcome = resultMessage !== undefined ? resultMessage.content : "(no result recorded this run)";
53305
+ lines.push(`- workspace_propose: ${argSummary} -> ${outcome}`);
53306
+ }
53307
+ }
53308
+ return lines;
53309
+ }
53310
+ async function flowDefersCompletionToVerifier(cwd, flowId) {
53311
+ try {
53312
+ const dir = await resolveFlowDir(cwd, flowId);
53313
+ const text = await readFile79(acPath(cwd, dir), "utf8");
53314
+ const normalized = text.replace(/\s+/g, " ");
53315
+ return normalized.includes("judged by the verifier subagent");
53316
+ } catch {
53317
+ return false;
53318
+ }
53319
+ }
53320
+ async function buildVerifierEvidence(cwd, slateSession, history) {
53321
+ const slate = await readSlate(slateSession.dir).catch(() => {
53322
+ return;
53323
+ });
53324
+ const lines = [...slate !== undefined ? summarizeRecentSeeds(slate.seeds) : [], ...summarizeWorkspaceProposals(history)];
53325
+ const evidenceText = lines.length > 0 ? lines.join(`
53326
+ `) : "(no Seeds or workspace_propose records were recorded this run)";
53327
+ const flowId = slate?.course.flowRef;
53328
+ const deferToVerifier = flowId !== undefined ? await flowDefersCompletionToVerifier(cwd, flowId) : false;
53329
+ return { evidenceText, deferToVerifier };
53330
+ }
53331
+ async function runGoalVerifier(deps, goalText, cwd, slateSession, history, io, mintCallId) {
53255
53332
  const tool = deps.tools.find((candidate) => candidate.definition.name === "spawn_subagent");
53256
53333
  if (tool === undefined) {
53257
53334
  return;
53258
53335
  }
53336
+ const { evidenceText, deferToVerifier } = await buildVerifierEvidence(cwd, slateSession, history);
53259
53337
  const task = [
53260
53338
  "Independently verify whether the following goal has ACTUALLY been achieved, based on the",
53261
53339
  "current, real state of the repository (read the real files/tests \u2014 never trust a prior",
@@ -53263,17 +53341,49 @@ async function runGoalVerifier(deps, goalText) {
53263
53341
  "",
53264
53342
  `Goal: "${goalText}"`,
53265
53343
  "",
53344
+ "Evidence this run already produced (recent Slate Seeds and workspace_propose records) \u2014",
53345
+ "weigh this as real evidence of what was actually done, not merely a claim:",
53346
+ evidenceText,
53347
+ "",
53348
+ ...deferToVerifier ? [
53349
+ "This run's Task Manager flow acceptance criteria explicitly defer the completion",
53350
+ "judgment to THIS verifier check, not to the flow's own task checkboxes \u2014 its tasks",
53351
+ "are expected to remain unchecked even when the goal is genuinely achieved. Do NOT",
53352
+ "treat an incomplete/unchecked flow task list, by itself, as evidence the goal is NOT",
53353
+ "achieved; judge achievement from the real repository state and the evidence above.",
53354
+ ""
53355
+ ] : [],
53266
53356
  "Reply with EXACTLY one JSON object and nothing else, no prose before or after it:",
53267
53357
  '{"achieved": true or false, "gaps": ["specific reason it is not fully achieved", ...]}',
53268
53358
  '"gaps" must be empty when "achieved" is true.'
53269
53359
  ].join(`
53270
53360
  `);
53361
+ const input2 = { task, mode: "read_only", label: "goal-verifier" };
53362
+ const callId = mintCallId();
53363
+ io.onToolCall?.("spawn_subagent", JSON.stringify(input2));
53364
+ history.push({
53365
+ role: "assistant",
53366
+ content: "",
53367
+ provenance: "model",
53368
+ toolCalls: [{ id: callId, name: "spawn_subagent", arguments: JSON.stringify(input2) }]
53369
+ });
53370
+ io.onHistoryChange?.("tool");
53271
53371
  let result;
53272
53372
  try {
53273
- result = await tool.invoke({ task, mode: "read_only", label: "goal-verifier" });
53274
- } catch {
53373
+ result = await tool.invoke(input2);
53374
+ } catch (err) {
53375
+ const errorResult = {
53376
+ output: `spawn_subagent dispatch failed: ${err instanceof Error ? err.message : String(err)}`,
53377
+ isError: true
53378
+ };
53379
+ io.onToolResult?.("spawn_subagent", errorResult);
53380
+ history.push({ role: "tool", content: errorResult.output, provenance: "tool", toolCallId: callId });
53381
+ io.onHistoryChange?.("tool");
53275
53382
  return;
53276
53383
  }
53384
+ io.onToolResult?.("spawn_subagent", result);
53385
+ history.push({ role: "tool", content: result.output, provenance: "tool", toolCallId: callId });
53386
+ io.onHistoryChange?.("tool");
53277
53387
  if (result.isError) {
53278
53388
  return;
53279
53389
  }
@@ -53363,10 +53473,21 @@ async function runGoalCommand(params) {
53363
53473
  systemLine(io, `/goal --auto: round ${round4}/${roundsCap + 1} \u2014 continuing toward the goal.
53364
53474
  `);
53365
53475
  await runAgentTurn(io, deps, history, continuationText, turnOptions);
53476
+ if (continuationRoundClaimsDone(history)) {
53477
+ systemLine(io, `/goal --auto: model signaled this round's work is complete (round ${round4}/${roundsCap + 1}) \u2014 ` + `ending the round budget early; the verifier will confirm.
53478
+ `);
53479
+ break;
53480
+ }
53366
53481
  }
53367
53482
  const wasOpenBeforeVerifier = slateSession.opened;
53368
- const verdict = await runGoalVerifier(deps, parsed.text);
53369
- if (verdict !== undefined && !verdict.achieved) {
53483
+ const verdict = await runGoalVerifier(deps, parsed.text, cwd, slateSession, history, io, mintAttemptId);
53484
+ if (verdict === undefined) {
53485
+ systemLine(io, `/goal --auto: verifier unavailable \u2014 outcome not independently checked.
53486
+ `);
53487
+ } else if (verdict.achieved) {
53488
+ systemLine(io, `/goal --auto: verifier confirmed the goal is achieved.
53489
+ `);
53490
+ } else {
53370
53491
  systemLine(io, `/goal --auto: verifier found the goal not fully achieved${verdict.gaps.length > 0 ? ` \u2014 ${verdict.gaps.join("; ")}` : " (no specific gaps reported)"}
53371
53492
  `);
53372
53493
  if (roundsLeft > 0) {
@@ -53414,7 +53535,7 @@ import { spawnSync as spawnSync2 } from "child_process";
53414
53535
  // package.json
53415
53536
  var package_default = {
53416
53537
  name: "@mrciphersmith/keryx",
53417
- version: "0.2.55",
53538
+ version: "0.2.56",
53418
53539
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
53419
53540
  private: false,
53420
53541
  publishConfig: {
@@ -54674,7 +54795,7 @@ init_store3();
54674
54795
  init_proposal_lifecycle();
54675
54796
  init_workspace_service();
54676
54797
  import { randomUUID as randomUUID24 } from "crypto";
54677
- import { readdir as readdir26 } from "fs/promises";
54798
+ import { readdir as readdir26, stat as stat8 } from "fs/promises";
54678
54799
  import path150 from "path";
54679
54800
 
54680
54801
  // src/sac/lifecycle-flag.ts
@@ -54725,14 +54846,18 @@ async function computeLifecycleFlags(cwd, now = () => new Date) {
54725
54846
 
54726
54847
  // src/sac/catch-up.ts
54727
54848
  init_proposal_evidence();
54849
+ init_collect();
54850
+ init_store();
54728
54851
  async function buildCatchUp(input2) {
54729
- const [proposals, sessionCategories, lifecycleFlagsAll] = await Promise.all([
54852
+ const [proposals, sessionCategories, lifecycleFlagsAll, unreviewedPathsAll] = await Promise.all([
54730
54853
  collectProposals(input2.cwd, input2.workspaceId),
54731
54854
  collectSessionCategories(input2.cwd),
54732
- computeLifecycleFlags(input2.cwd)
54855
+ computeLifecycleFlags(input2.cwd),
54856
+ detectUnreviewedSacPathChanges(input2.cwd, listSessions(input2.cwd))
54733
54857
  ]);
54734
54858
  const lifecycleFlags = input2.workspaceId === undefined ? lifecycleFlagsAll : lifecycleFlagsAll.filter((flag) => flag.kind !== "workspace" || flag.ref === input2.workspaceId);
54735
- return { proposals, ...sessionCategories, lifecycleFlags };
54859
+ const unreviewedPaths = input2.workspaceId === undefined ? unreviewedPathsAll : unreviewedPathsAll.filter((item) => item.workspaceId === input2.workspaceId);
54860
+ return { proposals, ...sessionCategories, lifecycleFlags, unreviewedPaths };
54736
54861
  }
54737
54862
  async function collectProposals(cwd, workspaceId) {
54738
54863
  const authorizationServer = localWorkspaceAuthorizationServer();
@@ -54795,8 +54920,82 @@ async function classifySession(session) {
54795
54920
  const workspaceId = (await safeReadSlate(dir))?.workspaceId;
54796
54921
  return { kind: "unknown", item: { type: "unknown", sessionId: session.id, ...workspaceId !== undefined ? { workspaceId } : {}, lastSeenAt: session.updatedAt } };
54797
54922
  }
54923
+ async function readExternalUnboundCandidates(cwd) {
54924
+ const extDir = externalSlatesDir(cwd);
54925
+ let externalIds;
54926
+ try {
54927
+ const entries = await readdir26(extDir);
54928
+ externalIds = entries.filter((name) => name.endsWith(".json")).map((name) => name.slice(0, -".json".length));
54929
+ } catch {
54930
+ return [];
54931
+ }
54932
+ const candidates = [];
54933
+ for (const id of externalIds) {
54934
+ const slate = await readExternalSlate(cwd, id);
54935
+ if (!slate)
54936
+ continue;
54937
+ if (slate.closedAt === undefined || slate.workspaceId !== undefined)
54938
+ continue;
54939
+ const unbound2 = await readNewestUnboundCandidateForExternal(cwd, id);
54940
+ if (unbound2) {
54941
+ candidates.push({
54942
+ type: "unbound-candidate",
54943
+ externalSessionId: id,
54944
+ evidencePath: unbound2.evidencePath,
54945
+ summary: unbound2.summary
54946
+ });
54947
+ } else {
54948
+ const summary = summarizeUnboundCandidate((slate.seeds ?? []).reduce((groups, seed) => {
54949
+ const kind2 = seed.kind ?? "follow-up";
54950
+ const existing = groups.find((g) => g.kind === kind2);
54951
+ if (existing) {
54952
+ existing.seeds = (existing.seeds ?? []).concat([{ text: seed.text }]);
54953
+ } else {
54954
+ groups.push({ kind: kind2, seeds: [{ text: seed.text }] });
54955
+ }
54956
+ return groups;
54957
+ }, []));
54958
+ candidates.push({
54959
+ type: "unbound-candidate",
54960
+ externalSessionId: id,
54961
+ evidencePath: path150.join(extDir, `${id}.json`),
54962
+ summary
54963
+ });
54964
+ }
54965
+ }
54966
+ return candidates;
54967
+ }
54968
+ async function readNewestUnboundCandidateForExternal(cwd, externalSessionId) {
54969
+ const evidenceDir = path150.join(externalSlatesDir(cwd), externalSessionId);
54970
+ let entries;
54971
+ try {
54972
+ entries = (await readdir26(evidenceDir)).filter((name) => name.endsWith("-unbound-candidate.json"));
54973
+ } catch {
54974
+ return;
54975
+ }
54976
+ entries.sort();
54977
+ for (let i = entries.length - 1;i >= 0; i--) {
54978
+ const evidencePath = path150.join(evidenceDir, entries[i]);
54979
+ const result = readConfigFile(evidencePath);
54980
+ if (!result.ok) {
54981
+ continue;
54982
+ }
54983
+ try {
54984
+ const parsed = JSON.parse(result.text);
54985
+ if (parsed.recordType !== "unbound-candidate")
54986
+ continue;
54987
+ return { evidencePath, summary: summarizeUnboundCandidate(parsed.groups) };
54988
+ } catch {
54989
+ continue;
54990
+ }
54991
+ }
54992
+ return;
54993
+ }
54798
54994
  async function collectSessionCategories(cwd) {
54799
- const classified = await Promise.all(listSessions(cwd).map((session) => classifySession(session)));
54995
+ const [classified, externalUnboundCandidates] = await Promise.all([
54996
+ Promise.all(listSessions(cwd).map((session) => classifySession(session))),
54997
+ readExternalUnboundCandidates(cwd)
54998
+ ]);
54800
54999
  const blocked2 = [];
54801
55000
  const unboundCandidates = [];
54802
55001
  const unknown = [];
@@ -54810,8 +55009,151 @@ async function collectSessionCategories(cwd) {
54810
55009
  else
54811
55010
  unknown.push(category.item);
54812
55011
  }
55012
+ unboundCandidates.push(...externalUnboundCandidates);
54813
55013
  return { blocked: blocked2, unboundCandidates, unknown };
54814
55014
  }
55015
+ var SESSION_ATTRIBUTION_SLACK_MS = 5 * 60000;
55016
+ var MTIME_CLOCK_SKEW_TOLERANCE_MS = 5000;
55017
+ async function collectReceiptTargets(cwd, owner) {
55018
+ const targets = new Set;
55019
+ const workspacesDir = path150.join(cwd, ".metaproject", "workspaces");
55020
+ let workspaceIds;
55021
+ try {
55022
+ workspaceIds = (await readdir26(workspacesDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
55023
+ } catch {
55024
+ return targets;
55025
+ }
55026
+ for (const workspaceId of workspaceIds) {
55027
+ const receiptsDir = path150.join(workspacesDir, workspaceId, `${owner}-write-receipts`);
55028
+ let files;
55029
+ try {
55030
+ files = (await readdir26(receiptsDir)).filter((name) => name.endsWith(".json"));
55031
+ } catch {
55032
+ continue;
55033
+ }
55034
+ for (const file of files) {
55035
+ const result = readConfigFile(path150.join(receiptsDir, file));
55036
+ if (!result.ok)
55037
+ continue;
55038
+ try {
55039
+ const receipt = JSON.parse(result.text);
55040
+ if (typeof receipt.targetRef === "string") {
55041
+ targets.add(receipt.targetRef.replace(/^\.\//, ""));
55042
+ }
55043
+ } catch {
55044
+ continue;
55045
+ }
55046
+ }
55047
+ }
55048
+ return targets;
55049
+ }
55050
+ async function attributeToSession(absolutePath, sessionsNewestFirst) {
55051
+ let mtimeMs;
55052
+ try {
55053
+ mtimeMs = (await stat8(absolutePath)).mtimeMs;
55054
+ } catch {
55055
+ return;
55056
+ }
55057
+ for (const session of sessionsNewestFirst) {
55058
+ const start = Date.parse(session.createdAt);
55059
+ const end = Date.parse(session.updatedAt);
55060
+ if (Number.isNaN(start) || Number.isNaN(end))
55061
+ continue;
55062
+ if (mtimeMs >= start - MTIME_CLOCK_SKEW_TOLERANCE_MS && mtimeMs <= end + SESSION_ATTRIBUTION_SLACK_MS) {
55063
+ const workspaceId = (await safeReadSlate(sessionDir(session.projectPath, session.id)))?.workspaceId;
55064
+ return { sessionId: session.id, workspaceId, changedAt: new Date(mtimeMs).toISOString() };
55065
+ }
55066
+ }
55067
+ return;
55068
+ }
55069
+ async function findSkillFiles(dir) {
55070
+ let entries;
55071
+ try {
55072
+ entries = await readdir26(dir, { withFileTypes: true });
55073
+ } catch {
55074
+ return [];
55075
+ }
55076
+ const found = [];
55077
+ for (const entry of entries) {
55078
+ const full = path150.join(dir, entry.name);
55079
+ if (entry.isDirectory()) {
55080
+ found.push(...await findSkillFiles(full));
55081
+ } else if (entry.isFile() && entry.name === "SKILL.md") {
55082
+ found.push(full);
55083
+ }
55084
+ }
55085
+ return found;
55086
+ }
55087
+ async function detectUnreviewedSacPathChanges(cwd, sessions) {
55088
+ const scoped = [...sessions].filter((session) => session.projectPath === cwd).sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
55089
+ if (scoped.length === 0)
55090
+ return [];
55091
+ const [wikiReceipts, memoryReceipts, skillReceipts] = await Promise.all([
55092
+ collectReceiptTargets(cwd, "wiki"),
55093
+ collectReceiptTargets(cwd, "memory"),
55094
+ collectReceiptTargets(cwd, "skill")
55095
+ ]);
55096
+ const items = [];
55097
+ const wikiPages = await collectPages(cwd).catch(() => []);
55098
+ for (const page of wikiPages) {
55099
+ if ((page.status ?? "").toLowerCase() !== "accepted")
55100
+ continue;
55101
+ const targetRef = `wiki/${page.relativePath}`;
55102
+ if (wikiReceipts.has(targetRef))
55103
+ continue;
55104
+ const attribution = await attributeToSession(page.absolutePath, scoped);
55105
+ if (attribution === undefined)
55106
+ continue;
55107
+ items.push({
55108
+ type: "unreviewed-sac-path",
55109
+ sessionId: attribution.sessionId,
55110
+ ...attribution.workspaceId !== undefined ? { workspaceId: attribution.workspaceId } : {},
55111
+ owner: "wiki",
55112
+ path: targetRef,
55113
+ ...page.status !== null ? { status: page.status } : {},
55114
+ changedAt: attribution.changedAt
55115
+ });
55116
+ }
55117
+ const memoryEntries = await collectEntries(cwd).catch(() => []);
55118
+ for (const entry of memoryEntries) {
55119
+ if (entry.status !== "accepted")
55120
+ continue;
55121
+ const targetRef = `memory/${entry.relativePath}`;
55122
+ if (memoryReceipts.has(targetRef))
55123
+ continue;
55124
+ const attribution = await attributeToSession(entry.absolutePath, scoped);
55125
+ if (attribution === undefined)
55126
+ continue;
55127
+ items.push({
55128
+ type: "unreviewed-sac-path",
55129
+ sessionId: attribution.sessionId,
55130
+ ...attribution.workspaceId !== undefined ? { workspaceId: attribution.workspaceId } : {},
55131
+ owner: "memory",
55132
+ path: targetRef,
55133
+ status: entry.status,
55134
+ changedAt: attribution.changedAt
55135
+ });
55136
+ }
55137
+ const sacSkillsDir = path150.join(cwd, ".metaproject", "project-skills", "sac");
55138
+ const skillFiles = await findSkillFiles(sacSkillsDir);
55139
+ for (const absolutePath of skillFiles) {
55140
+ const targetRef = path150.relative(path150.join(cwd, ".metaproject"), absolutePath).split(path150.sep).join("/");
55141
+ if (skillReceipts.has(targetRef))
55142
+ continue;
55143
+ const attribution = await attributeToSession(absolutePath, scoped);
55144
+ if (attribution === undefined)
55145
+ continue;
55146
+ items.push({
55147
+ type: "unreviewed-sac-path",
55148
+ sessionId: attribution.sessionId,
55149
+ ...attribution.workspaceId !== undefined ? { workspaceId: attribution.workspaceId } : {},
55150
+ owner: "skill",
55151
+ path: targetRef,
55152
+ changedAt: attribution.changedAt
55153
+ });
55154
+ }
55155
+ return items;
55156
+ }
54815
55157
  async function isSlateEngaged(dir) {
54816
55158
  if (await pathExists(path150.join(dir, "slate.json")))
54817
55159
  return true;
@@ -55078,7 +55420,7 @@ async function loadInspectorCatchUp(cwd) {
55078
55420
  try {
55079
55421
  return await buildCatchUp({ cwd });
55080
55422
  } catch {
55081
- return { proposals: [], blocked: [], unboundCandidates: [], unknown: [], lifecycleFlags: [] };
55423
+ return { proposals: [], blocked: [], unboundCandidates: [], unknown: [], lifecycleFlags: [], unreviewedPaths: [] };
55082
55424
  }
55083
55425
  }
55084
55426
  function catchUpItems(report) {
@@ -65153,6 +65495,22 @@ New session ${shortSessionId(live.summary.id)}.
65153
65495
  slateSession,
65154
65496
  mintAttemptId: mintTimestampAttemptId
65155
65497
  });
65498
+ } else if (command === "/theme") {
65499
+ const wanted = rest.trim();
65500
+ if (wanted.length === 0) {
65501
+ agentIo.onSystem?.(formatThemeList(getThemeId()));
65502
+ } else {
65503
+ const next = parseThemeId(wanted);
65504
+ if (next === undefined) {
65505
+ agentIo.onSystem?.(`Unknown theme '${wanted}'.
65506
+ ${formatThemeList(getThemeId())}`);
65507
+ } else {
65508
+ applyThemeId(next);
65509
+ persistThemeId(next);
65510
+ agentIo.onSystem?.(`Theme: ${themeLabel(next)}
65511
+ `);
65512
+ }
65513
+ }
65156
65514
  } else {
65157
65515
  agentIo.onSystem?.(describeUnavailableCommand(command, "agent") ?? `Unknown command: ${command}. Type /help.
65158
65516
  `);
@@ -65417,6 +65775,12 @@ async function shellCommand(args2, runtime = {}) {
65417
65775
  }
65418
65776
  }
65419
65777
  const rl = readline2.createInterface({ input: process.stdin });
65778
+ if (!process.stdin.isTTY) {
65779
+ process.on("SIGINT", () => {
65780
+ rl.close();
65781
+ process.exit(130);
65782
+ });
65783
+ }
65420
65784
  const lineIterator = rl[Symbol.asyncIterator]();
65421
65785
  const sharedLines = { [Symbol.asyncIterator]: () => lineIterator };
65422
65786
  const { io, emitSystem, printHeader, printPrompt, destroy } = createRichIo(sharedLines, versionCheck);
@@ -65683,7 +66047,7 @@ Shell:
65683
66047
 
65684
66048
  // src/commands/modules.ts
65685
66049
  init_fs();
65686
- import { readFile as readFile79 } from "fs/promises";
66050
+ import { readFile as readFile80 } from "fs/promises";
65687
66051
  import { stdin } from "process";
65688
66052
  import path153 from "path";
65689
66053
  var MODULES = [
@@ -65745,7 +66109,7 @@ async function modulesCommand(args2 = []) {
65745
66109
  }
65746
66110
  let manifest = {};
65747
66111
  try {
65748
- manifest = JSON.parse(await readFile79(manifestPath, "utf8"));
66112
+ manifest = JSON.parse(await readFile80(manifestPath, "utf8"));
65749
66113
  } catch {}
65750
66114
  const enabled = new Set(MODULES.filter((module) => manifest.modules?.[module.name]?.enabled === true).map((module) => module.name));
65751
66115
  if (wantsJson) {
@@ -67719,7 +68083,7 @@ function printHelp17() {
67719
68083
 
67720
68084
  // src/commands/update.ts
67721
68085
  import { spawn as spawn5 } from "child_process";
67722
- import { chmod as chmod4, mkdir as mkdir56, readFile as readFile80, readdir as readdir27, writeFile as writeFile49 } from "fs/promises";
68086
+ import { chmod as chmod4, mkdir as mkdir56, readFile as readFile81, readdir as readdir27, writeFile as writeFile49 } from "fs/promises";
67723
68087
  import { access as access4, constants as constants2, existsSync as existsSync30 } from "fs";
67724
68088
  import path158 from "path";
67725
68089
  import { fileURLToPath as fileURLToPath7 } from "url";
@@ -68030,7 +68394,7 @@ async function shouldInstallDashboardPostCommitHook(projectRoot, manifest) {
68030
68394
  if (!await pathExists(hookPath)) {
68031
68395
  return false;
68032
68396
  }
68033
- return (await readFile80(hookPath, "utf8")).includes("# keryx:");
68397
+ return (await readFile81(hookPath, "utf8")).includes("# keryx:");
68034
68398
  }
68035
68399
  async function collectDashboardData(metaprojectRoot) {
68036
68400
  const data = {};
@@ -68082,12 +68446,12 @@ async function collectTasksDashboardData(metaprojectRoot) {
68082
68446
  continue;
68083
68447
  }
68084
68448
  try {
68085
- const flow = JSON.parse(await readFile80(flowPath, "utf8"));
68449
+ const flow = JSON.parse(await readFile81(flowPath, "utf8"));
68086
68450
  const tasks = Array.isArray(flow.tasks) ? flow.tasks : [];
68087
68451
  let acTotal = 0;
68088
68452
  const acPath2 = path158.join(flowsRoot2, dir, "acceptance-criteria.md");
68089
68453
  if (await pathExists(acPath2)) {
68090
- const acContent = await readFile80(acPath2, "utf8");
68454
+ const acContent = await readFile81(acPath2, "utf8");
68091
68455
  acTotal = (acContent.match(/^- AC\d+:/gm) ?? []).length;
68092
68456
  }
68093
68457
  flows.push({
@@ -68143,7 +68507,7 @@ async function collectDashboardDocs(metaprojectRoot, wiki, memory) {
68143
68507
  if (!await pathExists(filePath)) {
68144
68508
  continue;
68145
68509
  }
68146
- const content = await readFile80(filePath, "utf8");
68510
+ const content = await readFile81(filePath, "utf8");
68147
68511
  docs[href] = content.length > 40000 ? `${content.slice(0, 40000)}
68148
68512
 
68149
68513
  \u2026truncated\u2026` : content;
@@ -68160,7 +68524,7 @@ async function collectHealthDashboardData(metaprojectRoot) {
68160
68524
  if (!await pathExists(reportPath2)) {
68161
68525
  return;
68162
68526
  }
68163
- const report = JSON.parse(await readFile80(reportPath2, "utf8"));
68527
+ const report = JSON.parse(await readFile81(reportPath2, "utf8"));
68164
68528
  const metrics = Array.isArray(report.metrics) ? report.metrics : [];
68165
68529
  const findings = Array.isArray(report.findings) ? report.findings : [];
68166
68530
  const project = metrics.find((metric) => metric.key === "project") ?? {};
@@ -68274,7 +68638,7 @@ async function collectGraphDashboardData(metaprojectRoot) {
68274
68638
  let nodes = 0;
68275
68639
  let files = 0;
68276
68640
  let assets = 0;
68277
- for (const node of parseJsonl2(await readFile80(nodesPath, "utf8"))) {
68641
+ for (const node of parseJsonl2(await readFile81(nodesPath, "utf8"))) {
68278
68642
  nodes += 1;
68279
68643
  if (node.kind === "asset") {
68280
68644
  assets += 1;
@@ -68290,7 +68654,7 @@ async function collectGraphDashboardData(metaprojectRoot) {
68290
68654
  let imports = 0;
68291
68655
  let assetEdges = 0;
68292
68656
  let unresolved = 0;
68293
- for (const edge of parseJsonl2(await readFile80(edgesPath, "utf8"))) {
68657
+ for (const edge of parseJsonl2(await readFile81(edgesPath, "utf8"))) {
68294
68658
  edges += 1;
68295
68659
  if (edge.kind === "imports") {
68296
68660
  imports += 1;
@@ -68320,7 +68684,7 @@ async function collectTestingDashboardData(metaprojectRoot) {
68320
68684
  const reportPath2 = path158.join(metaprojectRoot, "data", "testing", "artifacts", "latest.json");
68321
68685
  const contextPath = path158.join(metaprojectRoot, "data", "testing", "context.md");
68322
68686
  if (await pathExists(reportPath2)) {
68323
- const report = JSON.parse(await readFile80(reportPath2, "utf8"));
68687
+ const report = JSON.parse(await readFile81(reportPath2, "utf8"));
68324
68688
  const totalTests = numberOrUndefined(report.total);
68325
68689
  const failedTests = Array.isArray(report.failures) ? report.failures.length : numberOrUndefined(report.failed);
68326
68690
  return {
@@ -68351,7 +68715,7 @@ async function collectMarkdownPages(root, hrefPrefix) {
68351
68715
  if (relativePath === "index.md" || relativePath.startsWith("templates/")) {
68352
68716
  continue;
68353
68717
  }
68354
- const content = await readFile80(filePath, "utf8");
68718
+ const content = await readFile81(filePath, "utf8");
68355
68719
  const embedded = content.length > 24000 ? `${content.slice(0, 24000)}
68356
68720
 
68357
68721
  \u2026truncated\u2026` : content;
@@ -68527,7 +68891,7 @@ async function enableTasksInManifest(metaprojectRoot) {
68527
68891
  }
68528
68892
  let raw;
68529
68893
  try {
68530
- raw = JSON.parse(await readFile80(manifestPath, "utf8"));
68894
+ raw = JSON.parse(await readFile81(manifestPath, "utf8"));
68531
68895
  } catch {
68532
68896
  return;
68533
68897
  }
@@ -68550,7 +68914,7 @@ async function updateManifestAgentEntrypoints(metaprojectRoot, ruleSources) {
68550
68914
  }
68551
68915
  let raw;
68552
68916
  try {
68553
- raw = JSON.parse(await readFile80(manifestPath, "utf8"));
68917
+ raw = JSON.parse(await readFile81(manifestPath, "utf8"));
68554
68918
  } catch {
68555
68919
  return;
68556
68920
  }
@@ -68659,7 +69023,7 @@ async function installManagedHook2(projectRoot, hookName, blockId, content) {
68659
69023
  const managedBlock = `${blockStart}
68660
69024
  ${content.trim()}
68661
69025
  ${blockEnd}`;
68662
- const existing = await pathExists(hookPath) ? await readFile80(hookPath, "utf8") : `#!/usr/bin/env sh
69026
+ const existing = await pathExists(hookPath) ? await readFile81(hookPath, "utf8") : `#!/usr/bin/env sh
68663
69027
  `;
68664
69028
  const blockPattern = new RegExp(`${escapeRegExp6(blockStart)}[\\s\\S]*?${escapeRegExp6(blockEnd)}`);
68665
69029
  const next = blockPattern.test(existing) ? existing.replace(blockPattern, managedBlock) : `${existing.trimEnd()}
@@ -68678,7 +69042,7 @@ async function removeManagedHook2(projectRoot, hookName, blockId) {
68678
69042
  if (!await pathExists(hookPath)) {
68679
69043
  return;
68680
69044
  }
68681
- const existing = await readFile80(hookPath, "utf8");
69045
+ const existing = await readFile81(hookPath, "utf8");
68682
69046
  const blockStart = `# keryx:${blockId}:begin`;
68683
69047
  const blockEnd = `# keryx:${blockId}:end`;
68684
69048
  const blockPattern = new RegExp(`\\n*${escapeRegExp6(blockStart)}[\\s\\S]*?${escapeRegExp6(blockEnd)}\\n*`);
@@ -68700,7 +69064,7 @@ async function prePushHasSecurityBlock2(projectRoot) {
68700
69064
  if (!await pathExists(hookPath)) {
68701
69065
  return false;
68702
69066
  }
68703
- const hook = await readFile80(hookPath, "utf8");
69067
+ const hook = await readFile81(hookPath, "utf8");
68704
69068
  return hook.includes("# keryx:security-pre-push:begin");
68705
69069
  }
68706
69070
  async function agentSettingsHasSecuritySentinel2(projectRoot) {
@@ -68708,7 +69072,7 @@ async function agentSettingsHasSecuritySentinel2(projectRoot) {
68708
69072
  if (!await pathExists(file)) {
68709
69073
  return false;
68710
69074
  }
68711
- return (await readFile80(file, "utf8")).includes(AGENT_HOOKS_SENTINEL);
69075
+ return (await readFile81(file, "utf8")).includes(AGENT_HOOKS_SENTINEL);
68712
69076
  }
68713
69077
  async function readManifest5(metaprojectRoot) {
68714
69078
  const manifestPath = path158.join(metaprojectRoot, "metaproject.json");
@@ -68721,7 +69085,7 @@ async function readManifest5(metaprojectRoot) {
68721
69085
  };
68722
69086
  }
68723
69087
  try {
68724
- const manifest = JSON.parse(await readFile80(manifestPath, "utf8"));
69088
+ const manifest = JSON.parse(await readFile81(manifestPath, "utf8"));
68725
69089
  const normalized = normalizeManifest(manifest);
68726
69090
  return {
68727
69091
  exists: true,
@@ -68845,7 +69209,7 @@ async function run(command, args2, cwd) {
68845
69209
  });
68846
69210
  }
68847
69211
  async function writeTextIfChanged4(filePath, content) {
68848
- if (await pathExists(filePath) && await readFile80(filePath, "utf8") === content) {
69212
+ if (await pathExists(filePath) && await readFile81(filePath, "utf8") === content) {
68849
69213
  return;
68850
69214
  }
68851
69215
  await mkdir56(path158.dirname(filePath), { recursive: true });
@@ -68859,8 +69223,8 @@ async function writeTextIfMissing4(filePath, content) {
68859
69223
  await writeFile49(filePath, content, "utf8");
68860
69224
  }
68861
69225
  async function copyFileIfChanged2(from, to) {
68862
- const next = await readFile80(from, "utf8");
68863
- if (await pathExists(to) && await readFile80(to, "utf8") === next) {
69226
+ const next = await readFile81(from, "utf8");
69227
+ if (await pathExists(to) && await readFile81(to, "utf8") === next) {
68864
69228
  return;
68865
69229
  }
68866
69230
  await mkdir56(path158.dirname(to), { recursive: true });
@@ -68970,7 +69334,7 @@ function printHelp19() {
68970
69334
  import { readFileSync as readFileSync10 } from "fs";
68971
69335
 
68972
69336
  // src/agents/bootstrap.ts
68973
- import { mkdir as mkdir57, readFile as readFile81, writeFile as writeFile50 } from "fs/promises";
69337
+ import { mkdir as mkdir57, readFile as readFile82, writeFile as writeFile50 } from "fs/promises";
68974
69338
  import { homedir as homedir7 } from "os";
68975
69339
  import path160 from "path";
68976
69340
  init_fs();
@@ -69043,7 +69407,7 @@ function resolveAgentBootstrapRuntimes(ids) {
69043
69407
  async function agentBootstrapStatus(runtime, homeRoot = homedir7()) {
69044
69408
  const filePath = runtime.filePath(homeRoot);
69045
69409
  const exists2 = await pathExists(filePath);
69046
- const content = exists2 ? await readFile81(filePath, "utf8") : "";
69410
+ const content = exists2 ? await readFile82(filePath, "utf8") : "";
69047
69411
  const expected = renderAgentBootstrapBlock(runtime.fileName).trim();
69048
69412
  const installed = content.includes(AGENT_BOOTSTRAP_START);
69049
69413
  const current = installed && extractManagedBlock(content)?.trim() === expected;
@@ -69053,7 +69417,7 @@ async function installAgentBootstrap(runtime, options = {}) {
69053
69417
  const homeRoot = options.homeRoot ?? homedir7();
69054
69418
  const filePath = runtime.filePath(homeRoot);
69055
69419
  const exists2 = await pathExists(filePath);
69056
- const current = exists2 ? await readFile81(filePath, "utf8") : "";
69420
+ const current = exists2 ? await readFile82(filePath, "utf8") : "";
69057
69421
  const next = upsertManagedBlock(current || defaultAgentFile(runtime), renderAgentBootstrapBlock(runtime.fileName));
69058
69422
  const dryRun = options.dryRun === true;
69059
69423
  const wrote = next !== current;
@@ -69068,7 +69432,7 @@ async function uninstallAgentBootstrap(runtime, options = {}) {
69068
69432
  const homeRoot = options.homeRoot ?? homedir7();
69069
69433
  const filePath = runtime.filePath(homeRoot);
69070
69434
  const exists2 = await pathExists(filePath);
69071
- const current = exists2 ? await readFile81(filePath, "utf8") : "";
69435
+ const current = exists2 ? await readFile82(filePath, "utf8") : "";
69072
69436
  const next = removeManagedBlock(current);
69073
69437
  const dryRun = options.dryRun === true;
69074
69438
  const removed = next !== current;
@@ -69594,7 +69958,7 @@ function printBootstrapHelp() {
69594
69958
 
69595
69959
  // src/commands/metrics.ts
69596
69960
  init_args();
69597
- import { readFile as readFile82 } from "fs/promises";
69961
+ import { readFile as readFile83 } from "fs/promises";
69598
69962
  import path162 from "path";
69599
69963
 
69600
69964
  // src/metrics/benchmark.ts
@@ -70870,7 +71234,7 @@ async function metricsCommand(args2 = [], projectRoot = process.cwd()) {
70870
71234
  process.exitCode = 1;
70871
71235
  return;
70872
71236
  }
70873
- const record2 = JSON.parse(await readFile82(path162.resolve(projectRoot, file), "utf8"));
71237
+ const record2 = JSON.parse(await readFile83(path162.resolve(projectRoot, file), "utf8"));
70874
71238
  const result = validateRunRecord(record2);
70875
71239
  console.log(result.valid ? "valid: yes" : "valid: no");
70876
71240
  for (const error2 of result.errors)
@@ -70901,7 +71265,7 @@ async function metricsCommand(args2 = [], projectRoot = process.cwd()) {
70901
71265
  process.exitCode = 1;
70902
71266
  return;
70903
71267
  }
70904
- console.log(await readFile82(file, "utf8"));
71268
+ console.log(await readFile83(file, "utf8"));
70905
71269
  return;
70906
71270
  }
70907
71271
  if (subcommand === "compare") {
@@ -70912,8 +71276,8 @@ async function metricsCommand(args2 = [], projectRoot = process.cwd()) {
70912
71276
  process.exitCode = 1;
70913
71277
  return;
70914
71278
  }
70915
- const a = JSON.parse(await readFile82(path162.join(metricsRoot(projectRoot), "runs", `${runA}.json`), "utf8"));
70916
- const b = JSON.parse(await readFile82(path162.join(metricsRoot(projectRoot), "runs", `${runB}.json`), "utf8"));
71279
+ const a = JSON.parse(await readFile83(path162.join(metricsRoot(projectRoot), "runs", `${runA}.json`), "utf8"));
71280
+ const b = JSON.parse(await readFile83(path162.join(metricsRoot(projectRoot), "runs", `${runB}.json`), "utf8"));
70917
71281
  const comparison = compareExecutionRuns(a, b);
70918
71282
  console.log(stableJson(comparison));
70919
71283
  return;
@@ -70962,7 +71326,7 @@ async function metricsCommand(args2 = [], projectRoot = process.cwd()) {
70962
71326
  process.exitCode = 1;
70963
71327
  return;
70964
71328
  }
70965
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, file), "utf8"));
71329
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, file), "utf8"));
70966
71330
  const input2 = Array.isArray(raw) ? raw : raw.runs ?? [];
70967
71331
  const result = validatePairedBenchmark(input2);
70968
71332
  console.log(stableJson(result));
@@ -70974,7 +71338,7 @@ async function metricsCommand(args2 = [], projectRoot = process.cwd()) {
70974
71338
  process.exitCode = 1;
70975
71339
  }
70976
71340
  async function loadAffectedSets(projectRoot, file) {
70977
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, file), "utf8"));
71341
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, file), "utf8"));
70978
71342
  const map = new Map;
70979
71343
  for (const entry of raw.targets ?? []) {
70980
71344
  if (typeof entry.target === "string")
@@ -71045,7 +71409,7 @@ async function runHarnessLayer(projectRoot, args2, ladder) {
71045
71409
  let tasks;
71046
71410
  let model;
71047
71411
  try {
71048
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, resultsPath), "utf8"));
71412
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, resultsPath), "utf8"));
71049
71413
  tasks = raw.tasks ?? [];
71050
71414
  model = raw.model;
71051
71415
  } catch (error2) {
@@ -71076,7 +71440,7 @@ async function runSafetyCompletionHonestyLayer(projectRoot, args2, ladder) {
71076
71440
  let cases;
71077
71441
  let model;
71078
71442
  try {
71079
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, resultsPath), "utf8"));
71443
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, resultsPath), "utf8"));
71080
71444
  cases = raw.cases ?? [];
71081
71445
  model = raw.model;
71082
71446
  } catch (error2) {
@@ -71101,7 +71465,7 @@ async function runSafetyFalsePremiseLayer(projectRoot, args2, ladder) {
71101
71465
  let cases;
71102
71466
  let model;
71103
71467
  try {
71104
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, resultsPath), "utf8"));
71468
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, resultsPath), "utf8"));
71105
71469
  cases = raw.cases ?? [];
71106
71470
  model = raw.model;
71107
71471
  } catch (error2) {
@@ -71126,7 +71490,7 @@ async function runSafetyContainmentLayer(projectRoot, args2, ladder, caseClass)
71126
71490
  let cases;
71127
71491
  let model;
71128
71492
  try {
71129
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, resultsPath), "utf8"));
71493
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, resultsPath), "utf8"));
71130
71494
  cases = raw.cases ?? [];
71131
71495
  model = raw.model;
71132
71496
  } catch (error2) {
@@ -71217,7 +71581,7 @@ async function runGdgraphLayer(projectRoot, args2, ladder) {
71217
71581
  return allValid;
71218
71582
  }
71219
71583
  async function loadCoverageMap2(projectRoot, file) {
71220
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, file), "utf8"));
71584
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, file), "utf8"));
71221
71585
  return raw.coverageMap ?? {};
71222
71586
  }
71223
71587
  async function runTestingLayer(projectRoot, args2, ladder) {
@@ -71254,7 +71618,7 @@ async function runTestingLayer(projectRoot, args2, ladder) {
71254
71618
  return result.valid;
71255
71619
  }
71256
71620
  async function loadMemoryGoldK(projectRoot, file) {
71257
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, file), "utf8"));
71621
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, file), "utf8"));
71258
71622
  return typeof raw.k === "number" && raw.k > 0 ? raw.k : 3;
71259
71623
  }
71260
71624
  async function runMemoryLayer(projectRoot, args2, ladder) {
@@ -71291,11 +71655,11 @@ async function runMemoryLayer(projectRoot, args2, ladder) {
71291
71655
  return result.valid;
71292
71656
  }
71293
71657
  async function loadWikiGoldK(projectRoot, file) {
71294
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, file), "utf8"));
71658
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, file), "utf8"));
71295
71659
  return typeof raw.k === "number" && raw.k > 0 ? raw.k : 5;
71296
71660
  }
71297
71661
  async function loadWikiGroundedness(projectRoot, file) {
71298
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, file), "utf8"));
71662
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, file), "utf8"));
71299
71663
  const map = new Map;
71300
71664
  for (const entry of raw.targets ?? []) {
71301
71665
  if (typeof entry.target !== "string" || !Array.isArray(entry.scores) || entry.scores.length !== 3)
@@ -71351,7 +71715,7 @@ async function runWikiLayer(projectRoot, args2, ladder) {
71351
71715
  return result.valid;
71352
71716
  }
71353
71717
  async function loadGdctxFacts(projectRoot, file) {
71354
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, file), "utf8"));
71718
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, file), "utf8"));
71355
71719
  const inputs = [];
71356
71720
  for (const entry of raw.inputs ?? []) {
71357
71721
  if (typeof entry.input === "string") {
@@ -71389,7 +71753,7 @@ async function collect(projectRoot, args2) {
71389
71753
  process.exitCode = 1;
71390
71754
  return;
71391
71755
  }
71392
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, eventFile), "utf8"));
71756
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, eventFile), "utf8"));
71393
71757
  const events2 = Array.isArray(raw) ? raw : raw.events;
71394
71758
  const startedAt = optionValue(args2, "--started-at") ?? events2[0]?.timestamp_utc ?? new Date().toISOString();
71395
71759
  const finishedAt = optionValue(args2, "--finished-at") ?? events2.at(-1)?.timestamp_utc ?? startedAt;
@@ -71789,6 +72153,7 @@ function renderCatchUp(report, includeLifecycleFlags = true) {
71789
72153
  sections.push(renderSection("Blocked sessions (stopped unattended)", report.blocked, (item) => `- Session ${item.sessionId} stopped unattended (${item.terminalState.reason}) at ${item.terminalState.occurredAt}. Resume it, or archive and move on? ` + `Recommendation: \`keryx shell -r ${item.sessionId}\` to resume and unblock it.`));
71790
72154
  sections.push(renderSection("Unbound candidates (wrap-up ran, no workspace bound)", report.unboundCandidates, (item) => `- Session ${item.sessionId} produced untriaged seeds with no workspace bound (${item.summary}). Bind to a workspace and propose, or discard? ` + `Recommendation: pick a workspace, then \`keryx workspace propose <workspace-id> --kind <kind> --session ${item.sessionId}\` (evidence: ${item.evidencePath}).`));
71791
72155
  sections.push(renderSection("Unknown (no resolution recorded)", report.unknown, (item) => `- Session ${item.sessionId} was last seen ${item.lastSeenAt} with no proposal, terminal state, or unbound-candidate artifact recorded. Investigate, or ignore? ` + `Recommendation: \`keryx sessions list\` / \`keryx shell -r ${item.sessionId}\` to see what happened.`));
72156
+ sections.push(renderSection("Unreviewed SAC-owned changes (no proposal on record)", report.unreviewedPaths, (item) => `- Session ${item.sessionId} changed ${item.owner} path \`${item.path}\`${item.status !== undefined ? ` (Status: ${item.status})` : ""} at ${item.changedAt} with NO SAC proposal/receipt behind it \u2014 this looks like it bypassed review. Was this reviewed some other way, or should it be? ` + `Recommendation: \`keryx shell -r ${item.sessionId}\` to see what happened; if the content is good, route it through a real proposal (\`keryx workspace propose ...\`) before trusting it as durable knowledge.`));
71792
72157
  if (includeLifecycleFlags) {
71793
72158
  sections.push(renderSection("Lifecycle flags (component no longer in the graph)", report.lifecycleFlags, (item) => `- ${item.kind} \`${item.ref}\` scopes to \`${item.missingComponent}\`, which is no longer in the code graph (flagged ${item.flaggedAt}). Still relevant, or safe to clean up? ` + `Recommendation: this is report-only \u2014 nothing was archived/edited/removed automatically; ${item.kind === "workspace" ? "`keryx workspace archive " + item.ref + "`" : item.kind === "memory-entry" ? "`keryx memory supersede` or edit the entry directly" : "edit or remove the wiki page directly"} if you decide it's actually stale.`));
71794
72159
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.2.55",
3
+ "version": "0.2.56",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {