@mrciphersmith/keryx 0.2.54 → 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 +453 -80
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -19314,6 +19314,8 @@ async function runAgentTurnCore(io, deps, history, userLine, options = {}) {
19314
19314
  const spawnConcurrencyCandidates = calls.filter((call) => call.name === "spawn_subagent" && reservationByCallId.get(call.id)?.ok === true);
19315
19315
  const untrustedGateBlocksSpawns = untrustedContentSeen || batchContainsUntrustedWeb;
19316
19316
  const concurrentSpawnResults = spawnConcurrencyCandidates.length >= 2 && !untrustedGateBlocksSpawns ? await runConcurrentSpawnBatch(spawnConcurrencyCandidates, toolByName, io, deps) : undefined;
19317
+ let anchorsToAnnounce;
19318
+ let repeatedFailureHint;
19317
19319
  for (const call of calls) {
19318
19320
  if (isAborted()) {
19319
19321
  system(`
@@ -19375,8 +19377,7 @@ ${modelOutput}` : modelOutput,
19375
19377
  runtime: { provider: deps.providerId, model: deps.modelId }
19376
19378
  });
19377
19379
  if (touch.changed) {
19378
- history.push({ role: "user", content: renderAnchorsBlock(touch.slate.anchors), provenance: "project" });
19379
- io.onHistoryChange?.("tool");
19380
+ anchorsToAnnounce = touch.slate.anchors;
19380
19381
  }
19381
19382
  } catch (err) {
19382
19383
  io.onSystem?.(`slate touch update failed (ignored): ${err instanceof Error ? err.message : String(err)}
@@ -19397,14 +19398,21 @@ ${modelOutput}` : modelOutput,
19397
19398
  system(`
19398
19399
  ${hint}
19399
19400
  `);
19400
- history.push({ role: "user", content: hint, provenance: "project" });
19401
- io.onHistoryChange?.("tool");
19401
+ repeatedFailureHint = hint;
19402
19402
  }
19403
19403
  } else {
19404
19404
  lastErrorByHash.delete(reservation.hash);
19405
19405
  errorStreakByHash.delete(reservation.hash);
19406
19406
  }
19407
19407
  }
19408
+ if (anchorsToAnnounce !== undefined) {
19409
+ history.push({ role: "user", content: renderAnchorsBlock(anchorsToAnnounce), provenance: "project" });
19410
+ io.onHistoryChange?.("tool");
19411
+ }
19412
+ if (repeatedFailureHint !== undefined) {
19413
+ history.push({ role: "user", content: repeatedFailureHint, provenance: "project" });
19414
+ io.onHistoryChange?.("tool");
19415
+ }
19408
19416
  const noProgress = !executedAny && calls.length > 0;
