@mrciphersmith/keryx 0.2.140 → 0.2.143

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/README.md +10 -0
  2. package/dist/cli.js +687 -67
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -248,6 +248,16 @@ What is in it today:
248
248
  session that keeps its ancestry, without editing a transcript by hand.
249
249
  `/status` is the session inspector (identity, context window and limits when
250
250
  the provider reported them); `/session-info` and `/info` are not aliases.
251
+ - **Plans that survive the turn.** For multi-step work, the agent can keep a
252
+ structured execution plan in the session. The TUI shows pending, active,
253
+ completed, blocked, and skipped steps in the sidebar, and restored sessions
254
+ continue from the same plan instead of rebuilding it from transcript text.
255
+ - **Theme-aware rich transcripts.** Assistant prose, headings, emphasis,
256
+ inline code, fenced code, diffs, and GFM pipe tables use semantic colors
257
+ derived from the selected `/theme`. Use a language fence such as
258
+ ```` ```typescript ```` for syntax color, ```` ```diff ```` for addition and
259
+ deletion rows, or ```` ```text ````/```` ```txt ```` when content must remain
260
+ literal, including terminal output and ASCII diagrams.
251
261
  - **Responsive busy-turn UX.** A running main turn can be interrupted with
252
262
  `/interrupt`; additional prompts are queued and answered as read-only side workers
253
263
  (`side-1`) in the TUI so the shell stays usable under long-running turns.
package/dist/cli.js CHANGED
@@ -23987,7 +23987,7 @@ async function openSlateAtomic(dir, mintAttemptId, build) {
23987
23987
  if (existing !== undefined) {
23988
23988
  await archiveIfExistsLocked(dir, mintAttemptId());
23989
23989
  }
23990
- const next = build();
23990
+ const next = build(existing);
23991
23991
  await writeFileAtomic(slatePath(dir), `${JSON.stringify(next, null, 2)}
23992
23992
  `);
23993
23993
  await rm7(path93.join(dir, "terminal-state.json"), { force: true });
@@ -24089,6 +24089,123 @@ var init_slate = __esm(() => {
24089
24089
  ];
24090
24090
  });
24091
24091
 
24092
+ // src/session/execution-plan.ts
24093
+ function clonePlan(plan) {
24094
+ return { revision: plan.revision, items: plan.items.map((item) => ({ ...item })) };
24095
+ }
24096
+ function validateItems(items) {
24097
+ const ids = new Set;
24098
+ let active = 0;
24099
+ return items.map((item) => {
24100
+ const id = item.id.trim();
24101
+ const title = item.title.trim();
24102
+ if (id.length === 0)
24103
+ throw new ExecutionPlanValidationError("item id must be non-empty");
24104
+ if (ids.has(id))
24105
+ throw new ExecutionPlanValidationError(`duplicate item id: ${id}`);
24106
+ ids.add(id);
24107
+ if (title.length === 0)
24108
+ throw new ExecutionPlanValidationError(`item ${id} has an empty title`);
24109
+ if (!EXECUTION_PLAN_STATUSES.includes(item.status)) {
24110
+ throw new ExecutionPlanValidationError(`item ${id} has unknown status ${JSON.stringify(item.status)}`);
24111
+ }
24112
+ if (item.status === "in_progress" && ++active > 1) {
24113
+ throw new ExecutionPlanValidationError("at most one item may be in_progress");
24114
+ }
24115
+ return { id, title, status: item.status };
24116
+ });
24117
+ }
24118
+ function notify(dir, plan) {
24119
+ for (const listener of listeners) {
24120
+ try {
24121
+ listener(dir, clonePlan(plan));
24122
+ } catch {}
24123
+ }
24124
+ }
24125
+ function subscribeExecutionPlans(listener) {
24126
+ listeners.add(listener);
24127
+ return () => listeners.delete(listener);
24128
+ }
24129
+ async function getExecutionPlan(dir) {
24130
+ const plan = (await readSlate(dir))?.executionPlan;
24131
+ return plan === undefined ? undefined : clonePlan(plan);
24132
+ }
24133
+ async function setExecutionPlan(dir, input) {
24134
+ const items = validateItems(input.items);
24135
+ let result;
24136
+ await writeSlate(dir, (prev) => {
24137
+ if (prev === undefined)
24138
+ throw new ExecutionPlanValidationError("no open Slate");
24139
+ const actual = prev.executionPlan?.revision ?? 0;
24140
+ if (input.expectedRevision !== actual)
24141
+ throw new ExecutionPlanConflictError(input.expectedRevision, actual);
24142
+ result = { revision: actual + 1, items };
24143
+ return { ...prev, executionPlan: result };
24144
+ });
24145
+ const plan = clonePlan(result);
24146
+ notify(dir, plan);
24147
+ return plan;
24148
+ }
24149
+ async function updateExecutionPlan(dir, input) {
24150
+ let result;
24151
+ await writeSlate(dir, (prev) => {
24152
+ if (prev === undefined)
24153
+ throw new ExecutionPlanValidationError("no open Slate");
24154
+ const current = prev.executionPlan;
24155
+ const actual = current?.revision ?? 0;
24156
+ if (input.expectedRevision !== actual)
24157
+ throw new ExecutionPlanConflictError(input.expectedRevision, actual);
24158
+ if (current === undefined)
24159
+ throw new ExecutionPlanValidationError("no execution plan exists");
24160
+ if (!EXECUTION_PLAN_STATUSES.includes(input.status)) {
24161
+ throw new ExecutionPlanValidationError(`unknown status ${JSON.stringify(input.status)}`);
24162
+ }
24163
+ if (!current.items.some((item) => item.id === input.itemId)) {
24164
+ throw new ExecutionPlanValidationError(`unknown item id: ${input.itemId}`);
24165
+ }
24166
+ const items = validateItems(current.items.map((item) => item.id === input.itemId ? { ...item, status: input.status } : item));
24167
+ result = { revision: actual + 1, items };
24168
+ return { ...prev, executionPlan: result };
24169
+ });
24170
+ const plan = clonePlan(result);
24171
+ notify(dir, plan);
24172
+ return plan;
24173
+ }
24174
+ function hasActionableExecutionPlanItems(plan) {
24175
+ return plan?.items.some((item) => item.status === "pending" || item.status === "in_progress") ?? false;
24176
+ }
24177
+ function renderExecutionPlanSnapshot(plan, maxItems = 7) {
24178
+ if (plan === undefined || plan.items.length === 0)
24179
+ return;
24180
+ const rows = plan.items.slice(0, Math.max(0, maxItems)).map((item) => {
24181
+ const title = item.title.length > 120 ? `${item.title.slice(0, 119)}\u2026` : item.title;
24182
+ return `- ${item.id} [${item.status}]: ${title}`;
24183
+ });
24184
+ if (plan.items.length > rows.length)
24185
+ rows.push(`- \u2026 ${plan.items.length - rows.length} more item(s)`);
24186
+ return `Current execution plan (revision ${plan.revision}):
24187
+ ${rows.join(`
24188
+ `)}`;
24189
+ }
24190
+ var EXECUTION_PLAN_STATUSES, listeners, ExecutionPlanConflictError, ExecutionPlanValidationError;
24191
+ var init_execution_plan = __esm(() => {
24192
+ init_slate();
24193
+ EXECUTION_PLAN_STATUSES = ["pending", "in_progress", "completed", "blocked", "skipped"];
24194
+ listeners = new Set;
24195
+ ExecutionPlanConflictError = class ExecutionPlanConflictError extends Error {
24196
+ constructor(expected, actual) {
24197
+ super(`execution plan revision conflict: expected ${expected}, current ${actual}`);
24198
+ this.name = "ExecutionPlanConflictError";
24199
+ }
24200
+ };
24201
+ ExecutionPlanValidationError = class ExecutionPlanValidationError extends Error {
24202
+ constructor(message) {
24203
+ super(`invalid execution plan: ${message}`);
24204
+ this.name = "ExecutionPlanValidationError";
24205
+ }
24206
+ };
24207
+ });
24208
+
24092
24209
  // src/flow/store.ts
