syntaur 0.3.0 → 0.4.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/README.md +26 -3
- package/dist/dashboard/server.js +497 -315
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +1240 -550
- package/dist/index.js.map +1 -1
- package/package.json +8 -2
- package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
- package/platforms/claude-code/agents/syntaur-expert.md +35 -20
- package/platforms/claude-code/commands/complete-assignment/complete-assignment.md +20 -0
- package/platforms/claude-code/commands/create-assignment/create-assignment.md +20 -0
- package/platforms/claude-code/commands/create-project/create-project.md +20 -0
- package/platforms/claude-code/commands/grab-assignment/grab-assignment.md +20 -0
- package/platforms/claude-code/commands/plan-assignment/plan-assignment.md +20 -0
- package/platforms/claude-code/commands/track-session/track-session.md +43 -18
- package/platforms/claude-code/hooks/hooks.json +11 -0
- package/platforms/claude-code/hooks/session-cleanup.sh +13 -23
- package/platforms/claude-code/hooks/session-start.sh +80 -0
- package/platforms/codex/.codex-plugin/plugin.json +1 -1
- package/platforms/codex/agents/syntaur-operator.md +6 -4
- package/platforms/codex/scripts/resolve-session.sh +49 -0
- package/statusline/statusline.sh +133 -0
- package/vendor/syntaur-skills/LICENSE +21 -0
- package/vendor/syntaur-skills/README.md +43 -0
- package/vendor/syntaur-skills/skills/complete-assignment/SKILL.md +146 -0
- package/vendor/syntaur-skills/skills/create-assignment/SKILL.md +72 -0
- package/vendor/syntaur-skills/skills/create-project/SKILL.md +56 -0
- package/vendor/syntaur-skills/skills/grab-assignment/SKILL.md +158 -0
- package/vendor/syntaur-skills/skills/plan-assignment/SKILL.md +137 -0
- package/vendor/syntaur-skills/skills/syntaur-protocol/SKILL.md +119 -0
- package/vendor/syntaur-skills/skills/syntaur-protocol/references/file-ownership.md +67 -0
- package/vendor/syntaur-skills/skills/syntaur-protocol/references/protocol-summary.md +82 -0
- package/platforms/claude-code/skills/complete-assignment/SKILL.md +0 -155
- package/platforms/claude-code/skills/create-assignment/SKILL.md +0 -67
- package/platforms/claude-code/skills/grab-assignment/SKILL.md +0 -187
- package/platforms/claude-code/skills/plan-assignment/SKILL.md +0 -148
- package/platforms/claude-code/skills/syntaur-protocol/SKILL.md +0 -86
- package/platforms/codex/skills/complete-assignment/SKILL.md +0 -64
- package/platforms/codex/skills/create-assignment/SKILL.md +0 -49
- package/platforms/codex/skills/grab-assignment/SKILL.md +0 -71
- package/platforms/codex/skills/plan-assignment/SKILL.md +0 -57
- package/platforms/codex/skills/syntaur-protocol/SKILL.md +0 -102
package/dist/dashboard/server.js
CHANGED
|
@@ -484,9 +484,144 @@ var init_config = __esm({
|
|
|
484
484
|
}
|
|
485
485
|
});
|
|
486
486
|
|
|
487
|
+
// src/utils/fs-migration.ts
|
|
488
|
+
import { readdir, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
489
|
+
import { resolve as resolve3 } from "path";
|
|
490
|
+
async function migrateLegacyProjectFiles(projectsDir) {
|
|
491
|
+
const result = {
|
|
492
|
+
renamedProjectFiles: [],
|
|
493
|
+
legacyExtras: []
|
|
494
|
+
};
|
|
495
|
+
if (!await fileExists(projectsDir)) return result;
|
|
496
|
+
let entries;
|
|
497
|
+
try {
|
|
498
|
+
entries = await readdir(projectsDir, { withFileTypes: true });
|
|
499
|
+
} catch {
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
for (const entry of entries) {
|
|
503
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
504
|
+
const projectDir = resolve3(projectsDir, entry.name);
|
|
505
|
+
const legacy = resolve3(projectDir, "mission.md");
|
|
506
|
+
const target = resolve3(projectDir, "project.md");
|
|
507
|
+
try {
|
|
508
|
+
if (await fileExists(legacy) && !await fileExists(target)) {
|
|
509
|
+
await rename2(legacy, target);
|
|
510
|
+
result.renamedProjectFiles.push(`${entry.name}/mission.md`);
|
|
511
|
+
}
|
|
512
|
+
} catch {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
for (const stale of ["agent.md", "claude.md"]) {
|
|
516
|
+
try {
|
|
517
|
+
if (await fileExists(resolve3(projectDir, stale))) {
|
|
518
|
+
result.legacyExtras.push(`${entry.name}/${stale}`);
|
|
519
|
+
}
|
|
520
|
+
} catch {
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return result;
|
|
525
|
+
}
|
|
526
|
+
async function migrateLegacyConfig(configPath) {
|
|
527
|
+
const result = {
|
|
528
|
+
renamedField: false,
|
|
529
|
+
renamedDir: false,
|
|
530
|
+
resolvedProjectsDir: null
|
|
531
|
+
};
|
|
532
|
+
if (!await fileExists(configPath)) return result;
|
|
533
|
+
let content;
|
|
534
|
+
try {
|
|
535
|
+
content = await readFile2(configPath, "utf-8");
|
|
536
|
+
} catch {
|
|
537
|
+
return result;
|
|
538
|
+
}
|
|
539
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
540
|
+
if (!fmMatch) return result;
|
|
541
|
+
const fmBlock = fmMatch[1];
|
|
542
|
+
const afterFm = content.slice(fmMatch[0].length);
|
|
543
|
+
const missionLineRe = /^(\s*)defaultMissionDir\s*:\s*(.*)$/m;
|
|
544
|
+
const missionLineMatch = fmBlock.match(missionLineRe);
|
|
545
|
+
const hasProjectLine = /^\s*defaultProjectDir\s*:/m.test(fmBlock);
|
|
546
|
+
let newFmBlock = fmBlock;
|
|
547
|
+
let missionValue = null;
|
|
548
|
+
if (missionLineMatch) {
|
|
549
|
+
missionValue = missionLineMatch[2].trim();
|
|
550
|
+
if (!hasProjectLine) {
|
|
551
|
+
newFmBlock = fmBlock.replace(
|
|
552
|
+
missionLineRe,
|
|
553
|
+
`$1defaultProjectDir: ${missionValue}`
|
|
554
|
+
);
|
|
555
|
+
result.renamedField = true;
|
|
556
|
+
} else {
|
|
557
|
+
newFmBlock = fmBlock.replace(missionLineRe, "").replace(/\n{2,}/g, "\n");
|
|
558
|
+
result.renamedField = true;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const projectLineRe = /^\s*defaultProjectDir\s*:\s*(.*)$/m;
|
|
562
|
+
const projectLineMatch = newFmBlock.match(projectLineRe);
|
|
563
|
+
const projectsDirRaw = projectLineMatch ? projectLineMatch[1].trim().replace(/^['"]|['"]$/g, "") : missionValue;
|
|
564
|
+
const expand = (p) => p.startsWith("~") ? resolve3(process.env.HOME ?? "/", p.slice(p.startsWith("~/") ? 2 : 1)) : p;
|
|
565
|
+
let resolvedProjectsDir = projectsDirRaw ? expand(projectsDirRaw) : null;
|
|
566
|
+
if (resolvedProjectsDir && resolvedProjectsDir.endsWith("/missions")) {
|
|
567
|
+
const siblingProjectsDir = resolvedProjectsDir.replace(/\/missions$/, "/projects");
|
|
568
|
+
if (await fileExists(resolvedProjectsDir) && !await fileExists(siblingProjectsDir)) {
|
|
569
|
+
try {
|
|
570
|
+
await rename2(resolvedProjectsDir, siblingProjectsDir);
|
|
571
|
+
const newValue = projectsDirRaw.endsWith("/missions") ? projectsDirRaw.replace(/\/missions$/, "/projects") : siblingProjectsDir;
|
|
572
|
+
newFmBlock = newFmBlock.replace(
|
|
573
|
+
projectLineRe,
|
|
574
|
+
`defaultProjectDir: ${newValue}`
|
|
575
|
+
);
|
|
576
|
+
resolvedProjectsDir = siblingProjectsDir;
|
|
577
|
+
result.renamedDir = true;
|
|
578
|
+
} catch {
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
result.resolvedProjectsDir = resolvedProjectsDir;
|
|
583
|
+
if (result.renamedField || result.renamedDir) {
|
|
584
|
+
const newContent = `---
|
|
585
|
+
${newFmBlock.replace(/\n+$/, "")}
|
|
586
|
+
---
|
|
587
|
+
${afterFm.startsWith("\n") ? afterFm.slice(1) : afterFm}`;
|
|
588
|
+
try {
|
|
589
|
+
await writeFile2(configPath, newContent, "utf-8");
|
|
590
|
+
} catch {
|
|
591
|
+
result.renamedField = false;
|
|
592
|
+
result.renamedDir = false;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return result;
|
|
596
|
+
}
|
|
597
|
+
function summarizeMigration(project, config) {
|
|
598
|
+
const parts = [];
|
|
599
|
+
if (project.renamedProjectFiles.length > 0) {
|
|
600
|
+
const firstThree = project.renamedProjectFiles.map((p) => p.split("/")[0]).slice(0, 3).join(", ");
|
|
601
|
+
const more = project.renamedProjectFiles.length > 3 ? ` and ${project.renamedProjectFiles.length - 3} more` : "";
|
|
602
|
+
parts.push(
|
|
603
|
+
`renamed mission.md \u2192 project.md in ${project.renamedProjectFiles.length} project${project.renamedProjectFiles.length === 1 ? "" : "s"} (${firstThree}${more})`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
if (config?.renamedField) parts.push("updated config defaultMissionDir \u2192 defaultProjectDir");
|
|
607
|
+
if (config?.renamedDir) parts.push("renamed projects directory on disk");
|
|
608
|
+
if (project.legacyExtras.length > 0) {
|
|
609
|
+
parts.push(
|
|
610
|
+
`${project.legacyExtras.length} legacy agent.md / claude.md file${project.legacyExtras.length === 1 ? "" : "s"} left in place (no longer read)`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
return parts.length ? `[syntaur] legacy migration: ${parts.join("; ")}` : "";
|
|
614
|
+
}
|
|
615
|
+
var init_fs_migration = __esm({
|
|
616
|
+
"src/utils/fs-migration.ts"() {
|
|
617
|
+
"use strict";
|
|
618
|
+
init_fs();
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
|
|
487
622
|
// src/utils/config.ts
|
|
488
|
-
import { readFile as
|
|
489
|
-
import { resolve as
|
|
623
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
624
|
+
import { resolve as resolve4, isAbsolute } from "path";
|
|
490
625
|
function parseFrontmatter(content) {
|
|
491
626
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
492
627
|
if (!match) return {};
|
|
@@ -673,10 +808,10 @@ function parseOptionalAbsolutePath(value, fieldName) {
|
|
|
673
808
|
);
|
|
674
809
|
return null;
|
|
675
810
|
}
|
|
676
|
-
return
|
|
811
|
+
return resolve4(expanded);
|
|
677
812
|
}
|
|
678
813
|
async function writeStatusConfig(statuses) {
|
|
679
|
-
const configPath =
|
|
814
|
+
const configPath = resolve4(syntaurRoot(), "config.md");
|
|
680
815
|
const statusBlock = serializeStatusConfig(statuses);
|
|
681
816
|
if (!await fileExists(configPath)) {
|
|
682
817
|
const content = `---
|
|
@@ -688,7 +823,7 @@ ${statusBlock}
|
|
|
688
823
|
await writeFileForce(configPath, content);
|
|
689
824
|
return;
|
|
690
825
|
}
|
|
691
|
-
const existing = await
|
|
826
|
+
const existing = await readFile3(configPath, "utf-8");
|
|
692
827
|
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
693
828
|
if (!fmMatch) {
|
|
694
829
|
const content = `---
|
|
@@ -730,9 +865,9 @@ ${statusBlock}
|
|
|
730
865
|
await writeFileForce(configPath, newContent);
|
|
731
866
|
}
|
|
732
867
|
async function deleteStatusConfig() {
|
|
733
|
-
const configPath =
|
|
868
|
+
const configPath = resolve4(syntaurRoot(), "config.md");
|
|
734
869
|
if (!await fileExists(configPath)) return;
|
|
735
|
-
const existing = await
|
|
870
|
+
const existing = await readFile3(configPath, "utf-8");
|
|
736
871
|
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
737
872
|
if (!fmMatch) return;
|
|
738
873
|
const fmBlock = fmMatch[2];
|
|
@@ -744,7 +879,7 @@ ${cleanedFm}
|
|
|
744
879
|
await writeFileForce(configPath, newContent);
|
|
745
880
|
}
|
|
746
881
|
async function updateBackupConfig(backup) {
|
|
747
|
-
const configPath =
|
|
882
|
+
const configPath = resolve4(syntaurRoot(), "config.md");
|
|
748
883
|
const current = (await readConfig()).backup;
|
|
749
884
|
const nextBackup = {
|
|
750
885
|
repo: current?.repo ?? null,
|
|
@@ -754,7 +889,7 @@ async function updateBackupConfig(backup) {
|
|
|
754
889
|
...backup
|
|
755
890
|
};
|
|
756
891
|
const backupBlock = serializeBackupConfig(nextBackup);
|
|
757
|
-
const existing = await fileExists(configPath) ? await
|
|
892
|
+
const existing = await fileExists(configPath) ? await readFile3(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
758
893
|
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
759
894
|
if (!fmMatch) {
|
|
760
895
|
const content = `---
|
|
@@ -778,11 +913,15 @@ ${normalizedFm}
|
|
|
778
913
|
await writeFileForce(configPath, newContent);
|
|
779
914
|
}
|
|
780
915
|
async function readConfig() {
|
|
781
|
-
const configPath =
|
|
916
|
+
const configPath = resolve4(syntaurRoot(), "config.md");
|
|
782
917
|
if (!await fileExists(configPath)) {
|
|
783
918
|
return { ...DEFAULT_CONFIG };
|
|
784
919
|
}
|
|
785
|
-
|
|
920
|
+
if (!migratedConfigPaths.has(configPath)) {
|
|
921
|
+
migratedConfigPaths.add(configPath);
|
|
922
|
+
await migrateLegacyConfig(configPath);
|
|
923
|
+
}
|
|
924
|
+
const content = await readFile3(configPath, "utf-8");
|
|
786
925
|
const fm = parseFrontmatter(content);
|
|
787
926
|
if (Object.keys(fm).length === 0) {
|
|
788
927
|
console.warn("Warning: ~/.syntaur/config.md has malformed frontmatter, using defaults");
|
|
@@ -829,13 +968,14 @@ async function readConfig() {
|
|
|
829
968
|
types: null
|
|
830
969
|
};
|
|
831
970
|
}
|
|
832
|
-
var DEFAULT_CONFIG;
|
|
971
|
+
var DEFAULT_CONFIG, migratedConfigPaths;
|
|
833
972
|
var init_config2 = __esm({
|
|
834
973
|
"src/utils/config.ts"() {
|
|
835
974
|
"use strict";
|
|
836
975
|
init_paths();
|
|
837
976
|
init_fs();
|
|
838
977
|
init_config();
|
|
978
|
+
init_fs_migration();
|
|
839
979
|
DEFAULT_CONFIG = {
|
|
840
980
|
version: "2.0",
|
|
841
981
|
defaultProjectDir: defaultProjectDir(),
|
|
@@ -855,6 +995,7 @@ var init_config2 = __esm({
|
|
|
855
995
|
statuses: null,
|
|
856
996
|
types: null
|
|
857
997
|
};
|
|
998
|
+
migratedConfigPaths = /* @__PURE__ */ new Set();
|
|
858
999
|
}
|
|
859
1000
|
});
|
|
860
1001
|
|
|
@@ -908,9 +1049,10 @@ function parseListField(frontmatter, fieldName) {
|
|
|
908
1049
|
}
|
|
909
1050
|
function parseProject(fileContent) {
|
|
910
1051
|
const [fm, body] = extractFrontmatter2(fileContent);
|
|
1052
|
+
const slug = getField(fm, "slug") ?? getField(fm, "mission") ?? "";
|
|
911
1053
|
return {
|
|
912
1054
|
id: getField(fm, "id") ?? "",
|
|
913
|
-
slug
|
|
1055
|
+
slug,
|
|
914
1056
|
title: getField(fm, "title") ?? "",
|
|
915
1057
|
archived: getField(fm, "archived") === "true",
|
|
916
1058
|
archivedAt: getField(fm, "archivedAt"),
|
|
@@ -1143,13 +1285,13 @@ var init_parser = __esm({
|
|
|
1143
1285
|
});
|
|
1144
1286
|
|
|
1145
1287
|
// src/utils/assignment-resolver.ts
|
|
1146
|
-
import { resolve as
|
|
1147
|
-
import { readdir, readFile as
|
|
1288
|
+
import { resolve as resolve5 } from "path";
|
|
1289
|
+
import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
|
|
1148
1290
|
async function resolveAssignmentById(projectsDir, assignmentsDir, id) {
|
|
1149
1291
|
let standaloneMatch = null;
|
|
1150
1292
|
let projectMatch = null;
|
|
1151
|
-
const standaloneDir =
|
|
1152
|
-
const standalonePath =
|
|
1293
|
+
const standaloneDir = resolve5(assignmentsDir, id);
|
|
1294
|
+
const standalonePath = resolve5(standaloneDir, "assignment.md");
|
|
1153
1295
|
if (await fileExists(standalonePath)) {
|
|
1154
1296
|
standaloneMatch = {
|
|
1155
1297
|
assignmentDir: standaloneDir,
|
|
@@ -1161,24 +1303,24 @@ async function resolveAssignmentById(projectsDir, assignmentsDir, id) {
|
|
|
1161
1303
|
}
|
|
1162
1304
|
if (await fileExists(projectsDir)) {
|
|
1163
1305
|
try {
|
|
1164
|
-
const projects = await
|
|
1306
|
+
const projects = await readdir2(projectsDir, { withFileTypes: true });
|
|
1165
1307
|
for (const p of projects) {
|
|
1166
1308
|
if (!p.isDirectory()) continue;
|
|
1167
1309
|
if (p.name.startsWith(".") || p.name.startsWith("_")) continue;
|
|
1168
|
-
const assignmentsPath =
|
|
1310
|
+
const assignmentsPath = resolve5(projectsDir, p.name, "assignments");
|
|
1169
1311
|
if (!await fileExists(assignmentsPath)) continue;
|
|
1170
|
-
const entries = await
|
|
1312
|
+
const entries = await readdir2(assignmentsPath, { withFileTypes: true });
|
|
1171
1313
|
for (const a of entries) {
|
|
1172
1314
|
if (!a.isDirectory()) continue;
|
|
1173
|
-
const aPath =
|
|
1315
|
+
const aPath = resolve5(assignmentsPath, a.name, "assignment.md");
|
|
1174
1316
|
if (!await fileExists(aPath)) continue;
|
|
1175
1317
|
try {
|
|
1176
|
-
const content = await
|
|
1318
|
+
const content = await readFile4(aPath, "utf-8");
|
|
1177
1319
|
const [fm] = extractFrontmatter2(content);
|
|
1178
1320
|
const fileId = getField(fm, "id");
|
|
1179
1321
|
if (fileId === id) {
|
|
1180
1322
|
projectMatch = {
|
|
1181
|
-
assignmentDir:
|
|
1323
|
+
assignmentDir: resolve5(assignmentsPath, a.name),
|
|
1182
1324
|
projectSlug: p.name,
|
|
1183
1325
|
assignmentSlug: a.name,
|
|
1184
1326
|
id,
|
|
@@ -1542,8 +1684,8 @@ var init_help = __esm({
|
|
|
1542
1684
|
// --- Session & server tracking (index 17) ---
|
|
1543
1685
|
{
|
|
1544
1686
|
command: "syntaur track-session",
|
|
1545
|
-
description: "Register an agent session,
|
|
1546
|
-
example: "syntaur track-session --agent claude --project ui-overhaul --assignment implement-overview"
|
|
1687
|
+
description: "Register an agent session. Requires --session-id from the agent runtime (real, not generated). Pass --transcript-path for the rollout/transcript file. --project and --assignment are optional.",
|
|
1688
|
+
example: "syntaur track-session --agent claude --session-id <real-id> --transcript-path <path> --project ui-overhaul --assignment implement-overview"
|
|
1547
1689
|
},
|
|
1548
1690
|
// --- Browsing & playbooks (indices 18-20) ---
|
|
1549
1691
|
{
|
|
@@ -1626,8 +1768,8 @@ var init_help = __esm({
|
|
|
1626
1768
|
});
|
|
1627
1769
|
|
|
1628
1770
|
// src/dashboard/servers.ts
|
|
1629
|
-
import { readdir as
|
|
1630
|
-
import { resolve as
|
|
1771
|
+
import { readdir as readdir3, readFile as readFile5, unlink } from "fs/promises";
|
|
1772
|
+
import { resolve as resolve6 } from "path";
|
|
1631
1773
|
function sanitizeSessionName(name) {
|
|
1632
1774
|
return name.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
1633
1775
|
}
|
|
@@ -1675,18 +1817,18 @@ async function registerSession(dir, rawName) {
|
|
|
1675
1817
|
lastRefreshed: now,
|
|
1676
1818
|
overrides: {}
|
|
1677
1819
|
});
|
|
1678
|
-
await writeFileForce(
|
|
1820
|
+
await writeFileForce(resolve6(dir, `${name}.md`), content);
|
|
1679
1821
|
return name;
|
|
1680
1822
|
}
|
|
1681
1823
|
async function listSessionFiles(dir) {
|
|
1682
1824
|
if (!await fileExists(dir)) return [];
|
|
1683
|
-
const entries = await
|
|
1825
|
+
const entries = await readdir3(dir);
|
|
1684
1826
|
return entries.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
|
|
1685
1827
|
}
|
|
1686
1828
|
async function readSessionFile(dir, name) {
|
|
1687
|
-
const filePath =
|
|
1829
|
+
const filePath = resolve6(dir, `${sanitizeSessionName(name)}.md`);
|
|
1688
1830
|
if (!await fileExists(filePath)) return null;
|
|
1689
|
-
const raw = await
|
|
1831
|
+
const raw = await readFile5(filePath, "utf-8");
|
|
1690
1832
|
const [frontmatter] = extractFrontmatter2(raw);
|
|
1691
1833
|
if (!frontmatter) return null;
|
|
1692
1834
|
const session = getField(frontmatter, "session") ?? name;
|
|
@@ -1726,7 +1868,7 @@ async function readSessionFile(dir, name) {
|
|
|
1726
1868
|
};
|
|
1727
1869
|
}
|
|
1728
1870
|
async function removeSession(dir, name) {
|
|
1729
|
-
const filePath =
|
|
1871
|
+
const filePath = resolve6(dir, `${sanitizeSessionName(name)}.md`);
|
|
1730
1872
|
if (await fileExists(filePath)) {
|
|
1731
1873
|
await unlink(filePath);
|
|
1732
1874
|
}
|
|
@@ -1735,7 +1877,7 @@ async function updateLastRefreshed(dir, name) {
|
|
|
1735
1877
|
const data = await readSessionFile(dir, name);
|
|
1736
1878
|
if (!data) return;
|
|
1737
1879
|
const content = buildSessionContent({ ...data, lastRefreshed: nowTimestamp2() });
|
|
1738
|
-
await writeFileForce(
|
|
1880
|
+
await writeFileForce(resolve6(dir, `${sanitizeSessionName(name)}.md`), content);
|
|
1739
1881
|
}
|
|
1740
1882
|
async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment) {
|
|
1741
1883
|
const data = await readSessionFile(dir, sessionName);
|
|
@@ -1747,7 +1889,7 @@ async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment)
|
|
|
1747
1889
|
delete data.overrides[key];
|
|
1748
1890
|
}
|
|
1749
1891
|
const content = buildSessionContent({ ...data });
|
|
1750
|
-
await writeFileForce(
|
|
1892
|
+
await writeFileForce(resolve6(dir, `${sanitizeSessionName(sessionName)}.md`), content);
|
|
1751
1893
|
}
|
|
1752
1894
|
async function registerAutoSession(dir, rawName, opts) {
|
|
1753
1895
|
const name = sanitizeSessionName(rawName);
|
|
@@ -1764,7 +1906,7 @@ async function registerAutoSession(dir, rawName, opts) {
|
|
|
1764
1906
|
ports: opts.ports,
|
|
1765
1907
|
cwd: opts.cwd
|
|
1766
1908
|
});
|
|
1767
|
-
await writeFileForce(
|
|
1909
|
+
await writeFileForce(resolve6(dir, `${name}.md`), content);
|
|
1768
1910
|
return name;
|
|
1769
1911
|
}
|
|
1770
1912
|
var init_servers = __esm({
|
|
@@ -1796,8 +1938,8 @@ __export(scanner_exports, {
|
|
|
1796
1938
|
});
|
|
1797
1939
|
import { execFile } from "child_process";
|
|
1798
1940
|
import { promisify } from "util";
|
|
1799
|
-
import { resolve as
|
|
1800
|
-
import { realpath, readdir as
|
|
1941
|
+
import { resolve as resolve7 } from "path";
|
|
1942
|
+
import { realpath, readdir as readdir4, readFile as readFile6 } from "fs/promises";
|
|
1801
1943
|
function clearScanCache() {
|
|
1802
1944
|
cache = null;
|
|
1803
1945
|
}
|
|
@@ -1892,8 +2034,8 @@ async function getGitInfo(cwd) {
|
|
|
1892
2034
|
let isWorktree = false;
|
|
1893
2035
|
if (commonDir && gitDir && commonDir !== gitDir) {
|
|
1894
2036
|
try {
|
|
1895
|
-
const resolvedCommon = await realpath(
|
|
1896
|
-
const resolvedGit = await realpath(
|
|
2037
|
+
const resolvedCommon = await realpath(resolve7(cwd, commonDir));
|
|
2038
|
+
const resolvedGit = await realpath(resolve7(cwd, gitDir));
|
|
1897
2039
|
isWorktree = resolvedCommon !== resolvedGit;
|
|
1898
2040
|
} catch {
|
|
1899
2041
|
isWorktree = false;
|
|
@@ -1906,17 +2048,17 @@ async function loadWorkspaceRecords(projectsDir, assignmentsDir) {
|
|
|
1906
2048
|
try {
|
|
1907
2049
|
const projects = await listProjects(projectsDir);
|
|
1908
2050
|
for (const project of projects) {
|
|
1909
|
-
const projectAssignmentsDir =
|
|
2051
|
+
const projectAssignmentsDir = resolve7(projectsDir, project.slug, "assignments");
|
|
1910
2052
|
let slugs;
|
|
1911
2053
|
try {
|
|
1912
|
-
slugs = await
|
|
2054
|
+
slugs = await readdir4(projectAssignmentsDir);
|
|
1913
2055
|
} catch {
|
|
1914
2056
|
continue;
|
|
1915
2057
|
}
|
|
1916
2058
|
for (const aslug of slugs) {
|
|
1917
|
-
const aFile =
|
|
2059
|
+
const aFile = resolve7(projectAssignmentsDir, aslug, "assignment.md");
|
|
1918
2060
|
try {
|
|
1919
|
-
const raw = await
|
|
2061
|
+
const raw = await readFile6(aFile, "utf-8");
|
|
1920
2062
|
const [fm] = extractFrontmatter2(raw);
|
|
1921
2063
|
if (!fm) continue;
|
|
1922
2064
|
records.push({
|
|
@@ -1935,12 +2077,12 @@ async function loadWorkspaceRecords(projectsDir, assignmentsDir) {
|
|
|
1935
2077
|
}
|
|
1936
2078
|
if (assignmentsDir) {
|
|
1937
2079
|
try {
|
|
1938
|
-
const entries = await
|
|
2080
|
+
const entries = await readdir4(assignmentsDir);
|
|
1939
2081
|
for (const id of entries) {
|
|
1940
2082
|
if (id.startsWith(".") || id.startsWith("_")) continue;
|
|
1941
|
-
const aFile =
|
|
2083
|
+
const aFile = resolve7(assignmentsDir, id, "assignment.md");
|
|
1942
2084
|
try {
|
|
1943
|
-
const raw = await
|
|
2085
|
+
const raw = await readFile6(aFile, "utf-8");
|
|
1944
2086
|
const [fm] = extractFrontmatter2(raw);
|
|
1945
2087
|
if (!fm) continue;
|
|
1946
2088
|
records.push({
|
|
@@ -2188,20 +2330,20 @@ var init_scanner = __esm({
|
|
|
2188
2330
|
});
|
|
2189
2331
|
|
|
2190
2332
|
// src/dashboard/api.ts
|
|
2191
|
-
import { readdir as
|
|
2192
|
-
import { resolve as
|
|
2333
|
+
import { readdir as readdir5, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
|
|
2334
|
+
import { resolve as resolve8, dirname as dirname2 } from "path";
|
|
2193
2335
|
async function listStandaloneRecords(assignmentsDir) {
|
|
2194
2336
|
if (!assignmentsDir) return [];
|
|
2195
2337
|
if (!await fileExists(assignmentsDir)) return [];
|
|
2196
|
-
const entries = await
|
|
2338
|
+
const entries = await readdir5(assignmentsDir, { withFileTypes: true });
|
|
2197
2339
|
const records = [];
|
|
2198
2340
|
for (const entry of entries) {
|
|
2199
2341
|
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
|
|
2200
|
-
const assignmentDir =
|
|
2201
|
-
const assignmentMdPath =
|
|
2342
|
+
const assignmentDir = resolve8(assignmentsDir, entry.name);
|
|
2343
|
+
const assignmentMdPath = resolve8(assignmentDir, "assignment.md");
|
|
2202
2344
|
if (!await fileExists(assignmentMdPath)) continue;
|
|
2203
2345
|
try {
|
|
2204
|
-
const content = await
|
|
2346
|
+
const content = await readFile7(assignmentMdPath, "utf-8");
|
|
2205
2347
|
const record = parseAssignmentFull(content);
|
|
2206
2348
|
records.push({ assignmentDir, id: entry.name, record });
|
|
2207
2349
|
} catch {
|
|
@@ -2270,9 +2412,9 @@ async function listProjects(projectsDir) {
|
|
|
2270
2412
|
return projectRecords.map((record) => record.summary);
|
|
2271
2413
|
}
|
|
2272
2414
|
async function readWorkspaceRegistry(projectsDir) {
|
|
2273
|
-
const registryPath =
|
|
2415
|
+
const registryPath = resolve8(dirname2(projectsDir), "workspaces.json");
|
|
2274
2416
|
try {
|
|
2275
|
-
const raw = await
|
|
2417
|
+
const raw = await readFile7(registryPath, "utf-8");
|
|
2276
2418
|
const parsed = JSON.parse(raw);
|
|
2277
2419
|
return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
|
|
2278
2420
|
} catch {
|
|
@@ -2280,8 +2422,8 @@ async function readWorkspaceRegistry(projectsDir) {
|
|
|
2280
2422
|
}
|
|
2281
2423
|
}
|
|
2282
2424
|
async function writeWorkspaceRegistry(projectsDir, workspaces) {
|
|
2283
|
-
const registryPath =
|
|
2284
|
-
await
|
|
2425
|
+
const registryPath = resolve8(dirname2(projectsDir), "workspaces.json");
|
|
2426
|
+
await writeFile3(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
|
|
2285
2427
|
}
|
|
2286
2428
|
async function listWorkspaces(projectsDir) {
|
|
2287
2429
|
const [projectRecords, registered] = await Promise.all([
|
|
@@ -2480,7 +2622,7 @@ async function getEditableDocument(projectsDir, documentType, projectSlug, assig
|
|
|
2480
2622
|
if (!filePath || !await fileExists(filePath)) {
|
|
2481
2623
|
return null;
|
|
2482
2624
|
}
|
|
2483
|
-
const content = await
|
|
2625
|
+
const content = await readFile7(filePath, "utf-8");
|
|
2484
2626
|
const title = getEditableDocumentTitle(documentType, projectSlug, assignmentSlug);
|
|
2485
2627
|
return {
|
|
2486
2628
|
documentType,
|
|
@@ -2504,9 +2646,9 @@ async function getEditableDocumentById(projectsDir, assignmentsDir, documentType
|
|
|
2504
2646
|
}
|
|
2505
2647
|
const fileName = documentType === "assignment" ? "assignment.md" : documentType === "plan" ? "plan.md" : documentType === "scratchpad" ? "scratchpad.md" : documentType === "handoff" ? "handoff.md" : documentType === "decision-record" ? "decision-record.md" : null;
|
|
2506
2648
|
if (!fileName) return null;
|
|
2507
|
-
const filePath =
|
|
2649
|
+
const filePath = resolve8(resolved.assignmentDir, fileName);
|
|
2508
2650
|
if (!await fileExists(filePath)) return null;
|
|
2509
|
-
const content = await
|
|
2651
|
+
const content = await readFile7(filePath, "utf-8");
|
|
2510
2652
|
const label = resolved.id;
|
|
2511
2653
|
const title = documentType === "assignment" ? `Edit Assignment: ${label}` : documentType === "plan" ? `Edit Plan: ${label}` : documentType === "scratchpad" ? `Edit Scratchpad: ${label}` : documentType === "handoff" ? `Append Handoff: ${label}` : `Append Decision: ${label}`;
|
|
2512
2654
|
return {
|
|
@@ -2520,12 +2662,12 @@ async function getEditableDocumentById(projectsDir, assignmentsDir, documentType
|
|
|
2520
2662
|
};
|
|
2521
2663
|
}
|
|
2522
2664
|
async function getProjectDetail(projectsDir, slug) {
|
|
2523
|
-
const projectPath =
|
|
2524
|
-
const projectMdPath =
|
|
2665
|
+
const projectPath = resolve8(projectsDir, slug);
|
|
2666
|
+
const projectMdPath = resolve8(projectPath, "project.md");
|
|
2525
2667
|
if (!await fileExists(projectMdPath)) {
|
|
2526
2668
|
return null;
|
|
2527
2669
|
}
|
|
2528
|
-
const projectContent = await
|
|
2670
|
+
const projectContent = await readFile7(projectMdPath, "utf-8");
|
|
2529
2671
|
const project = parseProject(projectContent);
|
|
2530
2672
|
const assignments = await listAssignmentRecords(projectPath);
|
|
2531
2673
|
const rollup = await buildProjectRollup(projectPath, project, assignments);
|
|
@@ -2555,17 +2697,17 @@ async function getProjectDetail(projectsDir, slug) {
|
|
|
2555
2697
|
};
|
|
2556
2698
|
}
|
|
2557
2699
|
async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
2558
|
-
const assignmentDir =
|
|
2559
|
-
const assignmentMdPath =
|
|
2700
|
+
const assignmentDir = resolve8(projectsDir, projectSlug, "assignments", assignmentSlug);
|
|
2701
|
+
const assignmentMdPath = resolve8(assignmentDir, "assignment.md");
|
|
2560
2702
|
if (!await fileExists(assignmentMdPath)) {
|
|
2561
2703
|
return null;
|
|
2562
2704
|
}
|
|
2563
|
-
const assignmentContent = await
|
|
2705
|
+
const assignmentContent = await readFile7(assignmentMdPath, "utf-8");
|
|
2564
2706
|
const assignment = parseAssignmentFull(assignmentContent);
|
|
2565
2707
|
let plan = null;
|
|
2566
|
-
const planPath =
|
|
2708
|
+
const planPath = resolve8(assignmentDir, "plan.md");
|
|
2567
2709
|
if (await fileExists(planPath)) {
|
|
2568
|
-
const planContent = await
|
|
2710
|
+
const planContent = await readFile7(planPath, "utf-8");
|
|
2569
2711
|
const parsed = parsePlan(planContent);
|
|
2570
2712
|
plan = {
|
|
2571
2713
|
status: parsed.status,
|
|
@@ -2574,9 +2716,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2574
2716
|
};
|
|
2575
2717
|
}
|
|
2576
2718
|
let scratchpad = null;
|
|
2577
|
-
const scratchpadPath =
|
|
2719
|
+
const scratchpadPath = resolve8(assignmentDir, "scratchpad.md");
|
|
2578
2720
|
if (await fileExists(scratchpadPath)) {
|
|
2579
|
-
const scratchpadContent = await
|
|
2721
|
+
const scratchpadContent = await readFile7(scratchpadPath, "utf-8");
|
|
2580
2722
|
const parsed = parseScratchpad(scratchpadContent);
|
|
2581
2723
|
scratchpad = {
|
|
2582
2724
|
updated: parsed.updated,
|
|
@@ -2584,9 +2726,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2584
2726
|
};
|
|
2585
2727
|
}
|
|
2586
2728
|
let handoff = null;
|
|
2587
|
-
const handoffPath =
|
|
2729
|
+
const handoffPath = resolve8(assignmentDir, "handoff.md");
|
|
2588
2730
|
if (await fileExists(handoffPath)) {
|
|
2589
|
-
const handoffContent = await
|
|
2731
|
+
const handoffContent = await readFile7(handoffPath, "utf-8");
|
|
2590
2732
|
const parsed = parseHandoff(handoffContent);
|
|
2591
2733
|
handoff = {
|
|
2592
2734
|
updated: parsed.updated,
|
|
@@ -2595,9 +2737,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2595
2737
|
};
|
|
2596
2738
|
}
|
|
2597
2739
|
let decisionRecord = null;
|
|
2598
|
-
const decisionRecordPath =
|
|
2740
|
+
const decisionRecordPath = resolve8(assignmentDir, "decision-record.md");
|
|
2599
2741
|
if (await fileExists(decisionRecordPath)) {
|
|
2600
|
-
const decisionRecordContent = await
|
|
2742
|
+
const decisionRecordContent = await readFile7(decisionRecordPath, "utf-8");
|
|
2601
2743
|
const parsed = parseDecisionRecord(decisionRecordContent);
|
|
2602
2744
|
decisionRecord = {
|
|
2603
2745
|
updated: parsed.updated,
|
|
@@ -2606,9 +2748,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2606
2748
|
};
|
|
2607
2749
|
}
|
|
2608
2750
|
let progress = null;
|
|
2609
|
-
const progressPath =
|
|
2751
|
+
const progressPath = resolve8(assignmentDir, "progress.md");
|
|
2610
2752
|
if (await fileExists(progressPath)) {
|
|
2611
|
-
const progressContent = await
|
|
2753
|
+
const progressContent = await readFile7(progressPath, "utf-8");
|
|
2612
2754
|
const parsed = parseProgress(progressContent);
|
|
2613
2755
|
progress = {
|
|
2614
2756
|
updated: parsed.updated,
|
|
@@ -2617,9 +2759,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2617
2759
|
};
|
|
2618
2760
|
}
|
|
2619
2761
|
let comments = null;
|
|
2620
|
-
const commentsPath =
|
|
2762
|
+
const commentsPath = resolve8(assignmentDir, "comments.md");
|
|
2621
2763
|
if (await fileExists(commentsPath)) {
|
|
2622
|
-
const commentsContent = await
|
|
2764
|
+
const commentsContent = await readFile7(commentsPath, "utf-8");
|
|
2623
2765
|
const parsed = parseComments(commentsContent);
|
|
2624
2766
|
comments = {
|
|
2625
2767
|
updated: parsed.updated,
|
|
@@ -2733,7 +2875,7 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
|
|
|
2733
2875
|
slug: a.slug,
|
|
2734
2876
|
title: a.title,
|
|
2735
2877
|
projectSlug: rec.summary.slug,
|
|
2736
|
-
assignmentDir:
|
|
2878
|
+
assignmentDir: resolve8(rec.projectPath, "assignments", a.slug)
|
|
2737
2879
|
});
|
|
2738
2880
|
}
|
|
2739
2881
|
}
|
|
@@ -2766,17 +2908,17 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
|
|
|
2766
2908
|
}
|
|
2767
2909
|
async function countMentionsInAssignment(sourceDir, target) {
|
|
2768
2910
|
const bodies = [];
|
|
2769
|
-
const assignmentMd =
|
|
2911
|
+
const assignmentMd = resolve8(sourceDir, "assignment.md");
|
|
2770
2912
|
if (await fileExists(assignmentMd)) {
|
|
2771
|
-
const content = await
|
|
2913
|
+
const content = await readFile7(assignmentMd, "utf-8");
|
|
2772
2914
|
const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
|
|
2773
2915
|
if (todosMatch) bodies.push(todosMatch[1]);
|
|
2774
2916
|
}
|
|
2775
2917
|
for (const filename of ["progress.md", "comments.md", "handoff.md"]) {
|
|
2776
|
-
const path =
|
|
2918
|
+
const path = resolve8(sourceDir, filename);
|
|
2777
2919
|
if (await fileExists(path)) {
|
|
2778
2920
|
try {
|
|
2779
|
-
bodies.push(await
|
|
2921
|
+
bodies.push(await readFile7(path, "utf-8"));
|
|
2780
2922
|
} catch {
|
|
2781
2923
|
}
|
|
2782
2924
|
}
|
|
@@ -2834,44 +2976,44 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir, id) {
|
|
|
2834
2976
|
}
|
|
2835
2977
|
async function buildStandaloneAssignmentDetail(resolved) {
|
|
2836
2978
|
const assignmentDir = resolved.assignmentDir;
|
|
2837
|
-
const assignmentMdPath =
|
|
2979
|
+
const assignmentMdPath = resolve8(assignmentDir, "assignment.md");
|
|
2838
2980
|
if (!await fileExists(assignmentMdPath)) return null;
|
|
2839
|
-
const assignmentContent = await
|
|
2981
|
+
const assignmentContent = await readFile7(assignmentMdPath, "utf-8");
|
|
2840
2982
|
const assignment = parseAssignmentFull(assignmentContent);
|
|
2841
2983
|
let plan = null;
|
|
2842
|
-
const planPath =
|
|
2984
|
+
const planPath = resolve8(assignmentDir, "plan.md");
|
|
2843
2985
|
if (await fileExists(planPath)) {
|
|
2844
|
-
const parsed = parsePlan(await
|
|
2986
|
+
const parsed = parsePlan(await readFile7(planPath, "utf-8"));
|
|
2845
2987
|
plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
|
|
2846
2988
|
}
|
|
2847
2989
|
let scratchpad = null;
|
|
2848
|
-
const scratchpadPath =
|
|
2990
|
+
const scratchpadPath = resolve8(assignmentDir, "scratchpad.md");
|
|
2849
2991
|
if (await fileExists(scratchpadPath)) {
|
|
2850
|
-
const parsed = parseScratchpad(await
|
|
2992
|
+
const parsed = parseScratchpad(await readFile7(scratchpadPath, "utf-8"));
|
|
2851
2993
|
scratchpad = { updated: parsed.updated, body: parsed.body };
|
|
2852
2994
|
}
|
|
2853
2995
|
let handoff = null;
|
|
2854
|
-
const handoffPath =
|
|
2996
|
+
const handoffPath = resolve8(assignmentDir, "handoff.md");
|
|
2855
2997
|
if (await fileExists(handoffPath)) {
|
|
2856
|
-
const parsed = parseHandoff(await
|
|
2998
|
+
const parsed = parseHandoff(await readFile7(handoffPath, "utf-8"));
|
|
2857
2999
|
handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
|
|
2858
3000
|
}
|
|
2859
3001
|
let decisionRecord = null;
|
|
2860
|
-
const decisionRecordPath =
|
|
3002
|
+
const decisionRecordPath = resolve8(assignmentDir, "decision-record.md");
|
|
2861
3003
|
if (await fileExists(decisionRecordPath)) {
|
|
2862
|
-
const parsed = parseDecisionRecord(await
|
|
3004
|
+
const parsed = parseDecisionRecord(await readFile7(decisionRecordPath, "utf-8"));
|
|
2863
3005
|
decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
|
|
2864
3006
|
}
|
|
2865
3007
|
let progress = null;
|
|
2866
|
-
const progressPath =
|
|
3008
|
+
const progressPath = resolve8(assignmentDir, "progress.md");
|
|
2867
3009
|
if (await fileExists(progressPath)) {
|
|
2868
|
-
const parsed = parseProgress(await
|
|
3010
|
+
const parsed = parseProgress(await readFile7(progressPath, "utf-8"));
|
|
2869
3011
|
progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
|
|
2870
3012
|
}
|
|
2871
3013
|
let comments = null;
|
|
2872
|
-
const commentsPath =
|
|
3014
|
+
const commentsPath = resolve8(assignmentDir, "comments.md");
|
|
2873
3015
|
if (await fileExists(commentsPath)) {
|
|
2874
|
-
const parsed = parseComments(await
|
|
3016
|
+
const parsed = parseComments(await readFile7(commentsPath, "utf-8"));
|
|
2875
3017
|
comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
|
|
2876
3018
|
}
|
|
2877
3019
|
const detail = {
|
|
@@ -2909,16 +3051,20 @@ async function listProjectRecords(projectsDir) {
|
|
|
2909
3051
|
if (!await fileExists(projectsDir)) {
|
|
2910
3052
|
return [];
|
|
2911
3053
|
}
|
|
2912
|
-
|
|
3054
|
+
if (!migratedProjectsDirs.has(projectsDir)) {
|
|
3055
|
+
migratedProjectsDirs.add(projectsDir);
|
|
3056
|
+
await migrateLegacyProjectFiles(projectsDir);
|
|
3057
|
+
}
|
|
3058
|
+
const entries = await readdir5(projectsDir, { withFileTypes: true });
|
|
2913
3059
|
const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
|
|
2914
3060
|
const records = [];
|
|
2915
3061
|
for (const entry of projectDirs) {
|
|
2916
|
-
const projectPath =
|
|
2917
|
-
const projectMdPath =
|
|
3062
|
+
const projectPath = resolve8(projectsDir, entry.name);
|
|
3063
|
+
const projectMdPath = resolve8(projectPath, "project.md");
|
|
2918
3064
|
if (!await fileExists(projectMdPath)) {
|
|
2919
3065
|
continue;
|
|
2920
3066
|
}
|
|
2921
|
-
const projectContent = await
|
|
3067
|
+
const projectContent = await readFile7(projectMdPath, "utf-8");
|
|
2922
3068
|
const project = parseProject(projectContent);
|
|
2923
3069
|
const assignments = await listAssignmentRecords(projectPath);
|
|
2924
3070
|
const rollup = await buildProjectRollup(projectPath, project, assignments);
|
|
@@ -2949,39 +3095,39 @@ async function listProjectRecords(projectsDir) {
|
|
|
2949
3095
|
return records;
|
|
2950
3096
|
}
|
|
2951
3097
|
async function listAssignmentRecords(projectPath) {
|
|
2952
|
-
const assignmentsDir =
|
|
3098
|
+
const assignmentsDir = resolve8(projectPath, "assignments");
|
|
2953
3099
|
if (!await fileExists(assignmentsDir)) {
|
|
2954
3100
|
return [];
|
|
2955
3101
|
}
|
|
2956
|
-
const entries = await
|
|
3102
|
+
const entries = await readdir5(assignmentsDir, { withFileTypes: true });
|
|
2957
3103
|
const records = [];
|
|
2958
3104
|
for (const entry of entries) {
|
|
2959
3105
|
if (!entry.isDirectory()) {
|
|
2960
3106
|
continue;
|
|
2961
3107
|
}
|
|
2962
|
-
const assignmentMd =
|
|
3108
|
+
const assignmentMd = resolve8(assignmentsDir, entry.name, "assignment.md");
|
|
2963
3109
|
if (!await fileExists(assignmentMd)) {
|
|
2964
3110
|
continue;
|
|
2965
3111
|
}
|
|
2966
|
-
const content = await
|
|
3112
|
+
const content = await readFile7(assignmentMd, "utf-8");
|
|
2967
3113
|
records.push(parseAssignmentFull(content));
|
|
2968
3114
|
}
|
|
2969
3115
|
records.sort((left, right) => compareTimestamps(right.updated, left.updated));
|
|
2970
3116
|
return records;
|
|
2971
3117
|
}
|
|
2972
3118
|
async function listResources(projectPath) {
|
|
2973
|
-
const resourcesDir =
|
|
3119
|
+
const resourcesDir = resolve8(projectPath, "resources");
|
|
2974
3120
|
if (!await fileExists(resourcesDir)) {
|
|
2975
3121
|
return [];
|
|
2976
3122
|
}
|
|
2977
|
-
const entries = await
|
|
3123
|
+
const entries = await readdir5(resourcesDir, { withFileTypes: true });
|
|
2978
3124
|
const results = [];
|
|
2979
3125
|
for (const entry of entries) {
|
|
2980
3126
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
|
|
2981
3127
|
continue;
|
|
2982
3128
|
}
|
|
2983
|
-
const filePath =
|
|
2984
|
-
const content = await
|
|
3129
|
+
const filePath = resolve8(resourcesDir, entry.name);
|
|
3130
|
+
const content = await readFile7(filePath, "utf-8");
|
|
2985
3131
|
const parsed = parseResource(content);
|
|
2986
3132
|
results.push({
|
|
2987
3133
|
name: parsed.name,
|
|
@@ -2996,18 +3142,18 @@ async function listResources(projectPath) {
|
|
|
2996
3142
|
return results;
|
|
2997
3143
|
}
|
|
2998
3144
|
async function listMemories(projectPath) {
|
|
2999
|
-
const memoriesDir =
|
|
3145
|
+
const memoriesDir = resolve8(projectPath, "memories");
|
|
3000
3146
|
if (!await fileExists(memoriesDir)) {
|
|
3001
3147
|
return [];
|
|
3002
3148
|
}
|
|
3003
|
-
const entries = await
|
|
3149
|
+
const entries = await readdir5(memoriesDir, { withFileTypes: true });
|
|
3004
3150
|
const results = [];
|
|
3005
3151
|
for (const entry of entries) {
|
|
3006
3152
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
|
|
3007
3153
|
continue;
|
|
3008
3154
|
}
|
|
3009
|
-
const filePath =
|
|
3010
|
-
const content = await
|
|
3155
|
+
const filePath = resolve8(memoriesDir, entry.name);
|
|
3156
|
+
const content = await readFile7(filePath, "utf-8");
|
|
3011
3157
|
const parsed = parseMemory(content);
|
|
3012
3158
|
results.push({
|
|
3013
3159
|
name: parsed.name,
|
|
@@ -3022,9 +3168,9 @@ async function listMemories(projectPath) {
|
|
|
3022
3168
|
return results;
|
|
3023
3169
|
}
|
|
3024
3170
|
async function loadDependencyGraph(projectPath, assignments) {
|
|
3025
|
-
const statusPath =
|
|
3171
|
+
const statusPath = resolve8(projectPath, "_status.md");
|
|
3026
3172
|
if (await fileExists(statusPath)) {
|
|
3027
|
-
const statusContent = await
|
|
3173
|
+
const statusContent = await readFile7(statusPath, "utf-8");
|
|
3028
3174
|
const parsed = parseStatus(statusContent);
|
|
3029
3175
|
const derivedGraph = extractMermaidGraph(parsed.body);
|
|
3030
3176
|
if (derivedGraph) {
|
|
@@ -3124,7 +3270,7 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
|
|
|
3124
3270
|
const config = await getStatusConfig();
|
|
3125
3271
|
const transitionDefs = getTransitionDefinitions(config);
|
|
3126
3272
|
const actions = [];
|
|
3127
|
-
const projectPath =
|
|
3273
|
+
const projectPath = resolve8(projectsDir, projectSlug);
|
|
3128
3274
|
for (const definition of transitionDefs) {
|
|
3129
3275
|
let warning = null;
|
|
3130
3276
|
if (definition.command === "start" && !assignment.assignee) {
|
|
@@ -3154,12 +3300,12 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses) {
|
|
|
3154
3300
|
const terminals = terminalStatuses ?? /* @__PURE__ */ new Set(["completed"]);
|
|
3155
3301
|
const unmet = [];
|
|
3156
3302
|
for (const dependency of dependsOn) {
|
|
3157
|
-
const dependencyPath =
|
|
3303
|
+
const dependencyPath = resolve8(projectPath, "assignments", dependency, "assignment.md");
|
|
3158
3304
|
if (!await fileExists(dependencyPath)) {
|
|
3159
3305
|
unmet.push(`${dependency} (missing)`);
|
|
3160
3306
|
continue;
|
|
3161
3307
|
}
|
|
3162
|
-
const content = await
|
|
3308
|
+
const content = await readFile7(dependencyPath, "utf-8");
|
|
3163
3309
|
const parsed = parseAssignmentFull(content);
|
|
3164
3310
|
if (!terminals.has(parsed.status)) {
|
|
3165
3311
|
unmet.push(`${dependency} (${parsed.status})`);
|
|
@@ -3314,7 +3460,7 @@ function isStale(updated) {
|
|
|
3314
3460
|
return Date.now() - timestamp > STALE_ASSIGNMENT_MS;
|
|
3315
3461
|
}
|
|
3316
3462
|
async function countOpenQuestions(projectPath, assignmentSlug) {
|
|
3317
|
-
const commentsPath =
|
|
3463
|
+
const commentsPath = resolve8(
|
|
3318
3464
|
projectPath,
|
|
3319
3465
|
"assignments",
|
|
3320
3466
|
assignmentSlug,
|
|
@@ -3324,7 +3470,7 @@ async function countOpenQuestions(projectPath, assignmentSlug) {
|
|
|
3324
3470
|
return 0;
|
|
3325
3471
|
}
|
|
3326
3472
|
try {
|
|
3327
|
-
const content = await
|
|
3473
|
+
const content = await readFile7(commentsPath, "utf-8");
|
|
3328
3474
|
const parsed = parseComments(content);
|
|
3329
3475
|
return parsed.entries.filter(
|
|
3330
3476
|
(e) => e.type === "question" && e.resolved !== true
|
|
@@ -3345,17 +3491,17 @@ function getProjectActivityTimestamp(projectUpdated, assignments) {
|
|
|
3345
3491
|
function getDocumentPath(projectsDir, documentType, projectSlug, assignmentSlug) {
|
|
3346
3492
|
switch (documentType) {
|
|
3347
3493
|
case "project":
|
|
3348
|
-
return
|
|
3494
|
+
return resolve8(projectsDir, projectSlug, "project.md");
|
|
3349
3495
|
case "assignment":
|
|
3350
|
-
return assignmentSlug ?
|
|
3496
|
+
return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
|
|
3351
3497
|
case "plan":
|
|
3352
|
-
return assignmentSlug ?
|
|
3498
|
+
return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
|
|
3353
3499
|
case "scratchpad":
|
|
3354
|
-
return assignmentSlug ?
|
|
3500
|
+
return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
|
|
3355
3501
|
case "handoff":
|
|
3356
|
-
return assignmentSlug ?
|
|
3502
|
+
return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
|
|
3357
3503
|
case "decision-record":
|
|
3358
|
-
return assignmentSlug ?
|
|
3504
|
+
return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
|
|
3359
3505
|
default:
|
|
3360
3506
|
return null;
|
|
3361
3507
|
}
|
|
@@ -3382,12 +3528,12 @@ function getEditableDocumentTitle(documentType, projectSlug, assignmentSlug) {
|
|
|
3382
3528
|
}
|
|
3383
3529
|
async function listPlaybooks(playbooksDir2) {
|
|
3384
3530
|
if (!await fileExists(playbooksDir2)) return [];
|
|
3385
|
-
const entries = await
|
|
3531
|
+
const entries = await readdir5(playbooksDir2, { withFileTypes: true });
|
|
3386
3532
|
const playbooks = [];
|
|
3387
3533
|
for (const entry of entries) {
|
|
3388
3534
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
|
|
3389
|
-
const filePath =
|
|
3390
|
-
const raw = await
|
|
3535
|
+
const filePath = resolve8(playbooksDir2, entry.name);
|
|
3536
|
+
const raw = await readFile7(filePath, "utf-8");
|
|
3391
3537
|
const parsed = parsePlaybook(raw);
|
|
3392
3538
|
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
3393
3539
|
playbooks.push({
|
|
@@ -3403,9 +3549,9 @@ async function listPlaybooks(playbooksDir2) {
|
|
|
3403
3549
|
return playbooks.sort((a, b) => (b.updated || b.created).localeCompare(a.updated || a.created));
|
|
3404
3550
|
}
|
|
3405
3551
|
async function getPlaybookDetail(playbooksDir2, slug) {
|
|
3406
|
-
const filePath =
|
|
3552
|
+
const filePath = resolve8(playbooksDir2, `${slug}.md`);
|
|
3407
3553
|
if (!await fileExists(filePath)) return null;
|
|
3408
|
-
const raw = await
|
|
3554
|
+
const raw = await readFile7(filePath, "utf-8");
|
|
3409
3555
|
const parsed = parsePlaybook(raw);
|
|
3410
3556
|
return {
|
|
3411
3557
|
slug: parsed.slug || slug,
|
|
@@ -3418,13 +3564,14 @@ async function getPlaybookDetail(playbooksDir2, slug) {
|
|
|
3418
3564
|
body: parsed.body
|
|
3419
3565
|
};
|
|
3420
3566
|
}
|
|
3421
|
-
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, DEFAULT_GRAPH_COLORS;
|
|
3567
|
+
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;
|
|
3422
3568
|
var init_api = __esm({
|
|
3423
3569
|
"src/dashboard/api.ts"() {
|
|
3424
3570
|
"use strict";
|
|
3425
3571
|
init_lifecycle();
|
|
3426
3572
|
init_fs();
|
|
3427
3573
|
init_config2();
|
|
3574
|
+
init_fs_migration();
|
|
3428
3575
|
init_assignment_resolver();
|
|
3429
3576
|
init_parser();
|
|
3430
3577
|
init_help();
|
|
@@ -3487,6 +3634,7 @@ var init_api = __esm({
|
|
|
3487
3634
|
};
|
|
3488
3635
|
_cachedConfig = null;
|
|
3489
3636
|
REFERENCED_BY_LIMIT = 50;
|
|
3637
|
+
migratedProjectsDirs = /* @__PURE__ */ new Set();
|
|
3490
3638
|
DEFAULT_GRAPH_COLORS = {
|
|
3491
3639
|
completed: "fill:#4ea84f,stroke:#1f6b29,color:#ffffff",
|
|
3492
3640
|
in_progress: "fill:#1e6fd9,stroke:#0f3f8f,color:#ffffff",
|
|
@@ -3519,8 +3667,8 @@ __export(parser_exports, {
|
|
|
3519
3667
|
writeChecklist: () => writeChecklist
|
|
3520
3668
|
});
|
|
3521
3669
|
import { randomBytes } from "crypto";
|
|
3522
|
-
import { readFile as
|
|
3523
|
-
import { resolve as
|
|
3670
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
3671
|
+
import { resolve as resolve15 } from "path";
|
|
3524
3672
|
function generateShortId() {
|
|
3525
3673
|
return randomBytes(2).toString("hex");
|
|
3526
3674
|
}
|
|
@@ -3680,10 +3828,10 @@ function serializeLogEntry(entry) {
|
|
|
3680
3828
|
return lines.join("\n");
|
|
3681
3829
|
}
|
|
3682
3830
|
function checklistPath(todosDir2, workspace) {
|
|
3683
|
-
return
|
|
3831
|
+
return resolve15(todosDir2, `${workspace}.md`);
|
|
3684
3832
|
}
|
|
3685
3833
|
function logPath(todosDir2, workspace) {
|
|
3686
|
-
return
|
|
3834
|
+
return resolve15(todosDir2, `${workspace}-log.md`);
|
|
3687
3835
|
}
|
|
3688
3836
|
function archivePath(todosDir2, workspace, interval, now = /* @__PURE__ */ new Date()) {
|
|
3689
3837
|
const year = now.getFullYear();
|
|
@@ -3707,14 +3855,14 @@ function archivePath(todosDir2, workspace, interval, now = /* @__PURE__ */ new D
|
|
|
3707
3855
|
default:
|
|
3708
3856
|
suffix = `${year}-${month}-${day}`;
|
|
3709
3857
|
}
|
|
3710
|
-
return
|
|
3858
|
+
return resolve15(todosDir2, "archive", `${workspace}-${suffix}.md`);
|
|
3711
3859
|
}
|
|
3712
3860
|
async function readChecklist(todosDir2, workspace) {
|
|
3713
3861
|
const path = checklistPath(todosDir2, workspace);
|
|
3714
3862
|
if (!await fileExists(path)) {
|
|
3715
3863
|
return { workspace, archiveInterval: "weekly", items: [] };
|
|
3716
3864
|
}
|
|
3717
|
-
const content = await
|
|
3865
|
+
const content = await readFile12(path, "utf-8");
|
|
3718
3866
|
return parseChecklist(content);
|
|
3719
3867
|
}
|
|
3720
3868
|
async function writeChecklist(todosDir2, checklist) {
|
|
@@ -3727,7 +3875,7 @@ async function readLog(todosDir2, workspace) {
|
|
|
3727
3875
|
if (!await fileExists(path)) {
|
|
3728
3876
|
return { workspace, entries: [] };
|
|
3729
3877
|
}
|
|
3730
|
-
const content = await
|
|
3878
|
+
const content = await readFile12(path, "utf-8");
|
|
3731
3879
|
return parseLog(content);
|
|
3732
3880
|
}
|
|
3733
3881
|
async function appendLogEntry2(todosDir2, workspace, entry) {
|
|
@@ -3735,7 +3883,7 @@ async function appendLogEntry2(todosDir2, workspace, entry) {
|
|
|
3735
3883
|
const path = logPath(todosDir2, workspace);
|
|
3736
3884
|
let content;
|
|
3737
3885
|
if (await fileExists(path)) {
|
|
3738
|
-
content = await
|
|
3886
|
+
content = await readFile12(path, "utf-8");
|
|
3739
3887
|
content = content.trimEnd() + "\n\n" + serializeLogEntry(entry) + "\n";
|
|
3740
3888
|
} else {
|
|
3741
3889
|
const fm = `---
|
|
@@ -3774,21 +3922,21 @@ init_api();
|
|
|
3774
3922
|
init_assignment_resolver();
|
|
3775
3923
|
import express from "express";
|
|
3776
3924
|
import { createServer } from "http";
|
|
3777
|
-
import { resolve as
|
|
3778
|
-
import { writeFile as
|
|
3925
|
+
import { resolve as resolve17 } from "path";
|
|
3926
|
+
import { writeFile as writeFile5, unlink as unlink4 } from "fs/promises";
|
|
3779
3927
|
import { WebSocketServer, WebSocket } from "ws";
|
|
3780
3928
|
|
|
3781
3929
|
// src/dashboard/agent-sessions.ts
|
|
3782
3930
|
init_fs();
|
|
3783
|
-
import { readFile as
|
|
3784
|
-
import { resolve as
|
|
3931
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
3932
|
+
import { resolve as resolve10 } from "path";
|
|
3785
3933
|
|
|
3786
3934
|
// src/dashboard/session-db.ts
|
|
3787
3935
|
init_paths();
|
|
3788
3936
|
init_fs();
|
|
3789
3937
|
import Database from "better-sqlite3";
|
|
3790
|
-
import { resolve as
|
|
3791
|
-
import { readdir as
|
|
3938
|
+
import { resolve as resolve9 } from "path";
|
|
3939
|
+
import { readdir as readdir6 } from "fs/promises";
|
|
3792
3940
|
var db = null;
|
|
3793
3941
|
var SCHEMA_VERSION = "3";
|
|
3794
3942
|
var SCHEMA_SQL = `
|
|
@@ -3806,14 +3954,16 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
3806
3954
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3807
3955
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
3808
3956
|
);
|
|
3809
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
|
|
3810
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
|
|
3811
3957
|
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
3812
3958
|
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
|
|
3813
3959
|
`;
|
|
3960
|
+
var POST_MIGRATION_INDEXES_SQL = `
|
|
3961
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
|
|
3962
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
|
|
3963
|
+
`;
|
|
3814
3964
|
function initSessionDb(dbPath) {
|
|
3815
3965
|
if (db) return db;
|
|
3816
|
-
const finalPath = dbPath ??
|
|
3966
|
+
const finalPath = dbPath ?? resolve9(syntaurRoot(), "syntaur.db");
|
|
3817
3967
|
db = new Database(finalPath);
|
|
3818
3968
|
db.pragma("journal_mode = WAL");
|
|
3819
3969
|
db.exec(SCHEMA_SQL);
|
|
@@ -3821,57 +3971,74 @@ function initSessionDb(dbPath) {
|
|
|
3821
3971
|
"schema_version",
|
|
3822
3972
|
SCHEMA_VERSION
|
|
3823
3973
|
);
|
|
3824
|
-
const
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3974
|
+
const database = db;
|
|
3975
|
+
const runMigrations = database.transaction(() => {
|
|
3976
|
+
const vBeforeV2 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
|
|
3977
|
+
if (vBeforeV2 === "1") {
|
|
3978
|
+
database.exec(`
|
|
3979
|
+
CREATE TABLE sessions_v2 (
|
|
3980
|
+
session_id TEXT PRIMARY KEY,
|
|
3981
|
+
project_slug TEXT,
|
|
3982
|
+
assignment_slug TEXT,
|
|
3983
|
+
agent TEXT NOT NULL,
|
|
3984
|
+
started TEXT NOT NULL,
|
|
3985
|
+
ended TEXT,
|
|
3986
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
3987
|
+
path TEXT,
|
|
3988
|
+
description TEXT,
|
|
3989
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3990
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
3991
|
+
);
|
|
3992
|
+
INSERT INTO sessions_v2 SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, NULL, created_at, updated_at FROM sessions;
|
|
3993
|
+
DROP TABLE sessions;
|
|
3994
|
+
ALTER TABLE sessions_v2 RENAME TO sessions;
|
|
3995
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
|
|
3996
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
|
|
3997
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
3998
|
+
UPDATE meta SET value = '2' WHERE key = 'schema_version';
|
|
3999
|
+
`);
|
|
4000
|
+
}
|
|
4001
|
+
const vBeforeV3 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
|
|
4002
|
+
if (vBeforeV3 === "2") {
|
|
4003
|
+
const v2Columns = database.prepare("PRAGMA table_info(sessions)").all();
|
|
4004
|
+
const v2ColNames = v2Columns.map((c) => c.name);
|
|
4005
|
+
const hasProject = v2ColNames.includes("project_slug");
|
|
4006
|
+
const hasMission = v2ColNames.includes("mission_slug");
|
|
4007
|
+
const projectSlugExpr = hasProject && hasMission ? "COALESCE(project_slug, mission_slug)" : hasProject ? "project_slug" : hasMission ? "mission_slug" : null;
|
|
4008
|
+
if (!projectSlugExpr) {
|
|
4009
|
+
throw new Error(
|
|
4010
|
+
"sessions table has neither project_slug nor mission_slug; cannot migrate from v2 to v3"
|
|
4011
|
+
);
|
|
4012
|
+
}
|
|
4013
|
+
database.exec(`
|
|
4014
|
+
CREATE TABLE sessions_v3 (
|
|
4015
|
+
session_id TEXT PRIMARY KEY,
|
|
4016
|
+
project_slug TEXT,
|
|
4017
|
+
assignment_slug TEXT,
|
|
4018
|
+
agent TEXT NOT NULL,
|
|
4019
|
+
started TEXT NOT NULL,
|
|
4020
|
+
ended TEXT,
|
|
4021
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
4022
|
+
path TEXT,
|
|
4023
|
+
description TEXT,
|
|
4024
|
+
transcript_path TEXT,
|
|
4025
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4026
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4027
|
+
);
|
|
4028
|
+
INSERT INTO sessions_v3
|
|
4029
|
+
SELECT session_id, ${projectSlugExpr}, assignment_slug, agent, started, ended, status, path, description, NULL, created_at, updated_at
|
|
4030
|
+
FROM sessions;
|
|
4031
|
+
DROP TABLE sessions;
|
|
4032
|
+
ALTER TABLE sessions_v3 RENAME TO sessions;
|
|
4033
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
|
|
4034
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
|
|
4035
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
4036
|
+
UPDATE meta SET value = '3' WHERE key = 'schema_version';
|
|
4037
|
+
`);
|
|
4038
|
+
}
|
|
4039
|
+
});
|
|
4040
|
+
runMigrations.exclusive();
|
|
4041
|
+
db.exec(POST_MIGRATION_INDEXES_SQL);
|
|
3875
4042
|
return db;
|
|
3876
4043
|
}
|
|
3877
4044
|
function getSessionDb() {
|
|
@@ -3893,12 +4060,12 @@ async function migrateFromMarkdown(projectsDir) {
|
|
|
3893
4060
|
const count = database.prepare("SELECT COUNT(*) as count FROM sessions").get();
|
|
3894
4061
|
if (count.count > 0) return 0;
|
|
3895
4062
|
if (!await fileExists(projectsDir)) return 0;
|
|
3896
|
-
const entries = await
|
|
4063
|
+
const entries = await readdir6(projectsDir, { withFileTypes: true });
|
|
3897
4064
|
const allSessions = [];
|
|
3898
4065
|
for (const entry of entries) {
|
|
3899
4066
|
if (!entry.isDirectory()) continue;
|
|
3900
|
-
const projectDir =
|
|
3901
|
-
const indexPath =
|
|
4067
|
+
const projectDir = resolve9(projectsDir, entry.name);
|
|
4068
|
+
const indexPath = resolve9(projectDir, "_index-sessions.md");
|
|
3902
4069
|
if (!await fileExists(indexPath)) continue;
|
|
3903
4070
|
const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
|
|
3904
4071
|
allSessions.push(...sessions);
|
|
@@ -3918,8 +4085,8 @@ async function migrateFromMarkdown(projectsDir) {
|
|
|
3918
4085
|
return allSessions.length;
|
|
3919
4086
|
}
|
|
3920
4087
|
async function parseMarkdownSessionsIndex(filePath, projectSlug) {
|
|
3921
|
-
const { readFile:
|
|
3922
|
-
const raw = await
|
|
4088
|
+
const { readFile: readFile14 } = await import("fs/promises");
|
|
4089
|
+
const raw = await readFile14(filePath, "utf-8");
|
|
3923
4090
|
const sessions = [];
|
|
3924
4091
|
const lines = raw.split("\n");
|
|
3925
4092
|
let inTable = false;
|
|
@@ -3975,13 +4142,13 @@ async function appendSession(_projectDir, session) {
|
|
|
3975
4142
|
INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path)
|
|
3976
4143
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3977
4144
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
3978
|
-
project_slug = COALESCE(excluded.project_slug, project_slug),
|
|
3979
|
-
assignment_slug = COALESCE(excluded.assignment_slug, assignment_slug),
|
|
4145
|
+
project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),
|
|
4146
|
+
assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),
|
|
3980
4147
|
agent = excluded.agent,
|
|
3981
4148
|
status = CASE WHEN status IN ('completed','stopped') THEN status ELSE excluded.status END,
|
|
3982
|
-
path = COALESCE(excluded.path, path),
|
|
3983
|
-
description = COALESCE(excluded.description, description),
|
|
3984
|
-
transcript_path = COALESCE(excluded.transcript_path, transcript_path),
|
|
4149
|
+
path = COALESCE(NULLIF(excluded.path, ''), path),
|
|
4150
|
+
description = COALESCE(NULLIF(excluded.description, ''), description),
|
|
4151
|
+
transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),
|
|
3985
4152
|
updated_at = datetime('now')
|
|
3986
4153
|
`).run(
|
|
3987
4154
|
session.sessionId,
|
|
@@ -4031,13 +4198,13 @@ async function deleteSessions(sessionIds) {
|
|
|
4031
4198
|
var DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
|
|
4032
4199
|
async function readAssignmentStatusFromPath(assignmentMdPath) {
|
|
4033
4200
|
if (!await fileExists(assignmentMdPath)) return null;
|
|
4034
|
-
const raw = await
|
|
4201
|
+
const raw = await readFile8(assignmentMdPath, "utf-8");
|
|
4035
4202
|
const match = raw.match(/^status:\s*(.+)$/m);
|
|
4036
4203
|
return match ? match[1].trim() : null;
|
|
4037
4204
|
}
|
|
4038
4205
|
async function readAssignmentStatus(projectDir, assignmentSlug) {
|
|
4039
4206
|
return readAssignmentStatusFromPath(
|
|
4040
|
-
|
|
4207
|
+
resolve10(projectDir, "assignments", assignmentSlug, "assignment.md")
|
|
4041
4208
|
);
|
|
4042
4209
|
}
|
|
4043
4210
|
async function reconcileActiveSessions(projectsDir, assignmentsDir) {
|
|
@@ -4055,13 +4222,13 @@ async function reconcileActiveSessions(projectsDir, assignmentsDir) {
|
|
|
4055
4222
|
seen.add(key);
|
|
4056
4223
|
if (session.project_slug) {
|
|
4057
4224
|
const status = await readAssignmentStatus(
|
|
4058
|
-
|
|
4225
|
+
resolve10(projectsDir, session.project_slug),
|
|
4059
4226
|
aslug
|
|
4060
4227
|
);
|
|
4061
4228
|
if (status) assignmentStatuses.set(key, status);
|
|
4062
4229
|
} else if (assignmentsDir) {
|
|
4063
4230
|
const status = await readAssignmentStatusFromPath(
|
|
4064
|
-
|
|
4231
|
+
resolve10(assignmentsDir, aslug, "assignment.md")
|
|
4065
4232
|
);
|
|
4066
4233
|
if (status) assignmentStatuses.set(key, status);
|
|
4067
4234
|
}
|
|
@@ -4275,8 +4442,8 @@ init_config2();
|
|
|
4275
4442
|
// src/dashboard/api-write.ts
|
|
4276
4443
|
init_lifecycle();
|
|
4277
4444
|
import { Router } from "express";
|
|
4278
|
-
import { resolve as
|
|
4279
|
-
import { rm, readFile as
|
|
4445
|
+
import { resolve as resolve11 } from "path";
|
|
4446
|
+
import { rm, readFile as readFile9 } from "fs/promises";
|
|
4280
4447
|
|
|
4281
4448
|
// src/utils/slug.ts
|
|
4282
4449
|
function isValidSlug(slug) {
|
|
@@ -4799,7 +4966,7 @@ async function readCurrentDocument(filePath) {
|
|
|
4799
4966
|
if (!await fileExists(filePath)) {
|
|
4800
4967
|
return null;
|
|
4801
4968
|
}
|
|
4802
|
-
return
|
|
4969
|
+
return readFile9(filePath, "utf-8");
|
|
4803
4970
|
}
|
|
4804
4971
|
function createWriteRouter(projectsDir, assignmentsDir) {
|
|
4805
4972
|
const router = Router();
|
|
@@ -4929,26 +5096,26 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
4929
5096
|
res.status(400).json({ error: `Invalid slug "${slug}". Must be lowercase and hyphen-separated.` });
|
|
4930
5097
|
return;
|
|
4931
5098
|
}
|
|
4932
|
-
const projectDir =
|
|
5099
|
+
const projectDir = resolve11(projectsDir, slug);
|
|
4933
5100
|
if (await fileExists(projectDir)) {
|
|
4934
5101
|
res.status(409).json({ error: `Project "${slug}" already exists` });
|
|
4935
5102
|
return;
|
|
4936
5103
|
}
|
|
4937
5104
|
const title = fields.title;
|
|
4938
5105
|
const timestamp = fields.created || nowTimestamp();
|
|
4939
|
-
await ensureDir(
|
|
4940
|
-
await ensureDir(
|
|
4941
|
-
await ensureDir(
|
|
4942
|
-
await writeFileForce(
|
|
5106
|
+
await ensureDir(resolve11(projectDir, "assignments"));
|
|
5107
|
+
await ensureDir(resolve11(projectDir, "resources"));
|
|
5108
|
+
await ensureDir(resolve11(projectDir, "memories"));
|
|
5109
|
+
await writeFileForce(resolve11(projectDir, "project.md"), content);
|
|
4943
5110
|
try {
|
|
4944
5111
|
const companions = [
|
|
4945
|
-
[
|
|
4946
|
-
[
|
|
4947
|
-
[
|
|
4948
|
-
[
|
|
4949
|
-
[
|
|
4950
|
-
[
|
|
4951
|
-
[
|
|
5112
|
+
[resolve11(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
|
|
5113
|
+
[resolve11(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
|
|
5114
|
+
[resolve11(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
|
|
5115
|
+
[resolve11(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
|
|
5116
|
+
[resolve11(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
|
|
5117
|
+
[resolve11(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
|
|
5118
|
+
[resolve11(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
|
|
4952
5119
|
];
|
|
4953
5120
|
for (const [filePath, fileContent] of companions) {
|
|
4954
5121
|
await writeFileForce(filePath, fileContent);
|
|
@@ -4969,8 +5136,8 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
4969
5136
|
router.post("/api/projects/:slug/assignments", async (req, res) => {
|
|
4970
5137
|
try {
|
|
4971
5138
|
const projectSlug = getParam(req.params.slug);
|
|
4972
|
-
const projectDir =
|
|
4973
|
-
const projectMdPath =
|
|
5139
|
+
const projectDir = resolve11(projectsDir, projectSlug);
|
|
5140
|
+
const projectMdPath = resolve11(projectDir, "project.md");
|
|
4974
5141
|
if (!await fileExists(projectMdPath)) {
|
|
4975
5142
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
4976
5143
|
return;
|
|
@@ -5000,7 +5167,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5000
5167
|
res.status(400).json({ error: `Invalid priority "${priority}". Must be low, medium, high, or critical.` });
|
|
5001
5168
|
return;
|
|
5002
5169
|
}
|
|
5003
|
-
const assignmentDir =
|
|
5170
|
+
const assignmentDir = resolve11(projectDir, "assignments", assignmentSlug);
|
|
5004
5171
|
if (await fileExists(assignmentDir)) {
|
|
5005
5172
|
res.status(409).json({
|
|
5006
5173
|
error: `Assignment "${assignmentSlug}" already exists in project "${projectSlug}"`
|
|
@@ -5009,12 +5176,12 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5009
5176
|
}
|
|
5010
5177
|
const timestamp = fields.created || nowTimestamp();
|
|
5011
5178
|
await ensureDir(assignmentDir);
|
|
5012
|
-
await writeFileForce(
|
|
5179
|
+
await writeFileForce(resolve11(assignmentDir, "assignment.md"), content);
|
|
5013
5180
|
try {
|
|
5014
5181
|
const companions = [
|
|
5015
|
-
[
|
|
5016
|
-
[
|
|
5017
|
-
[
|
|
5182
|
+
[resolve11(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
|
|
5183
|
+
[resolve11(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
|
|
5184
|
+
[resolve11(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
|
|
5018
5185
|
];
|
|
5019
5186
|
for (const [filePath, fileContent] of companions) {
|
|
5020
5187
|
await writeFileForce(filePath, fileContent);
|
|
@@ -5035,7 +5202,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5035
5202
|
router.patch("/api/projects/:slug", async (req, res) => {
|
|
5036
5203
|
try {
|
|
5037
5204
|
const projectSlug = getParam(req.params.slug);
|
|
5038
|
-
const projectPath =
|
|
5205
|
+
const projectPath = resolve11(projectsDir, projectSlug, "project.md");
|
|
5039
5206
|
const currentContent = await readCurrentDocument(projectPath);
|
|
5040
5207
|
if (!currentContent) {
|
|
5041
5208
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
@@ -5068,7 +5235,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5068
5235
|
try {
|
|
5069
5236
|
const projectSlug = getParam(req.params.slug);
|
|
5070
5237
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5071
|
-
const assignmentPath =
|
|
5238
|
+
const assignmentPath = resolve11(
|
|
5072
5239
|
projectsDir,
|
|
5073
5240
|
projectSlug,
|
|
5074
5241
|
"assignments",
|
|
@@ -5111,7 +5278,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5111
5278
|
try {
|
|
5112
5279
|
const projectSlug = getParam(req.params.slug);
|
|
5113
5280
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5114
|
-
const assignmentPath =
|
|
5281
|
+
const assignmentPath = resolve11(
|
|
5115
5282
|
projectsDir,
|
|
5116
5283
|
projectSlug,
|
|
5117
5284
|
"assignments",
|
|
@@ -5147,7 +5314,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5147
5314
|
try {
|
|
5148
5315
|
const projectSlug = getParam(req.params.slug);
|
|
5149
5316
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5150
|
-
const planPath =
|
|
5317
|
+
const planPath = resolve11(
|
|
5151
5318
|
projectsDir,
|
|
5152
5319
|
projectSlug,
|
|
5153
5320
|
"assignments",
|
|
@@ -5185,7 +5352,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5185
5352
|
try {
|
|
5186
5353
|
const projectSlug = getParam(req.params.slug);
|
|
5187
5354
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5188
|
-
const scratchpadPath =
|
|
5355
|
+
const scratchpadPath = resolve11(
|
|
5189
5356
|
projectsDir,
|
|
5190
5357
|
projectSlug,
|
|
5191
5358
|
"assignments",
|
|
@@ -5223,7 +5390,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5223
5390
|
try {
|
|
5224
5391
|
const projectSlug = getParam(req.params.slug);
|
|
5225
5392
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5226
|
-
const handoffPath =
|
|
5393
|
+
const handoffPath = resolve11(
|
|
5227
5394
|
projectsDir,
|
|
5228
5395
|
projectSlug,
|
|
5229
5396
|
"assignments",
|
|
@@ -5261,7 +5428,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5261
5428
|
try {
|
|
5262
5429
|
const projectSlug = getParam(req.params.slug);
|
|
5263
5430
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5264
|
-
const decisionPath =
|
|
5431
|
+
const decisionPath = resolve11(
|
|
5265
5432
|
projectsDir,
|
|
5266
5433
|
projectSlug,
|
|
5267
5434
|
"assignments",
|
|
@@ -5299,7 +5466,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5299
5466
|
try {
|
|
5300
5467
|
const projectSlug = getParam(req.params.slug);
|
|
5301
5468
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5302
|
-
const commentsPath =
|
|
5469
|
+
const commentsPath = resolve11(
|
|
5303
5470
|
projectsDir,
|
|
5304
5471
|
projectSlug,
|
|
5305
5472
|
"assignments",
|
|
@@ -5317,7 +5484,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
|
|
|
5317
5484
|
let currentContent;
|
|
5318
5485
|
let currentCount = 0;
|
|
5319
5486
|
if (await fileExists(commentsPath)) {
|
|
5320
|
-
currentContent = await
|
|
5487
|
+
currentContent = await readFile9(commentsPath, "utf-8");
|
|
5321
5488
|
const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
|
|
5322
5489
|
if (countMatch) currentCount = parseInt(countMatch[1], 10);
|
|
5323
5490
|
} else {
|
|
@@ -5358,7 +5525,7 @@ ${entry}`;
|
|
|
5358
5525
|
const projectSlug = getParam(req.params.slug);
|
|
5359
5526
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5360
5527
|
const commentId = getParam(req.params.commentId);
|
|
5361
|
-
const commentsPath =
|
|
5528
|
+
const commentsPath = resolve11(
|
|
5362
5529
|
projectsDir,
|
|
5363
5530
|
projectSlug,
|
|
5364
5531
|
"assignments",
|
|
@@ -5374,7 +5541,7 @@ ${entry}`;
|
|
|
5374
5541
|
res.status(400).json({ error: "resolved (boolean) is required" });
|
|
5375
5542
|
return;
|
|
5376
5543
|
}
|
|
5377
|
-
const content = await
|
|
5544
|
+
const content = await readFile9(commentsPath, "utf-8");
|
|
5378
5545
|
const parsed = parseComments(content);
|
|
5379
5546
|
const target = parsed.entries.find((e) => e.id === commentId);
|
|
5380
5547
|
if (!target) {
|
|
@@ -5409,7 +5576,7 @@ ${entry}`;
|
|
|
5409
5576
|
router.post("/api/projects/:slug/move-workspace", async (req, res) => {
|
|
5410
5577
|
try {
|
|
5411
5578
|
const projectSlug = getParam(req.params.slug);
|
|
5412
|
-
const projectPath =
|
|
5579
|
+
const projectPath = resolve11(projectsDir, projectSlug, "project.md");
|
|
5413
5580
|
if (!await fileExists(projectPath)) {
|
|
5414
5581
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
5415
5582
|
return;
|
|
@@ -5419,7 +5586,7 @@ ${entry}`;
|
|
|
5419
5586
|
res.status(400).json({ error: "workspace must be a non-empty string or null (for ungrouped)." });
|
|
5420
5587
|
return;
|
|
5421
5588
|
}
|
|
5422
|
-
let content = await
|
|
5589
|
+
let content = await readFile9(projectPath, "utf-8");
|
|
5423
5590
|
content = setTopLevelField(content, "workspace", workspace ?? null);
|
|
5424
5591
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
5425
5592
|
await writeFileForce(projectPath, content);
|
|
@@ -5433,7 +5600,7 @@ ${entry}`;
|
|
|
5433
5600
|
router.post("/api/projects/:slug/status-override", async (req, res) => {
|
|
5434
5601
|
try {
|
|
5435
5602
|
const projectSlug = getParam(req.params.slug);
|
|
5436
|
-
const projectPath =
|
|
5603
|
+
const projectPath = resolve11(projectsDir, projectSlug, "project.md");
|
|
5437
5604
|
if (!await fileExists(projectPath)) {
|
|
5438
5605
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
5439
5606
|
return;
|
|
@@ -5445,7 +5612,7 @@ ${entry}`;
|
|
|
5445
5612
|
res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}, or null to clear.` });
|
|
5446
5613
|
return;
|
|
5447
5614
|
}
|
|
5448
|
-
let content = await
|
|
5615
|
+
let content = await readFile9(projectPath, "utf-8");
|
|
5449
5616
|
content = setTopLevelField(content, "statusOverride", status ?? null);
|
|
5450
5617
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
5451
5618
|
await writeFileForce(projectPath, content);
|
|
@@ -5460,7 +5627,7 @@ ${entry}`;
|
|
|
5460
5627
|
try {
|
|
5461
5628
|
const projectSlug = getParam(req.params.slug);
|
|
5462
5629
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5463
|
-
const assignmentPath =
|
|
5630
|
+
const assignmentPath = resolve11(
|
|
5464
5631
|
projectsDir,
|
|
5465
5632
|
projectSlug,
|
|
5466
5633
|
"assignments",
|
|
@@ -5478,7 +5645,7 @@ ${entry}`;
|
|
|
5478
5645
|
res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
|
|
5479
5646
|
return;
|
|
5480
5647
|
}
|
|
5481
|
-
let content = await
|
|
5648
|
+
let content = await readFile9(assignmentPath, "utf-8");
|
|
5482
5649
|
content = setTopLevelField(content, "status", status);
|
|
5483
5650
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
5484
5651
|
if (status !== "blocked") {
|
|
@@ -5503,8 +5670,8 @@ ${entry}`;
|
|
|
5503
5670
|
res.status(400).json({ error: `Unsupported transition command "${req.params.command}"` });
|
|
5504
5671
|
return;
|
|
5505
5672
|
}
|
|
5506
|
-
const projectDir =
|
|
5507
|
-
const assignmentPath =
|
|
5673
|
+
const projectDir = resolve11(projectsDir, projectSlug);
|
|
5674
|
+
const assignmentPath = resolve11(projectDir, "assignments", assignmentSlug, "assignment.md");
|
|
5508
5675
|
if (!await fileExists(assignmentPath)) {
|
|
5509
5676
|
res.status(404).json({ error: "Assignment not found" });
|
|
5510
5677
|
return;
|
|
@@ -5530,8 +5697,8 @@ ${entry}`;
|
|
|
5530
5697
|
try {
|
|
5531
5698
|
const projectSlug = getParam(req.params.slug);
|
|
5532
5699
|
const assignmentSlug = getParam(req.params.aslug);
|
|
5533
|
-
const assignmentDir =
|
|
5534
|
-
const assignmentPath =
|
|
5700
|
+
const assignmentDir = resolve11(projectsDir, projectSlug, "assignments", assignmentSlug);
|
|
5701
|
+
const assignmentPath = resolve11(assignmentDir, "assignment.md");
|
|
5535
5702
|
if (!await fileExists(assignmentPath)) {
|
|
5536
5703
|
res.status(404).json({ error: `Assignment "${assignmentSlug}" not found in project "${projectSlug}"` });
|
|
5537
5704
|
return;
|
|
@@ -5560,7 +5727,7 @@ ${entry}`;
|
|
|
5560
5727
|
return;
|
|
5561
5728
|
}
|
|
5562
5729
|
const id = generateId();
|
|
5563
|
-
const assignmentDir =
|
|
5730
|
+
const assignmentDir = resolve11(assignmentsDir, id);
|
|
5564
5731
|
if (await fileExists(assignmentDir)) {
|
|
5565
5732
|
res.status(500).json({ error: "UUID collision \u2014 try again" });
|
|
5566
5733
|
return;
|
|
@@ -5580,25 +5747,25 @@ ${entry}`;
|
|
|
5580
5747
|
project: null,
|
|
5581
5748
|
type: typeof type === "string" ? type : void 0
|
|
5582
5749
|
});
|
|
5583
|
-
await writeFileForce(
|
|
5750
|
+
await writeFileForce(resolve11(assignmentDir, "assignment.md"), assignmentContent);
|
|
5584
5751
|
await writeFileForce(
|
|
5585
|
-
|
|
5752
|
+
resolve11(assignmentDir, "scratchpad.md"),
|
|
5586
5753
|
renderScratchpad({ assignmentSlug: id, timestamp })
|
|
5587
5754
|
);
|
|
5588
5755
|
await writeFileForce(
|
|
5589
|
-
|
|
5756
|
+
resolve11(assignmentDir, "handoff.md"),
|
|
5590
5757
|
renderHandoff({ assignmentSlug: id, timestamp })
|
|
5591
5758
|
);
|
|
5592
5759
|
await writeFileForce(
|
|
5593
|
-
|
|
5760
|
+
resolve11(assignmentDir, "decision-record.md"),
|
|
5594
5761
|
renderDecisionRecord({ assignmentSlug: id, timestamp })
|
|
5595
5762
|
);
|
|
5596
5763
|
await writeFileForce(
|
|
5597
|
-
|
|
5764
|
+
resolve11(assignmentDir, "progress.md"),
|
|
5598
5765
|
renderProgress({ assignment: id, timestamp })
|
|
5599
5766
|
);
|
|
5600
5767
|
await writeFileForce(
|
|
5601
|
-
|
|
5768
|
+
resolve11(assignmentDir, "comments.md"),
|
|
5602
5769
|
renderComments({ assignment: id, timestamp })
|
|
5603
5770
|
);
|
|
5604
5771
|
const detail = await getAssignmentDetailById(projectsDir, assignmentsDir, id);
|
|
@@ -5726,7 +5893,7 @@ ${entry}`;
|
|
|
5726
5893
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
5727
5894
|
return;
|
|
5728
5895
|
}
|
|
5729
|
-
const assignmentPath =
|
|
5896
|
+
const assignmentPath = resolve11(resolved.assignmentDir, "assignment.md");
|
|
5730
5897
|
const currentContent = await readCurrentDocument(assignmentPath);
|
|
5731
5898
|
if (!currentContent) {
|
|
5732
5899
|
res.status(404).json({ error: "Assignment not found" });
|
|
@@ -5768,7 +5935,7 @@ ${entry}`;
|
|
|
5768
5935
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
5769
5936
|
return;
|
|
5770
5937
|
}
|
|
5771
|
-
const planPath =
|
|
5938
|
+
const planPath = resolve11(resolved.assignmentDir, "plan.md");
|
|
5772
5939
|
const currentContent = await readCurrentDocument(planPath);
|
|
5773
5940
|
if (!currentContent) {
|
|
5774
5941
|
res.status(404).json({ error: "Plan not found" });
|
|
@@ -5802,7 +5969,7 @@ ${entry}`;
|
|
|
5802
5969
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
5803
5970
|
return;
|
|
5804
5971
|
}
|
|
5805
|
-
const scratchpadPath =
|
|
5972
|
+
const scratchpadPath = resolve11(resolved.assignmentDir, "scratchpad.md");
|
|
5806
5973
|
const currentContent = await readCurrentDocument(scratchpadPath);
|
|
5807
5974
|
if (!currentContent) {
|
|
5808
5975
|
res.status(404).json({ error: "Scratchpad not found" });
|
|
@@ -5836,7 +6003,7 @@ ${entry}`;
|
|
|
5836
6003
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
5837
6004
|
return;
|
|
5838
6005
|
}
|
|
5839
|
-
const handoffPath =
|
|
6006
|
+
const handoffPath = resolve11(resolved.assignmentDir, "handoff.md");
|
|
5840
6007
|
const currentContent = await readCurrentDocument(handoffPath);
|
|
5841
6008
|
if (!currentContent) {
|
|
5842
6009
|
res.status(404).json({ error: "Handoff log not found" });
|
|
@@ -5876,7 +6043,7 @@ ${entry}`;
|
|
|
5876
6043
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
5877
6044
|
return;
|
|
5878
6045
|
}
|
|
5879
|
-
const decisionPath =
|
|
6046
|
+
const decisionPath = resolve11(resolved.assignmentDir, "decision-record.md");
|
|
5880
6047
|
const currentContent = await readCurrentDocument(decisionPath);
|
|
5881
6048
|
if (!currentContent) {
|
|
5882
6049
|
res.status(404).json({ error: "Decision record not found" });
|
|
@@ -5916,7 +6083,7 @@ ${entry}`;
|
|
|
5916
6083
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
5917
6084
|
return;
|
|
5918
6085
|
}
|
|
5919
|
-
const assignmentPath =
|
|
6086
|
+
const assignmentPath = resolve11(resolved.assignmentDir, "assignment.md");
|
|
5920
6087
|
if (!await fileExists(assignmentPath)) {
|
|
5921
6088
|
res.status(404).json({ error: "Assignment not found" });
|
|
5922
6089
|
return;
|
|
@@ -5928,7 +6095,7 @@ ${entry}`;
|
|
|
5928
6095
|
res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
|
|
5929
6096
|
return;
|
|
5930
6097
|
}
|
|
5931
|
-
let content = await
|
|
6098
|
+
let content = await readFile9(assignmentPath, "utf-8");
|
|
5932
6099
|
content = setTopLevelField(content, "status", status);
|
|
5933
6100
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
5934
6101
|
if (status !== "blocked") {
|
|
@@ -5954,7 +6121,7 @@ ${entry}`;
|
|
|
5954
6121
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
5955
6122
|
return;
|
|
5956
6123
|
}
|
|
5957
|
-
const assignmentPath =
|
|
6124
|
+
const assignmentPath = resolve11(resolved.assignmentDir, "assignment.md");
|
|
5958
6125
|
const currentContent = await readCurrentDocument(assignmentPath);
|
|
5959
6126
|
if (!currentContent) {
|
|
5960
6127
|
res.status(404).json({ error: "Assignment not found" });
|
|
@@ -6019,7 +6186,7 @@ function slugifyLocal(input) {
|
|
|
6019
6186
|
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
|
|
6020
6187
|
}
|
|
6021
6188
|
async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDetail) {
|
|
6022
|
-
const commentsPath =
|
|
6189
|
+
const commentsPath = resolve11(assignmentDir, "comments.md");
|
|
6023
6190
|
const { body, author, type, replyTo } = req.body || {};
|
|
6024
6191
|
if (!body || typeof body !== "string" || !body.trim()) {
|
|
6025
6192
|
res.status(400).json({ error: "body is required" });
|
|
@@ -6031,7 +6198,7 @@ async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDet
|
|
|
6031
6198
|
let currentContent;
|
|
6032
6199
|
let currentCount = 0;
|
|
6033
6200
|
if (await fileExists(commentsPath)) {
|
|
6034
|
-
currentContent = await
|
|
6201
|
+
currentContent = await readFile9(commentsPath, "utf-8");
|
|
6035
6202
|
const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
|
|
6036
6203
|
if (countMatch) currentCount = parseInt(countMatch[1], 10);
|
|
6037
6204
|
} else {
|
|
@@ -6061,7 +6228,7 @@ ${entry}`;
|
|
|
6061
6228
|
res.status(201).json({ assignment, comment: { id: comment.id } });
|
|
6062
6229
|
}
|
|
6063
6230
|
async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloadDetail) {
|
|
6064
|
-
const commentsPath =
|
|
6231
|
+
const commentsPath = resolve11(assignmentDir, "comments.md");
|
|
6065
6232
|
if (!await fileExists(commentsPath)) {
|
|
6066
6233
|
res.status(404).json({ error: "Comments file not found" });
|
|
6067
6234
|
return;
|
|
@@ -6071,7 +6238,7 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloa
|
|
|
6071
6238
|
res.status(400).json({ error: "resolved (boolean) is required" });
|
|
6072
6239
|
return;
|
|
6073
6240
|
}
|
|
6074
|
-
const content = await
|
|
6241
|
+
const content = await readFile9(commentsPath, "utf-8");
|
|
6075
6242
|
const parsed = parseComments(content);
|
|
6076
6243
|
const target = parsed.entries.find((e) => e.id === commentId);
|
|
6077
6244
|
if (!target) {
|
|
@@ -6216,7 +6383,7 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir) {
|
|
|
6216
6383
|
|
|
6217
6384
|
// src/dashboard/api-agent-sessions.ts
|
|
6218
6385
|
import { Router as Router3 } from "express";
|
|
6219
|
-
import { resolve as
|
|
6386
|
+
import { resolve as resolve12 } from "path";
|
|
6220
6387
|
init_fs();
|
|
6221
6388
|
function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
|
|
6222
6389
|
const router = Router3();
|
|
@@ -6233,7 +6400,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
|
|
|
6233
6400
|
try {
|
|
6234
6401
|
const { projectSlug } = req.params;
|
|
6235
6402
|
const assignment = req.query.assignment;
|
|
6236
|
-
const projectDir =
|
|
6403
|
+
const projectDir = resolve12(projectsDir, projectSlug);
|
|
6237
6404
|
if (!await fileExists(projectDir)) {
|
|
6238
6405
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
6239
6406
|
return;
|
|
@@ -6259,7 +6426,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
|
|
|
6259
6426
|
return;
|
|
6260
6427
|
}
|
|
6261
6428
|
if (projectSlug) {
|
|
6262
|
-
const projectDir =
|
|
6429
|
+
const projectDir = resolve12(projectsDir, projectSlug);
|
|
6263
6430
|
if (!await fileExists(projectDir)) {
|
|
6264
6431
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
6265
6432
|
return;
|
|
@@ -6327,8 +6494,8 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
|
|
|
6327
6494
|
init_api();
|
|
6328
6495
|
init_parser();
|
|
6329
6496
|
import { Router as Router4 } from "express";
|
|
6330
|
-
import { resolve as
|
|
6331
|
-
import { readFile as
|
|
6497
|
+
import { resolve as resolve14 } from "path";
|
|
6498
|
+
import { readFile as readFile11, unlink as unlink2 } from "fs/promises";
|
|
6332
6499
|
init_timestamp();
|
|
6333
6500
|
init_fs();
|
|
6334
6501
|
|
|
@@ -6336,15 +6503,15 @@ init_fs();
|
|
|
6336
6503
|
init_fs();
|
|
6337
6504
|
init_parser();
|
|
6338
6505
|
init_timestamp();
|
|
6339
|
-
import { resolve as
|
|
6340
|
-
import { readdir as
|
|
6506
|
+
import { resolve as resolve13 } from "path";
|
|
6507
|
+
import { readdir as readdir7, readFile as readFile10 } from "fs/promises";
|
|
6341
6508
|
async function rebuildPlaybookManifest(playbooksDir2) {
|
|
6342
6509
|
if (!await fileExists(playbooksDir2)) return;
|
|
6343
|
-
const entries = await
|
|
6510
|
+
const entries = await readdir7(playbooksDir2, { withFileTypes: true });
|
|
6344
6511
|
const rows = [];
|
|
6345
6512
|
for (const entry of entries) {
|
|
6346
6513
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
|
|
6347
|
-
const raw = await
|
|
6514
|
+
const raw = await readFile10(resolve13(playbooksDir2, entry.name), "utf-8");
|
|
6348
6515
|
const parsed = parsePlaybook(raw);
|
|
6349
6516
|
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
6350
6517
|
rows.push({
|
|
@@ -6374,7 +6541,7 @@ async function rebuildPlaybookManifest(playbooksDir2) {
|
|
|
6374
6541
|
}
|
|
6375
6542
|
}
|
|
6376
6543
|
lines.push("");
|
|
6377
|
-
await writeFileForce(
|
|
6544
|
+
await writeFileForce(resolve13(playbooksDir2, "manifest.md"), lines.join("\n"));
|
|
6378
6545
|
}
|
|
6379
6546
|
|
|
6380
6547
|
// src/dashboard/api-playbooks.ts
|
|
@@ -6415,12 +6582,12 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
6415
6582
|
});
|
|
6416
6583
|
router.get("/:slug/edit", async (req, res) => {
|
|
6417
6584
|
try {
|
|
6418
|
-
const filePath =
|
|
6585
|
+
const filePath = resolve14(playbooksDir2, `${req.params.slug}.md`);
|
|
6419
6586
|
if (!await fileExists(filePath)) {
|
|
6420
6587
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
6421
6588
|
return;
|
|
6422
6589
|
}
|
|
6423
|
-
const content = await
|
|
6590
|
+
const content = await readFile11(filePath, "utf-8");
|
|
6424
6591
|
res.json({
|
|
6425
6592
|
documentType: "playbook",
|
|
6426
6593
|
title: `Edit Playbook: ${req.params.slug}`,
|
|
@@ -6445,7 +6612,7 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
6445
6612
|
return;
|
|
6446
6613
|
}
|
|
6447
6614
|
await ensureDir(playbooksDir2);
|
|
6448
|
-
const filePath =
|
|
6615
|
+
const filePath = resolve14(playbooksDir2, `${slug}.md`);
|
|
6449
6616
|
if (await fileExists(filePath)) {
|
|
6450
6617
|
res.status(409).json({ error: `Playbook "${slug}" already exists` });
|
|
6451
6618
|
return;
|
|
@@ -6464,7 +6631,7 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
6464
6631
|
res.status(400).json({ error: "content is required" });
|
|
6465
6632
|
return;
|
|
6466
6633
|
}
|
|
6467
|
-
const filePath =
|
|
6634
|
+
const filePath = resolve14(playbooksDir2, `${req.params.slug}.md`);
|
|
6468
6635
|
if (!await fileExists(filePath)) {
|
|
6469
6636
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
6470
6637
|
return;
|
|
@@ -6482,7 +6649,7 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
6482
6649
|
res.status(403).json({ error: "The playbook manifest cannot be deleted" });
|
|
6483
6650
|
return;
|
|
6484
6651
|
}
|
|
6485
|
-
const filePath =
|
|
6652
|
+
const filePath = resolve14(playbooksDir2, `${req.params.slug}.md`);
|
|
6486
6653
|
if (!await fileExists(filePath)) {
|
|
6487
6654
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
6488
6655
|
return;
|
|
@@ -6497,11 +6664,14 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
6497
6664
|
return router;
|
|
6498
6665
|
}
|
|
6499
6666
|
|
|
6667
|
+
// src/dashboard/server.ts
|
|
6668
|
+
init_fs_migration();
|
|
6669
|
+
|
|
6500
6670
|
// src/dashboard/api-todos.ts
|
|
6501
6671
|
init_parser2();
|
|
6502
6672
|
init_fs();
|
|
6503
6673
|
import { Router as Router5 } from "express";
|
|
6504
|
-
import { readdir as
|
|
6674
|
+
import { readdir as readdir8 } from "fs/promises";
|
|
6505
6675
|
var WORKSPACE_REGEX = /^[a-z0-9_][a-z0-9-]*$/;
|
|
6506
6676
|
function getWorkspaceParam(value) {
|
|
6507
6677
|
if (Array.isArray(value)) {
|
|
@@ -6535,7 +6705,7 @@ function createTodosRouter(todosDir2, broadcast) {
|
|
|
6535
6705
|
router.get("/", async (_req, res) => {
|
|
6536
6706
|
try {
|
|
6537
6707
|
await ensureDir(todosDir2);
|
|
6538
|
-
const files = await
|
|
6708
|
+
const files = await readdir8(todosDir2).catch(() => []);
|
|
6539
6709
|
const workspaces = [];
|
|
6540
6710
|
for (const file of files) {
|
|
6541
6711
|
if (typeof file !== "string") continue;
|
|
@@ -6640,8 +6810,8 @@ function createTodosRouter(todosDir2, broadcast) {
|
|
|
6640
6810
|
router.post("/:workspace/archive", async (req, res) => {
|
|
6641
6811
|
try {
|
|
6642
6812
|
const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
|
|
6643
|
-
const { resolve:
|
|
6644
|
-
const { readFile:
|
|
6813
|
+
const { resolve: resolve18 } = await import("path");
|
|
6814
|
+
const { readFile: readFile14 } = await import("fs/promises");
|
|
6645
6815
|
const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
6646
6816
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
6647
6817
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
@@ -6657,10 +6827,10 @@ function createTodosRouter(todosDir2, broadcast) {
|
|
|
6657
6827
|
(e) => e.itemIds.every((id) => completedIds.has(id))
|
|
6658
6828
|
);
|
|
6659
6829
|
const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
|
|
6660
|
-
await ensureDir(
|
|
6830
|
+
await ensureDir(resolve18(todosDir2, "archive"));
|
|
6661
6831
|
let archContent = "";
|
|
6662
6832
|
if (await fileExists(archFile)) {
|
|
6663
|
-
archContent = await
|
|
6833
|
+
archContent = await readFile14(archFile, "utf-8");
|
|
6664
6834
|
archContent = archContent.trimEnd() + "\n\n";
|
|
6665
6835
|
} else {
|
|
6666
6836
|
archContent = `---
|
|
@@ -6920,8 +7090,8 @@ init_fs();
|
|
|
6920
7090
|
init_config2();
|
|
6921
7091
|
import { execFile as execFile2 } from "child_process";
|
|
6922
7092
|
import { promisify as promisify2 } from "util";
|
|
6923
|
-
import { cp, mkdtemp, rm as rm2, readFile as
|
|
6924
|
-
import { resolve as
|
|
7093
|
+
import { cp, mkdtemp, rm as rm2, readFile as readFile13, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename3 } from "fs/promises";
|
|
7094
|
+
import { resolve as resolve16, join as join2 } from "path";
|
|
6925
7095
|
import { tmpdir } from "os";
|
|
6926
7096
|
var exec2 = promisify2(execFile2);
|
|
6927
7097
|
var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
|
|
@@ -6961,7 +7131,7 @@ async function resolveCategoryPath(category) {
|
|
|
6961
7131
|
case "servers":
|
|
6962
7132
|
return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
|
|
6963
7133
|
case "config":
|
|
6964
|
-
return { sourcePath:
|
|
7134
|
+
return { sourcePath: resolve16(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
|
|
6965
7135
|
}
|
|
6966
7136
|
}
|
|
6967
7137
|
async function checkGitInstalled() {
|
|
@@ -6972,7 +7142,7 @@ async function checkGitInstalled() {
|
|
|
6972
7142
|
}
|
|
6973
7143
|
}
|
|
6974
7144
|
async function acquireLock() {
|
|
6975
|
-
const lockPath =
|
|
7145
|
+
const lockPath = resolve16(syntaurRoot(), LOCK_FILE_NAME);
|
|
6976
7146
|
await ensureDir(syntaurRoot());
|
|
6977
7147
|
try {
|
|
6978
7148
|
const handle = await open(lockPath, "wx");
|
|
@@ -6981,7 +7151,7 @@ async function acquireLock() {
|
|
|
6981
7151
|
return lockPath;
|
|
6982
7152
|
} catch (err) {
|
|
6983
7153
|
if (err.code === "EEXIST") {
|
|
6984
|
-
const pid = await
|
|
7154
|
+
const pid = await readFile13(lockPath, "utf-8").catch(() => "");
|
|
6985
7155
|
throw new Error(
|
|
6986
7156
|
`Backup operation already in progress (lock file at ${lockPath}, pid ${pid.trim() || "unknown"}). If stale, delete the file and retry.`
|
|
6987
7157
|
);
|
|
@@ -7019,7 +7189,7 @@ async function copyRecursive(src, dest) {
|
|
|
7019
7189
|
await ensureDir(dest);
|
|
7020
7190
|
await cp(src, dest, { recursive: true, force: true });
|
|
7021
7191
|
} else {
|
|
7022
|
-
await ensureDir(
|
|
7192
|
+
await ensureDir(resolve16(dest, ".."));
|
|
7023
7193
|
await cp(src, dest, { force: true });
|
|
7024
7194
|
}
|
|
7025
7195
|
}
|
|
@@ -7028,7 +7198,7 @@ function resolveCategoriesStrict(csv) {
|
|
|
7028
7198
|
return parseCategoriesStrict(parts);
|
|
7029
7199
|
}
|
|
7030
7200
|
async function readSanitizedConfig(configPath) {
|
|
7031
|
-
const content = await
|
|
7201
|
+
const content = await readFile13(configPath, "utf-8");
|
|
7032
7202
|
return content.replace(/^(\s*lastBackup:\s*).*$/m, "$1null").replace(/^(\s*lastRestore:\s*).*$/m, "$1null");
|
|
7033
7203
|
}
|
|
7034
7204
|
async function backupToGithub(overrides) {
|
|
@@ -7067,8 +7237,8 @@ async function backupToGithub(overrides) {
|
|
|
7067
7237
|
}
|
|
7068
7238
|
if (category === "config") {
|
|
7069
7239
|
const sanitized = await readSanitizedConfig(sourcePath);
|
|
7070
|
-
await ensureDir(
|
|
7071
|
-
await
|
|
7240
|
+
await ensureDir(resolve16(destPath, ".."));
|
|
7241
|
+
await writeFile4(destPath, sanitized, "utf-8");
|
|
7072
7242
|
} else {
|
|
7073
7243
|
await copyRecursive(sourcePath, destPath);
|
|
7074
7244
|
}
|
|
@@ -7121,7 +7291,7 @@ async function backupToGithub(overrides) {
|
|
|
7121
7291
|
}
|
|
7122
7292
|
async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
|
|
7123
7293
|
if (isFile) {
|
|
7124
|
-
await ensureDir(
|
|
7294
|
+
await ensureDir(resolve16(localPath, ".."));
|
|
7125
7295
|
await cp(repoSrcPath, localPath, { force: true });
|
|
7126
7296
|
return;
|
|
7127
7297
|
}
|
|
@@ -7132,7 +7302,7 @@ async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
|
|
|
7132
7302
|
const localExistsBefore = await fileExists(localPath);
|
|
7133
7303
|
if (backupExistsBefore) {
|
|
7134
7304
|
if (!localExistsBefore) {
|
|
7135
|
-
await
|
|
7305
|
+
await rename3(backupPath, localPath);
|
|
7136
7306
|
} else {
|
|
7137
7307
|
throw new Error(
|
|
7138
7308
|
`Cannot restore "${localPath}": a stale crash-recovery backup exists at ${backupPath} while the current path also exists. Inspect both and remove the one you don't need, then retry.`
|
|
@@ -7144,15 +7314,15 @@ async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
|
|
|
7144
7314
|
await cp(repoSrcPath, stagingPath, { recursive: true, force: true });
|
|
7145
7315
|
const localExists = await fileExists(localPath);
|
|
7146
7316
|
if (localExists) {
|
|
7147
|
-
await
|
|
7317
|
+
await rename3(localPath, backupPath);
|
|
7148
7318
|
localMovedAside = true;
|
|
7149
7319
|
}
|
|
7150
|
-
await
|
|
7320
|
+
await rename3(stagingPath, localPath);
|
|
7151
7321
|
await rm2(backupPath, { recursive: true, force: true }).catch(() => {
|
|
7152
7322
|
});
|
|
7153
7323
|
} catch (err) {
|
|
7154
7324
|
if (localMovedAside && await fileExists(backupPath)) {
|
|
7155
|
-
await
|
|
7325
|
+
await rename3(backupPath, localPath).catch(() => {
|
|
7156
7326
|
});
|
|
7157
7327
|
}
|
|
7158
7328
|
await rm2(stagingPath, { recursive: true, force: true }).catch(() => {
|
|
@@ -7222,7 +7392,7 @@ async function restoreFromGithub(overrides) {
|
|
|
7222
7392
|
}
|
|
7223
7393
|
async function getBackupStatus() {
|
|
7224
7394
|
const config = await readConfig();
|
|
7225
|
-
const lockPath =
|
|
7395
|
+
const lockPath = resolve16(syntaurRoot(), LOCK_FILE_NAME);
|
|
7226
7396
|
const locked = await fileExists(lockPath);
|
|
7227
7397
|
return {
|
|
7228
7398
|
repo: config.backup?.repo ?? null,
|
|
@@ -7556,6 +7726,18 @@ function createDashboardServer(options) {
|
|
|
7556
7726
|
migrateFromMarkdown(projectsDir).catch((err) => {
|
|
7557
7727
|
console.error("Session migration from markdown failed:", err);
|
|
7558
7728
|
});
|
|
7729
|
+
(async () => {
|
|
7730
|
+
try {
|
|
7731
|
+
const configResult = await migrateLegacyConfig(
|
|
7732
|
+
resolve17(syntaurRoot(), "config.md")
|
|
7733
|
+
);
|
|
7734
|
+
const projectResult = await migrateLegacyProjectFiles(projectsDir);
|
|
7735
|
+
const summary = summarizeMigration(projectResult, configResult);
|
|
7736
|
+
if (summary) console.log(summary);
|
|
7737
|
+
} catch (err) {
|
|
7738
|
+
console.error("Legacy filesystem migration failed:", err);
|
|
7739
|
+
}
|
|
7740
|
+
})();
|
|
7559
7741
|
app.use(express.json());
|
|
7560
7742
|
app.get("/api/overview", async (_req, res) => {
|
|
7561
7743
|
try {
|
|
@@ -7775,7 +7957,7 @@ function createDashboardServer(options) {
|
|
|
7775
7957
|
if (serveStaticUi && dashboardDistPath) {
|
|
7776
7958
|
app.use(express.static(dashboardDistPath));
|
|
7777
7959
|
app.get("{*path}", async (_req, res) => {
|
|
7778
|
-
const indexPath =
|
|
7960
|
+
const indexPath = resolve17(dashboardDistPath, "index.html");
|
|
7779
7961
|
if (await fileExists(indexPath)) {
|
|
7780
7962
|
res.sendFile(indexPath);
|
|
7781
7963
|
} else {
|
|
@@ -7808,8 +7990,8 @@ function createDashboardServer(options) {
|
|
|
7808
7990
|
}
|
|
7809
7991
|
});
|
|
7810
7992
|
server.listen(port, () => {
|
|
7811
|
-
const portFile =
|
|
7812
|
-
|
|
7993
|
+
const portFile = resolve17(syntaurRoot(), "dashboard-port");
|
|
7994
|
+
writeFile5(portFile, String(port), "utf-8").catch(() => {
|
|
7813
7995
|
});
|
|
7814
7996
|
resolvePromise();
|
|
7815
7997
|
});
|
|
@@ -7825,7 +8007,7 @@ function createDashboardServer(options) {
|
|
|
7825
8007
|
client.terminate();
|
|
7826
8008
|
}
|
|
7827
8009
|
clients.clear();
|
|
7828
|
-
const portFile =
|
|
8010
|
+
const portFile = resolve17(syntaurRoot(), "dashboard-port");
|
|
7829
8011
|
await unlink4(portFile).catch(() => {
|
|
7830
8012
|
});
|
|
7831
8013
|
server.closeAllConnections?.();
|