choda-deck 0.1.1 → 0.2.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/README.md +230 -168
- package/dist/cli.cjs +39261 -0
- package/dist/{session-rules.md → mcp-rules.md} +94 -83
- package/dist/mcp-server.cjs +1562 -358
- package/package.json +78 -74
package/dist/mcp-server.cjs
CHANGED
|
@@ -2234,8 +2234,8 @@ var require_resolve = __commonJS({
|
|
|
2234
2234
|
}
|
|
2235
2235
|
return count;
|
|
2236
2236
|
}
|
|
2237
|
-
function getFullPath(resolver, id = "",
|
|
2238
|
-
if (
|
|
2237
|
+
function getFullPath(resolver, id = "", normalize3) {
|
|
2238
|
+
if (normalize3 !== false)
|
|
2239
2239
|
id = normalizeId(id);
|
|
2240
2240
|
const p = resolver.parse(id);
|
|
2241
2241
|
return _getFullPath(resolver, p);
|
|
@@ -3225,8 +3225,8 @@ var require_utils = __commonJS({
|
|
|
3225
3225
|
}
|
|
3226
3226
|
return ind;
|
|
3227
3227
|
}
|
|
3228
|
-
function removeDotSegments(
|
|
3229
|
-
let input =
|
|
3228
|
+
function removeDotSegments(path11) {
|
|
3229
|
+
let input = path11;
|
|
3230
3230
|
const output = [];
|
|
3231
3231
|
let nextSlash = -1;
|
|
3232
3232
|
let len = 0;
|
|
@@ -3425,8 +3425,8 @@ var require_schemes = __commonJS({
|
|
|
3425
3425
|
wsComponent.secure = void 0;
|
|
3426
3426
|
}
|
|
3427
3427
|
if (wsComponent.resourceName) {
|
|
3428
|
-
const [
|
|
3429
|
-
wsComponent.path =
|
|
3428
|
+
const [path11, query] = wsComponent.resourceName.split("?");
|
|
3429
|
+
wsComponent.path = path11 && path11 !== "/" ? path11 : void 0;
|
|
3430
3430
|
wsComponent.query = query;
|
|
3431
3431
|
wsComponent.resourceName = void 0;
|
|
3432
3432
|
}
|
|
@@ -3575,7 +3575,7 @@ var require_fast_uri = __commonJS({
|
|
|
3575
3575
|
"use strict";
|
|
3576
3576
|
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
|
|
3577
3577
|
var { SCHEMES, getSchemeHandler } = require_schemes();
|
|
3578
|
-
function
|
|
3578
|
+
function normalize3(uri, options) {
|
|
3579
3579
|
if (typeof uri === "string") {
|
|
3580
3580
|
uri = /** @type {T} */
|
|
3581
3581
|
serialize(parse3(uri, options), options);
|
|
@@ -3811,7 +3811,7 @@ var require_fast_uri = __commonJS({
|
|
|
3811
3811
|
}
|
|
3812
3812
|
var fastUri = {
|
|
3813
3813
|
SCHEMES,
|
|
3814
|
-
normalize:
|
|
3814
|
+
normalize: normalize3,
|
|
3815
3815
|
resolve: resolve3,
|
|
3816
3816
|
resolveComponent,
|
|
3817
3817
|
equal,
|
|
@@ -6788,12 +6788,12 @@ var require_dist = __commonJS({
|
|
|
6788
6788
|
throw new Error(`Unknown format "${name}"`);
|
|
6789
6789
|
return f;
|
|
6790
6790
|
};
|
|
6791
|
-
function addFormats(ajv, list,
|
|
6791
|
+
function addFormats(ajv, list, fs8, exportName) {
|
|
6792
6792
|
var _a2;
|
|
6793
6793
|
var _b;
|
|
6794
6794
|
(_a2 = (_b = ajv.opts.code).formats) !== null && _a2 !== void 0 ? _a2 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
|
|
6795
6795
|
for (const f of list)
|
|
6796
|
-
ajv.addFormat(f,
|
|
6796
|
+
ajv.addFormat(f, fs8[f]);
|
|
6797
6797
|
}
|
|
6798
6798
|
module2.exports = exports2 = formatsPlugin;
|
|
6799
6799
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
@@ -6864,6 +6864,10 @@ var init_local_embedding_provider = __esm({
|
|
|
6864
6864
|
}
|
|
6865
6865
|
});
|
|
6866
6866
|
|
|
6867
|
+
// src/adapters/mcp/server-bootstrap.ts
|
|
6868
|
+
var fs7 = __toESM(require("fs"));
|
|
6869
|
+
var path10 = __toESM(require("path"));
|
|
6870
|
+
|
|
6867
6871
|
// node_modules/zod/v3/helpers/util.js
|
|
6868
6872
|
var util;
|
|
6869
6873
|
(function(util2) {
|
|
@@ -7223,8 +7227,8 @@ function getErrorMap() {
|
|
|
7223
7227
|
|
|
7224
7228
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
7225
7229
|
var makeIssue = (params) => {
|
|
7226
|
-
const { data, path:
|
|
7227
|
-
const fullPath = [...
|
|
7230
|
+
const { data, path: path11, errorMaps, issueData } = params;
|
|
7231
|
+
const fullPath = [...path11, ...issueData.path || []];
|
|
7228
7232
|
const fullIssue = {
|
|
7229
7233
|
...issueData,
|
|
7230
7234
|
path: fullPath
|
|
@@ -7339,11 +7343,11 @@ var errorUtil;
|
|
|
7339
7343
|
|
|
7340
7344
|
// node_modules/zod/v3/types.js
|
|
7341
7345
|
var ParseInputLazyPath = class {
|
|
7342
|
-
constructor(parent, value,
|
|
7346
|
+
constructor(parent, value, path11, key) {
|
|
7343
7347
|
this._cachedPath = [];
|
|
7344
7348
|
this.parent = parent;
|
|
7345
7349
|
this.data = value;
|
|
7346
|
-
this._path =
|
|
7350
|
+
this._path = path11;
|
|
7347
7351
|
this._key = key;
|
|
7348
7352
|
}
|
|
7349
7353
|
get path() {
|
|
@@ -11266,10 +11270,10 @@ function mergeDefs(...defs) {
|
|
|
11266
11270
|
function cloneDef(schema) {
|
|
11267
11271
|
return mergeDefs(schema._zod.def);
|
|
11268
11272
|
}
|
|
11269
|
-
function getElementAtPath(obj,
|
|
11270
|
-
if (!
|
|
11273
|
+
function getElementAtPath(obj, path11) {
|
|
11274
|
+
if (!path11)
|
|
11271
11275
|
return obj;
|
|
11272
|
-
return
|
|
11276
|
+
return path11.reduce((acc, key) => acc?.[key], obj);
|
|
11273
11277
|
}
|
|
11274
11278
|
function promiseAllObject(promisesObj) {
|
|
11275
11279
|
const keys = Object.keys(promisesObj);
|
|
@@ -11652,11 +11656,11 @@ function aborted(x, startIndex = 0) {
|
|
|
11652
11656
|
}
|
|
11653
11657
|
return false;
|
|
11654
11658
|
}
|
|
11655
|
-
function prefixIssues(
|
|
11659
|
+
function prefixIssues(path11, issues) {
|
|
11656
11660
|
return issues.map((iss) => {
|
|
11657
11661
|
var _a2;
|
|
11658
11662
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
11659
|
-
iss.path.unshift(
|
|
11663
|
+
iss.path.unshift(path11);
|
|
11660
11664
|
return iss;
|
|
11661
11665
|
});
|
|
11662
11666
|
}
|
|
@@ -11839,7 +11843,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
|
|
|
11839
11843
|
}
|
|
11840
11844
|
function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
11841
11845
|
const result = { errors: [] };
|
|
11842
|
-
const processError = (error49,
|
|
11846
|
+
const processError = (error49, path11 = []) => {
|
|
11843
11847
|
var _a2, _b;
|
|
11844
11848
|
for (const issue2 of error49.issues) {
|
|
11845
11849
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
@@ -11849,7 +11853,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
11849
11853
|
} else if (issue2.code === "invalid_element") {
|
|
11850
11854
|
processError({ issues: issue2.issues }, issue2.path);
|
|
11851
11855
|
} else {
|
|
11852
|
-
const fullpath = [...
|
|
11856
|
+
const fullpath = [...path11, ...issue2.path];
|
|
11853
11857
|
if (fullpath.length === 0) {
|
|
11854
11858
|
result.errors.push(mapper(issue2));
|
|
11855
11859
|
continue;
|
|
@@ -11881,8 +11885,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
11881
11885
|
}
|
|
11882
11886
|
function toDotPath(_path) {
|
|
11883
11887
|
const segs = [];
|
|
11884
|
-
const
|
|
11885
|
-
for (const seg of
|
|
11888
|
+
const path11 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
11889
|
+
for (const seg of path11) {
|
|
11886
11890
|
if (typeof seg === "number")
|
|
11887
11891
|
segs.push(`[${seg}]`);
|
|
11888
11892
|
else if (typeof seg === "symbol")
|
|
@@ -24288,13 +24292,13 @@ function resolveRef(ref, ctx) {
|
|
|
24288
24292
|
if (!ref.startsWith("#")) {
|
|
24289
24293
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
24290
24294
|
}
|
|
24291
|
-
const
|
|
24292
|
-
if (
|
|
24295
|
+
const path11 = ref.slice(1).split("/").filter(Boolean);
|
|
24296
|
+
if (path11.length === 0) {
|
|
24293
24297
|
return ctx.rootSchema;
|
|
24294
24298
|
}
|
|
24295
24299
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
24296
|
-
if (
|
|
24297
|
-
const key =
|
|
24300
|
+
if (path11[0] === defsKey) {
|
|
24301
|
+
const key = path11[1];
|
|
24298
24302
|
if (!key || !ctx.defs[key]) {
|
|
24299
24303
|
throw new Error(`Reference not found: ${ref}`);
|
|
24300
24304
|
}
|
|
@@ -27465,13 +27469,13 @@ var zodToJsonSchema = (schema, options) => {
|
|
|
27465
27469
|
}, true) ?? parseAnyDef(refs)
|
|
27466
27470
|
}), {}) : void 0;
|
|
27467
27471
|
const name = typeof options === "string" ? options : options?.nameStrategy === "title" ? void 0 : options?.name;
|
|
27468
|
-
const
|
|
27472
|
+
const main = parseDef(schema._def, name === void 0 ? refs : {
|
|
27469
27473
|
...refs,
|
|
27470
27474
|
currentPath: [...refs.basePath, refs.definitionPath, name]
|
|
27471
27475
|
}, false) ?? parseAnyDef(refs);
|
|
27472
27476
|
const title = typeof options === "object" && options.name !== void 0 && options.nameStrategy === "title" ? options.name : void 0;
|
|
27473
27477
|
if (title !== void 0) {
|
|
27474
|
-
|
|
27478
|
+
main.title = title;
|
|
27475
27479
|
}
|
|
27476
27480
|
if (refs.flags.hasReferencedOpenAiAnyType) {
|
|
27477
27481
|
if (!definitions) {
|
|
@@ -27492,9 +27496,9 @@ var zodToJsonSchema = (schema, options) => {
|
|
|
27492
27496
|
}
|
|
27493
27497
|
}
|
|
27494
27498
|
const combined = name === void 0 ? definitions ? {
|
|
27495
|
-
...
|
|
27499
|
+
...main,
|
|
27496
27500
|
[refs.definitionPath]: definitions
|
|
27497
|
-
} :
|
|
27501
|
+
} : main : {
|
|
27498
27502
|
$ref: [
|
|
27499
27503
|
...refs.$refStrategy === "relative" ? [] : refs.basePath,
|
|
27500
27504
|
refs.definitionPath,
|
|
@@ -27502,7 +27506,7 @@ var zodToJsonSchema = (schema, options) => {
|
|
|
27502
27506
|
].join("/"),
|
|
27503
27507
|
[refs.definitionPath]: {
|
|
27504
27508
|
...definitions,
|
|
27505
|
-
[name]:
|
|
27509
|
+
[name]: main
|
|
27506
27510
|
}
|
|
27507
27511
|
};
|
|
27508
27512
|
if (refs.target === "jsonSchema7") {
|
|
@@ -30406,6 +30410,16 @@ var WorkspaceResolutionError = class extends LifecycleError {
|
|
|
30406
30410
|
this.name = "WorkspaceResolutionError";
|
|
30407
30411
|
}
|
|
30408
30412
|
};
|
|
30413
|
+
var QueueDirtyTreeError = class extends LifecycleError {
|
|
30414
|
+
constructor(cwd, porcelain) {
|
|
30415
|
+
super(
|
|
30416
|
+
"QUEUE_DIRTY_TREE",
|
|
30417
|
+
`Queue refuses to start: working tree at ${cwd} is dirty. Commit or stash first.
|
|
30418
|
+
${porcelain.trim()}`
|
|
30419
|
+
);
|
|
30420
|
+
this.name = "QueueDirtyTreeError";
|
|
30421
|
+
}
|
|
30422
|
+
};
|
|
30409
30423
|
|
|
30410
30424
|
// src/core/domain/lifecycle/inbox-lifecycle-service.ts
|
|
30411
30425
|
var InboxLifecycleService = class {
|
|
@@ -30510,32 +30524,6 @@ function generateId(prefix) {
|
|
|
30510
30524
|
return `${prefix}-${Date.now()}-${idCounter}`;
|
|
30511
30525
|
}
|
|
30512
30526
|
|
|
30513
|
-
// src/core/domain/lifecycle/sanitize.ts
|
|
30514
|
-
var LEAK_MARKERS = [
|
|
30515
|
-
"</resumePoint>",
|
|
30516
|
-
"<parameter name",
|
|
30517
|
-
"<parameter ",
|
|
30518
|
-
"<invoke ",
|
|
30519
|
-
"<invoke>",
|
|
30520
|
-
"</invoke>",
|
|
30521
|
-
"<",
|
|
30522
|
-
"</",
|
|
30523
|
-
"<function_calls>",
|
|
30524
|
-
"</function_calls>"
|
|
30525
|
-
];
|
|
30526
|
-
function stripToolCallLeak(text) {
|
|
30527
|
-
if (!text) return "";
|
|
30528
|
-
let earliest = -1;
|
|
30529
|
-
for (const marker of LEAK_MARKERS) {
|
|
30530
|
-
const idx = text.indexOf(marker);
|
|
30531
|
-
if (idx !== -1 && (earliest === -1 || idx < earliest)) {
|
|
30532
|
-
earliest = idx;
|
|
30533
|
-
}
|
|
30534
|
-
}
|
|
30535
|
-
if (earliest === -1) return text;
|
|
30536
|
-
return text.slice(0, earliest).trimEnd();
|
|
30537
|
-
}
|
|
30538
|
-
|
|
30539
30527
|
// src/core/domain/lifecycle/conversation-lifecycle-service.ts
|
|
30540
30528
|
var ConversationLifecycleService = class {
|
|
30541
30529
|
constructor(db, conversations, tasks, sessions) {
|
|
@@ -30608,17 +30596,16 @@ var ConversationLifecycleService = class {
|
|
|
30608
30596
|
const tx = this.db.transaction(() => {
|
|
30609
30597
|
const conv = this.conversations.get(id);
|
|
30610
30598
|
if (!conv) throw new ConversationNotFoundError(id);
|
|
30611
|
-
const cleanDecision = stripToolCallLeak(input.decision);
|
|
30612
30599
|
this.conversations.addMessage({
|
|
30613
30600
|
conversationId: id,
|
|
30614
30601
|
authorName: input.author,
|
|
30615
|
-
content:
|
|
30602
|
+
content: input.decision,
|
|
30616
30603
|
messageType: "decision"
|
|
30617
30604
|
});
|
|
30618
30605
|
const decidedAt = now();
|
|
30619
30606
|
const updated = this.conversations.update(id, {
|
|
30620
30607
|
status: "decided",
|
|
30621
|
-
decisionSummary:
|
|
30608
|
+
decisionSummary: input.decision,
|
|
30622
30609
|
decidedAt
|
|
30623
30610
|
});
|
|
30624
30611
|
const actions = (input.actions ?? []).map(
|
|
@@ -30654,7 +30641,12 @@ var ConversationLifecycleService = class {
|
|
|
30654
30641
|
"only decided or closed conversations can reopen"
|
|
30655
30642
|
);
|
|
30656
30643
|
}
|
|
30657
|
-
const updated = this.conversations.update(id, {
|
|
30644
|
+
const updated = this.conversations.update(id, {
|
|
30645
|
+
status: "discussing",
|
|
30646
|
+
closedAt: null,
|
|
30647
|
+
decidedAt: null,
|
|
30648
|
+
decisionSummary: null
|
|
30649
|
+
});
|
|
30658
30650
|
this.conversations.emitLifecycleEvent(id, "conversation.reopen", "system", now());
|
|
30659
30651
|
return updated;
|
|
30660
30652
|
});
|
|
@@ -30740,8 +30732,7 @@ var SessionLifecycleService = class {
|
|
|
30740
30732
|
throw new SessionStatusError(id, session.status, "only active sessions can end");
|
|
30741
30733
|
}
|
|
30742
30734
|
const endedAt = now();
|
|
30743
|
-
const
|
|
30744
|
-
const decisionSummary = stripToolCallLeak(rawSummary) || "Session ended";
|
|
30735
|
+
const decisionSummary = input.decisionSummary ?? input.handoff.resumePoint ?? "Session ended";
|
|
30745
30736
|
const closedConversationIds = [];
|
|
30746
30737
|
const linkedConvs = this.conversations.findByLink("session", id);
|
|
30747
30738
|
for (const conv of linkedConvs) {
|
|
@@ -30770,6 +30761,36 @@ var SessionLifecycleService = class {
|
|
|
30770
30761
|
});
|
|
30771
30762
|
return tx();
|
|
30772
30763
|
}
|
|
30764
|
+
abandonSession(id, reason) {
|
|
30765
|
+
const tx = this.db.transaction(() => {
|
|
30766
|
+
const session = this.sessions.get(id);
|
|
30767
|
+
if (!session) throw new SessionNotFoundError(id);
|
|
30768
|
+
if (session.status !== "active") {
|
|
30769
|
+
throw new SessionStatusError(id, session.status, "only active sessions can be abandoned");
|
|
30770
|
+
}
|
|
30771
|
+
const endedAt = now();
|
|
30772
|
+
const decisionSummary = `Abandoned: ${reason}`;
|
|
30773
|
+
const closedConversationIds = [];
|
|
30774
|
+
const linkedConvs = this.conversations.findByLink("session", id);
|
|
30775
|
+
for (const conv of linkedConvs) {
|
|
30776
|
+
if (conv.status === "closed") continue;
|
|
30777
|
+
this.conversations.update(conv.id, {
|
|
30778
|
+
status: "closed",
|
|
30779
|
+
decisionSummary,
|
|
30780
|
+
closedAt: endedAt
|
|
30781
|
+
});
|
|
30782
|
+
closedConversationIds.push(conv.id);
|
|
30783
|
+
}
|
|
30784
|
+
const handoff = { ...session.handoff ?? {}, failureReason: reason };
|
|
30785
|
+
const updated = this.sessions.update(id, {
|
|
30786
|
+
status: "completed",
|
|
30787
|
+
endedAt,
|
|
30788
|
+
handoff
|
|
30789
|
+
});
|
|
30790
|
+
return { session: updated, closedConversationIds };
|
|
30791
|
+
});
|
|
30792
|
+
return tx();
|
|
30793
|
+
}
|
|
30773
30794
|
checkpointSession(id, input) {
|
|
30774
30795
|
const session = this.sessions.get(id);
|
|
30775
30796
|
if (!session) throw new SessionNotFoundError(id);
|
|
@@ -30796,9 +30817,947 @@ var SessionLifecycleService = class {
|
|
|
30796
30817
|
}
|
|
30797
30818
|
};
|
|
30798
30819
|
|
|
30820
|
+
// src/core/domain/lifecycle/queue-lifecycle-service.ts
|
|
30821
|
+
var path2 = __toESM(require("node:path"));
|
|
30822
|
+
|
|
30823
|
+
// src/core/utils/lines.ts
|
|
30824
|
+
function splitLines(content) {
|
|
30825
|
+
return content.split(/\r?\n/);
|
|
30826
|
+
}
|
|
30827
|
+
|
|
30828
|
+
// src/core/domain/auto-safe-validator.ts
|
|
30829
|
+
var AUTO_SAFE_LABEL = "auto-safe";
|
|
30830
|
+
var AUTO_SAFE_SCOPE_HOURS_CEILING = 3;
|
|
30831
|
+
function validateAutoSafeTask(task) {
|
|
30832
|
+
const errors = [];
|
|
30833
|
+
const body = (task.body ?? "").trim();
|
|
30834
|
+
if (!body) {
|
|
30835
|
+
errors.push("Task body is empty \u2014 auto-safe requires AC, File Pointers, and Scope sections");
|
|
30836
|
+
return { valid: false, errors };
|
|
30837
|
+
}
|
|
30838
|
+
const ac = extractSection(body, /^acceptance(?:\s+criteria)?$/i);
|
|
30839
|
+
const filePointers = extractSection(body, /^file\s+pointers$/i);
|
|
30840
|
+
const scope = extractSection(body, /^scope$/i);
|
|
30841
|
+
if (!ac.trim()) {
|
|
30842
|
+
errors.push("Missing ## Acceptance (or ## Acceptance Criteria) section");
|
|
30843
|
+
} else if (!hasVerifiableShellCommand(ac)) {
|
|
30844
|
+
errors.push(
|
|
30845
|
+
"## Acceptance has no verifiable shell command (need `pnpm `, `node `, or a ```bash code block)"
|
|
30846
|
+
);
|
|
30847
|
+
}
|
|
30848
|
+
if (!filePointers.trim()) {
|
|
30849
|
+
errors.push("Missing ## File Pointers section");
|
|
30850
|
+
} else if (!hasConcretePath(filePointers)) {
|
|
30851
|
+
errors.push("## File Pointers has no concrete path (need at least one .ts/.md/.json/etc)");
|
|
30852
|
+
}
|
|
30853
|
+
if (!scope.trim()) {
|
|
30854
|
+
errors.push("Missing ## Scope section");
|
|
30855
|
+
} else {
|
|
30856
|
+
const upper = parseScopeHours(scope);
|
|
30857
|
+
if (upper === null) {
|
|
30858
|
+
errors.push('## Scope has no parseable hour estimate (e.g. "~2-3h", "2h", "1.5h")');
|
|
30859
|
+
} else if (upper > AUTO_SAFE_SCOPE_HOURS_CEILING) {
|
|
30860
|
+
errors.push(
|
|
30861
|
+
`## Scope estimate ${upper}h exceeds auto-safe ceiling of ${AUTO_SAFE_SCOPE_HOURS_CEILING}h`
|
|
30862
|
+
);
|
|
30863
|
+
}
|
|
30864
|
+
}
|
|
30865
|
+
if (mentionsBuildSensitive(body) && !hasSmokeStep(ac)) {
|
|
30866
|
+
errors.push(
|
|
30867
|
+
"## Acceptance must include a smoke step (body mentions build:mcp / build:cli / loader / asset copy)"
|
|
30868
|
+
);
|
|
30869
|
+
}
|
|
30870
|
+
return { valid: errors.length === 0, errors };
|
|
30871
|
+
}
|
|
30872
|
+
function extractSection(body, headingMatcher) {
|
|
30873
|
+
const lines = body.split(/\r?\n/);
|
|
30874
|
+
const out = [];
|
|
30875
|
+
let inSection = false;
|
|
30876
|
+
for (const line of lines) {
|
|
30877
|
+
const headingMatch = /^##\s+(.+?)\s*$/.exec(line);
|
|
30878
|
+
if (headingMatch) {
|
|
30879
|
+
if (inSection) break;
|
|
30880
|
+
if (headingMatcher.test(headingMatch[1])) {
|
|
30881
|
+
inSection = true;
|
|
30882
|
+
continue;
|
|
30883
|
+
}
|
|
30884
|
+
}
|
|
30885
|
+
if (inSection) out.push(line);
|
|
30886
|
+
}
|
|
30887
|
+
return out.join("\n");
|
|
30888
|
+
}
|
|
30889
|
+
function hasVerifiableShellCommand(section) {
|
|
30890
|
+
if (/(?:^|[\s`])(?:pnpm|node)\s+\S/m.test(section)) return true;
|
|
30891
|
+
if (/```bash[\s\S]*?```/.test(section)) return true;
|
|
30892
|
+
return false;
|
|
30893
|
+
}
|
|
30894
|
+
function hasConcretePath(section) {
|
|
30895
|
+
return /[\w./\\-]+\.(?:ts|tsx|js|mjs|cjs|mts|json|md|sh|yml|yaml)\b/.test(section);
|
|
30896
|
+
}
|
|
30897
|
+
function parseScopeHours(section) {
|
|
30898
|
+
const match = /(\d+(?:\.\d+)?)\s*(?:[-–]\s*(\d+(?:\.\d+)?))?\s*h\b/i.exec(section);
|
|
30899
|
+
if (!match) return null;
|
|
30900
|
+
return parseFloat(match[2] ?? match[1]);
|
|
30901
|
+
}
|
|
30902
|
+
function mentionsBuildSensitive(body) {
|
|
30903
|
+
return /build:(?:mcp|cli)|\bloader\b|asset\s+cop/i.test(body);
|
|
30904
|
+
}
|
|
30905
|
+
function hasSmokeStep(ac) {
|
|
30906
|
+
return /\bsmoke\b/i.test(ac) || /pnpm\s+run\s+build:(?:mcp|cli)/i.test(ac);
|
|
30907
|
+
}
|
|
30908
|
+
|
|
30909
|
+
// src/core/domain/lifecycle/ac-parser.ts
|
|
30910
|
+
function parseAcCommands(body) {
|
|
30911
|
+
const section = extractAcSection(body);
|
|
30912
|
+
if (!section) return [];
|
|
30913
|
+
const commands = [];
|
|
30914
|
+
const bashBlockRe = /```bash\s*\n([\s\S]*?)```/g;
|
|
30915
|
+
let cursor = 0;
|
|
30916
|
+
let match;
|
|
30917
|
+
while ((match = bashBlockRe.exec(section)) !== null) {
|
|
30918
|
+
const before = section.slice(cursor, match.index);
|
|
30919
|
+
extractFromProse(before, commands);
|
|
30920
|
+
extractFromBashBlock(match[1], commands);
|
|
30921
|
+
cursor = match.index + match[0].length;
|
|
30922
|
+
}
|
|
30923
|
+
extractFromProse(section.slice(cursor), commands);
|
|
30924
|
+
return commands;
|
|
30925
|
+
}
|
|
30926
|
+
function extractAcSection(body) {
|
|
30927
|
+
const lines = splitLines(body);
|
|
30928
|
+
const startIdx = lines.findIndex((l) => /^##\s+Acceptance\b/i.test(l));
|
|
30929
|
+
if (startIdx === -1) return null;
|
|
30930
|
+
let endIdx = lines.length;
|
|
30931
|
+
let inFence = false;
|
|
30932
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
30933
|
+
if (/^```/.test(lines[i])) {
|
|
30934
|
+
inFence = !inFence;
|
|
30935
|
+
continue;
|
|
30936
|
+
}
|
|
30937
|
+
if (!inFence && /^#{1,2}\s+/.test(lines[i])) {
|
|
30938
|
+
endIdx = i;
|
|
30939
|
+
break;
|
|
30940
|
+
}
|
|
30941
|
+
}
|
|
30942
|
+
return lines.slice(startIdx + 1, endIdx).join("\n");
|
|
30943
|
+
}
|
|
30944
|
+
function extractFromBashBlock(content, out) {
|
|
30945
|
+
for (const raw of splitLines(content)) {
|
|
30946
|
+
const line = raw.trim();
|
|
30947
|
+
if (!line || line.startsWith("#")) continue;
|
|
30948
|
+
out.push(line);
|
|
30949
|
+
}
|
|
30950
|
+
}
|
|
30951
|
+
function extractFromProse(prose, out) {
|
|
30952
|
+
const inlineRe = /`((?:pnpm|node)\s[^`]+)`/g;
|
|
30953
|
+
for (const line of splitLines(prose)) {
|
|
30954
|
+
let matched = false;
|
|
30955
|
+
let m;
|
|
30956
|
+
inlineRe.lastIndex = 0;
|
|
30957
|
+
while ((m = inlineRe.exec(line)) !== null) {
|
|
30958
|
+
out.push(m[1].trim());
|
|
30959
|
+
matched = true;
|
|
30960
|
+
}
|
|
30961
|
+
if (matched) continue;
|
|
30962
|
+
const stripped = line.replace(/^\s*(?:[-*]\s+(?:\[[xX\s]?\]\s+)?)?/, "");
|
|
30963
|
+
if (/^(pnpm|node)\s/.test(stripped)) out.push(stripped.trim());
|
|
30964
|
+
}
|
|
30965
|
+
}
|
|
30966
|
+
|
|
30967
|
+
// src/core/executor/prewarm-compose.ts
|
|
30968
|
+
function extractFilePointersSection(taskBody) {
|
|
30969
|
+
const headingIdx = taskBody.indexOf("## File Pointers");
|
|
30970
|
+
if (headingIdx === -1) return null;
|
|
30971
|
+
const afterHeading = taskBody.slice(headingIdx + "## File Pointers".length);
|
|
30972
|
+
const nextHeadingMatch = afterHeading.match(/\n##\s/);
|
|
30973
|
+
const sectionEnd = nextHeadingMatch?.index ?? afterHeading.length;
|
|
30974
|
+
return afterHeading.slice(0, sectionEnd);
|
|
30975
|
+
}
|
|
30976
|
+
function parseFilePointers(section) {
|
|
30977
|
+
const pointers = [];
|
|
30978
|
+
const lineRe = /^-\s+(?:`([^`]+)`|(\S+))/gm;
|
|
30979
|
+
let m;
|
|
30980
|
+
while ((m = lineRe.exec(section)) !== null) {
|
|
30981
|
+
const raw = m[1] ?? m[2];
|
|
30982
|
+
if (!raw) continue;
|
|
30983
|
+
const colonIdx = raw.lastIndexOf(":");
|
|
30984
|
+
if (colonIdx === -1) {
|
|
30985
|
+
pointers.push({ filePath: raw, startLine: void 0, endLine: void 0 });
|
|
30986
|
+
continue;
|
|
30987
|
+
}
|
|
30988
|
+
const afterColon = raw.slice(colonIdx + 1);
|
|
30989
|
+
const rangeMatch = afterColon.match(/^(\d+)(?:-(\d+))?$/);
|
|
30990
|
+
if (!rangeMatch) {
|
|
30991
|
+
pointers.push({ filePath: raw, startLine: void 0, endLine: void 0 });
|
|
30992
|
+
continue;
|
|
30993
|
+
}
|
|
30994
|
+
const filePath = raw.slice(0, colonIdx);
|
|
30995
|
+
const startLine = parseInt(rangeMatch[1], 10);
|
|
30996
|
+
const endLine = rangeMatch[2] ? parseInt(rangeMatch[2], 10) : startLine;
|
|
30997
|
+
pointers.push({ filePath, startLine, endLine });
|
|
30998
|
+
}
|
|
30999
|
+
return pointers;
|
|
31000
|
+
}
|
|
31001
|
+
|
|
31002
|
+
// src/core/executor/queue-claude-spawn.ts
|
|
31003
|
+
var QUEUE_SPAWN_TOOLS = "Read,Edit,Write,Bash,Grep,Glob";
|
|
31004
|
+
var QUEUE_SPAWN_ALLOWED_TOOLS = "Bash(pnpm *) Bash(node *) Bash(git diff*) Bash(git status*)";
|
|
31005
|
+
var TOKENS_PER_CHAR = 3.5;
|
|
31006
|
+
function computeToolSchemaTokens() {
|
|
31007
|
+
const totalChars = QUEUE_SPAWN_TOOLS.length + QUEUE_SPAWN_ALLOWED_TOOLS.length;
|
|
31008
|
+
return Math.ceil(totalChars / TOKENS_PER_CHAR);
|
|
31009
|
+
}
|
|
31010
|
+
|
|
31011
|
+
// src/core/domain/lifecycle/queue-start-preflight.ts
|
|
31012
|
+
var path = __toESM(require("node:path"));
|
|
31013
|
+
async function validateQueueStartPreflight(input) {
|
|
31014
|
+
const { tasks, repoCwd, baseRef, worktreesParentDir, branchPrefix, fns } = input;
|
|
31015
|
+
const globalErrors = [];
|
|
31016
|
+
const [baseSha, parentExists, ghOk] = await Promise.all([
|
|
31017
|
+
fns.resolveRef(repoCwd, baseRef),
|
|
31018
|
+
fns.pathExists(worktreesParentDir),
|
|
31019
|
+
fns.ghAuthStatus()
|
|
31020
|
+
]);
|
|
31021
|
+
if (baseSha === null) {
|
|
31022
|
+
globalErrors.push(`baseRef "${baseRef}" is unresolvable in ${repoCwd}`);
|
|
31023
|
+
}
|
|
31024
|
+
if (!parentExists) {
|
|
31025
|
+
globalErrors.push(`worktrees parent dir does not exist: ${worktreesParentDir}`);
|
|
31026
|
+
} else {
|
|
31027
|
+
const writable = await fns.isWritable(worktreesParentDir);
|
|
31028
|
+
if (!writable) {
|
|
31029
|
+
globalErrors.push(`worktrees parent dir is not writable: ${worktreesParentDir}`);
|
|
31030
|
+
}
|
|
31031
|
+
}
|
|
31032
|
+
if (!ghOk) {
|
|
31033
|
+
globalErrors.push("gh auth status failed \u2014 `gh` is not authenticated (queue start ends with PR create)");
|
|
31034
|
+
}
|
|
31035
|
+
const failures = [];
|
|
31036
|
+
for (const task of tasks) {
|
|
31037
|
+
const reasons = await validateTask(task, repoCwd, baseSha, worktreesParentDir, branchPrefix, fns);
|
|
31038
|
+
if (reasons.length > 0) {
|
|
31039
|
+
failures.push({ taskId: task.id, reasons });
|
|
31040
|
+
}
|
|
31041
|
+
}
|
|
31042
|
+
return {
|
|
31043
|
+
ok: globalErrors.length === 0 && failures.length === 0,
|
|
31044
|
+
baseSha,
|
|
31045
|
+
failures,
|
|
31046
|
+
globalErrors
|
|
31047
|
+
};
|
|
31048
|
+
}
|
|
31049
|
+
async function validateTask(task, repoCwd, baseSha, worktreesParentDir, branchPrefix, fns) {
|
|
31050
|
+
const reasons = [];
|
|
31051
|
+
if (!task.labels.includes(AUTO_SAFE_LABEL)) {
|
|
31052
|
+
reasons.push(`missing label "${AUTO_SAFE_LABEL}"`);
|
|
31053
|
+
}
|
|
31054
|
+
const structural = validateAutoSafeTask(task);
|
|
31055
|
+
if (!structural.valid) {
|
|
31056
|
+
for (const err of structural.errors) {
|
|
31057
|
+
reasons.push(`structural: ${err}`);
|
|
31058
|
+
}
|
|
31059
|
+
}
|
|
31060
|
+
const worktreePath = path.join(worktreesParentDir, task.id);
|
|
31061
|
+
const branchName = `${branchPrefix}${task.id}`;
|
|
31062
|
+
const [worktreeExists, branchAlreadyThere] = await Promise.all([
|
|
31063
|
+
fns.pathExists(worktreePath),
|
|
31064
|
+
fns.branchExists(repoCwd, branchName)
|
|
31065
|
+
]);
|
|
31066
|
+
if (worktreeExists) {
|
|
31067
|
+
reasons.push(`worktree path already exists: ${worktreePath} (run cleanup_worktree_orphans first)`);
|
|
31068
|
+
}
|
|
31069
|
+
if (branchAlreadyThere) {
|
|
31070
|
+
reasons.push(`branch already exists: ${branchName}`);
|
|
31071
|
+
}
|
|
31072
|
+
if (baseSha !== null && structural.valid) {
|
|
31073
|
+
const section = extractFilePointersSection(task.body ?? "");
|
|
31074
|
+
if (section !== null) {
|
|
31075
|
+
const pointers = parseFilePointers(section);
|
|
31076
|
+
for (const pointer of pointers) {
|
|
31077
|
+
if (pointer.startLine === void 0) continue;
|
|
31078
|
+
const exists = await fns.fileExistsAtSha(repoCwd, baseSha, pointer.filePath);
|
|
31079
|
+
if (!exists) {
|
|
31080
|
+
reasons.push(`File Pointer with range references missing file at baseSha: ${pointer.filePath}`);
|
|
31081
|
+
}
|
|
31082
|
+
}
|
|
31083
|
+
}
|
|
31084
|
+
}
|
|
31085
|
+
return reasons;
|
|
31086
|
+
}
|
|
31087
|
+
|
|
31088
|
+
// src/core/domain/lifecycle/queue-lifecycle-service.ts
|
|
31089
|
+
var DEFAULT_MAX_COST_PER_TASK = 1.5;
|
|
31090
|
+
var DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
31091
|
+
var DEFAULT_AC_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
31092
|
+
var AUTO_FAILED_LABEL = "auto-failed";
|
|
31093
|
+
var TOKENS_PER_CHAR2 = 3.5;
|
|
31094
|
+
function estimateMcpTokens(content) {
|
|
31095
|
+
return Math.ceil(content.length / TOKENS_PER_CHAR2);
|
|
31096
|
+
}
|
|
31097
|
+
var TRANSIENT_PATTERNS = [
|
|
31098
|
+
/rate.?limit/i,
|
|
31099
|
+
/overloaded/i,
|
|
31100
|
+
/service unavailable/i,
|
|
31101
|
+
/\bECONNRESET\b/,
|
|
31102
|
+
/\bETIMEDOUT\b/,
|
|
31103
|
+
/\bECONNREFUSED\b/,
|
|
31104
|
+
/out of memory/i,
|
|
31105
|
+
/\bOOM\b/,
|
|
31106
|
+
/timed out/i
|
|
31107
|
+
];
|
|
31108
|
+
function isTransientMessage(msg) {
|
|
31109
|
+
return TRANSIENT_PATTERNS.some((p) => p.test(msg));
|
|
31110
|
+
}
|
|
31111
|
+
var QueueLifecycleService = class {
|
|
31112
|
+
constructor(tasks, workspaces, conversations, sessions, runtime) {
|
|
31113
|
+
this.tasks = tasks;
|
|
31114
|
+
this.workspaces = workspaces;
|
|
31115
|
+
this.conversations = conversations;
|
|
31116
|
+
this.sessions = sessions;
|
|
31117
|
+
this.runtime = runtime;
|
|
31118
|
+
}
|
|
31119
|
+
tasks;
|
|
31120
|
+
workspaces;
|
|
31121
|
+
conversations;
|
|
31122
|
+
sessions;
|
|
31123
|
+
runtime;
|
|
31124
|
+
async runQueue(opts) {
|
|
31125
|
+
const ws = this.workspaces.get(opts.workspaceId);
|
|
31126
|
+
if (!ws) {
|
|
31127
|
+
throw new WorkspaceResolutionError(`workspace ${opts.workspaceId} not found`);
|
|
31128
|
+
}
|
|
31129
|
+
const porcelain = await this.runtime.gitStatusPorcelain(ws.cwd);
|
|
31130
|
+
if (porcelain.trim()) throw new QueueDirtyTreeError(ws.cwd, porcelain);
|
|
31131
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
31132
|
+
const branch = await this.runtime.gitCurrentBranch(ws.cwd);
|
|
31133
|
+
const commitSha = await this.runtime.gitHeadSha(ws.cwd);
|
|
31134
|
+
const eligible = this.collectEligibleTasks(ws);
|
|
31135
|
+
const taskCap = opts.maxTasks ?? eligible.length;
|
|
31136
|
+
const tasks = eligible.slice(0, taskCap);
|
|
31137
|
+
const queueRunId = `${Date.now()}-${Math.floor(Math.random() * 1e6).toString(36)}`;
|
|
31138
|
+
const artifactDir = path2.join(this.runtime.artifactsDir, `queue-${queueRunId}`);
|
|
31139
|
+
if (opts.dryRun) {
|
|
31140
|
+
return {
|
|
31141
|
+
done: [],
|
|
31142
|
+
failed: [],
|
|
31143
|
+
skipped: tasks,
|
|
31144
|
+
totalCostUsd: 0,
|
|
31145
|
+
halted: false,
|
|
31146
|
+
haltReason: null,
|
|
31147
|
+
haltCode: null,
|
|
31148
|
+
queueRunId,
|
|
31149
|
+
artifactDir
|
|
31150
|
+
};
|
|
31151
|
+
}
|
|
31152
|
+
await this.runtime.mkdir(artifactDir);
|
|
31153
|
+
const maxCostPerTask = opts.maxCostPerTask ?? DEFAULT_MAX_COST_PER_TASK;
|
|
31154
|
+
const maxBudgetUsd = round2(maxCostPerTask * 0.95);
|
|
31155
|
+
const model = opts.model ?? DEFAULT_MODEL;
|
|
31156
|
+
const claudeBin = opts.claudeBin ?? "claude";
|
|
31157
|
+
const acTimeoutMs = opts.acTimeoutMs ?? DEFAULT_AC_TIMEOUT_MS;
|
|
31158
|
+
let mcpTokensPerSpawn = 0;
|
|
31159
|
+
try {
|
|
31160
|
+
const mcpConfig = await this.runtime.readFile(this.runtime.queueMcpEmptyPath);
|
|
31161
|
+
mcpTokensPerSpawn = estimateMcpTokens(mcpConfig);
|
|
31162
|
+
} catch {
|
|
31163
|
+
mcpTokensPerSpawn = 0;
|
|
31164
|
+
}
|
|
31165
|
+
const taskOutcomes = [];
|
|
31166
|
+
const done = [];
|
|
31167
|
+
const failed = [];
|
|
31168
|
+
let totalCostUsd = 0;
|
|
31169
|
+
let halted = false;
|
|
31170
|
+
let haltReason = null;
|
|
31171
|
+
let haltCode = null;
|
|
31172
|
+
let skipped = [];
|
|
31173
|
+
const profile = this.runtime.mcpProfile;
|
|
31174
|
+
let queueCacheReadTokens = 0;
|
|
31175
|
+
let queueTotalInputTokens = 0;
|
|
31176
|
+
let hasTokenData = false;
|
|
31177
|
+
let queueFilesTouched = 0;
|
|
31178
|
+
let queueNewFilesCreated = 0;
|
|
31179
|
+
const profileOutcomes = {};
|
|
31180
|
+
const bumpProfile = (outcome) => {
|
|
31181
|
+
if (!profileOutcomes[profile]) profileOutcomes[profile] = { success: 0, failed: 0 };
|
|
31182
|
+
profileOutcomes[profile][outcome] += 1;
|
|
31183
|
+
};
|
|
31184
|
+
try {
|
|
31185
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
31186
|
+
const task = tasks[i];
|
|
31187
|
+
if (opts.maxQueueCost !== void 0 && totalCostUsd + maxCostPerTask > opts.maxQueueCost) {
|
|
31188
|
+
halted = true;
|
|
31189
|
+
haltCode = "queue-cost-cap";
|
|
31190
|
+
haltReason = `queue-cost-cap-exceeded: cumulative ${totalCostUsd.toFixed(
|
|
31191
|
+
2
|
|
31192
|
+
)} + per-task ${maxCostPerTask.toFixed(2)} > ${opts.maxQueueCost.toFixed(2)}`;
|
|
31193
|
+
break;
|
|
31194
|
+
}
|
|
31195
|
+
const taskDir = path2.join(artifactDir, "tasks", task.id);
|
|
31196
|
+
await this.runtime.mkdir(taskDir);
|
|
31197
|
+
const promptText = task.body ?? "";
|
|
31198
|
+
await this.runtime.writeFile(path2.join(taskDir, "prompt.md"), promptText);
|
|
31199
|
+
const startResult = this.sessions.startSession({
|
|
31200
|
+
projectId: ws.projectId,
|
|
31201
|
+
workspaceId: ws.id,
|
|
31202
|
+
taskId: task.id
|
|
31203
|
+
});
|
|
31204
|
+
const sessionId = startResult.session.id;
|
|
31205
|
+
const taskModel = resolveModelForTask(task, model);
|
|
31206
|
+
const spawnAttempt = await this.spawnWithRetry({
|
|
31207
|
+
taskBody: promptText,
|
|
31208
|
+
cwd: ws.cwd,
|
|
31209
|
+
model: taskModel,
|
|
31210
|
+
maxBudgetUsd,
|
|
31211
|
+
queueMcpEmptyPath: this.runtime.queueMcpEmptyPath,
|
|
31212
|
+
claudeBin
|
|
31213
|
+
});
|
|
31214
|
+
if (spawnAttempt.error) {
|
|
31215
|
+
const reason = `spawn-error: ${spawnAttempt.error.message}`;
|
|
31216
|
+
const errStats = await this.writeDiffArtifact(taskDir, ws.cwd);
|
|
31217
|
+
queueFilesTouched += errStats.filesTouched;
|
|
31218
|
+
queueNewFilesCreated += errStats.newFiles;
|
|
31219
|
+
await this.failTask(task, sessionId, reason, taskDir);
|
|
31220
|
+
bumpProfile("failed");
|
|
31221
|
+
taskOutcomes.push({ id: task.id, outcome: "FAILED", reason });
|
|
31222
|
+
failed.push(task);
|
|
31223
|
+
halted = true;
|
|
31224
|
+
haltCode = "spawn-error";
|
|
31225
|
+
haltReason = reason;
|
|
31226
|
+
break;
|
|
31227
|
+
}
|
|
31228
|
+
const spawn = spawnAttempt.output;
|
|
31229
|
+
const cacheRead = spawn.cacheReadInputTokens ?? null;
|
|
31230
|
+
const inputTokens = spawn.totalInputTokens ?? null;
|
|
31231
|
+
if (cacheRead !== null) queueCacheReadTokens += cacheRead;
|
|
31232
|
+
if (inputTokens !== null) {
|
|
31233
|
+
queueTotalInputTokens += inputTokens;
|
|
31234
|
+
hasTokenData = true;
|
|
31235
|
+
}
|
|
31236
|
+
await this.runtime.writeFile(path2.join(taskDir, "claude.json"), spawn.rawJson);
|
|
31237
|
+
const diffStats = await this.writeDiffArtifact(taskDir, ws.cwd);
|
|
31238
|
+
queueFilesTouched += diffStats.filesTouched;
|
|
31239
|
+
queueNewFilesCreated += diffStats.newFiles;
|
|
31240
|
+
totalCostUsd = round4(totalCostUsd + spawn.totalCostUsd);
|
|
31241
|
+
if (spawn.isError) {
|
|
31242
|
+
const reason = `claude-error: ${spawn.resultText.slice(0, 500)}`;
|
|
31243
|
+
await this.failTask(task, sessionId, reason, taskDir);
|
|
31244
|
+
bumpProfile("failed");
|
|
31245
|
+
taskOutcomes.push({ id: task.id, outcome: "FAILED", costUsd: spawn.totalCostUsd, reason });
|
|
31246
|
+
failed.push(task);
|
|
31247
|
+
halted = true;
|
|
31248
|
+
haltCode = "claude-error";
|
|
31249
|
+
haltReason = reason;
|
|
31250
|
+
break;
|
|
31251
|
+
}
|
|
31252
|
+
const acReason = await this.runAcCommands(promptText, ws.cwd, taskDir, acTimeoutMs);
|
|
31253
|
+
if (acReason) {
|
|
31254
|
+
await this.failTask(task, sessionId, acReason, taskDir);
|
|
31255
|
+
bumpProfile("failed");
|
|
31256
|
+
taskOutcomes.push({ id: task.id, outcome: "FAILED", costUsd: spawn.totalCostUsd, reason: acReason });
|
|
31257
|
+
failed.push(task);
|
|
31258
|
+
halted = true;
|
|
31259
|
+
haltCode = "ac-failed";
|
|
31260
|
+
haltReason = acReason;
|
|
31261
|
+
break;
|
|
31262
|
+
}
|
|
31263
|
+
if (spawn.totalCostUsd > maxCostPerTask) {
|
|
31264
|
+
const reason = `cost-cap-exceeded: ${spawn.totalCostUsd.toFixed(
|
|
31265
|
+
2
|
|
31266
|
+
)} > ${maxCostPerTask.toFixed(2)}`;
|
|
31267
|
+
await this.failTask(task, sessionId, reason, taskDir);
|
|
31268
|
+
bumpProfile("failed");
|
|
31269
|
+
taskOutcomes.push({ id: task.id, outcome: "FAILED", costUsd: spawn.totalCostUsd, reason });
|
|
31270
|
+
failed.push(task);
|
|
31271
|
+
halted = true;
|
|
31272
|
+
haltCode = "cost-cap";
|
|
31273
|
+
haltReason = reason;
|
|
31274
|
+
break;
|
|
31275
|
+
}
|
|
31276
|
+
this.sessions.endSession(sessionId, {
|
|
31277
|
+
handoff: {
|
|
31278
|
+
resumePoint: `auto-completed by queue runner (queue ${queueRunId})`,
|
|
31279
|
+
decisions: [`Queue ${queueRunId} marked ${task.id} DONE \u2014 diff at ${taskDir}/diff.patch`]
|
|
31280
|
+
}
|
|
31281
|
+
});
|
|
31282
|
+
bumpProfile("success");
|
|
31283
|
+
taskOutcomes.push({ id: task.id, outcome: "DONE", costUsd: spawn.totalCostUsd, numTurns: spawn.numTurns });
|
|
31284
|
+
done.push(task);
|
|
31285
|
+
}
|
|
31286
|
+
} finally {
|
|
31287
|
+
skipped = tasks.slice(done.length + failed.length);
|
|
31288
|
+
for (const t of skipped) {
|
|
31289
|
+
taskOutcomes.push({ id: t.id, outcome: "SKIPPED" });
|
|
31290
|
+
}
|
|
31291
|
+
const cacheHitEstimate = hasTokenData ? Math.min(1, Math.max(0, queueCacheReadTokens / Math.max(queueTotalInputTokens, 1))) : null;
|
|
31292
|
+
const runMeta = {
|
|
31293
|
+
queueRunId,
|
|
31294
|
+
workspaceId: opts.workspaceId,
|
|
31295
|
+
branch,
|
|
31296
|
+
commitSha,
|
|
31297
|
+
model,
|
|
31298
|
+
claudeBin,
|
|
31299
|
+
startedAt,
|
|
31300
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31301
|
+
maxCostPerTask,
|
|
31302
|
+
maxQueueCost: opts.maxQueueCost ?? null,
|
|
31303
|
+
maxTasks: opts.maxTasks ?? null,
|
|
31304
|
+
totalCostUsd,
|
|
31305
|
+
halted,
|
|
31306
|
+
haltReason,
|
|
31307
|
+
haltCode,
|
|
31308
|
+
mcp_tokens_per_spawn: mcpTokensPerSpawn,
|
|
31309
|
+
tool_schema_tokens_total: computeToolSchemaTokens(),
|
|
31310
|
+
mcp_profile_used: profile,
|
|
31311
|
+
cache_read_input_tokens: queueCacheReadTokens,
|
|
31312
|
+
cache_hit_estimate: cacheHitEstimate,
|
|
31313
|
+
spawn_mode: profile === "empty" ? "zero-mcp" : "selective",
|
|
31314
|
+
task_outcome_per_mcp_profile: profileOutcomes,
|
|
31315
|
+
files_touched_count: queueFilesTouched,
|
|
31316
|
+
new_files_created_count: queueNewFilesCreated,
|
|
31317
|
+
tasks: taskOutcomes
|
|
31318
|
+
};
|
|
31319
|
+
await this.runtime.writeFile(path2.join(artifactDir, "queue-run.json"), JSON.stringify(runMeta, null, 2));
|
|
31320
|
+
}
|
|
31321
|
+
return {
|
|
31322
|
+
done,
|
|
31323
|
+
failed,
|
|
31324
|
+
skipped,
|
|
31325
|
+
totalCostUsd,
|
|
31326
|
+
halted,
|
|
31327
|
+
haltReason,
|
|
31328
|
+
haltCode,
|
|
31329
|
+
queueRunId,
|
|
31330
|
+
artifactDir
|
|
31331
|
+
};
|
|
31332
|
+
}
|
|
31333
|
+
/**
|
|
31334
|
+
* `choda-deck queue start` orchestration per ADR-019 Phase 3 / TASK-728.
|
|
31335
|
+
*
|
|
31336
|
+
* Differs from `runQueue` on three axes:
|
|
31337
|
+
* - Pre-flight halt-all (`validateQueueStartPreflight`) before any spawn — global error
|
|
31338
|
+
* aborts the whole batch; per-task failure aborts unless `forceContinue` is set.
|
|
31339
|
+
* - Each task spawns in its own `git worktree add -b auto/<taskId> <baseSha>` cwd so
|
|
31340
|
+
* diffs and branches don't collide. Worktrees are left intact regardless of outcome —
|
|
31341
|
+
* cleanup is the orphan-cleaner's job (TASK-687).
|
|
31342
|
+
* - Mid-run policy is CONTINUE: a failure writes artifacts + marks AUTO_FAILED + moves
|
|
31343
|
+
* on. There is no `haltCode`; the runner only stops at end-of-list.
|
|
31344
|
+
*/
|
|
31345
|
+
async runQueueStart(opts) {
|
|
31346
|
+
const ws = this.workspaces.get(opts.workspaceId);
|
|
31347
|
+
if (!ws) {
|
|
31348
|
+
throw new WorkspaceResolutionError(`workspace ${opts.workspaceId} not found`);
|
|
31349
|
+
}
|
|
31350
|
+
const branchPrefix = opts.branchPrefix ?? "auto/";
|
|
31351
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
31352
|
+
const eligible = this.collectEligibleTasks(ws);
|
|
31353
|
+
const taskCap = opts.maxTasks ?? eligible.length;
|
|
31354
|
+
const allTasks = eligible.slice(0, taskCap);
|
|
31355
|
+
const queueRunId = `${Date.now()}-${Math.floor(Math.random() * 1e6).toString(36)}`;
|
|
31356
|
+
const artifactDir = path2.join(this.runtime.artifactsDir, `queue-start-${queueRunId}`);
|
|
31357
|
+
const preflight = await validateQueueStartPreflight({
|
|
31358
|
+
tasks: allTasks,
|
|
31359
|
+
repoCwd: ws.cwd,
|
|
31360
|
+
baseRef: opts.baseRef,
|
|
31361
|
+
worktreesParentDir: opts.worktreesParentDir,
|
|
31362
|
+
branchPrefix,
|
|
31363
|
+
fns: this.runtime
|
|
31364
|
+
});
|
|
31365
|
+
if (opts.dryRun) {
|
|
31366
|
+
return this.emptyQueueStartResult({
|
|
31367
|
+
workspaceId: opts.workspaceId,
|
|
31368
|
+
queueRunId,
|
|
31369
|
+
artifactDir,
|
|
31370
|
+
baseRef: opts.baseRef,
|
|
31371
|
+
baseSha: preflight.baseSha,
|
|
31372
|
+
preflightAborted: !preflight.ok && !opts.forceContinue,
|
|
31373
|
+
preflightAbortReason: preflightAbortMessage(preflight),
|
|
31374
|
+
taskOutcomes: allTasks.map((t) => ({
|
|
31375
|
+
taskId: t.id,
|
|
31376
|
+
worktreePath: null,
|
|
31377
|
+
branch: null,
|
|
31378
|
+
headSha: null,
|
|
31379
|
+
outcome: "SKIPPED_PREFLIGHT"
|
|
31380
|
+
}))
|
|
31381
|
+
});
|
|
31382
|
+
}
|
|
31383
|
+
if (preflight.globalErrors.length > 0 || !preflight.ok && !opts.forceContinue) {
|
|
31384
|
+
await this.runtime.mkdir(artifactDir);
|
|
31385
|
+
const taskOutcomes2 = allTasks.map((task) => {
|
|
31386
|
+
const fail = preflight.failures.find((f) => f.taskId === task.id);
|
|
31387
|
+
return {
|
|
31388
|
+
taskId: task.id,
|
|
31389
|
+
worktreePath: null,
|
|
31390
|
+
branch: null,
|
|
31391
|
+
headSha: null,
|
|
31392
|
+
outcome: "SKIPPED_PREFLIGHT",
|
|
31393
|
+
reason: fail ? `preflight: ${fail.reasons.join("; ")}` : "preflight: aborted by global error"
|
|
31394
|
+
};
|
|
31395
|
+
});
|
|
31396
|
+
await this.writeQueueStartMeta(artifactDir, {
|
|
31397
|
+
queueRunId,
|
|
31398
|
+
workspaceId: opts.workspaceId,
|
|
31399
|
+
baseRef: opts.baseRef,
|
|
31400
|
+
baseSha: preflight.baseSha,
|
|
31401
|
+
midRunPolicy: "continue",
|
|
31402
|
+
startedAt,
|
|
31403
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31404
|
+
model: opts.model ?? DEFAULT_MODEL,
|
|
31405
|
+
claudeBin: opts.claudeBin ?? "claude",
|
|
31406
|
+
totalCostUsd: 0,
|
|
31407
|
+
preflightAborted: true,
|
|
31408
|
+
preflightAbortReason: preflightAbortMessage(preflight),
|
|
31409
|
+
taskOutcomes: taskOutcomes2
|
|
31410
|
+
});
|
|
31411
|
+
return {
|
|
31412
|
+
workspaceId: opts.workspaceId,
|
|
31413
|
+
queueRunId,
|
|
31414
|
+
artifactDir,
|
|
31415
|
+
baseRef: opts.baseRef,
|
|
31416
|
+
baseSha: preflight.baseSha,
|
|
31417
|
+
taskOutcomes: taskOutcomes2,
|
|
31418
|
+
totalCostUsd: 0,
|
|
31419
|
+
preflightAborted: true,
|
|
31420
|
+
preflightAbortReason: preflightAbortMessage(preflight),
|
|
31421
|
+
doneCount: 0,
|
|
31422
|
+
failedCount: 0,
|
|
31423
|
+
preflightSkippedCount: taskOutcomes2.length
|
|
31424
|
+
};
|
|
31425
|
+
}
|
|
31426
|
+
const baseSha = preflight.baseSha;
|
|
31427
|
+
await this.runtime.mkdir(artifactDir);
|
|
31428
|
+
const failedTaskIds = new Set(preflight.failures.map((f) => f.taskId));
|
|
31429
|
+
const maxCostPerTask = opts.maxCostPerTask ?? DEFAULT_MAX_COST_PER_TASK;
|
|
31430
|
+
const maxBudgetUsd = round2(maxCostPerTask * 0.95);
|
|
31431
|
+
const model = opts.model ?? DEFAULT_MODEL;
|
|
31432
|
+
const claudeBin = opts.claudeBin ?? "claude";
|
|
31433
|
+
const acTimeoutMs = opts.acTimeoutMs ?? DEFAULT_AC_TIMEOUT_MS;
|
|
31434
|
+
const taskOutcomes = [];
|
|
31435
|
+
let totalCostUsd = 0;
|
|
31436
|
+
for (const task of allTasks) {
|
|
31437
|
+
if (failedTaskIds.has(task.id)) {
|
|
31438
|
+
const fail = preflight.failures.find((f) => f.taskId === task.id);
|
|
31439
|
+
taskOutcomes.push({
|
|
31440
|
+
taskId: task.id,
|
|
31441
|
+
worktreePath: null,
|
|
31442
|
+
branch: null,
|
|
31443
|
+
headSha: null,
|
|
31444
|
+
outcome: "SKIPPED_PREFLIGHT",
|
|
31445
|
+
reason: `preflight: ${fail.reasons.join("; ")}`
|
|
31446
|
+
});
|
|
31447
|
+
continue;
|
|
31448
|
+
}
|
|
31449
|
+
const worktreePath = path2.join(opts.worktreesParentDir, task.id);
|
|
31450
|
+
const branch = `${branchPrefix}${task.id}`;
|
|
31451
|
+
const taskDir = path2.join(artifactDir, "tasks", task.id);
|
|
31452
|
+
await this.runtime.mkdir(taskDir);
|
|
31453
|
+
const promptText = task.body ?? "";
|
|
31454
|
+
await this.runtime.writeFile(path2.join(taskDir, "prompt.md"), promptText);
|
|
31455
|
+
try {
|
|
31456
|
+
await this.runtime.gitWorktreeAdd({
|
|
31457
|
+
repoCwd: ws.cwd,
|
|
31458
|
+
worktreePath,
|
|
31459
|
+
branch,
|
|
31460
|
+
baseSha
|
|
31461
|
+
});
|
|
31462
|
+
} catch (err) {
|
|
31463
|
+
const reason = `worktree-add-failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
31464
|
+
const startResult2 = this.sessions.startSession({
|
|
31465
|
+
projectId: ws.projectId,
|
|
31466
|
+
workspaceId: ws.id,
|
|
31467
|
+
taskId: task.id
|
|
31468
|
+
});
|
|
31469
|
+
await this.failTask(task, startResult2.session.id, reason, taskDir);
|
|
31470
|
+
taskOutcomes.push({
|
|
31471
|
+
taskId: task.id,
|
|
31472
|
+
worktreePath,
|
|
31473
|
+
branch,
|
|
31474
|
+
headSha: null,
|
|
31475
|
+
outcome: "FAILED",
|
|
31476
|
+
reason
|
|
31477
|
+
});
|
|
31478
|
+
continue;
|
|
31479
|
+
}
|
|
31480
|
+
const startResult = this.sessions.startSession({
|
|
31481
|
+
projectId: ws.projectId,
|
|
31482
|
+
workspaceId: ws.id,
|
|
31483
|
+
taskId: task.id
|
|
31484
|
+
});
|
|
31485
|
+
const sessionId = startResult.session.id;
|
|
31486
|
+
const taskModel = resolveModelForTask(task, model);
|
|
31487
|
+
const spawnAttempt = await this.spawnWithRetry({
|
|
31488
|
+
taskBody: promptText,
|
|
31489
|
+
cwd: worktreePath,
|
|
31490
|
+
model: taskModel,
|
|
31491
|
+
maxBudgetUsd,
|
|
31492
|
+
queueMcpEmptyPath: this.runtime.queueMcpEmptyPath,
|
|
31493
|
+
claudeBin
|
|
31494
|
+
});
|
|
31495
|
+
if (spawnAttempt.error) {
|
|
31496
|
+
const reason = `spawn-error: ${spawnAttempt.error.message}`;
|
|
31497
|
+
await this.writeDiffArtifact(taskDir, worktreePath);
|
|
31498
|
+
await this.failTask(task, sessionId, reason, taskDir);
|
|
31499
|
+
const headSha2 = await this.safeHeadSha(worktreePath);
|
|
31500
|
+
taskOutcomes.push({
|
|
31501
|
+
taskId: task.id,
|
|
31502
|
+
worktreePath,
|
|
31503
|
+
branch,
|
|
31504
|
+
headSha: headSha2,
|
|
31505
|
+
outcome: "FAILED",
|
|
31506
|
+
reason
|
|
31507
|
+
});
|
|
31508
|
+
continue;
|
|
31509
|
+
}
|
|
31510
|
+
const spawn = spawnAttempt.output;
|
|
31511
|
+
await this.runtime.writeFile(path2.join(taskDir, "claude.json"), spawn.rawJson);
|
|
31512
|
+
await this.writeDiffArtifact(taskDir, worktreePath);
|
|
31513
|
+
totalCostUsd = round4(totalCostUsd + spawn.totalCostUsd);
|
|
31514
|
+
if (spawn.isError) {
|
|
31515
|
+
const reason = `claude-error: ${spawn.resultText.slice(0, 500)}`;
|
|
31516
|
+
await this.failTask(task, sessionId, reason, taskDir);
|
|
31517
|
+
const headSha2 = await this.safeHeadSha(worktreePath);
|
|
31518
|
+
taskOutcomes.push({
|
|
31519
|
+
taskId: task.id,
|
|
31520
|
+
worktreePath,
|
|
31521
|
+
branch,
|
|
31522
|
+
headSha: headSha2,
|
|
31523
|
+
outcome: "FAILED",
|
|
31524
|
+
costUsd: spawn.totalCostUsd,
|
|
31525
|
+
reason
|
|
31526
|
+
});
|
|
31527
|
+
continue;
|
|
31528
|
+
}
|
|
31529
|
+
const acReason = await this.runAcCommands(promptText, worktreePath, taskDir, acTimeoutMs);
|
|
31530
|
+
if (acReason) {
|
|
31531
|
+
await this.failTask(task, sessionId, acReason, taskDir);
|
|
31532
|
+
const headSha2 = await this.safeHeadSha(worktreePath);
|
|
31533
|
+
taskOutcomes.push({
|
|
31534
|
+
taskId: task.id,
|
|
31535
|
+
worktreePath,
|
|
31536
|
+
branch,
|
|
31537
|
+
headSha: headSha2,
|
|
31538
|
+
outcome: "FAILED",
|
|
31539
|
+
costUsd: spawn.totalCostUsd,
|
|
31540
|
+
reason: acReason
|
|
31541
|
+
});
|
|
31542
|
+
continue;
|
|
31543
|
+
}
|
|
31544
|
+
if (spawn.totalCostUsd > maxCostPerTask) {
|
|
31545
|
+
const reason = `cost-cap-exceeded: ${spawn.totalCostUsd.toFixed(
|
|
31546
|
+
2
|
|
31547
|
+
)} > ${maxCostPerTask.toFixed(2)}`;
|
|
31548
|
+
await this.failTask(task, sessionId, reason, taskDir);
|
|
31549
|
+
const headSha2 = await this.safeHeadSha(worktreePath);
|
|
31550
|
+
taskOutcomes.push({
|
|
31551
|
+
taskId: task.id,
|
|
31552
|
+
worktreePath,
|
|
31553
|
+
branch,
|
|
31554
|
+
headSha: headSha2,
|
|
31555
|
+
outcome: "FAILED",
|
|
31556
|
+
costUsd: spawn.totalCostUsd,
|
|
31557
|
+
reason
|
|
31558
|
+
});
|
|
31559
|
+
continue;
|
|
31560
|
+
}
|
|
31561
|
+
this.sessions.endSession(sessionId, {
|
|
31562
|
+
handoff: {
|
|
31563
|
+
resumePoint: `auto-completed by queue start (run ${queueRunId})`,
|
|
31564
|
+
decisions: [
|
|
31565
|
+
`Queue start ${queueRunId} marked ${task.id} DONE \u2014 worktree ${worktreePath}, branch ${branch}`
|
|
31566
|
+
]
|
|
31567
|
+
}
|
|
31568
|
+
});
|
|
31569
|
+
const headSha = await this.safeHeadSha(worktreePath);
|
|
31570
|
+
taskOutcomes.push({
|
|
31571
|
+
taskId: task.id,
|
|
31572
|
+
worktreePath,
|
|
31573
|
+
branch,
|
|
31574
|
+
headSha,
|
|
31575
|
+
outcome: "DONE",
|
|
31576
|
+
costUsd: spawn.totalCostUsd,
|
|
31577
|
+
numTurns: spawn.numTurns
|
|
31578
|
+
});
|
|
31579
|
+
}
|
|
31580
|
+
const doneCount = taskOutcomes.filter((o) => o.outcome === "DONE").length;
|
|
31581
|
+
const failedCount = taskOutcomes.filter((o) => o.outcome === "FAILED").length;
|
|
31582
|
+
const preflightSkippedCount = taskOutcomes.filter((o) => o.outcome === "SKIPPED_PREFLIGHT").length;
|
|
31583
|
+
await this.writeQueueStartMeta(artifactDir, {
|
|
31584
|
+
queueRunId,
|
|
31585
|
+
workspaceId: opts.workspaceId,
|
|
31586
|
+
baseRef: opts.baseRef,
|
|
31587
|
+
baseSha,
|
|
31588
|
+
midRunPolicy: "continue",
|
|
31589
|
+
startedAt,
|
|
31590
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31591
|
+
model,
|
|
31592
|
+
claudeBin,
|
|
31593
|
+
totalCostUsd,
|
|
31594
|
+
preflightAborted: false,
|
|
31595
|
+
preflightAbortReason: null,
|
|
31596
|
+
taskOutcomes
|
|
31597
|
+
});
|
|
31598
|
+
return {
|
|
31599
|
+
workspaceId: opts.workspaceId,
|
|
31600
|
+
queueRunId,
|
|
31601
|
+
artifactDir,
|
|
31602
|
+
baseRef: opts.baseRef,
|
|
31603
|
+
baseSha,
|
|
31604
|
+
taskOutcomes,
|
|
31605
|
+
totalCostUsd,
|
|
31606
|
+
preflightAborted: false,
|
|
31607
|
+
preflightAbortReason: null,
|
|
31608
|
+
doneCount,
|
|
31609
|
+
failedCount,
|
|
31610
|
+
preflightSkippedCount
|
|
31611
|
+
};
|
|
31612
|
+
}
|
|
31613
|
+
/**
|
|
31614
|
+
* `gitHeadSha` may throw if the worktree disappeared between spawn and query; swallow
|
|
31615
|
+
* that into a `null` outcome rather than failing the whole loop.
|
|
31616
|
+
*/
|
|
31617
|
+
async safeHeadSha(cwd) {
|
|
31618
|
+
try {
|
|
31619
|
+
return await this.runtime.gitHeadSha(cwd);
|
|
31620
|
+
} catch {
|
|
31621
|
+
return null;
|
|
31622
|
+
}
|
|
31623
|
+
}
|
|
31624
|
+
async writeQueueStartMeta(artifactDir, meta3) {
|
|
31625
|
+
await this.runtime.writeFile(
|
|
31626
|
+
path2.join(artifactDir, "queue-run.json"),
|
|
31627
|
+
JSON.stringify(meta3, null, 2)
|
|
31628
|
+
);
|
|
31629
|
+
}
|
|
31630
|
+
emptyQueueStartResult(seed) {
|
|
31631
|
+
return {
|
|
31632
|
+
...seed,
|
|
31633
|
+
totalCostUsd: 0,
|
|
31634
|
+
doneCount: 0,
|
|
31635
|
+
failedCount: 0,
|
|
31636
|
+
preflightSkippedCount: seed.taskOutcomes.length
|
|
31637
|
+
};
|
|
31638
|
+
}
|
|
31639
|
+
/**
|
|
31640
|
+
* Spawn with at most one retry for transient errors. The same prompt is sent on retry —
|
|
31641
|
+
* Claude is stateless across `-p` invocations so this is safe. Final attempt's output
|
|
31642
|
+
* (success or last failure) is what flows into per-task artifact writing and post-hoc
|
|
31643
|
+
* cost accounting; no double-counting because we only act on the final result.
|
|
31644
|
+
*/
|
|
31645
|
+
async spawnWithRetry(input) {
|
|
31646
|
+
let lastOutput = null;
|
|
31647
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
31648
|
+
try {
|
|
31649
|
+
const output = await this.runtime.spawnClaude(input);
|
|
31650
|
+
lastOutput = output;
|
|
31651
|
+
if (output.isError && attempt === 0 && isTransientMessage(output.resultText)) {
|
|
31652
|
+
continue;
|
|
31653
|
+
}
|
|
31654
|
+
return { output, error: null };
|
|
31655
|
+
} catch (err) {
|
|
31656
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
31657
|
+
if (attempt === 0 && isTransientMessage(e.message)) continue;
|
|
31658
|
+
return { output: null, error: e };
|
|
31659
|
+
}
|
|
31660
|
+
}
|
|
31661
|
+
if (lastOutput) return { output: lastOutput, error: null };
|
|
31662
|
+
return { output: null, error: new Error("spawn retry loop produced no result") };
|
|
31663
|
+
}
|
|
31664
|
+
collectEligibleTasks(ws) {
|
|
31665
|
+
const candidates = this.tasks.find({ projectId: ws.projectId, status: "READY" });
|
|
31666
|
+
return candidates.filter(
|
|
31667
|
+
(t) => t.labels.includes(AUTO_SAFE_LABEL) && !t.labels.includes(AUTO_FAILED_LABEL) && validateAutoSafeTask(t).valid
|
|
31668
|
+
).sort((x, y) => x.id.localeCompare(y.id));
|
|
31669
|
+
}
|
|
31670
|
+
async runAcCommands(body, cwd, taskDir, timeoutMs) {
|
|
31671
|
+
const cmds = parseAcCommands(body);
|
|
31672
|
+
for (let i = 0; i < cmds.length; i++) {
|
|
31673
|
+
const cmd = cmds[i];
|
|
31674
|
+
const r = await this.runtime.execShell(cmd, { cwd, timeoutMs });
|
|
31675
|
+
const log = `$ ${cmd}
|
|
31676
|
+
exit ${r.exitCode}
|
|
31677
|
+
--- stdout ---
|
|
31678
|
+
${r.stdout}
|
|
31679
|
+
--- stderr ---
|
|
31680
|
+
${r.stderr}
|
|
31681
|
+
`;
|
|
31682
|
+
await this.runtime.writeFile(path2.join(taskDir, `ac-${i}.log`), log);
|
|
31683
|
+
if (r.exitCode !== 0) {
|
|
31684
|
+
return `ac-failed: \`${cmd}\` exit ${r.exitCode}`;
|
|
31685
|
+
}
|
|
31686
|
+
}
|
|
31687
|
+
return null;
|
|
31688
|
+
}
|
|
31689
|
+
async writeDiffArtifact(taskDir, cwd) {
|
|
31690
|
+
const diff = await this.runtime.gitDiff(cwd);
|
|
31691
|
+
await this.runtime.writeFile(path2.join(taskDir, "diff.patch"), diff);
|
|
31692
|
+
const stats = parseDiffStats(diff);
|
|
31693
|
+
const untracked = await this.runtime.gitUntrackedFiles(cwd);
|
|
31694
|
+
return { filesTouched: stats.filesTouched, newFiles: stats.newFiles + untracked.length };
|
|
31695
|
+
}
|
|
31696
|
+
async failTask(task, sessionId, reason, taskDir) {
|
|
31697
|
+
const refreshed = this.tasks.get(task.id);
|
|
31698
|
+
if (!refreshed) throw new TaskNotFoundError(task.id);
|
|
31699
|
+
const nextLabels = refreshed.labels.includes(AUTO_FAILED_LABEL) ? refreshed.labels : [...refreshed.labels, AUTO_FAILED_LABEL];
|
|
31700
|
+
this.tasks.update(task.id, { labels: nextLabels, status: "READY" });
|
|
31701
|
+
const linkedConvs = this.conversations.findByLink("task", task.id);
|
|
31702
|
+
for (const conv of linkedConvs) {
|
|
31703
|
+
if (conv.status === "closed") continue;
|
|
31704
|
+
this.conversations.addMessage({
|
|
31705
|
+
conversationId: conv.id,
|
|
31706
|
+
authorName: "queue-runner",
|
|
31707
|
+
content: `Auto-failed: ${reason}
|
|
31708
|
+
Diff: ${path2.join(taskDir, "diff.patch")}`,
|
|
31709
|
+
messageType: "comment"
|
|
31710
|
+
});
|
|
31711
|
+
}
|
|
31712
|
+
this.sessions.abandonSession(sessionId, reason);
|
|
31713
|
+
}
|
|
31714
|
+
};
|
|
31715
|
+
function resolveModelForTask(task, defaultModel) {
|
|
31716
|
+
for (const label of task.labels) {
|
|
31717
|
+
const m = label.match(/^model:(.+)$/);
|
|
31718
|
+
if (m) {
|
|
31719
|
+
const value = m[1];
|
|
31720
|
+
if (!value.startsWith("claude-")) {
|
|
31721
|
+
process.stderr.write(`[queue] warning: model label "${label}" value does not start with "claude-"
|
|
31722
|
+
`);
|
|
31723
|
+
}
|
|
31724
|
+
return value;
|
|
31725
|
+
}
|
|
31726
|
+
}
|
|
31727
|
+
return defaultModel;
|
|
31728
|
+
}
|
|
31729
|
+
function round2(n) {
|
|
31730
|
+
return Math.round(n * 100) / 100;
|
|
31731
|
+
}
|
|
31732
|
+
function round4(n) {
|
|
31733
|
+
return Math.round(n * 1e4) / 1e4;
|
|
31734
|
+
}
|
|
31735
|
+
function preflightAbortMessage(preflight) {
|
|
31736
|
+
if (preflight.ok) return null;
|
|
31737
|
+
const parts = [];
|
|
31738
|
+
if (preflight.globalErrors.length > 0) {
|
|
31739
|
+
parts.push(`global: ${preflight.globalErrors.join("; ")}`);
|
|
31740
|
+
}
|
|
31741
|
+
if (preflight.failures.length > 0) {
|
|
31742
|
+
parts.push(
|
|
31743
|
+
`tasks: ${preflight.failures.map((f) => `${f.taskId} (${f.reasons.join(", ")})`).join("; ")}`
|
|
31744
|
+
);
|
|
31745
|
+
}
|
|
31746
|
+
return parts.join(" | ");
|
|
31747
|
+
}
|
|
31748
|
+
function parseDiffStats(diff) {
|
|
31749
|
+
let totalFiles = 0;
|
|
31750
|
+
let newFiles = 0;
|
|
31751
|
+
for (const line of splitLines(diff)) {
|
|
31752
|
+
if (line.startsWith("diff --git ")) totalFiles += 1;
|
|
31753
|
+
else if (line.startsWith("new file mode")) newFiles += 1;
|
|
31754
|
+
}
|
|
31755
|
+
return { filesTouched: totalFiles - newFiles, newFiles };
|
|
31756
|
+
}
|
|
31757
|
+
|
|
30799
31758
|
// src/core/domain/knowledge-service.ts
|
|
30800
31759
|
var fs = __toESM(require("fs"));
|
|
30801
|
-
var
|
|
31760
|
+
var path4 = __toESM(require("path"));
|
|
30802
31761
|
|
|
30803
31762
|
// src/core/domain/knowledge-git.ts
|
|
30804
31763
|
var import_child_process = require("child_process");
|
|
@@ -31012,6 +31971,15 @@ function quoteIfNeeded(s) {
|
|
|
31012
31971
|
return s;
|
|
31013
31972
|
}
|
|
31014
31973
|
|
|
31974
|
+
// src/core/worktree-path.ts
|
|
31975
|
+
var path3 = __toESM(require("path"));
|
|
31976
|
+
var WORKTREE_SEGMENT_RE = /\.worktrees([\\/]|$)/i;
|
|
31977
|
+
function isLikelyWorktreePath(absPath) {
|
|
31978
|
+
if (!absPath) return false;
|
|
31979
|
+
const normalized = path3.normalize(absPath);
|
|
31980
|
+
return WORKTREE_SEGMENT_RE.test(normalized);
|
|
31981
|
+
}
|
|
31982
|
+
|
|
31015
31983
|
// src/core/domain/knowledge-service.ts
|
|
31016
31984
|
var KnowledgeNotFoundError = class extends Error {
|
|
31017
31985
|
constructor(slug) {
|
|
@@ -31064,6 +32032,11 @@ var KnowledgeService = class {
|
|
|
31064
32032
|
}
|
|
31065
32033
|
const stalenessCwd = workspaceCwd ?? project.cwd;
|
|
31066
32034
|
const filePath = this.resolveFilePath(input.scope, project.cwd, slug, workspaceCwd);
|
|
32035
|
+
if (isLikelyWorktreePath(filePath)) {
|
|
32036
|
+
throw new KnowledgeValidationError(
|
|
32037
|
+
`target path is inside a git worktree (${filePath}) \u2014 knowledge there would be orphaned when the worktree is removed. Create knowledge from the main checkout instead.`
|
|
32038
|
+
);
|
|
32039
|
+
}
|
|
31067
32040
|
if (fs.existsSync(filePath)) {
|
|
31068
32041
|
throw new KnowledgeConflictError(slug, `file already exists at ${filePath}`);
|
|
31069
32042
|
}
|
|
@@ -31080,7 +32053,7 @@ var KnowledgeService = class {
|
|
|
31080
32053
|
lastVerifiedAt: isoDate
|
|
31081
32054
|
};
|
|
31082
32055
|
const content = serializeFrontmatter(frontmatter, input.body);
|
|
31083
|
-
fs.mkdirSync(
|
|
32056
|
+
fs.mkdirSync(path4.dirname(filePath), { recursive: true });
|
|
31084
32057
|
fs.writeFileSync(filePath, content, "utf8");
|
|
31085
32058
|
const indexRow = {
|
|
31086
32059
|
slug,
|
|
@@ -31131,7 +32104,7 @@ var KnowledgeService = class {
|
|
|
31131
32104
|
`frontmatter workspaceId (${frontmatter.workspaceId ?? "null"}) does not match input (${input.workspaceId ?? "null"})`
|
|
31132
32105
|
);
|
|
31133
32106
|
}
|
|
31134
|
-
const slug =
|
|
32107
|
+
const slug = path4.basename(input.filePath, ".md");
|
|
31135
32108
|
if (!slug) throw new KnowledgeValidationError(`cannot derive slug from path: ${input.filePath}`);
|
|
31136
32109
|
const indexRow = {
|
|
31137
32110
|
slug,
|
|
@@ -31329,9 +32302,9 @@ var KnowledgeService = class {
|
|
|
31329
32302
|
resolveFilePath(scope, projectCwd, slug, workspaceCwd) {
|
|
31330
32303
|
if (scope === "project") {
|
|
31331
32304
|
const cwd = workspaceCwd ?? projectCwd;
|
|
31332
|
-
return
|
|
32305
|
+
return path4.join(cwd, "docs", "knowledge", `${slug}.md`);
|
|
31333
32306
|
}
|
|
31334
|
-
return
|
|
32307
|
+
return path4.join(this.contentRoot, "30-Knowledge", `${slug}.md`);
|
|
31335
32308
|
}
|
|
31336
32309
|
materializeRefs(inputRefs, projectCwd, scope) {
|
|
31337
32310
|
if (inputRefs.length === 0) return [];
|
|
@@ -31363,11 +32336,12 @@ var KnowledgeService = class {
|
|
|
31363
32336
|
this.writeIndexMd(projectCwd, `Knowledge \u2014 ${projectId}`, rows);
|
|
31364
32337
|
}
|
|
31365
32338
|
regenerateWorkspaceIndexMd(projectId, workspaceId, workspaceCwd) {
|
|
32339
|
+
if (!fs.existsSync(workspaceCwd)) return;
|
|
31366
32340
|
const rows = this.knowledge.list({ projectId, workspaceId, scope: "project" });
|
|
31367
32341
|
this.writeIndexMd(workspaceCwd, `Knowledge \u2014 ${projectId}/${workspaceId}`, rows);
|
|
31368
32342
|
}
|
|
31369
32343
|
writeIndexMd(cwd, heading, rows) {
|
|
31370
|
-
const indexPath =
|
|
32344
|
+
const indexPath = path4.join(cwd, "docs", "knowledge", "INDEX.md");
|
|
31371
32345
|
const lines = ["# " + heading, ""];
|
|
31372
32346
|
if (rows.length === 0) {
|
|
31373
32347
|
lines.push("_No entries yet._");
|
|
@@ -31382,7 +32356,7 @@ var KnowledgeService = class {
|
|
|
31382
32356
|
}
|
|
31383
32357
|
}
|
|
31384
32358
|
lines.push("");
|
|
31385
|
-
fs.mkdirSync(
|
|
32359
|
+
fs.mkdirSync(path4.dirname(indexPath), { recursive: true });
|
|
31386
32360
|
fs.writeFileSync(indexPath, lines.join("\n") + "\n", "utf8");
|
|
31387
32361
|
}
|
|
31388
32362
|
isEntryStale(row, projectCwd) {
|
|
@@ -31908,6 +32882,16 @@ function createM1Tables(db) {
|
|
|
31908
32882
|
db.exec("ALTER TABLE knowledge_index ADD COLUMN workspace_id TEXT REFERENCES workspaces(id)");
|
|
31909
32883
|
} catch {
|
|
31910
32884
|
}
|
|
32885
|
+
db.exec(`
|
|
32886
|
+
CREATE TABLE IF NOT EXISTS tool_invocations (
|
|
32887
|
+
id INTEGER PRIMARY KEY,
|
|
32888
|
+
tool_name TEXT NOT NULL,
|
|
32889
|
+
ts TEXT NOT NULL,
|
|
32890
|
+
duration_ms INTEGER NOT NULL,
|
|
32891
|
+
ok INTEGER NOT NULL,
|
|
32892
|
+
error_kind TEXT
|
|
32893
|
+
)
|
|
32894
|
+
`);
|
|
31911
32895
|
}
|
|
31912
32896
|
function createIndexes(db) {
|
|
31913
32897
|
db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id)");
|
|
@@ -31940,6 +32924,9 @@ function createIndexes(db) {
|
|
|
31940
32924
|
db.exec("CREATE INDEX IF NOT EXISTS idx_knowledge_type ON knowledge_index(project_id, type)");
|
|
31941
32925
|
db.exec("CREATE INDEX IF NOT EXISTS idx_knowledge_scope ON knowledge_index(project_id, scope)");
|
|
31942
32926
|
db.exec("CREATE INDEX IF NOT EXISTS idx_knowledge_workspace ON knowledge_index(workspace_id)");
|
|
32927
|
+
db.exec(
|
|
32928
|
+
"CREATE INDEX IF NOT EXISTS idx_tool_invocations_tool_ts ON tool_invocations(tool_name, ts)"
|
|
32929
|
+
);
|
|
31943
32930
|
}
|
|
31944
32931
|
function cleanupPoisonedTaskIds(db) {
|
|
31945
32932
|
const rows = db.prepare("SELECT id FROM tasks WHERE id GLOB 'TASK-[0-9]*'").all();
|
|
@@ -32620,43 +33607,43 @@ var ContextSourceRepository = class {
|
|
|
32620
33607
|
|
|
32621
33608
|
// src/core/domain/services/event-emitter.ts
|
|
32622
33609
|
var fs2 = __toESM(require("fs"));
|
|
32623
|
-
var
|
|
33610
|
+
var path6 = __toESM(require("path"));
|
|
32624
33611
|
|
|
32625
33612
|
// src/core/paths.ts
|
|
32626
33613
|
var os = __toESM(require("os"));
|
|
32627
|
-
var
|
|
33614
|
+
var path5 = __toESM(require("path"));
|
|
32628
33615
|
function resolveDataPaths(electronDataDir) {
|
|
32629
33616
|
const legacyDbPath = process.env.CHODA_DB_PATH;
|
|
32630
33617
|
const envDataDir = process.env.CHODA_DATA_DIR;
|
|
32631
|
-
let
|
|
32632
|
-
let
|
|
33618
|
+
let dataDir;
|
|
33619
|
+
let dbPath;
|
|
32633
33620
|
if (legacyDbPath) {
|
|
32634
33621
|
if (envDataDir) {
|
|
32635
33622
|
console.warn("[paths] Both CHODA_DB_PATH and CHODA_DATA_DIR set \u2014 CHODA_DB_PATH wins for dbPath");
|
|
32636
33623
|
}
|
|
32637
|
-
|
|
32638
|
-
|
|
33624
|
+
dbPath = path5.resolve(legacyDbPath);
|
|
33625
|
+
dataDir = electronDataDir ?? envDataDir ?? path5.dirname(dbPath);
|
|
32639
33626
|
} else if (envDataDir) {
|
|
32640
|
-
|
|
32641
|
-
|
|
33627
|
+
dataDir = path5.resolve(envDataDir);
|
|
33628
|
+
dbPath = path5.join(dataDir, "database", "choda-deck.db");
|
|
32642
33629
|
} else if (electronDataDir) {
|
|
32643
|
-
|
|
32644
|
-
|
|
33630
|
+
dataDir = electronDataDir;
|
|
33631
|
+
dbPath = path5.join(dataDir, "database", "choda-deck.db");
|
|
32645
33632
|
} else {
|
|
32646
|
-
|
|
32647
|
-
|
|
33633
|
+
dataDir = path5.join(process.cwd(), "data");
|
|
33634
|
+
dbPath = path5.join(dataDir, "database", "choda-deck.db");
|
|
32648
33635
|
}
|
|
32649
33636
|
return {
|
|
32650
|
-
dataDir
|
|
32651
|
-
dbPath
|
|
32652
|
-
artifactsDir:
|
|
32653
|
-
backupsDir:
|
|
33637
|
+
dataDir,
|
|
33638
|
+
dbPath,
|
|
33639
|
+
artifactsDir: path5.join(dataDir, "artifacts"),
|
|
33640
|
+
backupsDir: path5.join(dataDir, "backups")
|
|
32654
33641
|
};
|
|
32655
33642
|
}
|
|
32656
33643
|
function resolveEventDir() {
|
|
32657
33644
|
const envDir = process.env.CHODA_EVENT_DIR;
|
|
32658
|
-
if (envDir) return
|
|
32659
|
-
return
|
|
33645
|
+
if (envDir) return path5.resolve(envDir);
|
|
33646
|
+
return path5.join(os.tmpdir(), "choda-events");
|
|
32660
33647
|
}
|
|
32661
33648
|
|
|
32662
33649
|
// src/core/domain/services/event-emitter.ts
|
|
@@ -32668,7 +33655,7 @@ function emitConversationEvent(projectId, event) {
|
|
|
32668
33655
|
try {
|
|
32669
33656
|
const dir = resolveEventDir();
|
|
32670
33657
|
fs2.mkdirSync(dir, { recursive: true });
|
|
32671
|
-
const file2 =
|
|
33658
|
+
const file2 = path6.join(dir, `${projectId}.jsonl`);
|
|
32672
33659
|
const normalized = {
|
|
32673
33660
|
...event,
|
|
32674
33661
|
timestamp: normalizeEventTimestamp(event.timestamp)
|
|
@@ -32678,6 +33665,15 @@ function emitConversationEvent(projectId, event) {
|
|
|
32678
33665
|
console.warn("[conversation-event-emitter] emit failed:", err);
|
|
32679
33666
|
}
|
|
32680
33667
|
}
|
|
33668
|
+
function emitConversationEventFanout(ownerProjectId, targetProjectIds, event) {
|
|
33669
|
+
emitConversationEvent(ownerProjectId, event);
|
|
33670
|
+
const seen = /* @__PURE__ */ new Set([ownerProjectId]);
|
|
33671
|
+
for (const target of targetProjectIds) {
|
|
33672
|
+
if (seen.has(target)) continue;
|
|
33673
|
+
seen.add(target);
|
|
33674
|
+
emitConversationEvent(target, event);
|
|
33675
|
+
}
|
|
33676
|
+
}
|
|
32681
33677
|
|
|
32682
33678
|
// src/core/domain/repositories/conversation-repository.ts
|
|
32683
33679
|
function rowToConversation(row) {
|
|
@@ -32868,7 +33864,8 @@ var ConversationRepository = class {
|
|
|
32868
33864
|
if (allRoles.length === 0) return;
|
|
32869
33865
|
roles = allRoles;
|
|
32870
33866
|
}
|
|
32871
|
-
|
|
33867
|
+
const targetProjectIds = this.resolveFanoutTargets(roles, conv.projectId);
|
|
33868
|
+
emitConversationEventFanout(conv.projectId, targetProjectIds, {
|
|
32872
33869
|
type,
|
|
32873
33870
|
conversationId,
|
|
32874
33871
|
roles,
|
|
@@ -32877,6 +33874,32 @@ var ConversationRepository = class {
|
|
|
32877
33874
|
timestamp
|
|
32878
33875
|
});
|
|
32879
33876
|
}
|
|
33877
|
+
// ADR-021 Phase 3: parse "<projectId>/<workspaceId>" address strings from
|
|
33878
|
+
// roles[] and return the unique, validated set of fan-out target projectIds
|
|
33879
|
+
// (owner excluded). Unknown projectIds are logged and skipped — never throw.
|
|
33880
|
+
resolveFanoutTargets(roles, ownerProjectId) {
|
|
33881
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
33882
|
+
for (const role of roles) {
|
|
33883
|
+
const slash = role.indexOf("/");
|
|
33884
|
+
if (slash <= 0) continue;
|
|
33885
|
+
const projectId = role.slice(0, slash);
|
|
33886
|
+
if (projectId === ownerProjectId) continue;
|
|
33887
|
+
candidates.add(projectId);
|
|
33888
|
+
}
|
|
33889
|
+
if (candidates.size === 0) return [];
|
|
33890
|
+
const targets = [];
|
|
33891
|
+
for (const projectId of candidates) {
|
|
33892
|
+
const exists = this.db.prepare("SELECT 1 FROM projects WHERE id = ?").get(projectId);
|
|
33893
|
+
if (exists) {
|
|
33894
|
+
targets.push(projectId);
|
|
33895
|
+
} else {
|
|
33896
|
+
console.warn(
|
|
33897
|
+
`[conversation-event-emitter] unknown target projectId in role address: ${projectId}`
|
|
33898
|
+
);
|
|
33899
|
+
}
|
|
33900
|
+
}
|
|
33901
|
+
return targets;
|
|
33902
|
+
}
|
|
32880
33903
|
getMessages(conversationId) {
|
|
32881
33904
|
const rows = this.db.prepare(
|
|
32882
33905
|
"SELECT * FROM conversation_messages WHERE conversation_id = ? ORDER BY created_at, id"
|
|
@@ -33053,6 +34076,52 @@ var CounterRepository = class {
|
|
|
33053
34076
|
}
|
|
33054
34077
|
};
|
|
33055
34078
|
|
|
34079
|
+
// src/core/domain/repositories/tool-invocations-repository.ts
|
|
34080
|
+
var ToolInvocationsRepository = class {
|
|
34081
|
+
insertStmt;
|
|
34082
|
+
countStmt;
|
|
34083
|
+
queryStmt;
|
|
34084
|
+
constructor(db) {
|
|
34085
|
+
this.insertStmt = db.prepare(
|
|
34086
|
+
`INSERT INTO tool_invocations (tool_name, ts, duration_ms, ok, error_kind)
|
|
34087
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
34088
|
+
);
|
|
34089
|
+
this.countStmt = db.prepare("SELECT COUNT(*) AS n FROM tool_invocations");
|
|
34090
|
+
this.queryStmt = db.prepare(
|
|
34091
|
+
`SELECT
|
|
34092
|
+
tool_name AS tool,
|
|
34093
|
+
COUNT(*) AS calls,
|
|
34094
|
+
SUM(1 - ok) AS errors,
|
|
34095
|
+
AVG(duration_ms) AS avgDurationMs,
|
|
34096
|
+
MAX(ts) AS lastUsedAt
|
|
34097
|
+
FROM tool_invocations
|
|
34098
|
+
WHERE (@since IS NULL OR ts >= @since)
|
|
34099
|
+
AND (@until IS NULL OR ts <= @until)
|
|
34100
|
+
GROUP BY tool_name`
|
|
34101
|
+
);
|
|
34102
|
+
}
|
|
34103
|
+
recordToolInvocation(invocation) {
|
|
34104
|
+
this.insertStmt.run(
|
|
34105
|
+
invocation.toolName,
|
|
34106
|
+
invocation.ts,
|
|
34107
|
+
invocation.durationMs,
|
|
34108
|
+
invocation.ok ? 1 : 0,
|
|
34109
|
+
invocation.errorKind
|
|
34110
|
+
);
|
|
34111
|
+
}
|
|
34112
|
+
countToolInvocations() {
|
|
34113
|
+
const row = this.countStmt.get();
|
|
34114
|
+
return row.n;
|
|
34115
|
+
}
|
|
34116
|
+
queryToolInvocations(window) {
|
|
34117
|
+
const rows = this.queryStmt.all({
|
|
34118
|
+
since: window.since,
|
|
34119
|
+
until: window.until
|
|
34120
|
+
});
|
|
34121
|
+
return rows;
|
|
34122
|
+
}
|
|
34123
|
+
};
|
|
34124
|
+
|
|
33056
34125
|
// src/core/domain/sqlite-task-service.ts
|
|
33057
34126
|
var SqliteTaskService = class {
|
|
33058
34127
|
db;
|
|
@@ -33067,6 +34136,7 @@ var SqliteTaskService = class {
|
|
|
33067
34136
|
conversations;
|
|
33068
34137
|
inbox;
|
|
33069
34138
|
counters;
|
|
34139
|
+
toolInvocations;
|
|
33070
34140
|
inboxLifecycle;
|
|
33071
34141
|
conversationLifecycle;
|
|
33072
34142
|
sessionLifecycle;
|
|
@@ -33075,8 +34145,8 @@ var SqliteTaskService = class {
|
|
|
33075
34145
|
embeddingStore;
|
|
33076
34146
|
embeddingProviderPromise;
|
|
33077
34147
|
embeddingReadyPromise = null;
|
|
33078
|
-
constructor(
|
|
33079
|
-
this.db = new import_better_sqlite3.default(
|
|
34148
|
+
constructor(dbPath) {
|
|
34149
|
+
this.db = new import_better_sqlite3.default(dbPath);
|
|
33080
34150
|
this.db.pragma("journal_mode = WAL");
|
|
33081
34151
|
this.db.pragma("foreign_keys = ON");
|
|
33082
34152
|
const vecLoaded = loadVecExtension(this.db);
|
|
@@ -33090,6 +34160,7 @@ var SqliteTaskService = class {
|
|
|
33090
34160
|
this.workspaces = new WorkspaceRepository(this.db);
|
|
33091
34161
|
this.relationships = new RelationshipRepository(this.db);
|
|
33092
34162
|
this.counters = new CounterRepository(this.db);
|
|
34163
|
+
this.toolInvocations = new ToolInvocationsRepository(this.db);
|
|
33093
34164
|
this.tasks = new TaskRepository(this.db, this.relationships, this.counters);
|
|
33094
34165
|
this.documents = new DocumentRepository(this.db);
|
|
33095
34166
|
this.tagsRepo = new TagRepository(this.db);
|
|
@@ -33154,6 +34225,16 @@ var SqliteTaskService = class {
|
|
|
33154
34225
|
const escaped = absolutePath.replace(/'/g, "''");
|
|
33155
34226
|
this.db.exec(`VACUUM INTO '${escaped}'`);
|
|
33156
34227
|
}
|
|
34228
|
+
// ── Tool invocations (TASK-681) ────────────────────────────────────────────
|
|
34229
|
+
recordToolInvocation(invocation) {
|
|
34230
|
+
this.toolInvocations.recordToolInvocation(invocation);
|
|
34231
|
+
}
|
|
34232
|
+
countToolInvocations() {
|
|
34233
|
+
return this.toolInvocations.countToolInvocations();
|
|
34234
|
+
}
|
|
34235
|
+
queryToolInvocations(window) {
|
|
34236
|
+
return this.toolInvocations.queryToolInvocations(window);
|
|
34237
|
+
}
|
|
33157
34238
|
ensureProject(id, name, cwd) {
|
|
33158
34239
|
this.projects.ensure(id, name, cwd);
|
|
33159
34240
|
}
|
|
@@ -33178,12 +34259,6 @@ var SqliteTaskService = class {
|
|
|
33178
34259
|
unarchiveWorkspace(id) {
|
|
33179
34260
|
return this.workspaces.unarchive(id);
|
|
33180
34261
|
}
|
|
33181
|
-
deleteWorkspace(id) {
|
|
33182
|
-
this.workspaces.delete(id);
|
|
33183
|
-
}
|
|
33184
|
-
countWorkspaceReferences(id) {
|
|
33185
|
-
return this.workspaces.countReferences(id);
|
|
33186
|
-
}
|
|
33187
34262
|
// ── Task operations ────────────────────────────────────────────────────────
|
|
33188
34263
|
createTask(input) {
|
|
33189
34264
|
return this.tasks.create(input);
|
|
@@ -33393,12 +34468,25 @@ var SqliteTaskService = class {
|
|
|
33393
34468
|
endSession(id, input) {
|
|
33394
34469
|
return this.sessionLifecycle.endSession(id, input);
|
|
33395
34470
|
}
|
|
34471
|
+
abandonSession(id, reason) {
|
|
34472
|
+
return this.sessionLifecycle.abandonSession(id, reason);
|
|
34473
|
+
}
|
|
33396
34474
|
checkpointSession(id, input) {
|
|
33397
34475
|
return this.sessionLifecycle.checkpointSession(id, input);
|
|
33398
34476
|
}
|
|
33399
34477
|
resumeSession(id) {
|
|
33400
34478
|
return this.sessionLifecycle.resumeSession(id);
|
|
33401
34479
|
}
|
|
34480
|
+
// ── Queue lifecycle (autonomous queue runner per ADR-019) ─────────────────
|
|
34481
|
+
createQueueLifecycle(runtime) {
|
|
34482
|
+
return new QueueLifecycleService(
|
|
34483
|
+
this.tasks,
|
|
34484
|
+
this.workspaces,
|
|
34485
|
+
this.conversations,
|
|
34486
|
+
this.sessionLifecycle,
|
|
34487
|
+
runtime
|
|
34488
|
+
);
|
|
34489
|
+
}
|
|
33402
34490
|
// ── Knowledge ─────────────────────────────────────────────────────────────
|
|
33403
34491
|
createKnowledge(input) {
|
|
33404
34492
|
return this.knowledgeService.createKnowledge(input);
|
|
@@ -33438,6 +34526,45 @@ function loadVecExtension(db) {
|
|
|
33438
34526
|
}
|
|
33439
34527
|
}
|
|
33440
34528
|
|
|
34529
|
+
// src/adapters/mcp/instrumented-server.ts
|
|
34530
|
+
function createInstrumentedServer(server, sink) {
|
|
34531
|
+
const names = [];
|
|
34532
|
+
const registerTool = ((name, config2, cb) => {
|
|
34533
|
+
names.push(name);
|
|
34534
|
+
const wrappedCb = async (...callArgs) => {
|
|
34535
|
+
const start = Date.now();
|
|
34536
|
+
let ok = true;
|
|
34537
|
+
let errorKind = null;
|
|
34538
|
+
try {
|
|
34539
|
+
return await cb(...callArgs);
|
|
34540
|
+
} catch (e) {
|
|
34541
|
+
ok = false;
|
|
34542
|
+
errorKind = e?.name ?? "Error";
|
|
34543
|
+
throw e;
|
|
34544
|
+
} finally {
|
|
34545
|
+
try {
|
|
34546
|
+
sink.recordToolInvocation({
|
|
34547
|
+
toolName: name,
|
|
34548
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
34549
|
+
durationMs: Date.now() - start,
|
|
34550
|
+
ok,
|
|
34551
|
+
errorKind
|
|
34552
|
+
});
|
|
34553
|
+
} catch (logErr) {
|
|
34554
|
+
console.warn("[choda-deck] tool invocation insert failed", logErr);
|
|
34555
|
+
}
|
|
34556
|
+
}
|
|
34557
|
+
};
|
|
34558
|
+
return server.registerTool(name, config2, wrappedCb);
|
|
34559
|
+
});
|
|
34560
|
+
return {
|
|
34561
|
+
registerTool,
|
|
34562
|
+
get registeredToolNames() {
|
|
34563
|
+
return names;
|
|
34564
|
+
}
|
|
34565
|
+
};
|
|
34566
|
+
}
|
|
34567
|
+
|
|
33441
34568
|
// src/adapters/mcp/mcp-tools/types.ts
|
|
33442
34569
|
function textResponse(payload) {
|
|
33443
34570
|
const text = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
|
|
@@ -33446,7 +34573,7 @@ function textResponse(payload) {
|
|
|
33446
34573
|
|
|
33447
34574
|
// src/adapters/mcp/mcp-tools/task-context-graphify.ts
|
|
33448
34575
|
var fs3 = __toESM(require("node:fs"));
|
|
33449
|
-
var
|
|
34576
|
+
var path7 = __toESM(require("node:path"));
|
|
33450
34577
|
var STALE_DAYS = 7;
|
|
33451
34578
|
var BFS_DEPTH = 2;
|
|
33452
34579
|
var MAX_AFFECTED_FILES = 15;
|
|
@@ -33487,7 +34614,33 @@ var KEYWORD_STOPWORDS = /* @__PURE__ */ new Set([
|
|
|
33487
34614
|
"feature",
|
|
33488
34615
|
"work",
|
|
33489
34616
|
"change",
|
|
33490
|
-
"changes"
|
|
34617
|
+
"changes",
|
|
34618
|
+
"trong",
|
|
34619
|
+
"kh\xF4ng",
|
|
34620
|
+
"tr\xEAn",
|
|
34621
|
+
"d\u01B0\u1EDBi",
|
|
34622
|
+
"ch\u1EE9a",
|
|
34623
|
+
"\u0111\xFAng",
|
|
34624
|
+
"ng\u01B0\u1EE3c",
|
|
34625
|
+
"ph\u1EA3i",
|
|
34626
|
+
"c\u1EA7n",
|
|
34627
|
+
"n\u1EBFu",
|
|
34628
|
+
"khi",
|
|
34629
|
+
"nh\u01B0",
|
|
34630
|
+
"theo",
|
|
34631
|
+
"sau",
|
|
34632
|
+
"tr\u01B0\u1EDBc"
|
|
34633
|
+
]);
|
|
34634
|
+
var LABEL_KEY_PREFIX = /^(assignee|adr|phase)[:-]/i;
|
|
34635
|
+
var LABEL_EXACT_DROP = /* @__PURE__ */ new Set([
|
|
34636
|
+
"auto-safe",
|
|
34637
|
+
"bug",
|
|
34638
|
+
"feat",
|
|
34639
|
+
"fix",
|
|
34640
|
+
"test",
|
|
34641
|
+
"metrics",
|
|
34642
|
+
"chore",
|
|
34643
|
+
"docs"
|
|
33491
34644
|
]);
|
|
33492
34645
|
function buildGraphifyContext(task, svc) {
|
|
33493
34646
|
const graphPath = findGraphPath(task.projectId, svc);
|
|
@@ -33498,20 +34651,21 @@ function buildGraphifyContext(task, svc) {
|
|
|
33498
34651
|
};
|
|
33499
34652
|
}
|
|
33500
34653
|
const keywords = extractKeywords(task);
|
|
33501
|
-
|
|
34654
|
+
const filePointerPaths = extractFilePointers(task.body ?? "");
|
|
34655
|
+
if (keywords.length === 0 && filePointerPaths.length === 0) {
|
|
33502
34656
|
return {
|
|
33503
34657
|
status: "no-matches",
|
|
33504
|
-
message: "Task title/AC/labels produced no usable
|
|
34658
|
+
message: "Task title/AC/labels/file-pointers produced no usable signal."
|
|
33505
34659
|
};
|
|
33506
34660
|
}
|
|
33507
34661
|
const data = JSON.parse(fs3.readFileSync(graphPath, "utf8"));
|
|
33508
34662
|
const nodeIndex = new Map(data.nodes.map((n) => [n.id, n]));
|
|
33509
34663
|
const adj = buildAdjacency(data.links);
|
|
33510
|
-
const startNodes = findStartNodes(data.nodes, keywords, 3);
|
|
34664
|
+
const startNodes = findStartNodes(data.nodes, keywords, filePointerPaths, 3);
|
|
33511
34665
|
if (startNodes.length === 0) {
|
|
33512
34666
|
return {
|
|
33513
34667
|
status: "no-matches",
|
|
33514
|
-
message: `No graph nodes matched keywords: ${keywords.join(", ")}`
|
|
34668
|
+
message: filePointerPaths.length > 0 ? `File pointers not in graph: ${filePointerPaths.join(", ")}; keywords: ${keywords.join(", ")}` : `No graph nodes matched keywords: ${keywords.join(", ")}`
|
|
33515
34669
|
};
|
|
33516
34670
|
}
|
|
33517
34671
|
const subgraph = bfs(
|
|
@@ -33542,12 +34696,12 @@ function buildGraphifyContext(task, svc) {
|
|
|
33542
34696
|
function findGraphPath(projectId, svc) {
|
|
33543
34697
|
const workspaces = svc.findWorkspaces(projectId);
|
|
33544
34698
|
for (const ws of workspaces) {
|
|
33545
|
-
const p =
|
|
34699
|
+
const p = path7.join(ws.cwd, "graphify-out", "graph.json");
|
|
33546
34700
|
if (fs3.existsSync(p)) return p;
|
|
33547
34701
|
}
|
|
33548
34702
|
const project = svc.getProject(projectId);
|
|
33549
34703
|
if (project) {
|
|
33550
|
-
const p =
|
|
34704
|
+
const p = path7.join(project.cwd, "graphify-out", "graph.json");
|
|
33551
34705
|
if (fs3.existsSync(p)) return p;
|
|
33552
34706
|
}
|
|
33553
34707
|
return null;
|
|
@@ -33555,7 +34709,7 @@ function findGraphPath(projectId, svc) {
|
|
|
33555
34709
|
function extractKeywords(task) {
|
|
33556
34710
|
const acSection = extractAcceptanceSection(task.body ?? "");
|
|
33557
34711
|
const raw = `${task.title} ${acSection}`.split(/\s+/);
|
|
33558
|
-
const fromLabels = (task.labels ?? []).map((l) => l.toLowerCase());
|
|
34712
|
+
const fromLabels = (task.labels ?? []).map((l) => l.toLowerCase()).filter((l) => !LABEL_KEY_PREFIX.test(l) && !LABEL_EXACT_DROP.has(l));
|
|
33559
34713
|
const all = [...raw.map((t) => t.toLowerCase()), ...fromLabels];
|
|
33560
34714
|
const deduped = /* @__PURE__ */ new Set();
|
|
33561
34715
|
for (const t of all) {
|
|
@@ -33566,15 +34720,51 @@ function extractKeywords(task) {
|
|
|
33566
34720
|
}
|
|
33567
34721
|
return Array.from(deduped);
|
|
33568
34722
|
}
|
|
34723
|
+
function extractFilePointers(body) {
|
|
34724
|
+
const match = body.match(/(?:^|\n)##\s*File Pointers\s*\n[\s\S]*?(?=\n##\s|$)/i);
|
|
34725
|
+
if (!match) return [];
|
|
34726
|
+
const paths = [];
|
|
34727
|
+
for (const line of splitLines(match[0])) {
|
|
34728
|
+
if (/\(NEW\)/i.test(line)) continue;
|
|
34729
|
+
const pathMatch = line.match(/^\s*-\s+`([^`]+)`/);
|
|
34730
|
+
if (!pathMatch) continue;
|
|
34731
|
+
paths.push(pathMatch[1].replace(/\\/g, "/"));
|
|
34732
|
+
}
|
|
34733
|
+
return paths;
|
|
34734
|
+
}
|
|
33569
34735
|
function cleanToken(t) {
|
|
33570
34736
|
return t.replace(/^[^a-z0-9_-]+|[^a-z0-9_-]+$/gi, "");
|
|
33571
34737
|
}
|
|
33572
34738
|
function extractAcceptanceSection(body) {
|
|
33573
|
-
const match = body.match(
|
|
34739
|
+
const match = body.match(/(?:^|\n)##\s*Acceptance\s*\n[\s\S]*?(?=\n##\s|$)/i);
|
|
33574
34740
|
if (!match) return "";
|
|
33575
34741
|
return match[0].slice(0, 500);
|
|
33576
34742
|
}
|
|
33577
|
-
function findStartNodes(nodes, keywords, topK) {
|
|
34743
|
+
function findStartNodes(nodes, keywords, filePointerPaths, topK) {
|
|
34744
|
+
if (filePointerPaths.length > 0) {
|
|
34745
|
+
const fileMatches = findStartNodesByFile(nodes, filePointerPaths);
|
|
34746
|
+
if (fileMatches.length > 0) return fileMatches;
|
|
34747
|
+
}
|
|
34748
|
+
return findStartNodesByKeyword(nodes, keywords, topK);
|
|
34749
|
+
}
|
|
34750
|
+
function findStartNodesByFile(nodes, paths) {
|
|
34751
|
+
const targets = new Set(paths.map((p) => p.replace(/\\/g, "/")));
|
|
34752
|
+
const matches = [];
|
|
34753
|
+
for (const n of nodes) {
|
|
34754
|
+
if (!n.source_file) continue;
|
|
34755
|
+
const normalized = n.source_file.replace(/\\/g, "/");
|
|
34756
|
+
if (targets.has(normalized)) {
|
|
34757
|
+
matches.push({ id: n.id, score: 100 });
|
|
34758
|
+
}
|
|
34759
|
+
}
|
|
34760
|
+
matches.sort((a, b) => {
|
|
34761
|
+
if (a.id < b.id) return -1;
|
|
34762
|
+
if (a.id > b.id) return 1;
|
|
34763
|
+
return 0;
|
|
34764
|
+
});
|
|
34765
|
+
return matches;
|
|
34766
|
+
}
|
|
34767
|
+
function findStartNodesByKeyword(nodes, keywords, topK) {
|
|
33578
34768
|
const scored = [];
|
|
33579
34769
|
for (const n of nodes) {
|
|
33580
34770
|
const label = (n.label ?? "").toLowerCase();
|
|
@@ -33662,87 +34852,6 @@ function computeStaleness(graphPath) {
|
|
|
33662
34852
|
};
|
|
33663
34853
|
}
|
|
33664
34854
|
|
|
33665
|
-
// src/core/domain/auto-safe-validator.ts
|
|
33666
|
-
var AUTO_SAFE_LABEL = "auto-safe";
|
|
33667
|
-
var AUTO_SAFE_SCOPE_HOURS_CEILING = 3;
|
|
33668
|
-
function validateAutoSafeTask(task) {
|
|
33669
|
-
const errors = [];
|
|
33670
|
-
const body = (task.body ?? "").trim();
|
|
33671
|
-
if (!body) {
|
|
33672
|
-
errors.push("Task body is empty \u2014 auto-safe requires AC, File Pointers, and Scope sections");
|
|
33673
|
-
return { valid: false, errors };
|
|
33674
|
-
}
|
|
33675
|
-
const ac = extractSection(body, /^acceptance(?:\s+criteria)?$/i);
|
|
33676
|
-
const filePointers = extractSection(body, /^file\s+pointers$/i);
|
|
33677
|
-
const scope = extractSection(body, /^scope$/i);
|
|
33678
|
-
if (!ac.trim()) {
|
|
33679
|
-
errors.push("Missing ## Acceptance (or ## Acceptance Criteria) section");
|
|
33680
|
-
} else if (!hasVerifiableShellCommand(ac)) {
|
|
33681
|
-
errors.push(
|
|
33682
|
-
"## Acceptance has no verifiable shell command (need `pnpm `, `node `, or a ```bash code block)"
|
|
33683
|
-
);
|
|
33684
|
-
}
|
|
33685
|
-
if (!filePointers.trim()) {
|
|
33686
|
-
errors.push("Missing ## File Pointers section");
|
|
33687
|
-
} else if (!hasConcretePath(filePointers)) {
|
|
33688
|
-
errors.push("## File Pointers has no concrete path (need at least one .ts/.md/.json/etc)");
|
|
33689
|
-
}
|
|
33690
|
-
if (!scope.trim()) {
|
|
33691
|
-
errors.push("Missing ## Scope section");
|
|
33692
|
-
} else {
|
|
33693
|
-
const upper = parseScopeHours(scope);
|
|
33694
|
-
if (upper === null) {
|
|
33695
|
-
errors.push('## Scope has no parseable hour estimate (e.g. "~2-3h", "2h", "1.5h")');
|
|
33696
|
-
} else if (upper > AUTO_SAFE_SCOPE_HOURS_CEILING) {
|
|
33697
|
-
errors.push(
|
|
33698
|
-
`## Scope estimate ${upper}h exceeds auto-safe ceiling of ${AUTO_SAFE_SCOPE_HOURS_CEILING}h`
|
|
33699
|
-
);
|
|
33700
|
-
}
|
|
33701
|
-
}
|
|
33702
|
-
if (mentionsBuildSensitive(body) && !hasSmokeStep(ac)) {
|
|
33703
|
-
errors.push(
|
|
33704
|
-
"## Acceptance must include a smoke step (body mentions build:mcp / loader / asset copy)"
|
|
33705
|
-
);
|
|
33706
|
-
}
|
|
33707
|
-
return { valid: errors.length === 0, errors };
|
|
33708
|
-
}
|
|
33709
|
-
function extractSection(body, headingMatcher) {
|
|
33710
|
-
const lines = body.split(/\r?\n/);
|
|
33711
|
-
const out = [];
|
|
33712
|
-
let inSection = false;
|
|
33713
|
-
for (const line of lines) {
|
|
33714
|
-
const headingMatch = /^##\s+(.+?)\s*$/.exec(line);
|
|
33715
|
-
if (headingMatch) {
|
|
33716
|
-
if (inSection) break;
|
|
33717
|
-
if (headingMatcher.test(headingMatch[1])) {
|
|
33718
|
-
inSection = true;
|
|
33719
|
-
continue;
|
|
33720
|
-
}
|
|
33721
|
-
}
|
|
33722
|
-
if (inSection) out.push(line);
|
|
33723
|
-
}
|
|
33724
|
-
return out.join("\n");
|
|
33725
|
-
}
|
|
33726
|
-
function hasVerifiableShellCommand(section) {
|
|
33727
|
-
if (/(?:^|[\s`])(?:pnpm|node)\s+\S/m.test(section)) return true;
|
|
33728
|
-
if (/```bash[\s\S]*?```/.test(section)) return true;
|
|
33729
|
-
return false;
|
|
33730
|
-
}
|
|
33731
|
-
function hasConcretePath(section) {
|
|
33732
|
-
return /[\w./\\-]+\.(?:ts|tsx|js|mjs|cjs|mts|json|md|sh|yml|yaml)\b/.test(section);
|
|
33733
|
-
}
|
|
33734
|
-
function parseScopeHours(section) {
|
|
33735
|
-
const match = /(\d+(?:\.\d+)?)\s*(?:[-–]\s*(\d+(?:\.\d+)?))?\s*h\b/i.exec(section);
|
|
33736
|
-
if (!match) return null;
|
|
33737
|
-
return parseFloat(match[2] ?? match[1]);
|
|
33738
|
-
}
|
|
33739
|
-
function mentionsBuildSensitive(body) {
|
|
33740
|
-
return /build:mcp|\bloader\b|asset\s+cop/i.test(body);
|
|
33741
|
-
}
|
|
33742
|
-
function hasSmokeStep(ac) {
|
|
33743
|
-
return /\bsmoke\b/i.test(ac) || /pnpm\s+run\s+build:mcp/i.test(ac);
|
|
33744
|
-
}
|
|
33745
|
-
|
|
33746
34855
|
// src/adapters/mcp/mcp-tools/task-tools.ts
|
|
33747
34856
|
function defaultBody(id, title) {
|
|
33748
34857
|
return `# ${id}: ${title}
|
|
@@ -33880,24 +34989,6 @@ var register = (server, svc) => {
|
|
|
33880
34989
|
return textResponse(svc.updateTask(id, input));
|
|
33881
34990
|
}
|
|
33882
34991
|
);
|
|
33883
|
-
server.registerTool(
|
|
33884
|
-
"tasks_update_batch",
|
|
33885
|
-
{
|
|
33886
|
-
description: "Update multiple tasks with the same patch (e.g. bulk mark DONE)",
|
|
33887
|
-
inputSchema: {
|
|
33888
|
-
ids: external_exports3.array(external_exports3.string()).describe("List of task IDs to update"),
|
|
33889
|
-
status: external_exports3.enum(["TODO", "READY", "IN-PROGRESS", "DONE", "CANCELLED"]).optional(),
|
|
33890
|
-
priority: external_exports3.enum(["critical", "high", "medium", "low"]).nullable().optional(),
|
|
33891
|
-
labels: external_exports3.array(external_exports3.string()).optional(),
|
|
33892
|
-
pinned: external_exports3.boolean().optional()
|
|
33893
|
-
}
|
|
33894
|
-
},
|
|
33895
|
-
async ({ ids, ...patch }) => {
|
|
33896
|
-
for (const id of ids) enforceAutoSafe(svc, id, patch);
|
|
33897
|
-
const results = ids.map((id) => svc.updateTask(id, patch));
|
|
33898
|
-
return textResponse(results);
|
|
33899
|
-
}
|
|
33900
|
-
);
|
|
33901
34992
|
};
|
|
33902
34993
|
function enforceAutoSafe(svc, id, input) {
|
|
33903
34994
|
if (!input.labels?.includes(AUTO_SAFE_LABEL)) return;
|
|
@@ -33917,6 +35008,58 @@ function enforceAutoSafe(svc, id, input) {
|
|
|
33917
35008
|
}
|
|
33918
35009
|
}
|
|
33919
35010
|
|
|
35011
|
+
// src/adapters/mcp/rules/mcp-rules-loader.ts
|
|
35012
|
+
var import_fs = require("fs");
|
|
35013
|
+
var import_path = require("path");
|
|
35014
|
+
var RULES_FILENAME = "mcp-rules.md";
|
|
35015
|
+
function rulesPath() {
|
|
35016
|
+
return (0, import_path.join)(__dirname, RULES_FILENAME);
|
|
35017
|
+
}
|
|
35018
|
+
function parseSection(content, heading) {
|
|
35019
|
+
const headingRe = new RegExp(`^##\\s+${heading}\\s*$`, "m");
|
|
35020
|
+
const head = content.match(headingRe);
|
|
35021
|
+
if (!head || head.index === void 0) return "";
|
|
35022
|
+
const startIdx = head.index + head[0].length;
|
|
35023
|
+
const rest = content.slice(startIdx);
|
|
35024
|
+
const next = rest.match(/^##\s/m);
|
|
35025
|
+
const endIdx = next && next.index !== void 0 ? startIdx + next.index : content.length;
|
|
35026
|
+
return content.slice(startIdx, endIdx).trim();
|
|
35027
|
+
}
|
|
35028
|
+
var EMPTY_RULES = {
|
|
35029
|
+
sessionStart: "",
|
|
35030
|
+
sessionCheckpoint: "",
|
|
35031
|
+
sessionResume: "",
|
|
35032
|
+
sessionEnd: "",
|
|
35033
|
+
conversationRead: ""
|
|
35034
|
+
};
|
|
35035
|
+
function loadMcpRules(path11 = rulesPath()) {
|
|
35036
|
+
if (!(0, import_fs.existsSync)(path11)) {
|
|
35037
|
+
console.warn(`[mcp-rules] file not found at ${path11} \u2014 returning empty rules`);
|
|
35038
|
+
return { ...EMPTY_RULES };
|
|
35039
|
+
}
|
|
35040
|
+
try {
|
|
35041
|
+
const content = (0, import_fs.readFileSync)(path11, "utf-8");
|
|
35042
|
+
const sessionStart = parseSection(content, "On session_start");
|
|
35043
|
+
const sessionCheckpoint = parseSection(content, "On session_checkpoint");
|
|
35044
|
+
const sessionResume = parseSection(content, "On session_resume");
|
|
35045
|
+
const sessionEnd = parseSection(content, "On session_end");
|
|
35046
|
+
const conversationRead = parseSection(content, "On conversation_read");
|
|
35047
|
+
for (const [name, value] of [
|
|
35048
|
+
["On session_start", sessionStart],
|
|
35049
|
+
["On session_checkpoint", sessionCheckpoint],
|
|
35050
|
+
["On session_resume", sessionResume],
|
|
35051
|
+
["On session_end", sessionEnd],
|
|
35052
|
+
["On conversation_read", conversationRead]
|
|
35053
|
+
]) {
|
|
35054
|
+
if (!value) console.warn(`[mcp-rules] "## ${name}" section missing in ${path11}`);
|
|
35055
|
+
}
|
|
35056
|
+
return { sessionStart, sessionCheckpoint, sessionResume, sessionEnd, conversationRead };
|
|
35057
|
+
} catch (err) {
|
|
35058
|
+
console.error(`[mcp-rules] failed to read ${path11}:`, err);
|
|
35059
|
+
return { ...EMPTY_RULES };
|
|
35060
|
+
}
|
|
35061
|
+
}
|
|
35062
|
+
|
|
33920
35063
|
// src/adapters/mcp/mcp-tools/conversation-tools.ts
|
|
33921
35064
|
var participantTypeSchema = external_exports3.enum(["human", "agent", "role"]);
|
|
33922
35065
|
var messageTypeSchema = external_exports3.enum([
|
|
@@ -33961,6 +35104,21 @@ function tryLifecycle(fn) {
|
|
|
33961
35104
|
throw e;
|
|
33962
35105
|
}
|
|
33963
35106
|
}
|
|
35107
|
+
function shouldInjectConversationEtiquette(status) {
|
|
35108
|
+
return status === "open" || status === "discussing";
|
|
35109
|
+
}
|
|
35110
|
+
function readConversation(svc, conversationId) {
|
|
35111
|
+
const conv = svc.getConversation(conversationId);
|
|
35112
|
+
if (!conv) return null;
|
|
35113
|
+
return {
|
|
35114
|
+
...conv,
|
|
35115
|
+
participants: svc.getConversationParticipants(conversationId),
|
|
35116
|
+
messages: svc.getConversationMessages(conversationId),
|
|
35117
|
+
actions: svc.getConversationActions(conversationId),
|
|
35118
|
+
links: svc.getConversationLinks(conversationId),
|
|
35119
|
+
etiquette: shouldInjectConversationEtiquette(conv.status) ? loadMcpRules().conversationRead : null
|
|
35120
|
+
};
|
|
35121
|
+
}
|
|
33964
35122
|
var register2 = (server, svc) => {
|
|
33965
35123
|
server.registerTool(
|
|
33966
35124
|
"conversation_open",
|
|
@@ -34124,22 +35282,16 @@ var register2 = (server, svc) => {
|
|
|
34124
35282
|
inputSchema: { conversationId: external_exports3.string() }
|
|
34125
35283
|
},
|
|
34126
35284
|
async ({ conversationId }) => {
|
|
34127
|
-
const
|
|
34128
|
-
if (!
|
|
34129
|
-
return textResponse(
|
|
34130
|
-
...conv,
|
|
34131
|
-
participants: svc.getConversationParticipants(conversationId),
|
|
34132
|
-
messages: svc.getConversationMessages(conversationId),
|
|
34133
|
-
actions: svc.getConversationActions(conversationId),
|
|
34134
|
-
links: svc.getConversationLinks(conversationId)
|
|
34135
|
-
});
|
|
35285
|
+
const result = readConversation(svc, conversationId);
|
|
35286
|
+
if (!result) return textResponse(`Conversation ${conversationId} not found`);
|
|
35287
|
+
return textResponse(result);
|
|
34136
35288
|
}
|
|
34137
35289
|
);
|
|
34138
35290
|
};
|
|
34139
35291
|
|
|
34140
35292
|
// src/adapters/mcp/mcp-tools/project-context-builder.ts
|
|
34141
35293
|
var fs4 = __toESM(require("fs"));
|
|
34142
|
-
var
|
|
35294
|
+
var path8 = __toESM(require("path"));
|
|
34143
35295
|
var SUMMARY_MAX_CHARS = 600;
|
|
34144
35296
|
function buildProjectContext(svc, projectId, depth = "full", contentRoot = process.env.CHODA_CONTENT_ROOT || "") {
|
|
34145
35297
|
const project = fetchProject(svc, projectId);
|
|
@@ -34222,7 +35374,7 @@ function loadRecentDecisions(sources, contentRoot, depth) {
|
|
|
34222
35374
|
}).filter((d) => d.excerpt.length > 0);
|
|
34223
35375
|
}
|
|
34224
35376
|
function readMarkdown(contentRoot, sourcePath, depth) {
|
|
34225
|
-
const absolute =
|
|
35377
|
+
const absolute = path8.isAbsolute(sourcePath) ? sourcePath : path8.join(contentRoot, sourcePath);
|
|
34226
35378
|
if (!fs4.existsSync(absolute)) return null;
|
|
34227
35379
|
try {
|
|
34228
35380
|
const raw = fs4.readFileSync(absolute, "utf-8");
|
|
@@ -34350,37 +35502,6 @@ var register4 = (server, svc) => {
|
|
|
34350
35502
|
return textResponse(result);
|
|
34351
35503
|
}
|
|
34352
35504
|
);
|
|
34353
|
-
server.registerTool(
|
|
34354
|
-
"workspace_remove",
|
|
34355
|
-
{
|
|
34356
|
-
description: "Remove a workspace. Default soft (archive). Pass hard=true for permanent DELETE \u2014 rejected if any sessions still reference the workspace.",
|
|
34357
|
-
inputSchema: {
|
|
34358
|
-
projectId: external_exports3.string().describe("Parent project ID"),
|
|
34359
|
-
workspaceId: external_exports3.string().describe("Workspace ID to remove"),
|
|
34360
|
-
hard: external_exports3.boolean().optional().describe("true = permanent DELETE (rejected if referenced); default false = soft archive")
|
|
34361
|
-
}
|
|
34362
|
-
},
|
|
34363
|
-
async ({ projectId, workspaceId, hard }) => {
|
|
34364
|
-
if (!hard) {
|
|
34365
|
-
return textResponse(archiveOrError(svc, projectId, workspaceId));
|
|
34366
|
-
}
|
|
34367
|
-
const ws = svc.getWorkspace(workspaceId);
|
|
34368
|
-
if (!ws) return textResponse(`Workspace ${workspaceId} not found`);
|
|
34369
|
-
if (ws.projectId !== projectId) {
|
|
34370
|
-
return textResponse(
|
|
34371
|
-
`Workspace ${workspaceId} belongs to project ${ws.projectId}, not ${projectId}`
|
|
34372
|
-
);
|
|
34373
|
-
}
|
|
34374
|
-
const refs = svc.countWorkspaceReferences(workspaceId);
|
|
34375
|
-
if (refs.sessions > 0) {
|
|
34376
|
-
return textResponse(
|
|
34377
|
-
`Cannot hard-delete workspace ${workspaceId} \u2014 ${refs.sessions} session(s) still reference it. Use workspace_archive instead.`
|
|
34378
|
-
);
|
|
34379
|
-
}
|
|
34380
|
-
svc.deleteWorkspace(workspaceId);
|
|
34381
|
-
return textResponse({ ok: true, hard: true });
|
|
34382
|
-
}
|
|
34383
|
-
);
|
|
34384
35505
|
};
|
|
34385
35506
|
function archiveOrError(svc, projectId, workspaceId) {
|
|
34386
35507
|
const ws = svc.getWorkspace(workspaceId);
|
|
@@ -34398,68 +35519,19 @@ function archiveOrError(svc, projectId, workspaceId) {
|
|
|
34398
35519
|
return { ok: true, archivedAt: archived.archivedAt };
|
|
34399
35520
|
}
|
|
34400
35521
|
|
|
34401
|
-
// src/adapters/mcp/rules/session-rules-loader.ts
|
|
34402
|
-
var import_fs = require("fs");
|
|
34403
|
-
var import_path = require("path");
|
|
34404
|
-
var RULES_FILENAME = "session-rules.md";
|
|
34405
|
-
function rulesPath() {
|
|
34406
|
-
return (0, import_path.join)(__dirname, RULES_FILENAME);
|
|
34407
|
-
}
|
|
34408
|
-
function parseSection(content, heading) {
|
|
34409
|
-
const headingRe = new RegExp(`^##\\s+${heading}\\s*$`, "m");
|
|
34410
|
-
const head = content.match(headingRe);
|
|
34411
|
-
if (!head || head.index === void 0) return "";
|
|
34412
|
-
const startIdx = head.index + head[0].length;
|
|
34413
|
-
const rest = content.slice(startIdx);
|
|
34414
|
-
const next = rest.match(/^##\s/m);
|
|
34415
|
-
const endIdx = next && next.index !== void 0 ? startIdx + next.index : content.length;
|
|
34416
|
-
return content.slice(startIdx, endIdx).trim();
|
|
34417
|
-
}
|
|
34418
|
-
var EMPTY_RULES = {
|
|
34419
|
-
sessionStart: "",
|
|
34420
|
-
sessionCheckpoint: "",
|
|
34421
|
-
sessionResume: "",
|
|
34422
|
-
sessionEnd: ""
|
|
34423
|
-
};
|
|
34424
|
-
function loadSessionRules(path7 = rulesPath()) {
|
|
34425
|
-
if (!(0, import_fs.existsSync)(path7)) {
|
|
34426
|
-
console.warn(`[session-rules] file not found at ${path7} \u2014 returning empty rules`);
|
|
34427
|
-
return { ...EMPTY_RULES };
|
|
34428
|
-
}
|
|
34429
|
-
try {
|
|
34430
|
-
const content = (0, import_fs.readFileSync)(path7, "utf-8");
|
|
34431
|
-
const sessionStart = parseSection(content, "On session_start");
|
|
34432
|
-
const sessionCheckpoint = parseSection(content, "On session_checkpoint");
|
|
34433
|
-
const sessionResume = parseSection(content, "On session_resume");
|
|
34434
|
-
const sessionEnd = parseSection(content, "On session_end");
|
|
34435
|
-
for (const [name, value] of [
|
|
34436
|
-
["On session_start", sessionStart],
|
|
34437
|
-
["On session_checkpoint", sessionCheckpoint],
|
|
34438
|
-
["On session_resume", sessionResume],
|
|
34439
|
-
["On session_end", sessionEnd]
|
|
34440
|
-
]) {
|
|
34441
|
-
if (!value) console.warn(`[session-rules] "## ${name}" section missing in ${path7}`);
|
|
34442
|
-
}
|
|
34443
|
-
return { sessionStart, sessionCheckpoint, sessionResume, sessionEnd };
|
|
34444
|
-
} catch (err) {
|
|
34445
|
-
console.error(`[session-rules] failed to read ${path7}:`, err);
|
|
34446
|
-
return { ...EMPTY_RULES };
|
|
34447
|
-
}
|
|
34448
|
-
}
|
|
34449
|
-
|
|
34450
35522
|
// src/adapters/mcp/mcp-tools/workspace-resolver.ts
|
|
34451
|
-
var
|
|
35523
|
+
var path9 = __toESM(require("path"));
|
|
34452
35524
|
var isWindows = process.platform === "win32";
|
|
34453
|
-
function
|
|
34454
|
-
const resolved =
|
|
35525
|
+
function normalize2(p) {
|
|
35526
|
+
const resolved = path9.resolve(p).replace(/[\\/]+$/, "");
|
|
34455
35527
|
return isWindows ? resolved.toLowerCase().replace(/\//g, "\\") : resolved;
|
|
34456
35528
|
}
|
|
34457
35529
|
function isDescendantOrEqual(parent, child) {
|
|
34458
35530
|
if (parent === child) return true;
|
|
34459
|
-
const rel =
|
|
35531
|
+
const rel = path9.relative(parent, child);
|
|
34460
35532
|
if (rel === "") return true;
|
|
34461
35533
|
if (rel.startsWith("..")) return false;
|
|
34462
|
-
return !
|
|
35534
|
+
return !path9.isAbsolute(rel);
|
|
34463
35535
|
}
|
|
34464
35536
|
function resolveWorkspaceId(input) {
|
|
34465
35537
|
const { explicitWorkspaceId, cwd, workspaces } = input;
|
|
@@ -34472,8 +35544,8 @@ function resolveWorkspaceId(input) {
|
|
|
34472
35544
|
${list}`
|
|
34473
35545
|
);
|
|
34474
35546
|
}
|
|
34475
|
-
const normalizedCwd =
|
|
34476
|
-
const matches = workspaces.map((w) => ({ workspace: w, normalized:
|
|
35547
|
+
const normalizedCwd = normalize2(cwd);
|
|
35548
|
+
const matches = workspaces.map((w) => ({ workspace: w, normalized: normalize2(w.cwd) })).filter((m) => isDescendantOrEqual(m.normalized, normalizedCwd)).sort((a, b) => b.normalized.length - a.normalized.length);
|
|
34477
35549
|
if (matches.length === 0) {
|
|
34478
35550
|
const list = workspaces.map((w) => ` - ${w.id} (${w.cwd})`).join("\n");
|
|
34479
35551
|
throw new WorkspaceResolutionError(
|
|
@@ -34639,7 +35711,7 @@ var register5 = (server, svc, git = new GitOpsImpl()) => {
|
|
|
34639
35711
|
});
|
|
34640
35712
|
const lastSession = loadLastSession(svc, projectId, resolvedWorkspaceId);
|
|
34641
35713
|
const bundle = buildProjectContext(svc, projectId, "summary");
|
|
34642
|
-
const rules =
|
|
35714
|
+
const rules = loadMcpRules();
|
|
34643
35715
|
return {
|
|
34644
35716
|
sessionId: session.id,
|
|
34645
35717
|
workspaceId: session.workspaceId,
|
|
@@ -34731,7 +35803,7 @@ var register5 = (server, svc, git = new GitOpsImpl()) => {
|
|
|
34731
35803
|
},
|
|
34732
35804
|
async ({ sessionId }) => tryLifecycle2(() => {
|
|
34733
35805
|
const result = svc.resumeSession(sessionId);
|
|
34734
|
-
const rules =
|
|
35806
|
+
const rules = loadMcpRules();
|
|
34735
35807
|
return {
|
|
34736
35808
|
session: result.session,
|
|
34737
35809
|
checkpoint: result.checkpoint,
|
|
@@ -34799,7 +35871,7 @@ function buildProjectSummary(bundle) {
|
|
|
34799
35871
|
if (!bundle) return null;
|
|
34800
35872
|
const pieces = [];
|
|
34801
35873
|
if (bundle.architecture) {
|
|
34802
|
-
pieces.push(bundle.architecture
|
|
35874
|
+
pieces.push(splitLines(bundle.architecture).slice(0, 3).join(" ").slice(0, 200));
|
|
34803
35875
|
}
|
|
34804
35876
|
return pieces.length > 0 ? pieces.join(" \u2014 ") : null;
|
|
34805
35877
|
}
|
|
@@ -34975,24 +36047,6 @@ var register6 = (server, svc) => {
|
|
|
34975
36047
|
},
|
|
34976
36048
|
async ({ id, reason }) => tryLifecycle3(() => svc.archiveInbox(id, reason))
|
|
34977
36049
|
);
|
|
34978
|
-
server.registerTool(
|
|
34979
|
-
"inbox_delete",
|
|
34980
|
-
{
|
|
34981
|
-
description: "Hard delete an inbox item. Only allowed for raw or archived items.",
|
|
34982
|
-
inputSchema: { id: external_exports3.string() }
|
|
34983
|
-
},
|
|
34984
|
-
async ({ id }) => {
|
|
34985
|
-
const item = svc.getInbox(id);
|
|
34986
|
-
if (!item) return textResponse(`Inbox ${id} not found`);
|
|
34987
|
-
if (item.status !== "raw" && item.status !== "archived") {
|
|
34988
|
-
return textResponse(
|
|
34989
|
-
`Inbox ${id} is ${item.status} \u2014 only raw or archived items can be deleted`
|
|
34990
|
-
);
|
|
34991
|
-
}
|
|
34992
|
-
svc.deleteInbox(id);
|
|
34993
|
-
return textResponse({ deleted: id });
|
|
34994
|
-
}
|
|
34995
|
-
);
|
|
34996
36050
|
};
|
|
34997
36051
|
|
|
34998
36052
|
// src/adapters/mcp/mcp-tools/backup-tools.ts
|
|
@@ -35057,22 +36111,22 @@ function runBackup(db, userData, now2 = /* @__PURE__ */ new Date()) {
|
|
|
35057
36111
|
}
|
|
35058
36112
|
|
|
35059
36113
|
// src/adapters/mcp/mcp-tools/backup-tools.ts
|
|
35060
|
-
function register7(server, svc,
|
|
36114
|
+
function register7(server, svc, dataDir, dbPath) {
|
|
35061
36115
|
server.registerTool(
|
|
35062
36116
|
"backup_list",
|
|
35063
36117
|
{
|
|
35064
36118
|
description: "List existing SQLite backups under <dataDir>/backups, newest first. Use before destructive batch ops to confirm a recent backup exists.",
|
|
35065
36119
|
inputSchema: {}
|
|
35066
36120
|
},
|
|
35067
|
-
async () => textResponse(listBackups(
|
|
36121
|
+
async () => textResponse(listBackups(dataDir))
|
|
35068
36122
|
);
|
|
35069
36123
|
server.registerTool(
|
|
35070
36124
|
"backup_create",
|
|
35071
36125
|
{
|
|
35072
|
-
description: "Create a SQLite backup at <dataDir>/backups/choda-deck-<YYYY-MM-DD>.db (overwrites same-day file, prunes to 7 newest). Returns the new BackupInfo. Run before risky operations
|
|
36126
|
+
description: "Create a SQLite backup at <dataDir>/backups/choda-deck-<YYYY-MM-DD>.db (overwrites same-day file, prunes to 7 newest). Returns the new BackupInfo. Run before risky bulk operations.",
|
|
35073
36127
|
inputSchema: {}
|
|
35074
36128
|
},
|
|
35075
|
-
async () => textResponse(runBackup(svc,
|
|
36129
|
+
async () => textResponse(runBackup(svc, dataDir))
|
|
35076
36130
|
);
|
|
35077
36131
|
server.registerTool(
|
|
35078
36132
|
"backup_restore",
|
|
@@ -35083,13 +36137,13 @@ function register7(server, svc, dataDir2, dbPath2) {
|
|
|
35083
36137
|
}
|
|
35084
36138
|
},
|
|
35085
36139
|
async ({ filename }) => {
|
|
35086
|
-
const source = (0, import_path3.join)(backupDir(
|
|
36140
|
+
const source = (0, import_path3.join)(backupDir(dataDir), filename);
|
|
35087
36141
|
if (!(0, import_fs3.existsSync)(source)) {
|
|
35088
36142
|
return textResponse({ ok: false, error: `Backup file not found: ${filename}` });
|
|
35089
36143
|
}
|
|
35090
36144
|
try {
|
|
35091
36145
|
svc.close();
|
|
35092
|
-
(0, import_fs3.copyFileSync)(source,
|
|
36146
|
+
(0, import_fs3.copyFileSync)(source, dbPath);
|
|
35093
36147
|
} catch (err) {
|
|
35094
36148
|
return textResponse({ ok: false, error: err.message });
|
|
35095
36149
|
}
|
|
@@ -35142,18 +36196,6 @@ var register8 = (server, svc) => {
|
|
|
35142
36196
|
})
|
|
35143
36197
|
)
|
|
35144
36198
|
);
|
|
35145
|
-
server.registerTool(
|
|
35146
|
-
"knowledge_register_existing",
|
|
35147
|
-
{
|
|
35148
|
-
description: "Index an existing knowledge MD file without writing/overwriting it. Reads frontmatter, INSERTs (or upserts on slug match) into knowledge_index, schedules embedding. Use for backfill of pre-existing ADRs (e.g. workspace ADRs migrated under a multi-repo project). Frontmatter projectId + workspaceId must match the arguments.",
|
|
35149
|
-
inputSchema: {
|
|
35150
|
-
filePath: external_exports3.string().describe("Absolute path to the existing .md file"),
|
|
35151
|
-
projectId: external_exports3.string().describe("Project ID (must match frontmatter)"),
|
|
35152
|
-
workspaceId: external_exports3.string().optional().describe("Workspace ID (must match frontmatter; omit for project-level entries)")
|
|
35153
|
-
}
|
|
35154
|
-
},
|
|
35155
|
-
async ({ filePath, projectId, workspaceId }) => textResponse(svc.registerExistingKnowledge({ filePath, projectId, workspaceId }))
|
|
35156
|
-
);
|
|
35157
36199
|
server.registerTool(
|
|
35158
36200
|
"knowledge_get",
|
|
35159
36201
|
{
|
|
@@ -35240,27 +36282,189 @@ var register8 = (server, svc) => {
|
|
|
35240
36282
|
);
|
|
35241
36283
|
};
|
|
35242
36284
|
|
|
35243
|
-
// src/
|
|
35244
|
-
var
|
|
35245
|
-
|
|
36285
|
+
// src/core/domain/stats-service.ts
|
|
36286
|
+
var FLOOR_CALLS = 5;
|
|
36287
|
+
var BROKEN_ERROR_RATE = 0.2;
|
|
36288
|
+
var MVP_ERROR_RATE = 0.05;
|
|
36289
|
+
var MVP_TOP_FRACTION = 0.25;
|
|
36290
|
+
function computeStatsReport(input) {
|
|
36291
|
+
const merged = mergeWithCanonical(input.rows, input.canonical);
|
|
36292
|
+
const totalCalls = merged.reduce((sum, t) => sum + t.calls, 0);
|
|
36293
|
+
const mvpThreshold = computeMvpThreshold(merged);
|
|
36294
|
+
const perTool = merged.map((t) => ({
|
|
36295
|
+
tool: t.tool,
|
|
36296
|
+
calls: t.calls,
|
|
36297
|
+
errorRate: t.calls === 0 ? 0 : t.errors / t.calls,
|
|
36298
|
+
avgDurationMs: t.avgDurationMs,
|
|
36299
|
+
lastUsedAt: t.lastUsedAt,
|
|
36300
|
+
classification: classify(t, mvpThreshold)
|
|
36301
|
+
}));
|
|
36302
|
+
perTool.sort((a, b) => b.calls - a.calls || a.tool.localeCompare(b.tool));
|
|
36303
|
+
return {
|
|
36304
|
+
period: input.period,
|
|
36305
|
+
totalCalls,
|
|
36306
|
+
perTool,
|
|
36307
|
+
deadInWindow: perTool.filter((t) => t.classification === "dead-in-window").map((t) => t.tool),
|
|
36308
|
+
brokenTools: perTool.filter((t) => t.classification === "broken").map((t) => t.tool)
|
|
36309
|
+
};
|
|
36310
|
+
}
|
|
36311
|
+
function mergeWithCanonical(rows, canonical) {
|
|
36312
|
+
const seen = /* @__PURE__ */ new Map();
|
|
36313
|
+
for (const r of rows) {
|
|
36314
|
+
seen.set(r.tool, {
|
|
36315
|
+
tool: r.tool,
|
|
36316
|
+
calls: r.calls,
|
|
36317
|
+
errors: r.errors,
|
|
36318
|
+
avgDurationMs: r.avgDurationMs,
|
|
36319
|
+
lastUsedAt: r.lastUsedAt
|
|
36320
|
+
});
|
|
36321
|
+
}
|
|
36322
|
+
for (const name of canonical) {
|
|
36323
|
+
if (!seen.has(name)) {
|
|
36324
|
+
seen.set(name, {
|
|
36325
|
+
tool: name,
|
|
36326
|
+
calls: 0,
|
|
36327
|
+
errors: 0,
|
|
36328
|
+
avgDurationMs: 0,
|
|
36329
|
+
lastUsedAt: null
|
|
36330
|
+
});
|
|
36331
|
+
}
|
|
36332
|
+
}
|
|
36333
|
+
return Array.from(seen.values());
|
|
36334
|
+
}
|
|
36335
|
+
function computeMvpThreshold(merged) {
|
|
36336
|
+
const active = merged.filter((t) => t.calls > 0).map((t) => t.calls);
|
|
36337
|
+
if (active.length === 0) return Infinity;
|
|
36338
|
+
active.sort((a, b) => b - a);
|
|
36339
|
+
const topN = Math.max(1, Math.ceil(active.length * MVP_TOP_FRACTION));
|
|
36340
|
+
return active[topN - 1];
|
|
36341
|
+
}
|
|
36342
|
+
function classify(t, mvpThreshold) {
|
|
36343
|
+
if (t.calls === 0) return "dead-in-window";
|
|
36344
|
+
const errorRate = t.errors / t.calls;
|
|
36345
|
+
if (t.calls >= FLOOR_CALLS && errorRate > BROKEN_ERROR_RATE) return "broken";
|
|
36346
|
+
if (t.calls >= FLOOR_CALLS && errorRate < MVP_ERROR_RATE && t.calls >= mvpThreshold) {
|
|
36347
|
+
return "mvp";
|
|
36348
|
+
}
|
|
36349
|
+
return "emerging";
|
|
36350
|
+
}
|
|
36351
|
+
|
|
36352
|
+
// src/adapters/mcp/mcp-tools/stats-tools.ts
|
|
36353
|
+
var register9 = (server, svc) => {
|
|
36354
|
+
server.registerTool(
|
|
36355
|
+
"stats_report",
|
|
36356
|
+
{
|
|
36357
|
+
description: "Report MCP tool usage stats over an optional ISO time window \u2014 returns per-tool calls / errorRate / avgDurationMs / lastUsedAt + classification (mvp / broken / dead-in-window / emerging) plus deadInWindow + brokenTools name lists. No projectId/session breakdown V0. Self-records (this call appears in the next stats_report).",
|
|
36358
|
+
inputSchema: {
|
|
36359
|
+
since: external_exports3.string().optional().describe("Inclusive lower bound (ISO 8601). Omit for all-time."),
|
|
36360
|
+
until: external_exports3.string().optional().describe("Inclusive upper bound (ISO 8601). Omit for now.")
|
|
36361
|
+
}
|
|
36362
|
+
},
|
|
36363
|
+
async ({ since, until }) => {
|
|
36364
|
+
const period = { since: since ?? null, until: until ?? null };
|
|
36365
|
+
const rows = svc.queryToolInvocations(period);
|
|
36366
|
+
const report = computeStatsReport({
|
|
36367
|
+
rows,
|
|
36368
|
+
canonical: server.registeredToolNames,
|
|
36369
|
+
period
|
|
36370
|
+
});
|
|
36371
|
+
return textResponse(report);
|
|
36372
|
+
}
|
|
36373
|
+
);
|
|
36374
|
+
};
|
|
36375
|
+
|
|
36376
|
+
// src/adapters/mcp/mcp-tools/cleanup-tools.ts
|
|
36377
|
+
var fs6 = __toESM(require("node:fs"));
|
|
36378
|
+
var KNOWLEDGE_ACTION_ENUM = ["delete", "leave"];
|
|
36379
|
+
var register10 = (server, svc) => {
|
|
36380
|
+
server.registerTool(
|
|
36381
|
+
"cleanup_worktree_orphans",
|
|
36382
|
+
{
|
|
36383
|
+
description: "Detect and clean orphan workspaces + knowledge entries left by deleted git worktrees. Detection: cwd / filePath matches `.worktrees` segment AND the path no longer exists on disk. Default dry-run \u2014 pass dryRun=false to mutate. Workspaces are archived (idempotent); knowledge action is configurable: `delete` removes the row + INDEX entry, `leave` (default) reports them for manual recovery without mutating.",
|
|
36384
|
+
inputSchema: {
|
|
36385
|
+
projectId: external_exports3.string().describe("Project ID to scan"),
|
|
36386
|
+
dryRun: external_exports3.boolean().optional().describe("Default true \u2014 list candidates without mutating. Pass false to apply."),
|
|
36387
|
+
knowledgeAction: external_exports3.enum(KNOWLEDGE_ACTION_ENUM).optional().describe("How to handle orphan knowledge when not dry-run. Default `leave`.")
|
|
36388
|
+
}
|
|
36389
|
+
},
|
|
36390
|
+
async ({ projectId, dryRun, knowledgeAction }) => {
|
|
36391
|
+
const project = svc.getProject(projectId);
|
|
36392
|
+
if (!project) return textResponse(`Project ${projectId} not found`);
|
|
36393
|
+
const isDryRun = dryRun ?? true;
|
|
36394
|
+
const action = knowledgeAction ?? "leave";
|
|
36395
|
+
const orphanWorkspaces = svc.findWorkspaces(projectId, false).filter((ws) => isLikelyWorktreePath(ws.cwd) && !fs6.existsSync(ws.cwd)).map((ws) => ({ id: ws.id, label: ws.label, cwd: ws.cwd }));
|
|
36396
|
+
const orphanKnowledge = svc.listKnowledge({ projectId }).filter((k) => isLikelyWorktreePath(k.filePath) && !fs6.existsSync(k.filePath)).map((k) => ({ slug: k.slug, filePath: k.filePath, workspaceId: k.workspaceId }));
|
|
36397
|
+
const candidates = { workspaces: orphanWorkspaces, knowledge: orphanKnowledge };
|
|
36398
|
+
if (isDryRun) {
|
|
36399
|
+
const result2 = {
|
|
36400
|
+
dryRun: true,
|
|
36401
|
+
knowledgeAction: action,
|
|
36402
|
+
archivedWorkspaces: [],
|
|
36403
|
+
deletedKnowledge: [],
|
|
36404
|
+
leftKnowledge: [],
|
|
36405
|
+
candidates
|
|
36406
|
+
};
|
|
36407
|
+
return textResponse(result2);
|
|
36408
|
+
}
|
|
36409
|
+
const archivedWorkspaces = [];
|
|
36410
|
+
for (const ws of orphanWorkspaces) {
|
|
36411
|
+
const archived = svc.archiveWorkspace(ws.id);
|
|
36412
|
+
if (archived) archivedWorkspaces.push(ws);
|
|
36413
|
+
}
|
|
36414
|
+
const deletedKnowledge = [];
|
|
36415
|
+
const leftKnowledge = [];
|
|
36416
|
+
if (action === "delete") {
|
|
36417
|
+
for (const k of orphanKnowledge) {
|
|
36418
|
+
svc.deleteKnowledge(k.slug);
|
|
36419
|
+
deletedKnowledge.push(k);
|
|
36420
|
+
}
|
|
36421
|
+
} else {
|
|
36422
|
+
for (const k of orphanKnowledge) leftKnowledge.push(k);
|
|
36423
|
+
}
|
|
36424
|
+
const result = {
|
|
36425
|
+
dryRun: false,
|
|
36426
|
+
knowledgeAction: action,
|
|
36427
|
+
archivedWorkspaces,
|
|
36428
|
+
deletedKnowledge,
|
|
36429
|
+
leftKnowledge,
|
|
36430
|
+
candidates
|
|
36431
|
+
};
|
|
36432
|
+
return textResponse(result);
|
|
36433
|
+
}
|
|
36434
|
+
);
|
|
36435
|
+
};
|
|
36436
|
+
|
|
36437
|
+
// src/adapters/mcp/server-bootstrap.ts
|
|
36438
|
+
async function startMcpServer() {
|
|
36439
|
+
const { dbPath, dataDir } = resolveDataPaths();
|
|
36440
|
+
fs7.mkdirSync(path10.dirname(dbPath), { recursive: true });
|
|
35246
36441
|
const svc = new SqliteTaskService(dbPath);
|
|
35247
36442
|
await svc.initializeAsync();
|
|
35248
36443
|
const server = new McpServer(
|
|
35249
36444
|
{ name: "choda-tasks", version: "0.2.0" },
|
|
35250
36445
|
{ capabilities: { tools: {} } }
|
|
35251
36446
|
);
|
|
35252
|
-
|
|
35253
|
-
|
|
35254
|
-
|
|
35255
|
-
|
|
35256
|
-
|
|
35257
|
-
|
|
35258
|
-
|
|
35259
|
-
|
|
36447
|
+
const instrumented = createInstrumentedServer(server, svc);
|
|
36448
|
+
register(instrumented, svc);
|
|
36449
|
+
register2(instrumented, svc);
|
|
36450
|
+
register3(instrumented, svc);
|
|
36451
|
+
register4(instrumented, svc);
|
|
36452
|
+
register5(instrumented, svc);
|
|
36453
|
+
register6(instrumented, svc);
|
|
36454
|
+
register7(instrumented, svc, dataDir, dbPath);
|
|
36455
|
+
register8(instrumented, svc);
|
|
36456
|
+
register9(instrumented, svc);
|
|
36457
|
+
register10(instrumented, svc);
|
|
36458
|
+
console.error(`[choda-deck] registered ${instrumented.registeredToolNames.length} MCP tools`);
|
|
35260
36459
|
const transport = new StdioServerTransport();
|
|
35261
36460
|
await server.connect(transport);
|
|
35262
36461
|
}
|
|
35263
|
-
|
|
36462
|
+
|
|
36463
|
+
// src/adapters/mcp/server.ts
|
|
36464
|
+
process.stderr.write(
|
|
36465
|
+
"[choda-deck] DEPRECATED: dist/mcp-server.cjs will be removed in v0.2. Update your MCP config to run `choda-deck mcp serve` instead.\n"
|
|
36466
|
+
);
|
|
36467
|
+
startMcpServer().catch((err) => {
|
|
35264
36468
|
console.error("MCP Task Server failed:", err);
|
|
35265
36469
|
process.exit(1);
|
|
35266
36470
|
});
|