@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/builtin.d.ts +6 -0
- package/dist/builtin.js +717 -381
- package/dist/codebase-index/codebase-index-tool.d.ts +6 -0
- package/dist/codebase-index/index.d.ts +1 -0
- package/dist/codebase-index/index.js +114 -90
- package/dist/codebase-index/indexer.d.ts +6 -0
- package/dist/codebase-index/project-server-endpoint.d.ts +1 -2
- package/dist/codebase-index/project-server.js +131 -109
- 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 +793 -402
- 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 +716 -381
- package/dist/plan.js +601 -289
- package/dist/read.js +112 -90
- package/dist/session-kanban.d.ts +111 -1
- package/dist/session-kanban.js +232 -46
- package/dist/task.js +601 -289
- package/dist/todo.js +601 -289
- package/dist/tool-tier.js +716 -381
- package/package.json +4 -3
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
|
|
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) =>
|
|
13116
|
+
bm25Rows.forEach((r, i) => {
|
|
13117
|
+
bm25Rank.set(r.id, i);
|
|
13118
|
+
});
|
|
13118
13119
|
const vecRank = /* @__PURE__ */ new Map();
|
|
13119
|
-
vecScores.forEach((r, 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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
23180
|
-
|
|
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
|
-
|
|
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 = "
|
|
23307
|
-
var KANBAN_TOOL_USAGE_HINT =
|
|
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: {
|
|
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
|
-
|
|
23595
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
23921
|
-
|
|
23922
|
-
...task.description ? { activeForm: task.description } : {}
|
|
24088
|
+
...task.description ? { activeForm: task.description } : {},
|
|
24089
|
+
...blockedBy.length ? { blockedBy } : {}
|
|
23923
24090
|
};
|
|
23924
24091
|
}
|
|
23925
|
-
function managedTodoFromTask(task,
|
|
24092
|
+
function managedTodoFromTask(task, board) {
|
|
23926
24093
|
return {
|
|
23927
|
-
...sessionTodoFromTask(task,
|
|
23928
|
-
|
|
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 =
|
|
23945
|
-
|
|
23946
|
-
|
|
23947
|
-
|
|
23948
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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)
|
|
25890
|
+
if (!task) {
|
|
25891
|
+
const { blockedBy: _discarded, ...rest } = item;
|
|
25892
|
+
return { ...rest };
|
|
25893
|
+
}
|
|
25633
25894
|
used.add(task.id);
|
|
25634
|
-
|
|
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
|
-
|
|
25727
|
-
|
|
25728
|
-
|
|
25729
|
-
|
|
25730
|
-
|
|
25731
|
-
|
|
25732
|
-
|
|
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 &&
|
|
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
|
-
|
|
25823
|
-
const
|
|
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 =
|
|
25826
|
-
|
|
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)
|
|
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 (
|
|
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 (
|
|
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,
|