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