syntaur 0.8.0 → 0.12.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/.claude-plugin/plugin.json +9 -1
- package/dashboard/dist/assets/{_basePickBy-CIZortGD.js → _basePickBy-XOV2F7dX.js} +1 -1
- package/dashboard/dist/assets/{_baseUniq-BHP5NBQ7.js → _baseUniq-Dccfk3fK.js} +1 -1
- package/dashboard/dist/assets/{arc-BLtQy7Cl.js → arc-TwJ5RF-H.js} +1 -1
- package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-0mhIBjWy.js → architectureDiagram-2XIMDMQ5-CpkVJudw.js} +1 -1
- package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-CM7K5Tf5.js → blockDiagram-WCTKOSBZ-Ck6VQ4Fh.js} +1 -1
- package/dashboard/dist/assets/{c4Diagram-IC4MRINW-gsbeygvS.js → c4Diagram-IC4MRINW-B5V7YG03.js} +1 -1
- package/dashboard/dist/assets/channel-AEMTX70R.js +1 -0
- package/dashboard/dist/assets/{chunk-4BX2VUAB-CneRz9Nd.js → chunk-4BX2VUAB-rBdyOxAe.js} +1 -1
- package/dashboard/dist/assets/{chunk-55IACEB6-uetANWHd.js → chunk-55IACEB6-DKlNXjYt.js} +1 -1
- package/dashboard/dist/assets/{chunk-FMBD7UC4-3-tNszoF.js → chunk-FMBD7UC4-Cqm0PtUI.js} +1 -1
- package/dashboard/dist/assets/{chunk-JSJVCQXG-DOO88rZD.js → chunk-JSJVCQXG-CQyXQcG3.js} +1 -1
- package/dashboard/dist/assets/{chunk-KX2RTZJC-Bnc1CFy2.js → chunk-KX2RTZJC-DmqjHPpJ.js} +1 -1
- package/dashboard/dist/assets/{chunk-NQ4KR5QH-BfViC1Ms.js → chunk-NQ4KR5QH-CHXoBcYa.js} +1 -1
- package/dashboard/dist/assets/{chunk-QZHKN3VN-CgjyAt2l.js → chunk-QZHKN3VN-Dd4sChBS.js} +1 -1
- package/dashboard/dist/assets/{chunk-WL4C6EOR-BYFermSR.js → chunk-WL4C6EOR-D-QLnkY8.js} +1 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-LzuOAGzu.js +1 -0
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-LzuOAGzu.js +1 -0
- package/dashboard/dist/assets/clone-Cw63QGUJ.js +1 -0
- package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-CE-k5SLv.js → cose-bilkent-S5V4N54A-VZi6uV56.js} +1 -1
- package/dashboard/dist/assets/{dagre-KLK3FWXG-CX7Br8LQ.js → dagre-KLK3FWXG-CtT97Qhx.js} +1 -1
- package/dashboard/dist/assets/{diagram-E7M64L7V-t-xhWjCU.js → diagram-E7M64L7V-BjYTNNHb.js} +1 -1
- package/dashboard/dist/assets/{diagram-IFDJBPK2-BVhcTXsJ.js → diagram-IFDJBPK2-qj8_r6fb.js} +1 -1
- package/dashboard/dist/assets/{diagram-P4PSJMXO-fxt8ZSca.js → diagram-P4PSJMXO-DlfWo7Vt.js} +1 -1
- package/dashboard/dist/assets/{erDiagram-INFDFZHY-C20k2bxX.js → erDiagram-INFDFZHY-Ch_nApTv.js} +1 -1
- package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-CdeygexM.js → flowDiagram-PKNHOUZH-EOVsqNyi.js} +1 -1
- package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-CPsoOAV-.js → ganttDiagram-A5KZAMGK-CS-lx0B-.js} +1 -1
- package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-t0tCTIZy.js → gitGraphDiagram-K3NZZRJ6-Ba6RDzPy.js} +1 -1
- package/dashboard/dist/assets/{graph-Bcqd31wL.js → graph-BKaAyLk-.js} +1 -1
- package/dashboard/dist/assets/index-BTWPZaIA.css +1 -0
- package/dashboard/dist/assets/index-Dk9PBm5B.js +510 -0
- package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-DgoKvH3N.js → infoDiagram-LFFYTUFH-Cr20xEsd.js} +1 -1
- package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-CGr4P8do.js → ishikawaDiagram-PHBUUO56-CuILV9ph.js} +1 -1
- package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-CHYRi6wF.js → journeyDiagram-4ABVD52K-CO-l7Mpm.js} +1 -1
- package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-kwzqrFcW.js → kanban-definition-K7BYSVSG-CcirPw_S.js} +1 -1
- package/dashboard/dist/assets/{layout-wud47qGs.js → layout-BXvr8dMw.js} +1 -1
- package/dashboard/dist/assets/{linear-DG5js90X.js → linear-B85hgYnX.js} +1 -1
- package/dashboard/dist/assets/{mermaid.core-BDw69uC2.js → mermaid.core-DcZN9Xp2.js} +4 -4
- package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-Dw_uyg1-.js → mindmap-definition-YRQLILUH-Bxi6p5_3.js} +1 -1
- package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-C7NBi6tK.js → pieDiagram-SKSYHLDU-DOSvU0UD.js} +1 -1
- package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-_N_BSBd4.js → quadrantDiagram-337W2JSQ-Cmo-5tW9.js} +1 -1
- package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BqUiqfP1.js → requirementDiagram-Z7DCOOCP-Dw8alwuI.js} +1 -1
- package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-C2Qq5ULW.js → sankeyDiagram-WA2Y5GQK-ZPB-rzOg.js} +1 -1
- package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-BOCF2z8f.js → sequenceDiagram-2WXFIKYE-Ct8bkK2h.js} +1 -1
- package/dashboard/dist/assets/{stateDiagram-RAJIS63D-BvDyTxC5.js → stateDiagram-RAJIS63D-DnGOXlmx.js} +1 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-COGzlZxn.js +1 -0
- package/dashboard/dist/assets/{timeline-definition-YZTLITO2-DNNZxSrG.js → timeline-definition-YZTLITO2-AUFmbJjr.js} +1 -1
- package/dashboard/dist/assets/{treemap-KZPCXAKY-C6hOdiGH.js → treemap-KZPCXAKY-Dw5FDFAV.js} +1 -1
- package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-FmXl6puP.js → vennDiagram-LZ73GAT5-DfxicU4N.js} +1 -1
- package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-Bjav12WG.js → xychartDiagram-JWTSCODW--9N0V1r_.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/syntaur-logo.svg +5 -13
- package/dist/dashboard/server.js +363 -3
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +2140 -153
- package/dist/index.js.map +1 -1
- package/examples/playbooks/keep-records-updated.md +2 -1
- package/package.json +1 -1
- package/platforms/claude-code/.claude-plugin/plugin.json +9 -1
- package/platforms/claude-code/agents/syntaur-expert.md +1 -1
- package/platforms/claude-code/commands/add-memory/add-memory.md +18 -0
- package/platforms/claude-code/commands/add-resource/add-resource.md +16 -0
- package/platforms/claude-code/commands/list-assignments/list-assignments.md +16 -0
- package/platforms/claude-code/commands/log-progress/log-progress.md +16 -0
- package/platforms/claude-code/commands/replan/replan.md +16 -0
- package/platforms/claude-code/commands/resume-session/resume-session.md +16 -0
- package/platforms/claude-code/commands/set-workspace/set-workspace.md +16 -0
- package/platforms/claude-code/commands/syntaur-worktree/syntaur-worktree.md +16 -0
- package/platforms/claude-code/references/protocol-summary.md +8 -0
- package/platforms/claude-code/skills/add-memory/SKILL.md +96 -0
- package/platforms/claude-code/skills/add-resource/SKILL.md +85 -0
- package/platforms/claude-code/skills/capture-artifacts/SKILL.md +84 -0
- package/platforms/claude-code/skills/complete-assignment/SKILL.md +2 -1
- package/platforms/claude-code/skills/list-assignments/SKILL.md +73 -0
- package/platforms/claude-code/skills/log-progress/SKILL.md +128 -0
- package/platforms/claude-code/skills/plan-assignment/SKILL.md +29 -8
- package/platforms/claude-code/skills/replan/SKILL.md +113 -0
- package/platforms/claude-code/skills/resume-session/SKILL.md +83 -0
- package/platforms/claude-code/skills/set-workspace/SKILL.md +109 -0
- package/platforms/claude-code/skills/syntaur-protocol/SKILL.md +4 -0
- package/platforms/claude-code/skills/syntaur-protocol/references/protocol-summary.md +8 -0
- package/platforms/claude-code/skills/syntaur-worktree/SKILL.md +117 -0
- package/platforms/codex/README.md +2 -2
- package/platforms/codex/agents/syntaur-operator.md +7 -0
- package/platforms/codex/commands/add-memory.md +16 -0
- package/platforms/codex/commands/add-resource.md +14 -0
- package/platforms/codex/commands/list-assignments.md +13 -0
- package/platforms/codex/commands/log-progress.md +14 -0
- package/platforms/codex/commands/replan.md +15 -0
- package/platforms/codex/commands/resume-session.md +15 -0
- package/platforms/codex/commands/set-workspace.md +15 -0
- package/platforms/codex/commands/syntaur-worktree.md +15 -0
- package/platforms/codex/references/protocol-summary.md +8 -0
- package/platforms/codex/skills/add-memory/SKILL.md +96 -0
- package/platforms/codex/skills/add-resource/SKILL.md +85 -0
- package/platforms/codex/skills/capture-artifacts/SKILL.md +84 -0
- package/platforms/codex/skills/complete-assignment/SKILL.md +2 -1
- package/platforms/codex/skills/list-assignments/SKILL.md +73 -0
- package/platforms/codex/skills/log-progress/SKILL.md +128 -0
- package/platforms/codex/skills/plan-assignment/SKILL.md +29 -8
- package/platforms/codex/skills/replan/SKILL.md +113 -0
- package/platforms/codex/skills/resume-session/SKILL.md +83 -0
- package/platforms/codex/skills/set-workspace/SKILL.md +109 -0
- package/platforms/codex/skills/syntaur-protocol/SKILL.md +4 -0
- package/platforms/codex/skills/syntaur-protocol/references/protocol-summary.md +8 -0
- package/platforms/codex/skills/syntaur-worktree/SKILL.md +117 -0
- package/skills/add-memory/SKILL.md +96 -0
- package/skills/add-resource/SKILL.md +85 -0
- package/skills/capture-artifacts/SKILL.md +84 -0
- package/skills/complete-assignment/SKILL.md +2 -1
- package/skills/list-assignments/SKILL.md +73 -0
- package/skills/log-progress/SKILL.md +128 -0
- package/skills/plan-assignment/SKILL.md +29 -8
- package/skills/replan/SKILL.md +113 -0
- package/skills/resume-session/SKILL.md +83 -0
- package/skills/set-workspace/SKILL.md +109 -0
- package/skills/syntaur-protocol/SKILL.md +4 -0
- package/skills/syntaur-protocol/references/protocol-summary.md +8 -0
- package/skills/syntaur-worktree/SKILL.md +117 -0
- package/dashboard/dist/assets/channel-FWLgstBg.js +0 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-BAvQTSn_.js +0 -1
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BAvQTSn_.js +0 -1
- package/dashboard/dist/assets/clone-rSEllQ1P.js +0 -1
- package/dashboard/dist/assets/index-bLWoSCLL.css +0 -1
- package/dashboard/dist/assets/index-eS0wtDO0.js +0 -500
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-TaLLhLWD.js +0 -1
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ __export(paths_exports, {
|
|
|
16
16
|
expandHome: () => expandHome,
|
|
17
17
|
playbooksDir: () => playbooksDir,
|
|
18
18
|
projectTodosDir: () => projectTodosDir,
|
|
19
|
+
proofDir: () => proofDir,
|
|
19
20
|
serversDir: () => serversDir,
|
|
20
21
|
syntaurRoot: () => syntaurRoot,
|
|
21
22
|
todoPlanDir: () => todoPlanDir,
|
|
@@ -57,6 +58,9 @@ function projectTodosDir(projectsDir2, projectSlug) {
|
|
|
57
58
|
function todoPlanDir(todosDir2, workspaceOrProject, todoId) {
|
|
58
59
|
return resolve(todosDir2, "plans", workspaceOrProject, todoId);
|
|
59
60
|
}
|
|
61
|
+
function proofDir(assignmentDir) {
|
|
62
|
+
return resolve(assignmentDir, "proof");
|
|
63
|
+
}
|
|
60
64
|
var init_paths = __esm({
|
|
61
65
|
"src/utils/paths.ts"() {
|
|
62
66
|
"use strict";
|
|
@@ -393,6 +397,7 @@ function parseMemory(fileContent) {
|
|
|
393
397
|
scope: getField(fm, "scope") ?? "",
|
|
394
398
|
sourceAssignment: getField(fm, "sourceAssignment"),
|
|
395
399
|
relatedAssignments: parseListField(fm, "relatedAssignments"),
|
|
400
|
+
tags: parseListField(fm, "tags"),
|
|
396
401
|
created: getField(fm, "created") ?? "",
|
|
397
402
|
updated: getField(fm, "updated") ?? "",
|
|
398
403
|
body
|
|
@@ -2157,6 +2162,40 @@ var init_decision_record = __esm({
|
|
|
2157
2162
|
});
|
|
2158
2163
|
|
|
2159
2164
|
// src/templates/index-stubs.ts
|
|
2165
|
+
function renderMemoryStub(params2) {
|
|
2166
|
+
return `---
|
|
2167
|
+
type: memory
|
|
2168
|
+
name: ${escapeYamlString(params2.name)}
|
|
2169
|
+
source: claude
|
|
2170
|
+
sourceAssignment: null
|
|
2171
|
+
relatedAssignments: []
|
|
2172
|
+
scope: project
|
|
2173
|
+
created: "${params2.timestamp}"
|
|
2174
|
+
updated: "${params2.timestamp}"
|
|
2175
|
+
tags: []
|
|
2176
|
+
---
|
|
2177
|
+
|
|
2178
|
+
# ${params2.name}
|
|
2179
|
+
|
|
2180
|
+
`;
|
|
2181
|
+
}
|
|
2182
|
+
function renderResourceStub(params2) {
|
|
2183
|
+
return `---
|
|
2184
|
+
type: resource
|
|
2185
|
+
name: ${escapeYamlString(params2.name)}
|
|
2186
|
+
source: claude
|
|
2187
|
+
category: documentation
|
|
2188
|
+
sourceUrl: null
|
|
2189
|
+
sourceAssignment: null
|
|
2190
|
+
relatedAssignments: []
|
|
2191
|
+
created: "${params2.timestamp}"
|
|
2192
|
+
updated: "${params2.timestamp}"
|
|
2193
|
+
---
|
|
2194
|
+
|
|
2195
|
+
# ${params2.name}
|
|
2196
|
+
|
|
2197
|
+
`;
|
|
2198
|
+
}
|
|
2160
2199
|
function renderIndexAssignments(params2) {
|
|
2161
2200
|
return `---
|
|
2162
2201
|
project: ${params2.slug}
|
|
@@ -2269,6 +2308,7 @@ total: 0
|
|
|
2269
2308
|
var init_index_stubs = __esm({
|
|
2270
2309
|
"src/templates/index-stubs.ts"() {
|
|
2271
2310
|
"use strict";
|
|
2311
|
+
init_yaml();
|
|
2272
2312
|
}
|
|
2273
2313
|
});
|
|
2274
2314
|
|
|
@@ -2511,6 +2551,15 @@ If the global Syntaur Codex plugin is installed, prefer these workflows instead
|
|
|
2511
2551
|
- \`plan-assignment\` -- write a versioned plan file (\`plan.md\`, \`plan-v2.md\`, ...) and link it from the \`## Todos\` section of \`assignment.md\`
|
|
2512
2552
|
- \`complete-assignment\` -- write the cross-ticket \`handoff.md\` entry, append a final entry to \`progress.md\`, close the session, and transition state
|
|
2513
2553
|
- \`save-session-summary\` -- write per-session continuity at \`<assignmentDir>/sessions/<sessionId>/summary.md\` for resume across sessions of the same agent. Codex has no \`PreCompact\` hook event \u2014 invoke this manually before compaction or session end.
|
|
2554
|
+
- \`capture-artifacts\` -- capture typed proof artifacts (screenshot/video/asciinema/http/text) for the active assignment. Criterion linkage is optional. Run \`syntaur proof build\` to render \`proof.html\`.
|
|
2555
|
+
- \`resume-session\` -- counterpart to \`save-session-summary\`; loads the latest summary, \`.syntaur/context.json\`, and any open handoff so a fresh session re-orients without re-reading the transcript
|
|
2556
|
+
- \`replan\` -- bump the active assignment to a new \`plan-v<N>.md\` per the Plan Versioning playbook (CLI does file ops, skill writes the body)
|
|
2557
|
+
- \`syntaur-worktree\` -- atomic worktree creation under \`<repository>/.worktrees/<branch>\` plus assign + start + context binding in one move
|
|
2558
|
+
- \`add-resource\` -- register a project-level resource (link to dashboard / doc / ticket); CLI regenerates \`_index.md\` server-side
|
|
2559
|
+
- \`add-memory\` -- capture a project-level Syntaur memory; CLI regenerates \`_index.md\` server-side (distinct from user-global Claude Code auto-memory)
|
|
2560
|
+
- \`list-assignments\` -- cross-project listing with filters by status, project, tag, age (scriptable; not the interactive \`browse\` TUI)
|
|
2561
|
+
- \`log-progress\` -- append a timestamped entry to the active \`progress.md\` and bump frontmatter (Keep Records Updated playbook)
|
|
2562
|
+
- \`set-workspace\` -- populate the four \`workspace.*\` fields in \`assignment.md\`; validates via \`syntaur doctor --assignment --json\` before writing
|
|
2514
2563
|
- \`track-session\` -- manage tracked tmux sessions for the dashboard
|
|
2515
2564
|
|
|
2516
2565
|
If the plugin is unavailable, follow the same workflow manually with the \`syntaur\` CLI and keep the protocol files current yourself.
|
|
@@ -2639,6 +2688,8 @@ Use the \`syntaur\` CLI for state transitions and coordination:
|
|
|
2639
2688
|
- \`syntaur fail ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- mark as failed
|
|
2640
2689
|
- \`syntaur comment ${params2.assignmentSlug} "body" --type question|note|feedback [--reply-to <id>]\` -- append to \`comments.md\` (use for all Q&A; questions support resolve toggle)
|
|
2641
2690
|
- \`syntaur request ${params2.assignmentSlug} <target-slug-or-uuid> "text"\` -- append a todo to another assignment's \`## Todos\` annotated \`(from: ${params2.assignmentSlug})\`
|
|
2691
|
+
- \`syntaur capture --kind <screenshot|video|asciinema|http|text> [--file <path>] [--criterion <index>] [--note <text>] ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- record a proof artifact. \`--kind=text\` requires \`--note\` and forbids \`--file\`. Criterion linkage is optional.
|
|
2692
|
+
- \`syntaur proof build ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- render \`proof.html\` and \`proof.md\` at the assignment dir. Atomic overwrite \u2014 safe to re-run.
|
|
2642
2693
|
|
|
2643
2694
|
## Troubleshooting
|
|
2644
2695
|
|
|
@@ -4456,7 +4507,7 @@ var init_scanner = __esm({
|
|
|
4456
4507
|
|
|
4457
4508
|
// src/dashboard/api.ts
|
|
4458
4509
|
import { readdir as readdir7, readFile as readFile9, writeFile as writeFile3 } from "fs/promises";
|
|
4459
|
-
import { resolve as resolve12, dirname as dirname3 } from "path";
|
|
4510
|
+
import { resolve as resolve12, dirname as dirname3, basename } from "path";
|
|
4460
4511
|
async function listStandaloneRecords(assignmentsDir2) {
|
|
4461
4512
|
if (!assignmentsDir2) return [];
|
|
4462
4513
|
if (!await fileExists(assignmentsDir2)) return [];
|
|
@@ -4465,10 +4516,10 @@ async function listStandaloneRecords(assignmentsDir2) {
|
|
|
4465
4516
|
for (const entry of entries) {
|
|
4466
4517
|
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
|
|
4467
4518
|
const assignmentDir = resolve12(assignmentsDir2, entry.name);
|
|
4468
|
-
const
|
|
4469
|
-
if (!await fileExists(
|
|
4519
|
+
const assignmentMdPath2 = resolve12(assignmentDir, "assignment.md");
|
|
4520
|
+
if (!await fileExists(assignmentMdPath2)) continue;
|
|
4470
4521
|
try {
|
|
4471
|
-
const content = await readFile9(
|
|
4522
|
+
const content = await readFile9(assignmentMdPath2, "utf-8");
|
|
4472
4523
|
const record = parseAssignmentFull(content);
|
|
4473
4524
|
records.push({ assignmentDir, id: entry.name, record });
|
|
4474
4525
|
} catch {
|
|
@@ -4832,11 +4883,11 @@ async function getProjectDetail(projectsDir2, slug) {
|
|
|
4832
4883
|
}
|
|
4833
4884
|
async function getAssignmentDetail(projectsDir2, projectSlug, assignmentSlug) {
|
|
4834
4885
|
const assignmentDir = resolve12(projectsDir2, projectSlug, "assignments", assignmentSlug);
|
|
4835
|
-
const
|
|
4836
|
-
if (!await fileExists(
|
|
4886
|
+
const assignmentMdPath2 = resolve12(assignmentDir, "assignment.md");
|
|
4887
|
+
if (!await fileExists(assignmentMdPath2)) {
|
|
4837
4888
|
return null;
|
|
4838
4889
|
}
|
|
4839
|
-
const assignmentContent = await readFile9(
|
|
4890
|
+
const assignmentContent = await readFile9(assignmentMdPath2, "utf-8");
|
|
4840
4891
|
const assignment = parseAssignmentFull(assignmentContent);
|
|
4841
4892
|
let plan = null;
|
|
4842
4893
|
const planPath = resolve12(assignmentDir, "plan.md");
|
|
@@ -5110,9 +5161,9 @@ async function getAssignmentDetailById(projectsDir2, assignmentsDir2, id) {
|
|
|
5110
5161
|
}
|
|
5111
5162
|
async function buildStandaloneAssignmentDetail(resolved) {
|
|
5112
5163
|
const assignmentDir = resolved.assignmentDir;
|
|
5113
|
-
const
|
|
5114
|
-
if (!await fileExists(
|
|
5115
|
-
const assignmentContent = await readFile9(
|
|
5164
|
+
const assignmentMdPath2 = resolve12(assignmentDir, "assignment.md");
|
|
5165
|
+
if (!await fileExists(assignmentMdPath2)) return null;
|
|
5166
|
+
const assignmentContent = await readFile9(assignmentMdPath2, "utf-8");
|
|
5116
5167
|
const assignment = parseAssignmentFull(assignmentContent);
|
|
5117
5168
|
let plan = null;
|
|
5118
5169
|
const planPath = resolve12(assignmentDir, "plan.md");
|
|
@@ -5295,12 +5346,102 @@ async function listMemories(projectPath) {
|
|
|
5295
5346
|
source: parsed.source,
|
|
5296
5347
|
scope: parsed.scope,
|
|
5297
5348
|
sourceAssignment: parsed.sourceAssignment,
|
|
5349
|
+
relatedAssignments: parsed.relatedAssignments,
|
|
5298
5350
|
updated: parsed.updated
|
|
5299
5351
|
});
|
|
5300
5352
|
}
|
|
5301
5353
|
results.sort((left, right) => compareTimestamps(right.updated, left.updated));
|
|
5302
5354
|
return results;
|
|
5303
5355
|
}
|
|
5356
|
+
async function listAllMemories(projectsDir2) {
|
|
5357
|
+
const projectRecords = await listProjectRecords(projectsDir2);
|
|
5358
|
+
const all = [];
|
|
5359
|
+
for (const record of projectRecords) {
|
|
5360
|
+
const memories = await listMemories(record.projectPath);
|
|
5361
|
+
for (const memory of memories) {
|
|
5362
|
+
all.push({
|
|
5363
|
+
...memory,
|
|
5364
|
+
projectSlug: basename(record.projectPath),
|
|
5365
|
+
projectTitle: record.summary.title
|
|
5366
|
+
});
|
|
5367
|
+
}
|
|
5368
|
+
}
|
|
5369
|
+
all.sort((left, right) => compareTimestamps(right.updated, left.updated));
|
|
5370
|
+
return all;
|
|
5371
|
+
}
|
|
5372
|
+
async function listAllResources(projectsDir2) {
|
|
5373
|
+
const projectRecords = await listProjectRecords(projectsDir2);
|
|
5374
|
+
const all = [];
|
|
5375
|
+
for (const record of projectRecords) {
|
|
5376
|
+
const resources = await listResources(record.projectPath);
|
|
5377
|
+
for (const resource of resources) {
|
|
5378
|
+
all.push({
|
|
5379
|
+
...resource,
|
|
5380
|
+
projectSlug: basename(record.projectPath),
|
|
5381
|
+
projectTitle: record.summary.title
|
|
5382
|
+
});
|
|
5383
|
+
}
|
|
5384
|
+
}
|
|
5385
|
+
all.sort((left, right) => compareTimestamps(right.updated, left.updated));
|
|
5386
|
+
return all;
|
|
5387
|
+
}
|
|
5388
|
+
async function resolveProjectPath(projectsDir2, projectSlug) {
|
|
5389
|
+
const direct = resolve12(projectsDir2, projectSlug);
|
|
5390
|
+
if (await fileExists(resolve12(direct, "project.md"))) return direct;
|
|
5391
|
+
const records = await listProjectRecords(projectsDir2);
|
|
5392
|
+
const match = records.find((r) => r.summary.slug === projectSlug);
|
|
5393
|
+
return match ? match.projectPath : null;
|
|
5394
|
+
}
|
|
5395
|
+
async function getMemoryDetail(projectsDir2, projectSlug, itemSlug) {
|
|
5396
|
+
if (itemSlug.startsWith("_")) return null;
|
|
5397
|
+
const projectRecords = await listProjectRecords(projectsDir2);
|
|
5398
|
+
const projectRecord = projectRecords.find(
|
|
5399
|
+
(p) => basename(p.projectPath) === projectSlug || p.summary.slug === projectSlug
|
|
5400
|
+
);
|
|
5401
|
+
if (!projectRecord) return null;
|
|
5402
|
+
const filePath = resolve12(projectRecord.projectPath, "memories", `${itemSlug}.md`);
|
|
5403
|
+
if (!await fileExists(filePath)) return null;
|
|
5404
|
+
const content = await readFile9(filePath, "utf-8");
|
|
5405
|
+
const parsed = parseMemory(content);
|
|
5406
|
+
return {
|
|
5407
|
+
name: parsed.name,
|
|
5408
|
+
slug: itemSlug,
|
|
5409
|
+
source: parsed.source,
|
|
5410
|
+
scope: parsed.scope,
|
|
5411
|
+
sourceAssignment: parsed.sourceAssignment,
|
|
5412
|
+
relatedAssignments: parsed.relatedAssignments,
|
|
5413
|
+
updated: parsed.updated,
|
|
5414
|
+
created: parsed.created,
|
|
5415
|
+
body: parsed.body,
|
|
5416
|
+
tags: parsed.tags,
|
|
5417
|
+
projectSlug: basename(projectRecord.projectPath),
|
|
5418
|
+
projectTitle: projectRecord.summary.title
|
|
5419
|
+
};
|
|
5420
|
+
}
|
|
5421
|
+
async function getResourceDetail(projectsDir2, projectSlug, itemSlug) {
|
|
5422
|
+
if (itemSlug.startsWith("_")) return null;
|
|
5423
|
+
const projectRecords = await listProjectRecords(projectsDir2);
|
|
5424
|
+
const projectRecord = projectRecords.find(
|
|
5425
|
+
(p) => basename(p.projectPath) === projectSlug || p.summary.slug === projectSlug
|
|
5426
|
+
);
|
|
5427
|
+
if (!projectRecord) return null;
|
|
5428
|
+
const filePath = resolve12(projectRecord.projectPath, "resources", `${itemSlug}.md`);
|
|
5429
|
+
if (!await fileExists(filePath)) return null;
|
|
5430
|
+
const content = await readFile9(filePath, "utf-8");
|
|
5431
|
+
const parsed = parseResource(content);
|
|
5432
|
+
return {
|
|
5433
|
+
name: parsed.name,
|
|
5434
|
+
slug: itemSlug,
|
|
5435
|
+
category: parsed.category,
|
|
5436
|
+
source: parsed.source,
|
|
5437
|
+
relatedAssignments: parsed.relatedAssignments,
|
|
5438
|
+
updated: parsed.updated,
|
|
5439
|
+
created: parsed.created,
|
|
5440
|
+
body: parsed.body,
|
|
5441
|
+
projectSlug: basename(projectRecord.projectPath),
|
|
5442
|
+
projectTitle: projectRecord.summary.title
|
|
5443
|
+
};
|
|
5444
|
+
}
|
|
5304
5445
|
async function loadDependencyGraph(projectPath, assignments) {
|
|
5305
5446
|
const statusPath = resolve12(projectPath, "_status.md");
|
|
5306
5447
|
if (await fileExists(statusPath)) {
|
|
@@ -5636,6 +5777,10 @@ function getDocumentPath(projectsDir2, documentType, projectSlug, assignmentSlug
|
|
|
5636
5777
|
return assignmentSlug ? resolve12(projectsDir2, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
|
|
5637
5778
|
case "decision-record":
|
|
5638
5779
|
return assignmentSlug ? resolve12(projectsDir2, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
|
|
5780
|
+
case "memory":
|
|
5781
|
+
return assignmentSlug ? resolve12(projectsDir2, projectSlug, "memories", `${assignmentSlug}.md`) : null;
|
|
5782
|
+
case "resource":
|
|
5783
|
+
return assignmentSlug ? resolve12(projectsDir2, projectSlug, "resources", `${assignmentSlug}.md`) : null;
|
|
5639
5784
|
default:
|
|
5640
5785
|
return null;
|
|
5641
5786
|
}
|
|
@@ -5656,6 +5801,10 @@ function getEditableDocumentTitle(documentType, projectSlug, assignmentSlug) {
|
|
|
5656
5801
|
return `Append Decision: ${assignmentSlug || "assignment"}`;
|
|
5657
5802
|
case "playbook":
|
|
5658
5803
|
return `Edit Playbook: ${projectSlug}`;
|
|
5804
|
+
case "memory":
|
|
5805
|
+
return `Edit Memory: ${assignmentSlug || "memory"}`;
|
|
5806
|
+
case "resource":
|
|
5807
|
+
return `Edit Resource: ${assignmentSlug || "resource"}`;
|
|
5659
5808
|
default:
|
|
5660
5809
|
return projectSlug;
|
|
5661
5810
|
}
|
|
@@ -6933,7 +7082,7 @@ var init_git_worktree = __esm({
|
|
|
6933
7082
|
});
|
|
6934
7083
|
|
|
6935
7084
|
// src/index.ts
|
|
6936
|
-
import { Command as
|
|
7085
|
+
import { Command as Command12 } from "commander";
|
|
6937
7086
|
|
|
6938
7087
|
// src/commands/init.ts
|
|
6939
7088
|
init_paths();
|
|
@@ -7263,8 +7412,8 @@ async function migrateFromMarkdown(projectsDir2) {
|
|
|
7263
7412
|
return allSessions.length;
|
|
7264
7413
|
}
|
|
7265
7414
|
async function parseMarkdownSessionsIndex(filePath, projectSlug) {
|
|
7266
|
-
const { readFile:
|
|
7267
|
-
const raw = await
|
|
7415
|
+
const { readFile: readFile43 } = await import("fs/promises");
|
|
7416
|
+
const raw = await readFile43(filePath, "utf-8");
|
|
7268
7417
|
const sessions = [];
|
|
7269
7418
|
const lines = raw.split("\n");
|
|
7270
7419
|
let inTable = false;
|
|
@@ -7315,8 +7464,8 @@ function rowToSession(row) {
|
|
|
7315
7464
|
};
|
|
7316
7465
|
}
|
|
7317
7466
|
async function appendSession(_projectDir, session) {
|
|
7318
|
-
const
|
|
7319
|
-
|
|
7467
|
+
const db3 = getSessionDb();
|
|
7468
|
+
db3.prepare(`
|
|
7320
7469
|
INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path)
|
|
7321
7470
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7322
7471
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
@@ -7341,42 +7490,42 @@ async function appendSession(_projectDir, session) {
|
|
|
7341
7490
|
);
|
|
7342
7491
|
}
|
|
7343
7492
|
async function updateSessionStatus(_projectDir, sessionId, status) {
|
|
7344
|
-
const
|
|
7493
|
+
const db3 = getSessionDb();
|
|
7345
7494
|
const isTerminal = status === "completed" || status === "stopped";
|
|
7346
|
-
const result = isTerminal ?
|
|
7495
|
+
const result = isTerminal ? db3.prepare(
|
|
7347
7496
|
"UPDATE sessions SET status = ?, ended = datetime('now'), updated_at = datetime('now') WHERE session_id = ?"
|
|
7348
|
-
).run(status, sessionId) :
|
|
7497
|
+
).run(status, sessionId) : db3.prepare(
|
|
7349
7498
|
"UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE session_id = ?"
|
|
7350
7499
|
).run(status, sessionId);
|
|
7351
7500
|
return result.changes > 0;
|
|
7352
7501
|
}
|
|
7353
7502
|
async function listAllSessions(_projectsDir) {
|
|
7354
|
-
const
|
|
7355
|
-
const rows =
|
|
7503
|
+
const db3 = getSessionDb();
|
|
7504
|
+
const rows = db3.prepare("SELECT * FROM sessions ORDER BY started DESC").all();
|
|
7356
7505
|
return rows.map(rowToSession);
|
|
7357
7506
|
}
|
|
7358
7507
|
async function listProjectSessions(_projectsDir, projectSlug, assignmentSlug) {
|
|
7359
|
-
const
|
|
7508
|
+
const db3 = getSessionDb();
|
|
7360
7509
|
if (assignmentSlug) {
|
|
7361
|
-
const rows2 =
|
|
7510
|
+
const rows2 = db3.prepare(
|
|
7362
7511
|
"SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
|
|
7363
7512
|
).all(projectSlug, assignmentSlug);
|
|
7364
7513
|
return rows2.map(rowToSession);
|
|
7365
7514
|
}
|
|
7366
|
-
const rows =
|
|
7515
|
+
const rows = db3.prepare("SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC").all(projectSlug);
|
|
7367
7516
|
return rows.map(rowToSession);
|
|
7368
7517
|
}
|
|
7369
7518
|
async function deleteSessions(sessionIds) {
|
|
7370
7519
|
if (sessionIds.length === 0) return 0;
|
|
7371
|
-
const
|
|
7520
|
+
const db3 = getSessionDb();
|
|
7372
7521
|
const placeholders = sessionIds.map(() => "?").join(", ");
|
|
7373
|
-
const result =
|
|
7522
|
+
const result = db3.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
|
|
7374
7523
|
return result.changes;
|
|
7375
7524
|
}
|
|
7376
7525
|
var DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
|
|
7377
|
-
async function readAssignmentStatusFromPath(
|
|
7378
|
-
if (!await fileExists(
|
|
7379
|
-
const raw = await readFile10(
|
|
7526
|
+
async function readAssignmentStatusFromPath(assignmentMdPath2) {
|
|
7527
|
+
if (!await fileExists(assignmentMdPath2)) return null;
|
|
7528
|
+
const raw = await readFile10(assignmentMdPath2, "utf-8");
|
|
7380
7529
|
const match = raw.match(/^status:\s*(.+)$/m);
|
|
7381
7530
|
return match ? match[1].trim() : null;
|
|
7382
7531
|
}
|
|
@@ -7386,8 +7535,8 @@ async function readAssignmentStatus(projectDir, assignmentSlug) {
|
|
|
7386
7535
|
);
|
|
7387
7536
|
}
|
|
7388
7537
|
async function reconcileActiveSessions(projectsDir2, assignmentsDir2) {
|
|
7389
|
-
const
|
|
7390
|
-
const activeSessions =
|
|
7538
|
+
const db3 = getSessionDb();
|
|
7539
|
+
const activeSessions = db3.prepare("SELECT * FROM sessions WHERE status = 'active' AND assignment_slug IS NOT NULL").all();
|
|
7391
7540
|
if (activeSessions.length === 0) return 0;
|
|
7392
7541
|
const assignmentStatuses = /* @__PURE__ */ new Map();
|
|
7393
7542
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -7424,10 +7573,10 @@ async function reconcileActiveSessions(projectsDir2, assignmentsDir2) {
|
|
|
7424
7573
|
return totalUpdated;
|
|
7425
7574
|
}
|
|
7426
7575
|
async function listSessionsByAssignment(projectSlug, assignmentSlug) {
|
|
7427
|
-
const
|
|
7428
|
-
const rows = projectSlug === null ?
|
|
7576
|
+
const db3 = getSessionDb();
|
|
7577
|
+
const rows = projectSlug === null ? db3.prepare(
|
|
7429
7578
|
"SELECT * FROM sessions WHERE assignment_slug = ? AND project_slug IS NULL ORDER BY started DESC"
|
|
7430
|
-
).all(assignmentSlug) :
|
|
7579
|
+
).all(assignmentSlug) : db3.prepare(
|
|
7431
7580
|
"SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
|
|
7432
7581
|
).all(projectSlug, assignmentSlug);
|
|
7433
7582
|
return rows.map(rowToSession);
|
|
@@ -7633,8 +7782,8 @@ init_timestamp();
|
|
|
7633
7782
|
init_fs();
|
|
7634
7783
|
init_parser();
|
|
7635
7784
|
import { Router } from "express";
|
|
7636
|
-
import { resolve as resolve15 } from "path";
|
|
7637
|
-
import { rm, readFile as readFile11 } from "fs/promises";
|
|
7785
|
+
import { resolve as resolve15, basename as basename2 } from "path";
|
|
7786
|
+
import { rm, readFile as readFile11, open as fsOpen } from "fs/promises";
|
|
7638
7787
|
|
|
7639
7788
|
// src/dashboard/acceptance-criteria.ts
|
|
7640
7789
|
function splitFrontmatter(content) {
|
|
@@ -7906,6 +8055,210 @@ function createWriteRouter(projectsDir2, assignmentsDir2) {
|
|
|
7906
8055
|
}
|
|
7907
8056
|
res.json(document);
|
|
7908
8057
|
});
|
|
8058
|
+
router.get("/api/projects/:slug/memories/:itemSlug/edit", async (req, res) => {
|
|
8059
|
+
const slug = getParam(req.params.slug);
|
|
8060
|
+
const itemSlug = getParam(req.params.itemSlug);
|
|
8061
|
+
if (!isValidSlug(itemSlug)) {
|
|
8062
|
+
res.status(400).json({ error: "Invalid memory slug." });
|
|
8063
|
+
return;
|
|
8064
|
+
}
|
|
8065
|
+
const projectDir = await resolveProjectPath(projectsDir2, slug);
|
|
8066
|
+
if (!projectDir) {
|
|
8067
|
+
res.status(404).json({ error: `Project "${slug}" not found` });
|
|
8068
|
+
return;
|
|
8069
|
+
}
|
|
8070
|
+
const document = await getEditableDocument(projectsDir2, "memory", basename2(projectDir), itemSlug);
|
|
8071
|
+
if (!document) {
|
|
8072
|
+
res.status(404).json({ error: "Memory not found" });
|
|
8073
|
+
return;
|
|
8074
|
+
}
|
|
8075
|
+
res.json(document);
|
|
8076
|
+
});
|
|
8077
|
+
router.get("/api/projects/:slug/resources/:itemSlug/edit", async (req, res) => {
|
|
8078
|
+
const slug = getParam(req.params.slug);
|
|
8079
|
+
const itemSlug = getParam(req.params.itemSlug);
|
|
8080
|
+
if (!isValidSlug(itemSlug)) {
|
|
8081
|
+
res.status(400).json({ error: "Invalid resource slug." });
|
|
8082
|
+
return;
|
|
8083
|
+
}
|
|
8084
|
+
const projectDir = await resolveProjectPath(projectsDir2, slug);
|
|
8085
|
+
if (!projectDir) {
|
|
8086
|
+
res.status(404).json({ error: `Project "${slug}" not found` });
|
|
8087
|
+
return;
|
|
8088
|
+
}
|
|
8089
|
+
const document = await getEditableDocument(projectsDir2, "resource", basename2(projectDir), itemSlug);
|
|
8090
|
+
if (!document) {
|
|
8091
|
+
res.status(404).json({ error: "Resource not found" });
|
|
8092
|
+
return;
|
|
8093
|
+
}
|
|
8094
|
+
res.json(document);
|
|
8095
|
+
});
|
|
8096
|
+
function itemFolder(kind) {
|
|
8097
|
+
return kind === "memory" ? "memories" : "resources";
|
|
8098
|
+
}
|
|
8099
|
+
function renderItemStub(kind, params2) {
|
|
8100
|
+
return kind === "memory" ? renderMemoryStub(params2) : renderResourceStub(params2);
|
|
8101
|
+
}
|
|
8102
|
+
function replaceStubBody(stub, body) {
|
|
8103
|
+
const match = stub.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n)/);
|
|
8104
|
+
if (!match) return stub;
|
|
8105
|
+
return `${match[1]}
|
|
8106
|
+
${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"}`;
|
|
8107
|
+
}
|
|
8108
|
+
function extractFrontmatterBlock(content) {
|
|
8109
|
+
const match = content.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n)/);
|
|
8110
|
+
return match ? match[1] : null;
|
|
8111
|
+
}
|
|
8112
|
+
function parseItem(kind, content) {
|
|
8113
|
+
return kind === "memory" ? parseMemory(content) : parseResource(content);
|
|
8114
|
+
}
|
|
8115
|
+
async function getItemDetail(kind, projectSlug, itemSlug) {
|
|
8116
|
+
return kind === "memory" ? await getMemoryDetail(projectsDir2, projectSlug, itemSlug) : await getResourceDetail(projectsDir2, projectSlug, itemSlug);
|
|
8117
|
+
}
|
|
8118
|
+
async function resolveProjectDir(projectSlug) {
|
|
8119
|
+
return resolveProjectPath(projectsDir2, projectSlug);
|
|
8120
|
+
}
|
|
8121
|
+
function rejectBadItemSlug(itemSlug, kind, res) {
|
|
8122
|
+
if (isValidSlug(itemSlug)) return false;
|
|
8123
|
+
res.status(400).json({
|
|
8124
|
+
error: `Invalid ${kind} slug "${itemSlug}". Slugs must be lowercase letters, numbers, and hyphens only.`
|
|
8125
|
+
});
|
|
8126
|
+
return true;
|
|
8127
|
+
}
|
|
8128
|
+
for (const kind of ["memory", "resource"]) {
|
|
8129
|
+
const folder = itemFolder(kind);
|
|
8130
|
+
router.get(`/api/projects/:slug/${folder}/:itemSlug`, async (req, res) => {
|
|
8131
|
+
try {
|
|
8132
|
+
const projectSlug = getParam(req.params.slug);
|
|
8133
|
+
const itemSlug = getParam(req.params.itemSlug);
|
|
8134
|
+
if (rejectBadItemSlug(itemSlug, kind, res)) return;
|
|
8135
|
+
const detail = await getItemDetail(kind, projectSlug, itemSlug);
|
|
8136
|
+
if (!detail) {
|
|
8137
|
+
res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
|
|
8138
|
+
return;
|
|
8139
|
+
}
|
|
8140
|
+
res.json(detail);
|
|
8141
|
+
} catch (error) {
|
|
8142
|
+
console.error(`Error fetching ${kind} detail:`, error);
|
|
8143
|
+
res.status(500).json({ error: `Failed to load ${kind}: ${error.message}` });
|
|
8144
|
+
}
|
|
8145
|
+
});
|
|
8146
|
+
router.post(`/api/projects/:slug/${folder}`, async (req, res) => {
|
|
8147
|
+
try {
|
|
8148
|
+
const projectSlug = getParam(req.params.slug);
|
|
8149
|
+
const projectDir = await resolveProjectDir(projectSlug);
|
|
8150
|
+
if (!projectDir) {
|
|
8151
|
+
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
8152
|
+
return;
|
|
8153
|
+
}
|
|
8154
|
+
const body = req.body ?? {};
|
|
8155
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
8156
|
+
if (!name) {
|
|
8157
|
+
res.status(400).json({ error: "Name is required." });
|
|
8158
|
+
return;
|
|
8159
|
+
}
|
|
8160
|
+
const requestedSlug = typeof body.slug === "string" && body.slug.trim() ? body.slug.trim() : slugify(name);
|
|
8161
|
+
if (!requestedSlug || !isValidSlug(requestedSlug)) {
|
|
8162
|
+
res.status(400).json({
|
|
8163
|
+
error: `Slug "${requestedSlug}" is invalid. Slugs must be lowercase letters, numbers, and hyphens only.`
|
|
8164
|
+
});
|
|
8165
|
+
return;
|
|
8166
|
+
}
|
|
8167
|
+
const folderPath = resolve15(projectDir, folder);
|
|
8168
|
+
await ensureDir(folderPath);
|
|
8169
|
+
const filePath = resolve15(folderPath, `${requestedSlug}.md`);
|
|
8170
|
+
const timestamp = nowTimestamp();
|
|
8171
|
+
let content = renderItemStub(kind, {
|
|
8172
|
+
slug: requestedSlug,
|
|
8173
|
+
name,
|
|
8174
|
+
projectSlug: basename2(projectDir),
|
|
8175
|
+
timestamp
|
|
8176
|
+
});
|
|
8177
|
+
const customBody = typeof body.body === "string" ? body.body : "";
|
|
8178
|
+
if (customBody.trim()) {
|
|
8179
|
+
content = replaceStubBody(content, customBody);
|
|
8180
|
+
}
|
|
8181
|
+
try {
|
|
8182
|
+
const handle = await fsOpen(filePath, "wx");
|
|
8183
|
+
try {
|
|
8184
|
+
await handle.writeFile(content, "utf-8");
|
|
8185
|
+
} finally {
|
|
8186
|
+
await handle.close();
|
|
8187
|
+
}
|
|
8188
|
+
} catch (err2) {
|
|
8189
|
+
if (err2.code === "EEXIST") {
|
|
8190
|
+
res.status(409).json({
|
|
8191
|
+
error: `${kind === "memory" ? "Memory" : "Resource"} with slug "${requestedSlug}" already exists in project "${basename2(projectDir)}".`
|
|
8192
|
+
});
|
|
8193
|
+
return;
|
|
8194
|
+
}
|
|
8195
|
+
throw err2;
|
|
8196
|
+
}
|
|
8197
|
+
res.status(201).json({ slug: requestedSlug, projectSlug: basename2(projectDir), content });
|
|
8198
|
+
} catch (error) {
|
|
8199
|
+
console.error(`Error creating ${kind}:`, error);
|
|
8200
|
+
res.status(500).json({ error: `Failed to create ${kind}: ${error.message}` });
|
|
8201
|
+
}
|
|
8202
|
+
});
|
|
8203
|
+
router.patch(`/api/projects/:slug/${folder}/:itemSlug`, async (req, res) => {
|
|
8204
|
+
try {
|
|
8205
|
+
const projectSlug = getParam(req.params.slug);
|
|
8206
|
+
const itemSlug = getParam(req.params.itemSlug);
|
|
8207
|
+
if (rejectBadItemSlug(itemSlug, kind, res)) return;
|
|
8208
|
+
const projectDir = await resolveProjectDir(projectSlug);
|
|
8209
|
+
if (!projectDir) {
|
|
8210
|
+
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
8211
|
+
return;
|
|
8212
|
+
}
|
|
8213
|
+
const filePath = resolve15(projectDir, folder, `${itemSlug}.md`);
|
|
8214
|
+
if (!await fileExists(filePath)) {
|
|
8215
|
+
res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
|
|
8216
|
+
return;
|
|
8217
|
+
}
|
|
8218
|
+
const nextContentRaw = requireContent(req, res);
|
|
8219
|
+
if (!nextContentRaw) return;
|
|
8220
|
+
const currentContent = await readFile11(filePath, "utf-8");
|
|
8221
|
+
const frontmatterBlock = extractFrontmatterBlock(currentContent);
|
|
8222
|
+
if (!frontmatterBlock) {
|
|
8223
|
+
res.status(500).json({ error: `${kind} file is malformed (no frontmatter)` });
|
|
8224
|
+
return;
|
|
8225
|
+
}
|
|
8226
|
+
const next = parseItem(kind, nextContentRaw);
|
|
8227
|
+
const nextBody = next.body.trimStart();
|
|
8228
|
+
let merged = `${frontmatterBlock}
|
|
8229
|
+
${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
8230
|
+
merged = setTopLevelField(merged, "updated", nowTimestamp());
|
|
8231
|
+
await writeFileForce(filePath, merged);
|
|
8232
|
+
const detail = await getItemDetail(kind, basename2(projectDir), itemSlug);
|
|
8233
|
+
res.json({ [kind]: detail, content: merged });
|
|
8234
|
+
} catch (error) {
|
|
8235
|
+
console.error(`Error updating ${kind}:`, error);
|
|
8236
|
+
res.status(500).json({ error: `Failed to update ${kind}: ${error.message}` });
|
|
8237
|
+
}
|
|
8238
|
+
});
|
|
8239
|
+
router.delete(`/api/projects/:slug/${folder}/:itemSlug`, async (req, res) => {
|
|
8240
|
+
try {
|
|
8241
|
+
const projectSlug = getParam(req.params.slug);
|
|
8242
|
+
const itemSlug = getParam(req.params.itemSlug);
|
|
8243
|
+
if (rejectBadItemSlug(itemSlug, kind, res)) return;
|
|
8244
|
+
const projectDir = await resolveProjectDir(projectSlug);
|
|
8245
|
+
if (!projectDir) {
|
|
8246
|
+
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
8247
|
+
return;
|
|
8248
|
+
}
|
|
8249
|
+
const filePath = resolve15(projectDir, folder, `${itemSlug}.md`);
|
|
8250
|
+
if (!await fileExists(filePath)) {
|
|
8251
|
+
res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
|
|
8252
|
+
return;
|
|
8253
|
+
}
|
|
8254
|
+
await rm(filePath);
|
|
8255
|
+
res.status(204).end();
|
|
8256
|
+
} catch (error) {
|
|
8257
|
+
console.error(`Error deleting ${kind}:`, error);
|
|
8258
|
+
res.status(500).json({ error: `Failed to delete ${kind}: ${error.message}` });
|
|
8259
|
+
}
|
|
8260
|
+
});
|
|
8261
|
+
}
|
|
7909
8262
|
router.post("/api/projects", async (req, res) => {
|
|
7910
8263
|
try {
|
|
7911
8264
|
const content = requireContent(req, res);
|
|
@@ -9736,8 +10089,8 @@ function createTodosRouter(todosDir2, broadcast, projectsDir2) {
|
|
|
9736
10089
|
router.post("/:workspace/archive", async (req, res) => {
|
|
9737
10090
|
try {
|
|
9738
10091
|
const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
|
|
9739
|
-
const { resolve:
|
|
9740
|
-
const { readFile:
|
|
10092
|
+
const { resolve: resolve62 } = await import("path");
|
|
10093
|
+
const { readFile: readFile43 } = await import("fs/promises");
|
|
9741
10094
|
const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
9742
10095
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
9743
10096
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
@@ -9753,10 +10106,10 @@ function createTodosRouter(todosDir2, broadcast, projectsDir2) {
|
|
|
9753
10106
|
(e) => e.itemIds.every((id) => completedIds.has(id))
|
|
9754
10107
|
);
|
|
9755
10108
|
const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
|
|
9756
|
-
await ensureDir(
|
|
10109
|
+
await ensureDir(resolve62(todosDir2, "archive"));
|
|
9757
10110
|
let archContent = "";
|
|
9758
10111
|
if (await fileExists(archFile)) {
|
|
9759
|
-
archContent = await
|
|
10112
|
+
archContent = await readFile43(archFile, "utf-8");
|
|
9760
10113
|
archContent = archContent.trimEnd() + "\n\n";
|
|
9761
10114
|
} else {
|
|
9762
10115
|
archContent = `---
|
|
@@ -10015,7 +10368,7 @@ workspace: ${workspace}
|
|
|
10015
10368
|
const { readConfig: readConfig3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
|
|
10016
10369
|
const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
|
|
10017
10370
|
const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
10018
|
-
const { readFile:
|
|
10371
|
+
const { readFile: readFile43 } = await import("fs/promises");
|
|
10019
10372
|
const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
|
|
10020
10373
|
const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
|
|
10021
10374
|
let assignmentRef;
|
|
@@ -10049,11 +10402,11 @@ workspace: ${workspace}
|
|
|
10049
10402
|
} else {
|
|
10050
10403
|
return { error: `Invalid target.assignment "${tg}"` };
|
|
10051
10404
|
}
|
|
10052
|
-
const
|
|
10053
|
-
if (!await fileExists2(
|
|
10405
|
+
const assignmentMdPath3 = resolvePath2(assignmentDir, "assignment.md");
|
|
10406
|
+
if (!await fileExists2(assignmentMdPath3)) return { error: `Target assignment not found: ${assignmentMdPath3}` };
|
|
10054
10407
|
}
|
|
10055
|
-
const
|
|
10056
|
-
let content = await
|
|
10408
|
+
const assignmentMdPath2 = resolvePath2(assignmentDir, "assignment.md");
|
|
10409
|
+
let content = await readFile43(assignmentMdPath2, "utf-8");
|
|
10057
10410
|
content = appendTodosToAssignmentBody2(
|
|
10058
10411
|
content,
|
|
10059
10412
|
items.map((it) => ({
|
|
@@ -10062,7 +10415,7 @@ workspace: ${workspace}
|
|
|
10062
10415
|
}))
|
|
10063
10416
|
);
|
|
10064
10417
|
content = touchAssignmentUpdated2(content, nowTimestamp3());
|
|
10065
|
-
await writeFileForce2(
|
|
10418
|
+
await writeFileForce2(assignmentMdPath2, content);
|
|
10066
10419
|
if (!keepSource) {
|
|
10067
10420
|
for (const item of items) {
|
|
10068
10421
|
item.status = "completed";
|
|
@@ -10902,11 +11255,11 @@ workspace: ${slug}
|
|
|
10902
11255
|
} else {
|
|
10903
11256
|
return { error: `Invalid target.assignment "${tg}"` };
|
|
10904
11257
|
}
|
|
10905
|
-
const
|
|
10906
|
-
if (!await fileExists(
|
|
11258
|
+
const assignmentMdPath3 = resolve19(assignmentDir, "assignment.md");
|
|
11259
|
+
if (!await fileExists(assignmentMdPath3)) return { error: `Target assignment not found: ${assignmentMdPath3}` };
|
|
10907
11260
|
}
|
|
10908
|
-
const
|
|
10909
|
-
let content = await readFile14(
|
|
11261
|
+
const assignmentMdPath2 = resolve19(assignmentDir, "assignment.md");
|
|
11262
|
+
let content = await readFile14(assignmentMdPath2, "utf-8");
|
|
10910
11263
|
content = appendTodosToAssignmentBody2(
|
|
10911
11264
|
content,
|
|
10912
11265
|
items.map((it) => ({
|
|
@@ -10915,7 +11268,7 @@ workspace: ${slug}
|
|
|
10915
11268
|
}))
|
|
10916
11269
|
);
|
|
10917
11270
|
content = touchAssignmentUpdated2(content, nowTimestamp3());
|
|
10918
|
-
await writeFileForce(
|
|
11271
|
+
await writeFileForce(assignmentMdPath2, content);
|
|
10919
11272
|
if (!keepSource) {
|
|
10920
11273
|
for (const item of items) {
|
|
10921
11274
|
item.status = "completed";
|
|
@@ -12077,6 +12430,24 @@ function createDashboardServer(options) {
|
|
|
12077
12430
|
app.use("/api/servers", createServersRouter(serversDir2, projectsDir2, assignmentsDir2));
|
|
12078
12431
|
app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir2, broadcast, assignmentsDir2));
|
|
12079
12432
|
app.use("/api/playbooks", createPlaybooksRouter(playbooksDir3));
|
|
12433
|
+
app.get("/api/memories", async (_req, res) => {
|
|
12434
|
+
try {
|
|
12435
|
+
const memories = await listAllMemories(projectsDir2);
|
|
12436
|
+
res.json({ generatedAt: (/* @__PURE__ */ new Date()).toISOString(), memories });
|
|
12437
|
+
} catch (error) {
|
|
12438
|
+
console.error("Error listing memories:", error);
|
|
12439
|
+
res.status(500).json({ error: `Failed to load memories: ${error.message}` });
|
|
12440
|
+
}
|
|
12441
|
+
});
|
|
12442
|
+
app.get("/api/resources", async (_req, res) => {
|
|
12443
|
+
try {
|
|
12444
|
+
const resources = await listAllResources(projectsDir2);
|
|
12445
|
+
res.json({ generatedAt: (/* @__PURE__ */ new Date()).toISOString(), resources });
|
|
12446
|
+
} catch (error) {
|
|
12447
|
+
console.error("Error listing resources:", error);
|
|
12448
|
+
res.status(500).json({ error: `Failed to load resources: ${error.message}` });
|
|
12449
|
+
}
|
|
12450
|
+
});
|
|
12080
12451
|
app.use("/api/todos", createTodosRouter(todosDir2, broadcast, projectsDir2));
|
|
12081
12452
|
app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir2, broadcast, todosDir2));
|
|
12082
12453
|
app.use("/api/backup", createBackupRouter());
|
|
@@ -12454,7 +12825,7 @@ import {
|
|
|
12454
12825
|
} from "fs/promises";
|
|
12455
12826
|
import { existsSync } from "fs";
|
|
12456
12827
|
import { homedir as homedir2 } from "os";
|
|
12457
|
-
import { basename, dirname as dirname8, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve25 } from "path";
|
|
12828
|
+
import { basename as basename3, dirname as dirname8, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve25 } from "path";
|
|
12458
12829
|
|
|
12459
12830
|
// src/utils/package-root.ts
|
|
12460
12831
|
init_fs();
|
|
@@ -12916,7 +13287,7 @@ async function ensureClaudeUserMarketplace() {
|
|
|
12916
13287
|
async function detectClaudeMarketplaceForTarget(targetDir) {
|
|
12917
13288
|
const normalizedTargetDir = normalizeAbsoluteInstallPath(targetDir, "Claude plugin target");
|
|
12918
13289
|
const pluginsDir = dirname8(normalizedTargetDir);
|
|
12919
|
-
if (
|
|
13290
|
+
if (basename3(pluginsDir) !== "plugins") {
|
|
12920
13291
|
return null;
|
|
12921
13292
|
}
|
|
12922
13293
|
const rootDir = dirname8(pluginsDir);
|
|
@@ -12925,7 +13296,7 @@ async function detectClaudeMarketplaceForTarget(targetDir) {
|
|
|
12925
13296
|
return null;
|
|
12926
13297
|
}
|
|
12927
13298
|
const marketplace = await readClaudeMarketplaceFile(manifestPath);
|
|
12928
|
-
const name = typeof marketplace.name === "string" && marketplace.name.trim() !== "" ? marketplace.name :
|
|
13299
|
+
const name = typeof marketplace.name === "string" && marketplace.name.trim() !== "" ? marketplace.name : basename3(rootDir);
|
|
12929
13300
|
return {
|
|
12930
13301
|
name,
|
|
12931
13302
|
rootDir,
|
|
@@ -13396,7 +13767,16 @@ var KNOWN_SKILL_NAMES = [
|
|
|
13396
13767
|
"clear-assignment",
|
|
13397
13768
|
"save-session-summary",
|
|
13398
13769
|
"track-session",
|
|
13399
|
-
"track-server"
|
|
13770
|
+
"track-server",
|
|
13771
|
+
"capture-artifacts",
|
|
13772
|
+
"replan",
|
|
13773
|
+
"resume-session",
|
|
13774
|
+
"syntaur-worktree",
|
|
13775
|
+
"add-resource",
|
|
13776
|
+
"add-memory",
|
|
13777
|
+
"list-assignments",
|
|
13778
|
+
"log-progress",
|
|
13779
|
+
"set-workspace"
|
|
13400
13780
|
];
|
|
13401
13781
|
var KNOWN_SKILLS = KNOWN_SKILL_NAMES;
|
|
13402
13782
|
async function getSkillsDir() {
|
|
@@ -13459,8 +13839,8 @@ async function skillMatches(srcDir, destDir) {
|
|
|
13459
13839
|
}
|
|
13460
13840
|
async function isSymlink(path) {
|
|
13461
13841
|
try {
|
|
13462
|
-
const
|
|
13463
|
-
return
|
|
13842
|
+
const stat9 = await lstat2(path);
|
|
13843
|
+
return stat9.isSymbolicLink();
|
|
13464
13844
|
} catch {
|
|
13465
13845
|
return false;
|
|
13466
13846
|
}
|
|
@@ -13728,7 +14108,7 @@ async function installPluginCommand(options) {
|
|
|
13728
14108
|
}
|
|
13729
14109
|
}
|
|
13730
14110
|
console.log("\nThe plugin is now available in Claude Code.");
|
|
13731
|
-
console.log(" Slash commands: /grab-assignment, /plan-assignment, /complete-assignment, /create-assignment, /create-project, /track-session, /clear-assignment, /manage-statuses");
|
|
14111
|
+
console.log(" Slash commands: /grab-assignment, /plan-assignment, /complete-assignment, /create-assignment, /create-project, /track-session, /clear-assignment, /manage-statuses, /replan, /resume-session, /syntaur-worktree, /add-resource, /add-memory, /list-assignments, /log-progress, /set-workspace");
|
|
13732
14112
|
console.log(" Background: syntaur-protocol skill (auto-invoked)");
|
|
13733
14113
|
console.log(" Hook: write boundary enforcement (PreToolUse) + SessionStart/End");
|
|
13734
14114
|
}
|
|
@@ -14281,10 +14661,10 @@ async function installCodexPluginCommand(options) {
|
|
|
14281
14661
|
}
|
|
14282
14662
|
console.log("\nThe plugin is now available to Codex.");
|
|
14283
14663
|
console.log(
|
|
14284
|
-
" Protocol skills: syntaur-protocol, create-project, create-assignment, grab-assignment, plan-assignment, complete-assignment, save-session-summary"
|
|
14664
|
+
" Protocol skills: syntaur-protocol, create-project, create-assignment, grab-assignment, plan-assignment, complete-assignment, save-session-summary, manage-statuses, clear-assignment, replan, resume-session, syntaur-worktree, add-resource, add-memory, list-assignments, log-progress, set-workspace"
|
|
14285
14665
|
);
|
|
14286
14666
|
console.log(" Codex-specific: track-session skill (rollout path aware)");
|
|
14287
|
-
console.log(" Slash commands: /track-session, /save-session-summary (no PreCompact hook on Codex \u2014 invoke manually before compaction)");
|
|
14667
|
+
console.log(" Slash commands: /track-session, /save-session-summary, /replan, /resume-session, /syntaur-worktree, /add-resource, /add-memory, /list-assignments, /log-progress, /set-workspace (no PreCompact hook on Codex \u2014 invoke save-session-summary manually before compaction)");
|
|
14288
14668
|
console.log(" Hooks: write boundary enforcement, session cleanup");
|
|
14289
14669
|
}
|
|
14290
14670
|
|
|
@@ -14562,8 +14942,8 @@ async function setupAdapterCommand(framework, options) {
|
|
|
14562
14942
|
`Project "${options.project}" not found at ${projectDir}.`
|
|
14563
14943
|
);
|
|
14564
14944
|
}
|
|
14565
|
-
const
|
|
14566
|
-
if (!await fileExists(assignmentDir) || !await fileExists(
|
|
14945
|
+
const assignmentMdPath2 = resolve31(assignmentDir, "assignment.md");
|
|
14946
|
+
if (!await fileExists(assignmentDir) || !await fileExists(assignmentMdPath2)) {
|
|
14567
14947
|
throw new Error(
|
|
14568
14948
|
`Assignment "${options.assignment}" not found at ${assignmentDir}.`
|
|
14569
14949
|
);
|
|
@@ -14821,7 +15201,7 @@ function computeWorktreeDefaults(opts) {
|
|
|
14821
15201
|
const repository = opts.existing.repository ?? detectCurrentGitRoot();
|
|
14822
15202
|
const branch = opts.projectSlug ? `syntaur/${opts.projectSlug}/${opts.assignmentSlug}` : `syntaur/${opts.assignmentSlug}`;
|
|
14823
15203
|
const parentBranch = opts.existing.parentBranch ?? detectCurrentBranch() ?? "main";
|
|
14824
|
-
const worktreeBase = resolve34(
|
|
15204
|
+
const worktreeBase = repository ? resolve34(repository, ".worktrees", branch) : resolve34(
|
|
14825
15205
|
syntaurRoot(),
|
|
14826
15206
|
"worktrees",
|
|
14827
15207
|
opts.projectSlug || "standalone",
|
|
@@ -15524,9 +15904,9 @@ async function promoteTodos(ids, options) {
|
|
|
15524
15904
|
throw new Error(`Invalid --to-assignment target "${target}". Use <project>/<slug> or a bare UUID.`);
|
|
15525
15905
|
}
|
|
15526
15906
|
const { fileExists: fileExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
15527
|
-
const
|
|
15528
|
-
if (!await fileExists2(
|
|
15529
|
-
throw new Error(`Target assignment not found: ${
|
|
15907
|
+
const assignmentMdPath2 = resolvePath2(assignmentDir, "assignment.md");
|
|
15908
|
+
if (!await fileExists2(assignmentMdPath2)) {
|
|
15909
|
+
throw new Error(`Target assignment not found: ${assignmentMdPath2}`);
|
|
15530
15910
|
}
|
|
15531
15911
|
await injectPromotedTodos(assignmentDir, todos, scopeLabel);
|
|
15532
15912
|
if (!options.keepSource) {
|
|
@@ -15541,12 +15921,12 @@ function describeScope(scope) {
|
|
|
15541
15921
|
}
|
|
15542
15922
|
async function injectPromotedTodos(assignmentDir, todos, scopeLabel) {
|
|
15543
15923
|
const { resolve: resolvePath2 } = await import("path");
|
|
15544
|
-
const { readFile:
|
|
15924
|
+
const { readFile: readFile43 } = await import("fs/promises");
|
|
15545
15925
|
const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
15546
15926
|
const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
|
|
15547
15927
|
const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
|
|
15548
|
-
const
|
|
15549
|
-
let content = await
|
|
15928
|
+
const assignmentMdPath2 = resolvePath2(assignmentDir, "assignment.md");
|
|
15929
|
+
let content = await readFile43(assignmentMdPath2, "utf-8");
|
|
15550
15930
|
content = appendTodosToAssignmentBody2(
|
|
15551
15931
|
content,
|
|
15552
15932
|
todos.map((t) => ({
|
|
@@ -15555,7 +15935,7 @@ async function injectPromotedTodos(assignmentDir, todos, scopeLabel) {
|
|
|
15555
15935
|
}))
|
|
15556
15936
|
);
|
|
15557
15937
|
content = touchAssignmentUpdated2(content, nowTimestamp3());
|
|
15558
|
-
await writeFileForce2(
|
|
15938
|
+
await writeFileForce2(assignmentMdPath2, content);
|
|
15559
15939
|
}
|
|
15560
15940
|
async function markPromotedComplete(todos, todosPath, workspace, checklist, _scope, summary) {
|
|
15561
15941
|
for (const item of todos) {
|
|
@@ -15591,8 +15971,8 @@ todoCommand.command("plan").description("Create or open a plan directory for a t
|
|
|
15591
15971
|
}
|
|
15592
15972
|
const planDir = todoPlanDir(todosPath, workspace, id);
|
|
15593
15973
|
await ensureDir(planDir);
|
|
15594
|
-
const { readdir:
|
|
15595
|
-
const existingFiles = (await
|
|
15974
|
+
const { readdir: readdir22 } = await import("fs/promises");
|
|
15975
|
+
const existingFiles = (await readdir22(planDir).catch(() => [])).filter(
|
|
15596
15976
|
(f) => /^plan(?:-v\d+)?\.md$/.test(f)
|
|
15597
15977
|
);
|
|
15598
15978
|
let target;
|
|
@@ -15677,10 +16057,10 @@ async function moveTodo(id, options) {
|
|
|
15677
16057
|
if (await fileExists(newPlanDir)) {
|
|
15678
16058
|
throw new Error(`Plan directory already exists at target: ${newPlanDir}; refusing to move.`);
|
|
15679
16059
|
}
|
|
15680
|
-
const { rename:
|
|
16060
|
+
const { rename: rename8, mkdir: mkdir8 } = await import("fs/promises");
|
|
15681
16061
|
const { dirname: dirname16 } = await import("path");
|
|
15682
|
-
await
|
|
15683
|
-
await
|
|
16062
|
+
await mkdir8(dirname16(newPlanDir), { recursive: true });
|
|
16063
|
+
await rename8(item.planDir, newPlanDir);
|
|
15684
16064
|
item.planDir = newPlanDir;
|
|
15685
16065
|
}
|
|
15686
16066
|
sourceChecklist.items.splice(idx, 1);
|
|
@@ -15796,6 +16176,8 @@ backupCommand.command("config").description("Show or update backup configuration
|
|
|
15796
16176
|
|
|
15797
16177
|
// src/commands/doctor.ts
|
|
15798
16178
|
import { Command as Command3 } from "commander";
|
|
16179
|
+
import { readFile as readFile31 } from "fs/promises";
|
|
16180
|
+
import { isAbsolute as isAbsolute7, resolve as resolve47 } from "path";
|
|
15799
16181
|
|
|
15800
16182
|
// src/utils/doctor/index.ts
|
|
15801
16183
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
@@ -15812,14 +16194,14 @@ async function buildCheckContext(cwd = process.cwd()) {
|
|
|
15812
16194
|
const config = await readConfig();
|
|
15813
16195
|
const root = syntaurRoot();
|
|
15814
16196
|
const dbPath = resolve38(root, "syntaur.db");
|
|
15815
|
-
let
|
|
16197
|
+
let db3 = null;
|
|
15816
16198
|
let dbError = null;
|
|
15817
16199
|
if (await fileExists(dbPath)) {
|
|
15818
16200
|
try {
|
|
15819
|
-
|
|
16201
|
+
db3 = new Database2(dbPath, { readonly: true, fileMustExist: true });
|
|
15820
16202
|
} catch (err2) {
|
|
15821
16203
|
dbError = err2 instanceof Error ? err2.message : String(err2);
|
|
15822
|
-
|
|
16204
|
+
db3 = null;
|
|
15823
16205
|
}
|
|
15824
16206
|
} else {
|
|
15825
16207
|
dbError = `database file not found at ${dbPath}`;
|
|
@@ -15827,7 +16209,7 @@ async function buildCheckContext(cwd = process.cwd()) {
|
|
|
15827
16209
|
return {
|
|
15828
16210
|
config,
|
|
15829
16211
|
syntaurRoot: root,
|
|
15830
|
-
db:
|
|
16212
|
+
db: db3,
|
|
15831
16213
|
dbError,
|
|
15832
16214
|
cwd,
|
|
15833
16215
|
now: /* @__PURE__ */ new Date()
|
|
@@ -17121,7 +17503,7 @@ function skipped(check, reason) {
|
|
|
17121
17503
|
|
|
17122
17504
|
// src/utils/doctor/checks/integrations.ts
|
|
17123
17505
|
init_fs();
|
|
17124
|
-
import { resolve as resolve44, dirname as dirname12, basename as
|
|
17506
|
+
import { resolve as resolve44, dirname as dirname12, basename as basename4 } from "path";
|
|
17125
17507
|
import { readdir as readdir16, readFile as readFile27 } from "fs/promises";
|
|
17126
17508
|
import { homedir as homedir6 } from "os";
|
|
17127
17509
|
var CATEGORY6 = "integrations";
|
|
@@ -17224,7 +17606,7 @@ var claudeMarketplaceRegistered = {
|
|
|
17224
17606
|
return skipped2(this, "claudePluginDir does not exist (run install-plugin)");
|
|
17225
17607
|
}
|
|
17226
17608
|
const pluginsParent = dirname12(dir);
|
|
17227
|
-
if (
|
|
17609
|
+
if (basename4(pluginsParent) !== "plugins") {
|
|
17228
17610
|
return {
|
|
17229
17611
|
id: this.id,
|
|
17230
17612
|
category: this.category,
|
|
@@ -17272,7 +17654,7 @@ var claudeMarketplaceRegistered = {
|
|
|
17272
17654
|
autoFixable: false
|
|
17273
17655
|
};
|
|
17274
17656
|
}
|
|
17275
|
-
const marketplaceName = parsed.name ??
|
|
17657
|
+
const marketplaceName = parsed.name ?? basename4(marketplaceRoot);
|
|
17276
17658
|
const hasSyntaurEntry = (parsed.plugins ?? []).some((p) => p?.name === "syntaur");
|
|
17277
17659
|
const known = await readKnownMarketplaces();
|
|
17278
17660
|
const registered = known[marketplaceName]?.installLocation === marketplaceRoot || Object.values(known).some((v) => v.installLocation === marketplaceRoot);
|
|
@@ -17865,7 +18247,93 @@ function renderJson(report) {
|
|
|
17865
18247
|
// src/commands/doctor.ts
|
|
17866
18248
|
init_fs();
|
|
17867
18249
|
init_paths();
|
|
17868
|
-
|
|
18250
|
+
init_frontmatter();
|
|
18251
|
+
var REQUIRED_WORKSPACE_FIELDS = [
|
|
18252
|
+
"repository",
|
|
18253
|
+
"worktreePath",
|
|
18254
|
+
"branch",
|
|
18255
|
+
"parentBranch"
|
|
18256
|
+
];
|
|
18257
|
+
var ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
|
|
18258
|
+
async function validateAssignmentFile(inputPath, cwd = process.cwd()) {
|
|
18259
|
+
const absolute = isAbsolute7(inputPath) ? inputPath : resolve47(cwd, inputPath);
|
|
18260
|
+
const errors = [];
|
|
18261
|
+
const warnings = [];
|
|
18262
|
+
if (!await fileExists(absolute)) {
|
|
18263
|
+
return {
|
|
18264
|
+
ok: false,
|
|
18265
|
+
path: absolute,
|
|
18266
|
+
errors: [`File does not exist: ${absolute}`],
|
|
18267
|
+
warnings: []
|
|
18268
|
+
};
|
|
18269
|
+
}
|
|
18270
|
+
let content;
|
|
18271
|
+
try {
|
|
18272
|
+
content = await readFile31(absolute, "utf-8");
|
|
18273
|
+
} catch (err2) {
|
|
18274
|
+
return {
|
|
18275
|
+
ok: false,
|
|
18276
|
+
path: absolute,
|
|
18277
|
+
errors: [
|
|
18278
|
+
`Could not read file: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
18279
|
+
],
|
|
18280
|
+
warnings: []
|
|
18281
|
+
};
|
|
18282
|
+
}
|
|
18283
|
+
if (!content.startsWith("---")) {
|
|
18284
|
+
errors.push("Missing YAML frontmatter \u2014 file does not start with `---`.");
|
|
18285
|
+
}
|
|
18286
|
+
let parsed;
|
|
18287
|
+
try {
|
|
18288
|
+
parsed = parseAssignmentFrontmatter(content);
|
|
18289
|
+
} catch (err2) {
|
|
18290
|
+
errors.push(
|
|
18291
|
+
`Frontmatter parse error: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
18292
|
+
);
|
|
18293
|
+
return { ok: false, path: absolute, errors, warnings };
|
|
18294
|
+
}
|
|
18295
|
+
if (!parsed.id) errors.push("Missing or empty `id` field.");
|
|
18296
|
+
if (!parsed.slug) errors.push("Missing or empty `slug` field.");
|
|
18297
|
+
if (!parsed.title) errors.push("Missing or empty `title` field.");
|
|
18298
|
+
if (!parsed.status) errors.push("Missing or empty `status` field.");
|
|
18299
|
+
if (!parsed.created) errors.push("Missing or empty `created` field.");
|
|
18300
|
+
else if (!ISO_DATE.test(parsed.created))
|
|
18301
|
+
warnings.push(`Field \`created\` is not ISO 8601: "${parsed.created}".`);
|
|
18302
|
+
if (!parsed.updated) errors.push("Missing or empty `updated` field.");
|
|
18303
|
+
else if (!ISO_DATE.test(parsed.updated))
|
|
18304
|
+
warnings.push(`Field \`updated\` is not ISO 8601: "${parsed.updated}".`);
|
|
18305
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
18306
|
+
const fmText = fmMatch ? fmMatch[1] : "";
|
|
18307
|
+
for (const key of REQUIRED_WORKSPACE_FIELDS) {
|
|
18308
|
+
if (!new RegExp(`^\\s+${key}:`, "m").test(fmText)) {
|
|
18309
|
+
errors.push(`Workspace block is missing required field \`${key}\`.`);
|
|
18310
|
+
}
|
|
18311
|
+
}
|
|
18312
|
+
return { ok: errors.length === 0, path: absolute, errors, warnings };
|
|
18313
|
+
}
|
|
18314
|
+
var doctorCommand = new Command3("doctor").description("Diagnose Syntaur state and surface issues with suggested fixes").option("--json", "Emit structured JSON for programmatic consumers").option("--fix", "Apply safe auto-fixes (v1: no-op; reserved for future checks)").option("--only <check-id>", "Run only the check with this ID").option("--verbose", "Include passing checks in human output").option(
|
|
18315
|
+
"--assignment <path>",
|
|
18316
|
+
"Validate a single assignment.md frontmatter and exit (used by set-workspace skill pre-write)"
|
|
18317
|
+
).action(async (options) => {
|
|
18318
|
+
if (options.assignment) {
|
|
18319
|
+
const result = await validateAssignmentFile(options.assignment);
|
|
18320
|
+
if (options.json) {
|
|
18321
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
18322
|
+
} else if (result.ok) {
|
|
18323
|
+
process.stdout.write(`OK ${result.path}
|
|
18324
|
+
`);
|
|
18325
|
+
for (const w of result.warnings) process.stdout.write(` warn: ${w}
|
|
18326
|
+
`);
|
|
18327
|
+
} else {
|
|
18328
|
+
process.stderr.write(`FAIL ${result.path}
|
|
18329
|
+
`);
|
|
18330
|
+
for (const e of result.errors) process.stderr.write(` error: ${e}
|
|
18331
|
+
`);
|
|
18332
|
+
for (const w of result.warnings) process.stderr.write(` warn: ${w}
|
|
18333
|
+
`);
|
|
18334
|
+
}
|
|
18335
|
+
process.exit(result.ok ? 0 : 1);
|
|
18336
|
+
}
|
|
17869
18337
|
if (!await fileExists(syntaurRoot())) {
|
|
17870
18338
|
const msg = "~/.syntaur/ does not exist. Run `syntaur init` first.";
|
|
17871
18339
|
if (options.json) {
|
|
@@ -18125,8 +18593,8 @@ init_uuid();
|
|
|
18125
18593
|
init_timestamp();
|
|
18126
18594
|
init_assignment_resolver();
|
|
18127
18595
|
init_templates();
|
|
18128
|
-
import { resolve as
|
|
18129
|
-
import { readFile as
|
|
18596
|
+
import { resolve as resolve48 } from "path";
|
|
18597
|
+
import { readFile as readFile32 } from "fs/promises";
|
|
18130
18598
|
function shortId() {
|
|
18131
18599
|
return generateId().split("-")[0];
|
|
18132
18600
|
}
|
|
@@ -18156,7 +18624,7 @@ async function commentCommand(target, text, options = {}) {
|
|
|
18156
18624
|
if (!isValidSlug(target)) {
|
|
18157
18625
|
throw new Error(`Invalid assignment slug "${target}".`);
|
|
18158
18626
|
}
|
|
18159
|
-
assignmentDir =
|
|
18627
|
+
assignmentDir = resolve48(baseDir, options.project, "assignments", target);
|
|
18160
18628
|
assignmentRef = target;
|
|
18161
18629
|
} else {
|
|
18162
18630
|
const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), target);
|
|
@@ -18166,13 +18634,13 @@ async function commentCommand(target, text, options = {}) {
|
|
|
18166
18634
|
assignmentDir = resolved.assignmentDir;
|
|
18167
18635
|
assignmentRef = resolved.standalone ? resolved.id : resolved.assignmentSlug;
|
|
18168
18636
|
}
|
|
18169
|
-
const commentsPath =
|
|
18637
|
+
const commentsPath = resolve48(assignmentDir, "comments.md");
|
|
18170
18638
|
const timestamp = nowTimestamp();
|
|
18171
18639
|
const author = options.author ?? process.env.USER ?? "unknown";
|
|
18172
18640
|
let currentContent;
|
|
18173
18641
|
let currentCount = 0;
|
|
18174
18642
|
if (await fileExists(commentsPath)) {
|
|
18175
|
-
currentContent = await
|
|
18643
|
+
currentContent = await readFile32(commentsPath, "utf-8");
|
|
18176
18644
|
const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
|
|
18177
18645
|
if (countMatch) currentCount = parseInt(countMatch[1], 10);
|
|
18178
18646
|
} else {
|
|
@@ -18205,77 +18673,1578 @@ ${entry}`;
|
|
|
18205
18673
|
}
|
|
18206
18674
|
}
|
|
18207
18675
|
|
|
18208
|
-
// src/commands/
|
|
18676
|
+
// src/commands/capture.ts
|
|
18677
|
+
import { resolve as resolve51, relative as relative4 } from "path";
|
|
18678
|
+
import { copyFile as copyFile3, mkdir as mkdir7, realpath as realpath2, stat as stat6 } from "fs/promises";
|
|
18679
|
+
|
|
18680
|
+
// src/utils/assignment-target.ts
|
|
18209
18681
|
init_paths();
|
|
18210
18682
|
init_fs();
|
|
18211
18683
|
init_config2();
|
|
18212
18684
|
init_slug();
|
|
18213
|
-
init_timestamp();
|
|
18214
18685
|
init_assignment_resolver();
|
|
18215
|
-
|
|
18216
|
-
import { resolve as
|
|
18217
|
-
import { readFile as
|
|
18218
|
-
|
|
18219
|
-
|
|
18220
|
-
|
|
18221
|
-
|
|
18222
|
-
|
|
18223
|
-
|
|
18224
|
-
|
|
18225
|
-
|
|
18226
|
-
|
|
18227
|
-
|
|
18228
|
-
|
|
18229
|
-
}
|
|
18230
|
-
if (!isValidSlug(target)) {
|
|
18231
|
-
throw new Error(`Invalid assignment slug "${target}".`);
|
|
18232
|
-
}
|
|
18233
|
-
assignmentDir = resolve48(baseDir, options.project, "assignments", target);
|
|
18234
|
-
targetRef = target;
|
|
18235
|
-
} else {
|
|
18236
|
-
const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), target);
|
|
18237
|
-
if (!resolved) {
|
|
18238
|
-
throw new Error(`Target assignment "${target}" not found. Provide --project <slug> or a valid UUID.`);
|
|
18239
|
-
}
|
|
18240
|
-
assignmentDir = resolved.assignmentDir;
|
|
18241
|
-
targetRef = resolved.standalone ? resolved.id : resolved.assignmentSlug;
|
|
18242
|
-
}
|
|
18243
|
-
const assignmentMdPath = resolve48(assignmentDir, "assignment.md");
|
|
18244
|
-
if (!await fileExists(assignmentMdPath)) {
|
|
18245
|
-
throw new Error(`assignment.md not found at ${assignmentMdPath}`);
|
|
18686
|
+
init_parser();
|
|
18687
|
+
import { resolve as resolve49 } from "path";
|
|
18688
|
+
import { readFile as readFile33 } from "fs/promises";
|
|
18689
|
+
var AssignmentTargetError = class extends Error {
|
|
18690
|
+
};
|
|
18691
|
+
async function readAssignmentFrontmatterId(assignmentDir) {
|
|
18692
|
+
const path = resolve49(assignmentDir, "assignment.md");
|
|
18693
|
+
if (!await fileExists(path)) return null;
|
|
18694
|
+
try {
|
|
18695
|
+
const content = await readFile33(path, "utf-8");
|
|
18696
|
+
const [fm] = extractFrontmatter(content);
|
|
18697
|
+
return getField(fm, "id");
|
|
18698
|
+
} catch {
|
|
18699
|
+
return null;
|
|
18246
18700
|
}
|
|
18247
|
-
const source = options.from ?? process.env.SYNTAUR_ASSIGNMENT ?? "unknown";
|
|
18248
|
-
let content = await readFile32(assignmentMdPath, "utf-8");
|
|
18249
|
-
content = appendTodosToAssignmentBody(content, [
|
|
18250
|
-
{ description: `${text.trim()} (from: ${source})` }
|
|
18251
|
-
]);
|
|
18252
|
-
content = touchAssignmentUpdated(content, nowTimestamp());
|
|
18253
|
-
await writeFileForce(assignmentMdPath, content);
|
|
18254
|
-
console.log(`Added todo to ${targetRef}: ${text.trim()} (from: ${source})`);
|
|
18255
18701
|
}
|
|
18256
|
-
|
|
18257
|
-
|
|
18258
|
-
|
|
18259
|
-
import { readdir as readdir18 } from "fs/promises";
|
|
18260
|
-
async function hasAnyProjectContent(projectsDir2) {
|
|
18702
|
+
async function readContextJson(cwd) {
|
|
18703
|
+
const path = resolve49(cwd, ".syntaur", "context.json");
|
|
18704
|
+
if (!await fileExists(path)) return null;
|
|
18261
18705
|
try {
|
|
18262
|
-
const
|
|
18263
|
-
return
|
|
18706
|
+
const raw = await readFile33(path, "utf-8");
|
|
18707
|
+
return JSON.parse(raw);
|
|
18264
18708
|
} catch {
|
|
18265
|
-
return
|
|
18709
|
+
return null;
|
|
18266
18710
|
}
|
|
18267
18711
|
}
|
|
18268
|
-
async function
|
|
18712
|
+
async function resolveAssignmentTarget(input4, opts = {}) {
|
|
18269
18713
|
const config = await readConfig();
|
|
18270
|
-
|
|
18271
|
-
|
|
18714
|
+
const baseDir = opts.dir ? expandHome(opts.dir) : config.defaultProjectDir;
|
|
18715
|
+
if (opts.project) {
|
|
18716
|
+
if (!input4) {
|
|
18717
|
+
throw new AssignmentTargetError(
|
|
18718
|
+
"--project requires an assignment slug as a positional argument."
|
|
18719
|
+
);
|
|
18720
|
+
}
|
|
18721
|
+
if (!isValidSlug(opts.project)) {
|
|
18722
|
+
throw new AssignmentTargetError(`Invalid project slug "${opts.project}".`);
|
|
18723
|
+
}
|
|
18724
|
+
if (!isValidSlug(input4)) {
|
|
18725
|
+
throw new AssignmentTargetError(`Invalid assignment slug "${input4}".`);
|
|
18726
|
+
}
|
|
18727
|
+
const projectDir = resolve49(baseDir, opts.project);
|
|
18728
|
+
const projectMdPath = resolve49(projectDir, "project.md");
|
|
18729
|
+
if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
|
|
18730
|
+
throw new AssignmentTargetError(
|
|
18731
|
+
`Project "${opts.project}" not found at ${projectDir}.`
|
|
18732
|
+
);
|
|
18733
|
+
}
|
|
18734
|
+
const assignmentDir = resolve49(projectDir, "assignments", input4);
|
|
18735
|
+
const assignmentMdPath2 = resolve49(assignmentDir, "assignment.md");
|
|
18736
|
+
if (!await fileExists(assignmentMdPath2)) {
|
|
18737
|
+
throw new AssignmentTargetError(
|
|
18738
|
+
`Assignment "${input4}" not found in project "${opts.project}".`
|
|
18739
|
+
);
|
|
18740
|
+
}
|
|
18741
|
+
const id = await readAssignmentFrontmatterId(assignmentDir) ?? input4;
|
|
18742
|
+
return {
|
|
18743
|
+
assignmentDir,
|
|
18744
|
+
projectSlug: opts.project,
|
|
18745
|
+
assignmentSlug: input4,
|
|
18746
|
+
id,
|
|
18747
|
+
standalone: false,
|
|
18748
|
+
workspaceGroup: null
|
|
18749
|
+
};
|
|
18272
18750
|
}
|
|
18273
|
-
|
|
18274
|
-
|
|
18275
|
-
|
|
18276
|
-
|
|
18277
|
-
|
|
18278
|
-
|
|
18751
|
+
if (input4) {
|
|
18752
|
+
const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), input4);
|
|
18753
|
+
if (!resolved) {
|
|
18754
|
+
throw new AssignmentTargetError(
|
|
18755
|
+
`Assignment "${input4}" not found. Provide --project <slug> + <slug> or a valid standalone UUID.`
|
|
18756
|
+
);
|
|
18757
|
+
}
|
|
18758
|
+
return resolved;
|
|
18759
|
+
}
|
|
18760
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
18761
|
+
const ctx = await readContextJson(cwd);
|
|
18762
|
+
if (!ctx) {
|
|
18763
|
+
throw new AssignmentTargetError(
|
|
18764
|
+
"No assignment specified. Provide an argument, --project + slug, or run from a directory with .syntaur/context.json."
|
|
18765
|
+
);
|
|
18766
|
+
}
|
|
18767
|
+
if (ctx.assignmentDir) {
|
|
18768
|
+
const dir = expandHome(ctx.assignmentDir);
|
|
18769
|
+
const assignmentMdPath2 = resolve49(dir, "assignment.md");
|
|
18770
|
+
if (!await fileExists(assignmentMdPath2)) {
|
|
18771
|
+
throw new AssignmentTargetError(
|
|
18772
|
+
`.syntaur/context.json points to a missing assignment dir: ${dir}.`
|
|
18773
|
+
);
|
|
18774
|
+
}
|
|
18775
|
+
const id = await readAssignmentFrontmatterId(dir);
|
|
18776
|
+
if (!id || id.trim() === "") {
|
|
18777
|
+
throw new AssignmentTargetError(
|
|
18778
|
+
`.syntaur/context.json points to an assignment with no frontmatter \`id\`: ${dir}.`
|
|
18779
|
+
);
|
|
18780
|
+
}
|
|
18781
|
+
const assignmentSlug = ctx.assignmentSlug ?? dir.split("/").pop() ?? "";
|
|
18782
|
+
const projectSlug = ctx.projectSlug ?? null;
|
|
18783
|
+
return {
|
|
18784
|
+
assignmentDir: dir,
|
|
18785
|
+
projectSlug,
|
|
18786
|
+
assignmentSlug,
|
|
18787
|
+
id,
|
|
18788
|
+
standalone: projectSlug === null,
|
|
18789
|
+
workspaceGroup: null
|
|
18790
|
+
};
|
|
18791
|
+
}
|
|
18792
|
+
if (ctx.projectSlug && ctx.assignmentSlug) {
|
|
18793
|
+
if (!isValidSlug(ctx.projectSlug) || !isValidSlug(ctx.assignmentSlug)) {
|
|
18794
|
+
throw new AssignmentTargetError(
|
|
18795
|
+
`.syntaur/context.json contains invalid slugs: project="${ctx.projectSlug}" assignment="${ctx.assignmentSlug}".`
|
|
18796
|
+
);
|
|
18797
|
+
}
|
|
18798
|
+
const assignmentDir = resolve49(baseDir, ctx.projectSlug, "assignments", ctx.assignmentSlug);
|
|
18799
|
+
const assignmentMdPath2 = resolve49(assignmentDir, "assignment.md");
|
|
18800
|
+
if (!await fileExists(assignmentMdPath2)) {
|
|
18801
|
+
throw new AssignmentTargetError(
|
|
18802
|
+
`.syntaur/context.json points to a missing assignment: ${assignmentDir}.`
|
|
18803
|
+
);
|
|
18804
|
+
}
|
|
18805
|
+
const id = await readAssignmentFrontmatterId(assignmentDir) ?? ctx.assignmentSlug;
|
|
18806
|
+
return {
|
|
18807
|
+
assignmentDir,
|
|
18808
|
+
projectSlug: ctx.projectSlug,
|
|
18809
|
+
assignmentSlug: ctx.assignmentSlug,
|
|
18810
|
+
id,
|
|
18811
|
+
standalone: false,
|
|
18812
|
+
workspaceGroup: null
|
|
18813
|
+
};
|
|
18814
|
+
}
|
|
18815
|
+
throw new AssignmentTargetError(
|
|
18816
|
+
".syntaur/context.json exists but contains neither assignmentDir nor projectSlug+assignmentSlug."
|
|
18817
|
+
);
|
|
18818
|
+
}
|
|
18819
|
+
|
|
18820
|
+
// src/commands/capture.ts
|
|
18821
|
+
init_paths();
|
|
18822
|
+
|
|
18823
|
+
// src/utils/proof-artifact-id.ts
|
|
18824
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
18825
|
+
function generateArtifactId() {
|
|
18826
|
+
const ts = Date.now().toString(36);
|
|
18827
|
+
const rand = randomBytes2(2).toString("hex");
|
|
18828
|
+
return `${ts}-${rand}`;
|
|
18829
|
+
}
|
|
18830
|
+
var EXTENSIONS = {
|
|
18831
|
+
screenshot: "png",
|
|
18832
|
+
video: "mp4",
|
|
18833
|
+
asciinema: "cast",
|
|
18834
|
+
http: "txt",
|
|
18835
|
+
text: "txt"
|
|
18836
|
+
};
|
|
18837
|
+
function extensionForKind(kind) {
|
|
18838
|
+
const ext = EXTENSIONS[kind];
|
|
18839
|
+
if (!ext) {
|
|
18840
|
+
throw new Error(`Unknown artifact kind: ${kind}`);
|
|
18841
|
+
}
|
|
18842
|
+
return ext;
|
|
18843
|
+
}
|
|
18844
|
+
var ARTIFACT_KINDS = [
|
|
18845
|
+
"screenshot",
|
|
18846
|
+
"video",
|
|
18847
|
+
"asciinema",
|
|
18848
|
+
"http",
|
|
18849
|
+
"text"
|
|
18850
|
+
];
|
|
18851
|
+
function isArtifactKind(value) {
|
|
18852
|
+
return typeof value === "string" && ARTIFACT_KINDS.includes(value);
|
|
18853
|
+
}
|
|
18854
|
+
|
|
18855
|
+
// src/commands/capture.ts
|
|
18856
|
+
init_fs();
|
|
18857
|
+
|
|
18858
|
+
// src/db/proof-db.ts
|
|
18859
|
+
init_paths();
|
|
18860
|
+
import Database3 from "better-sqlite3";
|
|
18861
|
+
import { resolve as resolve50 } from "path";
|
|
18862
|
+
var db2 = null;
|
|
18863
|
+
var PROOF_SCHEMA_VERSION = "1";
|
|
18864
|
+
var SCHEMA_SQL2 = `
|
|
18865
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
18866
|
+
id TEXT PRIMARY KEY,
|
|
18867
|
+
assignment_id TEXT NOT NULL,
|
|
18868
|
+
assignment_dir TEXT NOT NULL,
|
|
18869
|
+
criterion_index INTEGER,
|
|
18870
|
+
kind TEXT NOT NULL,
|
|
18871
|
+
file_path TEXT,
|
|
18872
|
+
note TEXT,
|
|
18873
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
18874
|
+
);
|
|
18875
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_assignment ON artifacts(assignment_id);
|
|
18876
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_assignment_criterion ON artifacts(assignment_id, criterion_index);
|
|
18877
|
+
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
|
|
18878
|
+
`;
|
|
18879
|
+
function initProofDb(dbPath) {
|
|
18880
|
+
if (db2) return db2;
|
|
18881
|
+
const finalPath = dbPath ?? resolve50(syntaurRoot(), "syntaur.db");
|
|
18882
|
+
db2 = new Database3(finalPath);
|
|
18883
|
+
db2.pragma("journal_mode = WAL");
|
|
18884
|
+
db2.exec(SCHEMA_SQL2);
|
|
18885
|
+
db2.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run(
|
|
18886
|
+
"proof_schema_version",
|
|
18887
|
+
PROOF_SCHEMA_VERSION
|
|
18888
|
+
);
|
|
18889
|
+
const database = db2;
|
|
18890
|
+
const runMigrations = database.transaction(() => {
|
|
18891
|
+
});
|
|
18892
|
+
runMigrations.exclusive();
|
|
18893
|
+
return db2;
|
|
18894
|
+
}
|
|
18895
|
+
function getProofDb() {
|
|
18896
|
+
if (!db2) {
|
|
18897
|
+
throw new Error("Proof database not initialized. Call initProofDb() first.");
|
|
18898
|
+
}
|
|
18899
|
+
return db2;
|
|
18900
|
+
}
|
|
18901
|
+
function insertArtifact(input4) {
|
|
18902
|
+
const database = getProofDb();
|
|
18903
|
+
database.prepare(
|
|
18904
|
+
`INSERT INTO artifacts (id, assignment_id, assignment_dir, criterion_index, kind, file_path, note)
|
|
18905
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
18906
|
+
).run(
|
|
18907
|
+
input4.id,
|
|
18908
|
+
input4.assignmentId,
|
|
18909
|
+
input4.assignmentDir,
|
|
18910
|
+
input4.criterionIndex,
|
|
18911
|
+
input4.kind,
|
|
18912
|
+
input4.filePath,
|
|
18913
|
+
input4.note
|
|
18914
|
+
);
|
|
18915
|
+
}
|
|
18916
|
+
function listArtifactsByAssignment(assignmentId) {
|
|
18917
|
+
const database = getProofDb();
|
|
18918
|
+
const rows = database.prepare(
|
|
18919
|
+
`SELECT id, assignment_id, assignment_dir, criterion_index, kind, file_path, note, created_at
|
|
18920
|
+
FROM artifacts
|
|
18921
|
+
WHERE assignment_id = ?
|
|
18922
|
+
ORDER BY (criterion_index IS NULL), criterion_index, created_at`
|
|
18923
|
+
).all(assignmentId);
|
|
18924
|
+
return rows;
|
|
18925
|
+
}
|
|
18926
|
+
|
|
18927
|
+
// src/commands/capture.ts
|
|
18928
|
+
var MAX_ID_RETRIES = 5;
|
|
18929
|
+
function normalizeCriterionIndex(raw) {
|
|
18930
|
+
if (raw === void 0 || raw === null || raw === "") return null;
|
|
18931
|
+
if (typeof raw === "number") {
|
|
18932
|
+
if (!Number.isInteger(raw) || raw < 0) {
|
|
18933
|
+
throw new Error(`--criterion must be a non-negative integer (got "${raw}")`);
|
|
18934
|
+
}
|
|
18935
|
+
return raw;
|
|
18936
|
+
}
|
|
18937
|
+
const s = String(raw).trim();
|
|
18938
|
+
if (!/^\d+$/.test(s)) {
|
|
18939
|
+
throw new Error(`--criterion must be a non-negative integer (got "${raw}")`);
|
|
18940
|
+
}
|
|
18941
|
+
return parseInt(s, 10);
|
|
18942
|
+
}
|
|
18943
|
+
function isUniqueConstraintError(err2) {
|
|
18944
|
+
if (!err2 || typeof err2 !== "object") return false;
|
|
18945
|
+
const e = err2;
|
|
18946
|
+
if (e.code === "SQLITE_CONSTRAINT_PRIMARYKEY" || e.code === "SQLITE_CONSTRAINT_UNIQUE") return true;
|
|
18947
|
+
if (e.code === "SQLITE_CONSTRAINT" && /UNIQUE|PRIMARY KEY/i.test(e.message ?? "")) return true;
|
|
18948
|
+
return false;
|
|
18949
|
+
}
|
|
18950
|
+
async function captureCommand(target, options = {}) {
|
|
18951
|
+
if (!options.kind) {
|
|
18952
|
+
throw new Error(
|
|
18953
|
+
`--kind is required. Must be one of: ${ARTIFACT_KINDS.join(", ")}.`
|
|
18954
|
+
);
|
|
18955
|
+
}
|
|
18956
|
+
if (!isArtifactKind(options.kind)) {
|
|
18957
|
+
throw new Error(
|
|
18958
|
+
`Invalid --kind "${options.kind}". Must be one of: ${ARTIFACT_KINDS.join(", ")}.`
|
|
18959
|
+
);
|
|
18960
|
+
}
|
|
18961
|
+
const kind = options.kind;
|
|
18962
|
+
if (kind === "text") {
|
|
18963
|
+
if (options.file) {
|
|
18964
|
+
throw new Error("--kind=text forbids --file. Use --note for text payloads.");
|
|
18965
|
+
}
|
|
18966
|
+
if (!options.note || options.note.trim() === "") {
|
|
18967
|
+
throw new Error("--kind=text requires --note.");
|
|
18968
|
+
}
|
|
18969
|
+
} else {
|
|
18970
|
+
if (kind !== "http" && !options.file) {
|
|
18971
|
+
throw new Error(`--kind=${kind} requires --file.`);
|
|
18972
|
+
}
|
|
18973
|
+
if (kind === "http" && !options.file && (!options.note || options.note.trim() === "")) {
|
|
18974
|
+
throw new Error("--kind=http requires --file or --note (one or the other).");
|
|
18975
|
+
}
|
|
18976
|
+
}
|
|
18977
|
+
const criterionIndex = normalizeCriterionIndex(options.criterion);
|
|
18978
|
+
const resolved = await resolveAssignmentTarget(target, {
|
|
18979
|
+
project: options.project,
|
|
18980
|
+
dir: options.dir,
|
|
18981
|
+
cwd: options.cwd
|
|
18982
|
+
});
|
|
18983
|
+
let resolvedSource = null;
|
|
18984
|
+
if (options.file) {
|
|
18985
|
+
const expanded = options.file.startsWith("~/") ? resolve51(process.env.HOME ?? "", options.file.slice(2)) : resolve51(options.file);
|
|
18986
|
+
if (!await fileExists(expanded)) {
|
|
18987
|
+
throw new Error(`--file does not exist: ${options.file}`);
|
|
18988
|
+
}
|
|
18989
|
+
let real;
|
|
18990
|
+
try {
|
|
18991
|
+
real = await realpath2(expanded);
|
|
18992
|
+
} catch (e) {
|
|
18993
|
+
throw new Error(
|
|
18994
|
+
`--file is unreadable: ${options.file} (${e instanceof Error ? e.message : String(e)})`
|
|
18995
|
+
);
|
|
18996
|
+
}
|
|
18997
|
+
const st = await stat6(real);
|
|
18998
|
+
if (!st.isFile()) {
|
|
18999
|
+
throw new Error(`--file is not a regular file: ${options.file}`);
|
|
19000
|
+
}
|
|
19001
|
+
resolvedSource = real;
|
|
19002
|
+
}
|
|
19003
|
+
if (!resolved.id || resolved.id.trim() === "") {
|
|
19004
|
+
throw new Error(
|
|
19005
|
+
`Resolved assignment is missing a frontmatter \`id\`: ${resolved.assignmentDir}. Cannot record artifact.`
|
|
19006
|
+
);
|
|
19007
|
+
}
|
|
19008
|
+
initProofDb();
|
|
19009
|
+
const subdir = criterionIndex === null ? "untagged" : String(criterionIndex);
|
|
19010
|
+
const destDir = resolve51(proofDir(resolved.assignmentDir), subdir);
|
|
19011
|
+
if (resolvedSource) await mkdir7(destDir, { recursive: true });
|
|
19012
|
+
const ext = resolvedSource ? extensionForKind(kind) : null;
|
|
19013
|
+
let id = null;
|
|
19014
|
+
let relativeFilePath = null;
|
|
19015
|
+
let absPath = null;
|
|
19016
|
+
let lastErr = null;
|
|
19017
|
+
for (let attempt = 0; attempt < MAX_ID_RETRIES; attempt += 1) {
|
|
19018
|
+
const candidate = generateArtifactId();
|
|
19019
|
+
const candidateAbsPath = resolvedSource && ext ? resolve51(destDir, `${candidate}.${ext}`) : null;
|
|
19020
|
+
const candidateRel = candidateAbsPath ? relative4(resolved.assignmentDir, candidateAbsPath) : null;
|
|
19021
|
+
try {
|
|
19022
|
+
insertArtifact({
|
|
19023
|
+
id: candidate,
|
|
19024
|
+
assignmentId: resolved.id,
|
|
19025
|
+
assignmentDir: resolved.assignmentDir,
|
|
19026
|
+
criterionIndex,
|
|
19027
|
+
kind,
|
|
19028
|
+
filePath: candidateRel,
|
|
19029
|
+
note: options.note ?? null
|
|
19030
|
+
});
|
|
19031
|
+
id = candidate;
|
|
19032
|
+
absPath = candidateAbsPath;
|
|
19033
|
+
relativeFilePath = candidateRel;
|
|
19034
|
+
break;
|
|
19035
|
+
} catch (e) {
|
|
19036
|
+
if (isUniqueConstraintError(e)) {
|
|
19037
|
+
lastErr = e;
|
|
19038
|
+
continue;
|
|
19039
|
+
}
|
|
19040
|
+
throw e;
|
|
19041
|
+
}
|
|
19042
|
+
}
|
|
19043
|
+
if (!id) {
|
|
19044
|
+
throw new Error(
|
|
19045
|
+
`Failed to generate a unique artifact id after ${MAX_ID_RETRIES} attempts: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
|
|
19046
|
+
);
|
|
19047
|
+
}
|
|
19048
|
+
if (resolvedSource && absPath) {
|
|
19049
|
+
await copyFile3(resolvedSource, absPath);
|
|
19050
|
+
}
|
|
19051
|
+
const ref = resolved.standalone ? resolved.id : `${resolved.projectSlug}/${resolved.assignmentSlug}`;
|
|
19052
|
+
const tagSuffix = criterionIndex === null ? "untagged" : `criterion ${criterionIndex}`;
|
|
19053
|
+
console.log(`Captured artifact ${id} (${kind}) for ${ref} \u2014 ${tagSuffix}.`);
|
|
19054
|
+
if (relativeFilePath) {
|
|
19055
|
+
console.log(` file: ${resolve51(resolved.assignmentDir, relativeFilePath)}`);
|
|
19056
|
+
}
|
|
19057
|
+
}
|
|
19058
|
+
|
|
19059
|
+
// src/commands/proof.ts
|
|
19060
|
+
import { Command as Command5 } from "commander";
|
|
19061
|
+
import { readFile as readFile34, writeFile as writeFile10, rename as rename7, stat as stat7 } from "fs/promises";
|
|
19062
|
+
import { resolve as resolve52, relative as relative5, isAbsolute as isAbsolute8 } from "path";
|
|
19063
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
19064
|
+
|
|
19065
|
+
// src/utils/acceptance-criteria-parse.ts
|
|
19066
|
+
var SECTION_HEADING = /^##\s+Acceptance Criteria\s*$/i;
|
|
19067
|
+
var NEXT_HEADING = /^#{1,2}\s+\S/;
|
|
19068
|
+
var CHECKBOX_LINE = /^\s*[-*]\s+\[( |x|X)\]\s+(.*)$/;
|
|
19069
|
+
function parseAcceptanceCriteria(content) {
|
|
19070
|
+
const fmMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
19071
|
+
const body = fmMatch ? content.slice(fmMatch[0].length) : content;
|
|
19072
|
+
const lines = body.split("\n");
|
|
19073
|
+
const sectionStart = lines.findIndex((line) => SECTION_HEADING.test(line.trim()));
|
|
19074
|
+
if (sectionStart === -1) return [];
|
|
19075
|
+
let sectionEnd = lines.length;
|
|
19076
|
+
for (let i = sectionStart + 1; i < lines.length; i += 1) {
|
|
19077
|
+
if (NEXT_HEADING.test(lines[i].trim())) {
|
|
19078
|
+
sectionEnd = i;
|
|
19079
|
+
break;
|
|
19080
|
+
}
|
|
19081
|
+
}
|
|
19082
|
+
const criteria = [];
|
|
19083
|
+
for (let i = sectionStart + 1; i < sectionEnd; i += 1) {
|
|
19084
|
+
const m = lines[i].match(CHECKBOX_LINE);
|
|
19085
|
+
if (!m) continue;
|
|
19086
|
+
const checked = m[1].toLowerCase() === "x";
|
|
19087
|
+
const text = m[2].trim();
|
|
19088
|
+
criteria.push({ index: criteria.length, text, checked });
|
|
19089
|
+
}
|
|
19090
|
+
return criteria;
|
|
19091
|
+
}
|
|
19092
|
+
|
|
19093
|
+
// src/commands/proof.ts
|
|
19094
|
+
init_fs();
|
|
19095
|
+
init_paths();
|
|
19096
|
+
|
|
19097
|
+
// src/templates/proof-md.ts
|
|
19098
|
+
function renderArtifactBullet(a) {
|
|
19099
|
+
const parts = [];
|
|
19100
|
+
parts.push(`**${a.kind}**`);
|
|
19101
|
+
parts.push(`\`${a.id}\``);
|
|
19102
|
+
if (a.file_path) parts.push(`file: \`${a.file_path}\``);
|
|
19103
|
+
if (a.note) parts.push(`note: ${a.note}`);
|
|
19104
|
+
return `- ${parts.join(" \u2014 ")}`;
|
|
19105
|
+
}
|
|
19106
|
+
function renderProofMarkdown(params2) {
|
|
19107
|
+
const { assignment, title, generated, criteria, artifactsByCriterion, untagged, staleByOriginalIndex } = params2;
|
|
19108
|
+
const totalArtifacts = untagged.length + staleByOriginalIndex.length + Array.from(artifactsByCriterion.values()).reduce((sum, list) => sum + list.length, 0);
|
|
19109
|
+
const lines = [];
|
|
19110
|
+
lines.push("---");
|
|
19111
|
+
lines.push(`assignment: ${assignment}`);
|
|
19112
|
+
lines.push(`generated: "${generated}"`);
|
|
19113
|
+
lines.push("---");
|
|
19114
|
+
lines.push("");
|
|
19115
|
+
lines.push(`# Proof \u2014 ${title}`);
|
|
19116
|
+
lines.push("");
|
|
19117
|
+
if (totalArtifacts === 0) {
|
|
19118
|
+
lines.push("_No artifacts captured for this assignment yet._");
|
|
19119
|
+
lines.push("");
|
|
19120
|
+
return lines.join("\n");
|
|
19121
|
+
}
|
|
19122
|
+
for (const c2 of criteria) {
|
|
19123
|
+
const checkbox2 = c2.checked ? "[x]" : "[ ]";
|
|
19124
|
+
lines.push(`## ${c2.index}. ${checkbox2} ${c2.text}`);
|
|
19125
|
+
lines.push("");
|
|
19126
|
+
const tagged = artifactsByCriterion.get(c2.index) ?? [];
|
|
19127
|
+
if (tagged.length === 0) {
|
|
19128
|
+
lines.push("_no artifacts captured_");
|
|
19129
|
+
} else {
|
|
19130
|
+
for (const a of tagged) lines.push(renderArtifactBullet(a));
|
|
19131
|
+
}
|
|
19132
|
+
lines.push("");
|
|
19133
|
+
}
|
|
19134
|
+
if (untagged.length > 0 || staleByOriginalIndex.length > 0) {
|
|
19135
|
+
lines.push("## Other artifacts");
|
|
19136
|
+
lines.push("");
|
|
19137
|
+
for (const a of untagged) lines.push(renderArtifactBullet(a));
|
|
19138
|
+
for (const a of staleByOriginalIndex) {
|
|
19139
|
+
const note = a.note ? ` \u2014 note: ${a.note}` : "";
|
|
19140
|
+
const file = a.file_path ? ` \u2014 file: \`${a.file_path}\`` : "";
|
|
19141
|
+
lines.push(
|
|
19142
|
+
`- _(was tagged criterion ${a.criterion_index} \u2014 no longer present)_ **${a.kind}** \`${a.id}\`${file}${note}`
|
|
19143
|
+
);
|
|
19144
|
+
}
|
|
19145
|
+
lines.push("");
|
|
19146
|
+
}
|
|
19147
|
+
return lines.join("\n");
|
|
19148
|
+
}
|
|
19149
|
+
|
|
19150
|
+
// src/utils/escape-html.ts
|
|
19151
|
+
function escapeHtml(value) {
|
|
19152
|
+
if (value === null || value === void 0) return "";
|
|
19153
|
+
const s = typeof value === "string" ? value : String(value);
|
|
19154
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
19155
|
+
}
|
|
19156
|
+
|
|
19157
|
+
// src/templates/proof-html.ts
|
|
19158
|
+
var INLINE_TEXT_LIMIT_BYTES = 256 * 1024;
|
|
19159
|
+
var STYLE_BLOCK = `
|
|
19160
|
+
body { max-width: 920px; margin: 2rem auto; font-family: system-ui, -apple-system, sans-serif; padding: 0 1rem; color: #222; line-height: 1.5; }
|
|
19161
|
+
h1, h2, h3 { line-height: 1.25; }
|
|
19162
|
+
nav.toc { padding: 0.5rem 1rem; background: #f9f9f9; border-radius: 4px; margin-bottom: 2rem; }
|
|
19163
|
+
nav.toc ul { margin: 0; padding-left: 1.25rem; }
|
|
19164
|
+
section.criterion { padding: 1rem 0; border-bottom: 1px solid #eee; }
|
|
19165
|
+
section.criterion:last-of-type { border-bottom: none; }
|
|
19166
|
+
img, video { max-width: 100%; height: auto; border-radius: 4px; display: block; }
|
|
19167
|
+
pre { max-height: 480px; overflow: auto; padding: 1rem; background: #f5f5f5; border-radius: 4px; white-space: pre-wrap; word-break: break-word; }
|
|
19168
|
+
.artifact { margin: 1rem 0; }
|
|
19169
|
+
.artifact-meta { font-size: 0.85em; color: #666; margin-bottom: 0.25rem; }
|
|
19170
|
+
.untagged { color: #666; font-size: 0.9em; }
|
|
19171
|
+
.stale { color: #b04a4a; font-style: italic; font-size: 0.9em; }
|
|
19172
|
+
.empty-criterion { color: #888; font-style: italic; }
|
|
19173
|
+
a { color: #2563eb; }
|
|
19174
|
+
`;
|
|
19175
|
+
function renderArtifact(a, ctx) {
|
|
19176
|
+
const idEsc = escapeHtml(a.id);
|
|
19177
|
+
const noteHtml = a.note ? `<div class="artifact-meta">note: ${escapeHtml(a.note)}</div>` : "";
|
|
19178
|
+
const filePathEsc = a.file_path ? escapeHtml(a.file_path) : null;
|
|
19179
|
+
let body = "";
|
|
19180
|
+
switch (a.kind) {
|
|
19181
|
+
case "screenshot":
|
|
19182
|
+
body = filePathEsc ? `<img src="${filePathEsc}" alt="screenshot artifact ${idEsc}">` : "<em>missing file</em>";
|
|
19183
|
+
break;
|
|
19184
|
+
case "video":
|
|
19185
|
+
body = filePathEsc ? `<video controls preload="metadata" src="${filePathEsc}"></video>` : "<em>missing file</em>";
|
|
19186
|
+
break;
|
|
19187
|
+
case "asciinema":
|
|
19188
|
+
body = filePathEsc ? `<a href="${filePathEsc}" download>Open .cast file (${idEsc})</a> — <code>asciinema play <file></code> to play locally` : "<em>missing file</em>";
|
|
19189
|
+
break;
|
|
19190
|
+
case "http":
|
|
19191
|
+
case "text": {
|
|
19192
|
+
const inline = a.file_path ? ctx.inlineFiles.get(a.file_path) : null;
|
|
19193
|
+
if (a.kind === "http" && a.file_path && inline === null) {
|
|
19194
|
+
body = `<a href="${filePathEsc}" download>Open transcript (${idEsc})</a>`;
|
|
19195
|
+
} else if (inline !== void 0 && inline !== null) {
|
|
19196
|
+
body = `<pre><code>${escapeHtml(inline)}</code></pre>`;
|
|
19197
|
+
} else if (a.note) {
|
|
19198
|
+
body = `<pre><code>${escapeHtml(a.note)}</code></pre>`;
|
|
19199
|
+
} else {
|
|
19200
|
+
body = "<em>(empty)</em>";
|
|
19201
|
+
}
|
|
19202
|
+
break;
|
|
19203
|
+
}
|
|
19204
|
+
default:
|
|
19205
|
+
body = `<em>unknown kind: ${escapeHtml(a.kind)}</em>`;
|
|
19206
|
+
}
|
|
19207
|
+
return `<div class="artifact">${noteHtml}${body}<div class="artifact-meta"><code>${idEsc}</code></div></div>`;
|
|
19208
|
+
}
|
|
19209
|
+
function renderProofHtml(params2, inlineFiles = /* @__PURE__ */ new Map()) {
|
|
19210
|
+
const { assignment, title, generated, criteria, artifactsByCriterion, untagged, staleByOriginalIndex } = params2;
|
|
19211
|
+
const ctx = { inlineFiles };
|
|
19212
|
+
const totalArtifacts = untagged.length + staleByOriginalIndex.length + Array.from(artifactsByCriterion.values()).reduce((sum, list) => sum + list.length, 0);
|
|
19213
|
+
const titleEsc = escapeHtml(title);
|
|
19214
|
+
const assignmentEsc = escapeHtml(assignment);
|
|
19215
|
+
const generatedEsc = escapeHtml(generated);
|
|
19216
|
+
const tocItems = criteria.map((c2) => `<li><a href="#criterion-${c2.index}">${c2.index}. ${escapeHtml(c2.text)}</a></li>`).join("\n ");
|
|
19217
|
+
const otherTocItem = untagged.length > 0 || staleByOriginalIndex.length > 0 ? '<li><a href="#other-artifacts">Other artifacts</a></li>' : "";
|
|
19218
|
+
const criteriaHtml = criteria.map((c2) => {
|
|
19219
|
+
const tagged = artifactsByCriterion.get(c2.index) ?? [];
|
|
19220
|
+
const heading = `<h2>${c2.index}. <input type="checkbox" disabled${c2.checked ? " checked" : ""}> ${escapeHtml(c2.text)}</h2>`;
|
|
19221
|
+
const body = tagged.length === 0 ? '<p class="empty-criterion">no artifacts captured</p>' : tagged.map((a) => renderArtifact(a, ctx)).join("\n ");
|
|
19222
|
+
return `<section id="criterion-${c2.index}" class="criterion">
|
|
19223
|
+
${heading}
|
|
19224
|
+
${body}
|
|
19225
|
+
</section>`;
|
|
19226
|
+
}).join("\n ");
|
|
19227
|
+
const otherSection = untagged.length > 0 || staleByOriginalIndex.length > 0 ? `<section id="other-artifacts" class="criterion">
|
|
19228
|
+
<h2>Other artifacts</h2>
|
|
19229
|
+
${untagged.map((a) => `<div class="untagged">${renderArtifact(a, ctx)}</div>`).join("\n ")}
|
|
19230
|
+
${staleByOriginalIndex.map(
|
|
19231
|
+
(a) => `<div class="stale">(was tagged criterion ${a.criterion_index ?? ""} \u2014 no longer present)
|
|
19232
|
+
${renderArtifact(a, ctx)}</div>`
|
|
19233
|
+
).join("\n ")}
|
|
19234
|
+
</section>` : "";
|
|
19235
|
+
const emptyState = totalArtifacts === 0 && criteria.length === 0 ? "<p>No artifacts captured for this assignment yet.</p>" : "";
|
|
19236
|
+
return `<!DOCTYPE html>
|
|
19237
|
+
<html lang="en">
|
|
19238
|
+
<head>
|
|
19239
|
+
<meta charset="utf-8">
|
|
19240
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
19241
|
+
<title>Proof \u2014 ${titleEsc}</title>
|
|
19242
|
+
<style>${STYLE_BLOCK}</style>
|
|
19243
|
+
</head>
|
|
19244
|
+
<body>
|
|
19245
|
+
<header>
|
|
19246
|
+
<h1>Proof \u2014 ${titleEsc}</h1>
|
|
19247
|
+
<p><small>Assignment: <code>${assignmentEsc}</code> \xB7 Generated: ${generatedEsc}</small></p>
|
|
19248
|
+
</header>
|
|
19249
|
+
${criteria.length > 0 || untagged.length > 0 || staleByOriginalIndex.length > 0 ? `<nav class="toc"><ul>
|
|
19250
|
+
${tocItems}
|
|
19251
|
+
${otherTocItem}
|
|
19252
|
+
</ul></nav>` : ""}
|
|
19253
|
+
<main>
|
|
19254
|
+
${emptyState}
|
|
19255
|
+
${criteriaHtml}
|
|
19256
|
+
${otherSection}
|
|
19257
|
+
</main>
|
|
19258
|
+
</body>
|
|
19259
|
+
</html>
|
|
19260
|
+
`;
|
|
19261
|
+
}
|
|
19262
|
+
|
|
19263
|
+
// src/commands/proof.ts
|
|
19264
|
+
async function readAssignmentMeta(assignmentDir) {
|
|
19265
|
+
const path = resolve52(assignmentDir, "assignment.md");
|
|
19266
|
+
if (!await fileExists(path)) {
|
|
19267
|
+
return { title: "", body: "" };
|
|
19268
|
+
}
|
|
19269
|
+
const content = await readFile34(path, "utf-8");
|
|
19270
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
19271
|
+
let title = "";
|
|
19272
|
+
if (fmMatch) {
|
|
19273
|
+
const titleLine = fmMatch[1].split("\n").find((l) => /^title:\s/i.test(l));
|
|
19274
|
+
if (titleLine) {
|
|
19275
|
+
const raw = titleLine.replace(/^title:\s*/i, "").trim();
|
|
19276
|
+
title = raw.replace(/^["']|["']$/g, "");
|
|
19277
|
+
}
|
|
19278
|
+
}
|
|
19279
|
+
return { title, body: content };
|
|
19280
|
+
}
|
|
19281
|
+
async function atomicWrite(destPath, content) {
|
|
19282
|
+
const tmp = `${destPath}.${randomBytes3(4).toString("hex")}.tmp`;
|
|
19283
|
+
await writeFile10(tmp, content, "utf-8");
|
|
19284
|
+
await rename7(tmp, destPath);
|
|
19285
|
+
}
|
|
19286
|
+
function isWithin(root, target) {
|
|
19287
|
+
const rel = relative5(root, target);
|
|
19288
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute8(rel);
|
|
19289
|
+
}
|
|
19290
|
+
function groupArtifacts(rows, criterionCount) {
|
|
19291
|
+
const artifactsByCriterion = /* @__PURE__ */ new Map();
|
|
19292
|
+
const untagged = [];
|
|
19293
|
+
const staleByOriginalIndex = [];
|
|
19294
|
+
for (const row of rows) {
|
|
19295
|
+
const idx = row.criterion_index;
|
|
19296
|
+
if (idx === null || idx === void 0) {
|
|
19297
|
+
untagged.push(row);
|
|
19298
|
+
continue;
|
|
19299
|
+
}
|
|
19300
|
+
if (idx >= 0 && idx < criterionCount) {
|
|
19301
|
+
const list = artifactsByCriterion.get(idx) ?? [];
|
|
19302
|
+
list.push(row);
|
|
19303
|
+
artifactsByCriterion.set(idx, list);
|
|
19304
|
+
} else {
|
|
19305
|
+
staleByOriginalIndex.push(row);
|
|
19306
|
+
}
|
|
19307
|
+
}
|
|
19308
|
+
return { artifactsByCriterion, untagged, staleByOriginalIndex };
|
|
19309
|
+
}
|
|
19310
|
+
async function loadInlineFiles(rows, assignmentDir) {
|
|
19311
|
+
const out = /* @__PURE__ */ new Map();
|
|
19312
|
+
const proofRoot = proofDir(assignmentDir);
|
|
19313
|
+
for (const r of rows) {
|
|
19314
|
+
if (!r.file_path) continue;
|
|
19315
|
+
if (r.kind !== "http" && r.kind !== "text") continue;
|
|
19316
|
+
const abs = resolve52(assignmentDir, r.file_path);
|
|
19317
|
+
if (!isWithin(proofRoot, abs)) {
|
|
19318
|
+
out.set(r.file_path, null);
|
|
19319
|
+
continue;
|
|
19320
|
+
}
|
|
19321
|
+
if (!await fileExists(abs)) {
|
|
19322
|
+
out.set(r.file_path, null);
|
|
19323
|
+
continue;
|
|
19324
|
+
}
|
|
19325
|
+
const st = await stat7(abs);
|
|
19326
|
+
if (st.size > INLINE_TEXT_LIMIT_BYTES) {
|
|
19327
|
+
out.set(r.file_path, null);
|
|
19328
|
+
continue;
|
|
19329
|
+
}
|
|
19330
|
+
try {
|
|
19331
|
+
out.set(r.file_path, await readFile34(abs, "utf-8"));
|
|
19332
|
+
} catch {
|
|
19333
|
+
out.set(r.file_path, null);
|
|
19334
|
+
}
|
|
19335
|
+
}
|
|
19336
|
+
return out;
|
|
19337
|
+
}
|
|
19338
|
+
async function proofBuildCommand(target, options = {}) {
|
|
19339
|
+
const resolved = await resolveAssignmentTarget(target, {
|
|
19340
|
+
project: options.project,
|
|
19341
|
+
dir: options.dir,
|
|
19342
|
+
cwd: options.cwd
|
|
19343
|
+
});
|
|
19344
|
+
const meta = await readAssignmentMeta(resolved.assignmentDir);
|
|
19345
|
+
const criteria = parseAcceptanceCriteria(meta.body);
|
|
19346
|
+
initProofDb();
|
|
19347
|
+
const rows = listArtifactsByAssignment(resolved.id);
|
|
19348
|
+
const { artifactsByCriterion, untagged, staleByOriginalIndex } = groupArtifacts(
|
|
19349
|
+
rows,
|
|
19350
|
+
criteria.length
|
|
19351
|
+
);
|
|
19352
|
+
const inlineFiles = await loadInlineFiles(rows, resolved.assignmentDir);
|
|
19353
|
+
const renderParams = {
|
|
19354
|
+
assignment: resolved.standalone ? resolved.id : `${resolved.projectSlug}/${resolved.assignmentSlug}`,
|
|
19355
|
+
title: meta.title || resolved.assignmentSlug,
|
|
19356
|
+
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19357
|
+
criteria,
|
|
19358
|
+
artifactsByCriterion,
|
|
19359
|
+
untagged,
|
|
19360
|
+
staleByOriginalIndex
|
|
19361
|
+
};
|
|
19362
|
+
const md = renderProofMarkdown(renderParams);
|
|
19363
|
+
const html = renderProofHtml(renderParams, inlineFiles);
|
|
19364
|
+
const mdPath = resolve52(resolved.assignmentDir, "proof.md");
|
|
19365
|
+
const htmlPath = resolve52(resolved.assignmentDir, "proof.html");
|
|
19366
|
+
await atomicWrite(mdPath, md);
|
|
19367
|
+
await atomicWrite(htmlPath, html);
|
|
19368
|
+
console.log(`Wrote ${htmlPath}`);
|
|
19369
|
+
console.log(`Wrote ${mdPath}`);
|
|
19370
|
+
return { htmlPath, mdPath };
|
|
19371
|
+
}
|
|
19372
|
+
var proofCommand = new Command5("proof").description("Render proof artifacts for an assignment");
|
|
19373
|
+
proofCommand.command("build").description("Render proof.html and proof.md for an assignment").argument("[target]", "Assignment slug (with --project) or UUID; falls back to .syntaur/context.json").option("--project <slug>", "Project slug if the target is project-nested").option("--dir <path>", "Override default project directory").action(async (target, options) => {
|
|
19374
|
+
try {
|
|
19375
|
+
await proofBuildCommand(target, options);
|
|
19376
|
+
} catch (error) {
|
|
19377
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
19378
|
+
process.exit(1);
|
|
19379
|
+
}
|
|
19380
|
+
});
|
|
19381
|
+
|
|
19382
|
+
// src/commands/request.ts
|
|
19383
|
+
init_paths();
|
|
19384
|
+
init_fs();
|
|
19385
|
+
init_config2();
|
|
19386
|
+
init_slug();
|
|
19387
|
+
init_timestamp();
|
|
19388
|
+
init_assignment_resolver();
|
|
19389
|
+
init_assignment_todos();
|
|
19390
|
+
import { resolve as resolve53 } from "path";
|
|
19391
|
+
import { readFile as readFile35 } from "fs/promises";
|
|
19392
|
+
async function requestCommand(target, text, options = {}) {
|
|
19393
|
+
if (!text || !text.trim()) {
|
|
19394
|
+
throw new Error("Request text cannot be empty.");
|
|
19395
|
+
}
|
|
19396
|
+
const config = await readConfig();
|
|
19397
|
+
const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
|
|
19398
|
+
let assignmentDir;
|
|
19399
|
+
let targetRef;
|
|
19400
|
+
if (options.project) {
|
|
19401
|
+
if (!isValidSlug(options.project)) {
|
|
19402
|
+
throw new Error(`Invalid project slug "${options.project}".`);
|
|
19403
|
+
}
|
|
19404
|
+
if (!isValidSlug(target)) {
|
|
19405
|
+
throw new Error(`Invalid assignment slug "${target}".`);
|
|
19406
|
+
}
|
|
19407
|
+
assignmentDir = resolve53(baseDir, options.project, "assignments", target);
|
|
19408
|
+
targetRef = target;
|
|
19409
|
+
} else {
|
|
19410
|
+
const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), target);
|
|
19411
|
+
if (!resolved) {
|
|
19412
|
+
throw new Error(`Target assignment "${target}" not found. Provide --project <slug> or a valid UUID.`);
|
|
19413
|
+
}
|
|
19414
|
+
assignmentDir = resolved.assignmentDir;
|
|
19415
|
+
targetRef = resolved.standalone ? resolved.id : resolved.assignmentSlug;
|
|
19416
|
+
}
|
|
19417
|
+
const assignmentMdPath2 = resolve53(assignmentDir, "assignment.md");
|
|
19418
|
+
if (!await fileExists(assignmentMdPath2)) {
|
|
19419
|
+
throw new Error(`assignment.md not found at ${assignmentMdPath2}`);
|
|
19420
|
+
}
|
|
19421
|
+
const source = options.from ?? process.env.SYNTAUR_ASSIGNMENT ?? "unknown";
|
|
19422
|
+
let content = await readFile35(assignmentMdPath2, "utf-8");
|
|
19423
|
+
content = appendTodosToAssignmentBody(content, [
|
|
19424
|
+
{ description: `${text.trim()} (from: ${source})` }
|
|
19425
|
+
]);
|
|
19426
|
+
content = touchAssignmentUpdated(content, nowTimestamp());
|
|
19427
|
+
await writeFileForce(assignmentMdPath2, content);
|
|
19428
|
+
console.log(`Added todo to ${targetRef}: ${text.trim()} (from: ${source})`);
|
|
19429
|
+
}
|
|
19430
|
+
|
|
19431
|
+
// src/commands/plan.ts
|
|
19432
|
+
init_fs();
|
|
19433
|
+
init_paths();
|
|
19434
|
+
import { Command as Command6 } from "commander";
|
|
19435
|
+
import { readFile as readFile36, readdir as readdir18 } from "fs/promises";
|
|
19436
|
+
import { resolve as resolve54 } from "path";
|
|
19437
|
+
async function readContextAssignmentDir(cwd) {
|
|
19438
|
+
const path = resolve54(cwd, ".syntaur", "context.json");
|
|
19439
|
+
if (!await fileExists(path)) return null;
|
|
19440
|
+
try {
|
|
19441
|
+
const raw = await readFile36(path, "utf-8");
|
|
19442
|
+
const ctx = JSON.parse(raw);
|
|
19443
|
+
if (typeof ctx.assignmentDir === "string" && ctx.assignmentDir.length > 0) {
|
|
19444
|
+
return ctx.assignmentDir;
|
|
19445
|
+
}
|
|
19446
|
+
return null;
|
|
19447
|
+
} catch {
|
|
19448
|
+
return null;
|
|
19449
|
+
}
|
|
19450
|
+
}
|
|
19451
|
+
async function resolveAssignmentDir(opts) {
|
|
19452
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
19453
|
+
if (opts.assignment) {
|
|
19454
|
+
if (opts.project) {
|
|
19455
|
+
return resolve54(defaultProjectDir(), opts.project, "assignments", opts.assignment);
|
|
19456
|
+
}
|
|
19457
|
+
return resolve54(assignmentsDir(), opts.assignment);
|
|
19458
|
+
}
|
|
19459
|
+
const fromCtx = await readContextAssignmentDir(cwd);
|
|
19460
|
+
if (fromCtx) return fromCtx;
|
|
19461
|
+
throw new Error(
|
|
19462
|
+
"No active assignment. Pass --assignment <slug> --project <slug> or run from a workspace with .syntaur/context.json."
|
|
19463
|
+
);
|
|
19464
|
+
}
|
|
19465
|
+
var PLAN_PATTERN = /^plan(?:-v(\d+))?\.md$/;
|
|
19466
|
+
async function listPlanFiles(assignmentDir) {
|
|
19467
|
+
if (!await fileExists(assignmentDir)) return [];
|
|
19468
|
+
const entries = await readdir18(assignmentDir, { withFileTypes: true });
|
|
19469
|
+
const out = [];
|
|
19470
|
+
for (const e of entries) {
|
|
19471
|
+
if (!e.isFile()) continue;
|
|
19472
|
+
const m = e.name.match(PLAN_PATTERN);
|
|
19473
|
+
if (!m) continue;
|
|
19474
|
+
const version2 = m[1] ? parseInt(m[1], 10) : 1;
|
|
19475
|
+
out.push({ fileName: e.name, version: version2 });
|
|
19476
|
+
}
|
|
19477
|
+
out.sort((a, b) => a.version - b.version);
|
|
19478
|
+
return out;
|
|
19479
|
+
}
|
|
19480
|
+
function nextPlanFileName(currentVersion) {
|
|
19481
|
+
const next = currentVersion + 1;
|
|
19482
|
+
return { fileName: `plan-v${next}.md`, version: next };
|
|
19483
|
+
}
|
|
19484
|
+
function planLabel(version2) {
|
|
19485
|
+
return version2 === 1 ? "plan" : `plan v${version2}`;
|
|
19486
|
+
}
|
|
19487
|
+
function planLinkText(version2) {
|
|
19488
|
+
return version2 === 1 ? "plan" : `plan v${version2}`;
|
|
19489
|
+
}
|
|
19490
|
+
function planFileName(version2) {
|
|
19491
|
+
return version2 === 1 ? "plan.md" : `plan-v${version2}.md`;
|
|
19492
|
+
}
|
|
19493
|
+
function rewriteAssignmentTodos(content, oldVersion, newVersion) {
|
|
19494
|
+
const lines = content.split("\n");
|
|
19495
|
+
const todosHeaderIdx = lines.findIndex((l) => /^##\s+Todos\s*$/.test(l));
|
|
19496
|
+
if (todosHeaderIdx === -1) {
|
|
19497
|
+
throw new Error("assignment.md has no `## Todos` section to rewrite.");
|
|
19498
|
+
}
|
|
19499
|
+
let endIdx = lines.length;
|
|
19500
|
+
for (let i = todosHeaderIdx + 1; i < lines.length; i++) {
|
|
19501
|
+
if (/^##\s/.test(lines[i])) {
|
|
19502
|
+
endIdx = i;
|
|
19503
|
+
break;
|
|
19504
|
+
}
|
|
19505
|
+
}
|
|
19506
|
+
const oldFile = planFileName(oldVersion);
|
|
19507
|
+
const oldLink = `./${oldFile}`;
|
|
19508
|
+
const newFile = planFileName(newVersion);
|
|
19509
|
+
const newLabel = planLinkText(newVersion);
|
|
19510
|
+
const supersededTag = `(superseded by plan-v${newVersion})`;
|
|
19511
|
+
const CYCLE_VERB_PATTERN = /^(\s*-\s*\[[ xX]\]\s*)(Create|Review|Implement|Review implementation of)(\s+\[)/;
|
|
19512
|
+
let rewrote = 0;
|
|
19513
|
+
for (let i = todosHeaderIdx + 1; i < endIdx; i++) {
|
|
19514
|
+
const line = lines[i];
|
|
19515
|
+
if (!line.includes(oldLink)) continue;
|
|
19516
|
+
if (!CYCLE_VERB_PATTERN.test(line)) continue;
|
|
19517
|
+
if (line.includes("(superseded by plan-v")) continue;
|
|
19518
|
+
let next = line.replace(/^(\s*-\s*)\[[ xX]\]/, "$1[x]");
|
|
19519
|
+
next = next.replace(
|
|
19520
|
+
/^(\s*-\s*\[x\]\s*)(.*)$/,
|
|
19521
|
+
(_m, prefix, rest) => {
|
|
19522
|
+
const body = rest.endsWith(" ") ? rest.trimEnd() : rest;
|
|
19523
|
+
if (body.startsWith("~~") && body.endsWith("~~")) {
|
|
19524
|
+
return `${prefix}${body} ${supersededTag}`;
|
|
19525
|
+
}
|
|
19526
|
+
return `${prefix}~~${body}~~ ${supersededTag}`;
|
|
19527
|
+
}
|
|
19528
|
+
);
|
|
19529
|
+
lines[i] = next;
|
|
19530
|
+
rewrote += 1;
|
|
19531
|
+
}
|
|
19532
|
+
const newTodos = [
|
|
19533
|
+
`- [ ] Create [${newLabel}](./${newFile})`,
|
|
19534
|
+
`- [ ] Review [${newLabel}](./${newFile})`,
|
|
19535
|
+
`- [ ] Implement [${newLabel}](./${newFile})`,
|
|
19536
|
+
`- [ ] Review implementation of [${newLabel}](./${newFile})`
|
|
19537
|
+
];
|
|
19538
|
+
const insertAt = endIdx;
|
|
19539
|
+
const prevLine = lines[insertAt - 1] ?? "";
|
|
19540
|
+
const toInsert = [];
|
|
19541
|
+
if (prevLine.trim() !== "") toInsert.push("");
|
|
19542
|
+
toInsert.push(...newTodos);
|
|
19543
|
+
lines.splice(insertAt, 0, ...toInsert);
|
|
19544
|
+
return {
|
|
19545
|
+
updated: lines.join("\n"),
|
|
19546
|
+
rewrote,
|
|
19547
|
+
appended: newTodos.length
|
|
19548
|
+
};
|
|
19549
|
+
}
|
|
19550
|
+
function extractUncheckedTodos(planBody) {
|
|
19551
|
+
const out = [];
|
|
19552
|
+
for (const line of planBody.split("\n")) {
|
|
19553
|
+
if (/^\s*-\s*\[\s\]\s+/.test(line)) {
|
|
19554
|
+
out.push(line);
|
|
19555
|
+
}
|
|
19556
|
+
}
|
|
19557
|
+
return out;
|
|
19558
|
+
}
|
|
19559
|
+
function isoNow() {
|
|
19560
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
19561
|
+
}
|
|
19562
|
+
function buildNewPlanStub(opts) {
|
|
19563
|
+
const created = isoNow();
|
|
19564
|
+
const oldLabel = planLabel(opts.oldVersion);
|
|
19565
|
+
const carriedSection = opts.uncheckedTodos.length === 0 ? "_No unchecked tasks carried forward from the prior plan._" : opts.uncheckedTodos.join("\n");
|
|
19566
|
+
return `---
|
|
19567
|
+
assignment: ${opts.assignmentSlug}
|
|
19568
|
+
status: draft
|
|
19569
|
+
created: "${created}"
|
|
19570
|
+
updated: "${created}"
|
|
19571
|
+
---
|
|
19572
|
+
|
|
19573
|
+
# ${opts.assignmentSlug} \u2014 Implementation Plan v${opts.newVersion}
|
|
19574
|
+
|
|
19575
|
+
**Date:** ${created.slice(0, 10)}
|
|
19576
|
+
**Supersedes:** [${oldLabel}](./${planFileName(opts.oldVersion)})
|
|
19577
|
+
|
|
19578
|
+
## Objective
|
|
19579
|
+
|
|
19580
|
+
<!-- Describe what changed and why a new plan is needed. -->
|
|
19581
|
+
|
|
19582
|
+
## Carried-forward tasks
|
|
19583
|
+
|
|
19584
|
+
${carriedSection}
|
|
19585
|
+
|
|
19586
|
+
## Tasks
|
|
19587
|
+
|
|
19588
|
+
<!-- Add the new plan tasks here. -->
|
|
19589
|
+
|
|
19590
|
+
## Verification
|
|
19591
|
+
|
|
19592
|
+
<!-- Add verification steps here. -->
|
|
19593
|
+
`;
|
|
19594
|
+
}
|
|
19595
|
+
async function runPlanVersion(options) {
|
|
19596
|
+
const assignmentDir = await resolveAssignmentDir(options);
|
|
19597
|
+
if (!await fileExists(assignmentDir)) {
|
|
19598
|
+
throw new Error(`Assignment directory does not exist: ${assignmentDir}`);
|
|
19599
|
+
}
|
|
19600
|
+
const assignmentMdPath2 = resolve54(assignmentDir, "assignment.md");
|
|
19601
|
+
if (!await fileExists(assignmentMdPath2)) {
|
|
19602
|
+
throw new Error(`Missing assignment.md at: ${assignmentMdPath2}`);
|
|
19603
|
+
}
|
|
19604
|
+
const planFiles = await listPlanFiles(assignmentDir);
|
|
19605
|
+
if (planFiles.length === 0) {
|
|
19606
|
+
throw new Error(
|
|
19607
|
+
`No plan.md (or plan-v<N>.md) found in ${assignmentDir}. Run /plan-assignment to create plan.md first.`
|
|
19608
|
+
);
|
|
19609
|
+
}
|
|
19610
|
+
const current = planFiles[planFiles.length - 1];
|
|
19611
|
+
const next = nextPlanFileName(current.version);
|
|
19612
|
+
const newPath = resolve54(assignmentDir, next.fileName);
|
|
19613
|
+
if (await fileExists(newPath) && !options.force) {
|
|
19614
|
+
throw new Error(`${next.fileName} already exists. Use --force to overwrite.`);
|
|
19615
|
+
}
|
|
19616
|
+
const assignmentMd = await readFile36(assignmentMdPath2, "utf-8");
|
|
19617
|
+
const slugMatch = assignmentMd.match(/^slug:\s*(.+?)\s*$/m);
|
|
19618
|
+
const slug = slugMatch ? slugMatch[1].trim() : assignmentDir.split("/").pop() ?? "";
|
|
19619
|
+
const oldPlanPath = resolve54(assignmentDir, current.fileName);
|
|
19620
|
+
const oldPlanContent = await readFile36(oldPlanPath, "utf-8");
|
|
19621
|
+
const oldBody = oldPlanContent.replace(/^---[\s\S]*?\n---\n?/, "");
|
|
19622
|
+
const carriedTodos = extractUncheckedTodos(oldBody);
|
|
19623
|
+
const stub = buildNewPlanStub({
|
|
19624
|
+
assignmentSlug: slug,
|
|
19625
|
+
newVersion: next.version,
|
|
19626
|
+
oldVersion: current.version,
|
|
19627
|
+
uncheckedTodos: carriedTodos
|
|
19628
|
+
});
|
|
19629
|
+
const { updated, rewrote, appended } = rewriteAssignmentTodos(
|
|
19630
|
+
assignmentMd,
|
|
19631
|
+
current.version,
|
|
19632
|
+
next.version
|
|
19633
|
+
);
|
|
19634
|
+
await writeFileForce(newPath, stub);
|
|
19635
|
+
await writeFileForce(assignmentMdPath2, updated);
|
|
19636
|
+
console.log(
|
|
19637
|
+
`Created ${next.fileName} (superseding ${current.fileName}). Rewrote ${rewrote} prior todo(s); appended ${appended} new todo(s).`
|
|
19638
|
+
);
|
|
19639
|
+
console.log(`Path: ${newPath}`);
|
|
19640
|
+
console.log(`Carried forward: ${carriedTodos.length} unchecked task(s).`);
|
|
19641
|
+
}
|
|
19642
|
+
var planCommand = new Command6("plan").description("Manage plan files for the active assignment");
|
|
19643
|
+
planCommand.command("version").description(
|
|
19644
|
+
"Create the next plan-v<N>.md, supersede the prior plan in assignment.md ## Todos, and carry forward unchecked tasks"
|
|
19645
|
+
).option("--assignment <slug>", "Assignment slug (UUID for standalone). Defaults to .syntaur/context.json").option("--project <slug>", "Project slug. Required when --assignment is given for a project-nested assignment").option("--force", "Overwrite if the next plan-v<N>.md already exists").action(async (options) => {
|
|
19646
|
+
try {
|
|
19647
|
+
await runPlanVersion(options);
|
|
19648
|
+
} catch (error) {
|
|
19649
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
19650
|
+
process.exit(1);
|
|
19651
|
+
}
|
|
19652
|
+
});
|
|
19653
|
+
|
|
19654
|
+
// src/commands/session.ts
|
|
19655
|
+
init_fs();
|
|
19656
|
+
import { Command as Command7 } from "commander";
|
|
19657
|
+
import { readFile as readFile37, readdir as readdir19, stat as stat8 } from "fs/promises";
|
|
19658
|
+
import { resolve as resolve55 } from "path";
|
|
19659
|
+
async function readContext(cwd) {
|
|
19660
|
+
const path = resolve55(cwd, ".syntaur", "context.json");
|
|
19661
|
+
if (!await fileExists(path)) return null;
|
|
19662
|
+
try {
|
|
19663
|
+
const raw = await readFile37(path, "utf-8");
|
|
19664
|
+
return JSON.parse(raw);
|
|
19665
|
+
} catch {
|
|
19666
|
+
return null;
|
|
19667
|
+
}
|
|
19668
|
+
}
|
|
19669
|
+
async function findLatestSessionSummary(assignmentDir) {
|
|
19670
|
+
const sessionsRoot = resolve55(assignmentDir, "sessions");
|
|
19671
|
+
if (!await fileExists(sessionsRoot)) return null;
|
|
19672
|
+
const entries = await readdir19(sessionsRoot, { withFileTypes: true });
|
|
19673
|
+
let best = null;
|
|
19674
|
+
for (const entry of entries) {
|
|
19675
|
+
if (!entry.isDirectory()) continue;
|
|
19676
|
+
const summaryPath = resolve55(sessionsRoot, entry.name, "summary.md");
|
|
19677
|
+
if (!await fileExists(summaryPath)) continue;
|
|
19678
|
+
const st = await stat8(summaryPath);
|
|
19679
|
+
if (best === null || st.mtime.getTime() > best.mtime.getTime()) {
|
|
19680
|
+
best = { sessionId: entry.name, path: summaryPath, mtime: st.mtime };
|
|
19681
|
+
}
|
|
19682
|
+
}
|
|
19683
|
+
return best;
|
|
19684
|
+
}
|
|
19685
|
+
async function findOpenHandoff(assignmentDir) {
|
|
19686
|
+
const handoffPath = resolve55(assignmentDir, "handoff.md");
|
|
19687
|
+
if (!await fileExists(handoffPath)) return null;
|
|
19688
|
+
const content = await readFile37(handoffPath, "utf-8");
|
|
19689
|
+
const body = content.replace(/^---[\s\S]*?\n---\n?/, "").trim();
|
|
19690
|
+
if (body.length === 0) return null;
|
|
19691
|
+
if (/^<!--[\s\S]*-->$/.test(body)) return null;
|
|
19692
|
+
return handoffPath;
|
|
19693
|
+
}
|
|
19694
|
+
async function buildResumeOutput(cwd) {
|
|
19695
|
+
const warnings = [];
|
|
19696
|
+
const context = await readContext(cwd);
|
|
19697
|
+
if (!context) {
|
|
19698
|
+
return {
|
|
19699
|
+
ok: false,
|
|
19700
|
+
context: null,
|
|
19701
|
+
latestSession: null,
|
|
19702
|
+
openHandoff: null,
|
|
19703
|
+
warnings: [
|
|
19704
|
+
"No .syntaur/context.json in current directory. Run grab-assignment first."
|
|
19705
|
+
]
|
|
19706
|
+
};
|
|
19707
|
+
}
|
|
19708
|
+
if (!context.assignmentDir) {
|
|
19709
|
+
return {
|
|
19710
|
+
ok: false,
|
|
19711
|
+
context,
|
|
19712
|
+
latestSession: null,
|
|
19713
|
+
openHandoff: null,
|
|
19714
|
+
warnings: [
|
|
19715
|
+
"context.json present but no assignmentDir field \u2014 only a session record exists. Nothing to resume."
|
|
19716
|
+
]
|
|
19717
|
+
};
|
|
19718
|
+
}
|
|
19719
|
+
const latestSession = await findLatestSessionSummary(context.assignmentDir);
|
|
19720
|
+
if (!latestSession) {
|
|
19721
|
+
warnings.push(
|
|
19722
|
+
`No session summary found under ${context.assignmentDir}/sessions/. Run /save-session-summary in a prior session to leave a resume baton.`
|
|
19723
|
+
);
|
|
19724
|
+
}
|
|
19725
|
+
const openHandoff = await findOpenHandoff(context.assignmentDir);
|
|
19726
|
+
return {
|
|
19727
|
+
ok: true,
|
|
19728
|
+
context,
|
|
19729
|
+
latestSession: latestSession ? { sessionId: latestSession.sessionId, path: latestSession.path } : null,
|
|
19730
|
+
openHandoff,
|
|
19731
|
+
warnings
|
|
19732
|
+
};
|
|
19733
|
+
}
|
|
19734
|
+
function renderHumanOutput(out) {
|
|
19735
|
+
const lines = [];
|
|
19736
|
+
if (!out.ok) {
|
|
19737
|
+
lines.push("Cannot resume:");
|
|
19738
|
+
for (const w of out.warnings) lines.push(` - ${w}`);
|
|
19739
|
+
return lines.join("\n");
|
|
19740
|
+
}
|
|
19741
|
+
const ctx = out.context;
|
|
19742
|
+
lines.push("Resuming Syntaur session");
|
|
19743
|
+
lines.push("");
|
|
19744
|
+
lines.push(` Project: ${ctx.projectSlug ?? "(standalone)"}`);
|
|
19745
|
+
lines.push(` Assignment: ${ctx.assignmentSlug ?? "(unknown)"}`);
|
|
19746
|
+
if (ctx.title) lines.push(` Title: ${ctx.title}`);
|
|
19747
|
+
if (ctx.branch) lines.push(` Branch: ${ctx.branch}`);
|
|
19748
|
+
if (ctx.workspaceRoot) lines.push(` Workspace root: ${ctx.workspaceRoot}`);
|
|
19749
|
+
lines.push(` Assignment dir: ${ctx.assignmentDir}`);
|
|
19750
|
+
lines.push("");
|
|
19751
|
+
if (out.latestSession) {
|
|
19752
|
+
lines.push(`Latest session summary: ${out.latestSession.path}`);
|
|
19753
|
+
lines.push(`Read it next to load What\u2019s Next + Open Questions.`);
|
|
19754
|
+
} else {
|
|
19755
|
+
lines.push("No prior session summary on disk.");
|
|
19756
|
+
}
|
|
19757
|
+
if (out.openHandoff) {
|
|
19758
|
+
lines.push("");
|
|
19759
|
+
lines.push(`Open handoff: ${out.openHandoff}`);
|
|
19760
|
+
lines.push("Read it before continuing \u2014 there is an outstanding baton.");
|
|
19761
|
+
}
|
|
19762
|
+
if (out.warnings.length > 0) {
|
|
19763
|
+
lines.push("");
|
|
19764
|
+
lines.push("Warnings:");
|
|
19765
|
+
for (const w of out.warnings) lines.push(` - ${w}`);
|
|
19766
|
+
}
|
|
19767
|
+
return lines.join("\n");
|
|
19768
|
+
}
|
|
19769
|
+
async function runSessionResume(options, cwd = process.cwd()) {
|
|
19770
|
+
const out = await buildResumeOutput(cwd);
|
|
19771
|
+
if (options.json) {
|
|
19772
|
+
console.log(JSON.stringify(out, null, 2));
|
|
19773
|
+
} else {
|
|
19774
|
+
console.log(renderHumanOutput(out));
|
|
19775
|
+
}
|
|
19776
|
+
return out;
|
|
19777
|
+
}
|
|
19778
|
+
var sessionCommand = new Command7("session").description("Manage agent sessions for the active assignment");
|
|
19779
|
+
sessionCommand.command("resume").description(
|
|
19780
|
+
"Re-orient a fresh session: print active assignment context, latest saved session summary, and any open handoff. Idempotent \u2014 does not mutate state."
|
|
19781
|
+
).option("--json", "Emit machine-readable JSON instead of human-readable text").action(async (options) => {
|
|
19782
|
+
try {
|
|
19783
|
+
const out = await runSessionResume(options);
|
|
19784
|
+
if (!out.ok) process.exit(1);
|
|
19785
|
+
} catch (error) {
|
|
19786
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
19787
|
+
process.exit(1);
|
|
19788
|
+
}
|
|
19789
|
+
});
|
|
19790
|
+
|
|
19791
|
+
// src/commands/worktree.ts
|
|
19792
|
+
init_git_worktree();
|
|
19793
|
+
init_fs();
|
|
19794
|
+
init_paths();
|
|
19795
|
+
import { Command as Command8 } from "commander";
|
|
19796
|
+
import { readFile as readFile38 } from "fs/promises";
|
|
19797
|
+
import { resolve as resolve56 } from "path";
|
|
19798
|
+
async function readContext2(cwd) {
|
|
19799
|
+
const path = resolve56(cwd, ".syntaur", "context.json");
|
|
19800
|
+
if (!await fileExists(path)) return null;
|
|
19801
|
+
try {
|
|
19802
|
+
return JSON.parse(await readFile38(path, "utf-8"));
|
|
19803
|
+
} catch {
|
|
19804
|
+
return null;
|
|
19805
|
+
}
|
|
19806
|
+
}
|
|
19807
|
+
async function resolveAssignmentPath2(opts) {
|
|
19808
|
+
if (opts.assignment) {
|
|
19809
|
+
if (opts.project) {
|
|
19810
|
+
return resolve56(
|
|
19811
|
+
defaultProjectDir(),
|
|
19812
|
+
opts.project,
|
|
19813
|
+
"assignments",
|
|
19814
|
+
opts.assignment,
|
|
19815
|
+
"assignment.md"
|
|
19816
|
+
);
|
|
19817
|
+
}
|
|
19818
|
+
return resolve56(assignmentsDir(), opts.assignment, "assignment.md");
|
|
19819
|
+
}
|
|
19820
|
+
const ctx = await readContext2(opts.cwd);
|
|
19821
|
+
if (ctx?.assignmentDir) return resolve56(ctx.assignmentDir, "assignment.md");
|
|
19822
|
+
throw new Error(
|
|
19823
|
+
"No active assignment. Pass --assignment <slug> [--project <slug>] or run from a workspace with .syntaur/context.json."
|
|
19824
|
+
);
|
|
19825
|
+
}
|
|
19826
|
+
async function runWorktreeCreate(options, cwd = process.cwd()) {
|
|
19827
|
+
if (!options.branch) {
|
|
19828
|
+
throw new Error("--branch is required.");
|
|
19829
|
+
}
|
|
19830
|
+
const repository = options.repository ?? cwd;
|
|
19831
|
+
const parentBranch = options.parentBranch ?? "main";
|
|
19832
|
+
const worktreePath = options.worktreePath ?? resolve56(repository, ".worktrees", options.branch);
|
|
19833
|
+
const assignmentPath = await resolveAssignmentPath2({
|
|
19834
|
+
assignment: options.assignment,
|
|
19835
|
+
project: options.project,
|
|
19836
|
+
cwd
|
|
19837
|
+
});
|
|
19838
|
+
if (!await fileExists(assignmentPath)) {
|
|
19839
|
+
throw new Error(`Assignment file not found: ${assignmentPath}`);
|
|
19840
|
+
}
|
|
19841
|
+
await createWorktreeAndRecord({
|
|
19842
|
+
repository,
|
|
19843
|
+
branch: options.branch,
|
|
19844
|
+
worktreePath,
|
|
19845
|
+
parentBranch,
|
|
19846
|
+
assignmentPath
|
|
19847
|
+
});
|
|
19848
|
+
return { worktreePath, assignmentPath };
|
|
19849
|
+
}
|
|
19850
|
+
var worktreeCommand = new Command8("worktree").description("Manage git worktrees bound to Syntaur assignments");
|
|
19851
|
+
worktreeCommand.command("create").description(
|
|
19852
|
+
"Create a worktree at <repository>/.worktrees/<branch> and record it in the assignment workspace block. Atomic \u2014 rolls back the worktree if writing assignment.md fails."
|
|
19853
|
+
).requiredOption("--branch <name>", "Branch name to create (also used as worktree dir name)").option("--repository <path>", "Repository root (defaults to current working directory)").option("--parent-branch <name>", "Parent branch to fork from", "main").option("--assignment <slug>", "Assignment slug (UUID for standalone). Defaults to .syntaur/context.json").option("--project <slug>", "Project slug. Required when --assignment is given for a project-nested assignment").option("--worktree-path <path>", "Override the computed <repository>/.worktrees/<branch> path").action(async (options) => {
|
|
19854
|
+
try {
|
|
19855
|
+
const { worktreePath, assignmentPath } = await runWorktreeCreate(options);
|
|
19856
|
+
console.log(`Created worktree at ${worktreePath}`);
|
|
19857
|
+
console.log(`Recorded workspace fields in ${assignmentPath}`);
|
|
19858
|
+
} catch (error) {
|
|
19859
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
19860
|
+
process.exit(1);
|
|
19861
|
+
}
|
|
19862
|
+
});
|
|
19863
|
+
|
|
19864
|
+
// src/commands/resource.ts
|
|
19865
|
+
init_paths();
|
|
19866
|
+
init_fs();
|
|
19867
|
+
init_slug();
|
|
19868
|
+
import { Command as Command9 } from "commander";
|
|
19869
|
+
import { resolve as resolve58 } from "path";
|
|
19870
|
+
|
|
19871
|
+
// src/utils/project-indexes.ts
|
|
19872
|
+
init_parser();
|
|
19873
|
+
init_fs();
|
|
19874
|
+
import { readdir as readdir20, readFile as readFile39 } from "fs/promises";
|
|
19875
|
+
import { resolve as resolve57 } from "path";
|
|
19876
|
+
function nowIso() {
|
|
19877
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
19878
|
+
}
|
|
19879
|
+
function readProjectSlug(projectDir) {
|
|
19880
|
+
return projectDir.split("/").filter(Boolean).pop() ?? "";
|
|
19881
|
+
}
|
|
19882
|
+
async function listSlugFiles(dir) {
|
|
19883
|
+
if (!await fileExists(dir)) return [];
|
|
19884
|
+
const entries = await readdir20(dir, { withFileTypes: true });
|
|
19885
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".md") && !e.name.startsWith("_")).map((e) => e.name).sort();
|
|
19886
|
+
}
|
|
19887
|
+
function escapeCell(value) {
|
|
19888
|
+
return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ").trim();
|
|
19889
|
+
}
|
|
19890
|
+
function joinList(items) {
|
|
19891
|
+
return items.length === 0 ? "\u2014" : items.map(escapeCell).join(", ");
|
|
19892
|
+
}
|
|
19893
|
+
async function rebuildResourcesIndex(projectDir) {
|
|
19894
|
+
const dir = resolve57(projectDir, "resources");
|
|
19895
|
+
await ensureDir(dir);
|
|
19896
|
+
const files = await listSlugFiles(dir);
|
|
19897
|
+
const slug = readProjectSlug(projectDir);
|
|
19898
|
+
const lines = [];
|
|
19899
|
+
lines.push("---");
|
|
19900
|
+
lines.push(`project: ${slug}`);
|
|
19901
|
+
lines.push(`generated: "${nowIso()}"`);
|
|
19902
|
+
lines.push(`total: ${files.length}`);
|
|
19903
|
+
lines.push("---");
|
|
19904
|
+
lines.push("");
|
|
19905
|
+
lines.push("# Resources");
|
|
19906
|
+
lines.push("");
|
|
19907
|
+
lines.push("| Name | Category | Source | Related Assignments | Updated |");
|
|
19908
|
+
lines.push("|------|----------|--------|---------------------|---------|");
|
|
19909
|
+
for (const fileName of files) {
|
|
19910
|
+
const content = await readFile39(resolve57(dir, fileName), "utf-8");
|
|
19911
|
+
const parsed = parseResource(content);
|
|
19912
|
+
const slugBase = fileName.replace(/\.md$/, "");
|
|
19913
|
+
const name = parsed.name || slugBase;
|
|
19914
|
+
const link = `[${escapeCell(name)}](./${fileName})`;
|
|
19915
|
+
lines.push(
|
|
19916
|
+
`| ${link} | ${escapeCell(parsed.category)} | ${escapeCell(parsed.source)} | ${joinList(parsed.relatedAssignments)} | ${escapeCell(parsed.updated)} |`
|
|
19917
|
+
);
|
|
19918
|
+
}
|
|
19919
|
+
lines.push("");
|
|
19920
|
+
const indexPath = resolve57(dir, "_index.md");
|
|
19921
|
+
await writeFileForce(indexPath, lines.join("\n"));
|
|
19922
|
+
return { total: files.length, path: indexPath };
|
|
19923
|
+
}
|
|
19924
|
+
async function rebuildMemoriesIndex(projectDir) {
|
|
19925
|
+
const dir = resolve57(projectDir, "memories");
|
|
19926
|
+
await ensureDir(dir);
|
|
19927
|
+
const files = await listSlugFiles(dir);
|
|
19928
|
+
const slug = readProjectSlug(projectDir);
|
|
19929
|
+
const lines = [];
|
|
19930
|
+
lines.push("---");
|
|
19931
|
+
lines.push(`project: ${slug}`);
|
|
19932
|
+
lines.push(`generated: "${nowIso()}"`);
|
|
19933
|
+
lines.push(`total: ${files.length}`);
|
|
19934
|
+
lines.push("---");
|
|
19935
|
+
lines.push("");
|
|
19936
|
+
lines.push("# Memories");
|
|
19937
|
+
lines.push("");
|
|
19938
|
+
lines.push("| Name | Source | Scope | Source Assignment | Updated |");
|
|
19939
|
+
lines.push("|------|--------|-------|-------------------|---------|");
|
|
19940
|
+
for (const fileName of files) {
|
|
19941
|
+
const content = await readFile39(resolve57(dir, fileName), "utf-8");
|
|
19942
|
+
const parsed = parseMemory(content);
|
|
19943
|
+
const slugBase = fileName.replace(/\.md$/, "");
|
|
19944
|
+
const name = parsed.name || slugBase;
|
|
19945
|
+
const link = `[${escapeCell(name)}](./${fileName})`;
|
|
19946
|
+
lines.push(
|
|
19947
|
+
`| ${link} | ${escapeCell(parsed.source)} | ${escapeCell(parsed.scope)} | ${escapeCell(parsed.sourceAssignment ?? "\u2014")} | ${escapeCell(parsed.updated)} |`
|
|
19948
|
+
);
|
|
19949
|
+
}
|
|
19950
|
+
lines.push("");
|
|
19951
|
+
const indexPath = resolve57(dir, "_index.md");
|
|
19952
|
+
await writeFileForce(indexPath, lines.join("\n"));
|
|
19953
|
+
return { total: files.length, path: indexPath };
|
|
19954
|
+
}
|
|
19955
|
+
|
|
19956
|
+
// src/commands/resource.ts
|
|
19957
|
+
function nowIso2() {
|
|
19958
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
19959
|
+
}
|
|
19960
|
+
function parseList(value) {
|
|
19961
|
+
if (!value) return [];
|
|
19962
|
+
return value.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
|
|
19963
|
+
}
|
|
19964
|
+
function yamlQuote(value) {
|
|
19965
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
19966
|
+
}
|
|
19967
|
+
function renderResourceFile(opts) {
|
|
19968
|
+
const created = nowIso2();
|
|
19969
|
+
const related = opts.relatedAssignments.length === 0 ? "[]" : `
|
|
19970
|
+
${opts.relatedAssignments.map((a) => ` - ${a}`).join("\n")}`;
|
|
19971
|
+
return `---
|
|
19972
|
+
name: ${yamlQuote(opts.name)}
|
|
19973
|
+
category: ${yamlQuote(opts.category)}
|
|
19974
|
+
source: ${yamlQuote(opts.source)}
|
|
19975
|
+
relatedAssignments:${related}
|
|
19976
|
+
created: "${created}"
|
|
19977
|
+
updated: "${created}"
|
|
19978
|
+
---
|
|
19979
|
+
|
|
19980
|
+
# ${opts.name}
|
|
19981
|
+
|
|
19982
|
+
${opts.body ?? "<!-- Add notes about this resource here. -->"}
|
|
19983
|
+
`;
|
|
19984
|
+
}
|
|
19985
|
+
async function runResourceAdd(options) {
|
|
19986
|
+
if (!isValidSlug(options.project)) {
|
|
19987
|
+
throw new Error(`Invalid project slug: "${options.project}".`);
|
|
19988
|
+
}
|
|
19989
|
+
const projectDir = resolve58(defaultProjectDir(), options.project);
|
|
19990
|
+
if (!await fileExists(resolve58(projectDir, "project.md"))) {
|
|
19991
|
+
throw new Error(`Project "${options.project}" not found at ${projectDir}.`);
|
|
19992
|
+
}
|
|
19993
|
+
if (!options.name) throw new Error("--name is required.");
|
|
19994
|
+
if (!options.source) throw new Error("--source is required.");
|
|
19995
|
+
const slug = options.slug ?? slugify(options.name);
|
|
19996
|
+
if (!isValidSlug(slug)) {
|
|
19997
|
+
throw new Error(`Invalid resource slug: "${slug}".`);
|
|
19998
|
+
}
|
|
19999
|
+
const filePath = resolve58(projectDir, "resources", `${slug}.md`);
|
|
20000
|
+
if (await fileExists(filePath) && !options.force) {
|
|
20001
|
+
throw new Error(
|
|
20002
|
+
`Resource "${slug}" already exists at ${filePath}. Use --force to overwrite.`
|
|
20003
|
+
);
|
|
20004
|
+
}
|
|
20005
|
+
const content = renderResourceFile({
|
|
20006
|
+
name: options.name,
|
|
20007
|
+
source: options.source,
|
|
20008
|
+
category: options.category ?? "",
|
|
20009
|
+
relatedAssignments: parseList(options.relatedAssignments)
|
|
20010
|
+
});
|
|
20011
|
+
await writeFileForce(filePath, content);
|
|
20012
|
+
const { path: indexPath, total } = await rebuildResourcesIndex(projectDir);
|
|
20013
|
+
return { filePath, indexPath, total };
|
|
20014
|
+
}
|
|
20015
|
+
var resourceCommand = new Command9("resource").description("Manage project-level resource entries");
|
|
20016
|
+
resourceCommand.command("add").description(
|
|
20017
|
+
"Add a project resource at <projectDir>/resources/<slug>.md and regenerate _index.md"
|
|
20018
|
+
).requiredOption("--project <slug>", "Project slug").requiredOption("--name <name>", "Human-readable resource name").requiredOption("--source <url-or-path>", "Resource source (URL or path)").option("--category <name>", "Optional category (e.g. dashboard, doc, ticket)").option("--slug <slug>", "Override the slug (default: slugify(--name))").option("--related-assignments <slugs>", "Comma-separated related assignment slugs").option("--force", "Overwrite if the resource file already exists").action(async (options) => {
|
|
20019
|
+
try {
|
|
20020
|
+
const { filePath, indexPath, total } = await runResourceAdd(options);
|
|
20021
|
+
console.log(`Wrote ${filePath}`);
|
|
20022
|
+
console.log(`Rebuilt ${indexPath} (${total} total resources).`);
|
|
20023
|
+
} catch (error) {
|
|
20024
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
20025
|
+
process.exit(1);
|
|
20026
|
+
}
|
|
20027
|
+
});
|
|
20028
|
+
|
|
20029
|
+
// src/commands/memory.ts
|
|
20030
|
+
init_paths();
|
|
20031
|
+
init_fs();
|
|
20032
|
+
init_slug();
|
|
20033
|
+
import { Command as Command10 } from "commander";
|
|
20034
|
+
import { resolve as resolve59 } from "path";
|
|
20035
|
+
function nowIso3() {
|
|
20036
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
20037
|
+
}
|
|
20038
|
+
function parseList2(value) {
|
|
20039
|
+
if (!value) return [];
|
|
20040
|
+
return value.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
|
|
20041
|
+
}
|
|
20042
|
+
function yamlQuote2(value) {
|
|
20043
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
20044
|
+
}
|
|
20045
|
+
function renderMemoryFile(opts) {
|
|
20046
|
+
const created = nowIso3();
|
|
20047
|
+
const related = opts.relatedAssignments.length === 0 ? "[]" : `
|
|
20048
|
+
${opts.relatedAssignments.map((a) => ` - ${a}`).join("\n")}`;
|
|
20049
|
+
const sourceAssignment = opts.sourceAssignment === null ? "null" : yamlQuote2(opts.sourceAssignment);
|
|
20050
|
+
return `---
|
|
20051
|
+
name: ${yamlQuote2(opts.name)}
|
|
20052
|
+
source: ${yamlQuote2(opts.source)}
|
|
20053
|
+
scope: ${yamlQuote2(opts.scope)}
|
|
20054
|
+
sourceAssignment: ${sourceAssignment}
|
|
20055
|
+
relatedAssignments:${related}
|
|
20056
|
+
created: "${created}"
|
|
20057
|
+
updated: "${created}"
|
|
20058
|
+
---
|
|
20059
|
+
|
|
20060
|
+
# ${opts.name}
|
|
20061
|
+
|
|
20062
|
+
${opts.body ?? "<!-- Capture the load-bearing context for this memory here. -->"}
|
|
20063
|
+
`;
|
|
20064
|
+
}
|
|
20065
|
+
async function runMemoryAdd(options) {
|
|
20066
|
+
if (!isValidSlug(options.project)) {
|
|
20067
|
+
throw new Error(`Invalid project slug: "${options.project}".`);
|
|
20068
|
+
}
|
|
20069
|
+
const projectDir = resolve59(defaultProjectDir(), options.project);
|
|
20070
|
+
if (!await fileExists(resolve59(projectDir, "project.md"))) {
|
|
20071
|
+
throw new Error(`Project "${options.project}" not found at ${projectDir}.`);
|
|
20072
|
+
}
|
|
20073
|
+
if (!options.name) throw new Error("--name is required.");
|
|
20074
|
+
if (!options.source) throw new Error("--source is required.");
|
|
20075
|
+
const slug = options.slug ?? slugify(options.name);
|
|
20076
|
+
if (!isValidSlug(slug)) {
|
|
20077
|
+
throw new Error(`Invalid memory slug: "${slug}".`);
|
|
20078
|
+
}
|
|
20079
|
+
const filePath = resolve59(projectDir, "memories", `${slug}.md`);
|
|
20080
|
+
if (await fileExists(filePath) && !options.force) {
|
|
20081
|
+
throw new Error(
|
|
20082
|
+
`Memory "${slug}" already exists at ${filePath}. Use --force to overwrite.`
|
|
20083
|
+
);
|
|
20084
|
+
}
|
|
20085
|
+
const content = renderMemoryFile({
|
|
20086
|
+
name: options.name,
|
|
20087
|
+
source: options.source,
|
|
20088
|
+
scope: options.scope ?? "project",
|
|
20089
|
+
sourceAssignment: options.sourceAssignment ?? null,
|
|
20090
|
+
relatedAssignments: parseList2(options.relatedAssignments)
|
|
20091
|
+
});
|
|
20092
|
+
await writeFileForce(filePath, content);
|
|
20093
|
+
const { path: indexPath, total } = await rebuildMemoriesIndex(projectDir);
|
|
20094
|
+
return { filePath, indexPath, total };
|
|
20095
|
+
}
|
|
20096
|
+
var memoryCommand = new Command10("memory").description("Manage project-level memory entries");
|
|
20097
|
+
memoryCommand.command("add").description(
|
|
20098
|
+
"Add a project memory at <projectDir>/memories/<slug>.md and regenerate _index.md"
|
|
20099
|
+
).requiredOption("--project <slug>", "Project slug").requiredOption("--name <name>", "Human-readable memory name").requiredOption("--source <text>", "Where this memory came from (conversation, decision, doc URL, etc.)").option("--scope <scope>", "Memory scope (default: project)", "project").option("--source-assignment <slug>", "Assignment slug this memory was captured during").option("--slug <slug>", "Override the slug (default: slugify(--name))").option("--related-assignments <slugs>", "Comma-separated related assignment slugs").option("--force", "Overwrite if the memory file already exists").action(async (options) => {
|
|
20100
|
+
try {
|
|
20101
|
+
const { filePath, indexPath, total } = await runMemoryAdd(options);
|
|
20102
|
+
console.log(`Wrote ${filePath}`);
|
|
20103
|
+
console.log(`Rebuilt ${indexPath} (${total} total memories).`);
|
|
20104
|
+
} catch (error) {
|
|
20105
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
20106
|
+
process.exit(1);
|
|
20107
|
+
}
|
|
20108
|
+
});
|
|
20109
|
+
|
|
20110
|
+
// src/commands/ls.ts
|
|
20111
|
+
init_api();
|
|
20112
|
+
init_paths();
|
|
20113
|
+
init_fs();
|
|
20114
|
+
init_frontmatter();
|
|
20115
|
+
import { Command as Command11 } from "commander";
|
|
20116
|
+
import { readFile as readFile40 } from "fs/promises";
|
|
20117
|
+
import { resolve as resolve60 } from "path";
|
|
20118
|
+
var AGE_PATTERN = /^(\d+)([dhwm])$/i;
|
|
20119
|
+
function parseAgeToCutoff(age) {
|
|
20120
|
+
const match = age.match(AGE_PATTERN);
|
|
20121
|
+
if (!match) {
|
|
20122
|
+
throw new Error(
|
|
20123
|
+
`Invalid --age value: "${age}". Use formats like 7d, 24h, 2w, 1m.`
|
|
20124
|
+
);
|
|
20125
|
+
}
|
|
20126
|
+
const n = parseInt(match[1], 10);
|
|
20127
|
+
const unit = match[2].toLowerCase();
|
|
20128
|
+
const ms = unit === "h" ? n * 60 * 60 * 1e3 : unit === "d" ? n * 24 * 60 * 60 * 1e3 : unit === "w" ? n * 7 * 24 * 60 * 60 * 1e3 : (
|
|
20129
|
+
/* m: months ≈ 30 days */
|
|
20130
|
+
n * 30 * 24 * 60 * 60 * 1e3
|
|
20131
|
+
);
|
|
20132
|
+
return new Date(Date.now() - ms);
|
|
20133
|
+
}
|
|
20134
|
+
function assignmentMdPath(item) {
|
|
20135
|
+
if (item.projectSlug) {
|
|
20136
|
+
return resolve60(
|
|
20137
|
+
defaultProjectDir(),
|
|
20138
|
+
item.projectSlug,
|
|
20139
|
+
"assignments",
|
|
20140
|
+
item.slug,
|
|
20141
|
+
"assignment.md"
|
|
20142
|
+
);
|
|
20143
|
+
}
|
|
20144
|
+
return resolve60(assignmentsDir(), item.id, "assignment.md");
|
|
20145
|
+
}
|
|
20146
|
+
async function loadTags(item) {
|
|
20147
|
+
const path = assignmentMdPath(item);
|
|
20148
|
+
if (!await fileExists(path)) return [];
|
|
20149
|
+
try {
|
|
20150
|
+
const content = await readFile40(path, "utf-8");
|
|
20151
|
+
return parseAssignmentFrontmatter(content).tags;
|
|
20152
|
+
} catch {
|
|
20153
|
+
return [];
|
|
20154
|
+
}
|
|
20155
|
+
}
|
|
20156
|
+
async function runLs(options) {
|
|
20157
|
+
const board = await listAssignmentsBoard(
|
|
20158
|
+
defaultProjectDir(),
|
|
20159
|
+
assignmentsDir()
|
|
20160
|
+
);
|
|
20161
|
+
let items = board.assignments;
|
|
20162
|
+
if (options.status) {
|
|
20163
|
+
const statuses = options.status.split(",").map((s) => s.trim()).filter(Boolean);
|
|
20164
|
+
items = items.filter((a) => statuses.includes(a.status));
|
|
20165
|
+
}
|
|
20166
|
+
if (options.project) {
|
|
20167
|
+
items = items.filter((a) => a.projectSlug === options.project);
|
|
20168
|
+
}
|
|
20169
|
+
if (options.age) {
|
|
20170
|
+
const cutoff = parseAgeToCutoff(options.age);
|
|
20171
|
+
items = items.filter((a) => {
|
|
20172
|
+
const updatedTs = Date.parse(a.updated);
|
|
20173
|
+
return Number.isFinite(updatedTs) && updatedTs >= cutoff.getTime();
|
|
20174
|
+
});
|
|
20175
|
+
}
|
|
20176
|
+
if (options.tag) {
|
|
20177
|
+
const wanted = options.tag.split(",").map((s) => s.trim()).filter(Boolean);
|
|
20178
|
+
const tagged = await Promise.all(
|
|
20179
|
+
items.map(async (a) => {
|
|
20180
|
+
const tags = await loadTags(a);
|
|
20181
|
+
return { item: a, tags };
|
|
20182
|
+
})
|
|
20183
|
+
);
|
|
20184
|
+
items = tagged.filter(({ tags }) => wanted.every((t) => tags.includes(t))).map(({ item }) => item);
|
|
20185
|
+
}
|
|
20186
|
+
return { items };
|
|
20187
|
+
}
|
|
20188
|
+
function pad(value, width) {
|
|
20189
|
+
if (value.length >= width) return value.slice(0, width);
|
|
20190
|
+
return value + " ".repeat(width - value.length);
|
|
20191
|
+
}
|
|
20192
|
+
function renderTable(items) {
|
|
20193
|
+
if (items.length === 0) return "No assignments matched.";
|
|
20194
|
+
const rows = items.map((a) => [
|
|
20195
|
+
a.projectSlug ?? "(standalone)",
|
|
20196
|
+
a.slug,
|
|
20197
|
+
a.status,
|
|
20198
|
+
a.priority,
|
|
20199
|
+
a.assignee ?? "\u2014",
|
|
20200
|
+
a.updated.slice(0, 10),
|
|
20201
|
+
a.title
|
|
20202
|
+
]);
|
|
20203
|
+
const header = ["PROJECT", "SLUG", "STATUS", "PRIORITY", "ASSIGNEE", "UPDATED", "TITLE"];
|
|
20204
|
+
const all = [header, ...rows];
|
|
20205
|
+
const widths = header.map(
|
|
20206
|
+
(_, c2) => Math.min(60, Math.max(...all.map((row) => row[c2]?.length ?? 0)))
|
|
20207
|
+
);
|
|
20208
|
+
return all.map((row) => row.map((cell, c2) => pad(cell ?? "", widths[c2])).join(" ")).join("\n");
|
|
20209
|
+
}
|
|
20210
|
+
var lsCommand = new Command11("ls").description(
|
|
20211
|
+
"List assignments across all projects with optional filters by status, project, tag, or age."
|
|
20212
|
+
).option("--status <list>", "Comma-separated status filter (e.g. pending,in_progress)").option("--project <slug>", "Filter to one project").option("--tag <list>", "Comma-separated tag filter (assignment must have ALL tags)").option("--age <duration>", "Only include assignments updated within duration (e.g. 7d, 24h, 2w, 1m)").option("--json", "Emit JSON instead of a table").action(async (options) => {
|
|
20213
|
+
try {
|
|
20214
|
+
const { items } = await runLs(options);
|
|
20215
|
+
if (options.json) {
|
|
20216
|
+
console.log(JSON.stringify({ assignments: items }, null, 2));
|
|
20217
|
+
} else {
|
|
20218
|
+
console.log(renderTable(items));
|
|
20219
|
+
}
|
|
20220
|
+
} catch (error) {
|
|
20221
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
20222
|
+
process.exit(1);
|
|
20223
|
+
}
|
|
20224
|
+
});
|
|
20225
|
+
|
|
20226
|
+
// src/cli-default-command.ts
|
|
20227
|
+
init_config2();
|
|
20228
|
+
import { readdir as readdir21 } from "fs/promises";
|
|
20229
|
+
async function hasAnyProjectContent(projectsDir2) {
|
|
20230
|
+
try {
|
|
20231
|
+
const entries = await readdir21(projectsDir2, { withFileTypes: true });
|
|
20232
|
+
return entries.some((entry) => entry.isDirectory());
|
|
20233
|
+
} catch {
|
|
20234
|
+
return false;
|
|
20235
|
+
}
|
|
20236
|
+
}
|
|
20237
|
+
async function isSetupComplete() {
|
|
20238
|
+
const config = await readConfig();
|
|
20239
|
+
if (config.onboarding.completed) {
|
|
20240
|
+
return true;
|
|
20241
|
+
}
|
|
20242
|
+
const [
|
|
20243
|
+
claudePluginDir,
|
|
20244
|
+
codexPluginDir,
|
|
20245
|
+
codexMarketplacePath,
|
|
20246
|
+
hasProjectContent
|
|
20247
|
+
] = await Promise.all([
|
|
18279
20248
|
getConfiguredOrLegacyManagedPluginDir("claude"),
|
|
18280
20249
|
getConfiguredOrLegacyManagedPluginDir("codex"),
|
|
18281
20250
|
getConfiguredOrLegacyMarketplacePath(),
|
|
@@ -18296,20 +20265,20 @@ async function getDefaultCommandName() {
|
|
|
18296
20265
|
init_paths();
|
|
18297
20266
|
init_fs();
|
|
18298
20267
|
import { fileURLToPath as fileURLToPath8 } from "url";
|
|
18299
|
-
import { readFile as
|
|
18300
|
-
import { dirname as dirname15, join as join8, resolve as
|
|
20268
|
+
import { readFile as readFile42 } from "fs/promises";
|
|
20269
|
+
import { dirname as dirname15, join as join8, resolve as resolve61 } from "path";
|
|
18301
20270
|
import { spawn as spawn4 } from "child_process";
|
|
18302
20271
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
18303
20272
|
|
|
18304
20273
|
// src/utils/version.ts
|
|
18305
20274
|
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
18306
|
-
import { readFile as
|
|
20275
|
+
import { readFile as readFile41 } from "fs/promises";
|
|
18307
20276
|
import { dirname as dirname14, join as join7 } from "path";
|
|
18308
20277
|
async function readPackageVersion(scriptUrl) {
|
|
18309
20278
|
try {
|
|
18310
20279
|
const scriptPath = fileURLToPath7(scriptUrl);
|
|
18311
20280
|
const pkgRoot = dirname14(dirname14(scriptPath));
|
|
18312
|
-
const raw = await
|
|
20281
|
+
const raw = await readFile41(join7(pkgRoot, "package.json"), "utf-8");
|
|
18313
20282
|
const parsed = JSON.parse(raw);
|
|
18314
20283
|
return typeof parsed.version === "string" ? parsed.version : null;
|
|
18315
20284
|
} catch {
|
|
@@ -18318,7 +20287,7 @@ async function readPackageVersion(scriptUrl) {
|
|
|
18318
20287
|
}
|
|
18319
20288
|
|
|
18320
20289
|
// src/utils/npx-prompt.ts
|
|
18321
|
-
var STATE_FILE =
|
|
20290
|
+
var STATE_FILE = resolve61(syntaurRoot(), "npx-install.json");
|
|
18322
20291
|
var META_ARGS = /* @__PURE__ */ new Set(["-h", "--help", "-V", "--version", "help"]);
|
|
18323
20292
|
var GLOBAL_VERSION_TIMEOUT_MS = 2e3;
|
|
18324
20293
|
function isRunningViaNpx(scriptUrl) {
|
|
@@ -18339,7 +20308,7 @@ function isRunningViaNpx(scriptUrl) {
|
|
|
18339
20308
|
async function readState() {
|
|
18340
20309
|
if (!await fileExists(STATE_FILE)) return null;
|
|
18341
20310
|
try {
|
|
18342
|
-
const raw = await
|
|
20311
|
+
const raw = await readFile42(STATE_FILE, "utf-8");
|
|
18343
20312
|
return JSON.parse(raw);
|
|
18344
20313
|
} catch {
|
|
18345
20314
|
return null;
|
|
@@ -18398,7 +20367,7 @@ async function readGlobalVersion() {
|
|
|
18398
20367
|
try {
|
|
18399
20368
|
const manifestPath = join8(rootPath, "syntaur", "package.json");
|
|
18400
20369
|
if (!await fileExists(manifestPath)) return null;
|
|
18401
|
-
const raw = await
|
|
20370
|
+
const raw = await readFile42(manifestPath, "utf-8");
|
|
18402
20371
|
const parsed = JSON.parse(raw);
|
|
18403
20372
|
return typeof parsed.version === "string" ? parsed.version : null;
|
|
18404
20373
|
} catch {
|
|
@@ -18517,7 +20486,7 @@ async function maybePromptInstall(scriptUrl) {
|
|
|
18517
20486
|
|
|
18518
20487
|
// src/index.ts
|
|
18519
20488
|
await maybePromptInstall(import.meta.url);
|
|
18520
|
-
var program = new
|
|
20489
|
+
var program = new Command12();
|
|
18521
20490
|
var version = await readPackageVersion(import.meta.url) ?? "0.0.0";
|
|
18522
20491
|
program.name("syntaur").description("CLI scaffolding tool for the Syntaur protocol").version(version);
|
|
18523
20492
|
program.command("init").description("Initialize ~/.syntaur/ directory structure and config").option("--force", "Overwrite existing config file").action(async (options) => {
|
|
@@ -18568,6 +20537,17 @@ program.command("comment").description("Add a comment to an assignment (CLI-medi
|
|
|
18568
20537
|
process.exit(1);
|
|
18569
20538
|
}
|
|
18570
20539
|
});
|
|
20540
|
+
program.command("capture").description("Capture a typed proof artifact for an assignment").argument("[target]", "Assignment slug (with --project) or UUID; falls back to .syntaur/context.json").option("--kind <type>", "Artifact kind: screenshot | video | asciinema | http | text").option("--file <path>", "Source file to ingest (forbidden for --kind=text)").option("--criterion <index>", "Optional 0-based criterion index to tag").option("--note <text>", "Optional note (required for --kind=text)").option("--project <slug>", "Project slug if the target is project-nested").option("--dir <path>", "Override default project directory").action(async (target, options) => {
|
|
20541
|
+
try {
|
|
20542
|
+
await captureCommand(target, options);
|
|
20543
|
+
} catch (error) {
|
|
20544
|
+
console.error(
|
|
20545
|
+
"Error:",
|
|
20546
|
+
error instanceof Error ? error.message : String(error)
|
|
20547
|
+
);
|
|
20548
|
+
process.exit(1);
|
|
20549
|
+
}
|
|
20550
|
+
});
|
|
18571
20551
|
program.command("request").description("Append a todo to another assignment (cross-assignment work request)").argument("<target>", "Target assignment slug (with --project) or UUID (standalone)").argument("<text>", "Todo text").option("--project <slug>", "Project slug if the target is project-nested").option("--from <source>", "Source assignment (default: $SYNTAUR_ASSIGNMENT)").option("--dir <path>", "Override default project directory").action(async (target, text, options) => {
|
|
18572
20552
|
try {
|
|
18573
20553
|
await requestCommand(target, text, options);
|
|
@@ -18878,6 +20858,13 @@ program.addCommand(todoCommand);
|
|
|
18878
20858
|
program.addCommand(backupCommand);
|
|
18879
20859
|
program.addCommand(doctorCommand);
|
|
18880
20860
|
program.addCommand(agentsCommand);
|
|
20861
|
+
program.addCommand(proofCommand);
|
|
20862
|
+
program.addCommand(planCommand);
|
|
20863
|
+
program.addCommand(sessionCommand);
|
|
20864
|
+
program.addCommand(worktreeCommand);
|
|
20865
|
+
program.addCommand(resourceCommand);
|
|
20866
|
+
program.addCommand(memoryCommand);
|
|
20867
|
+
program.addCommand(lsCommand);
|
|
18881
20868
|
if (process.argv.length <= 2) {
|
|
18882
20869
|
process.argv.push(await getDefaultCommandName());
|
|
18883
20870
|
}
|