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.7";
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, slug, filter) {
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
- const s = slug.trim().toLowerCase();
546
- if (c === f || s === f) return true;
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
- try {
586
- const discovered = findEsedreConfig(this.workspaceRoot);
587
- this.config = discovered.config || {};
588
- } catch {
589
- this.config = {};
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() || p.slug.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 slug = cleanCode.toLowerCase();
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() !== slug) return false;
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() === slug
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() === slug);
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() === slug ? existingPJson.code : cleanCode;
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() === slug);
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, l.project.slug, 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.slug.toLowerCase()) {
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 && (l.project.code.toLowerCase() === meta.project.toLowerCase() || l.project.slug.toLowerCase() === meta.project.toLowerCase())
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 || m.loc.project.slug.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, loc.project.slug, filter.project)) {
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 && (l.project.code.toLowerCase() === meta.project.toLowerCase() || l.project.slug.toLowerCase() === meta.project.toLowerCase())
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, effectiveLoc.project.slug, filter.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 || l.project.slug?.toLowerCase() === lower || String(l.project.id) === lower || l.project.name?.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 detailMd = `# Ticket #${nextId}: ${meta.title}
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 < 3500) {
2585
- await new Promise((r) => setTimeout(r, 100));
2586
- const ping = await pingDaemon(port, 150);
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. Instant Zero-Latency Context (\`.esedre/snapshot.json\`)
2967
+ ## 0. Authoritative Ingress Hierarchy & Zero-Latency Context (\`.esedre/snapshot.json\`)
2873
2968
 
2874
- Before querying tickets over the network or CLI, check \`.esedre/snapshot.json\` in the workspace root. It provides a local read-only projection containing all active and completed tickets, summaries, implementation plans, revision numbers, completion dates (\`completedAt\`), and staleness metrics (\`daysSinceUpdate\`).
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 = null;
3409
- var cachedAllTimestamp = 0;
3531
+ var cachedAllData = /* @__PURE__ */ new Map();
3410
3532
  var CACHE_TTL_MS = 5e3;
3411
3533
  function invalidateApiCache() {
3412
- cachedAllData = null;
3413
- cachedAllTimestamp = 0;
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 storage.getProjects();
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 storage.listTickets({
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 storage.getTicket(idStr);
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
- if (cachedAllData && now - cachedAllTimestamp < CACHE_TTL_MS) {
3483
- sendJson(res, 200, cachedAllData);
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 storage.listTickets({});
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 storage.getProjects();
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 = responsePayload;
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 storage.listTickets({});
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 storage.listTickets({});
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 storage.listTickets({});
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 storage.listTickets({});
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(storage, projectCode, workspaceRoot);
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 storage.createTicket(body);
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 storage.registerProject({
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 storage.savePlan(ticketId, planMarkdown, lastHash);
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 storage.addComment(ticketId, { author: author || "Developer", text });
3647
- const ticket = await storage.getTicket(ticketId);
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 storage.updateTicket(ticketId, updates, lastHash);
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 storage.updateTicket(ticketId, {
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 storage.updateTicket(ticketId, metaUpdates);
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} [--project <code>] [--json]
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
- console.log(`${colors.green}\u2714 Implementation plan saved for Ticket #${id}${colors.reset}`);
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
- console.log(`${colors.dim}No implementation plan found for Ticket #${id}.${colors.reset}`);
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));