@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/index.js CHANGED
@@ -11170,7 +11170,6 @@ import * as fs12 from "node:fs";
11170
11170
  import * as os5 from "node:os";
11171
11171
  import * as path17 from "node:path";
11172
11172
  import { fileURLToPath } from "node:url";
11173
- import { assertUnixSocketPathWithinLimit } from "@wrongstack/core/utils";
11174
11173
 
11175
11174
  // src/codebase-index/writer.ts
11176
11175
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
@@ -12991,7 +12990,7 @@ var IndexStore = class _IndexStore {
12991
12990
  const ftsSchema = this.stmt(
12992
12991
  "SELECT sql FROM sqlite_master WHERE type='table' AND name='symbols_fts'"
12993
12992
  ).get();
12994
- if (ftsSchema?.sql && ftsSchema.sql.includes("unicode61")) {
12993
+ if (ftsSchema?.sql?.includes("unicode61")) {
12995
12994
  this.db.exec("DROP TABLE IF EXISTS symbols_fts");
12996
12995
  }
12997
12996
  this.db.exec(SYMBOLS_FTS_SQL);
@@ -13484,9 +13483,13 @@ var IndexStore = class _IndexStore {
13484
13483
  sim: cosineSimilarity(queryVec, decodeVector(r.vector))
13485
13484
  })).sort((a, b) => b.sim - a.sim);
13486
13485
  const bm25Rank = /* @__PURE__ */ new Map();
13487
- bm25Rows.forEach((r, i) => bm25Rank.set(r.id, i));
13486
+ bm25Rows.forEach((r, i) => {
13487
+ bm25Rank.set(r.id, i);
13488
+ });
13488
13489
  const vecRank = /* @__PURE__ */ new Map();
13489
- vecScores.forEach((r, i) => vecRank.set(r.id, i));
13490
+ vecScores.forEach((r, i) => {
13491
+ vecRank.set(r.id, i);
13492
+ });
13490
13493
  const fused = reciprocalRankFusion(bm25Rank, vecRank, 60);
13491
13494
  const fusedScore = new Map(fused);
13492
13495
  const sorted = [...bm25Rows].sort(
@@ -14889,6 +14892,86 @@ import {
14889
14892
  isFrugalPerf
14890
14893
  } from "@wrongstack/core/utils";
14891
14894
 
14895
+ // src/codebase-index/content-hash.ts
14896
+ var PRIME64_1 = 0x9e3779b185ebca87n;
14897
+ var PRIME64_2 = 0xc2b2ae3d27d4eb4fn;
14898
+ var PRIME64_3 = 0x165667b19e3779f9n;
14899
+ var PRIME64_4 = 0x85ebca77c2b2ae63n;
14900
+ var PRIME64_5 = 0x27d4eb2f165667c5n;
14901
+ var MASK64 = 0xffffffffffffffffn;
14902
+ function mul64(a, b) {
14903
+ return (a & MASK64) * (b & MASK64) & MASK64;
14904
+ }
14905
+ function rotl64(x, n) {
14906
+ const v = x & MASK64;
14907
+ return (v << BigInt(n) | v >> BigInt(64 - n)) & MASK64;
14908
+ }
14909
+ function readU64LE(buf, off) {
14910
+ let v = 0n;
14911
+ for (let i = 7; i >= 0; i--) {
14912
+ v = v << 8n | BigInt(buf[off + i] ?? 0);
14913
+ }
14914
+ return v & MASK64;
14915
+ }
14916
+ function readU32LE(buf, off) {
14917
+ return (BigInt(buf[off] ?? 0) | BigInt(buf[off + 1] ?? 0) << 8n | BigInt(buf[off + 2] ?? 0) << 16n | BigInt(buf[off + 3] ?? 0) << 24n) & MASK64;
14918
+ }
14919
+ function xxh64Round(acc, lane) {
14920
+ return mul64(rotl64(acc + mul64(lane, PRIME64_2) & MASK64, 31), PRIME64_1);
14921
+ }
14922
+ function xxh64MergeRound(acc, val) {
14923
+ return mul64(acc ^ xxh64Round(0n, val), PRIME64_1) + PRIME64_4 & MASK64;
14924
+ }
14925
+ function xxhash64Hex(buf, explicitLen) {
14926
+ const length = explicitLen ?? buf.length;
14927
+ let h;
14928
+ let off = 0;
14929
+ if (length >= 32) {
14930
+ let v1 = PRIME64_1 + PRIME64_2 & MASK64;
14931
+ let v2 = PRIME64_2;
14932
+ let v3 = 0n;
14933
+ let v4 = 0n - PRIME64_1 & MASK64;
14934
+ const end32 = length - 32;
14935
+ while (off <= end32) {
14936
+ v1 = xxh64Round(v1, readU64LE(buf, off));
14937
+ v2 = xxh64Round(v2, readU64LE(buf, off + 8));
14938
+ v3 = xxh64Round(v3, readU64LE(buf, off + 16));
14939
+ v4 = xxh64Round(v4, readU64LE(buf, off + 24));
14940
+ off += 32;
14941
+ }
14942
+ h = rotl64(v1, 1) + rotl64(v2, 7) + rotl64(v3, 12) + rotl64(v4, 18) & MASK64;
14943
+ h = xxh64MergeRound(h, v1);
14944
+ h = xxh64MergeRound(h, v2);
14945
+ h = xxh64MergeRound(h, v3);
14946
+ h = xxh64MergeRound(h, v4);
14947
+ } else {
14948
+ h = PRIME64_5;
14949
+ }
14950
+ h = h + BigInt(length) & MASK64;
14951
+ while (off + 8 <= length) {
14952
+ const k1 = xxh64Round(0n, readU64LE(buf, off));
14953
+ h = mul64(rotl64(h ^ k1, 27), PRIME64_1) + PRIME64_4 & MASK64;
14954
+ off += 8;
14955
+ }
14956
+ if (off + 4 <= length) {
14957
+ h = mul64(rotl64(h ^ mul64(readU32LE(buf, off), PRIME64_1), 23), PRIME64_2) + PRIME64_3 & MASK64;
14958
+ off += 4;
14959
+ }
14960
+ while (off < length) {
14961
+ h = mul64(rotl64(h ^ mul64(BigInt(buf[off] ?? 0), PRIME64_5), 11), PRIME64_1) & MASK64;
14962
+ off += 1;
14963
+ }
14964
+ h = (h ^ h >> 33n) & MASK64;
14965
+ h = mul64(h, PRIME64_2);
14966
+ h = (h ^ h >> 29n) & MASK64;
14967
+ h = mul64(h, PRIME64_3);
14968
+ h = (h ^ h >> 32n) & MASK64;
14969
+ return h.toString(16).padStart(16, "0");
14970
+ }
14971
+ function xxhash64String(content) {
14972
+ return xxhash64Hex(new TextEncoder().encode(content));
14973
+ }
14974
+
14892
14975
  // src/codebase-index/gitignore.ts
14893
14976
  import * as fs14 from "node:fs/promises";
14894
14977
  import * as path18 from "node:path";
@@ -15616,91 +15699,14 @@ function getParserPool() {
15616
15699
  return _pool;
15617
15700
  }
15618
15701
 
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
15702
  // src/codebase-index/indexer.ts
15700
15703
  var YIELD_EVERY_N = 50;
15701
15704
  function resolveParallelBatch() {
15702
15705
  return indexParallelBatchSize(availableParallelism());
15703
15706
  }
15707
+ function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
15708
+ return !isFrugalPerf() && candidateFileCount >= WORKER_POOL_THRESHOLD && parseBatchCount > 1;
15709
+ }
15704
15710
  function yieldEventLoop() {
15705
15711
  return new Promise((resolve17) => setImmediate(resolve17));
15706
15712
  }
@@ -15877,11 +15883,7 @@ async function resolveProjectRelations(store, projectRoot, opts) {
15877
15883
  const structure = await detectModuleRoots(projectRoot, indexedFiles);
15878
15884
  if (opts.signal?.aborted) return;
15879
15885
  store.setFilePackages(assignPackageLabels(structure, indexedFiles));
15880
- const resolver = new ModuleResolver(
15881
- structure,
15882
- indexedFiles,
15883
- store.getNamespaceDeclarations()
15884
- );
15886
+ const resolver = new ModuleResolver(structure, indexedFiles, store.getNamespaceDeclarations());
15885
15887
  const pending2 = store.getUnresolvedImports(opts.onlyFiles);
15886
15888
  const resolutions = [];
15887
15889
  for (const entry of pending2) {
@@ -15906,6 +15908,10 @@ async function runIndexerWithStore(store, opts) {
15906
15908
  const errors = [];
15907
15909
  const langStats = {};
15908
15910
  let filesIndexed = 0;
15911
+ let filesParsed = 0;
15912
+ let filesSkipped = 0;
15913
+ let filesEmpty = 0;
15914
+ let filesFailed = 0;
15909
15915
  let symbolsIndexed = 0;
15910
15916
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
15911
15917
  let files;
@@ -15947,12 +15953,14 @@ async function runIndexerWithStore(store, opts) {
15947
15953
  langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
15948
15954
  symbolsIndexed += meta.symbolCount;
15949
15955
  filesIndexed++;
15956
+ filesSkipped++;
15950
15957
  filesPreSkipped++;
15951
15958
  return false;
15952
15959
  });
15953
15960
  if (filesPreSkipped > 0) opts.onProgress?.(filesPreSkipped, totalFilesForProgress);
15954
15961
  }
15955
15962
  const parallelBatch = resolveParallelBatch();
15963
+ const parserPoolCandidateCount = files.length;
15956
15964
  let filesSinceLastYield = 0;
15957
15965
  for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
15958
15966
  const batchEnd = Math.min(batchStart + parallelBatch, files.length);
@@ -16045,7 +16053,7 @@ async function runIndexerWithStore(store, opts) {
16045
16053
  });
