@wrongstack/tui 0.303.0 → 0.305.0

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.
@@ -85,7 +85,7 @@ export type Settings = {
85
85
  * live-settings coercion always fills in a full map.
86
86
  */
87
87
  panelPositions?: Partial<PanelPositionMap>;
88
- /** Show SAGE Memory Inject blocks in tool results. Default: false (hidden). */
88
+ /** Show SAGE Memory Inject chips in tool results. Default: true (compact). */
89
89
  showSageMemoryInject: boolean;
90
90
  /** Minimum relation strength for SAGE memory injection. Default: 0.85. */
91
91
  sageMemoryInjectThreshold: number;
@@ -38,6 +38,15 @@ export declare function computeBoardColumnLayout(totalColumns: number, terminalW
38
38
  columnWidth: number;
39
39
  overflow: number;
40
40
  };
41
+ /**
42
+ * The next stage a managed card may advance to, or null when the card is
43
+ * already at the terminal `done` stage (or the current stage is unknown).
44
+ * The domain only allows one-step forward transitions; callers must keep
45
+ * this in lockstep with `KANBAN_AGENT_STAGES` in `@wrongstack/kanban`.
46
+ *
47
+ * Exported so unit tests can pin the stage order without rendering the panel.
48
+ */
49
+ export declare function nextManagedStage(current: KanbanLifecycleStage): KanbanLifecycleStage | null;
41
50
  /**
42
51
  * Look up the lifecycle stage that owns the given column under a managed
43
52
  * Kanban Agent board. Returns null when the board is not managed, or when
@@ -58,9 +58,8 @@ export declare const MULTI_DIFF_SUMMARY_THRESHOLD_PRESETS: number[];
58
58
  export declare const ENHANCE_LANGUAGES: readonly ['original', 'english'];
59
59
  export type EnhanceLanguage = (typeof ENHANCE_LANGUAGES)[number];
60
60
  /** Token-saving tier options — cyclable via ←/→ in the settings picker.
61
- * `auto` is the default: it keeps all tools and picks a leaner PROMPT only on
62
- * small context windows (cache-safe). The concrete tiers below force a fixed
63
- * level regardless of window. */
61
+ * `auto` is the default: it keeps the executable catalog, uses the medium
62
+ * direct surface, and adapts prompt trimming to the model window. */
64
63
  export declare const TOKEN_SAVING_TIERS: readonly ['auto', 'off', 'minimal', 'light', 'medium', 'aggressive'];
65
64
  export type TokenSavingTierTui = (typeof TOKEN_SAVING_TIERS)[number];
66
65
  export declare const TOKEN_SAVING_TIER_DESCS: Record<TokenSavingTierTui, string>;
@@ -4,6 +4,7 @@ export interface ToolPickerItem {
4
4
  owner: string;
5
5
  category: string;
6
6
  enabled: boolean;
7
+ exposure: 'direct' | 'lazy' | 'disabled';
7
8
  mutating: boolean;
8
9
  permission: string;
9
10
  descMode: 'extend' | 'simple';
package/dist/index.js CHANGED
@@ -5081,7 +5081,8 @@ function buildTodoPreviewRows(todos, width, maxRows = 2) {
5081
5081
  const shown = ordered.slice(0, maxRows);
5082
5082
  const textWidth = Math.max(4, width - 5);
5083
5083
  const rows = shown.map((todo) => {
5084
- const label = todo.status === "in_progress" && todo.activeForm ? todo.activeForm : todo.content;
5084
+ const base = todo.status === "in_progress" && todo.activeForm ? todo.activeForm : todo.content;
5085
+ const label = todo.blockedBy?.[0] ? `${base} \u2014 waiting on ${todo.blockedBy[0]}` : base;
5085
5086
  if (todo.status === "completed") {
5086
5087
  return {
5087
5088
  id: todo.id,
@@ -6141,6 +6142,7 @@ import {
6141
6142
  moveTask,
6142
6143
  removeTask,
6143
6144
  transferTaskToBoard,
6145
+ transitionTask,
6144
6146
  updateTask
6145
6147
  } from "@wrongstack/kanban";
6146
6148
  import { applySessionKanbanTaskToSource } from "@wrongstack/tools/session-kanban";
@@ -6149,6 +6151,7 @@ import { useEffect as useEffect9, useMemo as useMemo4, useState as useState10 }
6149
6151
  // src/kanban-audit.ts
6150
6152
  var DEFAULT_REVIEW_STALE_AFTER_MS = 72 * 60 * 60 * 1e3;
6151
6153
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "archived"]);
6154
+ var BOARD_SOFT_MAX_BYTES = 512 * 1024;
6152
6155
  function auditKanbanBoard(board, options) {
6153
6156
  const now = toEpoch(options.now);
6154
6157
  if (now === null) throw new TypeError("Kanban audit requires a valid current time");
@@ -6160,15 +6163,16 @@ function auditKanbanBoard(board, options) {
6160
6163
  );
6161
6164
  const lifecycleOrder = resolveLifecycleOrder(auditableBoard);
6162
6165
  const managed = isManagedBoard(auditableBoard);
6163
- const requireDueDate = managed || options.requireDueDate === true;
6166
+ const requireDueDate = options.requireDueDate === true;
6164
6167
  const issues = [];
6165
6168
  for (const task of board.tasks) {
6166
- if (lifecycleOrder.length > 1) addLifecycleIssue(issues, task, lifecycleOrder);
6167
6169
  if (TERMINAL_STATUSES.has(task.status)) continue;
6168
6170
  addRequiredDetailIssues(issues, task, requireDueDate, managed);
6169
6171
  addRunningIssue(issues, task, now, liveIdentities);
6170
6172
  addReviewIssue(issues, task, now, reviewStaleAfterMs);
6173
+ if (lifecycleOrder.length > 1) addLifecycleIssue(issues, task, lifecycleOrder);
6171
6174
  }
6175
+ addBoardSizeIssue(issues, board);
6172
6176
  issues.sort(compareIssues);
6173
6177
  return {
6174
6178
  generatedAt: new Date(now).toISOString(),
@@ -6209,7 +6213,7 @@ function addRequiredDetailIssues(issues, task, requireDueDate, managed) {
6209
6213
  if (!hasAssignee) {
6210
6214
  pushIssue(issues, task, "missing-assignee", "warning", "Assign an owner");
6211
6215
  }
6212
- if (requireDueDate && (!hasText(task.dueDate) || !Number.isFinite(Date.parse(task.dueDate)))) {
6216
+ if (requireDueDate && toEpoch(task.dueDate) === null) {
6213
6217
  pushIssue(issues, task, "missing-due-date", "warning", "Set a valid due date");
6214
6218
  }
6215
6219
  if (!hasTextItem(task.labels) && (managed || !hasTextItem(task.tags))) {
@@ -6234,6 +6238,25 @@ function addRequiredDetailIssues(issues, task, requireDueDate, managed) {
6234
6238
  pushIssue(issues, task, "missing-success-criteria", "warning", "Add success criteria");
6235
6239
  }
6236
6240
  }
6241
+ function addBoardSizeIssue(issues, board) {
6242
+ const bytes = boardByteSize(board);
6243
+ if (bytes <= BOARD_SOFT_MAX_BYTES) return;
6244
+ issues.push({
6245
+ id: `${board.id}:board-oversized`,
6246
+ taskId: board.id,
6247
+ taskTitle: board.title,
6248
+ code: "board-oversized",
6249
+ severity: "warning",
6250
+ message: `Board is ~${Math.round(bytes / 1024)} KB across ${board.tasks.length} cards; archive completed cards before it stops syncing to HQ`
6251
+ });
6252
+ }
6253
+ function boardByteSize(board) {
6254
+ try {
6255
+ return JSON.stringify(board)?.length ?? 0;
6256
+ } catch {
6257
+ return 0;
6258
+ }
6259
+ }
6237
6260
  function addRunningIssue(issues, task, now, liveIdentities) {
6238
6261
  const assignment = task.assignment;
6239
6262
  const running = task.status === "in_progress" || assignment?.status === "running";
@@ -6390,7 +6413,7 @@ function normalizeIdentities(values) {
6390
6413
  return normalized;
6391
6414
  }
6392
6415
  function normalizeIdentity(value) {
6393
- return value.trim().toLocaleLowerCase();
6416
+ return value.trim().toLowerCase();
6394
6417
  }
6395
6418
  function firstText(...values) {
6396
6419
  for (const value of values) {
@@ -6536,6 +6559,25 @@ function KanbanPanel({
6536
6559
  });
6537
6560
  return;
6538
6561
  }
6562
+ if (prompt.kind === "transitionTask") {
6563
+ const comment = prompt.buffer.trim();
6564
+ if (!comment) return;
6565
+ const target = prompt.to;
6566
+ const task = prompt.task;
6567
+ setPrompt(null);
6568
+ await runMutation(async () => {
6569
+ if (!board) return null;
6570
+ const actor = sessionId ?? "tui-operator";
6571
+ const result = await transitionTask(projectRoot, board.id, task.id, {
6572
+ to: target,
6573
+ actor,
6574
+ comment
6575
+ });
6576
+ if (result) await syncSource(result.board, task.id, false, task);
6577
+ return result ? `Transitioned "${task.title}" \u2192 ${target}` : "Transition failed (task or board not found)";
6578
+ });
6579
+ return;
6580
+ }
6539
6581
  const value = prompt.buffer.trim();
6540
6582
  if (!value) return;
6541
6583
  const kind = prompt.kind;
@@ -6635,28 +6677,46 @@ function KanbanPanel({
6635
6677
  return;
6636
6678
  }
6637
6679
  if (key.rightArrow && board && activeTask) {
6638
- const nextColumn = adjacentColumn(sortedColumns, activeTask.columnId, 1);
6639
- if (nextColumn) {
6640
- void runMutation(async () => {
6641
- const nextBoard2 = await moveTask(projectRoot, board.id, activeTask.id, nextColumn.id);
6642
- await syncSource(nextBoard2, activeTask.id);
6643
- return `Moved to ${nextColumn.title}`;
6644
- });
6680
+ if (isManagedBoard2(board)) {
6681
+ const current = stageForColumn(board, activeTask.columnId);
6682
+ const target = current ? nextManagedStage(current) : null;
6683
+ if (target) {
6684
+ setPrompt({ kind: "transitionTask", task: activeTask, to: target, buffer: "" });
6685
+ } else {
6686
+ setNotice("Task is already at the final stage (done).");
6687
+ }
6688
+ } else {
6689
+ const nextColumn = adjacentColumn(sortedColumns, activeTask.columnId, 1);
6690
+ if (nextColumn) {
6691
+ void runMutation(async () => {
6692
+ const nextBoard2 = await moveTask(projectRoot, board.id, activeTask.id, nextColumn.id);
6693
+ await syncSource(nextBoard2, activeTask.id);
6694
+ return `Moved to ${nextColumn.title}`;
6695
+ });
6696
+ }
6645
6697
  }
6646
6698
  return;
6647
6699
  }
6648
6700
  if (key.leftArrow && board && activeTask) {
6649
- const prevColumn = adjacentColumn(sortedColumns, activeTask.columnId, -1);
6650
- if (prevColumn) {
6651
- void runMutation(async () => {
6652
- const nextBoard2 = await moveTask(projectRoot, board.id, activeTask.id, prevColumn.id);
6653
- await syncSource(nextBoard2, activeTask.id);
6654
- return `Moved to ${prevColumn.title}`;
6655
- });
6701
+ if (isManagedBoard2(board)) {
6702
+ setNotice("Managed boards advance forward only; use the kanban tool to repair backward.");
6703
+ } else {
6704
+ const prevColumn = adjacentColumn(sortedColumns, activeTask.columnId, -1);
6705
+ if (prevColumn) {
6706
+ void runMutation(async () => {
6707
+ const nextBoard2 = await moveTask(projectRoot, board.id, activeTask.id, prevColumn.id);
6708
+ await syncSource(nextBoard2, activeTask.id);
6709
+ return `Moved to ${prevColumn.title}`;
6710
+ });
6711
+ }
6656
6712
  }
6657
6713
  return;
6658
6714
  }
6659
6715
  if (shortcutsEnabled && (input === " " || input === "D") && board && activeTask) {
6716
+ if (isManagedBoard2(board)) {
6717
+ setNotice("Managed cards advance via \u2192 or t (transition). Use a non-managed board for direct status edits.");
6718
+ return;
6719
+ }
6660
6720
  void runMutation(async () => {
6661
6721
  const nextBoard2 = await updateTask(projectRoot, board.id, activeTask.id, {
6662
6722
  status: "completed",
@@ -6668,6 +6728,10 @@ function KanbanPanel({
6668
6728
  return;
6669
6729
  }
6670
6730
  if (shortcutsEnabled && input === "b" && board && activeTask) {
6731
+ if (isManagedBoard2(board)) {
6732
+ setNotice('Managed cards advance via \u2192 or t (transition); "blocked" is a status, not a lifecycle stage.');
6733
+ return;
6734
+ }
6671
6735
  void runMutation(async () => {
6672
6736
  const nextBoard2 = await updateTask(projectRoot, board.id, activeTask.id, {
6673
6737
  status: "blocked"
@@ -6677,6 +6741,16 @@ function KanbanPanel({
6677
6741
  });
6678
6742
  return;
6679
6743
  }
6744
+ if (shortcutsEnabled && input === "t" && board && activeTask && isManagedBoard2(board)) {
6745
+ const current = stageForColumn(board, activeTask.columnId);
6746
+ const target = current ? nextManagedStage(current) : null;
6747
+ if (!target) {
6748
+ setNotice("Task is already at the final stage (done).");
6749
+ } else {
6750
+ setPrompt({ kind: "transitionTask", task: activeTask, to: target, buffer: "" });
6751
+ }
6752
+ return;
6753
+ }
6680
6754
  if (shortcutsEnabled && input === "x" && activeTask) {
6681
6755
  setPrompt({ kind: "confirmDeleteTask", task: activeTask });
6682
6756
  return;
@@ -6766,6 +6840,22 @@ function PromptLine({ prompt }) {
6766
6840
  '"? y/n'
6767
6841
  ] });
6768
6842
  }
6843
+ if (prompt.kind === "transitionTask") {
6844
+ return /* @__PURE__ */ jsxs14(Text, { children: [
6845
+ /* @__PURE__ */ jsxs14(Text, { color: "magenta", children: [
6846
+ prompt.to.toUpperCase(),
6847
+ ' "',
6848
+ prompt.task.title,
6849
+ '":',
6850
+ " "
6851
+ ] }),
6852
+ prompt.buffer,
6853
+ /* @__PURE__ */ jsxs14(Text, { dimColor: true, children: [
6854
+ "_ ",
6855
+ /* @__PURE__ */ jsx15(Text, { dimColor: true, children: "(Esc cancel \xB7 Enter confirm \u2014 comment required)" })
6856
+ ] })
6857
+ ] });
6858
+ }
6769
6859
  const label = prompt.kind === "createBoard" ? "New board title" : "New task title";
6770
6860
  return /* @__PURE__ */ jsxs14(Text, { children: [
6771
6861
  /* @__PURE__ */ jsxs14(Text, { color: "cyan", children: [
@@ -6882,6 +6972,20 @@ function BoardColumns({
6882
6972
  ] }) : null
6883
6973
  ] });
6884
6974
  }
6975
+ function isManagedBoard2(board) {
6976
+ return board?.lifecycle?.mode === "managed";
6977
+ }
6978
+ var MANAGED_STAGE_ORDER = [
6979
+ "backlog",
6980
+ "todo",
6981
+ "running",
6982
+ "review",
6983
+ "done"
6984
+ ];
6985
+ function nextManagedStage(current) {
6986
+ const idx = MANAGED_STAGE_ORDER.indexOf(current);
6987
+ return idx >= 0 && idx < MANAGED_STAGE_ORDER.length - 1 ? MANAGED_STAGE_ORDER[idx + 1] : null;
6988
+ }
6885
6989
  function stageForColumn(board, columnId) {
6886
6990
  const policy = board.lifecycle;
6887
6991
  if (policy?.mode !== "managed") return null;
@@ -6977,7 +7081,7 @@ function TaskDetail({
6977
7081
  "C/T target: ",
6978
7082
  target.title
6979
7083
  ] }) : null,
6980
- /* @__PURE__ */ jsx15(Text, { dimColor: true, children: "space done | b block | x delete" })
7084
+ /* @__PURE__ */ jsx15(Text, { dimColor: true, children: board.lifecycle?.mode === "managed" ? "\u2192/t transition | x delete" : "space done | b block | x delete" })
6981
7085
  ] })
6982
7086
  ] });
6983
7087
  }
@@ -17212,22 +17316,6 @@ function estimateTextRows(text, contentWidth, maxRows) {
17212
17316
  rows += Math.max(1, Math.ceil(displayWidth(safeText.slice(lineStart)) / width));
17213
17317
  return Math.min(maxRows, Math.max(1, rows));
17214
17318
  }
17215
- function estimateSageKvRows(cells, contentWidth) {
17216
- if (cells.length === 0) return 0;
17217
- const gap = 2;
17218
- let rows = 1;
17219
- let usedColumns = 0;
17220
- for (const cell of cells) {
17221
- const cellWidth = displayWidth(cell) + gap;
17222
- if (usedColumns > 0 && usedColumns + cellWidth > contentWidth) {
17223
- rows += 1;
17224
- usedColumns = cellWidth;
17225
- } else {
17226
- usedColumns += cellWidth;
17227
- }
17228
- }
17229
- return rows;
17230
- }
17231
17319
  function estimateRenderGroupRows(group, contentWidth, showSageMemoryInject) {
17232
17320
  if (group.type === "tool-group") return group.data.entries.length + 2;
17233
17321
  const { entry } = group;
@@ -17235,34 +17323,13 @@ function estimateRenderGroupRows(group, contentWidth, showSageMemoryInject) {
17235
17323
  const { cleanOutput, sageLines } = resolveEntrySage(entry.output, entry.sageLines);
17236
17324
  const memoryLines = sageLines.slice(1);
17237
17325
  const panelContentWidth = Math.max(1, contentWidth - 4);
17238
- const sagePanelRows = showSageMemoryInject === false || memoryLines.length === 0 ? 0 : 2 + estimateTextRows(
17239
- `\u{1F9E0} SAGE MEMORY INJECTED \xB7 ${entry.name} ${memoryLines.length} ${memoryLines.length === 1 ? "memory" : "memories"}${entry.sageStats ? ` \xB7 ${entry.sageStats}` : ""}`,
17240
- panelContentWidth,
17241
- MAX_TEXT_ESTIMATE_ROWS
17242
- ) + memoryLines.reduce((rows, line) => {
17326
+ const sagePanelRows = showSageMemoryInject === false || memoryLines.length === 0 ? 0 : 4 + memoryLines.reduce((sum, line) => {
17243
17327
  const parsed = parseSageMemoryLine(line);
17244
- if (!parsed) {
17245
- return rows + estimateTextRows(line, panelContentWidth, MAX_TEXT_ESTIMATE_ROWS);
17246
- }
17247
- const labelRow = estimateTextRows(
17248
- parsed.labels.length > 0 ? parsed.labels.map((l) => `[${l}]`).join("") : "[memory]",
17249
- panelContentWidth,
17250
- MAX_TEXT_ESTIMATE_ROWS
17251
- );
17252
- const bodyRow = estimateTextRows(
17253
- parsed.text,
17254
- panelContentWidth,
17255
- MAX_TEXT_ESTIMATE_ROWS
17256
- );
17257
- const kvCells = [];
17258
- kvCells.push(`id: ${parsed.id}`);
17259
- if (parsed.anchor) kvCells.push(`anchor: ${parsed.anchor}`);
17260
- if (parsed.relation) kvCells.push(`relation: ${parsed.relation}`);
17261
- if (parsed.tags && parsed.tags.length > 0)
17262
- kvCells.push(`tags: ${parsed.tags.join(", ")}`);
17263
- const kvRows = estimateSageKvRows(kvCells, panelContentWidth);
17264
- const gap = rows > 0 ? 1 : 0;
17265
- return rows + labelRow + bodyRow + kvRows + gap;
17328
+ const bodySource = parsed ? parsed.text : line;
17329
+ const bodyWidth = displayWidth(sanitizeTerminalText(bodySource));
17330
+ const bodyRows = Math.max(1, Math.ceil(bodyWidth / panelContentWidth));
17331
+ const kvRows = panelContentWidth < 48 ? 2 : 1;
17332
+ return sum + 1 + bodyRows + kvRows;
17266
17333
  }, 0);
17267
17334
  if (entry.resultRenderMode === "simple") return 3 + sagePanelRows;
17268
17335
  if (STRUCTURED_DIFF_TOOLS.has(entry.name)) {
@@ -19025,11 +19092,23 @@ var Entry = React4.memo(function Entry2({
19025
19092
  function SageMemoryBlock({
19026
19093
  sageLines,
19027
19094
  toolName,
19028
- stats
19095
+ stats,
19096
+ compact = false
19029
19097
  }) {
19030
19098
  if (!sageLines || sageLines.length < 2) return null;
19031
19099
  const memoryLines = sageLines.slice(1);
19032
19100
  if (memoryLines.length === 0) return null;
19101
+ if (compact) {
19102
+ const first = parseSageMemoryLine(memoryLines[0] ?? "");
19103
+ const preview = first ? sanitizeTerminalText(first.text).slice(0, 72) : sanitizeTerminalText(memoryLines[0] ?? "").slice(0, 72);
19104
+ const labels = first?.labels?.length ? first.labels.map((l) => `[${l}]`).join("") : "";
19105
+ return /* @__PURE__ */ jsx68(Box, { flexDirection: "row", marginTop: 0, children: /* @__PURE__ */ jsxs65(Text, { color: theme.accent, children: [
19106
+ `\u{1F9E0} ${memoryLines.length} SAGE \xB7 ${toolName}`,
19107
+ stats ? ` \xB7 ${stats}` : "",
19108
+ labels ? ` ${labels}` : "",
19109
+ preview ? ` \u2014 ${preview}${preview.length >= 72 ? "\u2026" : ""}` : ""
19110
+ ] }) });
19111
+ }
19033
19112
  return /* @__PURE__ */ jsxs65(
19034
19113
  Box,
19035
19114
  {
@@ -20237,12 +20316,12 @@ var TOKEN_SAVING_TIERS = [
20237
20316
  "aggressive"
20238
20317
  ];
20239
20318
  var TOKEN_SAVING_TIER_DESCS = {
20240
- auto: "By window \u2014 full tools, lean prompt on small models (cache-safe)",
20241
- off: "All tools enabled (full prompt)",
20242
- minimal: "~2.5\u20132.7k tokens \u2014 core tools only",
20243
- light: "~2.3\u20132.4k tokens \u2014 core + patterns",
20244
- medium: "~1.4\u20131.5k tokens \u2014 most tools enabled",
20245
- aggressive: "~1k tokens \u2014 many tools, trimmed prompt"
20319
+ auto: "Medium direct surface; prompt adapts to the model window",
20320
+ off: "Every enabled catalog tool is sent directly; full prompt",
20321
+ minimal: "Essential direct tools; compact prompt and guidance",
20322
+ light: "Essential direct tools; retains common workflow guidance",
20323
+ medium: "Essential + regular development tools; moderate trimming",
20324
+ aggressive: "Smallest direct surface and most compact prompt"
20246
20325
  };
20247
20326
  var FLEET_CHAT_MODES = ["off", "full"];
20248
20327
  var FLEET_CHAT_MODE_DESCS = {
@@ -20815,7 +20894,7 @@ var SETTINGS_DEFAULTS = Object.freeze({
20815
20894
  showAgentSwarmPanel: "bottom",
20816
20895
  panelPositions: DEFAULT_PANEL_POSITIONS,
20817
20896
  readSymbols: false,
20818
- showSageMemoryInject: false,
20897
+ showSageMemoryInject: true,
20819
20898
  sageMemoryInjectThreshold: 0.85,
20820
20899
  nextStepsTool: false
20821
20900
  });
@@ -21612,11 +21691,15 @@ function buildMissionRows(todos, maxRows) {
21612
21691
  const rank = (t) => t.status === "in_progress" ? 0 : t.status === "pending" ? 1 : 2;
21613
21692
  const ordered = [...list].sort((a, b) => rank(a) - rank(b));
21614
21693
  const shown = ordered.slice(0, maxRows);
21615
- const rows = shown.map((t) => ({
21616
- id: t.id,
21617
- status: t.status,
21618
- label: t.status === "in_progress" && t.activeForm ? t.activeForm : t.content
21619
- }));
21694
+ const rows = shown.map((t) => {
21695
+ const base = t.status === "in_progress" && t.activeForm ? t.activeForm : t.content;
21696
+ const blocker = t.blockedBy?.[0];
21697
+ return {
21698
+ id: t.id,
21699
+ status: t.status,
21700
+ label: blocker ? `${base} \u2014 waiting on ${blocker}` : base
21701
+ };
21702
+ });
21620
21703
  return { rows, overflow: ordered.length - shown.length, done, total };
21621
21704
  }
21622
21705
  function DialRow({
@@ -22567,7 +22650,8 @@ function TodosPanelSidebar({ todos, width }) {
22567
22650
  children: /* @__PURE__ */ jsx79(SidebarPanelCard, { innerWidth: inner, marginBottom: 0, children: ordered.length === 0 ? /* @__PURE__ */ jsx79(Text, { color: theme.textMuted, children: "no todos" }) : ordered.map((t) => {
22568
22651
  const icon = t.status === "completed" ? glyphs.success : t.status === "in_progress" ? "\u25CF" : "\u25CB";
22569
22652
  const color = t.status === "completed" ? theme.success : t.status === "in_progress" ? theme.accent : theme.textMuted;
22570
- const label = t.status === "in_progress" && t.activeForm ? t.activeForm : t.content;
22653
+ const base = t.status === "in_progress" && t.activeForm ? t.activeForm : t.content;
22654
+ const label = t.blockedBy?.[0] ? `${base} \u2014 waiting on ${t.blockedBy[0]}` : base;
22571
22655
  return /* @__PURE__ */ jsx79(
22572
22656
  SidebarWorklistRow,
22573
22657
  {
@@ -23492,10 +23576,12 @@ import { Fragment as Fragment30, jsx as jsx84, jsxs as jsxs81 } from "react/jsx-
23492
23576
  var CHROME_ROWS6 = 15;
23493
23577
  var MAX_PICKER_ITEMS2 = 15;
23494
23578
  function toolActionLabel(enabled) {
23495
- return enabled ? "Enter makes this tool passive" : "Enter re-enables this tool";
23579
+ return enabled ? "Enter disables this tool" : "Enter re-enables this tool";
23496
23580
  }
23497
- function statusBadge2(enabled) {
23498
- return enabled ? /* @__PURE__ */ jsx84(Text, { color: "green", children: "\u25CF active" }) : /* @__PURE__ */ jsx84(Text, { color: "red", children: "\u25CB passive" });
23581
+ function statusBadge2(exposure) {
23582
+ if (exposure === "direct") return /* @__PURE__ */ jsx84(Text, { color: "green", children: "\u25CF direct" });
23583
+ if (exposure === "lazy") return /* @__PURE__ */ jsx84(Text, { color: "cyan", children: "\u25C7 lazy" });
23584
+ return /* @__PURE__ */ jsx84(Text, { color: "red", children: "\u25CB disabled" });
23499
23585
  }
23500
23586
  function rwBadge(mutating) {
23501
23587
  return mutating ? /* @__PURE__ */ jsx84(Text, { color: "yellow", children: "mut" }) : /* @__PURE__ */ jsx84(Text, { color: "cyan", children: "ro" });
@@ -23589,8 +23675,9 @@ function ToolsPicker({
23589
23675
  visibleSelected = selected;
23590
23676
  }
23591
23677
  const rows = buildRows2(visibleItems);
23592
- const enabledCount = items.filter((item) => item.enabled).length;
23593
- const passiveCount = total - enabledCount;
23678
+ const directCount = items.filter((item) => item.exposure === "direct").length;
23679
+ const lazyCount = items.filter((item) => item.exposure === "lazy").length;
23680
+ const disabledCount = items.filter((item) => item.exposure === "disabled").length;
23594
23681
  const selectedItem = visibleItems[visibleSelected];
23595
23682
  const win = windowRows3(rows, visibleSelected, maxVisible);
23596
23683
  const displayRows = win.rows;
@@ -23599,8 +23686,8 @@ function ToolsPicker({
23599
23686
  const hasFilter = filterActive;
23600
23687
  return /* @__PURE__ */ jsxs81(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, children: [
23601
23688
  /* @__PURE__ */ jsx84(Text, { bold: true, color: "green", children: "\u2501\u2501 Tools \u2501\u2501" }),
23602
- /* @__PURE__ */ jsx84(Text, { dimColor: true, children: hasFilter ? `Filter: ${filter} (${visibleItems.length} match${visibleItems.length === 1 ? "" : "es"}) \xB7 Backspace edit \xB7 Esc clear` : `\u2191/\u2193 select \xB7 type to filter \xB7 Enter toggles \xB7 Esc close \xB7 ${enabledCount} active / ${passiveCount} passive` }),
23603
- /* @__PURE__ */ jsx84(Text, { dimColor: true, children: "Passive tools stay listed for this session so you can select them and re-enable them later." }),
23689
+ /* @__PURE__ */ jsx84(Text, { dimColor: true, children: hasFilter ? `Filter: ${filter} (${visibleItems.length} match${visibleItems.length === 1 ? "" : "es"}) \xB7 Backspace edit \xB7 Esc clear` : `\u2191/\u2193 select \xB7 type to filter \xB7 Enter toggles \xB7 Esc close \xB7 ${directCount} direct / ${lazyCount} lazy / ${disabledCount} disabled` }),
23690
+ /* @__PURE__ */ jsx84(Text, { dimColor: true, children: "Lazy tools stay executable through discovery gateways without sending every schema to the provider." }),
23604
23691
  /* @__PURE__ */ jsx84(Box, { marginTop: 1, flexDirection: "column", children: total === 0 ? /* @__PURE__ */ jsx84(Text, { dimColor: true, children: busy ? "Loading tools\u2026" : "No tools registered." }) : visibleItems.length === 0 ? /* @__PURE__ */ jsxs81(Text, { dimColor: true, children: [
23605
23692
  'No tools match "',
23606
23693
  filter,
@@ -23617,7 +23704,7 @@ function ToolsPicker({
23617
23704
  wrap: "truncate-end",
23618
23705
  children: [
23619
23706
  focused ? "\u203A " : " ",
23620
- statusBadge2(item.enabled),
23707
+ statusBadge2(item.exposure),
23621
23708
  " ",
23622
23709
  /* @__PURE__ */ jsx84(Text, { bold: true, children: fitCell(item.name, columnWidths.name) }),
23623
23710
  " ",
@@ -23628,7 +23715,6 @@ function ToolsPicker({
23628
23715
  " ",
23629
23716
  rwBadge(item.mutating),
23630
23717
  " ",
23631
- " ",
23632
23718
  fitCell(item.permission, columnWidths.permission),
23633
23719
  " ",
23634
23720
  /* @__PURE__ */ jsxs81(Text, { color: item.descMode === "simple" ? "yellow" : void 0, children: [
@@ -23646,7 +23732,13 @@ function ToolsPicker({
23646
23732
  selectedItem ? /* @__PURE__ */ jsxs81(Box, { marginTop: 1, flexDirection: "column", children: [
23647
23733
  /* @__PURE__ */ jsxs81(Text, { children: [
23648
23734
  /* @__PURE__ */ jsx84(Text, { color: selectedItem.enabled ? "green" : "red", children: toolActionLabel(selectedItem.enabled) }),
23649
- /* @__PURE__ */ jsx84(Text, { dimColor: true, children: ` \xB7 ${selectedItem.mutating ? "Mutating" : "Read-only"} \xB7 permission: ${selectedItem.permission} \xB7 descriptions: ${selectedItem.descMode}` })
23735
+ /* @__PURE__ */ jsx84(
23736
+ Text,
23737
+ {
23738
+ dimColor: true,
23739
+ children: ` \xB7 ${selectedItem.mutating ? "Mutating" : "Read-only"} \xB7 permission: ${selectedItem.permission} \xB7 descriptions: ${selectedItem.descMode}`
23740
+ }
23741
+ )
23650
23742
  ] }),
23651
23743
  /* @__PURE__ */ jsx84(Text, { dimColor: true, wrap: "truncate-end", children: selectedItem.description || "No description provided." })
23652
23744
  ] }) : null,
@@ -26775,7 +26867,9 @@ function createInitialState(options) {
26775
26867
  showModelReasoning: true,
26776
26868
  showAgentSwarmPanel: "bottom",
26777
26869
  panelPositions: DEFAULT_PANEL_POSITIONS,
26778
- showSageMemoryInject: false,
26870
+ // Default on: compact one-line chip (see SageMemoryBlock). Full bordered
26871
+ // panel remains available when operators expand inject visibility later.
26872
+ showSageMemoryInject: true,
26779
26873
  sageMemoryInjectThreshold: 0.85,
26780
26874
  nextStepsTool: false,
26781
26875
  readSymbols: false
@@ -31539,7 +31633,7 @@ import {
31539
31633
  updateBoard,
31540
31634
  updateTask as updateTask2
31541
31635
  } from "@wrongstack/kanban";
31542
- var USAGE2 = "Usage:\n /kanban \u2014 open the project kanban panel\n /kanban create <title> \u2014 create a board, then open the panel\n /kanban add <title> [--column X] \u2014 add a task to the active board\n /kanban use <boardId|title|tag> \u2014 open the panel on a specific board\n /kanban boards \u2014 list every board in the project\n /kanban health \u2014 show kanban queue health summary\n /kanban audit [boardId|all] \u2014 show kanban cleaner audit (all boards if omitted)\n /kanban boundary <board> show [--task ID]\n /kanban boundary <board> allow|deny <kind> <access> <path> [--task ID] [--enforcement confirm|block] [--shell allow|confirm|block]\n /kanban boundary <board> clear [--task ID]\n /kanban help \u2014 show this help";
31636
+ var USAGE2 = "Usage:\n /kanban \u2014 open the project kanban panel\n /kanban create <title> \u2014 create a board, then open the panel\n /kanban add <title> [--column X] [--desc <text>] \u2014 add a task to the active board (managed boards require --desc)\n /kanban use <boardId|title|tag> \u2014 open the panel on a specific board\n /kanban boards \u2014 list every board in the project\n /kanban health \u2014 show kanban queue health summary\n /kanban audit [boardId|all] \u2014 show kanban cleaner audit (all boards if omitted)\n /kanban boundary <board> show [--task ID]\n /kanban boundary <board> allow|deny <kind> <access> <path> [--task ID] [--enforcement confirm|block] [--shell allow|confirm|block]\n /kanban boundary <board> clear [--task ID]\n /kanban help \u2014 show this help";
31543
31637
  var KANBAN_PANEL_ACTION = "toggleKanbanPanel";
31544
31638
  function createKanbanSlashCommand(deps) {
31545
31639
  return {
@@ -31587,15 +31681,26 @@ function createKanbanSlashCommand(deps) {
31587
31681
  message: "No kanban board found. Use `/kanban create <title>` to make one first."
31588
31682
  };
31589
31683
  }
31590
- const created = await addTaskToBoard(deps.projectRoot, target, parsed.title);
31684
+ if (target.managed && !parsed.description) {
31685
+ return {
31686
+ message: `"${target.boardTitle}" is a managed board \u2014 every card needs a description. Try: /kanban add "${parsed.title}" --desc "<what the task is>"`
31687
+ };
31688
+ }
31689
+ const created = await addTaskToBoard(
31690
+ deps.projectRoot,
31691
+ target,
31692
+ parsed.title,
31693
+ parsed.description
31694
+ );
31591
31695
  if (!created) {
31592
31696
  return {
31593
31697
  message: `Failed to add task to "${target.boardTitle}" \u2014 no writable column found.`
31594
31698
  };
31595
31699
  }
31596
31700
  deps.onPanelOpen?.current?.(KANBAN_PANEL_ACTION);
31701
+ const columnHint = target.managed ? " (backlog)" : "";
31597
31702
  return {
31598
- message: `Added task "${created.title}" to "${target.boardTitle}" \u2192 ${created.columnId}. Opening panel\u2026`
31703
+ message: `Added task "${created.title}" to "${target.boardTitle}" \u2192 ${created.columnId}${columnHint}. Opening panel\u2026`
31599
31704
  };
31600
31705
  }
31601
31706
  if (parsed.kind === "use") {
@@ -31661,6 +31766,7 @@ function parseKanbanArgs(raw) {
31661
31766
  if (head === "add" || head === "add-task" || head === "task") {
31662
31767
  let title = "";
31663
31768
  let column = null;
31769
+ let description = null;
31664
31770
  for (let i = 1; i < tokens.length; i++) {
31665
31771
  const tok = tokens[i] ?? "";
31666
31772
  if (tok === "--column" || tok === "-c") {
@@ -31675,11 +31781,28 @@ function parseKanbanArgs(raw) {
31675
31781
  column = tok.slice("--column=".length);
31676
31782
  continue;
31677
31783
  }
31784
+ if (tok === "--desc" || tok === "-d") {
31785
+ const next = tokens[i + 1];
31786
+ if (next) {
31787
+ description = next;
31788
+ i++;
31789
+ }
31790
+ continue;
31791
+ }
31792
+ if (tok.startsWith("--desc=")) {
31793
+ description = tok.slice("--desc=".length);
31794
+ continue;
31795
+ }
31678
31796
  title = title ? `${title} ${tok}` : tok;
31679
31797
  }
31680
31798
  title = title.trim();
31681
31799
  if (!title) return { kind: "help" };
31682
- return { kind: "add", title, column };
31800
+ return {
31801
+ kind: "add",
31802
+ title,
31803
+ column,
31804
+ ...description ? { description } : {}
31805
+ };
31683
31806
  }
31684
31807
  if (head === "use" || head === "open-board" || head === "focus") {
31685
31808
  const query = tokens.slice(1).join(" ").trim();
@@ -31792,9 +31915,7 @@ function renderBoundaryPolicy(policy) {
31792
31915
  if (!policy?.enabled) return "unrestricted";
31793
31916
  const lines = [
31794
31917
  `mode ${policy.enforcement} \xB7 shell ${policy.shellAccess}`,
31795
- ...policy.allow.map(
31796
- (selector) => `ALLOW ${selector.access} ${selector.kind}:${selector.path}`
31797
- ),
31918
+ ...policy.allow.map((selector) => `ALLOW ${selector.access} ${selector.kind}:${selector.path}`),
31798
31919
  ...(policy.deny ?? []).map(
31799
31920
  (selector) => `DENY ${selector.access} ${selector.kind}:${selector.path}`
31800
31921
  )
@@ -31871,13 +31992,17 @@ async function resolveAddTarget(deps, column) {
31871
31992
  if (!preferred) return null;
31872
31993
  const board = await getBoard3(deps.projectRoot, preferred.id);
31873
31994
  if (!board) return null;
31995
+ const managed = board.lifecycle?.mode === "managed";
31874
31996
  const firstColumn = board.columns[0];
31875
- const targetColumn = resolveColumn(board.columns, column) ?? firstColumn;
31997
+ const backlogColumnId = managed ? board.lifecycle?.columns?.backlog : void 0;
31998
+ const backlogColumn = backlogColumnId ? board.columns.find((c) => c.id === backlogColumnId) : void 0;
31999
+ const targetColumn = managed ? backlogColumn ?? firstColumn : resolveColumn(board.columns, column) ?? firstColumn;
31876
32000
  if (!targetColumn) return null;
31877
32001
  return {
31878
32002
  boardId: board.id,
31879
32003
  boardTitle: board.title,
31880
- columnId: targetColumn.id
32004
+ columnId: targetColumn.id,
32005
+ managed
31881
32006
  };
31882
32007
  }
31883
32008
  function resolveColumn(columns, hint) {
@@ -31912,7 +32037,15 @@ function renderHealthReport(health2) {
31912
32037
  c.queued > 0 ? ` + ${c.queued} currently queued for assignment (subset)` : "",
31913
32038
  "",
31914
32039
  " **Per-status counts**",
31915
- ` ready ${c.ready} \xB7 running ${c.running} \xB7 review ${c.review} \xB7 failed ${c.failed} \xB7 completed ${c.completed}`,
32040
+ // `startable`, not `ready`. `counts.ready` tallies the stored status field,
32041
+ // which no dispatcher writes, so it printed a permanent 0 while
32042
+ // `ready_tasks` on the same board returned work. `counts.startable` is the
32043
+ // derived answer and agrees with `listReadyTasks`.
32044
+ //
32045
+ // `lifecycleTotal` above still sums `c.ready` on purpose: that is the
32046
+ // single-count partition, and `startable` overlaps `pending`/`ready`, so
32047
+ // swapping it there would double-count. These two lines are meant to differ.
32048
+ ` startable ${c.startable} \xB7 running ${c.running} \xB7 review ${c.review} \xB7 failed ${c.failed} \xB7 completed ${c.completed}`,
31916
32049
  ` pending ${c.pending} \xB7 archived ${c.archived} \xB7 blocked ${c.blocked}` + (c.queued > 0 ? ` \xB7 queued ${c.queued}` : ""),
31917
32050
  ""
31918
32051
  ].filter((line) => line !== "");
@@ -32038,10 +32171,11 @@ function renderAuditReport(rows, multi) {
32038
32171
  function severityRank2(severity) {
32039
32172
  return severity === "error" ? 0 : 1;
32040
32173
  }
32041
- async function addTaskToBoard(cwd, target, title) {
32174
+ async function addTaskToBoard(cwd, target, title, description) {
32042
32175
  const input = {
32043
32176
  title,
32044
- columnId: target.columnId
32177
+ columnId: target.columnId,
32178
+ ...description ? { description } : {}
32045
32179
  };
32046
32180
  const result = await addTask2(cwd, target.boardId, input);
32047
32181
  if (!result) return null;
@@ -3,10 +3,17 @@
3
3
  *
4
4
  * Mirrors the WebUI's `packages/webui/src/lib/kanban-cleaner.ts` but
5
5
  * lives in the TUI package so we don't have to thread `@wrongstack/webui`
6
- * (which depends on React) into a server-side TUI pipeline. The two
7
- * implementations intentionally share the same audit-code vocabulary
8
- * (10 codes) and severity levels (`error`, `warning`) so a board audited
9
- * from either surface produces the same findings.
6
+ * (which depends on React) into a server-side TUI pipeline.
7
+ *
8
+ * The two implementations must produce the same verdicts for the same board.
9
+ * That claim used to be a comment; it is now enforced by
10
+ * `packages/cli/tests/kanban-cleaner-parity.test.ts`, which runs a shared
11
+ * corpus through both and compares the `taskId:code:severity` triples. Only
12
+ * three differences are deliberate, and all three are API shape rather than
13
+ * verdict: this file also accepts `liveAgentIdentities`, returns eight extra
14
+ * summary fields the WebUI's `KanbanCleanerAlert` fills from queue health, and
15
+ * exports `summarizeAuditHeadline` / `topAuditIssues` for the panel badge.
16
+ * Messages may differ; codes and severities may not.
10
17
  *
11
18
  * The output of `auditKanbanBoard()` feeds the TUI panel header badge so
12
19
  * users see Cleaner warnings inline — mirroring the WebUI's
@@ -14,7 +21,13 @@
14
21
  */
15
22
  import type { KanbanBoard } from '@wrongstack/kanban';
16
23
  export type KanbanAuditSeverity = 'error' | 'warning';
17
- export type KanbanAuditIssueCode = 'missing-description' | 'missing-assignee' | 'missing-due-date' | 'missing-labels' | 'missing-subtasks' | 'missing-success-criteria' | 'skipped-lifecycle-state' | 'abandoned-running-task' | 'stale-running-task' | 'stale-review';
24
+ /**
25
+ * The audit vocabulary, as a runtime value so the parity test can compare the
26
+ * two implementations' code sets instead of trusting that both type unions
27
+ * were edited together.
28
+ */
29
+ export declare const ALL_AUDIT_CODES: readonly ['abandoned-running-task', 'board-oversized', 'missing-assignee', 'missing-description', 'missing-due-date', 'missing-labels', 'missing-subtasks', 'missing-success-criteria', 'skipped-lifecycle-state', 'stale-review', 'stale-running-task'];
30
+ export type KanbanAuditIssueCode = (typeof ALL_AUDIT_CODES)[number];
18
31
  export interface KanbanAuditIssue {
19
32
  id: string;
20
33
  taskId: string;
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * /kanban — open the project kanban panel
8
8
  * /kanban create <title> — create a new board, then open the panel
9
- * /kanban add <title> [--column X] — add a task to the active board
9
+ * /kanban add <title> [--column X] [--desc <text>] — add a task to the active board (managed boards require --desc)
10
10
  * /kanban use <boardId|title|tag> — open the kanban panel on a specific board
11
11
  * /kanban boards — list every board in the project
12
12
  * /kanban health — show kanban queue health summary
@@ -19,7 +19,7 @@
19
19
  * is the text-first equivalent for when the panel is closed.
20
20
  */
21
21
  import type { SlashCommand } from '@wrongstack/core/types';
22
- import type { KanbanBoundaryAccess, KanbanBoundaryPolicy, KanbanBoundarySelectorKind, KanbanBoardSummary, KanbanQueueHealth } from '@wrongstack/kanban';
22
+ import type { KanbanBoardSummary, KanbanBoundaryAccess, KanbanBoundaryPolicy, KanbanBoundarySelectorKind, KanbanQueueHealth } from '@wrongstack/kanban';
23
23
  import { type KanbanAuditSummary } from './kanban-audit.js';
24
24
  /**
25
25
  * Result of resolving the board that an `add` operation should target.
@@ -34,6 +34,12 @@ export interface KanbanAddTarget {
34
34
  boardTitle: string;
35
35
  /** Column that the task will be placed in. Defaults to the first column. */
36
36
  columnId: string;
37
+ /**
38
+ * Whether the board enforces the managed Kanban Agent lifecycle. Managed
39
+ * boards require new cards to be created in the Backlog column and with a
40
+ * description, so callers route the user accordingly.
41
+ */
42
+ managed: boolean;
37
43
  }
38
44
  export interface KanbanSlashDeps {
39
45
  /**
@@ -78,6 +84,7 @@ export type ParsedKanbanArgs = {
78
84
  kind: 'add';
79
85
  title: string;
80
86
  column: string | null;
87
+ description?: string | undefined;
81
88
  } | {
82
89
  kind: 'use';
83
90
  query: string;
@@ -126,6 +126,7 @@ export interface ToolPickerItem {
126
126
  owner: string;
127
127
  category: string;
128
128
  enabled: boolean;
129
+ exposure: 'direct' | 'lazy' | 'disabled';
129
130
  mutating: boolean;
130
131
  permission: string;
131
132
  descMode: 'extend' | 'simple';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/tui",
3
- "version": "0.303.0",
3
+ "version": "0.305.0",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack terminal UI: Ink-based interactive renderer with history, pickers, and slash menus.",
6
6
  "repository": {
@@ -29,12 +29,12 @@
29
29
  "ink": "7.1.1",
30
30
  "react": "^19.2.8",
31
31
  "string-width": "8.2.2",
32
- "@wrongstack/kanban": "0.303.0",
33
- "@wrongstack/core": "0.303.0",
34
- "@wrongstack/sdd": "0.303.0",
35
- "@wrongstack/runtime": "0.303.0",
36
- "@wrongstack/sage": "0.303.0",
37
- "@wrongstack/tools": "0.303.0"
32
+ "@wrongstack/core": "0.305.0",
33
+ "@wrongstack/kanban": "0.305.0",
34
+ "@wrongstack/sage": "0.305.0",
35
+ "@wrongstack/runtime": "0.305.0",
36
+ "@wrongstack/sdd": "0.305.0",
37
+ "@wrongstack/tools": "0.305.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@testing-library/dom": "^10.4.1",