syntaur 0.4.4 → 0.4.5
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-ZY1FrcKw.js} +1 -1
- package/dashboard/dist/assets/{_baseUniq-C-qj6E4l.js → _baseUniq-C7xZB6ea.js} +1 -1
- package/dashboard/dist/assets/{arc-Dn5BIqMa.js → arc-CPtDVk1A.js} +1 -1
- package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-D5D0K7rY.js → architectureDiagram-2XIMDMQ5-Dr5rnxwf.js} +1 -1
- package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-DVmYPMbu.js → blockDiagram-WCTKOSBZ-SsDboTb2.js} +1 -1
- package/dashboard/dist/assets/{c4Diagram-IC4MRINW-BEasxbnl.js → c4Diagram-IC4MRINW-CZqjXmV0.js} +1 -1
- package/dashboard/dist/assets/channel-ejDeCb7i.js +1 -0
- package/dashboard/dist/assets/{chunk-4BX2VUAB-LDIrtI5E.js → chunk-4BX2VUAB-BYskd63Z.js} +1 -1
- package/dashboard/dist/assets/{chunk-55IACEB6-CaEBUJYu.js → chunk-55IACEB6-CWLImr1E.js} +1 -1
- package/dashboard/dist/assets/{chunk-FMBD7UC4-B-GjCpdr.js → chunk-FMBD7UC4-fQXSIXhy.js} +1 -1
- package/dashboard/dist/assets/{chunk-JSJVCQXG-BLVVcezm.js → chunk-JSJVCQXG-DJXtEexG.js} +1 -1
- package/dashboard/dist/assets/{chunk-KX2RTZJC-DqCNEw4h.js → chunk-KX2RTZJC-C0ivaZ1M.js} +1 -1
- package/dashboard/dist/assets/{chunk-NQ4KR5QH-BCPbFf5I.js → chunk-NQ4KR5QH-DlwLjqC5.js} +1 -1
- package/dashboard/dist/assets/{chunk-QZHKN3VN-Ci0C85q_.js → chunk-QZHKN3VN-DBEYrRqx.js} +1 -1
- package/dashboard/dist/assets/{chunk-WL4C6EOR-VVhAMMYU.js → chunk-WL4C6EOR-BQyYCp1z.js} +1 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-BW_esmsF.js +1 -0
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BW_esmsF.js +1 -0
- package/dashboard/dist/assets/clone-y13L00zF.js +1 -0
- package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-CO9uwgYO.js → cose-bilkent-S5V4N54A-CAYzelqd.js} +1 -1
- package/dashboard/dist/assets/{dagre-KLK3FWXG-bwLLXcL4.js → dagre-KLK3FWXG-xpTJb8E7.js} +1 -1
- package/dashboard/dist/assets/{diagram-E7M64L7V-RuS5R6V1.js → diagram-E7M64L7V-DRDjskHd.js} +1 -1
- package/dashboard/dist/assets/{diagram-IFDJBPK2-BQDJAHQd.js → diagram-IFDJBPK2-B1r1ZXm3.js} +1 -1
- package/dashboard/dist/assets/{diagram-P4PSJMXO-yLEsgzE5.js → diagram-P4PSJMXO-BeE6-ZUH.js} +1 -1
- package/dashboard/dist/assets/{erDiagram-INFDFZHY-na6dUhY0.js → erDiagram-INFDFZHY-BG6KKBm1.js} +1 -1
- package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-BIcrzwJR.js → flowDiagram-PKNHOUZH-CSFv3RpZ.js} +1 -1
- package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-DHWRJn-D.js → ganttDiagram-A5KZAMGK-Off4zK-k.js} +1 -1
- package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-LGxDjL71.js → gitGraphDiagram-K3NZZRJ6-Msv_4mYB.js} +1 -1
- package/dashboard/dist/assets/{graph-BUqNu277.js → graph-rPPnNEMq.js} +1 -1
- package/dashboard/dist/assets/index-Bu6ma6my.css +1 -0
- package/dashboard/dist/assets/{index-D-fepllQ.js → index-D_uE8gHg.js} +87 -87
- package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-DidoA2hb.js → infoDiagram-LFFYTUFH-CeBirxFd.js} +1 -1
- package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-CdlZkbhV.js → ishikawaDiagram-PHBUUO56-BZ_w8PcD.js} +1 -1
- package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-luhcz_gn.js → journeyDiagram-4ABVD52K-3FqMPEfs.js} +1 -1
- package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-Coidw9XE.js → kanban-definition-K7BYSVSG-BlCmFkEx.js} +1 -1
- package/dashboard/dist/assets/{layout-_aBAAleE.js → layout-C50nYMhx.js} +1 -1
- package/dashboard/dist/assets/{linear-D8mFnDSx.js → linear-D2Cjmh1X.js} +1 -1
- package/dashboard/dist/assets/{mermaid.core-BpP2keU-.js → mermaid.core-BXFuNYa4.js} +4 -4
- package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-LjvrTe2z.js → mindmap-definition-YRQLILUH-CowIfM07.js} +1 -1
- package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CkA2iU6e.js → pieDiagram-SKSYHLDU-DG5IQcKl.js} +1 -1
- package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-BRmhKHQG.js → quadrantDiagram-337W2JSQ-DKD46CSX.js} +1 -1
- package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BYCQ4uFX.js → requirementDiagram-Z7DCOOCP-Dd-qX6Ul.js} +1 -1
- package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-C8SVk50M.js → sankeyDiagram-WA2Y5GQK-CDAylULt.js} +1 -1
- package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-CbA_2lnP.js → sequenceDiagram-2WXFIKYE-D-5Jq5jy.js} +1 -1
- package/dashboard/dist/assets/{stateDiagram-RAJIS63D-D6ZtjAHE.js → stateDiagram-RAJIS63D-BmBOa8i0.js} +1 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-Dsk6o9iT.js +1 -0
- package/dashboard/dist/assets/{timeline-definition-YZTLITO2-B2Uf-emK.js → timeline-definition-YZTLITO2-C9Sdg7du.js} +1 -1
- package/dashboard/dist/assets/{treemap-KZPCXAKY-CYnFKsuJ.js → treemap-KZPCXAKY-D-PDxUdK.js} +1 -1
- package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-Cnj0qiDO.js → vennDiagram-LZ73GAT5-CBO5f6MT.js} +1 -1
- package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-BJy2mbL9.js → xychartDiagram-JWTSCODW-4xZrquqP.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/dashboard/server.js +601 -285
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +567 -147
- 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-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
|
@@ -622,6 +622,27 @@ var init_fs_migration = __esm({
|
|
|
622
622
|
// src/utils/config.ts
|
|
623
623
|
import { readFile as readFile3 } from "fs/promises";
|
|
624
624
|
import { resolve as resolve4, isAbsolute } from "path";
|
|
625
|
+
function cloneDefaultConfig() {
|
|
626
|
+
return {
|
|
627
|
+
...DEFAULT_CONFIG,
|
|
628
|
+
onboarding: { ...DEFAULT_CONFIG.onboarding },
|
|
629
|
+
agentDefaults: { ...DEFAULT_CONFIG.agentDefaults },
|
|
630
|
+
integrations: { ...DEFAULT_CONFIG.integrations },
|
|
631
|
+
backup: DEFAULT_CONFIG.backup ? { ...DEFAULT_CONFIG.backup } : null,
|
|
632
|
+
statuses: DEFAULT_CONFIG.statuses ? {
|
|
633
|
+
statuses: DEFAULT_CONFIG.statuses.statuses.map((s) => ({ ...s })),
|
|
634
|
+
order: [...DEFAULT_CONFIG.statuses.order],
|
|
635
|
+
transitions: DEFAULT_CONFIG.statuses.transitions.map((t) => ({ ...t }))
|
|
636
|
+
} : null,
|
|
637
|
+
types: DEFAULT_CONFIG.types ? {
|
|
638
|
+
definitions: DEFAULT_CONFIG.types.definitions.map((d) => ({ ...d })),
|
|
639
|
+
default: DEFAULT_CONFIG.types.default
|
|
640
|
+
} : null,
|
|
641
|
+
playbooks: {
|
|
642
|
+
disabled: [...DEFAULT_CONFIG.playbooks.disabled]
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
}
|
|
625
646
|
function parseFrontmatter(content) {
|
|
626
647
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
627
648
|
if (!match) return {};
|
|
@@ -774,6 +795,82 @@ function serializeBackupConfig(backup) {
|
|
|
774
795
|
lines.push(` lastRestore: ${backup.lastRestore ?? "null"}`);
|
|
775
796
|
return lines.join("\n");
|
|
776
797
|
}
|
|
798
|
+
function serializePlaybooksConfig(playbooks) {
|
|
799
|
+
if (!playbooks.disabled || playbooks.disabled.length === 0) {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
const lines = ["playbooks:", " disabled:"];
|
|
803
|
+
for (const slug of playbooks.disabled) {
|
|
804
|
+
lines.push(` - ${slug}`);
|
|
805
|
+
}
|
|
806
|
+
return lines.join("\n");
|
|
807
|
+
}
|
|
808
|
+
function parsePlaybooksConfig(fmBlock) {
|
|
809
|
+
const blockStart = fmBlock.match(/^playbooks:\s*$/m);
|
|
810
|
+
if (!blockStart) {
|
|
811
|
+
return { disabled: [] };
|
|
812
|
+
}
|
|
813
|
+
const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
|
|
814
|
+
const remaining = fmBlock.slice(startIdx).split("\n");
|
|
815
|
+
const disabled = [];
|
|
816
|
+
let currentSection = null;
|
|
817
|
+
for (const line of remaining) {
|
|
818
|
+
const trimmed = line.trimStart();
|
|
819
|
+
const indent = line.length - trimmed.length;
|
|
820
|
+
if (indent === 0 && trimmed.length > 0) break;
|
|
821
|
+
if (trimmed === "") continue;
|
|
822
|
+
if (indent === 2 && trimmed.startsWith("disabled:")) {
|
|
823
|
+
currentSection = "disabled";
|
|
824
|
+
const afterColon = trimmed.slice("disabled:".length).trim();
|
|
825
|
+
if (afterColon === "[]" || afterColon === "") {
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
if (currentSection === "disabled" && indent >= 4 && trimmed.startsWith("- ")) {
|
|
831
|
+
const raw = trimmed.slice(2).trim().replace(/^["']|["']$/g, "");
|
|
832
|
+
if (raw.length === 0) continue;
|
|
833
|
+
if (/\s/.test(raw)) {
|
|
834
|
+
console.warn(`Warning: config.md playbooks.disabled entry "${raw}" contains whitespace, ignoring`);
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
disabled.push(raw);
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return { disabled };
|
|
842
|
+
}
|
|
843
|
+
async function updatePlaybooksConfig(playbooks) {
|
|
844
|
+
const configPath = resolve4(syntaurRoot(), "config.md");
|
|
845
|
+
const current = (await readConfig()).playbooks;
|
|
846
|
+
const nextPlaybooks = {
|
|
847
|
+
disabled: Array.from(new Set(playbooks.disabled ?? current.disabled))
|
|
848
|
+
};
|
|
849
|
+
const playbooksBlock = serializePlaybooksConfig(nextPlaybooks);
|
|
850
|
+
const existing = await fileExists(configPath) ? await readFile3(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
851
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
852
|
+
if (!fmMatch) {
|
|
853
|
+
const bodyBlock = playbooksBlock ? `${playbooksBlock}
|
|
854
|
+
` : "";
|
|
855
|
+
const content = `---
|
|
856
|
+
version: "2.0"
|
|
857
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
858
|
+
${bodyBlock}---
|
|
859
|
+
${existing}`;
|
|
860
|
+
await writeFileForce(configPath, content);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const fmBlock = fmMatch[2];
|
|
864
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
865
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "playbooks");
|
|
866
|
+
const newFm = playbooksBlock ? `${cleanedFm}
|
|
867
|
+
${playbooksBlock}`.replace(/^\n+/, "") : cleanedFm;
|
|
868
|
+
const normalizedFm = newFm.replace(/\n+$/, "");
|
|
869
|
+
const newContent = `---
|
|
870
|
+
${normalizedFm}
|
|
871
|
+
---${afterFrontmatter}`;
|
|
872
|
+
await writeFileForce(configPath, newContent);
|
|
873
|
+
}
|
|
777
874
|
function stripTopLevelBlock(fmBlock, key) {
|
|
778
875
|
const blockStart = fmBlock.match(new RegExp(`^${key}:\\s*$`, "m"));
|
|
779
876
|
if (!blockStart) {
|
|
@@ -915,7 +1012,7 @@ ${normalizedFm}
|
|
|
915
1012
|
async function readConfig() {
|
|
916
1013
|
const configPath = resolve4(syntaurRoot(), "config.md");
|
|
917
1014
|
if (!await fileExists(configPath)) {
|
|
918
|
-
return
|
|
1015
|
+
return cloneDefaultConfig();
|
|
919
1016
|
}
|
|
920
1017
|
if (!migratedConfigPaths.has(configPath)) {
|
|
921
1018
|
migratedConfigPaths.add(configPath);
|
|
@@ -925,7 +1022,7 @@ async function readConfig() {
|
|
|
925
1022
|
const fm = parseFrontmatter(content);
|
|
926
1023
|
if (Object.keys(fm).length === 0) {
|
|
927
1024
|
console.warn("Warning: ~/.syntaur/config.md has malformed frontmatter, using defaults");
|
|
928
|
-
return
|
|
1025
|
+
return cloneDefaultConfig();
|
|
929
1026
|
}
|
|
930
1027
|
let projectDir = fm["defaultProjectDir"] ? expandHome(String(fm["defaultProjectDir"])) : DEFAULT_CONFIG.defaultProjectDir;
|
|
931
1028
|
if (!isAbsolute(projectDir)) {
|
|
@@ -934,6 +1031,7 @@ async function readConfig() {
|
|
|
934
1031
|
);
|
|
935
1032
|
projectDir = DEFAULT_CONFIG.defaultProjectDir;
|
|
936
1033
|
}
|
|
1034
|
+
const fmBlock = content.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
|
|
937
1035
|
return {
|
|
938
1036
|
version: fm["version"] || DEFAULT_CONFIG.version,
|
|
939
1037
|
defaultProjectDir: projectDir,
|
|
@@ -965,7 +1063,8 @@ async function readConfig() {
|
|
|
965
1063
|
lastRestore: fm["backup.lastRestore"] && fm["backup.lastRestore"] !== "null" ? fm["backup.lastRestore"] : null
|
|
966
1064
|
} : null,
|
|
967
1065
|
statuses: parseStatusConfig(content),
|
|
968
|
-
types: null
|
|
1066
|
+
types: null,
|
|
1067
|
+
playbooks: parsePlaybooksConfig(fmBlock)
|
|
969
1068
|
};
|
|
970
1069
|
}
|
|
971
1070
|
var DEFAULT_CONFIG, migratedConfigPaths;
|
|
@@ -993,7 +1092,10 @@ var init_config2 = __esm({
|
|
|
993
1092
|
},
|
|
994
1093
|
backup: null,
|
|
995
1094
|
statuses: null,
|
|
996
|
-
types: null
|
|
1095
|
+
types: null,
|
|
1096
|
+
playbooks: {
|
|
1097
|
+
disabled: []
|
|
1098
|
+
}
|
|
997
1099
|
};
|
|
998
1100
|
migratedConfigPaths = /* @__PURE__ */ new Set();
|
|
999
1101
|
}
|
|
@@ -1128,6 +1230,7 @@ function parseAssignmentFull(fileContent) {
|
|
|
1128
1230
|
slug: getField(fm, "slug") ?? "",
|
|
1129
1231
|
title: getField(fm, "title") ?? "",
|
|
1130
1232
|
project: getField(fm, "project"),
|
|
1233
|
+
workspaceGroup: getField(fm, "workspaceGroup"),
|
|
1131
1234
|
type: getField(fm, "type"),
|
|
1132
1235
|
status: getField(fm, "status") ?? "pending",
|
|
1133
1236
|
priority: getField(fm, "priority") ?? "medium",
|
|
@@ -1284,47 +1387,160 @@ var init_parser = __esm({
|
|
|
1284
1387
|
}
|
|
1285
1388
|
});
|
|
1286
1389
|
|
|
1287
|
-
// src/utils/
|
|
1390
|
+
// src/utils/playbooks.ts
|
|
1288
1391
|
import { resolve as resolve5 } from "path";
|
|
1289
1392
|
import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
|
|
1393
|
+
function isVisiblePlaybookFile(name, isFile) {
|
|
1394
|
+
return isFile && name.endsWith(".md") && !name.startsWith("_") && name !== "manifest.md";
|
|
1395
|
+
}
|
|
1396
|
+
async function resolvePlaybookSlug(playbooksDir2, slug) {
|
|
1397
|
+
if (!await fileExists(playbooksDir2)) return null;
|
|
1398
|
+
const entries = await readdir2(playbooksDir2, { withFileTypes: true });
|
|
1399
|
+
let filenameStemFallback = null;
|
|
1400
|
+
for (const entry of entries) {
|
|
1401
|
+
if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
|
|
1402
|
+
const filePath = resolve5(playbooksDir2, entry.name);
|
|
1403
|
+
const raw = await readFile4(filePath, "utf-8");
|
|
1404
|
+
const parsed = parsePlaybook(raw);
|
|
1405
|
+
const canonical = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
1406
|
+
if (canonical === slug) {
|
|
1407
|
+
return { filename: entry.name, slug: canonical, parsed };
|
|
1408
|
+
}
|
|
1409
|
+
if (!parsed.slug && entry.name.replace(/\.md$/, "") === slug) {
|
|
1410
|
+
filenameStemFallback = { filename: entry.name, slug: canonical, parsed };
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return filenameStemFallback;
|
|
1414
|
+
}
|
|
1415
|
+
async function setPlaybookEnabled(playbooksDir2, slug, enabled) {
|
|
1416
|
+
const resolved = await resolvePlaybookSlug(playbooksDir2, slug);
|
|
1417
|
+
if (!resolved) {
|
|
1418
|
+
throw new Error(`Playbook "${slug}" not found in ${playbooksDir2}`);
|
|
1419
|
+
}
|
|
1420
|
+
const config = await readConfig();
|
|
1421
|
+
const disabledSet = new Set(config.playbooks.disabled);
|
|
1422
|
+
const wasDisabled = disabledSet.has(resolved.slug);
|
|
1423
|
+
const shouldBeDisabled = !enabled;
|
|
1424
|
+
if (wasDisabled === shouldBeDisabled) {
|
|
1425
|
+
return { slug: resolved.slug, enabled, changed: false };
|
|
1426
|
+
}
|
|
1427
|
+
if (shouldBeDisabled) {
|
|
1428
|
+
disabledSet.add(resolved.slug);
|
|
1429
|
+
} else {
|
|
1430
|
+
disabledSet.delete(resolved.slug);
|
|
1431
|
+
}
|
|
1432
|
+
await updatePlaybooksConfig({ disabled: Array.from(disabledSet).sort() });
|
|
1433
|
+
await rebuildPlaybookManifest(playbooksDir2);
|
|
1434
|
+
return { slug: resolved.slug, enabled, changed: true };
|
|
1435
|
+
}
|
|
1436
|
+
async function removeFromDisabledList(slug) {
|
|
1437
|
+
const config = await readConfig();
|
|
1438
|
+
if (!config.playbooks.disabled.includes(slug)) return;
|
|
1439
|
+
await updatePlaybooksConfig({
|
|
1440
|
+
disabled: config.playbooks.disabled.filter((s) => s !== slug)
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
async function rebuildPlaybookManifest(playbooksDir2) {
|
|
1444
|
+
if (!await fileExists(playbooksDir2)) return;
|
|
1445
|
+
const config = await readConfig();
|
|
1446
|
+
const disabledSet = new Set(config.playbooks.disabled);
|
|
1447
|
+
const entries = await readdir2(playbooksDir2, { withFileTypes: true });
|
|
1448
|
+
const rows = [];
|
|
1449
|
+
for (const entry of entries) {
|
|
1450
|
+
if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
|
|
1451
|
+
const raw = await readFile4(resolve5(playbooksDir2, entry.name), "utf-8");
|
|
1452
|
+
const parsed = parsePlaybook(raw);
|
|
1453
|
+
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
1454
|
+
if (disabledSet.has(slug)) continue;
|
|
1455
|
+
rows.push({
|
|
1456
|
+
name: parsed.name || slug,
|
|
1457
|
+
slug,
|
|
1458
|
+
description: parsed.description,
|
|
1459
|
+
whenToUse: parsed.whenToUse
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
1463
|
+
const timestamp = nowTimestamp();
|
|
1464
|
+
const lines = [
|
|
1465
|
+
"---",
|
|
1466
|
+
`generated: "${timestamp}"`,
|
|
1467
|
+
`total: ${rows.length}`,
|
|
1468
|
+
"---",
|
|
1469
|
+
"",
|
|
1470
|
+
"# Playbooks",
|
|
1471
|
+
"",
|
|
1472
|
+
"Behavioral rules for AI agents. Read and follow all playbooks before starting work.",
|
|
1473
|
+
""
|
|
1474
|
+
];
|
|
1475
|
+
for (const row of rows) {
|
|
1476
|
+
lines.push(`- **[${row.name}](${row.slug}.md)** \u2014 ${row.description}`);
|
|
1477
|
+
if (row.whenToUse) {
|
|
1478
|
+
lines.push(` _When to use: ${row.whenToUse}_`);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
lines.push("");
|
|
1482
|
+
await writeFileForce(resolve5(playbooksDir2, "manifest.md"), lines.join("\n"));
|
|
1483
|
+
}
|
|
1484
|
+
var init_playbooks = __esm({
|
|
1485
|
+
"src/utils/playbooks.ts"() {
|
|
1486
|
+
"use strict";
|
|
1487
|
+
init_fs();
|
|
1488
|
+
init_parser();
|
|
1489
|
+
init_timestamp();
|
|
1490
|
+
init_config2();
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
// src/utils/assignment-resolver.ts
|
|
1495
|
+
import { resolve as resolve6 } from "path";
|
|
1496
|
+
import { readdir as readdir3, readFile as readFile5 } from "fs/promises";
|
|
1290
1497
|
async function resolveAssignmentById(projectsDir, assignmentsDir, id) {
|
|
1291
1498
|
let standaloneMatch = null;
|
|
1292
1499
|
let projectMatch = null;
|
|
1293
|
-
const standaloneDir =
|
|
1294
|
-
const standalonePath =
|
|
1500
|
+
const standaloneDir = resolve6(assignmentsDir, id);
|
|
1501
|
+
const standalonePath = resolve6(standaloneDir, "assignment.md");
|
|
1295
1502
|
if (await fileExists(standalonePath)) {
|
|
1503
|
+
let workspaceGroup = null;
|
|
1504
|
+
try {
|
|
1505
|
+
const content = await readFile5(standalonePath, "utf-8");
|
|
1506
|
+
const [fm] = extractFrontmatter2(content);
|
|
1507
|
+
workspaceGroup = getField(fm, "workspaceGroup");
|
|
1508
|
+
} catch {
|
|
1509
|
+
}
|
|
1296
1510
|
standaloneMatch = {
|
|
1297
1511
|
assignmentDir: standaloneDir,
|
|
1298
1512
|
projectSlug: null,
|
|
1299
1513
|
assignmentSlug: id,
|
|
1300
1514
|
id,
|
|
1301
|
-
standalone: true
|
|
1515
|
+
standalone: true,
|
|
1516
|
+
workspaceGroup
|
|
1302
1517
|
};
|
|
1303
1518
|
}
|
|
1304
1519
|
if (await fileExists(projectsDir)) {
|
|
1305
1520
|
try {
|
|
1306
|
-
const projects = await
|
|
1521
|
+
const projects = await readdir3(projectsDir, { withFileTypes: true });
|
|
1307
1522
|
for (const p of projects) {
|
|
1308
1523
|
if (!p.isDirectory()) continue;
|
|
1309
1524
|
if (p.name.startsWith(".") || p.name.startsWith("_")) continue;
|
|
1310
|
-
const assignmentsPath =
|
|
1525
|
+
const assignmentsPath = resolve6(projectsDir, p.name, "assignments");
|
|
1311
1526
|
if (!await fileExists(assignmentsPath)) continue;
|
|
1312
|
-
const entries = await
|
|
1527
|
+
const entries = await readdir3(assignmentsPath, { withFileTypes: true });
|
|
1313
1528
|
for (const a of entries) {
|
|
1314
1529
|
if (!a.isDirectory()) continue;
|
|
1315
|
-
const aPath =
|
|
1530
|
+
const aPath = resolve6(assignmentsPath, a.name, "assignment.md");
|
|
1316
1531
|
if (!await fileExists(aPath)) continue;
|
|
1317
1532
|
try {
|
|
1318
|
-
const content = await
|
|
1533
|
+
const content = await readFile5(aPath, "utf-8");
|
|
1319
1534
|
const [fm] = extractFrontmatter2(content);
|
|
1320
1535
|
const fileId = getField(fm, "id");
|
|
1321
1536
|
if (fileId === id) {
|
|
1322
1537
|
projectMatch = {
|
|
1323
|
-
assignmentDir:
|
|
1538
|
+
assignmentDir: resolve6(assignmentsPath, a.name),
|
|
1324
1539
|
projectSlug: p.name,
|
|
1325
1540
|
assignmentSlug: a.name,
|
|
1326
1541
|
id,
|
|
1327
|
-
standalone: false
|
|
1542
|
+
standalone: false,
|
|
1543
|
+
workspaceGroup: null
|
|
1328
1544
|
};
|
|
1329
1545
|
break;
|
|
1330
1546
|
}
|
|
@@ -1700,8 +1916,18 @@ var init_help = __esm({
|
|
|
1700
1916
|
},
|
|
1701
1917
|
{
|
|
1702
1918
|
command: "syntaur list-playbooks",
|
|
1703
|
-
description: "List
|
|
1704
|
-
example: "syntaur list-playbooks"
|
|
1919
|
+
description: "List playbooks in the Syntaur home directory. Disabled playbooks are excluded by default; pass --all to include them with a (disabled) tag.",
|
|
1920
|
+
example: "syntaur list-playbooks --all"
|
|
1921
|
+
},
|
|
1922
|
+
{
|
|
1923
|
+
command: "syntaur enable-playbook",
|
|
1924
|
+
description: "Re-enable a previously-disabled playbook so agents load it again. Updates config.md and rebuilds manifest.md.",
|
|
1925
|
+
example: "syntaur enable-playbook commit-discipline"
|
|
1926
|
+
},
|
|
1927
|
+
{
|
|
1928
|
+
command: "syntaur disable-playbook",
|
|
1929
|
+
description: "Disable a playbook so agents no longer list or load it. Playbook file is untouched; state is tracked in config.md.",
|
|
1930
|
+
example: "syntaur disable-playbook commit-discipline"
|
|
1705
1931
|
}
|
|
1706
1932
|
];
|
|
1707
1933
|
WORKFLOW = [
|
|
@@ -1768,8 +1994,8 @@ var init_help = __esm({
|
|
|
1768
1994
|
});
|
|
1769
1995
|
|
|
1770
1996
|
// src/dashboard/servers.ts
|
|
1771
|
-
import { readdir as
|
|
1772
|
-
import { resolve as
|
|
1997
|
+
import { readdir as readdir4, readFile as readFile6, unlink } from "fs/promises";
|
|
1998
|
+
import { resolve as resolve7 } from "path";
|
|
1773
1999
|
function sanitizeSessionName(name) {
|
|
1774
2000
|
return name.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
1775
2001
|
}
|
|
@@ -1817,18 +2043,18 @@ async function registerSession(dir, rawName) {
|
|
|
1817
2043
|
lastRefreshed: now,
|
|
1818
2044
|
overrides: {}
|
|
1819
2045
|
});
|
|
1820
|
-
await writeFileForce(
|
|
2046
|
+
await writeFileForce(resolve7(dir, `${name}.md`), content);
|
|
1821
2047
|
return name;
|
|
1822
2048
|
}
|
|
1823
2049
|
async function listSessionFiles(dir) {
|
|
1824
2050
|
if (!await fileExists(dir)) return [];
|
|
1825
|
-
const entries = await
|
|
2051
|
+
const entries = await readdir4(dir);
|
|
1826
2052
|
return entries.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
|
|
1827
2053
|
}
|
|
1828
2054
|
async function readSessionFile(dir, name) {
|
|
1829
|
-
const filePath =
|
|
2055
|
+
const filePath = resolve7(dir, `${sanitizeSessionName(name)}.md`);
|
|
1830
2056
|
if (!await fileExists(filePath)) return null;
|
|
1831
|
-
const raw = await
|
|
2057
|
+
const raw = await readFile6(filePath, "utf-8");
|
|
1832
2058
|
const [frontmatter] = extractFrontmatter2(raw);
|
|
1833
2059
|
if (!frontmatter) return null;
|
|
1834
2060
|
const session = getField(frontmatter, "session") ?? name;
|
|
@@ -1868,7 +2094,7 @@ async function readSessionFile(dir, name) {
|
|
|
1868
2094
|
};
|
|
1869
2095
|
}
|
|
1870
2096
|
async function removeSession(dir, name) {
|
|
1871
|
-
const filePath =
|
|
2097
|
+
const filePath = resolve7(dir, `${sanitizeSessionName(name)}.md`);
|
|
1872
2098
|
if (await fileExists(filePath)) {
|
|
1873
2099
|
await unlink(filePath);
|
|
1874
2100
|
}
|
|
@@ -1877,7 +2103,7 @@ async function updateLastRefreshed(dir, name) {
|
|
|
1877
2103
|
const data = await readSessionFile(dir, name);
|
|
1878
2104
|
if (!data) return;
|
|
1879
2105
|
const content = buildSessionContent({ ...data, lastRefreshed: nowTimestamp2() });
|
|
1880
|
-
await writeFileForce(
|
|
2106
|
+
await writeFileForce(resolve7(dir, `${sanitizeSessionName(name)}.md`), content);
|
|
1881
2107
|
}
|
|
1882
2108
|
async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment) {
|
|
1883
2109
|
const data = await readSessionFile(dir, sessionName);
|
|
@@ -1889,7 +2115,7 @@ async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment)
|
|
|
1889
2115
|
delete data.overrides[key];
|
|
1890
2116
|
}
|
|
1891
2117
|
const content = buildSessionContent({ ...data });
|
|
1892
|
-
await writeFileForce(
|
|
2118
|
+
await writeFileForce(resolve7(dir, `${sanitizeSessionName(sessionName)}.md`), content);
|
|
1893
2119
|
}
|
|
1894
2120
|
async function registerAutoSession(dir, rawName, opts) {
|
|
1895
2121
|
const name = sanitizeSessionName(rawName);
|
|
@@ -1906,7 +2132,7 @@ async function registerAutoSession(dir, rawName, opts) {
|
|
|
1906
2132
|
ports: opts.ports,
|
|
1907
2133
|
cwd: opts.cwd
|
|
1908
2134
|
});
|
|
1909
|
-
await writeFileForce(
|
|
2135
|
+
await writeFileForce(resolve7(dir, `${name}.md`), content);
|
|
1910
2136
|
return name;
|
|
1911
2137
|
}
|
|
1912
2138
|
var init_servers = __esm({
|
|
@@ -1938,8 +2164,8 @@ __export(scanner_exports, {
|
|
|
1938
2164
|
});
|
|
1939
2165
|
import { execFile } from "child_process";
|
|
1940
2166
|
import { promisify } from "util";
|
|
1941
|
-
import { resolve as
|
|
1942
|
-
import { realpath, readdir as
|
|
2167
|
+
import { resolve as resolve8 } from "path";
|
|
2168
|
+
import { realpath, readdir as readdir5, readFile as readFile7 } from "fs/promises";
|
|
1943
2169
|
function clearScanCache() {
|
|
1944
2170
|
cache = null;
|
|
1945
2171
|
}
|
|
@@ -2034,8 +2260,8 @@ async function getGitInfo(cwd) {
|
|
|
2034
2260
|
let isWorktree = false;
|
|
2035
2261
|
if (commonDir && gitDir && commonDir !== gitDir) {
|
|
2036
2262
|
try {
|
|
2037
|
-
const resolvedCommon = await realpath(
|
|
2038
|
-
const resolvedGit = await realpath(
|
|
2263
|
+
const resolvedCommon = await realpath(resolve8(cwd, commonDir));
|
|
2264
|
+
const resolvedGit = await realpath(resolve8(cwd, gitDir));
|
|
2039
2265
|
isWorktree = resolvedCommon !== resolvedGit;
|
|
2040
2266
|
} catch {
|
|
2041
2267
|
isWorktree = false;
|
|
@@ -2048,17 +2274,17 @@ async function loadWorkspaceRecords(projectsDir, assignmentsDir) {
|
|
|
2048
2274
|
try {
|
|
2049
2275
|
const projects = await listProjects(projectsDir);
|
|
2050
2276
|
for (const project of projects) {
|
|
2051
|
-
const projectAssignmentsDir =
|
|
2277
|
+
const projectAssignmentsDir = resolve8(projectsDir, project.slug, "assignments");
|
|
2052
2278
|
let slugs;
|
|
2053
2279
|
try {
|
|
2054
|
-
slugs = await
|
|
2280
|
+
slugs = await readdir5(projectAssignmentsDir);
|
|
2055
2281
|
} catch {
|
|
2056
2282
|
continue;
|
|
2057
2283
|
}
|
|
2058
2284
|
for (const aslug of slugs) {
|
|
2059
|
-
const aFile =
|
|
2285
|
+
const aFile = resolve8(projectAssignmentsDir, aslug, "assignment.md");
|
|
2060
2286
|
try {
|
|
2061
|
-
const raw = await
|
|
2287
|
+
const raw = await readFile7(aFile, "utf-8");
|
|
2062
2288
|
const [fm] = extractFrontmatter2(raw);
|
|
2063
2289
|
if (!fm) continue;
|
|
2064
2290
|
records.push({
|
|
@@ -2077,12 +2303,12 @@ async function loadWorkspaceRecords(projectsDir, assignmentsDir) {
|
|
|
2077
2303
|
}
|
|
2078
2304
|
if (assignmentsDir) {
|
|
2079
2305
|
try {
|
|
2080
|
-
const entries = await
|
|
2306
|
+
const entries = await readdir5(assignmentsDir);
|
|
2081
2307
|
for (const id of entries) {
|
|
2082
2308
|
if (id.startsWith(".") || id.startsWith("_")) continue;
|
|
2083
|
-
const aFile =
|
|
2309
|
+
const aFile = resolve8(assignmentsDir, id, "assignment.md");
|
|
2084
2310
|
try {
|
|
2085
|
-
const raw = await
|
|
2311
|
+
const raw = await readFile7(aFile, "utf-8");
|
|
2086
2312
|
const [fm] = extractFrontmatter2(raw);
|
|
2087
2313
|
if (!fm) continue;
|
|
2088
2314
|
records.push({
|
|
@@ -2330,20 +2556,20 @@ var init_scanner = __esm({
|
|
|
2330
2556
|
});
|
|
2331
2557
|
|
|
2332
2558
|
// src/dashboard/api.ts
|
|
2333
|
-
import { readdir as
|
|
2334
|
-
import { resolve as
|
|
2559
|
+
import { readdir as readdir6, readFile as readFile8, writeFile as writeFile3 } from "fs/promises";
|
|
2560
|
+
import { resolve as resolve9, dirname as dirname2 } from "path";
|
|
2335
2561
|
async function listStandaloneRecords(assignmentsDir) {
|
|
2336
2562
|
if (!assignmentsDir) return [];
|
|
2337
2563
|
if (!await fileExists(assignmentsDir)) return [];
|
|
2338
|
-
const entries = await
|
|
2564
|
+
const entries = await readdir6(assignmentsDir, { withFileTypes: true });
|
|
2339
2565
|
const records = [];
|
|
2340
2566
|
for (const entry of entries) {
|
|
2341
2567
|
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
|
|
2342
|
-
const assignmentDir =
|
|
2343
|
-
const assignmentMdPath =
|
|
2568
|
+
const assignmentDir = resolve9(assignmentsDir, entry.name);
|
|
2569
|
+
const assignmentMdPath = resolve9(assignmentDir, "assignment.md");
|
|
2344
2570
|
if (!await fileExists(assignmentMdPath)) continue;
|
|
2345
2571
|
try {
|
|
2346
|
-
const content = await
|
|
2572
|
+
const content = await readFile8(assignmentMdPath, "utf-8");
|
|
2347
2573
|
const record = parseAssignmentFull(content);
|
|
2348
2574
|
records.push({ assignmentDir, id: entry.name, record });
|
|
2349
2575
|
} catch {
|
|
@@ -2412,9 +2638,9 @@ async function listProjects(projectsDir) {
|
|
|
2412
2638
|
return projectRecords.map((record) => record.summary);
|
|
2413
2639
|
}
|
|
2414
2640
|
async function readWorkspaceRegistry(projectsDir) {
|
|
2415
|
-
const registryPath =
|
|
2641
|
+
const registryPath = resolve9(dirname2(projectsDir), "workspaces.json");
|
|
2416
2642
|
try {
|
|
2417
|
-
const raw = await
|
|
2643
|
+
const raw = await readFile8(registryPath, "utf-8");
|
|
2418
2644
|
const parsed = JSON.parse(raw);
|
|
2419
2645
|
return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
|
|
2420
2646
|
} catch {
|
|
@@ -2422,13 +2648,14 @@ async function readWorkspaceRegistry(projectsDir) {
|
|
|
2422
2648
|
}
|
|
2423
2649
|
}
|
|
2424
2650
|
async function writeWorkspaceRegistry(projectsDir, workspaces) {
|
|
2425
|
-
const registryPath =
|
|
2651
|
+
const registryPath = resolve9(dirname2(projectsDir), "workspaces.json");
|
|
2426
2652
|
await writeFile3(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
|
|
2427
2653
|
}
|
|
2428
|
-
async function listWorkspaces(projectsDir) {
|
|
2429
|
-
const [projectRecords, registered] = await Promise.all([
|
|
2654
|
+
async function listWorkspaces(projectsDir, assignmentsDir) {
|
|
2655
|
+
const [projectRecords, registered, standaloneRecords] = await Promise.all([
|
|
2430
2656
|
listProjectRecords(projectsDir),
|
|
2431
|
-
readWorkspaceRegistry(projectsDir)
|
|
2657
|
+
readWorkspaceRegistry(projectsDir),
|
|
2658
|
+
listStandaloneRecords(assignmentsDir)
|
|
2432
2659
|
]);
|
|
2433
2660
|
const workspaceSet = new Set(registered);
|
|
2434
2661
|
let hasUngrouped = false;
|
|
@@ -2439,6 +2666,13 @@ async function listWorkspaces(projectsDir) {
|
|
|
2439
2666
|
hasUngrouped = true;
|
|
2440
2667
|
}
|
|
2441
2668
|
}
|
|
2669
|
+
for (const sr of standaloneRecords) {
|
|
2670
|
+
if (sr.record.workspaceGroup) {
|
|
2671
|
+
workspaceSet.add(sr.record.workspaceGroup);
|
|
2672
|
+
} else {
|
|
2673
|
+
hasUngrouped = true;
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2442
2676
|
const workspaces = Array.from(workspaceSet).sort();
|
|
2443
2677
|
return { workspaces, hasUngrouped };
|
|
2444
2678
|
}
|
|
@@ -2587,7 +2821,7 @@ async function toStandaloneBoardItem(sr) {
|
|
|
2587
2821
|
projectSlug: null,
|
|
2588
2822
|
projectTitle: null,
|
|
2589
2823
|
blockedReason: sr.record.blockedReason,
|
|
2590
|
-
projectWorkspace: null,
|
|
2824
|
+
projectWorkspace: sr.record.workspaceGroup ?? null,
|
|
2591
2825
|
availableTransitions: await getStandaloneAvailableTransitions(sr.record)
|
|
2592
2826
|
};
|
|
2593
2827
|
}
|
|
@@ -2622,7 +2856,7 @@ async function getEditableDocument(projectsDir, documentType, projectSlug, assig
|
|
|
2622
2856
|
if (!filePath || !await fileExists(filePath)) {
|
|
2623
2857
|
return null;
|
|
2624
2858
|
}
|
|
2625
|
-
const content = await
|
|
2859
|
+
const content = await readFile8(filePath, "utf-8");
|
|
2626
2860
|
const title = getEditableDocumentTitle(documentType, projectSlug, assignmentSlug);
|
|
2627
2861
|
return {
|
|
2628
2862
|
documentType,
|
|
@@ -2646,9 +2880,9 @@ async function getEditableDocumentById(projectsDir, assignmentsDir, documentType
|
|
|
2646
2880
|
}
|
|
2647
2881
|
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
2882
|
if (!fileName) return null;
|
|
2649
|
-
const filePath =
|
|
2883
|
+
const filePath = resolve9(resolved.assignmentDir, fileName);
|
|
2650
2884
|
if (!await fileExists(filePath)) return null;
|
|
2651
|
-
const content = await
|
|
2885
|
+
const content = await readFile8(filePath, "utf-8");
|
|
2652
2886
|
const label = resolved.id;
|
|
2653
2887
|
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
2888
|
return {
|
|
@@ -2662,12 +2896,12 @@ async function getEditableDocumentById(projectsDir, assignmentsDir, documentType
|
|
|
2662
2896
|
};
|
|
2663
2897
|
}
|
|
2664
2898
|
async function getProjectDetail(projectsDir, slug) {
|
|
2665
|
-
const projectPath =
|
|
2666
|
-
const projectMdPath =
|
|
2899
|
+
const projectPath = resolve9(projectsDir, slug);
|
|
2900
|
+
const projectMdPath = resolve9(projectPath, "project.md");
|
|
2667
2901
|
if (!await fileExists(projectMdPath)) {
|
|
2668
2902
|
return null;
|
|
2669
2903
|
}
|
|
2670
|
-
const projectContent = await
|
|
2904
|
+
const projectContent = await readFile8(projectMdPath, "utf-8");
|
|
2671
2905
|
const project = parseProject(projectContent);
|
|
2672
2906
|
const assignments = await listAssignmentRecords(projectPath);
|
|
2673
2907
|
const rollup = await buildProjectRollup(projectPath, project, assignments);
|
|
@@ -2697,17 +2931,17 @@ async function getProjectDetail(projectsDir, slug) {
|
|
|
2697
2931
|
};
|
|
2698
2932
|
}
|
|
2699
2933
|
async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
2700
|
-
const assignmentDir =
|
|
2701
|
-
const assignmentMdPath =
|
|
2934
|
+
const assignmentDir = resolve9(projectsDir, projectSlug, "assignments", assignmentSlug);
|
|
2935
|
+
const assignmentMdPath = resolve9(assignmentDir, "assignment.md");
|
|
2702
2936
|
if (!await fileExists(assignmentMdPath)) {
|
|
2703
2937
|
return null;
|
|
2704
2938
|
}
|
|
2705
|
-
const assignmentContent = await
|
|
2939
|
+
const assignmentContent = await readFile8(assignmentMdPath, "utf-8");
|
|
2706
2940
|
const assignment = parseAssignmentFull(assignmentContent);
|
|
2707
2941
|
let plan = null;
|
|
2708
|
-
const planPath =
|
|
2942
|
+
const planPath = resolve9(assignmentDir, "plan.md");
|
|
2709
2943
|
if (await fileExists(planPath)) {
|
|
2710
|
-
const planContent = await
|
|
2944
|
+
const planContent = await readFile8(planPath, "utf-8");
|
|
2711
2945
|
const parsed = parsePlan(planContent);
|
|
2712
2946
|
plan = {
|
|
2713
2947
|
status: parsed.status,
|
|
@@ -2716,9 +2950,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2716
2950
|
};
|
|
2717
2951
|
}
|
|
2718
2952
|
let scratchpad = null;
|
|
2719
|
-
const scratchpadPath =
|
|
2953
|
+
const scratchpadPath = resolve9(assignmentDir, "scratchpad.md");
|
|
2720
2954
|
if (await fileExists(scratchpadPath)) {
|
|
2721
|
-
const scratchpadContent = await
|
|
2955
|
+
const scratchpadContent = await readFile8(scratchpadPath, "utf-8");
|
|
2722
2956
|
const parsed = parseScratchpad(scratchpadContent);
|
|
2723
2957
|
scratchpad = {
|
|
2724
2958
|
updated: parsed.updated,
|
|
@@ -2726,9 +2960,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2726
2960
|
};
|
|
2727
2961
|
}
|
|
2728
2962
|
let handoff = null;
|
|
2729
|
-
const handoffPath =
|
|
2963
|
+
const handoffPath = resolve9(assignmentDir, "handoff.md");
|
|
2730
2964
|
if (await fileExists(handoffPath)) {
|
|
2731
|
-
const handoffContent = await
|
|
2965
|
+
const handoffContent = await readFile8(handoffPath, "utf-8");
|
|
2732
2966
|
const parsed = parseHandoff(handoffContent);
|
|
2733
2967
|
handoff = {
|
|
2734
2968
|
updated: parsed.updated,
|
|
@@ -2737,9 +2971,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2737
2971
|
};
|
|
2738
2972
|
}
|
|
2739
2973
|
let decisionRecord = null;
|
|
2740
|
-
const decisionRecordPath =
|
|
2974
|
+
const decisionRecordPath = resolve9(assignmentDir, "decision-record.md");
|
|
2741
2975
|
if (await fileExists(decisionRecordPath)) {
|
|
2742
|
-
const decisionRecordContent = await
|
|
2976
|
+
const decisionRecordContent = await readFile8(decisionRecordPath, "utf-8");
|
|
2743
2977
|
const parsed = parseDecisionRecord(decisionRecordContent);
|
|
2744
2978
|
decisionRecord = {
|
|
2745
2979
|
updated: parsed.updated,
|
|
@@ -2748,9 +2982,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2748
2982
|
};
|
|
2749
2983
|
}
|
|
2750
2984
|
let progress = null;
|
|
2751
|
-
const progressPath =
|
|
2985
|
+
const progressPath = resolve9(assignmentDir, "progress.md");
|
|
2752
2986
|
if (await fileExists(progressPath)) {
|
|
2753
|
-
const progressContent = await
|
|
2987
|
+
const progressContent = await readFile8(progressPath, "utf-8");
|
|
2754
2988
|
const parsed = parseProgress(progressContent);
|
|
2755
2989
|
progress = {
|
|
2756
2990
|
updated: parsed.updated,
|
|
@@ -2759,9 +2993,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2759
2993
|
};
|
|
2760
2994
|
}
|
|
2761
2995
|
let comments = null;
|
|
2762
|
-
const commentsPath =
|
|
2996
|
+
const commentsPath = resolve9(assignmentDir, "comments.md");
|
|
2763
2997
|
if (await fileExists(commentsPath)) {
|
|
2764
|
-
const commentsContent = await
|
|
2998
|
+
const commentsContent = await readFile8(commentsPath, "utf-8");
|
|
2765
2999
|
const parsed = parseComments(commentsContent);
|
|
2766
3000
|
comments = {
|
|
2767
3001
|
updated: parsed.updated,
|
|
@@ -2875,7 +3109,7 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
|
|
|
2875
3109
|
slug: a.slug,
|
|
2876
3110
|
title: a.title,
|
|
2877
3111
|
projectSlug: rec.summary.slug,
|
|
2878
|
-
assignmentDir:
|
|
3112
|
+
assignmentDir: resolve9(rec.projectPath, "assignments", a.slug)
|
|
2879
3113
|
});
|
|
2880
3114
|
}
|
|
2881
3115
|
}
|
|
@@ -2908,17 +3142,17 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
|
|
|
2908
3142
|
}
|
|
2909
3143
|
async function countMentionsInAssignment(sourceDir, target) {
|
|
2910
3144
|
const bodies = [];
|
|
2911
|
-
const assignmentMd =
|
|
3145
|
+
const assignmentMd = resolve9(sourceDir, "assignment.md");
|
|
2912
3146
|
if (await fileExists(assignmentMd)) {
|
|
2913
|
-
const content = await
|
|
3147
|
+
const content = await readFile8(assignmentMd, "utf-8");
|
|
2914
3148
|
const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
|
|
2915
3149
|
if (todosMatch) bodies.push(todosMatch[1]);
|
|
2916
3150
|
}
|
|
2917
3151
|
for (const filename of ["progress.md", "comments.md", "handoff.md"]) {
|
|
2918
|
-
const path =
|
|
3152
|
+
const path = resolve9(sourceDir, filename);
|
|
2919
3153
|
if (await fileExists(path)) {
|
|
2920
3154
|
try {
|
|
2921
|
-
bodies.push(await
|
|
3155
|
+
bodies.push(await readFile8(path, "utf-8"));
|
|
2922
3156
|
} catch {
|
|
2923
3157
|
}
|
|
2924
3158
|
}
|
|
@@ -2976,44 +3210,44 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir, id) {
|
|
|
2976
3210
|
}
|
|
2977
3211
|
async function buildStandaloneAssignmentDetail(resolved) {
|
|
2978
3212
|
const assignmentDir = resolved.assignmentDir;
|
|
2979
|
-
const assignmentMdPath =
|
|
3213
|
+
const assignmentMdPath = resolve9(assignmentDir, "assignment.md");
|
|
2980
3214
|
if (!await fileExists(assignmentMdPath)) return null;
|
|
2981
|
-
const assignmentContent = await
|
|
3215
|
+
const assignmentContent = await readFile8(assignmentMdPath, "utf-8");
|
|
2982
3216
|
const assignment = parseAssignmentFull(assignmentContent);
|
|
2983
3217
|
let plan = null;
|
|
2984
|
-
const planPath =
|
|
3218
|
+
const planPath = resolve9(assignmentDir, "plan.md");
|
|
2985
3219
|
if (await fileExists(planPath)) {
|
|
2986
|
-
const parsed = parsePlan(await
|
|
3220
|
+
const parsed = parsePlan(await readFile8(planPath, "utf-8"));
|
|
2987
3221
|
plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
|
|
2988
3222
|
}
|
|
2989
3223
|
let scratchpad = null;
|
|
2990
|
-
const scratchpadPath =
|
|
3224
|
+
const scratchpadPath = resolve9(assignmentDir, "scratchpad.md");
|
|
2991
3225
|
if (await fileExists(scratchpadPath)) {
|
|
2992
|
-
const parsed = parseScratchpad(await
|
|
3226
|
+
const parsed = parseScratchpad(await readFile8(scratchpadPath, "utf-8"));
|
|
2993
3227
|
scratchpad = { updated: parsed.updated, body: parsed.body };
|
|
2994
3228
|
}
|
|
2995
3229
|
let handoff = null;
|
|
2996
|
-
const handoffPath =
|
|
3230
|
+
const handoffPath = resolve9(assignmentDir, "handoff.md");
|
|
2997
3231
|
if (await fileExists(handoffPath)) {
|
|
2998
|
-
const parsed = parseHandoff(await
|
|
3232
|
+
const parsed = parseHandoff(await readFile8(handoffPath, "utf-8"));
|
|
2999
3233
|
handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
|
|
3000
3234
|
}
|
|
3001
3235
|
let decisionRecord = null;
|
|
3002
|
-
const decisionRecordPath =
|
|
3236
|
+
const decisionRecordPath = resolve9(assignmentDir, "decision-record.md");
|
|
3003
3237
|
if (await fileExists(decisionRecordPath)) {
|
|
3004
|
-
const parsed = parseDecisionRecord(await
|
|
3238
|
+
const parsed = parseDecisionRecord(await readFile8(decisionRecordPath, "utf-8"));
|
|
3005
3239
|
decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
|
|
3006
3240
|
}
|
|
3007
3241
|
let progress = null;
|
|
3008
|
-
const progressPath =
|
|
3242
|
+
const progressPath = resolve9(assignmentDir, "progress.md");
|
|
3009
3243
|
if (await fileExists(progressPath)) {
|
|
3010
|
-
const parsed = parseProgress(await
|
|
3244
|
+
const parsed = parseProgress(await readFile8(progressPath, "utf-8"));
|
|
3011
3245
|
progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
|
|
3012
3246
|
}
|
|
3013
3247
|
let comments = null;
|
|
3014
|
-
const commentsPath =
|
|
3248
|
+
const commentsPath = resolve9(assignmentDir, "comments.md");
|
|
3015
3249
|
if (await fileExists(commentsPath)) {
|
|
3016
|
-
const parsed = parseComments(await
|
|
3250
|
+
const parsed = parseComments(await readFile8(commentsPath, "utf-8"));
|
|
3017
3251
|
comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
|
|
3018
3252
|
}
|
|
3019
3253
|
const detail = {
|
|
@@ -3055,16 +3289,16 @@ async function listProjectRecords(projectsDir) {
|
|
|
3055
3289
|
migratedProjectsDirs.add(projectsDir);
|
|
3056
3290
|
await migrateLegacyProjectFiles(projectsDir);
|
|
3057
3291
|
}
|
|
3058
|
-
const entries = await
|
|
3292
|
+
const entries = await readdir6(projectsDir, { withFileTypes: true });
|
|
3059
3293
|
const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
|
|
3060
3294
|
const records = [];
|
|
3061
3295
|
for (const entry of projectDirs) {
|
|
3062
|
-
const projectPath =
|
|
3063
|
-
const projectMdPath =
|
|
3296
|
+
const projectPath = resolve9(projectsDir, entry.name);
|
|
3297
|
+
const projectMdPath = resolve9(projectPath, "project.md");
|
|
3064
3298
|
if (!await fileExists(projectMdPath)) {
|
|
3065
3299
|
continue;
|
|
3066
3300
|
}
|
|
3067
|
-
const projectContent = await
|
|
3301
|
+
const projectContent = await readFile8(projectMdPath, "utf-8");
|
|
3068
3302
|
const project = parseProject(projectContent);
|
|
3069
3303
|
const assignments = await listAssignmentRecords(projectPath);
|
|
3070
3304
|
const rollup = await buildProjectRollup(projectPath, project, assignments);
|
|
@@ -3095,39 +3329,39 @@ async function listProjectRecords(projectsDir) {
|
|
|
3095
3329
|
return records;
|
|
3096
3330
|
}
|
|
3097
3331
|
async function listAssignmentRecords(projectPath) {
|
|
3098
|
-
const assignmentsDir =
|
|
3332
|
+
const assignmentsDir = resolve9(projectPath, "assignments");
|
|
3099
3333
|
if (!await fileExists(assignmentsDir)) {
|
|
3100
3334
|
return [];
|
|
3101
3335
|
}
|
|
3102
|
-
const entries = await
|
|
3336
|
+
const entries = await readdir6(assignmentsDir, { withFileTypes: true });
|
|
3103
3337
|
const records = [];
|
|
3104
3338
|
for (const entry of entries) {
|
|
3105
3339
|
if (!entry.isDirectory()) {
|
|
3106
3340
|
continue;
|
|
3107
3341
|
}
|
|
3108
|
-
const assignmentMd =
|
|
3342
|
+
const assignmentMd = resolve9(assignmentsDir, entry.name, "assignment.md");
|
|
3109
3343
|
if (!await fileExists(assignmentMd)) {
|
|
3110
3344
|
continue;
|
|
3111
3345
|
}
|
|
3112
|
-
const content = await
|
|
3346
|
+
const content = await readFile8(assignmentMd, "utf-8");
|
|
3113
3347
|
records.push(parseAssignmentFull(content));
|
|
3114
3348
|
}
|
|
3115
3349
|
records.sort((left, right) => compareTimestamps(right.updated, left.updated));
|
|
3116
3350
|
return records;
|
|
3117
3351
|
}
|
|
3118
3352
|
async function listResources(projectPath) {
|
|
3119
|
-
const resourcesDir =
|
|
3353
|
+
const resourcesDir = resolve9(projectPath, "resources");
|
|
3120
3354
|
if (!await fileExists(resourcesDir)) {
|
|
3121
3355
|
return [];
|
|
3122
3356
|
}
|
|
3123
|
-
const entries = await
|
|
3357
|
+
const entries = await readdir6(resourcesDir, { withFileTypes: true });
|
|
3124
3358
|
const results = [];
|
|
3125
3359
|
for (const entry of entries) {
|
|
3126
3360
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
|
|
3127
3361
|
continue;
|
|
3128
3362
|
}
|
|
3129
|
-
const filePath =
|
|
3130
|
-
const content = await
|
|
3363
|
+
const filePath = resolve9(resourcesDir, entry.name);
|
|
3364
|
+
const content = await readFile8(filePath, "utf-8");
|
|
3131
3365
|
const parsed = parseResource(content);
|
|
3132
3366
|
results.push({
|
|
3133
3367
|
name: parsed.name,
|
|
@@ -3142,18 +3376,18 @@ async function listResources(projectPath) {
|
|
|
3142
3376
|
return results;
|
|
3143
3377
|
}
|
|
3144
3378
|
async function listMemories(projectPath) {
|
|
3145
|
-
const memoriesDir =
|
|
3379
|
+
const memoriesDir = resolve9(projectPath, "memories");
|
|
3146
3380
|
if (!await fileExists(memoriesDir)) {
|
|
3147
3381
|
return [];
|
|
3148
3382
|
}
|
|
3149
|
-
const entries = await
|
|
3383
|
+
const entries = await readdir6(memoriesDir, { withFileTypes: true });
|
|
3150
3384
|
const results = [];
|
|
3151
3385
|
for (const entry of entries) {
|
|
3152
3386
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
|
|
3153
3387
|
continue;
|
|
3154
3388
|
}
|
|
3155
|
-
const filePath =
|
|
3156
|
-
const content = await
|
|
3389
|
+
const filePath = resolve9(memoriesDir, entry.name);
|
|
3390
|
+
const content = await readFile8(filePath, "utf-8");
|
|
3157
3391
|
const parsed = parseMemory(content);
|
|
3158
3392
|
results.push({
|
|
3159
3393
|
name: parsed.name,
|
|
@@ -3168,9 +3402,9 @@ async function listMemories(projectPath) {
|
|
|
3168
3402
|
return results;
|
|
3169
3403
|
}
|
|
3170
3404
|
async function loadDependencyGraph(projectPath, assignments) {
|
|
3171
|
-
const statusPath =
|
|
3405
|
+
const statusPath = resolve9(projectPath, "_status.md");
|
|
3172
3406
|
if (await fileExists(statusPath)) {
|
|
3173
|
-
const statusContent = await
|
|
3407
|
+
const statusContent = await readFile8(statusPath, "utf-8");
|
|
3174
3408
|
const parsed = parseStatus(statusContent);
|
|
3175
3409
|
const derivedGraph = extractMermaidGraph(parsed.body);
|
|
3176
3410
|
if (derivedGraph) {
|
|
@@ -3270,7 +3504,7 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
|
|
|
3270
3504
|
const config = await getStatusConfig();
|
|
3271
3505
|
const transitionDefs = getTransitionDefinitions(config);
|
|
3272
3506
|
const actions = [];
|
|
3273
|
-
const projectPath =
|
|
3507
|
+
const projectPath = resolve9(projectsDir, projectSlug);
|
|
3274
3508
|
for (const definition of transitionDefs) {
|
|
3275
3509
|
let warning = null;
|
|
3276
3510
|
if (definition.command === "start" && !assignment.assignee) {
|
|
@@ -3300,12 +3534,12 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses) {
|
|
|
3300
3534
|
const terminals = terminalStatuses ?? /* @__PURE__ */ new Set(["completed"]);
|
|
3301
3535
|
const unmet = [];
|
|
3302
3536
|
for (const dependency of dependsOn) {
|
|
3303
|
-
const dependencyPath =
|
|
3537
|
+
const dependencyPath = resolve9(projectPath, "assignments", dependency, "assignment.md");
|
|
3304
3538
|
if (!await fileExists(dependencyPath)) {
|
|
3305
3539
|
unmet.push(`${dependency} (missing)`);
|
|
3306
3540
|
continue;
|
|
3307
3541
|
}
|
|
3308
|
-
const content = await
|
|
3542
|
+
const content = await readFile8(dependencyPath, "utf-8");
|
|
3309
3543
|
const parsed = parseAssignmentFull(content);
|
|
3310
3544
|
if (!terminals.has(parsed.status)) {
|
|
3311
3545
|
unmet.push(`${dependency} (${parsed.status})`);
|
|
@@ -3460,7 +3694,7 @@ function isStale(updated) {
|
|
|
3460
3694
|
return Date.now() - timestamp > STALE_ASSIGNMENT_MS;
|
|
3461
3695
|
}
|
|
3462
3696
|
async function countOpenQuestions(projectPath, assignmentSlug) {
|
|
3463
|
-
const commentsPath =
|
|
3697
|
+
const commentsPath = resolve9(
|
|
3464
3698
|
projectPath,
|
|
3465
3699
|
"assignments",
|
|
3466
3700
|
assignmentSlug,
|
|
@@ -3470,7 +3704,7 @@ async function countOpenQuestions(projectPath, assignmentSlug) {
|
|
|
3470
3704
|
return 0;
|
|
3471
3705
|
}
|
|
3472
3706
|
try {
|
|
3473
|
-
const content = await
|
|
3707
|
+
const content = await readFile8(commentsPath, "utf-8");
|
|
3474
3708
|
const parsed = parseComments(content);
|
|
3475
3709
|
return parsed.entries.filter(
|
|
3476
3710
|
(e) => e.type === "question" && e.resolved !== true
|
|
@@ -3491,17 +3725,17 @@ function getProjectActivityTimestamp(projectUpdated, assignments) {
|
|
|
3491
3725
|
function getDocumentPath(projectsDir, documentType, projectSlug, assignmentSlug) {
|
|
3492
3726
|
switch (documentType) {
|
|
3493
3727
|
case "project":
|
|
3494
|
-
return
|
|
3728
|
+
return resolve9(projectsDir, projectSlug, "project.md");
|
|
3495
3729
|
case "assignment":
|
|
3496
|
-
return assignmentSlug ?
|
|
3730
|
+
return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
|
|
3497
3731
|
case "plan":
|
|
3498
|
-
return assignmentSlug ?
|
|
3732
|
+
return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
|
|
3499
3733
|
case "scratchpad":
|
|
3500
|
-
return assignmentSlug ?
|
|
3734
|
+
return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
|
|
3501
3735
|
case "handoff":
|
|
3502
|
-
return assignmentSlug ?
|
|
3736
|
+
return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
|
|
3503
3737
|
case "decision-record":
|
|
3504
|
-
return assignmentSlug ?
|
|
3738
|
+
return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
|
|
3505
3739
|
default:
|
|
3506
3740
|
return null;
|
|
3507
3741
|
}
|
|
@@ -3528,12 +3762,14 @@ function getEditableDocumentTitle(documentType, projectSlug, assignmentSlug) {
|
|
|
3528
3762
|
}
|
|
3529
3763
|
async function listPlaybooks(playbooksDir2) {
|
|
3530
3764
|
if (!await fileExists(playbooksDir2)) return [];
|
|
3531
|
-
const
|
|
3765
|
+
const config = await readConfig();
|
|
3766
|
+
const disabledSet = new Set(config.playbooks.disabled);
|
|
3767
|
+
const entries = await readdir6(playbooksDir2, { withFileTypes: true });
|
|
3532
3768
|
const playbooks = [];
|
|
3533
3769
|
for (const entry of entries) {
|
|
3534
3770
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
|
|
3535
|
-
const filePath =
|
|
3536
|
-
const raw = await
|
|
3771
|
+
const filePath = resolve9(playbooksDir2, entry.name);
|
|
3772
|
+
const raw = await readFile8(filePath, "utf-8");
|
|
3537
3773
|
const parsed = parsePlaybook(raw);
|
|
3538
3774
|
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
3539
3775
|
playbooks.push({
|
|
@@ -3543,25 +3779,28 @@ async function listPlaybooks(playbooksDir2) {
|
|
|
3543
3779
|
whenToUse: parsed.whenToUse,
|
|
3544
3780
|
tags: parsed.tags,
|
|
3545
3781
|
created: parsed.created,
|
|
3546
|
-
updated: parsed.updated
|
|
3782
|
+
updated: parsed.updated,
|
|
3783
|
+
enabled: !disabledSet.has(slug)
|
|
3547
3784
|
});
|
|
3548
3785
|
}
|
|
3549
3786
|
return playbooks.sort((a, b) => (b.updated || b.created).localeCompare(a.updated || a.created));
|
|
3550
3787
|
}
|
|
3551
3788
|
async function getPlaybookDetail(playbooksDir2, slug) {
|
|
3552
|
-
const
|
|
3553
|
-
if (!
|
|
3554
|
-
const
|
|
3555
|
-
const
|
|
3789
|
+
const resolved = await resolvePlaybookSlug(playbooksDir2, slug);
|
|
3790
|
+
if (!resolved) return null;
|
|
3791
|
+
const config = await readConfig();
|
|
3792
|
+
const enabled = !config.playbooks.disabled.includes(resolved.slug);
|
|
3793
|
+
const parsed = resolved.parsed;
|
|
3556
3794
|
return {
|
|
3557
|
-
slug:
|
|
3558
|
-
name: parsed.name || slug,
|
|
3795
|
+
slug: resolved.slug,
|
|
3796
|
+
name: parsed.name || resolved.slug,
|
|
3559
3797
|
description: parsed.description,
|
|
3560
3798
|
whenToUse: parsed.whenToUse,
|
|
3561
3799
|
tags: parsed.tags,
|
|
3562
3800
|
created: parsed.created,
|
|
3563
3801
|
updated: parsed.updated,
|
|
3564
|
-
body: parsed.body
|
|
3802
|
+
body: parsed.body,
|
|
3803
|
+
enabled
|
|
3565
3804
|
};
|
|
3566
3805
|
}
|
|
3567
3806
|
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 +3810,7 @@ var init_api = __esm({
|
|
|
3571
3810
|
init_lifecycle();
|
|
3572
3811
|
init_fs();
|
|
3573
3812
|
init_config2();
|
|
3813
|
+
init_playbooks();
|
|
3574
3814
|
init_fs_migration();
|
|
3575
3815
|
init_assignment_resolver();
|
|
3576
3816
|
init_parser();
|
|
@@ -3928,15 +4168,15 @@ import { WebSocketServer, WebSocket } from "ws";
|
|
|
3928
4168
|
|
|
3929
4169
|
// src/dashboard/agent-sessions.ts
|
|
3930
4170
|
init_fs();
|
|
3931
|
-
import { readFile as
|
|
3932
|
-
import { resolve as
|
|
4171
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
4172
|
+
import { resolve as resolve11 } from "path";
|
|
3933
4173
|
|
|
3934
4174
|
// src/dashboard/session-db.ts
|
|
3935
4175
|
init_paths();
|
|
3936
4176
|
init_fs();
|
|
3937
4177
|
import Database from "better-sqlite3";
|
|
3938
|
-
import { resolve as
|
|
3939
|
-
import { readdir as
|
|
4178
|
+
import { resolve as resolve10 } from "path";
|
|
4179
|
+
import { readdir as readdir7 } from "fs/promises";
|
|
3940
4180
|
var db = null;
|
|
3941
4181
|
var SCHEMA_VERSION = "3";
|
|
3942
4182
|
var SCHEMA_SQL = `
|
|
@@ -3963,7 +4203,7 @@ CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, ass
|
|
|
3963
4203
|
`;
|
|
3964
4204
|
function initSessionDb(dbPath) {
|
|
3965
4205
|
if (db) return db;
|
|
3966
|
-
const finalPath = dbPath ??
|
|
4206
|
+
const finalPath = dbPath ?? resolve10(syntaurRoot(), "syntaur.db");
|
|
3967
4207
|
db = new Database(finalPath);
|
|
3968
4208
|
db.pragma("journal_mode = WAL");
|
|
3969
4209
|
db.exec(SCHEMA_SQL);
|
|
@@ -4060,12 +4300,12 @@ async function migrateFromMarkdown(projectsDir) {
|
|
|
4060
4300
|
const count = database.prepare("SELECT COUNT(*) as count FROM sessions").get();
|
|
4061
4301
|
if (count.count > 0) return 0;
|
|
4062
4302
|
if (!await fileExists(projectsDir)) return 0;
|
|
4063
|
-
const entries = await
|
|
4303
|
+
const entries = await readdir7(projectsDir, { withFileTypes: true });
|
|
4064
4304
|
const allSessions = [];
|
|
4065
4305
|
for (const entry of entries) {
|
|
4066
4306
|
if (!entry.isDirectory()) continue;
|
|
4067
|
-
const projectDir =
|
|
4068
|
-
const indexPath =
|
|
4307
|
+
const projectDir = resolve10(projectsDir, entry.name);
|
|
4308
|
+
const indexPath = resolve10(projectDir, "_index-sessions.md");
|
|
4069
4309
|
if (!await fileExists(indexPath)) continue;
|
|
4070
4310
|
const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
|
|
4071
4311
|
allSessions.push(...sessions);
|
|
@@ -4198,13 +4438,13 @@ async function deleteSessions(sessionIds) {
|
|
|
4198
4438
|
var DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
|
|
4199
4439
|
async function readAssignmentStatusFromPath(assignmentMdPath) {
|
|
4200
4440
|
if (!await fileExists(assignmentMdPath)) return null;
|
|
4201
|
-
const raw = await
|
|
4441
|
+
const raw = await readFile9(assignmentMdPath, "utf-8");
|
|
4202
4442
|
const match = raw.match(/^status:\s*(.+)$/m);
|
|
4203
4443
|
return match ? match[1].trim() : null;
|
|
4204
4444
|
}
|
|
4205
4445
|
async function readAssignmentStatus(projectDir, assignmentSlug) {
|
|
4206
4446
|
return readAssignmentStatusFromPath(
|
|
4207
|
-
|
|
4447
|
+
resolve11(projectDir, "assignments", assignmentSlug, "assignment.md")
|
|
4208
4448
|
);
|
|
4209
4449
|
}
|
|
4210
4450
|
async function reconcileActiveSessions(projectsDir, assignmentsDir) {
|
|
@@ -4222,13 +4462,13 @@ async function reconcileActiveSessions(projectsDir, assignmentsDir) {
|
|
|
4222
4462
|
seen.add(key);
|
|
4223
4463
|
if (session.project_slug) {
|
|
4224
4464
|
const status = await readAssignmentStatus(
|
|
4225
|
-
|
|
4465
|
+
resolve11(projectsDir, session.project_slug),
|
|
4226
4466
|
aslug
|
|
4227
4467
|
);
|
|
4228
4468
|
if (status) assignmentStatuses.set(key, status);
|
|
4229
4469
|
} else if (assignmentsDir) {
|
|
4230
4470
|
const status = await readAssignmentStatusFromPath(
|
|
4231
|
-
|
|
4471
|
+
resolve11(assignmentsDir, aslug, "assignment.md")
|
|
4232
4472
|
);
|
|
4233
4473
|
if (status) assignmentStatuses.set(key, status);
|
|
4234
4474
|
}
|
|
@@ -4442,8 +4682,8 @@ init_config2();
|
|
|
4442
4682
|
// src/dashboard/api-write.ts
|
|
4443
4683
|
init_lifecycle();
|
|
4444
4684
|
import { Router } from "express";
|
|
4445
|
-
import { resolve as
|
|
4446
|
-
import { rm, readFile as
|
|
4685
|
+
import { resolve as resolve12 } from "path";
|
|
4686
|
+
import { rm, readFile as readFile10 } from "fs/promises";
|
|
4447
4687
|
|
|
4448
4688
|
// src/utils/slug.ts
|
|
4449
4689
|
function isValidSlug(slug) {
|
|
@@ -4585,12 +4825,25 @@ function renderAssignment(params) {
|
|
|
4585
4825
|
const linksYaml = params.links.length === 0 ? "links: []" : `links:
|
|
4586
4826
|
- ${params.links.join("\n - ")}`;
|
|
4587
4827
|
const projectYaml = `project: ${params.project == null ? "null" : params.project}`;
|
|
4828
|
+
const workspaceGroupLine = params.workspaceGroup ? `
|
|
4829
|
+
workspaceGroup: ${params.workspaceGroup}` : "";
|
|
4588
4830
|
const typeYaml = `type: ${params.type ?? "feature"}`;
|
|
4831
|
+
const todosSection = params.includeTodos ? `## Todos
|
|
4832
|
+
|
|
4833
|
+
<!--
|
|
4834
|
+
Checklist of work items for this assignment. Items may be simple tasks
|
|
4835
|
+
or a markdown link to a plan file (e.g., "- [ ] Execute [plan](./plan.md)").
|
|
4836
|
+
When a plan is superseded by a new one, mark the old todo as:
|
|
4837
|
+
- [x] ~~Execute [old plan](./plan.md)~~ (superseded by plan-v2)
|
|
4838
|
+
Never delete superseded todos \u2014 preserve the history.
|
|
4839
|
+
-->
|
|
4840
|
+
|
|
4841
|
+
` : "";
|
|
4589
4842
|
return `---
|
|
4590
4843
|
id: ${params.id}
|
|
4591
4844
|
slug: ${params.slug}
|
|
4592
4845
|
title: ${safeTitle}
|
|
4593
|
-
${projectYaml}
|
|
4846
|
+
${projectYaml}${workspaceGroupLine}
|
|
4594
4847
|
${typeYaml}
|
|
4595
4848
|
status: pending
|
|
4596
4849
|
priority: ${params.priority}
|
|
@@ -4621,17 +4874,7 @@ tags: []
|
|
|
4621
4874
|
- [ ] <!-- criterion 2 -->
|
|
4622
4875
|
- [ ] <!-- criterion 3 -->
|
|
4623
4876
|
|
|
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
|
|
4877
|
+
${todosSection}## Context
|
|
4635
4878
|
|
|
4636
4879
|
<!-- Links to relevant docs, code, or other assignments. -->
|
|
4637
4880
|
|
|
@@ -4966,7 +5209,7 @@ async function readCurrentDocument(filePath) {
|
|
|
4966
5209
|
if (!await fileExists(filePath)) {
|
|
4967
5210
|
return null;
|
|
4968
5211
|
}
|
|
4969
|
-
return
|
|
5212
|
+
return readFile10(filePath, "utf-8");
|
|
4970
5213
|
}
|
|
4971
5214
|
function createWriteRouter(projectsDir, assignmentsDir) {
|
|
4972
5215
|
const router = Router();
|
|
@@ -4979,7 +5222,15 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
4979
5222
|
});
|
|
4980
5223
|
res.json({ content });
|
|
4981
5224
|
});
|
|
4982
|
-
router.get("/api/templates/assignment", (
|
|
5225
|
+
router.get("/api/templates/assignment", (req, res) => {
|
|
5226
|
+
const standalone = req.query.standalone === "1";
|
|
5227
|
+
const workspaceParam = typeof req.query.workspace === "string" ? req.query.workspace : "";
|
|
5228
|
+
if (workspaceParam && !isValidSlug(workspaceParam)) {
|
|
5229
|
+
res.status(400).json({
|
|
5230
|
+
error: `Invalid workspace slug "${workspaceParam}". Slugs must be lowercase, hyphen-separated, with no special characters.`
|
|
5231
|
+
});
|
|
5232
|
+
return;
|
|
5233
|
+
}
|
|
4983
5234
|
const content = renderAssignment({
|
|
4984
5235
|
id: generateId(),
|
|
4985
5236
|
slug: "my-new-assignment",
|
|
@@ -4987,7 +5238,9 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
4987
5238
|
timestamp: nowTimestamp(),
|
|
4988
5239
|
priority: "medium",
|
|
4989
5240
|
dependsOn: [],
|
|
4990
|
-
links: []
|
|
5241
|
+
links: [],
|
|
5242
|
+
project: standalone ? null : void 0,
|
|
5243
|
+
workspaceGroup: standalone && workspaceParam ? workspaceParam : null
|
|
4991
5244
|
});
|
|
4992
5245
|
res.json({ content });
|
|
4993
5246
|
});
|
|
@@ -5096,26 +5349,26 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5096
5349
|
res.status(400).json({ error: `Invalid slug "${slug}". Must be lowercase and hyphen-separated.` });
|
|
5097
5350
|
return;
|
|
5098
5351
|
}
|
|
5099
|
-
const projectDir =
|
|
5352
|
+
const projectDir = resolve12(projectsDir, slug);
|
|
5100
5353
|
if (await fileExists(projectDir)) {
|
|
5101
5354
|
res.status(409).json({ error: `Project "${slug}" already exists` });
|
|
5102
5355
|
return;
|
|
5103
5356
|
}
|
|
5104
5357
|
const title = fields.title;
|
|
5105
5358
|
const timestamp = fields.created || nowTimestamp();
|
|
5106
|
-
await ensureDir(
|
|
5107
|
-
await ensureDir(
|
|
5108
|
-
await ensureDir(
|
|
5109
|
-
await writeFileForce(
|
|
5359
|
+
await ensureDir(resolve12(projectDir, "assignments"));
|
|
5360
|
+
await ensureDir(resolve12(projectDir, "resources"));
|
|
5361
|
+
await ensureDir(resolve12(projectDir, "memories"));
|
|
5362
|
+
await writeFileForce(resolve12(projectDir, "project.md"), content);
|
|
5110
5363
|
try {
|
|
5111
5364
|
const companions = [
|
|
5112
|
-
[
|
|
5113
|
-
[
|
|
5114
|
-
[
|
|
5115
|
-
[
|
|
5116
|
-
[
|
|
5117
|
-
[
|
|
5118
|
-
[
|
|
5365
|
+
[resolve12(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
|
|
5366
|
+
[resolve12(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
|
|
5367
|
+
[resolve12(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
|
|
5368
|
+
[resolve12(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
|
|
5369
|
+
[resolve12(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
|
|
5370
|
+
[resolve12(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
|
|
5371
|
+
[resolve12(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
|
|
5119
5372
|
];
|
|
5120
5373
|
for (const [filePath, fileContent] of companions) {
|
|
5121
5374
|
await writeFileForce(filePath, fileContent);
|
|
@@ -5136,8 +5389,8 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5136
5389
|
router.post("/api/projects/:slug/assignments", async (req, res) => {
|
|
5137
5390
|
try {
|
|
5138
5391
|
const projectSlug = getParam(req.params.slug);
|
|
5139
|
-
const projectDir =
|
|
5140
|
-
const projectMdPath =
|
|
5392
|
+
const projectDir = resolve12(projectsDir, projectSlug);
|
|
5393
|
+
const projectMdPath = resolve12(projectDir, "project.md");
|
|
5141
5394
|
if (!await fileExists(projectMdPath)) {
|
|
5142
5395
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
5143
5396
|
return;
|
|
@@ -5167,7 +5420,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5167
5420
|
res.status(400).json({ error: `Invalid priority "${priority}". Must be low, medium, high, or critical.` });
|
|
5168
5421
|
return;
|
|
5169
5422
|
}
|
|
5170
|
-
const assignmentDir =
|
|
5423
|
+
const assignmentDir = resolve12(projectDir, "assignments", assignmentSlug);
|
|
5171
5424
|
if (await fileExists(assignmentDir)) {
|
|
5172
5425
|
res.status(409).json({
|
|
5173
5426
|
error: `Assignment "${assignmentSlug}" already exists in project "${projectSlug}"`
|
|
@@ -5176,12 +5429,12 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5176
5429
|
}
|
|
5177
5430
|
const timestamp = fields.created || nowTimestamp();
|
|
5178
5431
|
await ensureDir(assignmentDir);
|
|
5179
|
-
await writeFileForce(
|
|
5432
|
+
await writeFileForce(resolve12(assignmentDir, "assignment.md"), content);
|
|
5180
5433
|
try {
|
|
5181
5434
|
const companions = [
|
|
5182
|
-
[
|
|
5183
|
-
[
|
|
5184
|
-
[
|
|
5435
|
+
[resolve12(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
|
|
5436
|
+
[resolve12(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
|
|
5437
|
+
[resolve12(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
|
|
5185
5438
|
];
|
|
5186
5439
|
for (const [filePath, fileContent] of companions) {
|
|
5187
5440
|
await writeFileForce(filePath, fileContent);
|
|
@@ -5202,7 +5455,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5202
5455
|
router.patch("/api/projects/:slug", async (req, res) => {
|
|
5203
5456
|
try {
|
|
5204
5457
|
const projectSlug = getParam(req.params.slug);
|
|
5205
|
-
const projectPath =
|
|
5458
|
+
const projectPath = resolve12(projectsDir, projectSlug, "project.md");
|
|
5206
5459
|
const currentContent = await readCurrentDocument(projectPath);
|
|
5207
5460
|
if (!currentContent) {
|
|
5208
5461
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
@@ -5235,7 +5488,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5235
5488
|
try {
|
|
5236
5489
|
const projectSlug = getParam(req.params.slug);
|
|
5237
5490
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5238
|
-
const assignmentPath =
|
|
5491
|
+
const assignmentPath = resolve12(
|
|
5239
5492
|
projectsDir,
|
|
5240
5493
|
projectSlug,
|
|
5241
5494
|
"assignments",
|
|
@@ -5278,7 +5531,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5278
5531
|
try {
|
|
5279
5532
|
const projectSlug = getParam(req.params.slug);
|
|
5280
5533
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5281
|
-
const assignmentPath =
|
|
5534
|
+
const assignmentPath = resolve12(
|
|
5282
5535
|
projectsDir,
|
|
5283
5536
|
projectSlug,
|
|
5284
5537
|
"assignments",
|
|
@@ -5314,7 +5567,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5314
5567
|
try {
|
|
5315
5568
|
const projectSlug = getParam(req.params.slug);
|
|
5316
5569
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5317
|
-
const planPath =
|
|
5570
|
+
const planPath = resolve12(
|
|
5318
5571
|
projectsDir,
|
|
5319
5572
|
projectSlug,
|
|
5320
5573
|
"assignments",
|
|
@@ -5352,7 +5605,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5352
5605
|
try {
|
|
5353
5606
|
const projectSlug = getParam(req.params.slug);
|
|
5354
5607
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5355
|
-
const scratchpadPath =
|
|
5608
|
+
const scratchpadPath = resolve12(
|
|
5356
5609
|
projectsDir,
|
|
5357
5610
|
projectSlug,
|
|
5358
5611
|
"assignments",
|
|
@@ -5390,7 +5643,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5390
5643
|
try {
|
|
5391
5644
|
const projectSlug = getParam(req.params.slug);
|
|
5392
5645
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5393
|
-
const handoffPath =
|
|
5646
|
+
const handoffPath = resolve12(
|
|
5394
5647
|
projectsDir,
|
|
5395
5648
|
projectSlug,
|
|
5396
5649
|
"assignments",
|
|
@@ -5428,7 +5681,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5428
5681
|
try {
|
|
5429
5682
|
const projectSlug = getParam(req.params.slug);
|
|
5430
5683
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5431
|
-
const decisionPath =
|
|
5684
|
+
const decisionPath = resolve12(
|
|
5432
5685
|
projectsDir,
|
|
5433
5686
|
projectSlug,
|
|
5434
5687
|
"assignments",
|
|
@@ -5466,7 +5719,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5466
5719
|
try {
|
|
5467
5720
|
const projectSlug = getParam(req.params.slug);
|
|
5468
5721
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5469
|
-
const commentsPath =
|
|
5722
|
+
const commentsPath = resolve12(
|
|
5470
5723
|
projectsDir,
|
|
5471
5724
|
projectSlug,
|
|
5472
5725
|
"assignments",
|
|
@@ -5484,7 +5737,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5484
5737
|
let currentContent;
|
|
5485
5738
|
let currentCount = 0;
|
|
5486
5739
|
if (await fileExists(commentsPath)) {
|
|
5487
|
-
currentContent = await
|
|
5740
|
+
currentContent = await readFile10(commentsPath, "utf-8");
|
|
5488
5741
|
const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
|
|
5489
5742
|
if (countMatch) currentCount = parseInt(countMatch[1], 10);
|
|
5490
5743
|
} else {
|
|
@@ -5525,7 +5778,7 @@ ${entry}`;
|
|
|
5525
5778
|
const projectSlug = getParam(req.params.slug);
|
|
5526
5779
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5527
5780
|
const commentId = getParam(req.params.commentId);
|
|
5528
|
-
const commentsPath =
|
|
5781
|
+
const commentsPath = resolve12(
|
|
5529
5782
|
projectsDir,
|
|
5530
5783
|
projectSlug,
|
|
5531
5784
|
"assignments",
|
|
@@ -5541,7 +5794,7 @@ ${entry}`;
|
|
|
5541
5794
|
res.status(400).json({ error: "resolved (boolean) is required" });
|
|
5542
5795
|
return;
|
|
5543
5796
|
}
|
|
5544
|
-
const content = await
|
|
5797
|
+
const content = await readFile10(commentsPath, "utf-8");
|
|
5545
5798
|
const parsed = parseComments(content);
|
|
5546
5799
|
const target = parsed.entries.find((e) => e.id === commentId);
|
|
5547
5800
|
if (!target) {
|
|
@@ -5576,7 +5829,7 @@ ${entry}`;
|
|
|
5576
5829
|
router.post("/api/projects/:slug/move-workspace", async (req, res) => {
|
|
5577
5830
|
try {
|
|
5578
5831
|
const projectSlug = getParam(req.params.slug);
|
|
5579
|
-
const projectPath =
|
|
5832
|
+
const projectPath = resolve12(projectsDir, projectSlug, "project.md");
|
|
5580
5833
|
if (!await fileExists(projectPath)) {
|
|
5581
5834
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
5582
5835
|
return;
|
|
@@ -5586,7 +5839,7 @@ ${entry}`;
|
|
|
5586
5839
|
res.status(400).json({ error: "workspace must be a non-empty string or null (for ungrouped)." });
|
|
5587
5840
|
return;
|
|
5588
5841
|
}
|
|
5589
|
-
let content = await
|
|
5842
|
+
let content = await readFile10(projectPath, "utf-8");
|
|
5590
5843
|
content = setTopLevelField(content, "workspace", workspace ?? null);
|
|
5591
5844
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
5592
5845
|
await writeFileForce(projectPath, content);
|
|
@@ -5600,7 +5853,7 @@ ${entry}`;
|
|
|
5600
5853
|
router.post("/api/projects/:slug/status-override", async (req, res) => {
|
|
5601
5854
|
try {
|
|
5602
5855
|
const projectSlug = getParam(req.params.slug);
|
|
5603
|
-
const projectPath =
|
|
5856
|
+
const projectPath = resolve12(projectsDir, projectSlug, "project.md");
|
|
5604
5857
|
if (!await fileExists(projectPath)) {
|
|
5605
5858
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
5606
5859
|
return;
|
|
@@ -5612,7 +5865,7 @@ ${entry}`;
|
|
|
5612
5865
|
res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}, or null to clear.` });
|
|
5613
5866
|
return;
|
|
5614
5867
|
}
|
|
5615
|
-
let content = await
|
|
5868
|
+
let content = await readFile10(projectPath, "utf-8");
|
|
5616
5869
|
content = setTopLevelField(content, "statusOverride", status ?? null);
|
|
5617
5870
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
5618
5871
|
await writeFileForce(projectPath, content);
|
|
@@ -5627,7 +5880,7 @@ ${entry}`;
|
|
|
5627
5880
|
try {
|
|
5628
5881
|
const projectSlug = getParam(req.params.slug);
|
|
5629
5882
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5630
|
-
const assignmentPath =
|
|
5883
|
+
const assignmentPath = resolve12(
|
|
5631
5884
|
projectsDir,
|
|
5632
5885
|
projectSlug,
|
|
5633
5886
|
"assignments",
|
|
@@ -5645,7 +5898,7 @@ ${entry}`;
|
|
|
5645
5898
|
res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
|
|
5646
5899
|
return;
|
|
5647
5900
|
}
|
|
5648
|
-
let content = await
|
|
5901
|
+
let content = await readFile10(assignmentPath, "utf-8");
|
|
5649
5902
|
content = setTopLevelField(content, "status", status);
|
|
5650
5903
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
5651
5904
|
if (status !== "blocked") {
|
|
@@ -5670,8 +5923,8 @@ ${entry}`;
|
|
|
5670
5923
|
res.status(400).json({ error: `Unsupported transition command "${req.params.command}"` });
|
|
5671
5924
|
return;
|
|
5672
5925
|
}
|
|
5673
|
-
const projectDir =
|
|
5674
|
-
const assignmentPath =
|
|
5926
|
+
const projectDir = resolve12(projectsDir, projectSlug);
|
|
5927
|
+
const assignmentPath = resolve12(projectDir, "assignments", assignmentSlug, "assignment.md");
|
|
5675
5928
|
if (!await fileExists(assignmentPath)) {
|
|
5676
5929
|
res.status(404).json({ error: "Assignment not found" });
|
|
5677
5930
|
return;
|
|
@@ -5697,8 +5950,8 @@ ${entry}`;
|
|
|
5697
5950
|
try {
|
|
5698
5951
|
const projectSlug = getParam(req.params.slug);
|
|
5699
5952
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5700
|
-
const assignmentDir =
|
|
5701
|
-
const assignmentPath =
|
|
5953
|
+
const assignmentDir = resolve12(projectsDir, projectSlug, "assignments", assignmentSlug);
|
|
5954
|
+
const assignmentPath = resolve12(assignmentDir, "assignment.md");
|
|
5702
5955
|
if (!await fileExists(assignmentPath)) {
|
|
5703
5956
|
res.status(404).json({ error: `Assignment "${assignmentSlug}" not found in project "${projectSlug}"` });
|
|
5704
5957
|
return;
|
|
@@ -5716,6 +5969,76 @@ ${entry}`;
|
|
|
5716
5969
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
5717
5970
|
return;
|
|
5718
5971
|
}
|
|
5972
|
+
const rawContent = typeof req.body?.content === "string" ? req.body.content : "";
|
|
5973
|
+
if (rawContent.trim()) {
|
|
5974
|
+
const fields = extractFrontmatter3(rawContent);
|
|
5975
|
+
if (!fields) {
|
|
5976
|
+
res.status(400).json({ error: "Invalid frontmatter: missing --- delimiters" });
|
|
5977
|
+
return;
|
|
5978
|
+
}
|
|
5979
|
+
const validation = validateRequired(fields, ["slug", "title"]);
|
|
5980
|
+
if (!validation.valid) {
|
|
5981
|
+
res.status(400).json({ error: `Missing required fields: ${validation.missing.join(", ")}` });
|
|
5982
|
+
return;
|
|
5983
|
+
}
|
|
5984
|
+
const submittedSlug = fields.slug;
|
|
5985
|
+
if (!isValidSlug(submittedSlug)) {
|
|
5986
|
+
res.status(400).json({ error: `Invalid slug "${submittedSlug}". Must be lowercase and hyphen-separated.` });
|
|
5987
|
+
return;
|
|
5988
|
+
}
|
|
5989
|
+
const validPriorities = ["low", "medium", "high", "critical"];
|
|
5990
|
+
const submittedPriority = fields.priority || "medium";
|
|
5991
|
+
if (!validPriorities.includes(submittedPriority)) {
|
|
5992
|
+
res.status(400).json({ error: `Invalid priority "${submittedPriority}". Must be low, medium, high, or critical.` });
|
|
5993
|
+
return;
|
|
5994
|
+
}
|
|
5995
|
+
if (fields.project && fields.project !== "null") {
|
|
5996
|
+
res.status(400).json({
|
|
5997
|
+
error: 'Standalone assignments cannot have a project; remove "project" or set it to null.'
|
|
5998
|
+
});
|
|
5999
|
+
return;
|
|
6000
|
+
}
|
|
6001
|
+
const submittedWorkspaceGroup = fields.workspaceGroup && fields.workspaceGroup !== "null" ? fields.workspaceGroup : "";
|
|
6002
|
+
if (submittedWorkspaceGroup && !isValidSlug(submittedWorkspaceGroup)) {
|
|
6003
|
+
res.status(400).json({
|
|
6004
|
+
error: `Invalid workspace slug "${submittedWorkspaceGroup}". Slugs must be lowercase, hyphen-separated, with no special characters.`
|
|
6005
|
+
});
|
|
6006
|
+
return;
|
|
6007
|
+
}
|
|
6008
|
+
const id2 = generateId();
|
|
6009
|
+
const assignmentDir2 = resolve12(assignmentsDir, id2);
|
|
6010
|
+
if (await fileExists(assignmentDir2)) {
|
|
6011
|
+
res.status(500).json({ error: "UUID collision \u2014 try again" });
|
|
6012
|
+
return;
|
|
6013
|
+
}
|
|
6014
|
+
const timestamp2 = fields.created || nowTimestamp();
|
|
6015
|
+
await ensureDir(assignmentDir2);
|
|
6016
|
+
const normalizedContent = setTopLevelField(rawContent, "id", id2);
|
|
6017
|
+
await writeFileForce(resolve12(assignmentDir2, "assignment.md"), normalizedContent);
|
|
6018
|
+
await writeFileForce(
|
|
6019
|
+
resolve12(assignmentDir2, "scratchpad.md"),
|
|
6020
|
+
renderScratchpad({ assignmentSlug: id2, timestamp: timestamp2 })
|
|
6021
|
+
);
|
|
6022
|
+
await writeFileForce(
|
|
6023
|
+
resolve12(assignmentDir2, "handoff.md"),
|
|
6024
|
+
renderHandoff({ assignmentSlug: id2, timestamp: timestamp2 })
|
|
6025
|
+
);
|
|
6026
|
+
await writeFileForce(
|
|
6027
|
+
resolve12(assignmentDir2, "decision-record.md"),
|
|
6028
|
+
renderDecisionRecord({ assignmentSlug: id2, timestamp: timestamp2 })
|
|
6029
|
+
);
|
|
6030
|
+
await writeFileForce(
|
|
6031
|
+
resolve12(assignmentDir2, "progress.md"),
|
|
6032
|
+
renderProgress({ assignment: id2, timestamp: timestamp2 })
|
|
6033
|
+
);
|
|
6034
|
+
await writeFileForce(
|
|
6035
|
+
resolve12(assignmentDir2, "comments.md"),
|
|
6036
|
+
renderComments({ assignment: id2, timestamp: timestamp2 })
|
|
6037
|
+
);
|
|
6038
|
+
const detail2 = await getAssignmentDetailById(projectsDir, assignmentsDir, id2);
|
|
6039
|
+
res.status(201).json({ assignment: detail2 });
|
|
6040
|
+
return;
|
|
6041
|
+
}
|
|
5719
6042
|
const { title, slug, priority, type } = req.body || {};
|
|
5720
6043
|
if (!title || typeof title !== "string" || !title.trim()) {
|
|
5721
6044
|
res.status(400).json({ error: "title is required" });
|
|
@@ -5727,7 +6050,7 @@ ${entry}`;
|
|
|
5727
6050
|
return;
|
|
5728
6051
|
}
|
|
5729
6052
|
const id = generateId();
|
|
5730
|
-
const assignmentDir =
|
|
6053
|
+
const assignmentDir = resolve12(assignmentsDir, id);
|
|
5731
6054
|
if (await fileExists(assignmentDir)) {
|
|
5732
6055
|
res.status(500).json({ error: "UUID collision \u2014 try again" });
|
|
5733
6056
|
return;
|
|
@@ -5747,25 +6070,25 @@ ${entry}`;
|
|
|
5747
6070
|
project: null,
|
|
5748
6071
|
type: typeof type === "string" ? type : void 0
|
|
5749
6072
|
});
|
|
5750
|
-
await writeFileForce(
|
|
6073
|
+
await writeFileForce(resolve12(assignmentDir, "assignment.md"), assignmentContent);
|
|
5751
6074
|
await writeFileForce(
|
|
5752
|
-
|
|
6075
|
+
resolve12(assignmentDir, "scratchpad.md"),
|
|
5753
6076
|
renderScratchpad({ assignmentSlug: id, timestamp })
|
|
5754
6077
|
);
|
|
5755
6078
|
await writeFileForce(
|
|
5756
|
-
|
|
6079
|
+
resolve12(assignmentDir, "handoff.md"),
|
|
5757
6080
|
renderHandoff({ assignmentSlug: id, timestamp })
|
|
5758
6081
|
);
|
|
5759
6082
|
await writeFileForce(
|
|
5760
|
-
|
|
6083
|
+
resolve12(assignmentDir, "decision-record.md"),
|
|
5761
6084
|
renderDecisionRecord({ assignmentSlug: id, timestamp })
|
|
5762
6085
|
);
|
|
5763
6086
|
await writeFileForce(
|
|
5764
|
-
|
|
6087
|
+
resolve12(assignmentDir, "progress.md"),
|
|
5765
6088
|
renderProgress({ assignment: id, timestamp })
|
|
5766
6089
|
);
|
|
5767
6090
|
await writeFileForce(
|
|
5768
|
-
|
|
6091
|
+
resolve12(assignmentDir, "comments.md"),
|
|
5769
6092
|
renderComments({ assignment: id, timestamp })
|
|
5770
6093
|
);
|
|
5771
6094
|
const detail = await getAssignmentDetailById(projectsDir, assignmentsDir, id);
|
|
@@ -5893,7 +6216,7 @@ ${entry}`;
|
|
|
5893
6216
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
5894
6217
|
return;
|
|
5895
6218
|
}
|
|
5896
|
-
const assignmentPath =
|
|
6219
|
+
const assignmentPath = resolve12(resolved.assignmentDir, "assignment.md");
|
|
5897
6220
|
const currentContent = await readCurrentDocument(assignmentPath);
|
|
5898
6221
|
if (!currentContent) {
|
|
5899
6222
|
res.status(404).json({ error: "Assignment not found" });
|
|
@@ -5935,7 +6258,7 @@ ${entry}`;
|
|
|
5935
6258
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
5936
6259
|
return;
|
|
5937
6260
|
}
|
|
5938
|
-
const planPath =
|
|
6261
|
+
const planPath = resolve12(resolved.assignmentDir, "plan.md");
|
|
5939
6262
|
const currentContent = await readCurrentDocument(planPath);
|
|
5940
6263
|
if (!currentContent) {
|
|
5941
6264
|
res.status(404).json({ error: "Plan not found" });
|
|
@@ -5969,7 +6292,7 @@ ${entry}`;
|
|
|
5969
6292
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
5970
6293
|
return;
|
|
5971
6294
|
}
|
|
5972
|
-
const scratchpadPath =
|
|
6295
|
+
const scratchpadPath = resolve12(resolved.assignmentDir, "scratchpad.md");
|
|
5973
6296
|
const currentContent = await readCurrentDocument(scratchpadPath);
|
|
5974
6297
|
if (!currentContent) {
|
|
5975
6298
|
res.status(404).json({ error: "Scratchpad not found" });
|
|
@@ -6003,7 +6326,7 @@ ${entry}`;
|
|
|
6003
6326
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6004
6327
|
return;
|
|
6005
6328
|
}
|
|
6006
|
-
const handoffPath =
|
|
6329
|
+
const handoffPath = resolve12(resolved.assignmentDir, "handoff.md");
|
|
6007
6330
|
const currentContent = await readCurrentDocument(handoffPath);
|
|
6008
6331
|
if (!currentContent) {
|
|
6009
6332
|
res.status(404).json({ error: "Handoff log not found" });
|
|
@@ -6043,7 +6366,7 @@ ${entry}`;
|
|
|
6043
6366
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6044
6367
|
return;
|
|
6045
6368
|
}
|
|
6046
|
-
const decisionPath =
|
|
6369
|
+
const decisionPath = resolve12(resolved.assignmentDir, "decision-record.md");
|
|
6047
6370
|
const currentContent = await readCurrentDocument(decisionPath);
|
|
6048
6371
|
if (!currentContent) {
|
|
6049
6372
|
res.status(404).json({ error: "Decision record not found" });
|
|
@@ -6083,7 +6406,7 @@ ${entry}`;
|
|
|
6083
6406
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6084
6407
|
return;
|
|
6085
6408
|
}
|
|
6086
|
-
const assignmentPath =
|
|
6409
|
+
const assignmentPath = resolve12(resolved.assignmentDir, "assignment.md");
|
|
6087
6410
|
if (!await fileExists(assignmentPath)) {
|
|
6088
6411
|
res.status(404).json({ error: "Assignment not found" });
|
|
6089
6412
|
return;
|
|
@@ -6095,7 +6418,7 @@ ${entry}`;
|
|
|
6095
6418
|
res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
|
|
6096
6419
|
return;
|
|
6097
6420
|
}
|
|
6098
|
-
let content = await
|
|
6421
|
+
let content = await readFile10(assignmentPath, "utf-8");
|
|
6099
6422
|
content = setTopLevelField(content, "status", status);
|
|
6100
6423
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
6101
6424
|
if (status !== "blocked") {
|
|
@@ -6121,7 +6444,7 @@ ${entry}`;
|
|
|
6121
6444
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6122
6445
|
return;
|
|
6123
6446
|
}
|
|
6124
|
-
const assignmentPath =
|
|
6447
|
+
const assignmentPath = resolve12(resolved.assignmentDir, "assignment.md");
|
|
6125
6448
|
const currentContent = await readCurrentDocument(assignmentPath);
|
|
6126
6449
|
if (!currentContent) {
|
|
6127
6450
|
res.status(404).json({ error: "Assignment not found" });
|
|
@@ -6186,7 +6509,7 @@ function slugifyLocal(input) {
|
|
|
6186
6509
|
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
|
|
6187
6510
|
}
|
|
6188
6511
|
async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDetail) {
|
|
6189
|
-
const commentsPath =
|
|
6512
|
+
const commentsPath = resolve12(assignmentDir, "comments.md");
|
|
6190
6513
|
const { body, author, type, replyTo } = req.body || {};
|
|
6191
6514
|
if (!body || typeof body !== "string" || !body.trim()) {
|
|
6192
6515
|
res.status(400).json({ error: "body is required" });
|
|
@@ -6198,7 +6521,7 @@ async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDet
|
|
|
6198
6521
|
let currentContent;
|
|
6199
6522
|
let currentCount = 0;
|
|
6200
6523
|
if (await fileExists(commentsPath)) {
|
|
6201
|
-
currentContent = await
|
|
6524
|
+
currentContent = await readFile10(commentsPath, "utf-8");
|
|
6202
6525
|
const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
|
|
6203
6526
|
if (countMatch) currentCount = parseInt(countMatch[1], 10);
|
|
6204
6527
|
} else {
|
|
@@ -6228,7 +6551,7 @@ ${entry}`;
|
|
|
6228
6551
|
res.status(201).json({ assignment, comment: { id: comment.id } });
|
|
6229
6552
|
}
|
|
6230
6553
|
async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloadDetail) {
|
|
6231
|
-
const commentsPath =
|
|
6554
|
+
const commentsPath = resolve12(assignmentDir, "comments.md");
|
|
6232
6555
|
if (!await fileExists(commentsPath)) {
|
|
6233
6556
|
res.status(404).json({ error: "Comments file not found" });
|
|
6234
6557
|
return;
|
|
@@ -6238,7 +6561,7 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloa
|
|
|
6238
6561
|
res.status(400).json({ error: "resolved (boolean) is required" });
|
|
6239
6562
|
return;
|
|
6240
6563
|
}
|
|
6241
|
-
const content = await
|
|
6564
|
+
const content = await readFile10(commentsPath, "utf-8");
|
|
6242
6565
|
const parsed = parseComments(content);
|
|
6243
6566
|
const target = parsed.entries.find((e) => e.id === commentId);
|
|
6244
6567
|
if (!target) {
|
|
@@ -6383,7 +6706,7 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir) {
|
|
|
6383
6706
|
|
|
6384
6707
|
// src/dashboard/api-agent-sessions.ts
|
|
6385
6708
|
import { Router as Router3 } from "express";
|
|
6386
|
-
import { resolve as
|
|
6709
|
+
import { resolve as resolve13 } from "path";
|
|
6387
6710
|
init_fs();
|
|
6388
6711
|
function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
|
|
6389
6712
|
const router = Router3();
|
|
@@ -6400,7 +6723,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
|
|
|
6400
6723
|
try {
|
|
6401
6724
|
const { projectSlug } = req.params;
|
|
6402
6725
|
const assignment = req.query.assignment;
|
|
6403
|
-
const projectDir =
|
|
6726
|
+
const projectDir = resolve13(projectsDir, projectSlug);
|
|
6404
6727
|
if (!await fileExists(projectDir)) {
|
|
6405
6728
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
6406
6729
|
return;
|
|
@@ -6426,7 +6749,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
|
|
|
6426
6749
|
return;
|
|
6427
6750
|
}
|
|
6428
6751
|
if (projectSlug) {
|
|
6429
|
-
const projectDir =
|
|
6752
|
+
const projectDir = resolve13(projectsDir, projectSlug);
|
|
6430
6753
|
if (!await fileExists(projectDir)) {
|
|
6431
6754
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
6432
6755
|
return;
|
|
@@ -6498,53 +6821,7 @@ import { resolve as resolve14 } from "path";
|
|
|
6498
6821
|
import { readFile as readFile11, unlink as unlink2 } from "fs/promises";
|
|
6499
6822
|
init_timestamp();
|
|
6500
6823
|
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
|
|
6824
|
+
init_playbooks();
|
|
6548
6825
|
function createPlaybooksRouter(playbooksDir2) {
|
|
6549
6826
|
const router = Router4();
|
|
6550
6827
|
router.get("/", async (_req, res) => {
|
|
@@ -6568,6 +6845,32 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
6568
6845
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get template" });
|
|
6569
6846
|
}
|
|
6570
6847
|
});
|
|
6848
|
+
router.post("/:slug/enable", async (req, res) => {
|
|
6849
|
+
try {
|
|
6850
|
+
const result = await setPlaybookEnabled(playbooksDir2, req.params.slug, true);
|
|
6851
|
+
res.json({ slug: result.slug, enabled: result.enabled, changed: result.changed });
|
|
6852
|
+
} catch (error) {
|
|
6853
|
+
const msg = error instanceof Error ? error.message : "Failed to enable playbook";
|
|
6854
|
+
if (msg.startsWith("Playbook ")) {
|
|
6855
|
+
res.status(404).json({ error: msg });
|
|
6856
|
+
return;
|
|
6857
|
+
}
|
|
6858
|
+
res.status(500).json({ error: msg });
|
|
6859
|
+
}
|
|
6860
|
+
});
|
|
6861
|
+
router.post("/:slug/disable", async (req, res) => {
|
|
6862
|
+
try {
|
|
6863
|
+
const result = await setPlaybookEnabled(playbooksDir2, req.params.slug, false);
|
|
6864
|
+
res.json({ slug: result.slug, enabled: result.enabled, changed: result.changed });
|
|
6865
|
+
} catch (error) {
|
|
6866
|
+
const msg = error instanceof Error ? error.message : "Failed to disable playbook";
|
|
6867
|
+
if (msg.startsWith("Playbook ")) {
|
|
6868
|
+
res.status(404).json({ error: msg });
|
|
6869
|
+
return;
|
|
6870
|
+
}
|
|
6871
|
+
res.status(500).json({ error: msg });
|
|
6872
|
+
}
|
|
6873
|
+
});
|
|
6571
6874
|
router.get("/:slug", async (req, res) => {
|
|
6572
6875
|
try {
|
|
6573
6876
|
const detail = await getPlaybookDetail(playbooksDir2, req.params.slug);
|
|
@@ -6582,17 +6885,18 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
6582
6885
|
});
|
|
6583
6886
|
router.get("/:slug/edit", async (req, res) => {
|
|
6584
6887
|
try {
|
|
6585
|
-
const
|
|
6586
|
-
if (!
|
|
6888
|
+
const resolved = await resolvePlaybookSlug(playbooksDir2, req.params.slug);
|
|
6889
|
+
if (!resolved) {
|
|
6587
6890
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
6588
6891
|
return;
|
|
6589
6892
|
}
|
|
6893
|
+
const filePath = resolve14(playbooksDir2, resolved.filename);
|
|
6590
6894
|
const content = await readFile11(filePath, "utf-8");
|
|
6591
6895
|
res.json({
|
|
6592
6896
|
documentType: "playbook",
|
|
6593
|
-
title: `Edit Playbook: ${
|
|
6897
|
+
title: `Edit Playbook: ${resolved.slug}`,
|
|
6594
6898
|
content,
|
|
6595
|
-
slug:
|
|
6899
|
+
slug: resolved.slug
|
|
6596
6900
|
});
|
|
6597
6901
|
} catch (error) {
|
|
6598
6902
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get playbook for editing" });
|
|
@@ -6631,14 +6935,15 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
6631
6935
|
res.status(400).json({ error: "content is required" });
|
|
6632
6936
|
return;
|
|
6633
6937
|
}
|
|
6634
|
-
const
|
|
6635
|
-
if (!
|
|
6938
|
+
const resolved = await resolvePlaybookSlug(playbooksDir2, req.params.slug);
|
|
6939
|
+
if (!resolved) {
|
|
6636
6940
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
6637
6941
|
return;
|
|
6638
6942
|
}
|
|
6943
|
+
const filePath = resolve14(playbooksDir2, resolved.filename);
|
|
6639
6944
|
await writeFileForce(filePath, content);
|
|
6640
6945
|
await rebuildPlaybookManifest(playbooksDir2);
|
|
6641
|
-
res.json({ slug:
|
|
6946
|
+
res.json({ slug: resolved.slug, path: filePath });
|
|
6642
6947
|
} catch (error) {
|
|
6643
6948
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to update playbook" });
|
|
6644
6949
|
}
|
|
@@ -6649,14 +6954,16 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
6649
6954
|
res.status(403).json({ error: "The playbook manifest cannot be deleted" });
|
|
6650
6955
|
return;
|
|
6651
6956
|
}
|
|
6652
|
-
const
|
|
6653
|
-
if (!
|
|
6957
|
+
const resolved = await resolvePlaybookSlug(playbooksDir2, req.params.slug);
|
|
6958
|
+
if (!resolved) {
|
|
6654
6959
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
6655
6960
|
return;
|
|
6656
6961
|
}
|
|
6962
|
+
const filePath = resolve14(playbooksDir2, resolved.filename);
|
|
6657
6963
|
await unlink2(filePath);
|
|
6964
|
+
await removeFromDisabledList(resolved.slug);
|
|
6658
6965
|
await rebuildPlaybookManifest(playbooksDir2);
|
|
6659
|
-
res.json({ deleted:
|
|
6966
|
+
res.json({ deleted: resolved.slug });
|
|
6660
6967
|
} catch (error) {
|
|
6661
6968
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to delete playbook" });
|
|
6662
6969
|
}
|
|
@@ -7836,7 +8143,7 @@ function createDashboardServer(options) {
|
|
|
7836
8143
|
});
|
|
7837
8144
|
app.get("/api/workspaces", async (_req, res) => {
|
|
7838
8145
|
try {
|
|
7839
|
-
const result = await listWorkspaces(projectsDir);
|
|
8146
|
+
const result = await listWorkspaces(projectsDir, assignmentsDir);
|
|
7840
8147
|
res.json(result);
|
|
7841
8148
|
} catch (error) {
|
|
7842
8149
|
console.error("Error listing workspaces:", error);
|
|
@@ -7955,16 +8262,25 @@ function createDashboardServer(options) {
|
|
|
7955
8262
|
app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
|
|
7956
8263
|
app.use("/api/backup", createBackupRouter());
|
|
7957
8264
|
if (serveStaticUi && dashboardDistPath) {
|
|
7958
|
-
app.use(express.static(dashboardDistPath));
|
|
7959
|
-
app.get("{*path}", async (
|
|
8265
|
+
app.use("/assets", express.static(resolve17(dashboardDistPath, "assets")));
|
|
8266
|
+
app.get("{*path}", async (req, res) => {
|
|
8267
|
+
if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
|
|
8268
|
+
res.status(404).json({ error: "Not Found" });
|
|
8269
|
+
return;
|
|
8270
|
+
}
|
|
7960
8271
|
const indexPath = resolve17(dashboardDistPath, "index.html");
|
|
7961
|
-
if (await fileExists(indexPath)) {
|
|
7962
|
-
res.sendFile(indexPath);
|
|
7963
|
-
} else {
|
|
8272
|
+
if (!await fileExists(indexPath)) {
|
|
7964
8273
|
res.status(503).send(
|
|
7965
8274
|
'Dashboard not built. Run "npm run build:dashboard" first.'
|
|
7966
8275
|
);
|
|
8276
|
+
return;
|
|
7967
8277
|
}
|
|
8278
|
+
res.sendFile(indexPath, (err) => {
|
|
8279
|
+
if (err) {
|
|
8280
|
+
console.error("Error sending dashboard index.html:", err);
|
|
8281
|
+
if (!res.headersSent) res.status(500).send("Dashboard load error");
|
|
8282
|
+
}
|
|
8283
|
+
});
|
|
7968
8284
|
});
|
|
7969
8285
|
}
|
|
7970
8286
|
let watcherHandle = null;
|