24093
24210
  import { createHash as createHash16 } from "crypto";
24094
24211
  import { appendFile as appendFile3, mkdir as mkdir32, readFile as readFile49, readdir as readdir12 } from "fs/promises";
@@ -28849,7 +28966,12 @@ async function openSlate(opts) {
28849
28966
  cwd: opts.cwd,
28850
28967
  ...opts.runtime !== undefined ? { runtime: opts.runtime } : {}
28851
28968
  });
28852
- return openSlateAtomic(opts.dir, opts.mintAttemptId, () => ({ anchors, course: {}, seeds: [] }));
28969
+ return openSlateAtomic(opts.dir, opts.mintAttemptId, (existing) => ({
28970
+ anchors,
28971
+ course: {},
28972
+ seeds: [],
28973
+ ...existing?.executionPlan === undefined ? {} : { executionPlan: existing.executionPlan }
28974
+ }));
28853
28975
  }
28854
28976
  async function closeSlate(dir, mintAttemptId) {
28855
28977
  await archiveSlate(dir, mintAttemptId());
@@ -28891,7 +29013,12 @@ async function ensureSlateOpened(ref, mintAttemptId, runtime) {
28891
29013
  if (isSlateSessionDetached(ref)) {
28892
29014
  return;
28893
29015
  }
28894
- await openSlateAtomic(ref.dir, mintAttemptId, () => ({ anchors, course: {}, seeds: [] }));
29016
+ await openSlateAtomic(ref.dir, mintAttemptId, (existing) => ({
29017
+ anchors,
29018
+ course: {},
29019
+ seeds: [],
29020
+ ...existing?.executionPlan === undefined ? {} : { executionPlan: existing.executionPlan }
29021
+ }));
28895
29022
  ref.opened = true;
28896
29023
  }
28897
29024
  async function closeSlateSession(ref, mintAttemptId) {
@@ -29360,7 +29487,7 @@ function buildAgentSystemInstruction(orient, ctx = {}) {
29360
29487
  ` + locateRule + "- read_file returns a bounded window starting at `start_line` (default 1). A large file " + "is read by paging: its truncation notice names the start_line to continue from, and a " + `line number from ${hasGraph ? "search_code or graph_symbol" : "search_code"} can be read directly \u2014 do not re-read the ` + `same head hoping for more.
29361
29488
  ` + "- Prefer ONE correct shell_exec over many exploratory tool calls when the user asks " + `to run a known keryx workflow.
29362
29489
  ` + "- Tool-call budget: shell_exec, file-mutating shell, workspace_create/workspace_propose, and spawn_subagent " + "all share ONE small per-turn pool (distinct non-read actions), separate from the much larger read-tool pool. " + (hasGraph ? "search_code/graph_*/memory_search/read_wiki/wiki_*/test_related/health_status/flow_status/repomap/read_file/" : "search_code/read_file/") + "list_dir do NOT touch it. Conserve the small pool: batch multiple shell steps into ONE call with `&&` instead " + "of issuing them one at a time, get a command's arguments right the first time instead of trying variants, and " + "for any check covered by a read tool above, use that tool instead of shelling out to the equivalent `keryx \u2026` " + `CLI command.
29363
- ` + "- This session has its own Slate (working-set scratch, not project knowledge): " + "**slate_read** shows the Course (if a Flow is bound) and Seeds recorded so far \u2014 nothing " + "here is auto-injected, so call it if you want to see it. **slate_write_seed** with " + "`{ text, kind? }` records a draft hypothesis/decision/follow-up worth a later human " + "review. WRITE A SEED when you: found a root cause or a bug worth remembering; changed or " + "added code (summarize WHAT changed and WHY); took a design/architecture decision; " + "identified a risk; or discovered a constraint/lesson. Use the `kind` that fits: " + "`decision` (a choice made), `wiki-update` (something a wiki page should say), " + "`memory-entry` (a lesson/constraint), `follow-up` (a TODO for a later session), " + "`risk`, or `contract-change`. Keep each Seed to 2-3 sentences, concrete and specific. " + "Do NOT write Seeds for routine progress notes, one-shot operational requests (e.g. " + '"run git pull", "count files"), or trivia \u2014 those need no workspace and no proposal. ' + "Seeds are the ONLY input wrap-up proposes from: a session whose Slate has zero Seeds " + `produces zero proposals. A Seed is never accepted knowledge by itself.
29490
+ ` + "- For multi-step work, use **plan_set** to publish a structured plan, **plan_update** after each real status " + "change, and **plan_get** before resolving a revision conflict. Keep stable item ids, at most one " + "`in_progress` item, and do not mark work complete before verification. These tools update session metadata " + "only; `/plan` remains the operator's separate read-only permission mode.\n" + "- This session has its own Slate (working-set scratch, not project knowledge): " + "**slate_read** shows the Course (if a Flow is bound) and Seeds recorded so far \u2014 nothing " + "here is auto-injected, so call it if you want to see it. **slate_write_seed** with " + "`{ text, kind? }` records a draft hypothesis/decision/follow-up worth a later human " + "review. WRITE A SEED when you: found a root cause or a bug worth remembering; changed or " + "added code (summarize WHAT changed and WHY); took a design/architecture decision; " + "identified a risk; or discovered a constraint/lesson. Use the `kind` that fits: " + "`decision` (a choice made), `wiki-update` (something a wiki page should say), " + "`memory-entry` (a lesson/constraint), `follow-up` (a TODO for a later session), " + "`risk`, or `contract-change`. Keep each Seed to 2-3 sentences, concrete and specific. " + "Do NOT write Seeds for routine progress notes, one-shot operational requests (e.g. " + '"run git pull", "count files"), or trivia \u2014 those need no workspace and no proposal. ' + "Seeds are the ONLY input wrap-up proposes from: a session whose Slate has zero Seeds " + `produces zero proposals. A Seed is never accepted knowledge by itself.
29364
29491
  ` + "- Shared Agent Context (SAC) workspaces hold accepted, evidence-backed project context " + "beyond this codebase. **workspace_list** with `{ includeArchived? }` shows every workspace " + "visible to you \u2014 call it first when the user references a shared team workspace or accepted " + "project context, or before creating a new workspace, to judge whether an existing one " + "already fits the current topic. **workspace_show** with `{ workspaceId }` shows one " + "workspace's manifest. **workspace_overview** with `{ workspaceId }`, then **workspace_read** " + "with `{ workspaceId, itemId }` for one specific item, reads its accepted Facts/Work/Know-how. " + "**workspace_create** with `{ title, component? }` creates a new workspace AND binds it to " + "this session's slate (wrap-up then proposes into it) \u2014 only when workspace_list found no " + "fitting one and the session has real, durable results worth persisting; a workspace is " + "meant to persist across sessions, so prefer an existing one over creating another for the " + "same topic, and do NOT create one for one-shot operational requests. **workspace_propose** with " + "`{ workspaceId, kind, sessionId?, note? }` (sessionId defaults to this session) proposes a decision/wiki-update/memory-entry/" + "follow-up/contract-change/risk from this session for later human review \u2014 it never accepts " + "anything by itself; accepting always requires a human running `keryx workspace review` at a " + `real terminal, never this tool.
29365
29492
  ` + "- When you need a decision, interview step, or clarification: use **ask_user** with " + "2\u20136 options `{ id, label, description, recommended? }` (mark one recommended). " + `Do not dump long prose questions without options.
29366
29493
  ` + "- For a focused independent subtask (investigate X, review Y, research Z): use " + "**spawn_subagent** with `{ task, mode?: 'read_only'|'general', label? }`. " + "Default mode is read_only (no shell). Prefer spawn for work that can finish " + `without your intermediate turns; do not spawn for trivial one-line answers.
@@ -29657,6 +29784,7 @@ async function runAgentTurnCore(io, deps, history, userLine, options = {}) {
29657
29784
  return "stop";
29658
29785
  };
29659
29786
  let toollessReprompts = 0;
29787
+ let planFollowThroughUsed = false;
29660
29788
  let lastToollessText;
29661
29789
  for (;; ) {
29662
29790
  if (roundState.round >= roundState.maxRounds) {
@@ -29666,7 +29794,20 @@ async function runAgentTurnCore(io, deps, history, userLine, options = {}) {
29666
29794
  return { finishReason: "budget" };
29667
29795
  }
29668
29796
  roundState.round += 1;
29669
- const preRequestEstimate = estimateRequestTokens(history, deps.systemInstruction, toolDefs);
29797
+ let currentPlan;
29798
+ if (options.slateSession?.opened === true) {
29799
+ try {
29800
+ currentPlan = await getExecutionPlan(options.slateSession.dir);
29801
+ } catch (cause) {
29802
+ io.onSystem?.(`execution plan read failed (ignored): ${cause instanceof Error ? cause.message : String(cause)}
29803
+ `);
29804
+ }
29805
+ }
29806
+ const planSnapshot = renderExecutionPlanSnapshot(currentPlan);
29807
+ const roundSystemInstruction = planSnapshot === undefined ? deps.systemInstruction : `${deps.systemInstruction}
29808
+
29809
+ ${planSnapshot}`;
29810
+ const preRequestEstimate = estimateRequestTokens(history, roundSystemInstruction, toolDefs);
29670
29811
  if (needsCompaction(preRequestEstimate, deps.contextWindow)) {
29671
29812
  const compacted = compactMessages(history, { keepLastUserTurns: 3 });
29672
29813
  if (!compacted.noop) {
@@ -29681,7 +29822,7 @@ async function runAgentTurnCore(io, deps, history, userLine, options = {}) {
29681
29822
  const baseRequest = {
29682
29823
  providerId: deps.providerId,
29683
29824
  modelId: deps.modelId,
29684
- systemInstruction: deps.systemInstruction,
29825
+ systemInstruction: roundSystemInstruction,
29685
29826
  messages: [...history],
29686
29827
  tools: toolDefs,
29687
29828
  budget: { maxOutputTokens, runReservation: maxOutputTokens },
@@ -29911,6 +30052,32 @@ async function runAgentTurnCore(io, deps, history, userLine, options = {}) {
29911
30052
  }
29912
30053
  continue;
29913
30054
  }
30055
+ if (!planFollowThroughUsed && hasActionableExecutionPlanItems(currentPlan)) {
30056
+ planFollowThroughUsed = true;
30057
+ history.push({
30058
+ role: "user",
30059
+ content: "[system] The current execution plan still has actionable items remaining. Continue the work now. " + "Do not give another final reply until the plan is complete or genuinely blocked.",
30060
+ provenance: "project",
30061
+ ts: now()
30062
+ });
30063
+ io.onHistoryChange?.("tool");
30064
+ continue;
30065
+ }
30066
+ if (planFollowThroughUsed && hasActionableExecutionPlanItems(currentPlan)) {
30067
+ const actionable = currentPlan?.items.filter((item) => item.status === "pending" || item.status === "in_progress") ?? [];
30068
+ const shown = actionable.slice(0, 7).map((item) => {
30069
+ const title = item.title.length > 120 ? `${item.title.slice(0, 119)}\u2026` : item.title;
30070
+ return `- ${item.id} [${item.status}]: ${title}`;
30071
+ });
30072
+ if (actionable.length > shown.length) {
30073
+ shown.push(`- \u2026 ${actionable.length - shown.length} more actionable item(s)`);
30074
+ }
30075
+ system(`
30076
+ [plan] Actionable items remain after the single follow-through:
30077
+ ${shown.join(`
30078
+ `)}
30079
+ `);
30080
+ }
29914
30081
  return {};
29915
30082
  }
29916
30083
  if (isAborted()) {
@@ -30481,6 +30648,7 @@ var init_agent = __esm(() => {
30481
30648
  init_peer_notification();
30482
30649
  init_scheduler();
30483
30650
  init_slate();
30651
+ init_execution_plan();
30484
30652
  init_slate_course();
30485
30653
  init_machine_wrap_up();
30486
30654
  init_slate_lifecycle();
@@ -44782,7 +44950,7 @@ import { stdout } from "process";
44782
44950
  // src/lib/md-blocks.ts
44783
44951
  var FENCE_LINE = /^[ \t]{0,3}(```|~~~)(.*)\r?$/;
44784
44952
  var HUNK_HEADER = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/;
44785
- var MARKDOWN_LANGS = new Set(["md", "markdown", "prompt", "txt", "text"]);
44953
+ var MARKDOWN_LANGS = new Set(["md", "markdown", "prompt"]);
44786
44954
  var DIFF_LANGS = new Set(["diff", "patch"]);
44787
44955
  function stripTrailingCr(line) {
44788
44956
  return line.endsWith("\r") ? line.slice(0, -1) : line;
@@ -44791,6 +44959,101 @@ function splitLines(text) {
44791
44959
  return text.split(`
44792
44960
  `).map(stripTrailingCr);
44793
44961
  }
44962
+ function markdownTableCells(line) {
44963
+ const source = line.trim();
44964
+ if (!source.includes("|")) {
44965
+ return;
44966
+ }
44967
+ const cells = [];
44968
+ let cell = "";
44969
+ let inCode = false;
44970
+ for (let index = 0;index < source.length; index += 1) {
44971
+ const char = source[index] ?? "";
44972
+ if (char === "\\" && source[index + 1] === "|") {
44973
+ cell += "|";
44974
+ index += 1;
44975
+ continue;
44976
+ }
44977
+ if (char === "`") {
44978
+ inCode = !inCode;
44979
+ cell += char;
44980
+ continue;
44981
+ }
44982
+ if (char === "|" && !inCode) {
44983
+ cells.push(cell.trim());
44984
+ cell = "";
44985
+ continue;
44986
+ }
44987
+ cell += char;
44988
+ }
44989
+ cells.push(cell.trim());
44990
+ if (source.startsWith("|")) {
44991
+ cells.shift();
44992
+ }
44993
+ if (source.endsWith("|")) {
44994
+ cells.pop();
44995
+ }
44996
+ return cells.length >= 2 ? cells : undefined;
44997
+ }
44998
+ function tableAlignments(line, columns) {
44999
+ const cells = markdownTableCells(line);
45000
+ if (cells === undefined || cells.length !== columns) {
45001
+ return;
45002
+ }
45003
+ const alignments = [];
45004
+ for (const cell of cells) {
45005
+ const marker = cell.trim();
45006
+ if (!/^:?-{3,}:?$/.test(marker)) {
45007
+ return;
45008
+ }
45009
+ alignments.push(marker.startsWith(":") && marker.endsWith(":") ? "center" : marker.endsWith(":") ? "right" : "left");
45010
+ }
45011
+ return alignments;
45012
+ }
45013
+ function parseMarkdownTables(text) {
45014
+ if (text.length === 0) {
45015
+ return [];
45016
+ }
45017
+ const lines = splitLines(text);
45018
+ const parts = [];
45019
+ let proseStart = 0;
45020
+ const flushProse = (end) => {
45021
+ if (end <= proseStart) {
45022
+ return;
45023
+ }
45024
+ const prose = lines.slice(proseStart, end).join(`
45025
+ `);
45026
+ if (prose.length > 0) {
45027
+ parts.push({ kind: "prose", text: prose });
45028
+ }
45029
+ };
45030
+ for (let index = 0;index + 1 < lines.length; index += 1) {
45031
+ const headers = markdownTableCells(lines[index] ?? "");
45032
+ if (headers === undefined) {
45033
+ continue;
45034
+ }
45035
+ const alignments = tableAlignments(lines[index + 1] ?? "", headers.length);
45036
+ if (alignments === undefined) {
45037
+ continue;
45038
+ }
45039
+ flushProse(index);
45040
+ const rows = [];
45041
+ let cursor = index + 2;
45042
+ while (cursor < lines.length) {
45043
+ const row = markdownTableCells(lines[cursor] ?? "");
45044
+ if (row === undefined || row.length !== headers.length) {
45045
+ break;
45046
+ }
45047
+ rows.push(row);
45048
+ cursor += 1;
45049
+ }
45050
+ parts.push({ kind: "table", table: { headers, alignments, rows } });
45051
+ proseStart = cursor;
45052
+ index = cursor - 1;
45053
+ }
45054
+ flushProse(lines.length);
45055
+ return parts.length > 0 ? parts : [{ kind: "prose", text }];
45056
+ }
44794
45057
  function fenceInfo(line) {
44795
45058
  const match = FENCE_LINE.exec(line);
44796
45059
  if (match === null) {
@@ -51141,7 +51404,7 @@ import path47 from "path";
51141
51404
  // package.json
51142
51405
  var package_default = {
51143
51406
  name: "@mrciphersmith/keryx",
51144
- version: "0.2.140",
51407
+ version: "0.2.143",
51145
51408
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
51146
51409
  private: false,
51147
51410
  publishConfig: {
@@ -86842,6 +87105,107 @@ shell_exec: no output for ${info.idleTimeoutMs}ms, so the command was killed (ra
86842
87105
  };
86843
87106
  }
86844
87107
 
87108
+ // src/harness/tool/builtin/execution-plan-tool.ts
87109
+ init_execution_plan();
87110
+ var statusSchema = { type: "string", enum: [...EXECUTION_PLAN_STATUSES] };
87111
+ function noSession(name) {
87112
+ return { output: `${name}: no active session in this run`, isError: true };
87113
+ }
87114
+ function failed(name, cause) {
87115
+ return { output: `${name} failed: ${cause instanceof Error ? cause.message : String(cause)}`, isError: true };
87116
+ }
87117
+ function executionPlanTools(getSessionDir) {
87118
+ const get = {
87119
+ definition: {
87120
+ name: "plan_get",
87121
+ description: "Read the current structured execution plan. Input: {}.",
87122
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
87123
+ risk: "read"
87124
+ },
87125
+ invoke: async () => {
87126
+ const dir = getSessionDir();
87127
+ if (dir === undefined)
87128
+ return noSession("plan_get");
87129
+ try {
87130
+ return { output: JSON.stringify(await getExecutionPlan(dir) ?? null, null, 2), isError: false };
87131
+ } catch (cause) {
87132
+ return failed("plan_get", cause);
87133
+ }
87134
+ }
87135
+ };
87136
+ const set = {
87137
+ definition: {
87138
+ name: "plan_set",
87139
+ description: "Replace the structured execution plan using optimistic revision control.",
87140
+ inputSchema: {
87141
+ type: "object",
87142
+ properties: {
87143
+ expectedRevision: { type: "integer", minimum: 0 },
87144
+ items: {
87145
+ type: "array",
87146
+ items: {
87147
+ type: "object",
87148
+ properties: { id: { type: "string", minLength: 1 }, title: { type: "string", minLength: 1 }, status: statusSchema },
87149
+ required: ["id", "title", "status"],
87150
+ additionalProperties: false
87151
+ }
87152
+ }
87153
+ },
87154
+ required: ["expectedRevision", "items"],
87155
+ additionalProperties: false
87156
+ },
87157
+ risk: "read"
87158
+ },
87159
+ invoke: async (input) => {
87160
+ const dir = getSessionDir();
87161
+ if (dir === undefined)
87162
+ return noSession("plan_set");
87163
+ try {
87164
+ const plan = await setExecutionPlan(dir, {
87165
+ expectedRevision: input.expectedRevision,
87166
+ items: input.items
87167
+ });
87168
+ return { output: JSON.stringify(plan, null, 2), isError: false };
87169
+ } catch (cause) {
87170
+ return failed("plan_set", cause);
87171
+ }
87172
+ }
87173
+ };
87174
+ const update = {
87175
+ definition: {
87176
+ name: "plan_update",
87177
+ description: "Update one execution-plan item's status using optimistic revision control.",
87178
+ inputSchema: {
87179
+ type: "object",
87180
+ properties: {
87181
+ expectedRevision: { type: "integer", minimum: 0 },
87182
+ itemId: { type: "string", minLength: 1 },
87183
+ status: statusSchema
87184
+ },
87185
+ required: ["expectedRevision", "itemId", "status"],
87186
+ additionalProperties: false
87187
+ },
87188
+ risk: "read"
87189
+ },
87190
+ invoke: async (input) => {
87191
+ const dir = getSessionDir();
87192
+ if (dir === undefined)
87193
+ return noSession("plan_update");
87194
+ try {
87195
+ const plan = await updateExecutionPlan(dir, {
87196
+ expectedRevision: input.expectedRevision,
87197
+ itemId: input.itemId,
87198
+ status: input.status
87199
+ });
87200
+ return { output: JSON.stringify(plan, null, 2), isError: false };
87201
+ } catch (cause) {
87202
+ return failed("plan_update", cause);
87203
+ }
87204
+ }
87205
+ };
87206
+ return [set, update, get];
87207
+ }
87208
+
86845
87209
  // src/harness/tool/builtin/slate-tool.ts
86846
87210
  init_slate();
86847
87211
  init_slate_course();
@@ -87760,6 +88124,7 @@ function buildInteractiveAgentTools(input) {
87760
88124
  createAskUserTool(invokeAskUserHost),
87761
88125
  slateReadTool(input.cwd, getSessionDir),
87762
88126
  slateWriteSeedTool(getSessionDir, idSeq, clock),
88127
+ ...executionPlanTools(getSessionDir),
87763
88128
  ...input.bus?.client() === undefined ? [] : buildBusTools(input.bus.client),
87764
88129
  ...input.mcp === undefined ? [] : createMcpInteractiveTools({
87765
88130
  catalog: input.mcp.catalog,
@@ -91315,6 +91680,64 @@ class LiveMarkdownBlock {
91315
91680
  init_config_dir();
91316
91681
  import { existsSync as existsSync33 } from "fs";
91317
91682
  import path192 from "path";
91683
+ function hexChannels(hex) {
91684
+ return [1, 3, 5].map((offset) => Number.parseInt(hex.slice(offset, offset + 2), 16));
91685
+ }
91686
+ function mixHex(base, accent, accentWeight) {
91687
+ const a = hexChannels(base);
91688
+ const b = hexChannels(accent);
91689
+ const channel = (index) => Math.round((a[index] ?? 0) * (1 - accentWeight) + (b[index] ?? 0) * accentWeight).toString(16).padStart(2, "0");
91690
+ return `#${channel(0)}${channel(1)}${channel(2)}`;
91691
+ }
91692
+ function relativeLuminance(hex) {
91693
+ const linear = hexChannels(hex).map((channel) => {
91694
+ const value = channel / 255;
91695
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
91696
+ });
91697
+ return 0.2126 * (linear[0] ?? 0) + 0.7152 * (linear[1] ?? 0) + 0.0722 * (linear[2] ?? 0);
91698
+ }
91699
+ function contrastRatio(a, b) {
91700
+ const [light, dark] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x);
91701
+ return ((light ?? 0) + 0.05) / ((dark ?? 0) + 0.05);
91702
+ }
91703
+ function transcriptAccent(theme, accent) {
91704
+ for (let weight = 0.5;weight >= 0; weight -= 0.05) {
91705
+ const candidate = mixHex(theme.text, accent, weight);
91706
+ if (contrastRatio(candidate, theme.bg) >= 3 && contrastRatio(candidate, theme.panel) >= 3) {
91707
+ return candidate;
91708
+ }
91709
+ }
91710
+ return theme.text;
91711
+ }
91712
+ function deriveTranscriptPalette(theme) {
91713
+ const heading = transcriptAccent(theme, theme.assistant);
91714
+ const muted = transcriptAccent(theme, theme.muted);
91715
+ const positive = transcriptAccent(theme, theme.ok);
91716
+ const negative = transcriptAccent(theme, theme.error);
91717
+ const focus = transcriptAccent(theme, theme.focus);
91718
+ return {
91719
+ prose: theme.text,
91720
+ heading,
91721
+ emphasis: theme.text,
91722
+ inlineCode: focus,
91723
+ inlineCodeBackground: theme.highlight,
91724
+ blockMeta: theme.muted,
91725
+ tableHeader: heading,
91726
+ tableBorder: theme.border,
91727
+ codePlain: theme.text,
91728
+ codeComment: muted,
91729
+ codeString: positive,
91730
+ codeNumber: focus,
91731
+ codeKeyword: heading,
91732
+ diffContext: theme.text,
91733
+ diffMeta: muted,
91734
+ diffHunk: heading,
91735
+ diffAdd: positive,
91736
+ diffDelete: negative,
91737
+ diffAddBackground: mixHex(theme.panel, theme.ok, 0.12),
91738
+ diffDeleteBackground: mixHex(theme.panel, theme.error, 0.12)
91739
+ };
91740
+ }
91318
91741
  var THEME_NAMES = [
91319
91742
  "groknight",
91320
91743
  "tokyonight",
@@ -91528,7 +91951,7 @@ var THEMES = {
91528
91951
  };
91529
91952
  var kind = DEFAULT_THEME_ID;
91530
91953
  var resolved = THEMES.groknight;
91531
- var listeners = new Set;
91954
+ var listeners2 = new Set;
91532
91955
  function parseThemeId(raw) {
91533
91956
  const key = raw.trim().toLowerCase();
91534
91957
  if (key === "auto" || key === "system") {
@@ -91568,15 +91991,15 @@ function themeLabel(id) {
91568
91991
  function applyThemeId(id, mode = null) {
91569
91992
  kind = id;
91570
91993
  resolved = resolveTheme(id, mode);
91571
- for (const listener of listeners) {
91994
+ for (const listener of listeners2) {
91572
91995
  listener(resolved, kind);
91573
91996
  }
91574
91997
  return resolved;
91575
91998
  }
91576
91999
  function onThemeChange(listener) {
91577
- listeners.add(listener);
92000
+ listeners2.add(listener);
91578
92001
  return () => {
91579
- listeners.delete(listener);
92002
+ listeners2.delete(listener);
91580
92003
  };
91581
92004
  }
91582
92005
  function tuiConfigPath(dir) {
@@ -92305,11 +92728,15 @@ function clipBody(text, maxLines = MAX_BODY_LINES) {
92305
92728
  return [...lines.slice(0, maxLines), `\u2026 (${hidden} more line${hidden === 1 ? "" : "s"} not shown)`].join(`
92306
92729
  `);
92307
92730
  }
92308
- function markdownToChunks(otui, md) {
92731
+ function themedChunk(otui, text, fg, bg) {
92732
+ const foreground = otui.fg(fg)(text);
92733
+ return bg === undefined ? foreground : otui.bg(bg)(foreground);
92734
+ }
92735
+ function markdownToChunks(otui, md, palette = deriveTranscriptPalette(getTheme())) {
92309
92736
  const out = [];
92310
92737
  const plain = (s) => {
92311
92738
  if (s.length > 0) {
92312
- out.push(...otui.stringToStyledText(s).chunks);
92739
+ out.push(themedChunk(otui, s, palette.prose));
92313
92740
  }
92314
92741
  };
92315
92742
  const inline = (text) => {
@@ -92319,9 +92746,9 @@ function markdownToChunks(otui, md) {
92319
92746
  while ((m = re.exec(text)) !== null) {
92320
92747
  plain(text.slice(last, m.index));
92321
92748
  if (m[1] !== undefined) {
92322
- out.push(otui.dim(m[1].slice(1, -1)));
92749
+ out.push(themedChunk(otui, m[1].slice(1, -1), palette.inlineCode, palette.inlineCodeBackground));
92323
92750
  } else if (m[2] !== undefined) {
92324
- out.push(otui.bold(m[2].slice(2, -2)));
92751
+ out.push(otui.bold(themedChunk(otui, m[2].slice(2, -2), palette.emphasis)));
92325
92752
  }
92326
92753
  last = m.index + m[0].length;
92327
92754
  }
@@ -92340,12 +92767,12 @@ function markdownToChunks(otui, md) {
92340
92767
  `);
92341
92768
  }
92342
92769
  if (inCode) {
92343
- out.push(otui.dim(line));
92770
+ out.push(themedChunk(otui, line, palette.codePlain));
92344
92771
  continue;
92345
92772
  }
92346
92773
  const heading = /^#{1,6}\s+(.*)$/.exec(line);
92347
92774
  if (heading !== null) {
92348
- out.push(otui.cyan(otui.bold(heading[1] ?? "")));
92775
+ out.push(otui.bold(themedChunk(otui, heading[1] ?? "", palette.heading)));
92349
92776
  continue;
92350
92777
  }
92351
92778
  const bullet = /^(\s*)[-*]\s+(.*)$/.exec(line);
@@ -92358,109 +92785,150 @@ function markdownToChunks(otui, md) {
92358
92785
  }
92359
92786
  return out;
92360
92787
  }
92361
- function diffChunks(otui, text) {
92788
+ function diffChunks(otui, text, palette = deriveTranscriptPalette(getTheme())) {
92362
92789
  const out = [];
92363
92790
  const lines = splitLines(text);
92364
92791
  for (const [index, line] of lines.entries()) {
92365
92792
  if (index > 0) {
92366
- out.push(...otui.stringToStyledText(`
92367
- `).chunks);
92793
+ out.push(themedChunk(otui, `
92794
+ `, palette.diffContext));
92368
92795
  }
92369
92796
  switch (classifyDiffLine(line)) {
92370
92797
  case "add":
92371
- out.push(otui.green(line));
92798
+ out.push(themedChunk(otui, line, palette.diffAdd, palette.diffAddBackground));
92372
92799
  break;
92373
92800
  case "del":
92374
- out.push(otui.red(line));
92801
+ out.push(themedChunk(otui, line, palette.diffDelete, palette.diffDeleteBackground));
92375
92802
  break;
92376
92803
  case "hunk":
92377
- out.push(otui.cyan(line));
92804
+ out.push(themedChunk(otui, line, palette.diffHunk));
92378
92805
  break;
92379
92806
  case "meta":
92380
- out.push(otui.dim(line));
92807
+ out.push(themedChunk(otui, line, palette.diffMeta));
92381
92808
  break;
92382
92809
  default:
92383
- out.push(...otui.stringToStyledText(line).chunks);
92810
+ out.push(themedChunk(otui, line, palette.diffContext));
92384
92811
  }
92385
92812
  }
92386
92813
  return out;
92387
92814
  }
92388
- function flatDimChunks(otui, text) {
92815
+ function flatDimChunks(otui, text, palette = deriveTranscriptPalette(getTheme())) {
92389
92816
  const out = [];
92390
92817
  for (const [index, line] of splitLines(text).entries()) {
92391
92818
  if (index > 0) {
92392
- out.push(...otui.stringToStyledText(`
92393
- `).chunks);
92819
+ out.push(otui.dim(themedChunk(otui, `
92820
+ `, palette.blockMeta)));
92394
92821
  }
92395
- out.push(otui.dim(line));
92822
+ out.push(otui.dim(themedChunk(otui, line, palette.blockMeta)));
92396
92823
  }
92397
92824
  return out;
92398
92825
  }
92399
- function codeChunks(otui, text, lang) {
92826
+ function codeChunks(otui, text, lang, palette) {
92400
92827
  const out = [];
92401
92828
  for (const [index, line] of splitLines(text).entries()) {
92402
92829
  if (index > 0) {
92403
- out.push(...otui.stringToStyledText(`
92404
- `).chunks);
92830
+ out.push(themedChunk(otui, `
92831
+ `, palette.codePlain));
92405
92832
  }
92406
92833
  for (const token of tokenizeCodeLine(line, lang)) {
92407
92834
  switch (token.kind) {
92408
92835
  case "comment":
92409
- out.push(otui.dim(token.text));
92836
+ out.push(themedChunk(otui, token.text, palette.codeComment));
92410
92837
  break;
92411
92838
  case "string":
92412
- out.push(otui.green(token.text));
92839
+ out.push(themedChunk(otui, token.text, palette.codeString));
92413
92840
  break;
92414
92841
  case "number":
92415
- out.push(otui.yellow(token.text));
92842
+ out.push(themedChunk(otui, token.text, palette.codeNumber));
92416
92843
  break;
92417
92844
  case "keyword":
92418
- out.push(otui.cyan(token.text));
92845
+ out.push(themedChunk(otui, token.text, palette.codeKeyword));
92419
92846
  break;
92420
92847
  default:
92421
- out.push(...otui.stringToStyledText(token.text).chunks);
92848
+ out.push(themedChunk(otui, token.text, palette.codePlain));
92422
92849
  }
92423
92850
  }
92424
92851
  }
92425
92852
  return out;
92426
92853
  }
92427
- function payloadChunks(otui, text, lang = "") {
92854
+ function payloadChunks(otui, text, lang = "", palette = deriveTranscriptPalette(getTheme())) {
92428
92855
  const kind = payloadKind(lang, lineCountOf(text));
92429
92856
  if (kind === "diff" || looksLikeUnifiedDiff(text)) {
92430
- return diffChunks(otui, text);
92857
+ return diffChunks(otui, text, palette);
92431
92858
  }
92432
92859
  if (kind === "markdown" || lang.length === 0) {
92433
- return markdownToChunks(otui, text);
92860
+ return markdownToChunks(otui, text, palette);
92434
92861
  }
92435
- return codeChunks(otui, text, lang);
92862
+ return codeChunks(otui, text, lang, palette);
92436
92863
  }
92437
92864
  function createSegmentView(otui, renderer, parent, segment) {
92438
92865
  viewSeq += 1;
92439
92866
  const id = `seg${viewSeq}`;
92440
92867
  if (segment.kind === "text") {
92441
- const text = new otui.TextRenderable(renderer, {
92442
- id,
92443
- content: new otui.StyledText(markdownToChunks(otui, segment.text))
92444
- });
92445
- parent.add(text);
92868
+ let mounted = [];
92869
+ const paint = (source) => {
92870
+ for (const child of mounted) {
92871
+ try {
92872
+ parent.remove(child);
92873
+ child.destroyRecursively();
92874
+ } catch {}
92875
+ }
92876
+ mounted = [];
92877
+ const palette = deriveTranscriptPalette(getTheme());
92878
+ for (const [index, part] of parseMarkdownTables(source).entries()) {
92879
+ if (part.kind === "prose") {
92880
+ const text = new otui.TextRenderable(renderer, {
92881
+ id: `${id}-prose-${index}`,
92882
+ content: new otui.StyledText(markdownToChunks(otui, part.text, palette))
92883
+ });
92884
+ parent.add(text);
92885
+ mounted.push(text);
92886
+ continue;
92887
+ }
92888
+ const header = part.table.headers.map((cell) => [otui.bold(themedChunk(otui, cell, palette.tableHeader))]);
92889
+ const rows = part.table.rows.map((row) => row.map((cell) => markdownToChunks(otui, cell, palette)));
92890
+ const table = new otui.TextTableRenderable(renderer, {
92891
+ id: `${id}-table-${index}`,
92892
+ content: [header, ...rows],
92893
+ width: "100%",
92894
+ wrapMode: "word",
92895
+ columnWidthMode: "full",
92896
+ columnFitter: "balanced",
92897
+ cellPaddingX: 1,
92898
+ cellPaddingY: 0,
92899
+ showBorders: true,
92900
+ outerBorder: true,
92901
+ borderStyle: "rounded",
92902
+ borderColor: palette.tableBorder,
92903
+ fg: palette.prose,
92904
+ backgroundColor: getTheme().panel
92905
+ });
92906
+ parent.add(table);
92907
+ mounted.push(table);
92908
+ }
92909
+ };
92910
+ paint(segment.text);
92446
92911
  return {
92447
92912
  kind: "text",
92448
92913
  update: (next) => {
92449
92914
  if (next.kind === "text") {
92450
- text.content = new otui.StyledText(markdownToChunks(otui, next.text));
92915
+ paint(next.text);
92451
92916
  }
92452
92917
  },
92453
92918
  destroy: () => {
92454
- try {
92455
- parent.remove(text);
92456
- text.destroyRecursively();
92457
- } catch {}
92919
+ for (const child of mounted) {
92920
+ try {
92921
+ parent.remove(child);
92922
+ child.destroyRecursively();
92923
+ } catch {}
92924
+ }
92925
+ mounted = [];
92458
92926
  }
92459
92927
  };
92460
92928
  }
92461
92929
  const tag = (lang, body) => {
92462
92930
  const n = lineCountOf(body);
92463
- return `${lang.length > 0 ? lang : "text"} \xB7 ${n} ${n === 1 ? "line" : "lines"} \xB7 y copy`;
92931
+ return `${lang.length > 0 ? lang : "text"} \xB7 ${n} ${n === 1 ? "line" : "lines"} \xB7 ctrl+o then y copy`;
92464
92932
  };
92465
92933
  const frameWidth = (lang, body) => Math.max(hugWidth(body, FRAME_CHROME), hugWidth(tag(lang, body), FRAME_CHROME));
92466
92934
  const frame = new otui.BoxRenderable(renderer, {
@@ -92471,12 +92939,15 @@ function createSegmentView(otui, renderer, parent, segment) {
92471
92939
  borderStyle: "rounded",
92472
92940
  border: true,
92473
92941
  borderColor: frameColor(),
92942
+ backgroundColor: getTheme().panel,
92474
92943
  paddingLeft: 1,
92475
92944
  paddingRight: 1
92476
92945
  });
92477
92946
  const header = new otui.TextRenderable(renderer, {
92478
92947
  id: `${id}-tag`,
92479
- content: otui.t`${otui.dim(tag(segment.lang, segment.body))}`
92948
+ content: new otui.StyledText([
92949
+ themedChunk(otui, tag(segment.lang, segment.body), deriveTranscriptPalette(getTheme()).blockMeta)
92950
+ ])
92480
92951
  });
92481
92952
  const body = new otui.TextRenderable(renderer, {
92482
92953
  id: `${id}-body`,
@@ -92491,7 +92962,9 @@ function createSegmentView(otui, renderer, parent, segment) {
92491
92962
  if (next.kind !== "code") {
92492
92963
  return;
92493
92964
  }
92494
- header.content = otui.t`${otui.dim(tag(next.lang, next.body))}`;
92965
+ header.content = new otui.StyledText([
92966
+ themedChunk(otui, tag(next.lang, next.body), deriveTranscriptPalette(getTheme()).blockMeta)
92967
+ ]);
92495
92968
  body.content = new otui.StyledText(payloadChunks(otui, next.body, next.lang));
92496
92969
  frame.maxWidth = frameWidth(next.lang, next.body);
92497
92970
  },
@@ -95107,12 +95580,50 @@ function themeColorRemap(from, to) {
95107
95580
  remap.set(oldColor, newColor);
95108
95581
  }
95109
95582
  }
95583
+ const fromTranscript = deriveTranscriptPalette(from);
95584
+ const toTranscript = deriveTranscriptPalette(to);
95585
+ for (const slot of Object.keys(fromTranscript)) {
95586
+ const oldColor = fromTranscript[slot];
95587
+ const newColor = toTranscript[slot];
95588
+ if (oldColor !== newColor) {
95589
+ remap.set(oldColor, newColor);
95590
+ }
95591
+ }
95110
95592
  return remap;
95111
95593
  }
95112
- function recolorThemeTree(node, remap) {
95594
+ function recolorContent(value, remap, color) {
95595
+ if (Array.isArray(value)) {
95596
+ let changed = false;
95597
+ for (const item of value) {
95598
+ changed = recolorContent(item, remap, color) || changed;
95599
+ }
95600
+ return changed;
95601
+ }
95602
+ if (value === null || typeof value !== "object") {
95603
+ return false;
95604
+ }
95605
+ const target = value;
95606
+ let changed = false;
95607
+ if (typeof target.text === "string") {
95608
+ for (const prop of ["fg", "bg"]) {
95609
+ const hex = themeColorToHex(target[prop]);
95610
+ const next = hex === undefined ? undefined : remap.get(hex);
95611
+ if (next !== undefined) {
95612
+ target[prop] = color(next);
95613
+ changed = true;
95614
+ }
95615
+ }
95616
+ }
95617
+ if (target.chunks !== undefined) {
95618
+ changed = recolorContent(target.chunks, remap, color) || changed;
95619
+ }
95620
+ return changed;
95621
+ }
95622
+ function recolorThemeTree(node, fromTheme, toTheme, color = (hex) => hex) {
95113
95623
  if (node === null || node === undefined) {
95114
95624
  return;
95115
95625
  }
95626
+ const remap = themeColorRemap(fromTheme, toTheme);
95116
95627
  const target = node;
95117
95628
  for (const prop of ["borderColor", "backgroundColor", "fg"]) {
95118
95629
  const hex = themeColorToHex(target[prop]);
@@ -95123,9 +95634,13 @@ function recolorThemeTree(node, remap) {
95123
95634
  }
95124
95635
  }
95125
95636
  }
95637
+ const content = target.content;
95638
+ if (content !== undefined && recolorContent(content, remap, color)) {
95639
+ target.content = content;
95640
+ }
95126
95641
  if (typeof target.getChildren === "function") {
95127
95642
  for (const child of target.getChildren()) {
95128
- recolorThemeTree(child, remap);
95643
+ recolorThemeTree(child, fromTheme, toTheme, color);
95129
95644
  }
95130
95645
  }
95131
95646
  }
@@ -95745,7 +96260,6 @@ async function createShellChrome(otui, renderer, opts) {
95745
96260
  }
95746
96261
  });
95747
96262
  const applyTheme = (theme = getTheme()) => {
95748
- const remap = themeColorRemap(appliedTheme, theme);
95749
96263
  try {
95750
96264
  r.setBackgroundColor(theme.bg);
95751
96265
  } catch {}
@@ -95770,14 +96284,15 @@ async function createShellChrome(otui, renderer, opts) {
95770
96284
  menu.selectedTextColor = theme.focus;
95771
96285
  menu.descriptionColor = theme.muted;
95772
96286
  menu.selectedDescriptionColor = theme.muted;
95773
- recolorThemeTree(transcript, remap);
95774
- recolorThemeTree(dock, remap);
95775
- recolorThemeTree(queueDock, remap);
95776
- recolorThemeTree(sidebarTop, remap);
95777
- recolorThemeTree(menu, remap);
95778
- recolorThemeTree(composer, remap);
95779
- recolorThemeTree(header, remap);
95780
- recolorThemeTree(footer, remap);
96287
+ const chunkColor = (hex) => otui.RGBA.fromHex(hex);
96288
+ recolorThemeTree(transcript, appliedTheme, theme, chunkColor);
96289
+ recolorThemeTree(dock, appliedTheme, theme, chunkColor);
96290
+ recolorThemeTree(queueDock, appliedTheme, theme, chunkColor);
96291
+ recolorThemeTree(sidebarTop, appliedTheme, theme, chunkColor);
96292
+ recolorThemeTree(menu, appliedTheme, theme, chunkColor);
96293
+ recolorThemeTree(composer, appliedTheme, theme, chunkColor);
96294
+ recolorThemeTree(header, appliedTheme, theme, chunkColor);
96295
+ recolorThemeTree(footer, appliedTheme, theme, chunkColor);
95781
96296
  appliedTheme = theme;
95782
96297
  };
95783
96298
  const unsubTheme = onThemeChange((theme) => applyTheme(theme));
@@ -102363,6 +102878,96 @@ function paintBackgroundJobSidebar(otui, renderer, parent, entries, options) {
102363
102878
  }
102364
102879
  }
102365
102880
 
102881
+ // src/tui/execution-plan-panel.ts
102882
+ init_execution_plan();
102883
+ var GLYPHS = {
102884
+ completed: "\u2713",
102885
+ in_progress: "\u25B6",
102886
+ pending: "\u25CB",
102887
+ blocked: "!",
102888
+ skipped: "\u2212"
102889
+ };
102890
+ function truncate7(text, width) {
102891
+ if (width <= 0)
102892
+ return "";
102893
+ if (displayWidth(text) <= width)
102894
+ return text;
102895
+ if (width === 1)
102896
+ return "\u2026";
102897
+ let prefix = "";
102898
+ for (const character of text) {
102899
+ if (displayWidth(prefix + character) > width - 1)
102900
+ break;
102901
+ prefix += character;
102902
+ }
102903
+ return `${prefix}\u2026`;
102904
+ }
102905
+ function projectExecutionPlanPanel(plan, options) {
102906
+ if (plan === undefined || plan.items.length === 0) {
102907
+ return { visible: false, rows: [], revision: undefined };
102908
+ }
102909
+ const size = Math.max(1, Math.min(options.maxRows ?? 7, 7, plan.items.length));
102910
+ const active = Math.max(0, plan.items.findIndex((item) => item.status === "in_progress"));
102911
+ const start = Math.max(0, Math.min(active - Math.floor(size / 2), plan.items.length - size));
102912
+ const rows = plan.items.slice(start, start + size).map((item) => {
102913
+ const glyph = GLYPHS[item.status];
102914
+ return { id: item.id, status: item.status, glyph, text: truncate7(`${glyph} ${item.title}`, options.width) };
102915
+ });
102916
+ return { visible: true, rows, revision: plan.revision };
102917
+ }
102918
+ function mountExecutionPlanPanel(otui, renderer, parent, options) {
102919
+ const box = parent;
102920
+ let paintedDir;
102921
+ let paintedRevision;
102922
+ let refreshGeneration = 0;
102923
+ let disposed = false;
102924
+ const paint = (plan, dir) => {
102925
+ const projected = projectExecutionPlanPanel(plan, {
102926
+ width: options.width,
102927
+ ...options.maxRows === undefined ? {} : { maxRows: options.maxRows }
102928
+ });
102929
+ if (paintedDir === dir && paintedRevision === projected.revision)
102930
+ return;
102931
+ paintedDir = dir;
102932
+ paintedRevision = projected.revision;
102933
+ if (box.getChildren !== undefined && box.remove !== undefined) {
102934
+ for (const child of [...box.getChildren()])
102935
+ box.remove(child);
102936
+ }
102937
+ const Ctor = otui.TextRenderable;
102938
+ if (!projected.visible || box.add === undefined || Ctor === undefined)
102939
+ return;
102940
+ box.add(new Ctor(renderer, { id: "sb-plan-h", content: "Plan", marginTop: 1 }));
102941
+ for (const row of projected.rows)
102942
+ box.add(new Ctor(renderer, { id: `sb-plan-${row.id}`, content: row.text }));
102943
+ };
102944
+ const refresh = async () => {
102945
+ const dir = options.getSessionDir();
102946
+ const generation = ++refreshGeneration;
102947
+ const plan = dir === undefined ? undefined : await getExecutionPlan(dir).catch(() => {
102948
+ return;
102949
+ });
102950
+ if (disposed || generation !== refreshGeneration || dir !== options.getSessionDir())
102951
+ return;
102952
+ paint(plan, dir);
102953
+ };
102954
+ const unsubscribe = subscribeExecutionPlans((dir, plan) => {
102955
+ if (dir === options.getSessionDir()) {
102956
+ refreshGeneration += 1;
102957
+ paint(plan, dir);
102958
+ }
102959
+ });
102960
+ refresh();
102961
+ return {
102962
+ refresh,
102963
+ dispose: () => {
102964
+ disposed = true;
102965
+ refreshGeneration += 1;
102966
+ unsubscribe();
102967
+ }
102968
+ };
102969
+ }
102970
+
102366
102971
  // src/tui/bus-command.ts
102367
102972
  var SEND_USAGE = "usage: /bus send @<name> <text> (or /bus @<name> <text>)";
102368
102973
  var ASK_USAGE = "usage: /bus ask @<name> <text>";
@@ -104346,6 +104951,7 @@ async function launchTuiAgentShell(opts) {
104346
104951
  let mountedChrome;
104347
104952
  let liveDeps;
104348
104953
  let liveJobs;
104954
+ let disposeExecutionPlanPanel;
104349
104955
  let detachExternal;
104350
104956
  let liveBus;
104351
104957
  let destroyed = false;
@@ -104368,6 +104974,7 @@ async function launchTuiAgentShell(opts) {
104368
104974
  try {
104369
104975
  const r = renderer = await createShellRenderer(otui, {
104370
104976
  onDestroy: () => {
104977
+ disposeExecutionPlanPanel?.();
104371
104978
  destroyed = true;
104372
104979
  foregroundOperation.cancel("renderer destroyed");
104373
104980
  foregroundOperation.dispose();
@@ -104407,6 +105014,7 @@ async function launchTuiAgentShell(opts) {
104407
105014
  saveShellConfig(sel.baseUrl === undefined ? { provider: sel.provider, model: sel.model } : { provider: sel.provider, model: sel.model, baseUrl: sel.baseUrl });
104408
105015
  let currentSel = sel;
104409
105016
  const suggestionGate = new NextStepSuggestionGate;
105017
+ let slateSession;
104410
105018
  const liveSlateSession = () => whilePersisting(slateSession, () => sessionLease.canPersist());
104411
105019
  let deps = await opts.makeAgentDeps(sel, liveSlateSession, busClientRef);
104412
105020
  liveDeps = deps;
@@ -104550,6 +105158,18 @@ async function launchTuiAgentShell(opts) {
104550
105158
  marginTop: 1
104551
105159
  });
104552
105160
  sidebar.add(sbSubagents);
105161
+ const sbPlan = new otui.BoxRenderable(r, {
105162
+ id: "sb-plan",
105163
+ flexDirection: "column",
105164
+ flexShrink: 0
105165
+ });
105166
+ sidebar.add(sbPlan);
105167
+ const executionPlanPanel = mountExecutionPlanPanel(otui, r, sbPlan, {
105168
+ getSessionDir: () => liveSlateSession()?.dir,
105169
+ width: SIDEBAR_TEXT_WIDTH,
105170
+ maxRows: 7
105171
+ });
105172
+ disposeExecutionPlanPanel = executionPlanPanel.dispose;
104553
105173
  const sbJobs = new otui.BoxRenderable(r, {
104554
105174
  id: "sb-jobs",
104555
105175
  flexDirection: "column",
@@ -105134,9 +105754,9 @@ async function launchTuiAgentShell(opts) {
105134
105754
  let archive = [];
105135
105755
  let nextArchiveIndex = 0;
105136
105756
  let sessionPersistTimer;
105137
- let slateSession;
105138
105757
  const bindSlateToLiveSession = () => {
105139
105758
  slateSession = freshSlateSessionRef(liveSession.dir, sessionCwd);
105759
+ executionPlanPanel.refresh();
105140
105760
  };
105141
105761
  const onContextCompaction = (r) => {
105142
105762
  if (!sessionLease.canPersist()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.2.140",
3
+ "version": "0.2.143",
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": {