syntaur 0.1.14 → 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-eih-KlEh.js → _basePickBy-CHKX1r7P.js} +1 -1
- package/dashboard/dist/assets/{_baseUniq-M21wg9ZQ.js → _baseUniq-CTxTc4MS.js} +1 -1
- package/dashboard/dist/assets/{arc-uKZMelpQ.js → arc-BUo5zftd.js} +1 -1
- package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-CpMG5exj.js → architectureDiagram-2XIMDMQ5-CrJLm-P0.js} +1 -1
- package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-BHnCCKl_.js → blockDiagram-WCTKOSBZ-BK60lBBJ.js} +1 -1
- package/dashboard/dist/assets/{c4Diagram-IC4MRINW-B-n3zU9i.js → c4Diagram-IC4MRINW-C7oJEvA0.js} +1 -1
- package/dashboard/dist/assets/channel-DdltvFFH.js +1 -0
- package/dashboard/dist/assets/{chunk-4BX2VUAB-ChD9Iuih.js → chunk-4BX2VUAB-CjUPlzHz.js} +1 -1
- package/dashboard/dist/assets/{chunk-55IACEB6-B3vP9Psg.js → chunk-55IACEB6-6HmWguiO.js} +1 -1
- package/dashboard/dist/assets/{chunk-FMBD7UC4-CIhWgxPS.js → chunk-FMBD7UC4-CLuJnd1b.js} +1 -1
- package/dashboard/dist/assets/{chunk-JSJVCQXG-DiGIV_cB.js → chunk-JSJVCQXG-B4d62qWV.js} +1 -1
- package/dashboard/dist/assets/{chunk-KX2RTZJC-DnGsx5jo.js → chunk-KX2RTZJC-AsEKRPq2.js} +1 -1
- package/dashboard/dist/assets/{chunk-NQ4KR5QH-BFBu1fmg.js → chunk-NQ4KR5QH-DQhHHvwY.js} +1 -1
- package/dashboard/dist/assets/{chunk-QZHKN3VN-DYtumHth.js → chunk-QZHKN3VN-Ds1TtI3E.js} +1 -1
- package/dashboard/dist/assets/{chunk-WL4C6EOR-BzCrQPuw.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-Bl8mb5eY.js → cose-bilkent-S5V4N54A-C9ka5v1m.js} +1 -1
- package/dashboard/dist/assets/{dagre-KLK3FWXG-BHffcOgo.js → dagre-KLK3FWXG-BbgPQBKy.js} +1 -1
- package/dashboard/dist/assets/{diagram-E7M64L7V-Ib83qzT_.js → diagram-E7M64L7V-DpdeZFD4.js} +1 -1
- package/dashboard/dist/assets/{diagram-IFDJBPK2-hOdh63_T.js → diagram-IFDJBPK2-FlHLQzOV.js} +1 -1
- package/dashboard/dist/assets/{diagram-P4PSJMXO-D4ocLmc5.js → diagram-P4PSJMXO-B22NkEF_.js} +1 -1
- package/dashboard/dist/assets/{erDiagram-INFDFZHY-CHJ6zqnJ.js → erDiagram-INFDFZHY-zSqmtDid.js} +1 -1
- package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-DEz5g2Ye.js → flowDiagram-PKNHOUZH-BP_0XmVV.js} +1 -1
- package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-BSftxDHA.js → ganttDiagram-A5KZAMGK-8uRyYgZV.js} +1 -1
- package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-Cr3vGf07.js → gitGraphDiagram-K3NZZRJ6-JFqg8sv4.js} +1 -1
- package/dashboard/dist/assets/{graph-D4us8trI.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-CH_jVfru.js → infoDiagram-LFFYTUFH-C3kq7Nbv.js} +1 -1
- package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-BdKLa5GC.js → ishikawaDiagram-PHBUUO56-Kqi4EZ-n.js} +1 -1
- package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-C_SMzNGF.js → journeyDiagram-4ABVD52K-CTfv0Wcr.js} +1 -1
- package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-BeA-egRW.js → kanban-definition-K7BYSVSG-Dmx0lgvR.js} +1 -1
- package/dashboard/dist/assets/{layout-B8tDmL4j.js → layout-KKRbT2Od.js} +1 -1
- package/dashboard/dist/assets/{linear-CeGJyrHS.js → linear-5egaBiw7.js} +1 -1
- package/dashboard/dist/assets/{mermaid.core-DyEs-LPd.js → mermaid.core-C9pF_oFQ.js} +4 -4
- package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-DCxzAr8m.js → mindmap-definition-YRQLILUH-C7HXYEXt.js} +1 -1
- package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CEj5dRDi.js → pieDiagram-SKSYHLDU-DkdZm-YP.js} +1 -1
- package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-CKfvAEQg.js → quadrantDiagram-337W2JSQ-DkcRJs5F.js} +1 -1
- package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-CTRqKPtJ.js → requirementDiagram-Z7DCOOCP-BaTDVYTl.js} +1 -1
- package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-BlYbz8UR.js → sankeyDiagram-WA2Y5GQK-DvPLbGV5.js} +1 -1
- package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-PT2t7ryQ.js → sequenceDiagram-2WXFIKYE-DQoZ2xMK.js} +1 -1
- package/dashboard/dist/assets/{stateDiagram-RAJIS63D-eDX7IUuV.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-By11B1Ow.js → timeline-definition-YZTLITO2-aC0iCFCW.js} +1 -1
- package/dashboard/dist/assets/{treemap-KZPCXAKY-rvdLeWWV.js → treemap-KZPCXAKY-Ie-PFjgx.js} +1 -1
- package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-Br_oZ1wv.js → vennDiagram-LZ73GAT5-CJN3ExTQ.js} +1 -1
- package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-D-MWVqrT.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 +585 -641
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +3388 -1315
- 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-DVBgSlOI.js +0 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-B7dxBacd.js +0 -1
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-B7dxBacd.js +0 -1
- package/dashboard/dist/assets/clone-DAOrHcCC.js +0 -1
- package/dashboard/dist/assets/index-AXntWS_w.css +0 -1
- package/dashboard/dist/assets/index-CEMjexkj.js +0 -460
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO--yuSBnLh.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,41 +410,11 @@ 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
|
-
function serversDir() {
|
|
393
|
-
return resolve2(syntaurRoot(), "servers");
|
|
394
|
-
}
|
|
395
|
-
function playbooksDir() {
|
|
396
|
-
return resolve2(syntaurRoot(), "playbooks");
|
|
397
|
-
}
|
|
398
|
-
function todosDir() {
|
|
399
|
-
return resolve2(syntaurRoot(), "todos");
|
|
400
|
-
}
|
|
401
|
-
var init_paths = __esm({
|
|
402
|
-
"src/utils/paths.ts"() {
|
|
403
|
-
"use strict";
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
|
|
407
413
|
// src/templates/config.ts
|
|
408
414
|
function renderConfig(params) {
|
|
409
415
|
return `---
|
|
410
416
|
version: "1.0"
|
|
411
|
-
|
|
417
|
+
defaultProjectDir: ${params.defaultProjectDir}
|
|
412
418
|
onboarding:
|
|
413
419
|
completed: false
|
|
414
420
|
agentDefaults:
|
|
@@ -416,7 +422,7 @@ agentDefaults:
|
|
|
416
422
|
autoApprove: false
|
|
417
423
|
backup:
|
|
418
424
|
repo: null
|
|
419
|
-
categories:
|
|
425
|
+
categories: projects, playbooks, todos, servers, config
|
|
420
426
|
lastBackup: null
|
|
421
427
|
lastRestore: null
|
|
422
428
|
---
|
|
@@ -628,8 +634,8 @@ async function writeStatusConfig(statuses) {
|
|
|
628
634
|
const statusBlock = serializeStatusConfig(statuses);
|
|
629
635
|
if (!await fileExists(configPath)) {
|
|
630
636
|
const content = `---
|
|
631
|
-
version: "
|
|
632
|
-
|
|
637
|
+
version: "2.0"
|
|
638
|
+
defaultProjectDir: ~/projects
|
|
633
639
|
${statusBlock}
|
|
634
640
|
---
|
|
635
641
|
`;
|
|
@@ -640,7 +646,7 @@ ${statusBlock}
|
|
|
640
646
|
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
641
647
|
if (!fmMatch) {
|
|
642
648
|
const content = `---
|
|
643
|
-
version: "
|
|
649
|
+
version: "2.0"
|
|
644
650
|
${statusBlock}
|
|
645
651
|
---
|
|
646
652
|
${existing}`;
|
|
@@ -696,18 +702,18 @@ async function updateBackupConfig(backup) {
|
|
|
696
702
|
const current = (await readConfig()).backup;
|
|
697
703
|
const nextBackup = {
|
|
698
704
|
repo: current?.repo ?? null,
|
|
699
|
-
categories: current?.categories ?? "
|
|
705
|
+
categories: current?.categories ?? "projects, playbooks, todos, servers, config",
|
|
700
706
|
lastBackup: current?.lastBackup ?? null,
|
|
701
707
|
lastRestore: current?.lastRestore ?? null,
|
|
702
708
|
...backup
|
|
703
709
|
};
|
|
704
710
|
const backupBlock = serializeBackupConfig(nextBackup);
|
|
705
|
-
const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({
|
|
711
|
+
const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
706
712
|
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
707
713
|
if (!fmMatch) {
|
|
708
714
|
const content = `---
|
|
709
|
-
version: "
|
|
710
|
-
|
|
715
|
+
version: "2.0"
|
|
716
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
711
717
|
${backupBlock}
|
|
712
718
|
---
|
|
713
719
|
${existing}`;
|
|
@@ -736,16 +742,16 @@ async function readConfig() {
|
|
|
736
742
|
console.warn("Warning: ~/.syntaur/config.md has malformed frontmatter, using defaults");
|
|
737
743
|
return { ...DEFAULT_CONFIG };
|
|
738
744
|
}
|
|
739
|
-
let
|
|
740
|
-
if (!isAbsolute(
|
|
745
|
+
let projectDir = fm["defaultProjectDir"] ? expandHome(String(fm["defaultProjectDir"])) : DEFAULT_CONFIG.defaultProjectDir;
|
|
746
|
+
if (!isAbsolute(projectDir)) {
|
|
741
747
|
console.warn(
|
|
742
|
-
`Warning: config.md
|
|
748
|
+
`Warning: config.md defaultProjectDir is not an absolute path ("${fm["defaultProjectDir"]}"), using default`
|
|
743
749
|
);
|
|
744
|
-
|
|
750
|
+
projectDir = DEFAULT_CONFIG.defaultProjectDir;
|
|
745
751
|
}
|
|
746
752
|
return {
|
|
747
753
|
version: fm["version"] || DEFAULT_CONFIG.version,
|
|
748
|
-
|
|
754
|
+
defaultProjectDir: projectDir,
|
|
749
755
|
onboarding: {
|
|
750
756
|
completed: fm["onboarding.completed"] === "true"
|
|
751
757
|
},
|
|
@@ -769,11 +775,12 @@ async function readConfig() {
|
|
|
769
775
|
},
|
|
770
776
|
backup: fm["backup.repo"] || fm["backup.categories"] ? {
|
|
771
777
|
repo: fm["backup.repo"] && fm["backup.repo"] !== "null" ? fm["backup.repo"] : null,
|
|
772
|
-
categories: fm["backup.categories"] || "
|
|
778
|
+
categories: fm["backup.categories"] || "projects, playbooks, todos, servers, config",
|
|
773
779
|
lastBackup: fm["backup.lastBackup"] && fm["backup.lastBackup"] !== "null" ? fm["backup.lastBackup"] : null,
|
|
774
780
|
lastRestore: fm["backup.lastRestore"] && fm["backup.lastRestore"] !== "null" ? fm["backup.lastRestore"] : null
|
|
775
781
|
} : null,
|
|
776
|
-
statuses: parseStatusConfig(content)
|
|
782
|
+
statuses: parseStatusConfig(content),
|
|
783
|
+
types: null
|
|
777
784
|
};
|
|
778
785
|
}
|
|
779
786
|
var DEFAULT_CONFIG;
|
|
@@ -784,8 +791,8 @@ var init_config2 = __esm({
|
|
|
784
791
|
init_fs();
|
|
785
792
|
init_config();
|
|
786
793
|
DEFAULT_CONFIG = {
|
|
787
|
-
version: "
|
|
788
|
-
|
|
794
|
+
version: "2.0",
|
|
795
|
+
defaultProjectDir: defaultProjectDir(),
|
|
789
796
|
onboarding: {
|
|
790
797
|
completed: false
|
|
791
798
|
},
|
|
@@ -799,7 +806,8 @@ var init_config2 = __esm({
|
|
|
799
806
|
codexMarketplacePath: null
|
|
800
807
|
},
|
|
801
808
|
backup: null,
|
|
802
|
-
statuses: null
|
|
809
|
+
statuses: null,
|
|
810
|
+
types: null
|
|
803
811
|
};
|
|
804
812
|
}
|
|
805
813
|
});
|
|
@@ -852,7 +860,7 @@ function parseListField(frontmatter, fieldName) {
|
|
|
852
860
|
}
|
|
853
861
|
return results;
|
|
854
862
|
}
|
|
855
|
-
function
|
|
863
|
+
function parseProject(fileContent) {
|
|
856
864
|
const [fm, body] = extractFrontmatter2(fileContent);
|
|
857
865
|
return {
|
|
858
866
|
id: getField(fm, "id") ?? "",
|
|
@@ -883,13 +891,13 @@ function parseStatus(fileContent) {
|
|
|
883
891
|
}
|
|
884
892
|
}
|
|
885
893
|
return {
|
|
886
|
-
|
|
894
|
+
project: getField(fm, "project") ?? "",
|
|
887
895
|
status: getField(fm, "status") ?? "pending",
|
|
888
896
|
progress,
|
|
889
897
|
needsAttention: {
|
|
890
898
|
blockedCount: parseInt(getNestedField(fm, "needsAttention", "blockedCount") ?? "0", 10),
|
|
891
899
|
failedCount: parseInt(getNestedField(fm, "needsAttention", "failedCount") ?? "0", 10),
|
|
892
|
-
|
|
900
|
+
openQuestions: parseInt(getNestedField(fm, "needsAttention", "openQuestions") ?? "0", 10)
|
|
893
901
|
},
|
|
894
902
|
body
|
|
895
903
|
};
|
|
@@ -931,6 +939,8 @@ function parseAssignmentFull(fileContent) {
|
|
|
931
939
|
id: getField(fm, "id") ?? "",
|
|
932
940
|
slug: getField(fm, "slug") ?? "",
|
|
933
941
|
title: getField(fm, "title") ?? "",
|
|
942
|
+
project: getField(fm, "project"),
|
|
943
|
+
type: getField(fm, "type"),
|
|
934
944
|
status: getField(fm, "status") ?? "pending",
|
|
935
945
|
priority: getField(fm, "priority") ?? "medium",
|
|
936
946
|
assignee: getField(fm, "assignee"),
|
|
@@ -1050,17 +1060,17 @@ async function getDashboardHelp() {
|
|
|
1050
1060
|
return {
|
|
1051
1061
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1052
1062
|
whatIsSyntaur: {
|
|
1053
|
-
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.",
|
|
1054
1064
|
bullets: [
|
|
1055
1065
|
"Markdown files are the source of truth.",
|
|
1056
|
-
"The UI reads
|
|
1066
|
+
"The UI reads project folders, assignment files, and derived indexes from the local filesystem.",
|
|
1057
1067
|
"Derived underscore-prefixed files are projections, not the canonical edit target."
|
|
1058
1068
|
]
|
|
1059
1069
|
},
|
|
1060
1070
|
coreConcepts: [
|
|
1061
1071
|
{
|
|
1062
|
-
term: "
|
|
1063
|
-
description: "A
|
|
1072
|
+
term: "Project",
|
|
1073
|
+
description: "A project is the higher-level objective. It owns assignments, shared resources, and project memories."
|
|
1064
1074
|
},
|
|
1065
1075
|
{
|
|
1066
1076
|
term: "Assignment",
|
|
@@ -1068,15 +1078,15 @@ async function getDashboardHelp() {
|
|
|
1068
1078
|
},
|
|
1069
1079
|
{
|
|
1070
1080
|
term: "Resource",
|
|
1071
|
-
description: "A
|
|
1081
|
+
description: "A project-level shared reference file that provides source material or constraints for the work."
|
|
1072
1082
|
},
|
|
1073
1083
|
{
|
|
1074
1084
|
term: "Memory",
|
|
1075
|
-
description: "A
|
|
1085
|
+
description: "A project-level learning or pattern captured during execution so future assignments can reuse it."
|
|
1076
1086
|
},
|
|
1077
1087
|
{
|
|
1078
1088
|
term: "Manifest",
|
|
1079
|
-
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."
|
|
1080
1090
|
},
|
|
1081
1091
|
{
|
|
1082
1092
|
term: "Derived file",
|
|
@@ -1112,12 +1122,12 @@ async function getDashboardHelp() {
|
|
|
1112
1122
|
ownershipRules: [
|
|
1113
1123
|
{
|
|
1114
1124
|
label: "Human-authored files",
|
|
1115
|
-
files: ["
|
|
1116
|
-
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."
|
|
1117
1127
|
},
|
|
1118
1128
|
{
|
|
1119
1129
|
label: "Assignment working files",
|
|
1120
|
-
files: ["assignment.md", "plan
|
|
1130
|
+
files: ["assignment.md", "plan*.md (optional, versioned)", "scratchpad.md"],
|
|
1121
1131
|
description: "These are agent-writable files. The dashboard lets you edit the source markdown while preserving unsupported frontmatter keys."
|
|
1122
1132
|
},
|
|
1123
1133
|
{
|
|
@@ -1139,13 +1149,13 @@ async function getDashboardHelp() {
|
|
|
1139
1149
|
href: "/"
|
|
1140
1150
|
},
|
|
1141
1151
|
{
|
|
1142
|
-
label: "
|
|
1143
|
-
description: "Browse, search, filter, and sort the
|
|
1144
|
-
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"
|
|
1145
1155
|
},
|
|
1146
1156
|
{
|
|
1147
1157
|
label: "Assignments",
|
|
1148
|
-
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.",
|
|
1149
1159
|
href: "/assignments"
|
|
1150
1160
|
},
|
|
1151
1161
|
{
|
|
@@ -1179,14 +1189,14 @@ async function getDashboardHelp() {
|
|
|
1179
1189
|
href: "/settings"
|
|
1180
1190
|
},
|
|
1181
1191
|
{
|
|
1182
|
-
label: "
|
|
1183
|
-
description: "The
|
|
1184
|
-
href: "/
|
|
1192
|
+
label: "Project page",
|
|
1193
|
+
description: "The project workspace shows health stats, assignment list, dependency graph, shared resources, and memories.",
|
|
1194
|
+
href: "/projects"
|
|
1185
1195
|
},
|
|
1186
1196
|
{
|
|
1187
1197
|
label: "Assignment page",
|
|
1188
1198
|
description: "The assignment workspace shows lifecycle actions, plan editor, scratchpad, handoff log, decision records, and agent sessions.",
|
|
1189
|
-
href: "/
|
|
1199
|
+
href: "/projects"
|
|
1190
1200
|
}
|
|
1191
1201
|
],
|
|
1192
1202
|
faq: [
|
|
@@ -1223,16 +1233,16 @@ async function getDashboardHelp() {
|
|
|
1223
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."
|
|
1224
1234
|
}
|
|
1225
1235
|
],
|
|
1226
|
-
|
|
1236
|
+
firstProjectChecklist: [
|
|
1227
1237
|
{
|
|
1228
|
-
title: "Create the
|
|
1229
|
-
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.",
|
|
1230
1240
|
command: CLI_COMMANDS[1],
|
|
1231
|
-
href: "/create/
|
|
1241
|
+
href: "/create/project"
|
|
1232
1242
|
},
|
|
1233
1243
|
{
|
|
1234
1244
|
title: "Create at least one assignment",
|
|
1235
|
-
detail: "Break the
|
|
1245
|
+
detail: "Break the project into executable work units with explicit priority and dependencies.",
|
|
1236
1246
|
command: CLI_COMMANDS[2]
|
|
1237
1247
|
},
|
|
1238
1248
|
{
|
|
@@ -1242,8 +1252,8 @@ async function getDashboardHelp() {
|
|
|
1242
1252
|
},
|
|
1243
1253
|
{
|
|
1244
1254
|
title: "Use the assignment workspace for execution",
|
|
1245
|
-
detail: "Keep the objective in assignment.md,
|
|
1246
|
-
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"
|
|
1247
1257
|
},
|
|
1248
1258
|
{
|
|
1249
1259
|
title: "Record handoffs and decisions without rewriting history",
|
|
@@ -1257,14 +1267,14 @@ async function getDashboardHelp() {
|
|
|
1257
1267
|
],
|
|
1258
1268
|
links: [
|
|
1259
1269
|
{ label: "Overview", href: "/" },
|
|
1260
|
-
{ label: "
|
|
1270
|
+
{ label: "Project Directory", href: "/projects" },
|
|
1261
1271
|
{ label: "Assignments Board", href: "/assignments" },
|
|
1262
1272
|
{ label: "Attention Queue", href: "/attention" },
|
|
1263
1273
|
{ label: "Servers", href: "/servers" },
|
|
1264
1274
|
{ label: "Agent Sessions", href: "/agent-sessions" },
|
|
1265
1275
|
{ label: "Playbooks", href: "/playbooks" },
|
|
1266
1276
|
{ label: "Settings", href: "/settings" },
|
|
1267
|
-
{ label: "Create
|
|
1277
|
+
{ label: "Create Project", href: "/create/project" }
|
|
1268
1278
|
]
|
|
1269
1279
|
};
|
|
1270
1280
|
}
|
|
@@ -1286,60 +1296,60 @@ var init_help = __esm({
|
|
|
1286
1296
|
example: "syntaur init"
|
|
1287
1297
|
},
|
|
1288
1298
|
{
|
|
1289
|
-
command: "syntaur create-
|
|
1290
|
-
description: "Create a new
|
|
1291
|
-
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"'
|
|
1292
1302
|
},
|
|
1293
1303
|
{
|
|
1294
1304
|
command: "syntaur create-assignment",
|
|
1295
|
-
description: "Create a new assignment inside a
|
|
1296
|
-
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'
|
|
1297
1307
|
},
|
|
1298
1308
|
{
|
|
1299
1309
|
command: "syntaur assign",
|
|
1300
1310
|
description: "Set the assignee for an assignment before work begins.",
|
|
1301
|
-
example: "syntaur assign implement-overview --
|
|
1311
|
+
example: "syntaur assign implement-overview --project ui-overhaul --agent codex-1"
|
|
1302
1312
|
},
|
|
1303
1313
|
// --- Lifecycle transitions (indices 5-11) ---
|
|
1304
1314
|
{
|
|
1305
1315
|
command: "syntaur start",
|
|
1306
1316
|
description: "Transition an assignment to in_progress.",
|
|
1307
|
-
example: "syntaur start implement-overview --
|
|
1317
|
+
example: "syntaur start implement-overview --project ui-overhaul"
|
|
1308
1318
|
},
|
|
1309
1319
|
{
|
|
1310
1320
|
command: "syntaur review",
|
|
1311
1321
|
description: "Move active work into review once implementation is ready for inspection.",
|
|
1312
|
-
example: "syntaur review implement-overview --
|
|
1322
|
+
example: "syntaur review implement-overview --project ui-overhaul"
|
|
1313
1323
|
},
|
|
1314
1324
|
{
|
|
1315
1325
|
command: "syntaur complete",
|
|
1316
1326
|
description: "Mark an assignment completed after review or direct completion.",
|
|
1317
|
-
example: "syntaur complete implement-overview --
|
|
1327
|
+
example: "syntaur complete implement-overview --project ui-overhaul"
|
|
1318
1328
|
},
|
|
1319
1329
|
{
|
|
1320
1330
|
command: "syntaur block",
|
|
1321
1331
|
description: "Mark an assignment blocked and record the explicit reason.",
|
|
1322
|
-
example: 'syntaur block implement-overview --
|
|
1332
|
+
example: 'syntaur block implement-overview --project ui-overhaul --reason "Waiting on API spec"'
|
|
1323
1333
|
},
|
|
1324
1334
|
{
|
|
1325
1335
|
command: "syntaur unblock",
|
|
1326
1336
|
description: "Move a blocked assignment back to in_progress after the blocker is cleared.",
|
|
1327
|
-
example: "syntaur unblock implement-overview --
|
|
1337
|
+
example: "syntaur unblock implement-overview --project ui-overhaul"
|
|
1328
1338
|
},
|
|
1329
1339
|
{
|
|
1330
1340
|
command: "syntaur fail",
|
|
1331
1341
|
description: "Mark an assignment failed when it cannot be completed as planned.",
|
|
1332
|
-
example: "syntaur fail implement-overview --
|
|
1342
|
+
example: "syntaur fail implement-overview --project ui-overhaul"
|
|
1333
1343
|
},
|
|
1334
1344
|
{
|
|
1335
1345
|
command: "syntaur reopen",
|
|
1336
1346
|
description: "Reopen a completed or failed assignment back to in_progress.",
|
|
1337
|
-
example: "syntaur reopen implement-overview --
|
|
1347
|
+
example: "syntaur reopen implement-overview --project ui-overhaul"
|
|
1338
1348
|
},
|
|
1339
1349
|
// --- Dashboard (index 12) ---
|
|
1340
1350
|
{
|
|
1341
1351
|
command: "syntaur dashboard",
|
|
1342
|
-
description: "Start the local dashboard UI over the
|
|
1352
|
+
description: "Start the local dashboard UI over the project files on disk.",
|
|
1343
1353
|
example: "syntaur dashboard --port 4800"
|
|
1344
1354
|
},
|
|
1345
1355
|
// --- Plugin & adapter setup (indices 13-16) ---
|
|
@@ -1361,18 +1371,18 @@ var init_help = __esm({
|
|
|
1361
1371
|
{
|
|
1362
1372
|
command: "syntaur setup-adapter",
|
|
1363
1373
|
description: "Generate adapter instruction files for cursor, codex, or opencode in the current directory.",
|
|
1364
|
-
example: "syntaur setup-adapter cursor --
|
|
1374
|
+
example: "syntaur setup-adapter cursor --project ui-overhaul --assignment implement-overview"
|
|
1365
1375
|
},
|
|
1366
1376
|
// --- Session & server tracking (index 17) ---
|
|
1367
1377
|
{
|
|
1368
1378
|
command: "syntaur track-session",
|
|
1369
|
-
description: "Register an agent session, optionally linked to a
|
|
1370
|
-
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"
|
|
1371
1381
|
},
|
|
1372
1382
|
// --- Browsing & playbooks (indices 18-20) ---
|
|
1373
1383
|
{
|
|
1374
1384
|
command: "syntaur browse",
|
|
1375
|
-
description: "Interactive TUI browser for
|
|
1385
|
+
description: "Interactive TUI browser for projects and assignments.",
|
|
1376
1386
|
example: "syntaur browse"
|
|
1377
1387
|
},
|
|
1378
1388
|
{
|
|
@@ -1393,14 +1403,14 @@ var init_help = __esm({
|
|
|
1393
1403
|
command: CLI_COMMANDS[0]
|
|
1394
1404
|
},
|
|
1395
1405
|
{
|
|
1396
|
-
title: "Create a
|
|
1397
|
-
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.",
|
|
1398
1408
|
command: CLI_COMMANDS[2],
|
|
1399
|
-
href: "/create/
|
|
1409
|
+
href: "/create/project"
|
|
1400
1410
|
},
|
|
1401
1411
|
{
|
|
1402
1412
|
title: "Create the first assignment",
|
|
1403
|
-
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.",
|
|
1404
1414
|
command: CLI_COMMANDS[3]
|
|
1405
1415
|
},
|
|
1406
1416
|
{
|
|
@@ -1415,7 +1425,7 @@ var init_help = __esm({
|
|
|
1415
1425
|
},
|
|
1416
1426
|
{
|
|
1417
1427
|
title: "Use the dashboard for triage and context",
|
|
1418
|
-
detail: "Overview shows the current queue,
|
|
1428
|
+
detail: "Overview shows the current queue, project pages show health, assignment pages show the execution surface.",
|
|
1419
1429
|
command: CLI_COMMANDS[12],
|
|
1420
1430
|
href: "/"
|
|
1421
1431
|
}
|
|
@@ -1483,7 +1493,7 @@ function buildSessionContent(opts) {
|
|
|
1483
1493
|
if (Object.keys(opts.overrides).length > 0) {
|
|
1484
1494
|
lines.push("overrides:");
|
|
1485
1495
|
for (const [key, val] of Object.entries(opts.overrides)) {
|
|
1486
|
-
lines.push(` "${key}": {
|
|
1496
|
+
lines.push(` "${key}": { project: "${val.project}", assignment: "${val.assignment}" }`);
|
|
1487
1497
|
}
|
|
1488
1498
|
}
|
|
1489
1499
|
lines.push("---", "");
|
|
@@ -1520,10 +1530,10 @@ async function readSessionFile(dir, name) {
|
|
|
1520
1530
|
const overridesMatch = frontmatter.match(/^overrides:\n((?:\s+".+\n?)*)/m);
|
|
1521
1531
|
if (overridesMatch) {
|
|
1522
1532
|
const overrideLines = overridesMatch[1].matchAll(
|
|
1523
|
-
/^\s+"([^"]+)":\s*\{\s*
|
|
1533
|
+
/^\s+"([^"]+)":\s*\{\s*project:\s*"([^"]+)",\s*assignment:\s*"([^"]+)"\s*\}/gm
|
|
1524
1534
|
);
|
|
1525
1535
|
for (const m of overrideLines) {
|
|
1526
|
-
overrides[m[1]] = {
|
|
1536
|
+
overrides[m[1]] = { project: m[2], assignment: m[3] };
|
|
1527
1537
|
}
|
|
1528
1538
|
}
|
|
1529
1539
|
const autoField = getField(frontmatter, "auto");
|
|
@@ -1725,12 +1735,12 @@ async function getGitInfo(cwd) {
|
|
|
1725
1735
|
}
|
|
1726
1736
|
return { branch: branch || null, worktree: isWorktree };
|
|
1727
1737
|
}
|
|
1728
|
-
async function loadWorkspaceRecords(
|
|
1738
|
+
async function loadWorkspaceRecords(projectsDir) {
|
|
1729
1739
|
const records = [];
|
|
1730
1740
|
try {
|
|
1731
|
-
const
|
|
1732
|
-
for (const
|
|
1733
|
-
const assignmentsDir = resolve5(
|
|
1741
|
+
const projects = await listProjects(projectsDir);
|
|
1742
|
+
for (const project of projects) {
|
|
1743
|
+
const assignmentsDir = resolve5(projectsDir, project.slug, "assignments");
|
|
1734
1744
|
let slugs;
|
|
1735
1745
|
try {
|
|
1736
1746
|
slugs = await readdir2(assignmentsDir);
|
|
@@ -1744,7 +1754,7 @@ async function loadWorkspaceRecords(missionsDir) {
|
|
|
1744
1754
|
const [fm] = extractFrontmatter2(raw);
|
|
1745
1755
|
if (!fm) continue;
|
|
1746
1756
|
records.push({
|
|
1747
|
-
|
|
1757
|
+
projectSlug: project.slug,
|
|
1748
1758
|
assignmentSlug: aslug,
|
|
1749
1759
|
assignmentTitle: getField(fm, "title") ?? aslug,
|
|
1750
1760
|
worktreePath: getNestedField(fm, "workspace", "worktreePath") ?? null,
|
|
@@ -1773,14 +1783,14 @@ async function autoLinkPane(cwd, branch, records) {
|
|
|
1773
1783
|
if (rec.worktreePath) {
|
|
1774
1784
|
const normalizedWt = await resolveAndNormalize(rec.worktreePath);
|
|
1775
1785
|
if (normalizedCwd === normalizedWt) {
|
|
1776
|
-
return {
|
|
1786
|
+
return { project: rec.projectSlug, slug: rec.assignmentSlug, title: rec.assignmentTitle };
|
|
1777
1787
|
}
|
|
1778
1788
|
}
|
|
1779
1789
|
}
|
|
1780
1790
|
if (branch) {
|
|
1781
1791
|
for (const rec of records) {
|
|
1782
1792
|
if (rec.branch && rec.branch === branch) {
|
|
1783
|
-
return {
|
|
1793
|
+
return { project: rec.projectSlug, slug: rec.assignmentSlug, title: rec.assignmentTitle };
|
|
1784
1794
|
}
|
|
1785
1795
|
}
|
|
1786
1796
|
}
|
|
@@ -1850,10 +1860,10 @@ async function scanSession(sessionData, lsofOutput, workspaceRecords) {
|
|
|
1850
1860
|
let assignment = null;
|
|
1851
1861
|
if (override) {
|
|
1852
1862
|
const rec = workspaceRecords.find(
|
|
1853
|
-
(r) => r.
|
|
1863
|
+
(r) => r.projectSlug === override.project && r.assignmentSlug === override.assignment
|
|
1854
1864
|
);
|
|
1855
1865
|
assignment = {
|
|
1856
|
-
|
|
1866
|
+
project: override.project,
|
|
1857
1867
|
slug: override.assignment,
|
|
1858
1868
|
title: rec?.assignmentTitle ?? override.assignment
|
|
1859
1869
|
};
|
|
@@ -1912,10 +1922,10 @@ async function scanProcessSession(sessionData, lsofOutput, workspaceRecords) {
|
|
|
1912
1922
|
let assignment = null;
|
|
1913
1923
|
if (override) {
|
|
1914
1924
|
const rec = workspaceRecords.find(
|
|
1915
|
-
(r) => r.
|
|
1925
|
+
(r) => r.projectSlug === override.project && r.assignmentSlug === override.assignment
|
|
1916
1926
|
);
|
|
1917
1927
|
assignment = {
|
|
1918
|
-
|
|
1928
|
+
project: override.project,
|
|
1919
1929
|
slug: override.assignment,
|
|
1920
1930
|
title: rec?.assignmentTitle ?? override.assignment
|
|
1921
1931
|
};
|
|
@@ -1942,14 +1952,14 @@ async function scanProcessSession(sessionData, lsofOutput, workspaceRecords) {
|
|
|
1942
1952
|
windows: [{ index: 0, name: "process", panes: [pane] }]
|
|
1943
1953
|
};
|
|
1944
1954
|
}
|
|
1945
|
-
async function scanAllSessions(serversDir2,
|
|
1955
|
+
async function scanAllSessions(serversDir2, projectsDir, options) {
|
|
1946
1956
|
if (!options?.bypassCache && cache && Date.now() < cache.expiry) {
|
|
1947
1957
|
return cache.data;
|
|
1948
1958
|
}
|
|
1949
1959
|
const tmuxAvailable = await checkTmuxAvailable();
|
|
1950
1960
|
const names = await listSessionFiles(serversDir2);
|
|
1951
1961
|
const lsofOutput = await getLsofOutput();
|
|
1952
|
-
const workspaceRecords = await loadWorkspaceRecords(
|
|
1962
|
+
const workspaceRecords = await loadWorkspaceRecords(projectsDir);
|
|
1953
1963
|
const sessions = [];
|
|
1954
1964
|
for (const name of names) {
|
|
1955
1965
|
const data = await readSessionFile(serversDir2, name);
|
|
@@ -1964,11 +1974,11 @@ async function scanAllSessions(serversDir2, missionsDir, options) {
|
|
|
1964
1974
|
cache = { data: result, expiry: Date.now() + CACHE_TTL_MS };
|
|
1965
1975
|
return result;
|
|
1966
1976
|
}
|
|
1967
|
-
async function scanSingleSession(serversDir2,
|
|
1977
|
+
async function scanSingleSession(serversDir2, projectsDir, name) {
|
|
1968
1978
|
const data = await readSessionFile(serversDir2, name);
|
|
1969
1979
|
if (!data) return null;
|
|
1970
1980
|
const lsofOutput = await getLsofOutput();
|
|
1971
|
-
const workspaceRecords = await loadWorkspaceRecords(
|
|
1981
|
+
const workspaceRecords = await loadWorkspaceRecords(projectsDir);
|
|
1972
1982
|
if (data.kind === "process") {
|
|
1973
1983
|
return scanProcessSession(data, lsofOutput, workspaceRecords);
|
|
1974
1984
|
}
|
|
@@ -2045,12 +2055,12 @@ async function getStatusConfig() {
|
|
|
2045
2055
|
function clearStatusConfigCache() {
|
|
2046
2056
|
_cachedConfig = null;
|
|
2047
2057
|
}
|
|
2048
|
-
async function
|
|
2049
|
-
const
|
|
2050
|
-
return
|
|
2058
|
+
async function listProjects(projectsDir) {
|
|
2059
|
+
const projectRecords = await listProjectRecords(projectsDir);
|
|
2060
|
+
return projectRecords.map((record) => record.summary);
|
|
2051
2061
|
}
|
|
2052
|
-
async function readWorkspaceRegistry(
|
|
2053
|
-
const registryPath = resolve6(dirname2(
|
|
2062
|
+
async function readWorkspaceRegistry(projectsDir) {
|
|
2063
|
+
const registryPath = resolve6(dirname2(projectsDir), "workspaces.json");
|
|
2054
2064
|
try {
|
|
2055
2065
|
const raw = await readFile5(registryPath, "utf-8");
|
|
2056
2066
|
const parsed = JSON.parse(raw);
|
|
@@ -2059,20 +2069,20 @@ async function readWorkspaceRegistry(missionsDir) {
|
|
|
2059
2069
|
return [];
|
|
2060
2070
|
}
|
|
2061
2071
|
}
|
|
2062
|
-
async function writeWorkspaceRegistry(
|
|
2063
|
-
const registryPath = resolve6(dirname2(
|
|
2072
|
+
async function writeWorkspaceRegistry(projectsDir, workspaces) {
|
|
2073
|
+
const registryPath = resolve6(dirname2(projectsDir), "workspaces.json");
|
|
2064
2074
|
await writeFile2(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
|
|
2065
2075
|
}
|
|
2066
|
-
async function listWorkspaces(
|
|
2067
|
-
const [
|
|
2068
|
-
|
|
2069
|
-
readWorkspaceRegistry(
|
|
2076
|
+
async function listWorkspaces(projectsDir) {
|
|
2077
|
+
const [projectRecords, registered] = await Promise.all([
|
|
2078
|
+
listProjectRecords(projectsDir),
|
|
2079
|
+
readWorkspaceRegistry(projectsDir)
|
|
2070
2080
|
]);
|
|
2071
2081
|
const workspaceSet = new Set(registered);
|
|
2072
2082
|
let hasUngrouped = false;
|
|
2073
|
-
for (const record of
|
|
2074
|
-
if (record.
|
|
2075
|
-
workspaceSet.add(record.
|
|
2083
|
+
for (const record of projectRecords) {
|
|
2084
|
+
if (record.project.workspace) {
|
|
2085
|
+
workspaceSet.add(record.project.workspace);
|
|
2076
2086
|
} else {
|
|
2077
2087
|
hasUngrouped = true;
|
|
2078
2088
|
}
|
|
@@ -2080,28 +2090,28 @@ async function listWorkspaces(missionsDir) {
|
|
|
2080
2090
|
const workspaces = Array.from(workspaceSet).sort();
|
|
2081
2091
|
return { workspaces, hasUngrouped };
|
|
2082
2092
|
}
|
|
2083
|
-
async function createWorkspace(
|
|
2084
|
-
const registered = await readWorkspaceRegistry(
|
|
2093
|
+
async function createWorkspace(projectsDir, name) {
|
|
2094
|
+
const registered = await readWorkspaceRegistry(projectsDir);
|
|
2085
2095
|
if (!registered.includes(name)) {
|
|
2086
2096
|
registered.push(name);
|
|
2087
2097
|
registered.sort();
|
|
2088
|
-
await writeWorkspaceRegistry(
|
|
2098
|
+
await writeWorkspaceRegistry(projectsDir, registered);
|
|
2089
2099
|
}
|
|
2090
2100
|
}
|
|
2091
|
-
async function deleteWorkspace(
|
|
2092
|
-
const registered = await readWorkspaceRegistry(
|
|
2101
|
+
async function deleteWorkspace(projectsDir, name) {
|
|
2102
|
+
const registered = await readWorkspaceRegistry(projectsDir);
|
|
2093
2103
|
const filtered = registered.filter((w) => w !== name);
|
|
2094
|
-
await writeWorkspaceRegistry(
|
|
2104
|
+
await writeWorkspaceRegistry(projectsDir, filtered);
|
|
2095
2105
|
}
|
|
2096
|
-
async function getOverview(
|
|
2097
|
-
const
|
|
2098
|
-
const attention = buildAttentionItems(
|
|
2099
|
-
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);
|
|
2100
2110
|
let serverStats;
|
|
2101
2111
|
if (serversDir2) {
|
|
2102
2112
|
try {
|
|
2103
2113
|
const { scanAllSessions: scanAllSessions2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
|
|
2104
|
-
const servers = await scanAllSessions2(serversDir2,
|
|
2114
|
+
const servers = await scanAllSessions2(serversDir2, projectsDir);
|
|
2105
2115
|
if (servers.tmuxAvailable) {
|
|
2106
2116
|
const alive = servers.sessions.filter((s) => s.alive).length;
|
|
2107
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);
|
|
@@ -2117,50 +2127,50 @@ async function getOverview(missionsDir, serversDir2) {
|
|
|
2117
2127
|
}
|
|
2118
2128
|
return {
|
|
2119
2129
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2120
|
-
firstRun:
|
|
2130
|
+
firstRun: projectRecords.length === 0,
|
|
2121
2131
|
stats: {
|
|
2122
|
-
|
|
2123
|
-
inProgressAssignments:
|
|
2132
|
+
activeProjects: projectRecords.filter((record) => record.summary.status === "active").length,
|
|
2133
|
+
inProgressAssignments: projectRecords.reduce(
|
|
2124
2134
|
(total, record) => total + (record.summary.progress["in_progress"] ?? 0),
|
|
2125
2135
|
0
|
|
2126
2136
|
),
|
|
2127
|
-
blockedAssignments:
|
|
2137
|
+
blockedAssignments: projectRecords.reduce(
|
|
2128
2138
|
(total, record) => total + (record.summary.progress["blocked"] ?? 0),
|
|
2129
2139
|
0
|
|
2130
2140
|
),
|
|
2131
|
-
reviewAssignments:
|
|
2141
|
+
reviewAssignments: projectRecords.reduce(
|
|
2132
2142
|
(total, record) => total + (record.summary.progress["review"] ?? 0),
|
|
2133
2143
|
0
|
|
2134
2144
|
),
|
|
2135
|
-
failedAssignments:
|
|
2145
|
+
failedAssignments: projectRecords.reduce(
|
|
2136
2146
|
(total, record) => total + (record.summary.progress["failed"] ?? 0),
|
|
2137
2147
|
0
|
|
2138
2148
|
),
|
|
2139
|
-
staleAssignments:
|
|
2149
|
+
staleAssignments: projectRecords.reduce(
|
|
2140
2150
|
(total, record) => total + record.assignments.filter((assignment) => isStale(assignment.updated)).length,
|
|
2141
2151
|
0
|
|
2142
2152
|
)
|
|
2143
2153
|
},
|
|
2144
2154
|
attention: attention.slice(0, OVERVIEW_ATTENTION_LIMIT),
|
|
2145
|
-
|
|
2155
|
+
recentProjects: projectRecords.map((record) => record.summary).sort((left, right) => compareTimestamps(right.updated, left.updated)).slice(0, RECENT_PROJECTS_LIMIT),
|
|
2146
2156
|
recentActivity: recentActivity.slice(0, RECENT_ACTIVITY_LIMIT),
|
|
2147
2157
|
serverStats
|
|
2148
2158
|
};
|
|
2149
2159
|
}
|
|
2150
|
-
async function getAttention(
|
|
2151
|
-
const
|
|
2152
|
-
const items = buildAttentionItems(
|
|
2160
|
+
async function getAttention(projectsDir, serversDir2) {
|
|
2161
|
+
const projectRecords = await listProjectRecords(projectsDir);
|
|
2162
|
+
const items = buildAttentionItems(projectRecords);
|
|
2153
2163
|
if (serversDir2) {
|
|
2154
2164
|
try {
|
|
2155
2165
|
const { scanAllSessions: scanAllSessions2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
|
|
2156
|
-
const servers = await scanAllSessions2(serversDir2,
|
|
2166
|
+
const servers = await scanAllSessions2(serversDir2, projectsDir);
|
|
2157
2167
|
for (const session of servers.sessions) {
|
|
2158
2168
|
if (!session.alive) {
|
|
2159
2169
|
items.push({
|
|
2160
2170
|
id: `server-dead-${session.name}`,
|
|
2161
2171
|
severity: "low",
|
|
2162
|
-
|
|
2163
|
-
|
|
2172
|
+
projectSlug: "",
|
|
2173
|
+
projectTitle: "",
|
|
2164
2174
|
assignmentSlug: "",
|
|
2165
2175
|
assignmentTitle: `tmux: ${session.name}`,
|
|
2166
2176
|
status: "failed",
|
|
@@ -2197,13 +2207,13 @@ async function getAttention(missionsDir, serversDir2) {
|
|
|
2197
2207
|
items: pagedItems
|
|
2198
2208
|
};
|
|
2199
2209
|
}
|
|
2200
|
-
async function listAssignmentsBoard(
|
|
2201
|
-
const
|
|
2210
|
+
async function listAssignmentsBoard(projectsDir) {
|
|
2211
|
+
const projectRecords = await listProjectRecords(projectsDir);
|
|
2202
2212
|
const assignments = await Promise.all(
|
|
2203
|
-
|
|
2213
|
+
projectRecords.flatMap(
|
|
2204
2214
|
async (record) => Promise.all(
|
|
2205
2215
|
record.assignments.map(
|
|
2206
|
-
async (assignment) => toAssignmentBoardItem(
|
|
2216
|
+
async (assignment) => toAssignmentBoardItem(projectsDir, record, assignment)
|
|
2207
2217
|
)
|
|
2208
2218
|
)
|
|
2209
2219
|
)
|
|
@@ -2216,59 +2226,59 @@ async function listAssignmentsBoard(missionsDir) {
|
|
|
2216
2226
|
async function getHelp() {
|
|
2217
2227
|
return getDashboardHelp();
|
|
2218
2228
|
}
|
|
2219
|
-
async function getEditableDocument(
|
|
2220
|
-
const filePath = getDocumentPath(
|
|
2229
|
+
async function getEditableDocument(projectsDir, documentType, projectSlug, assignmentSlug) {
|
|
2230
|
+
const filePath = getDocumentPath(projectsDir, documentType, projectSlug, assignmentSlug);
|
|
2221
2231
|
if (!filePath || !await fileExists(filePath)) {
|
|
2222
2232
|
return null;
|
|
2223
2233
|
}
|
|
2224
2234
|
const content = await readFile5(filePath, "utf-8");
|
|
2225
|
-
const title = getEditableDocumentTitle(documentType,
|
|
2235
|
+
const title = getEditableDocumentTitle(documentType, projectSlug, assignmentSlug);
|
|
2226
2236
|
return {
|
|
2227
2237
|
documentType,
|
|
2228
2238
|
title,
|
|
2229
2239
|
content,
|
|
2230
|
-
|
|
2240
|
+
projectSlug,
|
|
2231
2241
|
assignmentSlug,
|
|
2232
2242
|
appendOnly: documentType === "handoff" || documentType === "decision-record"
|
|
2233
2243
|
};
|
|
2234
2244
|
}
|
|
2235
|
-
async function
|
|
2236
|
-
const
|
|
2237
|
-
const
|
|
2238
|
-
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)) {
|
|
2239
2249
|
return null;
|
|
2240
2250
|
}
|
|
2241
|
-
const
|
|
2242
|
-
const
|
|
2243
|
-
const assignments = await listAssignmentRecords(
|
|
2244
|
-
const rollup =
|
|
2245
|
-
const dependencyGraph = await loadDependencyGraph(
|
|
2246
|
-
const resources = await listResources(
|
|
2247
|
-
const memories = await listMemories(
|
|
2248
|
-
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);
|
|
2249
2259
|
return {
|
|
2250
|
-
slug:
|
|
2251
|
-
title:
|
|
2260
|
+
slug: project.slug || slug,
|
|
2261
|
+
title: project.title,
|
|
2252
2262
|
status: rollup.status,
|
|
2253
|
-
statusOverride:
|
|
2254
|
-
archived:
|
|
2255
|
-
archivedAt:
|
|
2256
|
-
archivedReason:
|
|
2257
|
-
created:
|
|
2263
|
+
statusOverride: project.statusOverride,
|
|
2264
|
+
archived: project.archived,
|
|
2265
|
+
archivedAt: project.archivedAt,
|
|
2266
|
+
archivedReason: project.archivedReason,
|
|
2267
|
+
created: project.created,
|
|
2258
2268
|
updated,
|
|
2259
|
-
tags:
|
|
2260
|
-
body:
|
|
2269
|
+
tags: project.tags,
|
|
2270
|
+
body: project.body,
|
|
2261
2271
|
progress: rollup.progress,
|
|
2262
2272
|
needsAttention: rollup.needsAttention,
|
|
2263
2273
|
assignments: assignments.map(toAssignmentSummary).sort((left, right) => compareTimestamps(right.updated, left.updated)),
|
|
2264
2274
|
resources,
|
|
2265
2275
|
memories,
|
|
2266
2276
|
dependencyGraph,
|
|
2267
|
-
workspace:
|
|
2277
|
+
workspace: project.workspace
|
|
2268
2278
|
};
|
|
2269
2279
|
}
|
|
2270
|
-
async function getAssignmentDetail(
|
|
2271
|
-
const assignmentDir = resolve6(
|
|
2280
|
+
async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
2281
|
+
const assignmentDir = resolve6(projectsDir, projectSlug, "assignments", assignmentSlug);
|
|
2272
2282
|
const assignmentMdPath = resolve6(assignmentDir, "assignment.md");
|
|
2273
2283
|
if (!await fileExists(assignmentMdPath)) {
|
|
2274
2284
|
return null;
|
|
@@ -2320,7 +2330,7 @@ async function getAssignmentDetail(missionsDir, missionSlug, assignmentSlug) {
|
|
|
2320
2330
|
}
|
|
2321
2331
|
const detail = {
|
|
2322
2332
|
id: assignment.id,
|
|
2323
|
-
|
|
2333
|
+
projectSlug,
|
|
2324
2334
|
slug: assignment.slug || assignmentSlug,
|
|
2325
2335
|
title: assignment.title,
|
|
2326
2336
|
status: assignment.status,
|
|
@@ -2342,16 +2352,16 @@ async function getAssignmentDetail(missionsDir, missionSlug, assignmentSlug) {
|
|
|
2342
2352
|
handoff,
|
|
2343
2353
|
decisionRecord,
|
|
2344
2354
|
availableTransitions: await getAvailableTransitions(
|
|
2345
|
-
|
|
2346
|
-
|
|
2355
|
+
projectsDir,
|
|
2356
|
+
projectSlug,
|
|
2347
2357
|
assignmentSlug,
|
|
2348
2358
|
assignment
|
|
2349
2359
|
)
|
|
2350
2360
|
};
|
|
2351
|
-
const selfSlug = `${
|
|
2352
|
-
const
|
|
2361
|
+
const selfSlug = `${projectSlug}/${detail.slug}`;
|
|
2362
|
+
const projectRecords = await listProjectRecords(projectsDir);
|
|
2353
2363
|
const reverseLinks = [];
|
|
2354
|
-
for (const mr of
|
|
2364
|
+
for (const mr of projectRecords) {
|
|
2355
2365
|
for (const a of mr.assignments) {
|
|
2356
2366
|
const qualifiedSlug = `${mr.summary.slug}/${a.slug}`;
|
|
2357
2367
|
if (qualifiedSlug === selfSlug) continue;
|
|
@@ -2369,10 +2379,10 @@ async function getAssignmentDetail(missionsDir, missionSlug, assignmentSlug) {
|
|
|
2369
2379
|
const dedupedReverseLinks = reverseLinks.filter((l) => !forwardSet.has(l));
|
|
2370
2380
|
detail.links = forwardLinks;
|
|
2371
2381
|
detail.reverseLinks = dedupedReverseLinks;
|
|
2372
|
-
const
|
|
2373
|
-
for (const mr of
|
|
2382
|
+
const allProjectAssignments = /* @__PURE__ */ new Map();
|
|
2383
|
+
for (const mr of projectRecords) {
|
|
2374
2384
|
for (const a of mr.assignments) {
|
|
2375
|
-
|
|
2385
|
+
allProjectAssignments.set(`${mr.summary.slug}/${a.slug}`, {
|
|
2376
2386
|
title: a.title,
|
|
2377
2387
|
status: a.status
|
|
2378
2388
|
});
|
|
@@ -2381,10 +2391,10 @@ async function getAssignmentDetail(missionsDir, missionSlug, assignmentSlug) {
|
|
|
2381
2391
|
const enrichedLinks = [];
|
|
2382
2392
|
for (const linkSlug of forwardLinks) {
|
|
2383
2393
|
const [ms, as] = linkSlug.split("/");
|
|
2384
|
-
const info =
|
|
2394
|
+
const info = allProjectAssignments.get(linkSlug);
|
|
2385
2395
|
enrichedLinks.push({
|
|
2386
2396
|
slug: linkSlug,
|
|
2387
|
-
|
|
2397
|
+
projectSlug: ms,
|
|
2388
2398
|
assignmentSlug: as,
|
|
2389
2399
|
title: info?.title ?? linkSlug,
|
|
2390
2400
|
status: info?.status ?? "pending",
|
|
@@ -2393,10 +2403,10 @@ async function getAssignmentDetail(missionsDir, missionSlug, assignmentSlug) {
|
|
|
2393
2403
|
}
|
|
2394
2404
|
for (const linkSlug of dedupedReverseLinks) {
|
|
2395
2405
|
const [ms, as] = linkSlug.split("/");
|
|
2396
|
-
const info =
|
|
2406
|
+
const info = allProjectAssignments.get(linkSlug);
|
|
2397
2407
|
enrichedLinks.push({
|
|
2398
2408
|
slug: linkSlug,
|
|
2399
|
-
|
|
2409
|
+
projectSlug: ms,
|
|
2400
2410
|
assignmentSlug: as,
|
|
2401
2411
|
title: info?.title ?? linkSlug,
|
|
2402
2412
|
status: info?.status ?? "pending",
|
|
@@ -2406,51 +2416,51 @@ async function getAssignmentDetail(missionsDir, missionSlug, assignmentSlug) {
|
|
|
2406
2416
|
detail.enrichedLinks = enrichedLinks;
|
|
2407
2417
|
return detail;
|
|
2408
2418
|
}
|
|
2409
|
-
async function
|
|
2410
|
-
if (!await fileExists(
|
|
2419
|
+
async function listProjectRecords(projectsDir) {
|
|
2420
|
+
if (!await fileExists(projectsDir)) {
|
|
2411
2421
|
return [];
|
|
2412
2422
|
}
|
|
2413
|
-
const entries = await readdir3(
|
|
2414
|
-
const
|
|
2423
|
+
const entries = await readdir3(projectsDir, { withFileTypes: true });
|
|
2424
|
+
const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
|
|
2415
2425
|
const records = [];
|
|
2416
|
-
for (const entry of
|
|
2417
|
-
const
|
|
2418
|
-
const
|
|
2419
|
-
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)) {
|
|
2420
2430
|
continue;
|
|
2421
2431
|
}
|
|
2422
|
-
const
|
|
2423
|
-
const
|
|
2424
|
-
const assignments = await listAssignmentRecords(
|
|
2425
|
-
const rollup =
|
|
2426
|
-
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);
|
|
2427
2437
|
records.push({
|
|
2428
|
-
|
|
2429
|
-
|
|
2438
|
+
projectPath,
|
|
2439
|
+
project,
|
|
2430
2440
|
assignments,
|
|
2431
|
-
dependencyGraph: await loadDependencyGraph(
|
|
2441
|
+
dependencyGraph: await loadDependencyGraph(projectPath, assignments),
|
|
2432
2442
|
summary: {
|
|
2433
|
-
slug:
|
|
2434
|
-
title:
|
|
2443
|
+
slug: project.slug || entry.name,
|
|
2444
|
+
title: project.title,
|
|
2435
2445
|
status: rollup.status,
|
|
2436
|
-
statusOverride:
|
|
2437
|
-
archived:
|
|
2438
|
-
archivedAt:
|
|
2439
|
-
archivedReason:
|
|
2440
|
-
created:
|
|
2446
|
+
statusOverride: project.statusOverride,
|
|
2447
|
+
archived: project.archived,
|
|
2448
|
+
archivedAt: project.archivedAt,
|
|
2449
|
+
archivedReason: project.archivedReason,
|
|
2450
|
+
created: project.created,
|
|
2441
2451
|
updated,
|
|
2442
|
-
tags:
|
|
2452
|
+
tags: project.tags,
|
|
2443
2453
|
progress: rollup.progress,
|
|
2444
2454
|
needsAttention: rollup.needsAttention,
|
|
2445
|
-
workspace:
|
|
2455
|
+
workspace: project.workspace
|
|
2446
2456
|
}
|
|
2447
2457
|
});
|
|
2448
2458
|
}
|
|
2449
2459
|
records.sort((left, right) => compareTimestamps(right.summary.updated, left.summary.updated));
|
|
2450
2460
|
return records;
|
|
2451
2461
|
}
|
|
2452
|
-
async function listAssignmentRecords(
|
|
2453
|
-
const assignmentsDir = resolve6(
|
|
2462
|
+
async function listAssignmentRecords(projectPath) {
|
|
2463
|
+
const assignmentsDir = resolve6(projectPath, "assignments");
|
|
2454
2464
|
if (!await fileExists(assignmentsDir)) {
|
|
2455
2465
|
return [];
|
|
2456
2466
|
}
|
|
@@ -2470,8 +2480,8 @@ async function listAssignmentRecords(missionPath) {
|
|
|
2470
2480
|
records.sort((left, right) => compareTimestamps(right.updated, left.updated));
|
|
2471
2481
|
return records;
|
|
2472
2482
|
}
|
|
2473
|
-
async function listResources(
|
|
2474
|
-
const resourcesDir = resolve6(
|
|
2483
|
+
async function listResources(projectPath) {
|
|
2484
|
+
const resourcesDir = resolve6(projectPath, "resources");
|
|
2475
2485
|
if (!await fileExists(resourcesDir)) {
|
|
2476
2486
|
return [];
|
|
2477
2487
|
}
|
|
@@ -2496,8 +2506,8 @@ async function listResources(missionPath) {
|
|
|
2496
2506
|
results.sort((left, right) => compareTimestamps(right.updated, left.updated));
|
|
2497
2507
|
return results;
|
|
2498
2508
|
}
|
|
2499
|
-
async function listMemories(
|
|
2500
|
-
const memoriesDir = resolve6(
|
|
2509
|
+
async function listMemories(projectPath) {
|
|
2510
|
+
const memoriesDir = resolve6(projectPath, "memories");
|
|
2501
2511
|
if (!await fileExists(memoriesDir)) {
|
|
2502
2512
|
return [];
|
|
2503
2513
|
}
|
|
@@ -2522,8 +2532,8 @@ async function listMemories(missionPath) {
|
|
|
2522
2532
|
results.sort((left, right) => compareTimestamps(right.updated, left.updated));
|
|
2523
2533
|
return results;
|
|
2524
2534
|
}
|
|
2525
|
-
async function loadDependencyGraph(
|
|
2526
|
-
const statusPath = resolve6(
|
|
2535
|
+
async function loadDependencyGraph(projectPath, assignments) {
|
|
2536
|
+
const statusPath = resolve6(projectPath, "_status.md");
|
|
2527
2537
|
if (await fileExists(statusPath)) {
|
|
2528
2538
|
const statusContent = await readFile5(statusPath, "utf-8");
|
|
2529
2539
|
const parsed = parseStatus(statusContent);
|
|
@@ -2534,23 +2544,23 @@ async function loadDependencyGraph(missionPath, assignments) {
|
|
|
2534
2544
|
}
|
|
2535
2545
|
return buildDependencyGraph(assignments);
|
|
2536
2546
|
}
|
|
2537
|
-
function
|
|
2547
|
+
function buildProjectRollup(project, assignments) {
|
|
2538
2548
|
const progress = { total: assignments.length };
|
|
2539
|
-
let
|
|
2549
|
+
let openQuestions = 0;
|
|
2540
2550
|
for (const assignment of assignments) {
|
|
2541
2551
|
const s = assignment.status;
|
|
2542
2552
|
progress[s] = (progress[s] ?? 0) + 1;
|
|
2543
|
-
|
|
2553
|
+
openQuestions += countPendingAnswers(assignment.body);
|
|
2544
2554
|
}
|
|
2545
2555
|
const needsAttention = {
|
|
2546
2556
|
blockedCount: progress["blocked"] ?? 0,
|
|
2547
2557
|
failedCount: progress["failed"] ?? 0,
|
|
2548
|
-
|
|
2558
|
+
openQuestions
|
|
2549
2559
|
};
|
|
2550
2560
|
let status = "pending";
|
|
2551
|
-
if (
|
|
2552
|
-
status =
|
|
2553
|
-
} else if (
|
|
2561
|
+
if (project.statusOverride) {
|
|
2562
|
+
status = project.statusOverride;
|
|
2563
|
+
} else if (project.archived) {
|
|
2554
2564
|
status = "archived";
|
|
2555
2565
|
} else if (progress.total > 0 && (progress["completed"] ?? 0) === progress.total) {
|
|
2556
2566
|
status = "completed";
|
|
@@ -2580,16 +2590,16 @@ function toAssignmentSummary(assignment) {
|
|
|
2580
2590
|
updated: assignment.updated
|
|
2581
2591
|
};
|
|
2582
2592
|
}
|
|
2583
|
-
async function toAssignmentBoardItem(
|
|
2593
|
+
async function toAssignmentBoardItem(projectsDir, projectRecord, assignment) {
|
|
2584
2594
|
return {
|
|
2585
2595
|
...toAssignmentSummary(assignment),
|
|
2586
|
-
|
|
2587
|
-
|
|
2596
|
+
projectSlug: projectRecord.summary.slug,
|
|
2597
|
+
projectTitle: projectRecord.summary.title,
|
|
2588
2598
|
blockedReason: assignment.blockedReason,
|
|
2589
|
-
|
|
2599
|
+
projectWorkspace: projectRecord.project.workspace,
|
|
2590
2600
|
availableTransitions: await getAvailableTransitions(
|
|
2591
|
-
|
|
2592
|
-
|
|
2601
|
+
projectsDir,
|
|
2602
|
+
projectRecord.summary.slug,
|
|
2593
2603
|
assignment.slug,
|
|
2594
2604
|
assignment
|
|
2595
2605
|
)
|
|
@@ -2621,18 +2631,18 @@ function buildDependencyGraph(assignments) {
|
|
|
2621
2631
|
function findAssignmentStatus(assignments, slug) {
|
|
2622
2632
|
return assignments.find((assignment) => assignment.slug === slug)?.status ?? "pending";
|
|
2623
2633
|
}
|
|
2624
|
-
async function getAvailableTransitions(
|
|
2634
|
+
async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug, assignment) {
|
|
2625
2635
|
const config = await getStatusConfig();
|
|
2626
2636
|
const transitionDefs = getTransitionDefinitions(config);
|
|
2627
2637
|
const actions = [];
|
|
2628
|
-
const
|
|
2638
|
+
const projectPath = resolve6(projectsDir, projectSlug);
|
|
2629
2639
|
for (const definition of transitionDefs) {
|
|
2630
2640
|
let warning = null;
|
|
2631
2641
|
if (definition.command === "start" && !assignment.assignee) {
|
|
2632
2642
|
warning = "No assignee set \u2014 consider assigning before starting.";
|
|
2633
2643
|
}
|
|
2634
2644
|
if (definition.command === "start" && assignment.dependsOn.length > 0) {
|
|
2635
|
-
const unmetDependencies = await getUnmetDependencies(
|
|
2645
|
+
const unmetDependencies = await getUnmetDependencies(projectPath, assignment.dependsOn, config.terminalStatuses);
|
|
2636
2646
|
if (unmetDependencies.length > 0) {
|
|
2637
2647
|
warning = `Unmet dependencies: ${unmetDependencies.join(", ")}.`;
|
|
2638
2648
|
}
|
|
@@ -2651,11 +2661,11 @@ async function getAvailableTransitions(missionsDir, missionSlug, assignmentSlug,
|
|
|
2651
2661
|
}
|
|
2652
2662
|
return actions;
|
|
2653
2663
|
}
|
|
2654
|
-
async function getUnmetDependencies(
|
|
2664
|
+
async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses) {
|
|
2655
2665
|
const terminals = terminalStatuses ?? /* @__PURE__ */ new Set(["completed"]);
|
|
2656
2666
|
const unmet = [];
|
|
2657
2667
|
for (const dependency of dependsOn) {
|
|
2658
|
-
const dependencyPath = resolve6(
|
|
2668
|
+
const dependencyPath = resolve6(projectPath, "assignments", dependency, "assignment.md");
|
|
2659
2669
|
if (!await fileExists(dependencyPath)) {
|
|
2660
2670
|
unmet.push(`${dependency} (missing)`);
|
|
2661
2671
|
continue;
|
|
@@ -2668,19 +2678,19 @@ async function getUnmetDependencies(missionPath, dependsOn, terminalStatuses) {
|
|
|
2668
2678
|
}
|
|
2669
2679
|
return unmet;
|
|
2670
2680
|
}
|
|
2671
|
-
function buildAttentionItems(
|
|
2681
|
+
function buildAttentionItems(projectRecords) {
|
|
2672
2682
|
const items = [];
|
|
2673
|
-
for (const record of
|
|
2683
|
+
for (const record of projectRecords) {
|
|
2674
2684
|
for (const assignment of record.assignments) {
|
|
2675
2685
|
const stale = isStale(assignment.updated);
|
|
2676
2686
|
const base = {
|
|
2677
|
-
|
|
2678
|
-
|
|
2687
|
+
projectSlug: record.summary.slug,
|
|
2688
|
+
projectTitle: record.summary.title,
|
|
2679
2689
|
assignmentSlug: assignment.slug,
|
|
2680
2690
|
assignmentTitle: assignment.title,
|
|
2681
2691
|
status: assignment.status,
|
|
2682
2692
|
updated: assignment.updated,
|
|
2683
|
-
href: `/
|
|
2693
|
+
href: `/projects/${record.summary.slug}/assignments/${assignment.slug}`,
|
|
2684
2694
|
blockedReason: assignment.blockedReason,
|
|
2685
2695
|
stale
|
|
2686
2696
|
};
|
|
@@ -2720,19 +2730,19 @@ function buildAttentionItems(missionRecords) {
|
|
|
2720
2730
|
}
|
|
2721
2731
|
return items.sort(compareAttentionItems);
|
|
2722
2732
|
}
|
|
2723
|
-
function buildRecentActivity(
|
|
2733
|
+
function buildRecentActivity(projectRecords) {
|
|
2724
2734
|
const activity = [];
|
|
2725
|
-
for (const record of
|
|
2735
|
+
for (const record of projectRecords) {
|
|
2726
2736
|
activity.push({
|
|
2727
|
-
id: `
|
|
2728
|
-
type: "
|
|
2737
|
+
id: `project:${record.summary.slug}`,
|
|
2738
|
+
type: "project",
|
|
2729
2739
|
title: record.summary.title,
|
|
2730
2740
|
updated: record.summary.updated,
|
|
2731
|
-
href: `/
|
|
2732
|
-
|
|
2733
|
-
|
|
2741
|
+
href: `/projects/${record.summary.slug}`,
|
|
2742
|
+
projectSlug: record.summary.slug,
|
|
2743
|
+
projectTitle: record.summary.title,
|
|
2734
2744
|
assignmentSlug: null,
|
|
2735
|
-
summary: `
|
|
2745
|
+
summary: `Project status is ${record.summary.status}.`
|
|
2736
2746
|
});
|
|
2737
2747
|
for (const assignment of record.assignments) {
|
|
2738
2748
|
activity.push({
|
|
@@ -2740,9 +2750,9 @@ function buildRecentActivity(missionRecords) {
|
|
|
2740
2750
|
type: "assignment",
|
|
2741
2751
|
title: assignment.title,
|
|
2742
2752
|
updated: assignment.updated,
|
|
2743
|
-
href: `/
|
|
2744
|
-
|
|
2745
|
-
|
|
2753
|
+
href: `/projects/${record.summary.slug}/assignments/${assignment.slug}`,
|
|
2754
|
+
projectSlug: record.summary.slug,
|
|
2755
|
+
projectTitle: record.summary.title,
|
|
2746
2756
|
assignmentSlug: assignment.slug,
|
|
2747
2757
|
summary: `Assignment is ${assignment.status} with ${assignment.priority} priority.`
|
|
2748
2758
|
});
|
|
@@ -2777,8 +2787,8 @@ function countPendingAnswers(body) {
|
|
|
2777
2787
|
const matches = body.match(/^\*\*A:\*\*\s+pending\s*$/gim);
|
|
2778
2788
|
return matches ? matches.length : 0;
|
|
2779
2789
|
}
|
|
2780
|
-
function
|
|
2781
|
-
let latest =
|
|
2790
|
+
function getProjectActivityTimestamp(projectUpdated, assignments) {
|
|
2791
|
+
let latest = projectUpdated;
|
|
2782
2792
|
for (const assignment of assignments) {
|
|
2783
2793
|
if (compareTimestamps(assignment.updated, latest) > 0) {
|
|
2784
2794
|
latest = assignment.updated;
|
|
@@ -2786,28 +2796,28 @@ function getMissionActivityTimestamp(missionUpdated, assignments) {
|
|
|
2786
2796
|
}
|
|
2787
2797
|
return latest;
|
|
2788
2798
|
}
|
|
2789
|
-
function getDocumentPath(
|
|
2799
|
+
function getDocumentPath(projectsDir, documentType, projectSlug, assignmentSlug) {
|
|
2790
2800
|
switch (documentType) {
|
|
2791
|
-
case "
|
|
2792
|
-
return resolve6(
|
|
2801
|
+
case "project":
|
|
2802
|
+
return resolve6(projectsDir, projectSlug, "project.md");
|
|
2793
2803
|
case "assignment":
|
|
2794
|
-
return assignmentSlug ? resolve6(
|
|
2804
|
+
return assignmentSlug ? resolve6(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
|
|
2795
2805
|
case "plan":
|
|
2796
|
-
return assignmentSlug ? resolve6(
|
|
2806
|
+
return assignmentSlug ? resolve6(projectsDir, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
|
|
2797
2807
|
case "scratchpad":
|
|
2798
|
-
return assignmentSlug ? resolve6(
|
|
2808
|
+
return assignmentSlug ? resolve6(projectsDir, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
|
|
2799
2809
|
case "handoff":
|
|
2800
|
-
return assignmentSlug ? resolve6(
|
|
2810
|
+
return assignmentSlug ? resolve6(projectsDir, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
|
|
2801
2811
|
case "decision-record":
|
|
2802
|
-
return assignmentSlug ? resolve6(
|
|
2812
|
+
return assignmentSlug ? resolve6(projectsDir, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
|
|
2803
2813
|
default:
|
|
2804
2814
|
return null;
|
|
2805
2815
|
}
|
|
2806
2816
|
}
|
|
2807
|
-
function getEditableDocumentTitle(documentType,
|
|
2817
|
+
function getEditableDocumentTitle(documentType, projectSlug, assignmentSlug) {
|
|
2808
2818
|
switch (documentType) {
|
|
2809
|
-
case "
|
|
2810
|
-
return `Edit
|
|
2819
|
+
case "project":
|
|
2820
|
+
return `Edit Project: ${projectSlug}`;
|
|
2811
2821
|
case "assignment":
|
|
2812
2822
|
return `Edit Assignment: ${assignmentSlug || "assignment"}`;
|
|
2813
2823
|
case "plan":
|
|
@@ -2819,9 +2829,9 @@ function getEditableDocumentTitle(documentType, missionSlug, assignmentSlug) {
|
|
|
2819
2829
|
case "decision-record":
|
|
2820
2830
|
return `Append Decision: ${assignmentSlug || "assignment"}`;
|
|
2821
2831
|
case "playbook":
|
|
2822
|
-
return `Edit Playbook: ${
|
|
2832
|
+
return `Edit Playbook: ${projectSlug}`;
|
|
2823
2833
|
default:
|
|
2824
|
-
return
|
|
2834
|
+
return projectSlug;
|
|
2825
2835
|
}
|
|
2826
2836
|
}
|
|
2827
2837
|
async function listPlaybooks(playbooksDir2) {
|
|
@@ -2862,7 +2872,7 @@ async function getPlaybookDetail(playbooksDir2, slug) {
|
|
|
2862
2872
|
body: parsed.body
|
|
2863
2873
|
};
|
|
2864
2874
|
}
|
|
2865
|
-
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;
|
|
2866
2876
|
var init_api = __esm({
|
|
2867
2877
|
"src/dashboard/api.ts"() {
|
|
2868
2878
|
"use strict";
|
|
@@ -2874,7 +2884,7 @@ var init_api = __esm({
|
|
|
2874
2884
|
STALE_ASSIGNMENT_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2875
2885
|
ATTENTION_PAGE_LIMIT = 50;
|
|
2876
2886
|
OVERVIEW_ATTENTION_LIMIT = 6;
|
|
2877
|
-
|
|
2887
|
+
RECENT_PROJECTS_LIMIT = 6;
|
|
2878
2888
|
RECENT_ACTIVITY_LIMIT = 12;
|
|
2879
2889
|
DEFAULT_TRANSITION_DEFINITIONS = [
|
|
2880
2890
|
{
|
|
@@ -3211,11 +3221,11 @@ var init_parser2 = __esm({
|
|
|
3211
3221
|
});
|
|
3212
3222
|
|
|
3213
3223
|
// src/dashboard/server.ts
|
|
3224
|
+
init_paths();
|
|
3214
3225
|
init_api();
|
|
3215
3226
|
import express from "express";
|
|
3216
3227
|
import { createServer } from "http";
|
|
3217
3228
|
import { resolve as resolve15 } from "path";
|
|
3218
|
-
import { homedir as homedir2 } from "os";
|
|
3219
3229
|
import { writeFile as writeFile4, unlink as unlink4 } from "fs/promises";
|
|
3220
3230
|
import { WebSocketServer, WebSocket } from "ws";
|
|
3221
3231
|
|
|
@@ -3223,34 +3233,34 @@ import { WebSocketServer, WebSocket } from "ws";
|
|
|
3223
3233
|
import { watch } from "chokidar";
|
|
3224
3234
|
import { relative, sep } from "path";
|
|
3225
3235
|
function createWatcher(options) {
|
|
3226
|
-
const {
|
|
3236
|
+
const { projectsDir, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, onMessage, debounceMs = 300 } = options;
|
|
3227
3237
|
const pendingEvents = /* @__PURE__ */ new Map();
|
|
3228
|
-
const
|
|
3238
|
+
const projectsWatcher = watch(projectsDir, {
|
|
3229
3239
|
ignoreInitial: true,
|
|
3230
3240
|
persistent: true,
|
|
3231
3241
|
depth: 10,
|
|
3232
3242
|
ignored: /(^|[\/\\])\../
|
|
3233
3243
|
});
|
|
3234
|
-
function
|
|
3235
|
-
const rel = relative(
|
|
3244
|
+
function handleProjectChange(filePath) {
|
|
3245
|
+
const rel = relative(projectsDir, filePath);
|
|
3236
3246
|
const parts = rel.split(sep);
|
|
3237
3247
|
if (parts.length === 0) return;
|
|
3238
|
-
const
|
|
3248
|
+
const projectSlug = parts[0];
|
|
3239
3249
|
let assignmentSlug;
|
|
3240
3250
|
if (parts.length >= 3 && parts[1] === "assignments") {
|
|
3241
3251
|
assignmentSlug = parts[2];
|
|
3242
3252
|
}
|
|
3243
|
-
const debounceKey = assignmentSlug ? `${
|
|
3253
|
+
const debounceKey = assignmentSlug ? `${projectSlug}/${assignmentSlug}` : projectSlug;
|
|
3244
3254
|
const existing = pendingEvents.get(debounceKey);
|
|
3245
3255
|
if (existing) clearTimeout(existing);
|
|
3246
|
-
const messageType = assignmentSlug ? "assignment-updated" : "
|
|
3256
|
+
const messageType = assignmentSlug ? "assignment-updated" : "project-updated";
|
|
3247
3257
|
pendingEvents.set(
|
|
3248
3258
|
debounceKey,
|
|
3249
3259
|
setTimeout(() => {
|
|
3250
3260
|
pendingEvents.delete(debounceKey);
|
|
3251
3261
|
const message = {
|
|
3252
3262
|
type: messageType,
|
|
3253
|
-
|
|
3263
|
+
projectSlug,
|
|
3254
3264
|
assignmentSlug,
|
|
3255
3265
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3256
3266
|
};
|
|
@@ -3258,9 +3268,9 @@ function createWatcher(options) {
|
|
|
3258
3268
|
}, debounceMs)
|
|
3259
3269
|
);
|
|
3260
3270
|
}
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3271
|
+
projectsWatcher.on("change", handleProjectChange);
|
|
3272
|
+
projectsWatcher.on("add", handleProjectChange);
|
|
3273
|
+
projectsWatcher.on("unlink", handleProjectChange);
|
|
3264
3274
|
let serversWatcher = null;
|
|
3265
3275
|
if (serversDir2) {
|
|
3266
3276
|
let handleServerChange2 = function() {
|
|
@@ -3354,7 +3364,7 @@ function createWatcher(options) {
|
|
|
3354
3364
|
clearTimeout(timeout);
|
|
3355
3365
|
});
|
|
3356
3366
|
pendingEvents.clear();
|
|
3357
|
-
await
|
|
3367
|
+
await projectsWatcher.close();
|
|
3358
3368
|
if (serversWatcher) await serversWatcher.close();
|
|
3359
3369
|
if (playbooksWatcher) await playbooksWatcher.close();
|
|
3360
3370
|
if (todosWatcher) await todosWatcher.close();
|
|
@@ -3442,15 +3452,15 @@ init_config();
|
|
|
3442
3452
|
// src/templates/manifest.ts
|
|
3443
3453
|
function renderManifest(params) {
|
|
3444
3454
|
return `---
|
|
3445
|
-
version: "
|
|
3446
|
-
|
|
3455
|
+
version: "2.0"
|
|
3456
|
+
project: ${params.slug}
|
|
3447
3457
|
generated: "${params.timestamp}"
|
|
3448
3458
|
---
|
|
3449
3459
|
|
|
3450
|
-
#
|
|
3460
|
+
# Project: ${params.slug}
|
|
3451
3461
|
|
|
3452
3462
|
## Overview
|
|
3453
|
-
- [
|
|
3463
|
+
- [Project Overview](./project.md)
|
|
3454
3464
|
|
|
3455
3465
|
## Indexes
|
|
3456
3466
|
- [Assignments](./_index-assignments.md)
|
|
@@ -3459,10 +3469,6 @@ generated: "${params.timestamp}"
|
|
|
3459
3469
|
- [Status](./_status.md)
|
|
3460
3470
|
- [Resources](./resources/_index.md)
|
|
3461
3471
|
- [Memories](./memories/_index.md)
|
|
3462
|
-
|
|
3463
|
-
## Config
|
|
3464
|
-
- [Agent Instructions](./agent.md)
|
|
3465
|
-
- [Claude Code Instructions](./claude.md)
|
|
3466
3472
|
`;
|
|
3467
3473
|
}
|
|
3468
3474
|
|
|
@@ -3477,8 +3483,8 @@ function escapeYamlString(value) {
|
|
|
3477
3483
|
return `"${escaped}"`;
|
|
3478
3484
|
}
|
|
3479
3485
|
|
|
3480
|
-
// src/templates/
|
|
3481
|
-
function
|
|
3486
|
+
// src/templates/project.ts
|
|
3487
|
+
function renderProject(params) {
|
|
3482
3488
|
const safeTitle = escapeYamlString(params.title);
|
|
3483
3489
|
const workspaceLine = params.workspace ? `
|
|
3484
3490
|
workspace: ${params.workspace}` : "";
|
|
@@ -3499,7 +3505,7 @@ tags: []${workspaceLine}
|
|
|
3499
3505
|
|
|
3500
3506
|
## Overview
|
|
3501
3507
|
|
|
3502
|
-
<!-- Describe the
|
|
3508
|
+
<!-- Describe the project goal, context, and success criteria here. -->
|
|
3503
3509
|
|
|
3504
3510
|
## Notes
|
|
3505
3511
|
|
|
@@ -3507,43 +3513,6 @@ tags: []${workspaceLine}
|
|
|
3507
3513
|
`;
|
|
3508
3514
|
}
|
|
3509
3515
|
|
|
3510
|
-
// src/templates/agent.ts
|
|
3511
|
-
function renderAgent(params) {
|
|
3512
|
-
return `---
|
|
3513
|
-
mission: ${params.slug}
|
|
3514
|
-
updated: "${params.timestamp}"
|
|
3515
|
-
---
|
|
3516
|
-
|
|
3517
|
-
# Agent Instructions
|
|
3518
|
-
|
|
3519
|
-
All agents working on this mission must follow these guidelines.
|
|
3520
|
-
|
|
3521
|
-
## Conventions
|
|
3522
|
-
|
|
3523
|
-
<!-- Coding conventions, naming standards, architectural patterns. -->
|
|
3524
|
-
|
|
3525
|
-
## Boundaries
|
|
3526
|
-
|
|
3527
|
-
<!-- What agents should NOT do. Files/systems that are off-limits. -->
|
|
3528
|
-
|
|
3529
|
-
## Resources
|
|
3530
|
-
|
|
3531
|
-
<!-- Links to key resources agents should consult. -->
|
|
3532
|
-
`;
|
|
3533
|
-
}
|
|
3534
|
-
|
|
3535
|
-
// src/templates/claude.ts
|
|
3536
|
-
function renderClaude(params) {
|
|
3537
|
-
return `# Claude Code Instructions \u2014 ${params.slug}
|
|
3538
|
-
|
|
3539
|
-
Read \`agent.md\` first for universal conventions and boundaries.
|
|
3540
|
-
|
|
3541
|
-
## Additional Claude Code Rules
|
|
3542
|
-
|
|
3543
|
-
<!-- Add Claude Code-specific rules here. -->
|
|
3544
|
-
`;
|
|
3545
|
-
}
|
|
3546
|
-
|
|
3547
3516
|
// src/templates/assignment.ts
|
|
3548
3517
|
function renderAssignment(params) {
|
|
3549
3518
|
const safeTitle = escapeYamlString(params.title);
|
|
@@ -3551,10 +3520,14 @@ function renderAssignment(params) {
|
|
|
3551
3520
|
- ${params.dependsOn.join("\n - ")}`;
|
|
3552
3521
|
const linksYaml = params.links.length === 0 ? "links: []" : `links:
|
|
3553
3522
|
- ${params.links.join("\n - ")}`;
|
|
3523
|
+
const projectYaml = `project: ${params.project == null ? "null" : params.project}`;
|
|
3524
|
+
const typeYaml = `type: ${params.type ?? "feature"}`;
|
|
3554
3525
|
return `---
|
|
3555
3526
|
id: ${params.id}
|
|
3556
3527
|
slug: ${params.slug}
|
|
3557
3528
|
title: ${safeTitle}
|
|
3529
|
+
${projectYaml}
|
|
3530
|
+
${typeYaml}
|
|
3558
3531
|
status: pending
|
|
3559
3532
|
priority: ${params.priority}
|
|
3560
3533
|
created: "${params.timestamp}"
|
|
@@ -3584,56 +3557,30 @@ tags: []
|
|
|
3584
3557
|
- [ ] <!-- criterion 2 -->
|
|
3585
3558
|
- [ ] <!-- criterion 3 -->
|
|
3586
3559
|
|
|
3587
|
-
##
|
|
3588
|
-
|
|
3589
|
-
<!-- Links to relevant docs, code, or other assignments. -->
|
|
3590
|
-
|
|
3591
|
-
## Questions & Answers
|
|
3560
|
+
## Todos
|
|
3592
3561
|
|
|
3593
|
-
|
|
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
|
+
-->
|
|
3594
3569
|
|
|
3595
|
-
##
|
|
3570
|
+
## Context
|
|
3596
3571
|
|
|
3597
|
-
|
|
3572
|
+
<!-- Links to relevant docs, code, or other assignments. -->
|
|
3598
3573
|
|
|
3599
3574
|
## Links
|
|
3600
3575
|
|
|
3601
|
-
- [
|
|
3576
|
+
- [Progress](./progress.md)
|
|
3577
|
+
- [Comments](./comments.md)
|
|
3602
3578
|
- [Scratchpad](./scratchpad.md)
|
|
3603
3579
|
- [Handoff](./handoff.md)
|
|
3604
3580
|
- [Decision Record](./decision-record.md)
|
|
3605
3581
|
`;
|
|
3606
3582
|
}
|
|
3607
3583
|
|
|
3608
|
-
// src/templates/plan.ts
|
|
3609
|
-
function renderPlan(params) {
|
|
3610
|
-
return `---
|
|
3611
|
-
assignment: ${params.assignmentSlug}
|
|
3612
|
-
status: draft
|
|
3613
|
-
created: "${params.timestamp}"
|
|
3614
|
-
updated: "${params.timestamp}"
|
|
3615
|
-
---
|
|
3616
|
-
|
|
3617
|
-
# Plan: ${params.title}
|
|
3618
|
-
|
|
3619
|
-
## Approach
|
|
3620
|
-
|
|
3621
|
-
<!-- High-level description of how to accomplish the objective. -->
|
|
3622
|
-
|
|
3623
|
-
## Tasks
|
|
3624
|
-
|
|
3625
|
-
- [ ] <!-- step 1 -->
|
|
3626
|
-
- [ ] <!-- step 2 -->
|
|
3627
|
-
- [ ] <!-- step 3 -->
|
|
3628
|
-
|
|
3629
|
-
## Risks & Mitigations
|
|
3630
|
-
|
|
3631
|
-
| Risk | Mitigation |
|
|
3632
|
-
|------|------------|
|
|
3633
|
-
| <!-- risk --> | <!-- mitigation --> |
|
|
3634
|
-
`;
|
|
3635
|
-
}
|
|
3636
|
-
|
|
3637
3584
|
// src/templates/scratchpad.ts
|
|
3638
3585
|
function renderScratchpad(params) {
|
|
3639
3586
|
return `---
|
|
@@ -3678,7 +3625,7 @@ No decisions recorded yet.
|
|
|
3678
3625
|
// src/templates/index-stubs.ts
|
|
3679
3626
|
function renderIndexAssignments(params) {
|
|
3680
3627
|
return `---
|
|
3681
|
-
|
|
3628
|
+
project: ${params.slug}
|
|
3682
3629
|
generated: "${params.timestamp}"
|
|
3683
3630
|
total: 0
|
|
3684
3631
|
by_status:
|
|
@@ -3698,7 +3645,7 @@ by_status:
|
|
|
3698
3645
|
}
|
|
3699
3646
|
function renderIndexPlans(params) {
|
|
3700
3647
|
return `---
|
|
3701
|
-
|
|
3648
|
+
project: ${params.slug}
|
|
3702
3649
|
generated: "${params.timestamp}"
|
|
3703
3650
|
---
|
|
3704
3651
|
|
|
@@ -3710,7 +3657,7 @@ generated: "${params.timestamp}"
|
|
|
3710
3657
|
}
|
|
3711
3658
|
function renderIndexDecisions(params) {
|
|
3712
3659
|
return `---
|
|
3713
|
-
|
|
3660
|
+
project: ${params.slug}
|
|
3714
3661
|
generated: "${params.timestamp}"
|
|
3715
3662
|
---
|
|
3716
3663
|
|
|
@@ -3722,7 +3669,7 @@ generated: "${params.timestamp}"
|
|
|
3722
3669
|
}
|
|
3723
3670
|
function renderStatus(params) {
|
|
3724
3671
|
return `---
|
|
3725
|
-
|
|
3672
|
+
project: ${params.slug}
|
|
3726
3673
|
generated: "${params.timestamp}"
|
|
3727
3674
|
status: pending
|
|
3728
3675
|
progress:
|
|
@@ -3736,10 +3683,10 @@ progress:
|
|
|
3736
3683
|
needsAttention:
|
|
3737
3684
|
blockedCount: 0
|
|
3738
3685
|
failedCount: 0
|
|
3739
|
-
|
|
3686
|
+
openQuestions: 0
|
|
3740
3687
|
---
|
|
3741
3688
|
|
|
3742
|
-
#
|
|
3689
|
+
# Project Status: ${params.title}
|
|
3743
3690
|
|
|
3744
3691
|
**Status:** pending
|
|
3745
3692
|
**Progress:** 0/0 assignments complete
|
|
@@ -3761,7 +3708,7 @@ No dependencies yet.
|
|
|
3761
3708
|
}
|
|
3762
3709
|
function renderResourcesIndex(params) {
|
|
3763
3710
|
return `---
|
|
3764
|
-
|
|
3711
|
+
project: ${params.slug}
|
|
3765
3712
|
generated: "${params.timestamp}"
|
|
3766
3713
|
total: 0
|
|
3767
3714
|
---
|
|
@@ -3774,7 +3721,7 @@ total: 0
|
|
|
3774
3721
|
}
|
|
3775
3722
|
function renderMemoriesIndex(params) {
|
|
3776
3723
|
return `---
|
|
3777
|
-
|
|
3724
|
+
project: ${params.slug}
|
|
3778
3725
|
generated: "${params.timestamp}"
|
|
3779
3726
|
total: 0
|
|
3780
3727
|
---
|
|
@@ -3907,13 +3854,13 @@ async function readCurrentDocument(filePath) {
|
|
|
3907
3854
|
}
|
|
3908
3855
|
return readFile6(filePath, "utf-8");
|
|
3909
3856
|
}
|
|
3910
|
-
function createWriteRouter(
|
|
3857
|
+
function createWriteRouter(projectsDir) {
|
|
3911
3858
|
const router = Router();
|
|
3912
|
-
router.get("/api/templates/
|
|
3913
|
-
const content =
|
|
3859
|
+
router.get("/api/templates/project", (_req, res) => {
|
|
3860
|
+
const content = renderProject({
|
|
3914
3861
|
id: generateId(),
|
|
3915
|
-
slug: "my-new-
|
|
3916
|
-
title: "My New
|
|
3862
|
+
slug: "my-new-project",
|
|
3863
|
+
title: "My New Project",
|
|
3917
3864
|
timestamp: nowTimestamp()
|
|
3918
3865
|
});
|
|
3919
3866
|
res.json({ content });
|
|
@@ -3930,20 +3877,20 @@ function createWriteRouter(missionsDir) {
|
|
|
3930
3877
|
});
|
|
3931
3878
|
res.json({ content });
|
|
3932
3879
|
});
|
|
3933
|
-
router.get("/api/
|
|
3880
|
+
router.get("/api/projects/:slug/edit", async (req, res) => {
|
|
3934
3881
|
const slug = getParam(req.params.slug);
|
|
3935
|
-
const document = await getEditableDocument(
|
|
3882
|
+
const document = await getEditableDocument(projectsDir, "project", slug);
|
|
3936
3883
|
if (!document) {
|
|
3937
|
-
res.status(404).json({ error: `
|
|
3884
|
+
res.status(404).json({ error: `Project "${slug}" not found` });
|
|
3938
3885
|
return;
|
|
3939
3886
|
}
|
|
3940
3887
|
res.json(document);
|
|
3941
3888
|
});
|
|
3942
|
-
router.get("/api/
|
|
3889
|
+
router.get("/api/projects/:slug/assignments/:aslug/edit", async (req, res) => {
|
|
3943
3890
|
const slug = getParam(req.params.slug);
|
|
3944
3891
|
const assignmentSlug = getParam(req.params.aslug);
|
|
3945
3892
|
const document = await getEditableDocument(
|
|
3946
|
-
|
|
3893
|
+
projectsDir,
|
|
3947
3894
|
"assignment",
|
|
3948
3895
|
slug,
|
|
3949
3896
|
assignmentSlug
|
|
@@ -3954,11 +3901,11 @@ function createWriteRouter(missionsDir) {
|
|
|
3954
3901
|
}
|
|
3955
3902
|
res.json(document);
|
|
3956
3903
|
});
|
|
3957
|
-
router.get("/api/
|
|
3904
|
+
router.get("/api/projects/:slug/assignments/:aslug/plan/edit", async (req, res) => {
|
|
3958
3905
|
const slug = getParam(req.params.slug);
|
|
3959
3906
|
const assignmentSlug = getParam(req.params.aslug);
|
|
3960
3907
|
const document = await getEditableDocument(
|
|
3961
|
-
|
|
3908
|
+
projectsDir,
|
|
3962
3909
|
"plan",
|
|
3963
3910
|
slug,
|
|
3964
3911
|
assignmentSlug
|
|
@@ -3969,11 +3916,11 @@ function createWriteRouter(missionsDir) {
|
|
|
3969
3916
|
}
|
|
3970
3917
|
res.json(document);
|
|
3971
3918
|
});
|
|
3972
|
-
router.get("/api/
|
|
3919
|
+
router.get("/api/projects/:slug/assignments/:aslug/scratchpad/edit", async (req, res) => {
|
|
3973
3920
|
const slug = getParam(req.params.slug);
|
|
3974
3921
|
const assignmentSlug = getParam(req.params.aslug);
|
|
3975
3922
|
const document = await getEditableDocument(
|
|
3976
|
-
|
|
3923
|
+
projectsDir,
|
|
3977
3924
|
"scratchpad",
|
|
3978
3925
|
slug,
|
|
3979
3926
|
assignmentSlug
|
|
@@ -3984,11 +3931,11 @@ function createWriteRouter(missionsDir) {
|
|
|
3984
3931
|
}
|
|
3985
3932
|
res.json(document);
|
|
3986
3933
|
});
|
|
3987
|
-
router.get("/api/
|
|
3934
|
+
router.get("/api/projects/:slug/assignments/:aslug/handoff/edit", async (req, res) => {
|
|
3988
3935
|
const slug = getParam(req.params.slug);
|
|
3989
3936
|
const assignmentSlug = getParam(req.params.aslug);
|
|
3990
3937
|
const document = await getEditableDocument(
|
|
3991
|
-
|
|
3938
|
+
projectsDir,
|
|
3992
3939
|
"handoff",
|
|
3993
3940
|
slug,
|
|
3994
3941
|
assignmentSlug
|
|
@@ -3999,11 +3946,11 @@ function createWriteRouter(missionsDir) {
|
|
|
3999
3946
|
}
|
|
4000
3947
|
res.json(document);
|
|
4001
3948
|
});
|
|
4002
|
-
router.get("/api/
|
|
3949
|
+
router.get("/api/projects/:slug/assignments/:aslug/decision-record/edit", async (req, res) => {
|
|
4003
3950
|
const slug = getParam(req.params.slug);
|
|
4004
3951
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4005
3952
|
const document = await getEditableDocument(
|
|
4006
|
-
|
|
3953
|
+
projectsDir,
|
|
4007
3954
|
"decision-record",
|
|
4008
3955
|
slug,
|
|
4009
3956
|
assignmentSlug
|
|
@@ -4014,7 +3961,7 @@ function createWriteRouter(missionsDir) {
|
|
|
4014
3961
|
}
|
|
4015
3962
|
res.json(document);
|
|
4016
3963
|
});
|
|
4017
|
-
router.post("/api/
|
|
3964
|
+
router.post("/api/projects", async (req, res) => {
|
|
4018
3965
|
try {
|
|
4019
3966
|
const content = requireContent(req, res);
|
|
4020
3967
|
if (!content) {
|
|
@@ -4035,52 +3982,50 @@ function createWriteRouter(missionsDir) {
|
|
|
4035
3982
|
res.status(400).json({ error: `Invalid slug "${slug}". Must be lowercase and hyphen-separated.` });
|
|
4036
3983
|
return;
|
|
4037
3984
|
}
|
|
4038
|
-
const
|
|
4039
|
-
if (await fileExists(
|
|
4040
|
-
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` });
|
|
4041
3988
|
return;
|
|
4042
3989
|
}
|
|
4043
3990
|
const title = fields.title;
|
|
4044
3991
|
const timestamp = fields.created || nowTimestamp();
|
|
4045
|
-
await ensureDir(resolve7(
|
|
4046
|
-
await ensureDir(resolve7(
|
|
4047
|
-
await ensureDir(resolve7(
|
|
4048
|
-
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);
|
|
4049
3996
|
try {
|
|
4050
3997
|
const companions = [
|
|
4051
|
-
[resolve7(
|
|
4052
|
-
[resolve7(
|
|
4053
|
-
[resolve7(
|
|
4054
|
-
[resolve7(
|
|
4055
|
-
[resolve7(
|
|
4056
|
-
[resolve7(
|
|
4057
|
-
[resolve7(
|
|
4058
|
-
[resolve7(missionDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
|
|
4059
|
-
[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 })]
|
|
4060
4005
|
];
|
|
4061
4006
|
for (const [filePath, fileContent] of companions) {
|
|
4062
4007
|
await writeFileForce(filePath, fileContent);
|
|
4063
4008
|
}
|
|
4064
4009
|
} catch (companionError) {
|
|
4065
4010
|
try {
|
|
4066
|
-
await rm(
|
|
4011
|
+
await rm(projectDir, { recursive: true, force: true });
|
|
4067
4012
|
} catch {
|
|
4068
4013
|
}
|
|
4069
4014
|
throw companionError;
|
|
4070
4015
|
}
|
|
4071
4016
|
res.status(201).json({ slug });
|
|
4072
4017
|
} catch (error) {
|
|
4073
|
-
console.error("Error creating
|
|
4074
|
-
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}` });
|
|
4075
4020
|
}
|
|
4076
4021
|
});
|
|
4077
|
-
router.post("/api/
|
|
4022
|
+
router.post("/api/projects/:slug/assignments", async (req, res) => {
|
|
4078
4023
|
try {
|
|
4079
|
-
const
|
|
4080
|
-
const
|
|
4081
|
-
const
|
|
4082
|
-
if (!await fileExists(
|
|
4083
|
-
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` });
|
|
4084
4029
|
return;
|
|
4085
4030
|
}
|
|
4086
4031
|
const content = requireContent(req, res);
|
|
@@ -4108,20 +4053,18 @@ function createWriteRouter(missionsDir) {
|
|
|
4108
4053
|
res.status(400).json({ error: `Invalid priority "${priority}". Must be low, medium, high, or critical.` });
|
|
4109
4054
|
return;
|
|
4110
4055
|
}
|
|
4111
|
-
const assignmentDir = resolve7(
|
|
4056
|
+
const assignmentDir = resolve7(projectDir, "assignments", assignmentSlug);
|
|
4112
4057
|
if (await fileExists(assignmentDir)) {
|
|
4113
4058
|
res.status(409).json({
|
|
4114
|
-
error: `Assignment "${assignmentSlug}" already exists in
|
|
4059
|
+
error: `Assignment "${assignmentSlug}" already exists in project "${projectSlug}"`
|
|
4115
4060
|
});
|
|
4116
4061
|
return;
|
|
4117
4062
|
}
|
|
4118
|
-
const title = fields.title;
|
|
4119
4063
|
const timestamp = fields.created || nowTimestamp();
|
|
4120
4064
|
await ensureDir(assignmentDir);
|
|
4121
4065
|
await writeFileForce(resolve7(assignmentDir, "assignment.md"), content);
|
|
4122
4066
|
try {
|
|
4123
4067
|
const companions = [
|
|
4124
|
-
[resolve7(assignmentDir, "plan.md"), renderPlan({ assignmentSlug, title, timestamp })],
|
|
4125
4068
|
[resolve7(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
|
|
4126
4069
|
[resolve7(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
|
|
4127
4070
|
[resolve7(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
|
|
@@ -4136,51 +4079,51 @@ function createWriteRouter(missionsDir) {
|
|
|
4136
4079
|
}
|
|
4137
4080
|
throw companionError;
|
|
4138
4081
|
}
|
|
4139
|
-
res.status(201).json({ slug: assignmentSlug,
|
|
4082
|
+
res.status(201).json({ slug: assignmentSlug, projectSlug });
|
|
4140
4083
|
} catch (error) {
|
|
4141
4084
|
console.error("Error creating assignment:", error);
|
|
4142
4085
|
res.status(500).json({ error: `Failed to create assignment: ${error.message}` });
|
|
4143
4086
|
}
|
|
4144
4087
|
});
|
|
4145
|
-
router.patch("/api/
|
|
4088
|
+
router.patch("/api/projects/:slug", async (req, res) => {
|
|
4146
4089
|
try {
|
|
4147
|
-
const
|
|
4148
|
-
const
|
|
4149
|
-
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);
|
|
4150
4093
|
if (!currentContent) {
|
|
4151
|
-
res.status(404).json({ error: `
|
|
4094
|
+
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
4152
4095
|
return;
|
|
4153
4096
|
}
|
|
4154
4097
|
const nextContentRaw = requireContent(req, res);
|
|
4155
4098
|
if (!nextContentRaw) {
|
|
4156
4099
|
return;
|
|
4157
4100
|
}
|
|
4158
|
-
const current =
|
|
4159
|
-
const next =
|
|
4101
|
+
const current = parseProject(currentContent);
|
|
4102
|
+
const next = parseProject(nextContentRaw);
|
|
4160
4103
|
if (!next.slug || !next.title) {
|
|
4161
|
-
res.status(400).json({ error: "
|
|
4104
|
+
res.status(400).json({ error: "Project content must include slug and title." });
|
|
4162
4105
|
return;
|
|
4163
4106
|
}
|
|
4164
4107
|
if (next.slug !== current.slug) {
|
|
4165
|
-
res.status(400).json({ error: "
|
|
4108
|
+
res.status(400).json({ error: "Project slug cannot be changed once created." });
|
|
4166
4109
|
return;
|
|
4167
4110
|
}
|
|
4168
4111
|
const nextContent = setTopLevelField(nextContentRaw, "updated", nowTimestamp());
|
|
4169
|
-
await writeFileForce(
|
|
4170
|
-
const
|
|
4171
|
-
res.json({
|
|
4112
|
+
await writeFileForce(projectPath, nextContent);
|
|
4113
|
+
const project = await getProjectDetail(projectsDir, projectSlug);
|
|
4114
|
+
res.json({ project, content: nextContent });
|
|
4172
4115
|
} catch (error) {
|
|
4173
|
-
console.error("Error updating
|
|
4174
|
-
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}` });
|
|
4175
4118
|
}
|
|
4176
4119
|
});
|
|
4177
|
-
router.patch("/api/
|
|
4120
|
+
router.patch("/api/projects/:slug/assignments/:aslug", async (req, res) => {
|
|
4178
4121
|
try {
|
|
4179
|
-
const
|
|
4122
|
+
const projectSlug = getParam(req.params.slug);
|
|
4180
4123
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4181
4124
|
const assignmentPath = resolve7(
|
|
4182
|
-
|
|
4183
|
-
|
|
4125
|
+
projectsDir,
|
|
4126
|
+
projectSlug,
|
|
4184
4127
|
"assignments",
|
|
4185
4128
|
assignmentSlug,
|
|
4186
4129
|
"assignment.md"
|
|
@@ -4210,20 +4153,20 @@ function createWriteRouter(missionsDir) {
|
|
|
4210
4153
|
}
|
|
4211
4154
|
nextContent = setTopLevelField(nextContent, "updated", nowTimestamp());
|
|
4212
4155
|
await writeFileForce(assignmentPath, nextContent);
|
|
4213
|
-
const assignment = await getAssignmentDetail(
|
|
4156
|
+
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
4214
4157
|
res.json({ assignment, content: nextContent });
|
|
4215
4158
|
} catch (error) {
|
|
4216
4159
|
console.error("Error updating assignment:", error);
|
|
4217
4160
|
res.status(500).json({ error: `Failed to update assignment: ${error.message}` });
|
|
4218
4161
|
}
|
|
4219
4162
|
});
|
|
4220
|
-
router.patch("/api/
|
|
4163
|
+
router.patch("/api/projects/:slug/assignments/:aslug/acceptance-criteria/:index", async (req, res) => {
|
|
4221
4164
|
try {
|
|
4222
|
-
const
|
|
4165
|
+
const projectSlug = getParam(req.params.slug);
|
|
4223
4166
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4224
4167
|
const assignmentPath = resolve7(
|
|
4225
|
-
|
|
4226
|
-
|
|
4168
|
+
projectsDir,
|
|
4169
|
+
projectSlug,
|
|
4227
4170
|
"assignments",
|
|
4228
4171
|
assignmentSlug,
|
|
4229
4172
|
"assignment.md"
|
|
@@ -4246,20 +4189,20 @@ function createWriteRouter(missionsDir) {
|
|
|
4246
4189
|
}
|
|
4247
4190
|
const nextContent = setTopLevelField(result.content, "updated", nowTimestamp());
|
|
4248
4191
|
await writeFileForce(assignmentPath, nextContent);
|
|
4249
|
-
const assignment = await getAssignmentDetail(
|
|
4192
|
+
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
4250
4193
|
res.json({ assignment, content: nextContent });
|
|
4251
4194
|
} catch (error) {
|
|
4252
4195
|
console.error("Error toggling acceptance criterion:", error);
|
|
4253
4196
|
res.status(500).json({ error: `Failed to toggle acceptance criterion: ${error.message}` });
|
|
4254
4197
|
}
|
|
4255
4198
|
});
|
|
4256
|
-
router.patch("/api/
|
|
4199
|
+
router.patch("/api/projects/:slug/assignments/:aslug/plan", async (req, res) => {
|
|
4257
4200
|
try {
|
|
4258
|
-
const
|
|
4201
|
+
const projectSlug = getParam(req.params.slug);
|
|
4259
4202
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4260
4203
|
const planPath = resolve7(
|
|
4261
|
-
|
|
4262
|
-
|
|
4204
|
+
projectsDir,
|
|
4205
|
+
projectSlug,
|
|
4263
4206
|
"assignments",
|
|
4264
4207
|
assignmentSlug,
|
|
4265
4208
|
"plan.md"
|
|
@@ -4284,20 +4227,20 @@ function createWriteRouter(missionsDir) {
|
|
|
4284
4227
|
}
|
|
4285
4228
|
const nextContent = setTopLevelField(nextContentRaw, "updated", nowTimestamp());
|
|
4286
4229
|
await writeFileForce(planPath, nextContent);
|
|
4287
|
-
const assignment = await getAssignmentDetail(
|
|
4230
|
+
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
4288
4231
|
res.json({ assignment, content: nextContent });
|
|
4289
4232
|
} catch (error) {
|
|
4290
4233
|
console.error("Error updating plan:", error);
|
|
4291
4234
|
res.status(500).json({ error: `Failed to update plan: ${error.message}` });
|
|
4292
4235
|
}
|
|
4293
4236
|
});
|
|
4294
|
-
router.patch("/api/
|
|
4237
|
+
router.patch("/api/projects/:slug/assignments/:aslug/scratchpad", async (req, res) => {
|
|
4295
4238
|
try {
|
|
4296
|
-
const
|
|
4239
|
+
const projectSlug = getParam(req.params.slug);
|
|
4297
4240
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4298
4241
|
const scratchpadPath = resolve7(
|
|
4299
|
-
|
|
4300
|
-
|
|
4242
|
+
projectsDir,
|
|
4243
|
+
projectSlug,
|
|
4301
4244
|
"assignments",
|
|
4302
4245
|
assignmentSlug,
|
|
4303
4246
|
"scratchpad.md"
|
|
@@ -4322,20 +4265,20 @@ function createWriteRouter(missionsDir) {
|
|
|
4322
4265
|
}
|
|
4323
4266
|
const nextContent = setTopLevelField(nextContentRaw, "updated", nowTimestamp());
|
|
4324
4267
|
await writeFileForce(scratchpadPath, nextContent);
|
|
4325
|
-
const assignment = await getAssignmentDetail(
|
|
4268
|
+
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
4326
4269
|
res.json({ assignment, content: nextContent });
|
|
4327
4270
|
} catch (error) {
|
|
4328
4271
|
console.error("Error updating scratchpad:", error);
|
|
4329
4272
|
res.status(500).json({ error: `Failed to update scratchpad: ${error.message}` });
|
|
4330
4273
|
}
|
|
4331
4274
|
});
|
|
4332
|
-
router.post("/api/
|
|
4275
|
+
router.post("/api/projects/:slug/assignments/:aslug/handoff/entries", async (req, res) => {
|
|
4333
4276
|
try {
|
|
4334
|
-
const
|
|
4277
|
+
const projectSlug = getParam(req.params.slug);
|
|
4335
4278
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4336
4279
|
const handoffPath = resolve7(
|
|
4337
|
-
|
|
4338
|
-
|
|
4280
|
+
projectsDir,
|
|
4281
|
+
projectSlug,
|
|
4339
4282
|
"assignments",
|
|
4340
4283
|
assignmentSlug,
|
|
4341
4284
|
"handoff.md"
|
|
@@ -4360,20 +4303,20 @@ function createWriteRouter(missionsDir) {
|
|
|
4360
4303
|
"No handoffs recorded yet."
|
|
4361
4304
|
);
|
|
4362
4305
|
await writeFileForce(handoffPath, nextContent);
|
|
4363
|
-
const assignment = await getAssignmentDetail(
|
|
4306
|
+
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
4364
4307
|
res.status(201).json({ assignment, content: nextContent });
|
|
4365
4308
|
} catch (error) {
|
|
4366
4309
|
console.error("Error appending handoff entry:", error);
|
|
4367
4310
|
res.status(500).json({ error: `Failed to append handoff entry: ${error.message}` });
|
|
4368
4311
|
}
|
|
4369
4312
|
});
|
|
4370
|
-
router.post("/api/
|
|
4313
|
+
router.post("/api/projects/:slug/assignments/:aslug/decision-record/entries", async (req, res) => {
|
|
4371
4314
|
try {
|
|
4372
|
-
const
|
|
4315
|
+
const projectSlug = getParam(req.params.slug);
|
|
4373
4316
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4374
4317
|
const decisionPath = resolve7(
|
|
4375
|
-
|
|
4376
|
-
|
|
4318
|
+
projectsDir,
|
|
4319
|
+
projectSlug,
|
|
4377
4320
|
"assignments",
|
|
4378
4321
|
assignmentSlug,
|
|
4379
4322
|
"decision-record.md"
|
|
@@ -4398,19 +4341,19 @@ function createWriteRouter(missionsDir) {
|
|
|
4398
4341
|
"No decisions recorded yet."
|
|
4399
4342
|
);
|
|
4400
4343
|
await writeFileForce(decisionPath, nextContent);
|
|
4401
|
-
const assignment = await getAssignmentDetail(
|
|
4344
|
+
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
4402
4345
|
res.status(201).json({ assignment, content: nextContent });
|
|
4403
4346
|
} catch (error) {
|
|
4404
4347
|
console.error("Error appending decision entry:", error);
|
|
4405
4348
|
res.status(500).json({ error: `Failed to append decision entry: ${error.message}` });
|
|
4406
4349
|
}
|
|
4407
4350
|
});
|
|
4408
|
-
router.post("/api/
|
|
4351
|
+
router.post("/api/projects/:slug/move-workspace", async (req, res) => {
|
|
4409
4352
|
try {
|
|
4410
|
-
const
|
|
4411
|
-
const
|
|
4412
|
-
if (!await fileExists(
|
|
4413
|
-
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` });
|
|
4414
4357
|
return;
|
|
4415
4358
|
}
|
|
4416
4359
|
const { workspace } = req.body || {};
|
|
@@ -4418,23 +4361,23 @@ function createWriteRouter(missionsDir) {
|
|
|
4418
4361
|
res.status(400).json({ error: "workspace must be a non-empty string or null (for ungrouped)." });
|
|
4419
4362
|
return;
|
|
4420
4363
|
}
|
|
4421
|
-
let content = await readFile6(
|
|
4364
|
+
let content = await readFile6(projectPath, "utf-8");
|
|
4422
4365
|
content = setTopLevelField(content, "workspace", workspace ?? null);
|
|
4423
4366
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
4424
|
-
await writeFileForce(
|
|
4425
|
-
const
|
|
4426
|
-
res.json({
|
|
4367
|
+
await writeFileForce(projectPath, content);
|
|
4368
|
+
const project = await getProjectDetail(projectsDir, projectSlug);
|
|
4369
|
+
res.json({ project });
|
|
4427
4370
|
} catch (error) {
|
|
4428
|
-
console.error("Error moving
|
|
4371
|
+
console.error("Error moving project workspace:", error);
|
|
4429
4372
|
res.status(500).json({ error: `Failed to move workspace: ${error.message}` });
|
|
4430
4373
|
}
|
|
4431
4374
|
});
|
|
4432
|
-
router.post("/api/
|
|
4375
|
+
router.post("/api/projects/:slug/status-override", async (req, res) => {
|
|
4433
4376
|
try {
|
|
4434
|
-
const
|
|
4435
|
-
const
|
|
4436
|
-
if (!await fileExists(
|
|
4437
|
-
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` });
|
|
4438
4381
|
return;
|
|
4439
4382
|
}
|
|
4440
4383
|
const { status } = req.body || {};
|
|
@@ -4444,24 +4387,24 @@ function createWriteRouter(missionsDir) {
|
|
|
4444
4387
|
res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}, or null to clear.` });
|
|
4445
4388
|
return;
|
|
4446
4389
|
}
|
|
4447
|
-
let content = await readFile6(
|
|
4390
|
+
let content = await readFile6(projectPath, "utf-8");
|
|
4448
4391
|
content = setTopLevelField(content, "statusOverride", status ?? null);
|
|
4449
4392
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
4450
|
-
await writeFileForce(
|
|
4451
|
-
const
|
|
4452
|
-
res.json({
|
|
4393
|
+
await writeFileForce(projectPath, content);
|
|
4394
|
+
const project = await getProjectDetail(projectsDir, projectSlug);
|
|
4395
|
+
res.json({ project });
|
|
4453
4396
|
} catch (error) {
|
|
4454
|
-
console.error("Error setting
|
|
4397
|
+
console.error("Error setting project status override:", error);
|
|
4455
4398
|
res.status(500).json({ error: `Failed to set status override: ${error.message}` });
|
|
4456
4399
|
}
|
|
4457
4400
|
});
|
|
4458
|
-
router.post("/api/
|
|
4401
|
+
router.post("/api/projects/:slug/assignments/:aslug/status-override", async (req, res) => {
|
|
4459
4402
|
try {
|
|
4460
|
-
const
|
|
4403
|
+
const projectSlug = getParam(req.params.slug);
|
|
4461
4404
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4462
4405
|
const assignmentPath = resolve7(
|
|
4463
|
-
|
|
4464
|
-
|
|
4406
|
+
projectsDir,
|
|
4407
|
+
projectSlug,
|
|
4465
4408
|
"assignments",
|
|
4466
4409
|
assignmentSlug,
|
|
4467
4410
|
"assignment.md"
|
|
@@ -4484,16 +4427,16 @@ function createWriteRouter(missionsDir) {
|
|
|
4484
4427
|
content = setTopLevelField(content, "blockedReason", null);
|
|
4485
4428
|
}
|
|
4486
4429
|
await writeFileForce(assignmentPath, content);
|
|
4487
|
-
const assignment = await getAssignmentDetail(
|
|
4430
|
+
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
4488
4431
|
res.json({ assignment });
|
|
4489
4432
|
} catch (error) {
|
|
4490
4433
|
console.error("Error overriding assignment status:", error);
|
|
4491
4434
|
res.status(500).json({ error: `Failed to override status: ${error.message}` });
|
|
4492
4435
|
}
|
|
4493
4436
|
});
|
|
4494
|
-
router.post("/api/
|
|
4437
|
+
router.post("/api/projects/:slug/assignments/:aslug/transitions/:command", async (req, res) => {
|
|
4495
4438
|
try {
|
|
4496
|
-
const
|
|
4439
|
+
const projectSlug = getParam(req.params.slug);
|
|
4497
4440
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4498
4441
|
const command = req.params.command;
|
|
4499
4442
|
const config = await getStatusConfig();
|
|
@@ -4502,14 +4445,14 @@ function createWriteRouter(missionsDir) {
|
|
|
4502
4445
|
res.status(400).json({ error: `Unsupported transition command "${req.params.command}"` });
|
|
4503
4446
|
return;
|
|
4504
4447
|
}
|
|
4505
|
-
const
|
|
4506
|
-
const assignmentPath = resolve7(
|
|
4448
|
+
const projectDir = resolve7(projectsDir, projectSlug);
|
|
4449
|
+
const assignmentPath = resolve7(projectDir, "assignments", assignmentSlug, "assignment.md");
|
|
4507
4450
|
if (!await fileExists(assignmentPath)) {
|
|
4508
4451
|
res.status(404).json({ error: "Assignment not found" });
|
|
4509
4452
|
return;
|
|
4510
4453
|
}
|
|
4511
4454
|
const { reason } = req.body || {};
|
|
4512
|
-
const result = await executeTransition(
|
|
4455
|
+
const result = await executeTransition(projectDir, assignmentSlug, command, {
|
|
4513
4456
|
reason: typeof reason === "string" ? reason : void 0,
|
|
4514
4457
|
transitionTable: config.custom ? config.transitionTable : void 0,
|
|
4515
4458
|
terminalStatuses: config.custom ? config.terminalStatuses : void 0
|
|
@@ -4518,25 +4461,25 @@ function createWriteRouter(missionsDir) {
|
|
|
4518
4461
|
res.status(400).json({ error: result.message });
|
|
4519
4462
|
return;
|
|
4520
4463
|
}
|
|
4521
|
-
const assignment = await getAssignmentDetail(
|
|
4464
|
+
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
4522
4465
|
res.json({ assignment, transition: result });
|
|
4523
4466
|
} catch (error) {
|
|
4524
4467
|
console.error("Error running assignment transition:", error);
|
|
4525
4468
|
res.status(500).json({ error: `Failed to transition assignment: ${error.message}` });
|
|
4526
4469
|
}
|
|
4527
4470
|
});
|
|
4528
|
-
router.delete("/api/
|
|
4471
|
+
router.delete("/api/projects/:slug/assignments/:aslug", async (req, res) => {
|
|
4529
4472
|
try {
|
|
4530
|
-
const
|
|
4473
|
+
const projectSlug = getParam(req.params.slug);
|
|
4531
4474
|
const assignmentSlug = getParam(req.params.aslug);
|
|
4532
|
-
const assignmentDir = resolve7(
|
|
4475
|
+
const assignmentDir = resolve7(projectsDir, projectSlug, "assignments", assignmentSlug);
|
|
4533
4476
|
const assignmentPath = resolve7(assignmentDir, "assignment.md");
|
|
4534
4477
|
if (!await fileExists(assignmentPath)) {
|
|
4535
|
-
res.status(404).json({ error: `Assignment "${assignmentSlug}" not found in
|
|
4478
|
+
res.status(404).json({ error: `Assignment "${assignmentSlug}" not found in project "${projectSlug}"` });
|
|
4536
4479
|
return;
|
|
4537
4480
|
}
|
|
4538
4481
|
await rm(assignmentDir, { recursive: true, force: true });
|
|
4539
|
-
res.json({ deleted: assignmentSlug,
|
|
4482
|
+
res.json({ deleted: assignmentSlug, projectSlug });
|
|
4540
4483
|
} catch (error) {
|
|
4541
4484
|
console.error("Error deleting assignment:", error);
|
|
4542
4485
|
res.status(500).json({ error: `Failed to delete assignment: ${error.message}` });
|
|
@@ -4549,11 +4492,11 @@ function createWriteRouter(missionsDir) {
|
|
|
4549
4492
|
init_servers();
|
|
4550
4493
|
init_scanner();
|
|
4551
4494
|
import { Router as Router2 } from "express";
|
|
4552
|
-
function createServersRouter(serversDir2,
|
|
4495
|
+
function createServersRouter(serversDir2, projectsDir) {
|
|
4553
4496
|
const router = Router2();
|
|
4554
4497
|
router.get("/", async (_req, res) => {
|
|
4555
4498
|
try {
|
|
4556
|
-
const result = await scanAllSessions(serversDir2,
|
|
4499
|
+
const result = await scanAllSessions(serversDir2, projectsDir);
|
|
4557
4500
|
res.json(result);
|
|
4558
4501
|
} catch (error) {
|
|
4559
4502
|
res.status(500).json({ error: error instanceof Error ? error.message : "Scan failed" });
|
|
@@ -4561,7 +4504,7 @@ function createServersRouter(serversDir2, missionsDir) {
|
|
|
4561
4504
|
});
|
|
4562
4505
|
router.get("/:name", async (req, res) => {
|
|
4563
4506
|
try {
|
|
4564
|
-
const session = await scanSingleSession(serversDir2,
|
|
4507
|
+
const session = await scanSingleSession(serversDir2, projectsDir, req.params.name);
|
|
4565
4508
|
if (!session) {
|
|
4566
4509
|
res.status(404).json({ error: "Session not found" });
|
|
4567
4510
|
return;
|
|
@@ -4612,7 +4555,7 @@ function createServersRouter(serversDir2, missionsDir) {
|
|
|
4612
4555
|
await updateLastRefreshed(serversDir2, name);
|
|
4613
4556
|
}
|
|
4614
4557
|
clearScanCache();
|
|
4615
|
-
const result = await scanAllSessions(serversDir2,
|
|
4558
|
+
const result = await scanAllSessions(serversDir2, projectsDir, { bypassCache: true });
|
|
4616
4559
|
res.json(result);
|
|
4617
4560
|
} catch (error) {
|
|
4618
4561
|
res.status(500).json({ error: error instanceof Error ? error.message : "Refresh failed" });
|
|
@@ -4627,7 +4570,7 @@ function createServersRouter(serversDir2, missionsDir) {
|
|
|
4627
4570
|
}
|
|
4628
4571
|
await updateLastRefreshed(serversDir2, req.params.name);
|
|
4629
4572
|
clearScanCache();
|
|
4630
|
-
const session = await scanSingleSession(serversDir2,
|
|
4573
|
+
const session = await scanSingleSession(serversDir2, projectsDir, req.params.name);
|
|
4631
4574
|
res.json(session);
|
|
4632
4575
|
} catch (error) {
|
|
4633
4576
|
res.status(500).json({ error: error instanceof Error ? error.message : "Refresh failed" });
|
|
@@ -4642,7 +4585,7 @@ function createServersRouter(serversDir2, missionsDir) {
|
|
|
4642
4585
|
return;
|
|
4643
4586
|
}
|
|
4644
4587
|
const body = req.body;
|
|
4645
|
-
if (body === null || body && body.
|
|
4588
|
+
if (body === null || body && body.project && body.assignment) {
|
|
4646
4589
|
await setOverride(
|
|
4647
4590
|
serversDir2,
|
|
4648
4591
|
name,
|
|
@@ -4653,7 +4596,7 @@ function createServersRouter(serversDir2, missionsDir) {
|
|
|
4653
4596
|
clearScanCache();
|
|
4654
4597
|
res.json({ updated: true });
|
|
4655
4598
|
} else {
|
|
4656
|
-
res.status(400).json({ error: "Body must be {
|
|
4599
|
+
res.status(400).json({ error: "Body must be { project, assignment } or null" });
|
|
4657
4600
|
}
|
|
4658
4601
|
} catch (error) {
|
|
4659
4602
|
res.status(500).json({ error: error instanceof Error ? error.message : "Update failed" });
|
|
@@ -4683,7 +4626,7 @@ var SCHEMA_VERSION = "2";
|
|
|
4683
4626
|
var SCHEMA_SQL = `
|
|
4684
4627
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
4685
4628
|
session_id TEXT PRIMARY KEY,
|
|
4686
|
-
|
|
4629
|
+
project_slug TEXT,
|
|
4687
4630
|
assignment_slug TEXT,
|
|
4688
4631
|
agent TEXT NOT NULL,
|
|
4689
4632
|
started TEXT NOT NULL,
|
|
@@ -4694,8 +4637,8 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
4694
4637
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4695
4638
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4696
4639
|
);
|
|
4697
|
-
CREATE INDEX IF NOT EXISTS
|
|
4698
|
-
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);
|
|
4699
4642
|
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
4700
4643
|
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
|
|
4701
4644
|
`;
|
|
@@ -4714,7 +4657,7 @@ function initSessionDb(dbPath) {
|
|
|
4714
4657
|
db.exec(`
|
|
4715
4658
|
CREATE TABLE sessions_v2 (
|
|
4716
4659
|
session_id TEXT PRIMARY KEY,
|
|
4717
|
-
|
|
4660
|
+
project_slug TEXT,
|
|
4718
4661
|
assignment_slug TEXT,
|
|
4719
4662
|
agent TEXT NOT NULL,
|
|
4720
4663
|
started TEXT NOT NULL,
|
|
@@ -4725,11 +4668,11 @@ function initSessionDb(dbPath) {
|
|
|
4725
4668
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4726
4669
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4727
4670
|
);
|
|
4728
|
-
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;
|
|
4729
4672
|
DROP TABLE sessions;
|
|
4730
4673
|
ALTER TABLE sessions_v2 RENAME TO sessions;
|
|
4731
|
-
CREATE INDEX IF NOT EXISTS
|
|
4732
|
-
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);
|
|
4733
4676
|
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
4734
4677
|
UPDATE meta SET value = '2' WHERE key = 'schema_version';
|
|
4735
4678
|
`);
|
|
@@ -4750,36 +4693,36 @@ function closeSessionDb() {
|
|
|
4750
4693
|
db = null;
|
|
4751
4694
|
}
|
|
4752
4695
|
}
|
|
4753
|
-
async function migrateFromMarkdown(
|
|
4696
|
+
async function migrateFromMarkdown(projectsDir) {
|
|
4754
4697
|
const database = getSessionDb();
|
|
4755
4698
|
const count = database.prepare("SELECT COUNT(*) as count FROM sessions").get();
|
|
4756
4699
|
if (count.count > 0) return 0;
|
|
4757
|
-
if (!await fileExists(
|
|
4758
|
-
const entries = await readdir4(
|
|
4700
|
+
if (!await fileExists(projectsDir)) return 0;
|
|
4701
|
+
const entries = await readdir4(projectsDir, { withFileTypes: true });
|
|
4759
4702
|
const allSessions = [];
|
|
4760
4703
|
for (const entry of entries) {
|
|
4761
4704
|
if (!entry.isDirectory()) continue;
|
|
4762
|
-
const
|
|
4763
|
-
const indexPath = resolve8(
|
|
4705
|
+
const projectDir = resolve8(projectsDir, entry.name);
|
|
4706
|
+
const indexPath = resolve8(projectDir, "_index-sessions.md");
|
|
4764
4707
|
if (!await fileExists(indexPath)) continue;
|
|
4765
4708
|
const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
|
|
4766
4709
|
allSessions.push(...sessions);
|
|
4767
4710
|
}
|
|
4768
4711
|
if (allSessions.length === 0) return 0;
|
|
4769
4712
|
const insert = database.prepare(`
|
|
4770
|
-
INSERT OR IGNORE INTO sessions (session_id,
|
|
4713
|
+
INSERT OR IGNORE INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path)
|
|
4771
4714
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
4772
4715
|
`);
|
|
4773
4716
|
const insertAll = database.transaction((sessions) => {
|
|
4774
4717
|
for (const s of sessions) {
|
|
4775
|
-
insert.run(s.sessionId, s.
|
|
4718
|
+
insert.run(s.sessionId, s.projectSlug, s.assignmentSlug, s.agent, s.started, s.status, s.path);
|
|
4776
4719
|
}
|
|
4777
4720
|
});
|
|
4778
4721
|
insertAll(allSessions);
|
|
4779
4722
|
console.log(`Migrated ${allSessions.length} sessions from markdown to SQLite.`);
|
|
4780
4723
|
return allSessions.length;
|
|
4781
4724
|
}
|
|
4782
|
-
async function parseMarkdownSessionsIndex(filePath,
|
|
4725
|
+
async function parseMarkdownSessionsIndex(filePath, projectSlug) {
|
|
4783
4726
|
const { readFile: readFile12 } = await import("fs/promises");
|
|
4784
4727
|
const raw = await readFile12(filePath, "utf-8");
|
|
4785
4728
|
const sessions = [];
|
|
@@ -4808,7 +4751,7 @@ async function parseMarkdownSessionsIndex(filePath, missionSlug) {
|
|
|
4808
4751
|
started: cells[3],
|
|
4809
4752
|
status: cells[4] || "active",
|
|
4810
4753
|
path: cells[5],
|
|
4811
|
-
|
|
4754
|
+
projectSlug
|
|
4812
4755
|
});
|
|
4813
4756
|
}
|
|
4814
4757
|
}
|
|
@@ -4820,7 +4763,7 @@ async function parseMarkdownSessionsIndex(filePath, missionSlug) {
|
|
|
4820
4763
|
function rowToSession(row) {
|
|
4821
4764
|
return {
|
|
4822
4765
|
sessionId: row.session_id,
|
|
4823
|
-
|
|
4766
|
+
projectSlug: row.project_slug ?? null,
|
|
4824
4767
|
assignmentSlug: row.assignment_slug ?? null,
|
|
4825
4768
|
agent: row.agent,
|
|
4826
4769
|
started: row.started,
|
|
@@ -4830,14 +4773,14 @@ function rowToSession(row) {
|
|
|
4830
4773
|
description: row.description ?? null
|
|
4831
4774
|
};
|
|
4832
4775
|
}
|
|
4833
|
-
async function appendSession(
|
|
4776
|
+
async function appendSession(_projectDir, session) {
|
|
4834
4777
|
const db2 = getSessionDb();
|
|
4835
4778
|
db2.prepare(`
|
|
4836
|
-
INSERT INTO sessions (session_id,
|
|
4779
|
+
INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description)
|
|
4837
4780
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
4838
4781
|
`).run(
|
|
4839
4782
|
session.sessionId,
|
|
4840
|
-
session.
|
|
4783
|
+
session.projectSlug ?? null,
|
|
4841
4784
|
session.assignmentSlug ?? null,
|
|
4842
4785
|
session.agent,
|
|
4843
4786
|
session.started,
|
|
@@ -4846,7 +4789,7 @@ async function appendSession(_missionDir, session) {
|
|
|
4846
4789
|
session.description ?? null
|
|
4847
4790
|
);
|
|
4848
4791
|
}
|
|
4849
|
-
async function updateSessionStatus(
|
|
4792
|
+
async function updateSessionStatus(_projectDir, sessionId, status) {
|
|
4850
4793
|
const db2 = getSessionDb();
|
|
4851
4794
|
const isTerminal = status === "completed" || status === "stopped";
|
|
4852
4795
|
const result = isTerminal ? db2.prepare(
|
|
@@ -4856,20 +4799,20 @@ async function updateSessionStatus(_missionDir, sessionId, status) {
|
|
|
4856
4799
|
).run(status, sessionId);
|
|
4857
4800
|
return result.changes > 0;
|
|
4858
4801
|
}
|
|
4859
|
-
async function listAllSessions(
|
|
4802
|
+
async function listAllSessions(_projectsDir) {
|
|
4860
4803
|
const db2 = getSessionDb();
|
|
4861
4804
|
const rows = db2.prepare("SELECT * FROM sessions ORDER BY started DESC").all();
|
|
4862
4805
|
return rows.map(rowToSession);
|
|
4863
4806
|
}
|
|
4864
|
-
async function
|
|
4807
|
+
async function listProjectSessions(_projectsDir, projectSlug, assignmentSlug) {
|
|
4865
4808
|
const db2 = getSessionDb();
|
|
4866
4809
|
if (assignmentSlug) {
|
|
4867
4810
|
const rows2 = db2.prepare(
|
|
4868
|
-
"SELECT * FROM sessions WHERE
|
|
4869
|
-
).all(
|
|
4811
|
+
"SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
|
|
4812
|
+
).all(projectSlug, assignmentSlug);
|
|
4870
4813
|
return rows2.map(rowToSession);
|
|
4871
4814
|
}
|
|
4872
|
-
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);
|
|
4873
4816
|
return rows.map(rowToSession);
|
|
4874
4817
|
}
|
|
4875
4818
|
async function deleteSessions(sessionIds) {
|
|
@@ -4880,34 +4823,34 @@ async function deleteSessions(sessionIds) {
|
|
|
4880
4823
|
return result.changes;
|
|
4881
4824
|
}
|
|
4882
4825
|
var DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
|
|
4883
|
-
async function readAssignmentStatus(
|
|
4884
|
-
const assignmentPath = resolve9(
|
|
4826
|
+
async function readAssignmentStatus(projectDir, assignmentSlug) {
|
|
4827
|
+
const assignmentPath = resolve9(projectDir, "assignments", assignmentSlug, "assignment.md");
|
|
4885
4828
|
if (!await fileExists(assignmentPath)) return null;
|
|
4886
4829
|
const raw = await readFile7(assignmentPath, "utf-8");
|
|
4887
4830
|
const match = raw.match(/^status:\s*(.+)$/m);
|
|
4888
4831
|
return match ? match[1].trim() : null;
|
|
4889
4832
|
}
|
|
4890
|
-
async function reconcileActiveSessions(
|
|
4833
|
+
async function reconcileActiveSessions(projectsDir) {
|
|
4891
4834
|
const db2 = getSessionDb();
|
|
4892
|
-
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();
|
|
4893
4836
|
if (activeSessions.length === 0) return 0;
|
|
4894
4837
|
const toCheck = /* @__PURE__ */ new Map();
|
|
4895
4838
|
for (const session of activeSessions) {
|
|
4896
|
-
const slugs = toCheck.get(session.
|
|
4839
|
+
const slugs = toCheck.get(session.project_slug) ?? /* @__PURE__ */ new Set();
|
|
4897
4840
|
slugs.add(session.assignment_slug);
|
|
4898
|
-
toCheck.set(session.
|
|
4841
|
+
toCheck.set(session.project_slug, slugs);
|
|
4899
4842
|
}
|
|
4900
4843
|
const assignmentStatuses = /* @__PURE__ */ new Map();
|
|
4901
|
-
for (const [
|
|
4902
|
-
const
|
|
4844
|
+
for (const [projectSlug, slugs] of toCheck) {
|
|
4845
|
+
const projectDir = resolve9(projectsDir, projectSlug);
|
|
4903
4846
|
for (const slug of slugs) {
|
|
4904
|
-
const status = await readAssignmentStatus(
|
|
4905
|
-
if (status) assignmentStatuses.set(`${
|
|
4847
|
+
const status = await readAssignmentStatus(projectDir, slug);
|
|
4848
|
+
if (status) assignmentStatuses.set(`${projectSlug}/${slug}`, status);
|
|
4906
4849
|
}
|
|
4907
4850
|
}
|
|
4908
4851
|
let totalUpdated = 0;
|
|
4909
4852
|
for (const session of activeSessions) {
|
|
4910
|
-
const key = `${session.
|
|
4853
|
+
const key = `${session.project_slug}/${session.assignment_slug}`;
|
|
4911
4854
|
const assignmentStatus = assignmentStatuses.get(key);
|
|
4912
4855
|
if (!assignmentStatus || !DONE_ASSIGNMENT_STATUSES.has(assignmentStatus)) continue;
|
|
4913
4856
|
const newStatus = assignmentStatus === "failed" ? "stopped" : "completed";
|
|
@@ -4919,28 +4862,28 @@ async function reconcileActiveSessions(missionsDir) {
|
|
|
4919
4862
|
|
|
4920
4863
|
// src/dashboard/api-agent-sessions.ts
|
|
4921
4864
|
init_fs();
|
|
4922
|
-
function createAgentSessionsRouter(
|
|
4865
|
+
function createAgentSessionsRouter(projectsDir, broadcast) {
|
|
4923
4866
|
const router = Router3();
|
|
4924
4867
|
router.get("/", async (_req, res) => {
|
|
4925
4868
|
try {
|
|
4926
|
-
await reconcileActiveSessions(
|
|
4927
|
-
const sessions = await listAllSessions(
|
|
4869
|
+
await reconcileActiveSessions(projectsDir);
|
|
4870
|
+
const sessions = await listAllSessions(projectsDir);
|
|
4928
4871
|
res.json({ sessions, generatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
4929
4872
|
} catch (error) {
|
|
4930
4873
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to list sessions" });
|
|
4931
4874
|
}
|
|
4932
4875
|
});
|
|
4933
|
-
router.get("/:
|
|
4876
|
+
router.get("/:projectSlug", async (req, res) => {
|
|
4934
4877
|
try {
|
|
4935
|
-
const {
|
|
4878
|
+
const { projectSlug } = req.params;
|
|
4936
4879
|
const assignment = req.query.assignment;
|
|
4937
|
-
const
|
|
4938
|
-
if (!await fileExists(
|
|
4939
|
-
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` });
|
|
4940
4883
|
return;
|
|
4941
4884
|
}
|
|
4942
|
-
await reconcileActiveSessions(
|
|
4943
|
-
const sessions = await
|
|
4885
|
+
await reconcileActiveSessions(projectsDir);
|
|
4886
|
+
const sessions = await listProjectSessions(projectsDir, projectSlug, assignment);
|
|
4944
4887
|
res.json({ sessions, generatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
4945
4888
|
} catch (error) {
|
|
4946
4889
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to list sessions" });
|
|
@@ -4948,21 +4891,21 @@ function createAgentSessionsRouter(missionsDir, broadcast) {
|
|
|
4948
4891
|
});
|
|
4949
4892
|
router.post("/", async (req, res) => {
|
|
4950
4893
|
try {
|
|
4951
|
-
const {
|
|
4894
|
+
const { projectSlug, assignmentSlug, agent, sessionId, path, description } = req.body;
|
|
4952
4895
|
if (!agent) {
|
|
4953
4896
|
res.status(400).json({ error: "agent is required" });
|
|
4954
4897
|
return;
|
|
4955
4898
|
}
|
|
4956
|
-
if (
|
|
4957
|
-
const
|
|
4958
|
-
if (!await fileExists(
|
|
4959
|
-
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` });
|
|
4960
4903
|
return;
|
|
4961
4904
|
}
|
|
4962
4905
|
}
|
|
4963
4906
|
const id = sessionId || randomUUID2();
|
|
4964
4907
|
const session = {
|
|
4965
|
-
|
|
4908
|
+
projectSlug: projectSlug || null,
|
|
4966
4909
|
assignmentSlug: assignmentSlug || null,
|
|
4967
4910
|
agent,
|
|
4968
4911
|
sessionId: id,
|
|
@@ -5619,7 +5562,7 @@ import { cp, mkdtemp, rm as rm2, readFile as readFile11, writeFile as writeFile3
|
|
|
5619
5562
|
import { resolve as resolve14, join as join2 } from "path";
|
|
5620
5563
|
import { tmpdir } from "os";
|
|
5621
5564
|
var exec2 = promisify2(execFile2);
|
|
5622
|
-
var VALID_CATEGORIES = ["
|
|
5565
|
+
var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
|
|
5623
5566
|
var LOCK_FILE_NAME = ".backup-lock";
|
|
5624
5567
|
function parseCategoriesStrict(cats) {
|
|
5625
5568
|
const unknown = [];
|
|
@@ -5645,9 +5588,9 @@ function validateRepoUrl(url) {
|
|
|
5645
5588
|
}
|
|
5646
5589
|
async function resolveCategoryPath(category) {
|
|
5647
5590
|
switch (category) {
|
|
5648
|
-
case "
|
|
5591
|
+
case "projects": {
|
|
5649
5592
|
const config = await readConfig();
|
|
5650
|
-
return { sourcePath: config.
|
|
5593
|
+
return { sourcePath: config.defaultProjectDir, repoPath: "projects", isFile: false };
|
|
5651
5594
|
}
|
|
5652
5595
|
case "playbooks":
|
|
5653
5596
|
return { sourcePath: playbooksDir(), repoPath: "playbooks", isFile: false };
|
|
@@ -5737,7 +5680,7 @@ async function backupToGithub(overrides) {
|
|
|
5737
5680
|
if (!validateRepoUrl(repo)) {
|
|
5738
5681
|
throw new Error(`Invalid repo URL: "${rawRepo}". Must start with https:// or git@.`);
|
|
5739
5682
|
}
|
|
5740
|
-
const categoriesCsv = config.backup?.categories ?? "
|
|
5683
|
+
const categoriesCsv = config.backup?.categories ?? "projects, playbooks, todos, servers, config";
|
|
5741
5684
|
const categories = overrides?.categories ?? resolveCategoriesStrict(categoriesCsv);
|
|
5742
5685
|
if (categories.length === 0) {
|
|
5743
5686
|
throw new Error("No valid backup categories selected.");
|
|
@@ -5866,7 +5809,7 @@ async function restoreFromGithub(overrides) {
|
|
|
5866
5809
|
if (!validateRepoUrl(repo)) {
|
|
5867
5810
|
throw new Error(`Invalid repo URL: "${rawRepo}".`);
|
|
5868
5811
|
}
|
|
5869
|
-
const categoriesCsv = config.backup?.categories ?? "
|
|
5812
|
+
const categoriesCsv = config.backup?.categories ?? "projects, playbooks, todos, servers, config";
|
|
5870
5813
|
const categories = overrides?.categories ?? resolveCategoriesStrict(categoriesCsv);
|
|
5871
5814
|
if (categories.length === 0) {
|
|
5872
5815
|
throw new Error("No valid restore categories selected.");
|
|
@@ -5921,7 +5864,7 @@ async function getBackupStatus() {
|
|
|
5921
5864
|
const locked = await fileExists(lockPath);
|
|
5922
5865
|
return {
|
|
5923
5866
|
repo: config.backup?.repo ?? null,
|
|
5924
|
-
categories: config.backup?.categories ?? "
|
|
5867
|
+
categories: config.backup?.categories ?? "projects, playbooks, todos, servers, config",
|
|
5925
5868
|
lastBackup: config.backup?.lastBackup ?? null,
|
|
5926
5869
|
lastRestore: config.backup?.lastRestore ?? null,
|
|
5927
5870
|
locked
|
|
@@ -6072,7 +6015,7 @@ async function stopAutodiscovery() {
|
|
|
6072
6015
|
function runReconcile() {
|
|
6073
6016
|
if (activeReconcile || !savedOptions) return;
|
|
6074
6017
|
const opts = savedOptions;
|
|
6075
|
-
activeReconcile = reconcile(opts.serversDir, opts.
|
|
6018
|
+
activeReconcile = reconcile(opts.serversDir, opts.projectsDir, opts.excludePids).catch((err) => {
|
|
6076
6019
|
console.error("[autodiscovery] reconcile failed:", err);
|
|
6077
6020
|
}).finally(() => {
|
|
6078
6021
|
activeReconcile = null;
|
|
@@ -6083,10 +6026,10 @@ async function listAllTmuxSessions() {
|
|
|
6083
6026
|
if (!output) return [];
|
|
6084
6027
|
return output.split("\n").filter((line) => line.length > 0);
|
|
6085
6028
|
}
|
|
6086
|
-
async function discoverTmuxSessions(serversDir2,
|
|
6029
|
+
async function discoverTmuxSessions(serversDir2, projectsDir, existingNames) {
|
|
6087
6030
|
const tmuxAvailable = await checkTmuxAvailable();
|
|
6088
6031
|
if (!tmuxAvailable) return false;
|
|
6089
|
-
const workspaceRecords = await loadWorkspaceRecords(
|
|
6032
|
+
const workspaceRecords = await loadWorkspaceRecords(projectsDir);
|
|
6090
6033
|
if (workspaceRecords.length === 0) return false;
|
|
6091
6034
|
const sessions = await listAllTmuxSessions();
|
|
6092
6035
|
let changed = false;
|
|
@@ -6127,8 +6070,8 @@ async function getProcessCwd(pid) {
|
|
|
6127
6070
|
}
|
|
6128
6071
|
return null;
|
|
6129
6072
|
}
|
|
6130
|
-
async function discoverProcesses(serversDir2,
|
|
6131
|
-
const workspaceRecords = await loadWorkspaceRecords(
|
|
6073
|
+
async function discoverProcesses(serversDir2, projectsDir, existingFiles, excludePids) {
|
|
6074
|
+
const workspaceRecords = await loadWorkspaceRecords(projectsDir);
|
|
6132
6075
|
if (workspaceRecords.length === 0) return false;
|
|
6133
6076
|
const lsofOutput = await getLsofOutput();
|
|
6134
6077
|
if (!lsofOutput) return false;
|
|
@@ -6193,7 +6136,7 @@ async function isProcessAlive(pid) {
|
|
|
6193
6136
|
return false;
|
|
6194
6137
|
}
|
|
6195
6138
|
}
|
|
6196
|
-
async function reconcile(serversDir2,
|
|
6139
|
+
async function reconcile(serversDir2, projectsDir, excludePids) {
|
|
6197
6140
|
const names = await listSessionFiles(serversDir2);
|
|
6198
6141
|
const existingFiles = /* @__PURE__ */ new Map();
|
|
6199
6142
|
for (const name of names) {
|
|
@@ -6205,8 +6148,8 @@ async function reconcile(serversDir2, missionsDir, excludePids) {
|
|
|
6205
6148
|
existingFiles.delete(name);
|
|
6206
6149
|
}
|
|
6207
6150
|
const existingNames = new Set(existingFiles.keys());
|
|
6208
|
-
const tmuxChanged = await discoverTmuxSessions(serversDir2,
|
|
6209
|
-
const processChanged = await discoverProcesses(serversDir2,
|
|
6151
|
+
const tmuxChanged = await discoverTmuxSessions(serversDir2, projectsDir, existingNames);
|
|
6152
|
+
const processChanged = await discoverProcesses(serversDir2, projectsDir, existingFiles, excludePids);
|
|
6210
6153
|
if (tmuxChanged || processChanged || cleanupChanged) {
|
|
6211
6154
|
clearScanCache();
|
|
6212
6155
|
}
|
|
@@ -6214,7 +6157,7 @@ async function reconcile(serversDir2, missionsDir, excludePids) {
|
|
|
6214
6157
|
|
|
6215
6158
|
// src/dashboard/server.ts
|
|
6216
6159
|
function createDashboardServer(options) {
|
|
6217
|
-
const { port,
|
|
6160
|
+
const { port, projectsDir, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, serveStaticUi, dashboardDistPath } = options;
|
|
6218
6161
|
const app = express();
|
|
6219
6162
|
const server = createServer(app);
|
|
6220
6163
|
const wss = new WebSocketServer({ noServer: true });
|
|
@@ -6248,13 +6191,13 @@ function createDashboardServer(options) {
|
|
|
6248
6191
|
}
|
|
6249
6192
|
}
|
|
6250
6193
|
initSessionDb();
|
|
6251
|
-
migrateFromMarkdown(
|
|
6194
|
+
migrateFromMarkdown(projectsDir).catch((err) => {
|
|
6252
6195
|
console.error("Session migration from markdown failed:", err);
|
|
6253
6196
|
});
|
|
6254
6197
|
app.use(express.json());
|
|
6255
6198
|
app.get("/api/overview", async (_req, res) => {
|
|
6256
6199
|
try {
|
|
6257
|
-
const overview = await getOverview(
|
|
6200
|
+
const overview = await getOverview(projectsDir, serversDir2);
|
|
6258
6201
|
res.json(overview);
|
|
6259
6202
|
} catch (error) {
|
|
6260
6203
|
console.error("Error getting overview:", error);
|
|
@@ -6263,7 +6206,7 @@ function createDashboardServer(options) {
|
|
|
6263
6206
|
});
|
|
6264
6207
|
app.get("/api/attention", async (_req, res) => {
|
|
6265
6208
|
try {
|
|
6266
|
-
const attention = await getAttention(
|
|
6209
|
+
const attention = await getAttention(projectsDir, serversDir2);
|
|
6267
6210
|
res.json(attention);
|
|
6268
6211
|
} catch (error) {
|
|
6269
6212
|
console.error("Error getting attention queue:", error);
|
|
@@ -6330,26 +6273,26 @@ function createDashboardServer(options) {
|
|
|
6330
6273
|
res.status(500).json({ error: "Failed to reset status config" });
|
|
6331
6274
|
}
|
|
6332
6275
|
});
|
|
6333
|
-
app.get("/api/
|
|
6276
|
+
app.get("/api/projects", async (req, res) => {
|
|
6334
6277
|
try {
|
|
6335
|
-
let
|
|
6278
|
+
let projects = await listProjects(projectsDir);
|
|
6336
6279
|
const workspaceParam = req.query.workspace;
|
|
6337
6280
|
if (workspaceParam) {
|
|
6338
6281
|
if (workspaceParam === "_ungrouped") {
|
|
6339
|
-
|
|
6282
|
+
projects = projects.filter((m) => m.workspace === null);
|
|
6340
6283
|
} else {
|
|
6341
|
-
|
|
6284
|
+
projects = projects.filter((m) => m.workspace === workspaceParam);
|
|
6342
6285
|
}
|
|
6343
6286
|
}
|
|
6344
|
-
res.json(
|
|
6287
|
+
res.json(projects);
|
|
6345
6288
|
} catch (error) {
|
|
6346
|
-
console.error("Error listing
|
|
6347
|
-
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" });
|
|
6348
6291
|
}
|
|
6349
6292
|
});
|
|
6350
6293
|
app.get("/api/workspaces", async (_req, res) => {
|
|
6351
6294
|
try {
|
|
6352
|
-
const result = await listWorkspaces(
|
|
6295
|
+
const result = await listWorkspaces(projectsDir);
|
|
6353
6296
|
res.json(result);
|
|
6354
6297
|
} catch (error) {
|
|
6355
6298
|
console.error("Error listing workspaces:", error);
|
|
@@ -6363,8 +6306,8 @@ function createDashboardServer(options) {
|
|
|
6363
6306
|
res.status(400).json({ error: "Invalid workspace name. Use lowercase letters, numbers, and hyphens." });
|
|
6364
6307
|
return;
|
|
6365
6308
|
}
|
|
6366
|
-
await createWorkspace(
|
|
6367
|
-
broadcast({ type: "
|
|
6309
|
+
await createWorkspace(projectsDir, name);
|
|
6310
|
+
broadcast({ type: "project-updated", projectSlug: "", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
6368
6311
|
res.json({ name });
|
|
6369
6312
|
} catch (error) {
|
|
6370
6313
|
console.error("Error creating workspace:", error);
|
|
@@ -6373,8 +6316,8 @@ function createDashboardServer(options) {
|
|
|
6373
6316
|
});
|
|
6374
6317
|
app.delete("/api/workspaces/:name", async (req, res) => {
|
|
6375
6318
|
try {
|
|
6376
|
-
await deleteWorkspace(
|
|
6377
|
-
broadcast({ type: "
|
|
6319
|
+
await deleteWorkspace(projectsDir, req.params.name);
|
|
6320
|
+
broadcast({ type: "project-updated", projectSlug: "", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
6378
6321
|
res.json({ ok: true });
|
|
6379
6322
|
} catch (error) {
|
|
6380
6323
|
console.error("Error deleting workspace:", error);
|
|
@@ -6383,13 +6326,13 @@ function createDashboardServer(options) {
|
|
|
6383
6326
|
});
|
|
6384
6327
|
app.get("/api/assignments", async (req, res) => {
|
|
6385
6328
|
try {
|
|
6386
|
-
const result = await listAssignmentsBoard(
|
|
6329
|
+
const result = await listAssignmentsBoard(projectsDir);
|
|
6387
6330
|
const workspaceParam = req.query.workspace;
|
|
6388
6331
|
if (workspaceParam) {
|
|
6389
6332
|
if (workspaceParam === "_ungrouped") {
|
|
6390
|
-
result.assignments = result.assignments.filter((a) => a.
|
|
6333
|
+
result.assignments = result.assignments.filter((a) => a.projectWorkspace === null);
|
|
6391
6334
|
} else {
|
|
6392
|
-
result.assignments = result.assignments.filter((a) => a.
|
|
6335
|
+
result.assignments = result.assignments.filter((a) => a.projectWorkspace === workspaceParam);
|
|
6393
6336
|
}
|
|
6394
6337
|
}
|
|
6395
6338
|
res.json(result);
|
|
@@ -6398,29 +6341,29 @@ function createDashboardServer(options) {
|
|
|
6398
6341
|
res.status(500).json({ error: "Failed to list assignments" });
|
|
6399
6342
|
}
|
|
6400
6343
|
});
|
|
6401
|
-
app.get("/api/
|
|
6344
|
+
app.get("/api/projects/:slug", async (req, res) => {
|
|
6402
6345
|
try {
|
|
6403
|
-
const detail = await
|
|
6346
|
+
const detail = await getProjectDetail(projectsDir, req.params.slug);
|
|
6404
6347
|
if (!detail) {
|
|
6405
|
-
res.status(404).json({ error: `
|
|
6348
|
+
res.status(404).json({ error: `Project "${req.params.slug}" not found` });
|
|
6406
6349
|
return;
|
|
6407
6350
|
}
|
|
6408
6351
|
res.json(detail);
|
|
6409
6352
|
} catch (error) {
|
|
6410
|
-
console.error("Error getting
|
|
6411
|
-
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" });
|
|
6412
6355
|
}
|
|
6413
6356
|
});
|
|
6414
|
-
app.get("/api/
|
|
6357
|
+
app.get("/api/projects/:slug/assignments/:aslug", async (req, res) => {
|
|
6415
6358
|
try {
|
|
6416
6359
|
const detail = await getAssignmentDetail(
|
|
6417
|
-
|
|
6360
|
+
projectsDir,
|
|
6418
6361
|
req.params.slug,
|
|
6419
6362
|
req.params.aslug
|
|
6420
6363
|
);
|
|
6421
6364
|
if (!detail) {
|
|
6422
6365
|
res.status(404).json({
|
|
6423
|
-
error: `Assignment "${req.params.aslug}" not found in
|
|
6366
|
+
error: `Assignment "${req.params.aslug}" not found in project "${req.params.slug}"`
|
|
6424
6367
|
});
|
|
6425
6368
|
return;
|
|
6426
6369
|
}
|
|
@@ -6430,9 +6373,9 @@ function createDashboardServer(options) {
|
|
|
6430
6373
|
res.status(500).json({ error: "Failed to get assignment detail" });
|
|
6431
6374
|
}
|
|
6432
6375
|
});
|
|
6433
|
-
app.use(createWriteRouter(
|
|
6434
|
-
app.use("/api/servers", createServersRouter(serversDir2,
|
|
6435
|
-
app.use("/api/agent-sessions", createAgentSessionsRouter(
|
|
6376
|
+
app.use(createWriteRouter(projectsDir));
|
|
6377
|
+
app.use("/api/servers", createServersRouter(serversDir2, projectsDir));
|
|
6378
|
+
app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir, broadcast));
|
|
6436
6379
|
app.use("/api/playbooks", createPlaybooksRouter(playbooksDir2));
|
|
6437
6380
|
app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
|
|
6438
6381
|
app.use("/api/backup", createBackupRouter());
|
|
@@ -6453,13 +6396,13 @@ function createDashboardServer(options) {
|
|
|
6453
6396
|
return {
|
|
6454
6397
|
async start() {
|
|
6455
6398
|
watcherHandle = createWatcher({
|
|
6456
|
-
|
|
6399
|
+
projectsDir,
|
|
6457
6400
|
serversDir: serversDir2,
|
|
6458
6401
|
playbooksDir: playbooksDir2,
|
|
6459
6402
|
todosDir: todosDir2,
|
|
6460
6403
|
onMessage: broadcast
|
|
6461
6404
|
});
|
|
6462
|
-
startAutodiscovery({ serversDir: serversDir2,
|
|
6405
|
+
startAutodiscovery({ serversDir: serversDir2, projectsDir, excludePids: /* @__PURE__ */ new Set([process.pid]) });
|
|
6463
6406
|
return new Promise((resolvePromise, reject) => {
|
|
6464
6407
|
server.on("error", (err) => {
|
|
6465
6408
|
if (err.code === "EADDRINUSE") {
|
|
@@ -6471,7 +6414,7 @@ function createDashboardServer(options) {
|
|
|
6471
6414
|
}
|
|
6472
6415
|
});
|
|
6473
6416
|
server.listen(port, () => {
|
|
6474
|
-
const portFile = resolve15(
|
|
6417
|
+
const portFile = resolve15(syntaurRoot(), "dashboard-port");
|
|
6475
6418
|
writeFile4(portFile, String(port), "utf-8").catch(() => {
|
|
6476
6419
|
});
|
|
6477
6420
|
resolvePromise();
|
|
@@ -6485,12 +6428,13 @@ function createDashboardServer(options) {
|
|
|
6485
6428
|
}
|
|
6486
6429
|
closeSessionDb();
|
|
6487
6430
|
for (const client of clients) {
|
|
6488
|
-
client.
|
|
6431
|
+
client.terminate();
|
|
6489
6432
|
}
|
|
6490
6433
|
clients.clear();
|
|
6491
|
-
const portFile = resolve15(
|
|
6434
|
+
const portFile = resolve15(syntaurRoot(), "dashboard-port");
|
|
6492
6435
|
await unlink4(portFile).catch(() => {
|
|
6493
6436
|
});
|
|
6437
|
+
server.closeAllConnections?.();
|
|
6494
6438
|
return new Promise((resolvePromise) => {
|
|
6495
6439
|
server.close(() => resolvePromise());
|
|
6496
6440
|
});
|