@wrongstack/tools 0.303.0 → 0.305.1

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.
package/dist/read.js CHANGED
@@ -2867,6 +2867,86 @@ import {
2867
2867
  isFrugalPerf
2868
2868
  } from "@wrongstack/core/utils";
2869
2869
 
2870
+ // src/codebase-index/content-hash.ts
2871
+ var PRIME64_1 = 0x9e3779b185ebca87n;
2872
+ var PRIME64_2 = 0xc2b2ae3d27d4eb4fn;
2873
+ var PRIME64_3 = 0x165667b19e3779f9n;
2874
+ var PRIME64_4 = 0x85ebca77c2b2ae63n;
2875
+ var PRIME64_5 = 0x27d4eb2f165667c5n;
2876
+ var MASK64 = 0xffffffffffffffffn;
2877
+ function mul64(a, b) {
2878
+ return (a & MASK64) * (b & MASK64) & MASK64;
2879
+ }
2880
+ function rotl64(x, n) {
2881
+ const v = x & MASK64;
2882
+ return (v << BigInt(n) | v >> BigInt(64 - n)) & MASK64;
2883
+ }
2884
+ function readU64LE(buf, off) {
2885
+ let v = 0n;
2886
+ for (let i = 7; i >= 0; i--) {
2887
+ v = v << 8n | BigInt(buf[off + i] ?? 0);
2888
+ }
2889
+ return v & MASK64;
2890
+ }
2891
+ function readU32LE(buf, off) {
2892
+ return (BigInt(buf[off] ?? 0) | BigInt(buf[off + 1] ?? 0) << 8n | BigInt(buf[off + 2] ?? 0) << 16n | BigInt(buf[off + 3] ?? 0) << 24n) & MASK64;
2893
+ }
2894
+ function xxh64Round(acc, lane) {
2895
+ return mul64(rotl64(acc + mul64(lane, PRIME64_2) & MASK64, 31), PRIME64_1);
2896
+ }
2897
+ function xxh64MergeRound(acc, val) {
2898
+ return mul64(acc ^ xxh64Round(0n, val), PRIME64_1) + PRIME64_4 & MASK64;
2899
+ }
2900
+ function xxhash64Hex(buf, explicitLen) {
2901
+ const length = explicitLen ?? buf.length;
2902
+ let h;
2903
+ let off = 0;
2904
+ if (length >= 32) {
2905
+ let v1 = PRIME64_1 + PRIME64_2 & MASK64;
2906
+ let v2 = PRIME64_2;
2907
+ let v3 = 0n;
2908
+ let v4 = 0n - PRIME64_1 & MASK64;
2909
+ const end32 = length - 32;
2910
+ while (off <= end32) {
2911
+ v1 = xxh64Round(v1, readU64LE(buf, off));
2912
+ v2 = xxh64Round(v2, readU64LE(buf, off + 8));
2913
+ v3 = xxh64Round(v3, readU64LE(buf, off + 16));
2914
+ v4 = xxh64Round(v4, readU64LE(buf, off + 24));
2915
+ off += 32;
2916
+ }
2917
+ h = rotl64(v1, 1) + rotl64(v2, 7) + rotl64(v3, 12) + rotl64(v4, 18) & MASK64;
2918
+ h = xxh64MergeRound(h, v1);
2919
+ h = xxh64MergeRound(h, v2);
2920
+ h = xxh64MergeRound(h, v3);
2921
+ h = xxh64MergeRound(h, v4);
2922
+ } else {
2923
+ h = PRIME64_5;
2924
+ }
2925
+ h = h + BigInt(length) & MASK64;
2926
+ while (off + 8 <= length) {
2927
+ const k1 = xxh64Round(0n, readU64LE(buf, off));
2928
+ h = mul64(rotl64(h ^ k1, 27), PRIME64_1) + PRIME64_4 & MASK64;
2929
+ off += 8;
2930
+ }
2931
+ if (off + 4 <= length) {
2932
+ h = mul64(rotl64(h ^ mul64(readU32LE(buf, off), PRIME64_1), 23), PRIME64_2) + PRIME64_3 & MASK64;
2933
+ off += 4;
2934
+ }
2935
+ while (off < length) {
2936
+ h = mul64(rotl64(h ^ mul64(BigInt(buf[off] ?? 0), PRIME64_5), 11), PRIME64_1) & MASK64;
2937
+ off += 1;
2938
+ }
2939
+ h = (h ^ h >> 33n) & MASK64;
2940
+ h = mul64(h, PRIME64_2);
2941
+ h = (h ^ h >> 29n) & MASK64;
2942
+ h = mul64(h, PRIME64_3);
2943
+ h = (h ^ h >> 32n) & MASK64;
2944
+ return h.toString(16).padStart(16, "0");
2945
+ }
2946
+ function xxhash64String(content) {
2947
+ return xxhash64Hex(new TextEncoder().encode(content));
2948
+ }
2949
+
2870
2950
  // src/codebase-index/gitignore.ts
