syntaur 0.45.0 → 0.47.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-RQBuJKcX.js → _basePickBy-DgR0_P-o.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-_J7s4kD3.js → _baseUniq-C8_Ych09.js} +1 -1
  4. package/dashboard/dist/assets/{arc-_9SyUgKQ.js → arc-yMHz4vGa.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-C8LeFMgr.js → architectureDiagram-2XIMDMQ5-ColWcH3P.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-gMh0EPEh.js → blockDiagram-WCTKOSBZ-Bo8Npvfq.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-cHwecwLI.js → c4Diagram-IC4MRINW-B2ky8AT7.js} +1 -1
  8. package/dashboard/dist/assets/channel-CUTEvTdk.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-Bb2anYuQ.js → chunk-4BX2VUAB-CyF6Z6dx.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-DYIRGzA1.js → chunk-55IACEB6-BJOEnwNN.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-sgRWBbaF.js → chunk-FMBD7UC4-D3siQyQ4.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-DlYKMl_j.js → chunk-JSJVCQXG-DKGuxEMf.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-D0YDLAOF.js → chunk-KX2RTZJC-CNIWWO2F.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-D-Y-CUx6.js → chunk-NQ4KR5QH-DXt05c7h.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-D7FpSvb5.js → chunk-QZHKN3VN-CM63uYnf.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-CtXgQLdS.js → chunk-WL4C6EOR-Dqvl_14m.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-Bkoc7orC.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-Bkoc7orC.js +1 -0
  19. package/dashboard/dist/assets/clone-CltBg7cH.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-YbTaohoJ.js → cose-bilkent-S5V4N54A-WBLtT1w9.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-CMtwGAnP.js → dagre-KLK3FWXG-DIdQdwa7.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-D8wBMBAX.js → diagram-E7M64L7V-BEH6P_Sk.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-DfudLpiJ.js → diagram-IFDJBPK2-BuhxBcSy.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-CyMy61wE.js → diagram-P4PSJMXO-DPSNVVzN.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-BlB4ZQl9.js → erDiagram-INFDFZHY-DYJb_rF5.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-DbhDQJM3.js → flowDiagram-PKNHOUZH-B9_8BI26.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-DJFqteNi.js → ganttDiagram-A5KZAMGK-Bsg3QOhs.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-D8etA_mm.js → gitGraphDiagram-K3NZZRJ6-Cf5G9x_K.js} +1 -1
  29. package/dashboard/dist/assets/{graph-Ce86jeZn.js → graph-DyXfcrIH.js} +1 -1
  30. package/dashboard/dist/assets/index-C3kYxhbQ.js +567 -0
  31. package/dashboard/dist/assets/index-DKr21dk8.css +1 -0
  32. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-Cx35U-h8.js → infoDiagram-LFFYTUFH-Bu1zlXs2.js} +1 -1
  33. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-C04Y2nj8.js → ishikawaDiagram-PHBUUO56-fb8C-XRT.js} +1 -1
  34. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-D8-cxbxE.js → journeyDiagram-4ABVD52K-smlBWs2O.js} +1 -1
  35. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-DVKqMylP.js → kanban-definition-K7BYSVSG-Bz1AxFRE.js} +1 -1
  36. package/dashboard/dist/assets/{layout-98xZDpgu.js → layout-VsTD3onG.js} +1 -1
  37. package/dashboard/dist/assets/{linear-0jk_IwAc.js → linear-CE8xncGu.js} +1 -1
  38. package/dashboard/dist/assets/{mermaid.core-C337VWfr.js → mermaid.core-C0KQpDyW.js} +4 -4
  39. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-8sNYGYEP.js → mindmap-definition-YRQLILUH-SRE5Immj.js} +1 -1
  40. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-afcmzHxf.js → pieDiagram-SKSYHLDU-CaZ_aCcD.js} +1 -1
  41. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-B4RjcpOq.js → quadrantDiagram-337W2JSQ-Dd6MIruu.js} +1 -1
  42. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-CRavU6cI.js → requirementDiagram-Z7DCOOCP-BBXvP53l.js} +1 -1
  43. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DFomU3z-.js → sankeyDiagram-WA2Y5GQK-DnS1SMIm.js} +1 -1
  44. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-CGKO7nmK.js → sequenceDiagram-2WXFIKYE-CLHJ1Uhx.js} +1 -1
  45. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-BjFI1K8h.js → stateDiagram-RAJIS63D-B6vrAeYw.js} +1 -1
  46. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-BeqNZKbk.js +1 -0
  47. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-BBo8XJFG.js → timeline-definition-YZTLITO2-BlHwGfnL.js} +1 -1
  48. package/dashboard/dist/assets/{treemap-KZPCXAKY-COd6i6TE.js → treemap-KZPCXAKY-D9kOGUYR.js} +1 -1
  49. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-CGQweQ36.js → vennDiagram-LZ73GAT5-BpQgeveT.js} +1 -1
  50. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-mfJ5So7N.js → xychartDiagram-JWTSCODW-DRch79fE.js} +1 -1
  51. package/dashboard/dist/index.html +2 -2
  52. package/dist/dashboard/server.js +1405 -210
  53. package/dist/dashboard/server.js.map +1 -1
  54. package/dist/index.js +2092 -1485
  55. package/dist/index.js.map +1 -1
  56. package/dist/launch/index.d.ts +19 -0
  57. package/dist/launch/index.js +528 -17
  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-C36dnl_e.js +0 -1
  71. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BsoGa6_a.js +0 -1
  72. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BsoGa6_a.js +0 -1
  73. package/dashboard/dist/assets/clone-Bz6jW3OY.js +0 -1
  74. package/dashboard/dist/assets/index-DRng26Jg.js +0 -567
  75. package/dashboard/dist/assets/index-DzHQIE2n.css +0 -1
  76. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-BtxefYKD.js +0 -1
