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