@wrongstack/tools 0.303.0 → 0.305.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pack.js CHANGED
@@ -12621,7 +12621,7 @@ var IndexStore = class _IndexStore {
12621
12621
  const ftsSchema = this.stmt(
12622
12622
  "SELECT sql FROM sqlite_master WHERE type='table' AND name='symbols_fts'"
12623
12623
  ).get();
12624
- if (ftsSchema?.sql && ftsSchema.sql.includes("unicode61")) {
12624
+ if (ftsSchema?.sql?.includes("unicode61")) {
12625
12625
  this.db.exec("DROP TABLE IF EXISTS symbols_fts");
12626
12626
  }
12627
12627
  this.db.exec(SYMBOLS_FTS_SQL);
@@ -13114,9 +13114,13 @@ var IndexStore = class _IndexStore {
13114
13114
  sim: cosineSimilarity(queryVec, decodeVector(r.vector))
13115
13115
  })).sort((a, b) => b.sim - a.sim);
13116
13116
  const bm25Rank = /* @__PURE__ */ new Map();
13117
- bm25Rows.forEach((r, i) => bm25Rank.set(r.id, i));
13117
+ bm25Rows.forEach((r, i) => {
13118
+ bm25Rank.set(r.id, i);
13119
+ });
13118
13120
  const vecRank = /* @__PURE__ */ new Map();
13119
- vecScores.forEach((r, i) => vecRank.set(r.id, i));
13121
+ vecScores.forEach((r, i) => {
13122
+ vecRank.set(r.id, i);
13123
+ });
13120
13124
  const fused = reciprocalRankFusion(bm25Rank, vecRank, 60);
13121
13125
  const fusedScore = new Map(fused);
13122
13126
  const sorted = [...bm25Rows].sort(
@@ -14485,6 +14489,86 @@ import {
14485
14489
  isFrugalPerf
14486
14490
  } from "@wrongstack/core/utils";
14487
14491
 
14492
+ // src/codebase-index/content-hash.ts
14493
+ var PRIME64_1 = 0x9e3779b185ebca87n;
14494
+ var PRIME64_2 = 0xc2b2ae3d27d4eb4fn;
14495
+ var PRIME64_3 = 0x165667b19e3779f9n;
14496
+ var PRIME64_4 = 0x85ebca77c2b2ae63n;
14497
+ var PRIME64_5 = 0x27d4eb2f165667c5n;
14498
+ var MASK64 = 0xffffffffffffffffn;
14499
+ function mul64(a, b) {
14500
+ return (a & MASK64) * (b & MASK64) & MASK64;
14501
+ }
14502
+ function rotl64(x, n) {
14503
+ const v = x & MASK64;
14504
+ return (v << BigInt(n) | v >> BigInt(64 - n)) & MASK64;
14505
+ }
14506
+ function readU64LE(buf, off) {
14507
+ let v = 0n;
14508
+ for (let i = 7; i >= 0; i--) {
14509
+ v = v << 8n | BigInt(buf[off + i] ?? 0);
14510
+ }
14511
+ return v & MASK64;
14512
+ }
14513
+ function readU32LE(buf, off) {
14514
+ return (BigInt(buf[off] ?? 0) | BigInt(buf[off + 1] ?? 0) << 8n | BigInt(buf[off + 2] ?? 0) << 16n | BigInt(buf[off + 3] ?? 0) << 24n) & MASK64;
14515
+ }
14516
+ function xxh64Round(acc, lane) {
14517
+ return mul64(rotl64(acc + mul64(lane, PRIME64_2) & MASK64, 31), PRIME64_1);
14518
+ }
14519
+ function xxh64MergeRound(acc, val) {
14520
+ return mul64(acc ^ xxh64Round(0n, val), PRIME64_1) + PRIME64_4 & MASK64;
14521
+ }
14522
+ function xxhash64Hex(buf, explicitLen) {
14523
+ const length = explicitLen ?? buf.length;
14524
+ let h;
14525
+ let off = 0;
14526
+ if (length >= 32) {
14527
+ let v1 = PRIME64_1 + PRIME64_2 & MASK64;
14528
+ let v2 = PRIME64_2;
14529
+ let v3 = 0n;
14530
+ let v4 = 0n - PRIME64_1 & MASK64;
14531
+ const end32 = length - 32;
14532
+ while (off <= end32) {
14533
+ v1 = xxh64Round(v1, readU64LE(buf, off));
14534
+ v2 = xxh64Round(v2, readU64LE(buf, off + 8));
14535
+ v3 = xxh64Round(v3, readU64LE(buf, off + 16));
14536
+ v4 = xxh64Round(v4, readU64LE(buf, off + 24));
14537
+ off += 32;
14538
+ }
14539
+ h = rotl64(v1, 1) + rotl64(v2, 7) + rotl64(v3, 12) + rotl64(v4, 18) & MASK64;
14540
+ h = xxh64MergeRound(h, v1);
14541
+ h = xxh64MergeRound(h, v2);
14542
+ h = xxh64MergeRound(h, v3);
14543
+ h = xxh64MergeRound(h, v4);
14544
+ } else {
14545
+ h = PRIME64_5;
14546
+ }
14547
+ h = h + BigInt(length) & MASK64;
14548
+ while (off + 8 <= length) {
14549
+ const k1 = xxh64Round(0n, readU64LE(buf, off));
14550
+ h = mul64(rotl64(h ^ k1, 27), PRIME64_1) + PRIME64_4 & MASK64;
14551
+ off += 8;
14552
+ }
14553
+ if (off + 4 <= length) {
14554
+ h = mul64(rotl64(h ^ mul64(readU32LE(buf, off), PRIME64_1), 23), PRIME64_2) + PRIME64_3 & MASK64;
14555
+ off += 4;
14556
+ }
14557
+ while (off < length) {
14558
+ h = mul64(rotl64(h ^ mul64(BigInt(buf[off] ?? 0), PRIME64_5), 11), PRIME64_1) & MASK64;
14559
+ off += 1;
14560
+ }
14561
+ h = (h ^ h >> 33n) & MASK64;
14562
+ h = mul64(h, PRIME64_2);
14563
+ h = (h ^ h >> 29n) & MASK64;
14564
+ h = mul64(h, PRIME64_3);
14565
+ h = (h ^ h >> 32n) & MASK64;
14566
+ return h.toString(16).padStart(16, "0");
14567
+ }
14568
+ function xxhash64String(content) {
14569
+ return xxhash64Hex(new TextEncoder().encode(content));
14570
+ }
14571
+
14488
14572
  // src/codebase-index/gitignore.ts
14489
14573
  import * as fs14 from "node:fs/promises";
14490
14574
  import * as path18 from "node:path";
@@ -15212,91 +15296,14 @@ function getParserPool() {
15212
15296
  return _pool;
15213
15297
  }
15214
15298
 
15215
- // src/codebase-index/content-hash.ts
15216
- var PRIME64_1 = 0x9e3779b185ebca87n;
15217
- var PRIME64_2 = 0xc2b2ae3d27d4eb4fn;
15218
- var PRIME64_3 = 0x165667b19e3779f9n;
15219
- var PRIME64_4 = 0x85ebca77c2b2ae63n;
15220
- var PRIME64_5 = 0x27d4eb2f165667c5n;
15221
- var MASK64 = 0xffffffffffffffffn;
15222
- function mul64(a, b) {
15223
- return (a & MASK64) * (b & MASK64) & MASK64;
15224
- }
15225
- function rotl64(x, n) {
15226
- const v = x & MASK64;
15227
- return (v << BigInt(n) | v >> BigInt(64 - n)) & MASK64;
15228
- }
15229
- function readU64LE(buf, off) {
15230
- let v = 0n;
15231
- for (let i = 7; i >= 0; i--) {
15232
- v = v << 8n | BigInt(buf[off + i] ?? 0);
15233
- }
15234
- return v & MASK64;
15235
- }
15236
- function readU32LE(buf, off) {
15237
- return (BigInt(buf[off] ?? 0) | BigInt(buf[off + 1] ?? 0) << 8n | BigInt(buf[off + 2] ?? 0) << 16n | BigInt(buf[off + 3] ?? 0) << 24n) & MASK64;
15238
- }
15239
- function xxh64Round(acc, lane) {
15240
- return mul64(rotl64(acc + mul64(lane, PRIME64_2) & MASK64, 31), PRIME64_1);
15241
- }
15242
- function xxh64MergeRound(acc, val) {
15243
- return mul64(acc ^ xxh64Round(0n, val), PRIME64_1) + PRIME64_4 & MASK64;
15244
- }
15245
- function xxhash64Hex(buf, explicitLen) {
15246
- const length = explicitLen ?? buf.length;
15247
- let h;
15248
- let off = 0;
15249
- if (length >= 32) {
15250
- let v1 = PRIME64_1 + PRIME64_2 & MASK64;
15251
- let v2 = PRIME64_2;
15252
- let v3 = 0n;
15253
- let v4 = 0n - PRIME64_1 & MASK64;
15254
- const end32 = length - 32;
15255
- while (off <= end32) {
15256
- v1 = xxh64Round(v1, readU64LE(buf, off));
15257
- v2 = xxh64Round(v2, readU64LE(buf, off + 8));
15258
- v3 = xxh64Round(v3, readU64LE(buf, off + 16));
15259
- v4 = xxh64Round(v4, readU64LE(buf, off + 24));
15260
- off += 32;
15261
- }
15262
- h = rotl64(v1, 1) + rotl64(v2, 7) + rotl64(v3, 12) + rotl64(v4, 18) & MASK64;
15263
- h = xxh64MergeRound(h, v1);
15264
- h = xxh64MergeRound(h, v2);
15265
- h = xxh64MergeRound(h, v3);
15266
- h = xxh64MergeRound(h, v4);
15267
- } else {
15268
- h = PRIME64_5;
15269
- }
15270
- h = h + BigInt(length) & MASK64;
15271
- while (off + 8 <= length) {
15272
- const k1 = xxh64Round(0n, readU64LE(buf, off));
15273
- h = mul64(rotl64(h ^ k1, 27), PRIME64_1) + PRIME64_4 & MASK64;
15274
- off += 8;
15275
- }
15276
- if (off + 4 <= length) {
15277
- h = mul64(rotl64(h ^ mul64(readU32LE(buf, off), PRIME64_1), 23), PRIME64_2) + PRIME64_3 & MASK64;
15278
- off += 4;
15279
- }
15280
- while (off < length) {
15281
- h = mul64(rotl64(h ^ mul64(BigInt(buf[off] ?? 0), PRIME64_5), 11), PRIME64_1) & MASK64;
15282
- off += 1;
15283
- }
15284
- h = (h ^ h >> 33n) & MASK64;
15285
- h = mul64(h, PRIME64_2);
15286
- h = (h ^ h >> 29n) & MASK64;
15287
- h = mul64(h, PRIME64_3);
15288
- h = (h ^ h >> 32n) & MASK64;
15289
- return h.toString(16).padStart(16, "0");
15290
- }
15291
- function xxhash64String(content) {
15292
- return xxhash64Hex(new TextEncoder().encode(content));
15293
- }
15294
-
15295
15299
  // src/codebase-index/indexer.ts
15296
15300
  var YIELD_EVERY_N = 50;
15297
15301
  function resolveParallelBatch() {
15298
15302
  return indexParallelBatchSize(availableParallelism());
15299
15303
  }
15304
+ function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
15305
+ return !isFrugalPerf() && candidateFileCount >= WORKER_POOL_THRESHOLD && parseBatchCount > 1;
15306
+ }
15300
15307
  function yieldEventLoop() {
15301
15308
  return new Promise((resolve16) => setImmediate(resolve16));
15302
15309
  }
