syntaur 0.1.13 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dashboard/dist/assets/{_basePickBy-DXzhD14q.js → _basePickBy-CHKX1r7P.js} +1 -1
- package/dashboard/dist/assets/{_baseUniq-gxypqvP5.js → _baseUniq-CTxTc4MS.js} +1 -1
- package/dashboard/dist/assets/{arc-Ce7nYKSm.js → arc-BUo5zftd.js} +1 -1
- package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-zX4f4_Mf.js → architectureDiagram-2XIMDMQ5-CrJLm-P0.js} +1 -1
- package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-auOdy7nH.js → blockDiagram-WCTKOSBZ-BK60lBBJ.js} +1 -1
- package/dashboard/dist/assets/{c4Diagram-IC4MRINW-C2kkjPbW.js → c4Diagram-IC4MRINW-C7oJEvA0.js} +1 -1
- package/dashboard/dist/assets/channel-DdltvFFH.js +1 -0
- package/dashboard/dist/assets/{chunk-4BX2VUAB-B7dfpnbG.js → chunk-4BX2VUAB-CjUPlzHz.js} +1 -1
- package/dashboard/dist/assets/{chunk-55IACEB6-r1_jHZYp.js → chunk-55IACEB6-6HmWguiO.js} +1 -1
- package/dashboard/dist/assets/{chunk-FMBD7UC4-5mMONjMK.js → chunk-FMBD7UC4-CLuJnd1b.js} +1 -1
- package/dashboard/dist/assets/{chunk-JSJVCQXG-CwKj-Es4.js → chunk-JSJVCQXG-B4d62qWV.js} +1 -1
- package/dashboard/dist/assets/{chunk-KX2RTZJC-ByoW-HgN.js → chunk-KX2RTZJC-AsEKRPq2.js} +1 -1
- package/dashboard/dist/assets/{chunk-NQ4KR5QH-D1olOovd.js → chunk-NQ4KR5QH-DQhHHvwY.js} +1 -1
- package/dashboard/dist/assets/{chunk-QZHKN3VN-CB8_FC8w.js → chunk-QZHKN3VN-Ds1TtI3E.js} +1 -1
- package/dashboard/dist/assets/{chunk-WL4C6EOR-CFEqRrE1.js → chunk-WL4C6EOR-C7jE3-cR.js} +1 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-BHqdFE-8.js +1 -0
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BHqdFE-8.js +1 -0
- package/dashboard/dist/assets/clone-CBJOOeOm.js +1 -0
- package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-D6dGVXEI.js → cose-bilkent-S5V4N54A-C9ka5v1m.js} +1 -1
- package/dashboard/dist/assets/{dagre-KLK3FWXG-Cvg9CgP-.js → dagre-KLK3FWXG-BbgPQBKy.js} +1 -1
- package/dashboard/dist/assets/{diagram-E7M64L7V-iCBudhZD.js → diagram-E7M64L7V-DpdeZFD4.js} +1 -1
- package/dashboard/dist/assets/{diagram-IFDJBPK2-BGniy7VQ.js → diagram-IFDJBPK2-FlHLQzOV.js} +1 -1
- package/dashboard/dist/assets/{diagram-P4PSJMXO-B6Ie044E.js → diagram-P4PSJMXO-B22NkEF_.js} +1 -1
- package/dashboard/dist/assets/{erDiagram-INFDFZHY-BHvFRNhJ.js → erDiagram-INFDFZHY-zSqmtDid.js} +1 -1
- package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-CN86Zu3Q.js → flowDiagram-PKNHOUZH-BP_0XmVV.js} +1 -1
- package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-D-1fKFjW.js → ganttDiagram-A5KZAMGK-8uRyYgZV.js} +1 -1
- package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-Dtf1A6KL.js → gitGraphDiagram-K3NZZRJ6-JFqg8sv4.js} +1 -1
- package/dashboard/dist/assets/{graph-B6H_kXSs.js → graph-a-PAH599.js} +1 -1
- package/dashboard/dist/assets/index-CoVCLSh2.css +1 -0
- package/dashboard/dist/assets/index-yyAIuzrP.js +471 -0
- package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-R9wJj4JF.js → infoDiagram-LFFYTUFH-C3kq7Nbv.js} +1 -1
- package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-CJmeR-bX.js → ishikawaDiagram-PHBUUO56-Kqi4EZ-n.js} +1 -1
- package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-FcUhyu8I.js → journeyDiagram-4ABVD52K-CTfv0Wcr.js} +1 -1
- package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-C8UcTIwW.js → kanban-definition-K7BYSVSG-Dmx0lgvR.js} +1 -1
- package/dashboard/dist/assets/{layout-DzBy6alw.js → layout-KKRbT2Od.js} +1 -1
- package/dashboard/dist/assets/{linear-CZJCNOB9.js → linear-5egaBiw7.js} +1 -1
- package/dashboard/dist/assets/{mermaid.core-fMQRe9Gq.js → mermaid.core-C9pF_oFQ.js} +4 -4
- package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-BFwp-LS-.js → mindmap-definition-YRQLILUH-C7HXYEXt.js} +1 -1
- package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CQLmPkkd.js → pieDiagram-SKSYHLDU-DkdZm-YP.js} +1 -1
- package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-DAmi-dmD.js → quadrantDiagram-337W2JSQ-DkcRJs5F.js} +1 -1
- package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-Dcdts4kX.js → requirementDiagram-Z7DCOOCP-BaTDVYTl.js} +1 -1
- package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-By8LRvM0.js → sankeyDiagram-WA2Y5GQK-DvPLbGV5.js} +1 -1
- package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-BsvqgtTz.js → sequenceDiagram-2WXFIKYE-DQoZ2xMK.js} +1 -1
- package/dashboard/dist/assets/{stateDiagram-RAJIS63D-DFNOD7cx.js → stateDiagram-RAJIS63D-CS4l0OjM.js} +1 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DkBtE1WJ.js +1 -0
- package/dashboard/dist/assets/{timeline-definition-YZTLITO2-CMcgJGjn.js → timeline-definition-YZTLITO2-aC0iCFCW.js} +1 -1
- package/dashboard/dist/assets/{treemap-KZPCXAKY-BWsRNHwq.js → treemap-KZPCXAKY-Ie-PFjgx.js} +1 -1
- package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-io7-2Tod.js → vennDiagram-LZ73GAT5-CJN3ExTQ.js} +1 -1
- package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-AVnh4fDS.js → xychartDiagram-JWTSCODW-DSiDu1CN.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/dashboard/server.d.ts +1 -1
- package/dist/dashboard/server.js +1163 -734
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +3979 -1372
- package/dist/index.js.map +1 -1
- package/examples/playbooks/keep-records-updated.md +1 -1
- package/examples/playbooks/read-before-plan.md +5 -5
- package/examples/{sample-mission → sample-project}/_index-assignments.md +1 -1
- package/examples/{sample-mission → sample-project}/_index-decisions.md +1 -1
- package/examples/{sample-mission → sample-project}/_index-plans.md +1 -1
- package/examples/{sample-mission → sample-project}/_status.md +3 -3
- package/examples/{sample-mission → sample-project}/assignments/design-auth-schema/assignment.md +4 -1
- package/examples/{sample-mission → sample-project}/assignments/implement-jwt-middleware/assignment.md +4 -1
- package/examples/{sample-mission → sample-project}/assignments/write-auth-tests/assignment.md +4 -1
- package/examples/{sample-mission → sample-project}/manifest.md +3 -3
- package/examples/{sample-mission → sample-project}/memories/_index.md +2 -2
- package/examples/{sample-mission → sample-project}/memories/postgres-connection-pooling.md +1 -1
- package/examples/{sample-mission → sample-project}/resources/_index.md +1 -1
- package/package.json +5 -3
- package/platforms/README.md +7 -7
- package/platforms/claude-code/README.md +1 -1
- package/platforms/claude-code/agents/syntaur-expert.md +57 -57
- package/platforms/claude-code/commands/doctor-syntaur/doctor-syntaur.md +112 -0
- package/platforms/claude-code/commands/track-session/track-session.md +8 -8
- package/platforms/claude-code/hooks/enforce-boundaries.sh +4 -4
- package/platforms/claude-code/hooks/hooks.json +1 -1
- package/platforms/claude-code/hooks/session-cleanup.sh +5 -5
- package/platforms/claude-code/references/file-ownership.md +8 -8
- package/platforms/claude-code/references/protocol-summary.md +7 -6
- package/platforms/claude-code/skills/complete-assignment/SKILL.md +21 -17
- package/platforms/claude-code/skills/create-assignment/SKILL.md +15 -14
- package/platforms/claude-code/skills/grab-assignment/SKILL.md +56 -49
- package/platforms/claude-code/skills/plan-assignment/SKILL.md +57 -10
- package/platforms/claude-code/skills/syntaur-protocol/SKILL.md +21 -17
- package/platforms/codex/.codex-plugin/plugin.json +3 -3
- package/platforms/codex/README.md +1 -1
- package/platforms/codex/adapters/AGENTS.md.template +3 -3
- package/platforms/codex/agents/openai.yaml +2 -2
- package/platforms/codex/agents/syntaur-operator.md +33 -30
- package/platforms/codex/references/file-ownership.md +8 -8
- package/platforms/codex/references/protocol-summary.md +11 -6
- package/platforms/codex/scripts/enforce-boundaries.sh +2 -2
- package/platforms/codex/scripts/session-cleanup.sh +2 -2
- package/platforms/codex/skills/complete-assignment/SKILL.md +6 -6
- package/platforms/codex/skills/create-assignment/SKILL.md +8 -7
- package/platforms/codex/skills/grab-assignment/SKILL.md +30 -20
- package/platforms/codex/skills/plan-assignment/SKILL.md +19 -11
- package/platforms/codex/skills/syntaur-protocol/SKILL.md +26 -21
- package/platforms/cursor/README.md +1 -1
- package/platforms/cursor/adapters/syntaur-protocol.mdc +1 -1
- package/platforms/opencode/README.md +1 -1
- package/platforms/opencode/adapters/opencode.json.template +1 -1
- package/dashboard/dist/assets/channel-PMR2DuGi.js +0 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-DmESf_RL.js +0 -1
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-DmESf_RL.js +0 -1
- package/dashboard/dist/assets/clone-WlIeyha4.js +0 -1
- package/dashboard/dist/assets/index-BhuXD-Q5.js +0 -445
- package/dashboard/dist/assets/index-BnqH-RIk.css +0 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DVO-Epiz.js +0 -1
- package/examples/sample-mission/agent.md +0 -33
- package/examples/sample-mission/claude.md +0 -13
- package/platforms/claude-code/skills/create-mission/SKILL.md +0 -51
- package/platforms/codex/skills/create-mission/SKILL.md +0 -35
- /package/examples/{sample-mission → sample-project}/assignments/design-auth-schema/decision-record.md +0 -0
- /package/examples/{sample-mission → sample-project}/assignments/design-auth-schema/handoff.md +0 -0
- /package/examples/{sample-mission → sample-project}/assignments/design-auth-schema/plan.md +0 -0
- /package/examples/{sample-mission → sample-project}/assignments/design-auth-schema/scratchpad.md +0 -0
- /package/examples/{sample-mission → sample-project}/assignments/implement-jwt-middleware/decision-record.md +0 -0
- /package/examples/{sample-mission → sample-project}/assignments/implement-jwt-middleware/handoff.md +0 -0
- /package/examples/{sample-mission → sample-project}/assignments/implement-jwt-middleware/plan.md +0 -0
- /package/examples/{sample-mission → sample-project}/assignments/implement-jwt-middleware/scratchpad.md +0 -0
- /package/examples/{sample-mission → sample-project}/assignments/write-auth-tests/decision-record.md +0 -0
- /package/examples/{sample-mission → sample-project}/assignments/write-auth-tests/handoff.md +0 -0
- /package/examples/{sample-mission → sample-project}/assignments/write-auth-tests/plan.md +0 -0
- /package/examples/{sample-mission → sample-project}/assignments/write-auth-tests/scratchpad.md +0 -0
- /package/examples/{sample-mission/mission.md → sample-project/project.md} +0 -0
- /package/examples/{sample-mission → sample-project}/resources/auth-requirements.md +0 -0
package/dist/dashboard/server.js
CHANGED
|
@@ -8,6 +8,40 @@ var __export = (target, all) => {
|
|
|
8
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// src/utils/paths.ts
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import { resolve } from "path";
|
|
14
|
+
function expandHome(p) {
|
|
15
|
+
if (p.startsWith("~/") || p === "~") {
|
|
16
|
+
return resolve(homedir(), p.slice(2));
|
|
17
|
+
}
|
|
18
|
+
return p;
|
|
19
|
+
}
|
|
20
|
+
function syntaurRoot() {
|
|
21
|
+
const override = process.env.SYNTAUR_HOME;
|
|
22
|
+
if (override && override.length > 0) {
|
|
23
|
+
return resolve(expandHome(override));
|
|
24
|
+
}
|
|
25
|
+
return resolve(homedir(), ".syntaur");
|
|
26
|
+
}
|
|
27
|
+
function defaultProjectDir() {
|
|
28
|
+
return resolve(syntaurRoot(), "projects");
|
|
29
|
+
}
|
|
30
|
+
function serversDir() {
|
|
31
|
+
return resolve(syntaurRoot(), "servers");
|
|
32
|
+
}
|
|
33
|
+
function playbooksDir() {
|
|
34
|
+
return resolve(syntaurRoot(), "playbooks");
|
|
35
|
+
}
|
|
36
|
+
function todosDir() {
|
|
37
|
+
return resolve(syntaurRoot(), "todos");
|
|
38
|
+
}
|
|
39
|
+
var init_paths = __esm({
|
|
40
|
+
"src/utils/paths.ts"() {
|
|
41
|
+
"use strict";
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
11
45
|
// src/lifecycle/types.ts
|
|
12
46
|
var DEFAULT_STATUSES;
|
|
13
47
|
var init_types = __esm({
|
|
@@ -184,6 +218,8 @@ function parseAssignmentFrontmatter(fileContent) {
|
|
|
184
218
|
id: getField2("id") ?? "",
|
|
185
219
|
slug: getField2("slug") ?? "",
|
|
186
220
|
title: getField2("title") ?? "",
|
|
221
|
+
project: getField2("project"),
|
|
222
|
+
type: getField2("type"),
|
|
187
223
|
status: getField2("status") ?? "pending",
|
|
188
224
|
priority: getField2("priority") ?? "medium",
|
|
189
225
|
created: getField2("created") ?? "",
|
|
@@ -282,10 +318,10 @@ var init_timestamp = __esm({
|
|
|
282
318
|
});
|
|
283
319
|
|
|
284
320
|
// src/lifecycle/transitions.ts
|
|
285
|
-
import { resolve } from "path";
|
|
321
|
+
import { resolve as resolve2 } from "path";
|
|
286
322
|
import { readFile } from "fs/promises";
|
|
287
|
-
function resolveAssignmentPath(
|
|
288
|
-
return
|
|
323
|
+
function resolveAssignmentPath(projectDir, assignmentSlug) {
|
|
324
|
+
return resolve2(projectDir, "assignments", assignmentSlug, "assignment.md");
|
|
289
325
|
}
|
|
290
326
|
async function readAssignment(filePath) {
|
|
291
327
|
if (!await fileExists(filePath)) {
|
|
@@ -295,11 +331,11 @@ async function readAssignment(filePath) {
|
|
|
295
331
|
const frontmatter = parseAssignmentFrontmatter(content);
|
|
296
332
|
return { content, frontmatter };
|
|
297
333
|
}
|
|
298
|
-
async function checkDependencies(
|
|
334
|
+
async function checkDependencies(projectDir, dependsOn, terminalStatuses) {
|
|
299
335
|
const terminals = terminalStatuses ?? /* @__PURE__ */ new Set(["completed"]);
|
|
300
336
|
const unmet = [];
|
|
301
337
|
for (const depSlug of dependsOn) {
|
|
302
|
-
const depPath = resolveAssignmentPath(
|
|
338
|
+
const depPath = resolveAssignmentPath(projectDir, depSlug);
|
|
303
339
|
if (!await fileExists(depPath)) {
|
|
304
340
|
unmet.push(`${depSlug} (file not found)`);
|
|
305
341
|
continue;
|
|
@@ -312,8 +348,8 @@ async function checkDependencies(missionDir, dependsOn, terminalStatuses) {
|
|
|
312
348
|
}
|
|
313
349
|
return { satisfied: unmet.length === 0, unmet };
|
|
314
350
|
}
|
|
315
|
-
async function executeTransition(
|
|
316
|
-
const filePath = resolveAssignmentPath(
|
|
351
|
+
async function executeTransition(projectDir, assignmentSlug, command, options = {}) {
|
|
352
|
+
const filePath = resolveAssignmentPath(projectDir, assignmentSlug);
|
|
317
353
|
const { content, frontmatter } = await readAssignment(filePath);
|
|
318
354
|
const targetStatus = getTargetStatus(frontmatter.status, command, options.transitionTable);
|
|
319
355
|
if (!targetStatus) {
|
|
@@ -325,7 +361,7 @@ async function executeTransition(missionDir, assignmentSlug, command, options =
|
|
|
325
361
|
}
|
|
326
362
|
const warnings = [];
|
|
327
363
|
if (command === "start" && frontmatter.dependsOn.length > 0) {
|
|
328
|
-
const depCheck = await checkDependencies(
|
|
364
|
+
const depCheck = await checkDependencies(projectDir, frontmatter.dependsOn, options.terminalStatuses);
|
|
329
365
|
if (!depCheck.satisfied) {
|
|
330
366
|
warnings.push(`Starting with unmet dependencies: ${depCheck.unmet.join(", ")}`);
|
|
331
367
|
}
|
|
@@ -374,28 +410,28 @@ var init_lifecycle = __esm({
|
|
|
374
410
|
}
|
|
375
411
|
});
|
|
376
412
|
|
|
377
|
-
// src/utils/paths.ts
|
|
378
|
-
import { homedir } from "os";
|
|
379
|
-
import { resolve as resolve2 } from "path";
|
|
380
|
-
function expandHome(p) {
|
|
381
|
-
if (p.startsWith("~/") || p === "~") {
|
|
382
|
-
return resolve2(homedir(), p.slice(2));
|
|
383
|
-
}
|
|
384
|
-
return p;
|
|
385
|
-
}
|
|
386
|
-
function syntaurRoot() {
|
|
387
|
-
return resolve2(homedir(), ".syntaur");
|
|
388
|
-
}
|
|
389
|
-
function defaultMissionDir() {
|
|
390
|
-
return resolve2(syntaurRoot(), "missions");
|
|
391
|
-
}
|
|
392
|
-
var init_paths = __esm({
|
|
393
|
-
"src/utils/paths.ts"() {
|
|
394
|
-
"use strict";
|
|
395
|
-
}
|
|
396
|
-
});
|
|
397
|
-
|
|
398
413
|
// src/templates/config.ts
|
|
414
|
+
function renderConfig(params) {
|
|
415
|
+
return `---
|
|
416
|
+
version: "1.0"
|
|
417
|
+
defaultProjectDir: ${params.defaultProjectDir}
|
|
418
|
+
onboarding:
|
|
419
|
+
completed: false
|
|
420
|
+
agentDefaults:
|
|
421
|
+
trustLevel: medium
|
|
422
|
+
autoApprove: false
|
|
423
|
+
backup:
|
|
424
|
+
repo: null
|
|
425
|
+
categories: projects, playbooks, todos, servers, config
|
|
426
|
+
lastBackup: null
|
|
427
|
+
lastRestore: null
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
# Syntaur Configuration
|
|
431
|
+
|
|
432
|
+
Global configuration for the Syntaur CLI.
|
|
433
|
+
`;
|
|
434
|
+
}
|
|
399
435
|
var init_config = __esm({
|
|
400
436
|
"src/templates/config.ts"() {
|
|
401
437
|
"use strict";
|
|
@@ -549,6 +585,14 @@ function serializeStatusConfig(statuses) {
|
|
|
549
585
|
}
|
|
550
586
|
return lines.join("\n");
|
|
551
587
|
}
|
|
588
|
+
function serializeBackupConfig(backup) {
|
|
589
|
+
const lines = ["backup:"];
|
|
590
|
+
lines.push(` repo: ${backup.repo ?? "null"}`);
|
|
591
|
+
lines.push(` categories: ${backup.categories}`);
|
|
592
|
+
lines.push(` lastBackup: ${backup.lastBackup ?? "null"}`);
|
|
593
|
+
lines.push(` lastRestore: ${backup.lastRestore ?? "null"}`);
|
|
594
|
+
return lines.join("\n");
|
|
595
|
+
}
|
|
552
596
|
function stripTopLevelBlock(fmBlock, key) {
|
|
553
597
|
const blockStart = fmBlock.match(new RegExp(`^${key}:\\s*$`, "m"));
|
|
554
598
|
if (!blockStart) {
|
|
@@ -590,8 +634,8 @@ async function writeStatusConfig(statuses) {
|
|
|
590
634
|
const statusBlock = serializeStatusConfig(statuses);
|
|
591
635
|
if (!await fileExists(configPath)) {
|
|
592
636
|
const content = `---
|
|
593
|
-
version: "
|
|
594
|
-
|
|
637
|
+
version: "2.0"
|
|
638
|
+
defaultProjectDir: ~/projects
|
|
595
639
|
${statusBlock}
|
|
596
640
|
---
|
|
597
641
|
`;
|
|
@@ -602,7 +646,7 @@ ${statusBlock}
|
|
|
602
646
|
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
603
647
|
if (!fmMatch) {
|
|
604
648
|
const content = `---
|
|
605
|
-
version: "
|
|
649
|
+
version: "2.0"
|
|
606
650
|
${statusBlock}
|
|
607
651
|
---
|
|
608
652
|
${existing}`;
|
|
@@ -653,6 +697,40 @@ ${cleanedFm}
|
|
|
653
697
|
---${afterFrontmatter}`;
|
|
654
698
|
await writeFileForce(configPath, newContent);
|
|
655
699
|
}
|
|
700
|
+
async function updateBackupConfig(backup) {
|
|
701
|
+
const configPath = resolve3(syntaurRoot(), "config.md");
|
|
702
|
+
const current = (await readConfig()).backup;
|
|
703
|
+
const nextBackup = {
|
|
704
|
+
repo: current?.repo ?? null,
|
|
705
|
+
categories: current?.categories ?? "projects, playbooks, todos, servers, config",
|
|
706
|
+
lastBackup: current?.lastBackup ?? null,
|
|
707
|
+
lastRestore: current?.lastRestore ?? null,
|
|
708
|
+
...backup
|
|
709
|
+
};
|
|
710
|
+
const backupBlock = serializeBackupConfig(nextBackup);
|
|
711
|
+
const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
712
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
713
|
+
if (!fmMatch) {
|
|
714
|
+
const content = `---
|
|
715
|
+
version: "2.0"
|
|
716
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
717
|
+
${backupBlock}
|
|
718
|
+
---
|
|
719
|
+
${existing}`;
|
|
720
|
+
await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const fmBlock = fmMatch[2];
|
|
724
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
725
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "backup");
|
|
726
|
+
const newFm = `${cleanedFm}
|
|
727
|
+
${backupBlock}`.replace(/^\n+/, "");
|
|
728
|
+
const normalizedFm = newFm.replace(/\n+$/, "");
|
|
729
|
+
const newContent = `---
|
|
730
|
+
${normalizedFm}
|
|
731
|
+
---${afterFrontmatter}`;
|
|
732
|
+
await writeFileForce(configPath, newContent);
|
|
733
|
+
}
|
|
656
734
|
async function readConfig() {
|
|
657
735
|
const configPath = resolve3(syntaurRoot(), "config.md");
|
|
658
736
|
if (!await fileExists(configPath)) {
|
|
@@ -664,16 +742,16 @@ async function readConfig() {
|
|
|
664
742
|
console.warn("Warning: ~/.syntaur/config.md has malformed frontmatter, using defaults");
|
|
665
743
|
return { ...DEFAULT_CONFIG };
|
|
666
744
|
}
|
|
667
|
-
let
|
|
668
|
-
if (!isAbsolute(
|
|
745
|
+
let projectDir = fm["defaultProjectDir"] ? expandHome(String(fm["defaultProjectDir"])) : DEFAULT_CONFIG.defaultProjectDir;
|
|
746
|
+
if (!isAbsolute(projectDir)) {
|
|
669
747
|
console.warn(
|
|
670
|
-
`Warning: config.md
|
|
748
|
+
`Warning: config.md defaultProjectDir is not an absolute path ("${fm["defaultProjectDir"]}"), using default`
|
|
671
749
|
);
|
|
672
|
-
|
|
750
|
+
projectDir = DEFAULT_CONFIG.defaultProjectDir;
|
|
673
751
|
}
|
|
674
752
|
return {
|
|
675
753
|
version: fm["version"] || DEFAULT_CONFIG.version,
|
|
676
|
-
|
|
754
|
+
defaultProjectDir: projectDir,
|
|
677
755
|
onboarding: {
|
|
678
756
|
completed: fm["onboarding.completed"] === "true"
|
|
679
757
|
},
|
|
@@ -695,7 +773,14 @@ async function readConfig() {
|
|
|
695
773
|
"integrations.codexMarketplacePath"
|
|
696
774
|
)
|
|
697
775
|
},
|
|
698
|
-
|
|
776
|
+
backup: fm["backup.repo"] || fm["backup.categories"] ? {
|
|
777
|
+
repo: fm["backup.repo"] && fm["backup.repo"] !== "null" ? fm["backup.repo"] : null,
|
|
778
|
+
categories: fm["backup.categories"] || "projects, playbooks, todos, servers, config",
|
|
779
|
+
lastBackup: fm["backup.lastBackup"] && fm["backup.lastBackup"] !== "null" ? fm["backup.lastBackup"] : null,
|
|
780
|
+
lastRestore: fm["backup.lastRestore"] && fm["backup.lastRestore"] !== "null" ? fm["backup.lastRestore"] : null
|
|
781
|
+
} : null,
|
|
782
|
+
statuses: parseStatusConfig(content),
|
|
783
|
+
types: null
|
|
699
784
|
};
|
|
700
785
|
}
|
|
701
786
|
var DEFAULT_CONFIG;
|
|
@@ -706,8 +791,8 @@ var init_config2 = __esm({
|
|
|
706
791
|
init_fs();
|
|
707
792
|
init_config();
|
|
708
793
|
DEFAULT_CONFIG = {
|
|
709
|
-
version: "
|
|
710
|
-
|
|
794
|
+
version: "2.0",
|
|
795
|
+
defaultProjectDir: defaultProjectDir(),
|
|
711
796
|
onboarding: {
|
|
712
797
|
completed: false
|
|
713
798
|
},
|
|
@@ -720,7 +805,9 @@ var init_config2 = __esm({
|
|
|
720
805
|
codexPluginDir: null,
|
|
721
806
|
codexMarketplacePath: null
|
|
722
807
|
},
|
|
723
|
-
|
|
808
|
+
backup: null,
|
|
809
|
+
statuses: null,
|
|
810
|
+
types: null
|
|
724
811
|
};
|
|
725
812
|
}
|
|
726
813
|
});
|
|
@@ -773,7 +860,7 @@ function parseListField(frontmatter, fieldName) {
|
|
|
773
860
|
}
|
|
774
861
|
return results;
|
|
775
862
|
}
|
|
776
|
-
function
|
|
863
|
+
function parseProject(fileContent) {
|
|
777
864
|
const [fm, body] = extractFrontmatter2(fileContent);
|
|
778
865
|
return {
|
|
779
866
|
id: getField(fm, "id") ?? "",
|
|
@@ -804,13 +891,13 @@ function parseStatus(fileContent) {
|
|
|
804
891
|
}
|
|
805
892
|
}
|
|
806
893
|
return {
|
|
807
|
-
|
|
894
|
+
project: getField(fm, "project") ?? "",
|
|
808
895
|
status: getField(fm, "status") ?? "pending",
|
|
809
896
|
progress,
|
|
810
897
|
needsAttention: {
|
|
811
898
|
blockedCount: parseInt(getNestedField(fm, "needsAttention", "blockedCount") ?? "0", 10),
|
|
812
899
|
failedCount: parseInt(getNestedField(fm, "needsAttention", "failedCount") ?? "0", 10),
|
|
813
|
-
|
|
900
|
+
openQuestions: parseInt(getNestedField(fm, "needsAttention", "openQuestions") ?? "0", 10)
|
|
814
901
|
},
|
|
815
902
|
body
|
|
816
903
|
};
|
|
@@ -852,6 +939,8 @@ function parseAssignmentFull(fileContent) {
|
|
|
852
939
|
id: getField(fm, "id") ?? "",
|
|
853
940
|
slug: getField(fm, "slug") ?? "",
|
|
854
941
|
title: getField(fm, "title") ?? "",
|
|
942
|
+
project: getField(fm, "project"),
|
|
943
|
+
type: getField(fm, "type"),
|
|
855
944
|
status: getField(fm, "status") ?? "pending",
|
|
856
945
|
priority: getField(fm, "priority") ?? "medium",
|
|
857
946
|
assignee: getField(fm, "assignee"),
|
|
@@ -971,17 +1060,17 @@ async function getDashboardHelp() {
|
|
|
971
1060
|
return {
|
|
972
1061
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
973
1062
|
whatIsSyntaur: {
|
|
974
|
-
summary: "Syntaur is a local-first, markdown-backed agent work system. The dashboard is a live view over
|
|
1063
|
+
summary: "Syntaur is a local-first, markdown-backed agent work system. The dashboard is a live view over project folders and files on disk.",
|
|
975
1064
|
bullets: [
|
|
976
1065
|
"Markdown files are the source of truth.",
|
|
977
|
-
"The UI reads
|
|
1066
|
+
"The UI reads project folders, assignment files, and derived indexes from the local filesystem.",
|
|
978
1067
|
"Derived underscore-prefixed files are projections, not the canonical edit target."
|
|
979
1068
|
]
|
|
980
1069
|
},
|
|
981
1070
|
coreConcepts: [
|
|
982
1071
|
{
|
|
983
|
-
term: "
|
|
984
|
-
description: "A
|
|
1072
|
+
term: "Project",
|
|
1073
|
+
description: "A project is the higher-level objective. It owns assignments, shared resources, and project memories."
|
|
985
1074
|
},
|
|
986
1075
|
{
|
|
987
1076
|
term: "Assignment",
|
|
@@ -989,15 +1078,15 @@ async function getDashboardHelp() {
|
|
|
989
1078
|
},
|
|
990
1079
|
{
|
|
991
1080
|
term: "Resource",
|
|
992
|
-
description: "A
|
|
1081
|
+
description: "A project-level shared reference file that provides source material or constraints for the work."
|
|
993
1082
|
},
|
|
994
1083
|
{
|
|
995
1084
|
term: "Memory",
|
|
996
|
-
description: "A
|
|
1085
|
+
description: "A project-level learning or pattern captured during execution so future assignments can reuse it."
|
|
997
1086
|
},
|
|
998
1087
|
{
|
|
999
1088
|
term: "Manifest",
|
|
1000
|
-
description: "A derived navigation file that points agents at the
|
|
1089
|
+
description: "A derived navigation file that points agents at the project overview, indexes, and agent instructions."
|
|
1001
1090
|
},
|
|
1002
1091
|
{
|
|
1003
1092
|
term: "Derived file",
|
|
@@ -1033,12 +1122,12 @@ async function getDashboardHelp() {
|
|
|
1033
1122
|
ownershipRules: [
|
|
1034
1123
|
{
|
|
1035
1124
|
label: "Human-authored files",
|
|
1036
|
-
files: ["
|
|
1037
|
-
description: "These files define
|
|
1125
|
+
files: ["project.md", "agent.md", "claude.md"],
|
|
1126
|
+
description: "These files define project intent and instructions. The dashboard treats project status as derived except for the archive fields."
|
|
1038
1127
|
},
|
|
1039
1128
|
{
|
|
1040
1129
|
label: "Assignment working files",
|
|
1041
|
-
files: ["assignment.md", "plan
|
|
1130
|
+
files: ["assignment.md", "plan*.md (optional, versioned)", "scratchpad.md"],
|
|
1042
1131
|
description: "These are agent-writable files. The dashboard lets you edit the source markdown while preserving unsupported frontmatter keys."
|
|
1043
1132
|
},
|
|
1044
1133
|
{
|
|
@@ -1060,13 +1149,13 @@ async function getDashboardHelp() {
|
|
|
1060
1149
|
href: "/"
|
|
1061
1150
|
},
|
|
1062
1151
|
{
|
|
1063
|
-
label: "
|
|
1064
|
-
description: "Browse, search, filter, and sort the
|
|
1065
|
-
href: "/
|
|
1152
|
+
label: "Projects",
|
|
1153
|
+
description: "Browse, search, filter, and sort the project directory. Create new projects and drill into project workspaces.",
|
|
1154
|
+
href: "/projects"
|
|
1066
1155
|
},
|
|
1067
1156
|
{
|
|
1068
1157
|
label: "Assignments",
|
|
1069
|
-
description: "Cross-
|
|
1158
|
+
description: "Cross-project kanban board of all assignments. Drag cards between columns to change status, or filter by project, assignee, or status.",
|
|
1070
1159
|
href: "/assignments"
|
|
1071
1160
|
},
|
|
1072
1161
|
{
|
|
@@ -1100,14 +1189,14 @@ async function getDashboardHelp() {
|
|
|
1100
1189
|
href: "/settings"
|
|
1101
1190
|
},
|
|
1102
1191
|
{
|
|
1103
|
-
label: "
|
|
1104
|
-
description: "The
|
|
1105
|
-
href: "/
|
|
1192
|
+
label: "Project page",
|
|
1193
|
+
description: "The project workspace shows health stats, assignment list, dependency graph, shared resources, and memories.",
|
|
1194
|
+
href: "/projects"
|
|
1106
1195
|
},
|
|
1107
1196
|
{
|
|
1108
1197
|
label: "Assignment page",
|
|
1109
1198
|
description: "The assignment workspace shows lifecycle actions, plan editor, scratchpad, handoff log, decision records, and agent sessions.",
|
|
1110
|
-
href: "/
|
|
1199
|
+
href: "/projects"
|
|
1111
1200
|
}
|
|
1112
1201
|
],
|
|
1113
1202
|
faq: [
|
|
@@ -1144,16 +1233,16 @@ async function getDashboardHelp() {
|
|
|
1144
1233
|
answer: "Syntaur tracks tmux sessions to discover running dev servers, their ports, git branches, and linked assignments. Register sessions on the Servers page or let autodiscovery find them. Pane info refreshes automatically."
|
|
1145
1234
|
}
|
|
1146
1235
|
],
|
|
1147
|
-
|
|
1236
|
+
firstProjectChecklist: [
|
|
1148
1237
|
{
|
|
1149
|
-
title: "Create the
|
|
1150
|
-
detail: "Describe the overall objective in
|
|
1238
|
+
title: "Create the project",
|
|
1239
|
+
detail: "Describe the overall objective in project.md, then add tags and archive metadata only when needed.",
|
|
1151
1240
|
command: CLI_COMMANDS[1],
|
|
1152
|
-
href: "/create/
|
|
1241
|
+
href: "/create/project"
|
|
1153
1242
|
},
|
|
1154
1243
|
{
|
|
1155
1244
|
title: "Create at least one assignment",
|
|
1156
|
-
detail: "Break the
|
|
1245
|
+
detail: "Break the project into executable work units with explicit priority and dependencies.",
|
|
1157
1246
|
command: CLI_COMMANDS[2]
|
|
1158
1247
|
},
|
|
1159
1248
|
{
|
|
@@ -1163,8 +1252,8 @@ async function getDashboardHelp() {
|
|
|
1163
1252
|
},
|
|
1164
1253
|
{
|
|
1165
1254
|
title: "Use the assignment workspace for execution",
|
|
1166
|
-
detail: "Keep the objective in assignment.md,
|
|
1167
|
-
href: "/
|
|
1255
|
+
detail: "Keep the objective and todos in assignment.md, implementation plans in optional versioned plan files (plan.md, plan-v2.md, ...), and transient notes in scratchpad.md.",
|
|
1256
|
+
href: "/projects"
|
|
1168
1257
|
},
|
|
1169
1258
|
{
|
|
1170
1259
|
title: "Record handoffs and decisions without rewriting history",
|
|
@@ -1178,14 +1267,14 @@ async function getDashboardHelp() {
|
|
|
1178
1267
|
],
|
|
1179
1268
|
links: [
|
|
1180
1269
|
{ label: "Overview", href: "/" },
|
|
1181
|
-
{ label: "
|
|
1270
|
+
{ label: "Project Directory", href: "/projects" },
|
|
1182
1271
|
{ label: "Assignments Board", href: "/assignments" },
|
|
1183
1272
|
{ label: "Attention Queue", href: "/attention" },
|
|
1184
1273
|
{ label: "Servers", href: "/servers" },
|
|
1185
1274
|
{ label: "Agent Sessions", href: "/agent-sessions" },
|
|
1186
1275
|
{ label: "Playbooks", href: "/playbooks" },
|
|
1187
1276
|
{ label: "Settings", href: "/settings" },
|
|
1188
|
-
{ label: "Create
|
|
1277
|
+
{ label: "Create Project", href: "/create/project" }
|
|
1189
1278
|
]
|
|
1190
1279
|
};
|
|
1191
1280
|
}
|
|
@@ -1207,60 +1296,60 @@ var init_help = __esm({
|
|
|
1207
1296
|
example: "syntaur init"
|
|
1208
1297
|
},
|
|
1209
1298
|
{
|
|
1210
|
-
command: "syntaur create-
|
|
1211
|
-
description: "Create a new
|
|
1212
|
-
example: 'syntaur create-
|
|
1299
|
+
command: "syntaur create-project",
|
|
1300
|
+
description: "Create a new project folder with the required source and derived files.",
|
|
1301
|
+
example: 'syntaur create-project "Ship dashboard overhaul"'
|
|
1213
1302
|
},
|
|
1214
1303
|
{
|
|
1215
1304
|
command: "syntaur create-assignment",
|
|
1216
|
-
description: "Create a new assignment inside a
|
|
1217
|
-
example: 'syntaur create-assignment "Implement overview API" --
|
|
1305
|
+
description: "Create a new assignment inside a project.",
|
|
1306
|
+
example: 'syntaur create-assignment "Implement overview API" --project ui-overhaul'
|
|
1218
1307
|
},
|
|
1219
1308
|
{
|
|
1220
1309
|
command: "syntaur assign",
|
|
1221
1310
|
description: "Set the assignee for an assignment before work begins.",
|
|
1222
|
-
example: "syntaur assign implement-overview --
|
|
1311
|
+
example: "syntaur assign implement-overview --project ui-overhaul --agent codex-1"
|
|
1223
1312
|
},
|
|
1224
1313
|
// --- Lifecycle transitions (indices 5-11) ---
|
|
1225
1314
|
{
|
|
1226
1315
|
command: "syntaur start",
|
|
1227
1316
|
description: "Transition an assignment to in_progress.",
|
|
1228
|
-
example: "syntaur start implement-overview --
|
|
1317
|
+
example: "syntaur start implement-overview --project ui-overhaul"
|
|
1229
1318
|
},
|
|
1230
1319
|
{
|
|
1231
1320
|
command: "syntaur review",
|
|
1232
1321
|
description: "Move active work into review once implementation is ready for inspection.",
|
|
1233
|
-
example: "syntaur review implement-overview --
|
|
1322
|
+
example: "syntaur review implement-overview --project ui-overhaul"
|
|
1234
1323
|
},
|
|
1235
1324
|
{
|
|
1236
1325
|
command: "syntaur complete",
|
|
1237
1326
|
description: "Mark an assignment completed after review or direct completion.",
|
|
1238
|
-
example: "syntaur complete implement-overview --
|
|
1327
|
+
example: "syntaur complete implement-overview --project ui-overhaul"
|
|
1239
1328
|
},
|
|
1240
1329
|
{
|
|
1241
1330
|
command: "syntaur block",
|
|
1242
1331
|
description: "Mark an assignment blocked and record the explicit reason.",
|
|
1243
|
-
example: 'syntaur block implement-overview --
|
|
1332
|
+
example: 'syntaur block implement-overview --project ui-overhaul --reason "Waiting on API spec"'
|
|
1244
1333
|
},
|
|
1245
1334
|
{
|
|
1246
1335
|
command: "syntaur unblock",
|
|
1247
1336
|
description: "Move a blocked assignment back to in_progress after the blocker is cleared.",
|
|
1248
|
-
example: "syntaur unblock implement-overview --
|
|
1337
|
+
example: "syntaur unblock implement-overview --project ui-overhaul"
|
|
1249
1338
|
},
|
|
1250
1339
|
{
|
|
1251
1340
|
command: "syntaur fail",
|
|
1252
1341
|
description: "Mark an assignment failed when it cannot be completed as planned.",
|
|
1253
|
-
example: "syntaur fail implement-overview --
|
|
1342
|
+
example: "syntaur fail implement-overview --project ui-overhaul"
|
|
1254
1343
|
},
|
|
1255
1344
|
{
|
|
1256
1345
|
command: "syntaur reopen",
|
|
1257
1346
|
description: "Reopen a completed or failed assignment back to in_progress.",
|
|
1258
|
-
example: "syntaur reopen implement-overview --
|
|
1347
|
+
example: "syntaur reopen implement-overview --project ui-overhaul"
|
|
1259
1348
|
},
|
|
1260
1349
|
// --- Dashboard (index 12) ---
|
|
1261
1350
|
{
|
|
1262
1351
|
command: "syntaur dashboard",
|
|
1263
|
-
description: "Start the local dashboard UI over the
|
|
1352
|
+
description: "Start the local dashboard UI over the project files on disk.",
|
|
1264
1353
|
example: "syntaur dashboard --port 4800"
|
|
1265
1354
|
},
|
|
1266
1355
|
// --- Plugin & adapter setup (indices 13-16) ---
|
|
@@ -1282,18 +1371,18 @@ var init_help = __esm({
|
|
|
1282
1371
|
{
|
|
1283
1372
|
command: "syntaur setup-adapter",
|
|
1284
1373
|
description: "Generate adapter instruction files for cursor, codex, or opencode in the current directory.",
|
|
1285
|
-
example: "syntaur setup-adapter cursor --
|
|
1374
|
+
example: "syntaur setup-adapter cursor --project ui-overhaul --assignment implement-overview"
|
|
1286
1375
|
},
|
|
1287
1376
|
// --- Session & server tracking (index 17) ---
|
|
1288
1377
|
{
|
|
1289
1378
|
command: "syntaur track-session",
|
|
1290
|
-
description: "Register an agent session, optionally linked to a
|
|
1291
|
-
example: "syntaur track-session --agent claude --
|
|
1379
|
+
description: "Register an agent session, optionally linked to a project and assignment.",
|
|
1380
|
+
example: "syntaur track-session --agent claude --project ui-overhaul --assignment implement-overview"
|
|
1292
1381
|
},
|
|
1293
1382
|
// --- Browsing & playbooks (indices 18-20) ---
|
|
1294
1383
|
{
|
|
1295
1384
|
command: "syntaur browse",
|
|
1296
|
-
description: "Interactive TUI browser for
|
|
1385
|
+
description: "Interactive TUI browser for projects and assignments.",
|
|
1297
1386
|
example: "syntaur browse"
|
|
1298
1387
|
},
|
|
1299
1388
|
{
|
|
@@ -1314,14 +1403,14 @@ var init_help = __esm({
|
|
|
1314
1403
|
command: CLI_COMMANDS[0]
|
|
1315
1404
|
},
|
|
1316
1405
|
{
|
|
1317
|
-
title: "Create a
|
|
1318
|
-
detail: "Use a
|
|
1406
|
+
title: "Create a project",
|
|
1407
|
+
detail: "Use a project for a higher-level objective. Projects group assignments, shared resources, and memories.",
|
|
1319
1408
|
command: CLI_COMMANDS[2],
|
|
1320
|
-
href: "/create/
|
|
1409
|
+
href: "/create/project"
|
|
1321
1410
|
},
|
|
1322
1411
|
{
|
|
1323
1412
|
title: "Create the first assignment",
|
|
1324
|
-
detail: "Assignments are the execution unit. Create one for each concrete chunk of work inside the
|
|
1413
|
+
detail: "Assignments are the execution unit. Create one for each concrete chunk of work inside the project.",
|
|
1325
1414
|
command: CLI_COMMANDS[3]
|
|
1326
1415
|
},
|
|
1327
1416
|
{
|
|
@@ -1336,7 +1425,7 @@ var init_help = __esm({
|
|
|
1336
1425
|
},
|
|
1337
1426
|
{
|
|
1338
1427
|
title: "Use the dashboard for triage and context",
|
|
1339
|
-
detail: "Overview shows the current queue,
|
|
1428
|
+
detail: "Overview shows the current queue, project pages show health, assignment pages show the execution surface.",
|
|
1340
1429
|
command: CLI_COMMANDS[12],
|
|
1341
1430
|
href: "/"
|
|
1342
1431
|
}
|
|
@@ -1404,7 +1493,7 @@ function buildSessionContent(opts) {
|
|
|
1404
1493
|
if (Object.keys(opts.overrides).length > 0) {
|
|
1405
1494
|
lines.push("overrides:");
|
|
1406
1495
|
for (const [key, val] of Object.entries(opts.overrides)) {
|
|
1407
|
-
lines.push(` "${key}": {
|
|
1496
|
+
lines.push(` "${key}": { project: "${val.project}", assignment: "${val.assignment}" }`);
|
|
1408
1497
|
}
|
|
1409
1498
|
}
|
|
1410
1499
|
lines.push("---", "");
|
|
@@ -1441,10 +1530,10 @@ async function readSessionFile(dir, name) {
|
|
|
1441
1530
|
const overridesMatch = frontmatter.match(/^overrides:\n((?:\s+".+\n?)*)/m);
|
|
1442
1531
|
if (overridesMatch) {
|
|
1443
1532
|
const overrideLines = overridesMatch[1].matchAll(
|
|
1444
|
-
/^\s+"([^"]+)":\s*\{\s*
|
|
1533
|
+
/^\s+"([^"]+)":\s*\{\s*project:\s*"([^"]+)",\s*assignment:\s*"([^"]+)"\s*\}/gm
|
|
1445
1534
|
);
|
|
1446
1535
|
for (const m of overrideLines) {
|
|
1447
|
-
overrides[m[1]] = {
|
|
1536
|
+
overrides[m[1]] = { project: m[2], assignment: m[3] };
|
|
1448
1537
|
}
|
|
1449
1538
|
}
|
|
1450
1539
|
const autoField = getField(frontmatter, "auto");
|
|
@@ -1646,12 +1735,12 @@ async function getGitInfo(cwd) {
|
|
|
1646
1735
|
}
|
|
1647
1736
|
return { branch: branch || null, worktree: isWorktree };
|
|
1648
1737
|
}
|
|
1649
|
-
async function loadWorkspaceRecords(
|
|
1738
|
+
async function loadWorkspaceRecords(projectsDir) {
|
|
1650
1739
|
const records = [];
|
|
1651
1740
|
try {
|
|
1652
|
-
const
|
|
1653
|
-
for (const
|
|
1654
|
-
const assignmentsDir = resolve5(
|
|
1741
|
+
const projects = await listProjects(projectsDir);
|
|
1742
|
+
for (const project of projects) {
|
|
1743
|
+
const assignmentsDir = resolve5(projectsDir, project.slug, "assignments");
|
|
1655
1744
|
let slugs;
|
|
1656
1745
|
try {
|
|
1657
1746
|
slugs = await readdir2(assignmentsDir);
|
|
@@ -1665,7 +1754,7 @@ async function loadWorkspaceRecords(missionsDir) {
|
|
|
1665
1754
|
const [fm] = extractFrontmatter2(raw);
|
|
1666
1755
|
if (!fm) continue;
|
|
1667
1756
|
records.push({
|
|
1668
|
-
|
|
1757
|
+
projectSlug: project.slug,
|
|
1669
1758
|
assignmentSlug: aslug,
|
|
1670
1759
|
assignmentTitle: getField(fm, "title") ?? aslug,
|
|
1671
1760
|
worktreePath: getNestedField(fm, "workspace", "worktreePath") ?? null,
|
|
@@ -1694,14 +1783,14 @@ async function autoLinkPane(cwd, branch, records) {
|
|
|
1694
1783
|
if (rec.worktreePath) {
|
|
1695
1784
|
const normalizedWt = await resolveAndNormalize(rec.worktreePath);
|
|
1696
1785
|
if (normalizedCwd === normalizedWt) {
|
|
1697
|
-
return {
|
|
1786
|
+
return { project: rec.projectSlug, slug: rec.assignmentSlug, title: rec.assignmentTitle };
|
|
1698
1787
|
}
|
|
1699
1788
|
}
|
|
1700
1789
|
}
|
|
1701
1790
|
if (branch) {
|
|
1702
1791
|
for (const rec of records) {
|
|
1703
1792
|
if (rec.branch && rec.branch === branch) {
|
|
1704
|
-
return {
|
|
1793
|
+
return { project: rec.projectSlug, slug: rec.assignmentSlug, title: rec.assignmentTitle };
|
|
1705
1794
|
}
|
|
1706
1795
|
}
|
|
1707
1796
|
}
|
|
@@ -1771,10 +1860,10 @@ async function scanSession(sessionData, lsofOutput, workspaceRecords) {
|
|
|
1771
1860
|
let assignment = null;
|
|
1772
1861
|
if (override) {
|
|
1773
1862
|
const rec = workspaceRecords.find(
|
|
1774
|
-
(r) => r.
|
|
1863
|
+
(r) => r.projectSlug === override.project && r.assignmentSlug === override.assignment
|
|
1775
1864
|
);
|
|
1776
1865
|
assignment = {
|
|
1777
|
-
|
|
1866
|
+
project: override.project,
|
|
1778
1867
|
slug: override.assignment,
|
|
1779
1868
|
title: rec?.assignmentTitle ?? override.assignment
|
|
1780
1869
|
};
|
|
@@ -1833,10 +1922,10 @@ async function scanProcessSession(sessionData, lsofOutput, workspaceRecords) {
|
|
|
1833
1922
|
let assignment = null;
|
|
1834
1923
|
if (override) {
|
|
1835
1924
|
const rec = workspaceRecords.find(
|
|
1836
|
-
(r) => r.
|
|
1925
|
+
(r) => r.projectSlug === override.project && r.assignmentSlug === override.assignment
|
|
1837
1926
|
);
|
|
1838
1927
|
assignment = {
|
|
1839
|
-
|
|
1928
|
+
project: override.project,
|
|
1840
1929
|
slug: override.assignment,
|
|
1841
1930
|
title: rec?.assignmentTitle ?? override.assignment
|
|
1842
1931
|
};
|
|
@@ -1863,17 +1952,17 @@ async function scanProcessSession(sessionData, lsofOutput, workspaceRecords) {
|
|
|
1863
1952
|
windows: [{ index: 0, name: "process", panes: [pane] }]
|
|
1864
1953
|
};
|
|
1865
1954
|
}
|
|
1866
|
-
async function scanAllSessions(
|
|
1955
|
+
async function scanAllSessions(serversDir2, projectsDir, options) {
|
|
1867
1956
|
if (!options?.bypassCache && cache && Date.now() < cache.expiry) {
|
|
1868
1957
|
return cache.data;
|
|
1869
1958
|
}
|
|
1870
1959
|
const tmuxAvailable = await checkTmuxAvailable();
|
|
1871
|
-
const names = await listSessionFiles(
|
|
1960
|
+
const names = await listSessionFiles(serversDir2);
|
|
1872
1961
|
const lsofOutput = await getLsofOutput();
|
|
1873
|
-
const workspaceRecords = await loadWorkspaceRecords(
|
|
1962
|
+
const workspaceRecords = await loadWorkspaceRecords(projectsDir);
|
|
1874
1963
|
const sessions = [];
|
|
1875
1964
|
for (const name of names) {
|
|
1876
|
-
const data = await readSessionFile(
|
|
1965
|
+
const data = await readSessionFile(serversDir2, name);
|
|
1877
1966
|
if (!data) continue;
|
|
1878
1967
|
if (data.kind === "process") {
|
|
1879
1968
|
sessions.push(await scanProcessSession(data, lsofOutput, workspaceRecords));
|
|
@@ -1885,11 +1974,11 @@ async function scanAllSessions(serversDir, missionsDir, options) {
|
|
|
1885
1974
|
cache = { data: result, expiry: Date.now() + CACHE_TTL_MS };
|
|
1886
1975
|
return result;
|
|
1887
1976
|
}
|
|
1888
|
-
async function scanSingleSession(
|
|
1889
|
-
const data = await readSessionFile(
|
|
1977
|
+
async function scanSingleSession(serversDir2, projectsDir, name) {
|
|
1978
|
+
const data = await readSessionFile(serversDir2, name);
|
|
1890
1979
|
if (!data) return null;
|
|
1891
1980
|
const lsofOutput = await getLsofOutput();
|
|
1892
|
-
const workspaceRecords = await loadWorkspaceRecords(
|
|
1981
|
+
const workspaceRecords = await loadWorkspaceRecords(projectsDir);
|
|
1893
1982
|
if (data.kind === "process") {
|
|
1894
1983
|
return scanProcessSession(data, lsofOutput, workspaceRecords);
|
|
1895
1984
|
}
|
|
@@ -1966,12 +2055,12 @@ async function getStatusConfig() {
|
|
|
1966
2055
|
function clearStatusConfigCache() {
|
|
1967
2056
|
_cachedConfig = null;
|
|
1968
2057
|
}
|
|
1969
|
-
async function
|
|
1970
|
-
const
|
|
1971
|
-
return
|
|
2058
|
+
async function listProjects(projectsDir) {
|
|
2059
|
+
const projectRecords = await listProjectRecords(projectsDir);
|
|
2060
|
+
return projectRecords.map((record) => record.summary);
|
|
1972
2061
|
}
|
|
1973
|
-
async function readWorkspaceRegistry(
|
|
1974
|
-
const registryPath = resolve6(dirname2(
|
|
2062
|
+
async function readWorkspaceRegistry(projectsDir) {
|
|
2063
|
+
const registryPath = resolve6(dirname2(projectsDir), "workspaces.json");
|
|
1975
2064
|
try {
|
|
1976
2065
|
const raw = await readFile5(registryPath, "utf-8");
|
|
1977
2066
|
const parsed = JSON.parse(raw);
|
|
@@ -1980,20 +2069,20 @@ async function readWorkspaceRegistry(missionsDir) {
|
|
|
1980
2069
|
return [];
|
|
1981
2070
|
}
|
|
1982
2071
|
}
|
|
1983
|
-
async function writeWorkspaceRegistry(
|
|
1984
|
-
const registryPath = resolve6(dirname2(
|
|
2072
|
+
async function writeWorkspaceRegistry(projectsDir, workspaces) {
|
|
2073
|
+
const registryPath = resolve6(dirname2(projectsDir), "workspaces.json");
|
|
1985
2074
|
await writeFile2(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
|
|
1986
2075
|
}
|
|
1987
|
-
async function listWorkspaces(
|
|
1988
|
-
const [
|
|
1989
|
-
|
|
1990
|
-
readWorkspaceRegistry(
|
|
2076
|
+
async function listWorkspaces(projectsDir) {
|
|
2077
|
+
const [projectRecords, registered] = await Promise.all([
|
|
2078
|
+
listProjectRecords(projectsDir),
|
|
2079
|
+
readWorkspaceRegistry(projectsDir)
|
|
1991
2080
|
]);
|
|
1992
2081
|
const workspaceSet = new Set(registered);
|
|
1993
2082
|
let hasUngrouped = false;
|
|
1994
|
-
for (const record of
|
|
1995
|
-
if (record.
|
|
1996
|
-
workspaceSet.add(record.
|
|
2083
|
+
for (const record of projectRecords) {
|
|
2084
|
+
if (record.project.workspace) {
|
|
2085
|
+
workspaceSet.add(record.project.workspace);
|
|
1997
2086
|
} else {
|
|
1998
2087
|
hasUngrouped = true;
|
|
1999
2088
|
}
|
|
@@ -2001,28 +2090,28 @@ async function listWorkspaces(missionsDir) {
|
|
|
2001
2090
|
const workspaces = Array.from(workspaceSet).sort();
|
|
2002
2091
|
return { workspaces, hasUngrouped };
|
|
2003
2092
|
}
|
|
2004
|
-
async function createWorkspace(
|
|
2005
|
-
const registered = await readWorkspaceRegistry(
|
|
2093
|
+
async function createWorkspace(projectsDir, name) {
|
|
2094
|
+
const registered = await readWorkspaceRegistry(projectsDir);
|
|
2006
2095
|
if (!registered.includes(name)) {
|
|
2007
2096
|
registered.push(name);
|
|
2008
2097
|
registered.sort();
|
|
2009
|
-
await writeWorkspaceRegistry(
|
|
2098
|
+
await writeWorkspaceRegistry(projectsDir, registered);
|
|
2010
2099
|
}
|
|
2011
2100
|
}
|
|
2012
|
-
async function deleteWorkspace(
|
|
2013
|
-
const registered = await readWorkspaceRegistry(
|
|
2101
|
+
async function deleteWorkspace(projectsDir, name) {
|
|
2102
|
+
const registered = await readWorkspaceRegistry(projectsDir);
|
|
2014
2103
|
const filtered = registered.filter((w) => w !== name);
|
|
2015
|
-
await writeWorkspaceRegistry(
|
|
2104
|
+
await writeWorkspaceRegistry(projectsDir, filtered);
|
|
2016
2105
|
}
|
|
2017
|
-
async function getOverview(
|
|
2018
|
-
const
|
|
2019
|
-
const attention = buildAttentionItems(
|
|
2020
|
-
const recentActivity = buildRecentActivity(
|
|
2106
|
+
async function getOverview(projectsDir, serversDir2) {
|
|
2107
|
+
const projectRecords = await listProjectRecords(projectsDir);
|
|
2108
|
+
const attention = buildAttentionItems(projectRecords);
|
|
2109
|
+
const recentActivity = buildRecentActivity(projectRecords);
|
|
2021
2110
|
let serverStats;
|
|
2022
|
-
if (
|
|
2111
|
+
if (serversDir2) {
|
|
2023
2112
|
try {
|
|
2024
2113
|
const { scanAllSessions: scanAllSessions2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
|
|
2025
|
-
const servers = await scanAllSessions2(
|
|
2114
|
+
const servers = await scanAllSessions2(serversDir2, projectsDir);
|
|
2026
2115
|
if (servers.tmuxAvailable) {
|
|
2027
2116
|
const alive = servers.sessions.filter((s) => s.alive).length;
|
|
2028
2117
|
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);
|
|
@@ -2038,50 +2127,50 @@ async function getOverview(missionsDir, serversDir) {
|
|
|
2038
2127
|
}
|
|
2039
2128
|
return {
|
|
2040
2129
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2041
|
-
firstRun:
|
|
2130
|
+
firstRun: projectRecords.length === 0,
|
|
2042
2131
|
stats: {
|
|
2043
|
-
|
|
2044
|
-
inProgressAssignments:
|
|
2132
|
+
activeProjects: projectRecords.filter((record) => record.summary.status === "active").length,
|
|
2133
|
+
inProgressAssignments: projectRecords.reduce(
|
|
2045
2134
|
(total, record) => total + (record.summary.progress["in_progress"] ?? 0),
|
|
2046
2135
|
0
|
|
2047
2136
|
),
|
|
2048
|
-
blockedAssignments:
|
|
2137
|
+
blockedAssignments: projectRecords.reduce(
|
|
2049
2138
|
(total, record) => total + (record.summary.progress["blocked"] ?? 0),
|
|
2050
2139
|
0
|
|
2051
2140
|
),
|
|
2052
|
-
reviewAssignments:
|
|
2141
|
+
reviewAssignments: projectRecords.reduce(
|
|
2053
2142
|
(total, record) => total + (record.summary.progress["review"] ?? 0),
|
|
2054
2143
|
0
|
|
2055
2144
|
),
|
|
2056
|
-
failedAssignments:
|
|
2145
|
+
failedAssignments: projectRecords.reduce(
|
|
2057
2146
|
(total, record) => total + (record.summary.progress["failed"] ?? 0),
|
|
2058
2147
|
0
|
|
2059
2148
|
),
|
|
2060
|
-
staleAssignments:
|
|
2149
|
+
staleAssignments: projectRecords.reduce(
|
|
2061
2150
|
(total, record) => total + record.assignments.filter((assignment) => isStale(assignment.updated)).length,
|
|
2062
2151
|
0
|
|
2063
2152
|
)
|
|
2064
2153
|
},
|
|
2065
2154
|
attention: attention.slice(0, OVERVIEW_ATTENTION_LIMIT),
|
|
2066
|
-
|
|
2155
|
+
recentProjects: projectRecords.map((record) => record.summary).sort((left, right) => compareTimestamps(right.updated, left.updated)).slice(0, RECENT_PROJECTS_LIMIT),
|
|
2067
2156
|
recentActivity: recentActivity.slice(0, RECENT_ACTIVITY_LIMIT),
|
|
2068
2157
|
serverStats
|
|
2069
2158
|
};
|
|
2070
2159
|
}
|
|
2071
|
-
async function getAttention(
|
|
2072
|
-
const
|
|
2073
|
-
const items = buildAttentionItems(
|
|
2074
|
-
if (
|
|
2160
|
+
async function getAttention(projectsDir, serversDir2) {
|
|
2161
|
+
const projectRecords = await listProjectRecords(projectsDir);
|
|
2162
|
+
const items = buildAttentionItems(projectRecords);
|
|
2163
|
+
if (serversDir2) {
|
|
2075
2164
|
try {
|
|
2076
2165
|
const { scanAllSessions: scanAllSessions2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
|
|
2077
|
-
const servers = await scanAllSessions2(
|
|
2166
|
+
const servers = await scanAllSessions2(serversDir2, projectsDir);
|
|
2078
2167
|
for (const session of servers.sessions) {
|
|
2079
2168
|
if (!session.alive) {
|
|
2080
2169
|
items.push({
|
|
2081
2170
|
id: `server-dead-${session.name}`,
|
|
2082
2171
|
severity: "low",
|
|
2083
|
-
|
|
2084
|
-
|
|
2172
|
+
projectSlug: "",
|
|
2173
|
+
projectTitle: "",
|
|
2085
2174
|
assignmentSlug: "",
|
|
2086
2175
|
assignmentTitle: `tmux: ${session.name}`,
|
|
2087
2176
|
status: "failed",
|
|
@@ -2118,13 +2207,13 @@ async function getAttention(missionsDir, serversDir) {
|
|
|
2118
2207
|
items: pagedItems
|
|
2119
2208
|
};
|
|
2120
2209
|
}
|
|
2121
|
-
async function listAssignmentsBoard(
|
|
2122
|
-
const
|
|
2210
|
+
async function listAssignmentsBoard(projectsDir) {
|
|
2211
|
+
const projectRecords = await listProjectRecords(projectsDir);
|
|
2123
2212
|
const assignments = await Promise.all(
|
|
2124
|
-
|
|
2213
|
+
projectRecords.flatMap(
|
|
2125
2214
|
async (record) => Promise.all(
|
|
2126
2215
|
record.assignments.map(
|
|
2127
|
-
async (assignment) => toAssignmentBoardItem(
|
|
2216
|
+
async (assignment) => toAssignmentBoardItem(projectsDir, record, assignment)
|
|
2128
2217
|
)
|
|
2129
2218
|
)
|
|
2130
2219
|
)
|
|
@@ -2137,59 +2226,59 @@ async function listAssignmentsBoard(missionsDir) {
|
|
|
2137
2226
|
async function getHelp() {
|
|
2138
2227
|
return getDashboardHelp();
|
|
2139
2228
|
}
|
|
2140
|
-
async function getEditableDocument(
|
|
2141
|
-
const filePath = getDocumentPath(
|
|
2229
|
+
async function getEditableDocument(projectsDir, documentType, projectSlug, assignmentSlug) {
|
|
2230
|
+
const filePath = getDocumentPath(projectsDir, documentType, projectSlug, assignmentSlug);
|
|
2142
2231
|
if (!filePath || !await fileExists(filePath)) {
|
|
2143
2232
|
return null;
|
|
2144
2233
|
}
|
|
2145
2234
|
const content = await readFile5(filePath, "utf-8");
|
|
2146
|
-
const title = getEditableDocumentTitle(documentType,
|
|
2235
|
+
const title = getEditableDocumentTitle(documentType, projectSlug, assignmentSlug);
|
|
2147
2236
|
return {
|
|
2148
2237
|
documentType,
|
|
2149
2238
|
title,
|
|
2150
2239
|
content,
|
|
2151
|
-
|
|
2240
|
+
projectSlug,
|
|
2152
2241
|
assignmentSlug,
|
|
2153
2242
|
appendOnly: documentType === "handoff" || documentType === "decision-record"
|
|
2154
2243
|
};
|
|
2155
2244
|
}
|
|
2156
|
-
async function
|
|
2157
|
-
const
|
|
2158
|
-
const
|
|
2159
|
-
if (!await fileExists(
|
|
2245
|
+
async function getProjectDetail(projectsDir, slug) {
|
|
2246
|
+
const projectPath = resolve6(projectsDir, slug);
|
|
2247
|
+
const projectMdPath = resolve6(projectPath, "project.md");
|
|
2248
|
+
if (!await fileExists(projectMdPath)) {
|
|
2160
2249
|
return null;
|
|
2161
2250
|
}
|
|
2162
|
-
const
|
|
2163
|
-
const
|
|
2164
|
-
const assignments = await listAssignmentRecords(
|
|
2165
|
-
const rollup =
|
|
2166
|
-
const dependencyGraph = await loadDependencyGraph(
|
|
2167
|
-
const resources = await listResources(
|
|
2168
|
-
const memories = await listMemories(
|
|
2169
|
-
const updated =
|
|
2251
|
+
const projectContent = await readFile5(projectMdPath, "utf-8");
|
|
2252
|
+
const project = parseProject(projectContent);
|
|
2253
|
+
const assignments = await listAssignmentRecords(projectPath);
|
|
2254
|
+
const rollup = buildProjectRollup(project, assignments);
|
|
2255
|
+
const dependencyGraph = await loadDependencyGraph(projectPath, assignments);
|
|
2256
|
+
const resources = await listResources(projectPath);
|
|
2257
|
+
const memories = await listMemories(projectPath);
|
|
2258
|
+
const updated = getProjectActivityTimestamp(project.updated, assignments);
|
|
2170
2259
|
return {
|
|
2171
|
-
slug:
|
|
2172
|
-
title:
|
|
2260
|
+
slug: project.slug || slug,
|
|
2261
|
+
title: project.title,
|
|
2173
2262
|
status: rollup.status,
|
|
2174
|
-
statusOverride:
|
|
2175
|
-
archived:
|
|
2176
|
-
archivedAt:
|
|
2177
|
-
archivedReason:
|
|
2178
|
-
created:
|
|
2263
|
+
statusOverride: project.statusOverride,
|
|
2264
|
+
archived: project.archived,
|
|
2265
|
+
archivedAt: project.archivedAt,
|
|
2266
|
+
archivedReason: project.archivedReason,
|
|
2267
|
+
created: project.created,
|
|
2179
2268
|
updated,
|
|
2180
|
-
tags:
|
|
2181
|
-
body:
|
|
2269
|
+
tags: project.tags,
|
|
2270
|
+
body: project.body,
|
|
2182
2271
|
progress: rollup.progress,
|
|
2183
2272
|
needsAttention: rollup.needsAttention,
|
|
2184
2273
|
assignments: assignments.map(toAssignmentSummary).sort((left, right) => compareTimestamps(right.updated, left.updated)),
|
|
2185
2274
|
resources,
|
|
2186
2275
|
memories,
|
|
2187
2276
|
dependencyGraph,
|
|
2188
|
-
workspace:
|
|
2277
|
+
workspace: project.workspace
|
|
2189
2278
|
};
|
|
2190
2279
|
}
|
|
2191
|
-
async function getAssignmentDetail(
|
|
2192
|
-
const assignmentDir = resolve6(
|
|
2280
|
+
async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
2281
|
+
const assignmentDir = resolve6(projectsDir, projectSlug, "assignments", assignmentSlug);
|
|
2193
2282
|
const assignmentMdPath = resolve6(assignmentDir, "assignment.md");
|
|
2194
2283
|
if (!await fileExists(assignmentMdPath)) {
|
|
2195
2284
|
return null;
|
|
@@ -2241,7 +2330,7 @@ async function getAssignmentDetail(missionsDir, missionSlug, assignmentSlug) {
|
|
|
2241
2330
|
}
|
|
2242
2331
|
const detail = {
|
|
2243
2332
|
id: assignment.id,
|
|
2244
|
-
|
|
2333
|
+
projectSlug,
|
|
2245
2334
|
slug: assignment.slug || assignmentSlug,
|
|
2246
2335
|
title: assignment.title,
|
|
2247
2336
|
status: assignment.status,
|
|
@@ -2263,16 +2352,16 @@ async function getAssignmentDetail(missionsDir, missionSlug, assignmentSlug) {
|
|
|
2263
2352
|
handoff,
|
|
2264
2353
|
decisionRecord,
|
|
2265
2354
|
availableTransitions: await getAvailableTransitions(
|
|
2266
|
-
|
|
2267
|
-
|
|
2355
|
+
projectsDir,
|
|
2356
|
+
projectSlug,
|
|
2268
2357
|
assignmentSlug,
|
|
2269
2358
|
assignment
|
|
2270
2359
|
)
|
|
2271
2360
|
};
|
|
2272
|
-
const selfSlug = `${
|
|
2273
|
-
const
|
|
2361
|
+
const selfSlug = `${projectSlug}/${detail.slug}`;
|
|
2362
|
+
const projectRecords = await listProjectRecords(projectsDir);
|
|
2274
2363
|
const reverseLinks = [];
|
|
2275
|
-
for (const mr of
|
|
2364
|
+
for (const mr of projectRecords) {
|
|
2276
2365
|
for (const a of mr.assignments) {
|
|
2277
2366
|
const qualifiedSlug = `${mr.summary.slug}/${a.slug}`;
|
|
2278
2367
|
if (qualifiedSlug === selfSlug) continue;
|
|
@@ -2290,10 +2379,10 @@ async function getAssignmentDetail(missionsDir, missionSlug, assignmentSlug) {
|
|
|
2290
2379
|
const dedupedReverseLinks = reverseLinks.filter((l) => !forwardSet.has(l));
|
|
2291
2380
|
detail.links = forwardLinks;
|
|
2292
2381
|
detail.reverseLinks = dedupedReverseLinks;
|
|
2293
|
-
const
|
|
2294
|
-
for (const mr of
|
|
2382
|
+
const allProjectAssignments = /* @__PURE__ */ new Map();
|
|
2383
|
+
for (const mr of projectRecords) {
|
|
2295
2384
|
for (const a of mr.assignments) {
|
|
2296
|
-
|
|
2385
|
+
allProjectAssignments.set(`${mr.summary.slug}/${a.slug}`, {
|
|
2297
2386
|
title: a.title,
|
|
2298
2387
|
status: a.status
|
|
2299
2388
|
});
|
|
@@ -2302,10 +2391,10 @@ async function getAssignmentDetail(missionsDir, missionSlug, assignmentSlug) {
|
|
|
2302
2391
|
const enrichedLinks = [];
|
|
2303
2392
|
for (const linkSlug of forwardLinks) {
|
|
2304
2393
|
const [ms, as] = linkSlug.split("/");
|
|
2305
|
-
const info =
|
|
2394
|
+
const info = allProjectAssignments.get(linkSlug);
|
|
2306
2395
|
enrichedLinks.push({
|
|
2307
2396
|
slug: linkSlug,
|
|
2308
|
-
|
|
2397
|
+
projectSlug: ms,
|
|
2309
2398
|
assignmentSlug: as,
|
|
2310
2399
|
title: info?.title ?? linkSlug,
|
|
2311
2400
|
status: info?.status ?? "pending",
|
|
@@ -2314,10 +2403,10 @@ async function getAssignmentDetail(missionsDir, missionSlug, assignmentSlug) {
|
|
|
2314
2403
|
}
|
|
2315
2404
|
for (const linkSlug of dedupedReverseLinks) {
|
|
2316
2405
|
const [ms, as] = linkSlug.split("/");
|
|
2317
|
-
const info =
|
|
2406
|
+
const info = allProjectAssignments.get(linkSlug);
|
|
2318
2407
|
enrichedLinks.push({
|
|
2319
2408
|
slug: linkSlug,
|
|
2320
|
-
|
|
2409
|
+
projectSlug: ms,
|
|
2321
2410
|
assignmentSlug: as,
|
|
2322
2411
|
title: info?.title ?? linkSlug,
|
|
2323
2412
|
status: info?.status ?? "pending",
|
|
@@ -2327,51 +2416,51 @@ async function getAssignmentDetail(missionsDir, missionSlug, assignmentSlug) {
|
|
|
2327
2416
|
detail.enrichedLinks = enrichedLinks;
|
|
2328
2417
|
return detail;
|
|
2329
2418
|
}
|
|
2330
|
-
async function
|
|
2331
|
-
if (!await fileExists(
|
|
2419
|
+
async function listProjectRecords(projectsDir) {
|
|
2420
|
+
if (!await fileExists(projectsDir)) {
|
|
2332
2421
|
return [];
|
|
2333
2422
|
}
|
|
2334
|
-
const entries = await readdir3(
|
|
2335
|
-
const
|
|
2423
|
+
const entries = await readdir3(projectsDir, { withFileTypes: true });
|
|
2424
|
+
const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
|
|
2336
2425
|
const records = [];
|
|
2337
|
-
for (const entry of
|
|
2338
|
-
const
|
|
2339
|
-
const
|
|
2340
|
-
if (!await fileExists(
|
|
2426
|
+
for (const entry of projectDirs) {
|
|
2427
|
+
const projectPath = resolve6(projectsDir, entry.name);
|
|
2428
|
+
const projectMdPath = resolve6(projectPath, "project.md");
|
|
2429
|
+
if (!await fileExists(projectMdPath)) {
|
|
2341
2430
|
continue;
|
|
2342
2431
|
}
|
|
2343
|
-
const
|
|
2344
|
-
const
|
|
2345
|
-
const assignments = await listAssignmentRecords(
|
|
2346
|
-
const rollup =
|
|
2347
|
-
const updated =
|
|
2432
|
+
const projectContent = await readFile5(projectMdPath, "utf-8");
|
|
2433
|
+
const project = parseProject(projectContent);
|
|
2434
|
+
const assignments = await listAssignmentRecords(projectPath);
|
|
2435
|
+
const rollup = buildProjectRollup(project, assignments);
|
|
2436
|
+
const updated = getProjectActivityTimestamp(project.updated, assignments);
|
|
2348
2437
|
records.push({
|
|
2349
|
-
|
|
2350
|
-
|
|
2438
|
+
projectPath,
|
|
2439
|
+
project,
|
|
2351
2440
|
assignments,
|
|
2352
|
-
dependencyGraph: await loadDependencyGraph(
|
|
2441
|
+
dependencyGraph: await loadDependencyGraph(projectPath, assignments),
|
|
2353
2442
|
summary: {
|
|
2354
|
-
slug:
|
|
2355
|
-
title:
|
|
2443
|
+
slug: project.slug || entry.name,
|
|
2444
|
+
title: project.title,
|
|
2356
2445
|
status: rollup.status,
|
|
2357
|
-
statusOverride:
|
|
2358
|
-
archived:
|
|
2359
|
-
archivedAt:
|
|
2360
|
-
archivedReason:
|
|
2361
|
-
created:
|
|
2446
|
+
statusOverride: project.statusOverride,
|
|
2447
|
+
archived: project.archived,
|
|
2448
|
+
archivedAt: project.archivedAt,
|
|
2449
|
+
archivedReason: project.archivedReason,
|
|
2450
|
+
created: project.created,
|
|
2362
2451
|
updated,
|
|
2363
|
-
tags:
|
|
2452
|
+
tags: project.tags,
|
|
2364
2453
|
progress: rollup.progress,
|
|
2365
2454
|
needsAttention: rollup.needsAttention,
|
|
2366
|
-
workspace:
|
|
2455
|
+
workspace: project.workspace
|
|
2367
2456
|
}
|
|
2368
2457
|
});
|
|
2369
2458
|
}
|
|
2370
2459
|
records.sort((left, right) => compareTimestamps(right.summary.updated, left.summary.updated));
|
|
2371
2460
|
return records;
|
|
2372
2461
|
}
|
|
2373
|
-
async function listAssignmentRecords(
|
|
2374
|
-
const assignmentsDir = resolve6(
|
|
2462
|
+
async function listAssignmentRecords(projectPath) {
|
|
2463
|
+
const assignmentsDir = resolve6(projectPath, "assignments");
|
|
2375
2464
|
if (!await fileExists(assignmentsDir)) {
|
|
2376
2465
|
return [];
|
|
2377
2466
|
}
|
|
@@ -2391,8 +2480,8 @@ async function listAssignmentRecords(missionPath) {
|
|
|
2391
2480
|
records.sort((left, right) => compareTimestamps(right.updated, left.updated));
|
|
2392
2481
|
return records;
|
|
2393
2482
|
}
|
|
2394
|
-
async function listResources(
|
|
2395
|
-
const resourcesDir = resolve6(
|
|
2483
|
+
async function listResources(projectPath) {
|
|
2484
|
+
const resourcesDir = resolve6(projectPath, "resources");
|
|
2396
2485
|
if (!await fileExists(resourcesDir)) {
|
|
2397
2486
|
return [];
|
|
2398
2487
|
}
|
|
@@ -2417,8 +2506,8 @@ async function listResources(missionPath) {
|
|
|
2417
2506
|
results.sort((left, right) => compareTimestamps(right.updated, left.updated));
|
|
2418
2507
|
return results;
|
|
2419
2508
|
}
|
|
2420
|
-
async function listMemories(
|
|
2421
|
-
const memoriesDir = resolve6(
|
|
2509
|
+
async function listMemories(projectPath) {
|
|
2510
|
+
const memoriesDir = resolve6(projectPath, "memories");
|
|
2422
2511
|
if (!await fileExists(memoriesDir)) {
|
|
2423
2512
|
return [];
|
|
2424
2513
|
}
|
|
@@ -2443,8 +2532,8 @@ async function listMemories(missionPath) {
|
|
|
2443
2532
|
results.sort((left, right) => compareTimestamps(right.updated, left.updated));
|
|
2444
2533
|
return results;
|
|
2445
2534
|
}
|
|
2446
|
-
async function loadDependencyGraph(
|
|
2447
|
-
const statusPath = resolve6(
|
|
2535
|
+
async function loadDependencyGraph(projectPath, assignments) {
|
|
2536
|
+
const statusPath = resolve6(projectPath, "_status.md");
|
|
2448
2537
|
if (await fileExists(statusPath)) {
|
|
2449
2538
|
const statusContent = await readFile5(statusPath, "utf-8");
|
|
2450
2539
|
const parsed = parseStatus(statusContent);
|
|
@@ -2455,23 +2544,23 @@ async function loadDependencyGraph(missionPath, assignments) {
|
|
|
2455
2544
|
}
|
|
2456
2545
|
return buildDependencyGraph(assignments);
|
|
2457
2546
|
}
|
|
2458
|
-
function
|
|
2547
|
+
function buildProjectRollup(project, assignments) {
|
|
2459
2548
|
const progress = { total: assignments.length };
|
|
2460
|
-
let
|
|
2549
|
+
let openQuestions = 0;
|
|
2461
2550
|
for (const assignment of assignments) {
|
|
2462
2551
|
const s = assignment.status;
|
|
2463
2552
|
progress[s] = (progress[s] ?? 0) + 1;
|
|
2464
|
-
|
|
2553
|
+
openQuestions += countPendingAnswers(assignment.body);
|
|
2465
2554
|
}
|
|
2466
2555
|
const needsAttention = {
|
|
2467
2556
|
blockedCount: progress["blocked"] ?? 0,
|
|
2468
2557
|
failedCount: progress["failed"] ?? 0,
|
|
2469
|
-
|
|
2558
|
+
openQuestions
|
|
2470
2559
|
};
|
|
2471
2560
|
let status = "pending";
|
|
2472
|
-
if (
|
|
2473
|
-
status =
|
|
2474
|
-
} else if (
|
|
2561
|
+
if (project.statusOverride) {
|
|
2562
|
+
status = project.statusOverride;
|
|
2563
|
+
} else if (project.archived) {
|
|
2475
2564
|
status = "archived";
|
|
2476
2565
|
} else if (progress.total > 0 && (progress["completed"] ?? 0) === progress.total) {
|
|
2477
2566
|
status = "completed";
|
|
@@ -2501,16 +2590,16 @@ function toAssignmentSummary(assignment) {
|
|
|
2501
2590
|
updated: assignment.updated
|
|
2502
2591
|
};
|
|
2503
2592
|
}
|
|
2504
|
-
async function toAssignmentBoardItem(
|
|
2593
|
+
async function toAssignmentBoardItem(projectsDir, projectRecord, assignment) {
|
|
2505
2594
|
return {
|
|
2506
2595
|
...toAssignmentSummary(assignment),
|
|
2507
|
-
|
|
2508
|
-
|
|
2596
|
+
projectSlug: projectRecord.summary.slug,
|
|
2597
|
+
projectTitle: projectRecord.summary.title,
|
|
2509
2598
|
blockedReason: assignment.blockedReason,
|
|
2510
|
-
|
|
2599
|
+
projectWorkspace: projectRecord.project.workspace,
|
|
2511
2600
|
availableTransitions: await getAvailableTransitions(
|
|
2512
|
-
|
|
2513
|
-
|
|
2601
|
+
projectsDir,
|
|
2602
|
+
projectRecord.summary.slug,
|
|
2514
2603
|
assignment.slug,
|
|
2515
2604
|
assignment
|
|
2516
2605
|
)
|
|
@@ -2542,18 +2631,18 @@ function buildDependencyGraph(assignments) {
|
|
|
2542
2631
|
function findAssignmentStatus(assignments, slug) {
|
|
2543
2632
|
return assignments.find((assignment) => assignment.slug === slug)?.status ?? "pending";
|
|
2544
2633
|
}
|
|
2545
|
-
async function getAvailableTransitions(
|
|
2634
|
+
async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug, assignment) {
|
|
2546
2635
|
const config = await getStatusConfig();
|
|
2547
2636
|
const transitionDefs = getTransitionDefinitions(config);
|
|
2548
2637
|
const actions = [];
|
|
2549
|
-
const
|
|
2638
|
+
const projectPath = resolve6(projectsDir, projectSlug);
|
|
2550
2639
|
for (const definition of transitionDefs) {
|
|
2551
2640
|
let warning = null;
|
|
2552
2641
|
if (definition.command === "start" && !assignment.assignee) {
|
|
2553
2642
|
warning = "No assignee set \u2014 consider assigning before starting.";
|
|
2554
2643
|
}
|
|
2555
2644
|
if (definition.command === "start" && assignment.dependsOn.length > 0) {
|
|
2556
|
-
const unmetDependencies = await getUnmetDependencies(
|
|
2645
|
+
const unmetDependencies = await getUnmetDependencies(projectPath, assignment.dependsOn, config.terminalStatuses);
|
|
2557
2646
|
if (unmetDependencies.length > 0) {
|
|
2558
2647
|
warning = `Unmet dependencies: ${unmetDependencies.join(", ")}.`;
|
|
2559
2648
|
}
|
|
@@ -2572,11 +2661,11 @@ async function getAvailableTransitions(missionsDir, missionSlug, assignmentSlug,
|
|
|
2572
2661
|
}
|
|
2573
2662
|
return actions;
|
|
2574
2663
|
}
|
|
2575
|
-
async function getUnmetDependencies(
|
|
2664
|
+
async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses) {
|
|
2576
2665
|
const terminals = terminalStatuses ?? /* @__PURE__ */ new Set(["completed"]);
|
|
2577
2666
|
const unmet = [];
|
|
2578
2667
|
for (const dependency of dependsOn) {
|
|
2579
|
-
const dependencyPath = resolve6(
|
|
2668
|
+
const dependencyPath = resolve6(projectPath, "assignments", dependency, "assignment.md");
|
|
2580
2669
|
if (!await fileExists(dependencyPath)) {
|
|
2581
2670
|
unmet.push(`${dependency} (missing)`);
|
|
2582
2671
|
continue;
|
|
@@ -2589,19 +2678,19 @@ async function getUnmetDependencies(missionPath, dependsOn, terminalStatuses) {
|
|
|
2589
2678
|
}
|
|
2590
2679
|
return unmet;
|
|
2591
2680
|
}
|
|
2592
|
-
function buildAttentionItems(
|
|
2681
|
+
function buildAttentionItems(projectRecords) {
|
|
2593
2682
|
const items = [];
|
|
2594
|
-
for (const record of
|
|
2683
|
+
for (const record of projectRecords) {
|
|
2595
2684
|
for (const assignment of record.assignments) {
|
|
2596
2685
|
const stale = isStale(assignment.updated);
|
|
2597
2686
|
const base = {
|
|
2598
|
-
|
|
2599
|
-
|
|
2687
|
+
projectSlug: record.summary.slug,
|
|
2688
|
+
projectTitle: record.summary.title,
|
|
2600
2689
|
assignmentSlug: assignment.slug,
|
|
2601
2690
|
assignmentTitle: assignment.title,
|
|
2602
2691
|
status: assignment.status,
|
|
2603
2692
|
updated: assignment.updated,
|
|
2604
|
-
href: `/
|
|
2693
|
+
href: `/projects/${record.summary.slug}/assignments/${assignment.slug}`,
|
|
2605
2694
|
blockedReason: assignment.blockedReason,
|
|
2606
2695
|
stale
|
|
2607
2696
|
};
|
|
@@ -2641,19 +2730,19 @@ function buildAttentionItems(missionRecords) {
|
|
|
2641
2730
|
}
|
|
2642
2731
|
return items.sort(compareAttentionItems);
|
|
2643
2732
|
}
|
|
2644
|
-
function buildRecentActivity(
|
|
2733
|
+
function buildRecentActivity(projectRecords) {
|
|
2645
2734
|
const activity = [];
|
|
2646
|
-
for (const record of
|
|
2735
|
+
for (const record of projectRecords) {
|
|
2647
2736
|
activity.push({
|
|
2648
|
-
id: `
|
|
2649
|
-
type: "
|
|
2737
|
+
id: `project:${record.summary.slug}`,
|
|
2738
|
+
type: "project",
|
|
2650
2739
|
title: record.summary.title,
|
|
2651
2740
|
updated: record.summary.updated,
|
|
2652
|
-
href: `/
|
|
2653
|
-
|
|
2654
|
-
|
|
2741
|
+
href: `/projects/${record.summary.slug}`,
|
|
2742
|
+
projectSlug: record.summary.slug,
|
|
2743
|
+
projectTitle: record.summary.title,
|
|
2655
2744
|
assignmentSlug: null,
|
|
2656
|
-
summary: `
|
|
2745
|
+
summary: `Project status is ${record.summary.status}.`
|
|
2657
2746
|
});
|
|
2658
2747
|
for (const assignment of record.assignments) {
|
|
2659
2748
|
activity.push({
|
|
@@ -2661,9 +2750,9 @@ function buildRecentActivity(missionRecords) {
|
|
|
2661
2750
|
type: "assignment",
|
|
2662
2751
|
title: assignment.title,
|
|
2663
2752
|
updated: assignment.updated,
|
|
2664
|
-
href: `/
|
|
2665
|
-
|
|
2666
|
-
|
|
2753
|
+
href: `/projects/${record.summary.slug}/assignments/${assignment.slug}`,
|
|
2754
|
+
projectSlug: record.summary.slug,
|
|
2755
|
+
projectTitle: record.summary.title,
|
|
2667
2756
|
assignmentSlug: assignment.slug,
|
|
2668
2757
|
summary: `Assignment is ${assignment.status} with ${assignment.priority} priority.`
|
|
2669
2758
|
});
|
|
@@ -2698,8 +2787,8 @@ function countPendingAnswers(body) {
|
|
|
2698
2787
|
const matches = body.match(/^\*\*A:\*\*\s+pending\s*$/gim);
|
|
2699
2788
|
return matches ? matches.length : 0;
|
|
2700
2789
|
}
|
|
2701
|
-
function
|
|
2702
|
-
let latest =
|
|
2790
|
+
function getProjectActivityTimestamp(projectUpdated, assignments) {
|
|
2791
|
+
let latest = projectUpdated;
|
|
2703
2792
|
for (const assignment of assignments) {
|
|
2704
2793
|
if (compareTimestamps(assignment.updated, latest) > 0) {
|
|
2705
2794
|
latest = assignment.updated;
|
|
@@ -2707,28 +2796,28 @@ function getMissionActivityTimestamp(missionUpdated, assignments) {
|
|
|
2707
2796
|
}
|
|
2708
2797
|
return latest;
|
|
2709
2798
|
}
|
|
2710
|
-
function getDocumentPath(
|
|
2799
|
+
function getDocumentPath(projectsDir, documentType, projectSlug, assignmentSlug) {
|
|
2711
2800
|
switch (documentType) {
|
|
2712
|
-
case "
|
|
2713
|
-
return resolve6(
|
|
2801
|
+
case "project":
|
|
2802
|
+
return resolve6(projectsDir, projectSlug, "project.md");
|
|
2714
2803
|
case "assignment":
|
|
2715
|
-
return assignmentSlug ? resolve6(
|
|
2804
|
+
return assignmentSlug ? resolve6(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
|
|
2716
2805
|
case "plan":
|
|
2717
|
-
return assignmentSlug ? resolve6(
|
|
2806
|
+
return assignmentSlug ? resolve6(projectsDir, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
|
|
2718
2807
|
case "scratchpad":
|
|
2719
|
-
return assignmentSlug ? resolve6(
|
|
2808
|
+
return assignmentSlug ? resolve6(projectsDir, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
|
|
2720
2809
|
case "handoff":
|
|
2721
|
-
return assignmentSlug ? resolve6(
|
|
2810
|
+
return assignmentSlug ? resolve6(projectsDir, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
|
|
2722
2811
|
case "decision-record":
|
|
2723
|
-
return assignmentSlug ? resolve6(
|
|
2812
|
+
return assignmentSlug ? resolve6(projectsDir, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
|
|
2724
2813
|
default:
|
|
2725
2814
|
return null;
|
|
2726
2815
|
}
|
|
2727
2816
|
}
|
|
2728
|
-
function getEditableDocumentTitle(documentType,
|
|
2817
|
+
function getEditableDocumentTitle(documentType, projectSlug, assignmentSlug) {
|
|
2729
2818
|
switch (documentType) {
|
|
2730
|
-
case "
|
|
2731
|
-
return `Edit
|
|
2819
|
+
case "project":
|
|
2820
|
+
return `Edit Project: ${projectSlug}`;
|
|
2732
2821
|
case "assignment":
|
|
2733
2822
|
return `Edit Assignment: ${assignmentSlug || "assignment"}`;
|
|
2734
2823
|
case "plan":
|
|
@@ -2740,18 +2829,18 @@ function getEditableDocumentTitle(documentType, missionSlug, assignmentSlug) {
|
|
|
2740
2829
|
case "decision-record":
|
|
2741
2830
|
return `Append Decision: ${assignmentSlug || "assignment"}`;
|
|
2742
2831
|
case "playbook":
|
|
2743
|
-
return `Edit Playbook: ${
|
|
2832
|
+
return `Edit Playbook: ${projectSlug}`;
|
|
2744
2833
|
default:
|
|
2745
|
-
return
|
|
2834
|
+
return projectSlug;
|
|
2746
2835
|
}
|
|
2747
2836
|
}
|
|
2748
|
-
async function listPlaybooks(
|
|
2749
|
-
if (!await fileExists(
|
|
2750
|
-
const entries = await readdir3(
|
|
2837
|
+
async function listPlaybooks(playbooksDir2) {
|
|
2838
|
+
if (!await fileExists(playbooksDir2)) return [];
|
|
2839
|
+
const entries = await readdir3(playbooksDir2, { withFileTypes: true });
|
|
2751
2840
|
const playbooks = [];
|
|
2752
2841
|
for (const entry of entries) {
|
|
2753
2842
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
|
|
2754
|
-
const filePath = resolve6(
|
|
2843
|
+
const filePath = resolve6(playbooksDir2, entry.name);
|
|
2755
2844
|
const raw = await readFile5(filePath, "utf-8");
|
|
2756
2845
|
const parsed = parsePlaybook(raw);
|
|
2757
2846
|
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
@@ -2767,8 +2856,8 @@ async function listPlaybooks(playbooksDir) {
|
|
|
2767
2856
|
}
|
|
2768
2857
|
return playbooks.sort((a, b) => (b.updated || b.created).localeCompare(a.updated || a.created));
|
|
2769
2858
|
}
|
|
2770
|
-
async function getPlaybookDetail(
|
|
2771
|
-
const filePath = resolve6(
|
|
2859
|
+
async function getPlaybookDetail(playbooksDir2, slug) {
|
|
2860
|
+
const filePath = resolve6(playbooksDir2, `${slug}.md`);
|
|
2772
2861
|
if (!await fileExists(filePath)) return null;
|
|
2773
2862
|
const raw = await readFile5(filePath, "utf-8");
|
|
2774
2863
|
const parsed = parsePlaybook(raw);
|
|
@@ -2783,7 +2872,7 @@ async function getPlaybookDetail(playbooksDir, slug) {
|
|
|
2783
2872
|
body: parsed.body
|
|
2784
2873
|
};
|
|
2785
2874
|
}
|
|
2786
|
-
var STALE_ASSIGNMENT_MS, ATTENTION_PAGE_LIMIT, OVERVIEW_ATTENTION_LIMIT,
|
|
2875
|
+
var STALE_ASSIGNMENT_MS, ATTENTION_PAGE_LIMIT, OVERVIEW_ATTENTION_LIMIT, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, DEFAULT_TRANSITION_DEFINITIONS, DEFAULT_STATUS_COLORS, _cachedConfig, DEFAULT_GRAPH_COLORS;
|
|
2787
2876
|
var init_api = __esm({
|
|
2788
2877
|
"src/dashboard/api.ts"() {
|
|
2789
2878
|
"use strict";
|
|
@@ -2795,7 +2884,7 @@ var init_api = __esm({
|
|
|
2795
2884
|
STALE_ASSIGNMENT_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2796
2885
|
ATTENTION_PAGE_LIMIT = 50;
|
|
2797
2886
|
OVERVIEW_ATTENTION_LIMIT = 6;
|
|
2798
|
-
|
|
2887
|
+
RECENT_PROJECTS_LIMIT = 6;
|
|
2799
2888
|
RECENT_ACTIVITY_LIMIT = 12;
|
|
2800
2889
|
DEFAULT_TRANSITION_DEFINITIONS = [
|
|
2801
2890
|
{
|
|
@@ -3042,13 +3131,13 @@ function serializeLogEntry(entry) {
|
|
|
3042
3131
|
if (entry.status) lines.push(`**Status:** ${entry.status}`);
|
|
3043
3132
|
return lines.join("\n");
|
|
3044
3133
|
}
|
|
3045
|
-
function checklistPath(
|
|
3046
|
-
return resolve13(
|
|
3134
|
+
function checklistPath(todosDir2, workspace) {
|
|
3135
|
+
return resolve13(todosDir2, `${workspace}.md`);
|
|
3047
3136
|
}
|
|
3048
|
-
function logPath(
|
|
3049
|
-
return resolve13(
|
|
3137
|
+
function logPath(todosDir2, workspace) {
|
|
3138
|
+
return resolve13(todosDir2, `${workspace}-log.md`);
|
|
3050
3139
|
}
|
|
3051
|
-
function archivePath(
|
|
3140
|
+
function archivePath(todosDir2, workspace, interval, now = /* @__PURE__ */ new Date()) {
|
|
3052
3141
|
const year = now.getFullYear();
|
|
3053
3142
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
3054
3143
|
const day = String(now.getDate()).padStart(2, "0");
|
|
@@ -3070,32 +3159,32 @@ function archivePath(todosDir, workspace, interval, now = /* @__PURE__ */ new Da
|
|
|
3070
3159
|
default:
|
|
3071
3160
|
suffix = `${year}-${month}-${day}`;
|
|
3072
3161
|
}
|
|
3073
|
-
return resolve13(
|
|
3162
|
+
return resolve13(todosDir2, "archive", `${workspace}-${suffix}.md`);
|
|
3074
3163
|
}
|
|
3075
|
-
async function readChecklist(
|
|
3076
|
-
const path = checklistPath(
|
|
3164
|
+
async function readChecklist(todosDir2, workspace) {
|
|
3165
|
+
const path = checklistPath(todosDir2, workspace);
|
|
3077
3166
|
if (!await fileExists(path)) {
|
|
3078
3167
|
return { workspace, archiveInterval: "weekly", items: [] };
|
|
3079
3168
|
}
|
|
3080
3169
|
const content = await readFile10(path, "utf-8");
|
|
3081
3170
|
return parseChecklist(content);
|
|
3082
3171
|
}
|
|
3083
|
-
async function writeChecklist(
|
|
3084
|
-
await ensureDir(
|
|
3085
|
-
const path = checklistPath(
|
|
3172
|
+
async function writeChecklist(todosDir2, checklist) {
|
|
3173
|
+
await ensureDir(todosDir2);
|
|
3174
|
+
const path = checklistPath(todosDir2, checklist.workspace);
|
|
3086
3175
|
await writeFileForce(path, serializeChecklist(checklist));
|
|
3087
3176
|
}
|
|
3088
|
-
async function readLog(
|
|
3089
|
-
const path = logPath(
|
|
3177
|
+
async function readLog(todosDir2, workspace) {
|
|
3178
|
+
const path = logPath(todosDir2, workspace);
|
|
3090
3179
|
if (!await fileExists(path)) {
|
|
3091
3180
|
return { workspace, entries: [] };
|
|
3092
3181
|
}
|
|
3093
3182
|
const content = await readFile10(path, "utf-8");
|
|
3094
3183
|
return parseLog(content);
|
|
3095
3184
|
}
|
|
3096
|
-
async function appendLogEntry2(
|
|
3097
|
-
await ensureDir(
|
|
3098
|
-
const path = logPath(
|
|
3185
|
+
async function appendLogEntry2(todosDir2, workspace, entry) {
|
|
3186
|
+
await ensureDir(todosDir2);
|
|
3187
|
+
const path = logPath(todosDir2, workspace);
|
|
3099
3188
|
let content;
|
|
3100
3189
|
if (await fileExists(path)) {
|
|
3101
3190
|
content = await readFile10(path, "utf-8");
|
|
@@ -3132,46 +3221,46 @@ var init_parser2 = __esm({
|
|
|
3132
3221
|
});
|
|
3133
3222
|
|
|
3134
3223
|
// src/dashboard/server.ts
|
|
3224
|
+
init_paths();
|
|
3135
3225
|
init_api();
|
|
3136
3226
|
import express from "express";
|
|
3137
3227
|
import { createServer } from "http";
|
|
3138
|
-
import { resolve as
|
|
3139
|
-
import {
|
|
3140
|
-
import { writeFile as writeFile3, unlink as unlink3 } from "fs/promises";
|
|
3228
|
+
import { resolve as resolve15 } from "path";
|
|
3229
|
+
import { writeFile as writeFile4, unlink as unlink4 } from "fs/promises";
|
|
3141
3230
|
import { WebSocketServer, WebSocket } from "ws";
|
|
3142
3231
|
|
|
3143
3232
|
// src/dashboard/watcher.ts
|
|
3144
3233
|
import { watch } from "chokidar";
|
|
3145
3234
|
import { relative, sep } from "path";
|
|
3146
3235
|
function createWatcher(options) {
|
|
3147
|
-
const {
|
|
3236
|
+
const { projectsDir, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, onMessage, debounceMs = 300 } = options;
|
|
3148
3237
|
const pendingEvents = /* @__PURE__ */ new Map();
|
|
3149
|
-
const
|
|
3238
|
+
const projectsWatcher = watch(projectsDir, {
|
|
3150
3239
|
ignoreInitial: true,
|
|
3151
3240
|
persistent: true,
|
|
3152
3241
|
depth: 10,
|
|
3153
3242
|
ignored: /(^|[\/\\])\../
|
|
3154
3243
|
});
|
|
3155
|
-
function
|
|
3156
|
-
const rel = relative(
|
|
3244
|
+
function handleProjectChange(filePath) {
|
|
3245
|
+
const rel = relative(projectsDir, filePath);
|
|
3157
3246
|
const parts = rel.split(sep);
|
|
3158
3247
|
if (parts.length === 0) return;
|
|
3159
|
-
const
|
|
3248
|
+
const projectSlug = parts[0];
|
|
3160
3249
|
let assignmentSlug;
|
|
3161
3250
|
if (parts.length >= 3 && parts[1] === "assignments") {
|
|
3162
3251
|
assignmentSlug = parts[2];
|
|
3163
3252
|
}
|
|
3164
|
-
const debounceKey = assignmentSlug ? `${
|
|
3253
|
+
const debounceKey = assignmentSlug ? `${projectSlug}/${assignmentSlug}` : projectSlug;
|
|
3165
3254
|
const existing = pendingEvents.get(debounceKey);
|
|
3166
3255
|
if (existing) clearTimeout(existing);
|
|
3167
|
-
const messageType = assignmentSlug ? "assignment-updated" : "
|
|
3256
|
+
const messageType = assignmentSlug ? "assignment-updated" : "project-updated";
|
|
3168
3257
|
pendingEvents.set(
|
|
3169
3258
|
debounceKey,
|
|
3170
3259
|
setTimeout(() => {
|
|
3171
3260
|
pendingEvents.delete(debounceKey);
|
|
3172
3261
|
const message = {
|
|
3173
3262
|
type: messageType,
|
|
3174
|
-
|
|
3263
|
+
projectSlug,
|
|
3175
3264
|
assignmentSlug,
|
|
3176
3265
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3177
3266
|
};
|
|
@@ -3179,11 +3268,11 @@ function createWatcher(options) {
|
|
|
3179
3268
|
}, debounceMs)
|
|
3180
3269
|
);
|
|
3181
3270
|
}
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3271
|
+
projectsWatcher.on("change", handleProjectChange);
|
|
3272
|
+
projectsWatcher.on("add", handleProjectChange);
|
|
3273
|
+
projectsWatcher.on("unlink", handleProjectChange);
|
|
3185
3274
|
let serversWatcher = null;
|
|
3186
|
-
if (
|
|
3275
|
+
if (serversDir2) {
|
|
3187
3276
|
let handleServerChange2 = function() {
|
|
3188
3277
|
const debounceKey = "__servers__";
|
|
3189
3278
|
const existing = pendingEvents.get(debounceKey);
|
|
@@ -3201,7 +3290,7 @@ function createWatcher(options) {
|
|
|
3201
3290
|
);
|
|
3202
3291
|
};
|
|
3203
3292
|
var handleServerChange = handleServerChange2;
|
|
3204
|
-
serversWatcher = watch(
|
|
3293
|
+
serversWatcher = watch(serversDir2, {
|
|
3205
3294
|
ignoreInitial: true,
|
|
3206
3295
|
persistent: true,
|
|
3207
3296
|
depth: 1,
|
|
@@ -3212,7 +3301,7 @@ function createWatcher(options) {
|
|
|
3212
3301
|
serversWatcher.on("unlink", handleServerChange2);
|
|
3213
3302
|
}
|
|
3214
3303
|
let playbooksWatcher = null;
|
|
3215
|
-
if (
|
|
3304
|
+
if (playbooksDir2) {
|
|
3216
3305
|
let handlePlaybookChange2 = function() {
|
|
3217
3306
|
const debounceKey = "__playbooks__";
|
|
3218
3307
|
const existing = pendingEvents.get(debounceKey);
|
|
@@ -3230,7 +3319,7 @@ function createWatcher(options) {
|
|
|
3230
3319
|
);
|
|
3231
3320
|
};
|
|
3232
3321
|
var handlePlaybookChange = handlePlaybookChange2;
|
|
3233
|
-
playbooksWatcher = watch(
|
|
3322
|
+
playbooksWatcher = watch(playbooksDir2, {
|
|
3234
3323
|
ignoreInitial: true,
|
|
3235
3324
|
persistent: true,
|
|
3236
3325
|
depth: 1,
|
|
@@ -3241,7 +3330,7 @@ function createWatcher(options) {
|
|
|
3241
3330
|
playbooksWatcher.on("unlink", handlePlaybookChange2);
|
|
3242
3331
|
}
|
|
3243
3332
|
let todosWatcher = null;
|
|
3244
|
-
if (
|
|
3333
|
+
if (todosDir2) {
|
|
3245
3334
|
let handleTodoChange2 = function() {
|
|
3246
3335
|
const debounceKey = "__todos__";
|
|
3247
3336
|
const existing = pendingEvents.get(debounceKey);
|
|
@@ -3259,7 +3348,7 @@ function createWatcher(options) {
|
|
|
3259
3348
|
);
|
|
3260
3349
|
};
|
|
3261
3350
|
var handleTodoChange = handleTodoChange2;
|
|
3262
|
-
todosWatcher = watch(
|
|
3351
|
+
todosWatcher = watch(todosDir2, {
|
|
3263
3352
|
ignoreInitial: true,
|
|
3264
3353
|
persistent: true,
|
|
3265
3354
|
depth: 1,
|
|
@@ -3275,7 +3364,7 @@ function createWatcher(options) {
|
|
|
3275
3364
|
clearTimeout(timeout);
|
|
3276
3365
|
});
|
|
3277
3366
|
pendingEvents.clear();
|
|
3278
|
-
await
|
|
3367
|
+
await projectsWatcher.close();
|
|
3279
3368
|
if (serversWatcher) await serversWatcher.close();
|
|
3280
3369
|
if (playbooksWatcher) await playbooksWatcher.close();
|
|
3281
3370
|
if (todosWatcher) await todosWatcher.close();
|
|
@@ -3363,15 +3452,15 @@ init_config();
|
|
|
3363
3452
|
// src/templates/manifest.ts
|
|
3364
3453
|
function renderManifest(params) {
|
|
3365
3454
|
return `---
|
|
3366
|
-
version: "
|
|
3367
|
-
|
|
3455
|
+
version: "2.0"
|
|
3456
|
+
project: ${params.slug}
|
|
3368
3457
|
generated: "${params.timestamp}"
|
|
3369
3458
|
---
|
|
3370
3459
|
|
|
3371
|
-
#
|
|
3460
|
+
# Project: ${params.slug}
|
|
3372
3461
|
|
|
3373
3462
|
## Overview
|
|
3374
|
-
- [
|
|
3463
|
+
- [Project Overview](./project.md)
|
|
3375
3464
|
|
|
3376
3465
|
## Indexes
|
|
3377
3466
|
- [Assignments](./_index-assignments.md)
|
|
@@ -3380,10 +3469,6 @@ generated: "${params.timestamp}"
|
|
|
3380
3469
|
- [Status](./_status.md)
|
|
3381
3470
|
- [Resources](./resources/_index.md)
|
|
3382
3471
|
- [Memories](./memories/_index.md)
|
|
3383
|
-
|
|
3384
|
-
## Config
|
|
3385
|
-
- [Agent Instructions](./agent.md)
|
|
3386
|
-
- [Claude Code Instructions](./claude.md)
|
|
3387
3472
|
`;
|
|
3388
3473
|
}
|
|
3389
3474
|
|
|
@@ -3398,8 +3483,8 @@ function escapeYamlString(value) {
|
|
|
3398
3483
|
return `"${escaped}"`;
|
|
3399
3484
|
}
|
|
3400
3485
|
|
|
3401
|
-
// src/templates/
|
|
3402
|
-
function
|
|
3486
|
+
// src/templates/project.ts
|
|
3487
|
+
function renderProject(params) {
|
|
3403
3488
|
const safeTitle = escapeYamlString(params.title);
|
|
3404
3489
|
const workspaceLine = params.workspace ? `
|
|
3405
3490
|
workspace: ${params.workspace}` : "";
|
|
@@ -3420,7 +3505,7 @@ tags: []${workspaceLine}
|
|
|
3420
3505
|
|
|
3421
3506
|
## Overview
|
|
3422
3507
|
|
|
3423
|
-
<!-- Describe the
|
|
3508
|
+
<!-- Describe the project goal, context, and success criteria here. -->
|
|
3424
3509
|
|
|
3425
3510
|
## Notes
|
|
3426
3511
|
|
|
@@ -3428,43 +3513,6 @@ tags: []${workspaceLine}
|
|
|
3428
3513
|
`;
|
|
3429
3514
|
}
|
|
3430
3515
|
|
|
3431
|
-
// src/templates/agent.ts
|
|
3432
|
-
function renderAgent(params) {
|
|
3433
|
-
return `---
|
|
3434
|
-
mission: ${params.slug}
|
|
3435
|
-
updated: "${params.timestamp}"
|
|
3436
|
-
---
|
|
3437
|
-
|
|
3438
|
-
# Agent Instructions
|
|
3439
|
-
|
|
3440
|
-
All agents working on this mission must follow these guidelines.
|
|
3441
|
-
|
|
3442
|
-
## Conventions
|
|
3443
|
-
|
|
3444
|
-
<!-- Coding conventions, naming standards, architectural patterns. -->
|
|
3445
|
-
|
|
3446
|
-
## Boundaries
|
|
3447
|
-
|
|
3448
|
-
<!-- What agents should NOT do. Files/systems that are off-limits. -->
|
|
3449
|
-
|
|
3450
|
-
## Resources
|
|
3451
|
-
|
|
3452
|
-
<!-- Links to key resources agents should consult. -->
|
|
3453
|
-
`;
|
|
3454
|
-
}
|
|
3455
|
-
|
|
3456
|
-
// src/templates/claude.ts
|
|
3457
|
-
function renderClaude(params) {
|
|
3458
|
-
return `# Claude Code Instructions \u2014 ${params.slug}
|
|
3459
|
-
|
|
3460
|
-
Read \`agent.md\` first for universal conventions and boundaries.
|
|
3461
|
-
|
|
3462
|
-
## Additional Claude Code Rules
|
|
3463
|
-
|
|
3464
|
-
<!-- Add Claude Code-specific rules here. -->
|
|
3465
|
-
`;
|
|
3466
|
-
}
|
|
3467
|
-
|
|
3468
3516
|
// src/templates/assignment.ts
|
|
3469
3517
|
function renderAssignment(params) {
|
|
3470
3518
|
const safeTitle = escapeYamlString(params.title);
|
|
@@ -3472,10 +3520,14 @@ function renderAssignment(params) {
|
|
|
3472
3520
|
- ${params.dependsOn.join("\n - ")}`;
|
|
3473
3521
|
const linksYaml = params.links.length === 0 ? "links: []" : `links:
|
|
3474
3522
|
- ${params.links.join("\n - ")}`;
|
|
3523
|
+
const projectYaml = `project: ${params.project == null ? "null" : params.project}`;
|
|
3524
|
+
const typeYaml = `type: ${params.type ?? "feature"}`;
|
|
3475
3525
|
return `---
|
|
3476
3526
|
id: ${params.id}
|
|
3477
3527
|
slug: ${params.slug}
|
|
3478
3528
|
title: ${safeTitle}
|
|
3529
|
+
${projectYaml}
|
|
3530
|
+
${typeYaml}
|
|
3479
3531
|
status: pending
|
|
3480
3532
|
priority: ${params.priority}
|
|
3481
3533
|
created: "${params.timestamp}"
|
|
@@ -3505,56 +3557,30 @@ tags: []
|
|
|
3505
3557
|
- [ ] <!-- criterion 2 -->
|
|
3506
3558
|
- [ ] <!-- criterion 3 -->
|
|
3507
3559
|
|
|
3508
|
-
##
|
|
3509
|
-
|
|
3510
|
-
<!-- Links to relevant docs, code, or other assignments. -->
|
|
3511
|
-
|
|
3512
|
-
## Questions & Answers
|
|
3560
|
+
## Todos
|
|
3513
3561
|
|
|
3514
|
-
|
|
3562
|
+
<!--
|
|
3563
|
+
Checklist of work items for this assignment. Items may be simple tasks
|
|
3564
|
+
or a markdown link to a plan file (e.g., "- [ ] Execute [plan](./plan.md)").
|
|
3565
|
+
When a plan is superseded by a new one, mark the old todo as:
|
|
3566
|
+
- [x] ~~Execute [old plan](./plan.md)~~ (superseded by plan-v2)
|
|
3567
|
+
Never delete superseded todos \u2014 preserve the history.
|
|
3568
|
+
-->
|
|
3515
3569
|
|
|
3516
|
-
##
|
|
3570
|
+
## Context
|
|
3517
3571
|
|
|
3518
|
-
|
|
3572
|
+
<!-- Links to relevant docs, code, or other assignments. -->
|
|
3519
3573
|
|
|
3520
3574
|
## Links
|
|
3521
3575
|
|
|
3522
|
-
- [
|
|
3576
|
+
- [Progress](./progress.md)
|
|
3577
|
+
- [Comments](./comments.md)
|
|
3523
3578
|
- [Scratchpad](./scratchpad.md)
|
|
3524
3579
|
- [Handoff](./handoff.md)
|
|
3525
3580
|
- [Decision Record](./decision-record.md)
|
|
3526
3581
|
`;
|
|
3527
3582
|
}
|
|
3528
3583
|
|
|
3529
|
-
// src/templates/plan.ts
|
|
3530
|
-
function renderPlan(params) {
|
|
3531
|
-
return `---
|
|
3532
|
-
assignment: ${params.assignmentSlug}
|
|
3533
|
-
status: draft
|
|
3534
|
-
created: "${params.timestamp}"
|
|
3535
|
-
updated: "${params.timestamp}"
|
|
3536
|
-
---
|
|
3537
|
-
|
|
3538
|
-
# Plan: ${params.title}
|
|
3539
|
-
|
|
3540
|
-
## Approach
|
|
3541
|
-
|
|
3542
|
-
<!-- High-level description of how to accomplish the objective. -->
|
|
3543
|
-
|
|
3544
|
-
## Tasks
|
|
3545
|
-
|
|
3546
|
-
- [ ] <!-- step 1 -->
|
|
3547
|
-
- [ ] <!-- step 2 -->
|
|
3548
|
-
- [ ] <!-- step 3 -->
|
|
3549
|
-
|
|
3550
|
-
## Risks & Mitigations
|
|
3551
|
-
|
|
3552
|
-
| Risk | Mitigation |
|
|
3553
|
-
|------|------------|
|
|
3554
|
-
| <!-- risk --> | <!-- mitigation --> |
|
|
3555
|
-
`;
|
|
3556
|
-
}
|
|
3557
|
-
|
|
3558
3584
|
// src/templates/scratchpad.ts
|
|
3559
3585
|
function renderScratchpad(params) {
|
|
3560
3586
|
return `---
|
|
@@ -3599,7 +3625,7 @@ No decisions recorded yet.
|
|
|
3599
3625
|
// src/templates/index-stubs.ts
|
|
3600
3626
|
function renderIndexAssignments(params) {
|
|
3601
3627
|
return `---
|
|
3602
|
-
|
|
3628
|
+
project: ${params.slug}
|
|
3603
3629
|
generated: "${params.timestamp}"
|
|
3604
3630
|
total: 0
|
|
3605
3631
|
by_status:
|
|
@@ -3619,7 +3645,7 @@ by_status:
|
|
|
3619
3645
|
}
|
|
3620
3646
|
function renderIndexPlans(params) {
|
|
3621
3647
|
return `---
|
|
3622
|
-
|
|
3648
|
+
project: ${params.slug}
|
|
3623
3649
|
generated: "${params.timestamp}"
|
|
3624
3650
|
---
|
|
3625
3651
|
|
|
@@ -3631,7 +3657,7 @@ generated: "${params.timestamp}"
|
|
|
3631
3657
|
}
|
|
3632
3658
|
function renderIndexDecisions(params) {
|
|
3633
3659
|
return `---
|
|
3634
|
-
|
|
3660
|
+
project: ${params.slug}
|
|
3635
3661
|
generated: "${params.timestamp}"
|
|
3636
3662
|
---
|
|
3637
3663
|
|
|
@@ -3643,7 +3669,7 @@ generated: "${params.timestamp}"
|
|
|
3643
3669
|
}
|
|
3644
3670
|
function renderStatus(params) {
|
|
3645
3671
|
return `---
|
|
3646
|
-
|
|
3672
|
+
project: ${params.slug}
|
|
3647
3673
|
generated: "${params.timestamp}"
|
|
3648
3674
|
status: pending
|
|
3649
3675
|
progress:
|
|
@@ -3657,10 +3683,10 @@ progress:
|
|
|
3657
3683
|
needsAttention:
|
|
3658
3684
|
blockedCount: 0
|
|
3659
3685
|
failedCount: 0
|
|
3660
|
-
|
|
3686
|
+
openQuestions: 0
|
|
3661
3687
|
---
|
|
3662
3688
|
|
|
3663
|
-
#
|
|
3689
|
+
# Project Status: ${params.title}
|
|
3664
3690
|
|
|
3665
3691
|
**Status:** pending
|
|
3666
3692
|
**Progress:** 0/0 assignments complete
|
|
@@ -3682,7 +3708,7 @@ No dependencies yet.
|
|
|
3682
3708
|
}
|
|
3683
3709
|
function renderResourcesIndex(params) {
|
|
3684
3710
|
return `---
|
|
3685
|
-
|
|
3711
|
+
project: ${params.slug}
|
|
3686
3712
|
generated: "${params.timestamp}"
|
|
3687
3713
|
total: 0
|
|
3688
3714
|
---
|
|
@@ -3695,7 +3721,7 @@ total: 0
|
|
|
3695
3721
|
}
|
|
3696
3722
|
function renderMemoriesIndex(params) {
|
|
3697
3723
|
return `---
|
|
3698
|
-
|
|
3724
|
+
project: ${params.slug}
|
|
3699
3725
|
generated: "${params.timestamp}"
|
|
3700
3726
|
total: 0
|
|
3701
3727
|
---
|
|
@@ -3828,13 +3854,13 @@ async function readCurrentDocument(filePath) {
|
|
|
3828
3854
|
}
|
|
3829
3855
|
return readFile6(filePath, "utf-8");
|
|
3830
3856
|
}
|
|
3831
|
-
function createWriteRouter(
|
|
3857
|
+
function createWriteRouter(projectsDir) {
|
|
3832
3858
|
const router = Router();
|
|
3833
|
-
router.get("/api/templates/
|
|
3834
|
-
const content =
|
|
3859
|
+
router.get("/api/templates/project", (_req, res) => {
|
|
3860
|
+
const content = renderProject({
|
|
3835
3861
|
id: generateId(),
|
|
3836
|
-
slug: "my-new-
|
|
3837
|
-
title: "My New
|
|
3862
|
+
slug: "my-new-project",
|
|
3863
|
+
title: "My New Project",
|
|
3838
3864
|
timestamp: nowTimestamp()
|
|
3839
3865
|
});
|
|
3840
3866
|
res.json({ content });
|
|
@@ -3851,20 +3877,20 @@ function createWriteRouter(missionsDir) {
|
|
|
3851
3877
|
});
|
|
3852
3878
|
res.json({ content });
|
|
3853
3879
|
});
|
|
3854
|
-
router.get("/api/
|
|
3880
|
+
router.get("/api/projects/:slug/edit", async (req, res) => {
|
|
3855
3881
|
const slug = getParam(req.params.slug);
|
|
3856
|
-
const document = await getEditableDocument(
|
|
3882
|
+
const document = await getEditableDocument(projectsDir, "project", slug);
|
|
3857
3883
|
if (!document) {
|
|
3858
|
-
res.status(404).json({ error: `
|
|
3884
|
+
res.status(404).json({ error: `Project "${slug}" not found` });
|
|
3859
3885
|
return;
|
|
3860
3886
|
}
|
|
3861
3887
|
res.json(document);
|
|
3862
3888
|
});
|
|
3863
|
-
router.get("/api/
|
|
3889
|
+
router.get("/api/projects/:slug/assignments/:aslug/edit", async (req, res) => {
|
|
3864
3890
|
const slug = getParam(req.params.slug);
|
|
3865
3891
|
const assignmentSlug = getParam(req.params.aslug);
|
|
3866
3892
|
const document = await getEditableDocument(
|
|
3867
|
-
|
|
3893
|
+
projectsDir,
|
|
3868
3894
|
"assignment",
|
|
3869
3895
|
slug,
|
|
3870
3896
|
assignmentSlug
|
|
@@ -3875,11 +3901,11 @@ function createWriteRouter(missionsDir) {
|
|
|
3875
3901
|
}
|
|
3876
3902
|
res.json(document);
|
|
3877
3903
|
});
|
|
3878
|
-
router.get("/api/
|
|
3904
|
+
router.get("/api/projects/:slug/assignments/:aslug/plan/edit", async (req, res) => {
|
|
3879
3905
|
const slug = getParam(req.params.slug);
|
|
3880
3906
|
const assignmentSlug = getParam(req.params.aslug);
|
|
3881
3907
|
const document = await getEditableDocument(
|
|
3882
|
-
|
|
3908
|
+
projectsDir,
|
|
3883
3909
|
"plan",
|
|
3884
3910
|
slug,
|
|
3885
3911
|
assignmentSlug
|
|
@@ -3890,11 +3916,11 @@ function createWriteRouter(missionsDir) {
|
|
|
3890
3916
|
}
|
|
3891
3917
|
res.json(document);
|
|
3892
3918
|
});
|
|
3893
|
-
router.get("/api/
|
|
3919
|
+
router.get("/api/projects/:slug/assignments/:aslug/scratchpad/edit", async (req, res) => {
|
|
3894
3920
|
const slug = getParam(req.params.slug);
|
|
3895
3921
|
const assignmentSlug = getParam(req.params.aslug);
|
|
3896
3922
|
const document = await getEditableDocument(
|
|
3897
|
-
|
|
3923
|
+
projectsDir,
|
|
3898
3924
|
"scratchpad",
|
|
3899
3925
|
slug,
|
|
3900
3926
|
assignmentSlug
|
|
@@ -3905,11 +3931,11 @@ function createWriteRouter(missionsDir) {
|
|
|
3905
3931
|
}
|
|
3906
3932
|
res.json(document);
|
|
3907
3933
|
});
|
|
3908
|
-
router.get("/api/
|
|
3934
|
+
router.get("/api/projects/:slug/assignments/:aslug/handoff/edit", async (req, res) => {
|
|
3909
3935
|
const slug = getParam(req.params.slug);
|
|
3910
3936
|
const assignmentSlug = getParam(req.params.aslug);
|
|
3911
3937
|
const document = await getEditableDocument(
|
|
3912
|
-
|
|
3938
|
+
projectsDir,
|
|
3913
3939
|
"handoff",
|
|
3914
3940
|
slug,
|
|
3915
3941
|
assignmentSlug
|
|
@@ -3920,11 +3946,11 @@ function createWriteRouter(missionsDir) {
|
|
|
3920
3946
|
}
|
|
3921
3947
|
res.json(document);
|
|
3922
3948
|
});
|
|
3923
|
-
router.get("/api/
|
|
3949
|
+
router.get("/api/projects/:slug/assignments/:aslug/decision-record/edit", async (req, res) => {
|
|
3924
3950
|
const slug = getParam(req.params.slug);
|
|
3925
3951
|
const assignmentSlug = getParam(req.params.aslug);
|
|
3926
3952
|
const document = await getEditableDocument(
|
|
3927
|
-
|
|
3953
|
+
projectsDir,
|
|
3928
3954
|
"decision-record",
|
|
3929
3955
|
slug,
|
|
3930
3956
|
assignmentSlug
|
|
@@ -3935,7 +3961,7 @@ function createWriteRouter(missionsDir) {
|
|
|
3935
3961
|
}
|
|
3936
3962
|
res.json(document);
|
|
3937
3963
|
});
|
|
3938
|
-
router.post("/api/
|
|
3964
|
+
router.post("/api/projects", async (req, res) => {
|
|
3939
3965
|
try {
|
|
3940
3966
|
const content = requireContent(req, res);
|
|
3941
3967
|
if (!content) {
|
|
@@ -3956,52 +3982,50 @@ function createWriteRouter(missionsDir) {
|
|
|
3956
3982
|
res.status(400).json({ error: `Invalid slug "${slug}". Must be lowercase and hyphen-separated.` });
|
|
3957
3983
|
return;
|
|
3958
3984
|
}
|
|
3959
|
-
const
|
|
3960
|
-
if (await fileExists(
|
|
3961
|
-
res.status(409).json({ error: `
|
|
3985
|
+
const projectDir = resolve7(projectsDir, slug);
|
|
3986
|
+
if (await fileExists(projectDir)) {
|
|
3987
|
+
res.status(409).json({ error: `Project "${slug}" already exists` });
|
|
3962
3988
|
return;
|
|
3963
3989
|
}
|
|
3964
3990
|
const title = fields.title;
|
|
3965
3991
|
const timestamp = fields.created || nowTimestamp();
|
|
3966
|
-
await ensureDir(resolve7(
|
|
3967
|
-
await ensureDir(resolve7(
|
|
3968
|
-
await ensureDir(resolve7(
|
|
3969
|
-
await writeFileForce(resolve7(
|
|
3992
|
+
await ensureDir(resolve7(projectDir, "assignments"));
|
|
3993
|
+
await ensureDir(resolve7(projectDir, "resources"));
|
|
3994
|
+
await ensureDir(resolve7(projectDir, "memories"));
|
|
3995
|
+
await writeFileForce(resolve7(projectDir, "project.md"), content);
|
|
3970
3996
|
try {
|
|
3971
3997
|
const companions = [
|
|
3972
|
-
[resolve7(
|
|
3973
|
-
[resolve7(
|
|
3974
|
-
[resolve7(
|
|
3975
|
-
[resolve7(
|
|
3976
|
-
[resolve7(
|
|
3977
|
-
[resolve7(
|
|
3978
|
-
[resolve7(
|
|
3979
|
-
[resolve7(missionDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
|
|
3980
|
-
[resolve7(missionDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
|
|
3998
|
+
[resolve7(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
|
|
3999
|
+
[resolve7(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
|
|
4000
|
+
[resolve7(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
|
|
4001
|
+
[resolve7(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
|
|
4002
|
+
[resolve7(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
|
|
4003
|
+
[resolve7(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
|
|
4004
|
+
[resolve7(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
|
|
3981
4005
|
];
|
|
3982
4006
|
for (const [filePath, fileContent] of companions) {
|
|
3983
4007
|
await writeFileForce(filePath, fileContent);
|
|
3984
4008
|
}
|
|
3985
4009
|
} catch (companionError) {
|
|
3986
4010
|
try {
|
|
3987
|
-
await rm(
|
|
4011
|
+
await rm(projectDir, { recursive: true, force: true });
|
|
3988
4012
|
} catch {
|
|
3989
4013
|
}
|
|
3990
4014
|
throw companionError;
|
|
3991
4015
|
}
|
|
3992
4016
|
res.status(201).json({ slug });
|
|
3993
4017
|
} catch (error) {
|
|
3994
|
-
console.error("Error creating
|
|
3995
|
-
res.status(500).json({ error: `Failed to create
|
|
4018
|
+
console.error("Error creating project:", error);
|
|
4019
|
+
res.status(500).json({ error: `Failed to create project: ${error.message}` });
|
|
3996
4020
|
}
|
|
3997
4021
|
});
|
|
3998
|
-
router.post("/api/
|
|
4022
|
+
router.post("/api/projects/:slug/assignments", async (req, res) => {
|
|
3999
4023
|
try {
|
|
4000
|
-
const
|
|
4001
|
-
const
|
|
4002
|
-
const
|
|
4003
|
-
if (!await fileExists(
|
|
4004
|
-
res.status(404).json({ error: `
|
|
4024
|
+
const projectSlug = getParam(req.params.slug);
|
|
4025
|
+
const projectDir = resolve7(projectsDir, projectSlug);
|
|
4026
|
+
const projectMdPath = resolve7(projectDir, "project.md");
|
|
4027
|
+
if (!await fileExists(projectMdPath)) {
|
|
4028
|
+
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
4005
4029
|
return;
|
|
4006
4030
|
}
|
|
4007
4031
|
const content = requireContent(req, res);
|
|
@@ -4029,20 +4053,18 @@ function createWriteRouter(missionsDir) {
|
|
|
4029
4053
|
res.status(400).json({ error: `Invalid priority "${priority}". Must be low, medium, high, or critical.` });
|
|
4030
4054
|
return;
|
|
4031
4055
|
}
|
|
4032
|
-
const assignmentDir = resolve7(
|
|
4056
|
+
const assignmentDir = resolve7(projectDir, "assignments", assignmentSlug);
|
|
4033
4057
|
if (await fileExists(assignmentDir)) {
|
|
4034
4058
|
res.status(409).json({
|
|
4035
|
-
error: `Assignment "${assignmentSlug}" already exists in
|
|
4059
|
+
error: `Assignment "${assignmentSlug}" already exists in project "${projectSlug}"`
|
|
4036
4060
|
});
|
|
4037
4061
|
return;
|
|
4038
4062
|
}
|
|
4039
|
-
const title = fields.title;
|
|
4040
4063
|
const timestamp = fields.created || nowTimestamp();
|
|
4041
4064
|
await ensureDir(assignmentDir);
|
|
4042
4065
|
await writeFileForce(resolve7(assignmentDir, "assignment.md"), content);
|
|
4043
4066
|
try {
|
|
4044
4067
|
const companions = [
|
|
4045
|
-
[resolve7(assignmentDir, "plan.md"), renderPlan({ assignmentSlug, title, timestamp })],
|
|
4046
4068
|
[resolve7(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
|
|
4047
4069
|
[resolve7(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
|
|
4048
4070
|
[resolve7(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
|
|
@@ -4057,51 +4079,51 @@ function createWriteRouter(missionsDir) {
|
|
|
4057
4079
|
}
|
|
4058
4080
|
throw companionError;
|
|
4059
4081
|
}
|
|
4060
|
-
res.status(201).json({ slug: assignmentSlug,
|
|
4082
|
+
res.status(201).json({ slug: assignmentSlug, projectSlug });
|
|
4061
4083
|
} catch (error) {
|
|
4062
4084
|
console.error("Error creating assignment:", error);
|
|
4063
4085
|
res.status(500).json({ error: `Failed to create assignment: ${error.message}` });
|
|
4064
4086
|
}
|
|
4065
4087
|
});
|
|
4066
|
-
router.patch("/api/
|
|
4088
|
+
router.patch("/api/projects/:slug", async (req, res) => {
|
|
4067
4089
|
try {
|
|
4068
|
-
const
|
|
4069
|
-
const
|
|
4070
|
-
const currentContent = await readCurrentDocument(
|
|
4090
|
+
const projectSlug = getParam(req.params.slug);
|
|
4091
|
+
const projectPath = resolve7(projectsDir, projectSlug, "project.md");
|
|
4092
|
+
const currentContent = await readCurrentDocument(projectPath);
|
|
4071
4093
|
if (!currentContent) {
|
|
4072
|
-
res.status(404).json({ error: `
|
|
4094
|
+
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
4073
4095
|
return;
|
|
4074
4096
|
}
|
|
4075
4097
|
const nextContentRaw = requireContent(req, res);
|
|
4076
4098
|
if (!nextContentRaw) {
|
|
4077
4099
|
return;
|
|
4078
4100
|
}
|
|
4079
|
-
const current =
|
|
4080
|
-
const next =
|
|
4101
|
+
const current = parseProject(currentContent);
|
|
4102
|
+
const next = parseProject(nextContentRaw);
|
|
4081
4103
|
if (!next.slug || !next.title) {
|
|
4082
|
-
res.status(400).json({ error: "
|
|
4104
|
+
res.status(400).json({ error: "Project content must include slug and title." });
|
|
4083
4105
|
return;
|
|
4084
4106
|
}
|
|
4085
4107
|
if (next.slug !== current.slug) {
|
|
4086
|
-
res.status(400).json({ error: "
|
|
4108
|
+
res.status(400).json({ error: "Project slug cannot be changed once created." });
|
|
4087
4109
|
return;
|
|
4088
4110
|
}
|
|
4089
4111
|
const nextContent = setTopLevelField(nextContentRaw, "updated", nowTimestamp());
|
|
4090
|
-
await writeFileForce(
|
|
4091
|
-
const
|
|
4092
|
-
res.json({
|
|
4112
|
+
await writeFileForce(projectPath, nextContent);
|
|
4113
|
+
const project = await getProjectDetail(projectsDir, projectSlug);
|
|
4114
|
+
res.json({ project, content: nextContent });
|
|
4093
4115
|
} catch (error) {
|
|
4094
|
-
console.error("Error updating
|
|
4095
|
-
res.status(500).json({ error: `Failed to update
|
|
4116
|
+
console.error("Error updating project:", error);
|
|
4117
|
+
res.status(500).json({ error: `Failed to update project: ${error.message}` });
|
|
4096
4118
|
}
|
|
4097
4119
|
});
|
|
4098
|
-
router.patch("/api/
|
|
4120
|
+
router.patch("/api/projects/:slug/assignments/:aslug", async (req, res) => {
|
|
4099
4121
|
try {
|
|
4100
|
-
const
|
|
4122
|
+
const projectSlug = getParam(req.params.slug);
|
|
4101
4123
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4102
4124
|
const assignmentPath = resolve7(
|
|
4103
|
-
|
|
4104
|
-
|
|
4125
|
+
projectsDir,
|
|
4126
|
+
projectSlug,
|
|
4105
4127
|
"assignments",
|
|
4106
4128
|
assignmentSlug,
|
|
4107
4129
|
"assignment.md"
|
|
@@ -4131,20 +4153,20 @@ function createWriteRouter(missionsDir) {
|
|
|
4131
4153
|
}
|
|
4132
4154
|
nextContent = setTopLevelField(nextContent, "updated", nowTimestamp());
|
|
4133
4155
|
await writeFileForce(assignmentPath, nextContent);
|
|
4134
|
-
const assignment = await getAssignmentDetail(
|
|
4156
|
+
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
4135
4157
|
res.json({ assignment, content: nextContent });
|
|
4136
4158
|
} catch (error) {
|
|
4137
4159
|
console.error("Error updating assignment:", error);
|
|
4138
4160
|
res.status(500).json({ error: `Failed to update assignment: ${error.message}` });
|
|
4139
4161
|
}
|
|
4140
4162
|
});
|
|
4141
|
-
router.patch("/api/
|
|
4163
|
+
router.patch("/api/projects/:slug/assignments/:aslug/acceptance-criteria/:index", async (req, res) => {
|
|
4142
4164
|
try {
|
|
4143
|
-
const
|
|
4165
|
+
const projectSlug = getParam(req.params.slug);
|
|
4144
4166
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4145
4167
|
const assignmentPath = resolve7(
|
|
4146
|
-
|
|
4147
|
-
|
|
4168
|
+
projectsDir,
|
|
4169
|
+
projectSlug,
|
|
4148
4170
|
"assignments",
|
|
4149
4171
|
assignmentSlug,
|
|
4150
4172
|
"assignment.md"
|
|
@@ -4167,20 +4189,20 @@ function createWriteRouter(missionsDir) {
|
|
|
4167
4189
|
}
|
|
4168
4190
|
const nextContent = setTopLevelField(result.content, "updated", nowTimestamp());
|
|
4169
4191
|
await writeFileForce(assignmentPath, nextContent);
|
|
4170
|
-
const assignment = await getAssignmentDetail(
|
|
4192
|
+
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
4171
4193
|
res.json({ assignment, content: nextContent });
|
|
4172
4194
|
} catch (error) {
|
|
4173
4195
|
console.error("Error toggling acceptance criterion:", error);
|
|
4174
4196
|
res.status(500).json({ error: `Failed to toggle acceptance criterion: ${error.message}` });
|
|
4175
4197
|
}
|
|
4176
4198
|
});
|
|
4177
|
-
router.patch("/api/
|
|
4199
|
+
router.patch("/api/projects/:slug/assignments/:aslug/plan", async (req, res) => {
|
|
4178
4200
|
try {
|
|
4179
|
-
const
|
|
4201
|
+
const projectSlug = getParam(req.params.slug);
|
|
4180
4202
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4181
4203
|
const planPath = resolve7(
|
|
4182
|
-
|
|
4183
|
-
|
|
4204
|
+
projectsDir,
|
|
4205
|
+
projectSlug,
|
|
4184
4206
|
"assignments",
|
|
4185
4207
|
assignmentSlug,
|
|
4186
4208
|
"plan.md"
|
|
@@ -4205,20 +4227,20 @@ function createWriteRouter(missionsDir) {
|
|
|
4205
4227
|
}
|
|
4206
4228
|
const nextContent = setTopLevelField(nextContentRaw, "updated", nowTimestamp());
|
|
4207
4229
|
await writeFileForce(planPath, nextContent);
|
|
4208
|
-
const assignment = await getAssignmentDetail(
|
|
4230
|
+
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
4209
4231
|
res.json({ assignment, content: nextContent });
|
|
4210
4232
|
} catch (error) {
|
|
4211
4233
|
console.error("Error updating plan:", error);
|
|
4212
4234
|
res.status(500).json({ error: `Failed to update plan: ${error.message}` });
|
|
4213
4235
|
}
|
|
4214
4236
|
});
|
|
4215
|
-
router.patch("/api/
|
|
4237
|
+
router.patch("/api/projects/:slug/assignments/:aslug/scratchpad", async (req, res) => {
|
|
4216
4238
|
try {
|
|
4217
|
-
const
|
|
4239
|
+
const projectSlug = getParam(req.params.slug);
|
|
4218
4240
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4219
4241
|
const scratchpadPath = resolve7(
|
|
4220
|
-
|
|
4221
|
-
|
|
4242
|
+
projectsDir,
|
|
4243
|
+
projectSlug,
|
|
4222
4244
|
"assignments",
|
|
4223
4245
|
assignmentSlug,
|
|
4224
4246
|
"scratchpad.md"
|
|
@@ -4243,20 +4265,20 @@ function createWriteRouter(missionsDir) {
|
|
|
4243
4265
|
}
|
|
4244
4266
|
const nextContent = setTopLevelField(nextContentRaw, "updated", nowTimestamp());
|
|
4245
4267
|
await writeFileForce(scratchpadPath, nextContent);
|
|
4246
|
-
const assignment = await getAssignmentDetail(
|
|
4268
|
+
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
4247
4269
|
res.json({ assignment, content: nextContent });
|
|
4248
4270
|
} catch (error) {
|
|
4249
4271
|
console.error("Error updating scratchpad:", error);
|
|
4250
4272
|
res.status(500).json({ error: `Failed to update scratchpad: ${error.message}` });
|
|
4251
4273
|
}
|
|
4252
4274
|
});
|
|
4253
|
-
router.post("/api/
|
|
4275
|
+
router.post("/api/projects/:slug/assignments/:aslug/handoff/entries", async (req, res) => {
|
|
4254
4276
|
try {
|
|
4255
|
-
const
|
|
4277
|
+
const projectSlug = getParam(req.params.slug);
|
|
4256
4278
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4257
4279
|
const handoffPath = resolve7(
|
|
4258
|
-
|
|
4259
|
-
|
|
4280
|
+
projectsDir,
|
|
4281
|
+
projectSlug,
|
|
4260
4282
|
"assignments",
|
|
4261
4283
|
assignmentSlug,
|
|
4262
4284
|
"handoff.md"
|
|
@@ -4281,20 +4303,20 @@ function createWriteRouter(missionsDir) {
|
|
|
4281
4303
|
"No handoffs recorded yet."
|
|
4282
4304
|
);
|
|
4283
4305
|
await writeFileForce(handoffPath, nextContent);
|
|
4284
|
-
const assignment = await getAssignmentDetail(
|
|
4306
|
+
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
4285
4307
|
res.status(201).json({ assignment, content: nextContent });
|
|
4286
4308
|
} catch (error) {
|
|
4287
4309
|
console.error("Error appending handoff entry:", error);
|
|
4288
4310
|
res.status(500).json({ error: `Failed to append handoff entry: ${error.message}` });
|
|
4289
4311
|
}
|
|
4290
4312
|
});
|
|
4291
|
-
router.post("/api/
|
|
4313
|
+
router.post("/api/projects/:slug/assignments/:aslug/decision-record/entries", async (req, res) => {
|
|
4292
4314
|
try {
|
|
4293
|
-
const
|
|
4315
|
+
const projectSlug = getParam(req.params.slug);
|
|
4294
4316
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4295
4317
|
const decisionPath = resolve7(
|
|
4296
|
-
|
|
4297
|
-
|
|
4318
|
+
projectsDir,
|
|
4319
|
+
projectSlug,
|
|
4298
4320
|
"assignments",
|
|
4299
4321
|
assignmentSlug,
|
|
4300
4322
|
"decision-record.md"
|
|
@@ -4319,19 +4341,19 @@ function createWriteRouter(missionsDir) {
|
|
|
4319
4341
|
"No decisions recorded yet."
|
|
4320
4342
|
);
|
|
4321
4343
|
await writeFileForce(decisionPath, nextContent);
|
|
4322
|
-
const assignment = await getAssignmentDetail(
|
|
4344
|
+
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
4323
4345
|
res.status(201).json({ assignment, content: nextContent });
|
|
4324
4346
|
} catch (error) {
|
|
4325
4347
|
console.error("Error appending decision entry:", error);
|
|
4326
4348
|
res.status(500).json({ error: `Failed to append decision entry: ${error.message}` });
|
|
4327
4349
|
}
|
|
4328
4350
|
});
|
|
4329
|
-
router.post("/api/
|
|
4351
|
+
router.post("/api/projects/:slug/move-workspace", async (req, res) => {
|
|
4330
4352
|
try {
|
|
4331
|
-
const
|
|
4332
|
-
const
|
|
4333
|
-
if (!await fileExists(
|
|
4334
|
-
res.status(404).json({ error: `
|
|
4353
|
+
const projectSlug = getParam(req.params.slug);
|
|
4354
|
+
const projectPath = resolve7(projectsDir, projectSlug, "project.md");
|
|
4355
|
+
if (!await fileExists(projectPath)) {
|
|
4356
|
+
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
4335
4357
|
return;
|
|
4336
4358
|
}
|
|
4337
4359
|
const { workspace } = req.body || {};
|
|
@@ -4339,23 +4361,23 @@ function createWriteRouter(missionsDir) {
|
|
|
4339
4361
|
res.status(400).json({ error: "workspace must be a non-empty string or null (for ungrouped)." });
|
|
4340
4362
|
return;
|
|
4341
4363
|
}
|
|
4342
|
-
let content = await readFile6(
|
|
4364
|
+
let content = await readFile6(projectPath, "utf-8");
|
|
4343
4365
|
content = setTopLevelField(content, "workspace", workspace ?? null);
|
|
4344
4366
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
4345
|
-
await writeFileForce(
|
|
4346
|
-
const
|
|
4347
|
-
res.json({
|
|
4367
|
+
await writeFileForce(projectPath, content);
|
|
4368
|
+
const project = await getProjectDetail(projectsDir, projectSlug);
|
|
4369
|
+
res.json({ project });
|
|
4348
4370
|
} catch (error) {
|
|
4349
|
-
console.error("Error moving
|
|
4371
|
+
console.error("Error moving project workspace:", error);
|
|
4350
4372
|
res.status(500).json({ error: `Failed to move workspace: ${error.message}` });
|
|
4351
4373
|
}
|
|
4352
4374
|
});
|
|
4353
|
-
router.post("/api/
|
|
4375
|
+
router.post("/api/projects/:slug/status-override", async (req, res) => {
|
|
4354
4376
|
try {
|
|
4355
|
-
const
|
|
4356
|
-
const
|
|
4357
|
-
if (!await fileExists(
|
|
4358
|
-
res.status(404).json({ error: `
|
|
4377
|
+
const projectSlug = getParam(req.params.slug);
|
|
4378
|
+
const projectPath = resolve7(projectsDir, projectSlug, "project.md");
|
|
4379
|
+
if (!await fileExists(projectPath)) {
|
|
4380
|
+
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
4359
4381
|
return;
|
|
4360
4382
|
}
|
|
4361
4383
|
const { status } = req.body || {};
|
|
@@ -4365,24 +4387,24 @@ function createWriteRouter(missionsDir) {
|
|
|
4365
4387
|
res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}, or null to clear.` });
|
|
4366
4388
|
return;
|
|
4367
4389
|
}
|
|
4368
|
-
let content = await readFile6(
|
|
4390
|
+
let content = await readFile6(projectPath, "utf-8");
|
|
4369
4391
|
content = setTopLevelField(content, "statusOverride", status ?? null);
|
|
4370
4392
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
4371
|
-
await writeFileForce(
|
|
4372
|
-
const
|
|
4373
|
-
res.json({
|
|
4393
|
+
await writeFileForce(projectPath, content);
|
|
4394
|
+
const project = await getProjectDetail(projectsDir, projectSlug);
|
|
4395
|
+
res.json({ project });
|
|
4374
4396
|
} catch (error) {
|
|
4375
|
-
console.error("Error setting
|
|
4397
|
+
console.error("Error setting project status override:", error);
|
|
4376
4398
|
res.status(500).json({ error: `Failed to set status override: ${error.message}` });
|
|
4377
4399
|
}
|
|
4378
4400
|
});
|
|
4379
|
-
router.post("/api/
|
|
4401
|
+
router.post("/api/projects/:slug/assignments/:aslug/status-override", async (req, res) => {
|
|
4380
4402
|
try {
|
|
4381
|
-
const
|
|
4403
|
+
const projectSlug = getParam(req.params.slug);
|
|
4382
4404
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4383
4405
|
const assignmentPath = resolve7(
|
|
4384
|
-
|
|
4385
|
-
|
|
4406
|
+
projectsDir,
|
|
4407
|
+
projectSlug,
|
|
4386
4408
|
"assignments",
|
|
4387
4409
|
assignmentSlug,
|
|
4388
4410
|
"assignment.md"
|
|
@@ -4405,16 +4427,16 @@ function createWriteRouter(missionsDir) {
|
|
|
4405
4427
|
content = setTopLevelField(content, "blockedReason", null);
|
|
4406
4428
|
}
|
|
4407
4429
|
await writeFileForce(assignmentPath, content);
|
|
4408
|
-
const assignment = await getAssignmentDetail(
|
|
4430
|
+
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
4409
4431
|
res.json({ assignment });
|
|
4410
4432
|
} catch (error) {
|
|
4411
4433
|
console.error("Error overriding assignment status:", error);
|
|
4412
4434
|
res.status(500).json({ error: `Failed to override status: ${error.message}` });
|
|
4413
4435
|
}
|
|
4414
4436
|
});
|
|
4415
|
-
router.post("/api/
|
|
4437
|
+
router.post("/api/projects/:slug/assignments/:aslug/transitions/:command", async (req, res) => {
|
|
4416
4438
|
try {
|
|
4417
|
-
const
|
|
4439
|
+
const projectSlug = getParam(req.params.slug);
|
|
4418
4440
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4419
4441
|
const command = req.params.command;
|
|
4420
4442
|
const config = await getStatusConfig();
|
|
@@ -4423,14 +4445,14 @@ function createWriteRouter(missionsDir) {
|
|
|
4423
4445
|
res.status(400).json({ error: `Unsupported transition command "${req.params.command}"` });
|
|
4424
4446
|
return;
|
|
4425
4447
|
}
|
|
4426
|
-
const
|
|
4427
|
-
const assignmentPath = resolve7(
|
|
4448
|
+
const projectDir = resolve7(projectsDir, projectSlug);
|
|
4449
|
+
const assignmentPath = resolve7(projectDir, "assignments", assignmentSlug, "assignment.md");
|
|
4428
4450
|
if (!await fileExists(assignmentPath)) {
|
|
4429
4451
|
res.status(404).json({ error: "Assignment not found" });
|
|
4430
4452
|
return;
|
|
4431
4453
|
}
|
|
4432
4454
|
const { reason } = req.body || {};
|
|
4433
|
-
const result = await executeTransition(
|
|
4455
|
+
const result = await executeTransition(projectDir, assignmentSlug, command, {
|
|
4434
4456
|
reason: typeof reason === "string" ? reason : void 0,
|
|
4435
4457
|
transitionTable: config.custom ? config.transitionTable : void 0,
|
|
4436
4458
|
terminalStatuses: config.custom ? config.terminalStatuses : void 0
|
|
@@ -4439,25 +4461,25 @@ function createWriteRouter(missionsDir) {
|
|
|
4439
4461
|
res.status(400).json({ error: result.message });
|
|
4440
4462
|
return;
|
|
4441
4463
|
}
|
|
4442
|
-
const assignment = await getAssignmentDetail(
|
|
4464
|
+
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
4443
4465
|
res.json({ assignment, transition: result });
|
|
4444
4466
|
} catch (error) {
|
|
4445
4467
|
console.error("Error running assignment transition:", error);
|
|
4446
4468
|
res.status(500).json({ error: `Failed to transition assignment: ${error.message}` });
|
|
4447
4469
|
}
|
|
4448
4470
|
});
|
|
4449
|
-
router.delete("/api/
|
|
4471
|
+
router.delete("/api/projects/:slug/assignments/:aslug", async (req, res) => {
|
|
4450
4472
|
try {
|
|
4451
|
-
const
|
|
4473
|
+
const projectSlug = getParam(req.params.slug);
|
|
4452
4474
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4453
|
-
const assignmentDir = resolve7(
|
|
4475
|
+
const assignmentDir = resolve7(projectsDir, projectSlug, "assignments", assignmentSlug);
|
|
4454
4476
|
const assignmentPath = resolve7(assignmentDir, "assignment.md");
|
|
4455
4477
|
if (!await fileExists(assignmentPath)) {
|
|
4456
|
-
res.status(404).json({ error: `Assignment "${assignmentSlug}" not found in
|
|
4478
|
+
res.status(404).json({ error: `Assignment "${assignmentSlug}" not found in project "${projectSlug}"` });
|
|
4457
4479
|
return;
|
|
4458
4480
|
}
|
|
4459
4481
|
await rm(assignmentDir, { recursive: true, force: true });
|
|
4460
|
-
res.json({ deleted: assignmentSlug,
|
|
4482
|
+
res.json({ deleted: assignmentSlug, projectSlug });
|
|
4461
4483
|
} catch (error) {
|
|
4462
4484
|
console.error("Error deleting assignment:", error);
|
|
4463
4485
|
res.status(500).json({ error: `Failed to delete assignment: ${error.message}` });
|
|
@@ -4470,11 +4492,11 @@ function createWriteRouter(missionsDir) {
|
|
|
4470
4492
|
init_servers();
|
|
4471
4493
|
init_scanner();
|
|
4472
4494
|
import { Router as Router2 } from "express";
|
|
4473
|
-
function createServersRouter(
|
|
4495
|
+
function createServersRouter(serversDir2, projectsDir) {
|
|
4474
4496
|
const router = Router2();
|
|
4475
4497
|
router.get("/", async (_req, res) => {
|
|
4476
4498
|
try {
|
|
4477
|
-
const result = await scanAllSessions(
|
|
4499
|
+
const result = await scanAllSessions(serversDir2, projectsDir);
|
|
4478
4500
|
res.json(result);
|
|
4479
4501
|
} catch (error) {
|
|
4480
4502
|
res.status(500).json({ error: error instanceof Error ? error.message : "Scan failed" });
|
|
@@ -4482,7 +4504,7 @@ function createServersRouter(serversDir, missionsDir) {
|
|
|
4482
4504
|
});
|
|
4483
4505
|
router.get("/:name", async (req, res) => {
|
|
4484
4506
|
try {
|
|
4485
|
-
const session = await scanSingleSession(
|
|
4507
|
+
const session = await scanSingleSession(serversDir2, projectsDir, req.params.name);
|
|
4486
4508
|
if (!session) {
|
|
4487
4509
|
res.status(404).json({ error: "Session not found" });
|
|
4488
4510
|
return;
|
|
@@ -4500,12 +4522,12 @@ function createServersRouter(serversDir, missionsDir) {
|
|
|
4500
4522
|
return;
|
|
4501
4523
|
}
|
|
4502
4524
|
const sanitized = sanitizeSessionName(name);
|
|
4503
|
-
const existing = await readSessionFile(
|
|
4525
|
+
const existing = await readSessionFile(serversDir2, sanitized);
|
|
4504
4526
|
if (existing) {
|
|
4505
4527
|
res.status(409).json({ error: `Session "${sanitized}" already registered` });
|
|
4506
4528
|
return;
|
|
4507
4529
|
}
|
|
4508
|
-
await registerSession(
|
|
4530
|
+
await registerSession(serversDir2, name);
|
|
4509
4531
|
clearScanCache();
|
|
4510
4532
|
res.status(201).json({ name: sanitized });
|
|
4511
4533
|
} catch (error) {
|
|
@@ -4514,12 +4536,12 @@ function createServersRouter(serversDir, missionsDir) {
|
|
|
4514
4536
|
});
|
|
4515
4537
|
router.delete("/:name", async (req, res) => {
|
|
4516
4538
|
try {
|
|
4517
|
-
const data = await readSessionFile(
|
|
4539
|
+
const data = await readSessionFile(serversDir2, req.params.name);
|
|
4518
4540
|
if (!data) {
|
|
4519
4541
|
res.status(404).json({ error: "Session not found" });
|
|
4520
4542
|
return;
|
|
4521
4543
|
}
|
|
4522
|
-
await removeSession(
|
|
4544
|
+
await removeSession(serversDir2, req.params.name);
|
|
4523
4545
|
clearScanCache();
|
|
4524
4546
|
res.json({ removed: req.params.name });
|
|
4525
4547
|
} catch (error) {
|
|
@@ -4528,12 +4550,12 @@ function createServersRouter(serversDir, missionsDir) {
|
|
|
4528
4550
|
});
|
|
4529
4551
|
router.post("/refresh", async (_req, res) => {
|
|
4530
4552
|
try {
|
|
4531
|
-
const names = await listSessionFiles(
|
|
4553
|
+
const names = await listSessionFiles(serversDir2);
|
|
4532
4554
|
for (const name of names) {
|
|
4533
|
-
await updateLastRefreshed(
|
|
4555
|
+
await updateLastRefreshed(serversDir2, name);
|
|
4534
4556
|
}
|
|
4535
4557
|
clearScanCache();
|
|
4536
|
-
const result = await scanAllSessions(
|
|
4558
|
+
const result = await scanAllSessions(serversDir2, projectsDir, { bypassCache: true });
|
|
4537
4559
|
res.json(result);
|
|
4538
4560
|
} catch (error) {
|
|
4539
4561
|
res.status(500).json({ error: error instanceof Error ? error.message : "Refresh failed" });
|
|
@@ -4541,14 +4563,14 @@ function createServersRouter(serversDir, missionsDir) {
|
|
|
4541
4563
|
});
|
|
4542
4564
|
router.post("/:name/refresh", async (req, res) => {
|
|
4543
4565
|
try {
|
|
4544
|
-
const data = await readSessionFile(
|
|
4566
|
+
const data = await readSessionFile(serversDir2, req.params.name);
|
|
4545
4567
|
if (!data) {
|
|
4546
4568
|
res.status(404).json({ error: "Session not found" });
|
|
4547
4569
|
return;
|
|
4548
4570
|
}
|
|
4549
|
-
await updateLastRefreshed(
|
|
4571
|
+
await updateLastRefreshed(serversDir2, req.params.name);
|
|
4550
4572
|
clearScanCache();
|
|
4551
|
-
const session = await scanSingleSession(
|
|
4573
|
+
const session = await scanSingleSession(serversDir2, projectsDir, req.params.name);
|
|
4552
4574
|
res.json(session);
|
|
4553
4575
|
} catch (error) {
|
|
4554
4576
|
res.status(500).json({ error: error instanceof Error ? error.message : "Refresh failed" });
|
|
@@ -4557,15 +4579,15 @@ function createServersRouter(serversDir, missionsDir) {
|
|
|
4557
4579
|
router.patch("/:name/panes/:windowIndex/:paneIndex/assignment", async (req, res) => {
|
|
4558
4580
|
try {
|
|
4559
4581
|
const { name, windowIndex, paneIndex } = req.params;
|
|
4560
|
-
const data = await readSessionFile(
|
|
4582
|
+
const data = await readSessionFile(serversDir2, name);
|
|
4561
4583
|
if (!data) {
|
|
4562
4584
|
res.status(404).json({ error: "Session not found" });
|
|
4563
4585
|
return;
|
|
4564
4586
|
}
|
|
4565
4587
|
const body = req.body;
|
|
4566
|
-
if (body === null || body && body.
|
|
4588
|
+
if (body === null || body && body.project && body.assignment) {
|
|
4567
4589
|
await setOverride(
|
|
4568
|
-
|
|
4590
|
+
serversDir2,
|
|
4569
4591
|
name,
|
|
4570
4592
|
parseInt(windowIndex, 10),
|
|
4571
4593
|
parseInt(paneIndex, 10),
|
|
@@ -4574,7 +4596,7 @@ function createServersRouter(serversDir, missionsDir) {
|
|
|
4574
4596
|
clearScanCache();
|
|
4575
4597
|
res.json({ updated: true });
|
|
4576
4598
|
} else {
|
|
4577
|
-
res.status(400).json({ error: "Body must be {
|
|
4599
|
+
res.status(400).json({ error: "Body must be { project, assignment } or null" });
|
|
4578
4600
|
}
|
|
4579
4601
|
} catch (error) {
|
|
4580
4602
|
res.status(500).json({ error: error instanceof Error ? error.message : "Update failed" });
|
|
@@ -4604,7 +4626,7 @@ var SCHEMA_VERSION = "2";
|
|
|
4604
4626
|
var SCHEMA_SQL = `
|
|
4605
4627
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
4606
4628
|
session_id TEXT PRIMARY KEY,
|
|
4607
|
-
|
|
4629
|
+
project_slug TEXT,
|
|
4608
4630
|
assignment_slug TEXT,
|
|
4609
4631
|
agent TEXT NOT NULL,
|
|
4610
4632
|
started TEXT NOT NULL,
|
|
@@ -4615,8 +4637,8 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
4615
4637
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4616
4638
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4617
4639
|
);
|
|
4618
|
-
CREATE INDEX IF NOT EXISTS
|
|
4619
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(
|
|
4640
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
|
|
4641
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
|
|
4620
4642
|
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
4621
4643
|
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
|
|
4622
4644
|
`;
|
|
@@ -4635,7 +4657,7 @@ function initSessionDb(dbPath) {
|
|
|
4635
4657
|
db.exec(`
|
|
4636
4658
|
CREATE TABLE sessions_v2 (
|
|
4637
4659
|
session_id TEXT PRIMARY KEY,
|
|
4638
|
-
|
|
4660
|
+
project_slug TEXT,
|
|
4639
4661
|
assignment_slug TEXT,
|
|
4640
4662
|
agent TEXT NOT NULL,
|
|
4641
4663
|
started TEXT NOT NULL,
|
|
@@ -4646,11 +4668,11 @@ function initSessionDb(dbPath) {
|
|
|
4646
4668
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4647
4669
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4648
4670
|
);
|
|
4649
|
-
INSERT INTO sessions_v2 SELECT session_id,
|
|
4671
|
+
INSERT INTO sessions_v2 SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, NULL, created_at, updated_at FROM sessions;
|
|
4650
4672
|
DROP TABLE sessions;
|
|
4651
4673
|
ALTER TABLE sessions_v2 RENAME TO sessions;
|
|
4652
|
-
CREATE INDEX IF NOT EXISTS
|
|
4653
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(
|
|
4674
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
|
|
4675
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
|
|
4654
4676
|
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
4655
4677
|
UPDATE meta SET value = '2' WHERE key = 'schema_version';
|
|
4656
4678
|
`);
|
|
@@ -4671,38 +4693,38 @@ function closeSessionDb() {
|
|
|
4671
4693
|
db = null;
|
|
4672
4694
|
}
|
|
4673
4695
|
}
|
|
4674
|
-
async function migrateFromMarkdown(
|
|
4696
|
+
async function migrateFromMarkdown(projectsDir) {
|
|
4675
4697
|
const database = getSessionDb();
|
|
4676
4698
|
const count = database.prepare("SELECT COUNT(*) as count FROM sessions").get();
|
|
4677
4699
|
if (count.count > 0) return 0;
|
|
4678
|
-
if (!await fileExists(
|
|
4679
|
-
const entries = await readdir4(
|
|
4700
|
+
if (!await fileExists(projectsDir)) return 0;
|
|
4701
|
+
const entries = await readdir4(projectsDir, { withFileTypes: true });
|
|
4680
4702
|
const allSessions = [];
|
|
4681
4703
|
for (const entry of entries) {
|
|
4682
4704
|
if (!entry.isDirectory()) continue;
|
|
4683
|
-
const
|
|
4684
|
-
const indexPath = resolve8(
|
|
4705
|
+
const projectDir = resolve8(projectsDir, entry.name);
|
|
4706
|
+
const indexPath = resolve8(projectDir, "_index-sessions.md");
|
|
4685
4707
|
if (!await fileExists(indexPath)) continue;
|
|
4686
4708
|
const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
|
|
4687
4709
|
allSessions.push(...sessions);
|
|
4688
4710
|
}
|
|
4689
4711
|
if (allSessions.length === 0) return 0;
|
|
4690
4712
|
const insert = database.prepare(`
|
|
4691
|
-
INSERT OR IGNORE INTO sessions (session_id,
|
|
4713
|
+
INSERT OR IGNORE INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path)
|
|
4692
4714
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
4693
4715
|
`);
|
|
4694
4716
|
const insertAll = database.transaction((sessions) => {
|
|
4695
4717
|
for (const s of sessions) {
|
|
4696
|
-
insert.run(s.sessionId, s.
|
|
4718
|
+
insert.run(s.sessionId, s.projectSlug, s.assignmentSlug, s.agent, s.started, s.status, s.path);
|
|
4697
4719
|
}
|
|
4698
4720
|
});
|
|
4699
4721
|
insertAll(allSessions);
|
|
4700
4722
|
console.log(`Migrated ${allSessions.length} sessions from markdown to SQLite.`);
|
|
4701
4723
|
return allSessions.length;
|
|
4702
4724
|
}
|
|
4703
|
-
async function parseMarkdownSessionsIndex(filePath,
|
|
4704
|
-
const { readFile:
|
|
4705
|
-
const raw = await
|
|
4725
|
+
async function parseMarkdownSessionsIndex(filePath, projectSlug) {
|
|
4726
|
+
const { readFile: readFile12 } = await import("fs/promises");
|
|
4727
|
+
const raw = await readFile12(filePath, "utf-8");
|
|
4706
4728
|
const sessions = [];
|
|
4707
4729
|
const lines = raw.split("\n");
|
|
4708
4730
|
let inTable = false;
|
|
@@ -4729,7 +4751,7 @@ async function parseMarkdownSessionsIndex(filePath, missionSlug) {
|
|
|
4729
4751
|
started: cells[3],
|
|
4730
4752
|
status: cells[4] || "active",
|
|
4731
4753
|
path: cells[5],
|
|
4732
|
-
|
|
4754
|
+
projectSlug
|
|
4733
4755
|
});
|
|
4734
4756
|
}
|
|
4735
4757
|
}
|
|
@@ -4741,7 +4763,7 @@ async function parseMarkdownSessionsIndex(filePath, missionSlug) {
|
|
|
4741
4763
|
function rowToSession(row) {
|
|
4742
4764
|
return {
|
|
4743
4765
|
sessionId: row.session_id,
|
|
4744
|
-
|
|
4766
|
+
projectSlug: row.project_slug ?? null,
|
|
4745
4767
|
assignmentSlug: row.assignment_slug ?? null,
|
|
4746
4768
|
agent: row.agent,
|
|
4747
4769
|
started: row.started,
|
|
@@ -4751,14 +4773,14 @@ function rowToSession(row) {
|
|
|
4751
4773
|
description: row.description ?? null
|
|
4752
4774
|
};
|
|
4753
4775
|
}
|
|
4754
|
-
async function appendSession(
|
|
4776
|
+
async function appendSession(_projectDir, session) {
|
|
4755
4777
|
const db2 = getSessionDb();
|
|
4756
4778
|
db2.prepare(`
|
|
4757
|
-
INSERT INTO sessions (session_id,
|
|
4779
|
+
INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description)
|
|
4758
4780
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
4759
4781
|
`).run(
|
|
4760
4782
|
session.sessionId,
|
|
4761
|
-
session.
|
|
4783
|
+
session.projectSlug ?? null,
|
|
4762
4784
|
session.assignmentSlug ?? null,
|
|
4763
4785
|
session.agent,
|
|
4764
4786
|
session.started,
|
|
@@ -4767,7 +4789,7 @@ async function appendSession(_missionDir, session) {
|
|
|
4767
4789
|
session.description ?? null
|
|
4768
4790
|
);
|
|
4769
4791
|
}
|
|
4770
|
-
async function updateSessionStatus(
|
|
4792
|
+
async function updateSessionStatus(_projectDir, sessionId, status) {
|
|
4771
4793
|
const db2 = getSessionDb();
|
|
4772
4794
|
const isTerminal = status === "completed" || status === "stopped";
|
|
4773
4795
|
const result = isTerminal ? db2.prepare(
|
|
@@ -4777,20 +4799,20 @@ async function updateSessionStatus(_missionDir, sessionId, status) {
|
|
|
4777
4799
|
).run(status, sessionId);
|
|
4778
4800
|
return result.changes > 0;
|
|
4779
4801
|
}
|
|
4780
|
-
async function listAllSessions(
|
|
4802
|
+
async function listAllSessions(_projectsDir) {
|
|
4781
4803
|
const db2 = getSessionDb();
|
|
4782
4804
|
const rows = db2.prepare("SELECT * FROM sessions ORDER BY started DESC").all();
|
|
4783
4805
|
return rows.map(rowToSession);
|
|
4784
4806
|
}
|
|
4785
|
-
async function
|
|
4807
|
+
async function listProjectSessions(_projectsDir, projectSlug, assignmentSlug) {
|
|
4786
4808
|
const db2 = getSessionDb();
|
|
4787
4809
|
if (assignmentSlug) {
|
|
4788
4810
|
const rows2 = db2.prepare(
|
|
4789
|
-
"SELECT * FROM sessions WHERE
|
|
4790
|
-
).all(
|
|
4811
|
+
"SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
|
|
4812
|
+
).all(projectSlug, assignmentSlug);
|
|
4791
4813
|
return rows2.map(rowToSession);
|
|
4792
4814
|
}
|
|
4793
|
-
const rows = db2.prepare("SELECT * FROM sessions WHERE
|
|
4815
|
+
const rows = db2.prepare("SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC").all(projectSlug);
|
|
4794
4816
|
return rows.map(rowToSession);
|
|
4795
4817
|
}
|
|
4796
4818
|
async function deleteSessions(sessionIds) {
|
|
@@ -4801,34 +4823,34 @@ async function deleteSessions(sessionIds) {
|
|
|
4801
4823
|
return result.changes;
|
|
4802
4824
|
}
|
|
4803
4825
|
var DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
|
|
4804
|
-
async function readAssignmentStatus(
|
|
4805
|
-
const assignmentPath = resolve9(
|
|
4826
|
+
async function readAssignmentStatus(projectDir, assignmentSlug) {
|
|
4827
|
+
const assignmentPath = resolve9(projectDir, "assignments", assignmentSlug, "assignment.md");
|
|
4806
4828
|
if (!await fileExists(assignmentPath)) return null;
|
|
4807
4829
|
const raw = await readFile7(assignmentPath, "utf-8");
|
|
4808
4830
|
const match = raw.match(/^status:\s*(.+)$/m);
|
|
4809
4831
|
return match ? match[1].trim() : null;
|
|
4810
4832
|
}
|
|
4811
|
-
async function reconcileActiveSessions(
|
|
4833
|
+
async function reconcileActiveSessions(projectsDir) {
|
|
4812
4834
|
const db2 = getSessionDb();
|
|
4813
|
-
const activeSessions = db2.prepare("SELECT * FROM sessions WHERE status = 'active' AND
|
|
4835
|
+
const activeSessions = db2.prepare("SELECT * FROM sessions WHERE status = 'active' AND project_slug IS NOT NULL AND assignment_slug IS NOT NULL").all();
|
|
4814
4836
|
if (activeSessions.length === 0) return 0;
|
|
4815
4837
|
const toCheck = /* @__PURE__ */ new Map();
|
|
4816
4838
|
for (const session of activeSessions) {
|
|
4817
|
-
const slugs = toCheck.get(session.
|
|
4839
|
+
const slugs = toCheck.get(session.project_slug) ?? /* @__PURE__ */ new Set();
|
|
4818
4840
|
slugs.add(session.assignment_slug);
|
|
4819
|
-
toCheck.set(session.
|
|
4841
|
+
toCheck.set(session.project_slug, slugs);
|
|
4820
4842
|
}
|
|
4821
4843
|
const assignmentStatuses = /* @__PURE__ */ new Map();
|
|
4822
|
-
for (const [
|
|
4823
|
-
const
|
|
4844
|
+
for (const [projectSlug, slugs] of toCheck) {
|
|
4845
|
+
const projectDir = resolve9(projectsDir, projectSlug);
|
|
4824
4846
|
for (const slug of slugs) {
|
|
4825
|
-
const status = await readAssignmentStatus(
|
|
4826
|
-
if (status) assignmentStatuses.set(`${
|
|
4847
|
+
const status = await readAssignmentStatus(projectDir, slug);
|
|
4848
|
+
if (status) assignmentStatuses.set(`${projectSlug}/${slug}`, status);
|
|
4827
4849
|
}
|
|
4828
4850
|
}
|
|
4829
4851
|
let totalUpdated = 0;
|
|
4830
4852
|
for (const session of activeSessions) {
|
|
4831
|
-
const key = `${session.
|
|
4853
|
+
const key = `${session.project_slug}/${session.assignment_slug}`;
|
|
4832
4854
|
const assignmentStatus = assignmentStatuses.get(key);
|
|
4833
4855
|
if (!assignmentStatus || !DONE_ASSIGNMENT_STATUSES.has(assignmentStatus)) continue;
|
|
4834
4856
|
const newStatus = assignmentStatus === "failed" ? "stopped" : "completed";
|
|
@@ -4840,28 +4862,28 @@ async function reconcileActiveSessions(missionsDir) {
|
|
|
4840
4862
|
|
|
4841
4863
|
// src/dashboard/api-agent-sessions.ts
|
|
4842
4864
|
init_fs();
|
|
4843
|
-
function createAgentSessionsRouter(
|
|
4865
|
+
function createAgentSessionsRouter(projectsDir, broadcast) {
|
|
4844
4866
|
const router = Router3();
|
|
4845
4867
|
router.get("/", async (_req, res) => {
|
|
4846
4868
|
try {
|
|
4847
|
-
await reconcileActiveSessions(
|
|
4848
|
-
const sessions = await listAllSessions(
|
|
4869
|
+
await reconcileActiveSessions(projectsDir);
|
|
4870
|
+
const sessions = await listAllSessions(projectsDir);
|
|
4849
4871
|
res.json({ sessions, generatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
4850
4872
|
} catch (error) {
|
|
4851
4873
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to list sessions" });
|
|
4852
4874
|
}
|
|
4853
4875
|
});
|
|
4854
|
-
router.get("/:
|
|
4876
|
+
router.get("/:projectSlug", async (req, res) => {
|
|
4855
4877
|
try {
|
|
4856
|
-
const {
|
|
4878
|
+
const { projectSlug } = req.params;
|
|
4857
4879
|
const assignment = req.query.assignment;
|
|
4858
|
-
const
|
|
4859
|
-
if (!await fileExists(
|
|
4860
|
-
res.status(404).json({ error: `
|
|
4880
|
+
const projectDir = resolve10(projectsDir, projectSlug);
|
|
4881
|
+
if (!await fileExists(projectDir)) {
|
|
4882
|
+
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
4861
4883
|
return;
|
|
4862
4884
|
}
|
|
4863
|
-
await reconcileActiveSessions(
|
|
4864
|
-
const sessions = await
|
|
4885
|
+
await reconcileActiveSessions(projectsDir);
|
|
4886
|
+
const sessions = await listProjectSessions(projectsDir, projectSlug, assignment);
|
|
4865
4887
|
res.json({ sessions, generatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
4866
4888
|
} catch (error) {
|
|
4867
4889
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to list sessions" });
|
|
@@ -4869,21 +4891,21 @@ function createAgentSessionsRouter(missionsDir, broadcast) {
|
|
|
4869
4891
|
});
|
|
4870
4892
|
router.post("/", async (req, res) => {
|
|
4871
4893
|
try {
|
|
4872
|
-
const {
|
|
4894
|
+
const { projectSlug, assignmentSlug, agent, sessionId, path, description } = req.body;
|
|
4873
4895
|
if (!agent) {
|
|
4874
4896
|
res.status(400).json({ error: "agent is required" });
|
|
4875
4897
|
return;
|
|
4876
4898
|
}
|
|
4877
|
-
if (
|
|
4878
|
-
const
|
|
4879
|
-
if (!await fileExists(
|
|
4880
|
-
res.status(404).json({ error: `
|
|
4899
|
+
if (projectSlug) {
|
|
4900
|
+
const projectDir = resolve10(projectsDir, projectSlug);
|
|
4901
|
+
if (!await fileExists(projectDir)) {
|
|
4902
|
+
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
4881
4903
|
return;
|
|
4882
4904
|
}
|
|
4883
4905
|
}
|
|
4884
4906
|
const id = sessionId || randomUUID2();
|
|
4885
4907
|
const session = {
|
|
4886
|
-
|
|
4908
|
+
projectSlug: projectSlug || null,
|
|
4887
4909
|
assignmentSlug: assignmentSlug || null,
|
|
4888
4910
|
agent,
|
|
4889
4911
|
sessionId: id,
|
|
@@ -4954,13 +4976,13 @@ init_parser();
|
|
|
4954
4976
|
init_timestamp();
|
|
4955
4977
|
import { resolve as resolve11 } from "path";
|
|
4956
4978
|
import { readdir as readdir5, readFile as readFile8 } from "fs/promises";
|
|
4957
|
-
async function rebuildPlaybookManifest(
|
|
4958
|
-
if (!await fileExists(
|
|
4959
|
-
const entries = await readdir5(
|
|
4979
|
+
async function rebuildPlaybookManifest(playbooksDir2) {
|
|
4980
|
+
if (!await fileExists(playbooksDir2)) return;
|
|
4981
|
+
const entries = await readdir5(playbooksDir2, { withFileTypes: true });
|
|
4960
4982
|
const rows = [];
|
|
4961
4983
|
for (const entry of entries) {
|
|
4962
4984
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
|
|
4963
|
-
const raw = await readFile8(resolve11(
|
|
4985
|
+
const raw = await readFile8(resolve11(playbooksDir2, entry.name), "utf-8");
|
|
4964
4986
|
const parsed = parsePlaybook(raw);
|
|
4965
4987
|
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
4966
4988
|
rows.push({
|
|
@@ -4990,15 +5012,15 @@ async function rebuildPlaybookManifest(playbooksDir) {
|
|
|
4990
5012
|
}
|
|
4991
5013
|
}
|
|
4992
5014
|
lines.push("");
|
|
4993
|
-
await writeFileForce(resolve11(
|
|
5015
|
+
await writeFileForce(resolve11(playbooksDir2, "manifest.md"), lines.join("\n"));
|
|
4994
5016
|
}
|
|
4995
5017
|
|
|
4996
5018
|
// src/dashboard/api-playbooks.ts
|
|
4997
|
-
function createPlaybooksRouter(
|
|
5019
|
+
function createPlaybooksRouter(playbooksDir2) {
|
|
4998
5020
|
const router = Router4();
|
|
4999
5021
|
router.get("/", async (_req, res) => {
|
|
5000
5022
|
try {
|
|
5001
|
-
const playbooks = await listPlaybooks(
|
|
5023
|
+
const playbooks = await listPlaybooks(playbooksDir2);
|
|
5002
5024
|
res.json({ playbooks, generatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5003
5025
|
} catch (error) {
|
|
5004
5026
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to list playbooks" });
|
|
@@ -5019,7 +5041,7 @@ function createPlaybooksRouter(playbooksDir) {
|
|
|
5019
5041
|
});
|
|
5020
5042
|
router.get("/:slug", async (req, res) => {
|
|
5021
5043
|
try {
|
|
5022
|
-
const detail = await getPlaybookDetail(
|
|
5044
|
+
const detail = await getPlaybookDetail(playbooksDir2, req.params.slug);
|
|
5023
5045
|
if (!detail) {
|
|
5024
5046
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
5025
5047
|
return;
|
|
@@ -5031,7 +5053,7 @@ function createPlaybooksRouter(playbooksDir) {
|
|
|
5031
5053
|
});
|
|
5032
5054
|
router.get("/:slug/edit", async (req, res) => {
|
|
5033
5055
|
try {
|
|
5034
|
-
const filePath = resolve12(
|
|
5056
|
+
const filePath = resolve12(playbooksDir2, `${req.params.slug}.md`);
|
|
5035
5057
|
if (!await fileExists(filePath)) {
|
|
5036
5058
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
5037
5059
|
return;
|
|
@@ -5060,14 +5082,14 @@ function createPlaybooksRouter(playbooksDir) {
|
|
|
5060
5082
|
res.status(400).json({ error: `Invalid or missing slug: "${slug}"` });
|
|
5061
5083
|
return;
|
|
5062
5084
|
}
|
|
5063
|
-
await ensureDir(
|
|
5064
|
-
const filePath = resolve12(
|
|
5085
|
+
await ensureDir(playbooksDir2);
|
|
5086
|
+
const filePath = resolve12(playbooksDir2, `${slug}.md`);
|
|
5065
5087
|
if (await fileExists(filePath)) {
|
|
5066
5088
|
res.status(409).json({ error: `Playbook "${slug}" already exists` });
|
|
5067
5089
|
return;
|
|
5068
5090
|
}
|
|
5069
5091
|
await writeFileForce(filePath, content);
|
|
5070
|
-
await rebuildPlaybookManifest(
|
|
5092
|
+
await rebuildPlaybookManifest(playbooksDir2);
|
|
5071
5093
|
res.status(201).json({ slug, path: filePath });
|
|
5072
5094
|
} catch (error) {
|
|
5073
5095
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to create playbook" });
|
|
@@ -5080,13 +5102,13 @@ function createPlaybooksRouter(playbooksDir) {
|
|
|
5080
5102
|
res.status(400).json({ error: "content is required" });
|
|
5081
5103
|
return;
|
|
5082
5104
|
}
|
|
5083
|
-
const filePath = resolve12(
|
|
5105
|
+
const filePath = resolve12(playbooksDir2, `${req.params.slug}.md`);
|
|
5084
5106
|
if (!await fileExists(filePath)) {
|
|
5085
5107
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
5086
5108
|
return;
|
|
5087
5109
|
}
|
|
5088
5110
|
await writeFileForce(filePath, content);
|
|
5089
|
-
await rebuildPlaybookManifest(
|
|
5111
|
+
await rebuildPlaybookManifest(playbooksDir2);
|
|
5090
5112
|
res.json({ slug: req.params.slug, path: filePath });
|
|
5091
5113
|
} catch (error) {
|
|
5092
5114
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to update playbook" });
|
|
@@ -5098,13 +5120,13 @@ function createPlaybooksRouter(playbooksDir) {
|
|
|
5098
5120
|
res.status(403).json({ error: "The playbook manifest cannot be deleted" });
|
|
5099
5121
|
return;
|
|
5100
5122
|
}
|
|
5101
|
-
const filePath = resolve12(
|
|
5123
|
+
const filePath = resolve12(playbooksDir2, `${req.params.slug}.md`);
|
|
5102
5124
|
if (!await fileExists(filePath)) {
|
|
5103
5125
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
5104
5126
|
return;
|
|
5105
5127
|
}
|
|
5106
5128
|
await unlink2(filePath);
|
|
5107
|
-
await rebuildPlaybookManifest(
|
|
5129
|
+
await rebuildPlaybookManifest(playbooksDir2);
|
|
5108
5130
|
res.json({ deleted: req.params.slug });
|
|
5109
5131
|
} catch (error) {
|
|
5110
5132
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to delete playbook" });
|
|
@@ -5134,7 +5156,7 @@ function withLock(workspace, fn) {
|
|
|
5134
5156
|
}));
|
|
5135
5157
|
return next;
|
|
5136
5158
|
}
|
|
5137
|
-
function createTodosRouter(
|
|
5159
|
+
function createTodosRouter(todosDir2, broadcast) {
|
|
5138
5160
|
const router = Router5();
|
|
5139
5161
|
function broadcastUpdate() {
|
|
5140
5162
|
broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -5150,14 +5172,14 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5150
5172
|
router.param("workspace", validateWorkspace);
|
|
5151
5173
|
router.get("/", async (_req, res) => {
|
|
5152
5174
|
try {
|
|
5153
|
-
await ensureDir(
|
|
5154
|
-
const files = await readdir6(
|
|
5175
|
+
await ensureDir(todosDir2);
|
|
5176
|
+
const files = await readdir6(todosDir2).catch(() => []);
|
|
5155
5177
|
const workspaces = [];
|
|
5156
5178
|
for (const file of files) {
|
|
5157
5179
|
if (typeof file !== "string") continue;
|
|
5158
5180
|
if (!file.endsWith(".md") || file.endsWith("-log.md")) continue;
|
|
5159
5181
|
const workspace = file.replace(".md", "");
|
|
5160
|
-
const checklist = await readChecklist(
|
|
5182
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5161
5183
|
workspaces.push({
|
|
5162
5184
|
workspace: checklist.workspace,
|
|
5163
5185
|
archiveInterval: checklist.archiveInterval,
|
|
@@ -5173,7 +5195,7 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5173
5195
|
router.get("/:workspace", async (req, res) => {
|
|
5174
5196
|
try {
|
|
5175
5197
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5176
|
-
const checklist = await readChecklist(
|
|
5198
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5177
5199
|
res.json({
|
|
5178
5200
|
workspace: checklist.workspace,
|
|
5179
5201
|
archiveInterval: checklist.archiveInterval,
|
|
@@ -5193,7 +5215,7 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5193
5215
|
return;
|
|
5194
5216
|
}
|
|
5195
5217
|
const item = await withLock(workspace, async () => {
|
|
5196
|
-
const checklist = await readChecklist(
|
|
5218
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5197
5219
|
const existingIds = new Set(checklist.items.map((i) => i.id));
|
|
5198
5220
|
const id = generateUniqueId(existingIds);
|
|
5199
5221
|
const newItem = {
|
|
@@ -5204,7 +5226,7 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5204
5226
|
session: null
|
|
5205
5227
|
};
|
|
5206
5228
|
checklist.items.push(newItem);
|
|
5207
|
-
await writeChecklist(
|
|
5229
|
+
await writeChecklist(todosDir2, checklist);
|
|
5208
5230
|
return newItem;
|
|
5209
5231
|
});
|
|
5210
5232
|
broadcastUpdate();
|
|
@@ -5222,7 +5244,7 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5222
5244
|
return;
|
|
5223
5245
|
}
|
|
5224
5246
|
const items = await withLock(workspace, async () => {
|
|
5225
|
-
const checklist = await readChecklist(
|
|
5247
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5226
5248
|
const itemMap = new Map(checklist.items.map((i) => [i.id, i]));
|
|
5227
5249
|
const reordered = [];
|
|
5228
5250
|
for (const id of ids) {
|
|
@@ -5236,7 +5258,7 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5236
5258
|
reordered.push(item);
|
|
5237
5259
|
}
|
|
5238
5260
|
checklist.items = reordered;
|
|
5239
|
-
await writeChecklist(
|
|
5261
|
+
await writeChecklist(todosDir2, checklist);
|
|
5240
5262
|
return reordered;
|
|
5241
5263
|
});
|
|
5242
5264
|
broadcastUpdate();
|
|
@@ -5247,7 +5269,7 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5247
5269
|
});
|
|
5248
5270
|
router.get("/:workspace/log", async (req, res) => {
|
|
5249
5271
|
try {
|
|
5250
|
-
const log = await readLog(
|
|
5272
|
+
const log = await readLog(todosDir2, getWorkspaceParam(req.params.workspace));
|
|
5251
5273
|
res.json(log);
|
|
5252
5274
|
} catch (error) {
|
|
5253
5275
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get log" });
|
|
@@ -5256,12 +5278,12 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5256
5278
|
router.post("/:workspace/archive", async (req, res) => {
|
|
5257
5279
|
try {
|
|
5258
5280
|
const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
|
|
5259
|
-
const { resolve:
|
|
5260
|
-
const { readFile:
|
|
5281
|
+
const { resolve: resolve16 } = await import("path");
|
|
5282
|
+
const { readFile: readFile12 } = await import("fs/promises");
|
|
5261
5283
|
const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
5262
5284
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5263
|
-
const checklist = await readChecklist(
|
|
5264
|
-
const log = await readLog(
|
|
5285
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5286
|
+
const log = await readLog(todosDir2, workspace);
|
|
5265
5287
|
const completedIds = new Set(
|
|
5266
5288
|
checklist.items.filter((i) => i.status === "completed").map((i) => i.id)
|
|
5267
5289
|
);
|
|
@@ -5272,11 +5294,11 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5272
5294
|
const toArchive = log.entries.filter(
|
|
5273
5295
|
(e) => e.itemIds.every((id) => completedIds.has(id))
|
|
5274
5296
|
);
|
|
5275
|
-
const archFile = archivePath2(
|
|
5276
|
-
await ensureDir(
|
|
5297
|
+
const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
|
|
5298
|
+
await ensureDir(resolve16(todosDir2, "archive"));
|
|
5277
5299
|
let archContent = "";
|
|
5278
5300
|
if (await fileExists(archFile)) {
|
|
5279
|
-
archContent = await
|
|
5301
|
+
archContent = await readFile12(archFile, "utf-8");
|
|
5280
5302
|
archContent = archContent.trimEnd() + "\n\n";
|
|
5281
5303
|
} else {
|
|
5282
5304
|
archContent = `---
|
|
@@ -5310,7 +5332,7 @@ workspace: ${workspace}
|
|
|
5310
5332
|
}
|
|
5311
5333
|
await writeFileForce2(archFile, archContent);
|
|
5312
5334
|
checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
|
|
5313
|
-
await writeChecklist(
|
|
5335
|
+
await writeChecklist(todosDir2, checklist);
|
|
5314
5336
|
broadcastUpdate();
|
|
5315
5337
|
res.json({ archived: completedIds.size, logEntries: toArchive.length });
|
|
5316
5338
|
} catch (error) {
|
|
@@ -5319,7 +5341,7 @@ workspace: ${workspace}
|
|
|
5319
5341
|
});
|
|
5320
5342
|
router.get("/:workspace/log/:id", async (req, res) => {
|
|
5321
5343
|
try {
|
|
5322
|
-
const log = await readLog(
|
|
5344
|
+
const log = await readLog(todosDir2, getWorkspaceParam(req.params.workspace));
|
|
5323
5345
|
const entries = log.entries.filter((e) => e.itemIds.includes(req.params.id));
|
|
5324
5346
|
res.json({ workspace: log.workspace, entries });
|
|
5325
5347
|
} catch (error) {
|
|
@@ -5329,13 +5351,13 @@ workspace: ${workspace}
|
|
|
5329
5351
|
router.get("/:workspace/:id", async (req, res) => {
|
|
5330
5352
|
try {
|
|
5331
5353
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5332
|
-
const checklist = await readChecklist(
|
|
5354
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5333
5355
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
5334
5356
|
if (!item) {
|
|
5335
5357
|
res.status(404).json({ error: `Todo "${req.params.id}" not found` });
|
|
5336
5358
|
return;
|
|
5337
5359
|
}
|
|
5338
|
-
const log = await readLog(
|
|
5360
|
+
const log = await readLog(todosDir2, workspace);
|
|
5339
5361
|
const logEntries = log.entries.filter((e) => e.itemIds.includes(req.params.id));
|
|
5340
5362
|
res.json({ ...item, log: logEntries });
|
|
5341
5363
|
} catch (error) {
|
|
@@ -5346,12 +5368,12 @@ workspace: ${workspace}
|
|
|
5346
5368
|
try {
|
|
5347
5369
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5348
5370
|
const result = await withLock(workspace, async () => {
|
|
5349
|
-
const checklist = await readChecklist(
|
|
5371
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5350
5372
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
5351
5373
|
if (!item) return null;
|
|
5352
5374
|
if (req.body.description !== void 0) item.description = req.body.description;
|
|
5353
5375
|
if (Array.isArray(req.body.tags)) item.tags = req.body.tags;
|
|
5354
|
-
await writeChecklist(
|
|
5376
|
+
await writeChecklist(todosDir2, checklist);
|
|
5355
5377
|
return { ...item };
|
|
5356
5378
|
});
|
|
5357
5379
|
if (!result) {
|
|
@@ -5368,11 +5390,11 @@ workspace: ${workspace}
|
|
|
5368
5390
|
try {
|
|
5369
5391
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5370
5392
|
const deleted = await withLock(workspace, async () => {
|
|
5371
|
-
const checklist = await readChecklist(
|
|
5393
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5372
5394
|
const idx = checklist.items.findIndex((i) => i.id === req.params.id);
|
|
5373
5395
|
if (idx === -1) return false;
|
|
5374
5396
|
checklist.items.splice(idx, 1);
|
|
5375
|
-
await writeChecklist(
|
|
5397
|
+
await writeChecklist(todosDir2, checklist);
|
|
5376
5398
|
return true;
|
|
5377
5399
|
});
|
|
5378
5400
|
if (!deleted) {
|
|
@@ -5389,13 +5411,13 @@ workspace: ${workspace}
|
|
|
5389
5411
|
try {
|
|
5390
5412
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5391
5413
|
const result = await withLock(workspace, async () => {
|
|
5392
|
-
const checklist = await readChecklist(
|
|
5414
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5393
5415
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
5394
5416
|
if (!item) return { error: "not_found" };
|
|
5395
5417
|
if (item.status === "in_progress") return { error: "conflict", session: item.session };
|
|
5396
5418
|
item.status = "in_progress";
|
|
5397
5419
|
item.session = req.body.session || null;
|
|
5398
|
-
await writeChecklist(
|
|
5420
|
+
await writeChecklist(todosDir2, checklist);
|
|
5399
5421
|
return { item: { ...item } };
|
|
5400
5422
|
});
|
|
5401
5423
|
if ("error" in result) {
|
|
@@ -5416,12 +5438,12 @@ workspace: ${workspace}
|
|
|
5416
5438
|
try {
|
|
5417
5439
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5418
5440
|
const result = await withLock(workspace, async () => {
|
|
5419
|
-
const checklist = await readChecklist(
|
|
5441
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5420
5442
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
5421
5443
|
if (!item) return null;
|
|
5422
5444
|
item.status = "completed";
|
|
5423
5445
|
item.session = null;
|
|
5424
|
-
await writeChecklist(
|
|
5446
|
+
await writeChecklist(todosDir2, checklist);
|
|
5425
5447
|
const entry = {
|
|
5426
5448
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5427
5449
|
itemIds: [item.id],
|
|
@@ -5432,7 +5454,7 @@ workspace: ${workspace}
|
|
|
5432
5454
|
blockers: null,
|
|
5433
5455
|
status: null
|
|
5434
5456
|
};
|
|
5435
|
-
await appendLogEntry2(
|
|
5457
|
+
await appendLogEntry2(todosDir2, workspace, entry);
|
|
5436
5458
|
return { ...item };
|
|
5437
5459
|
});
|
|
5438
5460
|
if (!result) {
|
|
@@ -5450,12 +5472,12 @@ workspace: ${workspace}
|
|
|
5450
5472
|
const reason = req.body.reason || null;
|
|
5451
5473
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5452
5474
|
const result = await withLock(workspace, async () => {
|
|
5453
|
-
const checklist = await readChecklist(
|
|
5475
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5454
5476
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
5455
5477
|
if (!item) return null;
|
|
5456
5478
|
item.status = "blocked";
|
|
5457
5479
|
item.session = null;
|
|
5458
|
-
await writeChecklist(
|
|
5480
|
+
await writeChecklist(todosDir2, checklist);
|
|
5459
5481
|
const entry = {
|
|
5460
5482
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5461
5483
|
itemIds: [item.id],
|
|
@@ -5466,7 +5488,7 @@ workspace: ${workspace}
|
|
|
5466
5488
|
blockers: reason,
|
|
5467
5489
|
status: "blocked"
|
|
5468
5490
|
};
|
|
5469
|
-
await appendLogEntry2(
|
|
5491
|
+
await appendLogEntry2(todosDir2, workspace, entry);
|
|
5470
5492
|
return { ...item };
|
|
5471
5493
|
});
|
|
5472
5494
|
if (!result) {
|
|
@@ -5483,12 +5505,12 @@ workspace: ${workspace}
|
|
|
5483
5505
|
try {
|
|
5484
5506
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5485
5507
|
const result = await withLock(workspace, async () => {
|
|
5486
|
-
const checklist = await readChecklist(
|
|
5508
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5487
5509
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
5488
5510
|
if (!item) return null;
|
|
5489
5511
|
item.status = "open";
|
|
5490
5512
|
item.session = null;
|
|
5491
|
-
await writeChecklist(
|
|
5513
|
+
await writeChecklist(todosDir2, checklist);
|
|
5492
5514
|
return { ...item };
|
|
5493
5515
|
});
|
|
5494
5516
|
if (!result) {
|
|
@@ -5505,12 +5527,12 @@ workspace: ${workspace}
|
|
|
5505
5527
|
try {
|
|
5506
5528
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5507
5529
|
const result = await withLock(workspace, async () => {
|
|
5508
|
-
const checklist = await readChecklist(
|
|
5530
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5509
5531
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
5510
5532
|
if (!item) return null;
|
|
5511
5533
|
item.status = "open";
|
|
5512
5534
|
item.session = null;
|
|
5513
|
-
await writeChecklist(
|
|
5535
|
+
await writeChecklist(todosDir2, checklist);
|
|
5514
5536
|
return { ...item };
|
|
5515
5537
|
});
|
|
5516
5538
|
if (!result) {
|
|
@@ -5526,6 +5548,411 @@ workspace: ${workspace}
|
|
|
5526
5548
|
return router;
|
|
5527
5549
|
}
|
|
5528
5550
|
|
|
5551
|
+
// src/dashboard/api-backup.ts
|
|
5552
|
+
init_config2();
|
|
5553
|
+
import { Router as Router6 } from "express";
|
|
5554
|
+
|
|
5555
|
+
// src/utils/github-backup.ts
|
|
5556
|
+
init_paths();
|
|
5557
|
+
init_fs();
|
|
5558
|
+
init_config2();
|
|
5559
|
+
import { execFile as execFile2 } from "child_process";
|
|
5560
|
+
import { promisify as promisify2 } from "util";
|
|
5561
|
+
import { cp, mkdtemp, rm as rm2, readFile as readFile11, writeFile as writeFile3, unlink as unlink3, stat, open, rename as rename2 } from "fs/promises";
|
|
5562
|
+
import { resolve as resolve14, join as join2 } from "path";
|
|
5563
|
+
import { tmpdir } from "os";
|
|
5564
|
+
var exec2 = promisify2(execFile2);
|
|
5565
|
+
var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
|
|
5566
|
+
var LOCK_FILE_NAME = ".backup-lock";
|
|
5567
|
+
function parseCategoriesStrict(cats) {
|
|
5568
|
+
const unknown = [];
|
|
5569
|
+
const valid = [];
|
|
5570
|
+
for (const cat of cats) {
|
|
5571
|
+
if (VALID_CATEGORIES.includes(cat)) {
|
|
5572
|
+
valid.push(cat);
|
|
5573
|
+
} else {
|
|
5574
|
+
unknown.push(cat);
|
|
5575
|
+
}
|
|
5576
|
+
}
|
|
5577
|
+
if (unknown.length > 0) {
|
|
5578
|
+
throw new Error(
|
|
5579
|
+
`Unknown categor${unknown.length === 1 ? "y" : "ies"}: ${unknown.map((c) => `"${c}"`).join(", ")}. Valid: ${VALID_CATEGORIES.join(", ")}`
|
|
5580
|
+
);
|
|
5581
|
+
}
|
|
5582
|
+
return valid;
|
|
5583
|
+
}
|
|
5584
|
+
function validateRepoUrl(url) {
|
|
5585
|
+
if (!url || typeof url !== "string") return false;
|
|
5586
|
+
const trimmed = url.trim();
|
|
5587
|
+
return trimmed.startsWith("https://") || trimmed.startsWith("git@");
|
|
5588
|
+
}
|
|
5589
|
+
async function resolveCategoryPath(category) {
|
|
5590
|
+
switch (category) {
|
|
5591
|
+
case "projects": {
|
|
5592
|
+
const config = await readConfig();
|
|
5593
|
+
return { sourcePath: config.defaultProjectDir, repoPath: "projects", isFile: false };
|
|
5594
|
+
}
|
|
5595
|
+
case "playbooks":
|
|
5596
|
+
return { sourcePath: playbooksDir(), repoPath: "playbooks", isFile: false };
|
|
5597
|
+
case "todos":
|
|
5598
|
+
return { sourcePath: todosDir(), repoPath: "todos", isFile: false };
|
|
5599
|
+
case "servers":
|
|
5600
|
+
return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
|
|
5601
|
+
case "config":
|
|
5602
|
+
return { sourcePath: resolve14(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
async function checkGitInstalled() {
|
|
5606
|
+
try {
|
|
5607
|
+
await exec2("git", ["--version"]);
|
|
5608
|
+
} catch {
|
|
5609
|
+
throw new Error("git is not installed or not on PATH. Install git and try again.");
|
|
5610
|
+
}
|
|
5611
|
+
}
|
|
5612
|
+
async function acquireLock() {
|
|
5613
|
+
const lockPath = resolve14(syntaurRoot(), LOCK_FILE_NAME);
|
|
5614
|
+
await ensureDir(syntaurRoot());
|
|
5615
|
+
try {
|
|
5616
|
+
const handle = await open(lockPath, "wx");
|
|
5617
|
+
await handle.write(String(process.pid));
|
|
5618
|
+
await handle.close();
|
|
5619
|
+
return lockPath;
|
|
5620
|
+
} catch (err) {
|
|
5621
|
+
if (err.code === "EEXIST") {
|
|
5622
|
+
const pid = await readFile11(lockPath, "utf-8").catch(() => "");
|
|
5623
|
+
throw new Error(
|
|
5624
|
+
`Backup operation already in progress (lock file at ${lockPath}, pid ${pid.trim() || "unknown"}). If stale, delete the file and retry.`
|
|
5625
|
+
);
|
|
5626
|
+
}
|
|
5627
|
+
throw err;
|
|
5628
|
+
}
|
|
5629
|
+
}
|
|
5630
|
+
async function releaseLock(lockPath) {
|
|
5631
|
+
try {
|
|
5632
|
+
await unlink3(lockPath);
|
|
5633
|
+
} catch {
|
|
5634
|
+
}
|
|
5635
|
+
}
|
|
5636
|
+
async function runGit(args, cwd) {
|
|
5637
|
+
return exec2("git", args, { cwd });
|
|
5638
|
+
}
|
|
5639
|
+
async function cloneOrInit(repoUrl, destDir) {
|
|
5640
|
+
try {
|
|
5641
|
+
await exec2("git", ["clone", "--depth", "1", repoUrl, destDir]);
|
|
5642
|
+
} catch (error) {
|
|
5643
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5644
|
+
if (message.includes("Repository not found") || message.includes("does not appear to be a git repository")) {
|
|
5645
|
+
throw new Error(`Repository not found or inaccessible: ${repoUrl}. Check URL and credentials.`);
|
|
5646
|
+
}
|
|
5647
|
+
if (message.includes("Authentication failed") || message.includes("could not read Username")) {
|
|
5648
|
+
throw new Error(`Authentication failed for ${repoUrl}. Check SSH keys or credentials.`);
|
|
5649
|
+
}
|
|
5650
|
+
throw new Error(`git clone failed: ${message}`);
|
|
5651
|
+
}
|
|
5652
|
+
}
|
|
5653
|
+
async function copyRecursive(src, dest) {
|
|
5654
|
+
if (!await fileExists(src)) return;
|
|
5655
|
+
const s = await stat(src);
|
|
5656
|
+
if (s.isDirectory()) {
|
|
5657
|
+
await ensureDir(dest);
|
|
5658
|
+
await cp(src, dest, { recursive: true, force: true });
|
|
5659
|
+
} else {
|
|
5660
|
+
await ensureDir(resolve14(dest, ".."));
|
|
5661
|
+
await cp(src, dest, { force: true });
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
5664
|
+
function resolveCategoriesStrict(csv) {
|
|
5665
|
+
const parts = csv.split(",").map((s) => s.trim()).filter(Boolean);
|
|
5666
|
+
return parseCategoriesStrict(parts);
|
|
5667
|
+
}
|
|
5668
|
+
async function readSanitizedConfig(configPath) {
|
|
5669
|
+
const content = await readFile11(configPath, "utf-8");
|
|
5670
|
+
return content.replace(/^(\s*lastBackup:\s*).*$/m, "$1null").replace(/^(\s*lastRestore:\s*).*$/m, "$1null");
|
|
5671
|
+
}
|
|
5672
|
+
async function backupToGithub(overrides) {
|
|
5673
|
+
await checkGitInstalled();
|
|
5674
|
+
const config = await readConfig();
|
|
5675
|
+
const rawRepo = overrides?.repo ?? config.backup?.repo ?? null;
|
|
5676
|
+
if (!rawRepo) {
|
|
5677
|
+
throw new Error("No backup repo configured. Set it via `syntaur backup config --repo <url>` or the dashboard.");
|
|
5678
|
+
}
|
|
5679
|
+
const repo = rawRepo.trim();
|
|
5680
|
+
if (!validateRepoUrl(repo)) {
|
|
5681
|
+
throw new Error(`Invalid repo URL: "${rawRepo}". Must start with https:// or git@.`);
|
|
5682
|
+
}
|
|
5683
|
+
const categoriesCsv = config.backup?.categories ?? "projects, playbooks, todos, servers, config";
|
|
5684
|
+
const categories = overrides?.categories ?? resolveCategoriesStrict(categoriesCsv);
|
|
5685
|
+
if (categories.length === 0) {
|
|
5686
|
+
throw new Error("No valid backup categories selected.");
|
|
5687
|
+
}
|
|
5688
|
+
const lockPath = await acquireLock();
|
|
5689
|
+
let tmpDir = null;
|
|
5690
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
5691
|
+
try {
|
|
5692
|
+
tmpDir = await mkdtemp(join2(tmpdir(), "syntaur-backup-"));
|
|
5693
|
+
await cloneOrInit(repo, tmpDir);
|
|
5694
|
+
for (const category of categories) {
|
|
5695
|
+
const { sourcePath, repoPath, isFile } = await resolveCategoryPath(category);
|
|
5696
|
+
const destPath = join2(tmpDir, repoPath);
|
|
5697
|
+
if (isFile) {
|
|
5698
|
+
await rm2(destPath, { force: true });
|
|
5699
|
+
} else {
|
|
5700
|
+
await rm2(destPath, { recursive: true, force: true });
|
|
5701
|
+
}
|
|
5702
|
+
if (!await fileExists(sourcePath)) {
|
|
5703
|
+
console.warn(`Category "${category}": no local data at ${sourcePath}; backup will reflect deletion.`);
|
|
5704
|
+
continue;
|
|
5705
|
+
}
|
|
5706
|
+
if (category === "config") {
|
|
5707
|
+
const sanitized = await readSanitizedConfig(sourcePath);
|
|
5708
|
+
await ensureDir(resolve14(destPath, ".."));
|
|
5709
|
+
await writeFile3(destPath, sanitized, "utf-8");
|
|
5710
|
+
} else {
|
|
5711
|
+
await copyRecursive(sourcePath, destPath);
|
|
5712
|
+
}
|
|
5713
|
+
}
|
|
5714
|
+
await runGit(["add", "-A"], tmpDir);
|
|
5715
|
+
const { stdout: status } = await runGit(["status", "--porcelain"], tmpDir);
|
|
5716
|
+
if (!status.trim()) {
|
|
5717
|
+
await updateBackupConfig({ lastBackup: timestamp }).catch(() => {
|
|
5718
|
+
});
|
|
5719
|
+
return {
|
|
5720
|
+
success: true,
|
|
5721
|
+
timestamp,
|
|
5722
|
+
message: "No changes to back up.",
|
|
5723
|
+
committed: false
|
|
5724
|
+
};
|
|
5725
|
+
}
|
|
5726
|
+
try {
|
|
5727
|
+
await runGit(["config", "user.email", "syntaur@local"], tmpDir);
|
|
5728
|
+
await runGit(["config", "user.name", "Syntaur Backup"], tmpDir);
|
|
5729
|
+
} catch {
|
|
5730
|
+
}
|
|
5731
|
+
await runGit(["commit", "-m", `Syntaur backup ${timestamp}`], tmpDir);
|
|
5732
|
+
try {
|
|
5733
|
+
await runGit(["push"], tmpDir);
|
|
5734
|
+
} catch (error) {
|
|
5735
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5736
|
+
if (message.includes("non-fast-forward") || message.includes("rejected")) {
|
|
5737
|
+
throw new Error("Push rejected (non-fast-forward). Pull and resolve manually, or delete remote contents.");
|
|
5738
|
+
}
|
|
5739
|
+
if (message.includes("Authentication") || message.includes("could not read Username")) {
|
|
5740
|
+
throw new Error("Push authentication failed. Check SSH keys or credentials.");
|
|
5741
|
+
}
|
|
5742
|
+
throw new Error(`git push failed: ${message}`);
|
|
5743
|
+
}
|
|
5744
|
+
await updateBackupConfig({ lastBackup: timestamp }).catch(() => {
|
|
5745
|
+
});
|
|
5746
|
+
return {
|
|
5747
|
+
success: true,
|
|
5748
|
+
timestamp,
|
|
5749
|
+
message: `Backed up ${categories.length} categor${categories.length === 1 ? "y" : "ies"} to ${repo}.`,
|
|
5750
|
+
committed: true
|
|
5751
|
+
};
|
|
5752
|
+
} finally {
|
|
5753
|
+
if (tmpDir) {
|
|
5754
|
+
await rm2(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
5755
|
+
});
|
|
5756
|
+
}
|
|
5757
|
+
await releaseLock(lockPath);
|
|
5758
|
+
}
|
|
5759
|
+
}
|
|
5760
|
+
async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
|
|
5761
|
+
if (isFile) {
|
|
5762
|
+
await ensureDir(resolve14(localPath, ".."));
|
|
5763
|
+
await cp(repoSrcPath, localPath, { force: true });
|
|
5764
|
+
return;
|
|
5765
|
+
}
|
|
5766
|
+
const stagingPath = `${localPath}.syntaur-restore-staging`;
|
|
5767
|
+
const backupPath = `${localPath}.syntaur-restore-backup`;
|
|
5768
|
+
await rm2(stagingPath, { recursive: true, force: true });
|
|
5769
|
+
const backupExistsBefore = await fileExists(backupPath);
|
|
5770
|
+
const localExistsBefore = await fileExists(localPath);
|
|
5771
|
+
if (backupExistsBefore) {
|
|
5772
|
+
if (!localExistsBefore) {
|
|
5773
|
+
await rename2(backupPath, localPath);
|
|
5774
|
+
} else {
|
|
5775
|
+
throw new Error(
|
|
5776
|
+
`Cannot restore "${localPath}": a stale crash-recovery backup exists at ${backupPath} while the current path also exists. Inspect both and remove the one you don't need, then retry.`
|
|
5777
|
+
);
|
|
5778
|
+
}
|
|
5779
|
+
}
|
|
5780
|
+
let localMovedAside = false;
|
|
5781
|
+
try {
|
|
5782
|
+
await cp(repoSrcPath, stagingPath, { recursive: true, force: true });
|
|
5783
|
+
const localExists = await fileExists(localPath);
|
|
5784
|
+
if (localExists) {
|
|
5785
|
+
await rename2(localPath, backupPath);
|
|
5786
|
+
localMovedAside = true;
|
|
5787
|
+
}
|
|
5788
|
+
await rename2(stagingPath, localPath);
|
|
5789
|
+
await rm2(backupPath, { recursive: true, force: true }).catch(() => {
|
|
5790
|
+
});
|
|
5791
|
+
} catch (err) {
|
|
5792
|
+
if (localMovedAside && await fileExists(backupPath)) {
|
|
5793
|
+
await rename2(backupPath, localPath).catch(() => {
|
|
5794
|
+
});
|
|
5795
|
+
}
|
|
5796
|
+
await rm2(stagingPath, { recursive: true, force: true }).catch(() => {
|
|
5797
|
+
});
|
|
5798
|
+
throw err;
|
|
5799
|
+
}
|
|
5800
|
+
}
|
|
5801
|
+
async function restoreFromGithub(overrides) {
|
|
5802
|
+
await checkGitInstalled();
|
|
5803
|
+
const config = await readConfig();
|
|
5804
|
+
const rawRepo = overrides?.repo ?? config.backup?.repo ?? null;
|
|
5805
|
+
if (!rawRepo) {
|
|
5806
|
+
throw new Error("No backup repo configured.");
|
|
5807
|
+
}
|
|
5808
|
+
const repo = rawRepo.trim();
|
|
5809
|
+
if (!validateRepoUrl(repo)) {
|
|
5810
|
+
throw new Error(`Invalid repo URL: "${rawRepo}".`);
|
|
5811
|
+
}
|
|
5812
|
+
const categoriesCsv = config.backup?.categories ?? "projects, playbooks, todos, servers, config";
|
|
5813
|
+
const categories = overrides?.categories ?? resolveCategoriesStrict(categoriesCsv);
|
|
5814
|
+
if (categories.length === 0) {
|
|
5815
|
+
throw new Error("No valid restore categories selected.");
|
|
5816
|
+
}
|
|
5817
|
+
const lockPath = await acquireLock();
|
|
5818
|
+
let tmpDir = null;
|
|
5819
|
+
const restored = [];
|
|
5820
|
+
const failed = [];
|
|
5821
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
5822
|
+
try {
|
|
5823
|
+
await updateBackupConfig({ lastRestore: timestamp });
|
|
5824
|
+
tmpDir = await mkdtemp(join2(tmpdir(), "syntaur-restore-"));
|
|
5825
|
+
await cloneOrInit(repo, tmpDir);
|
|
5826
|
+
for (const category of categories) {
|
|
5827
|
+
if (category === "config") {
|
|
5828
|
+
console.warn('Skipping "config" on restore (would overwrite local backup settings).');
|
|
5829
|
+
continue;
|
|
5830
|
+
}
|
|
5831
|
+
try {
|
|
5832
|
+
const { sourcePath: localPath, repoPath, isFile } = await resolveCategoryPath(category);
|
|
5833
|
+
const repoSrcPath = join2(tmpDir, repoPath);
|
|
5834
|
+
if (!await fileExists(repoSrcPath)) {
|
|
5835
|
+
console.warn(`Category "${category}" not found in backup repo, skipping.`);
|
|
5836
|
+
continue;
|
|
5837
|
+
}
|
|
5838
|
+
await safeRestoreCategory(localPath, repoSrcPath, isFile);
|
|
5839
|
+
restored.push(category);
|
|
5840
|
+
} catch (error) {
|
|
5841
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
5842
|
+
console.error(`Failed to restore "${category}": ${msg}`);
|
|
5843
|
+
failed.push(category);
|
|
5844
|
+
}
|
|
5845
|
+
}
|
|
5846
|
+
const success = failed.length === 0;
|
|
5847
|
+
return {
|
|
5848
|
+
success,
|
|
5849
|
+
timestamp,
|
|
5850
|
+
message: success ? `Restored ${restored.length} categor${restored.length === 1 ? "y" : "ies"} from ${repo}.` : `Partial restore: ${restored.length} succeeded, ${failed.length} failed (${failed.join(", ")}).`,
|
|
5851
|
+
committed: false
|
|
5852
|
+
};
|
|
5853
|
+
} finally {
|
|
5854
|
+
if (tmpDir) {
|
|
5855
|
+
await rm2(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
5856
|
+
});
|
|
5857
|
+
}
|
|
5858
|
+
await releaseLock(lockPath);
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5861
|
+
async function getBackupStatus() {
|
|
5862
|
+
const config = await readConfig();
|
|
5863
|
+
const lockPath = resolve14(syntaurRoot(), LOCK_FILE_NAME);
|
|
5864
|
+
const locked = await fileExists(lockPath);
|
|
5865
|
+
return {
|
|
5866
|
+
repo: config.backup?.repo ?? null,
|
|
5867
|
+
categories: config.backup?.categories ?? "projects, playbooks, todos, servers, config",
|
|
5868
|
+
lastBackup: config.backup?.lastBackup ?? null,
|
|
5869
|
+
lastRestore: config.backup?.lastRestore ?? null,
|
|
5870
|
+
locked
|
|
5871
|
+
};
|
|
5872
|
+
}
|
|
5873
|
+
|
|
5874
|
+
// src/dashboard/api-backup.ts
|
|
5875
|
+
function createBackupRouter() {
|
|
5876
|
+
const router = Router6();
|
|
5877
|
+
router.get("/", async (_req, res) => {
|
|
5878
|
+
try {
|
|
5879
|
+
const status = await getBackupStatus();
|
|
5880
|
+
res.json(status);
|
|
5881
|
+
} catch (error) {
|
|
5882
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
5883
|
+
}
|
|
5884
|
+
});
|
|
5885
|
+
router.put("/config", async (req, res) => {
|
|
5886
|
+
try {
|
|
5887
|
+
const body = req.body ?? {};
|
|
5888
|
+
const updates = {};
|
|
5889
|
+
if (body.repo !== void 0) {
|
|
5890
|
+
const trimmed = typeof body.repo === "string" ? body.repo.trim() : body.repo;
|
|
5891
|
+
if (trimmed !== null && trimmed !== "" && !validateRepoUrl(trimmed)) {
|
|
5892
|
+
return res.status(400).json({
|
|
5893
|
+
error: `Invalid repo URL. Must start with https:// or git@.`
|
|
5894
|
+
});
|
|
5895
|
+
}
|
|
5896
|
+
updates.repo = trimmed || null;
|
|
5897
|
+
}
|
|
5898
|
+
if (body.categories !== void 0) {
|
|
5899
|
+
let cats;
|
|
5900
|
+
if (Array.isArray(body.categories)) {
|
|
5901
|
+
cats = body.categories.map((s) => String(s).trim()).filter(Boolean);
|
|
5902
|
+
} else if (typeof body.categories === "string") {
|
|
5903
|
+
cats = body.categories.split(",").map((s) => s.trim()).filter(Boolean);
|
|
5904
|
+
} else {
|
|
5905
|
+
return res.status(400).json({ error: "categories must be a string or array" });
|
|
5906
|
+
}
|
|
5907
|
+
if (cats.length === 0) {
|
|
5908
|
+
return res.status(400).json({
|
|
5909
|
+
error: `No categories provided. Valid: ${VALID_CATEGORIES.join(", ")}`
|
|
5910
|
+
});
|
|
5911
|
+
}
|
|
5912
|
+
try {
|
|
5913
|
+
const valid = parseCategoriesStrict(cats);
|
|
5914
|
+
updates.categories = valid.join(", ");
|
|
5915
|
+
} catch (err) {
|
|
5916
|
+
return res.status(400).json({
|
|
5917
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5918
|
+
});
|
|
5919
|
+
}
|
|
5920
|
+
}
|
|
5921
|
+
if (Object.keys(updates).length === 0) {
|
|
5922
|
+
return res.status(400).json({ error: "No fields to update" });
|
|
5923
|
+
}
|
|
5924
|
+
await updateBackupConfig(updates);
|
|
5925
|
+
const status = await getBackupStatus();
|
|
5926
|
+
res.json(status);
|
|
5927
|
+
} catch (error) {
|
|
5928
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
5929
|
+
}
|
|
5930
|
+
});
|
|
5931
|
+
router.post("/push", async (_req, res) => {
|
|
5932
|
+
try {
|
|
5933
|
+
const result = await backupToGithub();
|
|
5934
|
+
res.json(result);
|
|
5935
|
+
} catch (error) {
|
|
5936
|
+
res.status(500).json({
|
|
5937
|
+
success: false,
|
|
5938
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5939
|
+
});
|
|
5940
|
+
}
|
|
5941
|
+
});
|
|
5942
|
+
router.post("/pull", async (_req, res) => {
|
|
5943
|
+
try {
|
|
5944
|
+
const result = await restoreFromGithub();
|
|
5945
|
+
res.json(result);
|
|
5946
|
+
} catch (error) {
|
|
5947
|
+
res.status(500).json({
|
|
5948
|
+
success: false,
|
|
5949
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5950
|
+
});
|
|
5951
|
+
}
|
|
5952
|
+
});
|
|
5953
|
+
return router;
|
|
5954
|
+
}
|
|
5955
|
+
|
|
5529
5956
|
// src/dashboard/autodiscovery.ts
|
|
5530
5957
|
init_scanner();
|
|
5531
5958
|
init_servers();
|
|
@@ -5588,7 +6015,7 @@ async function stopAutodiscovery() {
|
|
|
5588
6015
|
function runReconcile() {
|
|
5589
6016
|
if (activeReconcile || !savedOptions) return;
|
|
5590
6017
|
const opts = savedOptions;
|
|
5591
|
-
activeReconcile = reconcile(opts.serversDir, opts.
|
|
6018
|
+
activeReconcile = reconcile(opts.serversDir, opts.projectsDir, opts.excludePids).catch((err) => {
|
|
5592
6019
|
console.error("[autodiscovery] reconcile failed:", err);
|
|
5593
6020
|
}).finally(() => {
|
|
5594
6021
|
activeReconcile = null;
|
|
@@ -5599,10 +6026,10 @@ async function listAllTmuxSessions() {
|
|
|
5599
6026
|
if (!output) return [];
|
|
5600
6027
|
return output.split("\n").filter((line) => line.length > 0);
|
|
5601
6028
|
}
|
|
5602
|
-
async function discoverTmuxSessions(
|
|
6029
|
+
async function discoverTmuxSessions(serversDir2, projectsDir, existingNames) {
|
|
5603
6030
|
const tmuxAvailable = await checkTmuxAvailable();
|
|
5604
6031
|
if (!tmuxAvailable) return false;
|
|
5605
|
-
const workspaceRecords = await loadWorkspaceRecords(
|
|
6032
|
+
const workspaceRecords = await loadWorkspaceRecords(projectsDir);
|
|
5606
6033
|
if (workspaceRecords.length === 0) return false;
|
|
5607
6034
|
const sessions = await listAllTmuxSessions();
|
|
5608
6035
|
let changed = false;
|
|
@@ -5627,7 +6054,7 @@ async function discoverTmuxSessions(serversDir, missionsDir, existingNames) {
|
|
|
5627
6054
|
}
|
|
5628
6055
|
}
|
|
5629
6056
|
if (matched) {
|
|
5630
|
-
await registerAutoSession(
|
|
6057
|
+
await registerAutoSession(serversDir2, sessionName, { kind: "tmux" });
|
|
5631
6058
|
changed = true;
|
|
5632
6059
|
}
|
|
5633
6060
|
}
|
|
@@ -5643,8 +6070,8 @@ async function getProcessCwd(pid) {
|
|
|
5643
6070
|
}
|
|
5644
6071
|
return null;
|
|
5645
6072
|
}
|
|
5646
|
-
async function discoverProcesses(
|
|
5647
|
-
const workspaceRecords = await loadWorkspaceRecords(
|
|
6073
|
+
async function discoverProcesses(serversDir2, projectsDir, existingFiles, excludePids) {
|
|
6074
|
+
const workspaceRecords = await loadWorkspaceRecords(projectsDir);
|
|
5648
6075
|
if (workspaceRecords.length === 0) return false;
|
|
5649
6076
|
const lsofOutput = await getLsofOutput();
|
|
5650
6077
|
if (!lsofOutput) return false;
|
|
@@ -5668,7 +6095,7 @@ async function discoverProcesses(serversDir, missionsDir, existingFiles, exclude
|
|
|
5668
6095
|
const sanitized = sanitizeSessionName(sessionName);
|
|
5669
6096
|
if (existingFiles.has(sanitized)) continue;
|
|
5670
6097
|
const ports = parsePortsForPid(lsofOutput, proc.pid);
|
|
5671
|
-
await registerAutoSession(
|
|
6098
|
+
await registerAutoSession(serversDir2, sessionName, {
|
|
5672
6099
|
kind: "process",
|
|
5673
6100
|
pid: proc.pid,
|
|
5674
6101
|
ports: ports.length > 0 ? ports : [proc.port],
|
|
@@ -5678,7 +6105,7 @@ async function discoverProcesses(serversDir, missionsDir, existingFiles, exclude
|
|
|
5678
6105
|
}
|
|
5679
6106
|
return changed;
|
|
5680
6107
|
}
|
|
5681
|
-
async function cleanupDeadAutoSessions(
|
|
6108
|
+
async function cleanupDeadAutoSessions(serversDir2, existingFiles) {
|
|
5682
6109
|
let changed = false;
|
|
5683
6110
|
const removedNames = /* @__PURE__ */ new Set();
|
|
5684
6111
|
const tmuxAvailable = await checkTmuxAvailable();
|
|
@@ -5694,7 +6121,7 @@ async function cleanupDeadAutoSessions(serversDir, existingFiles) {
|
|
|
5694
6121
|
continue;
|
|
5695
6122
|
}
|
|
5696
6123
|
if (!alive) {
|
|
5697
|
-
await removeSession(
|
|
6124
|
+
await removeSession(serversDir2, name);
|
|
5698
6125
|
removedNames.add(name);
|
|
5699
6126
|
changed = true;
|
|
5700
6127
|
}
|
|
@@ -5709,20 +6136,20 @@ async function isProcessAlive(pid) {
|
|
|
5709
6136
|
return false;
|
|
5710
6137
|
}
|
|
5711
6138
|
}
|
|
5712
|
-
async function reconcile(
|
|
5713
|
-
const names = await listSessionFiles(
|
|
6139
|
+
async function reconcile(serversDir2, projectsDir, excludePids) {
|
|
6140
|
+
const names = await listSessionFiles(serversDir2);
|
|
5714
6141
|
const existingFiles = /* @__PURE__ */ new Map();
|
|
5715
6142
|
for (const name of names) {
|
|
5716
|
-
const data = await readSessionFile(
|
|
6143
|
+
const data = await readSessionFile(serversDir2, name);
|
|
5717
6144
|
if (data) existingFiles.set(name, data);
|
|
5718
6145
|
}
|
|
5719
|
-
const { changed: cleanupChanged, removedNames } = await cleanupDeadAutoSessions(
|
|
6146
|
+
const { changed: cleanupChanged, removedNames } = await cleanupDeadAutoSessions(serversDir2, existingFiles);
|
|
5720
6147
|
for (const name of removedNames) {
|
|
5721
6148
|
existingFiles.delete(name);
|
|
5722
6149
|
}
|
|
5723
6150
|
const existingNames = new Set(existingFiles.keys());
|
|
5724
|
-
const tmuxChanged = await discoverTmuxSessions(
|
|
5725
|
-
const processChanged = await discoverProcesses(
|
|
6151
|
+
const tmuxChanged = await discoverTmuxSessions(serversDir2, projectsDir, existingNames);
|
|
6152
|
+
const processChanged = await discoverProcesses(serversDir2, projectsDir, existingFiles, excludePids);
|
|
5726
6153
|
if (tmuxChanged || processChanged || cleanupChanged) {
|
|
5727
6154
|
clearScanCache();
|
|
5728
6155
|
}
|
|
@@ -5730,7 +6157,7 @@ async function reconcile(serversDir, missionsDir, excludePids) {
|
|
|
5730
6157
|
|
|
5731
6158
|
// src/dashboard/server.ts
|
|
5732
6159
|
function createDashboardServer(options) {
|
|
5733
|
-
const { port,
|
|
6160
|
+
const { port, projectsDir, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, serveStaticUi, dashboardDistPath } = options;
|
|
5734
6161
|
const app = express();
|
|
5735
6162
|
const server = createServer(app);
|
|
5736
6163
|
const wss = new WebSocketServer({ noServer: true });
|
|
@@ -5764,13 +6191,13 @@ function createDashboardServer(options) {
|
|
|
5764
6191
|
}
|
|
5765
6192
|
}
|
|
5766
6193
|
initSessionDb();
|
|
5767
|
-
migrateFromMarkdown(
|
|
6194
|
+
migrateFromMarkdown(projectsDir).catch((err) => {
|
|
5768
6195
|
console.error("Session migration from markdown failed:", err);
|
|
5769
6196
|
});
|
|
5770
6197
|
app.use(express.json());
|
|
5771
6198
|
app.get("/api/overview", async (_req, res) => {
|
|
5772
6199
|
try {
|
|
5773
|
-
const overview = await getOverview(
|
|
6200
|
+
const overview = await getOverview(projectsDir, serversDir2);
|
|
5774
6201
|
res.json(overview);
|
|
5775
6202
|
} catch (error) {
|
|
5776
6203
|
console.error("Error getting overview:", error);
|
|
@@ -5779,7 +6206,7 @@ function createDashboardServer(options) {
|
|
|
5779
6206
|
});
|
|
5780
6207
|
app.get("/api/attention", async (_req, res) => {
|
|
5781
6208
|
try {
|
|
5782
|
-
const attention = await getAttention(
|
|
6209
|
+
const attention = await getAttention(projectsDir, serversDir2);
|
|
5783
6210
|
res.json(attention);
|
|
5784
6211
|
} catch (error) {
|
|
5785
6212
|
console.error("Error getting attention queue:", error);
|
|
@@ -5846,26 +6273,26 @@ function createDashboardServer(options) {
|
|
|
5846
6273
|
res.status(500).json({ error: "Failed to reset status config" });
|
|
5847
6274
|
}
|
|
5848
6275
|
});
|
|
5849
|
-
app.get("/api/
|
|
6276
|
+
app.get("/api/projects", async (req, res) => {
|
|
5850
6277
|
try {
|
|
5851
|
-
let
|
|
6278
|
+
let projects = await listProjects(projectsDir);
|
|
5852
6279
|
const workspaceParam = req.query.workspace;
|
|
5853
6280
|
if (workspaceParam) {
|
|
5854
6281
|
if (workspaceParam === "_ungrouped") {
|
|
5855
|
-
|
|
6282
|
+
projects = projects.filter((m) => m.workspace === null);
|
|
5856
6283
|
} else {
|
|
5857
|
-
|
|
6284
|
+
projects = projects.filter((m) => m.workspace === workspaceParam);
|
|
5858
6285
|
}
|
|
5859
6286
|
}
|
|
5860
|
-
res.json(
|
|
6287
|
+
res.json(projects);
|
|
5861
6288
|
} catch (error) {
|
|
5862
|
-
console.error("Error listing
|
|
5863
|
-
res.status(500).json({ error: "Failed to list
|
|
6289
|
+
console.error("Error listing projects:", error);
|
|
6290
|
+
res.status(500).json({ error: "Failed to list projects" });
|
|
5864
6291
|
}
|
|
5865
6292
|
});
|
|
5866
6293
|
app.get("/api/workspaces", async (_req, res) => {
|
|
5867
6294
|
try {
|
|
5868
|
-
const result = await listWorkspaces(
|
|
6295
|
+
const result = await listWorkspaces(projectsDir);
|
|
5869
6296
|
res.json(result);
|
|
5870
6297
|
} catch (error) {
|
|
5871
6298
|
console.error("Error listing workspaces:", error);
|
|
@@ -5879,8 +6306,8 @@ function createDashboardServer(options) {
|
|
|
5879
6306
|
res.status(400).json({ error: "Invalid workspace name. Use lowercase letters, numbers, and hyphens." });
|
|
5880
6307
|
return;
|
|
5881
6308
|
}
|
|
5882
|
-
await createWorkspace(
|
|
5883
|
-
broadcast({ type: "
|
|
6309
|
+
await createWorkspace(projectsDir, name);
|
|
6310
|
+
broadcast({ type: "project-updated", projectSlug: "", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5884
6311
|
res.json({ name });
|
|
5885
6312
|
} catch (error) {
|
|
5886
6313
|
console.error("Error creating workspace:", error);
|
|
@@ -5889,8 +6316,8 @@ function createDashboardServer(options) {
|
|
|
5889
6316
|
});
|
|
5890
6317
|
app.delete("/api/workspaces/:name", async (req, res) => {
|
|
5891
6318
|
try {
|
|
5892
|
-
await deleteWorkspace(
|
|
5893
|
-
broadcast({ type: "
|
|
6319
|
+
await deleteWorkspace(projectsDir, req.params.name);
|
|
6320
|
+
broadcast({ type: "project-updated", projectSlug: "", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5894
6321
|
res.json({ ok: true });
|
|
5895
6322
|
} catch (error) {
|
|
5896
6323
|
console.error("Error deleting workspace:", error);
|
|
@@ -5899,13 +6326,13 @@ function createDashboardServer(options) {
|
|
|
5899
6326
|
});
|
|
5900
6327
|
app.get("/api/assignments", async (req, res) => {
|
|
5901
6328
|
try {
|
|
5902
|
-
const result = await listAssignmentsBoard(
|
|
6329
|
+
const result = await listAssignmentsBoard(projectsDir);
|
|
5903
6330
|
const workspaceParam = req.query.workspace;
|
|
5904
6331
|
if (workspaceParam) {
|
|
5905
6332
|
if (workspaceParam === "_ungrouped") {
|
|
5906
|
-
result.assignments = result.assignments.filter((a) => a.
|
|
6333
|
+
result.assignments = result.assignments.filter((a) => a.projectWorkspace === null);
|
|
5907
6334
|
} else {
|
|
5908
|
-
result.assignments = result.assignments.filter((a) => a.
|
|
6335
|
+
result.assignments = result.assignments.filter((a) => a.projectWorkspace === workspaceParam);
|
|
5909
6336
|
}
|
|
5910
6337
|
}
|
|
5911
6338
|
res.json(result);
|
|
@@ -5914,29 +6341,29 @@ function createDashboardServer(options) {
|
|
|
5914
6341
|
res.status(500).json({ error: "Failed to list assignments" });
|
|
5915
6342
|
}
|
|
5916
6343
|
});
|
|
5917
|
-
app.get("/api/
|
|
6344
|
+
app.get("/api/projects/:slug", async (req, res) => {
|
|
5918
6345
|
try {
|
|
5919
|
-
const detail = await
|
|
6346
|
+
const detail = await getProjectDetail(projectsDir, req.params.slug);
|
|
5920
6347
|
if (!detail) {
|
|
5921
|
-
res.status(404).json({ error: `
|
|
6348
|
+
res.status(404).json({ error: `Project "${req.params.slug}" not found` });
|
|
5922
6349
|
return;
|
|
5923
6350
|
}
|
|
5924
6351
|
res.json(detail);
|
|
5925
6352
|
} catch (error) {
|
|
5926
|
-
console.error("Error getting
|
|
5927
|
-
res.status(500).json({ error: "Failed to get
|
|
6353
|
+
console.error("Error getting project detail:", error);
|
|
6354
|
+
res.status(500).json({ error: "Failed to get project detail" });
|
|
5928
6355
|
}
|
|
5929
6356
|
});
|
|
5930
|
-
app.get("/api/
|
|
6357
|
+
app.get("/api/projects/:slug/assignments/:aslug", async (req, res) => {
|
|
5931
6358
|
try {
|
|
5932
6359
|
const detail = await getAssignmentDetail(
|
|
5933
|
-
|
|
6360
|
+
projectsDir,
|
|
5934
6361
|
req.params.slug,
|
|
5935
6362
|
req.params.aslug
|
|
5936
6363
|
);
|
|
5937
6364
|
if (!detail) {
|
|
5938
6365
|
res.status(404).json({
|
|
5939
|
-
error: `Assignment "${req.params.aslug}" not found in
|
|
6366
|
+
error: `Assignment "${req.params.aslug}" not found in project "${req.params.slug}"`
|
|
5940
6367
|
});
|
|
5941
6368
|
return;
|
|
5942
6369
|
}
|
|
@@ -5946,15 +6373,16 @@ function createDashboardServer(options) {
|
|
|
5946
6373
|
res.status(500).json({ error: "Failed to get assignment detail" });
|
|
5947
6374
|
}
|
|
5948
6375
|
});
|
|
5949
|
-
app.use(createWriteRouter(
|
|
5950
|
-
app.use("/api/servers", createServersRouter(
|
|
5951
|
-
app.use("/api/agent-sessions", createAgentSessionsRouter(
|
|
5952
|
-
app.use("/api/playbooks", createPlaybooksRouter(
|
|
5953
|
-
app.use("/api/todos", createTodosRouter(
|
|
6376
|
+
app.use(createWriteRouter(projectsDir));
|
|
6377
|
+
app.use("/api/servers", createServersRouter(serversDir2, projectsDir));
|
|
6378
|
+
app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir, broadcast));
|
|
6379
|
+
app.use("/api/playbooks", createPlaybooksRouter(playbooksDir2));
|
|
6380
|
+
app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
|
|
6381
|
+
app.use("/api/backup", createBackupRouter());
|
|
5954
6382
|
if (serveStaticUi && dashboardDistPath) {
|
|
5955
6383
|
app.use(express.static(dashboardDistPath));
|
|
5956
6384
|
app.get("{*path}", async (_req, res) => {
|
|
5957
|
-
const indexPath =
|
|
6385
|
+
const indexPath = resolve15(dashboardDistPath, "index.html");
|
|
5958
6386
|
if (await fileExists(indexPath)) {
|
|
5959
6387
|
res.sendFile(indexPath);
|
|
5960
6388
|
} else {
|
|
@@ -5968,13 +6396,13 @@ function createDashboardServer(options) {
|
|
|
5968
6396
|
return {
|
|
5969
6397
|
async start() {
|
|
5970
6398
|
watcherHandle = createWatcher({
|
|
5971
|
-
|
|
5972
|
-
serversDir,
|
|
5973
|
-
playbooksDir,
|
|
5974
|
-
todosDir,
|
|
6399
|
+
projectsDir,
|
|
6400
|
+
serversDir: serversDir2,
|
|
6401
|
+
playbooksDir: playbooksDir2,
|
|
6402
|
+
todosDir: todosDir2,
|
|
5975
6403
|
onMessage: broadcast
|
|
5976
6404
|
});
|
|
5977
|
-
startAutodiscovery({ serversDir,
|
|
6405
|
+
startAutodiscovery({ serversDir: serversDir2, projectsDir, excludePids: /* @__PURE__ */ new Set([process.pid]) });
|
|
5978
6406
|
return new Promise((resolvePromise, reject) => {
|
|
5979
6407
|
server.on("error", (err) => {
|
|
5980
6408
|
if (err.code === "EADDRINUSE") {
|
|
@@ -5986,8 +6414,8 @@ function createDashboardServer(options) {
|
|
|
5986
6414
|
}
|
|
5987
6415
|
});
|
|
5988
6416
|
server.listen(port, () => {
|
|
5989
|
-
const portFile =
|
|
5990
|
-
|
|
6417
|
+
const portFile = resolve15(syntaurRoot(), "dashboard-port");
|
|
6418
|
+
writeFile4(portFile, String(port), "utf-8").catch(() => {
|
|
5991
6419
|
});
|
|
5992
6420
|
resolvePromise();
|
|
5993
6421
|
});
|
|
@@ -6000,12 +6428,13 @@ function createDashboardServer(options) {
|
|
|
6000
6428
|
}
|
|
6001
6429
|
closeSessionDb();
|
|
6002
6430
|
for (const client of clients) {
|
|
6003
|
-
client.
|
|
6431
|
+
client.terminate();
|
|
6004
6432
|
}
|
|
6005
6433
|
clients.clear();
|
|
6006
|
-
const portFile =
|
|
6007
|
-
await
|
|
6434
|
+
const portFile = resolve15(syntaurRoot(), "dashboard-port");
|
|
6435
|
+
await unlink4(portFile).catch(() => {
|
|
6008
6436
|
});
|
|
6437
|
+
server.closeAllConnections?.();
|
|
6009
6438
|
return new Promise((resolvePromise) => {
|
|
6010
6439
|
server.close(() => resolvePromise());
|
|
6011
6440
|
});
|