@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/builtin.d.ts +6 -0
- package/dist/builtin.js +715 -378
- package/dist/codebase-index/codebase-index-tool.d.ts +6 -0
- package/dist/codebase-index/index.js +112 -89
- package/dist/codebase-index/indexer.d.ts +6 -0
- package/dist/codebase-index/project-server.js +112 -89
- package/dist/codebase-index/schema.d.ts +11 -0
- package/dist/codebase-index/worker.js +112 -89
- package/dist/index.d.ts +3 -3
- package/dist/index.js +777 -397
- package/dist/kanban-contract-actions.d.ts +7 -0
- package/dist/kanban-task-inputs.d.ts +15 -2
- package/dist/kanban-tool-schema.d.ts +2 -2
- package/dist/kanban-tool-types.d.ts +32 -11
- package/dist/kanban.js +366 -245
- package/dist/pack.js +714 -378
- package/dist/plan.js +601 -289
- package/dist/read.js +112 -89
- package/dist/session-kanban.d.ts +86 -1
- package/dist/session-kanban.js +216 -46
- package/dist/task.js +601 -289
- package/dist/todo.js +601 -289
- package/dist/tool-tier.js +714 -378
- package/package.json +3 -3
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
|
|
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) =>
|
|
13117
|
+
bm25Rows.forEach((r, i) => {
|
|
13118
|
+
bm25Rank.set(r.id, i);
|
|
13119
|
+
});
|
|
13118
13120
|
const vecRank = /* @__PURE__ */ new Map();
|
|
13119
|
-
vecScores.forEach((r, 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
|
|
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
|
-
|
|
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
|
-
|
|
23180
|
-
|
|
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
|
-
|
|
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 = "
|
|
23307
|
-
var KANBAN_TOOL_USAGE_HINT =
|
|
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: {
|
|
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
|
-
|
|
23595
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
23921
|
-
|
|
23922
|
-
...task.description ? { activeForm: task.description } : {}
|
|
24089
|
+
...task.description ? { activeForm: task.description } : {},
|
|
24090
|
+
...blockedBy.length ? { blockedBy } : {}
|
|
23923
24091
|
};
|
|
23924
24092
|
}
|
|
23925
|
-
function managedTodoFromTask(task,
|
|
24093
|
+
function managedTodoFromTask(task, board) {
|
|
23926
24094
|
return {
|
|
23927
|
-
...sessionTodoFromTask(task,
|
|
23928
|
-
|
|
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 =
|
|
23945
|
-
|
|
23946
|
-
|
|
23947
|
-
|
|
23948
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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)
|
|
25891
|
+
if (!task) {
|
|
25892
|
+
const { blockedBy: _discarded, ...rest } = item;
|
|
25893
|
+
return { ...rest };
|
|
25894
|
+
}
|
|
25633
25895
|
used.add(task.id);
|
|
25634
|
-
|
|
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
|
-
|
|
25727
|
-
|
|
25728
|
-
|
|
25729
|
-
|
|
25730
|
-
|
|
25731
|
-
|
|
25732
|
-
|
|
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 &&
|
|
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
|
-
|
|
25823
|
-
const
|
|
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 =
|
|
25826
|
-
|
|
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)
|
|
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 (
|
|
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 (
|
|
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,
|