2871
2951
  import * as fs from "node:fs/promises";
2872
2952
  import * as path2 from "node:path";
@@ -3898,86 +3978,6 @@ function getParserPool() {
3898
3978
  return _pool;
3899
3979
  }
3900
3980
 
3901
- // src/codebase-index/content-hash.ts
3902
- var PRIME64_1 = 0x9e3779b185ebca87n;
3903
- var PRIME64_2 = 0xc2b2ae3d27d4eb4fn;
3904
- var PRIME64_3 = 0x165667b19e3779f9n;
3905
- var PRIME64_4 = 0x85ebca77c2b2ae63n;
3906
- var PRIME64_5 = 0x27d4eb2f165667c5n;
3907
- var MASK64 = 0xffffffffffffffffn;
3908
- function mul64(a, b) {
3909
- return (a & MASK64) * (b & MASK64) & MASK64;
3910
- }
3911
- function rotl64(x, n) {
3912
- const v = x & MASK64;
3913
- return (v << BigInt(n) | v >> BigInt(64 - n)) & MASK64;
3914
- }
3915
- function readU64LE(buf, off) {
3916
- let v = 0n;
3917
- for (let i = 7; i >= 0; i--) {
3918
- v = v << 8n | BigInt(buf[off + i] ?? 0);
3919
- }
3920
- return v & MASK64;
3921
- }
3922
- function readU32LE(buf, off) {
3923
- return (BigInt(buf[off] ?? 0) | BigInt(buf[off + 1] ?? 0) << 8n | BigInt(buf[off + 2] ?? 0) << 16n | BigInt(buf[off + 3] ?? 0) << 24n) & MASK64;
3924
- }
3925
- function xxh64Round(acc, lane) {
3926
- return mul64(rotl64(acc + mul64(lane, PRIME64_2) & MASK64, 31), PRIME64_1);
3927
- }
3928
- function xxh64MergeRound(acc, val) {
3929
- return mul64(acc ^ xxh64Round(0n, val), PRIME64_1) + PRIME64_4 & MASK64;
3930
- }
3931
- function xxhash64Hex(buf, explicitLen) {
3932
- const length = explicitLen ?? buf.length;
3933
- let h;
3934
- let off = 0;
3935
- if (length >= 32) {
3936
- let v1 = PRIME64_1 + PRIME64_2 & MASK64;
3937
- let v2 = PRIME64_2;
3938
- let v3 = 0n;
3939
- let v4 = 0n - PRIME64_1 & MASK64;
3940
- const end32 = length - 32;
3941
- while (off <= end32) {
3942
- v1 = xxh64Round(v1, readU64LE(buf, off));
3943
- v2 = xxh64Round(v2, readU64LE(buf, off + 8));
3944
- v3 = xxh64Round(v3, readU64LE(buf, off + 16));
3945
- v4 = xxh64Round(v4, readU64LE(buf, off + 24));
3946
- off += 32;
3947
- }
3948
- h = rotl64(v1, 1) + rotl64(v2, 7) + rotl64(v3, 12) + rotl64(v4, 18) & MASK64;
3949
- h = xxh64MergeRound(h, v1);
3950
- h = xxh64MergeRound(h, v2);
3951
- h = xxh64MergeRound(h, v3);
3952
- h = xxh64MergeRound(h, v4);
3953
- } else {
3954
- h = PRIME64_5;
3955
- }
3956
- h = h + BigInt(length) & MASK64;
3957
- while (off + 8 <= length) {
3958
- const k1 = xxh64Round(0n, readU64LE(buf, off));
3959
- h = mul64(rotl64(h ^ k1, 27), PRIME64_1) + PRIME64_4 & MASK64;
3960
- off += 8;
3961
- }
3962
- if (off + 4 <= length) {
3963
- h = mul64(rotl64(h ^ mul64(readU32LE(buf, off), PRIME64_1), 23), PRIME64_2) + PRIME64_3 & MASK64;
3964
- off += 4;
3965
- }
3966
- while (off < length) {
3967
- h = mul64(rotl64(h ^ mul64(BigInt(buf[off] ?? 0), PRIME64_5), 11), PRIME64_1) & MASK64;
3968
- off += 1;
3969
- }
3970
- h = (h ^ h >> 33n) & MASK64;
3971
- h = mul64(h, PRIME64_2);
3972
- h = (h ^ h >> 29n) & MASK64;
3973
- h = mul64(h, PRIME64_3);
3974
- h = (h ^ h >> 32n) & MASK64;
3975
- return h.toString(16).padStart(16, "0");
3976
- }
3977
- function xxhash64String(content) {
3978
- return xxhash64Hex(new TextEncoder().encode(content));
3979
- }
3980
-
3981
3981
  // src/codebase-index/writer.ts