@@ -3366,6 +3366,7 @@ function cloneDefaultConfig() {
3366
3366
  ...DEFAULT_CONFIG,
3367
3367
  onboarding: { ...DEFAULT_CONFIG.onboarding },
3368
3368
  agentDefaults: { ...DEFAULT_CONFIG.agentDefaults },
3369
+ session: { ...DEFAULT_CONFIG.session },
3369
3370
  integrations: { ...DEFAULT_CONFIG.integrations },
3370
3371
  backup: DEFAULT_CONFIG.backup ? { ...DEFAULT_CONFIG.backup } : null,
3371
3372
  statuses: DEFAULT_CONFIG.statuses ? {
@@ -4715,6 +4716,11 @@ async function readConfig() {
4715
4716
  fm["agentDefaults.autoCreateWorktree"]
4716
4717
  ) ? fm["agentDefaults.autoCreateWorktree"] : DEFAULT_CONFIG.agentDefaults.autoCreateWorktree
4717
4718
  },
4719
+ session: {
4720
+ autoTrack: SESSION_AUTO_TRACK_VALUES.includes(
4721
+ fm["session.autoTrack"]
4722
+ ) ? fm["session.autoTrack"] : DEFAULT_CONFIG.session.autoTrack
4723
+ },
4718
4724
  integrations: {
4719
4725
  claudePluginDir: parseOptionalAbsolutePath(
4720
4726
  fm["integrations.claudePluginDir"],
@@ -4814,7 +4820,7 @@ async function updateAgentsConfig(mutation, options = {}) {
4814
4820
  await writeAgentsConfig(next);
4815
4821
  return { previous, next, written: true };
4816
4822
  }
4817
- 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;
4818
4824
  var init_config2 = __esm({
4819
4825
  "src/utils/config.ts"() {
4820
4826
  "use strict";
@@ -4880,6 +4886,9 @@ var init_config2 = __esm({
4880
4886
  autoApprove: false,
4881
4887
  autoCreateWorktree: "ask"
4882
4888
  },
4889
+ session: {
4890
+ autoTrack: "all"
4891
+ },
4883
4892
  integrations: {
4884
4893
  claudePluginDir: null,
4885
4894
  codexPluginDir: null,
@@ -4900,6 +4909,7 @@ var init_config2 = __esm({
4900
4909
  }
4901
4910
  };
4902
4911
  AUTO_CREATE_WORKTREE_VALUES = ["skip", "ask", "always"];
4912
+ SESSION_AUTO_TRACK_VALUES = ["all", "workspaces-only", "off"];
4903
4913
  AgentConfigError = class extends Error {
4904
4914
  };
4905
4915
  DEFAULT_STATUS_COLORS = {
@@ -5760,6 +5770,15 @@ var init_help = __esm({
5760
5770
  });
5761
5771
 
5762
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
+ });
5763
5782
  import Database from "better-sqlite3";
5764
5783
  import { resolve as resolve9 } from "path";
5765
5784
  import { readdir as readdir5 } from "fs/promises";
@@ -5904,6 +5923,9 @@ function initSessionDb(dbPath) {
5904
5923
  db.exec(POST_MIGRATION_INDEXES_SQL);
5905
5924
  return db;
5906
5925
  }
5926
+ function isSessionDbInitialized() {
5927
+ return db !== null;
5928
+ }
5907
5929
  function getSessionDb() {
5908
5930
  if (!db) {
5909
5931
  throw new Error(
@@ -5918,6 +5940,9 @@ function closeSessionDb() {
5918
5940
  db = null;
5919
5941
  }
5920
5942
  }
5943
+ function resetSessionDb() {
5944
+ db = null;
5945
+ }
5921
5946
  async function migrateFromMarkdown(projectsDir) {
5922
5947
  const database = getSessionDb();
5923
5948
  const count = database.prepare("SELECT COUNT(*) as count FROM sessions").get();
@@ -5948,8 +5973,8 @@ async function migrateFromMarkdown(projectsDir) {
5948
5973
  return allSessions.length;
5949
5974
  }
5950
5975
  async function parseMarkdownSessionsIndex(filePath, projectSlug) {
5951
- const { readFile: readFile23 } = await import("fs/promises");
5952
- const raw2 = await readFile23(filePath, "utf-8");
5976
+ const { readFile: readFile25 } = await import("fs/promises");
5977
+ const raw2 = await readFile25(filePath, "utf-8");
5953
5978
  const sessions = [];
5954
5979
  const lines = raw2.split("\n");
5955
5980
  let inTable = false;
@@ -6036,10 +6061,11 @@ function rowToSession(row) {
6036
6061
  transcriptPath: row.transcript_path ?? null,
6037
6062
  pid: row.pid ?? null,
6038
6063
  pidStartedAt: row.pid_started_at ?? null,
6039
- originalHeadSha: row.original_head_sha ?? null
6064
+ originalHeadSha: row.original_head_sha ?? null,
6065
+ updatedAt: row.updated_at ?? null
6040
6066
  };
6041
6067
  }
6042
- async function appendSession(_projectDir, session) {
6068
+ async function appendSession(_projectDir, session, opts) {
6043
6069
  const db4 = getSessionDb();
6044
6070
  db4.prepare(`
6045
6071
  INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path, pid, pid_started_at, original_head_sha)
@@ -6048,7 +6074,11 @@ async function appendSession(_projectDir, session) {
6048
6074
  project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),
6049
6075
  assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),
6050
6076
  agent = excluded.agent,
6051
- status = CASE WHEN status IN ('completed','stopped') THEN status ELSE excluded.status END,
6077
+ status = CASE
6078
+ WHEN status = 'completed' THEN status
6079
+ WHEN status = 'stopped' AND NOT (? AND excluded.status = 'active') THEN status
6080
+ ELSE excluded.status
6081
+ END,
6052
6082
  path = COALESCE(NULLIF(excluded.path, ''), path),
6053
6083
  description = COALESCE(NULLIF(excluded.description, ''), description),
6054
6084
  transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),
@@ -6068,15 +6098,16 @@ async function appendSession(_projectDir, session) {
6068
6098
  session.transcriptPath ?? null,
6069
6099
  session.pid ?? null,
6070
6100
  session.pidStartedAt ?? null,
6071
- session.originalHeadSha ?? null
6101
+ session.originalHeadSha ?? null,
6102
+ opts?.reviveStopped ? 1 : 0
6072
6103
  );
6073
6104
  }
6074
- async function updateSessionStatus(_projectDir, sessionId, status) {
6105
+ async function updateSessionStatus(_projectDir, sessionId, status, endedAt) {
6075
6106
  const db4 = getSessionDb();
6076
6107
  const isTerminal = status === "completed" || status === "stopped";
6077
6108
  const result = isTerminal ? db4.prepare(
6078
- "UPDATE sessions SET status = ?, ended = datetime('now'), updated_at = datetime('now') WHERE session_id = ?"
6079
- ).run(status, sessionId) : db4.prepare(
6109
+ "UPDATE sessions SET status = ?, ended = COALESCE(?, datetime('now')), updated_at = datetime('now') WHERE session_id = ?"
6110
+ ).run(status, endedAt ?? null, sessionId) : db4.prepare(
6080
6111
  "UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE session_id = ?"
6081
6112
  ).run(status, sessionId);
6082
6113
  return result.changes > 0;
@@ -6385,8 +6416,8 @@ function scanKey(serversDir2, projectsDir, assignmentsDir2) {
6385
6416
  return `${serversDir2}\0${projectsDir}\0${assignmentsDir2 ?? ""}`;
6386
6417
  }
6387
6418
  function delay(ms) {
6388
- return new Promise((resolve34) => {
6389
- const timer2 = setTimeout(resolve34, ms);
6419
+ return new Promise((resolve38) => {
6420
+ const timer2 = setTimeout(resolve38, ms);
6390
6421
  if (typeof timer2.unref === "function") {
6391
6422
  timer2.unref();
6392
6423
  }
@@ -9324,6 +9355,431 @@ var init_api = __esm({
9324
9355
  }
9325
9356
  });
9326
9357
 
9358
+ // src/templates/cursor-rules.ts
9359
+ function renderCursorProtocol() {
9360
+ return `---
9361
+ description: Syntaur protocol rules for multi-agent coordination
9362
+ globs:
9363
+ alwaysApply: true
9364
+ ---
9365
+
9366
+ # Syntaur Protocol
9367
+
9368
+ You are working within the Syntaur protocol for multi-agent project coordination.
9369
+
9370
+ ## Directory Structure
9371
+
9372
+ \`\`\`
9373
+ ~/.syntaur/
9374
+ config.md
9375
+ projects/
9376
+ <project-slug>/
9377
+ manifest.md # Derived: root navigation (read-only)
9378
+ project.md # Human-authored: project overview (read-only)
9379
+ _index-assignments.md # Derived (read-only)
9380
+ _index-plans.md # Derived (read-only)
9381
+ _index-decisions.md # Derived (read-only)
9382
+ _status.md # Derived (read-only)
9383
+ assignments/
9384
+ <assignment-slug>/
9385
+ assignment.md # Agent-writable: source of truth for state (includes ## Todos)
9386
+ plan*.md # Agent-writable: versioned implementation plans (optional, one per ## Todos entry)
9387
+ progress.md # Agent-writable, append-only: timestamped progress log
9388
+ comments.md # CLI-mediated: threaded questions/notes/feedback (via \`syntaur comment\`)
9389
+ scratchpad.md # Agent-writable: working notes
9390
+ handoff.md # Agent-writable: append-only cross-ticket outbound at completion
9391
+ decision-record.md # Agent-writable: append-only decision log
9392
+ sessions/
9393
+ <session-id>/
9394
+ summary.md # Agent-writable: per-session continuity (single doc, overwritten)
9395
+ resources/
9396
+ _index.md # Derived (read-only)
9397
+ <resource-slug>.md # Shared-writable
9398
+ memories/
9399
+ _index.md # Derived (read-only)
9400
+ <memory-slug>.md # Shared-writable
9401
+ assignments/
9402
+ <assignment-id>/ # Standalone assignments \u2014 folder = UUID, \`project: null\`, slug display-only
9403
+ assignment.md
9404
+ plan*.md
9405
+ progress.md
9406
+ comments.md
9407
+ scratchpad.md
9408
+ handoff.md
9409
+ decision-record.md
9410
+ sessions/<session-id>/summary.md # Per-session continuity (same as project-nested)
9411
+ \`\`\`
9412
+
9413
+ ## Write Boundary Rules (CRITICAL)
9414
+
9415
+ ### Files you may WRITE:
9416
+ 1. **Your assignment folder** -- only the assignment you are currently working on:
9417
+ - \`assignment.md\`, \`plan*.md\` (0 or more versioned plan files), \`progress.md\`, \`scratchpad.md\`, \`handoff.md\` (cross-ticket outbound at completion), \`decision-record.md\`
9418
+ - \`sessions/<session-id>/summary.md\` -- per-session continuity (single doc per session id, overwritten on save). Distinct from \`handoff.md\`.
9419
+ - Path (project-nested): \`~/.syntaur/projects/<project>/assignments/<your-assignment>/\`
9420
+ - Path (standalone): \`~/.syntaur/assignments/<your-assignment-uuid>/\`
9421
+ 2. **Shared resources and memories** at the project level:
9422
+ - \`~/.syntaur/projects/<project>/resources/<slug>.md\`
9423
+ - \`~/.syntaur/projects/<project>/memories/<slug>.md\`
9424
+ 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.
9425
+
9426
+ > **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.
9427
+
9428
+ ### Files written only via CLI (never edit directly):
9429
+ - \`comments.md\` (any assignment) -- use \`syntaur comment <slug-or-uuid> "body" [--type question|note|feedback] [--reply-to <id>]\`
9430
+ - Another assignment's \`## Todos\` section -- use \`syntaur request <source> <target> "text"\` to request cross-assignment work
9431
+
9432
+ ### Files you must NEVER write:
9433
+ 1. \`project.md\` -- human-authored, read-only
9434
+ 2. \`manifest.md\` -- derived, rebuilt by tooling
9435
+ 3. Any file prefixed with \`_\` -- derived
9436
+ 4. Other agents' assignment folders (except via the CLI-mediated channels above)
9437
+ 5. Any files outside your workspace boundary
9438
+
9439
+ ## Assignment Lifecycle
9440
+
9441
+ | Status | Meaning |
9442
+ |--------|---------|
9443
+ | \`pending\` | Not yet started |
9444
+ | \`in_progress\` | Actively being worked on |
9445
+ | \`blocked\` | Manually blocked (requires blockedReason) |
9446
+ | \`review\` | Work complete, awaiting review |
9447
+ | \`completed\` | Done |
9448
+ | \`failed\` | Could not be completed |
9449
+
9450
+ ## Valid State Transitions
9451
+
9452
+ | From | Command | To |
9453
+ |------|---------|-----|
9454
+ | pending | start | in_progress |
9455
+ | pending | block | blocked |
9456
+ | in_progress | block | blocked |
9457
+ | in_progress | review | review |
9458
+ | in_progress | complete | completed |
9459
+ | in_progress | fail | failed |
9460
+ | blocked | unblock | in_progress |
9461
+ | review | start | in_progress |
9462
+ | review | complete | completed |
9463
+ | review | fail | failed |
9464
+
9465
+ ## Lifecycle Commands
9466
+
9467
+ Use the \`syntaur\` CLI for state transitions and coordination:
9468
+ - \`syntaur assign <slug> --agent <name> --project <project>\` -- set assignee
9469
+ - \`syntaur start <slug> --project <project>\` -- pending -> in_progress
9470
+ - \`syntaur review <slug> --project <project>\` -- in_progress -> review
9471
+ - \`syntaur complete <slug> --project <project>\` -- in_progress/review -> completed
9472
+ - \`syntaur block <slug> --project <project> --reason <text>\` -- block an assignment
9473
+ - \`syntaur unblock <slug> --project <project>\` -- unblock
9474
+ - \`syntaur fail <slug> --project <project>\` -- mark as failed
9475
+ - \`syntaur create-assignment "Title" [--type <type>] [--project <slug> | --one-off]\` -- create project-nested or standalone assignment
9476
+ - \`syntaur comment <slug-or-uuid> "body" --type question|note|feedback [--reply-to <id>]\` -- append to \`comments.md\` (questions support resolve toggle via dashboard)
9477
+ - \`syntaur request <source> <target> "text"\` -- append a todo to another assignment's \`## Todos\` annotated \`(from: <source>)\`
9478
+
9479
+ ## Playbooks
9480
+
9481
+ Playbooks are user-defined behavioral rules stored in \`~/.syntaur/playbooks/\`. Read the playbook manifest before starting work:
9482
+
9483
+ \`\`\`bash
9484
+ cat ~/.syntaur/playbooks/manifest.md
9485
+ \`\`\`
9486
+
9487
+ Follow the rules in each playbook. They take precedence over default conventions when they conflict.
9488
+
9489
+ ## Conventions
9490
+
9491
+ - 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.
9492
+ - Slugs are lowercase, hyphen-separated. Standalone assignment folders are named by UUID; \`slug\` is display-only in that case.
9493
+ - Always read \`project.md\` at the project level (when project-nested) before starting work.
9494
+ - Append timestamped entries to \`progress.md\` (never to \`assignment.md\`).
9495
+ - Record questions, notes, and feedback via \`syntaur comment\`. Never edit \`comments.md\` directly.
9496
+ - To route work to another assignment, use \`syntaur request\`.
9497
+ - Commit frequently with messages referencing the assignment slug.
9498
+ `;
9499
+ }
9500
+ function renderCursorAssignment(params2) {
9501
+ return `---
9502
+ description: Syntaur assignment context for ${params2.projectSlug}/${params2.assignmentSlug}
9503
+ globs:
9504
+ alwaysApply: true
9505
+ ---
9506
+
9507
+ # Current Assignment Context
9508
+
9509
+ - **Project:** ${params2.projectSlug}
9510
+ - **Assignment:** ${params2.assignmentSlug}
9511
+ - **Project directory:** ${params2.projectDir}
9512
+ - **Assignment directory:** ${params2.assignmentDir}
9513
+
9514
+ ## Reading Order
9515
+
9516
+ Before starting work, read these files in order:
9517
+ 1. \`${params2.projectDir}/project.md\` -- project overview and goals (project-nested assignments only)
9518
+ 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\`.
9519
+ 3. any \`${params2.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
9520
+ 4. \`${params2.assignmentDir}/progress.md\` -- reverse-chron progress log (if present)
9521
+ 5. \`${params2.assignmentDir}/comments.md\` -- threaded questions/notes/feedback (if present)
9522
+ 6. \`${params2.assignmentDir}/handoff.md\` -- cross-ticket outbound history (entries from prior agents/humans handing this assignment off)
9523
+ 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)
9524
+
9525
+ ## Your Writable Files
9526
+
9527
+ You may write directly to these files inside your assignment folder:
9528
+ - \`${params2.assignmentDir}/assignment.md\`
9529
+ - \`${params2.assignmentDir}/plan*.md\` (0 or more versioned plan files, e.g., \`plan.md\`, \`plan-v2.md\`)
9530
+ - \`${params2.assignmentDir}/progress.md\` (append timestamped entries, newest first)
9531
+ - \`${params2.assignmentDir}/scratchpad.md\`
9532
+ - \`${params2.assignmentDir}/handoff.md\`
9533
+ - \`${params2.assignmentDir}/decision-record.md\`
9534
+ - \`${params2.assignmentDir}/sessions/<session-id>/summary.md\` (per-session continuity)
9535
+
9536
+ 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.
9537
+
9538
+ 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.
9539
+ `;
9540
+ }
9541
+ var init_cursor_rules = __esm({
9542
+ "src/templates/cursor-rules.ts"() {
9543
+ "use strict";
9544
+ }
9545
+ });
9546
+
9547
+ // src/templates/codex-agents.ts
9548
+ function renderCodexAgents(params2) {
9549
+ return `# Syntaur Protocol -- Agent Instructions
9550
+
9551
+ This project uses the Syntaur protocol for multi-agent project coordination.
9552
+
9553
+ ## Current Assignment
9554
+
9555
+ - **Project:** ${params2.projectSlug}
9556
+ - **Assignment:** ${params2.assignmentSlug}
9557
+ - **Project directory:** ${params2.projectDir}
9558
+ - **Assignment directory:** ${params2.assignmentDir}
9559
+
9560
+ ## Preferred Workflow
9561
+
9562
+ If the global Syntaur Codex plugin is installed, prefer these workflows instead of ad hoc protocol edits:
9563
+
9564
+ - \`syntaur-operator\` agent -- use for broad Syntaur protocol work or when a task spans multiple lifecycle steps
9565
+ - \`syntaur-protocol\` -- background protocol and write-boundary rules
9566
+ - \`create-project\` -- scaffold a project
9567
+ - \`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)
9568
+ - \`grab-assignment\` -- claim work, create \`.syntaur/context.json\`, and register a session
9569
+ - \`plan-assignment\` -- write a versioned plan file (\`plan.md\`, \`plan-v2.md\`, ...) and link it from the \`## Todos\` section of \`assignment.md\`
9570
+ - \`complete-assignment\` -- write the cross-ticket \`handoff.md\` entry, append a final entry to \`progress.md\`, close the session, and transition state
9571
+ - \`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.
9572
+ - \`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\`.
9573
+ - \`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
9574
+ - \`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)
9575
+ - \`syntaur-worktree\` -- atomic worktree creation under \`<repository>/.worktrees/<branch>\` plus assign + start + context binding in one move
9576
+ - \`add-resource\` -- register a project-level resource (link to dashboard / doc / ticket); CLI regenerates \`_index.md\` server-side
9577
+ - \`add-memory\` -- capture a project-level Syntaur memory; CLI regenerates \`_index.md\` server-side (distinct from user-global Claude Code auto-memory)
9578
+ - \`list-assignments\` -- cross-project listing with filters by status, project, tag, age (scriptable; not the interactive \`browse\` TUI)
9579
+ - \`log-progress\` -- append a timestamped entry to the active \`progress.md\` and bump frontmatter (Keep Records Updated playbook)
9580
+ - \`set-workspace\` -- populate the four \`workspace.*\` fields in \`assignment.md\`; validates via \`syntaur doctor --assignment --json\` before writing
9581
+ - \`track-session\` -- manage tracked tmux sessions for the dashboard
9582
+
9583
+ If the plugin is unavailable, follow the same workflow manually with the \`syntaur\` CLI and keep the protocol files current yourself.
9584
+
9585
+ ## Reading Order
9586
+
9587
+ Before starting work, read these files in order:
9588
+ 1. \`${params2.projectDir}/manifest.md\` -- root navigation entry point (project-nested assignments only)
9589
+ 2. \`${params2.projectDir}/project.md\` -- project overview and goals (project-nested assignments only)
9590
+ 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\`.
9591
+ 4. any \`${params2.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
9592
+ 5. \`${params2.assignmentDir}/progress.md\` -- reverse-chron progress log (if present)
9593
+ 6. \`${params2.assignmentDir}/comments.md\` -- threaded questions/notes/feedback (if present)
9594
+ 7. \`${params2.assignmentDir}/handoff.md\` -- cross-ticket outbound history (entries from prior agents/humans handing this assignment off)
9595
+ 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)
9596
+
9597
+ ## Context File
9598
+
9599
+ - Treat \`.syntaur/context.json\` in the current working directory as the active assignment context when it exists.
9600
+ - 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.
9601
+ - 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.
9602
+
9603
+ ## Directory Structure
9604
+
9605
+ \`\`\`
9606
+ ~/.syntaur/
9607
+ config.md
9608
+ projects/
9609
+ <project-slug>/
9610
+ manifest.md # Derived: root navigation (read-only)
9611
+ project.md # Human-authored: project overview (read-only)
9612
+ _index-assignments.md # Derived (read-only)
9613
+ _index-plans.md # Derived (read-only)
9614
+ _index-decisions.md # Derived (read-only)
9615
+ _status.md # Derived (read-only)
9616
+ assignments/
9617
+ <assignment-slug>/
9618
+ assignment.md # Agent-writable: source of truth for state (includes ## Todos)
9619
+ plan*.md # Agent-writable: versioned implementation plans (optional, one per ## Todos entry)
9620
+ progress.md # Agent-writable, append-only: timestamped progress log
9621
+ comments.md # CLI-mediated: threaded questions/notes/feedback (via \`syntaur comment\`)
9622
+ scratchpad.md # Agent-writable: working notes
9623
+ handoff.md # Agent-writable: append-only cross-ticket outbound at completion
9624
+ decision-record.md # Agent-writable: append-only decision log
9625
+ sessions/
9626
+ <session-id>/
9627
+ summary.md # Agent-writable: per-session continuity (single doc, overwritten)
9628
+ resources/
9629
+ _index.md # Derived (read-only)
9630
+ <resource-slug>.md # Shared-writable
9631
+ memories/
9632
+ _index.md # Derived (read-only)
9633
+ <memory-slug>.md # Shared-writable
9634
+ assignments/
9635
+ <assignment-id>/ # Standalone assignments \u2014 folder = UUID, \`project: null\`, slug display-only
9636
+ assignment.md
9637
+ plan*.md
9638
+ progress.md
9639
+ comments.md
9640
+ scratchpad.md
9641
+ handoff.md
9642
+ decision-record.md
9643
+ sessions/<session-id>/summary.md # Per-session continuity (same as project-nested)
9644
+ \`\`\`
9645
+
9646
+ ## Write Boundary Rules (CRITICAL)
9647
+
9648
+ ### Files you may WRITE:
9649
+ 1. **Your assignment folder** -- only the assignment you are currently working on:
9650
+ - \`assignment.md\`, \`plan*.md\` (0 or more versioned plan files), \`progress.md\`, \`scratchpad.md\`, \`handoff.md\` (cross-ticket outbound at completion), \`decision-record.md\`
9651
+ - \`sessions/<session-id>/summary.md\` -- per-session continuity (single doc per session id, overwritten on save). Distinct from \`handoff.md\`.
9652
+ - Path: \`${params2.assignmentDir}/\`
9653
+ 2. **Shared resources and memories** at the project level:
9654
+ - \`${params2.projectDir}/resources/<slug>.md\`
9655
+ - \`${params2.projectDir}/memories/<slug>.md\`
9656
+ 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.
9657
+
9658
+ > **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.
9659
+
9660
+ ### Files written only via CLI (never edit directly):
9661
+ - \`comments.md\` (any assignment) -- use \`syntaur comment <slug-or-uuid> "body" [--type question|note|feedback] [--reply-to <id>]\`
9662
+ - Another assignment's \`## Todos\` section -- use \`syntaur request <source> <target> "text"\` to request cross-assignment work
9663
+
9664
+ ### Files you must NEVER write:
9665
+ 1. \`project.md\` -- human-authored, read-only
9666
+ 2. \`manifest.md\` -- derived, rebuilt by tooling
9667
+ 3. Any file prefixed with \`_\` -- derived
9668
+ 4. Other agents' assignment folders (except via the CLI-mediated channels above)
9669
+ 5. Any files outside your workspace boundary
9670
+
9671
+ ## Assignment Lifecycle
9672
+
9673
+ | Status | Meaning |
9674
+ |--------|---------|
9675
+ | \`pending\` | Not yet started |
9676
+ | \`in_progress\` | Actively being worked on |
9677
+ | \`blocked\` | Manually blocked (requires blockedReason) |
9678
+ | \`review\` | Work complete, awaiting review |
9679
+ | \`completed\` | Done |
9680
+ | \`failed\` | Could not be completed |
9681
+
9682
+ ## Valid State Transitions
9683
+
9684
+ | From | Command | To |
9685
+ |------|---------|-----|
9686
+ | pending | start | in_progress |
9687
+ | pending | block | blocked |
9688
+ | in_progress | block | blocked |
9689
+ | in_progress | review | review |
9690
+ | in_progress | complete | completed |
9691
+ | in_progress | fail | failed |
9692
+ | blocked | unblock | in_progress |
9693
+ | review | start | in_progress |
9694
+ | review | complete | completed |
9695
+ | review | fail | failed |
9696
+
9697
+ ## Lifecycle Commands
9698
+
9699
+ Use the \`syntaur\` CLI for state transitions and coordination:
9700
+ - \`syntaur assign ${params2.assignmentSlug} --agent <name> --project ${params2.projectSlug}\` -- set assignee
9701
+ - \`syntaur start ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- pending -> in_progress
9702
+ - \`syntaur review ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- in_progress -> review
9703
+ - \`syntaur complete ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- in_progress/review -> completed
9704
+ - \`syntaur block ${params2.assignmentSlug} --project ${params2.projectSlug} --reason <text>\` -- block
9705
+ - \`syntaur unblock ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- unblock
9706
+ - \`syntaur fail ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- mark as failed
9707
+ - \`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)
9708
+ - \`syntaur request ${params2.assignmentSlug} <target-slug-or-uuid> "text"\` -- append a todo to another assignment's \`## Todos\` annotated \`(from: ${params2.assignmentSlug})\`
9709
+ - \`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\`).
9710
+ - \`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.
9711
+
9712
+ ## Troubleshooting
9713
+
9714
+ If Syntaur state looks inconsistent (missing files, stale manifests, unexpected hook blocks), run \`syntaur doctor\` to diagnose. Use \`--json\` for structured output.
9715
+
9716
+ ## Playbooks
9717
+
9718
+ Playbooks are user-defined behavioral rules stored in \`~/.syntaur/playbooks/\`. Before starting work, read the playbook manifest and then each referenced playbook:
9719
+
9720
+ \`\`\`bash
9721
+ cat ~/.syntaur/playbooks/manifest.md
9722
+ \`\`\`
9723
+
9724
+ 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.
9725
+
9726
+ ## Conventions
9727
+
9728
+ - 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.
9729
+ - Slugs are lowercase, hyphen-separated. For standalone assignments, \`slug\` is display-only; the folder is named by the UUID.
9730
+ - Always read \`project.md\` at the project level (when project-nested) before starting work.
9731
+ - Keep \`assignment.md\` acceptance criteria and \`## Todos\` updated as work lands; append timestamped entries to \`progress.md\` (never to \`assignment.md\`).
9732
+ - 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.
9733
+ - 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.
9734
+ - Record questions, notes, and feedback via \`syntaur comment\`. Never edit \`comments.md\` directly. Resolve questions via the dashboard UI (toggle on the question entry).
9735
+ - To route work to another assignment, use \`syntaur request\`.
9736
+ - Commit frequently with messages referencing the assignment slug.
9737
+ `;
9738
+ }
9739
+ var init_codex_agents = __esm({
9740
+ "src/templates/codex-agents.ts"() {
9741
+ "use strict";
9742
+ }
9743
+ });
9744
+
9745
+ // src/templates/opencode-config.ts
9746
+ function renderOpenCodeConfig(params2) {
9747
+ const config = {
9748
+ instructions: [
9749
+ `Read AGENTS.md in this directory for Syntaur protocol (v2.0) instructions.`,
9750
+ `Read ${params2.projectDir}/project.md for project overview (project-nested assignments only).`,
9751
+ `Append timestamped progress entries to the assignment's progress.md (not to assignment.md).`,
9752
+ `Use 'syntaur comment <slug-or-uuid> "body" --type question|note|feedback' to append to comments.md \u2014 never edit it directly.`,
9753
+ `Use 'syntaur request <source> <target> "text"' to append a todo to another assignment's ## Todos.`,
9754
+ `Assignment folders are project-nested at ~/.syntaur/projects/<slug>/assignments/<aslug>/ or standalone at ~/.syntaur/assignments/<uuid>/ (project: null, slug display-only).`
9755
+ ]
9756
+ };
9757
+ return JSON.stringify(config, null, 2) + "\n";
9758
+ }
9759
+ var init_opencode_config = __esm({
9760
+ "src/templates/opencode-config.ts"() {
9761
+ "use strict";
9762
+ }
9763
+ });
9764
+
9765
+ // src/templates/hermes-soul.ts
9766
+ function renderHermesSoul(params2) {
9767
+ const body = renderCodexAgents(params2);
9768
+ return `# SOUL -- Syntaur Protocol Operator
9769
+
9770
+ This agent follows the Syntaur protocol for multi-agent project coordination.
9771
+ Hermes loads this file as part of its identity / system context; treat the
9772
+ Write Boundary Rules and Lifecycle sections below as binding.
9773
+
9774
+ ${body}`;
9775
+ }
9776
+ var init_hermes_soul = __esm({
9777
+ "src/templates/hermes-soul.ts"() {
9778
+ "use strict";
9779
+ init_codex_agents();
9780
+ }
9781
+ });
9782
+
9327
9783
  // src/lifecycle/recompute.ts
9328
9784
  var recompute_exports = {};
9329
9785
  __export(recompute_exports, {
@@ -9563,20 +10019,370 @@ var LOCK_FILE, LOCK_STALE_MS, LOCK_WAIT_MS, LOCK_MAX_WAITS, CAS_RETRIES, MIGRATI
9563
10019
  var init_recompute = __esm({
9564
10020
  "src/lifecycle/recompute.ts"() {
9565
10021
  "use strict";
9566
- init_config2();
9567
- init_fs();
10022
+ init_config2();
10023
+ init_fs();
10024
+ init_paths();
10025
+ init_timestamp();
10026
+ init_facts();
10027
+ init_derive();
10028
+ init_frontmatter();
10029
+ init_types();
10030
+ LOCK_FILE = ".derive.lock";
10031
+ LOCK_STALE_MS = 3e4;
10032
+ LOCK_WAIT_MS = 50;
10033
+ LOCK_MAX_WAITS = 100;
10034
+ CAS_RETRIES = 3;
10035
+ MIGRATION_MARKER = "derive-migrated";
10036
+ }
10037
+ });
10038
+
10039
+ // src/utils/transcript.ts
10040
+ import { open as open2 } from "fs/promises";
10041
+ async function derivePathFromTranscript(transcriptPath) {
10042
+ if (!transcriptPath) return null;
10043
+ let handle;
10044
+ try {
10045
+ handle = await open2(transcriptPath, "r");
10046
+ } catch {
10047
+ return null;
10048
+ }
10049
+ try {
10050
+ const stream = handle.createReadStream({ encoding: "utf-8" });
10051
+ let buffer = "";
10052
+ let scanned = 0;
10053
+ for await (const chunk of stream) {
10054
+ buffer += chunk;
10055
+ let nl = buffer.indexOf("\n");
10056
+ while (nl !== -1) {
10057
+ const line = buffer.slice(0, nl);
10058
+ buffer = buffer.slice(nl + 1);
10059
+ const cwd = extractCwd(line);
10060
+ if (cwd) {
10061
+ stream.destroy();
10062
+ return cwd;
10063
+ }
10064
+ scanned++;
10065
+ if (scanned >= MAX_LINES_SCANNED) {
10066
+ stream.destroy();
10067
+ return null;
10068
+ }
10069
+ nl = buffer.indexOf("\n");
10070
+ }
10071
+ }
10072
+ if (buffer.length > 0) {
10073
+ const cwd = extractCwd(buffer);
10074
+ if (cwd) return cwd;
10075
+ }
10076
+ return null;
10077
+ } finally {
10078
+ await handle.close().catch(() => {
10079
+ });
10080
+ }
10081
+ }
10082
+ function extractCwd(line) {
10083
+ const trimmed = line.trim();
10084
+ if (trimmed.length === 0 || trimmed[0] !== "{") return null;
10085
+ try {
10086
+ const parsed = JSON.parse(trimmed);
10087
+ if (typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
10088
+ return parsed.cwd;
10089
+ }
10090
+ } catch {
10091
+ }
10092
+ return null;
10093
+ }
10094
+ var MAX_LINES_SCANNED;
10095
+ var init_transcript = __esm({
10096
+ "src/utils/transcript.ts"() {
10097
+ "use strict";
10098
+ MAX_LINES_SCANNED = 50;
10099
+ }
10100
+ });
10101
+
10102
+ // src/utils/process-info.ts
10103
+ import { execFileSync as execFileSync2 } from "child_process";
10104
+ function captureProcessStartedAt(pid) {
10105
+ if (!Number.isFinite(pid) || pid <= 0) return null;
10106
+ try {
10107
+ const out = execFileSync2("ps", ["-o", "lstart=", "-p", String(pid)], {
10108
+ encoding: "utf8",
10109
+ stdio: ["ignore", "pipe", "ignore"]
10110
+ });
10111
+ const trimmed = out.trim();
10112
+ return trimmed === "" ? null : trimmed;
10113
+ } catch {
10114
+ return null;
10115
+ }
10116
+ }
10117
+ var init_process_info = __esm({
10118
+ "src/utils/process-info.ts"() {
10119
+ "use strict";
10120
+ }
10121
+ });
10122
+
10123
+ // src/usage/cwd-extractor.ts
10124
+ import { open as open3, readdir as readdir11, stat as stat2 } from "fs/promises";
10125
+ import { join as join3 } from "path";
10126
+ import { homedir as homedir3 } from "os";
10127
+ async function extractClaudeSessionMeta(jsonlPath) {
10128
+ const cwd = await derivePathFromTranscript(jsonlPath);
10129
+ if (!cwd) return null;
10130
+ const basename6 = jsonlPath.split("/").pop() ?? "";
10131
+ const sessionId = basename6.replace(/\.jsonl$/, "");
10132
+ if (!sessionId) return null;
10133
+ const startTs = await readFirstTimestamp(jsonlPath);
10134
+ const endTs = await readLastTimestamp(jsonlPath);
10135
+ return {
10136
+ tool: "claude",
10137
+ sessionId,
10138
+ cwd,
10139
+ startTs,
10140
+ endTs,
10141
+ path: jsonlPath
10142
+ };
10143
+ }
10144
+ async function extractCodexSessionMeta(jsonlPath) {
10145
+ let handle;
10146
+ try {
10147
+ handle = await open3(jsonlPath, "r");
10148
+ } catch {
10149
+ return null;
10150
+ }
10151
+ try {
10152
+ const stream = handle.createReadStream({ encoding: "utf-8" });
10153
+ let buffer = "";
10154
+ let firstLine = null;
10155
+ for await (const chunk of stream) {
10156
+ buffer += chunk;
10157
+ const nl = buffer.indexOf("\n");
10158
+ if (nl !== -1) {
10159
+ firstLine = buffer.slice(0, nl);
10160
+ stream.destroy();
10161
+ break;
10162
+ }
10163
+ }
10164
+ if (!firstLine && buffer.length > 0) firstLine = buffer;
10165
+ if (!firstLine) return null;
10166
+ let parsed;
10167
+ try {
10168
+ parsed = JSON.parse(firstLine);
10169
+ } catch {
10170
+ return null;
10171
+ }
10172
+ if (!parsed || typeof parsed !== "object") return null;
10173
+ const obj = parsed;
10174
+ if (obj.type !== "session_meta") return null;
10175
+ const timestamp = typeof obj.timestamp === "string" ? obj.timestamp : null;
10176
+ const payload = obj.payload;
10177
+ const id = payload && typeof payload.id === "string" ? payload.id : null;
10178
+ const cwd = payload && typeof payload.cwd === "string" ? payload.cwd : null;
10179
+ if (!timestamp || !id || !cwd) return null;
10180
+ const endTs = await readLastTimestamp(jsonlPath) ?? timestamp;
10181
+ return {
10182
+ tool: "codex",
10183
+ sessionId: id,
10184
+ cwd,
10185
+ startTs: timestamp,
10186
+ endTs,
10187
+ path: jsonlPath
10188
+ };
10189
+ } finally {
10190
+ await handle.close().catch(() => {
10191
+ });
10192
+ }
10193
+ }
10194
+ async function* walkClaudeProjects(opts = {}) {
10195
+ const root = expandHome(opts.root ?? "~/.claude/projects");
10196
+ const dirs = await listDirSafe(root);
10197
+ for (const dirent of dirs) {
10198
+ if (!dirent.isDirectory) continue;
10199
+ const dirPath = join3(root, dirent.name);
10200
+ const files = await listDirSafe(dirPath);
10201
+ let cachedCwd = null;
10202
+ for (const f of files) {
10203
+ if (!f.isFile || !f.name.endsWith(".jsonl")) continue;
10204
+ const filePath = join3(dirPath, f.name);
10205
+ if (opts.sinceMtimeMs !== void 0) {
10206
+ const mtime = await mtimeMs(filePath);
10207
+ if (mtime !== null && mtime < opts.sinceMtimeMs) continue;
10208
+ }
10209
+ let meta;
10210
+ if (cachedCwd) {
10211
+ const sessionId = f.name.replace(/\.jsonl$/, "");
10212
+ const startTs = await readFirstTimestamp(filePath);
10213
+ const endTs = await readLastTimestamp(filePath);
10214
+ meta = { tool: "claude", sessionId, cwd: cachedCwd, startTs, endTs, path: filePath };
10215
+ } else {
10216
+ meta = await extractClaudeSessionMeta(filePath);
10217
+ if (meta) cachedCwd = meta.cwd;
10218
+ }
10219
+ if (meta) yield meta;
10220
+ }
10221
+ }
10222
+ }
10223
+ async function* walkCodexSessions(opts = {}) {
10224
+ const root = resolveCodexSessionsRoot(opts.root);
10225
+ for await (const filePath of walkJsonlRecursive(root)) {
10226
+ const basename6 = filePath.split("/").pop() ?? "";
10227
+ if (!basename6.endsWith(".jsonl")) continue;
10228
+ if (opts.sinceMtimeMs !== void 0) {
10229
+ const mtime = await mtimeMs(filePath);
10230
+ if (mtime !== null && mtime < opts.sinceMtimeMs) continue;
10231
+ }
10232
+ const meta = await extractCodexSessionMeta(filePath);
10233
+ if (meta) yield meta;
10234
+ }
10235
+ }
10236
+ function resolveCodexSessionsRoot(override) {
10237
+ if (override) return expandHome(override);
10238
+ const fromSessionsEnv = process.env.CODEX_SESSIONS_DIR;
10239
+ if (fromSessionsEnv && fromSessionsEnv.length > 0) return expandHome(fromSessionsEnv);
10240
+ const fromHomeEnv = process.env.CODEX_HOME;
10241
+ if (fromHomeEnv && fromHomeEnv.length > 0) return join3(expandHome(fromHomeEnv), "sessions");
10242
+ return join3(homedir3(), ".codex", "sessions");
10243
+ }
10244
+ async function listDirSafe(path) {
10245
+ try {
10246
+ const entries = await readdir11(path, { withFileTypes: true });
10247
+ return entries.map((e) => ({
10248
+ name: e.name,
10249
+ isFile: e.isFile(),
10250
+ isDirectory: e.isDirectory()
10251
+ }));
10252
+ } catch {
10253
+ return [];
10254
+ }
10255
+ }
10256
+ async function* walkJsonlRecursive(root) {
10257
+ const stack = [root];
10258
+ while (stack.length > 0) {
10259
+ const current = stack.pop();
10260
+ const entries = await listDirSafe(current);
10261
+ for (const e of entries) {
10262
+ const full = join3(current, e.name);
10263
+ if (e.isDirectory) {
10264
+ stack.push(full);
10265
+ } else if (e.isFile && e.name.endsWith(".jsonl")) {
10266
+ yield full;
10267
+ }
10268
+ }
10269
+ }
10270
+ }
10271
+ async function mtimeMs(path) {
10272
+ try {
10273
+ const s = await stat2(path);
10274
+ return s.mtimeMs;
10275
+ } catch {
10276
+ return null;
10277
+ }
10278
+ }
10279
+ async function readFirstTimestamp(path) {
10280
+ let handle;
10281
+ try {
10282
+ handle = await open3(path, "r");
10283
+ } catch {
10284
+ return null;
10285
+ }
10286
+ try {
10287
+ const stream = handle.createReadStream({ encoding: "utf-8" });
10288
+ let buffer = "";
10289
+ let scanned = 0;
10290
+ for await (const chunk of stream) {
10291
+ buffer += chunk;
10292
+ let nl = buffer.indexOf("\n");
10293
+ while (nl !== -1) {
10294
+ const line = buffer.slice(0, nl);
10295
+ buffer = buffer.slice(nl + 1);
10296
+ const ts = extractTimestamp(line);
10297
+ if (ts) {
10298
+ stream.destroy();
10299
+ return ts;
10300
+ }
10301
+ scanned++;
10302
+ if (scanned >= SCAN_LINE_CAP) {
10303
+ stream.destroy();
10304
+ return null;
10305
+ }
10306
+ nl = buffer.indexOf("\n");
10307
+ }
10308
+ }
10309
+ if (buffer.length > 0) return extractTimestamp(buffer);
10310
+ return null;
10311
+ } finally {
10312
+ await handle.close().catch(() => {
10313
+ });
10314
+ }
10315
+ }
10316
+ async function readLastTimestamp(path) {
10317
+ let handle;
10318
+ try {
10319
+ handle = await open3(path, "r");
10320
+ } catch {
10321
+ return null;
10322
+ }
10323
+ try {
10324
+ const stats = await handle.stat();
10325
+ const size = stats.size;
10326
+ if (size === 0) return null;
10327
+ for (const windowBytes of [TAIL_READ_BYTES, TAIL_READ_BYTES_MAX]) {
10328
+ const start = Math.max(0, size - windowBytes);
10329
+ const length = size - start;
10330
+ const buf = Buffer.alloc(length);
10331
+ await handle.read(buf, 0, length, start);
10332
+ const text = buf.toString("utf-8");
10333
+ const lines = text.split("\n");
10334
+ if (start > 0) lines.shift();
10335
+ for (let i = lines.length - 1; i >= 0; i--) {
10336
+ const ts = extractTimestamp(lines[i]);
10337
+ if (ts) return ts;
10338
+ }
10339
+ if (start === 0) break;
10340
+ }
10341
+ return null;
10342
+ } finally {
10343
+ await handle.close().catch(() => {
10344
+ });
10345
+ }
10346
+ }
10347
+ function extractTimestamp(line) {
10348
+ const trimmed = line.trim();
10349
+ if (trimmed.length === 0 || trimmed[0] !== "{") return null;
10350
+ try {
10351
+ const parsed = JSON.parse(trimmed);
10352
+ if (typeof parsed.timestamp === "string" && parsed.timestamp.length > 0) {
10353
+ return parsed.timestamp;
10354
+ }
10355
+ } catch {
10356
+ }
10357
+ return null;
10358
+ }
10359
+ var SCAN_LINE_CAP, TAIL_READ_BYTES, TAIL_READ_BYTES_MAX;
10360
+ var init_cwd_extractor = __esm({
10361
+ "src/usage/cwd-extractor.ts"() {
10362
+ "use strict";
9568
10363
  init_paths();
9569
- init_timestamp();
9570
- init_facts();
9571
- init_derive();
9572
- init_frontmatter();
9573
- init_types();
9574
- LOCK_FILE = ".derive.lock";
9575
- LOCK_STALE_MS = 3e4;
9576
- LOCK_WAIT_MS = 50;
9577
- LOCK_MAX_WAITS = 100;
9578
- CAS_RETRIES = 3;
9579
- MIGRATION_MARKER = "derive-migrated";
10364
+ init_transcript();
10365
+ SCAN_LINE_CAP = 50;
10366
+ TAIL_READ_BYTES = 8 * 1024;
10367
+ TAIL_READ_BYTES_MAX = 64 * 1024;
10368
+ }
10369
+ });
10370
+
10371
+ // src/utils/session-id.ts
10372
+ import { execFileSync as execFileSync3 } from "child_process";
10373
+ import { mkdirSync, readFileSync, statSync as statSync3, writeFileSync } from "fs";
10374
+ import { homedir as homedir4 } from "os";
10375
+ import { dirname as dirname5, join as join4 } from "path";
10376
+ function isSafeSessionId(value) {
10377
+ return typeof value === "string" && value.length > 0 && value.length <= 256 && SAFE_SESSION_ID.test(value);
10378
+ }
10379
+ var SAFE_SESSION_ID;
10380
+ var init_session_id = __esm({
10381
+ "src/utils/session-id.ts"() {
10382
+ "use strict";
10383
+ init_process_info();
10384
+ init_cwd_extractor();
10385
+ SAFE_SESSION_ID = /^[A-Za-z0-9_-]+$/;
9580
10386
  }
9581
10387
  });
9582
10388
 
@@ -9627,6 +10433,420 @@ var init_assignment_todos = __esm({
9627
10433
  }
9628
10434
  });
9629
10435
 
10436
+ // src/targets/renderers.ts
10437
+ var RENDERERS;
10438
+ var init_renderers = __esm({
10439
+ "src/targets/renderers.ts"() {
10440
+ "use strict";
10441
+ init_cursor_rules();
10442
+ init_codex_agents();
10443
+ init_opencode_config();
10444
+ init_hermes_soul();
10445
+ RENDERERS = {
10446
+ codexAgents: (ctx) => renderCodexAgents(ctx),
10447
+ cursorProtocol: () => renderCursorProtocol(),
10448
+ cursorAssignment: (ctx) => renderCursorAssignment(ctx),
10449
+ openCodeConfig: (ctx) => renderOpenCodeConfig({ projectDir: ctx.projectDir }),
10450
+ hermesSoul: (ctx) => renderHermesSoul(ctx)
10451
+ };
10452
+ }
10453
+ });
10454
+
10455
+ // src/targets/user-descriptors.ts
10456
+ import { resolve as resolve34 } from "path";
10457
+ import { readFile as readFile23, readdir as readdir16 } from "fs/promises";
10458
+ var VALID_RENDERER_KEYS;
10459
+ var init_user_descriptors = __esm({
10460
+ "src/targets/user-descriptors.ts"() {
10461
+ "use strict";
10462
+ init_fs();
10463
+ init_paths();
10464
+ init_renderers();
10465
+ VALID_RENDERER_KEYS = new Set(Object.keys(RENDERERS));
10466
+ }
10467
+ });
10468
+
10469
+ // src/targets/registry.ts
10470
+ import { homedir as homedir6 } from "os";
10471
+ import { join as join8, resolve as resolve35 } from "path";
10472
+ function home(...segments) {
10473
+ return resolve35(homedir6(), ...segments);
10474
+ }
10475
+ function hermesHome() {
10476
+ const env = process.env.HERMES_HOME;
10477
+ return env && env.length > 0 ? resolve35(env) : home(".hermes");
10478
+ }
10479
+ function hermesSkillsDir() {
10480
+ return resolve35(hermesHome(), "skills");
10481
+ }
10482
+ function codexHome() {
10483
+ const env = process.env.CODEX_HOME;
10484
+ return env && env.length > 0 ? resolve35(env) : home(".codex");
10485
+ }
10486
+ function toDiscovered(meta) {
10487
+ if (!meta) return null;
10488
+ return {
10489
+ sessionId: meta.sessionId,
10490
+ cwd: meta.cwd,
10491
+ startedAt: meta.startTs,
10492
+ endedAt: meta.endTs,
10493
+ transcriptPath: meta.path
10494
+ };
10495
+ }
10496
+ var detectDir, claudeSessions, codexSessions, AGENT_TARGETS, AGENT_TARGETS_BY_ID;
10497
+ var init_registry = __esm({
10498
+ "src/targets/registry.ts"() {
10499
+ "use strict";
10500
+ init_fs();
10501
+ init_cwd_extractor();
10502
+ init_user_descriptors();
10503
+ detectDir = (dir) => () => fileExists(dir);
10504
+ claudeSessions = {
10505
+ globs: (root) => [join8(root ?? home(".claude", "projects"), "*", "*.jsonl")],
10506
+ parse: async (file) => toDiscovered(await extractClaudeSessionMeta(file)),
10507
+ walk: async function* (opts = {}) {
10508
+ for await (const meta of walkClaudeProjects({ root: opts.root, sinceMtimeMs: opts.sinceMtimeMs })) {
10509
+ const d = toDiscovered(meta);
10510
+ if (d) yield d;
10511
+ }
10512
+ }
10513
+ };
10514
+ codexSessions = {
10515
+ globs: (root) => [join8(root ?? resolveCodexSessionsRoot(), "**", "*.jsonl")],
10516
+ parse: async (file) => toDiscovered(await extractCodexSessionMeta(file)),
10517
+ walk: async function* (opts = {}) {
10518
+ for await (const meta of walkCodexSessions({ root: opts.root, sinceMtimeMs: opts.sinceMtimeMs })) {
10519
+ const d = toDiscovered(meta);
10520
+ if (d) yield d;
10521
+ }
10522
+ }
10523
+ };
10524
+ AGENT_TARGETS = [
10525
+ {
10526
+ id: "cursor",
10527
+ displayName: "Cursor",
10528
+ skillsShAgentId: "cursor",
10529
+ detect: detectDir(home(".cursor")),
10530
+ skillsDir: { global: home(".cursor", "skills") },
10531
+ instructions: {
10532
+ files: [
10533
+ { path: ".cursor/rules/syntaur-protocol.mdc", renderer: "cursorProtocol" },
10534
+ { path: ".cursor/rules/syntaur-assignment.mdc", renderer: "cursorAssignment" }
10535
+ ]
10536
+ }
10537
+ },
10538
+ {
10539
+ // codex is BOTH an adapter (writes AGENTS.md) AND a native plugin.
10540
+ id: "codex",
10541
+ displayName: "Codex",
10542
+ skillsShAgentId: "codex",
10543
+ nativePlugin: "codex",
10544
+ detect: detectDir(codexHome()),
10545
+ skillsDir: { global: resolve35(codexHome(), "skills") },
10546
+ instructions: { files: [{ path: "AGENTS.md", renderer: "codexAgents" }] },
10547
+ sessions: codexSessions
10548
+ },
10549
+ {
10550
+ id: "opencode",
10551
+ displayName: "OpenCode",
10552
+ skillsShAgentId: "opencode",
10553
+ detect: detectDir(home(".config", "opencode")),
10554
+ skillsDir: { global: home(".config", "opencode", "skills") },
10555
+ instructions: {
10556
+ files: [
10557
+ { path: "AGENTS.md", renderer: "codexAgents" },
10558
+ { path: "opencode.json", renderer: "openCodeConfig" }
10559
+ ]
10560
+ }
10561
+ },
10562
+ {
10563
+ // claude has NO adapter today (not in the old SUPPORTED_FRAMEWORKS) — the
10564
+ // full plugin path owns its skills/hooks/commands. Native-plugin only.
10565
+ id: "claude",
10566
+ displayName: "Claude Code",
10567
+ skillsShAgentId: "claude-code",
10568
+ nativePlugin: "claude",
10569
+ detect: detectDir(home(".claude")),
10570
+ skillsDir: { global: home(".claude", "skills") },
10571
+ sessions: claudeSessions
10572
+ },
10573
+ {
10574
+ id: "pi",
10575
+ displayName: "Pi",
10576
+ skillsShAgentId: "pi",
10577
+ detect: detectDir(home(".pi")),
10578
+ skillsDir: { global: home(".pi", "agent", "skills") },
10579
+ instructions: { files: [{ path: "AGENTS.md", renderer: "codexAgents" }] },
10580
+ tier3: {
10581
+ kind: "pi-extension",
10582
+ source: "platforms/pi/extensions/syntaur",
10583
+ installDir: () => home(".pi", "agent", "extensions", "syntaur"),
10584
+ entry: "index.ts"
10585
+ }
10586
+ },
10587
+ {
10588
+ id: "openclaw",
10589
+ displayName: "OpenClaw",
10590
+ skillsShAgentId: "openclaw",
10591
+ detect: detectDir(home(".openclaw")),
10592
+ skillsDir: { global: home(".openclaw", "skills") },
10593
+ instructions: { files: [{ path: "AGENTS.md", renderer: "codexAgents" }] },
10594
+ // OpenClaw runs on pi-coding-agent (design memo), so it reuses the pi
10595
+ // extension SOURCE; only the install dir differs.
10596
+ tier3: {
10597
+ kind: "pi-extension",
10598
+ source: "platforms/pi/extensions/syntaur",
10599
+ installDir: () => home(".openclaw", "extensions", "syntaur"),
10600
+ entry: "index.ts"
10601
+ }
10602
+ },
10603
+ {
10604
+ id: "hermes",
10605
+ displayName: "Hermes Agent",
10606
+ skillsShAgentId: "hermes-agent",
10607
+ detect: () => fileExists(hermesHome()),
10608
+ skillsDir: { global: hermesSkillsDir() },
10609
+ instructions: { files: [{ path: "SOUL.md", renderer: "hermesSoul" }] },
10610
+ tier3: {
10611
+ kind: "hermes-plugin",
10612
+ source: "platforms/hermes/plugins/syntaur",
10613
+ installDir: () => resolve35(hermesHome(), "plugins", "syntaur"),
10614
+ entry: "plugin.yaml"
10615
+ }
10616
+ }
10617
+ ];
10618
+ AGENT_TARGETS_BY_ID = Object.fromEntries(
10619
+ AGENT_TARGETS.map((t) => [t.id, t])
10620
+ );
10621
+ }
10622
+ });
10623
+
10624
+ // src/sessions/scanner.ts
10625
+ var scanner_exports2 = {};
10626
+ __export(scanner_exports2, {
10627
+ scanSessions: () => scanSessions
10628
+ });
10629
+ import { execFile as execFile3, execFileSync as execFileSync4 } from "child_process";
10630
+ import { promisify as promisify3 } from "util";
10631
+ import { statSync as statSync4 } from "fs";
10632
+ import { readFile as readFile24 } from "fs/promises";
10633
+ import { resolve as resolve36 } from "path";
10634
+ function emptySummary() {
10635
+ return { discovered: 0, inserted: 0, revived: 0, swept: 0, skipped: 0, changed: false };
10636
+ }
10637
+ function defaultStatMtimeMs(path) {
10638
+ try {
10639
+ return statSync4(path).mtimeMs;
10640
+ } catch {
10641
+ return null;
10642
+ }
10643
+ }
10644
+ function defaultIsPidAlive(pid) {
10645
+ if (!Number.isFinite(pid) || pid <= 0) return false;
10646
+ try {
10647
+ process.kill(pid, 0);
10648
+ return true;
10649
+ } catch (err) {
10650
+ return err.code === "EPERM";
10651
+ }
10652
+ }
10653
+ function defaultPidStartedAt(pid) {
10654
+ if (!Number.isFinite(pid) || pid <= 0) return null;
10655
+ try {
10656
+ const out = execFileSync4("ps", ["-o", "lstart=", "-p", String(pid)], {
10657
+ encoding: "utf8",
10658
+ stdio: ["ignore", "pipe", "ignore"]
10659
+ });
10660
+ const trimmed = out.trim();
10661
+ return trimmed === "" ? null : trimmed;
10662
+ } catch {
10663
+ return null;
10664
+ }
10665
+ }
10666
+ async function defaultOpenFiles(files) {
10667
+ const open5 = /* @__PURE__ */ new Set();
10668
+ for (let i = 0; i < files.length; i += LSOF_CHUNK) {
10669
+ const chunk = files.slice(i, i + LSOF_CHUNK);
10670
+ let stdout = "";
10671
+ try {
10672
+ const result = await execFileAsync("lsof", ["-Fn", "--", ...chunk], {
10673
+ maxBuffer: 8 * 1024 * 1024
10674
+ });
10675
+ stdout = result.stdout;
10676
+ } catch (err) {
10677
+ const maybe = err.stdout;
10678
+ stdout = typeof maybe === "string" ? maybe : "";
10679
+ }
10680
+ for (const line of stdout.split("\n")) {
10681
+ if (line.startsWith("n") && line.length > 1) open5.add(line.slice(1));
10682
+ }
10683
+ }
10684
+ return open5;
10685
+ }
10686
+ async function readContextLink(cwd, cache2) {
10687
+ if (cache2.has(cwd)) return cache2.get(cwd);
10688
+ let link = null;
10689
+ const path = resolve36(cwd, ".syntaur", "context.json");
10690
+ if (await fileExists(path)) {
10691
+ try {
10692
+ const parsed = JSON.parse(await readFile24(path, "utf-8"));
10693
+ link = {
10694
+ projectSlug: typeof parsed.projectSlug === "string" ? parsed.projectSlug : null,
10695
+ assignmentSlug: typeof parsed.assignmentSlug === "string" ? parsed.assignmentSlug : null
10696
+ };
10697
+ } catch {
10698
+ link = { projectSlug: null, assignmentSlug: null };
10699
+ }
10700
+ }
10701
+ cache2.set(cwd, link);
10702
+ return link;
10703
+ }
10704
+ function readWatermark() {
10705
+ const db4 = getSessionDb();
10706
+ const row = db4.prepare("SELECT value FROM meta WHERE key = ?").get(WATERMARK_KEY);
10707
+ if (!row) return null;
10708
+ const parsed = Number.parseInt(row.value, 10);
10709
+ return Number.isFinite(parsed) ? parsed : null;
10710
+ }
10711
+ function writeWatermark(ms) {
10712
+ const db4 = getSessionDb();
10713
+ db4.prepare(
10714
+ "INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
10715
+ ).run(WATERMARK_KEY, String(ms));
10716
+ }
10717
+ async function scanSessions(opts = {}, deps = {}) {
10718
+ const summary = emptySummary();
10719
+ const autoTrack = deps.autoTrack ?? (await readConfig()).session.autoTrack;
10720
+ if (autoTrack === "off") return summary;
10721
+ const now = deps.now ?? (() => Date.now());
10722
+ const statMtimeMs = deps.statMtimeMs ?? defaultStatMtimeMs;
10723
+ const openFiles = deps.openFiles ?? defaultOpenFiles;
10724
+ const isPidAlive = deps.isPidAlive ?? defaultIsPidAlive;
10725
+ const pidStartedAt = deps.pidStartedAt ?? defaultPidStartedAt;
10726
+ const targets = (deps.targets ?? AGENT_TARGETS).filter((t) => t.sessions !== void 0);
10727
+ const scanStartMs = now();
10728
+ const watermark = opts.full ? null : readWatermark();
10729
+ const discovered = [];
10730
+ for (const target of targets) {
10731
+ const walk = target.sessions.walk({
10732
+ root: deps.roots?.[target.id],
10733
+ sinceMtimeMs: watermark ?? void 0
10734
+ });
10735
+ for await (const session of walk) {
10736
+ if (!isSafeSessionId(session.sessionId)) continue;
10737
+ discovered.push({ ...session, agent: target.id });
10738
+ }
10739
+ }
10740
+ summary.discovered = discovered.length;
10741
+ const openSet = await openFiles(discovered.map((d) => d.transcriptPath));
10742
+ const contextCache = /* @__PURE__ */ new Map();
10743
+ for (const d of discovered) {
10744
+ const link = await readContextLink(d.cwd, contextCache);
10745
+ if (autoTrack === "workspaces-only" && link === null) {
10746
+ summary.skipped += 1;
10747
+ continue;
10748
+ }
10749
+ const mtime = statMtimeMs(d.transcriptPath);
10750
+ const heldOpen = openSet.has(d.transcriptPath);
10751
+ const isLive = heldOpen || mtime !== null && now() - mtime < FRESH_MTIME_MS;
10752
+ const prev = getSessionById(d.sessionId);
10753
+ const status = isLive ? "active" : prev?.status ?? "stopped";
10754
+ const started = d.startedAt ?? (mtime !== null ? new Date(mtime).toISOString() : new Date(now()).toISOString());
10755
+ await appendSession(
10756
+ "",
10757
+ {
10758
+ sessionId: d.sessionId,
10759
+ projectSlug: link?.projectSlug ?? null,
10760
+ assignmentSlug: link?.assignmentSlug ?? null,
10761
+ agent: d.agent,
10762
+ started,
10763
+ status,
10764
+ path: d.cwd,
10765
+ description: null,
10766
+ transcriptPath: d.transcriptPath,
10767
+ pid: null,
10768
+ pidStartedAt: null,
10769
+ originalHeadSha: null
10770
+ },
10771
+ // Narrow revival rule: only LIVE-PROCESS evidence (a process holding the
10772
+ // transcript open) may flip a stopped row back to active. mtime freshness
10773
+ // alone must not — a session stopped moments ago by its SessionEnd hook
10774
+ // still has a fresh transcript for up to 5 minutes and would flap back to
10775
+ // active. `completed` always sticks (appendSession enforces).
10776
+ { reviveStopped: heldOpen }
10777
+ );
10778
+ if (!isLive) {
10779
+ const after = getSessionById(d.sessionId);
10780
+ if (after && after.status === "stopped" && !after.ended) {
10781
+ const endedAt = d.endedAt ?? (mtime !== null ? new Date(mtime).toISOString() : void 0);
10782
+ await updateSessionStatus("", d.sessionId, "stopped", endedAt);
10783
+ }
10784
+ }
10785
+ if (!prev) {
10786
+ summary.inserted += 1;
10787
+ summary.changed = true;
10788
+ } else {
10789
+ if (prev.status === "stopped" && heldOpen) {
10790
+ summary.revived += 1;
10791
+ summary.changed = true;
10792
+ }
10793
+ if (link?.projectSlug && !prev.projectSlug || link?.assignmentSlug && !prev.assignmentSlug) {
10794
+ summary.changed = true;
10795
+ }
10796
+ }
10797
+ }
10798
+ const db4 = getSessionDb();
10799
+ const activeRows = db4.prepare("SELECT session_id, pid, pid_started_at, transcript_path FROM sessions WHERE status = 'active'").all();
10800
+ const sweepCandidates = [];
10801
+ for (const row of activeRows) {
10802
+ if (row.pid !== null) {
10803
+ const alive = isPidAlive(row.pid) && (!row.pid_started_at || (pidStartedAt(row.pid) ?? row.pid_started_at) === row.pid_started_at);
10804
+ if (alive) continue;
10805
+ }
10806
+ if (row.transcript_path) {
10807
+ sweepCandidates.push({ sessionId: row.session_id, transcriptPath: row.transcript_path });
10808
+ } else if (row.pid !== null) {
10809
+ sweepCandidates.push({ sessionId: row.session_id, transcriptPath: null });
10810
+ }
10811
+ }
10812
+ const sweepOpenSet = await openFiles(
10813
+ sweepCandidates.map((c) => c.transcriptPath).filter((p) => p !== null)
10814
+ );
10815
+ for (const candidate of sweepCandidates) {
10816
+ if (candidate.transcriptPath) {
10817
+ if (sweepOpenSet.has(candidate.transcriptPath)) continue;
10818
+ const mtime = statMtimeMs(candidate.transcriptPath);
10819
+ if (mtime !== null && now() - mtime < FRESH_MTIME_MS) continue;
10820
+ const endedAt = mtime !== null ? new Date(mtime).toISOString() : void 0;
10821
+ if (await updateSessionStatus("", candidate.sessionId, "stopped", endedAt)) {
10822
+ summary.swept += 1;
10823
+ summary.changed = true;
10824
+ }
10825
+ } else if (await updateSessionStatus("", candidate.sessionId, "stopped")) {
10826
+ summary.swept += 1;
10827
+ summary.changed = true;
10828
+ }
10829
+ }
10830
+ writeWatermark(scanStartMs);
10831
+ return summary;
10832
+ }
10833
+ var execFileAsync, FRESH_MTIME_MS, LSOF_CHUNK, WATERMARK_KEY;
10834
+ var init_scanner2 = __esm({
10835
+ "src/sessions/scanner.ts"() {
10836
+ "use strict";
10837
+ init_fs();
10838
+ init_config2();
10839
+ init_session_id();
10840
+ init_registry();
10841
+ init_session_db();
10842
+ init_agent_sessions();
10843
+ execFileAsync = promisify3(execFile3);
10844
+ FRESH_MTIME_MS = 5 * 60 * 1e3;
10845
+ LSOF_CHUNK = 64;
10846
+ WATERMARK_KEY = "sessions_scan_last_ms";
10847
+ }
10848
+ });
10849
+
9630
10850
  // src/dashboard/server.ts
9631
10851
  init_paths();
9632
10852
  init_api();
@@ -9634,7 +10854,7 @@ init_assignment_resolver();
9634
10854
  init_agent_sessions();
9635
10855
  import express from "express";
9636
10856
  import { createServer } from "http";
9637
- import { resolve as resolve33 } from "path";
10857
+ import { resolve as resolve37 } from "path";
9638
10858
  import { writeFile as writeFile8, unlink as unlink8 } from "fs/promises";
9639
10859
  import { WebSocketServer, WebSocket } from "ws";
9640
10860
 
@@ -9929,11 +11149,9 @@ function createWatcher(options) {
9929
11149
  debounceKey,
9930
11150
  setTimeout(() => {
9931
11151
  pendingEvents.delete(debounceKey);
9932
- const message = {
9933
- type: "leases-updated",
9934
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
9935
- };
9936
- onMessage(message);
11152
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
11153
+ onMessage({ type: "leases-updated", timestamp });
11154
+ onMessage({ type: "agent-sessions-updated", timestamp });
9937
11155
  }, debounceMs)
9938
11156
  );
9939
11157
  };
@@ -10004,7 +11222,11 @@ var SORT_FIELDS = [
10004
11222
  "assignee",
10005
11223
  "dependencies",
10006
11224
  "created",
10007
- "updated"
11225
+ "updated",
11226
+ "started",
11227
+ "lastActivity",
11228
+ "projectName",
11229
+ "agentName"
10008
11230
  ];
10009
11231
  var SORT_DIRECTIONS = ["asc", "desc"];
10010
11232
  var DENSITIES = ["comfortable", "compact"];
@@ -10017,7 +11239,7 @@ var GROUPINGS = [
10017
11239
  "project"
10018
11240
  ];
10019
11241
  var ACTIVITIES = ["all", "stale", "fresh"];
10020
- var DATE_RANGE_FIELDS = ["created", "updated"];
11242
+ var DATE_RANGE_FIELDS = ["created", "updated", "started"];
10021
11243
  var DATE_RANGE_PRESETS = [
10022
11244
  "last_24h",
10023
11245
  "last_7d",
@@ -10266,6 +11488,7 @@ function makeSeededView(id, name, filterOverrides) {
10266
11488
  id,
10267
11489
  name,
10268
11490
  workspace: null,
11491
+ entityType: "assignment",
10269
11492
  config: {
10270
11493
  viewMode: "list",
10271
11494
  filters: { ...DEFAULT_FILTERS, ...filterOverrides },
@@ -10303,7 +11526,13 @@ function isWidgetConfig(value) {
10303
11526
  if (obj.kind === "saved-view") {
10304
11527
  return typeof obj.viewId === "string" && obj.viewId.length > 0;
10305
11528
  }
10306
- return obj.kind === "agent-sessions" || obj.kind === "inventories";
11529
+ if (obj.kind === "agent-sessions") {
11530
+ if (obj.viewId !== void 0) {
11531
+ return typeof obj.viewId === "string" && obj.viewId.length > 0;
11532
+ }
11533
+ return true;
11534
+ }
11535
+ return obj.kind === "inventories";
10307
11536
  }
10308
11537
  function isListSectionVisibility(value) {
10309
11538
  if (!value || typeof value !== "object") return false;
@@ -10340,6 +11569,9 @@ function isSavedViewConfig(value) {
10340
11569
  function isSavedView(value) {
10341
11570
  if (!value || typeof value !== "object") return false;
10342
11571
  const obj = value;
11572
+ if (obj.entityType !== void 0 && obj.entityType !== "assignment" && obj.entityType !== "session") {
11573
+ return false;
11574
+ }
10343
11575
  return typeof obj.id === "string" && obj.id.length > 0 && typeof obj.name === "string" && (obj.workspace === null || typeof obj.workspace === "string") && isSavedViewConfig(obj.config) && typeof obj.createdAt === "string" && typeof obj.updatedAt === "string";
10344
11576
  }
10345
11577
  function isDashboardSlot(value) {
@@ -10434,6 +11666,8 @@ function createSavedView(file, input, now = () => (/* @__PURE__ */ new Date()).t
10434
11666
  id: randomUUID(),
10435
11667
  name: input.name,
10436
11668
  workspace: input.workspace,
11669
+ // Only persist entityType when explicitly a session view; absent === assignment.
11670
+ ...input.entityType === "session" ? { entityType: "session" } : {},
10437
11671
  config: input.config,
10438
11672
  createdAt: ts,
10439
11673
  updatedAt: ts
@@ -10532,12 +11766,16 @@ function validateCreateBody(body) {
10532
11766
  if (!isSavedViewConfig(obj.config)) {
10533
11767
  return { ok: false, error: "config must be a valid SavedViewConfig" };
10534
11768
  }
11769
+ if (obj.entityType !== void 0 && obj.entityType !== "assignment" && obj.entityType !== "session") {
11770
+ return { ok: false, error: "entityType must be 'assignment' or 'session'" };
11771
+ }
10535
11772
  return {
10536
11773
  ok: true,
10537
11774
  value: {
10538
11775
  name: obj.name.trim(),
10539
11776
  workspace: obj.workspace,
10540
- config: obj.config
11777
+ config: obj.config,
11778
+ ...obj.entityType !== void 0 ? { entityType: obj.entityType } : {}
10541
11779
  }
10542
11780
  };
10543
11781
  }
@@ -11537,6 +12775,12 @@ tags: []
11537
12775
  `;
11538
12776
  }
11539
12777
 
12778
+ // src/templates/index.ts
12779
+ init_cursor_rules();
12780
+ init_codex_agents();
12781
+ init_opencode_config();
12782
+ init_hermes_soul();
12783
+
11540
12784
  // src/dashboard/api-write.ts
11541
12785
  init_lifecycle();
11542
12786
  init_parser();
@@ -14261,86 +15505,11 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir2) {
14261
15505
  // src/dashboard/api-agent-sessions.ts
14262
15506
  init_agent_sessions();
14263
15507
  init_fs();
15508
+ init_transcript();
14264
15509
  import { Router as Router4 } from "express";
14265
15510
  import { resolve as resolve21 } from "path";
14266
-
14267
- // src/utils/transcript.ts
14268
- import { open as open2 } from "fs/promises";
14269
- var MAX_LINES_SCANNED = 50;
14270
- async function derivePathFromTranscript(transcriptPath) {
14271
- if (!transcriptPath) return null;
14272
- let handle;
14273
- try {
14274
- handle = await open2(transcriptPath, "r");
14275
- } catch {
14276
- return null;
14277
- }
14278
- try {
14279
- const stream = handle.createReadStream({ encoding: "utf-8" });
14280
- let buffer = "";
14281
- let scanned = 0;
14282
- for await (const chunk of stream) {
14283
- buffer += chunk;
14284
- let nl = buffer.indexOf("\n");
14285
- while (nl !== -1) {
14286
- const line = buffer.slice(0, nl);
14287
- buffer = buffer.slice(nl + 1);
14288
- const cwd = extractCwd(line);
14289
- if (cwd) {
14290
- stream.destroy();
14291
- return cwd;
14292
- }
14293
- scanned++;
14294
- if (scanned >= MAX_LINES_SCANNED) {
14295
- stream.destroy();
14296
- return null;
14297
- }
14298
- nl = buffer.indexOf("\n");
14299
- }
14300
- }
14301
- if (buffer.length > 0) {
14302
- const cwd = extractCwd(buffer);
14303
- if (cwd) return cwd;
14304
- }
14305
- return null;
14306
- } finally {
14307
- await handle.close().catch(() => {
14308
- });
14309
- }
14310
- }
14311
- function extractCwd(line) {
14312
- const trimmed = line.trim();
14313
- if (trimmed.length === 0 || trimmed[0] !== "{") return null;
14314
- try {
14315
- const parsed = JSON.parse(trimmed);
14316
- if (typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
14317
- return parsed.cwd;
14318
- }
14319
- } catch {
14320
- }
14321
- return null;
14322
- }
14323
-
14324
- // src/dashboard/api-agent-sessions.ts
14325
15511
  init_config2();
14326
-
14327
- // src/utils/process-info.ts
14328
- import { execFileSync as execFileSync2 } from "child_process";
14329
- function captureProcessStartedAt(pid) {
14330
- if (!Number.isFinite(pid) || pid <= 0) return null;
14331
- try {
14332
- const out = execFileSync2("ps", ["-o", "lstart=", "-p", String(pid)], {
14333
- encoding: "utf8",
14334
- stdio: ["ignore", "pipe", "ignore"]
14335
- });
14336
- const trimmed = out.trim();
14337
- return trimmed === "" ? null : trimmed;
14338
- } catch {
14339
- return null;
14340
- }
14341
- }
14342
-
14343
- // src/dashboard/api-agent-sessions.ts
15512
+ init_process_info();
14344
15513
  init_git_worktree();
14345
15514
  init_cwd();
14346
15515
  function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
@@ -15234,13 +16403,20 @@ async function resolveSessionPlan(input, terminal) {
15234
16403
  env: process.env,
15235
16404
  agentId: agent.id,
15236
16405
  fallbackWarning,
15237
- shellFallbackWarning
16406
+ shellFallbackWarning,
16407
+ // Resume continues the SAME session id; fork mints a new one in-agent.
16408
+ session: { sessionId: (input.mode ?? "resume") === "resume" ? session.sessionId : null }
15238
16409
  };
15239
16410
  }
15240
16411
 
15241
16412
  // src/launch/execute.ts
15242
16413
  import { spawn as spawn3 } from "child_process";
15243
- import { basename as basename4 } from "path";
16414
+ import { homedir as homedir5 } from "os";
16415
+ import { basename as basename4, join as join5, resolve as resolve23 } from "path";
16416
+ init_fs();
16417
+ init_config2();
16418
+ init_session_id();
16419
+ init_process_info();
15244
16420
  var CMUX_BUNDLE_ID = "com.cmuxterm.app";
15245
16421
  var CMUX_READINESS_MAX_MS = 20 * 250;
15246
16422
  var CMUX_LAUNCH_TIMEOUT_MS = CMUX_READINESS_MAX_MS + 3e3;
@@ -15265,8 +16441,8 @@ function buildShellCommandLine(plan) {
15265
16441
  init_paths();
15266
16442
  init_fs();
15267
16443
  import { fileURLToPath } from "url";
15268
- import { dirname as dirname5, resolve as resolve23, join as join3 } from "path";
15269
- import { realpathSync, readFileSync, mkdirSync } from "fs";
16444
+ import { dirname as dirname6, resolve as resolve24, join as join6 } from "path";
16445
+ import { realpathSync, readFileSync as readFileSync2, mkdirSync as mkdirSync2 } from "fs";
15270
16446
 
15271
16447
  // src/dashboard/api-launch-preflight.ts
15272
16448
  init_assignment_resolver();
@@ -15543,32 +16719,32 @@ import { Router as Router9 } from "express";
15543
16719
  // src/utils/status-config-resolution.ts
15544
16720
  init_frontmatter();
15545
16721
  import { readFile as readFile18, writeFile as writeFile5, rm as rm2 } from "fs/promises";
15546
- import { dirname as dirname6 } from "path";
16722
+ import { dirname as dirname7 } from "path";
15547
16723
 
15548
16724
  // src/utils/assignment-walk.ts
15549
16725
  init_fs();
15550
- import { resolve as resolve24 } from "path";
15551
- import { readdir as readdir11 } from "fs/promises";
16726
+ import { resolve as resolve25 } from "path";
16727
+ import { readdir as readdir12 } from "fs/promises";
15552
16728
  async function listAssignmentsByProject(projectsDir, standaloneDir) {
15553
16729
  const result = {
15554
16730
  withAssignmentMd: [],
15555
16731
  orphanFolders: []
15556
16732
  };
15557
16733
  if (await fileExists(projectsDir)) {
15558
- const projects = await readdir11(projectsDir, { withFileTypes: true });
16734
+ const projects = await readdir12(projectsDir, { withFileTypes: true });
15559
16735
  for (const m of projects) {
15560
16736
  if (!m.isDirectory()) continue;
15561
16737
  if (m.name.startsWith(".") || m.name.startsWith("_")) continue;
15562
- const assignmentsDir2 = resolve24(projectsDir, m.name, "assignments");
16738
+ const assignmentsDir2 = resolve25(projectsDir, m.name, "assignments");
15563
16739
  if (!await fileExists(assignmentsDir2)) continue;
15564
- const entries = await readdir11(assignmentsDir2, { withFileTypes: true });
16740
+ const entries = await readdir12(assignmentsDir2, { withFileTypes: true });
15565
16741
  for (const a of entries) {
15566
16742
  if (!a.isDirectory()) continue;
15567
16743
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
15568
- const assignmentDir = resolve24(assignmentsDir2, a.name);
15569
- const assignmentMd = resolve24(assignmentDir, "assignment.md");
16744
+ const assignmentDir = resolve25(assignmentsDir2, a.name);
16745
+ const assignmentMd = resolve25(assignmentDir, "assignment.md");
15570
16746
  const entry = {
15571
- projectDir: resolve24(projectsDir, m.name),
16747
+ projectDir: resolve25(projectsDir, m.name),
15572
16748
  projectSlug: m.name,
15573
16749
  assignmentDir,
15574
16750
  assignmentSlug: a.name,
@@ -15583,12 +16759,12 @@ async function listAssignmentsByProject(projectsDir, standaloneDir) {
15583
16759
  }
15584
16760
  }
15585
16761
  if (standaloneDir !== null && await fileExists(standaloneDir)) {
15586
- const entries = await readdir11(standaloneDir, { withFileTypes: true });
16762
+ const entries = await readdir12(standaloneDir, { withFileTypes: true });
15587
16763
  for (const a of entries) {
15588
16764
  if (!a.isDirectory()) continue;
15589
16765
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
15590
- const assignmentDir = resolve24(standaloneDir, a.name);
15591
- const assignmentMd = resolve24(assignmentDir, "assignment.md");
16766
+ const assignmentDir = resolve25(standaloneDir, a.name);
16767
+ const assignmentMd = resolve25(assignmentDir, "assignment.md");
15592
16768
  const entry = {
15593
16769
  projectDir: standaloneDir,
15594
16770
  projectSlug: null,
@@ -15780,7 +16956,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
15780
16956
  } catch {
15781
16957
  continue;
15782
16958
  }
15783
- const assignmentDir = dirname6(a.path);
16959
+ const assignmentDir = dirname7(a.path);
15784
16960
  try {
15785
16961
  await rm2(assignmentDir, { recursive: true, force: true });
15786
16962
  deleted++;
@@ -16098,7 +17274,7 @@ import { Router as Router10 } from "express";
16098
17274
  init_paths();
16099
17275
  import Database2 from "better-sqlite3";
16100
17276
  import { randomUUID as randomUUID3 } from "crypto";
16101
- import { resolve as resolve25 } from "path";
17277
+ import { resolve as resolve26 } from "path";
16102
17278
  var db2 = null;
16103
17279
  var LEASE_SCHEMA_VERSION = "1";
16104
17280
  var SCHEMA_SQL2 = `
@@ -16185,7 +17361,7 @@ function isBusyError(err) {
16185
17361
  }
16186
17362
  function initLeasesDb(dbPath) {
16187
17363
  if (db2) return db2;
16188
- const finalPath = dbPath ?? resolve25(syntaurRoot(), "syntaur.db");
17364
+ const finalPath = dbPath ?? resolve26(syntaurRoot(), "syntaur.db");
16189
17365
  db2 = new Database2(finalPath);
16190
17366
  db2.pragma("journal_mode = WAL");
16191
17367
  db2.pragma("busy_timeout = 5000");
@@ -16334,7 +17510,7 @@ import { Router as Router11 } from "express";
16334
17510
  // src/db/usage-db.ts
16335
17511
  init_paths();
16336
17512
  import Database3 from "better-sqlite3";
16337
- import { resolve as resolve26 } from "path";
17513
+ import { resolve as resolve27 } from "path";
16338
17514
  var db3 = null;
16339
17515
  var USAGE_SCHEMA_VERSION = "1";
16340
17516
  var SCHEMA_SQL3 = `
@@ -16391,7 +17567,7 @@ CREATE INDEX IF NOT EXISTS idx_usage_daily_day
16391
17567
  `;
16392
17568
  function initUsageDb(dbPath) {
16393
17569
  if (db3) return db3;
16394
- const finalPath = dbPath ?? resolve26(syntaurRoot(), "syntaur.db");
17570
+ const finalPath = dbPath ?? resolve27(syntaurRoot(), "syntaur.db");
16395
17571
  db3 = new Database3(finalPath);
16396
17572
  db3.pragma("journal_mode = WAL");
16397
17573
  db3.pragma("busy_timeout = 5000");
@@ -16650,7 +17826,7 @@ init_slug();
16650
17826
  init_timestamp();
16651
17827
  init_fs();
16652
17828
  import { Router as Router12 } from "express";
16653
- import { resolve as resolve27 } from "path";
17829
+ import { resolve as resolve28 } from "path";
16654
17830
  import { readFile as readFile19 } from "fs/promises";
16655
17831
  init_playbooks();
16656
17832
  function statusForPlaybookError(code) {
@@ -16733,7 +17909,7 @@ function createPlaybooksRouter(playbooksDir2) {
16733
17909
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
16734
17910
  return;
16735
17911
  }
16736
- const filePath = resolve27(playbooksDir2, resolved.filename);
17912
+ const filePath = resolve28(playbooksDir2, resolved.filename);
16737
17913
  const content = await readFile19(filePath, "utf-8");
16738
17914
  res.json({
16739
17915
  documentType: "playbook",
@@ -16759,7 +17935,7 @@ function createPlaybooksRouter(playbooksDir2) {
16759
17935
  return;
16760
17936
  }
16761
17937
  await ensureDir(playbooksDir2);
16762
- const filePath = resolve27(playbooksDir2, `${slug}.md`);
17938
+ const filePath = resolve28(playbooksDir2, `${slug}.md`);
16763
17939
  if (await fileExists(filePath)) {
16764
17940
  res.status(409).json({ error: `Playbook "${slug}" already exists` });
16765
17941
  return;
@@ -16783,7 +17959,7 @@ function createPlaybooksRouter(playbooksDir2) {
16783
17959
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
16784
17960
  return;
16785
17961
  }
16786
- const filePath = resolve27(playbooksDir2, resolved.filename);
17962
+ const filePath = resolve28(playbooksDir2, resolved.filename);
16787
17963
  await writeFileForce(filePath, content);
16788
17964
  await rebuildPlaybookManifest(playbooksDir2);
16789
17965
  res.json({ slug: resolved.slug, path: filePath });
@@ -16831,8 +18007,8 @@ init_parser2();
16831
18007
  init_fs();
16832
18008
  init_paths();
16833
18009
  import { Router as Router14 } from "express";
16834
- import { readdir as readdir13 } from "fs/promises";
16835
- import { resolve as resolvePath, dirname as dirname8 } from "path";
18010
+ import { readdir as readdir14 } from "fs/promises";
18011
+ import { resolve as resolvePath, dirname as dirname9 } from "path";
16836
18012
  import { rename as rename6, mkdir as mkdir4 } from "fs/promises";
16837
18013
  init_slug();
16838
18014
 
@@ -16842,7 +18018,7 @@ init_parser2();
16842
18018
  // src/commands/create-assignment.ts
16843
18019
  init_slug();
16844
18020
  init_timestamp();
16845
- import { resolve as resolve28 } from "path";
18021
+ import { resolve as resolve29 } from "path";
16846
18022
  init_paths();
16847
18023
  init_fs();
16848
18024
  init_config2();
@@ -16920,14 +18096,14 @@ async function createAssignmentCommand(title, options) {
16920
18096
  if (options.oneOff) {
16921
18097
  const standaloneRoot = assignmentsDir();
16922
18098
  folderName = id;
16923
- assignmentDir = resolve28(standaloneRoot, folderName);
18099
+ assignmentDir = resolve29(standaloneRoot, folderName);
16924
18100
  projectSlug = null;
16925
18101
  await ensureDir(standaloneRoot);
16926
18102
  } else {
16927
18103
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
16928
18104
  projectSlug = options.project;
16929
- const projectDir = resolve28(baseDir, projectSlug);
16930
- const projectMdPath = resolve28(projectDir, "project.md");
18105
+ const projectDir = resolve29(baseDir, projectSlug);
18106
+ const projectMdPath = resolve29(projectDir, "project.md");
16931
18107
  if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
16932
18108
  throw new Error(
16933
18109
  `Project "${projectSlug}" not found at ${projectDir}.
@@ -16935,9 +18111,9 @@ Run 'syntaur create-project' first or use --one-off.`
16935
18111
  );
16936
18112
  }
16937
18113
  if (dependsOn.length > 0) {
16938
- const depDirBase = resolve28(projectDir, "assignments");
18114
+ const depDirBase = resolve29(projectDir, "assignments");
16939
18115
  for (const dep of dependsOn) {
16940
- const depDir = resolve28(depDirBase, dep);
18116
+ const depDir = resolve29(depDirBase, dep);
16941
18117
  if (!await fileExists(depDir)) {
16942
18118
  console.warn(
16943
18119
  `Warning: dependency "${dep}" does not exist in project "${projectSlug}" yet.`
@@ -16946,7 +18122,7 @@ Run 'syntaur create-project' first or use --one-off.`
16946
18122
  }
16947
18123
  }
16948
18124
  folderName = assignmentSlug;
16949
- assignmentDir = resolve28(projectDir, "assignments", folderName);
18125
+ assignmentDir = resolve29(projectDir, "assignments", folderName);
16950
18126
  }
16951
18127
  if (await fileExists(assignmentDir)) {
16952
18128
  throw new Error(
@@ -16958,7 +18134,7 @@ Use --slug to specify a different slug.`
16958
18134
  const companionAssignmentRef = projectSlug === null ? id : assignmentSlug;
16959
18135
  const files = [
16960
18136
  [
16961
- resolve28(assignmentDir, "assignment.md"),
18137
+ resolve29(assignmentDir, "assignment.md"),
16962
18138
  renderAssignment({
16963
18139
  id,
16964
18140
  slug: assignmentSlug,
@@ -16976,35 +18152,35 @@ Use --slug to specify a different slug.`
16976
18152
  })
16977
18153
  ],
16978
18154
  [
16979
- resolve28(assignmentDir, "scratchpad.md"),
18155
+ resolve29(assignmentDir, "scratchpad.md"),
16980
18156
  renderScratchpad({
16981
18157
  assignmentSlug: companionAssignmentRef,
16982
18158
  timestamp
16983
18159
  })
16984
18160
  ],
16985
18161
  [
16986
- resolve28(assignmentDir, "handoff.md"),
18162
+ resolve29(assignmentDir, "handoff.md"),
16987
18163
  renderHandoff({
16988
18164
  assignmentSlug: companionAssignmentRef,
16989
18165
  timestamp
16990
18166
  })
16991
18167
  ],
16992
18168
  [
16993
- resolve28(assignmentDir, "decision-record.md"),
18169
+ resolve29(assignmentDir, "decision-record.md"),
16994
18170
  renderDecisionRecord({
16995
18171
  assignmentSlug: companionAssignmentRef,
16996
18172
  timestamp
16997
18173
  })
16998
18174
  ],
16999
18175
  [
17000
- resolve28(assignmentDir, "progress.md"),
18176
+ resolve29(assignmentDir, "progress.md"),
17001
18177
  renderProgress({
17002
18178
  assignment: companionAssignmentRef,
17003
18179
  timestamp
17004
18180
  })
17005
18181
  ],
17006
18182
  [
17007
- resolve28(assignmentDir, "comments.md"),
18183
+ resolve29(assignmentDir, "comments.md"),
17008
18184
  renderComments({
17009
18185
  assignment: companionAssignmentRef,
17010
18186
  timestamp
@@ -17177,8 +18353,8 @@ init_api();
17177
18353
  import { raw } from "express";
17178
18354
 
17179
18355
  // src/todos/attachments.ts
17180
- 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";
17181
- import { resolve as resolve29, basename as basename5, dirname as dirname7, extname } from "path";
18356
+ 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";
18357
+ import { resolve as resolve30, basename as basename5, dirname as dirname8, extname } from "path";
17182
18358
 
17183
18359
  // src/utils/proof-artifact-id.ts
17184
18360
  import { randomBytes as randomBytes2 } from "crypto";
@@ -17265,16 +18441,16 @@ function sanitizeAttachmentName(name) {
17265
18441
  return n;
17266
18442
  }
17267
18443
  function attachmentsRootDir(todosDir2) {
17268
- return resolve29(todosDir2, "attachments");
18444
+ return resolve30(todosDir2, "attachments");
17269
18445
  }
17270
18446
  function attachmentDirFor(todosDir2, scopeId, todoId) {
17271
18447
  assertScope(scopeId);
17272
18448
  assertTodoId(todoId);
17273
- return resolve29(attachmentsRootDir(todosDir2), scopeId, todoId);
18449
+ return resolve30(attachmentsRootDir(todosDir2), scopeId, todoId);
17274
18450
  }
17275
18451
  async function dirExists(p) {
17276
18452
  try {
17277
- return (await stat2(p)).isDirectory();
18453
+ return (await stat3(p)).isDirectory();
17278
18454
  } catch {
17279
18455
  return false;
17280
18456
  }
@@ -17284,7 +18460,7 @@ async function writeAttachment(todosDir2, scopeId, todoId, originalName, bytes)
17284
18460
  await mkdir3(dir, { recursive: true });
17285
18461
  const id = generateArtifactId();
17286
18462
  const filename = sanitizeAttachmentName(originalName);
17287
- await writeFile6(resolve29(dir, `${id}__${filename}`), bytes);
18463
+ await writeFile6(resolve30(dir, `${id}__${filename}`), bytes);
17288
18464
  return {
17289
18465
  id,
17290
18466
  filename,
@@ -17297,7 +18473,7 @@ async function listAttachments(todosDir2, scopeId, todoId) {
17297
18473
  const dir = attachmentDirFor(todosDir2, scopeId, todoId);
17298
18474
  let names;
17299
18475
  try {
17300
- names = await readdir12(dir);
18476
+ names = await readdir13(dir);
17301
18477
  } catch {
17302
18478
  return [];
17303
18479
  }
@@ -17309,7 +18485,7 @@ async function listAttachments(todosDir2, scopeId, todoId) {
17309
18485
  if (!ATTACHMENT_ID_RE.test(id)) continue;
17310
18486
  const filename = stored.slice(sep2 + 2);
17311
18487
  try {
17312
- const st = await stat2(resolve29(dir, stored));
18488
+ const st = await stat3(resolve30(dir, stored));
17313
18489
  if (!st.isFile()) continue;
17314
18490
  out.push({ id, filename, mime: mimeForName(filename), size: st.size, createdAt: st.mtime.toISOString() });
17315
18491
  } catch {
@@ -17320,10 +18496,10 @@ async function listAttachments(todosDir2, scopeId, todoId) {
17320
18496
  }
17321
18497
  async function readScopeAttachments(todosDir2, scopeId) {
17322
18498
  assertScope(scopeId);
17323
- const scopeDir = resolve29(attachmentsRootDir(todosDir2), scopeId);
18499
+ const scopeDir = resolve30(attachmentsRootDir(todosDir2), scopeId);
17324
18500
  let todoIds;
17325
18501
  try {
17326
- todoIds = await readdir12(scopeDir);
18502
+ todoIds = await readdir13(scopeDir);
17327
18503
  } catch {
17328
18504
  return {};
17329
18505
  }
@@ -17340,7 +18516,7 @@ async function resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId) {
17340
18516
  const dir = attachmentDirFor(todosDir2, scopeId, todoId);
17341
18517
  let names;
17342
18518
  try {
17343
- names = await readdir12(dir);
18519
+ names = await readdir13(dir);
17344
18520
  } catch {
17345
18521
  return null;
17346
18522
  }
@@ -17348,7 +18524,7 @@ async function resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId) {
17348
18524
  const stored = names.find((n) => n.startsWith(prefix));
17349
18525
  if (!stored) return null;
17350
18526
  const filename = stored.slice(prefix.length);
17351
- return { path: resolve29(dir, stored), filename, mime: mimeForName(filename) };
18527
+ return { path: resolve30(dir, stored), filename, mime: mimeForName(filename) };
17352
18528
  }
17353
18529
  async function deleteAttachment(todosDir2, scopeId, todoId, attachmentId) {
17354
18530
  const resolved = await resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId);
@@ -17368,7 +18544,7 @@ async function moveAttachments(srcTodosDir, srcScopeId, dstTodosDir, dstScopeId,
17368
18544
  const src = attachmentDirFor(srcTodosDir, srcScopeId, todoId);
17369
18545
  if (!await dirExists(src)) return;
17370
18546
  const dst = attachmentDirFor(dstTodosDir, dstScopeId, todoId);
17371
- await mkdir3(dirname7(dst), { recursive: true });
18547
+ await mkdir3(dirname8(dst), { recursive: true });
17372
18548
  try {
17373
18549
  await rename5(src, dst);
17374
18550
  } catch (err) {
@@ -17644,7 +18820,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
17644
18820
  router.get("/", async (_req, res) => {
17645
18821
  try {
17646
18822
  await ensureDir(todosDir2);
17647
- const files = await readdir13(todosDir2).catch(() => []);
18823
+ const files = await readdir14(todosDir2).catch(() => []);
17648
18824
  const workspaces = [];
17649
18825
  for (const file of files) {
17650
18826
  if (typeof file !== "string") continue;
@@ -17760,8 +18936,8 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
17760
18936
  router.post("/:workspace/archive", async (req, res) => {
17761
18937
  try {
17762
18938
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
17763
- const { resolve: resolve34 } = await import("path");
17764
- const { readFile: readFile23 } = await import("fs/promises");
18939
+ const { resolve: resolve38 } = await import("path");
18940
+ const { readFile: readFile25 } = await import("fs/promises");
17765
18941
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
17766
18942
  const workspace = getWorkspaceParam(req.params.workspace);
17767
18943
  const outcome = await wsLock(workspace, async () => {
@@ -17777,10 +18953,10 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
17777
18953
  (e) => e.itemIds.every((id) => completedIds.has(id))
17778
18954
  );
17779
18955
  const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
17780
- await ensureDir(resolve34(todosDir2, "archive"));
18956
+ await ensureDir(resolve38(todosDir2, "archive"));
17781
18957
  let archContent = "";
17782
18958
  if (await fileExists(archFile)) {
17783
- archContent = await readFile23(archFile, "utf-8");
18959
+ archContent = await readFile25(archFile, "utf-8");
17784
18960
  archContent = archContent.trimEnd() + "\n\n";
17785
18961
  } else {
17786
18962
  archContent = `---
@@ -18069,7 +19245,7 @@ workspace: ${workspace}
18069
19245
  const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
18070
19246
  const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
18071
19247
  const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
18072
- const { readFile: readFile23 } = await import("fs/promises");
19248
+ const { readFile: readFile25 } = await import("fs/promises");
18073
19249
  const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
18074
19250
  const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
18075
19251
  let assignmentRef;
@@ -18090,7 +19266,7 @@ workspace: ${workspace}
18090
19266
  }
18091
19267
  const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
18092
19268
  if (!await fileExists2(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
18093
- let content = await readFile23(assignmentMdPath, "utf-8");
19269
+ let content = await readFile25(assignmentMdPath, "utf-8");
18094
19270
  content = appendTodosToAssignmentBody2(
18095
19271
  content,
18096
19272
  items.map((it) => ({
@@ -18207,7 +19383,7 @@ workspace: ${workspace}
18207
19383
  return { status: 409, error: "attachments already exist in target" };
18208
19384
  }
18209
19385
  if (item.planDir && newPlanDir) {
18210
- await mkdir4(dirname8(newPlanDir), { recursive: true });
19386
+ await mkdir4(dirname9(newPlanDir), { recursive: true });
18211
19387
  await rename6(item.planDir, newPlanDir);
18212
19388
  item.planDir = newPlanDir;
18213
19389
  }
@@ -18286,7 +19462,7 @@ init_paths();
18286
19462
  init_slug();
18287
19463
  import { Router as Router15 } from "express";
18288
19464
  import { mkdir as mkdir5, readFile as readFile20, rename as rename7 } from "fs/promises";
18289
- import { resolve as resolve30, dirname as dirname9 } from "path";
19465
+ import { resolve as resolve31, dirname as dirname10 } from "path";
18290
19466
  init_api();
18291
19467
  var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
18292
19468
  function touchItem4(item) {
@@ -18302,7 +19478,7 @@ function params(req) {
18302
19478
  return req.params;
18303
19479
  }
18304
19480
  async function projectExists(projectsDir, slug) {
18305
- return fileExists(resolve30(projectsDir, slug, "project.md"));
19481
+ return fileExists(resolve31(projectsDir, slug, "project.md"));
18306
19482
  }
18307
19483
  async function ensureProjectTodosDir(projectsDir, slug) {
18308
19484
  const todosDir2 = projectTodosDir(projectsDir, slug);
@@ -18319,7 +19495,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
18319
19495
  throw err;
18320
19496
  }
18321
19497
  try {
18322
- await mkdir5(resolve30(todosDir2, "archive"), { recursive: false });
19498
+ await mkdir5(resolve31(todosDir2, "archive"), { recursive: false });
18323
19499
  } catch (err) {
18324
19500
  const code = err.code;
18325
19501
  if (code === "EEXIST") return;
@@ -18982,15 +20158,15 @@ workspace: ${slug}
18982
20158
  if (tg.includes("/")) {
18983
20159
  const parts = tg.split("/");
18984
20160
  if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
18985
- assignmentDir = resolve30(projectsDir, parts[0], "assignments", parts[1]);
20161
+ assignmentDir = resolve31(projectsDir, parts[0], "assignments", parts[1]);
18986
20162
  assignmentRef = `${parts[0]}/${parts[1]}`;
18987
20163
  } 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)) {
18988
- assignmentDir = resolve30(assignmentsDirFn(), tg);
20164
+ assignmentDir = resolve31(assignmentsDirFn(), tg);
18989
20165
  assignmentRef = tg;
18990
20166
  } else {
18991
20167
  return { error: `Invalid target.assignment "${tg}"` };
18992
20168
  }
18993
- const assignmentMdPath = resolve30(assignmentDir, "assignment.md");
20169
+ const assignmentMdPath = resolve31(assignmentDir, "assignment.md");
18994
20170
  if (!await fileExists(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
18995
20171
  let content = await readFile20(assignmentMdPath, "utf-8");
18996
20172
  content = appendTodosToAssignmentBody2(
@@ -19131,7 +20307,7 @@ workspace: ${slug}
19131
20307
  return { status: 409, error: "attachments already exist in target" };
19132
20308
  }
19133
20309
  if (item.planDir && newPlanDir) {
19134
- await mkdir5(dirname9(newPlanDir), { recursive: true });
20310
+ await mkdir5(dirname10(newPlanDir), { recursive: true });
19135
20311
  await rename7(item.planDir, newPlanDir);
19136
20312
  item.planDir = newPlanDir;
19137
20313
  }
@@ -19195,7 +20371,7 @@ workspace: ${slug}
19195
20371
 
19196
20372
  // src/dashboard/api-bundles.ts
19197
20373
  import { Router as Router16 } from "express";
19198
- import { readdir as readdir14 } from "fs/promises";
20374
+ import { readdir as readdir15 } from "fs/promises";
19199
20375
 
19200
20376
  // src/todos/bundle-parser.ts
19201
20377
  init_parser();
@@ -19319,7 +20495,7 @@ function createBundlesRouter(todosDir2, broadcast) {
19319
20495
  try {
19320
20496
  await ensureDir(todosDir2);
19321
20497
  const bundles = await readBundles(todosDir2);
19322
- const workspaceFiles = await readdir14(todosDir2).catch(() => []);
20498
+ const workspaceFiles = await readdir15(todosDir2).catch(() => []);
19323
20499
  const itemsByKey = /* @__PURE__ */ new Map();
19324
20500
  for (const f of workspaceFiles) {
19325
20501
  if (typeof f !== "string") continue;
@@ -19372,7 +20548,7 @@ init_fs();
19372
20548
  init_paths();
19373
20549
  init_slug();
19374
20550
  import { Router as Router17 } from "express";
19375
- import { resolve as resolve31 } from "path";
20551
+ import { resolve as resolve32 } from "path";
19376
20552
  init_parser2();
19377
20553
  function deriveStatus2(bundle, items) {
19378
20554
  const members = bundle.todoIds.map((id) => items.find((i) => i.id === id)).filter((i) => i !== void 0);
@@ -19414,7 +20590,7 @@ function createProjectBundlesRouter(projectsDir, broadcast) {
19414
20590
  router.get("/", async (req, res) => {
19415
20591
  try {
19416
20592
  const slug = getProjectIdParam2(req.params.projectId);
19417
- const projectMd = resolve31(projectsDir, slug, "project.md");
20593
+ const projectMd = resolve32(projectsDir, slug, "project.md");
19418
20594
  if (!await fileExists(projectMd)) {
19419
20595
  notFound2(res, slug);
19420
20596
  return;
@@ -19443,8 +20619,8 @@ init_fs();
19443
20619
  init_config2();
19444
20620
  import { execFile as execFile2 } from "child_process";
19445
20621
  import { promisify as promisify2 } from "util";
19446
- 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";
19447
- import { resolve as resolve32, join as join4 } from "path";
20622
+ 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";
20623
+ import { resolve as resolve33, join as join7 } from "path";
19448
20624
  import { tmpdir } from "os";
19449
20625
  var exec2 = promisify2(execFile2);
19450
20626
  var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
@@ -19484,7 +20660,7 @@ async function resolveCategoryPath(category) {
19484
20660
  case "servers":
19485
20661
  return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
19486
20662
  case "config":
19487
- return { sourcePath: resolve32(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
20663
+ return { sourcePath: resolve33(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
19488
20664
  }
19489
20665
  }
19490
20666
  async function checkGitInstalled() {
@@ -19495,10 +20671,10 @@ async function checkGitInstalled() {
19495
20671
  }
19496
20672
  }
19497
20673
  async function acquireLock2() {
19498
- const lockPath = resolve32(syntaurRoot(), LOCK_FILE_NAME);
20674
+ const lockPath = resolve33(syntaurRoot(), LOCK_FILE_NAME);
19499
20675
  await ensureDir(syntaurRoot());
19500
20676
  try {
19501
- const handle = await open3(lockPath, "wx");
20677
+ const handle = await open4(lockPath, "wx");
19502
20678
  await handle.write(String(process.pid));
19503
20679
  await handle.close();
19504
20680
  return lockPath;
@@ -19537,12 +20713,12 @@ async function cloneOrInit(repoUrl, destDir) {
19537
20713
  }
19538
20714
  async function copyRecursive(src, dest) {
19539
20715
  if (!await fileExists(src)) return;
19540
- const s = await stat3(src);
20716
+ const s = await stat4(src);
19541
20717
  if (s.isDirectory()) {
19542
20718
  await ensureDir(dest);
19543
20719
  await cp2(src, dest, { recursive: true, force: true });
19544
20720
  } else {
19545
- await ensureDir(resolve32(dest, ".."));
20721
+ await ensureDir(resolve33(dest, ".."));
19546
20722
  await cp2(src, dest, { force: true });
19547
20723
  }
19548
20724
  }
@@ -19574,11 +20750,11 @@ async function backupToGithub(overrides) {
19574
20750
  let tmpDir = null;
19575
20751
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
19576
20752
  try {
19577
- tmpDir = await mkdtemp(join4(tmpdir(), "syntaur-backup-"));
20753
+ tmpDir = await mkdtemp(join7(tmpdir(), "syntaur-backup-"));
19578
20754
  await cloneOrInit(repo, tmpDir);
19579
20755
  for (const category of categories) {
19580
20756
  const { sourcePath, repoPath, isFile } = await resolveCategoryPath(category);
19581
- const destPath = join4(tmpDir, repoPath);
20757
+ const destPath = join7(tmpDir, repoPath);
19582
20758
  if (isFile) {
19583
20759
  await rm4(destPath, { force: true });
19584
20760
  } else {
@@ -19590,7 +20766,7 @@ async function backupToGithub(overrides) {
19590
20766
  }
19591
20767
  if (category === "config") {
19592
20768
  const sanitized = await readSanitizedConfig(sourcePath);
19593
- await ensureDir(resolve32(destPath, ".."));
20769
+ await ensureDir(resolve33(destPath, ".."));
19594
20770
  await writeFile7(destPath, sanitized, "utf-8");
19595
20771
  } else {
19596
20772
  await copyRecursive(sourcePath, destPath);
@@ -19644,7 +20820,7 @@ async function backupToGithub(overrides) {
19644
20820
  }
19645
20821
  async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
19646
20822
  if (isFile) {
19647
- await ensureDir(resolve32(localPath, ".."));
20823
+ await ensureDir(resolve33(localPath, ".."));
19648
20824
  await cp2(repoSrcPath, localPath, { force: true });
19649
20825
  return;
19650
20826
  }
@@ -19706,7 +20882,7 @@ async function restoreFromGithub(overrides) {
19706
20882
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
19707
20883
  try {
19708
20884
  await updateBackupConfig({ lastRestore: timestamp });
19709
- tmpDir = await mkdtemp(join4(tmpdir(), "syntaur-restore-"));
20885
+ tmpDir = await mkdtemp(join7(tmpdir(), "syntaur-restore-"));
19710
20886
  await cloneOrInit(repo, tmpDir);
19711
20887
  for (const category of categories) {
19712
20888
  if (category === "config") {
@@ -19715,7 +20891,7 @@ async function restoreFromGithub(overrides) {
19715
20891
  }
19716
20892
  try {
19717
20893
  const { sourcePath: localPath, repoPath, isFile } = await resolveCategoryPath(category);
19718
- const repoSrcPath = join4(tmpDir, repoPath);
20894
+ const repoSrcPath = join7(tmpDir, repoPath);
19719
20895
  if (!await fileExists(repoSrcPath)) {
19720
20896
  console.warn(`Category "${category}" not found in backup repo, skipping.`);
19721
20897
  continue;
@@ -19745,7 +20921,7 @@ async function restoreFromGithub(overrides) {
19745
20921
  }
19746
20922
  async function getBackupStatus() {
19747
20923
  const config = await readConfig();
19748
- const lockPath = resolve32(syntaurRoot(), LOCK_FILE_NAME);
20924
+ const lockPath = resolve33(syntaurRoot(), LOCK_FILE_NAME);
19749
20925
  const locked = await fileExists(lockPath);
19750
20926
  return {
19751
20927
  repo: config.backup?.repo ?? null,
@@ -19906,7 +21082,7 @@ async function stopAutodiscovery() {
19906
21082
  function runReconcile() {
19907
21083
  if (activeReconcile || !savedOptions) return;
19908
21084
  const opts = savedOptions;
19909
- activeReconcile = reconcile(opts.serversDir, opts.projectsDir, opts.excludePids, opts.assignmentsDir).catch((err) => {
21085
+ activeReconcile = reconcile(opts.serversDir, opts.projectsDir, opts.excludePids, opts.assignmentsDir, opts.onAgentSessionsChanged).catch((err) => {
19910
21086
  console.error("[autodiscovery] reconcile failed:", err);
19911
21087
  }).finally(() => {
19912
21088
  activeReconcile = null;
@@ -20027,7 +21203,7 @@ async function isProcessAlive(pid) {
20027
21203
  return false;
20028
21204
  }
20029
21205
  }
20030
- async function reconcile(serversDir2, projectsDir, excludePids, assignmentsDir2) {
21206
+ async function reconcile(serversDir2, projectsDir, excludePids, assignmentsDir2, onAgentSessionsChanged) {
20031
21207
  const names = await listSessionFiles(serversDir2);
20032
21208
  const existingFiles = /* @__PURE__ */ new Map();
20033
21209
  for (const name of names) {
@@ -20044,6 +21220,16 @@ async function reconcile(serversDir2, projectsDir, excludePids, assignmentsDir2)
20044
21220
  if (tmuxChanged || processChanged || cleanupChanged) {
20045
21221
  clearScanCache();
20046
21222
  }
21223
+ const { isSessionDbInitialized: isSessionDbInitialized2 } = await Promise.resolve().then(() => (init_session_db(), session_db_exports));
21224
+ if (isSessionDbInitialized2()) {
21225
+ try {
21226
+ const { scanSessions: scanSessions2 } = await Promise.resolve().then(() => (init_scanner2(), scanner_exports2));
21227
+ const summary = await scanSessions2({});
21228
+ if (summary.changed) onAgentSessionsChanged?.();
21229
+ } catch (err) {
21230
+ console.error("[autodiscovery] session scan failed:", err);
21231
+ }
21232
+ }
20047
21233
  }
20048
21234
 
20049
21235
  // src/dashboard/server.ts
@@ -20093,7 +21279,7 @@ function createDashboardServer(options) {
20093
21279
  (async () => {
20094
21280
  try {
20095
21281
  const configResult = await migrateLegacyConfig(
20096
- resolve33(syntaurRoot(), "config.md")
21282
+ resolve37(syntaurRoot(), "config.md")
20097
21283
  );
20098
21284
  const projectResult = await migrateLegacyProjectFiles(projectsDir);
20099
21285
  const summary = summarizeMigration(projectResult, configResult);
@@ -20611,14 +21797,14 @@ function createDashboardServer(options) {
20611
21797
  app.use("/api/backup", createBackupRouter());
20612
21798
  if (serveStaticUi && dashboardDistPath) {
20613
21799
  const sendOpts = { dotfiles: "allow" };
20614
- app.use("/assets", express.static(resolve33(dashboardDistPath, "assets"), sendOpts));
21800
+ app.use("/assets", express.static(resolve37(dashboardDistPath, "assets"), sendOpts));
20615
21801
  app.use(express.static(dashboardDistPath, { ...sendOpts, index: false, fallthrough: true }));
20616
21802
  app.get("{*path}", async (req, res) => {
20617
21803
  if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
20618
21804
  res.status(404).json({ error: "Not Found" });
20619
21805
  return;
20620
21806
  }
20621
- const indexPath = resolve33(dashboardDistPath, "index.html");
21807
+ const indexPath = resolve37(dashboardDistPath, "index.html");
20622
21808
  if (!await fileExists(indexPath)) {
20623
21809
  res.status(503).send(
20624
21810
  'Dashboard not built. Run "npm run build:dashboard" first.'
@@ -20652,8 +21838,8 @@ function createDashboardServer(options) {
20652
21838
  if (!await migrationGate()) return;
20653
21839
  try {
20654
21840
  const context = await resolveDeriveContext2();
20655
- const projectDir = projectSlug ? resolve33(projectsDir, projectSlug) : null;
20656
- const path = projectDir ? resolve33(projectDir, "assignments", assignmentSlug, "assignment.md") : resolve33(assignmentsDir2, assignmentSlug, "assignment.md");
21841
+ const projectDir = projectSlug ? resolve37(projectsDir, projectSlug) : null;
21842
+ const path = projectDir ? resolve37(projectDir, "assignments", assignmentSlug, "assignment.md") : resolve37(assignmentsDir2, assignmentSlug, "assignment.md");
20657
21843
  if (!await fileExists(path)) return;
20658
21844
  const result = await recomputeAndWrite2(path, {
20659
21845
  cause: "derive",
@@ -20689,8 +21875,8 @@ function createDashboardServer(options) {
20689
21875
  serversDir: serversDir2,
20690
21876
  playbooksDir: playbooksDir2,
20691
21877
  todosDir: todosDir2,
20692
- dbPath: resolve33(syntaurRoot(), "syntaur.db"),
20693
- configPath: resolve33(syntaurRoot(), "config.md"),
21878
+ dbPath: resolve37(syntaurRoot(), "syntaur.db"),
21879
+ configPath: resolve37(syntaurRoot(), "config.md"),
20694
21880
  onMessage: broadcast,
20695
21881
  onAssignmentChanged: (projectSlug, assignmentSlug) => {
20696
21882
  void recomputeOne(projectSlug, assignmentSlug);
@@ -20701,7 +21887,16 @@ function createDashboardServer(options) {
20701
21887
  }
20702
21888
  });
20703
21889
  void sweepAll("boot-reconcile");
20704
- startAutodiscovery({ serversDir: serversDir2, projectsDir, assignmentsDir: assignmentsDir2, excludePids: /* @__PURE__ */ new Set([process.pid]) });
21890
+ startAutodiscovery({
21891
+ serversDir: serversDir2,
21892
+ projectsDir,
21893
+ assignmentsDir: assignmentsDir2,
21894
+ excludePids: /* @__PURE__ */ new Set([process.pid]),
21895
+ // Same WS frame the REST mutations emit, so the UI refreshes when the
21896
+ // session scan inserts/revives/sweeps rows. Autodiscovery's immediate
21897
+ // first run covers "scan at dashboard start".
21898
+ onAgentSessionsChanged: () => broadcast({ type: "agent-sessions-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() })
21899
+ });
20705
21900
  return new Promise((resolvePromise, reject) => {
20706
21901
  server.on("error", (err) => {
20707
21902
  if (err.code === "EADDRINUSE") {
@@ -20713,7 +21908,7 @@ function createDashboardServer(options) {
20713
21908
  }
20714
21909
  });
20715
21910
  server.listen(port, () => {
20716
- const portFile = resolve33(syntaurRoot(), "dashboard-port");
21911
+ const portFile = resolve37(syntaurRoot(), "dashboard-port");
20717
21912
  writeFile8(portFile, String(port), "utf-8").catch(() => {
20718
21913
  });
20719
21914
  resolvePromise();
@@ -20732,7 +21927,7 @@ function createDashboardServer(options) {
20732
21927
  client.terminate();
20733
21928
  }
20734
21929
  clients.clear();
20735
- const portFile = resolve33(syntaurRoot(), "dashboard-port");
21930
+ const portFile = resolve37(syntaurRoot(), "dashboard-port");
20736
21931
  await unlink8(portFile).catch(() => {
20737
21932
  });
20738
21933
  server.closeAllConnections?.();