choda-deck 0.2.3 → 0.3.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.
@@ -1869,8 +1869,8 @@ var require_keyword = __commonJS({
1869
1869
  var _a2;
1870
1870
  const { gen, keyword, schema, parentSchema, $data, it } = cxt;
1871
1871
  checkAsyncKeyword(it, def);
1872
- const validate3 = !$data && def.compile ? def.compile.call(it.self, schema, parentSchema, it) : def.validate;
1873
- const validateRef = useKeyword(gen, keyword, validate3);
1872
+ const validate2 = !$data && def.compile ? def.compile.call(it.self, schema, parentSchema, it) : def.validate;
1873
+ const validateRef = useKeyword(gen, keyword, validate2);
1874
1874
  const valid = gen.let("valid");
1875
1875
  cxt.block$data(valid, validateKeyword);
1876
1876
  cxt.ok((_a2 = def.valid) !== null && _a2 !== void 0 ? _a2 : valid);
@@ -2943,28 +2943,28 @@ var require_compile = __commonJS({
2943
2943
  if (this.opts.code.process)
2944
2944
  sourceCode = this.opts.code.process(sourceCode, sch);
2945
2945
  const makeValidate = new Function(`${names_1.default.self}`, `${names_1.default.scope}`, sourceCode);
2946
- const validate3 = makeValidate(this, this.scope.get());
2947
- this.scope.value(validateName, { ref: validate3 });
2948
- validate3.errors = null;
2949
- validate3.schema = sch.schema;
2950
- validate3.schemaEnv = sch;
2946
+ const validate2 = makeValidate(this, this.scope.get());
2947
+ this.scope.value(validateName, { ref: validate2 });
2948
+ validate2.errors = null;
2949
+ validate2.schema = sch.schema;
2950
+ validate2.schemaEnv = sch;
2951
2951
  if (sch.$async)
2952
- validate3.$async = true;
2952
+ validate2.$async = true;
2953
2953
  if (this.opts.code.source === true) {
2954
- validate3.source = { validateName, validateCode, scopeValues: gen._values };
2954
+ validate2.source = { validateName, validateCode, scopeValues: gen._values };
2955
2955
  }
2956
2956
  if (this.opts.unevaluated) {
2957
2957
  const { props, items } = schemaCxt;
2958
- validate3.evaluated = {
2958
+ validate2.evaluated = {
2959
2959
  props: props instanceof codegen_1.Name ? void 0 : props,
2960
2960
  items: items instanceof codegen_1.Name ? void 0 : items,
2961
2961
  dynamicProps: props instanceof codegen_1.Name,
2962
2962
  dynamicItems: items instanceof codegen_1.Name
2963
2963
  };
2964
- if (validate3.source)
2965
- validate3.source.evaluated = (0, codegen_1.stringify)(validate3.evaluated);
2964
+ if (validate2.source)
2965
+ validate2.source.evaluated = (0, codegen_1.stringify)(validate2.evaluated);
2966
2966
  }
2967
- sch.validate = validate3;
2967
+ sch.validate = validate2;
2968
2968
  return sch;
2969
2969
  } catch (e) {
2970
2970
  delete sch.validate;
@@ -3225,8 +3225,8 @@ var require_utils = __commonJS({
3225
3225
  }
3226
3226
  return ind;
3227
3227
  }
3228
- function removeDotSegments(path10) {
3229
- let input = path10;
3228
+ function removeDotSegments(path9) {
3229
+ let input = path9;
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 [path10, query] = wsComponent.resourceName.split("?");
3429
- wsComponent.path = path10 && path10 !== "/" ? path10 : void 0;
3428
+ const [path9, query] = wsComponent.resourceName.split("?");
3429
+ wsComponent.path = path9 && path9 !== "/" ? path9 : void 0;
3430
3430
  wsComponent.query = query;
3431
3431
  wsComponent.resourceName = void 0;
3432
3432
  }
@@ -6490,8 +6490,8 @@ var require_formats = __commonJS({
6490
6490
  "use strict";
6491
6491
  Object.defineProperty(exports2, "__esModule", { value: true });
6492
6492
  exports2.formatNames = exports2.fastFormats = exports2.fullFormats = void 0;
6493
- function fmtDef(validate3, compare) {
6494
- return { validate: validate3, compare };
6493
+ function fmtDef(validate2, compare) {
6494
+ return { validate: validate2, compare };
6495
6495
  }
6496
6496
  exports2.fullFormats = {
6497
6497
  // date: http://tools.ietf.org/html/rfc3339#section-5.6
@@ -7355,51 +7355,51 @@ var require_textParsers = __commonJS({
7355
7355
  result.radius = parseFloat(radius);
7356
7356
  return result;
7357
7357
  };
7358
- var init = function(register18) {
7359
- register18(20, parseBigInteger);
7360
- register18(21, parseInteger);
7361
- register18(23, parseInteger);
7362
- register18(26, parseInteger);
7363
- register18(700, parseFloat);
7364
- register18(701, parseFloat);
7365
- register18(16, parseBool);
7366
- register18(1082, parseDate);
7367
- register18(1114, parseDate);
7368
- register18(1184, parseDate);
7369
- register18(600, parsePoint);
7370
- register18(651, parseStringArray);
7371
- register18(718, parseCircle);
7372
- register18(1e3, parseBoolArray);
7373
- register18(1001, parseByteAArray);
7374
- register18(1005, parseIntegerArray);
7375
- register18(1007, parseIntegerArray);
7376
- register18(1028, parseIntegerArray);
7377
- register18(1016, parseBigIntegerArray);
7378
- register18(1017, parsePointArray);
7379
- register18(1021, parseFloatArray);
7380
- register18(1022, parseFloatArray);
7381
- register18(1231, parseFloatArray);
7382
- register18(1014, parseStringArray);
7383
- register18(1015, parseStringArray);
7384
- register18(1008, parseStringArray);
7385
- register18(1009, parseStringArray);
7386
- register18(1040, parseStringArray);
7387
- register18(1041, parseStringArray);
7388
- register18(1115, parseDateArray);
7389
- register18(1182, parseDateArray);
7390
- register18(1185, parseDateArray);
7391
- register18(1186, parseInterval);
7392
- register18(1187, parseIntervalArray);
7393
- register18(17, parseByteA);
7394
- register18(114, JSON.parse.bind(JSON));
7395
- register18(3802, JSON.parse.bind(JSON));
7396
- register18(199, parseJsonArray);
7397
- register18(3807, parseJsonArray);
7398
- register18(3907, parseStringArray);
7399
- register18(2951, parseStringArray);
7400
- register18(791, parseStringArray);
7401
- register18(1183, parseStringArray);
7402
- register18(1270, parseStringArray);
7358
+ var init = function(register22) {
7359
+ register22(20, parseBigInteger);
7360
+ register22(21, parseInteger);
7361
+ register22(23, parseInteger);
7362
+ register22(26, parseInteger);
7363
+ register22(700, parseFloat);
7364
+ register22(701, parseFloat);
7365
+ register22(16, parseBool);
7366
+ register22(1082, parseDate);
7367
+ register22(1114, parseDate);
7368
+ register22(1184, parseDate);
7369
+ register22(600, parsePoint);
7370
+ register22(651, parseStringArray);
7371
+ register22(718, parseCircle);
7372
+ register22(1e3, parseBoolArray);
7373
+ register22(1001, parseByteAArray);
7374
+ register22(1005, parseIntegerArray);
7375
+ register22(1007, parseIntegerArray);
7376
+ register22(1028, parseIntegerArray);
7377
+ register22(1016, parseBigIntegerArray);
7378
+ register22(1017, parsePointArray);
7379
+ register22(1021, parseFloatArray);
7380
+ register22(1022, parseFloatArray);
7381
+ register22(1231, parseFloatArray);
7382
+ register22(1014, parseStringArray);
7383
+ register22(1015, parseStringArray);
7384
+ register22(1008, parseStringArray);
7385
+ register22(1009, parseStringArray);
7386
+ register22(1040, parseStringArray);
7387
+ register22(1041, parseStringArray);
7388
+ register22(1115, parseDateArray);
7389
+ register22(1182, parseDateArray);
7390
+ register22(1185, parseDateArray);
7391
+ register22(1186, parseInterval);
7392
+ register22(1187, parseIntervalArray);
7393
+ register22(17, parseByteA);
7394
+ register22(114, JSON.parse.bind(JSON));
7395
+ register22(3802, JSON.parse.bind(JSON));
7396
+ register22(199, parseJsonArray);
7397
+ register22(3807, parseJsonArray);
7398
+ register22(3907, parseStringArray);
7399
+ register22(2951, parseStringArray);
7400
+ register22(791, parseStringArray);
7401
+ register22(1183, parseStringArray);
7402
+ register22(1270, parseStringArray);
7403
7403
  };
7404
7404
  module2.exports = {
7405
7405
  init
@@ -7663,23 +7663,23 @@ var require_binaryParsers = __commonJS({
7663
7663
  if (value === null) return null;
7664
7664
  return parseBits(value, 8) > 0;
7665
7665
  };
7666
- var init = function(register18) {
7667
- register18(20, parseInt64);
7668
- register18(21, parseInt16);
7669
- register18(23, parseInt32);
7670
- register18(26, parseInt32);
7671
- register18(1700, parseNumeric);
7672
- register18(700, parseFloat32);
7673
- register18(701, parseFloat64);
7674
- register18(16, parseBool);
7675
- register18(1114, parseDate.bind(null, false));
7676
- register18(1184, parseDate.bind(null, true));
7677
- register18(1e3, parseArray);
7678
- register18(1007, parseArray);
7679
- register18(1016, parseArray);
7680
- register18(1008, parseArray);
7681
- register18(1009, parseArray);
7682
- register18(25, parseText);
7666
+ var init = function(register22) {
7667
+ register22(20, parseInt64);
7668
+ register22(21, parseInt16);
7669
+ register22(23, parseInt32);
7670
+ register22(26, parseInt32);
7671
+ register22(1700, parseNumeric);
7672
+ register22(700, parseFloat32);
7673
+ register22(701, parseFloat64);
7674
+ register22(16, parseBool);
7675
+ register22(1114, parseDate.bind(null, false));
7676
+ register22(1184, parseDate.bind(null, true));
7677
+ register22(1e3, parseArray);
7678
+ register22(1007, parseArray);
7679
+ register22(1016, parseArray);
7680
+ register22(1008, parseArray);
7681
+ register22(1009, parseArray);
7682
+ register22(25, parseText);
7683
7683
  };
7684
7684
  module2.exports = {
7685
7685
  init
@@ -8017,7 +8017,7 @@ var require_utils3 = __commonJS({
8017
8017
  var nodeCrypto = require("crypto");
8018
8018
  module2.exports = {
8019
8019
  postgresMd5PasswordHash,
8020
- randomBytes: randomBytes3,
8020
+ randomBytes,
8021
8021
  deriveKey,
8022
8022
  sha256,
8023
8023
  hashByName,
@@ -8027,7 +8027,7 @@ var require_utils3 = __commonJS({
8027
8027
  var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
8028
8028
  var subtleCrypto = webCrypto.subtle;
8029
8029
  var textEncoder = new TextEncoder();
8030
- function randomBytes3(length) {
8030
+ function randomBytes(length) {
8031
8031
  return webCrypto.getRandomValues(Buffer.alloc(length));
8032
8032
  }
8033
8033
  async function md5(string4) {
@@ -10225,7 +10225,7 @@ var require_split2 = __commonJS({
10225
10225
  var require_helper = __commonJS({
10226
10226
  "node_modules/pgpass/lib/helper.js"(exports2, module2) {
10227
10227
  "use strict";
10228
- var path10 = require("path");
10228
+ var path9 = require("path");
10229
10229
  var Stream = require("stream").Stream;
10230
10230
  var split = require_split2();
10231
10231
  var util2 = require("util");
@@ -10264,7 +10264,7 @@ var require_helper = __commonJS({
10264
10264
  };
10265
10265
  module2.exports.getFileName = function(rawEnv) {
10266
10266
  var env = rawEnv || process.env;
10267
- var file2 = env.PGPASSFILE || (isWin ? path10.join(env.APPDATA || "./", "postgresql", "pgpass.conf") : path10.join(env.HOME || "./", ".pgpass"));
10267
+ var file2 = env.PGPASSFILE || (isWin ? path9.join(env.APPDATA || "./", "postgresql", "pgpass.conf") : path9.join(env.HOME || "./", ".pgpass"));
10268
10268
  return file2;
10269
10269
  };
10270
10270
  module2.exports.usePgPass = function(stats, fname) {
@@ -10396,7 +10396,7 @@ var require_helper = __commonJS({
10396
10396
  var require_lib = __commonJS({
10397
10397
  "node_modules/pgpass/lib/index.js"(exports2, module2) {
10398
10398
  "use strict";
10399
- var path10 = require("path");
10399
+ var path9 = require("path");
10400
10400
  var fs9 = require("fs");
10401
10401
  var helper = require_helper();
10402
10402
  module2.exports = function(connInfo, cb) {
@@ -11953,8 +11953,6 @@ var require_lib2 = __commonJS({
11953
11953
 
11954
11954
  // src/adapters/mcp/server-bootstrap.ts
11955
11955
  var fs8 = __toESM(require("fs"));
11956
- var path9 = __toESM(require("path"));
11957
- var import_better_sqlite32 = __toESM(require("better-sqlite3"));
11958
11956
 
11959
11957
  // node_modules/zod/v3/helpers/util.js
11960
11958
  var util;
@@ -12315,8 +12313,8 @@ function getErrorMap() {
12315
12313
 
12316
12314
  // node_modules/zod/v3/helpers/parseUtil.js
12317
12315
  var makeIssue = (params) => {
12318
- const { data, path: path10, errorMaps, issueData } = params;
12319
- const fullPath = [...path10, ...issueData.path || []];
12316
+ const { data, path: path9, errorMaps, issueData } = params;
12317
+ const fullPath = [...path9, ...issueData.path || []];
12320
12318
  const fullIssue = {
12321
12319
  ...issueData,
12322
12320
  path: fullPath
@@ -12431,11 +12429,11 @@ var errorUtil;
12431
12429
 
12432
12430
  // node_modules/zod/v3/types.js
12433
12431
  var ParseInputLazyPath = class {
12434
- constructor(parent, value, path10, key) {
12432
+ constructor(parent, value, path9, key) {
12435
12433
  this._cachedPath = [];
12436
12434
  this.parent = parent;
12437
12435
  this.data = value;
12438
- this._path = path10;
12436
+ this._path = path9;
12439
12437
  this._key = key;
12440
12438
  }
12441
12439
  get path() {
@@ -16358,10 +16356,10 @@ function mergeDefs(...defs) {
16358
16356
  function cloneDef(schema) {
16359
16357
  return mergeDefs(schema._zod.def);
16360
16358
  }
16361
- function getElementAtPath(obj, path10) {
16362
- if (!path10)
16359
+ function getElementAtPath(obj, path9) {
16360
+ if (!path9)
16363
16361
  return obj;
16364
- return path10.reduce((acc, key) => acc?.[key], obj);
16362
+ return path9.reduce((acc, key) => acc?.[key], obj);
16365
16363
  }
16366
16364
  function promiseAllObject(promisesObj) {
16367
16365
  const keys = Object.keys(promisesObj);
@@ -16744,11 +16742,11 @@ function aborted(x, startIndex = 0) {
16744
16742
  }
16745
16743
  return false;
16746
16744
  }
16747
- function prefixIssues(path10, issues) {
16745
+ function prefixIssues(path9, issues) {
16748
16746
  return issues.map((iss) => {
16749
16747
  var _a2;
16750
16748
  (_a2 = iss).path ?? (_a2.path = []);
16751
- iss.path.unshift(path10);
16749
+ iss.path.unshift(path9);
16752
16750
  return iss;
16753
16751
  });
16754
16752
  }
@@ -16931,7 +16929,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
16931
16929
  }
16932
16930
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
16933
16931
  const result = { errors: [] };
16934
- const processError = (error49, path10 = []) => {
16932
+ const processError = (error49, path9 = []) => {
16935
16933
  var _a2, _b;
16936
16934
  for (const issue2 of error49.issues) {
16937
16935
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -16941,7 +16939,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
16941
16939
  } else if (issue2.code === "invalid_element") {
16942
16940
  processError({ issues: issue2.issues }, issue2.path);
16943
16941
  } else {
16944
- const fullpath = [...path10, ...issue2.path];
16942
+ const fullpath = [...path9, ...issue2.path];
16945
16943
  if (fullpath.length === 0) {
16946
16944
  result.errors.push(mapper(issue2));
16947
16945
  continue;
@@ -16973,8 +16971,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
16973
16971
  }
16974
16972
  function toDotPath(_path) {
16975
16973
  const segs = [];
16976
- const path10 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16977
- for (const seg of path10) {
16974
+ const path9 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16975
+ for (const seg of path9) {
16978
16976
  if (typeof seg === "number")
16979
16977
  segs.push(`[${seg}]`);
16980
16978
  else if (typeof seg === "symbol")
@@ -18572,13 +18570,13 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
18572
18570
  }
18573
18571
  return propValues;
18574
18572
  });
18575
- const isObject2 = isObject;
18573
+ const isObject3 = isObject;
18576
18574
  const catchall = def.catchall;
18577
18575
  let value;
18578
18576
  inst._zod.parse = (payload, ctx) => {
18579
18577
  value ?? (value = _normalized.value);
18580
18578
  const input = payload.value;
18581
- if (!isObject2(input)) {
18579
+ if (!isObject3(input)) {
18582
18580
  payload.issues.push({
18583
18581
  expected: "object",
18584
18582
  code: "invalid_type",
@@ -18676,7 +18674,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
18676
18674
  return (payload, ctx) => fn(shape, payload, ctx);
18677
18675
  };
18678
18676
  let fastpass;
18679
- const isObject2 = isObject;
18677
+ const isObject3 = isObject;
18680
18678
  const jit = !globalConfig.jitless;
18681
18679
  const allowsEval2 = allowsEval;
18682
18680
  const fastEnabled = jit && allowsEval2.value;
@@ -18685,7 +18683,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
18685
18683
  inst._zod.parse = (payload, ctx) => {
18686
18684
  value ?? (value = _normalized.value);
18687
18685
  const input = payload.value;
18688
- if (!isObject2(input)) {
18686
+ if (!isObject3(input)) {
18689
18687
  payload.issues.push({
18690
18688
  expected: "object",
18691
18689
  code: "invalid_type",
@@ -29380,13 +29378,13 @@ function resolveRef(ref, ctx) {
29380
29378
  if (!ref.startsWith("#")) {
29381
29379
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
29382
29380
  }
29383
- const path10 = ref.slice(1).split("/").filter(Boolean);
29384
- if (path10.length === 0) {
29381
+ const path9 = ref.slice(1).split("/").filter(Boolean);
29382
+ if (path9.length === 0) {
29385
29383
  return ctx.rootSchema;
29386
29384
  }
29387
29385
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
29388
- if (path10[0] === defsKey) {
29389
- const key = path10[1];
29386
+ if (path9[0] === defsKey) {
29387
+ const key = path9[1];
29390
29388
  if (!key || !ctx.defs[key]) {
29391
29389
  throw new Error(`Reference not found: ${ref}`);
29392
29390
  }
@@ -32894,7 +32892,7 @@ var Protocol = class {
32894
32892
  const capturedTransport = this._transport;
32895
32893
  const relatedTaskId = request.params?._meta?.[RELATED_TASK_META_KEY]?.taskId;
32896
32894
  if (handler === void 0) {
32897
- const errorResponse3 = {
32895
+ const errorResponse = {
32898
32896
  jsonrpc: "2.0",
32899
32897
  id: request.id,
32900
32898
  error: {
@@ -32905,11 +32903,11 @@ var Protocol = class {
32905
32903
  if (relatedTaskId && this._taskMessageQueue) {
32906
32904
  this._enqueueTaskMessage(relatedTaskId, {
32907
32905
  type: "error",
32908
- message: errorResponse3,
32906
+ message: errorResponse,
32909
32907
  timestamp: Date.now()
32910
32908
  }, capturedTransport?.sessionId).catch((error48) => this._onerror(new Error(`Failed to enqueue error response: ${error48}`)));
32911
32909
  } else {
32912
- capturedTransport?.send(errorResponse3).catch((error48) => this._onerror(new Error(`Failed to send an error response: ${error48}`)));
32910
+ capturedTransport?.send(errorResponse).catch((error48) => this._onerror(new Error(`Failed to send an error response: ${error48}`)));
32913
32911
  }
32914
32912
  return;
32915
32913
  }
@@ -32979,7 +32977,7 @@ var Protocol = class {
32979
32977
  if (abortController.signal.aborted) {
32980
32978
  return;
32981
32979
  }
32982
- const errorResponse3 = {
32980
+ const errorResponse = {
32983
32981
  jsonrpc: "2.0",
32984
32982
  id: request.id,
32985
32983
  error: {
@@ -32991,11 +32989,11 @@ var Protocol = class {
32991
32989
  if (relatedTaskId && this._taskMessageQueue) {
32992
32990
  await this._enqueueTaskMessage(relatedTaskId, {
32993
32991
  type: "error",
32994
- message: errorResponse3,
32992
+ message: errorResponse,
32995
32993
  timestamp: Date.now()
32996
32994
  }, capturedTransport?.sessionId);
32997
32995
  } else {
32998
- await capturedTransport?.send(errorResponse3);
32996
+ await capturedTransport?.send(errorResponse);
32999
32997
  }
33000
32998
  }).catch((error48) => this._onerror(new Error(`Failed to send response: ${error48}`))).finally(() => {
33001
32999
  if (this._requestHandlerAbortControllers.get(request.id) === abortController) {
@@ -35281,6 +35279,60 @@ var path3 = __toESM(require("path"));
35281
35279
  var import_better_sqlite3 = __toESM(require("better-sqlite3"));
35282
35280
  var sqliteVec = __toESM(require("sqlite-vec"));
35283
35281
 
35282
+ // src/core/sync/syncable-tables.ts
35283
+ var SYNCABLE_TABLES = [
35284
+ "projects",
35285
+ "workspaces",
35286
+ "tasks",
35287
+ "inbox_items",
35288
+ "conversations",
35289
+ "conversation_messages",
35290
+ "conversation_actions"
35291
+ ];
35292
+ var SYNC_COLUMNS = [
35293
+ { name: "sync_updated_at", sqliteType: "INTEGER", pgType: "BIGINT" },
35294
+ { name: "sync_deleted_at", sqliteType: "INTEGER", pgType: "BIGINT" },
35295
+ { name: "sync_origin", sqliteType: "TEXT", pgType: "TEXT" }
35296
+ ];
35297
+
35298
+ // src/core/sync/sync-source.ts
35299
+ function fetchSinceFromSqlite(db, since) {
35300
+ const deltas = [];
35301
+ for (const table of SYNCABLE_TABLES) {
35302
+ const rows = db.prepare(
35303
+ `SELECT * FROM ${table} WHERE sync_updated_at > ? OR sync_deleted_at > ? ORDER BY sync_updated_at`
35304
+ ).all(since, since);
35305
+ if (rows.length > 0) deltas.push({ table, rows });
35306
+ }
35307
+ return deltas;
35308
+ }
35309
+ async function fetchSinceFromPg(conn, since) {
35310
+ const deltas = [];
35311
+ for (const table of SYNCABLE_TABLES) {
35312
+ const result = await conn.query(
35313
+ `SELECT * FROM ${table} WHERE sync_updated_at > $1 OR sync_deleted_at > $1 ORDER BY sync_updated_at`,
35314
+ [since]
35315
+ );
35316
+ if (result.rows.length > 0) {
35317
+ deltas.push({ table, rows: result.rows.map(normalizePgRow) });
35318
+ }
35319
+ }
35320
+ return deltas;
35321
+ }
35322
+ function normalizePgRow(row) {
35323
+ const out = {};
35324
+ for (const [k, v] of Object.entries(row)) {
35325
+ if (k === "sync_updated_at" || k === "sync_deleted_at") {
35326
+ out[k] = v === null || v === void 0 ? null : Number(v);
35327
+ } else if (v instanceof Date) {
35328
+ out[k] = v.toISOString();
35329
+ } else {
35330
+ out[k] = v;
35331
+ }
35332
+ }
35333
+ return out;
35334
+ }
35335
+
35284
35336
  // src/core/domain/embedding/embedding-store.ts
35285
35337
  var EmbeddingStore = class {
35286
35338
  db;
@@ -35549,6 +35601,33 @@ var NoActiveSessionError = class extends LifecycleError {
35549
35601
  this.name = "NoActiveSessionError";
35550
35602
  }
35551
35603
  };
35604
+ var InvestigationNotFoundError = class extends LifecycleError {
35605
+ constructor(id) {
35606
+ super("INVESTIGATION_NOT_FOUND", `Investigation ${id} not found`);
35607
+ this.name = "InvestigationNotFoundError";
35608
+ }
35609
+ };
35610
+ var InvestigationStatusError = class extends LifecycleError {
35611
+ constructor(id, current, message) {
35612
+ super("INVESTIGATION_INVALID_STATUS", `Investigation ${id} is ${current} \u2014 ${message}`);
35613
+ this.name = "InvestigationStatusError";
35614
+ }
35615
+ };
35616
+ var HypothesisNotFoundError = class extends LifecycleError {
35617
+ constructor(id) {
35618
+ super("HYPOTHESIS_NOT_FOUND", `Hypothesis ${id} not found`);
35619
+ this.name = "HypothesisNotFoundError";
35620
+ }
35621
+ };
35622
+ var HypothesisTransitionError = class extends LifecycleError {
35623
+ constructor(id, current, target) {
35624
+ super(
35625
+ "HYPOTHESIS_INVALID_TRANSITION",
35626
+ `Hypothesis ${id} is ${current} \u2014 cannot transition to ${target} (only testing \u2192 ruled_out | confirmed is allowed)`
35627
+ );
35628
+ this.name = "HypothesisTransitionError";
35629
+ }
35630
+ };
35552
35631
 
35553
35632
  // src/core/domain/lifecycle/inbox-lifecycle-service.ts
35554
35633
  var InboxLifecycleService = class {
@@ -35609,7 +35688,8 @@ var InboxLifecycleService = class {
35609
35688
  this.closeLinkedConversations(id, `Converted to ${task.id}: ${input.title}`);
35610
35689
  const final = this.tasks.get(task.id);
35611
35690
  if (!final) throw new Error(`Task ${task.id} disappeared mid-transaction`);
35612
- return { inboxId: id, taskId: task.id, task: final };
35691
+ const localizationWarning = item.workspaceId ? void 0 : `${id} had no workspaceId \u2014 converted ${task.id} is not localized to an app. Set it during research (inbox_ready) next time, or scope the task via its worker session.`;
35692
+ return { inboxId: id, taskId: task.id, task: final, localizationWarning };
35613
35693
  });
35614
35694
  return tx();
35615
35695
  }
@@ -35876,13 +35956,15 @@ function computeLineOffsets(body, lineCount) {
35876
35956
 
35877
35957
  // src/core/domain/lifecycle/session-lifecycle-service.ts
35878
35958
  var SessionLifecycleService = class {
35879
- constructor(db, sessions, contextSources, conversations, tasks, sessionEvents, recallMemoriesFn) {
35959
+ constructor(db, sessions, contextSources, conversations, tasks, sessionEvents, relationships, codeRefs, recallMemoriesFn) {
35880
35960
  this.db = db;
35881
35961
  this.sessions = sessions;
35882
35962
  this.contextSources = contextSources;
35883
35963
  this.conversations = conversations;
35884
35964
  this.tasks = tasks;
35885
35965
  this.sessionEvents = sessionEvents;
35966
+ this.relationships = relationships;
35967
+ this.codeRefs = codeRefs;
35886
35968
  this.recallMemoriesFn = recallMemoriesFn;
35887
35969
  }
35888
35970
  db;
@@ -35891,6 +35973,8 @@ var SessionLifecycleService = class {
35891
35973
  conversations;
35892
35974
  tasks;
35893
35975
  sessionEvents;
35976
+ relationships;
35977
+ codeRefs;
35894
35978
  recallMemoriesFn;
35895
35979
  async startSession(input) {
35896
35980
  const tx = this.db.transaction(() => {
@@ -35913,7 +35997,8 @@ var SessionLifecycleService = class {
35913
35997
  workspaceId: input.workspaceId,
35914
35998
  taskId: input.taskId,
35915
35999
  startedAt: now(),
35916
- status: "active"
36000
+ status: "active",
36001
+ ccSessionId: input.ccSessionId
35917
36002
  });
35918
36003
  if (input.taskId) {
35919
36004
  this.tasks.update(input.taskId, { status: "IN-PROGRESS" });
@@ -35979,6 +36064,19 @@ var SessionLifecycleService = class {
35979
36064
  memoryCandidate: false
35980
36065
  });
35981
36066
  }
36067
+ if (session.taskId) {
36068
+ this.deriveTouchesFromFileEdits(id, session.taskId, session.projectId, endedAt);
36069
+ }
36070
+ const featureId = session.taskId ? this.inferFeatureForTask(session.taskId) : null;
36071
+ for (const decision of input.handoff.decisions ?? []) {
36072
+ const draft = draftGotchaFromDecision(decision, featureId);
36073
+ this.sessionEvents.create({
36074
+ sessionId: id,
36075
+ eventType: "observation",
36076
+ payloadJson: JSON.stringify({ kind: "gotcha_draft", ...draft }),
36077
+ memoryCandidate: true
36078
+ });
36079
+ }
35982
36080
  const memoryCandidates = this.sessionEvents.listMemoryCandidates(id);
35983
36081
  const selfEditPrompt = buildSelfEditPrompt(memoryCandidates);
35984
36082
  return { session: updated, closedConversationIds, taskUpdated, memoryCandidates, selfEditPrompt };
@@ -36027,6 +36125,30 @@ var SessionLifecycleService = class {
36027
36125
  });
36028
36126
  return { session: updated };
36029
36127
  }
36128
+ // TASK-998: the feature a task REALIZES (task → feature edge, TASK-992). A task
36129
+ // normally realizes one feature; if several, the first by edge order is used.
36130
+ // Returns null when the task has no REALIZES edge.
36131
+ inferFeatureForTask(taskId) {
36132
+ const edges = this.relationships.getFrom(taskId, "REALIZES");
36133
+ return edges.length > 0 ? edges[0].toId : null;
36134
+ }
36135
+ // INBOX-424: upsert a file-level code_ref (symbol=null — the sanctioned
36136
+ // convention for non-symbol refs) + a `modifies` TOUCHES edge for each DISTINCT
36137
+ // path the session edited. Pure-derivation: reads channel-1 events, writes the
36138
+ // graph projection; no AC / summary side effects.
36139
+ deriveTouchesFromFileEdits(sessionId, taskId, projectId, nowIso) {
36140
+ const paths = /* @__PURE__ */ new Set();
36141
+ for (const evt of this.sessionEvents.listBySession(sessionId, "observation")) {
36142
+ const payload = parseObservationPayload(evt.payloadJson);
36143
+ if (payload?.kind === "file_modified" && typeof payload.path === "string") {
36144
+ paths.add(payload.path);
36145
+ }
36146
+ }
36147
+ for (const path9 of paths) {
36148
+ const ref = this.codeRefs.upsert({ slug: fileRefSlug(path9), projectId, path: path9, symbol: null }, nowIso);
36149
+ this.codeRefs.addTouches(taskId, ref.slug, "modifies");
36150
+ }
36151
+ }
36030
36152
  async resumeSession(id) {
36031
36153
  const session = this.sessions.get(id);
36032
36154
  if (!session) throw new SessionNotFoundError(id);
@@ -36089,6 +36211,9 @@ function aggregateSessionSummary(sessionEvents, tasks, sessionId, summary) {
36089
36211
  acCoverage: mergedAcCoverage
36090
36212
  };
36091
36213
  }
36214
+ function fileRefSlug(path9) {
36215
+ return `code-ref-${path9.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase()}`;
36216
+ }
36092
36217
  function parseObservationPayload(json2) {
36093
36218
  if (!json2) return null;
36094
36219
  try {
@@ -36100,11 +36225,245 @@ function parseObservationPayload(json2) {
36100
36225
  }
36101
36226
  function buildSelfEditPrompt(candidates) {
36102
36227
  if (candidates.length === 0) return "";
36103
- const n = candidates.length;
36104
- const word = n === 1 ? "event" : "events";
36105
- return `Review these ${n} candidate ${word} from the session. Call memory_write for 1-3 entries worth remembering across sessions \u2014 use type='episodic' with scope='task' for task-specific learnings, or type='procedural' with scope='project' or 'workspace' for reusable patterns. Skip entirely if nothing here is worth keeping.`;
36228
+ const gotchaDrafts = candidates.filter((c) => {
36229
+ const p = parseObservationPayload(c.payloadJson);
36230
+ return p?.kind === "gotcha_draft";
36231
+ });
36232
+ const memN = candidates.length - gotchaDrafts.length;
36233
+ const parts = [];
36234
+ if (memN > 0) {
36235
+ const word = memN === 1 ? "event" : "events";
36236
+ parts.push(
36237
+ `Review ${memN} candidate ${word} from the session. Call memory_write for 1-3 entries worth remembering across sessions \u2014 use type='episodic' with scope='task' for task-specific learnings, or type='procedural' with scope='project' or 'workspace' for reusable patterns.`
36238
+ );
36239
+ }
36240
+ if (gotchaDrafts.length > 0) {
36241
+ const word = gotchaDrafts.length === 1 ? "gotcha draft" : "gotcha drafts";
36242
+ const needFeature = gotchaDrafts.some((c) => {
36243
+ const p = parseObservationPayload(c.payloadJson);
36244
+ return p?.needsFeature === true;
36245
+ });
36246
+ parts.push(
36247
+ `${gotchaDrafts.length} ${word} (kind='gotcha_draft') were proposed from the session's decisions. Review each, refine the structured fields (trigger / context / business_rule / resolution), and call knowledge_create(type='gotcha') for ones worth keeping.` + (needFeature ? ` Some have no affectedFeatureId (the task has no REALIZES edge) \u2014 ask the human which feature before creating those.` : "")
36248
+ );
36249
+ }
36250
+ parts.push("Skip entirely if nothing here is worth keeping.");
36251
+ return parts.join(" ");
36252
+ }
36253
+ function draftGotchaFromDecision(decision, featureId) {
36254
+ const text = decision.trim();
36255
+ const marker = text.match(/\bbecause\b|\bso that\b|[—–]| - |:/i);
36256
+ let businessRule = text;
36257
+ let resolution = "";
36258
+ if (marker && marker.index !== void 0 && marker.index > 0) {
36259
+ businessRule = text.slice(0, marker.index).replace(/[\s,;:—–-]+$/, "").trim();
36260
+ resolution = text.slice(marker.index + marker[0].length).trim();
36261
+ }
36262
+ return {
36263
+ trigger: "",
36264
+ context: text,
36265
+ businessRule,
36266
+ resolution,
36267
+ affectedFeatureId: featureId,
36268
+ needsFeature: featureId === null,
36269
+ sourceDecision: text
36270
+ };
36106
36271
  }
36107
36272
 
36273
+ // src/core/domain/lifecycle/investigation-lifecycle-service.ts
36274
+ var InvestigationLifecycleService = class {
36275
+ constructor(db, investigations) {
36276
+ this.db = db;
36277
+ this.investigations = investigations;
36278
+ }
36279
+ db;
36280
+ investigations;
36281
+ async startInvestigation(input) {
36282
+ return this.investigations.insertInvestigation(input);
36283
+ }
36284
+ async getInvestigation(id) {
36285
+ return this.investigations.getInvestigation(id);
36286
+ }
36287
+ async addHypothesis(investigationId, description) {
36288
+ const tx = this.db.transaction(() => {
36289
+ const inv = this.investigations.getInvestigation(investigationId);
36290
+ if (!inv) throw new InvestigationNotFoundError(investigationId);
36291
+ if (inv.status === "resolved") {
36292
+ throw new InvestigationStatusError(investigationId, inv.status, "cannot add a hypothesis");
36293
+ }
36294
+ return this.investigations.insertHypothesis(investigationId, description);
36295
+ });
36296
+ return tx();
36297
+ }
36298
+ // Only testing → ruled_out | confirmed is legal. A terminal hypothesis (already
36299
+ // ruled_out/confirmed) cannot transition again, and 'testing' is never a target.
36300
+ async setHypothesisStatus(hypothesisId, status) {
36301
+ const tx = this.db.transaction(() => {
36302
+ const hyp = this.investigations.getHypothesis(hypothesisId);
36303
+ if (!hyp) throw new HypothesisNotFoundError(hypothesisId);
36304
+ const legal = hyp.status === "testing" && (status === "ruled_out" || status === "confirmed");
36305
+ if (!legal) throw new HypothesisTransitionError(hypothesisId, hyp.status, status);
36306
+ this.investigations.setHypothesisStatus(hypothesisId, status);
36307
+ return this.investigations.getHypothesis(hypothesisId);
36308
+ });
36309
+ return tx();
36310
+ }
36311
+ async addEvidence(input) {
36312
+ const tx = this.db.transaction(() => {
36313
+ const inv = this.investigations.getInvestigation(input.investigationId);
36314
+ if (!inv) throw new InvestigationNotFoundError(input.investigationId);
36315
+ if (input.hypothesisId) {
36316
+ const hyp = this.investigations.getHypothesis(input.hypothesisId);
36317
+ if (!hyp || hyp.investigationId !== input.investigationId) {
36318
+ throw new HypothesisNotFoundError(input.hypothesisId);
36319
+ }
36320
+ }
36321
+ return this.investigations.insertEvidence(input);
36322
+ });
36323
+ return tx();
36324
+ }
36325
+ async resolveInvestigation(id, input) {
36326
+ const tx = this.db.transaction(() => {
36327
+ const inv = this.investigations.getInvestigation(id);
36328
+ if (!inv) throw new InvestigationNotFoundError(id);
36329
+ if (inv.status === "resolved") {
36330
+ throw new InvestigationStatusError(id, inv.status, "already resolved");
36331
+ }
36332
+ const patternTag = input.patternTag ?? null;
36333
+ this.investigations.setInvestigationResolved(id, {
36334
+ rootCause: input.rootCause,
36335
+ fixSummary: input.fixSummary,
36336
+ patternTag
36337
+ });
36338
+ const investigation = this.investigations.getInvestigation(id);
36339
+ const knowledgeDraft = buildKnowledgeDraft(investigation);
36340
+ return { investigation, knowledgeDraft };
36341
+ });
36342
+ return tx();
36343
+ }
36344
+ };
36345
+ function buildKnowledgeDraft(inv) {
36346
+ const title = inv.patternTag ? `Gotcha: ${inv.patternTag}` : `Gotcha: ${inv.symptom.slice(0, 80)}`;
36347
+ const body = [
36348
+ `**Symptom:** ${inv.symptom}`,
36349
+ `**Root cause:** ${inv.rootCause ?? ""}`,
36350
+ `**Fix:** ${inv.fixSummary ?? ""}`,
36351
+ inv.patternTag ? `**Pattern tag:** ${inv.patternTag}` : null,
36352
+ ``,
36353
+ `_Drafted from ${inv.id} (ADR-035). Review before committing via knowledge_create._`
36354
+ ].filter((line) => line !== null).join("\n");
36355
+ return { type: "gotcha", title, body, patternTag: inv.patternTag };
36356
+ }
36357
+
36358
+ // src/core/domain/repositories/investigation-repository.ts
36359
+ function rowToInvestigation(row) {
36360
+ return {
36361
+ id: row.id,
36362
+ symptom: row.symptom,
36363
+ status: row.status,
36364
+ taskId: row.task_id || null,
36365
+ sessionId: row.session_id || null,
36366
+ rootCause: row.root_cause || null,
36367
+ fixSummary: row.fix_summary || null,
36368
+ patternTag: row.pattern_tag || null,
36369
+ createdAt: row.created_at,
36370
+ resolvedAt: row.resolved_at || null,
36371
+ hypotheses: [],
36372
+ evidence: []
36373
+ };
36374
+ }
36375
+ function rowToHypothesis(row) {
36376
+ return {
36377
+ id: row.id,
36378
+ investigationId: row.investigation_id,
36379
+ description: row.description,
36380
+ status: row.status,
36381
+ createdAt: row.created_at
36382
+ };
36383
+ }
36384
+ function rowToEvidence(row) {
36385
+ return {
36386
+ id: row.id,
36387
+ investigationId: row.investigation_id,
36388
+ hypothesisId: row.hypothesis_id || null,
36389
+ type: row.type,
36390
+ ref: row.ref,
36391
+ note: row.note || null,
36392
+ createdAt: row.created_at
36393
+ };
36394
+ }
36395
+ var InvestigationRepository = class {
36396
+ constructor(db, counters) {
36397
+ this.db = db;
36398
+ this.counters = counters;
36399
+ }
36400
+ db;
36401
+ counters;
36402
+ nextId(prefix, entity) {
36403
+ return `${prefix}-${String(this.counters.nextNumber(entity)).padStart(3, "0")}`;
36404
+ }
36405
+ insertInvestigation(input) {
36406
+ const ts = now();
36407
+ const id = this.nextId("INV", "investigation");
36408
+ this.db.prepare(
36409
+ `INSERT INTO investigations (id, symptom, status, task_id, session_id, created_at)
36410
+ VALUES (?, ?, 'exploring', ?, ?, ?)`
36411
+ ).run(id, input.symptom, input.taskId ?? null, input.sessionId ?? null, ts);
36412
+ return this.getInvestigation(id);
36413
+ }
36414
+ // Nested read: investigation row + all hypotheses + all evidence (AC-5).
36415
+ getInvestigation(id) {
36416
+ const row = this.db.prepare("SELECT * FROM investigations WHERE id = ?").get(id);
36417
+ if (!row) return null;
36418
+ const investigation = rowToInvestigation(row);
36419
+ investigation.hypotheses = this.db.prepare("SELECT * FROM hypotheses WHERE investigation_id = ? ORDER BY created_at, rowid").all(id).map(rowToHypothesis);
36420
+ investigation.evidence = this.db.prepare("SELECT * FROM evidence WHERE investigation_id = ? ORDER BY created_at, rowid").all(id).map(rowToEvidence);
36421
+ return investigation;
36422
+ }
36423
+ setInvestigationResolved(id, fields) {
36424
+ this.db.prepare(
36425
+ `UPDATE investigations
36426
+ SET status = 'resolved', root_cause = ?, fix_summary = ?, pattern_tag = ?, resolved_at = ?
36427
+ WHERE id = ?`
36428
+ ).run(fields.rootCause, fields.fixSummary, fields.patternTag, now(), id);
36429
+ }
36430
+ insertHypothesis(investigationId, description) {
36431
+ const id = this.nextId("HYP", "hypothesis");
36432
+ this.db.prepare(
36433
+ `INSERT INTO hypotheses (id, investigation_id, description, status, created_at)
36434
+ VALUES (?, ?, ?, 'testing', ?)`
36435
+ ).run(id, investigationId, description, now());
36436
+ return this.getHypothesis(id);
36437
+ }
36438
+ getHypothesis(id) {
36439
+ const row = this.db.prepare("SELECT * FROM hypotheses WHERE id = ?").get(id);
36440
+ return row ? rowToHypothesis(row) : null;
36441
+ }
36442
+ setHypothesisStatus(id, status) {
36443
+ this.db.prepare("UPDATE hypotheses SET status = ? WHERE id = ?").run(status, id);
36444
+ }
36445
+ insertEvidence(input) {
36446
+ const id = this.nextId("EVID", "evidence");
36447
+ this.db.prepare(
36448
+ `INSERT INTO evidence (id, investigation_id, hypothesis_id, type, ref, note, created_at)
36449
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
36450
+ ).run(
36451
+ id,
36452
+ input.investigationId,
36453
+ input.hypothesisId ?? null,
36454
+ input.type,
36455
+ input.ref,
36456
+ input.note ?? null,
36457
+ now()
36458
+ );
36459
+ return this.getEvidence(id);
36460
+ }
36461
+ getEvidence(id) {
36462
+ const row = this.db.prepare("SELECT * FROM evidence WHERE id = ?").get(id);
36463
+ return row ? rowToEvidence(row) : null;
36464
+ }
36465
+ };
36466
+
36108
36467
  // src/core/domain/knowledge-service.ts
36109
36468
  var fs = __toESM(require("fs"));
36110
36469
  var path2 = __toESM(require("path"));
@@ -36142,6 +36501,17 @@ var GitOpsImpl = class {
36142
36501
  if (out === "") return [];
36143
36502
  return out.split(/\r?\n/).map((s) => s.trim()).filter((s) => s.length > 0);
36144
36503
  }
36504
+ commitsInWindow(cwd, sinceIso, grepTaskId) {
36505
+ const args = ["log", `--since=${sinceIso}`, "--format=%h %s"];
36506
+ if (grepTaskId) args.push(`--grep=${grepTaskId}`);
36507
+ try {
36508
+ const out = runGit(cwd, args).trim();
36509
+ if (out === "") return [];
36510
+ return out.split(/\r?\n/).map((s) => s.trim()).filter((s) => s.length > 0);
36511
+ } catch {
36512
+ return [];
36513
+ }
36514
+ }
36145
36515
  };
36146
36516
  function runGit(cwd, args) {
36147
36517
  try {
@@ -36158,11 +36528,23 @@ var KNOWLEDGE_TYPES = [
36158
36528
  "decision",
36159
36529
  "postmortem",
36160
36530
  "learning",
36161
- "evaluation"
36531
+ "evaluation",
36532
+ "feature",
36533
+ "code_ref",
36534
+ "gotcha"
36162
36535
  ];
36163
36536
  var KNOWLEDGE_SCOPES = ["project", "cross"];
36537
+ var EFFORT_BANDS = ["S", "M", "L", "XL"];
36538
+ var FEATURE_STATUSES = [
36539
+ "planned",
36540
+ "in-progress",
36541
+ "shipped",
36542
+ "blocked"
36543
+ ];
36164
36544
 
36165
36545
  // src/core/domain/knowledge-frontmatter.ts
36546
+ var STRUCTURED_SCALAR_KEYS = ["anchorTaskId", "effortBand", "status", "affectedFeatureId"];
36547
+ var STRUCTURED_LIST_KEYS = ["realizesTasks", "inWorkspaces"];
36166
36548
  var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
36167
36549
  var FrontmatterParseError = class extends Error {
36168
36550
  constructor(message) {
@@ -36176,6 +36558,7 @@ function parseFrontmatter(raw) {
36176
36558
  const fmText = m[1];
36177
36559
  const body = m[2] ?? "";
36178
36560
  const fm = { refs: [] };
36561
+ const structured = {};
36179
36562
  const lines = fmText.split(/\r?\n/);
36180
36563
  let i = 0;
36181
36564
  while (i < lines.length) {
@@ -36199,11 +36582,42 @@ function parseFrontmatter(raw) {
36199
36582
  const kv = line.match(/^([a-zA-Z]+):\s*(.*)$/);
36200
36583
  if (!kv) throw new FrontmatterParseError(`unrecognized line: ${line}`);
36201
36584
  const key = kv[1];
36202
- const value = unquote(kv[2].trim());
36203
- assignScalar(fm, key, value);
36585
+ const rawValue = kv[2].trim();
36586
+ if (assignStructured(structured, key, rawValue)) {
36587
+ i++;
36588
+ continue;
36589
+ }
36590
+ assignScalar(fm, key, unquote(rawValue));
36204
36591
  i++;
36205
36592
  }
36206
- return { frontmatter: validate(fm), body };
36593
+ return { frontmatter: validate(fm, structured), body };
36594
+ }
36595
+ function assignStructured(s, key, rawValue) {
36596
+ if (STRUCTURED_SCALAR_KEYS.includes(key)) {
36597
+ const value = unquote(rawValue);
36598
+ if (key === "effortBand") s.effortBand = value;
36599
+ else if (key === "status") s.status = value;
36600
+ else if (key === "anchorTaskId") s.anchorTaskId = value;
36601
+ else if (key === "affectedFeatureId") s.affectedFeatureId = value;
36602
+ return true;
36603
+ }
36604
+ if (STRUCTURED_LIST_KEYS.includes(key)) {
36605
+ const list = parseInlineList(rawValue);
36606
+ if (key === "realizesTasks") s.realizesTasks = list;
36607
+ else if (key === "inWorkspaces") s.inWorkspaces = list;
36608
+ return true;
36609
+ }
36610
+ return false;
36611
+ }
36612
+ function parseInlineList(raw) {
36613
+ const trimmed = raw.trim();
36614
+ if (trimmed === "[]" || trimmed === "") return [];
36615
+ try {
36616
+ const parsed = JSON.parse(trimmed);
36617
+ if (Array.isArray(parsed)) return parsed.map((v) => String(v));
36618
+ } catch {
36619
+ }
36620
+ return trimmed.replace(/^\[|\]$/g, "").split(",").map((v) => unquote(v.trim())).filter((v) => v.length > 0);
36207
36621
  }
36208
36622
  function parseRefsBlock(lines, startIdx) {
36209
36623
  const out = [];
@@ -36266,7 +36680,7 @@ function assignScalar(fm, key, value) {
36266
36680
  throw new FrontmatterParseError(`unknown key: ${key}`);
36267
36681
  }
36268
36682
  }
36269
- function validate(fm) {
36683
+ function validate(fm, structured) {
36270
36684
  if (!fm.type || !KNOWLEDGE_TYPES.includes(fm.type)) {
36271
36685
  throw new FrontmatterParseError(`invalid type: ${fm.type}`);
36272
36686
  }
@@ -36277,6 +36691,13 @@ function validate(fm) {
36277
36691
  if (!fm.projectId) throw new FrontmatterParseError("missing projectId");
36278
36692
  if (!fm.createdAt) throw new FrontmatterParseError("missing createdAt");
36279
36693
  if (!fm.lastVerifiedAt) throw new FrontmatterParseError("missing lastVerifiedAt");
36694
+ if (structured.effortBand && !EFFORT_BANDS.includes(structured.effortBand)) {
36695
+ throw new FrontmatterParseError(`invalid effortBand: ${structured.effortBand}`);
36696
+ }
36697
+ if (structured.status && !FEATURE_STATUSES.includes(structured.status)) {
36698
+ throw new FrontmatterParseError(`invalid status: ${structured.status}`);
36699
+ }
36700
+ const hasStructured = Object.values(structured).some((v) => v !== void 0);
36280
36701
  return {
36281
36702
  type: fm.type,
36282
36703
  title: fm.title,
@@ -36285,7 +36706,8 @@ function validate(fm) {
36285
36706
  scope: fm.scope,
36286
36707
  refs: fm.refs ?? [],
36287
36708
  createdAt: fm.createdAt,
36288
- lastVerifiedAt: fm.lastVerifiedAt
36709
+ lastVerifiedAt: fm.lastVerifiedAt,
36710
+ structured: hasStructured ? structured : void 0
36289
36711
  };
36290
36712
  }
36291
36713
  function serializeFrontmatter(fm, body) {
@@ -36306,6 +36728,19 @@ function serializeFrontmatter(fm, body) {
36306
36728
  }
36307
36729
  lines.push(`createdAt: ${fm.createdAt}`);
36308
36730
  lines.push(`lastVerifiedAt: ${fm.lastVerifiedAt}`);
36731
+ const s = fm.structured;
36732
+ if (s) {
36733
+ if (s.anchorTaskId) lines.push(`anchorTaskId: ${s.anchorTaskId}`);
36734
+ if (s.realizesTasks && s.realizesTasks.length > 0) {
36735
+ lines.push(`realizesTasks: ${JSON.stringify(s.realizesTasks)}`);
36736
+ }
36737
+ if (s.inWorkspaces && s.inWorkspaces.length > 0) {
36738
+ lines.push(`inWorkspaces: ${JSON.stringify(s.inWorkspaces)}`);
36739
+ }
36740
+ if (s.effortBand) lines.push(`effortBand: ${s.effortBand}`);
36741
+ if (s.status) lines.push(`status: ${s.status}`);
36742
+ if (s.affectedFeatureId) lines.push(`affectedFeatureId: ${s.affectedFeatureId}`);
36743
+ }
36309
36744
  lines.push("---");
36310
36745
  const trimmedBody = body.replace(/^\r?\n+/, "");
36311
36746
  return lines.join("\n") + "\n\n" + trimmedBody + (trimmedBody.endsWith("\n") ? "" : "\n");
@@ -36358,6 +36793,7 @@ var KnowledgeService = class {
36358
36793
  now;
36359
36794
  embeddingStore;
36360
36795
  embeddingProvider;
36796
+ edges;
36361
36797
  constructor(deps) {
36362
36798
  this.knowledge = deps.knowledge;
36363
36799
  this.projects = deps.projects;
@@ -36367,11 +36803,13 @@ var KnowledgeService = class {
36367
36803
  this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
36368
36804
  this.embeddingStore = deps.embeddingStore ?? null;
36369
36805
  this.embeddingProvider = deps.embeddingProvider ?? null;
36806
+ this.edges = deps.edges ?? null;
36370
36807
  }
36371
36808
  async createKnowledge(input) {
36372
36809
  this.validateInput(input);
36373
36810
  const project = await this.projects.get(input.projectId);
36374
36811
  if (!project) throw new KnowledgeValidationError(`unknown projectId: ${input.projectId}`);
36812
+ await this.validateStructured(input);
36375
36813
  const workspaceCwd = await this.resolveWorkspaceCwd(input.projectId, input.workspaceId);
36376
36814
  const slug = input.slug ?? slugify2(input.title);
36377
36815
  if (!slug) throw new KnowledgeValidationError("cannot derive slug from title");
@@ -36398,7 +36836,8 @@ var KnowledgeService = class {
36398
36836
  scope: input.scope,
36399
36837
  refs,
36400
36838
  createdAt: isoDate,
36401
- lastVerifiedAt: isoDate
36839
+ lastVerifiedAt: isoDate,
36840
+ structured: input.structured
36402
36841
  };
36403
36842
  const content = serializeFrontmatter(frontmatter, input.body);
36404
36843
  fs.mkdirSync(path2.dirname(filePath), { recursive: true });
@@ -36422,6 +36861,7 @@ var KnowledgeService = class {
36422
36861
  await this.regenerateIndexMd(input.projectId, project.cwd);
36423
36862
  }
36424
36863
  }
36864
+ await this.syncEdgesOnCreate(slug, input.type, input.structured);
36425
36865
  this.scheduleEmbed(slug, input.body);
36426
36866
  const staleness = this.computeStaleness(refs, stalenessCwd, input.scope);
36427
36867
  return {
@@ -36433,6 +36873,25 @@ var KnowledgeService = class {
36433
36873
  isStale: staleness.some((s) => s.commitsSince > 0)
36434
36874
  };
36435
36875
  }
36876
+ // TASK-992: derive ADR-NNN Pillar 3 edges from a new entry's structured
36877
+ // frontmatter so the relationships table stays current without re-running the
36878
+ // migration. feature → REALIZES (task→feature) + IN (feature→workspace);
36879
+ // gotcha → ABOUT (gotcha→feature). Edges are idempotent (INSERT OR IGNORE).
36880
+ // Only createKnowledge calls this — updateKnowledge cannot change structured
36881
+ // fields (it spreads the existing frontmatter), so there is nothing to re-sync.
36882
+ async syncEdgesOnCreate(slug, type, structured) {
36883
+ if (!this.edges || !structured) return;
36884
+ if (type === "feature") {
36885
+ for (const taskId of structured.realizesTasks ?? []) {
36886
+ await this.edges.add(taskId, slug, "REALIZES");
36887
+ }
36888
+ for (const ws of structured.inWorkspaces ?? []) {
36889
+ await this.edges.add(slug, ws, "IN");
36890
+ }
36891
+ } else if (type === "gotcha" && structured.affectedFeatureId) {
36892
+ await this.edges.add(slug, structured.affectedFeatureId, "ABOUT");
36893
+ }
36894
+ }
36436
36895
  async registerExistingKnowledge(input) {
36437
36896
  if (!fs.existsSync(input.filePath)) {
36438
36897
  throw new KnowledgeValidationError(`file not found: ${input.filePath}`);
@@ -36634,6 +37093,25 @@ var KnowledgeService = class {
36634
37093
  }));
36635
37094
  return { slug, refs: staleness, isStale: false, lastVerifiedAt: isoDate };
36636
37095
  }
37096
+ // TASK-988: structured-field validation for the first-class types. A gotcha's
37097
+ // affectedFeatureId must resolve to an existing `feature` entry — otherwise the
37098
+ // ABOUT edge dangles. Other structured fields (effortBand/status enums) are
37099
+ // validated in the frontmatter layer.
37100
+ async validateStructured(input) {
37101
+ const featureId = input.structured?.affectedFeatureId;
37102
+ if (!featureId) return;
37103
+ const target = await this.knowledge.get(featureId);
37104
+ if (!target) {
37105
+ throw new KnowledgeValidationError(
37106
+ `affectedFeatureId "${featureId}" does not resolve to a known knowledge entry`
37107
+ );
37108
+ }
37109
+ if (target.type !== "feature") {
37110
+ throw new KnowledgeValidationError(
37111
+ `affectedFeatureId "${featureId}" is a ${target.type}, expected a feature`
37112
+ );
37113
+ }
37114
+ }
36637
37115
  validateInput(input) {
36638
37116
  if (!KNOWLEDGE_TYPES.includes(input.type)) {
36639
37117
  throw new KnowledgeValidationError(`invalid type: ${input.type}`);
@@ -36887,18 +37365,168 @@ var KnowledgeRepository = class {
36887
37365
  }
36888
37366
  };
36889
37367
 
37368
+ // src/core/domain/repositories/code-ref-repository.ts
37369
+ function rowToCodeRef(row) {
37370
+ return {
37371
+ slug: row.slug,
37372
+ projectId: row.project_id,
37373
+ workspaceId: row.workspace_id ?? null,
37374
+ path: row.path,
37375
+ symbol: row.symbol ?? null,
37376
+ lineHint: row.line_hint ?? null,
37377
+ commitSha: row.commit_sha ?? null,
37378
+ createdAt: row.created_at,
37379
+ lastVerifiedAt: row.last_verified_at
37380
+ };
37381
+ }
37382
+ var CodeRefRepository = class {
37383
+ constructor(db) {
37384
+ this.db = db;
37385
+ }
37386
+ db;
37387
+ // Identity = (project_id, path, COALESCE(symbol,'')). A write matching an
37388
+ // existing identity re-pins commit_sha / line_hint / last_verified_at on the
37389
+ // ORIGINAL slug instead of inserting a second row (ADR Pillar 2c). The slug
37390
+ // supplied on such a write is ignored in favour of the existing one.
37391
+ upsert(input, nowIso) {
37392
+ const existing = this.getByIdentity(input.projectId, input.path, input.symbol ?? null);
37393
+ if (existing) {
37394
+ this.db.prepare(
37395
+ `UPDATE code_refs
37396
+ SET commit_sha = ?, line_hint = ?, workspace_id = ?, last_verified_at = ?
37397
+ WHERE slug = ?`
37398
+ ).run(
37399
+ input.commitSha ?? existing.commitSha,
37400
+ input.lineHint ?? existing.lineHint,
37401
+ input.workspaceId ?? existing.workspaceId,
37402
+ nowIso,
37403
+ existing.slug
37404
+ );
37405
+ return this.get(existing.slug);
37406
+ }
37407
+ this.db.prepare(
37408
+ `INSERT INTO code_refs
37409
+ (slug, project_id, workspace_id, path, symbol, line_hint, commit_sha, created_at, last_verified_at)
37410
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
37411
+ ).run(
37412
+ input.slug,
37413
+ input.projectId,
37414
+ input.workspaceId ?? null,
37415
+ input.path,
37416
+ input.symbol ?? null,
37417
+ input.lineHint ?? null,
37418
+ input.commitSha ?? null,
37419
+ nowIso,
37420
+ nowIso
37421
+ );
37422
+ return this.get(input.slug);
37423
+ }
37424
+ get(slug) {
37425
+ const row = this.db.prepare("SELECT * FROM code_refs WHERE slug = ?").get(slug);
37426
+ return row ? rowToCodeRef(row) : null;
37427
+ }
37428
+ getByIdentity(projectId, path9, symbol2) {
37429
+ const row = this.db.prepare(
37430
+ "SELECT * FROM code_refs WHERE project_id = ? AND path = ? AND COALESCE(symbol, '') = COALESCE(?, '')"
37431
+ ).get(projectId, path9, symbol2);
37432
+ return row ? rowToCodeRef(row) : null;
37433
+ }
37434
+ // Prefix query over the dotted symbol (ADR Pillar 2c): e.g. all Domain-layer
37435
+ // refs via symbolPrefix = 'Ichiba.Pim.TradingCatalog.Domain.'. Falls back to
37436
+ // a path filter, or lists the whole project when neither is given.
37437
+ listByPrefix(filter) {
37438
+ const where = ["project_id = ?"];
37439
+ const params = [filter.projectId];
37440
+ if (filter.symbolPrefix) {
37441
+ where.push("symbol LIKE ? ESCAPE ?");
37442
+ params.push(`${escapeLike(filter.symbolPrefix)}%`, "\\");
37443
+ }
37444
+ if (filter.path) {
37445
+ where.push("path = ?");
37446
+ params.push(filter.path);
37447
+ }
37448
+ const rows = this.db.prepare(`SELECT * FROM code_refs WHERE ${where.join(" AND ")} ORDER BY symbol, path`).all(...params);
37449
+ return rows.map(rowToCodeRef);
37450
+ }
37451
+ delete(slug) {
37452
+ this.db.prepare("DELETE FROM task_code_refs WHERE code_ref_slug = ?").run(slug);
37453
+ this.db.prepare("DELETE FROM code_refs WHERE slug = ?").run(slug);
37454
+ }
37455
+ // ── TOUCHES edges ──────────────────────────────────────────────────────────
37456
+ addTouches(taskId, codeRefSlug, relation) {
37457
+ this.db.prepare(
37458
+ `INSERT INTO task_code_refs (task_id, code_ref_slug, relation)
37459
+ VALUES (?, ?, ?)
37460
+ ON CONFLICT(task_id, code_ref_slug) DO UPDATE SET relation = excluded.relation`
37461
+ ).run(taskId, codeRefSlug, relation);
37462
+ }
37463
+ removeTouches(taskId, codeRefSlug) {
37464
+ this.db.prepare("DELETE FROM task_code_refs WHERE task_id = ? AND code_ref_slug = ?").run(taskId, codeRefSlug);
37465
+ }
37466
+ getTouchesForTask(taskId) {
37467
+ const rows = this.db.prepare("SELECT * FROM task_code_refs WHERE task_id = ? ORDER BY code_ref_slug").all(taskId);
37468
+ return rows.map((r) => ({
37469
+ taskId: r.task_id,
37470
+ codeRefSlug: r.code_ref_slug,
37471
+ relation: r.relation
37472
+ }));
37473
+ }
37474
+ getTouchesForCodeRef(codeRefSlug) {
37475
+ const rows = this.db.prepare("SELECT * FROM task_code_refs WHERE code_ref_slug = ? ORDER BY task_id").all(codeRefSlug);
37476
+ return rows.map((r) => ({
37477
+ taskId: r.task_id,
37478
+ codeRefSlug: r.code_ref_slug,
37479
+ relation: r.relation
37480
+ }));
37481
+ }
37482
+ };
37483
+ function escapeLike(s) {
37484
+ return s.replace(/[\\%_]/g, (m) => `\\${m}`);
37485
+ }
37486
+
37487
+ // src/core/sync/lamport-clock.ts
37488
+ function createSyncClockTables(db) {
37489
+ db.exec(`
37490
+ CREATE TABLE IF NOT EXISTS _sync_clock (
37491
+ id INTEGER PRIMARY KEY CHECK (id = 0),
37492
+ counter INTEGER NOT NULL DEFAULT 0
37493
+ )
37494
+ `);
37495
+ db.exec("INSERT OR IGNORE INTO _sync_clock (id, counter) VALUES (0, 0)");
37496
+ db.exec(`
37497
+ CREATE TABLE IF NOT EXISTS _sync_state (
37498
+ id INTEGER PRIMARY KEY CHECK (id = 0),
37499
+ last_pull_at INTEGER NOT NULL DEFAULT 0
37500
+ )
37501
+ `);
37502
+ db.exec("INSERT OR IGNORE INTO _sync_state (id, last_pull_at) VALUES (0, 0)");
37503
+ }
37504
+
36890
37505
  // src/core/domain/repositories/schema.ts
36891
- var SCHEMA_VERSION = 4;
37506
+ var SCHEMA_VERSION = 6;
36892
37507
  function initSchema(db) {
36893
37508
  createCoreTables(db);
36894
37509
  runLegacyMigrations(db);
36895
37510
  createM1Tables(db);
36896
37511
  createM2Tables(db);
36897
- createOAuthTables(db);
37512
+ createInvestigationTables(db);
37513
+ dropLegacyOAuthTables(db);
37514
+ addSyncColumns(db);
37515
+ createSyncClockTables(db);
36898
37516
  createIndexes(db);
36899
37517
  cleanupPoisonedTaskIds(db);
36900
37518
  seedSchemaVersion(db);
36901
37519
  }
37520
+ function addSyncColumns(db) {
37521
+ for (const table of SYNCABLE_TABLES) {
37522
+ for (const col of SYNC_COLUMNS) {
37523
+ try {
37524
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${col.name} ${col.sqliteType}`);
37525
+ } catch {
37526
+ }
37527
+ }
37528
+ }
37529
+ }
36902
37530
  function seedSchemaVersion(db) {
36903
37531
  db.exec(`
36904
37532
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -37030,6 +37658,10 @@ function runLegacyMigrations(db) {
37030
37658
  db.exec("ALTER TABLE sessions ADD COLUMN checkpoint_at TEXT");
37031
37659
  } catch {
37032
37660
  }
37661
+ try {
37662
+ db.exec("ALTER TABLE sessions ADD COLUMN cc_session_id TEXT");
37663
+ } catch {
37664
+ }
37033
37665
  try {
37034
37666
  db.exec("ALTER TABLE workspaces ADD COLUMN archived_at TEXT");
37035
37667
  } catch {
@@ -37077,6 +37709,34 @@ function migrateSessionsStatus(db) {
37077
37709
  db.exec("DROP TABLE sessions");
37078
37710
  db.exec("ALTER TABLE sessions_new RENAME TO sessions");
37079
37711
  }
37712
+ function migrateKnowledgeTypeCheck(db) {
37713
+ const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='knowledge_index'").get();
37714
+ if (!row?.sql) return;
37715
+ if (row.sql.includes("'feature'")) return;
37716
+ const cols = db.pragma("table_info(knowledge_index)").map(
37717
+ (c) => c.name
37718
+ );
37719
+ const colList = cols.join(", ");
37720
+ db.exec(`
37721
+ CREATE TABLE knowledge_index_new (
37722
+ slug TEXT PRIMARY KEY,
37723
+ project_id TEXT NOT NULL,
37724
+ scope TEXT NOT NULL CHECK (scope IN ('project','cross')),
37725
+ type TEXT NOT NULL CHECK (type IN ('spike','decision','postmortem','learning','evaluation','feature','code_ref','gotcha')),
37726
+ title TEXT NOT NULL,
37727
+ file_path TEXT NOT NULL,
37728
+ created_at TEXT NOT NULL,
37729
+ last_verified_at TEXT NOT NULL,
37730
+ embedding_provider_id TEXT,
37731
+ embedding_dims INTEGER,
37732
+ workspace_id TEXT REFERENCES workspaces(id),
37733
+ FOREIGN KEY (project_id) REFERENCES projects(id)
37734
+ )
37735
+ `);
37736
+ db.exec(`INSERT INTO knowledge_index_new (${colList}) SELECT ${colList} FROM knowledge_index`);
37737
+ db.exec("DROP TABLE knowledge_index");
37738
+ db.exec("ALTER TABLE knowledge_index_new RENAME TO knowledge_index");
37739
+ }
37080
37740
  var COUNTER_SANE_MAX = 1e5;
37081
37741
  function seedGlobalCounter(db, entityType, selectIdSql, prefixLen) {
37082
37742
  let max = 0;
@@ -37217,6 +37877,7 @@ function createM1Tables(db) {
37217
37877
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
37218
37878
  checkpoint TEXT,
37219
37879
  checkpoint_at TEXT,
37880
+ cc_session_id TEXT,
37220
37881
  FOREIGN KEY (project_id) REFERENCES projects(id)
37221
37882
  )
37222
37883
  `);
@@ -37304,6 +37965,7 @@ function createM1Tables(db) {
37304
37965
  CREATE TABLE IF NOT EXISTS inbox_items (
37305
37966
  id TEXT PRIMARY KEY,
37306
37967
  project_id TEXT,
37968
+ workspace_id TEXT,
37307
37969
  content TEXT NOT NULL,
37308
37970
  status TEXT NOT NULL DEFAULT 'raw',
37309
37971
  linked_task_id TEXT,
@@ -37311,12 +37973,16 @@ function createM1Tables(db) {
37311
37973
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
37312
37974
  )
37313
37975
  `);
37976
+ try {
37977
+ db.exec("ALTER TABLE inbox_items ADD COLUMN workspace_id TEXT");
37978
+ } catch {
37979
+ }
37314
37980
  db.exec(`
37315
37981
  CREATE TABLE IF NOT EXISTS knowledge_index (
37316
37982
  slug TEXT PRIMARY KEY,
37317
37983
  project_id TEXT NOT NULL,
37318
37984
  scope TEXT NOT NULL CHECK (scope IN ('project','cross')),
37319
- type TEXT NOT NULL CHECK (type IN ('spike','decision','postmortem','learning','evaluation')),
37985
+ type TEXT NOT NULL CHECK (type IN ('spike','decision','postmortem','learning','evaluation','feature','code_ref','gotcha')),
37320
37986
  title TEXT NOT NULL,
37321
37987
  file_path TEXT NOT NULL,
37322
37988
  created_at TEXT NOT NULL,
@@ -37338,6 +38004,37 @@ function createM1Tables(db) {
37338
38004
  db.exec("ALTER TABLE knowledge_index ADD COLUMN workspace_id TEXT REFERENCES workspaces(id)");
37339
38005
  } catch {
37340
38006
  }
38007
+ migrateKnowledgeTypeCheck(db);
38008
+ db.exec(`
38009
+ CREATE TABLE IF NOT EXISTS code_refs (
38010
+ slug TEXT PRIMARY KEY,
38011
+ project_id TEXT NOT NULL,
38012
+ workspace_id TEXT,
38013
+ path TEXT NOT NULL,
38014
+ symbol TEXT,
38015
+ line_hint INTEGER,
38016
+ commit_sha TEXT,
38017
+ created_at TEXT NOT NULL,
38018
+ last_verified_at TEXT NOT NULL,
38019
+ FOREIGN KEY (project_id) REFERENCES projects(id)
38020
+ )
38021
+ `);
38022
+ db.exec(
38023
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_code_refs_identity ON code_refs(project_id, path, COALESCE(symbol, ''))"
38024
+ );
38025
+ db.exec("CREATE INDEX IF NOT EXISTS idx_code_refs_symbol ON code_refs(project_id, symbol)");
38026
+ db.exec("CREATE INDEX IF NOT EXISTS idx_code_refs_path ON code_refs(project_id, path)");
38027
+ db.exec(`
38028
+ CREATE TABLE IF NOT EXISTS task_code_refs (
38029
+ task_id TEXT NOT NULL,
38030
+ code_ref_slug TEXT NOT NULL,
38031
+ relation TEXT NOT NULL CHECK (relation IN ('modifies','reference')),
38032
+ PRIMARY KEY (task_id, code_ref_slug),
38033
+ FOREIGN KEY (code_ref_slug) REFERENCES code_refs(slug)
38034
+ )
38035
+ `);
38036
+ db.exec("CREATE INDEX IF NOT EXISTS idx_task_code_refs_task ON task_code_refs(task_id)");
38037
+ db.exec("CREATE INDEX IF NOT EXISTS idx_task_code_refs_slug ON task_code_refs(code_ref_slug)");
37341
38038
  db.exec(`
37342
38039
  CREATE TABLE IF NOT EXISTS tool_invocations (
37343
38040
  id INTEGER PRIMARY KEY,
@@ -37377,37 +38074,52 @@ function createM2Tables(db) {
37377
38074
  )
37378
38075
  `);
37379
38076
  }
37380
- function createOAuthTables(db) {
38077
+ function createInvestigationTables(db) {
37381
38078
  db.exec(`
37382
- CREATE TABLE IF NOT EXISTS oauth_clients (
37383
- client_id TEXT PRIMARY KEY,
37384
- client_name TEXT NOT NULL,
37385
- redirect_uris TEXT NOT NULL,
37386
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
38079
+ CREATE TABLE IF NOT EXISTS investigations (
38080
+ id TEXT PRIMARY KEY,
38081
+ symptom TEXT NOT NULL,
38082
+ status TEXT NOT NULL DEFAULT 'exploring' CHECK (status IN ('exploring','confirmed','resolved')),
38083
+ task_id TEXT,
38084
+ session_id TEXT,
38085
+ root_cause TEXT,
38086
+ fix_summary TEXT,
38087
+ pattern_tag TEXT,
38088
+ created_at TEXT NOT NULL,
38089
+ resolved_at TEXT
37387
38090
  )
37388
38091
  `);
37389
38092
  db.exec(`
37390
- CREATE TABLE IF NOT EXISTS oauth_auth_codes (
37391
- code TEXT PRIMARY KEY,
37392
- client_id TEXT NOT NULL REFERENCES oauth_clients(client_id),
37393
- code_challenge TEXT NOT NULL,
37394
- code_challenge_method TEXT NOT NULL CHECK (code_challenge_method = 'S256'),
37395
- redirect_uri TEXT NOT NULL,
37396
- expires_at TEXT NOT NULL,
37397
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
38093
+ CREATE TABLE IF NOT EXISTS hypotheses (
38094
+ id TEXT PRIMARY KEY,
38095
+ investigation_id TEXT NOT NULL,
38096
+ description TEXT NOT NULL,
38097
+ status TEXT NOT NULL DEFAULT 'testing' CHECK (status IN ('testing','ruled_out','confirmed')),
38098
+ created_at TEXT NOT NULL,
38099
+ FOREIGN KEY (investigation_id) REFERENCES investigations(id)
37398
38100
  )
37399
38101
  `);
37400
38102
  db.exec(`
37401
- CREATE TABLE IF NOT EXISTS oauth_tokens (
37402
- access_token TEXT PRIMARY KEY,
37403
- refresh_token TEXT UNIQUE NOT NULL,
37404
- client_id TEXT NOT NULL REFERENCES oauth_clients(client_id),
37405
- access_expires_at TEXT NOT NULL,
37406
- refresh_expires_at TEXT NOT NULL,
37407
- revoked INTEGER NOT NULL DEFAULT 0,
37408
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
38103
+ CREATE TABLE IF NOT EXISTS evidence (
38104
+ id TEXT PRIMARY KEY,
38105
+ investigation_id TEXT NOT NULL,
38106
+ hypothesis_id TEXT,
38107
+ type TEXT NOT NULL CHECK (type IN ('screenshot','log','network','code_snippet')),
38108
+ ref TEXT NOT NULL,
38109
+ note TEXT,
38110
+ created_at TEXT NOT NULL,
38111
+ FOREIGN KEY (investigation_id) REFERENCES investigations(id),
38112
+ FOREIGN KEY (hypothesis_id) REFERENCES hypotheses(id)
37409
38113
  )
37410
38114
  `);
38115
+ db.exec("CREATE INDEX IF NOT EXISTS idx_hypotheses_investigation ON hypotheses(investigation_id)");
38116
+ db.exec("CREATE INDEX IF NOT EXISTS idx_evidence_investigation ON evidence(investigation_id)");
38117
+ db.exec("CREATE INDEX IF NOT EXISTS idx_evidence_hypothesis ON evidence(hypothesis_id)");
38118
+ }
38119
+ function dropLegacyOAuthTables(db) {
38120
+ db.exec("DROP TABLE IF EXISTS oauth_auth_codes");
38121
+ db.exec("DROP TABLE IF EXISTS oauth_tokens");
38122
+ db.exec("DROP TABLE IF EXISTS oauth_clients");
37411
38123
  }
37412
38124
  function createIndexes(db) {
37413
38125
  db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id)");
@@ -37433,6 +38145,7 @@ function createIndexes(db) {
37433
38145
  );
37434
38146
  db.exec("CREATE INDEX IF NOT EXISTS idx_inbox_project ON inbox_items(project_id)");
37435
38147
  db.exec("CREATE INDEX IF NOT EXISTS idx_inbox_status ON inbox_items(project_id, status)");
38148
+ db.exec("CREATE INDEX IF NOT EXISTS idx_inbox_workspace ON inbox_items(workspace_id)");
37436
38149
  db.exec(
37437
38150
  "CREATE INDEX IF NOT EXISTS idx_conversations_owner_session ON conversations(owner_session_id)"
37438
38151
  );
@@ -37452,16 +38165,6 @@ function createIndexes(db) {
37452
38165
  db.exec(
37453
38166
  "CREATE INDEX IF NOT EXISTS idx_agent_memories_recall ON agent_memories(importance DESC, recall_count DESC)"
37454
38167
  );
37455
- db.exec(
37456
- "CREATE INDEX IF NOT EXISTS idx_oauth_auth_codes_client ON oauth_auth_codes(client_id)"
37457
- );
37458
- db.exec(
37459
- "CREATE INDEX IF NOT EXISTS idx_oauth_auth_codes_expires ON oauth_auth_codes(expires_at)"
37460
- );
37461
- db.exec("CREATE INDEX IF NOT EXISTS idx_oauth_tokens_client ON oauth_tokens(client_id)");
37462
- db.exec(
37463
- "CREATE INDEX IF NOT EXISTS idx_oauth_tokens_access_expires ON oauth_tokens(access_expires_at)"
37464
- );
37465
38168
  }
37466
38169
  function cleanupPoisonedTaskIds(db) {
37467
38170
  const rows = db.prepare("SELECT id FROM tasks WHERE id GLOB 'TASK-[0-9]*'").all();
@@ -37952,6 +38655,14 @@ var RelationshipRepository = class {
37952
38655
  const rows = type ? this.db.prepare("SELECT * FROM relationships WHERE from_id = ? AND type = ?").all(itemId, type) : this.db.prepare("SELECT * FROM relationships WHERE from_id = ?").all(itemId);
37953
38656
  return rows.map(rowToRelationship);
37954
38657
  }
38658
+ // Inbound counterpart to getFrom — edges pointing AT itemId. Needed for the
38659
+ // ADR-NNN graph queries whose answer is the source node: "which tasks REALIZE
38660
+ // this feature?" = getTo(featureSlug, 'REALIZES') → from_ids; "which gotchas
38661
+ // are ABOUT it?" = getTo(featureSlug, 'ABOUT').
38662
+ getTo(itemId, type) {
38663
+ const rows = type ? this.db.prepare("SELECT * FROM relationships WHERE to_id = ? AND type = ?").all(itemId, type) : this.db.prepare("SELECT * FROM relationships WHERE to_id = ?").all(itemId);
38664
+ return rows.map(rowToRelationship);
38665
+ }
37955
38666
  };
37956
38667
 
37957
38668
  // src/core/domain/repositories/session-repository.ts
@@ -37964,6 +38675,7 @@ function rowToSession(row) {
37964
38675
  startedAt: row.started_at,
37965
38676
  endedAt: row.ended_at || null,
37966
38677
  status: row.status,
38678
+ ccSessionId: row.cc_session_id || null,
37967
38679
  handoff: row.handoff_json ? JSON.parse(row.handoff_json) : null,
37968
38680
  checkpoint: row.checkpoint ? JSON.parse(row.checkpoint) : null,
37969
38681
  checkpointAt: row.checkpoint_at || null,
@@ -37980,8 +38692,8 @@ var SessionRepository = class {
37980
38692
  const id = input.id || generateId("SESSION");
37981
38693
  const startedAt = input.startedAt || ts;
37982
38694
  this.db.prepare(
37983
- `INSERT INTO sessions (id, project_id, workspace_id, task_id, started_at, status, handoff_json, created_at)
37984
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
38695
+ `INSERT INTO sessions (id, project_id, workspace_id, task_id, started_at, status, cc_session_id, handoff_json, created_at)
38696
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
37985
38697
  ).run(
37986
38698
  id,
37987
38699
  input.projectId,
@@ -37989,6 +38701,7 @@ var SessionRepository = class {
37989
38701
  input.taskId || null,
37990
38702
  startedAt,
37991
38703
  input.status || "active",
38704
+ input.ccSessionId || null,
37992
38705
  input.handoff ? JSON.stringify(input.handoff) : null,
37993
38706
  ts
37994
38707
  );
@@ -38415,6 +39128,7 @@ function rowToInbox(row) {
38415
39128
  return {
38416
39129
  id: row.id,
38417
39130
  projectId: row.project_id || null,
39131
+ workspaceId: row.workspace_id || null,
38418
39132
  content: row.content,
38419
39133
  status: row.status,
38420
39134
  linkedTaskId: row.linked_task_id || null,
@@ -38437,9 +39151,9 @@ var InboxRepository = class {
38437
39151
  const projectId = input.projectId ?? null;
38438
39152
  const id = this.nextInboxId();
38439
39153
  this.db.prepare(
38440
- `INSERT INTO inbox_items (id, project_id, content, status, created_at, updated_at)
38441
- VALUES (?, ?, ?, 'raw', ?, ?)`
38442
- ).run(id, projectId, input.content, ts, ts);
39154
+ `INSERT INTO inbox_items (id, project_id, workspace_id, content, status, linked_task_id, created_at, updated_at)
39155
+ VALUES (?, ?, ?, ?, 'raw', ?, ?, ?)`
39156
+ ).run(id, projectId, input.workspaceId ?? null, input.content, input.linkedTaskId ?? null, ts, ts);
38443
39157
  return this.get(id);
38444
39158
  }
38445
39159
  update(id, input) {
@@ -38453,6 +39167,10 @@ var InboxRepository = class {
38453
39167
  sets.push("status = ?");
38454
39168
  params.push(input.status);
38455
39169
  }
39170
+ if (input.workspaceId !== void 0) {
39171
+ sets.push("workspace_id = ?");
39172
+ params.push(input.workspaceId);
39173
+ }
38456
39174
  if (input.linkedTaskId !== void 0) {
38457
39175
  sets.push("linked_task_id = ?");
38458
39176
  params.push(input.linkedTaskId);
@@ -38481,6 +39199,14 @@ var InboxRepository = class {
38481
39199
  params.push(filter.projectId);
38482
39200
  }
38483
39201
  }
39202
+ if (filter.workspaceId !== void 0) {
39203
+ if (filter.workspaceId === null) {
39204
+ wheres.push("workspace_id IS NULL");
39205
+ } else {
39206
+ wheres.push("workspace_id = ?");
39207
+ params.push(filter.workspaceId);
39208
+ }
39209
+ }
38484
39210
  if (filter.status) {
38485
39211
  wheres.push("status = ?");
38486
39212
  params.push(filter.status);
@@ -38723,11 +39449,14 @@ var SqliteTaskService = class {
38723
39449
  toolInvocations;
38724
39450
  sessionEvents;
38725
39451
  agentMemories;
39452
+ investigations;
38726
39453
  inboxLifecycle;
38727
39454
  conversationLifecycle;
38728
39455
  sessionLifecycle;
39456
+ investigationLifecycle;
38729
39457
  knowledgeRepo;
38730
39458
  knowledgeService;
39459
+ codeRefs;
38731
39460
  embeddingStore;
38732
39461
  embeddingProviderPromise;
38733
39462
  embeddingReadyPromise = null;
@@ -38756,6 +39485,8 @@ var SqliteTaskService = class {
38756
39485
  this.contextSources = new ContextSourceRepository(this.db);
38757
39486
  this.conversations = new ConversationRepository(this.db);
38758
39487
  this.inbox = new InboxRepository(this.db, this.counters);
39488
+ this.investigations = new InvestigationRepository(this.db, this.counters);
39489
+ this.investigationLifecycle = new InvestigationLifecycleService(this.db, this.investigations);
38759
39490
  this.inboxLifecycle = new InboxLifecycleService(
38760
39491
  this.db,
38761
39492
  this.inbox,
@@ -38768,6 +39499,7 @@ var SqliteTaskService = class {
38768
39499
  this.tasks,
38769
39500
  this.sessions
38770
39501
  );
39502
+ this.codeRefs = new CodeRefRepository(this.db);
38771
39503
  this.sessionLifecycle = new SessionLifecycleService(
38772
39504
  this.db,
38773
39505
  this.sessions,
@@ -38775,6 +39507,8 @@ var SqliteTaskService = class {
38775
39507
  this.conversations,
38776
39508
  this.tasks,
38777
39509
  this.sessionEvents,
39510
+ this.relationships,
39511
+ this.codeRefs,
38778
39512
  (input) => this.recallMemoriesSync(input)
38779
39513
  );
38780
39514
  this.knowledgeRepo = new KnowledgeRepository(this.db);
@@ -38783,7 +39517,8 @@ var SqliteTaskService = class {
38783
39517
  projects: this.projects,
38784
39518
  workspaces: this.workspaces,
38785
39519
  embeddingStore: this.embeddingStore,
38786
- embeddingProvider: () => this.embeddingProviderPromise
39520
+ embeddingProvider: () => this.embeddingProviderPromise,
39521
+ edges: this.relationships
38787
39522
  });
38788
39523
  }
38789
39524
  // ── Lifecycle ──────────────────────────────────────────────────────────────
@@ -38924,6 +39659,9 @@ var SqliteTaskService = class {
38924
39659
  async getRelationshipsFrom(itemId, type) {
38925
39660
  return this.relationships.getFrom(itemId, type);
38926
39661
  }
39662
+ async getRelationshipsTo(itemId, type) {
39663
+ return this.relationships.getTo(itemId, type);
39664
+ }
38927
39665
  // ── Session operations (M1) ────────────────────────────────────────────────
38928
39666
  async createSession(input) {
38929
39667
  return this.sessions.create(input);
@@ -39015,6 +39753,9 @@ var SqliteTaskService = class {
39015
39753
  return this.conversations.findByLink(linkedType, linkedId);
39016
39754
  }
39017
39755
  // ── Inbox ──────────────────────────────────────────────────────────────────
39756
+ async fetchSince(since) {
39757
+ return fetchSinceFromSqlite(this.db, since);
39758
+ }
39018
39759
  async createInbox(input) {
39019
39760
  return this.inbox.create(input);
39020
39761
  }
@@ -39066,6 +39807,25 @@ var SqliteTaskService = class {
39066
39807
  async resumeSession(id) {
39067
39808
  return this.sessionLifecycle.resumeSession(id);
39068
39809
  }
39810
+ // ── Investigation lifecycle (ADR-035, stdio-only) ──────────────────────────
39811
+ async startInvestigation(input) {
39812
+ return this.investigationLifecycle.startInvestigation(input);
39813
+ }
39814
+ async addHypothesis(investigationId, description) {
39815
+ return this.investigationLifecycle.addHypothesis(investigationId, description);
39816
+ }
39817
+ async setHypothesisStatus(hypothesisId, status) {
39818
+ return this.investigationLifecycle.setHypothesisStatus(hypothesisId, status);
39819
+ }
39820
+ async addEvidence(input) {
39821
+ return this.investigationLifecycle.addEvidence(input);
39822
+ }
39823
+ async resolveInvestigation(id, input) {
39824
+ return this.investigationLifecycle.resolveInvestigation(id, input);
39825
+ }
39826
+ async getInvestigation(id) {
39827
+ return this.investigationLifecycle.getInvestigation(id);
39828
+ }
39069
39829
  // ── Knowledge ─────────────────────────────────────────────────────────────
39070
39830
  async createKnowledge(input) {
39071
39831
  return this.knowledgeService.createKnowledge(input);
@@ -39091,6 +39851,31 @@ var SqliteTaskService = class {
39091
39851
  searchKnowledge(query, k) {
39092
39852
  return this.knowledgeService.searchKnowledge(query, k);
39093
39853
  }
39854
+ // ── Code refs + TOUCHES edges (TASK-988) ────────────────────────────────────
39855
+ async upsertCodeRef(input) {
39856
+ return this.codeRefs.upsert(input, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
39857
+ }
39858
+ async getCodeRef(slug) {
39859
+ return this.codeRefs.get(slug);
39860
+ }
39861
+ async listCodeRefsByPrefix(filter) {
39862
+ return this.codeRefs.listByPrefix(filter);
39863
+ }
39864
+ async deleteCodeRef(slug) {
39865
+ this.codeRefs.delete(slug);
39866
+ }
39867
+ async addTouches(taskId, codeRefSlug, relation) {
39868
+ this.codeRefs.addTouches(taskId, codeRefSlug, relation);
39869
+ }
39870
+ async removeTouches(taskId, codeRefSlug) {
39871
+ this.codeRefs.removeTouches(taskId, codeRefSlug);
39872
+ }
39873
+ async getTouchesForTask(taskId) {
39874
+ return this.codeRefs.getTouchesForTask(taskId);
39875
+ }
39876
+ async getTouchesForCodeRef(codeRefSlug) {
39877
+ return this.codeRefs.getTouchesForCodeRef(codeRefSlug);
39878
+ }
39094
39879
  // ── Session Events ─────────────────────────────────────────────────────────
39095
39880
  async createSessionEvent(input) {
39096
39881
  return this.sessionEvents.create(input);
@@ -39185,6 +39970,15 @@ function loadVecExtension(db) {
39185
39970
  }
39186
39971
 
39187
39972
  // src/core/domain/repositories/postgres/migrations.ts
39973
+ function buildSyncColumnsSql() {
39974
+ const lines = [];
39975
+ for (const table of SYNCABLE_TABLES) {
39976
+ for (const col of SYNC_COLUMNS) {
39977
+ lines.push(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${col.name} ${col.pgType};`);
39978
+ }
39979
+ }
39980
+ return lines.join("\n");
39981
+ }
39188
39982
  var MIGRATIONS = [
39189
39983
  {
39190
39984
  name: "001_init",
@@ -39350,6 +40144,15 @@ var MIGRATIONS = [
39350
40144
  CREATE INDEX IF NOT EXISTS inbox_status_idx ON inbox_items (project_id, status);
39351
40145
  `
39352
40146
  },
40147
+ {
40148
+ // P6 (ADR-032 Pillar 6, TASK-993): nullable workspace scope on inbox, filled
40149
+ // progressively. inbox_add is remote-allowlisted, so the PG path must persist it.
40150
+ name: "009_inbox_workspace",
40151
+ sql: `
40152
+ ALTER TABLE inbox_items ADD COLUMN IF NOT EXISTS workspace_id TEXT;
40153
+ CREATE INDEX IF NOT EXISTS inbox_workspace_idx ON inbox_items (workspace_id);
40154
+ `
40155
+ },
39353
40156
  {
39354
40157
  // TASK-972 Phase 1 (additive) — signed_off_json column + conversation_message_reads
39355
40158
  // side-table. PG equivalents of SQLite's INSERT OR IGNORE = ON CONFLICT DO NOTHING;
@@ -39430,6 +40233,40 @@ var MIGRATIONS = [
39430
40233
  CREATE INDEX IF NOT EXISTS oauth_tokens_client_idx ON oauth_tokens (client_id);
39431
40234
  CREATE INDEX IF NOT EXISTS oauth_tokens_access_expires_idx ON oauth_tokens (access_expires_at);
39432
40235
  `
40236
+ },
40237
+ {
40238
+ // ADR-034: drop the ADR-027 self-issued OAuth store. Keycloak now issues and
40239
+ // stores tokens; choda-deck only validates Keycloak JWTs. 010_oauth stays in
40240
+ // history (already applied on existing deploys) — this migration removes it.
40241
+ name: "011_drop_oauth",
40242
+ sql: `
40243
+ DROP TABLE IF EXISTS oauth_auth_codes;
40244
+ DROP TABLE IF EXISTS oauth_tokens;
40245
+ DROP TABLE IF EXISTS oauth_clients;
40246
+ `
40247
+ },
40248
+ {
40249
+ // ADR-030 Phase 1 (TASK-978) — additive sync metadata (updated_at / deleted_at
40250
+ // / origin) on every syncable entity table. Mirrors schema.ts addSyncColumns;
40251
+ // both generate from src/core/sync/syncable-tables.ts. No _sync_clock/_sync_state
40252
+ // singletons here — the Lamport clock is a local-SQLite concern (Postgres is
40253
+ // canonical and stamps updated_at server-side in Phase 2+).
40254
+ name: "012_sync_columns",
40255
+ sql: buildSyncColumnsSql()
40256
+ },
40257
+ {
40258
+ // ADR-030 Phase 2 (TASK-978) — server-side Lamport clock for the remote
40259
+ // (Postgres canonical) writer. inbox_add stamps sync_updated_at from this
40260
+ // counter so the local pull can order remote changes. Singleton row pinned
40261
+ // at id = 0 (mirrors SQLite's _sync_clock).
40262
+ name: "013_sync_clock",
40263
+ sql: `
40264
+ CREATE TABLE IF NOT EXISTS _sync_clock (
40265
+ id INTEGER PRIMARY KEY CHECK (id = 0),
40266
+ counter BIGINT NOT NULL DEFAULT 0
40267
+ );
40268
+ INSERT INTO _sync_clock (id, counter) VALUES (0, 0) ON CONFLICT (id) DO NOTHING;
40269
+ `
39433
40270
  }
39434
40271
  ];
39435
40272
  async function migrate(conn) {
@@ -39790,11 +40627,61 @@ var PostgresConversationRepository = class {
39790
40627
  }
39791
40628
  };
39792
40629
 
40630
+ // node_modules/pg/esm/index.mjs
40631
+ var import_lib = __toESM(require_lib2(), 1);
40632
+ var Client = import_lib.default.Client;
40633
+ var Pool = import_lib.default.Pool;
40634
+ var Connection = import_lib.default.Connection;
40635
+ var types = import_lib.default.types;
40636
+ var Query = import_lib.default.Query;
40637
+ var DatabaseError = import_lib.default.DatabaseError;
40638
+ var escapeIdentifier = import_lib.default.escapeIdentifier;
40639
+ var escapeLiteral = import_lib.default.escapeLiteral;
40640
+ var Result = import_lib.default.Result;
40641
+ var TypeOverrides = import_lib.default.TypeOverrides;
40642
+ var defaults = import_lib.default.defaults;
40643
+
40644
+ // src/core/domain/repositories/postgres/connection.ts
40645
+ async function runInTx(q, fn) {
40646
+ if (q.transaction) return q.transaction(fn);
40647
+ return fn(q);
40648
+ }
40649
+ var PgConnection = class {
40650
+ pool;
40651
+ constructor(config2) {
40652
+ this.pool = typeof config2 === "string" ? new Pool({ connectionString: config2 }) : new Pool(config2);
40653
+ }
40654
+ async query(text, params) {
40655
+ return this.pool.query(text, params);
40656
+ }
40657
+ async transaction(fn) {
40658
+ const client = await this.pool.connect();
40659
+ try {
40660
+ await client.query("BEGIN");
40661
+ const result = await fn(client);
40662
+ await client.query("COMMIT");
40663
+ return result;
40664
+ } catch (err) {
40665
+ try {
40666
+ await client.query("ROLLBACK");
40667
+ } catch {
40668
+ }
40669
+ throw err;
40670
+ } finally {
40671
+ client.release();
40672
+ }
40673
+ }
40674
+ async close() {
40675
+ await this.pool.end();
40676
+ }
40677
+ };
40678
+
39793
40679
  // src/core/domain/repositories/postgres/inbox-repository.pg.ts
39794
40680
  function mapRow5(row) {
39795
40681
  return {
39796
40682
  id: row.id,
39797
40683
  projectId: row.project_id,
40684
+ workspaceId: row.workspace_id,
39798
40685
  content: row.content,
39799
40686
  status: row.status,
39800
40687
  linkedTaskId: row.linked_task_id,
@@ -39802,7 +40689,7 @@ function mapRow5(row) {
39802
40689
  updatedAt: row.updated_at
39803
40690
  };
39804
40691
  }
39805
- var SELECT_COLS3 = "id, project_id, content, status, linked_task_id, created_at, updated_at";
40692
+ var SELECT_COLS3 = "id, project_id, workspace_id, content, status, linked_task_id, created_at, updated_at";
39806
40693
  var PostgresInboxRepository = class {
39807
40694
  constructor(conn, counters) {
39808
40695
  this.conn = conn;
@@ -39817,11 +40704,25 @@ var PostgresInboxRepository = class {
39817
40704
  async create(input) {
39818
40705
  const ts = now();
39819
40706
  const id = await this.nextInboxId();
39820
- await this.conn.query(
39821
- `INSERT INTO inbox_items (id, project_id, content, status, created_at, updated_at)
39822
- VALUES ($1, $2, $3, 'raw', $4, $4)`,
39823
- [id, input.projectId ?? null, input.content, ts]
39824
- );
40707
+ await runInTx(this.conn, async (tx) => {
40708
+ const clk = await tx.query(
40709
+ "UPDATE _sync_clock SET counter = counter + 1 WHERE id = 0 RETURNING counter"
40710
+ );
40711
+ const lamport = Number(clk.rows[0].counter);
40712
+ await tx.query(
40713
+ `INSERT INTO inbox_items (id, project_id, workspace_id, content, status, linked_task_id, created_at, updated_at, sync_updated_at, sync_origin)
40714
+ VALUES ($1, $2, $3, $4, 'raw', $5, $6, $6, $7, 'remote')`,
40715
+ [
40716
+ id,
40717
+ input.projectId ?? null,
40718
+ input.workspaceId ?? null,
40719
+ input.content,
40720
+ input.linkedTaskId ?? null,
40721
+ ts,
40722
+ lamport
40723
+ ]
40724
+ );
40725
+ });
39825
40726
  const got = await this.get(id);
39826
40727
  if (!got) throw new Error(`Inbox item disappeared after insert: ${id}`);
39827
40728
  return got;
@@ -39846,6 +40747,14 @@ var PostgresInboxRepository = class {
39846
40747
  params.push(filter.projectId);
39847
40748
  }
39848
40749
  }
40750
+ if (filter.workspaceId !== void 0) {
40751
+ if (filter.workspaceId === null) {
40752
+ wheres.push("workspace_id IS NULL");
40753
+ } else {
40754
+ wheres.push(`workspace_id = $${n++}`);
40755
+ params.push(filter.workspaceId);
40756
+ }
40757
+ }
39849
40758
  if (filter.status) {
39850
40759
  wheres.push(`status = $${n++}`);
39851
40760
  params.push(filter.status);
@@ -39955,54 +40864,8 @@ var PostgresTaskService = class {
39955
40864
  async getConversationActions(conversationId) {
39956
40865
  return this.conversations.getActions(conversationId);
39957
40866
  }
39958
- };
39959
-
39960
- // node_modules/pg/esm/index.mjs
39961
- var import_lib = __toESM(require_lib2(), 1);
39962
- var Client = import_lib.default.Client;
39963
- var Pool = import_lib.default.Pool;
39964
- var Connection = import_lib.default.Connection;
39965
- var types = import_lib.default.types;
39966
- var Query = import_lib.default.Query;
39967
- var DatabaseError = import_lib.default.DatabaseError;
39968
- var escapeIdentifier = import_lib.default.escapeIdentifier;
39969
- var escapeLiteral = import_lib.default.escapeLiteral;
39970
- var Result = import_lib.default.Result;
39971
- var TypeOverrides = import_lib.default.TypeOverrides;
39972
- var defaults = import_lib.default.defaults;
39973
-
39974
- // src/core/domain/repositories/postgres/connection.ts
39975
- async function runInTx(q, fn) {
39976
- if (q.transaction) return q.transaction(fn);
39977
- return fn(q);
39978
- }
39979
- var PgConnection = class {
39980
- pool;
39981
- constructor(config2) {
39982
- this.pool = typeof config2 === "string" ? new Pool({ connectionString: config2 }) : new Pool(config2);
39983
- }
39984
- async query(text, params) {
39985
- return this.pool.query(text, params);
39986
- }
39987
- async transaction(fn) {
39988
- const client = await this.pool.connect();
39989
- try {
39990
- await client.query("BEGIN");
39991
- const result = await fn(client);
39992
- await client.query("COMMIT");
39993
- return result;
39994
- } catch (err) {
39995
- try {
39996
- await client.query("ROLLBACK");
39997
- } catch {
39998
- }
39999
- throw err;
40000
- } finally {
40001
- client.release();
40002
- }
40003
- }
40004
- async close() {
40005
- await this.pool.end();
40867
+ async fetchSince(since) {
40868
+ return fetchSinceFromPg(this.conn, since);
40006
40869
  }
40007
40870
  };
40008
40871
 
@@ -40035,346 +40898,6 @@ function requireBackendForTransport(backend, transport) {
40035
40898
  }
40036
40899
  }
40037
40900
 
40038
- // src/core/domain/repositories/oauth-repository.ts
40039
- var import_crypto = require("crypto");
40040
- var OAuthRepository = class {
40041
- constructor(db) {
40042
- this.db = db;
40043
- this.insertClient = db.prepare(
40044
- `INSERT INTO oauth_clients (client_id, client_name, redirect_uris)
40045
- VALUES (?, ?, ?)`
40046
- );
40047
- this.selectClient = db.prepare("SELECT * FROM oauth_clients WHERE client_id = ?");
40048
- this.insertAuthCode = db.prepare(
40049
- `INSERT INTO oauth_auth_codes
40050
- (code, client_id, code_challenge, code_challenge_method, redirect_uri, expires_at)
40051
- VALUES (?, ?, ?, 'S256', ?, ?)`
40052
- );
40053
- this.consumeAuthCodeStmt = db.prepare(
40054
- "DELETE FROM oauth_auth_codes WHERE code = ? RETURNING *"
40055
- );
40056
- this.insertToken = db.prepare(
40057
- `INSERT INTO oauth_tokens
40058
- (access_token, refresh_token, client_id, access_expires_at, refresh_expires_at)
40059
- VALUES (?, ?, ?, ?, ?)`
40060
- );
40061
- this.selectAccessToken = db.prepare(
40062
- "SELECT * FROM oauth_tokens WHERE access_token = ? AND revoked = 0"
40063
- );
40064
- this.selectRefreshToken = db.prepare(
40065
- "SELECT * FROM oauth_tokens WHERE refresh_token = ?"
40066
- );
40067
- this.markRevoked = db.prepare(
40068
- "UPDATE oauth_tokens SET revoked = 1 WHERE access_token = ?"
40069
- );
40070
- this.revokeAllForClient = db.prepare(
40071
- "UPDATE oauth_tokens SET revoked = 1 WHERE client_id = ?"
40072
- );
40073
- }
40074
- db;
40075
- insertClient;
40076
- selectClient;
40077
- insertAuthCode;
40078
- consumeAuthCodeStmt;
40079
- insertToken;
40080
- selectAccessToken;
40081
- selectRefreshToken;
40082
- markRevoked;
40083
- revokeAllForClient;
40084
- async registerClient(input) {
40085
- const clientId = `cdck_cli_${randomToken(16)}`;
40086
- this.insertClient.run(clientId, input.clientName, JSON.stringify(input.redirectUris));
40087
- const row = this.selectClient.get(clientId);
40088
- return rowToClient(row);
40089
- }
40090
- async getClient(clientId) {
40091
- const row = this.selectClient.get(clientId);
40092
- return row ? rowToClient(row) : null;
40093
- }
40094
- async createAuthCode(input) {
40095
- const code = `cdck_code_${randomToken(32)}`;
40096
- const expiresAt = isoFromNow(input.ttlSeconds);
40097
- this.insertAuthCode.run(
40098
- code,
40099
- input.clientId,
40100
- input.codeChallenge,
40101
- input.redirectUri,
40102
- expiresAt
40103
- );
40104
- return {
40105
- code,
40106
- clientId: input.clientId,
40107
- codeChallenge: input.codeChallenge,
40108
- redirectUri: input.redirectUri,
40109
- expiresAt
40110
- };
40111
- }
40112
- // Single-use: deletes the row even if expired. Caller must check expiresAt.
40113
- async consumeAuthCode(code) {
40114
- const row = this.consumeAuthCodeStmt.get(code);
40115
- if (!row) return null;
40116
- return {
40117
- code: row.code,
40118
- clientId: row.client_id,
40119
- codeChallenge: row.code_challenge,
40120
- redirectUri: row.redirect_uri,
40121
- expiresAt: row.expires_at
40122
- };
40123
- }
40124
- async createTokens(input) {
40125
- const accessToken = `cdck_at_${randomToken(32)}`;
40126
- const refreshToken = `cdck_rt_${randomToken(32)}`;
40127
- const accessExpiresAt = isoFromNow(input.accessTtlSeconds);
40128
- const refreshExpiresAt = isoFromNow(input.refreshTtlSeconds);
40129
- this.insertToken.run(
40130
- accessToken,
40131
- refreshToken,
40132
- input.clientId,
40133
- accessExpiresAt,
40134
- refreshExpiresAt
40135
- );
40136
- return {
40137
- accessToken,
40138
- refreshToken,
40139
- clientId: input.clientId,
40140
- accessExpiresAt,
40141
- refreshExpiresAt
40142
- };
40143
- }
40144
- // Returns the row only if not revoked and not expired. Otherwise null.
40145
- async validateAccessToken(accessToken) {
40146
- const row = this.selectAccessToken.get(accessToken);
40147
- if (!row) return null;
40148
- if (Date.parse(row.access_expires_at) <= Date.now()) return null;
40149
- return rowToToken(row);
40150
- }
40151
- // Atomic transaction: detect replay (revoked refresh) → revoke chain;
40152
- // detect expiry → invalid_grant; happy path → mark old revoked + insert new.
40153
- async rotateRefresh(refreshToken, ttls) {
40154
- const tx = this.db.transaction(() => {
40155
- const row = this.selectRefreshToken.get(refreshToken);
40156
- if (!row) return { ok: false, error: "invalid_grant" };
40157
- if (row.revoked === 1) {
40158
- this.revokeAllForClient.run(row.client_id);
40159
- return { ok: false, error: "replay_detected" };
40160
- }
40161
- if (Date.parse(row.refresh_expires_at) <= Date.now()) {
40162
- return { ok: false, error: "invalid_grant" };
40163
- }
40164
- this.markRevoked.run(row.access_token);
40165
- const accessToken = `cdck_at_${randomToken(32)}`;
40166
- const newRefreshToken = `cdck_rt_${randomToken(32)}`;
40167
- const accessExpiresAt = isoFromNow(ttls.accessTtlSeconds);
40168
- const refreshExpiresAt = isoFromNow(ttls.refreshTtlSeconds);
40169
- this.insertToken.run(
40170
- accessToken,
40171
- newRefreshToken,
40172
- row.client_id,
40173
- accessExpiresAt,
40174
- refreshExpiresAt
40175
- );
40176
- return {
40177
- ok: true,
40178
- tokens: {
40179
- accessToken,
40180
- refreshToken: newRefreshToken,
40181
- clientId: row.client_id,
40182
- accessExpiresAt,
40183
- refreshExpiresAt
40184
- }
40185
- };
40186
- });
40187
- return tx();
40188
- }
40189
- };
40190
- function rowToClient(row) {
40191
- return {
40192
- clientId: row.client_id,
40193
- clientName: row.client_name,
40194
- redirectUris: JSON.parse(row.redirect_uris),
40195
- createdAt: row.created_at
40196
- };
40197
- }
40198
- function rowToToken(row) {
40199
- return {
40200
- accessToken: row.access_token,
40201
- refreshToken: row.refresh_token,
40202
- clientId: row.client_id,
40203
- accessExpiresAt: row.access_expires_at,
40204
- refreshExpiresAt: row.refresh_expires_at
40205
- };
40206
- }
40207
- function randomToken(bytes) {
40208
- return (0, import_crypto.randomBytes)(bytes).toString("base64url");
40209
- }
40210
- function isoFromNow(seconds) {
40211
- return new Date(Date.now() + seconds * 1e3).toISOString();
40212
- }
40213
-
40214
- // src/core/domain/repositories/postgres/oauth-repository.pg.ts
40215
- var import_crypto2 = require("crypto");
40216
- function rowToClient2(row) {
40217
- return {
40218
- clientId: row.client_id,
40219
- clientName: row.client_name,
40220
- redirectUris: row.redirect_uris,
40221
- createdAt: row.created_at.toISOString()
40222
- };
40223
- }
40224
- function rowToToken2(row) {
40225
- return {
40226
- accessToken: row.access_token,
40227
- refreshToken: row.refresh_token,
40228
- clientId: row.client_id,
40229
- accessExpiresAt: row.access_expires_at,
40230
- refreshExpiresAt: row.refresh_expires_at
40231
- };
40232
- }
40233
- function randomToken2(bytes) {
40234
- return (0, import_crypto2.randomBytes)(bytes).toString("base64url");
40235
- }
40236
- function isoFromNow2(seconds) {
40237
- return new Date(Date.now() + seconds * 1e3).toISOString();
40238
- }
40239
- var PostgresOAuthRepository = class {
40240
- constructor(conn) {
40241
- this.conn = conn;
40242
- }
40243
- conn;
40244
- async registerClient(input) {
40245
- const clientId = `cdck_cli_${randomToken2(16)}`;
40246
- const result = await this.conn.query(
40247
- `INSERT INTO oauth_clients (client_id, client_name, redirect_uris)
40248
- VALUES ($1, $2, $3::jsonb)
40249
- RETURNING client_id, client_name, redirect_uris, created_at`,
40250
- [clientId, input.clientName, JSON.stringify(input.redirectUris)]
40251
- );
40252
- return rowToClient2(result.rows[0]);
40253
- }
40254
- async getClient(clientId) {
40255
- const result = await this.conn.query(
40256
- `SELECT client_id, client_name, redirect_uris, created_at
40257
- FROM oauth_clients WHERE client_id = $1`,
40258
- [clientId]
40259
- );
40260
- const row = result.rows[0];
40261
- return row ? rowToClient2(row) : null;
40262
- }
40263
- async createAuthCode(input) {
40264
- const code = `cdck_code_${randomToken2(32)}`;
40265
- const expiresAt = isoFromNow2(input.ttlSeconds);
40266
- await this.conn.query(
40267
- `INSERT INTO oauth_auth_codes
40268
- (code, client_id, code_challenge, code_challenge_method, redirect_uri, expires_at)
40269
- VALUES ($1, $2, $3, 'S256', $4, $5)`,
40270
- [code, input.clientId, input.codeChallenge, input.redirectUri, expiresAt]
40271
- );
40272
- return {
40273
- code,
40274
- clientId: input.clientId,
40275
- codeChallenge: input.codeChallenge,
40276
- redirectUri: input.redirectUri,
40277
- expiresAt
40278
- };
40279
- }
40280
- // Single-use: DELETE...RETURNING removes the row even if expired.
40281
- // Caller is expected to check expiresAt.
40282
- async consumeAuthCode(code) {
40283
- const result = await this.conn.query(
40284
- `DELETE FROM oauth_auth_codes WHERE code = $1
40285
- RETURNING code, client_id, code_challenge, redirect_uri, expires_at`,
40286
- [code]
40287
- );
40288
- const row = result.rows[0];
40289
- if (!row) return null;
40290
- return {
40291
- code: row.code,
40292
- clientId: row.client_id,
40293
- codeChallenge: row.code_challenge,
40294
- redirectUri: row.redirect_uri,
40295
- expiresAt: row.expires_at
40296
- };
40297
- }
40298
- async createTokens(input) {
40299
- const accessToken = `cdck_at_${randomToken2(32)}`;
40300
- const refreshToken = `cdck_rt_${randomToken2(32)}`;
40301
- const accessExpiresAt = isoFromNow2(input.accessTtlSeconds);
40302
- const refreshExpiresAt = isoFromNow2(input.refreshTtlSeconds);
40303
- await this.conn.query(
40304
- `INSERT INTO oauth_tokens
40305
- (access_token, refresh_token, client_id, access_expires_at, refresh_expires_at)
40306
- VALUES ($1, $2, $3, $4, $5)`,
40307
- [accessToken, refreshToken, input.clientId, accessExpiresAt, refreshExpiresAt]
40308
- );
40309
- return {
40310
- accessToken,
40311
- refreshToken,
40312
- clientId: input.clientId,
40313
- accessExpiresAt,
40314
- refreshExpiresAt
40315
- };
40316
- }
40317
- // Returns the row only if not revoked AND not expired. Otherwise null.
40318
- async validateAccessToken(accessToken) {
40319
- const result = await this.conn.query(
40320
- `SELECT access_token, refresh_token, client_id, access_expires_at,
40321
- refresh_expires_at, revoked
40322
- FROM oauth_tokens WHERE access_token = $1 AND revoked = FALSE`,
40323
- [accessToken]
40324
- );
40325
- const row = result.rows[0];
40326
- if (!row) return null;
40327
- if (Date.parse(row.access_expires_at) <= Date.now()) return null;
40328
- return rowToToken2(row);
40329
- }
40330
- // Atomic: detect replay (revoked refresh) → revoke chain;
40331
- // detect expiry → invalid_grant; happy path → mark old revoked + insert new.
40332
- async rotateRefresh(refreshToken, ttls) {
40333
- return runInTx(this.conn, async (tx) => {
40334
- const lookup = await tx.query(
40335
- `SELECT access_token, refresh_token, client_id, access_expires_at,
40336
- refresh_expires_at, revoked
40337
- FROM oauth_tokens WHERE refresh_token = $1`,
40338
- [refreshToken]
40339
- );
40340
- const row = lookup.rows[0];
40341
- if (!row) return { ok: false, error: "invalid_grant" };
40342
- if (row.revoked) {
40343
- await tx.query("UPDATE oauth_tokens SET revoked = TRUE WHERE client_id = $1", [
40344
- row.client_id
40345
- ]);
40346
- return { ok: false, error: "replay_detected" };
40347
- }
40348
- if (Date.parse(row.refresh_expires_at) <= Date.now()) {
40349
- return { ok: false, error: "invalid_grant" };
40350
- }
40351
- await tx.query("UPDATE oauth_tokens SET revoked = TRUE WHERE access_token = $1", [
40352
- row.access_token
40353
- ]);
40354
- const accessToken = `cdck_at_${randomToken2(32)}`;
40355
- const newRefreshToken = `cdck_rt_${randomToken2(32)}`;
40356
- const accessExpiresAt = isoFromNow2(ttls.accessTtlSeconds);
40357
- const refreshExpiresAt = isoFromNow2(ttls.refreshTtlSeconds);
40358
- await tx.query(
40359
- `INSERT INTO oauth_tokens
40360
- (access_token, refresh_token, client_id, access_expires_at, refresh_expires_at)
40361
- VALUES ($1, $2, $3, $4, $5)`,
40362
- [accessToken, newRefreshToken, row.client_id, accessExpiresAt, refreshExpiresAt]
40363
- );
40364
- return {
40365
- ok: true,
40366
- tokens: {
40367
- accessToken,
40368
- refreshToken: newRefreshToken,
40369
- clientId: row.client_id,
40370
- accessExpiresAt,
40371
- refreshExpiresAt
40372
- }
40373
- };
40374
- });
40375
- }
40376
- };
40377
-
40378
40901
  // src/core/paths.ts
40379
40902
  var path4 = __toESM(require("path"));
40380
40903
  function resolveDataPaths(electronDataDir) {
@@ -40466,14 +40989,14 @@ function createInstrumentedServer(server, sink, toolAllowlist) {
40466
40989
 
40467
40990
  // src/adapters/mcp/http-transport.ts
40468
40991
  var import_http = require("http");
40469
- var import_buffer3 = require("buffer");
40470
- var import_crypto6 = require("crypto");
40992
+ var import_buffer = require("buffer");
40993
+ var import_crypto2 = require("crypto");
40471
40994
 
40472
40995
  // node_modules/@hono/node-server/dist/index.mjs
40473
40996
  var import_http2 = require("http2");
40474
40997
  var import_http22 = require("http2");
40475
40998
  var import_stream = require("stream");
40476
- var import_crypto3 = __toESM(require("crypto"), 1);
40999
+ var import_crypto = __toESM(require("crypto"), 1);
40477
41000
  var RequestError = class extends Error {
40478
41001
  constructor(message, options) {
40479
41002
  super(message, options);
@@ -40812,7 +41335,7 @@ var buildOutgoingHttpHeaders = (headers) => {
40812
41335
  };
40813
41336
  var X_ALREADY_SENT = "x-hono-already-sent";
40814
41337
  if (typeof global.crypto === "undefined") {
40815
- global.crypto = import_crypto3.default;
41338
+ global.crypto = import_crypto.default;
40816
41339
  }
40817
41340
  var outgoingEnded = /* @__PURE__ */ Symbol("outgoingEnded");
40818
41341
  var incomingDraining = /* @__PURE__ */ Symbol("incomingDraining");
@@ -41795,8 +42318,8 @@ var StreamableHTTPServerTransport = class {
41795
42318
  };
41796
42319
 
41797
42320
  // src/adapters/mcp/oauth/discovery.ts
41798
- function authServerMetadata(issuer) {
41799
- const base = stripTrailingSlash(issuer);
42321
+ function authServerMetadata(origin) {
42322
+ const base = stripTrailingSlash(origin);
41800
42323
  return {
41801
42324
  issuer: base,
41802
42325
  authorization_endpoint: `${base}/authorize`,
@@ -41808,8 +42331,8 @@ function authServerMetadata(issuer) {
41808
42331
  token_endpoint_auth_methods_supported: ["none"]
41809
42332
  };
41810
42333
  }
41811
- function protectedResourceMetadata(issuer) {
41812
- const base = stripTrailingSlash(issuer);
42334
+ function protectedResourceMetadata(origin) {
42335
+ const base = stripTrailingSlash(origin);
41813
42336
  return {
41814
42337
  resource: `${base}/mcp`,
41815
42338
  authorization_servers: [base],
@@ -41820,335 +42343,67 @@ function stripTrailingSlash(s) {
41820
42343
  return s.endsWith("/") ? s.slice(0, -1) : s;
41821
42344
  }
41822
42345
 
41823
- // src/adapters/mcp/oauth/register.ts
41824
- async function handleRegister(repo, raw) {
41825
- if (raw === null || typeof raw !== "object") {
41826
- return errorResponse(400, "invalid_client_metadata", "request body must be a JSON object");
41827
- }
41828
- const req = raw;
41829
- if (!Array.isArray(req.redirect_uris) || req.redirect_uris.length === 0) {
41830
- return errorResponse(
41831
- 400,
41832
- "invalid_redirect_uri",
41833
- "redirect_uris must be a non-empty array of URLs"
41834
- );
41835
- }
41836
- if (!req.redirect_uris.every((u) => typeof u === "string" && isHttpUrl(u))) {
41837
- return errorResponse(
41838
- 400,
41839
- "invalid_redirect_uri",
41840
- "each redirect_uri must be an http(s) URL"
41841
- );
42346
+ // src/adapters/mcp/oauth/keycloak-proxy.ts
42347
+ var FORWARDED_AUTHORIZE_PARAMS = [
42348
+ "response_type",
42349
+ "client_id",
42350
+ "redirect_uri",
42351
+ "state",
42352
+ "scope",
42353
+ "code_challenge",
42354
+ "code_challenge_method",
42355
+ "nonce",
42356
+ "resource"
42357
+ ];
42358
+ function handleAuthorizeRedirect(cfg, params) {
42359
+ const forwarded = new URLSearchParams();
42360
+ for (const key of FORWARDED_AUTHORIZE_PARAMS) {
42361
+ const value = params.get(key);
42362
+ if (value !== null) forwarded.set(key, value);
42363
+ }
42364
+ if (!forwarded.has("client_id")) forwarded.set("client_id", cfg.clientId);
42365
+ if (!forwarded.has("response_type")) forwarded.set("response_type", "code");
42366
+ return { location: `${cfg.authorizationEndpoint}?${forwarded.toString()}` };
42367
+ }
42368
+ async function handleTokenProxy(cfg, form, fetchImpl = fetch) {
42369
+ const upstream = new URLSearchParams(form);
42370
+ if (!upstream.has("client_id")) upstream.set("client_id", cfg.clientId);
42371
+ if (cfg.clientSecret && !upstream.has("client_secret")) {
42372
+ upstream.set("client_secret", cfg.clientSecret);
41842
42373
  }
41843
- const clientName = typeof req.client_name === "string" ? req.client_name : "mcp-client";
41844
- const client = await repo.registerClient({
41845
- clientName,
41846
- redirectUris: req.redirect_uris
41847
- });
41848
- return {
41849
- status: 201,
41850
- body: {
41851
- client_id: client.clientId,
41852
- client_id_issued_at: Math.floor(Date.now() / 1e3),
41853
- client_name: client.clientName,
41854
- redirect_uris: client.redirectUris,
41855
- grant_types: ["authorization_code", "refresh_token"],
41856
- response_types: ["code"],
41857
- token_endpoint_auth_method: "none"
41858
- }
41859
- };
41860
- }
41861
- function errorResponse(status, error48, errorDescription) {
41862
- return {
41863
- status,
41864
- body: { error: error48, error_description: errorDescription }
41865
- };
41866
- }
41867
- function isHttpUrl(s) {
41868
42374
  try {
41869
- const u = new URL(s);
41870
- return u.protocol === "https:" || u.protocol === "http:";
42375
+ const res = await fetchImpl(cfg.tokenEndpoint, {
42376
+ method: "POST",
42377
+ headers: {
42378
+ "content-type": "application/x-www-form-urlencoded",
42379
+ accept: "application/json"
42380
+ },
42381
+ body: upstream.toString()
42382
+ });
42383
+ const body = await res.json().catch(() => ({}));
42384
+ return { status: res.status, body };
41871
42385
  } catch {
41872
- return false;
41873
- }
41874
- }
41875
-
41876
- // src/adapters/mcp/oauth/authorize.ts
41877
- var import_crypto4 = require("crypto");
41878
- var import_buffer = require("buffer");
41879
-
41880
- // src/adapters/mcp/oauth/consent-template.ts
41881
- function renderConsentScreen(params) {
41882
- const errorBlock = params.errorMessage ? `<p class="err">${escapeHtml(params.errorMessage)}</p>` : "";
41883
- const stateField = params.state === null ? "" : `<input type="hidden" name="state" value="${escapeHtml(params.state)}" />`;
41884
- return `<!doctype html>
41885
- <html lang="en">
41886
- <head>
41887
- <meta charset="utf-8" />
41888
- <title>Authorize ${escapeHtml(params.clientName)}</title>
41889
- <style>
41890
- body { font-family: system-ui, sans-serif; max-width: 32rem; margin: 4rem auto; padding: 0 1rem; color: #111; }
41891
- h1 { font-size: 1.25rem; }
41892
- .client { background: #f5f5f5; padding: 0.75rem 1rem; border-radius: 4px; margin: 1rem 0; font-family: ui-monospace, monospace; font-size: 0.875rem; }
41893
- label { display: block; margin-top: 1rem; font-size: 0.9rem; }
41894
- input[type=password] { width: 100%; padding: 0.5rem; font-size: 1rem; box-sizing: border-box; }
41895
- button { margin-top: 1rem; padding: 0.5rem 1.25rem; font-size: 1rem; cursor: pointer; }
41896
- .err { color: #b00020; margin-top: 1rem; font-size: 0.9rem; }
41897
- .meta { color: #666; font-size: 0.8rem; margin-top: 2rem; }
41898
- </style>
41899
- </head>
41900
- <body>
41901
- <h1>Authorize access</h1>
41902
- <p>An application wants to connect to your choda-deck MCP server.</p>
41903
- <div class="client">
41904
- <div><strong>${escapeHtml(params.clientName)}</strong></div>
41905
- <div>${escapeHtml(params.clientId)}</div>
41906
- <div>Redirect: ${escapeHtml(params.redirectUri)}</div>
41907
- </div>
41908
- ${errorBlock}
41909
- <form method="POST" action="/authorize">
41910
- <input type="hidden" name="response_type" value="${escapeHtml(params.responseType)}" />
41911
- <input type="hidden" name="client_id" value="${escapeHtml(params.clientId)}" />
41912
- <input type="hidden" name="redirect_uri" value="${escapeHtml(params.redirectUri)}" />
41913
- <input type="hidden" name="code_challenge" value="${escapeHtml(params.codeChallenge)}" />
41914
- <input type="hidden" name="code_challenge_method" value="${escapeHtml(params.codeChallengeMethod)}" />
41915
- ${stateField}
41916
- <label for="password">Consent password</label>
41917
- <input id="password" name="consent_password" type="password" autocomplete="off" autofocus required />
41918
- <button type="submit">Approve</button>
41919
- </form>
41920
- <p class="meta">choda-deck OAuth 2.0 + DCR (ADR-027)</p>
41921
- </body>
41922
- </html>`;
41923
- }
41924
- function escapeHtml(s) {
41925
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
41926
- }
41927
-
41928
- // src/adapters/mcp/oauth/authorize.ts
41929
- var AUTH_CODE_TTL_SECONDS = 60;
41930
- var CHALLENGE_MIN_LEN = 43;
41931
- var CHALLENGE_MAX_LEN = 128;
41932
- async function handleAuthorizeGet(repo, query) {
41933
- const params = parseParams(query);
41934
- const v = await validate2(repo, params);
41935
- if (v.kind !== "ok") return v.result;
41936
- return renderForm(params, v.client.clientName);
41937
- }
41938
- async function handleAuthorizePost(repo, form, consentPasswordHashHex) {
41939
- const params = parseParams(form);
41940
- const v = await validate2(repo, params);
41941
- if (v.kind !== "ok") return v.result;
41942
- const submitted = form.get("consent_password") ?? "";
41943
- if (!verifyConsentPassword(submitted, consentPasswordHashHex)) {
41944
42386
  return {
41945
- kind: "html",
41946
- status: 401,
41947
- html: renderConsentScreen({
41948
- clientName: v.client.clientName,
41949
- clientId: params.clientId,
41950
- redirectUri: params.redirectUri,
41951
- state: params.state,
41952
- codeChallenge: params.codeChallenge,
41953
- codeChallengeMethod: "S256",
41954
- responseType: "code",
41955
- errorMessage: "Incorrect consent password."
41956
- })
42387
+ status: 502,
42388
+ body: { error: "server_error", error_description: "authorization server unreachable" }
41957
42389
  };
41958
42390
  }
41959
- const ac = await repo.createAuthCode({
41960
- clientId: params.clientId,
41961
- codeChallenge: params.codeChallenge,
41962
- redirectUri: params.redirectUri,
41963
- ttlSeconds: AUTH_CODE_TTL_SECONDS
41964
- });
41965
- const url2 = new URL(params.redirectUri);
41966
- url2.searchParams.set("code", ac.code);
41967
- if (params.state !== null) url2.searchParams.set("state", params.state);
41968
- return { kind: "redirect", status: 302, location: url2.toString() };
41969
- }
41970
- function parseParams(p) {
41971
- return {
41972
- responseType: p.get("response_type"),
41973
- clientId: p.get("client_id"),
41974
- redirectUri: p.get("redirect_uri"),
41975
- state: p.get("state"),
41976
- codeChallenge: p.get("code_challenge"),
41977
- codeChallengeMethod: p.get("code_challenge_method")
41978
- };
41979
- }
41980
- async function validate2(repo, p) {
41981
- if (!p.clientId) {
41982
- return { kind: "fatal", result: htmlError(400, "Missing client_id.") };
41983
- }
41984
- const client = await repo.getClient(p.clientId);
41985
- if (!client) {
41986
- return { kind: "fatal", result: htmlError(400, "Unknown client_id.") };
41987
- }
41988
- if (!p.redirectUri || !client.redirectUris.includes(p.redirectUri)) {
41989
- return {
41990
- kind: "fatal",
41991
- result: htmlError(400, "Missing or unregistered redirect_uri.")
41992
- };
41993
- }
41994
- if (p.responseType !== "code") {
41995
- return {
41996
- kind: "redirectError",
41997
- result: redirectWithError(p.redirectUri, p.state, "unsupported_response_type")
41998
- };
41999
- }
42000
- if (p.codeChallengeMethod !== "S256") {
42001
- return {
42002
- kind: "redirectError",
42003
- result: redirectWithError(
42004
- p.redirectUri,
42005
- p.state,
42006
- "invalid_request",
42007
- "code_challenge_method must be S256"
42008
- )
42009
- };
42010
- }
42011
- if (!p.codeChallenge || p.codeChallenge.length < CHALLENGE_MIN_LEN || p.codeChallenge.length > CHALLENGE_MAX_LEN) {
42012
- return {
42013
- kind: "redirectError",
42014
- result: redirectWithError(
42015
- p.redirectUri,
42016
- p.state,
42017
- "invalid_request",
42018
- "code_challenge missing or wrong length"
42019
- )
42020
- };
42021
- }
42022
- return { kind: "ok", client };
42023
- }
42024
- function renderForm(p, clientName) {
42025
- return {
42026
- kind: "html",
42027
- status: 200,
42028
- html: renderConsentScreen({
42029
- clientName,
42030
- clientId: p.clientId,
42031
- redirectUri: p.redirectUri,
42032
- state: p.state,
42033
- codeChallenge: p.codeChallenge,
42034
- codeChallengeMethod: "S256",
42035
- responseType: "code"
42036
- })
42037
- };
42038
- }
42039
- function htmlError(status, message) {
42040
- const html = `<!doctype html><html><head><meta charset="utf-8"/><title>OAuth error</title></head><body style="font-family:system-ui;max-width:32rem;margin:4rem auto;padding:0 1rem"><h1>Authorization error</h1><p>${escapeHtml2(message)}</p></body></html>`;
42041
- return { kind: "html", status, html };
42042
- }
42043
- function redirectWithError(redirectUri, state, error48, errorDescription) {
42044
- const url2 = new URL(redirectUri);
42045
- url2.searchParams.set("error", error48);
42046
- if (errorDescription) url2.searchParams.set("error_description", errorDescription);
42047
- if (state !== null) url2.searchParams.set("state", state);
42048
- return { kind: "redirect", status: 302, location: url2.toString() };
42049
- }
42050
- function verifyConsentPassword(submitted, expectedHashHex) {
42051
- if (submitted.length === 0 || expectedHashHex.length === 0) return false;
42052
- const submittedHash = (0, import_crypto4.createHash)("sha256").update(submitted, "utf8").digest();
42053
- let expected;
42054
- try {
42055
- expected = import_buffer.Buffer.from(expectedHashHex, "hex");
42056
- } catch {
42057
- return false;
42058
- }
42059
- if (submittedHash.length !== expected.length) return false;
42060
- return (0, import_crypto4.timingSafeEqual)(submittedHash, expected);
42061
- }
42062
- function escapeHtml2(s) {
42063
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
42064
- }
42065
-
42066
- // src/adapters/mcp/oauth/pkce.ts
42067
- var import_crypto5 = require("crypto");
42068
- var import_buffer2 = require("buffer");
42069
- function verifyPkceS256(verifier, challenge) {
42070
- if (verifier.length < 43 || verifier.length > 128) return false;
42071
- const computed = computeChallengeS256(verifier);
42072
- const a = import_buffer2.Buffer.from(computed, "utf8");
42073
- const b = import_buffer2.Buffer.from(challenge, "utf8");
42074
- if (a.length !== b.length) return false;
42075
- return (0, import_crypto5.timingSafeEqual)(a, b);
42076
- }
42077
- function computeChallengeS256(verifier) {
42078
- return (0, import_crypto5.createHash)("sha256").update(verifier, "utf8").digest("base64url");
42079
- }
42080
-
42081
- // src/adapters/mcp/oauth/token.ts
42082
- var ACCESS_TTL_SECONDS = 60 * 60;
42083
- var REFRESH_TTL_SECONDS = 60 * 60 * 24 * 30;
42084
- async function handleToken(repo, form) {
42085
- const grantType = form.get("grant_type");
42086
- if (grantType === "authorization_code") return handleCodeGrant(repo, form);
42087
- if (grantType === "refresh_token") return handleRefreshGrant(repo, form);
42088
- return errorResponse2(
42089
- 400,
42090
- "unsupported_grant_type",
42091
- "only authorization_code and refresh_token are supported"
42092
- );
42093
- }
42094
- async function handleCodeGrant(repo, form) {
42095
- const code = form.get("code");
42096
- const redirectUri = form.get("redirect_uri");
42097
- const clientId = form.get("client_id");
42098
- const codeVerifier = form.get("code_verifier");
42099
- if (!code || !redirectUri || !clientId || !codeVerifier) {
42100
- return errorResponse2(400, "invalid_request", "missing required parameter");
42101
- }
42102
- const consumed = await repo.consumeAuthCode(code);
42103
- if (!consumed) return errorResponse2(400, "invalid_grant", "code unknown or already used");
42104
- if (Date.parse(consumed.expiresAt) <= Date.now()) {
42105
- return errorResponse2(400, "invalid_grant", "code expired");
42106
- }
42107
- if (consumed.clientId !== clientId) {
42108
- return errorResponse2(400, "invalid_grant", "client_id does not match code");
42109
- }
42110
- if (consumed.redirectUri !== redirectUri) {
42111
- return errorResponse2(400, "invalid_grant", "redirect_uri does not match code");
42112
- }
42113
- if (!verifyPkceS256(codeVerifier, consumed.codeChallenge)) {
42114
- return errorResponse2(400, "invalid_grant", "PKCE verifier does not match challenge");
42115
- }
42116
- const tokens = await repo.createTokens({
42117
- clientId,
42118
- accessTtlSeconds: ACCESS_TTL_SECONDS,
42119
- refreshTtlSeconds: REFRESH_TTL_SECONDS
42120
- });
42121
- return tokenSuccess(tokens);
42122
- }
42123
- async function handleRefreshGrant(repo, form) {
42124
- const refreshToken = form.get("refresh_token");
42125
- if (!refreshToken) return errorResponse2(400, "invalid_request", "missing refresh_token");
42126
- const result = await repo.rotateRefresh(refreshToken, {
42127
- accessTtlSeconds: ACCESS_TTL_SECONDS,
42128
- refreshTtlSeconds: REFRESH_TTL_SECONDS
42129
- });
42130
- if (!result.ok) {
42131
- const description = result.error === "replay_detected" ? "refresh token replayed; chain revoked" : "refresh token unknown or expired";
42132
- return errorResponse2(400, "invalid_grant", description);
42133
- }
42134
- return tokenSuccess(result.tokens);
42135
42391
  }
42136
- function tokenSuccess(tokens) {
42392
+ function handleRegisterStatic(cfg, parsed) {
42393
+ const redirectUris = isObject2(parsed) && Array.isArray(parsed.redirect_uris) ? parsed.redirect_uris.filter((u) => typeof u === "string") : [];
42137
42394
  return {
42138
- status: 200,
42395
+ status: 201,
42139
42396
  body: {
42140
- access_token: tokens.accessToken,
42141
- token_type: "Bearer",
42142
- expires_in: ACCESS_TTL_SECONDS,
42143
- refresh_token: tokens.refreshToken
42397
+ client_id: cfg.clientId,
42398
+ token_endpoint_auth_method: cfg.clientSecret ? "client_secret_post" : "none",
42399
+ grant_types: ["authorization_code", "refresh_token"],
42400
+ response_types: ["code"],
42401
+ redirect_uris: redirectUris
42144
42402
  }
42145
42403
  };
42146
42404
  }
42147
- function errorResponse2(status, error48, errorDescription) {
42148
- return {
42149
- status,
42150
- body: { error: error48, error_description: errorDescription }
42151
- };
42405
+ function isObject2(v) {
42406
+ return typeof v === "object" && v !== null;
42152
42407
  }
42153
42408
 
42154
42409
  // src/adapters/mcp/http-transport.ts
@@ -42160,10 +42415,10 @@ var BodyTooLargeError = class extends Error {
42160
42415
  }
42161
42416
  };
42162
42417
  async function startHttpTransport(serverFactory, opts) {
42163
- const tokenBuf = import_buffer3.Buffer.from(opts.token, "utf8");
42418
+ const tokenBuf = import_buffer.Buffer.from(opts.token, "utf8");
42164
42419
  const oauth = opts.oauth;
42165
42420
  const httpServer = (0, import_http.createServer)((req, res) => {
42166
- handle(req, res, serverFactory, tokenBuf, oauth).catch((err) => {
42421
+ handle(req, res, serverFactory, tokenBuf, oauth, opts.syncSource).catch((err) => {
42167
42422
  console.error("[choda-deck] http handler error", err);
42168
42423
  if (!res.headersSent) {
42169
42424
  res.writeHead(500);
@@ -42184,10 +42439,11 @@ async function startHttpTransport(serverFactory, opts) {
42184
42439
  address: bound,
42185
42440
  close: () => new Promise((resolve3, reject) => {
42186
42441
  httpServer.close((err) => err ? reject(err) : resolve3());
42442
+ httpServer.closeIdleConnections();
42187
42443
  })
42188
42444
  };
42189
42445
  }
42190
- async function handle(req, res, serverFactory, tokenBuf, oauth) {
42446
+ async function handle(req, res, serverFactory, tokenBuf, oauth, syncSource) {
42191
42447
  const parsedUrl = new URL(req.url ?? "/", "http://placeholder");
42192
42448
  const pathname = parsedUrl.pathname;
42193
42449
  const method = req.method ?? "GET";
@@ -42197,19 +42453,42 @@ async function handle(req, res, serverFactory, tokenBuf, oauth) {
42197
42453
  if (oauth && await tryHandleOAuthRoute(req, res, pathname, method, parsedUrl, oauth)) {
42198
42454
  return;
42199
42455
  }
42456
+ if (pathname === "/sync/since" && method === "GET") {
42457
+ return handleSyncSince(req, res, tokenBuf, oauth, syncSource, parsedUrl);
42458
+ }
42200
42459
  if (pathname === "/mcp" && method === "POST") {
42201
42460
  return handleMcp(req, res, serverFactory, tokenBuf, oauth);
42202
42461
  }
42203
42462
  res.writeHead(404);
42204
42463
  res.end();
42205
42464
  }
42465
+ async function handleSyncSince(req, res, tokenBuf, oauth, syncSource, parsedUrl) {
42466
+ if (!syncSource) {
42467
+ res.writeHead(404);
42468
+ res.end();
42469
+ return;
42470
+ }
42471
+ if (!await isAuthorized(req.headers.authorization ?? "", tokenBuf, oauth)) {
42472
+ sendUnauthorized(res, oauth);
42473
+ return;
42474
+ }
42475
+ const sinceRaw = parsedUrl.searchParams.get("since");
42476
+ const since = Number.parseInt(sinceRaw ?? "0", 10);
42477
+ if (!Number.isFinite(since) || since < 0) {
42478
+ res.writeHead(400);
42479
+ res.end();
42480
+ return;
42481
+ }
42482
+ const deltas = await syncSource.fetchSince(since);
42483
+ sendJson(res, 200, { since, deltas });
42484
+ }
42206
42485
  async function tryHandleOAuthRoute(req, res, pathname, method, parsedUrl, oauth) {
42207
42486
  if (pathname === "/.well-known/oauth-authorization-server" && method === "GET") {
42208
- sendJson(res, 200, authServerMetadata(oauth.issuer));
42487
+ sendJson(res, 200, authServerMetadata(oauth.origin));
42209
42488
  return true;
42210
42489
  }
42211
42490
  if (pathname === "/.well-known/oauth-protected-resource" && method === "GET") {
42212
- sendJson(res, 200, protectedResourceMetadata(oauth.issuer));
42491
+ sendJson(res, 200, protectedResourceMetadata(oauth.origin));
42213
42492
  return true;
42214
42493
  }
42215
42494
  if (pathname === "/register" && method === "POST") {
@@ -42220,26 +42499,14 @@ async function tryHandleOAuthRoute(req, res, pathname, method, parsedUrl, oauth)
42220
42499
  writeBodyError(res, err);
42221
42500
  return true;
42222
42501
  }
42223
- const result = await handleRegister(oauth.repo, parsed);
42502
+ const result = handleRegisterStatic(oauth.keycloak, parsed);
42224
42503
  sendJson(res, result.status, result.body);
42225
42504
  return true;
42226
42505
  }
42227
42506
  if (pathname === "/authorize" && method === "GET") {
42228
- sendAuthorizeResult(res, await handleAuthorizeGet(oauth.repo, parsedUrl.searchParams));
42229
- return true;
42230
- }
42231
- if (pathname === "/authorize" && method === "POST") {
42232
- let form;
42233
- try {
42234
- form = await readForm(req);
42235
- } catch (err) {
42236
- writeBodyError(res, err);
42237
- return true;
42238
- }
42239
- sendAuthorizeResult(
42240
- res,
42241
- await handleAuthorizePost(oauth.repo, form, oauth.consentPasswordHashHex)
42242
- );
42507
+ const { location } = handleAuthorizeRedirect(oauth.keycloak, parsedUrl.searchParams);
42508
+ res.writeHead(302, { location });
42509
+ res.end();
42243
42510
  return true;
42244
42511
  }
42245
42512
  if (pathname === "/token" && method === "POST") {
@@ -42250,7 +42517,7 @@ async function tryHandleOAuthRoute(req, res, pathname, method, parsedUrl, oauth)
42250
42517
  writeBodyError(res, err);
42251
42518
  return true;
42252
42519
  }
42253
- const result = await handleToken(oauth.repo, form);
42520
+ const result = await handleTokenProxy(oauth.keycloak, form);
42254
42521
  res.setHeader("Cache-Control", "no-store");
42255
42522
  sendJson(res, result.status, result.body);
42256
42523
  return true;
@@ -42258,15 +42525,8 @@ async function tryHandleOAuthRoute(req, res, pathname, method, parsedUrl, oauth)
42258
42525
  return false;
42259
42526
  }
42260
42527
  async function handleMcp(req, res, serverFactory, tokenBuf, oauth) {
42261
- const authHeader = req.headers.authorization ?? "";
42262
- const authorized = oauth ? await verifyOAuthBearer(authHeader, oauth.repo) : verifyBearer(authHeader, tokenBuf);
42263
- if (!authorized) {
42264
- if (oauth) {
42265
- const resourceMetadataUrl = `${oauth.issuer}/.well-known/oauth-protected-resource`;
42266
- res.setHeader("WWW-Authenticate", `Bearer resource_metadata="${resourceMetadataUrl}"`);
42267
- }
42268
- res.writeHead(401);
42269
- res.end();
42528
+ if (!await isAuthorized(req.headers.authorization ?? "", tokenBuf, oauth)) {
42529
+ sendUnauthorized(res, oauth);
42270
42530
  return;
42271
42531
  }
42272
42532
  const contentType = (req.headers["content-type"] ?? "").toLowerCase();
@@ -42309,36 +42569,39 @@ async function handleMcp(req, res, serverFactory, tokenBuf, oauth) {
42309
42569
  }
42310
42570
  }
42311
42571
  }
42572
+ async function isAuthorized(authHeader, tokenBuf, oauth) {
42573
+ return oauth ? verifyOAuthBearer(authHeader, oauth.verifier) : verifyBearer(authHeader, tokenBuf);
42574
+ }
42575
+ function sendUnauthorized(res, oauth) {
42576
+ if (oauth) {
42577
+ const resourceMetadataUrl = `${oauth.origin}/.well-known/oauth-protected-resource`;
42578
+ res.setHeader("WWW-Authenticate", `Bearer resource_metadata="${resourceMetadataUrl}"`);
42579
+ }
42580
+ res.writeHead(401);
42581
+ res.end();
42582
+ }
42312
42583
  function verifyBearer(authHeader, tokenBuf) {
42313
42584
  const prefix = "Bearer ";
42314
42585
  if (!authHeader.startsWith(prefix)) return false;
42315
- const provided = import_buffer3.Buffer.from(authHeader.slice(prefix.length), "utf8");
42586
+ const provided = import_buffer.Buffer.from(authHeader.slice(prefix.length), "utf8");
42316
42587
  if (provided.length !== tokenBuf.length) return false;
42317
- return (0, import_crypto6.timingSafeEqual)(provided, tokenBuf);
42588
+ return (0, import_crypto2.timingSafeEqual)(provided, tokenBuf);
42318
42589
  }
42319
- async function verifyOAuthBearer(authHeader, repo) {
42590
+ async function verifyOAuthBearer(authHeader, verifier) {
42320
42591
  const prefix = "Bearer ";
42321
42592
  if (!authHeader.startsWith(prefix)) return false;
42322
42593
  const token = authHeader.slice(prefix.length);
42323
- return await repo.validateAccessToken(token) !== null;
42594
+ const claims = await verifier.verify(token);
42595
+ return claims !== null;
42324
42596
  }
42325
42597
  function sendJson(res, status, body) {
42326
42598
  const payload = JSON.stringify(body);
42327
42599
  res.writeHead(status, {
42328
42600
  "content-type": "application/json",
42329
- "content-length": import_buffer3.Buffer.byteLength(payload).toString()
42601
+ "content-length": import_buffer.Buffer.byteLength(payload).toString()
42330
42602
  });
42331
42603
  res.end(payload);
42332
42604
  }
42333
- function sendAuthorizeResult(res, result) {
42334
- if (result.kind === "redirect") {
42335
- res.writeHead(302, { location: result.location });
42336
- res.end();
42337
- return;
42338
- }
42339
- res.writeHead(result.status, { "content-type": "text/html; charset=utf-8" });
42340
- res.end(result.html);
42341
- }
42342
42605
  async function readJson(req) {
42343
42606
  const raw = await readBody(req);
42344
42607
  return JSON.parse(raw.toString("utf8"));
@@ -42379,7 +42642,7 @@ function readBody(req) {
42379
42642
  chunks.push(chunk);
42380
42643
  }
42381
42644
  });
42382
- req.on("end", () => settle(() => resolve3(import_buffer3.Buffer.concat(chunks))));
42645
+ req.on("end", () => settle(() => resolve3(import_buffer.Buffer.concat(chunks))));
42383
42646
  req.on("error", (err) => settle(() => reject(err)));
42384
42647
  });
42385
42648
  }
@@ -42401,293 +42664,119 @@ function listen(server, port, bind) {
42401
42664
  });
42402
42665
  }
42403
42666
 
42404
- // src/adapters/mcp/mcp-tools/types.ts
42405
- function textResponse(payload) {
42406
- const text = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
42407
- return { content: [{ type: "text", text }] };
42408
- }
42409
-
42410
- // src/adapters/mcp/mcp-tools/task-context-graphify.ts
42411
- var fs3 = __toESM(require("node:fs"));
42412
- var path5 = __toESM(require("node:path"));
42413
- var STALE_DAYS = 7;
42414
- var BFS_DEPTH = 2;
42415
- var MAX_AFFECTED_FILES = 15;
42416
- var MAX_GOD_NODES = 10;
42417
- var GOD_NODE_DEGREE_THRESHOLD = 5;
42418
- var CONFIDENCE_MIN = 0.7;
42419
- var RELATION_FILTER = /* @__PURE__ */ new Set([
42420
- "imports_from",
42421
- "calls",
42422
- "contains",
42423
- "method",
42424
- "implements",
42425
- "references"
42426
- ]);
42427
- var KEYWORD_STOPWORDS = /* @__PURE__ */ new Set([
42428
- "add",
42429
- "update",
42430
- "fix",
42431
- "create",
42432
- "make",
42433
- "implement",
42434
- "the",
42435
- "for",
42436
- "from",
42437
- "into",
42438
- "with",
42439
- "this",
42440
- "that",
42441
- "and",
42442
- "but",
42443
- "when",
42444
- "then",
42445
- "thing",
42446
- "stuff",
42447
- "code",
42448
- "task",
42449
- "tasks",
42450
- "feature",
42451
- "work",
42452
- "change",
42453
- "changes",
42454
- "trong",
42455
- "kh\xF4ng",
42456
- "tr\xEAn",
42457
- "d\u01B0\u1EDBi",
42458
- "ch\u1EE9a",
42459
- "\u0111\xFAng",
42460
- "ng\u01B0\u1EE3c",
42461
- "ph\u1EA3i",
42462
- "c\u1EA7n",
42463
- "n\u1EBFu",
42464
- "khi",
42465
- "nh\u01B0",
42466
- "theo",
42467
- "sau",
42468
- "tr\u01B0\u1EDBc"
42469
- ]);
42470
- var LABEL_KEY_PREFIX = /^(assignee|adr|phase)[:-]/i;
42471
- var LABEL_EXACT_DROP = /* @__PURE__ */ new Set([
42472
- "auto-safe",
42473
- "bug",
42474
- "feat",
42475
- "fix",
42476
- "test",
42477
- "metrics",
42478
- "chore",
42479
- "docs"
42480
- ]);
42481
- async function buildGraphifyContext(task, svc) {
42482
- const graphPath = await findGraphPath(task.projectId, svc);
42483
- if (!graphPath) {
42484
- return {
42485
- status: "no-graph",
42486
- message: "No graphify-out/graph.json in project workspaces. Run `/graphify <workspace>` to enable graph-driven context."
42487
- };
42488
- }
42489
- const keywords = extractKeywords(task);
42490
- const filePointerPaths = extractFilePointers(task.body ?? "");
42491
- if (keywords.length === 0 && filePointerPaths.length === 0) {
42492
- return {
42493
- status: "no-matches",
42494
- message: "Task title/AC/labels/file-pointers produced no usable signal."
42495
- };
42496
- }
42497
- const data = JSON.parse(fs3.readFileSync(graphPath, "utf8"));
42498
- const nodeIndex = new Map(data.nodes.map((n) => [n.id, n]));
42499
- const adj = buildAdjacency(data.links);
42500
- const startNodes = findStartNodes(data.nodes, keywords, filePointerPaths, 3);
42501
- if (startNodes.length === 0) {
42502
- return {
42503
- status: "no-matches",
42504
- message: filePointerPaths.length > 0 ? `File pointers not in graph: ${filePointerPaths.join(", ")}; keywords: ${keywords.join(", ")}` : `No graph nodes matched keywords: ${keywords.join(", ")}`
42505
- };
42667
+ // src/adapters/mcp/oauth/jwt-verifier.ts
42668
+ var import_crypto3 = require("crypto");
42669
+ var REFRESH_THROTTLE_MS = 1e4;
42670
+ function createKeycloakVerifier(config2, loadKeys) {
42671
+ return new KeycloakJwtVerifier(config2, loadKeys ?? defaultJwksLoader(config2.jwksUri));
42672
+ }
42673
+ var KeycloakJwtVerifier = class {
42674
+ constructor(config2, loadKeys) {
42675
+ this.config = config2;
42676
+ this.loadKeys = loadKeys;
42677
+ }
42678
+ config;
42679
+ loadKeys;
42680
+ keys = /* @__PURE__ */ new Map();
42681
+ lastFetchMs = 0;
42682
+ inflight = null;
42683
+ async verify(token) {
42684
+ const parts = token.split(".");
42685
+ if (parts.length !== 3) return null;
42686
+ const [headerB64, payloadB64, signatureB64] = parts;
42687
+ const header = decodeJson(headerB64);
42688
+ if (!header || header.alg !== "RS256" || !header.kid) return null;
42689
+ const key = await this.resolveKey(header.kid);
42690
+ if (!key) return null;
42691
+ if (!verifySignature(`${headerB64}.${payloadB64}`, signatureB64, key)) return null;
42692
+ const claims = decodeJson(payloadB64);
42693
+ if (!claims) return null;
42694
+ if (!this.claimsValid(claims)) return null;
42695
+ return claims;
42696
+ }
42697
+ claimsValid(claims) {
42698
+ if (claims.iss !== this.config.issuer) return false;
42699
+ if (typeof claims.exp !== "number" || claims.exp * 1e3 <= Date.now()) return false;
42700
+ return audienceMatches(claims, this.config.audience);
42701
+ }
42702
+ // Cache by kid; on an unknown kid (key rollover) refetch once, throttled so a
42703
+ // bogus-kid flood can't hammer Keycloak.
42704
+ async resolveKey(kid) {
42705
+ const cached2 = this.keys.get(kid);
42706
+ if (cached2) return cached2;
42707
+ if (Date.now() - this.lastFetchMs < REFRESH_THROTTLE_MS) return null;
42708
+ await this.refresh();
42709
+ return this.keys.get(kid) ?? null;
42710
+ }
42711
+ async refresh() {
42712
+ if (this.inflight) return this.inflight;
42713
+ this.inflight = (async () => {
42714
+ try {
42715
+ const jwks = await this.loadKeys();
42716
+ const next = /* @__PURE__ */ new Map();
42717
+ for (const jwk of jwks) {
42718
+ if (jwk.kty !== "RSA" || !jwk.kid || !jwk.n || !jwk.e) continue;
42719
+ try {
42720
+ next.set(
42721
+ jwk.kid,
42722
+ (0, import_crypto3.createPublicKey)({ key: jwk, format: "jwk" })
42723
+ );
42724
+ } catch {
42725
+ }
42726
+ }
42727
+ this.keys = next;
42728
+ this.lastFetchMs = Date.now();
42729
+ } catch {
42730
+ this.lastFetchMs = Date.now();
42731
+ } finally {
42732
+ this.inflight = null;
42733
+ }
42734
+ })();
42735
+ return this.inflight;
42506
42736
  }
42507
- const subgraph = bfs(
42508
- adj,
42509
- startNodes.map((s) => s.id),
42510
- BFS_DEPTH,
42511
- RELATION_FILTER,
42512
- CONFIDENCE_MIN
42513
- );
42514
- const affected_files = extractAffectedFiles(subgraph, nodeIndex, MAX_AFFECTED_FILES);
42515
- const god_nodes = identifyGodNodes(
42516
- subgraph,
42517
- adj,
42518
- nodeIndex,
42519
- GOD_NODE_DEGREE_THRESHOLD,
42520
- MAX_GOD_NODES
42521
- );
42522
- const affected_communities = identifyAffectedCommunities(subgraph, nodeIndex);
42523
- const staleness = computeStaleness(graphPath);
42524
- return {
42525
- affected_files,
42526
- god_nodes,
42527
- affected_communities,
42528
- keywords_used: keywords,
42529
- ...staleness
42530
- };
42737
+ };
42738
+ function audienceMatches(claims, expected) {
42739
+ const aud = claims.aud;
42740
+ if (typeof aud === "string" && aud === expected) return true;
42741
+ if (Array.isArray(aud) && aud.includes(expected)) return true;
42742
+ return claims.azp === expected;
42531
42743
  }
42532
- async function findGraphPath(projectId, svc) {
42533
- const workspaces = await svc.findWorkspaces(projectId);
42534
- for (const ws of workspaces) {
42535
- const p = path5.join(ws.cwd, "graphify-out", "graph.json");
42536
- if (fs3.existsSync(p)) return p;
42744
+ function verifySignature(signingInput, signatureB64, key) {
42745
+ try {
42746
+ const verifier = (0, import_crypto3.createVerify)("RSA-SHA256");
42747
+ verifier.update(signingInput);
42748
+ verifier.end();
42749
+ return verifier.verify(key, base64UrlToBuffer(signatureB64));
42750
+ } catch {
42751
+ return false;
42537
42752
  }
42538
- const project = await svc.getProject(projectId);
42539
- if (project) {
42540
- const p = path5.join(project.cwd, "graphify-out", "graph.json");
42541
- if (fs3.existsSync(p)) return p;
42753
+ }
42754
+ function decodeJson(segment) {
42755
+ try {
42756
+ return JSON.parse(base64UrlToBuffer(segment).toString("utf8"));
42757
+ } catch {
42758
+ return null;
42542
42759
  }
42543
- return null;
42544
42760
  }
42545
- function extractKeywords(task) {
42546
- const acSection = extractAcceptanceSection(task.body ?? "");
42547
- const raw = `${task.title} ${acSection}`.split(/\s+/);
42548
- const fromLabels = (task.labels ?? []).map((l) => l.toLowerCase()).filter((l) => !LABEL_KEY_PREFIX.test(l) && !LABEL_EXACT_DROP.has(l));
42549
- const all = [...raw.map((t) => t.toLowerCase()), ...fromLabels];
42550
- const deduped = /* @__PURE__ */ new Set();
42551
- for (const t of all) {
42552
- const cleaned = cleanToken(t);
42553
- if (cleaned.length <= 3) continue;
42554
- if (KEYWORD_STOPWORDS.has(cleaned)) continue;
42555
- deduped.add(cleaned);
42556
- }
42557
- return Array.from(deduped);
42558
- }
42559
- function extractFilePointers(body) {
42560
- const match = body.match(/(?:^|\n)##\s*File Pointers\s*\n[\s\S]*?(?=\n##\s|$)/i);
42561
- if (!match) return [];
42562
- const paths = [];
42563
- for (const line of splitLines(match[0])) {
42564
- if (/\(NEW\)/i.test(line)) continue;
42565
- const pathMatch = line.match(/^\s*-\s+`([^`]+)`/);
42566
- if (!pathMatch) continue;
42567
- paths.push(pathMatch[1].replace(/\\/g, "/"));
42568
- }
42569
- return paths;
42570
- }
42571
- function cleanToken(t) {
42572
- return t.replace(/^[^a-z0-9_-]+|[^a-z0-9_-]+$/gi, "");
42573
- }
42574
- function extractAcceptanceSection(body) {
42575
- const match = body.match(/(?:^|\n)##\s*Acceptance\s*\n[\s\S]*?(?=\n##\s|$)/i);
42576
- if (!match) return "";
42577
- return match[0].slice(0, 500);
42578
- }
42579
- function findStartNodes(nodes, keywords, filePointerPaths, topK) {
42580
- if (filePointerPaths.length > 0) {
42581
- const fileMatches = findStartNodesByFile(nodes, filePointerPaths);
42582
- if (fileMatches.length > 0) return fileMatches;
42583
- }
42584
- return findStartNodesByKeyword(nodes, keywords, topK);
42585
- }
42586
- function findStartNodesByFile(nodes, paths) {
42587
- const targets = new Set(paths.map((p) => p.replace(/\\/g, "/")));
42588
- const matches = [];
42589
- for (const n of nodes) {
42590
- if (!n.source_file) continue;
42591
- const normalized = n.source_file.replace(/\\/g, "/");
42592
- if (targets.has(normalized)) {
42593
- matches.push({ id: n.id, score: 100 });
42594
- }
42595
- }
42596
- matches.sort((a, b) => {
42597
- if (a.id < b.id) return -1;
42598
- if (a.id > b.id) return 1;
42599
- return 0;
42600
- });
42601
- return matches;
42602
- }
42603
- function findStartNodesByKeyword(nodes, keywords, topK) {
42604
- const scored = [];
42605
- for (const n of nodes) {
42606
- const label = (n.label ?? "").toLowerCase();
42607
- let score = 0;
42608
- for (const k of keywords) {
42609
- if (label.includes(k)) score += 1;
42610
- }
42611
- if (score > 0) scored.push({ id: n.id, score });
42612
- }
42613
- scored.sort((a, b) => {
42614
- if (b.score !== a.score) return b.score - a.score;
42615
- if (b.id > a.id) return 1;
42616
- if (b.id < a.id) return -1;
42617
- return 0;
42618
- });
42619
- return scored.slice(0, topK);
42620
- }
42621
- function buildAdjacency(links) {
42622
- const adj = /* @__PURE__ */ new Map();
42623
- const push = (from, to, link) => {
42624
- if (!adj.has(from)) adj.set(from, []);
42625
- adj.get(from).push({ neighbor: to, link });
42626
- };
42627
- for (const l of links) {
42628
- push(l.source, l.target, l);
42629
- push(l.target, l.source, l);
42630
- }
42631
- return adj;
42632
- }
42633
- function bfs(adj, startNodes, depth, relationFilter, confidenceMin) {
42634
- const visited = new Set(startNodes);
42635
- let frontier = new Set(startNodes);
42636
- for (let d = 0; d < depth; d += 1) {
42637
- const next = /* @__PURE__ */ new Set();
42638
- for (const n of frontier) {
42639
- const neighbors = adj.get(n) ?? [];
42640
- for (const { neighbor, link } of neighbors) {
42641
- if (link.relation && !relationFilter.has(link.relation)) continue;
42642
- if ((link.confidence_score ?? 1) < confidenceMin) continue;
42643
- if (!visited.has(neighbor)) next.add(neighbor);
42644
- }
42645
- }
42646
- for (const n of next) visited.add(n);
42647
- frontier = next;
42648
- }
42649
- return visited;
42650
- }
42651
- function extractAffectedFiles(subgraph, nodeIndex, max) {
42652
- const hits = /* @__PURE__ */ new Map();
42653
- for (const id of subgraph) {
42654
- const n = nodeIndex.get(id);
42655
- if (!n?.source_file) continue;
42656
- hits.set(n.source_file, (hits.get(n.source_file) ?? 0) + 1);
42657
- }
42658
- return Array.from(hits.entries()).map(([p, h]) => ({ path: p, hits: h })).sort((a, b) => b.hits - a.hits).slice(0, max);
42659
- }
42660
- function identifyGodNodes(subgraph, adj, nodeIndex, threshold, max) {
42661
- const result = [];
42662
- for (const id of subgraph) {
42663
- const deg = (adj.get(id) ?? []).length;
42664
- if (deg >= threshold) {
42665
- result.push({ id, label: nodeIndex.get(id)?.label ?? id, degree: deg });
42666
- }
42667
- }
42668
- return result.sort((a, b) => b.degree - a.degree).slice(0, max);
42669
- }
42670
- function identifyAffectedCommunities(subgraph, nodeIndex) {
42671
- const counts = /* @__PURE__ */ new Map();
42672
- for (const id of subgraph) {
42673
- const n = nodeIndex.get(id);
42674
- if (typeof n?.community !== "number") continue;
42675
- counts.set(n.community, (counts.get(n.community) ?? 0) + 1);
42676
- }
42677
- return Array.from(counts.entries()).map(([cid, count]) => ({ id: cid, label: null, nodeCount: count })).sort((a, b) => b.nodeCount - a.nodeCount);
42678
- }
42679
- function computeStaleness(graphPath) {
42680
- const stat = fs3.statSync(graphPath);
42681
- const mtimeMs = stat.mtimeMs;
42682
- const ageMs = Date.now() - mtimeMs;
42683
- const ageDays = ageMs / (1e3 * 60 * 60 * 24);
42684
- return {
42685
- graph_mtime_iso: new Date(mtimeMs).toISOString(),
42686
- graph_age_days: Math.round(ageDays * 10) / 10,
42687
- graph_is_stale: ageDays > STALE_DAYS
42761
+ function base64UrlToBuffer(input) {
42762
+ const padded = input.replace(/-/g, "+").replace(/_/g, "/");
42763
+ return Buffer.from(padded, "base64");
42764
+ }
42765
+ function defaultJwksLoader(jwksUri) {
42766
+ return async () => {
42767
+ const res = await fetch(jwksUri, { headers: { accept: "application/json" } });
42768
+ if (!res.ok) throw new Error(`JWKS fetch ${res.status}`);
42769
+ const body = await res.json();
42770
+ return body.keys ?? [];
42688
42771
  };
42689
42772
  }
42690
42773
 
42774
+ // src/adapters/mcp/mcp-tools/types.ts
42775
+ function textResponse(payload) {
42776
+ const text = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
42777
+ return { content: [{ type: "text", text }] };
42778
+ }
42779
+
42691
42780
  // src/adapters/mcp/mcp-tools/task-tools.ts
42692
42781
  function defaultBody(id, title) {
42693
42782
  return `# ${id}: ${title}
@@ -42738,7 +42827,6 @@ var register = (server, svc) => {
42738
42827
  }))
42739
42828
  }))
42740
42829
  );
42741
- const graphify_context = await buildGraphifyContext(task, svc);
42742
42830
  return textResponse({
42743
42831
  task,
42744
42832
  dependencies: deps,
@@ -42746,8 +42834,7 @@ var register = (server, svc) => {
42746
42834
  tags,
42747
42835
  relationships: rels,
42748
42836
  conversations,
42749
- body: task.body,
42750
- graphify_context
42837
+ body: task.body
42751
42838
  });
42752
42839
  }
42753
42840
  );
@@ -42869,13 +42956,13 @@ var EMPTY_RULES = {
42869
42956
  sessionEnd: "",
42870
42957
  conversationRead: ""
42871
42958
  };
42872
- function loadMcpRules(path10 = rulesPath()) {
42873
- if (!(0, import_fs.existsSync)(path10)) {
42874
- console.warn(`[mcp-rules] file not found at ${path10} \u2014 returning empty rules`);
42959
+ function loadMcpRules(path9 = rulesPath()) {
42960
+ if (!(0, import_fs.existsSync)(path9)) {
42961
+ console.warn(`[mcp-rules] file not found at ${path9} \u2014 returning empty rules`);
42875
42962
  return { ...EMPTY_RULES };
42876
42963
  }
42877
42964
  try {
42878
- const content = (0, import_fs.readFileSync)(path10, "utf-8");
42965
+ const content = (0, import_fs.readFileSync)(path9, "utf-8");
42879
42966
  const sessionStart = parseSection(content, "On session_start");
42880
42967
  const sessionCheckpoint = parseSection(content, "On session_checkpoint");
42881
42968
  const sessionResume = parseSection(content, "On session_resume");
@@ -42888,11 +42975,11 @@ function loadMcpRules(path10 = rulesPath()) {
42888
42975
  ["On session_end", sessionEnd],
42889
42976
  ["On conversation_read", conversationRead]
42890
42977
  ]) {
42891
- if (!value) console.warn(`[mcp-rules] "## ${name}" section missing in ${path10}`);
42978
+ if (!value) console.warn(`[mcp-rules] "## ${name}" section missing in ${path9}`);
42892
42979
  }
42893
42980
  return { sessionStart, sessionCheckpoint, sessionResume, sessionEnd, conversationRead };
42894
42981
  } catch (err) {
42895
- console.error(`[mcp-rules] failed to read ${path10}:`, err);
42982
+ console.error(`[mcp-rules] failed to read ${path9}:`, err);
42896
42983
  return { ...EMPTY_RULES };
42897
42984
  }
42898
42985
  }
@@ -43121,8 +43208,8 @@ var register2 = (server, svc) => {
43121
43208
  };
43122
43209
 
43123
43210
  // src/adapters/mcp/mcp-tools/project-context-builder.ts
43124
- var fs4 = __toESM(require("fs"));
43125
- var path6 = __toESM(require("path"));
43211
+ var fs3 = __toESM(require("fs"));
43212
+ var path5 = __toESM(require("path"));
43126
43213
 
43127
43214
  // src/core/domain/inbox-triage-policy.ts
43128
43215
  var STALE_RAW_DAYS = 3;
@@ -43225,10 +43312,10 @@ function loadRecentDecisions(sources, contentRoot, depth) {
43225
43312
  }).filter((d) => d.excerpt.length > 0);
43226
43313
  }
43227
43314
  function readMarkdown(contentRoot, sourcePath, depth) {
43228
- const absolute = path6.isAbsolute(sourcePath) ? sourcePath : path6.join(contentRoot, sourcePath);
43229
- if (!fs4.existsSync(absolute)) return null;
43315
+ const absolute = path5.isAbsolute(sourcePath) ? sourcePath : path5.join(contentRoot, sourcePath);
43316
+ if (!fs3.existsSync(absolute)) return null;
43230
43317
  try {
43231
- const raw = fs4.readFileSync(absolute, "utf-8");
43318
+ const raw = fs3.readFileSync(absolute, "utf-8");
43232
43319
  const stripped = stripFrontmatter(raw);
43233
43320
  return depth === "summary" ? summarize(stripped) : stripped;
43234
43321
  } catch {
@@ -43299,7 +43386,7 @@ var register3 = (server, svc) => {
43299
43386
  };
43300
43387
 
43301
43388
  // src/adapters/mcp/mcp-tools/workspace-tools.ts
43302
- var fs5 = __toESM(require("node:fs"));
43389
+ var fs4 = __toESM(require("node:fs"));
43303
43390
  var register4 = (server, svc) => {
43304
43391
  server.registerTool(
43305
43392
  "workspace_list",
@@ -43337,7 +43424,7 @@ var register4 = (server, svc) => {
43337
43424
  );
43338
43425
  }
43339
43426
  const ws = await svc.addWorkspace(projectId, id, label, cwd);
43340
- const cwdExists = fs5.existsSync(cwd);
43427
+ const cwdExists = fs4.existsSync(cwd);
43341
43428
  return textResponse({ workspace: ws, warning: cwdExists ? null : `cwd ${cwd} not found on disk` });
43342
43429
  }
43343
43430
  );
@@ -43373,18 +43460,18 @@ async function archiveOrError(svc, projectId, workspaceId) {
43373
43460
  }
43374
43461
 
43375
43462
  // src/adapters/mcp/mcp-tools/workspace-resolver.ts
43376
- var path7 = __toESM(require("path"));
43463
+ var path6 = __toESM(require("path"));
43377
43464
  var isWindows = process.platform === "win32";
43378
43465
  function normalize2(p) {
43379
- const resolved = path7.resolve(p).replace(/[\\/]+$/, "");
43466
+ const resolved = path6.resolve(p).replace(/[\\/]+$/, "");
43380
43467
  return isWindows ? resolved.toLowerCase().replace(/\//g, "\\") : resolved;
43381
43468
  }
43382
43469
  function isDescendantOrEqual(parent, child) {
43383
43470
  if (parent === child) return true;
43384
- const rel = path7.relative(parent, child);
43471
+ const rel = path6.relative(parent, child);
43385
43472
  if (rel === "") return true;
43386
43473
  if (rel.startsWith("..")) return false;
43387
- return !path7.isAbsolute(rel);
43474
+ return !path6.isAbsolute(rel);
43388
43475
  }
43389
43476
  function resolveWorkspaceId(input) {
43390
43477
  const { explicitWorkspaceId, cwd, workspaces } = input;
@@ -43512,6 +43599,72 @@ function collectFilesByCommit(cwd, commitLines, git) {
43512
43599
  return map2;
43513
43600
  }
43514
43601
 
43602
+ // src/core/domain/session-transcript.ts
43603
+ var fs5 = __toESM(require("fs"));
43604
+ var path7 = __toESM(require("path"));
43605
+ var os = __toESM(require("os"));
43606
+ function isTextBlock(b) {
43607
+ return typeof b === "object" && b !== null && b.type === "text" && typeof b.text === "string";
43608
+ }
43609
+ function cwdToProjectSlug(cwd) {
43610
+ return cwd.replace(/[^a-zA-Z0-9]/g, "-");
43611
+ }
43612
+ function parseTranscript(content) {
43613
+ const rows = [];
43614
+ for (const line of splitLines(content)) {
43615
+ if (!line.trim()) continue;
43616
+ try {
43617
+ rows.push(JSON.parse(line));
43618
+ } catch {
43619
+ }
43620
+ }
43621
+ return rows;
43622
+ }
43623
+ function extractResumePoint(rows) {
43624
+ for (let i = rows.length - 1; i >= 0; i--) {
43625
+ const r = rows[i];
43626
+ if (r.type !== "assistant") continue;
43627
+ const content = r.message?.content;
43628
+ if (!Array.isArray(content)) continue;
43629
+ const text = content.filter(isTextBlock).map((b) => b.text).join("\n").trim();
43630
+ if (text) return text;
43631
+ }
43632
+ return null;
43633
+ }
43634
+ var TranscriptOpsImpl = class {
43635
+ readResumePoint(opts) {
43636
+ try {
43637
+ const base = path7.join(
43638
+ opts.homeDir ?? os.homedir(),
43639
+ ".claude",
43640
+ "projects",
43641
+ cwdToProjectSlug(opts.cwd)
43642
+ );
43643
+ if (!fs5.existsSync(base)) return null;
43644
+ if (opts.ccSessionId) {
43645
+ const file2 = path7.join(base, `${opts.ccSessionId}.jsonl`);
43646
+ if (fs5.existsSync(file2)) {
43647
+ return extractResumePoint(parseTranscript(fs5.readFileSync(file2, "utf8")));
43648
+ }
43649
+ }
43650
+ const sinceMs = Date.parse(opts.startedAt);
43651
+ const candidates = fs5.readdirSync(base).filter((f) => f.endsWith(".jsonl")).map((f) => {
43652
+ const full = path7.join(base, f);
43653
+ let mtimeMs = 0;
43654
+ try {
43655
+ mtimeMs = fs5.statSync(full).mtimeMs;
43656
+ } catch {
43657
+ }
43658
+ return { full, mtimeMs };
43659
+ }).filter((c) => c.mtimeMs > 0 && (Number.isNaN(sinceMs) || c.mtimeMs >= sinceMs)).sort((a, b) => b.mtimeMs - a.mtimeMs);
43660
+ if (candidates.length === 0) return null;
43661
+ return extractResumePoint(parseTranscript(fs5.readFileSync(candidates[0].full, "utf8")));
43662
+ } catch {
43663
+ return null;
43664
+ }
43665
+ }
43666
+ };
43667
+
43515
43668
  // src/adapters/mcp/mcp-tools/session-tools.ts
43516
43669
  var sessionSummarySchema = external_exports3.object({
43517
43670
  summary: external_exports3.string(),
@@ -43544,9 +43697,13 @@ var sessionSummarySchema = external_exports3.object({
43544
43697
  });
43545
43698
  var handoffInputSchema = {
43546
43699
  sessionId: external_exports3.string(),
43547
- commits: external_exports3.array(external_exports3.string()).optional(),
43700
+ commits: external_exports3.array(external_exports3.string()).optional().describe(
43701
+ 'Format: "<short-sha> <subject>". Optional (ADR-031) \u2014 when omitted, the server derives commits from the session time window (filtered by the bound task id). Any value you provide wins; derivation only fills the gap.'
43702
+ ),
43548
43703
  decisions: external_exports3.array(external_exports3.string()).optional(),
43549
- resumePoint: external_exports3.string(),
43704
+ resumePoint: external_exports3.string().optional().describe(
43705
+ "One sentence: where you stopped + what to pick up next. Optional (ADR-031) \u2014 when omitted, the server derives it from the last text-bearing assistant turn in the session transcript. Best-effort; any value you provide wins."
43706
+ ),
43550
43707
  looseEnds: external_exports3.array(external_exports3.string()).optional(),
43551
43708
  notes: external_exports3.string().optional(),
43552
43709
  testResults: external_exports3.object({
@@ -43567,7 +43724,7 @@ async function tryLifecycle2(fn) {
43567
43724
  throw e;
43568
43725
  }
43569
43726
  }
43570
- var register5 = (server, svc, git = new GitOpsImpl()) => {
43727
+ var register5 = (server, svc, git = new GitOpsImpl(), transcript = new TranscriptOpsImpl()) => {
43571
43728
  server.registerTool(
43572
43729
  "session_start",
43573
43730
  {
@@ -43578,10 +43735,13 @@ var register5 = (server, svc, git = new GitOpsImpl()) => {
43578
43735
  workspaceId: external_exports3.string().optional().describe("Workspace ID (e.g. workflow-engine)"),
43579
43736
  cwd: external_exports3.string().optional().describe(
43580
43737
  "Current working directory \u2014 used to auto-detect workspaceId when not passed explicitly"
43738
+ ),
43739
+ ccSessionId: external_exports3.string().optional().describe(
43740
+ "Claude Code session UUID (the transcript .jsonl filename under ~/.claude/projects/). Pass it so session_end can derive resumePoint from the transcript (ADR-031). Optional \u2014 omitted falls back to heuristic correlation."
43581
43741
  )
43582
43742
  }
43583
43743
  },
43584
- async ({ projectId, taskId, workspaceId, cwd }) => tryLifecycle2(async () => {
43744
+ async ({ projectId, taskId, workspaceId, cwd, ccSessionId }) => tryLifecycle2(async () => {
43585
43745
  const project = await svc.getProject(projectId);
43586
43746
  if (!project) throw new Error(`Project ${projectId} not found`);
43587
43747
  const resolvedWorkspaceId = resolveWorkspaceId({
@@ -43592,7 +43752,8 @@ var register5 = (server, svc, git = new GitOpsImpl()) => {
43592
43752
  const { session, contextSources, existingActiveSessions, recalledMemories } = await svc.startSession({
43593
43753
  projectId,
43594
43754
  taskId,
43595
- workspaceId: resolvedWorkspaceId
43755
+ workspaceId: resolvedWorkspaceId,
43756
+ ccSessionId
43596
43757
  });
43597
43758
  const lastSession = await loadLastSession(svc, projectId, resolvedWorkspaceId);
43598
43759
  const bundle = await buildProjectContext(svc, projectId, "summary");
@@ -43706,10 +43867,12 @@ var register5 = (server, svc, git = new GitOpsImpl()) => {
43706
43867
  inputSchema: handoffInputSchema
43707
43868
  },
43708
43869
  async (input) => tryLifecycle2(async () => {
43870
+ const commits = await resolveCommits(svc, git, input.sessionId, input.commits);
43871
+ const resumePoint = await resolveResumePoint(svc, transcript, input.sessionId, input.resumePoint);
43709
43872
  const handoff = {
43710
- commits: input.commits,
43873
+ commits,
43711
43874
  decisions: input.decisions,
43712
- resumePoint: input.resumePoint,
43875
+ resumePoint,
43713
43876
  looseEnds: input.looseEnds,
43714
43877
  tasksUpdated: [],
43715
43878
  testResults: input.testResults
@@ -43732,7 +43895,58 @@ var register5 = (server, svc, git = new GitOpsImpl()) => {
43732
43895
  };
43733
43896
  })
43734
43897
  );
43898
+ server.registerTool(
43899
+ "session_cancel",
43900
+ {
43901
+ description: "Cancel (retire) an active session WITHOUT marking its task DONE. Use for an empty or abandoned session \u2014 an orphaned session_start where no real work followed, or a session you want to drop without completing the task. Marks the session completed with a failureReason, closes any linked conversations, and intentionally leaves the bound task status untouched (stays IN-PROGRESS for human review). Distinct from session_end, which marks the task DONE.",
43902
+ inputSchema: {
43903
+ sessionId: external_exports3.string(),
43904
+ reason: external_exports3.string().optional().describe(
43905
+ 'Why the session is being cancelled (e.g. "empty session \u2014 no work recorded"). Defaults to "cancelled \u2014 no work recorded".'
43906
+ )
43907
+ }
43908
+ },
43909
+ async ({ sessionId, reason }) => tryLifecycle2(async () => {
43910
+ const result = await svc.abandonSession(
43911
+ sessionId,
43912
+ reason ?? "cancelled \u2014 no work recorded"
43913
+ );
43914
+ return {
43915
+ sessionId: result.session.id,
43916
+ status: result.session.status,
43917
+ endedAt: result.session.endedAt,
43918
+ taskId: result.session.taskId,
43919
+ taskUntouched: true,
43920
+ closedConversationIds: result.closedConversationIds
43921
+ };
43922
+ })
43923
+ );
43735
43924
  };
43925
+ async function resolveCommits(svc, git, sessionId, provided) {
43926
+ if (provided && provided.length > 0) return provided;
43927
+ const session = await svc.getSession(sessionId);
43928
+ if (!session) return provided;
43929
+ const project = await svc.getProject(session.projectId);
43930
+ const cwd = project?.cwd;
43931
+ if (!cwd) return provided;
43932
+ const derived = git.commitsInWindow(cwd, session.startedAt, session.taskId ?? void 0);
43933
+ return derived.length > 0 ? derived : provided;
43934
+ }
43935
+ async function resolveResumePoint(svc, transcript, sessionId, provided) {
43936
+ if (provided && provided.trim()) return provided;
43937
+ const session = await svc.getSession(sessionId);
43938
+ if (!session) return provided;
43939
+ const project = await svc.getProject(session.projectId);
43940
+ const cwd = project?.cwd;
43941
+ if (!cwd) return provided;
43942
+ const derived = transcript.readResumePoint({
43943
+ cwd,
43944
+ ccSessionId: session.ccSessionId,
43945
+ startedAt: session.startedAt,
43946
+ endedAt: session.endedAt
43947
+ });
43948
+ return derived ?? provided;
43949
+ }
43736
43950
  async function buildSuggestedKnowledge(svc, git, projectId, handoff) {
43737
43951
  const project = await svc.getProject(projectId);
43738
43952
  const cwd = project?.cwd ?? "";
@@ -43793,7 +44007,8 @@ async function createLooseEndInboxes(svc, looseEnds, session) {
43793
44007
  projectId: session.projectId,
43794
44008
  content: `${content}
43795
44009
 
43796
- \u2014 from session ${tag}`
44010
+ \u2014 from session ${tag}`,
44011
+ ...session.taskId ? { linkedTaskId: session.taskId } : {}
43797
44012
  });
43798
44013
  ids.push(item.id);
43799
44014
  }
@@ -43816,10 +44031,13 @@ var register6 = (server, svc) => {
43816
44031
  description: "Add a raw idea to inbox. Returns INBOX-NNN with status=raw.",
43817
44032
  inputSchema: {
43818
44033
  projectId: external_exports3.string().describe("Project ID (required)"),
43819
- content: external_exports3.string().describe("Raw idea content")
44034
+ content: external_exports3.string().describe("Raw idea content"),
44035
+ workspaceId: external_exports3.string().optional().describe(
44036
+ "App/workspace scope, if already clear. Omit to leave domain-level (null) \u2014 research fills it later via inbox_ready."
44037
+ )
43820
44038
  }
43821
44039
  },
43822
- async ({ projectId, content }) => textResponse(await svc.createInbox({ projectId, content }))
44040
+ async ({ projectId, content, workspaceId }) => textResponse(await svc.createInbox({ projectId, content, workspaceId }))
43823
44041
  );
43824
44042
  server.registerTool(
43825
44043
  "inbox_update",
@@ -43903,16 +44121,24 @@ var register6 = (server, svc) => {
43903
44121
  server.registerTool(
43904
44122
  "inbox_ready",
43905
44123
  {
43906
- description: "Mark research complete. Transitions researching \u2192 ready.",
43907
- inputSchema: { id: external_exports3.string().describe("Inbox item ID") }
44124
+ description: "Mark research complete. Transitions researching \u2192 ready. Pass workspaceId to localize the item to the app research identified (ADR-032 Pillar 6).",
44125
+ inputSchema: {
44126
+ id: external_exports3.string().describe("Inbox item ID"),
44127
+ workspaceId: external_exports3.string().optional().describe("App/workspace identified during research \u2014 localizes the item before convert.")
44128
+ }
43908
44129
  },
43909
- async ({ id }) => {
44130
+ async ({ id, workspaceId }) => {
43910
44131
  const item = await svc.getInbox(id);
43911
44132
  if (!item) return textResponse(`Inbox ${id} not found`);
43912
44133
  if (item.status !== "researching") {
43913
44134
  return textResponse(`Inbox ${id} is ${item.status}, not researching`);
43914
44135
  }
43915
- return textResponse(await svc.updateInbox(id, { status: "ready" }));
44136
+ return textResponse(
44137
+ await svc.updateInbox(
44138
+ id,
44139
+ workspaceId !== void 0 ? { status: "ready", workspaceId } : { status: "ready" }
44140
+ )
44141
+ );
43916
44142
  }
43917
44143
  );
43918
44144
  server.registerTool(
@@ -43942,6 +44168,90 @@ var register6 = (server, svc) => {
43942
44168
  );
43943
44169
  };
43944
44170
 
44171
+ // src/adapters/mcp/mcp-tools/investigation-tools.ts
44172
+ async function tryLifecycle4(fn) {
44173
+ try {
44174
+ return textResponse(await fn());
44175
+ } catch (e) {
44176
+ if (e instanceof LifecycleError) return textResponse(e.message);
44177
+ throw e;
44178
+ }
44179
+ }
44180
+ var register7 = (server, svc) => {
44181
+ server.registerTool(
44182
+ "investigation_start",
44183
+ {
44184
+ description: "Start a debugging investigation (ADR-035). Creates a container for nonlinear trace state (hypotheses + evidence), status=exploring. Optionally bind to a task/session.",
44185
+ inputSchema: {
44186
+ symptom: external_exports3.string().describe('The observed symptom, e.g. "Test button does not work"'),
44187
+ taskId: external_exports3.string().optional().describe("Optional task to bind to (standalone allowed)"),
44188
+ sessionId: external_exports3.string().optional().describe("Optional session to bind to")
44189
+ }
44190
+ },
44191
+ async ({ symptom, taskId, sessionId }) => tryLifecycle4(() => svc.startInvestigation({ symptom, taskId, sessionId }))
44192
+ );
44193
+ server.registerTool(
44194
+ "investigation_add_hypothesis",
44195
+ {
44196
+ description: "Add a hypothesis to an investigation (status=testing). Blocked once the investigation is resolved.",
44197
+ inputSchema: {
44198
+ investigationId: external_exports3.string().describe("Investigation ID (e.g. INV-001)"),
44199
+ description: external_exports3.string().describe("What you think might be the cause")
44200
+ }
44201
+ },
44202
+ async ({ investigationId, description }) => tryLifecycle4(() => svc.addHypothesis(investigationId, description))
44203
+ );
44204
+ server.registerTool(
44205
+ "investigation_set_hypothesis_status",
44206
+ {
44207
+ description: "Transition a hypothesis: testing \u2192 ruled_out | confirmed. Ruled-out hypotheses persist (dead ends are kept). A terminal hypothesis cannot transition again.",
44208
+ inputSchema: {
44209
+ hypothesisId: external_exports3.string().describe("Hypothesis ID (e.g. HYP-001)"),
44210
+ status: external_exports3.enum(["ruled_out", "confirmed"]).describe("New status")
44211
+ }
44212
+ },
44213
+ async ({ hypothesisId, status }) => tryLifecycle4(() => svc.setHypothesisStatus(hypothesisId, status))
44214
+ );
44215
+ server.registerTool(
44216
+ "investigation_add_evidence",
44217
+ {
44218
+ description: "Attach typed evidence to an investigation, or to a specific hypothesis within it.",
44219
+ inputSchema: {
44220
+ investigationId: external_exports3.string().describe("Investigation ID"),
44221
+ type: external_exports3.enum(["screenshot", "log", "network", "code_snippet"]).describe("Evidence type"),
44222
+ ref: external_exports3.string().describe("Path / URL / locator for the evidence"),
44223
+ hypothesisId: external_exports3.string().optional().describe("Attach to this hypothesis instead of the investigation as a whole"),
44224
+ note: external_exports3.string().optional().describe("Optional note about the evidence")
44225
+ }
44226
+ },
44227
+ async ({ investigationId, type, ref, hypothesisId, note }) => tryLifecycle4(() => svc.addEvidence({ investigationId, type, ref, hypothesisId, note }))
44228
+ );
44229
+ server.registerTool(
44230
+ "investigation_resolve",
44231
+ {
44232
+ description: "Resolve an investigation (status=resolved). Returns a human-gated knowledge_create(gotcha) draft built from pattern_tag + root_cause + fix \u2014 NOT auto-written. Errors if already resolved.",
44233
+ inputSchema: {
44234
+ id: external_exports3.string().describe("Investigation ID"),
44235
+ rootCause: external_exports3.string().describe("The confirmed root cause"),
44236
+ fixSummary: external_exports3.string().describe("How it was fixed"),
44237
+ patternTag: external_exports3.string().optional().describe('Reusable pattern tag, e.g. "expression-no-upstream-data"')
44238
+ }
44239
+ },
44240
+ async ({ id, rootCause, fixSummary, patternTag }) => tryLifecycle4(() => svc.resolveInvestigation(id, { rootCause, fixSummary, patternTag }))
44241
+ );
44242
+ server.registerTool(
44243
+ "investigation_get",
44244
+ {
44245
+ description: "Get full investigation state \u2014 symptom, status, all hypotheses (incl. ruled_out), all evidence, root_cause/fix. Reconstructs a trace without prior context.",
44246
+ inputSchema: { id: external_exports3.string().describe("Investigation ID") }
44247
+ },
44248
+ async ({ id }) => {
44249
+ const inv = await svc.getInvestigation(id);
44250
+ return textResponse(inv ?? `Investigation ${id} not found`);
44251
+ }
44252
+ );
44253
+ };
44254
+
43945
44255
  // src/adapters/mcp/mcp-tools/backup-tools.ts
43946
44256
  var import_fs3 = require("fs");
43947
44257
  var import_path3 = require("path");
@@ -44004,7 +44314,7 @@ function runBackup(db, userData, now2 = /* @__PURE__ */ new Date()) {
44004
44314
  }
44005
44315
 
44006
44316
  // src/adapters/mcp/mcp-tools/backup-tools.ts
44007
- function register7(server, svc, dataDir, dbPath) {
44317
+ function register8(server, svc, dataDir, dbPath) {
44008
44318
  server.registerTool(
44009
44319
  "backup_list",
44010
44320
  {
@@ -44051,13 +44361,24 @@ function register7(server, svc, dataDir, dbPath) {
44051
44361
  }
44052
44362
 
44053
44363
  // src/adapters/mcp/mcp-tools/knowledge-tools.ts
44054
- var TYPE_ENUM = ["spike", "decision", "postmortem", "learning", "evaluation"];
44364
+ var TYPE_ENUM = [
44365
+ "spike",
44366
+ "decision",
44367
+ "postmortem",
44368
+ "learning",
44369
+ "evaluation",
44370
+ "feature",
44371
+ "code_ref",
44372
+ "gotcha"
44373
+ ];
44055
44374
  var SCOPE_ENUM = ["project", "cross"];
44056
- var register8 = (server, svc) => {
44375
+ var EFFORT_BAND_ENUM = ["S", "M", "L", "XL"];
44376
+ var FEATURE_STATUS_ENUM = ["planned", "in-progress", "shipped", "blocked"];
44377
+ var register9 = (server, svc) => {
44057
44378
  server.registerTool(
44058
44379
  "knowledge_create",
44059
44380
  {
44060
- description: "Create a knowledge entry (ADR, spike finding, postmortem, learning, evaluation). Project-scope writes to <projectCwd>/docs/knowledge/<slug>.md and tracks staleness against refs[]. When workspaceId is provided, the file is written to <workspaceCwd>/docs/knowledge/<slug>.md instead (workspace must belong to projectId). Cross-scope writes to vault/30-Knowledge/<slug>.md (no staleness).",
44381
+ description: "Create a knowledge entry. Original types (spike/decision/postmortem/learning/evaluation) capture findings/ADRs. First-class graph types (TASK-988): feature (a user-perceivable outcome \u2014 set structured.anchorTaskId/realizesTasks/inWorkspaces/effortBand/status), code_ref (a code anchor note \u2014 pair with the code_ref_upsert tool for the queryable identity row), gotcha (a concern \u2014 set structured.affectedFeatureId pointing at an existing feature slug). Project-scope writes to <projectCwd>/docs/knowledge/<slug>.md and tracks staleness against refs[]. When workspaceId is provided, the file is written to <workspaceCwd>/docs/knowledge/<slug>.md instead (workspace must belong to projectId). Cross-scope writes to vault/30-Knowledge/<slug>.md (no staleness).",
44061
44382
  inputSchema: {
44062
44383
  projectId: external_exports3.string().describe("Project ID"),
44063
44384
  workspaceId: external_exports3.string().optional().describe(
@@ -44073,11 +44394,19 @@ var register8 = (server, svc) => {
44073
44394
  commitSha: external_exports3.string().optional().describe("Pin SHA. Omit to auto-capture current HEAD.")
44074
44395
  })
44075
44396
  ).optional().describe("Code references for staleness tracking. Empty/omitted = no tracking."),
44076
- slug: external_exports3.string().optional().describe("Override auto-derived slug")
44397
+ slug: external_exports3.string().optional().describe("Override auto-derived slug"),
44398
+ structured: external_exports3.object({
44399
+ anchorTaskId: external_exports3.string().optional().describe("feature: epic-shape task it was promoted from"),
44400
+ realizesTasks: external_exports3.array(external_exports3.string()).optional().describe("feature: task ids that implement/modify it"),
44401
+ inWorkspaces: external_exports3.array(external_exports3.string()).optional().describe("feature: workspaces it touches"),
44402
+ effortBand: external_exports3.enum(EFFORT_BAND_ENUM).optional().describe("feature: S|M|L|XL band (NOT a day count)"),
44403
+ status: external_exports3.enum(FEATURE_STATUS_ENUM).optional().describe("feature: planned|in-progress|shipped|blocked"),
44404
+ affectedFeatureId: external_exports3.string().optional().describe("gotcha: slug of the feature this concern is about (must exist)")
44405
+ }).optional().describe("Structured fields for feature / gotcha first-class types (TASK-988).")
44077
44406
  }
44078
44407
  },
44079
- async ({ projectId, workspaceId, type, scope, title, body, refs, slug }) => textResponse(
44080
- svc.createKnowledge({
44408
+ async ({ projectId, workspaceId, type, scope, title, body, refs, slug, structured }) => textResponse(
44409
+ await svc.createKnowledge({
44081
44410
  projectId,
44082
44411
  workspaceId,
44083
44412
  type,
@@ -44085,7 +44414,8 @@ var register8 = (server, svc) => {
44085
44414
  title,
44086
44415
  body,
44087
44416
  refs: refs ?? [],
44088
- slug
44417
+ slug,
44418
+ structured
44089
44419
  })
44090
44420
  )
44091
44421
  );
@@ -44101,7 +44431,7 @@ var register8 = (server, svc) => {
44101
44431
  )
44102
44432
  }
44103
44433
  },
44104
- async ({ filePath, projectId, workspaceId }) => textResponse(svc.registerExistingKnowledge({ filePath, projectId, workspaceId }))
44434
+ async ({ filePath, projectId, workspaceId }) => textResponse(await svc.registerExistingKnowledge({ filePath, projectId, workspaceId }))
44105
44435
  );
44106
44436
  server.registerTool(
44107
44437
  "knowledge_get",
@@ -44112,7 +44442,7 @@ var register8 = (server, svc) => {
44112
44442
  }
44113
44443
  },
44114
44444
  async ({ slug }) => {
44115
- const entry = svc.getKnowledge(slug);
44445
+ const entry = await svc.getKnowledge(slug);
44116
44446
  if (!entry) return textResponse(`Knowledge ${slug} not found`);
44117
44447
  return textResponse(entry);
44118
44448
  }
@@ -44131,7 +44461,7 @@ var register8 = (server, svc) => {
44131
44461
  }
44132
44462
  },
44133
44463
  async ({ projectId, workspaceId, scope, type }) => textResponse(
44134
- svc.listKnowledge({
44464
+ await svc.listKnowledge({
44135
44465
  projectId,
44136
44466
  workspaceId: workspaceId === "" ? null : workspaceId,
44137
44467
  scope,
@@ -44154,7 +44484,7 @@ var register8 = (server, svc) => {
44154
44484
  ).optional().describe("Replace refs[]. Omit to re-pin existing refs to HEAD.")
44155
44485
  }
44156
44486
  },
44157
- async ({ slug, body, refs }) => textResponse(svc.updateKnowledge({ slug, body, refs }))
44487
+ async ({ slug, body, refs }) => textResponse(await svc.updateKnowledge({ slug, body, refs }))
44158
44488
  );
44159
44489
  server.registerTool(
44160
44490
  "knowledge_verify",
@@ -44164,7 +44494,7 @@ var register8 = (server, svc) => {
44164
44494
  slug: external_exports3.string().describe("Slug of the entry")
44165
44495
  }
44166
44496
  },
44167
- async ({ slug }) => textResponse(svc.verifyKnowledge(slug))
44497
+ async ({ slug }) => textResponse(await svc.verifyKnowledge(slug))
44168
44498
  );
44169
44499
  server.registerTool(
44170
44500
  "knowledge_delete",
@@ -44174,7 +44504,7 @@ var register8 = (server, svc) => {
44174
44504
  slug: external_exports3.string().describe("Slug of the entry to delete")
44175
44505
  }
44176
44506
  },
44177
- async ({ slug }) => textResponse(svc.deleteKnowledge(slug))
44507
+ async ({ slug }) => textResponse(await svc.deleteKnowledge(slug))
44178
44508
  );
44179
44509
  server.registerTool(
44180
44510
  "knowledge_search",
@@ -44189,6 +44519,472 @@ var register8 = (server, svc) => {
44189
44519
  );
44190
44520
  };
44191
44521
 
44522
+ // src/adapters/mcp/mcp-tools/code-ref-tools.ts
44523
+ var RELATION_ENUM = ["modifies", "reference"];
44524
+ var register10 = (server, svc) => {
44525
+ server.registerTool(
44526
+ "code_ref_upsert",
44527
+ {
44528
+ description: "Create or re-pin a code_ref identity row. Identity is (projectId, path, symbol) \u2014 symbol is NULL for file-level refs (.tsx/.md/migrations). A write matching an existing identity UPDATEs commit_sha / line_hint on the original slug instead of inserting a duplicate (ADR Pillar 2c). Returns the stored row.",
44529
+ inputSchema: {
44530
+ slug: external_exports3.string().describe("Slug for a NEW row (ignored when the identity already exists)"),
44531
+ projectId: external_exports3.string().describe("Project ID"),
44532
+ workspaceId: external_exports3.string().optional().describe("Workspace (app) the anchor lives in"),
44533
+ path: external_exports3.string().describe("Repo-relative file path"),
44534
+ symbol: external_exports3.string().optional().describe("Full dotted symbol (Namespace.Class.Method). Omit for file-level refs."),
44535
+ lineHint: external_exports3.number().int().optional().describe("Optional line hint \u2014 not trustworthy over time"),
44536
+ commitSha: external_exports3.string().optional().describe("Commit SHA captured at write time")
44537
+ }
44538
+ },
44539
+ async ({ slug, projectId, workspaceId, path: path9, symbol: symbol2, lineHint, commitSha }) => textResponse(
44540
+ await svc.upsertCodeRef({ slug, projectId, workspaceId, path: path9, symbol: symbol2, lineHint, commitSha })
44541
+ )
44542
+ );
44543
+ server.registerTool(
44544
+ "code_ref_prefix",
44545
+ {
44546
+ description: 'Query code_ref rows by dotted-symbol prefix (e.g. all Domain-layer anchors via symbolPrefix="Ichiba.Pim.TradingCatalog.Domain."), by exact path ("who anchors this file"), or list a whole project. Returns identity rows, not the .md notes.',
44547
+ inputSchema: {
44548
+ projectId: external_exports3.string().describe("Project ID"),
44549
+ symbolPrefix: external_exports3.string().optional().describe("Match symbols starting with this prefix"),
44550
+ path: external_exports3.string().optional().describe("Exact repo-relative path filter")
44551
+ }
44552
+ },
44553
+ async ({ projectId, symbolPrefix, path: path9 }) => textResponse(await svc.listCodeRefsByPrefix({ projectId, symbolPrefix, path: path9 }))
44554
+ );
44555
+ server.registerTool(
44556
+ "code_ref_delete",
44557
+ {
44558
+ description: "Delete a code_ref identity row by slug. Also removes its TOUCHES edges. The matching .md note (if any) is managed separately via knowledge_delete.",
44559
+ inputSchema: {
44560
+ slug: external_exports3.string().describe("Slug of the code_ref row to delete")
44561
+ }
44562
+ },
44563
+ async ({ slug }) => {
44564
+ await svc.deleteCodeRef(slug);
44565
+ return textResponse({ slug, deleted: true });
44566
+ }
44567
+ );
44568
+ server.registerTool(
44569
+ "touches_add",
44570
+ {
44571
+ description: 'Add (or update) a TOUCHES edge: task \u2192 code_ref with a required relation. "modifies" = the task edits the anchor; "reference" = the task reads it as a pattern. Re-adding the same (task, code_ref) pair overwrites the relation.',
44572
+ inputSchema: {
44573
+ taskId: external_exports3.string().describe("Task ID"),
44574
+ codeRefSlug: external_exports3.string().describe("Slug of an existing code_ref row"),
44575
+ relation: external_exports3.enum(RELATION_ENUM).describe("modifies | reference")
44576
+ }
44577
+ },
44578
+ async ({ taskId, codeRefSlug, relation }) => {
44579
+ await svc.addTouches(taskId, codeRefSlug, relation);
44580
+ return textResponse({ taskId, codeRefSlug, relation });
44581
+ }
44582
+ );
44583
+ server.registerTool(
44584
+ "touches_remove",
44585
+ {
44586
+ description: "Remove a TOUCHES edge between a task and a code_ref.",
44587
+ inputSchema: {
44588
+ taskId: external_exports3.string().describe("Task ID"),
44589
+ codeRefSlug: external_exports3.string().describe("Slug of the code_ref row")
44590
+ }
44591
+ },
44592
+ async ({ taskId, codeRefSlug }) => {
44593
+ await svc.removeTouches(taskId, codeRefSlug);
44594
+ return textResponse({ taskId, codeRefSlug, removed: true });
44595
+ }
44596
+ );
44597
+ server.registerTool(
44598
+ "task_touches",
44599
+ {
44600
+ description: "List the TOUCHES edges for a task \u2014 every code_ref it modifies or references, with the relation. Use before starting a task to see which code anchors it will edit vs. read.",
44601
+ inputSchema: {
44602
+ taskId: external_exports3.string().describe("Task ID")
44603
+ }
44604
+ },
44605
+ async ({ taskId }) => textResponse(await svc.getTouchesForTask(taskId))
44606
+ );
44607
+ };
44608
+
44609
+ // src/adapters/mcp/mcp-tools/graph-tools.ts
44610
+ var EDGE_TYPE_ENUM = [
44611
+ "DEPENDS_ON",
44612
+ "IMPLEMENTS",
44613
+ "USES_TECH",
44614
+ "DECIDED_BY",
44615
+ "REALIZES",
44616
+ "ABOUT",
44617
+ "PINS",
44618
+ "IN",
44619
+ "INTEGRATES_WITH"
44620
+ ];
44621
+ var register11 = (server, svc) => {
44622
+ server.registerTool(
44623
+ "graph_edges",
44624
+ {
44625
+ description: 'Query first-class knowledge-graph edges on any node (task ID, feature/gotcha slug, workspace ID, code_ref slug). direction "out" = edges FROM the node, "in" = edges pointing AT it, "both" = either. Examples: which tasks realize a feature \u2192 {nodeId:"feature-x", type:"REALIZES", direction:"in"}; which workspaces a feature is in \u2192 {nodeId:"feature-x", type:"IN", direction:"out"}; which gotchas are about it \u2192 {nodeId:"feature-x", type:"ABOUT", direction:"in"}.',
44626
+ inputSchema: {
44627
+ nodeId: external_exports3.string().describe("Node identity: TASK-NNN, knowledge slug, workspace ID, or code_ref slug"),
44628
+ type: external_exports3.enum(EDGE_TYPE_ENUM).optional().describe("Filter to one edge type. Omit to return all edge types on the node."),
44629
+ direction: external_exports3.enum(["out", "in", "both"]).optional().describe("out = from node, in = to node, both = either (default)")
44630
+ }
44631
+ },
44632
+ async ({ nodeId, type, direction }) => {
44633
+ const dir = direction ?? "both";
44634
+ const t = type;
44635
+ let edges;
44636
+ if (dir === "out") {
44637
+ edges = await svc.getRelationshipsFrom(nodeId, t);
44638
+ } else if (dir === "in") {
44639
+ edges = await svc.getRelationshipsTo(nodeId, t);
44640
+ } else {
44641
+ const all = await svc.getRelationships(nodeId);
44642
+ edges = t ? all.filter((e) => e.type === t) : all;
44643
+ }
44644
+ return textResponse(edges);
44645
+ }
44646
+ );
44647
+ };
44648
+
44649
+ // src/core/domain/services/feature-projection.ts
44650
+ var RoleBleedError = class extends Error {
44651
+ constructor(message) {
44652
+ super(message);
44653
+ this.name = "RoleBleedError";
44654
+ }
44655
+ };
44656
+ var DAY_NUMBER_RE = /\b\d+(?:\s*[-–]\s*\d+)?\s*(?:d|days?|hrs?|hours?|h|wks?|weeks?)\b/i;
44657
+ var FILE_PATH_RE = /\b[\w/\\.-]+\.(?:cs|ts|tsx|js|mjs|sql|md)\b/i;
44658
+ var DOTTED_SYMBOL_RE = /\b[A-Z][A-Za-z0-9]+(?:\.[A-Z][A-Za-z0-9]+){2,}\b/;
44659
+ var SQL_RE = /\b(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|jsonb)\b/;
44660
+ var DEPLOY_VERB = "deploy(?:ed|ment)?|releas(?:e|ed|es)|ship(?:ped|s)?|rollout|roll(?:ed)? ?out|launch(?:ed)?|go[- ]?live";
44661
+ var ISO_DATE = "\\d{4}-\\d{2}-\\d{2}(?:[T ]\\d{2}:\\d{2})?";
44662
+ var DEPLOY_DATE_RE = new RegExp(
44663
+ `(?:\\b(?:${DEPLOY_VERB})\\b[^.\\n]{0,30}?${ISO_DATE})|(?:${ISO_DATE}[^.\\n]{0,20}?\\b(?:${DEPLOY_VERB})\\b)`,
44664
+ "i"
44665
+ );
44666
+ function assertNoNumberOfDays(label, ...texts) {
44667
+ for (const text of texts) {
44668
+ if (!text) continue;
44669
+ const m = text.match(DAY_NUMBER_RE);
44670
+ if (m) {
44671
+ throw new RoleBleedError(
44672
+ `${label}: effort-band fidelity violation (M4) \u2014 found time quantity "${m[0]}"`
44673
+ );
44674
+ }
44675
+ }
44676
+ }
44677
+ function assertNoCodeBleed(label, ...texts) {
44678
+ for (const text of texts) {
44679
+ if (!text) continue;
44680
+ const path9 = text.match(FILE_PATH_RE);
44681
+ if (path9) throw new RoleBleedError(`${label}: role bleed (M3) \u2014 file path "${path9[0]}"`);
44682
+ const sym = text.match(DOTTED_SYMBOL_RE);
44683
+ if (sym) throw new RoleBleedError(`${label}: role bleed (M3) \u2014 dotted symbol "${sym[0]}"`);
44684
+ const sql = text.match(SQL_RE);
44685
+ if (sql) throw new RoleBleedError(`${label}: role bleed (M3) \u2014 SQL token "${sql[0]}"`);
44686
+ }
44687
+ }
44688
+ function assertHasCodeRefs(label, view, expectRefs) {
44689
+ if (expectRefs && view.codeRefs.length === 0) {
44690
+ throw new RoleBleedError(
44691
+ `${label}: role bleed (M3) \u2014 dev answer missing code_refs though REALIZES tasks TOUCH code`
44692
+ );
44693
+ }
44694
+ }
44695
+ function assertNoSymbolBleed(label, ...texts) {
44696
+ for (const text of texts) {
44697
+ if (!text) continue;
44698
+ const sym = text.match(DOTTED_SYMBOL_RE);
44699
+ if (sym) throw new RoleBleedError(`${label}: role bleed (M3) \u2014 dotted symbol "${sym[0]}"`);
44700
+ }
44701
+ }
44702
+ function assertNoDeploymentDate(label, ...texts) {
44703
+ for (const text of texts) {
44704
+ if (!text) continue;
44705
+ const m = text.match(DEPLOY_DATE_RE);
44706
+ if (m) {
44707
+ throw new RoleBleedError(`${label}: role bleed (M3) \u2014 deployment date "${m[0]}"`);
44708
+ }
44709
+ }
44710
+ }
44711
+ function sectionFor(input, ...names) {
44712
+ for (const name of names) {
44713
+ const text = input.sections[name];
44714
+ if (text && text.trim().length > 0) return text.trim();
44715
+ }
44716
+ return null;
44717
+ }
44718
+ function deriveEffortBand(signal) {
44719
+ if (signal.length === 0) return null;
44720
+ const taskCount = signal.length;
44721
+ const baseIndex = taskCount >= 7 ? 3 : taskCount >= 4 ? 2 : taskCount >= 2 ? 1 : 0;
44722
+ const totalAc = signal.reduce((sum, t) => sum + t.acItemCount, 0);
44723
+ const hasEpic = signal.some((t) => t.labels.includes("epic"));
44724
+ const maxBlocked = signal.reduce((max, t) => Math.max(max, t.blockedByCount), 0);
44725
+ const reasons = [`${taskCount} realized task${taskCount === 1 ? "" : "s"} (base ${EFFORT_BANDS[baseIndex]})`];
44726
+ let index = baseIndex;
44727
+ if (hasEpic) {
44728
+ index += 1;
44729
+ reasons.push("+1 epic task");
44730
+ }
44731
+ if (totalAc >= 15) {
44732
+ index += 1;
44733
+ reasons.push(`+1 heavy spec (${totalAc} AC items)`);
44734
+ }
44735
+ if (maxBlocked >= 2) {
44736
+ index += 1;
44737
+ reasons.push(`+1 blocked work (${maxBlocked} blockers)`);
44738
+ }
44739
+ index = Math.min(index, EFFORT_BANDS.length - 1);
44740
+ return { band: EFFORT_BANDS[index], reasoning: reasons.join("; ") };
44741
+ }
44742
+ function firstLine(text) {
44743
+ for (const line of text.split("\n")) {
44744
+ const trimmed = line.trim();
44745
+ if (trimmed.length > 0) return trimmed;
44746
+ }
44747
+ return text.trim();
44748
+ }
44749
+ function deriveCeoBlockers(input) {
44750
+ if (input.status === "shipped") return [];
44751
+ const blocking = sectionFor(input, "currently blocking");
44752
+ if (!blocking) return [];
44753
+ return [{ slug: "currently-blocking", title: firstLine(blocking) }];
44754
+ }
44755
+ function projectCeo(input) {
44756
+ const authored = input.effortBand ?? null;
44757
+ const derived = authored ? null : deriveEffortBand(input.effortSignal);
44758
+ return {
44759
+ description: sectionFor(input, "description"),
44760
+ apps: input.workspaces,
44761
+ teams: null,
44762
+ effortBand: authored ?? derived?.band ?? null,
44763
+ effortBandSource: authored ? "authored" : derived ? "derived" : null,
44764
+ effortBandReasoning: derived?.reasoning ?? null,
44765
+ status: input.status ?? null,
44766
+ blockers: deriveCeoBlockers(input),
44767
+ // Gotchas are concerns, not blockers. Titles only — bodies carry symbols/SQL.
44768
+ concerns: input.gotchas.map((g) => ({ slug: g.slug, title: g.title }))
44769
+ };
44770
+ }
44771
+ function projectDev(input) {
44772
+ const blocking = sectionFor(input, "currently blocking");
44773
+ const status = input.status ?? null;
44774
+ return {
44775
+ module: sectionFor(input, "description"),
44776
+ codeRefs: input.codeRefs,
44777
+ gotchas: input.gotchas,
44778
+ relevantDecisions: input.gotchas.map((g) => g.slug),
44779
+ breakingChangeNote: status === "blocked" && blocking ? `Feature is blocked: ${blocking}` : blocking
44780
+ };
44781
+ }
44782
+ function deriveEdgeCase(g) {
44783
+ const detail = [g.trigger, g.context].filter((s) => s && s.trim().length > 0).join(" \u2014 ");
44784
+ return detail ? `${g.title}: ${detail}` : g.title;
44785
+ }
44786
+ function projectTester(input) {
44787
+ return {
44788
+ acceptanceCriteria: input.realizesTasks,
44789
+ edgeCases: input.gotchas.map(deriveEdgeCase),
44790
+ regressionScope: input.realizesTasks.filter((t) => t.status === "DONE").map((t) => t.title)
44791
+ };
44792
+ }
44793
+ function computeHonesty(input, role) {
44794
+ const used = [];
44795
+ const lacked = [];
44796
+ if (sectionFor(input, "description")) used.push("description");
44797
+ if (input.workspaces.length > 0) used.push("workspaces");
44798
+ else lacked.push("workspaces");
44799
+ if (input.gotchas.length > 0) used.push("gotchas");
44800
+ else lacked.push("recall");
44801
+ lacked.push("team-boundaries");
44802
+ if (role === "ceo-po") {
44803
+ if (input.effortBand) used.push("effort-band (authored)");
44804
+ else if (input.effortSignal.length > 0) used.push("effort-band (derived)");
44805
+ else lacked.push("effort-band");
44806
+ }
44807
+ if (role === "dev") {
44808
+ if (input.codeRefs.length > 0) used.push("code-refs");
44809
+ else if (input.realizesTasksHaveTouches) lacked.push("code-refs");
44810
+ }
44811
+ if (role === "tester") {
44812
+ if (input.realizesTasks.some((t) => t.acItems.length > 0)) used.push("acceptance-criteria");
44813
+ else lacked.push("acceptance-criteria");
44814
+ if (input.gotchas.length > 0) used.push("edge-cases");
44815
+ else lacked.push("edge-cases");
44816
+ if (input.realizesTasks.some((t) => t.status === "DONE")) used.push("regression-scope");
44817
+ else lacked.push("regression-scope");
44818
+ }
44819
+ if (input.isStale) lacked.push("stale-refs");
44820
+ return { used, lacked };
44821
+ }
44822
+ function projectFeature(input, role) {
44823
+ const honesty = computeHonesty(input, role);
44824
+ if (role === "ceo-po") {
44825
+ const view2 = projectCeo(input);
44826
+ const ceoTitles = [...view2.blockers, ...view2.concerns].map((b) => b.title);
44827
+ assertNoCodeBleed("ceo-po", view2.description, view2.effortBandReasoning, ...ceoTitles);
44828
+ assertNoNumberOfDays("ceo-po", view2.description, view2.effortBand, view2.effortBandReasoning, ...ceoTitles);
44829
+ return { featureId: input.featureId, role, view: view2, recall: input.gotchas, honesty };
44830
+ }
44831
+ if (role === "tester") {
44832
+ const view2 = projectTester(input);
44833
+ const derived = [...view2.edgeCases, ...view2.regressionScope, ...view2.acceptanceCriteria.map((t) => t.title)];
44834
+ assertNoSymbolBleed("tester", ...derived);
44835
+ assertNoDeploymentDate("tester", ...derived);
44836
+ return { featureId: input.featureId, role, view: view2, recall: input.gotchas, honesty };
44837
+ }
44838
+ const view = projectDev(input);
44839
+ assertHasCodeRefs("dev", view, input.realizesTasksHaveTouches);
44840
+ return { featureId: input.featureId, role, view, recall: input.gotchas, honesty };
44841
+ }
44842
+
44843
+ // src/adapters/mcp/mcp-tools/feature-projection-builder.ts
44844
+ var FeatureNotFoundError = class extends Error {
44845
+ constructor(featureId) {
44846
+ super(`Feature ${featureId} not found or not a feature node`);
44847
+ this.name = "FeatureNotFoundError";
44848
+ }
44849
+ };
44850
+ function parseSections(body) {
44851
+ const sections = {};
44852
+ let current = null;
44853
+ let buffer = [];
44854
+ const flush = () => {
44855
+ if (current) sections[current] = buffer.join("\n").trim();
44856
+ };
44857
+ for (const line of splitLines(body)) {
44858
+ const heading = line.match(/^##\s+(.+?)\s*$/);
44859
+ if (heading) {
44860
+ flush();
44861
+ current = heading[1].toLowerCase();
44862
+ buffer = [];
44863
+ } else if (current) {
44864
+ buffer.push(line);
44865
+ }
44866
+ }
44867
+ flush();
44868
+ return sections;
44869
+ }
44870
+ async function gatherGotchas(svc, featureId) {
44871
+ const aboutEdges = await svc.getRelationshipsTo(featureId, "ABOUT");
44872
+ const gotchas = [];
44873
+ for (const edge of aboutEdges) {
44874
+ const entry = await svc.getKnowledge(edge.fromId);
44875
+ if (!entry) {
44876
+ gotchas.push({ slug: edge.fromId, title: edge.fromId });
44877
+ continue;
44878
+ }
44879
+ const sections = parseSections(entry.body);
44880
+ gotchas.push({
44881
+ slug: entry.slug,
44882
+ title: entry.frontmatter.title,
44883
+ trigger: sections["trigger"],
44884
+ context: sections["context"],
44885
+ resolution: sections["resolution"]
44886
+ });
44887
+ }
44888
+ return gotchas;
44889
+ }
44890
+ async function gatherRealizesTasks(svc, taskIds) {
44891
+ const tasks = [];
44892
+ for (const taskId of taskIds) {
44893
+ const task = await svc.getTask(taskId);
44894
+ if (!task) continue;
44895
+ tasks.push({
44896
+ taskId,
44897
+ title: task.title,
44898
+ status: task.status,
44899
+ acItems: findAcItems(task.body ?? "").map((i) => i.text)
44900
+ });
44901
+ }
44902
+ return tasks;
44903
+ }
44904
+ async function gatherEffortSignal(svc, taskIds) {
44905
+ const signal = [];
44906
+ for (const taskId of taskIds) {
44907
+ const task = await svc.getTask(taskId);
44908
+ if (!task) continue;
44909
+ signal.push({
44910
+ taskId,
44911
+ labels: task.labels,
44912
+ acItemCount: findAcItems(task.body ?? "").length,
44913
+ blockedByCount: task.blockedBy.length
44914
+ });
44915
+ }
44916
+ return signal;
44917
+ }
44918
+ async function gatherCodeRefs(svc, taskIds) {
44919
+ const pointers = [];
44920
+ let hasTouches = false;
44921
+ for (const taskId of taskIds) {
44922
+ const edges = await svc.getTouchesForTask(taskId);
44923
+ if (edges.length > 0) hasTouches = true;
44924
+ for (const edge of edges) {
44925
+ const ref = await svc.getCodeRef(edge.codeRefSlug);
44926
+ pointers.push({
44927
+ taskId,
44928
+ slug: edge.codeRefSlug,
44929
+ path: ref?.path ?? edge.codeRefSlug,
44930
+ symbol: ref?.symbol ?? null,
44931
+ relation: edge.relation
44932
+ });
44933
+ }
44934
+ }
44935
+ return { pointers, hasTouches };
44936
+ }
44937
+ async function buildFeatureProjection(svc, featureId, role) {
44938
+ const entry = await svc.getKnowledge(featureId);
44939
+ if (!entry || entry.frontmatter.type !== "feature") throw new FeatureNotFoundError(featureId);
44940
+ const structured = entry.frontmatter.structured ?? {};
44941
+ const inEdges = await svc.getRelationshipsFrom(featureId, "IN");
44942
+ const realizesEdges = await svc.getRelationshipsTo(featureId, "REALIZES");
44943
+ const realizesTaskIds = realizesEdges.map((e) => e.fromId);
44944
+ const gotchas = await gatherGotchas(svc, featureId);
44945
+ const dev = role === "dev" ? await gatherCodeRefs(svc, realizesTaskIds) : null;
44946
+ const realizesTasks = role === "tester" ? await gatherRealizesTasks(svc, realizesTaskIds) : [];
44947
+ const effortSignal = role === "ceo-po" && !structured.effortBand ? await gatherEffortSignal(svc, realizesTaskIds) : [];
44948
+ const input = {
44949
+ featureId,
44950
+ title: entry.frontmatter.title,
44951
+ status: structured.status,
44952
+ effortBand: structured.effortBand,
44953
+ sections: parseSections(entry.body),
44954
+ workspaces: inEdges.map((e) => e.toId),
44955
+ realizesTaskIds,
44956
+ effortSignal,
44957
+ gotchas,
44958
+ codeRefs: dev?.pointers ?? [],
44959
+ realizesTasksHaveTouches: dev?.hasTouches ?? false,
44960
+ realizesTasks,
44961
+ isStale: entry.isStale
44962
+ };
44963
+ return projectFeature(input, role);
44964
+ }
44965
+
44966
+ // src/adapters/mcp/mcp-tools/feature-projection-tools.ts
44967
+ var register12 = (server, svc) => {
44968
+ server.registerTool(
44969
+ "feature_projection",
44970
+ {
44971
+ description: 'Project a feature knowledge node to a role-appropriate answer. role="ceo-po" \u2192 business description + apps + effort BAND (never number-of-days) + blockers; role="dev" \u2192 module + code_ref pointers with modifies/reference relation + gotchas recalled before the first question; role="tester" \u2192 acceptance criteria collated per realized task + edge cases derived from gotcha trigger/context + regression scope (shipped tasks that must not break). Each bundle includes an honesty section (what the projection used vs. lacked). featureId is the feature slug (e.g. feature-crawler-list-ui-enhancements).',
44972
+ inputSchema: {
44973
+ featureId: external_exports3.string().describe("Feature knowledge slug"),
44974
+ role: external_exports3.enum(["ceo-po", "dev", "tester"]).describe("Asker role: ceo-po | dev | tester")
44975
+ }
44976
+ },
44977
+ async ({ featureId, role }) => {
44978
+ try {
44979
+ return textResponse(await buildFeatureProjection(svc, featureId, role));
44980
+ } catch (err) {
44981
+ if (err instanceof FeatureNotFoundError) return textResponse(err.message);
44982
+ throw err;
44983
+ }
44984
+ }
44985
+ );
44986
+ };
44987
+
44192
44988
  // src/core/domain/stats-service.ts
44193
44989
  var FLOOR_CALLS = 5;
44194
44990
  var BROKEN_ERROR_RATE = 0.2;
@@ -44257,7 +45053,7 @@ function classify(t, mvpThreshold) {
44257
45053
  }
44258
45054
 
44259
45055
  // src/adapters/mcp/mcp-tools/stats-tools.ts
44260
- var register9 = (server, svc) => {
45056
+ var register13 = (server, svc) => {
44261
45057
  server.registerTool(
44262
45058
  "stats_report",
44263
45059
  {
@@ -44283,7 +45079,7 @@ var register9 = (server, svc) => {
44283
45079
  // src/adapters/mcp/mcp-tools/cleanup-tools.ts
44284
45080
  var fs6 = __toESM(require("node:fs"));
44285
45081
  var KNOWLEDGE_ACTION_ENUM = ["delete", "leave"];
44286
- var register10 = (server, svc) => {
45082
+ var register14 = (server, svc) => {
44287
45083
  server.registerTool(
44288
45084
  "cleanup_worktree_orphans",
44289
45085
  {
@@ -44372,7 +45168,7 @@ function dirSizeBytes(dirPath) {
44372
45168
  }
44373
45169
  return total;
44374
45170
  }
44375
- var register11 = (server, artifactsDir) => {
45171
+ var register15 = (server, artifactsDir) => {
44376
45172
  server.registerTool(
44377
45173
  "cleanup_artifacts",
44378
45174
  {
@@ -44457,7 +45253,7 @@ var EVENT_TYPE_ENUM = [
44457
45253
  "memory_write",
44458
45254
  "memory_recall"
44459
45255
  ];
44460
- var register12 = (server, svc) => {
45256
+ var register16 = (server, svc) => {
44461
45257
  server.registerTool(
44462
45258
  "session_event_add",
44463
45259
  {
@@ -44490,7 +45286,7 @@ var EVENT_TYPE_ENUM2 = [
44490
45286
  "memory_write",
44491
45287
  "memory_recall"
44492
45288
  ];
44493
- var register13 = (server, svc) => {
45289
+ var register17 = (server, svc) => {
44494
45290
  server.registerTool(
44495
45291
  "session_event_list",
44496
45292
  {
@@ -44511,7 +45307,7 @@ var register13 = (server, svc) => {
44511
45307
  // src/adapters/mcp/mcp-tools/memory-write.ts
44512
45308
  var SCOPE_TYPE_ENUM = ["user", "project", "workspace", "task"];
44513
45309
  var MEMORY_TYPE_ENUM = ["episodic", "procedural"];
44514
- var register14 = (server, svc) => {
45310
+ var register18 = (server, svc) => {
44515
45311
  server.registerTool(
44516
45312
  "memory_write",
44517
45313
  {
@@ -44544,7 +45340,7 @@ var register14 = (server, svc) => {
44544
45340
  };
44545
45341
 
44546
45342
  // src/adapters/mcp/mcp-tools/memory-recall.ts
44547
- var register15 = (server, svc) => {
45343
+ var register19 = (server, svc) => {
44548
45344
  server.registerTool(
44549
45345
  "memory_recall",
44550
45346
  {
@@ -44569,7 +45365,7 @@ var register15 = (server, svc) => {
44569
45365
  };
44570
45366
 
44571
45367
  // src/adapters/mcp/mcp-tools/memory-promote-to-knowledge.ts
44572
- var register16 = (server, svc) => {
45368
+ var register20 = (server, svc) => {
44573
45369
  server.registerTool(
44574
45370
  "memory_promote_to_knowledge",
44575
45371
  {
@@ -44607,7 +45403,7 @@ var register16 = (server, svc) => {
44607
45403
  };
44608
45404
 
44609
45405
  // src/adapters/mcp/mcp-tools/ac-check.ts
44610
- var register17 = (server, svc) => {
45406
+ var register21 = (server, svc) => {
44611
45407
  server.registerTool(
44612
45408
  "ac_check",
44613
45409
  {
@@ -44667,17 +45463,21 @@ function buildMcpServer(deps, toolAllowlist, sink) {
44667
45463
  register4(instrumented, deps.svc);
44668
45464
  register5(instrumented, deps.svc);
44669
45465
  register6(instrumented, deps.svc);
44670
- register7(instrumented, deps.svc, deps.dataDir, deps.dbPath);
44671
- register8(instrumented, deps.svc);
45466
+ register7(instrumented, deps.svc);
45467
+ register8(instrumented, deps.svc, deps.dataDir, deps.dbPath);
44672
45468
  register9(instrumented, deps.svc);
44673
45469
  register10(instrumented, deps.svc);
44674
- register11(instrumented, deps.artifactsDir);
45470
+ register11(instrumented, deps.svc);
44675
45471
  register12(instrumented, deps.svc);
44676
45472
  register13(instrumented, deps.svc);
44677
45473
  register14(instrumented, deps.svc);
44678
- register15(instrumented, deps.svc);
45474
+ register15(instrumented, deps.artifactsDir);
44679
45475
  register16(instrumented, deps.svc);
44680
45476
  register17(instrumented, deps.svc);
45477
+ register18(instrumented, deps.svc);
45478
+ register19(instrumented, deps.svc);
45479
+ register20(instrumented, deps.svc);
45480
+ register21(instrumented, deps.svc);
44681
45481
  return { server, toolCount: instrumented.registeredToolNames.length };
44682
45482
  }
44683
45483
  async function startMcpServer() {
@@ -44699,11 +45499,11 @@ async function startMcpServer() {
44699
45499
  dbPath: dataPaths.dbPath
44700
45500
  };
44701
45501
  if (mode === "http") {
44702
- const oauth = await buildOAuthConfig(backend);
45502
+ const oauth = buildOAuthConfig();
44703
45503
  const token = process.env.MCP_HTTP_TOKEN ?? "";
44704
45504
  if (!oauth && token.length === 0) {
44705
45505
  process.stderr.write(
44706
- "[choda-deck] MCP_TRANSPORT=http requires MCP_HTTP_TOKEN (or MCP_OAUTH_MODE=1 + MCP_OAUTH_ISSUER) \u2014 refusing to expose unauthenticated\n"
45506
+ "[choda-deck] MCP_TRANSPORT=http requires MCP_HTTP_TOKEN (or MCP_OAUTH_MODE=1 + Keycloak config) \u2014 refusing to expose unauthenticated\n"
44707
45507
  );
44708
45508
  process.exit(2);
44709
45509
  }
@@ -44720,7 +45520,9 @@ async function startMcpServer() {
44720
45520
  port,
44721
45521
  bind,
44722
45522
  token,
44723
- oauth
45523
+ oauth,
45524
+ // ADR-030 Phase 2 — read-only pull source (GET /sync/since).
45525
+ syncSource: { fetchSince: (since) => svc.fetchSince(since) }
44724
45526
  }
44725
45527
  );
44726
45528
  return;
@@ -44730,44 +45532,44 @@ async function startMcpServer() {
44730
45532
  const transport = new StdioServerTransport();
44731
45533
  await server.connect(transport);
44732
45534
  }
44733
- async function buildOAuthConfig(backend) {
45535
+ function buildOAuthConfig() {
44734
45536
  if (process.env.MCP_OAUTH_MODE !== "1") return void 0;
44735
- const issuer = (process.env.MCP_OAUTH_ISSUER ?? "").replace(/\/$/, "");
44736
- if (issuer.length === 0) {
44737
- process.stderr.write(
44738
- "[choda-deck] MCP_OAUTH_MODE=1 requires MCP_OAUTH_ISSUER (e.g. https://mcp.choda.dev)\n"
44739
- );
44740
- process.exit(2);
44741
- }
44742
- const passwordFile = process.env.MCP_OAUTH_CONSENT_PASSWORD_FILE ?? path9.join(process.cwd(), "sensitive_information", "oauth-consent-password.txt");
44743
- if (!fs8.existsSync(passwordFile)) {
44744
- process.stderr.write(
44745
- `[choda-deck] MCP_OAUTH_MODE=1 needs consent password file at ${passwordFile}
44746
- `
44747
- );
44748
- process.exit(2);
44749
- }
44750
- const hash2 = fs8.readFileSync(passwordFile, "utf8").trim();
44751
- if (!/^[0-9a-f]{64}$/i.test(hash2)) {
44752
- process.stderr.write(
44753
- `[choda-deck] consent password file must contain a 64-char hex SHA-256 hash (got ${hash2.length} chars)
44754
- `
44755
- );
45537
+ const origin = requireEnv("MCP_OAUTH_ISSUER", "public origin e.g. https://mcp.choda.dev").replace(
45538
+ /\/$/,
45539
+ ""
45540
+ );
45541
+ const realmIssuer = requireEnv(
45542
+ "MCP_OIDC_ISSUER",
45543
+ "Keycloak realm issuer e.g. https://id.choda.dev/realms/choda"
45544
+ ).replace(/\/$/, "");
45545
+ const clientId = requireEnv("MCP_OIDC_CLIENT_ID", "pinned Keycloak public client id");
45546
+ const audience = process.env.MCP_OIDC_AUDIENCE ?? clientId;
45547
+ const secretFile = process.env.MCP_OIDC_CLIENT_SECRET_FILE;
45548
+ const clientSecret = secretFile && fs8.existsSync(secretFile) ? fs8.readFileSync(secretFile, "utf8").trim() : void 0;
45549
+ const verifier = createKeycloakVerifier({
45550
+ issuer: realmIssuer,
45551
+ audience,
45552
+ jwksUri: `${realmIssuer}/protocol/openid-connect/certs`
45553
+ });
45554
+ return {
45555
+ origin,
45556
+ keycloak: {
45557
+ authorizationEndpoint: `${realmIssuer}/protocol/openid-connect/auth`,
45558
+ tokenEndpoint: `${realmIssuer}/protocol/openid-connect/token`,
45559
+ clientId,
45560
+ clientSecret
45561
+ },
45562
+ verifier
45563
+ };
45564
+ }
45565
+ function requireEnv(name, hint) {
45566
+ const value = (process.env[name] ?? "").trim();
45567
+ if (value.length === 0) {
45568
+ process.stderr.write(`[choda-deck] MCP_OAUTH_MODE=1 requires ${name} (${hint})
45569
+ `);
44756
45570
  process.exit(2);
44757
45571
  }
44758
- const repo = backend.kind === "postgres" ? await buildPostgresOAuthRepo(backend.connectionString) : buildSqliteOAuthRepo(backend.dbPath);
44759
- return { repo, issuer, consentPasswordHashHex: hash2 };
44760
- }
44761
- function buildSqliteOAuthRepo(dbPath) {
44762
- const oauthDb = new import_better_sqlite32.default(dbPath);
44763
- oauthDb.pragma("foreign_keys = ON");
44764
- return new OAuthRepository(oauthDb);
44765
- }
44766
- async function buildPostgresOAuthRepo(connectionString) {
44767
- const poolMax = Number.parseInt(process.env.CHODA_PG_POOL_SIZE ?? "10", 10);
44768
- const conn = new PgConnection({ connectionString, max: poolMax });
44769
- await migrate(conn);
44770
- return new PostgresOAuthRepository(conn);
45572
+ return value;
44771
45573
  }
44772
45574
 
44773
45575
  // src/adapters/mcp/server.ts