19409
19417
  if (exhaustedBudget !== undefined || noProgress) {
19410
19418
  const finishReason = exhaustedBudget !== undefined ? "budget" : "no-progress";
@@ -26103,6 +26111,10 @@ ${markdown.slice(4)}`;
26103
26111
  }
26104
26112
  return markdown;
26105
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
+ }
26106
26118
  async function mapPool(items, concurrency, worker) {
26107
26119
  const results = new Array(items.length);
26108
26120
  let next = 0;
@@ -26129,7 +26141,6 @@ async function wikiEnrich(input2) {
26129
26141
  const concurrency = Math.max(1, Math.min(MAX_CONCURRENCY, input2.concurrency ?? DEFAULT_CONCURRENCY));
26130
26142
  const maxOutputTokens = input2.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
26131
26143
  const validate = input2.validate !== false;
26132
- const markAccepted = input2.keepStatus === true ? false : input2.markAccepted !== false;
26133
26144
  const wikiConfig = await loadWikiConfig(input2.cwd);
26134
26145
  const result = {
26135
26146
  provider,
@@ -26223,9 +26234,7 @@ async function wikiEnrich(input2) {
26223
26234
  return { path: page.relativePath, action: "failed", reason: `validation: ${structural}` };
26224
26235
  }
26225
26236
  }
26226
- if (markAccepted) {
26227
- enriched = setFrontmatterStatus(enriched, "accepted");
26228
- }
26237
+ enriched = setFrontmatterStatus(enriched, extractFrontmatterStatus(original));
26229
26238
  if (input2.dryRun) {
26230
26239
  onPage({ index, total, path: page.relativePath, status, phase: "done" });
26231
26240
  return {
@@ -26239,7 +26248,7 @@ async function wikiEnrich(input2) {
26239
26248
  await writeFile36(page.absolutePath, `${enriched.endsWith(`
26240
26249
  `) ? enriched : `${enriched}
26241
26250
  `}`, "utf8");
26242
- onPage({ index, total, path: page.relativePath, status: markAccepted ? "accepted" : status, phase: "done" });
26251
+ onPage({ index, total, path: page.relativePath, status, phase: "done" });
26243
26252
  return {
26244
26253
  path: page.relativePath,
26245
26254
  action: "enriched",
@@ -26265,7 +26274,6 @@ async function wikiEnrich(input2) {
26265
26274
  model,
26266
26275
  maxOutputTokens,
26267
26276
  validate,
26268
- markAccepted,
26269
26277
  concurrency,
26270
26278
  total,
26271
26279
  onPage,
@@ -26348,8 +26356,8 @@ function finalizeEnrichedText(original, rawText, options) {
26348
26356
  if (options.validate) {
26349
26357
  structuralError = validateEnrichedMarkdown(original, content);
26350
26358
  }
26351
- if (structuralError === null && options.markAccepted) {
26352
- content = setFrontmatterStatus(content, "accepted");
26359
+ if (structuralError === null) {
26360
+ content = setFrontmatterStatus(content, extractFrontmatterStatus(original));
26353
26361
  }
26354
26362
  return { content, structuralError };
26355
26363
  }
@@ -26376,7 +26384,7 @@ async function finishSuccess(ctx, page, originalRaw, content, keyFiles, extra) {
26376
26384
  index,
26377
26385
  total: ctx.total,
26378
26386
  path: page.relativePath,
26379
- status: ctx.markAccepted ? "accepted" : status,
26387
+ status,
26380
26388
  phase: "done"
26381
26389
  });
26382
26390
  return {
@@ -26420,8 +26428,7 @@ async function runDeepSingle(ctx, item) {
26420
26428
  let content = null;
26421
26429
  if (rawText !== null) {
26422
26430
  const finalized = finalizeEnrichedText(original, rawText, {
26423
- validate: ctx.validate,
26424
- markAccepted: ctx.markAccepted
26431
+ validate: ctx.validate
26425
26432
  });
26426
26433
  if (finalized.structuralError === null) {
26427
26434
  content = finalized.content;
@@ -26606,8 +26613,7 @@ async function runLightBatch(ctx, items) {
26606
26613
  continue;
26607
26614
  }
26608
26615
  const finalized = finalizeEnrichedText(item.original, rawText, {
26609
- validate: ctx.validate,
26610
- markAccepted: ctx.markAccepted
26616
+ validate: ctx.validate
26611
26617
  });
26612
26618
  if (finalized.structuralError !== null) {
26613
26619
  ctx.onPage({ index, total: ctx.total, path: item.page.relativePath, status, phase: "failed" });
@@ -26658,7 +26664,6 @@ async function runRlmPipeline(ctxInput) {
26658
26664
  model: ctxInput.model,
26659
26665
  maxOutputTokens: ctxInput.maxOutputTokens,
26660
26666
  validate: ctxInput.validate,
26661
- markAccepted: ctxInput.markAccepted,
26662
26667
  total: ctxInput.total,
26663
26668
  onPage: ctxInput.onPage,
26664
26669
  input: input2,
@@ -36667,7 +36672,6 @@ async function runEnrich(args2) {
36667
36672
  const resume = args2.includes("--resume");
36668
36673
  const refreshGraph = args2.includes("--refresh-graph");
36669
36674
  const dryRun = args2.includes("--dry-run");
36670
- const keepStatus = args2.includes("--keep-status");
36671
36675
  const noValidate = args2.includes("--no-validate");
36672
36676
  const valueFlags = new Set([
36673
36677
  "--page",
@@ -36736,9 +36740,7 @@ async function runEnrich(args2) {
36736
36740
  resume,
36737
36741
  refreshGraph,
36738
36742
  dryRun,
36739
- keepStatus,
36740
36743
  validate: !noValidate,
36741
- markAccepted: !keepStatus,
36742
36744
  ...prompt ? { prompt } : {},
36743
36745
  ...provider ? { provider } : {},
36744
36746
  ...model ? { model } : {},
@@ -36815,10 +36817,12 @@ Usage:
36815
36817
  keryx wiki validate
36816
36818
  keryx wiki ask "<question>" [--k <n>] [--rerank]
36817
36819
  keryx wiki enrich [<page>|--all] [--force] [--list] [--resume] [--limit N] [--concurrency N]
36818
- [--refresh-graph] [--max-tokens N] [--keep-status] [--no-validate]
36820
+ [--refresh-graph] [--max-tokens N] [--no-validate]
36819
36821
  [--prompt "<i>"] [--provider <p>] [--model <m>] [--dry-run] [--json]
36820
36822
  # defaults: drafts only; provider/model from auth.json; validate on;
36821
- # 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)
36822
36826
  keryx wiki context
36823
36827
  keryx wiki backlinks <wiki-page-or-code-file>
36824
36828
 
@@ -49933,7 +49937,8 @@ var PREFIX_BANNED = new Set([
49933
49937
  "osascript",
49934
49938
  "open",
49935
49939
  "tee",
49936
- "cd"
49940
+ "cd",
49941
+ "keryx"
49937
49942
  ]);
49938
49943
  var PREFIX_BANNED_READERS = new Set([
49939
49944
  "cat",
@@ -50004,6 +50009,17 @@ function validateShellPattern(pattern) {
50004
50009
  }
50005
50010
  return { ok: true };
50006
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
+ }
50007
50023
  function bannedPrefixGrant(pattern, firstToken) {
50008
50024
  const rest = pattern.slice(firstToken.length).trim();
50009
50025
  const wildcardOnly = /^\*+$/.test(rest) || rest.length === 0 && /\*+$/.test(firstToken);
@@ -50016,6 +50032,13 @@ function bannedPrefixGrant(pattern, firstToken) {
50016
50032
  reason: `\`${word} *\` grants arbitrary execution: ${word} is an interpreter or wrapper, so its first token does not constrain what runs`
50017
50033
  };