3982
3982
  import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
3983
3983
  import * as fs8 from "node:fs";
@@ -5495,7 +5495,7 @@ var IndexStore = class _IndexStore {
5495
5495
  const ftsSchema = this.stmt(
5496
5496
  "SELECT sql FROM sqlite_master WHERE type='table' AND name='symbols_fts'"
5497
5497
  ).get();
5498
- if (ftsSchema?.sql && ftsSchema.sql.includes("unicode61")) {
5498
+ if (ftsSchema?.sql?.includes("unicode61")) {
5499
5499
  this.db.exec("DROP TABLE IF EXISTS symbols_fts");
5500
5500
  }
5501
5501
  this.db.exec(SYMBOLS_FTS_SQL);
@@ -5988,9 +5988,13 @@ var IndexStore = class _IndexStore {
5988
5988
  sim: cosineSimilarity(queryVec, decodeVector(r.vector))
5989
5989
  })).sort((a, b) => b.sim - a.sim);
5990
5990
  const bm25Rank = /* @__PURE__ */ new Map();
5991
- bm25Rows.forEach((r, i) => bm25Rank.set(r.id, i));
5991
+ bm25Rows.forEach((r, i) => {
5992
+ bm25Rank.set(r.id, i);
5993
+ });
5992
5994
  const vecRank = /* @__PURE__ */ new Map();
5993
- vecScores.forEach((r, i) => vecRank.set(r.id, i));
5995
+ vecScores.forEach((r, i) => {
5996
+ vecRank.set(r.id, i);
5997
+ });
5994
5998
  const fused = reciprocalRankFusion(bm25Rank, vecRank, 60);
5995
5999
  const fusedScore = new Map(fused);
5996
6000
  const sorted = [...bm25Rows].sort(
@@ -6600,6 +6604,9 @@ var YIELD_EVERY_N = 50;
6600
6604
  function resolveParallelBatch() {
6601
6605
  return indexParallelBatchSize(availableParallelism());
6602
6606
  }
6607
+ function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
6608
+ return !isFrugalPerf() && candidateFileCount >= WORKER_POOL_THRESHOLD && parseBatchCount > 1;
6609
+ }
6603
6610
  function yieldEventLoop() {
6604
6611
  return new Promise((resolve4) => setImmediate(resolve4));
6605
6612
  }
