@wrongstack/tools 0.303.0 → 0.305.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtin.d.ts +6 -0
- package/dist/builtin.js +715 -378
- package/dist/codebase-index/codebase-index-tool.d.ts +6 -0
- package/dist/codebase-index/index.js +112 -89
- package/dist/codebase-index/indexer.d.ts +6 -0
- package/dist/codebase-index/project-server.js +112 -89
- package/dist/codebase-index/schema.d.ts +11 -0
- package/dist/codebase-index/worker.js +112 -89
- package/dist/index.d.ts +3 -3
- package/dist/index.js +777 -397
- package/dist/kanban-contract-actions.d.ts +7 -0
- package/dist/kanban-task-inputs.d.ts +15 -2
- package/dist/kanban-tool-schema.d.ts +2 -2
- package/dist/kanban-tool-types.d.ts +32 -11
- package/dist/kanban.js +366 -245
- package/dist/pack.js +714 -378
- package/dist/plan.js +601 -289
- package/dist/read.js +112 -89
- package/dist/session-kanban.d.ts +86 -1
- package/dist/session-kanban.js +216 -46
- package/dist/task.js +601 -289
- package/dist/todo.js +601 -289
- package/dist/tool-tier.js +714 -378
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -12991,7 +12991,7 @@ var IndexStore = class _IndexStore {
|
|
|
12991
12991
|
const ftsSchema = this.stmt(
|
|
12992
12992
|
"SELECT sql FROM sqlite_master WHERE type='table' AND name='symbols_fts'"
|
|
12993
12993
|
).get();
|
|
12994
|
-
if (ftsSchema?.sql
|
|
12994
|
+
if (ftsSchema?.sql?.includes("unicode61")) {
|
|
12995
12995
|
this.db.exec("DROP TABLE IF EXISTS symbols_fts");
|
|
12996
12996
|
}
|
|
12997
12997
|
this.db.exec(SYMBOLS_FTS_SQL);
|
|
@@ -13484,9 +13484,13 @@ var IndexStore = class _IndexStore {
|
|
|
13484
13484
|
sim: cosineSimilarity(queryVec, decodeVector(r.vector))
|
|
13485
13485
|
})).sort((a, b) => b.sim - a.sim);
|
|
13486
13486
|
const bm25Rank = /* @__PURE__ */ new Map();
|
|
13487
|
-
bm25Rows.forEach((r, i) =>
|
|
13487
|
+
bm25Rows.forEach((r, i) => {
|
|
13488
|
+
bm25Rank.set(r.id, i);
|
|
13489
|
+
});
|
|
13488
13490
|
const vecRank = /* @__PURE__ */ new Map();
|
|
13489
|
-
vecScores.forEach((r, i) =>
|
|
13491
|
+
vecScores.forEach((r, i) => {
|
|
13492
|
+
vecRank.set(r.id, i);
|
|
13493
|
+
});
|
|
13490
13494
|
const fused = reciprocalRankFusion(bm25Rank, vecRank, 60);
|
|
13491
13495
|
const fusedScore = new Map(fused);
|
|
13492
13496
|
const sorted = [...bm25Rows].sort(
|
|
@@ -14889,6 +14893,86 @@ import {
|
|
|
14889
14893
|
isFrugalPerf
|
|
14890
14894
|
} from "@wrongstack/core/utils";
|
|
14891
14895
|
|
|
14896
|
+
// src/codebase-index/content-hash.ts
|
|
14897
|
+
var PRIME64_1 = 0x9e3779b185ebca87n;
|
|
14898
|
+
var PRIME64_2 = 0xc2b2ae3d27d4eb4fn;
|
|
14899
|
+
var PRIME64_3 = 0x165667b19e3779f9n;
|
|
14900
|
+
var PRIME64_4 = 0x85ebca77c2b2ae63n;
|
|
14901
|
+
var PRIME64_5 = 0x27d4eb2f165667c5n;
|
|
14902
|
+
var MASK64 = 0xffffffffffffffffn;
|
|
14903
|
+
function mul64(a, b) {
|
|
14904
|
+
return (a & MASK64) * (b & MASK64) & MASK64;
|
|
14905
|
+
}
|
|
14906
|
+
function rotl64(x, n) {
|
|
14907
|
+
const v = x & MASK64;
|
|
14908
|
+
return (v << BigInt(n) | v >> BigInt(64 - n)) & MASK64;
|
|
14909
|
+
}
|
|
14910
|
+
function readU64LE(buf, off) {
|
|
14911
|
+
let v = 0n;
|
|
14912
|
+
for (let i = 7; i >= 0; i--) {
|
|
14913
|
+
v = v << 8n | BigInt(buf[off + i] ?? 0);
|
|
14914
|
+
}
|
|
14915
|
+
return v & MASK64;
|
|
14916
|
+
}
|
|
14917
|
+
function readU32LE(buf, off) {
|
|
14918
|
+
return (BigInt(buf[off] ?? 0) | BigInt(buf[off + 1] ?? 0) << 8n | BigInt(buf[off + 2] ?? 0) << 16n | BigInt(buf[off + 3] ?? 0) << 24n) & MASK64;
|
|
14919
|
+
}
|
|
14920
|
+
function xxh64Round(acc, lane) {
|
|
14921
|
+
return mul64(rotl64(acc + mul64(lane, PRIME64_2) & MASK64, 31), PRIME64_1);
|
|
14922
|
+
}
|
|
14923
|
+
function xxh64MergeRound(acc, val) {
|
|
14924
|
+
return mul64(acc ^ xxh64Round(0n, val), PRIME64_1) + PRIME64_4 & MASK64;
|
|
14925
|
+
}
|
|
14926
|
+
function xxhash64Hex(buf, explicitLen) {
|
|
14927
|
+
const length = explicitLen ?? buf.length;
|
|
14928
|
+
let h;
|
|
14929
|
+
let off = 0;
|
|
14930
|
+
if (length >= 32) {
|
|
14931
|
+
let v1 = PRIME64_1 + PRIME64_2 & MASK64;
|
|
14932
|
+
let v2 = PRIME64_2;
|
|
14933
|
+
let v3 = 0n;
|
|
14934
|
+
let v4 = 0n - PRIME64_1 & MASK64;
|
|
14935
|
+
const end32 = length - 32;
|
|
14936
|
+
while (off <= end32) {
|
|
14937
|
+
v1 = xxh64Round(v1, readU64LE(buf, off));
|
|
14938
|
+
v2 = xxh64Round(v2, readU64LE(buf, off + 8));
|
|
14939
|
+
v3 = xxh64Round(v3, readU64LE(buf, off + 16));
|
|
14940
|
+
v4 = xxh64Round(v4, readU64LE(buf, off + 24));
|
|
14941
|
+
off += 32;
|
|
14942
|
+
}
|
|
14943
|
+
h = rotl64(v1, 1) + rotl64(v2, 7) + rotl64(v3, 12) + rotl64(v4, 18) & MASK64;
|
|
14944
|
+
h = xxh64MergeRound(h, v1);
|
|
14945
|
+
h = xxh64MergeRound(h, v2);
|
|
14946
|
+
h = xxh64MergeRound(h, v3);
|
|
14947
|
+
h = xxh64MergeRound(h, v4);
|
|
14948
|
+
} else {
|
|
14949
|
+
h = PRIME64_5;
|
|
14950
|
+
}
|
|
14951
|
+
h = h + BigInt(length) & MASK64;
|
|
14952
|
+
while (off + 8 <= length) {
|
|
14953
|
+
const k1 = xxh64Round(0n, readU64LE(buf, off));
|
|
14954
|
+
h = mul64(rotl64(h ^ k1, 27), PRIME64_1) + PRIME64_4 & MASK64;
|
|
14955
|
+
off += 8;
|
|
14956
|
+
}
|
|
14957
|
+
if (off + 4 <= length) {
|
|
14958
|
+
h = mul64(rotl64(h ^ mul64(readU32LE(buf, off), PRIME64_1), 23), PRIME64_2) + PRIME64_3 & MASK64;
|
|
14959
|
+
off += 4;
|
|
14960
|
+
}
|
|
14961
|
+
while (off < length) {
|
|
14962
|
+
h = mul64(rotl64(h ^ mul64(BigInt(buf[off] ?? 0), PRIME64_5), 11), PRIME64_1) & MASK64;
|
|
14963
|
+
off += 1;
|
|
14964
|
+
}
|
|
14965
|
+
h = (h ^ h >> 33n) & MASK64;
|
|
14966
|
+
h = mul64(h, PRIME64_2);
|
|
14967
|
+
h = (h ^ h >> 29n) & MASK64;
|
|
14968
|
+
h = mul64(h, PRIME64_3);
|
|
14969
|
+
h = (h ^ h >> 32n) & MASK64;
|
|
14970
|
+
return h.toString(16).padStart(16, "0");
|
|
14971
|
+
}
|
|
14972
|
+
function xxhash64String(content) {
|
|
14973
|
+
return xxhash64Hex(new TextEncoder().encode(content));
|
|
14974
|
+
}
|
|
14975
|
+
|
|
14892
14976
|
// src/codebase-index/gitignore.ts
|
|
14893
14977
|
import * as fs14 from "node:fs/promises";
|
|
14894
14978
|
import * as path18 from "node:path";
|
|
@@ -15616,91 +15700,14 @@ function getParserPool() {
|
|
|
15616
15700
|
return _pool;
|
|
15617
15701
|
}
|
|
15618
15702
|
|
|
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
15703
|
// src/codebase-index/indexer.ts
|
|
15700
15704
|
var YIELD_EVERY_N = 50;
|
|
15701
15705
|
function resolveParallelBatch() {
|
|
15702
15706
|
return indexParallelBatchSize(availableParallelism());
|
|
15703
15707
|
}
|
|
15708
|
+
function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
|
|
15709
|
+
return !isFrugalPerf() && candidateFileCount >= WORKER_POOL_THRESHOLD && parseBatchCount > 1;
|
|
15710
|
+
}
|
|
15704
15711
|
function yieldEventLoop() {
|
|
15705
15712
|
return new Promise((resolve17) => setImmediate(resolve17));
|
|
15706
15713
|
}
|
|
@@ -15877,11 +15884,7 @@ async function resolveProjectRelations(store, projectRoot, opts) {
|
|
|
15877
15884
|
const structure = await detectModuleRoots(projectRoot, indexedFiles);
|
|
15878
15885
|
if (opts.signal?.aborted) return;
|
|
15879
15886
|
store.setFilePackages(assignPackageLabels(structure, indexedFiles));
|
|
15880
|
-
const resolver = new ModuleResolver(
|
|
15881
|
-
structure,
|
|
15882
|
-
indexedFiles,
|
|
15883
|
-
store.getNamespaceDeclarations()
|
|
15884
|
-
);
|
|
15887
|
+
const resolver = new ModuleResolver(structure, indexedFiles, store.getNamespaceDeclarations());
|
|
15885
15888
|
const pending2 = store.getUnresolvedImports(opts.onlyFiles);
|
|
15886
15889
|
const resolutions = [];
|
|
15887
15890
|
for (const entry of pending2) {
|
|
@@ -15906,6 +15909,10 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15906
15909
|
const errors = [];
|
|
15907
15910
|
const langStats = {};
|
|
15908
15911
|
let filesIndexed = 0;
|
|
15912
|
+
let filesParsed = 0;
|
|
15913
|
+
let filesSkipped = 0;
|
|
15914
|
+
let filesEmpty = 0;
|
|
15915
|
+
let filesFailed = 0;
|
|
15909
15916
|
let symbolsIndexed = 0;
|
|
15910
15917
|
const isGitIgnored = await loadGitignoreMatcher(projectRoot);
|
|
15911
15918
|
let files;
|
|
@@ -15947,12 +15954,14 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15947
15954
|
langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
|
|
15948
15955
|
symbolsIndexed += meta.symbolCount;
|
|
15949
15956
|
filesIndexed++;
|
|
15957
|
+
filesSkipped++;
|
|
15950
15958
|
filesPreSkipped++;
|
|
15951
15959
|
return false;
|
|
15952
15960
|
});
|
|
15953
15961
|
if (filesPreSkipped > 0) opts.onProgress?.(filesPreSkipped, totalFilesForProgress);
|
|
15954
15962
|
}
|
|
15955
15963
|
const parallelBatch = resolveParallelBatch();
|
|
15964
|
+
const parserPoolCandidateCount = files.length;
|
|
15956
15965
|
let filesSinceLastYield = 0;
|
|
15957
15966
|
for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
|
|
15958
15967
|
const batchEnd = Math.min(batchStart + parallelBatch, files.length);
|
|
@@ -16045,7 +16054,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16045
16054
|
});
|
|
16046
16055
|
}
|
|
16047
16056
|
if (toParse.length > 0) {
|
|
16048
|
-
let pool = toParse.length
|
|
16057
|
+
let pool = shouldUseParserWorkerPool(parserPoolCandidateCount, toParse.length) ? getParserPool() : null;
|
|
16049
16058
|
if (pool) {
|
|
16050
16059
|
try {
|
|
16051
16060
|
await pool.ensureReady();
|
|
@@ -16095,12 +16104,14 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16095
16104
|
const err = settled.reason;
|
|
16096
16105
|
if (err instanceof Error && isAbortError(err)) throw err;
|
|
16097
16106
|
errors.push(`batch error: ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
16107
|
+
filesFailed++;
|
|
16098
16108
|
continue;
|
|
16099
16109
|
}
|
|
16100
16110
|
const result = settled.value;
|
|
16101
16111
|
if (result.error) {
|
|
16102
16112
|
if (result.missing) store.deleteFile(file);
|
|
16103
16113
|
errors.push(`${file}: ${result.error}`);
|
|
16114
|
+
filesFailed++;
|
|
16104
16115
|
continue;
|
|
16105
16116
|
}
|
|
16106
16117
|
const { stat: stat19, lang, parsed } = result;
|
|
@@ -16108,6 +16119,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16108
16119
|
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
16109
16120
|
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
16110
16121
|
filesIndexed++;
|
|
16122
|
+
filesSkipped++;
|
|
16111
16123
|
const stored = existingMeta.get(file);
|
|
16112
16124
|
if (stored && stored.mtimeMs !== result.skippedMeta.mtimeMs) {
|
|
16113
16125
|
store.upsertFile({
|
|
@@ -16132,6 +16144,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16132
16144
|
contentHash: result.contentHash ?? ""
|
|
16133
16145
|
});
|
|
16134
16146
|
filesIndexed++;
|
|
16147
|
+
filesEmpty++;
|
|
16135
16148
|
}
|
|
16136
16149
|
continue;
|
|
16137
16150
|
}
|
|
@@ -16145,6 +16158,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16145
16158
|
contentHash: result.contentHash ?? ""
|
|
16146
16159
|
});
|
|
16147
16160
|
filesIndexed++;
|
|
16161
|
+
filesEmpty++;
|
|
16148
16162
|
continue;
|
|
16149
16163
|
}
|
|
16150
16164
|
batchEntries.push({
|
|
@@ -16166,6 +16180,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16166
16180
|
symbolsIndexed += count;
|
|
16167
16181
|
langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
|
|
16168
16182
|
filesIndexed++;
|
|
16183
|
+
filesParsed++;
|
|
16169
16184
|
}
|
|
16170
16185
|
} catch (err) {
|
|
16171
16186
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -16178,6 +16193,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16178
16193
|
symbolsIndexed += symbolsWithIds.length;
|
|
16179
16194
|
langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
|
|
16180
16195
|
filesIndexed++;
|
|
16196
|
+
filesParsed++;
|
|
16181
16197
|
if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
|
|
16182
16198
|
const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
|
|
16183
16199
|
if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
|
|
@@ -16195,6 +16211,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16195
16211
|
contentHash: entry.contentHash
|
|
16196
16212
|
});
|
|
16197
16213
|
} catch (innerErr) {
|
|
16214
|
+
filesFailed++;
|
|
16198
16215
|
errors.push(
|
|
16199
16216
|
`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
|
|
16200
16217
|
);
|
|
@@ -16227,6 +16244,12 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16227
16244
|
const durationMs = Date.now() - startMs;
|
|
16228
16245
|
return {
|
|
16229
16246
|
filesIndexed,
|
|
16247
|
+
fileOutcomes: {
|
|
16248
|
+
parsed: filesParsed,
|
|
16249
|
+
skipped: filesSkipped,
|
|
16250
|
+
empty: filesEmpty,
|
|
16251
|
+
failed: filesFailed
|
|
16252
|
+
},
|
|
16230
16253
|
symbolsIndexed,
|
|
16231
16254
|
langStats,
|
|
16232
16255
|
durationMs,
|
|
@@ -22847,7 +22870,6 @@ import { randomUUID as randomUUID2 } from "node:crypto";
|
|
|
22847
22870
|
import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
|
|
22848
22871
|
import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
|
|
22849
22872
|
import {
|
|
22850
|
-
addColumn,
|
|
22851
22873
|
addTask,
|
|
22852
22874
|
adoptManagedLifecycle,
|
|
22853
22875
|
assignTask,
|
|
@@ -22862,7 +22884,7 @@ import {
|
|
|
22862
22884
|
exportBoardToTaskGraph,
|
|
22863
22885
|
finalizeTaskCompletion,
|
|
22864
22886
|
getBoard as getBoard3,
|
|
22865
|
-
getKanbanOrchestrationSnapshot,
|
|
22887
|
+
getKanbanOrchestrationSnapshot as getKanbanOrchestrationSnapshot2,
|
|
22866
22888
|
getKanbanQueueHealth,
|
|
22867
22889
|
getTask,
|
|
22868
22890
|
getTaskChain,
|
|
@@ -22876,16 +22898,16 @@ import {
|
|
|
22876
22898
|
recoverStaleTaskAssignments,
|
|
22877
22899
|
releaseTaskClaim,
|
|
22878
22900
|
removeBoard as removeBoard2,
|
|
22879
|
-
removeColumn,
|
|
22880
22901
|
removeTask,
|
|
22881
22902
|
repairManagedTaskProjection,
|
|
22903
|
+
resolveAutoAccept,
|
|
22882
22904
|
searchKanban,
|
|
22883
22905
|
setTaskChain,
|
|
22906
|
+
stripLifecycleIssues,
|
|
22884
22907
|
syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
|
|
22885
22908
|
transferTaskToBoard,
|
|
22886
22909
|
transitionTask,
|
|
22887
22910
|
updateBoard as updateBoard2,
|
|
22888
|
-
updateColumn,
|
|
22889
22911
|
updateTask as updateTask2,
|
|
22890
22912
|
updateTaskAssignment,
|
|
22891
22913
|
verifyTaskCompletion as verifyTaskCompletion2
|
|
@@ -22935,6 +22957,137 @@ function duplicateBoardOptions(input) {
|
|
|
22935
22957
|
};
|
|
22936
22958
|
}
|
|
22937
22959
|
|
|
22960
|
+
// src/kanban-contract-actions.ts
|
|
22961
|
+
import {
|
|
22962
|
+
addContractEdge,
|
|
22963
|
+
configureContractGraph,
|
|
22964
|
+
evaluateTaskContractGraph,
|
|
22965
|
+
getContractGraph,
|
|
22966
|
+
removeContractEdge,
|
|
22967
|
+
removeContractNode,
|
|
22968
|
+
upsertContractNode
|
|
22969
|
+
} from "@wrongstack/kanban";
|
|
22970
|
+
|
|
22971
|
+
// src/kanban-tool-results.ts
|
|
22972
|
+
function atomicityNudge(task) {
|
|
22973
|
+
if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
|
|
22974
|
+
const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
|
|
22975
|
+
return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
|
|
22976
|
+
}
|
|
22977
|
+
function readEnvGateEnforcement() {
|
|
22978
|
+
const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
|
|
22979
|
+
return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
|
|
22980
|
+
}
|
|
22981
|
+
function fail(message) {
|
|
22982
|
+
return { ok: false, message };
|
|
22983
|
+
}
|
|
22984
|
+
function okBoard(board, message = "Board loaded.") {
|
|
22985
|
+
return { ok: true, message, board };
|
|
22986
|
+
}
|
|
22987
|
+
function okTask(board, task, message) {
|
|
22988
|
+
return { ok: true, message, board, task };
|
|
22989
|
+
}
|
|
22990
|
+
|
|
22991
|
+
// src/kanban-contract-actions.ts
|
|
22992
|
+
async function handleKanbanContractAction(projectRoot, input, actor) {
|
|
22993
|
+
switch (input.action) {
|
|
22994
|
+
case "get_contract_graph": {
|
|
22995
|
+
if (!input.boardId) return fail("get_contract_graph requires boardId.");
|
|
22996
|
+
const found = await getContractGraph(projectRoot, input.boardId);
|
|
22997
|
+
if (!found) return fail("Board not found.");
|
|
22998
|
+
const evaluated = input.taskId ? await evaluateTaskContractGraph(projectRoot, input.boardId, input.taskId) : null;
|
|
22999
|
+
if (input.taskId && !evaluated) return fail("Task not found on this board.");
|
|
23000
|
+
return {
|
|
23001
|
+
ok: true,
|
|
23002
|
+
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.",
|
|
23003
|
+
board: found.board,
|
|
23004
|
+
contractGraph: found.graph,
|
|
23005
|
+
...evaluated ? { contractEvaluation: evaluated.evaluation } : {}
|
|
23006
|
+
};
|
|
23007
|
+
}
|
|
23008
|
+
case "configure_contract_graph": {
|
|
23009
|
+
if (!input.boardId) return fail("configure_contract_graph requires boardId.");
|
|
23010
|
+
const enforcement = input.contractEnforcement ?? "advisory";
|
|
23011
|
+
const board = await configureContractGraph(projectRoot, input.boardId, enforcement);
|
|
23012
|
+
return board ? okBoard(board, `Contract map enforcement set to ${enforcement}.`) : fail("Board not found.");
|
|
23013
|
+
}
|
|
23014
|
+
case "upsert_contract_node": {
|
|
23015
|
+
if (!input.boardId || !input.taskId) {
|
|
23016
|
+
return fail("upsert_contract_node requires boardId and taskId.");
|
|
23017
|
+
}
|
|
23018
|
+
if (!input.contractNodeKind || !input.contractNodeTitle) {
|
|
23019
|
+
return fail("upsert_contract_node requires contractNodeKind and contractNodeTitle.");
|
|
23020
|
+
}
|
|
23021
|
+
const waiver = input.contractNodeState === "waived" ? {
|
|
23022
|
+
actor: actor ?? "agent",
|
|
23023
|
+
reason: input.contractWaiverReason ?? "",
|
|
23024
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
23025
|
+
} : void 0;
|
|
23026
|
+
if (waiver && !waiver.reason.trim()) {
|
|
23027
|
+
return fail("A waived contract node requires contractWaiverReason.");
|
|
23028
|
+
}
|
|
23029
|
+
const result = await upsertContractNode(projectRoot, input.boardId, {
|
|
23030
|
+
taskId: input.taskId,
|
|
23031
|
+
kind: input.contractNodeKind,
|
|
23032
|
+
title: input.contractNodeTitle,
|
|
23033
|
+
...input.contractNodeId !== void 0 ? { id: input.contractNodeId } : {},
|
|
23034
|
+
...input.contractNodeDescription !== void 0 ? { description: input.contractNodeDescription } : {},
|
|
23035
|
+
...input.contractNodeState !== void 0 ? { state: input.contractNodeState } : {},
|
|
23036
|
+
...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
|
|
23037
|
+
...input.contractCheckId !== void 0 ? { checkId: input.contractCheckId } : {},
|
|
23038
|
+
...input.contractMetricId !== void 0 ? { metricId: input.contractMetricId } : {},
|
|
23039
|
+
...waiver ? { waiver } : {},
|
|
23040
|
+
...actor !== void 0 ? { createdBy: actor } : {}
|
|
23041
|
+
});
|
|
23042
|
+
if (!result) return fail("Board or task not found.");
|
|
23043
|
+
return {
|
|
23044
|
+
ok: true,
|
|
23045
|
+
message: `Contract node ${result.node.kind} "${result.node.title}" saved (${result.node.id}).`,
|
|
23046
|
+
board: result.board,
|
|
23047
|
+
contractGraph: result.board.contractGraph ?? null
|
|
23048
|
+
};
|
|
23049
|
+
}
|
|
23050
|
+
case "remove_contract_node": {
|
|
23051
|
+
if (!input.boardId || !input.contractNodeId) {
|
|
23052
|
+
return fail("remove_contract_node requires boardId and contractNodeId.");
|
|
23053
|
+
}
|
|
23054
|
+
const board = await removeContractNode(projectRoot, input.boardId, input.contractNodeId);
|
|
23055
|
+
return board ? okBoard(board, "Contract node removed, along with every edge that touched it.") : fail("Contract node not found.");
|
|
23056
|
+
}
|
|
23057
|
+
case "add_contract_edge": {
|
|
23058
|
+
if (!input.boardId || !input.contractEdgeFrom || !input.contractEdgeTo) {
|
|
23059
|
+
return fail("add_contract_edge requires boardId, contractEdgeFrom, and contractEdgeTo.");
|
|
23060
|
+
}
|
|
23061
|
+
if (!input.contractEdgeType) return fail("add_contract_edge requires contractEdgeType.");
|
|
23062
|
+
const result = await addContractEdge(projectRoot, input.boardId, {
|
|
23063
|
+
from: input.contractEdgeFrom,
|
|
23064
|
+
to: input.contractEdgeTo,
|
|
23065
|
+
type: input.contractEdgeType,
|
|
23066
|
+
...input.contractEdgeId !== void 0 ? { id: input.contractEdgeId } : {},
|
|
23067
|
+
...input.contractNodeEnforcement !== void 0 ? { enforcement: input.contractNodeEnforcement } : {},
|
|
23068
|
+
...input.contractEdgeRationale !== void 0 ? { rationale: input.contractEdgeRationale } : {},
|
|
23069
|
+
...actor !== void 0 ? { createdBy: actor } : {}
|
|
23070
|
+
});
|
|
23071
|
+
if (!result) return fail("Board not found.");
|
|
23072
|
+
return {
|
|
23073
|
+
ok: true,
|
|
23074
|
+
message: `Contract edge ${result.edge.type}: ${result.edge.from} \u2192 ${result.edge.to}.`,
|
|
23075
|
+
board: result.board,
|
|
23076
|
+
contractGraph: result.board.contractGraph ?? null
|
|
23077
|
+
};
|
|
23078
|
+
}
|
|
23079
|
+
case "remove_contract_edge": {
|
|
23080
|
+
if (!input.boardId || !input.contractEdgeId) {
|
|
23081
|
+
return fail("remove_contract_edge requires boardId and contractEdgeId.");
|
|
23082
|
+
}
|
|
23083
|
+
const board = await removeContractEdge(projectRoot, input.boardId, input.contractEdgeId);
|
|
23084
|
+
return board ? okBoard(board, "Contract edge removed.") : fail("Contract edge not found.");
|
|
23085
|
+
}
|
|
23086
|
+
default:
|
|
23087
|
+
return void 0;
|
|
23088
|
+
}
|
|
23089
|
+
}
|
|
23090
|
+
|
|
22938
23091
|
// src/kanban-decomposition-actions.ts
|
|
22939
23092
|
import {
|
|
22940
23093
|
assessTaskAtomicity,
|
|
@@ -22966,26 +23119,6 @@ function recordKanbanVerificationEvidence(ctx, report) {
|
|
|
22966
23119
|
}
|
|
22967
23120
|
}
|
|
22968
23121
|
|
|
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
23122
|
// src/kanban-decomposition-actions.ts
|
|
22990
23123
|
async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
22991
23124
|
switch (input.action) {
|
|
@@ -23070,20 +23203,14 @@ async function handleKanbanDecompositionAction(projectRoot, input, ctx) {
|
|
|
23070
23203
|
// src/kanban-detail-actions.ts
|
|
23071
23204
|
import {
|
|
23072
23205
|
addCheckToTask,
|
|
23073
|
-
addContractEdge,
|
|
23074
23206
|
addDependency,
|
|
23075
23207
|
addGoalMetricToTask,
|
|
23076
23208
|
addLinkToTask,
|
|
23077
23209
|
addNoteToTask,
|
|
23078
|
-
configureContractGraph,
|
|
23079
|
-
evaluateTaskContractGraph,
|
|
23080
|
-
getContractGraph,
|
|
23081
23210
|
getKanbanWorkbench,
|
|
23082
|
-
|
|
23083
|
-
removeContractNode,
|
|
23211
|
+
removeCheckFromTask,
|
|
23084
23212
|
updateCheckOnTask,
|
|
23085
|
-
updateGoalMetricOnTask
|
|
23086
|
-
upsertContractNode
|
|
23213
|
+
updateGoalMetricOnTask
|
|
23087
23214
|
} from "@wrongstack/kanban";
|
|
23088
23215
|
|
|
23089
23216
|
// src/kanban-split-task-handler.ts
|
|
@@ -23149,131 +23276,6 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
23149
23276
|
workbench
|
|
23150
23277
|
};
|
|
23151
23278
|
}
|
|
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
23279
|
case "add_dependency": {
|
|
23278
23280
|
if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
|
|
23279
23281
|
return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
|
|
@@ -23326,8 +23328,9 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
23326
23328
|
}
|
|
23327
23329
|
const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
|
|
23328
23330
|
description: input.checkDescription,
|
|
23329
|
-
type: "manual",
|
|
23330
|
-
status: input.checkStatus
|
|
23331
|
+
type: input.checkType ?? "manual",
|
|
23332
|
+
status: input.checkStatus,
|
|
23333
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
23331
23334
|
});
|
|
23332
23335
|
return board ? okBoard(board, "Check added.") : fail("Task not found.");
|
|
23333
23336
|
}
|
|
@@ -23342,11 +23345,27 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
23342
23345
|
input.checkId,
|
|
23343
23346
|
{
|
|
23344
23347
|
...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
|
|
23345
|
-
...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
|
|
23348
|
+
...input.checkStatus !== void 0 ? { status: input.checkStatus } : {},
|
|
23349
|
+
// Promoting an existing manual criterion to an executable one is the
|
|
23350
|
+
// common repair: the card was written before anyone knew the command.
|
|
23351
|
+
...input.checkType !== void 0 ? { type: input.checkType } : {},
|
|
23352
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
23346
23353
|
}
|
|
23347
23354
|
);
|
|
23348
23355
|
return board ? okBoard(board, "Check updated.") : fail("Check not found.");
|
|
23349
23356
|
}
|
|
23357
|
+
case "remove_check": {
|
|
23358
|
+
if (!input.boardId || !input.taskId || !input.checkId) {
|
|
23359
|
+
return fail("remove_check requires boardId, taskId, and checkId.");
|
|
23360
|
+
}
|
|
23361
|
+
const board = await removeCheckFromTask(
|
|
23362
|
+
projectRoot,
|
|
23363
|
+
input.boardId,
|
|
23364
|
+
input.taskId,
|
|
23365
|
+
input.checkId
|
|
23366
|
+
);
|
|
23367
|
+
return board ? okBoard(board, "Acceptance criterion removed.") : fail("Check not found on this task.");
|
|
23368
|
+
}
|
|
23350
23369
|
case "add_note": {
|
|
23351
23370
|
if (!input.boardId || !input.taskId || !input.note)
|
|
23352
23371
|
return fail("add_note requires boardId, taskId, and note.");
|
|
@@ -23421,14 +23440,25 @@ function taskInput(input) {
|
|
|
23421
23440
|
...input.order !== void 0 ? { order: input.order } : {},
|
|
23422
23441
|
...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
|
|
23423
23442
|
...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
|
|
23443
|
+
// The system prompt has always told the model it may "set atomic: true"
|
|
23444
|
+
// when creating a composite parent. It could not: the field reached
|
|
23445
|
+
// neither the create input nor the patch, so the instruction described a
|
|
23446
|
+
// capability that did not exist and the attempt was silently dropped.
|
|
23447
|
+
...input.atomic !== void 0 ? { atomic: input.atomic } : {},
|
|
23424
23448
|
...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
|
|
23425
23449
|
...input.checkDescription !== void 0 ? {
|
|
23426
23450
|
successCriteria: [
|
|
23427
23451
|
{
|
|
23428
23452
|
id: randomUUID(),
|
|
23429
23453
|
description: input.checkDescription,
|
|
23430
|
-
|
|
23431
|
-
|
|
23454
|
+
// `manual` only as the fallback. Hard-coding it here meant every
|
|
23455
|
+
// agent-authored criterion was unverifiable by construction: the
|
|
23456
|
+
// deterministic plugins never matched, the registry passed the
|
|
23457
|
+
// hand-set status straight through, and "verified" collapsed into
|
|
23458
|
+
// "the author ticked its own box".
|
|
23459
|
+
type: input.checkType ?? "manual",
|
|
23460
|
+
status: input.checkStatus ?? "pending",
|
|
23461
|
+
...input.checkNotes !== void 0 ? { notes: input.checkNotes } : {}
|
|
23432
23462
|
}
|
|
23433
23463
|
]
|
|
23434
23464
|
} : {},
|
|
@@ -23476,11 +23506,11 @@ function taskInput(input) {
|
|
|
23476
23506
|
};
|
|
23477
23507
|
}
|
|
23478
23508
|
function mergedDependsOn(input) {
|
|
23479
|
-
|
|
23509
|
+
if (input.dependsOn === void 0 && input.dependencyTaskId === void 0) return void 0;
|
|
23510
|
+
return [
|
|
23480
23511
|
...input.dependsOn ?? [],
|
|
23481
23512
|
...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
|
|
23482
23513
|
].filter((id, i, arr) => id && arr.indexOf(id) === i);
|
|
23483
|
-
return ids.length > 0 ? ids : void 0;
|
|
23484
23514
|
}
|
|
23485
23515
|
function taskPatch(input) {
|
|
23486
23516
|
return {
|
|
@@ -23494,7 +23524,15 @@ function taskPatch(input) {
|
|
|
23494
23524
|
status: input.status,
|
|
23495
23525
|
labels: input.labels,
|
|
23496
23526
|
assignedAgent: input.agentId,
|
|
23497
|
-
...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
|
|
23527
|
+
...mergedDependsOn(input) !== void 0 ? { dependsOn: mergedDependsOn(input) } : {},
|
|
23528
|
+
// `atomic` and `childTaskIds` are the composite-parent contract, and the
|
|
23529
|
+
// managed gate reads both: an `atomic` parent may not move forward without
|
|
23530
|
+
// children, and may not reach Done until every child is completed. The
|
|
23531
|
+
// manager has always accepted both on a patch; only this surface withheld
|
|
23532
|
+
// them, so `split_atomic` was a one-way door — delete the children and the
|
|
23533
|
+
// parent was stranded with no way to declare itself a leaf again.
|
|
23534
|
+
...input.atomic !== void 0 ? { atomic: input.atomic } : {},
|
|
23535
|
+
...input.childTaskIds !== void 0 ? { childTaskIds: input.childTaskIds } : {},
|
|
23498
23536
|
...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
|
|
23499
23537
|
...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
|
|
23500
23538
|
};
|
|
@@ -23554,8 +23592,8 @@ function assignmentForTaskCreate(input) {
|
|
|
23554
23592
|
}
|
|
23555
23593
|
|
|
23556
23594
|
// src/kanban-tool-schema.ts
|
|
23557
|
-
var KANBAN_TOOL_DESCRIPTION = "
|
|
23558
|
-
var KANBAN_TOOL_USAGE_HINT =
|
|
23595
|
+
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.";
|
|
23596
|
+
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
23597
|
var KANBAN_INPUT_SCHEMA = {
|
|
23560
23598
|
type: "object",
|
|
23561
23599
|
properties: {
|
|
@@ -23568,6 +23606,7 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
23568
23606
|
"duplicate_board",
|
|
23569
23607
|
"update_board",
|
|
23570
23608
|
"adopt_managed_lifecycle",
|
|
23609
|
+
"release_managed_lifecycle",
|
|
23571
23610
|
"delete_board",
|
|
23572
23611
|
"generate_board",
|
|
23573
23612
|
"export_markdown",
|
|
@@ -23579,9 +23618,6 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
23579
23618
|
"ready_tasks",
|
|
23580
23619
|
"snapshot",
|
|
23581
23620
|
"workbench",
|
|
23582
|
-
"add_column",
|
|
23583
|
-
"update_column",
|
|
23584
|
-
"delete_column",
|
|
23585
23621
|
"add_task",
|
|
23586
23622
|
"split_task",
|
|
23587
23623
|
"merge_tasks",
|
|
@@ -23596,13 +23632,6 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
23596
23632
|
"delete_task",
|
|
23597
23633
|
"set_chain",
|
|
23598
23634
|
"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
23635
|
"claim_task",
|
|
23607
23636
|
"release_task",
|
|
23608
23637
|
"assign_task",
|
|
@@ -23616,49 +23645,27 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
23616
23645
|
"update_goal_metric",
|
|
23617
23646
|
"add_check",
|
|
23618
23647
|
"update_check",
|
|
23648
|
+
"remove_check",
|
|
23619
23649
|
"add_note",
|
|
23620
23650
|
"add_link",
|
|
23621
23651
|
"verify_completion",
|
|
23622
23652
|
"split_atomic",
|
|
23623
23653
|
"assess_atomicity",
|
|
23624
|
-
"propose_decomposition"
|
|
23654
|
+
"propose_decomposition",
|
|
23655
|
+
"get_contract_graph",
|
|
23656
|
+
"configure_contract_graph",
|
|
23657
|
+
"upsert_contract_node",
|
|
23658
|
+
"remove_contract_node",
|
|
23659
|
+
"add_contract_edge",
|
|
23660
|
+
"remove_contract_edge"
|
|
23625
23661
|
]
|
|
23626
23662
|
},
|
|
23627
23663
|
boardId: { type: "string" },
|
|
23628
23664
|
taskId: { type: "string" },
|
|
23629
23665
|
taskIds: { type: "array", items: { type: "string" } },
|
|
23630
23666
|
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
23667
|
fromNodeId: { type: "string" },
|
|
23660
23668
|
toNodeId: { type: "string" },
|
|
23661
|
-
contractRationale: { type: "string" },
|
|
23662
23669
|
baseline: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
23663
23670
|
threshold: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
23664
23671
|
columnId: { type: "string" },
|
|
@@ -23742,7 +23749,20 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
23742
23749
|
costCeilingUsd: { type: "number" },
|
|
23743
23750
|
retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
|
|
23744
23751
|
lastFailureKind: { type: "string" },
|
|
23745
|
-
dependsOn: {
|
|
23752
|
+
dependsOn: {
|
|
23753
|
+
type: "array",
|
|
23754
|
+
items: { type: "string" },
|
|
23755
|
+
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."
|
|
23756
|
+
},
|
|
23757
|
+
atomic: {
|
|
23758
|
+
type: "boolean",
|
|
23759
|
+
description: "Composite parent (true) or executable leaf (false). Set false to make a stranded parent a leaf again after its children were dropped."
|
|
23760
|
+
},
|
|
23761
|
+
childTaskIds: {
|
|
23762
|
+
type: "array",
|
|
23763
|
+
items: { type: "string" },
|
|
23764
|
+
description: "Children of a composite parent. On update_task an explicit empty array detaches them all."
|
|
23765
|
+
},
|
|
23746
23766
|
estimatedHours: { type: "number" },
|
|
23747
23767
|
actualHours: { type: "number" },
|
|
23748
23768
|
taskGraph: { type: "object" },
|
|
@@ -23776,6 +23796,74 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
23776
23796
|
checkId: { type: "string" },
|
|
23777
23797
|
checkDescription: { type: "string" },
|
|
23778
23798
|
checkStatus: { type: "string", enum: ["pending", "passed", "failed", "skipped"] },
|
|
23799
|
+
checkType: {
|
|
23800
|
+
type: "string",
|
|
23801
|
+
// Only types a verifier can actually execute. `manual` is the default and
|
|
23802
|
+
// means a human or agent asserts the status by hand. The rest are run by
|
|
23803
|
+
// `verify_completion` against the default deterministic registry. Types
|
|
23804
|
+
// with no plugin in that registry (`auto`, `review`, `agent`, `council`)
|
|
23805
|
+
// are deliberately omitted: offering them would produce criteria that
|
|
23806
|
+
// silently report `skipped — no verifier plugin registered`.
|
|
23807
|
+
enum: ["manual", "command", "test", "file_exists", "file_matches", "git_diff", "metric"],
|
|
23808
|
+
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.'
|
|
23809
|
+
},
|
|
23810
|
+
checkNotes: {
|
|
23811
|
+
type: "string",
|
|
23812
|
+
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"}.'
|
|
23813
|
+
},
|
|
23814
|
+
// ── Contract map ───────────────────────────────────────────────────
|
|
23815
|
+
// The card contract: what this work targets, what it must not break, what
|
|
23816
|
+
// it risks, and what verifies it. Advisory by default — the readiness gate
|
|
23817
|
+
// deliberately does not require map structure, so a map is an operator
|
|
23818
|
+
// review aid, not work the model must complete before implementing.
|
|
23819
|
+
contractEnforcement: {
|
|
23820
|
+
type: "string",
|
|
23821
|
+
enum: ["off", "advisory", "strict"],
|
|
23822
|
+
description: "Board-level contract map enforcement. Default when first configured: advisory."
|
|
23823
|
+
},
|
|
23824
|
+
contractNodeId: { type: "string" },
|
|
23825
|
+
contractNodeKind: {
|
|
23826
|
+
type: "string",
|
|
23827
|
+
enum: ["objective", "guardrail", "risk", "component", "artifact", "verification"],
|
|
23828
|
+
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."
|
|
23829
|
+
},
|
|
23830
|
+
contractNodeTitle: { type: "string" },
|
|
23831
|
+
contractNodeDescription: { type: "string" },
|
|
23832
|
+
contractNodeState: {
|
|
23833
|
+
type: "string",
|
|
23834
|
+
enum: ["unknown", "active", "satisfied", "violated", "waived", "resolved"]
|
|
23835
|
+
},
|
|
23836
|
+
contractNodeEnforcement: {
|
|
23837
|
+
type: "string",
|
|
23838
|
+
enum: ["blocking", "advisory", "informational"]
|
|
23839
|
+
},
|
|
23840
|
+
/** Bind a node to an acceptance criterion or goal metric already on the task. */
|
|
23841
|
+
contractCheckId: { type: "string" },
|
|
23842
|
+
contractMetricId: { type: "string" },
|
|
23843
|
+
contractWaiverReason: {
|
|
23844
|
+
type: "string",
|
|
23845
|
+
description: 'Required, with an actor, when contractNodeState is "waived".'
|
|
23846
|
+
},
|
|
23847
|
+
contractEdgeId: { type: "string" },
|
|
23848
|
+
contractEdgeFrom: {
|
|
23849
|
+
type: "string",
|
|
23850
|
+
description: 'A contract node id, or a task id (bare or "task:<id>") for the card endpoint.'
|
|
23851
|
+
},
|
|
23852
|
+
contractEdgeTo: { type: "string" },
|
|
23853
|
+
contractEdgeType: {
|
|
23854
|
+
type: "string",
|
|
23855
|
+
enum: [
|
|
23856
|
+
"targets",
|
|
23857
|
+
"affects",
|
|
23858
|
+
"must_preserve",
|
|
23859
|
+
"exposes",
|
|
23860
|
+
"verified_by",
|
|
23861
|
+
"conflicts_with",
|
|
23862
|
+
"derived_from",
|
|
23863
|
+
"relates_to"
|
|
23864
|
+
]
|
|
23865
|
+
},
|
|
23866
|
+
contractEdgeRationale: { type: "string" },
|
|
23779
23867
|
note: { type: "string" },
|
|
23780
23868
|
author: { type: "string" },
|
|
23781
23869
|
url: { type: "string" },
|
|
@@ -23830,12 +23918,17 @@ import {
|
|
|
23830
23918
|
mutateTasks
|
|
23831
23919
|
} from "@wrongstack/core/storage";
|
|
23832
23920
|
import { deserializeTaskGraph } from "@wrongstack/core/tasking";
|
|
23833
|
-
import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
|
|
23921
|
+
import { formatTodosForModel, resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
|
|
23834
23922
|
import {
|
|
23835
23923
|
bridgeKanbanSupervisor,
|
|
23924
|
+
compactSessionMirrorBoard,
|
|
23836
23925
|
createBoard,
|
|
23926
|
+
DEFAULT_COLUMNS,
|
|
23837
23927
|
getBoard as getBoard2,
|
|
23928
|
+
getDependencyReadinessIssues,
|
|
23929
|
+
getKanbanOrchestrationSnapshot,
|
|
23838
23930
|
listBoards,
|
|
23931
|
+
pruneSessionBoards,
|
|
23839
23932
|
removeBoard,
|
|
23840
23933
|
syncBoardFromTaskGraph,
|
|
23841
23934
|
touchKanbanPresence as touchKanbanPresence2,
|
|
@@ -23843,16 +23936,14 @@ import {
|
|
|
23843
23936
|
} from "@wrongstack/kanban";
|
|
23844
23937
|
var SESSION_BOARD_TAG = "session-work";
|
|
23845
23938
|
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
|
-
];
|
|
23939
|
+
var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
|
|
23940
|
+
...column
|
|
23941
|
+
}));
|
|
23852
23942
|
var boardQueue = /* @__PURE__ */ new Map();
|
|
23853
23943
|
var boardEnsures = /* @__PURE__ */ new Map();
|
|
23854
23944
|
var pendingMirrors = /* @__PURE__ */ new Map();
|
|
23855
23945
|
var activeMirrors = /* @__PURE__ */ new Set();
|
|
23946
|
+
var mirrorFailures = /* @__PURE__ */ new Map();
|
|
23856
23947
|
var bindings = /* @__PURE__ */ new WeakMap();
|
|
23857
23948
|
var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
|
|
23858
23949
|
var activeSessionBoards = /* @__PURE__ */ new Map();
|
|
@@ -23862,6 +23953,33 @@ function boardKey(projectRoot, sessionId) {
|
|
|
23862
23953
|
function mirrorKey(projectRoot, sessionId, sourceSystem) {
|
|
23863
23954
|
return `${boardKey(projectRoot, sessionId)}\0${sourceSystem}`;
|
|
23864
23955
|
}
|
|
23956
|
+
function completedReconciliationGraph(latest, candidates) {
|
|
23957
|
+
const latestNodeIds = new Set(latest.nodes.map((node) => node.id));
|
|
23958
|
+
const carriedNodeIds = /* @__PURE__ */ new Set();
|
|
23959
|
+
const completedNodes = candidates.flatMap(
|
|
23960
|
+
(candidate) => candidate.nodes.filter((node) => {
|
|
23961
|
+
if (node.status !== "completed" || latestNodeIds.has(node.id) || carriedNodeIds.has(node.id)) {
|
|
23962
|
+
return false;
|
|
23963
|
+
}
|
|
23964
|
+
carriedNodeIds.add(node.id);
|
|
23965
|
+
return true;
|
|
23966
|
+
})
|
|
23967
|
+
);
|
|
23968
|
+
if (completedNodes.length === 0) return void 0;
|
|
23969
|
+
const carriedRequirements = completedNodes.flatMap(
|
|
23970
|
+
(node) => node.specRequirementId ? [node.specRequirementId] : []
|
|
23971
|
+
);
|
|
23972
|
+
return {
|
|
23973
|
+
...latest,
|
|
23974
|
+
nodes: [...latest.nodes, ...completedNodes],
|
|
23975
|
+
rootNodes: [.../* @__PURE__ */ new Set([...latest.rootNodes, ...completedNodes.map((node) => node.id)])],
|
|
23976
|
+
...latest.requiredRequirementIds ? {
|
|
23977
|
+
requiredRequirementIds: [
|
|
23978
|
+
.../* @__PURE__ */ new Set([...latest.requiredRequirementIds, ...carriedRequirements])
|
|
23979
|
+
]
|
|
23980
|
+
} : {}
|
|
23981
|
+
};
|
|
23982
|
+
}
|
|
23865
23983
|
function sessionTag(sessionId) {
|
|
23866
23984
|
return `session:${sessionId}`;
|
|
23867
23985
|
}
|
|
@@ -24002,16 +24120,44 @@ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
24002
24120
|
sourceSystem,
|
|
24003
24121
|
tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
|
|
24004
24122
|
archiveMissingTasks: true,
|
|
24005
|
-
includeCompletedTasks: true
|
|
24123
|
+
includeCompletedTasks: true,
|
|
24124
|
+
// The scope ledger stays declared and accurate, but it may not veto a
|
|
24125
|
+
// projection. A session mirror reflects a tactical list that shrinks by
|
|
24126
|
+
// design, and refusing the sync never protected the removed row — it
|
|
24127
|
+
// froze the entire board, permanently, because the stored scope then
|
|
24128
|
+
// outlived every later snapshot (`session-kanban.mirror-failed`).
|
|
24129
|
+
// Nothing is lost by shrinking here: `archiveMissingTasks` keeps the
|
|
24130
|
+
// removed card on the board as `archived`, the reconciliation pass
|
|
24131
|
+
// first walks vanished completed rows to Done, and the session journal
|
|
24132
|
+
// remains the durable record.
|
|
24133
|
+
allowRequirementScopeShrink: true
|
|
24006
24134
|
}
|
|
24007
24135
|
);
|
|
24008
|
-
|
|
24136
|
+
if (!result) return null;
|
|
24137
|
+
const compacted = await compactSessionMirrorBoard(projectRoot, board.id);
|
|
24138
|
+
if (compacted?.removedTaskIds.length) {
|
|
24139
|
+
return await getBoard2(projectRoot, board.id) ?? result.board;
|
|
24140
|
+
}
|
|
24141
|
+
return result.board;
|
|
24009
24142
|
});
|
|
24010
24143
|
}
|
|
24011
24144
|
function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
24012
24145
|
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return;
|
|
24013
24146
|
const key = mirrorKey(projectRoot, sessionId, sourceSystem);
|
|
24014
|
-
pendingMirrors.
|
|
24147
|
+
const previous = pendingMirrors.get(key);
|
|
24148
|
+
const reconciliationGraph = previous ? completedReconciliationGraph(
|
|
24149
|
+
graph,
|
|
24150
|
+
[previous.reconciliationGraph, previous.graph].filter(
|
|
24151
|
+
(candidate) => candidate !== void 0
|
|
24152
|
+
)
|
|
24153
|
+
) : void 0;
|
|
24154
|
+
pendingMirrors.set(key, {
|
|
24155
|
+
projectRoot,
|
|
24156
|
+
sessionId,
|
|
24157
|
+
graph,
|
|
24158
|
+
...reconciliationGraph ? { reconciliationGraph } : {},
|
|
24159
|
+
sourceSystem
|
|
24160
|
+
});
|
|
24015
24161
|
if (activeMirrors.has(key)) return;
|
|
24016
24162
|
activeMirrors.add(key);
|
|
24017
24163
|
void (async () => {
|
|
@@ -24021,20 +24167,34 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
24021
24167
|
if (!pending2) break;
|
|
24022
24168
|
pendingMirrors.delete(key);
|
|
24023
24169
|
try {
|
|
24170
|
+
if (pending2.reconciliationGraph) {
|
|
24171
|
+
await projectGraph(
|
|
24172
|
+
pending2.projectRoot,
|
|
24173
|
+
pending2.sessionId,
|
|
24174
|
+
pending2.reconciliationGraph,
|
|
24175
|
+
pending2.sourceSystem
|
|
24176
|
+
);
|
|
24177
|
+
}
|
|
24024
24178
|
await projectGraph(
|
|
24025
24179
|
pending2.projectRoot,
|
|
24026
24180
|
pending2.sessionId,
|
|
24027
24181
|
pending2.graph,
|
|
24028
24182
|
pending2.sourceSystem
|
|
24029
24183
|
);
|
|
24184
|
+
mirrorFailures.delete(boardKey(pending2.projectRoot, pending2.sessionId));
|
|
24030
24185
|
} catch (error) {
|
|
24186
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
24187
|
+
mirrorFailures.set(boardKey(pending2.projectRoot, pending2.sessionId), {
|
|
24188
|
+
message,
|
|
24189
|
+
sourceSystem: pending2.sourceSystem
|
|
24190
|
+
});
|
|
24031
24191
|
console.warn(
|
|
24032
24192
|
JSON.stringify({
|
|
24033
24193
|
level: "warn",
|
|
24034
24194
|
event: "session-kanban.mirror-failed",
|
|
24035
24195
|
sessionId: pending2.sessionId,
|
|
24036
24196
|
sourceSystem: pending2.sourceSystem,
|
|
24037
|
-
message
|
|
24197
|
+
message,
|
|
24038
24198
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
24039
24199
|
})
|
|
24040
24200
|
);
|
|
@@ -24055,6 +24215,19 @@ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
|
|
|
24055
24215
|
}
|
|
24056
24216
|
})();
|
|
24057
24217
|
}
|
|
24218
|
+
function takeSessionMirrorFailure(projectRoot, sessionId) {
|
|
24219
|
+
if (!projectRoot || !sessionId) return void 0;
|
|
24220
|
+
const key = boardKey(projectRoot, sessionId);
|
|
24221
|
+
const failure = mirrorFailures.get(key);
|
|
24222
|
+
if (!failure) return void 0;
|
|
24223
|
+
mirrorFailures.delete(key);
|
|
24224
|
+
return `Kanban mirror (${failure.sourceSystem}) failed and the board may be stale: ${failure.message}`;
|
|
24225
|
+
}
|
|
24226
|
+
function hasInFlightTodoMirror(projectRoot, sessionId) {
|
|
24227
|
+
if (!projectRoot || !sessionId) return false;
|
|
24228
|
+
const key = mirrorKey(projectRoot, sessionId, "session-todo");
|
|
24229
|
+
return pendingMirrors.has(key) || activeMirrors.has(key);
|
|
24230
|
+
}
|
|
24058
24231
|
function todoListToSerializedGraph(todos, sessionId) {
|
|
24059
24232
|
const graphId = `todo:${sessionId}`;
|
|
24060
24233
|
const nodes = todos.map((todo, index) => ({
|
|
@@ -24216,11 +24389,11 @@ function broadcastTodoUpdate(context, todos) {
|
|
|
24216
24389
|
});
|
|
24217
24390
|
}
|
|
24218
24391
|
function notifyTodoUpdate(context, todos) {
|
|
24219
|
-
const summary = todos
|
|
24392
|
+
const summary = formatTodosForModel(todos);
|
|
24220
24393
|
const text = `[KANBAN TODO UPDATE]
|
|
24221
24394
|
Another Kanban agent reassessed the shared board. The canonical todo list is now:
|
|
24222
24395
|
${summary}
|
|
24223
|
-
Reassess your current plan before continuing; do not rely on the initial todo snapshot.`;
|
|
24396
|
+
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
24397
|
const state = context.state;
|
|
24225
24398
|
if (typeof state.appendBlockToLastUserMessage === "function") {
|
|
24226
24399
|
if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
|
|
@@ -24229,6 +24402,10 @@ Reassess your current plan before continuing; do not rely on the initial todo sn
|
|
|
24229
24402
|
state.appendMessage({ role: "user", content: [{ type: "text", text }] });
|
|
24230
24403
|
}
|
|
24231
24404
|
}
|
|
24405
|
+
function todosNeedingSessionMirror(todos, activeBoardId2) {
|
|
24406
|
+
if (!activeBoardId2) return todos;
|
|
24407
|
+
return todos.filter((todo) => todo.kanbanBoardId !== activeBoardId2 || !todo.kanbanTaskId);
|
|
24408
|
+
}
|
|
24232
24409
|
function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
|
|
24233
24410
|
queueLatestMirror(
|
|
24234
24411
|
projectRoot,
|
|
@@ -24375,16 +24552,9 @@ function attachSessionKanbanMirror(context) {
|
|
|
24375
24552
|
const unsubscribe = context.state.onChange((change) => {
|
|
24376
24553
|
if (change.kind === "todos_replaced" && !suppressedTodoMirrors.has(context)) {
|
|
24377
24554
|
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
|
-
);
|
|
24555
|
+
const unbound = todosNeedingSessionMirror(snapshot, activeManagedBoardId());
|
|
24556
|
+
if (snapshot.length > 0 && unbound.length === 0) return;
|
|
24557
|
+
mirrorSessionTodosToKanban(context.projectRoot, unbound, sessionId());
|
|
24388
24558
|
return;
|
|
24389
24559
|
}
|
|
24390
24560
|
if (change.kind === "meta_set" && (change.key === "plan.path" || change.key === "task.path" || change.key === "kanban")) {
|
|
@@ -24420,10 +24590,42 @@ function attachSessionKanbanMirror(context) {
|
|
|
24420
24590
|
bindings.set(context, detach);
|
|
24421
24591
|
return detach;
|
|
24422
24592
|
}
|
|
24593
|
+
async function rebindSessionKanbanTask(context) {
|
|
24594
|
+
const sessionId = context.session?.id;
|
|
24595
|
+
if (!sessionId || !context.projectRoot) return null;
|
|
24596
|
+
if (context.currentKanbanTaskId) return null;
|
|
24597
|
+
let best;
|
|
24598
|
+
try {
|
|
24599
|
+
const snapshot = await getKanbanOrchestrationSnapshot(context.projectRoot);
|
|
24600
|
+
const nowMs = Date.now();
|
|
24601
|
+
for (const result of snapshot.running) {
|
|
24602
|
+
const assignment = result.task.assignment;
|
|
24603
|
+
if (assignment?.status !== "running") continue;
|
|
24604
|
+
const expiresAt = assignment.leaseExpiresAt ? Date.parse(assignment.leaseExpiresAt) : Number.NaN;
|
|
24605
|
+
if (Number.isFinite(expiresAt) && expiresAt <= nowMs) continue;
|
|
24606
|
+
const entry = result.board.presence?.find(
|
|
24607
|
+
(candidate) => candidate.sessionId === sessionId && candidate.taskId === result.task.id
|
|
24608
|
+
);
|
|
24609
|
+
if (!entry) continue;
|
|
24610
|
+
if (!best || entry.lastSeenAt > best.lastSeenAt) {
|
|
24611
|
+
best = { boardId: result.board.id, taskId: result.task.id, lastSeenAt: entry.lastSeenAt };
|
|
24612
|
+
}
|
|
24613
|
+
}
|
|
24614
|
+
} catch {
|
|
24615
|
+
return null;
|
|
24616
|
+
}
|
|
24617
|
+
if (!best) return null;
|
|
24618
|
+
context.setCurrentKanbanTask(best.taskId, best.boardId);
|
|
24619
|
+
return { boardId: best.boardId, taskId: best.taskId };
|
|
24620
|
+
}
|
|
24423
24621
|
async function hydrateSessionKanban(context) {
|
|
24424
24622
|
const id = context.session?.id ?? "";
|
|
24425
24623
|
if (!id) return null;
|
|
24624
|
+
await rebindSessionKanbanTask(context);
|
|
24426
24625
|
await cleanupEmptySessionKanbanBoards(context.projectRoot, id);
|
|
24626
|
+
if (context.projectRoot) {
|
|
24627
|
+
fireAndForget("prune-session-boards", pruneSessionBoards(context.projectRoot));
|
|
24628
|
+
}
|
|
24427
24629
|
let board = await ensureSessionKanbanBoard(context.projectRoot, id);
|
|
24428
24630
|
if (context.todos.length) {
|
|
24429
24631
|
board = await projectSessionTodosToKanban(context.projectRoot, context.todos, id);
|
|
@@ -24454,26 +24656,68 @@ function todoStatus(task) {
|
|
|
24454
24656
|
if (status === "in_progress" || status === "review") return "in_progress";
|
|
24455
24657
|
return "pending";
|
|
24456
24658
|
}
|
|
24457
|
-
function sessionTodoFromTask(task,
|
|
24659
|
+
function sessionTodoFromTask(task, board) {
|
|
24660
|
+
const blockedBy = board ? blockingTitles(board, task) : [];
|
|
24458
24661
|
return {
|
|
24459
24662
|
id: task.origin?.taskId ?? task.id,
|
|
24460
24663
|
content: task.title,
|
|
24461
24664
|
status: todoStatus(task),
|
|
24462
|
-
|
|
24463
|
-
|
|
24464
|
-
...task.description ? { activeForm: task.description } : {}
|
|
24665
|
+
...task.description ? { activeForm: task.description } : {},
|
|
24666
|
+
...blockedBy.length ? { blockedBy } : {}
|
|
24465
24667
|
};
|
|
24466
24668
|
}
|
|
24467
|
-
function managedTodoFromTask(task,
|
|
24669
|
+
function managedTodoFromTask(task, board) {
|
|
24468
24670
|
return {
|
|
24469
|
-
...sessionTodoFromTask(task,
|
|
24470
|
-
|
|
24671
|
+
...sessionTodoFromTask(task, board),
|
|
24672
|
+
kanbanBoardId: board.id,
|
|
24673
|
+
kanbanTaskId: task.id
|
|
24471
24674
|
};
|
|
24472
24675
|
}
|
|
24676
|
+
function blockingTitles(board, task) {
|
|
24677
|
+
return getDependencyReadinessIssues(board, task).map((issue) => {
|
|
24678
|
+
const dependency = board.tasks.find((candidate) => candidate.id === issue.dependencyId);
|
|
24679
|
+
if (!dependency) return `${issue.dependencyId} (missing)`;
|
|
24680
|
+
return dependency.title;
|
|
24681
|
+
});
|
|
24682
|
+
}
|
|
24683
|
+
var PRIORITY_ORDER = {
|
|
24684
|
+
critical: 0,
|
|
24685
|
+
high: 1,
|
|
24686
|
+
medium: 2,
|
|
24687
|
+
low: 3
|
|
24688
|
+
};
|
|
24689
|
+
function orderTasksForTodos(board, tasks) {
|
|
24690
|
+
const columnOrder = new Map(board.columns.map((column) => [column.id, column.order]));
|
|
24691
|
+
const baseline = [...tasks].sort(
|
|
24692
|
+
(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)
|
|
24693
|
+
);
|
|
24694
|
+
const included = new Set(baseline.map((task) => task.id));
|
|
24695
|
+
const remaining = new Map(baseline.map((task) => [task.id, task]));
|
|
24696
|
+
const emitted = [];
|
|
24697
|
+
const done = /* @__PURE__ */ new Set();
|
|
24698
|
+
while (remaining.size > 0) {
|
|
24699
|
+
const ready = baseline.filter(
|
|
24700
|
+
(task) => remaining.has(task.id) && (task.dependsOn ?? []).every(
|
|
24701
|
+
(dependencyId) => !included.has(dependencyId) || done.has(dependencyId)
|
|
24702
|
+
)
|
|
24703
|
+
);
|
|
24704
|
+
if (ready.length === 0) break;
|
|
24705
|
+
for (const task of ready) {
|
|
24706
|
+
remaining.delete(task.id);
|
|
24707
|
+
done.add(task.id);
|
|
24708
|
+
emitted.push(task);
|
|
24709
|
+
}
|
|
24710
|
+
}
|
|
24711
|
+
for (const task of baseline) if (remaining.has(task.id)) emitted.push(task);
|
|
24712
|
+
return emitted;
|
|
24713
|
+
}
|
|
24473
24714
|
function sameTodos(left, right) {
|
|
24474
24715
|
return left.length === right.length && left.every((todo, index) => {
|
|
24475
24716
|
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
|
|
24717
|
+
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,
|
|
24718
|
+
// the rows are otherwise identical and the unblocking would never
|
|
24719
|
+
// reach the model.
|
|
24720
|
+
(candidate.blockedBy ?? []).join("\0") === (todo.blockedBy ?? []).join("\0");
|
|
24477
24721
|
});
|
|
24478
24722
|
}
|
|
24479
24723
|
function applySessionKanbanBoardToTodos(context, board) {
|
|
@@ -24481,13 +24725,13 @@ function applySessionKanbanBoardToTodos(context, board) {
|
|
|
24481
24725
|
if (!sessionId || sessionIdFromTags(board.tags) !== sessionId || !isOwnedSessionBoard(board.tags)) {
|
|
24482
24726
|
return [...context.todos];
|
|
24483
24727
|
}
|
|
24484
|
-
|
|
24485
|
-
|
|
24486
|
-
|
|
24487
|
-
|
|
24488
|
-
|
|
24489
|
-
|
|
24490
|
-
|
|
24728
|
+
if (hasInFlightTodoMirror(context.projectRoot, sessionId)) return [...context.todos];
|
|
24729
|
+
const projectedTodos = orderTasksForTodos(
|
|
24730
|
+
board,
|
|
24731
|
+
board.tasks.filter(
|
|
24732
|
+
(task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
|
|
24733
|
+
)
|
|
24734
|
+
).map((task) => sessionTodoFromTask(task, board));
|
|
24491
24735
|
const allCompleted = projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === "completed");
|
|
24492
24736
|
const effectiveTodos = allCompleted ? [] : projectedTodos;
|
|
24493
24737
|
if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];
|
|
@@ -24508,11 +24752,12 @@ function applyManagedKanbanBoardToTodos(context, board) {
|
|
|
24508
24752
|
if (!activeBoardId2 || board.id !== activeBoardId2 || board.lifecycle?.mode !== "managed") {
|
|
24509
24753
|
return [...context.todos];
|
|
24510
24754
|
}
|
|
24511
|
-
const projectedTodos =
|
|
24512
|
-
|
|
24513
|
-
|
|
24514
|
-
|
|
24515
|
-
|
|
24755
|
+
const projectedTodos = orderTasksForTodos(
|
|
24756
|
+
board,
|
|
24757
|
+
board.tasks.filter(
|
|
24758
|
+
(task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
|
|
24759
|
+
)
|
|
24760
|
+
).map((task) => managedTodoFromTask(task, board));
|
|
24516
24761
|
if (sameTodos(context.todos, projectedTodos)) return [...context.todos];
|
|
24517
24762
|
suppressedTodoMirrors.add(context);
|
|
24518
24763
|
try {
|
|
@@ -24547,7 +24792,11 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
|
|
|
24547
24792
|
const id = context.session?.id ?? "";
|
|
24548
24793
|
if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
|
|
24549
24794
|
const planPath = context.meta["plan.path"];
|
|
24550
|
-
if (typeof planPath !== "string" || !planPath)
|
|
24795
|
+
if (typeof planPath !== "string" || !planPath) {
|
|
24796
|
+
throw new Error(
|
|
24797
|
+
"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."
|
|
24798
|
+
);
|
|
24799
|
+
}
|
|
24551
24800
|
const plan = await mutatePlan(planPath, id, (file) => ({
|
|
24552
24801
|
...file,
|
|
24553
24802
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -24565,7 +24814,11 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
|
|
|
24565
24814
|
}
|
|
24566
24815
|
if (task.origin?.system === "session-task" || task.origin?.system === "session" || graphId.startsWith("session:")) {
|
|
24567
24816
|
const taskPath = context.meta["task.path"];
|
|
24568
|
-
if (typeof taskPath !== "string" || !taskPath)
|
|
24817
|
+
if (typeof taskPath !== "string" || !taskPath) {
|
|
24818
|
+
throw new Error(
|
|
24819
|
+
"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."
|
|
24820
|
+
);
|
|
24821
|
+
}
|
|
24569
24822
|
const tasks = await mutateTasks(taskPath, id, (file) => ({
|
|
24570
24823
|
...file,
|
|
24571
24824
|
tasks: options.remove ? file.tasks.filter((item) => item.id !== originId) : file.tasks.map(
|
|
@@ -24614,8 +24867,14 @@ var kanbanTool = {
|
|
|
24614
24867
|
}
|
|
24615
24868
|
case "create_board": {
|
|
24616
24869
|
if (!input.title) return fail("create_board requires title.");
|
|
24870
|
+
const existing = (await listBoards2(projectRoot)).filter(
|
|
24871
|
+
(candidate) => (candidate.kind ?? "project") === "project"
|
|
24872
|
+
);
|
|
24617
24873
|
const board = await createBoard2(projectRoot, boardCreateInput(input, input.title));
|
|
24618
|
-
|
|
24874
|
+
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(
|
|
24875
|
+
", "
|
|
24876
|
+
)}${existing.length > 3 ? ", \u2026" : ""}. If this work belongs to one of them, add_task there instead and delete this board.` : "";
|
|
24877
|
+
return { ok: true, message: `Board created: ${board.title}.${note}`, board };
|
|
24619
24878
|
}
|
|
24620
24879
|
case "update_board": {
|
|
24621
24880
|
if (!input.boardId) return fail("update_board requires boardId.");
|
|
@@ -24644,6 +24903,20 @@ var kanbanTool = {
|
|
|
24644
24903
|
});
|
|
24645
24904
|
return board ? okBoard(board, "Managed lifecycle adopted without moving existing cards.") : fail("Board not found.");
|
|
24646
24905
|
}
|
|
24906
|
+
// Adoption used to be a one-way door: the strict lifecycle carries
|
|
24907
|
+
// acceptance-criteria, verification-report, review-evidence and
|
|
24908
|
+
// one-stage-at-a-time gates, and nothing on the tool surface could
|
|
24909
|
+
// undo it, so a board adopted once kept its ceremony forever. The
|
|
24910
|
+
// gates are worth having where a fleet is supervised; they are not
|
|
24911
|
+
// worth being unable to leave. Cards and columns are untouched.
|
|
24912
|
+
case "release_managed_lifecycle": {
|
|
24913
|
+
if (!input.boardId) return fail("release_managed_lifecycle requires boardId.");
|
|
24914
|
+
const board = await updateBoard2(projectRoot, input.boardId, { lifecycle: null });
|
|
24915
|
+
return board ? okBoard(
|
|
24916
|
+
board,
|
|
24917
|
+
"Managed lifecycle released; the board now tracks work without strict gates."
|
|
24918
|
+
) : fail("Board not found.");
|
|
24919
|
+
}
|
|
24647
24920
|
case "duplicate_board": {
|
|
24648
24921
|
if (!input.boardId) return fail("duplicate_board requires boardId.");
|
|
24649
24922
|
const board = await duplicateBoard(
|
|
@@ -24663,8 +24936,7 @@ var kanbanTool = {
|
|
|
24663
24936
|
const boardInput = createBoardFromText({
|
|
24664
24937
|
description: input.description,
|
|
24665
24938
|
...input.title !== void 0 ? { title: input.title } : {},
|
|
24666
|
-
...input.context !== void 0 ? { context: input.context } : {}
|
|
24667
|
-
...input.columns !== void 0 ? { columns: input.columns } : {}
|
|
24939
|
+
...input.context !== void 0 ? { context: input.context } : {}
|
|
24668
24940
|
});
|
|
24669
24941
|
const board = await createBoard2(projectRoot, boardInput);
|
|
24670
24942
|
for (const taskInput2 of parseLinesIntoTasks(
|
|
@@ -24802,7 +25074,7 @@ var kanbanTool = {
|
|
|
24802
25074
|
return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
|
|
24803
25075
|
}
|
|
24804
25076
|
case "snapshot": {
|
|
24805
|
-
const snapshot = await
|
|
25077
|
+
const snapshot = await getKanbanOrchestrationSnapshot2(projectRoot, {
|
|
24806
25078
|
query: input.query,
|
|
24807
25079
|
boardId: input.boardId,
|
|
24808
25080
|
assignedAgent: input.agentId,
|
|
@@ -24817,33 +25089,6 @@ var kanbanTool = {
|
|
|
24817
25089
|
snapshot
|
|
24818
25090
|
};
|
|
24819
25091
|
}
|
|
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
25092
|
case "add_task": {
|
|
24848
25093
|
if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
|
|
24849
25094
|
const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
|
|
@@ -24925,6 +25170,32 @@ var kanbanTool = {
|
|
|
24925
25170
|
`Task is not implementation-ready: ${readiness.issues.map((issue) => issue.message).join(" | ")}`
|
|
24926
25171
|
);
|
|
24927
25172
|
}
|
|
25173
|
+
if (board.lifecycle?.mode !== "managed") {
|
|
25174
|
+
const now2 = /* @__PURE__ */ new Date();
|
|
25175
|
+
const assigned = await updateTaskAssignment(projectRoot, board.id, task.id, {
|
|
25176
|
+
status: "running",
|
|
25177
|
+
agentId: input.agentId ?? input.author,
|
|
25178
|
+
leaseId: input.leaseId ?? randomUUID2(),
|
|
25179
|
+
claimedAt: input.claimedAt ?? now2.toISOString(),
|
|
25180
|
+
heartbeatAt: input.heartbeatAt ?? now2.toISOString(),
|
|
25181
|
+
leaseExpiresAt: input.leaseExpiresAt ?? new Date(now2.getTime() + 15 * 6e4).toISOString(),
|
|
25182
|
+
attempt: input.attempt ?? 1,
|
|
25183
|
+
maxAttempts: input.maxAttempts ?? 3
|
|
25184
|
+
});
|
|
25185
|
+
if (!assigned) return fail("Task assignment could not be started.");
|
|
25186
|
+
const started = await updateTask2(projectRoot, board.id, task.id, {
|
|
25187
|
+
status: "in_progress"
|
|
25188
|
+
});
|
|
25189
|
+
const current = started ?? assigned;
|
|
25190
|
+
const claimed = task;
|
|
25191
|
+
const currentTask = current.tasks.find((candidate) => candidate.id === claimed.id) ?? claimed;
|
|
25192
|
+
ctx.setCurrentKanbanTask?.(currentTask.id, current.id);
|
|
25193
|
+
return okTask(
|
|
25194
|
+
current,
|
|
25195
|
+
currentTask,
|
|
25196
|
+
"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."
|
|
25197
|
+
);
|
|
25198
|
+
}
|
|
24928
25199
|
let stage = task.lifecycle?.currentStage;
|
|
24929
25200
|
if (stage === "backlog") {
|
|
24930
25201
|
const moved = await transitionTask(projectRoot, board.id, task.id, {
|
|
@@ -25065,6 +25336,9 @@ var kanbanTool = {
|
|
|
25065
25336
|
if (!input.boardId || !input.taskId)
|
|
25066
25337
|
return fail("delete_task requires boardId and taskId.");
|
|
25067
25338
|
const board = await removeTask(projectRoot, input.boardId, input.taskId);
|
|
25339
|
+
if (board && ctx.currentKanbanTaskId === input.taskId) {
|
|
25340
|
+
ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
|
|
25341
|
+
}
|
|
25068
25342
|
return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
|
|
25069
25343
|
}
|
|
25070
25344
|
case "set_chain": {
|
|
@@ -25197,7 +25471,7 @@ var kanbanTool = {
|
|
|
25197
25471
|
});
|
|
25198
25472
|
} catch (err) {
|
|
25199
25473
|
lifecycleWarnings.push(
|
|
25200
|
-
`Lifecycle transition to Running deferred: ${err instanceof Error ? err.message : String(err)}`
|
|
25474
|
+
`Lifecycle transition to Running deferred: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
|
|
25201
25475
|
);
|
|
25202
25476
|
}
|
|
25203
25477
|
}
|
|
@@ -25221,7 +25495,7 @@ var kanbanTool = {
|
|
|
25221
25495
|
});
|
|
25222
25496
|
} catch (err) {
|
|
25223
25497
|
lifecycleWarnings.push(
|
|
25224
|
-
`Lifecycle transition to Review failed: ${err instanceof Error ? err.message : String(err)}`
|
|
25498
|
+
`Lifecycle transition to Review failed: ${stripLifecycleIssues(err instanceof Error ? err.message : String(err))}`
|
|
25225
25499
|
);
|
|
25226
25500
|
}
|
|
25227
25501
|
if (transitionResult) {
|
|
@@ -25241,7 +25515,11 @@ var kanbanTool = {
|
|
|
25241
25515
|
successCriteria: verResult.task.successCriteria
|
|
25242
25516
|
});
|
|
25243
25517
|
const verdict = verResult.report.verdict;
|
|
25244
|
-
if (verdict === "passed") {
|
|
25518
|
+
if (verdict === "passed" && !resolveAutoAccept(board)) {
|
|
25519
|
+
lifecycleWarnings.push(
|
|
25520
|
+
"Verification passed, but this board does not auto-accept. The card is in Review awaiting an explicit transition_task to done."
|
|
25521
|
+
);
|
|
25522
|
+
} else if (verdict === "passed") {
|
|
25245
25523
|
try {
|
|
25246
25524
|
const doneResult = await transitionTask(
|
|
25247
25525
|
projectRoot,
|
|
@@ -25359,11 +25637,34 @@ var kanbanTool = {
|
|
|
25359
25637
|
});
|
|
25360
25638
|
return {
|
|
25361
25639
|
ok: true,
|
|
25362
|
-
message: `Counts:
|
|
25640
|
+
message: `Counts: startable=${health.counts.startable}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
|
|
25363
25641
|
queueHealth: health
|
|
25364
25642
|
};
|
|
25365
25643
|
}
|
|
25644
|
+
// Not every action is handled above. These are dispatched from here,
|
|
25645
|
+
// and the split has already cost real time: an agent that read this
|
|
25646
|
+
// file concluded `add_check` / `update_check` did not exist, wrote
|
|
25647
|
+
// that on a card, and spent a session trying to satisfy a gate it
|
|
25648
|
+
// already had the tool to clear. Keep this index in step with the
|
|
25649
|
+
// handlers.
|
|
25650
|
+
//
|
|
25651
|
+
// kanban-detail-actions.ts workbench · add_dependency ·
|
|
25652
|
+
// add_goal_metric · update_goal_metric · add_check ·
|
|
25653
|
+
// update_check · add_note · add_link · split_atomic
|
|
25654
|
+
// kanban-decomposition-actions.ts verify_completion ·
|
|
25655
|
+
// assess_atomicity · propose_decomposition
|
|
25656
|
+
// kanban-contract-actions.ts get_contract_graph ·
|
|
25657
|
+
// configure_contract_graph · upsert_contract_node ·
|
|
25658
|
+
// remove_contract_node · add_contract_edge · remove_contract_edge
|
|
25366
25659
|
default:
|
|
25660
|
+
{
|
|
25661
|
+
const contractResult = await handleKanbanContractAction(
|
|
25662
|
+
projectRoot,
|
|
25663
|
+
input,
|
|
25664
|
+
input.author ?? input.agentId
|
|
25665
|
+
);
|
|
25666
|
+
if (contractResult !== void 0) return contractResult;
|
|
25667
|
+
}
|
|
25367
25668
|
{
|
|
25368
25669
|
const detailResult = await handleKanbanDetailAction(projectRoot, input);
|
|
25369
25670
|
if (detailResult !== void 0) return detailResult;
|
|
@@ -25373,7 +25674,7 @@ var kanbanTool = {
|
|
|
25373
25674
|
})();
|
|
25374
25675
|
return withPresence(result);
|
|
25375
25676
|
} catch (err) {
|
|
25376
|
-
return fail(err instanceof Error ? err.message : String(err));
|
|
25677
|
+
return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
|
|
25377
25678
|
}
|
|
25378
25679
|
}
|
|
25379
25680
|
};
|
|
@@ -26226,7 +26527,7 @@ import {
|
|
|
26226
26527
|
saveTasks,
|
|
26227
26528
|
setPlanItemStatus
|
|
26228
26529
|
} from "@wrongstack/core/storage";
|
|
26229
|
-
import { getBoard as getBoard4 } from "@wrongstack/kanban";
|
|
26530
|
+
import { addTask as addTask2, getBoard as getBoard4 } from "@wrongstack/kanban";
|
|
26230
26531
|
function normalizedTitle(value) {
|
|
26231
26532
|
return value.trim().toLocaleLowerCase().replace(/\s+/g, " ");
|
|
26232
26533
|
}
|
|
@@ -26254,11 +26555,51 @@ function bindTodosToBoard(items, previous, board) {
|
|
|
26254
26555
|
available.find((task2) => !used.has(task2.id) && normalizedTitle(task2.title) === title)
|
|
26255
26556
|
];
|
|
26256
26557
|
const task = candidates.find((candidate) => candidate && !used.has(candidate.id));
|
|
26257
|
-
if (!task)
|
|
26558
|
+
if (!task) {
|
|
26559
|
+
const { blockedBy: _discarded, ...rest } = item;
|
|
26560
|
+
return { ...rest };
|
|
26561
|
+
}
|
|
26258
26562
|
used.add(task.id);
|
|
26259
|
-
|
|
26563
|
+
const blockedBy = blockingTitles(board, task);
|
|
26564
|
+
return {
|
|
26565
|
+
...item,
|
|
26566
|
+
kanbanBoardId: board.id,
|
|
26567
|
+
kanbanTaskId: task.id,
|
|
26568
|
+
...blockedBy.length ? { blockedBy } : { blockedBy: void 0 }
|
|
26569
|
+
};
|
|
26570
|
+
});
|
|
26571
|
+
}
|
|
26572
|
+
function demoteBlockedInProgress(items, warnings) {
|
|
26573
|
+
return items.map((item) => {
|
|
26574
|
+
if (item.status !== "in_progress" || !item.blockedBy?.length) return item;
|
|
26575
|
+
warnings.push(
|
|
26576
|
+
`"${item.content}" cannot start yet \u2014 it waits on: ${item.blockedBy.join("; ")}. Kept as pending; complete the blocking work first.`
|
|
26577
|
+
);
|
|
26578
|
+
return { ...item, status: "pending" };
|
|
26260
26579
|
});
|
|
26261
26580
|
}
|
|
26581
|
+
async function createMissingManagedCards(items, board, ctx, warnings) {
|
|
26582
|
+
const created = /* @__PURE__ */ new Map();
|
|
26583
|
+
for (const item of items) {
|
|
26584
|
+
if (item.kanbanBoardId === board.id && item.kanbanTaskId) continue;
|
|
26585
|
+
try {
|
|
26586
|
+
const result = await addTask2(ctx.projectRoot, board.id, {
|
|
26587
|
+
title: item.content,
|
|
26588
|
+
description: item.activeForm?.trim() || `Added from the session todo list: ${item.content}`
|
|
26589
|
+
});
|
|
26590
|
+
if (!result) {
|
|
26591
|
+
warnings.push(`Could not open a Kanban card for "${item.content}": board not found.`);
|
|
26592
|
+
continue;
|
|
26593
|
+
}
|
|
26594
|
+
created.set(item.id, result.task.id);
|
|
26595
|
+
} catch (error) {
|
|
26596
|
+
warnings.push(
|
|
26597
|
+
`Could not open a Kanban card for "${item.content}": ${error instanceof Error ? error.message : String(error)}`
|
|
26598
|
+
);
|
|
26599
|
+
}
|
|
26600
|
+
}
|
|
26601
|
+
return created;
|
|
26602
|
+
}
|
|
26262
26603
|
async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
26263
26604
|
let synced = 0;
|
|
26264
26605
|
const warnings = [];
|
|
@@ -26296,6 +26637,16 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
|
26296
26637
|
transitionComment: `Todo returned to queue: ${item.content}`
|
|
26297
26638
|
});
|
|
26298
26639
|
}
|
|
26640
|
+
for (const item of items) {
|
|
26641
|
+
if (item.status === "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
|
|
26642
|
+
continue;
|
|
26643
|
+
}
|
|
26644
|
+
const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
|
|
26645
|
+
if (task?.status !== "completed") continue;
|
|
26646
|
+
warnings.push(
|
|
26647
|
+
`"${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.`
|
|
26648
|
+
);
|
|
26649
|
+
}
|
|
26299
26650
|
for (const item of items) {
|
|
26300
26651
|
if (item.status !== "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
|
|
26301
26652
|
continue;
|
|
@@ -26347,17 +26698,25 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
|
26347
26698
|
const active = items.find(
|
|
26348
26699
|
(item) => item.status === "in_progress" && item.kanbanBoardId === board.id && Boolean(item.kanbanTaskId)
|
|
26349
26700
|
);
|
|
26701
|
+
const activeStage = active?.kanbanTaskId ? afterCompletions?.tasks.find((task) => task.id === active.kanbanTaskId)?.lifecycle?.currentStage : void 0;
|
|
26350
26702
|
if (active?.kanbanTaskId) {
|
|
26351
|
-
|
|
26352
|
-
|
|
26353
|
-
|
|
26354
|
-
|
|
26355
|
-
|
|
26356
|
-
|
|
26357
|
-
|
|
26358
|
-
|
|
26703
|
+
if (activeStage === "review" || activeStage === "done") {
|
|
26704
|
+
warnings.push(
|
|
26705
|
+
`"${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.")
|
|
26706
|
+
);
|
|
26707
|
+
} else {
|
|
26708
|
+
await execute({
|
|
26709
|
+
action: "start_task",
|
|
26710
|
+
boardId: board.id,
|
|
26711
|
+
taskId: active.kanbanTaskId,
|
|
26712
|
+
author: actor,
|
|
26713
|
+
agentId: actor,
|
|
26714
|
+
transitionComment: `Todo activated: ${active.content}`
|
|
26715
|
+
});
|
|
26716
|
+
}
|
|
26359
26717
|
}
|
|
26360
|
-
if (active?.kanbanTaskId &&
|
|
26718
|
+
if (active?.kanbanTaskId && activeStage === "review") {
|
|
26719
|
+
} else if (active?.kanbanTaskId && completionPending) {
|
|
26361
26720
|
warnings.push(
|
|
26362
26721
|
"A completed todo is still awaiting acceptance; the next independent Kanban task was started."
|
|
26363
26722
|
);
|
|
@@ -26444,29 +26803,47 @@ var todoTool = {
|
|
|
26444
26803
|
}
|
|
26445
26804
|
}
|
|
26446
26805
|
const boardId = activeBoardId(items, ctx);
|
|
26447
|
-
|
|
26448
|
-
const
|
|
26806
|
+
let board = boardId ? await getBoard4(ctx.projectRoot, boardId) : null;
|
|
26807
|
+
const managed = board?.lifecycle?.mode === "managed";
|
|
26808
|
+
let boundItems = managed && board ? bindTodosToBoard(items, ctx.todos ?? [], board) : items;
|
|
26809
|
+
const creationWarnings = [];
|
|
26810
|
+
if (managed && board) {
|
|
26811
|
+
const managedBoardId = board.id;
|
|
26812
|
+
const created = await createMissingManagedCards(boundItems, board, ctx, creationWarnings);
|
|
26813
|
+
if (created.size > 0) {
|
|
26814
|
+
boundItems = boundItems.map((item) => {
|
|
26815
|
+
const taskId = created.get(item.id);
|
|
26816
|
+
return taskId ? { ...item, kanbanBoardId: managedBoardId, kanbanTaskId: taskId } : item;
|
|
26817
|
+
});
|
|
26818
|
+
board = await getBoard4(ctx.projectRoot, managedBoardId) ?? board;
|
|
26819
|
+
boundItems = bindTodosToBoard(boundItems, ctx.todos ?? [], board);
|
|
26820
|
+
}
|
|
26821
|
+
boundItems = demoteBlockedInProgress(boundItems, creationWarnings);
|
|
26822
|
+
}
|
|
26449
26823
|
ctx.state.replaceTodos(boundItems);
|
|
26450
|
-
const kanbanSync =
|
|
26451
|
-
|
|
26824
|
+
const kanbanSync = managed && board ? await synchronizeManagedKanban(boundItems, board, ctx, call.signal) : { synced: 0, warnings: [] };
|
|
26825
|
+
kanbanSync.warnings.unshift(...creationWarnings);
|
|
26826
|
+
if (managed && board) {
|
|
26452
26827
|
const unresolved = boundItems.filter(
|
|
26453
26828
|
(item) => item.kanbanBoardId !== board.id || !item.kanbanTaskId
|
|
26454
26829
|
);
|
|
26455
26830
|
if (unresolved.length > 0) {
|
|
26456
26831
|
kanbanSync.warnings.push(
|
|
26457
|
-
`${unresolved.length} Todo row(s)
|
|
26832
|
+
`${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
26833
|
);
|
|
26459
26834
|
}
|
|
26460
26835
|
}
|
|
26836
|
+
const mirrorFailure = takeSessionMirrorFailure(ctx.projectRoot, ctx.session?.id ?? "");
|
|
26837
|
+
if (mirrorFailure) kanbanSync.warnings.push(mirrorFailure);
|
|
26461
26838
|
let projectedBoard = board;
|
|
26462
|
-
if (
|
|
26839
|
+
if (managed && board) {
|
|
26463
26840
|
const refreshed = await getBoard4(ctx.projectRoot, board.id);
|
|
26464
26841
|
if (refreshed) {
|
|
26465
26842
|
projectedBoard = refreshed;
|
|
26466
26843
|
applyManagedKanbanBoardToTodos(ctx, refreshed);
|
|
26467
26844
|
}
|
|
26468
26845
|
}
|
|
26469
|
-
if (
|
|
26846
|
+
if (!managed) {
|
|
26470
26847
|
mirrorSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? "session");
|
|
26471
26848
|
}
|
|
26472
26849
|
const completedPlanIds = /* @__PURE__ */ new Set();
|
|
@@ -29502,6 +29879,7 @@ var OPTIONAL_TOOLS = [
|
|
|
29502
29879
|
toolHelpTool,
|
|
29503
29880
|
setWorkingDirTool
|
|
29504
29881
|
];
|
|
29882
|
+
var OFF_ONLY_TOOLS = [...browserTools, e2ePlanTool];
|
|
29505
29883
|
var TIER1_TOOLS = [
|
|
29506
29884
|
readTool,
|
|
29507
29885
|
writeTool,
|
|
@@ -30983,6 +31361,7 @@ export {
|
|
|
30983
31361
|
IndexCircuitBreaker,
|
|
30984
31362
|
IndexTimeoutError,
|
|
30985
31363
|
LanguageProfileRegistry,
|
|
31364
|
+
OFF_ONLY_TOOLS,
|
|
30986
31365
|
OPTIONAL_TOOLS,
|
|
30987
31366
|
PRIMARY_LANGUAGE_PROFILES,
|
|
30988
31367
|
SESSION_KANBAN_COLUMNS,
|
|
@@ -31102,6 +31481,7 @@ export {
|
|
|
31102
31481
|
projectSessionTasksToKanban,
|
|
31103
31482
|
projectSessionTodosToKanban,
|
|
31104
31483
|
readTool,
|
|
31484
|
+
rebindSessionKanbanTask,
|
|
31105
31485
|
recordKanbanVerificationEvidence,
|
|
31106
31486
|
redactBrowserText,
|
|
31107
31487
|
registerBuiltinToolTier,
|