syntaur 0.69.0 → 0.71.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-DKk6tHtk.js → _basePickBy-BfpzwNC3.js} +1 -1
- package/dashboard/dist/assets/{_baseUniq-DM-f7DWz.js → _baseUniq-wFEHBTCT.js} +1 -1
- package/dashboard/dist/assets/{arc-ZBlA3YdV.js → arc-Bdcw4sCg.js} +1 -1
- package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-BUmvtGTF.js → architectureDiagram-2XIMDMQ5-BessFAtJ.js} +1 -1
- package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-B3qxWK6s.js → blockDiagram-WCTKOSBZ-CmjyyZPp.js} +1 -1
- package/dashboard/dist/assets/{c4Diagram-IC4MRINW-BEq_UJO-.js → c4Diagram-IC4MRINW-dT6p8cwc.js} +1 -1
- package/dashboard/dist/assets/channel-BOguQ5S4.js +1 -0
- package/dashboard/dist/assets/{chunk-4BX2VUAB-C-Y9ryMm.js → chunk-4BX2VUAB-BlY-k8DY.js} +1 -1
- package/dashboard/dist/assets/{chunk-55IACEB6-CGdtbsjw.js → chunk-55IACEB6-CtQ0p0jb.js} +1 -1
- package/dashboard/dist/assets/{chunk-FMBD7UC4-DllxJhUp.js → chunk-FMBD7UC4-DEevt_Yh.js} +1 -1
- package/dashboard/dist/assets/{chunk-JSJVCQXG-jjMM8O5F.js → chunk-JSJVCQXG-CpjLL7Nh.js} +1 -1
- package/dashboard/dist/assets/{chunk-KX2RTZJC-B_6BPltQ.js → chunk-KX2RTZJC-BWNRms_o.js} +1 -1
- package/dashboard/dist/assets/{chunk-NQ4KR5QH-D0hJiXHp.js → chunk-NQ4KR5QH-qo2zbylv.js} +1 -1
- package/dashboard/dist/assets/{chunk-QZHKN3VN-BCWo4hLS.js → chunk-QZHKN3VN-DRvojtzc.js} +1 -1
- package/dashboard/dist/assets/{chunk-WL4C6EOR-DH_jEAwg.js → chunk-WL4C6EOR-A40iPU5N.js} +1 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-BLSHooma.js +1 -0
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BLSHooma.js +1 -0
- package/dashboard/dist/assets/clone-iSEXw5tA.js +1 -0
- package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-5ld00TOH.js → cose-bilkent-S5V4N54A-CYaO7nsk.js} +1 -1
- package/dashboard/dist/assets/{dagre-KLK3FWXG-Cnu6eQWy.js → dagre-KLK3FWXG-NX3inwVZ.js} +1 -1
- package/dashboard/dist/assets/{diagram-E7M64L7V-_CBBKNP-.js → diagram-E7M64L7V-D9cJwhQW.js} +1 -1
- package/dashboard/dist/assets/{diagram-IFDJBPK2-DE6WjIb1.js → diagram-IFDJBPK2-Bo5sFrMg.js} +1 -1
- package/dashboard/dist/assets/{diagram-P4PSJMXO-DpW7UzNK.js → diagram-P4PSJMXO-DfRH0XU7.js} +1 -1
- package/dashboard/dist/assets/{erDiagram-INFDFZHY-BD2409fE.js → erDiagram-INFDFZHY-BgP1bwgw.js} +1 -1
- package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-1p0khhFI.js → flowDiagram-PKNHOUZH-C8Wws1ci.js} +1 -1
- package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-B2zFyA4s.js → ganttDiagram-A5KZAMGK-BgJhUuj8.js} +1 -1
- package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-bH-4YH7h.js → gitGraphDiagram-K3NZZRJ6-CI9FgCgo.js} +1 -1
- package/dashboard/dist/assets/{graph-BT24B6iQ.js → graph-B_7GyeiA.js} +1 -1
- package/dashboard/dist/assets/{index-Cxqr3rQB.js → index-CbndxFlO.js} +3 -3
- package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-CMzP4Hcg.js → infoDiagram-LFFYTUFH-BEtZcRxI.js} +1 -1
- package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-DMEmFC7M.js → ishikawaDiagram-PHBUUO56-0o9Bksuc.js} +1 -1
- package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-CAtzYQUm.js → journeyDiagram-4ABVD52K-RyRU3as7.js} +1 -1
- package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-d1JVbtvX.js → kanban-definition-K7BYSVSG-hLrHHizB.js} +1 -1
- package/dashboard/dist/assets/{layout-BVuI38I_.js → layout-w458wydd.js} +1 -1
- package/dashboard/dist/assets/{linear-Bc8PGMbp.js → linear-CnRlR5_w.js} +1 -1
- package/dashboard/dist/assets/{mermaid.core-UtwFYLNj.js → mermaid.core-DJ_9ty0Y.js} +4 -4
- package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-DHc5RCDj.js → mindmap-definition-YRQLILUH-C7TIzJVC.js} +1 -1
- package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-9anIsdIA.js → pieDiagram-SKSYHLDU-BZ9ysteM.js} +1 -1
- package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-FZ0D9HnU.js → quadrantDiagram-337W2JSQ-DpaMVSO1.js} +1 -1
- package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BkjCvH_u.js → requirementDiagram-Z7DCOOCP-EXFcskZZ.js} +1 -1
- package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DzqwYHDo.js → sankeyDiagram-WA2Y5GQK-BdImdXkY.js} +1 -1
- package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-BW4g5Ao-.js → sequenceDiagram-2WXFIKYE-CWN3faYf.js} +1 -1
- package/dashboard/dist/assets/{stateDiagram-RAJIS63D-D0tKU3Z0.js → stateDiagram-RAJIS63D-DIQdn-BF.js} +1 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-1u62oWzq.js +1 -0
- package/dashboard/dist/assets/{timeline-definition-YZTLITO2-Bu269QDX.js → timeline-definition-YZTLITO2-DMTIpUOE.js} +1 -1
- package/dashboard/dist/assets/{treemap-KZPCXAKY-BGu_rrva.js → treemap-KZPCXAKY-Y2AH2GB7.js} +1 -1
- package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-Cx_n5FRZ.js → vennDiagram-LZ73GAT5-BF4bJK-l.js} +1 -1
- package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-BOJsKV_W.js → xychartDiagram-JWTSCODW-DH-tOBC5.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dist/dashboard/server.js +1427 -550
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +2816 -1559
- package/dist/index.js.map +1 -1
- package/dist/launch/index.js +913 -141
- package/dist/launch/index.js.map +1 -1
- package/package.json +1 -1
- package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
- package/platforms/claude-code/hooks/enforce-boundaries.sh +49 -16
- package/platforms/claude-code/hooks/session-cleanup.sh +4 -2
- package/platforms/claude-code/skills/add-memory/SKILL.md +3 -2
- package/platforms/claude-code/skills/add-resource/SKILL.md +3 -2
- package/platforms/claude-code/skills/bundle-worktree/SKILL.md +3 -3
- package/platforms/claude-code/skills/capture-artifacts/SKILL.md +2 -2
- package/platforms/claude-code/skills/claim-resource/SKILL.md +3 -3
- package/platforms/claude-code/skills/clear-assignment/SKILL.md +9 -27
- package/platforms/claude-code/skills/complete-assignment/SKILL.md +3 -3
- package/platforms/claude-code/skills/create-assignment/SKILL.md +2 -2
- package/platforms/claude-code/skills/extend-resource/SKILL.md +2 -2
- package/platforms/claude-code/skills/grab-assignment/SKILL.md +17 -12
- package/platforms/claude-code/skills/grab-bundle/SKILL.md +10 -6
- package/platforms/claude-code/skills/log-progress/SKILL.md +6 -4
- package/platforms/claude-code/skills/plan-assignment/SKILL.md +3 -3
- package/platforms/claude-code/skills/plan-bundle/SKILL.md +4 -3
- package/platforms/claude-code/skills/replan/SKILL.md +9 -6
- package/platforms/claude-code/skills/resume-session/SKILL.md +14 -8
- package/platforms/claude-code/skills/save-session-summary/SKILL.md +4 -4
- package/platforms/claude-code/skills/set-workspace/SKILL.md +9 -6
- package/platforms/claude-code/skills/syntaur-protocol/SKILL.md +9 -7
- package/platforms/claude-code/skills/syntaur-protocol/references/file-ownership.md +1 -1
- package/platforms/claude-code/skills/syntaur-worktree/SKILL.md +11 -7
- package/platforms/codex/.codex-plugin/plugin.json +1 -1
- package/platforms/codex/scripts/enforce-boundaries.sh +74 -12
- package/platforms/codex/scripts/session-cleanup.sh +20 -13
- package/platforms/codex/skills/add-memory/SKILL.md +3 -2
- package/platforms/codex/skills/add-resource/SKILL.md +3 -2
- package/platforms/codex/skills/bundle-worktree/SKILL.md +3 -3
- package/platforms/codex/skills/capture-artifacts/SKILL.md +2 -2
- package/platforms/codex/skills/claim-resource/SKILL.md +3 -3
- package/platforms/codex/skills/clear-assignment/SKILL.md +9 -27
- package/platforms/codex/skills/complete-assignment/SKILL.md +3 -3
- package/platforms/codex/skills/create-assignment/SKILL.md +2 -2
- package/platforms/codex/skills/extend-resource/SKILL.md +2 -2
- package/platforms/codex/skills/grab-assignment/SKILL.md +17 -12
- package/platforms/codex/skills/grab-bundle/SKILL.md +10 -6
- package/platforms/codex/skills/log-progress/SKILL.md +6 -4
- package/platforms/codex/skills/plan-assignment/SKILL.md +3 -3
- package/platforms/codex/skills/plan-bundle/SKILL.md +4 -3
- package/platforms/codex/skills/replan/SKILL.md +9 -6
- package/platforms/codex/skills/resume-session/SKILL.md +14 -8
- package/platforms/codex/skills/save-session-summary/SKILL.md +4 -4
- package/platforms/codex/skills/set-workspace/SKILL.md +9 -6
- package/platforms/codex/skills/syntaur-protocol/SKILL.md +9 -7
- package/platforms/codex/skills/syntaur-protocol/references/file-ownership.md +1 -1
- package/platforms/codex/skills/syntaur-worktree/SKILL.md +11 -7
- package/platforms/hermes/plugins/syntaur/__init__.py +29 -3
- 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/platforms/hermes/plugins/syntaur/boundary.py +18 -9
- package/platforms/pi/extensions/syntaur/index.ts +84 -29
- package/skills/add-memory/SKILL.md +3 -2
- package/skills/add-resource/SKILL.md +3 -2
- package/skills/bundle-worktree/SKILL.md +3 -3
- package/skills/capture-artifacts/SKILL.md +2 -2
- package/skills/claim-resource/SKILL.md +3 -3
- package/skills/clear-assignment/SKILL.md +9 -27
- package/skills/complete-assignment/SKILL.md +3 -3
- package/skills/create-assignment/SKILL.md +2 -2
- package/skills/extend-resource/SKILL.md +2 -2
- package/skills/grab-assignment/SKILL.md +17 -12
- package/skills/grab-bundle/SKILL.md +10 -6
- package/skills/log-progress/SKILL.md +6 -4
- package/skills/plan-assignment/SKILL.md +3 -3
- package/skills/plan-bundle/SKILL.md +4 -3
- package/skills/replan/SKILL.md +9 -6
- package/skills/resume-session/SKILL.md +14 -8
- package/skills/save-session-summary/SKILL.md +4 -4
- package/skills/set-workspace/SKILL.md +9 -6
- package/skills/syntaur-protocol/SKILL.md +9 -7
- package/skills/syntaur-protocol/references/file-ownership.md +1 -1
- package/skills/syntaur-worktree/SKILL.md +11 -7
- package/dashboard/dist/assets/channel-fypxffzQ.js +0 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-1KnjQvtL.js +0 -1
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-1KnjQvtL.js +0 -1
- package/dashboard/dist/assets/clone-CKMabBhS.js +0 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DxWQhjNO.js +0 -1
package/dist/dashboard/server.js
CHANGED
|
@@ -36,9 +36,9 @@ function expandHome(p) {
|
|
|
36
36
|
return p;
|
|
37
37
|
}
|
|
38
38
|
function syntaurRoot() {
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
return resolve(expandHome(
|
|
39
|
+
const override3 = process.env.SYNTAUR_HOME;
|
|
40
|
+
if (override3 && override3.length > 0) {
|
|
41
|
+
return resolve(expandHome(override3));
|
|
42
42
|
}
|
|
43
43
|
return resolve(homedir(), ".syntaur");
|
|
44
44
|
}
|
|
@@ -122,7 +122,7 @@ function getTargetStatus(_from, command, table) {
|
|
|
122
122
|
if (!table) {
|
|
123
123
|
return DEFAULT_COMMAND_TARGETS.get(command) ?? null;
|
|
124
124
|
}
|
|
125
|
-
return table.get(command) ?? table.get(
|
|
125
|
+
return table.get(`${_from}:${command}`) ?? table.get(command) ?? null;
|
|
126
126
|
}
|
|
127
127
|
var DEFAULT_COMMAND_TARGETS, DEFAULT_TRANSITION_TABLE;
|
|
128
128
|
var init_state_machine = __esm({
|
|
@@ -452,6 +452,7 @@ function parseAssignmentFrontmatter(fileContent) {
|
|
|
452
452
|
planApproval: parsePlanApproval(frontmatter),
|
|
453
453
|
parked: getField2("parked") === "true",
|
|
454
454
|
reviewRequested: getField2("reviewRequested") === "true",
|
|
455
|
+
reworkRequested: getField2("reworkRequested") === "true",
|
|
455
456
|
implementationStarted: getField2("implementationStarted") === "true",
|
|
456
457
|
override: parseOverride(frontmatter),
|
|
457
458
|
facts: parseFactsMap(frontmatter),
|
|
@@ -634,11 +635,11 @@ function updatePlanApproval(fileContent, approval) {
|
|
|
634
635
|
approval === null ? null : { file: approval.file, digest: approval.digest, by: approval.by, at: approval.at }
|
|
635
636
|
);
|
|
636
637
|
}
|
|
637
|
-
function updateOverride(fileContent,
|
|
638
|
+
function updateOverride(fileContent, override3) {
|
|
638
639
|
return updateNestedBlock(
|
|
639
640
|
fileContent,
|
|
640
641
|
"override",
|
|
641
|
-
|
|
642
|
+
override3 === null ? null : { status: override3.status, source: override3.source, reason: override3.reason, at: override3.at }
|
|
642
643
|
);
|
|
643
644
|
}
|
|
644
645
|
function updateFactsMap(fileContent, name, value) {
|
|
@@ -1271,6 +1272,7 @@ function parseAssignmentFull(fileContent) {
|
|
|
1271
1272
|
disposition: getField(fm, "disposition"),
|
|
1272
1273
|
parked: getField(fm, "parked") === "true",
|
|
1273
1274
|
reviewRequested: getField(fm, "reviewRequested") === "true",
|
|
1275
|
+
reworkRequested: getField(fm, "reworkRequested") === "true",
|
|
1274
1276
|
implementationStarted: getField(fm, "implementationStarted") === "true",
|
|
1275
1277
|
planApproval: (() => {
|
|
1276
1278
|
const file = getNestedField(fm, "planApproval", "file");
|
|
@@ -2737,7 +2739,9 @@ var init_derive_config = __esm({
|
|
|
2737
2739
|
},
|
|
2738
2740
|
{
|
|
2739
2741
|
phase: "review",
|
|
2740
|
-
|
|
2742
|
+
// Rework (a new `implement` stage after `review`) must leave review even
|
|
2743
|
+
// when ACs stay checked — hence `AND NOT reworkRequested` (stage-fact-status-bridge).
|
|
2744
|
+
when: "(acAllChecked:true OR reviewRequested:true) AND NOT reworkRequested:true",
|
|
2741
2745
|
next: "Complete, or address review feedback"
|
|
2742
2746
|
}
|
|
2743
2747
|
],
|
|
@@ -2804,6 +2808,7 @@ var init_fields = __esm({
|
|
|
2804
2808
|
blocked: { kind: "bool" },
|
|
2805
2809
|
parked: { kind: "bool" },
|
|
2806
2810
|
reviewrequested: { kind: "bool", get: (i) => i["reviewRequested"] },
|
|
2811
|
+
reworkrequested: { kind: "bool", get: (i) => i["reworkRequested"] },
|
|
2807
2812
|
pinned: { kind: "bool" }
|
|
2808
2813
|
};
|
|
2809
2814
|
}
|
|
@@ -3597,6 +3602,7 @@ function queryFieldNames(declarations) {
|
|
|
3597
3602
|
"blocked",
|
|
3598
3603
|
"parked",
|
|
3599
3604
|
"reviewRequested",
|
|
3605
|
+
"reworkRequested",
|
|
3600
3606
|
"pinned"
|
|
3601
3607
|
];
|
|
3602
3608
|
const custom = [];
|
|
@@ -3635,6 +3641,7 @@ var init_fact_registry = __esm({
|
|
|
3635
3641
|
blocked: { kind: "bool" },
|
|
3636
3642
|
parked: { kind: "bool" },
|
|
3637
3643
|
reviewrequested: { kind: "bool", get: (i) => i["reviewRequested"] },
|
|
3644
|
+
reworkrequested: { kind: "bool", get: (i) => i["reworkRequested"] },
|
|
3638
3645
|
pinned: { kind: "bool" }
|
|
3639
3646
|
};
|
|
3640
3647
|
}
|
|
@@ -5633,7 +5640,7 @@ function compiledWhen(registry, when) {
|
|
|
5633
5640
|
return pred;
|
|
5634
5641
|
}
|
|
5635
5642
|
function deriveDimensions(input) {
|
|
5636
|
-
const { facts, derive, currentStatus, terminalStatuses, knownStatusIds, override } = input;
|
|
5643
|
+
const { facts, derive, currentStatus, terminalStatuses, knownStatusIds, override: override3 } = input;
|
|
5637
5644
|
const registry = input.registry ?? DERIVE_FIELDS;
|
|
5638
5645
|
if (terminalStatuses.has(currentStatus)) return null;
|
|
5639
5646
|
const ctx = { now: 0 };
|
|
@@ -5667,8 +5674,8 @@ function deriveDimensions(input) {
|
|
|
5667
5674
|
derivedStatus = phase;
|
|
5668
5675
|
}
|
|
5669
5676
|
let status = derivedStatus;
|
|
5670
|
-
if (
|
|
5671
|
-
status =
|
|
5677
|
+
if (override3 && override3.status && !terminalStatuses.has(override3.status) && knownStatusIds.has(override3.status)) {
|
|
5678
|
+
status = override3.status;
|
|
5672
5679
|
}
|
|
5673
5680
|
return { phase, disposition, derivedStatus, status, nextAction };
|
|
5674
5681
|
}
|
|
@@ -5954,6 +5961,18 @@ async function resolveAssignmentById(projectsDir, assignmentsDir2, id) {
|
|
|
5954
5961
|
}
|
|
5955
5962
|
return standaloneMatch ?? projectMatch ?? null;
|
|
5956
5963
|
}
|
|
5964
|
+
async function resolveAssignmentBySlug(projectsDir, assignmentsDir2, projectSlug, assignmentSlug) {
|
|
5965
|
+
const path = projectSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : resolve9(assignmentsDir2, assignmentSlug, "assignment.md");
|
|
5966
|
+
if (!await fileExists(path)) return { exists: false, id: null };
|
|
5967
|
+
try {
|
|
5968
|
+
const content = await readFile7(path, "utf-8");
|
|
5969
|
+
const [fm] = extractFrontmatter2(content);
|
|
5970
|
+
const id = getField(fm, "id");
|
|
5971
|
+
return { exists: true, id: id ?? null };
|
|
5972
|
+
} catch {
|
|
5973
|
+
return { exists: false, id: null };
|
|
5974
|
+
}
|
|
5975
|
+
}
|
|
5957
5976
|
var init_assignment_resolver = __esm({
|
|
5958
5977
|
"src/utils/assignment-resolver.ts"() {
|
|
5959
5978
|
"use strict";
|
|
@@ -6322,8 +6341,7 @@ async function areDependenciesSatisfied(projectDir, dependsOn, terminalStatuses)
|
|
|
6322
6341
|
if (!await fileExists(depPath)) return false;
|
|
6323
6342
|
try {
|
|
6324
6343
|
const content = await readFile9(depPath, "utf-8");
|
|
6325
|
-
const
|
|
6326
|
-
const status = m ? m[1].trim() : "";
|
|
6344
|
+
const { status } = parseAssignmentFrontmatter(content);
|
|
6327
6345
|
if (!terminalStatuses.has(status)) return false;
|
|
6328
6346
|
} catch {
|
|
6329
6347
|
return false;
|
|
@@ -6390,6 +6408,7 @@ async function computeFactsDetailed(input) {
|
|
|
6390
6408
|
blocked: frontmatter.blockedReason !== null,
|
|
6391
6409
|
parked: frontmatter.parked,
|
|
6392
6410
|
reviewRequested: frontmatter.reviewRequested,
|
|
6411
|
+
reworkRequested: frontmatter.reworkRequested,
|
|
6393
6412
|
pinned: frontmatter.override !== null
|
|
6394
6413
|
};
|
|
6395
6414
|
const attestations = [];
|
|
@@ -6438,6 +6457,7 @@ var init_facts = __esm({
|
|
|
6438
6457
|
init_fs();
|
|
6439
6458
|
init_git_worktree();
|
|
6440
6459
|
init_derive();
|
|
6460
|
+
init_frontmatter();
|
|
6441
6461
|
HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
6442
6462
|
PLAN_FILE_RE = /^plan(?:-v(\d+))?\.md$/;
|
|
6443
6463
|
}
|
|
@@ -7443,6 +7463,139 @@ var init_help = __esm({
|
|
|
7443
7463
|
}
|
|
7444
7464
|
});
|
|
7445
7465
|
|
|
7466
|
+
// src/db/engagement-schema.ts
|
|
7467
|
+
var ENGAGEMENT_SCHEMA_VERSION, ENGAGEMENT_DDL;
|
|
7468
|
+
var init_engagement_schema = __esm({
|
|
7469
|
+
"src/db/engagement-schema.ts"() {
|
|
7470
|
+
"use strict";
|
|
7471
|
+
ENGAGEMENT_SCHEMA_VERSION = "1";
|
|
7472
|
+
ENGAGEMENT_DDL = `
|
|
7473
|
+
CREATE TABLE IF NOT EXISTS engagement (
|
|
7474
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
7475
|
+
session_id TEXT NOT NULL,
|
|
7476
|
+
assignment_id TEXT,
|
|
7477
|
+
project_slug TEXT,
|
|
7478
|
+
assignment_slug TEXT,
|
|
7479
|
+
stage TEXT NOT NULL DEFAULT 'implement',
|
|
7480
|
+
started_at TEXT NOT NULL,
|
|
7481
|
+
ended_at TEXT,
|
|
7482
|
+
tokens_at_open TEXT,
|
|
7483
|
+
tokens_at_close TEXT,
|
|
7484
|
+
close_reason TEXT
|
|
7485
|
+
);
|
|
7486
|
+
CREATE UNIQUE INDEX IF NOT EXISTS one_active_per_session
|
|
7487
|
+
ON engagement(session_id) WHERE ended_at IS NULL;
|
|
7488
|
+
CREATE INDEX IF NOT EXISTS idx_engagement_session ON engagement(session_id);
|
|
7489
|
+
CREATE INDEX IF NOT EXISTS idx_engagement_assignment ON engagement(assignment_id);
|
|
7490
|
+
CREATE INDEX IF NOT EXISTS idx_engagement_slug ON engagement(project_slug, assignment_slug);
|
|
7491
|
+
`;
|
|
7492
|
+
}
|
|
7493
|
+
});
|
|
7494
|
+
|
|
7495
|
+
// src/db/engagement-backfill.ts
|
|
7496
|
+
import { readdirSync, readFileSync, statSync as statSync2 } from "fs";
|
|
7497
|
+
import { resolve as resolve13 } from "path";
|
|
7498
|
+
function safeReaddir(dir) {
|
|
7499
|
+
try {
|
|
7500
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
7501
|
+
} catch {
|
|
7502
|
+
return [];
|
|
7503
|
+
}
|
|
7504
|
+
}
|
|
7505
|
+
function readAssignmentId(assignmentMdPath) {
|
|
7506
|
+
try {
|
|
7507
|
+
const fm = parseAssignmentFrontmatter(readFileSync(assignmentMdPath, "utf-8"));
|
|
7508
|
+
return fm.id || null;
|
|
7509
|
+
} catch {
|
|
7510
|
+
return null;
|
|
7511
|
+
}
|
|
7512
|
+
}
|
|
7513
|
+
function transcriptMtimeIso(path) {
|
|
7514
|
+
if (!path) return null;
|
|
7515
|
+
try {
|
|
7516
|
+
return statSync2(path).mtime.toISOString();
|
|
7517
|
+
} catch {
|
|
7518
|
+
return null;
|
|
7519
|
+
}
|
|
7520
|
+
}
|
|
7521
|
+
function projectKey(projectSlug, assignmentSlug) {
|
|
7522
|
+
return JSON.stringify([projectSlug, assignmentSlug]);
|
|
7523
|
+
}
|
|
7524
|
+
function buildSlugIdMaps() {
|
|
7525
|
+
const root = syntaurRoot();
|
|
7526
|
+
const project = /* @__PURE__ */ new Map();
|
|
7527
|
+
const standalone = /* @__PURE__ */ new Map();
|
|
7528
|
+
const projectsDir = resolve13(root, "projects");
|
|
7529
|
+
for (const proj of safeReaddir(projectsDir)) {
|
|
7530
|
+
if (!proj.isDirectory()) continue;
|
|
7531
|
+
const asgRoot = resolve13(projectsDir, proj.name, "assignments");
|
|
7532
|
+
for (const asg of safeReaddir(asgRoot)) {
|
|
7533
|
+
if (!asg.isDirectory()) continue;
|
|
7534
|
+
const id = readAssignmentId(resolve13(asgRoot, asg.name, "assignment.md"));
|
|
7535
|
+
if (id) project.set(projectKey(proj.name, asg.name), id);
|
|
7536
|
+
}
|
|
7537
|
+
}
|
|
7538
|
+
const standaloneDir = resolve13(root, "assignments");
|
|
7539
|
+
for (const asg of safeReaddir(standaloneDir)) {
|
|
7540
|
+
if (!asg.isDirectory()) continue;
|
|
7541
|
+
const id = readAssignmentId(resolve13(standaloneDir, asg.name, "assignment.md"));
|
|
7542
|
+
if (id) standalone.set(asg.name, id);
|
|
7543
|
+
}
|
|
7544
|
+
return { project, standalone };
|
|
7545
|
+
}
|
|
7546
|
+
function backfillEngagements(db5) {
|
|
7547
|
+
const { project, standalone } = buildSlugIdMaps();
|
|
7548
|
+
const sessions = db5.prepare(
|
|
7549
|
+
`SELECT session_id, project_slug, assignment_slug, status, started, ended,
|
|
7550
|
+
transcript_path, updated_at
|
|
7551
|
+
FROM sessions`
|
|
7552
|
+
).all();
|
|
7553
|
+
const insert = db5.prepare(
|
|
7554
|
+
`INSERT INTO engagement
|
|
7555
|
+
(session_id, assignment_id, project_slug, assignment_slug, stage,
|
|
7556
|
+
started_at, ended_at, tokens_at_open, tokens_at_close, close_reason)
|
|
7557
|
+
VALUES
|
|
7558
|
+
(@session_id, @assignment_id, @project_slug, @assignment_slug, 'implement',
|
|
7559
|
+
@started_at, @ended_at, NULL, NULL, @close_reason)`
|
|
7560
|
+
);
|
|
7561
|
+
const counts = { backfilled: 0, attributed: 0, unattributed: 0 };
|
|
7562
|
+
for (const s of sessions) {
|
|
7563
|
+
let assignmentId = null;
|
|
7564
|
+
if (s.project_slug && s.assignment_slug) {
|
|
7565
|
+
assignmentId = project.get(projectKey(s.project_slug, s.assignment_slug)) ?? null;
|
|
7566
|
+
} else if (!s.project_slug && s.assignment_slug) {
|
|
7567
|
+
assignmentId = standalone.get(s.assignment_slug) ?? null;
|
|
7568
|
+
}
|
|
7569
|
+
const isActive = s.status === "active";
|
|
7570
|
+
let endedAt = null;
|
|
7571
|
+
let closeReason = null;
|
|
7572
|
+
if (!isActive) {
|
|
7573
|
+
endedAt = s.ended ?? transcriptMtimeIso(s.transcript_path) ?? s.updated_at ?? s.started;
|
|
7574
|
+
closeReason = s.status === "completed" ? "completed" : "abandoned";
|
|
7575
|
+
}
|
|
7576
|
+
insert.run({
|
|
7577
|
+
session_id: s.session_id,
|
|
7578
|
+
assignment_id: assignmentId,
|
|
7579
|
+
project_slug: s.project_slug,
|
|
7580
|
+
assignment_slug: s.assignment_slug,
|
|
7581
|
+
started_at: s.started,
|
|
7582
|
+
ended_at: endedAt,
|
|
7583
|
+
close_reason: closeReason
|
|
7584
|
+
});
|
|
7585
|
+
counts.backfilled++;
|
|
7586
|
+
if (assignmentId) counts.attributed++;
|
|
7587
|
+
else counts.unattributed++;
|
|
7588
|
+
}
|
|
7589
|
+
return counts;
|
|
7590
|
+
}
|
|
7591
|
+
var init_engagement_backfill = __esm({
|
|
7592
|
+
"src/db/engagement-backfill.ts"() {
|
|
7593
|
+
"use strict";
|
|
7594
|
+
init_paths();
|
|
7595
|
+
init_frontmatter();
|
|
7596
|
+
}
|
|
7597
|
+
});
|
|
7598
|
+
|
|
7446
7599
|
// src/dashboard/session-db.ts
|
|
7447
7600
|
var session_db_exports = {};
|
|
7448
7601
|
__export(session_db_exports, {
|
|
@@ -7454,18 +7607,23 @@ __export(session_db_exports, {
|
|
|
7454
7607
|
resetSessionDb: () => resetSessionDb
|
|
7455
7608
|
});
|
|
7456
7609
|
import Database2 from "better-sqlite3";
|
|
7457
|
-
import { resolve as
|
|
7610
|
+
import { resolve as resolve14 } from "path";
|
|
7458
7611
|
import { readdir as readdir8 } from "fs/promises";
|
|
7459
7612
|
function initSessionDb(dbPath) {
|
|
7460
7613
|
if (db2) return db2;
|
|
7461
|
-
const finalPath = dbPath ??
|
|
7614
|
+
const finalPath = dbPath ?? resolve14(syntaurRoot(), "syntaur.db");
|
|
7462
7615
|
db2 = new Database2(finalPath);
|
|
7463
7616
|
db2.pragma("journal_mode = WAL");
|
|
7464
7617
|
db2.exec(SCHEMA_SQL2);
|
|
7618
|
+
db2.exec(ENGAGEMENT_DDL);
|
|
7465
7619
|
db2.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run(
|
|
7466
7620
|
"schema_version",
|
|
7467
7621
|
SCHEMA_VERSION
|
|
7468
7622
|
);
|
|
7623
|
+
db2.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run(
|
|
7624
|
+
"engagement_schema_version",
|
|
7625
|
+
ENGAGEMENT_SCHEMA_VERSION
|
|
7626
|
+
);
|
|
7469
7627
|
const database = db2;
|
|
7470
7628
|
const runMigrations = database.transaction(() => {
|
|
7471
7629
|
const vBeforeV2 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
|
|
@@ -7592,9 +7750,43 @@ function initSessionDb(dbPath) {
|
|
|
7592
7750
|
UPDATE meta SET value = '5' WHERE key = 'schema_version';
|
|
7593
7751
|
`);
|
|
7594
7752
|
}
|
|
7753
|
+
const vBeforeV6 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
|
|
7754
|
+
if (vBeforeV6 === "5") {
|
|
7755
|
+
database.exec(ENGAGEMENT_DDL);
|
|
7756
|
+
const counts = backfillEngagements(database);
|
|
7757
|
+
database.exec(`
|
|
7758
|
+
CREATE TABLE sessions_v6 (
|
|
7759
|
+
session_id TEXT PRIMARY KEY,
|
|
7760
|
+
agent TEXT NOT NULL,
|
|
7761
|
+
started TEXT NOT NULL,
|
|
7762
|
+
ended TEXT,
|
|
7763
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
7764
|
+
path TEXT,
|
|
7765
|
+
description TEXT,
|
|
7766
|
+
transcript_path TEXT,
|
|
7767
|
+
pid INTEGER,
|
|
7768
|
+
pid_started_at TEXT,
|
|
7769
|
+
original_head_sha TEXT,
|
|
7770
|
+
activity TEXT,
|
|
7771
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
7772
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
7773
|
+
);
|
|
7774
|
+
INSERT INTO sessions_v6
|
|
7775
|
+
SELECT session_id, agent, started, ended, status, path, description,
|
|
7776
|
+
transcript_path, pid, pid_started_at, original_head_sha, NULL,
|
|
7777
|
+
created_at, updated_at
|
|
7778
|
+
FROM sessions;
|
|
7779
|
+
DROP TABLE sessions;
|
|
7780
|
+
ALTER TABLE sessions_v6 RENAME TO sessions;
|
|
7781
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
7782
|
+
UPDATE meta SET value = '6' WHERE key = 'schema_version';
|
|
7783
|
+
`);
|
|
7784
|
+
console.log(
|
|
7785
|
+
`engagement backfill: backfilled=${counts.backfilled} attributed=${counts.attributed} unattributed=${counts.unattributed}`
|
|
7786
|
+
);
|
|
7787
|
+
}
|
|
7595
7788
|
});
|
|
7596
7789
|
runMigrations.exclusive();
|
|
7597
|
-
db2.exec(POST_MIGRATION_INDEXES_SQL);
|
|
7598
7790
|
return db2;
|
|
7599
7791
|
}
|
|
7600
7792
|
function isSessionDbInitialized() {
|
|
@@ -7626,20 +7818,38 @@ async function migrateFromMarkdown(projectsDir) {
|
|
|
7626
7818
|
const allSessions = [];
|
|
7627
7819
|
for (const entry of entries) {
|
|
7628
7820
|
if (!entry.isDirectory()) continue;
|
|
7629
|
-
const projectDir =
|
|
7630
|
-
const indexPath =
|
|
7821
|
+
const projectDir = resolve14(projectsDir, entry.name);
|
|
7822
|
+
const indexPath = resolve14(projectDir, "_index-sessions.md");
|
|
7631
7823
|
if (!await fileExists(indexPath)) continue;
|
|
7632
7824
|
const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
|
|
7633
7825
|
allSessions.push(...sessions);
|
|
7634
7826
|
}
|
|
7635
7827
|
if (allSessions.length === 0) return 0;
|
|
7636
7828
|
const insert = database.prepare(`
|
|
7637
|
-
INSERT OR IGNORE INTO sessions (session_id,
|
|
7638
|
-
VALUES (?, ?, ?, ?,
|
|
7829
|
+
INSERT OR IGNORE INTO sessions (session_id, agent, started, status, path)
|
|
7830
|
+
VALUES (?, ?, ?, ?, ?)
|
|
7831
|
+
`);
|
|
7832
|
+
const insertEngagement = database.prepare(`
|
|
7833
|
+
INSERT INTO engagement (session_id, project_slug, assignment_slug, stage, started_at, ended_at, close_reason)
|
|
7834
|
+
SELECT @sid, @ps, @as, 'implement', @started, @ended, @reason
|
|
7835
|
+
WHERE NOT EXISTS (
|
|
7836
|
+
SELECT 1 FROM engagement WHERE session_id = @sid AND ended_at IS NULL
|
|
7837
|
+
)
|
|
7639
7838
|
`);
|
|
7640
7839
|
const insertAll = database.transaction((sessions) => {
|
|
7641
7840
|
for (const s of sessions) {
|
|
7642
|
-
insert.run(s.sessionId, s.
|
|
7841
|
+
const res = insert.run(s.sessionId, s.agent, s.started, s.status, s.path);
|
|
7842
|
+
if (res.changes > 0 && (s.projectSlug || s.assignmentSlug)) {
|
|
7843
|
+
const terminal = s.status === "completed" || s.status === "stopped";
|
|
7844
|
+
insertEngagement.run({
|
|
7845
|
+
sid: s.sessionId,
|
|
7846
|
+
ps: s.projectSlug ?? null,
|
|
7847
|
+
as: s.assignmentSlug ?? null,
|
|
7848
|
+
started: s.started,
|
|
7849
|
+
ended: terminal ? s.started : null,
|
|
7850
|
+
reason: terminal ? s.status === "completed" ? "completed" : "abandoned" : null
|
|
7851
|
+
});
|
|
7852
|
+
}
|
|
7643
7853
|
}
|
|
7644
7854
|
});
|
|
7645
7855
|
insertAll(allSessions);
|
|
@@ -7647,8 +7857,8 @@ async function migrateFromMarkdown(projectsDir) {
|
|
|
7647
7857
|
return allSessions.length;
|
|
7648
7858
|
}
|
|
7649
7859
|
async function parseMarkdownSessionsIndex(filePath, projectSlug) {
|
|
7650
|
-
const { readFile:
|
|
7651
|
-
const raw2 = await
|
|
7860
|
+
const { readFile: readFile31 } = await import("fs/promises");
|
|
7861
|
+
const raw2 = await readFile31(filePath, "utf-8");
|
|
7652
7862
|
const sessions = [];
|
|
7653
7863
|
const lines = raw2.split("\n");
|
|
7654
7864
|
let inTable = false;
|
|
@@ -7682,19 +7892,19 @@ async function parseMarkdownSessionsIndex(filePath, projectSlug) {
|
|
|
7682
7892
|
}
|
|
7683
7893
|
return sessions;
|
|
7684
7894
|
}
|
|
7685
|
-
var db2, SCHEMA_VERSION, SCHEMA_SQL2
|
|
7895
|
+
var db2, SCHEMA_VERSION, SCHEMA_SQL2;
|
|
7686
7896
|
var init_session_db = __esm({
|
|
7687
7897
|
"src/dashboard/session-db.ts"() {
|
|
7688
7898
|
"use strict";
|
|
7689
7899
|
init_paths();
|
|
7690
7900
|
init_fs();
|
|
7901
|
+
init_engagement_schema();
|
|
7902
|
+
init_engagement_backfill();
|
|
7691
7903
|
db2 = null;
|
|
7692
|
-
SCHEMA_VERSION = "
|
|
7904
|
+
SCHEMA_VERSION = "6";
|
|
7693
7905
|
SCHEMA_SQL2 = `
|
|
7694
7906
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
7695
7907
|
session_id TEXT PRIMARY KEY,
|
|
7696
|
-
project_slug TEXT,
|
|
7697
|
-
assignment_slug TEXT,
|
|
7698
7908
|
agent TEXT NOT NULL,
|
|
7699
7909
|
started TEXT NOT NULL,
|
|
7700
7910
|
ended TEXT,
|
|
@@ -7705,34 +7915,175 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
7705
7915
|
pid INTEGER,
|
|
7706
7916
|
pid_started_at TEXT,
|
|
7707
7917
|
original_head_sha TEXT,
|
|
7918
|
+
activity TEXT,
|
|
7708
7919
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
7709
7920
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
7710
7921
|
);
|
|
7711
7922
|
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
7712
7923
|
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
|
|
7713
7924
|
`;
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7925
|
+
}
|
|
7926
|
+
});
|
|
7927
|
+
|
|
7928
|
+
// src/db/engagement-tokens.ts
|
|
7929
|
+
function serializeSnapshot(snapshot) {
|
|
7930
|
+
return snapshot === null ? null : JSON.stringify(snapshot);
|
|
7931
|
+
}
|
|
7932
|
+
function parseSnapshot(json) {
|
|
7933
|
+
return json === null ? null : JSON.parse(json);
|
|
7934
|
+
}
|
|
7935
|
+
function tableExists(name) {
|
|
7936
|
+
const row = getSessionDb().prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(name);
|
|
7937
|
+
return row !== void 0;
|
|
7938
|
+
}
|
|
7939
|
+
function getCumulativeTokenSource() {
|
|
7940
|
+
return override ?? productionCumulativeTokenSource;
|
|
7941
|
+
}
|
|
7942
|
+
var productionCumulativeTokenSource, override;
|
|
7943
|
+
var init_engagement_tokens = __esm({
|
|
7944
|
+
"src/db/engagement-tokens.ts"() {
|
|
7945
|
+
"use strict";
|
|
7946
|
+
init_session_db();
|
|
7947
|
+
productionCumulativeTokenSource = async (sessionId) => {
|
|
7948
|
+
const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7949
|
+
const db5 = getSessionDb();
|
|
7950
|
+
const collectorRow = db5.prepare("SELECT value FROM meta WHERE key = 'usage_last_collector_run'").get();
|
|
7951
|
+
const collectorRunAt = collectorRow?.value ?? null;
|
|
7952
|
+
const models = {};
|
|
7953
|
+
if (tableExists("usage_events")) {
|
|
7954
|
+
const rows = db5.prepare(
|
|
7955
|
+
`SELECT model, input_tokens, output_tokens, cache_creation_tokens,
|
|
7956
|
+
cache_read_tokens, total_tokens, total_cost
|
|
7957
|
+
FROM usage_events
|
|
7958
|
+
WHERE session_id = ?`
|
|
7959
|
+
).all(sessionId);
|
|
7960
|
+
for (const r of rows) {
|
|
7961
|
+
models[r.model] = {
|
|
7962
|
+
input: r.input_tokens,
|
|
7963
|
+
output: r.output_tokens,
|
|
7964
|
+
cacheCreation: r.cache_creation_tokens,
|
|
7965
|
+
cacheRead: r.cache_read_tokens,
|
|
7966
|
+
total: r.total_tokens,
|
|
7967
|
+
cost: r.total_cost
|
|
7968
|
+
};
|
|
7969
|
+
}
|
|
7970
|
+
}
|
|
7971
|
+
return { models, collectorRunAt, capturedAt };
|
|
7972
|
+
};
|
|
7973
|
+
override = null;
|
|
7974
|
+
}
|
|
7975
|
+
});
|
|
7976
|
+
|
|
7977
|
+
// src/db/engagement-db.ts
|
|
7978
|
+
function getOpenEngagement(sessionId) {
|
|
7979
|
+
const row = getSessionDb().prepare(
|
|
7980
|
+
"SELECT * FROM engagement WHERE session_id = ? AND ended_at IS NULL LIMIT 1"
|
|
7981
|
+
).get(sessionId);
|
|
7982
|
+
return row ?? null;
|
|
7983
|
+
}
|
|
7984
|
+
function getLatestEngagement(sessionId) {
|
|
7985
|
+
const row = getSessionDb().prepare(
|
|
7986
|
+
`SELECT * FROM engagement WHERE session_id = ?
|
|
7987
|
+
ORDER BY (ended_at IS NULL) DESC, started_at DESC, id DESC
|
|
7988
|
+
LIMIT 1`
|
|
7989
|
+
).get(sessionId);
|
|
7990
|
+
return row ?? null;
|
|
7991
|
+
}
|
|
7992
|
+
function hasAnyEngagement(sessionId) {
|
|
7993
|
+
return getSessionDb().prepare("SELECT 1 FROM engagement WHERE session_id = ? LIMIT 1").get(sessionId) !== void 0;
|
|
7994
|
+
}
|
|
7995
|
+
function insertClosedEngagement(input) {
|
|
7996
|
+
const res = getSessionDb().prepare(
|
|
7997
|
+
`INSERT INTO engagement
|
|
7998
|
+
(session_id, assignment_id, project_slug, assignment_slug, stage, started_at, ended_at, close_reason)
|
|
7999
|
+
VALUES (@sessionId, @assignmentId, @projectSlug, @assignmentSlug, @stage, @startedAt, @endedAt, @closeReason)`
|
|
8000
|
+
).run({
|
|
8001
|
+
sessionId: input.sessionId,
|
|
8002
|
+
assignmentId: input.assignmentId ?? null,
|
|
8003
|
+
projectSlug: input.projectSlug ?? null,
|
|
8004
|
+
assignmentSlug: input.assignmentSlug ?? null,
|
|
8005
|
+
stage: input.stage ?? DEFAULT_STAGE,
|
|
8006
|
+
startedAt: input.startedAt,
|
|
8007
|
+
endedAt: input.endedAt,
|
|
8008
|
+
closeReason: input.closeReason
|
|
8009
|
+
});
|
|
8010
|
+
return getEngagementById(Number(res.lastInsertRowid));
|
|
8011
|
+
}
|
|
8012
|
+
function getEngagementById(id) {
|
|
8013
|
+
const row = getSessionDb().prepare("SELECT * FROM engagement WHERE id = ?").get(id);
|
|
8014
|
+
if (!row) throw new Error(`engagement ${id} not found after insert`);
|
|
8015
|
+
return row;
|
|
8016
|
+
}
|
|
8017
|
+
function openEngagement(input) {
|
|
8018
|
+
const res = getSessionDb().prepare(
|
|
8019
|
+
`INSERT INTO engagement
|
|
8020
|
+
(session_id, assignment_id, project_slug, assignment_slug, stage, started_at, tokens_at_open)
|
|
8021
|
+
VALUES (@sessionId, @assignmentId, @projectSlug, @assignmentSlug, @stage, @startedAt, @tokensAtOpen)`
|
|
8022
|
+
).run({
|
|
8023
|
+
sessionId: input.sessionId,
|
|
8024
|
+
assignmentId: input.assignmentId ?? null,
|
|
8025
|
+
projectSlug: input.projectSlug ?? null,
|
|
8026
|
+
assignmentSlug: input.assignmentSlug ?? null,
|
|
8027
|
+
stage: input.stage ?? DEFAULT_STAGE,
|
|
8028
|
+
startedAt: input.startedAt,
|
|
8029
|
+
tokensAtOpen: serializeSnapshot(input.tokensAtOpen ?? null)
|
|
8030
|
+
});
|
|
8031
|
+
return getEngagementById(Number(res.lastInsertRowid));
|
|
8032
|
+
}
|
|
8033
|
+
function closeEngagementById(input) {
|
|
8034
|
+
const res = getSessionDb().prepare(
|
|
8035
|
+
`UPDATE engagement
|
|
8036
|
+
SET ended_at = @endedAt,
|
|
8037
|
+
tokens_at_close = @tokensAtClose,
|
|
8038
|
+
close_reason = @closeReason
|
|
8039
|
+
WHERE id = @id AND started_at = @startedAt AND ended_at IS NULL`
|
|
8040
|
+
).run({
|
|
8041
|
+
id: input.id,
|
|
8042
|
+
startedAt: input.startedAt,
|
|
8043
|
+
endedAt: input.endedAt,
|
|
8044
|
+
tokensAtClose: serializeSnapshot(input.tokensAtClose ?? null),
|
|
8045
|
+
closeReason: input.closeReason
|
|
8046
|
+
});
|
|
8047
|
+
return res.changes > 0;
|
|
8048
|
+
}
|
|
8049
|
+
function ensureOpenEngagement(input) {
|
|
8050
|
+
if (getOpenEngagement(input.sessionId)) return null;
|
|
8051
|
+
try {
|
|
8052
|
+
return openEngagement(input);
|
|
8053
|
+
} catch (err) {
|
|
8054
|
+
if (err instanceof Error && /UNIQUE constraint failed/.test(err.message)) {
|
|
8055
|
+
return null;
|
|
8056
|
+
}
|
|
8057
|
+
throw err;
|
|
8058
|
+
}
|
|
8059
|
+
}
|
|
8060
|
+
var DEFAULT_STAGE;
|
|
8061
|
+
var init_engagement_db = __esm({
|
|
8062
|
+
"src/db/engagement-db.ts"() {
|
|
8063
|
+
"use strict";
|
|
8064
|
+
init_session_db();
|
|
8065
|
+
init_engagement_tokens();
|
|
8066
|
+
DEFAULT_STAGE = "implement";
|
|
7718
8067
|
}
|
|
7719
8068
|
});
|
|
7720
8069
|
|
|
7721
8070
|
// src/dashboard/agent-sessions.ts
|
|
7722
8071
|
var agent_sessions_exports = {};
|
|
7723
8072
|
__export(agent_sessions_exports, {
|
|
8073
|
+
SessionResurrectionError: () => SessionResurrectionError,
|
|
7724
8074
|
appendSession: () => appendSession,
|
|
7725
8075
|
deleteSessions: () => deleteSessions,
|
|
7726
8076
|
getSessionById: () => getSessionById,
|
|
7727
8077
|
listAllSessions: () => listAllSessions,
|
|
7728
8078
|
listProjectSessions: () => listProjectSessions,
|
|
7729
8079
|
listSessionsByAssignment: () => listSessionsByAssignment,
|
|
8080
|
+
livenessStopSession: () => livenessStopSession,
|
|
7730
8081
|
parseSessionsIndex: () => parseSessionsIndex,
|
|
7731
8082
|
reconcileActiveSessions: () => reconcileActiveSessions,
|
|
7732
8083
|
updateSessionStatus: () => updateSessionStatus
|
|
7733
8084
|
});
|
|
7734
8085
|
import { readFile as readFile11 } from "fs/promises";
|
|
7735
|
-
import { resolve as
|
|
8086
|
+
import { resolve as resolve15 } from "path";
|
|
7736
8087
|
function rowToSession(row) {
|
|
7737
8088
|
return {
|
|
7738
8089
|
sessionId: row.session_id,
|
|
@@ -7748,28 +8099,64 @@ function rowToSession(row) {
|
|
|
7748
8099
|
pid: row.pid ?? null,
|
|
7749
8100
|
pidStartedAt: row.pid_started_at ?? null,
|
|
7750
8101
|
originalHeadSha: row.original_head_sha ?? null,
|
|
8102
|
+
activity: row.activity ?? null,
|
|
7751
8103
|
updatedAt: row.updated_at ?? null
|
|
7752
8104
|
};
|
|
7753
8105
|
}
|
|
7754
8106
|
async function parseSessionsIndex(_projectDir, projectSlug) {
|
|
7755
8107
|
const db5 = getSessionDb();
|
|
7756
|
-
const rows = db5.prepare(
|
|
8108
|
+
const rows = db5.prepare(`${SESSION_SELECT_WITH_BINDING} WHERE e.project_slug = ? ORDER BY s.started DESC`).all(projectSlug);
|
|
7757
8109
|
return rows.map(rowToSession);
|
|
7758
8110
|
}
|
|
8111
|
+
function reopenEngagementIfMissing(sessionId, freshBinding, freshStartedAt, tokensAtOpen) {
|
|
8112
|
+
if (getOpenEngagement(sessionId)) return;
|
|
8113
|
+
if (freshBinding && (freshBinding.projectSlug || freshBinding.assignmentSlug)) {
|
|
8114
|
+
ensureOpenEngagement({
|
|
8115
|
+
sessionId,
|
|
8116
|
+
assignmentId: freshBinding.assignmentId ?? null,
|
|
8117
|
+
projectSlug: freshBinding.projectSlug,
|
|
8118
|
+
assignmentSlug: freshBinding.assignmentSlug,
|
|
8119
|
+
stage: "implement",
|
|
8120
|
+
startedAt: freshStartedAt,
|
|
8121
|
+
tokensAtOpen: tokensAtOpen ?? null
|
|
8122
|
+
});
|
|
8123
|
+
return;
|
|
8124
|
+
}
|
|
8125
|
+
const latest = getLatestEngagement(sessionId);
|
|
8126
|
+
if (latest && (latest.project_slug || latest.assignment_slug)) {
|
|
8127
|
+
ensureOpenEngagement({
|
|
8128
|
+
sessionId,
|
|
8129
|
+
assignmentId: latest.assignment_id,
|
|
8130
|
+
projectSlug: latest.project_slug,
|
|
8131
|
+
assignmentSlug: latest.assignment_slug,
|
|
8132
|
+
stage: "implement",
|
|
8133
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8134
|
+
tokensAtOpen: tokensAtOpen ?? null
|
|
8135
|
+
});
|
|
8136
|
+
}
|
|
8137
|
+
}
|
|
7759
8138
|
async function appendSession(_projectDir, session, opts) {
|
|
7760
8139
|
const db5 = getSessionDb();
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
8140
|
+
let openSnapshot = null;
|
|
8141
|
+
try {
|
|
8142
|
+
openSnapshot = await getCumulativeTokenSource()(session.sessionId);
|
|
8143
|
+
} catch {
|
|
8144
|
+
openSnapshot = null;
|
|
8145
|
+
}
|
|
8146
|
+
const upsert = db5.prepare(`
|
|
8147
|
+
INSERT INTO sessions (session_id, agent, started, status, path, description, transcript_path, pid, pid_started_at, original_head_sha)
|
|
8148
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7764
8149
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
7765
|
-
project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),
|
|
7766
|
-
assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),
|
|
7767
8150
|
agent = excluded.agent,
|
|
7768
8151
|
status = CASE
|
|
7769
8152
|
WHEN status = 'completed' THEN status
|
|
7770
8153
|
WHEN status = 'stopped' AND NOT (? AND excluded.status = 'active') THEN status
|
|
7771
8154
|
ELSE excluded.status
|
|
7772
8155
|
END,
|
|
8156
|
+
ended = CASE
|
|
8157
|
+
WHEN status = 'stopped' AND ? AND excluded.status = 'active' THEN NULL
|
|
8158
|
+
ELSE ended
|
|
8159
|
+
END,
|
|
7773
8160
|
path = COALESCE(NULLIF(excluded.path, ''), path),
|
|
7774
8161
|
description = COALESCE(NULLIF(excluded.description, ''), description),
|
|
7775
8162
|
transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),
|
|
@@ -7777,59 +8164,151 @@ async function appendSession(_projectDir, session, opts) {
|
|
|
7777
8164
|
pid_started_at = COALESCE(NULLIF(excluded.pid_started_at, ''), pid_started_at),
|
|
7778
8165
|
original_head_sha = COALESCE(NULLIF(original_head_sha, ''), NULLIF(excluded.original_head_sha, '')),
|
|
7779
8166
|
updated_at = datetime('now')
|
|
7780
|
-
`)
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
8167
|
+
`);
|
|
8168
|
+
const apply = db5.transaction(() => {
|
|
8169
|
+
upsert.run(
|
|
8170
|
+
session.sessionId,
|
|
8171
|
+
session.agent,
|
|
8172
|
+
session.started,
|
|
8173
|
+
session.status,
|
|
8174
|
+
session.path,
|
|
8175
|
+
session.description ?? null,
|
|
8176
|
+
session.transcriptPath ?? null,
|
|
8177
|
+
session.pid ?? null,
|
|
8178
|
+
session.pidStartedAt ?? null,
|
|
8179
|
+
session.originalHeadSha ?? null,
|
|
8180
|
+
opts?.reviveStopped ? 1 : 0,
|
|
8181
|
+
// status CASE: revive guard
|
|
8182
|
+
opts?.reviveStopped ? 1 : 0
|
|
8183
|
+
// ended CASE: clear stale ended on revive
|
|
8184
|
+
);
|
|
8185
|
+
const persisted = db5.prepare("SELECT status, ended FROM sessions WHERE session_id = ?").get(session.sessionId);
|
|
8186
|
+
const freshBinding = {
|
|
8187
|
+
assignmentId: session.assignmentId ?? null,
|
|
8188
|
+
projectSlug: session.projectSlug ?? null,
|
|
8189
|
+
assignmentSlug: session.assignmentSlug ?? null
|
|
8190
|
+
};
|
|
8191
|
+
if (persisted?.status === "active") {
|
|
8192
|
+
reopenEngagementIfMissing(
|
|
8193
|
+
session.sessionId,
|
|
8194
|
+
freshBinding,
|
|
8195
|
+
session.started ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
8196
|
+
openSnapshot
|
|
8197
|
+
);
|
|
8198
|
+
} else if (persisted && (freshBinding.projectSlug || freshBinding.assignmentSlug) && !hasAnyEngagement(session.sessionId)) {
|
|
8199
|
+
insertClosedEngagement({
|
|
8200
|
+
sessionId: session.sessionId,
|
|
8201
|
+
projectSlug: freshBinding.projectSlug,
|
|
8202
|
+
assignmentSlug: freshBinding.assignmentSlug,
|
|
8203
|
+
startedAt: session.started ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
8204
|
+
endedAt: persisted.ended ?? session.started ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
8205
|
+
closeReason: persisted.status === "completed" ? "completed" : "abandoned"
|
|
8206
|
+
});
|
|
8207
|
+
}
|
|
8208
|
+
});
|
|
8209
|
+
apply.immediate();
|
|
7795
8210
|
}
|
|
7796
8211
|
async function updateSessionStatus(_projectDir, sessionId, status, endedAt) {
|
|
7797
8212
|
const db5 = getSessionDb();
|
|
7798
8213
|
const isTerminal = status === "completed" || status === "stopped";
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
8214
|
+
if (!isTerminal) {
|
|
8215
|
+
let reviveSnapshot = null;
|
|
8216
|
+
try {
|
|
8217
|
+
reviveSnapshot = await getCumulativeTokenSource()(sessionId);
|
|
8218
|
+
} catch {
|
|
8219
|
+
reviveSnapshot = null;
|
|
8220
|
+
}
|
|
8221
|
+
const apply2 = db5.transaction(() => {
|
|
8222
|
+
const current = db5.prepare("SELECT status FROM sessions WHERE session_id = ?").get(sessionId);
|
|
8223
|
+
if (!current) return false;
|
|
8224
|
+
if (current.status === "completed") {
|
|
8225
|
+
throw new SessionResurrectionError(
|
|
8226
|
+
`Refusing to revive completed session "${sessionId}" to active. A completed session is final.`
|
|
8227
|
+
);
|
|
8228
|
+
}
|
|
8229
|
+
const res = db5.prepare(
|
|
8230
|
+
"UPDATE sessions SET status = ?, ended = NULL, updated_at = datetime('now') WHERE session_id = ?"
|
|
8231
|
+
).run(status, sessionId);
|
|
8232
|
+
if (res.changes > 0) {
|
|
8233
|
+
reopenEngagementIfMissing(sessionId, null, (/* @__PURE__ */ new Date()).toISOString(), reviveSnapshot);
|
|
8234
|
+
}
|
|
8235
|
+
return res.changes > 0;
|
|
8236
|
+
});
|
|
8237
|
+
return apply2.immediate();
|
|
8238
|
+
}
|
|
8239
|
+
const captured = getOpenEngagement(sessionId);
|
|
8240
|
+
let snapshot = null;
|
|
8241
|
+
try {
|
|
8242
|
+
snapshot = await getCumulativeTokenSource()(sessionId);
|
|
8243
|
+
} catch {
|
|
8244
|
+
snapshot = null;
|
|
8245
|
+
}
|
|
8246
|
+
const apply = db5.transaction(() => {
|
|
8247
|
+
const stillTerminal = captured ? closeEngagementById({
|
|
8248
|
+
id: captured.id,
|
|
8249
|
+
startedAt: captured.started_at,
|
|
8250
|
+
closeReason: status === "completed" ? "completed" : "abandoned",
|
|
8251
|
+
tokensAtClose: snapshot,
|
|
8252
|
+
endedAt: endedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
8253
|
+
}) : getOpenEngagement(sessionId) === null;
|
|
8254
|
+
if (!stillTerminal) return false;
|
|
8255
|
+
const res = db5.prepare(
|
|
8256
|
+
"UPDATE sessions SET status = ?, ended = COALESCE(?, datetime('now')), updated_at = datetime('now') WHERE session_id = ?"
|
|
8257
|
+
).run(status, endedAt ?? null, sessionId);
|
|
8258
|
+
return res.changes > 0;
|
|
8259
|
+
});
|
|
8260
|
+
return apply.immediate();
|
|
8261
|
+
}
|
|
8262
|
+
function livenessStopSession(input) {
|
|
8263
|
+
const db5 = getSessionDb();
|
|
8264
|
+
const endedAt = input.endedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
8265
|
+
const apply = db5.transaction(() => {
|
|
8266
|
+
const stillDead = input.engagementId != null && input.engagementStartedAt != null ? closeEngagementById({
|
|
8267
|
+
id: input.engagementId,
|
|
8268
|
+
startedAt: input.engagementStartedAt,
|
|
8269
|
+
closeReason: "liveness_gc",
|
|
8270
|
+
tokensAtClose: input.tokensAtClose ?? null,
|
|
8271
|
+
endedAt
|
|
8272
|
+
}) : getOpenEngagement(input.sessionId) === null;
|
|
8273
|
+
if (!stillDead) return false;
|
|
8274
|
+
const res = db5.prepare(
|
|
8275
|
+
"UPDATE sessions SET status = 'stopped', ended = COALESCE(?, datetime('now')), updated_at = datetime('now') WHERE session_id = ? AND status = 'active'"
|
|
8276
|
+
).run(input.endedAt ?? null, input.sessionId);
|
|
8277
|
+
return res.changes > 0;
|
|
8278
|
+
});
|
|
8279
|
+
return apply.immediate();
|
|
7805
8280
|
}
|
|
7806
8281
|
async function listAllSessions(_projectsDir) {
|
|
7807
8282
|
const db5 = getSessionDb();
|
|
7808
|
-
const rows = db5.prepare(
|
|
8283
|
+
const rows = db5.prepare(`${SESSION_SELECT_WITH_BINDING} ORDER BY s.started DESC`).all();
|
|
7809
8284
|
return rows.map(rowToSession);
|
|
7810
8285
|
}
|
|
7811
8286
|
function getSessionById(sessionId) {
|
|
7812
8287
|
const db5 = getSessionDb();
|
|
7813
|
-
const row = db5.prepare(
|
|
8288
|
+
const row = db5.prepare(`${SESSION_SELECT_WITH_BINDING} WHERE s.session_id = ? LIMIT 1`).get(sessionId);
|
|
7814
8289
|
return row ? rowToSession(row) : null;
|
|
7815
8290
|
}
|
|
7816
8291
|
async function listProjectSessions(_projectsDir, projectSlug, assignmentSlug) {
|
|
7817
8292
|
const db5 = getSessionDb();
|
|
7818
8293
|
if (assignmentSlug) {
|
|
7819
8294
|
const rows2 = db5.prepare(
|
|
7820
|
-
|
|
8295
|
+
`${SESSION_SELECT_WITH_BINDING} WHERE e.project_slug = ? AND e.assignment_slug = ? ORDER BY s.started DESC`
|
|
7821
8296
|
).all(projectSlug, assignmentSlug);
|
|
7822
8297
|
return rows2.map(rowToSession);
|
|
7823
8298
|
}
|
|
7824
|
-
const rows = db5.prepare(
|
|
8299
|
+
const rows = db5.prepare(`${SESSION_SELECT_WITH_BINDING} WHERE e.project_slug = ? ORDER BY s.started DESC`).all(projectSlug);
|
|
7825
8300
|
return rows.map(rowToSession);
|
|
7826
8301
|
}
|
|
7827
8302
|
async function deleteSessions(sessionIds) {
|
|
7828
8303
|
if (sessionIds.length === 0) return 0;
|
|
7829
8304
|
const db5 = getSessionDb();
|
|
7830
8305
|
const placeholders = sessionIds.map(() => "?").join(", ");
|
|
7831
|
-
const
|
|
7832
|
-
|
|
8306
|
+
const run3 = db5.transaction(() => {
|
|
8307
|
+
db5.prepare(`DELETE FROM engagement WHERE session_id IN (${placeholders})`).run(...sessionIds);
|
|
8308
|
+
const result = db5.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
|
|
8309
|
+
return result.changes;
|
|
8310
|
+
});
|
|
8311
|
+
return run3.immediate();
|
|
7833
8312
|
}
|
|
7834
8313
|
async function readAssignmentStatusFromPath(assignmentMdPath) {
|
|
7835
8314
|
if (!await fileExists(assignmentMdPath)) return null;
|
|
@@ -7839,39 +8318,44 @@ async function readAssignmentStatusFromPath(assignmentMdPath) {
|
|
|
7839
8318
|
}
|
|
7840
8319
|
async function readAssignmentStatus(projectDir, assignmentSlug) {
|
|
7841
8320
|
return readAssignmentStatusFromPath(
|
|
7842
|
-
|
|
8321
|
+
resolve15(projectDir, "assignments", assignmentSlug, "assignment.md")
|
|
7843
8322
|
);
|
|
7844
8323
|
}
|
|
7845
8324
|
async function reconcileActiveSessions(projectsDir, assignmentsDir2) {
|
|
7846
8325
|
const db5 = getSessionDb();
|
|
7847
|
-
const activeSessions = db5.prepare(
|
|
8326
|
+
const activeSessions = db5.prepare(
|
|
8327
|
+
`SELECT s.*, e.project_slug AS project_slug, e.assignment_slug AS assignment_slug
|
|
8328
|
+
FROM sessions s
|
|
8329
|
+
JOIN engagement e ON e.session_id = s.session_id AND e.ended_at IS NULL
|
|
8330
|
+
WHERE s.status = 'active' AND e.assignment_slug IS NOT NULL`
|
|
8331
|
+
).all();
|
|
7848
8332
|
if (activeSessions.length === 0) return 0;
|
|
7849
8333
|
const assignmentStatuses = /* @__PURE__ */ new Map();
|
|
7850
8334
|
const seen = /* @__PURE__ */ new Set();
|
|
7851
8335
|
for (const session of activeSessions) {
|
|
7852
8336
|
const aslug = session.assignment_slug;
|
|
7853
8337
|
if (!aslug) continue;
|
|
7854
|
-
const
|
|
7855
|
-
const key = `${
|
|
8338
|
+
const projectKey2 = session.project_slug ?? "__standalone__";
|
|
8339
|
+
const key = `${projectKey2}/${aslug}`;
|
|
7856
8340
|
if (seen.has(key)) continue;
|
|
7857
8341
|
seen.add(key);
|
|
7858
8342
|
if (session.project_slug) {
|
|
7859
8343
|
const status = await readAssignmentStatus(
|
|
7860
|
-
|
|
8344
|
+
resolve15(projectsDir, session.project_slug),
|
|
7861
8345
|
aslug
|
|
7862
8346
|
);
|
|
7863
8347
|
if (status) assignmentStatuses.set(key, status);
|
|
7864
8348
|
} else if (assignmentsDir2) {
|
|
7865
8349
|
const status = await readAssignmentStatusFromPath(
|
|
7866
|
-
|
|
8350
|
+
resolve15(assignmentsDir2, aslug, "assignment.md")
|
|
7867
8351
|
);
|
|
7868
8352
|
if (status) assignmentStatuses.set(key, status);
|
|
7869
8353
|
}
|
|
7870
8354
|
}
|
|
7871
8355
|
let totalUpdated = 0;
|
|
7872
8356
|
for (const session of activeSessions) {
|
|
7873
|
-
const
|
|
7874
|
-
const key = `${
|
|
8357
|
+
const projectKey2 = session.project_slug ?? "__standalone__";
|
|
8358
|
+
const key = `${projectKey2}/${session.assignment_slug}`;
|
|
7875
8359
|
const assignmentStatus = assignmentStatuses.get(key);
|
|
7876
8360
|
if (!assignmentStatus || !DONE_ASSIGNMENT_STATUSES.has(assignmentStatus)) continue;
|
|
7877
8361
|
const newStatus = assignmentStatus === "failed" ? "stopped" : "completed";
|
|
@@ -7883,18 +8367,34 @@ async function reconcileActiveSessions(projectsDir, assignmentsDir2) {
|
|
|
7883
8367
|
async function listSessionsByAssignment(projectSlug, assignmentSlug) {
|
|
7884
8368
|
const db5 = getSessionDb();
|
|
7885
8369
|
const rows = projectSlug === null ? db5.prepare(
|
|
7886
|
-
|
|
8370
|
+
`${SESSION_SELECT_WITH_BINDING} WHERE e.assignment_slug = ? AND e.project_slug IS NULL ORDER BY s.started DESC`
|
|
7887
8371
|
).all(assignmentSlug) : db5.prepare(
|
|
7888
|
-
|
|
8372
|
+
`${SESSION_SELECT_WITH_BINDING} WHERE e.project_slug = ? AND e.assignment_slug = ? ORDER BY s.started DESC`
|
|
7889
8373
|
).all(projectSlug, assignmentSlug);
|
|
7890
8374
|
return rows.map(rowToSession);
|
|
7891
8375
|
}
|
|
7892
|
-
var DONE_ASSIGNMENT_STATUSES;
|
|
8376
|
+
var SESSION_SELECT_WITH_BINDING, SessionResurrectionError, DONE_ASSIGNMENT_STATUSES;
|
|
7893
8377
|
var init_agent_sessions = __esm({
|
|
7894
8378
|
"src/dashboard/agent-sessions.ts"() {
|
|
7895
8379
|
"use strict";
|
|
7896
8380
|
init_fs();
|
|
7897
8381
|
init_session_db();
|
|
8382
|
+
init_engagement_db();
|
|
8383
|
+
init_engagement_tokens();
|
|
8384
|
+
SESSION_SELECT_WITH_BINDING = `
|
|
8385
|
+
SELECT s.*,
|
|
8386
|
+
e.project_slug AS project_slug,
|
|
8387
|
+
e.assignment_slug AS assignment_slug,
|
|
8388
|
+
e.assignment_id AS assignment_id
|
|
8389
|
+
FROM sessions s
|
|
8390
|
+
LEFT JOIN engagement e ON e.id = (
|
|
8391
|
+
SELECT e2.id FROM engagement e2
|
|
8392
|
+
WHERE e2.session_id = s.session_id
|
|
8393
|
+
ORDER BY (e2.ended_at IS NULL) DESC, e2.started_at DESC, e2.id DESC
|
|
8394
|
+
LIMIT 1
|
|
8395
|
+
)`;
|
|
8396
|
+
SessionResurrectionError = class extends Error {
|
|
8397
|
+
};
|
|
7898
8398
|
DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
|
|
7899
8399
|
}
|
|
7900
8400
|
});
|
|
@@ -7987,7 +8487,7 @@ var init_classify = __esm({
|
|
|
7987
8487
|
|
|
7988
8488
|
// src/dashboard/servers.ts
|
|
7989
8489
|
import { readdir as readdir9, readFile as readFile12, unlink as unlink2 } from "fs/promises";
|
|
7990
|
-
import { resolve as
|
|
8490
|
+
import { resolve as resolve16 } from "path";
|
|
7991
8491
|
function sanitizeSessionName(name) {
|
|
7992
8492
|
return name.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
7993
8493
|
}
|
|
@@ -8035,7 +8535,7 @@ async function registerSession(dir, rawName) {
|
|
|
8035
8535
|
lastRefreshed: now,
|
|
8036
8536
|
overrides: {}
|
|
8037
8537
|
});
|
|
8038
|
-
await writeFileForce(
|
|
8538
|
+
await writeFileForce(resolve16(dir, `${name}.md`), content);
|
|
8039
8539
|
return name;
|
|
8040
8540
|
}
|
|
8041
8541
|
async function listSessionFiles(dir) {
|
|
@@ -8044,7 +8544,7 @@ async function listSessionFiles(dir) {
|
|
|
8044
8544
|
return entries.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
|
|
8045
8545
|
}
|
|
8046
8546
|
async function readSessionFile(dir, name) {
|
|
8047
|
-
const filePath =
|
|
8547
|
+
const filePath = resolve16(dir, `${sanitizeSessionName(name)}.md`);
|
|
8048
8548
|
if (!await fileExists(filePath)) return null;
|
|
8049
8549
|
const raw2 = await readFile12(filePath, "utf-8");
|
|
8050
8550
|
const [frontmatter] = extractFrontmatter2(raw2);
|
|
@@ -8086,7 +8586,7 @@ async function readSessionFile(dir, name) {
|
|
|
8086
8586
|
};
|
|
8087
8587
|
}
|
|
8088
8588
|
async function removeSession(dir, name) {
|
|
8089
|
-
const filePath =
|
|
8589
|
+
const filePath = resolve16(dir, `${sanitizeSessionName(name)}.md`);
|
|
8090
8590
|
if (await fileExists(filePath)) {
|
|
8091
8591
|
await unlink2(filePath);
|
|
8092
8592
|
}
|
|
@@ -8095,7 +8595,7 @@ async function updateLastRefreshed(dir, name) {
|
|
|
8095
8595
|
const data = await readSessionFile(dir, name);
|
|
8096
8596
|
if (!data) return;
|
|
8097
8597
|
const content = buildSessionContent({ ...data, lastRefreshed: nowTimestamp2() });
|
|
8098
|
-
await writeFileForce(
|
|
8598
|
+
await writeFileForce(resolve16(dir, `${sanitizeSessionName(name)}.md`), content);
|
|
8099
8599
|
}
|
|
8100
8600
|
async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment) {
|
|
8101
8601
|
const data = await readSessionFile(dir, sessionName);
|
|
@@ -8107,7 +8607,7 @@ async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment)
|
|
|
8107
8607
|
delete data.overrides[key];
|
|
8108
8608
|
}
|
|
8109
8609
|
const content = buildSessionContent({ ...data });
|
|
8110
|
-
await writeFileForce(
|
|
8610
|
+
await writeFileForce(resolve16(dir, `${sanitizeSessionName(sessionName)}.md`), content);
|
|
8111
8611
|
}
|
|
8112
8612
|
async function registerAutoSession(dir, rawName, opts) {
|
|
8113
8613
|
const name = sanitizeSessionName(rawName);
|
|
@@ -8124,7 +8624,7 @@ async function registerAutoSession(dir, rawName, opts) {
|
|
|
8124
8624
|
ports: opts.ports,
|
|
8125
8625
|
cwd: opts.cwd
|
|
8126
8626
|
});
|
|
8127
|
-
await writeFileForce(
|
|
8627
|
+
await writeFileForce(resolve16(dir, `${name}.md`), content);
|
|
8128
8628
|
return name;
|
|
8129
8629
|
}
|
|
8130
8630
|
var init_servers = __esm({
|
|
@@ -8159,7 +8659,7 @@ __export(scanner_exports, {
|
|
|
8159
8659
|
});
|
|
8160
8660
|
import { execFile } from "child_process";
|
|
8161
8661
|
import { promisify } from "util";
|
|
8162
|
-
import { resolve as
|
|
8662
|
+
import { resolve as resolve17 } from "path";
|
|
8163
8663
|
import { realpath } from "fs/promises";
|
|
8164
8664
|
function clearScanCache() {
|
|
8165
8665
|
cache2 = null;
|
|
@@ -8175,8 +8675,8 @@ function scanKey(serversDir2, projectsDir, assignmentsDir2) {
|
|
|
8175
8675
|
return `${serversDir2}\0${projectsDir}\0${assignmentsDir2 ?? ""}`;
|
|
8176
8676
|
}
|
|
8177
8677
|
function delay(ms) {
|
|
8178
|
-
return new Promise((
|
|
8179
|
-
const timer3 = setTimeout(
|
|
8678
|
+
return new Promise((resolve49) => {
|
|
8679
|
+
const timer3 = setTimeout(resolve49, ms);
|
|
8180
8680
|
if (typeof timer3.unref === "function") {
|
|
8181
8681
|
timer3.unref();
|
|
8182
8682
|
}
|
|
@@ -8312,8 +8812,8 @@ async function getGitInfo(cwd) {
|
|
|
8312
8812
|
let isWorktree = false;
|
|
8313
8813
|
if (commonDir && gitDir && commonDir !== gitDir) {
|
|
8314
8814
|
try {
|
|
8315
|
-
const resolvedCommon = await realpath(
|
|
8316
|
-
const resolvedGit = await realpath(
|
|
8815
|
+
const resolvedCommon = await realpath(resolve17(cwd, commonDir));
|
|
8816
|
+
const resolvedGit = await realpath(resolve17(cwd, gitDir));
|
|
8317
8817
|
isWorktree = resolvedCommon !== resolvedGit;
|
|
8318
8818
|
} catch {
|
|
8319
8819
|
isWorktree = false;
|
|
@@ -8415,16 +8915,16 @@ async function scanSession(sessionData, lsofOutput, workspaceRecords, procSnapsh
|
|
|
8415
8915
|
const gitInfo = gitInfoCache.get(rp.cwd) ?? { branch: null, worktree: false };
|
|
8416
8916
|
const ports = panePorts.get(key) ?? [];
|
|
8417
8917
|
const urls = ports.map((p) => `http://localhost:${p}`);
|
|
8418
|
-
const
|
|
8918
|
+
const override3 = sessionData.overrides[key];
|
|
8419
8919
|
let assignment = null;
|
|
8420
|
-
if (
|
|
8920
|
+
if (override3) {
|
|
8421
8921
|
const rec = workspaceRecords.find(
|
|
8422
|
-
(r) => r.projectSlug ===
|
|
8922
|
+
(r) => r.projectSlug === override3.project && r.assignmentSlug === override3.assignment
|
|
8423
8923
|
);
|
|
8424
8924
|
assignment = {
|
|
8425
|
-
project:
|
|
8426
|
-
slug:
|
|
8427
|
-
title: rec?.assignmentTitle ??
|
|
8925
|
+
project: override3.project,
|
|
8926
|
+
slug: override3.assignment,
|
|
8927
|
+
title: rec?.assignmentTitle ?? override3.assignment
|
|
8428
8928
|
};
|
|
8429
8929
|
} else {
|
|
8430
8930
|
assignment = await autoLinkPane(rp.cwd, gitInfo.branch, workspaceRecords);
|
|
@@ -8478,16 +8978,16 @@ async function scanProcessSession(sessionData, lsofOutput, workspaceRecords) {
|
|
|
8478
8978
|
}
|
|
8479
8979
|
const ports = findListeningPorts(lsofOutput, /* @__PURE__ */ new Set([sessionData.pid]));
|
|
8480
8980
|
const gitInfo = await getGitInfo(sessionData.cwd);
|
|
8481
|
-
const
|
|
8981
|
+
const override3 = sessionData.overrides["0:0"];
|
|
8482
8982
|
let assignment = null;
|
|
8483
|
-
if (
|
|
8983
|
+
if (override3) {
|
|
8484
8984
|
const rec = workspaceRecords.find(
|
|
8485
|
-
(r) => r.projectSlug ===
|
|
8985
|
+
(r) => r.projectSlug === override3.project && r.assignmentSlug === override3.assignment
|
|
8486
8986
|
);
|
|
8487
8987
|
assignment = {
|
|
8488
|
-
project:
|
|
8489
|
-
slug:
|
|
8490
|
-
title: rec?.assignmentTitle ??
|
|
8988
|
+
project: override3.project,
|
|
8989
|
+
slug: override3.assignment,
|
|
8990
|
+
title: rec?.assignmentTitle ?? override3.assignment
|
|
8491
8991
|
};
|
|
8492
8992
|
} else {
|
|
8493
8993
|
assignment = await autoLinkPane(sessionData.cwd, gitInfo.branch, workspaceRecords);
|
|
@@ -8651,7 +9151,7 @@ __export(api_exports, {
|
|
|
8651
9151
|
resolveWorkspaceMembers: () => resolveWorkspaceMembers
|
|
8652
9152
|
});
|
|
8653
9153
|
import { readdir as readdir10, readFile as readFile13, writeFile as writeFile3, stat as stat2 } from "fs/promises";
|
|
8654
|
-
import { resolve as
|
|
9154
|
+
import { resolve as resolve18, dirname as dirname2, basename } from "path";
|
|
8655
9155
|
function clearFrontmatterField(content, key) {
|
|
8656
9156
|
const fieldRegex = new RegExp(`^(${escapeRegExp2(key)}:)\\s*.*$`, "m");
|
|
8657
9157
|
return content.replace(fieldRegex, `$1 null`);
|
|
@@ -8735,8 +9235,8 @@ async function computeStandaloneRecords(assignmentsDir2) {
|
|
|
8735
9235
|
const records = [];
|
|
8736
9236
|
for (const entry of entries) {
|
|
8737
9237
|
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
|
|
8738
|
-
const assignmentDir =
|
|
8739
|
-
const assignmentMdPath =
|
|
9238
|
+
const assignmentDir = resolve18(assignmentsDir2, entry.name);
|
|
9239
|
+
const assignmentMdPath = resolve18(assignmentDir, "assignment.md");
|
|
8740
9240
|
if (!await fileExists(assignmentMdPath)) continue;
|
|
8741
9241
|
try {
|
|
8742
9242
|
const content = await readFile13(assignmentMdPath, "utf-8");
|
|
@@ -8823,7 +9323,7 @@ async function listProjects(projectsDir) {
|
|
|
8823
9323
|
return projectRecords.filter((record) => !isProjectArchived(record.summary)).map((record) => record.summary);
|
|
8824
9324
|
}
|
|
8825
9325
|
async function readWorkspaceRegistry(projectsDir) {
|
|
8826
|
-
const registryPath =
|
|
9326
|
+
const registryPath = resolve18(dirname2(projectsDir), "workspaces.json");
|
|
8827
9327
|
try {
|
|
8828
9328
|
const raw2 = await readFile13(registryPath, "utf-8");
|
|
8829
9329
|
const parsed = JSON.parse(raw2);
|
|
@@ -8833,7 +9333,7 @@ async function readWorkspaceRegistry(projectsDir) {
|
|
|
8833
9333
|
}
|
|
8834
9334
|
}
|
|
8835
9335
|
async function writeWorkspaceRegistry(projectsDir, workspaces) {
|
|
8836
|
-
const registryPath =
|
|
9336
|
+
const registryPath = resolve18(dirname2(projectsDir), "workspaces.json");
|
|
8837
9337
|
await writeFile3(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
|
|
8838
9338
|
}
|
|
8839
9339
|
async function listWorkspaces(projectsDir, assignmentsDir2) {
|
|
@@ -8924,7 +9424,7 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
|
|
|
8924
9424
|
if (cascade) {
|
|
8925
9425
|
const timestamp = nowTimestamp();
|
|
8926
9426
|
for (const slug of projectsReferencing) {
|
|
8927
|
-
const path =
|
|
9427
|
+
const path = resolve18(projectsDir, slug, "project.md");
|
|
8928
9428
|
const raw2 = await readFile13(path, "utf-8");
|
|
8929
9429
|
let next = clearFrontmatterField(raw2, "workspace");
|
|
8930
9430
|
next = setUpdatedField(next, timestamp);
|
|
@@ -8933,7 +9433,7 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
|
|
|
8933
9433
|
}
|
|
8934
9434
|
for (const id of standalonesReferencing) {
|
|
8935
9435
|
if (!opts.assignmentsDir) break;
|
|
8936
|
-
const path =
|
|
9436
|
+
const path = resolve18(opts.assignmentsDir, id, "assignment.md");
|
|
8937
9437
|
const raw2 = await readFile13(path, "utf-8");
|
|
8938
9438
|
let next = clearFrontmatterField(raw2, "workspaceGroup");
|
|
8939
9439
|
next = setUpdatedField(next, timestamp);
|
|
@@ -9212,7 +9712,7 @@ async function getEditableDocumentById(projectsDir, assignmentsDir2, documentTyp
|
|
|
9212
9712
|
}
|
|
9213
9713
|
const fileName = documentType === "assignment" ? "assignment.md" : documentType === "plan" ? "plan.md" : documentType === "scratchpad" ? "scratchpad.md" : documentType === "handoff" ? "handoff.md" : documentType === "decision-record" ? "decision-record.md" : null;
|
|
9214
9714
|
if (!fileName) return null;
|
|
9215
|
-
const filePath =
|
|
9715
|
+
const filePath = resolve18(resolved.assignmentDir, fileName);
|
|
9216
9716
|
if (!await fileExists(filePath)) return null;
|
|
9217
9717
|
const content = await readFile13(filePath, "utf-8");
|
|
9218
9718
|
const label = resolved.id;
|
|
@@ -9228,8 +9728,8 @@ async function getEditableDocumentById(projectsDir, assignmentsDir2, documentTyp
|
|
|
9228
9728
|
};
|
|
9229
9729
|
}
|
|
9230
9730
|
async function getProjectDetail(projectsDir, slug) {
|
|
9231
|
-
const projectPath =
|
|
9232
|
-
const projectMdPath =
|
|
9731
|
+
const projectPath = resolve18(projectsDir, slug);
|
|
9732
|
+
const projectMdPath = resolve18(projectPath, "project.md");
|
|
9233
9733
|
if (!await fileExists(projectMdPath)) {
|
|
9234
9734
|
return null;
|
|
9235
9735
|
}
|
|
@@ -9266,15 +9766,15 @@ async function getProjectDetail(projectsDir, slug) {
|
|
|
9266
9766
|
};
|
|
9267
9767
|
}
|
|
9268
9768
|
async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
9269
|
-
const assignmentDir =
|
|
9270
|
-
const assignmentMdPath =
|
|
9769
|
+
const assignmentDir = resolve18(projectsDir, projectSlug, "assignments", assignmentSlug);
|
|
9770
|
+
const assignmentMdPath = resolve18(assignmentDir, "assignment.md");
|
|
9271
9771
|
if (!await fileExists(assignmentMdPath)) {
|
|
9272
9772
|
return null;
|
|
9273
9773
|
}
|
|
9274
9774
|
const assignmentContent = await readFile13(assignmentMdPath, "utf-8");
|
|
9275
9775
|
const assignment = parseAssignmentFull(assignmentContent);
|
|
9276
9776
|
let projectWorkspace = null;
|
|
9277
|
-
const projectMdPath =
|
|
9777
|
+
const projectMdPath = resolve18(projectsDir, projectSlug, "project.md");
|
|
9278
9778
|
if (await fileExists(projectMdPath)) {
|
|
9279
9779
|
const projectContent = await readFile13(projectMdPath, "utf-8");
|
|
9280
9780
|
projectWorkspace = parseProject(projectContent).workspace;
|
|
@@ -9282,7 +9782,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
9282
9782
|
let plan = null;
|
|
9283
9783
|
const planFile = await latestPlanFile(assignmentDir);
|
|
9284
9784
|
if (planFile) {
|
|
9285
|
-
const planPath =
|
|
9785
|
+
const planPath = resolve18(assignmentDir, planFile);
|
|
9286
9786
|
if (await fileExists(planPath)) {
|
|
9287
9787
|
const planContent = await readFile13(planPath, "utf-8");
|
|
9288
9788
|
const parsed = parsePlan(planContent);
|
|
@@ -9294,7 +9794,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
9294
9794
|
}
|
|
9295
9795
|
}
|
|
9296
9796
|
let scratchpad = null;
|
|
9297
|
-
const scratchpadPath =
|
|
9797
|
+
const scratchpadPath = resolve18(assignmentDir, "scratchpad.md");
|
|
9298
9798
|
if (await fileExists(scratchpadPath)) {
|
|
9299
9799
|
const scratchpadContent = await readFile13(scratchpadPath, "utf-8");
|
|
9300
9800
|
const parsed = parseScratchpad(scratchpadContent);
|
|
@@ -9304,7 +9804,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
9304
9804
|
};
|
|
9305
9805
|
}
|
|
9306
9806
|
let handoff = null;
|
|
9307
|
-
const handoffPath =
|
|
9807
|
+
const handoffPath = resolve18(assignmentDir, "handoff.md");
|
|
9308
9808
|
if (await fileExists(handoffPath)) {
|
|
9309
9809
|
const handoffContent = await readFile13(handoffPath, "utf-8");
|
|
9310
9810
|
const parsed = parseHandoff(handoffContent);
|
|
@@ -9315,7 +9815,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
9315
9815
|
};
|
|
9316
9816
|
}
|
|
9317
9817
|
let decisionRecord = null;
|
|
9318
|
-
const decisionRecordPath =
|
|
9818
|
+
const decisionRecordPath = resolve18(assignmentDir, "decision-record.md");
|
|
9319
9819
|
if (await fileExists(decisionRecordPath)) {
|
|
9320
9820
|
const decisionRecordContent = await readFile13(decisionRecordPath, "utf-8");
|
|
9321
9821
|
const parsed = parseDecisionRecord(decisionRecordContent);
|
|
@@ -9326,7 +9826,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
9326
9826
|
};
|
|
9327
9827
|
}
|
|
9328
9828
|
let progress = null;
|
|
9329
|
-
const progressPath =
|
|
9829
|
+
const progressPath = resolve18(assignmentDir, "progress.md");
|
|
9330
9830
|
if (await fileExists(progressPath)) {
|
|
9331
9831
|
const progressContent = await readFile13(progressPath, "utf-8");
|
|
9332
9832
|
const parsed = parseProgress(progressContent);
|
|
@@ -9337,7 +9837,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
9337
9837
|
};
|
|
9338
9838
|
}
|
|
9339
9839
|
let comments = null;
|
|
9340
|
-
const commentsPath =
|
|
9840
|
+
const commentsPath = resolve18(assignmentDir, "comments.md");
|
|
9341
9841
|
if (await fileExists(commentsPath)) {
|
|
9342
9842
|
const commentsContent = await readFile13(commentsPath, "utf-8");
|
|
9343
9843
|
const parsed = parseComments(commentsContent);
|
|
@@ -9371,7 +9871,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
9371
9871
|
archivedReason: assignment.archivedReason,
|
|
9372
9872
|
...deriveStatusVirtuals(assignment, terminalStatuses),
|
|
9373
9873
|
override: assignment.override,
|
|
9374
|
-
derived: await buildDerivedDetail(assignment, assignmentDir,
|
|
9874
|
+
derived: await buildDerivedDetail(assignment, assignmentDir, resolve18(projectsDir, projectSlug)),
|
|
9375
9875
|
created: assignment.created,
|
|
9376
9876
|
updated: assignment.updated,
|
|
9377
9877
|
body: assignment.body,
|
|
@@ -9462,7 +9962,7 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir2) {
|
|
|
9462
9962
|
slug: a.slug,
|
|
9463
9963
|
title: a.title,
|
|
9464
9964
|
projectSlug: rec.summary.slug,
|
|
9465
|
-
assignmentDir:
|
|
9965
|
+
assignmentDir: resolve18(rec.projectPath, "assignments", a.slug)
|
|
9466
9966
|
});
|
|
9467
9967
|
}
|
|
9468
9968
|
}
|
|
@@ -9495,14 +9995,14 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir2) {
|
|
|
9495
9995
|
}
|
|
9496
9996
|
async function countMentionsInAssignment(sourceDir, target) {
|
|
9497
9997
|
const bodies = [];
|
|
9498
|
-
const assignmentMd =
|
|
9998
|
+
const assignmentMd = resolve18(sourceDir, "assignment.md");
|
|
9499
9999
|
if (await fileExists(assignmentMd)) {
|
|
9500
10000
|
const content = await readFile13(assignmentMd, "utf-8");
|
|
9501
10001
|
const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
|
|
9502
10002
|
if (todosMatch) bodies.push(todosMatch[1]);
|
|
9503
10003
|
}
|
|
9504
10004
|
for (const filename of ["progress.md", "comments.md", "handoff.md"]) {
|
|
9505
|
-
const path =
|
|
10005
|
+
const path = resolve18(sourceDir, filename);
|
|
9506
10006
|
if (await fileExists(path)) {
|
|
9507
10007
|
try {
|
|
9508
10008
|
bodies.push(await readFile13(path, "utf-8"));
|
|
@@ -9563,45 +10063,45 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir2, id) {
|
|
|
9563
10063
|
}
|
|
9564
10064
|
async function buildStandaloneAssignmentDetail(resolved) {
|
|
9565
10065
|
const assignmentDir = resolved.assignmentDir;
|
|
9566
|
-
const assignmentMdPath =
|
|
10066
|
+
const assignmentMdPath = resolve18(assignmentDir, "assignment.md");
|
|
9567
10067
|
if (!await fileExists(assignmentMdPath)) return null;
|
|
9568
10068
|
const assignmentContent = await readFile13(assignmentMdPath, "utf-8");
|
|
9569
10069
|
const assignment = parseAssignmentFull(assignmentContent);
|
|
9570
10070
|
let plan = null;
|
|
9571
10071
|
const planFile = await latestPlanFile(assignmentDir);
|
|
9572
10072
|
if (planFile) {
|
|
9573
|
-
const planPath =
|
|
10073
|
+
const planPath = resolve18(assignmentDir, planFile);
|
|
9574
10074
|
if (await fileExists(planPath)) {
|
|
9575
10075
|
const parsed = parsePlan(await readFile13(planPath, "utf-8"));
|
|
9576
10076
|
plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
|
|
9577
10077
|
}
|
|
9578
10078
|
}
|
|
9579
10079
|
let scratchpad = null;
|
|
9580
|
-
const scratchpadPath =
|
|
10080
|
+
const scratchpadPath = resolve18(assignmentDir, "scratchpad.md");
|
|
9581
10081
|
if (await fileExists(scratchpadPath)) {
|
|
9582
10082
|
const parsed = parseScratchpad(await readFile13(scratchpadPath, "utf-8"));
|
|
9583
10083
|
scratchpad = { updated: parsed.updated, body: parsed.body };
|
|
9584
10084
|
}
|
|
9585
10085
|
let handoff = null;
|
|
9586
|
-
const handoffPath =
|
|
10086
|
+
const handoffPath = resolve18(assignmentDir, "handoff.md");
|
|
9587
10087
|
if (await fileExists(handoffPath)) {
|
|
9588
10088
|
const parsed = parseHandoff(await readFile13(handoffPath, "utf-8"));
|
|
9589
10089
|
handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
|
|
9590
10090
|
}
|
|
9591
10091
|
let decisionRecord = null;
|
|
9592
|
-
const decisionRecordPath =
|
|
10092
|
+
const decisionRecordPath = resolve18(assignmentDir, "decision-record.md");
|
|
9593
10093
|
if (await fileExists(decisionRecordPath)) {
|
|
9594
10094
|
const parsed = parseDecisionRecord(await readFile13(decisionRecordPath, "utf-8"));
|
|
9595
10095
|
decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
|
|
9596
10096
|
}
|
|
9597
10097
|
let progress = null;
|
|
9598
|
-
const progressPath =
|
|
10098
|
+
const progressPath = resolve18(assignmentDir, "progress.md");
|
|
9599
10099
|
if (await fileExists(progressPath)) {
|
|
9600
10100
|
const parsed = parseProgress(await readFile13(progressPath, "utf-8"));
|
|
9601
10101
|
progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
|
|
9602
10102
|
}
|
|
9603
10103
|
let comments = null;
|
|
9604
|
-
const commentsPath =
|
|
10104
|
+
const commentsPath = resolve18(assignmentDir, "comments.md");
|
|
9605
10105
|
if (await fileExists(commentsPath)) {
|
|
9606
10106
|
const parsed = parseComments(await readFile13(commentsPath, "utf-8"));
|
|
9607
10107
|
comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
|
|
@@ -9667,8 +10167,8 @@ async function computeProjectRecords(projectsDir, traces) {
|
|
|
9667
10167
|
const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
|
|
9668
10168
|
const maybeRecords = await Promise.all(
|
|
9669
10169
|
projectDirs.map(async (entry) => {
|
|
9670
|
-
const projectPath =
|
|
9671
|
-
const projectMdPath =
|
|
10170
|
+
const projectPath = resolve18(projectsDir, entry.name);
|
|
10171
|
+
const projectMdPath = resolve18(projectPath, "project.md");
|
|
9672
10172
|
if (!await fileExists(projectMdPath)) {
|
|
9673
10173
|
return null;
|
|
9674
10174
|
}
|
|
@@ -9715,7 +10215,7 @@ async function computeProjectRecords(projectsDir, traces) {
|
|
|
9715
10215
|
return records;
|
|
9716
10216
|
}
|
|
9717
10217
|
async function listAssignmentRecords(projectPath, traces) {
|
|
9718
|
-
const assignmentsDir2 =
|
|
10218
|
+
const assignmentsDir2 = resolve18(projectPath, "assignments");
|
|
9719
10219
|
if (!await fileExists(assignmentsDir2)) {
|
|
9720
10220
|
return [];
|
|
9721
10221
|
}
|
|
@@ -9723,7 +10223,7 @@ async function listAssignmentRecords(projectPath, traces) {
|
|
|
9723
10223
|
const dirEntries = entries.filter((entry) => entry.isDirectory());
|
|
9724
10224
|
const maybeRecords = await Promise.all(
|
|
9725
10225
|
dirEntries.map(async (entry) => {
|
|
9726
|
-
const assignmentMd =
|
|
10226
|
+
const assignmentMd = resolve18(assignmentsDir2, entry.name, "assignment.md");
|
|
9727
10227
|
if (!await fileExists(assignmentMd)) {
|
|
9728
10228
|
return null;
|
|
9729
10229
|
}
|
|
@@ -9739,7 +10239,7 @@ async function listAssignmentRecords(projectPath, traces) {
|
|
|
9739
10239
|
return records;
|
|
9740
10240
|
}
|
|
9741
10241
|
async function listResources(projectPath) {
|
|
9742
|
-
const resourcesDir =
|
|
10242
|
+
const resourcesDir = resolve18(projectPath, "resources");
|
|
9743
10243
|
if (!await fileExists(resourcesDir)) {
|
|
9744
10244
|
return [];
|
|
9745
10245
|
}
|
|
@@ -9749,7 +10249,7 @@ async function listResources(projectPath) {
|
|
|
9749
10249
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
|
|
9750
10250
|
continue;
|
|
9751
10251
|
}
|
|
9752
|
-
const filePath =
|
|
10252
|
+
const filePath = resolve18(resourcesDir, entry.name);
|
|
9753
10253
|
const content = await readFile13(filePath, "utf-8");
|
|
9754
10254
|
const parsed = parseResource(content);
|
|
9755
10255
|
results.push({
|
|
@@ -9765,7 +10265,7 @@ async function listResources(projectPath) {
|
|
|
9765
10265
|
return results;
|
|
9766
10266
|
}
|
|
9767
10267
|
async function listMemories(projectPath) {
|
|
9768
|
-
const memoriesDir =
|
|
10268
|
+
const memoriesDir = resolve18(projectPath, "memories");
|
|
9769
10269
|
if (!await fileExists(memoriesDir)) {
|
|
9770
10270
|
return [];
|
|
9771
10271
|
}
|
|
@@ -9775,7 +10275,7 @@ async function listMemories(projectPath) {
|
|
|
9775
10275
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
|
|
9776
10276
|
continue;
|
|
9777
10277
|
}
|
|
9778
|
-
const filePath =
|
|
10278
|
+
const filePath = resolve18(memoriesDir, entry.name);
|
|
9779
10279
|
const content = await readFile13(filePath, "utf-8");
|
|
9780
10280
|
const parsed = parseMemory(content);
|
|
9781
10281
|
results.push({
|
|
@@ -9824,8 +10324,8 @@ async function listAllResources(projectsDir) {
|
|
|
9824
10324
|
return all;
|
|
9825
10325
|
}
|
|
9826
10326
|
async function resolveProjectPath(projectsDir, projectSlug) {
|
|
9827
|
-
const direct =
|
|
9828
|
-
if (await fileExists(
|
|
10327
|
+
const direct = resolve18(projectsDir, projectSlug);
|
|
10328
|
+
if (await fileExists(resolve18(direct, "project.md"))) return direct;
|
|
9829
10329
|
const records = await listProjectRecords(projectsDir);
|
|
9830
10330
|
const match = records.find((r) => r.summary.slug === projectSlug);
|
|
9831
10331
|
return match ? match.projectPath : null;
|
|
@@ -9837,7 +10337,7 @@ async function getMemoryDetail(projectsDir, projectSlug, itemSlug) {
|
|
|
9837
10337
|
(p) => basename(p.projectPath) === projectSlug || p.summary.slug === projectSlug
|
|
9838
10338
|
);
|
|
9839
10339
|
if (!projectRecord) return null;
|
|
9840
|
-
const filePath =
|
|
10340
|
+
const filePath = resolve18(projectRecord.projectPath, "memories", `${itemSlug}.md`);
|
|
9841
10341
|
if (!await fileExists(filePath)) return null;
|
|
9842
10342
|
const content = await readFile13(filePath, "utf-8");
|
|
9843
10343
|
const parsed = parseMemory(content);
|
|
@@ -9863,7 +10363,7 @@ async function getResourceDetail(projectsDir, projectSlug, itemSlug) {
|
|
|
9863
10363
|
(p) => basename(p.projectPath) === projectSlug || p.summary.slug === projectSlug
|
|
9864
10364
|
);
|
|
9865
10365
|
if (!projectRecord) return null;
|
|
9866
|
-
const filePath =
|
|
10366
|
+
const filePath = resolve18(projectRecord.projectPath, "resources", `${itemSlug}.md`);
|
|
9867
10367
|
if (!await fileExists(filePath)) return null;
|
|
9868
10368
|
const content = await readFile13(filePath, "utf-8");
|
|
9869
10369
|
const parsed = parseResource(content);
|
|
@@ -9881,7 +10381,7 @@ async function getResourceDetail(projectsDir, projectSlug, itemSlug) {
|
|
|
9881
10381
|
};
|
|
9882
10382
|
}
|
|
9883
10383
|
async function loadDependencyGraph(projectPath, assignments) {
|
|
9884
|
-
const statusPath =
|
|
10384
|
+
const statusPath = resolve18(projectPath, "_status.md");
|
|
9885
10385
|
if (await fileExists(statusPath)) {
|
|
9886
10386
|
const statusContent = await readFile13(statusPath, "utf-8");
|
|
9887
10387
|
const parsed = parseStatus(statusContent);
|
|
@@ -10050,7 +10550,7 @@ function toAssignmentSummary(assignment, terminalStatuses) {
|
|
|
10050
10550
|
async function toAssignmentBoardItem(projectsDir, projectRecord, assignment) {
|
|
10051
10551
|
const config = await getStatusConfig();
|
|
10052
10552
|
const { terminalStatuses } = config;
|
|
10053
|
-
const assignmentDir =
|
|
10553
|
+
const assignmentDir = resolve18(projectRecord.projectPath, "assignments", assignment.slug);
|
|
10054
10554
|
const projectDir = projectRecord.projectPath;
|
|
10055
10555
|
let facts;
|
|
10056
10556
|
try {
|
|
@@ -10111,7 +10611,7 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
|
|
|
10111
10611
|
const config = await getStatusConfig();
|
|
10112
10612
|
const transitionDefs = getTransitionDefinitions(config);
|
|
10113
10613
|
const actions = [];
|
|
10114
|
-
const projectPath =
|
|
10614
|
+
const projectPath = resolve18(projectsDir, projectSlug);
|
|
10115
10615
|
const traces = options?.traces;
|
|
10116
10616
|
for (const definition of transitionDefs) {
|
|
10117
10617
|
const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);
|
|
@@ -10159,7 +10659,7 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses, de
|
|
|
10159
10659
|
continue;
|
|
10160
10660
|
}
|
|
10161
10661
|
}
|
|
10162
|
-
const dependencyPath =
|
|
10662
|
+
const dependencyPath = resolve18(projectPath, "assignments", dependency, "assignment.md");
|
|
10163
10663
|
if (!await fileExists(dependencyPath)) {
|
|
10164
10664
|
unmet.push(`${dependency} (missing)`);
|
|
10165
10665
|
continue;
|
|
@@ -10239,13 +10739,13 @@ async function collectStaleCandidates(projectsDir, assignmentsDir2) {
|
|
|
10239
10739
|
const out = [];
|
|
10240
10740
|
for (const record of projectRecords) {
|
|
10241
10741
|
if (isProjectArchived(record.summary)) continue;
|
|
10242
|
-
const projectPath =
|
|
10742
|
+
const projectPath = resolve18(projectsDir, record.summary.slug);
|
|
10243
10743
|
const depMap = /* @__PURE__ */ new Map();
|
|
10244
10744
|
for (const a of record.assignments) depMap.set(a.slug, a.status);
|
|
10245
10745
|
for (const assignment of activeAssignments(record.assignments)) {
|
|
10246
10746
|
const depsSatisfied = assignment.dependsOn.length === 0 ? true : (await getUnmetDependencies(projectPath, assignment.dependsOn, terminalStatuses, depMap)).length === 0;
|
|
10247
10747
|
const lastActivityMs = await readProgressActivityMs(
|
|
10248
|
-
|
|
10748
|
+
resolve18(projectPath, "assignments", assignment.slug, "progress.md"),
|
|
10249
10749
|
now
|
|
10250
10750
|
);
|
|
10251
10751
|
const reasons = classifyAssignmentRecord(assignment, terminalStatuses, depsSatisfied, lastActivityMs, thresholds);
|
|
@@ -10256,7 +10756,7 @@ async function collectStaleCandidates(projectsDir, assignmentsDir2) {
|
|
|
10256
10756
|
}
|
|
10257
10757
|
for (const sr of standaloneRecords) {
|
|
10258
10758
|
if (sr.record.archived === true) continue;
|
|
10259
|
-
const lastActivityMs = await readProgressActivityMs(
|
|
10759
|
+
const lastActivityMs = await readProgressActivityMs(resolve18(sr.assignmentDir, "progress.md"), now);
|
|
10260
10760
|
const reasons = classifyAssignmentRecord(sr.record, terminalStatuses, true, lastActivityMs, thresholds);
|
|
10261
10761
|
if (reasons.length > 0) out.push({ assignmentId: sr.record.id, projectSlug: null, reasons });
|
|
10262
10762
|
}
|
|
@@ -10275,7 +10775,7 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
|
|
|
10275
10775
|
depMap.set(a.slug, a.status);
|
|
10276
10776
|
}
|
|
10277
10777
|
const visibleAssignments = activeAssignments(record.assignments);
|
|
10278
|
-
const projectPath =
|
|
10778
|
+
const projectPath = resolve18(projectsDir, record.summary.slug);
|
|
10279
10779
|
const resolvedTransitions = await Promise.all(
|
|
10280
10780
|
visibleAssignments.map(async (assignment) => {
|
|
10281
10781
|
const t0 = traces ? performance.now() : 0;
|
|
@@ -10289,7 +10789,7 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
|
|
|
10289
10789
|
if (traces) accumulatePhase(traces, "get-available-transitions", performance.now() - t0);
|
|
10290
10790
|
const depsSatisfied = assignment.dependsOn.length === 0 ? true : (await getUnmetDependencies(projectPath, assignment.dependsOn, terminalStatuses, depMap)).length === 0;
|
|
10291
10791
|
const lastActivityMs = await readProgressActivityMs(
|
|
10292
|
-
|
|
10792
|
+
resolve18(projectPath, "assignments", assignment.slug, "progress.md"),
|
|
10293
10793
|
now
|
|
10294
10794
|
);
|
|
10295
10795
|
return { assignment, availableTransitions, depsSatisfied, lastActivityMs };
|
|
@@ -10363,7 +10863,7 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
|
|
|
10363
10863
|
const t0 = traces ? performance.now() : 0;
|
|
10364
10864
|
const availableTransitions = await getStandaloneAvailableTransitions(sr.record);
|
|
10365
10865
|
if (traces) accumulatePhase(traces, "get-available-transitions", performance.now() - t0);
|
|
10366
|
-
const lastActivityMs = await readProgressActivityMs(
|
|
10866
|
+
const lastActivityMs = await readProgressActivityMs(resolve18(sr.assignmentDir, "progress.md"), now);
|
|
10367
10867
|
return { sr, availableTransitions, lastActivityMs };
|
|
10368
10868
|
})
|
|
10369
10869
|
);
|
|
@@ -10533,7 +11033,7 @@ function parseTimestamp(timestamp) {
|
|
|
10533
11033
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
10534
11034
|
}
|
|
10535
11035
|
async function countOpenQuestions(projectPath, assignmentSlug) {
|
|
10536
|
-
const commentsPath =
|
|
11036
|
+
const commentsPath = resolve18(
|
|
10537
11037
|
projectPath,
|
|
10538
11038
|
"assignments",
|
|
10539
11039
|
assignmentSlug,
|
|
@@ -10564,21 +11064,21 @@ function getProjectActivityTimestamp(projectUpdated, assignments) {
|
|
|
10564
11064
|
function getDocumentPath(projectsDir, documentType, projectSlug, assignmentSlug) {
|
|
10565
11065
|
switch (documentType) {
|
|
10566
11066
|
case "project":
|
|
10567
|
-
return
|
|
11067
|
+
return resolve18(projectsDir, projectSlug, "project.md");
|
|
10568
11068
|
case "assignment":
|
|
10569
|
-
return assignmentSlug ?
|
|
11069
|
+
return assignmentSlug ? resolve18(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
|
|
10570
11070
|
case "plan":
|
|
10571
|
-
return assignmentSlug ?
|
|
11071
|
+
return assignmentSlug ? resolve18(projectsDir, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
|
|
10572
11072
|
case "scratchpad":
|
|
10573
|
-
return assignmentSlug ?
|
|
11073
|
+
return assignmentSlug ? resolve18(projectsDir, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
|
|
10574
11074
|
case "handoff":
|
|
10575
|
-
return assignmentSlug ?
|
|
11075
|
+
return assignmentSlug ? resolve18(projectsDir, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
|
|
10576
11076
|
case "decision-record":
|
|
10577
|
-
return assignmentSlug ?
|
|
11077
|
+
return assignmentSlug ? resolve18(projectsDir, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
|
|
10578
11078
|
case "memory":
|
|
10579
|
-
return assignmentSlug ?
|
|
11079
|
+
return assignmentSlug ? resolve18(projectsDir, projectSlug, "memories", `${assignmentSlug}.md`) : null;
|
|
10580
11080
|
case "resource":
|
|
10581
|
-
return assignmentSlug ?
|
|
11081
|
+
return assignmentSlug ? resolve18(projectsDir, projectSlug, "resources", `${assignmentSlug}.md`) : null;
|
|
10582
11082
|
default:
|
|
10583
11083
|
return null;
|
|
10584
11084
|
}
|
|
@@ -10615,7 +11115,7 @@ async function listPlaybooks(playbooksDir2) {
|
|
|
10615
11115
|
const playbooks = [];
|
|
10616
11116
|
for (const entry of entries) {
|
|
10617
11117
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
|
|
10618
|
-
const filePath =
|
|
11118
|
+
const filePath = resolve18(playbooksDir2, entry.name);
|
|
10619
11119
|
const raw2 = await readFile13(filePath, "utf-8");
|
|
10620
11120
|
const parsed = parsePlaybook(raw2);
|
|
10621
11121
|
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
@@ -11220,12 +11720,12 @@ __export(recompute_exports, {
|
|
|
11220
11720
|
});
|
|
11221
11721
|
import { createHash as createHash2 } from "crypto";
|
|
11222
11722
|
import { open, readdir as readdir12, readFile as readFile17, unlink as unlink5, stat as stat3 } from "fs/promises";
|
|
11223
|
-
import { basename as basename3, dirname as dirname4, resolve as
|
|
11723
|
+
import { basename as basename3, dirname as dirname4, resolve as resolve23 } from "path";
|
|
11224
11724
|
async function isDeriveMigrated() {
|
|
11225
|
-
return fileExists(
|
|
11725
|
+
return fileExists(resolve23(syntaurRoot(), MIGRATION_MARKER));
|
|
11226
11726
|
}
|
|
11227
11727
|
async function markDeriveMigrated() {
|
|
11228
|
-
await writeFileForce(
|
|
11728
|
+
await writeFileForce(resolve23(syntaurRoot(), MIGRATION_MARKER), `${nowTimestamp()}
|
|
11229
11729
|
`);
|
|
11230
11730
|
}
|
|
11231
11731
|
async function resolveDeriveContext() {
|
|
@@ -11242,7 +11742,7 @@ async function resolveDeriveContext() {
|
|
|
11242
11742
|
};
|
|
11243
11743
|
}
|
|
11244
11744
|
async function acquireLock(assignmentDir) {
|
|
11245
|
-
const lockPath =
|
|
11745
|
+
const lockPath = resolve23(assignmentDir, LOCK_FILE);
|
|
11246
11746
|
const token = `${process.pid}:${createHash2("sha256").update(`${Math.random()}${Date.now()}`).digest("hex").slice(0, 12)}`;
|
|
11247
11747
|
for (let attempt = 0; attempt <= LOCK_MAX_WAITS; attempt++) {
|
|
11248
11748
|
try {
|
|
@@ -11386,7 +11886,7 @@ async function recomputeAndWrite(assignmentPath, opts) {
|
|
|
11386
11886
|
}
|
|
11387
11887
|
}
|
|
11388
11888
|
async function recomputeDependents(projectDir, changedSlug, opts) {
|
|
11389
|
-
const assignmentsDir2 =
|
|
11889
|
+
const assignmentsDir2 = resolve23(projectDir, "assignments");
|
|
11390
11890
|
let entries;
|
|
11391
11891
|
try {
|
|
11392
11892
|
entries = await readdir12(assignmentsDir2);
|
|
@@ -11396,7 +11896,7 @@ async function recomputeDependents(projectDir, changedSlug, opts) {
|
|
|
11396
11896
|
const results = [];
|
|
11397
11897
|
for (const slug of entries) {
|
|
11398
11898
|
if (slug === changedSlug) continue;
|
|
11399
|
-
const path =
|
|
11899
|
+
const path = resolve23(assignmentsDir2, slug, "assignment.md");
|
|
11400
11900
|
if (!await fileExists(path)) continue;
|
|
11401
11901
|
try {
|
|
11402
11902
|
const fm = parseAssignmentFrontmatter(await readFile17(path, "utf-8"));
|
|
@@ -11426,8 +11926,8 @@ async function recomputeAll(projectsDir, standaloneDir, opts) {
|
|
|
11426
11926
|
} catch {
|
|
11427
11927
|
}
|
|
11428
11928
|
for (const project of projects) {
|
|
11429
|
-
const projectDir =
|
|
11430
|
-
const assignmentsDir2 =
|
|
11929
|
+
const projectDir = resolve23(projectsDir, project);
|
|
11930
|
+
const assignmentsDir2 = resolve23(projectDir, "assignments");
|
|
11431
11931
|
let slugs = [];
|
|
11432
11932
|
try {
|
|
11433
11933
|
slugs = await readdir12(assignmentsDir2);
|
|
@@ -11435,7 +11935,7 @@ async function recomputeAll(projectsDir, standaloneDir, opts) {
|
|
|
11435
11935
|
continue;
|
|
11436
11936
|
}
|
|
11437
11937
|
for (const slug of slugs) {
|
|
11438
|
-
const path =
|
|
11938
|
+
const path = resolve23(assignmentsDir2, slug, "assignment.md");
|
|
11439
11939
|
if (await fileExists(path)) await sweepOne(path, projectDir);
|
|
11440
11940
|
}
|
|
11441
11941
|
}
|
|
@@ -11446,7 +11946,7 @@ async function recomputeAll(projectsDir, standaloneDir, opts) {
|
|
|
11446
11946
|
} catch {
|
|
11447
11947
|
}
|
|
11448
11948
|
for (const id of ids) {
|
|
11449
|
-
const path =
|
|
11949
|
+
const path = resolve23(standaloneDir, id, "assignment.md");
|
|
11450
11950
|
if (await fileExists(path)) await sweepOne(path, null);
|
|
11451
11951
|
}
|
|
11452
11952
|
}
|
|
@@ -11454,7 +11954,7 @@ async function recomputeAll(projectsDir, standaloneDir, opts) {
|
|
|
11454
11954
|
}
|
|
11455
11955
|
async function recomputeAssignmentDir(assignmentDir, cause, by) {
|
|
11456
11956
|
try {
|
|
11457
|
-
const assignmentPath =
|
|
11957
|
+
const assignmentPath = resolve23(assignmentDir, "assignment.md");
|
|
11458
11958
|
if (!await fileExists(assignmentPath)) return null;
|
|
11459
11959
|
const parent = dirname4(assignmentDir);
|
|
11460
11960
|
const projectDir = basename3(parent) === "assignments" ? dirname4(parent) : null;
|
|
@@ -11486,6 +11986,27 @@ var init_recompute = __esm({
|
|
|
11486
11986
|
}
|
|
11487
11987
|
});
|
|
11488
11988
|
|
|
11989
|
+
// src/utils/process-info.ts
|
|
11990
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
11991
|
+
function captureProcessStartedAt(pid) {
|
|
11992
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
11993
|
+
try {
|
|
11994
|
+
const out = execFileSync2("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
11995
|
+
encoding: "utf8",
|
|
11996
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
11997
|
+
});
|
|
11998
|
+
const trimmed = out.trim();
|
|
11999
|
+
return trimmed === "" ? null : trimmed;
|
|
12000
|
+
} catch {
|
|
12001
|
+
return null;
|
|
12002
|
+
}
|
|
12003
|
+
}
|
|
12004
|
+
var init_process_info = __esm({
|
|
12005
|
+
"src/utils/process-info.ts"() {
|
|
12006
|
+
"use strict";
|
|
12007
|
+
}
|
|
12008
|
+
});
|
|
12009
|
+
|
|
11489
12010
|
// src/utils/transcript.ts
|
|
11490
12011
|
import { open as open2 } from "fs/promises";
|
|
11491
12012
|
async function derivePathFromTranscript(transcriptPath) {
|
|
@@ -11549,31 +12070,10 @@ var init_transcript = __esm({
|
|
|
11549
12070
|
}
|
|
11550
12071
|
});
|
|
11551
12072
|
|
|
11552
|
-
// src/utils/process-info.ts
|
|
11553
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
11554
|
-
function captureProcessStartedAt(pid) {
|
|
11555
|
-
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
11556
|
-
try {
|
|
11557
|
-
const out = execFileSync2("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
11558
|
-
encoding: "utf8",
|
|
11559
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
11560
|
-
});
|
|
11561
|
-
const trimmed = out.trim();
|
|
11562
|
-
return trimmed === "" ? null : trimmed;
|
|
11563
|
-
} catch {
|
|
11564
|
-
return null;
|
|
11565
|
-
}
|
|
11566
|
-
}
|
|
11567
|
-
var init_process_info = __esm({
|
|
11568
|
-
"src/utils/process-info.ts"() {
|
|
11569
|
-
"use strict";
|
|
11570
|
-
}
|
|
11571
|
-
});
|
|
11572
|
-
|
|
11573
12073
|
// src/usage/cwd-extractor.ts
|
|
11574
12074
|
import { open as open3, readdir as readdir13, stat as stat4 } from "fs/promises";
|
|
11575
|
-
import { join as
|
|
11576
|
-
import { homedir as
|
|
12075
|
+
import { join as join3 } from "path";
|
|
12076
|
+
import { homedir as homedir2 } from "os";
|
|
11577
12077
|
async function extractClaudeSessionMeta(jsonlPath) {
|
|
11578
12078
|
const cwd = await derivePathFromTranscript(jsonlPath);
|
|
11579
12079
|
if (!cwd) return null;
|
|
@@ -11646,12 +12146,12 @@ async function* walkClaudeProjects(opts = {}) {
|
|
|
11646
12146
|
const dirs = await listDirSafe(root);
|
|
11647
12147
|
for (const dirent of dirs) {
|
|
11648
12148
|
if (!dirent.isDirectory) continue;
|
|
11649
|
-
const dirPath =
|
|
12149
|
+
const dirPath = join3(root, dirent.name);
|
|
11650
12150
|
const files = await listDirSafe(dirPath);
|
|
11651
12151
|
let cachedCwd = null;
|
|
11652
12152
|
for (const f of files) {
|
|
11653
12153
|
if (!f.isFile || !f.name.endsWith(".jsonl")) continue;
|
|
11654
|
-
const filePath =
|
|
12154
|
+
const filePath = join3(dirPath, f.name);
|
|
11655
12155
|
if (opts.sinceMtimeMs !== void 0) {
|
|
11656
12156
|
const mtime = await mtimeMs(filePath);
|
|
11657
12157
|
if (mtime !== null && mtime < opts.sinceMtimeMs) continue;
|
|
@@ -11683,13 +12183,13 @@ async function* walkCodexSessions(opts = {}) {
|
|
|
11683
12183
|
if (meta) yield meta;
|
|
11684
12184
|
}
|
|
11685
12185
|
}
|
|
11686
|
-
function resolveCodexSessionsRoot(
|
|
11687
|
-
if (
|
|
12186
|
+
function resolveCodexSessionsRoot(override3) {
|
|
12187
|
+
if (override3) return expandHome(override3);
|
|
11688
12188
|
const fromSessionsEnv = process.env.CODEX_SESSIONS_DIR;
|
|
11689
12189
|
if (fromSessionsEnv && fromSessionsEnv.length > 0) return expandHome(fromSessionsEnv);
|
|
11690
12190
|
const fromHomeEnv = process.env.CODEX_HOME;
|
|
11691
|
-
if (fromHomeEnv && fromHomeEnv.length > 0) return
|
|
11692
|
-
return
|
|
12191
|
+
if (fromHomeEnv && fromHomeEnv.length > 0) return join3(expandHome(fromHomeEnv), "sessions");
|
|
12192
|
+
return join3(homedir2(), ".codex", "sessions");
|
|
11693
12193
|
}
|
|
11694
12194
|
async function extractPiSessionMeta(jsonlPath) {
|
|
11695
12195
|
const basename7 = jsonlPath.split("/").pop() ?? "";
|
|
@@ -11743,23 +12243,23 @@ async function extractPiSessionMeta(jsonlPath) {
|
|
|
11743
12243
|
});
|
|
11744
12244
|
}
|
|
11745
12245
|
}
|
|
11746
|
-
function resolvePiSessionsRoot(
|
|
11747
|
-
if (
|
|
12246
|
+
function resolvePiSessionsRoot(override3) {
|
|
12247
|
+
if (override3) return expandHome(override3);
|
|
11748
12248
|
const fromHomeEnv = process.env.PI_AGENT_DIR;
|
|
11749
|
-
if (fromHomeEnv && fromHomeEnv.length > 0) return
|
|
11750
|
-
return
|
|
12249
|
+
if (fromHomeEnv && fromHomeEnv.length > 0) return join3(expandHome(fromHomeEnv), "sessions");
|
|
12250
|
+
return join3(homedir2(), ".pi", "agent", "sessions");
|
|
11751
12251
|
}
|
|
11752
12252
|
async function* walkPiSessions(opts = {}) {
|
|
11753
12253
|
const root = resolvePiSessionsRoot(opts.root);
|
|
11754
12254
|
const dirs = await listDirSafe(root);
|
|
11755
12255
|
for (const dirent of dirs) {
|
|
11756
12256
|
if (!dirent.isDirectory) continue;
|
|
11757
|
-
const dirPath =
|
|
12257
|
+
const dirPath = join3(root, dirent.name);
|
|
11758
12258
|
const files = await listDirSafe(dirPath);
|
|
11759
12259
|
let cachedCwd = null;
|
|
11760
12260
|
for (const f of files) {
|
|
11761
12261
|
if (!f.isFile || !f.name.endsWith(".jsonl")) continue;
|
|
11762
|
-
const filePath =
|
|
12262
|
+
const filePath = join3(dirPath, f.name);
|
|
11763
12263
|
if (opts.sinceMtimeMs !== void 0) {
|
|
11764
12264
|
const mtime = await mtimeMs(filePath);
|
|
11765
12265
|
if (mtime !== null && mtime < opts.sinceMtimeMs) continue;
|
|
@@ -11799,7 +12299,7 @@ async function* walkJsonlRecursive(root) {
|
|
|
11799
12299
|
const current = stack.pop();
|
|
11800
12300
|
const entries = await listDirSafe(current);
|
|
11801
12301
|
for (const e of entries) {
|
|
11802
|
-
const full =
|
|
12302
|
+
const full = join3(current, e.name);
|
|
11803
12303
|
if (e.isDirectory) {
|
|
11804
12304
|
stack.push(full);
|
|
11805
12305
|
} else if (e.isFile && e.name.endsWith(".jsonl")) {
|
|
@@ -11910,17 +12410,17 @@ var init_cwd_extractor = __esm({
|
|
|
11910
12410
|
|
|
11911
12411
|
// src/utils/session-id.ts
|
|
11912
12412
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
11913
|
-
import { mkdirSync, readFileSync, statSync as
|
|
11914
|
-
import { homedir as
|
|
11915
|
-
import { dirname as dirname5, join as
|
|
12413
|
+
import { mkdirSync, readFileSync as readFileSync2, statSync as statSync4, writeFileSync } from "fs";
|
|
12414
|
+
import { homedir as homedir3 } from "os";
|
|
12415
|
+
import { dirname as dirname5, join as join4 } from "path";
|
|
11916
12416
|
function isSafeSessionId(value) {
|
|
11917
12417
|
return typeof value === "string" && value.length > 0 && value.length <= 256 && SAFE_SESSION_ID.test(value);
|
|
11918
12418
|
}
|
|
11919
12419
|
function readRuntimeMarker(pid, dir) {
|
|
11920
12420
|
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
11921
|
-
const path =
|
|
12421
|
+
const path = join4(dir, `${pid}.json`);
|
|
11922
12422
|
try {
|
|
11923
|
-
const parsed = JSON.parse(
|
|
12423
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
11924
12424
|
if (parsed && typeof parsed === "object" && typeof parsed.sessionId === "string" && parsed.sessionId.length > 0) {
|
|
11925
12425
|
return parsed;
|
|
11926
12426
|
}
|
|
@@ -11930,7 +12430,7 @@ function readRuntimeMarker(pid, dir) {
|
|
|
11930
12430
|
}
|
|
11931
12431
|
}
|
|
11932
12432
|
function writeRuntimeMarker(pid, marker, dir) {
|
|
11933
|
-
const path =
|
|
12433
|
+
const path = join4(dir, `${pid}.json`);
|
|
11934
12434
|
mkdirSync(dirname5(path), { recursive: true });
|
|
11935
12435
|
writeFileSync(path, JSON.stringify(marker));
|
|
11936
12436
|
}
|
|
@@ -11991,6 +12491,22 @@ var init_assignment_todos = __esm({
|
|
|
11991
12491
|
}
|
|
11992
12492
|
});
|
|
11993
12493
|
|
|
12494
|
+
// src/utils/path-canon.ts
|
|
12495
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
12496
|
+
import { resolve as resolve44 } from "path";
|
|
12497
|
+
function canonicalPath(p) {
|
|
12498
|
+
try {
|
|
12499
|
+
return realpathSync2(resolve44(p));
|
|
12500
|
+
} catch {
|
|
12501
|
+
return resolve44(p).replace(/\/+$/, "");
|
|
12502
|
+
}
|
|
12503
|
+
}
|
|
12504
|
+
var init_path_canon = __esm({
|
|
12505
|
+
"src/utils/path-canon.ts"() {
|
|
12506
|
+
"use strict";
|
|
12507
|
+
}
|
|
12508
|
+
});
|
|
12509
|
+
|
|
11994
12510
|
// src/targets/renderers.ts
|
|
11995
12511
|
var RENDERERS;
|
|
11996
12512
|
var init_renderers = __esm({
|
|
@@ -12011,7 +12527,7 @@ var init_renderers = __esm({
|
|
|
12011
12527
|
});
|
|
12012
12528
|
|
|
12013
12529
|
// src/targets/user-descriptors.ts
|
|
12014
|
-
import { resolve as
|
|
12530
|
+
import { resolve as resolve45 } from "path";
|
|
12015
12531
|
import { readFile as readFile30, readdir as readdir18 } from "fs/promises";
|
|
12016
12532
|
var VALID_RENDERER_KEYS;
|
|
12017
12533
|
var init_user_descriptors = __esm({
|
|
@@ -12026,20 +12542,20 @@ var init_user_descriptors = __esm({
|
|
|
12026
12542
|
|
|
12027
12543
|
// src/targets/registry.ts
|
|
12028
12544
|
import { homedir as homedir7 } from "os";
|
|
12029
|
-
import { join as join10, resolve as
|
|
12545
|
+
import { join as join10, resolve as resolve46 } from "path";
|
|
12030
12546
|
function home(...segments) {
|
|
12031
|
-
return
|
|
12547
|
+
return resolve46(homedir7(), ...segments);
|
|
12032
12548
|
}
|
|
12033
12549
|
function hermesHome() {
|
|
12034
12550
|
const env = process.env.HERMES_HOME;
|
|
12035
|
-
return env && env.length > 0 ?
|
|
12551
|
+
return env && env.length > 0 ? resolve46(env) : home(".hermes");
|
|
12036
12552
|
}
|
|
12037
12553
|
function hermesSkillsDir() {
|
|
12038
|
-
return
|
|
12554
|
+
return resolve46(hermesHome(), "skills");
|
|
12039
12555
|
}
|
|
12040
12556
|
function codexHome() {
|
|
12041
12557
|
const env = process.env.CODEX_HOME;
|
|
12042
|
-
return env && env.length > 0 ?
|
|
12558
|
+
return env && env.length > 0 ? resolve46(env) : home(".codex");
|
|
12043
12559
|
}
|
|
12044
12560
|
function toDiscovered(meta) {
|
|
12045
12561
|
if (!meta) return null;
|
|
@@ -12110,7 +12626,7 @@ var init_registry = __esm({
|
|
|
12110
12626
|
skillsShAgentId: "codex",
|
|
12111
12627
|
nativePlugin: "codex",
|
|
12112
12628
|
detect: detectDir(codexHome()),
|
|
12113
|
-
skillsDir: { global:
|
|
12629
|
+
skillsDir: { global: resolve46(codexHome(), "skills") },
|
|
12114
12630
|
instructions: { files: [{ path: "AGENTS.md", renderer: "codexAgents" }] },
|
|
12115
12631
|
sessions: codexSessions
|
|
12116
12632
|
},
|
|
@@ -12179,7 +12695,7 @@ var init_registry = __esm({
|
|
|
12179
12695
|
tier3: {
|
|
12180
12696
|
kind: "hermes-plugin",
|
|
12181
12697
|
source: "platforms/hermes/plugins/syntaur",
|
|
12182
|
-
installDir: () =>
|
|
12698
|
+
installDir: () => resolve46(hermesHome(), "plugins", "syntaur"),
|
|
12183
12699
|
entry: "plugin.yaml"
|
|
12184
12700
|
}
|
|
12185
12701
|
}
|
|
@@ -12190,22 +12706,75 @@ var init_registry = __esm({
|
|
|
12190
12706
|
}
|
|
12191
12707
|
});
|
|
12192
12708
|
|
|
12709
|
+
// src/sessions/agent-view.ts
|
|
12710
|
+
import { execFile as execFile3 } from "child_process";
|
|
12711
|
+
import { promisify as promisify3 } from "util";
|
|
12712
|
+
function toActivityState(raw2) {
|
|
12713
|
+
switch (raw2) {
|
|
12714
|
+
case "working":
|
|
12715
|
+
case "idle":
|
|
12716
|
+
case "awaiting-input":
|
|
12717
|
+
return raw2;
|
|
12718
|
+
default:
|
|
12719
|
+
return null;
|
|
12720
|
+
}
|
|
12721
|
+
}
|
|
12722
|
+
function parseAgentView(json) {
|
|
12723
|
+
const live = /* @__PURE__ */ new Map();
|
|
12724
|
+
let parsed;
|
|
12725
|
+
try {
|
|
12726
|
+
parsed = JSON.parse(json);
|
|
12727
|
+
} catch {
|
|
12728
|
+
return live;
|
|
12729
|
+
}
|
|
12730
|
+
const entries = Array.isArray(parsed) ? parsed : Array.isArray(parsed.agents) ? parsed.agents : [];
|
|
12731
|
+
for (const entry of entries) {
|
|
12732
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
12733
|
+
const e = entry;
|
|
12734
|
+
const id = typeof e.session_id === "string" ? e.session_id : typeof e.sessionId === "string" ? e.sessionId : null;
|
|
12735
|
+
if (!id) continue;
|
|
12736
|
+
live.set(id, toActivityState(e.activity ?? e.status) ?? "idle");
|
|
12737
|
+
}
|
|
12738
|
+
return live;
|
|
12739
|
+
}
|
|
12740
|
+
function getAgentViewSource() {
|
|
12741
|
+
return override2 ?? productionAgentViewSource;
|
|
12742
|
+
}
|
|
12743
|
+
var execFileAsync, productionAgentViewSource, override2;
|
|
12744
|
+
var init_agent_view = __esm({
|
|
12745
|
+
"src/sessions/agent-view.ts"() {
|
|
12746
|
+
"use strict";
|
|
12747
|
+
execFileAsync = promisify3(execFile3);
|
|
12748
|
+
productionAgentViewSource = async () => {
|
|
12749
|
+
try {
|
|
12750
|
+
const { stdout } = await execFileAsync("claude", ["agents", "--json"], {
|
|
12751
|
+
timeout: 5e3,
|
|
12752
|
+
maxBuffer: 8 * 1024 * 1024
|
|
12753
|
+
});
|
|
12754
|
+
return parseAgentView(stdout);
|
|
12755
|
+
} catch {
|
|
12756
|
+
return /* @__PURE__ */ new Map();
|
|
12757
|
+
}
|
|
12758
|
+
};
|
|
12759
|
+
override2 = null;
|
|
12760
|
+
}
|
|
12761
|
+
});
|
|
12762
|
+
|
|
12193
12763
|
// src/sessions/scanner.ts
|
|
12194
12764
|
var scanner_exports2 = {};
|
|
12195
12765
|
__export(scanner_exports2, {
|
|
12196
12766
|
scanSessions: () => scanSessions
|
|
12197
12767
|
});
|
|
12198
|
-
import { execFile as
|
|
12199
|
-
import { promisify as
|
|
12200
|
-
import { statSync as
|
|
12201
|
-
import {
|
|
12202
|
-
import { resolve as resolve45 } from "path";
|
|
12768
|
+
import { execFile as execFile4, execFileSync as execFileSync4 } from "child_process";
|
|
12769
|
+
import { promisify as promisify4 } from "util";
|
|
12770
|
+
import { statSync as statSync5 } from "fs";
|
|
12771
|
+
import { resolve as resolve47 } from "path";
|
|
12203
12772
|
function emptySummary() {
|
|
12204
12773
|
return { discovered: 0, inserted: 0, revived: 0, swept: 0, skipped: 0, changed: false };
|
|
12205
12774
|
}
|
|
12206
12775
|
function defaultStatMtimeMs(path) {
|
|
12207
12776
|
try {
|
|
12208
|
-
return
|
|
12777
|
+
return statSync5(path).mtimeMs;
|
|
12209
12778
|
} catch {
|
|
12210
12779
|
return null;
|
|
12211
12780
|
}
|
|
@@ -12238,7 +12807,7 @@ async function defaultOpenFiles(files) {
|
|
|
12238
12807
|
const chunk = files.slice(i, i + LSOF_CHUNK);
|
|
12239
12808
|
let stdout = "";
|
|
12240
12809
|
try {
|
|
12241
|
-
const result = await
|
|
12810
|
+
const result = await execFileAsync2("lsof", ["-Fn", "--", ...chunk], {
|
|
12242
12811
|
maxBuffer: 8 * 1024 * 1024
|
|
12243
12812
|
});
|
|
12244
12813
|
stdout = result.stdout;
|
|
@@ -12252,23 +12821,17 @@ async function defaultOpenFiles(files) {
|
|
|
12252
12821
|
}
|
|
12253
12822
|
return open6;
|
|
12254
12823
|
}
|
|
12255
|
-
|
|
12824
|
+
function canonicalizeOpenSet(open6) {
|
|
12825
|
+
const out = /* @__PURE__ */ new Set();
|
|
12826
|
+
for (const p of open6) out.add(canonicalPath(p));
|
|
12827
|
+
return out;
|
|
12828
|
+
}
|
|
12829
|
+
async function isWorkspace(cwd, cache3) {
|
|
12256
12830
|
if (cache3.has(cwd)) return cache3.get(cwd);
|
|
12257
|
-
|
|
12258
|
-
const
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
const parsed = JSON.parse(await readFile31(path, "utf-8"));
|
|
12262
|
-
link = {
|
|
12263
|
-
projectSlug: typeof parsed.projectSlug === "string" ? parsed.projectSlug : null,
|
|
12264
|
-
assignmentSlug: typeof parsed.assignmentSlug === "string" ? parsed.assignmentSlug : null
|
|
12265
|
-
};
|
|
12266
|
-
} catch {
|
|
12267
|
-
link = { projectSlug: null, assignmentSlug: null };
|
|
12268
|
-
}
|
|
12269
|
-
}
|
|
12270
|
-
cache3.set(cwd, link);
|
|
12271
|
-
return link;
|
|
12831
|
+
const path = resolve47(cwd, ".syntaur", "context.json");
|
|
12832
|
+
const present = await fileExists(path);
|
|
12833
|
+
cache3.set(cwd, present);
|
|
12834
|
+
return present;
|
|
12272
12835
|
}
|
|
12273
12836
|
function readWatermark() {
|
|
12274
12837
|
const db5 = getSessionDb();
|
|
@@ -12292,9 +12855,16 @@ async function scanSessions(opts = {}, deps = {}) {
|
|
|
12292
12855
|
const openFiles = deps.openFiles ?? defaultOpenFiles;
|
|
12293
12856
|
const isPidAlive = deps.isPidAlive ?? defaultIsPidAlive;
|
|
12294
12857
|
const pidStartedAt = deps.pidStartedAt ?? defaultPidStartedAt;
|
|
12858
|
+
const agentView = deps.agentView ?? getAgentViewSource();
|
|
12295
12859
|
const targets = (deps.targets ?? AGENT_TARGETS).filter((t) => t.sessions !== void 0);
|
|
12296
12860
|
const scanStartMs = now();
|
|
12297
12861
|
const watermark = opts.full ? null : readWatermark();
|
|
12862
|
+
let liveActivity;
|
|
12863
|
+
try {
|
|
12864
|
+
liveActivity = await agentView();
|
|
12865
|
+
} catch {
|
|
12866
|
+
liveActivity = /* @__PURE__ */ new Map();
|
|
12867
|
+
}
|
|
12298
12868
|
const discovered = [];
|
|
12299
12869
|
for (const target of targets) {
|
|
12300
12870
|
const walk = target.sessions.walk({
|
|
@@ -12307,17 +12877,20 @@ async function scanSessions(opts = {}, deps = {}) {
|
|
|
12307
12877
|
}
|
|
12308
12878
|
}
|
|
12309
12879
|
summary.discovered = discovered.length;
|
|
12310
|
-
const openSet =
|
|
12311
|
-
|
|
12880
|
+
const openSet = canonicalizeOpenSet(
|
|
12881
|
+
await openFiles(discovered.map((d) => d.transcriptPath))
|
|
12882
|
+
);
|
|
12883
|
+
const workspaceCache = /* @__PURE__ */ new Map();
|
|
12312
12884
|
for (const d of discovered) {
|
|
12313
|
-
const
|
|
12314
|
-
if (autoTrack === "workspaces-only" &&
|
|
12885
|
+
const isWs = await isWorkspace(d.cwd, workspaceCache);
|
|
12886
|
+
if (autoTrack === "workspaces-only" && !isWs) {
|
|
12315
12887
|
summary.skipped += 1;
|
|
12316
12888
|
continue;
|
|
12317
12889
|
}
|
|
12318
12890
|
const mtime = statMtimeMs(d.transcriptPath);
|
|
12319
|
-
const heldOpen = openSet.has(d.transcriptPath);
|
|
12320
|
-
const
|
|
12891
|
+
const heldOpen = openSet.has(canonicalPath(d.transcriptPath));
|
|
12892
|
+
const agentViewLive = liveActivity.has(d.sessionId);
|
|
12893
|
+
const isLive = heldOpen || agentViewLive || mtime !== null && now() - mtime < FRESH_MTIME_MS;
|
|
12321
12894
|
const prev = getSessionById(d.sessionId);
|
|
12322
12895
|
const status = isLive ? "active" : prev?.status ?? "stopped";
|
|
12323
12896
|
const started = d.startedAt ?? (mtime !== null ? new Date(mtime).toISOString() : new Date(now()).toISOString());
|
|
@@ -12325,8 +12898,11 @@ async function scanSessions(opts = {}, deps = {}) {
|
|
|
12325
12898
|
"",
|
|
12326
12899
|
{
|
|
12327
12900
|
sessionId: d.sessionId,
|
|
12328
|
-
|
|
12329
|
-
|
|
12901
|
+
// Auto-discovered sessions are UNATTRIBUTED: the scanner no longer binds
|
|
12902
|
+
// them to the cwd context.json assignment scalar (see isWorkspace). An
|
|
12903
|
+
// explicit grab/track opens an engagement edge to attribute them.
|
|
12904
|
+
projectSlug: null,
|
|
12905
|
+
assignmentSlug: null,
|
|
12330
12906
|
agent: d.agent,
|
|
12331
12907
|
started,
|
|
12332
12908
|
status,
|
|
@@ -12338,11 +12914,12 @@ async function scanSessions(opts = {}, deps = {}) {
|
|
|
12338
12914
|
originalHeadSha: null
|
|
12339
12915
|
},
|
|
12340
12916
|
// Narrow revival rule: only LIVE-PROCESS evidence (a process holding the
|
|
12341
|
-
// transcript open) may flip a stopped row back
|
|
12342
|
-
// alone must not — a session stopped moments ago
|
|
12343
|
-
// still has a fresh transcript for up to 5 minutes
|
|
12344
|
-
// active. `completed` always sticks (appendSession
|
|
12345
|
-
|
|
12917
|
+
// transcript open) OR Agent-View live evidence may flip a stopped row back
|
|
12918
|
+
// to active. mtime freshness alone must not — a session stopped moments ago
|
|
12919
|
+
// by its SessionEnd hook still has a fresh transcript for up to 5 minutes
|
|
12920
|
+
// and would flap back to active. `completed` always sticks (appendSession
|
|
12921
|
+
// enforces).
|
|
12922
|
+
{ reviveStopped: heldOpen || agentViewLive }
|
|
12346
12923
|
);
|
|
12347
12924
|
if (!isLive) {
|
|
12348
12925
|
const after = getSessionById(d.sessionId);
|
|
@@ -12355,19 +12932,25 @@ async function scanSessions(opts = {}, deps = {}) {
|
|
|
12355
12932
|
summary.inserted += 1;
|
|
12356
12933
|
summary.changed = true;
|
|
12357
12934
|
} else {
|
|
12358
|
-
if (prev.status === "stopped" && heldOpen) {
|
|
12935
|
+
if (prev.status === "stopped" && (heldOpen || agentViewLive)) {
|
|
12359
12936
|
summary.revived += 1;
|
|
12360
12937
|
summary.changed = true;
|
|
12361
12938
|
}
|
|
12362
|
-
if (link?.projectSlug && !prev.projectSlug || link?.assignmentSlug && !prev.assignmentSlug) {
|
|
12363
|
-
summary.changed = true;
|
|
12364
|
-
}
|
|
12365
12939
|
}
|
|
12366
12940
|
}
|
|
12367
12941
|
const db5 = getSessionDb();
|
|
12942
|
+
if (liveActivity.size > 0) {
|
|
12943
|
+
const setActivity = db5.prepare(
|
|
12944
|
+
"UPDATE sessions SET activity = ?, updated_at = datetime('now') WHERE session_id = ? AND activity IS NOT ?"
|
|
12945
|
+
);
|
|
12946
|
+
for (const [sid, activity] of liveActivity) {
|
|
12947
|
+
if (setActivity.run(activity, sid, activity).changes > 0) summary.changed = true;
|
|
12948
|
+
}
|
|
12949
|
+
}
|
|
12368
12950
|
const activeRows = db5.prepare("SELECT session_id, pid, pid_started_at, transcript_path FROM sessions WHERE status = 'active'").all();
|
|
12369
12951
|
const sweepCandidates = [];
|
|
12370
12952
|
for (const row of activeRows) {
|
|
12953
|
+
if (liveActivity.has(row.session_id)) continue;
|
|
12371
12954
|
if (row.pid !== null) {
|
|
12372
12955
|
const alive = isPidAlive(row.pid) && (!row.pid_started_at || (pidStartedAt(row.pid) ?? row.pid_started_at) === row.pid_started_at);
|
|
12373
12956
|
if (alive) continue;
|
|
@@ -12378,20 +12961,33 @@ async function scanSessions(opts = {}, deps = {}) {
|
|
|
12378
12961
|
sweepCandidates.push({ sessionId: row.session_id, transcriptPath: null });
|
|
12379
12962
|
}
|
|
12380
12963
|
}
|
|
12381
|
-
const sweepOpenSet =
|
|
12382
|
-
|
|
12964
|
+
const sweepOpenSet = canonicalizeOpenSet(
|
|
12965
|
+
await openFiles(
|
|
12966
|
+
sweepCandidates.map((c) => c.transcriptPath).filter((p) => p !== null)
|
|
12967
|
+
)
|
|
12383
12968
|
);
|
|
12384
12969
|
for (const candidate of sweepCandidates) {
|
|
12970
|
+
let endedAt;
|
|
12385
12971
|
if (candidate.transcriptPath) {
|
|
12386
|
-
if (sweepOpenSet.has(candidate.transcriptPath)) continue;
|
|
12972
|
+
if (sweepOpenSet.has(canonicalPath(candidate.transcriptPath))) continue;
|
|
12387
12973
|
const mtime = statMtimeMs(candidate.transcriptPath);
|
|
12388
12974
|
if (mtime !== null && now() - mtime < FRESH_MTIME_MS) continue;
|
|
12389
|
-
|
|
12390
|
-
|
|
12391
|
-
|
|
12392
|
-
|
|
12393
|
-
|
|
12394
|
-
|
|
12975
|
+
endedAt = mtime !== null ? new Date(mtime).toISOString() : void 0;
|
|
12976
|
+
}
|
|
12977
|
+
const open6 = getOpenEngagement(candidate.sessionId);
|
|
12978
|
+
let snap = null;
|
|
12979
|
+
try {
|
|
12980
|
+
snap = await getCumulativeTokenSource()(candidate.sessionId);
|
|
12981
|
+
} catch {
|
|
12982
|
+
snap = null;
|
|
12983
|
+
}
|
|
12984
|
+
if (livenessStopSession({
|
|
12985
|
+
sessionId: candidate.sessionId,
|
|
12986
|
+
engagementId: open6?.id ?? null,
|
|
12987
|
+
engagementStartedAt: open6?.started_at ?? null,
|
|
12988
|
+
endedAt,
|
|
12989
|
+
tokensAtClose: snap
|
|
12990
|
+
})) {
|
|
12395
12991
|
summary.swept += 1;
|
|
12396
12992
|
summary.changed = true;
|
|
12397
12993
|
}
|
|
@@ -12399,17 +12995,21 @@ async function scanSessions(opts = {}, deps = {}) {
|
|
|
12399
12995
|
writeWatermark(scanStartMs);
|
|
12400
12996
|
return summary;
|
|
12401
12997
|
}
|
|
12402
|
-
var
|
|
12998
|
+
var execFileAsync2, FRESH_MTIME_MS, LSOF_CHUNK, WATERMARK_KEY;
|
|
12403
12999
|
var init_scanner2 = __esm({
|
|
12404
13000
|
"src/sessions/scanner.ts"() {
|
|
12405
13001
|
"use strict";
|
|
12406
13002
|
init_fs();
|
|
13003
|
+
init_path_canon();
|
|
12407
13004
|
init_config2();
|
|
12408
13005
|
init_session_id();
|
|
12409
13006
|
init_registry();
|
|
12410
13007
|
init_session_db();
|
|
12411
13008
|
init_agent_sessions();
|
|
12412
|
-
|
|
13009
|
+
init_engagement_db();
|
|
13010
|
+
init_engagement_tokens();
|
|
13011
|
+
init_agent_view();
|
|
13012
|
+
execFileAsync2 = promisify4(execFile4);
|
|
12413
13013
|
FRESH_MTIME_MS = 5 * 60 * 1e3;
|
|
12414
13014
|
LSOF_CHUNK = 64;
|
|
12415
13015
|
WATERMARK_KEY = "sessions_scan_last_ms";
|
|
@@ -12457,19 +13057,19 @@ init_assignment_resolver();
|
|
|
12457
13057
|
init_agent_sessions();
|
|
12458
13058
|
import express from "express";
|
|
12459
13059
|
import { createServer } from "http";
|
|
12460
|
-
import { resolve as
|
|
13060
|
+
import { resolve as resolve48 } from "path";
|
|
12461
13061
|
import { writeFile as writeFile8, unlink as unlink9 } from "fs/promises";
|
|
12462
13062
|
import { WebSocketServer, WebSocket } from "ws";
|
|
12463
13063
|
|
|
12464
13064
|
// src/dashboard/session-liveness.ts
|
|
12465
13065
|
import { execFileSync } from "child_process";
|
|
12466
|
-
import { statSync as
|
|
13066
|
+
import { statSync as statSync3 } from "fs";
|
|
12467
13067
|
var FIVE_MINUTES_MS = 5 * 60 * 1e3;
|
|
12468
13068
|
var DEFAULT_DEPS = {
|
|
12469
13069
|
now: () => Date.now(),
|
|
12470
13070
|
statMtimeMs: (path) => {
|
|
12471
13071
|
try {
|
|
12472
|
-
return
|
|
13072
|
+
return statSync3(path).mtimeMs;
|
|
12473
13073
|
} catch {
|
|
12474
13074
|
return null;
|
|
12475
13075
|
}
|
|
@@ -12965,10 +13565,10 @@ function mergePatch(current, patch) {
|
|
|
12965
13565
|
init_paths();
|
|
12966
13566
|
init_fs();
|
|
12967
13567
|
import { readFile as readFile14, rename as rename3, unlink as unlink3 } from "fs/promises";
|
|
12968
|
-
import { resolve as
|
|
13568
|
+
import { resolve as resolve19 } from "path";
|
|
12969
13569
|
function corruptFilePath() {
|
|
12970
13570
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
12971
|
-
return
|
|
13571
|
+
return resolve19(syntaurRoot(), `view-prefs.corrupt-${ts}.json`);
|
|
12972
13572
|
}
|
|
12973
13573
|
function isViewPrefsFileShape(value) {
|
|
12974
13574
|
if (!value || typeof value !== "object") return false;
|
|
@@ -13240,10 +13840,10 @@ init_paths();
|
|
|
13240
13840
|
init_fs();
|
|
13241
13841
|
import { readFile as readFile15, rename as rename4, unlink as unlink4 } from "fs/promises";
|
|
13242
13842
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
13243
|
-
import { resolve as
|
|
13843
|
+
import { resolve as resolve20 } from "path";
|
|
13244
13844
|
function corruptFilePath2() {
|
|
13245
13845
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
13246
|
-
return
|
|
13846
|
+
return resolve20(syntaurRoot(), `saved-views.corrupt-${ts}.json`);
|
|
13247
13847
|
}
|
|
13248
13848
|
function isSavedViewsFileShape(value) {
|
|
13249
13849
|
if (!value || typeof value !== "object") return false;
|
|
@@ -13618,19 +14218,19 @@ init_timestamp();
|
|
|
13618
14218
|
init_fs();
|
|
13619
14219
|
init_git_worktree();
|
|
13620
14220
|
import { Router as Router2 } from "express";
|
|
13621
|
-
import { resolve as
|
|
14221
|
+
import { resolve as resolve24, basename as basename4, isAbsolute as isAbsolute4 } from "path";
|
|
13622
14222
|
import { rm, readFile as readFile18, open as fsOpen, stat as fsStat, realpath as fsRealpath } from "fs/promises";
|
|
13623
14223
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
13624
14224
|
|
|
13625
14225
|
// src/utils/worktree-defaults.ts
|
|
13626
14226
|
init_paths();
|
|
13627
14227
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
13628
|
-
import { resolve as
|
|
14228
|
+
import { resolve as resolve21 } from "path";
|
|
13629
14229
|
function computeWorktreeDefaults(opts) {
|
|
13630
14230
|
const repository = opts.existing.repository ?? detectCurrentGitRoot(opts.cwd);
|
|
13631
14231
|
const branch = opts.projectSlug ? `syntaur/${opts.projectSlug}/${opts.assignmentSlug}` : `syntaur/${opts.assignmentSlug}`;
|
|
13632
14232
|
const parentBranch = opts.existing.parentBranch ?? detectCurrentBranch(opts.cwd) ?? "main";
|
|
13633
|
-
const worktreeBase = repository ?
|
|
14233
|
+
const worktreeBase = repository ? resolve21(repository, ".worktrees", branch) : resolve21(
|
|
13634
14234
|
syntaurRoot(),
|
|
13635
14235
|
"worktrees",
|
|
13636
14236
|
opts.projectSlug || "standalone",
|
|
@@ -13849,7 +14449,7 @@ function recreateOutcomeToHttp(outcome) {
|
|
|
13849
14449
|
init_fs();
|
|
13850
14450
|
init_parser();
|
|
13851
14451
|
import { readdir as readdir11, readFile as readFile16 } from "fs/promises";
|
|
13852
|
-
import { resolve as
|
|
14452
|
+
import { resolve as resolve22 } from "path";
|
|
13853
14453
|
function toSourceAssignment(parsed, fallbackId) {
|
|
13854
14454
|
const repository = parsed.workspace.repository?.trim();
|
|
13855
14455
|
const branch = parsed.workspace.branch?.trim();
|
|
@@ -13866,29 +14466,29 @@ function toSourceAssignment(parsed, fallbackId) {
|
|
|
13866
14466
|
async function getProjectRepositoryCandidates(projectsDir, projectSlug) {
|
|
13867
14467
|
const seen = /* @__PURE__ */ new Set();
|
|
13868
14468
|
const out = [];
|
|
13869
|
-
const projectPath =
|
|
14469
|
+
const projectPath = resolve22(projectsDir, projectSlug, "project.md");
|
|
13870
14470
|
if (await fileExists(projectPath)) {
|
|
13871
14471
|
const project = parseProject(await readFile16(projectPath, "utf-8"));
|
|
13872
14472
|
for (const raw2 of project.repositories) {
|
|
13873
14473
|
const path = raw2.trim();
|
|
13874
14474
|
if (!path) continue;
|
|
13875
|
-
const abs =
|
|
14475
|
+
const abs = resolve22(path);
|
|
13876
14476
|
if (seen.has(abs)) continue;
|
|
13877
14477
|
seen.add(abs);
|
|
13878
14478
|
out.push({ path: abs, source: "project", sourceAssignmentSlug: null });
|
|
13879
14479
|
}
|
|
13880
14480
|
}
|
|
13881
|
-
const assignmentsDir2 =
|
|
14481
|
+
const assignmentsDir2 = resolve22(projectsDir, projectSlug, "assignments");
|
|
13882
14482
|
if (await fileExists(assignmentsDir2)) {
|
|
13883
14483
|
const entries = await readdir11(assignmentsDir2, { withFileTypes: true });
|
|
13884
14484
|
for (const entry of entries) {
|
|
13885
14485
|
if (!entry.isDirectory()) continue;
|
|
13886
|
-
const assignmentMd =
|
|
14486
|
+
const assignmentMd = resolve22(assignmentsDir2, entry.name, "assignment.md");
|
|
13887
14487
|
if (!await fileExists(assignmentMd)) continue;
|
|
13888
14488
|
const parsed = parseAssignmentFull(await readFile16(assignmentMd, "utf-8"));
|
|
13889
14489
|
const repo = parsed.workspace.repository?.trim();
|
|
13890
14490
|
if (!repo) continue;
|
|
13891
|
-
const abs =
|
|
14491
|
+
const abs = resolve22(repo);
|
|
13892
14492
|
if (seen.has(abs)) continue;
|
|
13893
14493
|
seen.add(abs);
|
|
13894
14494
|
out.push({ path: abs, source: "sibling", sourceAssignmentSlug: parsed.slug });
|
|
@@ -13906,12 +14506,12 @@ async function getStandaloneRepositoryCandidates(assignmentsDir2, excludeAssignm
|
|
|
13906
14506
|
for (const entry of entries) {
|
|
13907
14507
|
if (!entry.isDirectory()) continue;
|
|
13908
14508
|
if (entry.name === excludeAssignmentId) continue;
|
|
13909
|
-
const assignmentMd =
|
|
14509
|
+
const assignmentMd = resolve22(assignmentsDir2, entry.name, "assignment.md");
|
|
13910
14510
|
if (!await fileExists(assignmentMd)) continue;
|
|
13911
14511
|
const parsed = parseAssignmentFull(await readFile16(assignmentMd, "utf-8"));
|
|
13912
14512
|
const repo = parsed.workspace.repository?.trim();
|
|
13913
14513
|
if (!repo) continue;
|
|
13914
|
-
const abs =
|
|
14514
|
+
const abs = resolve22(repo);
|
|
13915
14515
|
if (seen.has(abs)) continue;
|
|
13916
14516
|
seen.add(abs);
|
|
13917
14517
|
out.push({ path: abs, source: "sibling", sourceAssignmentSlug: parsed.slug });
|
|
@@ -13919,7 +14519,7 @@ async function getStandaloneRepositoryCandidates(assignmentsDir2, excludeAssignm
|
|
|
13919
14519
|
return out;
|
|
13920
14520
|
}
|
|
13921
14521
|
async function getProjectSourceAssignments(projectsDir, projectSlug, excludeSlug) {
|
|
13922
|
-
const assignmentsDir2 =
|
|
14522
|
+
const assignmentsDir2 = resolve22(projectsDir, projectSlug, "assignments");
|
|
13923
14523
|
if (!await fileExists(assignmentsDir2)) return [];
|
|
13924
14524
|
const seen = /* @__PURE__ */ new Set();
|
|
13925
14525
|
const out = [];
|
|
@@ -13927,7 +14527,7 @@ async function getProjectSourceAssignments(projectsDir, projectSlug, excludeSlug
|
|
|
13927
14527
|
for (const entry of entries) {
|
|
13928
14528
|
if (!entry.isDirectory()) continue;
|
|
13929
14529
|
if (entry.name === excludeSlug) continue;
|
|
13930
|
-
const assignmentMd =
|
|
14530
|
+
const assignmentMd = resolve22(assignmentsDir2, entry.name, "assignment.md");
|
|
13931
14531
|
if (!await fileExists(assignmentMd)) continue;
|
|
13932
14532
|
const parsed = parseAssignmentFull(await readFile16(assignmentMd, "utf-8"));
|
|
13933
14533
|
const source = toSourceAssignment(parsed, entry.name);
|
|
@@ -13946,7 +14546,7 @@ async function getStandaloneSourceAssignments(assignmentsDir2, excludeAssignment
|
|
|
13946
14546
|
for (const entry of entries) {
|
|
13947
14547
|
if (!entry.isDirectory()) continue;
|
|
13948
14548
|
if (entry.name === excludeAssignmentId) continue;
|
|
13949
|
-
const assignmentMd =
|
|
14549
|
+
const assignmentMd = resolve22(assignmentsDir2, entry.name, "assignment.md");
|
|
13950
14550
|
if (!await fileExists(assignmentMd)) continue;
|
|
13951
14551
|
const parsed = parseAssignmentFull(await readFile16(assignmentMd, "utf-8"));
|
|
13952
14552
|
const source = toSourceAssignment(parsed, entry.name);
|
|
@@ -14641,7 +15241,7 @@ async function handleWorktreeCreate(req2, res, ctx) {
|
|
|
14641
15241
|
});
|
|
14642
15242
|
const branch = typeof bodyBranch === "string" && bodyBranch.trim() ? bodyBranch.trim() : defaults.branch;
|
|
14643
15243
|
const parentBranch = typeof bodyParent === "string" && bodyParent.trim() ? bodyParent.trim() : defaults.parentBranch;
|
|
14644
|
-
const worktreePath =
|
|
15244
|
+
const worktreePath = resolve24(repo, ".worktrees", branch);
|
|
14645
15245
|
const refCheck = spawnSync3("git", ["-C", repo, "check-ref-format", "--branch", branch], {
|
|
14646
15246
|
encoding: "utf-8"
|
|
14647
15247
|
});
|
|
@@ -14936,9 +15536,9 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
|
|
|
14936
15536
|
});
|
|
14937
15537
|
return;
|
|
14938
15538
|
}
|
|
14939
|
-
const folderPath =
|
|
15539
|
+
const folderPath = resolve24(projectDir, folder);
|
|
14940
15540
|
await ensureDir(folderPath);
|
|
14941
|
-
const filePath =
|
|
15541
|
+
const filePath = resolve24(folderPath, `${requestedSlug}.md`);
|
|
14942
15542
|
const timestamp = nowTimestamp();
|
|
14943
15543
|
let content = renderItemStub(kind, {
|
|
14944
15544
|
slug: requestedSlug,
|
|
@@ -14982,7 +15582,7 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
|
|
|
14982
15582
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
14983
15583
|
return;
|
|
14984
15584
|
}
|
|
14985
|
-
const filePath =
|
|
15585
|
+
const filePath = resolve24(projectDir, folder, `${itemSlug}.md`);
|
|
14986
15586
|
if (!await fileExists(filePath)) {
|
|
14987
15587
|
res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
|
|
14988
15588
|
return;
|
|
@@ -15018,7 +15618,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
15018
15618
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
15019
15619
|
return;
|
|
15020
15620
|
}
|
|
15021
|
-
const filePath =
|
|
15621
|
+
const filePath = resolve24(projectDir, folder, `${itemSlug}.md`);
|
|
15022
15622
|
if (!await fileExists(filePath)) {
|
|
15023
15623
|
res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
|
|
15024
15624
|
return;
|
|
@@ -15052,26 +15652,26 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
15052
15652
|
res.status(400).json({ error: `Invalid slug "${slug}". Must be lowercase and hyphen-separated.` });
|
|
15053
15653
|
return;
|
|
15054
15654
|
}
|
|
15055
|
-
const projectDir =
|
|
15655
|
+
const projectDir = resolve24(projectsDir, slug);
|
|
15056
15656
|
if (await fileExists(projectDir)) {
|
|
15057
15657
|
res.status(409).json({ error: `Project "${slug}" already exists` });
|
|
15058
15658
|
return;
|
|
15059
15659
|
}
|
|
15060
15660
|
const title = fields.title;
|
|
15061
15661
|
const timestamp = fields.created || nowTimestamp();
|
|
15062
|
-
await ensureDir(
|
|
15063
|
-
await ensureDir(
|
|
15064
|
-
await ensureDir(
|
|
15065
|
-
await writeFileForce(
|
|
15662
|
+
await ensureDir(resolve24(projectDir, "assignments"));
|
|
15663
|
+
await ensureDir(resolve24(projectDir, "resources"));
|
|
15664
|
+
await ensureDir(resolve24(projectDir, "memories"));
|
|
15665
|
+
await writeFileForce(resolve24(projectDir, "project.md"), content);
|
|
15066
15666
|
try {
|
|
15067
15667
|
const companions = [
|
|
15068
|
-
[
|
|
15069
|
-
[
|
|
15070
|
-
[
|
|
15071
|
-
[
|
|
15072
|
-
[
|
|
15073
|
-
[
|
|
15074
|
-
[
|
|
15668
|
+
[resolve24(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
|
|
15669
|
+
[resolve24(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
|
|
15670
|
+
[resolve24(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
|
|
15671
|
+
[resolve24(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
|
|
15672
|
+
[resolve24(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
|
|
15673
|
+
[resolve24(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
|
|
15674
|
+
[resolve24(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
|
|
15075
15675
|
];
|
|
15076
15676
|
for (const [filePath, fileContent] of companions) {
|
|
15077
15677
|
await writeFileForce(filePath, fileContent);
|
|
@@ -15092,8 +15692,8 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
15092
15692
|
router.post("/api/projects/:slug/assignments", async (req2, res) => {
|
|
15093
15693
|
try {
|
|
15094
15694
|
const projectSlug = getParam(req2.params.slug);
|
|
15095
|
-
const projectDir =
|
|
15096
|
-
const projectMdPath =
|
|
15695
|
+
const projectDir = resolve24(projectsDir, projectSlug);
|
|
15696
|
+
const projectMdPath = resolve24(projectDir, "project.md");
|
|
15097
15697
|
if (!await fileExists(projectMdPath)) {
|
|
15098
15698
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
15099
15699
|
return;
|
|
@@ -15123,7 +15723,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
15123
15723
|
res.status(400).json({ error: `Invalid priority "${priority}". Must be low, medium, high, or critical.` });
|
|
15124
15724
|
return;
|
|
15125
15725
|
}
|
|
15126
|
-
const assignmentDir =
|
|
15726
|
+
const assignmentDir = resolve24(projectDir, "assignments", assignmentSlug);
|
|
15127
15727
|
if (await fileExists(assignmentDir)) {
|
|
15128
15728
|
res.status(409).json({
|
|
15129
15729
|
error: `Assignment "${assignmentSlug}" already exists in project "${projectSlug}"`
|
|
@@ -15141,12 +15741,12 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
15141
15741
|
command: "create",
|
|
15142
15742
|
by: null
|
|
15143
15743
|
}) : content;
|
|
15144
|
-
await writeFileForce(
|
|
15744
|
+
await writeFileForce(resolve24(assignmentDir, "assignment.md"), seededContent);
|
|
15145
15745
|
try {
|
|
15146
15746
|
const companions = [
|
|
15147
|
-
[
|
|
15148
|
-
[
|
|
15149
|
-
[
|
|
15747
|
+
[resolve24(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
|
|
15748
|
+
[resolve24(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
|
|
15749
|
+
[resolve24(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
|
|
15150
15750
|
];
|
|
15151
15751
|
for (const [filePath, fileContent] of companions) {
|
|
15152
15752
|
await writeFileForce(filePath, fileContent);
|
|
@@ -15174,7 +15774,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
15174
15774
|
router.patch("/api/projects/:slug", async (req2, res) => {
|
|
15175
15775
|
try {
|
|
15176
15776
|
const projectSlug = getParam(req2.params.slug);
|
|
15177
|
-
const projectPath =
|
|
15777
|
+
const projectPath = resolve24(projectsDir, projectSlug, "project.md");
|
|
15178
15778
|
const currentContent = await readCurrentDocument(projectPath);
|
|
15179
15779
|
if (!currentContent) {
|
|
15180
15780
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
@@ -15207,7 +15807,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
15207
15807
|
try {
|
|
15208
15808
|
const projectSlug = getParam(req2.params.slug);
|
|
15209
15809
|
const assignmentSlug = getParam(req2.params.aslug);
|
|
15210
|
-
const assignmentPath =
|
|
15810
|
+
const assignmentPath = resolve24(
|
|
15211
15811
|
projectsDir,
|
|
15212
15812
|
projectSlug,
|
|
15213
15813
|
"assignments",
|
|
@@ -15273,7 +15873,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
15273
15873
|
try {
|
|
15274
15874
|
const projectSlug = getParam(req2.params.slug);
|
|
15275
15875
|
const assignmentSlug = getParam(req2.params.aslug);
|
|
15276
|
-
const assignmentPath =
|
|
15876
|
+
const assignmentPath = resolve24(
|
|
15277
15877
|
projectsDir,
|
|
15278
15878
|
projectSlug,
|
|
15279
15879
|
"assignments",
|
|
@@ -15309,7 +15909,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
15309
15909
|
try {
|
|
15310
15910
|
const projectSlug = getParam(req2.params.slug);
|
|
15311
15911
|
const assignmentSlug = getParam(req2.params.aslug);
|
|
15312
|
-
const planPath =
|
|
15912
|
+
const planPath = resolve24(
|
|
15313
15913
|
projectsDir,
|
|
15314
15914
|
projectSlug,
|
|
15315
15915
|
"assignments",
|
|
@@ -15347,7 +15947,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
15347
15947
|
try {
|
|
15348
15948
|
const projectSlug = getParam(req2.params.slug);
|
|
15349
15949
|
const assignmentSlug = getParam(req2.params.aslug);
|
|
15350
|
-
const scratchpadPath =
|
|
15950
|
+
const scratchpadPath = resolve24(
|
|
15351
15951
|
projectsDir,
|
|
15352
15952
|
projectSlug,
|
|
15353
15953
|
"assignments",
|
|
@@ -15385,7 +15985,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
15385
15985
|
try {
|
|
15386
15986
|
const projectSlug = getParam(req2.params.slug);
|
|
15387
15987
|
const assignmentSlug = getParam(req2.params.aslug);
|
|
15388
|
-
const handoffPath =
|
|
15988
|
+
const handoffPath = resolve24(
|
|
15389
15989
|
projectsDir,
|
|
15390
15990
|
projectSlug,
|
|
15391
15991
|
"assignments",
|
|
@@ -15423,7 +16023,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
15423
16023
|
try {
|
|
15424
16024
|
const projectSlug = getParam(req2.params.slug);
|
|
15425
16025
|
const assignmentSlug = getParam(req2.params.aslug);
|
|
15426
|
-
const decisionPath =
|
|
16026
|
+
const decisionPath = resolve24(
|
|
15427
16027
|
projectsDir,
|
|
15428
16028
|
projectSlug,
|
|
15429
16029
|
"assignments",
|
|
@@ -15461,7 +16061,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
15461
16061
|
try {
|
|
15462
16062
|
const projectSlug = getParam(req2.params.slug);
|
|
15463
16063
|
const assignmentSlug = getParam(req2.params.aslug);
|
|
15464
|
-
const commentsPath =
|
|
16064
|
+
const commentsPath = resolve24(
|
|
15465
16065
|
projectsDir,
|
|
15466
16066
|
projectSlug,
|
|
15467
16067
|
"assignments",
|
|
@@ -15517,7 +16117,7 @@ ${entry}`;
|
|
|
15517
16117
|
}
|
|
15518
16118
|
await writeFileForce(commentsPath, next);
|
|
15519
16119
|
try {
|
|
15520
|
-
const assignmentMdPath =
|
|
16120
|
+
const assignmentMdPath = resolve24(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md");
|
|
15521
16121
|
if (await fileExists(assignmentMdPath)) {
|
|
15522
16122
|
const fm = parseAssignmentFull(await readFile18(assignmentMdPath, "utf-8"));
|
|
15523
16123
|
emitDashboardEvent(fm.id, projectSlug, "comment-added", {
|
|
@@ -15542,7 +16142,7 @@ ${entry}`;
|
|
|
15542
16142
|
const projectSlug = getParam(req2.params.slug);
|
|
15543
16143
|
const assignmentSlug = getParam(req2.params.aslug);
|
|
15544
16144
|
const commentId = getParam(req2.params.commentId);
|
|
15545
|
-
const commentsPath =
|
|
16145
|
+
const commentsPath = resolve24(
|
|
15546
16146
|
projectsDir,
|
|
15547
16147
|
projectSlug,
|
|
15548
16148
|
"assignments",
|
|
@@ -15585,7 +16185,7 @@ ${entry}`;
|
|
|
15585
16185
|
await writeFileForce(commentsPath, withUpdated);
|
|
15586
16186
|
if (target.resolved !== true && resolved === true) {
|
|
15587
16187
|
try {
|
|
15588
|
-
const assignmentMdPath =
|
|
16188
|
+
const assignmentMdPath = resolve24(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md");
|
|
15589
16189
|
if (await fileExists(assignmentMdPath)) {
|
|
15590
16190
|
const fm = parseAssignmentFull(await readFile18(assignmentMdPath, "utf-8"));
|
|
15591
16191
|
emitDashboardEvent(fm.id, projectSlug, "comment-resolved", { commentId });
|
|
@@ -15603,7 +16203,7 @@ ${entry}`;
|
|
|
15603
16203
|
router.post("/api/projects/:slug/move-workspace", async (req2, res) => {
|
|
15604
16204
|
try {
|
|
15605
16205
|
const projectSlug = getParam(req2.params.slug);
|
|
15606
|
-
const projectPath =
|
|
16206
|
+
const projectPath = resolve24(projectsDir, projectSlug, "project.md");
|
|
15607
16207
|
if (!await fileExists(projectPath)) {
|
|
15608
16208
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
15609
16209
|
return;
|
|
@@ -15651,7 +16251,7 @@ ${entry}`;
|
|
|
15651
16251
|
});
|
|
15652
16252
|
return;
|
|
15653
16253
|
}
|
|
15654
|
-
const assignmentPath =
|
|
16254
|
+
const assignmentPath = resolve24(resolved.assignmentDir, "assignment.md");
|
|
15655
16255
|
let content = await readFile18(assignmentPath, "utf-8");
|
|
15656
16256
|
content = setTopLevelField(content, "workspaceGroup", workspaceGroup ?? null);
|
|
15657
16257
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
@@ -15668,7 +16268,7 @@ ${entry}`;
|
|
|
15668
16268
|
async (req2, res) => {
|
|
15669
16269
|
try {
|
|
15670
16270
|
const projectSlug = getParam(req2.params.slug);
|
|
15671
|
-
const projectPath =
|
|
16271
|
+
const projectPath = resolve24(projectsDir, projectSlug, "project.md");
|
|
15672
16272
|
if (!await fileExists(projectPath)) {
|
|
15673
16273
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
15674
16274
|
return;
|
|
@@ -15727,7 +16327,7 @@ ${entry}`;
|
|
|
15727
16327
|
try {
|
|
15728
16328
|
const projectSlug = getParam(req2.params.slug);
|
|
15729
16329
|
const assignmentSlug = getParam(req2.params.aslug);
|
|
15730
|
-
const assignmentPath =
|
|
16330
|
+
const assignmentPath = resolve24(
|
|
15731
16331
|
projectsDir,
|
|
15732
16332
|
projectSlug,
|
|
15733
16333
|
"assignments",
|
|
@@ -15776,7 +16376,7 @@ ${entry}`;
|
|
|
15776
16376
|
try {
|
|
15777
16377
|
const projectSlug = getParam(req2.params.slug);
|
|
15778
16378
|
const assignmentSlug = getParam(req2.params.aslug);
|
|
15779
|
-
const assignmentPath =
|
|
16379
|
+
const assignmentPath = resolve24(
|
|
15780
16380
|
projectsDir,
|
|
15781
16381
|
projectSlug,
|
|
15782
16382
|
"assignments",
|
|
@@ -15835,7 +16435,7 @@ ${entry}`;
|
|
|
15835
16435
|
try {
|
|
15836
16436
|
const projectSlug = getParam(req2.params.slug);
|
|
15837
16437
|
const assignmentSlug = getParam(req2.params.aslug);
|
|
15838
|
-
const assignmentPath =
|
|
16438
|
+
const assignmentPath = resolve24(
|
|
15839
16439
|
projectsDir,
|
|
15840
16440
|
projectSlug,
|
|
15841
16441
|
"assignments",
|
|
@@ -15868,7 +16468,7 @@ ${entry}`;
|
|
|
15868
16468
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
15869
16469
|
return;
|
|
15870
16470
|
}
|
|
15871
|
-
const assignmentPath =
|
|
16471
|
+
const assignmentPath = resolve24(resolved.assignmentDir, "assignment.md");
|
|
15872
16472
|
const parsedForSlug = parseAssignmentFull(await readFile18(assignmentPath, "utf-8"));
|
|
15873
16473
|
const assignmentSlugForBranch = parsedForSlug.slug || resolved.id;
|
|
15874
16474
|
await handleWorktreeCreate(req2, res, {
|
|
@@ -15925,7 +16525,7 @@ ${entry}`;
|
|
|
15925
16525
|
router.post("/api/projects/:slug/status-override", async (req2, res) => {
|
|
15926
16526
|
try {
|
|
15927
16527
|
const projectSlug = getParam(req2.params.slug);
|
|
15928
|
-
const projectPath =
|
|
16528
|
+
const projectPath = resolve24(projectsDir, projectSlug, "project.md");
|
|
15929
16529
|
if (!await fileExists(projectPath)) {
|
|
15930
16530
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
15931
16531
|
return;
|
|
@@ -15952,7 +16552,7 @@ ${entry}`;
|
|
|
15952
16552
|
try {
|
|
15953
16553
|
const projectSlug = getParam(req2.params.slug);
|
|
15954
16554
|
const assignmentSlug = getParam(req2.params.aslug);
|
|
15955
|
-
const assignmentPath =
|
|
16555
|
+
const assignmentPath = resolve24(
|
|
15956
16556
|
projectsDir,
|
|
15957
16557
|
projectSlug,
|
|
15958
16558
|
"assignments",
|
|
@@ -15983,7 +16583,7 @@ ${entry}`;
|
|
|
15983
16583
|
const result = await recomputeAndWrite2(assignmentPath, {
|
|
15984
16584
|
cause: clearing ? "unpin" : "pin",
|
|
15985
16585
|
by: "human",
|
|
15986
|
-
projectDir:
|
|
16586
|
+
projectDir: resolve24(projectsDir, projectSlug),
|
|
15987
16587
|
context,
|
|
15988
16588
|
mutate: (content) => {
|
|
15989
16589
|
if (clearing) return updateOverride2(content, null);
|
|
@@ -16014,7 +16614,7 @@ ${entry}`;
|
|
|
16014
16614
|
router.post("/api/projects/:slug/archive", async (req2, res) => {
|
|
16015
16615
|
try {
|
|
16016
16616
|
const projectSlug = getParam(req2.params.slug);
|
|
16017
|
-
const projectPath =
|
|
16617
|
+
const projectPath = resolve24(projectsDir, projectSlug, "project.md");
|
|
16018
16618
|
if (!await fileExists(projectPath)) {
|
|
16019
16619
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
16020
16620
|
return;
|
|
@@ -16031,7 +16631,7 @@ ${entry}`;
|
|
|
16031
16631
|
router.post("/api/projects/:slug/unarchive", async (req2, res) => {
|
|
16032
16632
|
try {
|
|
16033
16633
|
const projectSlug = getParam(req2.params.slug);
|
|
16034
|
-
const projectPath =
|
|
16634
|
+
const projectPath = resolve24(projectsDir, projectSlug, "project.md");
|
|
16035
16635
|
if (!await fileExists(projectPath)) {
|
|
16036
16636
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
16037
16637
|
return;
|
|
@@ -16048,7 +16648,7 @@ ${entry}`;
|
|
|
16048
16648
|
async function handleAssignmentArchive(req2, res, archived) {
|
|
16049
16649
|
const projectSlug = getParam(req2.params.slug);
|
|
16050
16650
|
const assignmentSlug = getParam(req2.params.aslug);
|
|
16051
|
-
const assignmentPath =
|
|
16651
|
+
const assignmentPath = resolve24(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md");
|
|
16052
16652
|
if (!await fileExists(assignmentPath)) {
|
|
16053
16653
|
res.status(404).json({ error: "Assignment not found" });
|
|
16054
16654
|
return;
|
|
@@ -16088,7 +16688,7 @@ ${entry}`;
|
|
|
16088
16688
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
16089
16689
|
return;
|
|
16090
16690
|
}
|
|
16091
|
-
const assignmentPath =
|
|
16691
|
+
const assignmentPath = resolve24(resolved.assignmentDir, "assignment.md");
|
|
16092
16692
|
const content = await readFile18(assignmentPath, "utf-8");
|
|
16093
16693
|
const reason = archived ? archiveReason(req2.body) : null;
|
|
16094
16694
|
await writeFileForce(assignmentPath, applyArchiveFields(content, archived, reason));
|
|
@@ -16117,7 +16717,7 @@ ${entry}`;
|
|
|
16117
16717
|
try {
|
|
16118
16718
|
const projectSlug = getParam(req2.params.slug);
|
|
16119
16719
|
const assignmentSlug = getParam(req2.params.aslug);
|
|
16120
|
-
const assignmentPath =
|
|
16720
|
+
const assignmentPath = resolve24(
|
|
16121
16721
|
projectsDir,
|
|
16122
16722
|
projectSlug,
|
|
16123
16723
|
"assignments",
|
|
@@ -16155,7 +16755,7 @@ ${entry}`;
|
|
|
16155
16755
|
try {
|
|
16156
16756
|
const projectSlug = getParam(req2.params.slug);
|
|
16157
16757
|
const assignmentSlug = getParam(req2.params.aslug);
|
|
16158
|
-
const assignmentPath =
|
|
16758
|
+
const assignmentPath = resolve24(
|
|
16159
16759
|
projectsDir,
|
|
16160
16760
|
projectSlug,
|
|
16161
16761
|
"assignments",
|
|
@@ -16193,8 +16793,8 @@ ${entry}`;
|
|
|
16193
16793
|
res.status(400).json({ error: `Unsupported transition command "${req2.params.command}"` });
|
|
16194
16794
|
return;
|
|
16195
16795
|
}
|
|
16196
|
-
const projectDir =
|
|
16197
|
-
const assignmentPath =
|
|
16796
|
+
const projectDir = resolve24(projectsDir, projectSlug);
|
|
16797
|
+
const assignmentPath = resolve24(projectDir, "assignments", assignmentSlug, "assignment.md");
|
|
16198
16798
|
if (!await fileExists(assignmentPath)) {
|
|
16199
16799
|
res.status(404).json({ error: "Assignment not found" });
|
|
16200
16800
|
return;
|
|
@@ -16275,8 +16875,8 @@ ${entry}`;
|
|
|
16275
16875
|
try {
|
|
16276
16876
|
const projectSlug = getParam(req2.params.slug);
|
|
16277
16877
|
const assignmentSlug = getParam(req2.params.aslug);
|
|
16278
|
-
const assignmentDir =
|
|
16279
|
-
const assignmentPath =
|
|
16878
|
+
const assignmentDir = resolve24(projectsDir, projectSlug, "assignments", assignmentSlug);
|
|
16879
|
+
const assignmentPath = resolve24(assignmentDir, "assignment.md");
|
|
16280
16880
|
if (!await fileExists(assignmentPath)) {
|
|
16281
16881
|
res.status(404).json({ error: `Assignment "${assignmentSlug}" not found in project "${projectSlug}"` });
|
|
16282
16882
|
return;
|
|
@@ -16331,7 +16931,7 @@ ${entry}`;
|
|
|
16331
16931
|
return;
|
|
16332
16932
|
}
|
|
16333
16933
|
const id2 = generateId();
|
|
16334
|
-
const assignmentDir2 =
|
|
16934
|
+
const assignmentDir2 = resolve24(assignmentsDir2, id2);
|
|
16335
16935
|
if (await fileExists(assignmentDir2)) {
|
|
16336
16936
|
res.status(500).json({ error: "UUID collision \u2014 try again" });
|
|
16337
16937
|
return;
|
|
@@ -16350,25 +16950,25 @@ ${entry}`;
|
|
|
16350
16950
|
by: null
|
|
16351
16951
|
});
|
|
16352
16952
|
}
|
|
16353
|
-
await writeFileForce(
|
|
16953
|
+
await writeFileForce(resolve24(assignmentDir2, "assignment.md"), normalizedContent);
|
|
16354
16954
|
await writeFileForce(
|
|
16355
|
-
|
|
16955
|
+
resolve24(assignmentDir2, "scratchpad.md"),
|
|
16356
16956
|
renderScratchpad({ assignmentSlug: id2, timestamp: timestamp2 })
|
|
16357
16957
|
);
|
|
16358
16958
|
await writeFileForce(
|
|
16359
|
-
|
|
16959
|
+
resolve24(assignmentDir2, "handoff.md"),
|
|
16360
16960
|
renderHandoff({ assignmentSlug: id2, timestamp: timestamp2 })
|
|
16361
16961
|
);
|
|
16362
16962
|
await writeFileForce(
|
|
16363
|
-
|
|
16963
|
+
resolve24(assignmentDir2, "decision-record.md"),
|
|
16364
16964
|
renderDecisionRecord({ assignmentSlug: id2, timestamp: timestamp2 })
|
|
16365
16965
|
);
|
|
16366
16966
|
await writeFileForce(
|
|
16367
|
-
|
|
16967
|
+
resolve24(assignmentDir2, "progress.md"),
|
|
16368
16968
|
renderProgress({ assignment: id2, timestamp: timestamp2 })
|
|
16369
16969
|
);
|
|
16370
16970
|
await writeFileForce(
|
|
16371
|
-
|
|
16971
|
+
resolve24(assignmentDir2, "comments.md"),
|
|
16372
16972
|
renderComments({ assignment: id2, timestamp: timestamp2 })
|
|
16373
16973
|
);
|
|
16374
16974
|
if (seededHere) {
|
|
@@ -16393,7 +16993,7 @@ ${entry}`;
|
|
|
16393
16993
|
return;
|
|
16394
16994
|
}
|
|
16395
16995
|
const id = generateId();
|
|
16396
|
-
const assignmentDir =
|
|
16996
|
+
const assignmentDir = resolve24(assignmentsDir2, id);
|
|
16397
16997
|
if (await fileExists(assignmentDir)) {
|
|
16398
16998
|
res.status(500).json({ error: "UUID collision \u2014 try again" });
|
|
16399
16999
|
return;
|
|
@@ -16413,25 +17013,25 @@ ${entry}`;
|
|
|
16413
17013
|
project: null,
|
|
16414
17014
|
type: typeof type === "string" ? type : void 0
|
|
16415
17015
|
});
|
|
16416
|
-
await writeFileForce(
|
|
17016
|
+
await writeFileForce(resolve24(assignmentDir, "assignment.md"), assignmentContent);
|
|
16417
17017
|
await writeFileForce(
|
|
16418
|
-
|
|
17018
|
+
resolve24(assignmentDir, "scratchpad.md"),
|
|
16419
17019
|
renderScratchpad({ assignmentSlug: id, timestamp })
|
|
16420
17020
|
);
|
|
16421
17021
|
await writeFileForce(
|
|
16422
|
-
|
|
17022
|
+
resolve24(assignmentDir, "handoff.md"),
|
|
16423
17023
|
renderHandoff({ assignmentSlug: id, timestamp })
|
|
16424
17024
|
);
|
|
16425
17025
|
await writeFileForce(
|
|
16426
|
-
|
|
17026
|
+
resolve24(assignmentDir, "decision-record.md"),
|
|
16427
17027
|
renderDecisionRecord({ assignmentSlug: id, timestamp })
|
|
16428
17028
|
);
|
|
16429
17029
|
await writeFileForce(
|
|
16430
|
-
|
|
17030
|
+
resolve24(assignmentDir, "progress.md"),
|
|
16431
17031
|
renderProgress({ assignment: id, timestamp })
|
|
16432
17032
|
);
|
|
16433
17033
|
await writeFileForce(
|
|
16434
|
-
|
|
17034
|
+
resolve24(assignmentDir, "comments.md"),
|
|
16435
17035
|
renderComments({ assignment: id, timestamp })
|
|
16436
17036
|
);
|
|
16437
17037
|
emitDashboardEvent(id, null, "status-change", {
|
|
@@ -16564,7 +17164,7 @@ ${entry}`;
|
|
|
16564
17164
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
16565
17165
|
return;
|
|
16566
17166
|
}
|
|
16567
|
-
const assignmentPath =
|
|
17167
|
+
const assignmentPath = resolve24(resolved.assignmentDir, "assignment.md");
|
|
16568
17168
|
const currentContent = await readCurrentDocument(assignmentPath);
|
|
16569
17169
|
if (!currentContent) {
|
|
16570
17170
|
res.status(404).json({ error: "Assignment not found" });
|
|
@@ -16629,7 +17229,7 @@ ${entry}`;
|
|
|
16629
17229
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
16630
17230
|
return;
|
|
16631
17231
|
}
|
|
16632
|
-
const planPath =
|
|
17232
|
+
const planPath = resolve24(resolved.assignmentDir, "plan.md");
|
|
16633
17233
|
const currentContent = await readCurrentDocument(planPath);
|
|
16634
17234
|
if (!currentContent) {
|
|
16635
17235
|
res.status(404).json({ error: "Plan not found" });
|
|
@@ -16663,7 +17263,7 @@ ${entry}`;
|
|
|
16663
17263
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
16664
17264
|
return;
|
|
16665
17265
|
}
|
|
16666
|
-
const scratchpadPath =
|
|
17266
|
+
const scratchpadPath = resolve24(resolved.assignmentDir, "scratchpad.md");
|
|
16667
17267
|
const currentContent = await readCurrentDocument(scratchpadPath);
|
|
16668
17268
|
if (!currentContent) {
|
|
16669
17269
|
res.status(404).json({ error: "Scratchpad not found" });
|
|
@@ -16697,7 +17297,7 @@ ${entry}`;
|
|
|
16697
17297
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
16698
17298
|
return;
|
|
16699
17299
|
}
|
|
16700
|
-
const handoffPath =
|
|
17300
|
+
const handoffPath = resolve24(resolved.assignmentDir, "handoff.md");
|
|
16701
17301
|
const currentContent = await readCurrentDocument(handoffPath);
|
|
16702
17302
|
if (!currentContent) {
|
|
16703
17303
|
res.status(404).json({ error: "Handoff log not found" });
|
|
@@ -16737,7 +17337,7 @@ ${entry}`;
|
|
|
16737
17337
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
16738
17338
|
return;
|
|
16739
17339
|
}
|
|
16740
|
-
const decisionPath =
|
|
17340
|
+
const decisionPath = resolve24(resolved.assignmentDir, "decision-record.md");
|
|
16741
17341
|
const currentContent = await readCurrentDocument(decisionPath);
|
|
16742
17342
|
if (!currentContent) {
|
|
16743
17343
|
res.status(404).json({ error: "Decision record not found" });
|
|
@@ -16777,7 +17377,7 @@ ${entry}`;
|
|
|
16777
17377
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
16778
17378
|
return;
|
|
16779
17379
|
}
|
|
16780
|
-
const assignmentPath =
|
|
17380
|
+
const assignmentPath = resolve24(resolved.assignmentDir, "assignment.md");
|
|
16781
17381
|
if (!await fileExists(assignmentPath)) {
|
|
16782
17382
|
res.status(404).json({ error: "Assignment not found" });
|
|
16783
17383
|
return;
|
|
@@ -16799,7 +17399,7 @@ ${entry}`;
|
|
|
16799
17399
|
const { recomputeAndWrite: recomputeAndWrite2, resolveDeriveContext: resolveDeriveContext2 } = await Promise.resolve().then(() => (init_recompute(), recompute_exports));
|
|
16800
17400
|
const { updateOverride: updateOverride2 } = await Promise.resolve().then(() => (init_frontmatter(), frontmatter_exports));
|
|
16801
17401
|
const context = await resolveDeriveContext2();
|
|
16802
|
-
const projectDirForId = resolved.standalone ? null :
|
|
17402
|
+
const projectDirForId = resolved.standalone ? null : resolve24(resolved.assignmentDir, "..", "..");
|
|
16803
17403
|
const result = await recomputeAndWrite2(assignmentPath, {
|
|
16804
17404
|
cause: clearing ? "unpin" : "pin",
|
|
16805
17405
|
by: "human",
|
|
@@ -16839,7 +17439,7 @@ ${entry}`;
|
|
|
16839
17439
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
16840
17440
|
return;
|
|
16841
17441
|
}
|
|
16842
|
-
const assignmentPath =
|
|
17442
|
+
const assignmentPath = resolve24(resolved.assignmentDir, "assignment.md");
|
|
16843
17443
|
if (!await fileExists(assignmentPath)) {
|
|
16844
17444
|
res.status(404).json({ error: "Assignment not found" });
|
|
16845
17445
|
return;
|
|
@@ -16879,7 +17479,7 @@ ${entry}`;
|
|
|
16879
17479
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
16880
17480
|
return;
|
|
16881
17481
|
}
|
|
16882
|
-
const assignmentPath =
|
|
17482
|
+
const assignmentPath = resolve24(resolved.assignmentDir, "assignment.md");
|
|
16883
17483
|
if (!await fileExists(assignmentPath)) {
|
|
16884
17484
|
res.status(404).json({ error: "Assignment not found" });
|
|
16885
17485
|
return;
|
|
@@ -16912,7 +17512,7 @@ ${entry}`;
|
|
|
16912
17512
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
16913
17513
|
return;
|
|
16914
17514
|
}
|
|
16915
|
-
const assignmentPath =
|
|
17515
|
+
const assignmentPath = resolve24(resolved.assignmentDir, "assignment.md");
|
|
16916
17516
|
const currentContent = await readCurrentDocument(assignmentPath);
|
|
16917
17517
|
if (!currentContent) {
|
|
16918
17518
|
res.status(404).json({ error: "Assignment not found" });
|
|
@@ -16960,8 +17560,8 @@ ${entry}`;
|
|
|
16960
17560
|
}
|
|
16961
17561
|
const { recomputeAndWrite: recomputeAndWrite2, recomputeDependents: recomputeDependents2, resolveDeriveContext: resolveDeriveContext2 } = await Promise.resolve().then(() => (init_recompute(), recompute_exports));
|
|
16962
17562
|
const context = await resolveDeriveContext2();
|
|
16963
|
-
const byIdPath =
|
|
16964
|
-
const byIdProjectDir = resolved.standalone ? null :
|
|
17563
|
+
const byIdPath = resolve24(resolved.assignmentDir, "assignment.md");
|
|
17564
|
+
const byIdProjectDir = resolved.standalone ? null : resolve24(resolved.assignmentDir, "..", "..");
|
|
16965
17565
|
if (command === "block" || command === "unblock") {
|
|
16966
17566
|
const { updateAssignmentFile: updateAssignmentFile2 } = await Promise.resolve().then(() => (init_frontmatter(), frontmatter_exports));
|
|
16967
17567
|
const result = await recomputeAndWrite2(byIdPath, {
|
|
@@ -17082,7 +17682,7 @@ function validateTitleBody(body) {
|
|
|
17082
17682
|
return { ok: true, value: trimmed };
|
|
17083
17683
|
}
|
|
17084
17684
|
async function appendCommentTo(assignmentDir, assignmentRef, req2, res, reloadDetail) {
|
|
17085
|
-
const commentsPath =
|
|
17685
|
+
const commentsPath = resolve24(assignmentDir, "comments.md");
|
|
17086
17686
|
const { body, author, type, replyTo } = req2.body || {};
|
|
17087
17687
|
if (!body || typeof body !== "string" || !body.trim()) {
|
|
17088
17688
|
res.status(400).json({ error: "body is required" });
|
|
@@ -17129,7 +17729,7 @@ ${entry}`;
|
|
|
17129
17729
|
}
|
|
17130
17730
|
await writeFileForce(commentsPath, next);
|
|
17131
17731
|
try {
|
|
17132
|
-
const assignmentMdPath =
|
|
17732
|
+
const assignmentMdPath = resolve24(assignmentDir, "assignment.md");
|
|
17133
17733
|
if (await fileExists(assignmentMdPath)) {
|
|
17134
17734
|
const fm = parseAssignmentFull(await readFile18(assignmentMdPath, "utf-8"));
|
|
17135
17735
|
emitDashboardEvent(fm.id, fm.project, "comment-added", {
|
|
@@ -17146,7 +17746,7 @@ ${entry}`;
|
|
|
17146
17746
|
res.status(201).json({ assignment, comment: { id: comment.id } });
|
|
17147
17747
|
}
|
|
17148
17748
|
async function toggleCommentResolvedAt(assignmentDir, commentId, req2, res, reloadDetail) {
|
|
17149
|
-
const commentsPath =
|
|
17749
|
+
const commentsPath = resolve24(assignmentDir, "comments.md");
|
|
17150
17750
|
if (!await fileExists(commentsPath)) {
|
|
17151
17751
|
res.status(404).json({ error: "Comments file not found" });
|
|
17152
17752
|
return;
|
|
@@ -17180,7 +17780,7 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req2, res, relo
|
|
|
17180
17780
|
await writeFileForce(commentsPath, withUpdated);
|
|
17181
17781
|
if (target.resolved !== true && desired === true) {
|
|
17182
17782
|
try {
|
|
17183
|
-
const assignmentMdPath =
|
|
17783
|
+
const assignmentMdPath = resolve24(assignmentDir, "assignment.md");
|
|
17184
17784
|
if (await fileExists(assignmentMdPath)) {
|
|
17185
17785
|
const fm = parseAssignmentFull(await readFile18(assignmentMdPath, "utf-8"));
|
|
17186
17786
|
emitDashboardEvent(fm.id, fm.project, "comment-resolved", { commentId });
|
|
@@ -17312,9 +17912,12 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir2) {
|
|
|
17312
17912
|
// src/dashboard/api-agent-sessions.ts
|
|
17313
17913
|
init_agent_sessions();
|
|
17314
17914
|
init_fs();
|
|
17915
|
+
init_session_id();
|
|
17916
|
+
init_assignment_resolver();
|
|
17917
|
+
init_paths();
|
|
17315
17918
|
init_transcript();
|
|
17316
17919
|
import { Router as Router4 } from "express";
|
|
17317
|
-
import { resolve as
|
|
17920
|
+
import { resolve as resolve25 } from "path";
|
|
17318
17921
|
init_config2();
|
|
17319
17922
|
init_process_info();
|
|
17320
17923
|
init_git_worktree();
|
|
@@ -17338,7 +17941,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
|
|
|
17338
17941
|
try {
|
|
17339
17942
|
const { projectSlug } = req2.params;
|
|
17340
17943
|
const assignment = req2.query.assignment;
|
|
17341
|
-
const projectDir =
|
|
17944
|
+
const projectDir = resolve25(projectsDir, projectSlug);
|
|
17342
17945
|
if (!await fileExists(projectDir)) {
|
|
17343
17946
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
17344
17947
|
return;
|
|
@@ -17367,21 +17970,45 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
|
|
|
17367
17970
|
});
|
|
17368
17971
|
return;
|
|
17369
17972
|
}
|
|
17973
|
+
if (!isSafeSessionId(sessionId)) {
|
|
17974
|
+
res.status(400).json({
|
|
17975
|
+
error: "sessionId is malformed. Pass the real agent-generated session id."
|
|
17976
|
+
});
|
|
17977
|
+
return;
|
|
17978
|
+
}
|
|
17370
17979
|
if (projectSlug) {
|
|
17371
|
-
const projectDir =
|
|
17980
|
+
const projectDir = resolve25(projectsDir, projectSlug);
|
|
17372
17981
|
if (!await fileExists(projectDir)) {
|
|
17373
17982
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
17374
17983
|
return;
|
|
17375
17984
|
}
|
|
17376
17985
|
}
|
|
17986
|
+
let assignmentId = null;
|
|
17987
|
+
if (assignmentSlug) {
|
|
17988
|
+
const resolvedAssignment = await resolveAssignmentBySlug(
|
|
17989
|
+
projectsDir,
|
|
17990
|
+
assignmentsDir2 ?? assignmentsDir(),
|
|
17991
|
+
projectSlug || null,
|
|
17992
|
+
assignmentSlug
|
|
17993
|
+
);
|
|
17994
|
+
if (!resolvedAssignment.exists) {
|
|
17995
|
+
res.status(404).json({ error: `Assignment "${assignmentSlug}" not found` });
|
|
17996
|
+
return;
|
|
17997
|
+
}
|
|
17998
|
+
assignmentId = resolvedAssignment.id;
|
|
17999
|
+
}
|
|
17377
18000
|
const derivedPath = await derivePathFromTranscript(transcriptPath);
|
|
17378
18001
|
const recordedPath = derivedPath ?? path ?? "";
|
|
17379
18002
|
const pid = typeof rawPid === "number" && Number.isFinite(rawPid) && rawPid > 0 ? rawPid : null;
|
|
17380
18003
|
const pidStartedAt = pid !== null ? captureProcessStartedAt(pid) : null;
|
|
17381
18004
|
const originalHeadSha = isExistingDir(recordedPath) ? await captureHeadSha(recordedPath) : null;
|
|
17382
18005
|
const session = {
|
|
17383
|
-
|
|
18006
|
+
// L: a POST with no assignmentSlug is registration-only (unbound) — do
|
|
18007
|
+
// NOT open a project-bound engagement for an arbitrary session. Binding
|
|
18008
|
+
// requires a validated assignment selector (existence-checked above).
|
|
18009
|
+
projectSlug: assignmentSlug ? projectSlug || null : null,
|
|
17384
18010
|
assignmentSlug: assignmentSlug || null,
|
|
18011
|
+
assignmentId,
|
|
17385
18012
|
agent,
|
|
17386
18013
|
sessionId,
|
|
17387
18014
|
started: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -17460,6 +18087,10 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
|
|
|
17460
18087
|
broadcast?.({ type: "agent-sessions-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
17461
18088
|
res.json({ updated: true });
|
|
17462
18089
|
} catch (error) {
|
|
18090
|
+
if (error instanceof SessionResurrectionError) {
|
|
18091
|
+
res.status(409).json({ error: error.message });
|
|
18092
|
+
return;
|
|
18093
|
+
}
|
|
17463
18094
|
res.status(500).json({ error: error instanceof Error ? error.message : "Update failed" });
|
|
17464
18095
|
}
|
|
17465
18096
|
});
|
|
@@ -17784,8 +18415,8 @@ import { Router as Router6 } from "express";
|
|
|
17784
18415
|
// src/utils/terminal-probe.ts
|
|
17785
18416
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
17786
18417
|
import { existsSync as existsSync2 } from "fs";
|
|
17787
|
-
import { homedir as
|
|
17788
|
-
import { join as
|
|
18418
|
+
import { homedir as homedir4 } from "os";
|
|
18419
|
+
import { join as join5 } from "path";
|
|
17789
18420
|
var APP_BUNDLE_IDS = {
|
|
17790
18421
|
"terminal-app": "com.apple.Terminal",
|
|
17791
18422
|
iterm: "com.googlecode.iterm2",
|
|
@@ -17802,7 +18433,7 @@ var APP_FIXED_PATHS = {
|
|
|
17802
18433
|
"terminal-app": "/System/Applications/Utilities/Terminal.app"
|
|
17803
18434
|
};
|
|
17804
18435
|
function defaultApplicationsDirs() {
|
|
17805
|
-
return ["/Applications",
|
|
18436
|
+
return ["/Applications", join5(homedir4(), "Applications")];
|
|
17806
18437
|
}
|
|
17807
18438
|
function findAppBundle(terminal, dirs = defaultApplicationsDirs()) {
|
|
17808
18439
|
const fixed = APP_FIXED_PATHS[terminal];
|
|
@@ -17810,7 +18441,7 @@ function findAppBundle(terminal, dirs = defaultApplicationsDirs()) {
|
|
|
17810
18441
|
const bundleName = APP_BUNDLE_NAMES[terminal];
|
|
17811
18442
|
if (bundleName) {
|
|
17812
18443
|
for (const dir of dirs) {
|
|
17813
|
-
const candidate =
|
|
18444
|
+
const candidate = join5(dir, bundleName);
|
|
17814
18445
|
if (existsSync2(candidate)) return candidate;
|
|
17815
18446
|
}
|
|
17816
18447
|
}
|
|
@@ -17840,11 +18471,11 @@ var defaultCmuxLsappinfoRunner = () => {
|
|
|
17840
18471
|
function resolveCmuxCli(applicationsDirsOverride, cliDirsOverride, lsappinfoRunnerOverride) {
|
|
17841
18472
|
const bundle = findAppBundle("cmux", applicationsDirsOverride);
|
|
17842
18473
|
if (bundle) {
|
|
17843
|
-
const cli =
|
|
18474
|
+
const cli = join5(bundle, "Contents/Resources/bin/cmux");
|
|
17844
18475
|
if (existsSync2(cli)) return cli;
|
|
17845
18476
|
}
|
|
17846
18477
|
for (const dir of cliDirsOverride ?? CMUX_CLI_DIRS) {
|
|
17847
|
-
const cli =
|
|
18478
|
+
const cli = join5(dir, "cmux");
|
|
17848
18479
|
if (existsSync2(cli)) return cli;
|
|
17849
18480
|
}
|
|
17850
18481
|
if (process.platform === "darwin") {
|
|
@@ -17853,7 +18484,7 @@ function resolveCmuxCli(applicationsDirsOverride, cliDirsOverride, lsappinfoRunn
|
|
|
17853
18484
|
if (stdout) {
|
|
17854
18485
|
const match = stdout.match(/"LSBundlePath"="([^"]+)"/);
|
|
17855
18486
|
if (match) {
|
|
17856
|
-
const cli =
|
|
18487
|
+
const cli = join5(match[1], "Contents/Resources/bin/cmux");
|
|
17857
18488
|
if (existsSync2(cli)) return cli;
|
|
17858
18489
|
}
|
|
17859
18490
|
}
|
|
@@ -17992,7 +18623,7 @@ init_agents_schema();
|
|
|
17992
18623
|
init_cwd();
|
|
17993
18624
|
import { spawn as spawn2 } from "child_process";
|
|
17994
18625
|
import { mkdir as mkdir2, writeFile as writeFile4 } from "fs/promises";
|
|
17995
|
-
import { isAbsolute as isAbsolute5, resolve as
|
|
18626
|
+
import { isAbsolute as isAbsolute5, resolve as resolve26 } from "path";
|
|
17996
18627
|
init_paths();
|
|
17997
18628
|
init_playbooks();
|
|
17998
18629
|
init_cwd();
|
|
@@ -18195,6 +18826,12 @@ async function resolveSessionPlan(input, terminal) {
|
|
|
18195
18826
|
}
|
|
18196
18827
|
}
|
|
18197
18828
|
}
|
|
18829
|
+
if (!cwd.trim()) {
|
|
18830
|
+
throw new LaunchError(
|
|
18831
|
+
"workspace-path-invalid",
|
|
18832
|
+
`Session ${input.id} has no recorded working directory and no linked assignment workspace resolved \u2014 refusing to launch in an unknown directory.`
|
|
18833
|
+
);
|
|
18834
|
+
}
|
|
18198
18835
|
const agent = getAgents(input.config).find((a) => a.id === session.agent);
|
|
18199
18836
|
if (!agent) {
|
|
18200
18837
|
throw new LaunchError(
|
|
@@ -18223,7 +18860,7 @@ async function resolveSessionPlan(input, terminal) {
|
|
|
18223
18860
|
// src/launch/execute.ts
|
|
18224
18861
|
import { spawn as spawn3 } from "child_process";
|
|
18225
18862
|
import { homedir as homedir5 } from "os";
|
|
18226
|
-
import { basename as basename5, join as join6, resolve as
|
|
18863
|
+
import { basename as basename5, join as join6, resolve as resolve27 } from "path";
|
|
18227
18864
|
init_fs();
|
|
18228
18865
|
init_config2();
|
|
18229
18866
|
init_session_id();
|
|
@@ -18271,7 +18908,7 @@ async function executeLaunchPlan(plan, spawnFn = realSpawn) {
|
|
|
18271
18908
|
`Spawn failed: ${msg}. Verify the terminal is installed and on PATH.`
|
|
18272
18909
|
);
|
|
18273
18910
|
}
|
|
18274
|
-
await new Promise((
|
|
18911
|
+
await new Promise((resolve49, reject) => {
|
|
18275
18912
|
let settled = false;
|
|
18276
18913
|
let stderr = "";
|
|
18277
18914
|
const finishOk = () => {
|
|
@@ -18281,7 +18918,7 @@ async function executeLaunchPlan(plan, spawnFn = realSpawn) {
|
|
|
18281
18918
|
child.unref();
|
|
18282
18919
|
} catch {
|
|
18283
18920
|
}
|
|
18284
|
-
|
|
18921
|
+
resolve49();
|
|
18285
18922
|
};
|
|
18286
18923
|
const finishErr = (remediation) => {
|
|
18287
18924
|
if (settled) return;
|
|
@@ -18342,7 +18979,7 @@ async function registerLaunchAtBirth(plan, child) {
|
|
|
18342
18979
|
markerDir
|
|
18343
18980
|
);
|
|
18344
18981
|
if (!sessionId) return;
|
|
18345
|
-
if (autoTrack === "workspaces-only" && !await fileExists(
|
|
18982
|
+
if (autoTrack === "workspaces-only" && !await fileExists(resolve27(plan.cwd, ".syntaur", "context.json"))) {
|
|
18346
18983
|
return;
|
|
18347
18984
|
}
|
|
18348
18985
|
const { initSessionDb: initSessionDb2 } = await Promise.resolve().then(() => (init_session_db(), session_db_exports));
|
|
@@ -18518,8 +19155,8 @@ function appleScriptString(value) {
|
|
|
18518
19155
|
init_paths();
|
|
18519
19156
|
init_fs();
|
|
18520
19157
|
import { fileURLToPath } from "url";
|
|
18521
|
-
import { dirname as dirname6, resolve as
|
|
18522
|
-
import { realpathSync, readFileSync as
|
|
19158
|
+
import { dirname as dirname6, resolve as resolve28, join as join7 } from "path";
|
|
19159
|
+
import { realpathSync, readFileSync as readFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
18523
19160
|
|
|
18524
19161
|
// src/dashboard/api-launch-preflight.ts
|
|
18525
19162
|
init_assignment_resolver();
|
|
@@ -19585,7 +20222,7 @@ import { Router as Router12 } from "express";
|
|
|
19585
20222
|
init_paths();
|
|
19586
20223
|
import Database3 from "better-sqlite3";
|
|
19587
20224
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
19588
|
-
import { resolve as
|
|
20225
|
+
import { resolve as resolve29 } from "path";
|
|
19589
20226
|
var db3 = null;
|
|
19590
20227
|
var LEASE_SCHEMA_VERSION = "1";
|
|
19591
20228
|
var SCHEMA_SQL3 = `
|
|
@@ -19672,7 +20309,7 @@ function isBusyError(err) {
|
|
|
19672
20309
|
}
|
|
19673
20310
|
function initLeasesDb(dbPath) {
|
|
19674
20311
|
if (db3) return db3;
|
|
19675
|
-
const finalPath = dbPath ??
|
|
20312
|
+
const finalPath = dbPath ?? resolve29(syntaurRoot(), "syntaur.db");
|
|
19676
20313
|
db3 = new Database3(finalPath);
|
|
19677
20314
|
db3.pragma("journal_mode = WAL");
|
|
19678
20315
|
db3.pragma("busy_timeout = 5000");
|
|
@@ -19825,14 +20462,14 @@ init_paths();
|
|
|
19825
20462
|
init_timestamp();
|
|
19826
20463
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
19827
20464
|
import { readFile as readFile20, readdir as readdir14, rm as rm3 } from "fs/promises";
|
|
19828
|
-
import { resolve as
|
|
20465
|
+
import { resolve as resolve30 } from "path";
|
|
19829
20466
|
function schedulesDir() {
|
|
19830
|
-
const
|
|
19831
|
-
if (
|
|
19832
|
-
return
|
|
20467
|
+
const override3 = process.env.SYNTAUR_SCHEDULES_DIR;
|
|
20468
|
+
if (override3 && override3.length > 0) return resolve30(override3);
|
|
20469
|
+
return resolve30(syntaurRoot(), "schedules");
|
|
19833
20470
|
}
|
|
19834
20471
|
function jobPath(id) {
|
|
19835
|
-
return
|
|
20472
|
+
return resolve30(schedulesDir(), `${jobFileSafe(id)}.md`);
|
|
19836
20473
|
}
|
|
19837
20474
|
function jobFileSafe(id) {
|
|
19838
20475
|
if (!/^[A-Za-z0-9_-]+$/.test(id)) {
|
|
@@ -19975,7 +20612,7 @@ async function listJobs() {
|
|
|
19975
20612
|
for (const name of names) {
|
|
19976
20613
|
if (!name.endsWith(".md")) continue;
|
|
19977
20614
|
try {
|
|
19978
|
-
jobs.push(parseJobFile(await readFile20(
|
|
20615
|
+
jobs.push(parseJobFile(await readFile20(resolve30(schedulesDir(), name), "utf-8")));
|
|
19979
20616
|
} catch {
|
|
19980
20617
|
}
|
|
19981
20618
|
}
|
|
@@ -19986,12 +20623,12 @@ async function listJobs() {
|
|
|
19986
20623
|
// src/schedules/event-log.ts
|
|
19987
20624
|
init_timestamp();
|
|
19988
20625
|
import { appendFile, readFile as readFile21, mkdir as mkdir3 } from "fs/promises";
|
|
19989
|
-
import { dirname as dirname8, resolve as
|
|
20626
|
+
import { dirname as dirname8, resolve as resolve31 } from "path";
|
|
19990
20627
|
function eventLogPath(jobId) {
|
|
19991
20628
|
if (!/^[A-Za-z0-9_-]+$/.test(jobId)) {
|
|
19992
20629
|
throw new Error(`Unsafe schedule id: ${JSON.stringify(jobId)}`);
|
|
19993
20630
|
}
|
|
19994
|
-
return
|
|
20631
|
+
return resolve31(schedulesDir(), `${jobId}.jsonl`);
|
|
19995
20632
|
}
|
|
19996
20633
|
async function appendEvent(jobId, type, data) {
|
|
19997
20634
|
const event = { type, at: nowTimestamp(), ...data ? { data } : {} };
|
|
@@ -20024,7 +20661,7 @@ async function readEvents(jobId) {
|
|
|
20024
20661
|
// src/schedules/attempt.ts
|
|
20025
20662
|
import { createHash as createHash3 } from "crypto";
|
|
20026
20663
|
import { open as open4, readFile as readFile22, stat as stat5, unlink as unlink6 } from "fs/promises";
|
|
20027
|
-
import { resolve as
|
|
20664
|
+
import { resolve as resolve32 } from "path";
|
|
20028
20665
|
|
|
20029
20666
|
// src/schedules/triggers.ts
|
|
20030
20667
|
import { Cron } from "croner";
|
|
@@ -20044,8 +20681,7 @@ function predictReset(_provider, anchor) {
|
|
|
20044
20681
|
}
|
|
20045
20682
|
function verifyReset(provider, anchor, now) {
|
|
20046
20683
|
const reset = predictReset(provider, anchor);
|
|
20047
|
-
|
|
20048
|
-
return { eligible: false, rescheduleToIso: reset };
|
|
20684
|
+
return { eligible: now.getTime() >= Date.parse(reset) };
|
|
20049
20685
|
}
|
|
20050
20686
|
|
|
20051
20687
|
// src/schedules/triggers.ts
|
|
@@ -20069,13 +20705,15 @@ function evaluateKind(trigger, job, ctx) {
|
|
|
20069
20705
|
switch (trigger.kind) {
|
|
20070
20706
|
case "at": {
|
|
20071
20707
|
const at = Date.parse(trigger.at);
|
|
20072
|
-
const
|
|
20708
|
+
const beforeCreation = !Number.isNaN(createdAtMs) && at < createdAtMs;
|
|
20709
|
+
const due = !Number.isNaN(at) && ctx.now.getTime() >= at && !beforeCreation;
|
|
20073
20710
|
return { due, dedupeKey: `at:${trigger.at}`, nextFireIso: trigger.at };
|
|
20074
20711
|
}
|
|
20075
20712
|
case "in": {
|
|
20076
20713
|
const anchor = Date.parse(trigger.anchorIso);
|
|
20077
20714
|
const fireAt = anchor + trigger.durationMs;
|
|
20078
|
-
const
|
|
20715
|
+
const beforeCreation = !Number.isNaN(createdAtMs) && fireAt < createdAtMs;
|
|
20716
|
+
const due = !Number.isNaN(anchor) && ctx.now.getTime() >= fireAt && !beforeCreation;
|
|
20079
20717
|
const fireIso = Number.isNaN(anchor) ? null : iso(new Date(fireAt));
|
|
20080
20718
|
return { due, dedupeKey: `in:${fireAt}`, nextFireIso: fireIso };
|
|
20081
20719
|
}
|
|
@@ -20109,7 +20747,7 @@ function evaluateAfterReset(trigger, now) {
|
|
|
20109
20747
|
if (v.eligible) {
|
|
20110
20748
|
return { due: true, dedupeKey: `after-reset:${trigger.anchor.windowStartIso}`, nextFireIso: predicted };
|
|
20111
20749
|
}
|
|
20112
|
-
return { due: false, nextFireIso: predicted
|
|
20750
|
+
return { due: false, nextFireIso: predicted };
|
|
20113
20751
|
}
|
|
20114
20752
|
function evaluateWhenStatus(trigger, job, ctx, createdAtMs) {
|
|
20115
20753
|
const fm = ctx.assignment;
|
|
@@ -20202,7 +20840,7 @@ var LOCK_WAIT_MS2 = 50;
|
|
|
20202
20840
|
var LOCK_MAX_WAITS2 = 100;
|
|
20203
20841
|
var MAX_CONSUMED_EDGES = 50;
|
|
20204
20842
|
async function acquireJobLock(id) {
|
|
20205
|
-
const lockPath =
|
|
20843
|
+
const lockPath = resolve32(schedulesDir(), `${id}.lock`);
|
|
20206
20844
|
const token = `${process.pid}:${createHash3("sha256").update(`${Math.random()}${Date.now()}`).digest("hex").slice(0, 12)}`;
|
|
20207
20845
|
for (let attempt = 0; attempt <= LOCK_MAX_WAITS2; attempt++) {
|
|
20208
20846
|
try {
|
|
@@ -20517,7 +21155,7 @@ async function reapStale(jobs, deps) {
|
|
|
20517
21155
|
|
|
20518
21156
|
// src/schedules/unattended.ts
|
|
20519
21157
|
import { existsSync as existsSync3 } from "fs";
|
|
20520
|
-
import { resolve as
|
|
21158
|
+
import { resolve as resolve33 } from "path";
|
|
20521
21159
|
var UnattendedRefusalError = class extends Error {
|
|
20522
21160
|
constructor(message) {
|
|
20523
21161
|
super(message);
|
|
@@ -20533,7 +21171,7 @@ function assertUnattendedTerminalSupported(terminal) {
|
|
|
20533
21171
|
}
|
|
20534
21172
|
function isKillSwitchEngaged() {
|
|
20535
21173
|
if (process.env.SYNTAUR_SCHEDULES_DISABLED === "1") return true;
|
|
20536
|
-
return existsSync3(
|
|
21174
|
+
return existsSync3(resolve33(schedulesDir(), "KILL"));
|
|
20537
21175
|
}
|
|
20538
21176
|
function canFire(job, deps) {
|
|
20539
21177
|
const killSwitch = deps.killSwitch ?? isKillSwitchEngaged;
|
|
@@ -20685,25 +21323,25 @@ init_paths();
|
|
|
20685
21323
|
init_assignment_resolver();
|
|
20686
21324
|
init_frontmatter();
|
|
20687
21325
|
import { readFile as readFile23 } from "fs/promises";
|
|
20688
|
-
import { resolve as
|
|
21326
|
+
import { resolve as resolve34 } from "path";
|
|
20689
21327
|
|
|
20690
21328
|
// src/schedules/launch-ack.ts
|
|
20691
21329
|
init_session_id();
|
|
20692
|
-
import { readdirSync } from "fs";
|
|
21330
|
+
import { readdirSync as readdirSync2 } from "fs";
|
|
20693
21331
|
import { homedir as homedir6 } from "os";
|
|
20694
21332
|
import { join as join8 } from "path";
|
|
20695
21333
|
function defaultSleep(ms) {
|
|
20696
21334
|
return new Promise((r) => setTimeout(r, ms));
|
|
20697
21335
|
}
|
|
20698
21336
|
function runtimeSessionsDir() {
|
|
20699
|
-
const
|
|
20700
|
-
return
|
|
21337
|
+
const override3 = process.env.SYNTAUR_RUNTIME_SESSIONS_DIR;
|
|
21338
|
+
return override3 && override3.length > 0 ? override3 : join8(homedir6(), ".syntaur", "runtime", "sessions");
|
|
20701
21339
|
}
|
|
20702
21340
|
function defaultAckProbe(handle) {
|
|
20703
21341
|
const dir = runtimeSessionsDir();
|
|
20704
21342
|
let names;
|
|
20705
21343
|
try {
|
|
20706
|
-
names =
|
|
21344
|
+
names = readdirSync2(dir);
|
|
20707
21345
|
} catch {
|
|
20708
21346
|
return null;
|
|
20709
21347
|
}
|
|
@@ -20745,7 +21383,7 @@ async function defaultReadAssignment(assignmentId) {
|
|
|
20745
21383
|
const resolved = await resolveAssignmentById(projectsDir, assignmentsDir(), assignmentId);
|
|
20746
21384
|
if (!resolved) return null;
|
|
20747
21385
|
try {
|
|
20748
|
-
const content = await readFile23(
|
|
21386
|
+
const content = await readFile23(resolve34(resolved.assignmentDir, "assignment.md"), "utf-8");
|
|
20749
21387
|
return parseAssignmentFrontmatter(content);
|
|
20750
21388
|
} catch {
|
|
20751
21389
|
return null;
|
|
@@ -20837,7 +21475,7 @@ import { Router as Router14 } from "express";
|
|
|
20837
21475
|
// src/db/usage-db.ts
|
|
20838
21476
|
init_paths();
|
|
20839
21477
|
import Database4 from "better-sqlite3";
|
|
20840
|
-
import { resolve as
|
|
21478
|
+
import { resolve as resolve35 } from "path";
|
|
20841
21479
|
var db4 = null;
|
|
20842
21480
|
var USAGE_SCHEMA_VERSION = "1";
|
|
20843
21481
|
var SCHEMA_SQL4 = `
|
|
@@ -20911,7 +21549,7 @@ function nowIso2() {
|
|
|
20911
21549
|
}
|
|
20912
21550
|
function initUsageDb(dbPath) {
|
|
20913
21551
|
if (db4) return db4;
|
|
20914
|
-
const finalPath = dbPath ??
|
|
21552
|
+
const finalPath = dbPath ?? resolve35(syntaurRoot(), "syntaur.db");
|
|
20915
21553
|
db4 = new Database4(finalPath);
|
|
20916
21554
|
db4.pragma("journal_mode = WAL");
|
|
20917
21555
|
db4.pragma("busy_timeout = 5000");
|
|
@@ -21088,6 +21726,199 @@ function listDistinctTools() {
|
|
|
21088
21726
|
|
|
21089
21727
|
// src/dashboard/api-usage.ts
|
|
21090
21728
|
init_api();
|
|
21729
|
+
|
|
21730
|
+
// src/usage/engagement-cost.ts
|
|
21731
|
+
init_session_db();
|
|
21732
|
+
init_engagement_tokens();
|
|
21733
|
+
|
|
21734
|
+
// src/usage/pricing.ts
|
|
21735
|
+
var PER_MILLION = 1e6;
|
|
21736
|
+
var MODEL_PRICING = {
|
|
21737
|
+
// Moonshot Kimi K2.6 — the model pi emits today. Official Moonshot list price.
|
|
21738
|
+
// source: https://platform.moonshot.ai/ (official) — cross-checked
|
|
21739
|
+
// https://openrouter.ai/moonshotai/kimi-k2.6 (retrieved 2026-06-17)
|
|
21740
|
+
"moonshotai/kimi-k2.6": { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0.95 },
|
|
21741
|
+
// Moonshot Kimi K2.5 — prior Kimi model. Official Moonshot list price.
|
|
21742
|
+
// source: https://platform.moonshot.ai/ — cross-checked
|
|
21743
|
+
// https://openrouter.ai/moonshotai/kimi-k2.5 (retrieved 2026-06-17)
|
|
21744
|
+
"moonshotai/kimi-k2.5": { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0.6 },
|
|
21745
|
+
// Z.ai (Zhipu) GLM-5.2 — official z.ai platform list price.
|
|
21746
|
+
// source: https://docs.z.ai/guides/overview/pricing — cross-checked
|
|
21747
|
+
// https://openrouter.ai/z-ai/glm-5 (retrieved 2026-06-18)
|
|
21748
|
+
"zai-org/glm-5.2": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 1.4 },
|
|
21749
|
+
// MiniMax M2.5 — official MiniMax platform list price. No separately-pinned
|
|
21750
|
+
// cached-read rate is published, so cacheRead is set conservatively to the
|
|
21751
|
+
// input rate (upper bound); revise if MiniMax publishes a cache rate.
|
|
21752
|
+
// source: https://platform.minimax.io/docs/guides/pricing-token-plan
|
|
21753
|
+
// — cross-checked https://openrouter.ai/minimax/minimax-m2.5 (retrieved 2026-06-18)
|
|
21754
|
+
"minimaxai/minimax-m2.5": { input: 0.15, output: 0.9, cacheRead: 0.15, cacheWrite: 0.15 }
|
|
21755
|
+
// NOTE: opaque Synthetic tier aliases like `syn:large:text` have no public
|
|
21756
|
+
// per-token rate (they route to whatever Synthetic assigns), so they remain
|
|
21757
|
+
// unpriced (→ $0). Reseller discounts (e.g. DeepInfra K2.6 0.75/3.50/0.15) are
|
|
21758
|
+
// rejected by the canonical-source rule and are NOT used here.
|
|
21759
|
+
};
|
|
21760
|
+
function normalizeModelKey(model) {
|
|
21761
|
+
return model.replace(/^\s*\[[^\]]*\]\s*/, "").replace(/^hf:/i, "").trim().toLowerCase();
|
|
21762
|
+
}
|
|
21763
|
+
function priceForModel(model, tokens) {
|
|
21764
|
+
const rate = MODEL_PRICING[normalizeModelKey(model)];
|
|
21765
|
+
if (!rate) return null;
|
|
21766
|
+
return (tokens.inputTokens * rate.input + tokens.outputTokens * rate.output + tokens.cacheReadTokens * rate.cacheRead + tokens.cacheCreationTokens * rate.cacheWrite) / PER_MILLION;
|
|
21767
|
+
}
|
|
21768
|
+
|
|
21769
|
+
// src/usage/engagement-cost.ts
|
|
21770
|
+
var EMPTY = {
|
|
21771
|
+
cost: 0,
|
|
21772
|
+
pricedWindowCount: 0,
|
|
21773
|
+
uncomputableWindowCount: 0,
|
|
21774
|
+
negativeDeltaCount: 0
|
|
21775
|
+
};
|
|
21776
|
+
function engagementDb() {
|
|
21777
|
+
let db5;
|
|
21778
|
+
try {
|
|
21779
|
+
db5 = getSessionDb();
|
|
21780
|
+
} catch {
|
|
21781
|
+
return null;
|
|
21782
|
+
}
|
|
21783
|
+
const has = db5.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'engagement'").get();
|
|
21784
|
+
return has ? db5 : null;
|
|
21785
|
+
}
|
|
21786
|
+
function sinceBound(since) {
|
|
21787
|
+
return since ? `${since}T00:00:00.000Z` : void 0;
|
|
21788
|
+
}
|
|
21789
|
+
function untilBound(until) {
|
|
21790
|
+
return until ? `${until}T23:59:59.999Z` : void 0;
|
|
21791
|
+
}
|
|
21792
|
+
function toBuckets(open6, close) {
|
|
21793
|
+
const o = open6 ?? { input: 0, output: 0, cacheCreation: 0, cacheRead: 0, total: 0, cost: 0 };
|
|
21794
|
+
const c = close ?? { input: 0, output: 0, cacheCreation: 0, cacheRead: 0, total: 0, cost: 0 };
|
|
21795
|
+
return {
|
|
21796
|
+
inputTokens: Math.max(0, c.input - o.input),
|
|
21797
|
+
outputTokens: Math.max(0, c.output - o.output),
|
|
21798
|
+
cacheCreationTokens: Math.max(0, c.cacheCreation - o.cacheCreation),
|
|
21799
|
+
cacheReadTokens: Math.max(0, c.cacheRead - o.cacheRead)
|
|
21800
|
+
};
|
|
21801
|
+
}
|
|
21802
|
+
function windowCost(openJson, closeJson, modelFilter) {
|
|
21803
|
+
const open6 = parseSnapshot(openJson);
|
|
21804
|
+
const close = parseSnapshot(closeJson);
|
|
21805
|
+
if (!open6 || !close) return null;
|
|
21806
|
+
let cost = 0;
|
|
21807
|
+
let hadNegative = false;
|
|
21808
|
+
const models = /* @__PURE__ */ new Set([
|
|
21809
|
+
...Object.keys(open6.models),
|
|
21810
|
+
...Object.keys(close.models)
|
|
21811
|
+
]);
|
|
21812
|
+
for (const model of models) {
|
|
21813
|
+
if (modelFilter && model !== modelFilter) continue;
|
|
21814
|
+
const openCost = open6.models[model]?.cost ?? 0;
|
|
21815
|
+
const closeCost = close.models[model]?.cost ?? 0;
|
|
21816
|
+
let contribution = closeCost - openCost;
|
|
21817
|
+
if (contribution < 0) {
|
|
21818
|
+
hadNegative = true;
|
|
21819
|
+
contribution = 0;
|
|
21820
|
+
} else if (contribution === 0) {
|
|
21821
|
+
const buckets = toBuckets(open6.models[model], close.models[model]);
|
|
21822
|
+
if (buckets.inputTokens + buckets.outputTokens + buckets.cacheCreationTokens + buckets.cacheReadTokens > 0) {
|
|
21823
|
+
const priced = priceForModel(model, buckets);
|
|
21824
|
+
if (priced && priced > 0) contribution = priced;
|
|
21825
|
+
}
|
|
21826
|
+
}
|
|
21827
|
+
cost += contribution;
|
|
21828
|
+
}
|
|
21829
|
+
return { cost, hadNegative };
|
|
21830
|
+
}
|
|
21831
|
+
function rollupWindows(rows, modelFilter) {
|
|
21832
|
+
const out = { ...EMPTY };
|
|
21833
|
+
for (const row of rows) {
|
|
21834
|
+
const wc = windowCost(row.tokens_at_open, row.tokens_at_close, modelFilter);
|
|
21835
|
+
if (wc === null) {
|
|
21836
|
+
out.uncomputableWindowCount += 1;
|
|
21837
|
+
continue;
|
|
21838
|
+
}
|
|
21839
|
+
out.pricedWindowCount += 1;
|
|
21840
|
+
out.cost += wc.cost;
|
|
21841
|
+
if (wc.hadNegative) out.negativeDeltaCount += 1;
|
|
21842
|
+
}
|
|
21843
|
+
return out;
|
|
21844
|
+
}
|
|
21845
|
+
function assignmentWindowCost(opts) {
|
|
21846
|
+
const db5 = engagementDb();
|
|
21847
|
+
if (!db5) return { ...EMPTY };
|
|
21848
|
+
const clauses = ["ended_at IS NOT NULL"];
|
|
21849
|
+
const params2 = [];
|
|
21850
|
+
if (opts.assignmentId) {
|
|
21851
|
+
clauses.push("assignment_id = ?");
|
|
21852
|
+
params2.push(opts.assignmentId);
|
|
21853
|
+
} else {
|
|
21854
|
+
const proj = opts.projectSlug && opts.projectSlug.length > 0 ? opts.projectSlug : null;
|
|
21855
|
+
if (proj === null) {
|
|
21856
|
+
clauses.push("project_slug IS NULL");
|
|
21857
|
+
} else {
|
|
21858
|
+
clauses.push("project_slug = ?");
|
|
21859
|
+
params2.push(proj);
|
|
21860
|
+
}
|
|
21861
|
+
clauses.push("assignment_slug = ?");
|
|
21862
|
+
params2.push(opts.assignmentSlug ?? null);
|
|
21863
|
+
}
|
|
21864
|
+
const since = sinceBound(opts.since);
|
|
21865
|
+
const until = untilBound(opts.until);
|
|
21866
|
+
if (since) {
|
|
21867
|
+
clauses.push("ended_at >= ?");
|
|
21868
|
+
params2.push(since);
|
|
21869
|
+
}
|
|
21870
|
+
if (until) {
|
|
21871
|
+
clauses.push("ended_at <= ?");
|
|
21872
|
+
params2.push(until);
|
|
21873
|
+
}
|
|
21874
|
+
const rows = db5.prepare(
|
|
21875
|
+
`SELECT assignment_id, assignment_slug, tokens_at_open, tokens_at_close
|
|
21876
|
+
FROM engagement
|
|
21877
|
+
WHERE ${clauses.join(" AND ")}`
|
|
21878
|
+
).all(...params2);
|
|
21879
|
+
return rollupWindows(rows, opts.model);
|
|
21880
|
+
}
|
|
21881
|
+
function projectWindowCosts(opts) {
|
|
21882
|
+
const db5 = engagementDb();
|
|
21883
|
+
if (!db5) return /* @__PURE__ */ new Map();
|
|
21884
|
+
const clauses = ["ended_at IS NOT NULL", "project_slug = ?", "assignment_slug IS NOT NULL"];
|
|
21885
|
+
const params2 = [opts.projectSlug];
|
|
21886
|
+
const since = sinceBound(opts.since);
|
|
21887
|
+
const until = untilBound(opts.until);
|
|
21888
|
+
if (since) {
|
|
21889
|
+
clauses.push("ended_at >= ?");
|
|
21890
|
+
params2.push(since);
|
|
21891
|
+
}
|
|
21892
|
+
if (until) {
|
|
21893
|
+
clauses.push("ended_at <= ?");
|
|
21894
|
+
params2.push(until);
|
|
21895
|
+
}
|
|
21896
|
+
const rows = db5.prepare(
|
|
21897
|
+
`SELECT assignment_id, assignment_slug, tokens_at_open, tokens_at_close
|
|
21898
|
+
FROM engagement
|
|
21899
|
+
WHERE ${clauses.join(" AND ")}`
|
|
21900
|
+
).all(...params2);
|
|
21901
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
21902
|
+
const idForSlug = /* @__PURE__ */ new Map();
|
|
21903
|
+
for (const row of rows) {
|
|
21904
|
+
const slug = row.assignment_slug;
|
|
21905
|
+
const bucket = grouped.get(slug);
|
|
21906
|
+
if (bucket) bucket.push(row);
|
|
21907
|
+
else grouped.set(slug, [row]);
|
|
21908
|
+
if (row.assignment_id && !idForSlug.get(slug)) idForSlug.set(slug, row.assignment_id);
|
|
21909
|
+
}
|
|
21910
|
+
const out = /* @__PURE__ */ new Map();
|
|
21911
|
+
for (const [slug, bucket] of grouped) {
|
|
21912
|
+
out.set(slug, {
|
|
21913
|
+
assignmentSlug: slug,
|
|
21914
|
+
assignmentId: idForSlug.get(slug) ?? null,
|
|
21915
|
+
...rollupWindows(bucket, opts.model)
|
|
21916
|
+
});
|
|
21917
|
+
}
|
|
21918
|
+
return out;
|
|
21919
|
+
}
|
|
21920
|
+
|
|
21921
|
+
// src/dashboard/api-usage.ts
|
|
21091
21922
|
function createUsageRouter(projectsDir, assignmentsDir2) {
|
|
21092
21923
|
const router = Router14();
|
|
21093
21924
|
router.get("/facets", (_req, res) => {
|
|
@@ -21127,14 +21958,12 @@ function createUsageRouter(projectsDir, assignmentsDir2) {
|
|
|
21127
21958
|
try {
|
|
21128
21959
|
initUsageDb();
|
|
21129
21960
|
const projectSlug = req2.params.projectSlug;
|
|
21130
|
-
const
|
|
21131
|
-
|
|
21132
|
-
projectSlug
|
|
21133
|
-
});
|
|
21961
|
+
const common = extractCommonFilter(req2.query);
|
|
21962
|
+
const rows = listDaily({ ...common, projectSlug });
|
|
21134
21963
|
res.json({
|
|
21135
21964
|
projectSlug,
|
|
21136
21965
|
daily: rows,
|
|
21137
|
-
summary:
|
|
21966
|
+
summary: projectAssignmentRollup(projectSlug, rows, common)
|
|
21138
21967
|
});
|
|
21139
21968
|
} catch (error) {
|
|
21140
21969
|
res.status(500).json({
|
|
@@ -21160,7 +21989,13 @@ function createUsageRouter(projectsDir, assignmentsDir2) {
|
|
|
21160
21989
|
assignmentSlug,
|
|
21161
21990
|
daily: dailyRows,
|
|
21162
21991
|
events: eventRows,
|
|
21163
|
-
summary: buildAssignmentSummary(dailyRows
|
|
21992
|
+
summary: buildAssignmentSummary(dailyRows, {
|
|
21993
|
+
projectSlug,
|
|
21994
|
+
assignmentSlug,
|
|
21995
|
+
since: common.since,
|
|
21996
|
+
until: common.until,
|
|
21997
|
+
model: common.model
|
|
21998
|
+
})
|
|
21164
21999
|
});
|
|
21165
22000
|
} catch (error) {
|
|
21166
22001
|
res.status(500).json({
|
|
@@ -21185,7 +22020,15 @@ function createUsageRouter(projectsDir, assignmentsDir2) {
|
|
|
21185
22020
|
assignmentId: assignmentSlug,
|
|
21186
22021
|
daily: dailyRows,
|
|
21187
22022
|
events: eventRows,
|
|
21188
|
-
|
|
22023
|
+
// Standalone: engagement stores `project_slug IS NULL` (the reader maps
|
|
22024
|
+
// a null/empty projectSlug to the NULL match).
|
|
22025
|
+
summary: buildAssignmentSummary(dailyRows, {
|
|
22026
|
+
projectSlug: null,
|
|
22027
|
+
assignmentSlug,
|
|
22028
|
+
since: common.since,
|
|
22029
|
+
until: common.until,
|
|
22030
|
+
model: common.model
|
|
22031
|
+
})
|
|
21189
22032
|
});
|
|
21190
22033
|
} catch (error) {
|
|
21191
22034
|
res.status(500).json({
|
|
@@ -21235,13 +22078,17 @@ function byModelBreakdown(rows) {
|
|
|
21235
22078
|
}
|
|
21236
22079
|
return [...map.values()].sort((a, b) => b.totalTokens - a.totalTokens);
|
|
21237
22080
|
}
|
|
21238
|
-
function buildAssignmentSummary(rows) {
|
|
22081
|
+
function buildAssignmentSummary(rows, costKey) {
|
|
21239
22082
|
const totals = summarize(rows, "assignment")[0];
|
|
22083
|
+
const windows = assignmentWindowCost(costKey);
|
|
21240
22084
|
return {
|
|
21241
22085
|
totalTokens: totals?.totalTokens ?? 0,
|
|
21242
|
-
totalCost:
|
|
22086
|
+
totalCost: windows.cost,
|
|
21243
22087
|
lastEventDay: totals?.lastEventDay ?? null,
|
|
21244
|
-
byModel: byModelBreakdown(rows)
|
|
22088
|
+
byModel: byModelBreakdown(rows),
|
|
22089
|
+
pricedWindowCount: windows.pricedWindowCount,
|
|
22090
|
+
uncomputableWindowCount: windows.uncomputableWindowCount,
|
|
22091
|
+
negativeDeltaCount: windows.negativeDeltaCount
|
|
21245
22092
|
};
|
|
21246
22093
|
}
|
|
21247
22094
|
function summarize(rows, mode) {
|
|
@@ -21265,27 +22112,70 @@ function summarize(rows, mode) {
|
|
|
21265
22112
|
}
|
|
21266
22113
|
return [...map.values()].sort((a, b) => b.totalTokens - a.totalTokens);
|
|
21267
22114
|
}
|
|
22115
|
+
function projectAssignmentRollup(projectSlug, rows, common) {
|
|
22116
|
+
const byAssignment = /* @__PURE__ */ new Map();
|
|
22117
|
+
for (const row of summarize(rows, "assignment")) {
|
|
22118
|
+
byAssignment.set(row.assignmentSlug, {
|
|
22119
|
+
...row,
|
|
22120
|
+
totalCost: 0,
|
|
22121
|
+
// Counts present on EVERY per-assignment row (a daily-only assignment with
|
|
22122
|
+
// no closed window stays at 0/0/0); window overlay below replaces them.
|
|
22123
|
+
pricedWindowCount: 0,
|
|
22124
|
+
uncomputableWindowCount: 0,
|
|
22125
|
+
negativeDeltaCount: 0
|
|
22126
|
+
});
|
|
22127
|
+
}
|
|
22128
|
+
const windows = projectWindowCosts({
|
|
22129
|
+
projectSlug,
|
|
22130
|
+
since: common.since,
|
|
22131
|
+
until: common.until,
|
|
22132
|
+
model: common.model
|
|
22133
|
+
});
|
|
22134
|
+
for (const [assignmentSlug, w] of windows) {
|
|
22135
|
+
const existing = byAssignment.get(assignmentSlug);
|
|
22136
|
+
if (existing) {
|
|
22137
|
+
existing.totalCost = w.cost;
|
|
22138
|
+
existing.pricedWindowCount = w.pricedWindowCount;
|
|
22139
|
+
existing.uncomputableWindowCount = w.uncomputableWindowCount;
|
|
22140
|
+
existing.negativeDeltaCount = w.negativeDeltaCount;
|
|
22141
|
+
} else {
|
|
22142
|
+
byAssignment.set(assignmentSlug, {
|
|
22143
|
+
projectSlug,
|
|
22144
|
+
assignmentSlug,
|
|
22145
|
+
totalTokens: 0,
|
|
22146
|
+
totalCost: w.cost,
|
|
22147
|
+
lastEventDay: "",
|
|
22148
|
+
pricedWindowCount: w.pricedWindowCount,
|
|
22149
|
+
uncomputableWindowCount: w.uncomputableWindowCount,
|
|
22150
|
+
negativeDeltaCount: w.negativeDeltaCount
|
|
22151
|
+
});
|
|
22152
|
+
}
|
|
22153
|
+
}
|
|
22154
|
+
return [...byAssignment.values()].sort(
|
|
22155
|
+
(a, b) => b.totalCost - a.totalCost || b.totalTokens - a.totalTokens
|
|
22156
|
+
);
|
|
22157
|
+
}
|
|
21268
22158
|
|
|
21269
22159
|
// src/dashboard/api-events.ts
|
|
21270
22160
|
init_fs();
|
|
21271
22161
|
init_parser();
|
|
21272
22162
|
init_events_db();
|
|
21273
22163
|
import { Router as Router15 } from "express";
|
|
21274
|
-
import { resolve as
|
|
22164
|
+
import { resolve as resolve36 } from "path";
|
|
21275
22165
|
import { readFile as readFile24 } from "fs/promises";
|
|
21276
22166
|
function createEventsRouter(projectsDir, assignmentsDir2) {
|
|
21277
22167
|
const router = Router15();
|
|
21278
22168
|
router.get("/projects/:slug/assignments/:aslug/events", async (req2, res) => {
|
|
21279
22169
|
try {
|
|
21280
22170
|
const { slug, aslug } = req2.params;
|
|
21281
|
-
const assignmentMdPath =
|
|
22171
|
+
const assignmentMdPath = resolve36(
|
|
21282
22172
|
projectsDir,
|
|
21283
22173
|
slug,
|
|
21284
22174
|
"assignments",
|
|
21285
22175
|
aslug,
|
|
21286
22176
|
"assignment.md"
|
|
21287
22177
|
);
|
|
21288
|
-
const id = await
|
|
22178
|
+
const id = await readAssignmentId2(assignmentMdPath);
|
|
21289
22179
|
if (!id) {
|
|
21290
22180
|
res.json({ events: [] });
|
|
21291
22181
|
return;
|
|
@@ -21299,8 +22189,8 @@ function createEventsRouter(projectsDir, assignmentsDir2) {
|
|
|
21299
22189
|
router.get("/standalone/assignments/:id/events", async (req2, res) => {
|
|
21300
22190
|
try {
|
|
21301
22191
|
const { id: dirId } = req2.params;
|
|
21302
|
-
const assignmentMdPath =
|
|
21303
|
-
const id = await
|
|
22192
|
+
const assignmentMdPath = resolve36(assignmentsDir2, dirId, "assignment.md");
|
|
22193
|
+
const id = await readAssignmentId2(assignmentMdPath) ?? dirId;
|
|
21304
22194
|
res.json({ events: loadEvents(id, req2.query) });
|
|
21305
22195
|
} catch (error) {
|
|
21306
22196
|
console.warn("[events] failed to list standalone events:", error);
|
|
@@ -21309,7 +22199,7 @@ function createEventsRouter(projectsDir, assignmentsDir2) {
|
|
|
21309
22199
|
});
|
|
21310
22200
|
return router;
|
|
21311
22201
|
}
|
|
21312
|
-
async function
|
|
22202
|
+
async function readAssignmentId2(assignmentMdPath) {
|
|
21313
22203
|
if (!await fileExists(assignmentMdPath)) return null;
|
|
21314
22204
|
const content = await readFile24(assignmentMdPath, "utf-8");
|
|
21315
22205
|
const [fm] = extractFrontmatter2(content);
|
|
@@ -21356,7 +22246,7 @@ init_assignment_walk();
|
|
|
21356
22246
|
init_parser();
|
|
21357
22247
|
init_facts();
|
|
21358
22248
|
init_state_machine();
|
|
21359
|
-
import { resolve as
|
|
22249
|
+
import { resolve as resolve37 } from "path";
|
|
21360
22250
|
import { readFile as readFile25 } from "fs/promises";
|
|
21361
22251
|
|
|
21362
22252
|
// src/inbox/types.ts
|
|
@@ -21537,7 +22427,7 @@ async function computeInbox(opts) {
|
|
|
21537
22427
|
if (opts.project !== void 0 && entry.projectSlug !== opts.project) continue;
|
|
21538
22428
|
let parsed;
|
|
21539
22429
|
try {
|
|
21540
|
-
const content = await readFile25(
|
|
22430
|
+
const content = await readFile25(resolve37(entry.assignmentDir, "assignment.md"), "utf-8");
|
|
21541
22431
|
parsed = parseAssignmentFull(content);
|
|
21542
22432
|
} catch {
|
|
21543
22433
|
continue;
|
|
@@ -21580,7 +22470,7 @@ async function computeInbox(opts) {
|
|
|
21580
22470
|
});
|
|
21581
22471
|
}
|
|
21582
22472
|
if (!typeFilter || typeFilter.has("question")) {
|
|
21583
|
-
const commentsPath =
|
|
22473
|
+
const commentsPath = resolve37(entry.assignmentDir, "comments.md");
|
|
21584
22474
|
if (await fileExists(commentsPath)) {
|
|
21585
22475
|
try {
|
|
21586
22476
|
const content = await readFile25(commentsPath, "utf-8");
|
|
@@ -21694,7 +22584,7 @@ init_slug();
|
|
|
21694
22584
|
init_timestamp();
|
|
21695
22585
|
init_fs();
|
|
21696
22586
|
import { Router as Router17 } from "express";
|
|
21697
|
-
import { resolve as
|
|
22587
|
+
import { resolve as resolve38 } from "path";
|
|
21698
22588
|
import { readFile as readFile26 } from "fs/promises";
|
|
21699
22589
|
init_playbooks();
|
|
21700
22590
|
function statusForPlaybookError(code) {
|
|
@@ -21777,7 +22667,7 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
21777
22667
|
res.status(404).json({ error: `Playbook "${req2.params.slug}" not found` });
|
|
21778
22668
|
return;
|
|
21779
22669
|
}
|
|
21780
|
-
const filePath =
|
|
22670
|
+
const filePath = resolve38(playbooksDir2, resolved.filename);
|
|
21781
22671
|
const content = await readFile26(filePath, "utf-8");
|
|
21782
22672
|
res.json({
|
|
21783
22673
|
documentType: "playbook",
|
|
@@ -21803,7 +22693,7 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
21803
22693
|
return;
|
|
21804
22694
|
}
|
|
21805
22695
|
await ensureDir(playbooksDir2);
|
|
21806
|
-
const filePath =
|
|
22696
|
+
const filePath = resolve38(playbooksDir2, `${slug}.md`);
|
|
21807
22697
|
if (await fileExists(filePath)) {
|
|
21808
22698
|
res.status(409).json({ error: `Playbook "${slug}" already exists` });
|
|
21809
22699
|
return;
|
|
@@ -21827,7 +22717,7 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
21827
22717
|
res.status(404).json({ error: `Playbook "${req2.params.slug}" not found` });
|
|
21828
22718
|
return;
|
|
21829
22719
|
}
|
|
21830
|
-
const filePath =
|
|
22720
|
+
const filePath = resolve38(playbooksDir2, resolved.filename);
|
|
21831
22721
|
await writeFileForce(filePath, content);
|
|
21832
22722
|
await rebuildPlaybookManifest(playbooksDir2);
|
|
21833
22723
|
res.json({ slug: resolved.slug, path: filePath });
|
|
@@ -21890,7 +22780,7 @@ init_uuid();
|
|
|
21890
22780
|
init_paths();
|
|
21891
22781
|
init_fs();
|
|
21892
22782
|
init_config2();
|
|
21893
|
-
import { resolve as
|
|
22783
|
+
import { resolve as resolve39 } from "path";
|
|
21894
22784
|
async function createAssignmentCommand(title, options) {
|
|
21895
22785
|
if (!title.trim()) {
|
|
21896
22786
|
throw new Error("Assignment title cannot be empty.");
|
|
@@ -21965,14 +22855,14 @@ async function createAssignmentCommand(title, options) {
|
|
|
21965
22855
|
if (options.oneOff) {
|
|
21966
22856
|
const standaloneRoot = assignmentsDir();
|
|
21967
22857
|
folderName = id;
|
|
21968
|
-
assignmentDir =
|
|
22858
|
+
assignmentDir = resolve39(standaloneRoot, folderName);
|
|
21969
22859
|
projectSlug = null;
|
|
21970
22860
|
await ensureDir(standaloneRoot);
|
|
21971
22861
|
} else {
|
|
21972
22862
|
const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
|
|
21973
22863
|
projectSlug = options.project;
|
|
21974
|
-
const projectDir =
|
|
21975
|
-
const projectMdPath =
|
|
22864
|
+
const projectDir = resolve39(baseDir, projectSlug);
|
|
22865
|
+
const projectMdPath = resolve39(projectDir, "project.md");
|
|
21976
22866
|
if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
|
|
21977
22867
|
throw new Error(
|
|
21978
22868
|
`Project "${projectSlug}" not found at ${projectDir}.
|
|
@@ -21980,9 +22870,9 @@ Run 'syntaur create-project' first or use --one-off.`
|
|
|
21980
22870
|
);
|
|
21981
22871
|
}
|
|
21982
22872
|
if (dependsOn.length > 0) {
|
|
21983
|
-
const depDirBase =
|
|
22873
|
+
const depDirBase = resolve39(projectDir, "assignments");
|
|
21984
22874
|
for (const dep of dependsOn) {
|
|
21985
|
-
const depDir =
|
|
22875
|
+
const depDir = resolve39(depDirBase, dep);
|
|
21986
22876
|
if (!await fileExists(depDir)) {
|
|
21987
22877
|
console.warn(
|
|
21988
22878
|
`Warning: dependency "${dep}" does not exist in project "${projectSlug}" yet.`
|
|
@@ -21991,7 +22881,7 @@ Run 'syntaur create-project' first or use --one-off.`
|
|
|
21991
22881
|
}
|
|
21992
22882
|
}
|
|
21993
22883
|
folderName = assignmentSlug;
|
|
21994
|
-
assignmentDir =
|
|
22884
|
+
assignmentDir = resolve39(projectDir, "assignments", folderName);
|
|
21995
22885
|
}
|
|
21996
22886
|
if (await fileExists(assignmentDir)) {
|
|
21997
22887
|
throw new Error(
|
|
@@ -22003,7 +22893,7 @@ Use --slug to specify a different slug.`
|
|
|
22003
22893
|
const companionAssignmentRef = projectSlug === null ? id : assignmentSlug;
|
|
22004
22894
|
const files = [
|
|
22005
22895
|
[
|
|
22006
|
-
|
|
22896
|
+
resolve39(assignmentDir, "assignment.md"),
|
|
22007
22897
|
renderAssignment({
|
|
22008
22898
|
id,
|
|
22009
22899
|
slug: assignmentSlug,
|
|
@@ -22021,35 +22911,35 @@ Use --slug to specify a different slug.`
|
|
|
22021
22911
|
})
|
|
22022
22912
|
],
|
|
22023
22913
|
[
|
|
22024
|
-
|
|
22914
|
+
resolve39(assignmentDir, "scratchpad.md"),
|
|
22025
22915
|
renderScratchpad({
|
|
22026
22916
|
assignmentSlug: companionAssignmentRef,
|
|
22027
22917
|
timestamp
|
|
22028
22918
|
})
|
|
22029
22919
|
],
|
|
22030
22920
|
[
|
|
22031
|
-
|
|
22921
|
+
resolve39(assignmentDir, "handoff.md"),
|
|
22032
22922
|
renderHandoff({
|
|
22033
22923
|
assignmentSlug: companionAssignmentRef,
|
|
22034
22924
|
timestamp
|
|
22035
22925
|
})
|
|
22036
22926
|
],
|
|
22037
22927
|
[
|
|
22038
|
-
|
|
22928
|
+
resolve39(assignmentDir, "decision-record.md"),
|
|
22039
22929
|
renderDecisionRecord({
|
|
22040
22930
|
assignmentSlug: companionAssignmentRef,
|
|
22041
22931
|
timestamp
|
|
22042
22932
|
})
|
|
22043
22933
|
],
|
|
22044
22934
|
[
|
|
22045
|
-
|
|
22935
|
+
resolve39(assignmentDir, "progress.md"),
|
|
22046
22936
|
renderProgress({
|
|
22047
22937
|
assignment: companionAssignmentRef,
|
|
22048
22938
|
timestamp
|
|
22049
22939
|
})
|
|
22050
22940
|
],
|
|
22051
22941
|
[
|
|
22052
|
-
|
|
22942
|
+
resolve39(assignmentDir, "comments.md"),
|
|
22053
22943
|
renderComments({
|
|
22054
22944
|
assignment: companionAssignmentRef,
|
|
22055
22945
|
timestamp
|
|
@@ -22223,7 +23113,7 @@ import { raw } from "express";
|
|
|
22223
23113
|
|
|
22224
23114
|
// src/todos/attachments.ts
|
|
22225
23115
|
import { mkdir as mkdir4, readdir as readdir15, stat as stat6, rename as rename5, rm as rm4, unlink as unlink7, writeFile as writeFile6, cp } from "fs/promises";
|
|
22226
|
-
import { resolve as
|
|
23116
|
+
import { resolve as resolve40, basename as basename6, dirname as dirname9, extname } from "path";
|
|
22227
23117
|
|
|
22228
23118
|
// src/utils/proof-artifact-id.ts
|
|
22229
23119
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
@@ -22310,12 +23200,12 @@ function sanitizeAttachmentName(name) {
|
|
|
22310
23200
|
return n;
|
|
22311
23201
|
}
|
|
22312
23202
|
function attachmentsRootDir(todosDir2) {
|
|
22313
|
-
return
|
|
23203
|
+
return resolve40(todosDir2, "attachments");
|
|
22314
23204
|
}
|
|
22315
23205
|
function attachmentDirFor(todosDir2, scopeId, todoId) {
|
|
22316
23206
|
assertScope(scopeId);
|
|
22317
23207
|
assertTodoId(todoId);
|
|
22318
|
-
return
|
|
23208
|
+
return resolve40(attachmentsRootDir(todosDir2), scopeId, todoId);
|
|
22319
23209
|
}
|
|
22320
23210
|
async function dirExists(p) {
|
|
22321
23211
|
try {
|
|
@@ -22329,7 +23219,7 @@ async function writeAttachment(todosDir2, scopeId, todoId, originalName, bytes)
|
|
|
22329
23219
|
await mkdir4(dir, { recursive: true });
|
|
22330
23220
|
const id = generateArtifactId();
|
|
22331
23221
|
const filename = sanitizeAttachmentName(originalName);
|
|
22332
|
-
await writeFile6(
|
|
23222
|
+
await writeFile6(resolve40(dir, `${id}__${filename}`), bytes);
|
|
22333
23223
|
return {
|
|
22334
23224
|
id,
|
|
22335
23225
|
filename,
|
|
@@ -22354,7 +23244,7 @@ async function listAttachments(todosDir2, scopeId, todoId) {
|
|
|
22354
23244
|
if (!ATTACHMENT_ID_RE.test(id)) continue;
|
|
22355
23245
|
const filename = stored.slice(sep2 + 2);
|
|
22356
23246
|
try {
|
|
22357
|
-
const st = await stat6(
|
|
23247
|
+
const st = await stat6(resolve40(dir, stored));
|
|
22358
23248
|
if (!st.isFile()) continue;
|
|
22359
23249
|
out.push({ id, filename, mime: mimeForName(filename), size: st.size, createdAt: st.mtime.toISOString() });
|
|
22360
23250
|
} catch {
|
|
@@ -22365,7 +23255,7 @@ async function listAttachments(todosDir2, scopeId, todoId) {
|
|
|
22365
23255
|
}
|
|
22366
23256
|
async function readScopeAttachments(todosDir2, scopeId) {
|
|
22367
23257
|
assertScope(scopeId);
|
|
22368
|
-
const scopeDir =
|
|
23258
|
+
const scopeDir = resolve40(attachmentsRootDir(todosDir2), scopeId);
|
|
22369
23259
|
let todoIds;
|
|
22370
23260
|
try {
|
|
22371
23261
|
todoIds = await readdir15(scopeDir);
|
|
@@ -22393,7 +23283,7 @@ async function resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId) {
|
|
|
22393
23283
|
const stored = names.find((n) => n.startsWith(prefix));
|
|
22394
23284
|
if (!stored) return null;
|
|
22395
23285
|
const filename = stored.slice(prefix.length);
|
|
22396
|
-
return { path:
|
|
23286
|
+
return { path: resolve40(dir, stored), filename, mime: mimeForName(filename) };
|
|
22397
23287
|
}
|
|
22398
23288
|
async function deleteAttachment(todosDir2, scopeId, todoId, attachmentId) {
|
|
22399
23289
|
const resolved = await resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId);
|
|
@@ -22809,8 +23699,8 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
|
|
|
22809
23699
|
router.post("/:workspace/archive", async (req2, res) => {
|
|
22810
23700
|
try {
|
|
22811
23701
|
const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
|
|
22812
|
-
const { resolve:
|
|
22813
|
-
const { readFile:
|
|
23702
|
+
const { resolve: resolve49 } = await import("path");
|
|
23703
|
+
const { readFile: readFile31 } = await import("fs/promises");
|
|
22814
23704
|
const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
22815
23705
|
const workspace = getWorkspaceParam(req2.params.workspace);
|
|
22816
23706
|
const outcome = await wsLock(workspace, async () => {
|
|
@@ -22826,10 +23716,10 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
|
|
|
22826
23716
|
(e) => e.itemIds.every((id) => completedIds.has(id))
|
|
22827
23717
|
);
|
|
22828
23718
|
const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
|
|
22829
|
-
await ensureDir(
|
|
23719
|
+
await ensureDir(resolve49(todosDir2, "archive"));
|
|
22830
23720
|
let archContent = "";
|
|
22831
23721
|
if (await fileExists(archFile)) {
|
|
22832
|
-
archContent = await
|
|
23722
|
+
archContent = await readFile31(archFile, "utf-8");
|
|
22833
23723
|
archContent = archContent.trimEnd() + "\n\n";
|
|
22834
23724
|
} else {
|
|
22835
23725
|
archContent = `---
|
|
@@ -23130,7 +24020,7 @@ workspace: ${workspace}
|
|
|
23130
24020
|
const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
|
|
23131
24021
|
const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
|
|
23132
24022
|
const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
23133
|
-
const { readFile:
|
|
24023
|
+
const { readFile: readFile31 } = await import("fs/promises");
|
|
23134
24024
|
const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
|
|
23135
24025
|
const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
|
|
23136
24026
|
let assignmentRef;
|
|
@@ -23151,7 +24041,7 @@ workspace: ${workspace}
|
|
|
23151
24041
|
}
|
|
23152
24042
|
const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
|
|
23153
24043
|
if (!await fileExists2(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
|
|
23154
|
-
let content = await
|
|
24044
|
+
let content = await readFile31(assignmentMdPath, "utf-8");
|
|
23155
24045
|
content = appendTodosToAssignmentBody2(
|
|
23156
24046
|
content,
|
|
23157
24047
|
items.map((it) => ({
|
|
@@ -23347,7 +24237,7 @@ init_paths();
|
|
|
23347
24237
|
init_slug();
|
|
23348
24238
|
import { Router as Router20 } from "express";
|
|
23349
24239
|
import { mkdir as mkdir6, readFile as readFile27, rename as rename7 } from "fs/promises";
|
|
23350
|
-
import { resolve as
|
|
24240
|
+
import { resolve as resolve41, dirname as dirname11 } from "path";
|
|
23351
24241
|
init_api();
|
|
23352
24242
|
var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
|
|
23353
24243
|
function touchItem4(item) {
|
|
@@ -23363,7 +24253,7 @@ function params(req2) {
|
|
|
23363
24253
|
return req2.params;
|
|
23364
24254
|
}
|
|
23365
24255
|
async function projectExists(projectsDir, slug) {
|
|
23366
|
-
return fileExists(
|
|
24256
|
+
return fileExists(resolve41(projectsDir, slug, "project.md"));
|
|
23367
24257
|
}
|
|
23368
24258
|
async function ensureProjectTodosDir(projectsDir, slug) {
|
|
23369
24259
|
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
@@ -23380,7 +24270,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
|
|
|
23380
24270
|
throw err;
|
|
23381
24271
|
}
|
|
23382
24272
|
try {
|
|
23383
|
-
await mkdir6(
|
|
24273
|
+
await mkdir6(resolve41(todosDir2, "archive"), { recursive: false });
|
|
23384
24274
|
} catch (err) {
|
|
23385
24275
|
const code = err.code;
|
|
23386
24276
|
if (code === "EEXIST") return;
|
|
@@ -24059,15 +24949,15 @@ workspace: ${slug}
|
|
|
24059
24949
|
if (tg.includes("/")) {
|
|
24060
24950
|
const parts = tg.split("/");
|
|
24061
24951
|
if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
|
|
24062
|
-
assignmentDir =
|
|
24952
|
+
assignmentDir = resolve41(projectsDir, parts[0], "assignments", parts[1]);
|
|
24063
24953
|
assignmentRef = `${parts[0]}/${parts[1]}`;
|
|
24064
24954
|
} 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)) {
|
|
24065
|
-
assignmentDir =
|
|
24955
|
+
assignmentDir = resolve41(assignmentsDirFn(), tg);
|
|
24066
24956
|
assignmentRef = tg;
|
|
24067
24957
|
} else {
|
|
24068
24958
|
return { error: `Invalid target.assignment "${tg}"` };
|
|
24069
24959
|
}
|
|
24070
|
-
const assignmentMdPath =
|
|
24960
|
+
const assignmentMdPath = resolve41(assignmentDir, "assignment.md");
|
|
24071
24961
|
if (!await fileExists(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
|
|
24072
24962
|
let content = await readFile27(assignmentMdPath, "utf-8");
|
|
24073
24963
|
content = appendTodosToAssignmentBody2(
|
|
@@ -24449,7 +25339,7 @@ init_fs();
|
|
|
24449
25339
|
init_paths();
|
|
24450
25340
|
init_slug();
|
|
24451
25341
|
import { Router as Router22 } from "express";
|
|
24452
|
-
import { resolve as
|
|
25342
|
+
import { resolve as resolve42 } from "path";
|
|
24453
25343
|
init_parser2();
|
|
24454
25344
|
function deriveStatus2(bundle, items) {
|
|
24455
25345
|
const members = bundle.todoIds.map((id) => items.find((i) => i.id === id)).filter((i) => i !== void 0);
|
|
@@ -24491,7 +25381,7 @@ function createProjectBundlesRouter(projectsDir, broadcast) {
|
|
|
24491
25381
|
router.get("/", async (req2, res) => {
|
|
24492
25382
|
try {
|
|
24493
25383
|
const slug = getProjectIdParam2(req2.params.projectId);
|
|
24494
|
-
const projectMd =
|
|
25384
|
+
const projectMd = resolve42(projectsDir, slug, "project.md");
|
|
24495
25385
|
if (!await fileExists(projectMd)) {
|
|
24496
25386
|
notFound2(res, slug);
|
|
24497
25387
|
return;
|
|
@@ -24521,7 +25411,7 @@ init_config2();
|
|
|
24521
25411
|
import { execFile as execFile2 } from "child_process";
|
|
24522
25412
|
import { promisify as promisify2 } from "util";
|
|
24523
25413
|
import { cp as cp2, mkdtemp, rm as rm5, readFile as readFile29, writeFile as writeFile7, unlink as unlink8, stat as stat7, open as open5, rename as rename8 } from "fs/promises";
|
|
24524
|
-
import { resolve as
|
|
25414
|
+
import { resolve as resolve43, join as join9 } from "path";
|
|
24525
25415
|
import { tmpdir } from "os";
|
|
24526
25416
|
var exec2 = promisify2(execFile2);
|
|
24527
25417
|
var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
|
|
@@ -24561,7 +25451,7 @@ async function resolveCategoryPath(category) {
|
|
|
24561
25451
|
case "servers":
|
|
24562
25452
|
return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
|
|
24563
25453
|
case "config":
|
|
24564
|
-
return { sourcePath:
|
|
25454
|
+
return { sourcePath: resolve43(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
|
|
24565
25455
|
}
|
|
24566
25456
|
}
|
|
24567
25457
|
async function checkGitInstalled() {
|
|
@@ -24572,7 +25462,7 @@ async function checkGitInstalled() {
|
|
|
24572
25462
|
}
|
|
24573
25463
|
}
|
|
24574
25464
|
async function acquireLock2() {
|
|
24575
|
-
const lockPath =
|
|
25465
|
+
const lockPath = resolve43(syntaurRoot(), LOCK_FILE_NAME);
|
|
24576
25466
|
await ensureDir(syntaurRoot());
|
|
24577
25467
|
try {
|
|
24578
25468
|
const handle = await open5(lockPath, "wx");
|
|
@@ -24619,7 +25509,7 @@ async function copyRecursive(src, dest) {
|
|
|
24619
25509
|
await ensureDir(dest);
|
|
24620
25510
|
await cp2(src, dest, { recursive: true, force: true });
|
|
24621
25511
|
} else {
|
|
24622
|
-
await ensureDir(
|
|
25512
|
+
await ensureDir(resolve43(dest, ".."));
|
|
24623
25513
|
await cp2(src, dest, { force: true });
|
|
24624
25514
|
}
|
|
24625
25515
|
}
|
|
@@ -24667,7 +25557,7 @@ async function backupToGithub(overrides) {
|
|
|
24667
25557
|
}
|
|
24668
25558
|
if (category === "config") {
|
|
24669
25559
|
const sanitized = await readSanitizedConfig(sourcePath);
|
|
24670
|
-
await ensureDir(
|
|
25560
|
+
await ensureDir(resolve43(destPath, ".."));
|
|
24671
25561
|
await writeFile7(destPath, sanitized, "utf-8");
|
|
24672
25562
|
} else {
|
|
24673
25563
|
await copyRecursive(sourcePath, destPath);
|
|
@@ -24721,7 +25611,7 @@ async function backupToGithub(overrides) {
|
|
|
24721
25611
|
}
|
|
24722
25612
|
async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
|
|
24723
25613
|
if (isFile) {
|
|
24724
|
-
await ensureDir(
|
|
25614
|
+
await ensureDir(resolve43(localPath, ".."));
|
|
24725
25615
|
await cp2(repoSrcPath, localPath, { force: true });
|
|
24726
25616
|
return;
|
|
24727
25617
|
}
|
|
@@ -24822,7 +25712,7 @@ async function restoreFromGithub(overrides) {
|
|
|
24822
25712
|
}
|
|
24823
25713
|
async function getBackupStatus() {
|
|
24824
25714
|
const config = await readConfig();
|
|
24825
|
-
const lockPath =
|
|
25715
|
+
const lockPath = resolve43(syntaurRoot(), LOCK_FILE_NAME);
|
|
24826
25716
|
const locked = await fileExists(lockPath);
|
|
24827
25717
|
return {
|
|
24828
25718
|
repo: config.backup?.repo ?? null,
|
|
@@ -25305,7 +26195,7 @@ async function runCcusage(opts = {}) {
|
|
|
25305
26195
|
};
|
|
25306
26196
|
}
|
|
25307
26197
|
function runOnce(binary, args, env, timeoutMs, maxOutputBytes) {
|
|
25308
|
-
return new Promise((
|
|
26198
|
+
return new Promise((resolve49) => {
|
|
25309
26199
|
const child = spawn4(binary, args, {
|
|
25310
26200
|
env: env ?? process.env,
|
|
25311
26201
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -25319,7 +26209,7 @@ function runOnce(binary, args, env, timeoutMs, maxOutputBytes) {
|
|
|
25319
26209
|
if (settled) return;
|
|
25320
26210
|
settled = true;
|
|
25321
26211
|
clearTimeout(timer3);
|
|
25322
|
-
|
|
26212
|
+
resolve49(result);
|
|
25323
26213
|
};
|
|
25324
26214
|
const timer3 = setTimeout(() => {
|
|
25325
26215
|
timedOut = true;
|
|
@@ -25370,9 +26260,13 @@ function resolveAttribution(input, db5) {
|
|
|
25370
26260
|
const database = db5 ?? getSessionDb();
|
|
25371
26261
|
const direct = database.prepare(
|
|
25372
26262
|
`SELECT project_slug, assignment_slug
|
|
25373
|
-
FROM
|
|
25374
|
-
WHERE session_id =
|
|
25375
|
-
|
|
26263
|
+
FROM engagement
|
|
26264
|
+
WHERE session_id = ?
|
|
26265
|
+
AND julianday(started_at) <= julianday(?)
|
|
26266
|
+
AND (ended_at IS NULL OR julianday(ended_at) > julianday(?))
|
|
26267
|
+
ORDER BY julianday(started_at) DESC
|
|
26268
|
+
LIMIT 1`
|
|
26269
|
+
).get(input.sessionId, input.eventTs, input.eventTs);
|
|
25376
26270
|
if (direct) {
|
|
25377
26271
|
return {
|
|
25378
26272
|
projectSlug: direct.project_slug,
|
|
@@ -25381,25 +26275,43 @@ function resolveAttribution(input, db5) {
|
|
|
25381
26275
|
}
|
|
25382
26276
|
if (input.cwd) {
|
|
25383
26277
|
const exact = database.prepare(
|
|
25384
|
-
`SELECT project_slug, assignment_slug
|
|
25385
|
-
FROM sessions
|
|
25386
|
-
|
|
25387
|
-
|
|
25388
|
-
AND
|
|
25389
|
-
|
|
26278
|
+
`SELECT e.project_slug AS project_slug, e.assignment_slug AS assignment_slug
|
|
26279
|
+
FROM sessions s
|
|
26280
|
+
JOIN engagement e ON e.session_id = s.session_id
|
|
26281
|
+
WHERE s.path = ?
|
|
26282
|
+
AND julianday(s.started) <= julianday(?)
|
|
26283
|
+
AND (s.ended IS NULL OR julianday(s.ended) >= julianday(?))
|
|
26284
|
+
AND julianday(e.started_at) <= julianday(?)
|
|
26285
|
+
AND (e.ended_at IS NULL OR julianday(e.ended_at) > julianday(?))
|
|
26286
|
+
ORDER BY julianday(s.started) DESC, julianday(e.started_at) DESC
|
|
25390
26287
|
LIMIT 1`
|
|
25391
|
-
).get(
|
|
26288
|
+
).get(
|
|
26289
|
+
input.cwd,
|
|
26290
|
+
input.eventTs,
|
|
26291
|
+
input.eventTs,
|
|
26292
|
+
input.eventTs,
|
|
26293
|
+
input.eventTs
|
|
26294
|
+
);
|
|
25392
26295
|
if (exact) {
|
|
25393
26296
|
return { projectSlug: exact.project_slug, assignmentSlug: exact.assignment_slug };
|
|
25394
26297
|
}
|
|
25395
26298
|
if (input.eventTs.endsWith("T00:00:00.000Z")) {
|
|
25396
26299
|
const sameDay = database.prepare(
|
|
25397
|
-
`SELECT DISTINCT project_slug, assignment_slug
|
|
25398
|
-
FROM sessions
|
|
25399
|
-
|
|
25400
|
-
|
|
25401
|
-
AND
|
|
25402
|
-
|
|
26300
|
+
`SELECT DISTINCT e.project_slug AS project_slug, e.assignment_slug AS assignment_slug
|
|
26301
|
+
FROM sessions s
|
|
26302
|
+
JOIN engagement e ON e.session_id = s.session_id
|
|
26303
|
+
WHERE s.path = ?
|
|
26304
|
+
AND date(s.started) <= date(?)
|
|
26305
|
+
AND (s.ended IS NULL OR date(s.ended) >= date(?))
|
|
26306
|
+
AND date(e.started_at) <= date(?)
|
|
26307
|
+
AND (e.ended_at IS NULL OR date(e.ended_at) >= date(?))`
|
|
26308
|
+
).all(
|
|
26309
|
+
input.cwd,
|
|
26310
|
+
input.eventTs,
|
|
26311
|
+
input.eventTs,
|
|
26312
|
+
input.eventTs,
|
|
26313
|
+
input.eventTs
|
|
26314
|
+
);
|
|
25403
26315
|
if (sameDay.length === 1) {
|
|
25404
26316
|
return {
|
|
25405
26317
|
projectSlug: sameDay[0].project_slug,
|
|
@@ -25470,41 +26382,6 @@ function runRollup() {
|
|
|
25470
26382
|
return { daysComputed, rowsWritten };
|
|
25471
26383
|
}
|
|
25472
26384
|
|
|
25473
|
-
// src/usage/pricing.ts
|
|
25474
|
-
var PER_MILLION = 1e6;
|
|
25475
|
-
var MODEL_PRICING = {
|
|
25476
|
-
// Moonshot Kimi K2.6 — the model pi emits today. Official Moonshot list price.
|
|
25477
|
-
// source: https://platform.moonshot.ai/ (official) — cross-checked
|
|
25478
|
-
// https://openrouter.ai/moonshotai/kimi-k2.6 (retrieved 2026-06-17)
|
|
25479
|
-
"moonshotai/kimi-k2.6": { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0.95 },
|
|
25480
|
-
// Moonshot Kimi K2.5 — prior Kimi model. Official Moonshot list price.
|
|
25481
|
-
// source: https://platform.moonshot.ai/ — cross-checked
|
|
25482
|
-
// https://openrouter.ai/moonshotai/kimi-k2.5 (retrieved 2026-06-17)
|
|
25483
|
-
"moonshotai/kimi-k2.5": { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0.6 },
|
|
25484
|
-
// Z.ai (Zhipu) GLM-5.2 — official z.ai platform list price.
|
|
25485
|
-
// source: https://docs.z.ai/guides/overview/pricing — cross-checked
|
|
25486
|
-
// https://openrouter.ai/z-ai/glm-5 (retrieved 2026-06-18)
|
|
25487
|
-
"zai-org/glm-5.2": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 1.4 },
|
|
25488
|
-
// MiniMax M2.5 — official MiniMax platform list price. No separately-pinned
|
|
25489
|
-
// cached-read rate is published, so cacheRead is set conservatively to the
|
|
25490
|
-
// input rate (upper bound); revise if MiniMax publishes a cache rate.
|
|
25491
|
-
// source: https://platform.minimax.io/docs/guides/pricing-token-plan
|
|
25492
|
-
// — cross-checked https://openrouter.ai/minimax/minimax-m2.5 (retrieved 2026-06-18)
|
|
25493
|
-
"minimaxai/minimax-m2.5": { input: 0.15, output: 0.9, cacheRead: 0.15, cacheWrite: 0.15 }
|
|
25494
|
-
// NOTE: opaque Synthetic tier aliases like `syn:large:text` have no public
|
|
25495
|
-
// per-token rate (they route to whatever Synthetic assigns), so they remain
|
|
25496
|
-
// unpriced (→ $0). Reseller discounts (e.g. DeepInfra K2.6 0.75/3.50/0.15) are
|
|
25497
|
-
// rejected by the canonical-source rule and are NOT used here.
|
|
25498
|
-
};
|
|
25499
|
-
function normalizeModelKey(model) {
|
|
25500
|
-
return model.replace(/^\s*\[[^\]]*\]\s*/, "").replace(/^hf:/i, "").trim().toLowerCase();
|
|
25501
|
-
}
|
|
25502
|
-
function priceForModel(model, tokens) {
|
|
25503
|
-
const rate = MODEL_PRICING[normalizeModelKey(model)];
|
|
25504
|
-
if (!rate) return null;
|
|
25505
|
-
return (tokens.inputTokens * rate.input + tokens.outputTokens * rate.output + tokens.cacheReadTokens * rate.cacheRead + tokens.cacheCreationTokens * rate.cacheWrite) / PER_MILLION;
|
|
25506
|
-
}
|
|
25507
|
-
|
|
25508
26385
|
// src/usage/collect.ts
|
|
25509
26386
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
25510
26387
|
function formatDayFromCcusageDate(yyyymmdd) {
|
|
@@ -25723,7 +26600,7 @@ function createDashboardServer(options) {
|
|
|
25723
26600
|
(async () => {
|
|
25724
26601
|
try {
|
|
25725
26602
|
const configResult = await migrateLegacyConfig(
|
|
25726
|
-
|
|
26603
|
+
resolve48(syntaurRoot(), "config.md")
|
|
25727
26604
|
);
|
|
25728
26605
|
const projectResult = await migrateLegacyProjectFiles(projectsDir);
|
|
25729
26606
|
const summary = summarizeMigration(projectResult, configResult);
|
|
@@ -26246,14 +27123,14 @@ function createDashboardServer(options) {
|
|
|
26246
27123
|
app.use("/api/backup", createBackupRouter());
|
|
26247
27124
|
if (serveStaticUi && dashboardDistPath) {
|
|
26248
27125
|
const sendOpts = { dotfiles: "allow" };
|
|
26249
|
-
app.use("/assets", express.static(
|
|
27126
|
+
app.use("/assets", express.static(resolve48(dashboardDistPath, "assets"), sendOpts));
|
|
26250
27127
|
app.use(express.static(dashboardDistPath, { ...sendOpts, index: false, fallthrough: true }));
|
|
26251
27128
|
app.get("{*path}", async (req2, res) => {
|
|
26252
27129
|
if (req2.path.startsWith("/api") || req2.path === "/ws" || req2.path.startsWith("/assets")) {
|
|
26253
27130
|
res.status(404).json({ error: "Not Found" });
|
|
26254
27131
|
return;
|
|
26255
27132
|
}
|
|
26256
|
-
const indexPath =
|
|
27133
|
+
const indexPath = resolve48(dashboardDistPath, "index.html");
|
|
26257
27134
|
if (!await fileExists(indexPath)) {
|
|
26258
27135
|
res.status(503).send(
|
|
26259
27136
|
'Dashboard not built. Run "npm run build:dashboard" first.'
|
|
@@ -26289,8 +27166,8 @@ function createDashboardServer(options) {
|
|
|
26289
27166
|
if (!await migrationGate()) return;
|
|
26290
27167
|
try {
|
|
26291
27168
|
const context = await resolveDeriveContext2();
|
|
26292
|
-
const projectDir = projectSlug ?
|
|
26293
|
-
const path = projectDir ?
|
|
27169
|
+
const projectDir = projectSlug ? resolve48(projectsDir, projectSlug) : null;
|
|
27170
|
+
const path = projectDir ? resolve48(projectDir, "assignments", assignmentSlug, "assignment.md") : resolve48(assignmentsDir2, assignmentSlug, "assignment.md");
|
|
26294
27171
|
if (!await fileExists(path)) return;
|
|
26295
27172
|
const result = await recomputeAndWrite2(path, {
|
|
26296
27173
|
cause: "derive",
|
|
@@ -26343,8 +27220,8 @@ function createDashboardServer(options) {
|
|
|
26343
27220
|
serversDir: serversDir2,
|
|
26344
27221
|
playbooksDir: playbooksDir2,
|
|
26345
27222
|
todosDir: todosDir2,
|
|
26346
|
-
dbPath:
|
|
26347
|
-
configPath:
|
|
27223
|
+
dbPath: resolve48(syntaurRoot(), "syntaur.db"),
|
|
27224
|
+
configPath: resolve48(syntaurRoot(), "config.md"),
|
|
26348
27225
|
onMessage: broadcast,
|
|
26349
27226
|
onAssignmentChanged: (projectSlug, assignmentSlug) => {
|
|
26350
27227
|
void recomputeOne(projectSlug, assignmentSlug);
|
|
@@ -26412,7 +27289,7 @@ function createDashboardServer(options) {
|
|
|
26412
27289
|
}
|
|
26413
27290
|
});
|
|
26414
27291
|
server.listen(port, () => {
|
|
26415
|
-
const portFile =
|
|
27292
|
+
const portFile = resolve48(syntaurRoot(), "dashboard-port");
|
|
26416
27293
|
writeFile8(portFile, String(port), "utf-8").catch(() => {
|
|
26417
27294
|
});
|
|
26418
27295
|
resolvePromise();
|
|
@@ -26436,7 +27313,7 @@ function createDashboardServer(options) {
|
|
|
26436
27313
|
client.terminate();
|
|
26437
27314
|
}
|
|
26438
27315
|
clients.clear();
|
|
26439
|
-
const portFile =
|
|
27316
|
+
const portFile = resolve48(syntaurRoot(), "dashboard-port");
|
|
26440
27317
|
await unlink9(portFile).catch(() => {
|
|
26441
27318
|
});
|
|
26442
27319
|
server.closeAllConnections?.();
|