syntaur 0.4.4 → 0.5.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/dashboard/dist/assets/{_basePickBy-CWivToyi.js → _basePickBy-Bcut0btZ.js} +1 -1
- package/dashboard/dist/assets/{_baseUniq-C-qj6E4l.js → _baseUniq-AQSP2JEk.js} +1 -1
- package/dashboard/dist/assets/{arc-Dn5BIqMa.js → arc-BLTpY9lc.js} +1 -1
- package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-D5D0K7rY.js → architectureDiagram-2XIMDMQ5-CJtwMY_X.js} +1 -1
- package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-DVmYPMbu.js → blockDiagram-WCTKOSBZ-Don-O7X7.js} +1 -1
- package/dashboard/dist/assets/{c4Diagram-IC4MRINW-BEasxbnl.js → c4Diagram-IC4MRINW-C_M3yTTB.js} +1 -1
- package/dashboard/dist/assets/channel-BfXmPwE5.js +1 -0
- package/dashboard/dist/assets/{chunk-4BX2VUAB-LDIrtI5E.js → chunk-4BX2VUAB-CGss0jXe.js} +1 -1
- package/dashboard/dist/assets/{chunk-55IACEB6-CaEBUJYu.js → chunk-55IACEB6-BatoPJga.js} +1 -1
- package/dashboard/dist/assets/{chunk-FMBD7UC4-B-GjCpdr.js → chunk-FMBD7UC4-DxH4wO82.js} +1 -1
- package/dashboard/dist/assets/{chunk-JSJVCQXG-BLVVcezm.js → chunk-JSJVCQXG-BL3izAFQ.js} +1 -1
- package/dashboard/dist/assets/{chunk-KX2RTZJC-DqCNEw4h.js → chunk-KX2RTZJC-GnqXwnge.js} +1 -1
- package/dashboard/dist/assets/{chunk-NQ4KR5QH-BCPbFf5I.js → chunk-NQ4KR5QH-gvCn4QMb.js} +1 -1
- package/dashboard/dist/assets/{chunk-QZHKN3VN-Ci0C85q_.js → chunk-QZHKN3VN-CYGWogyi.js} +1 -1
- package/dashboard/dist/assets/{chunk-WL4C6EOR-VVhAMMYU.js → chunk-WL4C6EOR-D9mVTQ1F.js} +1 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-D7_G1qy0.js +1 -0
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-D7_G1qy0.js +1 -0
- package/dashboard/dist/assets/clone-BKG-N796.js +1 -0
- package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-CO9uwgYO.js → cose-bilkent-S5V4N54A-CUWQCKt4.js} +1 -1
- package/dashboard/dist/assets/{dagre-KLK3FWXG-bwLLXcL4.js → dagre-KLK3FWXG-CH3ijEvV.js} +1 -1
- package/dashboard/dist/assets/{diagram-E7M64L7V-RuS5R6V1.js → diagram-E7M64L7V-sq83lpV1.js} +1 -1
- package/dashboard/dist/assets/{diagram-IFDJBPK2-BQDJAHQd.js → diagram-IFDJBPK2-BzQG_rtq.js} +1 -1
- package/dashboard/dist/assets/{diagram-P4PSJMXO-yLEsgzE5.js → diagram-P4PSJMXO-Dg0eZn0q.js} +1 -1
- package/dashboard/dist/assets/{erDiagram-INFDFZHY-na6dUhY0.js → erDiagram-INFDFZHY-4b9eQ0uj.js} +1 -1
- package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-BIcrzwJR.js → flowDiagram-PKNHOUZH-C9fzKcsZ.js} +1 -1
- package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-DHWRJn-D.js → ganttDiagram-A5KZAMGK-Bzt6i9SH.js} +1 -1
- package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-LGxDjL71.js → gitGraphDiagram-K3NZZRJ6-D0wFOagh.js} +1 -1
- package/dashboard/dist/assets/{graph-BUqNu277.js → graph-EEIGvqDh.js} +1 -1
- package/dashboard/dist/assets/index-Bu6ma6my.css +1 -0
- package/dashboard/dist/assets/index-C7f0ySJE.js +481 -0
- package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-DidoA2hb.js → infoDiagram-LFFYTUFH-DLYMsj1D.js} +1 -1
- package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-CdlZkbhV.js → ishikawaDiagram-PHBUUO56-DVebKkzl.js} +1 -1
- package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-luhcz_gn.js → journeyDiagram-4ABVD52K-BsmgOWVw.js} +1 -1
- package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-Coidw9XE.js → kanban-definition-K7BYSVSG-BTnHf0ey.js} +1 -1
- package/dashboard/dist/assets/{layout-_aBAAleE.js → layout-BbM7HRvv.js} +1 -1
- package/dashboard/dist/assets/{linear-D8mFnDSx.js → linear-C37bJKPO.js} +1 -1
- package/dashboard/dist/assets/{mermaid.core-BpP2keU-.js → mermaid.core-MZ_JgnRL.js} +4 -4
- package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-LjvrTe2z.js → mindmap-definition-YRQLILUH-CgHS4hFo.js} +1 -1
- package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CkA2iU6e.js → pieDiagram-SKSYHLDU-CmAgopJe.js} +1 -1
- package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-BRmhKHQG.js → quadrantDiagram-337W2JSQ-BvzYUPR6.js} +1 -1
- package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BYCQ4uFX.js → requirementDiagram-Z7DCOOCP-Bs52VP7k.js} +1 -1
- package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-C8SVk50M.js → sankeyDiagram-WA2Y5GQK-aXvGPR1o.js} +1 -1
- package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-CbA_2lnP.js → sequenceDiagram-2WXFIKYE-CzgcfU6K.js} +1 -1
- package/dashboard/dist/assets/{stateDiagram-RAJIS63D-D6ZtjAHE.js → stateDiagram-RAJIS63D-BXBJf9Hq.js} +1 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-QqOtsuOs.js +1 -0
- package/dashboard/dist/assets/{timeline-definition-YZTLITO2-B2Uf-emK.js → timeline-definition-YZTLITO2-BsXp26Ai.js} +1 -1
- package/dashboard/dist/assets/{treemap-KZPCXAKY-CYnFKsuJ.js → treemap-KZPCXAKY-C3WbDii1.js} +1 -1
- package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-Cnj0qiDO.js → vennDiagram-LZ73GAT5-B28LMHWd.js} +1 -1
- package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-BJy2mbL9.js → xychartDiagram-JWTSCODW-C3Xwz8mS.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/dashboard/server.js +1329 -402
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +1591 -533
- package/dist/index.js.map +1 -1
- package/examples/playbooks/assignment-creation.md +19 -0
- package/examples/playbooks/assignment-planning.md +28 -0
- package/package.json +1 -1
- package/platforms/claude-code/.orphaned_at +1 -0
- package/platforms/claude-code/agents/syntaur-expert.md +2 -2
- package/platforms/claude-code/commands/create-assignment/create-assignment.md +1 -1
- package/platforms/codex/agents/syntaur-operator.md +2 -2
- package/vendor/syntaur-skills/README.md +12 -0
- package/vendor/syntaur-skills/skills/create-assignment/SKILL.md +5 -4
- package/dashboard/dist/assets/channel-DqU_8tiy.js +0 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-D29Eeoe8.js +0 -1
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-D29Eeoe8.js +0 -1
- package/dashboard/dist/assets/clone-Bok8Q3Jj.js +0 -1
- package/dashboard/dist/assets/index-D-fepllQ.js +0 -481
- package/dashboard/dist/assets/index-DnHyQJJH.css +0 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DeYN4wV6.js +0 -1
- package/examples/playbooks/plan-versioning.md +0 -36
- package/examples/playbooks/read-before-plan.md +0 -30
package/dist/dashboard/server.js
CHANGED
|
@@ -36,6 +36,9 @@ function playbooksDir() {
|
|
|
36
36
|
function todosDir() {
|
|
37
37
|
return resolve(syntaurRoot(), "todos");
|
|
38
38
|
}
|
|
39
|
+
function projectTodosDir(projectsDir, projectSlug) {
|
|
40
|
+
return resolve(projectsDir, projectSlug, "todos");
|
|
41
|
+
}
|
|
39
42
|
var init_paths = __esm({
|
|
40
43
|
"src/utils/paths.ts"() {
|
|
41
44
|
"use strict";
|
|
@@ -457,10 +460,10 @@ var init_lifecycle = __esm({
|
|
|
457
460
|
});
|
|
458
461
|
|
|
459
462
|
// src/templates/config.ts
|
|
460
|
-
function renderConfig(
|
|
463
|
+
function renderConfig(params2) {
|
|
461
464
|
return `---
|
|
462
465
|
version: "1.0"
|
|
463
|
-
defaultProjectDir: ${
|
|
466
|
+
defaultProjectDir: ${params2.defaultProjectDir}
|
|
464
467
|
onboarding:
|
|
465
468
|
completed: false
|
|
466
469
|
agentDefaults:
|
|
@@ -622,6 +625,27 @@ var init_fs_migration = __esm({
|
|
|
622
625
|
// src/utils/config.ts
|
|
623
626
|
import { readFile as readFile3 } from "fs/promises";
|
|
624
627
|
import { resolve as resolve4, isAbsolute } from "path";
|
|
628
|
+
function cloneDefaultConfig() {
|
|
629
|
+
return {
|
|
630
|
+
...DEFAULT_CONFIG,
|
|
631
|
+
onboarding: { ...DEFAULT_CONFIG.onboarding },
|
|
632
|
+
agentDefaults: { ...DEFAULT_CONFIG.agentDefaults },
|
|
633
|
+
integrations: { ...DEFAULT_CONFIG.integrations },
|
|
634
|
+
backup: DEFAULT_CONFIG.backup ? { ...DEFAULT_CONFIG.backup } : null,
|
|
635
|
+
statuses: DEFAULT_CONFIG.statuses ? {
|
|
636
|
+
statuses: DEFAULT_CONFIG.statuses.statuses.map((s) => ({ ...s })),
|
|
637
|
+
order: [...DEFAULT_CONFIG.statuses.order],
|
|
638
|
+
transitions: DEFAULT_CONFIG.statuses.transitions.map((t) => ({ ...t }))
|
|
639
|
+
} : null,
|
|
640
|
+
types: DEFAULT_CONFIG.types ? {
|
|
641
|
+
definitions: DEFAULT_CONFIG.types.definitions.map((d) => ({ ...d })),
|
|
642
|
+
default: DEFAULT_CONFIG.types.default
|
|
643
|
+
} : null,
|
|
644
|
+
playbooks: {
|
|
645
|
+
disabled: [...DEFAULT_CONFIG.playbooks.disabled]
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|
|
625
649
|
function parseFrontmatter(content) {
|
|
626
650
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
627
651
|
if (!match) return {};
|
|
@@ -774,6 +798,82 @@ function serializeBackupConfig(backup) {
|
|
|
774
798
|
lines.push(` lastRestore: ${backup.lastRestore ?? "null"}`);
|
|
775
799
|
return lines.join("\n");
|
|
776
800
|
}
|
|
801
|
+
function serializePlaybooksConfig(playbooks) {
|
|
802
|
+
if (!playbooks.disabled || playbooks.disabled.length === 0) {
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
const lines = ["playbooks:", " disabled:"];
|
|
806
|
+
for (const slug of playbooks.disabled) {
|
|
807
|
+
lines.push(` - ${slug}`);
|
|
808
|
+
}
|
|
809
|
+
return lines.join("\n");
|
|
810
|
+
}
|
|
811
|
+
function parsePlaybooksConfig(fmBlock) {
|
|
812
|
+
const blockStart = fmBlock.match(/^playbooks:\s*$/m);
|
|
813
|
+
if (!blockStart) {
|
|
814
|
+
return { disabled: [] };
|
|
815
|
+
}
|
|
816
|
+
const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
|
|
817
|
+
const remaining = fmBlock.slice(startIdx).split("\n");
|
|
818
|
+
const disabled = [];
|
|
819
|
+
let currentSection = null;
|
|
820
|
+
for (const line of remaining) {
|
|
821
|
+
const trimmed = line.trimStart();
|
|
822
|
+
const indent = line.length - trimmed.length;
|
|
823
|
+
if (indent === 0 && trimmed.length > 0) break;
|
|
824
|
+
if (trimmed === "") continue;
|
|
825
|
+
if (indent === 2 && trimmed.startsWith("disabled:")) {
|
|
826
|
+
currentSection = "disabled";
|
|
827
|
+
const afterColon = trimmed.slice("disabled:".length).trim();
|
|
828
|
+
if (afterColon === "[]" || afterColon === "") {
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (currentSection === "disabled" && indent >= 4 && trimmed.startsWith("- ")) {
|
|
834
|
+
const raw = trimmed.slice(2).trim().replace(/^["']|["']$/g, "");
|
|
835
|
+
if (raw.length === 0) continue;
|
|
836
|
+
if (/\s/.test(raw)) {
|
|
837
|
+
console.warn(`Warning: config.md playbooks.disabled entry "${raw}" contains whitespace, ignoring`);
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
disabled.push(raw);
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return { disabled };
|
|
845
|
+
}
|
|
846
|
+
async function updatePlaybooksConfig(playbooks) {
|
|
847
|
+
const configPath = resolve4(syntaurRoot(), "config.md");
|
|
848
|
+
const current = (await readConfig()).playbooks;
|
|
849
|
+
const nextPlaybooks = {
|
|
850
|
+
disabled: Array.from(new Set(playbooks.disabled ?? current.disabled))
|
|
851
|
+
};
|
|
852
|
+
const playbooksBlock = serializePlaybooksConfig(nextPlaybooks);
|
|
853
|
+
const existing = await fileExists(configPath) ? await readFile3(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
854
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
855
|
+
if (!fmMatch) {
|
|
856
|
+
const bodyBlock = playbooksBlock ? `${playbooksBlock}
|
|
857
|
+
` : "";
|
|
858
|
+
const content = `---
|
|
859
|
+
version: "2.0"
|
|
860
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
861
|
+
${bodyBlock}---
|
|
862
|
+
${existing}`;
|
|
863
|
+
await writeFileForce(configPath, content);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const fmBlock = fmMatch[2];
|
|
867
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
868
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "playbooks");
|
|
869
|
+
const newFm = playbooksBlock ? `${cleanedFm}
|
|
870
|
+
${playbooksBlock}`.replace(/^\n+/, "") : cleanedFm;
|
|
871
|
+
const normalizedFm = newFm.replace(/\n+$/, "");
|
|
872
|
+
const newContent = `---
|
|
873
|
+
${normalizedFm}
|
|
874
|
+
---${afterFrontmatter}`;
|
|
875
|
+
await writeFileForce(configPath, newContent);
|
|
876
|
+
}
|
|
777
877
|
function stripTopLevelBlock(fmBlock, key) {
|
|
778
878
|
const blockStart = fmBlock.match(new RegExp(`^${key}:\\s*$`, "m"));
|
|
779
879
|
if (!blockStart) {
|
|
@@ -915,7 +1015,7 @@ ${normalizedFm}
|
|
|
915
1015
|
async function readConfig() {
|
|
916
1016
|
const configPath = resolve4(syntaurRoot(), "config.md");
|
|
917
1017
|
if (!await fileExists(configPath)) {
|
|
918
|
-
return
|
|
1018
|
+
return cloneDefaultConfig();
|
|
919
1019
|
}
|
|
920
1020
|
if (!migratedConfigPaths.has(configPath)) {
|
|
921
1021
|
migratedConfigPaths.add(configPath);
|
|
@@ -925,7 +1025,7 @@ async function readConfig() {
|
|
|
925
1025
|
const fm = parseFrontmatter(content);
|
|
926
1026
|
if (Object.keys(fm).length === 0) {
|
|
927
1027
|
console.warn("Warning: ~/.syntaur/config.md has malformed frontmatter, using defaults");
|
|
928
|
-
return
|
|
1028
|
+
return cloneDefaultConfig();
|
|
929
1029
|
}
|
|
930
1030
|
let projectDir = fm["defaultProjectDir"] ? expandHome(String(fm["defaultProjectDir"])) : DEFAULT_CONFIG.defaultProjectDir;
|
|
931
1031
|
if (!isAbsolute(projectDir)) {
|
|
@@ -934,6 +1034,7 @@ async function readConfig() {
|
|
|
934
1034
|
);
|
|
935
1035
|
projectDir = DEFAULT_CONFIG.defaultProjectDir;
|
|
936
1036
|
}
|
|
1037
|
+
const fmBlock = content.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
|
|
937
1038
|
return {
|
|
938
1039
|
version: fm["version"] || DEFAULT_CONFIG.version,
|
|
939
1040
|
defaultProjectDir: projectDir,
|
|
@@ -965,7 +1066,8 @@ async function readConfig() {
|
|
|
965
1066
|
lastRestore: fm["backup.lastRestore"] && fm["backup.lastRestore"] !== "null" ? fm["backup.lastRestore"] : null
|
|
966
1067
|
} : null,
|
|
967
1068
|
statuses: parseStatusConfig(content),
|
|
968
|
-
types: null
|
|
1069
|
+
types: null,
|
|
1070
|
+
playbooks: parsePlaybooksConfig(fmBlock)
|
|
969
1071
|
};
|
|
970
1072
|
}
|
|
971
1073
|
var DEFAULT_CONFIG, migratedConfigPaths;
|
|
@@ -993,7 +1095,10 @@ var init_config2 = __esm({
|
|
|
993
1095
|
},
|
|
994
1096
|
backup: null,
|
|
995
1097
|
statuses: null,
|
|
996
|
-
types: null
|
|
1098
|
+
types: null,
|
|
1099
|
+
playbooks: {
|
|
1100
|
+
disabled: []
|
|
1101
|
+
}
|
|
997
1102
|
};
|
|
998
1103
|
migratedConfigPaths = /* @__PURE__ */ new Set();
|
|
999
1104
|
}
|
|
@@ -1128,6 +1233,7 @@ function parseAssignmentFull(fileContent) {
|
|
|
1128
1233
|
slug: getField(fm, "slug") ?? "",
|
|
1129
1234
|
title: getField(fm, "title") ?? "",
|
|
1130
1235
|
project: getField(fm, "project"),
|
|
1236
|
+
workspaceGroup: getField(fm, "workspaceGroup"),
|
|
1131
1237
|
type: getField(fm, "type"),
|
|
1132
1238
|
status: getField(fm, "status") ?? "pending",
|
|
1133
1239
|
priority: getField(fm, "priority") ?? "medium",
|
|
@@ -1284,47 +1390,160 @@ var init_parser = __esm({
|
|
|
1284
1390
|
}
|
|
1285
1391
|
});
|
|
1286
1392
|
|
|
1287
|
-
// src/utils/
|
|
1393
|
+
// src/utils/playbooks.ts
|
|
1288
1394
|
import { resolve as resolve5 } from "path";
|
|
1289
1395
|
import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
|
|
1396
|
+
function isVisiblePlaybookFile(name, isFile) {
|
|
1397
|
+
return isFile && name.endsWith(".md") && !name.startsWith("_") && name !== "manifest.md";
|
|
1398
|
+
}
|
|
1399
|
+
async function resolvePlaybookSlug(playbooksDir2, slug) {
|
|
1400
|
+
if (!await fileExists(playbooksDir2)) return null;
|
|
1401
|
+
const entries = await readdir2(playbooksDir2, { withFileTypes: true });
|
|
1402
|
+
let filenameStemFallback = null;
|
|
1403
|
+
for (const entry of entries) {
|
|
1404
|
+
if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
|
|
1405
|
+
const filePath = resolve5(playbooksDir2, entry.name);
|
|
1406
|
+
const raw = await readFile4(filePath, "utf-8");
|
|
1407
|
+
const parsed = parsePlaybook(raw);
|
|
1408
|
+
const canonical = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
1409
|
+
if (canonical === slug) {
|
|
1410
|
+
return { filename: entry.name, slug: canonical, parsed };
|
|
1411
|
+
}
|
|
1412
|
+
if (!parsed.slug && entry.name.replace(/\.md$/, "") === slug) {
|
|
1413
|
+
filenameStemFallback = { filename: entry.name, slug: canonical, parsed };
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
return filenameStemFallback;
|
|
1417
|
+
}
|
|
1418
|
+
async function setPlaybookEnabled(playbooksDir2, slug, enabled) {
|
|
1419
|
+
const resolved = await resolvePlaybookSlug(playbooksDir2, slug);
|
|
1420
|
+
if (!resolved) {
|
|
1421
|
+
throw new Error(`Playbook "${slug}" not found in ${playbooksDir2}`);
|
|
1422
|
+
}
|
|
1423
|
+
const config = await readConfig();
|
|
1424
|
+
const disabledSet = new Set(config.playbooks.disabled);
|
|
1425
|
+
const wasDisabled = disabledSet.has(resolved.slug);
|
|
1426
|
+
const shouldBeDisabled = !enabled;
|
|
1427
|
+
if (wasDisabled === shouldBeDisabled) {
|
|
1428
|
+
return { slug: resolved.slug, enabled, changed: false };
|
|
1429
|
+
}
|
|
1430
|
+
if (shouldBeDisabled) {
|
|
1431
|
+
disabledSet.add(resolved.slug);
|
|
1432
|
+
} else {
|
|
1433
|
+
disabledSet.delete(resolved.slug);
|
|
1434
|
+
}
|
|
1435
|
+
await updatePlaybooksConfig({ disabled: Array.from(disabledSet).sort() });
|
|
1436
|
+
await rebuildPlaybookManifest(playbooksDir2);
|
|
1437
|
+
return { slug: resolved.slug, enabled, changed: true };
|
|
1438
|
+
}
|
|
1439
|
+
async function removeFromDisabledList(slug) {
|
|
1440
|
+
const config = await readConfig();
|
|
1441
|
+
if (!config.playbooks.disabled.includes(slug)) return;
|
|
1442
|
+
await updatePlaybooksConfig({
|
|
1443
|
+
disabled: config.playbooks.disabled.filter((s) => s !== slug)
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
async function rebuildPlaybookManifest(playbooksDir2) {
|
|
1447
|
+
if (!await fileExists(playbooksDir2)) return;
|
|
1448
|
+
const config = await readConfig();
|
|
1449
|
+
const disabledSet = new Set(config.playbooks.disabled);
|
|
1450
|
+
const entries = await readdir2(playbooksDir2, { withFileTypes: true });
|
|
1451
|
+
const rows = [];
|
|
1452
|
+
for (const entry of entries) {
|
|
1453
|
+
if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
|
|
1454
|
+
const raw = await readFile4(resolve5(playbooksDir2, entry.name), "utf-8");
|
|
1455
|
+
const parsed = parsePlaybook(raw);
|
|
1456
|
+
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
1457
|
+
if (disabledSet.has(slug)) continue;
|
|
1458
|
+
rows.push({
|
|
1459
|
+
name: parsed.name || slug,
|
|
1460
|
+
slug,
|
|
1461
|
+
description: parsed.description,
|
|
1462
|
+
whenToUse: parsed.whenToUse
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
1466
|
+
const timestamp = nowTimestamp();
|
|
1467
|
+
const lines = [
|
|
1468
|
+
"---",
|
|
1469
|
+
`generated: "${timestamp}"`,
|
|
1470
|
+
`total: ${rows.length}`,
|
|
1471
|
+
"---",
|
|
1472
|
+
"",
|
|
1473
|
+
"# Playbooks",
|
|
1474
|
+
"",
|
|
1475
|
+
"Behavioral rules for AI agents. Read and follow all playbooks before starting work.",
|
|
1476
|
+
""
|
|
1477
|
+
];
|
|
1478
|
+
for (const row of rows) {
|
|
1479
|
+
lines.push(`- **[${row.name}](${row.slug}.md)** \u2014 ${row.description}`);
|
|
1480
|
+
if (row.whenToUse) {
|
|
1481
|
+
lines.push(` _When to use: ${row.whenToUse}_`);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
lines.push("");
|
|
1485
|
+
await writeFileForce(resolve5(playbooksDir2, "manifest.md"), lines.join("\n"));
|
|
1486
|
+
}
|
|
1487
|
+
var init_playbooks = __esm({
|
|
1488
|
+
"src/utils/playbooks.ts"() {
|
|
1489
|
+
"use strict";
|
|
1490
|
+
init_fs();
|
|
1491
|
+
init_parser();
|
|
1492
|
+
init_timestamp();
|
|
1493
|
+
init_config2();
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
// src/utils/assignment-resolver.ts
|
|
1498
|
+
import { resolve as resolve6 } from "path";
|
|
1499
|
+
import { readdir as readdir3, readFile as readFile5 } from "fs/promises";
|
|
1290
1500
|
async function resolveAssignmentById(projectsDir, assignmentsDir, id) {
|
|
1291
1501
|
let standaloneMatch = null;
|
|
1292
1502
|
let projectMatch = null;
|
|
1293
|
-
const standaloneDir =
|
|
1294
|
-
const standalonePath =
|
|
1503
|
+
const standaloneDir = resolve6(assignmentsDir, id);
|
|
1504
|
+
const standalonePath = resolve6(standaloneDir, "assignment.md");
|
|
1295
1505
|
if (await fileExists(standalonePath)) {
|
|
1506
|
+
let workspaceGroup = null;
|
|
1507
|
+
try {
|
|
1508
|
+
const content = await readFile5(standalonePath, "utf-8");
|
|
1509
|
+
const [fm] = extractFrontmatter2(content);
|
|
1510
|
+
workspaceGroup = getField(fm, "workspaceGroup");
|
|
1511
|
+
} catch {
|
|
1512
|
+
}
|
|
1296
1513
|
standaloneMatch = {
|
|
1297
1514
|
assignmentDir: standaloneDir,
|
|
1298
1515
|
projectSlug: null,
|
|
1299
1516
|
assignmentSlug: id,
|
|
1300
1517
|
id,
|
|
1301
|
-
standalone: true
|
|
1518
|
+
standalone: true,
|
|
1519
|
+
workspaceGroup
|
|
1302
1520
|
};
|
|
1303
1521
|
}
|
|
1304
1522
|
if (await fileExists(projectsDir)) {
|
|
1305
1523
|
try {
|
|
1306
|
-
const projects = await
|
|
1524
|
+
const projects = await readdir3(projectsDir, { withFileTypes: true });
|
|
1307
1525
|
for (const p of projects) {
|
|
1308
1526
|
if (!p.isDirectory()) continue;
|
|
1309
1527
|
if (p.name.startsWith(".") || p.name.startsWith("_")) continue;
|
|
1310
|
-
const assignmentsPath =
|
|
1528
|
+
const assignmentsPath = resolve6(projectsDir, p.name, "assignments");
|
|
1311
1529
|
if (!await fileExists(assignmentsPath)) continue;
|
|
1312
|
-
const entries = await
|
|
1530
|
+
const entries = await readdir3(assignmentsPath, { withFileTypes: true });
|
|
1313
1531
|
for (const a of entries) {
|
|
1314
1532
|
if (!a.isDirectory()) continue;
|
|
1315
|
-
const aPath =
|
|
1533
|
+
const aPath = resolve6(assignmentsPath, a.name, "assignment.md");
|
|
1316
1534
|
if (!await fileExists(aPath)) continue;
|
|
1317
1535
|
try {
|
|
1318
|
-
const content = await
|
|
1536
|
+
const content = await readFile5(aPath, "utf-8");
|
|
1319
1537
|
const [fm] = extractFrontmatter2(content);
|
|
1320
1538
|
const fileId = getField(fm, "id");
|
|
1321
1539
|
if (fileId === id) {
|
|
1322
1540
|
projectMatch = {
|
|
1323
|
-
assignmentDir:
|
|
1541
|
+
assignmentDir: resolve6(assignmentsPath, a.name),
|
|
1324
1542
|
projectSlug: p.name,
|
|
1325
1543
|
assignmentSlug: a.name,
|
|
1326
1544
|
id,
|
|
1327
|
-
standalone: false
|
|
1545
|
+
standalone: false,
|
|
1546
|
+
workspaceGroup: null
|
|
1328
1547
|
};
|
|
1329
1548
|
break;
|
|
1330
1549
|
}
|
|
@@ -1700,8 +1919,18 @@ var init_help = __esm({
|
|
|
1700
1919
|
},
|
|
1701
1920
|
{
|
|
1702
1921
|
command: "syntaur list-playbooks",
|
|
1703
|
-
description: "List
|
|
1704
|
-
example: "syntaur list-playbooks"
|
|
1922
|
+
description: "List playbooks in the Syntaur home directory. Disabled playbooks are excluded by default; pass --all to include them with a (disabled) tag.",
|
|
1923
|
+
example: "syntaur list-playbooks --all"
|
|
1924
|
+
},
|
|
1925
|
+
{
|
|
1926
|
+
command: "syntaur enable-playbook",
|
|
1927
|
+
description: "Re-enable a previously-disabled playbook so agents load it again. Updates config.md and rebuilds manifest.md.",
|
|
1928
|
+
example: "syntaur enable-playbook commit-discipline"
|
|
1929
|
+
},
|
|
1930
|
+
{
|
|
1931
|
+
command: "syntaur disable-playbook",
|
|
1932
|
+
description: "Disable a playbook so agents no longer list or load it. Playbook file is untouched; state is tracked in config.md.",
|
|
1933
|
+
example: "syntaur disable-playbook commit-discipline"
|
|
1705
1934
|
}
|
|
1706
1935
|
];
|
|
1707
1936
|
WORKFLOW = [
|
|
@@ -1768,8 +1997,8 @@ var init_help = __esm({
|
|
|
1768
1997
|
});
|
|
1769
1998
|
|
|
1770
1999
|
// src/dashboard/servers.ts
|
|
1771
|
-
import { readdir as
|
|
1772
|
-
import { resolve as
|
|
2000
|
+
import { readdir as readdir4, readFile as readFile6, unlink } from "fs/promises";
|
|
2001
|
+
import { resolve as resolve7 } from "path";
|
|
1773
2002
|
function sanitizeSessionName(name) {
|
|
1774
2003
|
return name.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
1775
2004
|
}
|
|
@@ -1817,18 +2046,18 @@ async function registerSession(dir, rawName) {
|
|
|
1817
2046
|
lastRefreshed: now,
|
|
1818
2047
|
overrides: {}
|
|
1819
2048
|
});
|
|
1820
|
-
await writeFileForce(
|
|
2049
|
+
await writeFileForce(resolve7(dir, `${name}.md`), content);
|
|
1821
2050
|
return name;
|
|
1822
2051
|
}
|
|
1823
2052
|
async function listSessionFiles(dir) {
|
|
1824
2053
|
if (!await fileExists(dir)) return [];
|
|
1825
|
-
const entries = await
|
|
2054
|
+
const entries = await readdir4(dir);
|
|
1826
2055
|
return entries.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
|
|
1827
2056
|
}
|
|
1828
2057
|
async function readSessionFile(dir, name) {
|
|
1829
|
-
const filePath =
|
|
2058
|
+
const filePath = resolve7(dir, `${sanitizeSessionName(name)}.md`);
|
|
1830
2059
|
if (!await fileExists(filePath)) return null;
|
|
1831
|
-
const raw = await
|
|
2060
|
+
const raw = await readFile6(filePath, "utf-8");
|
|
1832
2061
|
const [frontmatter] = extractFrontmatter2(raw);
|
|
1833
2062
|
if (!frontmatter) return null;
|
|
1834
2063
|
const session = getField(frontmatter, "session") ?? name;
|
|
@@ -1868,7 +2097,7 @@ async function readSessionFile(dir, name) {
|
|
|
1868
2097
|
};
|
|
1869
2098
|
}
|
|
1870
2099
|
async function removeSession(dir, name) {
|
|
1871
|
-
const filePath =
|
|
2100
|
+
const filePath = resolve7(dir, `${sanitizeSessionName(name)}.md`);
|
|
1872
2101
|
if (await fileExists(filePath)) {
|
|
1873
2102
|
await unlink(filePath);
|
|
1874
2103
|
}
|
|
@@ -1877,7 +2106,7 @@ async function updateLastRefreshed(dir, name) {
|
|
|
1877
2106
|
const data = await readSessionFile(dir, name);
|
|
1878
2107
|
if (!data) return;
|
|
1879
2108
|
const content = buildSessionContent({ ...data, lastRefreshed: nowTimestamp2() });
|
|
1880
|
-
await writeFileForce(
|
|
2109
|
+
await writeFileForce(resolve7(dir, `${sanitizeSessionName(name)}.md`), content);
|
|
1881
2110
|
}
|
|
1882
2111
|
async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment) {
|
|
1883
2112
|
const data = await readSessionFile(dir, sessionName);
|
|
@@ -1889,7 +2118,7 @@ async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment)
|
|
|
1889
2118
|
delete data.overrides[key];
|
|
1890
2119
|
}
|
|
1891
2120
|
const content = buildSessionContent({ ...data });
|
|
1892
|
-
await writeFileForce(
|
|
2121
|
+
await writeFileForce(resolve7(dir, `${sanitizeSessionName(sessionName)}.md`), content);
|
|
1893
2122
|
}
|
|
1894
2123
|
async function registerAutoSession(dir, rawName, opts) {
|
|
1895
2124
|
const name = sanitizeSessionName(rawName);
|
|
@@ -1906,7 +2135,7 @@ async function registerAutoSession(dir, rawName, opts) {
|
|
|
1906
2135
|
ports: opts.ports,
|
|
1907
2136
|
cwd: opts.cwd
|
|
1908
2137
|
});
|
|
1909
|
-
await writeFileForce(
|
|
2138
|
+
await writeFileForce(resolve7(dir, `${name}.md`), content);
|
|
1910
2139
|
return name;
|
|
1911
2140
|
}
|
|
1912
2141
|
var init_servers = __esm({
|
|
@@ -1938,8 +2167,8 @@ __export(scanner_exports, {
|
|
|
1938
2167
|
});
|
|
1939
2168
|
import { execFile } from "child_process";
|
|
1940
2169
|
import { promisify } from "util";
|
|
1941
|
-
import { resolve as
|
|
1942
|
-
import { realpath, readdir as
|
|
2170
|
+
import { resolve as resolve8 } from "path";
|
|
2171
|
+
import { realpath, readdir as readdir5, readFile as readFile7 } from "fs/promises";
|
|
1943
2172
|
function clearScanCache() {
|
|
1944
2173
|
cache = null;
|
|
1945
2174
|
}
|
|
@@ -2034,8 +2263,8 @@ async function getGitInfo(cwd) {
|
|
|
2034
2263
|
let isWorktree = false;
|
|
2035
2264
|
if (commonDir && gitDir && commonDir !== gitDir) {
|
|
2036
2265
|
try {
|
|
2037
|
-
const resolvedCommon = await realpath(
|
|
2038
|
-
const resolvedGit = await realpath(
|
|
2266
|
+
const resolvedCommon = await realpath(resolve8(cwd, commonDir));
|
|
2267
|
+
const resolvedGit = await realpath(resolve8(cwd, gitDir));
|
|
2039
2268
|
isWorktree = resolvedCommon !== resolvedGit;
|
|
2040
2269
|
} catch {
|
|
2041
2270
|
isWorktree = false;
|
|
@@ -2048,17 +2277,17 @@ async function loadWorkspaceRecords(projectsDir, assignmentsDir) {
|
|
|
2048
2277
|
try {
|
|
2049
2278
|
const projects = await listProjects(projectsDir);
|
|
2050
2279
|
for (const project of projects) {
|
|
2051
|
-
const projectAssignmentsDir =
|
|
2280
|
+
const projectAssignmentsDir = resolve8(projectsDir, project.slug, "assignments");
|
|
2052
2281
|
let slugs;
|
|
2053
2282
|
try {
|
|
2054
|
-
slugs = await
|
|
2283
|
+
slugs = await readdir5(projectAssignmentsDir);
|
|
2055
2284
|
} catch {
|
|
2056
2285
|
continue;
|
|
2057
2286
|
}
|
|
2058
2287
|
for (const aslug of slugs) {
|
|
2059
|
-
const aFile =
|
|
2288
|
+
const aFile = resolve8(projectAssignmentsDir, aslug, "assignment.md");
|
|
2060
2289
|
try {
|
|
2061
|
-
const raw = await
|
|
2290
|
+
const raw = await readFile7(aFile, "utf-8");
|
|
2062
2291
|
const [fm] = extractFrontmatter2(raw);
|
|
2063
2292
|
if (!fm) continue;
|
|
2064
2293
|
records.push({
|
|
@@ -2077,12 +2306,12 @@ async function loadWorkspaceRecords(projectsDir, assignmentsDir) {
|
|
|
2077
2306
|
}
|
|
2078
2307
|
if (assignmentsDir) {
|
|
2079
2308
|
try {
|
|
2080
|
-
const entries = await
|
|
2309
|
+
const entries = await readdir5(assignmentsDir);
|
|
2081
2310
|
for (const id of entries) {
|
|
2082
2311
|
if (id.startsWith(".") || id.startsWith("_")) continue;
|
|
2083
|
-
const aFile =
|
|
2312
|
+
const aFile = resolve8(assignmentsDir, id, "assignment.md");
|
|
2084
2313
|
try {
|
|
2085
|
-
const raw = await
|
|
2314
|
+
const raw = await readFile7(aFile, "utf-8");
|
|
2086
2315
|
const [fm] = extractFrontmatter2(raw);
|
|
2087
2316
|
if (!fm) continue;
|
|
2088
2317
|
records.push({
|
|
@@ -2330,20 +2559,20 @@ var init_scanner = __esm({
|
|
|
2330
2559
|
});
|
|
2331
2560
|
|
|
2332
2561
|
// src/dashboard/api.ts
|
|
2333
|
-
import { readdir as
|
|
2334
|
-
import { resolve as
|
|
2562
|
+
import { readdir as readdir6, readFile as readFile8, writeFile as writeFile3 } from "fs/promises";
|
|
2563
|
+
import { resolve as resolve9, dirname as dirname2 } from "path";
|
|
2335
2564
|
async function listStandaloneRecords(assignmentsDir) {
|
|
2336
2565
|
if (!assignmentsDir) return [];
|
|
2337
2566
|
if (!await fileExists(assignmentsDir)) return [];
|
|
2338
|
-
const entries = await
|
|
2567
|
+
const entries = await readdir6(assignmentsDir, { withFileTypes: true });
|
|
2339
2568
|
const records = [];
|
|
2340
2569
|
for (const entry of entries) {
|
|
2341
2570
|
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
|
|
2342
|
-
const assignmentDir =
|
|
2343
|
-
const assignmentMdPath =
|
|
2571
|
+
const assignmentDir = resolve9(assignmentsDir, entry.name);
|
|
2572
|
+
const assignmentMdPath = resolve9(assignmentDir, "assignment.md");
|
|
2344
2573
|
if (!await fileExists(assignmentMdPath)) continue;
|
|
2345
2574
|
try {
|
|
2346
|
-
const content = await
|
|
2575
|
+
const content = await readFile8(assignmentMdPath, "utf-8");
|
|
2347
2576
|
const record = parseAssignmentFull(content);
|
|
2348
2577
|
records.push({ assignmentDir, id: entry.name, record });
|
|
2349
2578
|
} catch {
|
|
@@ -2412,9 +2641,9 @@ async function listProjects(projectsDir) {
|
|
|
2412
2641
|
return projectRecords.map((record) => record.summary);
|
|
2413
2642
|
}
|
|
2414
2643
|
async function readWorkspaceRegistry(projectsDir) {
|
|
2415
|
-
const registryPath =
|
|
2644
|
+
const registryPath = resolve9(dirname2(projectsDir), "workspaces.json");
|
|
2416
2645
|
try {
|
|
2417
|
-
const raw = await
|
|
2646
|
+
const raw = await readFile8(registryPath, "utf-8");
|
|
2418
2647
|
const parsed = JSON.parse(raw);
|
|
2419
2648
|
return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
|
|
2420
2649
|
} catch {
|
|
@@ -2422,13 +2651,14 @@ async function readWorkspaceRegistry(projectsDir) {
|
|
|
2422
2651
|
}
|
|
2423
2652
|
}
|
|
2424
2653
|
async function writeWorkspaceRegistry(projectsDir, workspaces) {
|
|
2425
|
-
const registryPath =
|
|
2654
|
+
const registryPath = resolve9(dirname2(projectsDir), "workspaces.json");
|
|
2426
2655
|
await writeFile3(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
|
|
2427
2656
|
}
|
|
2428
|
-
async function listWorkspaces(projectsDir) {
|
|
2429
|
-
const [projectRecords, registered] = await Promise.all([
|
|
2657
|
+
async function listWorkspaces(projectsDir, assignmentsDir) {
|
|
2658
|
+
const [projectRecords, registered, standaloneRecords] = await Promise.all([
|
|
2430
2659
|
listProjectRecords(projectsDir),
|
|
2431
|
-
readWorkspaceRegistry(projectsDir)
|
|
2660
|
+
readWorkspaceRegistry(projectsDir),
|
|
2661
|
+
listStandaloneRecords(assignmentsDir)
|
|
2432
2662
|
]);
|
|
2433
2663
|
const workspaceSet = new Set(registered);
|
|
2434
2664
|
let hasUngrouped = false;
|
|
@@ -2439,6 +2669,13 @@ async function listWorkspaces(projectsDir) {
|
|
|
2439
2669
|
hasUngrouped = true;
|
|
2440
2670
|
}
|
|
2441
2671
|
}
|
|
2672
|
+
for (const sr of standaloneRecords) {
|
|
2673
|
+
if (sr.record.workspaceGroup) {
|
|
2674
|
+
workspaceSet.add(sr.record.workspaceGroup);
|
|
2675
|
+
} else {
|
|
2676
|
+
hasUngrouped = true;
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2442
2679
|
const workspaces = Array.from(workspaceSet).sort();
|
|
2443
2680
|
return { workspaces, hasUngrouped };
|
|
2444
2681
|
}
|
|
@@ -2587,7 +2824,7 @@ async function toStandaloneBoardItem(sr) {
|
|
|
2587
2824
|
projectSlug: null,
|
|
2588
2825
|
projectTitle: null,
|
|
2589
2826
|
blockedReason: sr.record.blockedReason,
|
|
2590
|
-
projectWorkspace: null,
|
|
2827
|
+
projectWorkspace: sr.record.workspaceGroup ?? null,
|
|
2591
2828
|
availableTransitions: await getStandaloneAvailableTransitions(sr.record)
|
|
2592
2829
|
};
|
|
2593
2830
|
}
|
|
@@ -2622,7 +2859,7 @@ async function getEditableDocument(projectsDir, documentType, projectSlug, assig
|
|
|
2622
2859
|
if (!filePath || !await fileExists(filePath)) {
|
|
2623
2860
|
return null;
|
|
2624
2861
|
}
|
|
2625
|
-
const content = await
|
|
2862
|
+
const content = await readFile8(filePath, "utf-8");
|
|
2626
2863
|
const title = getEditableDocumentTitle(documentType, projectSlug, assignmentSlug);
|
|
2627
2864
|
return {
|
|
2628
2865
|
documentType,
|
|
@@ -2646,9 +2883,9 @@ async function getEditableDocumentById(projectsDir, assignmentsDir, documentType
|
|
|
2646
2883
|
}
|
|
2647
2884
|
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;
|
|
2648
2885
|
if (!fileName) return null;
|
|
2649
|
-
const filePath =
|
|
2886
|
+
const filePath = resolve9(resolved.assignmentDir, fileName);
|
|
2650
2887
|
if (!await fileExists(filePath)) return null;
|
|
2651
|
-
const content = await
|
|
2888
|
+
const content = await readFile8(filePath, "utf-8");
|
|
2652
2889
|
const label = resolved.id;
|
|
2653
2890
|
const title = documentType === "assignment" ? `Edit Assignment: ${label}` : documentType === "plan" ? `Edit Plan: ${label}` : documentType === "scratchpad" ? `Edit Scratchpad: ${label}` : documentType === "handoff" ? `Append Handoff: ${label}` : `Append Decision: ${label}`;
|
|
2654
2891
|
return {
|
|
@@ -2662,12 +2899,12 @@ async function getEditableDocumentById(projectsDir, assignmentsDir, documentType
|
|
|
2662
2899
|
};
|
|
2663
2900
|
}
|
|
2664
2901
|
async function getProjectDetail(projectsDir, slug) {
|
|
2665
|
-
const projectPath =
|
|
2666
|
-
const projectMdPath =
|
|
2902
|
+
const projectPath = resolve9(projectsDir, slug);
|
|
2903
|
+
const projectMdPath = resolve9(projectPath, "project.md");
|
|
2667
2904
|
if (!await fileExists(projectMdPath)) {
|
|
2668
2905
|
return null;
|
|
2669
2906
|
}
|
|
2670
|
-
const projectContent = await
|
|
2907
|
+
const projectContent = await readFile8(projectMdPath, "utf-8");
|
|
2671
2908
|
const project = parseProject(projectContent);
|
|
2672
2909
|
const assignments = await listAssignmentRecords(projectPath);
|
|
2673
2910
|
const rollup = await buildProjectRollup(projectPath, project, assignments);
|
|
@@ -2697,17 +2934,17 @@ async function getProjectDetail(projectsDir, slug) {
|
|
|
2697
2934
|
};
|
|
2698
2935
|
}
|
|
2699
2936
|
async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
2700
|
-
const assignmentDir =
|
|
2701
|
-
const assignmentMdPath =
|
|
2937
|
+
const assignmentDir = resolve9(projectsDir, projectSlug, "assignments", assignmentSlug);
|
|
2938
|
+
const assignmentMdPath = resolve9(assignmentDir, "assignment.md");
|
|
2702
2939
|
if (!await fileExists(assignmentMdPath)) {
|
|
2703
2940
|
return null;
|
|
2704
2941
|
}
|
|
2705
|
-
const assignmentContent = await
|
|
2942
|
+
const assignmentContent = await readFile8(assignmentMdPath, "utf-8");
|
|
2706
2943
|
const assignment = parseAssignmentFull(assignmentContent);
|
|
2707
2944
|
let plan = null;
|
|
2708
|
-
const planPath =
|
|
2945
|
+
const planPath = resolve9(assignmentDir, "plan.md");
|
|
2709
2946
|
if (await fileExists(planPath)) {
|
|
2710
|
-
const planContent = await
|
|
2947
|
+
const planContent = await readFile8(planPath, "utf-8");
|
|
2711
2948
|
const parsed = parsePlan(planContent);
|
|
2712
2949
|
plan = {
|
|
2713
2950
|
status: parsed.status,
|
|
@@ -2716,9 +2953,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2716
2953
|
};
|
|
2717
2954
|
}
|
|
2718
2955
|
let scratchpad = null;
|
|
2719
|
-
const scratchpadPath =
|
|
2956
|
+
const scratchpadPath = resolve9(assignmentDir, "scratchpad.md");
|
|
2720
2957
|
if (await fileExists(scratchpadPath)) {
|
|
2721
|
-
const scratchpadContent = await
|
|
2958
|
+
const scratchpadContent = await readFile8(scratchpadPath, "utf-8");
|
|
2722
2959
|
const parsed = parseScratchpad(scratchpadContent);
|
|
2723
2960
|
scratchpad = {
|
|
2724
2961
|
updated: parsed.updated,
|
|
@@ -2726,9 +2963,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2726
2963
|
};
|
|
2727
2964
|
}
|
|
2728
2965
|
let handoff = null;
|
|
2729
|
-
const handoffPath =
|
|
2966
|
+
const handoffPath = resolve9(assignmentDir, "handoff.md");
|
|
2730
2967
|
if (await fileExists(handoffPath)) {
|
|
2731
|
-
const handoffContent = await
|
|
2968
|
+
const handoffContent = await readFile8(handoffPath, "utf-8");
|
|
2732
2969
|
const parsed = parseHandoff(handoffContent);
|
|
2733
2970
|
handoff = {
|
|
2734
2971
|
updated: parsed.updated,
|
|
@@ -2737,9 +2974,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2737
2974
|
};
|
|
2738
2975
|
}
|
|
2739
2976
|
let decisionRecord = null;
|
|
2740
|
-
const decisionRecordPath =
|
|
2977
|
+
const decisionRecordPath = resolve9(assignmentDir, "decision-record.md");
|
|
2741
2978
|
if (await fileExists(decisionRecordPath)) {
|
|
2742
|
-
const decisionRecordContent = await
|
|
2979
|
+
const decisionRecordContent = await readFile8(decisionRecordPath, "utf-8");
|
|
2743
2980
|
const parsed = parseDecisionRecord(decisionRecordContent);
|
|
2744
2981
|
decisionRecord = {
|
|
2745
2982
|
updated: parsed.updated,
|
|
@@ -2748,9 +2985,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2748
2985
|
};
|
|
2749
2986
|
}
|
|
2750
2987
|
let progress = null;
|
|
2751
|
-
const progressPath =
|
|
2988
|
+
const progressPath = resolve9(assignmentDir, "progress.md");
|
|
2752
2989
|
if (await fileExists(progressPath)) {
|
|
2753
|
-
const progressContent = await
|
|
2990
|
+
const progressContent = await readFile8(progressPath, "utf-8");
|
|
2754
2991
|
const parsed = parseProgress(progressContent);
|
|
2755
2992
|
progress = {
|
|
2756
2993
|
updated: parsed.updated,
|
|
@@ -2759,9 +2996,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2759
2996
|
};
|
|
2760
2997
|
}
|
|
2761
2998
|
let comments = null;
|
|
2762
|
-
const commentsPath =
|
|
2999
|
+
const commentsPath = resolve9(assignmentDir, "comments.md");
|
|
2763
3000
|
if (await fileExists(commentsPath)) {
|
|
2764
|
-
const commentsContent = await
|
|
3001
|
+
const commentsContent = await readFile8(commentsPath, "utf-8");
|
|
2765
3002
|
const parsed = parseComments(commentsContent);
|
|
2766
3003
|
comments = {
|
|
2767
3004
|
updated: parsed.updated,
|
|
@@ -2875,7 +3112,7 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
|
|
|
2875
3112
|
slug: a.slug,
|
|
2876
3113
|
title: a.title,
|
|
2877
3114
|
projectSlug: rec.summary.slug,
|
|
2878
|
-
assignmentDir:
|
|
3115
|
+
assignmentDir: resolve9(rec.projectPath, "assignments", a.slug)
|
|
2879
3116
|
});
|
|
2880
3117
|
}
|
|
2881
3118
|
}
|
|
@@ -2908,17 +3145,17 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
|
|
|
2908
3145
|
}
|
|
2909
3146
|
async function countMentionsInAssignment(sourceDir, target) {
|
|
2910
3147
|
const bodies = [];
|
|
2911
|
-
const assignmentMd =
|
|
3148
|
+
const assignmentMd = resolve9(sourceDir, "assignment.md");
|
|
2912
3149
|
if (await fileExists(assignmentMd)) {
|
|
2913
|
-
const content = await
|
|
3150
|
+
const content = await readFile8(assignmentMd, "utf-8");
|
|
2914
3151
|
const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
|
|
2915
3152
|
if (todosMatch) bodies.push(todosMatch[1]);
|
|
2916
3153
|
}
|
|
2917
3154
|
for (const filename of ["progress.md", "comments.md", "handoff.md"]) {
|
|
2918
|
-
const path =
|
|
3155
|
+
const path = resolve9(sourceDir, filename);
|
|
2919
3156
|
if (await fileExists(path)) {
|
|
2920
3157
|
try {
|
|
2921
|
-
bodies.push(await
|
|
3158
|
+
bodies.push(await readFile8(path, "utf-8"));
|
|
2922
3159
|
} catch {
|
|
2923
3160
|
}
|
|
2924
3161
|
}
|
|
@@ -2976,44 +3213,44 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir, id) {
|
|
|
2976
3213
|
}
|
|
2977
3214
|
async function buildStandaloneAssignmentDetail(resolved) {
|
|
2978
3215
|
const assignmentDir = resolved.assignmentDir;
|
|
2979
|
-
const assignmentMdPath =
|
|
3216
|
+
const assignmentMdPath = resolve9(assignmentDir, "assignment.md");
|
|
2980
3217
|
if (!await fileExists(assignmentMdPath)) return null;
|
|
2981
|
-
const assignmentContent = await
|
|
3218
|
+
const assignmentContent = await readFile8(assignmentMdPath, "utf-8");
|
|
2982
3219
|
const assignment = parseAssignmentFull(assignmentContent);
|
|
2983
3220
|
let plan = null;
|
|
2984
|
-
const planPath =
|
|
3221
|
+
const planPath = resolve9(assignmentDir, "plan.md");
|
|
2985
3222
|
if (await fileExists(planPath)) {
|
|
2986
|
-
const parsed = parsePlan(await
|
|
3223
|
+
const parsed = parsePlan(await readFile8(planPath, "utf-8"));
|
|
2987
3224
|
plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
|
|
2988
3225
|
}
|
|
2989
3226
|
let scratchpad = null;
|
|
2990
|
-
const scratchpadPath =
|
|
3227
|
+
const scratchpadPath = resolve9(assignmentDir, "scratchpad.md");
|
|
2991
3228
|
if (await fileExists(scratchpadPath)) {
|
|
2992
|
-
const parsed = parseScratchpad(await
|
|
3229
|
+
const parsed = parseScratchpad(await readFile8(scratchpadPath, "utf-8"));
|
|
2993
3230
|
scratchpad = { updated: parsed.updated, body: parsed.body };
|
|
2994
3231
|
}
|
|
2995
3232
|
let handoff = null;
|
|
2996
|
-
const handoffPath =
|
|
3233
|
+
const handoffPath = resolve9(assignmentDir, "handoff.md");
|
|
2997
3234
|
if (await fileExists(handoffPath)) {
|
|
2998
|
-
const parsed = parseHandoff(await
|
|
3235
|
+
const parsed = parseHandoff(await readFile8(handoffPath, "utf-8"));
|
|
2999
3236
|
handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
|
|
3000
3237
|
}
|
|
3001
3238
|
let decisionRecord = null;
|
|
3002
|
-
const decisionRecordPath =
|
|
3239
|
+
const decisionRecordPath = resolve9(assignmentDir, "decision-record.md");
|
|
3003
3240
|
if (await fileExists(decisionRecordPath)) {
|
|
3004
|
-
const parsed = parseDecisionRecord(await
|
|
3241
|
+
const parsed = parseDecisionRecord(await readFile8(decisionRecordPath, "utf-8"));
|
|
3005
3242
|
decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
|
|
3006
3243
|
}
|
|
3007
3244
|
let progress = null;
|
|
3008
|
-
const progressPath =
|
|
3245
|
+
const progressPath = resolve9(assignmentDir, "progress.md");
|
|
3009
3246
|
if (await fileExists(progressPath)) {
|
|
3010
|
-
const parsed = parseProgress(await
|
|
3247
|
+
const parsed = parseProgress(await readFile8(progressPath, "utf-8"));
|
|
3011
3248
|
progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
|
|
3012
3249
|
}
|
|
3013
3250
|
let comments = null;
|
|
3014
|
-
const commentsPath =
|
|
3251
|
+
const commentsPath = resolve9(assignmentDir, "comments.md");
|
|
3015
3252
|
if (await fileExists(commentsPath)) {
|
|
3016
|
-
const parsed = parseComments(await
|
|
3253
|
+
const parsed = parseComments(await readFile8(commentsPath, "utf-8"));
|
|
3017
3254
|
comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
|
|
3018
3255
|
}
|
|
3019
3256
|
const detail = {
|
|
@@ -3055,16 +3292,16 @@ async function listProjectRecords(projectsDir) {
|
|
|
3055
3292
|
migratedProjectsDirs.add(projectsDir);
|
|
3056
3293
|
await migrateLegacyProjectFiles(projectsDir);
|
|
3057
3294
|
}
|
|
3058
|
-
const entries = await
|
|
3295
|
+
const entries = await readdir6(projectsDir, { withFileTypes: true });
|
|
3059
3296
|
const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
|
|
3060
3297
|
const records = [];
|
|
3061
3298
|
for (const entry of projectDirs) {
|
|
3062
|
-
const projectPath =
|
|
3063
|
-
const projectMdPath =
|
|
3299
|
+
const projectPath = resolve9(projectsDir, entry.name);
|
|
3300
|
+
const projectMdPath = resolve9(projectPath, "project.md");
|
|
3064
3301
|
if (!await fileExists(projectMdPath)) {
|
|
3065
3302
|
continue;
|
|
3066
3303
|
}
|
|
3067
|
-
const projectContent = await
|
|
3304
|
+
const projectContent = await readFile8(projectMdPath, "utf-8");
|
|
3068
3305
|
const project = parseProject(projectContent);
|
|
3069
3306
|
const assignments = await listAssignmentRecords(projectPath);
|
|
3070
3307
|
const rollup = await buildProjectRollup(projectPath, project, assignments);
|
|
@@ -3095,39 +3332,39 @@ async function listProjectRecords(projectsDir) {
|
|
|
3095
3332
|
return records;
|
|
3096
3333
|
}
|
|
3097
3334
|
async function listAssignmentRecords(projectPath) {
|
|
3098
|
-
const assignmentsDir =
|
|
3335
|
+
const assignmentsDir = resolve9(projectPath, "assignments");
|
|
3099
3336
|
if (!await fileExists(assignmentsDir)) {
|
|
3100
3337
|
return [];
|
|
3101
3338
|
}
|
|
3102
|
-
const entries = await
|
|
3339
|
+
const entries = await readdir6(assignmentsDir, { withFileTypes: true });
|
|
3103
3340
|
const records = [];
|
|
3104
3341
|
for (const entry of entries) {
|
|
3105
3342
|
if (!entry.isDirectory()) {
|
|
3106
3343
|
continue;
|
|
3107
3344
|
}
|
|
3108
|
-
const assignmentMd =
|
|
3345
|
+
const assignmentMd = resolve9(assignmentsDir, entry.name, "assignment.md");
|
|
3109
3346
|
if (!await fileExists(assignmentMd)) {
|
|
3110
3347
|
continue;
|
|
3111
3348
|
}
|
|
3112
|
-
const content = await
|
|
3349
|
+
const content = await readFile8(assignmentMd, "utf-8");
|
|
3113
3350
|
records.push(parseAssignmentFull(content));
|
|
3114
3351
|
}
|
|
3115
3352
|
records.sort((left, right) => compareTimestamps(right.updated, left.updated));
|
|
3116
3353
|
return records;
|
|
3117
3354
|
}
|
|
3118
3355
|
async function listResources(projectPath) {
|
|
3119
|
-
const resourcesDir =
|
|
3356
|
+
const resourcesDir = resolve9(projectPath, "resources");
|
|
3120
3357
|
if (!await fileExists(resourcesDir)) {
|
|
3121
3358
|
return [];
|
|
3122
3359
|
}
|
|
3123
|
-
const entries = await
|
|
3360
|
+
const entries = await readdir6(resourcesDir, { withFileTypes: true });
|
|
3124
3361
|
const results = [];
|
|
3125
3362
|
for (const entry of entries) {
|
|
3126
3363
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
|
|
3127
3364
|
continue;
|
|
3128
3365
|
}
|
|
3129
|
-
const filePath =
|
|
3130
|
-
const content = await
|
|
3366
|
+
const filePath = resolve9(resourcesDir, entry.name);
|
|
3367
|
+
const content = await readFile8(filePath, "utf-8");
|
|
3131
3368
|
const parsed = parseResource(content);
|
|
3132
3369
|
results.push({
|
|
3133
3370
|
name: parsed.name,
|
|
@@ -3142,18 +3379,18 @@ async function listResources(projectPath) {
|
|
|
3142
3379
|
return results;
|
|
3143
3380
|
}
|
|
3144
3381
|
async function listMemories(projectPath) {
|
|
3145
|
-
const memoriesDir =
|
|
3382
|
+
const memoriesDir = resolve9(projectPath, "memories");
|
|
3146
3383
|
if (!await fileExists(memoriesDir)) {
|
|
3147
3384
|
return [];
|
|
3148
3385
|
}
|
|
3149
|
-
const entries = await
|
|
3386
|
+
const entries = await readdir6(memoriesDir, { withFileTypes: true });
|
|
3150
3387
|
const results = [];
|
|
3151
3388
|
for (const entry of entries) {
|
|
3152
3389
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
|
|
3153
3390
|
continue;
|
|
3154
3391
|
}
|
|
3155
|
-
const filePath =
|
|
3156
|
-
const content = await
|
|
3392
|
+
const filePath = resolve9(memoriesDir, entry.name);
|
|
3393
|
+
const content = await readFile8(filePath, "utf-8");
|
|
3157
3394
|
const parsed = parseMemory(content);
|
|
3158
3395
|
results.push({
|
|
3159
3396
|
name: parsed.name,
|
|
@@ -3168,9 +3405,9 @@ async function listMemories(projectPath) {
|
|
|
3168
3405
|
return results;
|
|
3169
3406
|
}
|
|
3170
3407
|
async function loadDependencyGraph(projectPath, assignments) {
|
|
3171
|
-
const statusPath =
|
|
3408
|
+
const statusPath = resolve9(projectPath, "_status.md");
|
|
3172
3409
|
if (await fileExists(statusPath)) {
|
|
3173
|
-
const statusContent = await
|
|
3410
|
+
const statusContent = await readFile8(statusPath, "utf-8");
|
|
3174
3411
|
const parsed = parseStatus(statusContent);
|
|
3175
3412
|
const derivedGraph = extractMermaidGraph(parsed.body);
|
|
3176
3413
|
if (derivedGraph) {
|
|
@@ -3270,7 +3507,7 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
|
|
|
3270
3507
|
const config = await getStatusConfig();
|
|
3271
3508
|
const transitionDefs = getTransitionDefinitions(config);
|
|
3272
3509
|
const actions = [];
|
|
3273
|
-
const projectPath =
|
|
3510
|
+
const projectPath = resolve9(projectsDir, projectSlug);
|
|
3274
3511
|
for (const definition of transitionDefs) {
|
|
3275
3512
|
let warning = null;
|
|
3276
3513
|
if (definition.command === "start" && !assignment.assignee) {
|
|
@@ -3300,12 +3537,12 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses) {
|
|
|
3300
3537
|
const terminals = terminalStatuses ?? /* @__PURE__ */ new Set(["completed"]);
|
|
3301
3538
|
const unmet = [];
|
|
3302
3539
|
for (const dependency of dependsOn) {
|
|
3303
|
-
const dependencyPath =
|
|
3540
|
+
const dependencyPath = resolve9(projectPath, "assignments", dependency, "assignment.md");
|
|
3304
3541
|
if (!await fileExists(dependencyPath)) {
|
|
3305
3542
|
unmet.push(`${dependency} (missing)`);
|
|
3306
3543
|
continue;
|
|
3307
3544
|
}
|
|
3308
|
-
const content = await
|
|
3545
|
+
const content = await readFile8(dependencyPath, "utf-8");
|
|
3309
3546
|
const parsed = parseAssignmentFull(content);
|
|
3310
3547
|
if (!terminals.has(parsed.status)) {
|
|
3311
3548
|
unmet.push(`${dependency} (${parsed.status})`);
|
|
@@ -3460,7 +3697,7 @@ function isStale(updated) {
|
|
|
3460
3697
|
return Date.now() - timestamp > STALE_ASSIGNMENT_MS;
|
|
3461
3698
|
}
|
|
3462
3699
|
async function countOpenQuestions(projectPath, assignmentSlug) {
|
|
3463
|
-
const commentsPath =
|
|
3700
|
+
const commentsPath = resolve9(
|
|
3464
3701
|
projectPath,
|
|
3465
3702
|
"assignments",
|
|
3466
3703
|
assignmentSlug,
|
|
@@ -3470,7 +3707,7 @@ async function countOpenQuestions(projectPath, assignmentSlug) {
|
|
|
3470
3707
|
return 0;
|
|
3471
3708
|
}
|
|
3472
3709
|
try {
|
|
3473
|
-
const content = await
|
|
3710
|
+
const content = await readFile8(commentsPath, "utf-8");
|
|
3474
3711
|
const parsed = parseComments(content);
|
|
3475
3712
|
return parsed.entries.filter(
|
|
3476
3713
|
(e) => e.type === "question" && e.resolved !== true
|
|
@@ -3491,17 +3728,17 @@ function getProjectActivityTimestamp(projectUpdated, assignments) {
|
|
|
3491
3728
|
function getDocumentPath(projectsDir, documentType, projectSlug, assignmentSlug) {
|
|
3492
3729
|
switch (documentType) {
|
|
3493
3730
|
case "project":
|
|
3494
|
-
return
|
|
3731
|
+
return resolve9(projectsDir, projectSlug, "project.md");
|
|
3495
3732
|
case "assignment":
|
|
3496
|
-
return assignmentSlug ?
|
|
3733
|
+
return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
|
|
3497
3734
|
case "plan":
|
|
3498
|
-
return assignmentSlug ?
|
|
3735
|
+
return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
|
|
3499
3736
|
case "scratchpad":
|
|
3500
|
-
return assignmentSlug ?
|
|
3737
|
+
return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
|
|
3501
3738
|
case "handoff":
|
|
3502
|
-
return assignmentSlug ?
|
|
3739
|
+
return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
|
|
3503
3740
|
case "decision-record":
|
|
3504
|
-
return assignmentSlug ?
|
|
3741
|
+
return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
|
|
3505
3742
|
default:
|
|
3506
3743
|
return null;
|
|
3507
3744
|
}
|
|
@@ -3528,12 +3765,14 @@ function getEditableDocumentTitle(documentType, projectSlug, assignmentSlug) {
|
|
|
3528
3765
|
}
|
|
3529
3766
|
async function listPlaybooks(playbooksDir2) {
|
|
3530
3767
|
if (!await fileExists(playbooksDir2)) return [];
|
|
3531
|
-
const
|
|
3768
|
+
const config = await readConfig();
|
|
3769
|
+
const disabledSet = new Set(config.playbooks.disabled);
|
|
3770
|
+
const entries = await readdir6(playbooksDir2, { withFileTypes: true });
|
|
3532
3771
|
const playbooks = [];
|
|
3533
3772
|
for (const entry of entries) {
|
|
3534
3773
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
|
|
3535
|
-
const filePath =
|
|
3536
|
-
const raw = await
|
|
3774
|
+
const filePath = resolve9(playbooksDir2, entry.name);
|
|
3775
|
+
const raw = await readFile8(filePath, "utf-8");
|
|
3537
3776
|
const parsed = parsePlaybook(raw);
|
|
3538
3777
|
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
3539
3778
|
playbooks.push({
|
|
@@ -3543,25 +3782,28 @@ async function listPlaybooks(playbooksDir2) {
|
|
|
3543
3782
|
whenToUse: parsed.whenToUse,
|
|
3544
3783
|
tags: parsed.tags,
|
|
3545
3784
|
created: parsed.created,
|
|
3546
|
-
updated: parsed.updated
|
|
3785
|
+
updated: parsed.updated,
|
|
3786
|
+
enabled: !disabledSet.has(slug)
|
|
3547
3787
|
});
|
|
3548
3788
|
}
|
|
3549
3789
|
return playbooks.sort((a, b) => (b.updated || b.created).localeCompare(a.updated || a.created));
|
|
3550
3790
|
}
|
|
3551
3791
|
async function getPlaybookDetail(playbooksDir2, slug) {
|
|
3552
|
-
const
|
|
3553
|
-
if (!
|
|
3554
|
-
const
|
|
3555
|
-
const
|
|
3792
|
+
const resolved = await resolvePlaybookSlug(playbooksDir2, slug);
|
|
3793
|
+
if (!resolved) return null;
|
|
3794
|
+
const config = await readConfig();
|
|
3795
|
+
const enabled = !config.playbooks.disabled.includes(resolved.slug);
|
|
3796
|
+
const parsed = resolved.parsed;
|
|
3556
3797
|
return {
|
|
3557
|
-
slug:
|
|
3558
|
-
name: parsed.name || slug,
|
|
3798
|
+
slug: resolved.slug,
|
|
3799
|
+
name: parsed.name || resolved.slug,
|
|
3559
3800
|
description: parsed.description,
|
|
3560
3801
|
whenToUse: parsed.whenToUse,
|
|
3561
3802
|
tags: parsed.tags,
|
|
3562
3803
|
created: parsed.created,
|
|
3563
3804
|
updated: parsed.updated,
|
|
3564
|
-
body: parsed.body
|
|
3805
|
+
body: parsed.body,
|
|
3806
|
+
enabled
|
|
3565
3807
|
};
|
|
3566
3808
|
}
|
|
3567
3809
|
var STALE_ASSIGNMENT_MS, ATTENTION_PAGE_LIMIT, OVERVIEW_ATTENTION_LIMIT, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, DEFAULT_TRANSITION_DEFINITIONS, DEFAULT_STATUS_COLORS, _cachedConfig, REFERENCED_BY_LIMIT, migratedProjectsDirs, DEFAULT_GRAPH_COLORS;
|
|
@@ -3571,6 +3813,7 @@ var init_api = __esm({
|
|
|
3571
3813
|
init_lifecycle();
|
|
3572
3814
|
init_fs();
|
|
3573
3815
|
init_config2();
|
|
3816
|
+
init_playbooks();
|
|
3574
3817
|
init_fs_migration();
|
|
3575
3818
|
init_assignment_resolver();
|
|
3576
3819
|
init_parser();
|
|
@@ -3922,21 +4165,21 @@ init_api();
|
|
|
3922
4165
|
init_assignment_resolver();
|
|
3923
4166
|
import express from "express";
|
|
3924
4167
|
import { createServer } from "http";
|
|
3925
|
-
import { resolve as
|
|
4168
|
+
import { resolve as resolve18 } from "path";
|
|
3926
4169
|
import { writeFile as writeFile5, unlink as unlink4 } from "fs/promises";
|
|
3927
4170
|
import { WebSocketServer, WebSocket } from "ws";
|
|
3928
4171
|
|
|
3929
4172
|
// src/dashboard/agent-sessions.ts
|
|
3930
4173
|
init_fs();
|
|
3931
|
-
import { readFile as
|
|
3932
|
-
import { resolve as
|
|
4174
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
4175
|
+
import { resolve as resolve11 } from "path";
|
|
3933
4176
|
|
|
3934
4177
|
// src/dashboard/session-db.ts
|
|
3935
4178
|
init_paths();
|
|
3936
4179
|
init_fs();
|
|
3937
4180
|
import Database from "better-sqlite3";
|
|
3938
|
-
import { resolve as
|
|
3939
|
-
import { readdir as
|
|
4181
|
+
import { resolve as resolve10 } from "path";
|
|
4182
|
+
import { readdir as readdir7 } from "fs/promises";
|
|
3940
4183
|
var db = null;
|
|
3941
4184
|
var SCHEMA_VERSION = "3";
|
|
3942
4185
|
var SCHEMA_SQL = `
|
|
@@ -3963,7 +4206,7 @@ CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, ass
|
|
|
3963
4206
|
`;
|
|
3964
4207
|
function initSessionDb(dbPath) {
|
|
3965
4208
|
if (db) return db;
|
|
3966
|
-
const finalPath = dbPath ??
|
|
4209
|
+
const finalPath = dbPath ?? resolve10(syntaurRoot(), "syntaur.db");
|
|
3967
4210
|
db = new Database(finalPath);
|
|
3968
4211
|
db.pragma("journal_mode = WAL");
|
|
3969
4212
|
db.exec(SCHEMA_SQL);
|
|
@@ -4060,12 +4303,12 @@ async function migrateFromMarkdown(projectsDir) {
|
|
|
4060
4303
|
const count = database.prepare("SELECT COUNT(*) as count FROM sessions").get();
|
|
4061
4304
|
if (count.count > 0) return 0;
|
|
4062
4305
|
if (!await fileExists(projectsDir)) return 0;
|
|
4063
|
-
const entries = await
|
|
4306
|
+
const entries = await readdir7(projectsDir, { withFileTypes: true });
|
|
4064
4307
|
const allSessions = [];
|
|
4065
4308
|
for (const entry of entries) {
|
|
4066
4309
|
if (!entry.isDirectory()) continue;
|
|
4067
|
-
const projectDir =
|
|
4068
|
-
const indexPath =
|
|
4310
|
+
const projectDir = resolve10(projectsDir, entry.name);
|
|
4311
|
+
const indexPath = resolve10(projectDir, "_index-sessions.md");
|
|
4069
4312
|
if (!await fileExists(indexPath)) continue;
|
|
4070
4313
|
const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
|
|
4071
4314
|
allSessions.push(...sessions);
|
|
@@ -4085,8 +4328,8 @@ async function migrateFromMarkdown(projectsDir) {
|
|
|
4085
4328
|
return allSessions.length;
|
|
4086
4329
|
}
|
|
4087
4330
|
async function parseMarkdownSessionsIndex(filePath, projectSlug) {
|
|
4088
|
-
const { readFile:
|
|
4089
|
-
const raw = await
|
|
4331
|
+
const { readFile: readFile15 } = await import("fs/promises");
|
|
4332
|
+
const raw = await readFile15(filePath, "utf-8");
|
|
4090
4333
|
const sessions = [];
|
|
4091
4334
|
const lines = raw.split("\n");
|
|
4092
4335
|
let inTable = false;
|
|
@@ -4198,13 +4441,13 @@ async function deleteSessions(sessionIds) {
|
|
|
4198
4441
|
var DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
|
|
4199
4442
|
async function readAssignmentStatusFromPath(assignmentMdPath) {
|
|
4200
4443
|
if (!await fileExists(assignmentMdPath)) return null;
|
|
4201
|
-
const raw = await
|
|
4444
|
+
const raw = await readFile9(assignmentMdPath, "utf-8");
|
|
4202
4445
|
const match = raw.match(/^status:\s*(.+)$/m);
|
|
4203
4446
|
return match ? match[1].trim() : null;
|
|
4204
4447
|
}
|
|
4205
4448
|
async function readAssignmentStatus(projectDir, assignmentSlug) {
|
|
4206
4449
|
return readAssignmentStatusFromPath(
|
|
4207
|
-
|
|
4450
|
+
resolve11(projectDir, "assignments", assignmentSlug, "assignment.md")
|
|
4208
4451
|
);
|
|
4209
4452
|
}
|
|
4210
4453
|
async function reconcileActiveSessions(projectsDir, assignmentsDir) {
|
|
@@ -4222,13 +4465,13 @@ async function reconcileActiveSessions(projectsDir, assignmentsDir) {
|
|
|
4222
4465
|
seen.add(key);
|
|
4223
4466
|
if (session.project_slug) {
|
|
4224
4467
|
const status = await readAssignmentStatus(
|
|
4225
|
-
|
|
4468
|
+
resolve11(projectsDir, session.project_slug),
|
|
4226
4469
|
aslug
|
|
4227
4470
|
);
|
|
4228
4471
|
if (status) assignmentStatuses.set(key, status);
|
|
4229
4472
|
} else if (assignmentsDir) {
|
|
4230
4473
|
const status = await readAssignmentStatusFromPath(
|
|
4231
|
-
|
|
4474
|
+
resolve11(assignmentsDir, aslug, "assignment.md")
|
|
4232
4475
|
);
|
|
4233
4476
|
if (status) assignmentStatuses.set(key, status);
|
|
4234
4477
|
}
|
|
@@ -4273,18 +4516,25 @@ function createWatcher(options) {
|
|
|
4273
4516
|
if (parts.length === 0) return;
|
|
4274
4517
|
const projectSlug = parts[0];
|
|
4275
4518
|
let assignmentSlug;
|
|
4519
|
+
let isProjectTodos = false;
|
|
4276
4520
|
if (parts.length >= 3 && parts[1] === "assignments") {
|
|
4277
4521
|
assignmentSlug = parts[2];
|
|
4522
|
+
} else if (parts.length >= 2 && parts[1] === "todos") {
|
|
4523
|
+
isProjectTodos = true;
|
|
4278
4524
|
}
|
|
4279
|
-
const debounceKey = assignmentSlug ? `${projectSlug}/${assignmentSlug}` : projectSlug;
|
|
4525
|
+
const debounceKey = isProjectTodos ? `todos:${projectSlug}` : assignmentSlug ? `${projectSlug}/${assignmentSlug}` : projectSlug;
|
|
4280
4526
|
const existing = pendingEvents.get(debounceKey);
|
|
4281
4527
|
if (existing) clearTimeout(existing);
|
|
4282
|
-
const messageType = assignmentSlug ? "assignment-updated" : "project-updated";
|
|
4528
|
+
const messageType = isProjectTodos ? "todos-updated" : assignmentSlug ? "assignment-updated" : "project-updated";
|
|
4283
4529
|
pendingEvents.set(
|
|
4284
4530
|
debounceKey,
|
|
4285
4531
|
setTimeout(() => {
|
|
4286
4532
|
pendingEvents.delete(debounceKey);
|
|
4287
|
-
const message = {
|
|
4533
|
+
const message = isProjectTodos ? {
|
|
4534
|
+
type: "todos-updated",
|
|
4535
|
+
projectSlug,
|
|
4536
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4537
|
+
} : {
|
|
4288
4538
|
type: messageType,
|
|
4289
4539
|
projectSlug,
|
|
4290
4540
|
assignmentSlug,
|
|
@@ -4442,8 +4692,8 @@ init_config2();
|
|
|
4442
4692
|
// src/dashboard/api-write.ts
|
|
4443
4693
|
init_lifecycle();
|
|
4444
4694
|
import { Router } from "express";
|
|
4445
|
-
import { resolve as
|
|
4446
|
-
import { rm, readFile as
|
|
4695
|
+
import { resolve as resolve12 } from "path";
|
|
4696
|
+
import { rm, readFile as readFile10 } from "fs/promises";
|
|
4447
4697
|
|
|
4448
4698
|
// src/utils/slug.ts
|
|
4449
4699
|
function isValidSlug(slug) {
|
|
@@ -4514,14 +4764,14 @@ init_assignment_resolver();
|
|
|
4514
4764
|
init_config();
|
|
4515
4765
|
|
|
4516
4766
|
// src/templates/manifest.ts
|
|
4517
|
-
function renderManifest(
|
|
4767
|
+
function renderManifest(params2) {
|
|
4518
4768
|
return `---
|
|
4519
4769
|
version: "2.0"
|
|
4520
|
-
project: ${
|
|
4521
|
-
generated: "${
|
|
4770
|
+
project: ${params2.slug}
|
|
4771
|
+
generated: "${params2.timestamp}"
|
|
4522
4772
|
---
|
|
4523
4773
|
|
|
4524
|
-
# Project: ${
|
|
4774
|
+
# Project: ${params2.slug}
|
|
4525
4775
|
|
|
4526
4776
|
## Overview
|
|
4527
4777
|
- [Project Overview](./project.md)
|
|
@@ -4548,24 +4798,24 @@ function escapeYamlString(value) {
|
|
|
4548
4798
|
}
|
|
4549
4799
|
|
|
4550
4800
|
// src/templates/project.ts
|
|
4551
|
-
function renderProject(
|
|
4552
|
-
const safeTitle = escapeYamlString(
|
|
4553
|
-
const workspaceLine =
|
|
4554
|
-
workspace: ${
|
|
4801
|
+
function renderProject(params2) {
|
|
4802
|
+
const safeTitle = escapeYamlString(params2.title);
|
|
4803
|
+
const workspaceLine = params2.workspace ? `
|
|
4804
|
+
workspace: ${params2.workspace}` : "";
|
|
4555
4805
|
return `---
|
|
4556
|
-
id: ${
|
|
4557
|
-
slug: ${
|
|
4806
|
+
id: ${params2.id}
|
|
4807
|
+
slug: ${params2.slug}
|
|
4558
4808
|
title: ${safeTitle}
|
|
4559
4809
|
archived: false
|
|
4560
4810
|
archivedAt: null
|
|
4561
4811
|
archivedReason: null
|
|
4562
|
-
created: "${
|
|
4563
|
-
updated: "${
|
|
4812
|
+
created: "${params2.timestamp}"
|
|
4813
|
+
updated: "${params2.timestamp}"
|
|
4564
4814
|
externalIds: []
|
|
4565
4815
|
tags: []${workspaceLine}
|
|
4566
4816
|
---
|
|
4567
4817
|
|
|
4568
|
-
# ${
|
|
4818
|
+
# ${params2.title}
|
|
4569
4819
|
|
|
4570
4820
|
## Overview
|
|
4571
4821
|
|
|
@@ -4578,24 +4828,37 @@ tags: []${workspaceLine}
|
|
|
4578
4828
|
}
|
|
4579
4829
|
|
|
4580
4830
|
// src/templates/assignment.ts
|
|
4581
|
-
function renderAssignment(
|
|
4582
|
-
const safeTitle = escapeYamlString(
|
|
4583
|
-
const dependsOnYaml =
|
|
4584
|
-
- ${
|
|
4585
|
-
const linksYaml =
|
|
4586
|
-
- ${
|
|
4587
|
-
const projectYaml = `project: ${
|
|
4588
|
-
const
|
|
4831
|
+
function renderAssignment(params2) {
|
|
4832
|
+
const safeTitle = escapeYamlString(params2.title);
|
|
4833
|
+
const dependsOnYaml = params2.dependsOn.length === 0 ? "dependsOn: []" : `dependsOn:
|
|
4834
|
+
- ${params2.dependsOn.join("\n - ")}`;
|
|
4835
|
+
const linksYaml = params2.links.length === 0 ? "links: []" : `links:
|
|
4836
|
+
- ${params2.links.join("\n - ")}`;
|
|
4837
|
+
const projectYaml = `project: ${params2.project == null ? "null" : params2.project}`;
|
|
4838
|
+
const workspaceGroupLine = params2.workspaceGroup ? `
|
|
4839
|
+
workspaceGroup: ${params2.workspaceGroup}` : "";
|
|
4840
|
+
const typeYaml = `type: ${params2.type ?? "feature"}`;
|
|
4841
|
+
const todosSection = params2.includeTodos ? `## Todos
|
|
4842
|
+
|
|
4843
|
+
<!--
|
|
4844
|
+
Checklist of work items for this assignment. Items may be simple tasks
|
|
4845
|
+
or a markdown link to a plan file (e.g., "- [ ] Execute [plan](./plan.md)").
|
|
4846
|
+
When a plan is superseded by a new one, mark the old todo as:
|
|
4847
|
+
- [x] ~~Execute [old plan](./plan.md)~~ (superseded by plan-v2)
|
|
4848
|
+
Never delete superseded todos \u2014 preserve the history.
|
|
4849
|
+
-->
|
|
4850
|
+
|
|
4851
|
+
` : "";
|
|
4589
4852
|
return `---
|
|
4590
|
-
id: ${
|
|
4591
|
-
slug: ${
|
|
4853
|
+
id: ${params2.id}
|
|
4854
|
+
slug: ${params2.slug}
|
|
4592
4855
|
title: ${safeTitle}
|
|
4593
|
-
${projectYaml}
|
|
4856
|
+
${projectYaml}${workspaceGroupLine}
|
|
4594
4857
|
${typeYaml}
|
|
4595
4858
|
status: pending
|
|
4596
|
-
priority: ${
|
|
4597
|
-
created: "${
|
|
4598
|
-
updated: "${
|
|
4859
|
+
priority: ${params2.priority}
|
|
4860
|
+
created: "${params2.timestamp}"
|
|
4861
|
+
updated: "${params2.timestamp}"
|
|
4599
4862
|
assignee: null
|
|
4600
4863
|
externalIds: []
|
|
4601
4864
|
${dependsOnYaml}
|
|
@@ -4609,7 +4872,7 @@ workspace:
|
|
|
4609
4872
|
tags: []
|
|
4610
4873
|
---
|
|
4611
4874
|
|
|
4612
|
-
# ${
|
|
4875
|
+
# ${params2.title}
|
|
4613
4876
|
|
|
4614
4877
|
## Objective
|
|
4615
4878
|
|
|
@@ -4621,17 +4884,7 @@ tags: []
|
|
|
4621
4884
|
- [ ] <!-- criterion 2 -->
|
|
4622
4885
|
- [ ] <!-- criterion 3 -->
|
|
4623
4886
|
|
|
4624
|
-
##
|
|
4625
|
-
|
|
4626
|
-
<!--
|
|
4627
|
-
Checklist of work items for this assignment. Items may be simple tasks
|
|
4628
|
-
or a markdown link to a plan file (e.g., "- [ ] Execute [plan](./plan.md)").
|
|
4629
|
-
When a plan is superseded by a new one, mark the old todo as:
|
|
4630
|
-
- [x] ~~Execute [old plan](./plan.md)~~ (superseded by plan-v2)
|
|
4631
|
-
Never delete superseded todos \u2014 preserve the history.
|
|
4632
|
-
-->
|
|
4633
|
-
|
|
4634
|
-
## Context
|
|
4887
|
+
${todosSection}## Context
|
|
4635
4888
|
|
|
4636
4889
|
<!-- Links to relevant docs, code, or other assignments. -->
|
|
4637
4890
|
|
|
@@ -4646,10 +4899,10 @@ Never delete superseded todos \u2014 preserve the history.
|
|
|
4646
4899
|
}
|
|
4647
4900
|
|
|
4648
4901
|
// src/templates/scratchpad.ts
|
|
4649
|
-
function renderScratchpad(
|
|
4902
|
+
function renderScratchpad(params2) {
|
|
4650
4903
|
return `---
|
|
4651
|
-
assignment: ${
|
|
4652
|
-
updated: "${
|
|
4904
|
+
assignment: ${params2.assignmentSlug}
|
|
4905
|
+
updated: "${params2.timestamp}"
|
|
4653
4906
|
---
|
|
4654
4907
|
|
|
4655
4908
|
# Scratchpad
|
|
@@ -4659,10 +4912,10 @@ No working notes yet.
|
|
|
4659
4912
|
}
|
|
4660
4913
|
|
|
4661
4914
|
// src/templates/handoff.ts
|
|
4662
|
-
function renderHandoff(
|
|
4915
|
+
function renderHandoff(params2) {
|
|
4663
4916
|
return `---
|
|
4664
|
-
assignment: ${
|
|
4665
|
-
updated: "${
|
|
4917
|
+
assignment: ${params2.assignmentSlug}
|
|
4918
|
+
updated: "${params2.timestamp}"
|
|
4666
4919
|
handoffCount: 0
|
|
4667
4920
|
---
|
|
4668
4921
|
|
|
@@ -4673,12 +4926,12 @@ No handoffs recorded yet.
|
|
|
4673
4926
|
}
|
|
4674
4927
|
|
|
4675
4928
|
// src/templates/progress.ts
|
|
4676
|
-
function renderProgress(
|
|
4929
|
+
function renderProgress(params2) {
|
|
4677
4930
|
return `---
|
|
4678
|
-
assignment: ${
|
|
4931
|
+
assignment: ${params2.assignment}
|
|
4679
4932
|
entryCount: 0
|
|
4680
|
-
generated: "${
|
|
4681
|
-
updated: "${
|
|
4933
|
+
generated: "${params2.timestamp}"
|
|
4934
|
+
updated: "${params2.timestamp}"
|
|
4682
4935
|
---
|
|
4683
4936
|
|
|
4684
4937
|
# Progress
|
|
@@ -4688,12 +4941,12 @@ No progress yet.
|
|
|
4688
4941
|
}
|
|
4689
4942
|
|
|
4690
4943
|
// src/templates/comments.ts
|
|
4691
|
-
function renderComments(
|
|
4944
|
+
function renderComments(params2) {
|
|
4692
4945
|
return `---
|
|
4693
|
-
assignment: ${
|
|
4946
|
+
assignment: ${params2.assignment}
|
|
4694
4947
|
entryCount: 0
|
|
4695
|
-
generated: "${
|
|
4696
|
-
updated: "${
|
|
4948
|
+
generated: "${params2.timestamp}"
|
|
4949
|
+
updated: "${params2.timestamp}"
|
|
4697
4950
|
---
|
|
4698
4951
|
|
|
4699
4952
|
# Comments
|
|
@@ -4721,10 +4974,10 @@ function formatCommentEntry(comment) {
|
|
|
4721
4974
|
}
|
|
4722
4975
|
|
|
4723
4976
|
// src/templates/decision-record.ts
|
|
4724
|
-
function renderDecisionRecord(
|
|
4977
|
+
function renderDecisionRecord(params2) {
|
|
4725
4978
|
return `---
|
|
4726
|
-
assignment: ${
|
|
4727
|
-
updated: "${
|
|
4979
|
+
assignment: ${params2.assignmentSlug}
|
|
4980
|
+
updated: "${params2.timestamp}"
|
|
4728
4981
|
decisionCount: 0
|
|
4729
4982
|
---
|
|
4730
4983
|
|
|
@@ -4735,10 +4988,10 @@ No decisions recorded yet.
|
|
|
4735
4988
|
}
|
|
4736
4989
|
|
|
4737
4990
|
// src/templates/index-stubs.ts
|
|
4738
|
-
function renderIndexAssignments(
|
|
4991
|
+
function renderIndexAssignments(params2) {
|
|
4739
4992
|
return `---
|
|
4740
|
-
project: ${
|
|
4741
|
-
generated: "${
|
|
4993
|
+
project: ${params2.slug}
|
|
4994
|
+
generated: "${params2.timestamp}"
|
|
4742
4995
|
total: 0
|
|
4743
4996
|
by_status:
|
|
4744
4997
|
pending: 0
|
|
@@ -4755,10 +5008,10 @@ by_status:
|
|
|
4755
5008
|
|------|-------|--------|----------|----------|--------------|---------|
|
|
4756
5009
|
`;
|
|
4757
5010
|
}
|
|
4758
|
-
function renderIndexPlans(
|
|
5011
|
+
function renderIndexPlans(params2) {
|
|
4759
5012
|
return `---
|
|
4760
|
-
project: ${
|
|
4761
|
-
generated: "${
|
|
5013
|
+
project: ${params2.slug}
|
|
5014
|
+
generated: "${params2.timestamp}"
|
|
4762
5015
|
---
|
|
4763
5016
|
|
|
4764
5017
|
# Plans
|
|
@@ -4767,10 +5020,10 @@ generated: "${params.timestamp}"
|
|
|
4767
5020
|
|------------|-------------|---------|
|
|
4768
5021
|
`;
|
|
4769
5022
|
}
|
|
4770
|
-
function renderIndexDecisions(
|
|
5023
|
+
function renderIndexDecisions(params2) {
|
|
4771
5024
|
return `---
|
|
4772
|
-
project: ${
|
|
4773
|
-
generated: "${
|
|
5025
|
+
project: ${params2.slug}
|
|
5026
|
+
generated: "${params2.timestamp}"
|
|
4774
5027
|
---
|
|
4775
5028
|
|
|
4776
5029
|
# Decision Records
|
|
@@ -4779,10 +5032,10 @@ generated: "${params.timestamp}"
|
|
|
4779
5032
|
|------------|-------|-----------------|---------------|---------|
|
|
4780
5033
|
`;
|
|
4781
5034
|
}
|
|
4782
|
-
function renderStatus(
|
|
5035
|
+
function renderStatus(params2) {
|
|
4783
5036
|
return `---
|
|
4784
|
-
project: ${
|
|
4785
|
-
generated: "${
|
|
5037
|
+
project: ${params2.slug}
|
|
5038
|
+
generated: "${params2.timestamp}"
|
|
4786
5039
|
status: pending
|
|
4787
5040
|
progress:
|
|
4788
5041
|
total: 0
|
|
@@ -4798,7 +5051,7 @@ needsAttention:
|
|
|
4798
5051
|
openQuestions: 0
|
|
4799
5052
|
---
|
|
4800
5053
|
|
|
4801
|
-
# Project Status: ${
|
|
5054
|
+
# Project Status: ${params2.title}
|
|
4802
5055
|
|
|
4803
5056
|
**Status:** pending
|
|
4804
5057
|
**Progress:** 0/0 assignments complete
|
|
@@ -4818,10 +5071,10 @@ No dependencies yet.
|
|
|
4818
5071
|
- **0 unanswered** questions
|
|
4819
5072
|
`;
|
|
4820
5073
|
}
|
|
4821
|
-
function renderResourcesIndex(
|
|
5074
|
+
function renderResourcesIndex(params2) {
|
|
4822
5075
|
return `---
|
|
4823
|
-
project: ${
|
|
4824
|
-
generated: "${
|
|
5076
|
+
project: ${params2.slug}
|
|
5077
|
+
generated: "${params2.timestamp}"
|
|
4825
5078
|
total: 0
|
|
4826
5079
|
---
|
|
4827
5080
|
|
|
@@ -4831,10 +5084,10 @@ total: 0
|
|
|
4831
5084
|
|------|----------|--------|---------------------|---------|
|
|
4832
5085
|
`;
|
|
4833
5086
|
}
|
|
4834
|
-
function renderMemoriesIndex(
|
|
5087
|
+
function renderMemoriesIndex(params2) {
|
|
4835
5088
|
return `---
|
|
4836
|
-
project: ${
|
|
4837
|
-
generated: "${
|
|
5089
|
+
project: ${params2.slug}
|
|
5090
|
+
generated: "${params2.timestamp}"
|
|
4838
5091
|
total: 0
|
|
4839
5092
|
---
|
|
4840
5093
|
|
|
@@ -4846,19 +5099,19 @@ total: 0
|
|
|
4846
5099
|
}
|
|
4847
5100
|
|
|
4848
5101
|
// src/templates/playbook.ts
|
|
4849
|
-
function renderPlaybook(
|
|
4850
|
-
const whenToUse =
|
|
5102
|
+
function renderPlaybook(params2) {
|
|
5103
|
+
const whenToUse = params2.whenToUse ? escapeYamlString(params2.whenToUse) : "null";
|
|
4851
5104
|
return `---
|
|
4852
|
-
name: ${escapeYamlString(
|
|
4853
|
-
slug: ${
|
|
4854
|
-
description: ${escapeYamlString(
|
|
5105
|
+
name: ${escapeYamlString(params2.name)}
|
|
5106
|
+
slug: ${params2.slug}
|
|
5107
|
+
description: ${escapeYamlString(params2.description)}
|
|
4855
5108
|
when_to_use: ${whenToUse}
|
|
4856
|
-
created: "${
|
|
4857
|
-
updated: "${
|
|
5109
|
+
created: "${params2.timestamp}"
|
|
5110
|
+
updated: "${params2.timestamp}"
|
|
4858
5111
|
tags: []
|
|
4859
5112
|
---
|
|
4860
5113
|
|
|
4861
|
-
# ${
|
|
5114
|
+
# ${params2.name}
|
|
4862
5115
|
|
|
4863
5116
|
<!-- Write imperative rules and workflows here. Keep it under 50 lines. -->
|
|
4864
5117
|
`;
|
|
@@ -4966,7 +5219,7 @@ async function readCurrentDocument(filePath) {
|
|
|
4966
5219
|
if (!await fileExists(filePath)) {
|
|
4967
5220
|
return null;
|
|
4968
5221
|
}
|
|
4969
|
-
return
|
|
5222
|
+
return readFile10(filePath, "utf-8");
|
|
4970
5223
|
}
|
|
4971
5224
|
function createWriteRouter(projectsDir, assignmentsDir) {
|
|
4972
5225
|
const router = Router();
|
|
@@ -4979,7 +5232,15 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
4979
5232
|
});
|
|
4980
5233
|
res.json({ content });
|
|
4981
5234
|
});
|
|
4982
|
-
router.get("/api/templates/assignment", (
|
|
5235
|
+
router.get("/api/templates/assignment", (req, res) => {
|
|
5236
|
+
const standalone = req.query.standalone === "1";
|
|
5237
|
+
const workspaceParam = typeof req.query.workspace === "string" ? req.query.workspace : "";
|
|
5238
|
+
if (workspaceParam && !isValidSlug(workspaceParam)) {
|
|
5239
|
+
res.status(400).json({
|
|
5240
|
+
error: `Invalid workspace slug "${workspaceParam}". Slugs must be lowercase, hyphen-separated, with no special characters.`
|
|
5241
|
+
});
|
|
5242
|
+
return;
|
|
5243
|
+
}
|
|
4983
5244
|
const content = renderAssignment({
|
|
4984
5245
|
id: generateId(),
|
|
4985
5246
|
slug: "my-new-assignment",
|
|
@@ -4987,7 +5248,9 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
4987
5248
|
timestamp: nowTimestamp(),
|
|
4988
5249
|
priority: "medium",
|
|
4989
5250
|
dependsOn: [],
|
|
4990
|
-
links: []
|
|
5251
|
+
links: [],
|
|
5252
|
+
project: standalone ? null : void 0,
|
|
5253
|
+
workspaceGroup: standalone && workspaceParam ? workspaceParam : null
|
|
4991
5254
|
});
|
|
4992
5255
|
res.json({ content });
|
|
4993
5256
|
});
|
|
@@ -5096,26 +5359,26 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5096
5359
|
res.status(400).json({ error: `Invalid slug "${slug}". Must be lowercase and hyphen-separated.` });
|
|
5097
5360
|
return;
|
|
5098
5361
|
}
|
|
5099
|
-
const projectDir =
|
|
5362
|
+
const projectDir = resolve12(projectsDir, slug);
|
|
5100
5363
|
if (await fileExists(projectDir)) {
|
|
5101
5364
|
res.status(409).json({ error: `Project "${slug}" already exists` });
|
|
5102
5365
|
return;
|
|
5103
5366
|
}
|
|
5104
5367
|
const title = fields.title;
|
|
5105
5368
|
const timestamp = fields.created || nowTimestamp();
|
|
5106
|
-
await ensureDir(
|
|
5107
|
-
await ensureDir(
|
|
5108
|
-
await ensureDir(
|
|
5109
|
-
await writeFileForce(
|
|
5369
|
+
await ensureDir(resolve12(projectDir, "assignments"));
|
|
5370
|
+
await ensureDir(resolve12(projectDir, "resources"));
|
|
5371
|
+
await ensureDir(resolve12(projectDir, "memories"));
|
|
5372
|
+
await writeFileForce(resolve12(projectDir, "project.md"), content);
|
|
5110
5373
|
try {
|
|
5111
5374
|
const companions = [
|
|
5112
|
-
[
|
|
5113
|
-
[
|
|
5114
|
-
[
|
|
5115
|
-
[
|
|
5116
|
-
[
|
|
5117
|
-
[
|
|
5118
|
-
[
|
|
5375
|
+
[resolve12(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
|
|
5376
|
+
[resolve12(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
|
|
5377
|
+
[resolve12(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
|
|
5378
|
+
[resolve12(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
|
|
5379
|
+
[resolve12(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
|
|
5380
|
+
[resolve12(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
|
|
5381
|
+
[resolve12(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
|
|
5119
5382
|
];
|
|
5120
5383
|
for (const [filePath, fileContent] of companions) {
|
|
5121
5384
|
await writeFileForce(filePath, fileContent);
|
|
@@ -5136,8 +5399,8 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5136
5399
|
router.post("/api/projects/:slug/assignments", async (req, res) => {
|
|
5137
5400
|
try {
|
|
5138
5401
|
const projectSlug = getParam(req.params.slug);
|
|
5139
|
-
const projectDir =
|
|
5140
|
-
const projectMdPath =
|
|
5402
|
+
const projectDir = resolve12(projectsDir, projectSlug);
|
|
5403
|
+
const projectMdPath = resolve12(projectDir, "project.md");
|
|
5141
5404
|
if (!await fileExists(projectMdPath)) {
|
|
5142
5405
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
5143
5406
|
return;
|
|
@@ -5167,7 +5430,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5167
5430
|
res.status(400).json({ error: `Invalid priority "${priority}". Must be low, medium, high, or critical.` });
|
|
5168
5431
|
return;
|
|
5169
5432
|
}
|
|
5170
|
-
const assignmentDir =
|
|
5433
|
+
const assignmentDir = resolve12(projectDir, "assignments", assignmentSlug);
|
|
5171
5434
|
if (await fileExists(assignmentDir)) {
|
|
5172
5435
|
res.status(409).json({
|
|
5173
5436
|
error: `Assignment "${assignmentSlug}" already exists in project "${projectSlug}"`
|
|
@@ -5176,12 +5439,12 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5176
5439
|
}
|
|
5177
5440
|
const timestamp = fields.created || nowTimestamp();
|
|
5178
5441
|
await ensureDir(assignmentDir);
|
|
5179
|
-
await writeFileForce(
|
|
5442
|
+
await writeFileForce(resolve12(assignmentDir, "assignment.md"), content);
|
|
5180
5443
|
try {
|
|
5181
5444
|
const companions = [
|
|
5182
|
-
[
|
|
5183
|
-
[
|
|
5184
|
-
[
|
|
5445
|
+
[resolve12(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
|
|
5446
|
+
[resolve12(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
|
|
5447
|
+
[resolve12(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
|
|
5185
5448
|
];
|
|
5186
5449
|
for (const [filePath, fileContent] of companions) {
|
|
5187
5450
|
await writeFileForce(filePath, fileContent);
|
|
@@ -5202,7 +5465,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5202
5465
|
router.patch("/api/projects/:slug", async (req, res) => {
|
|
5203
5466
|
try {
|
|
5204
5467
|
const projectSlug = getParam(req.params.slug);
|
|
5205
|
-
const projectPath =
|
|
5468
|
+
const projectPath = resolve12(projectsDir, projectSlug, "project.md");
|
|
5206
5469
|
const currentContent = await readCurrentDocument(projectPath);
|
|
5207
5470
|
if (!currentContent) {
|
|
5208
5471
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
@@ -5235,7 +5498,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5235
5498
|
try {
|
|
5236
5499
|
const projectSlug = getParam(req.params.slug);
|
|
5237
5500
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5238
|
-
const assignmentPath =
|
|
5501
|
+
const assignmentPath = resolve12(
|
|
5239
5502
|
projectsDir,
|
|
5240
5503
|
projectSlug,
|
|
5241
5504
|
"assignments",
|
|
@@ -5278,7 +5541,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5278
5541
|
try {
|
|
5279
5542
|
const projectSlug = getParam(req.params.slug);
|
|
5280
5543
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5281
|
-
const assignmentPath =
|
|
5544
|
+
const assignmentPath = resolve12(
|
|
5282
5545
|
projectsDir,
|
|
5283
5546
|
projectSlug,
|
|
5284
5547
|
"assignments",
|
|
@@ -5314,7 +5577,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5314
5577
|
try {
|
|
5315
5578
|
const projectSlug = getParam(req.params.slug);
|
|
5316
5579
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5317
|
-
const planPath =
|
|
5580
|
+
const planPath = resolve12(
|
|
5318
5581
|
projectsDir,
|
|
5319
5582
|
projectSlug,
|
|
5320
5583
|
"assignments",
|
|
@@ -5352,7 +5615,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5352
5615
|
try {
|
|
5353
5616
|
const projectSlug = getParam(req.params.slug);
|
|
5354
5617
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5355
|
-
const scratchpadPath =
|
|
5618
|
+
const scratchpadPath = resolve12(
|
|
5356
5619
|
projectsDir,
|
|
5357
5620
|
projectSlug,
|
|
5358
5621
|
"assignments",
|
|
@@ -5390,7 +5653,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5390
5653
|
try {
|
|
5391
5654
|
const projectSlug = getParam(req.params.slug);
|
|
5392
5655
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5393
|
-
const handoffPath =
|
|
5656
|
+
const handoffPath = resolve12(
|
|
5394
5657
|
projectsDir,
|
|
5395
5658
|
projectSlug,
|
|
5396
5659
|
"assignments",
|
|
@@ -5428,7 +5691,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5428
5691
|
try {
|
|
5429
5692
|
const projectSlug = getParam(req.params.slug);
|
|
5430
5693
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5431
|
-
const decisionPath =
|
|
5694
|
+
const decisionPath = resolve12(
|
|
5432
5695
|
projectsDir,
|
|
5433
5696
|
projectSlug,
|
|
5434
5697
|
"assignments",
|
|
@@ -5466,7 +5729,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5466
5729
|
try {
|
|
5467
5730
|
const projectSlug = getParam(req.params.slug);
|
|
5468
5731
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5469
|
-
const commentsPath =
|
|
5732
|
+
const commentsPath = resolve12(
|
|
5470
5733
|
projectsDir,
|
|
5471
5734
|
projectSlug,
|
|
5472
5735
|
"assignments",
|
|
@@ -5484,7 +5747,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5484
5747
|
let currentContent;
|
|
5485
5748
|
let currentCount = 0;
|
|
5486
5749
|
if (await fileExists(commentsPath)) {
|
|
5487
|
-
currentContent = await
|
|
5750
|
+
currentContent = await readFile10(commentsPath, "utf-8");
|
|
5488
5751
|
const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
|
|
5489
5752
|
if (countMatch) currentCount = parseInt(countMatch[1], 10);
|
|
5490
5753
|
} else {
|
|
@@ -5525,7 +5788,7 @@ ${entry}`;
|
|
|
5525
5788
|
const projectSlug = getParam(req.params.slug);
|
|
5526
5789
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5527
5790
|
const commentId = getParam(req.params.commentId);
|
|
5528
|
-
const commentsPath =
|
|
5791
|
+
const commentsPath = resolve12(
|
|
5529
5792
|
projectsDir,
|
|
5530
5793
|
projectSlug,
|
|
5531
5794
|
"assignments",
|
|
@@ -5541,7 +5804,7 @@ ${entry}`;
|
|
|
5541
5804
|
res.status(400).json({ error: "resolved (boolean) is required" });
|
|
5542
5805
|
return;
|
|
5543
5806
|
}
|
|
5544
|
-
const content = await
|
|
5807
|
+
const content = await readFile10(commentsPath, "utf-8");
|
|
5545
5808
|
const parsed = parseComments(content);
|
|
5546
5809
|
const target = parsed.entries.find((e) => e.id === commentId);
|
|
5547
5810
|
if (!target) {
|
|
@@ -5576,7 +5839,7 @@ ${entry}`;
|
|
|
5576
5839
|
router.post("/api/projects/:slug/move-workspace", async (req, res) => {
|
|
5577
5840
|
try {
|
|
5578
5841
|
const projectSlug = getParam(req.params.slug);
|
|
5579
|
-
const projectPath =
|
|
5842
|
+
const projectPath = resolve12(projectsDir, projectSlug, "project.md");
|
|
5580
5843
|
if (!await fileExists(projectPath)) {
|
|
5581
5844
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
5582
5845
|
return;
|
|
@@ -5586,7 +5849,7 @@ ${entry}`;
|
|
|
5586
5849
|
res.status(400).json({ error: "workspace must be a non-empty string or null (for ungrouped)." });
|
|
5587
5850
|
return;
|
|
5588
5851
|
}
|
|
5589
|
-
let content = await
|
|
5852
|
+
let content = await readFile10(projectPath, "utf-8");
|
|
5590
5853
|
content = setTopLevelField(content, "workspace", workspace ?? null);
|
|
5591
5854
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
5592
5855
|
await writeFileForce(projectPath, content);
|
|
@@ -5600,7 +5863,7 @@ ${entry}`;
|
|
|
5600
5863
|
router.post("/api/projects/:slug/status-override", async (req, res) => {
|
|
5601
5864
|
try {
|
|
5602
5865
|
const projectSlug = getParam(req.params.slug);
|
|
5603
|
-
const projectPath =
|
|
5866
|
+
const projectPath = resolve12(projectsDir, projectSlug, "project.md");
|
|
5604
5867
|
if (!await fileExists(projectPath)) {
|
|
5605
5868
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
5606
5869
|
return;
|
|
@@ -5612,7 +5875,7 @@ ${entry}`;
|
|
|
5612
5875
|
res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}, or null to clear.` });
|
|
5613
5876
|
return;
|
|
5614
5877
|
}
|
|
5615
|
-
let content = await
|
|
5878
|
+
let content = await readFile10(projectPath, "utf-8");
|
|
5616
5879
|
content = setTopLevelField(content, "statusOverride", status ?? null);
|
|
5617
5880
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
5618
5881
|
await writeFileForce(projectPath, content);
|
|
@@ -5627,7 +5890,7 @@ ${entry}`;
|
|
|
5627
5890
|
try {
|
|
5628
5891
|
const projectSlug = getParam(req.params.slug);
|
|
5629
5892
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5630
|
-
const assignmentPath =
|
|
5893
|
+
const assignmentPath = resolve12(
|
|
5631
5894
|
projectsDir,
|
|
5632
5895
|
projectSlug,
|
|
5633
5896
|
"assignments",
|
|
@@ -5645,7 +5908,7 @@ ${entry}`;
|
|
|
5645
5908
|
res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
|
|
5646
5909
|
return;
|
|
5647
5910
|
}
|
|
5648
|
-
let content = await
|
|
5911
|
+
let content = await readFile10(assignmentPath, "utf-8");
|
|
5649
5912
|
content = setTopLevelField(content, "status", status);
|
|
5650
5913
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
5651
5914
|
if (status !== "blocked") {
|
|
@@ -5670,8 +5933,8 @@ ${entry}`;
|
|
|
5670
5933
|
res.status(400).json({ error: `Unsupported transition command "${req.params.command}"` });
|
|
5671
5934
|
return;
|
|
5672
5935
|
}
|
|
5673
|
-
const projectDir =
|
|
5674
|
-
const assignmentPath =
|
|
5936
|
+
const projectDir = resolve12(projectsDir, projectSlug);
|
|
5937
|
+
const assignmentPath = resolve12(projectDir, "assignments", assignmentSlug, "assignment.md");
|
|
5675
5938
|
if (!await fileExists(assignmentPath)) {
|
|
5676
5939
|
res.status(404).json({ error: "Assignment not found" });
|
|
5677
5940
|
return;
|
|
@@ -5697,8 +5960,8 @@ ${entry}`;
|
|
|
5697
5960
|
try {
|
|
5698
5961
|
const projectSlug = getParam(req.params.slug);
|
|
5699
5962
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5700
|
-
const assignmentDir =
|
|
5701
|
-
const assignmentPath =
|
|
5963
|
+
const assignmentDir = resolve12(projectsDir, projectSlug, "assignments", assignmentSlug);
|
|
5964
|
+
const assignmentPath = resolve12(assignmentDir, "assignment.md");
|
|
5702
5965
|
if (!await fileExists(assignmentPath)) {
|
|
5703
5966
|
res.status(404).json({ error: `Assignment "${assignmentSlug}" not found in project "${projectSlug}"` });
|
|
5704
5967
|
return;
|
|
@@ -5716,6 +5979,76 @@ ${entry}`;
|
|
|
5716
5979
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
5717
5980
|
return;
|
|
5718
5981
|
}
|
|
5982
|
+
const rawContent = typeof req.body?.content === "string" ? req.body.content : "";
|
|
5983
|
+
if (rawContent.trim()) {
|
|
5984
|
+
const fields = extractFrontmatter3(rawContent);
|
|
5985
|
+
if (!fields) {
|
|
5986
|
+
res.status(400).json({ error: "Invalid frontmatter: missing --- delimiters" });
|
|
5987
|
+
return;
|
|
5988
|
+
}
|
|
5989
|
+
const validation = validateRequired(fields, ["slug", "title"]);
|
|
5990
|
+
if (!validation.valid) {
|
|
5991
|
+
res.status(400).json({ error: `Missing required fields: ${validation.missing.join(", ")}` });
|
|
5992
|
+
return;
|
|
5993
|
+
}
|
|
5994
|
+
const submittedSlug = fields.slug;
|
|
5995
|
+
if (!isValidSlug(submittedSlug)) {
|
|
5996
|
+
res.status(400).json({ error: `Invalid slug "${submittedSlug}". Must be lowercase and hyphen-separated.` });
|
|
5997
|
+
return;
|
|
5998
|
+
}
|
|
5999
|
+
const validPriorities = ["low", "medium", "high", "critical"];
|
|
6000
|
+
const submittedPriority = fields.priority || "medium";
|
|
6001
|
+
if (!validPriorities.includes(submittedPriority)) {
|
|
6002
|
+
res.status(400).json({ error: `Invalid priority "${submittedPriority}". Must be low, medium, high, or critical.` });
|
|
6003
|
+
return;
|
|
6004
|
+
}
|
|
6005
|
+
if (fields.project && fields.project !== "null") {
|
|
6006
|
+
res.status(400).json({
|
|
6007
|
+
error: 'Standalone assignments cannot have a project; remove "project" or set it to null.'
|
|
6008
|
+
});
|
|
6009
|
+
return;
|
|
6010
|
+
}
|
|
6011
|
+
const submittedWorkspaceGroup = fields.workspaceGroup && fields.workspaceGroup !== "null" ? fields.workspaceGroup : "";
|
|
6012
|
+
if (submittedWorkspaceGroup && !isValidSlug(submittedWorkspaceGroup)) {
|
|
6013
|
+
res.status(400).json({
|
|
6014
|
+
error: `Invalid workspace slug "${submittedWorkspaceGroup}". Slugs must be lowercase, hyphen-separated, with no special characters.`
|
|
6015
|
+
});
|
|
6016
|
+
return;
|
|
6017
|
+
}
|
|
6018
|
+
const id2 = generateId();
|
|
6019
|
+
const assignmentDir2 = resolve12(assignmentsDir, id2);
|
|
6020
|
+
if (await fileExists(assignmentDir2)) {
|
|
6021
|
+
res.status(500).json({ error: "UUID collision \u2014 try again" });
|
|
6022
|
+
return;
|
|
6023
|
+
}
|
|
6024
|
+
const timestamp2 = fields.created || nowTimestamp();
|
|
6025
|
+
await ensureDir(assignmentDir2);
|
|
6026
|
+
const normalizedContent = setTopLevelField(rawContent, "id", id2);
|
|
6027
|
+
await writeFileForce(resolve12(assignmentDir2, "assignment.md"), normalizedContent);
|
|
6028
|
+
await writeFileForce(
|
|
6029
|
+
resolve12(assignmentDir2, "scratchpad.md"),
|
|
6030
|
+
renderScratchpad({ assignmentSlug: id2, timestamp: timestamp2 })
|
|
6031
|
+
);
|
|
6032
|
+
await writeFileForce(
|
|
6033
|
+
resolve12(assignmentDir2, "handoff.md"),
|
|
6034
|
+
renderHandoff({ assignmentSlug: id2, timestamp: timestamp2 })
|
|
6035
|
+
);
|
|
6036
|
+
await writeFileForce(
|
|
6037
|
+
resolve12(assignmentDir2, "decision-record.md"),
|
|
6038
|
+
renderDecisionRecord({ assignmentSlug: id2, timestamp: timestamp2 })
|
|
6039
|
+
);
|
|
6040
|
+
await writeFileForce(
|
|
6041
|
+
resolve12(assignmentDir2, "progress.md"),
|
|
6042
|
+
renderProgress({ assignment: id2, timestamp: timestamp2 })
|
|
6043
|
+
);
|
|
6044
|
+
await writeFileForce(
|
|
6045
|
+
resolve12(assignmentDir2, "comments.md"),
|
|
6046
|
+
renderComments({ assignment: id2, timestamp: timestamp2 })
|
|
6047
|
+
);
|
|
6048
|
+
const detail2 = await getAssignmentDetailById(projectsDir, assignmentsDir, id2);
|
|
6049
|
+
res.status(201).json({ assignment: detail2 });
|
|
6050
|
+
return;
|
|
6051
|
+
}
|
|
5719
6052
|
const { title, slug, priority, type } = req.body || {};
|
|
5720
6053
|
if (!title || typeof title !== "string" || !title.trim()) {
|
|
5721
6054
|
res.status(400).json({ error: "title is required" });
|
|
@@ -5727,7 +6060,7 @@ ${entry}`;
|
|
|
5727
6060
|
return;
|
|
5728
6061
|
}
|
|
5729
6062
|
const id = generateId();
|
|
5730
|
-
const assignmentDir =
|
|
6063
|
+
const assignmentDir = resolve12(assignmentsDir, id);
|
|
5731
6064
|
if (await fileExists(assignmentDir)) {
|
|
5732
6065
|
res.status(500).json({ error: "UUID collision \u2014 try again" });
|
|
5733
6066
|
return;
|
|
@@ -5747,25 +6080,25 @@ ${entry}`;
|
|
|
5747
6080
|
project: null,
|
|
5748
6081
|
type: typeof type === "string" ? type : void 0
|
|
5749
6082
|
});
|
|
5750
|
-
await writeFileForce(
|
|
6083
|
+
await writeFileForce(resolve12(assignmentDir, "assignment.md"), assignmentContent);
|
|
5751
6084
|
await writeFileForce(
|
|
5752
|
-
|
|
6085
|
+
resolve12(assignmentDir, "scratchpad.md"),
|
|
5753
6086
|
renderScratchpad({ assignmentSlug: id, timestamp })
|
|
5754
6087
|
);
|
|
5755
6088
|
await writeFileForce(
|
|
5756
|
-
|
|
6089
|
+
resolve12(assignmentDir, "handoff.md"),
|
|
5757
6090
|
renderHandoff({ assignmentSlug: id, timestamp })
|
|
5758
6091
|
);
|
|
5759
6092
|
await writeFileForce(
|
|
5760
|
-
|
|
6093
|
+
resolve12(assignmentDir, "decision-record.md"),
|
|
5761
6094
|
renderDecisionRecord({ assignmentSlug: id, timestamp })
|
|
5762
6095
|
);
|
|
5763
6096
|
await writeFileForce(
|
|
5764
|
-
|
|
6097
|
+
resolve12(assignmentDir, "progress.md"),
|
|
5765
6098
|
renderProgress({ assignment: id, timestamp })
|
|
5766
6099
|
);
|
|
5767
6100
|
await writeFileForce(
|
|
5768
|
-
|
|
6101
|
+
resolve12(assignmentDir, "comments.md"),
|
|
5769
6102
|
renderComments({ assignment: id, timestamp })
|
|
5770
6103
|
);
|
|
5771
6104
|
const detail = await getAssignmentDetailById(projectsDir, assignmentsDir, id);
|
|
@@ -5893,7 +6226,7 @@ ${entry}`;
|
|
|
5893
6226
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
5894
6227
|
return;
|
|
5895
6228
|
}
|
|
5896
|
-
const assignmentPath =
|
|
6229
|
+
const assignmentPath = resolve12(resolved.assignmentDir, "assignment.md");
|
|
5897
6230
|
const currentContent = await readCurrentDocument(assignmentPath);
|
|
5898
6231
|
if (!currentContent) {
|
|
5899
6232
|
res.status(404).json({ error: "Assignment not found" });
|
|
@@ -5935,7 +6268,7 @@ ${entry}`;
|
|
|
5935
6268
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
5936
6269
|
return;
|
|
5937
6270
|
}
|
|
5938
|
-
const planPath =
|
|
6271
|
+
const planPath = resolve12(resolved.assignmentDir, "plan.md");
|
|
5939
6272
|
const currentContent = await readCurrentDocument(planPath);
|
|
5940
6273
|
if (!currentContent) {
|
|
5941
6274
|
res.status(404).json({ error: "Plan not found" });
|
|
@@ -5969,7 +6302,7 @@ ${entry}`;
|
|
|
5969
6302
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
5970
6303
|
return;
|
|
5971
6304
|
}
|
|
5972
|
-
const scratchpadPath =
|
|
6305
|
+
const scratchpadPath = resolve12(resolved.assignmentDir, "scratchpad.md");
|
|
5973
6306
|
const currentContent = await readCurrentDocument(scratchpadPath);
|
|
5974
6307
|
if (!currentContent) {
|
|
5975
6308
|
res.status(404).json({ error: "Scratchpad not found" });
|
|
@@ -6003,7 +6336,7 @@ ${entry}`;
|
|
|
6003
6336
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6004
6337
|
return;
|
|
6005
6338
|
}
|
|
6006
|
-
const handoffPath =
|
|
6339
|
+
const handoffPath = resolve12(resolved.assignmentDir, "handoff.md");
|
|
6007
6340
|
const currentContent = await readCurrentDocument(handoffPath);
|
|
6008
6341
|
if (!currentContent) {
|
|
6009
6342
|
res.status(404).json({ error: "Handoff log not found" });
|
|
@@ -6043,7 +6376,7 @@ ${entry}`;
|
|
|
6043
6376
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6044
6377
|
return;
|
|
6045
6378
|
}
|
|
6046
|
-
const decisionPath =
|
|
6379
|
+
const decisionPath = resolve12(resolved.assignmentDir, "decision-record.md");
|
|
6047
6380
|
const currentContent = await readCurrentDocument(decisionPath);
|
|
6048
6381
|
if (!currentContent) {
|
|
6049
6382
|
res.status(404).json({ error: "Decision record not found" });
|
|
@@ -6083,7 +6416,7 @@ ${entry}`;
|
|
|
6083
6416
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6084
6417
|
return;
|
|
6085
6418
|
}
|
|
6086
|
-
const assignmentPath =
|
|
6419
|
+
const assignmentPath = resolve12(resolved.assignmentDir, "assignment.md");
|
|
6087
6420
|
if (!await fileExists(assignmentPath)) {
|
|
6088
6421
|
res.status(404).json({ error: "Assignment not found" });
|
|
6089
6422
|
return;
|
|
@@ -6095,7 +6428,7 @@ ${entry}`;
|
|
|
6095
6428
|
res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
|
|
6096
6429
|
return;
|
|
6097
6430
|
}
|
|
6098
|
-
let content = await
|
|
6431
|
+
let content = await readFile10(assignmentPath, "utf-8");
|
|
6099
6432
|
content = setTopLevelField(content, "status", status);
|
|
6100
6433
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
6101
6434
|
if (status !== "blocked") {
|
|
@@ -6121,7 +6454,7 @@ ${entry}`;
|
|
|
6121
6454
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6122
6455
|
return;
|
|
6123
6456
|
}
|
|
6124
|
-
const assignmentPath =
|
|
6457
|
+
const assignmentPath = resolve12(resolved.assignmentDir, "assignment.md");
|
|
6125
6458
|
const currentContent = await readCurrentDocument(assignmentPath);
|
|
6126
6459
|
if (!currentContent) {
|
|
6127
6460
|
res.status(404).json({ error: "Assignment not found" });
|
|
@@ -6186,7 +6519,7 @@ function slugifyLocal(input) {
|
|
|
6186
6519
|
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
|
|
6187
6520
|
}
|
|
6188
6521
|
async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDetail) {
|
|
6189
|
-
const commentsPath =
|
|
6522
|
+
const commentsPath = resolve12(assignmentDir, "comments.md");
|
|
6190
6523
|
const { body, author, type, replyTo } = req.body || {};
|
|
6191
6524
|
if (!body || typeof body !== "string" || !body.trim()) {
|
|
6192
6525
|
res.status(400).json({ error: "body is required" });
|
|
@@ -6198,7 +6531,7 @@ async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDet
|
|
|
6198
6531
|
let currentContent;
|
|
6199
6532
|
let currentCount = 0;
|
|
6200
6533
|
if (await fileExists(commentsPath)) {
|
|
6201
|
-
currentContent = await
|
|
6534
|
+
currentContent = await readFile10(commentsPath, "utf-8");
|
|
6202
6535
|
const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
|
|
6203
6536
|
if (countMatch) currentCount = parseInt(countMatch[1], 10);
|
|
6204
6537
|
} else {
|
|
@@ -6228,7 +6561,7 @@ ${entry}`;
|
|
|
6228
6561
|
res.status(201).json({ assignment, comment: { id: comment.id } });
|
|
6229
6562
|
}
|
|
6230
6563
|
async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloadDetail) {
|
|
6231
|
-
const commentsPath =
|
|
6564
|
+
const commentsPath = resolve12(assignmentDir, "comments.md");
|
|
6232
6565
|
if (!await fileExists(commentsPath)) {
|
|
6233
6566
|
res.status(404).json({ error: "Comments file not found" });
|
|
6234
6567
|
return;
|
|
@@ -6238,7 +6571,7 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloa
|
|
|
6238
6571
|
res.status(400).json({ error: "resolved (boolean) is required" });
|
|
6239
6572
|
return;
|
|
6240
6573
|
}
|
|
6241
|
-
const content = await
|
|
6574
|
+
const content = await readFile10(commentsPath, "utf-8");
|
|
6242
6575
|
const parsed = parseComments(content);
|
|
6243
6576
|
const target = parsed.entries.find((e) => e.id === commentId);
|
|
6244
6577
|
if (!target) {
|
|
@@ -6383,7 +6716,7 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir) {
|
|
|
6383
6716
|
|
|
6384
6717
|
// src/dashboard/api-agent-sessions.ts
|
|
6385
6718
|
import { Router as Router3 } from "express";
|
|
6386
|
-
import { resolve as
|
|
6719
|
+
import { resolve as resolve13 } from "path";
|
|
6387
6720
|
init_fs();
|
|
6388
6721
|
function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
|
|
6389
6722
|
const router = Router3();
|
|
@@ -6400,7 +6733,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
|
|
|
6400
6733
|
try {
|
|
6401
6734
|
const { projectSlug } = req.params;
|
|
6402
6735
|
const assignment = req.query.assignment;
|
|
6403
|
-
const projectDir =
|
|
6736
|
+
const projectDir = resolve13(projectsDir, projectSlug);
|
|
6404
6737
|
if (!await fileExists(projectDir)) {
|
|
6405
6738
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
6406
6739
|
return;
|
|
@@ -6426,7 +6759,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
|
|
|
6426
6759
|
return;
|
|
6427
6760
|
}
|
|
6428
6761
|
if (projectSlug) {
|
|
6429
|
-
const projectDir =
|
|
6762
|
+
const projectDir = resolve13(projectsDir, projectSlug);
|
|
6430
6763
|
if (!await fileExists(projectDir)) {
|
|
6431
6764
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
6432
6765
|
return;
|
|
@@ -6498,53 +6831,7 @@ import { resolve as resolve14 } from "path";
|
|
|
6498
6831
|
import { readFile as readFile11, unlink as unlink2 } from "fs/promises";
|
|
6499
6832
|
init_timestamp();
|
|
6500
6833
|
init_fs();
|
|
6501
|
-
|
|
6502
|
-
// src/utils/playbooks.ts
|
|
6503
|
-
init_fs();
|
|
6504
|
-
init_parser();
|
|
6505
|
-
init_timestamp();
|
|
6506
|
-
import { resolve as resolve13 } from "path";
|
|
6507
|
-
import { readdir as readdir7, readFile as readFile10 } from "fs/promises";
|
|
6508
|
-
async function rebuildPlaybookManifest(playbooksDir2) {
|
|
6509
|
-
if (!await fileExists(playbooksDir2)) return;
|
|
6510
|
-
const entries = await readdir7(playbooksDir2, { withFileTypes: true });
|
|
6511
|
-
const rows = [];
|
|
6512
|
-
for (const entry of entries) {
|
|
6513
|
-
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
|
|
6514
|
-
const raw = await readFile10(resolve13(playbooksDir2, entry.name), "utf-8");
|
|
6515
|
-
const parsed = parsePlaybook(raw);
|
|
6516
|
-
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
6517
|
-
rows.push({
|
|
6518
|
-
name: parsed.name || slug,
|
|
6519
|
-
slug,
|
|
6520
|
-
description: parsed.description,
|
|
6521
|
-
whenToUse: parsed.whenToUse
|
|
6522
|
-
});
|
|
6523
|
-
}
|
|
6524
|
-
rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
6525
|
-
const timestamp = nowTimestamp();
|
|
6526
|
-
const lines = [
|
|
6527
|
-
"---",
|
|
6528
|
-
`generated: "${timestamp}"`,
|
|
6529
|
-
`total: ${rows.length}`,
|
|
6530
|
-
"---",
|
|
6531
|
-
"",
|
|
6532
|
-
"# Playbooks",
|
|
6533
|
-
"",
|
|
6534
|
-
"Behavioral rules for AI agents. Read and follow all playbooks before starting work.",
|
|
6535
|
-
""
|
|
6536
|
-
];
|
|
6537
|
-
for (const row of rows) {
|
|
6538
|
-
lines.push(`- **[${row.name}](${row.slug}.md)** \u2014 ${row.description}`);
|
|
6539
|
-
if (row.whenToUse) {
|
|
6540
|
-
lines.push(` _When to use: ${row.whenToUse}_`);
|
|
6541
|
-
}
|
|
6542
|
-
}
|
|
6543
|
-
lines.push("");
|
|
6544
|
-
await writeFileForce(resolve13(playbooksDir2, "manifest.md"), lines.join("\n"));
|
|
6545
|
-
}
|
|
6546
|
-
|
|
6547
|
-
// src/dashboard/api-playbooks.ts
|
|
6834
|
+
init_playbooks();
|
|
6548
6835
|
function createPlaybooksRouter(playbooksDir2) {
|
|
6549
6836
|
const router = Router4();
|
|
6550
6837
|
router.get("/", async (_req, res) => {
|
|
@@ -6568,6 +6855,32 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
6568
6855
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get template" });
|
|
6569
6856
|
}
|
|
6570
6857
|
});
|
|
6858
|
+
router.post("/:slug/enable", async (req, res) => {
|
|
6859
|
+
try {
|
|
6860
|
+
const result = await setPlaybookEnabled(playbooksDir2, req.params.slug, true);
|
|
6861
|
+
res.json({ slug: result.slug, enabled: result.enabled, changed: result.changed });
|
|
6862
|
+
} catch (error) {
|
|
6863
|
+
const msg = error instanceof Error ? error.message : "Failed to enable playbook";
|
|
6864
|
+
if (msg.startsWith("Playbook ")) {
|
|
6865
|
+
res.status(404).json({ error: msg });
|
|
6866
|
+
return;
|
|
6867
|
+
}
|
|
6868
|
+
res.status(500).json({ error: msg });
|
|
6869
|
+
}
|
|
6870
|
+
});
|
|
6871
|
+
router.post("/:slug/disable", async (req, res) => {
|
|
6872
|
+
try {
|
|
6873
|
+
const result = await setPlaybookEnabled(playbooksDir2, req.params.slug, false);
|
|
6874
|
+
res.json({ slug: result.slug, enabled: result.enabled, changed: result.changed });
|
|
6875
|
+
} catch (error) {
|
|
6876
|
+
const msg = error instanceof Error ? error.message : "Failed to disable playbook";
|
|
6877
|
+
if (msg.startsWith("Playbook ")) {
|
|
6878
|
+
res.status(404).json({ error: msg });
|
|
6879
|
+
return;
|
|
6880
|
+
}
|
|
6881
|
+
res.status(500).json({ error: msg });
|
|
6882
|
+
}
|
|
6883
|
+
});
|
|
6571
6884
|
router.get("/:slug", async (req, res) => {
|
|
6572
6885
|
try {
|
|
6573
6886
|
const detail = await getPlaybookDetail(playbooksDir2, req.params.slug);
|
|
@@ -6582,17 +6895,18 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
6582
6895
|
});
|
|
6583
6896
|
router.get("/:slug/edit", async (req, res) => {
|
|
6584
6897
|
try {
|
|
6585
|
-
const
|
|
6586
|
-
if (!
|
|
6898
|
+
const resolved = await resolvePlaybookSlug(playbooksDir2, req.params.slug);
|
|
6899
|
+
if (!resolved) {
|
|
6587
6900
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
6588
6901
|
return;
|
|
6589
6902
|
}
|
|
6903
|
+
const filePath = resolve14(playbooksDir2, resolved.filename);
|
|
6590
6904
|
const content = await readFile11(filePath, "utf-8");
|
|
6591
6905
|
res.json({
|
|
6592
6906
|
documentType: "playbook",
|
|
6593
|
-
title: `Edit Playbook: ${
|
|
6907
|
+
title: `Edit Playbook: ${resolved.slug}`,
|
|
6594
6908
|
content,
|
|
6595
|
-
slug:
|
|
6909
|
+
slug: resolved.slug
|
|
6596
6910
|
});
|
|
6597
6911
|
} catch (error) {
|
|
6598
6912
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get playbook for editing" });
|
|
@@ -6631,14 +6945,15 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
6631
6945
|
res.status(400).json({ error: "content is required" });
|
|
6632
6946
|
return;
|
|
6633
6947
|
}
|
|
6634
|
-
const
|
|
6635
|
-
if (!
|
|
6948
|
+
const resolved = await resolvePlaybookSlug(playbooksDir2, req.params.slug);
|
|
6949
|
+
if (!resolved) {
|
|
6636
6950
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
6637
6951
|
return;
|
|
6638
6952
|
}
|
|
6953
|
+
const filePath = resolve14(playbooksDir2, resolved.filename);
|
|
6639
6954
|
await writeFileForce(filePath, content);
|
|
6640
6955
|
await rebuildPlaybookManifest(playbooksDir2);
|
|
6641
|
-
res.json({ slug:
|
|
6956
|
+
res.json({ slug: resolved.slug, path: filePath });
|
|
6642
6957
|
} catch (error) {
|
|
6643
6958
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to update playbook" });
|
|
6644
6959
|
}
|
|
@@ -6649,14 +6964,16 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
6649
6964
|
res.status(403).json({ error: "The playbook manifest cannot be deleted" });
|
|
6650
6965
|
return;
|
|
6651
6966
|
}
|
|
6652
|
-
const
|
|
6653
|
-
if (!
|
|
6967
|
+
const resolved = await resolvePlaybookSlug(playbooksDir2, req.params.slug);
|
|
6968
|
+
if (!resolved) {
|
|
6654
6969
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
6655
6970
|
return;
|
|
6656
6971
|
}
|
|
6972
|
+
const filePath = resolve14(playbooksDir2, resolved.filename);
|
|
6657
6973
|
await unlink2(filePath);
|
|
6974
|
+
await removeFromDisabledList(resolved.slug);
|
|
6658
6975
|
await rebuildPlaybookManifest(playbooksDir2);
|
|
6659
|
-
res.json({ deleted:
|
|
6976
|
+
res.json({ deleted: resolved.slug });
|
|
6660
6977
|
} catch (error) {
|
|
6661
6978
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to delete playbook" });
|
|
6662
6979
|
}
|
|
@@ -6680,14 +6997,17 @@ function getWorkspaceParam(value) {
|
|
|
6680
6997
|
return value ?? "";
|
|
6681
6998
|
}
|
|
6682
6999
|
var writeLocks = /* @__PURE__ */ new Map();
|
|
6683
|
-
function withLock(
|
|
6684
|
-
const prev = writeLocks.get(
|
|
7000
|
+
function withLock(lockKey, fn) {
|
|
7001
|
+
const prev = writeLocks.get(lockKey) ?? Promise.resolve();
|
|
6685
7002
|
const next = prev.then(fn);
|
|
6686
|
-
writeLocks.set(
|
|
7003
|
+
writeLocks.set(lockKey, next.then(() => {
|
|
6687
7004
|
}, () => {
|
|
6688
7005
|
}));
|
|
6689
7006
|
return next;
|
|
6690
7007
|
}
|
|
7008
|
+
function wsLock(workspace, fn) {
|
|
7009
|
+
return withLock(`ws:${workspace}`, fn);
|
|
7010
|
+
}
|
|
6691
7011
|
function createTodosRouter(todosDir2, broadcast) {
|
|
6692
7012
|
const router = Router5();
|
|
6693
7013
|
function broadcastUpdate() {
|
|
@@ -6746,7 +7066,7 @@ function createTodosRouter(todosDir2, broadcast) {
|
|
|
6746
7066
|
res.status(400).json({ error: "description is required" });
|
|
6747
7067
|
return;
|
|
6748
7068
|
}
|
|
6749
|
-
const item = await
|
|
7069
|
+
const item = await wsLock(workspace, async () => {
|
|
6750
7070
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
6751
7071
|
const existingIds = new Set(checklist.items.map((i) => i.id));
|
|
6752
7072
|
const id = generateUniqueId(existingIds);
|
|
@@ -6775,7 +7095,7 @@ function createTodosRouter(todosDir2, broadcast) {
|
|
|
6775
7095
|
res.status(400).json({ error: "ids must be an array of strings" });
|
|
6776
7096
|
return;
|
|
6777
7097
|
}
|
|
6778
|
-
const items = await
|
|
7098
|
+
const items = await wsLock(workspace, async () => {
|
|
6779
7099
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
6780
7100
|
const itemMap = new Map(checklist.items.map((i) => [i.id, i]));
|
|
6781
7101
|
const reordered = [];
|
|
@@ -6810,8 +7130,8 @@ function createTodosRouter(todosDir2, broadcast) {
|
|
|
6810
7130
|
router.post("/:workspace/archive", async (req, res) => {
|
|
6811
7131
|
try {
|
|
6812
7132
|
const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
|
|
6813
|
-
const { resolve:
|
|
6814
|
-
const { readFile:
|
|
7133
|
+
const { resolve: resolve19 } = await import("path");
|
|
7134
|
+
const { readFile: readFile15 } = await import("fs/promises");
|
|
6815
7135
|
const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
6816
7136
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
6817
7137
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
@@ -6827,10 +7147,10 @@ function createTodosRouter(todosDir2, broadcast) {
|
|
|
6827
7147
|
(e) => e.itemIds.every((id) => completedIds.has(id))
|
|
6828
7148
|
);
|
|
6829
7149
|
const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
|
|
6830
|
-
await ensureDir(
|
|
7150
|
+
await ensureDir(resolve19(todosDir2, "archive"));
|
|
6831
7151
|
let archContent = "";
|
|
6832
7152
|
if (await fileExists(archFile)) {
|
|
6833
|
-
archContent = await
|
|
7153
|
+
archContent = await readFile15(archFile, "utf-8");
|
|
6834
7154
|
archContent = archContent.trimEnd() + "\n\n";
|
|
6835
7155
|
} else {
|
|
6836
7156
|
archContent = `---
|
|
@@ -6899,7 +7219,7 @@ workspace: ${workspace}
|
|
|
6899
7219
|
router.patch("/:workspace/:id", async (req, res) => {
|
|
6900
7220
|
try {
|
|
6901
7221
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
6902
|
-
const result = await
|
|
7222
|
+
const result = await wsLock(workspace, async () => {
|
|
6903
7223
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
6904
7224
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
6905
7225
|
if (!item) return null;
|
|
@@ -6921,7 +7241,7 @@ workspace: ${workspace}
|
|
|
6921
7241
|
router.delete("/:workspace/:id", async (req, res) => {
|
|
6922
7242
|
try {
|
|
6923
7243
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
6924
|
-
const deleted = await
|
|
7244
|
+
const deleted = await wsLock(workspace, async () => {
|
|
6925
7245
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
6926
7246
|
const idx = checklist.items.findIndex((i) => i.id === req.params.id);
|
|
6927
7247
|
if (idx === -1) return false;
|
|
@@ -6942,7 +7262,7 @@ workspace: ${workspace}
|
|
|
6942
7262
|
router.post("/:workspace/:id/start", async (req, res) => {
|
|
6943
7263
|
try {
|
|
6944
7264
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
6945
|
-
const result = await
|
|
7265
|
+
const result = await wsLock(workspace, async () => {
|
|
6946
7266
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
6947
7267
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
6948
7268
|
if (!item) return { error: "not_found" };
|
|
@@ -6969,7 +7289,7 @@ workspace: ${workspace}
|
|
|
6969
7289
|
router.post("/:workspace/:id/complete", async (req, res) => {
|
|
6970
7290
|
try {
|
|
6971
7291
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
6972
|
-
const result = await
|
|
7292
|
+
const result = await wsLock(workspace, async () => {
|
|
6973
7293
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
6974
7294
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
6975
7295
|
if (!item) return null;
|
|
@@ -7003,7 +7323,7 @@ workspace: ${workspace}
|
|
|
7003
7323
|
try {
|
|
7004
7324
|
const reason = req.body.reason || null;
|
|
7005
7325
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
7006
|
-
const result = await
|
|
7326
|
+
const result = await wsLock(workspace, async () => {
|
|
7007
7327
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
7008
7328
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
7009
7329
|
if (!item) return null;
|
|
@@ -7036,7 +7356,7 @@ workspace: ${workspace}
|
|
|
7036
7356
|
router.post("/:workspace/:id/reopen", async (req, res) => {
|
|
7037
7357
|
try {
|
|
7038
7358
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
7039
|
-
const result = await
|
|
7359
|
+
const result = await wsLock(workspace, async () => {
|
|
7040
7360
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
7041
7361
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
7042
7362
|
if (!item) return null;
|
|
@@ -7058,7 +7378,7 @@ workspace: ${workspace}
|
|
|
7058
7378
|
router.post("/:workspace/:id/unblock", async (req, res) => {
|
|
7059
7379
|
try {
|
|
7060
7380
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
7061
|
-
const result = await
|
|
7381
|
+
const result = await wsLock(workspace, async () => {
|
|
7062
7382
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
7063
7383
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
7064
7384
|
if (!item) return null;
|
|
@@ -7080,18 +7400,615 @@ workspace: ${workspace}
|
|
|
7080
7400
|
return router;
|
|
7081
7401
|
}
|
|
7082
7402
|
|
|
7083
|
-
// src/dashboard/api-
|
|
7084
|
-
|
|
7085
|
-
import { Router as Router6 } from "express";
|
|
7086
|
-
|
|
7087
|
-
// src/utils/github-backup.ts
|
|
7088
|
-
init_paths();
|
|
7403
|
+
// src/dashboard/api-project-todos.ts
|
|
7404
|
+
init_parser2();
|
|
7089
7405
|
init_fs();
|
|
7090
|
-
|
|
7406
|
+
init_paths();
|
|
7407
|
+
import { Router as Router6 } from "express";
|
|
7408
|
+
import { mkdir as mkdir2, readFile as readFile13 } from "fs/promises";
|
|
7409
|
+
import { resolve as resolve16 } from "path";
|
|
7410
|
+
var writeLocks2 = /* @__PURE__ */ new Map();
|
|
7411
|
+
function projLock(slug, fn) {
|
|
7412
|
+
const key = `proj:${slug}`;
|
|
7413
|
+
const prev = writeLocks2.get(key) ?? Promise.resolve();
|
|
7414
|
+
const next = prev.then(fn);
|
|
7415
|
+
writeLocks2.set(key, next.then(() => {
|
|
7416
|
+
}, () => {
|
|
7417
|
+
}));
|
|
7418
|
+
return next;
|
|
7419
|
+
}
|
|
7420
|
+
function getProjectIdParam(value) {
|
|
7421
|
+
if (Array.isArray(value)) return value[0] ?? "";
|
|
7422
|
+
return value ?? "";
|
|
7423
|
+
}
|
|
7424
|
+
function params(req) {
|
|
7425
|
+
return req.params;
|
|
7426
|
+
}
|
|
7427
|
+
async function projectExists(projectsDir, slug) {
|
|
7428
|
+
return fileExists(resolve16(projectsDir, slug, "project.md"));
|
|
7429
|
+
}
|
|
7430
|
+
async function ensureProjectTodosDir(projectsDir, slug) {
|
|
7431
|
+
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
7432
|
+
try {
|
|
7433
|
+
await mkdir2(todosDir2, { recursive: false });
|
|
7434
|
+
} catch (err) {
|
|
7435
|
+
const code = err.code;
|
|
7436
|
+
if (code === "EEXIST") return;
|
|
7437
|
+
if (code === "ENOENT") {
|
|
7438
|
+
const e = new Error("PROJECT_GONE");
|
|
7439
|
+
e.code = "PROJECT_GONE";
|
|
7440
|
+
throw e;
|
|
7441
|
+
}
|
|
7442
|
+
throw err;
|
|
7443
|
+
}
|
|
7444
|
+
try {
|
|
7445
|
+
await mkdir2(resolve16(todosDir2, "archive"), { recursive: false });
|
|
7446
|
+
} catch (err) {
|
|
7447
|
+
const code = err.code;
|
|
7448
|
+
if (code === "EEXIST") return;
|
|
7449
|
+
if (code === "ENOENT") {
|
|
7450
|
+
const e = new Error("PROJECT_GONE");
|
|
7451
|
+
e.code = "PROJECT_GONE";
|
|
7452
|
+
throw e;
|
|
7453
|
+
}
|
|
7454
|
+
throw err;
|
|
7455
|
+
}
|
|
7456
|
+
}
|
|
7457
|
+
function notFound(res, slug) {
|
|
7458
|
+
res.status(404).json({ error: `Project "${slug}" not found` });
|
|
7459
|
+
}
|
|
7460
|
+
function createProjectTodosRouter(projectsDir, broadcast) {
|
|
7461
|
+
const router = Router6({ mergeParams: true });
|
|
7462
|
+
function broadcastUpdate(projectSlug) {
|
|
7463
|
+
broadcast({ type: "todos-updated", projectSlug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
7464
|
+
}
|
|
7465
|
+
function validateProjectId(req, res, next) {
|
|
7466
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
7467
|
+
if (!slug || !isValidSlug(slug)) {
|
|
7468
|
+
res.status(400).json({ error: `Invalid project slug: "${slug}"` });
|
|
7469
|
+
return;
|
|
7470
|
+
}
|
|
7471
|
+
next();
|
|
7472
|
+
}
|
|
7473
|
+
router.use(validateProjectId);
|
|
7474
|
+
router.get("/", async (req, res) => {
|
|
7475
|
+
try {
|
|
7476
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
7477
|
+
if (!await projectExists(projectsDir, slug)) {
|
|
7478
|
+
notFound(res, slug);
|
|
7479
|
+
return;
|
|
7480
|
+
}
|
|
7481
|
+
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
7482
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
7483
|
+
res.json({
|
|
7484
|
+
workspace: checklist.workspace,
|
|
7485
|
+
archiveInterval: checklist.archiveInterval,
|
|
7486
|
+
items: checklist.items,
|
|
7487
|
+
counts: computeCounts(checklist.items)
|
|
7488
|
+
});
|
|
7489
|
+
} catch (error) {
|
|
7490
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get todos" });
|
|
7491
|
+
}
|
|
7492
|
+
});
|
|
7493
|
+
router.post("/", async (req, res) => {
|
|
7494
|
+
try {
|
|
7495
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
7496
|
+
const { description, tags } = req.body;
|
|
7497
|
+
if (!description || typeof description !== "string") {
|
|
7498
|
+
res.status(400).json({ error: "description is required" });
|
|
7499
|
+
return;
|
|
7500
|
+
}
|
|
7501
|
+
if (!await projectExists(projectsDir, slug)) {
|
|
7502
|
+
notFound(res, slug);
|
|
7503
|
+
return;
|
|
7504
|
+
}
|
|
7505
|
+
const item = await projLock(slug, async () => {
|
|
7506
|
+
if (!await projectExists(projectsDir, slug)) return null;
|
|
7507
|
+
await ensureProjectTodosDir(projectsDir, slug);
|
|
7508
|
+
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
7509
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
7510
|
+
const existingIds = new Set(checklist.items.map((i) => i.id));
|
|
7511
|
+
const id = generateUniqueId(existingIds);
|
|
7512
|
+
const newItem = {
|
|
7513
|
+
id,
|
|
7514
|
+
description,
|
|
7515
|
+
status: "open",
|
|
7516
|
+
tags: Array.isArray(tags) ? tags : [],
|
|
7517
|
+
session: null
|
|
7518
|
+
};
|
|
7519
|
+
checklist.workspace = slug;
|
|
7520
|
+
checklist.items.push(newItem);
|
|
7521
|
+
await writeChecklist(todosDir2, checklist);
|
|
7522
|
+
return newItem;
|
|
7523
|
+
});
|
|
7524
|
+
if (!item) {
|
|
7525
|
+
notFound(res, slug);
|
|
7526
|
+
return;
|
|
7527
|
+
}
|
|
7528
|
+
broadcastUpdate(slug);
|
|
7529
|
+
res.status(201).json(item);
|
|
7530
|
+
} catch (error) {
|
|
7531
|
+
if (error.code === "PROJECT_GONE") {
|
|
7532
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
7533
|
+
return;
|
|
7534
|
+
}
|
|
7535
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to add todo" });
|
|
7536
|
+
}
|
|
7537
|
+
});
|
|
7538
|
+
router.post("/reorder", async (req, res) => {
|
|
7539
|
+
try {
|
|
7540
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
7541
|
+
const { ids } = req.body;
|
|
7542
|
+
if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string")) {
|
|
7543
|
+
res.status(400).json({ error: "ids must be an array of strings" });
|
|
7544
|
+
return;
|
|
7545
|
+
}
|
|
7546
|
+
if (!await projectExists(projectsDir, slug)) {
|
|
7547
|
+
notFound(res, slug);
|
|
7548
|
+
return;
|
|
7549
|
+
}
|
|
7550
|
+
const items = await projLock(slug, async () => {
|
|
7551
|
+
if (!await projectExists(projectsDir, slug)) return null;
|
|
7552
|
+
await ensureProjectTodosDir(projectsDir, slug);
|
|
7553
|
+
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
7554
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
7555
|
+
const itemMap = new Map(checklist.items.map((i) => [i.id, i]));
|
|
7556
|
+
const reordered = [];
|
|
7557
|
+
for (const id of ids) {
|
|
7558
|
+
const item = itemMap.get(id);
|
|
7559
|
+
if (item) {
|
|
7560
|
+
reordered.push(item);
|
|
7561
|
+
itemMap.delete(id);
|
|
7562
|
+
}
|
|
7563
|
+
}
|
|
7564
|
+
for (const item of itemMap.values()) reordered.push(item);
|
|
7565
|
+
checklist.workspace = slug;
|
|
7566
|
+
checklist.items = reordered;
|
|
7567
|
+
await writeChecklist(todosDir2, checklist);
|
|
7568
|
+
return reordered;
|
|
7569
|
+
});
|
|
7570
|
+
if (!items) {
|
|
7571
|
+
notFound(res, slug);
|
|
7572
|
+
return;
|
|
7573
|
+
}
|
|
7574
|
+
broadcastUpdate(slug);
|
|
7575
|
+
res.json({ items });
|
|
7576
|
+
} catch (error) {
|
|
7577
|
+
if (error.code === "PROJECT_GONE") {
|
|
7578
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
7579
|
+
return;
|
|
7580
|
+
}
|
|
7581
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to reorder todos" });
|
|
7582
|
+
}
|
|
7583
|
+
});
|
|
7584
|
+
router.get("/log", async (req, res) => {
|
|
7585
|
+
try {
|
|
7586
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
7587
|
+
if (!await projectExists(projectsDir, slug)) {
|
|
7588
|
+
notFound(res, slug);
|
|
7589
|
+
return;
|
|
7590
|
+
}
|
|
7591
|
+
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
7592
|
+
const log = await readLog(todosDir2, slug);
|
|
7593
|
+
res.json(log);
|
|
7594
|
+
} catch (error) {
|
|
7595
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get log" });
|
|
7596
|
+
}
|
|
7597
|
+
});
|
|
7598
|
+
router.post("/archive", async (req, res) => {
|
|
7599
|
+
try {
|
|
7600
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
7601
|
+
if (!await projectExists(projectsDir, slug)) {
|
|
7602
|
+
notFound(res, slug);
|
|
7603
|
+
return;
|
|
7604
|
+
}
|
|
7605
|
+
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
7606
|
+
await ensureProjectTodosDir(projectsDir, slug);
|
|
7607
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
7608
|
+
const log = await readLog(todosDir2, slug);
|
|
7609
|
+
const completedIds = new Set(
|
|
7610
|
+
checklist.items.filter((i) => i.status === "completed").map((i) => i.id)
|
|
7611
|
+
);
|
|
7612
|
+
if (completedIds.size === 0) {
|
|
7613
|
+
res.json({ archived: 0, message: "No completed items to archive" });
|
|
7614
|
+
return;
|
|
7615
|
+
}
|
|
7616
|
+
const toArchive = log.entries.filter(
|
|
7617
|
+
(e) => e.itemIds.every((id) => completedIds.has(id))
|
|
7618
|
+
);
|
|
7619
|
+
const archFile = archivePath(todosDir2, slug, checklist.archiveInterval);
|
|
7620
|
+
let archContent = "";
|
|
7621
|
+
if (await fileExists(archFile)) {
|
|
7622
|
+
archContent = await readFile13(archFile, "utf-8");
|
|
7623
|
+
archContent = archContent.trimEnd() + "\n\n";
|
|
7624
|
+
} else {
|
|
7625
|
+
archContent = `---
|
|
7626
|
+
workspace: ${slug}
|
|
7627
|
+
---
|
|
7628
|
+
|
|
7629
|
+
# Archive
|
|
7630
|
+
|
|
7631
|
+
`;
|
|
7632
|
+
}
|
|
7633
|
+
const completedItems = checklist.items.filter((i) => completedIds.has(i.id));
|
|
7634
|
+
for (const item of completedItems) {
|
|
7635
|
+
archContent += `- [x] ${item.description} ${item.tags.map((t) => `#${t}`).join(" ")} [t:${item.id}]
|
|
7636
|
+
`;
|
|
7637
|
+
}
|
|
7638
|
+
archContent += "\n";
|
|
7639
|
+
for (const entry of toArchive) {
|
|
7640
|
+
archContent += `### ${entry.timestamp} \u2014 ${entry.itemIds.map((i) => `t:${i}`).join(", ")}
|
|
7641
|
+
`;
|
|
7642
|
+
if (entry.items) archContent += `**Items:** ${entry.items}
|
|
7643
|
+
`;
|
|
7644
|
+
if (entry.session) archContent += `**Session:** ${entry.session}
|
|
7645
|
+
`;
|
|
7646
|
+
if (entry.branch) archContent += `**Branch:** ${entry.branch}
|
|
7647
|
+
`;
|
|
7648
|
+
if (entry.summary) archContent += `**Summary:** ${entry.summary}
|
|
7649
|
+
`;
|
|
7650
|
+
if (entry.blockers) archContent += `**Blockers:** ${entry.blockers}
|
|
7651
|
+
`;
|
|
7652
|
+
archContent += "\n";
|
|
7653
|
+
}
|
|
7654
|
+
await writeFileForce(archFile, archContent);
|
|
7655
|
+
checklist.workspace = slug;
|
|
7656
|
+
checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
|
|
7657
|
+
await writeChecklist(todosDir2, checklist);
|
|
7658
|
+
broadcastUpdate(slug);
|
|
7659
|
+
res.json({ archived: completedIds.size, logEntries: toArchive.length });
|
|
7660
|
+
} catch (error) {
|
|
7661
|
+
if (error.code === "PROJECT_GONE") {
|
|
7662
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
7663
|
+
return;
|
|
7664
|
+
}
|
|
7665
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to archive" });
|
|
7666
|
+
}
|
|
7667
|
+
});
|
|
7668
|
+
router.get("/log/:id", async (req, res) => {
|
|
7669
|
+
try {
|
|
7670
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
7671
|
+
if (!await projectExists(projectsDir, slug)) {
|
|
7672
|
+
notFound(res, slug);
|
|
7673
|
+
return;
|
|
7674
|
+
}
|
|
7675
|
+
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
7676
|
+
const log = await readLog(todosDir2, slug);
|
|
7677
|
+
const entries = log.entries.filter((e) => e.itemIds.includes(params(req).id ?? ""));
|
|
7678
|
+
res.json({ workspace: log.workspace, entries });
|
|
7679
|
+
} catch (error) {
|
|
7680
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get log" });
|
|
7681
|
+
}
|
|
7682
|
+
});
|
|
7683
|
+
router.get("/:id", async (req, res) => {
|
|
7684
|
+
try {
|
|
7685
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
7686
|
+
if (!await projectExists(projectsDir, slug)) {
|
|
7687
|
+
notFound(res, slug);
|
|
7688
|
+
return;
|
|
7689
|
+
}
|
|
7690
|
+
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
7691
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
7692
|
+
const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
|
|
7693
|
+
if (!item) {
|
|
7694
|
+
res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
|
|
7695
|
+
return;
|
|
7696
|
+
}
|
|
7697
|
+
const log = await readLog(todosDir2, slug);
|
|
7698
|
+
const logEntries = log.entries.filter((e) => e.itemIds.includes(params(req).id ?? ""));
|
|
7699
|
+
res.json({ ...item, log: logEntries });
|
|
7700
|
+
} catch (error) {
|
|
7701
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get todo" });
|
|
7702
|
+
}
|
|
7703
|
+
});
|
|
7704
|
+
router.patch("/:id", async (req, res) => {
|
|
7705
|
+
try {
|
|
7706
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
7707
|
+
if (!await projectExists(projectsDir, slug)) {
|
|
7708
|
+
notFound(res, slug);
|
|
7709
|
+
return;
|
|
7710
|
+
}
|
|
7711
|
+
const result = await projLock(slug, async () => {
|
|
7712
|
+
if (!await projectExists(projectsDir, slug)) return "gone";
|
|
7713
|
+
await ensureProjectTodosDir(projectsDir, slug);
|
|
7714
|
+
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
7715
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
7716
|
+
const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
|
|
7717
|
+
if (!item) return null;
|
|
7718
|
+
if (req.body.description !== void 0) item.description = req.body.description;
|
|
7719
|
+
if (Array.isArray(req.body.tags)) item.tags = req.body.tags;
|
|
7720
|
+
checklist.workspace = slug;
|
|
7721
|
+
await writeChecklist(todosDir2, checklist);
|
|
7722
|
+
return { ...item };
|
|
7723
|
+
});
|
|
7724
|
+
if (result === "gone") {
|
|
7725
|
+
notFound(res, slug);
|
|
7726
|
+
return;
|
|
7727
|
+
}
|
|
7728
|
+
if (!result) {
|
|
7729
|
+
res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
|
|
7730
|
+
return;
|
|
7731
|
+
}
|
|
7732
|
+
broadcastUpdate(slug);
|
|
7733
|
+
res.json(result);
|
|
7734
|
+
} catch (error) {
|
|
7735
|
+
if (error.code === "PROJECT_GONE") {
|
|
7736
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
7737
|
+
return;
|
|
7738
|
+
}
|
|
7739
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to update todo" });
|
|
7740
|
+
}
|
|
7741
|
+
});
|
|
7742
|
+
router.delete("/:id", async (req, res) => {
|
|
7743
|
+
try {
|
|
7744
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
7745
|
+
if (!await projectExists(projectsDir, slug)) {
|
|
7746
|
+
notFound(res, slug);
|
|
7747
|
+
return;
|
|
7748
|
+
}
|
|
7749
|
+
const deleted = await projLock(slug, async () => {
|
|
7750
|
+
if (!await projectExists(projectsDir, slug)) return "gone";
|
|
7751
|
+
await ensureProjectTodosDir(projectsDir, slug);
|
|
7752
|
+
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
7753
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
7754
|
+
const idx = checklist.items.findIndex((i) => i.id === (params(req).id ?? ""));
|
|
7755
|
+
if (idx === -1) return false;
|
|
7756
|
+
checklist.items.splice(idx, 1);
|
|
7757
|
+
checklist.workspace = slug;
|
|
7758
|
+
await writeChecklist(todosDir2, checklist);
|
|
7759
|
+
return true;
|
|
7760
|
+
});
|
|
7761
|
+
if (deleted === "gone") {
|
|
7762
|
+
notFound(res, slug);
|
|
7763
|
+
return;
|
|
7764
|
+
}
|
|
7765
|
+
if (!deleted) {
|
|
7766
|
+
res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
|
|
7767
|
+
return;
|
|
7768
|
+
}
|
|
7769
|
+
broadcastUpdate(slug);
|
|
7770
|
+
res.json({ deleted: params(req).id ?? "" });
|
|
7771
|
+
} catch (error) {
|
|
7772
|
+
if (error.code === "PROJECT_GONE") {
|
|
7773
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
7774
|
+
return;
|
|
7775
|
+
}
|
|
7776
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to delete todo" });
|
|
7777
|
+
}
|
|
7778
|
+
});
|
|
7779
|
+
router.post("/:id/start", async (req, res) => {
|
|
7780
|
+
try {
|
|
7781
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
7782
|
+
if (!await projectExists(projectsDir, slug)) {
|
|
7783
|
+
notFound(res, slug);
|
|
7784
|
+
return;
|
|
7785
|
+
}
|
|
7786
|
+
const result = await projLock(slug, async () => {
|
|
7787
|
+
if (!await projectExists(projectsDir, slug)) return { error: "gone" };
|
|
7788
|
+
await ensureProjectTodosDir(projectsDir, slug);
|
|
7789
|
+
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
7790
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
7791
|
+
const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
|
|
7792
|
+
if (!item) return { error: "not_found" };
|
|
7793
|
+
if (item.status === "in_progress") return { error: "conflict", session: item.session };
|
|
7794
|
+
item.status = "in_progress";
|
|
7795
|
+
item.session = req.body.session || null;
|
|
7796
|
+
checklist.workspace = slug;
|
|
7797
|
+
await writeChecklist(todosDir2, checklist);
|
|
7798
|
+
return { item: { ...item } };
|
|
7799
|
+
});
|
|
7800
|
+
if ("error" in result) {
|
|
7801
|
+
if (result.error === "gone") {
|
|
7802
|
+
notFound(res, slug);
|
|
7803
|
+
return;
|
|
7804
|
+
}
|
|
7805
|
+
if (result.error === "not_found") {
|
|
7806
|
+
res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
|
|
7807
|
+
return;
|
|
7808
|
+
}
|
|
7809
|
+
res.status(409).json({ error: `Todo is already in progress (session: ${result.session})` });
|
|
7810
|
+
return;
|
|
7811
|
+
}
|
|
7812
|
+
broadcastUpdate(slug);
|
|
7813
|
+
res.json(result.item);
|
|
7814
|
+
} catch (error) {
|
|
7815
|
+
if (error.code === "PROJECT_GONE") {
|
|
7816
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
7817
|
+
return;
|
|
7818
|
+
}
|
|
7819
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to start todo" });
|
|
7820
|
+
}
|
|
7821
|
+
});
|
|
7822
|
+
router.post("/:id/complete", async (req, res) => {
|
|
7823
|
+
try {
|
|
7824
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
7825
|
+
if (!await projectExists(projectsDir, slug)) {
|
|
7826
|
+
notFound(res, slug);
|
|
7827
|
+
return;
|
|
7828
|
+
}
|
|
7829
|
+
const result = await projLock(slug, async () => {
|
|
7830
|
+
if (!await projectExists(projectsDir, slug)) return "gone";
|
|
7831
|
+
await ensureProjectTodosDir(projectsDir, slug);
|
|
7832
|
+
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
7833
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
7834
|
+
const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
|
|
7835
|
+
if (!item) return null;
|
|
7836
|
+
item.status = "completed";
|
|
7837
|
+
item.session = null;
|
|
7838
|
+
checklist.workspace = slug;
|
|
7839
|
+
await writeChecklist(todosDir2, checklist);
|
|
7840
|
+
const entry = {
|
|
7841
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7842
|
+
itemIds: [item.id],
|
|
7843
|
+
items: item.description,
|
|
7844
|
+
session: req.body.session || null,
|
|
7845
|
+
branch: req.body.branch || null,
|
|
7846
|
+
summary: req.body.summary || "Completed.",
|
|
7847
|
+
blockers: null,
|
|
7848
|
+
status: null
|
|
7849
|
+
};
|
|
7850
|
+
await appendLogEntry2(todosDir2, slug, entry);
|
|
7851
|
+
return { ...item };
|
|
7852
|
+
});
|
|
7853
|
+
if (result === "gone") {
|
|
7854
|
+
notFound(res, slug);
|
|
7855
|
+
return;
|
|
7856
|
+
}
|
|
7857
|
+
if (!result) {
|
|
7858
|
+
res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
|
|
7859
|
+
return;
|
|
7860
|
+
}
|
|
7861
|
+
broadcastUpdate(slug);
|
|
7862
|
+
res.json(result);
|
|
7863
|
+
} catch (error) {
|
|
7864
|
+
if (error.code === "PROJECT_GONE") {
|
|
7865
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
7866
|
+
return;
|
|
7867
|
+
}
|
|
7868
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to complete todo" });
|
|
7869
|
+
}
|
|
7870
|
+
});
|
|
7871
|
+
router.post("/:id/block", async (req, res) => {
|
|
7872
|
+
try {
|
|
7873
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
7874
|
+
const reason = req.body.reason || null;
|
|
7875
|
+
if (!await projectExists(projectsDir, slug)) {
|
|
7876
|
+
notFound(res, slug);
|
|
7877
|
+
return;
|
|
7878
|
+
}
|
|
7879
|
+
const result = await projLock(slug, async () => {
|
|
7880
|
+
if (!await projectExists(projectsDir, slug)) return "gone";
|
|
7881
|
+
await ensureProjectTodosDir(projectsDir, slug);
|
|
7882
|
+
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
7883
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
7884
|
+
const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
|
|
7885
|
+
if (!item) return null;
|
|
7886
|
+
item.status = "blocked";
|
|
7887
|
+
item.session = null;
|
|
7888
|
+
checklist.workspace = slug;
|
|
7889
|
+
await writeChecklist(todosDir2, checklist);
|
|
7890
|
+
const entry = {
|
|
7891
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7892
|
+
itemIds: [item.id],
|
|
7893
|
+
items: item.description,
|
|
7894
|
+
session: req.body.session || null,
|
|
7895
|
+
branch: null,
|
|
7896
|
+
summary: reason || "Blocked.",
|
|
7897
|
+
blockers: reason,
|
|
7898
|
+
status: "blocked"
|
|
7899
|
+
};
|
|
7900
|
+
await appendLogEntry2(todosDir2, slug, entry);
|
|
7901
|
+
return { ...item };
|
|
7902
|
+
});
|
|
7903
|
+
if (result === "gone") {
|
|
7904
|
+
notFound(res, slug);
|
|
7905
|
+
return;
|
|
7906
|
+
}
|
|
7907
|
+
if (!result) {
|
|
7908
|
+
res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
|
|
7909
|
+
return;
|
|
7910
|
+
}
|
|
7911
|
+
broadcastUpdate(slug);
|
|
7912
|
+
res.json(result);
|
|
7913
|
+
} catch (error) {
|
|
7914
|
+
if (error.code === "PROJECT_GONE") {
|
|
7915
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
7916
|
+
return;
|
|
7917
|
+
}
|
|
7918
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to block todo" });
|
|
7919
|
+
}
|
|
7920
|
+
});
|
|
7921
|
+
router.post("/:id/reopen", async (req, res) => {
|
|
7922
|
+
try {
|
|
7923
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
7924
|
+
if (!await projectExists(projectsDir, slug)) {
|
|
7925
|
+
notFound(res, slug);
|
|
7926
|
+
return;
|
|
7927
|
+
}
|
|
7928
|
+
const result = await projLock(slug, async () => {
|
|
7929
|
+
if (!await projectExists(projectsDir, slug)) return "gone";
|
|
7930
|
+
await ensureProjectTodosDir(projectsDir, slug);
|
|
7931
|
+
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
7932
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
7933
|
+
const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
|
|
7934
|
+
if (!item) return null;
|
|
7935
|
+
item.status = "open";
|
|
7936
|
+
item.session = null;
|
|
7937
|
+
checklist.workspace = slug;
|
|
7938
|
+
await writeChecklist(todosDir2, checklist);
|
|
7939
|
+
return { ...item };
|
|
7940
|
+
});
|
|
7941
|
+
if (result === "gone") {
|
|
7942
|
+
notFound(res, slug);
|
|
7943
|
+
return;
|
|
7944
|
+
}
|
|
7945
|
+
if (!result) {
|
|
7946
|
+
res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
|
|
7947
|
+
return;
|
|
7948
|
+
}
|
|
7949
|
+
broadcastUpdate(slug);
|
|
7950
|
+
res.json(result);
|
|
7951
|
+
} catch (error) {
|
|
7952
|
+
if (error.code === "PROJECT_GONE") {
|
|
7953
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
7954
|
+
return;
|
|
7955
|
+
}
|
|
7956
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to reopen todo" });
|
|
7957
|
+
}
|
|
7958
|
+
});
|
|
7959
|
+
router.post("/:id/unblock", async (req, res) => {
|
|
7960
|
+
try {
|
|
7961
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
7962
|
+
if (!await projectExists(projectsDir, slug)) {
|
|
7963
|
+
notFound(res, slug);
|
|
7964
|
+
return;
|
|
7965
|
+
}
|
|
7966
|
+
const result = await projLock(slug, async () => {
|
|
7967
|
+
if (!await projectExists(projectsDir, slug)) return "gone";
|
|
7968
|
+
await ensureProjectTodosDir(projectsDir, slug);
|
|
7969
|
+
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
7970
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
7971
|
+
const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
|
|
7972
|
+
if (!item) return null;
|
|
7973
|
+
item.status = "open";
|
|
7974
|
+
item.session = null;
|
|
7975
|
+
checklist.workspace = slug;
|
|
7976
|
+
await writeChecklist(todosDir2, checklist);
|
|
7977
|
+
return { ...item };
|
|
7978
|
+
});
|
|
7979
|
+
if (result === "gone") {
|
|
7980
|
+
notFound(res, slug);
|
|
7981
|
+
return;
|
|
7982
|
+
}
|
|
7983
|
+
if (!result) {
|
|
7984
|
+
res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
|
|
7985
|
+
return;
|
|
7986
|
+
}
|
|
7987
|
+
broadcastUpdate(slug);
|
|
7988
|
+
res.json(result);
|
|
7989
|
+
} catch (error) {
|
|
7990
|
+
if (error.code === "PROJECT_GONE") {
|
|
7991
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
7992
|
+
return;
|
|
7993
|
+
}
|
|
7994
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to unblock todo" });
|
|
7995
|
+
}
|
|
7996
|
+
});
|
|
7997
|
+
return router;
|
|
7998
|
+
}
|
|
7999
|
+
|
|
8000
|
+
// src/dashboard/api-backup.ts
|
|
8001
|
+
init_config2();
|
|
8002
|
+
import { Router as Router7 } from "express";
|
|
8003
|
+
|
|
8004
|
+
// src/utils/github-backup.ts
|
|
8005
|
+
init_paths();
|
|
8006
|
+
init_fs();
|
|
8007
|
+
init_config2();
|
|
7091
8008
|
import { execFile as execFile2 } from "child_process";
|
|
7092
8009
|
import { promisify as promisify2 } from "util";
|
|
7093
|
-
import { cp, mkdtemp, rm as rm2, readFile as
|
|
7094
|
-
import { resolve as
|
|
8010
|
+
import { cp, mkdtemp, rm as rm2, readFile as readFile14, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename3 } from "fs/promises";
|
|
8011
|
+
import { resolve as resolve17, join as join2 } from "path";
|
|
7095
8012
|
import { tmpdir } from "os";
|
|
7096
8013
|
var exec2 = promisify2(execFile2);
|
|
7097
8014
|
var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
|
|
@@ -7131,7 +8048,7 @@ async function resolveCategoryPath(category) {
|
|
|
7131
8048
|
case "servers":
|
|
7132
8049
|
return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
|
|
7133
8050
|
case "config":
|
|
7134
|
-
return { sourcePath:
|
|
8051
|
+
return { sourcePath: resolve17(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
|
|
7135
8052
|
}
|
|
7136
8053
|
}
|
|
7137
8054
|
async function checkGitInstalled() {
|
|
@@ -7142,7 +8059,7 @@ async function checkGitInstalled() {
|
|
|
7142
8059
|
}
|
|
7143
8060
|
}
|
|
7144
8061
|
async function acquireLock() {
|
|
7145
|
-
const lockPath =
|
|
8062
|
+
const lockPath = resolve17(syntaurRoot(), LOCK_FILE_NAME);
|
|
7146
8063
|
await ensureDir(syntaurRoot());
|
|
7147
8064
|
try {
|
|
7148
8065
|
const handle = await open(lockPath, "wx");
|
|
@@ -7151,7 +8068,7 @@ async function acquireLock() {
|
|
|
7151
8068
|
return lockPath;
|
|
7152
8069
|
} catch (err) {
|
|
7153
8070
|
if (err.code === "EEXIST") {
|
|
7154
|
-
const pid = await
|
|
8071
|
+
const pid = await readFile14(lockPath, "utf-8").catch(() => "");
|
|
7155
8072
|
throw new Error(
|
|
7156
8073
|
`Backup operation already in progress (lock file at ${lockPath}, pid ${pid.trim() || "unknown"}). If stale, delete the file and retry.`
|
|
7157
8074
|
);
|
|
@@ -7189,7 +8106,7 @@ async function copyRecursive(src, dest) {
|
|
|
7189
8106
|
await ensureDir(dest);
|
|
7190
8107
|
await cp(src, dest, { recursive: true, force: true });
|
|
7191
8108
|
} else {
|
|
7192
|
-
await ensureDir(
|
|
8109
|
+
await ensureDir(resolve17(dest, ".."));
|
|
7193
8110
|
await cp(src, dest, { force: true });
|
|
7194
8111
|
}
|
|
7195
8112
|
}
|
|
@@ -7198,7 +8115,7 @@ function resolveCategoriesStrict(csv) {
|
|
|
7198
8115
|
return parseCategoriesStrict(parts);
|
|
7199
8116
|
}
|
|
7200
8117
|
async function readSanitizedConfig(configPath) {
|
|
7201
|
-
const content = await
|
|
8118
|
+
const content = await readFile14(configPath, "utf-8");
|
|
7202
8119
|
return content.replace(/^(\s*lastBackup:\s*).*$/m, "$1null").replace(/^(\s*lastRestore:\s*).*$/m, "$1null");
|
|
7203
8120
|
}
|
|
7204
8121
|
async function backupToGithub(overrides) {
|
|
@@ -7237,7 +8154,7 @@ async function backupToGithub(overrides) {
|
|
|
7237
8154
|
}
|
|
7238
8155
|
if (category === "config") {
|
|
7239
8156
|
const sanitized = await readSanitizedConfig(sourcePath);
|
|
7240
|
-
await ensureDir(
|
|
8157
|
+
await ensureDir(resolve17(destPath, ".."));
|
|
7241
8158
|
await writeFile4(destPath, sanitized, "utf-8");
|
|
7242
8159
|
} else {
|
|
7243
8160
|
await copyRecursive(sourcePath, destPath);
|
|
@@ -7291,7 +8208,7 @@ async function backupToGithub(overrides) {
|
|
|
7291
8208
|
}
|
|
7292
8209
|
async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
|
|
7293
8210
|
if (isFile) {
|
|
7294
|
-
await ensureDir(
|
|
8211
|
+
await ensureDir(resolve17(localPath, ".."));
|
|
7295
8212
|
await cp(repoSrcPath, localPath, { force: true });
|
|
7296
8213
|
return;
|
|
7297
8214
|
}
|
|
@@ -7392,7 +8309,7 @@ async function restoreFromGithub(overrides) {
|
|
|
7392
8309
|
}
|
|
7393
8310
|
async function getBackupStatus() {
|
|
7394
8311
|
const config = await readConfig();
|
|
7395
|
-
const lockPath =
|
|
8312
|
+
const lockPath = resolve17(syntaurRoot(), LOCK_FILE_NAME);
|
|
7396
8313
|
const locked = await fileExists(lockPath);
|
|
7397
8314
|
return {
|
|
7398
8315
|
repo: config.backup?.repo ?? null,
|
|
@@ -7405,7 +8322,7 @@ async function getBackupStatus() {
|
|
|
7405
8322
|
|
|
7406
8323
|
// src/dashboard/api-backup.ts
|
|
7407
8324
|
function createBackupRouter() {
|
|
7408
|
-
const router =
|
|
8325
|
+
const router = Router7();
|
|
7409
8326
|
router.get("/", async (_req, res) => {
|
|
7410
8327
|
try {
|
|
7411
8328
|
const status = await getBackupStatus();
|
|
@@ -7729,7 +8646,7 @@ function createDashboardServer(options) {
|
|
|
7729
8646
|
(async () => {
|
|
7730
8647
|
try {
|
|
7731
8648
|
const configResult = await migrateLegacyConfig(
|
|
7732
|
-
|
|
8649
|
+
resolve18(syntaurRoot(), "config.md")
|
|
7733
8650
|
);
|
|
7734
8651
|
const projectResult = await migrateLegacyProjectFiles(projectsDir);
|
|
7735
8652
|
const summary = summarizeMigration(projectResult, configResult);
|
|
@@ -7836,7 +8753,7 @@ function createDashboardServer(options) {
|
|
|
7836
8753
|
});
|
|
7837
8754
|
app.get("/api/workspaces", async (_req, res) => {
|
|
7838
8755
|
try {
|
|
7839
|
-
const result = await listWorkspaces(projectsDir);
|
|
8756
|
+
const result = await listWorkspaces(projectsDir, assignmentsDir);
|
|
7840
8757
|
res.json(result);
|
|
7841
8758
|
} catch (error) {
|
|
7842
8759
|
console.error("Error listing workspaces:", error);
|
|
@@ -7953,18 +8870,28 @@ function createDashboardServer(options) {
|
|
|
7953
8870
|
app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir));
|
|
7954
8871
|
app.use("/api/playbooks", createPlaybooksRouter(playbooksDir2));
|
|
7955
8872
|
app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
|
|
8873
|
+
app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir, broadcast));
|
|
7956
8874
|
app.use("/api/backup", createBackupRouter());
|
|
7957
8875
|
if (serveStaticUi && dashboardDistPath) {
|
|
7958
|
-
app.use(express.static(dashboardDistPath));
|
|
7959
|
-
app.get("{*path}", async (
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
}
|
|
8876
|
+
app.use("/assets", express.static(resolve18(dashboardDistPath, "assets")));
|
|
8877
|
+
app.get("{*path}", async (req, res) => {
|
|
8878
|
+
if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
|
|
8879
|
+
res.status(404).json({ error: "Not Found" });
|
|
8880
|
+
return;
|
|
8881
|
+
}
|
|
8882
|
+
const indexPath = resolve18(dashboardDistPath, "index.html");
|
|
8883
|
+
if (!await fileExists(indexPath)) {
|
|
7964
8884
|
res.status(503).send(
|
|
7965
8885
|
'Dashboard not built. Run "npm run build:dashboard" first.'
|
|
7966
8886
|
);
|
|
8887
|
+
return;
|
|
7967
8888
|
}
|
|
8889
|
+
res.sendFile(indexPath, (err) => {
|
|
8890
|
+
if (err) {
|
|
8891
|
+
console.error("Error sending dashboard index.html:", err);
|
|
8892
|
+
if (!res.headersSent) res.status(500).send("Dashboard load error");
|
|
8893
|
+
}
|
|
8894
|
+
});
|
|
7968
8895
|
});
|
|
7969
8896
|
}
|
|
7970
8897
|
let watcherHandle = null;
|
|
@@ -7990,7 +8917,7 @@ function createDashboardServer(options) {
|
|
|
7990
8917
|
}
|
|
7991
8918
|
});
|
|
7992
8919
|
server.listen(port, () => {
|
|
7993
|
-
const portFile =
|
|
8920
|
+
const portFile = resolve18(syntaurRoot(), "dashboard-port");
|
|
7994
8921
|
writeFile5(portFile, String(port), "utf-8").catch(() => {
|
|
7995
8922
|
});
|
|
7996
8923
|
resolvePromise();
|
|
@@ -8007,7 +8934,7 @@ function createDashboardServer(options) {
|
|
|
8007
8934
|
client.terminate();
|
|
8008
8935
|
}
|
|
8009
8936
|
clients.clear();
|
|
8010
|
-
const portFile =
|
|
8937
|
+
const portFile = resolve18(syntaurRoot(), "dashboard-port");
|
|
8011
8938
|
await unlink4(portFile).catch(() => {
|
|
8012
8939
|
});
|
|
8013
8940
|
server.closeAllConnections?.();
|