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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/dashboard/server.js +1383 -209
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +2056 -1479
- package/dist/index.js.map +1 -1
- package/dist/launch/index.d.ts +19 -0
- package/dist/launch/index.js +526 -16
- package/dist/launch/index.js.map +1 -1
- package/package.json +1 -1
- package/platforms/SESSION-ID-RESOLUTION.md +41 -4
- package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
- package/platforms/claude-code/hooks/session-cleanup.sh +25 -64
- package/platforms/claude-code/hooks/session-start.sh +35 -109
- package/platforms/claude-code/skills/track-session/SKILL.md +12 -60
- package/platforms/codex/.codex-plugin/plugin.json +1 -1
- package/platforms/codex/skills/track-session/SKILL.md +12 -60
- package/platforms/hermes/plugins/syntaur/__pycache__/__init__.cpython-312.pyc +0 -0
- package/platforms/hermes/plugins/syntaur/__pycache__/boundary.cpython-312.pyc +0 -0
- package/skills/track-session/SKILL.md +12 -60
package/dist/dashboard/server.js
CHANGED
|
@@ -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:
|
|
5952
|
-
const raw2 = await
|
|
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
|
|
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((
|
|
6389
|
-
const timer2 = setTimeout(
|
|
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
|
|
9563
|
-
var
|
|
9564
|
-
"src/
|
|
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
|
-
|
|
9570
|
-
|
|
9571
|
-
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
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
|
|
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
|
|
9933
|
-
|
|
9934
|
-
|
|
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 {
|
|
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
|
|
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
|
|
16701
|
+
import { dirname as dirname7 } from "path";
|
|
15547
16702
|
|
|
15548
16703
|
// src/utils/assignment-walk.ts
|
|
15549
16704
|
init_fs();
|
|
15550
|
-
import { resolve as
|
|
15551
|
-
import { readdir as
|
|
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
|
|
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 =
|
|
16717
|
+
const assignmentsDir2 = resolve25(projectsDir, m.name, "assignments");
|
|
15563
16718
|
if (!await fileExists(assignmentsDir2)) continue;
|
|
15564
|
-
const entries = await
|
|
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 =
|
|
15569
|
-
const assignmentMd =
|
|
16723
|
+
const assignmentDir = resolve25(assignmentsDir2, a.name);
|
|
16724
|
+
const assignmentMd = resolve25(assignmentDir, "assignment.md");
|
|
15570
16725
|
const entry = {
|
|
15571
|
-
projectDir:
|
|
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
|
|
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 =
|
|
15591
|
-
const assignmentMd =
|
|
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 =
|
|
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
|
|
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 ??
|
|
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
|
|
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 ??
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
16835
|
-
import { resolve as resolvePath, dirname as
|
|
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
|
|
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 =
|
|
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 =
|
|
16930
|
-
const projectMdPath =
|
|
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 =
|
|
18093
|
+
const depDirBase = resolve29(projectDir, "assignments");
|
|
16939
18094
|
for (const dep of dependsOn) {
|
|
16940
|
-
const depDir =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
18134
|
+
resolve29(assignmentDir, "scratchpad.md"),
|
|
16980
18135
|
renderScratchpad({
|
|
16981
18136
|
assignmentSlug: companionAssignmentRef,
|
|
16982
18137
|
timestamp
|
|
16983
18138
|
})
|
|
16984
18139
|
],
|
|
16985
18140
|
[
|
|
16986
|
-
|
|
18141
|
+
resolve29(assignmentDir, "handoff.md"),
|
|
16987
18142
|
renderHandoff({
|
|
16988
18143
|
assignmentSlug: companionAssignmentRef,
|
|
16989
18144
|
timestamp
|
|
16990
18145
|
})
|
|
16991
18146
|
],
|
|
16992
18147
|
[
|
|
16993
|
-
|
|
18148
|
+
resolve29(assignmentDir, "decision-record.md"),
|
|
16994
18149
|
renderDecisionRecord({
|
|
16995
18150
|
assignmentSlug: companionAssignmentRef,
|
|
16996
18151
|
timestamp
|
|
16997
18152
|
})
|
|
16998
18153
|
],
|
|
16999
18154
|
[
|
|
17000
|
-
|
|
18155
|
+
resolve29(assignmentDir, "progress.md"),
|
|
17001
18156
|
renderProgress({
|
|
17002
18157
|
assignment: companionAssignmentRef,
|
|
17003
18158
|
timestamp
|
|
17004
18159
|
})
|
|
17005
18160
|
],
|
|
17006
18161
|
[
|
|
17007
|
-
|
|
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
|
|
17181
|
-
import { resolve as
|
|
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
|
|
18423
|
+
return resolve30(todosDir2, "attachments");
|
|
17269
18424
|
}
|
|
17270
18425
|
function attachmentDirFor(todosDir2, scopeId, todoId) {
|
|
17271
18426
|
assertScope(scopeId);
|
|
17272
18427
|
assertTodoId(todoId);
|
|
17273
|
-
return
|
|
18428
|
+
return resolve30(attachmentsRootDir(todosDir2), scopeId, todoId);
|
|
17274
18429
|
}
|
|
17275
18430
|
async function dirExists(p) {
|
|
17276
18431
|
try {
|
|
17277
|
-
return (await
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
18478
|
+
const scopeDir = resolve30(attachmentsRootDir(todosDir2), scopeId);
|
|
17324
18479
|
let todoIds;
|
|
17325
18480
|
try {
|
|
17326
|
-
todoIds = await
|
|
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
|
|
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:
|
|
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(
|
|
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
|
|
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:
|
|
17764
|
-
const { readFile:
|
|
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(
|
|
18935
|
+
await ensureDir(resolve38(todosDir2, "archive"));
|
|
17781
18936
|
let archContent = "";
|
|
17782
18937
|
if (await fileExists(archFile)) {
|
|
17783
|
-
archContent = await
|
|
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:
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
19447
|
-
import { resolve as
|
|
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:
|
|
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 =
|
|
20653
|
+
const lockPath = resolve33(syntaurRoot(), LOCK_FILE_NAME);
|
|
19499
20654
|
await ensureDir(syntaurRoot());
|
|
19500
20655
|
try {
|
|
19501
|
-
const handle = await
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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 ?
|
|
20656
|
-
const path = projectDir ?
|
|
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:
|
|
20693
|
-
configPath:
|
|
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({
|
|
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 =
|
|
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 =
|
|
21909
|
+
const portFile = resolve37(syntaurRoot(), "dashboard-port");
|
|
20736
21910
|
await unlink8(portFile).catch(() => {
|
|
20737
21911
|
});
|
|
20738
21912
|
server.closeAllConnections?.();
|