@wrongstack/tools 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.
package/dist/index.js CHANGED
@@ -12991,7 +12991,7 @@ var IndexStore = class _IndexStore {
12991
12991
  const ftsSchema = this.stmt(
12992
12992
  "SELECT sql FROM sqlite_master WHERE type='table' AND name='symbols_fts'"
12993
12993
  ).get();
12994
- if (ftsSchema?.sql && ftsSchema.sql.includes("unicode61")) {
12994
+ if (ftsSchema?.sql?.includes("unicode61")) {
12995
12995
  this.db.exec("DROP TABLE IF EXISTS symbols_fts");
12996
12996
  }
12997
12997
  this.db.exec(SYMBOLS_FTS_SQL);
@@ -13484,9 +13484,13 @@ var IndexStore = class _IndexStore {
13484
13484
  sim: cosineSimilarity(queryVec, decodeVector(r.vector))
13485
13485
  })).sort((a, b) => b.sim - a.sim);
13486
13486
  const bm25Rank = /* @__PURE__ */ new Map();
13487
- bm25Rows.forEach((r, i) => bm25Rank.set(r.id, i));
13487
+ bm25Rows.forEach((r, i) => {
13488
+ bm25Rank.set(r.id, i);
13489
+ });
13488
13490
  const vecRank = /* @__PURE__ */ new Map();
13489
- vecScores.forEach((r, i) => vecRank.set(r.id, i));
13491
+ vecScores.forEach((r, i) => {
13492
+ vecRank.set(r.id, i);
13493
+ });
13490
13494
  const fused = reciprocalRankFusion(bm25Rank, vecRank, 60);
13491
13495
  const fusedScore = new Map(fused);
13492
13496
  const sorted = [...bm25Rows].sort(
@@ -14889,6 +14893,86 @@ import {
14889
14893
  isFrugalPerf
14890
14894
  } from "@wrongstack/core/utils";
14891
14895
 
14896
+ // src/codebase-index/content-hash.ts
14897
+ var PRIME64_1 = 0x9e3779b185ebca87n;
14898
+ var PRIME64_2 = 0xc2b2ae3d27d4eb4fn;
14899
+ var PRIME64_3 = 0x165667b19e3779f9n;
14900
+ var PRIME64_4 = 0x85ebca77c2b2ae63n;
14901
+ var PRIME64_5 = 0x27d4eb2f165667c5n;
14902
+ var MASK64 = 0xffffffffffffffffn;
14903
+ function mul64(a, b) {
14904
+ return (a & MASK64) * (b & MASK64) & MASK64;
14905
+ }
14906
+ function rotl64(x, n) {
14907
+ const v = x & MASK64;
14908
+ return (v << BigInt(n) | v >> BigInt(64 - n)) & MASK64;
14909
+ }
14910
+ function readU64LE(buf, off) {
14911
+ let v = 0n;
14912
+ for (let i = 7; i >= 0; i--) {
14913
+ v = v << 8n | BigInt(buf[off + i] ?? 0);
14914
+ }
14915
+ return v & MASK64;
14916
+ }
14917
+ function readU32LE(buf, off) {
14918
+ return (BigInt(buf[off] ?? 0) | BigInt(buf[off + 1] ?? 0) << 8n | BigInt(buf[off + 2] ?? 0) << 16n | BigInt(buf[off + 3] ?? 0) << 24n) & MASK64;
14919
+ }
14920
+ function xxh64Round(acc, lane) {
14921
+ return mul64(rotl64(acc + mul64(lane, PRIME64_2) & MASK64, 31), PRIME64_1);
14922
+ }
14923
+ function xxh64MergeRound(acc, val) {
14924
+ return mul64(acc ^ xxh64Round(0n, val), PRIME64_1) + PRIME64_4 & MASK64;
14925
+ }
14926
+ function xxhash64Hex(buf, explicitLen) {
14927
+ const length = explicitLen ?? buf.length;
14928
+ let h;
14929
+ let off = 0;
14930
+ if (length >= 32) {
14931
+ let v1 = PRIME64_1 + PRIME64_2 & MASK64;
14932
+ let v2 = PRIME64_2;
14933
+ let v3 = 0n;
14934
+ let v4 = 0n - PRIME64_1 & MASK64;
14935
+ const end32 = length - 32;
14936
+ while (off <= end32) {
14937
+ v1 = xxh64Round(v1, readU64LE(buf, off));
14938
+ v2 = xxh64Round(v2, readU64LE(buf, off + 8));
14939
+ v3 = xxh64Round(v3, readU64LE(buf, off + 16));
14940
+ v4 = xxh64Round(v4, readU64LE(buf, off + 24));
14941
+ off += 32;
14942
+ }
14943
+ h = rotl64(v1, 1) + rotl64(v2, 7) + rotl64(v3, 12) + rotl64(v4, 18) & MASK64;
14944
+ h = xxh64MergeRound(h, v1);
14945
+ h = xxh64MergeRound(h, v2);
14946
+ h = xxh64MergeRound(h, v3);
14947
+ h = xxh64MergeRound(h, v4);
14948
+ } else {
14949
+ h = PRIME64_5;
14950
+ }
14951
+ h = h + BigInt(length) & MASK64;
14952
+ while (off + 8 <= length) {
14953
+ const k1 = xxh64Round(0n, readU64LE(buf, off));
14954
+ h = mul64(rotl64(h ^ k1, 27), PRIME64_1) + PRIME64_4 & MASK64;
14955
+ off += 8;
14956
+ }
14957
+ if (off + 4 <= length) {
14958
+ h = mul64(rotl64(h ^ mul64(readU32LE(buf, off), PRIME64_1), 23), PRIME64_2) + PRIME64_3 & MASK64;
14959
+ off += 4;
14960
+ }
14961
+ while (off < length) {
14962
+ h = mul64(rotl64(h ^ mul64(BigInt(buf[off] ?? 0), PRIME64_5), 11), PRIME64_1) & MASK64;
14963
+ off += 1;
14964
+ }
14965
+ h = (h ^ h >> 33n) & MASK64;
14966
+ h = mul64(h, PRIME64_2);
14967
+ h = (h ^ h >> 29n) & MASK64;
14968
+ h = mul64(h, PRIME64_3);
14969
+ h = (h ^ h >> 32n) & MASK64;
14970
+ return h.toString(16).padStart(16, "0");
14971
+ }
14972
+ function xxhash64String(content) {
14973
+ return xxhash64Hex(new TextEncoder().encode(content));
14974
+ }
14975
+
14892
14976
  // src/codebase-index/gitignore.ts
14893
14977
  import * as fs14 from "node:fs/promises";
14894
14978
  import * as path18 from "node:path";
@@ -15616,91 +15700,14 @@ function getParserPool() {
15616
15700
  return _pool;
15617
15701
  }
15618
15702
 
15619
- // src/codebase-index/content-hash.ts
15620
- var PRIME64_1 = 0x9e3779b185ebca87n;
15621
- var PRIME64_2 = 0xc2b2ae3d27d4eb4fn;
15622
- var PRIME64_3 = 0x165667b19e3779f9n;
15623
- var PRIME64_4 = 0x85ebca77c2b2ae63n;
15624
- var PRIME64_5 = 0x27d4eb2f165667c5n;
15625
- var MASK64 = 0xffffffffffffffffn;
15626
- function mul64(a, b) {
15627
- return (a & MASK64) * (b & MASK64) & MASK64;
15628
- }
15629
- function rotl64(x, n) {
15630
- const v = x & MASK64;
15631
- return (v << BigInt(n) | v >> BigInt(64 - n)) & MASK64;
15632
- }
15633
- function readU64LE(buf, off) {
15634
- let v = 0n;
15635
- for (let i = 7; i >= 0; i--) {
15636
- v = v << 8n | BigInt(buf[off + i] ?? 0);
15637
- }
15638
- return v & MASK64;
15639
- }
15640
- function readU32LE(buf, off) {
15641
- return (BigInt(buf[off] ?? 0) | BigInt(buf[off + 1] ?? 0) << 8n | BigInt(buf[off + 2] ?? 0) << 16n | BigInt(buf[off + 3] ?? 0) << 24n) & MASK64;
15642
- }
15643
- function xxh64Round(acc, lane) {
15644
- return mul64(rotl64(acc + mul64(lane, PRIME64_2) & MASK64, 31), PRIME64_1);
15645
- }
15646
- function xxh64MergeRound(acc, val) {
15647
- return mul64(acc ^ xxh64Round(0n, val), PRIME64_1) + PRIME64_4 & MASK64;
15648
- }
15649
- function xxhash64Hex(buf, explicitLen) {
15650
- const length = explicitLen ?? buf.length;
15651
- let h;
15652
- let off = 0;
15653
- if (length >= 32) {
15654
- let v1 = PRIME64_1 + PRIME64_2 & MASK64;
15655
- let v2 = PRIME64_2;
15656
- let v3 = 0n;
15657
- let v4 = 0n - PRIME64_1 & MASK64;
15658
- const end32 = length - 32;
15659
- while (off <= end32) {
15660
- v1 = xxh64Round(v1, readU64LE(buf, off));
15661
- v2 = xxh64Round(v2, readU64LE(buf, off + 8));
15662
- v3 = xxh64Round(v3, readU64LE(buf, off + 16));
15663
- v4 = xxh64Round(v4, readU64LE(buf, off + 24));
15664
- off += 32;
15665
- }
15666
- h = rotl64(v1, 1) + rotl64(v2, 7) + rotl64(v3, 12) + rotl64(v4, 18) & MASK64;
15667
- h = xxh64MergeRound(h, v1);
15668
- h = xxh64MergeRound(h, v2);
15669
- h = xxh64MergeRound(h, v3);
15670
- h = xxh64MergeRound(h, v4);
15671
- } else {
15672
- h = PRIME64_5;
15673
- }
15674
- h = h + BigInt(length) & MASK64;
15675
- while (off + 8 <= length) {
15676
- const k1 = xxh64Round(0n, readU64LE(buf, off));
15677
- h = mul64(rotl64(h ^ k1, 27), PRIME64_1) + PRIME64_4 & MASK64;
15678
- off += 8;
15679
- }
15680
- if (off + 4 <= length) {
15681
- h = mul64(rotl64(h ^ mul64(readU32LE(buf, off), PRIME64_1), 23), PRIME64_2) + PRIME64_3 & MASK64;
15682
- off += 4;
15683
- }
15684
- while (off < length) {
15685
- h = mul64(rotl64(h ^ mul64(BigInt(buf[off] ?? 0), PRIME64_5), 11), PRIME64_1) & MASK64;
15686
- off += 1;
15687
- }
15688
- h = (h ^ h >> 33n) & MASK64;
15689
- h = mul64(h, PRIME64_2);
15690
- h = (h ^ h >> 29n) & MASK64;
15691
- h = mul64(h, PRIME64_3);
15692
- h = (h ^ h >> 32n) & MASK64;
15693
- return h.toString(16).padStart(16, "0");
15694
- }
15695
- function xxhash64String(content) {
15696
- return xxhash64Hex(new TextEncoder().encode(content));
15697
- }
15698
-
15699
15703
  // src/codebase-index/indexer.ts
15700
15704
  var YIELD_EVERY_N = 50;
15701
15705
  function resolveParallelBatch() {
15702
15706
  return indexParallelBatchSize(availableParallelism());
15703
15707
  }
15708
+ function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
15709
+ return !isFrugalPerf() && candidateFileCount >= WORKER_POOL_THRESHOLD && parseBatchCount > 1;
15710
+ }
15704
15711
  function yieldEventLoop() {
15705
15712
  return new Promise((resolve17) => setImmediate(resolve17));
15706
15713
  }