16046
16054
  }
16047
16055
  if (toParse.length > 0) {
16048
- let pool = toParse.length >= WORKER_POOL_THRESHOLD ? getParserPool() : null;
16056
+ let pool = shouldUseParserWorkerPool(parserPoolCandidateCount, toParse.length) ? getParserPool() : null;
16049
16057
  if (pool) {
16050
16058
  try {
16051
16059
  await pool.ensureReady();
@@ -16095,12 +16103,14 @@ async function runIndexerWithStore(store, opts) {
16095
16103
  const err = settled.reason;
16096
16104
  if (err instanceof Error && isAbortError(err)) throw err;
16097
16105
  errors.push(`batch error: ${file}: ${err instanceof Error ? err.message : String(err)}`);
16106
+ filesFailed++;
16098
16107
  continue;
16099
16108
  }
16100
16109
  const result = settled.value;
16101
16110
  if (result.error) {
16102
16111
  if (result.missing) store.deleteFile(file);
16103
16112
  errors.push(`${file}: ${result.error}`);
16113
+ filesFailed++;
16104
16114
  continue;
16105
16115
  }
16106
16116
  const { stat: stat19, lang, parsed } = result;
@@ -16108,6 +16118,7 @@ async function runIndexerWithStore(store, opts) {
16108
16118
  langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
16109
16119
  symbolsIndexed += result.skippedMeta.symbolCount;
16110
16120
  filesIndexed++;
16121
+ filesSkipped++;
16111
16122
  const stored = existingMeta.get(file);
16112
16123
  if (stored && stored.mtimeMs !== result.skippedMeta.mtimeMs) {
16113
16124
  store.upsertFile({
@@ -16132,6 +16143,7 @@ async function runIndexerWithStore(store, opts) {
16132
16143
  contentHash: result.contentHash ?? ""
16133
16144
  });
16134
16145
  filesIndexed++;
16146
+ filesEmpty++;
16135
16147
  }
16136
16148
  continue;
16137
16149
  }
@@ -16145,6 +16157,7 @@ async function runIndexerWithStore(store, opts) {
16145
16157
  contentHash: result.contentHash ?? ""
16146
16158
  });
16147
16159
  filesIndexed++;
16160
+ filesEmpty++;
16148
16161
  continue;
16149
16162
  }
16150
16163
  batchEntries.push({
@@ -16166,6 +16179,7 @@ async function runIndexerWithStore(store, opts) {
16166
16179
  symbolsIndexed += count;
16167
16180
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
16168
16181
  filesIndexed++;
16182
+ filesParsed++;
16169
16183
  }
16170
16184
  } catch (err) {
16171
16185
  const message = err instanceof Error ? err.message : String(err);
@@ -16178,6 +16192,7 @@ async function runIndexerWithStore(store, opts) {
16178
16192
  symbolsIndexed += symbolsWithIds.length;
16179
16193
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
16180
16194
  filesIndexed++;
16195
+ filesParsed++;
16181
16196
  if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
16182
16197
  const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
16183
16198
  if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
@@ -16195,6 +16210,7 @@ async function runIndexerWithStore(store, opts) {
16195
16210
  contentHash: entry.contentHash
16196
16211
  });
16197
16212
  } catch (innerErr) {
16213
+ filesFailed++;
16198
16214
  errors.push(
16199
16215
  `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
16200
16216
  );
@@ -16227,6 +16243,12 @@ async function runIndexerWithStore(store, opts) {
16227
16243
  const durationMs = Date.now() - startMs;
16228
16244
  return {
16229
16245
  filesIndexed,
16246
+ fileOutcomes: {
16247
+ parsed: filesParsed,
16248
+ skipped: filesSkipped,
16249
+ empty: filesEmpty,
16250
+ failed: filesFailed
16251
+ },
16230
16252
  symbolsIndexed,
16231
16253
  langStats,
16232
16254
  durationMs,
@@ -21106,7 +21128,7 @@ async function detectFixer(cwd) {
21106
21128
  init_util();
21107
21129
  import { spawn as spawn9 } from "node:child_process";
21108
21130
  import { statSync as statSync4 } from "node:fs";
21109
- import { dirname as dirname14, resolve as resolve13, sep as sep6 } from "node:path";
21131
+ import { dirname as dirname13, resolve as resolve13, sep as sep6 } from "node:path";
21110
21132
  import { assessCommitSafety } from "@wrongstack/core/coordination";
21111
21133
  import { buildChildEnv as buildChildEnv4 } from "@wrongstack/core/utils";
21112
21134
  var TIMEOUT_MS2 = 3e4;
@@ -21285,7 +21307,7 @@ function findGitDir2(cwd, projectRoot) {
21285
21307
  } catch {
21286
21308
  }
21287
21309
  if (dir === root) break;
21288
- const parent = dirname14(dir);
21310
+ const parent = dirname13(dir);
21289
21311
  if (parent === dir) break;
21290
21312
  dir = parent;
21291
21313
  }
@@ -22847,7 +22869,6 @@ import { randomUUID as randomUUID2 } from "node:crypto";
22847
22869
  import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
22848
22870
  import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
22849
22871
  import {
22850
- addColumn,
22851
22872
  addTask,
22852
22873
  adoptManagedLifecycle,
22853
22874
  assignTask,
@@ -22862,7 +22883,7 @@ import {
22862
22883
  exportBoardToTaskGraph,
22863
22884
  finalizeTaskCompletion,
22864
22885
  getBoard as getBoard3,
22865
- getKanbanOrchestrationSnapshot,
22886
+ getKanbanOrchestrationSnapshot as getKanbanOrchestrationSnapshot2,
22866
22887
  getKanbanQueueHealth,
22867
22888
  getTask,
22868
22889
  getTaskChain,
@@ -22876,16 +22897,16 @@ import {
22876
22897
  recoverStaleTaskAssignments,
22877
22898
  releaseTaskClaim,
22878
22899
  removeBoard as removeBoard2,
22879
- removeColumn,
22880
22900
  removeTask,
22881
22901
  repairManagedTaskProjection,
22902
+ resolveAutoAccept,
22882
22903
  searchKanban,
22883
22904
  setTaskChain,
22905
+ stripLifecycleIssues,
22884
22906
  syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
22885
22907
  transferTaskToBoard,
22886
22908
  transitionTask,
22887
22909
  updateBoard as updateBoard2,
22888
- updateColumn,
22889
22910
  updateTask as updateTask2,
22890
22911
  updateTaskAssignment,
22891
22912
  verifyTaskCompletion as verifyTaskCompletion2
@@ -22935,6 +22956,137 @@ function duplicateBoardOptions(input) {
22935
22956
  };
22936
22957
  }
22937
22958
 
22959
+ // src/kanban-contract-actions.ts
22960
+ import {
22961
+ addContractEdge,
22962
+ configureContractGraph,
22963
+ evaluateTaskContractGraph,
22964
+ getContractGraph,
22965
+ removeContractEdge,
22966
+ removeContractNode,
22967
+ upsertContractNode
22968
+ } from "@wrongstack/kanban";
22969
+
22970
+ // src/kanban-tool-results.ts
22971
+ function atomicityNudge(task) {
22972
+ if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
22973
+ const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
22974
+ return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
22975
+ }
22976
+ function readEnvGateEnforcement() {
22977
+ const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
22978
+ return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
22979
+ }
22980
+ function fail(message) {
22981
+ return { ok: false, message };
22982
+ }
22983
+ function okBoard(board, message = "Board loaded.") {
22984
+ return { ok: true, message, board };
22985
+ }
22986
+ function okTask(board, task, message) {
22987
+ return { ok: true, message, board, task };
22988
+ }
22989
+
22990
+ // src/kanban-contract-actions.ts
22991
+ async function handleKanbanContractAction(projectRoot, input, actor) {
22992
+ switch (input.action) {
22993
+ case "get_contract_graph": {
22994
+ if (!input.boardId) return fail("get_contract_graph requires boardId.");
22995
+ const found = await getContractGraph(projectRoot, input.boardId);
22996
+ if (!found) return fail("Board not found.");
22997
+ const evaluated = input.taskId ? await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId) : null;
22998
+ if (input.taskId && !evaluated) return fail("Task not found on this board.");
22999
+ return {
23000
+ ok: true,
23001
+ 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.",
23002
+ board: found.board,
23003
+ contractGraph: found.graph,
23004
+ ...evaluated ? { contractEvaluation: evaluated.evaluation } : {}
23005
+ };
23006
+ }
23007
+ case "configure_contract_graph": {
23008
+ if (!input.boardId) return fail("configure_contract_graph requires boardId.");
23009
+ const enforcement = input.contractEnforcement ?? "advisory";
23010
+ const board = await configureContractGraph(projectRoot, input.boardId, enforcement);
23011
+ return board ? okBoard(board, `Contract map enforcement set to ${enforcement}.`) : fail("Board not found.");
23012
+ }
23013
+ case "upsert_contract_node": {
23014
+ if (!input.boardId || !input.taskId) {
23015
+ return fail("upsert_contract_node requires boardId and taskId.");
23016
+ }
23017
+ if (!input.contractNodeKind || !input.contractNodeTitle) {
23018
+ return fail("upsert_contract_node requires contractNodeKind and contractNodeTitle.");
23019
+ }
23020
+ const waiver = input.contractNodeState === "waived" ? {
23021
+ actor: actor ?? "agent",
23022
+ reason: input.contractWaiverReason ?? "",
23023
+ at: (/* @__PURE__ */ new Date()).toISOString()
23024
+ } : void 0;
23025
+ if (waiver && !waiver.reason.trim()) {
23026
+ return fail("A waived contract node requires contractWaiverReason.");
23027
+ }
23028
+ const result = await upsertContractNode(projectRoot, input.boardId, {
23029
+ taskId: input.taskId,
23030
+ kind: input.contractNodeKind,
23031
+ title: input.contractNodeTitle,
23032
+ ...input.contractNodeId !== void 0 ? { id: input.contractNodeId } : {},
23033
+ ...input.contractNodeDescription !== void 0 ? { description: input.contractNodeDescription } : {},
23034
+ ...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
23035
+ ...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
23036
+ ...input.contractCheckId !== void 0 ? { checkId: input.contractCheckId } : {},
23037
+ ...input.contractMetricId !== void 0 ? { metricId: input.contractMetricId } : {},
23038
+ ...waiver ? { waiver } : {},
23039
+ ...actor !== void 0 ? { createdBy: actor } : {}
23040
+ });
23041
+ if (!result) return fail("Board or task not found.");
23042
+ return {
23043
+ ok: true,
23044
+ message: `Contract node ${result.node.kind} "${result.node.title}" saved (${result.node.id}).`,
23045
+ board: result.board,
23046
+ contractGraph: result.board.contractGraph ?? null
23047
+ };
23048
+ }
23049
+ case "remove_contract_node": {
23050
+ if (!input.boardId || !input.contractNodeId) {
23051
+ return fail("remove_contract_node requires boardId and contractNodeId.");
23052
+ }
23053
+ const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
23054
+ return board ? okBoard(board, "Contract node removed, along with every edge that touched it.") : fail("Contract node not found.");
23055
+ }
23056
+ case "add_contract_edge": {
23057
+ if (!input.boardId || !input.contractEdgeFrom || !input.contractEdgeTo) {
23058
+ return fail("add_contract_edge requires boardId, contractEdgeFrom, and contractEdgeTo.");
23059
+ }
23060
+ if (!input.contractEdgeType) return fail("add_contract_edge requires contractEdgeType.");
23061
+ const result = await addContractEdge(projectRoot, input.boardId, {
23062
+ from: input.contractEdgeFrom,
23063
+ to: input.contractEdgeTo,
23064
+ type: input.contractEdgeType,
23065
+ ...input.contractEdgeId !== void 0 ? { id: input.contractEdgeId } : {},
23066
+ ...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
23067
+ ...input.contractEdgeRationale !== void 0 ? { rationale: input.contractEdgeRationale } : {},
23068
+ ...actor !== void 0 ? { createdBy: actor } : {}
23069
+ });
23070
+ if (!result) return fail("Board not found.");
23071
+ return {
23072
+ ok: true,
23073
+ message: `Contract edge ${result.edge.type}: ${result.edge.from} \u2192 ${result.edge.to}.`,
23074
+ board: result.board,
23075
+ contractGraph: result.board.contractGraph ?? null
23076
+ };
23077
+ }
23078
+ case "remove_contract_edge": {
23079
+ if (!input.boardId || !input.contractEdgeId) {
23080
+ return fail("remove_contract_edge requires boardId and contractEdgeId.");
23081
+ }
23082
+ const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
23083
+ return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
23084
+ }
23085
+ default:
23086
+ return void 0;
23087
+ }
23088
+ }
23089
+
22938
23090
  // src/kanban-decomposition-actions.ts
22939
23091
  import {
22940
23092
  assessTaskAtomicity,
@@ -22966,26 +23118,6 @@ function recordKanbanVerificationEvidence(ctx, report) {
22966
23118
  }
22967
23119
  }
22968
23120
 
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
23121
  // src/kanban-decomposition-actions.ts
22990
23122
  async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
22991
23123
  switch (input.action) {
@@ -23070,20 +23202,14 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
23070
23202
  // src/kanban-detail-actions.ts
23071
23203
  import {
23072
23204
  addCheckToTask,
23073
- addContractEdge,
23074
23205
  addDependency,
23075
23206
  addGoalMetricToTask,
23076
23207
  addLinkToTask,
23077
23208
  addNoteToTask,
23078
- configureContractGraph,
23079
- evaluateTaskContractGraph,
23080
- getContractGraph,
23081
23209
  getKanbanWorkbench,
23082
- removeContractEdge,
23083
- removeContractNode,
23210
+ removeCheckFromTask,
23084
23211
  updateCheckOnTask,
23085
- updateGoalMetricOnTask,
23086
- upsertContractNode
23212
+ updateGoalMetricOnTask
23087
23213
  } from "@wrongstack/kanban";
23088
23214
 
23089
23215
  // src/kanban-split-task-handler.ts
@@ -23149,131 +23275,6 @@ async function handleKanbanDetailAction(projectRoot, input) {
23149
23275
  workbench
23150
23276
  };
23151
23277
  }
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
23278
  case "add_dependency": {
23278
23279
  if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
23279
23280
  return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
@@ -23326,8 +23327,9 @@ async function handleKanbanDetailAction(projectRoot, input) {
23326
23327
  }
23327
23328
  const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
23328
23329
  description: input.checkDescription,
23329
- type: "manual",
23330
- status: input.checkStatus
23330
+ type: input.checkType ?? "manual",
23331
+ status: input.checkStatus,
23332
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
23331
23333
  });
23332
23334
  return board ? okBoard(board, "Check added.") : fail("Task not found.");
23333
23335
  }
@@ -23342,11 +23344,27 @@ async function handleKanbanDetailAction(projectRoot, input) {
23342
23344
  input.checkId,
23343
23345
  {
23344
23346
  ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
23345
- ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
23347
+ ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {},
23348
+ // Promoting an existing manual criterion to an executable one is the
23349
+ // common repair: the card was written before anyone knew the command.
23350
+ ...input.checkType !== void 0 ? { type: input.checkType } : {},
23351
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
23346
23352
  }
23347
23353
  );
23348
23354
  return board ? okBoard(board, "Check updated.") : fail("Check not found.");
23349
23355
  }
23356
+ case "remove_check": {
23357
+ if (!input.boardId || !input.taskId || !input.checkId) {
23358
+ return fail("remove_check requires boardId, taskId, and checkId.");
23359
+ }
23360
+ const board = await removeCheckFromTask(
23361
+ projectRoot,
23362
+ input.boardId,
23363
+ input.taskId,
23364
+ input.checkId
23365
+ );
23366
+ return board ? okBoard(board, "Acceptance criterion removed.") : fail("Check not found on this task.");
23367
+ }
23350
23368
  case "add_note": {
23351
23369
  if (!input.boardId || !input.taskId || !input.note)
23352
23370
  return fail("add_note requires boardId, taskId, and note.");
@@ -23421,14 +23439,25 @@ function taskInput(input) {
23421
23439
  ...input.order !== void 0 ? { order: input.order } : {},
23422
23440
  ...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
23423
23441
  ...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
23442
+ // The system prompt has always told the model it may "set atomic: true"
23443
+ // when creating a composite parent. It could not: the field reached
23444
+ // neither the create input nor the patch, so the instruction described a
23445
+ // capability that did not exist and the attempt was silently dropped.
23446
+ ...input.atomic !== void 0 ? { atomic: input.atomic } : {},
23424
23447
  ...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
23425
23448
  ...input.checkDescription !== void 0 ? {
23426
23449
  successCriteria: [
23427
23450
  {
23428
23451
  id: randomUUID(),
23429
23452
  description: input.checkDescription,
23430
- type: "manual",
23431
- status: input.checkStatus ?? "pending"
23453
+ // `manual` only as the fallback. Hard-coding it here meant every
23454
+ // agent-authored criterion was unverifiable by construction: the
23455
+ // deterministic plugins never matched, the registry passed the
23456
+ // hand-set status straight through, and "verified" collapsed into
23457
+ // "the author ticked its own box".
23458
+ type: input.checkType ?? "manual",
23459
+ status: input.checkStatus ?? "pending",
23460
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
23432
23461
  }
23433
23462
  ]
23434
23463
  } : {},
@@ -23476,11 +23505,11 @@ function taskInput(input) {
23476
23505
  };
23477
23506
  }
23478
23507
  function mergedDependsOn(input) {
23479
- const ids = [
23508
+ if (input.dependsOn === void 0 && input.dependencyTaskId === void 0) return void 0;
23509
+ return [
23480
23510
  ...input.dependsOn ?? [],
23481
23511
  ...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
23482
23512
  ].filter((id, i, arr) => id && arr.indexOf(id) === i);
23483
- return ids.length > 0 ? ids : void 0;
23484
23513
  }
23485
23514
  function taskPatch(input) {
23486
23515
  return {
@@ -23494,7 +23523,15 @@ function taskPatch(input) {
23494
23523
  status: input.status,
23495
23524
  labels: input.labels,
23496
23525
  assignedAgent: input.agentId,
23497
- ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
23526
+ ...mergedDependsOn(input) !== void 0 ? { dependsOn: mergedDependsOn(input) } : {},
23527
+ // `atomic` and `childTaskIds` are the composite-parent contract, and the
23528
+ // managed gate reads both: an `atomic` parent may not move forward without
23529
+ // children, and may not reach Done until every child is completed. The
23530
+ // manager has always accepted both on a patch; only this surface withheld
23531
+ // them, so `split_atomic` was a one-way door — delete the children and the
23532
+ // parent was stranded with no way to declare itself a leaf again.
23533
+ ...input.atomic !== void 0 ? { atomic: input.atomic } : {},
23534
+ ...input.childTaskIds !== void 0 ? { childTaskIds: input.childTaskIds } : {},
23498
23535
  ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
23499
23536
  ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
23500
23537
  };
@@ -23554,8 +23591,8 @@ function assignmentForTaskCreate(input) {
23554
23591
  }
23555
23592
 
23556
23593
  // 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.";
23594
+ 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.";
23595
+ 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
23596
  var KANBAN_INPUT_SCHEMA = {
23560
23597
  type: "object",
23561
23598
  properties: {
@@ -23568,6 +23605,7 @@ var KANBAN_INPUT_SCHEMA = {
23568
23605
  "duplicate_board",
23569
23606
  "update_board",
23570
23607
  "adopt_managed_lifecycle",
23608
+ "release_managed_lifecycle",
23571
23609
  "delete_board",
23572
23610
  "generate_board",
23573
23611
  "export_markdown",
@@ -23579,9 +23617,6 @@ var KANBAN_INPUT_SCHEMA = {
23579
23617
  "ready_tasks",
23580
23618
  "snapshot",
23581
23619
  "workbench",
23582
- "add_column",
23583
- "update_column",
23584
- "delete_column",
23585
23620
  "add_task",
23586
23621
  "split_task",
23587
23622
  "merge_tasks",
@@ -23596,13 +23631,6 @@ var KANBAN_INPUT_SCHEMA = {
23596
23631
  "delete_task",
23597
23632
  "set_chain",
23598
23633
  "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
23634
  "claim_task",
23607
23635
  "release_task",
23608
23636
  "assign_task",
@@ -23616,49 +23644,27 @@ var KANBAN_INPUT_SCHEMA = {
23616
23644
  "update_goal_metric",
23617
23645
  "add_check",
23618
23646
  "update_check",
23647
+ "remove_check",
23619
23648
  "add_note",
23620
23649
  "add_link",
23621
23650
  "verify_completion",
23622
23651
  "split_atomic",
23623
23652
  "assess_atomicity",
23624
- "propose_decomposition"
23653
+ "propose_decomposition",
23654
+ "get_contract_graph",
23655
+ "configure_contract_graph",
23656
+ "upsert_contract_node",
23657
+ "remove_contract_node",
23658
+ "add_contract_edge",
23659
+ "remove_contract_edge"
23625
23660
  ]
23626
23661
  },
23627
23662
  boardId: { type: "string" },
23628
23663
  taskId: { type: "string" },
23629
23664
  taskIds: { type: "array", items: { type: "string" } },
23630
23665
  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
23666
  fromNodeId: { type: "string" },
23660
23667
  toNodeId: { type: "string" },
23661
- contractRationale: { type: "string" },
23662
23668
  baseline: { oneOf: [{ type: "string" }, { type: "number" }] },
23663
23669
  threshold: { oneOf: [{ type: "string" }, { type: "number" }] },
23664
23670
  columnId: { type: "string" },
@@ -23742,7 +23748,20 @@ var KANBAN_INPUT_SCHEMA = {
23742
23748
  costCeilingUsd: { type: "number" },
23743
23749
  retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
23744
23750
  lastFailureKind: { type: "string" },
23745
- dependsOn: { type: "array", items: { type: "string" } },
23751
+ dependsOn: {
23752
+ type: "array",
23753
+ items: { type: "string" },
23754
+ 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."
23755
+ },
23756
+ atomic: {
23757
+ type: "boolean",
23758
+ description: "Composite parent (true) or executable leaf (false). Set false to make a stranded parent a leaf again after its children were dropped."
23759
+ },
23760
+ childTaskIds: {
23761
+ type: "array",
23762
+ items: { type: "string" },
23763
+ description: "Children of a composite parent. On update_task an explicit empty array detaches them all."
23764
+ },
23746
23765
  estimatedHours: { type: "number" },
23747
23766
  actualHours: { type: "number" },
23748
23767
  taskGraph: { type: "object" },
@@ -23776,6 +23795,74 @@ var KANBAN_INPUT_SCHEMA = {
23776
23795
  checkId: { type: "string" },
23777
23796
  checkDescription: { type: "string" },
23778
23797
  checkStatus: { type: "string", enum: ["pending", "passed", "failed", "skipped"] },
23798
+ checkType: {
23799
+ type: "string",
23800
+ // Only types a verifier can actually execute. `manual` is the default and
23801
+ // means a human or agent asserts the status by hand. The rest are run by
23802
+ // `verify_completion` against the default deterministic registry. Types
23803
+ // with no plugin in that registry (`auto`, `review`, `agent`, `council`)
23804
+ // are deliberately omitted: offering them would produce criteria that
23805
+ // silently report `skipped — no verifier plugin registered`.
23806
+ enum: ["manual", "command", "test", "file_exists", "file_matches", "git_diff", "metric"],
23807
+ 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.'
23808
+ },
23809
+ checkNotes: {
23810
+ type: "string",
23811
+ 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"}.'
23812
+ },
23813
+ // ── Contract map ───────────────────────────────────────────────────
23814
+ // The card contract: what this work targets, what it must not break, what
23815
+ // it risks, and what verifies it. Advisory by default — the readiness gate
23816
+ // deliberately does not require map structure, so a map is an operator
23817
+ // review aid, not work the model must complete before implementing.
23818
+ contractEnforcement: {
23819
+ type: "string",
23820
+ enum: ["off", "advisory", "strict"],
23821
+ description: "Board-level contract map enforcement. Default when first configured: advisory."
23822
+ },
23823
+ contractNodeId: { type: "string" },
23824
+ contractNodeKind: {
23825
+ type: "string",
23826
+ enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"],
23827
+ 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."
23828
+ },
23829
+ contractNodeTitle: { type: "string" },
23830
+ contractNodeDescription: { type: "string" },
23831
+ contractNodeState: {
23832
+ type: "string",
23833
+ enum: ["unknown", "active", "satisfied", "violated", "waived", "resolved"]
23834
+ },
23835
+ contractNodeEnforcement: {
23836
+ type: "string",
23837
+ enum: ["blocking", "advisory", "informational"]
23838
+ },
23839
+ /** Bind a node to an acceptance criterion or goal metric already on the task. */
23840
+ contractCheckId: { type: "string" },
23841
+ contractMetricId: { type: "string" },
23842
+ contractWaiverReason: {
23843
+ type: "string",
23844
+ description: 'Required, with an actor, when contractNodeState is "waived".'
23845
+ },
23846
+ contractEdgeId: { type: "string" },
23847
+ contractEdgeFrom: {
23848
+ type: "string",
23849
+ description: 'A contract node id, or a task id (bare or "task:<id>") for the card endpoint.'
23850
+ },
23851
+ contractEdgeTo: { type: "string" },
23852
+ contractEdgeType: {
23853
+ type: "string",
23854
+ enum: [
23855
+ "targets",
23856
+ "affects",
23857
+ "must_preserve",
23858
+ "exposes",
23859
+ "verified_by",
23860
+ "conflicts_with",
23861
+ "derived_from",
23862
+ "relates_to"
23863
+ ]
23864
+ },
23865
+ contractEdgeRationale: { type: "string" },
23779
23866
  note: { type: "string" },
23780
23867
  author: { type: "string" },
23781
23868
  url: { type: "string" },
@@ -23821,7 +23908,7 @@ var KANBAN_INPUT_SCHEMA = {
23821
23908
 
23822
23909
  // src/session-kanban.ts
23823
23910
  import { watch } from "node:fs";
23824
- import { basename as basename13, dirname as dirname15 } from "node:path";
23911
+ import { basename as basename13, dirname as dirname14 } from "node:path";
23825
23912
  import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
23826
23913
  import {
23827
23914
  loadPlan,
@@ -23830,12 +23917,17 @@ import {
23830
23917
  mutateTasks
23831
23918
  } from "@wrongstack/core/storage";
23832
23919
  import { deserializeTaskGraph } from "@wrongstack/core/tasking";
23833
- import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
23920
+ import { formatTodosForModel, resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
23834
23921
  import {
23835
23922
  bridgeKanbanSupervisor,
23923
+ compactSessionMirrorBoard,
23836
23924
  createBoard,
23925
+ DEFAULT_COLUMNS,
23837
23926
  getBoard as getBoard2,
23927
+ getDependencyReadinessIssues,
23928
+ getKanbanOrchestrationSnapshot,
23838
23929
  listBoards,
23930
+ pruneSessionBoards,
23839
23931
  removeBoard,
23840
23932
  syncBoardFromTaskGraph,
23841
23933
  touchKanbanPresence as touchKanbanPresence2,
@@ -23843,16 +23935,14 @@ import {
23843
23935
  } from "@wrongstack/kanban";
23844
23936
  var SESSION_BOARD_TAG = "session-work";
23845
23937
  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
- ];
23938
+ var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
23939
+ ...column
23940
+ }));
23852
23941
  var boardQueue = /* @__PURE__ */ new Map();
23853
23942
  var boardEnsures = /* @__PURE__ */ new Map();
23854
23943
  var pendingMirrors = /* @__PURE__ */ new Map();
23855
23944
  var activeMirrors = /* @__PURE__ */ new Set();
23945
+ var mirrorFailures = /* @__PURE__ */ new Map();
23856
23946
  var bindings = /* @__PURE__ */ new WeakMap();
23857
23947
  var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
23858
23948
  var activeSessionBoards = /* @__PURE__ */ new Map();
@@ -23862,6 +23952,33 @@ function boardKey(projectRoot, sessionId) {
23862
23952
  function mirrorKey(projectRoot, sessionId, sourceSystem) {
23863
23953
  return `${boardKey(projectRoot, sessionId)}\0${sourceSystem}`;
23864
23954
  }
23955
+ function completedReconciliationGraph(latest, candidates) {
23956
+ const latestNodeIds = new Set(latest.nodes.map((node) => node.id));
23957
+ const carriedNodeIds = /* @__PURE__ */ new Set();
23958
+ const completedNodes = candidates.flatMap(
23959
+ (candidate) => candidate.nodes.filter((node) => {
23960
+ if (node.status !== "completed" || latestNodeIds.has(node.id) || carriedNodeIds.has(node.id)) {
23961
+ return false;
23962
+ }
23963
+ carriedNodeIds.add(node.id);
23964
+ return true;
23965
+ })
23966
+ );
23967
+ if (completedNodes.length === 0) return void 0;
23968
+ const carriedRequirements = completedNodes.flatMap(
23969
+ (node) => node.specRequirementId ? [node.specRequirementId] : []
23970
+ );
23971
+ return {
23972
+ ...latest,
23973
+ nodes: [...latest.nodes, ...completedNodes],
23974
+ rootNodes: [.../* @__PURE__ */ new Set([...latest.rootNodes, ...completedNodes.map((node) => node.id)])],
23975
+ ...latest.requiredRequirementIds ? {
23976
+ requiredRequirementIds: [
23977
+ .../* @__PURE__ */ new Set([...latest.requiredRequirementIds, ...carriedRequirements])
23978
+ ]
23979
+ } : {}
23980
+ };
23981
+ }
23865
23982
  function sessionTag(sessionId) {
23866
23983
  return `session:${sessionId}`;
23867
23984
  }
@@ -24002,16 +24119,44 @@ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
24002
24119
  sourceSystem,
24003
24120
  tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
24004
24121
  archiveMissingTasks: true,
24005
- includeCompletedTasks: true
24122
+ includeCompletedTasks: true,
24123
+ // The scope ledger stays declared and accurate, but it may not veto a
24124
+ // projection. A session mirror reflects a tactical list that shrinks by
24125
+ // design, and refusing the sync never protected the removed row — it
24126
+ // froze the entire board, permanently, because the stored scope then
24127
+ // outlived every later snapshot (`session-kanban.mirror-failed`).
24128
+ // Nothing is lost by shrinking here: `archiveMissingTasks` keeps the
24129
+ // removed card on the board as `archived`, the reconciliation pass
24130
+ // first walks vanished completed rows to Done, and the session journal
24131
+ // remains the durable record.
24132
+ allowRequirementScopeShrink: true
24006
24133
  }
24007
24134
  );
24008
- return result?.board ?? null;
24135
+ if (!result) return null;
24136
+ const compacted = await compactSessionMirrorBoard(projectRoot, board.id);
24137
+ if (compacted?.removedTaskIds.length) {
24138
+ return await getBoard2(projectRoot, board.id) ?? result.board;
24139
+ }
24140
+ return result.board;
24009
24141
  });
24010
24142
  }
24011
24143
  function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
24012
24144
  if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return;
24013
24145
  const key = mirrorKey(projectRoot, sessionId, sourceSystem);
24014
- pendingMirrors.set(key, { projectRoot, sessionId, graph, sourceSystem });
24146
+ const previous = pendingMirrors.get(key);
24147
+ const reconciliationGraph = previous ? completedReconciliationGraph(
24148
+ graph,
24149
+ [previous.reconciliationGraph, previous.graph].filter(
24150
+ (candidate) => candidate !== void 0
24151
+ )
24152
+ ) : void 0;
24153
+ pendingMirrors.set(key, {
24154
+ projectRoot,
24155
+ sessionId,
24156
+ graph,
24157
+ ...reconciliationGraph ? { reconciliationGraph } : {},
24158
+ sourceSystem
24159
+ });
24015
24160
  if (activeMirrors.has(key)) return;
24016
24161
  activeMirrors.add(key);
24017
24162
  void (async () => {
@@ -24021,20 +24166,34 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
24021
24166
  if (!pending2) break;
24022
24167
  pendingMirrors.delete(key);
24023
24168
  try {
24169
+ if (pending2.reconciliationGraph) {
24170
+ await projectGraph(
24171
+ pending2.projectRoot,
24172
+ pending2.sessionId,
24173
+ pending2.reconciliationGraph,
24174
+ pending2.sourceSystem
24175
+ );
24176
+ }
24024
24177
  await projectGraph(
24025
24178
  pending2.projectRoot,
24026
24179
  pending2.sessionId,
24027
24180
  pending2.graph,
24028
24181
  pending2.sourceSystem
24029
24182
  );
24183
+ mirrorFailures.delete(boardKey(pending2.projectRoot, pending2.sessionId));
24030
24184
  } catch (error) {
24185
+ const message = error instanceof Error ? error.message : String(error);
24186
+ mirrorFailures.set(boardKey(pending2.projectRoot, pending2.sessionId), {
24187
+ message,
24188
+ sourceSystem: pending2.sourceSystem
24189
+ });
24031
24190
  console.warn(
24032
24191
  JSON.stringify({
24033
24192
  level: "warn",
24034
24193
  event: "session-kanban.mirror-failed",
24035
24194
  sessionId: pending2.sessionId,
24036
24195
  sourceSystem: pending2.sourceSystem,
24037
- message: error instanceof Error ? error.message : String(error),
24196
+ message,
24038
24197
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
24039
24198
  })
24040
24199
  );
@@ -24055,6 +24214,19 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
24055
24214
  }
24056
24215
  })();
24057
24216
  }
24217
+ function takeSessionMirrorFailure(projectRoot, sessionId) {
24218
+ if (!projectRoot || !sessionId) return void 0;
24219
+ const key = boardKey(projectRoot, sessionId);
24220
+ const failure = mirrorFailures.get(key);
24221
+ if (!failure) return void 0;
24222
+ mirrorFailures.delete(key);
24223
+ return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
24224
+ }
24225
+ function hasInFlightTodoMirror(projectRoot, sessionId) {
24226
+ if (!projectRoot || !sessionId) return false;
24227
+ const key = mirrorKey(projectRoot, sessionId, "session-todo");
24228
+ return pendingMirrors.has(key) || activeMirrors.has(key);
24229
+ }
24058
24230
  function todoListToSerializedGraph(todos, sessionId) {
24059
24231
  const graphId = `todo:${sessionId}`;
24060
24232
  const nodes = todos.map((todo, index) => ({
@@ -24216,11 +24388,11 @@ function broadcastTodoUpdate(context, todos) {
24216
24388
  });
24217
24389
  }
24218
24390
  function notifyTodoUpdate(context, todos) {
24219
- const summary = todos.length ? todos.map((todo) => `- [${todo.status}] ${todo.content} (${todo.id})`).join("\n") : "- No active todos remain.";
24391
+ const summary = formatTodosForModel(todos);
24220
24392
  const text = `[KANBAN TODO UPDATE]
24221
24393
  Another Kanban agent reassessed the shared board. The canonical todo list is now:
24222
24394
  ${summary}
24223
- Reassess your current plan before continuing; do not rely on the initial todo snapshot.`;
24395
+ 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
24396
  const state = context.state;
24225
24397
  if (typeof state.appendBlockToLastUserMessage === "function") {
24226
24398
  if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
@@ -24229,6 +24401,10 @@ Reassess your current plan before continuing; do not rely on the initial todo sn
24229
24401
  state.appendMessage({ role: "user", content: [{ type: "text", text }] });
24230
24402
  }
24231
24403
  }
24404
+ function todosNeedingSessionMirror(todos, activeBoardId2) {
24405
+ if (!activeBoardId2) return todos;
24406
+ return todos.filter((todo) => todo.kanbanBoardId !== activeBoardId2 || !todo.kanbanTaskId);
24407
+ }
24232
24408
  function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
24233
24409
  queueLatestMirror(
24234
24410
  projectRoot,
@@ -24350,7 +24526,7 @@ function attachSessionKanbanMirror(context) {
24350
24526
  const configureWatcher = () => {
24351
24527
  const planPath = context.meta["plan.path"];
24352
24528
  const taskPath = context.meta["task.path"];
24353
- const candidate = typeof planPath === "string" && planPath ? dirname15(planPath) : typeof taskPath === "string" && taskPath ? dirname15(taskPath) : "";
24529
+ const candidate = typeof planPath === "string" && planPath ? dirname14(planPath) : typeof taskPath === "string" && taskPath ? dirname14(taskPath) : "";
24354
24530
  if (!candidate || candidate === watchedDir) return;
24355
24531
  watcher?.close();
24356
24532
  watcher = null;
@@ -24375,16 +24551,9 @@ function attachSessionKanbanMirror(context) {
24375
24551
  const unsubscribe = context.state.onChange((change) => {
24376
24552
  if (change.kind === "todos_replaced" && !suppressedTodoMirrors.has(context)) {
24377
24553
  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
- );
24554
+ const unbound = todosNeedingSessionMirror(snapshot, activeManagedBoardId());
24555
+ if (snapshot.length > 0 && unbound.length === 0) return;
24556
+ mirrorSessionTodosToKanban(context.projectRoot, unbound, sessionId());
24388
24557
  return;
24389
24558
  }
24390
24559
  if (change.kind === "meta_set" && (change.key === "plan.path" || change.key === "task.path" || change.key === "kanban")) {
@@ -24420,10 +24589,54 @@ function attachSessionKanbanMirror(context) {
24420
24589
  bindings.set(context, detach);
24421
24590
  return detach;
24422
24591
  }
24592
+ async function rebindSessionKanbanTask(context) {
24593
+ const sessionId = context.session?.id;
24594
+ if (!sessionId || !context.projectRoot) return null;
24595
+ if (context.currentKanbanTaskId) return null;
24596
+ let best;
24597
+ try {
24598
+ const snapshot = await getKanbanOrchestrationSnapshot(context.projectRoot);
24599
+ const nowMs = Date.now();
24600
+ for (const result of snapshot.running) {
24601
+ const assignment = result.task.assignment;
24602
+ if (assignment?.status !== "running") continue;
24603
+ const expiresAt = assignment.leaseExpiresAt ? Date.parse(assignment.leaseExpiresAt) : Number.NaN;
24604
+ if (Number.isFinite(expiresAt) && expiresAt <= nowMs) continue;
24605
+ const entry = result.board.presence?.find(
24606
+ (candidate) => candidate.sessionId === sessionId && candidate.taskId === result.task.id
24607
+ );
24608
+ if (!entry) continue;
24609
+ if (!best || entry.lastSeenAt > best.lastSeenAt) {
24610
+ best = { boardId: result.board.id, taskId: result.task.id, lastSeenAt: entry.lastSeenAt };
24611
+ }
24612
+ }
24613
+ } catch {
24614
+ return null;
24615
+ }
24616
+ if (!best) return null;
24617
+ context.setCurrentKanbanTask(best.taskId, best.boardId);
24618
+ return { boardId: best.boardId, taskId: best.taskId };
24619
+ }
24620
+ var degradationReason;
24423
24621
  async function hydrateSessionKanban(context) {
24424
24622
  const id = context.session?.id ?? "";
24425
24623
  if (!id) return null;
24624
+ try {
24625
+ const board = await hydrateSessionKanbanBoard(context, id);
24626
+ degradationReason = void 0;
24627
+ return board;
24628
+ } catch (error) {
24629
+ degradationReason = error instanceof Error ? error.message : String(error);
24630
+ fireAndForget("hydrate", Promise.reject(error));
24631
+ return null;
24632
+ }
24633
+ }
24634
+ async function hydrateSessionKanbanBoard(context, id) {
24635
+ await rebindSessionKanbanTask(context);
24426
24636
  await cleanupEmptySessionKanbanBoards(context.projectRoot, id);
24637
+ if (context.projectRoot) {
24638
+ fireAndForget("prune-session-boards", pruneSessionBoards(context.projectRoot));
24639
+ }
24427
24640
  let board = await ensureSessionKanbanBoard(context.projectRoot, id);
24428
24641
  if (context.todos.length) {
24429
24642
  board = await projectSessionTodosToKanban(context.projectRoot, context.todos, id);
@@ -24454,26 +24667,68 @@ function todoStatus(task) {
24454
24667
  if (status === "in_progress" || status === "review") return "in_progress";
24455
24668
  return "pending";
24456
24669
  }
24457
- function sessionTodoFromTask(task, boardId) {
24670
+ function sessionTodoFromTask(task, board) {
24671
+ const blockedBy = board ? blockingTitles(board, task) : [];
24458
24672
  return {
24459
24673
  id: task.origin?.taskId ?? task.id,
24460
24674
  content: task.title,
24461
24675
  status: todoStatus(task),
24462
- kanbanBoardId: boardId,
24463
- kanbanTaskId: task.id,
24464
- ...task.description ? { activeForm: task.description } : {}
24676
+ ...task.description ? { activeForm: task.description } : {},
24677
+ ...blockedBy.length ? { blockedBy } : {}
24465
24678
  };
24466
24679
  }
24467
- function managedTodoFromTask(task, boardId) {
24680
+ function managedTodoFromTask(task, board) {
24468
24681
  return {
24469
- ...sessionTodoFromTask(task, boardId),
24470
- status: task.status === "completed" ? "completed" : task.status === "in_progress" ? "in_progress" : "pending"
24682
+ ...sessionTodoFromTask(task, board),
24683
+ kanbanBoardId: board.id,
24684
+ kanbanTaskId: task.id
24471
24685
  };
24472
24686
  }
24687
+ function blockingTitles(board, task) {
24688
+ return getDependencyReadinessIssues(board, task).map((issue) => {
24689
+ const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
24690
+ if (!dependency) return `${issue.dependencyId} (missing)`;
24691
+ return dependency.title;
24692
+ });
24693
+ }
24694
+ var PRIORITY_ORDER = {
24695
+ critical: 0,
24696
+ high: 1,
24697
+ medium: 2,
24698
+ low: 3
24699
+ };
24700
+ function orderTasksForTodos(board, tasks) {
24701
+ const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
24702
+ const baseline = [...tasks].sort(
24703
+ (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)
24704
+ );
24705
+ const included = new Set(baseline.map((task) => task.id));
24706
+ const remaining = new Map(baseline.map((task) => [task.id, task]));
24707
+ const emitted = [];
24708
+ const done = /* @__PURE__ */ new Set();
24709
+ while (remaining.size > 0) {
24710
+ const ready = baseline.filter(
24711
+ (task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
24712
+ (dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
24713
+ )
24714
+ );
24715
+ if (ready.length === 0) break;
24716
+ for (const task of ready) {
24717
+ remaining.delete(task.id);
24718
+ done.add(task.id);
24719
+ emitted.push(task);
24720
+ }
24721
+ }
24722
+ for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
24723
+ return emitted;
24724
+ }
24473
24725
  function sameTodos(left, right) {
24474
24726
  return left.length === right.length && left.every((todo, index) => {
24475
24727
  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;
24728
+ 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,
24729
+ // the rows are otherwise identical and the unblocking would never
24730
+ // reach the model.
24731
+ (candidate.blockedBy ?? []).join("\0") === (todo.blockedBy ?? []).join("\0");
24477
24732
  });
24478
24733
  }
24479
24734
  function applySessionKanbanBoardToTodos(context, board) {
@@ -24481,13 +24736,13 @@ function applySessionKanbanBoardToTodos(context, board) {
24481
24736
  if (!sessionId || sessionIdFromTags(board.tags) !== sessionId || !isOwnedSessionBoard(board.tags)) {
24482
24737
  return [...context.todos];
24483
24738
  }
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));
24739
+ if (hasInFlightTodoMirror(context.projectRoot, sessionId)) return [...context.todos];
24740
+ const projectedTodos = orderTasksForTodos(
24741
+ board,
24742
+ board.tasks.filter(
24743
+ (task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
24744
+ )
24745
+ ).map((task) => sessionTodoFromTask(task, board));
24491
24746
  const allCompleted = projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === "completed");
24492
24747
  const effectiveTodos = allCompleted ? [] : projectedTodos;
24493
24748
  if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];
@@ -24508,11 +24763,12 @@ function applyManagedKanbanBoardToTodos(context, board) {
24508
24763
  if (!activeBoardId2 || board.id !== activeBoardId2 || board.lifecycle?.mode !== "managed") {
24509
24764
  return [...context.todos];
24510
24765
  }
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));
24766
+ const projectedTodos = orderTasksForTodos(
24767
+ board,
24768
+ board.tasks.filter(
24769
+ (task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
24770
+ )
24771
+ ).map((task) => managedTodoFromTask(task, board));
24516
24772
  if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
24517
24773
  suppressedTodoMirrors.add(context);
24518
24774
  try {
@@ -24547,7 +24803,11 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
24547
24803
  const id = context.session?.id ?? "";
24548
24804
  if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
24549
24805
  const planPath = context.meta["plan.path"];
24550
- if (typeof planPath !== "string" || !planPath) return { source: "plan" };
24806
+ if (typeof planPath !== "string" || !planPath) {
24807
+ throw new Error(
24808
+ "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."
24809
+ );
24810
+ }
24551
24811
  const plan = await mutatePlan(planPath, id, (file) => ({
24552
24812
  ...file,
24553
24813
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -24565,7 +24825,11 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
24565
24825
  }
24566
24826
  if (task.origin?.system === "session-task" || task.origin?.system === "session" || graphId.startsWith("session:")) {
24567
24827
  const taskPath = context.meta["task.path"];
24568
- if (typeof taskPath !== "string" || !taskPath) return { source: "task" };
24828
+ if (typeof taskPath !== "string" || !taskPath) {
24829
+ throw new Error(
24830
+ "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."
24831
+ );
24832
+ }
24569
24833
  const tasks = await mutateTasks(taskPath, id, (file) => ({
24570
24834
  ...file,
24571
24835
  tasks: options.remove ? file.tasks.filter((item) => item.id !== originId) : file.tasks.map(
@@ -24614,8 +24878,14 @@ var kanbanTool = {
24614
24878
  }
24615
24879
  case "create_board": {
24616
24880
  if (!input.title) return fail("create_board requires title.");
24881
+ const existing = (await listBoards2(projectRoot)).filter(
24882
+ (candidate) => (candidate.kind ?? "project") === "project"
24883
+ );
24617
24884
  const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
24618
- return { ok: true, message: `Board created: ${board.title}`, board };
24885
+ 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(
24886
+ ", "
24887
+ )}${existing.length > 3 ? ", \u2026" : ""}. If this work belongs to one of them, add_task there instead and delete this board.` : "";
24888
+ return { ok: true, message: `Board created: ${board.title}.${note}`, board };
24619
24889
  }
24620
24890
  case "update_board": {
24621
24891
  if (!input.boardId) return fail("update_board requires boardId.");
@@ -24644,6 +24914,20 @@ var kanbanTool = {
24644
24914
  });
24645
24915
  return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
24646
24916
  }
24917
+ // Adoption used to be a one-way door: the strict lifecycle carries
24918
+ // acceptance-criteria, verification-report, review-evidence and
24919
+ // one-stage-at-a-time gates, and nothing on the tool surface could
24920
+ // undo it, so a board adopted once kept its ceremony forever. The
24921
+ // gates are worth having where a fleet is supervised; they are not
24922
+ // worth being unable to leave. Cards and columns are untouched.
24923
+ case "release_managed_lifecycle": {
24924
+ if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
24925
+ const board = await updateBoard2(projectRoot, input.boardId, { lifecycle: null });
24926
+ return board ? okBoard(
24927
+ board,
24928
+ "Managed lifecycle released; the board now tracks work without strict gates."
24929
+ ) : fail("Board not found.");
24930
+ }
24647
24931
  case "duplicate_board": {
24648
24932
  if (!input.boardId) return fail("duplicate_board requires boardId.");
24649
24933
  const board = await duplicateBoard(
@@ -24663,8 +24947,7 @@ var kanbanTool = {
24663
24947
  const boardInput = createBoardFromText({
24664
24948
  description: input.description,
24665
24949
  ...input.title !== void 0 ? { title: input.title } : {},
24666
- ...input.context !== void 0 ? { context: input.context } : {},
24667
- ...input.columns !== void 0 ? { columns: input.columns } : {}
24950
+ ...input.context !== void 0 ? { context: input.context } : {}
24668
24951
  });
24669
24952
  const board = await createBoard2(projectRoot, boardInput);
24670
24953
  for (const taskInput2 of parseLinesIntoTasks(
@@ -24802,7 +25085,7 @@ var kanbanTool = {
24802
25085
  return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
24803
25086
  }
24804
25087
  case "snapshot": {
24805
- const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
25088
+ const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
24806
25089
  query: input.query,
24807
25090
  boardId: input.boardId,
24808
25091
  assignedAgent: input.agentId,
@@ -24817,33 +25100,6 @@ var kanbanTool = {
24817
25100
  snapshot
24818
25101
  };
24819
25102
  }
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
25103
  case "add_task": {
24848
25104
  if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
24849
25105
  const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
@@ -24925,6 +25181,32 @@ var kanbanTool = {
24925
25181
  `Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
24926
25182
  );
24927
25183
  }
25184
+ if (board.lifecycle?.mode !== "managed") {
25185
+ const now2 = /* @__PURE__ */ new Date();
25186
+ const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
25187
+ status: "running",
25188
+ agentId: input.agentId ?? input.author,
25189
+ leaseId: input.leaseId ?? randomUUID2(),
25190
+ claimedAt: input.claimedAt ?? now2.toISOString(),
25191
+ heartbeatAt: input.heartbeatAt ?? now2.toISOString(),
25192
+ leaseExpiresAt: input.leaseExpiresAt ?? new Date(now2.getTime() + 15 * 6e4).toISOString(),
25193
+ attempt: input.attempt ?? 1,
25194
+ maxAttempts: input.maxAttempts ?? 3
25195
+ });
25196
+ if (!assigned) return fail("Task assignment could not be started.");
25197
+ const started = await updateTask2(projectRoot, board.id, task.id, {
25198
+ status: "in_progress"
25199
+ });
25200
+ const current = started ?? assigned;
25201
+ const claimed = task;
25202
+ const currentTask = current.tasks.find((candidate) => candidate.id === claimed.id) ?? claimed;
25203
+ ctx.setCurrentKanbanTask?.(currentTask.id, current.id);
25204
+ return okTask(
25205
+ current,
25206
+ currentTask,
25207
+ "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."
25208
+ );
25209
+ }
24928
25210
  let stage = task.lifecycle?.currentStage;
24929
25211
  if (stage === "backlog") {
24930
25212
  const moved = await transitionTask(projectRoot, board.id, task.id, {
@@ -25065,6 +25347,9 @@ var kanbanTool = {
25065
25347
  if (!input.boardId || !input.taskId)
25066
25348
  return fail("delete_task requires boardId and taskId.");
25067
25349
  const board = await removeTask(projectRoot, input.boardId, input.taskId);
25350
+ if (board && ctx.currentKanbanTaskId === input.taskId) {
25351
+ ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
25352
+ }
25068
25353
  return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
25069
25354
  }
25070
25355
  case "set_chain": {
@@ -25197,7 +25482,7 @@ var kanbanTool = {
25197
25482
  });
25198
25483
  } catch (err) {
25199
25484
  lifecycleWarnings.push(
25200
- `Lifecycle transition to Running deferred: ${err instanceof Error ? err.message : String(err)}`
25485
+ `Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
25201
25486
  );
25202
25487
  }
25203
25488
  }
@@ -25221,7 +25506,7 @@ var kanbanTool = {
25221
25506
  });
25222
25507
  } catch (err) {
25223
25508
  lifecycleWarnings.push(
25224
- `Lifecycle transition to Review failed: ${err instanceof Error ? err.message : String(err)}`
25509
+ `Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
25225
25510
  );
25226
25511
  }
25227
25512
  if (transitionResult) {
@@ -25241,7 +25526,11 @@ var kanbanTool = {
25241
25526
  successCriteria: verResult.task.successCriteria
25242
25527
  });
25243
25528
  const verdict = verResult.report.verdict;
25244
- if (verdict === "passed") {
25529
+ if (verdict === "passed" && !resolveAutoAccept(board)) {
25530
+ lifecycleWarnings.push(
25531
+ "Verification passed, but this board does not auto-accept. The card is in Review awaiting an explicit transition_task to done."
25532
+ );
25533
+ } else if (verdict === "passed") {
25245
25534
  try {
25246
25535
  const doneResult = await transitionTask(
25247
25536
  projectRoot,
@@ -25359,11 +25648,34 @@ var kanbanTool = {
25359
25648
  });
25360
25649
  return {
25361
25650
  ok: true,
25362
- message: `Counts: ready=${health.counts.ready}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
25651
+ message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
25363
25652
  queueHealth: health
25364
25653
  };
25365
25654
  }
25655
+ // Not every action is handled above. These are dispatched from here,
25656
+ // and the split has already cost real time: an agent that read this
25657
+ // file concluded `add_check` / `update_check` did not exist, wrote
25658
+ // that on a card, and spent a session trying to satisfy a gate it
25659
+ // already had the tool to clear. Keep this index in step with the
25660
+ // handlers.
25661
+ //
25662
+ // kanban-detail-actions.ts workbench · add_dependency ·
25663
+ // add_goal_metric · update_goal_metric · add_check ·
25664
+ // update_check · add_note · add_link · split_atomic
25665
+ // kanban-decomposition-actions.ts verify_completion ·
25666
+ // assess_atomicity · propose_decomposition
25667
+ // kanban-contract-actions.ts get_contract_graph ·
25668
+ // configure_contract_graph · upsert_contract_node ·
25669
+ // remove_contract_node · add_contract_edge · remove_contract_edge
25366
25670
  default:
25671
+ {
25672
+ const contractResult = await handleKanbanContractAction(
25673
+ projectRoot,
25674
+ input,
25675
+ input.author ?? input.agentId
25676
+ );
25677
+ if (contractResult !== void 0) return contractResult;
25678
+ }
25367
25679
  {
25368
25680
  const detailResult = await handleKanbanDetailAction(projectRoot, input);
25369
25681
  if (detailResult !== void 0) return detailResult;
@@ -25373,7 +25685,7 @@ var kanbanTool = {
25373
25685
  })();
25374
25686
  return withPresence(result);
25375
25687
  } catch (err) {
25376
- return fail(err instanceof Error ? err.message : String(err));
25688
+ return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
25377
25689
  }
25378
25690
  }
25379
25691
  };
@@ -26226,7 +26538,7 @@ import {
26226
26538
  saveTasks,
26227
26539
  setPlanItemStatus
26228
26540
  } from "@wrongstack/core/storage";
26229
- import { getBoard as getBoard4 } from "@wrongstack/kanban";
26541
+ import { addTask as addTask2, getBoard as getBoard4 } from "@wrongstack/kanban";
26230
26542
  function normalizedTitle(value) {
26231
26543
  return value.trim().toLocaleLowerCase().replace(/\s+/g, " ");
26232
26544
  }
@@ -26254,11 +26566,51 @@ function bindTodosToBoard(items, previous, board) {
26254
26566
  available.find((task2) => !used.has(task2.id) && normalizedTitle(task2.title) === title)
26255
26567
  ];
26256
26568
  const task = candidates.find((candidate) => candidate && !used.has(candidate.id));
26257
- if (!task) return { ...item };
26569
+ if (!task) {
26570
+ const { blockedBy: _discarded, ...rest } = item;
26571
+ return { ...rest };
26572
+ }
26258
26573
  used.add(task.id);
26259
- return { ...item, kanbanBoardId: board.id, kanbanTaskId: task.id };
26574
+ const blockedBy = blockingTitles(board, task);
26575
+ return {
26576
+ ...item,
26577
+ kanbanBoardId: board.id,
26578
+ kanbanTaskId: task.id,
26579
+ ...blockedBy.length ? { blockedBy } : { blockedBy: void 0 }
26580
+ };
26260
26581
  });
