@mrciphersmith/keryx 0.2.111 → 0.2.113

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 (3) hide show
  1. package/dist/cli.js +203 -14
  2. package/dist/core.js +22 -1
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -17108,6 +17108,7 @@ async function collectGraphWikiCandidates(cwd, generatedAt, limit, onlyModules)
17108
17108
  const fileOut = new Map;
17109
17109
  const moduleDeps = new Map;
17110
17110
  const moduleDependents = new Map;
17111
+ const excludedTestEdges = new Map;
17111
17112
  let edges = 0;
17112
17113
  let imports = 0;
17113
17114
  let unresolved = 0;
@@ -17128,6 +17129,11 @@ async function collectGraphWikiCandidates(cwd, generatedAt, limit, onlyModules)
17128
17129
  const fromModule = fileToModule.get(from) ?? moduleNameFromProjectPath(from);
17129
17130
  const toModule = fileToModule.get(to) ?? moduleNameFromProjectPath(to);
17130
17131
  if (fromModule && toModule && fromModule !== toModule) {
17132
+ if (isTestSourceFile(from)) {
17133
+ recordExcludedTestEdge(excludedTestEdges, fromModule, from);
17134
+ recordExcludedTestEdge(excludedTestEdges, toModule, from);
17135
+ continue;
17136
+ }
17131
17137
  bumpNestedCount(moduleDeps, fromModule, toModule);
17132
17138
  bumpNestedCount(moduleDependents, toModule, fromModule);
17133
17139
  }
@@ -17187,6 +17193,12 @@ async function collectGraphWikiCandidates(cwd, generatedAt, limit, onlyModules)
17187
17193
  pushRef("Key files", keyFiles.map((entry) => "- `" + entry.file + "` - imported by " + entry.incoming + ", imports " + entry.outgoing));
17188
17194
  pushRef("Depends on", deps.map(([target, count]) => "- `" + target + "` - " + count + " import(s)"));
17189
17195
  pushRef("Depended on by", dependents.map(([source, count]) => "- `" + source + "` - " + count + " import(s)"));
17196
+ const dropped = excludedTestEdges.get(moduleName);
17197
+ if (dropped) {
17198
+ pushRef("Dependency basis", [
17199
+ "- Production imports only: " + dropped.count + " import(s) from test file(s) (e.g. `" + dropped.example + "`) excluded from the two sections above in both directions."
17200
+ ]);
17201
+ }
17190
17202
  pushRef("Entry points", entryFiles.map((file) => "- `" + file + "`"));