@@ -15877,11 +15884,7 @@ async function resolveProjectRelations(store, projectRoot, opts) {
15877
15884
  const structure = await detectModuleRoots(projectRoot, indexedFiles);
15878
15885
  if (opts.signal?.aborted) return;
15879
15886
  store.setFilePackages(assignPackageLabels(structure, indexedFiles));
15880
- const resolver = new ModuleResolver(
15881
- structure,
15882
- indexedFiles,
15883
- store.getNamespaceDeclarations()
15884
- );
15887
+ const resolver = new ModuleResolver(structure, indexedFiles, store.getNamespaceDeclarations());
15885
15888
  const pending2 = store.getUnresolvedImports(opts.onlyFiles);
15886
15889
  const resolutions = [];
15887
15890
  for (const entry of pending2) {
@@ -15906,6 +15909,10 @@ async function runIndexerWithStore(store, opts) {
15906
15909
  const errors = [];
15907
15910
  const langStats = {};
15908
15911
  let filesIndexed = 0;
15912
+ let filesParsed = 0;
15913
+ let filesSkipped = 0;
15914
+ let filesEmpty = 0;
15915
+ let filesFailed = 0;
15909
15916
  let symbolsIndexed = 0;
15910
15917
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
15911
15918
  let files;
@@ -15947,12 +15954,14 @@ async function runIndexerWithStore(store, opts) {
15947
15954
  langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
15948
15955
  symbolsIndexed += meta.symbolCount;
15949
15956
  filesIndexed++;
15957
+ filesSkipped++;
15950
15958
  filesPreSkipped++;
15951
15959
  return false;
15952
15960
  });
15953
15961
  if (filesPreSkipped > 0) opts.onProgress?.(filesPreSkipped, totalFilesForProgress);
15954
15962
  }
15955
15963
  const parallelBatch = resolveParallelBatch();
15964
+ const parserPoolCandidateCount = files.length;
15956
15965
  let filesSinceLastYield = 0;
15957
15966
  for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
15958
15967
  const batchEnd = Math.min(batchStart + parallelBatch, files.length);
@@ -16045,7 +16054,7 @@ async function runIndexerWithStore(store, opts) {
16045
16054
  });
16046
16055
  }
