syntaur 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/assets/{_basePickBy-ij-Ukp6s.js → _basePickBy-BQIP1Ca7.js} +1 -1
- package/dashboard/dist/assets/{_baseUniq-CZKk9gZR.js → _baseUniq-BnBWRwT7.js} +1 -1
- package/dashboard/dist/assets/{arc-C30UbJZB.js → arc-BYWL4eq0.js} +1 -1
- package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-BDVieGIr.js → architectureDiagram-2XIMDMQ5-CD_SWPSa.js} +1 -1
- package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-DZYY4t9w.js → blockDiagram-WCTKOSBZ-BS1ZbFBU.js} +1 -1
- package/dashboard/dist/assets/{c4Diagram-IC4MRINW-B019MXol.js → c4Diagram-IC4MRINW-D99yg-l2.js} +1 -1
- package/dashboard/dist/assets/channel-Df6VrFK5.js +1 -0
- package/dashboard/dist/assets/{chunk-4BX2VUAB-DrkTwY15.js → chunk-4BX2VUAB-BkN9IORC.js} +1 -1
- package/dashboard/dist/assets/{chunk-55IACEB6-mFTAE8DD.js → chunk-55IACEB6-BQPHWefV.js} +1 -1
- package/dashboard/dist/assets/{chunk-FMBD7UC4-VgDZaNoS.js → chunk-FMBD7UC4-CNcExMdx.js} +1 -1
- package/dashboard/dist/assets/{chunk-JSJVCQXG-C_KXaq-c.js → chunk-JSJVCQXG-LXBmftkC.js} +1 -1
- package/dashboard/dist/assets/{chunk-KX2RTZJC-DI-P_pPL.js → chunk-KX2RTZJC-Tqi7zNqq.js} +1 -1
- package/dashboard/dist/assets/{chunk-NQ4KR5QH-TgYAsxTk.js → chunk-NQ4KR5QH-DkMbx-rW.js} +1 -1
- package/dashboard/dist/assets/{chunk-QZHKN3VN-Drfv_VpM.js → chunk-QZHKN3VN-BlrRCfkJ.js} +1 -1
- package/dashboard/dist/assets/{chunk-WL4C6EOR-CpLwvo_U.js → chunk-WL4C6EOR-of3XBzMu.js} +1 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-CyfzumTY.js +1 -0
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-CyfzumTY.js +1 -0
- package/dashboard/dist/assets/clone-CMs4Aqrx.js +1 -0
- package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-CkKtF37m.js → cose-bilkent-S5V4N54A-BlIiyO76.js} +1 -1
- package/dashboard/dist/assets/{dagre-KLK3FWXG-BBlY_FL3.js → dagre-KLK3FWXG-CYQjSI9N.js} +1 -1
- package/dashboard/dist/assets/{diagram-E7M64L7V-DLsFDLHm.js → diagram-E7M64L7V-BZHzTKct.js} +1 -1
- package/dashboard/dist/assets/{diagram-IFDJBPK2--sb7diMG.js → diagram-IFDJBPK2-kMP3WqBV.js} +1 -1
- package/dashboard/dist/assets/{diagram-P4PSJMXO-D2LuEWVt.js → diagram-P4PSJMXO-BWSHyFOv.js} +1 -1
- package/dashboard/dist/assets/{erDiagram-INFDFZHY-C1BEeili.js → erDiagram-INFDFZHY-B5HrvsPP.js} +1 -1
- package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-BpbapQbU.js → flowDiagram-PKNHOUZH-Dm4ewP7w.js} +1 -1
- package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-Io60qUuG.js → ganttDiagram-A5KZAMGK-DB3k27zu.js} +1 -1
- package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-oemlGgRh.js → gitGraphDiagram-K3NZZRJ6-G7y6Ey-m.js} +1 -1
- package/dashboard/dist/assets/{graph-BZb-lGfH.js → graph-CaM4i6vq.js} +1 -1
- package/dashboard/dist/assets/index-B4QMu-Oq.css +1 -0
- package/dashboard/dist/assets/index-BBWZjPBC.js +495 -0
- package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-Ca4mwnZF.js → infoDiagram-LFFYTUFH-JNTUbTjg.js} +1 -1
- package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-9zuQ8y8W.js → ishikawaDiagram-PHBUUO56-BZJt1ht8.js} +1 -1
- package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-OdeeOdMx.js → journeyDiagram-4ABVD52K-DPcqvl9A.js} +1 -1
- package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-Cie4JtFn.js → kanban-definition-K7BYSVSG-D1D7AuOV.js} +1 -1
- package/dashboard/dist/assets/{layout-Bmx2mvFv.js → layout-BTOh3EDT.js} +1 -1
- package/dashboard/dist/assets/{linear-CW6K_-MX.js → linear-MbCpC_Cg.js} +1 -1
- package/dashboard/dist/assets/{mermaid.core-DmfO6BgK.js → mermaid.core-CYbhqlNy.js} +4 -4
- package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-L6b3vG79.js → mindmap-definition-YRQLILUH-CwYCISFH.js} +1 -1
- package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CkHTCIWg.js → pieDiagram-SKSYHLDU-5qfZ73SG.js} +1 -1
- package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-B9MqhhIC.js → quadrantDiagram-337W2JSQ-WI8y1sQ_.js} +1 -1
- package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-CyHAfXCK.js → requirementDiagram-Z7DCOOCP-BFlD0ZTS.js} +1 -1
- package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DHNzGGyE.js → sankeyDiagram-WA2Y5GQK-Bdckv1Se.js} +1 -1
- package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-BVvcJkrx.js → sequenceDiagram-2WXFIKYE-DgzxKAlZ.js} +1 -1
- package/dashboard/dist/assets/{stateDiagram-RAJIS63D-CZ2cknh7.js → stateDiagram-RAJIS63D-DO4OXahC.js} +1 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-o8bgX-J3.js +1 -0
- package/dashboard/dist/assets/{timeline-definition-YZTLITO2-BXUtlVyd.js → timeline-definition-YZTLITO2-BBB01JWw.js} +1 -1
- package/dashboard/dist/assets/{treemap-KZPCXAKY-Dgi-hMKM.js → treemap-KZPCXAKY-Dr0jb8op.js} +1 -1
- package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-C9zGrrUQ.js → vennDiagram-LZ73GAT5-D40KFl2o.js} +1 -1
- package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-Dq71BUtc.js → xychartDiagram-JWTSCODW-DBUmWQfT.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/dashboard/server.js +2380 -1263
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +5601 -4684
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/vendor/syntaur-skills/README.md +2 -0
- package/vendor/syntaur-skills/skills/clear-assignment/SKILL.md +111 -0
- package/vendor/syntaur-skills/skills/manage-statuses/SKILL.md +72 -0
- package/dashboard/dist/assets/channel-DH4gshIt.js +0 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-emsfh8H4.js +0 -1
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-emsfh8H4.js +0 -1
- package/dashboard/dist/assets/clone-gdeRwgBN.js +0 -1
- package/dashboard/dist/assets/index-BSVCsfvM.css +0 -1
- package/dashboard/dist/assets/index-CXWVuGs-.js +0 -481
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-C4CPervD.js +0 -1
package/dist/dashboard/server.js
CHANGED
|
@@ -9,6 +9,18 @@ var __export = (target, all) => {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// src/utils/paths.ts
|
|
12
|
+
var paths_exports = {};
|
|
13
|
+
__export(paths_exports, {
|
|
14
|
+
assignmentsDir: () => assignmentsDir,
|
|
15
|
+
defaultProjectDir: () => defaultProjectDir,
|
|
16
|
+
expandHome: () => expandHome,
|
|
17
|
+
playbooksDir: () => playbooksDir,
|
|
18
|
+
projectTodosDir: () => projectTodosDir,
|
|
19
|
+
serversDir: () => serversDir,
|
|
20
|
+
syntaurRoot: () => syntaurRoot,
|
|
21
|
+
todoPlanDir: () => todoPlanDir,
|
|
22
|
+
todosDir: () => todosDir
|
|
23
|
+
});
|
|
12
24
|
import { homedir } from "os";
|
|
13
25
|
import { resolve } from "path";
|
|
14
26
|
function expandHome(p) {
|
|
@@ -27,6 +39,9 @@ function syntaurRoot() {
|
|
|
27
39
|
function defaultProjectDir() {
|
|
28
40
|
return resolve(syntaurRoot(), "projects");
|
|
29
41
|
}
|
|
42
|
+
function assignmentsDir() {
|
|
43
|
+
return resolve(syntaurRoot(), "assignments");
|
|
44
|
+
}
|
|
30
45
|
function serversDir() {
|
|
31
46
|
return resolve(syntaurRoot(), "servers");
|
|
32
47
|
}
|
|
@@ -39,6 +54,9 @@ function todosDir() {
|
|
|
39
54
|
function projectTodosDir(projectsDir, projectSlug) {
|
|
40
55
|
return resolve(projectsDir, projectSlug, "todos");
|
|
41
56
|
}
|
|
57
|
+
function todoPlanDir(todosDir2, workspaceOrProject, todoId) {
|
|
58
|
+
return resolve(todosDir2, "plans", workspaceOrProject, todoId);
|
|
59
|
+
}
|
|
42
60
|
var init_paths = __esm({
|
|
43
61
|
"src/utils/paths.ts"() {
|
|
44
62
|
"use strict";
|
|
@@ -311,6 +329,10 @@ var init_fs = __esm({
|
|
|
311
329
|
});
|
|
312
330
|
|
|
313
331
|
// src/utils/timestamp.ts
|
|
332
|
+
var timestamp_exports = {};
|
|
333
|
+
__export(timestamp_exports, {
|
|
334
|
+
nowTimestamp: () => nowTimestamp
|
|
335
|
+
});
|
|
314
336
|
function nowTimestamp() {
|
|
315
337
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
316
338
|
}
|
|
@@ -622,7 +644,124 @@ var init_fs_migration = __esm({
|
|
|
622
644
|
}
|
|
623
645
|
});
|
|
624
646
|
|
|
647
|
+
// src/utils/hotkeysCatalog.ts
|
|
648
|
+
function isBindableActionKind(value) {
|
|
649
|
+
return typeof value === "string" && BINDABLE_ACTION_KINDS.includes(value);
|
|
650
|
+
}
|
|
651
|
+
function canonicalizeCombo(input) {
|
|
652
|
+
if (typeof input !== "string") return "";
|
|
653
|
+
const trimmed = input.trim();
|
|
654
|
+
if (!trimmed) return "";
|
|
655
|
+
if (/\s/.test(trimmed) && !trimmed.includes("+")) {
|
|
656
|
+
return trimmed.split(/\s+/).map(canonicalizeCombo).filter((part) => part.length > 0).join(" ");
|
|
657
|
+
}
|
|
658
|
+
const parts = trimmed.split("+").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
659
|
+
if (parts.length === 0) return "";
|
|
660
|
+
if (parts.length === 1) {
|
|
661
|
+
return parts[0].toLowerCase();
|
|
662
|
+
}
|
|
663
|
+
const key = parts[parts.length - 1].toLowerCase();
|
|
664
|
+
const mods = parts.slice(0, -1).map((m) => m.toLowerCase());
|
|
665
|
+
const seen = /* @__PURE__ */ new Set();
|
|
666
|
+
const ordered = [];
|
|
667
|
+
for (const m of MODIFIER_ORDER) {
|
|
668
|
+
if (mods.includes(m) && !seen.has(m)) {
|
|
669
|
+
ordered.push(m);
|
|
670
|
+
seen.add(m);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
for (const m of mods) {
|
|
674
|
+
if (!seen.has(m)) {
|
|
675
|
+
ordered.push(m);
|
|
676
|
+
seen.add(m);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return [...ordered, key].join("+");
|
|
680
|
+
}
|
|
681
|
+
function isReservedCombo(combo) {
|
|
682
|
+
const c = canonicalizeCombo(combo);
|
|
683
|
+
if (!c) return false;
|
|
684
|
+
return BUILTIN_RESERVED_COMBOS.includes(c);
|
|
685
|
+
}
|
|
686
|
+
var BINDABLE_ACTION_KINDS, BUILTIN_RESERVED_COMBOS, MODIFIER_ORDER, DEFAULT_BINDABLE_HOTKEYS;
|
|
687
|
+
var init_hotkeysCatalog = __esm({
|
|
688
|
+
"src/utils/hotkeysCatalog.ts"() {
|
|
689
|
+
"use strict";
|
|
690
|
+
BINDABLE_ACTION_KINDS = [
|
|
691
|
+
"new-workspace",
|
|
692
|
+
"new-project",
|
|
693
|
+
"new-todo",
|
|
694
|
+
"new-assignment"
|
|
695
|
+
];
|
|
696
|
+
BUILTIN_RESERVED_COMBOS = [
|
|
697
|
+
"mod+k",
|
|
698
|
+
"mod+shift+k",
|
|
699
|
+
"?",
|
|
700
|
+
"escape",
|
|
701
|
+
"enter",
|
|
702
|
+
"shift+t",
|
|
703
|
+
// g-chord starter + suffixes
|
|
704
|
+
"g",
|
|
705
|
+
"g o",
|
|
706
|
+
"g m",
|
|
707
|
+
"g a",
|
|
708
|
+
"g t",
|
|
709
|
+
"g s",
|
|
710
|
+
"g !",
|
|
711
|
+
"g ,",
|
|
712
|
+
// list-scope navigation
|
|
713
|
+
"j",
|
|
714
|
+
"k",
|
|
715
|
+
"o",
|
|
716
|
+
// ProjectDetail page
|
|
717
|
+
"a",
|
|
718
|
+
"e",
|
|
719
|
+
// AssignmentsPage board
|
|
720
|
+
"/",
|
|
721
|
+
"r",
|
|
722
|
+
// AssignmentDetail page
|
|
723
|
+
"p",
|
|
724
|
+
"h",
|
|
725
|
+
"d",
|
|
726
|
+
"s",
|
|
727
|
+
"[",
|
|
728
|
+
"]"
|
|
729
|
+
];
|
|
730
|
+
MODIFIER_ORDER = ["mod", "ctrl", "alt", "shift"];
|
|
731
|
+
DEFAULT_BINDABLE_HOTKEYS = {
|
|
732
|
+
"new-workspace": canonicalizeCombo("Mod+Shift+Alt+w"),
|
|
733
|
+
"new-project": canonicalizeCombo("Mod+Shift+Alt+p"),
|
|
734
|
+
"new-todo": canonicalizeCombo("Mod+Shift+Alt+t"),
|
|
735
|
+
"new-assignment": canonicalizeCombo("Mod+Shift+Alt+a")
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
|
|
625
740
|
// src/utils/config.ts
|
|
741
|
+
var config_exports = {};
|
|
742
|
+
__export(config_exports, {
|
|
743
|
+
AgentConfigError: () => AgentConfigError,
|
|
744
|
+
BUILTIN_AGENTS: () => BUILTIN_AGENTS,
|
|
745
|
+
DEFAULT_ASSIGNMENT_TYPES: () => DEFAULT_ASSIGNMENT_TYPES,
|
|
746
|
+
deleteAgentsConfig: () => deleteAgentsConfig,
|
|
747
|
+
deleteHotkeyBindingsConfig: () => deleteHotkeyBindingsConfig,
|
|
748
|
+
deleteStatusConfig: () => deleteStatusConfig,
|
|
749
|
+
deleteThemeConfig: () => deleteThemeConfig,
|
|
750
|
+
getAgents: () => getAgents,
|
|
751
|
+
getAssignmentTypes: () => getAssignmentTypes,
|
|
752
|
+
parseAgentCommand: () => parseAgentCommand,
|
|
753
|
+
readConfig: () => readConfig,
|
|
754
|
+
updateAgentsConfig: () => updateAgentsConfig,
|
|
755
|
+
updateBackupConfig: () => updateBackupConfig,
|
|
756
|
+
updateIntegrationConfig: () => updateIntegrationConfig,
|
|
757
|
+
updateOnboardingConfig: () => updateOnboardingConfig,
|
|
758
|
+
updatePlaybooksConfig: () => updatePlaybooksConfig,
|
|
759
|
+
validateAgentList: () => validateAgentList,
|
|
760
|
+
writeAgentsConfig: () => writeAgentsConfig,
|
|
761
|
+
writeHotkeyBindingsConfig: () => writeHotkeyBindingsConfig,
|
|
762
|
+
writeStatusConfig: () => writeStatusConfig,
|
|
763
|
+
writeThemeConfig: () => writeThemeConfig
|
|
764
|
+
});
|
|
626
765
|
import { readFile as readFile3 } from "fs/promises";
|
|
627
766
|
import { resolve as resolve4, isAbsolute } from "path";
|
|
628
767
|
function parseAgentCommand(value, agentId) {
|
|
@@ -695,7 +834,8 @@ function cloneDefaultConfig() {
|
|
|
695
834
|
playbooks: {
|
|
696
835
|
disabled: [...DEFAULT_CONFIG.playbooks.disabled]
|
|
697
836
|
},
|
|
698
|
-
theme: DEFAULT_CONFIG.theme ? { ...DEFAULT_CONFIG.theme } : null
|
|
837
|
+
theme: DEFAULT_CONFIG.theme ? { ...DEFAULT_CONFIG.theme } : null,
|
|
838
|
+
hotkeys: DEFAULT_CONFIG.hotkeys ? { bindings: { ...DEFAULT_CONFIG.hotkeys.bindings } } : null
|
|
699
839
|
};
|
|
700
840
|
}
|
|
701
841
|
function parseFrontmatter(content) {
|
|
@@ -842,6 +982,25 @@ function serializeStatusConfig(statuses) {
|
|
|
842
982
|
}
|
|
843
983
|
return lines.join("\n");
|
|
844
984
|
}
|
|
985
|
+
function serializeIntegrationConfig(integrations) {
|
|
986
|
+
const lines = [];
|
|
987
|
+
if (integrations.claudePluginDir) {
|
|
988
|
+
lines.push(` claudePluginDir: ${integrations.claudePluginDir}`);
|
|
989
|
+
}
|
|
990
|
+
if (integrations.codexPluginDir) {
|
|
991
|
+
lines.push(` codexPluginDir: ${integrations.codexPluginDir}`);
|
|
992
|
+
}
|
|
993
|
+
if (integrations.codexMarketplacePath) {
|
|
994
|
+
lines.push(` codexMarketplacePath: ${integrations.codexMarketplacePath}`);
|
|
995
|
+
}
|
|
996
|
+
if (lines.length === 0) {
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
return ["integrations:", ...lines].join("\n");
|
|
1000
|
+
}
|
|
1001
|
+
function serializeOnboardingConfig(onboarding) {
|
|
1002
|
+
return ["onboarding:", ` completed: ${onboarding.completed ? "true" : "false"}`].join("\n");
|
|
1003
|
+
}
|
|
845
1004
|
function serializeBackupConfig(backup) {
|
|
846
1005
|
const lines = ["backup:"];
|
|
847
1006
|
lines.push(` repo: ${backup.repo ?? "null"}`);
|
|
@@ -991,6 +1150,101 @@ ${cleanedFm}
|
|
|
991
1150
|
---${afterFrontmatter}`;
|
|
992
1151
|
await writeFileForce(configPath, newContent);
|
|
993
1152
|
}
|
|
1153
|
+
function parseHotkeyBindingsConfig(content) {
|
|
1154
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1155
|
+
if (!match) return null;
|
|
1156
|
+
const fmBlock = match[1];
|
|
1157
|
+
const blockStart = fmBlock.match(/^hotkeys:\s*$/m);
|
|
1158
|
+
if (!blockStart) return null;
|
|
1159
|
+
const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
|
|
1160
|
+
const remaining = fmBlock.slice(startIdx).split("\n");
|
|
1161
|
+
const bindings = {};
|
|
1162
|
+
let inBindings = false;
|
|
1163
|
+
for (const line of remaining) {
|
|
1164
|
+
const trimmed = line.trimStart();
|
|
1165
|
+
const indent = line.length - trimmed.length;
|
|
1166
|
+
if (indent === 0 && trimmed.length > 0) break;
|
|
1167
|
+
if (trimmed === "") continue;
|
|
1168
|
+
if (indent === 2 && trimmed === "bindings:") {
|
|
1169
|
+
inBindings = true;
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
if (inBindings && indent === 4) {
|
|
1173
|
+
const colonIdx = trimmed.indexOf(":");
|
|
1174
|
+
if (colonIdx <= 0) continue;
|
|
1175
|
+
const rawKind = trimmed.slice(0, colonIdx).trim();
|
|
1176
|
+
const rawValue = trimmed.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, "");
|
|
1177
|
+
if (!isBindableActionKind(rawKind)) continue;
|
|
1178
|
+
if (rawValue.length === 0) continue;
|
|
1179
|
+
bindings[rawKind] = canonicalizeCombo(rawValue);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
if (Object.keys(bindings).length === 0) return null;
|
|
1183
|
+
return { bindings };
|
|
1184
|
+
}
|
|
1185
|
+
function serializeHotkeyBindingsConfig(cfg) {
|
|
1186
|
+
const lines = ["hotkeys:", " bindings:"];
|
|
1187
|
+
for (const kind of BINDABLE_ACTION_KINDS) {
|
|
1188
|
+
const value = cfg.bindings[kind];
|
|
1189
|
+
if (!value) continue;
|
|
1190
|
+
lines.push(` ${kind}: "${canonicalizeCombo(value)}"`);
|
|
1191
|
+
}
|
|
1192
|
+
if (lines.length === 2) return "";
|
|
1193
|
+
return lines.join("\n");
|
|
1194
|
+
}
|
|
1195
|
+
async function writeHotkeyBindingsConfig(cfg) {
|
|
1196
|
+
const cleaned = {};
|
|
1197
|
+
for (const kind of BINDABLE_ACTION_KINDS) {
|
|
1198
|
+
const raw = cfg.bindings[kind];
|
|
1199
|
+
if (typeof raw !== "string" || raw.trim() === "") continue;
|
|
1200
|
+
const canonical = canonicalizeCombo(raw);
|
|
1201
|
+
if (!canonical) continue;
|
|
1202
|
+
if (isReservedCombo(canonical)) continue;
|
|
1203
|
+
cleaned[kind] = canonical;
|
|
1204
|
+
}
|
|
1205
|
+
if (Object.keys(cleaned).length === 0) {
|
|
1206
|
+
await deleteHotkeyBindingsConfig();
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
const configPath = resolve4(syntaurRoot(), "config.md");
|
|
1210
|
+
const block = serializeHotkeyBindingsConfig({ bindings: cleaned });
|
|
1211
|
+
const existing = await fileExists(configPath) ? await readFile3(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
1212
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
1213
|
+
if (!fmMatch) {
|
|
1214
|
+
const content = `---
|
|
1215
|
+
version: "2.0"
|
|
1216
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
1217
|
+
${block}
|
|
1218
|
+
---
|
|
1219
|
+
${existing}`;
|
|
1220
|
+
await writeFileForce(configPath, content);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
const fmBlock = fmMatch[2];
|
|
1224
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
1225
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "hotkeys");
|
|
1226
|
+
const newFm = `${cleanedFm}
|
|
1227
|
+
${block}`.replace(/^\n+/, "");
|
|
1228
|
+
const normalizedFm = newFm.replace(/\n+$/, "");
|
|
1229
|
+
const newContent = `---
|
|
1230
|
+
${normalizedFm}
|
|
1231
|
+
---${afterFrontmatter}`;
|
|
1232
|
+
await writeFileForce(configPath, newContent);
|
|
1233
|
+
}
|
|
1234
|
+
async function deleteHotkeyBindingsConfig() {
|
|
1235
|
+
const configPath = resolve4(syntaurRoot(), "config.md");
|
|
1236
|
+
if (!await fileExists(configPath)) return;
|
|
1237
|
+
const existing = await readFile3(configPath, "utf-8");
|
|
1238
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
1239
|
+
if (!fmMatch) return;
|
|
1240
|
+
const fmBlock = fmMatch[2];
|
|
1241
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
1242
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "hotkeys");
|
|
1243
|
+
const newContent = `---
|
|
1244
|
+
${cleanedFm}
|
|
1245
|
+
---${afterFrontmatter}`;
|
|
1246
|
+
await writeFileForce(configPath, newContent);
|
|
1247
|
+
}
|
|
994
1248
|
function stripTopLevelBlock(fmBlock, key) {
|
|
995
1249
|
const blockStart = fmBlock.match(new RegExp(`^${key}:\\s*$`, "m"));
|
|
996
1250
|
if (!blockStart) {
|
|
@@ -1185,6 +1439,82 @@ function assignAgentField(target, key, rawValue) {
|
|
|
1185
1439
|
break;
|
|
1186
1440
|
}
|
|
1187
1441
|
}
|
|
1442
|
+
function yamlQuoteScalar(value) {
|
|
1443
|
+
if (/[\r\n]/.test(value)) {
|
|
1444
|
+
throw new AgentConfigError(
|
|
1445
|
+
`value contains newlines, which the agents config serializer does not support: ${JSON.stringify(value)}`
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
if (value === "" || /[:#{}[\],&*?|>!%@`"'\\\t]/.test(value) || /^\s|\s$/.test(value)) {
|
|
1449
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\t/g, "\\t");
|
|
1450
|
+
return `"${escaped}"`;
|
|
1451
|
+
}
|
|
1452
|
+
return value;
|
|
1453
|
+
}
|
|
1454
|
+
function serializeAgentsConfig(agents) {
|
|
1455
|
+
const lines = ["agents:"];
|
|
1456
|
+
for (const a of agents) {
|
|
1457
|
+
lines.push(` - id: ${yamlQuoteScalar(a.id)}`);
|
|
1458
|
+
lines.push(` label: ${yamlQuoteScalar(a.label)}`);
|
|
1459
|
+
lines.push(` command: ${yamlQuoteScalar(a.command)}`);
|
|
1460
|
+
if (a.args && a.args.length > 0) {
|
|
1461
|
+
lines.push(` args:`);
|
|
1462
|
+
for (const arg of a.args) {
|
|
1463
|
+
lines.push(` - ${yamlQuoteScalar(arg)}`);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
if (a.promptArgPosition && a.promptArgPosition !== "first") {
|
|
1467
|
+
lines.push(` promptArgPosition: ${a.promptArgPosition}`);
|
|
1468
|
+
}
|
|
1469
|
+
if (a.default) {
|
|
1470
|
+
lines.push(` default: true`);
|
|
1471
|
+
}
|
|
1472
|
+
if (a.resolveFromShellAliases) {
|
|
1473
|
+
lines.push(` resolveFromShellAliases: true`);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return lines.join("\n");
|
|
1477
|
+
}
|
|
1478
|
+
async function writeAgentsConfig(agents) {
|
|
1479
|
+
validateAgentList(agents);
|
|
1480
|
+
const configPath = resolve4(syntaurRoot(), "config.md");
|
|
1481
|
+
const agentsBlock = serializeAgentsConfig(agents);
|
|
1482
|
+
const existing = await fileExists(configPath) ? await readFile3(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
1483
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
1484
|
+
if (!fmMatch) {
|
|
1485
|
+
const content = `---
|
|
1486
|
+
version: "2.0"
|
|
1487
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
1488
|
+
${agentsBlock}
|
|
1489
|
+
---
|
|
1490
|
+
${existing}`;
|
|
1491
|
+
await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
const fmBlock = fmMatch[2];
|
|
1495
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
1496
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "agents");
|
|
1497
|
+
const newFm = `${cleanedFm}
|
|
1498
|
+
${agentsBlock}`.replace(/^\n+/, "").replace(/\n+$/, "");
|
|
1499
|
+
const newContent = `---
|
|
1500
|
+
${newFm}
|
|
1501
|
+
---${afterFrontmatter}`;
|
|
1502
|
+
await writeFileForce(configPath, newContent);
|
|
1503
|
+
}
|
|
1504
|
+
async function deleteAgentsConfig() {
|
|
1505
|
+
const configPath = resolve4(syntaurRoot(), "config.md");
|
|
1506
|
+
if (!await fileExists(configPath)) return;
|
|
1507
|
+
const existing = await readFile3(configPath, "utf-8");
|
|
1508
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
1509
|
+
if (!fmMatch) return;
|
|
1510
|
+
const fmBlock = fmMatch[2];
|
|
1511
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
1512
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "agents");
|
|
1513
|
+
const newContent = `---
|
|
1514
|
+
${cleanedFm}
|
|
1515
|
+
---${afterFrontmatter}`;
|
|
1516
|
+
await writeFileForce(configPath, newContent);
|
|
1517
|
+
}
|
|
1188
1518
|
async function writeStatusConfig(statuses) {
|
|
1189
1519
|
const configPath = resolve4(syntaurRoot(), "config.md");
|
|
1190
1520
|
const statusBlock = serializeStatusConfig(statuses);
|
|
@@ -1253,6 +1583,66 @@ ${cleanedFm}
|
|
|
1253
1583
|
---${afterFrontmatter}`;
|
|
1254
1584
|
await writeFileForce(configPath, newContent);
|
|
1255
1585
|
}
|
|
1586
|
+
async function updateIntegrationConfig(integrations) {
|
|
1587
|
+
const configPath = resolve4(syntaurRoot(), "config.md");
|
|
1588
|
+
const nextIntegrations = {
|
|
1589
|
+
...(await readConfig()).integrations,
|
|
1590
|
+
...integrations
|
|
1591
|
+
};
|
|
1592
|
+
const integrationBlock = serializeIntegrationConfig(nextIntegrations);
|
|
1593
|
+
const existing = await fileExists(configPath) ? await readFile3(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
1594
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
1595
|
+
if (!fmMatch) {
|
|
1596
|
+
const content = `---
|
|
1597
|
+
version: "2.0"
|
|
1598
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
1599
|
+
${integrationBlock ?? ""}
|
|
1600
|
+
---
|
|
1601
|
+
${existing}`;
|
|
1602
|
+
await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
const fmBlock = fmMatch[2];
|
|
1606
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
1607
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "integrations");
|
|
1608
|
+
const newFm = integrationBlock ? `${cleanedFm}
|
|
1609
|
+
${integrationBlock}`.replace(/^\n+/, "") : cleanedFm;
|
|
1610
|
+
const normalizedFm = newFm.replace(/\n+$/, "");
|
|
1611
|
+
const newContent = `---
|
|
1612
|
+
${normalizedFm}
|
|
1613
|
+
---${afterFrontmatter}`;
|
|
1614
|
+
await writeFileForce(configPath, newContent);
|
|
1615
|
+
}
|
|
1616
|
+
async function updateOnboardingConfig(onboarding) {
|
|
1617
|
+
const configPath = resolve4(syntaurRoot(), "config.md");
|
|
1618
|
+
const nextOnboarding = {
|
|
1619
|
+
...(await readConfig()).onboarding,
|
|
1620
|
+
...onboarding
|
|
1621
|
+
};
|
|
1622
|
+
const onboardingBlock = serializeOnboardingConfig(nextOnboarding);
|
|
1623
|
+
const existing = await fileExists(configPath) ? await readFile3(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
1624
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
1625
|
+
if (!fmMatch) {
|
|
1626
|
+
const content = `---
|
|
1627
|
+
version: "2.0"
|
|
1628
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
1629
|
+
${onboardingBlock}
|
|
1630
|
+
---
|
|
1631
|
+
${existing}`;
|
|
1632
|
+
await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
const fmBlock = fmMatch[2];
|
|
1636
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
1637
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "onboarding");
|
|
1638
|
+
const newFm = `${cleanedFm}
|
|
1639
|
+
${onboardingBlock}`.replace(/^\n+/, "");
|
|
1640
|
+
const normalizedFm = newFm.replace(/\n+$/, "");
|
|
1641
|
+
const newContent = `---
|
|
1642
|
+
${normalizedFm}
|
|
1643
|
+
---${afterFrontmatter}`;
|
|
1644
|
+
await writeFileForce(configPath, newContent);
|
|
1645
|
+
}
|
|
1256
1646
|
async function updateBackupConfig(backup) {
|
|
1257
1647
|
const configPath = resolve4(syntaurRoot(), "config.md");
|
|
1258
1648
|
const current = (await readConfig()).backup;
|
|
@@ -1347,10 +1737,28 @@ async function readConfig() {
|
|
|
1347
1737
|
types: null,
|
|
1348
1738
|
agents: normalizeAgentsFromConfig(parseAgentsConfig(content)),
|
|
1349
1739
|
playbooks: parsePlaybooksConfig(fmBlock),
|
|
1350
|
-
theme: parseThemeConfig(content)
|
|
1740
|
+
theme: parseThemeConfig(content),
|
|
1741
|
+
hotkeys: parseHotkeyBindingsConfig(content)
|
|
1351
1742
|
};
|
|
1352
1743
|
}
|
|
1353
|
-
|
|
1744
|
+
function getAssignmentTypes(config) {
|
|
1745
|
+
return config.types ?? DEFAULT_ASSIGNMENT_TYPES;
|
|
1746
|
+
}
|
|
1747
|
+
function getAgents(config) {
|
|
1748
|
+
return config.agents ?? BUILTIN_AGENTS;
|
|
1749
|
+
}
|
|
1750
|
+
async function updateAgentsConfig(mutation, options = {}) {
|
|
1751
|
+
const config = await readConfig();
|
|
1752
|
+
const previous = config.agents ?? [...BUILTIN_AGENTS];
|
|
1753
|
+
const next = mutation.apply(previous);
|
|
1754
|
+
validateAgentList(next);
|
|
1755
|
+
if (options.dryRun) {
|
|
1756
|
+
return { previous, next, written: false };
|
|
1757
|
+
}
|
|
1758
|
+
await writeAgentsConfig(next);
|
|
1759
|
+
return { previous, next, written: true };
|
|
1760
|
+
}
|
|
1761
|
+
var DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, BUILTIN_AGENTS, AGENT_ID_PATTERN, PROMPT_ARG_POSITIONS, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, migratedConfigPaths;
|
|
1354
1762
|
var init_config2 = __esm({
|
|
1355
1763
|
"src/utils/config.ts"() {
|
|
1356
1764
|
"use strict";
|
|
@@ -1358,6 +1766,17 @@ var init_config2 = __esm({
|
|
|
1358
1766
|
init_fs();
|
|
1359
1767
|
init_config();
|
|
1360
1768
|
init_fs_migration();
|
|
1769
|
+
init_hotkeysCatalog();
|
|
1770
|
+
DEFAULT_ASSIGNMENT_TYPES = {
|
|
1771
|
+
definitions: [
|
|
1772
|
+
{ id: "feature", label: "Feature" },
|
|
1773
|
+
{ id: "bug", label: "Bug" },
|
|
1774
|
+
{ id: "refactor", label: "Refactor" },
|
|
1775
|
+
{ id: "research", label: "Research" },
|
|
1776
|
+
{ id: "chore", label: "Chore" }
|
|
1777
|
+
],
|
|
1778
|
+
default: "feature"
|
|
1779
|
+
};
|
|
1361
1780
|
DEFAULT_CONFIG = {
|
|
1362
1781
|
version: "2.0",
|
|
1363
1782
|
defaultProjectDir: defaultProjectDir(),
|
|
@@ -1381,8 +1800,13 @@ var init_config2 = __esm({
|
|
|
1381
1800
|
playbooks: {
|
|
1382
1801
|
disabled: []
|
|
1383
1802
|
},
|
|
1384
|
-
theme: null
|
|
1803
|
+
theme: null,
|
|
1804
|
+
hotkeys: null
|
|
1385
1805
|
};
|
|
1806
|
+
BUILTIN_AGENTS = [
|
|
1807
|
+
{ id: "claude", label: "Claude", command: "claude", default: true },
|
|
1808
|
+
{ id: "codex", label: "Codex", command: "codex" }
|
|
1809
|
+
];
|
|
1386
1810
|
AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
1387
1811
|
PROMPT_ARG_POSITIONS = ["first", "last", "none"];
|
|
1388
1812
|
AUTO_CREATE_WORKTREE_VALUES = ["skip", "ask", "always"];
|
|
@@ -1785,10 +2209,10 @@ var init_playbooks = __esm({
|
|
|
1785
2209
|
// src/utils/assignment-resolver.ts
|
|
1786
2210
|
import { resolve as resolve6 } from "path";
|
|
1787
2211
|
import { readdir as readdir3, readFile as readFile5 } from "fs/promises";
|
|
1788
|
-
async function resolveAssignmentById(projectsDir,
|
|
2212
|
+
async function resolveAssignmentById(projectsDir, assignmentsDir2, id) {
|
|
1789
2213
|
let standaloneMatch = null;
|
|
1790
2214
|
let projectMatch = null;
|
|
1791
|
-
const standaloneDir = resolve6(
|
|
2215
|
+
const standaloneDir = resolve6(assignmentsDir2, id);
|
|
1792
2216
|
const standalonePath = resolve6(standaloneDir, "assignment.md");
|
|
1793
2217
|
if (await fileExists(standalonePath)) {
|
|
1794
2218
|
let workspaceGroup = null;
|
|
@@ -2560,7 +2984,7 @@ async function getGitInfo(cwd) {
|
|
|
2560
2984
|
}
|
|
2561
2985
|
return { branch: branch || null, worktree: isWorktree };
|
|
2562
2986
|
}
|
|
2563
|
-
async function loadWorkspaceRecords(projectsDir,
|
|
2987
|
+
async function loadWorkspaceRecords(projectsDir, assignmentsDir2) {
|
|
2564
2988
|
const records = [];
|
|
2565
2989
|
try {
|
|
2566
2990
|
const projects = await listProjects(projectsDir);
|
|
@@ -2592,12 +3016,12 @@ async function loadWorkspaceRecords(projectsDir, assignmentsDir) {
|
|
|
2592
3016
|
}
|
|
2593
3017
|
} catch {
|
|
2594
3018
|
}
|
|
2595
|
-
if (
|
|
3019
|
+
if (assignmentsDir2) {
|
|
2596
3020
|
try {
|
|
2597
|
-
const entries = await readdir5(
|
|
3021
|
+
const entries = await readdir5(assignmentsDir2);
|
|
2598
3022
|
for (const id of entries) {
|
|
2599
3023
|
if (id.startsWith(".") || id.startsWith("_")) continue;
|
|
2600
|
-
const aFile = resolve8(
|
|
3024
|
+
const aFile = resolve8(assignmentsDir2, id, "assignment.md");
|
|
2601
3025
|
try {
|
|
2602
3026
|
const raw = await readFile7(aFile, "utf-8");
|
|
2603
3027
|
const [fm] = extractFrontmatter2(raw);
|
|
@@ -2849,14 +3273,14 @@ var init_scanner = __esm({
|
|
|
2849
3273
|
// src/dashboard/api.ts
|
|
2850
3274
|
import { readdir as readdir6, readFile as readFile8, writeFile as writeFile3 } from "fs/promises";
|
|
2851
3275
|
import { resolve as resolve9, dirname as dirname2 } from "path";
|
|
2852
|
-
async function listStandaloneRecords(
|
|
2853
|
-
if (!
|
|
2854
|
-
if (!await fileExists(
|
|
2855
|
-
const entries = await readdir6(
|
|
3276
|
+
async function listStandaloneRecords(assignmentsDir2) {
|
|
3277
|
+
if (!assignmentsDir2) return [];
|
|
3278
|
+
if (!await fileExists(assignmentsDir2)) return [];
|
|
3279
|
+
const entries = await readdir6(assignmentsDir2, { withFileTypes: true });
|
|
2856
3280
|
const records = [];
|
|
2857
3281
|
for (const entry of entries) {
|
|
2858
3282
|
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
|
|
2859
|
-
const assignmentDir = resolve9(
|
|
3283
|
+
const assignmentDir = resolve9(assignmentsDir2, entry.name);
|
|
2860
3284
|
const assignmentMdPath = resolve9(assignmentDir, "assignment.md");
|
|
2861
3285
|
if (!await fileExists(assignmentMdPath)) continue;
|
|
2862
3286
|
try {
|
|
@@ -2942,11 +3366,11 @@ async function writeWorkspaceRegistry(projectsDir, workspaces) {
|
|
|
2942
3366
|
const registryPath = resolve9(dirname2(projectsDir), "workspaces.json");
|
|
2943
3367
|
await writeFile3(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
|
|
2944
3368
|
}
|
|
2945
|
-
async function listWorkspaces(projectsDir,
|
|
3369
|
+
async function listWorkspaces(projectsDir, assignmentsDir2) {
|
|
2946
3370
|
const [projectRecords, registered, standaloneRecords] = await Promise.all([
|
|
2947
3371
|
listProjectRecords(projectsDir),
|
|
2948
3372
|
readWorkspaceRegistry(projectsDir),
|
|
2949
|
-
listStandaloneRecords(
|
|
3373
|
+
listStandaloneRecords(assignmentsDir2)
|
|
2950
3374
|
]);
|
|
2951
3375
|
const workspaceSet = new Set(registered);
|
|
2952
3376
|
let hasUngrouped = false;
|
|
@@ -2980,16 +3404,16 @@ async function deleteWorkspace(projectsDir, name) {
|
|
|
2980
3404
|
const filtered = registered.filter((w) => w !== name);
|
|
2981
3405
|
await writeWorkspaceRegistry(projectsDir, filtered);
|
|
2982
3406
|
}
|
|
2983
|
-
async function getOverview(projectsDir, serversDir2,
|
|
3407
|
+
async function getOverview(projectsDir, serversDir2, assignmentsDir2) {
|
|
2984
3408
|
const projectRecords = await listProjectRecords(projectsDir);
|
|
2985
|
-
const standaloneRecords = await listStandaloneRecords(
|
|
3409
|
+
const standaloneRecords = await listStandaloneRecords(assignmentsDir2);
|
|
2986
3410
|
const attention = buildAttentionItems(projectRecords, standaloneRecords);
|
|
2987
3411
|
const recentActivity = buildRecentActivity(projectRecords, standaloneRecords);
|
|
2988
3412
|
let serverStats;
|
|
2989
3413
|
if (serversDir2) {
|
|
2990
3414
|
try {
|
|
2991
3415
|
const { scanAllSessions: scanAllSessions2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
|
|
2992
|
-
const servers = await scanAllSessions2(serversDir2, projectsDir, { assignmentsDir });
|
|
3416
|
+
const servers = await scanAllSessions2(serversDir2, projectsDir, { assignmentsDir: assignmentsDir2 });
|
|
2993
3417
|
if (servers.tmuxAvailable) {
|
|
2994
3418
|
const alive = servers.sessions.filter((s) => s.alive).length;
|
|
2995
3419
|
const totalPorts = servers.sessions.reduce((sum, s) => sum + s.windows.reduce((ws, w) => ws + w.panes.reduce((ps, p) => ps + p.ports.length, 0), 0), 0);
|
|
@@ -3035,14 +3459,14 @@ async function getOverview(projectsDir, serversDir2, assignmentsDir) {
|
|
|
3035
3459
|
serverStats
|
|
3036
3460
|
};
|
|
3037
3461
|
}
|
|
3038
|
-
async function getAttention(projectsDir, serversDir2,
|
|
3462
|
+
async function getAttention(projectsDir, serversDir2, assignmentsDir2) {
|
|
3039
3463
|
const projectRecords = await listProjectRecords(projectsDir);
|
|
3040
|
-
const standaloneRecords = await listStandaloneRecords(
|
|
3464
|
+
const standaloneRecords = await listStandaloneRecords(assignmentsDir2);
|
|
3041
3465
|
const items = buildAttentionItems(projectRecords, standaloneRecords);
|
|
3042
3466
|
if (serversDir2) {
|
|
3043
3467
|
try {
|
|
3044
3468
|
const { scanAllSessions: scanAllSessions2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
|
|
3045
|
-
const servers = await scanAllSessions2(serversDir2, projectsDir, { assignmentsDir });
|
|
3469
|
+
const servers = await scanAllSessions2(serversDir2, projectsDir, { assignmentsDir: assignmentsDir2 });
|
|
3046
3470
|
for (const session of servers.sessions) {
|
|
3047
3471
|
if (!session.alive) {
|
|
3048
3472
|
items.push({
|
|
@@ -3086,7 +3510,7 @@ async function getAttention(projectsDir, serversDir2, assignmentsDir) {
|
|
|
3086
3510
|
items: pagedItems
|
|
3087
3511
|
};
|
|
3088
3512
|
}
|
|
3089
|
-
async function listAssignmentsBoard(projectsDir,
|
|
3513
|
+
async function listAssignmentsBoard(projectsDir, assignmentsDir2) {
|
|
3090
3514
|
const projectRecords = await listProjectRecords(projectsDir);
|
|
3091
3515
|
const projectItems = await Promise.all(
|
|
3092
3516
|
projectRecords.flatMap(
|
|
@@ -3097,7 +3521,7 @@ async function listAssignmentsBoard(projectsDir, assignmentsDir) {
|
|
|
3097
3521
|
)
|
|
3098
3522
|
)
|
|
3099
3523
|
);
|
|
3100
|
-
const standaloneRecords = await listStandaloneRecords(
|
|
3524
|
+
const standaloneRecords = await listStandaloneRecords(assignmentsDir2);
|
|
3101
3525
|
const standaloneItems = await Promise.all(
|
|
3102
3526
|
standaloneRecords.map(async (sr) => toStandaloneBoardItem(sr))
|
|
3103
3527
|
);
|
|
@@ -3158,8 +3582,8 @@ async function getEditableDocument(projectsDir, documentType, projectSlug, assig
|
|
|
3158
3582
|
appendOnly: documentType === "handoff" || documentType === "decision-record"
|
|
3159
3583
|
};
|
|
3160
3584
|
}
|
|
3161
|
-
async function getEditableDocumentById(projectsDir,
|
|
3162
|
-
const resolved = await resolveAssignmentById(projectsDir,
|
|
3585
|
+
async function getEditableDocumentById(projectsDir, assignmentsDir2, documentType, id) {
|
|
3586
|
+
const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
|
|
3163
3587
|
if (!resolved) return null;
|
|
3164
3588
|
if (!resolved.standalone && resolved.projectSlug) {
|
|
3165
3589
|
return getEditableDocument(
|
|
@@ -3390,7 +3814,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
3390
3814
|
);
|
|
3391
3815
|
return detail;
|
|
3392
3816
|
}
|
|
3393
|
-
async function computeReferencedBy(target, projectsDir,
|
|
3817
|
+
async function computeReferencedBy(target, projectsDir, assignmentsDir2) {
|
|
3394
3818
|
const sources = [];
|
|
3395
3819
|
const projectRecords = await listProjectRecords(projectsDir);
|
|
3396
3820
|
for (const rec of projectRecords) {
|
|
@@ -3404,7 +3828,7 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
|
|
|
3404
3828
|
});
|
|
3405
3829
|
}
|
|
3406
3830
|
}
|
|
3407
|
-
const standaloneRecords = await listStandaloneRecords(
|
|
3831
|
+
const standaloneRecords = await listStandaloneRecords(assignmentsDir2);
|
|
3408
3832
|
for (const sr of standaloneRecords) {
|
|
3409
3833
|
sources.push({
|
|
3410
3834
|
id: sr.id,
|
|
@@ -3477,8 +3901,8 @@ function buildLinkPatternsForTarget(target) {
|
|
|
3477
3901
|
function escapeRegExpLocal(value) {
|
|
3478
3902
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3479
3903
|
}
|
|
3480
|
-
async function getAssignmentDetailById(projectsDir,
|
|
3481
|
-
const resolved = await resolveAssignmentById(projectsDir,
|
|
3904
|
+
async function getAssignmentDetailById(projectsDir, assignmentsDir2, id) {
|
|
3905
|
+
const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
|
|
3482
3906
|
if (!resolved) return null;
|
|
3483
3907
|
if (!resolved.standalone && resolved.projectSlug) {
|
|
3484
3908
|
const detail = await getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);
|
|
@@ -3486,7 +3910,7 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir, id) {
|
|
|
3486
3910
|
detail.referencedBy = await computeReferencedBy(
|
|
3487
3911
|
{ id: detail.id, projectSlug: detail.projectSlug, slug: detail.slug },
|
|
3488
3912
|
projectsDir,
|
|
3489
|
-
|
|
3913
|
+
assignmentsDir2
|
|
3490
3914
|
);
|
|
3491
3915
|
return detail;
|
|
3492
3916
|
}
|
|
@@ -3495,7 +3919,7 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir, id) {
|
|
|
3495
3919
|
standaloneDetail.referencedBy = await computeReferencedBy(
|
|
3496
3920
|
{ id: standaloneDetail.id, projectSlug: null, slug: standaloneDetail.slug },
|
|
3497
3921
|
projectsDir,
|
|
3498
|
-
|
|
3922
|
+
assignmentsDir2
|
|
3499
3923
|
);
|
|
3500
3924
|
return standaloneDetail;
|
|
3501
3925
|
}
|
|
@@ -3620,17 +4044,17 @@ async function listProjectRecords(projectsDir) {
|
|
|
3620
4044
|
return records;
|
|
3621
4045
|
}
|
|
3622
4046
|
async function listAssignmentRecords(projectPath) {
|
|
3623
|
-
const
|
|
3624
|
-
if (!await fileExists(
|
|
4047
|
+
const assignmentsDir2 = resolve9(projectPath, "assignments");
|
|
4048
|
+
if (!await fileExists(assignmentsDir2)) {
|
|
3625
4049
|
return [];
|
|
3626
4050
|
}
|
|
3627
|
-
const entries = await readdir6(
|
|
4051
|
+
const entries = await readdir6(assignmentsDir2, { withFileTypes: true });
|
|
3628
4052
|
const records = [];
|
|
3629
4053
|
for (const entry of entries) {
|
|
3630
4054
|
if (!entry.isDirectory()) {
|
|
3631
4055
|
continue;
|
|
3632
4056
|
}
|
|
3633
|
-
const assignmentMd = resolve9(
|
|
4057
|
+
const assignmentMd = resolve9(assignmentsDir2, entry.name, "assignment.md");
|
|
3634
4058
|
if (!await fileExists(assignmentMd)) {
|
|
3635
4059
|
continue;
|
|
3636
4060
|
}
|
|
@@ -4177,1236 +4601,1707 @@ var init_api = __esm({
|
|
|
4177
4601
|
}
|
|
4178
4602
|
});
|
|
4179
4603
|
|
|
4180
|
-
// src/
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
appendLogEntry: () => appendLogEntry2,
|
|
4184
|
-
archivePath: () => archivePath,
|
|
4185
|
-
checklistPath: () => checklistPath,
|
|
4186
|
-
computeCounts: () => computeCounts,
|
|
4187
|
-
generateShortId: () => generateShortId,
|
|
4188
|
-
generateUniqueId: () => generateUniqueId,
|
|
4189
|
-
logPath: () => logPath,
|
|
4190
|
-
parseChecklist: () => parseChecklist,
|
|
4191
|
-
parseChecklistItem: () => parseChecklistItem,
|
|
4192
|
-
parseLog: () => parseLog,
|
|
4193
|
-
readChecklist: () => readChecklist,
|
|
4194
|
-
readLog: () => readLog,
|
|
4195
|
-
serializeChecklist: () => serializeChecklist,
|
|
4196
|
-
serializeChecklistItem: () => serializeChecklistItem,
|
|
4197
|
-
serializeLogEntry: () => serializeLogEntry,
|
|
4198
|
-
writeChecklist: () => writeChecklist
|
|
4199
|
-
});
|
|
4200
|
-
import { randomBytes } from "crypto";
|
|
4201
|
-
import { readFile as readFile12 } from "fs/promises";
|
|
4202
|
-
import { resolve as resolve15 } from "path";
|
|
4203
|
-
function generateShortId() {
|
|
4204
|
-
return randomBytes(2).toString("hex");
|
|
4205
|
-
}
|
|
4206
|
-
function generateUniqueId(existingIds) {
|
|
4207
|
-
let id = generateShortId();
|
|
4208
|
-
let attempts = 0;
|
|
4209
|
-
while (existingIds.has(id) && attempts < 100) {
|
|
4210
|
-
id = generateShortId();
|
|
4211
|
-
attempts++;
|
|
4212
|
-
}
|
|
4213
|
-
return id;
|
|
4214
|
-
}
|
|
4215
|
-
function parseStatus2(marker) {
|
|
4216
|
-
if (marker === " ") return { status: "open", session: null };
|
|
4217
|
-
if (marker === "x") return { status: "completed", session: null };
|
|
4218
|
-
if (marker === "!") return { status: "blocked", session: null };
|
|
4219
|
-
if (marker.startsWith(">:")) return { status: "in_progress", session: marker.slice(2) };
|
|
4220
|
-
if (marker === ">") return { status: "in_progress", session: null };
|
|
4221
|
-
return { status: "open", session: null };
|
|
4604
|
+
// src/utils/slug.ts
|
|
4605
|
+
function slugify(title) {
|
|
4606
|
+
return title.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
4222
4607
|
}
|
|
4223
|
-
function
|
|
4224
|
-
return
|
|
4608
|
+
function isValidSlug(slug) {
|
|
4609
|
+
return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug);
|
|
4225
4610
|
}
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
return " ";
|
|
4230
|
-
case "completed":
|
|
4231
|
-
return "x";
|
|
4232
|
-
case "blocked":
|
|
4233
|
-
return "!";
|
|
4234
|
-
case "in_progress":
|
|
4235
|
-
return item.session ? `>:${sanitizeSession(item.session)}` : ">";
|
|
4611
|
+
var init_slug = __esm({
|
|
4612
|
+
"src/utils/slug.ts"() {
|
|
4613
|
+
"use strict";
|
|
4236
4614
|
}
|
|
4615
|
+
});
|
|
4616
|
+
|
|
4617
|
+
// src/utils/uuid.ts
|
|
4618
|
+
import { randomUUID } from "crypto";
|
|
4619
|
+
function generateId() {
|
|
4620
|
+
return randomUUID();
|
|
4237
4621
|
}
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
const marker = match[1];
|
|
4242
|
-
const rest = match[2];
|
|
4243
|
-
const { status, session } = parseStatus2(marker);
|
|
4244
|
-
const idMatch = rest.match(ID_REGEX);
|
|
4245
|
-
const id = idMatch ? idMatch[1] : "";
|
|
4246
|
-
const tags = [];
|
|
4247
|
-
let tagMatch;
|
|
4248
|
-
const tagRegex = new RegExp(TAG_REGEX.source, "g");
|
|
4249
|
-
while ((tagMatch = tagRegex.exec(rest)) !== null) {
|
|
4250
|
-
tags.push(tagMatch[1]);
|
|
4251
|
-
}
|
|
4252
|
-
let description = rest;
|
|
4253
|
-
const firstTagIdx = rest.search(/#[a-zA-Z0-9_-]/);
|
|
4254
|
-
const firstIdIdx = rest.search(/\[t:[a-f0-9]{4}\]/);
|
|
4255
|
-
const cutPoints = [firstTagIdx, firstIdIdx].filter((i) => i >= 0);
|
|
4256
|
-
if (cutPoints.length > 0) {
|
|
4257
|
-
description = rest.slice(0, Math.min(...cutPoints)).trim();
|
|
4258
|
-
}
|
|
4259
|
-
return { id, description, status, tags, session };
|
|
4260
|
-
}
|
|
4261
|
-
function serializeChecklistItem(item) {
|
|
4262
|
-
const marker = statusToMarker(item);
|
|
4263
|
-
const tagStr = item.tags.map((t) => `#${t}`).join(" ");
|
|
4264
|
-
const parts = [`- [${marker}] ${item.description}`];
|
|
4265
|
-
if (tagStr) parts.push(tagStr);
|
|
4266
|
-
parts.push(`[t:${item.id}]`);
|
|
4267
|
-
return parts.join(" ");
|
|
4268
|
-
}
|
|
4269
|
-
function parseChecklist(content) {
|
|
4270
|
-
const [fm, body] = extractFrontmatter2(content);
|
|
4271
|
-
const workspace = getField(fm, "workspace") || "_global";
|
|
4272
|
-
const archiveIntervalRaw = getField(fm, "archiveInterval") || "weekly";
|
|
4273
|
-
const archiveInterval = ["daily", "weekly", "monthly", "never"].includes(archiveIntervalRaw) ? archiveIntervalRaw : "weekly";
|
|
4274
|
-
const items = [];
|
|
4275
|
-
for (const line of body.split("\n")) {
|
|
4276
|
-
const item = parseChecklistItem(line);
|
|
4277
|
-
if (item) items.push(item);
|
|
4622
|
+
var init_uuid = __esm({
|
|
4623
|
+
"src/utils/uuid.ts"() {
|
|
4624
|
+
"use strict";
|
|
4278
4625
|
}
|
|
4279
|
-
|
|
4280
|
-
}
|
|
4281
|
-
function serializeChecklist(checklist) {
|
|
4282
|
-
const fm = [
|
|
4283
|
-
"---",
|
|
4284
|
-
`workspace: ${checklist.workspace}`,
|
|
4285
|
-
`archiveInterval: ${checklist.archiveInterval}`,
|
|
4286
|
-
"---"
|
|
4287
|
-
].join("\n");
|
|
4288
|
-
const header = "# Quick Todos";
|
|
4289
|
-
const items = checklist.items.map(serializeChecklistItem).join("\n");
|
|
4290
|
-
return `${fm}
|
|
4626
|
+
});
|
|
4291
4627
|
|
|
4292
|
-
|
|
4628
|
+
// src/templates/manifest.ts
|
|
4629
|
+
function renderManifest(params2) {
|
|
4630
|
+
return `---
|
|
4631
|
+
version: "2.0"
|
|
4632
|
+
project: ${params2.slug}
|
|
4633
|
+
generated: "${params2.timestamp}"
|
|
4634
|
+
---
|
|
4293
4635
|
|
|
4294
|
-
${
|
|
4636
|
+
# Project: ${params2.slug}
|
|
4637
|
+
|
|
4638
|
+
## Overview
|
|
4639
|
+
- [Project Overview](./project.md)
|
|
4640
|
+
|
|
4641
|
+
## Indexes
|
|
4642
|
+
- [Assignments](./_index-assignments.md)
|
|
4643
|
+
- [Plans](./_index-plans.md)
|
|
4644
|
+
- [Decision Records](./_index-decisions.md)
|
|
4645
|
+
- [Status](./_status.md)
|
|
4646
|
+
- [Resources](./resources/_index.md)
|
|
4647
|
+
- [Memories](./memories/_index.md)
|
|
4295
4648
|
`;
|
|
4296
4649
|
}
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
const entries = [];
|
|
4301
|
-
const sections = body.split(/^### /m).filter((s) => s.match(/^\d{4}-/));
|
|
4302
|
-
for (const section of sections) {
|
|
4303
|
-
const lines = section.split("\n");
|
|
4304
|
-
const heading = lines[0]?.trim() || "";
|
|
4305
|
-
const headingMatch = heading.match(/^(\S+)\s*—?\s*(.*)/);
|
|
4306
|
-
if (!headingMatch) continue;
|
|
4307
|
-
const timestamp = headingMatch[1];
|
|
4308
|
-
const idsPart = headingMatch[2] || "";
|
|
4309
|
-
const itemIds = [...idsPart.matchAll(/t:([a-f0-9]{4})/g)].map((m) => m[1]);
|
|
4310
|
-
const entry = {
|
|
4311
|
-
timestamp,
|
|
4312
|
-
itemIds,
|
|
4313
|
-
items: "",
|
|
4314
|
-
session: null,
|
|
4315
|
-
branch: null,
|
|
4316
|
-
summary: "",
|
|
4317
|
-
blockers: null,
|
|
4318
|
-
status: null
|
|
4319
|
-
};
|
|
4320
|
-
for (const line of lines.slice(1)) {
|
|
4321
|
-
const fieldMatch = line.match(/^\*\*(\w+):\*\*\s*(.*)/);
|
|
4322
|
-
if (!fieldMatch) continue;
|
|
4323
|
-
const key = fieldMatch[1].toLowerCase();
|
|
4324
|
-
const value = fieldMatch[2].trim();
|
|
4325
|
-
switch (key) {
|
|
4326
|
-
case "items":
|
|
4327
|
-
entry.items = value;
|
|
4328
|
-
break;
|
|
4329
|
-
case "session":
|
|
4330
|
-
entry.session = value;
|
|
4331
|
-
break;
|
|
4332
|
-
case "branch":
|
|
4333
|
-
entry.branch = value;
|
|
4334
|
-
break;
|
|
4335
|
-
case "summary":
|
|
4336
|
-
entry.summary = value;
|
|
4337
|
-
break;
|
|
4338
|
-
case "blockers":
|
|
4339
|
-
entry.blockers = value;
|
|
4340
|
-
break;
|
|
4341
|
-
case "status":
|
|
4342
|
-
entry.status = value;
|
|
4343
|
-
break;
|
|
4344
|
-
}
|
|
4345
|
-
}
|
|
4346
|
-
entries.push(entry);
|
|
4347
|
-
}
|
|
4348
|
-
return { workspace, entries };
|
|
4349
|
-
}
|
|
4350
|
-
function serializeLogEntry(entry) {
|
|
4351
|
-
const idStr = entry.itemIds.map((id) => `t:${id}`).join(", ");
|
|
4352
|
-
const lines = [`### ${entry.timestamp} \u2014 ${idStr}`];
|
|
4353
|
-
if (entry.items) lines.push(`**Items:** ${entry.items}`);
|
|
4354
|
-
if (entry.session) lines.push(`**Session:** ${entry.session}`);
|
|
4355
|
-
if (entry.branch) lines.push(`**Branch:** ${entry.branch}`);
|
|
4356
|
-
if (entry.summary) lines.push(`**Summary:** ${entry.summary}`);
|
|
4357
|
-
if (entry.blockers) lines.push(`**Blockers:** ${entry.blockers}`);
|
|
4358
|
-
if (entry.status) lines.push(`**Status:** ${entry.status}`);
|
|
4359
|
-
return lines.join("\n");
|
|
4360
|
-
}
|
|
4361
|
-
function checklistPath(todosDir2, workspace) {
|
|
4362
|
-
return resolve15(todosDir2, `${workspace}.md`);
|
|
4363
|
-
}
|
|
4364
|
-
function logPath(todosDir2, workspace) {
|
|
4365
|
-
return resolve15(todosDir2, `${workspace}-log.md`);
|
|
4366
|
-
}
|
|
4367
|
-
function archivePath(todosDir2, workspace, interval, now = /* @__PURE__ */ new Date()) {
|
|
4368
|
-
const year = now.getFullYear();
|
|
4369
|
-
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
4370
|
-
const day = String(now.getDate()).padStart(2, "0");
|
|
4371
|
-
let suffix;
|
|
4372
|
-
switch (interval) {
|
|
4373
|
-
case "daily":
|
|
4374
|
-
suffix = `${year}-${month}-${day}`;
|
|
4375
|
-
break;
|
|
4376
|
-
case "weekly": {
|
|
4377
|
-
const jan1 = new Date(year, 0, 1);
|
|
4378
|
-
const days = Math.floor((now.getTime() - jan1.getTime()) / 864e5);
|
|
4379
|
-
const week = String(Math.ceil((days + jan1.getDay() + 1) / 7)).padStart(2, "0");
|
|
4380
|
-
suffix = `${year}-W${week}`;
|
|
4381
|
-
break;
|
|
4382
|
-
}
|
|
4383
|
-
case "monthly":
|
|
4384
|
-
suffix = `${year}-${month}`;
|
|
4385
|
-
break;
|
|
4386
|
-
default:
|
|
4387
|
-
suffix = `${year}-${month}-${day}`;
|
|
4650
|
+
var init_manifest = __esm({
|
|
4651
|
+
"src/templates/manifest.ts"() {
|
|
4652
|
+
"use strict";
|
|
4388
4653
|
}
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
if (
|
|
4394
|
-
|
|
4654
|
+
});
|
|
4655
|
+
|
|
4656
|
+
// src/utils/yaml.ts
|
|
4657
|
+
function escapeYamlString(value) {
|
|
4658
|
+
if (value.includes("\n") || value.includes("\r")) {
|
|
4659
|
+
throw new Error(
|
|
4660
|
+
`YAML string values must be single-line. Got: "${value.slice(0, 50)}..."`
|
|
4661
|
+
);
|
|
4395
4662
|
}
|
|
4396
|
-
const
|
|
4397
|
-
return
|
|
4398
|
-
}
|
|
4399
|
-
async function writeChecklist(todosDir2, checklist) {
|
|
4400
|
-
await ensureDir(todosDir2);
|
|
4401
|
-
const path = checklistPath(todosDir2, checklist.workspace);
|
|
4402
|
-
await writeFileForce(path, serializeChecklist(checklist));
|
|
4663
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
4664
|
+
return `"${escaped}"`;
|
|
4403
4665
|
}
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
return { workspace, entries: [] };
|
|
4666
|
+
var init_yaml = __esm({
|
|
4667
|
+
"src/utils/yaml.ts"() {
|
|
4668
|
+
"use strict";
|
|
4408
4669
|
}
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
const
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4670
|
+
});
|
|
4671
|
+
|
|
4672
|
+
// src/templates/project.ts
|
|
4673
|
+
function renderProject(params2) {
|
|
4674
|
+
const safeTitle = escapeYamlString(params2.title);
|
|
4675
|
+
const workspaceLine = params2.workspace ? `
|
|
4676
|
+
workspace: ${params2.workspace}` : "";
|
|
4677
|
+
return `---
|
|
4678
|
+
id: ${params2.id}
|
|
4679
|
+
slug: ${params2.slug}
|
|
4680
|
+
title: ${safeTitle}
|
|
4681
|
+
archived: false
|
|
4682
|
+
archivedAt: null
|
|
4683
|
+
archivedReason: null
|
|
4684
|
+
created: "${params2.timestamp}"
|
|
4685
|
+
updated: "${params2.timestamp}"
|
|
4686
|
+
externalIds: []
|
|
4687
|
+
tags: []${workspaceLine}
|
|
4422
4688
|
---
|
|
4423
4689
|
|
|
4424
|
-
#
|
|
4690
|
+
# ${params2.title}
|
|
4691
|
+
|
|
4692
|
+
## Overview
|
|
4425
4693
|
|
|
4694
|
+
<!-- Describe the project goal, context, and success criteria here. -->
|
|
4695
|
+
|
|
4696
|
+
## Notes
|
|
4697
|
+
|
|
4698
|
+
<!-- Optional human notes, updates, or context. -->
|
|
4426
4699
|
`;
|
|
4427
|
-
content = fm + serializeLogEntry(entry) + "\n";
|
|
4428
|
-
}
|
|
4429
|
-
await writeFileForce(path, content);
|
|
4430
4700
|
}
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
for (const item of items) {
|
|
4434
|
-
counts[item.status]++;
|
|
4435
|
-
}
|
|
4436
|
-
return counts;
|
|
4437
|
-
}
|
|
4438
|
-
var ITEM_REGEX, ID_REGEX, TAG_REGEX;
|
|
4439
|
-
var init_parser2 = __esm({
|
|
4440
|
-
"src/todos/parser.ts"() {
|
|
4701
|
+
var init_project = __esm({
|
|
4702
|
+
"src/templates/project.ts"() {
|
|
4441
4703
|
"use strict";
|
|
4442
|
-
|
|
4443
|
-
init_fs();
|
|
4444
|
-
ITEM_REGEX = /^- \[([ x!]|>[^\]]*)\]\s+(.+)$/;
|
|
4445
|
-
ID_REGEX = /\[t:([a-f0-9]{4})\]/;
|
|
4446
|
-
TAG_REGEX = /#([a-zA-Z0-9_-]+)/g;
|
|
4704
|
+
init_yaml();
|
|
4447
4705
|
}
|
|
4448
4706
|
});
|
|
4449
4707
|
|
|
4450
|
-
// src/
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4708
|
+
// src/templates/assignment.ts
|
|
4709
|
+
function renderAssignment(params2) {
|
|
4710
|
+
const safeTitle = escapeYamlString(params2.title);
|
|
4711
|
+
const dependsOnYaml = params2.dependsOn.length === 0 ? "dependsOn: []" : `dependsOn:
|
|
4712
|
+
- ${params2.dependsOn.join("\n - ")}`;
|
|
4713
|
+
const linksYaml = params2.links.length === 0 ? "links: []" : `links:
|
|
4714
|
+
- ${params2.links.join("\n - ")}`;
|
|
4715
|
+
const projectYaml = `project: ${params2.project == null ? "null" : params2.project}`;
|
|
4716
|
+
const workspaceGroupLine = params2.workspaceGroup ? `
|
|
4717
|
+
workspaceGroup: ${params2.workspaceGroup}` : "";
|
|
4718
|
+
const typeYaml = `type: ${params2.type ?? "feature"}`;
|
|
4719
|
+
const todosSection = params2.includeTodos ? `## Todos
|
|
4720
|
+
|
|
4721
|
+
<!--
|
|
4722
|
+
Checklist of work items for this assignment. Items may be simple tasks
|
|
4723
|
+
or a markdown link to a plan file (e.g., "- [ ] Execute [plan](./plan.md)").
|
|
4724
|
+
When a plan is superseded by a new one, mark the old todo as:
|
|
4725
|
+
- [x] ~~Execute [old plan](./plan.md)~~ (superseded by plan-v2)
|
|
4726
|
+
Never delete superseded todos \u2014 preserve the history.
|
|
4727
|
+
-->
|
|
4728
|
+
|
|
4729
|
+
` : "";
|
|
4730
|
+
return `---
|
|
4731
|
+
id: ${params2.id}
|
|
4732
|
+
slug: ${params2.slug}
|
|
4733
|
+
title: ${safeTitle}
|
|
4734
|
+
${projectYaml}${workspaceGroupLine}
|
|
4735
|
+
${typeYaml}
|
|
4736
|
+
status: pending
|
|
4737
|
+
priority: ${params2.priority}
|
|
4738
|
+
created: "${params2.timestamp}"
|
|
4739
|
+
updated: "${params2.timestamp}"
|
|
4740
|
+
assignee: null
|
|
4741
|
+
externalIds: []
|
|
4742
|
+
${dependsOnYaml}
|
|
4743
|
+
${linksYaml}
|
|
4744
|
+
blockedReason: null
|
|
4745
|
+
workspace:
|
|
4746
|
+
repository: null
|
|
4747
|
+
worktreePath: null
|
|
4748
|
+
branch: null
|
|
4749
|
+
parentBranch: null
|
|
4750
|
+
tags: []
|
|
4751
|
+
---
|
|
4752
|
+
|
|
4753
|
+
# ${params2.title}
|
|
4754
|
+
|
|
4755
|
+
## Objective
|
|
4756
|
+
|
|
4757
|
+
<!-- Clear description of what needs to be done and why. -->
|
|
4758
|
+
|
|
4759
|
+
## Acceptance Criteria
|
|
4760
|
+
|
|
4761
|
+
- [ ] <!-- criterion 1 -->
|
|
4762
|
+
- [ ] <!-- criterion 2 -->
|
|
4763
|
+
- [ ] <!-- criterion 3 -->
|
|
4764
|
+
|
|
4765
|
+
${todosSection}## Context
|
|
4766
|
+
|
|
4767
|
+
<!-- Links to relevant docs, code, or other assignments. -->
|
|
4768
|
+
|
|
4769
|
+
## Links
|
|
4770
|
+
|
|
4771
|
+
- [Progress](./progress.md)
|
|
4772
|
+
- [Comments](./comments.md)
|
|
4773
|
+
- [Scratchpad](./scratchpad.md)
|
|
4774
|
+
- [Handoff](./handoff.md)
|
|
4775
|
+
- [Decision Record](./decision-record.md)
|
|
4490
4776
|
`;
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4777
|
+
}
|
|
4778
|
+
var init_assignment = __esm({
|
|
4779
|
+
"src/templates/assignment.ts"() {
|
|
4780
|
+
"use strict";
|
|
4781
|
+
init_yaml();
|
|
4782
|
+
}
|
|
4783
|
+
});
|
|
4784
|
+
|
|
4785
|
+
// src/templates/plan.ts
|
|
4786
|
+
var init_plan = __esm({
|
|
4787
|
+
"src/templates/plan.ts"() {
|
|
4788
|
+
"use strict";
|
|
4789
|
+
}
|
|
4790
|
+
});
|
|
4791
|
+
|
|
4792
|
+
// src/templates/scratchpad.ts
|
|
4793
|
+
function renderScratchpad(params2) {
|
|
4794
|
+
return `---
|
|
4795
|
+
assignment: ${params2.assignmentSlug}
|
|
4796
|
+
updated: "${params2.timestamp}"
|
|
4797
|
+
---
|
|
4798
|
+
|
|
4799
|
+
# Scratchpad
|
|
4800
|
+
|
|
4801
|
+
No working notes yet.
|
|
4494
4802
|
`;
|
|
4495
|
-
function initSessionDb(dbPath) {
|
|
4496
|
-
if (db) return db;
|
|
4497
|
-
const finalPath = dbPath ?? resolve10(syntaurRoot(), "syntaur.db");
|
|
4498
|
-
db = new Database(finalPath);
|
|
4499
|
-
db.pragma("journal_mode = WAL");
|
|
4500
|
-
db.exec(SCHEMA_SQL);
|
|
4501
|
-
db.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run(
|
|
4502
|
-
"schema_version",
|
|
4503
|
-
SCHEMA_VERSION
|
|
4504
|
-
);
|
|
4505
|
-
const database = db;
|
|
4506
|
-
const runMigrations = database.transaction(() => {
|
|
4507
|
-
const vBeforeV2 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
|
|
4508
|
-
if (vBeforeV2 === "1") {
|
|
4509
|
-
database.exec(`
|
|
4510
|
-
CREATE TABLE sessions_v2 (
|
|
4511
|
-
session_id TEXT PRIMARY KEY,
|
|
4512
|
-
project_slug TEXT,
|
|
4513
|
-
assignment_slug TEXT,
|
|
4514
|
-
agent TEXT NOT NULL,
|
|
4515
|
-
started TEXT NOT NULL,
|
|
4516
|
-
ended TEXT,
|
|
4517
|
-
status TEXT NOT NULL DEFAULT 'active',
|
|
4518
|
-
path TEXT,
|
|
4519
|
-
description TEXT,
|
|
4520
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4521
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4522
|
-
);
|
|
4523
|
-
INSERT INTO sessions_v2 SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, NULL, created_at, updated_at FROM sessions;
|
|
4524
|
-
DROP TABLE sessions;
|
|
4525
|
-
ALTER TABLE sessions_v2 RENAME TO sessions;
|
|
4526
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
|
|
4527
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
|
|
4528
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
4529
|
-
UPDATE meta SET value = '2' WHERE key = 'schema_version';
|
|
4530
|
-
`);
|
|
4531
|
-
}
|
|
4532
|
-
const vBeforeV3 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
|
|
4533
|
-
if (vBeforeV3 === "2") {
|
|
4534
|
-
const v2Columns = database.prepare("PRAGMA table_info(sessions)").all();
|
|
4535
|
-
const v2ColNames = v2Columns.map((c) => c.name);
|
|
4536
|
-
const hasProject = v2ColNames.includes("project_slug");
|
|
4537
|
-
const hasMission = v2ColNames.includes("mission_slug");
|
|
4538
|
-
const projectSlugExpr = hasProject && hasMission ? "COALESCE(project_slug, mission_slug)" : hasProject ? "project_slug" : hasMission ? "mission_slug" : null;
|
|
4539
|
-
if (!projectSlugExpr) {
|
|
4540
|
-
throw new Error(
|
|
4541
|
-
"sessions table has neither project_slug nor mission_slug; cannot migrate from v2 to v3"
|
|
4542
|
-
);
|
|
4543
|
-
}
|
|
4544
|
-
database.exec(`
|
|
4545
|
-
CREATE TABLE sessions_v3 (
|
|
4546
|
-
session_id TEXT PRIMARY KEY,
|
|
4547
|
-
project_slug TEXT,
|
|
4548
|
-
assignment_slug TEXT,
|
|
4549
|
-
agent TEXT NOT NULL,
|
|
4550
|
-
started TEXT NOT NULL,
|
|
4551
|
-
ended TEXT,
|
|
4552
|
-
status TEXT NOT NULL DEFAULT 'active',
|
|
4553
|
-
path TEXT,
|
|
4554
|
-
description TEXT,
|
|
4555
|
-
transcript_path TEXT,
|
|
4556
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4557
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4558
|
-
);
|
|
4559
|
-
INSERT INTO sessions_v3
|
|
4560
|
-
SELECT session_id, ${projectSlugExpr}, assignment_slug, agent, started, ended, status, path, description, NULL, created_at, updated_at
|
|
4561
|
-
FROM sessions;
|
|
4562
|
-
DROP TABLE sessions;
|
|
4563
|
-
ALTER TABLE sessions_v3 RENAME TO sessions;
|
|
4564
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
|
|
4565
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
|
|
4566
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
4567
|
-
UPDATE meta SET value = '3' WHERE key = 'schema_version';
|
|
4568
|
-
`);
|
|
4569
|
-
}
|
|
4570
|
-
});
|
|
4571
|
-
runMigrations.exclusive();
|
|
4572
|
-
db.exec(POST_MIGRATION_INDEXES_SQL);
|
|
4573
|
-
return db;
|
|
4574
4803
|
}
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
"Session database not initialized. Call initSessionDb() first."
|
|
4579
|
-
);
|
|
4804
|
+
var init_scratchpad = __esm({
|
|
4805
|
+
"src/templates/scratchpad.ts"() {
|
|
4806
|
+
"use strict";
|
|
4580
4807
|
}
|
|
4581
|
-
|
|
4808
|
+
});
|
|
4809
|
+
|
|
4810
|
+
// src/templates/handoff.ts
|
|
4811
|
+
function renderHandoff(params2) {
|
|
4812
|
+
return `---
|
|
4813
|
+
assignment: ${params2.assignmentSlug}
|
|
4814
|
+
updated: "${params2.timestamp}"
|
|
4815
|
+
handoffCount: 0
|
|
4816
|
+
---
|
|
4817
|
+
|
|
4818
|
+
# Handoff Log
|
|
4819
|
+
|
|
4820
|
+
No handoffs recorded yet.
|
|
4821
|
+
`;
|
|
4582
4822
|
}
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
db = null;
|
|
4823
|
+
var init_handoff = __esm({
|
|
4824
|
+
"src/templates/handoff.ts"() {
|
|
4825
|
+
"use strict";
|
|
4587
4826
|
}
|
|
4827
|
+
});
|
|
4828
|
+
|
|
4829
|
+
// src/templates/progress.ts
|
|
4830
|
+
function renderProgress(params2) {
|
|
4831
|
+
return `---
|
|
4832
|
+
assignment: ${params2.assignment}
|
|
4833
|
+
entryCount: 0
|
|
4834
|
+
generated: "${params2.timestamp}"
|
|
4835
|
+
updated: "${params2.timestamp}"
|
|
4836
|
+
---
|
|
4837
|
+
|
|
4838
|
+
# Progress
|
|
4839
|
+
|
|
4840
|
+
No progress yet.
|
|
4841
|
+
`;
|
|
4588
4842
|
}
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
if (count.count > 0) return 0;
|
|
4593
|
-
if (!await fileExists(projectsDir)) return 0;
|
|
4594
|
-
const entries = await readdir7(projectsDir, { withFileTypes: true });
|
|
4595
|
-
const allSessions = [];
|
|
4596
|
-
for (const entry of entries) {
|
|
4597
|
-
if (!entry.isDirectory()) continue;
|
|
4598
|
-
const projectDir = resolve10(projectsDir, entry.name);
|
|
4599
|
-
const indexPath = resolve10(projectDir, "_index-sessions.md");
|
|
4600
|
-
if (!await fileExists(indexPath)) continue;
|
|
4601
|
-
const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
|
|
4602
|
-
allSessions.push(...sessions);
|
|
4843
|
+
var init_progress = __esm({
|
|
4844
|
+
"src/templates/progress.ts"() {
|
|
4845
|
+
"use strict";
|
|
4603
4846
|
}
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4847
|
+
});
|
|
4848
|
+
|
|
4849
|
+
// src/templates/comments.ts
|
|
4850
|
+
function renderComments(params2) {
|
|
4851
|
+
return `---
|
|
4852
|
+
assignment: ${params2.assignment}
|
|
4853
|
+
entryCount: 0
|
|
4854
|
+
generated: "${params2.timestamp}"
|
|
4855
|
+
updated: "${params2.timestamp}"
|
|
4856
|
+
---
|
|
4857
|
+
|
|
4858
|
+
# Comments
|
|
4859
|
+
|
|
4860
|
+
No comments yet.
|
|
4861
|
+
`;
|
|
4617
4862
|
}
|
|
4618
|
-
|
|
4619
|
-
const
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
if (!trimmed) continue;
|
|
4628
|
-
if (trimmed.startsWith("| Assignment") || trimmed.startsWith("|Assignment")) {
|
|
4629
|
-
inTable = true;
|
|
4630
|
-
headerSeen = false;
|
|
4631
|
-
continue;
|
|
4632
|
-
}
|
|
4633
|
-
if (inTable && !headerSeen && trimmed.match(/^\|[-\s|]+\|$/)) {
|
|
4634
|
-
headerSeen = true;
|
|
4635
|
-
continue;
|
|
4636
|
-
}
|
|
4637
|
-
if (inTable && headerSeen && trimmed.startsWith("|")) {
|
|
4638
|
-
const cells = trimmed.split("|").slice(1, -1).map((c) => c.trim());
|
|
4639
|
-
if (cells.length >= 6) {
|
|
4640
|
-
sessions.push({
|
|
4641
|
-
assignmentSlug: cells[0],
|
|
4642
|
-
agent: cells[1],
|
|
4643
|
-
sessionId: cells[2],
|
|
4644
|
-
started: cells[3],
|
|
4645
|
-
status: cells[4] || "active",
|
|
4646
|
-
path: cells[5],
|
|
4647
|
-
projectSlug
|
|
4648
|
-
});
|
|
4649
|
-
}
|
|
4650
|
-
}
|
|
4863
|
+
function formatCommentEntry(comment) {
|
|
4864
|
+
const lines = [];
|
|
4865
|
+
lines.push(`## ${comment.id}`);
|
|
4866
|
+
lines.push("");
|
|
4867
|
+
lines.push(`**Recorded:** ${comment.timestamp}`);
|
|
4868
|
+
lines.push(`**Author:** ${comment.author}`);
|
|
4869
|
+
lines.push(`**Type:** ${comment.type}`);
|
|
4870
|
+
if (comment.replyTo) {
|
|
4871
|
+
lines.push(`**Reply to:** ${comment.replyTo}`);
|
|
4651
4872
|
}
|
|
4652
|
-
|
|
4873
|
+
if (comment.type === "question") {
|
|
4874
|
+
lines.push(`**Resolved:** ${comment.resolved ? "true" : "false"}`);
|
|
4875
|
+
}
|
|
4876
|
+
lines.push("");
|
|
4877
|
+
lines.push(comment.body.trim());
|
|
4878
|
+
lines.push("");
|
|
4879
|
+
return lines.join("\n");
|
|
4653
4880
|
}
|
|
4881
|
+
var init_comments = __esm({
|
|
4882
|
+
"src/templates/comments.ts"() {
|
|
4883
|
+
"use strict";
|
|
4884
|
+
}
|
|
4885
|
+
});
|
|
4654
4886
|
|
|
4655
|
-
// src/
|
|
4656
|
-
function
|
|
4657
|
-
return
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
transcriptPath: row.transcript_path ?? null
|
|
4668
|
-
};
|
|
4669
|
-
}
|
|
4670
|
-
async function appendSession(_projectDir, session) {
|
|
4671
|
-
const db2 = getSessionDb();
|
|
4672
|
-
db2.prepare(`
|
|
4673
|
-
INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path)
|
|
4674
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4675
|
-
ON CONFLICT(session_id) DO UPDATE SET
|
|
4676
|
-
project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),
|
|
4677
|
-
assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),
|
|
4678
|
-
agent = excluded.agent,
|
|
4679
|
-
status = CASE WHEN status IN ('completed','stopped') THEN status ELSE excluded.status END,
|
|
4680
|
-
path = COALESCE(NULLIF(excluded.path, ''), path),
|
|
4681
|
-
description = COALESCE(NULLIF(excluded.description, ''), description),
|
|
4682
|
-
transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),
|
|
4683
|
-
updated_at = datetime('now')
|
|
4684
|
-
`).run(
|
|
4685
|
-
session.sessionId,
|
|
4686
|
-
session.projectSlug ?? null,
|
|
4687
|
-
session.assignmentSlug ?? null,
|
|
4688
|
-
session.agent,
|
|
4689
|
-
session.started,
|
|
4690
|
-
session.status,
|
|
4691
|
-
session.path,
|
|
4692
|
-
session.description ?? null,
|
|
4693
|
-
session.transcriptPath ?? null
|
|
4694
|
-
);
|
|
4695
|
-
}
|
|
4696
|
-
async function updateSessionStatus(_projectDir, sessionId, status) {
|
|
4697
|
-
const db2 = getSessionDb();
|
|
4698
|
-
const isTerminal = status === "completed" || status === "stopped";
|
|
4699
|
-
const result = isTerminal ? db2.prepare(
|
|
4700
|
-
"UPDATE sessions SET status = ?, ended = datetime('now'), updated_at = datetime('now') WHERE session_id = ?"
|
|
4701
|
-
).run(status, sessionId) : db2.prepare(
|
|
4702
|
-
"UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE session_id = ?"
|
|
4703
|
-
).run(status, sessionId);
|
|
4704
|
-
return result.changes > 0;
|
|
4705
|
-
}
|
|
4706
|
-
async function listAllSessions(_projectsDir) {
|
|
4707
|
-
const db2 = getSessionDb();
|
|
4708
|
-
const rows = db2.prepare("SELECT * FROM sessions ORDER BY started DESC").all();
|
|
4709
|
-
return rows.map(rowToSession);
|
|
4887
|
+
// src/templates/decision-record.ts
|
|
4888
|
+
function renderDecisionRecord(params2) {
|
|
4889
|
+
return `---
|
|
4890
|
+
assignment: ${params2.assignmentSlug}
|
|
4891
|
+
updated: "${params2.timestamp}"
|
|
4892
|
+
decisionCount: 0
|
|
4893
|
+
---
|
|
4894
|
+
|
|
4895
|
+
# Decision Record
|
|
4896
|
+
|
|
4897
|
+
No decisions recorded yet.
|
|
4898
|
+
`;
|
|
4710
4899
|
}
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
const rows2 = db2.prepare(
|
|
4715
|
-
"SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
|
|
4716
|
-
).all(projectSlug, assignmentSlug);
|
|
4717
|
-
return rows2.map(rowToSession);
|
|
4900
|
+
var init_decision_record = __esm({
|
|
4901
|
+
"src/templates/decision-record.ts"() {
|
|
4902
|
+
"use strict";
|
|
4718
4903
|
}
|
|
4719
|
-
|
|
4720
|
-
|
|
4904
|
+
});
|
|
4905
|
+
|
|
4906
|
+
// src/templates/index-stubs.ts
|
|
4907
|
+
function renderIndexAssignments(params2) {
|
|
4908
|
+
return `---
|
|
4909
|
+
project: ${params2.slug}
|
|
4910
|
+
generated: "${params2.timestamp}"
|
|
4911
|
+
total: 0
|
|
4912
|
+
by_status:
|
|
4913
|
+
pending: 0
|
|
4914
|
+
in_progress: 0
|
|
4915
|
+
blocked: 0
|
|
4916
|
+
review: 0
|
|
4917
|
+
completed: 0
|
|
4918
|
+
failed: 0
|
|
4919
|
+
---
|
|
4920
|
+
|
|
4921
|
+
# Assignments
|
|
4922
|
+
|
|
4923
|
+
| Slug | Title | Status | Priority | Assignee | Dependencies | Updated |
|
|
4924
|
+
|------|-------|--------|----------|----------|--------------|---------|
|
|
4925
|
+
`;
|
|
4721
4926
|
}
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4927
|
+
function renderIndexPlans(params2) {
|
|
4928
|
+
return `---
|
|
4929
|
+
project: ${params2.slug}
|
|
4930
|
+
generated: "${params2.timestamp}"
|
|
4931
|
+
---
|
|
4932
|
+
|
|
4933
|
+
# Plans
|
|
4934
|
+
|
|
4935
|
+
| Assignment | Plan Status | Updated |
|
|
4936
|
+
|------------|-------------|---------|
|
|
4937
|
+
`;
|
|
4728
4938
|
}
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4939
|
+
function renderIndexDecisions(params2) {
|
|
4940
|
+
return `---
|
|
4941
|
+
project: ${params2.slug}
|
|
4942
|
+
generated: "${params2.timestamp}"
|
|
4943
|
+
---
|
|
4944
|
+
|
|
4945
|
+
# Decision Records
|
|
4946
|
+
|
|
4947
|
+
| Assignment | Count | Latest Decision | Latest Status | Updated |
|
|
4948
|
+
|------------|-------|-----------------|---------------|---------|
|
|
4949
|
+
`;
|
|
4735
4950
|
}
|
|
4736
|
-
|
|
4737
|
-
return
|
|
4738
|
-
|
|
4739
|
-
|
|
4951
|
+
function renderStatus(params2) {
|
|
4952
|
+
return `---
|
|
4953
|
+
project: ${params2.slug}
|
|
4954
|
+
generated: "${params2.timestamp}"
|
|
4955
|
+
status: pending
|
|
4956
|
+
progress:
|
|
4957
|
+
total: 0
|
|
4958
|
+
completed: 0
|
|
4959
|
+
in_progress: 0
|
|
4960
|
+
blocked: 0
|
|
4961
|
+
pending: 0
|
|
4962
|
+
review: 0
|
|
4963
|
+
failed: 0
|
|
4964
|
+
needsAttention:
|
|
4965
|
+
blockedCount: 0
|
|
4966
|
+
failedCount: 0
|
|
4967
|
+
openQuestions: 0
|
|
4968
|
+
---
|
|
4969
|
+
|
|
4970
|
+
# Project Status: ${params2.title}
|
|
4971
|
+
|
|
4972
|
+
**Status:** pending
|
|
4973
|
+
**Progress:** 0/0 assignments complete
|
|
4974
|
+
|
|
4975
|
+
## Assignments
|
|
4976
|
+
|
|
4977
|
+
No assignments yet.
|
|
4978
|
+
|
|
4979
|
+
## Dependency Graph
|
|
4980
|
+
|
|
4981
|
+
No dependencies yet.
|
|
4982
|
+
|
|
4983
|
+
## Needs Attention
|
|
4984
|
+
|
|
4985
|
+
- **0 blocked** assignments
|
|
4986
|
+
- **0 failed** assignments
|
|
4987
|
+
- **0 unanswered** questions
|
|
4988
|
+
`;
|
|
4740
4989
|
}
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
seen.add(key);
|
|
4754
|
-
if (session.project_slug) {
|
|
4755
|
-
const status = await readAssignmentStatus(
|
|
4756
|
-
resolve11(projectsDir, session.project_slug),
|
|
4757
|
-
aslug
|
|
4758
|
-
);
|
|
4759
|
-
if (status) assignmentStatuses.set(key, status);
|
|
4760
|
-
} else if (assignmentsDir) {
|
|
4761
|
-
const status = await readAssignmentStatusFromPath(
|
|
4762
|
-
resolve11(assignmentsDir, aslug, "assignment.md")
|
|
4763
|
-
);
|
|
4764
|
-
if (status) assignmentStatuses.set(key, status);
|
|
4765
|
-
}
|
|
4766
|
-
}
|
|
4767
|
-
let totalUpdated = 0;
|
|
4768
|
-
for (const session of activeSessions) {
|
|
4769
|
-
const projectKey = session.project_slug ?? "__standalone__";
|
|
4770
|
-
const key = `${projectKey}/${session.assignment_slug}`;
|
|
4771
|
-
const assignmentStatus = assignmentStatuses.get(key);
|
|
4772
|
-
if (!assignmentStatus || !DONE_ASSIGNMENT_STATUSES.has(assignmentStatus)) continue;
|
|
4773
|
-
const newStatus = assignmentStatus === "failed" ? "stopped" : "completed";
|
|
4774
|
-
await updateSessionStatus("", session.session_id, newStatus);
|
|
4775
|
-
totalUpdated++;
|
|
4776
|
-
}
|
|
4777
|
-
return totalUpdated;
|
|
4990
|
+
function renderResourcesIndex(params2) {
|
|
4991
|
+
return `---
|
|
4992
|
+
project: ${params2.slug}
|
|
4993
|
+
generated: "${params2.timestamp}"
|
|
4994
|
+
total: 0
|
|
4995
|
+
---
|
|
4996
|
+
|
|
4997
|
+
# Resources
|
|
4998
|
+
|
|
4999
|
+
| Name | Category | Source | Related Assignments | Updated |
|
|
5000
|
+
|------|----------|--------|---------------------|---------|
|
|
5001
|
+
`;
|
|
4778
5002
|
}
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
5003
|
+
function renderMemoriesIndex(params2) {
|
|
5004
|
+
return `---
|
|
5005
|
+
project: ${params2.slug}
|
|
5006
|
+
generated: "${params2.timestamp}"
|
|
5007
|
+
total: 0
|
|
5008
|
+
---
|
|
5009
|
+
|
|
5010
|
+
# Memories
|
|
5011
|
+
|
|
5012
|
+
| Name | Source | Scope | Source Assignment | Updated |
|
|
5013
|
+
|------|--------|-------|------------------|---------|
|
|
5014
|
+
`;
|
|
4787
5015
|
}
|
|
5016
|
+
var init_index_stubs = __esm({
|
|
5017
|
+
"src/templates/index-stubs.ts"() {
|
|
5018
|
+
"use strict";
|
|
5019
|
+
}
|
|
5020
|
+
});
|
|
4788
5021
|
|
|
4789
|
-
// src/
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
isProjectTodos = true;
|
|
4812
|
-
}
|
|
4813
|
-
const debounceKey = isProjectTodos ? `todos:${projectSlug}` : assignmentSlug ? `${projectSlug}/${assignmentSlug}` : projectSlug;
|
|
4814
|
-
const existing = pendingEvents.get(debounceKey);
|
|
4815
|
-
if (existing) clearTimeout(existing);
|
|
4816
|
-
const messageType = isProjectTodos ? "todos-updated" : assignmentSlug ? "assignment-updated" : "project-updated";
|
|
4817
|
-
pendingEvents.set(
|
|
4818
|
-
debounceKey,
|
|
4819
|
-
setTimeout(() => {
|
|
4820
|
-
pendingEvents.delete(debounceKey);
|
|
4821
|
-
const message = isProjectTodos ? {
|
|
4822
|
-
type: "todos-updated",
|
|
4823
|
-
projectSlug,
|
|
4824
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4825
|
-
} : {
|
|
4826
|
-
type: messageType,
|
|
4827
|
-
projectSlug,
|
|
4828
|
-
assignmentSlug,
|
|
4829
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4830
|
-
};
|
|
4831
|
-
onMessage(message);
|
|
4832
|
-
}, debounceMs)
|
|
4833
|
-
);
|
|
5022
|
+
// src/templates/playbook.ts
|
|
5023
|
+
function renderPlaybook(params2) {
|
|
5024
|
+
const whenToUse = params2.whenToUse ? escapeYamlString(params2.whenToUse) : "null";
|
|
5025
|
+
return `---
|
|
5026
|
+
name: ${escapeYamlString(params2.name)}
|
|
5027
|
+
slug: ${params2.slug}
|
|
5028
|
+
description: ${escapeYamlString(params2.description)}
|
|
5029
|
+
when_to_use: ${whenToUse}
|
|
5030
|
+
created: "${params2.timestamp}"
|
|
5031
|
+
updated: "${params2.timestamp}"
|
|
5032
|
+
tags: []
|
|
5033
|
+
---
|
|
5034
|
+
|
|
5035
|
+
# ${params2.name}
|
|
5036
|
+
|
|
5037
|
+
<!-- Write imperative rules and workflows here. Keep it under 50 lines. -->
|
|
5038
|
+
`;
|
|
5039
|
+
}
|
|
5040
|
+
var init_playbook = __esm({
|
|
5041
|
+
"src/templates/playbook.ts"() {
|
|
5042
|
+
"use strict";
|
|
5043
|
+
init_yaml();
|
|
4834
5044
|
}
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
const rel = relative(assignmentsDir, filePath);
|
|
4842
|
-
const parts = rel.split(sep);
|
|
4843
|
-
if (parts.length === 0) return;
|
|
4844
|
-
const assignmentId = parts[0];
|
|
4845
|
-
if (!assignmentId) return;
|
|
4846
|
-
const debounceKey = `__standalone__/${assignmentId}`;
|
|
4847
|
-
const existing = pendingEvents.get(debounceKey);
|
|
4848
|
-
if (existing) clearTimeout(existing);
|
|
4849
|
-
pendingEvents.set(
|
|
4850
|
-
debounceKey,
|
|
4851
|
-
setTimeout(() => {
|
|
4852
|
-
pendingEvents.delete(debounceKey);
|
|
4853
|
-
const message = {
|
|
4854
|
-
type: "assignment-updated",
|
|
4855
|
-
projectSlug: null,
|
|
4856
|
-
assignmentSlug: assignmentId,
|
|
4857
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4858
|
-
};
|
|
4859
|
-
onMessage(message);
|
|
4860
|
-
}, debounceMs)
|
|
4861
|
-
);
|
|
4862
|
-
};
|
|
4863
|
-
var handleStandaloneChange = handleStandaloneChange2;
|
|
4864
|
-
standaloneWatcher = watch(assignmentsDir, {
|
|
4865
|
-
ignoreInitial: true,
|
|
4866
|
-
persistent: true,
|
|
4867
|
-
depth: 5,
|
|
4868
|
-
ignored: /(^|[\/\\])\../
|
|
4869
|
-
});
|
|
4870
|
-
standaloneWatcher.on("change", handleStandaloneChange2);
|
|
4871
|
-
standaloneWatcher.on("add", handleStandaloneChange2);
|
|
4872
|
-
standaloneWatcher.on("unlink", handleStandaloneChange2);
|
|
5045
|
+
});
|
|
5046
|
+
|
|
5047
|
+
// src/templates/cursor-rules.ts
|
|
5048
|
+
var init_cursor_rules = __esm({
|
|
5049
|
+
"src/templates/cursor-rules.ts"() {
|
|
5050
|
+
"use strict";
|
|
4873
5051
|
}
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
pendingEvents.set(
|
|
4881
|
-
debounceKey,
|
|
4882
|
-
setTimeout(() => {
|
|
4883
|
-
pendingEvents.delete(debounceKey);
|
|
4884
|
-
const message = {
|
|
4885
|
-
type: "servers-updated",
|
|
4886
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4887
|
-
};
|
|
4888
|
-
onMessage(message);
|
|
4889
|
-
}, debounceMs)
|
|
4890
|
-
);
|
|
4891
|
-
};
|
|
4892
|
-
var handleServerChange = handleServerChange2;
|
|
4893
|
-
serversWatcher = watch(serversDir2, {
|
|
4894
|
-
ignoreInitial: true,
|
|
4895
|
-
persistent: true,
|
|
4896
|
-
depth: 1,
|
|
4897
|
-
ignored: /(^|[\/\\])\../
|
|
4898
|
-
});
|
|
4899
|
-
serversWatcher.on("change", handleServerChange2);
|
|
4900
|
-
serversWatcher.on("add", handleServerChange2);
|
|
4901
|
-
serversWatcher.on("unlink", handleServerChange2);
|
|
5052
|
+
});
|
|
5053
|
+
|
|
5054
|
+
// src/templates/codex-agents.ts
|
|
5055
|
+
var init_codex_agents = __esm({
|
|
5056
|
+
"src/templates/codex-agents.ts"() {
|
|
5057
|
+
"use strict";
|
|
4902
5058
|
}
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
pendingEvents.set(
|
|
4910
|
-
debounceKey,
|
|
4911
|
-
setTimeout(() => {
|
|
4912
|
-
pendingEvents.delete(debounceKey);
|
|
4913
|
-
const message = {
|
|
4914
|
-
type: "playbooks-updated",
|
|
4915
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4916
|
-
};
|
|
4917
|
-
onMessage(message);
|
|
4918
|
-
}, debounceMs)
|
|
4919
|
-
);
|
|
4920
|
-
};
|
|
4921
|
-
var handlePlaybookChange = handlePlaybookChange2;
|
|
4922
|
-
playbooksWatcher = watch(playbooksDir2, {
|
|
4923
|
-
ignoreInitial: true,
|
|
4924
|
-
persistent: true,
|
|
4925
|
-
depth: 1,
|
|
4926
|
-
ignored: /(^|[\/\\])\../
|
|
4927
|
-
});
|
|
4928
|
-
playbooksWatcher.on("change", handlePlaybookChange2);
|
|
4929
|
-
playbooksWatcher.on("add", handlePlaybookChange2);
|
|
4930
|
-
playbooksWatcher.on("unlink", handlePlaybookChange2);
|
|
5059
|
+
});
|
|
5060
|
+
|
|
5061
|
+
// src/templates/opencode-config.ts
|
|
5062
|
+
var init_opencode_config = __esm({
|
|
5063
|
+
"src/templates/opencode-config.ts"() {
|
|
5064
|
+
"use strict";
|
|
4931
5065
|
}
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
persistent: true,
|
|
4954
|
-
depth: 1,
|
|
4955
|
-
ignored: /(^|[\/\\])\../
|
|
4956
|
-
});
|
|
4957
|
-
todosWatcher.on("change", handleTodoChange2);
|
|
4958
|
-
todosWatcher.on("add", handleTodoChange2);
|
|
4959
|
-
todosWatcher.on("unlink", handleTodoChange2);
|
|
5066
|
+
});
|
|
5067
|
+
|
|
5068
|
+
// src/templates/index.ts
|
|
5069
|
+
var init_templates = __esm({
|
|
5070
|
+
"src/templates/index.ts"() {
|
|
5071
|
+
"use strict";
|
|
5072
|
+
init_config();
|
|
5073
|
+
init_manifest();
|
|
5074
|
+
init_project();
|
|
5075
|
+
init_assignment();
|
|
5076
|
+
init_plan();
|
|
5077
|
+
init_scratchpad();
|
|
5078
|
+
init_handoff();
|
|
5079
|
+
init_progress();
|
|
5080
|
+
init_comments();
|
|
5081
|
+
init_decision_record();
|
|
5082
|
+
init_index_stubs();
|
|
5083
|
+
init_playbook();
|
|
5084
|
+
init_cursor_rules();
|
|
5085
|
+
init_codex_agents();
|
|
5086
|
+
init_opencode_config();
|
|
4960
5087
|
}
|
|
4961
|
-
|
|
4962
|
-
close: async () => {
|
|
4963
|
-
pendingEvents.forEach((timeout) => {
|
|
4964
|
-
clearTimeout(timeout);
|
|
4965
|
-
});
|
|
4966
|
-
pendingEvents.clear();
|
|
4967
|
-
await projectsWatcher.close();
|
|
4968
|
-
if (standaloneWatcher) await standaloneWatcher.close();
|
|
4969
|
-
if (serversWatcher) await serversWatcher.close();
|
|
4970
|
-
if (playbooksWatcher) await playbooksWatcher.close();
|
|
4971
|
-
if (todosWatcher) await todosWatcher.close();
|
|
4972
|
-
}
|
|
4973
|
-
};
|
|
4974
|
-
}
|
|
4975
|
-
|
|
4976
|
-
// src/dashboard/server.ts
|
|
4977
|
-
init_fs();
|
|
4978
|
-
init_config2();
|
|
4979
|
-
|
|
4980
|
-
// src/dashboard/api-write.ts
|
|
4981
|
-
init_lifecycle();
|
|
4982
|
-
import { Router } from "express";
|
|
4983
|
-
import { resolve as resolve12 } from "path";
|
|
4984
|
-
import { rm, readFile as readFile10 } from "fs/promises";
|
|
5088
|
+
});
|
|
4985
5089
|
|
|
4986
|
-
// src/
|
|
4987
|
-
|
|
4988
|
-
|
|
5090
|
+
// src/todos/parser.ts
|
|
5091
|
+
var parser_exports = {};
|
|
5092
|
+
__export(parser_exports, {
|
|
5093
|
+
appendLogEntry: () => appendLogEntry2,
|
|
5094
|
+
archivePath: () => archivePath,
|
|
5095
|
+
checklistPath: () => checklistPath,
|
|
5096
|
+
computeCounts: () => computeCounts,
|
|
5097
|
+
decodeMetaValue: () => decodeMetaValue,
|
|
5098
|
+
encodeMetaValue: () => encodeMetaValue,
|
|
5099
|
+
generateShortId: () => generateShortId,
|
|
5100
|
+
generateUniqueId: () => generateUniqueId,
|
|
5101
|
+
logPath: () => logPath,
|
|
5102
|
+
parseChecklist: () => parseChecklist,
|
|
5103
|
+
parseChecklistItem: () => parseChecklistItem,
|
|
5104
|
+
parseLog: () => parseLog,
|
|
5105
|
+
parseMetaToken: () => parseMetaToken,
|
|
5106
|
+
readChecklist: () => readChecklist,
|
|
5107
|
+
readLog: () => readLog,
|
|
5108
|
+
serializeChecklist: () => serializeChecklist,
|
|
5109
|
+
serializeChecklistItem: () => serializeChecklistItem,
|
|
5110
|
+
serializeLogEntry: () => serializeLogEntry,
|
|
5111
|
+
serializeMetaToken: () => serializeMetaToken,
|
|
5112
|
+
writeChecklist: () => writeChecklist
|
|
5113
|
+
});
|
|
5114
|
+
import { randomBytes } from "crypto";
|
|
5115
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
5116
|
+
import { resolve as resolve15 } from "path";
|
|
5117
|
+
function generateShortId() {
|
|
5118
|
+
return randomBytes(2).toString("hex");
|
|
4989
5119
|
}
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
5120
|
+
function generateUniqueId(existingIds) {
|
|
5121
|
+
let id = generateShortId();
|
|
5122
|
+
let attempts = 0;
|
|
5123
|
+
while (existingIds.has(id) && attempts < 100) {
|
|
5124
|
+
id = generateShortId();
|
|
5125
|
+
attempts++;
|
|
5126
|
+
}
|
|
5127
|
+
return id;
|
|
4995
5128
|
}
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
const match = content.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n?)([\s\S]*)$/);
|
|
5005
|
-
if (!match) {
|
|
5006
|
-
return { prefix: "", body: content };
|
|
5129
|
+
function encodeMetaValue(value) {
|
|
5130
|
+
let out = "";
|
|
5131
|
+
for (const ch of value) {
|
|
5132
|
+
if (META_ENCODE_CHARS.includes(ch)) {
|
|
5133
|
+
out += "%" + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0");
|
|
5134
|
+
} else {
|
|
5135
|
+
out += ch;
|
|
5136
|
+
}
|
|
5007
5137
|
}
|
|
5008
|
-
return
|
|
5009
|
-
prefix: match[1],
|
|
5010
|
-
body: match[2]
|
|
5011
|
-
};
|
|
5138
|
+
return out;
|
|
5012
5139
|
}
|
|
5013
|
-
function
|
|
5014
|
-
|
|
5015
|
-
|
|
5140
|
+
function decodeMetaValue(value) {
|
|
5141
|
+
return value.replace(
|
|
5142
|
+
/%([0-9A-Fa-f]{2})/g,
|
|
5143
|
+
(_, hex) => String.fromCharCode(parseInt(hex, 16))
|
|
5144
|
+
);
|
|
5145
|
+
}
|
|
5146
|
+
function emptyMetaFields() {
|
|
5147
|
+
return { branch: null, worktreePath: null, createdAt: null, updatedAt: null, planDir: null };
|
|
5148
|
+
}
|
|
5149
|
+
function parseMetaToken(line) {
|
|
5150
|
+
const match = line.match(META_TOKEN_REGEX);
|
|
5151
|
+
if (!match) return emptyMetaFields();
|
|
5152
|
+
const body = match[1];
|
|
5153
|
+
if (!body) return emptyMetaFields();
|
|
5154
|
+
const fields = emptyMetaFields();
|
|
5155
|
+
for (const pair of body.split(";")) {
|
|
5156
|
+
const trimmed = pair.trim();
|
|
5157
|
+
if (!trimmed) continue;
|
|
5158
|
+
const eq = trimmed.indexOf("=");
|
|
5159
|
+
if (eq < 0) continue;
|
|
5160
|
+
const key = trimmed.slice(0, eq).trim();
|
|
5161
|
+
const rawValue = trimmed.slice(eq + 1);
|
|
5162
|
+
const value = decodeMetaValue(rawValue);
|
|
5163
|
+
switch (key) {
|
|
5164
|
+
case "b":
|
|
5165
|
+
fields.branch = value;
|
|
5166
|
+
break;
|
|
5167
|
+
case "w":
|
|
5168
|
+
fields.worktreePath = value;
|
|
5169
|
+
break;
|
|
5170
|
+
case "c":
|
|
5171
|
+
fields.createdAt = value;
|
|
5172
|
+
break;
|
|
5173
|
+
case "u":
|
|
5174
|
+
fields.updatedAt = value;
|
|
5175
|
+
break;
|
|
5176
|
+
case "p":
|
|
5177
|
+
fields.planDir = value;
|
|
5178
|
+
break;
|
|
5179
|
+
}
|
|
5016
5180
|
}
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5181
|
+
return fields;
|
|
5182
|
+
}
|
|
5183
|
+
function serializeMetaToken(item) {
|
|
5184
|
+
const pairs = [];
|
|
5185
|
+
if (item.branch !== null) pairs.push(`b=${encodeMetaValue(item.branch)}`);
|
|
5186
|
+
if (item.worktreePath !== null) pairs.push(`w=${encodeMetaValue(item.worktreePath)}`);
|
|
5187
|
+
if (item.createdAt !== null) pairs.push(`c=${encodeMetaValue(item.createdAt)}`);
|
|
5188
|
+
if (item.updatedAt !== null) pairs.push(`u=${encodeMetaValue(item.updatedAt)}`);
|
|
5189
|
+
if (item.planDir !== null) pairs.push(`p=${encodeMetaValue(item.planDir)}`);
|
|
5190
|
+
if (pairs.length === 0) return "";
|
|
5191
|
+
return `<${pairs.join(";")}>`;
|
|
5192
|
+
}
|
|
5193
|
+
function parseStatus2(marker) {
|
|
5194
|
+
if (marker === " ") return { status: "open", session: null };
|
|
5195
|
+
if (marker === "x") return { status: "completed", session: null };
|
|
5196
|
+
if (marker === "!") return { status: "blocked", session: null };
|
|
5197
|
+
if (marker.startsWith(">:")) return { status: "in_progress", session: marker.slice(2) };
|
|
5198
|
+
if (marker === ">") return { status: "in_progress", session: null };
|
|
5199
|
+
return { status: "open", session: null };
|
|
5200
|
+
}
|
|
5201
|
+
function sanitizeSession(session) {
|
|
5202
|
+
return session.replace(/[\[\]]/g, "");
|
|
5203
|
+
}
|
|
5204
|
+
function statusToMarker(item) {
|
|
5205
|
+
switch (item.status) {
|
|
5206
|
+
case "open":
|
|
5207
|
+
return " ";
|
|
5208
|
+
case "completed":
|
|
5209
|
+
return "x";
|
|
5210
|
+
case "blocked":
|
|
5211
|
+
return "!";
|
|
5212
|
+
case "in_progress":
|
|
5213
|
+
return item.session ? `>:${sanitizeSession(item.session)}` : ">";
|
|
5022
5214
|
}
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5215
|
+
}
|
|
5216
|
+
function parseChecklistItem(line) {
|
|
5217
|
+
const match = line.match(ITEM_REGEX);
|
|
5218
|
+
if (!match) return null;
|
|
5219
|
+
const marker = match[1];
|
|
5220
|
+
const rest = match[2];
|
|
5221
|
+
const { status, session } = parseStatus2(marker);
|
|
5222
|
+
const idMatch = rest.match(ID_REGEX);
|
|
5223
|
+
const id = idMatch ? idMatch[1] : "";
|
|
5224
|
+
const tags = [];
|
|
5225
|
+
let tagMatch;
|
|
5226
|
+
const tagRegex = new RegExp(TAG_REGEX.source, "g");
|
|
5227
|
+
while ((tagMatch = tagRegex.exec(rest)) !== null) {
|
|
5228
|
+
tags.push(tagMatch[1]);
|
|
5029
5229
|
}
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
);
|
|
5033
|
-
const
|
|
5034
|
-
if (
|
|
5035
|
-
|
|
5230
|
+
let description = rest;
|
|
5231
|
+
const firstTagIdx = rest.search(/#[a-zA-Z0-9_-]/);
|
|
5232
|
+
const firstIdIdx = rest.search(/\[t:[a-f0-9]{4}\]/);
|
|
5233
|
+
const cutPoints = [firstTagIdx, firstIdIdx].filter((i) => i >= 0);
|
|
5234
|
+
if (cutPoints.length > 0) {
|
|
5235
|
+
description = rest.slice(0, Math.min(...cutPoints)).trim();
|
|
5036
5236
|
}
|
|
5037
|
-
const
|
|
5038
|
-
/^(\s*[-*]\s+\[)( |x|X)(\]\s+.*)$/,
|
|
5039
|
-
`$1${checked ? "x" : " "}$3`
|
|
5040
|
-
);
|
|
5041
|
-
lines[target.lineIndex] = nextLine;
|
|
5237
|
+
const meta = parseMetaToken(line);
|
|
5042
5238
|
return {
|
|
5043
|
-
|
|
5239
|
+
id,
|
|
5240
|
+
description,
|
|
5241
|
+
status,
|
|
5242
|
+
tags,
|
|
5243
|
+
session,
|
|
5244
|
+
branch: meta.branch,
|
|
5245
|
+
worktreePath: meta.worktreePath,
|
|
5246
|
+
createdAt: meta.createdAt,
|
|
5247
|
+
updatedAt: meta.updatedAt,
|
|
5248
|
+
planDir: meta.planDir
|
|
5044
5249
|
};
|
|
5045
5250
|
}
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
function renderManifest(params2) {
|
|
5056
|
-
return `---
|
|
5057
|
-
version: "2.0"
|
|
5058
|
-
project: ${params2.slug}
|
|
5059
|
-
generated: "${params2.timestamp}"
|
|
5060
|
-
---
|
|
5061
|
-
|
|
5062
|
-
# Project: ${params2.slug}
|
|
5063
|
-
|
|
5064
|
-
## Overview
|
|
5065
|
-
- [Project Overview](./project.md)
|
|
5066
|
-
|
|
5067
|
-
## Indexes
|
|
5068
|
-
- [Assignments](./_index-assignments.md)
|
|
5069
|
-
- [Plans](./_index-plans.md)
|
|
5070
|
-
- [Decision Records](./_index-decisions.md)
|
|
5071
|
-
- [Status](./_status.md)
|
|
5072
|
-
- [Resources](./resources/_index.md)
|
|
5073
|
-
- [Memories](./memories/_index.md)
|
|
5074
|
-
`;
|
|
5251
|
+
function serializeChecklistItem(item) {
|
|
5252
|
+
const marker = statusToMarker(item);
|
|
5253
|
+
const tagStr = item.tags.map((t) => `#${t}`).join(" ");
|
|
5254
|
+
const parts = [`- [${marker}] ${item.description}`];
|
|
5255
|
+
if (tagStr) parts.push(tagStr);
|
|
5256
|
+
parts.push(`[t:${item.id}]`);
|
|
5257
|
+
const meta = serializeMetaToken(item);
|
|
5258
|
+
if (meta) parts.push(meta);
|
|
5259
|
+
return parts.join(" ");
|
|
5075
5260
|
}
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5261
|
+
function parseChecklist(content) {
|
|
5262
|
+
const [fm, body] = extractFrontmatter2(content);
|
|
5263
|
+
const workspace = getField(fm, "workspace") || "_global";
|
|
5264
|
+
const archiveIntervalRaw = getField(fm, "archiveInterval") || "weekly";
|
|
5265
|
+
const archiveInterval = ["daily", "weekly", "monthly", "never"].includes(archiveIntervalRaw) ? archiveIntervalRaw : "weekly";
|
|
5266
|
+
const items = [];
|
|
5267
|
+
for (const line of body.split("\n")) {
|
|
5268
|
+
const item = parseChecklistItem(line);
|
|
5269
|
+
if (item) items.push(item);
|
|
5083
5270
|
}
|
|
5084
|
-
|
|
5085
|
-
return `"${escaped}"`;
|
|
5271
|
+
return { workspace, archiveInterval, items };
|
|
5086
5272
|
}
|
|
5273
|
+
function serializeChecklist(checklist) {
|
|
5274
|
+
const fm = [
|
|
5275
|
+
"---",
|
|
5276
|
+
`workspace: ${checklist.workspace}`,
|
|
5277
|
+
`archiveInterval: ${checklist.archiveInterval}`,
|
|
5278
|
+
"---"
|
|
5279
|
+
].join("\n");
|
|
5280
|
+
const header = "# Quick Todos";
|
|
5281
|
+
const items = checklist.items.map(serializeChecklistItem).join("\n");
|
|
5282
|
+
return `${fm}
|
|
5087
5283
|
|
|
5088
|
-
|
|
5089
|
-
function renderProject(params2) {
|
|
5090
|
-
const safeTitle = escapeYamlString(params2.title);
|
|
5091
|
-
const workspaceLine = params2.workspace ? `
|
|
5092
|
-
workspace: ${params2.workspace}` : "";
|
|
5093
|
-
return `---
|
|
5094
|
-
id: ${params2.id}
|
|
5095
|
-
slug: ${params2.slug}
|
|
5096
|
-
title: ${safeTitle}
|
|
5097
|
-
archived: false
|
|
5098
|
-
archivedAt: null
|
|
5099
|
-
archivedReason: null
|
|
5100
|
-
created: "${params2.timestamp}"
|
|
5101
|
-
updated: "${params2.timestamp}"
|
|
5102
|
-
externalIds: []
|
|
5103
|
-
tags: []${workspaceLine}
|
|
5104
|
-
---
|
|
5105
|
-
|
|
5106
|
-
# ${params2.title}
|
|
5107
|
-
|
|
5108
|
-
## Overview
|
|
5109
|
-
|
|
5110
|
-
<!-- Describe the project goal, context, and success criteria here. -->
|
|
5111
|
-
|
|
5112
|
-
## Notes
|
|
5284
|
+
${header}
|
|
5113
5285
|
|
|
5114
|
-
|
|
5286
|
+
${items}
|
|
5115
5287
|
`;
|
|
5116
5288
|
}
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
const
|
|
5121
|
-
const
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
## Acceptance Criteria
|
|
5170
|
-
|
|
5171
|
-
- [ ] <!-- criterion 1 -->
|
|
5172
|
-
- [ ] <!-- criterion 2 -->
|
|
5173
|
-
- [ ] <!-- criterion 3 -->
|
|
5174
|
-
|
|
5175
|
-
${todosSection}## Context
|
|
5176
|
-
|
|
5177
|
-
<!-- Links to relevant docs, code, or other assignments. -->
|
|
5178
|
-
|
|
5179
|
-
## Links
|
|
5180
|
-
|
|
5181
|
-
- [Progress](./progress.md)
|
|
5182
|
-
- [Comments](./comments.md)
|
|
5183
|
-
- [Scratchpad](./scratchpad.md)
|
|
5184
|
-
- [Handoff](./handoff.md)
|
|
5185
|
-
- [Decision Record](./decision-record.md)
|
|
5186
|
-
`;
|
|
5289
|
+
function parseLog(content) {
|
|
5290
|
+
const [fm, body] = extractFrontmatter2(content);
|
|
5291
|
+
const workspace = getField(fm, "workspace") || "_global";
|
|
5292
|
+
const entries = [];
|
|
5293
|
+
const sections = body.split(/^### /m).filter((s) => s.match(/^\d{4}-/));
|
|
5294
|
+
for (const section of sections) {
|
|
5295
|
+
const lines = section.split("\n");
|
|
5296
|
+
const heading = lines[0]?.trim() || "";
|
|
5297
|
+
const headingMatch = heading.match(/^(\S+)\s*—?\s*(.*)/);
|
|
5298
|
+
if (!headingMatch) continue;
|
|
5299
|
+
const timestamp = headingMatch[1];
|
|
5300
|
+
const idsPart = headingMatch[2] || "";
|
|
5301
|
+
const itemIds = [...idsPart.matchAll(/t:([a-f0-9]{4})/g)].map((m) => m[1]);
|
|
5302
|
+
const entry = {
|
|
5303
|
+
timestamp,
|
|
5304
|
+
itemIds,
|
|
5305
|
+
items: "",
|
|
5306
|
+
session: null,
|
|
5307
|
+
branch: null,
|
|
5308
|
+
summary: "",
|
|
5309
|
+
blockers: null,
|
|
5310
|
+
status: null
|
|
5311
|
+
};
|
|
5312
|
+
for (const line of lines.slice(1)) {
|
|
5313
|
+
const fieldMatch = line.match(/^\*\*(\w+):\*\*\s*(.*)/);
|
|
5314
|
+
if (!fieldMatch) continue;
|
|
5315
|
+
const key = fieldMatch[1].toLowerCase();
|
|
5316
|
+
const value = fieldMatch[2].trim();
|
|
5317
|
+
switch (key) {
|
|
5318
|
+
case "items":
|
|
5319
|
+
entry.items = value;
|
|
5320
|
+
break;
|
|
5321
|
+
case "session":
|
|
5322
|
+
entry.session = value;
|
|
5323
|
+
break;
|
|
5324
|
+
case "branch":
|
|
5325
|
+
entry.branch = value;
|
|
5326
|
+
break;
|
|
5327
|
+
case "summary":
|
|
5328
|
+
entry.summary = value;
|
|
5329
|
+
break;
|
|
5330
|
+
case "blockers":
|
|
5331
|
+
entry.blockers = value;
|
|
5332
|
+
break;
|
|
5333
|
+
case "status":
|
|
5334
|
+
entry.status = value;
|
|
5335
|
+
break;
|
|
5336
|
+
}
|
|
5337
|
+
}
|
|
5338
|
+
entries.push(entry);
|
|
5339
|
+
}
|
|
5340
|
+
return { workspace, entries };
|
|
5187
5341
|
}
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
No working notes yet.
|
|
5199
|
-
`;
|
|
5342
|
+
function serializeLogEntry(entry) {
|
|
5343
|
+
const idStr = entry.itemIds.map((id) => `t:${id}`).join(", ");
|
|
5344
|
+
const lines = [`### ${entry.timestamp} \u2014 ${idStr}`];
|
|
5345
|
+
if (entry.items) lines.push(`**Items:** ${entry.items}`);
|
|
5346
|
+
if (entry.session) lines.push(`**Session:** ${entry.session}`);
|
|
5347
|
+
if (entry.branch) lines.push(`**Branch:** ${entry.branch}`);
|
|
5348
|
+
if (entry.summary) lines.push(`**Summary:** ${entry.summary}`);
|
|
5349
|
+
if (entry.blockers) lines.push(`**Blockers:** ${entry.blockers}`);
|
|
5350
|
+
if (entry.status) lines.push(`**Status:** ${entry.status}`);
|
|
5351
|
+
return lines.join("\n");
|
|
5200
5352
|
}
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5353
|
+
function checklistPath(todosDir2, workspace) {
|
|
5354
|
+
return resolve15(todosDir2, `${workspace}.md`);
|
|
5355
|
+
}
|
|
5356
|
+
function logPath(todosDir2, workspace) {
|
|
5357
|
+
return resolve15(todosDir2, `${workspace}-log.md`);
|
|
5358
|
+
}
|
|
5359
|
+
function archivePath(todosDir2, workspace, interval, now = /* @__PURE__ */ new Date()) {
|
|
5360
|
+
const year = now.getFullYear();
|
|
5361
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
5362
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
5363
|
+
let suffix;
|
|
5364
|
+
switch (interval) {
|
|
5365
|
+
case "daily":
|
|
5366
|
+
suffix = `${year}-${month}-${day}`;
|
|
5367
|
+
break;
|
|
5368
|
+
case "weekly": {
|
|
5369
|
+
const jan1 = new Date(year, 0, 1);
|
|
5370
|
+
const days = Math.floor((now.getTime() - jan1.getTime()) / 864e5);
|
|
5371
|
+
const week = String(Math.ceil((days + jan1.getDay() + 1) / 7)).padStart(2, "0");
|
|
5372
|
+
suffix = `${year}-W${week}`;
|
|
5373
|
+
break;
|
|
5374
|
+
}
|
|
5375
|
+
case "monthly":
|
|
5376
|
+
suffix = `${year}-${month}`;
|
|
5377
|
+
break;
|
|
5378
|
+
default:
|
|
5379
|
+
suffix = `${year}-${month}-${day}`;
|
|
5380
|
+
}
|
|
5381
|
+
return resolve15(todosDir2, "archive", `${workspace}-${suffix}.md`);
|
|
5382
|
+
}
|
|
5383
|
+
async function readChecklist(todosDir2, workspace) {
|
|
5384
|
+
const path = checklistPath(todosDir2, workspace);
|
|
5385
|
+
if (!await fileExists(path)) {
|
|
5386
|
+
return { workspace, archiveInterval: "weekly", items: [] };
|
|
5387
|
+
}
|
|
5388
|
+
const content = await readFile12(path, "utf-8");
|
|
5389
|
+
return parseChecklist(content);
|
|
5390
|
+
}
|
|
5391
|
+
async function writeChecklist(todosDir2, checklist) {
|
|
5392
|
+
await ensureDir(todosDir2);
|
|
5393
|
+
const path = checklistPath(todosDir2, checklist.workspace);
|
|
5394
|
+
await writeFileForce(path, serializeChecklist(checklist));
|
|
5395
|
+
}
|
|
5396
|
+
async function readLog(todosDir2, workspace) {
|
|
5397
|
+
const path = logPath(todosDir2, workspace);
|
|
5398
|
+
if (!await fileExists(path)) {
|
|
5399
|
+
return { workspace, entries: [] };
|
|
5400
|
+
}
|
|
5401
|
+
const content = await readFile12(path, "utf-8");
|
|
5402
|
+
return parseLog(content);
|
|
5403
|
+
}
|
|
5404
|
+
async function appendLogEntry2(todosDir2, workspace, entry) {
|
|
5405
|
+
await ensureDir(todosDir2);
|
|
5406
|
+
const path = logPath(todosDir2, workspace);
|
|
5407
|
+
let content;
|
|
5408
|
+
if (await fileExists(path)) {
|
|
5409
|
+
content = await readFile12(path, "utf-8");
|
|
5410
|
+
content = content.trimEnd() + "\n\n" + serializeLogEntry(entry) + "\n";
|
|
5411
|
+
} else {
|
|
5412
|
+
const fm = `---
|
|
5413
|
+
workspace: ${workspace}
|
|
5208
5414
|
---
|
|
5209
5415
|
|
|
5210
|
-
#
|
|
5416
|
+
# Todo Log
|
|
5211
5417
|
|
|
5212
|
-
No handoffs recorded yet.
|
|
5213
5418
|
`;
|
|
5419
|
+
content = fm + serializeLogEntry(entry) + "\n";
|
|
5420
|
+
}
|
|
5421
|
+
await writeFileForce(path, content);
|
|
5422
|
+
}
|
|
5423
|
+
function computeCounts(items) {
|
|
5424
|
+
const counts = { open: 0, in_progress: 0, completed: 0, blocked: 0, total: items.length };
|
|
5425
|
+
for (const item of items) {
|
|
5426
|
+
counts[item.status]++;
|
|
5427
|
+
}
|
|
5428
|
+
return counts;
|
|
5214
5429
|
}
|
|
5430
|
+
var ITEM_REGEX, ID_REGEX, TAG_REGEX, META_TOKEN_REGEX, META_ENCODE_CHARS;
|
|
5431
|
+
var init_parser2 = __esm({
|
|
5432
|
+
"src/todos/parser.ts"() {
|
|
5433
|
+
"use strict";
|
|
5434
|
+
init_parser();
|
|
5435
|
+
init_fs();
|
|
5436
|
+
ITEM_REGEX = /^- \[([ x!]|>[^\]]*)\]\s+(.+)$/;
|
|
5437
|
+
ID_REGEX = /\[t:([a-f0-9]{4})\]/;
|
|
5438
|
+
TAG_REGEX = /#([a-zA-Z0-9_-]+)/g;
|
|
5439
|
+
META_TOKEN_REGEX = /\[t:[a-f0-9]{4}\]\s+<([^>]*)>\s*$/;
|
|
5440
|
+
META_ENCODE_CHARS = ["%", "<", ">", "[", "]", "=", ";", "\n", "\r"];
|
|
5441
|
+
}
|
|
5442
|
+
});
|
|
5215
5443
|
|
|
5216
|
-
// src/
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5444
|
+
// src/utils/assignment-todos.ts
|
|
5445
|
+
var assignment_todos_exports = {};
|
|
5446
|
+
__export(assignment_todos_exports, {
|
|
5447
|
+
appendTodosToAssignmentBody: () => appendTodosToAssignmentBody,
|
|
5448
|
+
touchAssignmentUpdated: () => touchAssignmentUpdated
|
|
5449
|
+
});
|
|
5450
|
+
function setTopLevelField2(content, key, value) {
|
|
5451
|
+
const fieldRegex = new RegExp(`^(${key}:)\\s*.*$`, "m");
|
|
5452
|
+
if (fieldRegex.test(content)) {
|
|
5453
|
+
return content.replace(fieldRegex, `$1 ${value}`);
|
|
5454
|
+
}
|
|
5455
|
+
return content;
|
|
5456
|
+
}
|
|
5457
|
+
function appendTodosToAssignmentBody(body, todos) {
|
|
5458
|
+
if (todos.length === 0) return body;
|
|
5459
|
+
const lines = todos.map((t) => {
|
|
5460
|
+
const base = `- [ ] ${t.description.trim()}`;
|
|
5461
|
+
return t.trace ? `${base} <!-- ${t.trace} -->` : base;
|
|
5462
|
+
});
|
|
5463
|
+
const block = lines.join("\n");
|
|
5464
|
+
const todosHeading = /^## Todos\s*$/m;
|
|
5465
|
+
if (todosHeading.test(body)) {
|
|
5466
|
+
return body.replace(
|
|
5467
|
+
/(^## Todos[\s\S]*?)(\n## |\n*$)/m,
|
|
5468
|
+
(_m, section, nextHeading) => {
|
|
5469
|
+
return `${section.trimEnd()}
|
|
5470
|
+
${block}
|
|
5471
|
+
${nextHeading}`;
|
|
5472
|
+
}
|
|
5473
|
+
);
|
|
5474
|
+
}
|
|
5475
|
+
return `${body.trimEnd()}
|
|
5224
5476
|
|
|
5225
|
-
|
|
5477
|
+
## Todos
|
|
5226
5478
|
|
|
5227
|
-
|
|
5479
|
+
${block}
|
|
5228
5480
|
`;
|
|
5229
5481
|
}
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
function renderComments(params2) {
|
|
5233
|
-
return `---
|
|
5234
|
-
assignment: ${params2.assignment}
|
|
5235
|
-
entryCount: 0
|
|
5236
|
-
generated: "${params2.timestamp}"
|
|
5237
|
-
updated: "${params2.timestamp}"
|
|
5238
|
-
---
|
|
5239
|
-
|
|
5240
|
-
# Comments
|
|
5241
|
-
|
|
5242
|
-
No comments yet.
|
|
5243
|
-
`;
|
|
5482
|
+
function touchAssignmentUpdated(content, timestamp) {
|
|
5483
|
+
return setTopLevelField2(content, "updated", `"${timestamp}"`);
|
|
5244
5484
|
}
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
lines.push("");
|
|
5249
|
-
lines.push(`**Recorded:** ${comment.timestamp}`);
|
|
5250
|
-
lines.push(`**Author:** ${comment.author}`);
|
|
5251
|
-
lines.push(`**Type:** ${comment.type}`);
|
|
5252
|
-
if (comment.replyTo) {
|
|
5253
|
-
lines.push(`**Reply to:** ${comment.replyTo}`);
|
|
5485
|
+
var init_assignment_todos = __esm({
|
|
5486
|
+
"src/utils/assignment-todos.ts"() {
|
|
5487
|
+
"use strict";
|
|
5254
5488
|
}
|
|
5255
|
-
|
|
5256
|
-
|
|
5489
|
+
});
|
|
5490
|
+
|
|
5491
|
+
// src/commands/create-assignment.ts
|
|
5492
|
+
var create_assignment_exports = {};
|
|
5493
|
+
__export(create_assignment_exports, {
|
|
5494
|
+
createAssignmentCommand: () => createAssignmentCommand
|
|
5495
|
+
});
|
|
5496
|
+
import { resolve as resolve16 } from "path";
|
|
5497
|
+
async function createAssignmentCommand(title, options) {
|
|
5498
|
+
if (!title.trim()) {
|
|
5499
|
+
throw new Error("Assignment title cannot be empty.");
|
|
5257
5500
|
}
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5501
|
+
if (options.workspace && options.project) {
|
|
5502
|
+
throw new Error(
|
|
5503
|
+
"Cannot use --workspace with --project (projects already carry a workspace via project.workspace)."
|
|
5504
|
+
);
|
|
5505
|
+
}
|
|
5506
|
+
if (options.workspace && !options.oneOff) {
|
|
5507
|
+
throw new Error("--workspace requires --one-off.");
|
|
5508
|
+
}
|
|
5509
|
+
if (!options.project && !options.oneOff) {
|
|
5510
|
+
throw new Error(
|
|
5511
|
+
"Either --project <slug> or --one-off is required."
|
|
5512
|
+
);
|
|
5513
|
+
}
|
|
5514
|
+
if (options.project && options.oneOff) {
|
|
5515
|
+
throw new Error(
|
|
5516
|
+
"Cannot use both --project and --one-off. Use --project to add to an existing project, or --one-off to create a standalone assignment."
|
|
5517
|
+
);
|
|
5518
|
+
}
|
|
5519
|
+
if (options.project && !isValidSlug(options.project)) {
|
|
5520
|
+
throw new Error(
|
|
5521
|
+
`Invalid project slug "${options.project}". Slugs must be lowercase, hyphen-separated, with no special characters.`
|
|
5522
|
+
);
|
|
5523
|
+
}
|
|
5524
|
+
if (options.workspace && !isValidSlug(options.workspace)) {
|
|
5525
|
+
throw new Error(
|
|
5526
|
+
`Invalid workspace slug "${options.workspace}". Slugs must be lowercase, hyphen-separated, with no special characters.`
|
|
5527
|
+
);
|
|
5528
|
+
}
|
|
5529
|
+
if (options.oneOff && options.dependsOn) {
|
|
5530
|
+
throw new Error("Standalone assignments cannot have dependencies (--depends-on is not allowed with --one-off).");
|
|
5531
|
+
}
|
|
5532
|
+
const assignmentSlug = options.slug || slugify(title);
|
|
5533
|
+
if (!isValidSlug(assignmentSlug)) {
|
|
5534
|
+
throw new Error(
|
|
5535
|
+
`Invalid slug "${assignmentSlug}". Slugs must be lowercase, hyphen-separated, with no special characters.`
|
|
5536
|
+
);
|
|
5537
|
+
}
|
|
5538
|
+
const dependsOn = options.dependsOn ? options.dependsOn.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
5539
|
+
for (const dep of dependsOn) {
|
|
5540
|
+
if (!isValidSlug(dep)) {
|
|
5541
|
+
throw new Error(
|
|
5542
|
+
`Invalid dependency slug "${dep}". Slugs must be lowercase, hyphen-separated, with no special characters.`
|
|
5543
|
+
);
|
|
5544
|
+
}
|
|
5545
|
+
}
|
|
5546
|
+
const links = options.links ? options.links.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
5547
|
+
for (const link of links) {
|
|
5548
|
+
const parts = link.split("/");
|
|
5549
|
+
if (parts.length !== 2 || !parts.every(isValidSlug)) {
|
|
5550
|
+
throw new Error(
|
|
5551
|
+
`Invalid link "${link}". Links must be in projectSlug/assignmentSlug format (e.g., "my-project/my-assignment").`
|
|
5552
|
+
);
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
5555
|
+
const validPriorities = ["low", "medium", "high", "critical"];
|
|
5556
|
+
const priority = options.priority || "medium";
|
|
5557
|
+
if (!validPriorities.includes(priority)) {
|
|
5558
|
+
throw new Error(
|
|
5559
|
+
`Invalid priority "${options.priority}". Must be one of: ${validPriorities.join(", ")}`
|
|
5560
|
+
);
|
|
5561
|
+
}
|
|
5562
|
+
const config = await readConfig();
|
|
5563
|
+
const timestamp = nowTimestamp();
|
|
5564
|
+
const id = generateId();
|
|
5565
|
+
let assignmentDir;
|
|
5566
|
+
let projectSlug;
|
|
5567
|
+
let folderName;
|
|
5568
|
+
if (options.oneOff) {
|
|
5569
|
+
const standaloneRoot = assignmentsDir();
|
|
5570
|
+
folderName = id;
|
|
5571
|
+
assignmentDir = resolve16(standaloneRoot, folderName);
|
|
5572
|
+
projectSlug = null;
|
|
5573
|
+
await ensureDir(standaloneRoot);
|
|
5574
|
+
} else {
|
|
5575
|
+
const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
|
|
5576
|
+
projectSlug = options.project;
|
|
5577
|
+
const projectDir = resolve16(baseDir, projectSlug);
|
|
5578
|
+
const projectMdPath = resolve16(projectDir, "project.md");
|
|
5579
|
+
if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
|
|
5580
|
+
throw new Error(
|
|
5581
|
+
`Project "${projectSlug}" not found at ${projectDir}.
|
|
5582
|
+
Run 'syntaur create-project' first or use --one-off.`
|
|
5583
|
+
);
|
|
5584
|
+
}
|
|
5585
|
+
if (dependsOn.length > 0) {
|
|
5586
|
+
const depDirBase = resolve16(projectDir, "assignments");
|
|
5587
|
+
for (const dep of dependsOn) {
|
|
5588
|
+
const depDir = resolve16(depDirBase, dep);
|
|
5589
|
+
if (!await fileExists(depDir)) {
|
|
5590
|
+
console.warn(
|
|
5591
|
+
`Warning: dependency "${dep}" does not exist in project "${projectSlug}" yet.`
|
|
5592
|
+
);
|
|
5593
|
+
}
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5596
|
+
folderName = assignmentSlug;
|
|
5597
|
+
assignmentDir = resolve16(projectDir, "assignments", folderName);
|
|
5598
|
+
}
|
|
5599
|
+
if (await fileExists(assignmentDir)) {
|
|
5600
|
+
throw new Error(
|
|
5601
|
+
`Assignment folder already exists: ${assignmentDir}
|
|
5602
|
+
Use --slug to specify a different slug.`
|
|
5603
|
+
);
|
|
5604
|
+
}
|
|
5605
|
+
await ensureDir(assignmentDir);
|
|
5606
|
+
const companionAssignmentRef = projectSlug === null ? id : assignmentSlug;
|
|
5607
|
+
const files = [
|
|
5608
|
+
[
|
|
5609
|
+
resolve16(assignmentDir, "assignment.md"),
|
|
5610
|
+
renderAssignment({
|
|
5611
|
+
id,
|
|
5612
|
+
slug: assignmentSlug,
|
|
5613
|
+
title,
|
|
5614
|
+
timestamp,
|
|
5615
|
+
priority,
|
|
5616
|
+
dependsOn,
|
|
5617
|
+
links,
|
|
5618
|
+
project: projectSlug,
|
|
5619
|
+
workspaceGroup: options.workspace ?? null,
|
|
5620
|
+
type: options.type,
|
|
5621
|
+
includeTodos: options.withTodos === true
|
|
5622
|
+
})
|
|
5623
|
+
],
|
|
5624
|
+
[
|
|
5625
|
+
resolve16(assignmentDir, "scratchpad.md"),
|
|
5626
|
+
renderScratchpad({
|
|
5627
|
+
assignmentSlug: companionAssignmentRef,
|
|
5628
|
+
timestamp
|
|
5629
|
+
})
|
|
5630
|
+
],
|
|
5631
|
+
[
|
|
5632
|
+
resolve16(assignmentDir, "handoff.md"),
|
|
5633
|
+
renderHandoff({
|
|
5634
|
+
assignmentSlug: companionAssignmentRef,
|
|
5635
|
+
timestamp
|
|
5636
|
+
})
|
|
5637
|
+
],
|
|
5638
|
+
[
|
|
5639
|
+
resolve16(assignmentDir, "decision-record.md"),
|
|
5640
|
+
renderDecisionRecord({
|
|
5641
|
+
assignmentSlug: companionAssignmentRef,
|
|
5642
|
+
timestamp
|
|
5643
|
+
})
|
|
5644
|
+
],
|
|
5645
|
+
[
|
|
5646
|
+
resolve16(assignmentDir, "progress.md"),
|
|
5647
|
+
renderProgress({
|
|
5648
|
+
assignment: companionAssignmentRef,
|
|
5649
|
+
timestamp
|
|
5650
|
+
})
|
|
5651
|
+
],
|
|
5652
|
+
[
|
|
5653
|
+
resolve16(assignmentDir, "comments.md"),
|
|
5654
|
+
renderComments({
|
|
5655
|
+
assignment: companionAssignmentRef,
|
|
5656
|
+
timestamp
|
|
5657
|
+
})
|
|
5658
|
+
]
|
|
5659
|
+
];
|
|
5660
|
+
for (const [filePath, content] of files) {
|
|
5661
|
+
await writeFileForce(filePath, content);
|
|
5662
|
+
}
|
|
5663
|
+
if (!options.silent) {
|
|
5664
|
+
if (projectSlug === null) {
|
|
5665
|
+
console.log(
|
|
5666
|
+
`Created standalone assignment "${title}" at ${assignmentDir}/`
|
|
5667
|
+
);
|
|
5668
|
+
console.log(` UUID: ${id}`);
|
|
5669
|
+
console.log(` Slug: ${assignmentSlug} (display only)`);
|
|
5670
|
+
} else {
|
|
5671
|
+
console.log(
|
|
5672
|
+
`Created assignment "${title}" in project "${projectSlug}" at ${assignmentDir}/`
|
|
5673
|
+
);
|
|
5674
|
+
console.log(` Slug: ${assignmentSlug}`);
|
|
5675
|
+
}
|
|
5676
|
+
console.log(` Priority: ${priority}`);
|
|
5677
|
+
if (options.type) {
|
|
5678
|
+
console.log(` Type: ${options.type}`);
|
|
5679
|
+
}
|
|
5680
|
+
if (dependsOn.length > 0) {
|
|
5681
|
+
console.log(` Depends on: ${dependsOn.join(", ")}`);
|
|
5682
|
+
}
|
|
5683
|
+
if (links.length > 0) {
|
|
5684
|
+
console.log(` Links: ${links.join(", ")}`);
|
|
5685
|
+
}
|
|
5686
|
+
console.log(` Files created:`);
|
|
5687
|
+
console.log(` assignment.md`);
|
|
5688
|
+
console.log(` scratchpad.md`);
|
|
5689
|
+
console.log(` handoff.md`);
|
|
5690
|
+
console.log(` decision-record.md`);
|
|
5691
|
+
console.log(` progress.md`);
|
|
5692
|
+
console.log(` comments.md`);
|
|
5693
|
+
console.log(
|
|
5694
|
+
` Plan files (plan.md, plan-v2.md, ...) are created on demand by /plan-assignment.`
|
|
5695
|
+
);
|
|
5696
|
+
}
|
|
5697
|
+
return { id, slug: assignmentSlug, projectSlug, assignmentDir };
|
|
5262
5698
|
}
|
|
5699
|
+
var init_create_assignment = __esm({
|
|
5700
|
+
"src/commands/create-assignment.ts"() {
|
|
5701
|
+
"use strict";
|
|
5702
|
+
init_slug();
|
|
5703
|
+
init_timestamp();
|
|
5704
|
+
init_uuid();
|
|
5705
|
+
init_paths();
|
|
5706
|
+
init_fs();
|
|
5707
|
+
init_config2();
|
|
5708
|
+
init_templates();
|
|
5709
|
+
}
|
|
5710
|
+
});
|
|
5263
5711
|
|
|
5264
|
-
// src/
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5712
|
+
// src/dashboard/server.ts
|
|
5713
|
+
init_paths();
|
|
5714
|
+
init_api();
|
|
5715
|
+
init_assignment_resolver();
|
|
5716
|
+
import express from "express";
|
|
5717
|
+
import { createServer } from "http";
|
|
5718
|
+
import { resolve as resolve19 } from "path";
|
|
5719
|
+
import { writeFile as writeFile5, unlink as unlink4 } from "fs/promises";
|
|
5720
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
5271
5721
|
|
|
5272
|
-
|
|
5722
|
+
// src/dashboard/agent-sessions.ts
|
|
5723
|
+
init_fs();
|
|
5724
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
5725
|
+
import { resolve as resolve11 } from "path";
|
|
5273
5726
|
|
|
5274
|
-
|
|
5727
|
+
// src/dashboard/session-db.ts
|
|
5728
|
+
init_paths();
|
|
5729
|
+
init_fs();
|
|
5730
|
+
import Database from "better-sqlite3";
|
|
5731
|
+
import { resolve as resolve10 } from "path";
|
|
5732
|
+
import { readdir as readdir7 } from "fs/promises";
|
|
5733
|
+
var db = null;
|
|
5734
|
+
var SCHEMA_VERSION = "3";
|
|
5735
|
+
var SCHEMA_SQL = `
|
|
5736
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
5737
|
+
session_id TEXT PRIMARY KEY,
|
|
5738
|
+
project_slug TEXT,
|
|
5739
|
+
assignment_slug TEXT,
|
|
5740
|
+
agent TEXT NOT NULL,
|
|
5741
|
+
started TEXT NOT NULL,
|
|
5742
|
+
ended TEXT,
|
|
5743
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
5744
|
+
path TEXT,
|
|
5745
|
+
description TEXT,
|
|
5746
|
+
transcript_path TEXT,
|
|
5747
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
5748
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
5749
|
+
);
|
|
5750
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
5751
|
+
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
|
|
5275
5752
|
`;
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
function renderIndexAssignments(params2) {
|
|
5280
|
-
return `---
|
|
5281
|
-
project: ${params2.slug}
|
|
5282
|
-
generated: "${params2.timestamp}"
|
|
5283
|
-
total: 0
|
|
5284
|
-
by_status:
|
|
5285
|
-
pending: 0
|
|
5286
|
-
in_progress: 0
|
|
5287
|
-
blocked: 0
|
|
5288
|
-
review: 0
|
|
5289
|
-
completed: 0
|
|
5290
|
-
failed: 0
|
|
5291
|
-
---
|
|
5292
|
-
|
|
5293
|
-
# Assignments
|
|
5294
|
-
|
|
5295
|
-
| Slug | Title | Status | Priority | Assignee | Dependencies | Updated |
|
|
5296
|
-
|------|-------|--------|----------|----------|--------------|---------|
|
|
5753
|
+
var POST_MIGRATION_INDEXES_SQL = `
|
|
5754
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
|
|
5755
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
|
|
5297
5756
|
`;
|
|
5757
|
+
function initSessionDb(dbPath) {
|
|
5758
|
+
if (db) return db;
|
|
5759
|
+
const finalPath = dbPath ?? resolve10(syntaurRoot(), "syntaur.db");
|
|
5760
|
+
db = new Database(finalPath);
|
|
5761
|
+
db.pragma("journal_mode = WAL");
|
|
5762
|
+
db.exec(SCHEMA_SQL);
|
|
5763
|
+
db.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run(
|
|
5764
|
+
"schema_version",
|
|
5765
|
+
SCHEMA_VERSION
|
|
5766
|
+
);
|
|
5767
|
+
const database = db;
|
|
5768
|
+
const runMigrations = database.transaction(() => {
|
|
5769
|
+
const vBeforeV2 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
|
|
5770
|
+
if (vBeforeV2 === "1") {
|
|
5771
|
+
database.exec(`
|
|
5772
|
+
CREATE TABLE sessions_v2 (
|
|
5773
|
+
session_id TEXT PRIMARY KEY,
|
|
5774
|
+
project_slug TEXT,
|
|
5775
|
+
assignment_slug TEXT,
|
|
5776
|
+
agent TEXT NOT NULL,
|
|
5777
|
+
started TEXT NOT NULL,
|
|
5778
|
+
ended TEXT,
|
|
5779
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
5780
|
+
path TEXT,
|
|
5781
|
+
description TEXT,
|
|
5782
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
5783
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
5784
|
+
);
|
|
5785
|
+
INSERT INTO sessions_v2 SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, NULL, created_at, updated_at FROM sessions;
|
|
5786
|
+
DROP TABLE sessions;
|
|
5787
|
+
ALTER TABLE sessions_v2 RENAME TO sessions;
|
|
5788
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
|
|
5789
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
|
|
5790
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
5791
|
+
UPDATE meta SET value = '2' WHERE key = 'schema_version';
|
|
5792
|
+
`);
|
|
5793
|
+
}
|
|
5794
|
+
const vBeforeV3 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
|
|
5795
|
+
if (vBeforeV3 === "2") {
|
|
5796
|
+
const v2Columns = database.prepare("PRAGMA table_info(sessions)").all();
|
|
5797
|
+
const v2ColNames = v2Columns.map((c) => c.name);
|
|
5798
|
+
const hasProject = v2ColNames.includes("project_slug");
|
|
5799
|
+
const hasMission = v2ColNames.includes("mission_slug");
|
|
5800
|
+
const projectSlugExpr = hasProject && hasMission ? "COALESCE(project_slug, mission_slug)" : hasProject ? "project_slug" : hasMission ? "mission_slug" : null;
|
|
5801
|
+
if (!projectSlugExpr) {
|
|
5802
|
+
throw new Error(
|
|
5803
|
+
"sessions table has neither project_slug nor mission_slug; cannot migrate from v2 to v3"
|
|
5804
|
+
);
|
|
5805
|
+
}
|
|
5806
|
+
database.exec(`
|
|
5807
|
+
CREATE TABLE sessions_v3 (
|
|
5808
|
+
session_id TEXT PRIMARY KEY,
|
|
5809
|
+
project_slug TEXT,
|
|
5810
|
+
assignment_slug TEXT,
|
|
5811
|
+
agent TEXT NOT NULL,
|
|
5812
|
+
started TEXT NOT NULL,
|
|
5813
|
+
ended TEXT,
|
|
5814
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
5815
|
+
path TEXT,
|
|
5816
|
+
description TEXT,
|
|
5817
|
+
transcript_path TEXT,
|
|
5818
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
5819
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
5820
|
+
);
|
|
5821
|
+
INSERT INTO sessions_v3
|
|
5822
|
+
SELECT session_id, ${projectSlugExpr}, assignment_slug, agent, started, ended, status, path, description, NULL, created_at, updated_at
|
|
5823
|
+
FROM sessions;
|
|
5824
|
+
DROP TABLE sessions;
|
|
5825
|
+
ALTER TABLE sessions_v3 RENAME TO sessions;
|
|
5826
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
|
|
5827
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
|
|
5828
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
5829
|
+
UPDATE meta SET value = '3' WHERE key = 'schema_version';
|
|
5830
|
+
`);
|
|
5831
|
+
}
|
|
5832
|
+
});
|
|
5833
|
+
runMigrations.exclusive();
|
|
5834
|
+
db.exec(POST_MIGRATION_INDEXES_SQL);
|
|
5835
|
+
return db;
|
|
5298
5836
|
}
|
|
5299
|
-
function
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
| Assignment | Plan Status | Updated |
|
|
5308
|
-
|------------|-------------|---------|
|
|
5309
|
-
`;
|
|
5837
|
+
function getSessionDb() {
|
|
5838
|
+
if (!db) {
|
|
5839
|
+
throw new Error(
|
|
5840
|
+
"Session database not initialized. Call initSessionDb() first."
|
|
5841
|
+
);
|
|
5842
|
+
}
|
|
5843
|
+
return db;
|
|
5310
5844
|
}
|
|
5311
|
-
function
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5845
|
+
function closeSessionDb() {
|
|
5846
|
+
if (db) {
|
|
5847
|
+
db.close();
|
|
5848
|
+
db = null;
|
|
5849
|
+
}
|
|
5850
|
+
}
|
|
5851
|
+
async function migrateFromMarkdown(projectsDir) {
|
|
5852
|
+
const database = getSessionDb();
|
|
5853
|
+
const count = database.prepare("SELECT COUNT(*) as count FROM sessions").get();
|
|
5854
|
+
if (count.count > 0) return 0;
|
|
5855
|
+
if (!await fileExists(projectsDir)) return 0;
|
|
5856
|
+
const entries = await readdir7(projectsDir, { withFileTypes: true });
|
|
5857
|
+
const allSessions = [];
|
|
5858
|
+
for (const entry of entries) {
|
|
5859
|
+
if (!entry.isDirectory()) continue;
|
|
5860
|
+
const projectDir = resolve10(projectsDir, entry.name);
|
|
5861
|
+
const indexPath = resolve10(projectDir, "_index-sessions.md");
|
|
5862
|
+
if (!await fileExists(indexPath)) continue;
|
|
5863
|
+
const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
|
|
5864
|
+
allSessions.push(...sessions);
|
|
5865
|
+
}
|
|
5866
|
+
if (allSessions.length === 0) return 0;
|
|
5867
|
+
const insert = database.prepare(`
|
|
5868
|
+
INSERT OR IGNORE INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path)
|
|
5869
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
5870
|
+
`);
|
|
5871
|
+
const insertAll = database.transaction((sessions) => {
|
|
5872
|
+
for (const s of sessions) {
|
|
5873
|
+
insert.run(s.sessionId, s.projectSlug, s.assignmentSlug, s.agent, s.started, s.status, s.path);
|
|
5874
|
+
}
|
|
5875
|
+
});
|
|
5876
|
+
insertAll(allSessions);
|
|
5877
|
+
console.log(`Migrated ${allSessions.length} sessions from markdown to SQLite.`);
|
|
5878
|
+
return allSessions.length;
|
|
5879
|
+
}
|
|
5880
|
+
async function parseMarkdownSessionsIndex(filePath, projectSlug) {
|
|
5881
|
+
const { readFile: readFile15 } = await import("fs/promises");
|
|
5882
|
+
const raw = await readFile15(filePath, "utf-8");
|
|
5883
|
+
const sessions = [];
|
|
5884
|
+
const lines = raw.split("\n");
|
|
5885
|
+
let inTable = false;
|
|
5886
|
+
let headerSeen = false;
|
|
5887
|
+
for (const line of lines) {
|
|
5888
|
+
const trimmed = line.trim();
|
|
5889
|
+
if (!trimmed) continue;
|
|
5890
|
+
if (trimmed.startsWith("| Assignment") || trimmed.startsWith("|Assignment")) {
|
|
5891
|
+
inTable = true;
|
|
5892
|
+
headerSeen = false;
|
|
5893
|
+
continue;
|
|
5894
|
+
}
|
|
5895
|
+
if (inTable && !headerSeen && trimmed.match(/^\|[-\s|]+\|$/)) {
|
|
5896
|
+
headerSeen = true;
|
|
5897
|
+
continue;
|
|
5898
|
+
}
|
|
5899
|
+
if (inTable && headerSeen && trimmed.startsWith("|")) {
|
|
5900
|
+
const cells = trimmed.split("|").slice(1, -1).map((c) => c.trim());
|
|
5901
|
+
if (cells.length >= 6) {
|
|
5902
|
+
sessions.push({
|
|
5903
|
+
assignmentSlug: cells[0],
|
|
5904
|
+
agent: cells[1],
|
|
5905
|
+
sessionId: cells[2],
|
|
5906
|
+
started: cells[3],
|
|
5907
|
+
status: cells[4] || "active",
|
|
5908
|
+
path: cells[5],
|
|
5909
|
+
projectSlug
|
|
5910
|
+
});
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5914
|
+
return sessions;
|
|
5322
5915
|
}
|
|
5323
|
-
function renderStatus(params2) {
|
|
5324
|
-
return `---
|
|
5325
|
-
project: ${params2.slug}
|
|
5326
|
-
generated: "${params2.timestamp}"
|
|
5327
|
-
status: pending
|
|
5328
|
-
progress:
|
|
5329
|
-
total: 0
|
|
5330
|
-
completed: 0
|
|
5331
|
-
in_progress: 0
|
|
5332
|
-
blocked: 0
|
|
5333
|
-
pending: 0
|
|
5334
|
-
review: 0
|
|
5335
|
-
failed: 0
|
|
5336
|
-
needsAttention:
|
|
5337
|
-
blockedCount: 0
|
|
5338
|
-
failedCount: 0
|
|
5339
|
-
openQuestions: 0
|
|
5340
|
-
---
|
|
5341
|
-
|
|
5342
|
-
# Project Status: ${params2.title}
|
|
5343
|
-
|
|
5344
|
-
**Status:** pending
|
|
5345
|
-
**Progress:** 0/0 assignments complete
|
|
5346
|
-
|
|
5347
|
-
## Assignments
|
|
5348
|
-
|
|
5349
|
-
No assignments yet.
|
|
5350
|
-
|
|
5351
|
-
## Dependency Graph
|
|
5352
|
-
|
|
5353
|
-
No dependencies yet.
|
|
5354
5916
|
|
|
5355
|
-
|
|
5917
|
+
// src/dashboard/agent-sessions.ts
|
|
5918
|
+
function rowToSession(row) {
|
|
5919
|
+
return {
|
|
5920
|
+
sessionId: row.session_id,
|
|
5921
|
+
projectSlug: row.project_slug ?? null,
|
|
5922
|
+
assignmentSlug: row.assignment_slug ?? null,
|
|
5923
|
+
agent: row.agent,
|
|
5924
|
+
started: row.started,
|
|
5925
|
+
ended: row.ended ?? null,
|
|
5926
|
+
status: row.status,
|
|
5927
|
+
path: row.path ?? "",
|
|
5928
|
+
description: row.description ?? null,
|
|
5929
|
+
transcriptPath: row.transcript_path ?? null
|
|
5930
|
+
};
|
|
5931
|
+
}
|
|
5932
|
+
async function appendSession(_projectDir, session) {
|
|
5933
|
+
const db2 = getSessionDb();
|
|
5934
|
+
db2.prepare(`
|
|
5935
|
+
INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path)
|
|
5936
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5937
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
5938
|
+
project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),
|
|
5939
|
+
assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),
|
|
5940
|
+
agent = excluded.agent,
|
|
5941
|
+
status = CASE WHEN status IN ('completed','stopped') THEN status ELSE excluded.status END,
|
|
5942
|
+
path = COALESCE(NULLIF(excluded.path, ''), path),
|
|
5943
|
+
description = COALESCE(NULLIF(excluded.description, ''), description),
|
|
5944
|
+
transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),
|
|
5945
|
+
updated_at = datetime('now')
|
|
5946
|
+
`).run(
|
|
5947
|
+
session.sessionId,
|
|
5948
|
+
session.projectSlug ?? null,
|
|
5949
|
+
session.assignmentSlug ?? null,
|
|
5950
|
+
session.agent,
|
|
5951
|
+
session.started,
|
|
5952
|
+
session.status,
|
|
5953
|
+
session.path,
|
|
5954
|
+
session.description ?? null,
|
|
5955
|
+
session.transcriptPath ?? null
|
|
5956
|
+
);
|
|
5957
|
+
}
|
|
5958
|
+
async function updateSessionStatus(_projectDir, sessionId, status) {
|
|
5959
|
+
const db2 = getSessionDb();
|
|
5960
|
+
const isTerminal = status === "completed" || status === "stopped";
|
|
5961
|
+
const result = isTerminal ? db2.prepare(
|
|
5962
|
+
"UPDATE sessions SET status = ?, ended = datetime('now'), updated_at = datetime('now') WHERE session_id = ?"
|
|
5963
|
+
).run(status, sessionId) : db2.prepare(
|
|
5964
|
+
"UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE session_id = ?"
|
|
5965
|
+
).run(status, sessionId);
|
|
5966
|
+
return result.changes > 0;
|
|
5967
|
+
}
|
|
5968
|
+
async function listAllSessions(_projectsDir) {
|
|
5969
|
+
const db2 = getSessionDb();
|
|
5970
|
+
const rows = db2.prepare("SELECT * FROM sessions ORDER BY started DESC").all();
|
|
5971
|
+
return rows.map(rowToSession);
|
|
5972
|
+
}
|
|
5973
|
+
async function listProjectSessions(_projectsDir, projectSlug, assignmentSlug) {
|
|
5974
|
+
const db2 = getSessionDb();
|
|
5975
|
+
if (assignmentSlug) {
|
|
5976
|
+
const rows2 = db2.prepare(
|
|
5977
|
+
"SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
|
|
5978
|
+
).all(projectSlug, assignmentSlug);
|
|
5979
|
+
return rows2.map(rowToSession);
|
|
5980
|
+
}
|
|
5981
|
+
const rows = db2.prepare("SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC").all(projectSlug);
|
|
5982
|
+
return rows.map(rowToSession);
|
|
5983
|
+
}
|
|
5984
|
+
async function deleteSessions(sessionIds) {
|
|
5985
|
+
if (sessionIds.length === 0) return 0;
|
|
5986
|
+
const db2 = getSessionDb();
|
|
5987
|
+
const placeholders = sessionIds.map(() => "?").join(", ");
|
|
5988
|
+
const result = db2.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
|
|
5989
|
+
return result.changes;
|
|
5990
|
+
}
|
|
5991
|
+
var DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
|
|
5992
|
+
async function readAssignmentStatusFromPath(assignmentMdPath) {
|
|
5993
|
+
if (!await fileExists(assignmentMdPath)) return null;
|
|
5994
|
+
const raw = await readFile9(assignmentMdPath, "utf-8");
|
|
5995
|
+
const match = raw.match(/^status:\s*(.+)$/m);
|
|
5996
|
+
return match ? match[1].trim() : null;
|
|
5997
|
+
}
|
|
5998
|
+
async function readAssignmentStatus(projectDir, assignmentSlug) {
|
|
5999
|
+
return readAssignmentStatusFromPath(
|
|
6000
|
+
resolve11(projectDir, "assignments", assignmentSlug, "assignment.md")
|
|
6001
|
+
);
|
|
6002
|
+
}
|
|
6003
|
+
async function reconcileActiveSessions(projectsDir, assignmentsDir2) {
|
|
6004
|
+
const db2 = getSessionDb();
|
|
6005
|
+
const activeSessions = db2.prepare("SELECT * FROM sessions WHERE status = 'active' AND assignment_slug IS NOT NULL").all();
|
|
6006
|
+
if (activeSessions.length === 0) return 0;
|
|
6007
|
+
const assignmentStatuses = /* @__PURE__ */ new Map();
|
|
6008
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6009
|
+
for (const session of activeSessions) {
|
|
6010
|
+
const aslug = session.assignment_slug;
|
|
6011
|
+
if (!aslug) continue;
|
|
6012
|
+
const projectKey = session.project_slug ?? "__standalone__";
|
|
6013
|
+
const key = `${projectKey}/${aslug}`;
|
|
6014
|
+
if (seen.has(key)) continue;
|
|
6015
|
+
seen.add(key);
|
|
6016
|
+
if (session.project_slug) {
|
|
6017
|
+
const status = await readAssignmentStatus(
|
|
6018
|
+
resolve11(projectsDir, session.project_slug),
|
|
6019
|
+
aslug
|
|
6020
|
+
);
|
|
6021
|
+
if (status) assignmentStatuses.set(key, status);
|
|
6022
|
+
} else if (assignmentsDir2) {
|
|
6023
|
+
const status = await readAssignmentStatusFromPath(
|
|
6024
|
+
resolve11(assignmentsDir2, aslug, "assignment.md")
|
|
6025
|
+
);
|
|
6026
|
+
if (status) assignmentStatuses.set(key, status);
|
|
6027
|
+
}
|
|
6028
|
+
}
|
|
6029
|
+
let totalUpdated = 0;
|
|
6030
|
+
for (const session of activeSessions) {
|
|
6031
|
+
const projectKey = session.project_slug ?? "__standalone__";
|
|
6032
|
+
const key = `${projectKey}/${session.assignment_slug}`;
|
|
6033
|
+
const assignmentStatus = assignmentStatuses.get(key);
|
|
6034
|
+
if (!assignmentStatus || !DONE_ASSIGNMENT_STATUSES.has(assignmentStatus)) continue;
|
|
6035
|
+
const newStatus = assignmentStatus === "failed" ? "stopped" : "completed";
|
|
6036
|
+
await updateSessionStatus("", session.session_id, newStatus);
|
|
6037
|
+
totalUpdated++;
|
|
6038
|
+
}
|
|
6039
|
+
return totalUpdated;
|
|
6040
|
+
}
|
|
6041
|
+
async function listSessionsByAssignment(projectSlug, assignmentSlug) {
|
|
6042
|
+
const db2 = getSessionDb();
|
|
6043
|
+
const rows = projectSlug === null ? db2.prepare(
|
|
6044
|
+
"SELECT * FROM sessions WHERE assignment_slug = ? AND project_slug IS NULL ORDER BY started DESC"
|
|
6045
|
+
).all(assignmentSlug) : db2.prepare(
|
|
6046
|
+
"SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
|
|
6047
|
+
).all(projectSlug, assignmentSlug);
|
|
6048
|
+
return rows.map(rowToSession);
|
|
6049
|
+
}
|
|
5356
6050
|
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
6051
|
+
// src/dashboard/watcher.ts
|
|
6052
|
+
import { watch } from "chokidar";
|
|
6053
|
+
import { relative, sep } from "path";
|
|
6054
|
+
function createWatcher(options) {
|
|
6055
|
+
const { projectsDir, assignmentsDir: assignmentsDir2, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, onMessage, debounceMs = 300 } = options;
|
|
6056
|
+
const pendingEvents = /* @__PURE__ */ new Map();
|
|
6057
|
+
const projectsWatcher = watch(projectsDir, {
|
|
6058
|
+
ignoreInitial: true,
|
|
6059
|
+
persistent: true,
|
|
6060
|
+
depth: 10,
|
|
6061
|
+
ignored: /(^|[\/\\])\../
|
|
6062
|
+
});
|
|
6063
|
+
function handleProjectChange(filePath) {
|
|
6064
|
+
const rel = relative(projectsDir, filePath);
|
|
6065
|
+
const parts = rel.split(sep);
|
|
6066
|
+
if (parts.length === 0) return;
|
|
6067
|
+
const projectSlug = parts[0];
|
|
6068
|
+
let assignmentSlug;
|
|
6069
|
+
let isProjectTodos = false;
|
|
6070
|
+
if (parts.length >= 3 && parts[1] === "assignments") {
|
|
6071
|
+
assignmentSlug = parts[2];
|
|
6072
|
+
} else if (parts.length >= 2 && parts[1] === "todos") {
|
|
6073
|
+
isProjectTodos = true;
|
|
6074
|
+
}
|
|
6075
|
+
const debounceKey = isProjectTodos ? `todos:${projectSlug}` : assignmentSlug ? `${projectSlug}/${assignmentSlug}` : projectSlug;
|
|
6076
|
+
const existing = pendingEvents.get(debounceKey);
|
|
6077
|
+
if (existing) clearTimeout(existing);
|
|
6078
|
+
const messageType = isProjectTodos ? "todos-updated" : assignmentSlug ? "assignment-updated" : "project-updated";
|
|
6079
|
+
pendingEvents.set(
|
|
6080
|
+
debounceKey,
|
|
6081
|
+
setTimeout(() => {
|
|
6082
|
+
pendingEvents.delete(debounceKey);
|
|
6083
|
+
const message = isProjectTodos ? {
|
|
6084
|
+
type: "todos-updated",
|
|
6085
|
+
projectSlug,
|
|
6086
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6087
|
+
} : {
|
|
6088
|
+
type: messageType,
|
|
6089
|
+
projectSlug,
|
|
6090
|
+
assignmentSlug,
|
|
6091
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6092
|
+
};
|
|
6093
|
+
onMessage(message);
|
|
6094
|
+
}, debounceMs)
|
|
6095
|
+
);
|
|
6096
|
+
}
|
|
6097
|
+
projectsWatcher.on("change", handleProjectChange);
|
|
6098
|
+
projectsWatcher.on("add", handleProjectChange);
|
|
6099
|
+
projectsWatcher.on("unlink", handleProjectChange);
|
|
6100
|
+
let standaloneWatcher = null;
|
|
6101
|
+
if (assignmentsDir2) {
|
|
6102
|
+
let handleStandaloneChange2 = function(filePath) {
|
|
6103
|
+
const rel = relative(assignmentsDir2, filePath);
|
|
6104
|
+
const parts = rel.split(sep);
|
|
6105
|
+
if (parts.length === 0) return;
|
|
6106
|
+
const assignmentId = parts[0];
|
|
6107
|
+
if (!assignmentId) return;
|
|
6108
|
+
const debounceKey = `__standalone__/${assignmentId}`;
|
|
6109
|
+
const existing = pendingEvents.get(debounceKey);
|
|
6110
|
+
if (existing) clearTimeout(existing);
|
|
6111
|
+
pendingEvents.set(
|
|
6112
|
+
debounceKey,
|
|
6113
|
+
setTimeout(() => {
|
|
6114
|
+
pendingEvents.delete(debounceKey);
|
|
6115
|
+
const message = {
|
|
6116
|
+
type: "assignment-updated",
|
|
6117
|
+
projectSlug: null,
|
|
6118
|
+
assignmentSlug: assignmentId,
|
|
6119
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6120
|
+
};
|
|
6121
|
+
onMessage(message);
|
|
6122
|
+
}, debounceMs)
|
|
6123
|
+
);
|
|
6124
|
+
};
|
|
6125
|
+
var handleStandaloneChange = handleStandaloneChange2;
|
|
6126
|
+
standaloneWatcher = watch(assignmentsDir2, {
|
|
6127
|
+
ignoreInitial: true,
|
|
6128
|
+
persistent: true,
|
|
6129
|
+
depth: 5,
|
|
6130
|
+
ignored: /(^|[\/\\])\../
|
|
6131
|
+
});
|
|
6132
|
+
standaloneWatcher.on("change", handleStandaloneChange2);
|
|
6133
|
+
standaloneWatcher.on("add", handleStandaloneChange2);
|
|
6134
|
+
standaloneWatcher.on("unlink", handleStandaloneChange2);
|
|
6135
|
+
}
|
|
6136
|
+
let serversWatcher = null;
|
|
6137
|
+
if (serversDir2) {
|
|
6138
|
+
let handleServerChange2 = function() {
|
|
6139
|
+
const debounceKey = "__servers__";
|
|
6140
|
+
const existing = pendingEvents.get(debounceKey);
|
|
6141
|
+
if (existing) clearTimeout(existing);
|
|
6142
|
+
pendingEvents.set(
|
|
6143
|
+
debounceKey,
|
|
6144
|
+
setTimeout(() => {
|
|
6145
|
+
pendingEvents.delete(debounceKey);
|
|
6146
|
+
const message = {
|
|
6147
|
+
type: "servers-updated",
|
|
6148
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6149
|
+
};
|
|
6150
|
+
onMessage(message);
|
|
6151
|
+
}, debounceMs)
|
|
6152
|
+
);
|
|
6153
|
+
};
|
|
6154
|
+
var handleServerChange = handleServerChange2;
|
|
6155
|
+
serversWatcher = watch(serversDir2, {
|
|
6156
|
+
ignoreInitial: true,
|
|
6157
|
+
persistent: true,
|
|
6158
|
+
depth: 1,
|
|
6159
|
+
ignored: /(^|[\/\\])\../
|
|
6160
|
+
});
|
|
6161
|
+
serversWatcher.on("change", handleServerChange2);
|
|
6162
|
+
serversWatcher.on("add", handleServerChange2);
|
|
6163
|
+
serversWatcher.on("unlink", handleServerChange2);
|
|
6164
|
+
}
|
|
6165
|
+
let playbooksWatcher = null;
|
|
6166
|
+
if (playbooksDir2) {
|
|
6167
|
+
let handlePlaybookChange2 = function() {
|
|
6168
|
+
const debounceKey = "__playbooks__";
|
|
6169
|
+
const existing = pendingEvents.get(debounceKey);
|
|
6170
|
+
if (existing) clearTimeout(existing);
|
|
6171
|
+
pendingEvents.set(
|
|
6172
|
+
debounceKey,
|
|
6173
|
+
setTimeout(() => {
|
|
6174
|
+
pendingEvents.delete(debounceKey);
|
|
6175
|
+
const message = {
|
|
6176
|
+
type: "playbooks-updated",
|
|
6177
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6178
|
+
};
|
|
6179
|
+
onMessage(message);
|
|
6180
|
+
}, debounceMs)
|
|
6181
|
+
);
|
|
6182
|
+
};
|
|
6183
|
+
var handlePlaybookChange = handlePlaybookChange2;
|
|
6184
|
+
playbooksWatcher = watch(playbooksDir2, {
|
|
6185
|
+
ignoreInitial: true,
|
|
6186
|
+
persistent: true,
|
|
6187
|
+
depth: 1,
|
|
6188
|
+
ignored: /(^|[\/\\])\../
|
|
6189
|
+
});
|
|
6190
|
+
playbooksWatcher.on("change", handlePlaybookChange2);
|
|
6191
|
+
playbooksWatcher.on("add", handlePlaybookChange2);
|
|
6192
|
+
playbooksWatcher.on("unlink", handlePlaybookChange2);
|
|
6193
|
+
}
|
|
6194
|
+
let todosWatcher = null;
|
|
6195
|
+
if (todosDir2) {
|
|
6196
|
+
let handleTodoChange2 = function() {
|
|
6197
|
+
const debounceKey = "__todos__";
|
|
6198
|
+
const existing = pendingEvents.get(debounceKey);
|
|
6199
|
+
if (existing) clearTimeout(existing);
|
|
6200
|
+
pendingEvents.set(
|
|
6201
|
+
debounceKey,
|
|
6202
|
+
setTimeout(() => {
|
|
6203
|
+
pendingEvents.delete(debounceKey);
|
|
6204
|
+
const message = {
|
|
6205
|
+
type: "todos-updated",
|
|
6206
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6207
|
+
};
|
|
6208
|
+
onMessage(message);
|
|
6209
|
+
}, debounceMs)
|
|
6210
|
+
);
|
|
6211
|
+
};
|
|
6212
|
+
var handleTodoChange = handleTodoChange2;
|
|
6213
|
+
todosWatcher = watch(todosDir2, {
|
|
6214
|
+
ignoreInitial: true,
|
|
6215
|
+
persistent: true,
|
|
6216
|
+
depth: 1,
|
|
6217
|
+
ignored: /(^|[\/\\])\../
|
|
6218
|
+
});
|
|
6219
|
+
todosWatcher.on("change", handleTodoChange2);
|
|
6220
|
+
todosWatcher.on("add", handleTodoChange2);
|
|
6221
|
+
todosWatcher.on("unlink", handleTodoChange2);
|
|
6222
|
+
}
|
|
6223
|
+
return {
|
|
6224
|
+
close: async () => {
|
|
6225
|
+
pendingEvents.forEach((timeout) => {
|
|
6226
|
+
clearTimeout(timeout);
|
|
6227
|
+
});
|
|
6228
|
+
pendingEvents.clear();
|
|
6229
|
+
await projectsWatcher.close();
|
|
6230
|
+
if (standaloneWatcher) await standaloneWatcher.close();
|
|
6231
|
+
if (serversWatcher) await serversWatcher.close();
|
|
6232
|
+
if (playbooksWatcher) await playbooksWatcher.close();
|
|
6233
|
+
if (todosWatcher) await todosWatcher.close();
|
|
6234
|
+
}
|
|
6235
|
+
};
|
|
5361
6236
|
}
|
|
5362
|
-
function renderResourcesIndex(params2) {
|
|
5363
|
-
return `---
|
|
5364
|
-
project: ${params2.slug}
|
|
5365
|
-
generated: "${params2.timestamp}"
|
|
5366
|
-
total: 0
|
|
5367
|
-
---
|
|
5368
|
-
|
|
5369
|
-
# Resources
|
|
5370
6237
|
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
function renderMemoriesIndex(params2) {
|
|
5376
|
-
return `---
|
|
5377
|
-
project: ${params2.slug}
|
|
5378
|
-
generated: "${params2.timestamp}"
|
|
5379
|
-
total: 0
|
|
5380
|
-
---
|
|
6238
|
+
// src/dashboard/server.ts
|
|
6239
|
+
init_fs();
|
|
6240
|
+
init_config2();
|
|
6241
|
+
init_hotkeysCatalog();
|
|
5381
6242
|
|
|
5382
|
-
|
|
6243
|
+
// src/dashboard/api-write.ts
|
|
6244
|
+
init_lifecycle();
|
|
6245
|
+
init_slug();
|
|
6246
|
+
init_uuid();
|
|
6247
|
+
init_timestamp();
|
|
6248
|
+
init_fs();
|
|
6249
|
+
init_parser();
|
|
6250
|
+
import { Router } from "express";
|
|
6251
|
+
import { resolve as resolve12 } from "path";
|
|
6252
|
+
import { rm, readFile as readFile10 } from "fs/promises";
|
|
5383
6253
|
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
6254
|
+
// src/dashboard/acceptance-criteria.ts
|
|
6255
|
+
function splitFrontmatter(content) {
|
|
6256
|
+
const match = content.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n?)([\s\S]*)$/);
|
|
6257
|
+
if (!match) {
|
|
6258
|
+
return { prefix: "", body: content };
|
|
6259
|
+
}
|
|
6260
|
+
return {
|
|
6261
|
+
prefix: match[1],
|
|
6262
|
+
body: match[2]
|
|
6263
|
+
};
|
|
5387
6264
|
}
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
6265
|
+
function toggleAcceptanceCriterion(content, index, checked) {
|
|
6266
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
6267
|
+
return { error: "acceptance criteria index must be a non-negative integer" };
|
|
6268
|
+
}
|
|
6269
|
+
const { prefix, body } = splitFrontmatter(content);
|
|
6270
|
+
const lines = body.split("\n");
|
|
6271
|
+
const sectionStart = lines.findIndex((line) => /^##\s+Acceptance Criteria\s*$/i.test(line.trim()));
|
|
6272
|
+
if (sectionStart === -1) {
|
|
6273
|
+
return { error: "Acceptance Criteria section not found." };
|
|
6274
|
+
}
|
|
6275
|
+
let sectionEnd = lines.length;
|
|
6276
|
+
for (let lineIndex = sectionStart + 1; lineIndex < lines.length; lineIndex += 1) {
|
|
6277
|
+
if (/^#{1,2}\s+\S/.test(lines[lineIndex].trim())) {
|
|
6278
|
+
sectionEnd = lineIndex;
|
|
6279
|
+
break;
|
|
6280
|
+
}
|
|
6281
|
+
}
|
|
6282
|
+
const checklistLines = lines.map((line, lineIndex) => ({ line, lineIndex })).filter(
|
|
6283
|
+
({ lineIndex, line }) => lineIndex > sectionStart && lineIndex < sectionEnd && /^\s*[-*]\s+\[( |x|X)\]\s+.*$/.test(line)
|
|
6284
|
+
);
|
|
6285
|
+
const target = checklistLines[index];
|
|
6286
|
+
if (!target) {
|
|
6287
|
+
return { error: `Acceptance criteria item ${index} not found.` };
|
|
6288
|
+
}
|
|
6289
|
+
const nextLine = target.line.replace(
|
|
6290
|
+
/^(\s*[-*]\s+\[)( |x|X)(\]\s+.*)$/,
|
|
6291
|
+
`$1${checked ? "x" : " "}$3`
|
|
6292
|
+
);
|
|
6293
|
+
lines[target.lineIndex] = nextLine;
|
|
6294
|
+
return {
|
|
6295
|
+
content: `${prefix}${lines.join("\n")}`
|
|
6296
|
+
};
|
|
5406
6297
|
}
|
|
5407
6298
|
|
|
5408
6299
|
// src/dashboard/api-write.ts
|
|
6300
|
+
init_api();
|
|
6301
|
+
init_assignment_resolver();
|
|
6302
|
+
init_templates();
|
|
5409
6303
|
init_lifecycle();
|
|
6304
|
+
init_templates();
|
|
5410
6305
|
init_parser();
|
|
5411
6306
|
function extractFrontmatter3(content) {
|
|
5412
6307
|
const trimmed = content.trimStart();
|
|
@@ -5509,7 +6404,7 @@ async function readCurrentDocument(filePath) {
|
|
|
5509
6404
|
}
|
|
5510
6405
|
return readFile10(filePath, "utf-8");
|
|
5511
6406
|
}
|
|
5512
|
-
function createWriteRouter(projectsDir,
|
|
6407
|
+
function createWriteRouter(projectsDir, assignmentsDir2) {
|
|
5513
6408
|
const router = Router();
|
|
5514
6409
|
router.get("/api/templates/project", (_req, res) => {
|
|
5515
6410
|
const content = renderProject({
|
|
@@ -6263,7 +7158,7 @@ ${entry}`;
|
|
|
6263
7158
|
});
|
|
6264
7159
|
router.post("/api/assignments", async (req, res) => {
|
|
6265
7160
|
try {
|
|
6266
|
-
if (!
|
|
7161
|
+
if (!assignmentsDir2) {
|
|
6267
7162
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
6268
7163
|
return;
|
|
6269
7164
|
}
|
|
@@ -6304,7 +7199,7 @@ ${entry}`;
|
|
|
6304
7199
|
return;
|
|
6305
7200
|
}
|
|
6306
7201
|
const id2 = generateId();
|
|
6307
|
-
const assignmentDir2 = resolve12(
|
|
7202
|
+
const assignmentDir2 = resolve12(assignmentsDir2, id2);
|
|
6308
7203
|
if (await fileExists(assignmentDir2)) {
|
|
6309
7204
|
res.status(500).json({ error: "UUID collision \u2014 try again" });
|
|
6310
7205
|
return;
|
|
@@ -6333,7 +7228,7 @@ ${entry}`;
|
|
|
6333
7228
|
resolve12(assignmentDir2, "comments.md"),
|
|
6334
7229
|
renderComments({ assignment: id2, timestamp: timestamp2 })
|
|
6335
7230
|
);
|
|
6336
|
-
const detail2 = await getAssignmentDetailById(projectsDir,
|
|
7231
|
+
const detail2 = await getAssignmentDetailById(projectsDir, assignmentsDir2, id2);
|
|
6337
7232
|
res.status(201).json({ assignment: detail2 });
|
|
6338
7233
|
return;
|
|
6339
7234
|
}
|
|
@@ -6348,7 +7243,7 @@ ${entry}`;
|
|
|
6348
7243
|
return;
|
|
6349
7244
|
}
|
|
6350
7245
|
const id = generateId();
|
|
6351
|
-
const assignmentDir = resolve12(
|
|
7246
|
+
const assignmentDir = resolve12(assignmentsDir2, id);
|
|
6352
7247
|
if (await fileExists(assignmentDir)) {
|
|
6353
7248
|
res.status(500).json({ error: "UUID collision \u2014 try again" });
|
|
6354
7249
|
return;
|
|
@@ -6389,7 +7284,7 @@ ${entry}`;
|
|
|
6389
7284
|
resolve12(assignmentDir, "comments.md"),
|
|
6390
7285
|
renderComments({ assignment: id, timestamp })
|
|
6391
7286
|
);
|
|
6392
|
-
const detail = await getAssignmentDetailById(projectsDir,
|
|
7287
|
+
const detail = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
|
|
6393
7288
|
res.status(201).json({ assignment: detail });
|
|
6394
7289
|
} catch (error) {
|
|
6395
7290
|
console.error("Error creating standalone assignment:", error);
|
|
@@ -6398,18 +7293,18 @@ ${entry}`;
|
|
|
6398
7293
|
});
|
|
6399
7294
|
router.post("/api/assignments/:id/comments", async (req, res) => {
|
|
6400
7295
|
try {
|
|
6401
|
-
if (!
|
|
7296
|
+
if (!assignmentsDir2) {
|
|
6402
7297
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
6403
7298
|
return;
|
|
6404
7299
|
}
|
|
6405
7300
|
const id = getParam(req.params.id);
|
|
6406
|
-
const resolved = await resolveAssignmentById(projectsDir,
|
|
7301
|
+
const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
|
|
6407
7302
|
if (!resolved) {
|
|
6408
7303
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6409
7304
|
return;
|
|
6410
7305
|
}
|
|
6411
7306
|
await appendCommentTo(resolved.assignmentDir, resolved.standalone ? resolved.id : resolved.assignmentSlug, req, res, async () => {
|
|
6412
|
-
return resolved.standalone ? getAssignmentDetailById(projectsDir,
|
|
7307
|
+
return resolved.standalone ? getAssignmentDetailById(projectsDir, assignmentsDir2, id) : getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);
|
|
6413
7308
|
});
|
|
6414
7309
|
} catch (error) {
|
|
6415
7310
|
console.error("Error appending comment (by id):", error);
|
|
@@ -6418,19 +7313,19 @@ ${entry}`;
|
|
|
6418
7313
|
});
|
|
6419
7314
|
router.patch("/api/assignments/:id/comments/:commentId/resolved", async (req, res) => {
|
|
6420
7315
|
try {
|
|
6421
|
-
if (!
|
|
7316
|
+
if (!assignmentsDir2) {
|
|
6422
7317
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
6423
7318
|
return;
|
|
6424
7319
|
}
|
|
6425
7320
|
const id = getParam(req.params.id);
|
|
6426
7321
|
const commentId = getParam(req.params.commentId);
|
|
6427
|
-
const resolved = await resolveAssignmentById(projectsDir,
|
|
7322
|
+
const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
|
|
6428
7323
|
if (!resolved) {
|
|
6429
7324
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6430
7325
|
return;
|
|
6431
7326
|
}
|
|
6432
7327
|
await toggleCommentResolvedAt(resolved.assignmentDir, commentId, req, res, async () => {
|
|
6433
|
-
return resolved.standalone ? getAssignmentDetailById(projectsDir,
|
|
7328
|
+
return resolved.standalone ? getAssignmentDetailById(projectsDir, assignmentsDir2, id) : getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);
|
|
6434
7329
|
});
|
|
6435
7330
|
} catch (error) {
|
|
6436
7331
|
console.error("Error toggling comment resolved (by id):", error);
|
|
@@ -6438,12 +7333,12 @@ ${entry}`;
|
|
|
6438
7333
|
}
|
|
6439
7334
|
});
|
|
6440
7335
|
router.get("/api/assignments/:id/edit", async (req, res) => {
|
|
6441
|
-
if (!
|
|
7336
|
+
if (!assignmentsDir2) {
|
|
6442
7337
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
6443
7338
|
return;
|
|
6444
7339
|
}
|
|
6445
7340
|
const id = getParam(req.params.id);
|
|
6446
|
-
const doc = await getEditableDocumentById(projectsDir,
|
|
7341
|
+
const doc = await getEditableDocumentById(projectsDir, assignmentsDir2, "assignment", id);
|
|
6447
7342
|
if (!doc) {
|
|
6448
7343
|
res.status(404).json({ error: "Assignment not found" });
|
|
6449
7344
|
return;
|
|
@@ -6451,12 +7346,12 @@ ${entry}`;
|
|
|
6451
7346
|
res.json(doc);
|
|
6452
7347
|
});
|
|
6453
7348
|
router.get("/api/assignments/:id/plan/edit", async (req, res) => {
|
|
6454
|
-
if (!
|
|
7349
|
+
if (!assignmentsDir2) {
|
|
6455
7350
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
6456
7351
|
return;
|
|
6457
7352
|
}
|
|
6458
7353
|
const id = getParam(req.params.id);
|
|
6459
|
-
const doc = await getEditableDocumentById(projectsDir,
|
|
7354
|
+
const doc = await getEditableDocumentById(projectsDir, assignmentsDir2, "plan", id);
|
|
6460
7355
|
if (!doc) {
|
|
6461
7356
|
res.status(404).json({ error: "Plan not found" });
|
|
6462
7357
|
return;
|
|
@@ -6464,12 +7359,12 @@ ${entry}`;
|
|
|
6464
7359
|
res.json(doc);
|
|
6465
7360
|
});
|
|
6466
7361
|
router.get("/api/assignments/:id/scratchpad/edit", async (req, res) => {
|
|
6467
|
-
if (!
|
|
7362
|
+
if (!assignmentsDir2) {
|
|
6468
7363
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
6469
7364
|
return;
|
|
6470
7365
|
}
|
|
6471
7366
|
const id = getParam(req.params.id);
|
|
6472
|
-
const doc = await getEditableDocumentById(projectsDir,
|
|
7367
|
+
const doc = await getEditableDocumentById(projectsDir, assignmentsDir2, "scratchpad", id);
|
|
6473
7368
|
if (!doc) {
|
|
6474
7369
|
res.status(404).json({ error: "Scratchpad not found" });
|
|
6475
7370
|
return;
|
|
@@ -6477,12 +7372,12 @@ ${entry}`;
|
|
|
6477
7372
|
res.json(doc);
|
|
6478
7373
|
});
|
|
6479
7374
|
router.get("/api/assignments/:id/handoff/edit", async (req, res) => {
|
|
6480
|
-
if (!
|
|
7375
|
+
if (!assignmentsDir2) {
|
|
6481
7376
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
6482
7377
|
return;
|
|
6483
7378
|
}
|
|
6484
7379
|
const id = getParam(req.params.id);
|
|
6485
|
-
const doc = await getEditableDocumentById(projectsDir,
|
|
7380
|
+
const doc = await getEditableDocumentById(projectsDir, assignmentsDir2, "handoff", id);
|
|
6486
7381
|
if (!doc) {
|
|
6487
7382
|
res.status(404).json({ error: "Handoff log not found" });
|
|
6488
7383
|
return;
|
|
@@ -6490,12 +7385,12 @@ ${entry}`;
|
|
|
6490
7385
|
res.json(doc);
|
|
6491
7386
|
});
|
|
6492
7387
|
router.get("/api/assignments/:id/decision-record/edit", async (req, res) => {
|
|
6493
|
-
if (!
|
|
7388
|
+
if (!assignmentsDir2) {
|
|
6494
7389
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
6495
7390
|
return;
|
|
6496
7391
|
}
|
|
6497
7392
|
const id = getParam(req.params.id);
|
|
6498
|
-
const doc = await getEditableDocumentById(projectsDir,
|
|
7393
|
+
const doc = await getEditableDocumentById(projectsDir, assignmentsDir2, "decision-record", id);
|
|
6499
7394
|
if (!doc) {
|
|
6500
7395
|
res.status(404).json({ error: "Decision record not found" });
|
|
6501
7396
|
return;
|
|
@@ -6504,12 +7399,12 @@ ${entry}`;
|
|
|
6504
7399
|
});
|
|
6505
7400
|
router.patch("/api/assignments/:id", async (req, res) => {
|
|
6506
7401
|
try {
|
|
6507
|
-
if (!
|
|
7402
|
+
if (!assignmentsDir2) {
|
|
6508
7403
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
6509
7404
|
return;
|
|
6510
7405
|
}
|
|
6511
7406
|
const id = getParam(req.params.id);
|
|
6512
|
-
const resolved = await resolveAssignmentById(projectsDir,
|
|
7407
|
+
const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
|
|
6513
7408
|
if (!resolved) {
|
|
6514
7409
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6515
7410
|
return;
|
|
@@ -6537,7 +7432,7 @@ ${entry}`;
|
|
|
6537
7432
|
}
|
|
6538
7433
|
nextContent = setTopLevelField(nextContent, "updated", nowTimestamp());
|
|
6539
7434
|
await writeFileForce(assignmentPath, nextContent);
|
|
6540
|
-
const assignment = await getAssignmentDetailById(projectsDir,
|
|
7435
|
+
const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
|
|
6541
7436
|
res.json({ assignment, content: nextContent });
|
|
6542
7437
|
} catch (error) {
|
|
6543
7438
|
console.error("Error updating standalone assignment:", error);
|
|
@@ -6546,12 +7441,12 @@ ${entry}`;
|
|
|
6546
7441
|
});
|
|
6547
7442
|
router.patch("/api/assignments/:id/plan", async (req, res) => {
|
|
6548
7443
|
try {
|
|
6549
|
-
if (!
|
|
7444
|
+
if (!assignmentsDir2) {
|
|
6550
7445
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
6551
7446
|
return;
|
|
6552
7447
|
}
|
|
6553
7448
|
const id = getParam(req.params.id);
|
|
6554
|
-
const resolved = await resolveAssignmentById(projectsDir,
|
|
7449
|
+
const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
|
|
6555
7450
|
if (!resolved) {
|
|
6556
7451
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6557
7452
|
return;
|
|
@@ -6571,7 +7466,7 @@ ${entry}`;
|
|
|
6571
7466
|
}
|
|
6572
7467
|
const nextContent = setTopLevelField(nextContentRaw, "updated", nowTimestamp());
|
|
6573
7468
|
await writeFileForce(planPath, nextContent);
|
|
6574
|
-
const assignment = await getAssignmentDetailById(projectsDir,
|
|
7469
|
+
const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
|
|
6575
7470
|
res.json({ assignment, content: nextContent });
|
|
6576
7471
|
} catch (error) {
|
|
6577
7472
|
console.error("Error updating standalone plan:", error);
|
|
@@ -6580,12 +7475,12 @@ ${entry}`;
|
|
|
6580
7475
|
});
|
|
6581
7476
|
router.patch("/api/assignments/:id/scratchpad", async (req, res) => {
|
|
6582
7477
|
try {
|
|
6583
|
-
if (!
|
|
7478
|
+
if (!assignmentsDir2) {
|
|
6584
7479
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
6585
7480
|
return;
|
|
6586
7481
|
}
|
|
6587
7482
|
const id = getParam(req.params.id);
|
|
6588
|
-
const resolved = await resolveAssignmentById(projectsDir,
|
|
7483
|
+
const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
|
|
6589
7484
|
if (!resolved) {
|
|
6590
7485
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6591
7486
|
return;
|
|
@@ -6605,7 +7500,7 @@ ${entry}`;
|
|
|
6605
7500
|
}
|
|
6606
7501
|
const nextContent = setTopLevelField(nextContentRaw, "updated", nowTimestamp());
|
|
6607
7502
|
await writeFileForce(scratchpadPath, nextContent);
|
|
6608
|
-
const assignment = await getAssignmentDetailById(projectsDir,
|
|
7503
|
+
const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
|
|
6609
7504
|
res.json({ assignment, content: nextContent });
|
|
6610
7505
|
} catch (error) {
|
|
6611
7506
|
console.error("Error updating standalone scratchpad:", error);
|
|
@@ -6614,12 +7509,12 @@ ${entry}`;
|
|
|
6614
7509
|
});
|
|
6615
7510
|
router.post("/api/assignments/:id/handoff/entries", async (req, res) => {
|
|
6616
7511
|
try {
|
|
6617
|
-
if (!
|
|
7512
|
+
if (!assignmentsDir2) {
|
|
6618
7513
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
6619
7514
|
return;
|
|
6620
7515
|
}
|
|
6621
7516
|
const id = getParam(req.params.id);
|
|
6622
|
-
const resolved = await resolveAssignmentById(projectsDir,
|
|
7517
|
+
const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
|
|
6623
7518
|
if (!resolved) {
|
|
6624
7519
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6625
7520
|
return;
|
|
@@ -6645,7 +7540,7 @@ ${entry}`;
|
|
|
6645
7540
|
"No handoffs recorded yet."
|
|
6646
7541
|
);
|
|
6647
7542
|
await writeFileForce(handoffPath, nextContent);
|
|
6648
|
-
const assignment = await getAssignmentDetailById(projectsDir,
|
|
7543
|
+
const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
|
|
6649
7544
|
res.status(201).json({ assignment, content: nextContent });
|
|
6650
7545
|
} catch (error) {
|
|
6651
7546
|
console.error("Error appending standalone handoff entry:", error);
|
|
@@ -6654,12 +7549,12 @@ ${entry}`;
|
|
|
6654
7549
|
});
|
|
6655
7550
|
router.post("/api/assignments/:id/decision-record/entries", async (req, res) => {
|
|
6656
7551
|
try {
|
|
6657
|
-
if (!
|
|
7552
|
+
if (!assignmentsDir2) {
|
|
6658
7553
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
6659
7554
|
return;
|
|
6660
7555
|
}
|
|
6661
7556
|
const id = getParam(req.params.id);
|
|
6662
|
-
const resolved = await resolveAssignmentById(projectsDir,
|
|
7557
|
+
const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
|
|
6663
7558
|
if (!resolved) {
|
|
6664
7559
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6665
7560
|
return;
|
|
@@ -6685,7 +7580,7 @@ ${entry}`;
|
|
|
6685
7580
|
"No decisions recorded yet."
|
|
6686
7581
|
);
|
|
6687
7582
|
await writeFileForce(decisionPath, nextContent);
|
|
6688
|
-
const assignment = await getAssignmentDetailById(projectsDir,
|
|
7583
|
+
const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
|
|
6689
7584
|
res.status(201).json({ assignment, content: nextContent });
|
|
6690
7585
|
} catch (error) {
|
|
6691
7586
|
console.error("Error appending standalone decision entry:", error);
|
|
@@ -6694,12 +7589,12 @@ ${entry}`;
|
|
|
6694
7589
|
});
|
|
6695
7590
|
router.post("/api/assignments/:id/status-override", async (req, res) => {
|
|
6696
7591
|
try {
|
|
6697
|
-
if (!
|
|
7592
|
+
if (!assignmentsDir2) {
|
|
6698
7593
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
6699
7594
|
return;
|
|
6700
7595
|
}
|
|
6701
7596
|
const id = getParam(req.params.id);
|
|
6702
|
-
const resolved = await resolveAssignmentById(projectsDir,
|
|
7597
|
+
const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
|
|
6703
7598
|
if (!resolved) {
|
|
6704
7599
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6705
7600
|
return;
|
|
@@ -6723,7 +7618,7 @@ ${entry}`;
|
|
|
6723
7618
|
content = setTopLevelField(content, "blockedReason", null);
|
|
6724
7619
|
}
|
|
6725
7620
|
await writeFileForce(assignmentPath, content);
|
|
6726
|
-
const assignment = await getAssignmentDetailById(projectsDir,
|
|
7621
|
+
const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
|
|
6727
7622
|
res.json({ assignment });
|
|
6728
7623
|
} catch (error) {
|
|
6729
7624
|
console.error("Error overriding standalone status:", error);
|
|
@@ -6732,12 +7627,12 @@ ${entry}`;
|
|
|
6732
7627
|
});
|
|
6733
7628
|
router.patch("/api/assignments/:id/acceptance-criteria/:index", async (req, res) => {
|
|
6734
7629
|
try {
|
|
6735
|
-
if (!
|
|
7630
|
+
if (!assignmentsDir2) {
|
|
6736
7631
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
6737
7632
|
return;
|
|
6738
7633
|
}
|
|
6739
7634
|
const id = getParam(req.params.id);
|
|
6740
|
-
const resolved = await resolveAssignmentById(projectsDir,
|
|
7635
|
+
const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
|
|
6741
7636
|
if (!resolved) {
|
|
6742
7637
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6743
7638
|
return;
|
|
@@ -6761,7 +7656,7 @@ ${entry}`;
|
|
|
6761
7656
|
}
|
|
6762
7657
|
const nextContent = setTopLevelField(result.content, "updated", nowTimestamp());
|
|
6763
7658
|
await writeFileForce(assignmentPath, nextContent);
|
|
6764
|
-
const assignment = await getAssignmentDetailById(projectsDir,
|
|
7659
|
+
const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
|
|
6765
7660
|
res.json({ assignment, content: nextContent });
|
|
6766
7661
|
} catch (error) {
|
|
6767
7662
|
console.error("Error toggling standalone acceptance criterion:", error);
|
|
@@ -6770,13 +7665,13 @@ ${entry}`;
|
|
|
6770
7665
|
});
|
|
6771
7666
|
router.post("/api/assignments/:id/transitions/:command", async (req, res) => {
|
|
6772
7667
|
try {
|
|
6773
|
-
if (!
|
|
7668
|
+
if (!assignmentsDir2) {
|
|
6774
7669
|
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
6775
7670
|
return;
|
|
6776
7671
|
}
|
|
6777
7672
|
const id = getParam(req.params.id);
|
|
6778
7673
|
const command = getParam(req.params.command);
|
|
6779
|
-
const resolved = await resolveAssignmentById(projectsDir,
|
|
7674
|
+
const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
|
|
6780
7675
|
if (!resolved) {
|
|
6781
7676
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
6782
7677
|
return;
|
|
@@ -6794,7 +7689,7 @@ ${entry}`;
|
|
|
6794
7689
|
res.status(400).json({ error: transitionResult.message, fromStatus: transitionResult.fromStatus });
|
|
6795
7690
|
return;
|
|
6796
7691
|
}
|
|
6797
|
-
const detail = resolved.standalone ? await getAssignmentDetailById(projectsDir,
|
|
7692
|
+
const detail = resolved.standalone ? await getAssignmentDetailById(projectsDir, assignmentsDir2, id) : await getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);
|
|
6798
7693
|
res.json({ assignment: detail, warnings: transitionResult.warnings ?? [] });
|
|
6799
7694
|
} catch (error) {
|
|
6800
7695
|
console.error("Error transitioning by id:", error);
|
|
@@ -6889,11 +7784,11 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloa
|
|
|
6889
7784
|
init_servers();
|
|
6890
7785
|
init_scanner();
|
|
6891
7786
|
import { Router as Router2 } from "express";
|
|
6892
|
-
function createServersRouter(serversDir2, projectsDir,
|
|
7787
|
+
function createServersRouter(serversDir2, projectsDir, assignmentsDir2) {
|
|
6893
7788
|
const router = Router2();
|
|
6894
7789
|
router.get("/", async (_req, res) => {
|
|
6895
7790
|
try {
|
|
6896
|
-
const result = await scanAllSessions(serversDir2, projectsDir, { assignmentsDir });
|
|
7791
|
+
const result = await scanAllSessions(serversDir2, projectsDir, { assignmentsDir: assignmentsDir2 });
|
|
6897
7792
|
res.json(result);
|
|
6898
7793
|
} catch (error) {
|
|
6899
7794
|
res.status(500).json({ error: error instanceof Error ? error.message : "Scan failed" });
|
|
@@ -6901,7 +7796,7 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir) {
|
|
|
6901
7796
|
});
|
|
6902
7797
|
router.get("/:name", async (req, res) => {
|
|
6903
7798
|
try {
|
|
6904
|
-
const session = await scanSingleSession(serversDir2, projectsDir, req.params.name, { assignmentsDir });
|
|
7799
|
+
const session = await scanSingleSession(serversDir2, projectsDir, req.params.name, { assignmentsDir: assignmentsDir2 });
|
|
6905
7800
|
if (!session) {
|
|
6906
7801
|
res.status(404).json({ error: "Session not found" });
|
|
6907
7802
|
return;
|
|
@@ -6952,7 +7847,7 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir) {
|
|
|
6952
7847
|
await updateLastRefreshed(serversDir2, name);
|
|
6953
7848
|
}
|
|
6954
7849
|
clearScanCache();
|
|
6955
|
-
const result = await scanAllSessions(serversDir2, projectsDir, { bypassCache: true, assignmentsDir });
|
|
7850
|
+
const result = await scanAllSessions(serversDir2, projectsDir, { bypassCache: true, assignmentsDir: assignmentsDir2 });
|
|
6956
7851
|
res.json(result);
|
|
6957
7852
|
} catch (error) {
|
|
6958
7853
|
res.status(500).json({ error: error instanceof Error ? error.message : "Refresh failed" });
|
|
@@ -6967,7 +7862,7 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir) {
|
|
|
6967
7862
|
}
|
|
6968
7863
|
await updateLastRefreshed(serversDir2, req.params.name);
|
|
6969
7864
|
clearScanCache();
|
|
6970
|
-
const session = await scanSingleSession(serversDir2, projectsDir, req.params.name, { assignmentsDir });
|
|
7865
|
+
const session = await scanSingleSession(serversDir2, projectsDir, req.params.name, { assignmentsDir: assignmentsDir2 });
|
|
6971
7866
|
res.json(session);
|
|
6972
7867
|
} catch (error) {
|
|
6973
7868
|
res.status(500).json({ error: error instanceof Error ? error.message : "Refresh failed" });
|
|
@@ -7006,11 +7901,11 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir) {
|
|
|
7006
7901
|
import { Router as Router3 } from "express";
|
|
7007
7902
|
import { resolve as resolve13 } from "path";
|
|
7008
7903
|
init_fs();
|
|
7009
|
-
function createAgentSessionsRouter(projectsDir, broadcast,
|
|
7904
|
+
function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
|
|
7010
7905
|
const router = Router3();
|
|
7011
7906
|
router.get("/", async (_req, res) => {
|
|
7012
7907
|
try {
|
|
7013
|
-
await reconcileActiveSessions(projectsDir,
|
|
7908
|
+
await reconcileActiveSessions(projectsDir, assignmentsDir2);
|
|
7014
7909
|
const sessions = await listAllSessions(projectsDir);
|
|
7015
7910
|
res.json({ sessions, generatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
7016
7911
|
} catch (error) {
|
|
@@ -7026,7 +7921,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
|
|
|
7026
7921
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
7027
7922
|
return;
|
|
7028
7923
|
}
|
|
7029
|
-
await reconcileActiveSessions(projectsDir,
|
|
7924
|
+
await reconcileActiveSessions(projectsDir, assignmentsDir2);
|
|
7030
7925
|
const sessions = await listProjectSessions(projectsDir, projectSlug, assignment);
|
|
7031
7926
|
res.json({ sessions, generatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
7032
7927
|
} catch (error) {
|
|
@@ -7114,12 +8009,14 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
|
|
|
7114
8009
|
// src/dashboard/api-playbooks.ts
|
|
7115
8010
|
init_api();
|
|
7116
8011
|
init_parser();
|
|
7117
|
-
|
|
7118
|
-
import { resolve as resolve14 } from "path";
|
|
7119
|
-
import { readFile as readFile11, unlink as unlink2 } from "fs/promises";
|
|
8012
|
+
init_slug();
|
|
7120
8013
|
init_timestamp();
|
|
7121
8014
|
init_fs();
|
|
8015
|
+
init_playbook();
|
|
7122
8016
|
init_playbooks();
|
|
8017
|
+
import { Router as Router4 } from "express";
|
|
8018
|
+
import { resolve as resolve14 } from "path";
|
|
8019
|
+
import { readFile as readFile11, unlink as unlink2 } from "fs/promises";
|
|
7123
8020
|
function createPlaybooksRouter(playbooksDir2) {
|
|
7124
8021
|
const router = Router4();
|
|
7125
8022
|
router.get("/", async (_req, res) => {
|
|
@@ -7296,6 +8193,11 @@ function withLock(lockKey, fn) {
|
|
|
7296
8193
|
function wsLock(workspace, fn) {
|
|
7297
8194
|
return withLock(`ws:${workspace}`, fn);
|
|
7298
8195
|
}
|
|
8196
|
+
function touchItem(item) {
|
|
8197
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8198
|
+
if (item.createdAt === null) item.createdAt = now;
|
|
8199
|
+
item.updatedAt = now;
|
|
8200
|
+
}
|
|
7299
8201
|
function createTodosRouter(todosDir2, broadcast) {
|
|
7300
8202
|
const router = Router5();
|
|
7301
8203
|
function broadcastUpdate() {
|
|
@@ -7358,12 +8260,18 @@ function createTodosRouter(todosDir2, broadcast) {
|
|
|
7358
8260
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
7359
8261
|
const existingIds = new Set(checklist.items.map((i) => i.id));
|
|
7360
8262
|
const id = generateUniqueId(existingIds);
|
|
8263
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7361
8264
|
const newItem = {
|
|
7362
8265
|
id,
|
|
7363
8266
|
description,
|
|
7364
8267
|
status: "open",
|
|
7365
8268
|
tags: Array.isArray(tags) ? tags : [],
|
|
7366
|
-
session: null
|
|
8269
|
+
session: null,
|
|
8270
|
+
branch: null,
|
|
8271
|
+
worktreePath: null,
|
|
8272
|
+
createdAt: now,
|
|
8273
|
+
updatedAt: now,
|
|
8274
|
+
planDir: null
|
|
7367
8275
|
};
|
|
7368
8276
|
checklist.items.push(newItem);
|
|
7369
8277
|
await writeChecklist(todosDir2, checklist);
|
|
@@ -7418,7 +8326,7 @@ function createTodosRouter(todosDir2, broadcast) {
|
|
|
7418
8326
|
router.post("/:workspace/archive", async (req, res) => {
|
|
7419
8327
|
try {
|
|
7420
8328
|
const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
|
|
7421
|
-
const { resolve:
|
|
8329
|
+
const { resolve: resolve20 } = await import("path");
|
|
7422
8330
|
const { readFile: readFile15 } = await import("fs/promises");
|
|
7423
8331
|
const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
7424
8332
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
@@ -7435,7 +8343,7 @@ function createTodosRouter(todosDir2, broadcast) {
|
|
|
7435
8343
|
(e) => e.itemIds.every((id) => completedIds.has(id))
|
|
7436
8344
|
);
|
|
7437
8345
|
const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
|
|
7438
|
-
await ensureDir(
|
|
8346
|
+
await ensureDir(resolve20(todosDir2, "archive"));
|
|
7439
8347
|
let archContent = "";
|
|
7440
8348
|
if (await fileExists(archFile)) {
|
|
7441
8349
|
archContent = await readFile15(archFile, "utf-8");
|
|
@@ -7513,6 +8421,7 @@ workspace: ${workspace}
|
|
|
7513
8421
|
if (!item) return null;
|
|
7514
8422
|
if (req.body.description !== void 0) item.description = req.body.description;
|
|
7515
8423
|
if (Array.isArray(req.body.tags)) item.tags = req.body.tags;
|
|
8424
|
+
touchItem(item);
|
|
7516
8425
|
await writeChecklist(todosDir2, checklist);
|
|
7517
8426
|
return { ...item };
|
|
7518
8427
|
});
|
|
@@ -7557,6 +8466,9 @@ workspace: ${workspace}
|
|
|
7557
8466
|
if (item.status === "in_progress") return { error: "conflict", session: item.session };
|
|
7558
8467
|
item.status = "in_progress";
|
|
7559
8468
|
item.session = req.body.session || null;
|
|
8469
|
+
if (req.body.branch) item.branch = req.body.branch;
|
|
8470
|
+
if (req.body.worktreePath) item.worktreePath = req.body.worktreePath;
|
|
8471
|
+
touchItem(item);
|
|
7560
8472
|
await writeChecklist(todosDir2, checklist);
|
|
7561
8473
|
return { item: { ...item } };
|
|
7562
8474
|
});
|
|
@@ -7583,13 +8495,15 @@ workspace: ${workspace}
|
|
|
7583
8495
|
if (!item) return null;
|
|
7584
8496
|
item.status = "completed";
|
|
7585
8497
|
item.session = null;
|
|
8498
|
+
const branchForLog = req.body.branch || item.branch || null;
|
|
8499
|
+
touchItem(item);
|
|
7586
8500
|
await writeChecklist(todosDir2, checklist);
|
|
7587
8501
|
const entry = {
|
|
7588
8502
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7589
8503
|
itemIds: [item.id],
|
|
7590
8504
|
items: item.description,
|
|
7591
8505
|
session: req.body.session || null,
|
|
7592
|
-
branch:
|
|
8506
|
+
branch: branchForLog,
|
|
7593
8507
|
summary: req.body.summary || "Completed.",
|
|
7594
8508
|
blockers: null,
|
|
7595
8509
|
status: null
|
|
@@ -7617,6 +8531,7 @@ workspace: ${workspace}
|
|
|
7617
8531
|
if (!item) return null;
|
|
7618
8532
|
item.status = "blocked";
|
|
7619
8533
|
item.session = null;
|
|
8534
|
+
touchItem(item);
|
|
7620
8535
|
await writeChecklist(todosDir2, checklist);
|
|
7621
8536
|
const entry = {
|
|
7622
8537
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -7650,6 +8565,7 @@ workspace: ${workspace}
|
|
|
7650
8565
|
if (!item) return null;
|
|
7651
8566
|
item.status = "open";
|
|
7652
8567
|
item.session = null;
|
|
8568
|
+
touchItem(item);
|
|
7653
8569
|
await writeChecklist(todosDir2, checklist);
|
|
7654
8570
|
return { ...item };
|
|
7655
8571
|
});
|
|
@@ -7663,6 +8579,113 @@ workspace: ${workspace}
|
|
|
7663
8579
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to reopen todo" });
|
|
7664
8580
|
}
|
|
7665
8581
|
});
|
|
8582
|
+
router.post("/:workspace/promote", async (req, res) => {
|
|
8583
|
+
try {
|
|
8584
|
+
const workspace = getWorkspaceParam(req.params.workspace);
|
|
8585
|
+
const { todoIds, mode, target, title, type, priority, keepSource } = req.body ?? {};
|
|
8586
|
+
if (!Array.isArray(todoIds) || todoIds.length === 0) {
|
|
8587
|
+
res.status(400).json({ error: "todoIds (non-empty array of strings) is required" });
|
|
8588
|
+
return;
|
|
8589
|
+
}
|
|
8590
|
+
if (mode !== "new-assignment" && mode !== "to-assignment") {
|
|
8591
|
+
res.status(400).json({ error: 'mode must be "new-assignment" or "to-assignment"' });
|
|
8592
|
+
return;
|
|
8593
|
+
}
|
|
8594
|
+
const result = await wsLock(workspace, async () => {
|
|
8595
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
8596
|
+
const items = [];
|
|
8597
|
+
for (const id of todoIds) {
|
|
8598
|
+
const item = checklist.items.find((i) => i.id === id);
|
|
8599
|
+
if (!item) return { error: `Todo "${id}" not found` };
|
|
8600
|
+
if (item.status === "completed") return { error: `Todo "${id}" is already completed` };
|
|
8601
|
+
items.push(item);
|
|
8602
|
+
}
|
|
8603
|
+
const scopeLabel = workspace === "_global" ? "_global" : `workspace:${workspace}`;
|
|
8604
|
+
const { resolve: resolvePath } = await import("path");
|
|
8605
|
+
const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
|
|
8606
|
+
const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
|
|
8607
|
+
const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
8608
|
+
const { readFile: readFile15 } = await import("fs/promises");
|
|
8609
|
+
const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
|
|
8610
|
+
const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
|
|
8611
|
+
let assignmentRef;
|
|
8612
|
+
let assignmentDir;
|
|
8613
|
+
if (mode === "new-assignment") {
|
|
8614
|
+
const targetProject = target?.project;
|
|
8615
|
+
if (!targetProject) return { error: "target.project is required for new-assignment mode" };
|
|
8616
|
+
if (items.length > 1 && !title) return { error: "title is required when promoting multiple todos" };
|
|
8617
|
+
const { createAssignmentCommand: createAssignmentCommand2 } = await Promise.resolve().then(() => (init_create_assignment(), create_assignment_exports));
|
|
8618
|
+
const created = await createAssignmentCommand2(title || items[0].description, {
|
|
8619
|
+
project: targetProject,
|
|
8620
|
+
type,
|
|
8621
|
+
priority,
|
|
8622
|
+
withTodos: true,
|
|
8623
|
+
silent: true
|
|
8624
|
+
});
|
|
8625
|
+
assignmentDir = created.assignmentDir;
|
|
8626
|
+
assignmentRef = `${created.projectSlug}/${created.slug}`;
|
|
8627
|
+
} else {
|
|
8628
|
+
const tg = target?.assignment || "";
|
|
8629
|
+
if (!tg) return { error: "target.assignment is required for to-assignment mode" };
|
|
8630
|
+
if (tg.includes("/")) {
|
|
8631
|
+
const parts = tg.split("/");
|
|
8632
|
+
if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
|
|
8633
|
+
const config = await readConfig2();
|
|
8634
|
+
assignmentDir = resolvePath(config.defaultProjectDir, parts[0], "assignments", parts[1]);
|
|
8635
|
+
assignmentRef = `${parts[0]}/${parts[1]}`;
|
|
8636
|
+
} else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tg)) {
|
|
8637
|
+
assignmentDir = resolvePath(assignmentsDirFn(), tg);
|
|
8638
|
+
assignmentRef = tg;
|
|
8639
|
+
} else {
|
|
8640
|
+
return { error: `Invalid target.assignment "${tg}"` };
|
|
8641
|
+
}
|
|
8642
|
+
const assignmentMdPath2 = resolvePath(assignmentDir, "assignment.md");
|
|
8643
|
+
if (!await fileExists2(assignmentMdPath2)) return { error: `Target assignment not found: ${assignmentMdPath2}` };
|
|
8644
|
+
}
|
|
8645
|
+
const assignmentMdPath = resolvePath(assignmentDir, "assignment.md");
|
|
8646
|
+
let content = await readFile15(assignmentMdPath, "utf-8");
|
|
8647
|
+
content = appendTodosToAssignmentBody2(
|
|
8648
|
+
content,
|
|
8649
|
+
items.map((it) => ({
|
|
8650
|
+
description: it.description,
|
|
8651
|
+
trace: `promoted from t:${it.id} in ${scopeLabel}`
|
|
8652
|
+
}))
|
|
8653
|
+
);
|
|
8654
|
+
content = touchAssignmentUpdated2(content, nowTimestamp3());
|
|
8655
|
+
await writeFileForce2(assignmentMdPath, content);
|
|
8656
|
+
if (!keepSource) {
|
|
8657
|
+
for (const item of items) {
|
|
8658
|
+
item.status = "completed";
|
|
8659
|
+
item.session = null;
|
|
8660
|
+
touchItem(item);
|
|
8661
|
+
}
|
|
8662
|
+
await writeChecklist(todosDir2, checklist);
|
|
8663
|
+
for (const item of items) {
|
|
8664
|
+
const entry = {
|
|
8665
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8666
|
+
itemIds: [item.id],
|
|
8667
|
+
items: item.description,
|
|
8668
|
+
session: null,
|
|
8669
|
+
branch: item.branch || null,
|
|
8670
|
+
summary: `Promoted to assignment ${assignmentRef}`,
|
|
8671
|
+
blockers: null,
|
|
8672
|
+
status: null
|
|
8673
|
+
};
|
|
8674
|
+
await appendLogEntry2(todosDir2, workspace, entry);
|
|
8675
|
+
}
|
|
8676
|
+
}
|
|
8677
|
+
return { assignmentRef, assignmentDir, promoted: items.map((i) => i.id) };
|
|
8678
|
+
});
|
|
8679
|
+
if ("error" in result) {
|
|
8680
|
+
res.status(400).json({ error: result.error });
|
|
8681
|
+
return;
|
|
8682
|
+
}
|
|
8683
|
+
broadcastUpdate();
|
|
8684
|
+
res.json(result);
|
|
8685
|
+
} catch (error) {
|
|
8686
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to promote todos" });
|
|
8687
|
+
}
|
|
8688
|
+
});
|
|
7666
8689
|
router.post("/:workspace/:id/unblock", async (req, res) => {
|
|
7667
8690
|
try {
|
|
7668
8691
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
@@ -7672,6 +8695,7 @@ workspace: ${workspace}
|
|
|
7672
8695
|
if (!item) return null;
|
|
7673
8696
|
item.status = "open";
|
|
7674
8697
|
item.session = null;
|
|
8698
|
+
touchItem(item);
|
|
7675
8699
|
await writeChecklist(todosDir2, checklist);
|
|
7676
8700
|
return { ...item };
|
|
7677
8701
|
});
|
|
@@ -7692,9 +8716,10 @@ workspace: ${workspace}
|
|
|
7692
8716
|
init_parser2();
|
|
7693
8717
|
init_fs();
|
|
7694
8718
|
init_paths();
|
|
8719
|
+
init_slug();
|
|
7695
8720
|
import { Router as Router6 } from "express";
|
|
7696
8721
|
import { mkdir as mkdir2, readFile as readFile13 } from "fs/promises";
|
|
7697
|
-
import { resolve as
|
|
8722
|
+
import { resolve as resolve17 } from "path";
|
|
7698
8723
|
var writeLocks2 = /* @__PURE__ */ new Map();
|
|
7699
8724
|
function projLock(slug, fn) {
|
|
7700
8725
|
const key = `proj:${slug}`;
|
|
@@ -7705,6 +8730,11 @@ function projLock(slug, fn) {
|
|
|
7705
8730
|
}));
|
|
7706
8731
|
return next;
|
|
7707
8732
|
}
|
|
8733
|
+
function touchItem2(item) {
|
|
8734
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8735
|
+
if (item.createdAt === null) item.createdAt = now;
|
|
8736
|
+
item.updatedAt = now;
|
|
8737
|
+
}
|
|
7708
8738
|
function getProjectIdParam(value) {
|
|
7709
8739
|
if (Array.isArray(value)) return value[0] ?? "";
|
|
7710
8740
|
return value ?? "";
|
|
@@ -7713,7 +8743,7 @@ function params(req) {
|
|
|
7713
8743
|
return req.params;
|
|
7714
8744
|
}
|
|
7715
8745
|
async function projectExists(projectsDir, slug) {
|
|
7716
|
-
return fileExists(
|
|
8746
|
+
return fileExists(resolve17(projectsDir, slug, "project.md"));
|
|
7717
8747
|
}
|
|
7718
8748
|
async function ensureProjectTodosDir(projectsDir, slug) {
|
|
7719
8749
|
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
@@ -7730,7 +8760,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
|
|
|
7730
8760
|
throw err;
|
|
7731
8761
|
}
|
|
7732
8762
|
try {
|
|
7733
|
-
await mkdir2(
|
|
8763
|
+
await mkdir2(resolve17(todosDir2, "archive"), { recursive: false });
|
|
7734
8764
|
} catch (err) {
|
|
7735
8765
|
const code = err.code;
|
|
7736
8766
|
if (code === "EEXIST") return;
|
|
@@ -7797,12 +8827,18 @@ function createProjectTodosRouter(projectsDir, broadcast) {
|
|
|
7797
8827
|
const checklist = await readChecklist(todosDir2, slug);
|
|
7798
8828
|
const existingIds = new Set(checklist.items.map((i) => i.id));
|
|
7799
8829
|
const id = generateUniqueId(existingIds);
|
|
8830
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7800
8831
|
const newItem = {
|
|
7801
8832
|
id,
|
|
7802
8833
|
description,
|
|
7803
8834
|
status: "open",
|
|
7804
8835
|
tags: Array.isArray(tags) ? tags : [],
|
|
7805
|
-
session: null
|
|
8836
|
+
session: null,
|
|
8837
|
+
branch: null,
|
|
8838
|
+
worktreePath: null,
|
|
8839
|
+
createdAt: now,
|
|
8840
|
+
updatedAt: now,
|
|
8841
|
+
planDir: null
|
|
7806
8842
|
};
|
|
7807
8843
|
checklist.workspace = slug;
|
|
7808
8844
|
checklist.items.push(newItem);
|
|
@@ -8005,6 +9041,7 @@ workspace: ${slug}
|
|
|
8005
9041
|
if (!item) return null;
|
|
8006
9042
|
if (req.body.description !== void 0) item.description = req.body.description;
|
|
8007
9043
|
if (Array.isArray(req.body.tags)) item.tags = req.body.tags;
|
|
9044
|
+
touchItem2(item);
|
|
8008
9045
|
checklist.workspace = slug;
|
|
8009
9046
|
await writeChecklist(todosDir2, checklist);
|
|
8010
9047
|
return { ...item };
|
|
@@ -8081,6 +9118,9 @@ workspace: ${slug}
|
|
|
8081
9118
|
if (item.status === "in_progress") return { error: "conflict", session: item.session };
|
|
8082
9119
|
item.status = "in_progress";
|
|
8083
9120
|
item.session = req.body.session || null;
|
|
9121
|
+
if (req.body.branch) item.branch = req.body.branch;
|
|
9122
|
+
if (req.body.worktreePath) item.worktreePath = req.body.worktreePath;
|
|
9123
|
+
touchItem2(item);
|
|
8084
9124
|
checklist.workspace = slug;
|
|
8085
9125
|
await writeChecklist(todosDir2, checklist);
|
|
8086
9126
|
return { item: { ...item } };
|
|
@@ -8123,6 +9163,8 @@ workspace: ${slug}
|
|
|
8123
9163
|
if (!item) return null;
|
|
8124
9164
|
item.status = "completed";
|
|
8125
9165
|
item.session = null;
|
|
9166
|
+
const branchForLog = req.body.branch || item.branch || null;
|
|
9167
|
+
touchItem2(item);
|
|
8126
9168
|
checklist.workspace = slug;
|
|
8127
9169
|
await writeChecklist(todosDir2, checklist);
|
|
8128
9170
|
const entry = {
|
|
@@ -8130,7 +9172,7 @@ workspace: ${slug}
|
|
|
8130
9172
|
itemIds: [item.id],
|
|
8131
9173
|
items: item.description,
|
|
8132
9174
|
session: req.body.session || null,
|
|
8133
|
-
branch:
|
|
9175
|
+
branch: branchForLog,
|
|
8134
9176
|
summary: req.body.summary || "Completed.",
|
|
8135
9177
|
blockers: null,
|
|
8136
9178
|
status: null
|
|
@@ -8173,6 +9215,7 @@ workspace: ${slug}
|
|
|
8173
9215
|
if (!item) return null;
|
|
8174
9216
|
item.status = "blocked";
|
|
8175
9217
|
item.session = null;
|
|
9218
|
+
touchItem2(item);
|
|
8176
9219
|
checklist.workspace = slug;
|
|
8177
9220
|
await writeChecklist(todosDir2, checklist);
|
|
8178
9221
|
const entry = {
|
|
@@ -8222,6 +9265,7 @@ workspace: ${slug}
|
|
|
8222
9265
|
if (!item) return null;
|
|
8223
9266
|
item.status = "open";
|
|
8224
9267
|
item.session = null;
|
|
9268
|
+
touchItem2(item);
|
|
8225
9269
|
checklist.workspace = slug;
|
|
8226
9270
|
await writeChecklist(todosDir2, checklist);
|
|
8227
9271
|
return { ...item };
|
|
@@ -8260,6 +9304,7 @@ workspace: ${slug}
|
|
|
8260
9304
|
if (!item) return null;
|
|
8261
9305
|
item.status = "open";
|
|
8262
9306
|
item.session = null;
|
|
9307
|
+
touchItem2(item);
|
|
8263
9308
|
checklist.workspace = slug;
|
|
8264
9309
|
await writeChecklist(todosDir2, checklist);
|
|
8265
9310
|
return { ...item };
|
|
@@ -8296,7 +9341,7 @@ init_config2();
|
|
|
8296
9341
|
import { execFile as execFile2 } from "child_process";
|
|
8297
9342
|
import { promisify as promisify2 } from "util";
|
|
8298
9343
|
import { cp, mkdtemp, rm as rm2, readFile as readFile14, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename3 } from "fs/promises";
|
|
8299
|
-
import { resolve as
|
|
9344
|
+
import { resolve as resolve18, join as join2 } from "path";
|
|
8300
9345
|
import { tmpdir } from "os";
|
|
8301
9346
|
var exec2 = promisify2(execFile2);
|
|
8302
9347
|
var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
|
|
@@ -8336,7 +9381,7 @@ async function resolveCategoryPath(category) {
|
|
|
8336
9381
|
case "servers":
|
|
8337
9382
|
return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
|
|
8338
9383
|
case "config":
|
|
8339
|
-
return { sourcePath:
|
|
9384
|
+
return { sourcePath: resolve18(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
|
|
8340
9385
|
}
|
|
8341
9386
|
}
|
|
8342
9387
|
async function checkGitInstalled() {
|
|
@@ -8347,7 +9392,7 @@ async function checkGitInstalled() {
|
|
|
8347
9392
|
}
|
|
8348
9393
|
}
|
|
8349
9394
|
async function acquireLock() {
|
|
8350
|
-
const lockPath =
|
|
9395
|
+
const lockPath = resolve18(syntaurRoot(), LOCK_FILE_NAME);
|
|
8351
9396
|
await ensureDir(syntaurRoot());
|
|
8352
9397
|
try {
|
|
8353
9398
|
const handle = await open(lockPath, "wx");
|
|
@@ -8394,7 +9439,7 @@ async function copyRecursive(src, dest) {
|
|
|
8394
9439
|
await ensureDir(dest);
|
|
8395
9440
|
await cp(src, dest, { recursive: true, force: true });
|
|
8396
9441
|
} else {
|
|
8397
|
-
await ensureDir(
|
|
9442
|
+
await ensureDir(resolve18(dest, ".."));
|
|
8398
9443
|
await cp(src, dest, { force: true });
|
|
8399
9444
|
}
|
|
8400
9445
|
}
|
|
@@ -8442,7 +9487,7 @@ async function backupToGithub(overrides) {
|
|
|
8442
9487
|
}
|
|
8443
9488
|
if (category === "config") {
|
|
8444
9489
|
const sanitized = await readSanitizedConfig(sourcePath);
|
|
8445
|
-
await ensureDir(
|
|
9490
|
+
await ensureDir(resolve18(destPath, ".."));
|
|
8446
9491
|
await writeFile4(destPath, sanitized, "utf-8");
|
|
8447
9492
|
} else {
|
|
8448
9493
|
await copyRecursive(sourcePath, destPath);
|
|
@@ -8496,7 +9541,7 @@ async function backupToGithub(overrides) {
|
|
|
8496
9541
|
}
|
|
8497
9542
|
async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
|
|
8498
9543
|
if (isFile) {
|
|
8499
|
-
await ensureDir(
|
|
9544
|
+
await ensureDir(resolve18(localPath, ".."));
|
|
8500
9545
|
await cp(repoSrcPath, localPath, { force: true });
|
|
8501
9546
|
return;
|
|
8502
9547
|
}
|
|
@@ -8597,7 +9642,7 @@ async function restoreFromGithub(overrides) {
|
|
|
8597
9642
|
}
|
|
8598
9643
|
async function getBackupStatus() {
|
|
8599
9644
|
const config = await readConfig();
|
|
8600
|
-
const lockPath =
|
|
9645
|
+
const lockPath = resolve18(syntaurRoot(), LOCK_FILE_NAME);
|
|
8601
9646
|
const locked = await fileExists(lockPath);
|
|
8602
9647
|
return {
|
|
8603
9648
|
repo: config.backup?.repo ?? null,
|
|
@@ -8763,10 +9808,10 @@ async function listAllTmuxSessions() {
|
|
|
8763
9808
|
if (!output) return [];
|
|
8764
9809
|
return output.split("\n").filter((line) => line.length > 0);
|
|
8765
9810
|
}
|
|
8766
|
-
async function discoverTmuxSessions(serversDir2, projectsDir, existingNames,
|
|
9811
|
+
async function discoverTmuxSessions(serversDir2, projectsDir, existingNames, assignmentsDir2) {
|
|
8767
9812
|
const tmuxAvailable = await checkTmuxAvailable();
|
|
8768
9813
|
if (!tmuxAvailable) return false;
|
|
8769
|
-
const workspaceRecords = await loadWorkspaceRecords(projectsDir,
|
|
9814
|
+
const workspaceRecords = await loadWorkspaceRecords(projectsDir, assignmentsDir2);
|
|
8770
9815
|
if (workspaceRecords.length === 0) return false;
|
|
8771
9816
|
const sessions = await listAllTmuxSessions();
|
|
8772
9817
|
let changed = false;
|
|
@@ -8807,8 +9852,8 @@ async function getProcessCwd(pid) {
|
|
|
8807
9852
|
}
|
|
8808
9853
|
return null;
|
|
8809
9854
|
}
|
|
8810
|
-
async function discoverProcesses(serversDir2, projectsDir, existingFiles, excludePids,
|
|
8811
|
-
const workspaceRecords = await loadWorkspaceRecords(projectsDir,
|
|
9855
|
+
async function discoverProcesses(serversDir2, projectsDir, existingFiles, excludePids, assignmentsDir2) {
|
|
9856
|
+
const workspaceRecords = await loadWorkspaceRecords(projectsDir, assignmentsDir2);
|
|
8812
9857
|
if (workspaceRecords.length === 0) return false;
|
|
8813
9858
|
const lsofOutput = await getLsofOutput();
|
|
8814
9859
|
if (!lsofOutput) return false;
|
|
@@ -8873,7 +9918,7 @@ async function isProcessAlive(pid) {
|
|
|
8873
9918
|
return false;
|
|
8874
9919
|
}
|
|
8875
9920
|
}
|
|
8876
|
-
async function reconcile(serversDir2, projectsDir, excludePids,
|
|
9921
|
+
async function reconcile(serversDir2, projectsDir, excludePids, assignmentsDir2) {
|
|
8877
9922
|
const names = await listSessionFiles(serversDir2);
|
|
8878
9923
|
const existingFiles = /* @__PURE__ */ new Map();
|
|
8879
9924
|
for (const name of names) {
|
|
@@ -8885,8 +9930,8 @@ async function reconcile(serversDir2, projectsDir, excludePids, assignmentsDir)
|
|
|
8885
9930
|
existingFiles.delete(name);
|
|
8886
9931
|
}
|
|
8887
9932
|
const existingNames = new Set(existingFiles.keys());
|
|
8888
|
-
const tmuxChanged = await discoverTmuxSessions(serversDir2, projectsDir, existingNames,
|
|
8889
|
-
const processChanged = await discoverProcesses(serversDir2, projectsDir, existingFiles, excludePids,
|
|
9933
|
+
const tmuxChanged = await discoverTmuxSessions(serversDir2, projectsDir, existingNames, assignmentsDir2);
|
|
9934
|
+
const processChanged = await discoverProcesses(serversDir2, projectsDir, existingFiles, excludePids, assignmentsDir2);
|
|
8890
9935
|
if (tmuxChanged || processChanged || cleanupChanged) {
|
|
8891
9936
|
clearScanCache();
|
|
8892
9937
|
}
|
|
@@ -8894,7 +9939,7 @@ async function reconcile(serversDir2, projectsDir, excludePids, assignmentsDir)
|
|
|
8894
9939
|
|
|
8895
9940
|
// src/dashboard/server.ts
|
|
8896
9941
|
function createDashboardServer(options) {
|
|
8897
|
-
const { port, projectsDir, assignmentsDir, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, serveStaticUi, dashboardDistPath } = options;
|
|
9942
|
+
const { port, projectsDir, assignmentsDir: assignmentsDir2, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, serveStaticUi, dashboardDistPath } = options;
|
|
8898
9943
|
const app = express();
|
|
8899
9944
|
const server = createServer(app);
|
|
8900
9945
|
const wss = new WebSocketServer({ noServer: true });
|
|
@@ -8934,7 +9979,7 @@ function createDashboardServer(options) {
|
|
|
8934
9979
|
(async () => {
|
|
8935
9980
|
try {
|
|
8936
9981
|
const configResult = await migrateLegacyConfig(
|
|
8937
|
-
|
|
9982
|
+
resolve19(syntaurRoot(), "config.md")
|
|
8938
9983
|
);
|
|
8939
9984
|
const projectResult = await migrateLegacyProjectFiles(projectsDir);
|
|
8940
9985
|
const summary = summarizeMigration(projectResult, configResult);
|
|
@@ -8946,7 +9991,7 @@ function createDashboardServer(options) {
|
|
|
8946
9991
|
app.use(express.json());
|
|
8947
9992
|
app.get("/api/overview", async (_req, res) => {
|
|
8948
9993
|
try {
|
|
8949
|
-
const overview = await getOverview(projectsDir, serversDir2,
|
|
9994
|
+
const overview = await getOverview(projectsDir, serversDir2, assignmentsDir2);
|
|
8950
9995
|
res.json(overview);
|
|
8951
9996
|
} catch (error) {
|
|
8952
9997
|
console.error("Error getting overview:", error);
|
|
@@ -8955,7 +10000,7 @@ function createDashboardServer(options) {
|
|
|
8955
10000
|
});
|
|
8956
10001
|
app.get("/api/attention", async (_req, res) => {
|
|
8957
10002
|
try {
|
|
8958
|
-
const attention = await getAttention(projectsDir, serversDir2,
|
|
10003
|
+
const attention = await getAttention(projectsDir, serversDir2, assignmentsDir2);
|
|
8959
10004
|
res.json(attention);
|
|
8960
10005
|
} catch (error) {
|
|
8961
10006
|
console.error("Error getting attention queue:", error);
|
|
@@ -9059,6 +10104,78 @@ function createDashboardServer(options) {
|
|
|
9059
10104
|
res.status(500).json({ error: "Failed to reset theme config" });
|
|
9060
10105
|
}
|
|
9061
10106
|
});
|
|
10107
|
+
app.get("/api/config/hotkeys", async (_req, res) => {
|
|
10108
|
+
try {
|
|
10109
|
+
const config = await readConfig();
|
|
10110
|
+
const bindings = config.hotkeys?.bindings ?? {};
|
|
10111
|
+
res.json({ bindings, custom: config.hotkeys !== null });
|
|
10112
|
+
} catch (error) {
|
|
10113
|
+
console.error("Error getting hotkeys config:", error);
|
|
10114
|
+
res.status(500).json({ error: "Failed to get hotkeys config" });
|
|
10115
|
+
}
|
|
10116
|
+
});
|
|
10117
|
+
app.put("/api/config/hotkeys", async (req, res) => {
|
|
10118
|
+
try {
|
|
10119
|
+
const raw = req.body && typeof req.body === "object" ? req.body : {};
|
|
10120
|
+
const incoming = raw.bindings;
|
|
10121
|
+
if (!incoming || typeof incoming !== "object" || Array.isArray(incoming)) {
|
|
10122
|
+
res.status(400).json({ error: "bindings must be an object keyed by action kind" });
|
|
10123
|
+
return;
|
|
10124
|
+
}
|
|
10125
|
+
const cleaned = {};
|
|
10126
|
+
for (const [rawKind, rawValue] of Object.entries(incoming)) {
|
|
10127
|
+
if (!isBindableActionKind(rawKind)) {
|
|
10128
|
+
res.status(400).json({
|
|
10129
|
+
error: `unknown action kind "${rawKind}" \u2014 expected one of: ${BINDABLE_ACTION_KINDS.join(", ")}`
|
|
10130
|
+
});
|
|
10131
|
+
return;
|
|
10132
|
+
}
|
|
10133
|
+
if (typeof rawValue !== "string" || rawValue.trim() === "") {
|
|
10134
|
+
res.status(400).json({ error: `binding for "${rawKind}" must be a non-empty string` });
|
|
10135
|
+
return;
|
|
10136
|
+
}
|
|
10137
|
+
const canonical = canonicalizeCombo(rawValue);
|
|
10138
|
+
if (!canonical) {
|
|
10139
|
+
res.status(400).json({ error: `binding for "${rawKind}" is not a valid combo` });
|
|
10140
|
+
return;
|
|
10141
|
+
}
|
|
10142
|
+
if (isReservedCombo(canonical)) {
|
|
10143
|
+
res.status(400).json({
|
|
10144
|
+
error: `combo "${canonical}" is reserved by a built-in shortcut`,
|
|
10145
|
+
kind: rawKind,
|
|
10146
|
+
combo: canonical
|
|
10147
|
+
});
|
|
10148
|
+
return;
|
|
10149
|
+
}
|
|
10150
|
+
cleaned[rawKind] = canonical;
|
|
10151
|
+
}
|
|
10152
|
+
const seenCombos = /* @__PURE__ */ new Map();
|
|
10153
|
+
for (const [kind, combo] of Object.entries(cleaned)) {
|
|
10154
|
+
if (seenCombos.has(combo)) {
|
|
10155
|
+
res.status(400).json({
|
|
10156
|
+
error: `combo "${combo}" is bound to multiple actions`,
|
|
10157
|
+
kinds: [seenCombos.get(combo), kind]
|
|
10158
|
+
});
|
|
10159
|
+
return;
|
|
10160
|
+
}
|
|
10161
|
+
seenCombos.set(combo, kind);
|
|
10162
|
+
}
|
|
10163
|
+
await writeHotkeyBindingsConfig({ bindings: cleaned });
|
|
10164
|
+
res.json({ bindings: cleaned, custom: Object.keys(cleaned).length > 0 });
|
|
10165
|
+
} catch (error) {
|
|
10166
|
+
console.error("Error saving hotkeys config:", error);
|
|
10167
|
+
res.status(500).json({ error: "Failed to save hotkeys config" });
|
|
10168
|
+
}
|
|
10169
|
+
});
|
|
10170
|
+
app.delete("/api/config/hotkeys", async (_req, res) => {
|
|
10171
|
+
try {
|
|
10172
|
+
await deleteHotkeyBindingsConfig();
|
|
10173
|
+
res.json({ bindings: {}, custom: false });
|
|
10174
|
+
} catch (error) {
|
|
10175
|
+
console.error("Error resetting hotkeys config:", error);
|
|
10176
|
+
res.status(500).json({ error: "Failed to reset hotkeys config" });
|
|
10177
|
+
}
|
|
10178
|
+
});
|
|
9062
10179
|
app.get("/api/projects", async (req, res) => {
|
|
9063
10180
|
try {
|
|
9064
10181
|
let projects = await listProjects(projectsDir);
|
|
@@ -9078,7 +10195,7 @@ function createDashboardServer(options) {
|
|
|
9078
10195
|
});
|
|
9079
10196
|
app.get("/api/workspaces", async (_req, res) => {
|
|
9080
10197
|
try {
|
|
9081
|
-
const result = await listWorkspaces(projectsDir,
|
|
10198
|
+
const result = await listWorkspaces(projectsDir, assignmentsDir2);
|
|
9082
10199
|
res.json(result);
|
|
9083
10200
|
} catch (error) {
|
|
9084
10201
|
console.error("Error listing workspaces:", error);
|
|
@@ -9112,7 +10229,7 @@ function createDashboardServer(options) {
|
|
|
9112
10229
|
});
|
|
9113
10230
|
app.get("/api/assignments", async (req, res) => {
|
|
9114
10231
|
try {
|
|
9115
|
-
const result = await listAssignmentsBoard(projectsDir,
|
|
10232
|
+
const result = await listAssignmentsBoard(projectsDir, assignmentsDir2);
|
|
9116
10233
|
const workspaceParam = req.query.workspace;
|
|
9117
10234
|
if (workspaceParam) {
|
|
9118
10235
|
if (workspaceParam === "_ungrouped") {
|
|
@@ -9142,7 +10259,7 @@ function createDashboardServer(options) {
|
|
|
9142
10259
|
});
|
|
9143
10260
|
app.get("/api/assignments/:id", async (req, res) => {
|
|
9144
10261
|
try {
|
|
9145
|
-
const detail = await getAssignmentDetailById(projectsDir,
|
|
10262
|
+
const detail = await getAssignmentDetailById(projectsDir, assignmentsDir2, req.params.id);
|
|
9146
10263
|
if (!detail) {
|
|
9147
10264
|
res.status(404).json({ error: `Assignment "${req.params.id}" not found` });
|
|
9148
10265
|
return;
|
|
@@ -9155,12 +10272,12 @@ function createDashboardServer(options) {
|
|
|
9155
10272
|
});
|
|
9156
10273
|
app.get("/api/assignments/:id/sessions", async (req, res) => {
|
|
9157
10274
|
try {
|
|
9158
|
-
const resolved = await resolveAssignmentById(projectsDir,
|
|
10275
|
+
const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, req.params.id);
|
|
9159
10276
|
if (!resolved) {
|
|
9160
10277
|
res.status(404).json({ error: `Assignment "${req.params.id}" not found` });
|
|
9161
10278
|
return;
|
|
9162
10279
|
}
|
|
9163
|
-
await reconcileActiveSessions(projectsDir,
|
|
10280
|
+
await reconcileActiveSessions(projectsDir, assignmentsDir2);
|
|
9164
10281
|
const sessions = await listSessionsByAssignment(
|
|
9165
10282
|
resolved.standalone ? null : resolved.projectSlug,
|
|
9166
10283
|
resolved.standalone ? resolved.id : resolved.assignmentSlug
|
|
@@ -9190,23 +10307,23 @@ function createDashboardServer(options) {
|
|
|
9190
10307
|
res.status(500).json({ error: "Failed to get assignment detail" });
|
|
9191
10308
|
}
|
|
9192
10309
|
});
|
|
9193
|
-
app.use(createWriteRouter(projectsDir,
|
|
9194
|
-
app.use("/api/servers", createServersRouter(serversDir2, projectsDir,
|
|
9195
|
-
app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir, broadcast,
|
|
10310
|
+
app.use(createWriteRouter(projectsDir, assignmentsDir2));
|
|
10311
|
+
app.use("/api/servers", createServersRouter(serversDir2, projectsDir, assignmentsDir2));
|
|
10312
|
+
app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2));
|
|
9196
10313
|
app.use("/api/playbooks", createPlaybooksRouter(playbooksDir2));
|
|
9197
10314
|
app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
|
|
9198
10315
|
app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir, broadcast));
|
|
9199
10316
|
app.use("/api/backup", createBackupRouter());
|
|
9200
10317
|
if (serveStaticUi && dashboardDistPath) {
|
|
9201
10318
|
const sendOpts = { dotfiles: "allow" };
|
|
9202
|
-
app.use("/assets", express.static(
|
|
10319
|
+
app.use("/assets", express.static(resolve19(dashboardDistPath, "assets"), sendOpts));
|
|
9203
10320
|
app.use(express.static(dashboardDistPath, { ...sendOpts, index: false, fallthrough: true }));
|
|
9204
10321
|
app.get("{*path}", async (req, res) => {
|
|
9205
10322
|
if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
|
|
9206
10323
|
res.status(404).json({ error: "Not Found" });
|
|
9207
10324
|
return;
|
|
9208
10325
|
}
|
|
9209
|
-
const indexPath =
|
|
10326
|
+
const indexPath = resolve19(dashboardDistPath, "index.html");
|
|
9210
10327
|
if (!await fileExists(indexPath)) {
|
|
9211
10328
|
res.status(503).send(
|
|
9212
10329
|
'Dashboard not built. Run "npm run build:dashboard" first.'
|
|
@@ -9226,13 +10343,13 @@ function createDashboardServer(options) {
|
|
|
9226
10343
|
async start() {
|
|
9227
10344
|
watcherHandle = createWatcher({
|
|
9228
10345
|
projectsDir,
|
|
9229
|
-
assignmentsDir,
|
|
10346
|
+
assignmentsDir: assignmentsDir2,
|
|
9230
10347
|
serversDir: serversDir2,
|
|
9231
10348
|
playbooksDir: playbooksDir2,
|
|
9232
10349
|
todosDir: todosDir2,
|
|
9233
10350
|
onMessage: broadcast
|
|
9234
10351
|
});
|
|
9235
|
-
startAutodiscovery({ serversDir: serversDir2, projectsDir, assignmentsDir, excludePids: /* @__PURE__ */ new Set([process.pid]) });
|
|
10352
|
+
startAutodiscovery({ serversDir: serversDir2, projectsDir, assignmentsDir: assignmentsDir2, excludePids: /* @__PURE__ */ new Set([process.pid]) });
|
|
9236
10353
|
return new Promise((resolvePromise, reject) => {
|
|
9237
10354
|
server.on("error", (err) => {
|
|
9238
10355
|
if (err.code === "EADDRINUSE") {
|
|
@@ -9244,7 +10361,7 @@ function createDashboardServer(options) {
|
|
|
9244
10361
|
}
|
|
9245
10362
|
});
|
|
9246
10363
|
server.listen(port, () => {
|
|
9247
|
-
const portFile =
|
|
10364
|
+
const portFile = resolve19(syntaurRoot(), "dashboard-port");
|
|
9248
10365
|
writeFile5(portFile, String(port), "utf-8").catch(() => {
|
|
9249
10366
|
});
|
|
9250
10367
|
resolvePromise();
|
|
@@ -9261,7 +10378,7 @@ function createDashboardServer(options) {
|
|
|
9261
10378
|
client.terminate();
|
|
9262
10379
|
}
|
|
9263
10380
|
clients.clear();
|
|
9264
|
-
const portFile =
|
|
10381
|
+
const portFile = resolve19(syntaurRoot(), "dashboard-port");
|
|
9265
10382
|
await unlink4(portFile).catch(() => {
|
|
9266
10383
|
});
|
|
9267
10384
|
server.closeAllConnections?.();
|