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.
@@ -2234,8 +2234,8 @@ var require_resolve = __commonJS({
2234
2234
  }
2235
2235
  return count;
2236
2236
  }
2237
- function getFullPath(resolver, id = "", normalize2) {
2238
- if (normalize2 !== false)
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(path7) {
3229
- let input = path7;
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 [path7, query] = wsComponent.resourceName.split("?");
3429
- wsComponent.path = path7 && path7 !== "/" ? path7 : void 0;
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 normalize2(uri, options) {
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: normalize2,
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, fs6, exportName) {
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, fs6[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: path7, errorMaps, issueData } = params;
7227
- const fullPath = [...path7, ...issueData.path || []];
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, path7, key) {
7346
+ constructor(parent, value, path11, key) {
7343
7347
  this._cachedPath = [];
7344
7348
  this.parent = parent;
7345
7349
  this.data = value;
7346
- this._path = path7;
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, path7) {
11270
- if (!path7)
11273
+ function getElementAtPath(obj, path11) {
11274
+ if (!path11)
11271
11275
  return obj;
11272
- return path7.reduce((acc, key) => acc?.[key], obj);
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(path7, issues) {
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(path7);
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, path7 = []) => {
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 = [...path7, ...issue2.path];
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 path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
11885
- for (const seg of path7) {
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 path7 = ref.slice(1).split("/").filter(Boolean);
24292
- if (path7.length === 0) {
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 (path7[0] === defsKey) {
24297
- const key = path7[1];
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 main2 = parseDef(schema._def, name === void 0 ? refs : {
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
- main2.title = title;
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
- ...main2,
27499
+ ...main,
27496
27500
  [refs.definitionPath]: definitions
27497
- } : main2 : {
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]: main2
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: cleanDecision,
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: cleanDecision,
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, { status: "discussing" });
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 rawSummary = input.decisionSummary ?? input.handoff.resumePoint ?? "Session ended";
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 path = __toESM(require("path"));
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(path.dirname(filePath), { recursive: true });
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 = path.basename(input.filePath, ".md");
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 path.join(cwd, "docs", "knowledge", `${slug}.md`);
32305
+ return path4.join(cwd, "docs", "knowledge", `${slug}.md`);
31333
32306
  }
31334
- return path.join(this.contentRoot, "30-Knowledge", `${slug}.md`);
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 = path.join(cwd, "docs", "knowledge", "INDEX.md");
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(path.dirname(indexPath), { recursive: true });
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 path3 = __toESM(require("path"));
33610
+ var path6 = __toESM(require("path"));
32624
33611
 
32625
33612
  // src/core/paths.ts
32626
33613
  var os = __toESM(require("os"));
32627
- var path2 = __toESM(require("path"));
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 dataDir2;
32632
- let dbPath2;
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
- dbPath2 = path2.resolve(legacyDbPath);
32638
- dataDir2 = electronDataDir ?? envDataDir ?? path2.dirname(dbPath2);
33624
+ dbPath = path5.resolve(legacyDbPath);
33625
+ dataDir = electronDataDir ?? envDataDir ?? path5.dirname(dbPath);
32639
33626
  } else if (envDataDir) {
32640
- dataDir2 = path2.resolve(envDataDir);
32641
- dbPath2 = path2.join(dataDir2, "database", "choda-deck.db");
33627
+ dataDir = path5.resolve(envDataDir);
33628
+ dbPath = path5.join(dataDir, "database", "choda-deck.db");
32642
33629
  } else if (electronDataDir) {
32643
- dataDir2 = electronDataDir;
32644
- dbPath2 = path2.join(dataDir2, "database", "choda-deck.db");
33630
+ dataDir = electronDataDir;
33631
+ dbPath = path5.join(dataDir, "database", "choda-deck.db");
32645
33632
  } else {
32646
- dataDir2 = path2.join(process.cwd(), "data");
32647
- dbPath2 = path2.join(dataDir2, "database", "choda-deck.db");
33633
+ dataDir = path5.join(process.cwd(), "data");
33634
+ dbPath = path5.join(dataDir, "database", "choda-deck.db");
32648
33635
  }
32649
33636
  return {
32650
- dataDir: dataDir2,
32651
- dbPath: dbPath2,
32652
- artifactsDir: path2.join(dataDir2, "artifacts"),
32653
- backupsDir: path2.join(dataDir2, "backups")
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 path2.resolve(envDir);
32659
- return path2.join(os.tmpdir(), "choda-events");
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 = path3.join(dir, `${projectId}.jsonl`);
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
- emitConversationEvent(conv.projectId, {
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(dbPath2) {
33079
- this.db = new import_better_sqlite3.default(dbPath2);
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 path4 = __toESM(require("node:path"));
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
- if (keywords.length === 0) {
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 keywords."
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 = path4.join(ws.cwd, "graphify-out", "graph.json");
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 = path4.join(project.cwd, "graphify-out", "graph.json");
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(/##\s*Acceptance[\s\S]*?(?=\n##\s|\n$|$)/i);
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 conv = svc.getConversation(conversationId);
34128
- if (!conv) return textResponse(`Conversation ${conversationId} not found`);
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 path5 = __toESM(require("path"));
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 = path5.isAbsolute(sourcePath) ? sourcePath : path5.join(contentRoot, sourcePath);
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 path6 = __toESM(require("path"));
35523
+ var path9 = __toESM(require("path"));
34452
35524
  var isWindows = process.platform === "win32";
34453
- function normalize(p) {
34454
- const resolved = path6.resolve(p).replace(/[\\/]+$/, "");
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 = path6.relative(parent, child);
35531
+ const rel = path9.relative(parent, child);
34460
35532
  if (rel === "") return true;
34461
35533
  if (rel.startsWith("..")) return false;
34462
- return !path6.isAbsolute(rel);
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 = normalize(cwd);
34476
- const matches = workspaces.map((w) => ({ workspace: w, normalized: normalize(w.cwd) })).filter((m) => isDescendantOrEqual(m.normalized, normalizedCwd)).sort((a, b) => b.normalized.length - a.normalized.length);
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 = loadSessionRules();
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 = loadSessionRules();
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.split("\n").slice(0, 3).join(" ").slice(0, 200));
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, dataDir2, dbPath2) {
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(dataDir2))
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 like tasks_update_batch.",
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, dataDir2))
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(dataDir2), filename);
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, dbPath2);
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/adapters/mcp/server.ts
35244
- var { dbPath, dataDir } = resolveDataPaths();
35245
- async function main() {
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
- register(server, svc);
35253
- register2(server, svc);
35254
- register3(server, svc);
35255
- register4(server, svc);
35256
- register5(server, svc);
35257
- register6(server, svc);
35258
- register7(server, svc, dataDir, dbPath);
35259
- register8(server, svc);
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
- main().catch((err) => {
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
  });