syntaur 0.41.2 → 0.42.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 +1 -1
- package/dashboard/dist/assets/{_basePickBy-CMGil-NY.js → _basePickBy-Cd0RkcLT.js} +1 -1
- package/dashboard/dist/assets/{_baseUniq-DllyUaEJ.js → _baseUniq-DcVRMSTl.js} +1 -1
- package/dashboard/dist/assets/{arc-C6fNP_LJ.js → arc-B2m30WX5.js} +1 -1
- package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-CxXDnbMY.js → architectureDiagram-2XIMDMQ5-CJdPqqWS.js} +1 -1
- package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-B8UDJhxg.js → blockDiagram-WCTKOSBZ-BoThc9ue.js} +1 -1
- package/dashboard/dist/assets/{c4Diagram-IC4MRINW-9XDZP3AD.js → c4Diagram-IC4MRINW-DcLdX7Gp.js} +1 -1
- package/dashboard/dist/assets/channel-TK3AY7tt.js +1 -0
- package/dashboard/dist/assets/{chunk-4BX2VUAB-D1LR7D9Y.js → chunk-4BX2VUAB-DeyTroVn.js} +1 -1
- package/dashboard/dist/assets/{chunk-55IACEB6-sumE5d0X.js → chunk-55IACEB6-Pk1kQHZ3.js} +1 -1
- package/dashboard/dist/assets/{chunk-FMBD7UC4-C-Iy8wke.js → chunk-FMBD7UC4-DFoS6k4t.js} +1 -1
- package/dashboard/dist/assets/{chunk-JSJVCQXG-Clyrcmzt.js → chunk-JSJVCQXG-DN22e0xM.js} +1 -1
- package/dashboard/dist/assets/{chunk-KX2RTZJC-BQqetgrP.js → chunk-KX2RTZJC-MNrdiNWF.js} +1 -1
- package/dashboard/dist/assets/{chunk-NQ4KR5QH-Cw60fnx2.js → chunk-NQ4KR5QH-C0k2CIP7.js} +1 -1
- package/dashboard/dist/assets/{chunk-QZHKN3VN-Dv40SU2-.js → chunk-QZHKN3VN-C32xUlPx.js} +1 -1
- package/dashboard/dist/assets/{chunk-WL4C6EOR-DFiOufrs.js → chunk-WL4C6EOR-DB7YEwdA.js} +1 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-BcpVoyRF.js +1 -0
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BcpVoyRF.js +1 -0
- package/dashboard/dist/assets/clone-tv-jxopI.js +1 -0
- package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-DV306SRn.js → cose-bilkent-S5V4N54A-CMH4iDpK.js} +1 -1
- package/dashboard/dist/assets/{dagre-KLK3FWXG-DaQ1pWLV.js → dagre-KLK3FWXG-DdHflfb4.js} +1 -1
- package/dashboard/dist/assets/{diagram-E7M64L7V-2fsjMT-T.js → diagram-E7M64L7V-DtScFCCN.js} +1 -1
- package/dashboard/dist/assets/{diagram-IFDJBPK2-CoaSyKLw.js → diagram-IFDJBPK2-DqZgC_98.js} +1 -1
- package/dashboard/dist/assets/{diagram-P4PSJMXO-C_j6Kd6q.js → diagram-P4PSJMXO-BIjzlVHf.js} +1 -1
- package/dashboard/dist/assets/{erDiagram-INFDFZHY-CpOdYJWS.js → erDiagram-INFDFZHY-B_v2XAqY.js} +1 -1
- package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-KVRjmhbG.js → flowDiagram-PKNHOUZH-BIsbt9TK.js} +1 -1
- package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-CA_n5ynk.js → ganttDiagram-A5KZAMGK-3wW3k6UM.js} +1 -1
- package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-DKkS_iH8.js → gitGraphDiagram-K3NZZRJ6-J5jG9Qum.js} +1 -1
- package/dashboard/dist/assets/{graph-C6ehraTW.js → graph-6IHp6W8J.js} +1 -1
- package/dashboard/dist/assets/{index-CdHziP5R.css → index-6uihSopA.css} +1 -1
- package/dashboard/dist/assets/{index-SW4WrQLg.js → index-BfWuhZd9.js} +59 -59
- package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-H1Eg4YK9.js → infoDiagram-LFFYTUFH-BeUnIF7J.js} +1 -1
- package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-DSrc4sub.js → ishikawaDiagram-PHBUUO56-CBMlrqiK.js} +1 -1
- package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-Bl_0LgIo.js → journeyDiagram-4ABVD52K-C25hSiks.js} +1 -1
- package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-Cq2WGyif.js → kanban-definition-K7BYSVSG-DoJVBKAf.js} +1 -1
- package/dashboard/dist/assets/{layout-DJv9vite.js → layout-rmqkK4ql.js} +1 -1
- package/dashboard/dist/assets/{linear-CAef3hQD.js → linear-Dcvh5pG3.js} +1 -1
- package/dashboard/dist/assets/{mermaid.core-B_gAmtAa.js → mermaid.core-BAS-3wuz.js} +4 -4
- package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-4aIWu_CK.js → mindmap-definition-YRQLILUH-Dvs67C76.js} +1 -1
- package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-1ThATMqf.js → pieDiagram-SKSYHLDU-DsVBwJs_.js} +1 -1
- package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-BEq2jVyN.js → quadrantDiagram-337W2JSQ-C5dAkCkr.js} +1 -1
- package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-DbYJrAQ9.js → requirementDiagram-Z7DCOOCP-DEqcg6A2.js} +1 -1
- package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DMr3kn8l.js → sankeyDiagram-WA2Y5GQK-CvrgIFyY.js} +1 -1
- package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-BR03-l-y.js → sequenceDiagram-2WXFIKYE-Bu6tanJS.js} +1 -1
- package/dashboard/dist/assets/{stateDiagram-RAJIS63D-DUj-dVll.js → stateDiagram-RAJIS63D-D16mva7g.js} +1 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-XMWsEM8j.js +1 -0
- package/dashboard/dist/assets/{timeline-definition-YZTLITO2-DpN8jElm.js → timeline-definition-YZTLITO2-BcLXDlbF.js} +1 -1
- package/dashboard/dist/assets/{treemap-KZPCXAKY-CyUTDKiM.js → treemap-KZPCXAKY-BuA9iiXV.js} +1 -1
- package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-DRJFiQmT.js → vennDiagram-LZ73GAT5-CYNPHeLe.js} +1 -1
- package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-DcrZVnQ-.js → xychartDiagram-JWTSCODW-BJ4lD-Yr.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/dashboard/server.js +2412 -420
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +3783 -966
- package/dist/index.js.map +1 -1
- package/dist/launch/index.d.ts +33 -0
- package/dist/launch/index.js +1949 -70
- package/dist/launch/index.js.map +1 -1
- package/package.json +1 -1
- package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
- package/platforms/codex/.codex-plugin/plugin.json +1 -1
- package/platforms/hermes/plugins/syntaur/__pycache__/__init__.cpython-312.pyc +0 -0
- package/platforms/hermes/plugins/syntaur/__pycache__/boundary.cpython-312.pyc +0 -0
- package/dashboard/dist/assets/channel-OsoeK3Lk.js +0 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-BKX6nUBp.js +0 -1
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BKX6nUBp.js +0 -1
- package/dashboard/dist/assets/clone-f-TTh9ms.js +0 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-Dzzbhq6b.js +0 -1
package/dist/dashboard/server.js
CHANGED
|
@@ -88,7 +88,7 @@ var init_paths = __esm({
|
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
// src/lifecycle/types.ts
|
|
91
|
-
var DEFAULT_STATUSES;
|
|
91
|
+
var DEFAULT_STATUSES, DEFAULT_TERMINAL_STATUSES;
|
|
92
92
|
var init_types = __esm({
|
|
93
93
|
"src/lifecycle/types.ts"() {
|
|
94
94
|
"use strict";
|
|
@@ -103,6 +103,10 @@ var init_types = __esm({
|
|
|
103
103
|
"completed",
|
|
104
104
|
"failed"
|
|
105
105
|
];
|
|
106
|
+
DEFAULT_TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
107
|
+
"completed",
|
|
108
|
+
"failed"
|
|
109
|
+
]);
|
|
106
110
|
}
|
|
107
111
|
});
|
|
108
112
|
|
|
@@ -160,6 +164,17 @@ var init_state_machine = __esm({
|
|
|
160
164
|
});
|
|
161
165
|
|
|
162
166
|
// src/lifecycle/frontmatter.ts
|
|
167
|
+
var frontmatter_exports = {};
|
|
168
|
+
__export(frontmatter_exports, {
|
|
169
|
+
appendStatusHistoryEntry: () => appendStatusHistoryEntry,
|
|
170
|
+
parseAssignmentFrontmatter: () => parseAssignmentFrontmatter,
|
|
171
|
+
renameStatusInHistory: () => renameStatusInHistory,
|
|
172
|
+
updateAssignmentFile: () => updateAssignmentFile,
|
|
173
|
+
updateAssignmentWorkspace: () => updateAssignmentWorkspace,
|
|
174
|
+
updateNestedBlock: () => updateNestedBlock,
|
|
175
|
+
updateOverride: () => updateOverride,
|
|
176
|
+
updatePlanApproval: () => updatePlanApproval
|
|
177
|
+
});
|
|
163
178
|
function extractFrontmatter(fileContent) {
|
|
164
179
|
const match = fileContent.match(/^---\n([\s\S]*?)\n---/);
|
|
165
180
|
if (!match) {
|
|
@@ -172,7 +187,10 @@ function extractFrontmatter(fileContent) {
|
|
|
172
187
|
function parseSimpleValue(raw2) {
|
|
173
188
|
const trimmed = raw2.trim();
|
|
174
189
|
if (trimmed === "null" || trimmed === "~" || trimmed === "") return null;
|
|
175
|
-
if (trimmed.startsWith('"') && trimmed.endsWith('"')
|
|
190
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
|
|
191
|
+
return trimmed.slice(1, -1).replace(/\\(["\\])/g, "$1");
|
|
192
|
+
}
|
|
193
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2) {
|
|
176
194
|
return trimmed.slice(1, -1);
|
|
177
195
|
}
|
|
178
196
|
return trimmed;
|
|
@@ -232,6 +250,89 @@ function parseExternalIds(frontmatter) {
|
|
|
232
250
|
}
|
|
233
251
|
return results;
|
|
234
252
|
}
|
|
253
|
+
function parseStatusHistory(frontmatter) {
|
|
254
|
+
if (/^statusHistory:\s*\[\s*\]/m.test(frontmatter)) return [];
|
|
255
|
+
const headerMatch = frontmatter.match(/^statusHistory:\s*$/m);
|
|
256
|
+
if (!headerMatch) return [];
|
|
257
|
+
const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);
|
|
258
|
+
const bodyStart = headerStart + headerMatch[0].length + 1;
|
|
259
|
+
const after = frontmatter.slice(bodyStart);
|
|
260
|
+
const bodyLines = [];
|
|
261
|
+
for (const line of after.split("\n")) {
|
|
262
|
+
if (line.length === 0) {
|
|
263
|
+
bodyLines.push(line);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (line[0] !== " " && line[0] !== " ") break;
|
|
267
|
+
bodyLines.push(line);
|
|
268
|
+
}
|
|
269
|
+
const body = bodyLines.join("\n");
|
|
270
|
+
const results = [];
|
|
271
|
+
const itemBlocks = body.split(/\n\s+-\s+/).filter((b) => b.trim().length > 0);
|
|
272
|
+
for (const block of itemBlocks) {
|
|
273
|
+
const entry = {};
|
|
274
|
+
for (const line of block.split("\n")) {
|
|
275
|
+
const colonIdx = line.indexOf(":");
|
|
276
|
+
if (colonIdx < 0) continue;
|
|
277
|
+
const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
|
|
278
|
+
if (!key) continue;
|
|
279
|
+
entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
|
|
280
|
+
}
|
|
281
|
+
if (!entry["to"]) continue;
|
|
282
|
+
const result = {
|
|
283
|
+
at: entry["at"] ?? "",
|
|
284
|
+
from: entry["from"] ?? null,
|
|
285
|
+
to: entry["to"],
|
|
286
|
+
command: entry["command"] ?? "",
|
|
287
|
+
by: entry["by"] ?? null
|
|
288
|
+
};
|
|
289
|
+
if (entry["reason"] != null) result.reason = entry["reason"];
|
|
290
|
+
if ("phaseFrom" in entry) result.phaseFrom = entry["phaseFrom"];
|
|
291
|
+
if ("phaseTo" in entry) result.phaseTo = entry["phaseTo"];
|
|
292
|
+
if ("dispositionFrom" in entry) result.dispositionFrom = entry["dispositionFrom"];
|
|
293
|
+
if ("dispositionTo" in entry) result.dispositionTo = entry["dispositionTo"];
|
|
294
|
+
results.push(result);
|
|
295
|
+
}
|
|
296
|
+
return results;
|
|
297
|
+
}
|
|
298
|
+
function parseNestedBlock(frontmatter, header) {
|
|
299
|
+
if (new RegExp(`^${header}:\\s*(null|~)\\s*$`, "m").test(frontmatter)) return null;
|
|
300
|
+
const headerMatch = frontmatter.match(new RegExp(`^${header}:\\s*$`, "m"));
|
|
301
|
+
if (!headerMatch) return null;
|
|
302
|
+
const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);
|
|
303
|
+
const after = frontmatter.slice(headerStart + headerMatch[0].length + 1);
|
|
304
|
+
const out = {};
|
|
305
|
+
for (const line of after.split("\n")) {
|
|
306
|
+
if (line.length === 0) continue;
|
|
307
|
+
if (line[0] !== " " && line[0] !== " ") break;
|
|
308
|
+
const colonIdx = line.indexOf(":");
|
|
309
|
+
if (colonIdx < 0) continue;
|
|
310
|
+
const key = line.slice(0, colonIdx).trim();
|
|
311
|
+
if (!key) continue;
|
|
312
|
+
out[key] = parseSimpleValue(line.slice(colonIdx + 1));
|
|
313
|
+
}
|
|
314
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
315
|
+
}
|
|
316
|
+
function parsePlanApproval(frontmatter) {
|
|
317
|
+
const block = parseNestedBlock(frontmatter, "planApproval");
|
|
318
|
+
if (!block || !block["file"] || !block["digest"]) return null;
|
|
319
|
+
return {
|
|
320
|
+
file: block["file"],
|
|
321
|
+
digest: block["digest"],
|
|
322
|
+
by: block["by"] ?? null,
|
|
323
|
+
at: block["at"] ?? ""
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function parseOverride(frontmatter) {
|
|
327
|
+
const block = parseNestedBlock(frontmatter, "override");
|
|
328
|
+
if (!block || !block["status"]) return null;
|
|
329
|
+
return {
|
|
330
|
+
status: block["status"],
|
|
331
|
+
source: block["source"] ?? "human",
|
|
332
|
+
reason: block["reason"] ?? null,
|
|
333
|
+
at: block["at"] ?? ""
|
|
334
|
+
};
|
|
335
|
+
}
|
|
235
336
|
function parseWorkspace(frontmatter) {
|
|
236
337
|
const defaults = {
|
|
237
338
|
repository: null,
|
|
@@ -280,6 +381,7 @@ function parseAssignmentFrontmatter(fileContent) {
|
|
|
280
381
|
updated: getField2("updated") ?? "",
|
|
281
382
|
assignee: getField2("assignee"),
|
|
282
383
|
externalIds: parseExternalIds(frontmatter),
|
|
384
|
+
statusHistory: parseStatusHistory(frontmatter),
|
|
283
385
|
dependsOn: parseDependsOn(frontmatter),
|
|
284
386
|
links: parseLinks(frontmatter),
|
|
285
387
|
blockedReason: getField2("blockedReason"),
|
|
@@ -287,16 +389,29 @@ function parseAssignmentFrontmatter(fileContent) {
|
|
|
287
389
|
tags: parseTags(frontmatter),
|
|
288
390
|
archived: getField2("archived") === "true",
|
|
289
391
|
archivedAt: getField2("archivedAt"),
|
|
290
|
-
archivedReason: getField2("archivedReason")
|
|
392
|
+
archivedReason: getField2("archivedReason"),
|
|
393
|
+
phase: getField2("phase"),
|
|
394
|
+
disposition: getField2("disposition"),
|
|
395
|
+
planApproval: parsePlanApproval(frontmatter),
|
|
396
|
+
parked: getField2("parked") === "true",
|
|
397
|
+
reviewRequested: getField2("reviewRequested") === "true",
|
|
398
|
+
implementationStarted: getField2("implementationStarted") === "true",
|
|
399
|
+
override: parseOverride(frontmatter)
|
|
291
400
|
};
|
|
292
401
|
}
|
|
293
402
|
function formatYamlValue(value) {
|
|
294
403
|
if (typeof value === "boolean") return value ? "true" : "false";
|
|
295
404
|
if (value === null) return "null";
|
|
405
|
+
if (/[\r\n]/.test(value)) {
|
|
406
|
+
value = value.replace(/\s*[\r\n]+\s*/g, " ").trim();
|
|
407
|
+
}
|
|
296
408
|
if (/^\d{4}-\d{2}-\d{2}T/.test(value)) {
|
|
297
409
|
return `"${value}"`;
|
|
298
410
|
}
|
|
299
|
-
if (
|
|
411
|
+
if (/^(null|~|true|false|-?\d+(\.\d+)?)$/i.test(value)) {
|
|
412
|
+
return `"${value}"`;
|
|
413
|
+
}
|
|
414
|
+
if (/[:#{}[\],&*?|>!%@\`]/.test(value) || /^\s|\s$/.test(value) || value === "") {
|
|
300
415
|
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
301
416
|
return `"${escaped}"`;
|
|
302
417
|
}
|
|
@@ -376,6 +491,123 @@ ${lines.join("\n")}`;
|
|
|
376
491
|
}
|
|
377
492
|
return `${fmMatch[1]}${newFm}${fmMatch[3]}${fileContent.slice(fmMatch[0].length)}`;
|
|
378
493
|
}
|
|
494
|
+
function renameStatusInHistory(content, oldId, newId) {
|
|
495
|
+
const fmMatch = content.match(/^(---\n)([\s\S]*?)(\n---)/);
|
|
496
|
+
if (!fmMatch) return content;
|
|
497
|
+
const esc = oldId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
498
|
+
const re = new RegExp(`^(\\s+(?:from|to|phaseFrom|phaseTo):[ \\t]*)("?)${esc}\\2[ \\t]*$`, "gm");
|
|
499
|
+
const newFm = fmMatch[2].replace(re, (_m, prefix, quote) => `${prefix}${quote}${newId}${quote}`);
|
|
500
|
+
return `${fmMatch[1]}${newFm}${fmMatch[3]}${content.slice(fmMatch[0].length)}`;
|
|
501
|
+
}
|
|
502
|
+
function findStatusHistoryBlock(fmBlock) {
|
|
503
|
+
const headerMatch = fmBlock.match(/^statusHistory:\s*$/m);
|
|
504
|
+
if (!headerMatch) return null;
|
|
505
|
+
const headerStart = headerMatch.index ?? fmBlock.indexOf(headerMatch[0]);
|
|
506
|
+
const bodyStart = headerStart + headerMatch[0].length + 1;
|
|
507
|
+
const after = fmBlock.slice(bodyStart);
|
|
508
|
+
const lines = after.split("\n");
|
|
509
|
+
let consumed = 0;
|
|
510
|
+
for (const line of lines) {
|
|
511
|
+
if (line.length === 0) {
|
|
512
|
+
consumed += line.length + 1;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (line[0] !== " " && line[0] !== " ") break;
|
|
516
|
+
consumed += line.length + 1;
|
|
517
|
+
}
|
|
518
|
+
const bodyEnd = Math.min(bodyStart + consumed, fmBlock.length);
|
|
519
|
+
return { headerStart, bodyStart, bodyEnd };
|
|
520
|
+
}
|
|
521
|
+
function renderStatusHistoryItem(entry) {
|
|
522
|
+
const lines = [
|
|
523
|
+
` - at: ${formatYamlValue(entry.at)}`,
|
|
524
|
+
` from: ${formatYamlValue(entry.from)}`,
|
|
525
|
+
` to: ${formatYamlValue(entry.to)}`,
|
|
526
|
+
` command: ${formatYamlValue(entry.command)}`,
|
|
527
|
+
` by: ${formatYamlValue(entry.by)}`
|
|
528
|
+
];
|
|
529
|
+
if (entry.reason !== void 0 && entry.reason !== null) {
|
|
530
|
+
lines.push(` reason: ${formatYamlValue(entry.reason)}`);
|
|
531
|
+
}
|
|
532
|
+
for (const key of ["phaseFrom", "phaseTo", "dispositionFrom", "dispositionTo"]) {
|
|
533
|
+
if (entry[key] !== void 0) {
|
|
534
|
+
lines.push(` ${key}: ${formatYamlValue(entry[key] ?? null)}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return lines.join("\n");
|
|
538
|
+
}
|
|
539
|
+
function updateNestedBlock(fileContent, header, record) {
|
|
540
|
+
const fmMatch = fileContent.match(/^(---\n)([\s\S]*?)(\n---)/);
|
|
541
|
+
if (!fmMatch) {
|
|
542
|
+
throw new Error("No frontmatter found in assignment file. Expected --- delimiters.");
|
|
543
|
+
}
|
|
544
|
+
const fmBlock = fmMatch[2];
|
|
545
|
+
const rendered = record === null ? `${header}: null` : [`${header}:`, ...Object.entries(record).map(([k, v]) => ` ${k}: ${formatYamlValue(v)}`)].join("\n");
|
|
546
|
+
const headerRe = new RegExp(`^${header}:.*$`, "m");
|
|
547
|
+
const headerMatch = fmBlock.match(headerRe);
|
|
548
|
+
let newFm;
|
|
549
|
+
if (headerMatch) {
|
|
550
|
+
const start = headerMatch.index ?? 0;
|
|
551
|
+
let end = start + headerMatch[0].length;
|
|
552
|
+
const after = fmBlock.slice(end);
|
|
553
|
+
let scanned = 0;
|
|
554
|
+
for (const line of after.split("\n").slice(1)) {
|
|
555
|
+
if (line.length === 0) {
|
|
556
|
+
scanned += 1 + line.length;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (line[0] !== " " && line[0] !== " ") break;
|
|
560
|
+
scanned += 1 + line.length;
|
|
561
|
+
end += scanned;
|
|
562
|
+
scanned = 0;
|
|
563
|
+
}
|
|
564
|
+
newFm = fmBlock.slice(0, start) + rendered + fmBlock.slice(end);
|
|
565
|
+
} else {
|
|
566
|
+
newFm = `${fmBlock.replace(/\n+$/, "")}
|
|
567
|
+
${rendered}`;
|
|
568
|
+
}
|
|
569
|
+
return `${fmMatch[1]}${newFm}${fmMatch[3]}${fileContent.slice(fmMatch[0].length)}`;
|
|
570
|
+
}
|
|
571
|
+
function updatePlanApproval(fileContent, approval) {
|
|
572
|
+
return updateNestedBlock(
|
|
573
|
+
fileContent,
|
|
574
|
+
"planApproval",
|
|
575
|
+
approval === null ? null : { file: approval.file, digest: approval.digest, by: approval.by, at: approval.at }
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
function updateOverride(fileContent, override) {
|
|
579
|
+
return updateNestedBlock(
|
|
580
|
+
fileContent,
|
|
581
|
+
"override",
|
|
582
|
+
override === null ? null : { status: override.status, source: override.source, reason: override.reason, at: override.at }
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
function appendStatusHistoryEntry(fileContent, entry) {
|
|
586
|
+
const fmMatch = fileContent.match(/^(---\n)([\s\S]*?)(\n---)/);
|
|
587
|
+
if (!fmMatch) {
|
|
588
|
+
throw new Error("No frontmatter found in assignment file. Expected --- delimiters.");
|
|
589
|
+
}
|
|
590
|
+
const fmBlock = fmMatch[2];
|
|
591
|
+
const item = renderStatusHistoryItem(entry);
|
|
592
|
+
const inlineRegex = /^statusHistory:[ \t]*\[[ \t]*\][ \t]*$/m;
|
|
593
|
+
const block = findStatusHistoryBlock(fmBlock);
|
|
594
|
+
let newFm;
|
|
595
|
+
if (inlineRegex.test(fmBlock)) {
|
|
596
|
+
newFm = fmBlock.replace(inlineRegex, `statusHistory:
|
|
597
|
+
${item}`);
|
|
598
|
+
} else if (block) {
|
|
599
|
+
const before = fmBlock.slice(0, block.bodyEnd);
|
|
600
|
+
const rest = fmBlock.slice(block.bodyEnd);
|
|
601
|
+
const sep1 = before.endsWith("\n") ? "" : "\n";
|
|
602
|
+
const sep2 = rest.length > 0 && !rest.startsWith("\n") ? "\n" : "";
|
|
603
|
+
newFm = `${before}${sep1}${item}${sep2}${rest}`;
|
|
604
|
+
} else {
|
|
605
|
+
newFm = `${fmBlock.replace(/\n+$/, "")}
|
|
606
|
+
statusHistory:
|
|
607
|
+
${item}`;
|
|
608
|
+
}
|
|
609
|
+
return `${fmMatch[1]}${newFm}${fmMatch[3]}${fileContent.slice(fmMatch[0].length)}`;
|
|
610
|
+
}
|
|
379
611
|
var init_frontmatter = __esm({
|
|
380
612
|
"src/lifecycle/frontmatter.ts"() {
|
|
381
613
|
"use strict";
|
|
@@ -586,6 +818,51 @@ function parseExternalIds2(frontmatter) {
|
|
|
586
818
|
}
|
|
587
819
|
return results;
|
|
588
820
|
}
|
|
821
|
+
function parseStatusHistory2(frontmatter) {
|
|
822
|
+
if (/^statusHistory:\s*\[\s*\]/m.test(frontmatter)) return [];
|
|
823
|
+
const headerMatch = frontmatter.match(/^statusHistory:\s*$/m);
|
|
824
|
+
if (!headerMatch) return [];
|
|
825
|
+
const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);
|
|
826
|
+
const bodyStart = headerStart + headerMatch[0].length + 1;
|
|
827
|
+
const after = frontmatter.slice(bodyStart);
|
|
828
|
+
const bodyLines = [];
|
|
829
|
+
for (const line of after.split("\n")) {
|
|
830
|
+
if (line.length === 0) {
|
|
831
|
+
bodyLines.push(line);
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
if (line[0] !== " " && line[0] !== " ") break;
|
|
835
|
+
bodyLines.push(line);
|
|
836
|
+
}
|
|
837
|
+
const body = bodyLines.join("\n");
|
|
838
|
+
const results = [];
|
|
839
|
+
const itemBlocks = body.split(/\n\s+-\s+/).filter((b) => b.trim().length > 0);
|
|
840
|
+
for (const block of itemBlocks) {
|
|
841
|
+
const entry = {};
|
|
842
|
+
for (const line of block.split("\n")) {
|
|
843
|
+
const colonIdx = line.indexOf(":");
|
|
844
|
+
if (colonIdx < 0) continue;
|
|
845
|
+
const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
|
|
846
|
+
if (!key) continue;
|
|
847
|
+
entry[key] = parseSimpleValue2(line.slice(colonIdx + 1));
|
|
848
|
+
}
|
|
849
|
+
if (!entry["to"]) continue;
|
|
850
|
+
const result = {
|
|
851
|
+
at: entry["at"] ?? "",
|
|
852
|
+
from: entry["from"] ?? null,
|
|
853
|
+
to: entry["to"],
|
|
854
|
+
command: entry["command"] ?? "",
|
|
855
|
+
by: entry["by"] ?? null
|
|
856
|
+
};
|
|
857
|
+
if (entry["reason"] != null) result.reason = entry["reason"];
|
|
858
|
+
if ("phaseFrom" in entry) result.phaseFrom = entry["phaseFrom"];
|
|
859
|
+
if ("phaseTo" in entry) result.phaseTo = entry["phaseTo"];
|
|
860
|
+
if ("dispositionFrom" in entry) result.dispositionFrom = entry["dispositionFrom"];
|
|
861
|
+
if ("dispositionTo" in entry) result.dispositionTo = entry["dispositionTo"];
|
|
862
|
+
results.push(result);
|
|
863
|
+
}
|
|
864
|
+
return results;
|
|
865
|
+
}
|
|
589
866
|
function parseAssignmentFull(fileContent) {
|
|
590
867
|
const [fm, body] = extractFrontmatter2(fileContent);
|
|
591
868
|
return {
|
|
@@ -608,13 +885,40 @@ function parseAssignmentFull(fileContent) {
|
|
|
608
885
|
parentBranch: getNestedField(fm, "workspace", "parentBranch")
|
|
609
886
|
},
|
|
610
887
|
externalIds: parseExternalIds2(fm),
|
|
888
|
+
statusHistory: parseStatusHistory2(fm),
|
|
611
889
|
tags: parseListField(fm, "tags"),
|
|
612
890
|
archived: getField(fm, "archived") === "true",
|
|
613
891
|
archivedAt: getField(fm, "archivedAt"),
|
|
614
892
|
archivedReason: getField(fm, "archivedReason"),
|
|
615
893
|
created: getField(fm, "created") ?? "",
|
|
616
894
|
updated: getField(fm, "updated") ?? "",
|
|
617
|
-
body
|
|
895
|
+
body,
|
|
896
|
+
phase: getField(fm, "phase"),
|
|
897
|
+
disposition: getField(fm, "disposition"),
|
|
898
|
+
parked: getField(fm, "parked") === "true",
|
|
899
|
+
reviewRequested: getField(fm, "reviewRequested") === "true",
|
|
900
|
+
implementationStarted: getField(fm, "implementationStarted") === "true",
|
|
901
|
+
planApproval: (() => {
|
|
902
|
+
const file = getNestedField(fm, "planApproval", "file");
|
|
903
|
+
const digest = getNestedField(fm, "planApproval", "digest");
|
|
904
|
+
if (!file || !digest) return null;
|
|
905
|
+
return {
|
|
906
|
+
file,
|
|
907
|
+
digest,
|
|
908
|
+
by: getNestedField(fm, "planApproval", "by"),
|
|
909
|
+
at: getNestedField(fm, "planApproval", "at") ?? ""
|
|
910
|
+
};
|
|
911
|
+
})(),
|
|
912
|
+
override: (() => {
|
|
913
|
+
const status = getNestedField(fm, "override", "status");
|
|
914
|
+
if (!status) return null;
|
|
915
|
+
return {
|
|
916
|
+
status,
|
|
917
|
+
source: getNestedField(fm, "override", "source") ?? "human",
|
|
918
|
+
reason: getNestedField(fm, "override", "reason"),
|
|
919
|
+
at: getNestedField(fm, "override", "at") ?? ""
|
|
920
|
+
};
|
|
921
|
+
})()
|
|
618
922
|
};
|
|
619
923
|
}
|
|
620
924
|
function parsePlan(fileContent) {
|
|
@@ -1309,7 +1613,10 @@ async function checkDependencies(projectDir, dependsOn, terminalStatuses) {
|
|
|
1309
1613
|
async function executeTransition(projectDir, assignmentSlug, command, options = {}) {
|
|
1310
1614
|
const filePath = resolveAssignmentPath(projectDir, assignmentSlug);
|
|
1311
1615
|
const { content, frontmatter } = await readAssignment(filePath);
|
|
1312
|
-
const targetStatus = getTargetStatus(frontmatter.status, command, options.transitionTable)
|
|
1616
|
+
const targetStatus = (options.transitionTable ? getTargetStatus(frontmatter.status, command, options.transitionTable) : null) ?? options.commandTargets?.get(command) ?? // Built-ins apply only when NEITHER custom mechanism was supplied — a
|
|
1617
|
+
// provided-but-miss commandTargets means "custom config had no answer",
|
|
1618
|
+
// which must refuse, not silently fall back (codex r4).
|
|
1619
|
+
(!options.transitionTable && !options.commandTargets ? getTargetStatus(frontmatter.status, command) : null);
|
|
1313
1620
|
if (!targetStatus) {
|
|
1314
1621
|
return {
|
|
1315
1622
|
success: false,
|
|
@@ -1324,20 +1631,37 @@ async function executeTransition(projectDir, assignmentSlug, command, options =
|
|
|
1324
1631
|
warnings.push(`Starting with unmet dependencies: ${depCheck.unmet.join(", ")}`);
|
|
1325
1632
|
}
|
|
1326
1633
|
}
|
|
1634
|
+
const now = nowTimestamp();
|
|
1327
1635
|
const updates = {
|
|
1328
1636
|
status: targetStatus,
|
|
1329
|
-
updated:
|
|
1637
|
+
updated: now
|
|
1330
1638
|
};
|
|
1331
1639
|
if (ASSIGNEE_SETTING_COMMANDS.has(command) && options.agent && !frontmatter.assignee) {
|
|
1332
1640
|
updates.assignee = options.agent;
|
|
1333
1641
|
}
|
|
1334
1642
|
if (command === "block") {
|
|
1335
|
-
updates.blockedReason = options.reason ??
|
|
1643
|
+
updates.blockedReason = options.reason ?? "(unspecified)";
|
|
1336
1644
|
}
|
|
1337
1645
|
if (command === "unblock") {
|
|
1338
1646
|
updates.blockedReason = null;
|
|
1339
1647
|
}
|
|
1340
|
-
const
|
|
1648
|
+
const terminalSet = options.terminalStatuses ?? /* @__PURE__ */ new Set(["completed", "failed"]);
|
|
1649
|
+
const enteringTerminal = terminalSet.has(targetStatus) && frontmatter.disposition !== "terminal";
|
|
1650
|
+
if (enteringTerminal) {
|
|
1651
|
+
updates.disposition = "terminal";
|
|
1652
|
+
}
|
|
1653
|
+
let updatedContent = updateAssignmentFile(content, updates);
|
|
1654
|
+
if (targetStatus !== frontmatter.status) {
|
|
1655
|
+
updatedContent = appendStatusHistoryEntry(updatedContent, {
|
|
1656
|
+
at: now,
|
|
1657
|
+
from: frontmatter.status,
|
|
1658
|
+
to: targetStatus,
|
|
1659
|
+
command,
|
|
1660
|
+
by: options.agent ?? frontmatter.assignee ?? null,
|
|
1661
|
+
reason: command === "block" ? options.reason : void 0,
|
|
1662
|
+
...enteringTerminal ? { dispositionFrom: frontmatter.disposition, dispositionTo: "terminal" } : {}
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1341
1665
|
await writeFileForce(filePath, updatedContent);
|
|
1342
1666
|
await applyLinkedTodosSideEffect(options.linkedTodosLookup, command, targetStatus, frontmatter);
|
|
1343
1667
|
return {
|
|
@@ -1351,7 +1675,10 @@ async function executeTransition(projectDir, assignmentSlug, command, options =
|
|
|
1351
1675
|
async function executeTransitionByDir(assignmentDir, command, options = {}) {
|
|
1352
1676
|
const filePath = resolve4(assignmentDir, "assignment.md");
|
|
1353
1677
|
const { content, frontmatter } = await readAssignment(filePath);
|
|
1354
|
-
const targetStatus = getTargetStatus(frontmatter.status, command, options.transitionTable)
|
|
1678
|
+
const targetStatus = (options.transitionTable ? getTargetStatus(frontmatter.status, command, options.transitionTable) : null) ?? options.commandTargets?.get(command) ?? // Built-ins apply only when NEITHER custom mechanism was supplied — a
|
|
1679
|
+
// provided-but-miss commandTargets means "custom config had no answer",
|
|
1680
|
+
// which must refuse, not silently fall back (codex r4).
|
|
1681
|
+
(!options.transitionTable && !options.commandTargets ? getTargetStatus(frontmatter.status, command) : null);
|
|
1355
1682
|
if (!targetStatus) {
|
|
1356
1683
|
return {
|
|
1357
1684
|
success: false,
|
|
@@ -1371,20 +1698,37 @@ async function executeTransitionByDir(assignmentDir, command, options = {}) {
|
|
|
1371
1698
|
warnings.push(`Starting with unmet dependencies: ${depCheck.unmet.join(", ")}`);
|
|
1372
1699
|
}
|
|
1373
1700
|
}
|
|
1701
|
+
const now = nowTimestamp();
|
|
1374
1702
|
const updates = {
|
|
1375
1703
|
status: targetStatus,
|
|
1376
|
-
updated:
|
|
1704
|
+
updated: now
|
|
1377
1705
|
};
|
|
1378
1706
|
if (ASSIGNEE_SETTING_COMMANDS.has(command) && options.agent && !frontmatter.assignee) {
|
|
1379
1707
|
updates.assignee = options.agent;
|
|
1380
1708
|
}
|
|
1381
1709
|
if (command === "block") {
|
|
1382
|
-
updates.blockedReason = options.reason ??
|
|
1710
|
+
updates.blockedReason = options.reason ?? "(unspecified)";
|
|
1383
1711
|
}
|
|
1384
1712
|
if (command === "unblock") {
|
|
1385
1713
|
updates.blockedReason = null;
|
|
1386
1714
|
}
|
|
1387
|
-
const
|
|
1715
|
+
const terminalSetByDir = options.terminalStatuses ?? /* @__PURE__ */ new Set(["completed", "failed"]);
|
|
1716
|
+
const enteringTerminalByDir = terminalSetByDir.has(targetStatus) && frontmatter.disposition !== "terminal";
|
|
1717
|
+
if (enteringTerminalByDir) {
|
|
1718
|
+
updates.disposition = "terminal";
|
|
1719
|
+
}
|
|
1720
|
+
let updatedContent = updateAssignmentFile(content, updates);
|
|
1721
|
+
if (targetStatus !== frontmatter.status) {
|
|
1722
|
+
updatedContent = appendStatusHistoryEntry(updatedContent, {
|
|
1723
|
+
at: now,
|
|
1724
|
+
from: frontmatter.status,
|
|
1725
|
+
to: targetStatus,
|
|
1726
|
+
command,
|
|
1727
|
+
by: options.agent ?? frontmatter.assignee ?? null,
|
|
1728
|
+
reason: command === "block" ? options.reason : void 0,
|
|
1729
|
+
...enteringTerminalByDir ? { dispositionFrom: frontmatter.disposition, dispositionTo: "terminal" } : {}
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1388
1732
|
await writeFileForce(filePath, updatedContent);
|
|
1389
1733
|
await applyLinkedTodosSideEffect(options.linkedTodosLookup, command, targetStatus, frontmatter);
|
|
1390
1734
|
return {
|
|
@@ -1862,6 +2206,7 @@ __export(config_exports, {
|
|
|
1862
2206
|
AgentConfigError: () => AgentConfigError,
|
|
1863
2207
|
BUILTIN_AGENTS: () => BUILTIN_AGENTS,
|
|
1864
2208
|
DEFAULT_ASSIGNMENT_TYPES: () => DEFAULT_ASSIGNMENT_TYPES,
|
|
2209
|
+
DEFAULT_DERIVE_CONFIG: () => DEFAULT_DERIVE_CONFIG,
|
|
1865
2210
|
DEFAULT_STATUS_COLORS: () => DEFAULT_STATUS_COLORS,
|
|
1866
2211
|
PROMPT_ARG_POSITIONS: () => PROMPT_ARG_POSITIONS,
|
|
1867
2212
|
TERMINAL_CHOICES: () => TERMINAL_CHOICES,
|
|
@@ -1888,6 +2233,7 @@ __export(config_exports, {
|
|
|
1888
2233
|
updateOnboardingConfig: () => updateOnboardingConfig,
|
|
1889
2234
|
updatePlaybooksConfig: () => updatePlaybooksConfig,
|
|
1890
2235
|
validateAgentList: () => validateAgentList,
|
|
2236
|
+
validateDeriveConfig: () => validateDeriveConfig,
|
|
1891
2237
|
writeAgentsConfig: () => writeAgentsConfig,
|
|
1892
2238
|
writeHotkeyBindingsConfig: () => writeHotkeyBindingsConfig,
|
|
1893
2239
|
writeStatusConfig: () => writeStatusConfig,
|
|
@@ -2059,6 +2405,16 @@ function parseStatusConfig(content) {
|
|
|
2059
2405
|
const statuses = [];
|
|
2060
2406
|
const order = [];
|
|
2061
2407
|
const transitions = [];
|
|
2408
|
+
const phaseLadder = [];
|
|
2409
|
+
const disposition = [];
|
|
2410
|
+
const headline = {};
|
|
2411
|
+
const unquote = (v) => {
|
|
2412
|
+
const t = v.trim();
|
|
2413
|
+
if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) {
|
|
2414
|
+
return t.slice(1, -1);
|
|
2415
|
+
}
|
|
2416
|
+
return t;
|
|
2417
|
+
};
|
|
2062
2418
|
let currentSection = null;
|
|
2063
2419
|
const lines = remaining.split("\n");
|
|
2064
2420
|
function parseListEntry(lineIdx, baseIndent) {
|
|
@@ -2091,6 +2447,9 @@ function parseStatusConfig(content) {
|
|
|
2091
2447
|
if (key === "definitions") currentSection = "definitions";
|
|
2092
2448
|
else if (key === "order") currentSection = "order";
|
|
2093
2449
|
else if (key === "transitions") currentSection = "transitions";
|
|
2450
|
+
else if (key === "phaseLadder") currentSection = "phaseLadder";
|
|
2451
|
+
else if (key === "disposition") currentSection = "disposition";
|
|
2452
|
+
else if (key === "headline") currentSection = "headline";
|
|
2094
2453
|
else currentSection = null;
|
|
2095
2454
|
continue;
|
|
2096
2455
|
}
|
|
@@ -2129,12 +2488,52 @@ function parseStatusConfig(content) {
|
|
|
2129
2488
|
lineIdx += consumed - 1;
|
|
2130
2489
|
continue;
|
|
2131
2490
|
}
|
|
2491
|
+
if (currentSection === "phaseLadder" && indent >= 4 && trimmed.startsWith("- ")) {
|
|
2492
|
+
const { entry, consumed } = parseListEntry(lineIdx, indent);
|
|
2493
|
+
if (entry["phase"] && entry["when"] !== void 0) {
|
|
2494
|
+
phaseLadder.push({
|
|
2495
|
+
phase: unquote(entry["phase"]),
|
|
2496
|
+
when: unquote(entry["when"]),
|
|
2497
|
+
next: entry["next"] !== void 0 ? unquote(entry["next"]) : void 0
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
lineIdx += consumed - 1;
|
|
2501
|
+
continue;
|
|
2502
|
+
}
|
|
2503
|
+
if (currentSection === "disposition" && indent >= 4 && trimmed.startsWith("- ")) {
|
|
2504
|
+
const { entry, consumed } = parseListEntry(lineIdx, indent);
|
|
2505
|
+
if (entry["else"] !== void 0) {
|
|
2506
|
+
disposition.push({ when: null, is: unquote(entry["else"]) });
|
|
2507
|
+
} else if (entry["when"] !== void 0 && entry["is"]) {
|
|
2508
|
+
disposition.push({ when: unquote(entry["when"]), is: unquote(entry["is"]) });
|
|
2509
|
+
}
|
|
2510
|
+
lineIdx += consumed - 1;
|
|
2511
|
+
continue;
|
|
2512
|
+
}
|
|
2513
|
+
if (currentSection === "headline" && indent >= 4 && !trimmed.startsWith("- ")) {
|
|
2514
|
+
const ci = trimmed.indexOf(":");
|
|
2515
|
+
if (ci > 0) {
|
|
2516
|
+
headline[trimmed.slice(0, ci).trim()] = unquote(trimmed.slice(ci + 1));
|
|
2517
|
+
}
|
|
2518
|
+
continue;
|
|
2519
|
+
}
|
|
2132
2520
|
}
|
|
2133
2521
|
if (statuses.length === 0) return null;
|
|
2522
|
+
const derive = phaseLadder.length > 0 || disposition.length > 0 || Object.keys(headline).length > 0 ? {
|
|
2523
|
+
phaseLadder: phaseLadder.length > 0 ? phaseLadder : DEFAULT_DERIVE_CONFIG.phaseLadder,
|
|
2524
|
+
disposition: disposition.length > 0 ? disposition : DEFAULT_DERIVE_CONFIG.disposition,
|
|
2525
|
+
headline: {
|
|
2526
|
+
terminal: "passthrough",
|
|
2527
|
+
parked: headline["parked"] ?? DEFAULT_DERIVE_CONFIG.headline.parked,
|
|
2528
|
+
blocked: headline["blocked"] ?? DEFAULT_DERIVE_CONFIG.headline.blocked,
|
|
2529
|
+
active: "phase"
|
|
2530
|
+
}
|
|
2531
|
+
} : null;
|
|
2134
2532
|
return {
|
|
2135
2533
|
statuses,
|
|
2136
2534
|
order: order.length > 0 ? order : statuses.map((s) => s.id),
|
|
2137
|
-
transitions
|
|
2535
|
+
transitions,
|
|
2536
|
+
derive
|
|
2138
2537
|
};
|
|
2139
2538
|
}
|
|
2140
2539
|
function toTitleCase(s) {
|
|
@@ -2182,8 +2581,68 @@ function serializeStatusConfig(statuses) {
|
|
|
2182
2581
|
if (t.requiresReason) lines.push(` requiresReason: true`);
|
|
2183
2582
|
}
|
|
2184
2583
|
}
|
|
2584
|
+
if (statuses.derive) {
|
|
2585
|
+
const d = statuses.derive;
|
|
2586
|
+
lines.push(" phaseLadder:");
|
|
2587
|
+
for (const rung of d.phaseLadder) {
|
|
2588
|
+
lines.push(` - phase: ${rung.phase}`);
|
|
2589
|
+
lines.push(` when: "${rung.when.replace(/"/g, '\\"')}"`);
|
|
2590
|
+
if (rung.next) lines.push(` next: "${rung.next.replace(/"/g, '\\"')}"`);
|
|
2591
|
+
}
|
|
2592
|
+
lines.push(" disposition:");
|
|
2593
|
+
for (const rule of d.disposition) {
|
|
2594
|
+
if (rule.when === null) {
|
|
2595
|
+
lines.push(` - else: ${rule.is}`);
|
|
2596
|
+
} else {
|
|
2597
|
+
lines.push(` - when: "${rule.when.replace(/"/g, '\\"')}"`);
|
|
2598
|
+
lines.push(` is: ${rule.is}`);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
lines.push(" headline:");
|
|
2602
|
+
lines.push(` terminal: passthrough`);
|
|
2603
|
+
lines.push(` parked: ${d.headline.parked}`);
|
|
2604
|
+
lines.push(` blocked: ${d.headline.blocked}`);
|
|
2605
|
+
lines.push(` active: phase`);
|
|
2606
|
+
}
|
|
2185
2607
|
return lines.join("\n");
|
|
2186
2608
|
}
|
|
2609
|
+
function validateDeriveConfig(derive, statusConfig, validateWhen = () => null) {
|
|
2610
|
+
const problems = [];
|
|
2611
|
+
const ids = new Set(statusConfig.statuses.map((s) => s.id));
|
|
2612
|
+
if (derive.phaseLadder.length === 0) {
|
|
2613
|
+
problems.push("phaseLadder must have at least one rung");
|
|
2614
|
+
}
|
|
2615
|
+
for (const rung of derive.phaseLadder) {
|
|
2616
|
+
if (!ids.has(rung.phase)) {
|
|
2617
|
+
problems.push(`phaseLadder rung "${rung.phase}" is not a defined status id`);
|
|
2618
|
+
}
|
|
2619
|
+
const err = rung.when === "*" ? null : validateWhen(rung.when);
|
|
2620
|
+
if (err) problems.push(`phaseLadder rung "${rung.phase}": invalid condition \u2014 ${err}`);
|
|
2621
|
+
}
|
|
2622
|
+
const VALID_DISPOSITIONS = /* @__PURE__ */ new Set(["active", "blocked", "parked"]);
|
|
2623
|
+
let sawElse = false;
|
|
2624
|
+
for (const rule of derive.disposition) {
|
|
2625
|
+
if (!VALID_DISPOSITIONS.has(rule.is)) {
|
|
2626
|
+
problems.push(
|
|
2627
|
+
`disposition "${rule.is}" is not valid (expected active, blocked, or parked \u2014 terminal is never a rule)`
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2630
|
+
if (rule.when === null) sawElse = true;
|
|
2631
|
+
else {
|
|
2632
|
+
const err = validateWhen(rule.when);
|
|
2633
|
+
if (err) problems.push(`disposition rule "${rule.is}": invalid condition \u2014 ${err}`);
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
if (!sawElse) problems.push("disposition rules must end with an `else:` arm");
|
|
2637
|
+
for (const key of ["parked", "blocked"]) {
|
|
2638
|
+
if (!ids.has(derive.headline[key])) {
|
|
2639
|
+
problems.push(
|
|
2640
|
+
`headline.${key} \u2192 "${derive.headline[key]}" is not a defined status id (add the definition or run migrate-derive)`
|
|
2641
|
+
);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
return problems;
|
|
2645
|
+
}
|
|
2187
2646
|
function serializeIntegrationConfig(integrations) {
|
|
2188
2647
|
const lines = [];
|
|
2189
2648
|
if (integrations.claudePluginDir) {
|
|
@@ -3238,7 +3697,7 @@ async function updateAgentsConfig(mutation, options = {}) {
|
|
|
3238
3697
|
await writeAgentsConfig(next);
|
|
3239
3698
|
return { previous, next, written: true };
|
|
3240
3699
|
}
|
|
3241
|
-
var DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
|
|
3700
|
+
var DEFAULT_DERIVE_CONFIG, DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
|
|
3242
3701
|
var init_config2 = __esm({
|
|
3243
3702
|
"src/utils/config.ts"() {
|
|
3244
3703
|
"use strict";
|
|
@@ -3252,6 +3711,35 @@ var init_config2 = __esm({
|
|
|
3252
3711
|
init_slug();
|
|
3253
3712
|
init_terminal_schema();
|
|
3254
3713
|
init_workspace_visibility_schema();
|
|
3714
|
+
DEFAULT_DERIVE_CONFIG = {
|
|
3715
|
+
phaseLadder: [
|
|
3716
|
+
{ phase: "draft", when: "*", next: "Fill in the objective and acceptance criteria" },
|
|
3717
|
+
{
|
|
3718
|
+
// planExists-but-not-approved also sits here: the default status set has
|
|
3719
|
+
// no `planning` id. Users who define one add a `planExists:true` rung.
|
|
3720
|
+
phase: "ready_for_planning",
|
|
3721
|
+
when: "hasRealObjective:true AND acRealTotal > 0",
|
|
3722
|
+
next: "Write a plan and get it approved"
|
|
3723
|
+
},
|
|
3724
|
+
{ phase: "ready_to_implement", when: "planApproved:true", next: "Start implementing" },
|
|
3725
|
+
{
|
|
3726
|
+
phase: "in_progress",
|
|
3727
|
+
when: "planApproved:true AND implementationStarted:true",
|
|
3728
|
+
next: "Finish acceptance criteria, then request review"
|
|
3729
|
+
},
|
|
3730
|
+
{
|
|
3731
|
+
phase: "review",
|
|
3732
|
+
when: "acAllChecked:true OR reviewRequested:true",
|
|
3733
|
+
next: "Complete, or address review feedback"
|
|
3734
|
+
}
|
|
3735
|
+
],
|
|
3736
|
+
disposition: [
|
|
3737
|
+
{ when: "parked:true", is: "parked" },
|
|
3738
|
+
{ when: "blocked:true", is: "blocked" },
|
|
3739
|
+
{ when: null, is: "active" }
|
|
3740
|
+
],
|
|
3741
|
+
headline: { terminal: "passthrough", parked: "parked", blocked: "blocked", active: "phase" }
|
|
3742
|
+
};
|
|
3255
3743
|
DEFAULT_ASSIGNMENT_TYPES = {
|
|
3256
3744
|
definitions: [
|
|
3257
3745
|
{ id: "feature", label: "Feature" },
|
|
@@ -4230,8 +4718,8 @@ async function migrateFromMarkdown(projectsDir) {
|
|
|
4230
4718
|
return allSessions.length;
|
|
4231
4719
|
}
|
|
4232
4720
|
async function parseMarkdownSessionsIndex(filePath, projectSlug) {
|
|
4233
|
-
const { readFile:
|
|
4234
|
-
const raw2 = await
|
|
4721
|
+
const { readFile: readFile23 } = await import("fs/promises");
|
|
4722
|
+
const raw2 = await readFile23(filePath, "utf-8");
|
|
4235
4723
|
const sessions = [];
|
|
4236
4724
|
const lines = raw2.split("\n");
|
|
4237
4725
|
let inTable = false;
|
|
@@ -4667,8 +5155,8 @@ function scanKey(serversDir2, projectsDir, assignmentsDir2) {
|
|
|
4667
5155
|
return `${serversDir2}\0${projectsDir}\0${assignmentsDir2 ?? ""}`;
|
|
4668
5156
|
}
|
|
4669
5157
|
function delay(ms) {
|
|
4670
|
-
return new Promise((
|
|
4671
|
-
const timer2 = setTimeout(
|
|
5158
|
+
return new Promise((resolve34) => {
|
|
5159
|
+
const timer2 = setTimeout(resolve34, ms);
|
|
4672
5160
|
if (typeof timer2.unref === "function") {
|
|
4673
5161
|
timer2.unref();
|
|
4674
5162
|
}
|
|
@@ -5074,45 +5562,911 @@ async function scanAllSessions(serversDir2, projectsDir, options) {
|
|
|
5074
5562
|
delay(COLD_WAIT_BUDGET_MS).then(() => emptyScan())
|
|
5075
5563
|
]);
|
|
5076
5564
|
}
|
|
5077
|
-
return refresh;
|
|
5078
|
-
}
|
|
5079
|
-
async function scanSingleSession(serversDir2, projectsDir, name, options) {
|
|
5080
|
-
const data = await readSessionFile(serversDir2, name);
|
|
5081
|
-
if (!data) return null;
|
|
5082
|
-
const [lsofOutput, workspaceRecords, procSnapshot] = await Promise.all([
|
|
5083
|
-
getLsofOutput(),
|
|
5084
|
-
loadWorkspaceRecords(projectsDir, options?.assignmentsDir),
|
|
5085
|
-
getProcessSnapshot()
|
|
5086
|
-
]);
|
|
5087
|
-
if (data.kind === "process") {
|
|
5088
|
-
return scanProcessSession(data, lsofOutput, workspaceRecords);
|
|
5565
|
+
return refresh;
|
|
5566
|
+
}
|
|
5567
|
+
async function scanSingleSession(serversDir2, projectsDir, name, options) {
|
|
5568
|
+
const data = await readSessionFile(serversDir2, name);
|
|
5569
|
+
if (!data) return null;
|
|
5570
|
+
const [lsofOutput, workspaceRecords, procSnapshot] = await Promise.all([
|
|
5571
|
+
getLsofOutput(),
|
|
5572
|
+
loadWorkspaceRecords(projectsDir, options?.assignmentsDir),
|
|
5573
|
+
getProcessSnapshot()
|
|
5574
|
+
]);
|
|
5575
|
+
if (data.kind === "process") {
|
|
5576
|
+
return scanProcessSession(data, lsofOutput, workspaceRecords);
|
|
5577
|
+
}
|
|
5578
|
+
return scanSession(data, lsofOutput, workspaceRecords, procSnapshot);
|
|
5579
|
+
}
|
|
5580
|
+
var exec, cache, lastKnown, inFlight, forceFreshNext, scanEpoch, cacheKey, CACHE_TTL_MS, COLD_WAIT_BUDGET_MS, PROBE_TIMEOUT_MS, PROBE_MAX_BUFFER, tmuxAvailableCache;
|
|
5581
|
+
var init_scanner = __esm({
|
|
5582
|
+
"src/dashboard/scanner.ts"() {
|
|
5583
|
+
"use strict";
|
|
5584
|
+
init_api();
|
|
5585
|
+
init_servers();
|
|
5586
|
+
exec = promisify(execFile);
|
|
5587
|
+
cache = null;
|
|
5588
|
+
lastKnown = null;
|
|
5589
|
+
inFlight = null;
|
|
5590
|
+
forceFreshNext = false;
|
|
5591
|
+
scanEpoch = 0;
|
|
5592
|
+
cacheKey = null;
|
|
5593
|
+
CACHE_TTL_MS = 1e4;
|
|
5594
|
+
COLD_WAIT_BUDGET_MS = 2500;
|
|
5595
|
+
PROBE_TIMEOUT_MS = 15e3;
|
|
5596
|
+
PROBE_MAX_BUFFER = 32 * 1024 * 1024;
|
|
5597
|
+
tmuxAvailableCache = null;
|
|
5598
|
+
}
|
|
5599
|
+
});
|
|
5600
|
+
|
|
5601
|
+
// src/lifecycle/facts.ts
|
|
5602
|
+
var facts_exports = {};
|
|
5603
|
+
__export(facts_exports, {
|
|
5604
|
+
areDependenciesSatisfied: () => areDependenciesSatisfied,
|
|
5605
|
+
computeFacts: () => computeFacts,
|
|
5606
|
+
countRealAcceptanceCriteria: () => countRealAcceptanceCriteria,
|
|
5607
|
+
countUnresolvedQuestions: () => countUnresolvedQuestions,
|
|
5608
|
+
hasRealObjective: () => hasRealObjective,
|
|
5609
|
+
isPlanApproved: () => isPlanApproved,
|
|
5610
|
+
latestPlanFile: () => latestPlanFile,
|
|
5611
|
+
planDigest: () => planDigest
|
|
5612
|
+
});
|
|
5613
|
+
import { createHash } from "crypto";
|
|
5614
|
+
import { readdir as readdir7, readFile as readFile10 } from "fs/promises";
|
|
5615
|
+
import { resolve as resolve13 } from "path";
|
|
5616
|
+
function sectionBody(body, heading) {
|
|
5617
|
+
const re = new RegExp(`^##\\s+${heading}\\s*$`, "m");
|
|
5618
|
+
const m = body.match(re);
|
|
5619
|
+
if (!m || m.index === void 0) return null;
|
|
5620
|
+
const start = m.index + m[0].length;
|
|
5621
|
+
const rest = body.slice(start);
|
|
5622
|
+
const next = rest.search(/^##\s+/m);
|
|
5623
|
+
return next >= 0 ? rest.slice(0, next) : rest;
|
|
5624
|
+
}
|
|
5625
|
+
function hasRealObjective(body) {
|
|
5626
|
+
const section = sectionBody(body, "Objective");
|
|
5627
|
+
if (section === null) return false;
|
|
5628
|
+
return section.replace(HTML_COMMENT_RE, "").trim().length > 0;
|
|
5629
|
+
}
|
|
5630
|
+
function countRealAcceptanceCriteria(body) {
|
|
5631
|
+
const section = sectionBody(body, "Acceptance Criteria");
|
|
5632
|
+
if (section === null) return { total: 0, checked: 0 };
|
|
5633
|
+
let total = 0;
|
|
5634
|
+
let checked = 0;
|
|
5635
|
+
for (const line of section.split("\n")) {
|
|
5636
|
+
const m = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)$/);
|
|
5637
|
+
if (!m) continue;
|
|
5638
|
+
const content = m[2].replace(HTML_COMMENT_RE, "").trim();
|
|
5639
|
+
if (content.length === 0) continue;
|
|
5640
|
+
total++;
|
|
5641
|
+
if (m[1].toLowerCase() === "x") checked++;
|
|
5642
|
+
}
|
|
5643
|
+
return { total, checked };
|
|
5644
|
+
}
|
|
5645
|
+
async function latestPlanFile(assignmentDir) {
|
|
5646
|
+
let entries;
|
|
5647
|
+
try {
|
|
5648
|
+
entries = await readdir7(assignmentDir);
|
|
5649
|
+
} catch {
|
|
5650
|
+
return null;
|
|
5651
|
+
}
|
|
5652
|
+
let best = null;
|
|
5653
|
+
for (const name of entries) {
|
|
5654
|
+
const m = name.match(PLAN_FILE_RE);
|
|
5655
|
+
if (!m) continue;
|
|
5656
|
+
const version = m[1] ? parseInt(m[1], 10) : 1;
|
|
5657
|
+
if (!best || version > best.version) best = { name, version };
|
|
5658
|
+
}
|
|
5659
|
+
return best?.name ?? null;
|
|
5660
|
+
}
|
|
5661
|
+
function planDigest(content) {
|
|
5662
|
+
return createHash("sha256").update(content, "utf-8").digest("hex");
|
|
5663
|
+
}
|
|
5664
|
+
async function isPlanApproved(assignmentDir, frontmatter) {
|
|
5665
|
+
const approval = frontmatter.planApproval;
|
|
5666
|
+
if (!approval) return false;
|
|
5667
|
+
const latest = await latestPlanFile(assignmentDir);
|
|
5668
|
+
if (!latest || latest !== approval.file) return false;
|
|
5669
|
+
try {
|
|
5670
|
+
const content = await readFile10(resolve13(assignmentDir, latest), "utf-8");
|
|
5671
|
+
return planDigest(content) === approval.digest;
|
|
5672
|
+
} catch {
|
|
5673
|
+
return false;
|
|
5674
|
+
}
|
|
5675
|
+
}
|
|
5676
|
+
async function countUnresolvedQuestions(assignmentDir) {
|
|
5677
|
+
const commentsPath = resolve13(assignmentDir, "comments.md");
|
|
5678
|
+
if (!await fileExists(commentsPath)) return 0;
|
|
5679
|
+
try {
|
|
5680
|
+
const content = await readFile10(commentsPath, "utf-8");
|
|
5681
|
+
let count = 0;
|
|
5682
|
+
for (const block of content.split(/^##\s+/m).slice(1)) {
|
|
5683
|
+
if (/^\*\*Type:\*\*\s*question\s*$/m.test(block) && /^\*\*Resolved:\*\*\s*false\s*$/m.test(block)) {
|
|
5684
|
+
count++;
|
|
5685
|
+
}
|
|
5686
|
+
}
|
|
5687
|
+
return count;
|
|
5688
|
+
} catch {
|
|
5689
|
+
return 0;
|
|
5690
|
+
}
|
|
5691
|
+
}
|
|
5692
|
+
async function areDependenciesSatisfied(projectDir, dependsOn, terminalStatuses) {
|
|
5693
|
+
if (dependsOn.length === 0 || projectDir === null) return true;
|
|
5694
|
+
for (const depSlug of dependsOn) {
|
|
5695
|
+
const depPath = resolve13(projectDir, "assignments", depSlug, "assignment.md");
|
|
5696
|
+
if (!await fileExists(depPath)) return false;
|
|
5697
|
+
try {
|
|
5698
|
+
const content = await readFile10(depPath, "utf-8");
|
|
5699
|
+
const m = content.match(/^status:\s*(.+)$/m);
|
|
5700
|
+
const status = m ? m[1].trim() : "";
|
|
5701
|
+
if (!terminalStatuses.has(status)) return false;
|
|
5702
|
+
} catch {
|
|
5703
|
+
return false;
|
|
5704
|
+
}
|
|
5705
|
+
}
|
|
5706
|
+
return true;
|
|
5707
|
+
}
|
|
5708
|
+
async function computeFacts(input) {
|
|
5709
|
+
const { assignmentDir, frontmatter, body, projectDir, terminalStatuses } = input;
|
|
5710
|
+
const ac = countRealAcceptanceCriteria(body);
|
|
5711
|
+
const [planFile, planApproved, unresolvedQuestions, depsSatisfied] = await Promise.all([
|
|
5712
|
+
latestPlanFile(assignmentDir),
|
|
5713
|
+
isPlanApproved(assignmentDir, frontmatter),
|
|
5714
|
+
countUnresolvedQuestions(assignmentDir),
|
|
5715
|
+
areDependenciesSatisfied(projectDir, frontmatter.dependsOn, terminalStatuses)
|
|
5716
|
+
]);
|
|
5717
|
+
return {
|
|
5718
|
+
hasRealObjective: hasRealObjective(body),
|
|
5719
|
+
acRealTotal: ac.total,
|
|
5720
|
+
acRealChecked: ac.checked,
|
|
5721
|
+
acAllChecked: ac.total > 0 && ac.checked === ac.total,
|
|
5722
|
+
planExists: planFile !== null,
|
|
5723
|
+
planApproved,
|
|
5724
|
+
workspaceSet: frontmatter.workspace.repository !== null && frontmatter.workspace.branch !== null,
|
|
5725
|
+
implementationStarted: frontmatter.implementationStarted,
|
|
5726
|
+
depsSatisfied,
|
|
5727
|
+
unresolvedQuestions,
|
|
5728
|
+
blocked: frontmatter.blockedReason !== null,
|
|
5729
|
+
parked: frontmatter.parked,
|
|
5730
|
+
reviewRequested: frontmatter.reviewRequested,
|
|
5731
|
+
pinned: frontmatter.override !== null
|
|
5732
|
+
};
|
|
5733
|
+
}
|
|
5734
|
+
var HTML_COMMENT_RE, PLAN_FILE_RE;
|
|
5735
|
+
var init_facts = __esm({
|
|
5736
|
+
"src/lifecycle/facts.ts"() {
|
|
5737
|
+
"use strict";
|
|
5738
|
+
init_fs();
|
|
5739
|
+
HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
5740
|
+
PLAN_FILE_RE = /^plan(?:-v(\d+))?\.md$/;
|
|
5741
|
+
}
|
|
5742
|
+
});
|
|
5743
|
+
|
|
5744
|
+
// src/utils/query/fields.ts
|
|
5745
|
+
function resolveField(registry, name) {
|
|
5746
|
+
return registry[name.toLowerCase()] ?? null;
|
|
5747
|
+
}
|
|
5748
|
+
function readField(def, fieldName, item) {
|
|
5749
|
+
if (def.get) return def.get(item);
|
|
5750
|
+
return item[fieldName] ?? item[fieldName.toLowerCase()];
|
|
5751
|
+
}
|
|
5752
|
+
var init_fields = __esm({
|
|
5753
|
+
"src/utils/query/fields.ts"() {
|
|
5754
|
+
"use strict";
|
|
5755
|
+
}
|
|
5756
|
+
});
|
|
5757
|
+
|
|
5758
|
+
// src/utils/query/evaluate.ts
|
|
5759
|
+
function localDayBounds(date) {
|
|
5760
|
+
const [y, m, d] = date.split("-").map((n) => parseInt(n, 10));
|
|
5761
|
+
const start = new Date(y, m - 1, d).getTime();
|
|
5762
|
+
const end = new Date(y, m - 1, d + 1).getTime();
|
|
5763
|
+
return [start, end];
|
|
5764
|
+
}
|
|
5765
|
+
function toEpoch(value) {
|
|
5766
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
5767
|
+
if (typeof value === "string" && value.length > 0) {
|
|
5768
|
+
const t = Date.parse(value);
|
|
5769
|
+
return Number.isNaN(t) ? null : t;
|
|
5770
|
+
}
|
|
5771
|
+
return null;
|
|
5772
|
+
}
|
|
5773
|
+
function toNumber(value) {
|
|
5774
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
5775
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
5776
|
+
const n = Number(value);
|
|
5777
|
+
return Number.isFinite(n) ? n : null;
|
|
5778
|
+
}
|
|
5779
|
+
return null;
|
|
5780
|
+
}
|
|
5781
|
+
function ciEquals(a, b) {
|
|
5782
|
+
return typeof a === "string" && a.toLowerCase() === b.toLowerCase();
|
|
5783
|
+
}
|
|
5784
|
+
function isNone(value) {
|
|
5785
|
+
return value === null || value === void 0 || value === "";
|
|
5786
|
+
}
|
|
5787
|
+
function compileEquality(def, field, value, atomPos) {
|
|
5788
|
+
switch (def.kind) {
|
|
5789
|
+
case "enum":
|
|
5790
|
+
case "string":
|
|
5791
|
+
if (def.noneSentinel && value.raw.toLowerCase() === "none") {
|
|
5792
|
+
return (item) => isNone(readField(def, field, item));
|
|
5793
|
+
}
|
|
5794
|
+
return (item) => ciEquals(readField(def, field, item), value.raw);
|
|
5795
|
+
case "substring":
|
|
5796
|
+
return (item) => {
|
|
5797
|
+
const v = readField(def, field, item);
|
|
5798
|
+
return typeof v === "string" && v.toLowerCase().includes(value.raw.toLowerCase());
|
|
5799
|
+
};
|
|
5800
|
+
case "bool": {
|
|
5801
|
+
const want = value.raw.toLowerCase();
|
|
5802
|
+
if (want !== "true" && want !== "false") {
|
|
5803
|
+
throw new CompileError([
|
|
5804
|
+
{ pos: value.pos, message: `Field "${field}" is boolean \u2014 use ${field}:true or ${field}:false` }
|
|
5805
|
+
]);
|
|
5806
|
+
}
|
|
5807
|
+
const expected = want === "true";
|
|
5808
|
+
return (item) => {
|
|
5809
|
+
const v = readField(def, field, item);
|
|
5810
|
+
const b = typeof v === "boolean" ? v : v === "true" ? true : v === "false" || v === null || v === void 0 || v === "" ? false : null;
|
|
5811
|
+
return b !== null && b === expected;
|
|
5812
|
+
};
|
|
5813
|
+
}
|
|
5814
|
+
case "number": {
|
|
5815
|
+
const n = value.num ?? toNumber(value.raw);
|
|
5816
|
+
if (n === null) {
|
|
5817
|
+
throw new CompileError([{ pos: value.pos, message: `Field "${field}" is numeric \u2014 "${value.raw}" is not a number` }]);
|
|
5818
|
+
}
|
|
5819
|
+
return (item) => toNumber(readField(def, field, item)) === n;
|
|
5820
|
+
}
|
|
5821
|
+
case "ordinal":
|
|
5822
|
+
return (item) => ciEquals(readField(def, field, item), value.raw);
|
|
5823
|
+
case "list":
|
|
5824
|
+
return (item) => {
|
|
5825
|
+
const v = readField(def, field, item);
|
|
5826
|
+
return Array.isArray(v) && v.some((el) => ciEquals(el, value.raw));
|
|
5827
|
+
};
|
|
5828
|
+
case "timestamp": {
|
|
5829
|
+
if (value.type === "date") {
|
|
5830
|
+
const [start, end] = localDayBounds(value.raw);
|
|
5831
|
+
return (item) => {
|
|
5832
|
+
const t = toEpoch(readField(def, field, item));
|
|
5833
|
+
return t !== null && t >= start && t < end;
|
|
5834
|
+
};
|
|
5835
|
+
}
|
|
5836
|
+
throw new CompileError([
|
|
5837
|
+
{ pos: value.pos, message: `Field "${field}" is a timestamp \u2014 use a comparison (e.g. ${field} > -36h) or an absolute date (${field}:2026-06-01)` }
|
|
5838
|
+
]);
|
|
5839
|
+
}
|
|
5840
|
+
case "duration":
|
|
5841
|
+
throw new CompileError([
|
|
5842
|
+
{ pos: atomPos, message: `Field "${field}" is a duration \u2014 use a comparison (e.g. ${field} > 3d)` }
|
|
5843
|
+
]);
|
|
5844
|
+
}
|
|
5845
|
+
}
|
|
5846
|
+
function compileComparison(def, field, op, value) {
|
|
5847
|
+
const cmp = (a, b) => {
|
|
5848
|
+
switch (op) {
|
|
5849
|
+
case "<":
|
|
5850
|
+
return a < b;
|
|
5851
|
+
case ">":
|
|
5852
|
+
return a > b;
|
|
5853
|
+
case "<=":
|
|
5854
|
+
return a <= b;
|
|
5855
|
+
case ">=":
|
|
5856
|
+
return a >= b;
|
|
5857
|
+
case "=":
|
|
5858
|
+
return a === b;
|
|
5859
|
+
case "!=":
|
|
5860
|
+
return a !== b;
|
|
5861
|
+
default:
|
|
5862
|
+
return false;
|
|
5863
|
+
}
|
|
5864
|
+
};
|
|
5865
|
+
switch (def.kind) {
|
|
5866
|
+
case "number": {
|
|
5867
|
+
const n = value.num ?? toNumber(value.raw);
|
|
5868
|
+
if (n === null) {
|
|
5869
|
+
throw new CompileError([{ pos: value.pos, message: `"${value.raw}" is not a number (field "${field}")` }]);
|
|
5870
|
+
}
|
|
5871
|
+
return (item) => {
|
|
5872
|
+
const v = toNumber(readField(def, field, item));
|
|
5873
|
+
return v !== null && cmp(v, n);
|
|
5874
|
+
};
|
|
5875
|
+
}
|
|
5876
|
+
case "ordinal": {
|
|
5877
|
+
const order = def.order ?? [];
|
|
5878
|
+
const idx = order.findIndex((o) => o.toLowerCase() === value.raw.toLowerCase());
|
|
5879
|
+
if (idx < 0) {
|
|
5880
|
+
throw new CompileError([
|
|
5881
|
+
{ pos: value.pos, message: `"${value.raw}" is not a valid ${field} (expected one of: ${order.join(", ")})` }
|
|
5882
|
+
]);
|
|
5883
|
+
}
|
|
5884
|
+
return (item) => {
|
|
5885
|
+
const raw2 = readField(def, field, item);
|
|
5886
|
+
const vIdx = typeof raw2 === "string" ? order.findIndex((o) => o.toLowerCase() === raw2.toLowerCase()) : -1;
|
|
5887
|
+
return vIdx >= 0 && cmp(vIdx, idx);
|
|
5888
|
+
};
|
|
5889
|
+
}
|
|
5890
|
+
case "timestamp": {
|
|
5891
|
+
if (value.type === "duration") {
|
|
5892
|
+
const sign = value.sign === 0 ? -1 : value.sign ?? -1;
|
|
5893
|
+
const offset = sign * (value.num ?? 0);
|
|
5894
|
+
return (item, ctx) => {
|
|
5895
|
+
const t = toEpoch(readField(def, field, item));
|
|
5896
|
+
return t !== null && cmp(t, ctx.now + offset);
|
|
5897
|
+
};
|
|
5898
|
+
}
|
|
5899
|
+
if (value.type === "date") {
|
|
5900
|
+
const [start, end] = localDayBounds(value.raw);
|
|
5901
|
+
return (item) => {
|
|
5902
|
+
const t = toEpoch(readField(def, field, item));
|
|
5903
|
+
if (t === null) return false;
|
|
5904
|
+
switch (op) {
|
|
5905
|
+
case "<":
|
|
5906
|
+
return t < start;
|
|
5907
|
+
case "<=":
|
|
5908
|
+
return t < end;
|
|
5909
|
+
case ">":
|
|
5910
|
+
return t >= end;
|
|
5911
|
+
case ">=":
|
|
5912
|
+
return t >= start;
|
|
5913
|
+
case "=":
|
|
5914
|
+
return t >= start && t < end;
|
|
5915
|
+
case "!=":
|
|
5916
|
+
return t < start || t >= end;
|
|
5917
|
+
default:
|
|
5918
|
+
return false;
|
|
5919
|
+
}
|
|
5920
|
+
};
|
|
5921
|
+
}
|
|
5922
|
+
throw new CompileError([
|
|
5923
|
+
{ pos: value.pos, message: `Compare timestamp field "${field}" to a duration (e.g. -36h) or a date (YYYY-MM-DD)` }
|
|
5924
|
+
]);
|
|
5925
|
+
}
|
|
5926
|
+
case "duration": {
|
|
5927
|
+
if (value.type !== "duration") {
|
|
5928
|
+
throw new CompileError([
|
|
5929
|
+
{ pos: value.pos, message: `Compare duration field "${field}" to a duration literal (e.g. 3d)` }
|
|
5930
|
+
]);
|
|
5931
|
+
}
|
|
5932
|
+
const magnitude = value.num ?? 0;
|
|
5933
|
+
return (item) => {
|
|
5934
|
+
const v = toNumber(readField(def, field, item));
|
|
5935
|
+
return v !== null && cmp(v, magnitude);
|
|
5936
|
+
};
|
|
5937
|
+
}
|
|
5938
|
+
case "enum":
|
|
5939
|
+
case "string":
|
|
5940
|
+
case "substring":
|
|
5941
|
+
case "list": {
|
|
5942
|
+
if (op === "=") {
|
|
5943
|
+
return compileEquality(def, field, value, value.pos);
|
|
5944
|
+
}
|
|
5945
|
+
if (op === "!=") {
|
|
5946
|
+
const eq = compileEquality(def, field, value, value.pos);
|
|
5947
|
+
return (item, ctx) => !eq(item, ctx);
|
|
5948
|
+
}
|
|
5949
|
+
throw new CompileError([
|
|
5950
|
+
{ pos: value.pos, message: `Field "${field}" does not support ordering comparisons (use ":" or "=").` }
|
|
5951
|
+
]);
|
|
5952
|
+
}
|
|
5953
|
+
case "bool": {
|
|
5954
|
+
if (op === "=" || op === "!=") {
|
|
5955
|
+
const eq = compileEquality(def, field, value, value.pos);
|
|
5956
|
+
return op === "=" ? eq : (item, ctx) => !eq(item, ctx);
|
|
5957
|
+
}
|
|
5958
|
+
throw new CompileError([{ pos: value.pos, message: `Field "${field}" is boolean \u2014 use ${field}:true / ${field}:false` }]);
|
|
5959
|
+
}
|
|
5960
|
+
}
|
|
5961
|
+
}
|
|
5962
|
+
function compileAtom(atom, registry) {
|
|
5963
|
+
const def = resolveField(registry, atom.field);
|
|
5964
|
+
if (!def) {
|
|
5965
|
+
throw new CompileError([{ pos: atom.pos, message: `Unknown field "${atom.field}"` }]);
|
|
5966
|
+
}
|
|
5967
|
+
if (atom.op === ":") {
|
|
5968
|
+
const preds = atom.values.map((v) => compileEquality(def, atom.field, v, atom.pos));
|
|
5969
|
+
if (preds.length === 1) return preds[0];
|
|
5970
|
+
return (item, ctx) => preds.some((p) => p(item, ctx));
|
|
5971
|
+
}
|
|
5972
|
+
return compileComparison(def, atom.field, atom.op, atom.values[0]);
|
|
5973
|
+
}
|
|
5974
|
+
function compileNode(node, registry) {
|
|
5975
|
+
switch (node.kind) {
|
|
5976
|
+
case "all":
|
|
5977
|
+
return () => true;
|
|
5978
|
+
case "atom":
|
|
5979
|
+
return compileAtom(node, registry);
|
|
5980
|
+
case "not": {
|
|
5981
|
+
const inner = compileNode(node.child, registry);
|
|
5982
|
+
return (item, ctx) => !inner(item, ctx);
|
|
5983
|
+
}
|
|
5984
|
+
case "and": {
|
|
5985
|
+
const preds = node.children.map((c) => compileNode(c, registry));
|
|
5986
|
+
return (item, ctx) => preds.every((p) => p(item, ctx));
|
|
5987
|
+
}
|
|
5988
|
+
case "or": {
|
|
5989
|
+
const preds = node.children.map((c) => compileNode(c, registry));
|
|
5990
|
+
return (item, ctx) => preds.some((p) => p(item, ctx));
|
|
5991
|
+
}
|
|
5992
|
+
}
|
|
5993
|
+
}
|
|
5994
|
+
var CompileError;
|
|
5995
|
+
var init_evaluate = __esm({
|
|
5996
|
+
"src/utils/query/evaluate.ts"() {
|
|
5997
|
+
"use strict";
|
|
5998
|
+
init_fields();
|
|
5999
|
+
CompileError = class extends Error {
|
|
6000
|
+
constructor(errors) {
|
|
6001
|
+
super(errors.map((e) => `${e.message} (at ${e.pos})`).join("; "));
|
|
6002
|
+
this.errors = errors;
|
|
6003
|
+
this.name = "CompileError";
|
|
6004
|
+
}
|
|
6005
|
+
};
|
|
6006
|
+
}
|
|
6007
|
+
});
|
|
6008
|
+
|
|
6009
|
+
// src/utils/query/lexer.ts
|
|
6010
|
+
function lex(input) {
|
|
6011
|
+
const tokens = [];
|
|
6012
|
+
let i = 0;
|
|
6013
|
+
const numberOrDuration = (start, sign) => {
|
|
6014
|
+
let j = i;
|
|
6015
|
+
while (j < input.length && /\d/.test(input[j])) j++;
|
|
6016
|
+
const digits = input.slice(i, j);
|
|
6017
|
+
let unit = "";
|
|
6018
|
+
while (j < input.length && /[a-z]/i.test(input[j])) {
|
|
6019
|
+
unit += input[j];
|
|
6020
|
+
j++;
|
|
6021
|
+
}
|
|
6022
|
+
i = j;
|
|
6023
|
+
if (unit.length > 0) {
|
|
6024
|
+
const ms = DURATION_MS[unit.toLowerCase()];
|
|
6025
|
+
if (ms === void 0) {
|
|
6026
|
+
throw new LexError(start, `Unknown duration unit "${unit}" (expected h, d, w, m, mo, or y)`);
|
|
6027
|
+
}
|
|
6028
|
+
return {
|
|
6029
|
+
type: "DURATION",
|
|
6030
|
+
text: input.slice(start, j),
|
|
6031
|
+
pos: start,
|
|
6032
|
+
num: parseInt(digits, 10) * ms,
|
|
6033
|
+
sign
|
|
6034
|
+
};
|
|
6035
|
+
}
|
|
6036
|
+
if (sign !== 0) {
|
|
6037
|
+
return { type: "NUMBER", text: input.slice(start, j), pos: start, num: sign * parseInt(digits, 10) };
|
|
6038
|
+
}
|
|
6039
|
+
return { type: "NUMBER", text: digits, pos: start, num: parseInt(digits, 10) };
|
|
6040
|
+
};
|
|
6041
|
+
while (i < input.length) {
|
|
6042
|
+
const c = input[i];
|
|
6043
|
+
const start = i;
|
|
6044
|
+
if (c === " " || c === " " || c === "\n" || c === "\r") {
|
|
6045
|
+
i++;
|
|
6046
|
+
continue;
|
|
6047
|
+
}
|
|
6048
|
+
if (c === "(") {
|
|
6049
|
+
tokens.push({ type: "LPAREN", text: c, pos: start });
|
|
6050
|
+
i++;
|
|
6051
|
+
continue;
|
|
6052
|
+
}
|
|
6053
|
+
if (c === ")") {
|
|
6054
|
+
tokens.push({ type: "RPAREN", text: c, pos: start });
|
|
6055
|
+
i++;
|
|
6056
|
+
continue;
|
|
6057
|
+
}
|
|
6058
|
+
if (c === ",") {
|
|
6059
|
+
tokens.push({ type: "COMMA", text: c, pos: start });
|
|
6060
|
+
i++;
|
|
6061
|
+
continue;
|
|
6062
|
+
}
|
|
6063
|
+
if (c === ":") {
|
|
6064
|
+
tokens.push({ type: "COLON", text: c, pos: start });
|
|
6065
|
+
i++;
|
|
6066
|
+
continue;
|
|
6067
|
+
}
|
|
6068
|
+
if (c === "*") {
|
|
6069
|
+
tokens.push({ type: "STAR", text: c, pos: start });
|
|
6070
|
+
i++;
|
|
6071
|
+
continue;
|
|
6072
|
+
}
|
|
6073
|
+
if (c === "<" || c === ">") {
|
|
6074
|
+
if (input[i + 1] === "=") {
|
|
6075
|
+
tokens.push({ type: "OP", text: c + "=", pos: start });
|
|
6076
|
+
i += 2;
|
|
6077
|
+
} else {
|
|
6078
|
+
tokens.push({ type: "OP", text: c, pos: start });
|
|
6079
|
+
i++;
|
|
6080
|
+
}
|
|
6081
|
+
continue;
|
|
6082
|
+
}
|
|
6083
|
+
if (c === "!") {
|
|
6084
|
+
if (input[i + 1] === "=") {
|
|
6085
|
+
tokens.push({ type: "OP", text: "!=", pos: start });
|
|
6086
|
+
i += 2;
|
|
6087
|
+
continue;
|
|
6088
|
+
}
|
|
6089
|
+
throw new LexError(start, `Unexpected "!" (did you mean "!="?)`);
|
|
6090
|
+
}
|
|
6091
|
+
if (c === "=") {
|
|
6092
|
+
i += input[i + 1] === "=" ? 2 : 1;
|
|
6093
|
+
tokens.push({ type: "OP", text: "=", pos: start });
|
|
6094
|
+
continue;
|
|
6095
|
+
}
|
|
6096
|
+
if (c === '"' || c === "'") {
|
|
6097
|
+
const quote = c;
|
|
6098
|
+
let j = i + 1;
|
|
6099
|
+
let out = "";
|
|
6100
|
+
while (j < input.length && input[j] !== quote) {
|
|
6101
|
+
if (input[j] === "\\" && j + 1 < input.length) {
|
|
6102
|
+
out += input[j + 1];
|
|
6103
|
+
j += 2;
|
|
6104
|
+
} else {
|
|
6105
|
+
out += input[j];
|
|
6106
|
+
j++;
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
6109
|
+
if (j >= input.length) throw new LexError(start, "Unterminated string literal");
|
|
6110
|
+
tokens.push({ type: "STRING", text: out, pos: start });
|
|
6111
|
+
i = j + 1;
|
|
6112
|
+
continue;
|
|
6113
|
+
}
|
|
6114
|
+
if (c === "-" || c === "+") {
|
|
6115
|
+
if (/\d/.test(input[i + 1] ?? "")) {
|
|
6116
|
+
const sign = c === "-" ? -1 : 1;
|
|
6117
|
+
i++;
|
|
6118
|
+
tokens.push(numberOrDuration(start, sign));
|
|
6119
|
+
continue;
|
|
6120
|
+
}
|
|
6121
|
+
if (c === "-") {
|
|
6122
|
+
tokens.push({ type: "MINUS", text: "-", pos: start });
|
|
6123
|
+
i++;
|
|
6124
|
+
continue;
|
|
6125
|
+
}
|
|
6126
|
+
throw new LexError(start, 'Unexpected "+"');
|
|
6127
|
+
}
|
|
6128
|
+
if (/\d/.test(c)) {
|
|
6129
|
+
const dateMatch = input.slice(i).match(DATE_RE);
|
|
6130
|
+
if (dateMatch) {
|
|
6131
|
+
tokens.push({ type: "DATE", text: dateMatch[0], pos: start });
|
|
6132
|
+
i += dateMatch[0].length;
|
|
6133
|
+
continue;
|
|
6134
|
+
}
|
|
6135
|
+
tokens.push(numberOrDuration(start, 0));
|
|
6136
|
+
continue;
|
|
6137
|
+
}
|
|
6138
|
+
if (IDENT_START.test(c)) {
|
|
6139
|
+
let j = i + 1;
|
|
6140
|
+
while (j < input.length && IDENT_CHAR.test(input[j])) j++;
|
|
6141
|
+
const word = input.slice(i, j);
|
|
6142
|
+
const kw = word.toLowerCase();
|
|
6143
|
+
if (kw === "and") tokens.push({ type: "AND", text: word, pos: start });
|
|
6144
|
+
else if (kw === "or") tokens.push({ type: "OR", text: word, pos: start });
|
|
6145
|
+
else if (kw === "not") tokens.push({ type: "NOT", text: word, pos: start });
|
|
6146
|
+
else tokens.push({ type: "IDENT", text: word, pos: start });
|
|
6147
|
+
i = j;
|
|
6148
|
+
continue;
|
|
6149
|
+
}
|
|
6150
|
+
throw new LexError(start, `Unexpected character "${c}"`);
|
|
6151
|
+
}
|
|
6152
|
+
tokens.push({ type: "EOF", text: "", pos: input.length });
|
|
6153
|
+
return tokens;
|
|
6154
|
+
}
|
|
6155
|
+
var LexError, DURATION_MS, IDENT_START, IDENT_CHAR, DATE_RE;
|
|
6156
|
+
var init_lexer = __esm({
|
|
6157
|
+
"src/utils/query/lexer.ts"() {
|
|
6158
|
+
"use strict";
|
|
6159
|
+
LexError = class extends Error {
|
|
6160
|
+
constructor(pos, message) {
|
|
6161
|
+
super(message);
|
|
6162
|
+
this.pos = pos;
|
|
6163
|
+
this.name = "LexError";
|
|
6164
|
+
}
|
|
6165
|
+
};
|
|
6166
|
+
DURATION_MS = {
|
|
6167
|
+
h: 36e5,
|
|
6168
|
+
d: 864e5,
|
|
6169
|
+
w: 7 * 864e5,
|
|
6170
|
+
m: 30 * 864e5,
|
|
6171
|
+
mo: 30 * 864e5,
|
|
6172
|
+
y: 365 * 864e5
|
|
6173
|
+
};
|
|
6174
|
+
IDENT_START = /[A-Za-z_]/;
|
|
6175
|
+
IDENT_CHAR = /[A-Za-z0-9_-]/;
|
|
6176
|
+
DATE_RE = /^\d{4}-\d{2}-\d{2}/;
|
|
6177
|
+
}
|
|
6178
|
+
});
|
|
6179
|
+
|
|
6180
|
+
// src/utils/query/parser.ts
|
|
6181
|
+
function parseQuery(input) {
|
|
6182
|
+
try {
|
|
6183
|
+
const tokens = lex(input);
|
|
6184
|
+
const ast = new Parser(tokens).parseQuery();
|
|
6185
|
+
return { ast, errors: [] };
|
|
6186
|
+
} catch (err) {
|
|
6187
|
+
if (err instanceof LexError || err instanceof ParseError) {
|
|
6188
|
+
return { ast: null, errors: [{ pos: err.pos, message: err.message }] };
|
|
6189
|
+
}
|
|
6190
|
+
throw err;
|
|
6191
|
+
}
|
|
6192
|
+
}
|
|
6193
|
+
var ParseError, VALUE_TOKENS, TERM_START, Parser;
|
|
6194
|
+
var init_parser3 = __esm({
|
|
6195
|
+
"src/utils/query/parser.ts"() {
|
|
6196
|
+
"use strict";
|
|
6197
|
+
init_lexer();
|
|
6198
|
+
ParseError = class extends Error {
|
|
6199
|
+
constructor(pos, message) {
|
|
6200
|
+
super(message);
|
|
6201
|
+
this.pos = pos;
|
|
6202
|
+
this.name = "ParseError";
|
|
6203
|
+
}
|
|
6204
|
+
};
|
|
6205
|
+
VALUE_TOKENS = /* @__PURE__ */ new Set(["IDENT", "STRING", "NUMBER", "DATE", "DURATION"]);
|
|
6206
|
+
TERM_START = /* @__PURE__ */ new Set(["IDENT", "NOT", "MINUS", "LPAREN", "STAR"]);
|
|
6207
|
+
Parser = class {
|
|
6208
|
+
constructor(tokens) {
|
|
6209
|
+
this.tokens = tokens;
|
|
6210
|
+
}
|
|
6211
|
+
pos = 0;
|
|
6212
|
+
peek() {
|
|
6213
|
+
return this.tokens[this.pos];
|
|
6214
|
+
}
|
|
6215
|
+
next() {
|
|
6216
|
+
return this.tokens[this.pos++];
|
|
6217
|
+
}
|
|
6218
|
+
expect(type, what) {
|
|
6219
|
+
const tok = this.peek();
|
|
6220
|
+
if (tok.type !== type) {
|
|
6221
|
+
throw new ParseError(tok.pos, `Expected ${what}, got "${tok.text || tok.type}"`);
|
|
6222
|
+
}
|
|
6223
|
+
return this.next();
|
|
6224
|
+
}
|
|
6225
|
+
parseQuery() {
|
|
6226
|
+
if (this.peek().type === "EOF") return { kind: "all" };
|
|
6227
|
+
const node = this.orExpr();
|
|
6228
|
+
const tok = this.peek();
|
|
6229
|
+
if (tok.type !== "EOF") {
|
|
6230
|
+
throw new ParseError(tok.pos, `Unexpected "${tok.text}" \u2014 unbalanced parentheses or stray token`);
|
|
6231
|
+
}
|
|
6232
|
+
return node;
|
|
6233
|
+
}
|
|
6234
|
+
orExpr() {
|
|
6235
|
+
const children = [this.andExpr()];
|
|
6236
|
+
while (this.peek().type === "OR") {
|
|
6237
|
+
this.next();
|
|
6238
|
+
children.push(this.andExpr());
|
|
6239
|
+
}
|
|
6240
|
+
return children.length === 1 ? children[0] : { kind: "or", children };
|
|
6241
|
+
}
|
|
6242
|
+
andExpr() {
|
|
6243
|
+
const children = [this.unary()];
|
|
6244
|
+
for (; ; ) {
|
|
6245
|
+
const tok = this.peek();
|
|
6246
|
+
if (tok.type === "AND") {
|
|
6247
|
+
this.next();
|
|
6248
|
+
children.push(this.unary());
|
|
6249
|
+
} else if (TERM_START.has(tok.type)) {
|
|
6250
|
+
children.push(this.unary());
|
|
6251
|
+
} else {
|
|
6252
|
+
break;
|
|
6253
|
+
}
|
|
6254
|
+
}
|
|
6255
|
+
return children.length === 1 ? children[0] : { kind: "and", children };
|
|
6256
|
+
}
|
|
6257
|
+
unary() {
|
|
6258
|
+
const tok = this.peek();
|
|
6259
|
+
if (tok.type === "NOT") {
|
|
6260
|
+
this.next();
|
|
6261
|
+
return { kind: "not", child: this.unary() };
|
|
6262
|
+
}
|
|
6263
|
+
if (tok.type === "MINUS") {
|
|
6264
|
+
this.next();
|
|
6265
|
+
const inner = this.peek();
|
|
6266
|
+
if (inner.type !== "IDENT") {
|
|
6267
|
+
throw new ParseError(inner.pos, 'Expected a field atom after "-" negation');
|
|
6268
|
+
}
|
|
6269
|
+
return { kind: "not", child: this.atom() };
|
|
6270
|
+
}
|
|
6271
|
+
return this.primary();
|
|
6272
|
+
}
|
|
6273
|
+
primary() {
|
|
6274
|
+
const tok = this.peek();
|
|
6275
|
+
if (tok.type === "LPAREN") {
|
|
6276
|
+
this.next();
|
|
6277
|
+
const node = this.orExpr();
|
|
6278
|
+
this.expect("RPAREN", '")"');
|
|
6279
|
+
return node;
|
|
6280
|
+
}
|
|
6281
|
+
if (tok.type === "STAR") {
|
|
6282
|
+
this.next();
|
|
6283
|
+
return { kind: "all" };
|
|
6284
|
+
}
|
|
6285
|
+
if (tok.type === "IDENT") {
|
|
6286
|
+
return this.atom();
|
|
6287
|
+
}
|
|
6288
|
+
throw new ParseError(tok.pos, `Expected a field, "(", "*", or NOT \u2014 got "${tok.text || "end of query"}"`);
|
|
6289
|
+
}
|
|
6290
|
+
atom() {
|
|
6291
|
+
const fieldTok = this.expect("IDENT", "a field name");
|
|
6292
|
+
const opTok = this.peek();
|
|
6293
|
+
if (opTok.type === "COLON") {
|
|
6294
|
+
this.next();
|
|
6295
|
+
const values = this.valueOrList();
|
|
6296
|
+
return { kind: "atom", field: fieldTok.text, op: ":", values, pos: fieldTok.pos };
|
|
6297
|
+
}
|
|
6298
|
+
if (opTok.type === "OP") {
|
|
6299
|
+
this.next();
|
|
6300
|
+
const value = this.value();
|
|
6301
|
+
return {
|
|
6302
|
+
kind: "atom",
|
|
6303
|
+
field: fieldTok.text,
|
|
6304
|
+
op: opTok.text,
|
|
6305
|
+
values: [value],
|
|
6306
|
+
pos: fieldTok.pos
|
|
6307
|
+
};
|
|
6308
|
+
}
|
|
6309
|
+
throw new ParseError(
|
|
6310
|
+
opTok.pos,
|
|
6311
|
+
`Expected ":" or a comparison operator after field "${fieldTok.text}"`
|
|
6312
|
+
);
|
|
6313
|
+
}
|
|
6314
|
+
valueOrList() {
|
|
6315
|
+
if (this.peek().type === "LPAREN") {
|
|
6316
|
+
this.next();
|
|
6317
|
+
const values = [this.value()];
|
|
6318
|
+
while (this.peek().type === "COMMA") {
|
|
6319
|
+
this.next();
|
|
6320
|
+
values.push(this.value());
|
|
6321
|
+
}
|
|
6322
|
+
this.expect("RPAREN", '")" to close the value list');
|
|
6323
|
+
return values;
|
|
6324
|
+
}
|
|
6325
|
+
return [this.value()];
|
|
6326
|
+
}
|
|
6327
|
+
value() {
|
|
6328
|
+
const tok = this.peek();
|
|
6329
|
+
if (!VALUE_TOKENS.has(tok.type)) {
|
|
6330
|
+
throw new ParseError(tok.pos, `Expected a value, got "${tok.text || tok.type}"`);
|
|
6331
|
+
}
|
|
6332
|
+
this.next();
|
|
6333
|
+
switch (tok.type) {
|
|
6334
|
+
case "STRING":
|
|
6335
|
+
return { type: "string", raw: tok.text, pos: tok.pos };
|
|
6336
|
+
case "NUMBER":
|
|
6337
|
+
return { type: "number", raw: tok.text, num: tok.num, pos: tok.pos };
|
|
6338
|
+
case "DATE":
|
|
6339
|
+
return { type: "date", raw: tok.text, pos: tok.pos };
|
|
6340
|
+
case "DURATION":
|
|
6341
|
+
return { type: "duration", raw: tok.text, num: tok.num, sign: tok.sign ?? 0, pos: tok.pos };
|
|
6342
|
+
default:
|
|
6343
|
+
return { type: "word", raw: tok.text, pos: tok.pos };
|
|
6344
|
+
}
|
|
6345
|
+
}
|
|
6346
|
+
};
|
|
6347
|
+
}
|
|
6348
|
+
});
|
|
6349
|
+
|
|
6350
|
+
// src/utils/query/index.ts
|
|
6351
|
+
var init_query = __esm({
|
|
6352
|
+
"src/utils/query/index.ts"() {
|
|
6353
|
+
"use strict";
|
|
6354
|
+
init_evaluate();
|
|
6355
|
+
init_fields();
|
|
6356
|
+
init_parser3();
|
|
6357
|
+
init_parser3();
|
|
6358
|
+
init_evaluate();
|
|
6359
|
+
init_fields();
|
|
6360
|
+
}
|
|
6361
|
+
});
|
|
6362
|
+
|
|
6363
|
+
// src/lifecycle/derive.ts
|
|
6364
|
+
var derive_exports = {};
|
|
6365
|
+
__export(derive_exports, {
|
|
6366
|
+
DERIVE_FIELDS: () => DERIVE_FIELDS,
|
|
6367
|
+
deriveDimensions: () => deriveDimensions,
|
|
6368
|
+
validateDeriveCondition: () => validateDeriveCondition
|
|
6369
|
+
});
|
|
6370
|
+
function validateDeriveCondition(when) {
|
|
6371
|
+
if (when === "*") return null;
|
|
6372
|
+
const parsed = parseQuery(when);
|
|
6373
|
+
if (!parsed.ast) return parsed.errors[0]?.message ?? "unparseable condition";
|
|
6374
|
+
try {
|
|
6375
|
+
compileNode(parsed.ast, DERIVE_FIELDS);
|
|
6376
|
+
return null;
|
|
6377
|
+
} catch (err) {
|
|
6378
|
+
if (err instanceof CompileError) return err.errors[0]?.message ?? "invalid condition";
|
|
6379
|
+
throw err;
|
|
6380
|
+
}
|
|
6381
|
+
}
|
|
6382
|
+
function compiledWhen(derive, when) {
|
|
6383
|
+
let cache2 = conditionCache.get(derive);
|
|
6384
|
+
if (!cache2) {
|
|
6385
|
+
cache2 = /* @__PURE__ */ new Map();
|
|
6386
|
+
conditionCache.set(derive, cache2);
|
|
6387
|
+
}
|
|
6388
|
+
let pred = cache2.get(when);
|
|
6389
|
+
if (!pred) {
|
|
6390
|
+
if (when === "*") {
|
|
6391
|
+
pred = () => true;
|
|
6392
|
+
} else {
|
|
6393
|
+
const parsed = parseQuery(when);
|
|
6394
|
+
if (!parsed.ast) {
|
|
6395
|
+
throw new CompileError(parsed.errors);
|
|
6396
|
+
}
|
|
6397
|
+
pred = compileNode(parsed.ast, DERIVE_FIELDS);
|
|
6398
|
+
}
|
|
6399
|
+
cache2.set(when, pred);
|
|
6400
|
+
}
|
|
6401
|
+
return pred;
|
|
6402
|
+
}
|
|
6403
|
+
function deriveDimensions(input) {
|
|
6404
|
+
const { facts, derive, currentStatus, terminalStatuses, knownStatusIds, override } = input;
|
|
6405
|
+
if (terminalStatuses.has(currentStatus)) return null;
|
|
6406
|
+
const ctx = { now: 0 };
|
|
6407
|
+
const item = facts;
|
|
6408
|
+
let phase = derive.phaseLadder[0]?.phase ?? currentStatus;
|
|
6409
|
+
let nextAction = derive.phaseLadder[0]?.next ?? null;
|
|
6410
|
+
for (let i = derive.phaseLadder.length - 1; i >= 0; i--) {
|
|
6411
|
+
const rung = derive.phaseLadder[i];
|
|
6412
|
+
if (compiledWhen(derive, rung.when)(item, ctx)) {
|
|
6413
|
+
phase = rung.phase;
|
|
6414
|
+
nextAction = rung.next ?? null;
|
|
6415
|
+
break;
|
|
6416
|
+
}
|
|
6417
|
+
}
|
|
6418
|
+
let disposition = "active";
|
|
6419
|
+
for (const rule of derive.disposition) {
|
|
6420
|
+
if (rule.when === null || compiledWhen(derive, rule.when)(item, ctx)) {
|
|
6421
|
+
disposition = rule.is;
|
|
6422
|
+
break;
|
|
6423
|
+
}
|
|
6424
|
+
}
|
|
6425
|
+
let derivedStatus;
|
|
6426
|
+
switch (disposition) {
|
|
6427
|
+
case "parked":
|
|
6428
|
+
derivedStatus = knownStatusIds.has(derive.headline.parked) ? derive.headline.parked : phase;
|
|
6429
|
+
break;
|
|
6430
|
+
case "blocked":
|
|
6431
|
+
derivedStatus = knownStatusIds.has(derive.headline.blocked) ? derive.headline.blocked : phase;
|
|
6432
|
+
break;
|
|
6433
|
+
default:
|
|
6434
|
+
derivedStatus = phase;
|
|
6435
|
+
}
|
|
6436
|
+
let status = derivedStatus;
|
|
6437
|
+
if (override && override.status && !terminalStatuses.has(override.status) && knownStatusIds.has(override.status)) {
|
|
6438
|
+
status = override.status;
|
|
5089
6439
|
}
|
|
5090
|
-
return
|
|
6440
|
+
return { phase, disposition, derivedStatus, status, nextAction };
|
|
5091
6441
|
}
|
|
5092
|
-
var
|
|
5093
|
-
var
|
|
5094
|
-
"src/
|
|
6442
|
+
var DERIVE_FIELDS, conditionCache;
|
|
6443
|
+
var init_derive = __esm({
|
|
6444
|
+
"src/lifecycle/derive.ts"() {
|
|
5095
6445
|
"use strict";
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
6446
|
+
init_query();
|
|
6447
|
+
DERIVE_FIELDS = {
|
|
6448
|
+
hasrealobjective: { kind: "bool", get: (i) => i["hasRealObjective"] },
|
|
6449
|
+
acrealtotal: { kind: "number", get: (i) => i["acRealTotal"] },
|
|
6450
|
+
acrealchecked: { kind: "number", get: (i) => i["acRealChecked"] },
|
|
6451
|
+
acallchecked: { kind: "bool", get: (i) => i["acAllChecked"] },
|
|
6452
|
+
planexists: { kind: "bool", get: (i) => i["planExists"] },
|
|
6453
|
+
planapproved: { kind: "bool", get: (i) => i["planApproved"] },
|
|
6454
|
+
workspaceset: { kind: "bool", get: (i) => i["workspaceSet"] },
|
|
6455
|
+
implementationstarted: { kind: "bool", get: (i) => i["implementationStarted"] },
|
|
6456
|
+
depssatisfied: { kind: "bool", get: (i) => i["depsSatisfied"] },
|
|
6457
|
+
unresolvedquestions: { kind: "number", get: (i) => i["unresolvedQuestions"] },
|
|
6458
|
+
blocked: { kind: "bool" },
|
|
6459
|
+
parked: { kind: "bool" },
|
|
6460
|
+
reviewrequested: { kind: "bool", get: (i) => i["reviewRequested"] },
|
|
6461
|
+
pinned: { kind: "bool" }
|
|
6462
|
+
};
|
|
6463
|
+
conditionCache = /* @__PURE__ */ new WeakMap();
|
|
5110
6464
|
}
|
|
5111
6465
|
});
|
|
5112
6466
|
|
|
5113
6467
|
// src/dashboard/api.ts
|
|
5114
|
-
import { readdir as
|
|
5115
|
-
import { resolve as
|
|
6468
|
+
import { readdir as readdir8, readFile as readFile11, writeFile as writeFile3 } from "fs/promises";
|
|
6469
|
+
import { resolve as resolve14, dirname as dirname2, basename } from "path";
|
|
5116
6470
|
function clearFrontmatterField(content, key) {
|
|
5117
6471
|
const fieldRegex = new RegExp(`^(${escapeRegExp2(key)}:)\\s*.*$`, "m");
|
|
5118
6472
|
return content.replace(fieldRegex, `$1 null`);
|
|
@@ -5191,15 +6545,15 @@ async function listStandaloneRecords(assignmentsDir2) {
|
|
|
5191
6545
|
async function computeStandaloneRecords(assignmentsDir2) {
|
|
5192
6546
|
if (!assignmentsDir2) return [];
|
|
5193
6547
|
if (!await fileExists(assignmentsDir2)) return [];
|
|
5194
|
-
const entries = await
|
|
6548
|
+
const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
|
|
5195
6549
|
const records = [];
|
|
5196
6550
|
for (const entry of entries) {
|
|
5197
6551
|
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
|
|
5198
|
-
const assignmentDir =
|
|
5199
|
-
const assignmentMdPath =
|
|
6552
|
+
const assignmentDir = resolve14(assignmentsDir2, entry.name);
|
|
6553
|
+
const assignmentMdPath = resolve14(assignmentDir, "assignment.md");
|
|
5200
6554
|
if (!await fileExists(assignmentMdPath)) continue;
|
|
5201
6555
|
try {
|
|
5202
|
-
const content = await
|
|
6556
|
+
const content = await readFile11(assignmentMdPath, "utf-8");
|
|
5203
6557
|
const record = parseAssignmentFull(content);
|
|
5204
6558
|
records.push({ assignmentDir, id: entry.name, record });
|
|
5205
6559
|
} catch {
|
|
@@ -5240,7 +6594,8 @@ async function getStatusConfig() {
|
|
|
5240
6594
|
order: config.statuses.order,
|
|
5241
6595
|
transitions: effectiveTransitions,
|
|
5242
6596
|
transitionTable: buildTransitionTable(effectiveTransitions),
|
|
5243
|
-
terminalStatuses: terminalSet.size > 0 ? terminalSet : /* @__PURE__ */ new Set(["completed", "failed"])
|
|
6597
|
+
terminalStatuses: terminalSet.size > 0 ? terminalSet : /* @__PURE__ */ new Set(["completed", "failed"]),
|
|
6598
|
+
derive: config.statuses.derive ?? null
|
|
5244
6599
|
};
|
|
5245
6600
|
} else {
|
|
5246
6601
|
const def = buildDefaultStatusConfig();
|
|
@@ -5250,7 +6605,8 @@ async function getStatusConfig() {
|
|
|
5250
6605
|
order: def.order,
|
|
5251
6606
|
transitions: def.transitions,
|
|
5252
6607
|
transitionTable: DEFAULT_TRANSITION_TABLE,
|
|
5253
|
-
terminalStatuses: /* @__PURE__ */ new Set(["completed", "failed"])
|
|
6608
|
+
terminalStatuses: /* @__PURE__ */ new Set(["completed", "failed"]),
|
|
6609
|
+
derive: null
|
|
5254
6610
|
};
|
|
5255
6611
|
}
|
|
5256
6612
|
return _cachedConfig;
|
|
@@ -5263,9 +6619,9 @@ async function listProjects(projectsDir) {
|
|
|
5263
6619
|
return projectRecords.filter((record) => !isProjectArchived(record.summary)).map((record) => record.summary);
|
|
5264
6620
|
}
|
|
5265
6621
|
async function readWorkspaceRegistry(projectsDir) {
|
|
5266
|
-
const registryPath =
|
|
6622
|
+
const registryPath = resolve14(dirname2(projectsDir), "workspaces.json");
|
|
5267
6623
|
try {
|
|
5268
|
-
const raw2 = await
|
|
6624
|
+
const raw2 = await readFile11(registryPath, "utf-8");
|
|
5269
6625
|
const parsed = JSON.parse(raw2);
|
|
5270
6626
|
return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
|
|
5271
6627
|
} catch {
|
|
@@ -5273,7 +6629,7 @@ async function readWorkspaceRegistry(projectsDir) {
|
|
|
5273
6629
|
}
|
|
5274
6630
|
}
|
|
5275
6631
|
async function writeWorkspaceRegistry(projectsDir, workspaces) {
|
|
5276
|
-
const registryPath =
|
|
6632
|
+
const registryPath = resolve14(dirname2(projectsDir), "workspaces.json");
|
|
5277
6633
|
await writeFile3(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
|
|
5278
6634
|
}
|
|
5279
6635
|
async function listWorkspaces(projectsDir, assignmentsDir2) {
|
|
@@ -5353,8 +6709,8 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
|
|
|
5353
6709
|
if (cascade) {
|
|
5354
6710
|
const timestamp = nowTimestamp();
|
|
5355
6711
|
for (const slug of projectsReferencing) {
|
|
5356
|
-
const path =
|
|
5357
|
-
const raw2 = await
|
|
6712
|
+
const path = resolve14(projectsDir, slug, "project.md");
|
|
6713
|
+
const raw2 = await readFile11(path, "utf-8");
|
|
5358
6714
|
let next = clearFrontmatterField(raw2, "workspace");
|
|
5359
6715
|
next = setUpdatedField(next, timestamp);
|
|
5360
6716
|
await writeFileForce(path, next);
|
|
@@ -5362,8 +6718,8 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
|
|
|
5362
6718
|
}
|
|
5363
6719
|
for (const id of standalonesReferencing) {
|
|
5364
6720
|
if (!opts.assignmentsDir) break;
|
|
5365
|
-
const path =
|
|
5366
|
-
const raw2 = await
|
|
6721
|
+
const path = resolve14(opts.assignmentsDir, id, "assignment.md");
|
|
6722
|
+
const raw2 = await readFile11(path, "utf-8");
|
|
5367
6723
|
let next = clearFrontmatterField(raw2, "workspaceGroup");
|
|
5368
6724
|
next = setUpdatedField(next, timestamp);
|
|
5369
6725
|
await writeFileForce(path, next);
|
|
@@ -5559,8 +6915,9 @@ async function listArchived(projectsDir, assignmentsDir2) {
|
|
|
5559
6915
|
return { projects, assignments: individuallyArchived };
|
|
5560
6916
|
}
|
|
5561
6917
|
async function toStandaloneBoardItem(sr) {
|
|
6918
|
+
const { terminalStatuses } = await getStatusConfig();
|
|
5562
6919
|
return {
|
|
5563
|
-
...toAssignmentSummary(sr.record),
|
|
6920
|
+
...toAssignmentSummary(sr.record, terminalStatuses),
|
|
5564
6921
|
projectSlug: null,
|
|
5565
6922
|
projectTitle: null,
|
|
5566
6923
|
blockedReason: sr.record.blockedReason,
|
|
@@ -5600,7 +6957,7 @@ async function getEditableDocument(projectsDir, documentType, projectSlug, assig
|
|
|
5600
6957
|
if (!filePath || !await fileExists(filePath)) {
|
|
5601
6958
|
return null;
|
|
5602
6959
|
}
|
|
5603
|
-
const content = await
|
|
6960
|
+
const content = await readFile11(filePath, "utf-8");
|
|
5604
6961
|
const title = getEditableDocumentTitle(documentType, projectSlug, assignmentSlug);
|
|
5605
6962
|
return {
|
|
5606
6963
|
documentType,
|
|
@@ -5624,9 +6981,9 @@ async function getEditableDocumentById(projectsDir, assignmentsDir2, documentTyp
|
|
|
5624
6981
|
}
|
|
5625
6982
|
const fileName = documentType === "assignment" ? "assignment.md" : documentType === "plan" ? "plan.md" : documentType === "scratchpad" ? "scratchpad.md" : documentType === "handoff" ? "handoff.md" : documentType === "decision-record" ? "decision-record.md" : null;
|
|
5626
6983
|
if (!fileName) return null;
|
|
5627
|
-
const filePath =
|
|
6984
|
+
const filePath = resolve14(resolved.assignmentDir, fileName);
|
|
5628
6985
|
if (!await fileExists(filePath)) return null;
|
|
5629
|
-
const content = await
|
|
6986
|
+
const content = await readFile11(filePath, "utf-8");
|
|
5630
6987
|
const label = resolved.id;
|
|
5631
6988
|
const title = documentType === "assignment" ? `Edit Assignment: ${label}` : documentType === "plan" ? `Edit Plan: ${label}` : documentType === "scratchpad" ? `Edit Scratchpad: ${label}` : documentType === "handoff" ? `Append Handoff: ${label}` : `Append Decision: ${label}`;
|
|
5632
6989
|
return {
|
|
@@ -5640,12 +6997,12 @@ async function getEditableDocumentById(projectsDir, assignmentsDir2, documentTyp
|
|
|
5640
6997
|
};
|
|
5641
6998
|
}
|
|
5642
6999
|
async function getProjectDetail(projectsDir, slug) {
|
|
5643
|
-
const projectPath =
|
|
5644
|
-
const projectMdPath =
|
|
7000
|
+
const projectPath = resolve14(projectsDir, slug);
|
|
7001
|
+
const projectMdPath = resolve14(projectPath, "project.md");
|
|
5645
7002
|
if (!await fileExists(projectMdPath)) {
|
|
5646
7003
|
return null;
|
|
5647
7004
|
}
|
|
5648
|
-
const projectContent = await
|
|
7005
|
+
const projectContent = await readFile11(projectMdPath, "utf-8");
|
|
5649
7006
|
const project = parseProject(projectContent);
|
|
5650
7007
|
const assignments = await listAssignmentRecords(projectPath);
|
|
5651
7008
|
const rollup = await buildProjectRollup(projectPath, project, assignments);
|
|
@@ -5653,6 +7010,7 @@ async function getProjectDetail(projectsDir, slug) {
|
|
|
5653
7010
|
const resources = await listResources(projectPath);
|
|
5654
7011
|
const memories = await listMemories(projectPath);
|
|
5655
7012
|
const updated = getProjectActivityTimestamp(project.updated, activeAssignments(assignments));
|
|
7013
|
+
const { terminalStatuses } = await getStatusConfig();
|
|
5656
7014
|
return {
|
|
5657
7015
|
slug: project.slug || slug,
|
|
5658
7016
|
title: project.title,
|
|
@@ -5668,7 +7026,7 @@ async function getProjectDetail(projectsDir, slug) {
|
|
|
5668
7026
|
body: project.body,
|
|
5669
7027
|
progress: rollup.progress,
|
|
5670
7028
|
needsAttention: rollup.needsAttention,
|
|
5671
|
-
assignments: assignments.map(toAssignmentSummary).sort((left, right) => compareTimestamps(right.updated, left.updated)),
|
|
7029
|
+
assignments: assignments.map((a) => toAssignmentSummary(a, terminalStatuses)).sort((left, right) => compareTimestamps(right.updated, left.updated)),
|
|
5672
7030
|
resources,
|
|
5673
7031
|
memories,
|
|
5674
7032
|
dependencyGraph,
|
|
@@ -5677,23 +7035,23 @@ async function getProjectDetail(projectsDir, slug) {
|
|
|
5677
7035
|
};
|
|
5678
7036
|
}
|
|
5679
7037
|
async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
5680
|
-
const assignmentDir =
|
|
5681
|
-
const assignmentMdPath =
|
|
7038
|
+
const assignmentDir = resolve14(projectsDir, projectSlug, "assignments", assignmentSlug);
|
|
7039
|
+
const assignmentMdPath = resolve14(assignmentDir, "assignment.md");
|
|
5682
7040
|
if (!await fileExists(assignmentMdPath)) {
|
|
5683
7041
|
return null;
|
|
5684
7042
|
}
|
|
5685
|
-
const assignmentContent = await
|
|
7043
|
+
const assignmentContent = await readFile11(assignmentMdPath, "utf-8");
|
|
5686
7044
|
const assignment = parseAssignmentFull(assignmentContent);
|
|
5687
7045
|
let projectWorkspace = null;
|
|
5688
|
-
const projectMdPath =
|
|
7046
|
+
const projectMdPath = resolve14(projectsDir, projectSlug, "project.md");
|
|
5689
7047
|
if (await fileExists(projectMdPath)) {
|
|
5690
|
-
const projectContent = await
|
|
7048
|
+
const projectContent = await readFile11(projectMdPath, "utf-8");
|
|
5691
7049
|
projectWorkspace = parseProject(projectContent).workspace;
|
|
5692
7050
|
}
|
|
5693
7051
|
let plan = null;
|
|
5694
|
-
const planPath =
|
|
7052
|
+
const planPath = resolve14(assignmentDir, "plan.md");
|
|
5695
7053
|
if (await fileExists(planPath)) {
|
|
5696
|
-
const planContent = await
|
|
7054
|
+
const planContent = await readFile11(planPath, "utf-8");
|
|
5697
7055
|
const parsed = parsePlan(planContent);
|
|
5698
7056
|
plan = {
|
|
5699
7057
|
status: parsed.status,
|
|
@@ -5702,9 +7060,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
5702
7060
|
};
|
|
5703
7061
|
}
|
|
5704
7062
|
let scratchpad = null;
|
|
5705
|
-
const scratchpadPath =
|
|
7063
|
+
const scratchpadPath = resolve14(assignmentDir, "scratchpad.md");
|
|
5706
7064
|
if (await fileExists(scratchpadPath)) {
|
|
5707
|
-
const scratchpadContent = await
|
|
7065
|
+
const scratchpadContent = await readFile11(scratchpadPath, "utf-8");
|
|
5708
7066
|
const parsed = parseScratchpad(scratchpadContent);
|
|
5709
7067
|
scratchpad = {
|
|
5710
7068
|
updated: parsed.updated,
|
|
@@ -5712,9 +7070,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
5712
7070
|
};
|
|
5713
7071
|
}
|
|
5714
7072
|
let handoff = null;
|
|
5715
|
-
const handoffPath =
|
|
7073
|
+
const handoffPath = resolve14(assignmentDir, "handoff.md");
|
|
5716
7074
|
if (await fileExists(handoffPath)) {
|
|
5717
|
-
const handoffContent = await
|
|
7075
|
+
const handoffContent = await readFile11(handoffPath, "utf-8");
|
|
5718
7076
|
const parsed = parseHandoff(handoffContent);
|
|
5719
7077
|
handoff = {
|
|
5720
7078
|
updated: parsed.updated,
|
|
@@ -5723,9 +7081,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
5723
7081
|
};
|
|
5724
7082
|
}
|
|
5725
7083
|
let decisionRecord = null;
|
|
5726
|
-
const decisionRecordPath =
|
|
7084
|
+
const decisionRecordPath = resolve14(assignmentDir, "decision-record.md");
|
|
5727
7085
|
if (await fileExists(decisionRecordPath)) {
|
|
5728
|
-
const decisionRecordContent = await
|
|
7086
|
+
const decisionRecordContent = await readFile11(decisionRecordPath, "utf-8");
|
|
5729
7087
|
const parsed = parseDecisionRecord(decisionRecordContent);
|
|
5730
7088
|
decisionRecord = {
|
|
5731
7089
|
updated: parsed.updated,
|
|
@@ -5734,9 +7092,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
5734
7092
|
};
|
|
5735
7093
|
}
|
|
5736
7094
|
let progress = null;
|
|
5737
|
-
const progressPath =
|
|
7095
|
+
const progressPath = resolve14(assignmentDir, "progress.md");
|
|
5738
7096
|
if (await fileExists(progressPath)) {
|
|
5739
|
-
const progressContent = await
|
|
7097
|
+
const progressContent = await readFile11(progressPath, "utf-8");
|
|
5740
7098
|
const parsed = parseProgress(progressContent);
|
|
5741
7099
|
progress = {
|
|
5742
7100
|
updated: parsed.updated,
|
|
@@ -5745,9 +7103,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
5745
7103
|
};
|
|
5746
7104
|
}
|
|
5747
7105
|
let comments = null;
|
|
5748
|
-
const commentsPath =
|
|
7106
|
+
const commentsPath = resolve14(assignmentDir, "comments.md");
|
|
5749
7107
|
if (await fileExists(commentsPath)) {
|
|
5750
|
-
const commentsContent = await
|
|
7108
|
+
const commentsContent = await readFile11(commentsPath, "utf-8");
|
|
5751
7109
|
const parsed = parseComments(commentsContent);
|
|
5752
7110
|
comments = {
|
|
5753
7111
|
updated: parsed.updated,
|
|
@@ -5755,6 +7113,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
5755
7113
|
entries: parsed.entries
|
|
5756
7114
|
};
|
|
5757
7115
|
}
|
|
7116
|
+
const { terminalStatuses } = await getStatusConfig();
|
|
5758
7117
|
const detail = {
|
|
5759
7118
|
id: assignment.id,
|
|
5760
7119
|
projectSlug,
|
|
@@ -5776,6 +7135,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
5776
7135
|
archived: assignment.archived,
|
|
5777
7136
|
archivedAt: assignment.archivedAt,
|
|
5778
7137
|
archivedReason: assignment.archivedReason,
|
|
7138
|
+
...deriveStatusVirtuals(assignment, terminalStatuses),
|
|
7139
|
+
override: assignment.override,
|
|
7140
|
+
derived: await buildDerivedDetail(assignment, assignmentDir, resolve14(projectsDir, projectSlug)),
|
|
5779
7141
|
created: assignment.created,
|
|
5780
7142
|
updated: assignment.updated,
|
|
5781
7143
|
body: assignment.body,
|
|
@@ -5866,7 +7228,7 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir2) {
|
|
|
5866
7228
|
slug: a.slug,
|
|
5867
7229
|
title: a.title,
|
|
5868
7230
|
projectSlug: rec.summary.slug,
|
|
5869
|
-
assignmentDir:
|
|
7231
|
+
assignmentDir: resolve14(rec.projectPath, "assignments", a.slug)
|
|
5870
7232
|
});
|
|
5871
7233
|
}
|
|
5872
7234
|
}
|
|
@@ -5899,17 +7261,17 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir2) {
|
|
|
5899
7261
|
}
|
|
5900
7262
|
async function countMentionsInAssignment(sourceDir, target) {
|
|
5901
7263
|
const bodies = [];
|
|
5902
|
-
const assignmentMd =
|
|
7264
|
+
const assignmentMd = resolve14(sourceDir, "assignment.md");
|
|
5903
7265
|
if (await fileExists(assignmentMd)) {
|
|
5904
|
-
const content = await
|
|
7266
|
+
const content = await readFile11(assignmentMd, "utf-8");
|
|
5905
7267
|
const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
|
|
5906
7268
|
if (todosMatch) bodies.push(todosMatch[1]);
|
|
5907
7269
|
}
|
|
5908
7270
|
for (const filename of ["progress.md", "comments.md", "handoff.md"]) {
|
|
5909
|
-
const path =
|
|
7271
|
+
const path = resolve14(sourceDir, filename);
|
|
5910
7272
|
if (await fileExists(path)) {
|
|
5911
7273
|
try {
|
|
5912
|
-
bodies.push(await
|
|
7274
|
+
bodies.push(await readFile11(path, "utf-8"));
|
|
5913
7275
|
} catch {
|
|
5914
7276
|
}
|
|
5915
7277
|
}
|
|
@@ -5967,46 +7329,47 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir2, id) {
|
|
|
5967
7329
|
}
|
|
5968
7330
|
async function buildStandaloneAssignmentDetail(resolved) {
|
|
5969
7331
|
const assignmentDir = resolved.assignmentDir;
|
|
5970
|
-
const assignmentMdPath =
|
|
7332
|
+
const assignmentMdPath = resolve14(assignmentDir, "assignment.md");
|
|
5971
7333
|
if (!await fileExists(assignmentMdPath)) return null;
|
|
5972
|
-
const assignmentContent = await
|
|
7334
|
+
const assignmentContent = await readFile11(assignmentMdPath, "utf-8");
|
|
5973
7335
|
const assignment = parseAssignmentFull(assignmentContent);
|
|
5974
7336
|
let plan = null;
|
|
5975
|
-
const planPath =
|
|
7337
|
+
const planPath = resolve14(assignmentDir, "plan.md");
|
|
5976
7338
|
if (await fileExists(planPath)) {
|
|
5977
|
-
const parsed = parsePlan(await
|
|
7339
|
+
const parsed = parsePlan(await readFile11(planPath, "utf-8"));
|
|
5978
7340
|
plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
|
|
5979
7341
|
}
|
|
5980
7342
|
let scratchpad = null;
|
|
5981
|
-
const scratchpadPath =
|
|
7343
|
+
const scratchpadPath = resolve14(assignmentDir, "scratchpad.md");
|
|
5982
7344
|
if (await fileExists(scratchpadPath)) {
|
|
5983
|
-
const parsed = parseScratchpad(await
|
|
7345
|
+
const parsed = parseScratchpad(await readFile11(scratchpadPath, "utf-8"));
|
|
5984
7346
|
scratchpad = { updated: parsed.updated, body: parsed.body };
|
|
5985
7347
|
}
|
|
5986
7348
|
let handoff = null;
|
|
5987
|
-
const handoffPath =
|
|
7349
|
+
const handoffPath = resolve14(assignmentDir, "handoff.md");
|
|
5988
7350
|
if (await fileExists(handoffPath)) {
|
|
5989
|
-
const parsed = parseHandoff(await
|
|
7351
|
+
const parsed = parseHandoff(await readFile11(handoffPath, "utf-8"));
|
|
5990
7352
|
handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
|
|
5991
7353
|
}
|
|
5992
7354
|
let decisionRecord = null;
|
|
5993
|
-
const decisionRecordPath =
|
|
7355
|
+
const decisionRecordPath = resolve14(assignmentDir, "decision-record.md");
|
|
5994
7356
|
if (await fileExists(decisionRecordPath)) {
|
|
5995
|
-
const parsed = parseDecisionRecord(await
|
|
7357
|
+
const parsed = parseDecisionRecord(await readFile11(decisionRecordPath, "utf-8"));
|
|
5996
7358
|
decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
|
|
5997
7359
|
}
|
|
5998
7360
|
let progress = null;
|
|
5999
|
-
const progressPath =
|
|
7361
|
+
const progressPath = resolve14(assignmentDir, "progress.md");
|
|
6000
7362
|
if (await fileExists(progressPath)) {
|
|
6001
|
-
const parsed = parseProgress(await
|
|
7363
|
+
const parsed = parseProgress(await readFile11(progressPath, "utf-8"));
|
|
6002
7364
|
progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
|
|
6003
7365
|
}
|
|
6004
7366
|
let comments = null;
|
|
6005
|
-
const commentsPath =
|
|
7367
|
+
const commentsPath = resolve14(assignmentDir, "comments.md");
|
|
6006
7368
|
if (await fileExists(commentsPath)) {
|
|
6007
|
-
const parsed = parseComments(await
|
|
7369
|
+
const parsed = parseComments(await readFile11(commentsPath, "utf-8"));
|
|
6008
7370
|
comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
|
|
6009
7371
|
}
|
|
7372
|
+
const { terminalStatuses } = await getStatusConfig();
|
|
6010
7373
|
const detail = {
|
|
6011
7374
|
id: assignment.id,
|
|
6012
7375
|
projectSlug: null,
|
|
@@ -6029,6 +7392,9 @@ async function buildStandaloneAssignmentDetail(resolved) {
|
|
|
6029
7392
|
archived: assignment.archived,
|
|
6030
7393
|
archivedAt: assignment.archivedAt,
|
|
6031
7394
|
archivedReason: assignment.archivedReason,
|
|
7395
|
+
...deriveStatusVirtuals(assignment, terminalStatuses),
|
|
7396
|
+
override: assignment.override,
|
|
7397
|
+
derived: await buildDerivedDetail(assignment, assignmentDir, null),
|
|
6032
7398
|
created: assignment.created,
|
|
6033
7399
|
updated: assignment.updated,
|
|
6034
7400
|
body: assignment.body,
|
|
@@ -6060,17 +7426,17 @@ async function computeProjectRecords(projectsDir, traces) {
|
|
|
6060
7426
|
await migrateLegacyProjectFiles(projectsDir);
|
|
6061
7427
|
await migrateLegacyArchivedProjects(projectsDir);
|
|
6062
7428
|
}
|
|
6063
|
-
const entries = await
|
|
7429
|
+
const entries = await readdir8(projectsDir, { withFileTypes: true });
|
|
6064
7430
|
const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
|
|
6065
7431
|
const maybeRecords = await Promise.all(
|
|
6066
7432
|
projectDirs.map(async (entry) => {
|
|
6067
|
-
const projectPath =
|
|
6068
|
-
const projectMdPath =
|
|
7433
|
+
const projectPath = resolve14(projectsDir, entry.name);
|
|
7434
|
+
const projectMdPath = resolve14(projectPath, "project.md");
|
|
6069
7435
|
if (!await fileExists(projectMdPath)) {
|
|
6070
7436
|
return null;
|
|
6071
7437
|
}
|
|
6072
7438
|
const t0 = traces ? performance.now() : 0;
|
|
6073
|
-
const projectContent = await
|
|
7439
|
+
const projectContent = await readFile11(projectMdPath, "utf-8");
|
|
6074
7440
|
const project = parseProject(projectContent);
|
|
6075
7441
|
if (traces) accumulatePhase(traces, "parse-project-md", performance.now() - t0);
|
|
6076
7442
|
const t1 = traces ? performance.now() : 0;
|
|
@@ -6111,20 +7477,20 @@ async function computeProjectRecords(projectsDir, traces) {
|
|
|
6111
7477
|
return records;
|
|
6112
7478
|
}
|
|
6113
7479
|
async function listAssignmentRecords(projectPath, traces) {
|
|
6114
|
-
const assignmentsDir2 =
|
|
7480
|
+
const assignmentsDir2 = resolve14(projectPath, "assignments");
|
|
6115
7481
|
if (!await fileExists(assignmentsDir2)) {
|
|
6116
7482
|
return [];
|
|
6117
7483
|
}
|
|
6118
|
-
const entries = await
|
|
7484
|
+
const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
|
|
6119
7485
|
const dirEntries = entries.filter((entry) => entry.isDirectory());
|
|
6120
7486
|
const maybeRecords = await Promise.all(
|
|
6121
7487
|
dirEntries.map(async (entry) => {
|
|
6122
|
-
const assignmentMd =
|
|
7488
|
+
const assignmentMd = resolve14(assignmentsDir2, entry.name, "assignment.md");
|
|
6123
7489
|
if (!await fileExists(assignmentMd)) {
|
|
6124
7490
|
return null;
|
|
6125
7491
|
}
|
|
6126
7492
|
const t0 = traces ? performance.now() : 0;
|
|
6127
|
-
const content = await
|
|
7493
|
+
const content = await readFile11(assignmentMd, "utf-8");
|
|
6128
7494
|
const parsed = parseAssignmentFull(content);
|
|
6129
7495
|
if (traces) accumulatePhase(traces, "read-assignment-md", performance.now() - t0);
|
|
6130
7496
|
return parsed;
|
|
@@ -6135,18 +7501,18 @@ async function listAssignmentRecords(projectPath, traces) {
|
|
|
6135
7501
|
return records;
|
|
6136
7502
|
}
|
|
6137
7503
|
async function listResources(projectPath) {
|
|
6138
|
-
const resourcesDir =
|
|
7504
|
+
const resourcesDir = resolve14(projectPath, "resources");
|
|
6139
7505
|
if (!await fileExists(resourcesDir)) {
|
|
6140
7506
|
return [];
|
|
6141
7507
|
}
|
|
6142
|
-
const entries = await
|
|
7508
|
+
const entries = await readdir8(resourcesDir, { withFileTypes: true });
|
|
6143
7509
|
const results = [];
|
|
6144
7510
|
for (const entry of entries) {
|
|
6145
7511
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
|
|
6146
7512
|
continue;
|
|
6147
7513
|
}
|
|
6148
|
-
const filePath =
|
|
6149
|
-
const content = await
|
|
7514
|
+
const filePath = resolve14(resourcesDir, entry.name);
|
|
7515
|
+
const content = await readFile11(filePath, "utf-8");
|
|
6150
7516
|
const parsed = parseResource(content);
|
|
6151
7517
|
results.push({
|
|
6152
7518
|
name: parsed.name,
|
|
@@ -6161,18 +7527,18 @@ async function listResources(projectPath) {
|
|
|
6161
7527
|
return results;
|
|
6162
7528
|
}
|
|
6163
7529
|
async function listMemories(projectPath) {
|
|
6164
|
-
const memoriesDir =
|
|
7530
|
+
const memoriesDir = resolve14(projectPath, "memories");
|
|
6165
7531
|
if (!await fileExists(memoriesDir)) {
|
|
6166
7532
|
return [];
|
|
6167
7533
|
}
|
|
6168
|
-
const entries = await
|
|
7534
|
+
const entries = await readdir8(memoriesDir, { withFileTypes: true });
|
|
6169
7535
|
const results = [];
|
|
6170
7536
|
for (const entry of entries) {
|
|
6171
7537
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
|
|
6172
7538
|
continue;
|
|
6173
7539
|
}
|
|
6174
|
-
const filePath =
|
|
6175
|
-
const content = await
|
|
7540
|
+
const filePath = resolve14(memoriesDir, entry.name);
|
|
7541
|
+
const content = await readFile11(filePath, "utf-8");
|
|
6176
7542
|
const parsed = parseMemory(content);
|
|
6177
7543
|
results.push({
|
|
6178
7544
|
name: parsed.name,
|
|
@@ -6220,8 +7586,8 @@ async function listAllResources(projectsDir) {
|
|
|
6220
7586
|
return all;
|
|
6221
7587
|
}
|
|
6222
7588
|
async function resolveProjectPath(projectsDir, projectSlug) {
|
|
6223
|
-
const direct =
|
|
6224
|
-
if (await fileExists(
|
|
7589
|
+
const direct = resolve14(projectsDir, projectSlug);
|
|
7590
|
+
if (await fileExists(resolve14(direct, "project.md"))) return direct;
|
|
6225
7591
|
const records = await listProjectRecords(projectsDir);
|
|
6226
7592
|
const match = records.find((r) => r.summary.slug === projectSlug);
|
|
6227
7593
|
return match ? match.projectPath : null;
|
|
@@ -6233,9 +7599,9 @@ async function getMemoryDetail(projectsDir, projectSlug, itemSlug) {
|
|
|
6233
7599
|
(p) => basename(p.projectPath) === projectSlug || p.summary.slug === projectSlug
|
|
6234
7600
|
);
|
|
6235
7601
|
if (!projectRecord) return null;
|
|
6236
|
-
const filePath =
|
|
7602
|
+
const filePath = resolve14(projectRecord.projectPath, "memories", `${itemSlug}.md`);
|
|
6237
7603
|
if (!await fileExists(filePath)) return null;
|
|
6238
|
-
const content = await
|
|
7604
|
+
const content = await readFile11(filePath, "utf-8");
|
|
6239
7605
|
const parsed = parseMemory(content);
|
|
6240
7606
|
return {
|
|
6241
7607
|
name: parsed.name,
|
|
@@ -6259,9 +7625,9 @@ async function getResourceDetail(projectsDir, projectSlug, itemSlug) {
|
|
|
6259
7625
|
(p) => basename(p.projectPath) === projectSlug || p.summary.slug === projectSlug
|
|
6260
7626
|
);
|
|
6261
7627
|
if (!projectRecord) return null;
|
|
6262
|
-
const filePath =
|
|
7628
|
+
const filePath = resolve14(projectRecord.projectPath, "resources", `${itemSlug}.md`);
|
|
6263
7629
|
if (!await fileExists(filePath)) return null;
|
|
6264
|
-
const content = await
|
|
7630
|
+
const content = await readFile11(filePath, "utf-8");
|
|
6265
7631
|
const parsed = parseResource(content);
|
|
6266
7632
|
return {
|
|
6267
7633
|
name: parsed.name,
|
|
@@ -6277,9 +7643,9 @@ async function getResourceDetail(projectsDir, projectSlug, itemSlug) {
|
|
|
6277
7643
|
};
|
|
6278
7644
|
}
|
|
6279
7645
|
async function loadDependencyGraph(projectPath, assignments) {
|
|
6280
|
-
const statusPath =
|
|
7646
|
+
const statusPath = resolve14(projectPath, "_status.md");
|
|
6281
7647
|
if (await fileExists(statusPath)) {
|
|
6282
|
-
const statusContent = await
|
|
7648
|
+
const statusContent = await readFile11(statusPath, "utf-8");
|
|
6283
7649
|
const parsed = parseStatus(statusContent);
|
|
6284
7650
|
const derivedGraph = extractMermaidGraph(parsed.body);
|
|
6285
7651
|
if (derivedGraph) {
|
|
@@ -6329,7 +7695,79 @@ async function buildProjectRollup(projectPath, project, assignments, traces) {
|
|
|
6329
7695
|
}
|
|
6330
7696
|
return { progress, needsAttention, status };
|
|
6331
7697
|
}
|
|
6332
|
-
function
|
|
7698
|
+
function deriveStatusVirtuals(assignment, terminalStatuses) {
|
|
7699
|
+
const hist = assignment.statusHistory ?? [];
|
|
7700
|
+
let completedAt = null;
|
|
7701
|
+
if (terminalStatuses.has(assignment.status)) {
|
|
7702
|
+
for (const entry of hist) {
|
|
7703
|
+
if (entry.to === assignment.status) completedAt = entry.at;
|
|
7704
|
+
}
|
|
7705
|
+
}
|
|
7706
|
+
let statusAge = null;
|
|
7707
|
+
for (let i = hist.length - 1; i >= 0; i--) {
|
|
7708
|
+
const entry = hist[i];
|
|
7709
|
+
if (entry.from !== entry.to || entry.from === null) {
|
|
7710
|
+
const t = Date.parse(entry.at);
|
|
7711
|
+
statusAge = Number.isNaN(t) ? null : Date.now() - t;
|
|
7712
|
+
break;
|
|
7713
|
+
}
|
|
7714
|
+
}
|
|
7715
|
+
let phaseAge = null;
|
|
7716
|
+
for (let i = hist.length - 1; i >= 0; i--) {
|
|
7717
|
+
const entry = hist[i];
|
|
7718
|
+
if (entry.phaseTo !== void 0 && entry.phaseFrom !== entry.phaseTo) {
|
|
7719
|
+
const t = Date.parse(entry.at);
|
|
7720
|
+
phaseAge = Number.isNaN(t) ? null : Date.now() - t;
|
|
7721
|
+
break;
|
|
7722
|
+
}
|
|
7723
|
+
}
|
|
7724
|
+
return {
|
|
7725
|
+
completedAt,
|
|
7726
|
+
statusAge,
|
|
7727
|
+
phaseAge,
|
|
7728
|
+
phase: assignment.phase,
|
|
7729
|
+
disposition: assignment.disposition,
|
|
7730
|
+
pinned: assignment.override !== null
|
|
7731
|
+
};
|
|
7732
|
+
}
|
|
7733
|
+
async function buildDerivedDetail(assignment, assignmentDir, projectDir) {
|
|
7734
|
+
const config = await getStatusConfig();
|
|
7735
|
+
if (config.terminalStatuses.has(assignment.status)) return null;
|
|
7736
|
+
try {
|
|
7737
|
+
const { computeFacts: computeFacts2 } = await Promise.resolve().then(() => (init_facts(), facts_exports));
|
|
7738
|
+
const { deriveDimensions: deriveDimensions2 } = await Promise.resolve().then(() => (init_derive(), derive_exports));
|
|
7739
|
+
const { DEFAULT_DERIVE_CONFIG: DEFAULT_DERIVE_CONFIG2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
|
|
7740
|
+
const facts = await computeFacts2({
|
|
7741
|
+
assignmentDir,
|
|
7742
|
+
frontmatter: {
|
|
7743
|
+
...assignment
|
|
7744
|
+
// AssignmentRecord ⊃ the fields computeFacts reads; statusHistory and
|
|
7745
|
+
// derived caches ride along untouched.
|
|
7746
|
+
},
|
|
7747
|
+
body: assignment.body,
|
|
7748
|
+
projectDir,
|
|
7749
|
+
terminalStatuses: config.terminalStatuses
|
|
7750
|
+
});
|
|
7751
|
+
const dims = deriveDimensions2({
|
|
7752
|
+
facts,
|
|
7753
|
+
derive: config.derive ?? DEFAULT_DERIVE_CONFIG2,
|
|
7754
|
+
currentStatus: assignment.status,
|
|
7755
|
+
terminalStatuses: config.terminalStatuses,
|
|
7756
|
+
knownStatusIds: new Set(config.statuses.map((s) => s.id)),
|
|
7757
|
+
override: assignment.override
|
|
7758
|
+
});
|
|
7759
|
+
if (!dims) return null;
|
|
7760
|
+
return {
|
|
7761
|
+
derivedStatus: dims.derivedStatus,
|
|
7762
|
+
nextAction: dims.nextAction,
|
|
7763
|
+
facts
|
|
7764
|
+
};
|
|
7765
|
+
} catch (err) {
|
|
7766
|
+
console.warn(`buildDerivedDetail failed for ${assignmentDir}:`, err);
|
|
7767
|
+
return null;
|
|
7768
|
+
}
|
|
7769
|
+
}
|
|
7770
|
+
function toAssignmentSummary(assignment, terminalStatuses) {
|
|
6333
7771
|
return {
|
|
6334
7772
|
id: assignment.id,
|
|
6335
7773
|
slug: assignment.slug,
|
|
@@ -6345,12 +7783,14 @@ function toAssignmentSummary(assignment) {
|
|
|
6345
7783
|
updated: assignment.updated,
|
|
6346
7784
|
archived: assignment.archived,
|
|
6347
7785
|
archivedAt: assignment.archivedAt,
|
|
6348
|
-
archivedReason: assignment.archivedReason
|
|
7786
|
+
archivedReason: assignment.archivedReason,
|
|
7787
|
+
...deriveStatusVirtuals(assignment, terminalStatuses)
|
|
6349
7788
|
};
|
|
6350
7789
|
}
|
|
6351
7790
|
async function toAssignmentBoardItem(projectsDir, projectRecord, assignment) {
|
|
7791
|
+
const { terminalStatuses } = await getStatusConfig();
|
|
6352
7792
|
return {
|
|
6353
|
-
...toAssignmentSummary(assignment),
|
|
7793
|
+
...toAssignmentSummary(assignment, terminalStatuses),
|
|
6354
7794
|
projectSlug: projectRecord.summary.slug,
|
|
6355
7795
|
projectTitle: projectRecord.summary.title,
|
|
6356
7796
|
blockedReason: assignment.blockedReason,
|
|
@@ -6393,7 +7833,7 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
|
|
|
6393
7833
|
const config = await getStatusConfig();
|
|
6394
7834
|
const transitionDefs = getTransitionDefinitions(config);
|
|
6395
7835
|
const actions = [];
|
|
6396
|
-
const projectPath =
|
|
7836
|
+
const projectPath = resolve14(projectsDir, projectSlug);
|
|
6397
7837
|
const traces = options?.traces;
|
|
6398
7838
|
for (const definition of transitionDefs) {
|
|
6399
7839
|
const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);
|
|
@@ -6441,12 +7881,12 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses, de
|
|
|
6441
7881
|
continue;
|
|
6442
7882
|
}
|
|
6443
7883
|
}
|
|
6444
|
-
const dependencyPath =
|
|
7884
|
+
const dependencyPath = resolve14(projectPath, "assignments", dependency, "assignment.md");
|
|
6445
7885
|
if (!await fileExists(dependencyPath)) {
|
|
6446
7886
|
unmet.push(`${dependency} (missing)`);
|
|
6447
7887
|
continue;
|
|
6448
7888
|
}
|
|
6449
|
-
const content = await
|
|
7889
|
+
const content = await readFile11(dependencyPath, "utf-8");
|
|
6450
7890
|
const parsed = parseAssignmentFull(content);
|
|
6451
7891
|
if (!terminals.has(parsed.status)) {
|
|
6452
7892
|
unmet.push(`${dependency} (${parsed.status})`);
|
|
@@ -6731,7 +8171,7 @@ function isStale(updated) {
|
|
|
6731
8171
|
return Date.now() - timestamp > STALE_ASSIGNMENT_MS;
|
|
6732
8172
|
}
|
|
6733
8173
|
async function countOpenQuestions(projectPath, assignmentSlug) {
|
|
6734
|
-
const commentsPath =
|
|
8174
|
+
const commentsPath = resolve14(
|
|
6735
8175
|
projectPath,
|
|
6736
8176
|
"assignments",
|
|
6737
8177
|
assignmentSlug,
|
|
@@ -6741,7 +8181,7 @@ async function countOpenQuestions(projectPath, assignmentSlug) {
|
|
|
6741
8181
|
return 0;
|
|
6742
8182
|
}
|
|
6743
8183
|
try {
|
|
6744
|
-
const content = await
|
|
8184
|
+
const content = await readFile11(commentsPath, "utf-8");
|
|
6745
8185
|
const parsed = parseComments(content);
|
|
6746
8186
|
return parsed.entries.filter(
|
|
6747
8187
|
(e) => e.type === "question" && e.resolved !== true
|
|
@@ -6762,21 +8202,21 @@ function getProjectActivityTimestamp(projectUpdated, assignments) {
|
|
|
6762
8202
|
function getDocumentPath(projectsDir, documentType, projectSlug, assignmentSlug) {
|
|
6763
8203
|
switch (documentType) {
|
|
6764
8204
|
case "project":
|
|
6765
|
-
return
|
|
8205
|
+
return resolve14(projectsDir, projectSlug, "project.md");
|
|
6766
8206
|
case "assignment":
|
|
6767
|
-
return assignmentSlug ?
|
|
8207
|
+
return assignmentSlug ? resolve14(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
|
|
6768
8208
|
case "plan":
|
|
6769
|
-
return assignmentSlug ?
|
|
8209
|
+
return assignmentSlug ? resolve14(projectsDir, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
|
|
6770
8210
|
case "scratchpad":
|
|
6771
|
-
return assignmentSlug ?
|
|
8211
|
+
return assignmentSlug ? resolve14(projectsDir, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
|
|
6772
8212
|
case "handoff":
|
|
6773
|
-
return assignmentSlug ?
|
|
8213
|
+
return assignmentSlug ? resolve14(projectsDir, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
|
|
6774
8214
|
case "decision-record":
|
|
6775
|
-
return assignmentSlug ?
|
|
8215
|
+
return assignmentSlug ? resolve14(projectsDir, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
|
|
6776
8216
|
case "memory":
|
|
6777
|
-
return assignmentSlug ?
|
|
8217
|
+
return assignmentSlug ? resolve14(projectsDir, projectSlug, "memories", `${assignmentSlug}.md`) : null;
|
|
6778
8218
|
case "resource":
|
|
6779
|
-
return assignmentSlug ?
|
|
8219
|
+
return assignmentSlug ? resolve14(projectsDir, projectSlug, "resources", `${assignmentSlug}.md`) : null;
|
|
6780
8220
|
default:
|
|
6781
8221
|
return null;
|
|
6782
8222
|
}
|
|
@@ -6809,12 +8249,12 @@ async function listPlaybooks(playbooksDir2) {
|
|
|
6809
8249
|
if (!await fileExists(playbooksDir2)) return [];
|
|
6810
8250
|
const config = await readConfig();
|
|
6811
8251
|
const disabledSet = new Set(config.playbooks.disabled);
|
|
6812
|
-
const entries = await
|
|
8252
|
+
const entries = await readdir8(playbooksDir2, { withFileTypes: true });
|
|
6813
8253
|
const playbooks = [];
|
|
6814
8254
|
for (const entry of entries) {
|
|
6815
8255
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
|
|
6816
|
-
const filePath =
|
|
6817
|
-
const raw2 = await
|
|
8256
|
+
const filePath = resolve14(playbooksDir2, entry.name);
|
|
8257
|
+
const raw2 = await readFile11(filePath, "utf-8");
|
|
6818
8258
|
const parsed = parsePlaybook(raw2);
|
|
6819
8259
|
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
6820
8260
|
playbooks.push({
|
|
@@ -6977,6 +8417,244 @@ var init_api = __esm({
|
|
|
6977
8417
|
}
|
|
6978
8418
|
});
|
|
6979
8419
|
|
|
8420
|
+
// src/lifecycle/recompute.ts
|
|
8421
|
+
var recompute_exports = {};
|
|
8422
|
+
__export(recompute_exports, {
|
|
8423
|
+
isDeriveMigrated: () => isDeriveMigrated,
|
|
8424
|
+
markDeriveMigrated: () => markDeriveMigrated,
|
|
8425
|
+
recomputeAll: () => recomputeAll,
|
|
8426
|
+
recomputeAndWrite: () => recomputeAndWrite,
|
|
8427
|
+
recomputeDependents: () => recomputeDependents,
|
|
8428
|
+
resolveDeriveContext: () => resolveDeriveContext
|
|
8429
|
+
});
|
|
8430
|
+
import { createHash as createHash2 } from "crypto";
|
|
8431
|
+
import { open, readdir as readdir10, readFile as readFile16, unlink as unlink5, stat } from "fs/promises";
|
|
8432
|
+
import { dirname as dirname4, resolve as resolve19 } from "path";
|
|
8433
|
+
async function isDeriveMigrated() {
|
|
8434
|
+
return fileExists(resolve19(syntaurRoot(), MIGRATION_MARKER));
|
|
8435
|
+
}
|
|
8436
|
+
async function markDeriveMigrated() {
|
|
8437
|
+
await writeFileForce(resolve19(syntaurRoot(), MIGRATION_MARKER), `${nowTimestamp()}
|
|
8438
|
+
`);
|
|
8439
|
+
}
|
|
8440
|
+
async function resolveDeriveContext() {
|
|
8441
|
+
const config = await readConfig();
|
|
8442
|
+
const statusConfig = config.statuses ?? buildDefaultStatusConfig();
|
|
8443
|
+
const terminal = new Set(statusConfig.statuses.filter((s) => s.terminal).map((s) => s.id));
|
|
8444
|
+
return {
|
|
8445
|
+
derive: config.statuses?.derive ?? DEFAULT_DERIVE_CONFIG,
|
|
8446
|
+
terminalStatuses: terminal.size > 0 ? terminal : DEFAULT_TERMINAL_STATUSES,
|
|
8447
|
+
knownStatusIds: new Set(statusConfig.statuses.map((s) => s.id))
|
|
8448
|
+
};
|
|
8449
|
+
}
|
|
8450
|
+
async function acquireLock(assignmentDir) {
|
|
8451
|
+
const lockPath = resolve19(assignmentDir, LOCK_FILE);
|
|
8452
|
+
const token = `${process.pid}:${createHash2("sha256").update(`${Math.random()}${Date.now()}`).digest("hex").slice(0, 12)}`;
|
|
8453
|
+
for (let attempt = 0; attempt <= LOCK_MAX_WAITS; attempt++) {
|
|
8454
|
+
try {
|
|
8455
|
+
const handle = await open(lockPath, "wx");
|
|
8456
|
+
await handle.writeFile(token, "utf-8");
|
|
8457
|
+
await handle.close();
|
|
8458
|
+
return async () => {
|
|
8459
|
+
try {
|
|
8460
|
+
const current = await readFile16(lockPath, "utf-8");
|
|
8461
|
+
if (current === token) await unlink5(lockPath);
|
|
8462
|
+
} catch {
|
|
8463
|
+
}
|
|
8464
|
+
};
|
|
8465
|
+
} catch (err) {
|
|
8466
|
+
const code = err.code;
|
|
8467
|
+
if (code !== "EEXIST") throw err;
|
|
8468
|
+
try {
|
|
8469
|
+
const info = await stat(lockPath);
|
|
8470
|
+
if (Date.now() - info.mtimeMs > LOCK_STALE_MS) {
|
|
8471
|
+
await unlink5(lockPath).catch(() => {
|
|
8472
|
+
});
|
|
8473
|
+
continue;
|
|
8474
|
+
}
|
|
8475
|
+
} catch {
|
|
8476
|
+
continue;
|
|
8477
|
+
}
|
|
8478
|
+
await new Promise((r) => setTimeout(r, LOCK_WAIT_MS));
|
|
8479
|
+
}
|
|
8480
|
+
}
|
|
8481
|
+
throw new Error(`Timed out waiting for ${lockPath} (held > ${LOCK_WAIT_MS * LOCK_MAX_WAITS / 1e3}s)`);
|
|
8482
|
+
}
|
|
8483
|
+
function contentHash(content) {
|
|
8484
|
+
return createHash2("sha256").update(content, "utf-8").digest("hex");
|
|
8485
|
+
}
|
|
8486
|
+
function extractBody(content) {
|
|
8487
|
+
const m = content.match(/^---\n[\s\S]*?\n---/);
|
|
8488
|
+
return m ? content.slice(m[0].length) : content;
|
|
8489
|
+
}
|
|
8490
|
+
async function recomputeAndWrite(assignmentPath, opts) {
|
|
8491
|
+
const assignmentDir = dirname4(assignmentPath);
|
|
8492
|
+
const release = await acquireLock(assignmentDir);
|
|
8493
|
+
try {
|
|
8494
|
+
for (let attempt = 0; attempt < CAS_RETRIES; attempt++) {
|
|
8495
|
+
const original = await readFile16(assignmentPath, "utf-8");
|
|
8496
|
+
const hash = contentHash(original);
|
|
8497
|
+
const originalFm = parseAssignmentFrontmatter(original);
|
|
8498
|
+
if (opts.context.terminalStatuses.has(originalFm.status)) {
|
|
8499
|
+
return { changed: false, status: originalFm.status, dimensions: null, deferredTerminal: true };
|
|
8500
|
+
}
|
|
8501
|
+
const content = opts.mutate ? await opts.mutate(original) : original;
|
|
8502
|
+
const mutated = content !== original;
|
|
8503
|
+
const frontmatter2 = mutated ? parseAssignmentFrontmatter(content) : originalFm;
|
|
8504
|
+
const facts = await computeFacts({
|
|
8505
|
+
assignmentDir,
|
|
8506
|
+
frontmatter: frontmatter2,
|
|
8507
|
+
body: extractBody(content),
|
|
8508
|
+
projectDir: opts.projectDir,
|
|
8509
|
+
terminalStatuses: opts.context.terminalStatuses
|
|
8510
|
+
});
|
|
8511
|
+
const dims = deriveDimensions({
|
|
8512
|
+
facts,
|
|
8513
|
+
derive: opts.context.derive,
|
|
8514
|
+
currentStatus: frontmatter2.status,
|
|
8515
|
+
terminalStatuses: opts.context.terminalStatuses,
|
|
8516
|
+
knownStatusIds: opts.context.knownStatusIds,
|
|
8517
|
+
override: frontmatter2.override
|
|
8518
|
+
});
|
|
8519
|
+
if (dims === null) {
|
|
8520
|
+
return { changed: false, status: frontmatter2.status, dimensions: null, deferredTerminal: true };
|
|
8521
|
+
}
|
|
8522
|
+
const statusChanged = dims.status !== frontmatter2.status;
|
|
8523
|
+
const phaseChanged = dims.phase !== frontmatter2.phase;
|
|
8524
|
+
const dispositionChanged = dims.disposition !== frontmatter2.disposition;
|
|
8525
|
+
if (!statusChanged && !phaseChanged && !dispositionChanged) {
|
|
8526
|
+
if (mutated) {
|
|
8527
|
+
const current2 = await readFile16(assignmentPath, "utf-8");
|
|
8528
|
+
if (contentHash(current2) !== hash) continue;
|
|
8529
|
+
await writeFileForce(assignmentPath, content);
|
|
8530
|
+
return { changed: true, status: frontmatter2.status, dimensions: dims, deferredTerminal: false };
|
|
8531
|
+
}
|
|
8532
|
+
return { changed: false, status: frontmatter2.status, dimensions: dims, deferredTerminal: false };
|
|
8533
|
+
}
|
|
8534
|
+
const at = nowTimestamp();
|
|
8535
|
+
let next = updateAssignmentFile(content, {
|
|
8536
|
+
status: dims.status,
|
|
8537
|
+
phase: dims.phase,
|
|
8538
|
+
disposition: dims.disposition,
|
|
8539
|
+
updated: at
|
|
8540
|
+
});
|
|
8541
|
+
next = appendStatusHistoryEntry(next, {
|
|
8542
|
+
at,
|
|
8543
|
+
from: frontmatter2.status,
|
|
8544
|
+
to: dims.status,
|
|
8545
|
+
command: opts.cause,
|
|
8546
|
+
by: opts.by,
|
|
8547
|
+
...opts.reason !== void 0 ? { reason: opts.reason } : {},
|
|
8548
|
+
...phaseChanged ? { phaseFrom: frontmatter2.phase, phaseTo: dims.phase } : {},
|
|
8549
|
+
...dispositionChanged ? { dispositionFrom: frontmatter2.disposition, dispositionTo: dims.disposition } : {}
|
|
8550
|
+
});
|
|
8551
|
+
const current = await readFile16(assignmentPath, "utf-8");
|
|
8552
|
+
if (contentHash(current) !== hash) {
|
|
8553
|
+
continue;
|
|
8554
|
+
}
|
|
8555
|
+
await writeFileForce(assignmentPath, next);
|
|
8556
|
+
return { changed: true, status: dims.status, dimensions: dims, deferredTerminal: false };
|
|
8557
|
+
}
|
|
8558
|
+
const frontmatter = parseAssignmentFrontmatter(await readFile16(assignmentPath, "utf-8"));
|
|
8559
|
+
return {
|
|
8560
|
+
changed: false,
|
|
8561
|
+
status: frontmatter.status,
|
|
8562
|
+
dimensions: null,
|
|
8563
|
+
deferredTerminal: false,
|
|
8564
|
+
warning: `recompute skipped after ${CAS_RETRIES} concurrent-edit retries: ${assignmentPath}`
|
|
8565
|
+
};
|
|
8566
|
+
} finally {
|
|
8567
|
+
await release();
|
|
8568
|
+
}
|
|
8569
|
+
}
|
|
8570
|
+
async function recomputeDependents(projectDir, changedSlug, opts) {
|
|
8571
|
+
const assignmentsDir2 = resolve19(projectDir, "assignments");
|
|
8572
|
+
let entries;
|
|
8573
|
+
try {
|
|
8574
|
+
entries = await readdir10(assignmentsDir2);
|
|
8575
|
+
} catch {
|
|
8576
|
+
return [];
|
|
8577
|
+
}
|
|
8578
|
+
const results = [];
|
|
8579
|
+
for (const slug of entries) {
|
|
8580
|
+
if (slug === changedSlug) continue;
|
|
8581
|
+
const path = resolve19(assignmentsDir2, slug, "assignment.md");
|
|
8582
|
+
if (!await fileExists(path)) continue;
|
|
8583
|
+
try {
|
|
8584
|
+
const fm = parseAssignmentFrontmatter(await readFile16(path, "utf-8"));
|
|
8585
|
+
if (!fm.dependsOn.includes(changedSlug)) continue;
|
|
8586
|
+
results.push(await recomputeAndWrite(path, { ...opts, projectDir }));
|
|
8587
|
+
} catch {
|
|
8588
|
+
}
|
|
8589
|
+
}
|
|
8590
|
+
return results;
|
|
8591
|
+
}
|
|
8592
|
+
async function recomputeAll(projectsDir, standaloneDir, opts) {
|
|
8593
|
+
const summary = { scanned: 0, changed: 0, deferredTerminal: 0, warnings: [] };
|
|
8594
|
+
const sweepOne = async (path, projectDir) => {
|
|
8595
|
+
summary.scanned++;
|
|
8596
|
+
try {
|
|
8597
|
+
const result = await recomputeAndWrite(path, { ...opts, projectDir });
|
|
8598
|
+
if (result.changed) summary.changed++;
|
|
8599
|
+
if (result.deferredTerminal) summary.deferredTerminal++;
|
|
8600
|
+
if (result.warning) summary.warnings.push(result.warning);
|
|
8601
|
+
} catch (err) {
|
|
8602
|
+
summary.warnings.push(`${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
8603
|
+
}
|
|
8604
|
+
};
|
|
8605
|
+
let projects = [];
|
|
8606
|
+
try {
|
|
8607
|
+
projects = await readdir10(projectsDir);
|
|
8608
|
+
} catch {
|
|
8609
|
+
}
|
|
8610
|
+
for (const project of projects) {
|
|
8611
|
+
const projectDir = resolve19(projectsDir, project);
|
|
8612
|
+
const assignmentsDir2 = resolve19(projectDir, "assignments");
|
|
8613
|
+
let slugs = [];
|
|
8614
|
+
try {
|
|
8615
|
+
slugs = await readdir10(assignmentsDir2);
|
|
8616
|
+
} catch {
|
|
8617
|
+
continue;
|
|
8618
|
+
}
|
|
8619
|
+
for (const slug of slugs) {
|
|
8620
|
+
const path = resolve19(assignmentsDir2, slug, "assignment.md");
|
|
8621
|
+
if (await fileExists(path)) await sweepOne(path, projectDir);
|
|
8622
|
+
}
|
|
8623
|
+
}
|
|
8624
|
+
if (standaloneDir) {
|
|
8625
|
+
let ids = [];
|
|
8626
|
+
try {
|
|
8627
|
+
ids = await readdir10(standaloneDir);
|
|
8628
|
+
} catch {
|
|
8629
|
+
}
|
|
8630
|
+
for (const id of ids) {
|
|
8631
|
+
const path = resolve19(standaloneDir, id, "assignment.md");
|
|
8632
|
+
if (await fileExists(path)) await sweepOne(path, null);
|
|
8633
|
+
}
|
|
8634
|
+
}
|
|
8635
|
+
return summary;
|
|
8636
|
+
}
|
|
8637
|
+
var LOCK_FILE, LOCK_STALE_MS, LOCK_WAIT_MS, LOCK_MAX_WAITS, CAS_RETRIES, MIGRATION_MARKER;
|
|
8638
|
+
var init_recompute = __esm({
|
|
8639
|
+
"src/lifecycle/recompute.ts"() {
|
|
8640
|
+
"use strict";
|
|
8641
|
+
init_config2();
|
|
8642
|
+
init_fs();
|
|
8643
|
+
init_paths();
|
|
8644
|
+
init_timestamp();
|
|
8645
|
+
init_facts();
|
|
8646
|
+
init_derive();
|
|
8647
|
+
init_frontmatter();
|
|
8648
|
+
init_types();
|
|
8649
|
+
LOCK_FILE = ".derive.lock";
|
|
8650
|
+
LOCK_STALE_MS = 3e4;
|
|
8651
|
+
LOCK_WAIT_MS = 50;
|
|
8652
|
+
LOCK_MAX_WAITS = 100;
|
|
8653
|
+
CAS_RETRIES = 3;
|
|
8654
|
+
MIGRATION_MARKER = "derive-migrated";
|
|
8655
|
+
}
|
|
8656
|
+
});
|
|
8657
|
+
|
|
6980
8658
|
// src/utils/assignment-todos.ts
|
|
6981
8659
|
var assignment_todos_exports = {};
|
|
6982
8660
|
__export(assignment_todos_exports, {
|
|
@@ -7031,8 +8709,8 @@ init_assignment_resolver();
|
|
|
7031
8709
|
init_agent_sessions();
|
|
7032
8710
|
import express from "express";
|
|
7033
8711
|
import { createServer } from "http";
|
|
7034
|
-
import { resolve as
|
|
7035
|
-
import { writeFile as writeFile8, unlink as
|
|
8712
|
+
import { resolve as resolve33 } from "path";
|
|
8713
|
+
import { writeFile as writeFile8, unlink as unlink8 } from "fs/promises";
|
|
7036
8714
|
import { WebSocketServer, WebSocket } from "ws";
|
|
7037
8715
|
|
|
7038
8716
|
// src/dashboard/session-liveness.ts
|
|
@@ -7131,7 +8809,19 @@ function ignoreDotSegmentsBelow(root, pathApi = defaultPathApi) {
|
|
|
7131
8809
|
};
|
|
7132
8810
|
}
|
|
7133
8811
|
function createWatcher(options) {
|
|
7134
|
-
const {
|
|
8812
|
+
const {
|
|
8813
|
+
projectsDir,
|
|
8814
|
+
assignmentsDir: assignmentsDir2,
|
|
8815
|
+
serversDir: serversDir2,
|
|
8816
|
+
playbooksDir: playbooksDir2,
|
|
8817
|
+
todosDir: todosDir2,
|
|
8818
|
+
dbPath,
|
|
8819
|
+
configPath,
|
|
8820
|
+
onMessage,
|
|
8821
|
+
onAssignmentChanged,
|
|
8822
|
+
onConfigChanged,
|
|
8823
|
+
debounceMs = 300
|
|
8824
|
+
} = options;
|
|
7135
8825
|
const pendingEvents = /* @__PURE__ */ new Map();
|
|
7136
8826
|
const projectsWatcher = watch(projectsDir, {
|
|
7137
8827
|
ignoreInitial: true,
|
|
@@ -7170,6 +8860,9 @@ function createWatcher(options) {
|
|
|
7170
8860
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7171
8861
|
};
|
|
7172
8862
|
onMessage(message);
|
|
8863
|
+
if (assignmentSlug && onAssignmentChanged) {
|
|
8864
|
+
onAssignmentChanged(projectSlug, assignmentSlug);
|
|
8865
|
+
}
|
|
7173
8866
|
}, debounceMs)
|
|
7174
8867
|
);
|
|
7175
8868
|
}
|
|
@@ -7198,6 +8891,7 @@ function createWatcher(options) {
|
|
|
7198
8891
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7199
8892
|
};
|
|
7200
8893
|
onMessage(message);
|
|
8894
|
+
if (onAssignmentChanged) onAssignmentChanged(null, assignmentId);
|
|
7201
8895
|
}, debounceMs)
|
|
7202
8896
|
);
|
|
7203
8897
|
};
|
|
@@ -7331,6 +9025,29 @@ function createWatcher(options) {
|
|
|
7331
9025
|
leasesDbWatcher.on("add", handleDbChange2);
|
|
7332
9026
|
leasesDbWatcher.on("unlink", handleDbChange2);
|
|
7333
9027
|
}
|
|
9028
|
+
let configWatcher = null;
|
|
9029
|
+
if (configPath && onConfigChanged) {
|
|
9030
|
+
let handleConfigChange2 = function() {
|
|
9031
|
+
const debounceKey = "__config__";
|
|
9032
|
+
const existing = pendingEvents.get(debounceKey);
|
|
9033
|
+
if (existing) clearTimeout(existing);
|
|
9034
|
+
pendingEvents.set(
|
|
9035
|
+
debounceKey,
|
|
9036
|
+
setTimeout(() => {
|
|
9037
|
+
pendingEvents.delete(debounceKey);
|
|
9038
|
+
onConfigChanged();
|
|
9039
|
+
}, debounceMs)
|
|
9040
|
+
);
|
|
9041
|
+
};
|
|
9042
|
+
var handleConfigChange = handleConfigChange2;
|
|
9043
|
+
configWatcher = watch(configPath, {
|
|
9044
|
+
ignoreInitial: true,
|
|
9045
|
+
persistent: true,
|
|
9046
|
+
depth: 0
|
|
9047
|
+
});
|
|
9048
|
+
configWatcher.on("change", handleConfigChange2);
|
|
9049
|
+
configWatcher.on("add", handleConfigChange2);
|
|
9050
|
+
}
|
|
7334
9051
|
return {
|
|
7335
9052
|
close: async () => {
|
|
7336
9053
|
pendingEvents.forEach((timeout) => {
|
|
@@ -7343,6 +9060,7 @@ function createWatcher(options) {
|
|
|
7343
9060
|
if (playbooksWatcher) await playbooksWatcher.close();
|
|
7344
9061
|
if (todosWatcher) await todosWatcher.close();
|
|
7345
9062
|
if (leasesDbWatcher) await leasesDbWatcher.close();
|
|
9063
|
+
if (configWatcher) await configWatcher.close();
|
|
7346
9064
|
}
|
|
7347
9065
|
};
|
|
7348
9066
|
}
|
|
@@ -7496,11 +9214,11 @@ function mergePatch(current, patch) {
|
|
|
7496
9214
|
// src/utils/view-prefs.ts
|
|
7497
9215
|
init_paths();
|
|
7498
9216
|
init_fs();
|
|
7499
|
-
import { readFile as
|
|
7500
|
-
import { resolve as
|
|
9217
|
+
import { readFile as readFile12, rename as rename3, unlink as unlink3 } from "fs/promises";
|
|
9218
|
+
import { resolve as resolve15 } from "path";
|
|
7501
9219
|
function corruptFilePath() {
|
|
7502
9220
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
7503
|
-
return
|
|
9221
|
+
return resolve15(syntaurRoot(), `view-prefs.corrupt-${ts}.json`);
|
|
7504
9222
|
}
|
|
7505
9223
|
function isViewPrefsFileShape(value) {
|
|
7506
9224
|
if (!value || typeof value !== "object") return false;
|
|
@@ -7519,7 +9237,7 @@ async function readViewPrefsFile() {
|
|
|
7519
9237
|
}
|
|
7520
9238
|
let raw2;
|
|
7521
9239
|
try {
|
|
7522
|
-
raw2 = await
|
|
9240
|
+
raw2 = await readFile12(path, "utf-8");
|
|
7523
9241
|
} catch {
|
|
7524
9242
|
return { ...DEFAULT_VIEW_PREFS_FILE };
|
|
7525
9243
|
}
|
|
@@ -7707,12 +9425,12 @@ function isDashboardSlot(value) {
|
|
|
7707
9425
|
// src/utils/saved-views.ts
|
|
7708
9426
|
init_paths();
|
|
7709
9427
|
init_fs();
|
|
7710
|
-
import { readFile as
|
|
9428
|
+
import { readFile as readFile13, rename as rename4, unlink as unlink4 } from "fs/promises";
|
|
7711
9429
|
import { randomUUID } from "crypto";
|
|
7712
|
-
import { resolve as
|
|
9430
|
+
import { resolve as resolve16 } from "path";
|
|
7713
9431
|
function corruptFilePath2() {
|
|
7714
9432
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
7715
|
-
return
|
|
9433
|
+
return resolve16(syntaurRoot(), `saved-views.corrupt-${ts}.json`);
|
|
7716
9434
|
}
|
|
7717
9435
|
function isSavedViewsFileShape(value) {
|
|
7718
9436
|
if (!value || typeof value !== "object") return false;
|
|
@@ -7737,7 +9455,7 @@ async function readSavedViewsFile() {
|
|
|
7737
9455
|
}
|
|
7738
9456
|
let raw2;
|
|
7739
9457
|
try {
|
|
7740
|
-
raw2 = await
|
|
9458
|
+
raw2 = await readFile13(path, "utf-8");
|
|
7741
9459
|
} catch {
|
|
7742
9460
|
return cloneDefault();
|
|
7743
9461
|
}
|
|
@@ -8053,10 +9771,11 @@ function createDashboardLayoutRouter() {
|
|
|
8053
9771
|
|
|
8054
9772
|
// src/dashboard/api-write.ts
|
|
8055
9773
|
init_lifecycle();
|
|
9774
|
+
init_frontmatter();
|
|
8056
9775
|
init_slug();
|
|
8057
9776
|
import { Router as Router2 } from "express";
|
|
8058
|
-
import { resolve as
|
|
8059
|
-
import { rm, readFile as
|
|
9777
|
+
import { resolve as resolve20, basename as basename3, isAbsolute as isAbsolute4 } from "path";
|
|
9778
|
+
import { rm, readFile as readFile17, open as fsOpen, stat as fsStat, realpath as fsRealpath } from "fs/promises";
|
|
8060
9779
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
8061
9780
|
|
|
8062
9781
|
// src/utils/uuid.ts
|
|
@@ -8073,7 +9792,7 @@ init_fs();
|
|
|
8073
9792
|
init_frontmatter();
|
|
8074
9793
|
init_fs();
|
|
8075
9794
|
import { spawn } from "child_process";
|
|
8076
|
-
import { readFile as
|
|
9795
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
8077
9796
|
|
|
8078
9797
|
// src/launch/cwd.ts
|
|
8079
9798
|
import { existsSync, statSync as statSync2 } from "fs";
|
|
@@ -8276,7 +9995,7 @@ async function createWorktreeAndRecord(opts) {
|
|
|
8276
9995
|
const { assignmentPath, repository, branch, worktreePath, parentBranch } = opts;
|
|
8277
9996
|
await createWorktree({ repository, branch, worktreePath, parentBranch });
|
|
8278
9997
|
try {
|
|
8279
|
-
const content = await
|
|
9998
|
+
const content = await readFile14(assignmentPath, "utf-8");
|
|
8280
9999
|
const updated = updateAssignmentWorkspace(content, {
|
|
8281
10000
|
repository,
|
|
8282
10001
|
worktreePath,
|
|
@@ -8319,12 +10038,12 @@ function formatRollbackError(opts) {
|
|
|
8319
10038
|
// src/utils/worktree-defaults.ts
|
|
8320
10039
|
init_paths();
|
|
8321
10040
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
8322
|
-
import { resolve as
|
|
10041
|
+
import { resolve as resolve17 } from "path";
|
|
8323
10042
|
function computeWorktreeDefaults(opts) {
|
|
8324
10043
|
const repository = opts.existing.repository ?? detectCurrentGitRoot(opts.cwd);
|
|
8325
10044
|
const branch = opts.projectSlug ? `syntaur/${opts.projectSlug}/${opts.assignmentSlug}` : `syntaur/${opts.assignmentSlug}`;
|
|
8326
10045
|
const parentBranch = opts.existing.parentBranch ?? detectCurrentBranch(opts.cwd) ?? "main";
|
|
8327
|
-
const worktreeBase = repository ?
|
|
10046
|
+
const worktreeBase = repository ? resolve17(repository, ".worktrees", branch) : resolve17(
|
|
8328
10047
|
syntaurRoot(),
|
|
8329
10048
|
"worktrees",
|
|
8330
10049
|
opts.projectSlug || "standalone",
|
|
@@ -8537,8 +10256,8 @@ function recreateOutcomeToHttp(outcome) {
|
|
|
8537
10256
|
// src/dashboard/repository-candidates.ts
|
|
8538
10257
|
init_fs();
|
|
8539
10258
|
init_parser();
|
|
8540
|
-
import { readdir as
|
|
8541
|
-
import { resolve as
|
|
10259
|
+
import { readdir as readdir9, readFile as readFile15 } from "fs/promises";
|
|
10260
|
+
import { resolve as resolve18 } from "path";
|
|
8542
10261
|
function toSourceAssignment(parsed, fallbackId) {
|
|
8543
10262
|
const repository = parsed.workspace.repository?.trim();
|
|
8544
10263
|
const branch = parsed.workspace.branch?.trim();
|
|
@@ -8555,29 +10274,29 @@ function toSourceAssignment(parsed, fallbackId) {
|
|
|
8555
10274
|
async function getProjectRepositoryCandidates(projectsDir, projectSlug) {
|
|
8556
10275
|
const seen = /* @__PURE__ */ new Set();
|
|
8557
10276
|
const out = [];
|
|
8558
|
-
const projectPath =
|
|
10277
|
+
const projectPath = resolve18(projectsDir, projectSlug, "project.md");
|
|
8559
10278
|
if (await fileExists(projectPath)) {
|
|
8560
|
-
const project = parseProject(await
|
|
10279
|
+
const project = parseProject(await readFile15(projectPath, "utf-8"));
|
|
8561
10280
|
for (const raw2 of project.repositories) {
|
|
8562
10281
|
const path = raw2.trim();
|
|
8563
10282
|
if (!path) continue;
|
|
8564
|
-
const abs =
|
|
10283
|
+
const abs = resolve18(path);
|
|
8565
10284
|
if (seen.has(abs)) continue;
|
|
8566
10285
|
seen.add(abs);
|
|
8567
10286
|
out.push({ path: abs, source: "project", sourceAssignmentSlug: null });
|
|
8568
10287
|
}
|
|
8569
10288
|
}
|
|
8570
|
-
const assignmentsDir2 =
|
|
10289
|
+
const assignmentsDir2 = resolve18(projectsDir, projectSlug, "assignments");
|
|
8571
10290
|
if (await fileExists(assignmentsDir2)) {
|
|
8572
|
-
const entries = await
|
|
10291
|
+
const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
|
|
8573
10292
|
for (const entry of entries) {
|
|
8574
10293
|
if (!entry.isDirectory()) continue;
|
|
8575
|
-
const assignmentMd =
|
|
10294
|
+
const assignmentMd = resolve18(assignmentsDir2, entry.name, "assignment.md");
|
|
8576
10295
|
if (!await fileExists(assignmentMd)) continue;
|
|
8577
|
-
const parsed = parseAssignmentFull(await
|
|
10296
|
+
const parsed = parseAssignmentFull(await readFile15(assignmentMd, "utf-8"));
|
|
8578
10297
|
const repo = parsed.workspace.repository?.trim();
|
|
8579
10298
|
if (!repo) continue;
|
|
8580
|
-
const abs =
|
|
10299
|
+
const abs = resolve18(repo);
|
|
8581
10300
|
if (seen.has(abs)) continue;
|
|
8582
10301
|
seen.add(abs);
|
|
8583
10302
|
out.push({ path: abs, source: "sibling", sourceAssignmentSlug: parsed.slug });
|
|
@@ -8591,16 +10310,16 @@ async function getStandaloneRepositoryCandidates(assignmentsDir2, excludeAssignm
|
|
|
8591
10310
|
}
|
|
8592
10311
|
const seen = /* @__PURE__ */ new Set();
|
|
8593
10312
|
const out = [];
|
|
8594
|
-
const entries = await
|
|
10313
|
+
const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
|
|
8595
10314
|
for (const entry of entries) {
|
|
8596
10315
|
if (!entry.isDirectory()) continue;
|
|
8597
10316
|
if (entry.name === excludeAssignmentId) continue;
|
|
8598
|
-
const assignmentMd =
|
|
10317
|
+
const assignmentMd = resolve18(assignmentsDir2, entry.name, "assignment.md");
|
|
8599
10318
|
if (!await fileExists(assignmentMd)) continue;
|
|
8600
|
-
const parsed = parseAssignmentFull(await
|
|
10319
|
+
const parsed = parseAssignmentFull(await readFile15(assignmentMd, "utf-8"));
|
|
8601
10320
|
const repo = parsed.workspace.repository?.trim();
|
|
8602
10321
|
if (!repo) continue;
|
|
8603
|
-
const abs =
|
|
10322
|
+
const abs = resolve18(repo);
|
|
8604
10323
|
if (seen.has(abs)) continue;
|
|
8605
10324
|
seen.add(abs);
|
|
8606
10325
|
out.push({ path: abs, source: "sibling", sourceAssignmentSlug: parsed.slug });
|
|
@@ -8608,17 +10327,17 @@ async function getStandaloneRepositoryCandidates(assignmentsDir2, excludeAssignm
|
|
|
8608
10327
|
return out;
|
|
8609
10328
|
}
|
|
8610
10329
|
async function getProjectSourceAssignments(projectsDir, projectSlug, excludeSlug) {
|
|
8611
|
-
const assignmentsDir2 =
|
|
10330
|
+
const assignmentsDir2 = resolve18(projectsDir, projectSlug, "assignments");
|
|
8612
10331
|
if (!await fileExists(assignmentsDir2)) return [];
|
|
8613
10332
|
const seen = /* @__PURE__ */ new Set();
|
|
8614
10333
|
const out = [];
|
|
8615
|
-
const entries = await
|
|
10334
|
+
const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
|
|
8616
10335
|
for (const entry of entries) {
|
|
8617
10336
|
if (!entry.isDirectory()) continue;
|
|
8618
10337
|
if (entry.name === excludeSlug) continue;
|
|
8619
|
-
const assignmentMd =
|
|
10338
|
+
const assignmentMd = resolve18(assignmentsDir2, entry.name, "assignment.md");
|
|
8620
10339
|
if (!await fileExists(assignmentMd)) continue;
|
|
8621
|
-
const parsed = parseAssignmentFull(await
|
|
10340
|
+
const parsed = parseAssignmentFull(await readFile15(assignmentMd, "utf-8"));
|
|
8622
10341
|
const source = toSourceAssignment(parsed, entry.name);
|
|
8623
10342
|
if (!source) continue;
|
|
8624
10343
|
if (seen.has(entry.name)) continue;
|
|
@@ -8631,13 +10350,13 @@ async function getStandaloneSourceAssignments(assignmentsDir2, excludeAssignment
|
|
|
8631
10350
|
if (!await fileExists(assignmentsDir2)) return [];
|
|
8632
10351
|
const seen = /* @__PURE__ */ new Set();
|
|
8633
10352
|
const out = [];
|
|
8634
|
-
const entries = await
|
|
10353
|
+
const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
|
|
8635
10354
|
for (const entry of entries) {
|
|
8636
10355
|
if (!entry.isDirectory()) continue;
|
|
8637
10356
|
if (entry.name === excludeAssignmentId) continue;
|
|
8638
|
-
const assignmentMd =
|
|
10357
|
+
const assignmentMd = resolve18(assignmentsDir2, entry.name, "assignment.md");
|
|
8639
10358
|
if (!await fileExists(assignmentMd)) continue;
|
|
8640
|
-
const parsed = parseAssignmentFull(await
|
|
10359
|
+
const parsed = parseAssignmentFull(await readFile15(assignmentMd, "utf-8"));
|
|
8641
10360
|
const source = toSourceAssignment(parsed, entry.name);
|
|
8642
10361
|
if (!source) continue;
|
|
8643
10362
|
if (seen.has(entry.name)) continue;
|
|
@@ -8785,6 +10504,7 @@ function renderAssignment(params2) {
|
|
|
8785
10504
|
const workspaceGroupLine = params2.workspaceGroup ? `
|
|
8786
10505
|
workspaceGroup: ${params2.workspaceGroup}` : "";
|
|
8787
10506
|
const typeYaml = `type: ${params2.type ?? "feature"}`;
|
|
10507
|
+
const seedStatus = params2.status ?? "draft";
|
|
8788
10508
|
const criteriaLines = params2.acceptanceCriteria && params2.acceptanceCriteria.length > 0 ? params2.acceptanceCriteria.map((c) => `- [ ] ${c.replace(/\n/g, " ").trim()}`).join("\n") : `- [ ] <!-- criterion 1 -->
|
|
8789
10509
|
- [ ] <!-- criterion 2 -->
|
|
8790
10510
|
- [ ] <!-- criterion 3 -->`;
|
|
@@ -8805,12 +10525,18 @@ slug: ${params2.slug}
|
|
|
8805
10525
|
title: ${safeTitle}
|
|
8806
10526
|
${projectYaml}${workspaceGroupLine}
|
|
8807
10527
|
${typeYaml}
|
|
8808
|
-
status: ${
|
|
10528
|
+
status: ${seedStatus}
|
|
8809
10529
|
priority: ${params2.priority}
|
|
8810
10530
|
created: "${params2.timestamp}"
|
|
8811
10531
|
updated: "${params2.timestamp}"
|
|
8812
10532
|
assignee: null
|
|
8813
10533
|
externalIds: []
|
|
10534
|
+
statusHistory:
|
|
10535
|
+
- at: "${params2.timestamp}"
|
|
10536
|
+
from: null
|
|
10537
|
+
to: ${seedStatus}
|
|
10538
|
+
command: create
|
|
10539
|
+
by: null
|
|
8814
10540
|
${dependsOnYaml}
|
|
8815
10541
|
${linksYaml}
|
|
8816
10542
|
blockedReason: null
|
|
@@ -9212,7 +10938,7 @@ async function readCurrentDocument(filePath) {
|
|
|
9212
10938
|
if (!await fileExists(filePath)) {
|
|
9213
10939
|
return null;
|
|
9214
10940
|
}
|
|
9215
|
-
return
|
|
10941
|
+
return readFile17(filePath, "utf-8");
|
|
9216
10942
|
}
|
|
9217
10943
|
var worktreeInFlight = /* @__PURE__ */ new Set();
|
|
9218
10944
|
async function assertRepoRoot(repoInput) {
|
|
@@ -9266,7 +10992,7 @@ async function handleWorktreeCreate(req, res, ctx) {
|
|
|
9266
10992
|
}
|
|
9267
10993
|
worktreeInFlight.add(ctx.assignmentPath);
|
|
9268
10994
|
try {
|
|
9269
|
-
const parsed = parseAssignmentFull(await
|
|
10995
|
+
const parsed = parseAssignmentFull(await readFile17(ctx.assignmentPath, "utf-8"));
|
|
9270
10996
|
if (parsed.workspace.worktreePath) {
|
|
9271
10997
|
res.status(409).json({ error: "Worktree already configured for this assignment" });
|
|
9272
10998
|
return;
|
|
@@ -9293,7 +11019,7 @@ async function handleWorktreeCreate(req, res, ctx) {
|
|
|
9293
11019
|
});
|
|
9294
11020
|
const branch = typeof bodyBranch === "string" && bodyBranch.trim() ? bodyBranch.trim() : defaults.branch;
|
|
9295
11021
|
const parentBranch = typeof bodyParent === "string" && bodyParent.trim() ? bodyParent.trim() : defaults.parentBranch;
|
|
9296
|
-
const worktreePath =
|
|
11022
|
+
const worktreePath = resolve20(repo, ".worktrees", branch);
|
|
9297
11023
|
const refCheck = spawnSync3("git", ["-C", repo, "check-ref-format", "--branch", branch], {
|
|
9298
11024
|
encoding: "utf-8"
|
|
9299
11025
|
});
|
|
@@ -9355,6 +11081,11 @@ async function handleWorktreeCreate(req, res, ctx) {
|
|
|
9355
11081
|
worktreeInFlight.delete(ctx.assignmentPath);
|
|
9356
11082
|
}
|
|
9357
11083
|
}
|
|
11084
|
+
function unambiguousCommandTarget(transitions, command) {
|
|
11085
|
+
const targets = new Set(transitions.filter((t) => t.command === command).map((t) => t.to));
|
|
11086
|
+
if (targets.size === 1) return [...targets][0];
|
|
11087
|
+
return void 0;
|
|
11088
|
+
}
|
|
9358
11089
|
function createWriteRouter(projectsDir, assignmentsDir2, todosDir2) {
|
|
9359
11090
|
const linkedTodosLookup = todosDir2 ? { todosDir: todosDir2, projectsDir } : void 0;
|
|
9360
11091
|
const router = Router2();
|
|
@@ -9583,9 +11314,9 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
|
|
|
9583
11314
|
});
|
|
9584
11315
|
return;
|
|
9585
11316
|
}
|
|
9586
|
-
const folderPath =
|
|
11317
|
+
const folderPath = resolve20(projectDir, folder);
|
|
9587
11318
|
await ensureDir(folderPath);
|
|
9588
|
-
const filePath =
|
|
11319
|
+
const filePath = resolve20(folderPath, `${requestedSlug}.md`);
|
|
9589
11320
|
const timestamp = nowTimestamp();
|
|
9590
11321
|
let content = renderItemStub(kind, {
|
|
9591
11322
|
slug: requestedSlug,
|
|
@@ -9629,14 +11360,14 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
|
|
|
9629
11360
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
9630
11361
|
return;
|
|
9631
11362
|
}
|
|
9632
|
-
const filePath =
|
|
11363
|
+
const filePath = resolve20(projectDir, folder, `${itemSlug}.md`);
|
|
9633
11364
|
if (!await fileExists(filePath)) {
|
|
9634
11365
|
res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
|
|
9635
11366
|
return;
|
|
9636
11367
|
}
|
|
9637
11368
|
const nextContentRaw = requireContent(req, res);
|
|
9638
11369
|
if (!nextContentRaw) return;
|
|
9639
|
-
const currentContent = await
|
|
11370
|
+
const currentContent = await readFile17(filePath, "utf-8");
|
|
9640
11371
|
const frontmatterBlock = extractFrontmatterBlock(currentContent);
|
|
9641
11372
|
if (!frontmatterBlock) {
|
|
9642
11373
|
res.status(500).json({ error: `${kind} file is malformed (no frontmatter)` });
|
|
@@ -9665,7 +11396,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
9665
11396
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
9666
11397
|
return;
|
|
9667
11398
|
}
|
|
9668
|
-
const filePath =
|
|
11399
|
+
const filePath = resolve20(projectDir, folder, `${itemSlug}.md`);
|
|
9669
11400
|
if (!await fileExists(filePath)) {
|
|
9670
11401
|
res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
|
|
9671
11402
|
return;
|
|
@@ -9699,26 +11430,26 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
9699
11430
|
res.status(400).json({ error: `Invalid slug "${slug}". Must be lowercase and hyphen-separated.` });
|
|
9700
11431
|
return;
|
|
9701
11432
|
}
|
|
9702
|
-
const projectDir =
|
|
11433
|
+
const projectDir = resolve20(projectsDir, slug);
|
|
9703
11434
|
if (await fileExists(projectDir)) {
|
|
9704
11435
|
res.status(409).json({ error: `Project "${slug}" already exists` });
|
|
9705
11436
|
return;
|
|
9706
11437
|
}
|
|
9707
11438
|
const title = fields.title;
|
|
9708
11439
|
const timestamp = fields.created || nowTimestamp();
|
|
9709
|
-
await ensureDir(
|
|
9710
|
-
await ensureDir(
|
|
9711
|
-
await ensureDir(
|
|
9712
|
-
await writeFileForce(
|
|
11440
|
+
await ensureDir(resolve20(projectDir, "assignments"));
|
|
11441
|
+
await ensureDir(resolve20(projectDir, "resources"));
|
|
11442
|
+
await ensureDir(resolve20(projectDir, "memories"));
|
|
11443
|
+
await writeFileForce(resolve20(projectDir, "project.md"), content);
|
|
9713
11444
|
try {
|
|
9714
11445
|
const companions = [
|
|
9715
|
-
[
|
|
9716
|
-
[
|
|
9717
|
-
[
|
|
9718
|
-
[
|
|
9719
|
-
[
|
|
9720
|
-
[
|
|
9721
|
-
[
|
|
11446
|
+
[resolve20(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
|
|
11447
|
+
[resolve20(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
|
|
11448
|
+
[resolve20(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
|
|
11449
|
+
[resolve20(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
|
|
11450
|
+
[resolve20(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
|
|
11451
|
+
[resolve20(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
|
|
11452
|
+
[resolve20(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
|
|
9722
11453
|
];
|
|
9723
11454
|
for (const [filePath, fileContent] of companions) {
|
|
9724
11455
|
await writeFileForce(filePath, fileContent);
|
|
@@ -9739,8 +11470,8 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
9739
11470
|
router.post("/api/projects/:slug/assignments", async (req, res) => {
|
|
9740
11471
|
try {
|
|
9741
11472
|
const projectSlug = getParam(req.params.slug);
|
|
9742
|
-
const projectDir =
|
|
9743
|
-
const projectMdPath =
|
|
11473
|
+
const projectDir = resolve20(projectsDir, projectSlug);
|
|
11474
|
+
const projectMdPath = resolve20(projectDir, "project.md");
|
|
9744
11475
|
if (!await fileExists(projectMdPath)) {
|
|
9745
11476
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
9746
11477
|
return;
|
|
@@ -9770,7 +11501,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
9770
11501
|
res.status(400).json({ error: `Invalid priority "${priority}". Must be low, medium, high, or critical.` });
|
|
9771
11502
|
return;
|
|
9772
11503
|
}
|
|
9773
|
-
const assignmentDir =
|
|
11504
|
+
const assignmentDir = resolve20(projectDir, "assignments", assignmentSlug);
|
|
9774
11505
|
if (await fileExists(assignmentDir)) {
|
|
9775
11506
|
res.status(409).json({
|
|
9776
11507
|
error: `Assignment "${assignmentSlug}" already exists in project "${projectSlug}"`
|
|
@@ -9779,12 +11510,19 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
9779
11510
|
}
|
|
9780
11511
|
const timestamp = fields.created || nowTimestamp();
|
|
9781
11512
|
await ensureDir(assignmentDir);
|
|
9782
|
-
|
|
11513
|
+
const seededContent = parseAssignmentFull(content).statusHistory.length > 0 ? content : appendStatusHistoryEntry(content, {
|
|
11514
|
+
at: timestamp,
|
|
11515
|
+
from: null,
|
|
11516
|
+
to: parseAssignmentFull(content).status,
|
|
11517
|
+
command: "create",
|
|
11518
|
+
by: null
|
|
11519
|
+
});
|
|
11520
|
+
await writeFileForce(resolve20(assignmentDir, "assignment.md"), seededContent);
|
|
9783
11521
|
try {
|
|
9784
11522
|
const companions = [
|
|
9785
|
-
[
|
|
9786
|
-
[
|
|
9787
|
-
[
|
|
11523
|
+
[resolve20(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
|
|
11524
|
+
[resolve20(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
|
|
11525
|
+
[resolve20(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
|
|
9788
11526
|
];
|
|
9789
11527
|
for (const [filePath, fileContent] of companions) {
|
|
9790
11528
|
await writeFileForce(filePath, fileContent);
|
|
@@ -9805,7 +11543,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
9805
11543
|
router.patch("/api/projects/:slug", async (req, res) => {
|
|
9806
11544
|
try {
|
|
9807
11545
|
const projectSlug = getParam(req.params.slug);
|
|
9808
|
-
const projectPath =
|
|
11546
|
+
const projectPath = resolve20(projectsDir, projectSlug, "project.md");
|
|
9809
11547
|
const currentContent = await readCurrentDocument(projectPath);
|
|
9810
11548
|
if (!currentContent) {
|
|
9811
11549
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
@@ -9838,7 +11576,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
9838
11576
|
try {
|
|
9839
11577
|
const projectSlug = getParam(req.params.slug);
|
|
9840
11578
|
const assignmentSlug = getParam(req.params.aslug);
|
|
9841
|
-
const assignmentPath =
|
|
11579
|
+
const assignmentPath = resolve20(
|
|
9842
11580
|
projectsDir,
|
|
9843
11581
|
projectSlug,
|
|
9844
11582
|
"assignments",
|
|
@@ -9865,10 +11603,20 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
9865
11603
|
return;
|
|
9866
11604
|
}
|
|
9867
11605
|
let nextContent = nextContentRaw;
|
|
11606
|
+
const now = nowTimestamp();
|
|
9868
11607
|
if (next.status !== current.status && current.status === "blocked" && next.status !== "blocked") {
|
|
9869
11608
|
nextContent = setTopLevelField(nextContent, "blockedReason", null);
|
|
9870
11609
|
}
|
|
9871
|
-
nextContent = setTopLevelField(nextContent, "updated",
|
|
11610
|
+
nextContent = setTopLevelField(nextContent, "updated", now);
|
|
11611
|
+
if (next.status !== current.status) {
|
|
11612
|
+
nextContent = appendStatusHistoryEntry(nextContent, {
|
|
11613
|
+
at: now,
|
|
11614
|
+
from: current.status,
|
|
11615
|
+
to: next.status,
|
|
11616
|
+
command: "edit",
|
|
11617
|
+
by: null
|
|
11618
|
+
});
|
|
11619
|
+
}
|
|
9872
11620
|
await writeFileForce(assignmentPath, nextContent);
|
|
9873
11621
|
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
9874
11622
|
res.json({ assignment, content: nextContent });
|
|
@@ -9881,7 +11629,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
9881
11629
|
try {
|
|
9882
11630
|
const projectSlug = getParam(req.params.slug);
|
|
9883
11631
|
const assignmentSlug = getParam(req.params.aslug);
|
|
9884
|
-
const assignmentPath =
|
|
11632
|
+
const assignmentPath = resolve20(
|
|
9885
11633
|
projectsDir,
|
|
9886
11634
|
projectSlug,
|
|
9887
11635
|
"assignments",
|
|
@@ -9917,7 +11665,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
9917
11665
|
try {
|
|
9918
11666
|
const projectSlug = getParam(req.params.slug);
|
|
9919
11667
|
const assignmentSlug = getParam(req.params.aslug);
|
|
9920
|
-
const planPath =
|
|
11668
|
+
const planPath = resolve20(
|
|
9921
11669
|
projectsDir,
|
|
9922
11670
|
projectSlug,
|
|
9923
11671
|
"assignments",
|
|
@@ -9955,7 +11703,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
9955
11703
|
try {
|
|
9956
11704
|
const projectSlug = getParam(req.params.slug);
|
|
9957
11705
|
const assignmentSlug = getParam(req.params.aslug);
|
|
9958
|
-
const scratchpadPath =
|
|
11706
|
+
const scratchpadPath = resolve20(
|
|
9959
11707
|
projectsDir,
|
|
9960
11708
|
projectSlug,
|
|
9961
11709
|
"assignments",
|
|
@@ -9993,7 +11741,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
9993
11741
|
try {
|
|
9994
11742
|
const projectSlug = getParam(req.params.slug);
|
|
9995
11743
|
const assignmentSlug = getParam(req.params.aslug);
|
|
9996
|
-
const handoffPath =
|
|
11744
|
+
const handoffPath = resolve20(
|
|
9997
11745
|
projectsDir,
|
|
9998
11746
|
projectSlug,
|
|
9999
11747
|
"assignments",
|
|
@@ -10031,7 +11779,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
10031
11779
|
try {
|
|
10032
11780
|
const projectSlug = getParam(req.params.slug);
|
|
10033
11781
|
const assignmentSlug = getParam(req.params.aslug);
|
|
10034
|
-
const decisionPath =
|
|
11782
|
+
const decisionPath = resolve20(
|
|
10035
11783
|
projectsDir,
|
|
10036
11784
|
projectSlug,
|
|
10037
11785
|
"assignments",
|
|
@@ -10069,7 +11817,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
10069
11817
|
try {
|
|
10070
11818
|
const projectSlug = getParam(req.params.slug);
|
|
10071
11819
|
const assignmentSlug = getParam(req.params.aslug);
|
|
10072
|
-
const commentsPath =
|
|
11820
|
+
const commentsPath = resolve20(
|
|
10073
11821
|
projectsDir,
|
|
10074
11822
|
projectSlug,
|
|
10075
11823
|
"assignments",
|
|
@@ -10087,7 +11835,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
10087
11835
|
let currentContent;
|
|
10088
11836
|
let currentCount = 0;
|
|
10089
11837
|
if (await fileExists(commentsPath)) {
|
|
10090
|
-
currentContent = await
|
|
11838
|
+
currentContent = await readFile17(commentsPath, "utf-8");
|
|
10091
11839
|
const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
|
|
10092
11840
|
if (countMatch) currentCount = parseInt(countMatch[1], 10);
|
|
10093
11841
|
} else {
|
|
@@ -10128,7 +11876,7 @@ ${entry}`;
|
|
|
10128
11876
|
const projectSlug = getParam(req.params.slug);
|
|
10129
11877
|
const assignmentSlug = getParam(req.params.aslug);
|
|
10130
11878
|
const commentId = getParam(req.params.commentId);
|
|
10131
|
-
const commentsPath =
|
|
11879
|
+
const commentsPath = resolve20(
|
|
10132
11880
|
projectsDir,
|
|
10133
11881
|
projectSlug,
|
|
10134
11882
|
"assignments",
|
|
@@ -10144,7 +11892,7 @@ ${entry}`;
|
|
|
10144
11892
|
res.status(400).json({ error: "resolved (boolean) is required" });
|
|
10145
11893
|
return;
|
|
10146
11894
|
}
|
|
10147
|
-
const content = await
|
|
11895
|
+
const content = await readFile17(commentsPath, "utf-8");
|
|
10148
11896
|
const parsed = parseComments(content);
|
|
10149
11897
|
const target = parsed.entries.find((e) => e.id === commentId);
|
|
10150
11898
|
if (!target) {
|
|
@@ -10179,7 +11927,7 @@ ${entry}`;
|
|
|
10179
11927
|
router.post("/api/projects/:slug/move-workspace", async (req, res) => {
|
|
10180
11928
|
try {
|
|
10181
11929
|
const projectSlug = getParam(req.params.slug);
|
|
10182
|
-
const projectPath =
|
|
11930
|
+
const projectPath = resolve20(projectsDir, projectSlug, "project.md");
|
|
10183
11931
|
if (!await fileExists(projectPath)) {
|
|
10184
11932
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
10185
11933
|
return;
|
|
@@ -10191,7 +11939,7 @@ ${entry}`;
|
|
|
10191
11939
|
});
|
|
10192
11940
|
return;
|
|
10193
11941
|
}
|
|
10194
|
-
let content = await
|
|
11942
|
+
let content = await readFile17(projectPath, "utf-8");
|
|
10195
11943
|
content = setTopLevelField(content, "workspace", workspace ?? null);
|
|
10196
11944
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
10197
11945
|
await writeFileForce(projectPath, content);
|
|
@@ -10227,8 +11975,8 @@ ${entry}`;
|
|
|
10227
11975
|
});
|
|
10228
11976
|
return;
|
|
10229
11977
|
}
|
|
10230
|
-
const assignmentPath =
|
|
10231
|
-
let content = await
|
|
11978
|
+
const assignmentPath = resolve20(resolved.assignmentDir, "assignment.md");
|
|
11979
|
+
let content = await readFile17(assignmentPath, "utf-8");
|
|
10232
11980
|
content = setTopLevelField(content, "workspaceGroup", workspaceGroup ?? null);
|
|
10233
11981
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
10234
11982
|
await writeFileForce(assignmentPath, content);
|
|
@@ -10244,7 +11992,7 @@ ${entry}`;
|
|
|
10244
11992
|
async (req, res) => {
|
|
10245
11993
|
try {
|
|
10246
11994
|
const projectSlug = getParam(req.params.slug);
|
|
10247
|
-
const projectPath =
|
|
11995
|
+
const projectPath = resolve20(projectsDir, projectSlug, "project.md");
|
|
10248
11996
|
if (!await fileExists(projectPath)) {
|
|
10249
11997
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
10250
11998
|
return;
|
|
@@ -10303,7 +12051,7 @@ ${entry}`;
|
|
|
10303
12051
|
try {
|
|
10304
12052
|
const projectSlug = getParam(req.params.slug);
|
|
10305
12053
|
const assignmentSlug = getParam(req.params.aslug);
|
|
10306
|
-
const assignmentPath =
|
|
12054
|
+
const assignmentPath = resolve20(
|
|
10307
12055
|
projectsDir,
|
|
10308
12056
|
projectSlug,
|
|
10309
12057
|
"assignments",
|
|
@@ -10352,7 +12100,7 @@ ${entry}`;
|
|
|
10352
12100
|
try {
|
|
10353
12101
|
const projectSlug = getParam(req.params.slug);
|
|
10354
12102
|
const assignmentSlug = getParam(req.params.aslug);
|
|
10355
|
-
const assignmentPath =
|
|
12103
|
+
const assignmentPath = resolve20(
|
|
10356
12104
|
projectsDir,
|
|
10357
12105
|
projectSlug,
|
|
10358
12106
|
"assignments",
|
|
@@ -10411,7 +12159,7 @@ ${entry}`;
|
|
|
10411
12159
|
try {
|
|
10412
12160
|
const projectSlug = getParam(req.params.slug);
|
|
10413
12161
|
const assignmentSlug = getParam(req.params.aslug);
|
|
10414
|
-
const assignmentPath =
|
|
12162
|
+
const assignmentPath = resolve20(
|
|
10415
12163
|
projectsDir,
|
|
10416
12164
|
projectSlug,
|
|
10417
12165
|
"assignments",
|
|
@@ -10444,8 +12192,8 @@ ${entry}`;
|
|
|
10444
12192
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
10445
12193
|
return;
|
|
10446
12194
|
}
|
|
10447
|
-
const assignmentPath =
|
|
10448
|
-
const parsedForSlug = parseAssignmentFull(await
|
|
12195
|
+
const assignmentPath = resolve20(resolved.assignmentDir, "assignment.md");
|
|
12196
|
+
const parsedForSlug = parseAssignmentFull(await readFile17(assignmentPath, "utf-8"));
|
|
10449
12197
|
const assignmentSlugForBranch = parsedForSlug.slug || resolved.id;
|
|
10450
12198
|
await handleWorktreeCreate(req, res, {
|
|
10451
12199
|
assignmentPath,
|
|
@@ -10501,7 +12249,7 @@ ${entry}`;
|
|
|
10501
12249
|
router.post("/api/projects/:slug/status-override", async (req, res) => {
|
|
10502
12250
|
try {
|
|
10503
12251
|
const projectSlug = getParam(req.params.slug);
|
|
10504
|
-
const projectPath =
|
|
12252
|
+
const projectPath = resolve20(projectsDir, projectSlug, "project.md");
|
|
10505
12253
|
if (!await fileExists(projectPath)) {
|
|
10506
12254
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
10507
12255
|
return;
|
|
@@ -10513,7 +12261,7 @@ ${entry}`;
|
|
|
10513
12261
|
res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}, or null to clear.` });
|
|
10514
12262
|
return;
|
|
10515
12263
|
}
|
|
10516
|
-
let content = await
|
|
12264
|
+
let content = await readFile17(projectPath, "utf-8");
|
|
10517
12265
|
content = setTopLevelField(content, "statusOverride", status ?? null);
|
|
10518
12266
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
10519
12267
|
await writeFileForce(projectPath, content);
|
|
@@ -10528,7 +12276,7 @@ ${entry}`;
|
|
|
10528
12276
|
try {
|
|
10529
12277
|
const projectSlug = getParam(req.params.slug);
|
|
10530
12278
|
const assignmentSlug = getParam(req.params.aslug);
|
|
10531
|
-
const assignmentPath =
|
|
12279
|
+
const assignmentPath = resolve20(
|
|
10532
12280
|
projectsDir,
|
|
10533
12281
|
projectSlug,
|
|
10534
12282
|
"assignments",
|
|
@@ -10542,17 +12290,40 @@ ${entry}`;
|
|
|
10542
12290
|
const { status } = req.body || {};
|
|
10543
12291
|
const config = await getStatusConfig();
|
|
10544
12292
|
const validStatuses = config.statuses.map((s) => s.id);
|
|
10545
|
-
|
|
12293
|
+
const clearing = status === null;
|
|
12294
|
+
if (!clearing && (typeof status !== "string" || !validStatuses.includes(status))) {
|
|
10546
12295
|
res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
|
|
10547
12296
|
return;
|
|
10548
12297
|
}
|
|
10549
|
-
|
|
10550
|
-
|
|
10551
|
-
|
|
10552
|
-
|
|
10553
|
-
|
|
12298
|
+
if (!clearing && config.terminalStatuses.has(status)) {
|
|
12299
|
+
res.status(400).json({
|
|
12300
|
+
error: `"${status}" is terminal \u2014 use the complete/fail transition (gated), not an override.`
|
|
12301
|
+
});
|
|
12302
|
+
return;
|
|
12303
|
+
}
|
|
12304
|
+
const { recomputeAndWrite: recomputeAndWrite2, resolveDeriveContext: resolveDeriveContext2 } = await Promise.resolve().then(() => (init_recompute(), recompute_exports));
|
|
12305
|
+
const { updateOverride: updateOverride2 } = await Promise.resolve().then(() => (init_frontmatter(), frontmatter_exports));
|
|
12306
|
+
const context = await resolveDeriveContext2();
|
|
12307
|
+
const result = await recomputeAndWrite2(assignmentPath, {
|
|
12308
|
+
cause: clearing ? "unpin" : "pin",
|
|
12309
|
+
by: "human",
|
|
12310
|
+
projectDir: resolve20(projectsDir, projectSlug),
|
|
12311
|
+
context,
|
|
12312
|
+
mutate: (content) => {
|
|
12313
|
+
if (clearing) return updateOverride2(content, null);
|
|
12314
|
+
const current = parseAssignmentFull(content);
|
|
12315
|
+
if (current.override?.status === status) return content;
|
|
12316
|
+
return updateOverride2(content, { status, source: "human", reason: null, at: nowTimestamp() });
|
|
12317
|
+
}
|
|
12318
|
+
});
|
|
12319
|
+
if (result.deferredTerminal) {
|
|
12320
|
+
res.status(409).json({ error: "Assignment is terminal \u2014 reopen it first." });
|
|
12321
|
+
return;
|
|
12322
|
+
}
|
|
12323
|
+
if (result.warning) {
|
|
12324
|
+
res.status(503).json({ error: result.warning });
|
|
12325
|
+
return;
|
|
10554
12326
|
}
|
|
10555
|
-
await writeFileForce(assignmentPath, content);
|
|
10556
12327
|
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
10557
12328
|
res.json({ assignment });
|
|
10558
12329
|
} catch (error) {
|
|
@@ -10567,12 +12338,12 @@ ${entry}`;
|
|
|
10567
12338
|
router.post("/api/projects/:slug/archive", async (req, res) => {
|
|
10568
12339
|
try {
|
|
10569
12340
|
const projectSlug = getParam(req.params.slug);
|
|
10570
|
-
const projectPath =
|
|
12341
|
+
const projectPath = resolve20(projectsDir, projectSlug, "project.md");
|
|
10571
12342
|
if (!await fileExists(projectPath)) {
|
|
10572
12343
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
10573
12344
|
return;
|
|
10574
12345
|
}
|
|
10575
|
-
const content = await
|
|
12346
|
+
const content = await readFile17(projectPath, "utf-8");
|
|
10576
12347
|
await writeFileForce(projectPath, applyArchiveFields(content, true, archiveReason(req.body)));
|
|
10577
12348
|
const project = await getProjectDetail(projectsDir, projectSlug);
|
|
10578
12349
|
res.json({ project });
|
|
@@ -10584,12 +12355,12 @@ ${entry}`;
|
|
|
10584
12355
|
router.post("/api/projects/:slug/unarchive", async (req, res) => {
|
|
10585
12356
|
try {
|
|
10586
12357
|
const projectSlug = getParam(req.params.slug);
|
|
10587
|
-
const projectPath =
|
|
12358
|
+
const projectPath = resolve20(projectsDir, projectSlug, "project.md");
|
|
10588
12359
|
if (!await fileExists(projectPath)) {
|
|
10589
12360
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
10590
12361
|
return;
|
|
10591
12362
|
}
|
|
10592
|
-
const content = await
|
|
12363
|
+
const content = await readFile17(projectPath, "utf-8");
|
|
10593
12364
|
await writeFileForce(projectPath, applyArchiveFields(content, false, null));
|
|
10594
12365
|
const project = await getProjectDetail(projectsDir, projectSlug);
|
|
10595
12366
|
res.json({ project });
|
|
@@ -10601,12 +12372,12 @@ ${entry}`;
|
|
|
10601
12372
|
async function handleAssignmentArchive(req, res, archived) {
|
|
10602
12373
|
const projectSlug = getParam(req.params.slug);
|
|
10603
12374
|
const assignmentSlug = getParam(req.params.aslug);
|
|
10604
|
-
const assignmentPath =
|
|
12375
|
+
const assignmentPath = resolve20(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md");
|
|
10605
12376
|
if (!await fileExists(assignmentPath)) {
|
|
10606
12377
|
res.status(404).json({ error: "Assignment not found" });
|
|
10607
12378
|
return;
|
|
10608
12379
|
}
|
|
10609
|
-
const content = await
|
|
12380
|
+
const content = await readFile17(assignmentPath, "utf-8");
|
|
10610
12381
|
await writeFileForce(assignmentPath, applyArchiveFields(content, archived, archived ? archiveReason(req.body) : null));
|
|
10611
12382
|
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
10612
12383
|
res.json({ assignment });
|
|
@@ -10638,8 +12409,8 @@ ${entry}`;
|
|
|
10638
12409
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
10639
12410
|
return;
|
|
10640
12411
|
}
|
|
10641
|
-
const assignmentPath =
|
|
10642
|
-
const content = await
|
|
12412
|
+
const assignmentPath = resolve20(resolved.assignmentDir, "assignment.md");
|
|
12413
|
+
const content = await readFile17(assignmentPath, "utf-8");
|
|
10643
12414
|
await writeFileForce(assignmentPath, applyArchiveFields(content, archived, archived ? archiveReason(req.body) : null));
|
|
10644
12415
|
const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
|
|
10645
12416
|
res.json({ assignment });
|
|
@@ -10664,7 +12435,7 @@ ${entry}`;
|
|
|
10664
12435
|
try {
|
|
10665
12436
|
const projectSlug = getParam(req.params.slug);
|
|
10666
12437
|
const assignmentSlug = getParam(req.params.aslug);
|
|
10667
|
-
const assignmentPath =
|
|
12438
|
+
const assignmentPath = resolve20(
|
|
10668
12439
|
projectsDir,
|
|
10669
12440
|
projectSlug,
|
|
10670
12441
|
"assignments",
|
|
@@ -10680,7 +12451,7 @@ ${entry}`;
|
|
|
10680
12451
|
res.status(400).json({ error: validation.error });
|
|
10681
12452
|
return;
|
|
10682
12453
|
}
|
|
10683
|
-
let content = await
|
|
12454
|
+
let content = await readFile17(assignmentPath, "utf-8");
|
|
10684
12455
|
content = setTopLevelField(content, "assignee", validation.value);
|
|
10685
12456
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
10686
12457
|
await writeFileForce(assignmentPath, content);
|
|
@@ -10695,7 +12466,7 @@ ${entry}`;
|
|
|
10695
12466
|
try {
|
|
10696
12467
|
const projectSlug = getParam(req.params.slug);
|
|
10697
12468
|
const assignmentSlug = getParam(req.params.aslug);
|
|
10698
|
-
const assignmentPath =
|
|
12469
|
+
const assignmentPath = resolve20(
|
|
10699
12470
|
projectsDir,
|
|
10700
12471
|
projectSlug,
|
|
10701
12472
|
"assignments",
|
|
@@ -10711,7 +12482,7 @@ ${entry}`;
|
|
|
10711
12482
|
res.status(400).json({ error: validation.error });
|
|
10712
12483
|
return;
|
|
10713
12484
|
}
|
|
10714
|
-
let content = await
|
|
12485
|
+
let content = await readFile17(assignmentPath, "utf-8");
|
|
10715
12486
|
content = setTopLevelField(content, "title", validation.value);
|
|
10716
12487
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
10717
12488
|
await writeFileForce(assignmentPath, content);
|
|
@@ -10733,16 +12504,49 @@ ${entry}`;
|
|
|
10733
12504
|
res.status(400).json({ error: `Unsupported transition command "${req.params.command}"` });
|
|
10734
12505
|
return;
|
|
10735
12506
|
}
|
|
10736
|
-
const projectDir =
|
|
10737
|
-
const assignmentPath =
|
|
12507
|
+
const projectDir = resolve20(projectsDir, projectSlug);
|
|
12508
|
+
const assignmentPath = resolve20(projectDir, "assignments", assignmentSlug, "assignment.md");
|
|
10738
12509
|
if (!await fileExists(assignmentPath)) {
|
|
10739
12510
|
res.status(404).json({ error: "Assignment not found" });
|
|
10740
12511
|
return;
|
|
10741
12512
|
}
|
|
10742
12513
|
const { reason } = req.body || {};
|
|
12514
|
+
const { recomputeAndWrite: recomputeAndWrite2, recomputeDependents: recomputeDependents2, resolveDeriveContext: resolveDeriveContext2 } = await Promise.resolve().then(() => (init_recompute(), recompute_exports));
|
|
12515
|
+
const context = await resolveDeriveContext2();
|
|
12516
|
+
if (command === "block" || command === "unblock") {
|
|
12517
|
+
const { updateAssignmentFile: updateAssignmentFile2 } = await Promise.resolve().then(() => (init_frontmatter(), frontmatter_exports));
|
|
12518
|
+
const result2 = await recomputeAndWrite2(assignmentPath, {
|
|
12519
|
+
cause: command,
|
|
12520
|
+
by: "human",
|
|
12521
|
+
projectDir,
|
|
12522
|
+
context,
|
|
12523
|
+
reason: typeof reason === "string" ? reason : void 0,
|
|
12524
|
+
mutate: (content) => updateAssignmentFile2(content, {
|
|
12525
|
+
blockedReason: command === "block" ? typeof reason === "string" && reason ? reason : "(unspecified)" : null
|
|
12526
|
+
})
|
|
12527
|
+
});
|
|
12528
|
+
if (result2.deferredTerminal) {
|
|
12529
|
+
res.status(409).json({ error: "Assignment is terminal \u2014 reopen it first." });
|
|
12530
|
+
return;
|
|
12531
|
+
}
|
|
12532
|
+
if (result2.warning) {
|
|
12533
|
+
res.status(503).json({ error: result2.warning });
|
|
12534
|
+
return;
|
|
12535
|
+
}
|
|
12536
|
+
const assignment2 = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
12537
|
+
res.json({ assignment: assignment2, transition: { success: true, message: command, fromStatus: "", toStatus: result2.status } });
|
|
12538
|
+
return;
|
|
12539
|
+
}
|
|
12540
|
+
const GATED_TERMINAL = /* @__PURE__ */ new Set(["complete", "fail", "reopen"]);
|
|
12541
|
+
const gatedFallback = GATED_TERMINAL.has(command) ? unambiguousCommandTarget(config.transitions, command) : void 0;
|
|
10743
12542
|
const result = await executeTransition(projectDir, assignmentSlug, command, {
|
|
10744
12543
|
reason: typeof reason === "string" ? reason : void 0,
|
|
12544
|
+
// Gated terminal commands: the from-specific custom mapping wins
|
|
12545
|
+
// (passed via the table), with the unambiguous command target as the
|
|
12546
|
+
// guard-free fallback for legacy/undefined statuses (codex r3
|
|
12547
|
+
// finding 1 — ambiguous configs must not pick an arbitrary target).
|
|
10745
12548
|
transitionTable: config.custom ? config.transitionTable : void 0,
|
|
12549
|
+
commandTargets: config.custom && gatedFallback ? /* @__PURE__ */ new Map([[command, gatedFallback]]) : void 0,
|
|
10746
12550
|
terminalStatuses: config.custom ? config.terminalStatuses : void 0,
|
|
10747
12551
|
linkedTodosLookup
|
|
10748
12552
|
});
|
|
@@ -10750,6 +12554,25 @@ ${entry}`;
|
|
|
10750
12554
|
res.status(400).json({ error: result.message });
|
|
10751
12555
|
return;
|
|
10752
12556
|
}
|
|
12557
|
+
const settled = await recomputeAndWrite2(assignmentPath, {
|
|
12558
|
+
cause: command,
|
|
12559
|
+
by: "human",
|
|
12560
|
+
projectDir,
|
|
12561
|
+
context
|
|
12562
|
+
});
|
|
12563
|
+
if (settled.warning) {
|
|
12564
|
+
res.status(503).json({ error: settled.warning });
|
|
12565
|
+
return;
|
|
12566
|
+
}
|
|
12567
|
+
const wasTerminal = config.terminalStatuses.has(result.fromStatus);
|
|
12568
|
+
const isTerminal = result.toStatus ? config.terminalStatuses.has(result.toStatus) : false;
|
|
12569
|
+
if (wasTerminal !== isTerminal) {
|
|
12570
|
+
await recomputeDependents2(projectDir, assignmentSlug, {
|
|
12571
|
+
cause: "dep-terminal",
|
|
12572
|
+
by: "system",
|
|
12573
|
+
context
|
|
12574
|
+
});
|
|
12575
|
+
}
|
|
10753
12576
|
const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
|
|
10754
12577
|
res.json({ assignment, transition: result });
|
|
10755
12578
|
} catch (error) {
|
|
@@ -10761,8 +12584,8 @@ ${entry}`;
|
|
|
10761
12584
|
try {
|
|
10762
12585
|
const projectSlug = getParam(req.params.slug);
|
|
10763
12586
|
const assignmentSlug = getParam(req.params.aslug);
|
|
10764
|
-
const assignmentDir =
|
|
10765
|
-
const assignmentPath =
|
|
12587
|
+
const assignmentDir = resolve20(projectsDir, projectSlug, "assignments", assignmentSlug);
|
|
12588
|
+
const assignmentPath = resolve20(assignmentDir, "assignment.md");
|
|
10766
12589
|
if (!await fileExists(assignmentPath)) {
|
|
10767
12590
|
res.status(404).json({ error: `Assignment "${assignmentSlug}" not found in project "${projectSlug}"` });
|
|
10768
12591
|
return;
|
|
@@ -10817,33 +12640,42 @@ ${entry}`;
|
|
|
10817
12640
|
return;
|
|
10818
12641
|
}
|
|
10819
12642
|
const id2 = generateId();
|
|
10820
|
-
const assignmentDir2 =
|
|
12643
|
+
const assignmentDir2 = resolve20(assignmentsDir2, id2);
|
|
10821
12644
|
if (await fileExists(assignmentDir2)) {
|
|
10822
12645
|
res.status(500).json({ error: "UUID collision \u2014 try again" });
|
|
10823
12646
|
return;
|
|
10824
12647
|
}
|
|
10825
12648
|
const timestamp2 = fields.created || nowTimestamp();
|
|
10826
12649
|
await ensureDir(assignmentDir2);
|
|
10827
|
-
|
|
10828
|
-
|
|
12650
|
+
let normalizedContent = setTopLevelField(rawContent, "id", id2);
|
|
12651
|
+
if (parseAssignmentFull(normalizedContent).statusHistory.length === 0) {
|
|
12652
|
+
normalizedContent = appendStatusHistoryEntry(normalizedContent, {
|
|
12653
|
+
at: timestamp2,
|
|
12654
|
+
from: null,
|
|
12655
|
+
to: parseAssignmentFull(normalizedContent).status,
|
|
12656
|
+
command: "create",
|
|
12657
|
+
by: null
|
|
12658
|
+
});
|
|
12659
|
+
}
|
|
12660
|
+
await writeFileForce(resolve20(assignmentDir2, "assignment.md"), normalizedContent);
|
|
10829
12661
|
await writeFileForce(
|
|
10830
|
-
|
|
12662
|
+
resolve20(assignmentDir2, "scratchpad.md"),
|
|
10831
12663
|
renderScratchpad({ assignmentSlug: id2, timestamp: timestamp2 })
|
|
10832
12664
|
);
|
|
10833
12665
|
await writeFileForce(
|
|
10834
|
-
|
|
12666
|
+
resolve20(assignmentDir2, "handoff.md"),
|
|
10835
12667
|
renderHandoff({ assignmentSlug: id2, timestamp: timestamp2 })
|
|
10836
12668
|
);
|
|
10837
12669
|
await writeFileForce(
|
|
10838
|
-
|
|
12670
|
+
resolve20(assignmentDir2, "decision-record.md"),
|
|
10839
12671
|
renderDecisionRecord({ assignmentSlug: id2, timestamp: timestamp2 })
|
|
10840
12672
|
);
|
|
10841
12673
|
await writeFileForce(
|
|
10842
|
-
|
|
12674
|
+
resolve20(assignmentDir2, "progress.md"),
|
|
10843
12675
|
renderProgress({ assignment: id2, timestamp: timestamp2 })
|
|
10844
12676
|
);
|
|
10845
12677
|
await writeFileForce(
|
|
10846
|
-
|
|
12678
|
+
resolve20(assignmentDir2, "comments.md"),
|
|
10847
12679
|
renderComments({ assignment: id2, timestamp: timestamp2 })
|
|
10848
12680
|
);
|
|
10849
12681
|
const detail2 = await getAssignmentDetailById(projectsDir, assignmentsDir2, id2);
|
|
@@ -10861,7 +12693,7 @@ ${entry}`;
|
|
|
10861
12693
|
return;
|
|
10862
12694
|
}
|
|
10863
12695
|
const id = generateId();
|
|
10864
|
-
const assignmentDir =
|
|
12696
|
+
const assignmentDir = resolve20(assignmentsDir2, id);
|
|
10865
12697
|
if (await fileExists(assignmentDir)) {
|
|
10866
12698
|
res.status(500).json({ error: "UUID collision \u2014 try again" });
|
|
10867
12699
|
return;
|
|
@@ -10881,25 +12713,25 @@ ${entry}`;
|
|
|
10881
12713
|
project: null,
|
|
10882
12714
|
type: typeof type === "string" ? type : void 0
|
|
10883
12715
|
});
|
|
10884
|
-
await writeFileForce(
|
|
12716
|
+
await writeFileForce(resolve20(assignmentDir, "assignment.md"), assignmentContent);
|
|
10885
12717
|
await writeFileForce(
|
|
10886
|
-
|
|
12718
|
+
resolve20(assignmentDir, "scratchpad.md"),
|
|
10887
12719
|
renderScratchpad({ assignmentSlug: id, timestamp })
|
|
10888
12720
|
);
|
|
10889
12721
|
await writeFileForce(
|
|
10890
|
-
|
|
12722
|
+
resolve20(assignmentDir, "handoff.md"),
|
|
10891
12723
|
renderHandoff({ assignmentSlug: id, timestamp })
|
|
10892
12724
|
);
|
|
10893
12725
|
await writeFileForce(
|
|
10894
|
-
|
|
12726
|
+
resolve20(assignmentDir, "decision-record.md"),
|
|
10895
12727
|
renderDecisionRecord({ assignmentSlug: id, timestamp })
|
|
10896
12728
|
);
|
|
10897
12729
|
await writeFileForce(
|
|
10898
|
-
|
|
12730
|
+
resolve20(assignmentDir, "progress.md"),
|
|
10899
12731
|
renderProgress({ assignment: id, timestamp })
|
|
10900
12732
|
);
|
|
10901
12733
|
await writeFileForce(
|
|
10902
|
-
|
|
12734
|
+
resolve20(assignmentDir, "comments.md"),
|
|
10903
12735
|
renderComments({ assignment: id, timestamp })
|
|
10904
12736
|
);
|
|
10905
12737
|
const detail = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
|
|
@@ -11027,7 +12859,7 @@ ${entry}`;
|
|
|
11027
12859
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
11028
12860
|
return;
|
|
11029
12861
|
}
|
|
11030
|
-
const assignmentPath =
|
|
12862
|
+
const assignmentPath = resolve20(resolved.assignmentDir, "assignment.md");
|
|
11031
12863
|
const currentContent = await readCurrentDocument(assignmentPath);
|
|
11032
12864
|
if (!currentContent) {
|
|
11033
12865
|
res.status(404).json({ error: "Assignment not found" });
|
|
@@ -11045,10 +12877,20 @@ ${entry}`;
|
|
|
11045
12877
|
if (current.id) nextContent = setTopLevelField(nextContent, "id", current.id);
|
|
11046
12878
|
nextContent = setTopLevelField(nextContent, "project", null);
|
|
11047
12879
|
if (current.slug) nextContent = setTopLevelField(nextContent, "slug", current.slug);
|
|
12880
|
+
const now = nowTimestamp();
|
|
11048
12881
|
if (next.status !== current.status && current.status === "blocked" && next.status !== "blocked") {
|
|
11049
12882
|
nextContent = setTopLevelField(nextContent, "blockedReason", null);
|
|
11050
12883
|
}
|
|
11051
|
-
nextContent = setTopLevelField(nextContent, "updated",
|
|
12884
|
+
nextContent = setTopLevelField(nextContent, "updated", now);
|
|
12885
|
+
if (next.status !== current.status) {
|
|
12886
|
+
nextContent = appendStatusHistoryEntry(nextContent, {
|
|
12887
|
+
at: now,
|
|
12888
|
+
from: current.status,
|
|
12889
|
+
to: next.status,
|
|
12890
|
+
command: "edit",
|
|
12891
|
+
by: null
|
|
12892
|
+
});
|
|
12893
|
+
}
|
|
11052
12894
|
await writeFileForce(assignmentPath, nextContent);
|
|
11053
12895
|
const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
|
|
11054
12896
|
res.json({ assignment, content: nextContent });
|
|
@@ -11069,7 +12911,7 @@ ${entry}`;
|
|
|
11069
12911
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
11070
12912
|
return;
|
|
11071
12913
|
}
|
|
11072
|
-
const planPath =
|
|
12914
|
+
const planPath = resolve20(resolved.assignmentDir, "plan.md");
|
|
11073
12915
|
const currentContent = await readCurrentDocument(planPath);
|
|
11074
12916
|
if (!currentContent) {
|
|
11075
12917
|
res.status(404).json({ error: "Plan not found" });
|
|
@@ -11103,7 +12945,7 @@ ${entry}`;
|
|
|
11103
12945
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
11104
12946
|
return;
|
|
11105
12947
|
}
|
|
11106
|
-
const scratchpadPath =
|
|
12948
|
+
const scratchpadPath = resolve20(resolved.assignmentDir, "scratchpad.md");
|
|
11107
12949
|
const currentContent = await readCurrentDocument(scratchpadPath);
|
|
11108
12950
|
if (!currentContent) {
|
|
11109
12951
|
res.status(404).json({ error: "Scratchpad not found" });
|
|
@@ -11137,7 +12979,7 @@ ${entry}`;
|
|
|
11137
12979
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
11138
12980
|
return;
|
|
11139
12981
|
}
|
|
11140
|
-
const handoffPath =
|
|
12982
|
+
const handoffPath = resolve20(resolved.assignmentDir, "handoff.md");
|
|
11141
12983
|
const currentContent = await readCurrentDocument(handoffPath);
|
|
11142
12984
|
if (!currentContent) {
|
|
11143
12985
|
res.status(404).json({ error: "Handoff log not found" });
|
|
@@ -11177,7 +13019,7 @@ ${entry}`;
|
|
|
11177
13019
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
11178
13020
|
return;
|
|
11179
13021
|
}
|
|
11180
|
-
const decisionPath =
|
|
13022
|
+
const decisionPath = resolve20(resolved.assignmentDir, "decision-record.md");
|
|
11181
13023
|
const currentContent = await readCurrentDocument(decisionPath);
|
|
11182
13024
|
if (!currentContent) {
|
|
11183
13025
|
res.status(404).json({ error: "Decision record not found" });
|
|
@@ -11217,7 +13059,7 @@ ${entry}`;
|
|
|
11217
13059
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
11218
13060
|
return;
|
|
11219
13061
|
}
|
|
11220
|
-
const assignmentPath =
|
|
13062
|
+
const assignmentPath = resolve20(resolved.assignmentDir, "assignment.md");
|
|
11221
13063
|
if (!await fileExists(assignmentPath)) {
|
|
11222
13064
|
res.status(404).json({ error: "Assignment not found" });
|
|
11223
13065
|
return;
|
|
@@ -11225,17 +13067,41 @@ ${entry}`;
|
|
|
11225
13067
|
const { status } = req.body || {};
|
|
11226
13068
|
const config = await getStatusConfig();
|
|
11227
13069
|
const validStatuses = config.statuses.map((s) => s.id);
|
|
11228
|
-
|
|
13070
|
+
const clearing = status === null;
|
|
13071
|
+
if (!clearing && (typeof status !== "string" || !validStatuses.includes(status))) {
|
|
11229
13072
|
res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
|
|
11230
13073
|
return;
|
|
11231
13074
|
}
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
13075
|
+
if (!clearing && config.terminalStatuses.has(status)) {
|
|
13076
|
+
res.status(400).json({
|
|
13077
|
+
error: `"${status}" is terminal \u2014 use the complete/fail transition (gated), not an override.`
|
|
13078
|
+
});
|
|
13079
|
+
return;
|
|
13080
|
+
}
|
|
13081
|
+
const { recomputeAndWrite: recomputeAndWrite2, resolveDeriveContext: resolveDeriveContext2 } = await Promise.resolve().then(() => (init_recompute(), recompute_exports));
|
|
13082
|
+
const { updateOverride: updateOverride2 } = await Promise.resolve().then(() => (init_frontmatter(), frontmatter_exports));
|
|
13083
|
+
const context = await resolveDeriveContext2();
|
|
13084
|
+
const projectDirForId = resolved.standalone ? null : resolve20(resolved.assignmentDir, "..", "..");
|
|
13085
|
+
const result = await recomputeAndWrite2(assignmentPath, {
|
|
13086
|
+
cause: clearing ? "unpin" : "pin",
|
|
13087
|
+
by: "human",
|
|
13088
|
+
projectDir: projectDirForId,
|
|
13089
|
+
context,
|
|
13090
|
+
mutate: (content) => {
|
|
13091
|
+
if (clearing) return updateOverride2(content, null);
|
|
13092
|
+
const current = parseAssignmentFull(content);
|
|
13093
|
+
if (current.override?.status === status) return content;
|
|
13094
|
+
return updateOverride2(content, { status, source: "human", reason: null, at: nowTimestamp() });
|
|
13095
|
+
}
|
|
13096
|
+
});
|
|
13097
|
+
if (result.deferredTerminal) {
|
|
13098
|
+
res.status(409).json({ error: "Assignment is terminal \u2014 reopen it first." });
|
|
13099
|
+
return;
|
|
13100
|
+
}
|
|
13101
|
+
if (result.warning) {
|
|
13102
|
+
res.status(503).json({ error: result.warning });
|
|
13103
|
+
return;
|
|
11237
13104
|
}
|
|
11238
|
-
await writeFileForce(assignmentPath, content);
|
|
11239
13105
|
const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
|
|
11240
13106
|
res.json({ assignment });
|
|
11241
13107
|
} catch (error) {
|
|
@@ -11255,7 +13121,7 @@ ${entry}`;
|
|
|
11255
13121
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
11256
13122
|
return;
|
|
11257
13123
|
}
|
|
11258
|
-
const assignmentPath =
|
|
13124
|
+
const assignmentPath = resolve20(resolved.assignmentDir, "assignment.md");
|
|
11259
13125
|
if (!await fileExists(assignmentPath)) {
|
|
11260
13126
|
res.status(404).json({ error: "Assignment not found" });
|
|
11261
13127
|
return;
|
|
@@ -11265,7 +13131,7 @@ ${entry}`;
|
|
|
11265
13131
|
res.status(400).json({ error: validation.error });
|
|
11266
13132
|
return;
|
|
11267
13133
|
}
|
|
11268
|
-
let content = await
|
|
13134
|
+
let content = await readFile17(assignmentPath, "utf-8");
|
|
11269
13135
|
content = setTopLevelField(content, "assignee", validation.value);
|
|
11270
13136
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
11271
13137
|
await writeFileForce(assignmentPath, content);
|
|
@@ -11288,7 +13154,7 @@ ${entry}`;
|
|
|
11288
13154
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
11289
13155
|
return;
|
|
11290
13156
|
}
|
|
11291
|
-
const assignmentPath =
|
|
13157
|
+
const assignmentPath = resolve20(resolved.assignmentDir, "assignment.md");
|
|
11292
13158
|
if (!await fileExists(assignmentPath)) {
|
|
11293
13159
|
res.status(404).json({ error: "Assignment not found" });
|
|
11294
13160
|
return;
|
|
@@ -11298,7 +13164,7 @@ ${entry}`;
|
|
|
11298
13164
|
res.status(400).json({ error: validation.error });
|
|
11299
13165
|
return;
|
|
11300
13166
|
}
|
|
11301
|
-
let content = await
|
|
13167
|
+
let content = await readFile17(assignmentPath, "utf-8");
|
|
11302
13168
|
content = setTopLevelField(content, "title", validation.value);
|
|
11303
13169
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
11304
13170
|
await writeFileForce(assignmentPath, content);
|
|
@@ -11321,7 +13187,7 @@ ${entry}`;
|
|
|
11321
13187
|
res.status(404).json({ error: `Assignment "${id}" not found` });
|
|
11322
13188
|
return;
|
|
11323
13189
|
}
|
|
11324
|
-
const assignmentPath =
|
|
13190
|
+
const assignmentPath = resolve20(resolved.assignmentDir, "assignment.md");
|
|
11325
13191
|
const currentContent = await readCurrentDocument(assignmentPath);
|
|
11326
13192
|
if (!currentContent) {
|
|
11327
13193
|
res.status(404).json({ error: "Assignment not found" });
|
|
@@ -11361,12 +13227,57 @@ ${entry}`;
|
|
|
11361
13227
|
return;
|
|
11362
13228
|
}
|
|
11363
13229
|
const { reason } = req.body || {};
|
|
13230
|
+
const config = await getStatusConfig();
|
|
13231
|
+
const validCommandsById = [...new Set(config.transitions.map((t) => t.command))];
|
|
13232
|
+
if (!validCommandsById.includes(command)) {
|
|
13233
|
+
res.status(400).json({ error: `Unsupported transition command "${command}"` });
|
|
13234
|
+
return;
|
|
13235
|
+
}
|
|
13236
|
+
const { recomputeAndWrite: recomputeAndWrite2, recomputeDependents: recomputeDependents2, resolveDeriveContext: resolveDeriveContext2 } = await Promise.resolve().then(() => (init_recompute(), recompute_exports));
|
|
13237
|
+
const context = await resolveDeriveContext2();
|
|
13238
|
+
const byIdPath = resolve20(resolved.assignmentDir, "assignment.md");
|
|
13239
|
+
const byIdProjectDir = resolved.standalone ? null : resolve20(resolved.assignmentDir, "..", "..");
|
|
13240
|
+
if (command === "block" || command === "unblock") {
|
|
13241
|
+
const { updateAssignmentFile: updateAssignmentFile2 } = await Promise.resolve().then(() => (init_frontmatter(), frontmatter_exports));
|
|
13242
|
+
const result = await recomputeAndWrite2(byIdPath, {
|
|
13243
|
+
cause: command,
|
|
13244
|
+
by: "human",
|
|
13245
|
+
projectDir: byIdProjectDir,
|
|
13246
|
+
context,
|
|
13247
|
+
reason: typeof reason === "string" ? reason : void 0,
|
|
13248
|
+
mutate: (content) => updateAssignmentFile2(content, {
|
|
13249
|
+
blockedReason: command === "block" ? typeof reason === "string" && reason ? reason : "(unspecified)" : null
|
|
13250
|
+
})
|
|
13251
|
+
});
|
|
13252
|
+
if (result.deferredTerminal) {
|
|
13253
|
+
res.status(409).json({ error: "Assignment is terminal \u2014 reopen it first." });
|
|
13254
|
+
return;
|
|
13255
|
+
}
|
|
13256
|
+
if (result.warning) {
|
|
13257
|
+
res.status(503).json({ error: result.warning });
|
|
13258
|
+
return;
|
|
13259
|
+
}
|
|
13260
|
+
const detail2 = resolved.standalone ? await getAssignmentDetailById(projectsDir, assignmentsDir2, id) : await getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);
|
|
13261
|
+
res.json({ assignment: detail2, warnings: [] });
|
|
13262
|
+
return;
|
|
13263
|
+
}
|
|
13264
|
+
const GATED_TERMINAL = /* @__PURE__ */ new Set(["complete", "fail", "reopen"]);
|
|
13265
|
+
const gatedFallbackById = GATED_TERMINAL.has(command) ? unambiguousCommandTarget(config.transitions, command) : void 0;
|
|
11364
13266
|
const transitionResult = await executeTransitionByDir(
|
|
11365
13267
|
resolved.assignmentDir,
|
|
11366
13268
|
command,
|
|
11367
13269
|
{
|
|
11368
13270
|
standalone: resolved.standalone,
|
|
11369
13271
|
reason: typeof reason === "string" ? reason : void 0,
|
|
13272
|
+
// Same resolution as the project route: from-specific mapping wins,
|
|
13273
|
+
// unambiguous command target as guard-free fallback for gated
|
|
13274
|
+
// terminal commands. NOTE: by-id historically ran guard-free for
|
|
13275
|
+
// all commands; the custom from-table now applies only to gated
|
|
13276
|
+
// commands' resolution (their fallback), keeping non-terminal by-id
|
|
13277
|
+
// behavior guard-free as before.
|
|
13278
|
+
commandTargets: config.custom && gatedFallbackById ? /* @__PURE__ */ new Map([[command, gatedFallbackById]]) : void 0,
|
|
13279
|
+
transitionTable: config.custom && GATED_TERMINAL.has(command) ? config.transitionTable : void 0,
|
|
13280
|
+
terminalStatuses: config.custom ? config.terminalStatuses : void 0,
|
|
11370
13281
|
linkedTodosLookup
|
|
11371
13282
|
}
|
|
11372
13283
|
);
|
|
@@ -11374,6 +13285,27 @@ ${entry}`;
|
|
|
11374
13285
|
res.status(400).json({ error: transitionResult.message, fromStatus: transitionResult.fromStatus });
|
|
11375
13286
|
return;
|
|
11376
13287
|
}
|
|
13288
|
+
const settledById = await recomputeAndWrite2(byIdPath, {
|
|
13289
|
+
cause: command,
|
|
13290
|
+
by: "human",
|
|
13291
|
+
projectDir: byIdProjectDir,
|
|
13292
|
+
context
|
|
13293
|
+
});
|
|
13294
|
+
if (settledById.warning) {
|
|
13295
|
+
res.status(503).json({ error: settledById.warning });
|
|
13296
|
+
return;
|
|
13297
|
+
}
|
|
13298
|
+
if (byIdProjectDir) {
|
|
13299
|
+
const wasTerminal = config.terminalStatuses.has(transitionResult.fromStatus);
|
|
13300
|
+
const isTerminal = transitionResult.toStatus ? config.terminalStatuses.has(transitionResult.toStatus) : false;
|
|
13301
|
+
if (wasTerminal !== isTerminal) {
|
|
13302
|
+
await recomputeDependents2(byIdProjectDir, resolved.assignmentSlug, {
|
|
13303
|
+
cause: "dep-terminal",
|
|
13304
|
+
by: "system",
|
|
13305
|
+
context
|
|
13306
|
+
});
|
|
13307
|
+
}
|
|
13308
|
+
}
|
|
11377
13309
|
const detail = resolved.standalone ? await getAssignmentDetailById(projectsDir, assignmentsDir2, id) : await getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);
|
|
11378
13310
|
res.json({ assignment: detail, warnings: transitionResult.warnings ?? [] });
|
|
11379
13311
|
} catch (error) {
|
|
@@ -11423,7 +13355,7 @@ function validateTitleBody(body) {
|
|
|
11423
13355
|
return { ok: true, value: trimmed };
|
|
11424
13356
|
}
|
|
11425
13357
|
async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDetail) {
|
|
11426
|
-
const commentsPath =
|
|
13358
|
+
const commentsPath = resolve20(assignmentDir, "comments.md");
|
|
11427
13359
|
const { body, author, type, replyTo } = req.body || {};
|
|
11428
13360
|
if (!body || typeof body !== "string" || !body.trim()) {
|
|
11429
13361
|
res.status(400).json({ error: "body is required" });
|
|
@@ -11435,7 +13367,7 @@ async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDet
|
|
|
11435
13367
|
let currentContent;
|
|
11436
13368
|
let currentCount = 0;
|
|
11437
13369
|
if (await fileExists(commentsPath)) {
|
|
11438
|
-
currentContent = await
|
|
13370
|
+
currentContent = await readFile17(commentsPath, "utf-8");
|
|
11439
13371
|
const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
|
|
11440
13372
|
if (countMatch) currentCount = parseInt(countMatch[1], 10);
|
|
11441
13373
|
} else {
|
|
@@ -11465,7 +13397,7 @@ ${entry}`;
|
|
|
11465
13397
|
res.status(201).json({ assignment, comment: { id: comment.id } });
|
|
11466
13398
|
}
|
|
11467
13399
|
async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloadDetail) {
|
|
11468
|
-
const commentsPath =
|
|
13400
|
+
const commentsPath = resolve20(assignmentDir, "comments.md");
|
|
11469
13401
|
if (!await fileExists(commentsPath)) {
|
|
11470
13402
|
res.status(404).json({ error: "Comments file not found" });
|
|
11471
13403
|
return;
|
|
@@ -11475,7 +13407,7 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloa
|
|
|
11475
13407
|
res.status(400).json({ error: "resolved (boolean) is required" });
|
|
11476
13408
|
return;
|
|
11477
13409
|
}
|
|
11478
|
-
const content = await
|
|
13410
|
+
const content = await readFile17(commentsPath, "utf-8");
|
|
11479
13411
|
const parsed = parseComments(content);
|
|
11480
13412
|
const target = parsed.entries.find((e) => e.id === commentId);
|
|
11481
13413
|
if (!target) {
|
|
@@ -11622,16 +13554,16 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir2) {
|
|
|
11622
13554
|
init_agent_sessions();
|
|
11623
13555
|
init_fs();
|
|
11624
13556
|
import { Router as Router4 } from "express";
|
|
11625
|
-
import { resolve as
|
|
13557
|
+
import { resolve as resolve21 } from "path";
|
|
11626
13558
|
|
|
11627
13559
|
// src/utils/transcript.ts
|
|
11628
|
-
import { open } from "fs/promises";
|
|
13560
|
+
import { open as open2 } from "fs/promises";
|
|
11629
13561
|
var MAX_LINES_SCANNED = 50;
|
|
11630
13562
|
async function derivePathFromTranscript(transcriptPath) {
|
|
11631
13563
|
if (!transcriptPath) return null;
|
|
11632
13564
|
let handle;
|
|
11633
13565
|
try {
|
|
11634
|
-
handle = await
|
|
13566
|
+
handle = await open2(transcriptPath, "r");
|
|
11635
13567
|
} catch {
|
|
11636
13568
|
return null;
|
|
11637
13569
|
}
|
|
@@ -11720,7 +13652,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
|
|
|
11720
13652
|
try {
|
|
11721
13653
|
const { projectSlug } = req.params;
|
|
11722
13654
|
const assignment = req.query.assignment;
|
|
11723
|
-
const projectDir =
|
|
13655
|
+
const projectDir = resolve21(projectsDir, projectSlug);
|
|
11724
13656
|
if (!await fileExists(projectDir)) {
|
|
11725
13657
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
11726
13658
|
return;
|
|
@@ -11750,7 +13682,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
|
|
|
11750
13682
|
return;
|
|
11751
13683
|
}
|
|
11752
13684
|
if (projectSlug) {
|
|
11753
|
-
const projectDir =
|
|
13685
|
+
const projectDir = resolve21(projectsDir, projectSlug);
|
|
11754
13686
|
if (!await fileExists(projectDir)) {
|
|
11755
13687
|
res.status(404).json({ error: `Project "${projectSlug}" not found` });
|
|
11756
13688
|
return;
|
|
@@ -12255,7 +14187,7 @@ init_api();
|
|
|
12255
14187
|
init_agents_schema();
|
|
12256
14188
|
import { spawn as spawn2 } from "child_process";
|
|
12257
14189
|
import { mkdir as mkdir2, writeFile as writeFile4 } from "fs/promises";
|
|
12258
|
-
import { isAbsolute as isAbsolute5, resolve as
|
|
14190
|
+
import { isAbsolute as isAbsolute5, resolve as resolve22 } from "path";
|
|
12259
14191
|
var INITIAL_PROMPT = (params2) => {
|
|
12260
14192
|
const playbook = params2.playbook?.trim();
|
|
12261
14193
|
if (!playbook) {
|
|
@@ -12516,7 +14448,7 @@ function buildShellCommandLine(plan) {
|
|
|
12516
14448
|
init_paths();
|
|
12517
14449
|
init_fs();
|
|
12518
14450
|
import { fileURLToPath } from "url";
|
|
12519
|
-
import { dirname as
|
|
14451
|
+
import { dirname as dirname5, resolve as resolve23, join as join3 } from "path";
|
|
12520
14452
|
import { realpathSync, readFileSync, mkdirSync } from "fs";
|
|
12521
14453
|
|
|
12522
14454
|
// src/dashboard/api-launch-preflight.ts
|
|
@@ -12740,33 +14672,33 @@ import { Router as Router9 } from "express";
|
|
|
12740
14672
|
|
|
12741
14673
|
// src/utils/status-config-resolution.ts
|
|
12742
14674
|
init_frontmatter();
|
|
12743
|
-
import { readFile as
|
|
12744
|
-
import { dirname as
|
|
14675
|
+
import { readFile as readFile18, writeFile as writeFile5, rm as rm2 } from "fs/promises";
|
|
14676
|
+
import { dirname as dirname6 } from "path";
|
|
12745
14677
|
|
|
12746
14678
|
// src/utils/assignment-walk.ts
|
|
12747
14679
|
init_fs();
|
|
12748
|
-
import { resolve as
|
|
12749
|
-
import { readdir as
|
|
14680
|
+
import { resolve as resolve24 } from "path";
|
|
14681
|
+
import { readdir as readdir11 } from "fs/promises";
|
|
12750
14682
|
async function listAssignmentsByProject(projectsDir, standaloneDir) {
|
|
12751
14683
|
const result = {
|
|
12752
14684
|
withAssignmentMd: [],
|
|
12753
14685
|
orphanFolders: []
|
|
12754
14686
|
};
|
|
12755
14687
|
if (await fileExists(projectsDir)) {
|
|
12756
|
-
const projects = await
|
|
14688
|
+
const projects = await readdir11(projectsDir, { withFileTypes: true });
|
|
12757
14689
|
for (const m of projects) {
|
|
12758
14690
|
if (!m.isDirectory()) continue;
|
|
12759
14691
|
if (m.name.startsWith(".") || m.name.startsWith("_")) continue;
|
|
12760
|
-
const assignmentsDir2 =
|
|
14692
|
+
const assignmentsDir2 = resolve24(projectsDir, m.name, "assignments");
|
|
12761
14693
|
if (!await fileExists(assignmentsDir2)) continue;
|
|
12762
|
-
const entries = await
|
|
14694
|
+
const entries = await readdir11(assignmentsDir2, { withFileTypes: true });
|
|
12763
14695
|
for (const a of entries) {
|
|
12764
14696
|
if (!a.isDirectory()) continue;
|
|
12765
14697
|
if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
|
|
12766
|
-
const assignmentDir =
|
|
12767
|
-
const assignmentMd =
|
|
14698
|
+
const assignmentDir = resolve24(assignmentsDir2, a.name);
|
|
14699
|
+
const assignmentMd = resolve24(assignmentDir, "assignment.md");
|
|
12768
14700
|
const entry = {
|
|
12769
|
-
projectDir:
|
|
14701
|
+
projectDir: resolve24(projectsDir, m.name),
|
|
12770
14702
|
projectSlug: m.name,
|
|
12771
14703
|
assignmentDir,
|
|
12772
14704
|
assignmentSlug: a.name,
|
|
@@ -12781,12 +14713,12 @@ async function listAssignmentsByProject(projectsDir, standaloneDir) {
|
|
|
12781
14713
|
}
|
|
12782
14714
|
}
|
|
12783
14715
|
if (standaloneDir !== null && await fileExists(standaloneDir)) {
|
|
12784
|
-
const entries = await
|
|
14716
|
+
const entries = await readdir11(standaloneDir, { withFileTypes: true });
|
|
12785
14717
|
for (const a of entries) {
|
|
12786
14718
|
if (!a.isDirectory()) continue;
|
|
12787
14719
|
if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
|
|
12788
|
-
const assignmentDir =
|
|
12789
|
-
const assignmentMd =
|
|
14720
|
+
const assignmentDir = resolve24(standaloneDir, a.name);
|
|
14721
|
+
const assignmentMd = resolve24(assignmentDir, "assignment.md");
|
|
12790
14722
|
const entry = {
|
|
12791
14723
|
projectDir: standaloneDir,
|
|
12792
14724
|
projectSlug: null,
|
|
@@ -12823,7 +14755,7 @@ async function scanAssignmentsByStatus(projectsDir, standaloneDir, ids) {
|
|
|
12823
14755
|
const assignmentPath = `${entry.assignmentDir}/assignment.md`;
|
|
12824
14756
|
let content;
|
|
12825
14757
|
try {
|
|
12826
|
-
content = await
|
|
14758
|
+
content = await readFile18(assignmentPath, "utf-8");
|
|
12827
14759
|
} catch (err) {
|
|
12828
14760
|
const code = err?.code;
|
|
12829
14761
|
if (code === "ENOENT") {
|
|
@@ -12886,7 +14818,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
|
|
|
12886
14818
|
const list = affected.get(r.id) ?? [];
|
|
12887
14819
|
for (const a of list) {
|
|
12888
14820
|
try {
|
|
12889
|
-
const content = await
|
|
14821
|
+
const content = await readFile18(a.path, "utf-8");
|
|
12890
14822
|
buffer.set(a.path, content);
|
|
12891
14823
|
} catch (err) {
|
|
12892
14824
|
const code = err?.code;
|
|
@@ -12914,7 +14846,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
|
|
|
12914
14846
|
for (const a of list) {
|
|
12915
14847
|
let current;
|
|
12916
14848
|
try {
|
|
12917
|
-
current = await
|
|
14849
|
+
current = await readFile18(a.path, "utf-8");
|
|
12918
14850
|
} catch (err) {
|
|
12919
14851
|
const code = err?.code;
|
|
12920
14852
|
if (code === "ENOENT") {
|
|
@@ -12929,10 +14861,14 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
|
|
|
12929
14861
|
);
|
|
12930
14862
|
continue;
|
|
12931
14863
|
}
|
|
12932
|
-
const
|
|
12933
|
-
|
|
12934
|
-
|
|
12935
|
-
|
|
14864
|
+
const now = nowTimestamp();
|
|
14865
|
+
const next = appendStatusHistoryEntry(
|
|
14866
|
+
updateAssignmentFile(current, {
|
|
14867
|
+
status: r.target,
|
|
14868
|
+
updated: now
|
|
14869
|
+
}),
|
|
14870
|
+
{ at: now, from: r.id, to: r.target, command: "remap", by: null }
|
|
14871
|
+
);
|
|
12936
14872
|
await writeFile5(a.path, next, "utf-8");
|
|
12937
14873
|
writtenPaths.push(a.path);
|
|
12938
14874
|
remapped++;
|
|
@@ -12963,7 +14899,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
|
|
|
12963
14899
|
const list = affected.get(r.id) ?? [];
|
|
12964
14900
|
for (const a of list) {
|
|
12965
14901
|
try {
|
|
12966
|
-
const current = await
|
|
14902
|
+
const current = await readFile18(a.path, "utf-8");
|
|
12967
14903
|
const fm = parseAssignmentFrontmatter(current);
|
|
12968
14904
|
if (fm.status !== r.id) {
|
|
12969
14905
|
console.warn(
|
|
@@ -12974,7 +14910,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
|
|
|
12974
14910
|
} catch {
|
|
12975
14911
|
continue;
|
|
12976
14912
|
}
|
|
12977
|
-
const assignmentDir =
|
|
14913
|
+
const assignmentDir = dirname6(a.path);
|
|
12978
14914
|
try {
|
|
12979
14915
|
await rm2(assignmentDir, { recursive: true, force: true });
|
|
12980
14916
|
deleted++;
|
|
@@ -13231,7 +15167,7 @@ function createStatusConfigRouter(projectsDir, assignmentsDir2) {
|
|
|
13231
15167
|
throw err;
|
|
13232
15168
|
}
|
|
13233
15169
|
try {
|
|
13234
|
-
await writeStatusConfig({ statuses, order, transitions });
|
|
15170
|
+
await writeStatusConfig({ statuses, order, transitions, derive: currentConfig.derive ?? null });
|
|
13235
15171
|
} catch (err) {
|
|
13236
15172
|
console.error("Error saving status config after applying resolutions:", err);
|
|
13237
15173
|
res.status(500).json({
|
|
@@ -13285,7 +15221,7 @@ import { Router as Router10 } from "express";
|
|
|
13285
15221
|
init_paths();
|
|
13286
15222
|
import Database2 from "better-sqlite3";
|
|
13287
15223
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
13288
|
-
import { resolve as
|
|
15224
|
+
import { resolve as resolve25 } from "path";
|
|
13289
15225
|
var db2 = null;
|
|
13290
15226
|
var LEASE_SCHEMA_VERSION = "1";
|
|
13291
15227
|
var SCHEMA_SQL2 = `
|
|
@@ -13372,7 +15308,7 @@ function isBusyError(err) {
|
|
|
13372
15308
|
}
|
|
13373
15309
|
function initLeasesDb(dbPath) {
|
|
13374
15310
|
if (db2) return db2;
|
|
13375
|
-
const finalPath = dbPath ??
|
|
15311
|
+
const finalPath = dbPath ?? resolve25(syntaurRoot(), "syntaur.db");
|
|
13376
15312
|
db2 = new Database2(finalPath);
|
|
13377
15313
|
db2.pragma("journal_mode = WAL");
|
|
13378
15314
|
db2.pragma("busy_timeout = 5000");
|
|
@@ -13521,7 +15457,7 @@ import { Router as Router11 } from "express";
|
|
|
13521
15457
|
// src/db/usage-db.ts
|
|
13522
15458
|
init_paths();
|
|
13523
15459
|
import Database3 from "better-sqlite3";
|
|
13524
|
-
import { resolve as
|
|
15460
|
+
import { resolve as resolve26 } from "path";
|
|
13525
15461
|
var db3 = null;
|
|
13526
15462
|
var USAGE_SCHEMA_VERSION = "1";
|
|
13527
15463
|
var SCHEMA_SQL3 = `
|
|
@@ -13578,7 +15514,7 @@ CREATE INDEX IF NOT EXISTS idx_usage_daily_day
|
|
|
13578
15514
|
`;
|
|
13579
15515
|
function initUsageDb(dbPath) {
|
|
13580
15516
|
if (db3) return db3;
|
|
13581
|
-
const finalPath = dbPath ??
|
|
15517
|
+
const finalPath = dbPath ?? resolve26(syntaurRoot(), "syntaur.db");
|
|
13582
15518
|
db3 = new Database3(finalPath);
|
|
13583
15519
|
db3.pragma("journal_mode = WAL");
|
|
13584
15520
|
db3.pragma("busy_timeout = 5000");
|
|
@@ -13837,8 +15773,8 @@ init_slug();
|
|
|
13837
15773
|
init_timestamp();
|
|
13838
15774
|
init_fs();
|
|
13839
15775
|
import { Router as Router12 } from "express";
|
|
13840
|
-
import { resolve as
|
|
13841
|
-
import { readFile as
|
|
15776
|
+
import { resolve as resolve27 } from "path";
|
|
15777
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
13842
15778
|
init_playbooks();
|
|
13843
15779
|
function statusForPlaybookError(code) {
|
|
13844
15780
|
switch (code) {
|
|
@@ -13920,8 +15856,8 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
13920
15856
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
13921
15857
|
return;
|
|
13922
15858
|
}
|
|
13923
|
-
const filePath =
|
|
13924
|
-
const content = await
|
|
15859
|
+
const filePath = resolve27(playbooksDir2, resolved.filename);
|
|
15860
|
+
const content = await readFile19(filePath, "utf-8");
|
|
13925
15861
|
res.json({
|
|
13926
15862
|
documentType: "playbook",
|
|
13927
15863
|
title: `Edit Playbook: ${resolved.slug}`,
|
|
@@ -13946,7 +15882,7 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
13946
15882
|
return;
|
|
13947
15883
|
}
|
|
13948
15884
|
await ensureDir(playbooksDir2);
|
|
13949
|
-
const filePath =
|
|
15885
|
+
const filePath = resolve27(playbooksDir2, `${slug}.md`);
|
|
13950
15886
|
if (await fileExists(filePath)) {
|
|
13951
15887
|
res.status(409).json({ error: `Playbook "${slug}" already exists` });
|
|
13952
15888
|
return;
|
|
@@ -13970,7 +15906,7 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
13970
15906
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
13971
15907
|
return;
|
|
13972
15908
|
}
|
|
13973
|
-
const filePath =
|
|
15909
|
+
const filePath = resolve27(playbooksDir2, resolved.filename);
|
|
13974
15910
|
await writeFileForce(filePath, content);
|
|
13975
15911
|
await rebuildPlaybookManifest(playbooksDir2);
|
|
13976
15912
|
res.json({ slug: resolved.slug, path: filePath });
|
|
@@ -14018,8 +15954,8 @@ init_parser2();
|
|
|
14018
15954
|
init_fs();
|
|
14019
15955
|
init_paths();
|
|
14020
15956
|
import { Router as Router14 } from "express";
|
|
14021
|
-
import { readdir as
|
|
14022
|
-
import { resolve as resolvePath, dirname as
|
|
15957
|
+
import { readdir as readdir13 } from "fs/promises";
|
|
15958
|
+
import { resolve as resolvePath, dirname as dirname8 } from "path";
|
|
14023
15959
|
import { rename as rename6, mkdir as mkdir4 } from "fs/promises";
|
|
14024
15960
|
init_slug();
|
|
14025
15961
|
|
|
@@ -14029,7 +15965,7 @@ init_parser2();
|
|
|
14029
15965
|
// src/commands/create-assignment.ts
|
|
14030
15966
|
init_slug();
|
|
14031
15967
|
init_timestamp();
|
|
14032
|
-
import { resolve as
|
|
15968
|
+
import { resolve as resolve28 } from "path";
|
|
14033
15969
|
init_paths();
|
|
14034
15970
|
init_fs();
|
|
14035
15971
|
init_config2();
|
|
@@ -14107,14 +16043,14 @@ async function createAssignmentCommand(title, options) {
|
|
|
14107
16043
|
if (options.oneOff) {
|
|
14108
16044
|
const standaloneRoot = assignmentsDir();
|
|
14109
16045
|
folderName = id;
|
|
14110
|
-
assignmentDir =
|
|
16046
|
+
assignmentDir = resolve28(standaloneRoot, folderName);
|
|
14111
16047
|
projectSlug = null;
|
|
14112
16048
|
await ensureDir(standaloneRoot);
|
|
14113
16049
|
} else {
|
|
14114
16050
|
const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
|
|
14115
16051
|
projectSlug = options.project;
|
|
14116
|
-
const projectDir =
|
|
14117
|
-
const projectMdPath =
|
|
16052
|
+
const projectDir = resolve28(baseDir, projectSlug);
|
|
16053
|
+
const projectMdPath = resolve28(projectDir, "project.md");
|
|
14118
16054
|
if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
|
|
14119
16055
|
throw new Error(
|
|
14120
16056
|
`Project "${projectSlug}" not found at ${projectDir}.
|
|
@@ -14122,9 +16058,9 @@ Run 'syntaur create-project' first or use --one-off.`
|
|
|
14122
16058
|
);
|
|
14123
16059
|
}
|
|
14124
16060
|
if (dependsOn.length > 0) {
|
|
14125
|
-
const depDirBase =
|
|
16061
|
+
const depDirBase = resolve28(projectDir, "assignments");
|
|
14126
16062
|
for (const dep of dependsOn) {
|
|
14127
|
-
const depDir =
|
|
16063
|
+
const depDir = resolve28(depDirBase, dep);
|
|
14128
16064
|
if (!await fileExists(depDir)) {
|
|
14129
16065
|
console.warn(
|
|
14130
16066
|
`Warning: dependency "${dep}" does not exist in project "${projectSlug}" yet.`
|
|
@@ -14133,7 +16069,7 @@ Run 'syntaur create-project' first or use --one-off.`
|
|
|
14133
16069
|
}
|
|
14134
16070
|
}
|
|
14135
16071
|
folderName = assignmentSlug;
|
|
14136
|
-
assignmentDir =
|
|
16072
|
+
assignmentDir = resolve28(projectDir, "assignments", folderName);
|
|
14137
16073
|
}
|
|
14138
16074
|
if (await fileExists(assignmentDir)) {
|
|
14139
16075
|
throw new Error(
|
|
@@ -14145,7 +16081,7 @@ Use --slug to specify a different slug.`
|
|
|
14145
16081
|
const companionAssignmentRef = projectSlug === null ? id : assignmentSlug;
|
|
14146
16082
|
const files = [
|
|
14147
16083
|
[
|
|
14148
|
-
|
|
16084
|
+
resolve28(assignmentDir, "assignment.md"),
|
|
14149
16085
|
renderAssignment({
|
|
14150
16086
|
id,
|
|
14151
16087
|
slug: assignmentSlug,
|
|
@@ -14163,35 +16099,35 @@ Use --slug to specify a different slug.`
|
|
|
14163
16099
|
})
|
|
14164
16100
|
],
|
|
14165
16101
|
[
|
|
14166
|
-
|
|
16102
|
+
resolve28(assignmentDir, "scratchpad.md"),
|
|
14167
16103
|
renderScratchpad({
|
|
14168
16104
|
assignmentSlug: companionAssignmentRef,
|
|
14169
16105
|
timestamp
|
|
14170
16106
|
})
|
|
14171
16107
|
],
|
|
14172
16108
|
[
|
|
14173
|
-
|
|
16109
|
+
resolve28(assignmentDir, "handoff.md"),
|
|
14174
16110
|
renderHandoff({
|
|
14175
16111
|
assignmentSlug: companionAssignmentRef,
|
|
14176
16112
|
timestamp
|
|
14177
16113
|
})
|
|
14178
16114
|
],
|
|
14179
16115
|
[
|
|
14180
|
-
|
|
16116
|
+
resolve28(assignmentDir, "decision-record.md"),
|
|
14181
16117
|
renderDecisionRecord({
|
|
14182
16118
|
assignmentSlug: companionAssignmentRef,
|
|
14183
16119
|
timestamp
|
|
14184
16120
|
})
|
|
14185
16121
|
],
|
|
14186
16122
|
[
|
|
14187
|
-
|
|
16123
|
+
resolve28(assignmentDir, "progress.md"),
|
|
14188
16124
|
renderProgress({
|
|
14189
16125
|
assignment: companionAssignmentRef,
|
|
14190
16126
|
timestamp
|
|
14191
16127
|
})
|
|
14192
16128
|
],
|
|
14193
16129
|
[
|
|
14194
|
-
|
|
16130
|
+
resolve28(assignmentDir, "comments.md"),
|
|
14195
16131
|
renderComments({
|
|
14196
16132
|
assignment: companionAssignmentRef,
|
|
14197
16133
|
timestamp
|
|
@@ -14364,8 +16300,8 @@ init_api();
|
|
|
14364
16300
|
import { raw } from "express";
|
|
14365
16301
|
|
|
14366
16302
|
// src/todos/attachments.ts
|
|
14367
|
-
import { mkdir as mkdir3, readdir as
|
|
14368
|
-
import { resolve as
|
|
16303
|
+
import { mkdir as mkdir3, readdir as readdir12, stat as stat2, rename as rename5, rm as rm3, unlink as unlink6, writeFile as writeFile6, cp } from "fs/promises";
|
|
16304
|
+
import { resolve as resolve29, basename as basename5, dirname as dirname7, extname } from "path";
|
|
14369
16305
|
|
|
14370
16306
|
// src/utils/proof-artifact-id.ts
|
|
14371
16307
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
@@ -14452,16 +16388,16 @@ function sanitizeAttachmentName(name) {
|
|
|
14452
16388
|
return n;
|
|
14453
16389
|
}
|
|
14454
16390
|
function attachmentsRootDir(todosDir2) {
|
|
14455
|
-
return
|
|
16391
|
+
return resolve29(todosDir2, "attachments");
|
|
14456
16392
|
}
|
|
14457
16393
|
function attachmentDirFor(todosDir2, scopeId, todoId) {
|
|
14458
16394
|
assertScope(scopeId);
|
|
14459
16395
|
assertTodoId(todoId);
|
|
14460
|
-
return
|
|
16396
|
+
return resolve29(attachmentsRootDir(todosDir2), scopeId, todoId);
|
|
14461
16397
|
}
|
|
14462
16398
|
async function dirExists(p) {
|
|
14463
16399
|
try {
|
|
14464
|
-
return (await
|
|
16400
|
+
return (await stat2(p)).isDirectory();
|
|
14465
16401
|
} catch {
|
|
14466
16402
|
return false;
|
|
14467
16403
|
}
|
|
@@ -14471,7 +16407,7 @@ async function writeAttachment(todosDir2, scopeId, todoId, originalName, bytes)
|
|
|
14471
16407
|
await mkdir3(dir, { recursive: true });
|
|
14472
16408
|
const id = generateArtifactId();
|
|
14473
16409
|
const filename = sanitizeAttachmentName(originalName);
|
|
14474
|
-
await writeFile6(
|
|
16410
|
+
await writeFile6(resolve29(dir, `${id}__${filename}`), bytes);
|
|
14475
16411
|
return {
|
|
14476
16412
|
id,
|
|
14477
16413
|
filename,
|
|
@@ -14484,7 +16420,7 @@ async function listAttachments(todosDir2, scopeId, todoId) {
|
|
|
14484
16420
|
const dir = attachmentDirFor(todosDir2, scopeId, todoId);
|
|
14485
16421
|
let names;
|
|
14486
16422
|
try {
|
|
14487
|
-
names = await
|
|
16423
|
+
names = await readdir12(dir);
|
|
14488
16424
|
} catch {
|
|
14489
16425
|
return [];
|
|
14490
16426
|
}
|
|
@@ -14496,7 +16432,7 @@ async function listAttachments(todosDir2, scopeId, todoId) {
|
|
|
14496
16432
|
if (!ATTACHMENT_ID_RE.test(id)) continue;
|
|
14497
16433
|
const filename = stored.slice(sep2 + 2);
|
|
14498
16434
|
try {
|
|
14499
|
-
const st = await
|
|
16435
|
+
const st = await stat2(resolve29(dir, stored));
|
|
14500
16436
|
if (!st.isFile()) continue;
|
|
14501
16437
|
out.push({ id, filename, mime: mimeForName(filename), size: st.size, createdAt: st.mtime.toISOString() });
|
|
14502
16438
|
} catch {
|
|
@@ -14507,10 +16443,10 @@ async function listAttachments(todosDir2, scopeId, todoId) {
|
|
|
14507
16443
|
}
|
|
14508
16444
|
async function readScopeAttachments(todosDir2, scopeId) {
|
|
14509
16445
|
assertScope(scopeId);
|
|
14510
|
-
const scopeDir =
|
|
16446
|
+
const scopeDir = resolve29(attachmentsRootDir(todosDir2), scopeId);
|
|
14511
16447
|
let todoIds;
|
|
14512
16448
|
try {
|
|
14513
|
-
todoIds = await
|
|
16449
|
+
todoIds = await readdir12(scopeDir);
|
|
14514
16450
|
} catch {
|
|
14515
16451
|
return {};
|
|
14516
16452
|
}
|
|
@@ -14527,7 +16463,7 @@ async function resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId) {
|
|
|
14527
16463
|
const dir = attachmentDirFor(todosDir2, scopeId, todoId);
|
|
14528
16464
|
let names;
|
|
14529
16465
|
try {
|
|
14530
|
-
names = await
|
|
16466
|
+
names = await readdir12(dir);
|
|
14531
16467
|
} catch {
|
|
14532
16468
|
return null;
|
|
14533
16469
|
}
|
|
@@ -14535,12 +16471,12 @@ async function resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId) {
|
|
|
14535
16471
|
const stored = names.find((n) => n.startsWith(prefix));
|
|
14536
16472
|
if (!stored) return null;
|
|
14537
16473
|
const filename = stored.slice(prefix.length);
|
|
14538
|
-
return { path:
|
|
16474
|
+
return { path: resolve29(dir, stored), filename, mime: mimeForName(filename) };
|
|
14539
16475
|
}
|
|
14540
16476
|
async function deleteAttachment(todosDir2, scopeId, todoId, attachmentId) {
|
|
14541
16477
|
const resolved = await resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId);
|
|
14542
16478
|
if (!resolved) return false;
|
|
14543
|
-
await
|
|
16479
|
+
await unlink6(resolved.path);
|
|
14544
16480
|
return true;
|
|
14545
16481
|
}
|
|
14546
16482
|
async function deleteAllAttachments(todosDir2, scopeId, todoId) {
|
|
@@ -14555,7 +16491,7 @@ async function moveAttachments(srcTodosDir, srcScopeId, dstTodosDir, dstScopeId,
|
|
|
14555
16491
|
const src = attachmentDirFor(srcTodosDir, srcScopeId, todoId);
|
|
14556
16492
|
if (!await dirExists(src)) return;
|
|
14557
16493
|
const dst = attachmentDirFor(dstTodosDir, dstScopeId, todoId);
|
|
14558
|
-
await mkdir3(
|
|
16494
|
+
await mkdir3(dirname7(dst), { recursive: true });
|
|
14559
16495
|
try {
|
|
14560
16496
|
await rename5(src, dst);
|
|
14561
16497
|
} catch (err) {
|
|
@@ -14831,7 +16767,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
|
|
|
14831
16767
|
router.get("/", async (_req, res) => {
|
|
14832
16768
|
try {
|
|
14833
16769
|
await ensureDir(todosDir2);
|
|
14834
|
-
const files = await
|
|
16770
|
+
const files = await readdir13(todosDir2).catch(() => []);
|
|
14835
16771
|
const workspaces = [];
|
|
14836
16772
|
for (const file of files) {
|
|
14837
16773
|
if (typeof file !== "string") continue;
|
|
@@ -14947,8 +16883,8 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
|
|
|
14947
16883
|
router.post("/:workspace/archive", async (req, res) => {
|
|
14948
16884
|
try {
|
|
14949
16885
|
const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
|
|
14950
|
-
const { resolve:
|
|
14951
|
-
const { readFile:
|
|
16886
|
+
const { resolve: resolve34 } = await import("path");
|
|
16887
|
+
const { readFile: readFile23 } = await import("fs/promises");
|
|
14952
16888
|
const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
14953
16889
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
14954
16890
|
const outcome = await wsLock(workspace, async () => {
|
|
@@ -14964,10 +16900,10 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
|
|
|
14964
16900
|
(e) => e.itemIds.every((id) => completedIds.has(id))
|
|
14965
16901
|
);
|
|
14966
16902
|
const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
|
|
14967
|
-
await ensureDir(
|
|
16903
|
+
await ensureDir(resolve34(todosDir2, "archive"));
|
|
14968
16904
|
let archContent = "";
|
|
14969
16905
|
if (await fileExists(archFile)) {
|
|
14970
|
-
archContent = await
|
|
16906
|
+
archContent = await readFile23(archFile, "utf-8");
|
|
14971
16907
|
archContent = archContent.trimEnd() + "\n\n";
|
|
14972
16908
|
} else {
|
|
14973
16909
|
archContent = `---
|
|
@@ -15256,7 +17192,7 @@ workspace: ${workspace}
|
|
|
15256
17192
|
const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
|
|
15257
17193
|
const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
|
|
15258
17194
|
const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
15259
|
-
const { readFile:
|
|
17195
|
+
const { readFile: readFile23 } = await import("fs/promises");
|
|
15260
17196
|
const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
|
|
15261
17197
|
const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
|
|
15262
17198
|
let assignmentRef;
|
|
@@ -15277,7 +17213,7 @@ workspace: ${workspace}
|
|
|
15277
17213
|
}
|
|
15278
17214
|
const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
|
|
15279
17215
|
if (!await fileExists2(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
|
|
15280
|
-
let content = await
|
|
17216
|
+
let content = await readFile23(assignmentMdPath, "utf-8");
|
|
15281
17217
|
content = appendTodosToAssignmentBody2(
|
|
15282
17218
|
content,
|
|
15283
17219
|
items.map((it) => ({
|
|
@@ -15394,7 +17330,7 @@ workspace: ${workspace}
|
|
|
15394
17330
|
return { status: 409, error: "attachments already exist in target" };
|
|
15395
17331
|
}
|
|
15396
17332
|
if (item.planDir && newPlanDir) {
|
|
15397
|
-
await mkdir4(
|
|
17333
|
+
await mkdir4(dirname8(newPlanDir), { recursive: true });
|
|
15398
17334
|
await rename6(item.planDir, newPlanDir);
|
|
15399
17335
|
item.planDir = newPlanDir;
|
|
15400
17336
|
}
|
|
@@ -15472,8 +17408,8 @@ init_fs();
|
|
|
15472
17408
|
init_paths();
|
|
15473
17409
|
init_slug();
|
|
15474
17410
|
import { Router as Router15 } from "express";
|
|
15475
|
-
import { mkdir as mkdir5, readFile as
|
|
15476
|
-
import { resolve as
|
|
17411
|
+
import { mkdir as mkdir5, readFile as readFile20, rename as rename7 } from "fs/promises";
|
|
17412
|
+
import { resolve as resolve30, dirname as dirname9 } from "path";
|
|
15477
17413
|
init_api();
|
|
15478
17414
|
var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
|
|
15479
17415
|
function touchItem4(item) {
|
|
@@ -15489,7 +17425,7 @@ function params(req) {
|
|
|
15489
17425
|
return req.params;
|
|
15490
17426
|
}
|
|
15491
17427
|
async function projectExists(projectsDir, slug) {
|
|
15492
|
-
return fileExists(
|
|
17428
|
+
return fileExists(resolve30(projectsDir, slug, "project.md"));
|
|
15493
17429
|
}
|
|
15494
17430
|
async function ensureProjectTodosDir(projectsDir, slug) {
|
|
15495
17431
|
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
@@ -15506,7 +17442,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
|
|
|
15506
17442
|
throw err;
|
|
15507
17443
|
}
|
|
15508
17444
|
try {
|
|
15509
|
-
await mkdir5(
|
|
17445
|
+
await mkdir5(resolve30(todosDir2, "archive"), { recursive: false });
|
|
15510
17446
|
} catch (err) {
|
|
15511
17447
|
const code = err.code;
|
|
15512
17448
|
if (code === "EEXIST") return;
|
|
@@ -15710,7 +17646,7 @@ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
|
|
|
15710
17646
|
const archFile = archivePath(todosDir2, slug, checklist.archiveInterval);
|
|
15711
17647
|
let archContent = "";
|
|
15712
17648
|
if (await fileExists(archFile)) {
|
|
15713
|
-
archContent = await
|
|
17649
|
+
archContent = await readFile20(archFile, "utf-8");
|
|
15714
17650
|
archContent = archContent.trimEnd() + "\n\n";
|
|
15715
17651
|
} else {
|
|
15716
17652
|
archContent = `---
|
|
@@ -16169,17 +18105,17 @@ workspace: ${slug}
|
|
|
16169
18105
|
if (tg.includes("/")) {
|
|
16170
18106
|
const parts = tg.split("/");
|
|
16171
18107
|
if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
|
|
16172
|
-
assignmentDir =
|
|
18108
|
+
assignmentDir = resolve30(projectsDir, parts[0], "assignments", parts[1]);
|
|
16173
18109
|
assignmentRef = `${parts[0]}/${parts[1]}`;
|
|
16174
18110
|
} else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tg)) {
|
|
16175
|
-
assignmentDir =
|
|
18111
|
+
assignmentDir = resolve30(assignmentsDirFn(), tg);
|
|
16176
18112
|
assignmentRef = tg;
|
|
16177
18113
|
} else {
|
|
16178
18114
|
return { error: `Invalid target.assignment "${tg}"` };
|
|
16179
18115
|
}
|
|
16180
|
-
const assignmentMdPath =
|
|
18116
|
+
const assignmentMdPath = resolve30(assignmentDir, "assignment.md");
|
|
16181
18117
|
if (!await fileExists(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
|
|
16182
|
-
let content = await
|
|
18118
|
+
let content = await readFile20(assignmentMdPath, "utf-8");
|
|
16183
18119
|
content = appendTodosToAssignmentBody2(
|
|
16184
18120
|
content,
|
|
16185
18121
|
items.map((it) => ({
|
|
@@ -16318,7 +18254,7 @@ workspace: ${slug}
|
|
|
16318
18254
|
return { status: 409, error: "attachments already exist in target" };
|
|
16319
18255
|
}
|
|
16320
18256
|
if (item.planDir && newPlanDir) {
|
|
16321
|
-
await mkdir5(
|
|
18257
|
+
await mkdir5(dirname9(newPlanDir), { recursive: true });
|
|
16322
18258
|
await rename7(item.planDir, newPlanDir);
|
|
16323
18259
|
item.planDir = newPlanDir;
|
|
16324
18260
|
}
|
|
@@ -16382,7 +18318,7 @@ workspace: ${slug}
|
|
|
16382
18318
|
|
|
16383
18319
|
// src/dashboard/api-bundles.ts
|
|
16384
18320
|
import { Router as Router16 } from "express";
|
|
16385
|
-
import { readdir as
|
|
18321
|
+
import { readdir as readdir14 } from "fs/promises";
|
|
16386
18322
|
|
|
16387
18323
|
// src/todos/bundle-parser.ts
|
|
16388
18324
|
init_parser();
|
|
@@ -16390,7 +18326,7 @@ init_fs();
|
|
|
16390
18326
|
init_paths();
|
|
16391
18327
|
init_parser2();
|
|
16392
18328
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
16393
|
-
import { readFile as
|
|
18329
|
+
import { readFile as readFile21 } from "fs/promises";
|
|
16394
18330
|
var BUNDLE_ID_REGEX = /^[a-f0-9]{4}$/;
|
|
16395
18331
|
var SCOPE_VALUES = /* @__PURE__ */ new Set(["workspace", "project", "global"]);
|
|
16396
18332
|
var SCOPE_ID_REGEX = /^[a-z0-9_][a-z0-9_-]*$/;
|
|
@@ -16457,7 +18393,7 @@ function parseBundles(content) {
|
|
|
16457
18393
|
async function readBundles(todosDir2) {
|
|
16458
18394
|
const path = bundlesPath(todosDir2);
|
|
16459
18395
|
if (!await fileExists(path)) return [];
|
|
16460
|
-
const content = await
|
|
18396
|
+
const content = await readFile21(path, "utf-8");
|
|
16461
18397
|
return parseBundles(content).bundles;
|
|
16462
18398
|
}
|
|
16463
18399
|
|
|
@@ -16506,7 +18442,7 @@ function createBundlesRouter(todosDir2, broadcast) {
|
|
|
16506
18442
|
try {
|
|
16507
18443
|
await ensureDir(todosDir2);
|
|
16508
18444
|
const bundles = await readBundles(todosDir2);
|
|
16509
|
-
const workspaceFiles = await
|
|
18445
|
+
const workspaceFiles = await readdir14(todosDir2).catch(() => []);
|
|
16510
18446
|
const itemsByKey = /* @__PURE__ */ new Map();
|
|
16511
18447
|
for (const f of workspaceFiles) {
|
|
16512
18448
|
if (typeof f !== "string") continue;
|
|
@@ -16559,7 +18495,7 @@ init_fs();
|
|
|
16559
18495
|
init_paths();
|
|
16560
18496
|
init_slug();
|
|
16561
18497
|
import { Router as Router17 } from "express";
|
|
16562
|
-
import { resolve as
|
|
18498
|
+
import { resolve as resolve31 } from "path";
|
|
16563
18499
|
init_parser2();
|
|
16564
18500
|
function deriveStatus2(bundle, items) {
|
|
16565
18501
|
const members = bundle.todoIds.map((id) => items.find((i) => i.id === id)).filter((i) => i !== void 0);
|
|
@@ -16601,7 +18537,7 @@ function createProjectBundlesRouter(projectsDir, broadcast) {
|
|
|
16601
18537
|
router.get("/", async (req, res) => {
|
|
16602
18538
|
try {
|
|
16603
18539
|
const slug = getProjectIdParam2(req.params.projectId);
|
|
16604
|
-
const projectMd =
|
|
18540
|
+
const projectMd = resolve31(projectsDir, slug, "project.md");
|
|
16605
18541
|
if (!await fileExists(projectMd)) {
|
|
16606
18542
|
notFound2(res, slug);
|
|
16607
18543
|
return;
|
|
@@ -16630,8 +18566,8 @@ init_fs();
|
|
|
16630
18566
|
init_config2();
|
|
16631
18567
|
import { execFile as execFile2 } from "child_process";
|
|
16632
18568
|
import { promisify as promisify2 } from "util";
|
|
16633
|
-
import { cp as cp2, mkdtemp, rm as rm4, readFile as
|
|
16634
|
-
import { resolve as
|
|
18569
|
+
import { cp as cp2, mkdtemp, rm as rm4, readFile as readFile22, writeFile as writeFile7, unlink as unlink7, stat as stat3, open as open3, rename as rename8 } from "fs/promises";
|
|
18570
|
+
import { resolve as resolve32, join as join4 } from "path";
|
|
16635
18571
|
import { tmpdir } from "os";
|
|
16636
18572
|
var exec2 = promisify2(execFile2);
|
|
16637
18573
|
var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
|
|
@@ -16671,7 +18607,7 @@ async function resolveCategoryPath(category) {
|
|
|
16671
18607
|
case "servers":
|
|
16672
18608
|
return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
|
|
16673
18609
|
case "config":
|
|
16674
|
-
return { sourcePath:
|
|
18610
|
+
return { sourcePath: resolve32(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
|
|
16675
18611
|
}
|
|
16676
18612
|
}
|
|
16677
18613
|
async function checkGitInstalled() {
|
|
@@ -16681,17 +18617,17 @@ async function checkGitInstalled() {
|
|
|
16681
18617
|
throw new Error("git is not installed or not on PATH. Install git and try again.");
|
|
16682
18618
|
}
|
|
16683
18619
|
}
|
|
16684
|
-
async function
|
|
16685
|
-
const lockPath =
|
|
18620
|
+
async function acquireLock2() {
|
|
18621
|
+
const lockPath = resolve32(syntaurRoot(), LOCK_FILE_NAME);
|
|
16686
18622
|
await ensureDir(syntaurRoot());
|
|
16687
18623
|
try {
|
|
16688
|
-
const handle = await
|
|
18624
|
+
const handle = await open3(lockPath, "wx");
|
|
16689
18625
|
await handle.write(String(process.pid));
|
|
16690
18626
|
await handle.close();
|
|
16691
18627
|
return lockPath;
|
|
16692
18628
|
} catch (err) {
|
|
16693
18629
|
if (err.code === "EEXIST") {
|
|
16694
|
-
const pid = await
|
|
18630
|
+
const pid = await readFile22(lockPath, "utf-8").catch(() => "");
|
|
16695
18631
|
throw new Error(
|
|
16696
18632
|
`Backup operation already in progress (lock file at ${lockPath}, pid ${pid.trim() || "unknown"}). If stale, delete the file and retry.`
|
|
16697
18633
|
);
|
|
@@ -16701,7 +18637,7 @@ async function acquireLock() {
|
|
|
16701
18637
|
}
|
|
16702
18638
|
async function releaseLock(lockPath) {
|
|
16703
18639
|
try {
|
|
16704
|
-
await
|
|
18640
|
+
await unlink7(lockPath);
|
|
16705
18641
|
} catch {
|
|
16706
18642
|
}
|
|
16707
18643
|
}
|
|
@@ -16724,12 +18660,12 @@ async function cloneOrInit(repoUrl, destDir) {
|
|
|
16724
18660
|
}
|
|
16725
18661
|
async function copyRecursive(src, dest) {
|
|
16726
18662
|
if (!await fileExists(src)) return;
|
|
16727
|
-
const s = await
|
|
18663
|
+
const s = await stat3(src);
|
|
16728
18664
|
if (s.isDirectory()) {
|
|
16729
18665
|
await ensureDir(dest);
|
|
16730
18666
|
await cp2(src, dest, { recursive: true, force: true });
|
|
16731
18667
|
} else {
|
|
16732
|
-
await ensureDir(
|
|
18668
|
+
await ensureDir(resolve32(dest, ".."));
|
|
16733
18669
|
await cp2(src, dest, { force: true });
|
|
16734
18670
|
}
|
|
16735
18671
|
}
|
|
@@ -16738,7 +18674,7 @@ function resolveCategoriesStrict(csv) {
|
|
|
16738
18674
|
return parseCategoriesStrict(parts);
|
|
16739
18675
|
}
|
|
16740
18676
|
async function readSanitizedConfig(configPath) {
|
|
16741
|
-
const content = await
|
|
18677
|
+
const content = await readFile22(configPath, "utf-8");
|
|
16742
18678
|
return content.replace(/^(\s*lastBackup:\s*).*$/m, "$1null").replace(/^(\s*lastRestore:\s*).*$/m, "$1null");
|
|
16743
18679
|
}
|
|
16744
18680
|
async function backupToGithub(overrides) {
|
|
@@ -16757,7 +18693,7 @@ async function backupToGithub(overrides) {
|
|
|
16757
18693
|
if (categories.length === 0) {
|
|
16758
18694
|
throw new Error("No valid backup categories selected.");
|
|
16759
18695
|
}
|
|
16760
|
-
const lockPath = await
|
|
18696
|
+
const lockPath = await acquireLock2();
|
|
16761
18697
|
let tmpDir = null;
|
|
16762
18698
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
16763
18699
|
try {
|
|
@@ -16777,7 +18713,7 @@ async function backupToGithub(overrides) {
|
|
|
16777
18713
|
}
|
|
16778
18714
|
if (category === "config") {
|
|
16779
18715
|
const sanitized = await readSanitizedConfig(sourcePath);
|
|
16780
|
-
await ensureDir(
|
|
18716
|
+
await ensureDir(resolve32(destPath, ".."));
|
|
16781
18717
|
await writeFile7(destPath, sanitized, "utf-8");
|
|
16782
18718
|
} else {
|
|
16783
18719
|
await copyRecursive(sourcePath, destPath);
|
|
@@ -16831,7 +18767,7 @@ async function backupToGithub(overrides) {
|
|
|
16831
18767
|
}
|
|
16832
18768
|
async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
|
|
16833
18769
|
if (isFile) {
|
|
16834
|
-
await ensureDir(
|
|
18770
|
+
await ensureDir(resolve32(localPath, ".."));
|
|
16835
18771
|
await cp2(repoSrcPath, localPath, { force: true });
|
|
16836
18772
|
return;
|
|
16837
18773
|
}
|
|
@@ -16886,7 +18822,7 @@ async function restoreFromGithub(overrides) {
|
|
|
16886
18822
|
if (categories.length === 0) {
|
|
16887
18823
|
throw new Error("No valid restore categories selected.");
|
|
16888
18824
|
}
|
|
16889
|
-
const lockPath = await
|
|
18825
|
+
const lockPath = await acquireLock2();
|
|
16890
18826
|
let tmpDir = null;
|
|
16891
18827
|
const restored = [];
|
|
16892
18828
|
const failed = [];
|
|
@@ -16932,7 +18868,7 @@ async function restoreFromGithub(overrides) {
|
|
|
16932
18868
|
}
|
|
16933
18869
|
async function getBackupStatus() {
|
|
16934
18870
|
const config = await readConfig();
|
|
16935
|
-
const lockPath =
|
|
18871
|
+
const lockPath = resolve32(syntaurRoot(), LOCK_FILE_NAME);
|
|
16936
18872
|
const locked = await fileExists(lockPath);
|
|
16937
18873
|
return {
|
|
16938
18874
|
repo: config.backup?.repo ?? null,
|
|
@@ -17280,7 +19216,7 @@ function createDashboardServer(options) {
|
|
|
17280
19216
|
(async () => {
|
|
17281
19217
|
try {
|
|
17282
19218
|
const configResult = await migrateLegacyConfig(
|
|
17283
|
-
|
|
19219
|
+
resolve33(syntaurRoot(), "config.md")
|
|
17284
19220
|
);
|
|
17285
19221
|
const projectResult = await migrateLegacyProjectFiles(projectsDir);
|
|
17286
19222
|
const summary = summarizeMigration(projectResult, configResult);
|
|
@@ -17798,14 +19734,14 @@ function createDashboardServer(options) {
|
|
|
17798
19734
|
app.use("/api/backup", createBackupRouter());
|
|
17799
19735
|
if (serveStaticUi && dashboardDistPath) {
|
|
17800
19736
|
const sendOpts = { dotfiles: "allow" };
|
|
17801
|
-
app.use("/assets", express.static(
|
|
19737
|
+
app.use("/assets", express.static(resolve33(dashboardDistPath, "assets"), sendOpts));
|
|
17802
19738
|
app.use(express.static(dashboardDistPath, { ...sendOpts, index: false, fallthrough: true }));
|
|
17803
19739
|
app.get("{*path}", async (req, res) => {
|
|
17804
19740
|
if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
|
|
17805
19741
|
res.status(404).json({ error: "Not Found" });
|
|
17806
19742
|
return;
|
|
17807
19743
|
}
|
|
17808
|
-
const indexPath =
|
|
19744
|
+
const indexPath = resolve33(dashboardDistPath, "index.html");
|
|
17809
19745
|
if (!await fileExists(indexPath)) {
|
|
17810
19746
|
res.status(503).send(
|
|
17811
19747
|
'Dashboard not built. Run "npm run build:dashboard" first.'
|
|
@@ -17823,15 +19759,71 @@ function createDashboardServer(options) {
|
|
|
17823
19759
|
let watcherHandle = null;
|
|
17824
19760
|
return {
|
|
17825
19761
|
async start() {
|
|
19762
|
+
const { recomputeAndWrite: recomputeAndWrite2, recomputeAll: recomputeAll2, resolveDeriveContext: resolveDeriveContext2, isDeriveMigrated: isDeriveMigrated2 } = await Promise.resolve().then(() => (init_recompute(), recompute_exports));
|
|
19763
|
+
let warnedMigrationPending = false;
|
|
19764
|
+
const migrationGate = async () => {
|
|
19765
|
+
if (await isDeriveMigrated2()) return true;
|
|
19766
|
+
if (!warnedMigrationPending) {
|
|
19767
|
+
warnedMigrationPending = true;
|
|
19768
|
+
console.log(
|
|
19769
|
+
"derived-status: migration pending \u2014 implicit recomputes are dormant until `syntaur migrate-derive` runs."
|
|
19770
|
+
);
|
|
19771
|
+
}
|
|
19772
|
+
return false;
|
|
19773
|
+
};
|
|
19774
|
+
const recomputeOne = async (projectSlug, assignmentSlug) => {
|
|
19775
|
+
if (!await migrationGate()) return;
|
|
19776
|
+
try {
|
|
19777
|
+
const context = await resolveDeriveContext2();
|
|
19778
|
+
const projectDir = projectSlug ? resolve33(projectsDir, projectSlug) : null;
|
|
19779
|
+
const path = projectDir ? resolve33(projectDir, "assignments", assignmentSlug, "assignment.md") : resolve33(assignmentsDir2, assignmentSlug, "assignment.md");
|
|
19780
|
+
if (!await fileExists(path)) return;
|
|
19781
|
+
const result = await recomputeAndWrite2(path, {
|
|
19782
|
+
cause: "derive",
|
|
19783
|
+
by: "system",
|
|
19784
|
+
projectDir,
|
|
19785
|
+
context
|
|
19786
|
+
});
|
|
19787
|
+
if (result.warning) console.warn(result.warning);
|
|
19788
|
+
} catch (err) {
|
|
19789
|
+
console.error(`derive recompute failed for ${projectSlug ?? ""}/${assignmentSlug}:`, err);
|
|
19790
|
+
}
|
|
19791
|
+
};
|
|
19792
|
+
const sweepAll = async (cause) => {
|
|
19793
|
+
if (!await migrationGate()) return;
|
|
19794
|
+
try {
|
|
19795
|
+
const context = await resolveDeriveContext2();
|
|
19796
|
+
const summary = await recomputeAll2(projectsDir, assignmentsDir2, {
|
|
19797
|
+
cause,
|
|
19798
|
+
by: "system",
|
|
19799
|
+
context
|
|
19800
|
+
});
|
|
19801
|
+
if (summary.changed > 0) {
|
|
19802
|
+
console.log(`derive ${cause}: ${summary.changed}/${summary.scanned} assignment(s) re-derived.`);
|
|
19803
|
+
}
|
|
19804
|
+
for (const w of summary.warnings) console.warn(w);
|
|
19805
|
+
} catch (err) {
|
|
19806
|
+
console.error(`derive ${cause} sweep failed:`, err);
|
|
19807
|
+
}
|
|
19808
|
+
};
|
|
17826
19809
|
watcherHandle = createWatcher({
|
|
17827
19810
|
projectsDir,
|
|
17828
19811
|
assignmentsDir: assignmentsDir2,
|
|
17829
19812
|
serversDir: serversDir2,
|
|
17830
19813
|
playbooksDir: playbooksDir2,
|
|
17831
19814
|
todosDir: todosDir2,
|
|
17832
|
-
dbPath:
|
|
17833
|
-
|
|
19815
|
+
dbPath: resolve33(syntaurRoot(), "syntaur.db"),
|
|
19816
|
+
configPath: resolve33(syntaurRoot(), "config.md"),
|
|
19817
|
+
onMessage: broadcast,
|
|
19818
|
+
onAssignmentChanged: (projectSlug, assignmentSlug) => {
|
|
19819
|
+
void recomputeOne(projectSlug, assignmentSlug);
|
|
19820
|
+
},
|
|
19821
|
+
onConfigChanged: () => {
|
|
19822
|
+
clearStatusConfigCache();
|
|
19823
|
+
void sweepAll("config-change");
|
|
19824
|
+
}
|
|
17834
19825
|
});
|
|
19826
|
+
void sweepAll("boot-reconcile");
|
|
17835
19827
|
startAutodiscovery({ serversDir: serversDir2, projectsDir, assignmentsDir: assignmentsDir2, excludePids: /* @__PURE__ */ new Set([process.pid]) });
|
|
17836
19828
|
return new Promise((resolvePromise, reject) => {
|
|
17837
19829
|
server.on("error", (err) => {
|
|
@@ -17844,7 +19836,7 @@ function createDashboardServer(options) {
|
|
|
17844
19836
|
}
|
|
17845
19837
|
});
|
|
17846
19838
|
server.listen(port, () => {
|
|
17847
|
-
const portFile =
|
|
19839
|
+
const portFile = resolve33(syntaurRoot(), "dashboard-port");
|
|
17848
19840
|
writeFile8(portFile, String(port), "utf-8").catch(() => {
|
|
17849
19841
|
});
|
|
17850
19842
|
resolvePromise();
|
|
@@ -17863,8 +19855,8 @@ function createDashboardServer(options) {
|
|
|
17863
19855
|
client.terminate();
|
|
17864
19856
|
}
|
|
17865
19857
|
clients.clear();
|
|
17866
|
-
const portFile =
|
|
17867
|
-
await
|
|
19858
|
+
const portFile = resolve33(syntaurRoot(), "dashboard-port");
|
|
19859
|
+
await unlink8(portFile).catch(() => {
|
|
17868
19860
|
});
|
|
17869
19861
|
server.closeAllConnections?.();
|
|
17870
19862
|
return new Promise((resolvePromise) => {
|