@@ -15473,11 +15480,7 @@ async function resolveProjectRelations(store, projectRoot, opts) {
15473
15480
  const structure = await detectModuleRoots(projectRoot, indexedFiles);
15474
15481
  if (opts.signal?.aborted) return;
15475
15482
  store.setFilePackages(assignPackageLabels(structure, indexedFiles));
15476
- const resolver = new ModuleResolver(
15477
- structure,
15478
- indexedFiles,
15479
- store.getNamespaceDeclarations()
15480
- );
15483
+ const resolver = new ModuleResolver(structure, indexedFiles, store.getNamespaceDeclarations());
15481
15484
  const pending2 = store.getUnresolvedImports(opts.onlyFiles);
15482
15485
  const resolutions = [];
15483
15486
  for (const entry of pending2) {
@@ -15502,6 +15505,10 @@ async function runIndexerWithStore(store, opts) {
15502
15505
  const errors = [];
15503
15506
  const langStats = {};
15504
15507
  let filesIndexed = 0;
15508
+ let filesParsed = 0;
15509
+ let filesSkipped = 0;
15510
+ let filesEmpty = 0;
15511
+ let filesFailed = 0;
15505
15512
  let symbolsIndexed = 0;
15506
15513
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
15507
15514
  let files;
@@ -15543,12 +15550,14 @@ async function runIndexerWithStore(store, opts) {
15543
15550
  langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
15544
15551
  symbolsIndexed += meta.symbolCount;
15545
15552
  filesIndexed++;
15553
+ filesSkipped++;
15546
15554
  filesPreSkipped++;
15547
15555
  return false;
15548
15556
  });
15549
15557
  if (filesPreSkipped > 0) opts.onProgress?.(filesPreSkipped, totalFilesForProgress);
15550
15558
  }
15551
15559
  const parallelBatch = resolveParallelBatch();
15560
+ const parserPoolCandidateCount = files.length;
15552
15561
  let filesSinceLastYield = 0;
15553
15562
  for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
15554
15563
  const batchEnd = Math.min(batchStart + parallelBatch, files.length);
@@ -15641,7 +15650,7 @@ async function runIndexerWithStore(store, opts) {
15641
15650
  });
15642
15651
  }