50018
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
+ }
50019
50042
  if (PREFIX_BANNED_READERS.has(word)) {
50020
50043
  return {
50021
50044
  word,
@@ -53100,8 +53123,10 @@ init_slate();
53100
53123
  init_workspace_service();
53101
53124
  init_workspace_resolve();
53102
53125
  init_service7();
53126
+ init_store2();
53103
53127
  init_fs();
53104
53128
  import path149 from "path";
53129
+ import { readFile as readFile79 } from "fs/promises";
53105
53130
  var POSITIVE_INTEGER = /^[1-9][0-9]*$/;
53106
53131
  var DEFAULT_AUTO_GOAL_ROUNDS = 8;
53107
53132
  function parseGoalArgs(rest) {
@@ -53203,9 +53228,20 @@ async function autoProvisionFlow(cwd, goalText) {
53203
53228
  await service5.start({ cwd, id: result.flow.id });
53204
53229
  return result.flow.id;
53205
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
+ }
53206
53241
  async function buildContinuationMessage(cwd, slateSession, round4, roundsCap) {
53207
53242
  const totalRounds = roundsCap + 1;
53208
- 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}`;
53209
53245
  const slate = await readSlate(slateSession.dir).catch(() => {
53210
53246
  return;
53211
53247
  });
@@ -53243,11 +53279,61 @@ function parseVerifierVerdict(output2) {
53243
53279
  return;
53244
53280
  }
53245
53281
  }
53246
- 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) {
53247
53332
  const tool = deps.tools.find((candidate) => candidate.definition.name === "spawn_subagent");
53248
53333
  if (tool === undefined) {
53249
53334
  return;
53250
53335
  }
53336
+ const { evidenceText, deferToVerifier } = await buildVerifierEvidence(cwd, slateSession, history);
53251
53337
  const task = [
53252
53338
  "Independently verify whether the following goal has ACTUALLY been achieved, based on the",
53253
53339
  "current, real state of the repository (read the real files/tests \u2014 never trust a prior",
@@ -53255,17 +53341,49 @@ async function runGoalVerifier(deps, goalText) {
53255
53341
  "",
53256
53342
  `Goal: "${goalText}"`,
53257
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
+ ] : [],
53258
53356
  "Reply with EXACTLY one JSON object and nothing else, no prose before or after it:",
53259
53357
  '{"achieved": true or false, "gaps": ["specific reason it is not fully achieved", ...]}',
53260
53358
  '"gaps" must be empty when "achieved" is true.'
53261
53359
  ].join(`
53262
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");
53263
53371
  let result;
53264
53372
  try {
53265
- result = await tool.invoke({ task, mode: "read_only", label: "goal-verifier" });
53266
- } 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");
53267
53382
  return;
53268
53383
  }
53384
+ io.onToolResult?.("spawn_subagent", result);
53385
+ history.push({ role: "tool", content: result.output, provenance: "tool", toolCallId: callId });
53386
+ io.onHistoryChange?.("tool");
53269
53387
  if (result.isError) {
53270
53388
  return;
53271
53389
  }
@@ -53355,10 +53473,21 @@ async function runGoalCommand(params) {
53355
53473
  systemLine(io, `/goal --auto: round ${round4}/${roundsCap + 1} \u2014 continuing toward the goal.
53356
53474
  `);
53357
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
+ }
53358
53481
  }
53359
53482
  const wasOpenBeforeVerifier = slateSession.opened;
53360
- const verdict = await runGoalVerifier(deps, parsed.text);
53361
- 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 {
53362
53491
  systemLine(io, `/goal --auto: verifier found the goal not fully achieved${verdict.gaps.length > 0 ? ` \u2014 ${verdict.gaps.join("; ")}` : " (no specific gaps reported)"}
53363
53492
  `);
53364
53493
  if (roundsLeft > 0) {
@@ -53406,7 +53535,7 @@ import { spawnSync as spawnSync2 } from "child_process";
53406
53535
  // package.json
53407
53536
  var package_default = {
53408
53537
  name: "@mrciphersmith/keryx",
53409
- version: "0.2.54",
53538
+ version: "0.2.56",
53410
53539
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
53411
53540
  private: false,
53412
53541
  publishConfig: {
@@ -54666,7 +54795,7 @@ init_store3();
54666
54795
  init_proposal_lifecycle();
54667
54796
  init_workspace_service();
54668
54797
  import { randomUUID as randomUUID24 } from "crypto";
54669
- import { readdir as readdir26 } from "fs/promises";
54798
+ import { readdir as readdir26, stat as stat8 } from "fs/promises";
54670
54799
  import path150 from "path";
54671
54800
 
54672
54801
  // src/sac/lifecycle-flag.ts
@@ -54717,14 +54846,18 @@ async function computeLifecycleFlags(cwd, now = () => new Date) {
54717
54846
 
54718
54847
  // src/sac/catch-up.ts
54719
54848
  init_proposal_evidence();
54849
+ init_collect();
54850
+ init_store();
54720
54851
  async function buildCatchUp(input2) {
54721
- const [proposals, sessionCategories, lifecycleFlagsAll] = await Promise.all([
54852
+ const [proposals, sessionCategories, lifecycleFlagsAll, unreviewedPathsAll] = await Promise.all([
54722
54853
  collectProposals(input2.cwd, input2.workspaceId),
54723
54854
  collectSessionCategories(input2.cwd),
54724
- computeLifecycleFlags(input2.cwd)
54855
+ computeLifecycleFlags(input2.cwd),
54856
+ detectUnreviewedSacPathChanges(input2.cwd, listSessions(input2.cwd))
54725
54857
  ]);
54726
54858
  const lifecycleFlags = input2.workspaceId === undefined ? lifecycleFlagsAll : lifecycleFlagsAll.filter((flag) => flag.kind !== "workspace" || flag.ref === input2.workspaceId);
54727
- return { proposals, ...sessionCategories, lifecycleFlags };
54859
+ const unreviewedPaths = input2.workspaceId === undefined ? unreviewedPathsAll : unreviewedPathsAll.filter((item) => item.workspaceId === input2.workspaceId);
54860
+ return { proposals, ...sessionCategories, lifecycleFlags, unreviewedPaths };
54728
54861
  }
54729
54862
  async function collectProposals(cwd, workspaceId) {
54730
54863
  const authorizationServer = localWorkspaceAuthorizationServer();
@@ -54787,8 +54920,82 @@ async function classifySession(session) {
54787
54920
  const workspaceId = (await safeReadSlate(dir))?.workspaceId;
54788
54921
  return { kind: "unknown", item: { type: "unknown", sessionId: session.id, ...workspaceId !== undefined ? { workspaceId } : {}, lastSeenAt: session.updatedAt } };
54789
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
+ }
54790
54994
  async function collectSessionCategories(cwd) {
54791
- 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
+ ]);
54792
54999
  const blocked2 = [];
54793
55000
  const unboundCandidates = [];
54794
55001
  const unknown = [];
@@ -54802,8 +55009,151 @@ async function collectSessionCategories(cwd) {
54802
55009
  else
54803
55010
  unknown.push(category.item);
54804
55011
  }
55012
+ unboundCandidates.push(...externalUnboundCandidates);
54805
55013
  return { blocked: blocked2, unboundCandidates, unknown };
54806
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
+ }
54807
55157
  async function isSlateEngaged(dir) {
54808
55158
  if (await pathExists(path150.join(dir, "slate.json")))
54809
55159
  return true;
@@ -55070,7 +55420,7 @@ async function loadInspectorCatchUp(cwd) {
55070
55420
  try {
55071
55421
  return await buildCatchUp({ cwd });
55072
55422
  } catch {
55073
- return { proposals: [], blocked: [], unboundCandidates: [], unknown: [], lifecycleFlags: [] };
55423
+ return { proposals: [], blocked: [], unboundCandidates: [], unknown: [], lifecycleFlags: [], unreviewedPaths: [] };
55074
55424
  }
55075
55425
  }
55076
55426
  function catchUpItems(report) {
@@ -65145,6 +65495,22 @@ New session ${shortSessionId(live.summary.id)}.
65145
65495
  slateSession,
65146
65496
  mintAttemptId: mintTimestampAttemptId
65147
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
+ }
65148
65514
  } else {
65149
65515
  agentIo.onSystem?.(describeUnavailableCommand(command, "agent") ?? `Unknown command: ${command}. Type /help.
65150
65516
  `);
@@ -65409,6 +65775,12 @@ async function shellCommand(args2, runtime = {}) {
65409
65775
  }
65410
65776
  }
65411
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
+ }
65412
65784
  const lineIterator = rl[Symbol.asyncIterator]();
65413
65785
  const sharedLines = { [Symbol.asyncIterator]: () => lineIterator };
65414
65786
  const { io, emitSystem, printHeader, printPrompt, destroy } = createRichIo(sharedLines, versionCheck);
@@ -65675,7 +66047,7 @@ Shell:
65675
66047
 
65676
66048
  // src/commands/modules.ts
65677
66049
  init_fs();
65678
- import { readFile as readFile79 } from "fs/promises";
66050
+ import { readFile as readFile80 } from "fs/promises";
65679
66051
  import { stdin } from "process";
65680
66052
  import path153 from "path";
65681
66053
  var MODULES = [
@@ -65737,7 +66109,7 @@ async function modulesCommand(args2 = []) {
65737
66109
  }
65738
66110
  let manifest = {};
65739
66111
  try {
65740
- manifest = JSON.parse(await readFile79(manifestPath, "utf8"));
66112
+ manifest = JSON.parse(await readFile80(manifestPath, "utf8"));
65741
66113
  } catch {}
65742
66114
  const enabled = new Set(MODULES.filter((module) => manifest.modules?.[module.name]?.enabled === true).map((module) => module.name));
65743
66115
  if (wantsJson) {
@@ -67711,7 +68083,7 @@ function printHelp17() {
67711
68083
 
67712
68084
  // src/commands/update.ts
67713
68085
  import { spawn as spawn5 } from "child_process";
67714
- 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";
67715
68087
  import { access as access4, constants as constants2, existsSync as existsSync30 } from "fs";
67716
68088
  import path158 from "path";
67717
68089
  import { fileURLToPath as fileURLToPath7 } from "url";
@@ -68022,7 +68394,7 @@ async function shouldInstallDashboardPostCommitHook(projectRoot, manifest) {
68022
68394
  if (!await pathExists(hookPath)) {
68023
68395
  return false;
68024
68396
  }
68025
- return (await readFile80(hookPath, "utf8")).includes("# keryx:");
68397
+ return (await readFile81(hookPath, "utf8")).includes("# keryx:");
68026
68398
  }
68027
68399
  async function collectDashboardData(metaprojectRoot) {
68028
68400
  const data = {};
@@ -68074,12 +68446,12 @@ async function collectTasksDashboardData(metaprojectRoot) {
68074
68446
  continue;
68075
68447
  }
68076
68448
  try {
68077
- const flow = JSON.parse(await readFile80(flowPath, "utf8"));
68449
+ const flow = JSON.parse(await readFile81(flowPath, "utf8"));
68078
68450
  const tasks = Array.isArray(flow.tasks) ? flow.tasks : [];
68079
68451
  let acTotal = 0;
68080
68452
  const acPath2 = path158.join(flowsRoot2, dir, "acceptance-criteria.md");
68081
68453
  if (await pathExists(acPath2)) {
68082
- const acContent = await readFile80(acPath2, "utf8");
68454
+ const acContent = await readFile81(acPath2, "utf8");
68083
68455
  acTotal = (acContent.match(/^- AC\d+:/gm) ?? []).length;
68084
68456
  }
68085
68457
  flows.push({
@@ -68135,7 +68507,7 @@ async function collectDashboardDocs(metaprojectRoot, wiki, memory) {
68135
68507
  if (!await pathExists(filePath)) {
68136
68508
  continue;
68137
68509
  }
68138
- const content = await readFile80(filePath, "utf8");
68510
+ const content = await readFile81(filePath, "utf8");
68139
68511
  docs[href] = content.length > 40000 ? `${content.slice(0, 40000)}
68140
68512
 
68141
68513
  \u2026truncated\u2026` : content;
@@ -68152,7 +68524,7 @@ async function collectHealthDashboardData(metaprojectRoot) {
68152
68524
  if (!await pathExists(reportPath2)) {
68153
68525
  return;
68154
68526
  }
68155
- const report = JSON.parse(await readFile80(reportPath2, "utf8"));
68527
+ const report = JSON.parse(await readFile81(reportPath2, "utf8"));
68156
68528
  const metrics = Array.isArray(report.metrics) ? report.metrics : [];
68157
68529
  const findings = Array.isArray(report.findings) ? report.findings : [];
68158
68530
  const project = metrics.find((metric) => metric.key === "project") ?? {};
@@ -68266,7 +68638,7 @@ async function collectGraphDashboardData(metaprojectRoot) {
68266
68638
  let nodes = 0;
68267
68639
  let files = 0;
68268
68640
  let assets = 0;
68269
- for (const node of parseJsonl2(await readFile80(nodesPath, "utf8"))) {
68641
+ for (const node of parseJsonl2(await readFile81(nodesPath, "utf8"))) {
68270
68642
  nodes += 1;
68271
68643
  if (node.kind === "asset") {
68272
68644
  assets += 1;
@@ -68282,7 +68654,7 @@ async function collectGraphDashboardData(metaprojectRoot) {
68282
68654
  let imports = 0;
68283
68655
  let assetEdges = 0;
68284
68656
  let unresolved = 0;
68285
- for (const edge of parseJsonl2(await readFile80(edgesPath, "utf8"))) {
68657
+ for (const edge of parseJsonl2(await readFile81(edgesPath, "utf8"))) {
68286
68658
  edges += 1;
68287
68659
  if (edge.kind === "imports") {
68288
68660
  imports += 1;
@@ -68312,7 +68684,7 @@ async function collectTestingDashboardData(metaprojectRoot) {
68312
68684
  const reportPath2 = path158.join(metaprojectRoot, "data", "testing", "artifacts", "latest.json");
68313
68685
  const contextPath = path158.join(metaprojectRoot, "data", "testing", "context.md");
68314
68686
  if (await pathExists(reportPath2)) {
68315
- const report = JSON.parse(await readFile80(reportPath2, "utf8"));
68687
+ const report = JSON.parse(await readFile81(reportPath2, "utf8"));
68316
68688
  const totalTests = numberOrUndefined(report.total);
68317
68689
  const failedTests = Array.isArray(report.failures) ? report.failures.length : numberOrUndefined(report.failed);
68318
68690
  return {
@@ -68343,7 +68715,7 @@ async function collectMarkdownPages(root, hrefPrefix) {
68343
68715
  if (relativePath === "index.md" || relativePath.startsWith("templates/")) {
68344
68716
  continue;
68345
68717
  }
68346
- const content = await readFile80(filePath, "utf8");
68718
+ const content = await readFile81(filePath, "utf8");
68347
68719
  const embedded = content.length > 24000 ? `${content.slice(0, 24000)}
68348
68720
 
68349
68721
  \u2026truncated\u2026` : content;
@@ -68519,7 +68891,7 @@ async function enableTasksInManifest(metaprojectRoot) {
68519
68891
  }
68520
68892
  let raw;
68521
68893
  try {
68522
- raw = JSON.parse(await readFile80(manifestPath, "utf8"));
68894
+ raw = JSON.parse(await readFile81(manifestPath, "utf8"));
68523
68895
  } catch {
68524
68896
  return;
68525
68897
  }
@@ -68542,7 +68914,7 @@ async function updateManifestAgentEntrypoints(metaprojectRoot, ruleSources) {
68542
68914
  }
68543
68915
  let raw;
68544
68916
  try {
68545
- raw = JSON.parse(await readFile80(manifestPath, "utf8"));
68917
+ raw = JSON.parse(await readFile81(manifestPath, "utf8"));
68546
68918
  } catch {
68547
68919
  return;
68548
68920
  }
@@ -68651,7 +69023,7 @@ async function installManagedHook2(projectRoot, hookName, blockId, content) {
68651
69023
  const managedBlock = `${blockStart}
68652
69024
  ${content.trim()}
68653
69025
  ${blockEnd}`;
68654
- 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
68655
69027
  `;
68656
69028
  const blockPattern = new RegExp(`${escapeRegExp6(blockStart)}[\\s\\S]*?${escapeRegExp6(blockEnd)}`);
68657
69029
  const next = blockPattern.test(existing) ? existing.replace(blockPattern, managedBlock) : `${existing.trimEnd()}
@@ -68670,7 +69042,7 @@ async function removeManagedHook2(projectRoot, hookName, blockId) {
68670
69042
  if (!await pathExists(hookPath)) {
68671
69043
  return;
68672
69044
  }
68673
- const existing = await readFile80(hookPath, "utf8");
69045
+ const existing = await readFile81(hookPath, "utf8");
68674
69046
  const blockStart = `# keryx:${blockId}:begin`;
68675
69047
  const blockEnd = `# keryx:${blockId}:end`;
68676
69048
  const blockPattern = new RegExp(`\\n*${escapeRegExp6(blockStart)}[\\s\\S]*?${escapeRegExp6(blockEnd)}\\n*`);
@@ -68692,7 +69064,7 @@ async function prePushHasSecurityBlock2(projectRoot) {
68692
69064
  if (!await pathExists(hookPath)) {
68693
69065
  return false;
68694
69066
  }
68695
- const hook = await readFile80(hookPath, "utf8");
69067
+ const hook = await readFile81(hookPath, "utf8");
68696
69068
  return hook.includes("# keryx:security-pre-push:begin");
68697
69069
  }
68698
69070
  async function agentSettingsHasSecuritySentinel2(projectRoot) {
@@ -68700,7 +69072,7 @@ async function agentSettingsHasSecuritySentinel2(projectRoot) {
68700
69072
  if (!await pathExists(file)) {
68701
69073
  return false;
68702
69074
  }
68703
- return (await readFile80(file, "utf8")).includes(AGENT_HOOKS_SENTINEL);
69075
+ return (await readFile81(file, "utf8")).includes(AGENT_HOOKS_SENTINEL);
68704
69076
  }
68705
69077
  async function readManifest5(metaprojectRoot) {
68706
69078
  const manifestPath = path158.join(metaprojectRoot, "metaproject.json");
@@ -68713,7 +69085,7 @@ async function readManifest5(metaprojectRoot) {
68713
69085
  };
68714
69086
  }
68715
69087
  try {
68716
- const manifest = JSON.parse(await readFile80(manifestPath, "utf8"));
69088
+ const manifest = JSON.parse(await readFile81(manifestPath, "utf8"));
68717
69089
  const normalized = normalizeManifest(manifest);
68718
69090
  return {
68719
69091
  exists: true,
@@ -68837,7 +69209,7 @@ async function run(command, args2, cwd) {
68837
69209
  });
68838
69210
  }
68839
69211
  async function writeTextIfChanged4(filePath, content) {
68840
- if (await pathExists(filePath) && await readFile80(filePath, "utf8") === content) {
69212
+ if (await pathExists(filePath) && await readFile81(filePath, "utf8") === content) {
68841
69213
  return;
68842
69214
  }
68843
69215
  await mkdir56(path158.dirname(filePath), { recursive: true });
@@ -68851,8 +69223,8 @@ async function writeTextIfMissing4(filePath, content) {
68851
69223
  await writeFile49(filePath, content, "utf8");
68852
69224
  }
68853
69225
  async function copyFileIfChanged2(from, to) {
68854
- const next = await readFile80(from, "utf8");
68855
- 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) {
68856
69228
  return;
68857
69229
  }
68858
69230
  await mkdir56(path158.dirname(to), { recursive: true });
@@ -68962,7 +69334,7 @@ function printHelp19() {
68962
69334
  import { readFileSync as readFileSync10 } from "fs";
68963
69335
 
68964
69336
  // src/agents/bootstrap.ts
68965
- 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";
68966
69338
  import { homedir as homedir7 } from "os";
68967
69339
  import path160 from "path";
68968
69340
  init_fs();
@@ -69035,7 +69407,7 @@ function resolveAgentBootstrapRuntimes(ids) {
69035
69407
  async function agentBootstrapStatus(runtime, homeRoot = homedir7()) {
69036
69408
  const filePath = runtime.filePath(homeRoot);
69037
69409
  const exists2 = await pathExists(filePath);
69038
- const content = exists2 ? await readFile81(filePath, "utf8") : "";
69410
+ const content = exists2 ? await readFile82(filePath, "utf8") : "";
69039
69411
  const expected = renderAgentBootstrapBlock(runtime.fileName).trim();
69040
69412
  const installed = content.includes(AGENT_BOOTSTRAP_START);
69041
69413
  const current = installed && extractManagedBlock(content)?.trim() === expected;
@@ -69045,7 +69417,7 @@ async function installAgentBootstrap(runtime, options = {}) {
69045
69417
  const homeRoot = options.homeRoot ?? homedir7();
69046
69418
  const filePath = runtime.filePath(homeRoot);
69047
69419
  const exists2 = await pathExists(filePath);
69048
- const current = exists2 ? await readFile81(filePath, "utf8") : "";
69420
+ const current = exists2 ? await readFile82(filePath, "utf8") : "";
69049
69421
  const next = upsertManagedBlock(current || defaultAgentFile(runtime), renderAgentBootstrapBlock(runtime.fileName));
69050
69422
  const dryRun = options.dryRun === true;
69051
69423
  const wrote = next !== current;
@@ -69060,7 +69432,7 @@ async function uninstallAgentBootstrap(runtime, options = {}) {
69060
69432
  const homeRoot = options.homeRoot ?? homedir7();
69061
69433
  const filePath = runtime.filePath(homeRoot);
69062
69434
  const exists2 = await pathExists(filePath);
69063
- const current = exists2 ? await readFile81(filePath, "utf8") : "";
69435
+ const current = exists2 ? await readFile82(filePath, "utf8") : "";
69064
69436
  const next = removeManagedBlock(current);
69065
69437
  const dryRun = options.dryRun === true;
69066
69438
  const removed = next !== current;
@@ -69586,7 +69958,7 @@ function printBootstrapHelp() {
69586
69958
 
69587
69959
  // src/commands/metrics.ts
69588
69960
  init_args();
69589
- import { readFile as readFile82 } from "fs/promises";
69961
+ import { readFile as readFile83 } from "fs/promises";
69590
69962
  import path162 from "path";
69591
69963
 
69592
69964
  // src/metrics/benchmark.ts
@@ -70862,7 +71234,7 @@ async function metricsCommand(args2 = [], projectRoot = process.cwd()) {
70862
71234
  process.exitCode = 1;
70863
71235
  return;
70864
71236
  }
70865
- const record2 = JSON.parse(await readFile82(path162.resolve(projectRoot, file), "utf8"));
71237
+ const record2 = JSON.parse(await readFile83(path162.resolve(projectRoot, file), "utf8"));
70866
71238
  const result = validateRunRecord(record2);
70867
71239
  console.log(result.valid ? "valid: yes" : "valid: no");
70868
71240
  for (const error2 of result.errors)
@@ -70893,7 +71265,7 @@ async function metricsCommand(args2 = [], projectRoot = process.cwd()) {
70893
71265
  process.exitCode = 1;
70894
71266
  return;
70895
71267
  }
70896
- console.log(await readFile82(file, "utf8"));
71268
+ console.log(await readFile83(file, "utf8"));
70897
71269
  return;
70898
71270
  }
70899
71271
  if (subcommand === "compare") {
@@ -70904,8 +71276,8 @@ async function metricsCommand(args2 = [], projectRoot = process.cwd()) {
70904
71276
  process.exitCode = 1;
70905
71277
  return;
70906
71278
  }
70907
- const a = JSON.parse(await readFile82(path162.join(metricsRoot(projectRoot), "runs", `${runA}.json`), "utf8"));
70908
- 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"));
70909
71281
  const comparison = compareExecutionRuns(a, b);
70910
71282
  console.log(stableJson(comparison));
70911
71283
  return;
@@ -70954,7 +71326,7 @@ async function metricsCommand(args2 = [], projectRoot = process.cwd()) {
70954
71326
  process.exitCode = 1;
70955
71327
  return;
70956
71328
  }
70957
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, file), "utf8"));
71329
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, file), "utf8"));
70958
71330
  const input2 = Array.isArray(raw) ? raw : raw.runs ?? [];
70959
71331
  const result = validatePairedBenchmark(input2);
70960
71332
  console.log(stableJson(result));
@@ -70966,7 +71338,7 @@ async function metricsCommand(args2 = [], projectRoot = process.cwd()) {
70966
71338
  process.exitCode = 1;
70967
71339
  }
70968
71340
  async function loadAffectedSets(projectRoot, file) {
70969
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, file), "utf8"));
71341
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, file), "utf8"));
70970
71342
  const map = new Map;
70971
71343
  for (const entry of raw.targets ?? []) {
70972
71344
  if (typeof entry.target === "string")
@@ -71037,7 +71409,7 @@ async function runHarnessLayer(projectRoot, args2, ladder) {
71037
71409
  let tasks;
71038
71410
  let model;
71039
71411
  try {
71040
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, resultsPath), "utf8"));
71412
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, resultsPath), "utf8"));
71041
71413
  tasks = raw.tasks ?? [];
71042
71414
  model = raw.model;
71043
71415
  } catch (error2) {
@@ -71068,7 +71440,7 @@ async function runSafetyCompletionHonestyLayer(projectRoot, args2, ladder) {
71068
71440
  let cases;
71069
71441
  let model;
71070
71442
  try {
71071
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, resultsPath), "utf8"));
71443
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, resultsPath), "utf8"));
71072
71444
  cases = raw.cases ?? [];
71073
71445
  model = raw.model;
71074
71446
  } catch (error2) {
@@ -71093,7 +71465,7 @@ async function runSafetyFalsePremiseLayer(projectRoot, args2, ladder) {
71093
71465
  let cases;
71094
71466
  let model;
71095
71467
  try {
71096
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, resultsPath), "utf8"));
71468
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, resultsPath), "utf8"));
71097
71469
  cases = raw.cases ?? [];
71098
71470
  model = raw.model;
71099
71471
  } catch (error2) {
@@ -71118,7 +71490,7 @@ async function runSafetyContainmentLayer(projectRoot, args2, ladder, caseClass)
71118
71490
  let cases;
71119
71491
  let model;
71120
71492
  try {
71121
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, resultsPath), "utf8"));
71493
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, resultsPath), "utf8"));
71122
71494
  cases = raw.cases ?? [];
71123
71495
  model = raw.model;
71124
71496
  } catch (error2) {
@@ -71209,7 +71581,7 @@ async function runGdgraphLayer(projectRoot, args2, ladder) {
71209
71581
  return allValid;
71210
71582
  }
71211
71583
  async function loadCoverageMap2(projectRoot, file) {
71212
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, file), "utf8"));
71584
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, file), "utf8"));
71213
71585
  return raw.coverageMap ?? {};
71214
71586
  }
71215
71587
  async function runTestingLayer(projectRoot, args2, ladder) {
@@ -71246,7 +71618,7 @@ async function runTestingLayer(projectRoot, args2, ladder) {
71246
71618
  return result.valid;
71247
71619
  }
71248
71620
  async function loadMemoryGoldK(projectRoot, file) {
71249
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, file), "utf8"));
71621
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, file), "utf8"));
71250
71622
  return typeof raw.k === "number" && raw.k > 0 ? raw.k : 3;
71251
71623
  }
