syntaur 0.44.1 → 0.46.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.
Files changed (76) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dashboard/dist/assets/{_basePickBy-n0wU3YiR.js → _basePickBy-RQBuJKcX.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-CZE21ua2.js → _baseUniq-_J7s4kD3.js} +1 -1
  4. package/dashboard/dist/assets/{arc-Dd_UCPQq.js → arc-_9SyUgKQ.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-mgIxQnzX.js → architectureDiagram-2XIMDMQ5-C8LeFMgr.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-DhlycMw2.js → blockDiagram-WCTKOSBZ-gMh0EPEh.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-CuWqqKYM.js → c4Diagram-IC4MRINW-cHwecwLI.js} +1 -1
  8. package/dashboard/dist/assets/channel-C36dnl_e.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-CTP72FfB.js → chunk-4BX2VUAB-Bb2anYuQ.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-CE7fO2_0.js → chunk-55IACEB6-DYIRGzA1.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-DBsJdRy7.js → chunk-FMBD7UC4-sgRWBbaF.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-1dES-fUJ.js → chunk-JSJVCQXG-DlYKMl_j.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-U98s2l2I.js → chunk-KX2RTZJC-D0YDLAOF.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-DAy7YeTS.js → chunk-NQ4KR5QH-D-Y-CUx6.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-BeQRHiY8.js → chunk-QZHKN3VN-D7FpSvb5.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-Bt94wrMs.js → chunk-WL4C6EOR-CtXgQLdS.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BsoGa6_a.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BsoGa6_a.js +1 -0
  19. package/dashboard/dist/assets/clone-Bz6jW3OY.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-CdQh3kdW.js → cose-bilkent-S5V4N54A-YbTaohoJ.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-CKWb9fD7.js → dagre-KLK3FWXG-CMtwGAnP.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-Dlkqi8ga.js → diagram-E7M64L7V-D8wBMBAX.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-HhRKBN6J.js → diagram-IFDJBPK2-DfudLpiJ.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-B70t50AJ.js → diagram-P4PSJMXO-CyMy61wE.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-Cc6Lz8R-.js → erDiagram-INFDFZHY-BlB4ZQl9.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-UfEconjz.js → flowDiagram-PKNHOUZH-DbhDQJM3.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-B13deOQz.js → ganttDiagram-A5KZAMGK-DJFqteNi.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-CHRxKecy.js → gitGraphDiagram-K3NZZRJ6-D8etA_mm.js} +1 -1
  29. package/dashboard/dist/assets/{graph-BO4rYEQo.js → graph-Ce86jeZn.js} +1 -1
  30. package/dashboard/dist/assets/index-DRng26Jg.js +567 -0
  31. package/dashboard/dist/assets/index-DzHQIE2n.css +1 -0
  32. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-DYhPRnW_.js → infoDiagram-LFFYTUFH-Cx35U-h8.js} +1 -1
  33. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-uCkY17Z8.js → ishikawaDiagram-PHBUUO56-C04Y2nj8.js} +1 -1
  34. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-DT3-91Dx.js → journeyDiagram-4ABVD52K-D8-cxbxE.js} +1 -1
  35. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-CbNSU1jT.js → kanban-definition-K7BYSVSG-DVKqMylP.js} +1 -1
  36. package/dashboard/dist/assets/{layout-wdZYENqD.js → layout-98xZDpgu.js} +1 -1
  37. package/dashboard/dist/assets/{linear-DP2LUzjc.js → linear-0jk_IwAc.js} +1 -1
  38. package/dashboard/dist/assets/{mermaid.core-glRfe02B.js → mermaid.core-C337VWfr.js} +4 -4
  39. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-Bq2Qe7iv.js → mindmap-definition-YRQLILUH-8sNYGYEP.js} +1 -1
  40. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-BnG8dbQb.js → pieDiagram-SKSYHLDU-afcmzHxf.js} +1 -1
  41. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-CBvIkuCW.js → quadrantDiagram-337W2JSQ-B4RjcpOq.js} +1 -1
  42. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-CsHJZ9D_.js → requirementDiagram-Z7DCOOCP-CRavU6cI.js} +1 -1
  43. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DioS3JH6.js → sankeyDiagram-WA2Y5GQK-DFomU3z-.js} +1 -1
  44. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-_noTQrZH.js → sequenceDiagram-2WXFIKYE-CGKO7nmK.js} +1 -1
  45. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-K3R1Eh9N.js → stateDiagram-RAJIS63D-BjFI1K8h.js} +1 -1
  46. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-BtxefYKD.js +1 -0
  47. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-Bb3u4Ahz.js → timeline-definition-YZTLITO2-BBo8XJFG.js} +1 -1
  48. package/dashboard/dist/assets/{treemap-KZPCXAKY-BLiQz7AY.js → treemap-KZPCXAKY-COd6i6TE.js} +1 -1
  49. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-BLsoQIAw.js → vennDiagram-LZ73GAT5-CGQweQ36.js} +1 -1
  50. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-Dm1juAkc.js → xychartDiagram-JWTSCODW-mfJ5So7N.js} +1 -1
  51. package/dashboard/dist/index.html +2 -2
  52. package/dist/dashboard/server.js +1617 -300
  53. package/dist/dashboard/server.js.map +1 -1
  54. package/dist/index.js +2314 -1586
  55. package/dist/index.js.map +1 -1
  56. package/dist/launch/index.d.ts +21 -1
  57. package/dist/launch/index.js +685 -111
  58. package/dist/launch/index.js.map +1 -1
  59. package/package.json +1 -1
  60. package/platforms/SESSION-ID-RESOLUTION.md +41 -4
  61. package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
  62. package/platforms/claude-code/hooks/session-cleanup.sh +25 -64
  63. package/platforms/claude-code/hooks/session-start.sh +35 -109
  64. package/platforms/claude-code/skills/track-session/SKILL.md +12 -60
  65. package/platforms/codex/.codex-plugin/plugin.json +1 -1
  66. package/platforms/codex/skills/track-session/SKILL.md +12 -60
  67. package/platforms/hermes/plugins/syntaur/__pycache__/__init__.cpython-312.pyc +0 -0
  68. package/platforms/hermes/plugins/syntaur/__pycache__/boundary.cpython-312.pyc +0 -0
  69. package/skills/track-session/SKILL.md +12 -60
  70. package/dashboard/dist/assets/channel-BLQNOcNZ.js +0 -1
  71. package/dashboard/dist/assets/classDiagram-VBA2DB6C-Mx6la8yG.js +0 -1
  72. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-Mx6la8yG.js +0 -1
  73. package/dashboard/dist/assets/clone-58HOBY7h.js +0 -1
  74. package/dashboard/dist/assets/index-BKdHsXLj.js +0 -566
  75. package/dashboard/dist/assets/index-D1f1wB-7.css +0 -1
  76. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-BJes_eiS.js +0 -1