17191
17203
  if (readme) {
17192
17204
  pushRef("Module README", [readme]);
@@ -17229,6 +17241,13 @@ function bumpNestedCount(map, key, inner) {
17229
17241
  bucket.set(inner, (bucket.get(inner) ?? 0) + 1);
17230
17242
  map.set(key, bucket);
17231
17243
  }
17244
+ function isTestSourceFile(filePath) {
17245
+ return TEST_SOURCE_FILE.test(filePath);
17246
+ }
17247
+ function recordExcludedTestEdge(map, moduleName, testFile) {
17248
+ const current = map.get(moduleName);
17249
+ map.set(moduleName, { count: (current?.count ?? 0) + 1, example: current?.example ?? testFile });
17250
+ }
17232
17251
  function sumCounts(bucket) {
17233
17252
  if (!bucket) {
17234
17253
  return 0;
@@ -17832,7 +17851,7 @@ function compareSemver(a, b) {
17832
17851
  }
17833
17852
  return 0;
17834
17853
  }
17835
- var EXTERNAL_LINK, DEFAULT_COLLECT_LIMIT = 500, MIN_MODULE_FILES = 2, DEFAULT_EVIDENCE_BUDGET_TOKENS = 4000, DEFAULT_EVIDENCE_MAX_ITEMS = 8, WIKI_PAGE_PRODUCERS;
17854
+ var EXTERNAL_LINK, DEFAULT_COLLECT_LIMIT = 500, MIN_MODULE_FILES = 2, DEFAULT_EVIDENCE_BUDGET_TOKENS = 4000, DEFAULT_EVIDENCE_MAX_ITEMS = 8, WIKI_PAGE_PRODUCERS, TEST_SOURCE_FILE;
17836
17855
  var init_service3 = __esm(() => {
17837
17856
  init_fs();
17838
17857
  init_guard2();
@@ -17853,6 +17872,7 @@ var init_service3 = __esm(() => {
17853
17872
  path: /^decisions\/sac-[a-z]+-[0-9a-f]+\.md$/
17854
17873
  }
17855
17874
  ];
17875
+ TEST_SOURCE_FILE = /(?:^|\/)[^/]*\.(?:test|spec)\.[cm]?[jt]sx?$/;
17856
17876
  });
17857
17877
 
17858
17878
  // src/gdgraph/wiki-layer.ts
@@ -22072,10 +22092,13 @@ function isPermissionMode(value) {
22072
22092
  return PERMISSION_MODES.includes(value);
22073
22093
  }
22074
22094
  function resolveApprovalDecision(input) {
22075
- const { mode, risk, destructive, credentials, sacReviewConfirmation } = input;
22095
+ const { mode, risk, destructive, credentials, sacReviewConfirmation, readOnly } = input;
22076
22096
  if (risk === "read") {
22077
22097
  return "auto";
22078
22098
  }
22099
+ if (readOnly) {
22100
+ return "deny";
22101
+ }
22079
22102
  if (credentials || sacReviewConfirmation) {
22080
22103
  return "ask";
22081
22104
  }
@@ -27497,7 +27520,7 @@ function buildTaskNotification(completions) {
27497
27520
  ].join(" ");
27498
27521
  const tail = Buffer.byteLength(c.output, "utf8") > NOTIFICATION_OUTPUT_TAIL_BYTES ? c.output.slice(-NOTIFICATION_OUTPUT_TAIL_BYTES) : c.output;
27499
27522
  return `<task-notification ${attrs}>
27500
- ${tail.trimEnd()}
27523
+ ${redactSensitiveText(tail).trimEnd()}
27501
27524
  </task-notification>`;
27502
27525
  });
27503
27526
  return `${TASK_NOTIFICATION_BANNER}
@@ -28191,7 +28214,7 @@ async function runAgentTurnCore(io, deps, history, userLine, options = {}) {
28191
28214
  continue;
28192
28215
  }
28193
28216
  executedAny = true;
28194
- const result = concurrentSpawnResults?.get(call.id) ?? await executeCall(call, toolByName, io.requestApproval, io.permissionMode, io.onAutoApproved, hasInvocationCapacity, reserveInvocation, invocationBudget.maxCalls, signal);
28217
+ const result = concurrentSpawnResults?.get(call.id) ?? await executeCall(call, toolByName, io.requestApproval, io.permissionMode, io.readOnly, io.onAutoApproved, hasInvocationCapacity, reserveInvocation, invocationBudget.maxCalls, signal);
28195
28218
  io.onToolResult?.(call.name, result);
28196
28219
  const modelOutput = redactSensitiveText(result.output);
28197
28220
  const untrusted = result.untrusted === true;
@@ -28411,7 +28434,7 @@ async function runConcurrentSpawnBatch(spawnCalls, toolByName, io, deps, hasInvo
28411
28434
  dependsOn: [],
28412
28435
  budgetRequest: { reservationId: call.id, maxRuntimeMs: perTaskRuntimeMs }
28413
28436
  }));
28414
- const runOne = (call) => executeCall(call, toolByName, io.requestApproval, io.permissionMode, io.onAutoApproved, hasInvocationCapacity, reserveInvocation);
28437
+ const runOne = (call) => executeCall(call, toolByName, io.requestApproval, io.permissionMode, io.readOnly, io.onAutoApproved, hasInvocationCapacity, reserveInvocation);
28415
28438
  const plan = planWaves(tasks, {
28416
28439
  maxConcurrency,
28417
28440
  parentRemaining: { maxRuntimeMs: perTaskRuntimeMs * tasks.length }
@@ -28465,7 +28488,7 @@ async function runConcurrentSpawnBatch(spawnCalls, toolByName, io, deps, hasInvo
28465
28488
  return results;
28466
28489
  }
28467
28490
  }
28468
- async function executeCall(call, toolByName, requestApproval, permissionMode, onAutoApproved, hasInvocationCapacity, reserveInvocation, maxToolCalls, signal) {
28491
+ async function executeCall(call, toolByName, requestApproval, permissionMode, readOnly, onAutoApproved, hasInvocationCapacity, reserveInvocation, maxToolCalls, signal) {
28469
28492
  const tool = toolByName.get(call.name);
28470
28493
  if (tool === undefined) {
28471
28494
  return { output: `unknown tool: ${call.name}`, isError: true };
@@ -28484,12 +28507,23 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, on
28484
28507
  }
28485
28508
  const risk = tool.definition.risk;
28486
28509
  const mode = permissionMode?.() ?? DEFAULT_PERMISSION_MODE;
28510
+ const isReadOnly = readOnly?.() ?? false;
28487
28511
  if (risk === "shell" || risk === "destructive") {
28488
28512
  const command = typeof input.command === "string" ? input.command : "";
28489
28513
  const destructive = risk === "destructive" || isDestructiveCommand(command);
28490
28514
  const credentials = touchesAgentCredentials(command);
28491
28515
  const sacReviewConfirmation = touchesSacConfirmReview(command);
28492
- const decision = resolveApprovalDecision({ mode, risk, destructive, credentials, sacReviewConfirmation });
28516
+ const decision = resolveApprovalDecision({
28517
+ mode,
28518
+ risk,
28519
+ destructive,
28520
+ credentials,
28521
+ sacReviewConfirmation,
28522
+ readOnly: isReadOnly
28523
+ });
28524
+ if (decision === "deny") {
28525
+ return { output: `tool "${call.name}" is not permitted while read-only mode (/plan) is on`, isError: true };
28526
+ }
28493
28527
  if (decision === "auto") {
28494
28528
  onAutoApproved?.(call.name, call.input, { destructive, credentials });
28495
28529
  } else {
@@ -28504,7 +28538,17 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, on
28504
28538
  }
28505
28539
  }
28506
28540
  } else if (risk === "delegate") {
28507
- const decision = resolveApprovalDecision({ mode, risk, destructive: false, credentials: false, sacReviewConfirmation: false });
28541
+ const decision = resolveApprovalDecision({
28542
+ mode,
28543
+ risk,
28544
+ destructive: false,
28545
+ credentials: false,
28546
+ sacReviewConfirmation: false,
28547
+ readOnly: isReadOnly
28548
+ });
28549
+ if (decision === "deny") {
28550
+ return { output: `tool "${call.name}" is not permitted while read-only mode (/plan) is on`, isError: true };
28551
+ }
28508
28552
  if (decision === "auto") {
28509
28553
  onAutoApproved?.(call.name, call.input, { destructive: false, credentials: false });
28510
28554
  } else {
@@ -28517,7 +28561,17 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, on
28517
28561
  } else if (risk === "write") {
28518
28562
  const patch = typeof input.patch === "string" ? input.patch : "";
28519
28563
  const { destructive, credentials } = classifyPatchRisk(patch);
28520
- const decision = resolveApprovalDecision({ mode, risk, destructive, credentials, sacReviewConfirmation: false });
28564
+ const decision = resolveApprovalDecision({
28565
+ mode,
28566
+ risk,
28567
+ destructive,
28568
+ credentials,
28569
+ sacReviewConfirmation: false,
28570
+ readOnly: isReadOnly
28571
+ });
28572
+ if (decision === "deny") {
28573
+ return { output: `tool "${call.name}" is not permitted while read-only mode (/plan) is on`, isError: true };
28574
+ }
28521
28575
  if (decision === "auto") {
28522
28576
  onAutoApproved?.(call.name, call.input, { destructive, credentials });
28523
28577
  } else {
@@ -28937,7 +28991,7 @@ var init_quarantine = __esm(() => {
28937
28991
  },
28938
28992
  {
28939
28993
  name: "permission-config",
28940
- test: (t) => /\b(allowed[- ]?tools|disallowed[- ]?tools|permission[- ]?mode|bypass[- ]?permissions|dangerously[- ]?skip)\b/i.test(t)
28994
+ test: (t) => /\b(allowed[- ]?tools|disallowed[- ]?tools|permission[- ]?mode|bypass[- ]?permissions|dangerously[- ]?skip|readonly|read[- ]only[- ]?mode)\b|\/plan\b/i.test(t)
28941
28995
  }
28942
28996
  ];
28943
28997
  });
@@ -48777,7 +48831,7 @@ import path47 from "path";
48777
48831
  // package.json
48778
48832
  var package_default = {
48779
48833
  name: "@mrciphersmith/keryx",
48780
- version: "0.2.111",
48834
+ version: "0.2.113",
48781
48835
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
48782
48836
  private: false,
48783
48837
  publishConfig: {
@@ -88889,6 +88943,8 @@ function classifyBusyDispatch(params) {
88889
88943
  return "copy";
88890
88944
  if (commandName === "/mode")
88891
88945
  return "mode";
88946
+ if (commandName === "/plan")
88947
+ return "plan";
88892
88948
  if (commandName === "/demote")
88893
88949
  return "demote";
88894
88950
  if (commandName === "/game")
@@ -88911,6 +88967,67 @@ function classifyBusyDispatch(params) {
88911
88967
  return "not-a-command";
88912
88968
  }
88913
88969
 
88970
+ // src/tui/boot-animation.ts
88971
+ var DEFAULT_BOOT_DURATION_MS = 350;
88972
+ var WORDMARK = ["========================", " K E R Y X ", "========================"];
88973
+ var STEPS = ["Reading .metaproject index", "Connecting provider", "Warming graph"];
88974
+ async function playBootAnimation(otui, r, opts) {
88975
+ if (process.env.KERYX_SKIP_BOOT === "1" || opts.skip === true) {
88976
+ return;
88977
+ }
88978
+ const durationMs = opts.durationMs ?? DEFAULT_BOOT_DURATION_MS;
88979
+ const theme = getTheme();
88980
+ const box = new otui.BoxRenderable(r, {
88981
+ id: "boot-animation",
88982
+ position: "absolute",
88983
+ top: 0,
88984
+ left: 0,
88985
+ width: "100%",
88986
+ height: "100%",
88987
+ backgroundColor: theme.bg,
88988
+ flexDirection: "column",
88989
+ alignItems: "center",
88990
+ justifyContent: "center"
88991
+ });
88992
+ r.root.add(box);
88993
+ const logoBox = new otui.BoxRenderable(r, { id: "boot-logo-box", flexDirection: "column", alignItems: "center" });
88994
+ box.add(logoBox);
88995
+ for (const [index, line] of WORDMARK.entries()) {
88996
+ logoBox.add(new otui.TextRenderable(r, { id: `boot-logo-${index}`, content: otui.t`${otui.bold(line)}` }));
88997
+ }
88998
+ const statusText = new otui.TextRenderable(r, {
88999
+ id: "boot-status",
89000
+ content: otui.t`${otui.dim(STEPS[0] ?? "")}`,
89001
+ marginTop: 1
89002
+ });
89003
+ box.add(statusText);
89004
+ box.add(new otui.TextRenderable(r, { id: "boot-hint", content: otui.t`${otui.dim("any key to skip")}`, marginTop: 1 }));
89005
+ await new Promise((resolve) => {
89006
+ let settled = false;
89007
+ let stepIndex = 0;
89008
+ const stepTimer = setInterval(() => {
89009
+ stepIndex = Math.min(stepIndex + 1, STEPS.length - 1);
89010
+ const step = STEPS[stepIndex];
89011
+ if (step !== undefined) {
89012
+ statusText.content = otui.t`${otui.dim(step)}`;
89013
+ }
89014
+ }, Math.max(1, Math.floor(durationMs / STEPS.length)));
89015
+ const finish = () => {
89016
+ if (settled) {
89017
+ return;
89018
+ }
89019
+ settled = true;
89020
+ clearInterval(stepTimer);
89021
+ clearTimeout(doneTimer);
89022
+ unsubscribe();
89023
+ r.root.remove(box);
89024
+ resolve();
89025
+ };
89026
+ const doneTimer = setTimeout(finish, durationMs);
89027
+ const unsubscribe = opts.onKeypress(() => finish());
89028
+ });
89029
+ }
89030
+
88914
89031
  // src/tui/workspace-inspector.ts
88915
89032
  var WORKSPACE_COMMAND = "/workspace";
88916
89033
  var WORKSPACE_FOOTER = [
@@ -91033,6 +91150,11 @@ var AGENT_SLASH_COMMANDS = [
91033
91150
  description: "Show or switch the permission mode \u2014 /mode [ask|trust|auto]",
91034
91151
  modes: AGENT_ONLY
91035
91152
  },
91153
+ {
91154
+ name: "/plan",
91155
+ description: "Toggle read-only mode \u2014 /plan [on|off]",
91156
+ modes: AGENT_ONLY
91157
+ },
91036
91158
  { name: "/clear", description: "New session (alias of /new)", modes: BOTH },
91037
91159
  { name: "/interrupt", description: "Interrupt the running main agent turn", modes: AGENT_ONLY },
91038
91160
  {
@@ -93115,8 +93237,15 @@ async function createShellChrome(otui, renderer, opts) {
93115
93237
  paddingTop: 1
93116
93238
  });
93117
93239
  rootRow.add(sidebar);
93118
- const sidebarTop = new otui.BoxRenderable(r, { id: "sb-top", flexShrink: 0, flexDirection: "column" });
93119
- sidebar.add(sidebarTop);
93240
+ const sidebarTopScroll = new otui.ScrollBoxRenderable(r, {
93241
+ id: "sb-top",
93242
+ flexShrink: 1,
93243
+ minHeight: 0,
93244
+ scrollY: true,
93245
+ contentOptions: { flexDirection: "column" }
93246
+ });
93247
+ sidebar.add(sidebarTopScroll);
93248
+ const sidebarTop = sidebarTopScroll.content;
93120
93249
  if (opts.versionCheck !== undefined) {
93121
93250
  const versionNotice = new otui.TextRenderable(r, {
93122
93251
  id: `sb-version-${uid++}`,
@@ -93620,6 +93749,7 @@ async function createShellChrome(otui, renderer, opts) {
93620
93749
  main,
93621
93750
  sidebar,
93622
93751
  sidebarTop,
93752
+ sidebarScroll: sidebarTopScroll,
93623
93753
  header,
93624
93754
  scroll,
93625
93755
  transcript,
@@ -96048,6 +96178,12 @@ function attachBlockIo(io, addBlock, chrome = {}) {
96048
96178
  };
96049
96179
  return io;
96050
96180
  }
96181
+ function mountTitlePanel(otui, r, sidebarTop) {
96182
+ sidebarTop.add(new otui.TextRenderable(r, {
96183
+ id: "sb-title",
96184
+ content: otui.t`${otui.bold("keryx")} ${otui.dim(`v${package_default.version}`)}`
96185
+ }));
96186
+ }
96051
96187
  function mountCwdPanel(otui, r, sidebarTop, cwd, metadata = resolveSidebarMetadata(cwd)) {
96052
96188
  sidebarTop.add(new otui.TextRenderable(r, { id: "sb-cwd-k", content: otui.t`${otui.dim("Directory")}`, marginTop: 1 }));
96053
96189
  sidebarTop.add(new otui.TextRenderable(r, {
@@ -97117,6 +97253,7 @@ async function launchTuiAgentShell(opts) {
97117
97253
  }
97118
97254
  };
97119
97255
  r.on("theme_mode", onThemeMode);
97256
+ await playBootAnimation(otui, r, { onKeypress: (handler) => onKeypress4(r, handler) });
97120
97257
  const sel = opts.initial ?? await selectProviderModelInTui(otui, r, opts.detected);
97121
97258
  if (sel === undefined) {
97122
97259
  r.off("theme_mode", onThemeMode);
@@ -97159,7 +97296,7 @@ async function launchTuiAgentShell(opts) {
97159
97296
  chrome.stopBusy();
97160
97297
  };
97161
97298
  const sidebar = chrome.sidebarTop;
97162
- sidebar.add(new otui.TextRenderable(r, { id: "sb-title", content: otui.t`${otui.bold("keryx")}` }));
97299
+ mountTitlePanel(otui, r, sidebar);
97163
97300
  sidebar.add(new otui.TextRenderable(r, { id: "sb-model-k", content: otui.t`${otui.dim("Model")}`, marginTop: 1 }));
97164
97301
  const sbModelV = new otui.TextRenderable(r, { id: "sb-model-v", content: otui.t`${otui.dim(`${sel.provider}/${sel.model}`)}` });
97165
97302
  sidebar.add(sbModelV);
@@ -97763,6 +97900,8 @@ async function launchTuiAgentShell(opts) {
97763
97900
  const sessionCwd = opts.session?.cwd ?? process.cwd();
97764
97901
  let permissionMode = opts.initialPermissionMode ?? getProjectPermissionMode(sessionCwd) ?? DEFAULT_PERMISSION_MODE;
97765
97902
  io.permissionMode = () => permissionMode;
97903
+ let readOnly = false;
97904
+ io.readOnly = () => readOnly;
97766
97905
  io.onAutoApproved = (tool, input, meta) => {
97767
97906
  const preview = tool === "shell_exec" ? parseShellExecCommand(input) : tool;
97768
97907
  const label = meta.destructive ? `\u25C7 auto-approved (${permissionMode}) [destructive]` : `\u25C7 auto-approved (${permissionMode})`;
@@ -98237,6 +98376,26 @@ Staying in the current session.
98237
98376
  }
98238
98377
  })();
98239
98378
  };
98379
+ const runPlanCommand = (line) => {
98380
+ const planArgs = line.trim().split(/\s+/).slice(1).filter((p) => p.length > 0);
98381
+ const wanted = planArgs[0] ?? "";
98382
+ if (wanted.length === 0) {
98383
+ chrome.showToast(`Read-only mode: ${readOnly ? "on" : "off"}`);
98384
+ return;
98385
+ }
98386
+ if (wanted === "on") {
98387
+ readOnly = true;
98388
+ chrome.showToast("Read-only mode: on");
98389
+ return;
98390
+ }
98391
+ if (wanted === "off") {
98392
+ readOnly = false;
98393
+ chrome.showToast(`Read-only mode: off (permission mode stays: ${permissionMode})`);
98394
+ return;
98395
+ }
98396
+ io.onSystem?.(`Usage: /plan [on|off]
98397
+ `);
98398
+ };
98240
98399
  const updateModelLabels = () => {
98241
98400
  paintSessionHeader();
98242
98401
  const label = `${currentSel.provider}/${currentSel.model}`;
@@ -98730,6 +98889,10 @@ Staying in the current session.
98730
98889
  runModeCommand(line);
98731
98890
  return;
98732
98891
  }
98892
+ case "plan": {
98893
+ runPlanCommand(line);
98894
+ return;
98895
+ }
98733
98896
  case "session-info": {
98734
98897
  showSessionInfo();
98735
98898
  return;
@@ -99044,6 +99207,10 @@ ${formatThemeList(getThemeId())}`);
99044
99207
  runModeCommand(line);
