syntaur 0.4.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/assets/{_basePickBy-CWivToyi.js → _basePickBy-Bcut0btZ.js} +1 -1
- package/dashboard/dist/assets/{_baseUniq-C-qj6E4l.js → _baseUniq-AQSP2JEk.js} +1 -1
- package/dashboard/dist/assets/{arc-Dn5BIqMa.js → arc-BLTpY9lc.js} +1 -1
- package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-D5D0K7rY.js → architectureDiagram-2XIMDMQ5-CJtwMY_X.js} +1 -1
- package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-DVmYPMbu.js → blockDiagram-WCTKOSBZ-Don-O7X7.js} +1 -1
- package/dashboard/dist/assets/{c4Diagram-IC4MRINW-BEasxbnl.js → c4Diagram-IC4MRINW-C_M3yTTB.js} +1 -1
- package/dashboard/dist/assets/channel-BfXmPwE5.js +1 -0
- package/dashboard/dist/assets/{chunk-4BX2VUAB-LDIrtI5E.js → chunk-4BX2VUAB-CGss0jXe.js} +1 -1
- package/dashboard/dist/assets/{chunk-55IACEB6-CaEBUJYu.js → chunk-55IACEB6-BatoPJga.js} +1 -1
- package/dashboard/dist/assets/{chunk-FMBD7UC4-B-GjCpdr.js → chunk-FMBD7UC4-DxH4wO82.js} +1 -1
- package/dashboard/dist/assets/{chunk-JSJVCQXG-BLVVcezm.js → chunk-JSJVCQXG-BL3izAFQ.js} +1 -1
- package/dashboard/dist/assets/{chunk-KX2RTZJC-DqCNEw4h.js → chunk-KX2RTZJC-GnqXwnge.js} +1 -1
- package/dashboard/dist/assets/{chunk-NQ4KR5QH-BCPbFf5I.js → chunk-NQ4KR5QH-gvCn4QMb.js} +1 -1
- package/dashboard/dist/assets/{chunk-QZHKN3VN-Ci0C85q_.js → chunk-QZHKN3VN-CYGWogyi.js} +1 -1
- package/dashboard/dist/assets/{chunk-WL4C6EOR-VVhAMMYU.js → chunk-WL4C6EOR-D9mVTQ1F.js} +1 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-D7_G1qy0.js +1 -0
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-D7_G1qy0.js +1 -0
- package/dashboard/dist/assets/clone-BKG-N796.js +1 -0
- package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-CO9uwgYO.js → cose-bilkent-S5V4N54A-CUWQCKt4.js} +1 -1
- package/dashboard/dist/assets/{dagre-KLK3FWXG-bwLLXcL4.js → dagre-KLK3FWXG-CH3ijEvV.js} +1 -1
- package/dashboard/dist/assets/{diagram-E7M64L7V-RuS5R6V1.js → diagram-E7M64L7V-sq83lpV1.js} +1 -1
- package/dashboard/dist/assets/{diagram-IFDJBPK2-BQDJAHQd.js → diagram-IFDJBPK2-BzQG_rtq.js} +1 -1
- package/dashboard/dist/assets/{diagram-P4PSJMXO-yLEsgzE5.js → diagram-P4PSJMXO-Dg0eZn0q.js} +1 -1
- package/dashboard/dist/assets/{erDiagram-INFDFZHY-na6dUhY0.js → erDiagram-INFDFZHY-4b9eQ0uj.js} +1 -1
- package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-BIcrzwJR.js → flowDiagram-PKNHOUZH-C9fzKcsZ.js} +1 -1
- package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-DHWRJn-D.js → ganttDiagram-A5KZAMGK-Bzt6i9SH.js} +1 -1
- package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-LGxDjL71.js → gitGraphDiagram-K3NZZRJ6-D0wFOagh.js} +1 -1
- package/dashboard/dist/assets/{graph-BUqNu277.js → graph-EEIGvqDh.js} +1 -1
- package/dashboard/dist/assets/index-Bu6ma6my.css +1 -0
- package/dashboard/dist/assets/index-C7f0ySJE.js +481 -0
- package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-DidoA2hb.js → infoDiagram-LFFYTUFH-DLYMsj1D.js} +1 -1
- package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-CdlZkbhV.js → ishikawaDiagram-PHBUUO56-DVebKkzl.js} +1 -1
- package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-luhcz_gn.js → journeyDiagram-4ABVD52K-BsmgOWVw.js} +1 -1
- package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-Coidw9XE.js → kanban-definition-K7BYSVSG-BTnHf0ey.js} +1 -1
- package/dashboard/dist/assets/{layout-_aBAAleE.js → layout-BbM7HRvv.js} +1 -1
- package/dashboard/dist/assets/{linear-D8mFnDSx.js → linear-C37bJKPO.js} +1 -1
- package/dashboard/dist/assets/{mermaid.core-BpP2keU-.js → mermaid.core-MZ_JgnRL.js} +4 -4
- package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-LjvrTe2z.js → mindmap-definition-YRQLILUH-CgHS4hFo.js} +1 -1
- package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CkA2iU6e.js → pieDiagram-SKSYHLDU-CmAgopJe.js} +1 -1
- package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-BRmhKHQG.js → quadrantDiagram-337W2JSQ-BvzYUPR6.js} +1 -1
- package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BYCQ4uFX.js → requirementDiagram-Z7DCOOCP-Bs52VP7k.js} +1 -1
- package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-C8SVk50M.js → sankeyDiagram-WA2Y5GQK-aXvGPR1o.js} +1 -1
- package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-CbA_2lnP.js → sequenceDiagram-2WXFIKYE-CzgcfU6K.js} +1 -1
- package/dashboard/dist/assets/{stateDiagram-RAJIS63D-D6ZtjAHE.js → stateDiagram-RAJIS63D-BXBJf9Hq.js} +1 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-QqOtsuOs.js +1 -0
- package/dashboard/dist/assets/{timeline-definition-YZTLITO2-B2Uf-emK.js → timeline-definition-YZTLITO2-BsXp26Ai.js} +1 -1
- package/dashboard/dist/assets/{treemap-KZPCXAKY-CYnFKsuJ.js → treemap-KZPCXAKY-C3WbDii1.js} +1 -1
- package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-Cnj0qiDO.js → vennDiagram-LZ73GAT5-B28LMHWd.js} +1 -1
- package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-BJy2mbL9.js → xychartDiagram-JWTSCODW-C3Xwz8mS.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/dashboard/server.js +1329 -402
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +1591 -533
- package/dist/index.js.map +1 -1
- package/examples/playbooks/assignment-creation.md +19 -0
- package/examples/playbooks/assignment-planning.md +28 -0
- package/package.json +1 -1
- package/platforms/claude-code/.orphaned_at +1 -0
- package/platforms/claude-code/agents/syntaur-expert.md +2 -2
- package/platforms/claude-code/commands/create-assignment/create-assignment.md +1 -1
- package/platforms/codex/agents/syntaur-operator.md +2 -2
- package/vendor/syntaur-skills/README.md +12 -0
- package/vendor/syntaur-skills/skills/create-assignment/SKILL.md +5 -4
- package/dashboard/dist/assets/channel-DqU_8tiy.js +0 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-D29Eeoe8.js +0 -1
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-D29Eeoe8.js +0 -1
- package/dashboard/dist/assets/clone-Bok8Q3Jj.js +0 -1
- package/dashboard/dist/assets/index-D-fepllQ.js +0 -481
- package/dashboard/dist/assets/index-DnHyQJJH.css +0 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DeYN4wV6.js +0 -1
- package/examples/playbooks/plan-versioning.md +0 -36
- package/examples/playbooks/read-before-plan.md +0 -30
package/dist/index.js
CHANGED
|
@@ -39,6 +39,9 @@ function playbooksDir() {
|
|
|
39
39
|
function todosDir() {
|
|
40
40
|
return resolve(syntaurRoot(), "todos");
|
|
41
41
|
}
|
|
42
|
+
function projectTodosDir(projectsDir2, projectSlug) {
|
|
43
|
+
return resolve(projectsDir2, projectSlug, "todos");
|
|
44
|
+
}
|
|
42
45
|
var init_paths = __esm({
|
|
43
46
|
"src/utils/paths.ts"() {
|
|
44
47
|
"use strict";
|
|
@@ -91,10 +94,10 @@ var init_fs = __esm({
|
|
|
91
94
|
});
|
|
92
95
|
|
|
93
96
|
// src/templates/config.ts
|
|
94
|
-
function renderConfig(
|
|
97
|
+
function renderConfig(params2) {
|
|
95
98
|
return `---
|
|
96
99
|
version: "1.0"
|
|
97
|
-
defaultProjectDir: ${
|
|
100
|
+
defaultProjectDir: ${params2.defaultProjectDir}
|
|
98
101
|
onboarding:
|
|
99
102
|
completed: false
|
|
100
103
|
agentDefaults:
|
|
@@ -247,6 +250,7 @@ function parseAssignmentFull(fileContent) {
|
|
|
247
250
|
slug: getField(fm, "slug") ?? "",
|
|
248
251
|
title: getField(fm, "title") ?? "",
|
|
249
252
|
project: getField(fm, "project"),
|
|
253
|
+
workspaceGroup: getField(fm, "workspaceGroup"),
|
|
250
254
|
type: getField(fm, "type"),
|
|
251
255
|
status: getField(fm, "status") ?? "pending",
|
|
252
256
|
priority: getField(fm, "priority") ?? "medium",
|
|
@@ -414,8 +418,8 @@ var init_timestamp = __esm({
|
|
|
414
418
|
});
|
|
415
419
|
|
|
416
420
|
// src/utils/fs-migration.ts
|
|
417
|
-
import { readdir
|
|
418
|
-
import { resolve as
|
|
421
|
+
import { readdir, readFile, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
422
|
+
import { resolve as resolve2 } from "path";
|
|
419
423
|
async function migrateLegacyProjectFiles(projectsDir2) {
|
|
420
424
|
const result = {
|
|
421
425
|
renamedProjectFiles: [],
|
|
@@ -424,15 +428,15 @@ async function migrateLegacyProjectFiles(projectsDir2) {
|
|
|
424
428
|
if (!await fileExists(projectsDir2)) return result;
|
|
425
429
|
let entries;
|
|
426
430
|
try {
|
|
427
|
-
entries = await
|
|
431
|
+
entries = await readdir(projectsDir2, { withFileTypes: true });
|
|
428
432
|
} catch {
|
|
429
433
|
return result;
|
|
430
434
|
}
|
|
431
435
|
for (const entry of entries) {
|
|
432
436
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
433
|
-
const projectDir =
|
|
434
|
-
const legacy =
|
|
435
|
-
const target =
|
|
437
|
+
const projectDir = resolve2(projectsDir2, entry.name);
|
|
438
|
+
const legacy = resolve2(projectDir, "mission.md");
|
|
439
|
+
const target = resolve2(projectDir, "project.md");
|
|
436
440
|
try {
|
|
437
441
|
if (await fileExists(legacy) && !await fileExists(target)) {
|
|
438
442
|
await rename2(legacy, target);
|
|
@@ -443,7 +447,7 @@ async function migrateLegacyProjectFiles(projectsDir2) {
|
|
|
443
447
|
}
|
|
444
448
|
for (const stale of ["agent.md", "claude.md"]) {
|
|
445
449
|
try {
|
|
446
|
-
if (await fileExists(
|
|
450
|
+
if (await fileExists(resolve2(projectDir, stale))) {
|
|
447
451
|
result.legacyExtras.push(`${entry.name}/${stale}`);
|
|
448
452
|
}
|
|
449
453
|
} catch {
|
|
@@ -461,7 +465,7 @@ async function migrateLegacyConfig(configPath) {
|
|
|
461
465
|
if (!await fileExists(configPath)) return result;
|
|
462
466
|
let content;
|
|
463
467
|
try {
|
|
464
|
-
content = await
|
|
468
|
+
content = await readFile(configPath, "utf-8");
|
|
465
469
|
} catch {
|
|
466
470
|
return result;
|
|
467
471
|
}
|
|
@@ -490,7 +494,7 @@ async function migrateLegacyConfig(configPath) {
|
|
|
490
494
|
const projectLineRe = /^\s*defaultProjectDir\s*:\s*(.*)$/m;
|
|
491
495
|
const projectLineMatch = newFmBlock.match(projectLineRe);
|
|
492
496
|
const projectsDirRaw = projectLineMatch ? projectLineMatch[1].trim().replace(/^['"]|['"]$/g, "") : missionValue;
|
|
493
|
-
const expand = (p) => p.startsWith("~") ?
|
|
497
|
+
const expand = (p) => p.startsWith("~") ? resolve2(process.env.HOME ?? "/", p.slice(p.startsWith("~/") ? 2 : 1)) : p;
|
|
494
498
|
let resolvedProjectsDir = projectsDirRaw ? expand(projectsDirRaw) : null;
|
|
495
499
|
if (resolvedProjectsDir && resolvedProjectsDir.endsWith("/missions")) {
|
|
496
500
|
const siblingProjectsDir = resolvedProjectsDir.replace(/\/missions$/, "/projects");
|
|
@@ -549,8 +553,29 @@ var init_fs_migration = __esm({
|
|
|
549
553
|
});
|
|
550
554
|
|
|
551
555
|
// src/utils/config.ts
|
|
552
|
-
import { readFile as
|
|
553
|
-
import { resolve as
|
|
556
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
557
|
+
import { resolve as resolve3, isAbsolute } from "path";
|
|
558
|
+
function cloneDefaultConfig() {
|
|
559
|
+
return {
|
|
560
|
+
...DEFAULT_CONFIG,
|
|
561
|
+
onboarding: { ...DEFAULT_CONFIG.onboarding },
|
|
562
|
+
agentDefaults: { ...DEFAULT_CONFIG.agentDefaults },
|
|
563
|
+
integrations: { ...DEFAULT_CONFIG.integrations },
|
|
564
|
+
backup: DEFAULT_CONFIG.backup ? { ...DEFAULT_CONFIG.backup } : null,
|
|
565
|
+
statuses: DEFAULT_CONFIG.statuses ? {
|
|
566
|
+
statuses: DEFAULT_CONFIG.statuses.statuses.map((s) => ({ ...s })),
|
|
567
|
+
order: [...DEFAULT_CONFIG.statuses.order],
|
|
568
|
+
transitions: DEFAULT_CONFIG.statuses.transitions.map((t) => ({ ...t }))
|
|
569
|
+
} : null,
|
|
570
|
+
types: DEFAULT_CONFIG.types ? {
|
|
571
|
+
definitions: DEFAULT_CONFIG.types.definitions.map((d) => ({ ...d })),
|
|
572
|
+
default: DEFAULT_CONFIG.types.default
|
|
573
|
+
} : null,
|
|
574
|
+
playbooks: {
|
|
575
|
+
disabled: [...DEFAULT_CONFIG.playbooks.disabled]
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
554
579
|
function parseFrontmatter(content) {
|
|
555
580
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
556
581
|
if (!match) return {};
|
|
@@ -722,6 +747,82 @@ function serializeBackupConfig(backup) {
|
|
|
722
747
|
lines.push(` lastRestore: ${backup.lastRestore ?? "null"}`);
|
|
723
748
|
return lines.join("\n");
|
|
724
749
|
}
|
|
750
|
+
function serializePlaybooksConfig(playbooks) {
|
|
751
|
+
if (!playbooks.disabled || playbooks.disabled.length === 0) {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
const lines = ["playbooks:", " disabled:"];
|
|
755
|
+
for (const slug of playbooks.disabled) {
|
|
756
|
+
lines.push(` - ${slug}`);
|
|
757
|
+
}
|
|
758
|
+
return lines.join("\n");
|
|
759
|
+
}
|
|
760
|
+
function parsePlaybooksConfig(fmBlock) {
|
|
761
|
+
const blockStart = fmBlock.match(/^playbooks:\s*$/m);
|
|
762
|
+
if (!blockStart) {
|
|
763
|
+
return { disabled: [] };
|
|
764
|
+
}
|
|
765
|
+
const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
|
|
766
|
+
const remaining = fmBlock.slice(startIdx).split("\n");
|
|
767
|
+
const disabled = [];
|
|
768
|
+
let currentSection = null;
|
|
769
|
+
for (const line of remaining) {
|
|
770
|
+
const trimmed = line.trimStart();
|
|
771
|
+
const indent = line.length - trimmed.length;
|
|
772
|
+
if (indent === 0 && trimmed.length > 0) break;
|
|
773
|
+
if (trimmed === "") continue;
|
|
774
|
+
if (indent === 2 && trimmed.startsWith("disabled:")) {
|
|
775
|
+
currentSection = "disabled";
|
|
776
|
+
const afterColon = trimmed.slice("disabled:".length).trim();
|
|
777
|
+
if (afterColon === "[]" || afterColon === "") {
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
if (currentSection === "disabled" && indent >= 4 && trimmed.startsWith("- ")) {
|
|
783
|
+
const raw = trimmed.slice(2).trim().replace(/^["']|["']$/g, "");
|
|
784
|
+
if (raw.length === 0) continue;
|
|
785
|
+
if (/\s/.test(raw)) {
|
|
786
|
+
console.warn(`Warning: config.md playbooks.disabled entry "${raw}" contains whitespace, ignoring`);
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
disabled.push(raw);
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return { disabled };
|
|
794
|
+
}
|
|
795
|
+
async function updatePlaybooksConfig(playbooks) {
|
|
796
|
+
const configPath = resolve3(syntaurRoot(), "config.md");
|
|
797
|
+
const current = (await readConfig()).playbooks;
|
|
798
|
+
const nextPlaybooks = {
|
|
799
|
+
disabled: Array.from(new Set(playbooks.disabled ?? current.disabled))
|
|
800
|
+
};
|
|
801
|
+
const playbooksBlock = serializePlaybooksConfig(nextPlaybooks);
|
|
802
|
+
const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
803
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
804
|
+
if (!fmMatch) {
|
|
805
|
+
const bodyBlock = playbooksBlock ? `${playbooksBlock}
|
|
806
|
+
` : "";
|
|
807
|
+
const content = `---
|
|
808
|
+
version: "2.0"
|
|
809
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
810
|
+
${bodyBlock}---
|
|
811
|
+
${existing}`;
|
|
812
|
+
await writeFileForce(configPath, content);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const fmBlock = fmMatch[2];
|
|
816
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
817
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "playbooks");
|
|
818
|
+
const newFm = playbooksBlock ? `${cleanedFm}
|
|
819
|
+
${playbooksBlock}`.replace(/^\n+/, "") : cleanedFm;
|
|
820
|
+
const normalizedFm = newFm.replace(/\n+$/, "");
|
|
821
|
+
const newContent = `---
|
|
822
|
+
${normalizedFm}
|
|
823
|
+
---${afterFrontmatter}`;
|
|
824
|
+
await writeFileForce(configPath, newContent);
|
|
825
|
+
}
|
|
725
826
|
function stripTopLevelBlock(fmBlock, key) {
|
|
726
827
|
const blockStart = fmBlock.match(new RegExp(`^${key}:\\s*$`, "m"));
|
|
727
828
|
if (!blockStart) {
|
|
@@ -756,10 +857,10 @@ function parseOptionalAbsolutePath(value, fieldName) {
|
|
|
756
857
|
);
|
|
757
858
|
return null;
|
|
758
859
|
}
|
|
759
|
-
return
|
|
860
|
+
return resolve3(expanded);
|
|
760
861
|
}
|
|
761
862
|
async function writeStatusConfig(statuses) {
|
|
762
|
-
const configPath =
|
|
863
|
+
const configPath = resolve3(syntaurRoot(), "config.md");
|
|
763
864
|
const statusBlock = serializeStatusConfig(statuses);
|
|
764
865
|
if (!await fileExists(configPath)) {
|
|
765
866
|
const content = `---
|
|
@@ -771,7 +872,7 @@ ${statusBlock}
|
|
|
771
872
|
await writeFileForce(configPath, content);
|
|
772
873
|
return;
|
|
773
874
|
}
|
|
774
|
-
const existing = await
|
|
875
|
+
const existing = await readFile2(configPath, "utf-8");
|
|
775
876
|
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
776
877
|
if (!fmMatch) {
|
|
777
878
|
const content = `---
|
|
@@ -813,9 +914,9 @@ ${statusBlock}
|
|
|
813
914
|
await writeFileForce(configPath, newContent);
|
|
814
915
|
}
|
|
815
916
|
async function deleteStatusConfig() {
|
|
816
|
-
const configPath =
|
|
917
|
+
const configPath = resolve3(syntaurRoot(), "config.md");
|
|
817
918
|
if (!await fileExists(configPath)) return;
|
|
818
|
-
const existing = await
|
|
919
|
+
const existing = await readFile2(configPath, "utf-8");
|
|
819
920
|
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
820
921
|
if (!fmMatch) return;
|
|
821
922
|
const fmBlock = fmMatch[2];
|
|
@@ -827,13 +928,13 @@ ${cleanedFm}
|
|
|
827
928
|
await writeFileForce(configPath, newContent);
|
|
828
929
|
}
|
|
829
930
|
async function updateIntegrationConfig(integrations) {
|
|
830
|
-
const configPath =
|
|
931
|
+
const configPath = resolve3(syntaurRoot(), "config.md");
|
|
831
932
|
const nextIntegrations = {
|
|
832
933
|
...(await readConfig()).integrations,
|
|
833
934
|
...integrations
|
|
834
935
|
};
|
|
835
936
|
const integrationBlock = serializeIntegrationConfig(nextIntegrations);
|
|
836
|
-
const existing = await fileExists(configPath) ? await
|
|
937
|
+
const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
837
938
|
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
838
939
|
if (!fmMatch) {
|
|
839
940
|
const content = `---
|
|
@@ -857,13 +958,13 @@ ${normalizedFm}
|
|
|
857
958
|
await writeFileForce(configPath, newContent);
|
|
858
959
|
}
|
|
859
960
|
async function updateOnboardingConfig(onboarding) {
|
|
860
|
-
const configPath =
|
|
961
|
+
const configPath = resolve3(syntaurRoot(), "config.md");
|
|
861
962
|
const nextOnboarding = {
|
|
862
963
|
...(await readConfig()).onboarding,
|
|
863
964
|
...onboarding
|
|
864
965
|
};
|
|
865
966
|
const onboardingBlock = serializeOnboardingConfig(nextOnboarding);
|
|
866
|
-
const existing = await fileExists(configPath) ? await
|
|
967
|
+
const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
867
968
|
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
868
969
|
if (!fmMatch) {
|
|
869
970
|
const content = `---
|
|
@@ -887,7 +988,7 @@ ${normalizedFm}
|
|
|
887
988
|
await writeFileForce(configPath, newContent);
|
|
888
989
|
}
|
|
889
990
|
async function updateBackupConfig(backup) {
|
|
890
|
-
const configPath =
|
|
991
|
+
const configPath = resolve3(syntaurRoot(), "config.md");
|
|
891
992
|
const current = (await readConfig()).backup;
|
|
892
993
|
const nextBackup = {
|
|
893
994
|
repo: current?.repo ?? null,
|
|
@@ -897,7 +998,7 @@ async function updateBackupConfig(backup) {
|
|
|
897
998
|
...backup
|
|
898
999
|
};
|
|
899
1000
|
const backupBlock = serializeBackupConfig(nextBackup);
|
|
900
|
-
const existing = await fileExists(configPath) ? await
|
|
1001
|
+
const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
901
1002
|
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
902
1003
|
if (!fmMatch) {
|
|
903
1004
|
const content = `---
|
|
@@ -921,19 +1022,19 @@ ${normalizedFm}
|
|
|
921
1022
|
await writeFileForce(configPath, newContent);
|
|
922
1023
|
}
|
|
923
1024
|
async function readConfig() {
|
|
924
|
-
const configPath =
|
|
1025
|
+
const configPath = resolve3(syntaurRoot(), "config.md");
|
|
925
1026
|
if (!await fileExists(configPath)) {
|
|
926
|
-
return
|
|
1027
|
+
return cloneDefaultConfig();
|
|
927
1028
|
}
|
|
928
1029
|
if (!migratedConfigPaths.has(configPath)) {
|
|
929
1030
|
migratedConfigPaths.add(configPath);
|
|
930
1031
|
await migrateLegacyConfig(configPath);
|
|
931
1032
|
}
|
|
932
|
-
const content = await
|
|
1033
|
+
const content = await readFile2(configPath, "utf-8");
|
|
933
1034
|
const fm = parseFrontmatter(content);
|
|
934
1035
|
if (Object.keys(fm).length === 0) {
|
|
935
1036
|
console.warn("Warning: ~/.syntaur/config.md has malformed frontmatter, using defaults");
|
|
936
|
-
return
|
|
1037
|
+
return cloneDefaultConfig();
|
|
937
1038
|
}
|
|
938
1039
|
let projectDir = fm["defaultProjectDir"] ? expandHome(String(fm["defaultProjectDir"])) : DEFAULT_CONFIG.defaultProjectDir;
|
|
939
1040
|
if (!isAbsolute(projectDir)) {
|
|
@@ -942,6 +1043,7 @@ async function readConfig() {
|
|
|
942
1043
|
);
|
|
943
1044
|
projectDir = DEFAULT_CONFIG.defaultProjectDir;
|
|
944
1045
|
}
|
|
1046
|
+
const fmBlock = content.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
|
|
945
1047
|
return {
|
|
946
1048
|
version: fm["version"] || DEFAULT_CONFIG.version,
|
|
947
1049
|
defaultProjectDir: projectDir,
|
|
@@ -973,7 +1075,8 @@ async function readConfig() {
|
|
|
973
1075
|
lastRestore: fm["backup.lastRestore"] && fm["backup.lastRestore"] !== "null" ? fm["backup.lastRestore"] : null
|
|
974
1076
|
} : null,
|
|
975
1077
|
statuses: parseStatusConfig(content),
|
|
976
|
-
types: null
|
|
1078
|
+
types: null,
|
|
1079
|
+
playbooks: parsePlaybooksConfig(fmBlock)
|
|
977
1080
|
};
|
|
978
1081
|
}
|
|
979
1082
|
var DEFAULT_CONFIG, migratedConfigPaths;
|
|
@@ -1001,12 +1104,119 @@ var init_config2 = __esm({
|
|
|
1001
1104
|
},
|
|
1002
1105
|
backup: null,
|
|
1003
1106
|
statuses: null,
|
|
1004
|
-
types: null
|
|
1107
|
+
types: null,
|
|
1108
|
+
playbooks: {
|
|
1109
|
+
disabled: []
|
|
1110
|
+
}
|
|
1005
1111
|
};
|
|
1006
1112
|
migratedConfigPaths = /* @__PURE__ */ new Set();
|
|
1007
1113
|
}
|
|
1008
1114
|
});
|
|
1009
1115
|
|
|
1116
|
+
// src/utils/playbooks.ts
|
|
1117
|
+
import { resolve as resolve4 } from "path";
|
|
1118
|
+
import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
|
|
1119
|
+
function isVisiblePlaybookFile(name, isFile) {
|
|
1120
|
+
return isFile && name.endsWith(".md") && !name.startsWith("_") && name !== "manifest.md";
|
|
1121
|
+
}
|
|
1122
|
+
async function resolvePlaybookSlug(playbooksDir3, slug) {
|
|
1123
|
+
if (!await fileExists(playbooksDir3)) return null;
|
|
1124
|
+
const entries = await readdir2(playbooksDir3, { withFileTypes: true });
|
|
1125
|
+
let filenameStemFallback = null;
|
|
1126
|
+
for (const entry of entries) {
|
|
1127
|
+
if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
|
|
1128
|
+
const filePath = resolve4(playbooksDir3, entry.name);
|
|
1129
|
+
const raw = await readFile3(filePath, "utf-8");
|
|
1130
|
+
const parsed = parsePlaybook(raw);
|
|
1131
|
+
const canonical = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
1132
|
+
if (canonical === slug) {
|
|
1133
|
+
return { filename: entry.name, slug: canonical, parsed };
|
|
1134
|
+
}
|
|
1135
|
+
if (!parsed.slug && entry.name.replace(/\.md$/, "") === slug) {
|
|
1136
|
+
filenameStemFallback = { filename: entry.name, slug: canonical, parsed };
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return filenameStemFallback;
|
|
1140
|
+
}
|
|
1141
|
+
async function setPlaybookEnabled(playbooksDir3, slug, enabled) {
|
|
1142
|
+
const resolved = await resolvePlaybookSlug(playbooksDir3, slug);
|
|
1143
|
+
if (!resolved) {
|
|
1144
|
+
throw new Error(`Playbook "${slug}" not found in ${playbooksDir3}`);
|
|
1145
|
+
}
|
|
1146
|
+
const config = await readConfig();
|
|
1147
|
+
const disabledSet = new Set(config.playbooks.disabled);
|
|
1148
|
+
const wasDisabled = disabledSet.has(resolved.slug);
|
|
1149
|
+
const shouldBeDisabled = !enabled;
|
|
1150
|
+
if (wasDisabled === shouldBeDisabled) {
|
|
1151
|
+
return { slug: resolved.slug, enabled, changed: false };
|
|
1152
|
+
}
|
|
1153
|
+
if (shouldBeDisabled) {
|
|
1154
|
+
disabledSet.add(resolved.slug);
|
|
1155
|
+
} else {
|
|
1156
|
+
disabledSet.delete(resolved.slug);
|
|
1157
|
+
}
|
|
1158
|
+
await updatePlaybooksConfig({ disabled: Array.from(disabledSet).sort() });
|
|
1159
|
+
await rebuildPlaybookManifest(playbooksDir3);
|
|
1160
|
+
return { slug: resolved.slug, enabled, changed: true };
|
|
1161
|
+
}
|
|
1162
|
+
async function removeFromDisabledList(slug) {
|
|
1163
|
+
const config = await readConfig();
|
|
1164
|
+
if (!config.playbooks.disabled.includes(slug)) return;
|
|
1165
|
+
await updatePlaybooksConfig({
|
|
1166
|
+
disabled: config.playbooks.disabled.filter((s) => s !== slug)
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
async function rebuildPlaybookManifest(playbooksDir3) {
|
|
1170
|
+
if (!await fileExists(playbooksDir3)) return;
|
|
1171
|
+
const config = await readConfig();
|
|
1172
|
+
const disabledSet = new Set(config.playbooks.disabled);
|
|
1173
|
+
const entries = await readdir2(playbooksDir3, { withFileTypes: true });
|
|
1174
|
+
const rows = [];
|
|
1175
|
+
for (const entry of entries) {
|
|
1176
|
+
if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
|
|
1177
|
+
const raw = await readFile3(resolve4(playbooksDir3, entry.name), "utf-8");
|
|
1178
|
+
const parsed = parsePlaybook(raw);
|
|
1179
|
+
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
1180
|
+
if (disabledSet.has(slug)) continue;
|
|
1181
|
+
rows.push({
|
|
1182
|
+
name: parsed.name || slug,
|
|
1183
|
+
slug,
|
|
1184
|
+
description: parsed.description,
|
|
1185
|
+
whenToUse: parsed.whenToUse
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
1189
|
+
const timestamp = nowTimestamp();
|
|
1190
|
+
const lines = [
|
|
1191
|
+
"---",
|
|
1192
|
+
`generated: "${timestamp}"`,
|
|
1193
|
+
`total: ${rows.length}`,
|
|
1194
|
+
"---",
|
|
1195
|
+
"",
|
|
1196
|
+
"# Playbooks",
|
|
1197
|
+
"",
|
|
1198
|
+
"Behavioral rules for AI agents. Read and follow all playbooks before starting work.",
|
|
1199
|
+
""
|
|
1200
|
+
];
|
|
1201
|
+
for (const row of rows) {
|
|
1202
|
+
lines.push(`- **[${row.name}](${row.slug}.md)** \u2014 ${row.description}`);
|
|
1203
|
+
if (row.whenToUse) {
|
|
1204
|
+
lines.push(` _When to use: ${row.whenToUse}_`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
lines.push("");
|
|
1208
|
+
await writeFileForce(resolve4(playbooksDir3, "manifest.md"), lines.join("\n"));
|
|
1209
|
+
}
|
|
1210
|
+
var init_playbooks = __esm({
|
|
1211
|
+
"src/utils/playbooks.ts"() {
|
|
1212
|
+
"use strict";
|
|
1213
|
+
init_fs();
|
|
1214
|
+
init_parser();
|
|
1215
|
+
init_timestamp();
|
|
1216
|
+
init_config2();
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1010
1220
|
// src/lifecycle/types.ts
|
|
1011
1221
|
var DEFAULT_STATUSES, DEFAULT_TERMINAL_STATUSES;
|
|
1012
1222
|
var init_types = __esm({
|
|
@@ -1409,12 +1619,20 @@ async function resolveAssignmentById(projectsDir2, assignmentsDir2, id) {
|
|
|
1409
1619
|
const standaloneDir = resolve9(assignmentsDir2, id);
|
|
1410
1620
|
const standalonePath = resolve9(standaloneDir, "assignment.md");
|
|
1411
1621
|
if (await fileExists(standalonePath)) {
|
|
1622
|
+
let workspaceGroup = null;
|
|
1623
|
+
try {
|
|
1624
|
+
const content = await readFile6(standalonePath, "utf-8");
|
|
1625
|
+
const [fm] = extractFrontmatter(content);
|
|
1626
|
+
workspaceGroup = getField(fm, "workspaceGroup");
|
|
1627
|
+
} catch {
|
|
1628
|
+
}
|
|
1412
1629
|
standaloneMatch = {
|
|
1413
1630
|
assignmentDir: standaloneDir,
|
|
1414
1631
|
projectSlug: null,
|
|
1415
1632
|
assignmentSlug: id,
|
|
1416
1633
|
id,
|
|
1417
|
-
standalone: true
|
|
1634
|
+
standalone: true,
|
|
1635
|
+
workspaceGroup
|
|
1418
1636
|
};
|
|
1419
1637
|
}
|
|
1420
1638
|
if (await fileExists(projectsDir2)) {
|
|
@@ -1440,7 +1658,8 @@ async function resolveAssignmentById(projectsDir2, assignmentsDir2, id) {
|
|
|
1440
1658
|
projectSlug: p.name,
|
|
1441
1659
|
assignmentSlug: a.name,
|
|
1442
1660
|
id,
|
|
1443
|
-
standalone: false
|
|
1661
|
+
standalone: false,
|
|
1662
|
+
workspaceGroup: null
|
|
1444
1663
|
};
|
|
1445
1664
|
break;
|
|
1446
1665
|
}
|
|
@@ -1816,8 +2035,18 @@ var init_help = __esm({
|
|
|
1816
2035
|
},
|
|
1817
2036
|
{
|
|
1818
2037
|
command: "syntaur list-playbooks",
|
|
1819
|
-
description: "List
|
|
1820
|
-
example: "syntaur list-playbooks"
|
|
2038
|
+
description: "List playbooks in the Syntaur home directory. Disabled playbooks are excluded by default; pass --all to include them with a (disabled) tag.",
|
|
2039
|
+
example: "syntaur list-playbooks --all"
|
|
2040
|
+
},
|
|
2041
|
+
{
|
|
2042
|
+
command: "syntaur enable-playbook",
|
|
2043
|
+
description: "Re-enable a previously-disabled playbook so agents load it again. Updates config.md and rebuilds manifest.md.",
|
|
2044
|
+
example: "syntaur enable-playbook commit-discipline"
|
|
2045
|
+
},
|
|
2046
|
+
{
|
|
2047
|
+
command: "syntaur disable-playbook",
|
|
2048
|
+
description: "Disable a playbook so agents no longer list or load it. Playbook file is untouched; state is tracked in config.md.",
|
|
2049
|
+
example: "syntaur disable-playbook commit-discipline"
|
|
1821
2050
|
}
|
|
1822
2051
|
];
|
|
1823
2052
|
WORKFLOW = [
|
|
@@ -2541,10 +2770,11 @@ async function writeWorkspaceRegistry(projectsDir2, workspaces) {
|
|
|
2541
2770
|
const registryPath = resolve12(dirname3(projectsDir2), "workspaces.json");
|
|
2542
2771
|
await writeFile3(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
|
|
2543
2772
|
}
|
|
2544
|
-
async function listWorkspaces(projectsDir2) {
|
|
2545
|
-
const [projectRecords, registered] = await Promise.all([
|
|
2773
|
+
async function listWorkspaces(projectsDir2, assignmentsDir2) {
|
|
2774
|
+
const [projectRecords, registered, standaloneRecords] = await Promise.all([
|
|
2546
2775
|
listProjectRecords(projectsDir2),
|
|
2547
|
-
readWorkspaceRegistry(projectsDir2)
|
|
2776
|
+
readWorkspaceRegistry(projectsDir2),
|
|
2777
|
+
listStandaloneRecords(assignmentsDir2)
|
|
2548
2778
|
]);
|
|
2549
2779
|
const workspaceSet = new Set(registered);
|
|
2550
2780
|
let hasUngrouped = false;
|
|
@@ -2555,6 +2785,13 @@ async function listWorkspaces(projectsDir2) {
|
|
|
2555
2785
|
hasUngrouped = true;
|
|
2556
2786
|
}
|
|
2557
2787
|
}
|
|
2788
|
+
for (const sr of standaloneRecords) {
|
|
2789
|
+
if (sr.record.workspaceGroup) {
|
|
2790
|
+
workspaceSet.add(sr.record.workspaceGroup);
|
|
2791
|
+
} else {
|
|
2792
|
+
hasUngrouped = true;
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2558
2795
|
const workspaces = Array.from(workspaceSet).sort();
|
|
2559
2796
|
return { workspaces, hasUngrouped };
|
|
2560
2797
|
}
|
|
@@ -2703,7 +2940,7 @@ async function toStandaloneBoardItem(sr) {
|
|
|
2703
2940
|
projectSlug: null,
|
|
2704
2941
|
projectTitle: null,
|
|
2705
2942
|
blockedReason: sr.record.blockedReason,
|
|
2706
|
-
projectWorkspace: null,
|
|
2943
|
+
projectWorkspace: sr.record.workspaceGroup ?? null,
|
|
2707
2944
|
availableTransitions: await getStandaloneAvailableTransitions(sr.record)
|
|
2708
2945
|
};
|
|
2709
2946
|
}
|
|
@@ -3644,6 +3881,8 @@ function getEditableDocumentTitle(documentType, projectSlug, assignmentSlug) {
|
|
|
3644
3881
|
}
|
|
3645
3882
|
async function listPlaybooks(playbooksDir3) {
|
|
3646
3883
|
if (!await fileExists(playbooksDir3)) return [];
|
|
3884
|
+
const config = await readConfig();
|
|
3885
|
+
const disabledSet = new Set(config.playbooks.disabled);
|
|
3647
3886
|
const entries = await readdir7(playbooksDir3, { withFileTypes: true });
|
|
3648
3887
|
const playbooks = [];
|
|
3649
3888
|
for (const entry of entries) {
|
|
@@ -3659,25 +3898,28 @@ async function listPlaybooks(playbooksDir3) {
|
|
|
3659
3898
|
whenToUse: parsed.whenToUse,
|
|
3660
3899
|
tags: parsed.tags,
|
|
3661
3900
|
created: parsed.created,
|
|
3662
|
-
updated: parsed.updated
|
|
3901
|
+
updated: parsed.updated,
|
|
3902
|
+
enabled: !disabledSet.has(slug)
|
|
3663
3903
|
});
|
|
3664
3904
|
}
|
|
3665
3905
|
return playbooks.sort((a, b) => (b.updated || b.created).localeCompare(a.updated || a.created));
|
|
3666
3906
|
}
|
|
3667
3907
|
async function getPlaybookDetail(playbooksDir3, slug) {
|
|
3668
|
-
const
|
|
3669
|
-
if (!
|
|
3670
|
-
const
|
|
3671
|
-
const
|
|
3908
|
+
const resolved = await resolvePlaybookSlug(playbooksDir3, slug);
|
|
3909
|
+
if (!resolved) return null;
|
|
3910
|
+
const config = await readConfig();
|
|
3911
|
+
const enabled = !config.playbooks.disabled.includes(resolved.slug);
|
|
3912
|
+
const parsed = resolved.parsed;
|
|
3672
3913
|
return {
|
|
3673
|
-
slug:
|
|
3674
|
-
name: parsed.name || slug,
|
|
3914
|
+
slug: resolved.slug,
|
|
3915
|
+
name: parsed.name || resolved.slug,
|
|
3675
3916
|
description: parsed.description,
|
|
3676
3917
|
whenToUse: parsed.whenToUse,
|
|
3677
3918
|
tags: parsed.tags,
|
|
3678
3919
|
created: parsed.created,
|
|
3679
3920
|
updated: parsed.updated,
|
|
3680
|
-
body: parsed.body
|
|
3921
|
+
body: parsed.body,
|
|
3922
|
+
enabled
|
|
3681
3923
|
};
|
|
3682
3924
|
}
|
|
3683
3925
|
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;
|
|
@@ -3687,6 +3929,7 @@ var init_api = __esm({
|
|
|
3687
3929
|
init_lifecycle();
|
|
3688
3930
|
init_fs();
|
|
3689
3931
|
init_config2();
|
|
3932
|
+
init_playbooks();
|
|
3690
3933
|
init_fs_migration();
|
|
3691
3934
|
init_assignment_resolver();
|
|
3692
3935
|
init_parser();
|
|
@@ -4546,8 +4789,8 @@ __export(launch_exports, {
|
|
|
4546
4789
|
launchAgent: () => launchAgent
|
|
4547
4790
|
});
|
|
4548
4791
|
import { spawn as spawn2 } from "child_process";
|
|
4549
|
-
import { mkdir as
|
|
4550
|
-
import { resolve as
|
|
4792
|
+
import { mkdir as mkdir5, writeFile as writeFile9 } from "fs/promises";
|
|
4793
|
+
import { resolve as resolve32 } from "path";
|
|
4551
4794
|
async function launchAgent(options) {
|
|
4552
4795
|
const { projectsDir: projectsDir2, projectSlug, assignmentSlug, agent } = options;
|
|
4553
4796
|
const command = AGENT_COMMANDS[agent];
|
|
@@ -4557,10 +4800,10 @@ async function launchAgent(options) {
|
|
|
4557
4800
|
process.exit(1);
|
|
4558
4801
|
}
|
|
4559
4802
|
const workspaceDir = detail.workspace.worktreePath ?? (detail.workspace.repository?.startsWith("/") ? detail.workspace.repository : null) ?? process.cwd();
|
|
4560
|
-
const projectDir =
|
|
4561
|
-
const assignmentDir =
|
|
4562
|
-
const contextDir =
|
|
4563
|
-
await
|
|
4803
|
+
const projectDir = resolve32(projectsDir2, projectSlug);
|
|
4804
|
+
const assignmentDir = resolve32(projectDir, "assignments", assignmentSlug);
|
|
4805
|
+
const contextDir = resolve32(workspaceDir, ".syntaur");
|
|
4806
|
+
await mkdir5(contextDir, { recursive: true });
|
|
4564
4807
|
const context = {
|
|
4565
4808
|
projectSlug,
|
|
4566
4809
|
assignmentSlug,
|
|
@@ -4572,7 +4815,7 @@ async function launchAgent(options) {
|
|
|
4572
4815
|
grabbedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4573
4816
|
};
|
|
4574
4817
|
await writeFile9(
|
|
4575
|
-
|
|
4818
|
+
resolve32(contextDir, "context.json"),
|
|
4576
4819
|
JSON.stringify(context, null, 2) + "\n"
|
|
4577
4820
|
);
|
|
4578
4821
|
return new Promise((resolvePromise, reject) => {
|
|
@@ -4613,62 +4856,16 @@ import { Command as Command4 } from "commander";
|
|
|
4613
4856
|
init_paths();
|
|
4614
4857
|
init_fs();
|
|
4615
4858
|
init_config();
|
|
4616
|
-
|
|
4617
|
-
import {
|
|
4859
|
+
init_playbooks();
|
|
4860
|
+
import { resolve as resolve5, dirname as dirname2 } from "path";
|
|
4861
|
+
import { readdir as readdir3, readFile as readFile4 } from "fs/promises";
|
|
4618
4862
|
import { fileURLToPath } from "url";
|
|
4619
|
-
|
|
4620
|
-
// src/utils/playbooks.ts
|
|
4621
|
-
init_fs();
|
|
4622
|
-
init_parser();
|
|
4623
|
-
init_timestamp();
|
|
4624
|
-
import { resolve as resolve2 } from "path";
|
|
4625
|
-
import { readdir, readFile } from "fs/promises";
|
|
4626
|
-
async function rebuildPlaybookManifest(playbooksDir3) {
|
|
4627
|
-
if (!await fileExists(playbooksDir3)) return;
|
|
4628
|
-
const entries = await readdir(playbooksDir3, { withFileTypes: true });
|
|
4629
|
-
const rows = [];
|
|
4630
|
-
for (const entry of entries) {
|
|
4631
|
-
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
|
|
4632
|
-
const raw = await readFile(resolve2(playbooksDir3, entry.name), "utf-8");
|
|
4633
|
-
const parsed = parsePlaybook(raw);
|
|
4634
|
-
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
4635
|
-
rows.push({
|
|
4636
|
-
name: parsed.name || slug,
|
|
4637
|
-
slug,
|
|
4638
|
-
description: parsed.description,
|
|
4639
|
-
whenToUse: parsed.whenToUse
|
|
4640
|
-
});
|
|
4641
|
-
}
|
|
4642
|
-
rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
4643
|
-
const timestamp = nowTimestamp();
|
|
4644
|
-
const lines = [
|
|
4645
|
-
"---",
|
|
4646
|
-
`generated: "${timestamp}"`,
|
|
4647
|
-
`total: ${rows.length}`,
|
|
4648
|
-
"---",
|
|
4649
|
-
"",
|
|
4650
|
-
"# Playbooks",
|
|
4651
|
-
"",
|
|
4652
|
-
"Behavioral rules for AI agents. Read and follow all playbooks before starting work.",
|
|
4653
|
-
""
|
|
4654
|
-
];
|
|
4655
|
-
for (const row of rows) {
|
|
4656
|
-
lines.push(`- **[${row.name}](${row.slug}.md)** \u2014 ${row.description}`);
|
|
4657
|
-
if (row.whenToUse) {
|
|
4658
|
-
lines.push(` _When to use: ${row.whenToUse}_`);
|
|
4659
|
-
}
|
|
4660
|
-
}
|
|
4661
|
-
lines.push("");
|
|
4662
|
-
await writeFileForce(resolve2(playbooksDir3, "manifest.md"), lines.join("\n"));
|
|
4663
|
-
}
|
|
4664
|
-
|
|
4665
|
-
// src/commands/init.ts
|
|
4666
4863
|
async function initCommand(options) {
|
|
4667
4864
|
const root = syntaurRoot();
|
|
4668
4865
|
const projectsDir2 = defaultProjectDir();
|
|
4669
4866
|
const standaloneAssignmentsDir = assignmentsDir();
|
|
4670
|
-
const configPath =
|
|
4671
|
-
const playbooksDir3 =
|
|
4867
|
+
const configPath = resolve5(root, "config.md");
|
|
4868
|
+
const playbooksDir3 = resolve5(root, "playbooks");
|
|
4672
4869
|
await ensureDir(root);
|
|
4673
4870
|
await ensureDir(projectsDir2);
|
|
4674
4871
|
await ensureDir(standaloneAssignmentsDir);
|
|
@@ -4704,16 +4901,16 @@ async function initCommand(options) {
|
|
|
4704
4901
|
}
|
|
4705
4902
|
async function seedDefaultPlaybooks(playbooksDir3) {
|
|
4706
4903
|
const __filename = fileURLToPath(import.meta.url);
|
|
4707
|
-
const packageRoot =
|
|
4708
|
-
const examplesDir =
|
|
4904
|
+
const packageRoot = resolve5(dirname2(__filename), "..");
|
|
4905
|
+
const examplesDir = resolve5(packageRoot, "examples", "playbooks");
|
|
4709
4906
|
if (!await fileExists(examplesDir)) return 0;
|
|
4710
|
-
const entries = await
|
|
4907
|
+
const entries = await readdir3(examplesDir, { withFileTypes: true });
|
|
4711
4908
|
let count = 0;
|
|
4712
4909
|
for (const entry of entries) {
|
|
4713
4910
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
4714
|
-
const targetPath =
|
|
4911
|
+
const targetPath = resolve5(playbooksDir3, entry.name);
|
|
4715
4912
|
if (await fileExists(targetPath)) continue;
|
|
4716
|
-
const content = await
|
|
4913
|
+
const content = await readFile4(resolve5(examplesDir, entry.name), "utf-8");
|
|
4717
4914
|
await writeFileSafe(targetPath, content);
|
|
4718
4915
|
count++;
|
|
4719
4916
|
}
|
|
@@ -4749,14 +4946,14 @@ init_config2();
|
|
|
4749
4946
|
init_config();
|
|
4750
4947
|
|
|
4751
4948
|
// src/templates/manifest.ts
|
|
4752
|
-
function renderManifest(
|
|
4949
|
+
function renderManifest(params2) {
|
|
4753
4950
|
return `---
|
|
4754
4951
|
version: "2.0"
|
|
4755
|
-
project: ${
|
|
4756
|
-
generated: "${
|
|
4952
|
+
project: ${params2.slug}
|
|
4953
|
+
generated: "${params2.timestamp}"
|
|
4757
4954
|
---
|
|
4758
4955
|
|
|
4759
|
-
# Project: ${
|
|
4956
|
+
# Project: ${params2.slug}
|
|
4760
4957
|
|
|
4761
4958
|
## Overview
|
|
4762
4959
|
- [Project Overview](./project.md)
|
|
@@ -4783,24 +4980,24 @@ function escapeYamlString(value) {
|
|
|
4783
4980
|
}
|
|
4784
4981
|
|
|
4785
4982
|
// src/templates/project.ts
|
|
4786
|
-
function renderProject(
|
|
4787
|
-
const safeTitle = escapeYamlString(
|
|
4788
|
-
const workspaceLine =
|
|
4789
|
-
workspace: ${
|
|
4983
|
+
function renderProject(params2) {
|
|
4984
|
+
const safeTitle = escapeYamlString(params2.title);
|
|
4985
|
+
const workspaceLine = params2.workspace ? `
|
|
4986
|
+
workspace: ${params2.workspace}` : "";
|
|
4790
4987
|
return `---
|
|
4791
|
-
id: ${
|
|
4792
|
-
slug: ${
|
|
4988
|
+
id: ${params2.id}
|
|
4989
|
+
slug: ${params2.slug}
|
|
4793
4990
|
title: ${safeTitle}
|
|
4794
4991
|
archived: false
|
|
4795
4992
|
archivedAt: null
|
|
4796
4993
|
archivedReason: null
|
|
4797
|
-
created: "${
|
|
4798
|
-
updated: "${
|
|
4994
|
+
created: "${params2.timestamp}"
|
|
4995
|
+
updated: "${params2.timestamp}"
|
|
4799
4996
|
externalIds: []
|
|
4800
4997
|
tags: []${workspaceLine}
|
|
4801
4998
|
---
|
|
4802
4999
|
|
|
4803
|
-
# ${
|
|
5000
|
+
# ${params2.title}
|
|
4804
5001
|
|
|
4805
5002
|
## Overview
|
|
4806
5003
|
|
|
@@ -4813,24 +5010,37 @@ tags: []${workspaceLine}
|
|
|
4813
5010
|
}
|
|
4814
5011
|
|
|
4815
5012
|
// src/templates/assignment.ts
|
|
4816
|
-
function renderAssignment(
|
|
4817
|
-
const safeTitle = escapeYamlString(
|
|
4818
|
-
const dependsOnYaml =
|
|
4819
|
-
- ${
|
|
4820
|
-
const linksYaml =
|
|
4821
|
-
- ${
|
|
4822
|
-
const projectYaml = `project: ${
|
|
4823
|
-
const
|
|
5013
|
+
function renderAssignment(params2) {
|
|
5014
|
+
const safeTitle = escapeYamlString(params2.title);
|
|
5015
|
+
const dependsOnYaml = params2.dependsOn.length === 0 ? "dependsOn: []" : `dependsOn:
|
|
5016
|
+
- ${params2.dependsOn.join("\n - ")}`;
|
|
5017
|
+
const linksYaml = params2.links.length === 0 ? "links: []" : `links:
|
|
5018
|
+
- ${params2.links.join("\n - ")}`;
|
|
5019
|
+
const projectYaml = `project: ${params2.project == null ? "null" : params2.project}`;
|
|
5020
|
+
const workspaceGroupLine = params2.workspaceGroup ? `
|
|
5021
|
+
workspaceGroup: ${params2.workspaceGroup}` : "";
|
|
5022
|
+
const typeYaml = `type: ${params2.type ?? "feature"}`;
|
|
5023
|
+
const todosSection = params2.includeTodos ? `## Todos
|
|
5024
|
+
|
|
5025
|
+
<!--
|
|
5026
|
+
Checklist of work items for this assignment. Items may be simple tasks
|
|
5027
|
+
or a markdown link to a plan file (e.g., "- [ ] Execute [plan](./plan.md)").
|
|
5028
|
+
When a plan is superseded by a new one, mark the old todo as:
|
|
5029
|
+
- [x] ~~Execute [old plan](./plan.md)~~ (superseded by plan-v2)
|
|
5030
|
+
Never delete superseded todos \u2014 preserve the history.
|
|
5031
|
+
-->
|
|
5032
|
+
|
|
5033
|
+
` : "";
|
|
4824
5034
|
return `---
|
|
4825
|
-
id: ${
|
|
4826
|
-
slug: ${
|
|
5035
|
+
id: ${params2.id}
|
|
5036
|
+
slug: ${params2.slug}
|
|
4827
5037
|
title: ${safeTitle}
|
|
4828
|
-
${projectYaml}
|
|
5038
|
+
${projectYaml}${workspaceGroupLine}
|
|
4829
5039
|
${typeYaml}
|
|
4830
5040
|
status: pending
|
|
4831
|
-
priority: ${
|
|
4832
|
-
created: "${
|
|
4833
|
-
updated: "${
|
|
5041
|
+
priority: ${params2.priority}
|
|
5042
|
+
created: "${params2.timestamp}"
|
|
5043
|
+
updated: "${params2.timestamp}"
|
|
4834
5044
|
assignee: null
|
|
4835
5045
|
externalIds: []
|
|
4836
5046
|
${dependsOnYaml}
|
|
@@ -4844,7 +5054,7 @@ workspace:
|
|
|
4844
5054
|
tags: []
|
|
4845
5055
|
---
|
|
4846
5056
|
|
|
4847
|
-
# ${
|
|
5057
|
+
# ${params2.title}
|
|
4848
5058
|
|
|
4849
5059
|
## Objective
|
|
4850
5060
|
|
|
@@ -4856,17 +5066,7 @@ tags: []
|
|
|
4856
5066
|
- [ ] <!-- criterion 2 -->
|
|
4857
5067
|
- [ ] <!-- criterion 3 -->
|
|
4858
5068
|
|
|
4859
|
-
##
|
|
4860
|
-
|
|
4861
|
-
<!--
|
|
4862
|
-
Checklist of work items for this assignment. Items may be simple tasks
|
|
4863
|
-
or a markdown link to a plan file (e.g., "- [ ] Execute [plan](./plan.md)").
|
|
4864
|
-
When a plan is superseded by a new one, mark the old todo as:
|
|
4865
|
-
- [x] ~~Execute [old plan](./plan.md)~~ (superseded by plan-v2)
|
|
4866
|
-
Never delete superseded todos \u2014 preserve the history.
|
|
4867
|
-
-->
|
|
4868
|
-
|
|
4869
|
-
## Context
|
|
5069
|
+
${todosSection}## Context
|
|
4870
5070
|
|
|
4871
5071
|
<!-- Links to relevant docs, code, or other assignments. -->
|
|
4872
5072
|
|
|
@@ -4881,10 +5081,10 @@ Never delete superseded todos \u2014 preserve the history.
|
|
|
4881
5081
|
}
|
|
4882
5082
|
|
|
4883
5083
|
// src/templates/scratchpad.ts
|
|
4884
|
-
function renderScratchpad(
|
|
5084
|
+
function renderScratchpad(params2) {
|
|
4885
5085
|
return `---
|
|
4886
|
-
assignment: ${
|
|
4887
|
-
updated: "${
|
|
5086
|
+
assignment: ${params2.assignmentSlug}
|
|
5087
|
+
updated: "${params2.timestamp}"
|
|
4888
5088
|
---
|
|
4889
5089
|
|
|
4890
5090
|
# Scratchpad
|
|
@@ -4894,10 +5094,10 @@ No working notes yet.
|
|
|
4894
5094
|
}
|
|
4895
5095
|
|
|
4896
5096
|
// src/templates/handoff.ts
|
|
4897
|
-
function renderHandoff(
|
|
5097
|
+
function renderHandoff(params2) {
|
|
4898
5098
|
return `---
|
|
4899
|
-
assignment: ${
|
|
4900
|
-
updated: "${
|
|
5099
|
+
assignment: ${params2.assignmentSlug}
|
|
5100
|
+
updated: "${params2.timestamp}"
|
|
4901
5101
|
handoffCount: 0
|
|
4902
5102
|
---
|
|
4903
5103
|
|
|
@@ -4908,12 +5108,12 @@ No handoffs recorded yet.
|
|
|
4908
5108
|
}
|
|
4909
5109
|
|
|
4910
5110
|
// src/templates/progress.ts
|
|
4911
|
-
function renderProgress(
|
|
5111
|
+
function renderProgress(params2) {
|
|
4912
5112
|
return `---
|
|
4913
|
-
assignment: ${
|
|
5113
|
+
assignment: ${params2.assignment}
|
|
4914
5114
|
entryCount: 0
|
|
4915
|
-
generated: "${
|
|
4916
|
-
updated: "${
|
|
5115
|
+
generated: "${params2.timestamp}"
|
|
5116
|
+
updated: "${params2.timestamp}"
|
|
4917
5117
|
---
|
|
4918
5118
|
|
|
4919
5119
|
# Progress
|
|
@@ -4923,12 +5123,12 @@ No progress yet.
|
|
|
4923
5123
|
}
|
|
4924
5124
|
|
|
4925
5125
|
// src/templates/comments.ts
|
|
4926
|
-
function renderComments(
|
|
5126
|
+
function renderComments(params2) {
|
|
4927
5127
|
return `---
|
|
4928
|
-
assignment: ${
|
|
5128
|
+
assignment: ${params2.assignment}
|
|
4929
5129
|
entryCount: 0
|
|
4930
|
-
generated: "${
|
|
4931
|
-
updated: "${
|
|
5130
|
+
generated: "${params2.timestamp}"
|
|
5131
|
+
updated: "${params2.timestamp}"
|
|
4932
5132
|
---
|
|
4933
5133
|
|
|
4934
5134
|
# Comments
|
|
@@ -4956,10 +5156,10 @@ function formatCommentEntry(comment) {
|
|
|
4956
5156
|
}
|
|
4957
5157
|
|
|
4958
5158
|
// src/templates/decision-record.ts
|
|
4959
|
-
function renderDecisionRecord(
|
|
5159
|
+
function renderDecisionRecord(params2) {
|
|
4960
5160
|
return `---
|
|
4961
|
-
assignment: ${
|
|
4962
|
-
updated: "${
|
|
5161
|
+
assignment: ${params2.assignmentSlug}
|
|
5162
|
+
updated: "${params2.timestamp}"
|
|
4963
5163
|
decisionCount: 0
|
|
4964
5164
|
---
|
|
4965
5165
|
|
|
@@ -4970,10 +5170,10 @@ No decisions recorded yet.
|
|
|
4970
5170
|
}
|
|
4971
5171
|
|
|
4972
5172
|
// src/templates/index-stubs.ts
|
|
4973
|
-
function renderIndexAssignments(
|
|
5173
|
+
function renderIndexAssignments(params2) {
|
|
4974
5174
|
return `---
|
|
4975
|
-
project: ${
|
|
4976
|
-
generated: "${
|
|
5175
|
+
project: ${params2.slug}
|
|
5176
|
+
generated: "${params2.timestamp}"
|
|
4977
5177
|
total: 0
|
|
4978
5178
|
by_status:
|
|
4979
5179
|
pending: 0
|
|
@@ -4990,10 +5190,10 @@ by_status:
|
|
|
4990
5190
|
|------|-------|--------|----------|----------|--------------|---------|
|
|
4991
5191
|
`;
|
|
4992
5192
|
}
|
|
4993
|
-
function renderIndexPlans(
|
|
5193
|
+
function renderIndexPlans(params2) {
|
|
4994
5194
|
return `---
|
|
4995
|
-
project: ${
|
|
4996
|
-
generated: "${
|
|
5195
|
+
project: ${params2.slug}
|
|
5196
|
+
generated: "${params2.timestamp}"
|
|
4997
5197
|
---
|
|
4998
5198
|
|
|
4999
5199
|
# Plans
|
|
@@ -5002,10 +5202,10 @@ generated: "${params.timestamp}"
|
|
|
5002
5202
|
|------------|-------------|---------|
|
|
5003
5203
|
`;
|
|
5004
5204
|
}
|
|
5005
|
-
function renderIndexDecisions(
|
|
5205
|
+
function renderIndexDecisions(params2) {
|
|
5006
5206
|
return `---
|
|
5007
|
-
project: ${
|
|
5008
|
-
generated: "${
|
|
5207
|
+
project: ${params2.slug}
|
|
5208
|
+
generated: "${params2.timestamp}"
|
|
5009
5209
|
---
|
|
5010
5210
|
|
|
5011
5211
|
# Decision Records
|
|
@@ -5014,10 +5214,10 @@ generated: "${params.timestamp}"
|
|
|
5014
5214
|
|------------|-------|-----------------|---------------|---------|
|
|
5015
5215
|
`;
|
|
5016
5216
|
}
|
|
5017
|
-
function renderStatus(
|
|
5217
|
+
function renderStatus(params2) {
|
|
5018
5218
|
return `---
|
|
5019
|
-
project: ${
|
|
5020
|
-
generated: "${
|
|
5219
|
+
project: ${params2.slug}
|
|
5220
|
+
generated: "${params2.timestamp}"
|
|
5021
5221
|
status: pending
|
|
5022
5222
|
progress:
|
|
5023
5223
|
total: 0
|
|
@@ -5033,7 +5233,7 @@ needsAttention:
|
|
|
5033
5233
|
openQuestions: 0
|
|
5034
5234
|
---
|
|
5035
5235
|
|
|
5036
|
-
# Project Status: ${
|
|
5236
|
+
# Project Status: ${params2.title}
|
|
5037
5237
|
|
|
5038
5238
|
**Status:** pending
|
|
5039
5239
|
**Progress:** 0/0 assignments complete
|
|
@@ -5053,10 +5253,10 @@ No dependencies yet.
|
|
|
5053
5253
|
- **0 unanswered** questions
|
|
5054
5254
|
`;
|
|
5055
5255
|
}
|
|
5056
|
-
function renderResourcesIndex(
|
|
5256
|
+
function renderResourcesIndex(params2) {
|
|
5057
5257
|
return `---
|
|
5058
|
-
project: ${
|
|
5059
|
-
generated: "${
|
|
5258
|
+
project: ${params2.slug}
|
|
5259
|
+
generated: "${params2.timestamp}"
|
|
5060
5260
|
total: 0
|
|
5061
5261
|
---
|
|
5062
5262
|
|
|
@@ -5066,10 +5266,10 @@ total: 0
|
|
|
5066
5266
|
|------|----------|--------|---------------------|---------|
|
|
5067
5267
|
`;
|
|
5068
5268
|
}
|
|
5069
|
-
function renderMemoriesIndex(
|
|
5269
|
+
function renderMemoriesIndex(params2) {
|
|
5070
5270
|
return `---
|
|
5071
|
-
project: ${
|
|
5072
|
-
generated: "${
|
|
5271
|
+
project: ${params2.slug}
|
|
5272
|
+
generated: "${params2.timestamp}"
|
|
5073
5273
|
total: 0
|
|
5074
5274
|
---
|
|
5075
5275
|
|
|
@@ -5081,19 +5281,19 @@ total: 0
|
|
|
5081
5281
|
}
|
|
5082
5282
|
|
|
5083
5283
|
// src/templates/playbook.ts
|
|
5084
|
-
function renderPlaybook(
|
|
5085
|
-
const whenToUse =
|
|
5284
|
+
function renderPlaybook(params2) {
|
|
5285
|
+
const whenToUse = params2.whenToUse ? escapeYamlString(params2.whenToUse) : "null";
|
|
5086
5286
|
return `---
|
|
5087
|
-
name: ${escapeYamlString(
|
|
5088
|
-
slug: ${
|
|
5089
|
-
description: ${escapeYamlString(
|
|
5287
|
+
name: ${escapeYamlString(params2.name)}
|
|
5288
|
+
slug: ${params2.slug}
|
|
5289
|
+
description: ${escapeYamlString(params2.description)}
|
|
5090
5290
|
when_to_use: ${whenToUse}
|
|
5091
|
-
created: "${
|
|
5092
|
-
updated: "${
|
|
5291
|
+
created: "${params2.timestamp}"
|
|
5292
|
+
updated: "${params2.timestamp}"
|
|
5093
5293
|
tags: []
|
|
5094
5294
|
---
|
|
5095
5295
|
|
|
5096
|
-
# ${
|
|
5296
|
+
# ${params2.name}
|
|
5097
5297
|
|
|
5098
5298
|
<!-- Write imperative rules and workflows here. Keep it under 50 lines. -->
|
|
5099
5299
|
`;
|
|
@@ -5236,58 +5436,58 @@ Follow the rules in each playbook. They take precedence over default conventions
|
|
|
5236
5436
|
- Commit frequently with messages referencing the assignment slug.
|
|
5237
5437
|
`;
|
|
5238
5438
|
}
|
|
5239
|
-
function renderCursorAssignment(
|
|
5439
|
+
function renderCursorAssignment(params2) {
|
|
5240
5440
|
return `---
|
|
5241
|
-
description: Syntaur assignment context for ${
|
|
5441
|
+
description: Syntaur assignment context for ${params2.projectSlug}/${params2.assignmentSlug}
|
|
5242
5442
|
globs:
|
|
5243
5443
|
alwaysApply: true
|
|
5244
5444
|
---
|
|
5245
5445
|
|
|
5246
5446
|
# Current Assignment Context
|
|
5247
5447
|
|
|
5248
|
-
- **Project:** ${
|
|
5249
|
-
- **Assignment:** ${
|
|
5250
|
-
- **Project directory:** ${
|
|
5251
|
-
- **Assignment directory:** ${
|
|
5448
|
+
- **Project:** ${params2.projectSlug}
|
|
5449
|
+
- **Assignment:** ${params2.assignmentSlug}
|
|
5450
|
+
- **Project directory:** ${params2.projectDir}
|
|
5451
|
+
- **Assignment directory:** ${params2.assignmentDir}
|
|
5252
5452
|
|
|
5253
5453
|
## Reading Order
|
|
5254
5454
|
|
|
5255
5455
|
Before starting work, read these files in order:
|
|
5256
|
-
1. \`${
|
|
5257
|
-
2. \`${
|
|
5258
|
-
3. any \`${
|
|
5259
|
-
4. \`${
|
|
5260
|
-
5. \`${
|
|
5261
|
-
6. \`${
|
|
5456
|
+
1. \`${params2.projectDir}/project.md\` -- project overview and goals (project-nested assignments only)
|
|
5457
|
+
2. \`${params2.assignmentDir}/assignment.md\` -- your assignment details, acceptance criteria, todos, current status. Frontmatter includes \`project: <slug> | null\` (null for standalone) and \`type: <classification> | null\`.
|
|
5458
|
+
3. any \`${params2.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
|
|
5459
|
+
4. \`${params2.assignmentDir}/progress.md\` -- reverse-chron progress log (if present)
|
|
5460
|
+
5. \`${params2.assignmentDir}/comments.md\` -- threaded questions/notes/feedback (if present)
|
|
5461
|
+
6. \`${params2.assignmentDir}/handoff.md\` -- previous session handoff notes
|
|
5262
5462
|
|
|
5263
5463
|
## Your Writable Files
|
|
5264
5464
|
|
|
5265
5465
|
You may write directly to these files inside your assignment folder:
|
|
5266
|
-
- \`${
|
|
5267
|
-
- \`${
|
|
5268
|
-
- \`${
|
|
5269
|
-
- \`${
|
|
5270
|
-
- \`${
|
|
5271
|
-
- \`${
|
|
5466
|
+
- \`${params2.assignmentDir}/assignment.md\`
|
|
5467
|
+
- \`${params2.assignmentDir}/plan*.md\` (0 or more versioned plan files, e.g., \`plan.md\`, \`plan-v2.md\`)
|
|
5468
|
+
- \`${params2.assignmentDir}/progress.md\` (append timestamped entries, newest first)
|
|
5469
|
+
- \`${params2.assignmentDir}/scratchpad.md\`
|
|
5470
|
+
- \`${params2.assignmentDir}/handoff.md\`
|
|
5471
|
+
- \`${params2.assignmentDir}/decision-record.md\`
|
|
5272
5472
|
|
|
5273
|
-
Do NOT edit \`${
|
|
5473
|
+
Do NOT edit \`${params2.assignmentDir}/comments.md\` directly \u2014 use \`syntaur comment\`. Do NOT edit other assignments' files \u2014 use \`syntaur request\` for cross-assignment todos.
|
|
5274
5474
|
|
|
5275
5475
|
And source code files in your workspace. Read the \`workspace\` field from your assignment's frontmatter to determine the exact boundary. If not set, the current working directory is your workspace.
|
|
5276
5476
|
`;
|
|
5277
5477
|
}
|
|
5278
5478
|
|
|
5279
5479
|
// src/templates/codex-agents.ts
|
|
5280
|
-
function renderCodexAgents(
|
|
5480
|
+
function renderCodexAgents(params2) {
|
|
5281
5481
|
return `# Syntaur Protocol -- Agent Instructions
|
|
5282
5482
|
|
|
5283
5483
|
This project uses the Syntaur protocol for multi-agent project coordination.
|
|
5284
5484
|
|
|
5285
5485
|
## Current Assignment
|
|
5286
5486
|
|
|
5287
|
-
- **Project:** ${
|
|
5288
|
-
- **Assignment:** ${
|
|
5289
|
-
- **Project directory:** ${
|
|
5290
|
-
- **Assignment directory:** ${
|
|
5487
|
+
- **Project:** ${params2.projectSlug}
|
|
5488
|
+
- **Assignment:** ${params2.assignmentSlug}
|
|
5489
|
+
- **Project directory:** ${params2.projectDir}
|
|
5490
|
+
- **Assignment directory:** ${params2.assignmentDir}
|
|
5291
5491
|
|
|
5292
5492
|
## Preferred Workflow
|
|
5293
5493
|
|
|
@@ -5307,13 +5507,13 @@ If the plugin is unavailable, follow the same workflow manually with the \`synta
|
|
|
5307
5507
|
## Reading Order
|
|
5308
5508
|
|
|
5309
5509
|
Before starting work, read these files in order:
|
|
5310
|
-
1. \`${
|
|
5311
|
-
2. \`${
|
|
5312
|
-
3. \`${
|
|
5313
|
-
4. any \`${
|
|
5314
|
-
5. \`${
|
|
5315
|
-
6. \`${
|
|
5316
|
-
7. \`${
|
|
5510
|
+
1. \`${params2.projectDir}/manifest.md\` -- root navigation entry point (project-nested assignments only)
|
|
5511
|
+
2. \`${params2.projectDir}/project.md\` -- project overview and goals (project-nested assignments only)
|
|
5512
|
+
3. \`${params2.assignmentDir}/assignment.md\` -- your assignment details, acceptance criteria, todos, current status. Frontmatter now includes \`project: <slug> | null\` (null for standalone) and \`type: <classification> | null\`.
|
|
5513
|
+
4. any \`${params2.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
|
|
5514
|
+
5. \`${params2.assignmentDir}/progress.md\` -- reverse-chron progress log (if present)
|
|
5515
|
+
6. \`${params2.assignmentDir}/comments.md\` -- threaded questions/notes/feedback (if present)
|
|
5516
|
+
7. \`${params2.assignmentDir}/handoff.md\` -- previous session handoff notes
|
|
5317
5517
|
|
|
5318
5518
|
## Context File
|
|
5319
5519
|
|
|
@@ -5365,10 +5565,10 @@ Before starting work, read these files in order:
|
|
|
5365
5565
|
### Files you may WRITE:
|
|
5366
5566
|
1. **Your assignment folder** -- only the assignment you are currently working on:
|
|
5367
5567
|
- \`assignment.md\`, \`plan*.md\` (0 or more versioned plan files), \`progress.md\`, \`scratchpad.md\`, \`handoff.md\`, \`decision-record.md\`
|
|
5368
|
-
- Path: \`${
|
|
5568
|
+
- Path: \`${params2.assignmentDir}/\`
|
|
5369
5569
|
2. **Shared resources and memories** at the project level:
|
|
5370
|
-
- \`${
|
|
5371
|
-
- \`${
|
|
5570
|
+
- \`${params2.projectDir}/resources/<slug>.md\`
|
|
5571
|
+
- \`${params2.projectDir}/memories/<slug>.md\`
|
|
5372
5572
|
3. **Your workspace** -- source code files in the current working directory (the directory where this AGENTS.md lives). If your assignment's frontmatter specifies a \`workspace\` field, read it at runtime to determine the exact boundary.
|
|
5373
5573
|
|
|
5374
5574
|
> **Note:** Workspace boundaries are resolved by the agent at runtime by reading \`assignment.md\` frontmatter. If no \`workspace\` field is set, treat the current working directory as your workspace.
|
|
@@ -5413,15 +5613,15 @@ Before starting work, read these files in order:
|
|
|
5413
5613
|
## Lifecycle Commands
|
|
5414
5614
|
|
|
5415
5615
|
Use the \`syntaur\` CLI for state transitions and coordination:
|
|
5416
|
-
- \`syntaur assign ${
|
|
5417
|
-
- \`syntaur start ${
|
|
5418
|
-
- \`syntaur review ${
|
|
5419
|
-
- \`syntaur complete ${
|
|
5420
|
-
- \`syntaur block ${
|
|
5421
|
-
- \`syntaur unblock ${
|
|
5422
|
-
- \`syntaur fail ${
|
|
5423
|
-
- \`syntaur comment ${
|
|
5424
|
-
- \`syntaur request ${
|
|
5616
|
+
- \`syntaur assign ${params2.assignmentSlug} --agent <name> --project ${params2.projectSlug}\` -- set assignee
|
|
5617
|
+
- \`syntaur start ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- pending -> in_progress
|
|
5618
|
+
- \`syntaur review ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- in_progress -> review
|
|
5619
|
+
- \`syntaur complete ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- in_progress/review -> completed
|
|
5620
|
+
- \`syntaur block ${params2.assignmentSlug} --project ${params2.projectSlug} --reason <text>\` -- block
|
|
5621
|
+
- \`syntaur unblock ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- unblock
|
|
5622
|
+
- \`syntaur fail ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- mark as failed
|
|
5623
|
+
- \`syntaur comment ${params2.assignmentSlug} "body" --type question|note|feedback [--reply-to <id>]\` -- append to \`comments.md\` (use for all Q&A; questions support resolve toggle)
|
|
5624
|
+
- \`syntaur request ${params2.assignmentSlug} <target-slug-or-uuid> "text"\` -- append a todo to another assignment's \`## Todos\` annotated \`(from: ${params2.assignmentSlug})\`
|
|
5425
5625
|
|
|
5426
5626
|
## Troubleshooting
|
|
5427
5627
|
|
|
@@ -5452,11 +5652,11 @@ Read each linked playbook and follow the rules in its body section. The \`when_t
|
|
|
5452
5652
|
}
|
|
5453
5653
|
|
|
5454
5654
|
// src/templates/opencode-config.ts
|
|
5455
|
-
function renderOpenCodeConfig(
|
|
5655
|
+
function renderOpenCodeConfig(params2) {
|
|
5456
5656
|
const config = {
|
|
5457
5657
|
instructions: [
|
|
5458
5658
|
`Read AGENTS.md in this directory for Syntaur protocol (v2.0) instructions.`,
|
|
5459
|
-
`Read ${
|
|
5659
|
+
`Read ${params2.projectDir}/project.md for project overview (project-nested assignments only).`,
|
|
5460
5660
|
`Append timestamped progress entries to the assignment's progress.md (not to assignment.md).`,
|
|
5461
5661
|
`Use 'syntaur comment <slug-or-uuid> "body" --type question|note|feedback' to append to comments.md \u2014 never edit it directly.`,
|
|
5462
5662
|
`Use 'syntaur request <source> <target> "text"' to append a todo to another assignment's ## Todos.`,
|
|
@@ -5552,6 +5752,14 @@ async function createAssignmentCommand(title, options) {
|
|
|
5552
5752
|
if (!title.trim()) {
|
|
5553
5753
|
throw new Error("Assignment title cannot be empty.");
|
|
5554
5754
|
}
|
|
5755
|
+
if (options.workspace && options.project) {
|
|
5756
|
+
throw new Error(
|
|
5757
|
+
"Cannot use --workspace with --project (projects already carry a workspace via project.workspace)."
|
|
5758
|
+
);
|
|
5759
|
+
}
|
|
5760
|
+
if (options.workspace && !options.oneOff) {
|
|
5761
|
+
throw new Error("--workspace requires --one-off.");
|
|
5762
|
+
}
|
|
5555
5763
|
if (!options.project && !options.oneOff) {
|
|
5556
5764
|
throw new Error(
|
|
5557
5765
|
"Either --project <slug> or --one-off is required."
|
|
@@ -5567,6 +5775,11 @@ async function createAssignmentCommand(title, options) {
|
|
|
5567
5775
|
`Invalid project slug "${options.project}". Slugs must be lowercase, hyphen-separated, with no special characters.`
|
|
5568
5776
|
);
|
|
5569
5777
|
}
|
|
5778
|
+
if (options.workspace && !isValidSlug(options.workspace)) {
|
|
5779
|
+
throw new Error(
|
|
5780
|
+
`Invalid workspace slug "${options.workspace}". Slugs must be lowercase, hyphen-separated, with no special characters.`
|
|
5781
|
+
);
|
|
5782
|
+
}
|
|
5570
5783
|
if (options.oneOff && options.dependsOn) {
|
|
5571
5784
|
throw new Error("Standalone assignments cannot have dependencies (--depends-on is not allowed with --one-off).");
|
|
5572
5785
|
}
|
|
@@ -5657,7 +5870,9 @@ Use --slug to specify a different slug.`
|
|
|
5657
5870
|
dependsOn,
|
|
5658
5871
|
links,
|
|
5659
5872
|
project: projectSlug,
|
|
5660
|
-
|
|
5873
|
+
workspaceGroup: options.workspace ?? null,
|
|
5874
|
+
type: options.type,
|
|
5875
|
+
includeTodos: options.withTodos === true
|
|
5661
5876
|
})
|
|
5662
5877
|
],
|
|
5663
5878
|
[
|
|
@@ -5737,7 +5952,7 @@ Use --slug to specify a different slug.`
|
|
|
5737
5952
|
init_config2();
|
|
5738
5953
|
import { spawn } from "child_process";
|
|
5739
5954
|
import { createServer as createNetServer } from "net";
|
|
5740
|
-
import { resolve as
|
|
5955
|
+
import { resolve as resolve22, dirname as dirname4 } from "path";
|
|
5741
5956
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5742
5957
|
|
|
5743
5958
|
// src/dashboard/server.ts
|
|
@@ -5746,7 +5961,7 @@ init_api();
|
|
|
5746
5961
|
init_assignment_resolver();
|
|
5747
5962
|
import express from "express";
|
|
5748
5963
|
import { createServer } from "http";
|
|
5749
|
-
import { resolve as
|
|
5964
|
+
import { resolve as resolve21 } from "path";
|
|
5750
5965
|
import { writeFile as writeFile5, unlink as unlink4 } from "fs/promises";
|
|
5751
5966
|
import { WebSocketServer, WebSocket } from "ws";
|
|
5752
5967
|
|
|
@@ -5909,8 +6124,8 @@ async function migrateFromMarkdown(projectsDir2) {
|
|
|
5909
6124
|
return allSessions.length;
|
|
5910
6125
|
}
|
|
5911
6126
|
async function parseMarkdownSessionsIndex(filePath, projectSlug) {
|
|
5912
|
-
const { readFile:
|
|
5913
|
-
const raw = await
|
|
6127
|
+
const { readFile: readFile30 } = await import("fs/promises");
|
|
6128
|
+
const raw = await readFile30(filePath, "utf-8");
|
|
5914
6129
|
const sessions = [];
|
|
5915
6130
|
const lines = raw.split("\n");
|
|
5916
6131
|
let inTable = false;
|
|
@@ -6097,18 +6312,25 @@ function createWatcher(options) {
|
|
|
6097
6312
|
if (parts.length === 0) return;
|
|
6098
6313
|
const projectSlug = parts[0];
|
|
6099
6314
|
let assignmentSlug;
|
|
6315
|
+
let isProjectTodos = false;
|
|
6100
6316
|
if (parts.length >= 3 && parts[1] === "assignments") {
|
|
6101
6317
|
assignmentSlug = parts[2];
|
|
6318
|
+
} else if (parts.length >= 2 && parts[1] === "todos") {
|
|
6319
|
+
isProjectTodos = true;
|
|
6102
6320
|
}
|
|
6103
|
-
const debounceKey = assignmentSlug ? `${projectSlug}/${assignmentSlug}` : projectSlug;
|
|
6321
|
+
const debounceKey = isProjectTodos ? `todos:${projectSlug}` : assignmentSlug ? `${projectSlug}/${assignmentSlug}` : projectSlug;
|
|
6104
6322
|
const existing = pendingEvents.get(debounceKey);
|
|
6105
6323
|
if (existing) clearTimeout(existing);
|
|
6106
|
-
const messageType = assignmentSlug ? "assignment-updated" : "project-updated";
|
|
6324
|
+
const messageType = isProjectTodos ? "todos-updated" : assignmentSlug ? "assignment-updated" : "project-updated";
|
|
6107
6325
|
pendingEvents.set(
|
|
6108
6326
|
debounceKey,
|
|
6109
6327
|
setTimeout(() => {
|
|
6110
6328
|
pendingEvents.delete(debounceKey);
|
|
6111
|
-
const message = {
|
|
6329
|
+
const message = isProjectTodos ? {
|
|
6330
|
+
type: "todos-updated",
|
|
6331
|
+
projectSlug,
|
|
6332
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6333
|
+
} : {
|
|
6112
6334
|
type: messageType,
|
|
6113
6335
|
projectSlug,
|
|
6114
6336
|
assignmentSlug,
|
|
@@ -6434,7 +6656,15 @@ function createWriteRouter(projectsDir2, assignmentsDir2) {
|
|
|
6434
6656
|
});
|
|
6435
6657
|
res.json({ content });
|
|
6436
6658
|
});
|
|
6437
|
-
router.get("/api/templates/assignment", (
|
|
6659
|
+
router.get("/api/templates/assignment", (req, res) => {
|
|
6660
|
+
const standalone = req.query.standalone === "1";
|
|
6661
|
+
const workspaceParam = typeof req.query.workspace === "string" ? req.query.workspace : "";
|
|
6662
|
+
if (workspaceParam && !isValidSlug(workspaceParam)) {
|
|
6663
|
+
res.status(400).json({
|
|
6664
|
+
error: `Invalid workspace slug "${workspaceParam}". Slugs must be lowercase, hyphen-separated, with no special characters.`
|
|
6665
|
+
});
|
|
6666
|
+
return;
|
|
6667
|
+
}
|
|
6438
6668
|
const content = renderAssignment({
|
|
6439
6669
|
id: generateId(),
|
|
6440
6670
|
slug: "my-new-assignment",
|
|
@@ -6442,7 +6672,9 @@ function createWriteRouter(projectsDir2, assignmentsDir2) {
|
|
|
6442
6672
|
timestamp: nowTimestamp(),
|
|
6443
6673
|
priority: "medium",
|
|
6444
6674
|
dependsOn: [],
|
|
6445
|
-
links: []
|
|
6675
|
+
links: [],
|
|
6676
|
+
project: standalone ? null : void 0,
|
|
6677
|
+
workspaceGroup: standalone && workspaceParam ? workspaceParam : null
|
|
6446
6678
|
});
|
|
6447
6679
|
res.json({ content });
|
|
6448
6680
|
});
|
|
@@ -7171,6 +7403,76 @@ ${entry}`;
|
|
|
7171
7403
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
7172
7404
|
return;
|
|
7173
7405
|
}
|
|
7406
|
+
const rawContent = typeof req.body?.content === "string" ? req.body.content : "";
|
|
7407
|
+
if (rawContent.trim()) {
|
|
7408
|
+
const fields = extractFrontmatter3(rawContent);
|
|
7409
|
+
if (!fields) {
|
|
7410
|
+
res.status(400).json({ error: "Invalid frontmatter: missing --- delimiters" });
|
|
7411
|
+
return;
|
|
7412
|
+
}
|
|
7413
|
+
const validation = validateRequired(fields, ["slug", "title"]);
|
|
7414
|
+
if (!validation.valid) {
|
|
7415
|
+
res.status(400).json({ error: `Missing required fields: ${validation.missing.join(", ")}` });
|
|
7416
|
+
return;
|
|
7417
|
+
}
|
|
7418
|
+
const submittedSlug = fields.slug;
|
|
7419
|
+
if (!isValidSlug(submittedSlug)) {
|
|
7420
|
+
res.status(400).json({ error: `Invalid slug "${submittedSlug}". Must be lowercase and hyphen-separated.` });
|
|
7421
|
+
return;
|
|
7422
|
+
}
|
|
7423
|
+
const validPriorities = ["low", "medium", "high", "critical"];
|
|
7424
|
+
const submittedPriority = fields.priority || "medium";
|
|
7425
|
+
if (!validPriorities.includes(submittedPriority)) {
|
|
7426
|
+
res.status(400).json({ error: `Invalid priority "${submittedPriority}". Must be low, medium, high, or critical.` });
|
|
7427
|
+
return;
|
|
7428
|
+
}
|
|
7429
|
+
if (fields.project && fields.project !== "null") {
|
|
7430
|
+
res.status(400).json({
|
|
7431
|
+
error: 'Standalone assignments cannot have a project; remove "project" or set it to null.'
|
|
7432
|
+
});
|
|
7433
|
+
return;
|
|
7434
|
+
}
|
|
7435
|
+
const submittedWorkspaceGroup = fields.workspaceGroup && fields.workspaceGroup !== "null" ? fields.workspaceGroup : "";
|
|
7436
|
+
if (submittedWorkspaceGroup && !isValidSlug(submittedWorkspaceGroup)) {
|
|
7437
|
+
res.status(400).json({
|
|
7438
|
+
error: `Invalid workspace slug "${submittedWorkspaceGroup}". Slugs must be lowercase, hyphen-separated, with no special characters.`
|
|
7439
|
+
});
|
|
7440
|
+
return;
|
|
7441
|
+
}
|
|
7442
|
+
const id2 = generateId();
|
|
7443
|
+
const assignmentDir2 = resolve15(assignmentsDir2, id2);
|
|
7444
|
+
if (await fileExists(assignmentDir2)) {
|
|
7445
|
+
res.status(500).json({ error: "UUID collision \u2014 try again" });
|
|
7446
|
+
return;
|
|
7447
|
+
}
|
|
7448
|
+
const timestamp2 = fields.created || nowTimestamp();
|
|
7449
|
+
await ensureDir(assignmentDir2);
|
|
7450
|
+
const normalizedContent = setTopLevelField(rawContent, "id", id2);
|
|
7451
|
+
await writeFileForce(resolve15(assignmentDir2, "assignment.md"), normalizedContent);
|
|
7452
|
+
await writeFileForce(
|
|
7453
|
+
resolve15(assignmentDir2, "scratchpad.md"),
|
|
7454
|
+
renderScratchpad({ assignmentSlug: id2, timestamp: timestamp2 })
|
|
7455
|
+
);
|
|
7456
|
+
await writeFileForce(
|
|
7457
|
+
resolve15(assignmentDir2, "handoff.md"),
|
|
7458
|
+
renderHandoff({ assignmentSlug: id2, timestamp: timestamp2 })
|
|
7459
|
+
);
|
|
7460
|
+
await writeFileForce(
|
|
7461
|
+
resolve15(assignmentDir2, "decision-record.md"),
|
|
7462
|
+
renderDecisionRecord({ assignmentSlug: id2, timestamp: timestamp2 })
|
|
7463
|
+
);
|
|
7464
|
+
await writeFileForce(
|
|
7465
|
+
resolve15(assignmentDir2, "progress.md"),
|
|
7466
|
+
renderProgress({ assignment: id2, timestamp: timestamp2 })
|
|
7467
|
+
);
|
|
7468
|
+
await writeFileForce(
|
|
7469
|
+
resolve15(assignmentDir2, "comments.md"),
|
|
7470
|
+
renderComments({ assignment: id2, timestamp: timestamp2 })
|
|
7471
|
+
);
|
|
7472
|
+
const detail2 = await getAssignmentDetailById(projectsDir2, assignmentsDir2, id2);
|
|
7473
|
+
res.status(201).json({ assignment: detail2 });
|
|
7474
|
+
return;
|
|
7475
|
+
}
|
|
7174
7476
|
const { title, slug, priority, type } = req.body || {};
|
|
7175
7477
|
if (!title || typeof title !== "string" || !title.trim()) {
|
|
7176
7478
|
res.status(400).json({ error: "title is required" });
|
|
@@ -7953,6 +8255,7 @@ import { resolve as resolve17 } from "path";
|
|
|
7953
8255
|
import { readFile as readFile12, unlink as unlink2 } from "fs/promises";
|
|
7954
8256
|
init_timestamp();
|
|
7955
8257
|
init_fs();
|
|
8258
|
+
init_playbooks();
|
|
7956
8259
|
function createPlaybooksRouter(playbooksDir3) {
|
|
7957
8260
|
const router = Router4();
|
|
7958
8261
|
router.get("/", async (_req, res) => {
|
|
@@ -7976,6 +8279,32 @@ function createPlaybooksRouter(playbooksDir3) {
|
|
|
7976
8279
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get template" });
|
|
7977
8280
|
}
|
|
7978
8281
|
});
|
|
8282
|
+
router.post("/:slug/enable", async (req, res) => {
|
|
8283
|
+
try {
|
|
8284
|
+
const result = await setPlaybookEnabled(playbooksDir3, req.params.slug, true);
|
|
8285
|
+
res.json({ slug: result.slug, enabled: result.enabled, changed: result.changed });
|
|
8286
|
+
} catch (error) {
|
|
8287
|
+
const msg = error instanceof Error ? error.message : "Failed to enable playbook";
|
|
8288
|
+
if (msg.startsWith("Playbook ")) {
|
|
8289
|
+
res.status(404).json({ error: msg });
|
|
8290
|
+
return;
|
|
8291
|
+
}
|
|
8292
|
+
res.status(500).json({ error: msg });
|
|
8293
|
+
}
|
|
8294
|
+
});
|
|
8295
|
+
router.post("/:slug/disable", async (req, res) => {
|
|
8296
|
+
try {
|
|
8297
|
+
const result = await setPlaybookEnabled(playbooksDir3, req.params.slug, false);
|
|
8298
|
+
res.json({ slug: result.slug, enabled: result.enabled, changed: result.changed });
|
|
8299
|
+
} catch (error) {
|
|
8300
|
+
const msg = error instanceof Error ? error.message : "Failed to disable playbook";
|
|
8301
|
+
if (msg.startsWith("Playbook ")) {
|
|
8302
|
+
res.status(404).json({ error: msg });
|
|
8303
|
+
return;
|
|
8304
|
+
}
|
|
8305
|
+
res.status(500).json({ error: msg });
|
|
8306
|
+
}
|
|
8307
|
+
});
|
|
7979
8308
|
router.get("/:slug", async (req, res) => {
|
|
7980
8309
|
try {
|
|
7981
8310
|
const detail = await getPlaybookDetail(playbooksDir3, req.params.slug);
|
|
@@ -7990,17 +8319,18 @@ function createPlaybooksRouter(playbooksDir3) {
|
|
|
7990
8319
|
});
|
|
7991
8320
|
router.get("/:slug/edit", async (req, res) => {
|
|
7992
8321
|
try {
|
|
7993
|
-
const
|
|
7994
|
-
if (!
|
|
8322
|
+
const resolved = await resolvePlaybookSlug(playbooksDir3, req.params.slug);
|
|
8323
|
+
if (!resolved) {
|
|
7995
8324
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
7996
8325
|
return;
|
|
7997
8326
|
}
|
|
8327
|
+
const filePath = resolve17(playbooksDir3, resolved.filename);
|
|
7998
8328
|
const content = await readFile12(filePath, "utf-8");
|
|
7999
8329
|
res.json({
|
|
8000
8330
|
documentType: "playbook",
|
|
8001
|
-
title: `Edit Playbook: ${
|
|
8331
|
+
title: `Edit Playbook: ${resolved.slug}`,
|
|
8002
8332
|
content,
|
|
8003
|
-
slug:
|
|
8333
|
+
slug: resolved.slug
|
|
8004
8334
|
});
|
|
8005
8335
|
} catch (error) {
|
|
8006
8336
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get playbook for editing" });
|
|
@@ -8039,14 +8369,15 @@ function createPlaybooksRouter(playbooksDir3) {
|
|
|
8039
8369
|
res.status(400).json({ error: "content is required" });
|
|
8040
8370
|
return;
|
|
8041
8371
|
}
|
|
8042
|
-
const
|
|
8043
|
-
if (!
|
|
8372
|
+
const resolved = await resolvePlaybookSlug(playbooksDir3, req.params.slug);
|
|
8373
|
+
if (!resolved) {
|
|
8044
8374
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
8045
8375
|
return;
|
|
8046
8376
|
}
|
|
8377
|
+
const filePath = resolve17(playbooksDir3, resolved.filename);
|
|
8047
8378
|
await writeFileForce(filePath, content);
|
|
8048
8379
|
await rebuildPlaybookManifest(playbooksDir3);
|
|
8049
|
-
res.json({ slug:
|
|
8380
|
+
res.json({ slug: resolved.slug, path: filePath });
|
|
8050
8381
|
} catch (error) {
|
|
8051
8382
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to update playbook" });
|
|
8052
8383
|
}
|
|
@@ -8057,14 +8388,16 @@ function createPlaybooksRouter(playbooksDir3) {
|
|
|
8057
8388
|
res.status(403).json({ error: "The playbook manifest cannot be deleted" });
|
|
8058
8389
|
return;
|
|
8059
8390
|
}
|
|
8060
|
-
const
|
|
8061
|
-
if (!
|
|
8391
|
+
const resolved = await resolvePlaybookSlug(playbooksDir3, req.params.slug);
|
|
8392
|
+
if (!resolved) {
|
|
8062
8393
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
8063
8394
|
return;
|
|
8064
8395
|
}
|
|
8396
|
+
const filePath = resolve17(playbooksDir3, resolved.filename);
|
|
8065
8397
|
await unlink2(filePath);
|
|
8398
|
+
await removeFromDisabledList(resolved.slug);
|
|
8066
8399
|
await rebuildPlaybookManifest(playbooksDir3);
|
|
8067
|
-
res.json({ deleted:
|
|
8400
|
+
res.json({ deleted: resolved.slug });
|
|
8068
8401
|
} catch (error) {
|
|
8069
8402
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to delete playbook" });
|
|
8070
8403
|
}
|
|
@@ -8088,14 +8421,17 @@ function getWorkspaceParam(value) {
|
|
|
8088
8421
|
return value ?? "";
|
|
8089
8422
|
}
|
|
8090
8423
|
var writeLocks = /* @__PURE__ */ new Map();
|
|
8091
|
-
function withLock(
|
|
8092
|
-
const prev = writeLocks.get(
|
|
8424
|
+
function withLock(lockKey, fn) {
|
|
8425
|
+
const prev = writeLocks.get(lockKey) ?? Promise.resolve();
|
|
8093
8426
|
const next = prev.then(fn);
|
|
8094
|
-
writeLocks.set(
|
|
8427
|
+
writeLocks.set(lockKey, next.then(() => {
|
|
8095
8428
|
}, () => {
|
|
8096
8429
|
}));
|
|
8097
8430
|
return next;
|
|
8098
8431
|
}
|
|
8432
|
+
function wsLock(workspace, fn) {
|
|
8433
|
+
return withLock(`ws:${workspace}`, fn);
|
|
8434
|
+
}
|
|
8099
8435
|
function createTodosRouter(todosDir2, broadcast) {
|
|
8100
8436
|
const router = Router5();
|
|
8101
8437
|
function broadcastUpdate() {
|
|
@@ -8154,7 +8490,7 @@ function createTodosRouter(todosDir2, broadcast) {
|
|
|
8154
8490
|
res.status(400).json({ error: "description is required" });
|
|
8155
8491
|
return;
|
|
8156
8492
|
}
|
|
8157
|
-
const item = await
|
|
8493
|
+
const item = await wsLock(workspace, async () => {
|
|
8158
8494
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
8159
8495
|
const existingIds = new Set(checklist.items.map((i) => i.id));
|
|
8160
8496
|
const id = generateUniqueId(existingIds);
|
|
@@ -8183,7 +8519,7 @@ function createTodosRouter(todosDir2, broadcast) {
|
|
|
8183
8519
|
res.status(400).json({ error: "ids must be an array of strings" });
|
|
8184
8520
|
return;
|
|
8185
8521
|
}
|
|
8186
|
-
const items = await
|
|
8522
|
+
const items = await wsLock(workspace, async () => {
|
|
8187
8523
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
8188
8524
|
const itemMap = new Map(checklist.items.map((i) => [i.id, i]));
|
|
8189
8525
|
const reordered = [];
|
|
@@ -8218,8 +8554,8 @@ function createTodosRouter(todosDir2, broadcast) {
|
|
|
8218
8554
|
router.post("/:workspace/archive", async (req, res) => {
|
|
8219
8555
|
try {
|
|
8220
8556
|
const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
|
|
8221
|
-
const { resolve:
|
|
8222
|
-
const { readFile:
|
|
8557
|
+
const { resolve: resolve46 } = await import("path");
|
|
8558
|
+
const { readFile: readFile30 } = await import("fs/promises");
|
|
8223
8559
|
const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
8224
8560
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
8225
8561
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
@@ -8235,10 +8571,10 @@ function createTodosRouter(todosDir2, broadcast) {
|
|
|
8235
8571
|
(e) => e.itemIds.every((id) => completedIds.has(id))
|
|
8236
8572
|
);
|
|
8237
8573
|
const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
|
|
8238
|
-
await ensureDir(
|
|
8574
|
+
await ensureDir(resolve46(todosDir2, "archive"));
|
|
8239
8575
|
let archContent = "";
|
|
8240
8576
|
if (await fileExists(archFile)) {
|
|
8241
|
-
archContent = await
|
|
8577
|
+
archContent = await readFile30(archFile, "utf-8");
|
|
8242
8578
|
archContent = archContent.trimEnd() + "\n\n";
|
|
8243
8579
|
} else {
|
|
8244
8580
|
archContent = `---
|
|
@@ -8307,7 +8643,7 @@ workspace: ${workspace}
|
|
|
8307
8643
|
router.patch("/:workspace/:id", async (req, res) => {
|
|
8308
8644
|
try {
|
|
8309
8645
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
8310
|
-
const result = await
|
|
8646
|
+
const result = await wsLock(workspace, async () => {
|
|
8311
8647
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
8312
8648
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
8313
8649
|
if (!item) return null;
|
|
@@ -8329,7 +8665,7 @@ workspace: ${workspace}
|
|
|
8329
8665
|
router.delete("/:workspace/:id", async (req, res) => {
|
|
8330
8666
|
try {
|
|
8331
8667
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
8332
|
-
const deleted = await
|
|
8668
|
+
const deleted = await wsLock(workspace, async () => {
|
|
8333
8669
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
8334
8670
|
const idx = checklist.items.findIndex((i) => i.id === req.params.id);
|
|
8335
8671
|
if (idx === -1) return false;
|
|
@@ -8350,7 +8686,7 @@ workspace: ${workspace}
|
|
|
8350
8686
|
router.post("/:workspace/:id/start", async (req, res) => {
|
|
8351
8687
|
try {
|
|
8352
8688
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
8353
|
-
const result = await
|
|
8689
|
+
const result = await wsLock(workspace, async () => {
|
|
8354
8690
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
8355
8691
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
8356
8692
|
if (!item) return { error: "not_found" };
|
|
@@ -8377,7 +8713,7 @@ workspace: ${workspace}
|
|
|
8377
8713
|
router.post("/:workspace/:id/complete", async (req, res) => {
|
|
8378
8714
|
try {
|
|
8379
8715
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
8380
|
-
const result = await
|
|
8716
|
+
const result = await wsLock(workspace, async () => {
|
|
8381
8717
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
8382
8718
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
8383
8719
|
if (!item) return null;
|
|
@@ -8411,7 +8747,7 @@ workspace: ${workspace}
|
|
|
8411
8747
|
try {
|
|
8412
8748
|
const reason = req.body.reason || null;
|
|
8413
8749
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
8414
|
-
const result = await
|
|
8750
|
+
const result = await wsLock(workspace, async () => {
|
|
8415
8751
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
8416
8752
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
8417
8753
|
if (!item) return null;
|
|
@@ -8444,7 +8780,7 @@ workspace: ${workspace}
|
|
|
8444
8780
|
router.post("/:workspace/:id/reopen", async (req, res) => {
|
|
8445
8781
|
try {
|
|
8446
8782
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
8447
|
-
const result = await
|
|
8783
|
+
const result = await wsLock(workspace, async () => {
|
|
8448
8784
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
8449
8785
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
8450
8786
|
if (!item) return null;
|
|
@@ -8466,7 +8802,7 @@ workspace: ${workspace}
|
|
|
8466
8802
|
router.post("/:workspace/:id/unblock", async (req, res) => {
|
|
8467
8803
|
try {
|
|
8468
8804
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
8469
|
-
const result = await
|
|
8805
|
+
const result = await wsLock(workspace, async () => {
|
|
8470
8806
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
8471
8807
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
8472
8808
|
if (!item) return null;
|
|
@@ -8488,21 +8824,618 @@ workspace: ${workspace}
|
|
|
8488
8824
|
return router;
|
|
8489
8825
|
}
|
|
8490
8826
|
|
|
8491
|
-
// src/dashboard/api-
|
|
8492
|
-
|
|
8493
|
-
import { Router as Router6 } from "express";
|
|
8494
|
-
|
|
8495
|
-
// src/utils/github-backup.ts
|
|
8496
|
-
init_paths();
|
|
8827
|
+
// src/dashboard/api-project-todos.ts
|
|
8828
|
+
init_parser2();
|
|
8497
8829
|
init_fs();
|
|
8498
|
-
|
|
8499
|
-
import {
|
|
8500
|
-
import {
|
|
8501
|
-
import {
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8830
|
+
init_paths();
|
|
8831
|
+
import { Router as Router6 } from "express";
|
|
8832
|
+
import { mkdir as mkdir2, readFile as readFile14 } from "fs/promises";
|
|
8833
|
+
import { resolve as resolve19 } from "path";
|
|
8834
|
+
var writeLocks2 = /* @__PURE__ */ new Map();
|
|
8835
|
+
function projLock(slug, fn) {
|
|
8836
|
+
const key = `proj:${slug}`;
|
|
8837
|
+
const prev = writeLocks2.get(key) ?? Promise.resolve();
|
|
8838
|
+
const next = prev.then(fn);
|
|
8839
|
+
writeLocks2.set(key, next.then(() => {
|
|
8840
|
+
}, () => {
|
|
8841
|
+
}));
|
|
8842
|
+
return next;
|
|
8843
|
+
}
|
|
8844
|
+
function getProjectIdParam(value) {
|
|
8845
|
+
if (Array.isArray(value)) return value[0] ?? "";
|
|
8846
|
+
return value ?? "";
|
|
8847
|
+
}
|
|
8848
|
+
function params(req) {
|
|
8849
|
+
return req.params;
|
|
8850
|
+
}
|
|
8851
|
+
async function projectExists(projectsDir2, slug) {
|
|
8852
|
+
return fileExists(resolve19(projectsDir2, slug, "project.md"));
|
|
8853
|
+
}
|
|
8854
|
+
async function ensureProjectTodosDir(projectsDir2, slug) {
|
|
8855
|
+
const todosDir2 = projectTodosDir(projectsDir2, slug);
|
|
8856
|
+
try {
|
|
8857
|
+
await mkdir2(todosDir2, { recursive: false });
|
|
8858
|
+
} catch (err2) {
|
|
8859
|
+
const code = err2.code;
|
|
8860
|
+
if (code === "EEXIST") return;
|
|
8861
|
+
if (code === "ENOENT") {
|
|
8862
|
+
const e = new Error("PROJECT_GONE");
|
|
8863
|
+
e.code = "PROJECT_GONE";
|
|
8864
|
+
throw e;
|
|
8865
|
+
}
|
|
8866
|
+
throw err2;
|
|
8867
|
+
}
|
|
8868
|
+
try {
|
|
8869
|
+
await mkdir2(resolve19(todosDir2, "archive"), { recursive: false });
|
|
8870
|
+
} catch (err2) {
|
|
8871
|
+
const code = err2.code;
|
|
8872
|
+
if (code === "EEXIST") return;
|
|
8873
|
+
if (code === "ENOENT") {
|
|
8874
|
+
const e = new Error("PROJECT_GONE");
|
|
8875
|
+
e.code = "PROJECT_GONE";
|
|
8876
|
+
throw e;
|
|
8877
|
+
}
|
|
8878
|
+
throw err2;
|
|
8879
|
+
}
|
|
8880
|
+
}
|
|
8881
|
+
function notFound(res, slug) {
|
|
8882
|
+
res.status(404).json({ error: `Project "${slug}" not found` });
|
|
8883
|
+
}
|
|
8884
|
+
function createProjectTodosRouter(projectsDir2, broadcast) {
|
|
8885
|
+
const router = Router6({ mergeParams: true });
|
|
8886
|
+
function broadcastUpdate(projectSlug) {
|
|
8887
|
+
broadcast({ type: "todos-updated", projectSlug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
8888
|
+
}
|
|
8889
|
+
function validateProjectId(req, res, next) {
|
|
8890
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
8891
|
+
if (!slug || !isValidSlug(slug)) {
|
|
8892
|
+
res.status(400).json({ error: `Invalid project slug: "${slug}"` });
|
|
8893
|
+
return;
|
|
8894
|
+
}
|
|
8895
|
+
next();
|
|
8896
|
+
}
|
|
8897
|
+
router.use(validateProjectId);
|
|
8898
|
+
router.get("/", async (req, res) => {
|
|
8899
|
+
try {
|
|
8900
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
8901
|
+
if (!await projectExists(projectsDir2, slug)) {
|
|
8902
|
+
notFound(res, slug);
|
|
8903
|
+
return;
|
|
8904
|
+
}
|
|
8905
|
+
const todosDir2 = projectTodosDir(projectsDir2, slug);
|
|
8906
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
8907
|
+
res.json({
|
|
8908
|
+
workspace: checklist.workspace,
|
|
8909
|
+
archiveInterval: checklist.archiveInterval,
|
|
8910
|
+
items: checklist.items,
|
|
8911
|
+
counts: computeCounts(checklist.items)
|
|
8912
|
+
});
|
|
8913
|
+
} catch (error) {
|
|
8914
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get todos" });
|
|
8915
|
+
}
|
|
8916
|
+
});
|
|
8917
|
+
router.post("/", async (req, res) => {
|
|
8918
|
+
try {
|
|
8919
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
8920
|
+
const { description, tags } = req.body;
|
|
8921
|
+
if (!description || typeof description !== "string") {
|
|
8922
|
+
res.status(400).json({ error: "description is required" });
|
|
8923
|
+
return;
|
|
8924
|
+
}
|
|
8925
|
+
if (!await projectExists(projectsDir2, slug)) {
|
|
8926
|
+
notFound(res, slug);
|
|
8927
|
+
return;
|
|
8928
|
+
}
|
|
8929
|
+
const item = await projLock(slug, async () => {
|
|
8930
|
+
if (!await projectExists(projectsDir2, slug)) return null;
|
|
8931
|
+
await ensureProjectTodosDir(projectsDir2, slug);
|
|
8932
|
+
const todosDir2 = projectTodosDir(projectsDir2, slug);
|
|
8933
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
8934
|
+
const existingIds = new Set(checklist.items.map((i) => i.id));
|
|
8935
|
+
const id = generateUniqueId(existingIds);
|
|
8936
|
+
const newItem = {
|
|
8937
|
+
id,
|
|
8938
|
+
description,
|
|
8939
|
+
status: "open",
|
|
8940
|
+
tags: Array.isArray(tags) ? tags : [],
|
|
8941
|
+
session: null
|
|
8942
|
+
};
|
|
8943
|
+
checklist.workspace = slug;
|
|
8944
|
+
checklist.items.push(newItem);
|
|
8945
|
+
await writeChecklist(todosDir2, checklist);
|
|
8946
|
+
return newItem;
|
|
8947
|
+
});
|
|
8948
|
+
if (!item) {
|
|
8949
|
+
notFound(res, slug);
|
|
8950
|
+
return;
|
|
8951
|
+
}
|
|
8952
|
+
broadcastUpdate(slug);
|
|
8953
|
+
res.status(201).json(item);
|
|
8954
|
+
} catch (error) {
|
|
8955
|
+
if (error.code === "PROJECT_GONE") {
|
|
8956
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
8957
|
+
return;
|
|
8958
|
+
}
|
|
8959
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to add todo" });
|
|
8960
|
+
}
|
|
8961
|
+
});
|
|
8962
|
+
router.post("/reorder", async (req, res) => {
|
|
8963
|
+
try {
|
|
8964
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
8965
|
+
const { ids } = req.body;
|
|
8966
|
+
if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string")) {
|
|
8967
|
+
res.status(400).json({ error: "ids must be an array of strings" });
|
|
8968
|
+
return;
|
|
8969
|
+
}
|
|
8970
|
+
if (!await projectExists(projectsDir2, slug)) {
|
|
8971
|
+
notFound(res, slug);
|
|
8972
|
+
return;
|
|
8973
|
+
}
|
|
8974
|
+
const items = await projLock(slug, async () => {
|
|
8975
|
+
if (!await projectExists(projectsDir2, slug)) return null;
|
|
8976
|
+
await ensureProjectTodosDir(projectsDir2, slug);
|
|
8977
|
+
const todosDir2 = projectTodosDir(projectsDir2, slug);
|
|
8978
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
8979
|
+
const itemMap = new Map(checklist.items.map((i) => [i.id, i]));
|
|
8980
|
+
const reordered = [];
|
|
8981
|
+
for (const id of ids) {
|
|
8982
|
+
const item = itemMap.get(id);
|
|
8983
|
+
if (item) {
|
|
8984
|
+
reordered.push(item);
|
|
8985
|
+
itemMap.delete(id);
|
|
8986
|
+
}
|
|
8987
|
+
}
|
|
8988
|
+
for (const item of itemMap.values()) reordered.push(item);
|
|
8989
|
+
checklist.workspace = slug;
|
|
8990
|
+
checklist.items = reordered;
|
|
8991
|
+
await writeChecklist(todosDir2, checklist);
|
|
8992
|
+
return reordered;
|
|
8993
|
+
});
|
|
8994
|
+
if (!items) {
|
|
8995
|
+
notFound(res, slug);
|
|
8996
|
+
return;
|
|
8997
|
+
}
|
|
8998
|
+
broadcastUpdate(slug);
|
|
8999
|
+
res.json({ items });
|
|
9000
|
+
} catch (error) {
|
|
9001
|
+
if (error.code === "PROJECT_GONE") {
|
|
9002
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
9003
|
+
return;
|
|
9004
|
+
}
|
|
9005
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to reorder todos" });
|
|
9006
|
+
}
|
|
9007
|
+
});
|
|
9008
|
+
router.get("/log", async (req, res) => {
|
|
9009
|
+
try {
|
|
9010
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
9011
|
+
if (!await projectExists(projectsDir2, slug)) {
|
|
9012
|
+
notFound(res, slug);
|
|
9013
|
+
return;
|
|
9014
|
+
}
|
|
9015
|
+
const todosDir2 = projectTodosDir(projectsDir2, slug);
|
|
9016
|
+
const log = await readLog(todosDir2, slug);
|
|
9017
|
+
res.json(log);
|
|
9018
|
+
} catch (error) {
|
|
9019
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get log" });
|
|
9020
|
+
}
|
|
9021
|
+
});
|
|
9022
|
+
router.post("/archive", async (req, res) => {
|
|
9023
|
+
try {
|
|
9024
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
9025
|
+
if (!await projectExists(projectsDir2, slug)) {
|
|
9026
|
+
notFound(res, slug);
|
|
9027
|
+
return;
|
|
9028
|
+
}
|
|
9029
|
+
const todosDir2 = projectTodosDir(projectsDir2, slug);
|
|
9030
|
+
await ensureProjectTodosDir(projectsDir2, slug);
|
|
9031
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
9032
|
+
const log = await readLog(todosDir2, slug);
|
|
9033
|
+
const completedIds = new Set(
|
|
9034
|
+
checklist.items.filter((i) => i.status === "completed").map((i) => i.id)
|
|
9035
|
+
);
|
|
9036
|
+
if (completedIds.size === 0) {
|
|
9037
|
+
res.json({ archived: 0, message: "No completed items to archive" });
|
|
9038
|
+
return;
|
|
9039
|
+
}
|
|
9040
|
+
const toArchive = log.entries.filter(
|
|
9041
|
+
(e) => e.itemIds.every((id) => completedIds.has(id))
|
|
9042
|
+
);
|
|
9043
|
+
const archFile = archivePath(todosDir2, slug, checklist.archiveInterval);
|
|
9044
|
+
let archContent = "";
|
|
9045
|
+
if (await fileExists(archFile)) {
|
|
9046
|
+
archContent = await readFile14(archFile, "utf-8");
|
|
9047
|
+
archContent = archContent.trimEnd() + "\n\n";
|
|
9048
|
+
} else {
|
|
9049
|
+
archContent = `---
|
|
9050
|
+
workspace: ${slug}
|
|
9051
|
+
---
|
|
9052
|
+
|
|
9053
|
+
# Archive
|
|
9054
|
+
|
|
9055
|
+
`;
|
|
9056
|
+
}
|
|
9057
|
+
const completedItems = checklist.items.filter((i) => completedIds.has(i.id));
|
|
9058
|
+
for (const item of completedItems) {
|
|
9059
|
+
archContent += `- [x] ${item.description} ${item.tags.map((t) => `#${t}`).join(" ")} [t:${item.id}]
|
|
9060
|
+
`;
|
|
9061
|
+
}
|
|
9062
|
+
archContent += "\n";
|
|
9063
|
+
for (const entry of toArchive) {
|
|
9064
|
+
archContent += `### ${entry.timestamp} \u2014 ${entry.itemIds.map((i) => `t:${i}`).join(", ")}
|
|
9065
|
+
`;
|
|
9066
|
+
if (entry.items) archContent += `**Items:** ${entry.items}
|
|
9067
|
+
`;
|
|
9068
|
+
if (entry.session) archContent += `**Session:** ${entry.session}
|
|
9069
|
+
`;
|
|
9070
|
+
if (entry.branch) archContent += `**Branch:** ${entry.branch}
|
|
9071
|
+
`;
|
|
9072
|
+
if (entry.summary) archContent += `**Summary:** ${entry.summary}
|
|
9073
|
+
`;
|
|
9074
|
+
if (entry.blockers) archContent += `**Blockers:** ${entry.blockers}
|
|
9075
|
+
`;
|
|
9076
|
+
archContent += "\n";
|
|
9077
|
+
}
|
|
9078
|
+
await writeFileForce(archFile, archContent);
|
|
9079
|
+
checklist.workspace = slug;
|
|
9080
|
+
checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
|
|
9081
|
+
await writeChecklist(todosDir2, checklist);
|
|
9082
|
+
broadcastUpdate(slug);
|
|
9083
|
+
res.json({ archived: completedIds.size, logEntries: toArchive.length });
|
|
9084
|
+
} catch (error) {
|
|
9085
|
+
if (error.code === "PROJECT_GONE") {
|
|
9086
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
9087
|
+
return;
|
|
9088
|
+
}
|
|
9089
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to archive" });
|
|
9090
|
+
}
|
|
9091
|
+
});
|
|
9092
|
+
router.get("/log/:id", async (req, res) => {
|
|
9093
|
+
try {
|
|
9094
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
9095
|
+
if (!await projectExists(projectsDir2, slug)) {
|
|
9096
|
+
notFound(res, slug);
|
|
9097
|
+
return;
|
|
9098
|
+
}
|
|
9099
|
+
const todosDir2 = projectTodosDir(projectsDir2, slug);
|
|
9100
|
+
const log = await readLog(todosDir2, slug);
|
|
9101
|
+
const entries = log.entries.filter((e) => e.itemIds.includes(params(req).id ?? ""));
|
|
9102
|
+
res.json({ workspace: log.workspace, entries });
|
|
9103
|
+
} catch (error) {
|
|
9104
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get log" });
|
|
9105
|
+
}
|
|
9106
|
+
});
|
|
9107
|
+
router.get("/:id", async (req, res) => {
|
|
9108
|
+
try {
|
|
9109
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
9110
|
+
if (!await projectExists(projectsDir2, slug)) {
|
|
9111
|
+
notFound(res, slug);
|
|
9112
|
+
return;
|
|
9113
|
+
}
|
|
9114
|
+
const todosDir2 = projectTodosDir(projectsDir2, slug);
|
|
9115
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
9116
|
+
const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
|
|
9117
|
+
if (!item) {
|
|
9118
|
+
res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
|
|
9119
|
+
return;
|
|
9120
|
+
}
|
|
9121
|
+
const log = await readLog(todosDir2, slug);
|
|
9122
|
+
const logEntries = log.entries.filter((e) => e.itemIds.includes(params(req).id ?? ""));
|
|
9123
|
+
res.json({ ...item, log: logEntries });
|
|
9124
|
+
} catch (error) {
|
|
9125
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get todo" });
|
|
9126
|
+
}
|
|
9127
|
+
});
|
|
9128
|
+
router.patch("/:id", async (req, res) => {
|
|
9129
|
+
try {
|
|
9130
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
9131
|
+
if (!await projectExists(projectsDir2, slug)) {
|
|
9132
|
+
notFound(res, slug);
|
|
9133
|
+
return;
|
|
9134
|
+
}
|
|
9135
|
+
const result = await projLock(slug, async () => {
|
|
9136
|
+
if (!await projectExists(projectsDir2, slug)) return "gone";
|
|
9137
|
+
await ensureProjectTodosDir(projectsDir2, slug);
|
|
9138
|
+
const todosDir2 = projectTodosDir(projectsDir2, slug);
|
|
9139
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
9140
|
+
const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
|
|
9141
|
+
if (!item) return null;
|
|
9142
|
+
if (req.body.description !== void 0) item.description = req.body.description;
|
|
9143
|
+
if (Array.isArray(req.body.tags)) item.tags = req.body.tags;
|
|
9144
|
+
checklist.workspace = slug;
|
|
9145
|
+
await writeChecklist(todosDir2, checklist);
|
|
9146
|
+
return { ...item };
|
|
9147
|
+
});
|
|
9148
|
+
if (result === "gone") {
|
|
9149
|
+
notFound(res, slug);
|
|
9150
|
+
return;
|
|
9151
|
+
}
|
|
9152
|
+
if (!result) {
|
|
9153
|
+
res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
|
|
9154
|
+
return;
|
|
9155
|
+
}
|
|
9156
|
+
broadcastUpdate(slug);
|
|
9157
|
+
res.json(result);
|
|
9158
|
+
} catch (error) {
|
|
9159
|
+
if (error.code === "PROJECT_GONE") {
|
|
9160
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
9161
|
+
return;
|
|
9162
|
+
}
|
|
9163
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to update todo" });
|
|
9164
|
+
}
|
|
9165
|
+
});
|
|
9166
|
+
router.delete("/:id", async (req, res) => {
|
|
9167
|
+
try {
|
|
9168
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
9169
|
+
if (!await projectExists(projectsDir2, slug)) {
|
|
9170
|
+
notFound(res, slug);
|
|
9171
|
+
return;
|
|
9172
|
+
}
|
|
9173
|
+
const deleted = await projLock(slug, async () => {
|
|
9174
|
+
if (!await projectExists(projectsDir2, slug)) return "gone";
|
|
9175
|
+
await ensureProjectTodosDir(projectsDir2, slug);
|
|
9176
|
+
const todosDir2 = projectTodosDir(projectsDir2, slug);
|
|
9177
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
9178
|
+
const idx = checklist.items.findIndex((i) => i.id === (params(req).id ?? ""));
|
|
9179
|
+
if (idx === -1) return false;
|
|
9180
|
+
checklist.items.splice(idx, 1);
|
|
9181
|
+
checklist.workspace = slug;
|
|
9182
|
+
await writeChecklist(todosDir2, checklist);
|
|
9183
|
+
return true;
|
|
9184
|
+
});
|
|
9185
|
+
if (deleted === "gone") {
|
|
9186
|
+
notFound(res, slug);
|
|
9187
|
+
return;
|
|
9188
|
+
}
|
|
9189
|
+
if (!deleted) {
|
|
9190
|
+
res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
|
|
9191
|
+
return;
|
|
9192
|
+
}
|
|
9193
|
+
broadcastUpdate(slug);
|
|
9194
|
+
res.json({ deleted: params(req).id ?? "" });
|
|
9195
|
+
} catch (error) {
|
|
9196
|
+
if (error.code === "PROJECT_GONE") {
|
|
9197
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
9198
|
+
return;
|
|
9199
|
+
}
|
|
9200
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to delete todo" });
|
|
9201
|
+
}
|
|
9202
|
+
});
|
|
9203
|
+
router.post("/:id/start", async (req, res) => {
|
|
9204
|
+
try {
|
|
9205
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
9206
|
+
if (!await projectExists(projectsDir2, slug)) {
|
|
9207
|
+
notFound(res, slug);
|
|
9208
|
+
return;
|
|
9209
|
+
}
|
|
9210
|
+
const result = await projLock(slug, async () => {
|
|
9211
|
+
if (!await projectExists(projectsDir2, slug)) return { error: "gone" };
|
|
9212
|
+
await ensureProjectTodosDir(projectsDir2, slug);
|
|
9213
|
+
const todosDir2 = projectTodosDir(projectsDir2, slug);
|
|
9214
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
9215
|
+
const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
|
|
9216
|
+
if (!item) return { error: "not_found" };
|
|
9217
|
+
if (item.status === "in_progress") return { error: "conflict", session: item.session };
|
|
9218
|
+
item.status = "in_progress";
|
|
9219
|
+
item.session = req.body.session || null;
|
|
9220
|
+
checklist.workspace = slug;
|
|
9221
|
+
await writeChecklist(todosDir2, checklist);
|
|
9222
|
+
return { item: { ...item } };
|
|
9223
|
+
});
|
|
9224
|
+
if ("error" in result) {
|
|
9225
|
+
if (result.error === "gone") {
|
|
9226
|
+
notFound(res, slug);
|
|
9227
|
+
return;
|
|
9228
|
+
}
|
|
9229
|
+
if (result.error === "not_found") {
|
|
9230
|
+
res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
|
|
9231
|
+
return;
|
|
9232
|
+
}
|
|
9233
|
+
res.status(409).json({ error: `Todo is already in progress (session: ${result.session})` });
|
|
9234
|
+
return;
|
|
9235
|
+
}
|
|
9236
|
+
broadcastUpdate(slug);
|
|
9237
|
+
res.json(result.item);
|
|
9238
|
+
} catch (error) {
|
|
9239
|
+
if (error.code === "PROJECT_GONE") {
|
|
9240
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
9241
|
+
return;
|
|
9242
|
+
}
|
|
9243
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to start todo" });
|
|
9244
|
+
}
|
|
9245
|
+
});
|
|
9246
|
+
router.post("/:id/complete", async (req, res) => {
|
|
9247
|
+
try {
|
|
9248
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
9249
|
+
if (!await projectExists(projectsDir2, slug)) {
|
|
9250
|
+
notFound(res, slug);
|
|
9251
|
+
return;
|
|
9252
|
+
}
|
|
9253
|
+
const result = await projLock(slug, async () => {
|
|
9254
|
+
if (!await projectExists(projectsDir2, slug)) return "gone";
|
|
9255
|
+
await ensureProjectTodosDir(projectsDir2, slug);
|
|
9256
|
+
const todosDir2 = projectTodosDir(projectsDir2, slug);
|
|
9257
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
9258
|
+
const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
|
|
9259
|
+
if (!item) return null;
|
|
9260
|
+
item.status = "completed";
|
|
9261
|
+
item.session = null;
|
|
9262
|
+
checklist.workspace = slug;
|
|
9263
|
+
await writeChecklist(todosDir2, checklist);
|
|
9264
|
+
const entry = {
|
|
9265
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9266
|
+
itemIds: [item.id],
|
|
9267
|
+
items: item.description,
|
|
9268
|
+
session: req.body.session || null,
|
|
9269
|
+
branch: req.body.branch || null,
|
|
9270
|
+
summary: req.body.summary || "Completed.",
|
|
9271
|
+
blockers: null,
|
|
9272
|
+
status: null
|
|
9273
|
+
};
|
|
9274
|
+
await appendLogEntry2(todosDir2, slug, entry);
|
|
9275
|
+
return { ...item };
|
|
9276
|
+
});
|
|
9277
|
+
if (result === "gone") {
|
|
9278
|
+
notFound(res, slug);
|
|
9279
|
+
return;
|
|
9280
|
+
}
|
|
9281
|
+
if (!result) {
|
|
9282
|
+
res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
|
|
9283
|
+
return;
|
|
9284
|
+
}
|
|
9285
|
+
broadcastUpdate(slug);
|
|
9286
|
+
res.json(result);
|
|
9287
|
+
} catch (error) {
|
|
9288
|
+
if (error.code === "PROJECT_GONE") {
|
|
9289
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
9290
|
+
return;
|
|
9291
|
+
}
|
|
9292
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to complete todo" });
|
|
9293
|
+
}
|
|
9294
|
+
});
|
|
9295
|
+
router.post("/:id/block", async (req, res) => {
|
|
9296
|
+
try {
|
|
9297
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
9298
|
+
const reason = req.body.reason || null;
|
|
9299
|
+
if (!await projectExists(projectsDir2, slug)) {
|
|
9300
|
+
notFound(res, slug);
|
|
9301
|
+
return;
|
|
9302
|
+
}
|
|
9303
|
+
const result = await projLock(slug, async () => {
|
|
9304
|
+
if (!await projectExists(projectsDir2, slug)) return "gone";
|
|
9305
|
+
await ensureProjectTodosDir(projectsDir2, slug);
|
|
9306
|
+
const todosDir2 = projectTodosDir(projectsDir2, slug);
|
|
9307
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
9308
|
+
const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
|
|
9309
|
+
if (!item) return null;
|
|
9310
|
+
item.status = "blocked";
|
|
9311
|
+
item.session = null;
|
|
9312
|
+
checklist.workspace = slug;
|
|
9313
|
+
await writeChecklist(todosDir2, checklist);
|
|
9314
|
+
const entry = {
|
|
9315
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9316
|
+
itemIds: [item.id],
|
|
9317
|
+
items: item.description,
|
|
9318
|
+
session: req.body.session || null,
|
|
9319
|
+
branch: null,
|
|
9320
|
+
summary: reason || "Blocked.",
|
|
9321
|
+
blockers: reason,
|
|
9322
|
+
status: "blocked"
|
|
9323
|
+
};
|
|
9324
|
+
await appendLogEntry2(todosDir2, slug, entry);
|
|
9325
|
+
return { ...item };
|
|
9326
|
+
});
|
|
9327
|
+
if (result === "gone") {
|
|
9328
|
+
notFound(res, slug);
|
|
9329
|
+
return;
|
|
9330
|
+
}
|
|
9331
|
+
if (!result) {
|
|
9332
|
+
res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
|
|
9333
|
+
return;
|
|
9334
|
+
}
|
|
9335
|
+
broadcastUpdate(slug);
|
|
9336
|
+
res.json(result);
|
|
9337
|
+
} catch (error) {
|
|
9338
|
+
if (error.code === "PROJECT_GONE") {
|
|
9339
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
9340
|
+
return;
|
|
9341
|
+
}
|
|
9342
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to block todo" });
|
|
9343
|
+
}
|
|
9344
|
+
});
|
|
9345
|
+
router.post("/:id/reopen", async (req, res) => {
|
|
9346
|
+
try {
|
|
9347
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
9348
|
+
if (!await projectExists(projectsDir2, slug)) {
|
|
9349
|
+
notFound(res, slug);
|
|
9350
|
+
return;
|
|
9351
|
+
}
|
|
9352
|
+
const result = await projLock(slug, async () => {
|
|
9353
|
+
if (!await projectExists(projectsDir2, slug)) return "gone";
|
|
9354
|
+
await ensureProjectTodosDir(projectsDir2, slug);
|
|
9355
|
+
const todosDir2 = projectTodosDir(projectsDir2, slug);
|
|
9356
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
9357
|
+
const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
|
|
9358
|
+
if (!item) return null;
|
|
9359
|
+
item.status = "open";
|
|
9360
|
+
item.session = null;
|
|
9361
|
+
checklist.workspace = slug;
|
|
9362
|
+
await writeChecklist(todosDir2, checklist);
|
|
9363
|
+
return { ...item };
|
|
9364
|
+
});
|
|
9365
|
+
if (result === "gone") {
|
|
9366
|
+
notFound(res, slug);
|
|
9367
|
+
return;
|
|
9368
|
+
}
|
|
9369
|
+
if (!result) {
|
|
9370
|
+
res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
|
|
9371
|
+
return;
|
|
9372
|
+
}
|
|
9373
|
+
broadcastUpdate(slug);
|
|
9374
|
+
res.json(result);
|
|
9375
|
+
} catch (error) {
|
|
9376
|
+
if (error.code === "PROJECT_GONE") {
|
|
9377
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
9378
|
+
return;
|
|
9379
|
+
}
|
|
9380
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to reopen todo" });
|
|
9381
|
+
}
|
|
9382
|
+
});
|
|
9383
|
+
router.post("/:id/unblock", async (req, res) => {
|
|
9384
|
+
try {
|
|
9385
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
9386
|
+
if (!await projectExists(projectsDir2, slug)) {
|
|
9387
|
+
notFound(res, slug);
|
|
9388
|
+
return;
|
|
9389
|
+
}
|
|
9390
|
+
const result = await projLock(slug, async () => {
|
|
9391
|
+
if (!await projectExists(projectsDir2, slug)) return "gone";
|
|
9392
|
+
await ensureProjectTodosDir(projectsDir2, slug);
|
|
9393
|
+
const todosDir2 = projectTodosDir(projectsDir2, slug);
|
|
9394
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
9395
|
+
const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
|
|
9396
|
+
if (!item) return null;
|
|
9397
|
+
item.status = "open";
|
|
9398
|
+
item.session = null;
|
|
9399
|
+
checklist.workspace = slug;
|
|
9400
|
+
await writeChecklist(todosDir2, checklist);
|
|
9401
|
+
return { ...item };
|
|
9402
|
+
});
|
|
9403
|
+
if (result === "gone") {
|
|
9404
|
+
notFound(res, slug);
|
|
9405
|
+
return;
|
|
9406
|
+
}
|
|
9407
|
+
if (!result) {
|
|
9408
|
+
res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
|
|
9409
|
+
return;
|
|
9410
|
+
}
|
|
9411
|
+
broadcastUpdate(slug);
|
|
9412
|
+
res.json(result);
|
|
9413
|
+
} catch (error) {
|
|
9414
|
+
if (error.code === "PROJECT_GONE") {
|
|
9415
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
9416
|
+
return;
|
|
9417
|
+
}
|
|
9418
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to unblock todo" });
|
|
9419
|
+
}
|
|
9420
|
+
});
|
|
9421
|
+
return router;
|
|
9422
|
+
}
|
|
9423
|
+
|
|
9424
|
+
// src/dashboard/api-backup.ts
|
|
9425
|
+
init_config2();
|
|
9426
|
+
import { Router as Router7 } from "express";
|
|
9427
|
+
|
|
9428
|
+
// src/utils/github-backup.ts
|
|
9429
|
+
init_paths();
|
|
9430
|
+
init_fs();
|
|
9431
|
+
init_config2();
|
|
9432
|
+
import { execFile as execFile2 } from "child_process";
|
|
9433
|
+
import { promisify as promisify2 } from "util";
|
|
9434
|
+
import { cp, mkdtemp, rm as rm2, readFile as readFile15, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename3 } from "fs/promises";
|
|
9435
|
+
import { resolve as resolve20, join as join2 } from "path";
|
|
9436
|
+
import { tmpdir } from "os";
|
|
9437
|
+
var exec2 = promisify2(execFile2);
|
|
9438
|
+
var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
|
|
8506
9439
|
var LOCK_FILE_NAME = ".backup-lock";
|
|
8507
9440
|
function parseCategoriesStrict(cats) {
|
|
8508
9441
|
const unknown = [];
|
|
@@ -8539,7 +9472,7 @@ async function resolveCategoryPath(category) {
|
|
|
8539
9472
|
case "servers":
|
|
8540
9473
|
return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
|
|
8541
9474
|
case "config":
|
|
8542
|
-
return { sourcePath:
|
|
9475
|
+
return { sourcePath: resolve20(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
|
|
8543
9476
|
}
|
|
8544
9477
|
}
|
|
8545
9478
|
async function checkGitInstalled() {
|
|
@@ -8550,7 +9483,7 @@ async function checkGitInstalled() {
|
|
|
8550
9483
|
}
|
|
8551
9484
|
}
|
|
8552
9485
|
async function acquireLock() {
|
|
8553
|
-
const lockPath =
|
|
9486
|
+
const lockPath = resolve20(syntaurRoot(), LOCK_FILE_NAME);
|
|
8554
9487
|
await ensureDir(syntaurRoot());
|
|
8555
9488
|
try {
|
|
8556
9489
|
const handle = await open(lockPath, "wx");
|
|
@@ -8559,7 +9492,7 @@ async function acquireLock() {
|
|
|
8559
9492
|
return lockPath;
|
|
8560
9493
|
} catch (err2) {
|
|
8561
9494
|
if (err2.code === "EEXIST") {
|
|
8562
|
-
const pid = await
|
|
9495
|
+
const pid = await readFile15(lockPath, "utf-8").catch(() => "");
|
|
8563
9496
|
throw new Error(
|
|
8564
9497
|
`Backup operation already in progress (lock file at ${lockPath}, pid ${pid.trim() || "unknown"}). If stale, delete the file and retry.`
|
|
8565
9498
|
);
|
|
@@ -8597,7 +9530,7 @@ async function copyRecursive(src, dest) {
|
|
|
8597
9530
|
await ensureDir(dest);
|
|
8598
9531
|
await cp(src, dest, { recursive: true, force: true });
|
|
8599
9532
|
} else {
|
|
8600
|
-
await ensureDir(
|
|
9533
|
+
await ensureDir(resolve20(dest, ".."));
|
|
8601
9534
|
await cp(src, dest, { force: true });
|
|
8602
9535
|
}
|
|
8603
9536
|
}
|
|
@@ -8606,7 +9539,7 @@ function resolveCategoriesStrict(csv) {
|
|
|
8606
9539
|
return parseCategoriesStrict(parts);
|
|
8607
9540
|
}
|
|
8608
9541
|
async function readSanitizedConfig(configPath) {
|
|
8609
|
-
const content = await
|
|
9542
|
+
const content = await readFile15(configPath, "utf-8");
|
|
8610
9543
|
return content.replace(/^(\s*lastBackup:\s*).*$/m, "$1null").replace(/^(\s*lastRestore:\s*).*$/m, "$1null");
|
|
8611
9544
|
}
|
|
8612
9545
|
async function backupToGithub(overrides) {
|
|
@@ -8645,7 +9578,7 @@ async function backupToGithub(overrides) {
|
|
|
8645
9578
|
}
|
|
8646
9579
|
if (category === "config") {
|
|
8647
9580
|
const sanitized = await readSanitizedConfig(sourcePath);
|
|
8648
|
-
await ensureDir(
|
|
9581
|
+
await ensureDir(resolve20(destPath, ".."));
|
|
8649
9582
|
await writeFile4(destPath, sanitized, "utf-8");
|
|
8650
9583
|
} else {
|
|
8651
9584
|
await copyRecursive(sourcePath, destPath);
|
|
@@ -8699,7 +9632,7 @@ async function backupToGithub(overrides) {
|
|
|
8699
9632
|
}
|
|
8700
9633
|
async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
|
|
8701
9634
|
if (isFile) {
|
|
8702
|
-
await ensureDir(
|
|
9635
|
+
await ensureDir(resolve20(localPath, ".."));
|
|
8703
9636
|
await cp(repoSrcPath, localPath, { force: true });
|
|
8704
9637
|
return;
|
|
8705
9638
|
}
|
|
@@ -8800,7 +9733,7 @@ async function restoreFromGithub(overrides) {
|
|
|
8800
9733
|
}
|
|
8801
9734
|
async function getBackupStatus() {
|
|
8802
9735
|
const config = await readConfig();
|
|
8803
|
-
const lockPath =
|
|
9736
|
+
const lockPath = resolve20(syntaurRoot(), LOCK_FILE_NAME);
|
|
8804
9737
|
const locked = await fileExists(lockPath);
|
|
8805
9738
|
return {
|
|
8806
9739
|
repo: config.backup?.repo ?? null,
|
|
@@ -8813,7 +9746,7 @@ async function getBackupStatus() {
|
|
|
8813
9746
|
|
|
8814
9747
|
// src/dashboard/api-backup.ts
|
|
8815
9748
|
function createBackupRouter() {
|
|
8816
|
-
const router =
|
|
9749
|
+
const router = Router7();
|
|
8817
9750
|
router.get("/", async (_req, res) => {
|
|
8818
9751
|
try {
|
|
8819
9752
|
const status = await getBackupStatus();
|
|
@@ -9137,7 +10070,7 @@ function createDashboardServer(options) {
|
|
|
9137
10070
|
(async () => {
|
|
9138
10071
|
try {
|
|
9139
10072
|
const configResult = await migrateLegacyConfig(
|
|
9140
|
-
|
|
10073
|
+
resolve21(syntaurRoot(), "config.md")
|
|
9141
10074
|
);
|
|
9142
10075
|
const projectResult = await migrateLegacyProjectFiles(projectsDir2);
|
|
9143
10076
|
const summary = summarizeMigration(projectResult, configResult);
|
|
@@ -9244,7 +10177,7 @@ function createDashboardServer(options) {
|
|
|
9244
10177
|
});
|
|
9245
10178
|
app.get("/api/workspaces", async (_req, res) => {
|
|
9246
10179
|
try {
|
|
9247
|
-
const result = await listWorkspaces(projectsDir2);
|
|
10180
|
+
const result = await listWorkspaces(projectsDir2, assignmentsDir2);
|
|
9248
10181
|
res.json(result);
|
|
9249
10182
|
} catch (error) {
|
|
9250
10183
|
console.error("Error listing workspaces:", error);
|
|
@@ -9361,18 +10294,28 @@ function createDashboardServer(options) {
|
|
|
9361
10294
|
app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir2, broadcast, assignmentsDir2));
|
|
9362
10295
|
app.use("/api/playbooks", createPlaybooksRouter(playbooksDir3));
|
|
9363
10296
|
app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
|
|
10297
|
+
app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir2, broadcast));
|
|
9364
10298
|
app.use("/api/backup", createBackupRouter());
|
|
9365
10299
|
if (serveStaticUi && dashboardDistPath) {
|
|
9366
|
-
app.use(express.static(dashboardDistPath));
|
|
9367
|
-
app.get("{*path}", async (
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
}
|
|
10300
|
+
app.use("/assets", express.static(resolve21(dashboardDistPath, "assets")));
|
|
10301
|
+
app.get("{*path}", async (req, res) => {
|
|
10302
|
+
if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
|
|
10303
|
+
res.status(404).json({ error: "Not Found" });
|
|
10304
|
+
return;
|
|
10305
|
+
}
|
|
10306
|
+
const indexPath = resolve21(dashboardDistPath, "index.html");
|
|
10307
|
+
if (!await fileExists(indexPath)) {
|
|
9372
10308
|
res.status(503).send(
|
|
9373
10309
|
'Dashboard not built. Run "npm run build:dashboard" first.'
|
|
9374
10310
|
);
|
|
10311
|
+
return;
|
|
9375
10312
|
}
|
|
10313
|
+
res.sendFile(indexPath, (err2) => {
|
|
10314
|
+
if (err2) {
|
|
10315
|
+
console.error("Error sending dashboard index.html:", err2);
|
|
10316
|
+
if (!res.headersSent) res.status(500).send("Dashboard load error");
|
|
10317
|
+
}
|
|
10318
|
+
});
|
|
9376
10319
|
});
|
|
9377
10320
|
}
|
|
9378
10321
|
let watcherHandle = null;
|
|
@@ -9398,7 +10341,7 @@ function createDashboardServer(options) {
|
|
|
9398
10341
|
}
|
|
9399
10342
|
});
|
|
9400
10343
|
server.listen(port, () => {
|
|
9401
|
-
const portFile =
|
|
10344
|
+
const portFile = resolve21(syntaurRoot(), "dashboard-port");
|
|
9402
10345
|
writeFile5(portFile, String(port), "utf-8").catch(() => {
|
|
9403
10346
|
});
|
|
9404
10347
|
resolvePromise();
|
|
@@ -9415,7 +10358,7 @@ function createDashboardServer(options) {
|
|
|
9415
10358
|
client.terminate();
|
|
9416
10359
|
}
|
|
9417
10360
|
clients.clear();
|
|
9418
|
-
const portFile =
|
|
10361
|
+
const portFile = resolve21(syntaurRoot(), "dashboard-port");
|
|
9419
10362
|
await unlink4(portFile).catch(() => {
|
|
9420
10363
|
});
|
|
9421
10364
|
server.closeAllConnections?.();
|
|
@@ -9495,8 +10438,8 @@ async function dashboardCommand(options) {
|
|
|
9495
10438
|
port = availablePort;
|
|
9496
10439
|
}
|
|
9497
10440
|
const thisFile = fileURLToPath2(import.meta.url);
|
|
9498
|
-
const packageRoot =
|
|
9499
|
-
const dashboardDist =
|
|
10441
|
+
const packageRoot = resolve22(dirname4(thisFile), "..");
|
|
10442
|
+
const dashboardDist = resolve22(packageRoot, "dashboard", "dist");
|
|
9500
10443
|
const server = createDashboardServer({
|
|
9501
10444
|
port,
|
|
9502
10445
|
projectsDir: projectsDir2,
|
|
@@ -9510,8 +10453,8 @@ async function dashboardCommand(options) {
|
|
|
9510
10453
|
await server.start();
|
|
9511
10454
|
let viteProcess = null;
|
|
9512
10455
|
if (mode === "dev") {
|
|
9513
|
-
const dashboardDir =
|
|
9514
|
-
const viteBin =
|
|
10456
|
+
const dashboardDir = resolve22(packageRoot, "dashboard");
|
|
10457
|
+
const viteBin = resolve22(dashboardDir, "node_modules", ".bin", "vite");
|
|
9515
10458
|
if (!await fileExists(viteBin)) {
|
|
9516
10459
|
console.error(
|
|
9517
10460
|
'Vite not found. Run "npm ci --prefix dashboard" first, or use the default bundled dashboard mode.'
|
|
@@ -9583,7 +10526,7 @@ async function dashboardCommand(options) {
|
|
|
9583
10526
|
init_paths();
|
|
9584
10527
|
init_fs();
|
|
9585
10528
|
init_config2();
|
|
9586
|
-
import { resolve as
|
|
10529
|
+
import { resolve as resolve23 } from "path";
|
|
9587
10530
|
init_lifecycle();
|
|
9588
10531
|
init_assignment_resolver();
|
|
9589
10532
|
async function runTransition(assignment, command, options = {}) {
|
|
@@ -9596,8 +10539,8 @@ async function runTransition(assignment, command, options = {}) {
|
|
|
9596
10539
|
if (!isValidSlug(assignment)) {
|
|
9597
10540
|
throw new Error(`Invalid assignment slug "${assignment}".`);
|
|
9598
10541
|
}
|
|
9599
|
-
const projectDir =
|
|
9600
|
-
const projectMdPath =
|
|
10542
|
+
const projectDir = resolve23(baseDir, options.project);
|
|
10543
|
+
const projectMdPath = resolve23(projectDir, "project.md");
|
|
9601
10544
|
if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
|
|
9602
10545
|
throw new Error(`Project "${options.project}" not found at ${projectDir}.`);
|
|
9603
10546
|
}
|
|
@@ -9628,8 +10571,8 @@ async function runAssign(assignment, agent, options = {}) {
|
|
|
9628
10571
|
if (!isValidSlug(assignment)) {
|
|
9629
10572
|
throw new Error(`Invalid assignment slug "${assignment}".`);
|
|
9630
10573
|
}
|
|
9631
|
-
const projectDir =
|
|
9632
|
-
const projectMdPath =
|
|
10574
|
+
const projectDir = resolve23(baseDir, options.project);
|
|
10575
|
+
const projectMdPath = resolve23(projectDir, "project.md");
|
|
9633
10576
|
if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
|
|
9634
10577
|
throw new Error(`Project "${options.project}" not found at ${projectDir}.`);
|
|
9635
10578
|
}
|
|
@@ -9715,7 +10658,7 @@ import {
|
|
|
9715
10658
|
readdir as readdir10,
|
|
9716
10659
|
symlink,
|
|
9717
10660
|
lstat,
|
|
9718
|
-
readFile as
|
|
10661
|
+
readFile as readFile16,
|
|
9719
10662
|
readlink,
|
|
9720
10663
|
rm as rm3,
|
|
9721
10664
|
unlink as unlink5,
|
|
@@ -9723,20 +10666,20 @@ import {
|
|
|
9723
10666
|
} from "fs/promises";
|
|
9724
10667
|
import { existsSync } from "fs";
|
|
9725
10668
|
import { homedir as homedir2 } from "os";
|
|
9726
|
-
import { basename, dirname as dirname6, isAbsolute as isAbsolute2, relative as relative2, resolve as
|
|
10669
|
+
import { basename, dirname as dirname6, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve25 } from "path";
|
|
9727
10670
|
|
|
9728
10671
|
// src/utils/package-root.ts
|
|
9729
10672
|
init_fs();
|
|
9730
|
-
import { dirname as dirname5, resolve as
|
|
10673
|
+
import { dirname as dirname5, resolve as resolve24 } from "path";
|
|
9731
10674
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
9732
10675
|
async function findPackageRoot(expectedRelativePath) {
|
|
9733
10676
|
let currentDir = dirname5(fileURLToPath3(import.meta.url));
|
|
9734
10677
|
while (true) {
|
|
9735
|
-
const candidate =
|
|
10678
|
+
const candidate = resolve24(currentDir, expectedRelativePath);
|
|
9736
10679
|
if (await fileExists(candidate)) {
|
|
9737
10680
|
return currentDir;
|
|
9738
10681
|
}
|
|
9739
|
-
const parentDir =
|
|
10682
|
+
const parentDir = resolve24(currentDir, "..");
|
|
9740
10683
|
if (parentDir === currentDir) {
|
|
9741
10684
|
throw new Error(
|
|
9742
10685
|
`Could not locate package root containing ${expectedRelativePath}.`
|
|
@@ -9757,25 +10700,25 @@ function getPluginManifestRelativePath(pluginKind) {
|
|
|
9757
10700
|
}
|
|
9758
10701
|
function getDefaultPluginTargetDir(pluginKind) {
|
|
9759
10702
|
const home = homedir2();
|
|
9760
|
-
return pluginKind === "claude" ?
|
|
10703
|
+
return pluginKind === "claude" ? resolve25(home, ".claude", "plugins", "syntaur") : resolve25(home, "plugins", "syntaur");
|
|
9761
10704
|
}
|
|
9762
10705
|
function getDefaultMarketplacePath() {
|
|
9763
|
-
return
|
|
10706
|
+
return resolve25(homedir2(), ".agents", "plugins", "marketplace.json");
|
|
9764
10707
|
}
|
|
9765
10708
|
function getClaudeMarketplacesRoot() {
|
|
9766
|
-
return
|
|
10709
|
+
return resolve25(homedir2(), ".claude", "plugins", "marketplaces");
|
|
9767
10710
|
}
|
|
9768
10711
|
function getClaudeKnownMarketplacesPath() {
|
|
9769
|
-
return
|
|
10712
|
+
return resolve25(homedir2(), ".claude", "plugins", "known_marketplaces.json");
|
|
9770
10713
|
}
|
|
9771
10714
|
function getClaudeInstalledPluginsPath() {
|
|
9772
|
-
return
|
|
10715
|
+
return resolve25(homedir2(), ".claude", "plugins", "installed_plugins.json");
|
|
9773
10716
|
}
|
|
9774
10717
|
function getInstallMarkerPath(targetDir) {
|
|
9775
|
-
return
|
|
10718
|
+
return resolve25(targetDir, INSTALL_MARKER_FILENAME);
|
|
9776
10719
|
}
|
|
9777
10720
|
async function readPackageManifest(packageRoot) {
|
|
9778
|
-
const raw = await
|
|
10721
|
+
const raw = await readFile16(resolve25(packageRoot, "package.json"), "utf-8");
|
|
9779
10722
|
return JSON.parse(raw);
|
|
9780
10723
|
}
|
|
9781
10724
|
async function readJsonFileIfExists(pathValue) {
|
|
@@ -9783,7 +10726,7 @@ async function readJsonFileIfExists(pathValue) {
|
|
|
9783
10726
|
return null;
|
|
9784
10727
|
}
|
|
9785
10728
|
try {
|
|
9786
|
-
const raw = await
|
|
10729
|
+
const raw = await readFile16(pathValue, "utf-8");
|
|
9787
10730
|
return JSON.parse(raw);
|
|
9788
10731
|
} catch {
|
|
9789
10732
|
return null;
|
|
@@ -9791,15 +10734,15 @@ async function readJsonFileIfExists(pathValue) {
|
|
|
9791
10734
|
}
|
|
9792
10735
|
async function readClaudePluginManifest(pluginDir) {
|
|
9793
10736
|
return await readJsonFileIfExists(
|
|
9794
|
-
|
|
10737
|
+
resolve25(pluginDir, ".claude-plugin", "plugin.json")
|
|
9795
10738
|
) ?? {};
|
|
9796
10739
|
}
|
|
9797
10740
|
async function readPluginManifestName(targetDir, pluginKind) {
|
|
9798
|
-
const manifestPath =
|
|
10741
|
+
const manifestPath = resolve25(targetDir, getPluginManifestRelativePath(pluginKind));
|
|
9799
10742
|
if (!await fileExists(manifestPath)) {
|
|
9800
10743
|
return void 0;
|
|
9801
10744
|
}
|
|
9802
|
-
const raw = await
|
|
10745
|
+
const raw = await readFile16(manifestPath, "utf-8");
|
|
9803
10746
|
const parsed = JSON.parse(raw);
|
|
9804
10747
|
return parsed.name;
|
|
9805
10748
|
}
|
|
@@ -9809,7 +10752,7 @@ async function readInstallMetadata(targetDir) {
|
|
|
9809
10752
|
return null;
|
|
9810
10753
|
}
|
|
9811
10754
|
try {
|
|
9812
|
-
const raw = await
|
|
10755
|
+
const raw = await readFile16(markerPath, "utf-8");
|
|
9813
10756
|
return JSON.parse(raw);
|
|
9814
10757
|
} catch {
|
|
9815
10758
|
return null;
|
|
@@ -9822,7 +10765,7 @@ async function getInstallStatus(targetDir, pluginKind) {
|
|
|
9822
10765
|
const info = await lstat(targetDir);
|
|
9823
10766
|
if (info.isSymbolicLink()) {
|
|
9824
10767
|
const symlinkTarget = await readlink(targetDir);
|
|
9825
|
-
const resolvedTarget =
|
|
10768
|
+
const resolvedTarget = resolve25(dirname6(targetDir), symlinkTarget);
|
|
9826
10769
|
const manifestName2 = await readPluginManifestName(resolvedTarget, pluginKind);
|
|
9827
10770
|
return {
|
|
9828
10771
|
exists: true,
|
|
@@ -9868,7 +10811,7 @@ async function installLink(paths) {
|
|
|
9868
10811
|
await ensureDir(dirname6(paths.targetDir));
|
|
9869
10812
|
await rm3(paths.targetDir, { recursive: true, force: true });
|
|
9870
10813
|
await ensureDir(dirname6(paths.targetDir));
|
|
9871
|
-
await symlink(
|
|
10814
|
+
await symlink(resolve25(paths.sourceDir), paths.targetDir, "dir");
|
|
9872
10815
|
}
|
|
9873
10816
|
async function removeInstallMarker(targetDir) {
|
|
9874
10817
|
const markerPath = getInstallMarkerPath(targetDir);
|
|
@@ -9882,13 +10825,13 @@ function normalizeAbsoluteInstallPath(pathValue, label) {
|
|
|
9882
10825
|
if (!isAbsolute2(expanded)) {
|
|
9883
10826
|
throw new Error(`${label} must be an absolute path.`);
|
|
9884
10827
|
}
|
|
9885
|
-
return
|
|
10828
|
+
return resolve25(expanded);
|
|
9886
10829
|
}
|
|
9887
10830
|
async function resolvePluginPaths(pluginKind, targetDir) {
|
|
9888
10831
|
const packageRoot = await findPackageRoot(getPluginRelativePath(pluginKind));
|
|
9889
10832
|
return {
|
|
9890
10833
|
packageRoot,
|
|
9891
|
-
sourceDir:
|
|
10834
|
+
sourceDir: resolve25(packageRoot, getPluginRelativePath(pluginKind)),
|
|
9892
10835
|
targetDir: targetDir ?? getDefaultPluginTargetDir(pluginKind)
|
|
9893
10836
|
};
|
|
9894
10837
|
}
|
|
@@ -9992,8 +10935,8 @@ async function listClaudeMarketplaceCandidates() {
|
|
|
9992
10935
|
if (!entry.isDirectory()) {
|
|
9993
10936
|
continue;
|
|
9994
10937
|
}
|
|
9995
|
-
const candidateRoot =
|
|
9996
|
-
const manifestPath =
|
|
10938
|
+
const candidateRoot = resolve25(rootDir, entry.name);
|
|
10939
|
+
const manifestPath = resolve25(candidateRoot, ".claude-plugin", "marketplace.json");
|
|
9997
10940
|
if (!await fileExists(manifestPath)) {
|
|
9998
10941
|
continue;
|
|
9999
10942
|
}
|
|
@@ -10020,11 +10963,11 @@ async function listClaudeMarketplaceCandidates() {
|
|
|
10020
10963
|
if (!installLocation) {
|
|
10021
10964
|
continue;
|
|
10022
10965
|
}
|
|
10023
|
-
const candidateRoot =
|
|
10966
|
+
const candidateRoot = resolve25(expandHome(installLocation));
|
|
10024
10967
|
if (seen.has(candidateRoot)) {
|
|
10025
10968
|
continue;
|
|
10026
10969
|
}
|
|
10027
|
-
const manifestPath =
|
|
10970
|
+
const manifestPath = resolve25(candidateRoot, ".claude-plugin", "marketplace.json");
|
|
10028
10971
|
if (!await fileExists(manifestPath)) {
|
|
10029
10972
|
continue;
|
|
10030
10973
|
}
|
|
@@ -10052,7 +10995,7 @@ async function getPreferredClaudeMarketplace() {
|
|
|
10052
10995
|
name: candidate.name,
|
|
10053
10996
|
rootDir: candidate.rootDir,
|
|
10054
10997
|
manifestPath: candidate.manifestPath,
|
|
10055
|
-
targetDir:
|
|
10998
|
+
targetDir: resolve25(candidate.rootDir, "plugins", "syntaur")
|
|
10056
10999
|
};
|
|
10057
11000
|
}
|
|
10058
11001
|
async function registerKnownClaudeMarketplace(name, rootDir) {
|
|
@@ -10079,9 +11022,9 @@ async function ensureClaudeUserMarketplace() {
|
|
|
10079
11022
|
if (existing) {
|
|
10080
11023
|
return existing;
|
|
10081
11024
|
}
|
|
10082
|
-
const rootDir =
|
|
10083
|
-
const manifestPath =
|
|
10084
|
-
await ensureDir(
|
|
11025
|
+
const rootDir = resolve25(getClaudeMarketplacesRoot(), "user-plugins");
|
|
11026
|
+
const manifestPath = resolve25(rootDir, ".claude-plugin", "marketplace.json");
|
|
11027
|
+
await ensureDir(resolve25(rootDir, "plugins"));
|
|
10085
11028
|
if (!await fileExists(manifestPath)) {
|
|
10086
11029
|
const scaffold = {
|
|
10087
11030
|
plugins: []
|
|
@@ -10100,7 +11043,7 @@ async function ensureClaudeUserMarketplace() {
|
|
|
10100
11043
|
name: "user-plugins",
|
|
10101
11044
|
rootDir,
|
|
10102
11045
|
manifestPath,
|
|
10103
|
-
targetDir:
|
|
11046
|
+
targetDir: resolve25(rootDir, "plugins", "syntaur")
|
|
10104
11047
|
};
|
|
10105
11048
|
}
|
|
10106
11049
|
async function detectClaudeMarketplaceForTarget(targetDir) {
|
|
@@ -10110,7 +11053,7 @@ async function detectClaudeMarketplaceForTarget(targetDir) {
|
|
|
10110
11053
|
return null;
|
|
10111
11054
|
}
|
|
10112
11055
|
const rootDir = dirname6(pluginsDir);
|
|
10113
|
-
const manifestPath =
|
|
11056
|
+
const manifestPath = resolve25(rootDir, ".claude-plugin", "marketplace.json");
|
|
10114
11057
|
if (!await fileExists(manifestPath)) {
|
|
10115
11058
|
return null;
|
|
10116
11059
|
}
|
|
@@ -10126,7 +11069,7 @@ async function detectClaudeMarketplaceForTarget(targetDir) {
|
|
|
10126
11069
|
async function findManagedClaudeMarketplacePluginDir() {
|
|
10127
11070
|
const marketplaces = await listClaudeMarketplaceCandidates();
|
|
10128
11071
|
for (const marketplace of marketplaces) {
|
|
10129
|
-
const targetDir =
|
|
11072
|
+
const targetDir = resolve25(marketplace.rootDir, "plugins", "syntaur");
|
|
10130
11073
|
const status = await getInstallStatus(targetDir, "claude");
|
|
10131
11074
|
if (status.exists && status.managed) {
|
|
10132
11075
|
return targetDir;
|
|
@@ -10251,7 +11194,7 @@ async function installManagedPlugin(options) {
|
|
|
10251
11194
|
`${paths.targetDir} already exists and is not a Syntaur-managed install. Remove it manually before installing Syntaur there.`
|
|
10252
11195
|
);
|
|
10253
11196
|
}
|
|
10254
|
-
if (desiredMode === "link" && existing.exists && existing.installMode === "link" && existing.symlinkTarget ===
|
|
11197
|
+
if (desiredMode === "link" && existing.exists && existing.installMode === "link" && existing.symlinkTarget === resolve25(paths.sourceDir) && !force) {
|
|
10255
11198
|
return {
|
|
10256
11199
|
targetDir: paths.targetDir,
|
|
10257
11200
|
sourceDir: paths.sourceDir,
|
|
@@ -10303,7 +11246,7 @@ async function readMarketplaceFile(marketplacePath) {
|
|
|
10303
11246
|
plugins: []
|
|
10304
11247
|
};
|
|
10305
11248
|
}
|
|
10306
|
-
const raw = await
|
|
11249
|
+
const raw = await readFile16(marketplacePath, "utf-8");
|
|
10307
11250
|
const parsed = JSON.parse(raw);
|
|
10308
11251
|
return {
|
|
10309
11252
|
name: parsed.name ?? "local",
|
|
@@ -10464,13 +11407,13 @@ async function recommendMarketplacePath() {
|
|
|
10464
11407
|
return configuredOrManaged ?? getDefaultMarketplacePath();
|
|
10465
11408
|
}
|
|
10466
11409
|
async function isSyntaurDataInstalled() {
|
|
10467
|
-
return fileExists(
|
|
11410
|
+
return fileExists(resolve25(syntaurRoot(), "config.md"));
|
|
10468
11411
|
}
|
|
10469
11412
|
async function removeSyntaurData() {
|
|
10470
11413
|
await rm3(syntaurRoot(), { recursive: true, force: true });
|
|
10471
11414
|
}
|
|
10472
11415
|
async function getConfiguredProjectDir() {
|
|
10473
|
-
if (!await fileExists(
|
|
11416
|
+
if (!await fileExists(resolve25(syntaurRoot(), "config.md"))) {
|
|
10474
11417
|
return null;
|
|
10475
11418
|
}
|
|
10476
11419
|
return (await readConfig()).defaultProjectDir;
|
|
@@ -10539,8 +11482,8 @@ async function textPrompt(question, defaultValue) {
|
|
|
10539
11482
|
|
|
10540
11483
|
// src/utils/install-skills.ts
|
|
10541
11484
|
init_fs();
|
|
10542
|
-
import { readFile as
|
|
10543
|
-
import { dirname as dirname7, resolve as
|
|
11485
|
+
import { readFile as readFile17, readdir as readdir11, mkdir as mkdir3, copyFile, rm as rm4 } from "fs/promises";
|
|
11486
|
+
import { dirname as dirname7, resolve as resolve26, relative as relative3, join as join3 } from "path";
|
|
10544
11487
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
10545
11488
|
import { homedir as homedir3 } from "os";
|
|
10546
11489
|
var REQUIRED_SKILLS = [
|
|
@@ -10553,11 +11496,11 @@ var REQUIRED_SKILLS = [
|
|
|
10553
11496
|
];
|
|
10554
11497
|
function getVendoredSkillsDir() {
|
|
10555
11498
|
const here = dirname7(fileURLToPath4(import.meta.url));
|
|
10556
|
-
return
|
|
11499
|
+
return resolve26(here, "..", "vendor", "syntaur-skills", "skills");
|
|
10557
11500
|
}
|
|
10558
11501
|
function defaultSkillTargetDir(target) {
|
|
10559
|
-
if (target === "claude") return
|
|
10560
|
-
return
|
|
11502
|
+
if (target === "claude") return resolve26(homedir3(), ".claude", "skills");
|
|
11503
|
+
return resolve26(homedir3(), ".codex", "skills");
|
|
10561
11504
|
}
|
|
10562
11505
|
async function walkFiles(root) {
|
|
10563
11506
|
const out = [];
|
|
@@ -10577,7 +11520,7 @@ async function walkFiles(root) {
|
|
|
10577
11520
|
}
|
|
10578
11521
|
async function filesEqual(a, b) {
|
|
10579
11522
|
try {
|
|
10580
|
-
const [ba, bb] = await Promise.all([
|
|
11523
|
+
const [ba, bb] = await Promise.all([readFile17(a), readFile17(b)]);
|
|
10581
11524
|
if (ba.length !== bb.length) return false;
|
|
10582
11525
|
return ba.equals(bb);
|
|
10583
11526
|
} catch {
|
|
@@ -10585,7 +11528,7 @@ async function filesEqual(a, b) {
|
|
|
10585
11528
|
}
|
|
10586
11529
|
}
|
|
10587
11530
|
async function copyDir(srcDir, destDir) {
|
|
10588
|
-
await
|
|
11531
|
+
await mkdir3(destDir, { recursive: true });
|
|
10589
11532
|
const entries = await readdir11(srcDir, { withFileTypes: true });
|
|
10590
11533
|
for (const entry of entries) {
|
|
10591
11534
|
const src = join3(srcDir, entry.name);
|
|
@@ -10619,7 +11562,7 @@ async function installSkills(options) {
|
|
|
10619
11562
|
);
|
|
10620
11563
|
}
|
|
10621
11564
|
const results = [];
|
|
10622
|
-
await
|
|
11565
|
+
await mkdir3(targetRoot, { recursive: true });
|
|
10623
11566
|
for (const skill of REQUIRED_SKILLS) {
|
|
10624
11567
|
const srcDir = join3(source, skill);
|
|
10625
11568
|
const destDir = join3(targetRoot, skill);
|
|
@@ -10668,7 +11611,7 @@ async function uninstallSkills(options) {
|
|
|
10668
11611
|
if (!await fileExists(destDir)) continue;
|
|
10669
11612
|
const skillMd = join3(destDir, "SKILL.md");
|
|
10670
11613
|
if (!await fileExists(skillMd)) continue;
|
|
10671
|
-
const content = await
|
|
11614
|
+
const content = await readFile17(skillMd, "utf-8").catch(() => "");
|
|
10672
11615
|
const match = content.match(/^name:\s*(\S+)\s*$/m);
|
|
10673
11616
|
if (!match || match[1] !== skill) continue;
|
|
10674
11617
|
await rm4(destDir, { recursive: true, force: true });
|
|
@@ -10805,16 +11748,16 @@ async function installPluginCommand(options) {
|
|
|
10805
11748
|
// src/commands/install-statusline.ts
|
|
10806
11749
|
init_paths();
|
|
10807
11750
|
init_fs();
|
|
10808
|
-
import { readFile as
|
|
10809
|
-
import { resolve as
|
|
11751
|
+
import { readFile as readFile19, writeFile as writeFile8, copyFile as copyFile2, rm as rm5, stat as stat3, symlink as symlink2, unlink as unlink6, lstat as lstat2 } from "fs/promises";
|
|
11752
|
+
import { resolve as resolve28, dirname as dirname9 } from "path";
|
|
10810
11753
|
import { homedir as homedir4 } from "os";
|
|
10811
11754
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
10812
11755
|
|
|
10813
11756
|
// src/commands/configure-statusline.ts
|
|
10814
11757
|
init_paths();
|
|
10815
11758
|
init_fs();
|
|
10816
|
-
import { readFile as
|
|
10817
|
-
import { resolve as
|
|
11759
|
+
import { readFile as readFile18, writeFile as writeFile7 } from "fs/promises";
|
|
11760
|
+
import { resolve as resolve27, dirname as dirname8 } from "path";
|
|
10818
11761
|
import { spawnSync } from "child_process";
|
|
10819
11762
|
import { checkbox, input as input2, confirm } from "@inquirer/prompts";
|
|
10820
11763
|
var AVAILABLE_SEGMENTS = [
|
|
@@ -10835,12 +11778,12 @@ var PRESETS = {
|
|
|
10835
11778
|
tracker: { segments: ["git", "assignment", "external", "session"], separator: " \xB7 " }
|
|
10836
11779
|
};
|
|
10837
11780
|
function getConfigPath(installRoot) {
|
|
10838
|
-
return
|
|
11781
|
+
return resolve27(installRoot, "statusline.config.json");
|
|
10839
11782
|
}
|
|
10840
11783
|
async function readConfig2(path) {
|
|
10841
11784
|
if (!await fileExists(path)) return null;
|
|
10842
11785
|
try {
|
|
10843
|
-
const raw = await
|
|
11786
|
+
const raw = await readFile18(path, "utf-8");
|
|
10844
11787
|
const parsed = JSON.parse(raw);
|
|
10845
11788
|
if (!parsed || typeof parsed !== "object") return null;
|
|
10846
11789
|
const segments = Array.isArray(parsed.segments) ? parsed.segments.filter(isSegmentName) : [];
|
|
@@ -10993,7 +11936,7 @@ async function configureStatuslineCommand(options = {}) {
|
|
|
10993
11936
|
console.log(` segments: ${config.segments.join(", ")}`);
|
|
10994
11937
|
console.log(` separator: ${JSON.stringify(config.separator)}`);
|
|
10995
11938
|
if (config.wrap) console.log(` wrap: ${config.wrap}`);
|
|
10996
|
-
const script = options.statuslineScript ??
|
|
11939
|
+
const script = options.statuslineScript ?? resolve27(installRoot, "statusline.sh");
|
|
10997
11940
|
if (await fileExists(script)) {
|
|
10998
11941
|
console.log("");
|
|
10999
11942
|
console.log("Live preview:");
|
|
@@ -11024,11 +11967,11 @@ async function writeDefaultConfigIfMissing(installRoot) {
|
|
|
11024
11967
|
// src/commands/install-statusline.ts
|
|
11025
11968
|
function getPackageStatuslineSource() {
|
|
11026
11969
|
const here = dirname9(fileURLToPath5(import.meta.url));
|
|
11027
|
-
return
|
|
11970
|
+
return resolve28(here, "..", "statusline", "statusline.sh");
|
|
11028
11971
|
}
|
|
11029
11972
|
async function readSettingsJson(settingsPath) {
|
|
11030
11973
|
if (!await fileExists(settingsPath)) return {};
|
|
11031
|
-
const raw = await
|
|
11974
|
+
const raw = await readFile19(settingsPath, "utf-8");
|
|
11032
11975
|
if (raw.trim() === "") return {};
|
|
11033
11976
|
try {
|
|
11034
11977
|
const parsed = JSON.parse(raw);
|
|
@@ -11108,12 +12051,12 @@ async function installScript(sourceScript, destScript, link) {
|
|
|
11108
12051
|
}
|
|
11109
12052
|
async function installStatuslineCommand(options = {}) {
|
|
11110
12053
|
const mode = options.mode ?? "ask";
|
|
11111
|
-
const settingsPath = options.settingsPath ??
|
|
12054
|
+
const settingsPath = options.settingsPath ?? resolve28(homedir4(), ".claude", "settings.json");
|
|
11112
12055
|
const installRoot = options.installRoot ?? syntaurRoot();
|
|
11113
12056
|
const sourceScript = options.sourceScript ?? getPackageStatuslineSource();
|
|
11114
|
-
const destScript =
|
|
11115
|
-
const confPath =
|
|
11116
|
-
const backupPath =
|
|
12057
|
+
const destScript = resolve28(installRoot, "statusline.sh");
|
|
12058
|
+
const confPath = resolve28(installRoot, "statusline.conf");
|
|
12059
|
+
const backupPath = resolve28(installRoot, "statusline.backup.json");
|
|
11117
12060
|
if (!await fileExists(sourceScript)) {
|
|
11118
12061
|
throw new Error(
|
|
11119
12062
|
`Statusline source script not found at ${sourceScript}. Try re-installing syntaur (npm install -g syntaur) or pass --source-script explicitly.`
|
|
@@ -11149,7 +12092,7 @@ async function installStatuslineCommand(options = {}) {
|
|
|
11149
12092
|
if (parsed) {
|
|
11150
12093
|
wrapTarget = parsed;
|
|
11151
12094
|
} else {
|
|
11152
|
-
const wrapperPath =
|
|
12095
|
+
const wrapperPath = resolve28(installRoot, "statusline-wrapped.sh");
|
|
11153
12096
|
const wrapperBody = `#!/usr/bin/env bash
|
|
11154
12097
|
# Auto-generated by syntaur install-statusline.
|
|
11155
12098
|
# Executes the previously configured statusLine command.
|
|
@@ -11204,19 +12147,19 @@ async function chmodExec(path) {
|
|
|
11204
12147
|
}
|
|
11205
12148
|
}
|
|
11206
12149
|
async function uninstallStatuslineCommand(options = {}) {
|
|
11207
|
-
const settingsPath = options.settingsPath ??
|
|
12150
|
+
const settingsPath = options.settingsPath ?? resolve28(homedir4(), ".claude", "settings.json");
|
|
11208
12151
|
const installRoot = options.installRoot ?? syntaurRoot();
|
|
11209
|
-
const destScript =
|
|
11210
|
-
const confPath =
|
|
11211
|
-
const backupPath =
|
|
11212
|
-
const wrapperPath =
|
|
12152
|
+
const destScript = resolve28(installRoot, "statusline.sh");
|
|
12153
|
+
const confPath = resolve28(installRoot, "statusline.conf");
|
|
12154
|
+
const backupPath = resolve28(installRoot, "statusline.backup.json");
|
|
12155
|
+
const wrapperPath = resolve28(installRoot, "statusline-wrapped.sh");
|
|
11213
12156
|
const settings = await readSettingsJson(settingsPath);
|
|
11214
12157
|
const existing = extractExistingCommand(settings);
|
|
11215
12158
|
const ourCommand = `bash ${destScript}`;
|
|
11216
12159
|
let restored = null;
|
|
11217
12160
|
if (await fileExists(backupPath)) {
|
|
11218
12161
|
try {
|
|
11219
|
-
const raw = await
|
|
12162
|
+
const raw = await readFile19(backupPath, "utf-8");
|
|
11220
12163
|
const parsed = JSON.parse(raw);
|
|
11221
12164
|
const prev = parsed?.previousStatusLine;
|
|
11222
12165
|
if (prev && typeof prev === "object" && typeof prev.command === "string") {
|
|
@@ -11237,7 +12180,7 @@ async function uninstallStatuslineCommand(options = {}) {
|
|
|
11237
12180
|
await writeSettingsJson(settingsPath, settings);
|
|
11238
12181
|
}
|
|
11239
12182
|
if (!options.keepScript) {
|
|
11240
|
-
const configPath =
|
|
12183
|
+
const configPath = resolve28(installRoot, "statusline.config.json");
|
|
11241
12184
|
for (const path of [destScript, confPath, backupPath, wrapperPath, configPath]) {
|
|
11242
12185
|
try {
|
|
11243
12186
|
await rm5(path, { force: true });
|
|
@@ -11492,7 +12435,7 @@ async function setupCommand(options) {
|
|
|
11492
12435
|
}
|
|
11493
12436
|
|
|
11494
12437
|
// src/commands/uninstall.ts
|
|
11495
|
-
import { resolve as
|
|
12438
|
+
import { resolve as resolve29 } from "path";
|
|
11496
12439
|
init_paths();
|
|
11497
12440
|
function expandTargets(options) {
|
|
11498
12441
|
if (options.all) {
|
|
@@ -11572,7 +12515,7 @@ async function uninstallCommand(options) {
|
|
|
11572
12515
|
const configuredProjectDir = await getConfiguredProjectDir();
|
|
11573
12516
|
await removeSyntaurData();
|
|
11574
12517
|
console.log(`Removed ${syntaurRoot()}`);
|
|
11575
|
-
if (configuredProjectDir &&
|
|
12518
|
+
if (configuredProjectDir && resolve29(configuredProjectDir) !== resolve29(syntaurRoot(), "projects")) {
|
|
11576
12519
|
console.warn(
|
|
11577
12520
|
`Warning: config.md pointed to an external project directory (${configuredProjectDir}). That directory was not removed automatically.`
|
|
11578
12521
|
);
|
|
@@ -11587,7 +12530,7 @@ async function uninstallCommand(options) {
|
|
|
11587
12530
|
init_paths();
|
|
11588
12531
|
init_fs();
|
|
11589
12532
|
init_config2();
|
|
11590
|
-
import { resolve as
|
|
12533
|
+
import { resolve as resolve30 } from "path";
|
|
11591
12534
|
var SUPPORTED_FRAMEWORKS = ["cursor", "codex", "opencode"];
|
|
11592
12535
|
async function setupAdapterCommand(framework, options) {
|
|
11593
12536
|
if (!SUPPORTED_FRAMEWORKS.includes(framework)) {
|
|
@@ -11613,19 +12556,19 @@ async function setupAdapterCommand(framework, options) {
|
|
|
11613
12556
|
}
|
|
11614
12557
|
const config = await readConfig();
|
|
11615
12558
|
const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
|
|
11616
|
-
const projectDir =
|
|
11617
|
-
const assignmentDir =
|
|
12559
|
+
const projectDir = resolve30(baseDir, options.project);
|
|
12560
|
+
const assignmentDir = resolve30(
|
|
11618
12561
|
projectDir,
|
|
11619
12562
|
"assignments",
|
|
11620
12563
|
options.assignment
|
|
11621
12564
|
);
|
|
11622
|
-
const projectMdPath =
|
|
12565
|
+
const projectMdPath = resolve30(projectDir, "project.md");
|
|
11623
12566
|
if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
|
|
11624
12567
|
throw new Error(
|
|
11625
12568
|
`Project "${options.project}" not found at ${projectDir}.`
|
|
11626
12569
|
);
|
|
11627
12570
|
}
|
|
11628
|
-
const assignmentMdPath =
|
|
12571
|
+
const assignmentMdPath = resolve30(assignmentDir, "assignment.md");
|
|
11629
12572
|
if (!await fileExists(assignmentDir) || !await fileExists(assignmentMdPath)) {
|
|
11630
12573
|
throw new Error(
|
|
11631
12574
|
`Assignment "${options.assignment}" not found at ${assignmentDir}.`
|
|
@@ -11653,15 +12596,15 @@ async function setupAdapterCommand(framework, options) {
|
|
|
11653
12596
|
}
|
|
11654
12597
|
}
|
|
11655
12598
|
if (framework === "cursor") {
|
|
11656
|
-
const protocolPath =
|
|
11657
|
-
const assignmentPath =
|
|
12599
|
+
const protocolPath = resolve30(cwd, ".cursor", "rules", "syntaur-protocol.mdc");
|
|
12600
|
+
const assignmentPath = resolve30(cwd, ".cursor", "rules", "syntaur-assignment.mdc");
|
|
11658
12601
|
await writeAdapterFile(protocolPath, renderCursorProtocol());
|
|
11659
12602
|
await writeAdapterFile(assignmentPath, renderCursorAssignment(rendererParams));
|
|
11660
12603
|
} else if (framework === "codex" || framework === "opencode") {
|
|
11661
|
-
const agentsPath =
|
|
12604
|
+
const agentsPath = resolve30(cwd, "AGENTS.md");
|
|
11662
12605
|
await writeAdapterFile(agentsPath, renderCodexAgents(rendererParams));
|
|
11663
12606
|
if (framework === "opencode") {
|
|
11664
|
-
const configPath =
|
|
12607
|
+
const configPath = resolve30(cwd, "opencode.json");
|
|
11665
12608
|
await writeAdapterFile(configPath, renderOpenCodeConfig({ projectDir }));
|
|
11666
12609
|
}
|
|
11667
12610
|
}
|
|
@@ -11686,7 +12629,7 @@ async function setupAdapterCommand(framework, options) {
|
|
|
11686
12629
|
init_paths();
|
|
11687
12630
|
init_fs();
|
|
11688
12631
|
init_config2();
|
|
11689
|
-
import { resolve as
|
|
12632
|
+
import { resolve as resolve31 } from "path";
|
|
11690
12633
|
async function trackSessionCommand(options) {
|
|
11691
12634
|
if (!options.agent) {
|
|
11692
12635
|
throw new Error("--agent <name> is required.");
|
|
@@ -11699,7 +12642,7 @@ async function trackSessionCommand(options) {
|
|
|
11699
12642
|
if (options.project) {
|
|
11700
12643
|
const config = await readConfig();
|
|
11701
12644
|
const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
|
|
11702
|
-
const projectDir =
|
|
12645
|
+
const projectDir = resolve31(baseDir, options.project);
|
|
11703
12646
|
if (!await fileExists(projectDir)) {
|
|
11704
12647
|
throw new Error(
|
|
11705
12648
|
`Project "${options.project}" not found at ${projectDir}.`
|
|
@@ -11754,10 +12697,11 @@ async function browseCommand(options) {
|
|
|
11754
12697
|
}
|
|
11755
12698
|
|
|
11756
12699
|
// src/commands/create-playbook.ts
|
|
11757
|
-
import { resolve as
|
|
12700
|
+
import { resolve as resolve33 } from "path";
|
|
11758
12701
|
init_timestamp();
|
|
11759
12702
|
init_paths();
|
|
11760
12703
|
init_fs();
|
|
12704
|
+
init_playbooks();
|
|
11761
12705
|
async function createPlaybookCommand(name, options) {
|
|
11762
12706
|
if (!name.trim()) {
|
|
11763
12707
|
throw new Error("Playbook name cannot be empty.");
|
|
@@ -11770,7 +12714,7 @@ async function createPlaybookCommand(name, options) {
|
|
|
11770
12714
|
}
|
|
11771
12715
|
const dir = playbooksDir();
|
|
11772
12716
|
await ensureDir(dir);
|
|
11773
|
-
const filePath =
|
|
12717
|
+
const filePath = resolve33(dir, `${slug}.md`);
|
|
11774
12718
|
if (await fileExists(filePath)) {
|
|
11775
12719
|
throw new Error(
|
|
11776
12720
|
`Playbook "${slug}" already exists at ${filePath}
|
|
@@ -11791,32 +12735,97 @@ Use --slug to specify a different slug.`
|
|
|
11791
12735
|
init_paths();
|
|
11792
12736
|
init_fs();
|
|
11793
12737
|
init_parser();
|
|
11794
|
-
|
|
11795
|
-
import {
|
|
11796
|
-
|
|
12738
|
+
init_config2();
|
|
12739
|
+
import { readdir as readdir12, readFile as readFile20 } from "fs/promises";
|
|
12740
|
+
import { resolve as resolve34 } from "path";
|
|
12741
|
+
async function listPlaybooksCommand(options = {}) {
|
|
11797
12742
|
const dir = playbooksDir();
|
|
11798
12743
|
if (!await fileExists(dir)) {
|
|
11799
12744
|
console.log('No playbooks directory found. Run "syntaur init" first.');
|
|
11800
12745
|
return;
|
|
11801
12746
|
}
|
|
12747
|
+
const config = await readConfig();
|
|
12748
|
+
const disabledSet = new Set(config.playbooks.disabled);
|
|
11802
12749
|
const entries = await readdir12(dir, { withFileTypes: true });
|
|
11803
|
-
const mdFiles = entries.filter(
|
|
11804
|
-
|
|
11805
|
-
|
|
12750
|
+
const mdFiles = entries.filter(
|
|
12751
|
+
(e) => e.isFile() && e.name.endsWith(".md") && !e.name.startsWith("_") && e.name !== "manifest.md"
|
|
12752
|
+
);
|
|
12753
|
+
const rows = [];
|
|
12754
|
+
for (const entry of mdFiles) {
|
|
12755
|
+
const filePath = resolve34(dir, entry.name);
|
|
12756
|
+
const raw = await readFile20(filePath, "utf-8");
|
|
12757
|
+
const parsed = parsePlaybook(raw);
|
|
12758
|
+
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
12759
|
+
const disabled = disabledSet.has(slug);
|
|
12760
|
+
if (disabled && !options.all) continue;
|
|
12761
|
+
rows.push({
|
|
12762
|
+
slug,
|
|
12763
|
+
name: parsed.name || slug,
|
|
12764
|
+
desc: parsed.description || "",
|
|
12765
|
+
disabled
|
|
12766
|
+
});
|
|
12767
|
+
}
|
|
12768
|
+
if (rows.length === 0) {
|
|
12769
|
+
if (!options.all && disabledSet.size > 0) {
|
|
12770
|
+
console.log(
|
|
12771
|
+
`No enabled playbooks found (${disabledSet.size} disabled). Use --all to include disabled playbooks.`
|
|
12772
|
+
);
|
|
12773
|
+
} else {
|
|
12774
|
+
console.log('No playbooks found. Create one with "syntaur create-playbook <name>".');
|
|
12775
|
+
}
|
|
11806
12776
|
return;
|
|
11807
12777
|
}
|
|
11808
|
-
|
|
12778
|
+
const totalLabel = options.all ? `Found ${rows.length} playbook(s) (${[...disabledSet].length} disabled):` : `Found ${rows.length} enabled playbook(s):`;
|
|
12779
|
+
console.log(`${totalLabel}
|
|
11809
12780
|
`);
|
|
11810
12781
|
console.log(`${"Slug".padEnd(30)} ${"Name".padEnd(30)} Description`);
|
|
11811
12782
|
console.log(`${"\u2500".repeat(30)} ${"\u2500".repeat(30)} ${"\u2500".repeat(40)}`);
|
|
11812
|
-
for (const
|
|
11813
|
-
const
|
|
11814
|
-
const
|
|
11815
|
-
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
|
|
11819
|
-
|
|
12783
|
+
for (const row of rows) {
|
|
12784
|
+
const suffix = row.disabled ? " (disabled)" : "";
|
|
12785
|
+
const desc = `${row.desc}${suffix}`;
|
|
12786
|
+
console.log(`${row.slug.padEnd(30)} ${row.name.padEnd(30)} ${desc}`);
|
|
12787
|
+
}
|
|
12788
|
+
}
|
|
12789
|
+
|
|
12790
|
+
// src/commands/enable-playbook.ts
|
|
12791
|
+
init_paths();
|
|
12792
|
+
init_playbooks();
|
|
12793
|
+
async function enablePlaybookCommand(slug) {
|
|
12794
|
+
if (!slug.trim()) {
|
|
12795
|
+
throw new Error("Playbook slug cannot be empty.");
|
|
12796
|
+
}
|
|
12797
|
+
if (!isValidSlug(slug)) {
|
|
12798
|
+
throw new Error(
|
|
12799
|
+
`Invalid slug "${slug}". Slugs must be lowercase, hyphen-separated, with no special characters.`
|
|
12800
|
+
);
|
|
12801
|
+
}
|
|
12802
|
+
const dir = playbooksDir();
|
|
12803
|
+
const { slug: canonical, changed } = await setPlaybookEnabled(dir, slug, true);
|
|
12804
|
+
if (changed) {
|
|
12805
|
+
console.log(`Playbook "${canonical}" enabled.`);
|
|
12806
|
+
} else {
|
|
12807
|
+
console.log(`Playbook "${canonical}" is already enabled.`);
|
|
12808
|
+
}
|
|
12809
|
+
}
|
|
12810
|
+
|
|
12811
|
+
// src/commands/disable-playbook.ts
|
|
12812
|
+
init_paths();
|
|
12813
|
+
init_playbooks();
|
|
12814
|
+
async function disablePlaybookCommand(slug) {
|
|
12815
|
+
if (!slug.trim()) {
|
|
12816
|
+
throw new Error("Playbook slug cannot be empty.");
|
|
12817
|
+
}
|
|
12818
|
+
if (!isValidSlug(slug)) {
|
|
12819
|
+
throw new Error(
|
|
12820
|
+
`Invalid slug "${slug}". Slugs must be lowercase, hyphen-separated, with no special characters.`
|
|
12821
|
+
);
|
|
12822
|
+
}
|
|
12823
|
+
const dir = playbooksDir();
|
|
12824
|
+
const { slug: canonical, changed } = await setPlaybookEnabled(dir, slug, false);
|
|
12825
|
+
if (changed) {
|
|
12826
|
+
console.log(`Playbook "${canonical}" disabled.`);
|
|
12827
|
+
} else {
|
|
12828
|
+
console.log(`Playbook "${canonical}" is already disabled.`);
|
|
11820
12829
|
}
|
|
11821
12830
|
}
|
|
11822
12831
|
|
|
@@ -11824,28 +12833,44 @@ async function listPlaybooksCommand() {
|
|
|
11824
12833
|
init_paths();
|
|
11825
12834
|
init_parser2();
|
|
11826
12835
|
init_fs();
|
|
12836
|
+
init_config2();
|
|
11827
12837
|
import { Command } from "commander";
|
|
11828
|
-
import { readFile as
|
|
11829
|
-
import { resolve as
|
|
12838
|
+
import { readFile as readFile21 } from "fs/promises";
|
|
12839
|
+
import { resolve as resolve35 } from "path";
|
|
11830
12840
|
var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
|
|
11831
|
-
function
|
|
11832
|
-
|
|
12841
|
+
async function resolveScope(options) {
|
|
12842
|
+
const flagCount = [Boolean(options.project), Boolean(options.workspace), Boolean(options.global)].filter(Boolean).length;
|
|
12843
|
+
if (flagCount > 1) {
|
|
12844
|
+
throw new Error("Use at most one of --project, --workspace, --global.");
|
|
12845
|
+
}
|
|
12846
|
+
if (options.project) {
|
|
12847
|
+
if (!isValidSlug(options.project)) {
|
|
12848
|
+
throw new Error(`Invalid project slug: "${options.project}".`);
|
|
12849
|
+
}
|
|
12850
|
+
const config = await readConfig();
|
|
12851
|
+
const projectMd = resolve35(config.defaultProjectDir, options.project, "project.md");
|
|
12852
|
+
if (!await fileExists(projectMd)) {
|
|
12853
|
+
throw new Error(`Project "${options.project}" not found.`);
|
|
12854
|
+
}
|
|
12855
|
+
return { kind: "project", id: options.project, todosPath: projectTodosDir(config.defaultProjectDir, options.project) };
|
|
12856
|
+
}
|
|
11833
12857
|
if (options.workspace) {
|
|
11834
12858
|
if (!WORKSPACE_REGEX2.test(options.workspace)) {
|
|
11835
12859
|
throw new Error(`Invalid workspace name: "${options.workspace}". Use lowercase letters, numbers, hyphens, and underscores.`);
|
|
11836
12860
|
}
|
|
11837
|
-
return options.workspace;
|
|
12861
|
+
return { kind: "workspace", id: options.workspace, todosPath: todosDir() };
|
|
11838
12862
|
}
|
|
11839
|
-
return "_global";
|
|
12863
|
+
return { kind: "workspace", id: "_global", todosPath: todosDir() };
|
|
11840
12864
|
}
|
|
11841
12865
|
function nowISO() {
|
|
11842
12866
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
11843
12867
|
}
|
|
11844
12868
|
var todoCommand = new Command("todo").description("Manage quick todos");
|
|
11845
|
-
todoCommand.command("add").description("Add a new todo item").argument("<description>", "Todo description").option("--tags <tags>", "Comma-separated tags").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (description, options) => {
|
|
12869
|
+
todoCommand.command("add").description("Add a new todo item").argument("<description>", "Todo description").option("--tags <tags>", "Comma-separated tags").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (description, options) => {
|
|
11846
12870
|
try {
|
|
11847
|
-
const
|
|
11848
|
-
const
|
|
12871
|
+
const scope = await resolveScope(options);
|
|
12872
|
+
const todosPath = scope.todosPath;
|
|
12873
|
+
const workspace = scope.id;
|
|
11849
12874
|
const checklist = await readChecklist(todosPath, workspace);
|
|
11850
12875
|
const existingIds = new Set(checklist.items.map((i) => i.id));
|
|
11851
12876
|
const id = generateUniqueId(existingIds);
|
|
@@ -11859,10 +12884,11 @@ todoCommand.command("add").description("Add a new todo item").argument("<descrip
|
|
|
11859
12884
|
process.exit(1);
|
|
11860
12885
|
}
|
|
11861
12886
|
});
|
|
11862
|
-
todoCommand.command("list").description("List todo items").option("--tag <tag>", "Filter by tag").option("--status <status>", "Filter by status (open|in_progress|completed|blocked)").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (options) => {
|
|
12887
|
+
todoCommand.command("list").description("List todo items").option("--tag <tag>", "Filter by tag").option("--status <status>", "Filter by status (open|in_progress|completed|blocked)").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (options) => {
|
|
11863
12888
|
try {
|
|
11864
|
-
const
|
|
11865
|
-
const
|
|
12889
|
+
const scope = await resolveScope(options);
|
|
12890
|
+
const todosPath = scope.todosPath;
|
|
12891
|
+
const workspace = scope.id;
|
|
11866
12892
|
const checklist = await readChecklist(todosPath, workspace);
|
|
11867
12893
|
let items = checklist.items;
|
|
11868
12894
|
if (options.tag) {
|
|
@@ -11897,10 +12923,11 @@ ${counts.total} items: ${counts.open} open, ${counts.in_progress} active, ${coun
|
|
|
11897
12923
|
function findItem(items, id) {
|
|
11898
12924
|
return items.find((i) => i.id === id);
|
|
11899
12925
|
}
|
|
11900
|
-
todoCommand.command("start").description("Mark a todo as in-progress").argument("<id>", "Todo short ID (e.g. a3f1)").option("--session <session>", "Session ID").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (id, options) => {
|
|
12926
|
+
todoCommand.command("start").description("Mark a todo as in-progress").argument("<id>", "Todo short ID (e.g. a3f1)").option("--session <session>", "Session ID").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, options) => {
|
|
11901
12927
|
try {
|
|
11902
|
-
const
|
|
11903
|
-
const
|
|
12928
|
+
const scope = await resolveScope(options);
|
|
12929
|
+
const todosPath = scope.todosPath;
|
|
12930
|
+
const workspace = scope.id;
|
|
11904
12931
|
const checklist = await readChecklist(todosPath, workspace);
|
|
11905
12932
|
const item = findItem(checklist.items, id);
|
|
11906
12933
|
if (!item) {
|
|
@@ -11920,10 +12947,11 @@ todoCommand.command("start").description("Mark a todo as in-progress").argument(
|
|
|
11920
12947
|
process.exit(1);
|
|
11921
12948
|
}
|
|
11922
12949
|
});
|
|
11923
|
-
todoCommand.command("complete").description("Mark a todo as completed").argument("<id>", "Todo short ID").option("--summary <summary>", "Completion summary").option("--branch <branch>", "Git branch name").option("--session <session>", "Session ID").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (id, options) => {
|
|
12950
|
+
todoCommand.command("complete").description("Mark a todo as completed").argument("<id>", "Todo short ID").option("--summary <summary>", "Completion summary").option("--branch <branch>", "Git branch name").option("--session <session>", "Session ID").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, options) => {
|
|
11924
12951
|
try {
|
|
11925
|
-
const
|
|
11926
|
-
const
|
|
12952
|
+
const scope = await resolveScope(options);
|
|
12953
|
+
const todosPath = scope.todosPath;
|
|
12954
|
+
const workspace = scope.id;
|
|
11927
12955
|
const checklist = await readChecklist(todosPath, workspace);
|
|
11928
12956
|
const item = findItem(checklist.items, id);
|
|
11929
12957
|
if (!item) {
|
|
@@ -11950,10 +12978,11 @@ todoCommand.command("complete").description("Mark a todo as completed").argument
|
|
|
11950
12978
|
process.exit(1);
|
|
11951
12979
|
}
|
|
11952
12980
|
});
|
|
11953
|
-
todoCommand.command("block").description("Mark a todo as blocked").argument("<id>", "Todo short ID").requiredOption("--reason <reason>", "Blocking reason").option("--session <session>", "Session ID").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (id, options) => {
|
|
12981
|
+
todoCommand.command("block").description("Mark a todo as blocked").argument("<id>", "Todo short ID").requiredOption("--reason <reason>", "Blocking reason").option("--session <session>", "Session ID").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, options) => {
|
|
11954
12982
|
try {
|
|
11955
|
-
const
|
|
11956
|
-
const
|
|
12983
|
+
const scope = await resolveScope(options);
|
|
12984
|
+
const todosPath = scope.todosPath;
|
|
12985
|
+
const workspace = scope.id;
|
|
11957
12986
|
const checklist = await readChecklist(todosPath, workspace);
|
|
11958
12987
|
const item = findItem(checklist.items, id);
|
|
11959
12988
|
if (!item) {
|
|
@@ -11980,10 +13009,11 @@ todoCommand.command("block").description("Mark a todo as blocked").argument("<id
|
|
|
11980
13009
|
process.exit(1);
|
|
11981
13010
|
}
|
|
11982
13011
|
});
|
|
11983
|
-
todoCommand.command("unblock").description("Return a blocked todo to open").argument("<id>", "Todo short ID").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (id, options) => {
|
|
13012
|
+
todoCommand.command("unblock").description("Return a blocked todo to open").argument("<id>", "Todo short ID").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, options) => {
|
|
11984
13013
|
try {
|
|
11985
|
-
const
|
|
11986
|
-
const
|
|
13014
|
+
const scope = await resolveScope(options);
|
|
13015
|
+
const todosPath = scope.todosPath;
|
|
13016
|
+
const workspace = scope.id;
|
|
11987
13017
|
const checklist = await readChecklist(todosPath, workspace);
|
|
11988
13018
|
const item = findItem(checklist.items, id);
|
|
11989
13019
|
if (!item) {
|
|
@@ -11999,10 +13029,11 @@ todoCommand.command("unblock").description("Return a blocked todo to open").argu
|
|
|
11999
13029
|
process.exit(1);
|
|
12000
13030
|
}
|
|
12001
13031
|
});
|
|
12002
|
-
todoCommand.command("delete").description("Delete a todo item (no log entry)").argument("<id>", "Todo short ID").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (id, options) => {
|
|
13032
|
+
todoCommand.command("delete").description("Delete a todo item (no log entry)").argument("<id>", "Todo short ID").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, options) => {
|
|
12003
13033
|
try {
|
|
12004
|
-
const
|
|
12005
|
-
const
|
|
13034
|
+
const scope = await resolveScope(options);
|
|
13035
|
+
const todosPath = scope.todosPath;
|
|
13036
|
+
const workspace = scope.id;
|
|
12006
13037
|
const checklist = await readChecklist(todosPath, workspace);
|
|
12007
13038
|
const idx = checklist.items.findIndex((i) => i.id === id);
|
|
12008
13039
|
if (idx === -1) {
|
|
@@ -12018,10 +13049,11 @@ todoCommand.command("delete").description("Delete a todo item (no log entry)").a
|
|
|
12018
13049
|
process.exit(1);
|
|
12019
13050
|
}
|
|
12020
13051
|
});
|
|
12021
|
-
todoCommand.command("edit").description("Update a todo description").argument("<id>", "Todo short ID").argument("<description>", "New description").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (id, description, options) => {
|
|
13052
|
+
todoCommand.command("edit").description("Update a todo description").argument("<id>", "Todo short ID").argument("<description>", "New description").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, description, options) => {
|
|
12022
13053
|
try {
|
|
12023
|
-
const
|
|
12024
|
-
const
|
|
13054
|
+
const scope = await resolveScope(options);
|
|
13055
|
+
const todosPath = scope.todosPath;
|
|
13056
|
+
const workspace = scope.id;
|
|
12025
13057
|
const checklist = await readChecklist(todosPath, workspace);
|
|
12026
13058
|
const item = findItem(checklist.items, id);
|
|
12027
13059
|
if (!item) {
|
|
@@ -12036,10 +13068,11 @@ todoCommand.command("edit").description("Update a todo description").argument("<
|
|
|
12036
13068
|
process.exit(1);
|
|
12037
13069
|
}
|
|
12038
13070
|
});
|
|
12039
|
-
todoCommand.command("tag").description("Modify tags on a todo").argument("<id>", "Todo short ID").option("--add <tags>", "Tags to add (comma-separated)").option("--remove <tags>", "Tags to remove (comma-separated)").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (id, options) => {
|
|
13071
|
+
todoCommand.command("tag").description("Modify tags on a todo").argument("<id>", "Todo short ID").option("--add <tags>", "Tags to add (comma-separated)").option("--remove <tags>", "Tags to remove (comma-separated)").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, options) => {
|
|
12040
13072
|
try {
|
|
12041
|
-
const
|
|
12042
|
-
const
|
|
13073
|
+
const scope = await resolveScope(options);
|
|
13074
|
+
const todosPath = scope.todosPath;
|
|
13075
|
+
const workspace = scope.id;
|
|
12043
13076
|
const checklist = await readChecklist(todosPath, workspace);
|
|
12044
13077
|
const item = findItem(checklist.items, id);
|
|
12045
13078
|
if (!item) {
|
|
@@ -12063,10 +13096,11 @@ todoCommand.command("tag").description("Modify tags on a todo").argument("<id>",
|
|
|
12063
13096
|
process.exit(1);
|
|
12064
13097
|
}
|
|
12065
13098
|
});
|
|
12066
|
-
todoCommand.command("log").description("Show log entries").argument("[id]", "Optional todo short ID to filter").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (id, options) => {
|
|
13099
|
+
todoCommand.command("log").description("Show log entries").argument("[id]", "Optional todo short ID to filter").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, options) => {
|
|
12067
13100
|
try {
|
|
12068
|
-
const
|
|
12069
|
-
const
|
|
13101
|
+
const scope = await resolveScope(options);
|
|
13102
|
+
const todosPath = scope.todosPath;
|
|
13103
|
+
const workspace = scope.id;
|
|
12070
13104
|
const log = await readLog(todosPath, workspace);
|
|
12071
13105
|
let entries = log.entries;
|
|
12072
13106
|
if (id) {
|
|
@@ -12090,10 +13124,11 @@ ${entry.timestamp} \u2014 ${entry.itemIds.map((i) => `t:${i}`).join(", ")}`);
|
|
|
12090
13124
|
process.exit(1);
|
|
12091
13125
|
}
|
|
12092
13126
|
});
|
|
12093
|
-
todoCommand.command("archive").description("Archive completed todos and their log entries").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (options) => {
|
|
13127
|
+
todoCommand.command("archive").description("Archive completed todos and their log entries").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (options) => {
|
|
12094
13128
|
try {
|
|
12095
|
-
const
|
|
12096
|
-
const
|
|
13129
|
+
const scope = await resolveScope(options);
|
|
13130
|
+
const todosPath = scope.todosPath;
|
|
13131
|
+
const workspace = scope.id;
|
|
12097
13132
|
const checklist = await readChecklist(todosPath, workspace);
|
|
12098
13133
|
const log = await readLog(todosPath, workspace);
|
|
12099
13134
|
const completedIds = new Set(
|
|
@@ -12107,10 +13142,10 @@ todoCommand.command("archive").description("Archive completed todos and their lo
|
|
|
12107
13142
|
(e) => e.itemIds.every((id) => completedIds.has(id))
|
|
12108
13143
|
);
|
|
12109
13144
|
const archFile = archivePath(todosPath, workspace, checklist.archiveInterval);
|
|
12110
|
-
await ensureDir(
|
|
13145
|
+
await ensureDir(resolve35(todosPath, "archive"));
|
|
12111
13146
|
let archContent = "";
|
|
12112
13147
|
if (await fileExists(archFile)) {
|
|
12113
|
-
archContent = await
|
|
13148
|
+
archContent = await readFile21(archFile, "utf-8");
|
|
12114
13149
|
archContent = archContent.trimEnd() + "\n\n";
|
|
12115
13150
|
} else {
|
|
12116
13151
|
archContent = `---
|
|
@@ -12185,10 +13220,11 @@ workspace: ${workspace}
|
|
|
12185
13220
|
process.exit(1);
|
|
12186
13221
|
}
|
|
12187
13222
|
});
|
|
12188
|
-
todoCommand.command("promote").description("Promote a todo to a full assignment").argument("<id>", "Todo short ID").requiredOption("--project <slug>", "Target project slug").option("--workspace <slug>", "
|
|
13223
|
+
todoCommand.command("promote").description("Promote a todo to a full assignment").argument("<id>", "Todo short ID").requiredOption("--to-project <slug>", "Target project slug for the new assignment").option("--workspace <slug>", "Source workspace slug").option("--project <slug>", "Source project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, options) => {
|
|
12189
13224
|
try {
|
|
12190
|
-
const
|
|
12191
|
-
const
|
|
13225
|
+
const scope = await resolveScope(options);
|
|
13226
|
+
const todosPath = scope.todosPath;
|
|
13227
|
+
const workspace = scope.id;
|
|
12192
13228
|
const checklist = await readChecklist(todosPath, workspace);
|
|
12193
13229
|
const item = findItem(checklist.items, id);
|
|
12194
13230
|
if (!item) {
|
|
@@ -12204,13 +13240,13 @@ todoCommand.command("promote").description("Promote a todo to a full assignment"
|
|
|
12204
13240
|
items: item.description,
|
|
12205
13241
|
session: null,
|
|
12206
13242
|
branch: null,
|
|
12207
|
-
summary: `Promoted to assignment in project: ${options.
|
|
13243
|
+
summary: `Promoted to assignment in project: ${options.toProject}`,
|
|
12208
13244
|
blockers: null,
|
|
12209
13245
|
status: null
|
|
12210
13246
|
};
|
|
12211
13247
|
await appendLogEntry2(todosPath, workspace, entry);
|
|
12212
|
-
console.log(`Promoted [t:${id}] to assignment in project "${options.
|
|
12213
|
-
console.log(`Run: syntaur create-assignment --project ${options.
|
|
13248
|
+
console.log(`Promoted [t:${id}] to assignment in project "${options.toProject}".`);
|
|
13249
|
+
console.log(`Run: syntaur create-assignment --project ${options.toProject} "${item.description}"`);
|
|
12214
13250
|
} catch (error) {
|
|
12215
13251
|
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
12216
13252
|
process.exit(1);
|
|
@@ -12299,7 +13335,7 @@ import { Command as Command3 } from "commander";
|
|
|
12299
13335
|
|
|
12300
13336
|
// src/utils/doctor/index.ts
|
|
12301
13337
|
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
12302
|
-
import { readFile as
|
|
13338
|
+
import { readFile as readFile25 } from "fs/promises";
|
|
12303
13339
|
import { dirname as dirname11, join as join5 } from "path";
|
|
12304
13340
|
|
|
12305
13341
|
// src/utils/doctor/context.ts
|
|
@@ -12307,11 +13343,11 @@ init_config2();
|
|
|
12307
13343
|
init_paths();
|
|
12308
13344
|
init_fs();
|
|
12309
13345
|
import Database2 from "better-sqlite3";
|
|
12310
|
-
import { resolve as
|
|
13346
|
+
import { resolve as resolve36 } from "path";
|
|
12311
13347
|
async function buildCheckContext(cwd = process.cwd()) {
|
|
12312
13348
|
const config = await readConfig();
|
|
12313
13349
|
const root = syntaurRoot();
|
|
12314
|
-
const dbPath =
|
|
13350
|
+
const dbPath = resolve36(root, "syntaur.db");
|
|
12315
13351
|
let db2 = null;
|
|
12316
13352
|
let dbError = null;
|
|
12317
13353
|
if (await fileExists(dbPath)) {
|
|
@@ -12345,8 +13381,8 @@ function closeCheckContext(ctx) {
|
|
|
12345
13381
|
// src/utils/doctor/checks/env.ts
|
|
12346
13382
|
init_fs();
|
|
12347
13383
|
init_paths();
|
|
12348
|
-
import { resolve as
|
|
12349
|
-
import { readFile as
|
|
13384
|
+
import { resolve as resolve37, isAbsolute as isAbsolute3 } from "path";
|
|
13385
|
+
import { readFile as readFile22, stat as stat4 } from "fs/promises";
|
|
12350
13386
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
12351
13387
|
import { dirname as dirname10, join as join4 } from "path";
|
|
12352
13388
|
var CATEGORY = "env";
|
|
@@ -12386,7 +13422,7 @@ var configValid = {
|
|
|
12386
13422
|
category: CATEGORY,
|
|
12387
13423
|
title: "~/.syntaur/config.md is valid",
|
|
12388
13424
|
async run(ctx) {
|
|
12389
|
-
const configPath =
|
|
13425
|
+
const configPath = resolve37(ctx.syntaurRoot, "config.md");
|
|
12390
13426
|
if (!await fileExists(configPath)) {
|
|
12391
13427
|
return {
|
|
12392
13428
|
id: this.id,
|
|
@@ -12403,7 +13439,7 @@ var configValid = {
|
|
|
12403
13439
|
autoFixable: false
|
|
12404
13440
|
};
|
|
12405
13441
|
}
|
|
12406
|
-
const content = await
|
|
13442
|
+
const content = await readFile22(configPath, "utf-8");
|
|
12407
13443
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
12408
13444
|
if (!fmMatch || fmMatch[1].trim() === "") {
|
|
12409
13445
|
return {
|
|
@@ -12683,7 +13719,7 @@ async function readLocalPkg() {
|
|
|
12683
13719
|
for (let i = 0; i < 6; i++) {
|
|
12684
13720
|
const candidate = join4(dir, "package.json");
|
|
12685
13721
|
try {
|
|
12686
|
-
const text = await
|
|
13722
|
+
const text = await readFile22(candidate, "utf-8");
|
|
12687
13723
|
return JSON.parse(text);
|
|
12688
13724
|
} catch {
|
|
12689
13725
|
dir = dirname10(dir);
|
|
@@ -12735,7 +13771,7 @@ function versionGte(a, b) {
|
|
|
12735
13771
|
|
|
12736
13772
|
// src/utils/doctor/checks/structure.ts
|
|
12737
13773
|
init_fs();
|
|
12738
|
-
import { resolve as
|
|
13774
|
+
import { resolve as resolve38 } from "path";
|
|
12739
13775
|
import { readdir as readdir13, stat as stat5 } from "fs/promises";
|
|
12740
13776
|
var CATEGORY2 = "structure";
|
|
12741
13777
|
var KNOWN_TOP_LEVEL = /* @__PURE__ */ new Set([
|
|
@@ -12755,7 +13791,7 @@ var projectsDir = {
|
|
|
12755
13791
|
category: CATEGORY2,
|
|
12756
13792
|
title: "projects/ directory exists",
|
|
12757
13793
|
async run(ctx) {
|
|
12758
|
-
const p =
|
|
13794
|
+
const p = resolve38(ctx.syntaurRoot, "projects");
|
|
12759
13795
|
if (!await fileExists(p)) {
|
|
12760
13796
|
return {
|
|
12761
13797
|
id: this.id,
|
|
@@ -12780,7 +13816,7 @@ var playbooksDir2 = {
|
|
|
12780
13816
|
category: CATEGORY2,
|
|
12781
13817
|
title: "playbooks/ directory exists",
|
|
12782
13818
|
async run(ctx) {
|
|
12783
|
-
const p =
|
|
13819
|
+
const p = resolve38(ctx.syntaurRoot, "playbooks");
|
|
12784
13820
|
if (!await fileExists(p)) {
|
|
12785
13821
|
return {
|
|
12786
13822
|
id: this.id,
|
|
@@ -12805,7 +13841,7 @@ var todosDirValid = {
|
|
|
12805
13841
|
category: CATEGORY2,
|
|
12806
13842
|
title: "todos/ directory is readable (if present)",
|
|
12807
13843
|
async run(ctx) {
|
|
12808
|
-
const p =
|
|
13844
|
+
const p = resolve38(ctx.syntaurRoot, "todos");
|
|
12809
13845
|
if (!await fileExists(p)) {
|
|
12810
13846
|
return {
|
|
12811
13847
|
id: this.id,
|
|
@@ -12836,7 +13872,7 @@ var serversDirValid = {
|
|
|
12836
13872
|
category: CATEGORY2,
|
|
12837
13873
|
title: "servers/ directory is readable (if present)",
|
|
12838
13874
|
async run(ctx) {
|
|
12839
|
-
const p =
|
|
13875
|
+
const p = resolve38(ctx.syntaurRoot, "servers");
|
|
12840
13876
|
if (!await fileExists(p)) {
|
|
12841
13877
|
return {
|
|
12842
13878
|
id: this.id,
|
|
@@ -12881,7 +13917,7 @@ var knownFilesRecognized = {
|
|
|
12881
13917
|
title: this.title,
|
|
12882
13918
|
status: "warn",
|
|
12883
13919
|
detail: `unexpected top-level entries: ${unexpected.join(", ")}`,
|
|
12884
|
-
affected: unexpected.map((n) =>
|
|
13920
|
+
affected: unexpected.map((n) => resolve38(ctx.syntaurRoot, n)),
|
|
12885
13921
|
remediation: {
|
|
12886
13922
|
kind: "manual",
|
|
12887
13923
|
suggestion: "Review these entries \u2014 they may be leftover state from older versions",
|
|
@@ -12910,7 +13946,7 @@ function pass2(check) {
|
|
|
12910
13946
|
|
|
12911
13947
|
// src/utils/doctor/checks/project.ts
|
|
12912
13948
|
init_fs();
|
|
12913
|
-
import { resolve as
|
|
13949
|
+
import { resolve as resolve39 } from "path";
|
|
12914
13950
|
import { readdir as readdir14, stat as stat6 } from "fs/promises";
|
|
12915
13951
|
var CATEGORY3 = "project";
|
|
12916
13952
|
var REQUIRED_PROJECT_FILES = [
|
|
@@ -12940,10 +13976,10 @@ async function listProjects2(ctx) {
|
|
|
12940
13976
|
for (const e of entries) {
|
|
12941
13977
|
if (!e.isDirectory()) continue;
|
|
12942
13978
|
if (e.name.startsWith(".") || e.name.startsWith("_")) continue;
|
|
12943
|
-
const projectDir =
|
|
13979
|
+
const projectDir = resolve39(dir, e.name);
|
|
12944
13980
|
let looksLikeProject = false;
|
|
12945
13981
|
for (const marker of PROJECT_MARKERS) {
|
|
12946
|
-
if (await fileExists(
|
|
13982
|
+
if (await fileExists(resolve39(projectDir, marker))) {
|
|
12947
13983
|
looksLikeProject = true;
|
|
12948
13984
|
break;
|
|
12949
13985
|
}
|
|
@@ -12962,7 +13998,7 @@ var requiredFiles = {
|
|
|
12962
13998
|
for (const projectDir of projects) {
|
|
12963
13999
|
const missing = [];
|
|
12964
14000
|
for (const rel of REQUIRED_PROJECT_FILES) {
|
|
12965
|
-
const p =
|
|
14001
|
+
const p = resolve39(projectDir, rel);
|
|
12966
14002
|
if (!await fileExists(p)) missing.push(rel);
|
|
12967
14003
|
}
|
|
12968
14004
|
if (missing.length === 0) continue;
|
|
@@ -12972,7 +14008,7 @@ var requiredFiles = {
|
|
|
12972
14008
|
title: this.title,
|
|
12973
14009
|
status: "error",
|
|
12974
14010
|
detail: `project at ${projectDir} is missing: ${missing.join(", ")}`,
|
|
12975
|
-
affected: missing.map((m) =>
|
|
14011
|
+
affected: missing.map((m) => resolve39(projectDir, m)),
|
|
12976
14012
|
remediation: {
|
|
12977
14013
|
kind: "manual",
|
|
12978
14014
|
suggestion: "Recreate the missing scaffold files from templates",
|
|
@@ -12995,7 +14031,7 @@ var manifestStale = {
|
|
|
12995
14031
|
const projects = await listProjects2(ctx);
|
|
12996
14032
|
const results = [];
|
|
12997
14033
|
for (const projectDir of projects) {
|
|
12998
|
-
const manifestPath =
|
|
14034
|
+
const manifestPath = resolve39(projectDir, "manifest.md");
|
|
12999
14035
|
if (!await fileExists(manifestPath)) continue;
|
|
13000
14036
|
const manifestMtime = (await stat6(manifestPath)).mtimeMs;
|
|
13001
14037
|
const newestAssignment = await newestAssignmentMtime(projectDir);
|
|
@@ -13044,7 +14080,7 @@ var orphanFiles = {
|
|
|
13044
14080
|
title: this.title,
|
|
13045
14081
|
status: "warn",
|
|
13046
14082
|
detail: `project at ${projectDir} has unexpected entries: ${orphans.join(", ")}`,
|
|
13047
|
-
affected: orphans.map((o) =>
|
|
14083
|
+
affected: orphans.map((o) => resolve39(projectDir, o)),
|
|
13048
14084
|
autoFixable: false
|
|
13049
14085
|
});
|
|
13050
14086
|
}
|
|
@@ -13054,7 +14090,7 @@ var orphanFiles = {
|
|
|
13054
14090
|
};
|
|
13055
14091
|
var projectChecks = [requiredFiles, manifestStale, orphanFiles];
|
|
13056
14092
|
async function newestAssignmentMtime(projectDir) {
|
|
13057
|
-
const assignmentsRoot =
|
|
14093
|
+
const assignmentsRoot = resolve39(projectDir, "assignments");
|
|
13058
14094
|
if (!await fileExists(assignmentsRoot)) return 0;
|
|
13059
14095
|
let newest = 0;
|
|
13060
14096
|
let entries;
|
|
@@ -13065,7 +14101,7 @@ async function newestAssignmentMtime(projectDir) {
|
|
|
13065
14101
|
}
|
|
13066
14102
|
for (const e of entries) {
|
|
13067
14103
|
if (!e.isDirectory()) continue;
|
|
13068
|
-
const assignmentMd =
|
|
14104
|
+
const assignmentMd = resolve39(assignmentsRoot, e.name, "assignment.md");
|
|
13069
14105
|
try {
|
|
13070
14106
|
const s = await stat6(assignmentMd);
|
|
13071
14107
|
if (s.mtimeMs > newest) newest = s.mtimeMs;
|
|
@@ -13089,8 +14125,8 @@ init_fs();
|
|
|
13089
14125
|
init_parser();
|
|
13090
14126
|
init_types();
|
|
13091
14127
|
init_paths();
|
|
13092
|
-
import { resolve as
|
|
13093
|
-
import { readdir as readdir15, readFile as
|
|
14128
|
+
import { resolve as resolve40 } from "path";
|
|
14129
|
+
import { readdir as readdir15, readFile as readFile23 } from "fs/promises";
|
|
13094
14130
|
var CATEGORY4 = "assignment";
|
|
13095
14131
|
var STATUSES_REQUIRING_HANDOFF = /* @__PURE__ */ new Set(["review", "completed"]);
|
|
13096
14132
|
async function listAssignments(ctx) {
|
|
@@ -13101,16 +14137,16 @@ async function listAssignments(ctx) {
|
|
|
13101
14137
|
for (const m of projects) {
|
|
13102
14138
|
if (!m.isDirectory()) continue;
|
|
13103
14139
|
if (m.name.startsWith(".") || m.name.startsWith("_")) continue;
|
|
13104
|
-
const assignmentsDir2 =
|
|
14140
|
+
const assignmentsDir2 = resolve40(projectsDir2, m.name, "assignments");
|
|
13105
14141
|
if (!await fileExists(assignmentsDir2)) continue;
|
|
13106
14142
|
const entries = await readdir15(assignmentsDir2, { withFileTypes: true });
|
|
13107
14143
|
for (const a of entries) {
|
|
13108
14144
|
if (!a.isDirectory()) continue;
|
|
13109
14145
|
if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
|
|
13110
|
-
const assignmentDir =
|
|
13111
|
-
const assignmentMd =
|
|
14146
|
+
const assignmentDir = resolve40(assignmentsDir2, a.name);
|
|
14147
|
+
const assignmentMd = resolve40(assignmentDir, "assignment.md");
|
|
13112
14148
|
const entry = {
|
|
13113
|
-
projectDir:
|
|
14149
|
+
projectDir: resolve40(projectsDir2, m.name),
|
|
13114
14150
|
projectSlug: m.name,
|
|
13115
14151
|
assignmentDir,
|
|
13116
14152
|
assignmentSlug: a.name,
|
|
@@ -13130,8 +14166,8 @@ async function listAssignments(ctx) {
|
|
|
13130
14166
|
for (const a of entries) {
|
|
13131
14167
|
if (!a.isDirectory()) continue;
|
|
13132
14168
|
if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
|
|
13133
|
-
const assignmentDir =
|
|
13134
|
-
const assignmentMd =
|
|
14169
|
+
const assignmentDir = resolve40(standaloneRoot, a.name);
|
|
14170
|
+
const assignmentMd = resolve40(assignmentDir, "assignment.md");
|
|
13135
14171
|
const entry = {
|
|
13136
14172
|
projectDir: standaloneRoot,
|
|
13137
14173
|
projectSlug: null,
|
|
@@ -13209,7 +14245,7 @@ var invalidStatus = {
|
|
|
13209
14245
|
const allowed = configuredStatuses(ctx);
|
|
13210
14246
|
const results = [];
|
|
13211
14247
|
for (const a of withAssignmentMd) {
|
|
13212
|
-
const path =
|
|
14248
|
+
const path = resolve40(a.assignmentDir, "assignment.md");
|
|
13213
14249
|
const parsed = await parseSafe(path);
|
|
13214
14250
|
if (!parsed) continue;
|
|
13215
14251
|
if (!allowed.has(parsed.status)) {
|
|
@@ -13242,7 +14278,7 @@ var workspaceMissing = {
|
|
|
13242
14278
|
const terminal = terminalStatuses(ctx);
|
|
13243
14279
|
const results = [];
|
|
13244
14280
|
for (const a of withAssignmentMd) {
|
|
13245
|
-
const path =
|
|
14281
|
+
const path = resolve40(a.assignmentDir, "assignment.md");
|
|
13246
14282
|
const parsed = await parseSafe(path);
|
|
13247
14283
|
if (!parsed) continue;
|
|
13248
14284
|
if (terminal.has(parsed.status)) continue;
|
|
@@ -13289,12 +14325,12 @@ var requiredFilesByStatus = {
|
|
|
13289
14325
|
const { withAssignmentMd } = await listAssignments(ctx);
|
|
13290
14326
|
const results = [];
|
|
13291
14327
|
for (const a of withAssignmentMd) {
|
|
13292
|
-
const assignmentPath =
|
|
14328
|
+
const assignmentPath = resolve40(a.assignmentDir, "assignment.md");
|
|
13293
14329
|
const parsed = await parseSafe(assignmentPath);
|
|
13294
14330
|
if (!parsed) continue;
|
|
13295
14331
|
const missing = [];
|
|
13296
14332
|
if (STATUSES_REQUIRING_HANDOFF.has(parsed.status)) {
|
|
13297
|
-
const handoffPath =
|
|
14333
|
+
const handoffPath = resolve40(a.assignmentDir, "handoff.md");
|
|
13298
14334
|
if (!await fileExists(handoffPath)) missing.push("handoff.md");
|
|
13299
14335
|
}
|
|
13300
14336
|
if (missing.length === 0) continue;
|
|
@@ -13304,7 +14340,7 @@ var requiredFilesByStatus = {
|
|
|
13304
14340
|
title: this.title,
|
|
13305
14341
|
status: "warn",
|
|
13306
14342
|
detail: `${a.projectSlug}/${a.assignmentSlug} (status: ${parsed.status}) is missing ${missing.join(", ")}`,
|
|
13307
|
-
affected: missing.map((m) =>
|
|
14343
|
+
affected: missing.map((m) => resolve40(a.assignmentDir, m)),
|
|
13308
14344
|
remediation: {
|
|
13309
14345
|
kind: "manual",
|
|
13310
14346
|
suggestion: `Create the missing ${missing.join(" and ")} files for this assignment`,
|
|
@@ -13327,7 +14363,7 @@ var companionFilesScaffolded = {
|
|
|
13327
14363
|
for (const a of withAssignmentMd) {
|
|
13328
14364
|
const missing = [];
|
|
13329
14365
|
for (const filename of ["progress.md", "comments.md"]) {
|
|
13330
|
-
if (!await fileExists(
|
|
14366
|
+
if (!await fileExists(resolve40(a.assignmentDir, filename))) {
|
|
13331
14367
|
missing.push(filename);
|
|
13332
14368
|
}
|
|
13333
14369
|
}
|
|
@@ -13339,7 +14375,7 @@ var companionFilesScaffolded = {
|
|
|
13339
14375
|
title: this.title,
|
|
13340
14376
|
status: "warn",
|
|
13341
14377
|
detail: `${label} is missing ${missing.join(" and ")} (pre-v2.0 assignment \u2014 not required, but scaffolding them keeps the dashboard and CLIs consistent)`,
|
|
13342
|
-
affected: missing.map((m) =>
|
|
14378
|
+
affected: missing.map((m) => resolve40(a.assignmentDir, m)),
|
|
13343
14379
|
remediation: {
|
|
13344
14380
|
kind: "manual",
|
|
13345
14381
|
suggestion: `Create ${missing.join(" and ")} with the renderProgress/renderComments templates, or re-scaffold via the CLI`,
|
|
@@ -13372,7 +14408,7 @@ var typeDefinition = {
|
|
|
13372
14408
|
const { withAssignmentMd } = await listAssignments(ctx);
|
|
13373
14409
|
const results = [];
|
|
13374
14410
|
for (const a of withAssignmentMd) {
|
|
13375
|
-
const path =
|
|
14411
|
+
const path = resolve40(a.assignmentDir, "assignment.md");
|
|
13376
14412
|
const parsed = await parseSafe(path);
|
|
13377
14413
|
if (!parsed) continue;
|
|
13378
14414
|
if (!parsed.type) continue;
|
|
@@ -13406,7 +14442,7 @@ var projectFrontmatterMatchesContainer = {
|
|
|
13406
14442
|
const { withAssignmentMd } = await listAssignments(ctx);
|
|
13407
14443
|
const results = [];
|
|
13408
14444
|
for (const a of withAssignmentMd) {
|
|
13409
|
-
const path =
|
|
14445
|
+
const path = resolve40(a.assignmentDir, "assignment.md");
|
|
13410
14446
|
const parsed = await parseSafe(path);
|
|
13411
14447
|
if (!parsed) continue;
|
|
13412
14448
|
if (a.standalone) {
|
|
@@ -13461,7 +14497,7 @@ var assignmentChecks = [
|
|
|
13461
14497
|
];
|
|
13462
14498
|
async function parseSafe(path) {
|
|
13463
14499
|
try {
|
|
13464
|
-
const content = await
|
|
14500
|
+
const content = await readFile23(path, "utf-8");
|
|
13465
14501
|
return parseAssignmentFull(content);
|
|
13466
14502
|
} catch {
|
|
13467
14503
|
return null;
|
|
@@ -13480,7 +14516,7 @@ function pass4(check, detail) {
|
|
|
13480
14516
|
|
|
13481
14517
|
// src/utils/doctor/checks/dashboard.ts
|
|
13482
14518
|
init_fs();
|
|
13483
|
-
import { resolve as
|
|
14519
|
+
import { resolve as resolve41 } from "path";
|
|
13484
14520
|
var CATEGORY5 = "dashboard";
|
|
13485
14521
|
var dbReachable = {
|
|
13486
14522
|
id: "dashboard.db-reachable",
|
|
@@ -13494,7 +14530,7 @@ var dbReachable = {
|
|
|
13494
14530
|
title: this.title,
|
|
13495
14531
|
status: "error",
|
|
13496
14532
|
detail: `could not open syntaur.db: ${ctx.dbError ?? "unknown error"}`,
|
|
13497
|
-
affected: [
|
|
14533
|
+
affected: [resolve41(ctx.syntaurRoot, "syntaur.db")],
|
|
13498
14534
|
remediation: {
|
|
13499
14535
|
kind: "manual",
|
|
13500
14536
|
suggestion: "Start the dashboard once (`syntaur dashboard`) to initialize the DB, or restore it from backup",
|
|
@@ -13512,7 +14548,7 @@ var dbReachable = {
|
|
|
13512
14548
|
title: this.title,
|
|
13513
14549
|
status: "error",
|
|
13514
14550
|
detail: 'syntaur.db is missing the expected "sessions" table',
|
|
13515
|
-
affected: [
|
|
14551
|
+
affected: [resolve41(ctx.syntaurRoot, "syntaur.db")],
|
|
13516
14552
|
autoFixable: false
|
|
13517
14553
|
};
|
|
13518
14554
|
}
|
|
@@ -13524,7 +14560,7 @@ var dbReachable = {
|
|
|
13524
14560
|
title: this.title,
|
|
13525
14561
|
status: "error",
|
|
13526
14562
|
detail: `syntaur.db query failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
13527
|
-
affected: [
|
|
14563
|
+
affected: [resolve41(ctx.syntaurRoot, "syntaur.db")],
|
|
13528
14564
|
autoFixable: false
|
|
13529
14565
|
};
|
|
13530
14566
|
}
|
|
@@ -13550,7 +14586,7 @@ var ghostSessions = {
|
|
|
13550
14586
|
const results = [];
|
|
13551
14587
|
for (const row of rows) {
|
|
13552
14588
|
if (!row.project_slug) continue;
|
|
13553
|
-
const projectPath =
|
|
14589
|
+
const projectPath = resolve41(projectsDir2, row.project_slug, "project.md");
|
|
13554
14590
|
if (!await fileExists(projectPath)) {
|
|
13555
14591
|
results.push({
|
|
13556
14592
|
id: this.id,
|
|
@@ -13569,7 +14605,7 @@ var ghostSessions = {
|
|
|
13569
14605
|
continue;
|
|
13570
14606
|
}
|
|
13571
14607
|
if (row.assignment_slug) {
|
|
13572
|
-
const assignmentPath =
|
|
14608
|
+
const assignmentPath = resolve41(
|
|
13573
14609
|
projectsDir2,
|
|
13574
14610
|
row.project_slug,
|
|
13575
14611
|
"assignments",
|
|
@@ -13726,8 +14762,8 @@ function skipped2(check, reason) {
|
|
|
13726
14762
|
init_fs();
|
|
13727
14763
|
init_parser();
|
|
13728
14764
|
init_types();
|
|
13729
|
-
import { resolve as
|
|
13730
|
-
import { readFile as
|
|
14765
|
+
import { resolve as resolve42 } from "path";
|
|
14766
|
+
import { readFile as readFile24 } from "fs/promises";
|
|
13731
14767
|
var CATEGORY7 = "workspace";
|
|
13732
14768
|
var ASSIGNMENT_FIELDS = ["projectSlug", "assignmentSlug", "projectDir", "assignmentDir"];
|
|
13733
14769
|
function hasAnyAssignmentField(ctx) {
|
|
@@ -13739,12 +14775,12 @@ function isStandaloneSession(ctx) {
|
|
|
13739
14775
|
return !hasAnyAssignmentField(ctx) && typeof ctx.sessionId === "string" && ctx.sessionId.length > 0;
|
|
13740
14776
|
}
|
|
13741
14777
|
async function loadContext(ctx) {
|
|
13742
|
-
const path =
|
|
14778
|
+
const path = resolve42(ctx.cwd, ".syntaur", "context.json");
|
|
13743
14779
|
if (!await fileExists(path)) {
|
|
13744
14780
|
return { data: null, path, exists: false, parseError: null };
|
|
13745
14781
|
}
|
|
13746
14782
|
try {
|
|
13747
|
-
const raw = await
|
|
14783
|
+
const raw = await readFile24(path, "utf-8");
|
|
13748
14784
|
return { data: JSON.parse(raw), path, exists: true, parseError: null };
|
|
13749
14785
|
} catch (err2) {
|
|
13750
14786
|
return {
|
|
@@ -13819,7 +14855,7 @@ var contextAssignmentResolves = {
|
|
|
13819
14855
|
if (!exists) return skipped3(this, "no context to resolve");
|
|
13820
14856
|
if (isStandaloneSession(data)) return skipped3(this, "standalone session context \u2014 no assignment to resolve");
|
|
13821
14857
|
if (!data?.assignmentDir) return skipped3(this, "context has no assignmentDir");
|
|
13822
|
-
const assignmentMd =
|
|
14858
|
+
const assignmentMd = resolve42(data.assignmentDir, "assignment.md");
|
|
13823
14859
|
if (!await fileExists(assignmentMd)) {
|
|
13824
14860
|
return {
|
|
13825
14861
|
id: this.id,
|
|
@@ -13848,10 +14884,10 @@ var contextTerminal = {
|
|
|
13848
14884
|
if (!exists) return skipped3(this, "no context to check");
|
|
13849
14885
|
if (isStandaloneSession(data)) return skipped3(this, "standalone session context \u2014 no assignment to check");
|
|
13850
14886
|
if (!data?.assignmentDir) return skipped3(this, "context has no assignmentDir");
|
|
13851
|
-
const assignmentMd =
|
|
14887
|
+
const assignmentMd = resolve42(data.assignmentDir, "assignment.md");
|
|
13852
14888
|
if (!await fileExists(assignmentMd)) return skipped3(this, "assignment file missing");
|
|
13853
14889
|
try {
|
|
13854
|
-
const content = await
|
|
14890
|
+
const content = await readFile24(assignmentMd, "utf-8");
|
|
13855
14891
|
const parsed = parseAssignmentFull(content);
|
|
13856
14892
|
const terminal = terminalStatuses2(ctx);
|
|
13857
14893
|
if (terminal.has(parsed.status)) {
|
|
@@ -13998,7 +15034,7 @@ async function readVersion() {
|
|
|
13998
15034
|
let dir = dirname11(here);
|
|
13999
15035
|
for (let i = 0; i < 6; i++) {
|
|
14000
15036
|
try {
|
|
14001
|
-
const raw = await
|
|
15037
|
+
const raw = await readFile25(join5(dir, "package.json"), "utf-8");
|
|
14002
15038
|
const parsed = JSON.parse(raw);
|
|
14003
15039
|
return typeof parsed.version === "string" ? parsed.version : null;
|
|
14004
15040
|
} catch {
|
|
@@ -14135,8 +15171,8 @@ var doctorCommand = new Command3("doctor").description("Diagnose Syntaur state a
|
|
|
14135
15171
|
init_paths();
|
|
14136
15172
|
init_fs();
|
|
14137
15173
|
init_config2();
|
|
14138
|
-
import { resolve as
|
|
14139
|
-
import { readFile as
|
|
15174
|
+
import { resolve as resolve43 } from "path";
|
|
15175
|
+
import { readFile as readFile26 } from "fs/promises";
|
|
14140
15176
|
init_timestamp();
|
|
14141
15177
|
init_assignment_resolver();
|
|
14142
15178
|
function shortId() {
|
|
@@ -14168,7 +15204,7 @@ async function commentCommand(target, text, options = {}) {
|
|
|
14168
15204
|
if (!isValidSlug(target)) {
|
|
14169
15205
|
throw new Error(`Invalid assignment slug "${target}".`);
|
|
14170
15206
|
}
|
|
14171
|
-
assignmentDir =
|
|
15207
|
+
assignmentDir = resolve43(baseDir, options.project, "assignments", target);
|
|
14172
15208
|
assignmentRef = target;
|
|
14173
15209
|
} else {
|
|
14174
15210
|
const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), target);
|
|
@@ -14178,13 +15214,13 @@ async function commentCommand(target, text, options = {}) {
|
|
|
14178
15214
|
assignmentDir = resolved.assignmentDir;
|
|
14179
15215
|
assignmentRef = resolved.standalone ? resolved.id : resolved.assignmentSlug;
|
|
14180
15216
|
}
|
|
14181
|
-
const commentsPath =
|
|
15217
|
+
const commentsPath = resolve43(assignmentDir, "comments.md");
|
|
14182
15218
|
const timestamp = nowTimestamp();
|
|
14183
15219
|
const author = options.author ?? process.env.USER ?? "unknown";
|
|
14184
15220
|
let currentContent;
|
|
14185
15221
|
let currentCount = 0;
|
|
14186
15222
|
if (await fileExists(commentsPath)) {
|
|
14187
|
-
currentContent = await
|
|
15223
|
+
currentContent = await readFile26(commentsPath, "utf-8");
|
|
14188
15224
|
const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
|
|
14189
15225
|
if (countMatch) currentCount = parseInt(countMatch[1], 10);
|
|
14190
15226
|
} else {
|
|
@@ -14221,8 +15257,8 @@ ${entry}`;
|
|
|
14221
15257
|
init_paths();
|
|
14222
15258
|
init_fs();
|
|
14223
15259
|
init_config2();
|
|
14224
|
-
import { resolve as
|
|
14225
|
-
import { readFile as
|
|
15260
|
+
import { resolve as resolve44 } from "path";
|
|
15261
|
+
import { readFile as readFile27 } from "fs/promises";
|
|
14226
15262
|
init_timestamp();
|
|
14227
15263
|
init_assignment_resolver();
|
|
14228
15264
|
function setTopLevelField3(content, key, value) {
|
|
@@ -14247,7 +15283,7 @@ async function requestCommand(target, text, options = {}) {
|
|
|
14247
15283
|
if (!isValidSlug(target)) {
|
|
14248
15284
|
throw new Error(`Invalid assignment slug "${target}".`);
|
|
14249
15285
|
}
|
|
14250
|
-
assignmentDir =
|
|
15286
|
+
assignmentDir = resolve44(baseDir, options.project, "assignments", target);
|
|
14251
15287
|
targetRef = target;
|
|
14252
15288
|
} else {
|
|
14253
15289
|
const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), target);
|
|
@@ -14257,12 +15293,12 @@ async function requestCommand(target, text, options = {}) {
|
|
|
14257
15293
|
assignmentDir = resolved.assignmentDir;
|
|
14258
15294
|
targetRef = resolved.standalone ? resolved.id : resolved.assignmentSlug;
|
|
14259
15295
|
}
|
|
14260
|
-
const assignmentMdPath =
|
|
15296
|
+
const assignmentMdPath = resolve44(assignmentDir, "assignment.md");
|
|
14261
15297
|
if (!await fileExists(assignmentMdPath)) {
|
|
14262
15298
|
throw new Error(`assignment.md not found at ${assignmentMdPath}`);
|
|
14263
15299
|
}
|
|
14264
15300
|
const source = options.from ?? process.env.SYNTAUR_ASSIGNMENT ?? "unknown";
|
|
14265
|
-
let content = await
|
|
15301
|
+
let content = await readFile27(assignmentMdPath, "utf-8");
|
|
14266
15302
|
const todoLine = `- [ ] ${text.trim()} (from: ${source})`;
|
|
14267
15303
|
const todosHeading = /^## Todos\s*$/m;
|
|
14268
15304
|
if (todosHeading.test(content)) {
|
|
@@ -14330,20 +15366,20 @@ async function getDefaultCommandName() {
|
|
|
14330
15366
|
init_paths();
|
|
14331
15367
|
init_fs();
|
|
14332
15368
|
import { fileURLToPath as fileURLToPath9 } from "url";
|
|
14333
|
-
import { readFile as
|
|
14334
|
-
import { dirname as dirname13, join as join7, resolve as
|
|
15369
|
+
import { readFile as readFile29 } from "fs/promises";
|
|
15370
|
+
import { dirname as dirname13, join as join7, resolve as resolve45 } from "path";
|
|
14335
15371
|
import { spawn as spawn3 } from "child_process";
|
|
14336
15372
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
14337
15373
|
|
|
14338
15374
|
// src/utils/version.ts
|
|
14339
15375
|
import { fileURLToPath as fileURLToPath8 } from "url";
|
|
14340
|
-
import { readFile as
|
|
15376
|
+
import { readFile as readFile28 } from "fs/promises";
|
|
14341
15377
|
import { dirname as dirname12, join as join6 } from "path";
|
|
14342
15378
|
async function readPackageVersion(scriptUrl) {
|
|
14343
15379
|
try {
|
|
14344
15380
|
const scriptPath = fileURLToPath8(scriptUrl);
|
|
14345
15381
|
const pkgRoot = dirname12(dirname12(scriptPath));
|
|
14346
|
-
const raw = await
|
|
15382
|
+
const raw = await readFile28(join6(pkgRoot, "package.json"), "utf-8");
|
|
14347
15383
|
const parsed = JSON.parse(raw);
|
|
14348
15384
|
return typeof parsed.version === "string" ? parsed.version : null;
|
|
14349
15385
|
} catch {
|
|
@@ -14352,7 +15388,7 @@ async function readPackageVersion(scriptUrl) {
|
|
|
14352
15388
|
}
|
|
14353
15389
|
|
|
14354
15390
|
// src/utils/npx-prompt.ts
|
|
14355
|
-
var STATE_FILE =
|
|
15391
|
+
var STATE_FILE = resolve45(syntaurRoot(), "npx-install.json");
|
|
14356
15392
|
var META_ARGS = /* @__PURE__ */ new Set(["-h", "--help", "-V", "--version", "help"]);
|
|
14357
15393
|
var GLOBAL_VERSION_TIMEOUT_MS = 2e3;
|
|
14358
15394
|
function isRunningViaNpx(scriptUrl) {
|
|
@@ -14373,7 +15409,7 @@ function isRunningViaNpx(scriptUrl) {
|
|
|
14373
15409
|
async function readState() {
|
|
14374
15410
|
if (!await fileExists(STATE_FILE)) return null;
|
|
14375
15411
|
try {
|
|
14376
|
-
const raw = await
|
|
15412
|
+
const raw = await readFile29(STATE_FILE, "utf-8");
|
|
14377
15413
|
return JSON.parse(raw);
|
|
14378
15414
|
} catch {
|
|
14379
15415
|
return null;
|
|
@@ -14432,7 +15468,7 @@ async function readGlobalVersion() {
|
|
|
14432
15468
|
try {
|
|
14433
15469
|
const manifestPath = join7(rootPath, "syntaur", "package.json");
|
|
14434
15470
|
if (!await fileExists(manifestPath)) return null;
|
|
14435
|
-
const raw = await
|
|
15471
|
+
const raw = await readFile29(manifestPath, "utf-8");
|
|
14436
15472
|
const parsed = JSON.parse(raw);
|
|
14437
15473
|
return typeof parsed.version === "string" ? parsed.version : null;
|
|
14438
15474
|
} catch {
|
|
@@ -14580,7 +15616,7 @@ program.command("create-assignment").description("Create a new assignment within
|
|
|
14580
15616
|
"--priority <level>",
|
|
14581
15617
|
"Priority level (low|medium|high|critical)",
|
|
14582
15618
|
"medium"
|
|
14583
|
-
).option("--type <type>", "Assignment type (e.g. feature, bug, refactor)").option("--depends-on <slugs>", "Comma-separated dependency slugs (not allowed with --one-off)").option("--links <slugs>", "Comma-separated linked assignment slugs (projectSlug/assignmentSlug format)").option("--dir <path>", "Override default project directory (ignored for --one-off)").action(async (title, options) => {
|
|
15619
|
+
).option("--type <type>", "Assignment type (e.g. feature, bug, refactor)").option("--depends-on <slugs>", "Comma-separated dependency slugs (not allowed with --one-off)").option("--links <slugs>", "Comma-separated linked assignment slugs (projectSlug/assignmentSlug format)").option("--dir <path>", "Override default project directory (ignored for --one-off)").option("--with-todos", "Scaffold a ## Todos section in assignment.md (omitted by default; typically populated by /plan-assignment)").option("--workspace <slug>", "Workspace group slug (only valid with --one-off; mutually exclusive with --project)").action(async (title, options) => {
|
|
14584
15620
|
try {
|
|
14585
15621
|
await createAssignmentCommand(title, options);
|
|
14586
15622
|
} catch (error) {
|
|
@@ -14864,9 +15900,31 @@ program.command("create-playbook").description("Create a new playbook").argument
|
|
|
14864
15900
|
process.exit(1);
|
|
14865
15901
|
}
|
|
14866
15902
|
});
|
|
14867
|
-
program.command("list-playbooks").description("List all playbooks").action(async () => {
|
|
15903
|
+
program.command("list-playbooks").description("List playbooks (disabled playbooks are excluded unless --all is passed)").option("--all", "Include disabled playbooks").action(async (options) => {
|
|
15904
|
+
try {
|
|
15905
|
+
await listPlaybooksCommand({ all: Boolean(options?.all) });
|
|
15906
|
+
} catch (error) {
|
|
15907
|
+
console.error(
|
|
15908
|
+
"Error:",
|
|
15909
|
+
error instanceof Error ? error.message : String(error)
|
|
15910
|
+
);
|
|
15911
|
+
process.exit(1);
|
|
15912
|
+
}
|
|
15913
|
+
});
|
|
15914
|
+
program.command("enable-playbook").description("Enable a previously-disabled playbook").argument("<slug>", "Playbook slug").action(async (slug) => {
|
|
15915
|
+
try {
|
|
15916
|
+
await enablePlaybookCommand(slug);
|
|
15917
|
+
} catch (error) {
|
|
15918
|
+
console.error(
|
|
15919
|
+
"Error:",
|
|
15920
|
+
error instanceof Error ? error.message : String(error)
|
|
15921
|
+
);
|
|
15922
|
+
process.exit(1);
|
|
15923
|
+
}
|
|
15924
|
+
});
|
|
15925
|
+
program.command("disable-playbook").description("Disable a playbook so agents no longer load it").argument("<slug>", "Playbook slug").action(async (slug) => {
|
|
14868
15926
|
try {
|
|
14869
|
-
await
|
|
15927
|
+
await disablePlaybookCommand(slug);
|
|
14870
15928
|
} catch (error) {
|
|
14871
15929
|
console.error(
|
|
14872
15930
|
"Error:",
|