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/launch/index.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
1
2
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
3
|
var __esm = (fn, res) => function __init() {
|
|
3
4
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
4
5
|
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
5
10
|
|
|
6
11
|
// src/utils/terminal-schema.ts
|
|
7
12
|
var TERMINAL_CHOICES;
|
|
@@ -76,6 +81,27 @@ var init_fs = __esm({
|
|
|
76
81
|
});
|
|
77
82
|
|
|
78
83
|
// src/templates/config.ts
|
|
84
|
+
function renderConfig(params) {
|
|
85
|
+
return `---
|
|
86
|
+
version: "1.0"
|
|
87
|
+
defaultProjectDir: ${params.defaultProjectDir}
|
|
88
|
+
onboarding:
|
|
89
|
+
completed: false
|
|
90
|
+
agentDefaults:
|
|
91
|
+
trustLevel: medium
|
|
92
|
+
autoApprove: false
|
|
93
|
+
backup:
|
|
94
|
+
repo: null
|
|
95
|
+
categories: projects, playbooks, todos, servers, config
|
|
96
|
+
lastBackup: null
|
|
97
|
+
lastRestore: null
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
# Syntaur Configuration
|
|
101
|
+
|
|
102
|
+
Global configuration for the Syntaur CLI.
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
79
105
|
var init_config = __esm({
|
|
80
106
|
"src/templates/config.ts"() {
|
|
81
107
|
"use strict";
|
|
@@ -464,6 +490,51 @@ function parseExternalIds(frontmatter) {
|
|
|
464
490
|
}
|
|
465
491
|
return results;
|
|
466
492
|
}
|
|
493
|
+
function parseStatusHistory(frontmatter) {
|
|
494
|
+
if (/^statusHistory:\s*\[\s*\]/m.test(frontmatter)) return [];
|
|
495
|
+
const headerMatch = frontmatter.match(/^statusHistory:\s*$/m);
|
|
496
|
+
if (!headerMatch) return [];
|
|
497
|
+
const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);
|
|
498
|
+
const bodyStart = headerStart + headerMatch[0].length + 1;
|
|
499
|
+
const after = frontmatter.slice(bodyStart);
|
|
500
|
+
const bodyLines = [];
|
|
501
|
+
for (const line of after.split("\n")) {
|
|
502
|
+
if (line.length === 0) {
|
|
503
|
+
bodyLines.push(line);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (line[0] !== " " && line[0] !== " ") break;
|
|
507
|
+
bodyLines.push(line);
|
|
508
|
+
}
|
|
509
|
+
const body = bodyLines.join("\n");
|
|
510
|
+
const results = [];
|
|
511
|
+
const itemBlocks = body.split(/\n\s+-\s+/).filter((b) => b.trim().length > 0);
|
|
512
|
+
for (const block of itemBlocks) {
|
|
513
|
+
const entry = {};
|
|
514
|
+
for (const line of block.split("\n")) {
|
|
515
|
+
const colonIdx = line.indexOf(":");
|
|
516
|
+
if (colonIdx < 0) continue;
|
|
517
|
+
const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
|
|
518
|
+
if (!key) continue;
|
|
519
|
+
entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
|
|
520
|
+
}
|
|
521
|
+
if (!entry["to"]) continue;
|
|
522
|
+
const result = {
|
|
523
|
+
at: entry["at"] ?? "",
|
|
524
|
+
from: entry["from"] ?? null,
|
|
525
|
+
to: entry["to"],
|
|
526
|
+
command: entry["command"] ?? "",
|
|
527
|
+
by: entry["by"] ?? null
|
|
528
|
+
};
|
|
529
|
+
if (entry["reason"] != null) result.reason = entry["reason"];
|
|
530
|
+
if ("phaseFrom" in entry) result.phaseFrom = entry["phaseFrom"];
|
|
531
|
+
if ("phaseTo" in entry) result.phaseTo = entry["phaseTo"];
|
|
532
|
+
if ("dispositionFrom" in entry) result.dispositionFrom = entry["dispositionFrom"];
|
|
533
|
+
if ("dispositionTo" in entry) result.dispositionTo = entry["dispositionTo"];
|
|
534
|
+
results.push(result);
|
|
535
|
+
}
|
|
536
|
+
return results;
|
|
537
|
+
}
|
|
467
538
|
function parseAssignmentFull(fileContent) {
|
|
468
539
|
const [fm, body] = extractFrontmatter(fileContent);
|
|
469
540
|
return {
|
|
@@ -486,13 +557,40 @@ function parseAssignmentFull(fileContent) {
|
|
|
486
557
|
parentBranch: getNestedField(fm, "workspace", "parentBranch")
|
|
487
558
|
},
|
|
488
559
|
externalIds: parseExternalIds(fm),
|
|
560
|
+
statusHistory: parseStatusHistory(fm),
|
|
489
561
|
tags: parseListField(fm, "tags"),
|
|
490
562
|
archived: getField(fm, "archived") === "true",
|
|
491
563
|
archivedAt: getField(fm, "archivedAt"),
|
|
492
564
|
archivedReason: getField(fm, "archivedReason"),
|
|
493
565
|
created: getField(fm, "created") ?? "",
|
|
494
566
|
updated: getField(fm, "updated") ?? "",
|
|
495
|
-
body
|
|
567
|
+
body,
|
|
568
|
+
phase: getField(fm, "phase"),
|
|
569
|
+
disposition: getField(fm, "disposition"),
|
|
570
|
+
parked: getField(fm, "parked") === "true",
|
|
571
|
+
reviewRequested: getField(fm, "reviewRequested") === "true",
|
|
572
|
+
implementationStarted: getField(fm, "implementationStarted") === "true",
|
|
573
|
+
planApproval: (() => {
|
|
574
|
+
const file = getNestedField(fm, "planApproval", "file");
|
|
575
|
+
const digest = getNestedField(fm, "planApproval", "digest");
|
|
576
|
+
if (!file || !digest) return null;
|
|
577
|
+
return {
|
|
578
|
+
file,
|
|
579
|
+
digest,
|
|
580
|
+
by: getNestedField(fm, "planApproval", "by"),
|
|
581
|
+
at: getNestedField(fm, "planApproval", "at") ?? ""
|
|
582
|
+
};
|
|
583
|
+
})(),
|
|
584
|
+
override: (() => {
|
|
585
|
+
const status = getNestedField(fm, "override", "status");
|
|
586
|
+
if (!status) return null;
|
|
587
|
+
return {
|
|
588
|
+
status,
|
|
589
|
+
source: getNestedField(fm, "override", "source") ?? "human",
|
|
590
|
+
reason: getNestedField(fm, "override", "reason"),
|
|
591
|
+
at: getNestedField(fm, "override", "at") ?? ""
|
|
592
|
+
};
|
|
593
|
+
})()
|
|
496
594
|
};
|
|
497
595
|
}
|
|
498
596
|
function parsePlan(fileContent) {
|
|
@@ -675,7 +773,12 @@ function canonicalizeCombo(input) {
|
|
|
675
773
|
}
|
|
676
774
|
return [...ordered, key].join("+");
|
|
677
775
|
}
|
|
678
|
-
|
|
776
|
+
function isReservedCombo(combo) {
|
|
777
|
+
const c = canonicalizeCombo(combo);
|
|
778
|
+
if (!c) return false;
|
|
779
|
+
return BUILTIN_RESERVED_COMBOS.includes(c);
|
|
780
|
+
}
|
|
781
|
+
var BINDABLE_ACTION_KINDS, BUILTIN_RESERVED_COMBOS, MODIFIER_ORDER, DEFAULT_BINDABLE_HOTKEYS;
|
|
679
782
|
var init_hotkeysCatalog = __esm({
|
|
680
783
|
"src/utils/hotkeysCatalog.ts"() {
|
|
681
784
|
"use strict";
|
|
@@ -685,6 +788,40 @@ var init_hotkeysCatalog = __esm({
|
|
|
685
788
|
"new-todo",
|
|
686
789
|
"new-assignment"
|
|
687
790
|
];
|
|
791
|
+
BUILTIN_RESERVED_COMBOS = [
|
|
792
|
+
"mod+k",
|
|
793
|
+
"mod+shift+k",
|
|
794
|
+
"?",
|
|
795
|
+
"escape",
|
|
796
|
+
"enter",
|
|
797
|
+
"shift+t",
|
|
798
|
+
// g-chord starter + suffixes
|
|
799
|
+
"g",
|
|
800
|
+
"g o",
|
|
801
|
+
"g m",
|
|
802
|
+
"g a",
|
|
803
|
+
"g t",
|
|
804
|
+
"g s",
|
|
805
|
+
"g !",
|
|
806
|
+
"g ,",
|
|
807
|
+
// list-scope navigation
|
|
808
|
+
"j",
|
|
809
|
+
"k",
|
|
810
|
+
"o",
|
|
811
|
+
// ProjectDetail page
|
|
812
|
+
"a",
|
|
813
|
+
"e",
|
|
814
|
+
// AssignmentsPage board
|
|
815
|
+
"/",
|
|
816
|
+
"r",
|
|
817
|
+
// AssignmentDetail page
|
|
818
|
+
"p",
|
|
819
|
+
"h",
|
|
820
|
+
"d",
|
|
821
|
+
"s",
|
|
822
|
+
"[",
|
|
823
|
+
"]"
|
|
824
|
+
];
|
|
688
825
|
MODIFIER_ORDER = ["mod", "ctrl", "alt", "shift"];
|
|
689
826
|
DEFAULT_BINDABLE_HOTKEYS = {
|
|
690
827
|
"new-workspace": canonicalizeCombo("Mod+Shift+Alt+w"),
|
|
@@ -808,6 +945,47 @@ var init_workspace_visibility_schema = __esm({
|
|
|
808
945
|
});
|
|
809
946
|
|
|
810
947
|
// src/utils/config.ts
|
|
948
|
+
var config_exports = {};
|
|
949
|
+
__export(config_exports, {
|
|
950
|
+
AGENT_ID_PATTERN: () => AGENT_ID_PATTERN,
|
|
951
|
+
AgentConfigError: () => AgentConfigError,
|
|
952
|
+
BUILTIN_AGENTS: () => BUILTIN_AGENTS,
|
|
953
|
+
DEFAULT_ASSIGNMENT_TYPES: () => DEFAULT_ASSIGNMENT_TYPES,
|
|
954
|
+
DEFAULT_DERIVE_CONFIG: () => DEFAULT_DERIVE_CONFIG,
|
|
955
|
+
DEFAULT_STATUS_COLORS: () => DEFAULT_STATUS_COLORS,
|
|
956
|
+
PROMPT_ARG_POSITIONS: () => PROMPT_ARG_POSITIONS,
|
|
957
|
+
TERMINAL_CHOICES: () => TERMINAL_CHOICES,
|
|
958
|
+
TerminalConfigError: () => TerminalConfigError,
|
|
959
|
+
buildDefaultStatusConfig: () => buildDefaultStatusConfig,
|
|
960
|
+
deleteAgentsConfig: () => deleteAgentsConfig,
|
|
961
|
+
deleteHotkeyBindingsConfig: () => deleteHotkeyBindingsConfig,
|
|
962
|
+
deleteStatusConfig: () => deleteStatusConfig,
|
|
963
|
+
deleteTerminalConfig: () => deleteTerminalConfig,
|
|
964
|
+
deleteThemeConfig: () => deleteThemeConfig,
|
|
965
|
+
deleteWorkspaceVisibilityConfig: () => deleteWorkspaceVisibilityConfig,
|
|
966
|
+
getAgents: () => getAgents,
|
|
967
|
+
getAssignmentTypes: () => getAssignmentTypes,
|
|
968
|
+
getTerminal: () => getTerminal,
|
|
969
|
+
parseAgentCommand: () => parseAgentCommand,
|
|
970
|
+
parseStatusConfig: () => parseStatusConfig,
|
|
971
|
+
parseTerminalConfig: () => parseTerminalConfig,
|
|
972
|
+
readConfig: () => readConfig,
|
|
973
|
+
serializeStatusConfig: () => serializeStatusConfig,
|
|
974
|
+
toTitleCase: () => toTitleCase,
|
|
975
|
+
updateAgentsConfig: () => updateAgentsConfig,
|
|
976
|
+
updateBackupConfig: () => updateBackupConfig,
|
|
977
|
+
updateIntegrationConfig: () => updateIntegrationConfig,
|
|
978
|
+
updateOnboardingConfig: () => updateOnboardingConfig,
|
|
979
|
+
updatePlaybooksConfig: () => updatePlaybooksConfig,
|
|
980
|
+
validateAgentList: () => validateAgentList,
|
|
981
|
+
validateDeriveConfig: () => validateDeriveConfig,
|
|
982
|
+
writeAgentsConfig: () => writeAgentsConfig,
|
|
983
|
+
writeHotkeyBindingsConfig: () => writeHotkeyBindingsConfig,
|
|
984
|
+
writeStatusConfig: () => writeStatusConfig,
|
|
985
|
+
writeTerminalConfig: () => writeTerminalConfig,
|
|
986
|
+
writeThemeConfig: () => writeThemeConfig,
|
|
987
|
+
writeWorkspaceVisibilityConfig: () => writeWorkspaceVisibilityConfig
|
|
988
|
+
});
|
|
811
989
|
import { readFile as readFile5 } from "fs/promises";
|
|
812
990
|
import { spawnSync } from "child_process";
|
|
813
991
|
import { resolve as resolve6, isAbsolute } from "path";
|
|
@@ -972,6 +1150,16 @@ function parseStatusConfig(content) {
|
|
|
972
1150
|
const statuses = [];
|
|
973
1151
|
const order = [];
|
|
974
1152
|
const transitions = [];
|
|
1153
|
+
const phaseLadder = [];
|
|
1154
|
+
const disposition = [];
|
|
1155
|
+
const headline = {};
|
|
1156
|
+
const unquote = (v) => {
|
|
1157
|
+
const t = v.trim();
|
|
1158
|
+
if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) {
|
|
1159
|
+
return t.slice(1, -1);
|
|
1160
|
+
}
|
|
1161
|
+
return t;
|
|
1162
|
+
};
|
|
975
1163
|
let currentSection = null;
|
|
976
1164
|
const lines = remaining.split("\n");
|
|
977
1165
|
function parseListEntry(lineIdx, baseIndent) {
|
|
@@ -1004,6 +1192,9 @@ function parseStatusConfig(content) {
|
|
|
1004
1192
|
if (key === "definitions") currentSection = "definitions";
|
|
1005
1193
|
else if (key === "order") currentSection = "order";
|
|
1006
1194
|
else if (key === "transitions") currentSection = "transitions";
|
|
1195
|
+
else if (key === "phaseLadder") currentSection = "phaseLadder";
|
|
1196
|
+
else if (key === "disposition") currentSection = "disposition";
|
|
1197
|
+
else if (key === "headline") currentSection = "headline";
|
|
1007
1198
|
else currentSection = null;
|
|
1008
1199
|
continue;
|
|
1009
1200
|
}
|
|
@@ -1042,12 +1233,52 @@ function parseStatusConfig(content) {
|
|
|
1042
1233
|
lineIdx += consumed - 1;
|
|
1043
1234
|
continue;
|
|
1044
1235
|
}
|
|
1236
|
+
if (currentSection === "phaseLadder" && indent >= 4 && trimmed.startsWith("- ")) {
|
|
1237
|
+
const { entry, consumed } = parseListEntry(lineIdx, indent);
|
|
1238
|
+
if (entry["phase"] && entry["when"] !== void 0) {
|
|
1239
|
+
phaseLadder.push({
|
|
1240
|
+
phase: unquote(entry["phase"]),
|
|
1241
|
+
when: unquote(entry["when"]),
|
|
1242
|
+
next: entry["next"] !== void 0 ? unquote(entry["next"]) : void 0
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
lineIdx += consumed - 1;
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
if (currentSection === "disposition" && indent >= 4 && trimmed.startsWith("- ")) {
|
|
1249
|
+
const { entry, consumed } = parseListEntry(lineIdx, indent);
|
|
1250
|
+
if (entry["else"] !== void 0) {
|
|
1251
|
+
disposition.push({ when: null, is: unquote(entry["else"]) });
|
|
1252
|
+
} else if (entry["when"] !== void 0 && entry["is"]) {
|
|
1253
|
+
disposition.push({ when: unquote(entry["when"]), is: unquote(entry["is"]) });
|
|
1254
|
+
}
|
|
1255
|
+
lineIdx += consumed - 1;
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
if (currentSection === "headline" && indent >= 4 && !trimmed.startsWith("- ")) {
|
|
1259
|
+
const ci = trimmed.indexOf(":");
|
|
1260
|
+
if (ci > 0) {
|
|
1261
|
+
headline[trimmed.slice(0, ci).trim()] = unquote(trimmed.slice(ci + 1));
|
|
1262
|
+
}
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1045
1265
|
}
|
|
1046
1266
|
if (statuses.length === 0) return null;
|
|
1267
|
+
const derive = phaseLadder.length > 0 || disposition.length > 0 || Object.keys(headline).length > 0 ? {
|
|
1268
|
+
phaseLadder: phaseLadder.length > 0 ? phaseLadder : DEFAULT_DERIVE_CONFIG.phaseLadder,
|
|
1269
|
+
disposition: disposition.length > 0 ? disposition : DEFAULT_DERIVE_CONFIG.disposition,
|
|
1270
|
+
headline: {
|
|
1271
|
+
terminal: "passthrough",
|
|
1272
|
+
parked: headline["parked"] ?? DEFAULT_DERIVE_CONFIG.headline.parked,
|
|
1273
|
+
blocked: headline["blocked"] ?? DEFAULT_DERIVE_CONFIG.headline.blocked,
|
|
1274
|
+
active: "phase"
|
|
1275
|
+
}
|
|
1276
|
+
} : null;
|
|
1047
1277
|
return {
|
|
1048
1278
|
statuses,
|
|
1049
1279
|
order: order.length > 0 ? order : statuses.map((s) => s.id),
|
|
1050
|
-
transitions
|
|
1280
|
+
transitions,
|
|
1281
|
+
derive
|
|
1051
1282
|
};
|
|
1052
1283
|
}
|
|
1053
1284
|
function toTitleCase(s) {
|
|
@@ -1068,6 +1299,137 @@ function buildDefaultStatusConfig() {
|
|
|
1068
1299
|
})
|
|
1069
1300
|
};
|
|
1070
1301
|
}
|
|
1302
|
+
function serializeStatusConfig(statuses) {
|
|
1303
|
+
const lines = [];
|
|
1304
|
+
lines.push("statuses:");
|
|
1305
|
+
lines.push(" definitions:");
|
|
1306
|
+
for (const s of statuses.statuses) {
|
|
1307
|
+
lines.push(` - id: ${s.id}`);
|
|
1308
|
+
lines.push(` label: ${s.label}`);
|
|
1309
|
+
if (s.description) lines.push(` description: ${s.description}`);
|
|
1310
|
+
if (s.color) lines.push(` color: ${s.color}`);
|
|
1311
|
+
if (s.icon) lines.push(` icon: ${s.icon}`);
|
|
1312
|
+
if (s.terminal) lines.push(` terminal: true`);
|
|
1313
|
+
}
|
|
1314
|
+
lines.push(" order:");
|
|
1315
|
+
for (const id of statuses.order) {
|
|
1316
|
+
lines.push(` - ${id}`);
|
|
1317
|
+
}
|
|
1318
|
+
if (statuses.transitions.length > 0) {
|
|
1319
|
+
lines.push(" transitions:");
|
|
1320
|
+
for (const t of statuses.transitions) {
|
|
1321
|
+
lines.push(` - from: ${t.from}`);
|
|
1322
|
+
lines.push(` command: ${t.command}`);
|
|
1323
|
+
lines.push(` to: ${t.to}`);
|
|
1324
|
+
if (t.label) lines.push(` label: ${t.label}`);
|
|
1325
|
+
if (t.description) lines.push(` description: ${t.description}`);
|
|
1326
|
+
if (t.requiresReason) lines.push(` requiresReason: true`);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
if (statuses.derive) {
|
|
1330
|
+
const d = statuses.derive;
|
|
1331
|
+
lines.push(" phaseLadder:");
|
|
1332
|
+
for (const rung of d.phaseLadder) {
|
|
1333
|
+
lines.push(` - phase: ${rung.phase}`);
|
|
1334
|
+
lines.push(` when: "${rung.when.replace(/"/g, '\\"')}"`);
|
|
1335
|
+
if (rung.next) lines.push(` next: "${rung.next.replace(/"/g, '\\"')}"`);
|
|
1336
|
+
}
|
|
1337
|
+
lines.push(" disposition:");
|
|
1338
|
+
for (const rule of d.disposition) {
|
|
1339
|
+
if (rule.when === null) {
|
|
1340
|
+
lines.push(` - else: ${rule.is}`);
|
|
1341
|
+
} else {
|
|
1342
|
+
lines.push(` - when: "${rule.when.replace(/"/g, '\\"')}"`);
|
|
1343
|
+
lines.push(` is: ${rule.is}`);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
lines.push(" headline:");
|
|
1347
|
+
lines.push(` terminal: passthrough`);
|
|
1348
|
+
lines.push(` parked: ${d.headline.parked}`);
|
|
1349
|
+
lines.push(` blocked: ${d.headline.blocked}`);
|
|
1350
|
+
lines.push(` active: phase`);
|
|
1351
|
+
}
|
|
1352
|
+
return lines.join("\n");
|
|
1353
|
+
}
|
|
1354
|
+
function validateDeriveConfig(derive, statusConfig, validateWhen = () => null) {
|
|
1355
|
+
const problems = [];
|
|
1356
|
+
const ids = new Set(statusConfig.statuses.map((s) => s.id));
|
|
1357
|
+
if (derive.phaseLadder.length === 0) {
|
|
1358
|
+
problems.push("phaseLadder must have at least one rung");
|
|
1359
|
+
}
|
|
1360
|
+
for (const rung of derive.phaseLadder) {
|
|
1361
|
+
if (!ids.has(rung.phase)) {
|
|
1362
|
+
problems.push(`phaseLadder rung "${rung.phase}" is not a defined status id`);
|
|
1363
|
+
}
|
|
1364
|
+
const err = rung.when === "*" ? null : validateWhen(rung.when);
|
|
1365
|
+
if (err) problems.push(`phaseLadder rung "${rung.phase}": invalid condition \u2014 ${err}`);
|
|
1366
|
+
}
|
|
1367
|
+
const VALID_DISPOSITIONS = /* @__PURE__ */ new Set(["active", "blocked", "parked"]);
|
|
1368
|
+
let sawElse = false;
|
|
1369
|
+
for (const rule of derive.disposition) {
|
|
1370
|
+
if (!VALID_DISPOSITIONS.has(rule.is)) {
|
|
1371
|
+
problems.push(
|
|
1372
|
+
`disposition "${rule.is}" is not valid (expected active, blocked, or parked \u2014 terminal is never a rule)`
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
if (rule.when === null) sawElse = true;
|
|
1376
|
+
else {
|
|
1377
|
+
const err = validateWhen(rule.when);
|
|
1378
|
+
if (err) problems.push(`disposition rule "${rule.is}": invalid condition \u2014 ${err}`);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
if (!sawElse) problems.push("disposition rules must end with an `else:` arm");
|
|
1382
|
+
for (const key of ["parked", "blocked"]) {
|
|
1383
|
+
if (!ids.has(derive.headline[key])) {
|
|
1384
|
+
problems.push(
|
|
1385
|
+
`headline.${key} \u2192 "${derive.headline[key]}" is not a defined status id (add the definition or run migrate-derive)`
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return problems;
|
|
1390
|
+
}
|
|
1391
|
+
function serializeIntegrationConfig(integrations) {
|
|
1392
|
+
const lines = [];
|
|
1393
|
+
if (integrations.claudePluginDir) {
|
|
1394
|
+
lines.push(` claudePluginDir: ${integrations.claudePluginDir}`);
|
|
1395
|
+
}
|
|
1396
|
+
if (integrations.codexPluginDir) {
|
|
1397
|
+
lines.push(` codexPluginDir: ${integrations.codexPluginDir}`);
|
|
1398
|
+
}
|
|
1399
|
+
if (integrations.codexMarketplacePath) {
|
|
1400
|
+
lines.push(` codexMarketplacePath: ${integrations.codexMarketplacePath}`);
|
|
1401
|
+
}
|
|
1402
|
+
if (integrations.installedAgents) {
|
|
1403
|
+
for (const [id, rec] of Object.entries(integrations.installedAgents)) {
|
|
1404
|
+
lines.push(` installedAgents.${id}: ${rec.scope}`);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
if (lines.length === 0) {
|
|
1408
|
+
return null;
|
|
1409
|
+
}
|
|
1410
|
+
return ["integrations:", ...lines].join("\n");
|
|
1411
|
+
}
|
|
1412
|
+
function serializeOnboardingConfig(onboarding) {
|
|
1413
|
+
return ["onboarding:", ` completed: ${onboarding.completed ? "true" : "false"}`].join("\n");
|
|
1414
|
+
}
|
|
1415
|
+
function serializeBackupConfig(backup) {
|
|
1416
|
+
const lines = ["backup:"];
|
|
1417
|
+
lines.push(` repo: ${backup.repo ?? "null"}`);
|
|
1418
|
+
lines.push(` categories: ${backup.categories}`);
|
|
1419
|
+
lines.push(` lastBackup: ${backup.lastBackup ?? "null"}`);
|
|
1420
|
+
lines.push(` lastRestore: ${backup.lastRestore ?? "null"}`);
|
|
1421
|
+
return lines.join("\n");
|
|
1422
|
+
}
|
|
1423
|
+
function serializePlaybooksConfig(playbooks) {
|
|
1424
|
+
if (!playbooks.disabled || playbooks.disabled.length === 0) {
|
|
1425
|
+
return null;
|
|
1426
|
+
}
|
|
1427
|
+
const lines = ["playbooks:", " disabled:"];
|
|
1428
|
+
for (const slug of playbooks.disabled) {
|
|
1429
|
+
lines.push(` - ${slug}`);
|
|
1430
|
+
}
|
|
1431
|
+
return lines.join("\n");
|
|
1432
|
+
}
|
|
1071
1433
|
function parsePlaybooksConfig(fmBlock) {
|
|
1072
1434
|
const blockStart = fmBlock.match(/^playbooks:\s*$/m);
|
|
1073
1435
|
if (!blockStart) {
|
|
@@ -1103,6 +1465,37 @@ function parsePlaybooksConfig(fmBlock) {
|
|
|
1103
1465
|
}
|
|
1104
1466
|
return { disabled };
|
|
1105
1467
|
}
|
|
1468
|
+
async function updatePlaybooksConfig(playbooks) {
|
|
1469
|
+
const configPath = resolve6(syntaurRoot(), "config.md");
|
|
1470
|
+
const current = (await readConfig()).playbooks;
|
|
1471
|
+
const nextPlaybooks = {
|
|
1472
|
+
disabled: Array.from(new Set(playbooks.disabled ?? current.disabled))
|
|
1473
|
+
};
|
|
1474
|
+
const playbooksBlock = serializePlaybooksConfig(nextPlaybooks);
|
|
1475
|
+
const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
1476
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
1477
|
+
if (!fmMatch) {
|
|
1478
|
+
const bodyBlock = playbooksBlock ? `${playbooksBlock}
|
|
1479
|
+
` : "";
|
|
1480
|
+
const content = `---
|
|
1481
|
+
version: "2.0"
|
|
1482
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
1483
|
+
${bodyBlock}---
|
|
1484
|
+
${existing}`;
|
|
1485
|
+
await writeFileForce(configPath, content);
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
const fmBlock = fmMatch[2];
|
|
1489
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
1490
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "playbooks");
|
|
1491
|
+
const newFm = playbooksBlock ? `${cleanedFm}
|
|
1492
|
+
${playbooksBlock}`.replace(/^\n+/, "") : cleanedFm;
|
|
1493
|
+
const normalizedFm = newFm.replace(/\n+$/, "");
|
|
1494
|
+
const newContent = `---
|
|
1495
|
+
${normalizedFm}
|
|
1496
|
+
---${afterFrontmatter}`;
|
|
1497
|
+
await writeFileForce(configPath, newContent);
|
|
1498
|
+
}
|
|
1106
1499
|
function parseThemeConfig(content) {
|
|
1107
1500
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1108
1501
|
if (!match) return null;
|
|
@@ -1125,6 +1518,58 @@ function parseThemeConfig(content) {
|
|
|
1125
1518
|
if (!preset) return null;
|
|
1126
1519
|
return { preset };
|
|
1127
1520
|
}
|
|
1521
|
+
function serializeThemeConfig(theme) {
|
|
1522
|
+
return ["theme:", ` preset: ${theme.preset}`].join("\n");
|
|
1523
|
+
}
|
|
1524
|
+
async function writeThemeConfig(theme) {
|
|
1525
|
+
const configPath = resolve6(syntaurRoot(), "config.md");
|
|
1526
|
+
const themeBlock = serializeThemeConfig(theme);
|
|
1527
|
+
const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
1528
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
1529
|
+
if (!fmMatch) {
|
|
1530
|
+
const content = `---
|
|
1531
|
+
version: "2.0"
|
|
1532
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
1533
|
+
${themeBlock}
|
|
1534
|
+
---
|
|
1535
|
+
${existing}`;
|
|
1536
|
+
await writeFileForce(configPath, content);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
const fmBlock = fmMatch[2];
|
|
1540
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
1541
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "theme");
|
|
1542
|
+
const newFm = `${cleanedFm}
|
|
1543
|
+
${themeBlock}`.replace(/^\n+/, "");
|
|
1544
|
+
const normalizedFm = newFm.replace(/\n+$/, "");
|
|
1545
|
+
const newContent = `---
|
|
1546
|
+
${normalizedFm}
|
|
1547
|
+
---${afterFrontmatter}`;
|
|
1548
|
+
await writeFileForce(configPath, newContent);
|
|
1549
|
+
}
|
|
1550
|
+
async function deleteThemeConfig() {
|
|
1551
|
+
const configPath = resolve6(syntaurRoot(), "config.md");
|
|
1552
|
+
if (!await fileExists(configPath)) return;
|
|
1553
|
+
const existing = await readFile5(configPath, "utf-8");
|
|
1554
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
1555
|
+
if (!fmMatch) return;
|
|
1556
|
+
const fmBlock = fmMatch[2];
|
|
1557
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
1558
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "theme");
|
|
1559
|
+
const newContent = `---
|
|
1560
|
+
${cleanedFm}
|
|
1561
|
+
---${afterFrontmatter}`;
|
|
1562
|
+
await writeFileForce(configPath, newContent);
|
|
1563
|
+
}
|
|
1564
|
+
function serializeWorkspaceVisibilityConfig(cfg) {
|
|
1565
|
+
const hidden = normalizeHiddenList(cfg.hidden);
|
|
1566
|
+
if (hidden.length === 0) return null;
|
|
1567
|
+
const lines = ["workspaceVisibility:", " hidden:"];
|
|
1568
|
+
for (const name of hidden) {
|
|
1569
|
+
lines.push(` - ${JSON.stringify(name)}`);
|
|
1570
|
+
}
|
|
1571
|
+
return lines.join("\n");
|
|
1572
|
+
}
|
|
1128
1573
|
function parseWorkspaceVisibilityConfig(fmBlock) {
|
|
1129
1574
|
const blockStart = fmBlock.match(/^workspaceVisibility:\s*$/m);
|
|
1130
1575
|
if (!blockStart) {
|
|
@@ -1162,6 +1607,93 @@ function parseWorkspaceVisibilityConfig(fmBlock) {
|
|
|
1162
1607
|
}
|
|
1163
1608
|
return { hidden: normalizeHiddenList(hidden) };
|
|
1164
1609
|
}
|
|
1610
|
+
async function writeWorkspaceVisibilityConfig(cfg) {
|
|
1611
|
+
const configPath = resolve6(syntaurRoot(), "config.md");
|
|
1612
|
+
const block = serializeWorkspaceVisibilityConfig(cfg);
|
|
1613
|
+
const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
1614
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
1615
|
+
if (!fmMatch) {
|
|
1616
|
+
const bodyBlock = block ? `${block}
|
|
1617
|
+
` : "";
|
|
1618
|
+
const content = `---
|
|
1619
|
+
version: "2.0"
|
|
1620
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
1621
|
+
${bodyBlock}---
|
|
1622
|
+
${existing}`;
|
|
1623
|
+
await writeFileForce(configPath, content);
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
const fmBlock = fmMatch[2];
|
|
1627
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
1628
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "workspaceVisibility");
|
|
1629
|
+
const newFm = block ? `${cleanedFm}
|
|
1630
|
+
${block}`.replace(/^\n+/, "") : cleanedFm;
|
|
1631
|
+
const normalizedFm = newFm.replace(/\n+$/, "");
|
|
1632
|
+
const newContent = `---
|
|
1633
|
+
${normalizedFm}
|
|
1634
|
+
---${afterFrontmatter}`;
|
|
1635
|
+
await writeFileForce(configPath, newContent);
|
|
1636
|
+
}
|
|
1637
|
+
async function deleteWorkspaceVisibilityConfig() {
|
|
1638
|
+
const configPath = resolve6(syntaurRoot(), "config.md");
|
|
1639
|
+
if (!await fileExists(configPath)) return;
|
|
1640
|
+
const existing = await readFile5(configPath, "utf-8");
|
|
1641
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
1642
|
+
if (!fmMatch) return;
|
|
1643
|
+
const fmBlock = fmMatch[2];
|
|
1644
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
1645
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "workspaceVisibility");
|
|
1646
|
+
const newContent = `---
|
|
1647
|
+
${cleanedFm}
|
|
1648
|
+
---${afterFrontmatter}`;
|
|
1649
|
+
await writeFileForce(configPath, newContent);
|
|
1650
|
+
}
|
|
1651
|
+
function stripTopLevelScalar(fmBlock, key) {
|
|
1652
|
+
const lines = fmBlock.split("\n");
|
|
1653
|
+
const keyRegex = new RegExp(`^${key}:\\s*\\S`);
|
|
1654
|
+
const filtered = lines.filter((line) => !keyRegex.test(line));
|
|
1655
|
+
return filtered.join("\n").replace(/\n+$/, "");
|
|
1656
|
+
}
|
|
1657
|
+
async function writeTerminalConfig(terminal) {
|
|
1658
|
+
const configPath = resolve6(syntaurRoot(), "config.md");
|
|
1659
|
+
const terminalLine = `terminal: ${terminal}`;
|
|
1660
|
+
const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
1661
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
1662
|
+
if (!fmMatch) {
|
|
1663
|
+
const content = `---
|
|
1664
|
+
version: "2.0"
|
|
1665
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
1666
|
+
${terminalLine}
|
|
1667
|
+
---
|
|
1668
|
+
${existing}`;
|
|
1669
|
+
await writeFileForce(configPath, content);
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
const fmBlock = fmMatch[2];
|
|
1673
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
1674
|
+
const cleanedFm = stripTopLevelScalar(fmBlock, "terminal");
|
|
1675
|
+
const newFm = `${cleanedFm}
|
|
1676
|
+
${terminalLine}`.replace(/^\n+/, "");
|
|
1677
|
+
const normalizedFm = newFm.replace(/\n+$/, "");
|
|
1678
|
+
const newContent = `---
|
|
1679
|
+
${normalizedFm}
|
|
1680
|
+
---${afterFrontmatter}`;
|
|
1681
|
+
await writeFileForce(configPath, newContent);
|
|
1682
|
+
}
|
|
1683
|
+
async function deleteTerminalConfig() {
|
|
1684
|
+
const configPath = resolve6(syntaurRoot(), "config.md");
|
|
1685
|
+
if (!await fileExists(configPath)) return;
|
|
1686
|
+
const existing = await readFile5(configPath, "utf-8");
|
|
1687
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
1688
|
+
if (!fmMatch) return;
|
|
1689
|
+
const fmBlock = fmMatch[2];
|
|
1690
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
1691
|
+
const cleanedFm = stripTopLevelScalar(fmBlock, "terminal");
|
|
1692
|
+
const newContent = `---
|
|
1693
|
+
${cleanedFm}
|
|
1694
|
+
---${afterFrontmatter}`;
|
|
1695
|
+
await writeFileForce(configPath, newContent);
|
|
1696
|
+
}
|
|
1165
1697
|
function parseHotkeyBindingsConfig(content) {
|
|
1166
1698
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1167
1699
|
if (!match) return null;
|
|
@@ -1194,6 +1726,92 @@ function parseHotkeyBindingsConfig(content) {
|
|
|
1194
1726
|
if (Object.keys(bindings).length === 0) return null;
|
|
1195
1727
|
return { bindings };
|
|
1196
1728
|
}
|
|
1729
|
+
function serializeHotkeyBindingsConfig(cfg) {
|
|
1730
|
+
const lines = ["hotkeys:", " bindings:"];
|
|
1731
|
+
for (const kind of BINDABLE_ACTION_KINDS) {
|
|
1732
|
+
const value = cfg.bindings[kind];
|
|
1733
|
+
if (!value) continue;
|
|
1734
|
+
lines.push(` ${kind}: "${canonicalizeCombo(value)}"`);
|
|
1735
|
+
}
|
|
1736
|
+
if (lines.length === 2) return "";
|
|
1737
|
+
return lines.join("\n");
|
|
1738
|
+
}
|
|
1739
|
+
async function writeHotkeyBindingsConfig(cfg) {
|
|
1740
|
+
const cleaned = {};
|
|
1741
|
+
for (const kind of BINDABLE_ACTION_KINDS) {
|
|
1742
|
+
const raw = cfg.bindings[kind];
|
|
1743
|
+
if (typeof raw !== "string" || raw.trim() === "") continue;
|
|
1744
|
+
const canonical = canonicalizeCombo(raw);
|
|
1745
|
+
if (!canonical) continue;
|
|
1746
|
+
if (isReservedCombo(canonical)) continue;
|
|
1747
|
+
cleaned[kind] = canonical;
|
|
1748
|
+
}
|
|
1749
|
+
if (Object.keys(cleaned).length === 0) {
|
|
1750
|
+
await deleteHotkeyBindingsConfig();
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
const configPath = resolve6(syntaurRoot(), "config.md");
|
|
1754
|
+
const block = serializeHotkeyBindingsConfig({ bindings: cleaned });
|
|
1755
|
+
const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
1756
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
1757
|
+
if (!fmMatch) {
|
|
1758
|
+
const content = `---
|
|
1759
|
+
version: "2.0"
|
|
1760
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
1761
|
+
${block}
|
|
1762
|
+
---
|
|
1763
|
+
${existing}`;
|
|
1764
|
+
await writeFileForce(configPath, content);
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
const fmBlock = fmMatch[2];
|
|
1768
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
1769
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "hotkeys");
|
|
1770
|
+
const newFm = `${cleanedFm}
|
|
1771
|
+
${block}`.replace(/^\n+/, "");
|
|
1772
|
+
const normalizedFm = newFm.replace(/\n+$/, "");
|
|
1773
|
+
const newContent = `---
|
|
1774
|
+
${normalizedFm}
|
|
1775
|
+
---${afterFrontmatter}`;
|
|
1776
|
+
await writeFileForce(configPath, newContent);
|
|
1777
|
+
}
|
|
1778
|
+
async function deleteHotkeyBindingsConfig() {
|
|
1779
|
+
const configPath = resolve6(syntaurRoot(), "config.md");
|
|
1780
|
+
if (!await fileExists(configPath)) return;
|
|
1781
|
+
const existing = await readFile5(configPath, "utf-8");
|
|
1782
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
1783
|
+
if (!fmMatch) return;
|
|
1784
|
+
const fmBlock = fmMatch[2];
|
|
1785
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
1786
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "hotkeys");
|
|
1787
|
+
const newContent = `---
|
|
1788
|
+
${cleanedFm}
|
|
1789
|
+
---${afterFrontmatter}`;
|
|
1790
|
+
await writeFileForce(configPath, newContent);
|
|
1791
|
+
}
|
|
1792
|
+
function stripTopLevelBlock(fmBlock, key) {
|
|
1793
|
+
const blockStart = fmBlock.match(new RegExp(`^${key}:\\s*$`, "m"));
|
|
1794
|
+
if (!blockStart) {
|
|
1795
|
+
return fmBlock.replace(/\n+$/, "");
|
|
1796
|
+
}
|
|
1797
|
+
const startIdx = fmBlock.indexOf(blockStart[0]);
|
|
1798
|
+
const before = fmBlock.slice(0, startIdx);
|
|
1799
|
+
const after = fmBlock.slice(startIdx + blockStart[0].length);
|
|
1800
|
+
const remaining = after.split("\n");
|
|
1801
|
+
let endIdx = 0;
|
|
1802
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
1803
|
+
const line = remaining[i];
|
|
1804
|
+
if (line.trim() === "") {
|
|
1805
|
+
endIdx = i + 1;
|
|
1806
|
+
continue;
|
|
1807
|
+
}
|
|
1808
|
+
if (line.length > 0 && line[0] !== " ") {
|
|
1809
|
+
break;
|
|
1810
|
+
}
|
|
1811
|
+
endIdx = i + 1;
|
|
1812
|
+
}
|
|
1813
|
+
return (before + remaining.slice(endIdx).join("\n")).replace(/\n+$/, "");
|
|
1814
|
+
}
|
|
1197
1815
|
function parseOptionalAbsolutePath(value, fieldName) {
|
|
1198
1816
|
if (!value) {
|
|
1199
1817
|
return null;
|
|
@@ -1429,6 +2047,266 @@ function assignAgentField(target, key, rawValue) {
|
|
|
1429
2047
|
break;
|
|
1430
2048
|
}
|
|
1431
2049
|
}
|
|
2050
|
+
function yamlQuoteScalar(value) {
|
|
2051
|
+
if (/[\r\n]/.test(value)) {
|
|
2052
|
+
throw new AgentConfigError(
|
|
2053
|
+
`value contains newlines, which the agents config serializer does not support: ${JSON.stringify(value)}`
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
if (value === "" || /[:#{}[\],&*?|>!%@`"'\\\t]/.test(value) || /^\s|\s$/.test(value)) {
|
|
2057
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\t/g, "\\t");
|
|
2058
|
+
return `"${escaped}"`;
|
|
2059
|
+
}
|
|
2060
|
+
return value;
|
|
2061
|
+
}
|
|
2062
|
+
function serializeAgentsConfig(agents) {
|
|
2063
|
+
const lines = ["agents:"];
|
|
2064
|
+
for (const a of agents) {
|
|
2065
|
+
lines.push(` - id: ${yamlQuoteScalar(a.id)}`);
|
|
2066
|
+
lines.push(` label: ${yamlQuoteScalar(a.label)}`);
|
|
2067
|
+
lines.push(` command: ${yamlQuoteScalar(a.command)}`);
|
|
2068
|
+
if (a.model) {
|
|
2069
|
+
lines.push(` model: ${yamlQuoteScalar(a.model)}`);
|
|
2070
|
+
}
|
|
2071
|
+
if (a.playbook) {
|
|
2072
|
+
lines.push(` playbook: ${yamlQuoteScalar(a.playbook)}`);
|
|
2073
|
+
}
|
|
2074
|
+
if (a.args && a.args.length > 0) {
|
|
2075
|
+
lines.push(` args:`);
|
|
2076
|
+
for (const arg of a.args) {
|
|
2077
|
+
lines.push(` - ${yamlQuoteScalar(arg)}`);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
if (a.promptArgPosition && a.promptArgPosition !== "first") {
|
|
2081
|
+
lines.push(` promptArgPosition: ${a.promptArgPosition}`);
|
|
2082
|
+
}
|
|
2083
|
+
if (a.default) {
|
|
2084
|
+
lines.push(` default: true`);
|
|
2085
|
+
}
|
|
2086
|
+
if (a.resolveFromShellAliases) {
|
|
2087
|
+
lines.push(` resolveFromShellAliases: true`);
|
|
2088
|
+
}
|
|
2089
|
+
if (a.resume) {
|
|
2090
|
+
appendSessionInvocation(lines, "resume", a.resume);
|
|
2091
|
+
}
|
|
2092
|
+
if (a.fork) {
|
|
2093
|
+
appendSessionInvocation(lines, "fork", a.fork);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
return lines.join("\n");
|
|
2097
|
+
}
|
|
2098
|
+
function appendSessionInvocation(lines, key, invocation) {
|
|
2099
|
+
lines.push(` ${key}:`);
|
|
2100
|
+
if (invocation.command !== void 0) {
|
|
2101
|
+
lines.push(` command: ${yamlQuoteScalar(invocation.command)}`);
|
|
2102
|
+
}
|
|
2103
|
+
lines.push(` args:`);
|
|
2104
|
+
for (const arg of invocation.args) {
|
|
2105
|
+
lines.push(` - ${yamlQuoteScalar(arg)}`);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
async function writeAgentsConfig(agents) {
|
|
2109
|
+
validateAgentList(agents);
|
|
2110
|
+
const configPath = resolve6(syntaurRoot(), "config.md");
|
|
2111
|
+
const agentsBlock = serializeAgentsConfig(agents);
|
|
2112
|
+
const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
2113
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
2114
|
+
if (!fmMatch) {
|
|
2115
|
+
const content = `---
|
|
2116
|
+
version: "2.0"
|
|
2117
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
2118
|
+
${agentsBlock}
|
|
2119
|
+
---
|
|
2120
|
+
${existing}`;
|
|
2121
|
+
await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
const fmBlock = fmMatch[2];
|
|
2125
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
2126
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "agents");
|
|
2127
|
+
const newFm = `${cleanedFm}
|
|
2128
|
+
${agentsBlock}`.replace(/^\n+/, "").replace(/\n+$/, "");
|
|
2129
|
+
const newContent = `---
|
|
2130
|
+
${newFm}
|
|
2131
|
+
---${afterFrontmatter}`;
|
|
2132
|
+
await writeFileForce(configPath, newContent);
|
|
2133
|
+
}
|
|
2134
|
+
async function deleteAgentsConfig() {
|
|
2135
|
+
const configPath = resolve6(syntaurRoot(), "config.md");
|
|
2136
|
+
if (!await fileExists(configPath)) return;
|
|
2137
|
+
const existing = await readFile5(configPath, "utf-8");
|
|
2138
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
2139
|
+
if (!fmMatch) return;
|
|
2140
|
+
const fmBlock = fmMatch[2];
|
|
2141
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
2142
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "agents");
|
|
2143
|
+
const newContent = `---
|
|
2144
|
+
${cleanedFm}
|
|
2145
|
+
---${afterFrontmatter}`;
|
|
2146
|
+
await writeFileForce(configPath, newContent);
|
|
2147
|
+
}
|
|
2148
|
+
async function writeStatusConfig(statuses) {
|
|
2149
|
+
const configPath = resolve6(syntaurRoot(), "config.md");
|
|
2150
|
+
const statusBlock = serializeStatusConfig(statuses);
|
|
2151
|
+
if (!await fileExists(configPath)) {
|
|
2152
|
+
const content = `---
|
|
2153
|
+
version: "2.0"
|
|
2154
|
+
defaultProjectDir: ~/projects
|
|
2155
|
+
${statusBlock}
|
|
2156
|
+
---
|
|
2157
|
+
`;
|
|
2158
|
+
await writeFileForce(configPath, content);
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
const existing = await readFile5(configPath, "utf-8");
|
|
2162
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
2163
|
+
if (!fmMatch) {
|
|
2164
|
+
const content = `---
|
|
2165
|
+
version: "2.0"
|
|
2166
|
+
${statusBlock}
|
|
2167
|
+
---
|
|
2168
|
+
${existing}`;
|
|
2169
|
+
await writeFileForce(configPath, content);
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
const fmBlock = fmMatch[2];
|
|
2173
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
2174
|
+
const statusesStart = fmBlock.match(/^statuses:\s*$/m);
|
|
2175
|
+
let cleanedFm;
|
|
2176
|
+
if (statusesStart) {
|
|
2177
|
+
const startIdx = fmBlock.indexOf(statusesStart[0]);
|
|
2178
|
+
const before = fmBlock.slice(0, startIdx);
|
|
2179
|
+
const after = fmBlock.slice(startIdx + statusesStart[0].length);
|
|
2180
|
+
const remaining = after.split("\n");
|
|
2181
|
+
let endIdx = 0;
|
|
2182
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
2183
|
+
const line = remaining[i];
|
|
2184
|
+
if (line.trim() === "") {
|
|
2185
|
+
endIdx = i + 1;
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2188
|
+
if (line.length > 0 && line[0] !== " ") break;
|
|
2189
|
+
endIdx = i + 1;
|
|
2190
|
+
}
|
|
2191
|
+
cleanedFm = before + remaining.slice(endIdx).join("\n");
|
|
2192
|
+
} else {
|
|
2193
|
+
cleanedFm = fmBlock;
|
|
2194
|
+
}
|
|
2195
|
+
cleanedFm = cleanedFm.replace(/\n+$/, "");
|
|
2196
|
+
const newContent = `---
|
|
2197
|
+
${cleanedFm}
|
|
2198
|
+
${statusBlock}
|
|
2199
|
+
---${afterFrontmatter}`;
|
|
2200
|
+
await writeFileForce(configPath, newContent);
|
|
2201
|
+
}
|
|
2202
|
+
async function deleteStatusConfig() {
|
|
2203
|
+
const configPath = resolve6(syntaurRoot(), "config.md");
|
|
2204
|
+
if (!await fileExists(configPath)) return;
|
|
2205
|
+
const existing = await readFile5(configPath, "utf-8");
|
|
2206
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
2207
|
+
if (!fmMatch) return;
|
|
2208
|
+
const fmBlock = fmMatch[2];
|
|
2209
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
2210
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "statuses");
|
|
2211
|
+
const newContent = `---
|
|
2212
|
+
${cleanedFm}
|
|
2213
|
+
---${afterFrontmatter}`;
|
|
2214
|
+
await writeFileForce(configPath, newContent);
|
|
2215
|
+
}
|
|
2216
|
+
async function updateIntegrationConfig(integrations) {
|
|
2217
|
+
const configPath = resolve6(syntaurRoot(), "config.md");
|
|
2218
|
+
const nextIntegrations = {
|
|
2219
|
+
...(await readConfig()).integrations,
|
|
2220
|
+
...integrations
|
|
2221
|
+
};
|
|
2222
|
+
const integrationBlock = serializeIntegrationConfig(nextIntegrations);
|
|
2223
|
+
const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
2224
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
2225
|
+
if (!fmMatch) {
|
|
2226
|
+
const content = `---
|
|
2227
|
+
version: "2.0"
|
|
2228
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
2229
|
+
${integrationBlock ?? ""}
|
|
2230
|
+
---
|
|
2231
|
+
${existing}`;
|
|
2232
|
+
await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
const fmBlock = fmMatch[2];
|
|
2236
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
2237
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "integrations");
|
|
2238
|
+
const newFm = integrationBlock ? `${cleanedFm}
|
|
2239
|
+
${integrationBlock}`.replace(/^\n+/, "") : cleanedFm;
|
|
2240
|
+
const normalizedFm = newFm.replace(/\n+$/, "");
|
|
2241
|
+
const newContent = `---
|
|
2242
|
+
${normalizedFm}
|
|
2243
|
+
---${afterFrontmatter}`;
|
|
2244
|
+
await writeFileForce(configPath, newContent);
|
|
2245
|
+
}
|
|
2246
|
+
async function updateOnboardingConfig(onboarding) {
|
|
2247
|
+
const configPath = resolve6(syntaurRoot(), "config.md");
|
|
2248
|
+
const nextOnboarding = {
|
|
2249
|
+
...(await readConfig()).onboarding,
|
|
2250
|
+
...onboarding
|
|
2251
|
+
};
|
|
2252
|
+
const onboardingBlock = serializeOnboardingConfig(nextOnboarding);
|
|
2253
|
+
const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
2254
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
2255
|
+
if (!fmMatch) {
|
|
2256
|
+
const content = `---
|
|
2257
|
+
version: "2.0"
|
|
2258
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
2259
|
+
${onboardingBlock}
|
|
2260
|
+
---
|
|
2261
|
+
${existing}`;
|
|
2262
|
+
await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
const fmBlock = fmMatch[2];
|
|
2266
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
2267
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "onboarding");
|
|
2268
|
+
const newFm = `${cleanedFm}
|
|
2269
|
+
${onboardingBlock}`.replace(/^\n+/, "");
|
|
2270
|
+
const normalizedFm = newFm.replace(/\n+$/, "");
|
|
2271
|
+
const newContent = `---
|
|
2272
|
+
${normalizedFm}
|
|
2273
|
+
---${afterFrontmatter}`;
|
|
2274
|
+
await writeFileForce(configPath, newContent);
|
|
2275
|
+
}
|
|
2276
|
+
async function updateBackupConfig(backup) {
|
|
2277
|
+
const configPath = resolve6(syntaurRoot(), "config.md");
|
|
2278
|
+
const current = (await readConfig()).backup;
|
|
2279
|
+
const nextBackup = {
|
|
2280
|
+
repo: current?.repo ?? null,
|
|
2281
|
+
categories: current?.categories ?? "projects, playbooks, todos, servers, config",
|
|
2282
|
+
lastBackup: current?.lastBackup ?? null,
|
|
2283
|
+
lastRestore: current?.lastRestore ?? null,
|
|
2284
|
+
...backup
|
|
2285
|
+
};
|
|
2286
|
+
const backupBlock = serializeBackupConfig(nextBackup);
|
|
2287
|
+
const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
|
|
2288
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
2289
|
+
if (!fmMatch) {
|
|
2290
|
+
const content = `---
|
|
2291
|
+
version: "2.0"
|
|
2292
|
+
defaultProjectDir: ${defaultProjectDir()}
|
|
2293
|
+
${backupBlock}
|
|
2294
|
+
---
|
|
2295
|
+
${existing}`;
|
|
2296
|
+
await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
const fmBlock = fmMatch[2];
|
|
2300
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
2301
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "backup");
|
|
2302
|
+
const newFm = `${cleanedFm}
|
|
2303
|
+
${backupBlock}`.replace(/^\n+/, "");
|
|
2304
|
+
const normalizedFm = newFm.replace(/\n+$/, "");
|
|
2305
|
+
const newContent = `---
|
|
2306
|
+
${normalizedFm}
|
|
2307
|
+
---${afterFrontmatter}`;
|
|
2308
|
+
await writeFileForce(configPath, newContent);
|
|
2309
|
+
}
|
|
1432
2310
|
async function readConfig() {
|
|
1433
2311
|
const configPath = resolve6(syntaurRoot(), "config.md");
|
|
1434
2312
|
if (!await fileExists(configPath)) {
|
|
@@ -1504,6 +2382,9 @@ async function readConfig() {
|
|
|
1504
2382
|
workspaceVisibility: parseWorkspaceVisibilityConfig(fmBlock)
|
|
1505
2383
|
};
|
|
1506
2384
|
}
|
|
2385
|
+
function getAssignmentTypes(config) {
|
|
2386
|
+
return config.types ?? DEFAULT_ASSIGNMENT_TYPES;
|
|
2387
|
+
}
|
|
1507
2388
|
function getAgents(config) {
|
|
1508
2389
|
if (config.agents === null) return BUILTIN_AGENTS;
|
|
1509
2390
|
const builtinById = new Map(BUILTIN_AGENTS.map((a) => [a.id, a]));
|
|
@@ -1550,7 +2431,18 @@ function getTerminal(config) {
|
|
|
1550
2431
|
}
|
|
1551
2432
|
return "terminal-app";
|
|
1552
2433
|
}
|
|
1553
|
-
|
|
2434
|
+
async function updateAgentsConfig(mutation, options = {}) {
|
|
2435
|
+
const config = await readConfig();
|
|
2436
|
+
const previous = config.agents ?? [...BUILTIN_AGENTS];
|
|
2437
|
+
const next = mutation.apply(previous);
|
|
2438
|
+
validateAgentList(next);
|
|
2439
|
+
if (options.dryRun) {
|
|
2440
|
+
return { previous, next, written: false };
|
|
2441
|
+
}
|
|
2442
|
+
await writeAgentsConfig(next);
|
|
2443
|
+
return { previous, next, written: true };
|
|
2444
|
+
}
|
|
2445
|
+
var DEFAULT_DERIVE_CONFIG, DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
|
|
1554
2446
|
var init_config2 = __esm({
|
|
1555
2447
|
"src/utils/config.ts"() {
|
|
1556
2448
|
"use strict";
|
|
@@ -1564,6 +2456,45 @@ var init_config2 = __esm({
|
|
|
1564
2456
|
init_slug();
|
|
1565
2457
|
init_terminal_schema();
|
|
1566
2458
|
init_workspace_visibility_schema();
|
|
2459
|
+
DEFAULT_DERIVE_CONFIG = {
|
|
2460
|
+
phaseLadder: [
|
|
2461
|
+
{ phase: "draft", when: "*", next: "Fill in the objective and acceptance criteria" },
|
|
2462
|
+
{
|
|
2463
|
+
// planExists-but-not-approved also sits here: the default status set has
|
|
2464
|
+
// no `planning` id. Users who define one add a `planExists:true` rung.
|
|
2465
|
+
phase: "ready_for_planning",
|
|
2466
|
+
when: "hasRealObjective:true AND acRealTotal > 0",
|
|
2467
|
+
next: "Write a plan and get it approved"
|
|
2468
|
+
},
|
|
2469
|
+
{ phase: "ready_to_implement", when: "planApproved:true", next: "Start implementing" },
|
|
2470
|
+
{
|
|
2471
|
+
phase: "in_progress",
|
|
2472
|
+
when: "planApproved:true AND implementationStarted:true",
|
|
2473
|
+
next: "Finish acceptance criteria, then request review"
|
|
2474
|
+
},
|
|
2475
|
+
{
|
|
2476
|
+
phase: "review",
|
|
2477
|
+
when: "acAllChecked:true OR reviewRequested:true",
|
|
2478
|
+
next: "Complete, or address review feedback"
|
|
2479
|
+
}
|
|
2480
|
+
],
|
|
2481
|
+
disposition: [
|
|
2482
|
+
{ when: "parked:true", is: "parked" },
|
|
2483
|
+
{ when: "blocked:true", is: "blocked" },
|
|
2484
|
+
{ when: null, is: "active" }
|
|
2485
|
+
],
|
|
2486
|
+
headline: { terminal: "passthrough", parked: "parked", blocked: "blocked", active: "phase" }
|
|
2487
|
+
};
|
|
2488
|
+
DEFAULT_ASSIGNMENT_TYPES = {
|
|
2489
|
+
definitions: [
|
|
2490
|
+
{ id: "feature", label: "Feature" },
|
|
2491
|
+
{ id: "bug", label: "Bug" },
|
|
2492
|
+
{ id: "refactor", label: "Refactor" },
|
|
2493
|
+
{ id: "research", label: "Research" },
|
|
2494
|
+
{ id: "chore", label: "Chore" }
|
|
2495
|
+
],
|
|
2496
|
+
default: "feature"
|
|
2497
|
+
};
|
|
1567
2498
|
DEFAULT_CONFIG = {
|
|
1568
2499
|
version: "2.0",
|
|
1569
2500
|
defaultProjectDir: defaultProjectDir(),
|
|
@@ -1965,9 +2896,875 @@ var init_overviewCopy = __esm({
|
|
|
1965
2896
|
}
|
|
1966
2897
|
});
|
|
1967
2898
|
|
|
2899
|
+
// src/lifecycle/facts.ts
|
|
2900
|
+
var facts_exports = {};
|
|
2901
|
+
__export(facts_exports, {
|
|
2902
|
+
areDependenciesSatisfied: () => areDependenciesSatisfied,
|
|
2903
|
+
computeFacts: () => computeFacts,
|
|
2904
|
+
countRealAcceptanceCriteria: () => countRealAcceptanceCriteria,
|
|
2905
|
+
countUnresolvedQuestions: () => countUnresolvedQuestions,
|
|
2906
|
+
hasRealObjective: () => hasRealObjective,
|
|
2907
|
+
isPlanApproved: () => isPlanApproved,
|
|
2908
|
+
latestPlanFile: () => latestPlanFile,
|
|
2909
|
+
planDigest: () => planDigest
|
|
2910
|
+
});
|
|
2911
|
+
import { createHash } from "crypto";
|
|
2912
|
+
import { readdir as readdir6, readFile as readFile9 } from "fs/promises";
|
|
2913
|
+
import { resolve as resolve11 } from "path";
|
|
2914
|
+
function sectionBody(body, heading) {
|
|
2915
|
+
const re = new RegExp(`^##\\s+${heading}\\s*$`, "m");
|
|
2916
|
+
const m = body.match(re);
|
|
2917
|
+
if (!m || m.index === void 0) return null;
|
|
2918
|
+
const start = m.index + m[0].length;
|
|
2919
|
+
const rest = body.slice(start);
|
|
2920
|
+
const next = rest.search(/^##\s+/m);
|
|
2921
|
+
return next >= 0 ? rest.slice(0, next) : rest;
|
|
2922
|
+
}
|
|
2923
|
+
function hasRealObjective(body) {
|
|
2924
|
+
const section = sectionBody(body, "Objective");
|
|
2925
|
+
if (section === null) return false;
|
|
2926
|
+
return section.replace(HTML_COMMENT_RE, "").trim().length > 0;
|
|
2927
|
+
}
|
|
2928
|
+
function countRealAcceptanceCriteria(body) {
|
|
2929
|
+
const section = sectionBody(body, "Acceptance Criteria");
|
|
2930
|
+
if (section === null) return { total: 0, checked: 0 };
|
|
2931
|
+
let total = 0;
|
|
2932
|
+
let checked = 0;
|
|
2933
|
+
for (const line of section.split("\n")) {
|
|
2934
|
+
const m = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)$/);
|
|
2935
|
+
if (!m) continue;
|
|
2936
|
+
const content = m[2].replace(HTML_COMMENT_RE, "").trim();
|
|
2937
|
+
if (content.length === 0) continue;
|
|
2938
|
+
total++;
|
|
2939
|
+
if (m[1].toLowerCase() === "x") checked++;
|
|
2940
|
+
}
|
|
2941
|
+
return { total, checked };
|
|
2942
|
+
}
|
|
2943
|
+
async function latestPlanFile(assignmentDir) {
|
|
2944
|
+
let entries;
|
|
2945
|
+
try {
|
|
2946
|
+
entries = await readdir6(assignmentDir);
|
|
2947
|
+
} catch {
|
|
2948
|
+
return null;
|
|
2949
|
+
}
|
|
2950
|
+
let best = null;
|
|
2951
|
+
for (const name of entries) {
|
|
2952
|
+
const m = name.match(PLAN_FILE_RE);
|
|
2953
|
+
if (!m) continue;
|
|
2954
|
+
const version = m[1] ? parseInt(m[1], 10) : 1;
|
|
2955
|
+
if (!best || version > best.version) best = { name, version };
|
|
2956
|
+
}
|
|
2957
|
+
return best?.name ?? null;
|
|
2958
|
+
}
|
|
2959
|
+
function planDigest(content) {
|
|
2960
|
+
return createHash("sha256").update(content, "utf-8").digest("hex");
|
|
2961
|
+
}
|
|
2962
|
+
async function isPlanApproved(assignmentDir, frontmatter) {
|
|
2963
|
+
const approval = frontmatter.planApproval;
|
|
2964
|
+
if (!approval) return false;
|
|
2965
|
+
const latest = await latestPlanFile(assignmentDir);
|
|
2966
|
+
if (!latest || latest !== approval.file) return false;
|
|
2967
|
+
try {
|
|
2968
|
+
const content = await readFile9(resolve11(assignmentDir, latest), "utf-8");
|
|
2969
|
+
return planDigest(content) === approval.digest;
|
|
2970
|
+
} catch {
|
|
2971
|
+
return false;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
async function countUnresolvedQuestions(assignmentDir) {
|
|
2975
|
+
const commentsPath = resolve11(assignmentDir, "comments.md");
|
|
2976
|
+
if (!await fileExists(commentsPath)) return 0;
|
|
2977
|
+
try {
|
|
2978
|
+
const content = await readFile9(commentsPath, "utf-8");
|
|
2979
|
+
let count = 0;
|
|
2980
|
+
for (const block of content.split(/^##\s+/m).slice(1)) {
|
|
2981
|
+
if (/^\*\*Type:\*\*\s*question\s*$/m.test(block) && /^\*\*Resolved:\*\*\s*false\s*$/m.test(block)) {
|
|
2982
|
+
count++;
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
return count;
|
|
2986
|
+
} catch {
|
|
2987
|
+
return 0;
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
async function areDependenciesSatisfied(projectDir, dependsOn, terminalStatuses) {
|
|
2991
|
+
if (dependsOn.length === 0 || projectDir === null) return true;
|
|
2992
|
+
for (const depSlug of dependsOn) {
|
|
2993
|
+
const depPath = resolve11(projectDir, "assignments", depSlug, "assignment.md");
|
|
2994
|
+
if (!await fileExists(depPath)) return false;
|
|
2995
|
+
try {
|
|
2996
|
+
const content = await readFile9(depPath, "utf-8");
|
|
2997
|
+
const m = content.match(/^status:\s*(.+)$/m);
|
|
2998
|
+
const status = m ? m[1].trim() : "";
|
|
2999
|
+
if (!terminalStatuses.has(status)) return false;
|
|
3000
|
+
} catch {
|
|
3001
|
+
return false;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
return true;
|
|
3005
|
+
}
|
|
3006
|
+
async function computeFacts(input) {
|
|
3007
|
+
const { assignmentDir, frontmatter, body, projectDir, terminalStatuses } = input;
|
|
3008
|
+
const ac = countRealAcceptanceCriteria(body);
|
|
3009
|
+
const [planFile, planApproved, unresolvedQuestions, depsSatisfied] = await Promise.all([
|
|
3010
|
+
latestPlanFile(assignmentDir),
|
|
3011
|
+
isPlanApproved(assignmentDir, frontmatter),
|
|
3012
|
+
countUnresolvedQuestions(assignmentDir),
|
|
3013
|
+
areDependenciesSatisfied(projectDir, frontmatter.dependsOn, terminalStatuses)
|
|
3014
|
+
]);
|
|
3015
|
+
return {
|
|
3016
|
+
hasRealObjective: hasRealObjective(body),
|
|
3017
|
+
acRealTotal: ac.total,
|
|
3018
|
+
acRealChecked: ac.checked,
|
|
3019
|
+
acAllChecked: ac.total > 0 && ac.checked === ac.total,
|
|
3020
|
+
planExists: planFile !== null,
|
|
3021
|
+
planApproved,
|
|
3022
|
+
workspaceSet: frontmatter.workspace.repository !== null && frontmatter.workspace.branch !== null,
|
|
3023
|
+
implementationStarted: frontmatter.implementationStarted,
|
|
3024
|
+
depsSatisfied,
|
|
3025
|
+
unresolvedQuestions,
|
|
3026
|
+
blocked: frontmatter.blockedReason !== null,
|
|
3027
|
+
parked: frontmatter.parked,
|
|
3028
|
+
reviewRequested: frontmatter.reviewRequested,
|
|
3029
|
+
pinned: frontmatter.override !== null
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
var HTML_COMMENT_RE, PLAN_FILE_RE;
|
|
3033
|
+
var init_facts = __esm({
|
|
3034
|
+
"src/lifecycle/facts.ts"() {
|
|
3035
|
+
"use strict";
|
|
3036
|
+
init_fs();
|
|
3037
|
+
HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
3038
|
+
PLAN_FILE_RE = /^plan(?:-v(\d+))?\.md$/;
|
|
3039
|
+
}
|
|
3040
|
+
});
|
|
3041
|
+
|
|
3042
|
+
// src/utils/query/fields.ts
|
|
3043
|
+
function resolveField(registry, name) {
|
|
3044
|
+
return registry[name.toLowerCase()] ?? null;
|
|
3045
|
+
}
|
|
3046
|
+
function readField(def, fieldName, item) {
|
|
3047
|
+
if (def.get) return def.get(item);
|
|
3048
|
+
return item[fieldName] ?? item[fieldName.toLowerCase()];
|
|
3049
|
+
}
|
|
3050
|
+
var init_fields = __esm({
|
|
3051
|
+
"src/utils/query/fields.ts"() {
|
|
3052
|
+
"use strict";
|
|
3053
|
+
}
|
|
3054
|
+
});
|
|
3055
|
+
|
|
3056
|
+
// src/utils/query/evaluate.ts
|
|
3057
|
+
function localDayBounds(date) {
|
|
3058
|
+
const [y, m, d] = date.split("-").map((n) => parseInt(n, 10));
|
|
3059
|
+
const start = new Date(y, m - 1, d).getTime();
|
|
3060
|
+
const end = new Date(y, m - 1, d + 1).getTime();
|
|
3061
|
+
return [start, end];
|
|
3062
|
+
}
|
|
3063
|
+
function toEpoch(value) {
|
|
3064
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
3065
|
+
if (typeof value === "string" && value.length > 0) {
|
|
3066
|
+
const t = Date.parse(value);
|
|
3067
|
+
return Number.isNaN(t) ? null : t;
|
|
3068
|
+
}
|
|
3069
|
+
return null;
|
|
3070
|
+
}
|
|
3071
|
+
function toNumber(value) {
|
|
3072
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
3073
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
3074
|
+
const n = Number(value);
|
|
3075
|
+
return Number.isFinite(n) ? n : null;
|
|
3076
|
+
}
|
|
3077
|
+
return null;
|
|
3078
|
+
}
|
|
3079
|
+
function ciEquals(a, b) {
|
|
3080
|
+
return typeof a === "string" && a.toLowerCase() === b.toLowerCase();
|
|
3081
|
+
}
|
|
3082
|
+
function isNone(value) {
|
|
3083
|
+
return value === null || value === void 0 || value === "";
|
|
3084
|
+
}
|
|
3085
|
+
function compileEquality(def, field, value, atomPos) {
|
|
3086
|
+
switch (def.kind) {
|
|
3087
|
+
case "enum":
|
|
3088
|
+
case "string":
|
|
3089
|
+
if (def.noneSentinel && value.raw.toLowerCase() === "none") {
|
|
3090
|
+
return (item) => isNone(readField(def, field, item));
|
|
3091
|
+
}
|
|
3092
|
+
return (item) => ciEquals(readField(def, field, item), value.raw);
|
|
3093
|
+
case "substring":
|
|
3094
|
+
return (item) => {
|
|
3095
|
+
const v = readField(def, field, item);
|
|
3096
|
+
return typeof v === "string" && v.toLowerCase().includes(value.raw.toLowerCase());
|
|
3097
|
+
};
|
|
3098
|
+
case "bool": {
|
|
3099
|
+
const want = value.raw.toLowerCase();
|
|
3100
|
+
if (want !== "true" && want !== "false") {
|
|
3101
|
+
throw new CompileError([
|
|
3102
|
+
{ pos: value.pos, message: `Field "${field}" is boolean \u2014 use ${field}:true or ${field}:false` }
|
|
3103
|
+
]);
|
|
3104
|
+
}
|
|
3105
|
+
const expected = want === "true";
|
|
3106
|
+
return (item) => {
|
|
3107
|
+
const v = readField(def, field, item);
|
|
3108
|
+
const b = typeof v === "boolean" ? v : v === "true" ? true : v === "false" || v === null || v === void 0 || v === "" ? false : null;
|
|
3109
|
+
return b !== null && b === expected;
|
|
3110
|
+
};
|
|
3111
|
+
}
|
|
3112
|
+
case "number": {
|
|
3113
|
+
const n = value.num ?? toNumber(value.raw);
|
|
3114
|
+
if (n === null) {
|
|
3115
|
+
throw new CompileError([{ pos: value.pos, message: `Field "${field}" is numeric \u2014 "${value.raw}" is not a number` }]);
|
|
3116
|
+
}
|
|
3117
|
+
return (item) => toNumber(readField(def, field, item)) === n;
|
|
3118
|
+
}
|
|
3119
|
+
case "ordinal":
|
|
3120
|
+
return (item) => ciEquals(readField(def, field, item), value.raw);
|
|
3121
|
+
case "list":
|
|
3122
|
+
return (item) => {
|
|
3123
|
+
const v = readField(def, field, item);
|
|
3124
|
+
return Array.isArray(v) && v.some((el) => ciEquals(el, value.raw));
|
|
3125
|
+
};
|
|
3126
|
+
case "timestamp": {
|
|
3127
|
+
if (value.type === "date") {
|
|
3128
|
+
const [start, end] = localDayBounds(value.raw);
|
|
3129
|
+
return (item) => {
|
|
3130
|
+
const t = toEpoch(readField(def, field, item));
|
|
3131
|
+
return t !== null && t >= start && t < end;
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
throw new CompileError([
|
|
3135
|
+
{ 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)` }
|
|
3136
|
+
]);
|
|
3137
|
+
}
|
|
3138
|
+
case "duration":
|
|
3139
|
+
throw new CompileError([
|
|
3140
|
+
{ pos: atomPos, message: `Field "${field}" is a duration \u2014 use a comparison (e.g. ${field} > 3d)` }
|
|
3141
|
+
]);
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
function compileComparison(def, field, op, value) {
|
|
3145
|
+
const cmp = (a, b) => {
|
|
3146
|
+
switch (op) {
|
|
3147
|
+
case "<":
|
|
3148
|
+
return a < b;
|
|
3149
|
+
case ">":
|
|
3150
|
+
return a > b;
|
|
3151
|
+
case "<=":
|
|
3152
|
+
return a <= b;
|
|
3153
|
+
case ">=":
|
|
3154
|
+
return a >= b;
|
|
3155
|
+
case "=":
|
|
3156
|
+
return a === b;
|
|
3157
|
+
case "!=":
|
|
3158
|
+
return a !== b;
|
|
3159
|
+
default:
|
|
3160
|
+
return false;
|
|
3161
|
+
}
|
|
3162
|
+
};
|
|
3163
|
+
switch (def.kind) {
|
|
3164
|
+
case "number": {
|
|
3165
|
+
const n = value.num ?? toNumber(value.raw);
|
|
3166
|
+
if (n === null) {
|
|
3167
|
+
throw new CompileError([{ pos: value.pos, message: `"${value.raw}" is not a number (field "${field}")` }]);
|
|
3168
|
+
}
|
|
3169
|
+
return (item) => {
|
|
3170
|
+
const v = toNumber(readField(def, field, item));
|
|
3171
|
+
return v !== null && cmp(v, n);
|
|
3172
|
+
};
|
|
3173
|
+
}
|
|
3174
|
+
case "ordinal": {
|
|
3175
|
+
const order = def.order ?? [];
|
|
3176
|
+
const idx = order.findIndex((o) => o.toLowerCase() === value.raw.toLowerCase());
|
|
3177
|
+
if (idx < 0) {
|
|
3178
|
+
throw new CompileError([
|
|
3179
|
+
{ pos: value.pos, message: `"${value.raw}" is not a valid ${field} (expected one of: ${order.join(", ")})` }
|
|
3180
|
+
]);
|
|
3181
|
+
}
|
|
3182
|
+
return (item) => {
|
|
3183
|
+
const raw = readField(def, field, item);
|
|
3184
|
+
const vIdx = typeof raw === "string" ? order.findIndex((o) => o.toLowerCase() === raw.toLowerCase()) : -1;
|
|
3185
|
+
return vIdx >= 0 && cmp(vIdx, idx);
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
case "timestamp": {
|
|
3189
|
+
if (value.type === "duration") {
|
|
3190
|
+
const sign = value.sign === 0 ? -1 : value.sign ?? -1;
|
|
3191
|
+
const offset = sign * (value.num ?? 0);
|
|
3192
|
+
return (item, ctx) => {
|
|
3193
|
+
const t = toEpoch(readField(def, field, item));
|
|
3194
|
+
return t !== null && cmp(t, ctx.now + offset);
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
3197
|
+
if (value.type === "date") {
|
|
3198
|
+
const [start, end] = localDayBounds(value.raw);
|
|
3199
|
+
return (item) => {
|
|
3200
|
+
const t = toEpoch(readField(def, field, item));
|
|
3201
|
+
if (t === null) return false;
|
|
3202
|
+
switch (op) {
|
|
3203
|
+
case "<":
|
|
3204
|
+
return t < start;
|
|
3205
|
+
case "<=":
|
|
3206
|
+
return t < end;
|
|
3207
|
+
case ">":
|
|
3208
|
+
return t >= end;
|
|
3209
|
+
case ">=":
|
|
3210
|
+
return t >= start;
|
|
3211
|
+
case "=":
|
|
3212
|
+
return t >= start && t < end;
|
|
3213
|
+
case "!=":
|
|
3214
|
+
return t < start || t >= end;
|
|
3215
|
+
default:
|
|
3216
|
+
return false;
|
|
3217
|
+
}
|
|
3218
|
+
};
|
|
3219
|
+
}
|
|
3220
|
+
throw new CompileError([
|
|
3221
|
+
{ pos: value.pos, message: `Compare timestamp field "${field}" to a duration (e.g. -36h) or a date (YYYY-MM-DD)` }
|
|
3222
|
+
]);
|
|
3223
|
+
}
|
|
3224
|
+
case "duration": {
|
|
3225
|
+
if (value.type !== "duration") {
|
|
3226
|
+
throw new CompileError([
|
|
3227
|
+
{ pos: value.pos, message: `Compare duration field "${field}" to a duration literal (e.g. 3d)` }
|
|
3228
|
+
]);
|
|
3229
|
+
}
|
|
3230
|
+
const magnitude = value.num ?? 0;
|
|
3231
|
+
return (item) => {
|
|
3232
|
+
const v = toNumber(readField(def, field, item));
|
|
3233
|
+
return v !== null && cmp(v, magnitude);
|
|
3234
|
+
};
|
|
3235
|
+
}
|
|
3236
|
+
case "enum":
|
|
3237
|
+
case "string":
|
|
3238
|
+
case "substring":
|
|
3239
|
+
case "list": {
|
|
3240
|
+
if (op === "=") {
|
|
3241
|
+
return compileEquality(def, field, value, value.pos);
|
|
3242
|
+
}
|
|
3243
|
+
if (op === "!=") {
|
|
3244
|
+
const eq = compileEquality(def, field, value, value.pos);
|
|
3245
|
+
return (item, ctx) => !eq(item, ctx);
|
|
3246
|
+
}
|
|
3247
|
+
throw new CompileError([
|
|
3248
|
+
{ pos: value.pos, message: `Field "${field}" does not support ordering comparisons (use ":" or "=").` }
|
|
3249
|
+
]);
|
|
3250
|
+
}
|
|
3251
|
+
case "bool": {
|
|
3252
|
+
if (op === "=" || op === "!=") {
|
|
3253
|
+
const eq = compileEquality(def, field, value, value.pos);
|
|
3254
|
+
return op === "=" ? eq : (item, ctx) => !eq(item, ctx);
|
|
3255
|
+
}
|
|
3256
|
+
throw new CompileError([{ pos: value.pos, message: `Field "${field}" is boolean \u2014 use ${field}:true / ${field}:false` }]);
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
function compileAtom(atom, registry) {
|
|
3261
|
+
const def = resolveField(registry, atom.field);
|
|
3262
|
+
if (!def) {
|
|
3263
|
+
throw new CompileError([{ pos: atom.pos, message: `Unknown field "${atom.field}"` }]);
|
|
3264
|
+
}
|
|
3265
|
+
if (atom.op === ":") {
|
|
3266
|
+
const preds = atom.values.map((v) => compileEquality(def, atom.field, v, atom.pos));
|
|
3267
|
+
if (preds.length === 1) return preds[0];
|
|
3268
|
+
return (item, ctx) => preds.some((p) => p(item, ctx));
|
|
3269
|
+
}
|
|
3270
|
+
return compileComparison(def, atom.field, atom.op, atom.values[0]);
|
|
3271
|
+
}
|
|
3272
|
+
function compileNode(node, registry) {
|
|
3273
|
+
switch (node.kind) {
|
|
3274
|
+
case "all":
|
|
3275
|
+
return () => true;
|
|
3276
|
+
case "atom":
|
|
3277
|
+
return compileAtom(node, registry);
|
|
3278
|
+
case "not": {
|
|
3279
|
+
const inner = compileNode(node.child, registry);
|
|
3280
|
+
return (item, ctx) => !inner(item, ctx);
|
|
3281
|
+
}
|
|
3282
|
+
case "and": {
|
|
3283
|
+
const preds = node.children.map((c) => compileNode(c, registry));
|
|
3284
|
+
return (item, ctx) => preds.every((p) => p(item, ctx));
|
|
3285
|
+
}
|
|
3286
|
+
case "or": {
|
|
3287
|
+
const preds = node.children.map((c) => compileNode(c, registry));
|
|
3288
|
+
return (item, ctx) => preds.some((p) => p(item, ctx));
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
var CompileError;
|
|
3293
|
+
var init_evaluate = __esm({
|
|
3294
|
+
"src/utils/query/evaluate.ts"() {
|
|
3295
|
+
"use strict";
|
|
3296
|
+
init_fields();
|
|
3297
|
+
CompileError = class extends Error {
|
|
3298
|
+
constructor(errors) {
|
|
3299
|
+
super(errors.map((e) => `${e.message} (at ${e.pos})`).join("; "));
|
|
3300
|
+
this.errors = errors;
|
|
3301
|
+
this.name = "CompileError";
|
|
3302
|
+
}
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
});
|
|
3306
|
+
|
|
3307
|
+
// src/utils/query/lexer.ts
|
|
3308
|
+
function lex(input) {
|
|
3309
|
+
const tokens = [];
|
|
3310
|
+
let i = 0;
|
|
3311
|
+
const numberOrDuration = (start, sign) => {
|
|
3312
|
+
let j = i;
|
|
3313
|
+
while (j < input.length && /\d/.test(input[j])) j++;
|
|
3314
|
+
const digits = input.slice(i, j);
|
|
3315
|
+
let unit = "";
|
|
3316
|
+
while (j < input.length && /[a-z]/i.test(input[j])) {
|
|
3317
|
+
unit += input[j];
|
|
3318
|
+
j++;
|
|
3319
|
+
}
|
|
3320
|
+
i = j;
|
|
3321
|
+
if (unit.length > 0) {
|
|
3322
|
+
const ms = DURATION_MS[unit.toLowerCase()];
|
|
3323
|
+
if (ms === void 0) {
|
|
3324
|
+
throw new LexError(start, `Unknown duration unit "${unit}" (expected h, d, w, m, mo, or y)`);
|
|
3325
|
+
}
|
|
3326
|
+
return {
|
|
3327
|
+
type: "DURATION",
|
|
3328
|
+
text: input.slice(start, j),
|
|
3329
|
+
pos: start,
|
|
3330
|
+
num: parseInt(digits, 10) * ms,
|
|
3331
|
+
sign
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
if (sign !== 0) {
|
|
3335
|
+
return { type: "NUMBER", text: input.slice(start, j), pos: start, num: sign * parseInt(digits, 10) };
|
|
3336
|
+
}
|
|
3337
|
+
return { type: "NUMBER", text: digits, pos: start, num: parseInt(digits, 10) };
|
|
3338
|
+
};
|
|
3339
|
+
while (i < input.length) {
|
|
3340
|
+
const c = input[i];
|
|
3341
|
+
const start = i;
|
|
3342
|
+
if (c === " " || c === " " || c === "\n" || c === "\r") {
|
|
3343
|
+
i++;
|
|
3344
|
+
continue;
|
|
3345
|
+
}
|
|
3346
|
+
if (c === "(") {
|
|
3347
|
+
tokens.push({ type: "LPAREN", text: c, pos: start });
|
|
3348
|
+
i++;
|
|
3349
|
+
continue;
|
|
3350
|
+
}
|
|
3351
|
+
if (c === ")") {
|
|
3352
|
+
tokens.push({ type: "RPAREN", text: c, pos: start });
|
|
3353
|
+
i++;
|
|
3354
|
+
continue;
|
|
3355
|
+
}
|
|
3356
|
+
if (c === ",") {
|
|
3357
|
+
tokens.push({ type: "COMMA", text: c, pos: start });
|
|
3358
|
+
i++;
|
|
3359
|
+
continue;
|
|
3360
|
+
}
|
|
3361
|
+
if (c === ":") {
|
|
3362
|
+
tokens.push({ type: "COLON", text: c, pos: start });
|
|
3363
|
+
i++;
|
|
3364
|
+
continue;
|
|
3365
|
+
}
|
|
3366
|
+
if (c === "*") {
|
|
3367
|
+
tokens.push({ type: "STAR", text: c, pos: start });
|
|
3368
|
+
i++;
|
|
3369
|
+
continue;
|
|
3370
|
+
}
|
|
3371
|
+
if (c === "<" || c === ">") {
|
|
3372
|
+
if (input[i + 1] === "=") {
|
|
3373
|
+
tokens.push({ type: "OP", text: c + "=", pos: start });
|
|
3374
|
+
i += 2;
|
|
3375
|
+
} else {
|
|
3376
|
+
tokens.push({ type: "OP", text: c, pos: start });
|
|
3377
|
+
i++;
|
|
3378
|
+
}
|
|
3379
|
+
continue;
|
|
3380
|
+
}
|
|
3381
|
+
if (c === "!") {
|
|
3382
|
+
if (input[i + 1] === "=") {
|
|
3383
|
+
tokens.push({ type: "OP", text: "!=", pos: start });
|
|
3384
|
+
i += 2;
|
|
3385
|
+
continue;
|
|
3386
|
+
}
|
|
3387
|
+
throw new LexError(start, `Unexpected "!" (did you mean "!="?)`);
|
|
3388
|
+
}
|
|
3389
|
+
if (c === "=") {
|
|
3390
|
+
i += input[i + 1] === "=" ? 2 : 1;
|
|
3391
|
+
tokens.push({ type: "OP", text: "=", pos: start });
|
|
3392
|
+
continue;
|
|
3393
|
+
}
|
|
3394
|
+
if (c === '"' || c === "'") {
|
|
3395
|
+
const quote = c;
|
|
3396
|
+
let j = i + 1;
|
|
3397
|
+
let out = "";
|
|
3398
|
+
while (j < input.length && input[j] !== quote) {
|
|
3399
|
+
if (input[j] === "\\" && j + 1 < input.length) {
|
|
3400
|
+
out += input[j + 1];
|
|
3401
|
+
j += 2;
|
|
3402
|
+
} else {
|
|
3403
|
+
out += input[j];
|
|
3404
|
+
j++;
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
if (j >= input.length) throw new LexError(start, "Unterminated string literal");
|
|
3408
|
+
tokens.push({ type: "STRING", text: out, pos: start });
|
|
3409
|
+
i = j + 1;
|
|
3410
|
+
continue;
|
|
3411
|
+
}
|
|
3412
|
+
if (c === "-" || c === "+") {
|
|
3413
|
+
if (/\d/.test(input[i + 1] ?? "")) {
|
|
3414
|
+
const sign = c === "-" ? -1 : 1;
|
|
3415
|
+
i++;
|
|
3416
|
+
tokens.push(numberOrDuration(start, sign));
|
|
3417
|
+
continue;
|
|
3418
|
+
}
|
|
3419
|
+
if (c === "-") {
|
|
3420
|
+
tokens.push({ type: "MINUS", text: "-", pos: start });
|
|
3421
|
+
i++;
|
|
3422
|
+
continue;
|
|
3423
|
+
}
|
|
3424
|
+
throw new LexError(start, 'Unexpected "+"');
|
|
3425
|
+
}
|
|
3426
|
+
if (/\d/.test(c)) {
|
|
3427
|
+
const dateMatch = input.slice(i).match(DATE_RE);
|
|
3428
|
+
if (dateMatch) {
|
|
3429
|
+
tokens.push({ type: "DATE", text: dateMatch[0], pos: start });
|
|
3430
|
+
i += dateMatch[0].length;
|
|
3431
|
+
continue;
|
|
3432
|
+
}
|
|
3433
|
+
tokens.push(numberOrDuration(start, 0));
|
|
3434
|
+
continue;
|
|
3435
|
+
}
|
|
3436
|
+
if (IDENT_START.test(c)) {
|
|
3437
|
+
let j = i + 1;
|
|
3438
|
+
while (j < input.length && IDENT_CHAR.test(input[j])) j++;
|
|
3439
|
+
const word = input.slice(i, j);
|
|
3440
|
+
const kw = word.toLowerCase();
|
|
3441
|
+
if (kw === "and") tokens.push({ type: "AND", text: word, pos: start });
|
|
3442
|
+
else if (kw === "or") tokens.push({ type: "OR", text: word, pos: start });
|
|
3443
|
+
else if (kw === "not") tokens.push({ type: "NOT", text: word, pos: start });
|
|
3444
|
+
else tokens.push({ type: "IDENT", text: word, pos: start });
|
|
3445
|
+
i = j;
|
|
3446
|
+
continue;
|
|
3447
|
+
}
|
|
3448
|
+
throw new LexError(start, `Unexpected character "${c}"`);
|
|
3449
|
+
}
|
|
3450
|
+
tokens.push({ type: "EOF", text: "", pos: input.length });
|
|
3451
|
+
return tokens;
|
|
3452
|
+
}
|
|
3453
|
+
var LexError, DURATION_MS, IDENT_START, IDENT_CHAR, DATE_RE;
|
|
3454
|
+
var init_lexer = __esm({
|
|
3455
|
+
"src/utils/query/lexer.ts"() {
|
|
3456
|
+
"use strict";
|
|
3457
|
+
LexError = class extends Error {
|
|
3458
|
+
constructor(pos, message) {
|
|
3459
|
+
super(message);
|
|
3460
|
+
this.pos = pos;
|
|
3461
|
+
this.name = "LexError";
|
|
3462
|
+
}
|
|
3463
|
+
};
|
|
3464
|
+
DURATION_MS = {
|
|
3465
|
+
h: 36e5,
|
|
3466
|
+
d: 864e5,
|
|
3467
|
+
w: 7 * 864e5,
|
|
3468
|
+
m: 30 * 864e5,
|
|
3469
|
+
mo: 30 * 864e5,
|
|
3470
|
+
y: 365 * 864e5
|
|
3471
|
+
};
|
|
3472
|
+
IDENT_START = /[A-Za-z_]/;
|
|
3473
|
+
IDENT_CHAR = /[A-Za-z0-9_-]/;
|
|
3474
|
+
DATE_RE = /^\d{4}-\d{2}-\d{2}/;
|
|
3475
|
+
}
|
|
3476
|
+
});
|
|
3477
|
+
|
|
3478
|
+
// src/utils/query/parser.ts
|
|
3479
|
+
function parseQuery(input) {
|
|
3480
|
+
try {
|
|
3481
|
+
const tokens = lex(input);
|
|
3482
|
+
const ast = new Parser(tokens).parseQuery();
|
|
3483
|
+
return { ast, errors: [] };
|
|
3484
|
+
} catch (err) {
|
|
3485
|
+
if (err instanceof LexError || err instanceof ParseError) {
|
|
3486
|
+
return { ast: null, errors: [{ pos: err.pos, message: err.message }] };
|
|
3487
|
+
}
|
|
3488
|
+
throw err;
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
var ParseError, VALUE_TOKENS, TERM_START, Parser;
|
|
3492
|
+
var init_parser3 = __esm({
|
|
3493
|
+
"src/utils/query/parser.ts"() {
|
|
3494
|
+
"use strict";
|
|
3495
|
+
init_lexer();
|
|
3496
|
+
ParseError = class extends Error {
|
|
3497
|
+
constructor(pos, message) {
|
|
3498
|
+
super(message);
|
|
3499
|
+
this.pos = pos;
|
|
3500
|
+
this.name = "ParseError";
|
|
3501
|
+
}
|
|
3502
|
+
};
|
|
3503
|
+
VALUE_TOKENS = /* @__PURE__ */ new Set(["IDENT", "STRING", "NUMBER", "DATE", "DURATION"]);
|
|
3504
|
+
TERM_START = /* @__PURE__ */ new Set(["IDENT", "NOT", "MINUS", "LPAREN", "STAR"]);
|
|
3505
|
+
Parser = class {
|
|
3506
|
+
constructor(tokens) {
|
|
3507
|
+
this.tokens = tokens;
|
|
3508
|
+
}
|
|
3509
|
+
pos = 0;
|
|
3510
|
+
peek() {
|
|
3511
|
+
return this.tokens[this.pos];
|
|
3512
|
+
}
|
|
3513
|
+
next() {
|
|
3514
|
+
return this.tokens[this.pos++];
|
|
3515
|
+
}
|
|
3516
|
+
expect(type, what) {
|
|
3517
|
+
const tok = this.peek();
|
|
3518
|
+
if (tok.type !== type) {
|
|
3519
|
+
throw new ParseError(tok.pos, `Expected ${what}, got "${tok.text || tok.type}"`);
|
|
3520
|
+
}
|
|
3521
|
+
return this.next();
|
|
3522
|
+
}
|
|
3523
|
+
parseQuery() {
|
|
3524
|
+
if (this.peek().type === "EOF") return { kind: "all" };
|
|
3525
|
+
const node = this.orExpr();
|
|
3526
|
+
const tok = this.peek();
|
|
3527
|
+
if (tok.type !== "EOF") {
|
|
3528
|
+
throw new ParseError(tok.pos, `Unexpected "${tok.text}" \u2014 unbalanced parentheses or stray token`);
|
|
3529
|
+
}
|
|
3530
|
+
return node;
|
|
3531
|
+
}
|
|
3532
|
+
orExpr() {
|
|
3533
|
+
const children = [this.andExpr()];
|
|
3534
|
+
while (this.peek().type === "OR") {
|
|
3535
|
+
this.next();
|
|
3536
|
+
children.push(this.andExpr());
|
|
3537
|
+
}
|
|
3538
|
+
return children.length === 1 ? children[0] : { kind: "or", children };
|
|
3539
|
+
}
|
|
3540
|
+
andExpr() {
|
|
3541
|
+
const children = [this.unary()];
|
|
3542
|
+
for (; ; ) {
|
|
3543
|
+
const tok = this.peek();
|
|
3544
|
+
if (tok.type === "AND") {
|
|
3545
|
+
this.next();
|
|
3546
|
+
children.push(this.unary());
|
|
3547
|
+
} else if (TERM_START.has(tok.type)) {
|
|
3548
|
+
children.push(this.unary());
|
|
3549
|
+
} else {
|
|
3550
|
+
break;
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
return children.length === 1 ? children[0] : { kind: "and", children };
|
|
3554
|
+
}
|
|
3555
|
+
unary() {
|
|
3556
|
+
const tok = this.peek();
|
|
3557
|
+
if (tok.type === "NOT") {
|
|
3558
|
+
this.next();
|
|
3559
|
+
return { kind: "not", child: this.unary() };
|
|
3560
|
+
}
|
|
3561
|
+
if (tok.type === "MINUS") {
|
|
3562
|
+
this.next();
|
|
3563
|
+
const inner = this.peek();
|
|
3564
|
+
if (inner.type !== "IDENT") {
|
|
3565
|
+
throw new ParseError(inner.pos, 'Expected a field atom after "-" negation');
|
|
3566
|
+
}
|
|
3567
|
+
return { kind: "not", child: this.atom() };
|
|
3568
|
+
}
|
|
3569
|
+
return this.primary();
|
|
3570
|
+
}
|
|
3571
|
+
primary() {
|
|
3572
|
+
const tok = this.peek();
|
|
3573
|
+
if (tok.type === "LPAREN") {
|
|
3574
|
+
this.next();
|
|
3575
|
+
const node = this.orExpr();
|
|
3576
|
+
this.expect("RPAREN", '")"');
|
|
3577
|
+
return node;
|
|
3578
|
+
}
|
|
3579
|
+
if (tok.type === "STAR") {
|
|
3580
|
+
this.next();
|
|
3581
|
+
return { kind: "all" };
|
|
3582
|
+
}
|
|
3583
|
+
if (tok.type === "IDENT") {
|
|
3584
|
+
return this.atom();
|
|
3585
|
+
}
|
|
3586
|
+
throw new ParseError(tok.pos, `Expected a field, "(", "*", or NOT \u2014 got "${tok.text || "end of query"}"`);
|
|
3587
|
+
}
|
|
3588
|
+
atom() {
|
|
3589
|
+
const fieldTok = this.expect("IDENT", "a field name");
|
|
3590
|
+
const opTok = this.peek();
|
|
3591
|
+
if (opTok.type === "COLON") {
|
|
3592
|
+
this.next();
|
|
3593
|
+
const values = this.valueOrList();
|
|
3594
|
+
return { kind: "atom", field: fieldTok.text, op: ":", values, pos: fieldTok.pos };
|
|
3595
|
+
}
|
|
3596
|
+
if (opTok.type === "OP") {
|
|
3597
|
+
this.next();
|
|
3598
|
+
const value = this.value();
|
|
3599
|
+
return {
|
|
3600
|
+
kind: "atom",
|
|
3601
|
+
field: fieldTok.text,
|
|
3602
|
+
op: opTok.text,
|
|
3603
|
+
values: [value],
|
|
3604
|
+
pos: fieldTok.pos
|
|
3605
|
+
};
|
|
3606
|
+
}
|
|
3607
|
+
throw new ParseError(
|
|
3608
|
+
opTok.pos,
|
|
3609
|
+
`Expected ":" or a comparison operator after field "${fieldTok.text}"`
|
|
3610
|
+
);
|
|
3611
|
+
}
|
|
3612
|
+
valueOrList() {
|
|
3613
|
+
if (this.peek().type === "LPAREN") {
|
|
3614
|
+
this.next();
|
|
3615
|
+
const values = [this.value()];
|
|
3616
|
+
while (this.peek().type === "COMMA") {
|
|
3617
|
+
this.next();
|
|
3618
|
+
values.push(this.value());
|
|
3619
|
+
}
|
|
3620
|
+
this.expect("RPAREN", '")" to close the value list');
|
|
3621
|
+
return values;
|
|
3622
|
+
}
|
|
3623
|
+
return [this.value()];
|
|
3624
|
+
}
|
|
3625
|
+
value() {
|
|
3626
|
+
const tok = this.peek();
|
|
3627
|
+
if (!VALUE_TOKENS.has(tok.type)) {
|
|
3628
|
+
throw new ParseError(tok.pos, `Expected a value, got "${tok.text || tok.type}"`);
|
|
3629
|
+
}
|
|
3630
|
+
this.next();
|
|
3631
|
+
switch (tok.type) {
|
|
3632
|
+
case "STRING":
|
|
3633
|
+
return { type: "string", raw: tok.text, pos: tok.pos };
|
|
3634
|
+
case "NUMBER":
|
|
3635
|
+
return { type: "number", raw: tok.text, num: tok.num, pos: tok.pos };
|
|
3636
|
+
case "DATE":
|
|
3637
|
+
return { type: "date", raw: tok.text, pos: tok.pos };
|
|
3638
|
+
case "DURATION":
|
|
3639
|
+
return { type: "duration", raw: tok.text, num: tok.num, sign: tok.sign ?? 0, pos: tok.pos };
|
|
3640
|
+
default:
|
|
3641
|
+
return { type: "word", raw: tok.text, pos: tok.pos };
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
};
|
|
3645
|
+
}
|
|
3646
|
+
});
|
|
3647
|
+
|
|
3648
|
+
// src/utils/query/index.ts
|
|
3649
|
+
var init_query = __esm({
|
|
3650
|
+
"src/utils/query/index.ts"() {
|
|
3651
|
+
"use strict";
|
|
3652
|
+
init_evaluate();
|
|
3653
|
+
init_fields();
|
|
3654
|
+
init_parser3();
|
|
3655
|
+
init_parser3();
|
|
3656
|
+
init_evaluate();
|
|
3657
|
+
init_fields();
|
|
3658
|
+
}
|
|
3659
|
+
});
|
|
3660
|
+
|
|
3661
|
+
// src/lifecycle/derive.ts
|
|
3662
|
+
var derive_exports = {};
|
|
3663
|
+
__export(derive_exports, {
|
|
3664
|
+
DERIVE_FIELDS: () => DERIVE_FIELDS,
|
|
3665
|
+
deriveDimensions: () => deriveDimensions,
|
|
3666
|
+
validateDeriveCondition: () => validateDeriveCondition
|
|
3667
|
+
});
|
|
3668
|
+
function validateDeriveCondition(when) {
|
|
3669
|
+
if (when === "*") return null;
|
|
3670
|
+
const parsed = parseQuery(when);
|
|
3671
|
+
if (!parsed.ast) return parsed.errors[0]?.message ?? "unparseable condition";
|
|
3672
|
+
try {
|
|
3673
|
+
compileNode(parsed.ast, DERIVE_FIELDS);
|
|
3674
|
+
return null;
|
|
3675
|
+
} catch (err) {
|
|
3676
|
+
if (err instanceof CompileError) return err.errors[0]?.message ?? "invalid condition";
|
|
3677
|
+
throw err;
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
function compiledWhen(derive, when) {
|
|
3681
|
+
let cache = conditionCache.get(derive);
|
|
3682
|
+
if (!cache) {
|
|
3683
|
+
cache = /* @__PURE__ */ new Map();
|
|
3684
|
+
conditionCache.set(derive, cache);
|
|
3685
|
+
}
|
|
3686
|
+
let pred = cache.get(when);
|
|
3687
|
+
if (!pred) {
|
|
3688
|
+
if (when === "*") {
|
|
3689
|
+
pred = () => true;
|
|
3690
|
+
} else {
|
|
3691
|
+
const parsed = parseQuery(when);
|
|
3692
|
+
if (!parsed.ast) {
|
|
3693
|
+
throw new CompileError(parsed.errors);
|
|
3694
|
+
}
|
|
3695
|
+
pred = compileNode(parsed.ast, DERIVE_FIELDS);
|
|
3696
|
+
}
|
|
3697
|
+
cache.set(when, pred);
|
|
3698
|
+
}
|
|
3699
|
+
return pred;
|
|
3700
|
+
}
|
|
3701
|
+
function deriveDimensions(input) {
|
|
3702
|
+
const { facts, derive, currentStatus, terminalStatuses, knownStatusIds, override } = input;
|
|
3703
|
+
if (terminalStatuses.has(currentStatus)) return null;
|
|
3704
|
+
const ctx = { now: 0 };
|
|
3705
|
+
const item = facts;
|
|
3706
|
+
let phase = derive.phaseLadder[0]?.phase ?? currentStatus;
|
|
3707
|
+
let nextAction = derive.phaseLadder[0]?.next ?? null;
|
|
3708
|
+
for (let i = derive.phaseLadder.length - 1; i >= 0; i--) {
|
|
3709
|
+
const rung = derive.phaseLadder[i];
|
|
3710
|
+
if (compiledWhen(derive, rung.when)(item, ctx)) {
|
|
3711
|
+
phase = rung.phase;
|
|
3712
|
+
nextAction = rung.next ?? null;
|
|
3713
|
+
break;
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
let disposition = "active";
|
|
3717
|
+
for (const rule of derive.disposition) {
|
|
3718
|
+
if (rule.when === null || compiledWhen(derive, rule.when)(item, ctx)) {
|
|
3719
|
+
disposition = rule.is;
|
|
3720
|
+
break;
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
let derivedStatus;
|
|
3724
|
+
switch (disposition) {
|
|
3725
|
+
case "parked":
|
|
3726
|
+
derivedStatus = knownStatusIds.has(derive.headline.parked) ? derive.headline.parked : phase;
|
|
3727
|
+
break;
|
|
3728
|
+
case "blocked":
|
|
3729
|
+
derivedStatus = knownStatusIds.has(derive.headline.blocked) ? derive.headline.blocked : phase;
|
|
3730
|
+
break;
|
|
3731
|
+
default:
|
|
3732
|
+
derivedStatus = phase;
|
|
3733
|
+
}
|
|
3734
|
+
let status = derivedStatus;
|
|
3735
|
+
if (override && override.status && !terminalStatuses.has(override.status) && knownStatusIds.has(override.status)) {
|
|
3736
|
+
status = override.status;
|
|
3737
|
+
}
|
|
3738
|
+
return { phase, disposition, derivedStatus, status, nextAction };
|
|
3739
|
+
}
|
|
3740
|
+
var DERIVE_FIELDS, conditionCache;
|
|
3741
|
+
var init_derive = __esm({
|
|
3742
|
+
"src/lifecycle/derive.ts"() {
|
|
3743
|
+
"use strict";
|
|
3744
|
+
init_query();
|
|
3745
|
+
DERIVE_FIELDS = {
|
|
3746
|
+
hasrealobjective: { kind: "bool", get: (i) => i["hasRealObjective"] },
|
|
3747
|
+
acrealtotal: { kind: "number", get: (i) => i["acRealTotal"] },
|
|
3748
|
+
acrealchecked: { kind: "number", get: (i) => i["acRealChecked"] },
|
|
3749
|
+
acallchecked: { kind: "bool", get: (i) => i["acAllChecked"] },
|
|
3750
|
+
planexists: { kind: "bool", get: (i) => i["planExists"] },
|
|
3751
|
+
planapproved: { kind: "bool", get: (i) => i["planApproved"] },
|
|
3752
|
+
workspaceset: { kind: "bool", get: (i) => i["workspaceSet"] },
|
|
3753
|
+
implementationstarted: { kind: "bool", get: (i) => i["implementationStarted"] },
|
|
3754
|
+
depssatisfied: { kind: "bool", get: (i) => i["depsSatisfied"] },
|
|
3755
|
+
unresolvedquestions: { kind: "number", get: (i) => i["unresolvedQuestions"] },
|
|
3756
|
+
blocked: { kind: "bool" },
|
|
3757
|
+
parked: { kind: "bool" },
|
|
3758
|
+
reviewrequested: { kind: "bool", get: (i) => i["reviewRequested"] },
|
|
3759
|
+
pinned: { kind: "bool" }
|
|
3760
|
+
};
|
|
3761
|
+
conditionCache = /* @__PURE__ */ new WeakMap();
|
|
3762
|
+
}
|
|
3763
|
+
});
|
|
3764
|
+
|
|
1968
3765
|
// src/dashboard/api.ts
|
|
1969
|
-
import { readdir as
|
|
1970
|
-
import { resolve as
|
|
3766
|
+
import { readdir as readdir7, readFile as readFile10, writeFile as writeFile3 } from "fs/promises";
|
|
3767
|
+
import { resolve as resolve12, dirname as dirname2, basename } from "path";
|
|
1971
3768
|
function activeAssignments(items) {
|
|
1972
3769
|
return items.filter((item) => item.archived !== true);
|
|
1973
3770
|
}
|
|
@@ -1987,15 +3784,15 @@ async function listStandaloneRecords(assignmentsDir) {
|
|
|
1987
3784
|
async function computeStandaloneRecords(assignmentsDir) {
|
|
1988
3785
|
if (!assignmentsDir) return [];
|
|
1989
3786
|
if (!await fileExists(assignmentsDir)) return [];
|
|
1990
|
-
const entries = await
|
|
3787
|
+
const entries = await readdir7(assignmentsDir, { withFileTypes: true });
|
|
1991
3788
|
const records = [];
|
|
1992
3789
|
for (const entry of entries) {
|
|
1993
3790
|
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
|
|
1994
|
-
const assignmentDir =
|
|
1995
|
-
const assignmentMdPath =
|
|
3791
|
+
const assignmentDir = resolve12(assignmentsDir, entry.name);
|
|
3792
|
+
const assignmentMdPath = resolve12(assignmentDir, "assignment.md");
|
|
1996
3793
|
if (!await fileExists(assignmentMdPath)) continue;
|
|
1997
3794
|
try {
|
|
1998
|
-
const content = await
|
|
3795
|
+
const content = await readFile10(assignmentMdPath, "utf-8");
|
|
1999
3796
|
const record = parseAssignmentFull(content);
|
|
2000
3797
|
records.push({ assignmentDir, id: entry.name, record });
|
|
2001
3798
|
} catch {
|
|
@@ -2036,7 +3833,8 @@ async function getStatusConfig() {
|
|
|
2036
3833
|
order: config.statuses.order,
|
|
2037
3834
|
transitions: effectiveTransitions,
|
|
2038
3835
|
transitionTable: buildTransitionTable(effectiveTransitions),
|
|
2039
|
-
terminalStatuses: terminalSet.size > 0 ? terminalSet : /* @__PURE__ */ new Set(["completed", "failed"])
|
|
3836
|
+
terminalStatuses: terminalSet.size > 0 ? terminalSet : /* @__PURE__ */ new Set(["completed", "failed"]),
|
|
3837
|
+
derive: config.statuses.derive ?? null
|
|
2040
3838
|
};
|
|
2041
3839
|
} else {
|
|
2042
3840
|
const def = buildDefaultStatusConfig();
|
|
@@ -2046,7 +3844,8 @@ async function getStatusConfig() {
|
|
|
2046
3844
|
order: def.order,
|
|
2047
3845
|
transitions: def.transitions,
|
|
2048
3846
|
transitionTable: DEFAULT_TRANSITION_TABLE,
|
|
2049
|
-
terminalStatuses: /* @__PURE__ */ new Set(["completed", "failed"])
|
|
3847
|
+
terminalStatuses: /* @__PURE__ */ new Set(["completed", "failed"]),
|
|
3848
|
+
derive: null
|
|
2050
3849
|
};
|
|
2051
3850
|
}
|
|
2052
3851
|
return _cachedConfig;
|
|
@@ -2076,23 +3875,23 @@ async function getStandaloneAvailableTransitions(assignment) {
|
|
|
2076
3875
|
return actions;
|
|
2077
3876
|
}
|
|
2078
3877
|
async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
2079
|
-
const assignmentDir =
|
|
2080
|
-
const assignmentMdPath =
|
|
3878
|
+
const assignmentDir = resolve12(projectsDir, projectSlug, "assignments", assignmentSlug);
|
|
3879
|
+
const assignmentMdPath = resolve12(assignmentDir, "assignment.md");
|
|
2081
3880
|
if (!await fileExists(assignmentMdPath)) {
|
|
2082
3881
|
return null;
|
|
2083
3882
|
}
|
|
2084
|
-
const assignmentContent = await
|
|
3883
|
+
const assignmentContent = await readFile10(assignmentMdPath, "utf-8");
|
|
2085
3884
|
const assignment = parseAssignmentFull(assignmentContent);
|
|
2086
3885
|
let projectWorkspace = null;
|
|
2087
|
-
const projectMdPath =
|
|
3886
|
+
const projectMdPath = resolve12(projectsDir, projectSlug, "project.md");
|
|
2088
3887
|
if (await fileExists(projectMdPath)) {
|
|
2089
|
-
const projectContent = await
|
|
3888
|
+
const projectContent = await readFile10(projectMdPath, "utf-8");
|
|
2090
3889
|
projectWorkspace = parseProject(projectContent).workspace;
|
|
2091
3890
|
}
|
|
2092
3891
|
let plan = null;
|
|
2093
|
-
const planPath =
|
|
3892
|
+
const planPath = resolve12(assignmentDir, "plan.md");
|
|
2094
3893
|
if (await fileExists(planPath)) {
|
|
2095
|
-
const planContent = await
|
|
3894
|
+
const planContent = await readFile10(planPath, "utf-8");
|
|
2096
3895
|
const parsed = parsePlan(planContent);
|
|
2097
3896
|
plan = {
|
|
2098
3897
|
status: parsed.status,
|
|
@@ -2101,9 +3900,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2101
3900
|
};
|
|
2102
3901
|
}
|
|
2103
3902
|
let scratchpad = null;
|
|
2104
|
-
const scratchpadPath =
|
|
3903
|
+
const scratchpadPath = resolve12(assignmentDir, "scratchpad.md");
|
|
2105
3904
|
if (await fileExists(scratchpadPath)) {
|
|
2106
|
-
const scratchpadContent = await
|
|
3905
|
+
const scratchpadContent = await readFile10(scratchpadPath, "utf-8");
|
|
2107
3906
|
const parsed = parseScratchpad(scratchpadContent);
|
|
2108
3907
|
scratchpad = {
|
|
2109
3908
|
updated: parsed.updated,
|
|
@@ -2111,9 +3910,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2111
3910
|
};
|
|
2112
3911
|
}
|
|
2113
3912
|
let handoff = null;
|
|
2114
|
-
const handoffPath =
|
|
3913
|
+
const handoffPath = resolve12(assignmentDir, "handoff.md");
|
|
2115
3914
|
if (await fileExists(handoffPath)) {
|
|
2116
|
-
const handoffContent = await
|
|
3915
|
+
const handoffContent = await readFile10(handoffPath, "utf-8");
|
|
2117
3916
|
const parsed = parseHandoff(handoffContent);
|
|
2118
3917
|
handoff = {
|
|
2119
3918
|
updated: parsed.updated,
|
|
@@ -2122,9 +3921,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2122
3921
|
};
|
|
2123
3922
|
}
|
|
2124
3923
|
let decisionRecord = null;
|
|
2125
|
-
const decisionRecordPath =
|
|
3924
|
+
const decisionRecordPath = resolve12(assignmentDir, "decision-record.md");
|
|
2126
3925
|
if (await fileExists(decisionRecordPath)) {
|
|
2127
|
-
const decisionRecordContent = await
|
|
3926
|
+
const decisionRecordContent = await readFile10(decisionRecordPath, "utf-8");
|
|
2128
3927
|
const parsed = parseDecisionRecord(decisionRecordContent);
|
|
2129
3928
|
decisionRecord = {
|
|
2130
3929
|
updated: parsed.updated,
|
|
@@ -2133,9 +3932,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2133
3932
|
};
|
|
2134
3933
|
}
|
|
2135
3934
|
let progress = null;
|
|
2136
|
-
const progressPath =
|
|
3935
|
+
const progressPath = resolve12(assignmentDir, "progress.md");
|
|
2137
3936
|
if (await fileExists(progressPath)) {
|
|
2138
|
-
const progressContent = await
|
|
3937
|
+
const progressContent = await readFile10(progressPath, "utf-8");
|
|
2139
3938
|
const parsed = parseProgress(progressContent);
|
|
2140
3939
|
progress = {
|
|
2141
3940
|
updated: parsed.updated,
|
|
@@ -2144,9 +3943,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2144
3943
|
};
|
|
2145
3944
|
}
|
|
2146
3945
|
let comments = null;
|
|
2147
|
-
const commentsPath =
|
|
3946
|
+
const commentsPath = resolve12(assignmentDir, "comments.md");
|
|
2148
3947
|
if (await fileExists(commentsPath)) {
|
|
2149
|
-
const commentsContent = await
|
|
3948
|
+
const commentsContent = await readFile10(commentsPath, "utf-8");
|
|
2150
3949
|
const parsed = parseComments(commentsContent);
|
|
2151
3950
|
comments = {
|
|
2152
3951
|
updated: parsed.updated,
|
|
@@ -2154,6 +3953,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2154
3953
|
entries: parsed.entries
|
|
2155
3954
|
};
|
|
2156
3955
|
}
|
|
3956
|
+
const { terminalStatuses } = await getStatusConfig();
|
|
2157
3957
|
const detail = {
|
|
2158
3958
|
id: assignment.id,
|
|
2159
3959
|
projectSlug,
|
|
@@ -2175,6 +3975,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
2175
3975
|
archived: assignment.archived,
|
|
2176
3976
|
archivedAt: assignment.archivedAt,
|
|
2177
3977
|
archivedReason: assignment.archivedReason,
|
|
3978
|
+
...deriveStatusVirtuals(assignment, terminalStatuses),
|
|
3979
|
+
override: assignment.override,
|
|
3980
|
+
derived: await buildDerivedDetail(assignment, assignmentDir, resolve12(projectsDir, projectSlug)),
|
|
2178
3981
|
created: assignment.created,
|
|
2179
3982
|
updated: assignment.updated,
|
|
2180
3983
|
body: assignment.body,
|
|
@@ -2265,7 +4068,7 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
|
|
|
2265
4068
|
slug: a.slug,
|
|
2266
4069
|
title: a.title,
|
|
2267
4070
|
projectSlug: rec.summary.slug,
|
|
2268
|
-
assignmentDir:
|
|
4071
|
+
assignmentDir: resolve12(rec.projectPath, "assignments", a.slug)
|
|
2269
4072
|
});
|
|
2270
4073
|
}
|
|
2271
4074
|
}
|
|
@@ -2298,17 +4101,17 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
|
|
|
2298
4101
|
}
|
|
2299
4102
|
async function countMentionsInAssignment(sourceDir, target) {
|
|
2300
4103
|
const bodies = [];
|
|
2301
|
-
const assignmentMd =
|
|
4104
|
+
const assignmentMd = resolve12(sourceDir, "assignment.md");
|
|
2302
4105
|
if (await fileExists(assignmentMd)) {
|
|
2303
|
-
const content = await
|
|
4106
|
+
const content = await readFile10(assignmentMd, "utf-8");
|
|
2304
4107
|
const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
|
|
2305
4108
|
if (todosMatch) bodies.push(todosMatch[1]);
|
|
2306
4109
|
}
|
|
2307
4110
|
for (const filename of ["progress.md", "comments.md", "handoff.md"]) {
|
|
2308
|
-
const path =
|
|
4111
|
+
const path = resolve12(sourceDir, filename);
|
|
2309
4112
|
if (await fileExists(path)) {
|
|
2310
4113
|
try {
|
|
2311
|
-
bodies.push(await
|
|
4114
|
+
bodies.push(await readFile10(path, "utf-8"));
|
|
2312
4115
|
} catch {
|
|
2313
4116
|
}
|
|
2314
4117
|
}
|
|
@@ -2366,46 +4169,47 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir, id) {
|
|
|
2366
4169
|
}
|
|
2367
4170
|
async function buildStandaloneAssignmentDetail(resolved) {
|
|
2368
4171
|
const assignmentDir = resolved.assignmentDir;
|
|
2369
|
-
const assignmentMdPath =
|
|
4172
|
+
const assignmentMdPath = resolve12(assignmentDir, "assignment.md");
|
|
2370
4173
|
if (!await fileExists(assignmentMdPath)) return null;
|
|
2371
|
-
const assignmentContent = await
|
|
4174
|
+
const assignmentContent = await readFile10(assignmentMdPath, "utf-8");
|
|
2372
4175
|
const assignment = parseAssignmentFull(assignmentContent);
|
|
2373
4176
|
let plan = null;
|
|
2374
|
-
const planPath =
|
|
4177
|
+
const planPath = resolve12(assignmentDir, "plan.md");
|
|
2375
4178
|
if (await fileExists(planPath)) {
|
|
2376
|
-
const parsed = parsePlan(await
|
|
4179
|
+
const parsed = parsePlan(await readFile10(planPath, "utf-8"));
|
|
2377
4180
|
plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
|
|
2378
4181
|
}
|
|
2379
4182
|
let scratchpad = null;
|
|
2380
|
-
const scratchpadPath =
|
|
4183
|
+
const scratchpadPath = resolve12(assignmentDir, "scratchpad.md");
|
|
2381
4184
|
if (await fileExists(scratchpadPath)) {
|
|
2382
|
-
const parsed = parseScratchpad(await
|
|
4185
|
+
const parsed = parseScratchpad(await readFile10(scratchpadPath, "utf-8"));
|
|
2383
4186
|
scratchpad = { updated: parsed.updated, body: parsed.body };
|
|
2384
4187
|
}
|
|
2385
4188
|
let handoff = null;
|
|
2386
|
-
const handoffPath =
|
|
4189
|
+
const handoffPath = resolve12(assignmentDir, "handoff.md");
|
|
2387
4190
|
if (await fileExists(handoffPath)) {
|
|
2388
|
-
const parsed = parseHandoff(await
|
|
4191
|
+
const parsed = parseHandoff(await readFile10(handoffPath, "utf-8"));
|
|
2389
4192
|
handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
|
|
2390
4193
|
}
|
|
2391
4194
|
let decisionRecord = null;
|
|
2392
|
-
const decisionRecordPath =
|
|
4195
|
+
const decisionRecordPath = resolve12(assignmentDir, "decision-record.md");
|
|
2393
4196
|
if (await fileExists(decisionRecordPath)) {
|
|
2394
|
-
const parsed = parseDecisionRecord(await
|
|
4197
|
+
const parsed = parseDecisionRecord(await readFile10(decisionRecordPath, "utf-8"));
|
|
2395
4198
|
decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
|
|
2396
4199
|
}
|
|
2397
4200
|
let progress = null;
|
|
2398
|
-
const progressPath =
|
|
4201
|
+
const progressPath = resolve12(assignmentDir, "progress.md");
|
|
2399
4202
|
if (await fileExists(progressPath)) {
|
|
2400
|
-
const parsed = parseProgress(await
|
|
4203
|
+
const parsed = parseProgress(await readFile10(progressPath, "utf-8"));
|
|
2401
4204
|
progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
|
|
2402
4205
|
}
|
|
2403
4206
|
let comments = null;
|
|
2404
|
-
const commentsPath =
|
|
4207
|
+
const commentsPath = resolve12(assignmentDir, "comments.md");
|
|
2405
4208
|
if (await fileExists(commentsPath)) {
|
|
2406
|
-
const parsed = parseComments(await
|
|
4209
|
+
const parsed = parseComments(await readFile10(commentsPath, "utf-8"));
|
|
2407
4210
|
comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
|
|
2408
4211
|
}
|
|
4212
|
+
const { terminalStatuses } = await getStatusConfig();
|
|
2409
4213
|
const detail = {
|
|
2410
4214
|
id: assignment.id,
|
|
2411
4215
|
projectSlug: null,
|
|
@@ -2428,6 +4232,9 @@ async function buildStandaloneAssignmentDetail(resolved) {
|
|
|
2428
4232
|
archived: assignment.archived,
|
|
2429
4233
|
archivedAt: assignment.archivedAt,
|
|
2430
4234
|
archivedReason: assignment.archivedReason,
|
|
4235
|
+
...deriveStatusVirtuals(assignment, terminalStatuses),
|
|
4236
|
+
override: assignment.override,
|
|
4237
|
+
derived: await buildDerivedDetail(assignment, assignmentDir, null),
|
|
2431
4238
|
created: assignment.created,
|
|
2432
4239
|
updated: assignment.updated,
|
|
2433
4240
|
body: assignment.body,
|
|
@@ -2459,17 +4266,17 @@ async function computeProjectRecords(projectsDir, traces) {
|
|
|
2459
4266
|
await migrateLegacyProjectFiles(projectsDir);
|
|
2460
4267
|
await migrateLegacyArchivedProjects(projectsDir);
|
|
2461
4268
|
}
|
|
2462
|
-
const entries = await
|
|
4269
|
+
const entries = await readdir7(projectsDir, { withFileTypes: true });
|
|
2463
4270
|
const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
|
|
2464
4271
|
const maybeRecords = await Promise.all(
|
|
2465
4272
|
projectDirs.map(async (entry) => {
|
|
2466
|
-
const projectPath =
|
|
2467
|
-
const projectMdPath =
|
|
4273
|
+
const projectPath = resolve12(projectsDir, entry.name);
|
|
4274
|
+
const projectMdPath = resolve12(projectPath, "project.md");
|
|
2468
4275
|
if (!await fileExists(projectMdPath)) {
|
|
2469
4276
|
return null;
|
|
2470
4277
|
}
|
|
2471
4278
|
const t0 = traces ? performance.now() : 0;
|
|
2472
|
-
const projectContent = await
|
|
4279
|
+
const projectContent = await readFile10(projectMdPath, "utf-8");
|
|
2473
4280
|
const project = parseProject(projectContent);
|
|
2474
4281
|
if (traces) accumulatePhase(traces, "parse-project-md", performance.now() - t0);
|
|
2475
4282
|
const t1 = traces ? performance.now() : 0;
|
|
@@ -2510,20 +4317,20 @@ async function computeProjectRecords(projectsDir, traces) {
|
|
|
2510
4317
|
return records;
|
|
2511
4318
|
}
|
|
2512
4319
|
async function listAssignmentRecords(projectPath, traces) {
|
|
2513
|
-
const assignmentsDir =
|
|
4320
|
+
const assignmentsDir = resolve12(projectPath, "assignments");
|
|
2514
4321
|
if (!await fileExists(assignmentsDir)) {
|
|
2515
4322
|
return [];
|
|
2516
4323
|
}
|
|
2517
|
-
const entries = await
|
|
4324
|
+
const entries = await readdir7(assignmentsDir, { withFileTypes: true });
|
|
2518
4325
|
const dirEntries = entries.filter((entry) => entry.isDirectory());
|
|
2519
4326
|
const maybeRecords = await Promise.all(
|
|
2520
4327
|
dirEntries.map(async (entry) => {
|
|
2521
|
-
const assignmentMd =
|
|
4328
|
+
const assignmentMd = resolve12(assignmentsDir, entry.name, "assignment.md");
|
|
2522
4329
|
if (!await fileExists(assignmentMd)) {
|
|
2523
4330
|
return null;
|
|
2524
4331
|
}
|
|
2525
4332
|
const t0 = traces ? performance.now() : 0;
|
|
2526
|
-
const content = await
|
|
4333
|
+
const content = await readFile10(assignmentMd, "utf-8");
|
|
2527
4334
|
const parsed = parseAssignmentFull(content);
|
|
2528
4335
|
if (traces) accumulatePhase(traces, "read-assignment-md", performance.now() - t0);
|
|
2529
4336
|
return parsed;
|
|
@@ -2534,9 +4341,9 @@ async function listAssignmentRecords(projectPath, traces) {
|
|
|
2534
4341
|
return records;
|
|
2535
4342
|
}
|
|
2536
4343
|
async function loadDependencyGraph(projectPath, assignments) {
|
|
2537
|
-
const statusPath =
|
|
4344
|
+
const statusPath = resolve12(projectPath, "_status.md");
|
|
2538
4345
|
if (await fileExists(statusPath)) {
|
|
2539
|
-
const statusContent = await
|
|
4346
|
+
const statusContent = await readFile10(statusPath, "utf-8");
|
|
2540
4347
|
const parsed = parseStatus(statusContent);
|
|
2541
4348
|
const derivedGraph = extractMermaidGraph(parsed.body);
|
|
2542
4349
|
if (derivedGraph) {
|
|
@@ -2586,6 +4393,78 @@ async function buildProjectRollup(projectPath, project, assignments, traces) {
|
|
|
2586
4393
|
}
|
|
2587
4394
|
return { progress, needsAttention, status };
|
|
2588
4395
|
}
|
|
4396
|
+
function deriveStatusVirtuals(assignment, terminalStatuses) {
|
|
4397
|
+
const hist = assignment.statusHistory ?? [];
|
|
4398
|
+
let completedAt = null;
|
|
4399
|
+
if (terminalStatuses.has(assignment.status)) {
|
|
4400
|
+
for (const entry of hist) {
|
|
4401
|
+
if (entry.to === assignment.status) completedAt = entry.at;
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
let statusAge = null;
|
|
4405
|
+
for (let i = hist.length - 1; i >= 0; i--) {
|
|
4406
|
+
const entry = hist[i];
|
|
4407
|
+
if (entry.from !== entry.to || entry.from === null) {
|
|
4408
|
+
const t = Date.parse(entry.at);
|
|
4409
|
+
statusAge = Number.isNaN(t) ? null : Date.now() - t;
|
|
4410
|
+
break;
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
let phaseAge = null;
|
|
4414
|
+
for (let i = hist.length - 1; i >= 0; i--) {
|
|
4415
|
+
const entry = hist[i];
|
|
4416
|
+
if (entry.phaseTo !== void 0 && entry.phaseFrom !== entry.phaseTo) {
|
|
4417
|
+
const t = Date.parse(entry.at);
|
|
4418
|
+
phaseAge = Number.isNaN(t) ? null : Date.now() - t;
|
|
4419
|
+
break;
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
return {
|
|
4423
|
+
completedAt,
|
|
4424
|
+
statusAge,
|
|
4425
|
+
phaseAge,
|
|
4426
|
+
phase: assignment.phase,
|
|
4427
|
+
disposition: assignment.disposition,
|
|
4428
|
+
pinned: assignment.override !== null
|
|
4429
|
+
};
|
|
4430
|
+
}
|
|
4431
|
+
async function buildDerivedDetail(assignment, assignmentDir, projectDir) {
|
|
4432
|
+
const config = await getStatusConfig();
|
|
4433
|
+
if (config.terminalStatuses.has(assignment.status)) return null;
|
|
4434
|
+
try {
|
|
4435
|
+
const { computeFacts: computeFacts2 } = await Promise.resolve().then(() => (init_facts(), facts_exports));
|
|
4436
|
+
const { deriveDimensions: deriveDimensions2 } = await Promise.resolve().then(() => (init_derive(), derive_exports));
|
|
4437
|
+
const { DEFAULT_DERIVE_CONFIG: DEFAULT_DERIVE_CONFIG2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
|
|
4438
|
+
const facts = await computeFacts2({
|
|
4439
|
+
assignmentDir,
|
|
4440
|
+
frontmatter: {
|
|
4441
|
+
...assignment
|
|
4442
|
+
// AssignmentRecord ⊃ the fields computeFacts reads; statusHistory and
|
|
4443
|
+
// derived caches ride along untouched.
|
|
4444
|
+
},
|
|
4445
|
+
body: assignment.body,
|
|
4446
|
+
projectDir,
|
|
4447
|
+
terminalStatuses: config.terminalStatuses
|
|
4448
|
+
});
|
|
4449
|
+
const dims = deriveDimensions2({
|
|
4450
|
+
facts,
|
|
4451
|
+
derive: config.derive ?? DEFAULT_DERIVE_CONFIG2,
|
|
4452
|
+
currentStatus: assignment.status,
|
|
4453
|
+
terminalStatuses: config.terminalStatuses,
|
|
4454
|
+
knownStatusIds: new Set(config.statuses.map((s) => s.id)),
|
|
4455
|
+
override: assignment.override
|
|
4456
|
+
});
|
|
4457
|
+
if (!dims) return null;
|
|
4458
|
+
return {
|
|
4459
|
+
derivedStatus: dims.derivedStatus,
|
|
4460
|
+
nextAction: dims.nextAction,
|
|
4461
|
+
facts
|
|
4462
|
+
};
|
|
4463
|
+
} catch (err) {
|
|
4464
|
+
console.warn(`buildDerivedDetail failed for ${assignmentDir}:`, err);
|
|
4465
|
+
return null;
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
2589
4468
|
function buildDependencyGraph(assignments) {
|
|
2590
4469
|
const edges = [];
|
|
2591
4470
|
const usedStatuses = /* @__PURE__ */ new Set();
|
|
@@ -2616,7 +4495,7 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
|
|
|
2616
4495
|
const config = await getStatusConfig();
|
|
2617
4496
|
const transitionDefs = getTransitionDefinitions(config);
|
|
2618
4497
|
const actions = [];
|
|
2619
|
-
const projectPath =
|
|
4498
|
+
const projectPath = resolve12(projectsDir, projectSlug);
|
|
2620
4499
|
const traces = options?.traces;
|
|
2621
4500
|
for (const definition of transitionDefs) {
|
|
2622
4501
|
const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);
|
|
@@ -2664,12 +4543,12 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses, de
|
|
|
2664
4543
|
continue;
|
|
2665
4544
|
}
|
|
2666
4545
|
}
|
|
2667
|
-
const dependencyPath =
|
|
4546
|
+
const dependencyPath = resolve12(projectPath, "assignments", dependency, "assignment.md");
|
|
2668
4547
|
if (!await fileExists(dependencyPath)) {
|
|
2669
4548
|
unmet.push(`${dependency} (missing)`);
|
|
2670
4549
|
continue;
|
|
2671
4550
|
}
|
|
2672
|
-
const content = await
|
|
4551
|
+
const content = await readFile10(dependencyPath, "utf-8");
|
|
2673
4552
|
const parsed = parseAssignmentFull(content);
|
|
2674
4553
|
if (!terminals.has(parsed.status)) {
|
|
2675
4554
|
unmet.push(`${dependency} (${parsed.status})`);
|
|
@@ -2685,7 +4564,7 @@ function parseTimestamp(timestamp) {
|
|
|
2685
4564
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
2686
4565
|
}
|
|
2687
4566
|
async function countOpenQuestions(projectPath, assignmentSlug) {
|
|
2688
|
-
const commentsPath =
|
|
4567
|
+
const commentsPath = resolve12(
|
|
2689
4568
|
projectPath,
|
|
2690
4569
|
"assignments",
|
|
2691
4570
|
assignmentSlug,
|
|
@@ -2695,7 +4574,7 @@ async function countOpenQuestions(projectPath, assignmentSlug) {
|
|
|
2695
4574
|
return 0;
|
|
2696
4575
|
}
|
|
2697
4576
|
try {
|
|
2698
|
-
const content = await
|
|
4577
|
+
const content = await readFile10(commentsPath, "utf-8");
|
|
2699
4578
|
const parsed = parseComments(content);
|
|
2700
4579
|
return parsed.entries.filter(
|
|
2701
4580
|
(e) => e.type === "question" && e.resolved !== true
|
|
@@ -2951,7 +4830,7 @@ init_api();
|
|
|
2951
4830
|
init_agents_schema();
|
|
2952
4831
|
import { spawn } from "child_process";
|
|
2953
4832
|
import { mkdir as mkdir2, writeFile as writeFile4 } from "fs/promises";
|
|
2954
|
-
import { isAbsolute as isAbsolute3, resolve as
|
|
4833
|
+
import { isAbsolute as isAbsolute3, resolve as resolve13 } from "path";
|
|
2955
4834
|
|
|
2956
4835
|
// src/launch/cwd.ts
|
|
2957
4836
|
import { existsSync, statSync } from "fs";
|
|
@@ -3327,7 +5206,7 @@ async function executeLaunchPlan(plan, spawnFn = realSpawn) {
|
|
|
3327
5206
|
`Spawn failed: ${msg}. Verify the terminal is installed and on PATH.`
|
|
3328
5207
|
);
|
|
3329
5208
|
}
|
|
3330
|
-
await new Promise((
|
|
5209
|
+
await new Promise((resolve15, reject) => {
|
|
3331
5210
|
let settled = false;
|
|
3332
5211
|
let stderr = "";
|
|
3333
5212
|
const finishOk = () => {
|
|
@@ -3337,7 +5216,7 @@ async function executeLaunchPlan(plan, spawnFn = realSpawn) {
|
|
|
3337
5216
|
child.unref();
|
|
3338
5217
|
} catch {
|
|
3339
5218
|
}
|
|
3340
|
-
|
|
5219
|
+
resolve15();
|
|
3341
5220
|
};
|
|
3342
5221
|
const finishErr = (remediation) => {
|
|
3343
5222
|
if (settled) return;
|
|
@@ -3522,7 +5401,7 @@ function appleScriptString(value) {
|
|
|
3522
5401
|
init_paths();
|
|
3523
5402
|
init_fs();
|
|
3524
5403
|
import { fileURLToPath } from "url";
|
|
3525
|
-
import { dirname as dirname3, resolve as
|
|
5404
|
+
import { dirname as dirname3, resolve as resolve14, join as join3 } from "path";
|
|
3526
5405
|
import { realpathSync, readFileSync, mkdirSync } from "fs";
|
|
3527
5406
|
var NPX_PATTERNS = [
|
|
3528
5407
|
{ kind: "npm", re: /\/_npx\/([^/]+)\/node_modules(?:\/|$)/ },
|
|
@@ -3548,7 +5427,7 @@ function normalizeSlashes(p) {
|
|
|
3548
5427
|
}
|
|
3549
5428
|
function detectInstallKind(scriptUrl, opts = {}) {
|
|
3550
5429
|
const realpath = opts.realpath ?? realpathSync.native;
|
|
3551
|
-
const
|
|
5430
|
+
const readFile11 = opts.readFile ?? ((p) => readFileSync(p, "utf-8"));
|
|
3552
5431
|
const ua = opts.envUserAgent !== void 0 ? opts.envUserAgent : process.env.npm_config_user_agent ?? "";
|
|
3553
5432
|
const resolved = resolveScriptPath(scriptUrl, realpath);
|
|
3554
5433
|
if (resolved === null) {
|
|
@@ -3569,7 +5448,7 @@ function detectInstallKind(scriptUrl, opts = {}) {
|
|
|
3569
5448
|
const pkgJsonPath = join3(dir, "package.json");
|
|
3570
5449
|
let raw;
|
|
3571
5450
|
try {
|
|
3572
|
-
raw =
|
|
5451
|
+
raw = readFile11(pkgJsonPath);
|
|
3573
5452
|
} catch {
|
|
3574
5453
|
const parent2 = dirname3(dir);
|
|
3575
5454
|
if (parent2 === dir) break;
|
|
@@ -3601,7 +5480,7 @@ function extractNpxHash(scriptUrl, opts = {}) {
|
|
|
3601
5480
|
return null;
|
|
3602
5481
|
}
|
|
3603
5482
|
function nudgeStampDir() {
|
|
3604
|
-
return
|
|
5483
|
+
return resolve14(syntaurRoot(), "npx-handler-nudge");
|
|
3605
5484
|
}
|
|
3606
5485
|
function sanitizeHash(hash) {
|
|
3607
5486
|
return hash.replace(/[^A-Za-z0-9_-]/g, "_") || "_";
|