26261
26582
  }
26583
+ function demoteBlockedInProgress(items, warnings) {
26584
+ return items.map((item) => {
26585
+ if (item.status !== "in_progress" || !item.blockedBy?.length) return item;
26586
+ warnings.push(
26587
+ `"${item.content}" cannot start yet \u2014 it waits on: ${item.blockedBy.join("; ")}. Kept as pending; complete the blocking work first.`
26588
+ );
26589
+ return { ...item, status: "pending" };
26590
+ });
26591
+ }
26592
+ async function createMissingManagedCards(items, board, ctx, warnings) {
26593
+ const created = /* @__PURE__ */ new Map();
26594
+ for (const item of items) {
26595
+ if (item.kanbanBoardId === board.id && item.kanbanTaskId) continue;
26596
+ try {
26597
+ const result = await addTask2(ctx.projectRoot, board.id, {
26598
+ title: item.content,
26599
+ description: item.activeForm?.trim() || `Added from the session todo list: ${item.content}`
26600
+ });
26601
+ if (!result) {
26602
+ warnings.push(`Could not open a Kanban card for "${item.content}": board not found.`);
26603
+ continue;
26604
+ }
26605
+ created.set(item.id, result.task.id);
26606
+ } catch (error) {
26607
+ warnings.push(
26608
+ `Could not open a Kanban card for "${item.content}": ${error instanceof Error ? error.message : String(error)}`
26609
+ );
26610
+ }
26611
+ }
26612
+ return created;
26613
+ }
26262
26614
  async function synchronizeManagedKanban(items, board, ctx, signal) {
26263
26615
  let synced = 0;
26264
26616
  const warnings = [];
@@ -26296,6 +26648,16 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
26296
26648
  transitionComment: `Todo returned to queue: ${item.content}`
26297
26649
  });