99045
99208
  return;
99046
99209
  }
99210
+ if (command.name === "/plan") {
99211
+ runPlanCommand(line);
99212
+ return;
99213
+ }
99047
99214
  if (command.name === "/model") {
99048
99215
  (async () => {
99049
99216
  const detected = opts.redetect !== undefined ? await opts.redetect() : opts.detected;
@@ -99806,6 +99973,7 @@ var READLINE_AGENT_COMMANDS = [
99806
99973
  "/flows",
99807
99974
  "/theme",
99808
99975
  "/mode",
99976
+ "/plan",
99809
99977
  "/exit"
99810
99978
  ];
99811
99979
  function readlineAgentHelpText() {
@@ -100669,6 +100837,8 @@ ${text.trim()}`;
100669
100837
  const sessionsOn = sessionOpts !== undefined && sessionOpts.enabled !== false;
100670
100838
  let permissionMode = initialPermissionMode ?? getProjectPermissionMode(sessionCwd) ?? DEFAULT_PERMISSION_MODE;
100671
100839
  agentIo.permissionMode = () => permissionMode;
100840
+ let readOnly = false;
100841
+ agentIo.readOnly = () => readOnly;
100672
100842
  agentIo.onAutoApproved = (tool, input, meta) => {
100673
100843
  stopSpinner();
100674
100844
  const preview = tool === "shell_exec" ? parseShellExecCommand(input) : tool;
@@ -100952,6 +101122,25 @@ ${GUTTER}${turnSeparator()}
100952
101122
  }
100953
101123
  }
100954
101124
  }
101125
+ } else if (command === "/plan") {
101126
+ const planArgs = rest.split(/\s+/).filter((p) => p.length > 0);
101127
+ const wanted = planArgs[0] ?? "";
101128
+ if (wanted.length === 0) {
101129
+ agentIo.onSystem?.(`Read-only mode: ${readOnly ? "on" : "off"}
101130
+ ` + `Usage: /plan [on|off]
101131
+ `);
101132
+ } else if (wanted === "on") {
101133
+ readOnly = true;
101134
+ agentIo.onSystem?.(`Read-only mode: on
101135
+ `);
101136
+ } else if (wanted === "off") {
101137
+ readOnly = false;
101138
+ agentIo.onSystem?.(`Read-only mode: off (permission mode stays: ${permissionMode})
101139
+ `);
101140
+ } else {
101141
+ agentIo.onSystem?.(`Usage: /plan [on|off]
101142
+ `);
101143
+ }
100955
101144
  } else if (command === "/search-provider") {
100956
101145
  const args = parseSearchProviderArgs2(parts.slice(1));
100957
101146
  const all = searchProviderController.configurable();
package/dist/core.js CHANGED
@@ -9099,6 +9099,7 @@ __export(exports_service3, {
9099
9099
  collectGraphWikiCandidates: () => collectGraphWikiCandidates,
9100
9100
  createGdWikiService: () => createGdWikiService,
9101
9101
  extractModuleApi: () => extractModuleApi,
9102
+ isTestSourceFile: () => isTestSourceFile,
9102
9103
  moduleNameFromProjectPath: () => moduleNameFromProjectPath,
9103
9104
  validModuleNames: () => validModuleNames,
9104
9105
  wikiCheckLinks: () => wikiCheckLinks,
@@ -9472,6 +9473,7 @@ async function collectGraphWikiCandidates(cwd, generatedAt, limit, onlyModules)
9472
9473
  const fileOut = new Map;
9473
9474
  const moduleDeps = new Map;
9474
9475
  const moduleDependents = new Map;
9476
+ const excludedTestEdges = new Map;
9475
9477
  let edges = 0;
9476
9478
  let imports = 0;
9477
9479
  let unresolved = 0;
@@ -9492,6 +9494,11 @@ async function collectGraphWikiCandidates(cwd, generatedAt, limit, onlyModules)
9492
9494
  const fromModule = fileToModule.get(from) ?? moduleNameFromProjectPath(from);
9493
9495
  const toModule = fileToModule.get(to) ?? moduleNameFromProjectPath(to);
9494
9496
  if (fromModule && toModule && fromModule !== toModule) {
9497
+ if (isTestSourceFile(from)) {
9498
+ recordExcludedTestEdge(excludedTestEdges, fromModule, from);
9499
+ recordExcludedTestEdge(excludedTestEdges, toModule, from);
9500
+ continue;
9501
+ }
9495
9502
  bumpNestedCount(moduleDeps, fromModule, toModule);
9496
9503
  bumpNestedCount(moduleDependents, toModule, fromModule);
9497
9504
  }
@@ -9551,6 +9558,12 @@ async function collectGraphWikiCandidates(cwd, generatedAt, limit, onlyModules)
9551
9558
  pushRef("Key files", keyFiles.map((entry) => "- `" + entry.file + "` - imported by " + entry.incoming + ", imports " + entry.outgoing));
9552
9559
  pushRef("Depends on", deps.map(([target, count]) => "- `" + target + "` - " + count + " import(s)"));
9553
9560
  pushRef("Depended on by", dependents.map(([source, count]) => "- `" + source + "` - " + count + " import(s)"));
9561
+ const dropped = excludedTestEdges.get(moduleName);
9562
+ if (dropped) {
9563
+ pushRef("Dependency basis", [
9564
+ "- Production imports only: " + dropped.count + " import(s) from test file(s) (e.g. `" + dropped.example + "`) excluded from the two sections above in both directions."
9565
+ ]);
9566
+ }
9554
9567
  pushRef("Entry points", entryFiles.map((file) => "- `" + file + "`"));
9555
9568
  if (readme) {
9556
9569
  pushRef("Module README", [readme]);
@@ -9593,6 +9606,13 @@ function bumpNestedCount(map, key, inner) {
9593
9606
  bucket.set(inner, (bucket.get(inner) ?? 0) + 1);
9594
9607
  map.set(key, bucket);
9595
9608
  }
9609
+ function isTestSourceFile(filePath) {
9610
+ return TEST_SOURCE_FILE.test(filePath);
9611
+ }
9612
+ function recordExcludedTestEdge(map, moduleName, testFile) {
9613
+ const current = map.get(moduleName);
9614
+ map.set(moduleName, { count: (current?.count ?? 0) + 1, example: current?.example ?? testFile });
9615
+ }
9596
9616
  function sumCounts(bucket) {
9597
9617
  if (!bucket) {
9598
9618
  return 0;
@@ -10196,7 +10216,7 @@ function compareSemver(a, b) {
10196
10216
  }
10197
10217
  return 0;
10198
10218
  }
10199
- var EXTERNAL_LINK, DEFAULT_COLLECT_LIMIT = 500, MIN_MODULE_FILES = 2, DEFAULT_EVIDENCE_BUDGET_TOKENS = 4000, DEFAULT_EVIDENCE_MAX_ITEMS = 8, WIKI_PAGE_PRODUCERS;
10219
+ var EXTERNAL_LINK, DEFAULT_COLLECT_LIMIT = 500, MIN_MODULE_FILES = 2, DEFAULT_EVIDENCE_BUDGET_TOKENS = 4000, DEFAULT_EVIDENCE_MAX_ITEMS = 8, WIKI_PAGE_PRODUCERS, TEST_SOURCE_FILE;
10200
10220
  var init_service2 = __esm(() => {
10201
10221
  init_fs();
10202
10222
  init_guard();
@@ -10217,6 +10237,7 @@ var init_service2 = __esm(() => {
10217
10237
  path: /^decisions\/sac-[a-z]+-[0-9a-f]+\.md$/
10218
10238
  }
10219
10239
  ];
10240
+ TEST_SOURCE_FILE = /(?:^|\/)[^/]*\.(?:test|spec)\.[cm]?[jt]sx?$/;
10220
10241
  });
10221
10242
 
10222
10243
  // src/gdgraph/wiki-layer.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.2.111",
3
+ "version": "0.2.113",
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": {