esedre 0.1.7 → 0.1.9
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/dist/esedre.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import fs3 from "node:fs";
|
|
|
9
9
|
import path3 from "node:path";
|
|
10
10
|
|
|
11
11
|
// src/types.ts
|
|
12
|
-
var CURRENT_ESEDRE_VERSION = "0.1.
|
|
12
|
+
var CURRENT_ESEDRE_VERSION = "0.1.9";
|
|
13
13
|
var EsedreConflictError = class extends Error {
|
|
14
14
|
constructor(ticketId, currentHash, lastHash) {
|
|
15
15
|
super(
|
|
@@ -539,12 +539,12 @@ async function generateProjectSnapshot(storage, projectCode, outputDir) {
|
|
|
539
539
|
}
|
|
540
540
|
|
|
541
541
|
// src/storage/filesystem.ts
|
|
542
|
-
function isProjectMatch(code,
|
|
542
|
+
function isProjectMatch(code, filter) {
|
|
543
|
+
if (!code || !filter) return false;
|
|
543
544
|
const f = filter.trim().toLowerCase();
|
|
544
545
|
const c = code.trim().toLowerCase();
|
|
545
|
-
|
|
546
|
-
if (
|
|
547
|
-
if ((f === "prof" || f === "core" || f === "pasrc") && (c === "prof" || c === "core")) return true;
|
|
546
|
+
if (c === f) return true;
|
|
547
|
+
if ((f === "profe" || f === "prof" || f === "core" || f === "pasrc") && (c === "profe" || c === "prof" || c === "core")) return true;
|
|
548
548
|
if ((f === "esedre" || f === "ese" || f === "docs") && (c === "esedre" || c === "docs")) return true;
|
|
549
549
|
if ((f === "alce" || f === "web") && (c === "alce" || c === "web")) return true;
|
|
550
550
|
return false;
|
|
@@ -572,6 +572,9 @@ function writeSafeFile(filePath, content) {
|
|
|
572
572
|
var FilesystemStorageAdapter = class {
|
|
573
573
|
workspaceRoot;
|
|
574
574
|
config;
|
|
575
|
+
isConfigExplicit = false;
|
|
576
|
+
lastConfigCheck = 0;
|
|
577
|
+
configTtlMs = 3e3;
|
|
575
578
|
duplicateProjectWarnings = [];
|
|
576
579
|
getDuplicateProjectWarnings() {
|
|
577
580
|
this.resolveProjectLocations();
|
|
@@ -581,13 +584,39 @@ var FilesystemStorageAdapter = class {
|
|
|
581
584
|
this.workspaceRoot = workspaceRoot || this.resolveWorkspaceRoot();
|
|
582
585
|
if (config) {
|
|
583
586
|
this.config = config;
|
|
587
|
+
this.isConfigExplicit = true;
|
|
588
|
+
this.lastConfigCheck = Date.now();
|
|
584
589
|
} else {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
+
this.config = {};
|
|
591
|
+
this.refreshConfig();
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
refreshConfig() {
|
|
595
|
+
try {
|
|
596
|
+
const discovered = findEsedreConfig(this.workspaceRoot);
|
|
597
|
+
this.config = discovered.config || {};
|
|
598
|
+
} catch {
|
|
599
|
+
this.config = {};
|
|
600
|
+
}
|
|
601
|
+
this.lastConfigCheck = Date.now();
|
|
602
|
+
}
|
|
603
|
+
ensureFreshConfig() {
|
|
604
|
+
if (this.isConfigExplicit && this.config.allowedProjects?.includes("*")) {
|
|
605
|
+
if (Date.now() - this.lastConfigCheck > this.configTtlMs) {
|
|
606
|
+
try {
|
|
607
|
+
const discovered = findEsedreConfig(this.workspaceRoot, { fallbackToGlobal: true });
|
|
608
|
+
this.config = {
|
|
609
|
+
...discovered.config,
|
|
610
|
+
allowedProjects: ["*"]
|
|
611
|
+
};
|
|
612
|
+
} catch {
|
|
613
|
+
}
|
|
614
|
+
this.lastConfigCheck = Date.now();
|
|
590
615
|
}
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (!this.isConfigExplicit && Date.now() - this.lastConfigCheck > this.configTtlMs) {
|
|
619
|
+
this.refreshConfig();
|
|
591
620
|
}
|
|
592
621
|
}
|
|
593
622
|
resolveWorkspaceRoot() {
|
|
@@ -606,6 +635,7 @@ var FilesystemStorageAdapter = class {
|
|
|
606
635
|
return process.cwd();
|
|
607
636
|
}
|
|
608
637
|
resolveProjectLocations() {
|
|
638
|
+
this.ensureFreshConfig();
|
|
609
639
|
const locations = [];
|
|
610
640
|
const seenCodes = /* @__PURE__ */ new Set();
|
|
611
641
|
const codeToSource = /* @__PURE__ */ new Map();
|
|
@@ -649,9 +679,6 @@ var FilesystemStorageAdapter = class {
|
|
|
649
679
|
if (fs3.existsSync(pJsonPath)) {
|
|
650
680
|
try {
|
|
651
681
|
desc = JSON.parse(fs3.readFileSync(pJsonPath, "utf-8"));
|
|
652
|
-
if (desc && !desc.slug) {
|
|
653
|
-
desc.slug = (desc.code || ent.name).toLowerCase();
|
|
654
|
-
}
|
|
655
682
|
} catch {
|
|
656
683
|
}
|
|
657
684
|
}
|
|
@@ -663,7 +690,7 @@ var FilesystemStorageAdapter = class {
|
|
|
663
690
|
try {
|
|
664
691
|
const hubProjs = JSON.parse(fs3.readFileSync(f, "utf-8"));
|
|
665
692
|
desc = hubProjs.find(
|
|
666
|
-
(p) => p.code.toLowerCase() === ent.name.toLowerCase()
|
|
693
|
+
(p) => p.code.toLowerCase() === ent.name.toLowerCase()
|
|
667
694
|
);
|
|
668
695
|
if (desc) break;
|
|
669
696
|
} catch {
|
|
@@ -675,7 +702,6 @@ var FilesystemStorageAdapter = class {
|
|
|
675
702
|
desc = {
|
|
676
703
|
id: locations.length + 1,
|
|
677
704
|
code: ent.name,
|
|
678
|
-
slug: ent.name.toLowerCase(),
|
|
679
705
|
name: ent.name,
|
|
680
706
|
description: `${ent.name} project`
|
|
681
707
|
};
|
|
@@ -727,7 +753,6 @@ var FilesystemStorageAdapter = class {
|
|
|
727
753
|
descs = [{
|
|
728
754
|
id: 1,
|
|
729
755
|
code: pCode,
|
|
730
|
-
slug: pCode.toLowerCase(),
|
|
731
756
|
name: pCode,
|
|
732
757
|
description: `${pCode} project`
|
|
733
758
|
}];
|
|
@@ -782,9 +807,6 @@ var FilesystemStorageAdapter = class {
|
|
|
782
807
|
if (fs3.existsSync(cand)) {
|
|
783
808
|
try {
|
|
784
809
|
desc = JSON.parse(fs3.readFileSync(cand, "utf-8"));
|
|
785
|
-
if (desc && !desc.slug) {
|
|
786
|
-
desc.slug = (desc.code || code).toLowerCase();
|
|
787
|
-
}
|
|
788
810
|
break;
|
|
789
811
|
} catch {
|
|
790
812
|
}
|
|
@@ -794,7 +816,6 @@ var FilesystemStorageAdapter = class {
|
|
|
794
816
|
desc = {
|
|
795
817
|
id: locations.length + 1,
|
|
796
818
|
code,
|
|
797
|
-
slug: code.toLowerCase(),
|
|
798
819
|
name: code,
|
|
799
820
|
description: `${code} project`
|
|
800
821
|
};
|
|
@@ -862,7 +883,6 @@ var FilesystemStorageAdapter = class {
|
|
|
862
883
|
descs = [{
|
|
863
884
|
id: 1,
|
|
864
885
|
code: this.config.projectCode,
|
|
865
|
-
slug: this.config.projectCode.toLowerCase(),
|
|
866
886
|
name: pName,
|
|
867
887
|
description: `${pName} project`
|
|
868
888
|
}];
|
|
@@ -894,7 +914,7 @@ var FilesystemStorageAdapter = class {
|
|
|
894
914
|
const cleanCode = rawCode;
|
|
895
915
|
const cleanName = input.name?.trim() || cleanCode;
|
|
896
916
|
const cleanDesc = input.description?.trim() || `${cleanName} project`;
|
|
897
|
-
const
|
|
917
|
+
const cleanLower = cleanCode.toLowerCase();
|
|
898
918
|
const colors2 = input.colors || {
|
|
899
919
|
badge: "border-cyan-500/30 bg-cyan-500/10 text-cyan-300",
|
|
900
920
|
dot: "bg-cyan-400",
|
|
@@ -978,7 +998,7 @@ ${formattedList}`
|
|
|
978
998
|
const normalizedTargetHub = path3.resolve(targetHub).toLowerCase();
|
|
979
999
|
const existingLocations = this.resolveProjectLocations();
|
|
980
1000
|
const existingConflict = existingLocations.find((l) => {
|
|
981
|
-
if (l.project.code.toLowerCase() !==
|
|
1001
|
+
if (l.project.code.toLowerCase() !== cleanLower) return false;
|
|
982
1002
|
if (l.sourceType === "hub" && l.hubDir) {
|
|
983
1003
|
return path3.resolve(l.hubDir).toLowerCase() !== normalizedTargetHub;
|
|
984
1004
|
}
|
|
@@ -996,7 +1016,7 @@ ${formattedList}`
|
|
|
996
1016
|
try {
|
|
997
1017
|
const entries = fs3.readdirSync(hubProjectsDir, { withFileTypes: true });
|
|
998
1018
|
const matchedDir = entries.find(
|
|
999
|
-
(e) => e.isDirectory() && e.name.toLowerCase() ===
|
|
1019
|
+
(e) => e.isDirectory() && e.name.toLowerCase() === cleanLower
|
|
1000
1020
|
);
|
|
1001
1021
|
if (matchedDir) {
|
|
1002
1022
|
resolvedDirName = matchedDir.name;
|
|
@@ -1024,7 +1044,6 @@ ${formattedList}`
|
|
|
1024
1044
|
const projectDesc2 = {
|
|
1025
1045
|
id: id2,
|
|
1026
1046
|
code: resolvedCode2,
|
|
1027
|
-
slug,
|
|
1028
1047
|
name: cleanName,
|
|
1029
1048
|
description: cleanDesc,
|
|
1030
1049
|
colors: colors2
|
|
@@ -1034,7 +1053,7 @@ ${formattedList}`
|
|
|
1034
1053
|
if (fs3.existsSync(hubProjectsFile)) {
|
|
1035
1054
|
try {
|
|
1036
1055
|
const arr = JSON.parse(fs3.readFileSync(hubProjectsFile, "utf-8"));
|
|
1037
|
-
const idx = arr.findIndex((p) => p.code.toLowerCase() ===
|
|
1056
|
+
const idx = arr.findIndex((p) => p.code.toLowerCase() === cleanLower);
|
|
1038
1057
|
if (idx >= 0) {
|
|
1039
1058
|
arr[idx] = projectDesc2;
|
|
1040
1059
|
} else {
|
|
@@ -1059,12 +1078,11 @@ ${formattedList}`
|
|
|
1059
1078
|
} catch {
|
|
1060
1079
|
}
|
|
1061
1080
|
}
|
|
1062
|
-
const resolvedCode = existingPJson?.code && existingPJson.code.toLowerCase() ===
|
|
1081
|
+
const resolvedCode = existingPJson?.code && existingPJson.code.toLowerCase() === cleanLower ? existingPJson.code : cleanCode;
|
|
1063
1082
|
const id = existingPJson?.id || 1;
|
|
1064
1083
|
const projectDesc = {
|
|
1065
1084
|
id,
|
|
1066
1085
|
code: resolvedCode,
|
|
1067
|
-
slug,
|
|
1068
1086
|
name: cleanName,
|
|
1069
1087
|
description: cleanDesc,
|
|
1070
1088
|
colors: colors2
|
|
@@ -1074,7 +1092,7 @@ ${formattedList}`
|
|
|
1074
1092
|
if (fs3.existsSync(projectsJsonPath)) {
|
|
1075
1093
|
try {
|
|
1076
1094
|
const arr = JSON.parse(fs3.readFileSync(projectsJsonPath, "utf-8"));
|
|
1077
|
-
const idx = arr.findIndex((p) => p.code.toLowerCase() ===
|
|
1095
|
+
const idx = arr.findIndex((p) => p.code.toLowerCase() === cleanLower);
|
|
1078
1096
|
if (idx >= 0) {
|
|
1079
1097
|
arr[idx] = projectDesc;
|
|
1080
1098
|
} else {
|
|
@@ -1094,7 +1112,7 @@ ${formattedList}`
|
|
|
1094
1112
|
if (prefixMatch) {
|
|
1095
1113
|
const code = prefixMatch[1].toLowerCase();
|
|
1096
1114
|
const num2 = parseInt(prefixMatch[2], 10);
|
|
1097
|
-
const loc = locations.find((l) => isProjectMatch(l.project.code,
|
|
1115
|
+
const loc = locations.find((l) => isProjectMatch(l.project.code, code));
|
|
1098
1116
|
if (!loc) return null;
|
|
1099
1117
|
const ticketDir = path3.join(loc.ticketsDir, String(num2));
|
|
1100
1118
|
const metaPath = path3.join(ticketDir, "meta.json");
|
|
@@ -1106,7 +1124,7 @@ ${formattedList}`
|
|
|
1106
1124
|
meta.type = meta.type || meta.category || "Feature";
|
|
1107
1125
|
meta.category = meta.type;
|
|
1108
1126
|
const mProj = (meta.project || "").toLowerCase();
|
|
1109
|
-
if (mProj && mProj !== code && mProj !== loc.project.
|
|
1127
|
+
if (mProj && mProj !== code && mProj !== loc.project.code.toLowerCase()) {
|
|
1110
1128
|
return null;
|
|
1111
1129
|
}
|
|
1112
1130
|
} catch {
|
|
@@ -1131,7 +1149,7 @@ ${formattedList}`
|
|
|
1131
1149
|
const metaRaw = fs3.readFileSync(metaPath, "utf-8");
|
|
1132
1150
|
const meta = JSON.parse(metaRaw);
|
|
1133
1151
|
const matchedLoc = locations.find(
|
|
1134
|
-
(l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project &&
|
|
1152
|
+
(l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project && l.project.code.toLowerCase() === meta.project.toLowerCase()
|
|
1135
1153
|
) || loc;
|
|
1136
1154
|
matches.push({ loc: matchedLoc, ticketDir, id: num });
|
|
1137
1155
|
} catch {
|
|
@@ -1144,7 +1162,7 @@ ${formattedList}`
|
|
|
1144
1162
|
if (this.config.projectCode) {
|
|
1145
1163
|
const prefLower = this.config.projectCode.toLowerCase();
|
|
1146
1164
|
const preferredMatch = matches.find(
|
|
1147
|
-
(m) => m.loc.project.code.toLowerCase() === prefLower
|
|
1165
|
+
(m) => m.loc.project.code.toLowerCase() === prefLower
|
|
1148
1166
|
);
|
|
1149
1167
|
if (preferredMatch) return preferredMatch;
|
|
1150
1168
|
}
|
|
@@ -1166,7 +1184,7 @@ ${formattedList}`
|
|
|
1166
1184
|
for (const loc of locations) {
|
|
1167
1185
|
if (filter?.project && filter.project !== "all") {
|
|
1168
1186
|
const pLower = filter.project.toLowerCase();
|
|
1169
|
-
if (!isProjectMatch(loc.project.code,
|
|
1187
|
+
if (!isProjectMatch(loc.project.code, filter.project)) {
|
|
1170
1188
|
continue;
|
|
1171
1189
|
}
|
|
1172
1190
|
}
|
|
@@ -1198,7 +1216,7 @@ ${formattedList}`
|
|
|
1198
1216
|
let effectiveLoc = loc;
|
|
1199
1217
|
if (loc.isSharedDir) {
|
|
1200
1218
|
const found = locations.find(
|
|
1201
|
-
(l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project &&
|
|
1219
|
+
(l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project && l.project.code.toLowerCase() === meta.project.toLowerCase()
|
|
1202
1220
|
);
|
|
1203
1221
|
if (found) effectiveLoc = found;
|
|
1204
1222
|
}
|
|
@@ -1206,7 +1224,7 @@ ${formattedList}`
|
|
|
1206
1224
|
meta.projectId = effectiveLoc.project.id;
|
|
1207
1225
|
if (filter?.project && filter.project !== "all") {
|
|
1208
1226
|
const pLower = filter.project.toLowerCase();
|
|
1209
|
-
if (!isProjectMatch(meta.project,
|
|
1227
|
+
if (!isProjectMatch(meta.project, filter.project)) {
|
|
1210
1228
|
continue;
|
|
1211
1229
|
}
|
|
1212
1230
|
}
|
|
@@ -1373,7 +1391,7 @@ ${formattedList}`
|
|
|
1373
1391
|
if (requestedIdentifier) {
|
|
1374
1392
|
const lower = String(requestedIdentifier).trim().toLowerCase();
|
|
1375
1393
|
targetLoc = locations.find(
|
|
1376
|
-
(l) => l.project.code?.toLowerCase() === lower ||
|
|
1394
|
+
(l) => l.project.code?.toLowerCase() === lower || String(l.project.id) === lower || l.project.name?.toLowerCase() === lower
|
|
1377
1395
|
);
|
|
1378
1396
|
if (!targetLoc) {
|
|
1379
1397
|
throw new Error(`Project '${requestedIdentifier}' is invalid or not registered in this workspace.`);
|
|
@@ -1410,7 +1428,62 @@ ${formattedList}`
|
|
|
1410
1428
|
projectId: targetLoc.project.id,
|
|
1411
1429
|
project: targetLoc.project.code
|
|
1412
1430
|
};
|
|
1413
|
-
const
|
|
1431
|
+
const rawDetail = (input.detailMarkdown || input.detail)?.trim();
|
|
1432
|
+
let detailMd;
|
|
1433
|
+
if (rawDetail) {
|
|
1434
|
+
if (/^#\s+[^\n]+/m.test(rawDetail)) {
|
|
1435
|
+
let processed = rawDetail.replace(/^#\s+[^\n]+/m, `# Ticket #${nextId}: ${meta.title}`);
|
|
1436
|
+
if (!/\*\*(?:Type|Category)\*\*:/i.test(processed)) {
|
|
1437
|
+
const metaBlock = `
|
|
1438
|
+
**Category**: ${meta.category}
|
|
1439
|
+
**Complexity**: ${meta.complexity}
|
|
1440
|
+
**Estimated Effort**: ${meta.estimatedEffort}
|
|
1441
|
+
`;
|
|
1442
|
+
processed = processed.replace(/^(# Ticket[^\n]+\n)/m, `$1${metaBlock}`);
|
|
1443
|
+
}
|
|
1444
|
+
if (!/(?:##|###)\s*(?:Summary|Rationale)/i.test(processed)) {
|
|
1445
|
+
const summaryBlock = `
|
|
1446
|
+
### Summary
|
|
1447
|
+
${input.summary || "Summary to be defined."}
|
|
1448
|
+
`;
|
|
1449
|
+
processed = processed.replace(/^((?:# Ticket[^\n]+\n)(?:\*\*[^\n]+\n)*)/m, `$1${summaryBlock}`);
|
|
1450
|
+
}
|
|
1451
|
+
detailMd = processed.endsWith("\n") ? processed : `${processed}
|
|
1452
|
+
`;
|
|
1453
|
+
} else {
|
|
1454
|
+
const hasSummary = /(?:##|###)\s*(?:Summary|Rationale)/i.test(rawDetail);
|
|
1455
|
+
const hasBreakdown = /(?:##|###)\s*Feature Breakdown/i.test(rawDetail);
|
|
1456
|
+
let body = "";
|
|
1457
|
+
if (!hasSummary) {
|
|
1458
|
+
body += `### Summary
|
|
1459
|
+
${input.summary || "Summary to be defined."}
|
|
1460
|
+
|
|
1461
|
+
`;
|
|
1462
|
+
}
|
|
1463
|
+
if (!hasBreakdown && !rawDetail.startsWith("#")) {
|
|
1464
|
+
body += `### Feature Breakdown
|
|
1465
|
+
${rawDetail}
|
|
1466
|
+
|
|
1467
|
+
### Technical Details & Architecture
|
|
1468
|
+
- Architecture specifications to be documented.
|
|
1469
|
+
|
|
1470
|
+
### Open Questions & Decisions
|
|
1471
|
+
- None recorded at initialization.
|
|
1472
|
+
`;
|
|
1473
|
+
} else {
|
|
1474
|
+
body += `${rawDetail}
|
|
1475
|
+
`;
|
|
1476
|
+
}
|
|
1477
|
+
detailMd = `# Ticket #${nextId}: ${meta.title}
|
|
1478
|
+
**Category**: ${meta.category}
|
|
1479
|
+
**Complexity**: ${meta.complexity}
|
|
1480
|
+
**Estimated Effort**: ${meta.estimatedEffort}
|
|
1481
|
+
|
|
1482
|
+
${body.trim()}
|
|
1483
|
+
`;
|
|
1484
|
+
}
|
|
1485
|
+
} else {
|
|
1486
|
+
detailMd = `# Ticket #${nextId}: ${meta.title}
|
|
1414
1487
|
**Category**: ${meta.category}
|
|
1415
1488
|
**Complexity**: ${meta.complexity}
|
|
1416
1489
|
**Estimated Effort**: ${meta.estimatedEffort}
|
|
@@ -1428,6 +1501,7 @@ ${input.summary || "Summary to be defined."}
|
|
|
1428
1501
|
### Open Questions & Decisions
|
|
1429
1502
|
- None recorded at initialization.
|
|
1430
1503
|
`;
|
|
1504
|
+
}
|
|
1431
1505
|
writeSafeFile(path3.join(ticketDir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
|
|
1432
1506
|
writeSafeFile(path3.join(ticketDir, "detail.md"), detailMd);
|
|
1433
1507
|
const created = await this.getTicket(`${targetLoc.project.code}-${nextId}`);
|
|
@@ -2048,6 +2122,7 @@ Status: ${ticket.meta.status}`;
|
|
|
2048
2122
|
complexity: { type: "string", description: "Complexity (e.g. Low, Medium, High)" },
|
|
2049
2123
|
effort: { type: "string", description: "Estimated effort (e.g. 2.0 - 4.0 hours)" },
|
|
2050
2124
|
summary: { type: "string", description: "Initial feature summary" },
|
|
2125
|
+
detail: { type: "string", description: "Initial specification, feature breakdown, or technical detail markdown" },
|
|
2051
2126
|
author: { type: "string", description: "Submitting author name" }
|
|
2052
2127
|
},
|
|
2053
2128
|
required: ["title"]
|
|
@@ -2138,6 +2213,8 @@ Status: ${ticket.meta.status}`;
|
|
|
2138
2213
|
complexity: args.complexity,
|
|
2139
2214
|
estimatedEffort: args.effort,
|
|
2140
2215
|
summary: args.summary,
|
|
2216
|
+
detail: args.detail || args.detailMarkdown,
|
|
2217
|
+
detailMarkdown: args.detailMarkdown || args.detail,
|
|
2141
2218
|
submittedBy: args.author || "Agent"
|
|
2142
2219
|
});
|
|
2143
2220
|
return created;
|
|
@@ -2579,11 +2656,13 @@ async function startDaemon(options = {}) {
|
|
|
2579
2656
|
if (!pid) {
|
|
2580
2657
|
throw new Error("Failed to spawn Esedre daemon background process.");
|
|
2581
2658
|
}
|
|
2659
|
+
const timeoutMs = process.platform === "win32" ? 1e4 : 5e3;
|
|
2660
|
+
const pingTimeout = process.platform === "win32" ? 500 : 300;
|
|
2582
2661
|
const startTime = Date.now();
|
|
2583
2662
|
let ready = false;
|
|
2584
|
-
while (Date.now() - startTime <
|
|
2585
|
-
await new Promise((r) => setTimeout(r,
|
|
2586
|
-
const ping = await pingDaemon(port,
|
|
2663
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2664
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
2665
|
+
const ping = await pingDaemon(port, pingTimeout);
|
|
2587
2666
|
if (ping.responding && ping.isEsedre) {
|
|
2588
2667
|
ready = true;
|
|
2589
2668
|
break;
|
|
@@ -2784,6 +2863,14 @@ exit /b %ERRORLEVEL%
|
|
|
2784
2863
|
`;
|
|
2785
2864
|
var WRAPPER_PS1 = `# Esedre Autonomous Ticketing & Project Planning Engine Wrapper (PowerShell)
|
|
2786
2865
|
$ErrorActionPreference = "Stop"
|
|
2866
|
+
|
|
2867
|
+
# Ensure UTF-8 console output and pipeline encoding on Windows
|
|
2868
|
+
try {
|
|
2869
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
2870
|
+
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
2871
|
+
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
2872
|
+
} catch {}
|
|
2873
|
+
|
|
2787
2874
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
2788
2875
|
|
|
2789
2876
|
if (Get-Command "ese" -ErrorAction SilentlyContinue) {
|
|
@@ -2847,6 +2934,12 @@ REM Ese CLI short alias wrapper for Esedre (Windows CMD)
|
|
|
2847
2934
|
call "%~dp0esedre.cmd" %*
|
|
2848
2935
|
`;
|
|
2849
2936
|
var ESE_WRAPPER_PS1 = `# Ese CLI short alias wrapper for Esedre (PowerShell)
|
|
2937
|
+
try {
|
|
2938
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
2939
|
+
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
2940
|
+
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
2941
|
+
} catch {}
|
|
2942
|
+
|
|
2850
2943
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
2851
2944
|
& (Join-Path $scriptDir "esedre.ps1") @args
|
|
2852
2945
|
exit $LASTEXITCODE
|
|
@@ -2858,7 +2951,9 @@ exec "$DIR/esedre" "$@"
|
|
|
2858
2951
|
`;
|
|
2859
2952
|
var HISTORIC_SKILL_HASHES = [
|
|
2860
2953
|
"8611116c276fdfb598b965c716b251ce7c77c7f7",
|
|
2861
|
-
"05f129fb6e06a18df00215af105567abcbf0bc67"
|
|
2954
|
+
"05f129fb6e06a18df00215af105567abcbf0bc67",
|
|
2955
|
+
"5d330ecf8fc96f383946e0351adb60378afa70f0",
|
|
2956
|
+
"23716c9db561e52e5115dc364fe154d3a7bc1e23"
|
|
2862
2957
|
];
|
|
2863
2958
|
var ESEDRE_SKILL_TEMPLATE = `---
|
|
2864
2959
|
name: esedre
|
|
@@ -2869,9 +2964,18 @@ description: Tooling and workflow reference for interacting with the Esedre deve
|
|
|
2869
2964
|
|
|
2870
2965
|
Esedre is the developer ticketing and LLM coding partner coordination platform. The engine codebase resides in the standalone repository \`aellinsar/esedre\` (\`../esedre\`), and operates on decoupled ticket repositories (such as \`aellinsar/esedre-data\` configured in \`.esedre/esedre.json\`). Interact with tickets via in-repo shell wrappers in \`.esedre/\` (\`.esedre/ese\`), or the global \`ese\` CLI.
|
|
2871
2966
|
|
|
2872
|
-
## 0.
|
|
2967
|
+
## 0. Authoritative Ingress Hierarchy & Zero-Latency Context (\`.esedre/snapshot.json\`)
|
|
2873
2968
|
|
|
2874
|
-
|
|
2969
|
+
All ticket discovery, inspection, and state management strictly follow this positive ingress hierarchy:
|
|
2970
|
+
|
|
2971
|
+
1. **Step 1 (Default for Read-Only Inspection) - Local Snapshot Projection**:
|
|
2972
|
+
Always read \`.esedre/snapshot.json\` in the workspace root first. It provides an immediate, zero-latency local projection containing all active and completed tickets, summaries, implementation plans, revision numbers, completion dates (\`completedAt\`), and staleness metrics (\`daysSinceUpdate\`).
|
|
2973
|
+
2. **Step 2 (Dynamic Queries & State Mutations) - Esedre MCP Tools**:
|
|
2974
|
+
Use first-class MCP tools when active in the session (\`esedre_list_tickets\`, \`esedre_get_ticket\`, \`esedre_get_plan\`, \`esedre_save_plan\`, \`esedre_update_ticket\`, \`esedre_add_comment\`).
|
|
2975
|
+
3. **Step 3 (Terminal & Script Fallback) - In-Repo CLI Wrapper**:
|
|
2976
|
+
Use \`.esedre/ese\` (\`.esedre/ese get\`, \`.esedre/ese plan\`, \`.esedre/ese update\`, \`.esedre/ese snapshot\`, \`.esedre/ese refresh\`).
|
|
2977
|
+
4. **Storage Boundary**:
|
|
2978
|
+
Backing ticket hubs (such as \`esedre-data\`) represent data storage managed by the engine. All agent interactions with roadmap tickets flow through the snapshot projection, MCP tools, or CLI wrappers.
|
|
2875
2979
|
|
|
2876
2980
|
## 1. Model Context Protocol (MCP) Tools
|
|
2877
2981
|
|
|
@@ -2883,7 +2987,7 @@ When Esedre MCP is active in your agent session (\`.agents/mcp_config.json\`), u
|
|
|
2883
2987
|
| \`esedre_get_ticket\` | Full specification, summary, comments, revision & sha1 | \`ticketId\` (numeric e.g. \`96\` or compound e.g. \`Profe-96\`) |
|
|
2884
2988
|
| \`esedre_get_plan\` | Active implementation plan markdown | \`ticketId\` (numeric or compound) |
|
|
2885
2989
|
| \`esedre_save_plan\` | Save implementation plan markdown with OCC | \`ticketId\`, \`planMarkdown\`, \`lastHash\` |
|
|
2886
|
-
| \`esedre_create_ticket\` | Mint a new ticket with auto sequential ID | \`title\` (max 48 chars), \`type\`, \`project\`, \`effort\`, \`summary\` |
|
|
2990
|
+
| \`esedre_create_ticket\` | Mint a new ticket with auto sequential ID | \`title\` (max 48 chars), \`type\`, \`project\`, \`effort\`, \`summary\`, \`detail\` |
|
|
2887
2991
|
| \`esedre_update_ticket\` | Update ticket attributes with OCC | \`ticketId\`, \`status\`, \`type\`, \`title\`, \`complexity\`, \`effort\`, \`inDevelopment\`, \`featureFlag\`, \`lastHash\` |
|
|
2888
2992
|
| \`esedre_add_comment\` | Append developer or agent comment | \`ticketId\`, \`text\`, \`author\` |
|
|
2889
2993
|
|
|
@@ -2910,7 +3014,7 @@ Both \`esedre\` and \`ese\` work interchangeably:
|
|
|
2910
3014
|
|
|
2911
3015
|
### Create a Ticket
|
|
2912
3016
|
\`\`\`bash
|
|
2913
|
-
.esedre/ese create --title "..." [-p|--project <code>] [-t|--type Feature] [--complexity Medium] [--effort "2.0 - 4.0 hours"] [--json]
|
|
3017
|
+
.esedre/ese create --title "..." [-p|--project <code>] [-t|--type Feature] [--complexity Medium] [--effort "2.0 - 4.0 hours"] [--detail "<md>"] [--file <path>] [--json]
|
|
2914
3018
|
\`\`\`
|
|
2915
3019
|
|
|
2916
3020
|
### Update Ticket Status & Attributes
|
|
@@ -2929,6 +3033,7 @@ Both \`esedre\` and \`ese\` work interchangeably:
|
|
|
2929
3033
|
### Regenerate Snapshot
|
|
2930
3034
|
\`\`\`bash
|
|
2931
3035
|
.esedre/ese snapshot
|
|
3036
|
+
.esedre/ese refresh
|
|
2932
3037
|
\`\`\`
|
|
2933
3038
|
|
|
2934
3039
|
### Server Lifecycle & Daemon Management
|
|
@@ -3228,7 +3333,6 @@ function configureWorkspace(targetDir, options = {}) {
|
|
|
3228
3333
|
const projDesc = {
|
|
3229
3334
|
id: 1,
|
|
3230
3335
|
code: projectCode.toUpperCase(),
|
|
3231
|
-
slug: projectCode.toLowerCase(),
|
|
3232
3336
|
name: projectName || projectCode.toUpperCase(),
|
|
3233
3337
|
description: `${projectName || projectCode.toUpperCase()} project`,
|
|
3234
3338
|
colors: {
|
|
@@ -3281,6 +3385,18 @@ function configureWorkspace(targetDir, options = {}) {
|
|
|
3281
3385
|
};
|
|
3282
3386
|
}
|
|
3283
3387
|
var initWorkspace = configureWorkspace;
|
|
3388
|
+
var ESEDRE_HUB_AGENTS_TEMPLATE = `# Agent Guidelines: esedre-data
|
|
3389
|
+
|
|
3390
|
+
## 1. Role & Storage Boundary
|
|
3391
|
+
- **Backing Storage Repository**: \`esedre-data\` is a dedicated, decoupled data hub storing raw tickets, metadata manifests, and implementation plans managed by the Esedre engine.
|
|
3392
|
+
- **Authorized Ingress Channels**: Ticket discovery, read-only inspection, status updates, and plan authoring strictly ingress through the Esedre application layer in consuming project workspaces:
|
|
3393
|
+
1. **Local Workspace Snapshot**: Check the consuming workspace's \`.esedre/snapshot.json\` first for instant, zero-latency read access to ticket summaries, plans, and statuses.
|
|
3394
|
+
2. **Esedre MCP Tools**: Use \`esedre_get_ticket\`, \`esedre_list_tickets\`, \`esedre_get_plan\`, \`esedre_save_plan\`, and \`esedre_update_ticket\`.
|
|
3395
|
+
3. **Esedre CLI**: Execute \`.esedre/ese get <id>\`, \`.esedre/ese list\`, or \`.esedre/ese plan\` from within the relevant project workspace.
|
|
3396
|
+
|
|
3397
|
+
## 2. Ingress Redirection Protocol
|
|
3398
|
+
When inspecting or managing tickets for any project (such as \`Profe\`, \`Esedre\`, or \`Alce\`), navigate to that project's workspace and utilize its local \`.esedre/snapshot.json\`, Esedre MCP tools, or \`.esedre/ese\` CLI wrapper.
|
|
3399
|
+
`;
|
|
3284
3400
|
function initHub(targetDir) {
|
|
3285
3401
|
const resolved = path6.resolve(targetDir);
|
|
3286
3402
|
if (!fs6.existsSync(resolved)) {
|
|
@@ -3298,9 +3414,16 @@ function initHub(targetDir) {
|
|
|
3298
3414
|
fs6.writeFileSync(projectsJsonPath, "[]\n", "utf-8");
|
|
3299
3415
|
projectsJsonCreated = true;
|
|
3300
3416
|
}
|
|
3417
|
+
const agentsMdPath = path6.join(resolved, "AGENTS.md");
|
|
3418
|
+
let agentsMdCreated = false;
|
|
3419
|
+
if (!fs6.existsSync(agentsMdPath)) {
|
|
3420
|
+
fs6.writeFileSync(agentsMdPath, ESEDRE_HUB_AGENTS_TEMPLATE, "utf-8");
|
|
3421
|
+
agentsMdCreated = true;
|
|
3422
|
+
}
|
|
3301
3423
|
return {
|
|
3302
3424
|
projectsJsonCreated,
|
|
3303
3425
|
projectsDirCreated,
|
|
3426
|
+
agentsMdCreated,
|
|
3304
3427
|
hubDir: resolved.replace(/\\/g, "/")
|
|
3305
3428
|
};
|
|
3306
3429
|
}
|
|
@@ -3401,16 +3524,30 @@ function sendJson(res, status, data) {
|
|
|
3401
3524
|
"Content-Type": "application/json; charset=utf-8",
|
|
3402
3525
|
"Access-Control-Allow-Origin": "*",
|
|
3403
3526
|
"Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
|
|
3404
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
3527
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, x-esedre-allowed-projects"
|
|
3405
3528
|
});
|
|
3406
3529
|
res.end(JSON.stringify(data));
|
|
3407
3530
|
}
|
|
3408
|
-
var cachedAllData =
|
|
3409
|
-
var cachedAllTimestamp = 0;
|
|
3531
|
+
var cachedAllData = /* @__PURE__ */ new Map();
|
|
3410
3532
|
var CACHE_TTL_MS = 5e3;
|
|
3411
3533
|
function invalidateApiCache() {
|
|
3412
|
-
cachedAllData
|
|
3413
|
-
|
|
3534
|
+
cachedAllData.clear();
|
|
3535
|
+
}
|
|
3536
|
+
function getRequestStorage(baseStorage, req, url) {
|
|
3537
|
+
const headerAllowed = req.headers["x-esedre-allowed-projects"];
|
|
3538
|
+
const queryAllowed = url.searchParams.get("allowedProjects");
|
|
3539
|
+
const rawAllowed = (typeof headerAllowed === "string" ? headerAllowed : Array.isArray(headerAllowed) ? headerAllowed[0] : null) || queryAllowed;
|
|
3540
|
+
if (rawAllowed && typeof rawAllowed === "string") {
|
|
3541
|
+
const list = rawAllowed.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3542
|
+
if (list.length > 0 && !list.includes("*")) {
|
|
3543
|
+
const sortedKey = [...list].sort().join(",");
|
|
3544
|
+
return {
|
|
3545
|
+
storage: new SecurityFilter(baseStorage, { allowedProjects: list }),
|
|
3546
|
+
cacheKey: sortedKey
|
|
3547
|
+
};
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
return { storage: baseStorage, cacheKey: "*" };
|
|
3414
3551
|
}
|
|
3415
3552
|
function createApiHandler(storage, workspaceRoot) {
|
|
3416
3553
|
return async (req, res) => {
|
|
@@ -3436,6 +3573,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3436
3573
|
if (pathname.startsWith("/esedre/api")) {
|
|
3437
3574
|
pathname = pathname.slice("/esedre".length);
|
|
3438
3575
|
}
|
|
3576
|
+
const { storage: reqStorage, cacheKey } = getRequestStorage(storage, req, url);
|
|
3439
3577
|
try {
|
|
3440
3578
|
if (req.method === "GET") {
|
|
3441
3579
|
if (pathname === "/api/ping") {
|
|
@@ -3443,7 +3581,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3443
3581
|
return true;
|
|
3444
3582
|
}
|
|
3445
3583
|
if (pathname === "/api/planning/projects") {
|
|
3446
|
-
const projects = await
|
|
3584
|
+
const projects = await reqStorage.getProjects();
|
|
3447
3585
|
sendJson(res, 200, projects);
|
|
3448
3586
|
return true;
|
|
3449
3587
|
}
|
|
@@ -3453,7 +3591,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3453
3591
|
const type = url.searchParams.get("type") || url.searchParams.get("category") || void 0;
|
|
3454
3592
|
const category = type;
|
|
3455
3593
|
const search = url.searchParams.get("search") || void 0;
|
|
3456
|
-
const tickets = await
|
|
3594
|
+
const tickets = await reqStorage.listTickets({
|
|
3457
3595
|
project: project === "all" ? void 0 : project,
|
|
3458
3596
|
status,
|
|
3459
3597
|
type,
|
|
@@ -3469,7 +3607,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3469
3607
|
sendJson(res, 400, { error: "Invalid ticket ID" });
|
|
3470
3608
|
return true;
|
|
3471
3609
|
}
|
|
3472
|
-
const ticket = await
|
|
3610
|
+
const ticket = await reqStorage.getTicket(idStr);
|
|
3473
3611
|
if (!ticket) {
|
|
3474
3612
|
sendJson(res, 400, { error: "Ticket not found" });
|
|
3475
3613
|
return true;
|
|
@@ -3479,11 +3617,12 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3479
3617
|
}
|
|
3480
3618
|
if (pathname === "/api/planning/all") {
|
|
3481
3619
|
const now = Date.now();
|
|
3482
|
-
|
|
3483
|
-
|
|
3620
|
+
const cached = cachedAllData.get(cacheKey);
|
|
3621
|
+
if (cached && now - cached.timestamp < CACHE_TTL_MS) {
|
|
3622
|
+
sendJson(res, 200, cached.data);
|
|
3484
3623
|
return true;
|
|
3485
3624
|
}
|
|
3486
|
-
const tickets = await
|
|
3625
|
+
const tickets = await reqStorage.listTickets({});
|
|
3487
3626
|
const metasMap = {};
|
|
3488
3627
|
const detailsMap = {};
|
|
3489
3628
|
const plansMap = {};
|
|
@@ -3513,7 +3652,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3513
3652
|
}
|
|
3514
3653
|
}
|
|
3515
3654
|
}
|
|
3516
|
-
const projects = await
|
|
3655
|
+
const projects = await reqStorage.getProjects();
|
|
3517
3656
|
const responsePayload = {
|
|
3518
3657
|
success: true,
|
|
3519
3658
|
projects,
|
|
@@ -3526,13 +3665,12 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3526
3665
|
planHistory: {},
|
|
3527
3666
|
ticketHistory: {}
|
|
3528
3667
|
};
|
|
3529
|
-
cachedAllData
|
|
3530
|
-
cachedAllTimestamp = now;
|
|
3668
|
+
cachedAllData.set(cacheKey, { data: responsePayload, timestamp: now });
|
|
3531
3669
|
sendJson(res, 200, responsePayload);
|
|
3532
3670
|
return true;
|
|
3533
3671
|
}
|
|
3534
3672
|
if (pathname === "/api/planning/metas") {
|
|
3535
|
-
const tickets = await
|
|
3673
|
+
const tickets = await reqStorage.listTickets({});
|
|
3536
3674
|
const metasMap = {};
|
|
3537
3675
|
for (const t of tickets) {
|
|
3538
3676
|
metasMap[String(t.meta.id)] = t.meta;
|
|
@@ -3541,7 +3679,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3541
3679
|
return true;
|
|
3542
3680
|
}
|
|
3543
3681
|
if (pathname === "/api/planning/details") {
|
|
3544
|
-
const tickets = await
|
|
3682
|
+
const tickets = await reqStorage.listTickets({});
|
|
3545
3683
|
const detailsMap = {};
|
|
3546
3684
|
for (const t of tickets) {
|
|
3547
3685
|
if (t.detail?.raw) {
|
|
@@ -3552,7 +3690,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3552
3690
|
return true;
|
|
3553
3691
|
}
|
|
3554
3692
|
if (pathname === "/api/planning/plans") {
|
|
3555
|
-
const tickets = await
|
|
3693
|
+
const tickets = await reqStorage.listTickets({});
|
|
3556
3694
|
const plansMap = {};
|
|
3557
3695
|
for (const t of tickets) {
|
|
3558
3696
|
if (t.planMarkdown) {
|
|
@@ -3563,7 +3701,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3563
3701
|
return true;
|
|
3564
3702
|
}
|
|
3565
3703
|
if (pathname === "/api/planning/comments") {
|
|
3566
|
-
const tickets = await
|
|
3704
|
+
const tickets = await reqStorage.listTickets({});
|
|
3567
3705
|
const commentsMap = {};
|
|
3568
3706
|
for (const t of tickets) {
|
|
3569
3707
|
commentsMap[String(t.meta.id)] = t.comments || [];
|
|
@@ -3593,7 +3731,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3593
3731
|
sendJson(res, 400, { error: "Project query parameter is required for snapshot generation (no default fallback)." });
|
|
3594
3732
|
return true;
|
|
3595
3733
|
}
|
|
3596
|
-
const snapshot = await generateProjectSnapshot(
|
|
3734
|
+
const snapshot = await generateProjectSnapshot(reqStorage, projectCode, workspaceRoot);
|
|
3597
3735
|
sendJson(res, 200, snapshot);
|
|
3598
3736
|
return true;
|
|
3599
3737
|
}
|
|
@@ -3602,7 +3740,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3602
3740
|
const body = await readJsonBody(req);
|
|
3603
3741
|
if (pathname === "/api/planning/tickets") {
|
|
3604
3742
|
invalidateApiCache();
|
|
3605
|
-
const created = await
|
|
3743
|
+
const created = await reqStorage.createTicket(body);
|
|
3606
3744
|
sendJson(res, 201, { ticketId: String(created.meta.id), meta: created.meta });
|
|
3607
3745
|
return true;
|
|
3608
3746
|
}
|
|
@@ -3623,7 +3761,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3623
3761
|
return true;
|
|
3624
3762
|
}
|
|
3625
3763
|
invalidateApiCache();
|
|
3626
|
-
const project = await
|
|
3764
|
+
const project = await reqStorage.registerProject({
|
|
3627
3765
|
code,
|
|
3628
3766
|
name,
|
|
3629
3767
|
description,
|
|
@@ -3636,27 +3774,27 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3636
3774
|
if (pathname === "/api/planning/plans") {
|
|
3637
3775
|
const { ticketId, planMarkdown, lastHash } = body;
|
|
3638
3776
|
invalidateApiCache();
|
|
3639
|
-
await
|
|
3777
|
+
await reqStorage.savePlan(ticketId, planMarkdown, lastHash);
|
|
3640
3778
|
sendJson(res, 200, { success: true });
|
|
3641
3779
|
return true;
|
|
3642
3780
|
}
|
|
3643
3781
|
if (pathname === "/api/planning/comments") {
|
|
3644
3782
|
const { ticketId, text, author } = body;
|
|
3645
3783
|
invalidateApiCache();
|
|
3646
|
-
await
|
|
3647
|
-
const ticket = await
|
|
3784
|
+
await reqStorage.addComment(ticketId, { author: author || "Developer", text });
|
|
3785
|
+
const ticket = await reqStorage.getTicket(ticketId);
|
|
3648
3786
|
sendJson(res, 200, { comments: ticket?.comments || [] });
|
|
3649
3787
|
return true;
|
|
3650
3788
|
}
|
|
3651
3789
|
if (pathname === "/api/planning/update-meta") {
|
|
3652
3790
|
const { ticketId, updates, lastHash } = body;
|
|
3653
|
-
const updated = await
|
|
3791
|
+
const updated = await reqStorage.updateTicket(ticketId, updates, lastHash);
|
|
3654
3792
|
sendJson(res, 200, { meta: updated.meta });
|
|
3655
3793
|
return true;
|
|
3656
3794
|
}
|
|
3657
3795
|
if (pathname === "/api/planning/toggle-flag") {
|
|
3658
3796
|
const { ticketId, flagged } = body;
|
|
3659
|
-
const updated = await
|
|
3797
|
+
const updated = await reqStorage.updateTicket(ticketId, {
|
|
3660
3798
|
featureFlag: flagged ? "chat_enhanced" : ""
|
|
3661
3799
|
});
|
|
3662
3800
|
sendJson(res, 200, { meta: updated.meta });
|
|
@@ -3665,7 +3803,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3665
3803
|
if (pathname === "/api/planning/details") {
|
|
3666
3804
|
const { ticketId, detailMarkdown, metaUpdates } = body;
|
|
3667
3805
|
if (metaUpdates) {
|
|
3668
|
-
await
|
|
3806
|
+
await reqStorage.updateTicket(ticketId, metaUpdates);
|
|
3669
3807
|
}
|
|
3670
3808
|
sendJson(res, 200, { success: true, detail: detailMarkdown });
|
|
3671
3809
|
return true;
|
|
@@ -3674,6 +3812,10 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
3674
3812
|
sendJson(res, 404, { error: `Endpoint not found: ${pathname}` });
|
|
3675
3813
|
return true;
|
|
3676
3814
|
} catch (err) {
|
|
3815
|
+
if (err.name === "EsedreAuthorizationError" || err.message?.includes("outside this workspace's authorized scope") || err.message?.includes("Access Denied")) {
|
|
3816
|
+
sendJson(res, 403, { error: err.message });
|
|
3817
|
+
return true;
|
|
3818
|
+
}
|
|
3677
3819
|
console.error("[Esedre API Error]:", err);
|
|
3678
3820
|
sendJson(res, 500, { error: err.message });
|
|
3679
3821
|
return true;
|
|
@@ -3847,7 +3989,7 @@ ${colors.bold}ROADMAP COMMANDS (Pair Programming & LLM Agents):${colors.reset}
|
|
|
3847
3989
|
${colors.bold}plan${colors.reset} <id> [--file <path> | --set "<markdown>"] [--last-hash <sha1>] [--json]
|
|
3848
3990
|
View or update implementation plan with optimistic concurrency control.
|
|
3849
3991
|
|
|
3850
|
-
${colors.bold}create${colors.reset} --title "..." [-p|--project <code>] [-t|--type <type>] [--complexity <c>] [--effort "<e>"] [--summary "<s>"] [--json]
|
|
3992
|
+
${colors.bold}create${colors.reset} --title "..." [-p|--project <code>] [-t|--type <type>] [--complexity <c>] [--effort "<e>"] [--summary "<s>"] [--detail "<md>"] [--file <path>] [--json]
|
|
3851
3993
|
Mint a new roadmap ticket with sequential numeric ID.
|
|
3852
3994
|
|
|
3853
3995
|
${colors.bold}update${colors.reset} <id> [-s|--status <status>] [-t|--type <type>] [--title "..."] [--complexity <c>] [--effort "<e>"] [--in-dev] [--flag <name>] [--last-hash <sha1>] [--force] [--json]
|
|
@@ -3856,7 +3998,7 @@ ${colors.bold}ROADMAP COMMANDS (Pair Programming & LLM Agents):${colors.reset}
|
|
|
3856
3998
|
${colors.bold}comment${colors.reset} <id> ["<text>"] [--text "..."] [--author "..."] [--json]
|
|
3857
3999
|
Append a research finding, test verification, or note to ticket history.
|
|
3858
4000
|
|
|
3859
|
-
${colors.bold}snapshot${colors.reset}
|
|
4001
|
+
${colors.bold}snapshot, refresh${colors.reset} [--project <code>] [--json]
|
|
3860
4002
|
Generate lean read-only projection snapshot (.esedre/snapshot.json) for zero-latency agent context.
|
|
3861
4003
|
|
|
3862
4004
|
${colors.bold}SERVICE DAEMON COMMANDS:${colors.reset}
|
|
@@ -3900,6 +4042,8 @@ ${colors.bold}OPTIONS:${colors.reset}
|
|
|
3900
4042
|
-t, --type <type> Ticket type ('Feature', 'Platform', 'Tools', 'Idea', 'Bug').
|
|
3901
4043
|
-s, --status <stat> Ticket status ('Planned', 'In Development', 'Completed', 'Rejected').
|
|
3902
4044
|
-q, --search <query> Case-insensitive substring search query.
|
|
4045
|
+
--detail "<md>" Specification markdown for ticket detail during create.
|
|
4046
|
+
--file <path> Path to markdown file for detail (create) or implementation plan (plan).
|
|
3903
4047
|
--json Output raw machine-readable JSON (strongly recommended for autonomous LLM coding agents).
|
|
3904
4048
|
--last-hash <hash> Optimistic concurrency control: last known sha1 hash of the ticket from 'get'.
|
|
3905
4049
|
--force Bypass optimistic concurrency last-hash conflict checks on writes.
|
|
@@ -4412,6 +4556,7 @@ ${colors.bold}Commands:${colors.reset}`);
|
|
|
4412
4556
|
}
|
|
4413
4557
|
return;
|
|
4414
4558
|
}
|
|
4559
|
+
case "refresh":
|
|
4415
4560
|
case "snapshot": {
|
|
4416
4561
|
const projectCode = flags["project"] || discovered.config?.projectCode;
|
|
4417
4562
|
if (!projectCode) {
|
|
@@ -4435,17 +4580,22 @@ ${colors.bold}Commands:${colors.reset}`);
|
|
|
4435
4580
|
return;
|
|
4436
4581
|
}
|
|
4437
4582
|
case "start": {
|
|
4438
|
-
printDuplicateProjectWarnings(storage, isJson);
|
|
4439
4583
|
const flagPort = flags["port"] ? parseInt(String(flags["port"]), 10) : void 0;
|
|
4440
4584
|
const ports = resolvePorts(discovered.config, { port: flagPort });
|
|
4441
4585
|
const port = ports.gateway;
|
|
4442
4586
|
const isForeground = Boolean(flags["foreground"] || flags["f"]);
|
|
4587
|
+
const serverConfig = {
|
|
4588
|
+
...discovered.config,
|
|
4589
|
+
allowedProjects: ["*"]
|
|
4590
|
+
};
|
|
4591
|
+
const serverStorage = new FilesystemStorageAdapter(discovered.workspaceRoot, serverConfig);
|
|
4592
|
+
printDuplicateProjectWarnings(serverStorage, isJson);
|
|
4443
4593
|
if (isForeground) {
|
|
4444
4594
|
const cluster = startGatewayCluster({
|
|
4445
4595
|
gatewayPort: ports.gateway,
|
|
4446
4596
|
uiPort: ports.ui,
|
|
4447
4597
|
apiPort: ports.api,
|
|
4448
|
-
storage,
|
|
4598
|
+
storage: serverStorage,
|
|
4449
4599
|
workspaceRoot: discovered.workspaceRoot
|
|
4450
4600
|
});
|
|
4451
4601
|
console.log(`${colors.bold}${colors.green}\u2714 Esedre Server active on http://localhost:${ports.gateway}${colors.reset}`);
|
|
@@ -4613,12 +4763,27 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
4613
4763
|
content = fs9.readFileSync(filePath, "utf-8");
|
|
4614
4764
|
}
|
|
4615
4765
|
await storage.savePlan(id, content, lastHash);
|
|
4616
|
-
|
|
4766
|
+
if (isJson) {
|
|
4767
|
+
const updated = await storage.getTicket(id);
|
|
4768
|
+
console.log(JSON.stringify({
|
|
4769
|
+
success: true,
|
|
4770
|
+
ticketId: id,
|
|
4771
|
+
project: updated?.projectDescriptor?.code || updated?.meta?.project || "UNASSIGNED",
|
|
4772
|
+
planMarkdown: content,
|
|
4773
|
+
sha1: updated?.sha1 || updated?.meta?.sha1
|
|
4774
|
+
}, null, 2));
|
|
4775
|
+
} else {
|
|
4776
|
+
console.log(`${colors.green}\u2714 Implementation plan saved for Ticket #${id}${colors.reset}`);
|
|
4777
|
+
}
|
|
4617
4778
|
return;
|
|
4618
4779
|
}
|
|
4619
4780
|
const plan = await storage.getPlan(id);
|
|
4620
4781
|
if (!plan) {
|
|
4621
|
-
|
|
4782
|
+
if (isJson) {
|
|
4783
|
+
console.log(JSON.stringify({ ticketId: id, planMarkdown: null }, null, 2));
|
|
4784
|
+
} else {
|
|
4785
|
+
console.log(`${colors.dim}No implementation plan found for Ticket #${id}.${colors.reset}`);
|
|
4786
|
+
}
|
|
4622
4787
|
return;
|
|
4623
4788
|
}
|
|
4624
4789
|
if (isJson) {
|
|
@@ -4651,6 +4816,21 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
4651
4816
|
console.error(`${colors.red}Error: ${val.error}${colors.reset}`);
|
|
4652
4817
|
process.exit(1);
|
|
4653
4818
|
}
|
|
4819
|
+
const detailArg = flags["detail"];
|
|
4820
|
+
const fileArg = flags["file"];
|
|
4821
|
+
let detailMarkdown = detailArg;
|
|
4822
|
+
if (fileArg) {
|
|
4823
|
+
const filePath = path9.resolve(fileArg);
|
|
4824
|
+
if (!fs9.existsSync(filePath)) {
|
|
4825
|
+
if (isJson) {
|
|
4826
|
+
console.error(JSON.stringify({ error: `Detail file "${fileArg}" not found` }));
|
|
4827
|
+
} else {
|
|
4828
|
+
console.error(`${colors.red}Error: Detail file "${fileArg}" not found.${colors.reset}`);
|
|
4829
|
+
}
|
|
4830
|
+
process.exit(1);
|
|
4831
|
+
}
|
|
4832
|
+
detailMarkdown = fs9.readFileSync(filePath, "utf-8");
|
|
4833
|
+
}
|
|
4654
4834
|
const created = await storage.createTicket({
|
|
4655
4835
|
title,
|
|
4656
4836
|
type,
|
|
@@ -4659,7 +4839,9 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
4659
4839
|
complexity,
|
|
4660
4840
|
estimatedEffort,
|
|
4661
4841
|
summary,
|
|
4662
|
-
submittedBy
|
|
4842
|
+
submittedBy,
|
|
4843
|
+
detail: detailMarkdown,
|
|
4844
|
+
detailMarkdown
|
|
4663
4845
|
});
|
|
4664
4846
|
if (isJson) {
|
|
4665
4847
|
console.log(JSON.stringify(created, null, 2));
|