@@ -6776,11 +6783,7 @@ async function resolveProjectRelations(store, projectRoot, opts) {
6776
6783
  const structure = await detectModuleRoots(projectRoot, indexedFiles);
6777
6784
  if (opts.signal?.aborted) return;
6778
6785
  store.setFilePackages(assignPackageLabels(structure, indexedFiles));
6779
- const resolver = new ModuleResolver(
6780
- structure,
6781
- indexedFiles,
6782
- store.getNamespaceDeclarations()
6783
- );
6786
+ const resolver = new ModuleResolver(structure, indexedFiles, store.getNamespaceDeclarations());
6784
6787
  const pending2 = store.getUnresolvedImports(opts.onlyFiles);
6785
6788
  const resolutions = [];
6786
6789
  for (const entry of pending2) {
@@ -6805,6 +6808,10 @@ async function runIndexerWithStore(store, opts) {
6805
6808
  const errors = [];
6806
6809
  const langStats = {};
6807
6810
  let filesIndexed = 0;
6811
+ let filesParsed = 0;
6812
+ let filesSkipped = 0;
6813
+ let filesEmpty = 0;
6814
+ let filesFailed = 0;
6808
6815
  let symbolsIndexed = 0;
6809
6816
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
6810
6817
  let files;
@@ -6846,12 +6853,14 @@ async function runIndexerWithStore(store, opts) {
6846
6853
  langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
6847
6854
  symbolsIndexed += meta.symbolCount;
6848
6855
  filesIndexed++;
6856
+ filesSkipped++;
6849
6857
  filesPreSkipped++;
6850
6858
  return false;
6851
6859
  });
6852
6860
  if (filesPreSkipped > 0) opts.onProgress?.(filesPreSkipped, totalFilesForProgress);
6853
6861
  }
6854
6862
  const parallelBatch = resolveParallelBatch();
6863
+ const parserPoolCandidateCount = files.length;
6855
6864
  let filesSinceLastYield = 0;
6856
6865
  for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
6857
6866
  const batchEnd = Math.min(batchStart + parallelBatch, files.length);
@@ -6944,7 +6953,7 @@ async function runIndexerWithStore(store, opts) {
6944
6953
  });
6945
6954
  }