15643
15652
  if (toParse.length > 0) {
15644
- let pool = toParse.length >= WORKER_POOL_THRESHOLD ? getParserPool() : null;
15653
+ let pool = shouldUseParserWorkerPool(parserPoolCandidateCount, toParse.length) ? getParserPool() : null;
15645
15654
  if (pool) {
15646
15655
  try {
15647
15656
  await pool.ensureReady();
@@ -15691,12 +15700,14 @@ async function runIndexerWithStore(store, opts) {
15691
15700
  const err = settled.reason;
15692
15701
  if (err instanceof Error && isAbortError(err)) throw err;
15693
15702
  errors.push(`batch error: ${file}: ${err instanceof Error ? err.message : String(err)}`);
15703
+ filesFailed++;
15694
15704
  continue;
15695
15705
  }
15696
15706
  const result = settled.value;
15697
15707
  if (result.error) {
15698
15708
  if (result.missing) store.deleteFile(file);
15699
15709
  errors.push(`${file}: ${result.error}`);
15710
+ filesFailed++;
15700
15711
  continue;
15701
15712
  }
15702
15713
  const { stat: stat18, lang, parsed } = result;
@@ -15704,6 +15715,7 @@ async function runIndexerWithStore(store, opts) {
15704
15715
  langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
15705
15716
  symbolsIndexed += result.skippedMeta.symbolCount;
15706
15717
  filesIndexed++;
15718
+ filesSkipped++;
15707
15719
  const stored = existingMeta.get(file);
15708
15720
  if (stored && stored.mtimeMs !== result.skippedMeta.mtimeMs) {
15709
15721
  store.upsertFile({
@@ -15728,6 +15740,7 @@ async function runIndexerWithStore(store, opts) {
15728
15740
  contentHash: result.contentHash ?? ""
15729
15741
  });
15730
15742
  filesIndexed++;
15743
+ filesEmpty++;
15731
15744
  }
15732
15745
  continue;
15733
15746
  }
@@ -15741,6 +15754,7 @@ async function runIndexerWithStore(store, opts) {
15741
15754
  contentHash: result.contentHash ?? ""
15742
15755
  });
15743
15756
  filesIndexed++;
15757
+ filesEmpty++;
15744
15758
  continue;
15745
15759
  }
15746
15760
  batchEntries.push({
@@ -15762,6 +15776,7 @@ async function runIndexerWithStore(store, opts) {
15762
15776
  symbolsIndexed += count;
15763
15777
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
15764
15778
  filesIndexed++;
15779
+ filesParsed++;
15765
15780
  }
15766
15781
  } catch (err) {
15767
15782
  const message = err instanceof Error ? err.message : String(err);
@@ -15774,6 +15789,7 @@ async function runIndexerWithStore(store, opts) {
15774
15789
  symbolsIndexed += symbolsWithIds.length;
15775
15790
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
15776
15791
  filesIndexed++;
15792
+ filesParsed++;
15777
15793
  if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
15778
15794
  const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
15779
15795
  if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
@@ -15791,6 +15807,7 @@ async function runIndexerWithStore(store, opts) {
15791
15807
  contentHash: entry.contentHash
15792
15808
  });
15793
15809
  } catch (innerErr) {
15810
+ filesFailed++;
15794
15811
  errors.push(
15795
15812
  `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
15796
15813
  );
@@ -15823,6 +15840,12 @@ async function runIndexerWithStore(store, opts) {
15823
15840
  const durationMs = Date.now() - startMs;
15824
15841
  return {
15825
15842
  filesIndexed,
15843
+ fileOutcomes: {
15844
+ parsed: filesParsed,
15845
+ skipped: filesSkipped,
15846
+ empty: filesEmpty,
15847
+ failed: filesFailed
15848
+ },
15826
15849
  symbolsIndexed,
15827
15850
  langStats,
15828
15851
  durationMs,
@@ -22596,7 +22619,6 @@ import { randomUUID as randomUUID2 } from "node:crypto";
22596
22619
  import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
22597
22620
  import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
22598
22621
  import {
22599
- addColumn,
22600
22622
  addTask,
22601
22623
  adoptManagedLifecycle,
22602
22624
  assignTask,
@@ -22611,7 +22633,7 @@ import {
22611
22633
  exportBoardToTaskGraph,
22612
22634
  finalizeTaskCompletion,
22613
22635
  getBoard as getBoard3,
22614
- getKanbanOrchestrationSnapshot,
22636
+ getKanbanOrchestrationSnapshot as getKanbanOrchestrationSnapshot2,
22615
22637
  getKanbanQueueHealth,
22616
22638
  getTask,
22617
22639
  getTaskChain,
@@ -22625,16 +22647,16 @@ import {
22625
22647
  recoverStaleTaskAssignments,
22626
22648
  releaseTaskClaim,
22627
22649
  removeBoard as removeBoard2,
22628
- removeColumn,
22629
22650
  removeTask,
22630
22651
  repairManagedTaskProjection,
22652
+ resolveAutoAccept,
22631
22653
  searchKanban,
22632
22654
  setTaskChain,
22655
+ stripLifecycleIssues,
22633
22656
  syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
22634
22657
  transferTaskToBoard,
22635
22658
  transitionTask,
22636
22659
  updateBoard as updateBoard2,
22637
- updateColumn,
22638
22660
  updateTask as updateTask2,
22639
22661
  updateTaskAssignment,
22640
22662
  verifyTaskCompletion as verifyTaskCompletion2
@@ -22684,6 +22706,137 @@ function duplicateBoardOptions(input) {
22684
22706
  };
22685
22707
  }
22686
22708
 
22709
+ // src/kanban-contract-actions.ts
22710
+ import {
22711
+ addContractEdge,
22712
+ configureContractGraph,
22713
+ evaluateTaskContractGraph,
22714
+ getContractGraph,
22715
+ removeContractEdge,
22716
+ removeContractNode,
22717
+ upsertContractNode
22718
+ } from "@wrongstack/kanban";
22719
+
22720
+ // src/kanban-tool-results.ts
22721
+ function atomicityNudge(task) {
22722
+ if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
22723
+ const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
22724
+ return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
22725
+ }
22726
+ function readEnvGateEnforcement() {
22727
+ const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
22728
+ return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
22729
+ }
22730
+ function fail(message) {
22731
+ return { ok: false, message };
22732
+ }
22733
+ function okBoard(board, message = "Board loaded.") {
22734
+ return { ok: true, message, board };
22735
+ }
22736
+ function okTask(board, task, message) {
22737
+ return { ok: true, message, board, task };
22738
+ }
22739
+
22740
+ // src/kanban-contract-actions.ts
22741
+ async function handleKanbanContractAction(projectRoot, input, actor) {
22742
+ switch (input.action) {
22743
+ case "get_contract_graph": {
22744
+ if (!input.boardId) return fail("get_contract_graph requires boardId.");
22745
+ const found = await getContractGraph(projectRoot, input.boardId);
22746
+ if (!found) return fail("Board not found.");
22747
+ const evaluated = input.taskId ? await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId) : null;
22748
+ if (input.taskId && !evaluated) return fail("Task not found on this board.");
22749
+ return {
22750
+ ok: true,
22751
+ 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.",
22752
+ board: found.board,
22753
+ contractGraph: found.graph,
22754
+ ...evaluated ? { contractEvaluation: evaluated.evaluation } : {}
22755
+ };
22756
+ }
22757
+ case "configure_contract_graph": {
22758
+ if (!input.boardId) return fail("configure_contract_graph requires boardId.");
22759
+ const enforcement = input.contractEnforcement ?? "advisory";
22760
+ const board = await configureContractGraph(projectRoot, input.boardId, enforcement);
22761
+ return board ? okBoard(board, `Contract map enforcement set to ${enforcement}.`) : fail("Board not found.");
22762
+ }
22763
+ case "upsert_contract_node": {
22764
+ if (!input.boardId || !input.taskId) {
22765
+ return fail("upsert_contract_node requires boardId and taskId.");
22766
+ }
22767
+ if (!input.contractNodeKind || !input.contractNodeTitle) {
22768
+ return fail("upsert_contract_node requires contractNodeKind and contractNodeTitle.");
22769
+ }
22770
+ const waiver = input.contractNodeState === "waived" ? {
22771
+ actor: actor ?? "agent",
22772
+ reason: input.contractWaiverReason ?? "",
22773
+ at: (/* @__PURE__ */ new Date()).toISOString()
22774
+ } : void 0;
22775
+ if (waiver && !waiver.reason.trim()) {
22776
+ return fail("A waived contract node requires contractWaiverReason.");
22777
+ }
22778
+ const result = await upsertContractNode(projectRoot, input.boardId, {
22779
+ taskId: input.taskId,
22780
+ kind: input.contractNodeKind,
22781
+ title: input.contractNodeTitle,
22782
+ ...input.contractNodeId !== void 0 ? { id: input.contractNodeId } : {},
22783
+ ...input.contractNodeDescription !== void 0 ? { description: input.contractNodeDescription } : {},
22784
+ ...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
22785
+ ...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
22786
+ ...input.contractCheckId !== void 0 ? { checkId: input.contractCheckId } : {},
22787
+ ...input.contractMetricId !== void 0 ? { metricId: input.contractMetricId } : {},
22788
+ ...waiver ? { waiver } : {},
22789
+ ...actor !== void 0 ? { createdBy: actor } : {}
22790
+ });
22791
+ if (!result) return fail("Board or task not found.");
22792
+ return {
22793
+ ok: true,
22794
+ message: `Contract node ${result.node.kind} "${result.node.title}" saved (${result.node.id}).`,
22795
+ board: result.board,
22796
+ contractGraph: result.board.contractGraph ?? null
22797
+ };
22798
+ }
22799
+ case "remove_contract_node": {
22800
+ if (!input.boardId || !input.contractNodeId) {
22801
+ return fail("remove_contract_node requires boardId and contractNodeId.");
22802
+ }
22803
+ const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
22804
+ return board ? okBoard(board, "Contract node removed, along with every edge that touched it.") : fail("Contract node not found.");
22805
+ }
22806
+ case "add_contract_edge": {
22807
+ if (!input.boardId || !input.contractEdgeFrom || !input.contractEdgeTo) {
22808
+ return fail("add_contract_edge requires boardId, contractEdgeFrom, and contractEdgeTo.");
22809
+ }
22810
+ if (!input.contractEdgeType) return fail("add_contract_edge requires contractEdgeType.");
22811
+ const result = await addContractEdge(projectRoot, input.boardId, {
22812
+ from: input.contractEdgeFrom,
22813
+ to: input.contractEdgeTo,
22814
+ type: input.contractEdgeType,
22815
+ ...input.contractEdgeId !== void 0 ? { id: input.contractEdgeId } : {},
22816
+ ...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
22817
+ ...input.contractEdgeRationale !== void 0 ? { rationale: input.contractEdgeRationale } : {},
22818
+ ...actor !== void 0 ? { createdBy: actor } : {}
22819
+ });
22820
+ if (!result) return fail("Board not found.");
22821
+ return {
22822
+ ok: true,
22823
+ message: `Contract edge ${result.edge.type}: ${result.edge.from} \u2192 ${result.edge.to}.`,
22824
+ board: result.board,
22825
+ contractGraph: result.board.contractGraph ?? null
22826
+ };
22827
+ }
22828
+ case "remove_contract_edge": {
22829
+ if (!input.boardId || !input.contractEdgeId) {
22830
+ return fail("remove_contract_edge requires boardId and contractEdgeId.");
22831
+ }
22832
+ const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
22833
+ return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
22834
+ }
22835
+ default:
22836
+ return void 0;
22837
+ }
22838
+ }
22839
+
22687
22840
  // src/kanban-decomposition-actions.ts
22688
22841
  import {
22689
22842
  assessTaskAtomicity,
@@ -22715,26 +22868,6 @@ function recordKanbanVerificationEvidence(ctx, report) {
22715
22868
  }
22716
22869
  }
22717
22870
 
22718
- // src/kanban-tool-results.ts
22719
- function atomicityNudge(task) {
22720
- if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
22721
- const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
22722
- return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
22723
- }
22724
- function readEnvGateEnforcement() {
22725
- const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
22726
- return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
22727
- }
22728
- function fail(message) {
22729
- return { ok: false, message };
22730
- }
22731
- function okBoard(board, message = "Board loaded.") {
22732
- return { ok: true, message, board };
22733
- }
22734
- function okTask(board, task, message) {
22735
- return { ok: true, message, board, task };
22736
- }
22737
-
22738
22871
  // src/kanban-decomposition-actions.ts
22739
22872
  async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
22740
22873
  switch (input.action) {
@@ -22819,20 +22952,14 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
22819
22952
  // src/kanban-detail-actions.ts
22820
22953
  import {
22821
22954
  addCheckToTask,
22822
- addContractEdge,
22823
22955
  addDependency,
22824
22956
  addGoalMetricToTask,
22825
22957
  addLinkToTask,
22826
22958
  addNoteToTask,
22827
- configureContractGraph,
22828
- evaluateTaskContractGraph,
22829
- getContractGraph,
22830
22959
  getKanbanWorkbench,
22831
- removeContractEdge,
22832
- removeContractNode,
22960
+ removeCheckFromTask,
22833
22961
  updateCheckOnTask,
22834
- updateGoalMetricOnTask,
22835
- upsertContractNode
22962
+ updateGoalMetricOnTask
22836
22963
  } from "@wrongstack/kanban";
22837
22964
 
22838
22965
  // src/kanban-split-task-handler.ts
@@ -22898,131 +23025,6 @@ async function handleKanbanDetailAction(projectRoot, input) {
22898
23025
  workbench
22899
23026
  };
22900
23027
  }
22901
- case "get_contract_graph": {
22902
- if (!input.boardId) return fail("get_contract_graph requires boardId.");
22903
- const result = await getContractGraph(projectRoot, input.boardId);
22904
- return result ? {
22905
- ok: true,
22906
- message: result.graph ? `${result.graph.nodes.length} contract node(s), ${result.graph.edges.length} edge(s).` : "Contract graph is not configured.",
22907
- board: result.board,
22908
- ...result.graph ? { contractGraph: result.graph } : {}
22909
- } : fail("Board not found.");
22910
- }
22911
- case "configure_contract_graph": {
22912
- if (!input.boardId || !input.contractGraphEnforcement) {
22913
- return fail("configure_contract_graph requires boardId and contractGraphEnforcement.");
22914
- }
22915
- const current = await getContractGraph(projectRoot, input.boardId);
22916
- if (!current) return fail("Board not found.");
22917
- if (input.contractGraphEnforcement === "strict" && current.graph?.enforcement !== "strict") {
22918
- return fail(
22919
- "Strict Contract Map enforcement is operator-owned. Autonomous agents may use advisory maps but may not turn them into an execution gate."
22920
- );
22921
- }
22922
- if (current.graph?.enforcement === "strict" && input.contractGraphEnforcement !== "strict") {
22923
- return fail("An autonomous agent may not loosen a strict contract graph.");
22924
- }
22925
- const board = await configureContractGraph(
22926
- projectRoot,
22927
- input.boardId,
22928
- input.contractGraphEnforcement
22929
- );
22930
- return board ? okBoard(board, "Contract graph configured.") : fail("Board not found.");
22931
- }
22932
- case "upsert_contract_node": {
22933
- if (!input.boardId || !input.taskId || !input.contractNodeKind || !input.title) {
22934
- return fail("upsert_contract_node requires boardId, taskId, contractNodeKind, and title.");
22935
- }
22936
- if (input.contractNodeState === "waived") {
22937
- return fail(
22938
- "The autonomous kanban tool may not waive contract nodes; a human-owned review surface must record that exception."
22939
- );
22940
- }
22941
- if (input.contractNodeId) {
22942
- const current = await getContractGraph(projectRoot, input.boardId);
22943
- const existing = current?.graph?.nodes.find((node) => node.id === input.contractNodeId);
22944
- if (current?.graph?.enforcement === "strict" && existing && (existing.kind !== input.contractNodeKind || input.contractEnforcement !== void 0 && input.contractEnforcement !== existing.enforcement)) {
22945
- return fail(
22946
- "The autonomous kanban tool may not change the kind or enforcement of an existing strict contract node."
22947
- );
22948
- }
22949
- }
22950
- const result = await upsertContractNode(projectRoot, input.boardId, {
22951
- ...input.contractNodeId ? { id: input.contractNodeId } : {},
22952
- taskId: input.taskId,
22953
- kind: input.contractNodeKind,
22954
- title: input.title,
22955
- ...input.description !== void 0 ? { description: input.description } : {},
22956
- ...input.contractEnforcement !== void 0 ? { enforcement: input.contractEnforcement } : {},
22957
- ...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
22958
- ...input.checkId !== void 0 ? { checkId: input.checkId } : {},
22959
- ...input.metricId !== void 0 ? { metricId: input.metricId } : {},
22960
- ...input.baseline !== void 0 ? { baseline: input.baseline } : {},
22961
- ...input.threshold !== void 0 ? { threshold: input.threshold } : {},
22962
- ...input.author !== void 0 ? { createdBy: input.author } : {}
22963
- });
22964
- return result ? {
22965
- ...okBoard(result.board, "Contract node saved."),
22966
- contractGraph: result.board.contractGraph
22967
- } : fail("Task not found.");
22968
- }
22969
- case "link_contract_nodes": {
22970
- if (!input.boardId || !input.fromNodeId || !input.toNodeId || !input.contractEdgeType) {
22971
- return fail(
22972
- "link_contract_nodes requires boardId, fromNodeId, toNodeId, and contractEdgeType."
22973
- );
22974
- }
22975
- const result = await addContractEdge(projectRoot, input.boardId, {
22976
- from: input.fromNodeId,
22977
- to: input.toNodeId,
22978
- type: input.contractEdgeType,
22979
- ...input.contractEdgeId ? { id: input.contractEdgeId } : {},
22980
- ...input.contractEnforcement ? { enforcement: input.contractEnforcement } : {},
22981
- ...input.contractRationale ? { rationale: input.contractRationale } : {},
22982
- ...input.author ? { createdBy: input.author } : {}
22983
- });
22984
- return result ? {
22985
- ...okBoard(result.board, "Contract edge added."),
22986
- contractGraph: result.board.contractGraph
22987
- } : fail("Board not found.");
22988
- }
22989
- case "remove_contract_node": {
22990
- if (!input.boardId || !input.contractNodeId) {
22991
- return fail("remove_contract_node requires boardId and contractNodeId.");
22992
- }
22993
- const current = await getContractGraph(projectRoot, input.boardId);
22994
- const node = current?.graph?.nodes.find((candidate) => candidate.id === input.contractNodeId);
22995
- if (current?.graph?.enforcement === "strict" && node?.enforcement === "blocking") {
22996
- return fail("The autonomous kanban tool may not remove a blocking strict contract node.");
22997
- }
22998
- const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
22999
- return board ? okBoard(board, "Contract node removed.") : fail("Contract node not found.");
23000
- }
23001
- case "remove_contract_edge": {
23002
- if (!input.boardId || !input.contractEdgeId) {
23003
- return fail("remove_contract_edge requires boardId and contractEdgeId.");
23004
- }
23005
- const current = await getContractGraph(projectRoot, input.boardId);
23006
- const edge = current?.graph?.edges.find((candidate) => candidate.id === input.contractEdgeId);
23007
- if (current?.graph?.enforcement === "strict" && edge?.enforcement === "blocking") {
23008
- return fail("The autonomous kanban tool may not remove a blocking strict contract edge.");
23009
- }
23010
- const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
23011
- return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
23012
- }
23013
- case "evaluate_contract_graph": {
23014
- if (!input.boardId || !input.taskId) {
23015
- return fail("evaluate_contract_graph requires boardId and taskId.");
23016
- }
23017
- const result = await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId);
23018
- return result ? {
23019
- ok: result.evaluation.allowed,
23020
- message: result.evaluation.allowed ? "Contract graph is closed." : `Contract graph has ${result.evaluation.issues.length} unresolved issue(s).`,
23021
- board: result.board,
23022
- contractGraph: result.board.contractGraph,
23023
- contractEvaluation: result.evaluation
23024
- } : fail("Task not found.");
23025
- }
23026
23028
  case "add_dependency": {
23027
23029
  if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
23028
23030
  return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
@@ -23075,8 +23077,9 @@ async function handleKanbanDetailAction(projectRoot, input) {
23075
23077
  }
23076
23078
  const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
23077
23079
  description: input.checkDescription,
23078
- type: "manual",
23079
- status: input.checkStatus
23080
+ type: input.checkType ?? "manual",
23081
+ status: input.checkStatus,
23082
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
23080
23083
  });
23081
23084
  return board ? okBoard(board, "Check added.") : fail("Task not found.");
23082
23085
  }
@@ -23091,11 +23094,27 @@ async function handleKanbanDetailAction(projectRoot, input) {
23091
23094
  input.checkId,
23092
23095
  {
23093
23096
  ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
23094
- ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
23097
+ ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {},
23098
+ // Promoting an existing manual criterion to an executable one is the
23099
+ // common repair: the card was written before anyone knew the command.
23100
+ ...input.checkType !== void 0 ? { type: input.checkType } : {},
23101
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
23095
23102
  }
23096
23103
  );
23097
23104
  return board ? okBoard(board, "Check updated.") : fail("Check not found.");
23098
23105
  }
23106
+ case "remove_check": {
23107
+ if (!input.boardId || !input.taskId || !input.checkId) {
23108
+ return fail("remove_check requires boardId, taskId, and checkId.");
23109
+ }
23110
+ const board = await removeCheckFromTask(
23111
+ projectRoot,
23112
+ input.boardId,
23113
+ input.taskId,
23114
+ input.checkId
23115
+ );
23116
+ return board ? okBoard(board, "Acceptance criterion removed.") : fail("Check not found on this task.");
23117
+ }
23099
23118
  case "add_note": {
23100
23119
  if (!input.boardId || !input.taskId || !input.note)
23101
23120
  return fail("add_note requires boardId, taskId, and note.");
@@ -23170,14 +23189,25 @@ function taskInput(input) {
23170
23189
  ...input.order !== void 0 ? { order: input.order } : {},
23171
23190
  ...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
23172
23191
  ...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
23192
+ // The system prompt has always told the model it may "set atomic: true"
23193
+ // when creating a composite parent. It could not: the field reached
23194
+ // neither the create input nor the patch, so the instruction described a
23195
+ // capability that did not exist and the attempt was silently dropped.
23196
+ ...input.atomic !== void 0 ? { atomic: input.atomic } : {},
23173
23197
  ...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
23174
23198
  ...input.checkDescription !== void 0 ? {
23175
23199
  successCriteria: [
23176
23200
  {
23177
23201
  id: randomUUID(),
23178
23202
  description: input.checkDescription,
23179
- type: "manual",
23180
- status: input.checkStatus ?? "pending"
23203
+ // `manual` only as the fallback. Hard-coding it here meant every
23204
+ // agent-authored criterion was unverifiable by construction: the
23205
+ // deterministic plugins never matched, the registry passed the
23206
+ // hand-set status straight through, and "verified" collapsed into
23207
+ // "the author ticked its own box".
23208
+ type: input.checkType ?? "manual",
23209
+ status: input.checkStatus ?? "pending",
23210
+ ...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
23181
23211
  }
23182
23212
  ]
23183
23213
  } : {},
@@ -23225,11 +23255,11 @@ function taskInput(input) {
23225
23255
  };
23226
23256
  }
23227
23257
  function mergedDependsOn(input) {
23228
- const ids = [
23258
+ if (input.dependsOn === void 0 && input.dependencyTaskId === void 0) return void 0;
23259
+ return [
23229
23260
  ...input.dependsOn ?? [],
23230
23261
  ...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
23231
23262
  ].filter((id, i, arr) => id && arr.indexOf(id) === i);
23232
- return ids.length > 0 ? ids : void 0;
23233
23263
  }
23234
23264
  function taskPatch(input) {
23235
23265
  return {
@@ -23243,7 +23273,15 @@ function taskPatch(input) {
23243
23273
  status: input.status,
23244
23274
  labels: input.labels,
23245
23275
  assignedAgent: input.agentId,
23246
- ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
23276
+ ...mergedDependsOn(input) !== void 0 ? { dependsOn: mergedDependsOn(input) } : {},
23277
+ // `atomic` and `childTaskIds` are the composite-parent contract, and the
23278
+ // managed gate reads both: an `atomic` parent may not move forward without
23279
+ // children, and may not reach Done until every child is completed. The
23280
+ // manager has always accepted both on a patch; only this surface withheld
23281
+ // them, so `split_atomic` was a one-way door — delete the children and the
23282
+ // parent was stranded with no way to declare itself a leaf again.
23283
+ ...input.atomic !== void 0 ? { atomic: input.atomic } : {},
23284
+ ...input.childTaskIds !== void 0 ? { childTaskIds: input.childTaskIds } : {},
23247
23285
  ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
23248
23286
  ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
23249
23287
  };
@@ -23303,8 +23341,8 @@ function assignmentForTaskCreate(input) {
23303
23341
  }
23304
23342
 
23305
23343
  // src/kanban-tool-schema.ts
23306
- 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.";
23307
- 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.";
23344
+ 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.";
23345
+ 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.';
23308
23346
  var KANBAN_INPUT_SCHEMA = {
23309
23347
  type: "object",
23310
23348
  properties: {
@@ -23317,6 +23355,7 @@ var KANBAN_INPUT_SCHEMA = {
23317
23355
  "duplicate_board",
23318
23356
  "update_board",
23319
23357
  "adopt_managed_lifecycle",
23358
+ "release_managed_lifecycle",
23320
23359
  "delete_board",
23321
23360
  "generate_board",
23322
23361
  "export_markdown",
@@ -23328,9 +23367,6 @@ var KANBAN_INPUT_SCHEMA = {
23328
23367
  "ready_tasks",
23329
23368
  "snapshot",
23330
23369
  "workbench",
23331
- "add_column",
23332
- "update_column",
23333
- "delete_column",
23334
23370
  "add_task",
23335
23371
  "split_task",
23336
23372
  "merge_tasks",
@@ -23345,13 +23381,6 @@ var KANBAN_INPUT_SCHEMA = {
23345
23381
  "delete_task",
23346
23382
  "set_chain",
23347
23383
  "get_chain",
23348
- "get_contract_graph",
23349
- "configure_contract_graph",
23350
- "upsert_contract_node",
23351
- "link_contract_nodes",
23352
- "remove_contract_node",
23353
- "remove_contract_edge",
23354
- "evaluate_contract_graph",
23355
23384
  "claim_task",
23356
23385
  "release_task",
23357
23386
  "assign_task",
@@ -23365,49 +23394,27 @@ var KANBAN_INPUT_SCHEMA = {
23365
23394
  "update_goal_metric",
23366
23395
  "add_check",
23367
23396
  "update_check",
23397
+ "remove_check",
23368
23398
  "add_note",
23369
23399
  "add_link",
23370
23400
  "verify_completion",
23371
23401
  "split_atomic",
23372
23402
  "assess_atomicity",
23373
- "propose_decomposition"
23403
+ "propose_decomposition",
23404
+ "get_contract_graph",
23405
+ "configure_contract_graph",
23406
+ "upsert_contract_node",
23407
+ "remove_contract_node",
23408
+ "add_contract_edge",
23409
+ "remove_contract_edge"
23374
23410
  ]
23375
23411
  },
23376
23412
  boardId: { type: "string" },
23377
23413
  taskId: { type: "string" },
23378
23414
  taskIds: { type: "array", items: { type: "string" } },
23379
23415
  chainId: { type: "string" },
23380
- contractNodeId: { type: "string" },
23381
- contractNodeKind: {
23382
- type: "string",
23383
- enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"]
23384
- },
23385
- contractNodeState: {
23386
- type: "string",
23387
- enum: ["unknown", "active", "satisfied", "violated", "resolved"]
23388
- },
23389
- contractEnforcement: {
23390
- type: "string",
23391
- enum: ["blocking", "advisory", "informational"]
23392
- },
23393
- contractGraphEnforcement: { type: "string", enum: ["off", "advisory", "strict"] },
23394
- contractEdgeId: { type: "string" },
23395
- contractEdgeType: {
23396
- type: "string",
23397
- enum: [
23398
- "targets",
23399
- "affects",
23400
- "must_preserve",
23401
- "exposes",
23402
- "verified_by",
23403
- "conflicts_with",
23404
- "derived_from",
23405
- "relates_to"
23406
- ]
23407
- },
23408
23416
  fromNodeId: { type: "string" },
23409
23417
  toNodeId: { type: "string" },
23410
- contractRationale: { type: "string" },
23411
23418
  baseline: { oneOf: [{ type: "string" }, { type: "number" }] },
23412
23419
  threshold: { oneOf: [{ type: "string" }, { type: "number" }] },
23413
23420
  columnId: { type: "string" },
@@ -23491,7 +23498,20 @@ var KANBAN_INPUT_SCHEMA = {
23491
23498
  costCeilingUsd: { type: "number" },
23492
23499
  retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
23493
23500
  lastFailureKind: { type: "string" },
23494
- dependsOn: { type: "array", items: { type: "string" } },
23501
+ dependsOn: {
23502
+ type: "array",
23503
+ items: { type: "string" },
23504
+ 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."
23505
+ },
23506
+ atomic: {
23507
+ type: "boolean",
23508
+ description: "Composite parent (true) or executable leaf (false). Set false to make a stranded parent a leaf again after its children were dropped."
23509
+ },
23510
+ childTaskIds: {
23511
+ type: "array",
23512
+ items: { type: "string" },
23513
+ description: "Children of a composite parent. On update_task an explicit empty array detaches them all."
23514
+ },
23495
23515
  estimatedHours: { type: "number" },
23496
23516
  actualHours: { type: "number" },
23497
23517
  taskGraph: { type: "object" },
@@ -23525,6 +23545,74 @@ var KANBAN_INPUT_SCHEMA = {
23525
23545
  checkId: { type: "string" },
23526
23546
  checkDescription: { type: "string" },
23527
23547
  checkStatus: { type: "string", enum: ["pending", "passed", "failed", "skipped"] },
23548
+ checkType: {
23549
+ type: "string",
23550
+ // Only types a verifier can actually execute. `manual` is the default and
23551
+ // means a human or agent asserts the status by hand. The rest are run by
23552
+ // `verify_completion` against the default deterministic registry. Types
23553
+ // with no plugin in that registry (`auto`, `review`, `agent`, `council`)
23554
+ // are deliberately omitted: offering them would produce criteria that
23555
+ // silently report `skipped — no verifier plugin registered`.
23556
+ enum: ["manual", "command", "test", "file_exists", "file_matches", "git_diff", "metric"],
23557
+ 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.'
23558
+ },
23559
+ checkNotes: {
23560
+ type: "string",
23561
+ 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"}.'
23562
+ },
23563
+ // ── Contract map ───────────────────────────────────────────────────
23564
+ // The card contract: what this work targets, what it must not break, what
23565
+ // it risks, and what verifies it. Advisory by default — the readiness gate
23566
+ // deliberately does not require map structure, so a map is an operator
23567
+ // review aid, not work the model must complete before implementing.
23568
+ contractEnforcement: {
23569
+ type: "string",
23570
+ enum: ["off", "advisory", "strict"],
23571
+ description: "Board-level contract map enforcement. Default when first configured: advisory."
23572
+ },
23573
+ contractNodeId: { type: "string" },
23574
+ contractNodeKind: {
23575
+ type: "string",
23576
+ enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"],
23577
+ 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."
23578
+ },
23579
+ contractNodeTitle: { type: "string" },
23580
+ contractNodeDescription: { type: "string" },
23581
+ contractNodeState: {
23582
+ type: "string",
23583
+ enum: ["unknown", "active", "satisfied", "violated", "waived", "resolved"]
23584
+ },
23585
+ contractNodeEnforcement: {
23586
+ type: "string",
23587
+ enum: ["blocking", "advisory", "informational"]
23588
+ },
23589
+ /** Bind a node to an acceptance criterion or goal metric already on the task. */
23590
+ contractCheckId: { type: "string" },
23591
+ contractMetricId: { type: "string" },
23592
+ contractWaiverReason: {
23593
+ type: "string",
23594
+ description: 'Required, with an actor, when contractNodeState is "waived".'
23595
+ },
23596
+ contractEdgeId: { type: "string" },
23597
+ contractEdgeFrom: {
23598
+ type: "string",
23599
+ description: 'A contract node id, or a task id (bare or "task:<id>") for the card endpoint.'
23600
+ },
23601
+ contractEdgeTo: { type: "string" },
23602
+ contractEdgeType: {
23603
+ type: "string",
23604
+ enum: [
23605
+ "targets",
23606
+ "affects",
23607
+ "must_preserve",
23608
+ "exposes",
23609
+ "verified_by",
23610
+ "conflicts_with",
23611
+ "derived_from",
23612
+ "relates_to"
23613
+ ]
23614
+ },
23615
+ contractEdgeRationale: { type: "string" },
23528
23616
  note: { type: "string" },
23529
23617
  author: { type: "string" },
23530
23618
  url: { type: "string" },
@@ -23577,12 +23665,17 @@ import {
23577
23665
  mutateTasks
23578
23666
  } from "@wrongstack/core/storage";
23579
23667
  import { deserializeTaskGraph } from "@wrongstack/core/tasking";
23580
- import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
23668
+ import { formatTodosForModel, resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
23581
23669
  import {
23582
23670
  bridgeKanbanSupervisor,
23671
+ compactSessionMirrorBoard,
23583
23672
  createBoard,
23673
+ DEFAULT_COLUMNS,
23584
23674
  getBoard as getBoard2,
23675
+ getDependencyReadinessIssues,
23676
+ getKanbanOrchestrationSnapshot,
23585
23677
  listBoards,
23678
+ pruneSessionBoards,
23586
23679
  removeBoard,
23587
23680
  syncBoardFromTaskGraph,
23588
23681
  touchKanbanPresence as touchKanbanPresence2,
@@ -23590,16 +23683,14 @@ import {
23590
23683
  } from "@wrongstack/kanban";
23591
23684
  var SESSION_BOARD_TAG = "session-work";
23592
23685
  var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
23593
- var SESSION_KANBAN_COLUMNS = [
23594
- { id: "todo", title: "Todo", order: 0, wipLimit: 0, color: "#2563eb" },
23595
- { id: "in-progress", title: "Running", order: 1, wipLimit: 1, color: "#d97706" },
23596
- { id: "review", title: "Preview", order: 2, wipLimit: 0, color: "#7c3aed" },
23597
- { id: "done", title: "Done", order: 3, wipLimit: 0, color: "#16a34a" }
23598
- ];
23686
+ var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
23687
+ ...column
23688
+ }));
23599
23689
  var boardQueue = /* @__PURE__ */ new Map();
23600
23690
  var boardEnsures = /* @__PURE__ */ new Map();
23601
23691
  var pendingMirrors = /* @__PURE__ */ new Map();
23602
23692
  var activeMirrors = /* @__PURE__ */ new Set();
23693
+ var mirrorFailures = /* @__PURE__ */ new Map();
23603
23694
  var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
23604
23695
  function boardKey(projectRoot, sessionId) {
23605
23696
  return `${projectRoot}\0${sessionId}`;
@@ -23607,6 +23698,33 @@ function boardKey(projectRoot, sessionId) {
23607
23698
  function mirrorKey(projectRoot, sessionId, sourceSystem) {
23608
23699
  return `${boardKey(projectRoot, sessionId)}\0${sourceSystem}`;
23609
23700
  }
23701
+ function completedReconciliationGraph(latest, candidates) {
23702
+ const latestNodeIds = new Set(latest.nodes.map((node) => node.id));
23703
+ const carriedNodeIds = /* @__PURE__ */ new Set();
23704
+ const completedNodes = candidates.flatMap(
23705
+ (candidate) => candidate.nodes.filter((node) => {
23706
+ if (node.status !== "completed" || latestNodeIds.has(node.id) || carriedNodeIds.has(node.id)) {
23707
+ return false;
23708
+ }
23709
+ carriedNodeIds.add(node.id);
23710
+ return true;
23711
+ })
23712
+ );
23713
+ if (completedNodes.length === 0) return void 0;
23714
+ const carriedRequirements = completedNodes.flatMap(
23715
+ (node) => node.specRequirementId ? [node.specRequirementId] : []
23716
+ );
23717
+ return {
23718
+ ...latest,
23719
+ nodes: [...latest.nodes, ...completedNodes],
23720
+ rootNodes: [.../* @__PURE__ */ new Set([...latest.rootNodes, ...completedNodes.map((node) => node.id)])],
23721
+ ...latest.requiredRequirementIds ? {
23722
+ requiredRequirementIds: [
23723
+ .../* @__PURE__ */ new Set([...latest.requiredRequirementIds, ...carriedRequirements])
23724
+ ]
23725
+ } : {}
23726
+ };
23727
+ }
23610
23728
  function sessionTag(sessionId) {
23611
23729
  return `session:${sessionId}`;
23612
23730
  }
@@ -23685,16 +23803,44 @@ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
23685
23803
  sourceSystem,
23686
23804
  tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
23687
23805
  archiveMissingTasks: true,
23688
- includeCompletedTasks: true
23806
+ includeCompletedTasks: true,
23807
+ // The scope ledger stays declared and accurate, but it may not veto a
23808
+ // projection. A session mirror reflects a tactical list that shrinks by
23809
+ // design, and refusing the sync never protected the removed row — it
23810
+ // froze the entire board, permanently, because the stored scope then
23811
+ // outlived every later snapshot (`session-kanban.mirror-failed`).
23812
+ // Nothing is lost by shrinking here: `archiveMissingTasks` keeps the
23813
+ // removed card on the board as `archived`, the reconciliation pass
23814
+ // first walks vanished completed rows to Done, and the session journal
23815
+ // remains the durable record.
23816
+ allowRequirementScopeShrink: true
23689
23817
  }
23690
23818
  );
23691
- return result?.board ?? null;
23819
+ if (!result) return null;
23820
+ const compacted = await compactSessionMirrorBoard(projectRoot, board.id);
23821
+ if (compacted?.removedTaskIds.length) {
23822
+ return await getBoard2(projectRoot, board.id) ?? result.board;
23823
+ }
23824
+ return result.board;
23692
23825
  });
23693
23826
  }
23694
23827
  function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
23695
23828
  if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return;
23696
23829
  const key = mirrorKey(projectRoot, sessionId, sourceSystem);
23697
- pendingMirrors.set(key, { projectRoot, sessionId, graph, sourceSystem });
23830
+ const previous = pendingMirrors.get(key);
23831
+ const reconciliationGraph = previous ? completedReconciliationGraph(
23832
+ graph,
23833
+ [previous.reconciliationGraph, previous.graph].filter(
23834
+ (candidate) => candidate !== void 0
23835
+ )
23836
+ ) : void 0;
23837
+ pendingMirrors.set(key, {
23838
+ projectRoot,
23839
+ sessionId,
23840
+ graph,
23841
+ ...reconciliationGraph ? { reconciliationGraph } : {},
23842
+ sourceSystem
23843
+ });
23698
23844
  if (activeMirrors.has(key)) return;
23699
23845
  activeMirrors.add(key);
23700
23846
  void (async () => {
@@ -23704,20 +23850,34 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
23704
23850
  if (!pending2) break;
23705
23851
  pendingMirrors.delete(key);
23706
23852
  try {
23853
+ if (pending2.reconciliationGraph) {
23854
+ await projectGraph(
23855
+ pending2.projectRoot,
23856
+ pending2.sessionId,
23857
+ pending2.reconciliationGraph,
23858
+ pending2.sourceSystem
23859
+ );
23860
+ }
23707
23861
  await projectGraph(
23708
23862
  pending2.projectRoot,
23709
23863
  pending2.sessionId,
23710
23864
  pending2.graph,
23711
23865
  pending2.sourceSystem
23712
23866
  );
23867
+ mirrorFailures.delete(boardKey(pending2.projectRoot, pending2.sessionId));
23713
23868
  } catch (error) {
23869
+ const message = error instanceof Error ? error.message : String(error);
23870
+ mirrorFailures.set(boardKey(pending2.projectRoot, pending2.sessionId), {
23871
+ message,
23872
+ sourceSystem: pending2.sourceSystem
23873
+ });
23714
23874
  console.warn(
23715
23875
  JSON.stringify({
23716
23876
  level: "warn",
23717
23877
  event: "session-kanban.mirror-failed",
23718
23878
  sessionId: pending2.sessionId,
23719
23879
  sourceSystem: pending2.sourceSystem,
23720
- message: error instanceof Error ? error.message : String(error),
23880
+ message,
23721
23881
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
23722
23882
  })
23723
23883
  );
@@ -23738,6 +23898,14 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
23738
23898
  }
23739
23899
  })();
23740
23900
  }
23901
+ function takeSessionMirrorFailure(projectRoot, sessionId) {
23902
+ if (!projectRoot || !sessionId) return void 0;
23903
+ const key = boardKey(projectRoot, sessionId);
23904
+ const failure = mirrorFailures.get(key);
23905
+ if (!failure) return void 0;
23906
+ mirrorFailures.delete(key);
23907
+ return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
23908
+ }
23741
23909
  function todoListToSerializedGraph(todos, sessionId) {
23742
23910
  const graphId = `todo:${sessionId}`;
23743
23911
  const nodes = todos.map((todo, index) => ({
@@ -23869,11 +24037,11 @@ function broadcastTodoUpdate(context, todos) {
23869
24037
  });
23870
24038
  }
23871
24039
  function notifyTodoUpdate(context, todos) {
23872
- const summary = todos.length ? todos.map((todo) => `- [${todo.status}] ${todo.content} (${todo.id})`).join("\n") : "- No active todos remain.";
24040
+ const summary = formatTodosForModel(todos);
23873
24041
  const text = `[KANBAN TODO UPDATE]
23874
24042
  Another Kanban agent reassessed the shared board. The canonical todo list is now:
23875
24043
  ${summary}
23876
- Reassess your current plan before continuing; do not rely on the initial todo snapshot.`;
24044
+ 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.`;
23877
24045
  const state = context.state;
23878
24046
  if (typeof state.appendBlockToLastUserMessage === "function") {
23879
24047
  if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
@@ -23912,26 +24080,68 @@ function todoStatus(task) {
23912
24080
  if (status === "in_progress" || status === "review") return "in_progress";
23913
24081
  return "pending";
23914
24082
  }
23915
- function sessionTodoFromTask(task, boardId) {
24083
+ function sessionTodoFromTask(task, board) {
24084
+ const blockedBy = board ? blockingTitles(board, task) : [];
23916
24085
  return {
23917
24086
  id: task.origin?.taskId ?? task.id,
23918
24087
  content: task.title,
23919
24088
  status: todoStatus(task),
23920
- kanbanBoardId: boardId,
23921
- kanbanTaskId: task.id,
23922
- ...task.description ? { activeForm: task.description } : {}
24089
+ ...task.description ? { activeForm: task.description } : {},
24090
+ ...blockedBy.length ? { blockedBy } : {}
23923
24091
  };
23924
24092
  }
23925
- function managedTodoFromTask(task, boardId) {
24093
+ function managedTodoFromTask(task, board) {
23926
24094
  return {
23927
- ...sessionTodoFromTask(task, boardId),
23928
- status: task.status === "completed" ? "completed" : task.status === "in_progress" ? "in_progress" : "pending"
24095
+ ...sessionTodoFromTask(task, board),
24096
+ kanbanBoardId: board.id,
24097
+ kanbanTaskId: task.id
23929
24098
  };
23930
24099
  }
24100
+ function blockingTitles(board, task) {
24101
+ return getDependencyReadinessIssues(board, task).map((issue) => {
24102
+ const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
24103
+ if (!dependency) return `${issue.dependencyId} (missing)`;
24104
+ return dependency.title;
24105
+ });
24106
+ }
24107
+ var PRIORITY_ORDER = {
24108
+ critical: 0,
24109
+ high: 1,
24110
+ medium: 2,
24111
+ low: 3
24112
+ };
24113
+ function orderTasksForTodos(board, tasks) {
24114
+ const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
24115
+ const baseline = [...tasks].sort(
24116
+ (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)
24117
+ );
24118
+ const included = new Set(baseline.map((task) => task.id));
24119
+ const remaining = new Map(baseline.map((task) => [task.id, task]));
24120
+ const emitted = [];
24121
+ const done = /* @__PURE__ */ new Set();
24122
+ while (remaining.size > 0) {
24123
+ const ready = baseline.filter(
24124
+ (task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
24125
+ (dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
24126
+ )
24127
+ );
24128
+ if (ready.length === 0) break;
24129
+ for (const task of ready) {
24130
+ remaining.delete(task.id);
24131
+ done.add(task.id);
24132
+ emitted.push(task);
24133
+ }
24134
+ }
24135
+ for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
24136
+ return emitted;
24137
+ }
23931
24138
  function sameTodos(left, right) {
23932
24139
  return left.length === right.length && left.every((todo, index) => {
23933
24140
  const candidate = right[index];
23934
- 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;
24141
+ 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,
24142
+ // the rows are otherwise identical and the unblocking would never
24143
+ // reach the model.
24144
+ (candidate.blockedBy ?? []).join("\0") === (todo.blockedBy ?? []).join("\0");
23935
24145
  });
23936
24146
  }
23937
24147
  function applyManagedKanbanBoardToTodos(context, board) {
@@ -23941,11 +24151,12 @@ function applyManagedKanbanBoardToTodos(context, board) {
23941
24151
  if (!activeBoardId2 || board.id !== activeBoardId2 || board.lifecycle?.mode !== "managed") {
23942
24152
  return [...context.todos];
23943
24153
  }
23944
- const projectedTodos = board.tasks.filter(
23945
- (task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
23946
- ).sort(
23947
- (left, right) => left.createdAt.localeCompare(right.createdAt) || left.order - right.order
23948
- ).map((task) => managedTodoFromTask(task, board.id));
24154
+ const projectedTodos = orderTasksForTodos(
24155
+ board,
24156
+ board.tasks.filter(
24157
+ (task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
24158
+ )
24159
+ ).map((task) => managedTodoFromTask(task, board));
23949
24160
  if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
23950
24161
  suppressedTodoMirrors.add(context);
23951
24162
  try {
@@ -23989,8 +24200,14 @@ var kanbanTool = {
23989
24200
  }
23990
24201
  case "create_board": {
23991
24202
  if (!input.title) return fail("create_board requires title.");
24203
+ const existing = (await listBoards2(projectRoot)).filter(
24204
+ (candidate) => (candidate.kind ?? "project") === "project"
24205
+ );
23992
24206
  const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
23993
- return { ok: true, message: `Board created: ${board.title}`, board };
24207
+ 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(
24208
+ ", "
24209
+ )}${existing.length > 3 ? ", \u2026" : ""}. If this work belongs to one of them, add_task there instead and delete this board.` : "";
24210
+ return { ok: true, message: `Board created: ${board.title}.${note}`, board };
23994
24211
  }
23995
24212
  case "update_board": {
23996
24213
  if (!input.boardId) return fail("update_board requires boardId.");
@@ -24019,6 +24236,20 @@ var kanbanTool = {
24019
24236
  });
24020
24237
  return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
24021
24238
  }
24239
+ // Adoption used to be a one-way door: the strict lifecycle carries
24240
+ // acceptance-criteria, verification-report, review-evidence and
24241
+ // one-stage-at-a-time gates, and nothing on the tool surface could
24242
+ // undo it, so a board adopted once kept its ceremony forever. The
24243
+ // gates are worth having where a fleet is supervised; they are not
24244
+ // worth being unable to leave. Cards and columns are untouched.
24245
+ case "release_managed_lifecycle": {
24246
+ if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
24247
+ const board = await updateBoard2(projectRoot, input.boardId, { lifecycle: null });
24248
+ return board ? okBoard(
24249
+ board,
24250
+ "Managed lifecycle released; the board now tracks work without strict gates."
24251
+ ) : fail("Board not found.");
24252
+ }
24022
24253
  case "duplicate_board": {
24023
24254
  if (!input.boardId) return fail("duplicate_board requires boardId.");
24024
24255
  const board = await duplicateBoard(
@@ -24038,8 +24269,7 @@ var kanbanTool = {
24038
24269
  const boardInput = createBoardFromText({
24039
24270
  description: input.description,
24040
24271
  ...input.title !== void 0 ? { title: input.title } : {},
24041
- ...input.context !== void 0 ? { context: input.context } : {},
24042
- ...input.columns !== void 0 ? { columns: input.columns } : {}
24272
+ ...input.context !== void 0 ? { context: input.context } : {}
24043
24273
  });
24044
24274
  const board = await createBoard2(projectRoot, boardInput);
24045
24275
  for (const taskInput2 of parseLinesIntoTasks(
@@ -24177,7 +24407,7 @@ var kanbanTool = {
24177
24407
  return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
24178
24408
  }
24179
24409
  case "snapshot": {
24180
- const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
24410
+ const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
24181
24411
  query: input.query,
24182
24412
  boardId: input.boardId,
24183
24413
  assignedAgent: input.agentId,
@@ -24192,33 +24422,6 @@ var kanbanTool = {
24192
24422
  snapshot
24193
24423
  };
24194
24424
  }
24195
- case "add_column": {
24196
- if (!input.boardId || !input.title)
24197
- return fail("add_column requires boardId and title.");
24198
- const result2 = await addColumn(projectRoot, input.boardId, {
24199
- title: input.title,
24200
- ...input.description !== void 0 ? { description: input.description } : {}
24201
- });
24202
- return result2 ? okBoard(result2.board, "Column added.") : fail("Board not found.");
24203
- }
24204
- case "update_column": {
24205
- if (!input.boardId || !input.columnId)
24206
- return fail("update_column requires boardId and columnId.");
24207
- const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
24208
- ...input.title !== void 0 ? { title: input.title } : {},
24209
- ...input.description !== void 0 ? { description: input.description } : {},
24210
- ...input.order !== void 0 ? { order: input.order } : {}
24211
- });
24212
- return board ? okBoard(board, "Column updated.") : fail("Column not found.");
24213
- }
24214
- case "delete_column": {
24215
- if (!input.boardId || !input.columnId)
24216
- return fail("delete_column requires boardId and columnId.");
24217
- const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
24218
- moveTasksToColumnId: input.moveTasksToColumnId
24219
- });
24220
- return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
24221
- }
24222
24425
  case "add_task": {
24223
24426
  if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
24224
24427
  const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
@@ -24300,6 +24503,32 @@ var kanbanTool = {
24300
24503
  `Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
24301
24504
  );
24302
24505
  }
24506
+ if (board.lifecycle?.mode !== "managed") {
24507
+ const now = /* @__PURE__ */ new Date();
24508
+ const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
24509
+ status: "running",
24510
+ agentId: input.agentId ?? input.author,
24511
+ leaseId: input.leaseId ?? randomUUID2(),
24512
+ claimedAt: input.claimedAt ?? now.toISOString(),
24513
+ heartbeatAt: input.heartbeatAt ?? now.toISOString(),
24514
+ leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + 15 * 6e4).toISOString(),
24515
+ attempt: input.attempt ?? 1,
24516
+ maxAttempts: input.maxAttempts ?? 3
24517
+ });
24518
+ if (!assigned) return fail("Task assignment could not be started.");
24519
+ const started = await updateTask2(projectRoot, board.id, task.id, {
24520
+ status: "in_progress"
24521
+ });
24522
+ const current = started ?? assigned;
24523
+ const claimed = task;
24524
+ const currentTask = current.tasks.find((candidate) => candidate.id === claimed.id) ?? claimed;
24525
+ ctx.setCurrentKanbanTask?.(currentTask.id, current.id);
24526
+ return okTask(
24527
+ current,
24528
+ currentTask,
24529
+ "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."
24530
+ );
24531
+ }
24303
24532
  let stage = task.lifecycle?.currentStage;
24304
24533
  if (stage === "backlog") {
24305
24534
  const moved = await transitionTask(projectRoot, board.id, task.id, {
@@ -24440,6 +24669,9 @@ var kanbanTool = {
24440
24669
  if (!input.boardId || !input.taskId)
24441
24670
  return fail("delete_task requires boardId and taskId.");
24442
24671
  const board = await removeTask(projectRoot, input.boardId, input.taskId);
24672
+ if (board && ctx.currentKanbanTaskId === input.taskId) {
24673
+ ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
24674
+ }
24443
24675
  return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
24444
24676
  }
24445
24677
  case "set_chain": {
@@ -24572,7 +24804,7 @@ var kanbanTool = {
24572
24804
  });
24573
24805
  } catch (err) {
24574
24806
  lifecycleWarnings.push(
24575
- `Lifecycle transition to Running deferred: ${err instanceof Error ? err.message : String(err)}`
24807
+ `Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
24576
24808
  );
24577
24809
  }
24578
24810
  }
@@ -24596,7 +24828,7 @@ var kanbanTool = {
24596
24828
  });
24597
24829
  } catch (err) {
24598
24830
  lifecycleWarnings.push(
24599
- `Lifecycle transition to Review failed: ${err instanceof Error ? err.message : String(err)}`
24831
+ `Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
24600
24832
  );
24601
24833
  }
24602
24834
  if (transitionResult) {
@@ -24616,7 +24848,11 @@ var kanbanTool = {
24616
24848
  successCriteria: verResult.task.successCriteria
24617
24849
  });
24618
24850
  const verdict = verResult.report.verdict;
24619
- if (verdict === "passed") {
24851
+ if (verdict === "passed" && !resolveAutoAccept(board)) {
24852
+ lifecycleWarnings.push(
24853
+ "Verification passed, but this board does not auto-accept. The card is in Review awaiting an explicit transition_task to done."
24854
+ );
24855
+ } else if (verdict === "passed") {
24620
24856
  try {
24621
24857
  const doneResult = await transitionTask(
24622
24858
  projectRoot,
@@ -24734,11 +24970,34 @@ var kanbanTool = {
24734
24970
  });
24735
24971
  return {
24736
24972
  ok: true,
24737
- message: `Counts: ready=${health.counts.ready}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
24973
+ message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
24738
24974
  queueHealth: health
24739
24975
  };
24740
24976
  }
24977
+ // Not every action is handled above. These are dispatched from here,
24978
+ // and the split has already cost real time: an agent that read this
24979
+ // file concluded `add_check` / `update_check` did not exist, wrote
24980
+ // that on a card, and spent a session trying to satisfy a gate it
24981
+ // already had the tool to clear. Keep this index in step with the
24982
+ // handlers.
24983
+ //
24984
+ // kanban-detail-actions.ts workbench · add_dependency ·
24985
+ // add_goal_metric · update_goal_metric · add_check ·
24986
+ // update_check · add_note · add_link · split_atomic
24987
+ // kanban-decomposition-actions.ts verify_completion ·
24988
+ // assess_atomicity · propose_decomposition
24989
+ // kanban-contract-actions.ts get_contract_graph ·
24990
+ // configure_contract_graph · upsert_contract_node ·
24991
+ // remove_contract_node · add_contract_edge · remove_contract_edge
24741
24992
  default:
24993
+ {
24994
+ const contractResult = await handleKanbanContractAction(
24995
+ projectRoot,
24996
+ input,
24997
+ input.author ?? input.agentId
24998
+ );
24999
+ if (contractResult !== void 0) return contractResult;
25000
+ }
24742
25001
  {
24743
25002
  const detailResult = await handleKanbanDetailAction(projectRoot, input);
24744
25003
  if (detailResult !== void 0) return detailResult;
@@ -24748,7 +25007,7 @@ var kanbanTool = {
24748
25007
  })();
24749
25008
  return withPresence(result);
24750
25009
  } catch (err) {
24751
- return fail(err instanceof Error ? err.message : String(err));
25010
+ return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
24752
25011
  }
24753
25012
  }
24754
25013
  };
@@ -25601,7 +25860,7 @@ import {
25601
25860
  saveTasks,
25602
25861
  setPlanItemStatus
25603
25862
  } from "@wrongstack/core/storage";
25604
- import { getBoard as getBoard4 } from "@wrongstack/kanban";
25863
+ import { addTask as addTask2, getBoard as getBoard4 } from "@wrongstack/kanban";
25605
25864
  function normalizedTitle(value) {
25606
25865
  return value.trim().toLocaleLowerCase().replace(/\s+/g, " ");
25607
25866
  }
@@ -25629,11 +25888,51 @@ function bindTodosToBoard(items, previous, board) {
25629
25888
  available.find((task2) => !used.has(task2.id) && normalizedTitle(task2.title) === title)
25630
25889
  ];
25631
25890
  const task = candidates.find((candidate) => candidate && !used.has(candidate.id));
25632
- if (!task) return { ...item };
25891
+ if (!task) {
25892
+ const { blockedBy: _discarded, ...rest } = item;
25893
+ return { ...rest };
25894
+ }
25633
25895
  used.add(task.id);
25634
- return { ...item, kanbanBoardId: board.id, kanbanTaskId: task.id };
25896
+ const blockedBy = blockingTitles(board, task);
25897
+ return {
25898
+ ...item,
25899
+ kanbanBoardId: board.id,
25900
+ kanbanTaskId: task.id,
25901
+ ...blockedBy.length ? { blockedBy } : { blockedBy: void 0 }
25902
+ };
25635
25903
  });
25636
25904
  }
25905
+ function demoteBlockedInProgress(items, warnings) {
25906
+ return items.map((item) => {
25907
+ if (item.status !== "in_progress" || !item.blockedBy?.length) return item;
25908
+ warnings.push(
25909
+ `"${item.content}" cannot start yet \u2014 it waits on: ${item.blockedBy.join("; ")}. Kept as pending; complete the blocking work first.`
25910
+ );
25911
+ return { ...item, status: "pending" };
25912
+ });
25913
+ }
25914
+ async function createMissingManagedCards(items, board, ctx, warnings) {
25915
+ const created = /* @__PURE__ */ new Map();
25916
+ for (const item of items) {
25917
+ if (item.kanbanBoardId === board.id && item.kanbanTaskId) continue;
25918
+ try {
25919
+ const result = await addTask2(ctx.projectRoot, board.id, {
25920
+ title: item.content,
25921
+ description: item.activeForm?.trim() || `Added from the session todo list: ${item.content}`
25922
+ });
25923
+ if (!result) {
25924
+ warnings.push(`Could not open a Kanban card for "${item.content}": board not found.`);
25925
+ continue;
25926
+ }
25927
+ created.set(item.id, result.task.id);
25928
+ } catch (error) {
25929
+ warnings.push(
25930
+ `Could not open a Kanban card for "${item.content}": ${error instanceof Error ? error.message : String(error)}`
25931
+ );
25932
+ }
25933
+ }
25934
+ return created;
25935
+ }
25637
25936
  async function synchronizeManagedKanban(items, board, ctx, signal) {
25638
25937
  let synced = 0;
25639
25938
  const warnings = [];
@@ -25671,6 +25970,16 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
25671
25970
  transitionComment: `Todo returned to queue: ${item.content}`
25672
25971
  });
25673
25972
  }
25973
+ for (const item of items) {
25974
+ if (item.status === "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
25975
+ continue;
25976
+ }
25977
+ const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
25978
+ if (task?.status !== "completed") continue;
25979
+ warnings.push(
25980
+ `"${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.`
25981
+ );
25982
+ }
25674
25983
  for (const item of items) {
25675
25984
  if (item.status !== "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
25676
25985
  continue;
@@ -25722,17 +26031,25 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
25722
26031
  const active = items.find(
25723
26032
  (item) => item.status === "in_progress" && item.kanbanBoardId === board.id && Boolean(item.kanbanTaskId)
25724
26033
  );
26034
+ const activeStage = active?.kanbanTaskId ? afterCompletions?.tasks.find((task) => task.id === active.kanbanTaskId)?.lifecycle?.currentStage : void 0;
25725
26035
  if (active?.kanbanTaskId) {
25726
- await execute({
25727
- action: "start_task",
25728
- boardId: board.id,
25729
- taskId: active.kanbanTaskId,
25730
- author: actor,
25731
- agentId: actor,
25732
- transitionComment: `Todo activated: ${active.content}`
25733
- });
26036
+ if (activeStage === "review" || activeStage === "done") {
26037
+ warnings.push(
26038
+ `"${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.")
26039
+ );
26040
+ } else {
26041
+ await execute({
26042
+ action: "start_task",
26043
+ boardId: board.id,
26044
+ taskId: active.kanbanTaskId,
26045
+ author: actor,
26046
+ agentId: actor,
26047
+ transitionComment: `Todo activated: ${active.content}`
26048
+ });
26049
+ }
25734
26050
  }
25735
- if (active?.kanbanTaskId && completionPending) {
26051
+ if (active?.kanbanTaskId && activeStage === "review") {
26052
+ } else if (active?.kanbanTaskId && completionPending) {
25736
26053
  warnings.push(
25737
26054
  "A completed todo is still awaiting acceptance; the next independent Kanban task was started."
25738
26055
  );
@@ -25819,29 +26136,47 @@ var todoTool = {
25819
26136
  }
25820
26137
  }
25821
26138
  const boardId = activeBoardId(items, ctx);
25822
- const board = boardId ? await getBoard4(ctx.projectRoot, boardId) : null;
25823
- const boundItems = board?.lifecycle?.mode === "managed" ? bindTodosToBoard(items, ctx.todos ?? [], board) : items;
26139
+ let board = boardId ? await getBoard4(ctx.projectRoot, boardId) : null;
26140
+ const managed = board?.lifecycle?.mode === "managed";
26141
+ let boundItems = managed && board ? bindTodosToBoard(items, ctx.todos ?? [], board) : items;
26142
+ const creationWarnings = [];
26143
+ if (managed && board) {
26144
+ const managedBoardId = board.id;
26145
+ const created = await createMissingManagedCards(boundItems, board, ctx, creationWarnings);
26146
+ if (created.size > 0) {
26147
+ boundItems = boundItems.map((item) => {
26148
+ const taskId = created.get(item.id);
26149
+ return taskId ? { ...item, kanbanBoardId: managedBoardId, kanbanTaskId: taskId } : item;
26150
+ });
26151
+ board = await getBoard4(ctx.projectRoot, managedBoardId) ?? board;
26152
+ boundItems = bindTodosToBoard(boundItems, ctx.todos ?? [], board);
26153
+ }
26154
+ boundItems = demoteBlockedInProgress(boundItems, creationWarnings);
26155
+ }
25824
26156
  ctx.state.replaceTodos(boundItems);
25825
- const kanbanSync = board?.lifecycle?.mode === "managed" ? await synchronizeManagedKanban(boundItems, board, ctx, call.signal) : { synced: 0, warnings: [] };
25826
- if (board?.lifecycle?.mode === "managed") {
26157
+ const kanbanSync = managed && board ? await synchronizeManagedKanban(boundItems, board, ctx, call.signal) : { synced: 0, warnings: [] };
26158
+ kanbanSync.warnings.unshift(...creationWarnings);
26159
+ if (managed && board) {
25827
26160
  const unresolved = boundItems.filter(
25828
26161
  (item) => item.kanbanBoardId !== board.id || !item.kanbanTaskId
25829
26162
  );
25830
26163
  if (unresolved.length > 0) {
25831
26164
  kanbanSync.warnings.push(
25832
- `${unresolved.length} Todo row(s) did not match a real Kanban task and were not applied. Preserve kanbanBoardId/kanbanTaskId when updating the projection.`
26165
+ `${unresolved.length} Todo row(s) could not be bound to a Kanban task and were not applied. Preserve kanbanBoardId/kanbanTaskId when updating the projection.`
25833
26166
  );
25834
26167
  }
25835
26168
  }
26169
+ const mirrorFailure = takeSessionMirrorFailure(ctx.projectRoot, ctx.session?.id ?? "");
26170
+ if (mirrorFailure) kanbanSync.warnings.push(mirrorFailure);
25836
26171
  let projectedBoard = board;
25837
- if (board?.lifecycle?.mode === "managed") {
26172
+ if (managed && board) {
25838
26173
  const refreshed = await getBoard4(ctx.projectRoot, board.id);
25839
26174
  if (refreshed) {
25840
26175
  projectedBoard = refreshed;
25841
26176
  applyManagedKanbanBoardToTodos(ctx, refreshed);
25842
26177
  }
25843
26178
  }
25844
- if (board?.lifecycle?.mode !== "managed") {
26179
+ if (!managed) {
25845
26180
  mirrorSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? "session");
25846
26181
  }
25847
26182
  const completedPlanIds = /* @__PURE__ */ new Set();
@@ -28877,6 +29212,7 @@ var OPTIONAL_TOOLS = [
28877
29212
  toolHelpTool,
28878
29213
  setWorkingDirTool
28879
29214
  ];
29215
+ var OFF_ONLY_TOOLS = [...browserTools, e2ePlanTool];
28880
29216
  var builtinTools = [
28881
29217
  ...browserTools,
28882
29218
  e2ePlanTool,