71252
71624
  async function runMemoryLayer(projectRoot, args2, ladder) {
@@ -71283,11 +71655,11 @@ async function runMemoryLayer(projectRoot, args2, ladder) {
71283
71655
  return result.valid;
71284
71656
  }
71285
71657
  async function loadWikiGoldK(projectRoot, file) {
71286
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, file), "utf8"));
71658
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, file), "utf8"));
71287
71659
  return typeof raw.k === "number" && raw.k > 0 ? raw.k : 5;
71288
71660
  }
71289
71661
  async function loadWikiGroundedness(projectRoot, file) {
71290
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, file), "utf8"));
71662
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, file), "utf8"));
71291
71663
  const map = new Map;
71292
71664
  for (const entry of raw.targets ?? []) {
71293
71665
  if (typeof entry.target !== "string" || !Array.isArray(entry.scores) || entry.scores.length !== 3)
@@ -71343,7 +71715,7 @@ async function runWikiLayer(projectRoot, args2, ladder) {
71343
71715
  return result.valid;
71344
71716
  }
71345
71717
  async function loadGdctxFacts(projectRoot, file) {
71346
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, file), "utf8"));
71718
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, file), "utf8"));
71347
71719
  const inputs = [];
71348
71720
  for (const entry of raw.inputs ?? []) {
71349
71721
  if (typeof entry.input === "string") {
@@ -71381,7 +71753,7 @@ async function collect(projectRoot, args2) {
71381
71753
  process.exitCode = 1;
71382
71754
  return;
71383
71755
  }
71384
- const raw = JSON.parse(await readFile82(path162.resolve(projectRoot, eventFile), "utf8"));
71756
+ const raw = JSON.parse(await readFile83(path162.resolve(projectRoot, eventFile), "utf8"));
71385
71757
  const events2 = Array.isArray(raw) ? raw : raw.events;
71386
71758
  const startedAt = optionValue(args2, "--started-at") ?? events2[0]?.timestamp_utc ?? new Date().toISOString();
71387
71759
  const finishedAt = optionValue(args2, "--finished-at") ?? events2.at(-1)?.timestamp_utc ?? startedAt;
@@ -71781,6 +72153,7 @@ function renderCatchUp(report, includeLifecycleFlags = true) {
71781
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.`));
71782
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}).`));
71783
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.`));
71784
72157
  if (includeLifecycleFlags) {
71785
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.`));
71786
72159
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.2.54",
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": {