26298
26650
  }
26651
+ for (const item of items) {
26652
+ if (item.status === "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
26653
+ continue;
26654
+ }
26655
+ const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
26656
+ if (task?.status !== "completed") continue;
26657
+ warnings.push(
26658
+ `"${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.`
26659
+ );
26660
+ }
26299
26661
  for (const item of items) {
26300
26662
  if (item.status !== "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
26301
26663
  continue;
@@ -26347,17 +26709,25 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
26347
26709
  const active = items.find(
26348
26710
  (item) => item.status === "in_progress" && item.kanbanBoardId === board.id && Boolean(item.kanbanTaskId)
26349
26711
  );
26712
+ const activeStage = active?.kanbanTaskId ? afterCompletions?.tasks.find((task) => task.id === active.kanbanTaskId)?.lifecycle?.currentStage : void 0;
26350
26713
  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
- });
26714
+ if (activeStage === "review" || activeStage === "done") {
26715
+ warnings.push(
26716
+ `"${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.")
26717
+ );
26718
+ } else {
26719
+ await execute({
26720
+ action: "start_task",
26721
+ boardId: board.id,
26722
+ taskId: active.kanbanTaskId,
26723
+ author: actor,
26724
+ agentId: actor,
26725
+ transitionComment: `Todo activated: ${active.content}`
26726
+ });
26727
+ }
26359
26728
  }
26360
- if (active?.kanbanTaskId && completionPending) {
26729
+ if (active?.kanbanTaskId && activeStage === "review") {
26730
+ } else if (active?.kanbanTaskId && completionPending) {
26361
26731
  warnings.push(
26362
26732
  "A completed todo is still awaiting acceptance; the next independent Kanban task was started."
26363
26733
  );
@@ -26444,29 +26814,47 @@ var todoTool = {
26444
26814
  }
26445
26815
  }
26446
26816
  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;
26817
+ let board = boardId ? await getBoard4(ctx.projectRoot, boardId) : null;
26818
+ const managed = board?.lifecycle?.mode === "managed";
26819
+ let boundItems = managed && board ? bindTodosToBoard(items, ctx.todos ?? [], board) : items;
26820
+ const creationWarnings = [];
26821
+ if (managed && board) {
26822
+ const managedBoardId = board.id;
26823
+ const created = await createMissingManagedCards(boundItems, board, ctx, creationWarnings);
26824
+ if (created.size > 0) {
26825
+ boundItems = boundItems.map((item) => {
26826
+ const taskId = created.get(item.id);
26827
+ return taskId ? { ...item, kanbanBoardId: managedBoardId, kanbanTaskId: taskId } : item;
26828
+ });
26829
+ board = await getBoard4(ctx.projectRoot, managedBoardId) ?? board;
26830
+ boundItems = bindTodosToBoard(boundItems, ctx.todos ?? [], board);
26831
+ }
26832
+ boundItems = demoteBlockedInProgress(boundItems, creationWarnings);
26833
+ }
26449
26834
  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") {
26835
+ const kanbanSync = managed && board ? await synchronizeManagedKanban(boundItems, board, ctx, call.signal) : { synced: 0, warnings: [] };
26836
+ kanbanSync.warnings.unshift(...creationWarnings);
26837
+ if (managed && board) {
26452
26838
  const unresolved = boundItems.filter(
26453
26839
  (item) => item.kanbanBoardId !== board.id || !item.kanbanTaskId
26454
26840
  );
26455
26841
  if (unresolved.length > 0) {
26456
26842
  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.`
26843
+ `${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
26844
  );
26459
26845
  }
26460
26846
  }
26847
+ const mirrorFailure = takeSessionMirrorFailure(ctx.projectRoot, ctx.session?.id ?? "");
26848
+ if (mirrorFailure) kanbanSync.warnings.push(mirrorFailure);
26461
26849
  let projectedBoard = board;
26462
- if (board?.lifecycle?.mode === "managed") {
26850
+ if (managed && board) {
26463
26851
  const refreshed = await getBoard4(ctx.projectRoot, board.id);
26464
26852
  if (refreshed) {
26465
26853
  projectedBoard = refreshed;
26466
26854
  applyManagedKanbanBoardToTodos(ctx, refreshed);
26467
26855
  }
26468
26856
  }
26469
- if (board?.lifecycle?.mode !== "managed") {
26857
+ if (!managed) {
26470
26858
  mirrorSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? "session");
26471
26859
  }
26472
26860
  const completedPlanIds = /* @__PURE__ */ new Set();
@@ -29502,6 +29890,7 @@ var OPTIONAL_TOOLS = [
29502
29890
  toolHelpTool,
29503
29891
  setWorkingDirTool
29504
29892
  ];
29893
+ var OFF_ONLY_TOOLS = [...browserTools, e2ePlanTool];
29505
29894
  var TIER1_TOOLS = [
29506
29895
  readTool,
29507
29896
  writeTool,
@@ -30983,6 +31372,7 @@ export {
30983
31372
  IndexCircuitBreaker,
30984
31373
  IndexTimeoutError,
30985
31374
  LanguageProfileRegistry,
31375
+ OFF_ONLY_TOOLS,
30986
31376
  OPTIONAL_TOOLS,
30987
31377
  PRIMARY_LANGUAGE_PROFILES,
30988
31378
  SESSION_KANBAN_COLUMNS,
@@ -31102,6 +31492,7 @@ export {
31102
31492
  projectSessionTasksToKanban,
31103
31493
  projectSessionTodosToKanban,
31104
31494
  readTool,
31495
+ rebindSessionKanbanTask,
31105
31496
  recordKanbanVerificationEvidence,
31106
31497
  redactBrowserText,
31107
31498
  registerBuiltinToolTier,