16047
16056
  if (toParse.length > 0) {
16048
- let pool = toParse.length >= WORKER_POOL_THRESHOLD ? getParserPool() : null;
16057
+ let pool = shouldUseParserWorkerPool(parserPoolCandidateCount, toParse.length) ? getParserPool() : null;
16049
16058
  if (pool) {
16050
16059
  try {
16051
16060
  await pool.ensureReady();
@@ -16095,12 +16104,14 @@ async function runIndexerWithStore(store, opts) {
16095
16104
  const err = settled.reason;
16096
16105
  if (err instanceof Error && isAbortError(err)) throw err;
16097
16106
  errors.push(`batch error: ${file}: ${err instanceof Error ? err.message : String(err)}`);
16107
+ filesFailed++;
16098
16108
  continue;
16099
16109
  }
16100
16110
  const result = settled.value;
16101
16111
  if (result.error) {
16102
16112
  if (result.missing) store.deleteFile(file);
16103
16113
  errors.push(`${file}: ${result.error}`);
16114
+ filesFailed++;
16104
16115
  continue;
16105
16116
  }
16106
16117
  const { stat: stat19, lang, parsed } = result;
@@ -16108,6 +16119,7 @@ async function runIndexerWithStore(store, opts) {
16108
16119
  langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
16109
16120
  symbolsIndexed += result.skippedMeta.symbolCount;
16110
16121
  filesIndexed++;
16122
+ filesSkipped++;
16111
16123
  const stored = existingMeta.get(file);
16112
16124
  if (stored && stored.mtimeMs !== result.skippedMeta.mtimeMs) {
16113
16125
  store.upsertFile({
@@ -16132,6 +16144,7 @@ async function runIndexerWithStore(store, opts) {
16132
16144
  contentHash: result.contentHash ?? ""
16133
16145
  });
16134
16146
  filesIndexed++;
16147
+ filesEmpty++;
16135
16148
  }
16136
16149
  continue;
16137
16150
  }
@@ -16145,6 +16158,7 @@ async function runIndexerWithStore(store, opts) {
16145
16158
  contentHash: result.contentHash ?? ""
16146
16159
  });
16147
16160
  filesIndexed++;
16161
+ filesEmpty++;
16148
16162
  continue;
16149
16163
  }
16150
16164
  batchEntries.push({
@@ -16166,6 +16180,7 @@ async function runIndexerWithStore(store, opts) {
16166
16180
  symbolsIndexed += count;
16167
16181
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
16168
16182
  filesIndexed++;
16183
+ filesParsed++;
16169
16184
  }
16170
16185
  } catch (err) {
16171
16186
  const message = err instanceof Error ? err.message : String(err);
@@ -16178,6 +16193,7 @@ async function runIndexerWithStore(store, opts) {
16178
16193
  symbolsIndexed += symbolsWithIds.length;
16179
16194
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
16180
16195
  filesIndexed++;
16196
+ filesParsed++;
16181
16197
  if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
16182
16198
  const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
16183
16199
  if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
@@ -16195,6 +16211,7 @@ async function runIndexerWithStore(store, opts) {
16195
16211
  contentHash: entry.contentHash
16196
16212
  });
16197
16213
  } catch (innerErr) {
16214
+ filesFailed++;
16198
16215
  errors.push(
16199
16216
  `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
16200
16217
  );
@@ -16227,6 +16244,12 @@ async function runIndexerWithStore(store, opts) {
16227
16244
  const durationMs = Date.now() - startMs;
16228
16245
  return {
16229
16246
  filesIndexed,
16247
+ fileOutcomes: {
16248
+ parsed: filesParsed,
16249
+ skipped: filesSkipped,
16250
+ empty: filesEmpty,
16251
+ failed: filesFailed
16252
+ },
16230
16253
  symbolsIndexed,
16231
16254
  langStats,
16232
16255
  durationMs,
@@ -22847,7 +22870,6 @@ import { randomUUID as randomUUID2 } from "node:crypto";
22847
22870
  import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
22848
22871
  import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
22849
22872
  import {
22850
- addColumn,
22851
22873
  addTask,
22852
22874
  adoptManagedLifecycle,
22853
22875
  assignTask,
@@ -22862,7 +22884,7 @@ import {
22862
22884
  exportBoardToTaskGraph,
22863
22885
  finalizeTaskCompletion,
22864
22886
  getBoard as getBoard3,
22865
- getKanbanOrchestrationSnapshot,
22887
+ getKanbanOrchestrationSnapshot as getKanbanOrchestrationSnapshot2,
22866
22888
  getKanbanQueueHealth,
22867
22889
  getTask,
22868
22890
  getTaskChain,
@@ -22876,16 +22898,16 @@ import {
22876
22898
  recoverStaleTaskAssignments,
22877
22899
  releaseTaskClaim,
22878
22900
  removeBoard as removeBoard2,
22879
- removeColumn,
22880
22901
  removeTask,
22881
22902
  repairManagedTaskProjection,
22903
+ resolveAutoAccept,
22882
22904
  searchKanban,
22883
22905
  setTaskChain,
22906
+ stripLifecycleIssues,
22884
22907
  syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
22885
22908
  transferTaskToBoard,
22886
22909
  transitionTask,
22887
22910
  updateBoard as updateBoard2,
22888
- updateColumn,
22889
22911
  updateTask as updateTask2,
22890
22912
  updateTaskAssignment,
22891
22913
  verifyTaskCompletion as verifyTaskCompletion2
@@ -22935,6 +22957,137 @@ function duplicateBoardOptions(input) {
22935
22957
  };
22936
22958
  }
22937
22959
 
22960
+ // src/kanban-contract-actions.ts
22961
+ import {
22962
+ addContractEdge,
22963
+ configureContractGraph,
22964
+ evaluateTaskContractGraph,
22965
+ getContractGraph,
22966
+ removeContractEdge,
22967
+ removeContractNode,
22968
+ upsertContractNode
22969
+ } from "@wrongstack/kanban";
22970
+
22971
+ // src/kanban-tool-results.ts
22972
+ function atomicityNudge(task) {
22973
+ if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
22974
+ const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
22975
+ return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
22976
+ }
22977
+ function readEnvGateEnforcement() {
22978
+ const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
22979
+ return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
22980
+ }
22981
+ function fail(message) {
22982
+ return { ok: false, message };
22983
+ }
22984
+ function okBoard(board, message = "Board loaded.") {
22985
+ return { ok: true, message, board };
22986
+ }
22987
+ function okTask(board, task, message) {
22988
+ return { ok: true, message, board, task };
22989
+ }
22990
+
22991
+ // src/kanban-contract-actions.ts
22992
+ async function handleKanbanContractAction(projectRoot, input, actor) {
22993
+ switch (input.action) {
22994
+ case "get_contract_graph": {
22995
+ if (!input.boardId) return fail("get_contract_graph requires boardId.");
22996
+ const found = await getContractGraph(projectRoot, input.boardId);
22997
+ if (!found) return fail("Board not found.");
22998
+ const evaluated = input.taskId ? await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId) : null;
22999
+ if (input.taskId && !evaluated) return fail("Task not found on this board.");
23000
+ return {
23001
+ ok: true,
23002
+ message: found.graph ? `Contract map: ${found.graph.nodes.length} node(s), ${found.graph.edges.length} edge(s), enforcement ${found.graph.enforcement}.` : "No contract map on this board yet. Call configure_contract_graph to start one.",
23003
+ board: found.board,
23004
+ contractGraph: found.graph,
23005
+ ...evaluated ? { contractEvaluation: evaluated.evaluation } : {}
23006
+ };
23007
+ }
23008
+ case "configure_contract_graph": {
23009
+ if (!input.boardId) return fail("configure_contract_graph requires boardId.");
23010
+ const enforcement = input.contractEnforcement ?? "advisory";
23011
+ const board = await configureContractGraph(projectRoot, input.boardId, enforcement);
23012
+ return board ? okBoard(board, `Contract map enforcement set to ${enforcement}.`) : fail("Board not found.");
23013
+ }
23014
+ case "upsert_contract_node": {
23015
+ if (!input.boardId || !input.taskId) {
23016
+ return fail("upsert_contract_node requires boardId and taskId.");
23017
+ }
23018
+ if (!input.contractNodeKind || !input.contractNodeTitle) {
23019
+ return fail("upsert_contract_node requires contractNodeKind and contractNodeTitle.");
23020
+ }
23021
+ const waiver = input.contractNodeState === "waived" ? {
23022
+ actor: actor ?? "agent",
23023
+ reason: input.contractWaiverReason ?? "",
23024
+ at: (/* @__PURE__ */ new Date()).toISOString()
23025
+ } : void 0;
23026
+ if (waiver && !waiver.reason.trim()) {
23027
+ return fail("A waived contract node requires contractWaiverReason.");
23028
+ }
23029
+ const result = await upsertContractNode(projectRoot, input.boardId, {
23030
+ taskId: input.taskId,
23031
+ kind: input.contractNodeKind,
23032
+ title: input.contractNodeTitle,
23033
+ ...input.contractNodeId !== void 0 ? { id: input.contractNodeId } : {},
23034
+ ...input.contractNodeDescription !== void 0 ? { description: input.contractNodeDescription } : {},
23035
+ ...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
23036
+ ...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
23037
+ ...input.contractCheckId !== void 0 ? { checkId: input.contractCheckId } : {},
23038
+ ...input.contractMetricId !== void 0 ? { metricId: input.contractMetricId } : {},
23039
+ ...waiver ? { waiver } : {},
23040
+ ...actor !== void 0 ? { createdBy: actor } : {}
23041
+ });
23042
+ if (!result) return fail("Board or task not found.");
23043
+ return {
23044
+ ok: true,
23045
+ message: `Contract node ${result.node.kind} "${result.node.title}" saved (${result.node.id}).`,
23046
+ board: result.board,
23047
+ contractGraph: result.board.contractGraph ?? null
23048
+ };
23049
+ }
23050
+ case "remove_contract_node": {
23051
+ if (!input.boardId || !input.contractNodeId) {
23052
+ return fail("remove_contract_node requires boardId and contractNodeId.");
23053
+ }
23054
+ const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
23055
+ return board ? okBoard(board, "Contract node removed, along with every edge that touched it.") : fail("Contract node not found.");
23056
+ }
23057
+ case "add_contract_edge": {
23058
+ if (!input.boardId || !input.contractEdgeFrom || !input.contractEdgeTo) {
23059
+ return fail("add_contract_edge requires boardId, contractEdgeFrom, and contractEdgeTo.");
23060
+ }
23061
+ if (!input.contractEdgeType) return fail("add_contract_edge requires contractEdgeType.");
23062
+ const result = await addContractEdge(projectRoot, input.boardId, {
23063
+ from: input.contractEdgeFrom,
23064
+ to: input.contractEdgeTo,
23065
+ type: input.contractEdgeType,
23066
+ ...input.contractEdgeId !== void 0 ? { id: input.contractEdgeId } : {},
23067
+ ...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
23068
+ ...input.contractEdgeRationale !== void 0 ? { rationale: input.contractEdgeRationale } : {},
23069
+ ...actor !== void 0 ? { createdBy: actor } : {}
23070
+ });
23071
+ if (!result) return fail("Board not found.");
23072
+ return {
23073
+ ok: true,
23074
+ message: `Contract edge ${result.edge.type}: ${result.edge.from} \u2192 ${result.edge.to}.`,
23075
+ board: result.board,
23076
+ contractGraph: result.board.contractGraph ?? null
23077
+ };
23078
+ }
23079
+ case "remove_contract_edge": {
23080
+ if (!input.boardId || !input.contractEdgeId) {
23081
+ return fail("remove_contract_edge requires boardId and contractEdgeId.");
23082
+ }
23083
+ const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
23084
+ return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
23085
+ }
23086
+ default:
23087
+ return void 0;
23088
+ }
23089
+ }
23090
+
22938
23091
  // src/kanban-decomposition-actions.ts
22939
23092
  import {
22940
23093
  assessTaskAtomicity,
@@ -22966,26 +23119,6 @@ function recordKanbanVerificationEvidence(ctx, report) {
22966
23119
  }
22967
23120
  }
22968
23121
 
22969
- // src/kanban-tool-results.ts
22970
- function atomicityNudge(task) {
22971
- if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
22972
- const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
22973
- return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
22974
- }
22975
- function readEnvGateEnforcement() {
22976
- const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
22977
- return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
22978
- }
22979
- function fail(message) {
22980
- return { ok: false, message };
22981
- }
22982
- function okBoard(board, message = "Board loaded.") {
22983
- return { ok: true, message, board };
22984
- }
22985
- function okTask(board, task, message) {
22986
- return { ok: true, message, board, task };
22987
- }
22988
-
22989
23122
  // src/kanban-decomposition-actions.ts
22990
23123
  async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
22991
23124
  switch (input.action) {
@@ -23070,20 +23203,14 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
23070
23203
  // src/kanban-detail-actions.ts
23071
23204
  import {
23072
23205
  addCheckToTask,
23073
- addContractEdge,
23074
23206
  addDependency,
23075
23207
  addGoalMetricToTask,
23076
23208
  addLinkToTask,
23077
23209
  addNoteToTask,
23078
- configureContractGraph,
23079
- evaluateTaskContractGraph,
23080
- getContractGraph,
23081
23210
  getKanbanWorkbench,
23082
- removeContractEdge,
23083
- removeContractNode,
23211
+ removeCheckFromTask,
23084
23212
  updateCheckOnTask,
23085
- updateGoalMetricOnTask,
23086
- upsertContractNode
23213
+ updateGoalMetricOnTask
23087
23214
  } from "@wrongstack/kanban";
23088
23215
 
23089
23216
  // src/kanban-split-task-handler.ts
@@ -23149,131 +23276,6 @@ async function handleKanbanDetailAction(projectRoot, input) {
23149
23276
  workbench
23150
23277
  };
23151
23278
  }
23152
- case "get_contract_graph": {
23153
- if (!input.boardId) return fail("get_contract_graph requires boardId.");
23154
- const result = await getContractGraph(projectRoot, input.boardId);
23155
- return result ? {
23156
- ok: true,
23157
- message: result.graph ? `${result.graph.nodes.length} contract node(s), ${result.graph.edges.length} edge(s).` : "Contract graph is not configured.",
23158
- board: result.board,
23159
- ...result.graph ? { contractGraph: result.graph } : {}
23160
- } : fail("Board not found.");
23161
- }
23162
- case "configure_contract_graph": {
23163
- if (!input.boardId || !input.contractGraphEnforcement) {
23164
- return fail("configure_contract_graph requires boardId and contractGraphEnforcement.");
23165
- }
23166
- const current = await getContractGraph(projectRoot, input.boardId);
23167
- if (!current) return fail("Board not found.");
23168
- if (input.contractGraphEnforcement === "strict" && current.graph?.enforcement !== "strict") {
23169
- return fail(
23170
- "Strict Contract Map enforcement is operator-owned. Autonomous agents may use advisory maps but may not turn them into an execution gate."
23171
- );
23172
- }
23173
- if (current.graph?.enforcement === "strict" && input.contractGraphEnforcement !== "strict") {
23174
- return fail("An autonomous agent may not loosen a strict contract graph.");
23175
- }
23176
- const board = await configureContractGraph(
23177
- projectRoot,
23178
- input.boardId,
23179
- input.contractGraphEnforcement
23180
- );
23181
- return board ? okBoard(board, "Contract graph configured.") : fail("Board not found.");
23182
- }
23183
- case "upsert_contract_node": {
23184
- if (!input.boardId || !input.taskId || !input.contractNodeKind || !input.title) {
23185
- return fail("upsert_contract_node requires boardId, taskId, contractNodeKind, and title.");
23186
- }
23187
- if (input.contractNodeState === "waived") {
23188
- return fail(
23189
- "The autonomous kanban tool may not waive contract nodes; a human-owned review surface must record that exception."
23190
- );
23191
- }
23192
- if (input.contractNodeId) {
23193
- const current = await getContractGraph(projectRoot, input.boardId);
23194
- const existing = current?.graph?.nodes.find((node) => node.id === input.contractNodeId);
23195
- if (current?.graph?.enforcement === "strict" && existing && (existing.kind !== input.contractNodeKind || input.contractEnforcement !== void 0 && input.contractEnforcement !== existing.enforcement)) {
23196
- return fail(
23197
- "The autonomous kanban tool may not change the kind or enforcement of an existing strict contract node."
23198
- );
23199
- }
23200
- }
23201
- const result = await upsertContractNode(projectRoot, input.boardId, {
23202
- ...input.contractNodeId ? { id: input.contractNodeId } : {},
23203
- taskId: input.taskId,
23204
- kind: input.contractNodeKind,
23205
- title: input.title,
23206
- ...input.description !== void 0 ? { description: input.description } : {},
23207
- ...input.contractEnforcement !== void 0 ? { enforcement: input.contractEnforcement } : {},
23208
- ...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
23209
- ...input.checkId !== void 0 ? { checkId: input.checkId } : {},
23210
- ...input.metricId !== void 0 ? { metricId: input.metricId } : {},
23211
- ...input.baseline !== void 0 ? { baseline: input.baseline } : {},
23212
- ...input.threshold !== void 0 ? { threshold: input.threshold } : {},
23213
- ...input.author !== void 0 ? { createdBy: input.author } : {}
23214
- });
23215
- return result ? {
23216
- ...okBoard(result.board, "Contract node saved."),
23217
- contractGraph: result.board.contractGraph
23218
- } : fail("Task not found.");
23219
- }
23220
- case "link_contract_nodes": {
23221
- if (!input.boardId || !input.fromNodeId || !input.toNodeId || !input.contractEdgeType) {
23222
- return fail(
23223
- "link_contract_nodes requires boardId, fromNodeId, toNodeId, and contractEdgeType."
23224
- );
23225
- }
23226
- const result = await addContractEdge(projectRoot, input.boardId, {
23227
- from: input.fromNodeId,
23228
- to: input.toNodeId,
23229
- type: input.contractEdgeType,
23230
- ...input.contractEdgeId ? { id: input.contractEdgeId } : {},
23231
- ...input.contractEnforcement ? { enforcement: input.contractEnforcement } : {},
23232
- ...input.contractRationale ? { rationale: input.contractRationale } : {},
23233
- ...input.author ? { createdBy: input.author } : {}
23234
- });
23235
- return result ? {
23236
- ...okBoard(result.board, "Contract edge added."),
23237
- contractGraph: result.board.contractGraph
23238
- } : fail("Board not found.");
23239
- }
23240
- case "remove_contract_node": {
23241
- if (!input.boardId || !input.contractNodeId) {
23242
- return fail("remove_contract_node requires boardId and contractNodeId.");
23243
- }
23244
- const current = await getContractGraph(projectRoot, input.boardId);
23245
- const node = current?.graph?.nodes.find((candidate) => candidate.id === input.contractNodeId);
23246
- if (current?.graph?.enforcement === "strict" && node?.enforcement === "blocking") {
23247
- return fail("The autonomous kanban tool may not remove a blocking strict contract node.");
23248
- }
23249
- const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
23250
- return board ? okBoard(board, "Contract node removed.") : fail("Contract node not found.");
23251
- }
23252
- case "remove_contract_edge": {
23253
- if (!input.boardId || !input.contractEdgeId) {
23254
- return fail("remove_contract_edge requires boardId and contractEdgeId.");
23255
- }
23256
- const current = await getContractGraph(projectRoot, input.boardId);
23257
- const edge = current?.graph?.edges.find((candidate) => candidate.id === input.contractEdgeId);
23258
- if (current?.graph?.enforcement === "strict" && edge?.enforcement === "blocking") {
23259
- return fail("The autonomous kanban tool may not remove a blocking strict contract edge.");
23260
- }
23261
- const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
23262
- return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
23263
- }
23264
- case "evaluate_contract_graph": {
23265
- if (!input.boardId || !input.taskId) {
23266
- return fail("evaluate_contract_graph requires boardId and taskId.");
23267
- }
23268
- const result = await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId);
23269
- return result ? {
23270
- ok: result.evaluation.allowed,
23271
- message: result.evaluation.allowed ? "Contract graph is closed." : `Contract graph has ${result.evaluation.issues.length} unresolved issue(s).`,
23272
- board: result.board,
23273
- contractGraph: result.board.contractGraph,
23274
- contractEvaluation: result.evaluation
23275
- } : fail("Task not found.");
23276
- }
23277
23279
  case "add_dependency": {
23278
23280
  if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
23279
23281
  return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
@@ -23326,8 +23328,9 @@ async function handleKanbanDetailAction(projectRoot, input) {
23326
23328
  }
23327
23329
  const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
23328
23330
  description: input.checkDescription,
23329
- type: "manual",
23330
- status: input.checkStatus
23331
+ type: input.checkType ?? "manual",
23332
+ status: input.checkStatus,
23333
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
23331
23334
  });
23332
23335
  return board ? okBoard(board, "Check added.") : fail("Task not found.");
23333
23336
  }
@@ -23342,11 +23345,27 @@ async function handleKanbanDetailAction(projectRoot, input) {
23342
23345
  input.checkId,
23343
23346
  {
23344
23347
  ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
23345
- ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
23348
+ ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {},
23349
+ // Promoting an existing manual criterion to an executable one is the
23350
+ // common repair: the card was written before anyone knew the command.
23351
+ ...input.checkType !== void 0 ? { type: input.checkType } : {},
23352
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
23346
23353
  }
23347
23354
  );
23348
23355
  return board ? okBoard(board, "Check updated.") : fail("Check not found.");
23349
23356
  }
23357
+ case "remove_check": {
23358
+ if (!input.boardId || !input.taskId || !input.checkId) {
23359
+ return fail("remove_check requires boardId, taskId, and checkId.");
23360
+ }
23361
+ const board = await removeCheckFromTask(
23362
+ projectRoot,
23363
+ input.boardId,
23364
+ input.taskId,
23365
+ input.checkId
23366
+ );
23367
+ return board ? okBoard(board, "Acceptance criterion removed.") : fail("Check not found on this task.");
23368
+ }
23350
23369
  case "add_note": {
23351
23370
  if (!input.boardId || !input.taskId || !input.note)
23352
23371
  return fail("add_note requires boardId, taskId, and note.");
@@ -23421,14 +23440,25 @@ function taskInput(input) {
23421
23440
  ...input.order !== void 0 ? { order: input.order } : {},
23422
23441
  ...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
23423
23442
  ...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
23443
+ // The system prompt has always told the model it may "set atomic: true"
23444
+ // when creating a composite parent. It could not: the field reached
23445
+ // neither the create input nor the patch, so the instruction described a
23446
+ // capability that did not exist and the attempt was silently dropped.
23447
+ ...input.atomic !== void 0 ? { atomic: input.atomic } : {},
23424
23448
  ...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
23425
23449
  ...input.checkDescription !== void 0 ? {
23426
23450
  successCriteria: [
23427
23451
  {
23428
23452
  id: randomUUID(),
23429
23453
  description: input.checkDescription,
23430
- type: "manual",
23431
- status: input.checkStatus ?? "pending"
23454
+ // `manual` only as the fallback. Hard-coding it here meant every
23455
+ // agent-authored criterion was unverifiable by construction: the
23456
+ // deterministic plugins never matched, the registry passed the
23457
+ // hand-set status straight through, and "verified" collapsed into
23458
+ // "the author ticked its own box".
23459
+ type: input.checkType ?? "manual",
23460
+ status: input.checkStatus ?? "pending",
23461
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
23432
23462
  }
23433
23463
  ]
23434
23464
  } : {},
@@ -23476,11 +23506,11 @@ function taskInput(input) {
23476
23506
  };
23477
23507
  }
23478
23508
  function mergedDependsOn(input) {
23479
- const ids = [
23509
+ if (input.dependsOn === void 0 && input.dependencyTaskId === void 0) return void 0;
23510
+ return [
23480
23511
  ...input.dependsOn ?? [],
23481
23512
  ...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
23482
23513
  ].filter((id, i, arr) => id && arr.indexOf(id) === i);
23483
- return ids.length > 0 ? ids : void 0;
23484
23514
  }
23485
23515
  function taskPatch(input) {
23486
23516
  return {
@@ -23494,7 +23524,15 @@ function taskPatch(input) {
23494
23524
  status: input.status,
23495
23525
  labels: input.labels,
23496
23526
  assignedAgent: input.agentId,
23497
- ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
23527
+ ...mergedDependsOn(input) !== void 0 ? { dependsOn: mergedDependsOn(input) } : {},
23528
+ // `atomic` and `childTaskIds` are the composite-parent contract, and the
23529
+ // managed gate reads both: an `atomic` parent may not move forward without
23530
+ // children, and may not reach Done until every child is completed. The
23531
+ // manager has always accepted both on a patch; only this surface withheld
23532
+ // them, so `split_atomic` was a one-way door — delete the children and the
23533
+ // parent was stranded with no way to declare itself a leaf again.
23534
+ ...input.atomic !== void 0 ? { atomic: input.atomic } : {},
23535
+ ...input.childTaskIds !== void 0 ? { childTaskIds: input.childTaskIds } : {},
23498
23536
  ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
23499
23537
  ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
23500
23538
  };
@@ -23554,8 +23592,8 @@ function assignmentForTaskCreate(input) {
23554
23592
  }
23555
23593
 
23556
23594
  // src/kanban-tool-schema.ts
23557
- var KANBAN_TOOL_DESCRIPTION = "Manage and audit project-scoped multi-kanban boards through the shared IPC Kanban server and its SQLite store. Managed cards enforce fully specified details and adjacent Backlog \u2192 Todo \u2192 Running \u2192 Review \u2192 Done transitions with persistent comments and evidence. Contract Map actions are optional advisory metadata unless an operator explicitly enabled strict enforcement. Use verify_completion to validate executable success criteria before Done.";
23558
- var KANBAN_TOOL_USAGE_HINT = "Use this for durable project kanban state. Read workbench when orienting across boards; it returns bounded Now, Next, Blocked, Review lanes and operational alerts. Before coding mutation, create a fully detailed card on a managed board with executable acceptance criteria, then call start_task. The runtime blocks product mutations until start_task binds a ready Running card. Do not create, inspect, repair, or enable a Contract Map during ordinary work; every map mode stays off the execution and completion path. Worker completion enters Review; Done requires passed acceptance criteria and review evidence. Surface existing strict-map findings for operator review without stopping work to repair them.";
23595
+ var KANBAN_TOOL_DESCRIPTION = "Durable project task boards: create and move cards, record checks, notes, links and assignments. The board is a record of the work, not a permit for it \u2014 nothing here gates other tools. Managed boards additionally enforce ordered Backlog \u2192 Todo \u2192 Running \u2192 Review \u2192 Done transitions; release_managed_lifecycle turns that off.";
23596
+ var KANBAN_TOOL_USAGE_HINT = 'Track substantial or multi-step work so it survives the session; a trivial edit or a question needs no card. Work stays on ONE board: call list_boards first and add_task to the board this project already uses. create_board is for a genuinely separate line of work, not for each new piece of it \u2014 a second board splits the same effort in two, and a board holding a single card is the usual sign. Common flow: list_boards or search_tasks to orient, add_task to record work, start_task when you begin, update_check with checkStatus "passed" to tick acceptance criteria (read their ids from get_task), then transition_task. On a managed board a refused transition names the field it wants \u2014 supply it and retry. When the acceptance criterion is something a machine can run, say so: set checkType ("command", "test", "file_exists", "file_matches", "git_diff", "metric") and put the command, pattern or path in checkNotes, then verify_completion executes it and the result is real evidence. Leave checkType off (or "manual") only for criteria that genuinely need a human eye \u2014 a manual check records your assertion, it does not test anything.';
23559
23597
  var KANBAN_INPUT_SCHEMA = {
23560
23598
  type: "object",
23561
23599
  properties: {
@@ -23568,6 +23606,7 @@ var KANBAN_INPUT_SCHEMA = {
23568
23606
  "duplicate_board",
23569
23607
  "update_board",
23570
23608
  "adopt_managed_lifecycle",
23609
+ "release_managed_lifecycle",
23571
23610
  "delete_board",
23572
23611
  "generate_board",
23573
23612
  "export_markdown",
@@ -23579,9 +23618,6 @@ var KANBAN_INPUT_SCHEMA = {
23579
23618
  "ready_tasks",
23580
23619
  "snapshot",
23581
23620
  "workbench",
23582
- "add_column",
23583
- "update_column",
23584
- "delete_column",
23585
23621
  "add_task",
23586
23622
  "split_task",
23587
23623
  "merge_tasks",
@@ -23596,13 +23632,6 @@ var KANBAN_INPUT_SCHEMA = {
23596
23632
  "delete_task",
23597
23633
  "set_chain",
23598
23634
  "get_chain",
23599
- "get_contract_graph",
23600
- "configure_contract_graph",
23601
- "upsert_contract_node",
23602
- "link_contract_nodes",
23603
- "remove_contract_node",
23604
- "remove_contract_edge",
23605
- "evaluate_contract_graph",
23606
23635
  "claim_task",
23607
23636
  "release_task",
23608
23637
  "assign_task",
@@ -23616,49 +23645,27 @@ var KANBAN_INPUT_SCHEMA = {
23616
23645
  "update_goal_metric",
23617
23646
  "add_check",
23618
23647
  "update_check",
23648
+ "remove_check",
23619
23649
  "add_note",
23620
23650
  "add_link",
23621
23651
  "verify_completion",
23622
23652
  "split_atomic",
23623
23653
  "assess_atomicity",
23624
- "propose_decomposition"
23654
+ "propose_decomposition",
23655
+ "get_contract_graph",
23656
+ "configure_contract_graph",
23657
+ "upsert_contract_node",
23658
+ "remove_contract_node",
23659
+ "add_contract_edge",
23660
+ "remove_contract_edge"
23625
23661
  ]
23626
23662
  },
23627
23663
  boardId: { type: "string" },
23628
23664
  taskId: { type: "string" },
23629
23665
  taskIds: { type: "array", items: { type: "string" } },
23630
23666
  chainId: { type: "string" },
23631
- contractNodeId: { type: "string" },
23632
- contractNodeKind: {
23633
- type: "string",
23634
- enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"]
23635
- },
23636
- contractNodeState: {
23637
- type: "string",
23638
- enum: ["unknown", "active", "satisfied", "violated", "resolved"]
23639
- },
23640
- contractEnforcement: {
23641
- type: "string",
23642
- enum: ["blocking", "advisory", "informational"]
23643
- },
23644
- contractGraphEnforcement: { type: "string", enum: ["off", "advisory", "strict"] },
23645
- contractEdgeId: { type: "string" },
23646
- contractEdgeType: {
23647
- type: "string",
23648
- enum: [
23649
- "targets",
23650
- "affects",
23651
- "must_preserve",
23652
- "exposes",
23653
- "verified_by",
23654
- "conflicts_with",
23655
- "derived_from",
23656
- "relates_to"
23657
- ]
23658
- },
23659
23667
  fromNodeId: { type: "string" },
23660
23668
  toNodeId: { type: "string" },
23661
- contractRationale: { type: "string" },
23662
23669
  baseline: { oneOf: [{ type: "string" }, { type: "number" }] },
23663
23670
  threshold: { oneOf: [{ type: "string" }, { type: "number" }] },
23664
23671
  columnId: { type: "string" },
@@ -23742,7 +23749,20 @@ var KANBAN_INPUT_SCHEMA = {
23742
23749
  costCeilingUsd: { type: "number" },
23743
23750
  retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
23744
23751
  lastFailureKind: { type: "string" },
23745
- dependsOn: { type: "array", items: { type: "string" } },
23752
+ dependsOn: {
23753
+ type: "array",
23754
+ items: { type: "string" },
23755
+ description: "Task ids this card waits on. On update_task an explicit empty array clears them \u2014 use it when a dependency was recorded in error rather than completing work nobody wants."
23756
+ },
23757
+ atomic: {
23758
+ type: "boolean",
23759
+ description: "Composite parent (true) or executable leaf (false). Set false to make a stranded parent a leaf again after its children were dropped."
23760
+ },
23761
+ childTaskIds: {
23762
+ type: "array",
23763
+ items: { type: "string" },
23764
+ description: "Children of a composite parent. On update_task an explicit empty array detaches them all."
23765
+ },
23746
23766
  estimatedHours: { type: "number" },
23747
23767
  actualHours: { type: "number" },
23748
23768
  taskGraph: { type: "object" },
@@ -23776,6 +23796,74 @@ var KANBAN_INPUT_SCHEMA = {
23776
23796
  checkId: { type: "string" },
23777
23797
  checkDescription: { type: "string" },
23778
23798
  checkStatus: { type: "string", enum: ["pending", "passed", "failed", "skipped"] },
23799
+ checkType: {
23800
+ type: "string",
23801
+ // Only types a verifier can actually execute. `manual` is the default and
23802
+ // means a human or agent asserts the status by hand. The rest are run by
23803
+ // `verify_completion` against the default deterministic registry. Types
23804
+ // with no plugin in that registry (`auto`, `review`, `agent`, `council`)
23805
+ // are deliberately omitted: offering them would produce criteria that
23806
+ // silently report `skipped — no verifier plugin registered`.
23807
+ enum: ["manual", "command", "test", "file_exists", "file_matches", "git_diff", "metric"],
23808
+ description: 'How this acceptance criterion is verified. Default "manual" (status set by hand). Any other value makes verify_completion execute it, so the criterion becomes real evidence rather than a self-assertion. Pair with checkNotes.'
23809
+ },
23810
+ checkNotes: {
23811
+ type: "string",
23812
+ description: 'The executable body for a non-manual checkType, read in preference to checkDescription. command/test: the shell command or test pattern. file_exists: the path. file_matches: JSON {"file","pattern","flags"}. git_diff: JSON {"expectedFiles","minChanges","maxChanges"}.'
23813
+ },
23814
+ // ── Contract map ───────────────────────────────────────────────────
23815
+ // The card contract: what this work targets, what it must not break, what
23816
+ // it risks, and what verifies it. Advisory by default — the readiness gate
23817
+ // deliberately does not require map structure, so a map is an operator
23818
+ // review aid, not work the model must complete before implementing.
23819
+ contractEnforcement: {
23820
+ type: "string",
23821
+ enum: ["off", "advisory", "strict"],
23822
+ description: "Board-level contract map enforcement. Default when first configured: advisory."
23823
+ },
23824
+ contractNodeId: { type: "string" },
23825
+ contractNodeKind: {
23826
+ type: "string",
23827
+ enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"],
23828
+ description: "objective = what this card is for; guardrail = what must keep working; risk = what could go wrong; component/artifact = what it touches; verification = what settles it."
23829
+ },
23830
+ contractNodeTitle: { type: "string" },
23831
+ contractNodeDescription: { type: "string" },
23832
+ contractNodeState: {
23833
+ type: "string",
23834
+ enum: ["unknown", "active", "satisfied", "violated", "waived", "resolved"]
23835
+ },
23836
+ contractNodeEnforcement: {
23837
+ type: "string",
23838
+ enum: ["blocking", "advisory", "informational"]
23839
+ },
23840
+ /** Bind a node to an acceptance criterion or goal metric already on the task. */
23841
+ contractCheckId: { type: "string" },
23842
+ contractMetricId: { type: "string" },
23843
+ contractWaiverReason: {
23844
+ type: "string",
23845
+ description: 'Required, with an actor, when contractNodeState is "waived".'
23846
+ },
23847
+ contractEdgeId: { type: "string" },
23848
+ contractEdgeFrom: {
23849
+ type: "string",
23850
+ description: 'A contract node id, or a task id (bare or "task:<id>") for the card endpoint.'
23851
+ },
23852
+ contractEdgeTo: { type: "string" },
23853
+ contractEdgeType: {
23854
+ type: "string",
23855
+ enum: [
23856
+ "targets",
23857
+ "affects",
23858
+ "must_preserve",
23859
+ "exposes",
23860
+ "verified_by",
23861
+ "conflicts_with",
23862
+ "derived_from",
23863
+ "relates_to"
23864
+ ]
23865
+ },
23866
+ contractEdgeRationale: { type: "string" },
23779
23867
  note: { type: "string" },
23780
23868
  author: { type: "string" },
23781
23869
  url: { type: "string" },
@@ -23830,12 +23918,17 @@ import {
23830
23918
  mutateTasks
23831
23919
  } from "@wrongstack/core/storage";
23832
23920
  import { deserializeTaskGraph } from "@wrongstack/core/tasking";
23833
- import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
23921
+ import { formatTodosForModel, resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
23834
23922
  import {
23835
23923
  bridgeKanbanSupervisor,
23924
+ compactSessionMirrorBoard,
23836
23925
  createBoard,
23926
+ DEFAULT_COLUMNS,
23837
23927
  getBoard as getBoard2,
23928
+ getDependencyReadinessIssues,
23929
+ getKanbanOrchestrationSnapshot,
23838
23930
  listBoards,
23931
+ pruneSessionBoards,
23839
23932
  removeBoard,
23840
23933
  syncBoardFromTaskGraph,
23841
23934
  touchKanbanPresence as touchKanbanPresence2,
@@ -23843,16 +23936,14 @@ import {
23843
23936
  } from "@wrongstack/kanban";
23844
23937
  var SESSION_BOARD_TAG = "session-work";
23845
23938
  var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
23846
- var SESSION_KANBAN_COLUMNS = [
23847
- { id: "todo", title: "Todo", order: 0, wipLimit: 0, color: "#2563eb" },
23848
- { id: "in-progress", title: "Running", order: 1, wipLimit: 1, color: "#d97706" },
23849
- { id: "review", title: "Preview", order: 2, wipLimit: 0, color: "#7c3aed" },
23850
- { id: "done", title: "Done", order: 3, wipLimit: 0, color: "#16a34a" }
23851
- ];
23939
+ var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
23940
+ ...column
23941
+ }));
23852
23942
  var boardQueue = /* @__PURE__ */ new Map();
23853
23943
  var boardEnsures = /* @__PURE__ */ new Map();
23854
23944
  var pendingMirrors = /* @__PURE__ */ new Map();
23855
23945
  var activeMirrors = /* @__PURE__ */ new Set();
23946
+ var mirrorFailures = /* @__PURE__ */ new Map();
23856
23947
  var bindings = /* @__PURE__ */ new WeakMap();
23857
23948
  var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
23858
23949
  var activeSessionBoards = /* @__PURE__ */ new Map();
@@ -23862,6 +23953,33 @@ function boardKey(projectRoot, sessionId) {
23862
23953
  function mirrorKey(projectRoot, sessionId, sourceSystem) {
23863
23954
  return `${boardKey(projectRoot, sessionId)}\0${sourceSystem}`;
23864
23955
  }
23956
+ function completedReconciliationGraph(latest, candidates) {
23957
+ const latestNodeIds = new Set(latest.nodes.map((node) => node.id));
23958
+ const carriedNodeIds = /* @__PURE__ */ new Set();
23959
+ const completedNodes = candidates.flatMap(
23960
+ (candidate) => candidate.nodes.filter((node) => {
23961
+ if (node.status !== "completed" || latestNodeIds.has(node.id) || carriedNodeIds.has(node.id)) {
23962
+ return false;
23963
+ }
23964
+ carriedNodeIds.add(node.id);
23965
+ return true;
23966
+ })
23967
+ );
23968
+ if (completedNodes.length === 0) return void 0;
23969
+ const carriedRequirements = completedNodes.flatMap(
23970
+ (node) => node.specRequirementId ? [node.specRequirementId] : []
23971
+ );
23972
+ return {
23973
+ ...latest,
23974
+ nodes: [...latest.nodes, ...completedNodes],
23975
+ rootNodes: [.../* @__PURE__ */ new Set([...latest.rootNodes, ...completedNodes.map((node) => node.id)])],
23976
+ ...latest.requiredRequirementIds ? {
23977
+ requiredRequirementIds: [
23978
+ .../* @__PURE__ */ new Set([...latest.requiredRequirementIds, ...carriedRequirements])
23979
+ ]
23980
+ } : {}
23981
+ };
23982
+ }
23865
23983
  function sessionTag(sessionId) {
23866
23984
  return `session:${sessionId}`;
23867
23985
  }
@@ -24002,16 +24120,44 @@ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
24002
24120
  sourceSystem,
24003
24121
  tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
24004
24122
  archiveMissingTasks: true,
24005
- includeCompletedTasks: true
24123
+ includeCompletedTasks: true,
24124
+ // The scope ledger stays declared and accurate, but it may not veto a
24125
+ // projection. A session mirror reflects a tactical list that shrinks by
24126
+ // design, and refusing the sync never protected the removed row — it
24127
+ // froze the entire board, permanently, because the stored scope then
24128
+ // outlived every later snapshot (`session-kanban.mirror-failed`).
24129
+ // Nothing is lost by shrinking here: `archiveMissingTasks` keeps the
24130
+ // removed card on the board as `archived`, the reconciliation pass
24131
+ // first walks vanished completed rows to Done, and the session journal
24132
+ // remains the durable record.
24133
+ allowRequirementScopeShrink: true
24006
24134
  }
24007
24135
  );
24008
- return result?.board ?? null;
24136
+ if (!result) return null;
24137
+ const compacted = await compactSessionMirrorBoard(projectRoot, board.id);
24138
+ if (compacted?.removedTaskIds.length) {
24139
+ return await getBoard2(projectRoot, board.id) ?? result.board;
24140
+ }
24141
+ return result.board;
24009
24142
  });
24010
24143
  }
24011
24144
  function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
24012
24145
  if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return;
24013
24146
  const key = mirrorKey(projectRoot, sessionId, sourceSystem);
24014
- pendingMirrors.set(key, { projectRoot, sessionId, graph, sourceSystem });
24147
+ const previous = pendingMirrors.get(key);
24148
+ const reconciliationGraph = previous ? completedReconciliationGraph(
24149
+ graph,
24150
+ [previous.reconciliationGraph, previous.graph].filter(
24151
+ (candidate) => candidate !== void 0
24152
+ )
24153
+ ) : void 0;
24154
+ pendingMirrors.set(key, {
24155
+ projectRoot,
24156
+ sessionId,
24157
+ graph,
24158
+ ...reconciliationGraph ? { reconciliationGraph } : {},
24159
+ sourceSystem
24160
+ });
24015
24161
  if (activeMirrors.has(key)) return;
24016
24162
  activeMirrors.add(key);
24017
24163
  void (async () => {
@@ -24021,20 +24167,34 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
24021
24167
  if (!pending2) break;
24022
24168
  pendingMirrors.delete(key);
24023
24169
  try {
24170
+ if (pending2.reconciliationGraph) {
24171
+ await projectGraph(
24172
+ pending2.projectRoot,
24173
+ pending2.sessionId,
24174
+ pending2.reconciliationGraph,
24175
+ pending2.sourceSystem
24176
+ );
24177
+ }
24024
24178
  await projectGraph(
24025
24179
  pending2.projectRoot,
24026
24180
  pending2.sessionId,
24027
24181
  pending2.graph,
24028
24182
  pending2.sourceSystem
24029
24183
  );
24184
+ mirrorFailures.delete(boardKey(pending2.projectRoot, pending2.sessionId));
24030
24185
  } catch (error) {
24186
+ const message = error instanceof Error ? error.message : String(error);
24187
+ mirrorFailures.set(boardKey(pending2.projectRoot, pending2.sessionId), {
24188
+ message,
24189
+ sourceSystem: pending2.sourceSystem
24190
+ });
24031
24191
  console.warn(
24032
24192
  JSON.stringify({
24033
24193
  level: "warn",
24034
24194
  event: "session-kanban.mirror-failed",
24035
24195
  sessionId: pending2.sessionId,
24036
24196
  sourceSystem: pending2.sourceSystem,
24037
- message: error instanceof Error ? error.message : String(error),
24197
+ message,
24038
24198
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
24039
24199
  })
24040
24200
  );
@@ -24055,6 +24215,19 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
24055
24215
  }
24056
24216
  })();
24057
24217
  }
24218
+ function takeSessionMirrorFailure(projectRoot, sessionId) {
24219
+ if (!projectRoot || !sessionId) return void 0;
24220
+ const key = boardKey(projectRoot, sessionId);
24221
+ const failure = mirrorFailures.get(key);
24222
+ if (!failure) return void 0;
24223
+ mirrorFailures.delete(key);
24224
+ return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
24225
+ }
24226
+ function hasInFlightTodoMirror(projectRoot, sessionId) {
24227
+ if (!projectRoot || !sessionId) return false;
24228
+ const key = mirrorKey(projectRoot, sessionId, "session-todo");
24229
+ return pendingMirrors.has(key) || activeMirrors.has(key);
24230
+ }
24058
24231
  function todoListToSerializedGraph(todos, sessionId) {
24059
24232
  const graphId = `todo:${sessionId}`;
24060
24233
  const nodes = todos.map((todo, index) => ({
@@ -24216,11 +24389,11 @@ function broadcastTodoUpdate(context, todos) {
24216
24389
  });
24217
24390
  }
24218
24391
  function notifyTodoUpdate(context, todos) {
24219
- const summary = todos.length ? todos.map((todo) => `- [${todo.status}] ${todo.content} (${todo.id})`).join("\n") : "- No active todos remain.";
24392
+ const summary = formatTodosForModel(todos);
24220
24393
  const text = `[KANBAN TODO UPDATE]
24221
24394
  Another Kanban agent reassessed the shared board. The canonical todo list is now:
24222
24395
  ${summary}
24223
- Reassess your current plan before continuing; do not rely on the initial todo snapshot.`;
24396
+ Reassess your current plan before continuing; do not rely on the initial todo snapshot. Preserve each row's <kanban board/task> binding verbatim on your next \`todo\` call \u2014 a row that loses it stops advancing its card.`;
24224
24397
  const state = context.state;
24225
24398
  if (typeof state.appendBlockToLastUserMessage === "function") {
24226
24399
  if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
@@ -24229,6 +24402,10 @@ Reassess your current plan before continuing; do not rely on the initial todo sn
24229
24402
  state.appendMessage({ role: "user", content: [{ type: "text", text }] });
24230
24403
  }
24231
24404
  }
24405
+ function todosNeedingSessionMirror(todos, activeBoardId2) {
24406
+ if (!activeBoardId2) return todos;
24407
+ return todos.filter((todo) => todo.kanbanBoardId !== activeBoardId2 || !todo.kanbanTaskId);
24408
+ }
24232
24409
  function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
24233
24410
  queueLatestMirror(
24234
24411
  projectRoot,
@@ -24375,16 +24552,9 @@ function attachSessionKanbanMirror(context) {
24375
24552
  const unsubscribe = context.state.onChange((change) => {
24376
24553
  if (change.kind === "todos_replaced" && !suppressedTodoMirrors.has(context)) {
24377
24554
  const snapshot = change.completedSnapshot ?? change.todos;
24378
- if (snapshot.length > 0 && snapshot.every(
24379
- (todo) => todo.kanbanBoardId === activeManagedBoardId() && Boolean(todo.kanbanTaskId)
24380
- )) {
24381
- return;
24382
- }
24383
- mirrorSessionTodosToKanban(
24384
- context.projectRoot,
24385
- change.completedSnapshot ?? change.todos,
24386
- sessionId()
24387
- );
24555
+ const unbound = todosNeedingSessionMirror(snapshot, activeManagedBoardId());
24556
+ if (snapshot.length > 0 && unbound.length === 0) return;
24557
+ mirrorSessionTodosToKanban(context.projectRoot, unbound, sessionId());
24388
24558
  return;
24389
24559
  }
24390
24560
  if (change.kind === "meta_set" && (change.key === "plan.path" || change.key === "task.path" || change.key === "kanban")) {
@@ -24420,10 +24590,42 @@ function attachSessionKanbanMirror(context) {
24420
24590
  bindings.set(context, detach);
24421
24591
  return detach;
24422
24592
  }
24593
+ async function rebindSessionKanbanTask(context) {
24594
+ const sessionId = context.session?.id;
24595
+ if (!sessionId || !context.projectRoot) return null;
24596
+ if (context.currentKanbanTaskId) return null;
24597
+ let best;
24598
+ try {
24599
+ const snapshot = await getKanbanOrchestrationSnapshot(context.projectRoot);
24600
+ const nowMs = Date.now();
24601
+ for (const result of snapshot.running) {
24602
+ const assignment = result.task.assignment;
24603
+ if (assignment?.status !== "running") continue;
24604
+ const expiresAt = assignment.leaseExpiresAt ? Date.parse(assignment.leaseExpiresAt) : Number.NaN;
24605
+ if (Number.isFinite(expiresAt) && expiresAt <= nowMs) continue;
24606
+ const entry = result.board.presence?.find(
24607
+ (candidate) => candidate.sessionId === sessionId && candidate.taskId === result.task.id
24608
+ );
24609
+ if (!entry) continue;
24610
+ if (!best || entry.lastSeenAt > best.lastSeenAt) {
24611
+ best = { boardId: result.board.id, taskId: result.task.id, lastSeenAt: entry.lastSeenAt };
24612
+ }
24613
+ }
24614
+ } catch {
24615
+ return null;
24616
+ }
24617
+ if (!best) return null;
24618
+ context.setCurrentKanbanTask(best.taskId, best.boardId);
24619
+ return { boardId: best.boardId, taskId: best.taskId };
24620
+ }
24423
24621
  async function hydrateSessionKanban(context) {
24424
24622
  const id = context.session?.id ?? "";
24425
24623
  if (!id) return null;
24624
+ await rebindSessionKanbanTask(context);
24426
24625
  await cleanupEmptySessionKanbanBoards(context.projectRoot, id);
24626
+ if (context.projectRoot) {
24627
+ fireAndForget("prune-session-boards", pruneSessionBoards(context.projectRoot));
24628
+ }
24427
24629
  let board = await ensureSessionKanbanBoard(context.projectRoot, id);
24428
24630
  if (context.todos.length) {
24429
24631
  board = await projectSessionTodosToKanban(context.projectRoot, context.todos, id);
@@ -24454,26 +24656,68 @@ function todoStatus(task) {
24454
24656
  if (status === "in_progress" || status === "review") return "in_progress";
24455
24657
  return "pending";
24456
24658
  }
24457
- function sessionTodoFromTask(task, boardId) {
24659
+ function sessionTodoFromTask(task, board) {
24660
+ const blockedBy = board ? blockingTitles(board, task) : [];
24458
24661
  return {
24459
24662
  id: task.origin?.taskId ?? task.id,
24460
24663
  content: task.title,
24461
24664
  status: todoStatus(task),
24462
- kanbanBoardId: boardId,
24463
- kanbanTaskId: task.id,
24464
- ...task.description ? { activeForm: task.description } : {}
24665
+ ...task.description ? { activeForm: task.description } : {},
24666
+ ...blockedBy.length ? { blockedBy } : {}
24465
24667
  };
24466
24668
  }
24467
- function managedTodoFromTask(task, boardId) {
24669
+ function managedTodoFromTask(task, board) {
24468
24670
  return {
24469
- ...sessionTodoFromTask(task, boardId),
24470
- status: task.status === "completed" ? "completed" : task.status === "in_progress" ? "in_progress" : "pending"
24671
+ ...sessionTodoFromTask(task, board),
24672
+ kanbanBoardId: board.id,
24673
+ kanbanTaskId: task.id
24471
24674
  };
24472
24675
  }
24676
+ function blockingTitles(board, task) {
24677
+ return getDependencyReadinessIssues(board, task).map((issue) => {
24678
+ const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
24679
+ if (!dependency) return `${issue.dependencyId} (missing)`;
24680
+ return dependency.title;
24681
+ });
24682
+ }
24683
+ var PRIORITY_ORDER = {
24684
+ critical: 0,
24685
+ high: 1,
24686
+ medium: 2,
24687
+ low: 3
24688
+ };
24689
+ function orderTasksForTodos(board, tasks) {
24690
+ const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
24691
+ const baseline = [...tasks].sort(
24692
+ (left, right) => (columnOrder.get(left.columnId) ?? 0) - (columnOrder.get(right.columnId) ?? 0) || (PRIORITY_ORDER[left.priority] ?? 2) - (PRIORITY_ORDER[right.priority] ?? 2) || left.order - right.order || left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)
24693
+ );
24694
+ const included = new Set(baseline.map((task) => task.id));
24695
+ const remaining = new Map(baseline.map((task) => [task.id, task]));
24696
+ const emitted = [];
24697
+ const done = /* @__PURE__ */ new Set();
24698
+ while (remaining.size > 0) {
24699
+ const ready = baseline.filter(
24700
+ (task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
24701
+ (dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
24702
+ )
24703
+ );
24704
+ if (ready.length === 0) break;
24705
+ for (const task of ready) {
24706
+ remaining.delete(task.id);
24707
+ done.add(task.id);
24708
+ emitted.push(task);
24709
+ }
24710
+ }
24711
+ for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
24712
+ return emitted;
24713
+ }
24473
24714
  function sameTodos(left, right) {
24474
24715
  return left.length === right.length && left.every((todo, index) => {
24475
24716
  const candidate = right[index];
24476
- return candidate?.id === todo.id && candidate.content === todo.content && candidate.status === todo.status && candidate.activeForm === todo.activeForm && candidate.promotedFromPlan === todo.promotedFromPlan && candidate.promotedFromTask === todo.promotedFromTask && candidate.kanbanBoardId === todo.kanbanBoardId && candidate.kanbanTaskId === todo.kanbanTaskId;
24717
+ return candidate?.id === todo.id && candidate.content === todo.content && candidate.status === todo.status && candidate.activeForm === todo.activeForm && candidate.promotedFromPlan === todo.promotedFromPlan && candidate.promotedFromTask === todo.promotedFromTask && candidate.kanbanBoardId === todo.kanbanBoardId && candidate.kanbanTaskId === todo.kanbanTaskId && // Readiness is part of the projection: when a dependency completes,
24718
+ // the rows are otherwise identical and the unblocking would never
24719
+ // reach the model.
24720
+ (candidate.blockedBy ?? []).join("\0") === (todo.blockedBy ?? []).join("\0");
24477
24721
  });
24478
24722
  }
24479
24723
  function applySessionKanbanBoardToTodos(context, board) {
@@ -24481,13 +24725,13 @@ function applySessionKanbanBoardToTodos(context, board) {
24481
24725
  if (!sessionId || sessionIdFromTags(board.tags) !== sessionId || !isOwnedSessionBoard(board.tags)) {
24482
24726
  return [...context.todos];
24483
24727
  }
24484
- const projectedTodos = board.tasks.filter(
24485
- (task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
24486
- ).sort((left, right) => {
24487
- const leftColumn = board.columns.find((column) => column.id === left.columnId)?.order ?? 0;
24488
- const rightColumn = board.columns.find((column) => column.id === right.columnId)?.order ?? 0;
24489
- return leftColumn - rightColumn || left.order - right.order || left.createdAt.localeCompare(right.createdAt);
24490
- }).map((task) => sessionTodoFromTask(task, board.id));
24728
+ if (hasInFlightTodoMirror(context.projectRoot, sessionId)) return [...context.todos];
24729
+ const projectedTodos = orderTasksForTodos(
24730
+ board,
24731
+ board.tasks.filter(
24732
+ (task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
24733
+ )
24734
+ ).map((task) => sessionTodoFromTask(task, board));
24491
24735
  const allCompleted = projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === "completed");
24492
24736
  const effectiveTodos = allCompleted ? [] : projectedTodos;
24493
24737
  if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];
@@ -24508,11 +24752,12 @@ function applyManagedKanbanBoardToTodos(context, board) {
24508
24752
  if (!activeBoardId2 || board.id !== activeBoardId2 || board.lifecycle?.mode !== "managed") {
24509
24753
  return [...context.todos];
24510
24754
  }
24511
- const projectedTodos = board.tasks.filter(
24512
- (task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
24513
- ).sort(
24514
- (left, right) => left.createdAt.localeCompare(right.createdAt) || left.order - right.order
24515
- ).map((task) => managedTodoFromTask(task, board.id));
24755
+ const projectedTodos = orderTasksForTodos(
24756
+ board,
24757
+ board.tasks.filter(
24758
+ (task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
24759
+ )
24760
+ ).map((task) => managedTodoFromTask(task, board));
24516
24761
  if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
24517
24762
  suppressedTodoMirrors.add(context);
24518
24763
  try {
@@ -24547,7 +24792,11 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
24547
24792
  const id = context.session?.id ?? "";
24548
24793
  if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
24549
24794
  const planPath = context.meta["plan.path"];
24550
- if (typeof planPath !== "string" || !planPath) return { source: "plan" };
24795
+ if (typeof planPath !== "string" || !planPath) {
24796
+ throw new Error(
24797
+ "Cannot reflect this Kanban edit back to its plan source: the session has no plan.path configured. The board mutation already succeeded; the plan file is now out of sync."
24798
+ );
24799
+ }
24551
24800
  const plan = await mutatePlan(planPath, id, (file) => ({
24552
24801
  ...file,
24553
24802
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -24565,7 +24814,11 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
24565
24814
  }
24566
24815
  if (task.origin?.system === "session-task" || task.origin?.system === "session" || graphId.startsWith("session:")) {
24567
24816
  const taskPath = context.meta["task.path"];
24568
- if (typeof taskPath !== "string" || !taskPath) return { source: "task" };
24817
+ if (typeof taskPath !== "string" || !taskPath) {
24818
+ throw new Error(
24819
+ "Cannot reflect this Kanban edit back to its task source: the session has no task.path configured. The board mutation already succeeded; the task file is now out of sync."
24820
+ );
24821
+ }
24569
24822
  const tasks = await mutateTasks(taskPath, id, (file) => ({
24570
24823
  ...file,
24571
24824
  tasks: options.remove ? file.tasks.filter((item) => item.id !== originId) : file.tasks.map(
@@ -24614,8 +24867,14 @@ var kanbanTool = {
24614
24867
  }
24615
24868
  case "create_board": {
24616
24869
  if (!input.title) return fail("create_board requires title.");
24870
+ const existing = (await listBoards2(projectRoot)).filter(
24871
+ (candidate) => (candidate.kind ?? "project") === "project"
24872
+ );
24617
24873
  const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
24618
- return { ok: true, message: `Board created: ${board.title}`, board };
24874
+ const note = existing.length ? ` ${existing.length} other project board(s) already exist: ${existing.slice(0, 3).map((candidate) => `"${candidate.title}" (${candidate.taskCount} task(s))`).join(
24875
+ ", "
24876
+ )}${existing.length > 3 ? ", \u2026" : ""}. If this work belongs to one of them, add_task there instead and delete this board.` : "";
24877
+ return { ok: true, message: `Board created: ${board.title}.${note}`, board };
24619
24878
  }
24620
24879
  case "update_board": {
24621
24880
  if (!input.boardId) return fail("update_board requires boardId.");
@@ -24644,6 +24903,20 @@ var kanbanTool = {
24644
24903
  });
24645
24904
  return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
24646
24905
  }
24906
+ // Adoption used to be a one-way door: the strict lifecycle carries
24907
+ // acceptance-criteria, verification-report, review-evidence and
24908
+ // one-stage-at-a-time gates, and nothing on the tool surface could
24909
+ // undo it, so a board adopted once kept its ceremony forever. The
24910
+ // gates are worth having where a fleet is supervised; they are not
24911
+ // worth being unable to leave. Cards and columns are untouched.
24912
+ case "release_managed_lifecycle": {
24913
+ if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
24914
+ const board = await updateBoard2(projectRoot, input.boardId, { lifecycle: null });
24915
+ return board ? okBoard(
24916
+ board,
24917
+ "Managed lifecycle released; the board now tracks work without strict gates."
24918
+ ) : fail("Board not found.");
24919
+ }
24647
24920
  case "duplicate_board": {
24648
24921
  if (!input.boardId) return fail("duplicate_board requires boardId.");
24649
24922
  const board = await duplicateBoard(
@@ -24663,8 +24936,7 @@ var kanbanTool = {
24663
24936
  const boardInput = createBoardFromText({
24664
24937
  description: input.description,
24665
24938
  ...input.title !== void 0 ? { title: input.title } : {},
24666
- ...input.context !== void 0 ? { context: input.context } : {},
24667
- ...input.columns !== void 0 ? { columns: input.columns } : {}
24939
+ ...input.context !== void 0 ? { context: input.context } : {}
24668
24940
  });
24669
24941
  const board = await createBoard2(projectRoot, boardInput);
24670
24942
  for (const taskInput2 of parseLinesIntoTasks(
@@ -24802,7 +25074,7 @@ var kanbanTool = {
24802
25074
  return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
24803
25075
  }
24804
25076
  case "snapshot": {
24805
- const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
25077
+ const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
24806
25078
  query: input.query,
24807
25079
  boardId: input.boardId,
24808
25080
  assignedAgent: input.agentId,
@@ -24817,33 +25089,6 @@ var kanbanTool = {
24817
25089
  snapshot
24818
25090
  };
24819
25091
  }
24820
- case "add_column": {
24821
- if (!input.boardId || !input.title)
24822
- return fail("add_column requires boardId and title.");
24823
- const result2 = await addColumn(projectRoot, input.boardId, {
24824
- title: input.title,
24825
- ...input.description !== void 0 ? { description: input.description } : {}
24826
- });
24827
- return result2 ? okBoard(result2.board, "Column added.") : fail("Board not found.");
24828
- }
24829
- case "update_column": {
24830
- if (!input.boardId || !input.columnId)
24831
- return fail("update_column requires boardId and columnId.");
24832
- const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
24833
- ...input.title !== void 0 ? { title: input.title } : {},
24834
- ...input.description !== void 0 ? { description: input.description } : {},
24835
- ...input.order !== void 0 ? { order: input.order } : {}
24836
- });
24837
- return board ? okBoard(board, "Column updated.") : fail("Column not found.");
24838
- }
24839
- case "delete_column": {
24840
- if (!input.boardId || !input.columnId)
24841
- return fail("delete_column requires boardId and columnId.");
24842
- const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
24843
- moveTasksToColumnId: input.moveTasksToColumnId
24844
- });
24845
- return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
24846
- }
24847
25092
  case "add_task": {
24848
25093
  if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
24849
25094
  const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
@@ -24925,6 +25170,32 @@ var kanbanTool = {
24925
25170
  `Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
24926
25171
  );
24927
25172
  }
25173
+ if (board.lifecycle?.mode !== "managed") {
25174
+ const now2 = /* @__PURE__ */ new Date();
25175
+ const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
25176
+ status: "running",
25177
+ agentId: input.agentId ?? input.author,
25178
+ leaseId: input.leaseId ?? randomUUID2(),
25179
+ claimedAt: input.claimedAt ?? now2.toISOString(),
25180
+ heartbeatAt: input.heartbeatAt ?? now2.toISOString(),
25181
+ leaseExpiresAt: input.leaseExpiresAt ?? new Date(now2.getTime() + 15 * 6e4).toISOString(),
25182
+ attempt: input.attempt ?? 1,
25183
+ maxAttempts: input.maxAttempts ?? 3
25184
+ });
25185
+ if (!assigned) return fail("Task assignment could not be started.");
25186
+ const started = await updateTask2(projectRoot, board.id, task.id, {
25187
+ status: "in_progress"
25188
+ });
25189
+ const current = started ?? assigned;
25190
+ const claimed = task;
25191
+ const currentTask = current.tasks.find((candidate) => candidate.id === claimed.id) ?? claimed;
25192
+ ctx.setCurrentKanbanTask?.(currentTask.id, current.id);
25193
+ return okTask(
25194
+ current,
25195
+ currentTask,
25196
+ "Task is active and bound to this run for attribution. This board is not in managed lifecycle mode, so runtime Kanban governance was not bound to it."
25197
+ );
25198
+ }
24928
25199
  let stage = task.lifecycle?.currentStage;
24929
25200
  if (stage === "backlog") {
24930
25201
  const moved = await transitionTask(projectRoot, board.id, task.id, {
@@ -25065,6 +25336,9 @@ var kanbanTool = {
25065
25336
  if (!input.boardId || !input.taskId)
25066
25337
  return fail("delete_task requires boardId and taskId.");
25067
25338
  const board = await removeTask(projectRoot, input.boardId, input.taskId);
25339
+ if (board && ctx.currentKanbanTaskId === input.taskId) {
25340
+ ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
25341
+ }
25068
25342
  return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
25069
25343
  }
25070
25344
  case "set_chain": {
@@ -25197,7 +25471,7 @@ var kanbanTool = {
25197
25471
  });
25198
25472
  } catch (err) {
25199
25473
  lifecycleWarnings.push(
25200
- `Lifecycle transition to Running deferred: ${err instanceof Error ? err.message : String(err)}`
25474
+ `Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
25201
25475
  );
25202
25476
  }
25203
25477
  }
@@ -25221,7 +25495,7 @@ var kanbanTool = {
25221
25495
  });
25222
25496
  } catch (err) {
25223
25497
  lifecycleWarnings.push(
25224
- `Lifecycle transition to Review failed: ${err instanceof Error ? err.message : String(err)}`
25498
+ `Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
25225
25499
  );
25226
25500
  }
25227
25501
  if (transitionResult) {
@@ -25241,7 +25515,11 @@ var kanbanTool = {
25241
25515
  successCriteria: verResult.task.successCriteria
25242
25516
  });
25243
25517
  const verdict = verResult.report.verdict;
25244
- if (verdict === "passed") {
25518
+ if (verdict === "passed" && !resolveAutoAccept(board)) {
25519
+ lifecycleWarnings.push(
25520
+ "Verification passed, but this board does not auto-accept. The card is in Review awaiting an explicit transition_task to done."
25521
+ );
25522
+ } else if (verdict === "passed") {
25245
25523
  try {
25246
25524
  const doneResult = await transitionTask(
25247
25525
  projectRoot,
@@ -25359,11 +25637,34 @@ var kanbanTool = {
25359
25637
  });
25360
25638
  return {
25361
25639
  ok: true,
25362
- message: `Counts: ready=${health.counts.ready}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
25640
+ message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
25363
25641
  queueHealth: health
25364
25642
  };
25365
25643
  }
25644
+ // Not every action is handled above. These are dispatched from here,
25645
+ // and the split has already cost real time: an agent that read this
25646
+ // file concluded `add_check` / `update_check` did not exist, wrote
25647
+ // that on a card, and spent a session trying to satisfy a gate it
25648
+ // already had the tool to clear. Keep this index in step with the
25649
+ // handlers.
25650
+ //
25651
+ // kanban-detail-actions.ts workbench · add_dependency ·
25652
+ // add_goal_metric · update_goal_metric · add_check ·
25653
+ // update_check · add_note · add_link · split_atomic
25654
+ // kanban-decomposition-actions.ts verify_completion ·
25655
+ // assess_atomicity · propose_decomposition
25656
+ // kanban-contract-actions.ts get_contract_graph ·
25657
+ // configure_contract_graph · upsert_contract_node ·
25658
+ // remove_contract_node · add_contract_edge · remove_contract_edge
25366
25659
  default:
25660
+ {
25661
+ const contractResult = await handleKanbanContractAction(
25662
+ projectRoot,
25663
+ input,
25664
+ input.author ?? input.agentId
25665
+ );
25666
+ if (contractResult !== void 0) return contractResult;
25667
+ }
25367
25668
  {
25368
25669
  const detailResult = await handleKanbanDetailAction(projectRoot, input);
25369
25670
  if (detailResult !== void 0) return detailResult;
@@ -25373,7 +25674,7 @@ var kanbanTool = {
25373
25674
  })();
25374
25675
  return withPresence(result);
25375
25676
  } catch (err) {
25376
- return fail(err instanceof Error ? err.message : String(err));
25677
+ return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
25377
25678
  }
25378
25679
  }
25379
25680
  };
@@ -26226,7 +26527,7 @@ import {
26226
26527
  saveTasks,
26227
26528
  setPlanItemStatus
26228
26529
  } from "@wrongstack/core/storage";
26229
- import { getBoard as getBoard4 } from "@wrongstack/kanban";
26530
+ import { addTask as addTask2, getBoard as getBoard4 } from "@wrongstack/kanban";
26230
26531
  function normalizedTitle(value) {
26231
26532
  return value.trim().toLocaleLowerCase().replace(/\s+/g, " ");
26232
26533
  }
@@ -26254,11 +26555,51 @@ function bindTodosToBoard(items, previous, board) {
26254
26555
  available.find((task2) => !used.has(task2.id) && normalizedTitle(task2.title) === title)
26255
26556
  ];
26256
26557
  const task = candidates.find((candidate) => candidate && !used.has(candidate.id));
26257
- if (!task) return { ...item };
26558
+ if (!task) {
26559
+ const { blockedBy: _discarded, ...rest } = item;
26560
+ return { ...rest };
26561
+ }
26258
26562
  used.add(task.id);
26259
- return { ...item, kanbanBoardId: board.id, kanbanTaskId: task.id };
26563
+ const blockedBy = blockingTitles(board, task);
26564
+ return {
26565
+ ...item,
26566
+ kanbanBoardId: board.id,
26567
+ kanbanTaskId: task.id,
26568
+ ...blockedBy.length ? { blockedBy } : { blockedBy: void 0 }
26569
+ };
26570
+ });
26571
+ }
26572
+ function demoteBlockedInProgress(items, warnings) {
26573
+ return items.map((item) => {
26574
+ if (item.status !== "in_progress" || !item.blockedBy?.length) return item;
26575
+ warnings.push(
26576
+ `"${item.content}" cannot start yet \u2014 it waits on: ${item.blockedBy.join("; ")}. Kept as pending; complete the blocking work first.`
26577
+ );
26578
+ return { ...item, status: "pending" };
26260
26579
  });
26261
26580
  }
26581
+ async function createMissingManagedCards(items, board, ctx, warnings) {
26582
+ const created = /* @__PURE__ */ new Map();
26583
+ for (const item of items) {
26584
+ if (item.kanbanBoardId === board.id && item.kanbanTaskId) continue;
26585
+ try {
26586
+ const result = await addTask2(ctx.projectRoot, board.id, {
26587
+ title: item.content,
26588
+ description: item.activeForm?.trim() || `Added from the session todo list: ${item.content}`
26589
+ });
26590
+ if (!result) {
26591
+ warnings.push(`Could not open a Kanban card for "${item.content}": board not found.`);
26592
+ continue;
26593
+ }
26594
+ created.set(item.id, result.task.id);
26595
+ } catch (error) {
26596
+ warnings.push(
26597
+ `Could not open a Kanban card for "${item.content}": ${error instanceof Error ? error.message : String(error)}`
26598
+ );
26599
+ }
26600
+ }
26601
+ return created;
26602
+ }
26262
26603
  async function synchronizeManagedKanban(items, board, ctx, signal) {
26263
26604
  let synced = 0;
26264
26605
  const warnings = [];
@@ -26296,6 +26637,16 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
26296
26637
  transitionComment: `Todo returned to queue: ${item.content}`
26297
26638
  });
26298
26639
  }
26640
+ for (const item of items) {
26641
+ if (item.status === "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
26642
+ continue;
26643
+ }
26644
+ const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
26645
+ if (task?.status !== "completed") continue;
26646
+ warnings.push(
26647
+ `"${item.content}" is already Done on the Kanban board and a completed card cannot be reopened; the row stays completed. Create a follow-up card for any remaining work.`
26648
+ );
26649
+ }
26299
26650
  for (const item of items) {
26300
26651
  if (item.status !== "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
26301
26652
  continue;
@@ -26347,17 +26698,25 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
26347
26698
  const active = items.find(
26348
26699
  (item) => item.status === "in_progress" && item.kanbanBoardId === board.id && Boolean(item.kanbanTaskId)
26349
26700
  );
26701
+ const activeStage = active?.kanbanTaskId ? afterCompletions?.tasks.find((task) => task.id === active.kanbanTaskId)?.lifecycle?.currentStage : void 0;
26350
26702
  if (active?.kanbanTaskId) {
26351
- await execute({
26352
- action: "start_task",
26353
- boardId: board.id,
26354
- taskId: active.kanbanTaskId,
26355
- author: actor,
26356
- agentId: actor,
26357
- transitionComment: `Todo activated: ${active.content}`
26358
- });
26703
+ if (activeStage === "review" || activeStage === "done") {
26704
+ warnings.push(
26705
+ `"${active.content}" is in ${activeStage === "review" ? "Review" : "Done"} awaiting acceptance; not re-activating it from the todo list. ` + (activeStage === "review" ? "Call kanban start_task explicitly to reopen it as a repair." : "Done is terminal; reopen only by creating a follow-up card.")
26706
+ );
26707
+ } else {
26708
+ await execute({
26709
+ action: "start_task",
26710
+ boardId: board.id,
26711
+ taskId: active.kanbanTaskId,
26712
+ author: actor,
26713
+ agentId: actor,
26714
+ transitionComment: `Todo activated: ${active.content}`
26715
+ });
26716
+ }
26359
26717
  }
26360
- if (active?.kanbanTaskId && completionPending) {
26718
+ if (active?.kanbanTaskId && activeStage === "review") {
26719
+ } else if (active?.kanbanTaskId && completionPending) {
26361
26720
  warnings.push(
26362
26721
  "A completed todo is still awaiting acceptance; the next independent Kanban task was started."
26363
26722
  );
@@ -26444,29 +26803,47 @@ var todoTool = {
26444
26803
  }
26445
26804
  }
26446
26805
  const boardId = activeBoardId(items, ctx);
26447
- const board = boardId ? await getBoard4(ctx.projectRoot, boardId) : null;
26448
- const boundItems = board?.lifecycle?.mode === "managed" ? bindTodosToBoard(items, ctx.todos ?? [], board) : items;
26806
+ let board = boardId ? await getBoard4(ctx.projectRoot, boardId) : null;
26807
+ const managed = board?.lifecycle?.mode === "managed";
26808
+ let boundItems = managed && board ? bindTodosToBoard(items, ctx.todos ?? [], board) : items;
26809
+ const creationWarnings = [];
26810
+ if (managed && board) {
26811
+ const managedBoardId = board.id;
26812
+ const created = await createMissingManagedCards(boundItems, board, ctx, creationWarnings);
26813
+ if (created.size > 0) {
26814
+ boundItems = boundItems.map((item) => {
26815
+ const taskId = created.get(item.id);
26816
+ return taskId ? { ...item, kanbanBoardId: managedBoardId, kanbanTaskId: taskId } : item;
26817
+ });
26818
+ board = await getBoard4(ctx.projectRoot, managedBoardId) ?? board;
26819
+ boundItems = bindTodosToBoard(boundItems, ctx.todos ?? [], board);
26820
+ }
26821
+ boundItems = demoteBlockedInProgress(boundItems, creationWarnings);
26822
+ }
26449
26823
  ctx.state.replaceTodos(boundItems);
26450
- const kanbanSync = board?.lifecycle?.mode === "managed" ? await synchronizeManagedKanban(boundItems, board, ctx, call.signal) : { synced: 0, warnings: [] };
26451
- if (board?.lifecycle?.mode === "managed") {
26824
+ const kanbanSync = managed && board ? await synchronizeManagedKanban(boundItems, board, ctx, call.signal) : { synced: 0, warnings: [] };
26825
+ kanbanSync.warnings.unshift(...creationWarnings);
26826
+ if (managed && board) {
26452
26827
  const unresolved = boundItems.filter(
26453
26828
  (item) => item.kanbanBoardId !== board.id || !item.kanbanTaskId
26454
26829
  );
26455
26830
  if (unresolved.length > 0) {
26456
26831
  kanbanSync.warnings.push(
26457
- `${unresolved.length} Todo row(s) did not match a real Kanban task and were not applied. Preserve kanbanBoardId/kanbanTaskId when updating the projection.`
26832
+ `${unresolved.length} Todo row(s) could not be bound to a Kanban task and were not applied. Preserve kanbanBoardId/kanbanTaskId when updating the projection.`
26458
26833
  );
26459
26834
  }
26460
26835
  }
26836
+ const mirrorFailure = takeSessionMirrorFailure(ctx.projectRoot, ctx.session?.id ?? "");
26837
+ if (mirrorFailure) kanbanSync.warnings.push(mirrorFailure);
26461
26838
  let projectedBoard = board;
26462
- if (board?.lifecycle?.mode === "managed") {
26839
+ if (managed && board) {
26463
26840
  const refreshed = await getBoard4(ctx.projectRoot, board.id);
26464
26841
  if (refreshed) {
26465
26842
  projectedBoard = refreshed;
26466
26843
  applyManagedKanbanBoardToTodos(ctx, refreshed);
26467
26844
  }
26468
26845
  }
26469
- if (board?.lifecycle?.mode !== "managed") {
26846
+ if (!managed) {
26470
26847
  mirrorSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? "session");
26471
26848
  }
26472
26849
  const completedPlanIds = /* @__PURE__ */ new Set();
@@ -29502,6 +29879,7 @@ var OPTIONAL_TOOLS = [
29502
29879
  toolHelpTool,
29503
29880
  setWorkingDirTool
29504
29881
  ];
29882
+ var OFF_ONLY_TOOLS = [...browserTools, e2ePlanTool];
29505
29883
  var TIER1_TOOLS = [
29506
29884
  readTool,
29507
29885
  writeTool,
@@ -30983,6 +31361,7 @@ export {
30983
31361
  IndexCircuitBreaker,
30984
31362
  IndexTimeoutError,
30985
31363
  LanguageProfileRegistry,
31364
+ OFF_ONLY_TOOLS,
30986
31365
  OPTIONAL_TOOLS,
30987
31366
  PRIMARY_LANGUAGE_PROFILES,
30988
31367
  SESSION_KANBAN_COLUMNS,
@@ -31102,6 +31481,7 @@ export {
31102
31481
  projectSessionTasksToKanban,
31103
31482
  projectSessionTodosToKanban,
31104
31483
  readTool,
31484
+ rebindSessionKanbanTask,
31105
31485
  recordKanbanVerificationEvidence,
31106
31486
  redactBrowserText,
31107
31487
  registerBuiltinToolTier,