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