@mrciphersmith/keryx 0.2.109 → 0.2.110

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 +300 -14
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -28143,7 +28143,7 @@ async function runAgentTurnCore(io, deps, history, userLine, options = {}) {
28143
28143
  continue;
28144
28144
  }
28145
28145
  executedAny = true;
28146
- const result = concurrentSpawnResults?.get(call.id) ?? await executeCall(call, toolByName, io.requestApproval, io.permissionMode, io.onAutoApproved, hasInvocationCapacity, reserveInvocation, invocationBudget.maxCalls);
28146
+ const result = concurrentSpawnResults?.get(call.id) ?? await executeCall(call, toolByName, io.requestApproval, io.permissionMode, io.onAutoApproved, hasInvocationCapacity, reserveInvocation, invocationBudget.maxCalls, signal);
28147
28147
  io.onToolResult?.(call.name, result);
28148
28148
  const modelOutput = redactSensitiveText(result.output);
28149
28149
  const untrusted = result.untrusted === true;
@@ -28417,7 +28417,7 @@ async function runConcurrentSpawnBatch(spawnCalls, toolByName, io, deps, hasInvo
28417
28417
  return results;
28418
28418
  }
28419
28419
  }
28420
- async function executeCall(call, toolByName, requestApproval, permissionMode, onAutoApproved, hasInvocationCapacity, reserveInvocation, maxToolCalls) {
28420
+ async function executeCall(call, toolByName, requestApproval, permissionMode, onAutoApproved, hasInvocationCapacity, reserveInvocation, maxToolCalls, signal) {
28421
28421
  const tool = toolByName.get(call.name);
28422
28422
  if (tool === undefined) {
28423
28423
  return { output: `unknown tool: ${call.name}`, isError: true };
@@ -28489,7 +28489,7 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, on
28489
28489
  if (!reserveInvocation()) {
28490
28490
  return toolCallBudgetResult(maxToolCalls ?? 0, maxToolCalls ?? 0);
28491
28491
  }
28492
- return tool.invoke(input);
28492
+ return tool.invoke(input, { ...signal !== undefined ? { signal } : {} });
28493
28493
  }
28494
28494
  function validateDirectBudget(name, value, min) {
28495
28495
  if (value === undefined)
@@ -28519,7 +28519,11 @@ var init_agent = __esm(() => {
28519
28519
  init_slate_lifecycle();
28520
28520
  init_slate_terminal_state();
28521
28521
  DURABLE_READ_TOOL_NAMES = new Set(["workspace_create", "workspace_propose", "slate_write_seed"]);
28522
- REPEATABLE_TOOL_NAMES = new Set(["shell_job_output"]);
28522
+ REPEATABLE_TOOL_NAMES = new Set([
28523
+ "shell_job_output",
28524
+ "shell_task_output",
28525
+ "shell_task_wait"
28526
+ ]);
28523
28527
  INSTRUCTION_METAPROJECT_TOOLS = [
28524
28528
  "search_code",
28525
28529
  "graph_affected",
@@ -48683,7 +48687,7 @@ import path47 from "path";
48683
48687
  // package.json
48684
48688
  var package_default = {
48685
48689
  name: "@mrciphersmith/keryx",
48686
- version: "0.2.109",
48690
+ version: "0.2.110",
48687
48691
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
48688
48692
  private: false,
48689
48693
  publishConfig: {
@@ -80719,6 +80723,7 @@ function createJobRegistry(options) {
80719
80723
  const dropped = job.outputBuffer.length - TERMINATED_OUTPUT_TAIL_BYTES;
80720
80724
  job.outputBuffer = job.outputBuffer.slice(dropped);
80721
80725
  job.readCursor = Math.max(0, job.readCursor - dropped);
80726
+ job.droppedBytes += dropped;
80722
80727
  }
80723
80728
  function appendOutput(job, chunk, stream) {
80724
80729
  job.outputBuffer += chunk;
@@ -80732,6 +80737,7 @@ function createJobRegistry(options) {
80732
80737
  const dropped = job.outputBuffer.length - MAX_BACKGROUND_OUTPUT_BYTES;
80733
80738
  job.outputBuffer = job.outputBuffer.slice(dropped);
80734
80739
  job.readCursor = Math.max(0, job.readCursor - dropped);
80740
+ job.droppedBytes += dropped;
80735
80741
  requestKill(job, "output-cap");
80736
80742
  }
80737
80743
  }
@@ -80797,6 +80803,7 @@ function createJobRegistry(options) {
80797
80803
  outputBuffer: "",
80798
80804
  outputHead: "",
80799
80805
  readCursor: 0,
80806
+ droppedBytes: 0,
80800
80807
  exited: false,
80801
80808
  killRequested: false,
80802
80809
  phaseEmitted: false,
@@ -80874,6 +80881,27 @@ function createJobRegistry(options) {
80874
80881
  }
80875
80882
  return { ok: true, output };
80876
80883
  },
80884
+ readOutputSince(jobId, since) {
80885
+ const job = jobs.get(jobId);
80886
+ if (job === undefined) {
80887
+ return { ok: false, error: `unknown job_id: ${jobId}` };
80888
+ }
80889
+ const requested = Number.isFinite(since) && since > 0 ? Math.trunc(since) : 0;
80890
+ const missed = Math.max(0, job.droppedBytes - requested);
80891
+ const index = Math.min(job.outputBuffer.length, Math.max(0, requested - job.droppedBytes));
80892
+ return {
80893
+ ok: true,
80894
+ output: job.outputBuffer.slice(index),
80895
+ nextCursor: job.droppedBytes + job.outputBuffer.length,
80896
+ missed,
80897
+ status: job.info.status
80898
+ };
80899
+ },
80900
+ markObserved(jobId) {
80901
+ const job = jobs.get(jobId);
80902
+ if (job !== undefined)
80903
+ job.info.observed = true;
80904
+ },
80877
80905
  async waitForExit(jobId, ms) {
80878
80906
  const job = jobs.get(jobId);
80879
80907
  if (job === undefined)
@@ -80959,7 +80987,7 @@ function shellJobOutputTool(registry) {
80959
80987
  return {
80960
80988
  definition: {
80961
80989
  name: "shell_job_output",
80962
- description: "Return output produced by a shell task SINCE the previous call for that id \u2014 never the full transcript " + "again. Input: { job_id: string } \u2014 pass the task_id shell_exec returned for a command that outlived its " + "yield (or a background:true job). Poll this instead of re-running shell_exec to check on a long command.",
80990
+ description: "DEPRECATED \u2014 use shell_task_output, which takes an explicit cursor; this name is kept for one release. " + "Return output produced by a shell task SINCE the previous call for that id \u2014 never the full transcript " + "again. Input: { job_id: string } \u2014 pass the task_id shell_exec returned for a command that outlived its " + "yield (or a background:true job). Poll this instead of re-running shell_exec to check on a long command.",
80963
80991
  inputSchema: {
80964
80992
  type: "object",
80965
80993
  properties: { job_id: { type: "string" } },
@@ -80973,7 +81001,7 @@ function shellJobOutputTool(registry) {
80973
81001
  if (jobId.length === 0) {
80974
81002
  return { output: "shell_job_output requires a non-empty 'job_id'", isError: true };
80975
81003
  }
80976
- const result = registry.readOutput(jobId);
81004
+ const result = registry.readOutput(resolveTaskId(registry, jobId));
80977
81005
  if (!result.ok) {
80978
81006
  return { output: result.error, isError: true };
80979
81007
  }
@@ -80981,11 +81009,175 @@ function shellJobOutputTool(registry) {
80981
81009
  }
80982
81010
  };
80983
81011
  }
81012
+ function shellTaskOutputTool(registry, options) {
81013
+ return {
81014
+ definition: {
81015
+ name: "shell_task_output",
81016
+ description: "Read a shell task's output from an EXPLICIT cursor. Input: { task_id: string, since?: integer } \u2014 " + "omit `since` (or pass 0) for everything retained, then pass back the next_cursor from the previous " + "result to get only what is new. Two calls with the same `since` return the same bytes, so this is safe " + "to call again after a failure. The result states the task's status and its next_cursor, and says so " + "when older output was dropped before your cursor.",
81017
+ inputSchema: {
81018
+ type: "object",
81019
+ properties: {
81020
+ task_id: { type: "string" },
81021
+ since: { type: "integer" }
81022
+ },
81023
+ required: ["task_id"],
81024
+ additionalProperties: false
81025
+ },
81026
+ risk: "read"
81027
+ },
81028
+ invoke: async (input) => {
81029
+ const taskId = typeof input.task_id === "string" ? input.task_id : "";
81030
+ if (taskId.length === 0) {
81031
+ return { output: "shell_task_output requires a non-empty 'task_id'", isError: true };
81032
+ }
81033
+ const since = typeof input.since === "number" ? input.since : 0;
81034
+ const result = registry.readOutputSince(taskId, since);
81035
+ if (!result.ok) {
81036
+ return { output: result.error, isError: true };
81037
+ }
81038
+ if (options.observer === "main" && result.status !== "running") {
81039
+ registry.markObserved(taskId);
81040
+ }
81041
+ const missed = result.missed > 0 ? `
81042
+ [${result.missed} byte(s) older than your cursor were dropped and cannot be re-read]` : "";
81043
+ const trailer = `
81044
+ [task ${taskId} status=${result.status} next_cursor=${result.nextCursor}]`;
81045
+ return { output: `${result.output}${missed}${trailer}`, isError: false };
81046
+ }
81047
+ };
81048
+ }
81049
+ function demoteTask(registry, taskId) {
81050
+ const resolved = taskId;
81051
+ const info = registry.get(resolved);
81052
+ if (info === undefined) {
81053
+ return { ok: false, error: `unknown task_id: ${taskId}` };
81054
+ }
81055
+ if (info.status !== "running") {
81056
+ return { ok: false, error: `task ${resolved} is not running (status: ${info.status})` };
81057
+ }
81058
+ if (info.phase === "background") {
81059
+ return { ok: false, error: `task ${resolved} is already running in the background` };
81060
+ }
81061
+ return registry.promote(resolved);
81062
+ }
81063
+ var MAX_TASK_WAIT_MS = 300000;
81064
+ function clampTaskWaitMs(ms) {
81065
+ if (!Number.isFinite(ms))
81066
+ return MAX_TASK_WAIT_MS;
81067
+ if (ms <= 0)
81068
+ return 0;
81069
+ return Math.min(MAX_TASK_WAIT_MS, Math.trunc(ms));
81070
+ }
81071
+ function describeWaitedTask(registry, taskId) {
81072
+ const info = registry.get(taskId);
81073
+ if (info === undefined)
81074
+ return `${taskId}: unknown task_id (not tracked by this session)`;
81075
+ const exit = info.exitCode !== undefined ? ` exit=${info.exitCode}` : "";
81076
+ const reason = info.killReason !== undefined ? ` killReason=${info.killReason}` : "";
81077
+ return `${taskId}: ${info.status}${exit}${reason}`;
81078
+ }
81079
+ function shellTaskWaitTool(registry) {
81080
+ return {
81081
+ definition: {
81082
+ name: "shell_task_wait",
81083
+ description: 'Wait for shell tasks to finish instead of polling. Input: { task_ids: string[], mode: "any" | "all", ' + "timeout_ms?: integer } \u2014 `any` returns as soon as one of them ends, `all` when every one has. The wait is " + "bounded (timeout_ms is clamped to at most 300000) and NEVER kills: if the bound is reached you get each " + "task's current status and the tasks keep running. Read their output with shell_task_output(task_id).",
81084
+ inputSchema: {
81085
+ type: "object",
81086
+ properties: {
81087
+ task_ids: { type: "array", items: { type: "string" } },
81088
+ mode: { type: "string", enum: ["any", "all"] },
81089
+ timeout_ms: { type: "integer" }
81090
+ },
81091
+ required: ["task_ids", "mode"],
81092
+ additionalProperties: false
81093
+ },
81094
+ risk: "read"
81095
+ },
81096
+ invoke: async (input, ctx) => {
81097
+ const rawIds = Array.isArray(input.task_ids) ? input.task_ids : [];
81098
+ const taskIds = rawIds.filter((id) => typeof id === "string" && id.length > 0);
81099
+ if (taskIds.length === 0) {
81100
+ return { output: "shell_task_wait requires a non-empty 'task_ids'", isError: true };
81101
+ }
81102
+ const mode = input.mode === "any" ? "any" : "all";
81103
+ const timeoutMs = clampTaskWaitMs(typeof input.timeout_ms === "number" ? input.timeout_ms : MAX_TASK_WAIT_MS);
81104
+ const known = taskIds.filter((id) => registry.get(id) !== undefined);
81105
+ const signal = ctx?.signal;
81106
+ let onAbort;
81107
+ const aborted = new Promise((resolve) => {
81108
+ if (signal === undefined)
81109
+ return;
81110
+ if (signal.aborted) {
81111
+ resolve("aborted");
81112
+ return;
81113
+ }
81114
+ onAbort = () => resolve("aborted");
81115
+ signal.addEventListener("abort", onAbort, { once: true });
81116
+ });
81117
+ try {
81118
+ if (known.length > 0) {
81119
+ const waits = known.map((id) => registry.waitForExit(id, timeoutMs).then(() => id));
81120
+ const settled = mode === "any" ? await Promise.race([...waits, aborted]) : await Promise.race([Promise.all(waits).then(() => "all"), aborted]);
81121
+ if (settled === "aborted") {
81122
+ const lines = taskIds.map((id) => describeWaitedTask(registry, id));
81123
+ return {
81124
+ output: `interrupted: the wait was stopped and every task is STILL RUNNING \u2014 none was killed.
81125
+ ` + lines.join(`
81126
+ `),
81127
+ isError: false
81128
+ };
81129
+ }
81130
+ }
81131
+ const lines = taskIds.map((id) => describeWaitedTask(registry, id));
81132
+ return { output: lines.join(`
81133
+ `), isError: false };
81134
+ } finally {
81135
+ if (onAbort !== undefined)
81136
+ signal?.removeEventListener("abort", onAbort);
81137
+ }
81138
+ }
81139
+ };
81140
+ }
81141
+ function resolveTaskId(registry, id) {
81142
+ if (registry.get(id) !== undefined)
81143
+ return id;
81144
+ const swapped = id.startsWith("job-") ? `task-${id.slice("job-".length)}` : id.startsWith("task-") ? `job-${id.slice("task-".length)}` : undefined;
81145
+ if (swapped !== undefined && registry.get(swapped) !== undefined)
81146
+ return swapped;
81147
+ return id;
81148
+ }
81149
+ function shellTaskKillTool(registry) {
81150
+ return {
81151
+ definition: {
81152
+ name: "shell_task_kill",
81153
+ description: "Stop a running shell task \u2014 its entire process group, including any descendant it backgrounded. Input: " + "{ task_id: string }. Killing a task that has already finished is refused with its status rather than " + "signalling anything, so calling this twice is safe. Only tasks from this session can be targeted.",
81154
+ inputSchema: {
81155
+ type: "object",
81156
+ properties: { task_id: { type: "string" } },
81157
+ required: ["task_id"],
81158
+ additionalProperties: false
81159
+ },
81160
+ risk: "read"
81161
+ },
81162
+ invoke: async (input) => {
81163
+ const raw = typeof input.task_id === "string" ? input.task_id : "";
81164
+ if (raw.length === 0) {
81165
+ return { output: "shell_task_kill requires a non-empty 'task_id'", isError: true };
81166
+ }
81167
+ const taskId = raw;
81168
+ const result = await registry.kill(taskId);
81169
+ if (!result.ok) {
81170
+ return { output: result.error, isError: true };
81171
+ }
81172
+ return { output: `task ${taskId} killed`, isError: false };
81173
+ }
81174
+ };
81175
+ }
80984
81176
  function shellJobKillTool(registry) {
80985
81177
  return {
80986
81178
  definition: {
80987
81179
  name: "shell_job_kill",
80988
- description: "Stop a running shell task \u2014 its entire process group, including any descendant it backgrounded. Input: " + "{ job_id: string } \u2014 the task_id shell_exec returned for a command still running after its yield (or a " + "background:true job). Only tasks in this session's own registry can be targeted.",
81180
+ description: "DEPRECATED \u2014 use shell_task_kill; this name is kept for one release. " + "Stop a running shell task \u2014 its entire process group, including any descendant it backgrounded. Input: " + "{ job_id: string } \u2014 the task_id shell_exec returned for a command still running after its yield (or a " + "background:true job). Only tasks in this session's own registry can be targeted.",
80989
81181
  inputSchema: {
80990
81182
  type: "object",
80991
81183
  properties: { job_id: { type: "string" } },
@@ -81332,7 +81524,7 @@ function shellExecTool(root, run = makeCommandRunner(root), jobRegistry, options
81332
81524
  },
81333
81525
  risk: "shell"
81334
81526
  },
81335
- invoke: async (input) => {
81527
+ invoke: async (input, ctx) => {
81336
81528
  const command = typeof input.command === "string" ? input.command : "";
81337
81529
  if (command.length === 0) {
81338
81530
  return { output: "shell_exec requires a non-empty 'command'", isError: true };
@@ -81365,21 +81557,34 @@ function shellExecTool(root, run = makeCommandRunner(root), jobRegistry, options
81365
81557
  });
81366
81558
  }
81367
81559
  const yieldMs = options?.yieldMs ?? resolveShellYieldMs();
81368
- const outcome = await jobRegistry.waitForExit(taskId, yieldMs);
81560
+ const signal = ctx?.signal;
81561
+ let onAbort;
81562
+ const outcome = await (signal === undefined || signal.aborted === false ? Promise.race([
81563
+ jobRegistry.waitForExit(taskId, yieldMs),
81564
+ new Promise((resolve) => {
81565
+ if (signal === undefined)
81566
+ return;
81567
+ onAbort = () => resolve("aborted");
81568
+ signal.addEventListener("abort", onAbort, { once: true });
81569
+ })
81570
+ ]) : Promise.resolve("aborted"));
81571
+ if (onAbort !== undefined)
81572
+ signal?.removeEventListener("abort", onAbort);
81369
81573
  let collected = started.output;
81370
81574
  const pending = jobRegistry.readOutput(taskId);
81371
81575
  if (pending.ok)
81372
81576
  collected += pending.output;
81373
- if (outcome === "timeout") {
81577
+ if (outcome === "timeout" || outcome === "aborted") {
81374
81578
  const promoted = jobRegistry.promote(taskId);
81375
81579
  if (!promoted.ok) {
81376
81580
  return { output: promoted.error, isError: true };
81377
81581
  }
81582
+ const notice = outcome === "aborted" ? `interrupted: the wait was stopped, so this is now a background task and it is STILL RUNNING \u2014 it was not killed; read new output with shell_job_output("${taskId}") and stop it with shell_job_kill("${taskId}")` : `still running after ${yieldMs}ms, so it is now a background task; read new output with shell_job_output("${taskId}") and stop it with shell_job_kill("${taskId}")`;
81378
81583
  return taskHandleResult({
81379
81584
  taskId,
81380
81585
  pid: started.pid,
81381
81586
  output: collected,
81382
- notice: `still running after ${yieldMs}ms, so it is now a background task; read new output with shell_job_output("${taskId}") and stop it with shell_job_kill("${taskId}")`,
81587
+ notice,
81383
81588
  ...promoted.overCap !== undefined ? { overCap: promoted.overCap } : {}
81384
81589
  });
81385
81590
  }
@@ -82239,7 +82444,13 @@ function buildInteractiveAgentTools(input) {
82239
82444
  webFetchTool(),
82240
82445
  webSearchTool(input.searchController),
82241
82446
  shellExecTool(input.cwd, undefined, jobRegistry),
82242
- ...jobRegistry !== undefined ? [shellJobOutputTool(jobRegistry), shellJobKillTool(jobRegistry)] : [],
82447
+ ...jobRegistry !== undefined ? [
82448
+ shellTaskOutputTool(jobRegistry, { observer: "main" }),
82449
+ shellTaskWaitTool(jobRegistry),
82450
+ shellTaskKillTool(jobRegistry),
82451
+ shellJobOutputTool(jobRegistry),
82452
+ shellJobKillTool(jobRegistry)
82453
+ ] : [],
82243
82454
  applyPatchTool(input.cwd),
82244
82455
  workspaceOverviewTool(input.cwd),
82245
82456
  workspaceReadTool(input.cwd),
@@ -88278,6 +88489,8 @@ function classifyBusyDispatch(params) {
88278
88489
  return "copy";
88279
88490
  if (commandName === "/mode")
88280
88491
  return "mode";
88492
+ if (commandName === "/demote")
88493
+ return "demote";
88281
88494
  if (commandName === "/game")
88282
88495
  return "game";
88283
88496
  const isBusyReadonlyCommand = isSessionInfo || isFlows || isWorkspace || isReview || isMcp || isMcpConsumer;
@@ -90432,6 +90645,11 @@ var AGENT_SLASH_COMMANDS = [
90432
90645
  description: "Hand a task to an external agent CLI \u2014 /delegate <agent> <task>",
90433
90646
  modes: AGENT_ONLY
90434
90647
  },
90648
+ {
90649
+ name: "/demote",
90650
+ description: "Move a running foreground task to the background \u2014 /demote <task_id>",
90651
+ modes: AGENT_ONLY
90652
+ },
90435
90653
  {
90436
90654
  name: "/exit",
90437
90655
  description: "Leave the shell (/quit works too)",
@@ -90462,6 +90680,18 @@ function parseDelegateCommand(args) {
90462
90680
  }
90463
90681
  return { ok: true, agentId, task };
90464
90682
  }
90683
+ var DEMOTE_USAGE = "/demote <task_id> (the task_id shell_exec returned)";
90684
+ function parseDemoteCommand(args) {
90685
+ const trimmed = args.trim();
90686
+ if (trimmed.length === 0) {
90687
+ return { ok: false, reason: `usage: ${DEMOTE_USAGE}` };
90688
+ }
90689
+ const parts = trimmed.split(/\s+/);
90690
+ if (parts.length > 1) {
90691
+ return { ok: false, reason: `\`/demote\` takes ONE task_id \u2014 usage: ${DEMOTE_USAGE}` };
90692
+ }
90693
+ return { ok: true, taskId: parts[0] ?? "" };
90694
+ }
90465
90695
  function isCommandInMode(command, mode) {
90466
90696
  return command.modes.includes(mode);
90467
90697
  }
@@ -95116,7 +95346,12 @@ function paintBackgroundJobSidebar(otui, renderer, parent, entries, options) {
95116
95346
 
95117
95347
  // src/tui/tui-shell.ts
95118
95348
  var SESSION_PREVIEW_MESSAGE_COUNT = 200;
95119
- var SIDE_WORKER_DENIED_TOOL_NAMES = new Set(["shell_job_kill"]);
95349
+ var SIDE_WORKER_DENIED_TOOL_NAMES = new Set([
95350
+ "shell_job_kill",
95351
+ "shell_task_kill",
95352
+ "shell_task_wait",
95353
+ "shell_job_output"
95354
+ ]);
95120
95355
  function parseGitHubRemote(remote) {
95121
95356
  const trimmed = remote.trim();
95122
95357
  if (trimmed.length === 0) {
@@ -97989,6 +98224,22 @@ Staying in the current session.
97989
98224
  isMcpConsumer: isMcpConsumerCommand(line)
97990
98225
  });
97991
98226
  switch (decision) {
98227
+ case "demote": {
98228
+ const parsed = parseDemoteCommand(line.trim().replace(/^\/\S+\s*/, ""));
98229
+ if (!parsed.ok) {
98230
+ io.onSystem?.(`${parsed.reason}
98231
+ `);
98232
+ } else if (deps.jobRegistry === undefined) {
98233
+ io.onSystem?.(`This session tracks no shell tasks, so there is nothing to demote.
98234
+ `);
98235
+ } else {
98236
+ const demoted = demoteTask(deps.jobRegistry, parsed.taskId);
98237
+ io.onSystem?.(demoted.ok ? `Task ${parsed.taskId} keeps running, now in the background \u2014 it was NOT stopped.
98238
+ ` : `${demoted.error}
98239
+ `);
98240
+ }
98241
+ return;
98242
+ }
97992
98243
  case "exit": {
97993
98244
  foregroundOperation.cancel("shell exit");
97994
98245
  foregroundOperation.dispose();
@@ -98184,6 +98435,22 @@ Staying in the current session.
98184
98435
  })();
98185
98436
  return;
98186
98437
  }
98438
+ if (command.name === "/demote") {
98439
+ const parsed = parseDemoteCommand(line.trim().replace(/^\/\S+\s*/, ""));
98440
+ if (!parsed.ok) {
98441
+ io.onSystem?.(`${parsed.reason}
98442
+ `);
98443
+ } else if (deps.jobRegistry === undefined) {
98444
+ io.onSystem?.(`This session tracks no shell tasks, so there is nothing to demote.
98445
+ `);
98446
+ } else {
98447
+ const demoted = demoteTask(deps.jobRegistry, parsed.taskId);
98448
+ io.onSystem?.(demoted.ok ? `Task ${parsed.taskId} keeps running, now in the background \u2014 it was NOT stopped.
98449
+ ` : `${demoted.error}
98450
+ `);
98451
+ }
98452
+ return;
98453
+ }
98187
98454
  if (command.name === "/clear" || command.name === "/new") {
98188
98455
  (async () => {
98189
98456
  await closeSlateSession(slateSession, mintTimestampAttemptId);
@@ -100223,6 +100490,25 @@ ${GUTTER}${turnSeparator()}
100223
100490
  `);
100224
100491
  } else {
100225
100492
  agentIo.onSystem?.(`Compacted \u2212${packed.result.removed} context msgs \xB7 archive ${live.summary.archiveMessageCount} \xB7 compact\xD7${live.summary.compactCount}
100493
+ `);
100494
+ }
100495
+ }
100496
+ } else if (command === "/demote") {
100497
+ const parsed = parseDemoteCommand(rest);
100498
+ if (!parsed.ok) {
100499
+ agentIo.onSystem?.(`${parsed.reason}
100500
+ `);
100501
+ } else if (deps.jobRegistry === undefined) {
100502
+ agentIo.onSystem?.(`This session tracks no shell tasks, so there is nothing to demote.
100503
+ `);
100504
+ } else {
100505
+ const demoted = demoteTask(deps.jobRegistry, parsed.taskId);
100506
+ if (!demoted.ok) {
100507
+ agentIo.onSystem?.(`${demoted.error}
100508
+ `);
100509
+ } else {
100510
+ const overCap = demoted.overCap !== undefined ? ` Background tasks are over the cap; also running: ${demoted.overCap}.` : "";
100511
+ agentIo.onSystem?.(`Task ${parsed.taskId} keeps running, now in the background \u2014 it was NOT stopped.${overCap}
100226
100512
  `);
100227
100513
  }
100228
100514
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.2.109",
3
+ "version": "0.2.110",
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": {