syntaur 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -6039,7 +6064,7 @@ function rowToSession(row) {
6039
6064
  originalHeadSha: row.original_head_sha ?? null
6040
6065
  };
6041
6066
  }
6042
- async function appendSession(_projectDir, session) {
6067
+ async function appendSession(_projectDir, session, opts) {
6043
6068
  const db4 = getSessionDb();
6044
6069
  db4.prepare(`
6045
6070
  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 +6073,11 @@ async function appendSession(_projectDir, session) {
6048
6073
  project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),
6049
6074
  assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),
6050
6075
  agent = excluded.agent,
6051
- status = CASE WHEN status IN ('completed','stopped') THEN status ELSE excluded.status END,
6076
+ status = CASE
6077
+ WHEN status = 'completed' THEN status
6078
+ WHEN status = 'stopped' AND NOT (? AND excluded.status = 'active') THEN status
6079
+ ELSE excluded.status
6080
+ END,
6052
6081
  path = COALESCE(NULLIF(excluded.path, ''), path),
6053
6082
  description = COALESCE(NULLIF(excluded.description, ''), description),
6054
6083
  transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),
@@ -6068,15 +6097,16 @@ async function appendSession(_projectDir, session) {
6068
6097
  session.transcriptPath ?? null,
6069
6098
  session.pid ?? null,
6070
6099
  session.pidStartedAt ?? null,
6071
- session.originalHeadSha ?? null
6100
+ session.originalHeadSha ?? null,
6101
+ opts?.reviveStopped ? 1 : 0
6072
6102
  );
6073
6103
  }
6074
- async function updateSessionStatus(_projectDir, sessionId, status) {
6104
+ async function updateSessionStatus(_projectDir, sessionId, status, endedAt) {
6075
6105
  const db4 = getSessionDb();
6076
6106
  const isTerminal = status === "completed" || status === "stopped";
6077
6107
  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(
6108
+ "UPDATE sessions SET status = ?, ended = COALESCE(?, datetime('now')), updated_at = datetime('now') WHERE session_id = ?"
6109
+ ).run(status, endedAt ?? null, sessionId) : db4.prepare(
6080
6110
  "UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE session_id = ?"
6081
6111
  ).run(status, sessionId);
6082
6112
  return result.changes > 0;
@@ -6385,8 +6415,8 @@ function scanKey(serversDir2, projectsDir, assignmentsDir2) {
6385
6415
  return `${serversDir2}\0${projectsDir}\0${assignmentsDir2 ?? ""}`;
6386
6416
  }
6387
6417
  function delay(ms) {
6388
- return new Promise((resolve34) => {
6389
- const timer2 = setTimeout(resolve34, ms);
6418
+ return new Promise((resolve38) => {
6419
+ const timer2 = setTimeout(resolve38, ms);
6390
6420
  if (typeof timer2.unref === "function") {
6391
6421
  timer2.unref();
6392
6422
  }
@@ -9324,6 +9354,431 @@ var init_api = __esm({
9324
9354
  }
9325
9355
  });
9326
9356
 
9357
+ // src/templates/cursor-rules.ts
9358
+ function renderCursorProtocol() {
9359
+ return `---
9360
+ description: Syntaur protocol rules for multi-agent coordination
9361
+ globs:
9362
+ alwaysApply: true
9363
+ ---
9364
+
9365
+ # Syntaur Protocol
9366
+
9367
+ You are working within the Syntaur protocol for multi-agent project coordination.
9368
+
9369
+ ## Directory Structure
9370
+
9371
+ \`\`\`
9372
+ ~/.syntaur/
9373
+ config.md
9374
+ projects/
9375
+ <project-slug>/
9376
+ manifest.md # Derived: root navigation (read-only)
9377
+ project.md # Human-authored: project overview (read-only)
9378
+ _index-assignments.md # Derived (read-only)
9379
+ _index-plans.md # Derived (read-only)
9380
+ _index-decisions.md # Derived (read-only)
9381
+ _status.md # Derived (read-only)
9382
+ assignments/
9383
+ <assignment-slug>/
9384
+ assignment.md # Agent-writable: source of truth for state (includes ## Todos)
9385
+ plan*.md # Agent-writable: versioned implementation plans (optional, one per ## Todos entry)
9386
+ progress.md # Agent-writable, append-only: timestamped progress log
9387
+ comments.md # CLI-mediated: threaded questions/notes/feedback (via \`syntaur comment\`)
9388
+ scratchpad.md # Agent-writable: working notes
9389
+ handoff.md # Agent-writable: append-only cross-ticket outbound at completion
9390
+ decision-record.md # Agent-writable: append-only decision log
9391
+ sessions/
9392
+ <session-id>/
9393
+ summary.md # Agent-writable: per-session continuity (single doc, overwritten)
9394
+ resources/
9395
+ _index.md # Derived (read-only)
9396
+ <resource-slug>.md # Shared-writable
9397
+ memories/
9398
+ _index.md # Derived (read-only)
9399
+ <memory-slug>.md # Shared-writable
9400
+ assignments/
9401
+ <assignment-id>/ # Standalone assignments \u2014 folder = UUID, \`project: null\`, slug display-only
9402
+ assignment.md
9403
+ plan*.md
9404
+ progress.md
9405
+ comments.md
9406
+ scratchpad.md
9407
+ handoff.md
9408
+ decision-record.md
9409
+ sessions/<session-id>/summary.md # Per-session continuity (same as project-nested)
9410
+ \`\`\`
9411
+
9412
+ ## Write Boundary Rules (CRITICAL)
9413
+
9414
+ ### Files you may WRITE:
9415
+ 1. **Your assignment folder** -- only the assignment you are currently working on:
9416
+ - \`assignment.md\`, \`plan*.md\` (0 or more versioned plan files), \`progress.md\`, \`scratchpad.md\`, \`handoff.md\` (cross-ticket outbound at completion), \`decision-record.md\`
9417
+ - \`sessions/<session-id>/summary.md\` -- per-session continuity (single doc per session id, overwritten on save). Distinct from \`handoff.md\`.
9418
+ - Path (project-nested): \`~/.syntaur/projects/<project>/assignments/<your-assignment>/\`
9419
+ - Path (standalone): \`~/.syntaur/assignments/<your-assignment-uuid>/\`
9420
+ 2. **Shared resources and memories** at the project level:
9421
+ - \`~/.syntaur/projects/<project>/resources/<slug>.md\`
9422
+ - \`~/.syntaur/projects/<project>/memories/<slug>.md\`
9423
+ 3. **Your workspace** -- source code files in the current working directory (the directory where this adapter file lives). If your assignment's frontmatter specifies a \`workspace\` field, read it at runtime to determine the exact boundary.
9424
+
9425
+ > **Note:** The \`setup-adapter\` command does not parse assignment frontmatter for workspace paths. Workspace boundaries are resolved by the agent at runtime by reading \`assignment.md\` frontmatter. If no \`workspace\` field is set, treat the current working directory as your workspace.
9426
+
9427
+ ### Files written only via CLI (never edit directly):
9428
+ - \`comments.md\` (any assignment) -- use \`syntaur comment <slug-or-uuid> "body" [--type question|note|feedback] [--reply-to <id>]\`
9429
+ - Another assignment's \`## Todos\` section -- use \`syntaur request <source> <target> "text"\` to request cross-assignment work
9430
+
9431
+ ### Files you must NEVER write:
9432
+ 1. \`project.md\` -- human-authored, read-only
9433
+ 2. \`manifest.md\` -- derived, rebuilt by tooling
9434
+ 3. Any file prefixed with \`_\` -- derived
9435
+ 4. Other agents' assignment folders (except via the CLI-mediated channels above)
9436
+ 5. Any files outside your workspace boundary
9437
+
9438
+ ## Assignment Lifecycle
9439
+
9440
+ | Status | Meaning |
9441
+ |--------|---------|
9442
+ | \`pending\` | Not yet started |
9443
+ | \`in_progress\` | Actively being worked on |
9444
+ | \`blocked\` | Manually blocked (requires blockedReason) |
9445
+ | \`review\` | Work complete, awaiting review |
9446
+ | \`completed\` | Done |
9447
+ | \`failed\` | Could not be completed |
9448
+
9449
+ ## Valid State Transitions
9450
+
9451
+ | From | Command | To |
9452
+ |------|---------|-----|
9453
+ | pending | start | in_progress |
9454
+ | pending | block | blocked |
9455
+ | in_progress | block | blocked |
9456
+ | in_progress | review | review |
9457
+ | in_progress | complete | completed |
9458
+ | in_progress | fail | failed |
9459
+ | blocked | unblock | in_progress |
9460
+ | review | start | in_progress |
9461
+ | review | complete | completed |
9462
+ | review | fail | failed |
9463
+
9464
+ ## Lifecycle Commands
9465
+
9466
+ Use the \`syntaur\` CLI for state transitions and coordination:
9467
+ - \`syntaur assign <slug> --agent <name> --project <project>\` -- set assignee
9468
+ - \`syntaur start <slug> --project <project>\` -- pending -> in_progress
9469
+ - \`syntaur review <slug> --project <project>\` -- in_progress -> review
9470
+ - \`syntaur complete <slug> --project <project>\` -- in_progress/review -> completed
9471
+ - \`syntaur block <slug> --project <project> --reason <text>\` -- block an assignment
9472
+ - \`syntaur unblock <slug> --project <project>\` -- unblock
9473
+ - \`syntaur fail <slug> --project <project>\` -- mark as failed
9474
+ - \`syntaur create-assignment "Title" [--type <type>] [--project <slug> | --one-off]\` -- create project-nested or standalone assignment
9475
+ - \`syntaur comment <slug-or-uuid> "body" --type question|note|feedback [--reply-to <id>]\` -- append to \`comments.md\` (questions support resolve toggle via dashboard)
9476
+ - \`syntaur request <source> <target> "text"\` -- append a todo to another assignment's \`## Todos\` annotated \`(from: <source>)\`
9477
+
9478
+ ## Playbooks
9479
+
9480
+ Playbooks are user-defined behavioral rules stored in \`~/.syntaur/playbooks/\`. Read the playbook manifest before starting work:
9481
+
9482
+ \`\`\`bash
9483
+ cat ~/.syntaur/playbooks/manifest.md
9484
+ \`\`\`
9485
+
9486
+ Follow the rules in each playbook. They take precedence over default conventions when they conflict.
9487
+
9488
+ ## Conventions
9489
+
9490
+ - Assignment frontmatter is the single source of truth for state. \`project\` is the containing project slug (\`null\` for standalone); \`type\` is a classification validated against \`config.md\` \`types.definitions\` when present.
9491
+ - Slugs are lowercase, hyphen-separated. Standalone assignment folders are named by UUID; \`slug\` is display-only in that case.
9492
+ - Always read \`project.md\` at the project level (when project-nested) before starting work.
9493
+ - Append timestamped entries to \`progress.md\` (never to \`assignment.md\`).
9494
+ - Record questions, notes, and feedback via \`syntaur comment\`. Never edit \`comments.md\` directly.
9495
+ - To route work to another assignment, use \`syntaur request\`.
9496
+ - Commit frequently with messages referencing the assignment slug.
9497
+ `;
9498
+ }
9499
+ function renderCursorAssignment(params2) {
9500
+ return `---
9501
+ description: Syntaur assignment context for ${params2.projectSlug}/${params2.assignmentSlug}
9502
+ globs:
9503
+ alwaysApply: true
9504
+ ---
9505
+
9506
+ # Current Assignment Context
9507
+
9508
+ - **Project:** ${params2.projectSlug}
9509
+ - **Assignment:** ${params2.assignmentSlug}
9510
+ - **Project directory:** ${params2.projectDir}
9511
+ - **Assignment directory:** ${params2.assignmentDir}
9512
+
9513
+ ## Reading Order
9514
+
9515
+ Before starting work, read these files in order:
9516
+ 1. \`${params2.projectDir}/project.md\` -- project overview and goals (project-nested assignments only)
9517
+ 2. \`${params2.assignmentDir}/assignment.md\` -- your assignment details, acceptance criteria, todos, current status. Frontmatter includes \`project: <slug> | null\` (null for standalone) and \`type: <classification> | null\`.
9518
+ 3. any \`${params2.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
9519
+ 4. \`${params2.assignmentDir}/progress.md\` -- reverse-chron progress log (if present)
9520
+ 5. \`${params2.assignmentDir}/comments.md\` -- threaded questions/notes/feedback (if present)
9521
+ 6. \`${params2.assignmentDir}/handoff.md\` -- cross-ticket outbound history (entries from prior agents/humans handing this assignment off)
9522
+ 7. The latest \`${params2.assignmentDir}/sessions/<sid>/summary.md\` if present -- previous-session continuity (selected by \`summary.md\` file mtime; read it for "what was done / what's next" before resuming work in flight)
9523
+
9524
+ ## Your Writable Files
9525
+
9526
+ You may write directly to these files inside your assignment folder:
9527
+ - \`${params2.assignmentDir}/assignment.md\`
9528
+ - \`${params2.assignmentDir}/plan*.md\` (0 or more versioned plan files, e.g., \`plan.md\`, \`plan-v2.md\`)
9529
+ - \`${params2.assignmentDir}/progress.md\` (append timestamped entries, newest first)
9530
+ - \`${params2.assignmentDir}/scratchpad.md\`
9531
+ - \`${params2.assignmentDir}/handoff.md\`
9532
+ - \`${params2.assignmentDir}/decision-record.md\`
9533
+ - \`${params2.assignmentDir}/sessions/<session-id>/summary.md\` (per-session continuity)
9534
+
9535
+ Do NOT edit \`${params2.assignmentDir}/comments.md\` directly \u2014 use \`syntaur comment\`. Do NOT edit other assignments' files \u2014 use \`syntaur request\` for cross-assignment todos.
9536
+
9537
+ And source code files in your workspace. Read the \`workspace\` field from your assignment's frontmatter to determine the exact boundary. If not set, the current working directory is your workspace.
9538
+ `;
9539
+ }
9540
+ var init_cursor_rules = __esm({
9541
+ "src/templates/cursor-rules.ts"() {
9542
+ "use strict";
9543
+ }
9544
+ });
9545
+
9546
+ // src/templates/codex-agents.ts
9547
+ function renderCodexAgents(params2) {
9548
+ return `# Syntaur Protocol -- Agent Instructions
9549
+
9550
+ This project uses the Syntaur protocol for multi-agent project coordination.
9551
+
9552
+ ## Current Assignment
9553
+
9554
+ - **Project:** ${params2.projectSlug}
9555
+ - **Assignment:** ${params2.assignmentSlug}
9556
+ - **Project directory:** ${params2.projectDir}
9557
+ - **Assignment directory:** ${params2.assignmentDir}
9558
+
9559
+ ## Preferred Workflow
9560
+
9561
+ If the global Syntaur Codex plugin is installed, prefer these workflows instead of ad hoc protocol edits:
9562
+
9563
+ - \`syntaur-operator\` agent -- use for broad Syntaur protocol work or when a task spans multiple lifecycle steps
9564
+ - \`syntaur-protocol\` -- background protocol and write-boundary rules
9565
+ - \`create-project\` -- scaffold a project
9566
+ - \`create-assignment\` -- create a new assignment (use \`--type <bug|feature|chore|...>\` to classify; use \`--one-off\` to create a standalone assignment at \`~/.syntaur/assignments/<uuid>/\` with no parent project)
9567
+ - \`grab-assignment\` -- claim work, create \`.syntaur/context.json\`, and register a session
9568
+ - \`plan-assignment\` -- write a versioned plan file (\`plan.md\`, \`plan-v2.md\`, ...) and link it from the \`## Todos\` section of \`assignment.md\`
9569
+ - \`complete-assignment\` -- write the cross-ticket \`handoff.md\` entry, append a final entry to \`progress.md\`, close the session, and transition state
9570
+ - \`save-session-summary\` -- write per-session continuity at \`<assignmentDir>/sessions/<sessionId>/summary.md\` for resume across sessions of the same agent. Codex has no \`PreCompact\` hook event \u2014 invoke this manually before compaction or session end.
9571
+ - \`capture-artifacts\` -- capture typed proof artifacts (screenshot/video/asciinema/http/text) for the active assignment. Criterion linkage is optional. Run \`syntaur proof build\` to render \`proof.html\`.
9572
+ - \`resume-session\` -- counterpart to \`save-session-summary\`; loads the latest summary, \`.syntaur/context.json\`, and any open handoff so a fresh session re-orients without re-reading the transcript
9573
+ - \`replan\` -- bump the active assignment to a new \`plan-v<N>.md\` per the Plan Versioning playbook (CLI does file ops, skill writes the body)
9574
+ - \`syntaur-worktree\` -- atomic worktree creation under \`<repository>/.worktrees/<branch>\` plus assign + start + context binding in one move
9575
+ - \`add-resource\` -- register a project-level resource (link to dashboard / doc / ticket); CLI regenerates \`_index.md\` server-side
9576
+ - \`add-memory\` -- capture a project-level Syntaur memory; CLI regenerates \`_index.md\` server-side (distinct from user-global Claude Code auto-memory)
9577
+ - \`list-assignments\` -- cross-project listing with filters by status, project, tag, age (scriptable; not the interactive \`browse\` TUI)
9578
+ - \`log-progress\` -- append a timestamped entry to the active \`progress.md\` and bump frontmatter (Keep Records Updated playbook)
9579
+ - \`set-workspace\` -- populate the four \`workspace.*\` fields in \`assignment.md\`; validates via \`syntaur doctor --assignment --json\` before writing
9580
+ - \`track-session\` -- manage tracked tmux sessions for the dashboard
9581
+
9582
+ If the plugin is unavailable, follow the same workflow manually with the \`syntaur\` CLI and keep the protocol files current yourself.
9583
+
9584
+ ## Reading Order
9585
+
9586
+ Before starting work, read these files in order:
9587
+ 1. \`${params2.projectDir}/manifest.md\` -- root navigation entry point (project-nested assignments only)
9588
+ 2. \`${params2.projectDir}/project.md\` -- project overview and goals (project-nested assignments only)
9589
+ 3. \`${params2.assignmentDir}/assignment.md\` -- your assignment details, acceptance criteria, todos, current status. Frontmatter now includes \`project: <slug> | null\` (null for standalone) and \`type: <classification> | null\`.
9590
+ 4. any \`${params2.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
9591
+ 5. \`${params2.assignmentDir}/progress.md\` -- reverse-chron progress log (if present)
9592
+ 6. \`${params2.assignmentDir}/comments.md\` -- threaded questions/notes/feedback (if present)
9593
+ 7. \`${params2.assignmentDir}/handoff.md\` -- cross-ticket outbound history (entries from prior agents/humans handing this assignment off)
9594
+ 8. The latest \`${params2.assignmentDir}/sessions/<sid>/summary.md\` if present -- previous-session continuity (read it for "what was done / what's next" before resuming work in flight)
9595
+
9596
+ ## Context File
9597
+
9598
+ - Treat \`.syntaur/context.json\` in the current working directory as the active assignment context when it exists.
9599
+ - Use that file to resolve the workspace boundary, assignment path, and project path (the active assignment binding). The active session id, however, is resolved from *your* running process -- prefer \`$CLAUDE_CODE_SESSION_ID\` (or the peer \`OPENCODE_SESSION_ID\` / \`PI_SESSION_ID\`), otherwise run \`syntaur session resolve-id\`; the \`sessionId\` scalar in context.json is only a clobberable legacy hint, not authoritative.
9600
+ - If there is no context file yet and you are supposed to work on an assignment, claim or set up the assignment before editing code.
9601
+
9602
+ ## Directory Structure
9603
+
9604
+ \`\`\`
9605
+ ~/.syntaur/
9606
+ config.md
9607
+ projects/
9608
+ <project-slug>/
9609
+ manifest.md # Derived: root navigation (read-only)
9610
+ project.md # Human-authored: project overview (read-only)
9611
+ _index-assignments.md # Derived (read-only)
9612
+ _index-plans.md # Derived (read-only)
9613
+ _index-decisions.md # Derived (read-only)
9614
+ _status.md # Derived (read-only)
9615
+ assignments/
9616
+ <assignment-slug>/
9617
+ assignment.md # Agent-writable: source of truth for state (includes ## Todos)
9618
+ plan*.md # Agent-writable: versioned implementation plans (optional, one per ## Todos entry)
9619
+ progress.md # Agent-writable, append-only: timestamped progress log
9620
+ comments.md # CLI-mediated: threaded questions/notes/feedback (via \`syntaur comment\`)
9621
+ scratchpad.md # Agent-writable: working notes
9622
+ handoff.md # Agent-writable: append-only cross-ticket outbound at completion
9623
+ decision-record.md # Agent-writable: append-only decision log
9624
+ sessions/
9625
+ <session-id>/
9626
+ summary.md # Agent-writable: per-session continuity (single doc, overwritten)
9627
+ resources/
9628
+ _index.md # Derived (read-only)
9629
+ <resource-slug>.md # Shared-writable
9630
+ memories/
9631
+ _index.md # Derived (read-only)
9632
+ <memory-slug>.md # Shared-writable
9633
+ assignments/
9634
+ <assignment-id>/ # Standalone assignments \u2014 folder = UUID, \`project: null\`, slug display-only
9635
+ assignment.md
9636
+ plan*.md
9637
+ progress.md
9638
+ comments.md
9639
+ scratchpad.md
9640
+ handoff.md
9641
+ decision-record.md
9642
+ sessions/<session-id>/summary.md # Per-session continuity (same as project-nested)
9643
+ \`\`\`
9644
+
9645
+ ## Write Boundary Rules (CRITICAL)
9646
+
9647
+ ### Files you may WRITE:
9648
+ 1. **Your assignment folder** -- only the assignment you are currently working on:
9649
+ - \`assignment.md\`, \`plan*.md\` (0 or more versioned plan files), \`progress.md\`, \`scratchpad.md\`, \`handoff.md\` (cross-ticket outbound at completion), \`decision-record.md\`
9650
+ - \`sessions/<session-id>/summary.md\` -- per-session continuity (single doc per session id, overwritten on save). Distinct from \`handoff.md\`.
9651
+ - Path: \`${params2.assignmentDir}/\`
9652
+ 2. **Shared resources and memories** at the project level:
9653
+ - \`${params2.projectDir}/resources/<slug>.md\`
9654
+ - \`${params2.projectDir}/memories/<slug>.md\`
9655
+ 3. **Your workspace** -- source code files in the current working directory (the directory where this AGENTS.md lives). If your assignment's frontmatter specifies a \`workspace\` field, read it at runtime to determine the exact boundary.
9656
+
9657
+ > **Note:** Workspace boundaries are resolved by the agent at runtime by reading \`assignment.md\` frontmatter. If no \`workspace\` field is set, treat the current working directory as your workspace.
9658
+
9659
+ ### Files written only via CLI (never edit directly):
9660
+ - \`comments.md\` (any assignment) -- use \`syntaur comment <slug-or-uuid> "body" [--type question|note|feedback] [--reply-to <id>]\`
9661
+ - Another assignment's \`## Todos\` section -- use \`syntaur request <source> <target> "text"\` to request cross-assignment work
9662
+
9663
+ ### Files you must NEVER write:
9664
+ 1. \`project.md\` -- human-authored, read-only
9665
+ 2. \`manifest.md\` -- derived, rebuilt by tooling
9666
+ 3. Any file prefixed with \`_\` -- derived
9667
+ 4. Other agents' assignment folders (except via the CLI-mediated channels above)
9668
+ 5. Any files outside your workspace boundary
9669
+
9670
+ ## Assignment Lifecycle
9671
+
9672
+ | Status | Meaning |
9673
+ |--------|---------|
9674
+ | \`pending\` | Not yet started |
9675
+ | \`in_progress\` | Actively being worked on |
9676
+ | \`blocked\` | Manually blocked (requires blockedReason) |
9677
+ | \`review\` | Work complete, awaiting review |
9678
+ | \`completed\` | Done |
9679
+ | \`failed\` | Could not be completed |
9680
+
9681
+ ## Valid State Transitions
9682
+
9683
+ | From | Command | To |
9684
+ |------|---------|-----|
9685
+ | pending | start | in_progress |
9686
+ | pending | block | blocked |
9687
+ | in_progress | block | blocked |
9688
+ | in_progress | review | review |
9689
+ | in_progress | complete | completed |
9690
+ | in_progress | fail | failed |
9691
+ | blocked | unblock | in_progress |
9692
+ | review | start | in_progress |
9693
+ | review | complete | completed |
9694
+ | review | fail | failed |
9695
+
9696
+ ## Lifecycle Commands
9697
+
9698
+ Use the \`syntaur\` CLI for state transitions and coordination:
9699
+ - \`syntaur assign ${params2.assignmentSlug} --agent <name> --project ${params2.projectSlug}\` -- set assignee
9700
+ - \`syntaur start ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- pending -> in_progress
9701
+ - \`syntaur review ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- in_progress -> review
9702
+ - \`syntaur complete ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- in_progress/review -> completed
9703
+ - \`syntaur block ${params2.assignmentSlug} --project ${params2.projectSlug} --reason <text>\` -- block
9704
+ - \`syntaur unblock ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- unblock
9705
+ - \`syntaur fail ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- mark as failed
9706
+ - \`syntaur comment ${params2.assignmentSlug} "body" --type question|note|feedback [--reply-to <id>]\` -- append to \`comments.md\` (use for all Q&A; questions support resolve toggle)
9707
+ - \`syntaur request ${params2.assignmentSlug} <target-slug-or-uuid> "text"\` -- append a todo to another assignment's \`## Todos\` annotated \`(from: ${params2.assignmentSlug})\`
9708
+ - \`syntaur capture --kind <screenshot|video|asciinema|http|text> [--file <path>] [--criterion <index>] [--note <text>] [--transcribe] ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- record a proof artifact. \`--kind=text\` requires \`--note\` and forbids \`--file\`. Criterion linkage is optional. \`--transcribe\` is video-only and writes a sibling \`<id>.transcript.md\` (requires \`ELEVENLABS_API_KEY\` + \`ffmpeg\`).
9709
+ - \`syntaur proof build ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- render \`proof.html\` and \`proof.md\` at the assignment dir. Atomic overwrite \u2014 safe to re-run.
9710
+
9711
+ ## Troubleshooting
9712
+
9713
+ If Syntaur state looks inconsistent (missing files, stale manifests, unexpected hook blocks), run \`syntaur doctor\` to diagnose. Use \`--json\` for structured output.
9714
+
9715
+ ## Playbooks
9716
+
9717
+ Playbooks are user-defined behavioral rules stored in \`~/.syntaur/playbooks/\`. Before starting work, read the playbook manifest and then each referenced playbook:
9718
+
9719
+ \`\`\`bash
9720
+ cat ~/.syntaur/playbooks/manifest.md
9721
+ \`\`\`
9722
+
9723
+ Read each linked playbook and follow the rules in its body section. The \`when_to_use\` field tells you when each playbook applies. Playbooks take precedence over default conventions when they conflict.
9724
+
9725
+ ## Conventions
9726
+
9727
+ - Assignment frontmatter is the single source of truth for state. \`project\` is the containing project slug (\`null\` for standalone); \`type\` is a classification validated against \`config.md\` \`types.definitions\` when present.
9728
+ - Slugs are lowercase, hyphen-separated. For standalone assignments, \`slug\` is display-only; the folder is named by the UUID.
9729
+ - Always read \`project.md\` at the project level (when project-nested) before starting work.
9730
+ - Keep \`assignment.md\` acceptance criteria and \`## Todos\` updated as work lands; append timestamped entries to \`progress.md\` (never to \`assignment.md\`).
9731
+ - Keep active plan file(s) current after planning changes. Write \`handoff.md\` (via \`complete-assignment\`) at the cross-ticket boundary; write \`sessions/<sid>/summary.md\` (via \`/save-session-summary\`) before compaction or before ending a session mid-assignment so a future session can resume cleanly.
9732
+ - When requirements shift, supersede the prior plan todo (\`- [x] ~~...~~ (superseded by plan-v<N>)\`) and write a new plan file instead of rewriting the old one.
9733
+ - Record questions, notes, and feedback via \`syntaur comment\`. Never edit \`comments.md\` directly. Resolve questions via the dashboard UI (toggle on the question entry).
9734
+ - To route work to another assignment, use \`syntaur request\`.
9735
+ - Commit frequently with messages referencing the assignment slug.
9736
+ `;
9737
+ }
9738
+ var init_codex_agents = __esm({
9739
+ "src/templates/codex-agents.ts"() {
9740
+ "use strict";
9741
+ }
9742
+ });
9743
+
9744
+ // src/templates/opencode-config.ts
9745
+ function renderOpenCodeConfig(params2) {
9746
+ const config = {
9747
+ instructions: [
9748
+ `Read AGENTS.md in this directory for Syntaur protocol (v2.0) instructions.`,
9749
+ `Read ${params2.projectDir}/project.md for project overview (project-nested assignments only).`,
9750
+ `Append timestamped progress entries to the assignment's progress.md (not to assignment.md).`,
9751
+ `Use 'syntaur comment <slug-or-uuid> "body" --type question|note|feedback' to append to comments.md \u2014 never edit it directly.`,
9752
+ `Use 'syntaur request <source> <target> "text"' to append a todo to another assignment's ## Todos.`,
9753
+ `Assignment folders are project-nested at ~/.syntaur/projects/<slug>/assignments/<aslug>/ or standalone at ~/.syntaur/assignments/<uuid>/ (project: null, slug display-only).`
9754
+ ]
9755
+ };
9756
+ return JSON.stringify(config, null, 2) + "\n";
9757
+ }
9758
+ var init_opencode_config = __esm({
9759
+ "src/templates/opencode-config.ts"() {
9760
+ "use strict";
9761
+ }
9762
+ });
9763
+
9764
+ // src/templates/hermes-soul.ts
9765
+ function renderHermesSoul(params2) {
9766
+ const body = renderCodexAgents(params2);
9767
+ return `# SOUL -- Syntaur Protocol Operator
9768
+
9769
+ This agent follows the Syntaur protocol for multi-agent project coordination.
9770
+ Hermes loads this file as part of its identity / system context; treat the
9771
+ Write Boundary Rules and Lifecycle sections below as binding.
9772
+
9773
+ ${body}`;
9774
+ }
9775
+ var init_hermes_soul = __esm({
9776
+ "src/templates/hermes-soul.ts"() {
9777
+ "use strict";
9778
+ init_codex_agents();
9779
+ }
9780
+ });
9781
+
9327
9782
  // src/lifecycle/recompute.ts
9328
9783
  var recompute_exports = {};
9329
9784
  __export(recompute_exports, {
@@ -9557,26 +10012,376 @@ async function recomputeAll(projectsDir, standaloneDir, opts) {
9557
10012
  if (await fileExists(path)) await sweepOne(path, null);
9558
10013
  }
9559
10014
  }
9560
- return summary;
10015
+ return summary;
10016
+ }
10017
+ var LOCK_FILE, LOCK_STALE_MS, LOCK_WAIT_MS, LOCK_MAX_WAITS, CAS_RETRIES, MIGRATION_MARKER;
10018
+ var init_recompute = __esm({
10019
+ "src/lifecycle/recompute.ts"() {
10020
+ "use strict";
10021
+ init_config2();
10022
+ init_fs();
10023
+ init_paths();
10024
+ init_timestamp();
10025
+ init_facts();
10026
+ init_derive();
10027
+ init_frontmatter();
10028
+ init_types();
10029
+ LOCK_FILE = ".derive.lock";
10030
+ LOCK_STALE_MS = 3e4;
10031
+ LOCK_WAIT_MS = 50;
10032
+ LOCK_MAX_WAITS = 100;
10033
+ CAS_RETRIES = 3;
10034
+ MIGRATION_MARKER = "derive-migrated";
10035
+ }
10036
+ });
10037
+
10038
+ // src/utils/transcript.ts
10039
+ import { open as open2 } from "fs/promises";
10040
+ async function derivePathFromTranscript(transcriptPath) {
10041
+ if (!transcriptPath) return null;
10042
+ let handle;
10043
+ try {
10044
+ handle = await open2(transcriptPath, "r");
10045
+ } catch {
10046
+ return null;
10047
+ }
10048
+ try {
10049
+ const stream = handle.createReadStream({ encoding: "utf-8" });
10050
+ let buffer = "";
10051
+ let scanned = 0;
10052
+ for await (const chunk of stream) {
10053
+ buffer += chunk;
10054
+ let nl = buffer.indexOf("\n");
10055
+ while (nl !== -1) {
10056
+ const line = buffer.slice(0, nl);
10057
+ buffer = buffer.slice(nl + 1);
10058
+ const cwd = extractCwd(line);
10059
+ if (cwd) {
10060
+ stream.destroy();
10061
+ return cwd;
10062
+ }
10063
+ scanned++;
10064
+ if (scanned >= MAX_LINES_SCANNED) {
10065
+ stream.destroy();
10066
+ return null;
10067
+ }
10068
+ nl = buffer.indexOf("\n");
10069
+ }
10070
+ }
10071
+ if (buffer.length > 0) {
10072
+ const cwd = extractCwd(buffer);
10073
+ if (cwd) return cwd;
10074
+ }
10075
+ return null;
10076
+ } finally {
10077
+ await handle.close().catch(() => {
10078
+ });
10079
+ }
10080
+ }
10081
+ function extractCwd(line) {
10082
+ const trimmed = line.trim();
10083
+ if (trimmed.length === 0 || trimmed[0] !== "{") return null;
10084
+ try {
10085
+ const parsed = JSON.parse(trimmed);
10086
+ if (typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
10087
+ return parsed.cwd;
10088
+ }
10089
+ } catch {
10090
+ }
10091
+ return null;
10092
+ }
10093
+ var MAX_LINES_SCANNED;
10094
+ var init_transcript = __esm({
10095
+ "src/utils/transcript.ts"() {
10096
+ "use strict";
10097
+ MAX_LINES_SCANNED = 50;
10098
+ }
10099
+ });
10100
+
10101
+ // src/utils/process-info.ts
10102
+ import { execFileSync as execFileSync2 } from "child_process";
10103
+ function captureProcessStartedAt(pid) {
10104
+ if (!Number.isFinite(pid) || pid <= 0) return null;
10105
+ try {
10106
+ const out = execFileSync2("ps", ["-o", "lstart=", "-p", String(pid)], {
10107
+ encoding: "utf8",
10108
+ stdio: ["ignore", "pipe", "ignore"]
10109
+ });
10110
+ const trimmed = out.trim();
10111
+ return trimmed === "" ? null : trimmed;
10112
+ } catch {
10113
+ return null;
10114
+ }
10115
+ }
10116
+ var init_process_info = __esm({
10117
+ "src/utils/process-info.ts"() {
10118
+ "use strict";
10119
+ }
10120
+ });
10121
+
10122
+ // src/usage/cwd-extractor.ts
10123
+ import { open as open3, readdir as readdir11, stat as stat2 } from "fs/promises";
10124
+ import { join as join3 } from "path";
10125
+ import { homedir as homedir3 } from "os";
10126
+ async function extractClaudeSessionMeta(jsonlPath) {
10127
+ const cwd = await derivePathFromTranscript(jsonlPath);
10128
+ if (!cwd) return null;
10129
+ const basename6 = jsonlPath.split("/").pop() ?? "";
10130
+ const sessionId = basename6.replace(/\.jsonl$/, "");
10131
+ if (!sessionId) return null;
10132
+ const startTs = await readFirstTimestamp(jsonlPath);
10133
+ const endTs = await readLastTimestamp(jsonlPath);
10134
+ return {
10135
+ tool: "claude",
10136
+ sessionId,
10137
+ cwd,
10138
+ startTs,
10139
+ endTs,
10140
+ path: jsonlPath
10141
+ };
10142
+ }
10143
+ async function extractCodexSessionMeta(jsonlPath) {
10144
+ let handle;
10145
+ try {
10146
+ handle = await open3(jsonlPath, "r");
10147
+ } catch {
10148
+ return null;
10149
+ }
10150
+ try {
10151
+ const stream = handle.createReadStream({ encoding: "utf-8" });
10152
+ let buffer = "";
10153
+ let firstLine = null;
10154
+ for await (const chunk of stream) {
10155
+ buffer += chunk;
10156
+ const nl = buffer.indexOf("\n");
10157
+ if (nl !== -1) {
10158
+ firstLine = buffer.slice(0, nl);
10159
+ stream.destroy();
10160
+ break;
10161
+ }
10162
+ }
10163
+ if (!firstLine && buffer.length > 0) firstLine = buffer;
10164
+ if (!firstLine) return null;
10165
+ let parsed;
10166
+ try {
10167
+ parsed = JSON.parse(firstLine);
10168
+ } catch {
10169
+ return null;
10170
+ }
10171
+ if (!parsed || typeof parsed !== "object") return null;
10172
+ const obj = parsed;
10173
+ if (obj.type !== "session_meta") return null;
10174
+ const timestamp = typeof obj.timestamp === "string" ? obj.timestamp : null;
10175
+ const payload = obj.payload;
10176
+ const id = payload && typeof payload.id === "string" ? payload.id : null;
10177
+ const cwd = payload && typeof payload.cwd === "string" ? payload.cwd : null;
10178
+ if (!timestamp || !id || !cwd) return null;
10179
+ const endTs = await readLastTimestamp(jsonlPath) ?? timestamp;
10180
+ return {
10181
+ tool: "codex",
10182
+ sessionId: id,
10183
+ cwd,
10184
+ startTs: timestamp,
10185
+ endTs,
10186
+ path: jsonlPath
10187
+ };
10188
+ } finally {
10189
+ await handle.close().catch(() => {
10190
+ });
10191
+ }
10192
+ }
10193
+ async function* walkClaudeProjects(opts = {}) {
10194
+ const root = expandHome(opts.root ?? "~/.claude/projects");
10195
+ const dirs = await listDirSafe(root);
10196
+ for (const dirent of dirs) {
10197
+ if (!dirent.isDirectory) continue;
10198
+ const dirPath = join3(root, dirent.name);
10199
+ const files = await listDirSafe(dirPath);
10200
+ let cachedCwd = null;
10201
+ for (const f of files) {
10202
+ if (!f.isFile || !f.name.endsWith(".jsonl")) continue;
10203
+ const filePath = join3(dirPath, f.name);
10204
+ if (opts.sinceMtimeMs !== void 0) {
10205
+ const mtime = await mtimeMs(filePath);
10206
+ if (mtime !== null && mtime < opts.sinceMtimeMs) continue;
10207
+ }
10208
+ let meta;
10209
+ if (cachedCwd) {
10210
+ const sessionId = f.name.replace(/\.jsonl$/, "");
10211
+ const startTs = await readFirstTimestamp(filePath);
10212
+ const endTs = await readLastTimestamp(filePath);
10213
+ meta = { tool: "claude", sessionId, cwd: cachedCwd, startTs, endTs, path: filePath };
10214
+ } else {
10215
+ meta = await extractClaudeSessionMeta(filePath);
10216
+ if (meta) cachedCwd = meta.cwd;
10217
+ }
10218
+ if (meta) yield meta;
10219
+ }
10220
+ }
10221
+ }
10222
+ async function* walkCodexSessions(opts = {}) {
10223
+ const root = resolveCodexSessionsRoot(opts.root);
10224
+ for await (const filePath of walkJsonlRecursive(root)) {
10225
+ const basename6 = filePath.split("/").pop() ?? "";
10226
+ if (!basename6.endsWith(".jsonl")) continue;
10227
+ if (opts.sinceMtimeMs !== void 0) {
10228
+ const mtime = await mtimeMs(filePath);
10229
+ if (mtime !== null && mtime < opts.sinceMtimeMs) continue;
10230
+ }
10231
+ const meta = await extractCodexSessionMeta(filePath);
10232
+ if (meta) yield meta;
10233
+ }
10234
+ }
10235
+ function resolveCodexSessionsRoot(override) {
10236
+ if (override) return expandHome(override);
10237
+ const fromSessionsEnv = process.env.CODEX_SESSIONS_DIR;
10238
+ if (fromSessionsEnv && fromSessionsEnv.length > 0) return expandHome(fromSessionsEnv);
10239
+ const fromHomeEnv = process.env.CODEX_HOME;
10240
+ if (fromHomeEnv && fromHomeEnv.length > 0) return join3(expandHome(fromHomeEnv), "sessions");
10241
+ return join3(homedir3(), ".codex", "sessions");
10242
+ }
10243
+ async function listDirSafe(path) {
10244
+ try {
10245
+ const entries = await readdir11(path, { withFileTypes: true });
10246
+ return entries.map((e) => ({
10247
+ name: e.name,
10248
+ isFile: e.isFile(),
10249
+ isDirectory: e.isDirectory()
10250
+ }));
10251
+ } catch {
10252
+ return [];
10253
+ }
10254
+ }
10255
+ async function* walkJsonlRecursive(root) {
10256
+ const stack = [root];
10257
+ while (stack.length > 0) {
10258
+ const current = stack.pop();
10259
+ const entries = await listDirSafe(current);
10260
+ for (const e of entries) {
10261
+ const full = join3(current, e.name);
10262
+ if (e.isDirectory) {
10263
+ stack.push(full);
10264
+ } else if (e.isFile && e.name.endsWith(".jsonl")) {
10265
+ yield full;
10266
+ }
10267
+ }
10268
+ }
10269
+ }
10270
+ async function mtimeMs(path) {
10271
+ try {
10272
+ const s = await stat2(path);
10273
+ return s.mtimeMs;
10274
+ } catch {
10275
+ return null;
10276
+ }
10277
+ }
10278
+ async function readFirstTimestamp(path) {
10279
+ let handle;
10280
+ try {
10281
+ handle = await open3(path, "r");
10282
+ } catch {
10283
+ return null;
10284
+ }
10285
+ try {
10286
+ const stream = handle.createReadStream({ encoding: "utf-8" });
10287
+ let buffer = "";
10288
+ let scanned = 0;
10289
+ for await (const chunk of stream) {
10290
+ buffer += chunk;
10291
+ let nl = buffer.indexOf("\n");
10292
+ while (nl !== -1) {
10293
+ const line = buffer.slice(0, nl);
10294
+ buffer = buffer.slice(nl + 1);
10295
+ const ts = extractTimestamp(line);
10296
+ if (ts) {
10297
+ stream.destroy();
10298
+ return ts;
10299
+ }
10300
+ scanned++;
10301
+ if (scanned >= SCAN_LINE_CAP) {
10302
+ stream.destroy();
10303
+ return null;
10304
+ }
10305
+ nl = buffer.indexOf("\n");
10306
+ }
10307
+ }
10308
+ if (buffer.length > 0) return extractTimestamp(buffer);
10309
+ return null;
10310
+ } finally {
10311
+ await handle.close().catch(() => {
10312
+ });
10313
+ }
10314
+ }
10315
+ async function readLastTimestamp(path) {
10316
+ let handle;
10317
+ try {
10318
+ handle = await open3(path, "r");
10319
+ } catch {
10320
+ return null;
10321
+ }
10322
+ try {
10323
+ const stats = await handle.stat();
10324
+ const size = stats.size;
10325
+ if (size === 0) return null;
10326
+ for (const windowBytes of [TAIL_READ_BYTES, TAIL_READ_BYTES_MAX]) {
10327
+ const start = Math.max(0, size - windowBytes);
10328
+ const length = size - start;
10329
+ const buf = Buffer.alloc(length);
10330
+ await handle.read(buf, 0, length, start);
10331
+ const text = buf.toString("utf-8");
10332
+ const lines = text.split("\n");
10333
+ if (start > 0) lines.shift();
10334
+ for (let i = lines.length - 1; i >= 0; i--) {
10335
+ const ts = extractTimestamp(lines[i]);
10336
+ if (ts) return ts;
10337
+ }
10338
+ if (start === 0) break;
10339
+ }
10340
+ return null;
10341
+ } finally {
10342
+ await handle.close().catch(() => {
10343
+ });
10344
+ }
10345
+ }
10346
+ function extractTimestamp(line) {
10347
+ const trimmed = line.trim();
10348
+ if (trimmed.length === 0 || trimmed[0] !== "{") return null;
10349
+ try {
10350
+ const parsed = JSON.parse(trimmed);
10351
+ if (typeof parsed.timestamp === "string" && parsed.timestamp.length > 0) {
10352
+ return parsed.timestamp;
10353
+ }
10354
+ } catch {
10355
+ }
10356
+ return null;
9561
10357
  }
9562
- var LOCK_FILE, LOCK_STALE_MS, LOCK_WAIT_MS, LOCK_MAX_WAITS, CAS_RETRIES, MIGRATION_MARKER;
9563
- var init_recompute = __esm({
9564
- "src/lifecycle/recompute.ts"() {
10358
+ var SCAN_LINE_CAP, TAIL_READ_BYTES, TAIL_READ_BYTES_MAX;
10359
+ var init_cwd_extractor = __esm({
10360
+ "src/usage/cwd-extractor.ts"() {
9565
10361
  "use strict";
9566
- init_config2();
9567
- init_fs();
9568
10362
  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";
10363
+ init_transcript();
10364
+ SCAN_LINE_CAP = 50;
10365
+ TAIL_READ_BYTES = 8 * 1024;
10366
+ TAIL_READ_BYTES_MAX = 64 * 1024;
10367
+ }
10368
+ });
10369
+
10370
+ // src/utils/session-id.ts
10371
+ import { execFileSync as execFileSync3 } from "child_process";
10372
+ import { mkdirSync, readFileSync, statSync as statSync3, writeFileSync } from "fs";
10373
+ import { homedir as homedir4 } from "os";
10374
+ import { dirname as dirname5, join as join4 } from "path";
10375
+ function isSafeSessionId(value) {
10376
+ return typeof value === "string" && value.length > 0 && value.length <= 256 && SAFE_SESSION_ID.test(value);
10377
+ }
10378
+ var SAFE_SESSION_ID;
10379
+ var init_session_id = __esm({
10380
+ "src/utils/session-id.ts"() {
10381
+ "use strict";
10382
+ init_process_info();
10383
+ init_cwd_extractor();
10384
+ SAFE_SESSION_ID = /^[A-Za-z0-9_-]+$/;
9580
10385
  }
9581
10386
  });
9582
10387
 
@@ -9627,6 +10432,420 @@ var init_assignment_todos = __esm({
9627
10432
  }
9628
10433
  });
9629
10434
 
10435
+ // src/targets/renderers.ts
10436
+ var RENDERERS;
10437
+ var init_renderers = __esm({
10438
+ "src/targets/renderers.ts"() {
10439
+ "use strict";
10440
+ init_cursor_rules();
10441
+ init_codex_agents();
10442
+ init_opencode_config();
10443
+ init_hermes_soul();
10444
+ RENDERERS = {
10445
+ codexAgents: (ctx) => renderCodexAgents(ctx),
10446
+ cursorProtocol: () => renderCursorProtocol(),
10447
+ cursorAssignment: (ctx) => renderCursorAssignment(ctx),
10448
+ openCodeConfig: (ctx) => renderOpenCodeConfig({ projectDir: ctx.projectDir }),
10449
+ hermesSoul: (ctx) => renderHermesSoul(ctx)
10450
+ };
10451
+ }
10452
+ });
10453
+
10454
+ // src/targets/user-descriptors.ts
10455
+ import { resolve as resolve34 } from "path";
10456
+ import { readFile as readFile23, readdir as readdir16 } from "fs/promises";
10457
+ var VALID_RENDERER_KEYS;
10458
+ var init_user_descriptors = __esm({
10459
+ "src/targets/user-descriptors.ts"() {
10460
+ "use strict";
10461
+ init_fs();
10462
+ init_paths();
10463
+ init_renderers();
10464
+ VALID_RENDERER_KEYS = new Set(Object.keys(RENDERERS));
10465
+ }
10466
+ });
10467
+
10468
+ // src/targets/registry.ts
10469
+ import { homedir as homedir6 } from "os";
10470
+ import { join as join8, resolve as resolve35 } from "path";
10471
+ function home(...segments) {
10472
+ return resolve35(homedir6(), ...segments);
10473
+ }
10474
+ function hermesHome() {
10475
+ const env = process.env.HERMES_HOME;
10476
+ return env && env.length > 0 ? resolve35(env) : home(".hermes");
10477
+ }
10478
+ function hermesSkillsDir() {
10479
+ return resolve35(hermesHome(), "skills");
10480
+ }
10481
+ function codexHome() {
10482
+ const env = process.env.CODEX_HOME;
10483
+ return env && env.length > 0 ? resolve35(env) : home(".codex");
10484
+ }
10485
+ function toDiscovered(meta) {
10486
+ if (!meta) return null;
10487
+ return {
10488
+ sessionId: meta.sessionId,
10489
+ cwd: meta.cwd,
10490
+ startedAt: meta.startTs,
10491
+ endedAt: meta.endTs,
10492
+ transcriptPath: meta.path
10493
+ };
10494
+ }
10495
+ var detectDir, claudeSessions, codexSessions, AGENT_TARGETS, AGENT_TARGETS_BY_ID;
10496
+ var init_registry = __esm({
10497
+ "src/targets/registry.ts"() {
10498
+ "use strict";
10499
+ init_fs();
10500
+ init_cwd_extractor();
10501
+ init_user_descriptors();
10502
+ detectDir = (dir) => () => fileExists(dir);
10503
+ claudeSessions = {
10504
+ globs: (root) => [join8(root ?? home(".claude", "projects"), "*", "*.jsonl")],
10505
+ parse: async (file) => toDiscovered(await extractClaudeSessionMeta(file)),
10506
+ walk: async function* (opts = {}) {
10507
+ for await (const meta of walkClaudeProjects({ root: opts.root, sinceMtimeMs: opts.sinceMtimeMs })) {
10508
+ const d = toDiscovered(meta);
10509
+ if (d) yield d;
10510
+ }
10511
+ }
10512
+ };
10513
+ codexSessions = {
10514
+ globs: (root) => [join8(root ?? resolveCodexSessionsRoot(), "**", "*.jsonl")],
10515
+ parse: async (file) => toDiscovered(await extractCodexSessionMeta(file)),
10516
+ walk: async function* (opts = {}) {
10517
+ for await (const meta of walkCodexSessions({ root: opts.root, sinceMtimeMs: opts.sinceMtimeMs })) {
10518
+ const d = toDiscovered(meta);
10519
+ if (d) yield d;
10520
+ }
10521
+ }
10522
+ };
10523
+ AGENT_TARGETS = [
10524
+ {
10525
+ id: "cursor",
10526
+ displayName: "Cursor",
10527
+ skillsShAgentId: "cursor",
10528
+ detect: detectDir(home(".cursor")),
10529
+ skillsDir: { global: home(".cursor", "skills") },
10530
+ instructions: {
10531
+ files: [
10532
+ { path: ".cursor/rules/syntaur-protocol.mdc", renderer: "cursorProtocol" },
10533
+ { path: ".cursor/rules/syntaur-assignment.mdc", renderer: "cursorAssignment" }
10534
+ ]
10535
+ }
10536
+ },
10537
+ {
10538
+ // codex is BOTH an adapter (writes AGENTS.md) AND a native plugin.
10539
+ id: "codex",
10540
+ displayName: "Codex",
10541
+ skillsShAgentId: "codex",
10542
+ nativePlugin: "codex",
10543
+ detect: detectDir(codexHome()),
10544
+ skillsDir: { global: resolve35(codexHome(), "skills") },
10545
+ instructions: { files: [{ path: "AGENTS.md", renderer: "codexAgents" }] },
10546
+ sessions: codexSessions
10547
+ },
10548
+ {
10549
+ id: "opencode",
10550
+ displayName: "OpenCode",
10551
+ skillsShAgentId: "opencode",
10552
+ detect: detectDir(home(".config", "opencode")),
10553
+ skillsDir: { global: home(".config", "opencode", "skills") },
10554
+ instructions: {
10555
+ files: [
10556
+ { path: "AGENTS.md", renderer: "codexAgents" },
10557
+ { path: "opencode.json", renderer: "openCodeConfig" }
10558
+ ]
10559
+ }
10560
+ },
10561
+ {
10562
+ // claude has NO adapter today (not in the old SUPPORTED_FRAMEWORKS) — the
10563
+ // full plugin path owns its skills/hooks/commands. Native-plugin only.
10564
+ id: "claude",
10565
+ displayName: "Claude Code",
10566
+ skillsShAgentId: "claude-code",
10567
+ nativePlugin: "claude",
10568
+ detect: detectDir(home(".claude")),
10569
+ skillsDir: { global: home(".claude", "skills") },
10570
+ sessions: claudeSessions
10571
+ },
10572
+ {
10573
+ id: "pi",
10574
+ displayName: "Pi",
10575
+ skillsShAgentId: "pi",
10576
+ detect: detectDir(home(".pi")),
10577
+ skillsDir: { global: home(".pi", "agent", "skills") },
10578
+ instructions: { files: [{ path: "AGENTS.md", renderer: "codexAgents" }] },
10579
+ tier3: {
10580
+ kind: "pi-extension",
10581
+ source: "platforms/pi/extensions/syntaur",
10582
+ installDir: () => home(".pi", "agent", "extensions", "syntaur"),
10583
+ entry: "index.ts"
10584
+ }
10585
+ },
10586
+ {
10587
+ id: "openclaw",
10588
+ displayName: "OpenClaw",
10589
+ skillsShAgentId: "openclaw",
10590
+ detect: detectDir(home(".openclaw")),
10591
+ skillsDir: { global: home(".openclaw", "skills") },
10592
+ instructions: { files: [{ path: "AGENTS.md", renderer: "codexAgents" }] },
10593
+ // OpenClaw runs on pi-coding-agent (design memo), so it reuses the pi
10594
+ // extension SOURCE; only the install dir differs.
10595
+ tier3: {
10596
+ kind: "pi-extension",
10597
+ source: "platforms/pi/extensions/syntaur",
10598
+ installDir: () => home(".openclaw", "extensions", "syntaur"),
10599
+ entry: "index.ts"
10600
+ }
10601
+ },
10602
+ {
10603
+ id: "hermes",
10604
+ displayName: "Hermes Agent",
10605
+ skillsShAgentId: "hermes-agent",
10606
+ detect: () => fileExists(hermesHome()),
10607
+ skillsDir: { global: hermesSkillsDir() },
10608
+ instructions: { files: [{ path: "SOUL.md", renderer: "hermesSoul" }] },
10609
+ tier3: {
10610
+ kind: "hermes-plugin",
10611
+ source: "platforms/hermes/plugins/syntaur",
10612
+ installDir: () => resolve35(hermesHome(), "plugins", "syntaur"),
10613
+ entry: "plugin.yaml"
10614
+ }
10615
+ }
10616
+ ];
10617
+ AGENT_TARGETS_BY_ID = Object.fromEntries(
10618
+ AGENT_TARGETS.map((t) => [t.id, t])
10619
+ );
10620
+ }
10621
+ });
10622
+
10623
+ // src/sessions/scanner.ts
10624
+ var scanner_exports2 = {};
10625
+ __export(scanner_exports2, {
10626
+ scanSessions: () => scanSessions
10627
+ });
10628
+ import { execFile as execFile3, execFileSync as execFileSync4 } from "child_process";
10629
+ import { promisify as promisify3 } from "util";
10630
+ import { statSync as statSync4 } from "fs";
10631
+ import { readFile as readFile24 } from "fs/promises";
10632
+ import { resolve as resolve36 } from "path";
10633
+ function emptySummary() {
10634
+ return { discovered: 0, inserted: 0, revived: 0, swept: 0, skipped: 0, changed: false };
10635
+ }
10636
+ function defaultStatMtimeMs(path) {
10637
+ try {
10638
+ return statSync4(path).mtimeMs;
10639
+ } catch {
10640
+ return null;
10641
+ }
10642
+ }
10643
+ function defaultIsPidAlive(pid) {
10644
+ if (!Number.isFinite(pid) || pid <= 0) return false;
10645
+ try {
10646
+ process.kill(pid, 0);
10647
+ return true;
10648
+ } catch (err) {
10649
+ return err.code === "EPERM";
10650
+ }
10651
+ }
10652
+ function defaultPidStartedAt(pid) {
10653
+ if (!Number.isFinite(pid) || pid <= 0) return null;
10654
+ try {
10655
+ const out = execFileSync4("ps", ["-o", "lstart=", "-p", String(pid)], {
10656
+ encoding: "utf8",
10657
+ stdio: ["ignore", "pipe", "ignore"]
10658
+ });
10659
+ const trimmed = out.trim();
10660
+ return trimmed === "" ? null : trimmed;
10661
+ } catch {
10662
+ return null;
10663
+ }
10664
+ }
10665
+ async function defaultOpenFiles(files) {
10666
+ const open5 = /* @__PURE__ */ new Set();
10667
+ for (let i = 0; i < files.length; i += LSOF_CHUNK) {
10668
+ const chunk = files.slice(i, i + LSOF_CHUNK);
10669
+ let stdout = "";
10670
+ try {
10671
+ const result = await execFileAsync("lsof", ["-Fn", "--", ...chunk], {
10672
+ maxBuffer: 8 * 1024 * 1024
10673
+ });
10674
+ stdout = result.stdout;
10675
+ } catch (err) {
10676
+ const maybe = err.stdout;
10677
+ stdout = typeof maybe === "string" ? maybe : "";
10678
+ }
10679
+ for (const line of stdout.split("\n")) {
10680
+ if (line.startsWith("n") && line.length > 1) open5.add(line.slice(1));
10681
+ }
10682
+ }
10683
+ return open5;
10684
+ }
10685
+ async function readContextLink(cwd, cache2) {
10686
+ if (cache2.has(cwd)) return cache2.get(cwd);
10687
+ let link = null;
10688
+ const path = resolve36(cwd, ".syntaur", "context.json");
10689
+ if (await fileExists(path)) {
10690
+ try {
10691
+ const parsed = JSON.parse(await readFile24(path, "utf-8"));
10692
+ link = {
10693
+ projectSlug: typeof parsed.projectSlug === "string" ? parsed.projectSlug : null,
10694
+ assignmentSlug: typeof parsed.assignmentSlug === "string" ? parsed.assignmentSlug : null
10695
+ };
10696
+ } catch {
10697
+ link = { projectSlug: null, assignmentSlug: null };
10698
+ }
10699
+ }
10700
+ cache2.set(cwd, link);
10701
+ return link;
10702
+ }
10703
+ function readWatermark() {
10704
+ const db4 = getSessionDb();
10705
+ const row = db4.prepare("SELECT value FROM meta WHERE key = ?").get(WATERMARK_KEY);
10706
+ if (!row) return null;
10707
+ const parsed = Number.parseInt(row.value, 10);
10708
+ return Number.isFinite(parsed) ? parsed : null;
10709
+ }
10710
+ function writeWatermark(ms) {
10711
+ const db4 = getSessionDb();
10712
+ db4.prepare(
10713
+ "INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
10714
+ ).run(WATERMARK_KEY, String(ms));
10715
+ }
10716
+ async function scanSessions(opts = {}, deps = {}) {
10717
+ const summary = emptySummary();
10718
+ const autoTrack = deps.autoTrack ?? (await readConfig()).session.autoTrack;
10719
+ if (autoTrack === "off") return summary;
10720
+ const now = deps.now ?? (() => Date.now());
10721
+ const statMtimeMs = deps.statMtimeMs ?? defaultStatMtimeMs;
10722
+ const openFiles = deps.openFiles ?? defaultOpenFiles;
10723
+ const isPidAlive = deps.isPidAlive ?? defaultIsPidAlive;
10724
+ const pidStartedAt = deps.pidStartedAt ?? defaultPidStartedAt;
10725
+ const targets = (deps.targets ?? AGENT_TARGETS).filter((t) => t.sessions !== void 0);
10726
+ const scanStartMs = now();
10727
+ const watermark = opts.full ? null : readWatermark();
10728
+ const discovered = [];
10729
+ for (const target of targets) {
10730
+ const walk = target.sessions.walk({
10731
+ root: deps.roots?.[target.id],
10732
+ sinceMtimeMs: watermark ?? void 0
10733
+ });
10734
+ for await (const session of walk) {
10735
+ if (!isSafeSessionId(session.sessionId)) continue;
10736
+ discovered.push({ ...session, agent: target.id });
10737
+ }
10738
+ }
10739
+ summary.discovered = discovered.length;
10740
+ const openSet = await openFiles(discovered.map((d) => d.transcriptPath));
10741
+ const contextCache = /* @__PURE__ */ new Map();
10742
+ for (const d of discovered) {
10743
+ const link = await readContextLink(d.cwd, contextCache);
10744
+ if (autoTrack === "workspaces-only" && link === null) {
10745
+ summary.skipped += 1;
10746
+ continue;
10747
+ }
10748
+ const mtime = statMtimeMs(d.transcriptPath);
10749
+ const heldOpen = openSet.has(d.transcriptPath);
10750
+ const isLive = heldOpen || mtime !== null && now() - mtime < FRESH_MTIME_MS;
10751
+ const prev = getSessionById(d.sessionId);
10752
+ const status = isLive ? "active" : prev?.status ?? "stopped";
10753
+ const started = d.startedAt ?? (mtime !== null ? new Date(mtime).toISOString() : new Date(now()).toISOString());
10754
+ await appendSession(
10755
+ "",
10756
+ {
10757
+ sessionId: d.sessionId,
10758
+ projectSlug: link?.projectSlug ?? null,
10759
+ assignmentSlug: link?.assignmentSlug ?? null,
10760
+ agent: d.agent,
10761
+ started,
10762
+ status,
10763
+ path: d.cwd,
10764
+ description: null,
10765
+ transcriptPath: d.transcriptPath,
10766
+ pid: null,
10767
+ pidStartedAt: null,
10768
+ originalHeadSha: null
10769
+ },
10770
+ // Narrow revival rule: only LIVE-PROCESS evidence (a process holding the
10771
+ // transcript open) may flip a stopped row back to active. mtime freshness
10772
+ // alone must not — a session stopped moments ago by its SessionEnd hook
10773
+ // still has a fresh transcript for up to 5 minutes and would flap back to
10774
+ // active. `completed` always sticks (appendSession enforces).
10775
+ { reviveStopped: heldOpen }
10776
+ );
10777
+ if (!isLive) {
10778
+ const after = getSessionById(d.sessionId);
10779
+ if (after && after.status === "stopped" && !after.ended) {
10780
+ const endedAt = d.endedAt ?? (mtime !== null ? new Date(mtime).toISOString() : void 0);
10781
+ await updateSessionStatus("", d.sessionId, "stopped", endedAt);
10782
+ }
10783
+ }
10784
+ if (!prev) {
10785
+ summary.inserted += 1;
10786
+ summary.changed = true;
10787
+ } else {
10788
+ if (prev.status === "stopped" && heldOpen) {
10789
+ summary.revived += 1;
10790
+ summary.changed = true;
10791
+ }
10792
+ if (link?.projectSlug && !prev.projectSlug || link?.assignmentSlug && !prev.assignmentSlug) {
10793
+ summary.changed = true;
10794
+ }
10795
+ }
10796
+ }
10797
+ const db4 = getSessionDb();
10798
+ const activeRows = db4.prepare("SELECT session_id, pid, pid_started_at, transcript_path FROM sessions WHERE status = 'active'").all();
10799
+ const sweepCandidates = [];
10800
+ for (const row of activeRows) {
10801
+ if (row.pid !== null) {
10802
+ const alive = isPidAlive(row.pid) && (!row.pid_started_at || (pidStartedAt(row.pid) ?? row.pid_started_at) === row.pid_started_at);
10803
+ if (alive) continue;
10804
+ }
10805
+ if (row.transcript_path) {
10806
+ sweepCandidates.push({ sessionId: row.session_id, transcriptPath: row.transcript_path });
10807
+ } else if (row.pid !== null) {
10808
+ sweepCandidates.push({ sessionId: row.session_id, transcriptPath: null });
10809
+ }
10810
+ }
10811
+ const sweepOpenSet = await openFiles(
10812
+ sweepCandidates.map((c) => c.transcriptPath).filter((p) => p !== null)
10813
+ );
10814
+ for (const candidate of sweepCandidates) {
10815
+ if (candidate.transcriptPath) {
10816
+ if (sweepOpenSet.has(candidate.transcriptPath)) continue;
10817
+ const mtime = statMtimeMs(candidate.transcriptPath);
10818
+ if (mtime !== null && now() - mtime < FRESH_MTIME_MS) continue;
10819
+ const endedAt = mtime !== null ? new Date(mtime).toISOString() : void 0;
10820
+ if (await updateSessionStatus("", candidate.sessionId, "stopped", endedAt)) {
10821
+ summary.swept += 1;
10822
+ summary.changed = true;
10823
+ }
10824
+ } else if (await updateSessionStatus("", candidate.sessionId, "stopped")) {
10825
+ summary.swept += 1;
10826
+ summary.changed = true;
10827
+ }
10828
+ }
10829
+ writeWatermark(scanStartMs);
10830
+ return summary;
10831
+ }
10832
+ var execFileAsync, FRESH_MTIME_MS, LSOF_CHUNK, WATERMARK_KEY;
10833
+ var init_scanner2 = __esm({
10834
+ "src/sessions/scanner.ts"() {
10835
+ "use strict";
10836
+ init_fs();
10837
+ init_config2();
10838
+ init_session_id();
10839
+ init_registry();
10840
+ init_session_db();
10841
+ init_agent_sessions();
10842
+ execFileAsync = promisify3(execFile3);
10843
+ FRESH_MTIME_MS = 5 * 60 * 1e3;
10844
+ LSOF_CHUNK = 64;
10845
+ WATERMARK_KEY = "sessions_scan_last_ms";
10846
+ }
10847
+ });
10848
+
9630
10849
  // src/dashboard/server.ts
9631
10850
  init_paths();
9632
10851
  init_api();
@@ -9634,7 +10853,7 @@ init_assignment_resolver();
9634
10853
  init_agent_sessions();
9635
10854
  import express from "express";
9636
10855
  import { createServer } from "http";
9637
- import { resolve as resolve33 } from "path";
10856
+ import { resolve as resolve37 } from "path";
9638
10857
  import { writeFile as writeFile8, unlink as unlink8 } from "fs/promises";
9639
10858
  import { WebSocketServer, WebSocket } from "ws";
9640
10859
 
@@ -9929,11 +11148,9 @@ function createWatcher(options) {
9929
11148
  debounceKey,
9930
11149
  setTimeout(() => {
9931
11150
  pendingEvents.delete(debounceKey);
9932
- const message = {
9933
- type: "leases-updated",
9934
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
9935
- };
9936
- onMessage(message);
11151
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
11152
+ onMessage({ type: "leases-updated", timestamp });
11153
+ onMessage({ type: "agent-sessions-updated", timestamp });
9937
11154
  }, debounceMs)
9938
11155
  );
9939
11156
  };
@@ -11537,6 +12754,12 @@ tags: []
11537
12754
  `;
11538
12755
  }
11539
12756
 
12757
+ // src/templates/index.ts
12758
+ init_cursor_rules();
12759
+ init_codex_agents();
12760
+ init_opencode_config();
12761
+ init_hermes_soul();
12762
+
11540
12763
  // src/dashboard/api-write.ts
11541
12764
  init_lifecycle();
11542
12765
  init_parser();
@@ -14261,86 +15484,11 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir2) {
14261
15484
  // src/dashboard/api-agent-sessions.ts
14262
15485
  init_agent_sessions();
14263
15486
  init_fs();
15487
+ init_transcript();
14264
15488
  import { Router as Router4 } from "express";
14265
15489
  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
15490
  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
15491
+ init_process_info();
14344
15492
  init_git_worktree();
14345
15493
  init_cwd();
14346
15494
  function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
@@ -15234,13 +16382,20 @@ async function resolveSessionPlan(input, terminal) {
15234
16382
  env: process.env,
15235
16383
  agentId: agent.id,
15236
16384
  fallbackWarning,
15237
- shellFallbackWarning
16385
+ shellFallbackWarning,
16386
+ // Resume continues the SAME session id; fork mints a new one in-agent.
16387
+ session: { sessionId: (input.mode ?? "resume") === "resume" ? session.sessionId : null }
15238
16388
  };
15239
16389
  }
15240
16390
 
15241
16391
  // src/launch/execute.ts
15242
16392
  import { spawn as spawn3 } from "child_process";
15243
- import { basename as basename4 } from "path";
16393
+ import { homedir as homedir5 } from "os";
16394
+ import { basename as basename4, join as join5, resolve as resolve23 } from "path";
16395
+ init_fs();
16396
+ init_config2();
16397
+ init_session_id();
16398
+ init_process_info();
15244
16399
  var CMUX_BUNDLE_ID = "com.cmuxterm.app";
15245
16400
  var CMUX_READINESS_MAX_MS = 20 * 250;
15246
16401
  var CMUX_LAUNCH_TIMEOUT_MS = CMUX_READINESS_MAX_MS + 3e3;
@@ -15265,8 +16420,8 @@ function buildShellCommandLine(plan) {
15265
16420
  init_paths();
15266
16421
  init_fs();
15267
16422
  import { fileURLToPath } from "url";
15268
- import { dirname as dirname5, resolve as resolve23, join as join3 } from "path";
15269
- import { realpathSync, readFileSync, mkdirSync } from "fs";
16423
+ import { dirname as dirname6, resolve as resolve24, join as join6 } from "path";
16424
+ import { realpathSync, readFileSync as readFileSync2, mkdirSync as mkdirSync2 } from "fs";
15270
16425
 
15271
16426
  // src/dashboard/api-launch-preflight.ts
15272
16427
  init_assignment_resolver();
@@ -15543,32 +16698,32 @@ import { Router as Router9 } from "express";
15543
16698
  // src/utils/status-config-resolution.ts
15544
16699
  init_frontmatter();
15545
16700
  import { readFile as readFile18, writeFile as writeFile5, rm as rm2 } from "fs/promises";
15546
- import { dirname as dirname6 } from "path";
16701
+ import { dirname as dirname7 } from "path";
15547
16702
 
15548
16703
  // src/utils/assignment-walk.ts
15549
16704
  init_fs();
15550
- import { resolve as resolve24 } from "path";
15551
- import { readdir as readdir11 } from "fs/promises";
16705
+ import { resolve as resolve25 } from "path";
16706
+ import { readdir as readdir12 } from "fs/promises";
15552
16707
  async function listAssignmentsByProject(projectsDir, standaloneDir) {
15553
16708
  const result = {
15554
16709
  withAssignmentMd: [],
15555
16710
  orphanFolders: []
15556
16711
  };
15557
16712
  if (await fileExists(projectsDir)) {
15558
- const projects = await readdir11(projectsDir, { withFileTypes: true });
16713
+ const projects = await readdir12(projectsDir, { withFileTypes: true });
15559
16714
  for (const m of projects) {
15560
16715
  if (!m.isDirectory()) continue;
15561
16716
  if (m.name.startsWith(".") || m.name.startsWith("_")) continue;
15562
- const assignmentsDir2 = resolve24(projectsDir, m.name, "assignments");
16717
+ const assignmentsDir2 = resolve25(projectsDir, m.name, "assignments");
15563
16718
  if (!await fileExists(assignmentsDir2)) continue;
15564
- const entries = await readdir11(assignmentsDir2, { withFileTypes: true });
16719
+ const entries = await readdir12(assignmentsDir2, { withFileTypes: true });
15565
16720
  for (const a of entries) {
15566
16721
  if (!a.isDirectory()) continue;
15567
16722
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
15568
- const assignmentDir = resolve24(assignmentsDir2, a.name);
15569
- const assignmentMd = resolve24(assignmentDir, "assignment.md");
16723
+ const assignmentDir = resolve25(assignmentsDir2, a.name);
16724
+ const assignmentMd = resolve25(assignmentDir, "assignment.md");
15570
16725
  const entry = {
15571
- projectDir: resolve24(projectsDir, m.name),
16726
+ projectDir: resolve25(projectsDir, m.name),
15572
16727
  projectSlug: m.name,
15573
16728
  assignmentDir,
15574
16729
  assignmentSlug: a.name,
@@ -15583,12 +16738,12 @@ async function listAssignmentsByProject(projectsDir, standaloneDir) {
15583
16738
  }
15584
16739
  }
15585
16740
  if (standaloneDir !== null && await fileExists(standaloneDir)) {
15586
- const entries = await readdir11(standaloneDir, { withFileTypes: true });
16741
+ const entries = await readdir12(standaloneDir, { withFileTypes: true });
15587
16742
  for (const a of entries) {
15588
16743
  if (!a.isDirectory()) continue;
15589
16744
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
15590
- const assignmentDir = resolve24(standaloneDir, a.name);
15591
- const assignmentMd = resolve24(assignmentDir, "assignment.md");
16745
+ const assignmentDir = resolve25(standaloneDir, a.name);
16746
+ const assignmentMd = resolve25(assignmentDir, "assignment.md");
15592
16747
  const entry = {
15593
16748
  projectDir: standaloneDir,
15594
16749
  projectSlug: null,
@@ -15780,7 +16935,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
15780
16935
  } catch {
15781
16936
  continue;
15782
16937
  }
15783
- const assignmentDir = dirname6(a.path);
16938
+ const assignmentDir = dirname7(a.path);
15784
16939
  try {
15785
16940
  await rm2(assignmentDir, { recursive: true, force: true });
15786
16941
  deleted++;
@@ -16098,7 +17253,7 @@ import { Router as Router10 } from "express";
16098
17253
  init_paths();
16099
17254
  import Database2 from "better-sqlite3";
16100
17255
  import { randomUUID as randomUUID3 } from "crypto";
16101
- import { resolve as resolve25 } from "path";
17256
+ import { resolve as resolve26 } from "path";
16102
17257
  var db2 = null;
16103
17258
  var LEASE_SCHEMA_VERSION = "1";
16104
17259
  var SCHEMA_SQL2 = `
@@ -16185,7 +17340,7 @@ function isBusyError(err) {
16185
17340
  }
16186
17341
  function initLeasesDb(dbPath) {
16187
17342
  if (db2) return db2;
16188
- const finalPath = dbPath ?? resolve25(syntaurRoot(), "syntaur.db");
17343
+ const finalPath = dbPath ?? resolve26(syntaurRoot(), "syntaur.db");
16189
17344
  db2 = new Database2(finalPath);
16190
17345
  db2.pragma("journal_mode = WAL");
16191
17346
  db2.pragma("busy_timeout = 5000");
@@ -16334,7 +17489,7 @@ import { Router as Router11 } from "express";
16334
17489
  // src/db/usage-db.ts
16335
17490
  init_paths();
16336
17491
  import Database3 from "better-sqlite3";
16337
- import { resolve as resolve26 } from "path";
17492
+ import { resolve as resolve27 } from "path";
16338
17493
  var db3 = null;
16339
17494
  var USAGE_SCHEMA_VERSION = "1";
16340
17495
  var SCHEMA_SQL3 = `
@@ -16391,7 +17546,7 @@ CREATE INDEX IF NOT EXISTS idx_usage_daily_day
16391
17546
  `;
16392
17547
  function initUsageDb(dbPath) {
16393
17548
  if (db3) return db3;
16394
- const finalPath = dbPath ?? resolve26(syntaurRoot(), "syntaur.db");
17549
+ const finalPath = dbPath ?? resolve27(syntaurRoot(), "syntaur.db");
16395
17550
  db3 = new Database3(finalPath);
16396
17551
  db3.pragma("journal_mode = WAL");
16397
17552
  db3.pragma("busy_timeout = 5000");
@@ -16650,7 +17805,7 @@ init_slug();
16650
17805
  init_timestamp();
16651
17806
  init_fs();
16652
17807
  import { Router as Router12 } from "express";
16653
- import { resolve as resolve27 } from "path";
17808
+ import { resolve as resolve28 } from "path";
16654
17809
  import { readFile as readFile19 } from "fs/promises";
16655
17810
  init_playbooks();
16656
17811
  function statusForPlaybookError(code) {
@@ -16733,7 +17888,7 @@ function createPlaybooksRouter(playbooksDir2) {
16733
17888
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
16734
17889
  return;
16735
17890
  }
16736
- const filePath = resolve27(playbooksDir2, resolved.filename);
17891
+ const filePath = resolve28(playbooksDir2, resolved.filename);
16737
17892
  const content = await readFile19(filePath, "utf-8");
16738
17893
  res.json({
16739
17894
  documentType: "playbook",
@@ -16759,7 +17914,7 @@ function createPlaybooksRouter(playbooksDir2) {
16759
17914
  return;
16760
17915
  }
16761
17916
  await ensureDir(playbooksDir2);
16762
- const filePath = resolve27(playbooksDir2, `${slug}.md`);
17917
+ const filePath = resolve28(playbooksDir2, `${slug}.md`);
16763
17918
  if (await fileExists(filePath)) {
16764
17919
  res.status(409).json({ error: `Playbook "${slug}" already exists` });
16765
17920
  return;
@@ -16783,7 +17938,7 @@ function createPlaybooksRouter(playbooksDir2) {
16783
17938
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
16784
17939
  return;
16785
17940
  }
16786
- const filePath = resolve27(playbooksDir2, resolved.filename);
17941
+ const filePath = resolve28(playbooksDir2, resolved.filename);
16787
17942
  await writeFileForce(filePath, content);
16788
17943
  await rebuildPlaybookManifest(playbooksDir2);
16789
17944
  res.json({ slug: resolved.slug, path: filePath });
@@ -16831,8 +17986,8 @@ init_parser2();
16831
17986
  init_fs();
16832
17987
  init_paths();
16833
17988
  import { Router as Router14 } from "express";
16834
- import { readdir as readdir13 } from "fs/promises";
16835
- import { resolve as resolvePath, dirname as dirname8 } from "path";
17989
+ import { readdir as readdir14 } from "fs/promises";
17990
+ import { resolve as resolvePath, dirname as dirname9 } from "path";
16836
17991
  import { rename as rename6, mkdir as mkdir4 } from "fs/promises";
16837
17992
  init_slug();
16838
17993
 
@@ -16842,7 +17997,7 @@ init_parser2();
16842
17997
  // src/commands/create-assignment.ts
16843
17998
  init_slug();
16844
17999
  init_timestamp();
16845
- import { resolve as resolve28 } from "path";
18000
+ import { resolve as resolve29 } from "path";
16846
18001
  init_paths();
16847
18002
  init_fs();
16848
18003
  init_config2();
@@ -16920,14 +18075,14 @@ async function createAssignmentCommand(title, options) {
16920
18075
  if (options.oneOff) {
16921
18076
  const standaloneRoot = assignmentsDir();
16922
18077
  folderName = id;
16923
- assignmentDir = resolve28(standaloneRoot, folderName);
18078
+ assignmentDir = resolve29(standaloneRoot, folderName);
16924
18079
  projectSlug = null;
16925
18080
  await ensureDir(standaloneRoot);
16926
18081
  } else {
16927
18082
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
16928
18083
  projectSlug = options.project;
16929
- const projectDir = resolve28(baseDir, projectSlug);
16930
- const projectMdPath = resolve28(projectDir, "project.md");
18084
+ const projectDir = resolve29(baseDir, projectSlug);
18085
+ const projectMdPath = resolve29(projectDir, "project.md");
16931
18086
  if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
16932
18087
  throw new Error(
16933
18088
  `Project "${projectSlug}" not found at ${projectDir}.
@@ -16935,9 +18090,9 @@ Run 'syntaur create-project' first or use --one-off.`
16935
18090
  );
16936
18091
  }
16937
18092
  if (dependsOn.length > 0) {
16938
- const depDirBase = resolve28(projectDir, "assignments");
18093
+ const depDirBase = resolve29(projectDir, "assignments");
16939
18094
  for (const dep of dependsOn) {
16940
- const depDir = resolve28(depDirBase, dep);
18095
+ const depDir = resolve29(depDirBase, dep);
16941
18096
  if (!await fileExists(depDir)) {
16942
18097
  console.warn(
16943
18098
  `Warning: dependency "${dep}" does not exist in project "${projectSlug}" yet.`
@@ -16946,7 +18101,7 @@ Run 'syntaur create-project' first or use --one-off.`
16946
18101
  }
16947
18102
  }
16948
18103
  folderName = assignmentSlug;
16949
- assignmentDir = resolve28(projectDir, "assignments", folderName);
18104
+ assignmentDir = resolve29(projectDir, "assignments", folderName);
16950
18105
  }
16951
18106
  if (await fileExists(assignmentDir)) {
16952
18107
  throw new Error(
@@ -16958,7 +18113,7 @@ Use --slug to specify a different slug.`
16958
18113
  const companionAssignmentRef = projectSlug === null ? id : assignmentSlug;
16959
18114
  const files = [
16960
18115
  [
16961
- resolve28(assignmentDir, "assignment.md"),
18116
+ resolve29(assignmentDir, "assignment.md"),
16962
18117
  renderAssignment({
16963
18118
  id,
16964
18119
  slug: assignmentSlug,
@@ -16976,35 +18131,35 @@ Use --slug to specify a different slug.`
16976
18131
  })
16977
18132
  ],
16978
18133
  [
16979
- resolve28(assignmentDir, "scratchpad.md"),
18134
+ resolve29(assignmentDir, "scratchpad.md"),
16980
18135
  renderScratchpad({
16981
18136
  assignmentSlug: companionAssignmentRef,
16982
18137
  timestamp
16983
18138
  })
16984
18139
  ],
16985
18140
  [
16986
- resolve28(assignmentDir, "handoff.md"),
18141
+ resolve29(assignmentDir, "handoff.md"),
16987
18142
  renderHandoff({
16988
18143
  assignmentSlug: companionAssignmentRef,
16989
18144
  timestamp
16990
18145
  })
16991
18146
  ],
16992
18147
  [
16993
- resolve28(assignmentDir, "decision-record.md"),
18148
+ resolve29(assignmentDir, "decision-record.md"),
16994
18149
  renderDecisionRecord({
16995
18150
  assignmentSlug: companionAssignmentRef,
16996
18151
  timestamp
16997
18152
  })
16998
18153
  ],
16999
18154
  [
17000
- resolve28(assignmentDir, "progress.md"),
18155
+ resolve29(assignmentDir, "progress.md"),
17001
18156
  renderProgress({
17002
18157
  assignment: companionAssignmentRef,
17003
18158
  timestamp
17004
18159
  })
17005
18160
  ],
17006
18161
  [
17007
- resolve28(assignmentDir, "comments.md"),
18162
+ resolve29(assignmentDir, "comments.md"),
17008
18163
  renderComments({
17009
18164
  assignment: companionAssignmentRef,
17010
18165
  timestamp
@@ -17177,8 +18332,8 @@ init_api();
17177
18332
  import { raw } from "express";
17178
18333
 
17179
18334
  // 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";
18335
+ import { mkdir as mkdir3, readdir as readdir13, stat as stat3, rename as rename5, rm as rm3, unlink as unlink6, writeFile as writeFile6, cp } from "fs/promises";
18336
+ import { resolve as resolve30, basename as basename5, dirname as dirname8, extname } from "path";
17182
18337
 
17183
18338
  // src/utils/proof-artifact-id.ts
17184
18339
  import { randomBytes as randomBytes2 } from "crypto";
@@ -17265,16 +18420,16 @@ function sanitizeAttachmentName(name) {
17265
18420
  return n;
17266
18421
  }
17267
18422
  function attachmentsRootDir(todosDir2) {
17268
- return resolve29(todosDir2, "attachments");
18423
+ return resolve30(todosDir2, "attachments");
17269
18424
  }
17270
18425
  function attachmentDirFor(todosDir2, scopeId, todoId) {
17271
18426
  assertScope(scopeId);
17272
18427
  assertTodoId(todoId);
17273
- return resolve29(attachmentsRootDir(todosDir2), scopeId, todoId);
18428
+ return resolve30(attachmentsRootDir(todosDir2), scopeId, todoId);
17274
18429
  }
17275
18430
  async function dirExists(p) {
17276
18431
  try {
17277
- return (await stat2(p)).isDirectory();
18432
+ return (await stat3(p)).isDirectory();
17278
18433
  } catch {
17279
18434
  return false;
17280
18435
  }
@@ -17284,7 +18439,7 @@ async function writeAttachment(todosDir2, scopeId, todoId, originalName, bytes)
17284
18439
  await mkdir3(dir, { recursive: true });
17285
18440
  const id = generateArtifactId();
17286
18441
  const filename = sanitizeAttachmentName(originalName);
17287
- await writeFile6(resolve29(dir, `${id}__${filename}`), bytes);
18442
+ await writeFile6(resolve30(dir, `${id}__${filename}`), bytes);
17288
18443
  return {
17289
18444
  id,
17290
18445
  filename,
@@ -17297,7 +18452,7 @@ async function listAttachments(todosDir2, scopeId, todoId) {
17297
18452
  const dir = attachmentDirFor(todosDir2, scopeId, todoId);
17298
18453
  let names;
17299
18454
  try {
17300
- names = await readdir12(dir);
18455
+ names = await readdir13(dir);
17301
18456
  } catch {
17302
18457
  return [];
17303
18458
  }
@@ -17309,7 +18464,7 @@ async function listAttachments(todosDir2, scopeId, todoId) {
17309
18464
  if (!ATTACHMENT_ID_RE.test(id)) continue;
17310
18465
  const filename = stored.slice(sep2 + 2);
17311
18466
  try {
17312
- const st = await stat2(resolve29(dir, stored));
18467
+ const st = await stat3(resolve30(dir, stored));
17313
18468
  if (!st.isFile()) continue;
17314
18469
  out.push({ id, filename, mime: mimeForName(filename), size: st.size, createdAt: st.mtime.toISOString() });
17315
18470
  } catch {
@@ -17320,10 +18475,10 @@ async function listAttachments(todosDir2, scopeId, todoId) {
17320
18475
  }
17321
18476
  async function readScopeAttachments(todosDir2, scopeId) {
17322
18477
  assertScope(scopeId);
17323
- const scopeDir = resolve29(attachmentsRootDir(todosDir2), scopeId);
18478
+ const scopeDir = resolve30(attachmentsRootDir(todosDir2), scopeId);
17324
18479
  let todoIds;
17325
18480
  try {
17326
- todoIds = await readdir12(scopeDir);
18481
+ todoIds = await readdir13(scopeDir);
17327
18482
  } catch {
17328
18483
  return {};
17329
18484
  }
@@ -17340,7 +18495,7 @@ async function resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId) {
17340
18495
  const dir = attachmentDirFor(todosDir2, scopeId, todoId);
17341
18496
  let names;
17342
18497
  try {
17343
- names = await readdir12(dir);
18498
+ names = await readdir13(dir);
17344
18499
  } catch {
17345
18500
  return null;
17346
18501
  }
@@ -17348,7 +18503,7 @@ async function resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId) {
17348
18503
  const stored = names.find((n) => n.startsWith(prefix));
17349
18504
  if (!stored) return null;
17350
18505
  const filename = stored.slice(prefix.length);
17351
- return { path: resolve29(dir, stored), filename, mime: mimeForName(filename) };
18506
+ return { path: resolve30(dir, stored), filename, mime: mimeForName(filename) };
17352
18507
  }
17353
18508
  async function deleteAttachment(todosDir2, scopeId, todoId, attachmentId) {
17354
18509
  const resolved = await resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId);
@@ -17368,7 +18523,7 @@ async function moveAttachments(srcTodosDir, srcScopeId, dstTodosDir, dstScopeId,
17368
18523
  const src = attachmentDirFor(srcTodosDir, srcScopeId, todoId);
17369
18524
  if (!await dirExists(src)) return;
17370
18525
  const dst = attachmentDirFor(dstTodosDir, dstScopeId, todoId);
17371
- await mkdir3(dirname7(dst), { recursive: true });
18526
+ await mkdir3(dirname8(dst), { recursive: true });
17372
18527
  try {
17373
18528
  await rename5(src, dst);
17374
18529
  } catch (err) {
@@ -17644,7 +18799,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
17644
18799
  router.get("/", async (_req, res) => {
17645
18800
  try {
17646
18801
  await ensureDir(todosDir2);
17647
- const files = await readdir13(todosDir2).catch(() => []);
18802
+ const files = await readdir14(todosDir2).catch(() => []);
17648
18803
  const workspaces = [];
17649
18804
  for (const file of files) {
17650
18805
  if (typeof file !== "string") continue;
@@ -17760,8 +18915,8 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
17760
18915
  router.post("/:workspace/archive", async (req, res) => {
17761
18916
  try {
17762
18917
  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");
18918
+ const { resolve: resolve38 } = await import("path");
18919
+ const { readFile: readFile25 } = await import("fs/promises");
17765
18920
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
17766
18921
  const workspace = getWorkspaceParam(req.params.workspace);
17767
18922
  const outcome = await wsLock(workspace, async () => {
@@ -17777,10 +18932,10 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
17777
18932
  (e) => e.itemIds.every((id) => completedIds.has(id))
17778
18933
  );
17779
18934
  const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
17780
- await ensureDir(resolve34(todosDir2, "archive"));
18935
+ await ensureDir(resolve38(todosDir2, "archive"));
17781
18936
  let archContent = "";
17782
18937
  if (await fileExists(archFile)) {
17783
- archContent = await readFile23(archFile, "utf-8");
18938
+ archContent = await readFile25(archFile, "utf-8");
17784
18939
  archContent = archContent.trimEnd() + "\n\n";
17785
18940
  } else {
17786
18941
  archContent = `---
@@ -18069,7 +19224,7 @@ workspace: ${workspace}
18069
19224
  const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
18070
19225
  const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
18071
19226
  const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
18072
- const { readFile: readFile23 } = await import("fs/promises");
19227
+ const { readFile: readFile25 } = await import("fs/promises");
18073
19228
  const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
18074
19229
  const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
18075
19230
  let assignmentRef;
@@ -18090,7 +19245,7 @@ workspace: ${workspace}
18090
19245
  }
18091
19246
  const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
18092
19247
  if (!await fileExists2(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
18093
- let content = await readFile23(assignmentMdPath, "utf-8");
19248
+ let content = await readFile25(assignmentMdPath, "utf-8");
18094
19249
  content = appendTodosToAssignmentBody2(
18095
19250
  content,
18096
19251
  items.map((it) => ({
@@ -18207,7 +19362,7 @@ workspace: ${workspace}
18207
19362
  return { status: 409, error: "attachments already exist in target" };
18208
19363
  }
18209
19364
  if (item.planDir && newPlanDir) {
18210
- await mkdir4(dirname8(newPlanDir), { recursive: true });
19365
+ await mkdir4(dirname9(newPlanDir), { recursive: true });
18211
19366
  await rename6(item.planDir, newPlanDir);
18212
19367
  item.planDir = newPlanDir;
18213
19368
  }
@@ -18286,7 +19441,7 @@ init_paths();
18286
19441
  init_slug();
18287
19442
  import { Router as Router15 } from "express";
18288
19443
  import { mkdir as mkdir5, readFile as readFile20, rename as rename7 } from "fs/promises";
18289
- import { resolve as resolve30, dirname as dirname9 } from "path";
19444
+ import { resolve as resolve31, dirname as dirname10 } from "path";
18290
19445
  init_api();
18291
19446
  var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
18292
19447
  function touchItem4(item) {
@@ -18302,7 +19457,7 @@ function params(req) {
18302
19457
  return req.params;
18303
19458
  }
18304
19459
  async function projectExists(projectsDir, slug) {
18305
- return fileExists(resolve30(projectsDir, slug, "project.md"));
19460
+ return fileExists(resolve31(projectsDir, slug, "project.md"));
18306
19461
  }
18307
19462
  async function ensureProjectTodosDir(projectsDir, slug) {
18308
19463
  const todosDir2 = projectTodosDir(projectsDir, slug);
@@ -18319,7 +19474,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
18319
19474
  throw err;
18320
19475
  }
18321
19476
  try {
18322
- await mkdir5(resolve30(todosDir2, "archive"), { recursive: false });
19477
+ await mkdir5(resolve31(todosDir2, "archive"), { recursive: false });
18323
19478
  } catch (err) {
18324
19479
  const code = err.code;
18325
19480
  if (code === "EEXIST") return;
@@ -18982,15 +20137,15 @@ workspace: ${slug}
18982
20137
  if (tg.includes("/")) {
18983
20138
  const parts = tg.split("/");
18984
20139
  if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
18985
- assignmentDir = resolve30(projectsDir, parts[0], "assignments", parts[1]);
20140
+ assignmentDir = resolve31(projectsDir, parts[0], "assignments", parts[1]);
18986
20141
  assignmentRef = `${parts[0]}/${parts[1]}`;
18987
20142
  } else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tg)) {
18988
- assignmentDir = resolve30(assignmentsDirFn(), tg);
20143
+ assignmentDir = resolve31(assignmentsDirFn(), tg);
18989
20144
  assignmentRef = tg;
18990
20145
  } else {
18991
20146
  return { error: `Invalid target.assignment "${tg}"` };
18992
20147
  }
18993
- const assignmentMdPath = resolve30(assignmentDir, "assignment.md");
20148
+ const assignmentMdPath = resolve31(assignmentDir, "assignment.md");
18994
20149
  if (!await fileExists(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
18995
20150
  let content = await readFile20(assignmentMdPath, "utf-8");
18996
20151
  content = appendTodosToAssignmentBody2(
@@ -19131,7 +20286,7 @@ workspace: ${slug}
19131
20286
  return { status: 409, error: "attachments already exist in target" };
19132
20287
  }
19133
20288
  if (item.planDir && newPlanDir) {
19134
- await mkdir5(dirname9(newPlanDir), { recursive: true });
20289
+ await mkdir5(dirname10(newPlanDir), { recursive: true });
19135
20290
  await rename7(item.planDir, newPlanDir);
19136
20291
  item.planDir = newPlanDir;
19137
20292
  }
@@ -19195,7 +20350,7 @@ workspace: ${slug}
19195
20350
 
19196
20351
  // src/dashboard/api-bundles.ts
19197
20352
  import { Router as Router16 } from "express";
19198
- import { readdir as readdir14 } from "fs/promises";
20353
+ import { readdir as readdir15 } from "fs/promises";
19199
20354
 
19200
20355
  // src/todos/bundle-parser.ts
19201
20356
  init_parser();
@@ -19319,7 +20474,7 @@ function createBundlesRouter(todosDir2, broadcast) {
19319
20474
  try {
19320
20475
  await ensureDir(todosDir2);
19321
20476
  const bundles = await readBundles(todosDir2);
19322
- const workspaceFiles = await readdir14(todosDir2).catch(() => []);
20477
+ const workspaceFiles = await readdir15(todosDir2).catch(() => []);
19323
20478
  const itemsByKey = /* @__PURE__ */ new Map();
19324
20479
  for (const f of workspaceFiles) {
19325
20480
  if (typeof f !== "string") continue;
@@ -19372,7 +20527,7 @@ init_fs();
19372
20527
  init_paths();
19373
20528
  init_slug();
19374
20529
  import { Router as Router17 } from "express";
19375
- import { resolve as resolve31 } from "path";
20530
+ import { resolve as resolve32 } from "path";
19376
20531
  init_parser2();
19377
20532
  function deriveStatus2(bundle, items) {
19378
20533
  const members = bundle.todoIds.map((id) => items.find((i) => i.id === id)).filter((i) => i !== void 0);
@@ -19414,7 +20569,7 @@ function createProjectBundlesRouter(projectsDir, broadcast) {
19414
20569
  router.get("/", async (req, res) => {
19415
20570
  try {
19416
20571
  const slug = getProjectIdParam2(req.params.projectId);
19417
- const projectMd = resolve31(projectsDir, slug, "project.md");
20572
+ const projectMd = resolve32(projectsDir, slug, "project.md");
19418
20573
  if (!await fileExists(projectMd)) {
19419
20574
  notFound2(res, slug);
19420
20575
  return;
@@ -19443,8 +20598,8 @@ init_fs();
19443
20598
  init_config2();
19444
20599
  import { execFile as execFile2 } from "child_process";
19445
20600
  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";
20601
+ import { cp as cp2, mkdtemp, rm as rm4, readFile as readFile22, writeFile as writeFile7, unlink as unlink7, stat as stat4, open as open4, rename as rename8 } from "fs/promises";
20602
+ import { resolve as resolve33, join as join7 } from "path";
19448
20603
  import { tmpdir } from "os";
19449
20604
  var exec2 = promisify2(execFile2);
19450
20605
  var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
@@ -19484,7 +20639,7 @@ async function resolveCategoryPath(category) {
19484
20639
  case "servers":
19485
20640
  return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
19486
20641
  case "config":
19487
- return { sourcePath: resolve32(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
20642
+ return { sourcePath: resolve33(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
19488
20643
  }
19489
20644
  }
19490
20645
  async function checkGitInstalled() {
@@ -19495,10 +20650,10 @@ async function checkGitInstalled() {
19495
20650
  }
19496
20651
  }
19497
20652
  async function acquireLock2() {
19498
- const lockPath = resolve32(syntaurRoot(), LOCK_FILE_NAME);
20653
+ const lockPath = resolve33(syntaurRoot(), LOCK_FILE_NAME);
19499
20654
  await ensureDir(syntaurRoot());
19500
20655
  try {
19501
- const handle = await open3(lockPath, "wx");
20656
+ const handle = await open4(lockPath, "wx");
19502
20657
  await handle.write(String(process.pid));
19503
20658
  await handle.close();
19504
20659
  return lockPath;
@@ -19537,12 +20692,12 @@ async function cloneOrInit(repoUrl, destDir) {
19537
20692
  }
19538
20693
  async function copyRecursive(src, dest) {
19539
20694
  if (!await fileExists(src)) return;
19540
- const s = await stat3(src);
20695
+ const s = await stat4(src);
19541
20696
  if (s.isDirectory()) {
19542
20697
  await ensureDir(dest);
19543
20698
  await cp2(src, dest, { recursive: true, force: true });
19544
20699
  } else {
19545
- await ensureDir(resolve32(dest, ".."));
20700
+ await ensureDir(resolve33(dest, ".."));
19546
20701
  await cp2(src, dest, { force: true });
19547
20702
  }
19548
20703
  }
@@ -19574,11 +20729,11 @@ async function backupToGithub(overrides) {
19574
20729
  let tmpDir = null;
19575
20730
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
19576
20731
  try {
19577
- tmpDir = await mkdtemp(join4(tmpdir(), "syntaur-backup-"));
20732
+ tmpDir = await mkdtemp(join7(tmpdir(), "syntaur-backup-"));
19578
20733
  await cloneOrInit(repo, tmpDir);
19579
20734
  for (const category of categories) {
19580
20735
  const { sourcePath, repoPath, isFile } = await resolveCategoryPath(category);
19581
- const destPath = join4(tmpDir, repoPath);
20736
+ const destPath = join7(tmpDir, repoPath);
19582
20737
  if (isFile) {
19583
20738
  await rm4(destPath, { force: true });
19584
20739
  } else {
@@ -19590,7 +20745,7 @@ async function backupToGithub(overrides) {
19590
20745
  }
19591
20746
  if (category === "config") {
19592
20747
  const sanitized = await readSanitizedConfig(sourcePath);
19593
- await ensureDir(resolve32(destPath, ".."));
20748
+ await ensureDir(resolve33(destPath, ".."));
19594
20749
  await writeFile7(destPath, sanitized, "utf-8");
19595
20750
  } else {
19596
20751
  await copyRecursive(sourcePath, destPath);
@@ -19644,7 +20799,7 @@ async function backupToGithub(overrides) {
19644
20799
  }
19645
20800
  async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
19646
20801
  if (isFile) {
19647
- await ensureDir(resolve32(localPath, ".."));
20802
+ await ensureDir(resolve33(localPath, ".."));
19648
20803
  await cp2(repoSrcPath, localPath, { force: true });
19649
20804
  return;
19650
20805
  }
@@ -19706,7 +20861,7 @@ async function restoreFromGithub(overrides) {
19706
20861
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
19707
20862
  try {
19708
20863
  await updateBackupConfig({ lastRestore: timestamp });
19709
- tmpDir = await mkdtemp(join4(tmpdir(), "syntaur-restore-"));
20864
+ tmpDir = await mkdtemp(join7(tmpdir(), "syntaur-restore-"));
19710
20865
  await cloneOrInit(repo, tmpDir);
19711
20866
  for (const category of categories) {
19712
20867
  if (category === "config") {
@@ -19715,7 +20870,7 @@ async function restoreFromGithub(overrides) {
19715
20870
  }
19716
20871
  try {
19717
20872
  const { sourcePath: localPath, repoPath, isFile } = await resolveCategoryPath(category);
19718
- const repoSrcPath = join4(tmpDir, repoPath);
20873
+ const repoSrcPath = join7(tmpDir, repoPath);
19719
20874
  if (!await fileExists(repoSrcPath)) {
19720
20875
  console.warn(`Category "${category}" not found in backup repo, skipping.`);
19721
20876
  continue;
@@ -19745,7 +20900,7 @@ async function restoreFromGithub(overrides) {
19745
20900
  }
19746
20901
  async function getBackupStatus() {
19747
20902
  const config = await readConfig();
19748
- const lockPath = resolve32(syntaurRoot(), LOCK_FILE_NAME);
20903
+ const lockPath = resolve33(syntaurRoot(), LOCK_FILE_NAME);
19749
20904
  const locked = await fileExists(lockPath);
19750
20905
  return {
19751
20906
  repo: config.backup?.repo ?? null,
@@ -19906,7 +21061,7 @@ async function stopAutodiscovery() {
19906
21061
  function runReconcile() {
19907
21062
  if (activeReconcile || !savedOptions) return;
19908
21063
  const opts = savedOptions;
19909
- activeReconcile = reconcile(opts.serversDir, opts.projectsDir, opts.excludePids, opts.assignmentsDir).catch((err) => {
21064
+ activeReconcile = reconcile(opts.serversDir, opts.projectsDir, opts.excludePids, opts.assignmentsDir, opts.onAgentSessionsChanged).catch((err) => {
19910
21065
  console.error("[autodiscovery] reconcile failed:", err);
19911
21066
  }).finally(() => {
19912
21067
  activeReconcile = null;
@@ -20027,7 +21182,7 @@ async function isProcessAlive(pid) {
20027
21182
  return false;
20028
21183
  }
20029
21184
  }
20030
- async function reconcile(serversDir2, projectsDir, excludePids, assignmentsDir2) {
21185
+ async function reconcile(serversDir2, projectsDir, excludePids, assignmentsDir2, onAgentSessionsChanged) {
20031
21186
  const names = await listSessionFiles(serversDir2);
20032
21187
  const existingFiles = /* @__PURE__ */ new Map();
20033
21188
  for (const name of names) {
@@ -20044,6 +21199,16 @@ async function reconcile(serversDir2, projectsDir, excludePids, assignmentsDir2)
20044
21199
  if (tmuxChanged || processChanged || cleanupChanged) {
20045
21200
  clearScanCache();
20046
21201
  }
21202
+ const { isSessionDbInitialized: isSessionDbInitialized2 } = await Promise.resolve().then(() => (init_session_db(), session_db_exports));
21203
+ if (isSessionDbInitialized2()) {
21204
+ try {
21205
+ const { scanSessions: scanSessions2 } = await Promise.resolve().then(() => (init_scanner2(), scanner_exports2));
21206
+ const summary = await scanSessions2({});
21207
+ if (summary.changed) onAgentSessionsChanged?.();
21208
+ } catch (err) {
21209
+ console.error("[autodiscovery] session scan failed:", err);
21210
+ }
21211
+ }
20047
21212
  }
20048
21213
 
20049
21214
  // src/dashboard/server.ts
@@ -20093,7 +21258,7 @@ function createDashboardServer(options) {
20093
21258
  (async () => {
20094
21259
  try {
20095
21260
  const configResult = await migrateLegacyConfig(
20096
- resolve33(syntaurRoot(), "config.md")
21261
+ resolve37(syntaurRoot(), "config.md")
20097
21262
  );
20098
21263
  const projectResult = await migrateLegacyProjectFiles(projectsDir);
20099
21264
  const summary = summarizeMigration(projectResult, configResult);
@@ -20611,14 +21776,14 @@ function createDashboardServer(options) {
20611
21776
  app.use("/api/backup", createBackupRouter());
20612
21777
  if (serveStaticUi && dashboardDistPath) {
20613
21778
  const sendOpts = { dotfiles: "allow" };
20614
- app.use("/assets", express.static(resolve33(dashboardDistPath, "assets"), sendOpts));
21779
+ app.use("/assets", express.static(resolve37(dashboardDistPath, "assets"), sendOpts));
20615
21780
  app.use(express.static(dashboardDistPath, { ...sendOpts, index: false, fallthrough: true }));
20616
21781
  app.get("{*path}", async (req, res) => {
20617
21782
  if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
20618
21783
  res.status(404).json({ error: "Not Found" });
20619
21784
  return;
20620
21785
  }
20621
- const indexPath = resolve33(dashboardDistPath, "index.html");
21786
+ const indexPath = resolve37(dashboardDistPath, "index.html");
20622
21787
  if (!await fileExists(indexPath)) {
20623
21788
  res.status(503).send(
20624
21789
  'Dashboard not built. Run "npm run build:dashboard" first.'
@@ -20652,8 +21817,8 @@ function createDashboardServer(options) {
20652
21817
  if (!await migrationGate()) return;
20653
21818
  try {
20654
21819
  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");
21820
+ const projectDir = projectSlug ? resolve37(projectsDir, projectSlug) : null;
21821
+ const path = projectDir ? resolve37(projectDir, "assignments", assignmentSlug, "assignment.md") : resolve37(assignmentsDir2, assignmentSlug, "assignment.md");
20657
21822
  if (!await fileExists(path)) return;
20658
21823
  const result = await recomputeAndWrite2(path, {
20659
21824
  cause: "derive",
@@ -20689,8 +21854,8 @@ function createDashboardServer(options) {
20689
21854
  serversDir: serversDir2,
20690
21855
  playbooksDir: playbooksDir2,
20691
21856
  todosDir: todosDir2,
20692
- dbPath: resolve33(syntaurRoot(), "syntaur.db"),
20693
- configPath: resolve33(syntaurRoot(), "config.md"),
21857
+ dbPath: resolve37(syntaurRoot(), "syntaur.db"),
21858
+ configPath: resolve37(syntaurRoot(), "config.md"),
20694
21859
  onMessage: broadcast,
20695
21860
  onAssignmentChanged: (projectSlug, assignmentSlug) => {
20696
21861
  void recomputeOne(projectSlug, assignmentSlug);
@@ -20701,7 +21866,16 @@ function createDashboardServer(options) {
20701
21866
  }
20702
21867
  });
20703
21868
  void sweepAll("boot-reconcile");
20704
- startAutodiscovery({ serversDir: serversDir2, projectsDir, assignmentsDir: assignmentsDir2, excludePids: /* @__PURE__ */ new Set([process.pid]) });
21869
+ startAutodiscovery({
21870
+ serversDir: serversDir2,
21871
+ projectsDir,
21872
+ assignmentsDir: assignmentsDir2,
21873
+ excludePids: /* @__PURE__ */ new Set([process.pid]),
21874
+ // Same WS frame the REST mutations emit, so the UI refreshes when the
21875
+ // session scan inserts/revives/sweeps rows. Autodiscovery's immediate
21876
+ // first run covers "scan at dashboard start".
21877
+ onAgentSessionsChanged: () => broadcast({ type: "agent-sessions-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() })
21878
+ });
20705
21879
  return new Promise((resolvePromise, reject) => {
20706
21880
  server.on("error", (err) => {
20707
21881
  if (err.code === "EADDRINUSE") {
@@ -20713,7 +21887,7 @@ function createDashboardServer(options) {
20713
21887
  }
20714
21888
  });
20715
21889
  server.listen(port, () => {
20716
- const portFile = resolve33(syntaurRoot(), "dashboard-port");
21890
+ const portFile = resolve37(syntaurRoot(), "dashboard-port");
20717
21891
  writeFile8(portFile, String(port), "utf-8").catch(() => {
20718
21892
  });
20719
21893
  resolvePromise();
@@ -20732,7 +21906,7 @@ function createDashboardServer(options) {
20732
21906
  client.terminate();
20733
21907
  }
20734
21908
  clients.clear();
20735
- const portFile = resolve33(syntaurRoot(), "dashboard-port");
21909
+ const portFile = resolve37(syntaurRoot(), "dashboard-port");
20736
21910
  await unlink8(portFile).catch(() => {
20737
21911
  });
20738
21912
  server.closeAllConnections?.();