@@ -2374,7 +2374,11 @@ var init_fields = __esm({
2374
2374
  tags: { kind: "list" },
2375
2375
  archived: { kind: "bool" },
2376
2376
  title: { kind: "substring" },
2377
- search: { kind: "substring", get: (i) => i["title"] },
2377
+ // `search` reads a dedicated `searchText` haystack when the item provides one
2378
+ // (so the dashboard can match title + slug + project like its filter box),
2379
+ // falling back to `title` when absent. Backward-compatible: title-only when no
2380
+ // searchText. The `title` field stays title-only.
2381
+ search: { kind: "substring", get: (i) => i["searchText"] ?? i["title"] },
2378
2382
  created: { kind: "timestamp" },
2379
2383
  updated: { kind: "timestamp" },
2380
2384
  completedat: { kind: "timestamp", get: (i) => i["completedAt"] },
@@ -2997,6 +3001,20 @@ var init_parser3 = __esm({
2997
3001
  });
2998
3002
 
2999
3003
  // src/utils/query/index.ts
3004
+ function compileQuery(input, registry = ASSIGNMENT_FIELDS) {
3005
+ const parsed = parseQuery(input);
3006
+ if (!parsed.ast) return { query: null, errors: parsed.errors };
3007
+ try {
3008
+ const predicate = compileNode(parsed.ast, registry);
3009
+ return { query: { predicate, ast: parsed.ast }, errors: [] };
3010
+ } catch (err) {
3011
+ if (err instanceof CompileError) return { query: null, errors: err.errors };
3012
+ throw err;
3013
+ }
3014
+ }
3015
+ function validateQuery(input, registry = ASSIGNMENT_FIELDS) {
3016
+ return compileQuery(input, registry).errors;
3017
+ }
3000
3018
  var init_query = __esm({
3001
3019
  "src/utils/query/index.ts"() {
3002
3020
  "use strict";
@@ -3009,17 +3027,7 @@ var init_query = __esm({
3009
3027
  }
3010
3028
  });
3011
3029
 
3012
- // src/lifecycle/derive.ts
3013
- var derive_exports = {};
3014
- __export(derive_exports, {
3015
- DERIVE_FIELDS: () => DERIVE_FIELDS,
3016
- acceptFactDeclarations: () => acceptFactDeclarations,
3017
- buildDeriveRegistry: () => buildDeriveRegistry,
3018
- buildQueryRegistry: () => buildQueryRegistry,
3019
- deriveDimensions: () => deriveDimensions,
3020
- factFieldNames: () => factFieldNames,
3021
- validateDeriveCondition: () => validateDeriveCondition
3022
- });
3030
+ // src/utils/fact-registry.ts
3023
3031
  function factFieldNames(decl) {
3024
3032
  const name = decl.name;
3025
3033
  const exportNames = {
@@ -3086,85 +3094,63 @@ function buildQueryRegistry(accepted) {
3086
3094
  for (const decl of accepted) addFactFields(registry, decl);
3087
3095
  return registry;
3088
3096
  }
3089
- function validateDeriveCondition(when, registry = DERIVE_FIELDS) {
3090
- if (when === "*") return null;
3091
- const parsed = parseQuery(when);
3092
- if (!parsed.ast) return parsed.errors[0]?.message ?? "unparseable condition";
3093
- try {
3094
- compileNode(parsed.ast, registry);
3095
- return null;
3096
- } catch (err) {
3097
- if (err instanceof CompileError) return err.errors[0]?.message ?? "invalid condition";
3098
- throw err;
3099
- }
3100
- }
3101
- function compiledWhen(registry, when) {
3102
- let cache2 = conditionCache.get(registry);
3103
- if (!cache2) {
3104
- cache2 = /* @__PURE__ */ new Map();
3105
- conditionCache.set(registry, cache2);
3106
- }
3107
- let pred = cache2.get(when);
3108
- if (!pred) {
3109
- if (when === "*") {
3110
- pred = () => true;
3097
+ function queryFieldNames(declarations) {
3098
+ const builtins = [
3099
+ "status",
3100
+ "priority",
3101
+ "type",
3102
+ "assignee",
3103
+ "project",
3104
+ "tag",
3105
+ "tags",
3106
+ "archived",
3107
+ "title",
3108
+ "search",
3109
+ "created",
3110
+ "updated",
3111
+ "completedAt",
3112
+ "statusAge",
3113
+ "phase",
3114
+ "disposition",
3115
+ "phaseAge",
3116
+ "hasRealObjective",
3117
+ "acRealTotal",
3118
+ "acRealChecked",
3119
+ "acAllChecked",
3120
+ "planExists",
3121
+ "planApproved",
3122
+ "workspaceSet",
3123
+ "implementationStarted",
3124
+ "depsSatisfied",
3125
+ "unresolvedQuestions",
3126
+ "progressStaleDays",
3127
+ "blocked",
3128
+ "parked",
3129
+ "reviewRequested",
3130
+ "pinned"
3131
+ ];
3132
+ const custom = [];
3133
+ for (const decl of declarations) {
3134
+ const names = factFieldNames(decl);
3135
+ if (decl.type === "attestation") {
3136
+ custom.push(
3137
+ names.exports.fact,
3138
+ names.exports.approved,
3139
+ names.exports.changesRequested,
3140
+ names.exports.by,
3141
+ names.exports.approvedBy
3142
+ );
3111
3143
  } else {
3112
- const parsed = parseQuery(when);
3113
- if (!parsed.ast) {
3114
- throw new CompileError(parsed.errors);
3115
- }
3116
- pred = compileNode(parsed.ast, registry);
3144
+ custom.push(names.exports.fact);
3117
3145
  }
3118
- cache2.set(when, pred);
3119
3146
  }
3120
- return pred;
3147
+ return [...builtins, ...custom];
3121
3148
  }
3122
- function deriveDimensions(input) {
3123
- const { facts, derive, currentStatus, terminalStatuses, knownStatusIds, override } = input;
3124
- const registry = input.registry ?? DERIVE_FIELDS;
3125
- if (terminalStatuses.has(currentStatus)) return null;
3126
- const ctx = { now: 0 };
3127
- const item = facts;
3128
- let phase = derive.phaseLadder[0]?.phase ?? currentStatus;
3129
- let nextAction = derive.phaseLadder[0]?.next ?? null;
3130
- for (let i = derive.phaseLadder.length - 1; i >= 0; i--) {
3131
- const rung = derive.phaseLadder[i];
3132
- if (compiledWhen(registry, rung.when)(item, ctx)) {
3133
- phase = rung.phase;
3134
- nextAction = rung.next ?? null;
3135
- break;
3136
- }
3137
- }
3138
- let disposition = "active";
3139
- for (const rule of derive.disposition) {
3140
- if (rule.when === null || compiledWhen(registry, rule.when)(item, ctx)) {
3141
- disposition = rule.is;
3142
- break;
3143
- }
3144
- }
3145
- let derivedStatus;
3146
- switch (disposition) {
3147
- case "parked":
3148
- derivedStatus = knownStatusIds.has(derive.headline.parked) ? derive.headline.parked : phase;
3149
- break;
3150
- case "blocked":
3151
- derivedStatus = knownStatusIds.has(derive.headline.blocked) ? derive.headline.blocked : phase;
3152
- break;
3153
- default:
3154
- derivedStatus = phase;
3155
- }
3156
- let status = derivedStatus;
3157
- if (override && override.status && !terminalStatuses.has(override.status) && knownStatusIds.has(override.status)) {
3158
- status = override.status;
3159
- }
3160
- return { phase, disposition, derivedStatus, status, nextAction };
3161
- }
3162
- var DERIVE_FIELDS, conditionCache;
3163
- var init_derive = __esm({
3164
- "src/lifecycle/derive.ts"() {
3149
+ var DERIVE_FIELDS;
3150
+ var init_fact_registry = __esm({
3151
+ "src/utils/fact-registry.ts"() {
3165
3152
  "use strict";
3166
3153
  init_query();
3167
- init_query();
3168
3154
  DERIVE_FIELDS = {
3169
3155
  hasrealobjective: { kind: "bool", get: (i) => i["hasRealObjective"] },
3170
3156
  acrealtotal: { kind: "number", get: (i) => i["acRealTotal"] },
@@ -3181,7 +3167,6 @@ var init_derive = __esm({
3181
3167
  reviewrequested: { kind: "bool", get: (i) => i["reviewRequested"] },
3182
3168
  pinned: { kind: "bool" }
3183
3169
  };
3184
- conditionCache = /* @__PURE__ */ new WeakMap();
3185
3170
  }
3186
3171
  });
3187
3172
 
@@ -3381,6 +3366,7 @@ function cloneDefaultConfig() {
3381
3366
  ...DEFAULT_CONFIG,
3382
3367
  onboarding: { ...DEFAULT_CONFIG.onboarding },
3383
3368
  agentDefaults: { ...DEFAULT_CONFIG.agentDefaults },
3369
+ session: { ...DEFAULT_CONFIG.session },
3384
3370
  integrations: { ...DEFAULT_CONFIG.integrations },
3385
3371
  backup: DEFAULT_CONFIG.backup ? { ...DEFAULT_CONFIG.backup } : null,
3386
3372
  statuses: DEFAULT_CONFIG.statuses ? {
@@ -4730,6 +4716,11 @@ async function readConfig() {
4730
4716
  fm["agentDefaults.autoCreateWorktree"]
4731
4717
  ) ? fm["agentDefaults.autoCreateWorktree"] : DEFAULT_CONFIG.agentDefaults.autoCreateWorktree
4732
4718
  },
4719
+ session: {
4720
+ autoTrack: SESSION_AUTO_TRACK_VALUES.includes(
4721
+ fm["session.autoTrack"]
4722
+ ) ? fm["session.autoTrack"] : DEFAULT_CONFIG.session.autoTrack
4723
+ },
4733
4724
  integrations: {
4734
4725
  claudePluginDir: parseOptionalAbsolutePath(
4735
4726
  fm["integrations.claudePluginDir"],
@@ -4829,7 +4820,7 @@ async function updateAgentsConfig(mutation, options = {}) {
4829
4820
  await writeAgentsConfig(next);
4830
4821
  return { previous, next, written: true };
4831
4822
  }
4832
- var DEFAULT_DERIVE_CONFIG, DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
4823
+ var DEFAULT_DERIVE_CONFIG, DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, SESSION_AUTO_TRACK_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
4833
4824
  var init_config2 = __esm({
4834
4825
  "src/utils/config.ts"() {
4835
4826
  "use strict";
@@ -4841,7 +4832,7 @@ var init_config2 = __esm({
4841
4832
  init_hotkeysCatalog();
4842
4833
  init_agents_schema();
4843
4834
  init_slug();
4844
- init_derive();
4835
+ init_fact_registry();
4845
4836
  init_query();
4846
4837
  init_terminal_schema();
4847
4838
  init_workspace_visibility_schema();
@@ -4895,6 +4886,9 @@ var init_config2 = __esm({
4895
4886
  autoApprove: false,
4896
4887
  autoCreateWorktree: "ask"
4897
4888
  },
4889
+ session: {
4890
+ autoTrack: "all"
4891
+ },
4898
4892
  integrations: {
4899
4893
  claudePluginDir: null,
4900
4894
  codexPluginDir: null,
@@ -4915,6 +4909,7 @@ var init_config2 = __esm({
4915
4909
  }
4916
4910
  };
4917
4911
  AUTO_CREATE_WORKTREE_VALUES = ["skip", "ask", "always"];
4912
+ SESSION_AUTO_TRACK_VALUES = ["all", "workspaces-only", "off"];
4918
4913
  AgentConfigError = class extends Error {
4919
4914
  };
4920
4915
  DEFAULT_STATUS_COLORS = {
@@ -4942,6 +4937,103 @@ var init_config2 = __esm({
4942
4937
  }
4943
4938
  });
4944
4939
 
4940
+ // src/lifecycle/derive.ts
4941
+ var derive_exports = {};
4942
+ __export(derive_exports, {
4943
+ DERIVE_FIELDS: () => DERIVE_FIELDS,
4944
+ acceptFactDeclarations: () => acceptFactDeclarations,
4945
+ addFactFields: () => addFactFields,
4946
+ buildDeriveRegistry: () => buildDeriveRegistry,
4947
+ buildQueryRegistry: () => buildQueryRegistry,
4948
+ deriveDimensions: () => deriveDimensions,
4949
+ factFieldNames: () => factFieldNames,
4950
+ queryFieldNames: () => queryFieldNames,
4951
+ validateDeriveCondition: () => validateDeriveCondition
4952
+ });
4953
+ function validateDeriveCondition(when, registry = DERIVE_FIELDS) {
4954
+ if (when === "*") return null;
4955
+ const parsed = parseQuery(when);
4956
+ if (!parsed.ast) return parsed.errors[0]?.message ?? "unparseable condition";
4957
+ try {
4958
+ compileNode(parsed.ast, registry);
4959
+ return null;
4960
+ } catch (err) {
4961
+ if (err instanceof CompileError) return err.errors[0]?.message ?? "invalid condition";
4962
+ throw err;
4963
+ }
4964
+ }
4965
+ function compiledWhen(registry, when) {
4966
+ let cache2 = conditionCache.get(registry);
4967
+ if (!cache2) {
4968
+ cache2 = /* @__PURE__ */ new Map();
4969
+ conditionCache.set(registry, cache2);
4970
+ }
4971
+ let pred = cache2.get(when);
4972
+ if (!pred) {
4973
+ if (when === "*") {
4974
+ pred = () => true;
4975
+ } else {
4976
+ const parsed = parseQuery(when);
4977
+ if (!parsed.ast) {
4978
+ throw new CompileError(parsed.errors);
4979
+ }
4980
+ pred = compileNode(parsed.ast, registry);
4981
+ }
4982
+ cache2.set(when, pred);
4983
+ }
4984
+ return pred;
4985
+ }
4986
+ function deriveDimensions(input) {
4987
+ const { facts, derive, currentStatus, terminalStatuses, knownStatusIds, override } = input;
4988
+ const registry = input.registry ?? DERIVE_FIELDS;
4989
+ if (terminalStatuses.has(currentStatus)) return null;
4990
+ const ctx = { now: 0 };
4991
+ const item = facts;
4992
+ let phase = derive.phaseLadder[0]?.phase ?? currentStatus;
4993
+ let nextAction = derive.phaseLadder[0]?.next ?? null;
4994
+ for (let i = derive.phaseLadder.length - 1; i >= 0; i--) {
4995
+ const rung = derive.phaseLadder[i];
4996
+ if (compiledWhen(registry, rung.when)(item, ctx)) {
4997
+ phase = rung.phase;
4998
+ nextAction = rung.next ?? null;
4999
+ break;
5000
+ }
5001
+ }
5002
+ let disposition = "active";
5003
+ for (const rule of derive.disposition) {
5004
+ if (rule.when === null || compiledWhen(registry, rule.when)(item, ctx)) {
5005
+ disposition = rule.is;
5006
+ break;
5007
+ }
5008
+ }
5009
+ let derivedStatus;
5010
+ switch (disposition) {
5011
+ case "parked":
5012
+ derivedStatus = knownStatusIds.has(derive.headline.parked) ? derive.headline.parked : phase;
5013
+ break;
5014
+ case "blocked":
5015
+ derivedStatus = knownStatusIds.has(derive.headline.blocked) ? derive.headline.blocked : phase;
5016
+ break;
5017
+ default:
5018
+ derivedStatus = phase;
5019
+ }
5020
+ let status = derivedStatus;
5021
+ if (override && override.status && !terminalStatuses.has(override.status) && knownStatusIds.has(override.status)) {
5022
+ status = override.status;
5023
+ }
5024
+ return { phase, disposition, derivedStatus, status, nextAction };
5025
+ }
5026
+ var conditionCache;
5027
+ var init_derive = __esm({
5028
+ "src/lifecycle/derive.ts"() {
5029
+ "use strict";
5030
+ init_query();
5031
+ init_fact_registry();
5032
+ init_fact_registry();
5033
+ conditionCache = /* @__PURE__ */ new WeakMap();
5034
+ }
5035
+ });
5036
+
4945
5037
  // src/utils/playbooks.ts
4946
5038
  import { resolve as resolve7 } from "path";
4947
5039
  import { readdir as readdir3, readFile as readFile6, unlink } from "fs/promises";
@@ -5678,6 +5770,15 @@ var init_help = __esm({
5678
5770
  });
5679
5771
 
5680
5772
  // src/dashboard/session-db.ts
5773
+ var session_db_exports = {};
5774
+ __export(session_db_exports, {
5775
+ closeSessionDb: () => closeSessionDb,
5776
+ getSessionDb: () => getSessionDb,
5777
+ initSessionDb: () => initSessionDb,
5778
+ isSessionDbInitialized: () => isSessionDbInitialized,
5779
+ migrateFromMarkdown: () => migrateFromMarkdown,
5780
+ resetSessionDb: () => resetSessionDb
5781
+ });
5681
5782
  import Database from "better-sqlite3";
5682
5783
  import { resolve as resolve9 } from "path";
5683
5784
  import { readdir as readdir5 } from "fs/promises";
@@ -5822,6 +5923,9 @@ function initSessionDb(dbPath) {
5822
5923
  db.exec(POST_MIGRATION_INDEXES_SQL);
5823
5924
  return db;
5824
5925
  }
5926
+ function isSessionDbInitialized() {
5927
+ return db !== null;
5928
+ }
5825
5929
  function getSessionDb() {
5826
5930
  if (!db) {
5827
5931
  throw new Error(
@@ -5836,6 +5940,9 @@ function closeSessionDb() {
5836
5940
  db = null;
5837
5941
  }
5838
5942
  }
5943
+ function resetSessionDb() {
5944
+ db = null;
5945
+ }
5839
5946
  async function migrateFromMarkdown(projectsDir) {
5840
5947
  const database = getSessionDb();
5841
5948
  const count = database.prepare("SELECT COUNT(*) as count FROM sessions").get();
@@ -5866,8 +5973,8 @@ async function migrateFromMarkdown(projectsDir) {
5866
5973
  return allSessions.length;
5867
5974
  }
5868
5975
  async function parseMarkdownSessionsIndex(filePath, projectSlug) {
5869
- const { readFile: readFile23 } = await import("fs/promises");
5870
- const raw2 = await readFile23(filePath, "utf-8");
5976
+ const { readFile: readFile25 } = await import("fs/promises");
5977
+ const raw2 = await readFile25(filePath, "utf-8");
5871
5978
  const sessions = [];
5872
5979
  const lines = raw2.split("\n");
5873
5980
  let inTable = false;
@@ -5957,7 +6064,7 @@ function rowToSession(row) {
5957
6064
  originalHeadSha: row.original_head_sha ?? null
5958
6065
  };
5959
6066
  }
5960
- async function appendSession(_projectDir, session) {
6067
+ async function appendSession(_projectDir, session, opts) {
5961
6068
  const db4 = getSessionDb();
5962
6069
  db4.prepare(`
5963
6070
  INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path, pid, pid_started_at, original_head_sha)
@@ -5966,7 +6073,11 @@ async function appendSession(_projectDir, session) {
5966
6073
  project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),
5967
6074
  assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),
5968
6075
  agent = excluded.agent,
5969
- status = CASE WHEN status IN ('completed','stopped') THEN status ELSE excluded.status END,
6076
+ status = CASE
6077
+ WHEN status = 'completed' THEN status
6078
+ WHEN status = 'stopped' AND NOT (? AND excluded.status = 'active') THEN status
6079
+ ELSE excluded.status
6080
+ END,
5970
6081
  path = COALESCE(NULLIF(excluded.path, ''), path),
5971
6082
  description = COALESCE(NULLIF(excluded.description, ''), description),
5972
6083
  transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),
@@ -5986,15 +6097,16 @@ async function appendSession(_projectDir, session) {
5986
6097
  session.transcriptPath ?? null,
5987
6098
  session.pid ?? null,
5988
6099
  session.pidStartedAt ?? null,
5989
- session.originalHeadSha ?? null
6100
+ session.originalHeadSha ?? null,
6101
+ opts?.reviveStopped ? 1 : 0
5990
6102
  );
5991
6103
  }
5992
- async function updateSessionStatus(_projectDir, sessionId, status) {
6104
+ async function updateSessionStatus(_projectDir, sessionId, status, endedAt) {
5993
6105
  const db4 = getSessionDb();
5994
6106
  const isTerminal = status === "completed" || status === "stopped";
5995
6107
  const result = isTerminal ? db4.prepare(
5996
- "UPDATE sessions SET status = ?, ended = datetime('now'), updated_at = datetime('now') WHERE session_id = ?"
5997
- ).run(status, sessionId) : db4.prepare(
6108
+ "UPDATE sessions SET status = ?, ended = COALESCE(?, datetime('now')), updated_at = datetime('now') WHERE session_id = ?"
6109
+ ).run(status, endedAt ?? null, sessionId) : db4.prepare(
5998
6110
  "UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE session_id = ?"
5999
6111
  ).run(status, sessionId);
6000
6112
  return result.changes > 0;
@@ -6303,8 +6415,8 @@ function scanKey(serversDir2, projectsDir, assignmentsDir2) {
6303
6415
  return `${serversDir2}\0${projectsDir}\0${assignmentsDir2 ?? ""}`;
6304
6416
  }
6305
6417
  function delay(ms) {
6306
- return new Promise((resolve34) => {
6307
- const timer2 = setTimeout(resolve34, ms);
6418
+ return new Promise((resolve38) => {
6419
+ const timer2 = setTimeout(resolve38, ms);
6308
6420
  if (typeof timer2.unref === "function") {
6309
6421
  timer2.unref();
6310
6422
  }
@@ -7359,7 +7471,8 @@ async function getStatusConfig() {
7359
7471
  derive: config.statuses.derive ?? null,
7360
7472
  facts: config.statuses.facts ?? null,
7361
7473
  factDeclarations: accepted,
7362
- deriveRegistry: buildDeriveRegistry(accepted)
7474
+ deriveRegistry: buildDeriveRegistry(accepted),
7475
+ queryRegistry: buildQueryRegistry(accepted)
7363
7476
  };
7364
7477
  } else {
7365
7478
  const def = buildDefaultStatusConfig();
@@ -7373,7 +7486,8 @@ async function getStatusConfig() {
7373
7486
  derive: null,
7374
7487
  facts: null,
7375
7488
  factDeclarations: [],
7376
- deriveRegistry: buildDeriveRegistry([])
7489
+ deriveRegistry: buildDeriveRegistry([]),
7490
+ queryRegistry: buildQueryRegistry([])
7377
7491
  };
7378
7492
  }
7379
7493
  return _cachedConfig;
@@ -7682,14 +7796,30 @@ async function listArchived(projectsDir, assignmentsDir2) {
7682
7796
  return { projects, assignments: individuallyArchived };
7683
7797
  }
7684
7798
  async function toStandaloneBoardItem(sr) {
7685
- const { terminalStatuses } = await getStatusConfig();
7799
+ const config = await getStatusConfig();
7800
+ const { terminalStatuses } = config;
7801
+ let facts;
7802
+ try {
7803
+ const { computeFacts: computeFacts2 } = await Promise.resolve().then(() => (init_facts(), facts_exports));
7804
+ facts = await computeFacts2({
7805
+ assignmentDir: sr.assignmentDir,
7806
+ frontmatter: sr.record,
7807
+ body: sr.record.body,
7808
+ projectDir: null,
7809
+ terminalStatuses,
7810
+ declarations: config.factDeclarations
7811
+ });
7812
+ } catch (err) {
7813
+ console.warn(`toStandaloneBoardItem: computeFacts failed for ${sr.assignmentDir}:`, err);
7814
+ }
7686
7815
  return {
7687
7816
  ...toAssignmentSummary(sr.record, terminalStatuses),
7688
7817
  projectSlug: null,
7689
7818
  projectTitle: null,
7690
7819
  blockedReason: sr.record.blockedReason,
7691
7820
  projectWorkspace: sr.record.workspaceGroup ?? null,
7692
- availableTransitions: await getStandaloneAvailableTransitions(sr.record)
7821
+ availableTransitions: await getStandaloneAvailableTransitions(sr.record),
7822
+ facts
7693
7823
  };
7694
7824
  }
7695
7825
  async function getStandaloneAvailableTransitions(assignment) {
@@ -8576,7 +8706,24 @@ function toAssignmentSummary(assignment, terminalStatuses) {
8576
8706
  };
8577
8707
  }
8578
8708
  async function toAssignmentBoardItem(projectsDir, projectRecord, assignment) {
8579
- const { terminalStatuses } = await getStatusConfig();
8709
+ const config = await getStatusConfig();
8710
+ const { terminalStatuses } = config;
8711
+ const assignmentDir = resolve14(projectRecord.projectPath, "assignments", assignment.slug);
8712
+ const projectDir = projectRecord.projectPath;
8713
+ let facts;
8714
+ try {
8715
+ const { computeFacts: computeFacts2 } = await Promise.resolve().then(() => (init_facts(), facts_exports));
8716
+ facts = await computeFacts2({
8717
+ assignmentDir,
8718
+ frontmatter: assignment,
8719
+ body: assignment.body,
8720
+ projectDir,
8721
+ terminalStatuses,
8722
+ declarations: config.factDeclarations
8723
+ });
8724
+ } catch (err) {
8725
+ console.warn(`toAssignmentBoardItem: computeFacts failed for ${assignmentDir}:`, err);
8726
+ }
8580
8727
  return {
8581
8728
  ...toAssignmentSummary(assignment, terminalStatuses),
8582
8729
  projectSlug: projectRecord.summary.slug,
@@ -8588,7 +8735,8 @@ async function toAssignmentBoardItem(projectsDir, projectRecord, assignment) {
8588
8735
  projectRecord.summary.slug,
8589
8736
  assignment.slug,
8590
8737
  assignment
8591
- )
8738
+ ),
8739
+ facts
8592
8740
  };
8593
8741
  }
8594
8742
  function buildDependencyGraph(assignments) {
@@ -9206,6 +9354,431 @@ var init_api = __esm({
9206
9354
  }
9207
9355
  });
9208
9356
 
9357
+ // src/templates/cursor-rules.ts
9358
+ function renderCursorProtocol() {
9359
+ return `---
9360
+ description: Syntaur protocol rules for multi-agent coordination
9361
+ globs:
9362
+ alwaysApply: true
9363
+ ---
9364
+
9365
+ # Syntaur Protocol
9366
+
9367
+ You are working within the Syntaur protocol for multi-agent project coordination.
9368
+
9369
+ ## Directory Structure
9370
+
9371
+ \`\`\`
9372
+ ~/.syntaur/
9373
+ config.md
9374
+ projects/
9375
+ <project-slug>/
9376
+ manifest.md # Derived: root navigation (read-only)
9377
+ project.md # Human-authored: project overview (read-only)
9378
+ _index-assignments.md # Derived (read-only)
9379
+ _index-plans.md # Derived (read-only)
9380
+ _index-decisions.md # Derived (read-only)
9381
+ _status.md # Derived (read-only)
9382
+ assignments/
9383
+ <assignment-slug>/
9384
+ assignment.md # Agent-writable: source of truth for state (includes ## Todos)
9385
+ plan*.md # Agent-writable: versioned implementation plans (optional, one per ## Todos entry)
9386
+ progress.md # Agent-writable, append-only: timestamped progress log
9387
+ comments.md # CLI-mediated: threaded questions/notes/feedback (via \`syntaur comment\`)
9388
+ scratchpad.md # Agent-writable: working notes
9389
+ handoff.md # Agent-writable: append-only cross-ticket outbound at completion
9390
+ decision-record.md # Agent-writable: append-only decision log
9391
+ sessions/
9392
+ <session-id>/
9393
+ summary.md # Agent-writable: per-session continuity (single doc, overwritten)
9394
+ resources/
9395
+ _index.md # Derived (read-only)
9396
+ <resource-slug>.md # Shared-writable
9397
+ memories/
9398
+ _index.md # Derived (read-only)
9399
+ <memory-slug>.md # Shared-writable
9400
+ assignments/
9401
+ <assignment-id>/ # Standalone assignments \u2014 folder = UUID, \`project: null\`, slug display-only
9402
+ assignment.md
9403
+ plan*.md
9404
+ progress.md
9405
+ comments.md
9406
+ scratchpad.md
9407
+ handoff.md
9408
+ decision-record.md
9409
+ sessions/<session-id>/summary.md # Per-session continuity (same as project-nested)
9410
+ \`\`\`
9411
+
9412
+ ## Write Boundary Rules (CRITICAL)
9413
+
9414
+ ### Files you may WRITE:
9415
+ 1. **Your assignment folder** -- only the assignment you are currently working on:
9416
+ - \`assignment.md\`, \`plan*.md\` (0 or more versioned plan files), \`progress.md\`, \`scratchpad.md\`, \`handoff.md\` (cross-ticket outbound at completion), \`decision-record.md\`
9417
+ - \`sessions/<session-id>/summary.md\` -- per-session continuity (single doc per session id, overwritten on save). Distinct from \`handoff.md\`.
9418
+ - Path (project-nested): \`~/.syntaur/projects/<project>/assignments/<your-assignment>/\`
9419
+ - Path (standalone): \`~/.syntaur/assignments/<your-assignment-uuid>/\`
9420
+ 2. **Shared resources and memories** at the project level:
9421
+ - \`~/.syntaur/projects/<project>/resources/<slug>.md\`
9422
+ - \`~/.syntaur/projects/<project>/memories/<slug>.md\`
9423
+ 3. **Your workspace** -- source code files in the current working directory (the directory where this adapter file lives). If your assignment's frontmatter specifies a \`workspace\` field, read it at runtime to determine the exact boundary.
9424
+
9425
+ > **Note:** The \`setup-adapter\` command does not parse assignment frontmatter for workspace paths. Workspace boundaries are resolved by the agent at runtime by reading \`assignment.md\` frontmatter. If no \`workspace\` field is set, treat the current working directory as your workspace.
9426
+
9427
+ ### Files written only via CLI (never edit directly):
9428
+ - \`comments.md\` (any assignment) -- use \`syntaur comment <slug-or-uuid> "body" [--type question|note|feedback] [--reply-to <id>]\`
9429
+ - Another assignment's \`## Todos\` section -- use \`syntaur request <source> <target> "text"\` to request cross-assignment work
9430
+
9431
+ ### Files you must NEVER write:
9432
+ 1. \`project.md\` -- human-authored, read-only
9433
+ 2. \`manifest.md\` -- derived, rebuilt by tooling
9434
+ 3. Any file prefixed with \`_\` -- derived
9435
+ 4. Other agents' assignment folders (except via the CLI-mediated channels above)
9436
+ 5. Any files outside your workspace boundary
9437
+
9438
+ ## Assignment Lifecycle
9439
+
9440
+ | Status | Meaning |
9441
+ |--------|---------|
9442
+ | \`pending\` | Not yet started |
9443
+ | \`in_progress\` | Actively being worked on |
9444
+ | \`blocked\` | Manually blocked (requires blockedReason) |
9445
+ | \`review\` | Work complete, awaiting review |
9446
+ | \`completed\` | Done |
9447
+ | \`failed\` | Could not be completed |
9448
+
9449
+ ## Valid State Transitions
9450
+
9451
+ | From | Command | To |
9452
+ |------|---------|-----|
9453
+ | pending | start | in_progress |
9454
+ | pending | block | blocked |
9455
+ | in_progress | block | blocked |
9456
+ | in_progress | review | review |
9457
+ | in_progress | complete | completed |
9458
+ | in_progress | fail | failed |
9459
+ | blocked | unblock | in_progress |
9460
+ | review | start | in_progress |
9461
+ | review | complete | completed |
9462
+ | review | fail | failed |
9463
+
9464
+ ## Lifecycle Commands
9465
+
9466
+ Use the \`syntaur\` CLI for state transitions and coordination:
9467
+ - \`syntaur assign <slug> --agent <name> --project <project>\` -- set assignee
9468
+ - \`syntaur start <slug> --project <project>\` -- pending -> in_progress
9469
+ - \`syntaur review <slug> --project <project>\` -- in_progress -> review
9470
+ - \`syntaur complete <slug> --project <project>\` -- in_progress/review -> completed
9471
+ - \`syntaur block <slug> --project <project> --reason <text>\` -- block an assignment
9472
+ - \`syntaur unblock <slug> --project <project>\` -- unblock
9473
+ - \`syntaur fail <slug> --project <project>\` -- mark as failed
9474
+ - \`syntaur create-assignment "Title" [--type <type>] [--project <slug> | --one-off]\` -- create project-nested or standalone assignment
9475
+ - \`syntaur comment <slug-or-uuid> "body" --type question|note|feedback [--reply-to <id>]\` -- append to \`comments.md\` (questions support resolve toggle via dashboard)
9476
+ - \`syntaur request <source> <target> "text"\` -- append a todo to another assignment's \`## Todos\` annotated \`(from: <source>)\`
9477
+
9478
+ ## Playbooks
9479
+
9480
+ Playbooks are user-defined behavioral rules stored in \`~/.syntaur/playbooks/\`. Read the playbook manifest before starting work:
9481
+
9482
+ \`\`\`bash
9483
+ cat ~/.syntaur/playbooks/manifest.md
9484
+ \`\`\`
9485
+
9486
+ Follow the rules in each playbook. They take precedence over default conventions when they conflict.
9487
+
9488
+ ## Conventions
9489
+
9490
+ - Assignment frontmatter is the single source of truth for state. \`project\` is the containing project slug (\`null\` for standalone); \`type\` is a classification validated against \`config.md\` \`types.definitions\` when present.
9491
+ - Slugs are lowercase, hyphen-separated. Standalone assignment folders are named by UUID; \`slug\` is display-only in that case.
9492
+ - Always read \`project.md\` at the project level (when project-nested) before starting work.
9493
+ - Append timestamped entries to \`progress.md\` (never to \`assignment.md\`).
9494
+ - Record questions, notes, and feedback via \`syntaur comment\`. Never edit \`comments.md\` directly.
9495
+ - To route work to another assignment, use \`syntaur request\`.
9496
+ - Commit frequently with messages referencing the assignment slug.
9497
+ `;
9498
+ }
9499
+ function renderCursorAssignment(params2) {
9500
+ return `---
9501
+ description: Syntaur assignment context for ${params2.projectSlug}/${params2.assignmentSlug}
9502
+ globs:
9503
+ alwaysApply: true
9504
+ ---
9505
+
9506
+ # Current Assignment Context
9507
+
9508
+ - **Project:** ${params2.projectSlug}
9509
+ - **Assignment:** ${params2.assignmentSlug}
9510
+ - **Project directory:** ${params2.projectDir}
9511
+ - **Assignment directory:** ${params2.assignmentDir}
9512
+
9513
+ ## Reading Order
9514
+
9515
+ Before starting work, read these files in order:
9516
+ 1. \`${params2.projectDir}/project.md\` -- project overview and goals (project-nested assignments only)
9517
+ 2. \`${params2.assignmentDir}/assignment.md\` -- your assignment details, acceptance criteria, todos, current status. Frontmatter includes \`project: <slug> | null\` (null for standalone) and \`type: <classification> | null\`.
9518
+ 3. any \`${params2.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
9519
+ 4. \`${params2.assignmentDir}/progress.md\` -- reverse-chron progress log (if present)
9520
+ 5. \`${params2.assignmentDir}/comments.md\` -- threaded questions/notes/feedback (if present)
9521
+ 6. \`${params2.assignmentDir}/handoff.md\` -- cross-ticket outbound history (entries from prior agents/humans handing this assignment off)
9522
+ 7. The latest \`${params2.assignmentDir}/sessions/<sid>/summary.md\` if present -- previous-session continuity (selected by \`summary.md\` file mtime; read it for "what was done / what's next" before resuming work in flight)
9523
+
9524
+ ## Your Writable Files
9525
+
9526
+ You may write directly to these files inside your assignment folder:
9527
+ - \`${params2.assignmentDir}/assignment.md\`
9528
+ - \`${params2.assignmentDir}/plan*.md\` (0 or more versioned plan files, e.g., \`plan.md\`, \`plan-v2.md\`)
9529
+ - \`${params2.assignmentDir}/progress.md\` (append timestamped entries, newest first)
9530
+ - \`${params2.assignmentDir}/scratchpad.md\`
9531
+ - \`${params2.assignmentDir}/handoff.md\`
9532
+ - \`${params2.assignmentDir}/decision-record.md\`
9533
+ - \`${params2.assignmentDir}/sessions/<session-id>/summary.md\` (per-session continuity)
9534
+
9535
+ Do NOT edit \`${params2.assignmentDir}/comments.md\` directly \u2014 use \`syntaur comment\`. Do NOT edit other assignments' files \u2014 use \`syntaur request\` for cross-assignment todos.
9536
+
9537
+ And source code files in your workspace. Read the \`workspace\` field from your assignment's frontmatter to determine the exact boundary. If not set, the current working directory is your workspace.
9538
+ `;
9539
+ }
9540
+ var init_cursor_rules = __esm({
9541
+ "src/templates/cursor-rules.ts"() {
9542
+ "use strict";
9543
+ }
9544
+ });
9545
+
9546
+ // src/templates/codex-agents.ts
9547
+ function renderCodexAgents(params2) {
9548
+ return `# Syntaur Protocol -- Agent Instructions
9549
+
9550
+ This project uses the Syntaur protocol for multi-agent project coordination.
9551
+
9552
+ ## Current Assignment
9553
+
9554
+ - **Project:** ${params2.projectSlug}
9555
+ - **Assignment:** ${params2.assignmentSlug}
9556
+ - **Project directory:** ${params2.projectDir}
9557
+ - **Assignment directory:** ${params2.assignmentDir}
9558
+
9559
+ ## Preferred Workflow
9560
+
9561
+ If the global Syntaur Codex plugin is installed, prefer these workflows instead of ad hoc protocol edits:
9562
+
9563
+ - \`syntaur-operator\` agent -- use for broad Syntaur protocol work or when a task spans multiple lifecycle steps
9564
+ - \`syntaur-protocol\` -- background protocol and write-boundary rules
9565
+ - \`create-project\` -- scaffold a project
9566
+ - \`create-assignment\` -- create a new assignment (use \`--type <bug|feature|chore|...>\` to classify; use \`--one-off\` to create a standalone assignment at \`~/.syntaur/assignments/<uuid>/\` with no parent project)
9567
+ - \`grab-assignment\` -- claim work, create \`.syntaur/context.json\`, and register a session
9568
+ - \`plan-assignment\` -- write a versioned plan file (\`plan.md\`, \`plan-v2.md\`, ...) and link it from the \`## Todos\` section of \`assignment.md\`
9569
+ - \`complete-assignment\` -- write the cross-ticket \`handoff.md\` entry, append a final entry to \`progress.md\`, close the session, and transition state
9570
+ - \`save-session-summary\` -- write per-session continuity at \`<assignmentDir>/sessions/<sessionId>/summary.md\` for resume across sessions of the same agent. Codex has no \`PreCompact\` hook event \u2014 invoke this manually before compaction or session end.
9571
+ - \`capture-artifacts\` -- capture typed proof artifacts (screenshot/video/asciinema/http/text) for the active assignment. Criterion linkage is optional. Run \`syntaur proof build\` to render \`proof.html\`.
9572
+ - \`resume-session\` -- counterpart to \`save-session-summary\`; loads the latest summary, \`.syntaur/context.json\`, and any open handoff so a fresh session re-orients without re-reading the transcript
9573
+ - \`replan\` -- bump the active assignment to a new \`plan-v<N>.md\` per the Plan Versioning playbook (CLI does file ops, skill writes the body)
9574
+ - \`syntaur-worktree\` -- atomic worktree creation under \`<repository>/.worktrees/<branch>\` plus assign + start + context binding in one move
9575
+ - \`add-resource\` -- register a project-level resource (link to dashboard / doc / ticket); CLI regenerates \`_index.md\` server-side
9576
+ - \`add-memory\` -- capture a project-level Syntaur memory; CLI regenerates \`_index.md\` server-side (distinct from user-global Claude Code auto-memory)
9577
+ - \`list-assignments\` -- cross-project listing with filters by status, project, tag, age (scriptable; not the interactive \`browse\` TUI)
9578
+ - \`log-progress\` -- append a timestamped entry to the active \`progress.md\` and bump frontmatter (Keep Records Updated playbook)
9579
+ - \`set-workspace\` -- populate the four \`workspace.*\` fields in \`assignment.md\`; validates via \`syntaur doctor --assignment --json\` before writing
9580
+ - \`track-session\` -- manage tracked tmux sessions for the dashboard
9581
+
9582
+ If the plugin is unavailable, follow the same workflow manually with the \`syntaur\` CLI and keep the protocol files current yourself.
9583
+
9584
+ ## Reading Order
9585
+
9586
+ Before starting work, read these files in order:
9587
+ 1. \`${params2.projectDir}/manifest.md\` -- root navigation entry point (project-nested assignments only)
9588
+ 2. \`${params2.projectDir}/project.md\` -- project overview and goals (project-nested assignments only)
9589
+ 3. \`${params2.assignmentDir}/assignment.md\` -- your assignment details, acceptance criteria, todos, current status. Frontmatter now includes \`project: <slug> | null\` (null for standalone) and \`type: <classification> | null\`.
9590
+ 4. any \`${params2.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
9591
+ 5. \`${params2.assignmentDir}/progress.md\` -- reverse-chron progress log (if present)
9592
+ 6. \`${params2.assignmentDir}/comments.md\` -- threaded questions/notes/feedback (if present)
9593
+ 7. \`${params2.assignmentDir}/handoff.md\` -- cross-ticket outbound history (entries from prior agents/humans handing this assignment off)
9594
+ 8. The latest \`${params2.assignmentDir}/sessions/<sid>/summary.md\` if present -- previous-session continuity (read it for "what was done / what's next" before resuming work in flight)
9595
+
9596
+ ## Context File
9597
+
9598
+ - Treat \`.syntaur/context.json\` in the current working directory as the active assignment context when it exists.
9599
+ - Use that file to resolve the workspace boundary, assignment path, and project path (the active assignment binding). The active session id, however, is resolved from *your* running process -- prefer \`$CLAUDE_CODE_SESSION_ID\` (or the peer \`OPENCODE_SESSION_ID\` / \`PI_SESSION_ID\`), otherwise run \`syntaur session resolve-id\`; the \`sessionId\` scalar in context.json is only a clobberable legacy hint, not authoritative.
9600
+ - If there is no context file yet and you are supposed to work on an assignment, claim or set up the assignment before editing code.
9601
+
9602
+ ## Directory Structure
9603
+
9604
+ \`\`\`
9605
+ ~/.syntaur/
9606
+ config.md
9607
+ projects/
9608
+ <project-slug>/
9609
+ manifest.md # Derived: root navigation (read-only)
9610
+ project.md # Human-authored: project overview (read-only)
9611
+ _index-assignments.md # Derived (read-only)
9612
+ _index-plans.md # Derived (read-only)
9613
+ _index-decisions.md # Derived (read-only)
9614
+ _status.md # Derived (read-only)
9615
+ assignments/
9616
+ <assignment-slug>/
9617
+ assignment.md # Agent-writable: source of truth for state (includes ## Todos)
9618
+ plan*.md # Agent-writable: versioned implementation plans (optional, one per ## Todos entry)
9619
+ progress.md # Agent-writable, append-only: timestamped progress log
9620
+ comments.md # CLI-mediated: threaded questions/notes/feedback (via \`syntaur comment\`)
9621
+ scratchpad.md # Agent-writable: working notes
9622
+ handoff.md # Agent-writable: append-only cross-ticket outbound at completion
9623
+ decision-record.md # Agent-writable: append-only decision log
9624
+ sessions/
9625
+ <session-id>/
9626
+ summary.md # Agent-writable: per-session continuity (single doc, overwritten)
9627
+ resources/
9628
+ _index.md # Derived (read-only)
9629
+ <resource-slug>.md # Shared-writable
9630
+ memories/
9631
+ _index.md # Derived (read-only)
9632
+ <memory-slug>.md # Shared-writable
9633
+ assignments/
9634
+ <assignment-id>/ # Standalone assignments \u2014 folder = UUID, \`project: null\`, slug display-only
9635
+ assignment.md
9636
+ plan*.md
9637
+ progress.md
9638
+ comments.md
9639
+ scratchpad.md
9640
+ handoff.md
9641
+ decision-record.md
9642
+ sessions/<session-id>/summary.md # Per-session continuity (same as project-nested)
9643
+ \`\`\`
9644
+
9645
+ ## Write Boundary Rules (CRITICAL)
9646
+
9647
+ ### Files you may WRITE:
9648
+ 1. **Your assignment folder** -- only the assignment you are currently working on:
9649
+ - \`assignment.md\`, \`plan*.md\` (0 or more versioned plan files), \`progress.md\`, \`scratchpad.md\`, \`handoff.md\` (cross-ticket outbound at completion), \`decision-record.md\`
9650
+ - \`sessions/<session-id>/summary.md\` -- per-session continuity (single doc per session id, overwritten on save). Distinct from \`handoff.md\`.
9651
+ - Path: \`${params2.assignmentDir}/\`
9652
+ 2. **Shared resources and memories** at the project level:
9653
+ - \`${params2.projectDir}/resources/<slug>.md\`
9654
+ - \`${params2.projectDir}/memories/<slug>.md\`
9655
+ 3. **Your workspace** -- source code files in the current working directory (the directory where this AGENTS.md lives). If your assignment's frontmatter specifies a \`workspace\` field, read it at runtime to determine the exact boundary.
9656
+
9657
+ > **Note:** Workspace boundaries are resolved by the agent at runtime by reading \`assignment.md\` frontmatter. If no \`workspace\` field is set, treat the current working directory as your workspace.
9658
+
9659
+ ### Files written only via CLI (never edit directly):
9660
+ - \`comments.md\` (any assignment) -- use \`syntaur comment <slug-or-uuid> "body" [--type question|note|feedback] [--reply-to <id>]\`
9661
+ - Another assignment's \`## Todos\` section -- use \`syntaur request <source> <target> "text"\` to request cross-assignment work
9662
+
9663
+ ### Files you must NEVER write:
9664
+ 1. \`project.md\` -- human-authored, read-only
9665
+ 2. \`manifest.md\` -- derived, rebuilt by tooling
9666
+ 3. Any file prefixed with \`_\` -- derived
9667
+ 4. Other agents' assignment folders (except via the CLI-mediated channels above)
9668
+ 5. Any files outside your workspace boundary
9669
+
9670
+ ## Assignment Lifecycle
9671
+
9672
+ | Status | Meaning |
9673
+ |--------|---------|
9674
+ | \`pending\` | Not yet started |
9675
+ | \`in_progress\` | Actively being worked on |
9676
+ | \`blocked\` | Manually blocked (requires blockedReason) |
9677
+ | \`review\` | Work complete, awaiting review |
9678
+ | \`completed\` | Done |
9679
+ | \`failed\` | Could not be completed |
9680
+
9681
+ ## Valid State Transitions
9682
+
9683
+ | From | Command | To |
9684
+ |------|---------|-----|
9685
+ | pending | start | in_progress |
9686
+ | pending | block | blocked |
9687
+ | in_progress | block | blocked |
9688
+ | in_progress | review | review |
9689
+ | in_progress | complete | completed |
9690
+ | in_progress | fail | failed |
9691
+ | blocked | unblock | in_progress |
9692
+ | review | start | in_progress |
9693
+ | review | complete | completed |
9694
+ | review | fail | failed |
9695
+
9696
+ ## Lifecycle Commands
9697
+
9698
+ Use the \`syntaur\` CLI for state transitions and coordination:
9699
+ - \`syntaur assign ${params2.assignmentSlug} --agent <name> --project ${params2.projectSlug}\` -- set assignee
9700
+ - \`syntaur start ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- pending -> in_progress
9701
+ - \`syntaur review ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- in_progress -> review
9702
+ - \`syntaur complete ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- in_progress/review -> completed
9703
+ - \`syntaur block ${params2.assignmentSlug} --project ${params2.projectSlug} --reason <text>\` -- block
9704
+ - \`syntaur unblock ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- unblock
9705
+ - \`syntaur fail ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- mark as failed
9706
+ - \`syntaur comment ${params2.assignmentSlug} "body" --type question|note|feedback [--reply-to <id>]\` -- append to \`comments.md\` (use for all Q&A; questions support resolve toggle)
9707
+ - \`syntaur request ${params2.assignmentSlug} <target-slug-or-uuid> "text"\` -- append a todo to another assignment's \`## Todos\` annotated \`(from: ${params2.assignmentSlug})\`
9708
+ - \`syntaur capture --kind <screenshot|video|asciinema|http|text> [--file <path>] [--criterion <index>] [--note <text>] [--transcribe] ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- record a proof artifact. \`--kind=text\` requires \`--note\` and forbids \`--file\`. Criterion linkage is optional. \`--transcribe\` is video-only and writes a sibling \`<id>.transcript.md\` (requires \`ELEVENLABS_API_KEY\` + \`ffmpeg\`).
9709
+ - \`syntaur proof build ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- render \`proof.html\` and \`proof.md\` at the assignment dir. Atomic overwrite \u2014 safe to re-run.
9710
+
9711
+ ## Troubleshooting
9712
+
9713
+ If Syntaur state looks inconsistent (missing files, stale manifests, unexpected hook blocks), run \`syntaur doctor\` to diagnose. Use \`--json\` for structured output.
9714
+
9715
+ ## Playbooks
9716
+
9717
+ Playbooks are user-defined behavioral rules stored in \`~/.syntaur/playbooks/\`. Before starting work, read the playbook manifest and then each referenced playbook:
9718
+
9719
+ \`\`\`bash
9720
+ cat ~/.syntaur/playbooks/manifest.md
9721
+ \`\`\`
9722
+
9723
+ Read each linked playbook and follow the rules in its body section. The \`when_to_use\` field tells you when each playbook applies. Playbooks take precedence over default conventions when they conflict.
9724
+
9725
+ ## Conventions
9726
+
9727
+ - Assignment frontmatter is the single source of truth for state. \`project\` is the containing project slug (\`null\` for standalone); \`type\` is a classification validated against \`config.md\` \`types.definitions\` when present.
9728
+ - Slugs are lowercase, hyphen-separated. For standalone assignments, \`slug\` is display-only; the folder is named by the UUID.
9729
+ - Always read \`project.md\` at the project level (when project-nested) before starting work.
9730
+ - Keep \`assignment.md\` acceptance criteria and \`## Todos\` updated as work lands; append timestamped entries to \`progress.md\` (never to \`assignment.md\`).
9731
+ - Keep active plan file(s) current after planning changes. Write \`handoff.md\` (via \`complete-assignment\`) at the cross-ticket boundary; write \`sessions/<sid>/summary.md\` (via \`/save-session-summary\`) before compaction or before ending a session mid-assignment so a future session can resume cleanly.
9732
+ - When requirements shift, supersede the prior plan todo (\`- [x] ~~...~~ (superseded by plan-v<N>)\`) and write a new plan file instead of rewriting the old one.
9733
+ - Record questions, notes, and feedback via \`syntaur comment\`. Never edit \`comments.md\` directly. Resolve questions via the dashboard UI (toggle on the question entry).
9734
+ - To route work to another assignment, use \`syntaur request\`.
9735
+ - Commit frequently with messages referencing the assignment slug.
9736
+ `;
9737
+ }
9738
+ var init_codex_agents = __esm({
9739
+ "src/templates/codex-agents.ts"() {
9740
+ "use strict";
9741
+ }
9742
+ });
9743
+
9744
+ // src/templates/opencode-config.ts
9745
+ function renderOpenCodeConfig(params2) {
9746
+ const config = {
9747
+ instructions: [
9748
+ `Read AGENTS.md in this directory for Syntaur protocol (v2.0) instructions.`,
9749
+ `Read ${params2.projectDir}/project.md for project overview (project-nested assignments only).`,
9750
+ `Append timestamped progress entries to the assignment's progress.md (not to assignment.md).`,
9751
+ `Use 'syntaur comment <slug-or-uuid> "body" --type question|note|feedback' to append to comments.md \u2014 never edit it directly.`,
9752
+ `Use 'syntaur request <source> <target> "text"' to append a todo to another assignment's ## Todos.`,
9753
+ `Assignment folders are project-nested at ~/.syntaur/projects/<slug>/assignments/<aslug>/ or standalone at ~/.syntaur/assignments/<uuid>/ (project: null, slug display-only).`
9754
+ ]
9755
+ };
9756
+ return JSON.stringify(config, null, 2) + "\n";
9757
+ }
9758
+ var init_opencode_config = __esm({
9759
+ "src/templates/opencode-config.ts"() {
9760
+ "use strict";
9761
+ }
9762
+ });
9763
+
9764
+ // src/templates/hermes-soul.ts
9765
+ function renderHermesSoul(params2) {
9766
+ const body = renderCodexAgents(params2);
9767
+ return `# SOUL -- Syntaur Protocol Operator
9768
+
9769
+ This agent follows the Syntaur protocol for multi-agent project coordination.
9770
+ Hermes loads this file as part of its identity / system context; treat the
9771
+ Write Boundary Rules and Lifecycle sections below as binding.
9772
+
9773
+ ${body}`;
9774
+ }
9775
+ var init_hermes_soul = __esm({
9776
+ "src/templates/hermes-soul.ts"() {
9777
+ "use strict";
9778
+ init_codex_agents();
9779
+ }
9780
+ });
9781
+
9209
9782
  // src/lifecycle/recompute.ts
9210
9783
  var recompute_exports = {};
9211
9784
  __export(recompute_exports, {
@@ -9445,20 +10018,370 @@ var LOCK_FILE, LOCK_STALE_MS, LOCK_WAIT_MS, LOCK_MAX_WAITS, CAS_RETRIES, MIGRATI
9445
10018
  var init_recompute = __esm({
9446
10019
  "src/lifecycle/recompute.ts"() {
9447
10020
  "use strict";
9448
- init_config2();
9449
- init_fs();
9450
- init_paths();
9451
- init_timestamp();
9452
- init_facts();
9453
- init_derive();
9454
- init_frontmatter();
9455
- init_types();
9456
- LOCK_FILE = ".derive.lock";
9457
- LOCK_STALE_MS = 3e4;
9458
- LOCK_WAIT_MS = 50;
9459
- LOCK_MAX_WAITS = 100;
9460
- CAS_RETRIES = 3;
9461
- MIGRATION_MARKER = "derive-migrated";
10021
+ init_config2();
10022
+ init_fs();
10023
+ init_paths();
10024
+ init_timestamp();
10025
+ init_facts();
10026
+ init_derive();
10027
+ init_frontmatter();
10028
+ init_types();
10029
+ LOCK_FILE = ".derive.lock";
10030
+ LOCK_STALE_MS = 3e4;
10031
+ LOCK_WAIT_MS = 50;
10032
+ LOCK_MAX_WAITS = 100;
10033
+ CAS_RETRIES = 3;
10034
+ MIGRATION_MARKER = "derive-migrated";
10035
+ }
10036
+ });
10037
+
10038
+ // src/utils/transcript.ts
10039
+ import { open as open2 } from "fs/promises";
10040
+ async function derivePathFromTranscript(transcriptPath) {
10041
+ if (!transcriptPath) return null;
10042
+ let handle;
10043
+ try {
10044
+ handle = await open2(transcriptPath, "r");
10045
+ } catch {
10046
+ return null;
10047
+ }
10048
+ try {
10049
+ const stream = handle.createReadStream({ encoding: "utf-8" });
10050
+ let buffer = "";
10051
+ let scanned = 0;
10052
+ for await (const chunk of stream) {
10053
+ buffer += chunk;
10054
+ let nl = buffer.indexOf("\n");
10055
+ while (nl !== -1) {
10056
+ const line = buffer.slice(0, nl);
10057
+ buffer = buffer.slice(nl + 1);
10058
+ const cwd = extractCwd(line);
10059
+ if (cwd) {
10060
+ stream.destroy();
10061
+ return cwd;
10062
+ }
10063
+ scanned++;
10064
+ if (scanned >= MAX_LINES_SCANNED) {
10065
+ stream.destroy();
10066
+ return null;
10067
+ }
10068
+ nl = buffer.indexOf("\n");
10069
+ }
10070
+ }
10071
+ if (buffer.length > 0) {
10072
+ const cwd = extractCwd(buffer);
10073
+ if (cwd) return cwd;
10074
+ }
10075
+ return null;
10076
+ } finally {
10077
+ await handle.close().catch(() => {
10078
+ });
10079
+ }
10080
+ }
10081
+ function extractCwd(line) {
10082
+ const trimmed = line.trim();
10083
+ if (trimmed.length === 0 || trimmed[0] !== "{") return null;
10084
+ try {
10085
+ const parsed = JSON.parse(trimmed);
10086
+ if (typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
10087
+ return parsed.cwd;
10088
+ }
10089
+ } catch {
10090
+ }
10091
+ return null;
10092
+ }
10093
+ var MAX_LINES_SCANNED;
10094
+ var init_transcript = __esm({
10095
+ "src/utils/transcript.ts"() {
10096
+ "use strict";
10097
+ MAX_LINES_SCANNED = 50;
10098
+ }
10099
+ });
10100
+
10101
+ // src/utils/process-info.ts
10102
+ import { execFileSync as execFileSync2 } from "child_process";
10103
+ function captureProcessStartedAt(pid) {
10104
+ if (!Number.isFinite(pid) || pid <= 0) return null;
10105
+ try {
10106
+ const out = execFileSync2("ps", ["-o", "lstart=", "-p", String(pid)], {
10107
+ encoding: "utf8",
10108
+ stdio: ["ignore", "pipe", "ignore"]
10109
+ });
10110
+ const trimmed = out.trim();
10111
+ return trimmed === "" ? null : trimmed;
10112
+ } catch {
10113
+ return null;
10114
+ }
10115
+ }
10116
+ var init_process_info = __esm({
10117
+ "src/utils/process-info.ts"() {
10118
+ "use strict";
10119
+ }
10120
+ });
10121
+
10122
+ // src/usage/cwd-extractor.ts
10123
+ import { open as open3, readdir as readdir11, stat as stat2 } from "fs/promises";
10124
+ import { join as join3 } from "path";
10125
+ import { homedir as homedir3 } from "os";
10126
+ async function extractClaudeSessionMeta(jsonlPath) {
10127
+ const cwd = await derivePathFromTranscript(jsonlPath);
10128
+ if (!cwd) return null;
10129
+ const basename6 = jsonlPath.split("/").pop() ?? "";
10130
+ const sessionId = basename6.replace(/\.jsonl$/, "");
10131
+ if (!sessionId) return null;
10132
+ const startTs = await readFirstTimestamp(jsonlPath);
10133
+ const endTs = await readLastTimestamp(jsonlPath);
10134
+ return {
10135
+ tool: "claude",
10136
+ sessionId,
10137
+ cwd,
10138
+ startTs,
10139
+ endTs,
10140
+ path: jsonlPath
10141
+ };
10142
+ }
10143
+ async function extractCodexSessionMeta(jsonlPath) {
10144
+ let handle;
10145
+ try {
10146
+ handle = await open3(jsonlPath, "r");
10147
+ } catch {
10148
+ return null;
10149
+ }
10150
+ try {
10151
+ const stream = handle.createReadStream({ encoding: "utf-8" });
10152
+ let buffer = "";
10153
+ let firstLine = null;
10154
+ for await (const chunk of stream) {
10155
+ buffer += chunk;
10156
+ const nl = buffer.indexOf("\n");
10157
+ if (nl !== -1) {
10158
+ firstLine = buffer.slice(0, nl);
10159
+ stream.destroy();
10160
+ break;
10161
+ }
10162
+ }
10163
+ if (!firstLine && buffer.length > 0) firstLine = buffer;
10164
+ if (!firstLine) return null;
10165
+ let parsed;
10166
+ try {
10167
+ parsed = JSON.parse(firstLine);
10168
+ } catch {
10169
+ return null;
10170
+ }
10171
+ if (!parsed || typeof parsed !== "object") return null;
10172
+ const obj = parsed;
10173
+ if (obj.type !== "session_meta") return null;
10174
+ const timestamp = typeof obj.timestamp === "string" ? obj.timestamp : null;
10175
+ const payload = obj.payload;
10176
+ const id = payload && typeof payload.id === "string" ? payload.id : null;
10177
+ const cwd = payload && typeof payload.cwd === "string" ? payload.cwd : null;
10178
+ if (!timestamp || !id || !cwd) return null;
10179
+ const endTs = await readLastTimestamp(jsonlPath) ?? timestamp;
10180
+ return {
10181
+ tool: "codex",
10182
+ sessionId: id,
10183
+ cwd,
10184
+ startTs: timestamp,
10185
+ endTs,
10186
+ path: jsonlPath
10187
+ };
10188
+ } finally {
10189
+ await handle.close().catch(() => {
10190
+ });
10191
+ }
10192
+ }
10193
+ async function* walkClaudeProjects(opts = {}) {
10194
+ const root = expandHome(opts.root ?? "~/.claude/projects");
10195
+ const dirs = await listDirSafe(root);
10196
+ for (const dirent of dirs) {
10197
+ if (!dirent.isDirectory) continue;
10198
+ const dirPath = join3(root, dirent.name);
10199
+ const files = await listDirSafe(dirPath);
10200
+ let cachedCwd = null;
10201
+ for (const f of files) {
10202
+ if (!f.isFile || !f.name.endsWith(".jsonl")) continue;
10203
+ const filePath = join3(dirPath, f.name);
10204
+ if (opts.sinceMtimeMs !== void 0) {
10205
+ const mtime = await mtimeMs(filePath);
10206
+ if (mtime !== null && mtime < opts.sinceMtimeMs) continue;
10207
+ }
10208
+ let meta;
10209
+ if (cachedCwd) {
10210
+ const sessionId = f.name.replace(/\.jsonl$/, "");
10211
+ const startTs = await readFirstTimestamp(filePath);
10212
+ const endTs = await readLastTimestamp(filePath);
10213
+ meta = { tool: "claude", sessionId, cwd: cachedCwd, startTs, endTs, path: filePath };
10214
+ } else {
10215
+ meta = await extractClaudeSessionMeta(filePath);
10216
+ if (meta) cachedCwd = meta.cwd;
10217
+ }
10218
+ if (meta) yield meta;
10219
+ }
10220
+ }
10221
+ }
10222
+ async function* walkCodexSessions(opts = {}) {
10223
+ const root = resolveCodexSessionsRoot(opts.root);
10224
+ for await (const filePath of walkJsonlRecursive(root)) {
10225
+ const basename6 = filePath.split("/").pop() ?? "";
10226
+ if (!basename6.endsWith(".jsonl")) continue;
10227
+ if (opts.sinceMtimeMs !== void 0) {
10228
+ const mtime = await mtimeMs(filePath);
10229
+ if (mtime !== null && mtime < opts.sinceMtimeMs) continue;
10230
+ }
10231
+ const meta = await extractCodexSessionMeta(filePath);
10232
+ if (meta) yield meta;
10233
+ }
10234
+ }
10235
+ function resolveCodexSessionsRoot(override) {
10236
+ if (override) return expandHome(override);
10237
+ const fromSessionsEnv = process.env.CODEX_SESSIONS_DIR;
10238
+ if (fromSessionsEnv && fromSessionsEnv.length > 0) return expandHome(fromSessionsEnv);
10239
+ const fromHomeEnv = process.env.CODEX_HOME;
10240
+ if (fromHomeEnv && fromHomeEnv.length > 0) return join3(expandHome(fromHomeEnv), "sessions");
10241
+ return join3(homedir3(), ".codex", "sessions");
10242
+ }
10243
+ async function listDirSafe(path) {
10244
+ try {
10245
+ const entries = await readdir11(path, { withFileTypes: true });
10246
+ return entries.map((e) => ({
10247
+ name: e.name,
10248
+ isFile: e.isFile(),
10249
+ isDirectory: e.isDirectory()
10250
+ }));
10251
+ } catch {
10252
+ return [];
10253
+ }
10254
+ }
10255
+ async function* walkJsonlRecursive(root) {
10256
+ const stack = [root];
10257
+ while (stack.length > 0) {
10258
+ const current = stack.pop();
10259
+ const entries = await listDirSafe(current);
10260
+ for (const e of entries) {
10261
+ const full = join3(current, e.name);
10262
+ if (e.isDirectory) {
10263
+ stack.push(full);
10264
+ } else if (e.isFile && e.name.endsWith(".jsonl")) {
10265
+ yield full;
10266
+ }
10267
+ }
10268
+ }
10269
+ }
10270
+ async function mtimeMs(path) {
10271
+ try {
10272
+ const s = await stat2(path);
10273
+ return s.mtimeMs;
10274
+ } catch {
10275
+ return null;
10276
+ }
10277
+ }
10278
+ async function readFirstTimestamp(path) {
10279
+ let handle;
10280
+ try {
10281
+ handle = await open3(path, "r");
10282
+ } catch {
10283
+ return null;
10284
+ }
10285
+ try {
10286
+ const stream = handle.createReadStream({ encoding: "utf-8" });
10287
+ let buffer = "";
10288
+ let scanned = 0;
10289
+ for await (const chunk of stream) {
10290
+ buffer += chunk;
10291
+ let nl = buffer.indexOf("\n");
10292
+ while (nl !== -1) {
10293
+ const line = buffer.slice(0, nl);
10294
+ buffer = buffer.slice(nl + 1);
10295
+ const ts = extractTimestamp(line);
10296
+ if (ts) {
10297
+ stream.destroy();
10298
+ return ts;
10299
+ }
10300
+ scanned++;
10301
+ if (scanned >= SCAN_LINE_CAP) {
10302
+ stream.destroy();
10303
+ return null;
10304
+ }
10305
+ nl = buffer.indexOf("\n");
10306
+ }
10307
+ }
10308
+ if (buffer.length > 0) return extractTimestamp(buffer);
10309
+ return null;
10310
+ } finally {
10311
+ await handle.close().catch(() => {
10312
+ });
10313
+ }
10314
+ }
10315
+ async function readLastTimestamp(path) {
10316
+ let handle;
10317
+ try {
10318
+ handle = await open3(path, "r");
10319
+ } catch {
10320
+ return null;
10321
+ }
10322
+ try {
10323
+ const stats = await handle.stat();
10324
+ const size = stats.size;
10325
+ if (size === 0) return null;
10326
+ for (const windowBytes of [TAIL_READ_BYTES, TAIL_READ_BYTES_MAX]) {
10327
+ const start = Math.max(0, size - windowBytes);
10328
+ const length = size - start;
10329
+ const buf = Buffer.alloc(length);
10330
+ await handle.read(buf, 0, length, start);
10331
+ const text = buf.toString("utf-8");
10332
+ const lines = text.split("\n");
10333
+ if (start > 0) lines.shift();
10334
+ for (let i = lines.length - 1; i >= 0; i--) {
10335
+ const ts = extractTimestamp(lines[i]);
10336
+ if (ts) return ts;
10337
+ }
10338
+ if (start === 0) break;
10339
+ }
10340
+ return null;
10341
+ } finally {
10342
+ await handle.close().catch(() => {
10343
+ });
10344
+ }
10345
+ }
10346
+ function extractTimestamp(line) {
10347
+ const trimmed = line.trim();
10348
+ if (trimmed.length === 0 || trimmed[0] !== "{") return null;
10349
+ try {
10350
+ const parsed = JSON.parse(trimmed);
10351
+ if (typeof parsed.timestamp === "string" && parsed.timestamp.length > 0) {
10352
+ return parsed.timestamp;
10353
+ }
10354
+ } catch {
10355
+ }
10356
+ return null;
10357
+ }
10358
+ var SCAN_LINE_CAP, TAIL_READ_BYTES, TAIL_READ_BYTES_MAX;
10359
+ var init_cwd_extractor = __esm({
10360
+ "src/usage/cwd-extractor.ts"() {
10361
+ "use strict";
10362
+ init_paths();
10363
+ init_transcript();
10364
+ SCAN_LINE_CAP = 50;
10365
+ TAIL_READ_BYTES = 8 * 1024;
10366
+ TAIL_READ_BYTES_MAX = 64 * 1024;
10367
+ }
10368
+ });
10369
+
10370
+ // src/utils/session-id.ts
10371
+ import { execFileSync as execFileSync3 } from "child_process";
10372
+ import { mkdirSync, readFileSync, statSync as statSync3, writeFileSync } from "fs";
10373
+ import { homedir as homedir4 } from "os";
10374
+ import { dirname as dirname5, join as join4 } from "path";
10375
+ function isSafeSessionId(value) {
10376
+ return typeof value === "string" && value.length > 0 && value.length <= 256 && SAFE_SESSION_ID.test(value);
10377
+ }
10378
+ var SAFE_SESSION_ID;
10379
+ var init_session_id = __esm({
10380
+ "src/utils/session-id.ts"() {
10381
+ "use strict";
10382
+ init_process_info();
10383
+ init_cwd_extractor();
10384
+ SAFE_SESSION_ID = /^[A-Za-z0-9_-]+$/;
9462
10385
  }
9463
10386
  });
9464
10387
 
@@ -9509,6 +10432,420 @@ var init_assignment_todos = __esm({
9509
10432
  }
9510
10433
  });
9511
10434
 
10435
+ // src/targets/renderers.ts
10436
+ var RENDERERS;
10437
+ var init_renderers = __esm({
10438
+ "src/targets/renderers.ts"() {
10439
+ "use strict";
10440
+ init_cursor_rules();
10441
+ init_codex_agents();
10442
+ init_opencode_config();
10443
+ init_hermes_soul();
10444
+ RENDERERS = {
10445
+ codexAgents: (ctx) => renderCodexAgents(ctx),
10446
+ cursorProtocol: () => renderCursorProtocol(),
10447
+ cursorAssignment: (ctx) => renderCursorAssignment(ctx),
10448
+ openCodeConfig: (ctx) => renderOpenCodeConfig({ projectDir: ctx.projectDir }),
10449
+ hermesSoul: (ctx) => renderHermesSoul(ctx)
10450
+ };
10451
+ }
10452
+ });
10453
+
10454
+ // src/targets/user-descriptors.ts
10455
+ import { resolve as resolve34 } from "path";
10456
+ import { readFile as readFile23, readdir as readdir16 } from "fs/promises";
10457
+ var VALID_RENDERER_KEYS;
10458
+ var init_user_descriptors = __esm({
10459
+ "src/targets/user-descriptors.ts"() {
10460
+ "use strict";
10461
+ init_fs();
10462
+ init_paths();
10463
+ init_renderers();
10464
+ VALID_RENDERER_KEYS = new Set(Object.keys(RENDERERS));
10465
+ }
10466
+ });
10467
+
10468
+ // src/targets/registry.ts
10469
+ import { homedir as homedir6 } from "os";
10470
+ import { join as join8, resolve as resolve35 } from "path";
10471
+ function home(...segments) {
10472
+ return resolve35(homedir6(), ...segments);
10473
+ }
10474
+ function hermesHome() {
10475
+ const env = process.env.HERMES_HOME;
10476
+ return env && env.length > 0 ? resolve35(env) : home(".hermes");
10477
+ }
10478
+ function hermesSkillsDir() {
10479
+ return resolve35(hermesHome(), "skills");
10480
+ }
10481
+ function codexHome() {
10482
+ const env = process.env.CODEX_HOME;
10483
+ return env && env.length > 0 ? resolve35(env) : home(".codex");
10484
+ }
10485
+ function toDiscovered(meta) {
10486
+ if (!meta) return null;
10487
+ return {
10488
+ sessionId: meta.sessionId,
10489
+ cwd: meta.cwd,
10490
+ startedAt: meta.startTs,
10491
+ endedAt: meta.endTs,
10492
+ transcriptPath: meta.path
10493
+ };
10494
+ }
10495
+ var detectDir, claudeSessions, codexSessions, AGENT_TARGETS, AGENT_TARGETS_BY_ID;
10496
+ var init_registry = __esm({
10497
+ "src/targets/registry.ts"() {
10498
+ "use strict";
10499
+ init_fs();
10500
+ init_cwd_extractor();
10501
+ init_user_descriptors();
10502
+ detectDir = (dir) => () => fileExists(dir);
10503
+ claudeSessions = {
10504
+ globs: (root) => [join8(root ?? home(".claude", "projects"), "*", "*.jsonl")],
10505
+ parse: async (file) => toDiscovered(await extractClaudeSessionMeta(file)),
10506
+ walk: async function* (opts = {}) {
10507
+ for await (const meta of walkClaudeProjects({ root: opts.root, sinceMtimeMs: opts.sinceMtimeMs })) {
10508
+ const d = toDiscovered(meta);
10509
+ if (d) yield d;
10510
+ }
10511
+ }
10512
+ };
10513
+ codexSessions = {
10514
+ globs: (root) => [join8(root ?? resolveCodexSessionsRoot(), "**", "*.jsonl")],
10515
+ parse: async (file) => toDiscovered(await extractCodexSessionMeta(file)),
10516
+ walk: async function* (opts = {}) {
10517
+ for await (const meta of walkCodexSessions({ root: opts.root, sinceMtimeMs: opts.sinceMtimeMs })) {
10518
+ const d = toDiscovered(meta);
10519
+ if (d) yield d;
10520
+ }
10521
+ }
10522
+ };
10523
+ AGENT_TARGETS = [
10524
+ {
10525
+ id: "cursor",
10526
+ displayName: "Cursor",
10527
+ skillsShAgentId: "cursor",
10528
+ detect: detectDir(home(".cursor")),
10529
+ skillsDir: { global: home(".cursor", "skills") },
10530
+ instructions: {
10531
+ files: [
10532
+ { path: ".cursor/rules/syntaur-protocol.mdc", renderer: "cursorProtocol" },
10533
+ { path: ".cursor/rules/syntaur-assignment.mdc", renderer: "cursorAssignment" }
10534
+ ]
10535
+ }
10536
+ },
10537
+ {
10538
+ // codex is BOTH an adapter (writes AGENTS.md) AND a native plugin.
10539
+ id: "codex",
10540
+ displayName: "Codex",
10541
+ skillsShAgentId: "codex",
10542
+ nativePlugin: "codex",
10543
+ detect: detectDir(codexHome()),
10544
+ skillsDir: { global: resolve35(codexHome(), "skills") },
10545
+ instructions: { files: [{ path: "AGENTS.md", renderer: "codexAgents" }] },
10546
+ sessions: codexSessions
10547
+ },
10548
+ {
10549
+ id: "opencode",
10550
+ displayName: "OpenCode",
10551
+ skillsShAgentId: "opencode",
10552
+ detect: detectDir(home(".config", "opencode")),
10553
+ skillsDir: { global: home(".config", "opencode", "skills") },
10554
+ instructions: {
10555
+ files: [
10556
+ { path: "AGENTS.md", renderer: "codexAgents" },
10557
+ { path: "opencode.json", renderer: "openCodeConfig" }
10558
+ ]
10559
+ }
10560
+ },
10561
+ {
10562
+ // claude has NO adapter today (not in the old SUPPORTED_FRAMEWORKS) — the
10563
+ // full plugin path owns its skills/hooks/commands. Native-plugin only.
10564
+ id: "claude",
10565
+ displayName: "Claude Code",
10566
+ skillsShAgentId: "claude-code",
10567
+ nativePlugin: "claude",
10568
+ detect: detectDir(home(".claude")),
10569
+ skillsDir: { global: home(".claude", "skills") },
10570
+ sessions: claudeSessions
10571
+ },
10572
+ {
10573
+ id: "pi",
10574
+ displayName: "Pi",
10575
+ skillsShAgentId: "pi",
10576
+ detect: detectDir(home(".pi")),
10577
+ skillsDir: { global: home(".pi", "agent", "skills") },
10578
+ instructions: { files: [{ path: "AGENTS.md", renderer: "codexAgents" }] },
10579
+ tier3: {
10580
+ kind: "pi-extension",
10581
+ source: "platforms/pi/extensions/syntaur",
10582
+ installDir: () => home(".pi", "agent", "extensions", "syntaur"),
10583
+ entry: "index.ts"
10584
+ }
10585
+ },
10586
+ {
10587
+ id: "openclaw",
10588
+ displayName: "OpenClaw",
10589
+ skillsShAgentId: "openclaw",
10590
+ detect: detectDir(home(".openclaw")),
10591
+ skillsDir: { global: home(".openclaw", "skills") },
10592
+ instructions: { files: [{ path: "AGENTS.md", renderer: "codexAgents" }] },
10593
+ // OpenClaw runs on pi-coding-agent (design memo), so it reuses the pi
10594
+ // extension SOURCE; only the install dir differs.
10595
+ tier3: {
10596
+ kind: "pi-extension",
10597
+ source: "platforms/pi/extensions/syntaur",
10598
+ installDir: () => home(".openclaw", "extensions", "syntaur"),
10599
+ entry: "index.ts"
10600
+ }
10601
+ },
10602
+ {
10603
+ id: "hermes",
10604
+ displayName: "Hermes Agent",
10605
+ skillsShAgentId: "hermes-agent",
10606
+ detect: () => fileExists(hermesHome()),
10607
+ skillsDir: { global: hermesSkillsDir() },
10608
+ instructions: { files: [{ path: "SOUL.md", renderer: "hermesSoul" }] },
10609
+ tier3: {
10610
+ kind: "hermes-plugin",
10611
+ source: "platforms/hermes/plugins/syntaur",
10612
+ installDir: () => resolve35(hermesHome(), "plugins", "syntaur"),
10613
+ entry: "plugin.yaml"
10614
+ }
10615
+ }
10616
+ ];
10617
+ AGENT_TARGETS_BY_ID = Object.fromEntries(
10618
+ AGENT_TARGETS.map((t) => [t.id, t])
10619
+ );
10620
+ }
10621
+ });
10622
+
10623
+ // src/sessions/scanner.ts
10624
+ var scanner_exports2 = {};
10625
+ __export(scanner_exports2, {
10626
+ scanSessions: () => scanSessions
10627
+ });
10628
+ import { execFile as execFile3, execFileSync as execFileSync4 } from "child_process";
10629
+ import { promisify as promisify3 } from "util";
10630
+ import { statSync as statSync4 } from "fs";
10631
+ import { readFile as readFile24 } from "fs/promises";
10632
+ import { resolve as resolve36 } from "path";
10633
+ function emptySummary() {
10634
+ return { discovered: 0, inserted: 0, revived: 0, swept: 0, skipped: 0, changed: false };
10635
+ }
10636
+ function defaultStatMtimeMs(path) {
10637
+ try {
10638
+ return statSync4(path).mtimeMs;
10639
+ } catch {
10640
+ return null;
10641
+ }
10642
+ }
10643
+ function defaultIsPidAlive(pid) {
10644
+ if (!Number.isFinite(pid) || pid <= 0) return false;
10645
+ try {
10646
+ process.kill(pid, 0);
10647
+ return true;
10648
+ } catch (err) {
10649
+ return err.code === "EPERM";
10650
+ }
10651
+ }
10652
+ function defaultPidStartedAt(pid) {
10653
+ if (!Number.isFinite(pid) || pid <= 0) return null;
10654
+ try {
10655
+ const out = execFileSync4("ps", ["-o", "lstart=", "-p", String(pid)], {
10656
+ encoding: "utf8",
10657
+ stdio: ["ignore", "pipe", "ignore"]
10658
+ });
10659
+ const trimmed = out.trim();
10660
+ return trimmed === "" ? null : trimmed;
10661
+ } catch {
10662
+ return null;
10663
+ }
10664
+ }
10665
+ async function defaultOpenFiles(files) {
10666
+ const open5 = /* @__PURE__ */ new Set();
10667
+ for (let i = 0; i < files.length; i += LSOF_CHUNK) {
10668
+ const chunk = files.slice(i, i + LSOF_CHUNK);
10669
+ let stdout = "";
10670
+ try {
10671
+ const result = await execFileAsync("lsof", ["-Fn", "--", ...chunk], {
10672
+ maxBuffer: 8 * 1024 * 1024
10673
+ });
10674
+ stdout = result.stdout;
10675
+ } catch (err) {
10676
+ const maybe = err.stdout;
10677
+ stdout = typeof maybe === "string" ? maybe : "";
10678
+ }
10679
+ for (const line of stdout.split("\n")) {
10680
+ if (line.startsWith("n") && line.length > 1) open5.add(line.slice(1));
10681
+ }
10682
+ }
10683
+ return open5;
10684
+ }
10685
+ async function readContextLink(cwd, cache2) {
10686
+ if (cache2.has(cwd)) return cache2.get(cwd);
10687
+ let link = null;
10688
+ const path = resolve36(cwd, ".syntaur", "context.json");
10689
+ if (await fileExists(path)) {
10690
+ try {
10691
+ const parsed = JSON.parse(await readFile24(path, "utf-8"));
10692
+ link = {
10693
+ projectSlug: typeof parsed.projectSlug === "string" ? parsed.projectSlug : null,
10694
+ assignmentSlug: typeof parsed.assignmentSlug === "string" ? parsed.assignmentSlug : null
10695
+ };
10696
+ } catch {
10697
+ link = { projectSlug: null, assignmentSlug: null };
10698
+ }
10699
+ }
10700
+ cache2.set(cwd, link);
10701
+ return link;
10702
+ }
10703
+ function readWatermark() {
10704
+ const db4 = getSessionDb();
10705
+ const row = db4.prepare("SELECT value FROM meta WHERE key = ?").get(WATERMARK_KEY);
10706
+ if (!row) return null;
10707
+ const parsed = Number.parseInt(row.value, 10);
10708
+ return Number.isFinite(parsed) ? parsed : null;
10709
+ }
10710
+ function writeWatermark(ms) {
10711
+ const db4 = getSessionDb();
10712
+ db4.prepare(
10713
+ "INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
10714
+ ).run(WATERMARK_KEY, String(ms));
10715
+ }
10716
+ async function scanSessions(opts = {}, deps = {}) {
10717
+ const summary = emptySummary();
10718
+ const autoTrack = deps.autoTrack ?? (await readConfig()).session.autoTrack;
10719
+ if (autoTrack === "off") return summary;
10720
+ const now = deps.now ?? (() => Date.now());
10721
+ const statMtimeMs = deps.statMtimeMs ?? defaultStatMtimeMs;
10722
+ const openFiles = deps.openFiles ?? defaultOpenFiles;
10723
+ const isPidAlive = deps.isPidAlive ?? defaultIsPidAlive;
10724
+ const pidStartedAt = deps.pidStartedAt ?? defaultPidStartedAt;
10725
+ const targets = (deps.targets ?? AGENT_TARGETS).filter((t) => t.sessions !== void 0);
10726
+ const scanStartMs = now();
10727
+ const watermark = opts.full ? null : readWatermark();
10728
+ const discovered = [];
10729
+ for (const target of targets) {
10730
+ const walk = target.sessions.walk({
10731
+ root: deps.roots?.[target.id],
10732
+ sinceMtimeMs: watermark ?? void 0
10733
+ });
10734
+ for await (const session of walk) {
10735
+ if (!isSafeSessionId(session.sessionId)) continue;
10736
+ discovered.push({ ...session, agent: target.id });
10737
+ }
10738
+ }
10739
+ summary.discovered = discovered.length;
10740
+ const openSet = await openFiles(discovered.map((d) => d.transcriptPath));
10741
+ const contextCache = /* @__PURE__ */ new Map();
10742
+ for (const d of discovered) {
10743
+ const link = await readContextLink(d.cwd, contextCache);
10744
+ if (autoTrack === "workspaces-only" && link === null) {
10745
+ summary.skipped += 1;
10746
+ continue;
10747
+ }
10748
+ const mtime = statMtimeMs(d.transcriptPath);
10749
+ const heldOpen = openSet.has(d.transcriptPath);
10750
+ const isLive = heldOpen || mtime !== null && now() - mtime < FRESH_MTIME_MS;
10751
+ const prev = getSessionById(d.sessionId);
10752
+ const status = isLive ? "active" : prev?.status ?? "stopped";
10753
+ const started = d.startedAt ?? (mtime !== null ? new Date(mtime).toISOString() : new Date(now()).toISOString());
10754
+ await appendSession(
10755
+ "",
10756
+ {
10757
+ sessionId: d.sessionId,
10758
+ projectSlug: link?.projectSlug ?? null,
10759
+ assignmentSlug: link?.assignmentSlug ?? null,
10760
+ agent: d.agent,
10761
+ started,
10762
+ status,
10763
+ path: d.cwd,
10764
+ description: null,
10765
+ transcriptPath: d.transcriptPath,
10766
+ pid: null,
10767
+ pidStartedAt: null,
10768
+ originalHeadSha: null
10769
+ },
10770
+ // Narrow revival rule: only LIVE-PROCESS evidence (a process holding the
10771
+ // transcript open) may flip a stopped row back to active. mtime freshness
10772
+ // alone must not — a session stopped moments ago by its SessionEnd hook
10773
+ // still has a fresh transcript for up to 5 minutes and would flap back to
10774
+ // active. `completed` always sticks (appendSession enforces).
10775
+ { reviveStopped: heldOpen }
10776
+ );
10777
+ if (!isLive) {
10778
+ const after = getSessionById(d.sessionId);
10779
+ if (after && after.status === "stopped" && !after.ended) {
10780
+ const endedAt = d.endedAt ?? (mtime !== null ? new Date(mtime).toISOString() : void 0);
10781
+ await updateSessionStatus("", d.sessionId, "stopped", endedAt);
10782
+ }
10783
+ }
10784
+ if (!prev) {
10785
+ summary.inserted += 1;
10786
+ summary.changed = true;
10787
+ } else {
10788
+ if (prev.status === "stopped" && heldOpen) {
10789
+ summary.revived += 1;
10790
+ summary.changed = true;
10791
+ }
10792
+ if (link?.projectSlug && !prev.projectSlug || link?.assignmentSlug && !prev.assignmentSlug) {
10793
+ summary.changed = true;
10794
+ }
10795
+ }
10796
+ }
10797
+ const db4 = getSessionDb();
10798
+ const activeRows = db4.prepare("SELECT session_id, pid, pid_started_at, transcript_path FROM sessions WHERE status = 'active'").all();
10799
+ const sweepCandidates = [];
10800
+ for (const row of activeRows) {
10801
+ if (row.pid !== null) {
10802
+ const alive = isPidAlive(row.pid) && (!row.pid_started_at || (pidStartedAt(row.pid) ?? row.pid_started_at) === row.pid_started_at);
10803
+ if (alive) continue;
10804
+ }
10805
+ if (row.transcript_path) {
10806
+ sweepCandidates.push({ sessionId: row.session_id, transcriptPath: row.transcript_path });
10807
+ } else if (row.pid !== null) {
10808
+ sweepCandidates.push({ sessionId: row.session_id, transcriptPath: null });
10809
+ }
10810
+ }
10811
+ const sweepOpenSet = await openFiles(
10812
+ sweepCandidates.map((c) => c.transcriptPath).filter((p) => p !== null)
10813
+ );
10814
+ for (const candidate of sweepCandidates) {
10815
+ if (candidate.transcriptPath) {
10816
+ if (sweepOpenSet.has(candidate.transcriptPath)) continue;
10817
+ const mtime = statMtimeMs(candidate.transcriptPath);
10818
+ if (mtime !== null && now() - mtime < FRESH_MTIME_MS) continue;
10819
+ const endedAt = mtime !== null ? new Date(mtime).toISOString() : void 0;
10820
+ if (await updateSessionStatus("", candidate.sessionId, "stopped", endedAt)) {
10821
+ summary.swept += 1;
10822
+ summary.changed = true;
10823
+ }
10824
+ } else if (await updateSessionStatus("", candidate.sessionId, "stopped")) {
10825
+ summary.swept += 1;
10826
+ summary.changed = true;
10827
+ }
10828
+ }
10829
+ writeWatermark(scanStartMs);
10830
+ return summary;
10831
+ }
10832
+ var execFileAsync, FRESH_MTIME_MS, LSOF_CHUNK, WATERMARK_KEY;
10833
+ var init_scanner2 = __esm({
10834
+ "src/sessions/scanner.ts"() {
10835
+ "use strict";
10836
+ init_fs();
10837
+ init_config2();
10838
+ init_session_id();
10839
+ init_registry();
10840
+ init_session_db();
10841
+ init_agent_sessions();
10842
+ execFileAsync = promisify3(execFile3);
10843
+ FRESH_MTIME_MS = 5 * 60 * 1e3;
10844
+ LSOF_CHUNK = 64;
10845
+ WATERMARK_KEY = "sessions_scan_last_ms";
10846
+ }
10847
+ });
10848
+
9512
10849
  // src/dashboard/server.ts
9513
10850
  init_paths();
9514
10851
  init_api();
@@ -9516,7 +10853,7 @@ init_assignment_resolver();
9516
10853
  init_agent_sessions();
9517
10854
  import express from "express";
9518
10855
  import { createServer } from "http";
9519
- import { resolve as resolve33 } from "path";
10856
+ import { resolve as resolve37 } from "path";
9520
10857
  import { writeFile as writeFile8, unlink as unlink8 } from "fs/promises";
9521
10858
  import { WebSocketServer, WebSocket } from "ws";
9522
10859
 
@@ -9811,11 +11148,9 @@ function createWatcher(options) {
9811
11148
  debounceKey,
9812
11149
  setTimeout(() => {
9813
11150
  pendingEvents.delete(debounceKey);
9814
- const message = {
9815
- type: "leases-updated",
9816
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
9817
- };
9818
- onMessage(message);
11151
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
11152
+ onMessage({ type: "leases-updated", timestamp });
11153
+ onMessage({ type: "agent-sessions-updated", timestamp });
9819
11154
  }, debounceMs)
9820
11155
  );
9821
11156
  };
@@ -10115,6 +11450,9 @@ function isViewPrefsDefaults(file) {
10115
11450
  // src/dashboard/api-saved-views.ts
10116
11451
  import { Router } from "express";
10117
11452
 
11453
+ // src/utils/view-filters-query.ts
11454
+ init_query();
11455
+
10118
11456
  // src/utils/saved-views-schema.ts
10119
11457
  var TABLE_COLUMN_IDS = [
10120
11458
  "title",
@@ -10208,6 +11546,7 @@ function isViewFilters(value) {
10208
11546
  if (obj.activity !== void 0 && !isActivity(obj.activity)) return false;
10209
11547
  if (obj.dateRange !== void 0 && !isDateRange(obj.dateRange)) return false;
10210
11548
  if (obj.search !== void 0 && typeof obj.search !== "string") return false;
11549
+ if (obj.query !== void 0 && typeof obj.query !== "string") return false;
10211
11550
  return true;
10212
11551
  }
10213
11552
  function isSavedViewConfig(value) {
@@ -10393,6 +11732,8 @@ function withTwoLocks(keyA, keyB, fn) {
10393
11732
  }
10394
11733
 
10395
11734
  // src/dashboard/api-saved-views.ts
11735
+ init_api();
11736
+ init_query();
10396
11737
  var SAVED_VIEWS_LOCK = "sv:global";
10397
11738
  function validateCreateBody(body) {
10398
11739
  if (!body || typeof body !== "object" || Array.isArray(body)) {
@@ -10479,6 +11820,15 @@ function createSavedViewsRouter() {
10479
11820
  res.status(400).json({ error: result.error });
10480
11821
  return;
10481
11822
  }
11823
+ const queryStr = result.value.config.filters.query;
11824
+ if (typeof queryStr === "string" && queryStr.length > 0) {
11825
+ const statusConfig = await getStatusConfig();
11826
+ const queryErrors = validateQuery(queryStr, statusConfig.queryRegistry);
11827
+ if (queryErrors.length > 0) {
11828
+ res.status(400).json({ errors: queryErrors });
11829
+ return;
11830
+ }
11831
+ }
10482
11832
  try {
10483
11833
  const file = await withLock(SAVED_VIEWS_LOCK, async () => {
10484
11834
  const current = await readSavedViewsFile();
@@ -10503,6 +11853,15 @@ function createSavedViewsRouter() {
10503
11853
  res.status(400).json({ error: result.error });
10504
11854
  return;
10505
11855
  }
11856
+ const patchQueryStr = result.value.config?.filters.query;
11857
+ if (typeof patchQueryStr === "string" && patchQueryStr.length > 0) {
11858
+ const statusConfig = await getStatusConfig();
11859
+ const queryErrors = validateQuery(patchQueryStr, statusConfig.queryRegistry);
11860
+ if (queryErrors.length > 0) {
11861
+ res.status(400).json({ errors: queryErrors });
11862
+ return;
11863
+ }
11864
+ }
10506
11865
  try {
10507
11866
  const outcome = await withLock(SAVED_VIEWS_LOCK, async () => {
10508
11867
  const current = await readSavedViewsFile();
@@ -11395,6 +12754,12 @@ tags: []
11395
12754
  `;
11396
12755
  }
11397
12756
 
12757
+ // src/templates/index.ts
12758
+ init_cursor_rules();
12759
+ init_codex_agents();
12760
+ init_opencode_config();
12761
+ init_hermes_soul();
12762
+
11398
12763
  // src/dashboard/api-write.ts
11399
12764
  init_lifecycle();
11400
12765
  init_parser();
@@ -14119,86 +15484,11 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir2) {
14119
15484
  // src/dashboard/api-agent-sessions.ts
14120
15485
  init_agent_sessions();
14121
15486
  init_fs();
15487
+ init_transcript();
14122
15488
  import { Router as Router4 } from "express";
14123
15489
  import { resolve as resolve21 } from "path";
14124
-
14125
- // src/utils/transcript.ts
14126
- import { open as open2 } from "fs/promises";
14127
- var MAX_LINES_SCANNED = 50;
14128
- async function derivePathFromTranscript(transcriptPath) {
14129
- if (!transcriptPath) return null;
14130
- let handle;
14131
- try {
14132
- handle = await open2(transcriptPath, "r");
14133
- } catch {
14134
- return null;
14135
- }
14136
- try {
14137
- const stream = handle.createReadStream({ encoding: "utf-8" });
14138
- let buffer = "";
14139
- let scanned = 0;
14140
- for await (const chunk of stream) {
14141
- buffer += chunk;
14142
- let nl = buffer.indexOf("\n");
14143
- while (nl !== -1) {
14144
- const line = buffer.slice(0, nl);
14145
- buffer = buffer.slice(nl + 1);
14146
- const cwd = extractCwd(line);
14147
- if (cwd) {
14148
- stream.destroy();
14149
- return cwd;
14150
- }
14151
- scanned++;
14152
- if (scanned >= MAX_LINES_SCANNED) {
14153
- stream.destroy();
14154
- return null;
14155
- }
14156
- nl = buffer.indexOf("\n");
14157
- }
14158
- }
14159
- if (buffer.length > 0) {
14160
- const cwd = extractCwd(buffer);
14161
- if (cwd) return cwd;
14162
- }
14163
- return null;
14164
- } finally {
14165
- await handle.close().catch(() => {
14166
- });
14167
- }
14168
- }
14169
- function extractCwd(line) {
14170
- const trimmed = line.trim();
14171
- if (trimmed.length === 0 || trimmed[0] !== "{") return null;
14172
- try {
14173
- const parsed = JSON.parse(trimmed);
14174
- if (typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
14175
- return parsed.cwd;
14176
- }
14177
- } catch {
14178
- }
14179
- return null;
14180
- }
14181
-
14182
- // src/dashboard/api-agent-sessions.ts
14183
15490
  init_config2();
14184
-
14185
- // src/utils/process-info.ts
14186
- import { execFileSync as execFileSync2 } from "child_process";
14187
- function captureProcessStartedAt(pid) {
14188
- if (!Number.isFinite(pid) || pid <= 0) return null;
14189
- try {
14190
- const out = execFileSync2("ps", ["-o", "lstart=", "-p", String(pid)], {
14191
- encoding: "utf8",
14192
- stdio: ["ignore", "pipe", "ignore"]
14193
- });
14194
- const trimmed = out.trim();
14195
- return trimmed === "" ? null : trimmed;
14196
- } catch {
14197
- return null;
14198
- }
14199
- }
14200
-
14201
- // src/dashboard/api-agent-sessions.ts
15491
+ init_process_info();
14202
15492
  init_git_worktree();
14203
15493
  init_cwd();
14204
15494
  function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
@@ -15092,13 +16382,20 @@ async function resolveSessionPlan(input, terminal) {
15092
16382
  env: process.env,
15093
16383
  agentId: agent.id,
15094
16384
  fallbackWarning,
15095
- shellFallbackWarning
16385
+ shellFallbackWarning,
16386
+ // Resume continues the SAME session id; fork mints a new one in-agent.
16387
+ session: { sessionId: (input.mode ?? "resume") === "resume" ? session.sessionId : null }
15096
16388
  };
15097
16389
  }
15098
16390
 
15099
16391
  // src/launch/execute.ts
15100
16392
  import { spawn as spawn3 } from "child_process";
15101
- import { basename as basename4 } from "path";
16393
+ import { homedir as homedir5 } from "os";
16394
+ import { basename as basename4, join as join5, resolve as resolve23 } from "path";
16395
+ init_fs();
16396
+ init_config2();
16397
+ init_session_id();
16398
+ init_process_info();
15102
16399
  var CMUX_BUNDLE_ID = "com.cmuxterm.app";
15103
16400
  var CMUX_READINESS_MAX_MS = 20 * 250;
15104
16401
  var CMUX_LAUNCH_TIMEOUT_MS = CMUX_READINESS_MAX_MS + 3e3;
@@ -15123,8 +16420,8 @@ function buildShellCommandLine(plan) {
15123
16420
  init_paths();
15124
16421
  init_fs();
15125
16422
  import { fileURLToPath } from "url";
15126
- import { dirname as dirname5, resolve as resolve23, join as join3 } from "path";
15127
- import { realpathSync, readFileSync, mkdirSync } from "fs";
16423
+ import { dirname as dirname6, resolve as resolve24, join as join6 } from "path";
16424
+ import { realpathSync, readFileSync as readFileSync2, mkdirSync as mkdirSync2 } from "fs";
15128
16425
 
15129
16426
  // src/dashboard/api-launch-preflight.ts
15130
16427
  init_assignment_resolver();
@@ -15401,32 +16698,32 @@ import { Router as Router9 } from "express";
15401
16698
  // src/utils/status-config-resolution.ts
15402
16699
  init_frontmatter();
15403
16700
  import { readFile as readFile18, writeFile as writeFile5, rm as rm2 } from "fs/promises";
15404
- import { dirname as dirname6 } from "path";
16701
+ import { dirname as dirname7 } from "path";
15405
16702
 
15406
16703
  // src/utils/assignment-walk.ts
15407
16704
  init_fs();
15408
- import { resolve as resolve24 } from "path";
15409
- import { readdir as readdir11 } from "fs/promises";
16705
+ import { resolve as resolve25 } from "path";
16706
+ import { readdir as readdir12 } from "fs/promises";
15410
16707
  async function listAssignmentsByProject(projectsDir, standaloneDir) {
15411
16708
  const result = {
15412
16709
  withAssignmentMd: [],
15413
16710
  orphanFolders: []
15414
16711
  };
15415
16712
  if (await fileExists(projectsDir)) {
15416
- const projects = await readdir11(projectsDir, { withFileTypes: true });
16713
+ const projects = await readdir12(projectsDir, { withFileTypes: true });
15417
16714
  for (const m of projects) {
15418
16715
  if (!m.isDirectory()) continue;
15419
16716
  if (m.name.startsWith(".") || m.name.startsWith("_")) continue;
15420
- const assignmentsDir2 = resolve24(projectsDir, m.name, "assignments");
16717
+ const assignmentsDir2 = resolve25(projectsDir, m.name, "assignments");
15421
16718
  if (!await fileExists(assignmentsDir2)) continue;
15422
- const entries = await readdir11(assignmentsDir2, { withFileTypes: true });
16719
+ const entries = await readdir12(assignmentsDir2, { withFileTypes: true });
15423
16720
  for (const a of entries) {
15424
16721
  if (!a.isDirectory()) continue;
15425
16722
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
15426
- const assignmentDir = resolve24(assignmentsDir2, a.name);
15427
- const assignmentMd = resolve24(assignmentDir, "assignment.md");
16723
+ const assignmentDir = resolve25(assignmentsDir2, a.name);
16724
+ const assignmentMd = resolve25(assignmentDir, "assignment.md");
15428
16725
  const entry = {
15429
- projectDir: resolve24(projectsDir, m.name),
16726
+ projectDir: resolve25(projectsDir, m.name),
15430
16727
  projectSlug: m.name,
15431
16728
  assignmentDir,
15432
16729
  assignmentSlug: a.name,
@@ -15441,12 +16738,12 @@ async function listAssignmentsByProject(projectsDir, standaloneDir) {
15441
16738
  }
15442
16739
  }
15443
16740
  if (standaloneDir !== null && await fileExists(standaloneDir)) {
15444
- const entries = await readdir11(standaloneDir, { withFileTypes: true });
16741
+ const entries = await readdir12(standaloneDir, { withFileTypes: true });
15445
16742
  for (const a of entries) {
15446
16743
  if (!a.isDirectory()) continue;
15447
16744
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
15448
- const assignmentDir = resolve24(standaloneDir, a.name);
15449
- const assignmentMd = resolve24(assignmentDir, "assignment.md");
16745
+ const assignmentDir = resolve25(standaloneDir, a.name);
16746
+ const assignmentMd = resolve25(assignmentDir, "assignment.md");
15450
16747
  const entry = {
15451
16748
  projectDir: standaloneDir,
15452
16749
  projectSlug: null,
@@ -15638,7 +16935,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
15638
16935
  } catch {
15639
16936
  continue;
15640
16937
  }
15641
- const assignmentDir = dirname6(a.path);
16938
+ const assignmentDir = dirname7(a.path);
15642
16939
  try {
15643
16940
  await rm2(assignmentDir, { recursive: true, force: true });
15644
16941
  deleted++;
@@ -15771,7 +17068,8 @@ function createStatusConfigRouter(projectsDir, assignmentsDir2) {
15771
17068
  statuses: config.statuses,
15772
17069
  order: config.order,
15773
17070
  transitions: config.transitions,
15774
- custom: config.custom
17071
+ custom: config.custom,
17072
+ factDeclarations: config.factDeclarations
15775
17073
  });
15776
17074
  } catch (error) {
15777
17075
  console.error("Error getting status config:", error);
@@ -15955,7 +17253,7 @@ import { Router as Router10 } from "express";
15955
17253
  init_paths();
15956
17254
  import Database2 from "better-sqlite3";
15957
17255
  import { randomUUID as randomUUID3 } from "crypto";
15958
- import { resolve as resolve25 } from "path";
17256
+ import { resolve as resolve26 } from "path";
15959
17257
  var db2 = null;
15960
17258
  var LEASE_SCHEMA_VERSION = "1";
15961
17259
  var SCHEMA_SQL2 = `
@@ -16042,7 +17340,7 @@ function isBusyError(err) {
16042
17340
  }
16043
17341
  function initLeasesDb(dbPath) {
16044
17342
  if (db2) return db2;
16045
- const finalPath = dbPath ?? resolve25(syntaurRoot(), "syntaur.db");
17343
+ const finalPath = dbPath ?? resolve26(syntaurRoot(), "syntaur.db");
16046
17344
  db2 = new Database2(finalPath);
16047
17345
  db2.pragma("journal_mode = WAL");
16048
17346
  db2.pragma("busy_timeout = 5000");
@@ -16191,7 +17489,7 @@ import { Router as Router11 } from "express";
16191
17489
  // src/db/usage-db.ts
16192
17490
  init_paths();
16193
17491
  import Database3 from "better-sqlite3";
16194
- import { resolve as resolve26 } from "path";
17492
+ import { resolve as resolve27 } from "path";
16195
17493
  var db3 = null;
16196
17494
  var USAGE_SCHEMA_VERSION = "1";
16197
17495
  var SCHEMA_SQL3 = `
@@ -16248,7 +17546,7 @@ CREATE INDEX IF NOT EXISTS idx_usage_daily_day
16248
17546
  `;
16249
17547
  function initUsageDb(dbPath) {
16250
17548
  if (db3) return db3;
16251
- const finalPath = dbPath ?? resolve26(syntaurRoot(), "syntaur.db");
17549
+ const finalPath = dbPath ?? resolve27(syntaurRoot(), "syntaur.db");
16252
17550
  db3 = new Database3(finalPath);
16253
17551
  db3.pragma("journal_mode = WAL");
16254
17552
  db3.pragma("busy_timeout = 5000");
@@ -16507,7 +17805,7 @@ init_slug();
16507
17805
  init_timestamp();
16508
17806
  init_fs();
16509
17807
  import { Router as Router12 } from "express";
16510
- import { resolve as resolve27 } from "path";
17808
+ import { resolve as resolve28 } from "path";
16511
17809
  import { readFile as readFile19 } from "fs/promises";
16512
17810
  init_playbooks();
16513
17811
  function statusForPlaybookError(code) {
@@ -16590,7 +17888,7 @@ function createPlaybooksRouter(playbooksDir2) {
16590
17888
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
16591
17889
  return;
16592
17890
  }
16593
- const filePath = resolve27(playbooksDir2, resolved.filename);
17891
+ const filePath = resolve28(playbooksDir2, resolved.filename);
16594
17892
  const content = await readFile19(filePath, "utf-8");
16595
17893
  res.json({
16596
17894
  documentType: "playbook",
@@ -16616,7 +17914,7 @@ function createPlaybooksRouter(playbooksDir2) {
16616
17914
  return;
16617
17915
  }
16618
17916
  await ensureDir(playbooksDir2);
16619
- const filePath = resolve27(playbooksDir2, `${slug}.md`);
17917
+ const filePath = resolve28(playbooksDir2, `${slug}.md`);
16620
17918
  if (await fileExists(filePath)) {
16621
17919
  res.status(409).json({ error: `Playbook "${slug}" already exists` });
16622
17920
  return;
@@ -16640,7 +17938,7 @@ function createPlaybooksRouter(playbooksDir2) {
16640
17938
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
16641
17939
  return;
16642
17940
  }
16643
- const filePath = resolve27(playbooksDir2, resolved.filename);
17941
+ const filePath = resolve28(playbooksDir2, resolved.filename);
16644
17942
  await writeFileForce(filePath, content);
16645
17943
  await rebuildPlaybookManifest(playbooksDir2);
16646
17944
  res.json({ slug: resolved.slug, path: filePath });
@@ -16688,8 +17986,8 @@ init_parser2();
16688
17986
  init_fs();
16689
17987
  init_paths();
16690
17988
  import { Router as Router14 } from "express";
16691
- import { readdir as readdir13 } from "fs/promises";
16692
- import { resolve as resolvePath, dirname as dirname8 } from "path";
17989
+ import { readdir as readdir14 } from "fs/promises";
17990
+ import { resolve as resolvePath, dirname as dirname9 } from "path";
16693
17991
  import { rename as rename6, mkdir as mkdir4 } from "fs/promises";
16694
17992
  init_slug();
16695
17993
 
@@ -16699,7 +17997,7 @@ init_parser2();
16699
17997
  // src/commands/create-assignment.ts
16700
17998
  init_slug();
16701
17999
  init_timestamp();
16702
- import { resolve as resolve28 } from "path";
18000
+ import { resolve as resolve29 } from "path";
16703
18001
  init_paths();
16704
18002
  init_fs();
16705
18003
  init_config2();
@@ -16777,14 +18075,14 @@ async function createAssignmentCommand(title, options) {
16777
18075
  if (options.oneOff) {
16778
18076
  const standaloneRoot = assignmentsDir();
16779
18077
  folderName = id;
16780
- assignmentDir = resolve28(standaloneRoot, folderName);
18078
+ assignmentDir = resolve29(standaloneRoot, folderName);
16781
18079
  projectSlug = null;
16782
18080
  await ensureDir(standaloneRoot);
16783
18081
  } else {
16784
18082
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
16785
18083
  projectSlug = options.project;
16786
- const projectDir = resolve28(baseDir, projectSlug);
16787
- const projectMdPath = resolve28(projectDir, "project.md");
18084
+ const projectDir = resolve29(baseDir, projectSlug);
18085
+ const projectMdPath = resolve29(projectDir, "project.md");
16788
18086
  if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
16789
18087
  throw new Error(
16790
18088
  `Project "${projectSlug}" not found at ${projectDir}.
@@ -16792,9 +18090,9 @@ Run 'syntaur create-project' first or use --one-off.`
16792
18090
  );
16793
18091
  }
16794
18092
  if (dependsOn.length > 0) {
16795
- const depDirBase = resolve28(projectDir, "assignments");
18093
+ const depDirBase = resolve29(projectDir, "assignments");
16796
18094
  for (const dep of dependsOn) {
16797
- const depDir = resolve28(depDirBase, dep);
18095
+ const depDir = resolve29(depDirBase, dep);
16798
18096
  if (!await fileExists(depDir)) {
16799
18097
  console.warn(
16800
18098
  `Warning: dependency "${dep}" does not exist in project "${projectSlug}" yet.`
@@ -16803,7 +18101,7 @@ Run 'syntaur create-project' first or use --one-off.`
16803
18101
  }
16804
18102
  }
16805
18103
  folderName = assignmentSlug;
16806
- assignmentDir = resolve28(projectDir, "assignments", folderName);
18104
+ assignmentDir = resolve29(projectDir, "assignments", folderName);
16807
18105
  }
16808
18106
  if (await fileExists(assignmentDir)) {
16809
18107
  throw new Error(
@@ -16815,7 +18113,7 @@ Use --slug to specify a different slug.`
16815
18113
  const companionAssignmentRef = projectSlug === null ? id : assignmentSlug;
16816
18114
  const files = [
16817
18115
  [
16818
- resolve28(assignmentDir, "assignment.md"),
18116
+ resolve29(assignmentDir, "assignment.md"),
16819
18117
  renderAssignment({
16820
18118
  id,
16821
18119
  slug: assignmentSlug,
@@ -16833,35 +18131,35 @@ Use --slug to specify a different slug.`
16833
18131
  })
16834
18132
  ],
16835
18133
  [
16836
- resolve28(assignmentDir, "scratchpad.md"),
18134
+ resolve29(assignmentDir, "scratchpad.md"),
16837
18135
  renderScratchpad({
16838
18136
  assignmentSlug: companionAssignmentRef,
16839
18137
  timestamp
16840
18138
  })
16841
18139
  ],
16842
18140
  [
16843
- resolve28(assignmentDir, "handoff.md"),
18141
+ resolve29(assignmentDir, "handoff.md"),
16844
18142
  renderHandoff({
16845
18143
  assignmentSlug: companionAssignmentRef,
16846
18144
  timestamp
16847
18145
  })
16848
18146
  ],
16849
18147
  [
16850
- resolve28(assignmentDir, "decision-record.md"),
18148
+ resolve29(assignmentDir, "decision-record.md"),
16851
18149
  renderDecisionRecord({
16852
18150
  assignmentSlug: companionAssignmentRef,
16853
18151
  timestamp
16854
18152
  })
16855
18153
  ],
16856
18154
  [
16857
- resolve28(assignmentDir, "progress.md"),
18155
+ resolve29(assignmentDir, "progress.md"),
16858
18156
  renderProgress({
16859
18157
  assignment: companionAssignmentRef,
16860
18158
  timestamp
16861
18159
  })
16862
18160
  ],
16863
18161
  [
16864
- resolve28(assignmentDir, "comments.md"),
18162
+ resolve29(assignmentDir, "comments.md"),
16865
18163
  renderComments({
16866
18164
  assignment: companionAssignmentRef,
16867
18165
  timestamp
@@ -17034,8 +18332,8 @@ init_api();
17034
18332
  import { raw } from "express";
17035
18333
 
17036
18334
  // src/todos/attachments.ts
17037
- import { mkdir as mkdir3, readdir as readdir12, stat as stat2, rename as rename5, rm as rm3, unlink as unlink6, writeFile as writeFile6, cp } from "fs/promises";
17038
- import { resolve as resolve29, basename as basename5, dirname as dirname7, extname } from "path";
18335
+ import { mkdir as mkdir3, readdir as readdir13, stat as stat3, rename as rename5, rm as rm3, unlink as unlink6, writeFile as writeFile6, cp } from "fs/promises";
18336
+ import { resolve as resolve30, basename as basename5, dirname as dirname8, extname } from "path";
17039
18337
 
17040
18338
  // src/utils/proof-artifact-id.ts
17041
18339
  import { randomBytes as randomBytes2 } from "crypto";
@@ -17122,16 +18420,16 @@ function sanitizeAttachmentName(name) {
17122
18420
  return n;
17123
18421
  }
17124
18422
  function attachmentsRootDir(todosDir2) {
17125
- return resolve29(todosDir2, "attachments");
18423
+ return resolve30(todosDir2, "attachments");
17126
18424
  }
17127
18425
  function attachmentDirFor(todosDir2, scopeId, todoId) {
17128
18426
  assertScope(scopeId);
17129
18427
  assertTodoId(todoId);
17130
- return resolve29(attachmentsRootDir(todosDir2), scopeId, todoId);
18428
+ return resolve30(attachmentsRootDir(todosDir2), scopeId, todoId);
17131
18429
  }
17132
18430
  async function dirExists(p) {
17133
18431
  try {
17134
- return (await stat2(p)).isDirectory();
18432
+ return (await stat3(p)).isDirectory();
17135
18433
  } catch {
17136
18434
  return false;
17137
18435
  }
@@ -17141,7 +18439,7 @@ async function writeAttachment(todosDir2, scopeId, todoId, originalName, bytes)
17141
18439
  await mkdir3(dir, { recursive: true });
17142
18440
  const id = generateArtifactId();
17143
18441
  const filename = sanitizeAttachmentName(originalName);
17144
- await writeFile6(resolve29(dir, `${id}__${filename}`), bytes);
18442
+ await writeFile6(resolve30(dir, `${id}__${filename}`), bytes);
17145
18443
  return {
17146
18444
  id,
17147
18445
  filename,
@@ -17154,7 +18452,7 @@ async function listAttachments(todosDir2, scopeId, todoId) {
17154
18452
  const dir = attachmentDirFor(todosDir2, scopeId, todoId);
17155
18453
  let names;
17156
18454
  try {
17157
- names = await readdir12(dir);
18455
+ names = await readdir13(dir);
17158
18456
  } catch {
17159
18457
  return [];
17160
18458
  }
@@ -17166,7 +18464,7 @@ async function listAttachments(todosDir2, scopeId, todoId) {
17166
18464
  if (!ATTACHMENT_ID_RE.test(id)) continue;
17167
18465
  const filename = stored.slice(sep2 + 2);
17168
18466
  try {
17169
- const st = await stat2(resolve29(dir, stored));
18467
+ const st = await stat3(resolve30(dir, stored));
17170
18468
  if (!st.isFile()) continue;
17171
18469
  out.push({ id, filename, mime: mimeForName(filename), size: st.size, createdAt: st.mtime.toISOString() });
17172
18470
  } catch {
@@ -17177,10 +18475,10 @@ async function listAttachments(todosDir2, scopeId, todoId) {
17177
18475
  }
17178
18476
  async function readScopeAttachments(todosDir2, scopeId) {
17179
18477
  assertScope(scopeId);
17180
- const scopeDir = resolve29(attachmentsRootDir(todosDir2), scopeId);
18478
+ const scopeDir = resolve30(attachmentsRootDir(todosDir2), scopeId);
17181
18479
  let todoIds;
17182
18480
  try {
17183
- todoIds = await readdir12(scopeDir);
18481
+ todoIds = await readdir13(scopeDir);
17184
18482
  } catch {
17185
18483
  return {};
17186
18484
  }
@@ -17197,7 +18495,7 @@ async function resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId) {
17197
18495
  const dir = attachmentDirFor(todosDir2, scopeId, todoId);
17198
18496
  let names;
17199
18497
  try {
17200
- names = await readdir12(dir);
18498
+ names = await readdir13(dir);
17201
18499
  } catch {
17202
18500
  return null;
17203
18501
  }
@@ -17205,7 +18503,7 @@ async function resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId) {
17205
18503
  const stored = names.find((n) => n.startsWith(prefix));
17206
18504
  if (!stored) return null;
17207
18505
  const filename = stored.slice(prefix.length);
17208
- return { path: resolve29(dir, stored), filename, mime: mimeForName(filename) };
18506
+ return { path: resolve30(dir, stored), filename, mime: mimeForName(filename) };
17209
18507
  }
17210
18508
  async function deleteAttachment(todosDir2, scopeId, todoId, attachmentId) {
17211
18509
  const resolved = await resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId);
@@ -17225,7 +18523,7 @@ async function moveAttachments(srcTodosDir, srcScopeId, dstTodosDir, dstScopeId,
17225
18523
  const src = attachmentDirFor(srcTodosDir, srcScopeId, todoId);
17226
18524
  if (!await dirExists(src)) return;
17227
18525
  const dst = attachmentDirFor(dstTodosDir, dstScopeId, todoId);
17228
- await mkdir3(dirname7(dst), { recursive: true });
18526
+ await mkdir3(dirname8(dst), { recursive: true });
17229
18527
  try {
17230
18528
  await rename5(src, dst);
17231
18529
  } catch (err) {
@@ -17501,7 +18799,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
17501
18799
  router.get("/", async (_req, res) => {
17502
18800
  try {
17503
18801
  await ensureDir(todosDir2);
17504
- const files = await readdir13(todosDir2).catch(() => []);
18802
+ const files = await readdir14(todosDir2).catch(() => []);
17505
18803
  const workspaces = [];
17506
18804
  for (const file of files) {
17507
18805
  if (typeof file !== "string") continue;
@@ -17617,8 +18915,8 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
17617
18915
  router.post("/:workspace/archive", async (req, res) => {
17618
18916
  try {
17619
18917
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
17620
- const { resolve: resolve34 } = await import("path");
17621
- const { readFile: readFile23 } = await import("fs/promises");
18918
+ const { resolve: resolve38 } = await import("path");
18919
+ const { readFile: readFile25 } = await import("fs/promises");
17622
18920
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
17623
18921
  const workspace = getWorkspaceParam(req.params.workspace);
17624
18922
  const outcome = await wsLock(workspace, async () => {
@@ -17634,10 +18932,10 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
17634
18932
  (e) => e.itemIds.every((id) => completedIds.has(id))
17635
18933
  );
17636
18934
  const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
17637
- await ensureDir(resolve34(todosDir2, "archive"));
18935
+ await ensureDir(resolve38(todosDir2, "archive"));
17638
18936
  let archContent = "";
17639
18937
  if (await fileExists(archFile)) {
17640
- archContent = await readFile23(archFile, "utf-8");
18938
+ archContent = await readFile25(archFile, "utf-8");
17641
18939
  archContent = archContent.trimEnd() + "\n\n";
17642
18940
  } else {
17643
18941
  archContent = `---
@@ -17926,7 +19224,7 @@ workspace: ${workspace}
17926
19224
  const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
17927
19225
  const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
17928
19226
  const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
17929
- const { readFile: readFile23 } = await import("fs/promises");
19227
+ const { readFile: readFile25 } = await import("fs/promises");
17930
19228
  const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
17931
19229
  const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
17932
19230
  let assignmentRef;
@@ -17947,7 +19245,7 @@ workspace: ${workspace}
17947
19245
  }
17948
19246
  const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
17949
19247
  if (!await fileExists2(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
17950
- let content = await readFile23(assignmentMdPath, "utf-8");
19248
+ let content = await readFile25(assignmentMdPath, "utf-8");
17951
19249
  content = appendTodosToAssignmentBody2(
17952
19250
  content,
17953
19251
  items.map((it) => ({
@@ -18064,7 +19362,7 @@ workspace: ${workspace}
18064
19362
  return { status: 409, error: "attachments already exist in target" };
18065
19363
  }
18066
19364
  if (item.planDir && newPlanDir) {
18067
- await mkdir4(dirname8(newPlanDir), { recursive: true });
19365
+ await mkdir4(dirname9(newPlanDir), { recursive: true });
18068
19366
  await rename6(item.planDir, newPlanDir);
18069
19367
  item.planDir = newPlanDir;
18070
19368
  }
@@ -18143,7 +19441,7 @@ init_paths();
18143
19441
  init_slug();
18144
19442
  import { Router as Router15 } from "express";
18145
19443
  import { mkdir as mkdir5, readFile as readFile20, rename as rename7 } from "fs/promises";
18146
- import { resolve as resolve30, dirname as dirname9 } from "path";
19444
+ import { resolve as resolve31, dirname as dirname10 } from "path";
18147
19445
  init_api();
18148
19446
  var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
18149
19447
  function touchItem4(item) {
@@ -18159,7 +19457,7 @@ function params(req) {
18159
19457
  return req.params;
18160
19458
  }
18161
19459
  async function projectExists(projectsDir, slug) {
18162
- return fileExists(resolve30(projectsDir, slug, "project.md"));
19460
+ return fileExists(resolve31(projectsDir, slug, "project.md"));
18163
19461
  }
18164
19462
  async function ensureProjectTodosDir(projectsDir, slug) {
18165
19463
  const todosDir2 = projectTodosDir(projectsDir, slug);
@@ -18176,7 +19474,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
18176
19474
  throw err;
18177
19475
  }
18178
19476
  try {
18179
- await mkdir5(resolve30(todosDir2, "archive"), { recursive: false });
19477
+ await mkdir5(resolve31(todosDir2, "archive"), { recursive: false });
18180
19478
  } catch (err) {
18181
19479
  const code = err.code;
18182
19480
  if (code === "EEXIST") return;
@@ -18839,15 +20137,15 @@ workspace: ${slug}
18839
20137
  if (tg.includes("/")) {
18840
20138
  const parts = tg.split("/");
18841
20139
  if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
18842
- assignmentDir = resolve30(projectsDir, parts[0], "assignments", parts[1]);
20140
+ assignmentDir = resolve31(projectsDir, parts[0], "assignments", parts[1]);
18843
20141
  assignmentRef = `${parts[0]}/${parts[1]}`;
18844
20142
  } else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tg)) {
18845
- assignmentDir = resolve30(assignmentsDirFn(), tg);
20143
+ assignmentDir = resolve31(assignmentsDirFn(), tg);
18846
20144
  assignmentRef = tg;
18847
20145
  } else {
18848
20146
  return { error: `Invalid target.assignment "${tg}"` };
18849
20147
  }
18850
- const assignmentMdPath = resolve30(assignmentDir, "assignment.md");
20148
+ const assignmentMdPath = resolve31(assignmentDir, "assignment.md");
18851
20149
  if (!await fileExists(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
18852
20150
  let content = await readFile20(assignmentMdPath, "utf-8");
18853
20151
  content = appendTodosToAssignmentBody2(
@@ -18988,7 +20286,7 @@ workspace: ${slug}
18988
20286
  return { status: 409, error: "attachments already exist in target" };
18989
20287
  }
18990
20288
  if (item.planDir && newPlanDir) {
18991
- await mkdir5(dirname9(newPlanDir), { recursive: true });
20289
+ await mkdir5(dirname10(newPlanDir), { recursive: true });
18992
20290
  await rename7(item.planDir, newPlanDir);
18993
20291
  item.planDir = newPlanDir;
18994
20292
  }
@@ -19052,7 +20350,7 @@ workspace: ${slug}
19052
20350
 
19053
20351
  // src/dashboard/api-bundles.ts
19054
20352
  import { Router as Router16 } from "express";
19055
- import { readdir as readdir14 } from "fs/promises";
20353
+ import { readdir as readdir15 } from "fs/promises";
19056
20354
 
19057
20355
  // src/todos/bundle-parser.ts
19058
20356
  init_parser();
@@ -19176,7 +20474,7 @@ function createBundlesRouter(todosDir2, broadcast) {
19176
20474
  try {
19177
20475
  await ensureDir(todosDir2);
19178
20476
  const bundles = await readBundles(todosDir2);
19179
- const workspaceFiles = await readdir14(todosDir2).catch(() => []);
20477
+ const workspaceFiles = await readdir15(todosDir2).catch(() => []);
19180
20478
  const itemsByKey = /* @__PURE__ */ new Map();
19181
20479
  for (const f of workspaceFiles) {
19182
20480
  if (typeof f !== "string") continue;
@@ -19229,7 +20527,7 @@ init_fs();
19229
20527
  init_paths();
19230
20528
  init_slug();
19231
20529
  import { Router as Router17 } from "express";
19232
- import { resolve as resolve31 } from "path";
20530
+ import { resolve as resolve32 } from "path";
19233
20531
  init_parser2();
19234
20532
  function deriveStatus2(bundle, items) {
19235
20533
  const members = bundle.todoIds.map((id) => items.find((i) => i.id === id)).filter((i) => i !== void 0);
@@ -19271,7 +20569,7 @@ function createProjectBundlesRouter(projectsDir, broadcast) {
19271
20569
  router.get("/", async (req, res) => {
19272
20570
  try {
19273
20571
  const slug = getProjectIdParam2(req.params.projectId);
19274
- const projectMd = resolve31(projectsDir, slug, "project.md");
20572
+ const projectMd = resolve32(projectsDir, slug, "project.md");
19275
20573
  if (!await fileExists(projectMd)) {
19276
20574
  notFound2(res, slug);
19277
20575
  return;
@@ -19300,8 +20598,8 @@ init_fs();
19300
20598
  init_config2();
19301
20599
  import { execFile as execFile2 } from "child_process";
19302
20600
  import { promisify as promisify2 } from "util";
19303
- import { cp as cp2, mkdtemp, rm as rm4, readFile as readFile22, writeFile as writeFile7, unlink as unlink7, stat as stat3, open as open3, rename as rename8 } from "fs/promises";
19304
- import { resolve as resolve32, join as join4 } from "path";
20601
+ import { cp as cp2, mkdtemp, rm as rm4, readFile as readFile22, writeFile as writeFile7, unlink as unlink7, stat as stat4, open as open4, rename as rename8 } from "fs/promises";
20602
+ import { resolve as resolve33, join as join7 } from "path";
19305
20603
  import { tmpdir } from "os";
19306
20604
  var exec2 = promisify2(execFile2);
19307
20605
  var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
@@ -19341,7 +20639,7 @@ async function resolveCategoryPath(category) {
19341
20639
  case "servers":
19342
20640
  return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
19343
20641
  case "config":
19344
- return { sourcePath: resolve32(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
20642
+ return { sourcePath: resolve33(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
19345
20643
  }
19346
20644
  }
19347
20645
  async function checkGitInstalled() {
@@ -19352,10 +20650,10 @@ async function checkGitInstalled() {
19352
20650
  }
19353
20651
  }
19354
20652
  async function acquireLock2() {
19355
- const lockPath = resolve32(syntaurRoot(), LOCK_FILE_NAME);
20653
+ const lockPath = resolve33(syntaurRoot(), LOCK_FILE_NAME);
19356
20654
  await ensureDir(syntaurRoot());
19357
20655
  try {
19358
- const handle = await open3(lockPath, "wx");
20656
+ const handle = await open4(lockPath, "wx");
19359
20657
  await handle.write(String(process.pid));
19360
20658
  await handle.close();
19361
20659
  return lockPath;
@@ -19394,12 +20692,12 @@ async function cloneOrInit(repoUrl, destDir) {
19394
20692
  }
19395
20693
  async function copyRecursive(src, dest) {
19396
20694
  if (!await fileExists(src)) return;
19397
- const s = await stat3(src);
20695
+ const s = await stat4(src);
19398
20696
  if (s.isDirectory()) {
19399
20697
  await ensureDir(dest);
19400
20698
  await cp2(src, dest, { recursive: true, force: true });
19401
20699
  } else {
19402
- await ensureDir(resolve32(dest, ".."));
20700
+ await ensureDir(resolve33(dest, ".."));
19403
20701
  await cp2(src, dest, { force: true });
19404
20702
  }
19405
20703
  }
@@ -19431,11 +20729,11 @@ async function backupToGithub(overrides) {
19431
20729
  let tmpDir = null;
19432
20730
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
19433
20731
  try {
19434
- tmpDir = await mkdtemp(join4(tmpdir(), "syntaur-backup-"));
20732
+ tmpDir = await mkdtemp(join7(tmpdir(), "syntaur-backup-"));
19435
20733
  await cloneOrInit(repo, tmpDir);
19436
20734
  for (const category of categories) {
19437
20735
  const { sourcePath, repoPath, isFile } = await resolveCategoryPath(category);
19438
- const destPath = join4(tmpDir, repoPath);
20736
+ const destPath = join7(tmpDir, repoPath);
19439
20737
  if (isFile) {
19440
20738
  await rm4(destPath, { force: true });
19441
20739
  } else {
@@ -19447,7 +20745,7 @@ async function backupToGithub(overrides) {
19447
20745
  }
19448
20746
  if (category === "config") {
19449
20747
  const sanitized = await readSanitizedConfig(sourcePath);
19450
- await ensureDir(resolve32(destPath, ".."));
20748
+ await ensureDir(resolve33(destPath, ".."));
19451
20749
  await writeFile7(destPath, sanitized, "utf-8");
19452
20750
  } else {
19453
20751
  await copyRecursive(sourcePath, destPath);
@@ -19501,7 +20799,7 @@ async function backupToGithub(overrides) {
19501
20799
  }
19502
20800
  async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
19503
20801
  if (isFile) {
19504
- await ensureDir(resolve32(localPath, ".."));
20802
+ await ensureDir(resolve33(localPath, ".."));
19505
20803
  await cp2(repoSrcPath, localPath, { force: true });
19506
20804
  return;
19507
20805
  }
@@ -19563,7 +20861,7 @@ async function restoreFromGithub(overrides) {
19563
20861
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
19564
20862
  try {
19565
20863
  await updateBackupConfig({ lastRestore: timestamp });
19566
- tmpDir = await mkdtemp(join4(tmpdir(), "syntaur-restore-"));
20864
+ tmpDir = await mkdtemp(join7(tmpdir(), "syntaur-restore-"));
19567
20865
  await cloneOrInit(repo, tmpDir);
19568
20866
  for (const category of categories) {
19569
20867
  if (category === "config") {
@@ -19572,7 +20870,7 @@ async function restoreFromGithub(overrides) {
19572
20870
  }
19573
20871
  try {
19574
20872
  const { sourcePath: localPath, repoPath, isFile } = await resolveCategoryPath(category);
19575
- const repoSrcPath = join4(tmpDir, repoPath);
20873
+ const repoSrcPath = join7(tmpDir, repoPath);
19576
20874
  if (!await fileExists(repoSrcPath)) {
19577
20875
  console.warn(`Category "${category}" not found in backup repo, skipping.`);
19578
20876
  continue;
@@ -19602,7 +20900,7 @@ async function restoreFromGithub(overrides) {
19602
20900
  }
19603
20901
  async function getBackupStatus() {
19604
20902
  const config = await readConfig();
19605
- const lockPath = resolve32(syntaurRoot(), LOCK_FILE_NAME);
20903
+ const lockPath = resolve33(syntaurRoot(), LOCK_FILE_NAME);
19606
20904
  const locked = await fileExists(lockPath);
19607
20905
  return {
19608
20906
  repo: config.backup?.repo ?? null,
@@ -19763,7 +21061,7 @@ async function stopAutodiscovery() {
19763
21061
  function runReconcile() {
19764
21062
  if (activeReconcile || !savedOptions) return;
19765
21063
  const opts = savedOptions;
19766
- activeReconcile = reconcile(opts.serversDir, opts.projectsDir, opts.excludePids, opts.assignmentsDir).catch((err) => {
21064
+ activeReconcile = reconcile(opts.serversDir, opts.projectsDir, opts.excludePids, opts.assignmentsDir, opts.onAgentSessionsChanged).catch((err) => {
19767
21065
  console.error("[autodiscovery] reconcile failed:", err);
19768
21066
  }).finally(() => {
19769
21067
  activeReconcile = null;
@@ -19884,7 +21182,7 @@ async function isProcessAlive(pid) {
19884
21182
  return false;
19885
21183
  }
19886
21184
  }
19887
- async function reconcile(serversDir2, projectsDir, excludePids, assignmentsDir2) {
21185
+ async function reconcile(serversDir2, projectsDir, excludePids, assignmentsDir2, onAgentSessionsChanged) {
19888
21186
  const names = await listSessionFiles(serversDir2);
19889
21187
  const existingFiles = /* @__PURE__ */ new Map();
19890
21188
  for (const name of names) {
@@ -19901,6 +21199,16 @@ async function reconcile(serversDir2, projectsDir, excludePids, assignmentsDir2)
19901
21199
  if (tmuxChanged || processChanged || cleanupChanged) {
19902
21200
  clearScanCache();
19903
21201
  }
21202
+ const { isSessionDbInitialized: isSessionDbInitialized2 } = await Promise.resolve().then(() => (init_session_db(), session_db_exports));
21203
+ if (isSessionDbInitialized2()) {
21204
+ try {
21205
+ const { scanSessions: scanSessions2 } = await Promise.resolve().then(() => (init_scanner2(), scanner_exports2));
21206
+ const summary = await scanSessions2({});
21207
+ if (summary.changed) onAgentSessionsChanged?.();
21208
+ } catch (err) {
21209
+ console.error("[autodiscovery] session scan failed:", err);
21210
+ }
21211
+ }
19904
21212
  }
19905
21213
 
19906
21214
  // src/dashboard/server.ts
@@ -19950,7 +21258,7 @@ function createDashboardServer(options) {
19950
21258
  (async () => {
19951
21259
  try {
19952
21260
  const configResult = await migrateLegacyConfig(
19953
- resolve33(syntaurRoot(), "config.md")
21261
+ resolve37(syntaurRoot(), "config.md")
19954
21262
  );
19955
21263
  const projectResult = await migrateLegacyProjectFiles(projectsDir);
19956
21264
  const summary = summarizeMigration(projectResult, configResult);
@@ -20468,14 +21776,14 @@ function createDashboardServer(options) {
20468
21776
  app.use("/api/backup", createBackupRouter());
20469
21777
  if (serveStaticUi && dashboardDistPath) {
20470
21778
  const sendOpts = { dotfiles: "allow" };
20471
- app.use("/assets", express.static(resolve33(dashboardDistPath, "assets"), sendOpts));
21779
+ app.use("/assets", express.static(resolve37(dashboardDistPath, "assets"), sendOpts));
20472
21780
  app.use(express.static(dashboardDistPath, { ...sendOpts, index: false, fallthrough: true }));
20473
21781
  app.get("{*path}", async (req, res) => {
20474
21782
  if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
20475
21783
  res.status(404).json({ error: "Not Found" });
20476
21784
  return;
20477
21785
  }
20478
- const indexPath = resolve33(dashboardDistPath, "index.html");
21786
+ const indexPath = resolve37(dashboardDistPath, "index.html");
20479
21787
  if (!await fileExists(indexPath)) {
20480
21788
  res.status(503).send(
20481
21789
  'Dashboard not built. Run "npm run build:dashboard" first.'
@@ -20509,8 +21817,8 @@ function createDashboardServer(options) {
20509
21817
  if (!await migrationGate()) return;
20510
21818
  try {
20511
21819
  const context = await resolveDeriveContext2();
20512
- const projectDir = projectSlug ? resolve33(projectsDir, projectSlug) : null;
20513
- const path = projectDir ? resolve33(projectDir, "assignments", assignmentSlug, "assignment.md") : resolve33(assignmentsDir2, assignmentSlug, "assignment.md");
21820
+ const projectDir = projectSlug ? resolve37(projectsDir, projectSlug) : null;
21821
+ const path = projectDir ? resolve37(projectDir, "assignments", assignmentSlug, "assignment.md") : resolve37(assignmentsDir2, assignmentSlug, "assignment.md");
20514
21822
  if (!await fileExists(path)) return;
20515
21823
  const result = await recomputeAndWrite2(path, {
20516
21824
  cause: "derive",
@@ -20546,8 +21854,8 @@ function createDashboardServer(options) {
20546
21854
  serversDir: serversDir2,
20547
21855
  playbooksDir: playbooksDir2,
20548
21856
  todosDir: todosDir2,
20549
- dbPath: resolve33(syntaurRoot(), "syntaur.db"),
20550
- configPath: resolve33(syntaurRoot(), "config.md"),
21857
+ dbPath: resolve37(syntaurRoot(), "syntaur.db"),
21858
+ configPath: resolve37(syntaurRoot(), "config.md"),
20551
21859
  onMessage: broadcast,
20552
21860
  onAssignmentChanged: (projectSlug, assignmentSlug) => {
20553
21861
  void recomputeOne(projectSlug, assignmentSlug);
@@ -20558,7 +21866,16 @@ function createDashboardServer(options) {
20558
21866
  }
20559
21867
  });
20560
21868
  void sweepAll("boot-reconcile");
20561
- startAutodiscovery({ serversDir: serversDir2, projectsDir, assignmentsDir: assignmentsDir2, excludePids: /* @__PURE__ */ new Set([process.pid]) });
21869
+ startAutodiscovery({
21870
+ serversDir: serversDir2,
21871
+ projectsDir,
21872
+ assignmentsDir: assignmentsDir2,
21873
+ excludePids: /* @__PURE__ */ new Set([process.pid]),
21874
+ // Same WS frame the REST mutations emit, so the UI refreshes when the
21875
+ // session scan inserts/revives/sweeps rows. Autodiscovery's immediate
21876
+ // first run covers "scan at dashboard start".
21877
+ onAgentSessionsChanged: () => broadcast({ type: "agent-sessions-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() })
21878
+ });
20562
21879
  return new Promise((resolvePromise, reject) => {
20563
21880
  server.on("error", (err) => {
20564
21881
  if (err.code === "EADDRINUSE") {
@@ -20570,7 +21887,7 @@ function createDashboardServer(options) {
20570
21887
  }
20571
21888
  });
20572
21889
  server.listen(port, () => {
20573
- const portFile = resolve33(syntaurRoot(), "dashboard-port");
21890
+ const portFile = resolve37(syntaurRoot(), "dashboard-port");
20574
21891
  writeFile8(portFile, String(port), "utf-8").catch(() => {
20575
21892
  });
20576
21893
  resolvePromise();
@@ -20589,7 +21906,7 @@ function createDashboardServer(options) {
20589
21906
  client.terminate();
20590
21907
  }
20591
21908
  clients.clear();
20592
- const portFile = resolve33(syntaurRoot(), "dashboard-port");
21909
+ const portFile = resolve37(syntaurRoot(), "dashboard-port");
20593
21910
  await unlink8(portFile).catch(() => {
20594
21911
  });
20595
21912
  server.closeAllConnections?.();