6946
6955
  if (toParse.length > 0) {
6947
- let pool = toParse.length >= WORKER_POOL_THRESHOLD ? getParserPool() : null;
6956
+ let pool = shouldUseParserWorkerPool(parserPoolCandidateCount, toParse.length) ? getParserPool() : null;
6948
6957
  if (pool) {
6949
6958
  try {
6950
6959
  await pool.ensureReady();
@@ -6994,12 +7003,14 @@ async function runIndexerWithStore(store, opts) {
6994
7003
  const err = settled.reason;
6995
7004
  if (err instanceof Error && isAbortError(err)) throw err;
6996
7005
  errors.push(`batch error: ${file}: ${err instanceof Error ? err.message : String(err)}`);
7006
+ filesFailed++;
6997
7007
  continue;
6998
7008
  }
6999
7009
  const result = settled.value;
7000
7010
  if (result.error) {
7001
7011
  if (result.missing) store.deleteFile(file);
7002
7012
  errors.push(`${file}: ${result.error}`);
7013
+ filesFailed++;
7003
7014
  continue;
7004
7015
  }
7005
7016
  const { stat: stat3, lang, parsed } = result;
@@ -7007,6 +7018,7 @@ async function runIndexerWithStore(store, opts) {
7007
7018
  langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
7008
7019
  symbolsIndexed += result.skippedMeta.symbolCount;
7009
7020
  filesIndexed++;
7021
+ filesSkipped++;
7010
7022
  const stored = existingMeta.get(file);
7011
7023
  if (stored && stored.mtimeMs !== result.skippedMeta.mtimeMs) {
7012
7024
  store.upsertFile({
@@ -7031,6 +7043,7 @@ async function runIndexerWithStore(store, opts) {
7031
7043
  contentHash: result.contentHash ?? ""
7032
7044
  });
7033
7045
  filesIndexed++;
7046
+ filesEmpty++;
7034
7047
  }
7035
7048
  continue;
7036
7049
  }
@@ -7044,6 +7057,7 @@ async function runIndexerWithStore(store, opts) {
7044
7057
  contentHash: result.contentHash ?? ""
7045
7058
  });
7046
7059
  filesIndexed++;
7060
+ filesEmpty++;
7047
7061
  continue;
7048
7062
  }
7049
7063
  batchEntries.push({
@@ -7065,6 +7079,7 @@ async function runIndexerWithStore(store, opts) {
7065
7079
  symbolsIndexed += count;
7066
7080
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
7067
7081
  filesIndexed++;
7082
+ filesParsed++;
7068
7083
  }
7069
7084
  } catch (err) {
7070
7085
  const message = err instanceof Error ? err.message : String(err);
@@ -7077,6 +7092,7 @@ async function runIndexerWithStore(store, opts) {
7077
7092
  symbolsIndexed += symbolsWithIds.length;
7078
7093
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
7079
7094
  filesIndexed++;
7095
+ filesParsed++;
7080
7096
  if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
7081
7097
  const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
7082
7098
  if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
@@ -7094,6 +7110,7 @@ async function runIndexerWithStore(store, opts) {
7094
7110
  contentHash: entry.contentHash
7095
7111
  });
7096
7112
  } catch (innerErr) {
7113
+ filesFailed++;
7097
7114
  errors.push(
7098
7115
  `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
7099
7116
  );
@@ -7126,6 +7143,12 @@ async function runIndexerWithStore(store, opts) {
7126
7143
  const durationMs = Date.now() - startMs;
7127
7144
  return {
7128
7145
  filesIndexed,
7146
+ fileOutcomes: {
7147
+ parsed: filesParsed,
7148
+ skipped: filesSkipped,
7149
+ empty: filesEmpty,
7150
+ failed: filesFailed
7151
+ },
7129
7152
  symbolsIndexed,
7130
7153
  langStats,
7131
7154
  durationMs,
@@ -7254,7 +7277,6 @@ import * as fs10 from "node:fs";
7254
7277
  import * as os3 from "node:os";
7255
7278
  import * as path14 from "node:path";
7256
7279
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7257
- import { assertUnixSocketPathWithinLimit } from "@wrongstack/core/utils";
7258
7280
  var PROJECT_INDEX_SERVER_PROTOCOL_VERSION = 1;
7259
7281
  var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
7260
7282
  var PROJECT_INDEX_SERVER_SOCKET_DIR = `wsci-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`;
@@ -3,7 +3,16 @@ import { type PlanFile, type PlanItem, type TaskFile } from '@wrongstack/core/st
3
3
  import type { SerializedTaskGraph } from '@wrongstack/core/types';
4
4
  import { type TaskItem } from '@wrongstack/core/utils';
5
5
  import { type KanbanBoard, type KanbanColumn, type KanbanTask } from '@wrongstack/kanban';
6
- /** The canonical workflow shared by WebUI and TUI session boards. */
6
+ /**
7
+ * The canonical workflow shared by WebUI and TUI session boards.
8
+ *
9
+ * Session boards now use the same 5 standard columns as every other board
10
+ * (backlog, todo, in-progress, review, done). Formerly this shipped a 4-column
11
+ * variant without a backlog; the column lock unified the layout. Existing
12
+ * 4-column session_mirror boards are migrated automatically on the next session
13
+ * touch: `ensureSessionKanbanBoard`'s `sameColumns` check detects the drift
14
+ * and `updateBoard` reconciles to DEFAULT_COLUMNS.
15
+ */
7
16
  export declare const SESSION_KANBAN_COLUMNS: KanbanColumn[];
8
17
  /** Create (or migrate) the single Kanban board owned by a session. */
9
18
  export declare function ensureSessionKanbanBoard(projectRoot: string | undefined, sessionId: string): Promise<KanbanBoard | null>;
@@ -17,12 +26,35 @@ export declare function cleanupSessionKanbanBoard(projectRoot: string | undefine
17
26
  export declare function cleanupSessionKanbanBoardIfEmpty(projectRoot: string | undefined, sessionId: string): Promise<string[]>;
18
27
  /** Prune stale empty session boards while preserving manual and live boards. */
19
28
  export declare function cleanupEmptySessionKanbanBoards(projectRoot: string | undefined, activeSessionId?: string): Promise<string[]>;
29
+ /**
30
+ * Consume the last unreported mirror failure for a session, if any.
31
+ *
32
+ * The mirror is asynchronous and fire-and-forget, so a rejected projection
33
+ * used to be observable only in stdout. The `todo` tool drains this on its
34
+ * next call and reports it as a warning, so a board that stopped tracking the
35
+ * session is surfaced where the divergence actually matters.
36
+ */
37
+ export declare function takeSessionMirrorFailure(projectRoot: string | undefined, sessionId: string): string | undefined;
20
38
  export declare function todoListToSerializedGraph(todos: readonly TodoItem[], sessionId: string): SerializedTaskGraph;
21
39
  export declare function taskFileToSerializedGraph(tasks: readonly TaskItem[], sessionId: string): SerializedTaskGraph;
22
40
  export declare function planFileToSerializedGraph(items: readonly PlanItem[], sessionId: string): SerializedTaskGraph;
23
41
  export declare function projectSessionTodosToKanban(projectRoot: string | undefined, todos: readonly TodoItem[], sessionId: string): Promise<KanbanBoard | null>;
24
42
  export declare function projectSessionTasksToKanban(projectRoot: string | undefined, tasks: readonly TaskItem[], sessionId: string): Promise<KanbanBoard | null>;
25
43
  export declare function projectSessionPlanToKanban(projectRoot: string | undefined, items: readonly PlanItem[], sessionId: string): Promise<KanbanBoard | null>;
44
+ /**
45
+ * The todo rows that still need the session mirror: those not already cards on
46
+ * the board this session is working on.
47
+ *
48
+ * The rule used to be all-or-nothing — the mirror was skipped only when EVERY
49
+ * row was bound. A single unbound row (a todo the model added to the chat list
50
+ * without also filing a card) sent the whole list to a separate session board,
51
+ * so work already tracked on the real board showed up a second time there, and
52
+ * one stray row was enough to leave a session board holding a single card.
53
+ *
54
+ * With no active board every row still needs mirroring, since there is no other
55
+ * place the work is being recorded.
56
+ */
57
+ export declare function todosNeedingSessionMirror(todos: readonly TodoItem[], activeBoardId: string | undefined): readonly TodoItem[];
26
58
  export declare function mirrorSessionTodosToKanban(projectRoot: string | undefined, todos: readonly TodoItem[], sessionId: string): void;
27
59
  export declare function mirrorSessionTasksToKanban(projectRoot: string | undefined, tasks: readonly TaskItem[], sessionId: string): void;
28
60
  export declare function mirrorSessionPlanToKanban(projectRoot: string | undefined, items: readonly PlanItem[], sessionId: string): void;
@@ -33,6 +65,63 @@ export declare function mirrorSessionPlanToKanban(projectRoot: string | undefine
33
65
  */
34
66
  export declare function attachSessionKanbanMirror(context: Context): () => void;
35
67
  /** Fully hydrate a session board before a host announces the session as ready. */
68
+ /**
69
+ * Re-bind the card this session was working, after a restart or `--resume`.
70
+ *
71
+ * The binding produced by `start_task` lives in `ctx.meta.kanban`, which is
72
+ * conversation state — nothing restores it. So a resumed session came back
73
+ * with `currentKanbanTaskId` undefined while its card still sat in Running on
74
+ * the board: file events lost their task attribution, and under
75
+ * `tools.kanbanGovernance` the run was un-bound until the model happened to
76
+ * call `start_task` again.
77
+ *
78
+ * Board presence is the durable anchor. It is keyed `<sessionId>:<agentId>`,
79
+ * carries the `taskId`, and is stored on the board record itself, so it
80
+ * survives the restart even though its derived `active` flag does not (the TTL
81
+ * is two minutes). Matching it against the cards that are actually still
82
+ * running gives an unambiguous answer without guessing from free-text agent
83
+ * ids.
84
+ *
85
+ * Deliberately conservative — it prefers to do nothing over binding the wrong
86
+ * card:
87
+ * - never overrides a binding this process already has;
88
+ * - only considers cards whose assignment is still `running`;
89
+ * - skips an expired lease, because `recover_stale` may have reassigned that
90
+ * card to another worker;
91
+ * - picks the most recently seen entry when a session touched several.
92
+ *
93
+ * Session mirrors and archived boards are excluded by the snapshot's default
94
+ * board-kind filter, which is correct: `start_task` never targets them.
95
+ */
96
+ export declare function rebindSessionKanbanTask(context: Context): Promise<{
97
+ boardId: string;
98
+ taskId: string;
99
+ } | null>;
100
+ /**
101
+ * Why session Kanban is unavailable, or undefined while it is healthy.
102
+ *
103
+ * Surfaces exist so a startup path can tell the user their board will not sync
104
+ * without having to catch and classify the failure itself. Cleared on the
105
+ * first hydrate that succeeds, so a daemon that comes back stops being
106
+ * reported as down.
107
+ */
108
+ export declare function sessionKanbanDegradation(): string | undefined;
109
+ /**
110
+ * Project the session's todos, plan, and tasks onto its Kanban board.
111
+ *
112
+ * Kanban is a projection: every card here is derived from state that lives
113
+ * somewhere else and is authoritative there. Losing the board costs the user a
114
+ * view, not any work — so an unreachable Kanban daemon degrades this to `null`
115
+ * rather than propagating.
116
+ *
117
+ * That distinction used to be missing, and it cost an entire session. When a
118
+ * killed daemon left a stale IPC endpoint behind, the connect error thrown
119
+ * here unwound through `setupSession` and killed `wstack` at startup: no
120
+ * prompt, no session, just a socket path in a stack trace — for a board the
121
+ * user had not asked to see. `bindProjectEndpoint` now stops the daemon from
122
+ * wedging in the first place, but the call site must still hold: an optional
123
+ * view may never take down the session it decorates.
124
+ */
36
125
  export declare function hydrateSessionKanban(context: Context): Promise<KanbanBoard | null>;
37
126
  export interface SessionKanbanSourceUpdate {
38
127
  source: 'todo' | 'task' | 'plan' | null;
@@ -40,6 +129,27 @@ export interface SessionKanbanSourceUpdate {
40
129
  tasks?: TaskFile | undefined;
41
130
  plan?: PlanFile | undefined;
42
131
  }
132
+ /**
133
+ * Human-readable titles of the unfinished dependencies blocking a card.
134
+ *
135
+ * The board recomputes readiness on every mutation; this only carries the
136
+ * result to the todo surface. Titles rather than ids, because the row is read
137
+ * by a model and a human, neither of whom can resolve a raw card id.
138
+ */
139
+ export declare function blockingTitles(board: KanbanBoard, task: KanbanTask): string[];
140
+ /**
141
+ * Order cards the way they must actually be worked: dependencies first, then
142
+ * priority, then the board's own column/manual order.
143
+ *
144
+ * The managed projection previously sorted by `createdAt` — the moment a card
145
+ * happened to be created — which is unrelated to execution order, so the list
146
+ * the model reads top-to-bottom was not a plan. A dependency-respecting
147
+ * (Kahn) order makes the visible sequence the executable sequence; ties fall
148
+ * back to the deterministic board order so the list never reshuffles on its
149
+ * own. Any card left over by a dependency cycle is appended rather than
150
+ * dropped — the board rejects cycles, but a projection must never lose work.
151
+ */
152
+ export declare function orderTasksForTodos(board: KanbanBoard, tasks: readonly KanbanTask[]): KanbanTask[];
43
153
  /**
44
154
  * Replace the tactical todo list from the current cards on a session-owned board.
45
155
  *