esedre 1.1.0 → 1.3.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/dist/esedre.mjs CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // bin/esedre.ts
4
- import fs10 from "node:fs";
5
- import path10 from "node:path";
4
+ import fs11 from "node:fs";
5
+ import path11 from "node:path";
6
6
 
7
7
  // src/storage/filesystem.ts
8
8
  import fs3 from "node:fs";
9
9
  import path3 from "node:path";
10
- import os2 from "node:os";
10
+ import os3 from "node:os";
11
11
 
12
12
  // src/types.ts
13
- var CURRENT_ESEDRE_VERSION = "1.1.0";
13
+ var CURRENT_ESEDRE_VERSION = "1.3.0";
14
14
  var INVERSE_RELATIONS = {
15
15
  "relates-to": "relates-to",
16
16
  "blocks": "blocked-by",
@@ -20,6 +20,19 @@ var INVERSE_RELATIONS = {
20
20
  "duplicates": "duplicated-by",
21
21
  "duplicated-by": "duplicates"
22
22
  };
23
+ var DEFAULT_LEASE_TTL_MINUTES = 60;
24
+ var EsedreLeaseConflictError = class extends Error {
25
+ ticketKey;
26
+ activeLease;
27
+ constructor(ticketKey, activeLease) {
28
+ super(
29
+ `Lease Conflict on Ticket ${ticketKey}: currently leased by '${activeLease.holder}'${activeLease.hostname ? ` on ${activeLease.hostname}` : ""} until ${activeLease.expiresAt}. Pass --force (or force: true) to override.`
30
+ );
31
+ this.name = "EsedreLeaseConflictError";
32
+ this.ticketKey = ticketKey;
33
+ this.activeLease = activeLease;
34
+ }
35
+ };
23
36
  var EsedreConflictError = class extends Error {
24
37
  constructor(ticketId, currentHash, lastHash) {
25
38
  super(
@@ -35,10 +48,57 @@ var EsedreConflictError = class extends Error {
35
48
  lastHash;
36
49
  };
37
50
 
51
+ // src/utils/identity.ts
52
+ import os from "node:os";
53
+ import { execSync } from "node:child_process";
54
+ function detectGitBranch(cwd) {
55
+ try {
56
+ const out = execSync("git rev-parse --abbrev-ref HEAD", {
57
+ cwd: cwd || process.cwd(),
58
+ stdio: ["ignore", "pipe", "ignore"],
59
+ encoding: "utf-8",
60
+ timeout: 1500
61
+ }).trim();
62
+ return out && out !== "HEAD" ? out : void 0;
63
+ } catch {
64
+ return void 0;
65
+ }
66
+ }
67
+ function resolveWorkerIdentity(input) {
68
+ let hostname = input?.hostname?.trim() || "";
69
+ if (!hostname) {
70
+ try {
71
+ hostname = os.hostname() || "local-host";
72
+ } catch {
73
+ hostname = "local-host";
74
+ }
75
+ }
76
+ let username = "";
77
+ try {
78
+ username = os.userInfo().username || "";
79
+ } catch {
80
+ username = process.env.USER || process.env.USERNAME || "developer";
81
+ }
82
+ const envHolder = process.env.ESEDRE_WORKER_ID || process.env.ESEDRE_AGENT_ID || process.env.AGY_AGENT_ID || "";
83
+ const holder = input?.holder?.trim() || envHolder.trim() || input?.configDefaultAuthor?.trim() || `${username}@${hostname}`;
84
+ const agentType = input?.agentType?.trim() || process.env.ESEDRE_AGENT_TYPE?.trim() || (process.env.AGY_CONVERSATION_ID ? "antigravity" : void 0);
85
+ const sessionId = input?.sessionId?.trim() || process.env.ESEDRE_SESSION_ID?.trim() || process.env.AGY_CONVERSATION_ID?.trim() || void 0;
86
+ const worktree = input?.worktree?.trim() || process.cwd();
87
+ const branch = input?.branch?.trim() || detectGitBranch(worktree);
88
+ return {
89
+ holder,
90
+ ...agentType ? { agentType } : {},
91
+ hostname,
92
+ ...sessionId ? { sessionId } : {},
93
+ ...branch ? { branch } : {},
94
+ ...worktree ? { worktree } : {}
95
+ };
96
+ }
97
+
38
98
  // src/config.ts
39
99
  import fs from "node:fs";
40
100
  import path from "node:path";
41
- import os from "node:os";
101
+ import os2 from "node:os";
42
102
  var DEFAULT_ESEDRE_PORT = 5674;
43
103
  var MAX_PROJECT_CODE_LENGTH = 8;
44
104
  var PROJECT_CODE_REGEX = /^[a-zA-Z0-9]{1,8}$/;
@@ -113,7 +173,7 @@ function getGlobalConfigDir() {
113
173
  return process.env.ESEDRE_GLOBAL_DIR;
114
174
  }
115
175
  if (process.env.VITEST) {
116
- const testGlobalDir = path.join(os.tmpdir(), "esedre-vitest-global");
176
+ const testGlobalDir = path.join(os2.tmpdir(), "esedre-vitest-global");
117
177
  if (!fs.existsSync(testGlobalDir)) {
118
178
  try {
119
179
  fs.mkdirSync(testGlobalDir, { recursive: true });
@@ -122,7 +182,7 @@ function getGlobalConfigDir() {
122
182
  }
123
183
  return testGlobalDir;
124
184
  }
125
- return path.join(os.homedir(), ".esedre");
185
+ return path.join(os2.homedir(), ".esedre");
126
186
  }
127
187
  function getGlobalConfigPath() {
128
188
  return path.join(getGlobalConfigDir(), "config.json");
@@ -130,7 +190,7 @@ function getGlobalConfigPath() {
130
190
  function expandHome(filePath) {
131
191
  if (!filePath) return filePath;
132
192
  if (filePath === "~" || filePath.startsWith("~/") || filePath.startsWith("~\\")) {
133
- return path.join(os.homedir(), filePath.slice(1));
193
+ return path.join(os2.homedir(), filePath.slice(1));
134
194
  }
135
195
  return filePath;
136
196
  }
@@ -492,7 +552,8 @@ function migrateEsedreConfig(raw, targetVersion = CURRENT_ESEDRE_VERSION) {
492
552
  serverUrl: raw?.serverUrl,
493
553
  port: typeof raw?.port === "number" && raw.port !== DEFAULT_ESEDRE_PORT ? raw.port : void 0,
494
554
  ports: typeof raw?.ports === "object" && raw?.ports ? raw.ports : void 0,
495
- projects: typeof raw?.projects === "object" && raw?.projects ? raw.projects : void 0
555
+ projects: typeof raw?.projects === "object" && raw?.projects ? raw.projects : void 0,
556
+ defaultAuthor: typeof raw?.defaultAuthor === "string" && raw.defaultAuthor.trim() ? raw.defaultAuthor.trim() : void 0
496
557
  };
497
558
  return migrated;
498
559
  }
@@ -572,6 +633,7 @@ async function generateProjectSnapshot(storage, projectCode, outputDir) {
572
633
  inheritedFeatureFlag: ticketObj.meta.inheritedFeatureFlag,
573
634
  links: ticketObj.meta.links || [],
574
635
  isBlocked: ticketObj.isBlocked || false,
636
+ ...ticketObj.lease ? { lease: ticketObj.lease } : {},
575
637
  hasPlan: Boolean(plan && plan.trim().length > 0),
576
638
  planMarkdown: plan && plan.trim().length > 0 ? plan : void 0,
577
639
  createdAt,
@@ -607,13 +669,17 @@ async function generateProjectSnapshot(storage, projectCode, outputDir) {
607
669
  });
608
670
  } catch {
609
671
  }
672
+ const activeLeases = entries.map((e) => e.lease).filter((l) => Boolean(l));
673
+ const lastTicketId = entries.reduce((max, e) => Math.max(max, e.id), 0);
610
674
  const snapshot = {
611
675
  version: CURRENT_ESEDRE_VERSION,
612
676
  projectCode,
613
677
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
614
678
  totalTickets: entries.length,
679
+ lastTicketId,
615
680
  tickets: entries,
616
- ...milestoneSnapshots && milestoneSnapshots.length > 0 ? { milestones: milestoneSnapshots } : {}
681
+ ...milestoneSnapshots && milestoneSnapshots.length > 0 ? { milestones: milestoneSnapshots } : {},
682
+ ...activeLeases.length > 0 ? { activeLeases } : {}
617
683
  };
618
684
  const esedreDir = path2.join(outputDir, ".esedre");
619
685
  if (!fs2.existsSync(esedreDir)) {
@@ -801,6 +867,14 @@ function formatTicketDetail(ticket) {
801
867
  if (ticket.isBlocked) {
802
868
  lines.push(`${colors.bold}Dependency:${colors.reset} ${colors.red}${colors.bold}Blocked by uncompleted ticket${colors.reset}`);
803
869
  }
870
+ if (ticket.lease) {
871
+ const l = ticket.lease;
872
+ const hostInfo = l.hostname ? ` on ${l.hostname}` : "";
873
+ const branchInfo = l.branch ? ` [branch: ${l.branch}]` : "";
874
+ lines.push(
875
+ `${colors.bold}Active Lease:${colors.reset} ${colors.cyan}${colors.bold}${l.holder}${colors.reset}${hostInfo}${branchInfo} ${colors.dim}(expires ${l.expiresAt})${colors.reset}`
876
+ );
877
+ }
804
878
  if (detail?.summary) {
805
879
  lines.push("");
806
880
  lines.push(`${colors.bold}Summary:${colors.reset}`);
@@ -933,6 +1007,23 @@ function formatMilestoneListTable(milestones, tickets) {
933
1007
  });
934
1008
  return [header, divider, ...formattedRows].join("\n");
935
1009
  }
1010
+ function formatLeaseListTable(leases) {
1011
+ if (leases.length === 0) {
1012
+ return `${colors.dim}No active ticket leases.${colors.reset}`;
1013
+ }
1014
+ const lines = [];
1015
+ const header = `${pad("TICKET", 12)} ${pad("HOLDER", 22)} ${pad("HOST / AGENT", 22)} ${pad("BRANCH", 24)} ${pad("EXPIRES AT", 24)}`;
1016
+ lines.push(`${colors.bold}${colors.cyan}${header}${colors.reset}`);
1017
+ lines.push(`${colors.dim}${"-".repeat(108)}${colors.reset}`);
1018
+ for (const l of leases) {
1019
+ const hostAgent = [l.hostname, l.agentType].filter(Boolean).join(" / ") || "-";
1020
+ const branch = l.branch || "-";
1021
+ lines.push(
1022
+ `${colors.bold}${pad(l.ticketKey, 12)}${colors.reset} ${colors.green}${pad(l.holder.slice(0, 20), 22)}${colors.reset} ${pad(hostAgent.slice(0, 20), 22)} ${colors.cyan}${pad(branch.slice(0, 22), 24)}${colors.reset} ${colors.dim}${pad(l.expiresAt, 24)}${colors.reset}`
1023
+ );
1024
+ }
1025
+ return lines.join("\n");
1026
+ }
936
1027
 
937
1028
  // src/storage/filesystem.ts
938
1029
  function isProjectMatch(code, filter) {
@@ -1865,7 +1956,7 @@ ${formattedList}`
1865
1956
  } catch {
1866
1957
  }
1867
1958
  }
1868
- const globalConfigFile = path3.join(os2.homedir(), ".esedre", "config.json");
1959
+ const globalConfigFile = path3.join(os3.homedir(), ".esedre", "config.json");
1869
1960
  if (fs3.existsSync(globalConfigFile)) {
1870
1961
  try {
1871
1962
  const gCfg = JSON.parse(fs3.readFileSync(globalConfigFile, "utf-8"));
@@ -2202,7 +2293,8 @@ ${formattedList}`
2202
2293
  "implementation_plan.md",
2203
2294
  "comments.json",
2204
2295
  "answers.json",
2205
- "inline-comments.json"
2296
+ "inline-comments.json",
2297
+ "lease.json"
2206
2298
  ];
2207
2299
  const currentStats = {};
2208
2300
  for (const f of filesToCheck) {
@@ -2230,14 +2322,34 @@ ${formattedList}`
2230
2322
  }
2231
2323
  if (match) {
2232
2324
  let ticket = cached.ticket;
2233
- if (ticket.meta.links && ticket.meta.links.length > 0) {
2234
- ticket = {
2235
- ...ticket,
2236
- meta: { ...ticket.meta, links: ticket.meta.links.map((l) => ({ ...l })) }
2237
- };
2238
- this.enrichTicketLinks(ticket);
2325
+ if (ticket.lease) {
2326
+ const expMs = Date.parse(ticket.lease.expiresAt);
2327
+ if (!isNaN(expMs) && expMs <= Date.now()) {
2328
+ try {
2329
+ fs3.unlinkSync(path3.join(ticketDir, "lease.json"));
2330
+ } catch {
2331
+ }
2332
+ this.ticketMemoryCache.delete(resolvedTicketDir);
2333
+ } else {
2334
+ if (ticket.meta.links && ticket.meta.links.length > 0) {
2335
+ ticket = {
2336
+ ...ticket,
2337
+ meta: { ...ticket.meta, links: ticket.meta.links.map((l) => ({ ...l })) }
2338
+ };
2339
+ this.enrichTicketLinks(ticket);
2340
+ }
2341
+ return ticket;
2342
+ }
2343
+ } else {
2344
+ if (ticket.meta.links && ticket.meta.links.length > 0) {
2345
+ ticket = {
2346
+ ...ticket,
2347
+ meta: { ...ticket.meta, links: ticket.meta.links.map((l) => ({ ...l })) }
2348
+ };
2349
+ this.enrichTicketLinks(ticket);
2350
+ }
2351
+ return ticket;
2239
2352
  }
2240
- return ticket;
2241
2353
  }
2242
2354
  }
2243
2355
  }
@@ -2313,6 +2425,32 @@ ${formattedList}`
2313
2425
  } catch {
2314
2426
  }
2315
2427
  }
2428
+ let lease = null;
2429
+ const leasePath = path3.join(ticketDir, "lease.json");
2430
+ if (currentStats["lease.json"]) {
2431
+ try {
2432
+ const parsedLease = JSON.parse(fs3.readFileSync(leasePath, "utf-8"));
2433
+ const expMs = Date.parse(parsedLease.expiresAt);
2434
+ if (!isNaN(expMs) && expMs <= Date.now()) {
2435
+ try {
2436
+ fs3.unlinkSync(leasePath);
2437
+ delete currentStats["lease.json"];
2438
+ } catch {
2439
+ }
2440
+ } else {
2441
+ parsedLease.project = effectiveLoc.project.code;
2442
+ parsedLease.ticketId = meta.id;
2443
+ parsedLease.ticketKey = `${effectiveLoc.project.code}-${meta.id}`;
2444
+ lease = parsedLease;
2445
+ }
2446
+ } catch {
2447
+ }
2448
+ }
2449
+ if (lease) {
2450
+ meta.lease = lease;
2451
+ } else {
2452
+ delete meta.lease;
2453
+ }
2316
2454
  const ticketObj = {
2317
2455
  meta,
2318
2456
  detail,
@@ -2320,6 +2458,7 @@ ${formattedList}`
2320
2458
  comments,
2321
2459
  answers,
2322
2460
  inlineComments,
2461
+ lease,
2323
2462
  projectDescriptor: effectiveLoc.project
2324
2463
  };
2325
2464
  normalizeTicketFields(ticketObj);
@@ -2450,6 +2589,18 @@ ${formattedList}`
2450
2589
  continue;
2451
2590
  }
2452
2591
  }
2592
+ if (filter?.leased !== void 0) {
2593
+ const hasActiveLease = Boolean(ticketObj.lease);
2594
+ if (hasActiveLease !== Boolean(filter.leased)) {
2595
+ continue;
2596
+ }
2597
+ }
2598
+ if (filter?.leasedBy) {
2599
+ const holderFilter = filter.leasedBy.trim().toLowerCase();
2600
+ if (!ticketObj.lease || ticketObj.lease.holder.toLowerCase() !== holderFilter) {
2601
+ continue;
2602
+ }
2603
+ }
2453
2604
  tickets.push(ticketObj);
2454
2605
  }
2455
2606
  const locResolvedDir = path3.resolve(loc.ticketsDir);
@@ -2483,7 +2634,7 @@ ${formattedList}`
2483
2634
  const category = type;
2484
2635
  const complexityMatch = raw.match(/\*\*Complexity\*\*:\s*([^\n]+)/i);
2485
2636
  const complexity = complexityMatch ? complexityMatch[1].trim() : "Medium";
2486
- const effortMatch = raw.match(/\*\*Estimated Effort\*\*:\s*([^\n]+)/i);
2637
+ const effortMatch = raw.match(/\*\*(?:Estimated\s+)?Effort\*\*:\s*([^\n]+)/i);
2487
2638
  const estimatedEffort = effortMatch ? effortMatch[1].trim() : "2.0 - 4.0 hours";
2488
2639
  let summary;
2489
2640
  const summaryMatch = raw.match(/(?:##|###)\s*(?:Summary|Rationale)\s*\n([\s\S]*?)(?=\n##|\n###|\n#|$)/i);
@@ -2491,30 +2642,86 @@ ${formattedList}`
2491
2642
  summary = summaryMatch[1].trim();
2492
2643
  }
2493
2644
  const breakdown = [];
2494
- const breakdownMatch = raw.match(/(?:##|###)\s*Feature Breakdown\s*\n([\s\S]*?)(?=\n##|\n###|\n#|$)/i);
2645
+ const breakdownMatch = raw.match(/(?:##|###)\s*(?:Feature|Task|Fix|Implementation)\s+Breakdown\s*\n([\s\S]*?)(?=\n##|\n###|\n#|$)/i);
2495
2646
  if (breakdownMatch) {
2496
2647
  const lines = breakdownMatch[1].split("\n");
2648
+ let currentItem = "";
2497
2649
  for (const line of lines) {
2498
- const itemMatch = line.match(/^\s*(?:\d+\.|\*|-)\s+(.*)/);
2499
- if (itemMatch) breakdown.push(itemMatch[1].trim());
2650
+ const numberedMatch = line.match(/^\s*\d+\.\s+(.*)$/);
2651
+ if (numberedMatch) {
2652
+ if (currentItem) breakdown.push(currentItem);
2653
+ currentItem = numberedMatch[1].trim();
2654
+ } else if (/^\s*[-*•]\s+/.test(line)) {
2655
+ const bulletText = line.trim().replace(/^[-*•]\s+/, "").trim();
2656
+ if (/^\s{2,}[-*•]\s+/.test(line) && currentItem) {
2657
+ currentItem += "\n - " + bulletText;
2658
+ } else {
2659
+ if (currentItem) {
2660
+ breakdown.push(currentItem);
2661
+ currentItem = "";
2662
+ }
2663
+ breakdown.push(bulletText);
2664
+ }
2665
+ } else if (currentItem && line.trim()) {
2666
+ currentItem += " " + line.trim();
2667
+ }
2500
2668
  }
2669
+ if (currentItem) breakdown.push(currentItem);
2501
2670
  }
2502
2671
  const technicalDetails = [];
2503
- const techMatch = raw.match(/###\s*(?:Technical Details?|Architecture)\s*\n+([\s\S]*?)(?=\n###|\n---|\n##|$)/i);
2672
+ const openQuestions = [];
2673
+ const combinedMatch = raw.match(/(?:##|###)\s*Technical Details?\s*&\s*Open Questions?\s*\n+([\s\S]*?)(?=\n###|\n---|\n##|\n#|$)/i);
2674
+ if (combinedMatch) {
2675
+ const lines = combinedMatch[1].split("\n");
2676
+ for (const line of lines) {
2677
+ const itemMatch = line.match(/^\s*(?:\d+\.|\*|-|•)\s+(.*)/);
2678
+ if (!itemMatch) continue;
2679
+ const content = itemMatch[1].trim();
2680
+ if (!content) continue;
2681
+ if (/^\*\*Open Questions?\*\*:\s*/i.test(content)) {
2682
+ const q = content.replace(/^\*\*Open Questions?\*\*:\s*/i, "").trim();
2683
+ if (q && !/^(?:None|None recorded at initialization|N\/A)\.?$/i.test(q)) {
2684
+ openQuestions.push(q);
2685
+ }
2686
+ } else {
2687
+ const td = content.replace(/^\*\*Technical Details?\*\*:\s*/i, "").trim();
2688
+ if (td && !/^Architecture specifications to be documented\.?$/i.test(td)) {
2689
+ technicalDetails.push(td);
2690
+ }
2691
+ }
2692
+ }
2693
+ }
2694
+ const techMatch = raw.match(/(?:##|###)\s*(?:Technical Details?(?!\s*&\s*Open Questions?)(?:\s*&\s*Architecture)?|Architecture)\s*\n+([\s\S]*?)(?=\n###|\n---|\n##|\n#|$)/i);
2504
2695
  if (techMatch) {
2505
2696
  const lines = techMatch[1].split("\n");
2506
2697
  for (const line of lines) {
2507
- const itemMatch = line.match(/^\s*(?:\d+\.|\*|-)\s+(.*)/);
2508
- if (itemMatch) technicalDetails.push(itemMatch[1].trim());
2698
+ const itemMatch = line.match(/^\s*(?:\d+\.|\*|-|•)\s+(.*)/);
2699
+ if (!itemMatch) continue;
2700
+ const content = itemMatch[1].trim();
2701
+ if (!content) continue;
2702
+ if (/^\*\*Open Questions?\*\*:\s*/i.test(content)) {
2703
+ const q = content.replace(/^\*\*Open Questions?\*\*:\s*/i, "").trim();
2704
+ if (q && !/^(?:None|None recorded at initialization|N\/A)\.?$/i.test(q)) {
2705
+ openQuestions.push(q);
2706
+ }
2707
+ } else {
2708
+ const td = content.replace(/^\*\*Technical Details?\*\*:\s*/i, "").trim();
2709
+ if (td && !/^Architecture specifications to be documented\.?$/i.test(td)) {
2710
+ technicalDetails.push(td);
2711
+ }
2712
+ }
2509
2713
  }
2510
2714
  }
2511
- const openQuestions = [];
2512
- const questionsMatch = raw.match(/###\s*(?:Open Questions?|Technical Detail & Open Questions?|Decisions)\s*\n+([\s\S]*?)(?=\n###|\n---|\n##|$)/i);
2715
+ const questionsMatch = raw.match(/(?:##|###)\s*(?:Open Questions?(?:\s*&\s*Decisions)?|Decisions)\s*\n+([\s\S]*?)(?=\n###|\n---|\n##|\n#|$)/i);
2513
2716
  if (questionsMatch) {
2514
2717
  const lines = questionsMatch[1].split("\n");
2515
2718
  for (const line of lines) {
2516
- const itemMatch = line.match(/^\s*(?:\d+\.|\*|-)\s+(.*)/);
2517
- if (itemMatch) openQuestions.push(itemMatch[1].trim());
2719
+ const itemMatch = line.match(/^\s*(?:\d+\.|\*|-|•)\s+(.*)/);
2720
+ if (!itemMatch) continue;
2721
+ const content = itemMatch[1].trim().replace(/^\*\*Open Questions?\*\*:\s*/i, "").trim();
2722
+ if (content && !/^(?:None|None recorded at initialization|N\/A)\.?$/i.test(content)) {
2723
+ openQuestions.push(content);
2724
+ }
2518
2725
  }
2519
2726
  }
2520
2727
  const priorityMatch = raw.match(/\*\*Priority\*\*:\s*([^\n]+)/i);
@@ -2533,6 +2740,101 @@ ${formattedList}`
2533
2740
  raw
2534
2741
  };
2535
2742
  }
2743
+ reconcileDetailMetadataHeaders(content, meta, updates, syncAll = false) {
2744
+ const sectionSplitMatch = content.match(/\n(?=(?:##|###)\s+)/);
2745
+ const splitIdx = sectionSplitMatch && sectionSplitMatch.index !== void 0 ? sectionSplitMatch.index : content.length;
2746
+ let preamble = content.slice(0, splitIdx);
2747
+ const bodySection = content.slice(splitIdx);
2748
+ const replaceAndDedupeLine = (text, lineRegex, formatReplacement) => {
2749
+ let seen = false;
2750
+ const next = text.replace(lineRegex, (_full, lead = "", label = "") => {
2751
+ if (!seen) {
2752
+ seen = true;
2753
+ return formatReplacement(lead, label);
2754
+ }
2755
+ return "__ESEDRE_DEDUPE_REMOVE__";
2756
+ });
2757
+ return {
2758
+ text: next.replace(/\n?__ESEDRE_DEDUPE_REMOVE__[ \t]*\r?/g, ""),
2759
+ matched: seen
2760
+ };
2761
+ };
2762
+ if (syncAll || updates?.title) {
2763
+ preamble = preamble.replace(
2764
+ /^#\s*(?:Ticket\s*#?\d+\s*:\s*|\d+\s*:\s*)?[^\n]+/im,
2765
+ `# Ticket #${meta.id}: ${meta.title}`
2766
+ );
2767
+ }
2768
+ if (syncAll || updates?.type || updates?.category) {
2769
+ const effectiveType = meta.type || meta.category;
2770
+ const preferTypeLabel = Boolean(updates?.type || updates?.category);
2771
+ preamble = replaceAndDedupeLine(
2772
+ preamble,
2773
+ /^([ \t]*(?:[-*•]\s+)?)\*\*(Type|Category)\*\*:\s*[^\n\r]*/gim,
2774
+ (lead, label) => `${lead}**${preferTypeLabel ? "Type" : label}**: ${effectiveType}${lead.trim() ? "" : " "}`
2775
+ ).text;
2776
+ }
2777
+ if (syncAll || updates?.status) {
2778
+ preamble = replaceAndDedupeLine(
2779
+ preamble,
2780
+ /^([ \t]*(?:[-*•]\s+)?)\*\*(Status)\*\*:\s*[^\n\r]*/gim,
2781
+ (lead) => `${lead}**Status**: ${meta.status}${lead.trim() ? "" : " "}`
2782
+ ).text;
2783
+ }
2784
+ if (syncAll || updates && "priority" in updates) {
2785
+ if (meta.priority) {
2786
+ const res = replaceAndDedupeLine(
2787
+ preamble,
2788
+ /^([ \t]*(?:[-*•]\s+)?)\*\*(Priority)\*\*:\s*[^\n\r]*/gim,
2789
+ (lead) => `${lead}**Priority**: ${meta.priority}${lead.trim() ? "" : " "}`
2790
+ );
2791
+ preamble = res.text;
2792
+ if (!res.matched && /\*\*(?:Type|Category)\*\*:\s*[^\n]+/i.test(preamble)) {
2793
+ preamble = preamble.replace(
2794
+ /^([ \t]*(?:[-*•]\s+)?\*\*(?:Type|Category)\*\*:\s*[^\n\r]*)/im,
2795
+ `$1
2796
+ **Priority**: ${meta.priority} `
2797
+ );
2798
+ }
2799
+ } else {
2800
+ preamble = preamble.replace(/^[ \t]*(?:[-*•]\s+)?\*\*Priority\*\*:\s*[^\n\r]*\r?\n?/gim, "");
2801
+ }
2802
+ }
2803
+ if (syncAll || updates && "milestone" in updates) {
2804
+ if (meta.milestone) {
2805
+ const res = replaceAndDedupeLine(
2806
+ preamble,
2807
+ /^([ \t]*(?:[-*•]\s+)?)\*\*(Milestone)\*\*:\s*[^\n\r]*/gim,
2808
+ (lead) => `${lead}**Milestone**: ${meta.milestone}${lead.trim() ? "" : " "}`
2809
+ );
2810
+ preamble = res.text;
2811
+ if (!res.matched && /\*\*(?:Priority|Type|Category)\*\*:\s*[^\n]+/i.test(preamble)) {
2812
+ preamble = preamble.replace(
2813
+ /^([ \t]*(?:[-*•]\s+)?\*\*(?:Priority|Type|Category)\*\*:\s*[^\n\r]*)/im,
2814
+ `$1
2815
+ **Milestone**: ${meta.milestone} `
2816
+ );
2817
+ }
2818
+ } else {
2819
+ preamble = preamble.replace(/^[ \t]*(?:[-*•]\s+)?\*\*Milestone\*\*:\s*[^\n\r]*\r?\n?/gim, "");
2820
+ }
2821
+ }
2822
+ if (syncAll || updates?.complexity) {
2823
+ preamble = replaceAndDedupeLine(
2824
+ preamble,
2825
+ /^([ \t]*(?:[-*•]\s+)?)\*\*(Complexity)\*\*:\s*[^\n\r]*/gim,
2826
+ (lead) => `${lead}**Complexity**: ${meta.complexity}${lead.trim() ? "" : " "}`
2827
+ ).text;
2828
+ }
2829
+ if (syncAll || updates?.estimatedEffort) {
2830
+ preamble = replaceAndDedupeLine(
2831
+ preamble,
2832
+ /^([ \t]*(?:[-*•]\s+)?)\*\*((?:Estimated\s+)?Effort)\*\*:\s*[^\n\r]*/gim,
2833
+ (lead, label) => `${lead}**${label || "Estimated Effort"}**: ${meta.estimatedEffort}${lead.trim() ? "" : " "}`
2834
+ ).text;
2835
+ }
2836
+ return preamble + bodySection;
2837
+ }
2536
2838
  formatTicketDetailMarkdown(rawDetail, meta, fallbackSummary) {
2537
2839
  const trimmed = rawDetail.trim();
2538
2840
  const priorityLine = meta.priority ? `**Priority**: ${meta.priority}
@@ -2541,26 +2843,37 @@ ${formattedList}`
2541
2843
  ` : "";
2542
2844
  if (/^#\s+[^\n]+/m.test(trimmed)) {
2543
2845
  let processed = trimmed.replace(/^#\s+[^\n]+/m, `# Ticket #${meta.id}: ${meta.title}`);
2544
- if (!/\*\*(?:Type|Category)\*\*:/i.test(processed)) {
2846
+ const preambleCheck = processed.split(/\n(?=(?:##|###)\s+)/)[0];
2847
+ const hasAnyMetaLine = /\*\*(?:Type|Category|Priority|Milestone|Complexity|(?:Estimated\s+)?Effort|Status)\*\*:/i.test(
2848
+ preambleCheck
2849
+ );
2850
+ if (!hasAnyMetaLine) {
2545
2851
  const metaBlock = `
2546
2852
  **Category**: ${meta.category}
2547
2853
  ${priorityLine}${milestoneLine}**Complexity**: ${meta.complexity}
2548
2854
  **Estimated Effort**: ${meta.estimatedEffort}
2549
2855
  `;
2550
2856
  processed = processed.replace(/^(# Ticket[^\n]+\n)/m, `$1${metaBlock}`);
2857
+ } else if (!/\*\*(?:Type|Category)\*\*:/i.test(preambleCheck)) {
2858
+ processed = processed.replace(/^(# Ticket[^\n]+\n)/m, `$1**Category**: ${meta.category}
2859
+ `);
2551
2860
  }
2861
+ processed = this.reconcileDetailMetadataHeaders(processed, meta, void 0, true);
2552
2862
  if (!/(?:##|###)\s*(?:Summary|Rationale)/i.test(processed)) {
2553
2863
  const summaryBlock = `
2554
2864
  ### Summary
2555
2865
  ${fallbackSummary || "Summary to be defined."}
2556
2866
  `;
2557
- processed = processed.replace(/^((?:# Ticket[^\n]+\n)(?:\*\*[^\n]+\n)*)/m, `$1${summaryBlock}`);
2867
+ processed = processed.replace(
2868
+ /^((?:# Ticket[^\n]+\n)(?:[ \t]*(?:[-*•]\s+)?\*\*[^\n]+\n|\s*\n)*)/m,
2869
+ `$1${summaryBlock}`
2870
+ );
2558
2871
  }
2559
2872
  return processed.endsWith("\n") ? processed : `${processed}
2560
2873
  `;
2561
2874
  }
2562
2875
  const hasSummary = /(?:##|###)\s*(?:Summary|Rationale)/i.test(trimmed);
2563
- const hasBreakdown = /(?:##|###)\s*Feature Breakdown/i.test(trimmed);
2876
+ const hasBreakdown = /(?:##|###)\s*(?:Feature|Task|Fix|Implementation)\s+Breakdown/i.test(trimmed);
2564
2877
  let body = "";
2565
2878
  if (!hasSummary) {
2566
2879
  body += `### Summary
@@ -2767,7 +3080,17 @@ ${input.summary || "Summary to be defined."}
2767
3080
  }
2768
3081
  }
2769
3082
  }
3083
+ delete updatedMeta.lease;
2770
3084
  writeSafeFile(metaPath, JSON.stringify(updatedMeta, null, 2) + "\n");
3085
+ if (updates.status === "Completed" || updates.status === "Rejected") {
3086
+ const leasePath = path3.join(locInfo.ticketDir, "lease.json");
3087
+ if (fs3.existsSync(leasePath)) {
3088
+ try {
3089
+ fs3.unlinkSync(leasePath);
3090
+ } catch {
3091
+ }
3092
+ }
3093
+ }
2771
3094
  const newType = updates.type || updates.category;
2772
3095
  if (newType) {
2773
3096
  updatedMeta.type = newType;
@@ -2777,74 +3100,15 @@ ${input.summary || "Summary to be defined."}
2777
3100
  if (rawDetailArg !== void 0) {
2778
3101
  const cleanDetail = normalizeDashesAndMojibake(rawDetailArg);
2779
3102
  let content = this.formatTicketDetailMarkdown(cleanDetail, updatedMeta, existing.detail?.summary);
2780
- if (updates.type || updates.category) {
2781
- content = content.replace(/\*\*(?:Type|Category)\*\*:\s*[^\n]+/i, `**Type**: ${updatedMeta.type}`);
2782
- }
2783
- if (updatedMeta.priority) {
2784
- if (/\*\*Priority\*\*:\s*[^\n]+/i.test(content)) {
2785
- content = content.replace(/\*\*Priority\*\*:\s*[^\n]+/i, `**Priority**: ${updatedMeta.priority}`);
2786
- } else if (/\*\*(?:Type|Category)\*\*:\s*[^\n]+/i.test(content)) {
2787
- content = content.replace(/(\*\*(?:Type|Category)\*\*:\s*[^\n]+)/i, `$1
2788
- **Priority**: ${updatedMeta.priority}`);
2789
- }
2790
- } else if ("priority" in updates) {
2791
- content = content.replace(/\*\*Priority\*\*:\s*[^\n]+\n?/i, "");
2792
- }
2793
- if (updatedMeta.milestone) {
2794
- if (/\*\*Milestone\*\*:\s*[^\n]+/i.test(content)) {
2795
- content = content.replace(/\*\*Milestone\*\*:\s*[^\n]+/i, `**Milestone**: ${updatedMeta.milestone}`);
2796
- } else if (/\*\*(?:Priority|Type|Category)\*\*:\s*[^\n]+/i.test(content)) {
2797
- content = content.replace(/(\*\*(?:Priority|Type|Category)\*\*:\s*[^\n]+)/i, `$1
2798
- **Milestone**: ${updatedMeta.milestone}`);
2799
- }
2800
- } else if ("milestone" in updates) {
2801
- content = content.replace(/\*\*Milestone\*\*:\s*[^\n]+\n?/i, "");
2802
- }
2803
- if (updates.complexity) {
2804
- content = content.replace(/\*\*Complexity\*\*:\s*[^\n]+/i, `**Complexity**: ${updatedMeta.complexity}`);
2805
- }
2806
- if (updates.estimatedEffort) {
2807
- content = content.replace(/\*\*Estimated Effort\*\*:\s*[^\n]+/i, `**Estimated Effort**: ${updatedMeta.estimatedEffort}`);
2808
- }
3103
+ content = this.reconcileDetailMetadataHeaders(content, updatedMeta, updates, true);
2809
3104
  writeSafeFile(path3.join(locInfo.ticketDir, "detail.md"), content);
2810
- } else if (existing.detail && ("priority" in updates || "milestone" in updates || updates.title || updates.type || updates.category || updates.complexity || updates.estimatedEffort)) {
2811
- let content = existing.detail.raw;
2812
- if (updates.title) {
2813
- content = content.replace(/^#\s*(?:Ticket\s*#?\d+\s*:\s*|\d+\s*:\s*)?[^\n]+/im, `# Ticket #${locInfo.id}: ${updatedMeta.title}`);
2814
- }
2815
- if (updates.type || updates.category) {
2816
- content = content.replace(/\*\*(?:Type|Category)\*\*:\s*[^\n]+/i, `**Type**: ${updatedMeta.type}`);
2817
- }
2818
- if ("priority" in updates) {
2819
- if (updatedMeta.priority) {
2820
- if (/\*\*Priority\*\*:\s*[^\n]+/i.test(content)) {
2821
- content = content.replace(/\*\*Priority\*\*:\s*[^\n]+/i, `**Priority**: ${updatedMeta.priority}`);
2822
- } else if (/\*\*(?:Type|Category)\*\*:\s*[^\n]+/i.test(content)) {
2823
- content = content.replace(/(\*\*(?:Type|Category)\*\*:\s*[^\n]+)/i, `$1
2824
- **Priority**: ${updatedMeta.priority}`);
2825
- }
2826
- } else {
2827
- content = content.replace(/\*\*Priority\*\*:\s*[^\n]+\n?/i, "");
2828
- }
2829
- }
2830
- if ("milestone" in updates) {
2831
- if (updatedMeta.milestone) {
2832
- if (/\*\*Milestone\*\*:\s*[^\n]+/i.test(content)) {
2833
- content = content.replace(/\*\*Milestone\*\*:\s*[^\n]+/i, `**Milestone**: ${updatedMeta.milestone}`);
2834
- } else if (/\*\*(?:Priority|Type|Category)\*\*:\s*[^\n]+/i.test(content)) {
2835
- content = content.replace(/(\*\*(?:Priority|Type|Category)\*\*:\s*[^\n]+)/i, `$1
2836
- **Milestone**: ${updatedMeta.milestone}`);
2837
- }
2838
- } else {
2839
- content = content.replace(/\*\*Milestone\*\*:\s*[^\n]+\n?/i, "");
2840
- }
2841
- }
2842
- if (updates.complexity) {
2843
- content = content.replace(/\*\*Complexity\*\*:\s*[^\n]+/i, `**Complexity**: ${updatedMeta.complexity}`);
2844
- }
2845
- if (updates.estimatedEffort) {
2846
- content = content.replace(/\*\*Estimated Effort\*\*:\s*[^\n]+/i, `**Estimated Effort**: ${updatedMeta.estimatedEffort}`);
2847
- }
3105
+ } else if (existing.detail && ("priority" in updates || "milestone" in updates || updates.status || updates.title || updates.type || updates.category || updates.complexity || updates.estimatedEffort)) {
3106
+ const content = this.reconcileDetailMetadataHeaders(
3107
+ existing.detail.raw,
3108
+ updatedMeta,
3109
+ updates,
3110
+ false
3111
+ );
2848
3112
  writeSafeFile(path3.join(locInfo.ticketDir, "detail.md"), content);
2849
3113
  }
2850
3114
  this.invalidateTicketCache(locInfo.ticketDir);
@@ -3280,6 +3544,163 @@ ${input.summary || "Summary to be defined."}
3280
3544
  const updatedSource = await this.getTicket(sourceInfo.key);
3281
3545
  return { source: updatedSource, target: updatedTarget };
3282
3546
  }
3547
+ readValidLeaseFromDir(ticketDir, projectCode, ticketId) {
3548
+ const leasePath = path3.join(ticketDir, "lease.json");
3549
+ if (!fs3.existsSync(leasePath)) return null;
3550
+ try {
3551
+ const parsed = JSON.parse(fs3.readFileSync(leasePath, "utf-8"));
3552
+ const expMs = Date.parse(parsed.expiresAt);
3553
+ if (!isNaN(expMs) && expMs <= Date.now()) {
3554
+ try {
3555
+ fs3.unlinkSync(leasePath);
3556
+ } catch {
3557
+ }
3558
+ this.invalidateTicketCache(ticketDir);
3559
+ return null;
3560
+ }
3561
+ parsed.project = projectCode;
3562
+ parsed.ticketId = ticketId;
3563
+ parsed.ticketKey = `${projectCode}-${ticketId}`;
3564
+ return parsed;
3565
+ } catch {
3566
+ return null;
3567
+ }
3568
+ }
3569
+ async claimTicket(input) {
3570
+ const resolved = this.resolveTicketIdentifier(input.ticketId, input.project || this.config.projectCode);
3571
+ if (!resolved) {
3572
+ throw new Error(`Ticket #${input.ticketId} could not be located.`);
3573
+ }
3574
+ const identity = resolveWorkerIdentity({
3575
+ holder: input.holder,
3576
+ agentType: input.agentType,
3577
+ hostname: input.hostname,
3578
+ sessionId: input.sessionId,
3579
+ branch: input.branch,
3580
+ worktree: input.worktree || this.workspaceRoot,
3581
+ configDefaultAuthor: this.config.defaultAuthor
3582
+ });
3583
+ const ttlMinutes = typeof input.ttlMinutes === "number" && input.ttlMinutes > 0 ? Math.min(Math.max(1, Math.round(input.ttlMinutes)), 24 * 60) : DEFAULT_LEASE_TTL_MINUTES;
3584
+ const existingLease = this.readValidLeaseFromDir(resolved.ticketDir, resolved.project, resolved.id);
3585
+ let renewed = false;
3586
+ let overridden = false;
3587
+ if (existingLease) {
3588
+ const isSameHolder = existingLease.holder.toLowerCase() === identity.holder.toLowerCase();
3589
+ if (!isSameHolder && !input.force) {
3590
+ throw new EsedreLeaseConflictError(resolved.key, existingLease);
3591
+ }
3592
+ if (isSameHolder) {
3593
+ renewed = true;
3594
+ } else {
3595
+ overridden = true;
3596
+ }
3597
+ }
3598
+ const now = /* @__PURE__ */ new Date();
3599
+ const nowIso = now.toISOString();
3600
+ const expiresIso = new Date(now.getTime() + ttlMinutes * 60 * 1e3).toISOString();
3601
+ const lease = {
3602
+ ticketId: resolved.id,
3603
+ ticketKey: resolved.key,
3604
+ project: resolved.project,
3605
+ holder: normalizeDashesAndMojibake(identity.holder),
3606
+ ...identity.agentType ? { agentType: normalizeDashesAndMojibake(identity.agentType) } : {},
3607
+ ...identity.hostname ? { hostname: normalizeDashesAndMojibake(identity.hostname) } : {},
3608
+ ...identity.sessionId ? { sessionId: normalizeDashesAndMojibake(identity.sessionId) } : {},
3609
+ ...identity.branch ? { branch: normalizeDashesAndMojibake(identity.branch) } : {},
3610
+ ...identity.worktree ? { worktree: identity.worktree } : {},
3611
+ claimedAt: renewed && existingLease ? existingLease.claimedAt : nowIso,
3612
+ renewedAt: nowIso,
3613
+ expiresAt: expiresIso,
3614
+ ttlMinutes
3615
+ };
3616
+ const leasePath = path3.join(resolved.ticketDir, "lease.json");
3617
+ writeSafeFile(leasePath, JSON.stringify(lease, null, 2) + "\n");
3618
+ this.invalidateTicketCache(resolved.ticketDir);
3619
+ let ticket = await this.getTicket(resolved.key);
3620
+ const targetStatus = input.statusOnClaim ?? (ticket.meta.status === "Planned" ? "In Development" : void 0);
3621
+ if (targetStatus && ticket.meta.status !== targetStatus) {
3622
+ ticket = await this.updateTicket(resolved.key, {
3623
+ status: targetStatus,
3624
+ isActivePlanning: targetStatus === "In Development" ? true : ticket.meta.isActivePlanning
3625
+ });
3626
+ }
3627
+ return {
3628
+ lease,
3629
+ ticket,
3630
+ renewed,
3631
+ ...overridden ? { overridden: true } : {}
3632
+ };
3633
+ }
3634
+ async renewLease(input) {
3635
+ const resolved = this.resolveTicketIdentifier(input.ticketId, input.project || this.config.projectCode);
3636
+ if (!resolved) {
3637
+ throw new Error(`Ticket #${input.ticketId} could not be located.`);
3638
+ }
3639
+ const existingLease = this.readValidLeaseFromDir(resolved.ticketDir, resolved.project, resolved.id);
3640
+ if (!existingLease) {
3641
+ throw new Error(`Ticket ${resolved.key} does not have an active lease to renew.`);
3642
+ }
3643
+ if (input.holder && !input.force) {
3644
+ const requestedHolder = input.holder.trim().toLowerCase();
3645
+ if (existingLease.holder.toLowerCase() !== requestedHolder) {
3646
+ throw new EsedreLeaseConflictError(resolved.key, existingLease);
3647
+ }
3648
+ }
3649
+ const ttlMinutes = typeof input.ttlMinutes === "number" && input.ttlMinutes > 0 ? Math.min(Math.max(1, Math.round(input.ttlMinutes)), 24 * 60) : existingLease.ttlMinutes || DEFAULT_LEASE_TTL_MINUTES;
3650
+ const now = /* @__PURE__ */ new Date();
3651
+ const updatedLease = {
3652
+ ...existingLease,
3653
+ ...input.holder ? { holder: normalizeDashesAndMojibake(input.holder.trim()) } : {},
3654
+ renewedAt: now.toISOString(),
3655
+ expiresAt: new Date(now.getTime() + ttlMinutes * 60 * 1e3).toISOString(),
3656
+ ttlMinutes
3657
+ };
3658
+ const leasePath = path3.join(resolved.ticketDir, "lease.json");
3659
+ writeSafeFile(leasePath, JSON.stringify(updatedLease, null, 2) + "\n");
3660
+ this.invalidateTicketCache(resolved.ticketDir);
3661
+ return updatedLease;
3662
+ }
3663
+ async releaseTicket(input) {
3664
+ const resolved = this.resolveTicketIdentifier(input.ticketId, input.project || this.config.projectCode);
3665
+ if (!resolved) {
3666
+ throw new Error(`Ticket #${input.ticketId} could not be located.`);
3667
+ }
3668
+ const existingLease = this.readValidLeaseFromDir(resolved.ticketDir, resolved.project, resolved.id);
3669
+ if (!existingLease) {
3670
+ return { released: false, ticketKey: resolved.key };
3671
+ }
3672
+ if (input.holder && !input.force) {
3673
+ const requestedHolder = input.holder.trim().toLowerCase();
3674
+ if (existingLease.holder.toLowerCase() !== requestedHolder) {
3675
+ throw new EsedreLeaseConflictError(resolved.key, existingLease);
3676
+ }
3677
+ }
3678
+ const leasePath = path3.join(resolved.ticketDir, "lease.json");
3679
+ if (fs3.existsSync(leasePath)) {
3680
+ try {
3681
+ fs3.unlinkSync(leasePath);
3682
+ } catch {
3683
+ }
3684
+ }
3685
+ this.invalidateTicketCache(resolved.ticketDir);
3686
+ return {
3687
+ released: true,
3688
+ ticketKey: resolved.key,
3689
+ previousLease: existingLease
3690
+ };
3691
+ }
3692
+ async getTicketLease(id, projectCode) {
3693
+ const resolved = this.resolveTicketIdentifier(id, projectCode || this.config.projectCode);
3694
+ if (!resolved) return null;
3695
+ return this.readValidLeaseFromDir(resolved.ticketDir, resolved.project, resolved.id);
3696
+ }
3697
+ async listLeases(projectCode) {
3698
+ const tickets = await this.listTickets({
3699
+ project: projectCode === "all" ? void 0 : projectCode,
3700
+ leased: true
3701
+ });
3702
+ return tickets.map((t) => t.lease).filter((l) => Boolean(l));
3703
+ }
3283
3704
  };
3284
3705
 
3285
3706
  // src/securityFilter.ts
@@ -3576,9 +3997,64 @@ var SecurityFilter = class {
3576
3997
  }
3577
3998
  return this.target.getAttachmentPath ? this.target.getAttachmentPath(id, filename) : null;
3578
3999
  }
4000
+ async claimTicket(input) {
4001
+ if (input.project && !isProjectAuthorized(input.project, this.config.allowedProjects)) {
4002
+ throw new EsedreAuthorizationError(input.project);
4003
+ }
4004
+ const ticket = await this.getTicket(
4005
+ input.project && /^\d+$/.test(String(input.ticketId).trim()) ? `${input.project}-${input.ticketId}` : input.ticketId
4006
+ );
4007
+ if (!ticket) {
4008
+ throw new Error(`Ticket #${input.ticketId} not found`);
4009
+ }
4010
+ return this.target.claimTicket(input);
4011
+ }
4012
+ async renewLease(input) {
4013
+ if (input.project && !isProjectAuthorized(input.project, this.config.allowedProjects)) {
4014
+ throw new EsedreAuthorizationError(input.project);
4015
+ }
4016
+ const ticket = await this.getTicket(
4017
+ input.project && /^\d+$/.test(String(input.ticketId).trim()) ? `${input.project}-${input.ticketId}` : input.ticketId
4018
+ );
4019
+ if (!ticket) {
4020
+ throw new Error(`Ticket #${input.ticketId} not found`);
4021
+ }
4022
+ return this.target.renewLease(input);
4023
+ }
4024
+ async releaseTicket(input) {
4025
+ if (input.project && !isProjectAuthorized(input.project, this.config.allowedProjects)) {
4026
+ throw new EsedreAuthorizationError(input.project);
4027
+ }
4028
+ const ticket = await this.getTicket(
4029
+ input.project && /^\d+$/.test(String(input.ticketId).trim()) ? `${input.project}-${input.ticketId}` : input.ticketId
4030
+ );
4031
+ if (!ticket) {
4032
+ throw new Error(`Ticket #${input.ticketId} not found`);
4033
+ }
4034
+ return this.target.releaseTicket(input);
4035
+ }
4036
+ async getTicketLease(id, projectCode) {
4037
+ if (projectCode && !isProjectAuthorized(projectCode, this.config.allowedProjects)) {
4038
+ throw new EsedreAuthorizationError(projectCode);
4039
+ }
4040
+ const ticket = await this.getTicket(
4041
+ projectCode && /^\d+$/.test(String(id).trim()) ? `${projectCode}-${id}` : id
4042
+ );
4043
+ if (!ticket) return null;
4044
+ return this.target.getTicketLease(id, projectCode);
4045
+ }
4046
+ async listLeases(projectCode) {
4047
+ if (projectCode && projectCode !== "all" && !isProjectAuthorized(projectCode, this.config.allowedProjects)) {
4048
+ throw new EsedreAuthorizationError(projectCode);
4049
+ }
4050
+ const allLeases = await this.target.listLeases(projectCode);
4051
+ return allLeases.filter((l) => isProjectAuthorized(l.project, this.config.allowedProjects));
4052
+ }
3579
4053
  };
3580
4054
 
3581
4055
  // src/mcp/server.ts
4056
+ import fs4 from "node:fs";
4057
+ import path4 from "node:path";
3582
4058
  import readline from "node:readline";
3583
4059
  var EsedreMcpServer = class {
3584
4060
  storage;
@@ -3689,6 +4165,20 @@ var EsedreMcpServer = class {
3689
4165
  }
3690
4166
  };
3691
4167
  }
4168
+ if (err instanceof EsedreLeaseConflictError || err.name === "EsedreLeaseConflictError") {
4169
+ return {
4170
+ jsonrpc: "2.0",
4171
+ id,
4172
+ error: {
4173
+ code: -32001,
4174
+ message: err.message,
4175
+ data: {
4176
+ ticketKey: err.ticketKey,
4177
+ activeLease: err.activeLease
4178
+ }
4179
+ }
4180
+ };
4181
+ }
3692
4182
  return {
3693
4183
  jsonrpc: "2.0",
3694
4184
  id,
@@ -3800,7 +4290,8 @@ Status: ${ticket.meta.status}`;
3800
4290
  milestone: { type: "string", description: "Filter tickets by milestone name or ID" },
3801
4291
  search: { type: "string", description: "Search keywords in title or ID" },
3802
4292
  isBlocked: { type: "boolean", description: "Filter tickets blocked by uncompleted tickets" },
3803
- linkedTo: { type: "string", description: "Filter tickets linked to a specific ticket ID or key" }
4293
+ linkedTo: { type: "string", description: "Filter tickets linked to a specific ticket ID or key" },
4294
+ includePlan: { type: "boolean", description: "When true, includes implementation plan markdown (planMarkdown) for each ticket" }
3804
4295
  }
3805
4296
  }
3806
4297
  },
@@ -3892,9 +4383,10 @@ Status: ${ticket.meta.status}`;
3892
4383
  properties: {
3893
4384
  ticketId: { type: ["integer", "string"], description: "Numeric ticket ID or compound key" },
3894
4385
  text: { type: "string", description: "Comment message" },
4386
+ file: { type: "string", description: "Path to local file containing comment message markdown" },
3895
4387
  author: { type: "string", description: "Author name (e.g. Antigravity, Developer)" }
3896
4388
  },
3897
- required: ["ticketId", "text"]
4389
+ required: ["ticketId"]
3898
4390
  }
3899
4391
  },
3900
4392
  {
@@ -3983,6 +4475,71 @@ Status: ${ticket.meta.status}`;
3983
4475
  },
3984
4476
  required: ["sourceTicketId", "targetTicketId"]
3985
4477
  }
4478
+ },
4479
+ {
4480
+ name: "esedre_claim_ticket",
4481
+ description: "Claim an advisory TTL-backed lease on a ticket to coordinate multi-agent and multi-machine work and prevent duplicate effort.",
4482
+ inputSchema: {
4483
+ type: "object",
4484
+ properties: {
4485
+ ticketId: { type: ["number", "string"], description: 'Ticket ID or prefixed key (e.g. 69 or "Esedre-69")' },
4486
+ holder: { type: "string", description: 'Agent or developer identity claiming the ticket (e.g. "agy-subagent-1" or "desktop-worker")' },
4487
+ workerId: { type: "string", description: "Alias for holder: agent or worker identity claiming the ticket" },
4488
+ agentType: { type: "string", description: 'Optional agent runtime identifier (e.g. "antigravity", "claude-code", "codex")' },
4489
+ system: { type: "string", description: "Alias for agentType: system or runtime identifier" },
4490
+ sessionId: { type: "string", description: "Optional conversation or session identifier" },
4491
+ branch: { type: "string", description: "Optional Git branch associated with the lease" },
4492
+ ttlMinutes: { type: "number", description: "Lease time-to-live in minutes (default: 60)" },
4493
+ force: { type: "boolean", description: "Override an existing active lease held by another worker" },
4494
+ project: { type: "string", description: "Optional project code when numeric ticketId is used" }
4495
+ },
4496
+ required: ["ticketId"]
4497
+ }
4498
+ },
4499
+ {
4500
+ name: "esedre_renew_lease",
4501
+ description: "Renew (heartbeat) an active ticket lease to extend its expiration timestamp.",
4502
+ inputSchema: {
4503
+ type: "object",
4504
+ properties: {
4505
+ ticketId: { type: ["number", "string"], description: "Ticket ID or prefixed key" },
4506
+ holder: { type: "string", description: "Holder identity verifying lease ownership" },
4507
+ workerId: { type: "string", description: "Alias for holder: worker identity verifying lease ownership" },
4508
+ system: { type: "string", description: "Optional system or agent runtime identifier" },
4509
+ ttlMinutes: { type: "number", description: "New TTL in minutes from now (default: 60)" },
4510
+ force: { type: "boolean", description: "Force renewal even if held by another identity" },
4511
+ project: { type: "string", description: "Optional project code" }
4512
+ },
4513
+ required: ["ticketId"]
4514
+ }
4515
+ },
4516
+ {
4517
+ name: "esedre_release_ticket",
4518
+ description: "Release an active lease on a ticket when work is paused, handed off, or completed.",
4519
+ inputSchema: {
4520
+ type: "object",
4521
+ properties: {
4522
+ ticketId: { type: ["number", "string"], description: "Ticket ID or prefixed key" },
4523
+ holder: { type: "string", description: "Holder identity verifying lease ownership" },
4524
+ workerId: { type: "string", description: "Alias for holder: worker identity verifying lease ownership" },
4525
+ system: { type: "string", description: "Optional system or agent runtime identifier" },
4526
+ finalStatus: { type: "string", enum: ["Planned", "In Development", "Completed", "Rejected"], description: "Optional status to transition the ticket to upon release" },
4527
+ notes: { type: "string", description: "Optional verification or handoff comment to append upon release" },
4528
+ force: { type: "boolean", description: "Force release even if held by another identity" },
4529
+ project: { type: "string", description: "Optional project code" }
4530
+ },
4531
+ required: ["ticketId"]
4532
+ }
4533
+ },
4534
+ {
4535
+ name: "esedre_list_leases",
4536
+ description: "List all currently active (non-expired) ticket leases across authorized projects.",
4537
+ inputSchema: {
4538
+ type: "object",
4539
+ properties: {
4540
+ project: { type: "string", description: 'Optional project code filter (or "all")' }
4541
+ }
4542
+ }
3986
4543
  }
3987
4544
  ];
3988
4545
  }
@@ -3991,6 +4548,7 @@ Status: ${ticket.meta.status}`;
3991
4548
  switch (canonical) {
3992
4549
  case "list_tickets": {
3993
4550
  const type = args.type || args.category;
4551
+ const includePlan = Boolean(args.includePlan);
3994
4552
  const tickets = await this.storage.listTickets({
3995
4553
  project: args.project,
3996
4554
  status: args.status,
@@ -4002,23 +4560,39 @@ Status: ${ticket.meta.status}`;
4002
4560
  isBlocked: args.isBlocked !== void 0 ? Boolean(args.isBlocked) : void 0,
4003
4561
  linkedTo: args.linkedTo
4004
4562
  });
4005
- return tickets.map((t) => ({
4006
- id: t.meta.id,
4007
- title: t.meta.title,
4008
- type: t.meta.type || t.meta.category,
4009
- category: t.meta.type || t.meta.category,
4010
- status: t.meta.status,
4011
- priority: t.meta.priority,
4012
- complexity: t.meta.complexity,
4013
- effort: t.meta.estimatedEffort,
4014
- milestone: t.meta.milestone,
4015
- featureFlag: t.meta.featureFlag,
4016
- inheritedFeatureFlag: t.meta.inheritedFeatureFlag,
4017
- project: t.projectDescriptor?.code || t.meta.project || "UNASSIGNED",
4018
- sha1: t.sha1 || t.meta.sha1,
4019
- isBlocked: t.isBlocked,
4020
- links: t.links
4021
- }));
4563
+ const results = await Promise.all(
4564
+ tickets.map(async (t) => {
4565
+ let planMarkdown = t.planMarkdown;
4566
+ if (includePlan && planMarkdown === void 0) {
4567
+ const lookupKey = t.projectDescriptor?.code ? `${t.projectDescriptor.code}-${t.meta.id}` : t.meta.id;
4568
+ planMarkdown = await this.storage.getPlan(lookupKey) ?? null;
4569
+ }
4570
+ const hasPlan = Boolean(
4571
+ t.planMarkdown && t.planMarkdown.trim().length > 0 || planMarkdown && planMarkdown.trim().length > 0
4572
+ );
4573
+ return {
4574
+ id: t.meta.id,
4575
+ title: t.meta.title,
4576
+ type: t.meta.type || t.meta.category,
4577
+ category: t.meta.type || t.meta.category,
4578
+ status: t.meta.status,
4579
+ priority: t.meta.priority,
4580
+ complexity: t.meta.complexity,
4581
+ effort: t.meta.estimatedEffort,
4582
+ milestone: t.meta.milestone,
4583
+ featureFlag: t.meta.featureFlag,
4584
+ inheritedFeatureFlag: t.meta.inheritedFeatureFlag,
4585
+ project: t.projectDescriptor?.code || t.meta.project || "UNASSIGNED",
4586
+ summary: t.detail?.summary,
4587
+ hasPlan,
4588
+ ...includePlan ? { planMarkdown: planMarkdown || null } : {},
4589
+ sha1: t.sha1 || t.meta.sha1,
4590
+ isBlocked: t.isBlocked,
4591
+ links: t.links
4592
+ };
4593
+ })
4594
+ );
4595
+ return results;
4022
4596
  }
4023
4597
  case "get_ticket": {
4024
4598
  const id = this.parseTicketId(args.ticketId);
@@ -4077,8 +4651,26 @@ Status: ${ticket.meta.status}`;
4077
4651
  }
4078
4652
  case "add_comment": {
4079
4653
  const id = this.parseTicketId(args.ticketId);
4654
+ let text = args.text;
4655
+ if (!text && args.file) {
4656
+ const resolvedPath = path4.resolve(process.cwd(), args.file);
4657
+ if (!fs4.existsSync(resolvedPath)) {
4658
+ throw new Error(`File not found: ${args.file}`);
4659
+ }
4660
+ if (!fs4.statSync(resolvedPath).isFile()) {
4661
+ throw new Error(`Path is not a file: ${args.file}`);
4662
+ }
4663
+ const fileContent = fs4.readFileSync(resolvedPath, "utf-8");
4664
+ if (!fileContent.trim()) {
4665
+ throw new Error(`Comment file is empty: ${args.file}`);
4666
+ }
4667
+ text = fileContent;
4668
+ }
4669
+ if (!text || !text.trim()) {
4670
+ throw new Error("Comment text or file is required");
4671
+ }
4080
4672
  const comment = await this.storage.addComment(id, {
4081
- text: args.text,
4673
+ text,
4082
4674
  author: args.author || "Agent"
4083
4675
  });
4084
4676
  return comment;
@@ -4140,26 +4732,117 @@ Status: ${ticket.meta.status}`;
4140
4732
  target: result.target
4141
4733
  };
4142
4734
  }
4735
+ case "claim_ticket": {
4736
+ const identity = resolveWorkerIdentity({
4737
+ holder: args.holder || args.workerId,
4738
+ agentType: args.agentType || args.system || "mcp-agent",
4739
+ sessionId: args.sessionId,
4740
+ branch: args.branch
4741
+ });
4742
+ const res = await this.storage.claimTicket({
4743
+ ticketId: this.parseTicketId(args.ticketId),
4744
+ holder: identity.holder,
4745
+ agentType: identity.agentType,
4746
+ hostname: identity.hostname,
4747
+ sessionId: identity.sessionId,
4748
+ branch: identity.branch,
4749
+ worktree: identity.worktree,
4750
+ ttlMinutes: args.ttlMinutes,
4751
+ force: Boolean(args.force),
4752
+ project: args.project
4753
+ });
4754
+ return {
4755
+ success: true,
4756
+ renewed: res.renewed,
4757
+ overridden: res.overridden || false,
4758
+ lease: res.lease,
4759
+ ticket: {
4760
+ id: res.ticket.meta.id,
4761
+ project: res.ticket.meta.project,
4762
+ title: res.ticket.meta.title,
4763
+ status: res.ticket.meta.status,
4764
+ sha1: res.ticket.sha1
4765
+ }
4766
+ };
4767
+ }
4768
+ case "renew_lease": {
4769
+ const lease = await this.storage.renewLease({
4770
+ ticketId: this.parseTicketId(args.ticketId),
4771
+ holder: args.holder || args.workerId,
4772
+ ttlMinutes: args.ttlMinutes,
4773
+ force: Boolean(args.force),
4774
+ project: args.project
4775
+ });
4776
+ return {
4777
+ success: true,
4778
+ lease
4779
+ };
4780
+ }
4781
+ case "release_ticket": {
4782
+ const holder = args.holder || args.workerId;
4783
+ const parsedId = this.parseTicketId(args.ticketId);
4784
+ const res = await this.storage.releaseTicket({
4785
+ ticketId: parsedId,
4786
+ holder,
4787
+ force: Boolean(args.force),
4788
+ project: args.project
4789
+ });
4790
+ if (args.finalStatus) {
4791
+ await this.storage.updateTicket(res.ticketKey || parsedId, { status: args.finalStatus });
4792
+ }
4793
+ if (args.notes) {
4794
+ await this.storage.addComment(res.ticketKey || parsedId, {
4795
+ author: holder || res.previousLease?.holder || "mcp-agent",
4796
+ text: args.notes
4797
+ });
4798
+ }
4799
+ return {
4800
+ success: true,
4801
+ ...res
4802
+ };
4803
+ }
4804
+ case "list_leases": {
4805
+ const leases = await this.storage.listLeases(args.project);
4806
+ return {
4807
+ total: leases.length,
4808
+ leases
4809
+ };
4810
+ }
4143
4811
  default:
4144
4812
  throw new Error(`Unknown tool: ${name}`);
4145
4813
  }
4146
4814
  }
4147
4815
  };
4148
4816
 
4817
+ // src/utils/logger.ts
4818
+ var MAX_LOG_FILE_BYTES = 5 * 1024 * 1024;
4819
+ var debugEnabled = Boolean(process.env.ESEDRE_DEBUG);
4820
+ var logFileOverride;
4821
+ function initLogger(options) {
4822
+ if (options?.debug !== void 0) {
4823
+ debugEnabled = options.debug;
4824
+ } else if (process.env.ESEDRE_DEBUG) {
4825
+ debugEnabled = true;
4826
+ }
4827
+ if (options?.logFile) {
4828
+ logFileOverride = options.logFile;
4829
+ }
4830
+ }
4831
+
4149
4832
  // src/utils/manifestDetector.ts
4150
- import fs4 from "node:fs";
4151
- import path4 from "node:path";
4833
+ import fs5 from "node:fs";
4834
+ import path5 from "node:path";
4152
4835
  function sanitizeCode(input) {
4153
4836
  const clean = input.replace(/[^a-zA-Z0-9]/g, "");
4154
4837
  return clean.slice(0, MAX_PROJECT_CODE_LENGTH) || "APP";
4155
4838
  }
4156
4839
  function detectProjectManifest(targetDir) {
4157
- const dirBase = path4.basename(targetDir);
4840
+ const dirBase = path5.basename(targetDir);
4158
4841
  for (const settingsFile of ["settings.gradle.kts", "settings.gradle"]) {
4159
- const fullPath = path4.join(targetDir, settingsFile);
4160
- if (fs4.existsSync(fullPath)) {
4842
+ const fullPath = path5.join(targetDir, settingsFile);
4843
+ if (fs5.existsSync(fullPath)) {
4161
4844
  try {
4162
- const content = fs4.readFileSync(fullPath, "utf-8");
4845
+ const content = fs5.readFileSync(fullPath, "utf-8");
4163
4846
  const match = content.match(/rootProject\.name\s*=\s*["']([^"']+)["']/);
4164
4847
  if (match && match[1].trim()) {
4165
4848
  const rawName = match[1].trim();
@@ -4175,15 +4858,15 @@ function detectProjectManifest(targetDir) {
4175
4858
  }
4176
4859
  }
4177
4860
  for (const buildFile of [
4178
- path4.join("app", "build.gradle.kts"),
4861
+ path5.join("app", "build.gradle.kts"),
4179
4862
  "build.gradle.kts",
4180
- path4.join("app", "build.gradle"),
4863
+ path5.join("app", "build.gradle"),
4181
4864
  "build.gradle"
4182
4865
  ]) {
4183
- const fullPath = path4.join(targetDir, buildFile);
4184
- if (fs4.existsSync(fullPath)) {
4866
+ const fullPath = path5.join(targetDir, buildFile);
4867
+ if (fs5.existsSync(fullPath)) {
4185
4868
  try {
4186
- const content = fs4.readFileSync(fullPath, "utf-8");
4869
+ const content = fs5.readFileSync(fullPath, "utf-8");
4187
4870
  const match = content.match(/(?:namespace|applicationId)\s*=\s*["']([^"']+)["']/);
4188
4871
  if (match && match[1].trim()) {
4189
4872
  const ns = match[1].trim();
@@ -4200,10 +4883,10 @@ function detectProjectManifest(targetDir) {
4200
4883
  }
4201
4884
  }
4202
4885
  }
4203
- const pomPath = path4.join(targetDir, "pom.xml");
4204
- if (fs4.existsSync(pomPath)) {
4886
+ const pomPath = path5.join(targetDir, "pom.xml");
4887
+ if (fs5.existsSync(pomPath)) {
4205
4888
  try {
4206
- const content = fs4.readFileSync(pomPath, "utf-8");
4889
+ const content = fs5.readFileSync(pomPath, "utf-8");
4207
4890
  const stripped = content.replace(/<!--[\s\S]*?-->/g, "").replace(/<parent>[\s\S]*?<\/parent>/g, "");
4208
4891
  const nameMatch = stripped.match(/<name>\s*([^<]+)\s*<\/name>/);
4209
4892
  const artifactMatch = stripped.match(/<artifactId>\s*([^<]+)\s*<\/artifactId>/) || content.match(/<artifactId>\s*([^<]+)\s*<\/artifactId>/);
@@ -4222,10 +4905,10 @@ function detectProjectManifest(targetDir) {
4222
4905
  } catch {
4223
4906
  }
4224
4907
  }
4225
- const pkgPath = path4.join(targetDir, "package.json");
4226
- if (fs4.existsSync(pkgPath)) {
4908
+ const pkgPath = path5.join(targetDir, "package.json");
4909
+ if (fs5.existsSync(pkgPath)) {
4227
4910
  try {
4228
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
4911
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
4229
4912
  if (pkg.name && typeof pkg.name === "string") {
4230
4913
  const baseName = pkg.name.replace(/^@[\w-]+\//, "");
4231
4914
  return {
@@ -4238,10 +4921,10 @@ function detectProjectManifest(targetDir) {
4238
4921
  } catch {
4239
4922
  }
4240
4923
  }
4241
- const cargoPath = path4.join(targetDir, "Cargo.toml");
4242
- if (fs4.existsSync(cargoPath)) {
4924
+ const cargoPath = path5.join(targetDir, "Cargo.toml");
4925
+ if (fs5.existsSync(cargoPath)) {
4243
4926
  try {
4244
- const content = fs4.readFileSync(cargoPath, "utf-8");
4927
+ const content = fs5.readFileSync(cargoPath, "utf-8");
4245
4928
  const match = content.match(/\[package\][\s\S]*?name\s*=\s*["']([^"']+)["']/);
4246
4929
  if (match && match[1].trim()) {
4247
4930
  const rawName = match[1].trim();
@@ -4255,10 +4938,10 @@ function detectProjectManifest(targetDir) {
4255
4938
  } catch {
4256
4939
  }
4257
4940
  }
4258
- const pyprojectPath = path4.join(targetDir, "pyproject.toml");
4259
- if (fs4.existsSync(pyprojectPath)) {
4941
+ const pyprojectPath = path5.join(targetDir, "pyproject.toml");
4942
+ if (fs5.existsSync(pyprojectPath)) {
4260
4943
  try {
4261
- const content = fs4.readFileSync(pyprojectPath, "utf-8");
4944
+ const content = fs5.readFileSync(pyprojectPath, "utf-8");
4262
4945
  const match = content.match(/name\s*=\s*["']([^"']+)["']/);
4263
4946
  if (match && match[1].trim()) {
4264
4947
  const rawName = match[1].trim();
@@ -4272,10 +4955,10 @@ function detectProjectManifest(targetDir) {
4272
4955
  } catch {
4273
4956
  }
4274
4957
  }
4275
- const setupCfgPath = path4.join(targetDir, "setup.cfg");
4276
- if (fs4.existsSync(setupCfgPath)) {
4958
+ const setupCfgPath = path5.join(targetDir, "setup.cfg");
4959
+ if (fs5.existsSync(setupCfgPath)) {
4277
4960
  try {
4278
- const content = fs4.readFileSync(setupCfgPath, "utf-8");
4961
+ const content = fs5.readFileSync(setupCfgPath, "utf-8");
4279
4962
  const match = content.match(/^name\s*=\s*([^\r\n]+)/m);
4280
4963
  if (match && match[1].trim()) {
4281
4964
  const rawName = match[1].trim();
@@ -4289,10 +4972,10 @@ function detectProjectManifest(targetDir) {
4289
4972
  } catch {
4290
4973
  }
4291
4974
  }
4292
- const setupPyPath = path4.join(targetDir, "setup.py");
4293
- if (fs4.existsSync(setupPyPath)) {
4975
+ const setupPyPath = path5.join(targetDir, "setup.py");
4976
+ if (fs5.existsSync(setupPyPath)) {
4294
4977
  try {
4295
- const content = fs4.readFileSync(setupPyPath, "utf-8");
4978
+ const content = fs5.readFileSync(setupPyPath, "utf-8");
4296
4979
  const match = content.match(/name\s*=\s*["']([^"']+)["']/);
4297
4980
  if (match && match[1].trim()) {
4298
4981
  const rawName = match[1].trim();
@@ -4306,10 +4989,10 @@ function detectProjectManifest(targetDir) {
4306
4989
  } catch {
4307
4990
  }
4308
4991
  }
4309
- const goModPath = path4.join(targetDir, "go.mod");
4310
- if (fs4.existsSync(goModPath)) {
4992
+ const goModPath = path5.join(targetDir, "go.mod");
4993
+ if (fs5.existsSync(goModPath)) {
4311
4994
  try {
4312
- const content = fs4.readFileSync(goModPath, "utf-8");
4995
+ const content = fs5.readFileSync(goModPath, "utf-8");
4313
4996
  const match = content.match(/^module\s+([^\r\n\s]+)/m);
4314
4997
  if (match && match[1].trim()) {
4315
4998
  const modPath = match[1].trim();
@@ -4333,48 +5016,49 @@ function detectProjectManifest(targetDir) {
4333
5016
 
4334
5017
  // src/upgrade.ts
4335
5018
  import crypto2 from "node:crypto";
4336
- import fs6 from "node:fs";
4337
- import path6 from "node:path";
5019
+ import fs7 from "node:fs";
5020
+ import os5 from "node:os";
5021
+ import path7 from "node:path";
4338
5022
 
4339
5023
  // src/server/daemon.ts
4340
- import fs5 from "node:fs";
4341
- import path5 from "node:path";
4342
- import os3 from "node:os";
5024
+ import fs6 from "node:fs";
5025
+ import path6 from "node:path";
5026
+ import os4 from "node:os";
4343
5027
  import http from "node:http";
4344
5028
  import { spawn, spawnSync } from "node:child_process";
4345
5029
  import { fileURLToPath } from "node:url";
4346
5030
  var __filename = fileURLToPath(import.meta.url);
4347
- var __dirname = path5.dirname(__filename);
5031
+ var __dirname = path6.dirname(__filename);
4348
5032
  function getGlobalEsedreDir() {
4349
5033
  if (process.env.ESEDRE_GLOBAL_DIR) {
4350
5034
  return process.env.ESEDRE_GLOBAL_DIR;
4351
5035
  }
4352
- return path5.join(os3.homedir(), ".esedre");
5036
+ return path6.join(os4.homedir(), ".esedre");
4353
5037
  }
4354
5038
  function ensureGlobalEsedreStore() {
4355
5039
  const globalDir = getGlobalEsedreDir();
4356
- const runDir = path5.join(globalDir, "run");
4357
- const logsDir = path5.join(globalDir, "logs");
5040
+ const runDir = path6.join(globalDir, "run");
5041
+ const logsDir = path6.join(globalDir, "logs");
4358
5042
  try {
4359
- fs5.mkdirSync(runDir, { recursive: true });
4360
- fs5.mkdirSync(logsDir, { recursive: true });
5043
+ fs6.mkdirSync(runDir, { recursive: true });
5044
+ fs6.mkdirSync(logsDir, { recursive: true });
4361
5045
  try {
4362
- const runEntries = fs5.readdirSync(runDir);
5046
+ const runEntries = fs6.readdirSync(runDir);
4363
5047
  for (const entry of runEntries) {
4364
5048
  if (entry.startsWith(".probe-")) {
4365
- fs5.unlinkSync(path5.join(runDir, entry));
5049
+ fs6.unlinkSync(path6.join(runDir, entry));
4366
5050
  }
4367
5051
  }
4368
5052
  } catch {
4369
5053
  }
4370
- const testProbe = path5.join(runDir, `.probe-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`);
5054
+ const testProbe = path6.join(runDir, `.probe-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`);
4371
5055
  try {
4372
- fs5.writeFileSync(testProbe, "ok", "utf-8");
5056
+ fs6.writeFileSync(testProbe, "ok", "utf-8");
4373
5057
  return { ok: true, path: globalDir };
4374
5058
  } finally {
4375
- if (fs5.existsSync(testProbe)) {
5059
+ if (fs6.existsSync(testProbe)) {
4376
5060
  try {
4377
- fs5.unlinkSync(testProbe);
5061
+ fs6.unlinkSync(testProbe);
4378
5062
  } catch {
4379
5063
  }
4380
5064
  }
@@ -4389,10 +5073,10 @@ function resolveStoreDir(workspaceRoot) {
4389
5073
  return globalStore.path;
4390
5074
  }
4391
5075
  if (workspaceRoot) {
4392
- const localStore = path5.join(workspaceRoot, ".esedre");
5076
+ const localStore = path6.join(workspaceRoot, ".esedre");
4393
5077
  try {
4394
- fs5.mkdirSync(path5.join(localStore, "run"), { recursive: true });
4395
- fs5.mkdirSync(path5.join(localStore, "logs"), { recursive: true });
5078
+ fs6.mkdirSync(path6.join(localStore, "run"), { recursive: true });
5079
+ fs6.mkdirSync(path6.join(localStore, "logs"), { recursive: true });
4396
5080
  return localStore;
4397
5081
  } catch {
4398
5082
  }
@@ -4401,32 +5085,32 @@ function resolveStoreDir(workspaceRoot) {
4401
5085
  }
4402
5086
  function getDaemonStateFile(port, workspaceRoot) {
4403
5087
  const store = resolveStoreDir(workspaceRoot);
4404
- return path5.join(store, "run", `daemon-${port}.json`);
5088
+ return path6.join(store, "run", `daemon-${port}.json`);
4405
5089
  }
4406
5090
  function getDaemonLogFile(port, workspaceRoot) {
4407
5091
  const store = resolveStoreDir(workspaceRoot);
4408
- return path5.join(store, "logs", `daemon-${port}.log`);
5092
+ return path6.join(store, "logs", `daemon-${port}.log`);
4409
5093
  }
4410
5094
  function getDaemonState(port, workspaceRoot) {
4411
5095
  const stateFile = getDaemonStateFile(port, workspaceRoot);
4412
- if (!fs5.existsSync(stateFile)) {
5096
+ if (!fs6.existsSync(stateFile)) {
4413
5097
  return null;
4414
5098
  }
4415
5099
  try {
4416
- const raw = fs5.readFileSync(stateFile, "utf-8").trim();
5100
+ const raw = fs6.readFileSync(stateFile, "utf-8").trim();
4417
5101
  if (!raw) {
4418
- fs5.unlinkSync(stateFile);
5102
+ fs6.unlinkSync(stateFile);
4419
5103
  return null;
4420
5104
  }
4421
5105
  const state = JSON.parse(raw);
4422
5106
  if (!state || typeof state.pid !== "number" || typeof state.port !== "number") {
4423
- fs5.unlinkSync(stateFile);
5107
+ fs6.unlinkSync(stateFile);
4424
5108
  return null;
4425
5109
  }
4426
5110
  return state;
4427
5111
  } catch {
4428
5112
  try {
4429
- fs5.unlinkSync(stateFile);
5113
+ fs6.unlinkSync(stateFile);
4430
5114
  } catch {
4431
5115
  }
4432
5116
  return null;
@@ -4497,7 +5181,7 @@ async function startDaemon(options = {}) {
4497
5181
  }
4498
5182
  }
4499
5183
  try {
4500
- fs5.unlinkSync(stateFile);
5184
+ fs6.unlinkSync(stateFile);
4501
5185
  } catch {
4502
5186
  }
4503
5187
  }
@@ -4516,40 +5200,40 @@ async function startDaemon(options = {}) {
4516
5200
  };
4517
5201
  }
4518
5202
  resolveStoreDir(workspaceRoot);
4519
- fs5.mkdirSync(path5.dirname(logFile), { recursive: true });
4520
- fs5.mkdirSync(path5.dirname(stateFile), { recursive: true });
5203
+ fs6.mkdirSync(path6.dirname(logFile), { recursive: true });
5204
+ fs6.mkdirSync(path6.dirname(stateFile), { recursive: true });
4521
5205
  const MAX_LOG_SIZE_BYTES = 10 * 1024 * 1024;
4522
5206
  try {
4523
- if (fs5.existsSync(logFile)) {
4524
- const stats = fs5.statSync(logFile);
5207
+ if (fs6.existsSync(logFile)) {
5208
+ const stats = fs6.statSync(logFile);
4525
5209
  if (stats.size > MAX_LOG_SIZE_BYTES) {
4526
5210
  const oldLogFile = `${logFile}.old`;
4527
5211
  try {
4528
- if (fs5.existsSync(oldLogFile)) fs5.unlinkSync(oldLogFile);
4529
- fs5.renameSync(logFile, oldLogFile);
5212
+ if (fs6.existsSync(oldLogFile)) fs6.unlinkSync(oldLogFile);
5213
+ fs6.renameSync(logFile, oldLogFile);
4530
5214
  } catch {
4531
- fs5.truncateSync(logFile, 0);
5215
+ fs6.truncateSync(logFile, 0);
4532
5216
  }
4533
5217
  }
4534
5218
  }
4535
5219
  } catch {
4536
5220
  }
4537
- const logFd = fs5.openSync(logFile, "a");
4538
- const isEsedreArgv1 = process.argv[1] && /esedre/i.test(path5.basename(process.argv[1]));
5221
+ const logFd = fs6.openSync(logFile, "a");
5222
+ const isEsedreArgv1 = process.argv[1] && /esedre/i.test(path6.basename(process.argv[1]));
4539
5223
  const cliCandidates = [
4540
- isEsedreArgv1 ? path5.resolve(process.argv[1]) : void 0,
4541
- path5.resolve(workspaceRoot, "dist", "esedre.mjs"),
4542
- path5.resolve(workspaceRoot, "../esedre/dist", "esedre.mjs"),
4543
- path5.resolve(__dirname, "esedre.mjs"),
4544
- path5.resolve(__dirname, "..", "dist", "esedre.mjs"),
4545
- path5.resolve(__dirname, "..", "..", "dist", "esedre.mjs"),
4546
- path5.resolve(__dirname, "..", "..", "bin", "esedre.js"),
4547
- path5.resolve(__dirname, "..", "..", "bin", "esedre.ts"),
4548
- path5.resolve(__dirname, "..", "esedre.mjs"),
4549
- path5.resolve(__dirname, "../../../esedre/dist", "esedre.mjs")
5224
+ isEsedreArgv1 ? path6.resolve(process.argv[1]) : void 0,
5225
+ path6.resolve(workspaceRoot, "dist", "esedre.mjs"),
5226
+ path6.resolve(workspaceRoot, "../esedre/dist", "esedre.mjs"),
5227
+ path6.resolve(__dirname, "esedre.mjs"),
5228
+ path6.resolve(__dirname, "..", "dist", "esedre.mjs"),
5229
+ path6.resolve(__dirname, "..", "..", "dist", "esedre.mjs"),
5230
+ path6.resolve(__dirname, "..", "..", "bin", "esedre.js"),
5231
+ path6.resolve(__dirname, "..", "..", "bin", "esedre.ts"),
5232
+ path6.resolve(__dirname, "..", "esedre.mjs"),
5233
+ path6.resolve(__dirname, "../../../esedre/dist", "esedre.mjs")
4550
5234
  ].filter(Boolean);
4551
- const cliPath = cliCandidates.find((c) => fs5.existsSync(c) && (c.endsWith(".mjs") || c.endsWith(".js") || c.endsWith(".ts"))) || cliCandidates[0];
4552
- if (!fs5.existsSync(cliPath)) {
5235
+ const cliPath = cliCandidates.find((c) => fs6.existsSync(c) && (c.endsWith(".mjs") || c.endsWith(".js") || c.endsWith(".ts"))) || cliCandidates[0];
5236
+ if (!fs6.existsSync(cliPath)) {
4553
5237
  throw new Error(`Could not locate Esedre executable. Searched candidates: ${cliCandidates.join(", ")}`);
4554
5238
  }
4555
5239
  const child = spawn(process.execPath, [cliPath, "start", "--foreground", "--port", String(port)], {
@@ -4561,7 +5245,7 @@ async function startDaemon(options = {}) {
4561
5245
  });
4562
5246
  child.unref();
4563
5247
  try {
4564
- fs5.closeSync(logFd);
5248
+ fs6.closeSync(logFd);
4565
5249
  } catch {
4566
5250
  }
4567
5251
  const pid = child.pid;
@@ -4602,13 +5286,13 @@ async function startDaemon(options = {}) {
4602
5286
  logFile
4603
5287
  };
4604
5288
  const tmpState = `${stateFile}.tmp.${Date.now()}`;
4605
- fs5.writeFileSync(tmpState, JSON.stringify(state, null, 2) + "\n", "utf-8");
5289
+ fs6.writeFileSync(tmpState, JSON.stringify(state, null, 2) + "\n", "utf-8");
4606
5290
  try {
4607
- fs5.renameSync(tmpState, stateFile);
5291
+ fs6.renameSync(tmpState, stateFile);
4608
5292
  } catch {
4609
- fs5.writeFileSync(stateFile, JSON.stringify(state, null, 2) + "\n", "utf-8");
5293
+ fs6.writeFileSync(stateFile, JSON.stringify(state, null, 2) + "\n", "utf-8");
4610
5294
  try {
4611
- fs5.unlinkSync(tmpState);
5295
+ fs6.unlinkSync(tmpState);
4612
5296
  } catch {
4613
5297
  }
4614
5298
  }
@@ -4671,8 +5355,8 @@ async function stopDaemon(options = {}) {
4671
5355
  }
4672
5356
  }
4673
5357
  try {
4674
- if (fs5.existsSync(stateFile)) {
4675
- fs5.unlinkSync(stateFile);
5358
+ if (fs6.existsSync(stateFile)) {
5359
+ fs6.unlinkSync(stateFile);
4676
5360
  }
4677
5361
  } catch {
4678
5362
  }
@@ -4709,7 +5393,7 @@ async function getDaemonStatus(options = {}) {
4709
5393
  }
4710
5394
  if (state && !isProcessAlive(state.pid)) {
4711
5395
  try {
4712
- fs5.unlinkSync(stateFile);
5396
+ fs6.unlinkSync(stateFile);
4713
5397
  } catch {
4714
5398
  }
4715
5399
  }
@@ -4727,26 +5411,26 @@ function printDaemonLogs(options = {}) {
4727
5411
  const port = resolvePorts(void 0, { port: options.port }).gateway;
4728
5412
  const workspaceRoot = options.workspaceRoot || process.cwd();
4729
5413
  const logFile = getDaemonLogFile(port, workspaceRoot);
4730
- if (!fs5.existsSync(logFile)) {
5414
+ if (!fs6.existsSync(logFile)) {
4731
5415
  console.log(`\x1B[33mNo logs found at: ${logFile}\x1B[0m`);
4732
5416
  return;
4733
5417
  }
4734
5418
  const count = options.lines || 40;
4735
5419
  let content = "";
4736
5420
  try {
4737
- const stats = fs5.statSync(logFile);
5421
+ const stats = fs6.statSync(logFile);
4738
5422
  const maxReadBytes = Math.min(stats.size, Math.max(256 * 1024, count * 2048));
4739
5423
  if (stats.size <= maxReadBytes) {
4740
- content = fs5.readFileSync(logFile, "utf-8");
5424
+ content = fs6.readFileSync(logFile, "utf-8");
4741
5425
  } else {
4742
- const fd = fs5.openSync(logFile, "r");
5426
+ const fd = fs6.openSync(logFile, "r");
4743
5427
  const buffer = Buffer.alloc(maxReadBytes);
4744
- fs5.readSync(fd, buffer, 0, maxReadBytes, stats.size - maxReadBytes);
4745
- fs5.closeSync(fd);
5428
+ fs6.readSync(fd, buffer, 0, maxReadBytes, stats.size - maxReadBytes);
5429
+ fs6.closeSync(fd);
4746
5430
  content = buffer.toString("utf-8");
4747
5431
  }
4748
5432
  } catch {
4749
- content = fs5.readFileSync(logFile, "utf-8");
5433
+ content = fs6.readFileSync(logFile, "utf-8");
4750
5434
  }
4751
5435
  const allLines = content.split(/\r?\n/);
4752
5436
  const sanitizedLines = allLines.slice(-count).map((line) => {
@@ -4931,7 +5615,11 @@ var HISTORIC_SKILL_HASHES = [
4931
5615
  "8611116c276fdfb598b965c716b251ce7c77c7f7",
4932
5616
  "05f129fb6e06a18df00215af105567abcbf0bc67",
4933
5617
  "5d330ecf8fc96f383946e0351adb60378afa70f0",
4934
- "23716c9db561e52e5115dc364fe154d3a7bc1e23"
5618
+ "23716c9db561e52e5115dc364fe154d3a7bc1e23",
5619
+ "75d50d5f9cf9348cca9d9b2466e04cf7992ef17e",
5620
+ "d0bd99ea93d9c65245b3be51188d4bc153024092",
5621
+ "e5d4fe4f2ae0bd464bad877ea1e5ab63cef747fa",
5622
+ "d25597e8c2c26cdc5873e5dbf02335032db07580"
4935
5623
  ];
4936
5624
  var ESEDRE_SKILL_TEMPLATE = `---
4937
5625
  name: esedre
@@ -4940,7 +5628,7 @@ description: Tooling and workflow reference for interacting with the Esedre deve
4940
5628
 
4941
5629
  # Esedre Planning & Roadmap Workflow
4942
5630
 
4943
- 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.
5631
+ 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/config.json\` or \`.esedre/esedre.json\`). Interact with tickets via in-repo shell wrappers in \`.esedre/\` (\`.esedre/ese\` on POSIX/macOS/Linux/Git Bash, or \`.\\.esedre\\ese.cmd\` on Windows PowerShell/CMD), or the global \`ese\` CLI.
4944
5632
 
4945
5633
  ## 0. Authoritative Ingress Hierarchy & Zero-Latency Context (\`.esedre/snapshot.json\`)
4946
5634
 
@@ -4949,38 +5637,49 @@ All ticket discovery, inspection, and state management strictly follow this posi
4949
5637
  1. **Step 1 (Default for Read-Only Inspection) - Local Snapshot Projection**:
4950
5638
  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\`).
4951
5639
  2. **Step 2 (Dynamic Queries & State Mutations) - Esedre MCP Tools**:
4952
- 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\`).
5640
+ 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_claim_ticket\`, \`esedre_renew_lease\`, \`esedre_release_ticket\`, \`esedre_list_leases\`, \`esedre_add_comment\`).
4953
5641
  3. **Step 3 (Terminal & Script Fallback) - In-Repo CLI Wrapper**:
4954
- Use \`.esedre/ese\` (\`.esedre/ese get\`, \`.esedre/ese plan\`, \`.esedre/ese update\`, \`.esedre/ese snapshot\`, \`.esedre/ese refresh\`).
5642
+ - **POSIX (macOS, Linux, Git Bash, WSL)**: \`.esedre/ese <command>\` (e.g. \`.esedre/ese get\`, \`.esedre/ese plan\`, \`.esedre/ese update\`, \`.esedre/ese lease\`, \`.esedre/ese snapshot\`).
5643
+ - **Windows (PowerShell / CMD)**: \`.\\.esedre\\ese.cmd <command>\` or \`.\\.esedre\\ese.ps1 <command>\` (do not invoke the extensionless POSIX script \`.esedre/ese\` directly in PowerShell or \`cmd.exe\`, as Windows will fail or prompt to select an application to open the file).
4955
5644
  4. **Storage Boundary**:
4956
5645
  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.
4957
5646
 
4958
5647
  ## 1. Model Context Protocol (MCP) Tools
4959
5648
 
4960
- When Esedre MCP is active in your agent session (\`.agents/mcp_config.json\`), use these tools directly:
5649
+ When Esedre MCP is active in your agent session (\`.agents/mcp.json\` or \`.agents/mcp_config.json\`), use these tools directly:
4961
5650
 
4962
5651
  | Tool | Purpose | Key Arguments |
4963
5652
  |---|---|---|
4964
- | \`esedre_list_tickets\` | List roadmap tickets with filters | \`project\` (e.g. \`Profe\`, \`Esedre\`, \`Alce\`), \`status\`, \`type\`, \`search\` |
4965
- | \`esedre_get_ticket\` | Full specification, summary, comments, revision & sha1 | \`ticketId\` (numeric e.g. \`96\` or compound e.g. \`Profe-96\`) |
5653
+ | \`esedre_list_tickets\` | List roadmap tickets with filters | \`project\` (e.g. \`Profe\`, \`Esedre\`, \`Alce\`), \`status\`, \`type\`, \`priority\`, \`milestone\`, \`isBlocked\`, \`linkedTo\`, \`search\`, \`includePlan\` |
5654
+ | \`esedre_get_ticket\` | Full specification, summary, comments, links, lease, revision & sha1 | \`ticketId\` (numeric e.g. \`96\` or compound e.g. \`Profe-96\`) |
4966
5655
  | \`esedre_get_plan\` | Active implementation plan markdown | \`ticketId\` (numeric or compound) |
4967
5656
  | \`esedre_save_plan\` | Save implementation plan markdown with OCC | \`ticketId\`, \`planMarkdown\`, \`lastHash\` |
4968
- | \`esedre_create_ticket\` | Mint a new ticket with auto sequential ID | \`title\` (max 48 chars), \`type\`, \`project\`, \`effort\`, \`summary\`, \`detail\` |
4969
- | \`esedre_update_ticket\` | Update ticket attributes with OCC | \`ticketId\`, \`status\`, \`type\`, \`title\`, \`complexity\`, \`effort\`, \`inDevelopment\`, \`featureFlag\`, \`lastHash\` |
4970
- | \`esedre_add_comment\` | Append developer or agent comment | \`ticketId\`, \`text\`, \`author\` |
5657
+ | \`esedre_create_ticket\` | Mint a new ticket with auto sequential ID | \`title\` (max 48 chars), \`type\`, \`project\`, \`priority\`, \`milestone\`, \`effort\`, \`summary\`, \`detail\` |
5658
+ | \`esedre_update_ticket\` | Update ticket attributes with OCC | \`ticketId\`, \`status\`, \`type\`, \`priority\`, \`milestone\`, \`title\`, \`detail\`, \`detailMarkdown\`, \`complexity\`, \`effort\`, \`inDevelopment\`, \`featureFlag\`, \`lastHash\` |
5659
+ | \`esedre_claim_ticket\` | Claim an ephemeral TTL lease on a ticket (\`In Development\`) | \`ticketId\`, \`holder\`/\`workerId\`, \`agentType\`/\`system\`, \`ttlMinutes\`, \`branch\`, \`force\` |
5660
+ | \`esedre_renew_lease\` | Extend an active ticket lease TTL | \`ticketId\`, \`holder\`/\`workerId\`, \`system\`, \`ttlMinutes\` |
5661
+ | \`esedre_release_ticket\` | Release a ticket lease and optionally transition status | \`ticketId\`, \`holder\`/\`workerId\`, \`system\`, \`finalStatus\`, \`notes\`, \`force\` |
5662
+ | \`esedre_list_leases\` | List all active unexpired ticket leases | \`project\` |
5663
+ | \`esedre_link_ticket\` | Establish bi-directional ticket relationship | \`sourceTicketId\`, \`relation\` (\`relates-to\` \\| \`blocks\` \\| \`parent-of\` \\| \`duplicates\`), \`targetTicketId\`, \`author\` |
5664
+ | \`esedre_unlink_ticket\` | Remove bi-directional ticket relationship | \`sourceTicketId\`, \`targetTicketId\`, \`relation\` |
5665
+ | \`esedre_list_milestones\` | List project milestones and deliverables progress | \`project\` |
5666
+ | \`esedre_get_milestone\` | Get milestone specs, umbrella flag, and member tickets | \`milestoneId\` (numeric or title), \`project\` |
5667
+ | \`esedre_create_milestone\` | Create a project milestone with optional umbrella flag | \`title\`, \`project\`, \`featureFlag\`, \`status\`, \`targetDate\`, \`description\` |
5668
+ | \`esedre_update_milestone\` | Update milestone status, umbrella flag, or title | \`milestoneId\`, \`project\`, \`title\`, \`featureFlag\`, \`status\`, \`targetDate\`, \`description\` |
5669
+ | \`esedre_add_comment\` | Append developer or agent comment | \`ticketId\`, \`text\`, \`file\`, \`author\` |
4971
5670
 
4972
5671
  ## 2. Core CLI Commands (Fallback via \`run_command\`)
4973
5672
 
4974
- Both \`esedre\` and \`ese\` work interchangeably:
5673
+ Both \`esedre\` and \`ese\` work interchangeably. On POSIX shells use \`.esedre/ese <command>\`; on Windows PowerShell or CMD use \`.\\.esedre\\ese.cmd <command>\` (or \`.\\.esedre\\ese.ps1 <command>\`):
4975
5674
 
4976
5675
  ### List Tickets
4977
5676
  \`\`\`bash
4978
- .esedre/ese list [-p|--project <code|all>] [-s|--status <status>] [-t|--type <type>] [-q|--search <q>] [--json]
5677
+ .esedre/ese list [-p|--project <code|all>] [-s|--status <status>] [-t|--type <type>] [-P|--priority <priority>] [-m|--milestone <name>] [--blocked] [--linked-to <ticketKey>] [-q|--search <q>] [--json]
4979
5678
  \`\`\`
4980
5679
 
4981
5680
  ### Read Ticket Details
4982
5681
  \`\`\`bash
4983
- .esedre/ese get <ticketId> [--json]
5682
+ .esedre/ese get <ticketId...> [--json]
4984
5683
  \`\`\`
4985
5684
 
4986
5685
  ### Read or Update Implementation Plans
@@ -4992,22 +5691,61 @@ Both \`esedre\` and \`ese\` work interchangeably:
4992
5691
 
4993
5692
  ### Create a Ticket
4994
5693
  \`\`\`bash
4995
- .esedre/ese create --title "..." [-p|--project <code>] [-t|--type Feature] [--complexity Medium] [--effort "2.0 - 4.0 hours"] [--detail "<md>"] [--file <path>] [--json]
5694
+ .esedre/ese create --title "..." [-p|--project <code>] [-t|--type Feature] [-P|--priority High] [-m|--milestone <name>] [--complexity Medium] [--effort "2.0 - 4.0 hours"] [--detail "<md>"] [--file <path>] [--json]
4996
5695
  \`\`\`
4997
5696
 
4998
5697
  ### Update Ticket Status & Attributes
4999
5698
  \`\`\`bash
5000
5699
  .esedre/ese update <ticketId> --status "In Development"
5001
5700
  .esedre/ese update <ticketId> --status "Completed"
5002
- .esedre/ese update <ticketId> --type Feature --complexity Medium --effort "2.0 - 4.0 hours"
5701
+ .esedre/ese update <ticketId> --type Feature -P High --complexity Medium --effort "2.0 - 4.0 hours"
5702
+ .esedre/ese update <ticketId> -P none # Clears priority
5703
+ .esedre/ese update <ticketId> -m "Release 1.0" # Links ticket to milestone (inherits umbrella feature flag)
5704
+ .esedre/ese update <ticketId> -m none # Unlinks ticket from milestone
5705
+ .esedre/ese update <ticketId> --detail "<markdownContent>" # Updates specification markdown
5706
+ .esedre/ese update <ticketId> --file <detailFilePath> # Overwrites specification from file
5707
+ \`\`\`
5708
+
5709
+ ### Ticket Specification (\`detail.md\`) Sections
5710
+ When authoring or updating a ticket's \`detail.md\` (via \`--detail\`, \`--file\`, or \`esedre_update_ticket\`), the snapshot compiler and UI parser recognize the following canonical \`###\` headings:
5711
+ - \`### Summary\`: Concise overview of the problem or feature.
5712
+ - \`### Feature Breakdown\` (or aliases \`### Task Breakdown\`, \`### Fix Breakdown\`, \`### Implementation Breakdown\`): Numbered or bulleted deliverables and fix steps (indented sub-bullets are preserved under their parent item). Use \`### Fix Breakdown\` or \`### Task Breakdown\` naturally for \`Bug\` and \`Tools\` tickets.
5713
+ - \`### Technical Details\` (or \`### Technical Details & Architecture\`): Bold-keyed architectural notes (e.g. \`- **Architecture**: ...\`, \`- **Key Modules**: ...\`, \`- **Root Cause**: ...\`, \`- **Technical Detail**: ...\`).
5714
+ - \`### Open Questions\` (or \`### Open Questions & Decisions\`): Unresolved design questions (\`- **QuestionId**: Question text?\`), which populate \`openQuestions[]\` in \`.esedre/snapshot.json\`.
5715
+
5716
+ ### Ticket Leases (\`ese lease\`)
5717
+ \`\`\`bash
5718
+ .esedre/ese lease claim <ticketId> [--worker-id <id>] [--system <name>] [--ttl <minutes>] [--branch <branch>] [--force] [--json]
5719
+ .esedre/ese lease renew <ticketId> [--worker-id <id>] [--system <name>] [--ttl <minutes>] [--json]
5720
+ .esedre/ese lease release <ticketId> [--worker-id <id>] [--system <name>] [--status <Completed|Planned>] [--notes "..."] [--force] [--json]
5721
+ .esedre/ese lease list [-p|--project <code|all>] [--json]
5722
+ # Top-level aliases: ese claim, ese renew, ese release, ese leases
5003
5723
  \`\`\`
5004
5724
 
5005
5725
  ### Append Comments or Notes
5006
5726
  \`\`\`bash
5007
5727
  .esedre/ese comment <ticketId> "Verified implementation." --author "Agent"
5728
+ .esedre/ese comment <ticketId> --file <commentFilePath> --author "Agent"
5008
5729
  .esedre/ese comment <ticketId> --text "Verified implementation." --author "Agent"
5009
5730
  \`\`\`
5010
5731
 
5732
+ ### Link or Unlink Tickets
5733
+ \`\`\`bash
5734
+ .esedre/ese link <sourceId> <relation> <targetId> [--author "..."]
5735
+ .esedre/ese unlink <sourceId> <targetId> [--relation <relation>]
5736
+ # Supported relations: relates-to, blocks, parent-of, duplicates
5737
+ # Inverse reciprocal links are automatically synchronized.
5738
+ \`\`\`
5739
+
5740
+ ### Manage Milestones & Umbrella Feature Flags
5741
+ \`\`\`bash
5742
+ .esedre/ese milestone list [-p|--project <code>] [--json]
5743
+ .esedre/ese milestone get <id|title> [-p|--project <code>] [--json]
5744
+ .esedre/ese milestone create --title "..." [-p|--project <code>] [--flag <flag>] [--status <status>] [--target-date <YYYY-MM-DD>] [--description "..."] [--json]
5745
+ .esedre/ese milestone update <id|title> [-p|--project <code>] [--title "..."] [--flag <flag|none>] [--status <status>] [--target-date <YYYY-MM-DD>] [--description "..."] [--json]
5746
+ .esedre/ese milestone delete <id|title> [-p|--project <code>] [--json]
5747
+ \`\`\`
5748
+
5011
5749
  ### Regenerate Snapshot
5012
5750
  \`\`\`bash
5013
5751
  .esedre/ese snapshot
@@ -5050,7 +5788,10 @@ If the host application declares any or all of the 12 core design tokens on \`:r
5050
5788
  ## 4. Safety & Invariants
5051
5789
  - **Always use \`--json\`** for CLI programmatic inspection.
5052
5790
  - **Optimistic Concurrency**: Writes support \`--last-hash <hash>\` to prevent overwriting concurrent updates.
5791
+ - **Ticket Leases (\`ese lease\`)**: Claim a lease (\`ese lease claim <id>\` or \`esedre_claim_ticket\`) when starting active work so concurrent agent sessions or subagents do not collide on the same ticket, and release it (\`ese lease release <id>\`) upon completion.
5053
5792
  - **Universal Type Naming**: Always use \`type\` (\`Feature\`, \`Platform\`, \`Tools\`, \`Idea\`, \`Bug\`). The legacy name \`category\` is deprecated.
5793
+ - **Ticket Priority**: The \`priority\` field is optional (\`Critical\`, \`High\`, \`Medium\`, \`Low\`, or unset/null). Backwards compatible with legacy tickets lacking a priority field. To clear via CLI use \`-P none\` or \`--priority none\` (in code/MCP, pass \`null\` or \`'none'\`).
5794
+ - **Milestones & Umbrella Feature Flags**: Milestones group tickets into deliverables with completion metrics. Milestones can declare an optional umbrella feature flag which member tickets inherit automatically. Unlinking a ticket (\`-m none\`) removes inherited flags.
5054
5795
  - **Compound IDs for Multi-Project Portfolios**: In \`project=all\`, ticket IDs are \`\${projectCode}-\${id}\` (e.g. \`Profe-1\`, \`Esedre-1\`). DOM anchors strictly use \`feature-card-\${projectCode}-\${id}\`.
5055
5796
  - **Never delete tickets** directly via filesystem.
5056
5797
  - **Workspace Scoping**: An LLM coding partner operates strictly within the authorized project scope declared in the local repository configuration. Central repository linking and system-level configuration are managed separately; focus roadmap work, implementation plans, and verification notes on this project.
@@ -5059,9 +5800,38 @@ If the host application declares any or all of the 12 core design tokens on \`:r
5059
5800
  - **SEO Invariant**: These developer routes are private and MUST NEVER be exposed in \`sitemap.xml\`, \`robots.txt\`, \`llms.txt\`, or public navigation links.
5060
5801
  - The URL hash is preserved across hard page refreshes (F5) without falling back to \`#/chat\` or other views.
5061
5802
  `;
5803
+ var AGENT_RULE_MAX_BYTES = 24e3;
5804
+ function checkAgentRuleSizes(targetDir) {
5805
+ const candidates = [
5806
+ "AGENTS.md",
5807
+ "GEMINI.md",
5808
+ ".agents/AGENTS.md",
5809
+ ".agents/GEMINI.md",
5810
+ ".gemini/GEMINI.md"
5811
+ ];
5812
+ const warnings = [];
5813
+ for (const rel of candidates) {
5814
+ const fullPath = path7.join(targetDir, rel);
5815
+ if (fs7.existsSync(fullPath)) {
5816
+ try {
5817
+ const stat = fs7.statSync(fullPath);
5818
+ if (stat.isFile() && stat.size > AGENT_RULE_MAX_BYTES) {
5819
+ warnings.push({
5820
+ file: rel,
5821
+ bytes: stat.size,
5822
+ limitBytes: AGENT_RULE_MAX_BYTES,
5823
+ message: `${rel} is ${stat.size.toLocaleString()} bytes (exceeds ${AGENT_RULE_MAX_BYTES.toLocaleString()}-byte agent rule context limit and will be truncated; extract detailed specs into .agents/skills/<name>/SKILL.md).`
5824
+ });
5825
+ }
5826
+ } catch {
5827
+ }
5828
+ }
5829
+ }
5830
+ return warnings;
5831
+ }
5062
5832
  function plantWrappers(targetDir) {
5063
- const esedreDir = path6.join(targetDir, ".esedre");
5064
- fs6.mkdirSync(esedreDir, { recursive: true });
5833
+ const esedreDir = path7.join(targetDir, ".esedre");
5834
+ fs7.mkdirSync(esedreDir, { recursive: true });
5065
5835
  const wrappers = [
5066
5836
  { name: "esedre.cmd", content: WRAPPER_CMD, exec: false },
5067
5837
  { name: "esedre.ps1", content: WRAPPER_PS1, exec: false },
@@ -5071,62 +5841,117 @@ function plantWrappers(targetDir) {
5071
5841
  { name: "ese", content: ESE_WRAPPER_BASH, exec: true }
5072
5842
  ];
5073
5843
  for (const w of wrappers) {
5074
- const fullPath = path6.join(esedreDir, w.name);
5075
- fs6.writeFileSync(fullPath, w.content, "utf-8");
5844
+ const fullPath = path7.join(esedreDir, w.name);
5845
+ fs7.writeFileSync(fullPath, w.content, "utf-8");
5076
5846
  if (w.exec) {
5077
5847
  try {
5078
- fs6.chmodSync(fullPath, 493);
5848
+ fs7.chmodSync(fullPath, 493);
5079
5849
  } catch {
5080
5850
  }
5081
5851
  }
5082
5852
  }
5083
5853
  }
5084
- function setupMcpConfig(targetDir) {
5085
- const agentsDir = path6.join(targetDir, ".agents");
5086
- fs6.mkdirSync(agentsDir, { recursive: true });
5087
- const mcpConfigPath = path6.join(agentsDir, "mcp_config.json");
5088
- let configObj = { mcpServers: {} };
5089
- if (fs6.existsSync(mcpConfigPath)) {
5090
- try {
5091
- configObj = JSON.parse(fs6.readFileSync(mcpConfigPath, "utf-8"));
5092
- if (!configObj.mcpServers) configObj.mcpServers = {};
5093
- } catch {
5094
- }
5095
- }
5096
- if (process.platform === "win32") {
5097
- configObj.mcpServers.esedre = {
5098
- command: "cmd.exe",
5099
- args: ["/c", ".esedre\\esedre.cmd", "mcp"]
5100
- };
5101
- } else {
5102
- configObj.mcpServers.esedre = {
5103
- command: ".esedre/esedre",
5104
- args: ["mcp"]
5105
- };
5854
+ function setupGlobalAntigravityMcpConfig(options = {}) {
5855
+ const geminiDir = options.geminiDir || process.env.ESEDRE_GEMINI_DIR || (process.env.VITEST ? null : path7.join(os5.homedir(), ".gemini"));
5856
+ if (!geminiDir || !fs7.existsSync(geminiDir)) {
5857
+ return false;
5106
5858
  }
5107
5859
  try {
5108
- fs6.writeFileSync(mcpConfigPath, JSON.stringify(configObj, null, 2) + "\n", "utf-8");
5860
+ const configDir = path7.join(geminiDir, "config");
5861
+ fs7.mkdirSync(configDir, { recursive: true });
5862
+ const mcpConfigPath = path7.join(configDir, "mcp_config.json");
5863
+ let configObj = { mcpServers: {} };
5864
+ if (fs7.existsSync(mcpConfigPath)) {
5865
+ try {
5866
+ configObj = JSON.parse(fs7.readFileSync(mcpConfigPath, "utf-8"));
5867
+ if (!configObj || typeof configObj !== "object") configObj = {};
5868
+ if (!configObj.mcpServers || typeof configObj.mcpServers !== "object") {
5869
+ configObj.mcpServers = {};
5870
+ }
5871
+ } catch {
5872
+ configObj = { mcpServers: {} };
5873
+ }
5874
+ }
5875
+ const existing = configObj.mcpServers.esedre;
5876
+ const existingArgs = Array.isArray(existing?.args) ? existing.args.join(" ") : "";
5877
+ const isBareRelativeWrapper = existing?.command === ".esedre/esedre" || existing?.command === ".esedre\\esedre.cmd" || existing?.command === "cmd.exe" && existingArgs.includes(".esedre\\esedre.cmd") && !existingArgs.toLowerCase().includes("if exist");
5878
+ if (!existing || isBareRelativeWrapper) {
5879
+ if (process.platform === "win32") {
5880
+ const globalNpmEse = path7.join(
5881
+ process.env.APPDATA || path7.join(os5.homedir(), "AppData", "Roaming"),
5882
+ "npm",
5883
+ "ese.cmd"
5884
+ );
5885
+ const fallbackCmd = fs7.existsSync(globalNpmEse) ? `call "${globalNpmEse}" mcp` : "call ese mcp";
5886
+ configObj.mcpServers.esedre = {
5887
+ command: "cmd.exe",
5888
+ args: ["/c", `if exist .esedre\\esedre.cmd (call .esedre\\esedre.cmd mcp) else (${fallbackCmd})`]
5889
+ };
5890
+ } else {
5891
+ configObj.mcpServers.esedre = {
5892
+ command: "sh",
5893
+ args: ["-c", "if [ -x .esedre/esedre ]; then exec .esedre/esedre mcp; else exec ese mcp; fi"]
5894
+ };
5895
+ }
5896
+ fs7.writeFileSync(mcpConfigPath, JSON.stringify(configObj, null, 2) + "\n", "utf-8");
5897
+ }
5109
5898
  return true;
5110
5899
  } catch {
5111
5900
  return false;
5112
5901
  }
5113
5902
  }
5903
+ function setupMcpConfig(targetDir, options = {}) {
5904
+ const agentsDir = path7.join(targetDir, ".agents");
5905
+ fs7.mkdirSync(agentsDir, { recursive: true });
5906
+ const writeConfigFile = (fileName) => {
5907
+ const mcpConfigPath = path7.join(agentsDir, fileName);
5908
+ let configObj = { mcpServers: {} };
5909
+ if (fs7.existsSync(mcpConfigPath)) {
5910
+ try {
5911
+ configObj = JSON.parse(fs7.readFileSync(mcpConfigPath, "utf-8"));
5912
+ if (!configObj.mcpServers) configObj.mcpServers = {};
5913
+ } catch {
5914
+ }
5915
+ }
5916
+ if (process.platform === "win32") {
5917
+ configObj.mcpServers.esedre = {
5918
+ command: "cmd.exe",
5919
+ args: ["/c", ".esedre\\esedre.cmd", "mcp"]
5920
+ };
5921
+ } else {
5922
+ configObj.mcpServers.esedre = {
5923
+ command: ".esedre/esedre",
5924
+ args: ["mcp"]
5925
+ };
5926
+ }
5927
+ try {
5928
+ fs7.writeFileSync(mcpConfigPath, JSON.stringify(configObj, null, 2) + "\n", "utf-8");
5929
+ return true;
5930
+ } catch {
5931
+ return false;
5932
+ }
5933
+ };
5934
+ const okLegacy = writeConfigFile("mcp_config.json");
5935
+ const okModern = writeConfigFile("mcp.json");
5936
+ setupGlobalAntigravityMcpConfig(options);
5937
+ return okLegacy && okModern;
5938
+ }
5114
5939
  function setupSkill(targetDir, options = {}) {
5115
- const skillDir = path6.join(targetDir, ".agents", "skills", "esedre");
5116
- fs6.mkdirSync(skillDir, { recursive: true });
5117
- const skillFile = path6.join(skillDir, "SKILL.md");
5118
- if (!fs6.existsSync(skillFile)) {
5119
- fs6.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
5940
+ const skillDir = path7.join(targetDir, ".agents", "skills", "esedre");
5941
+ fs7.mkdirSync(skillDir, { recursive: true });
5942
+ const skillFile = path7.join(skillDir, "SKILL.md");
5943
+ if (!fs7.existsSync(skillFile)) {
5944
+ fs7.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
5120
5945
  return { status: "MISSING", updated: true };
5121
5946
  }
5122
- const currentContent = fs6.readFileSync(skillFile, "utf-8");
5947
+ const currentContent = fs7.readFileSync(skillFile, "utf-8");
5123
5948
  const status = classifyContent(currentContent, ESEDRE_SKILL_TEMPLATE, HISTORIC_SKILL_HASHES);
5124
5949
  if (status === "LATEST" && !options.force) {
5125
5950
  return { status, updated: false };
5126
5951
  }
5127
- const hasBannedOrDeprecatedTerms = currentContent.includes("companion agent") || currentContent.includes("companion LLM") || currentContent.includes("tools/esedre.mjs");
5952
+ const hasBannedOrDeprecatedTerms = currentContent.includes("companion agent") || currentContent.includes("companion LLM") || currentContent.includes("tools/esedre.mjs") || currentContent.includes("# Esedre Planning & Roadmap Workflow") && (!currentContent.includes("ese.cmd") || !currentContent.includes("Fix Breakdown") || !currentContent.includes("<ticketId...>"));
5128
5953
  if (options.force || status === "HISTORIC_DEFAULT" || status === "MISSING" || hasBannedOrDeprecatedTerms) {
5129
- fs6.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
5954
+ fs7.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
5130
5955
  return { status: "LATEST", updated: true };
5131
5956
  }
5132
5957
  return { status, updated: false };
@@ -5140,8 +5965,8 @@ var VITE_CONFIG_FILES = [
5140
5965
  ];
5141
5966
  function findViteConfig(targetDir) {
5142
5967
  for (const name of VITE_CONFIG_FILES) {
5143
- const fullPath = path6.join(targetDir, name);
5144
- if (fs6.existsSync(fullPath)) {
5968
+ const fullPath = path7.join(targetDir, name);
5969
+ if (fs7.existsSync(fullPath)) {
5145
5970
  return fullPath;
5146
5971
  }
5147
5972
  }
@@ -5205,10 +6030,10 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
5205
6030
  recommendedProxySnippet: snippet
5206
6031
  };
5207
6032
  }
5208
- const relativeName = path6.basename(configPath);
6033
+ const relativeName = path7.basename(configPath);
5209
6034
  let content = "";
5210
6035
  try {
5211
- content = fs6.readFileSync(configPath, "utf-8");
6036
+ content = fs7.readFileSync(configPath, "utf-8");
5212
6037
  } catch {
5213
6038
  return {
5214
6039
  status: "MANUAL_REQUIRED",
@@ -5239,7 +6064,7 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
5239
6064
  };
5240
6065
  }
5241
6066
  try {
5242
- fs6.writeFileSync(configPath, injected, "utf-8");
6067
+ fs7.writeFileSync(configPath, injected, "utf-8");
5243
6068
  return {
5244
6069
  status: "CONFIGURED",
5245
6070
  configFile: relativeName,
@@ -5254,15 +6079,15 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
5254
6079
  }
5255
6080
  }
5256
6081
  function configureWorkspace(targetDir, options = {}) {
5257
- const esedreDir = path6.join(targetDir, ".esedre");
5258
- if (!fs6.existsSync(esedreDir)) {
5259
- fs6.mkdirSync(esedreDir, { recursive: true });
6082
+ const esedreDir = path7.join(targetDir, ".esedre");
6083
+ if (!fs7.existsSync(esedreDir)) {
6084
+ fs7.mkdirSync(esedreDir, { recursive: true });
5260
6085
  }
5261
- const esedreJsonPath = path6.join(esedreDir, "esedre.json");
6086
+ const esedreJsonPath = path7.join(esedreDir, "esedre.json");
5262
6087
  let existingRaw = null;
5263
- if (fs6.existsSync(esedreJsonPath)) {
6088
+ if (fs7.existsSync(esedreJsonPath)) {
5264
6089
  try {
5265
- existingRaw = JSON.parse(fs6.readFileSync(esedreJsonPath, "utf-8"));
6090
+ existingRaw = JSON.parse(fs7.readFileSync(esedreJsonPath, "utf-8"));
5266
6091
  } catch {
5267
6092
  }
5268
6093
  }
@@ -5298,16 +6123,16 @@ function configureWorkspace(targetDir, options = {}) {
5298
6123
  port: options.port || void 0
5299
6124
  };
5300
6125
  }
5301
- fs6.writeFileSync(esedreJsonPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
6126
+ fs7.writeFileSync(esedreJsonPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
5302
6127
  let projectRegistered = false;
5303
6128
  if (projectCode) {
5304
- const hasExistingTicketsDir = fs6.existsSync(path6.join(targetDir, "tickets")) || fs6.existsSync(path6.join(targetDir, "src", "data", "planning", "tickets"));
5305
- const ticketsDir = path6.join(esedreDir, "tickets");
5306
- if (!hasExistingTicketsDir && !fs6.existsSync(ticketsDir)) {
5307
- fs6.mkdirSync(ticketsDir, { recursive: true });
6129
+ const hasExistingTicketsDir = fs7.existsSync(path7.join(targetDir, "tickets")) || fs7.existsSync(path7.join(targetDir, "src", "data", "planning", "tickets"));
6130
+ const ticketsDir = path7.join(esedreDir, "tickets");
6131
+ if (!hasExistingTicketsDir && !fs7.existsSync(ticketsDir)) {
6132
+ fs7.mkdirSync(ticketsDir, { recursive: true });
5308
6133
  }
5309
- const inRepoPJson = path6.join(esedreDir, "project.json");
5310
- if (!fs6.existsSync(inRepoPJson)) {
6134
+ const inRepoPJson = path7.join(esedreDir, "project.json");
6135
+ if (!fs7.existsSync(inRepoPJson)) {
5311
6136
  const projDesc = {
5312
6137
  id: 1,
5313
6138
  code: projectCode.toUpperCase(),
@@ -5319,14 +6144,14 @@ function configureWorkspace(targetDir, options = {}) {
5319
6144
  border: "border-cyan-500/40"
5320
6145
  }
5321
6146
  };
5322
- fs6.writeFileSync(inRepoPJson, JSON.stringify(projDesc, null, 2) + "\n", "utf-8");
6147
+ fs7.writeFileSync(inRepoPJson, JSON.stringify(projDesc, null, 2) + "\n", "utf-8");
5323
6148
  projectRegistered = true;
5324
6149
  } else if (projectName) {
5325
6150
  try {
5326
- const existing = JSON.parse(fs6.readFileSync(inRepoPJson, "utf-8"));
6151
+ const existing = JSON.parse(fs7.readFileSync(inRepoPJson, "utf-8"));
5327
6152
  if (existing.name !== projectName) {
5328
6153
  existing.name = projectName;
5329
- fs6.writeFileSync(inRepoPJson, JSON.stringify(existing, null, 2) + "\n", "utf-8");
6154
+ fs7.writeFileSync(inRepoPJson, JSON.stringify(existing, null, 2) + "\n", "utf-8");
5330
6155
  }
5331
6156
  } catch {
5332
6157
  }
@@ -5345,6 +6170,7 @@ function configureWorkspace(targetDir, options = {}) {
5345
6170
  if (!globalStoreRes.ok) {
5346
6171
  console.warn(`\x1B[33m\u26A0\uFE0F Warning: Could not initialize global store at ${globalStoreRes.path}: ${globalStoreRes.error}\x1B[0m`);
5347
6172
  }
6173
+ const ruleSizeWarnings = checkAgentRuleSizes(targetDir);
5348
6174
  return {
5349
6175
  esedreJsonCreatedOrUpdated: true,
5350
6176
  projectRegistered,
@@ -5359,7 +6185,8 @@ function configureWorkspace(targetDir, options = {}) {
5359
6185
  recommendedProxySnippet: proxyRes.recommendedProxySnippet,
5360
6186
  globalStoreStatus: globalStoreRes.ok ? "ready" : "warning",
5361
6187
  globalStorePath: globalStoreRes.path,
5362
- globalStoreError: globalStoreRes.error
6188
+ globalStoreError: globalStoreRes.error,
6189
+ ruleSizeWarnings
5363
6190
  };
5364
6191
  }
5365
6192
  var initWorkspace = configureWorkspace;
@@ -5369,33 +6196,33 @@ var ESEDRE_HUB_AGENTS_TEMPLATE = `# Agent Guidelines: esedre-data
5369
6196
  - **Backing Storage Repository**: \`esedre-data\` is a dedicated, decoupled data hub storing raw tickets, metadata manifests, and implementation plans managed by the Esedre engine.
5370
6197
  - **Authorized Ingress Channels**: Ticket discovery, read-only inspection, status updates, and plan authoring strictly ingress through the Esedre application layer in consuming project workspaces:
5371
6198
  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.
5372
- 2. **Esedre MCP Tools**: Use \`esedre_get_ticket\`, \`esedre_list_tickets\`, \`esedre_get_plan\`, \`esedre_save_plan\`, and \`esedre_update_ticket\`.
5373
- 3. **Esedre CLI**: Execute \`.esedre/ese get <id>\`, \`.esedre/ese list\`, or \`.esedre/ese plan\` from within the relevant project workspace.
6199
+ 2. **Esedre MCP Tools**: Use \`esedre_get_ticket\`, \`esedre_list_tickets\`, \`esedre_get_plan\`, \`esedre_save_plan\`, \`esedre_create_ticket\`, \`esedre_update_ticket\`, \`esedre_claim_ticket\`, \`esedre_renew_lease\`, \`esedre_release_ticket\`, \`esedre_list_leases\`, and \`esedre_add_comment\`.
6200
+ 3. **Esedre CLI**: Execute \`.esedre/ese get <id>\`, \`.esedre/ese list\`, \`.esedre/ese plan\`, \`.esedre/ese create\`, \`.esedre/ese update\`, \`.esedre/ese lease\`, \`.esedre/ese comment\`, \`.esedre/ese snapshot\`, or \`.esedre/ese refresh\` from within the relevant project workspace.
5374
6201
 
5375
6202
  ## 2. Ingress Redirection Protocol
5376
6203
  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.
5377
6204
  `;
5378
6205
  function initHub(targetDir) {
5379
- const resolved = path6.resolve(targetDir);
5380
- if (!fs6.existsSync(resolved)) {
5381
- fs6.mkdirSync(resolved, { recursive: true });
6206
+ const resolved = path7.resolve(targetDir);
6207
+ if (!fs7.existsSync(resolved)) {
6208
+ fs7.mkdirSync(resolved, { recursive: true });
5382
6209
  }
5383
- const projectsDir = path6.join(resolved, "projects");
6210
+ const projectsDir = path7.join(resolved, "projects");
5384
6211
  let projectsDirCreated = false;
5385
- if (!fs6.existsSync(projectsDir)) {
5386
- fs6.mkdirSync(projectsDir, { recursive: true });
6212
+ if (!fs7.existsSync(projectsDir)) {
6213
+ fs7.mkdirSync(projectsDir, { recursive: true });
5387
6214
  projectsDirCreated = true;
5388
6215
  }
5389
- const projectsJsonPath = path6.join(resolved, "projects.json");
6216
+ const projectsJsonPath = path7.join(resolved, "projects.json");
5390
6217
  let projectsJsonCreated = false;
5391
- if (!fs6.existsSync(projectsJsonPath)) {
5392
- fs6.writeFileSync(projectsJsonPath, "[]\n", "utf-8");
6218
+ if (!fs7.existsSync(projectsJsonPath)) {
6219
+ fs7.writeFileSync(projectsJsonPath, "[]\n", "utf-8");
5393
6220
  projectsJsonCreated = true;
5394
6221
  }
5395
- const agentsMdPath = path6.join(resolved, "AGENTS.md");
6222
+ const agentsMdPath = path7.join(resolved, "AGENTS.md");
5396
6223
  let agentsMdCreated = false;
5397
- if (!fs6.existsSync(agentsMdPath)) {
5398
- fs6.writeFileSync(agentsMdPath, ESEDRE_HUB_AGENTS_TEMPLATE, "utf-8");
6224
+ if (!fs7.existsSync(agentsMdPath)) {
6225
+ fs7.writeFileSync(agentsMdPath, ESEDRE_HUB_AGENTS_TEMPLATE, "utf-8");
5399
6226
  agentsMdCreated = true;
5400
6227
  }
5401
6228
  return {
@@ -5424,14 +6251,14 @@ function findOutdatedWrappers(currentWorkspaceRoot, globalConfig) {
5424
6251
  const outdated = [];
5425
6252
  const checkedDirs = /* @__PURE__ */ new Set();
5426
6253
  const checkDir = (dir, name) => {
5427
- const resolved = path6.resolve(expandHome(dir));
6254
+ const resolved = path7.resolve(expandHome(dir));
5428
6255
  if (checkedDirs.has(resolved.toLowerCase())) return;
5429
6256
  checkedDirs.add(resolved.toLowerCase());
5430
- if (!fs6.existsSync(resolved)) return;
5431
- const cmdPath = path6.join(resolved, ".esedre", "esedre.cmd");
5432
- if (fs6.existsSync(cmdPath)) {
6257
+ if (!fs7.existsSync(resolved)) return;
6258
+ const cmdPath = path7.join(resolved, ".esedre", "esedre.cmd");
6259
+ if (fs7.existsSync(cmdPath)) {
5433
6260
  try {
5434
- const content = fs6.readFileSync(cmdPath, "utf-8");
6261
+ const content = fs7.readFileSync(cmdPath, "utf-8");
5435
6262
  if (isWrapperOutdated(content)) {
5436
6263
  outdated.push({ name, dir: resolved, wrapperPath: cmdPath });
5437
6264
  }
@@ -5440,7 +6267,7 @@ function findOutdatedWrappers(currentWorkspaceRoot, globalConfig) {
5440
6267
  }
5441
6268
  };
5442
6269
  if (currentWorkspaceRoot) {
5443
- checkDir(currentWorkspaceRoot, path6.basename(currentWorkspaceRoot));
6270
+ checkDir(currentWorkspaceRoot, path7.basename(currentWorkspaceRoot));
5444
6271
  }
5445
6272
  if (globalConfig?.projects) {
5446
6273
  for (const [code, p] of Object.entries(globalConfig.projects)) {
@@ -5452,20 +6279,20 @@ function findOutdatedWrappers(currentWorkspaceRoot, globalConfig) {
5452
6279
  async function upgradeAllWorkspaces(options = {}) {
5453
6280
  const globalCfg = options.globalConfig !== void 0 ? options.globalConfig : readGlobalConfig();
5454
6281
  const workspaceMap = /* @__PURE__ */ new Map();
5455
- if (options.currentWorkspaceRoot && fs6.existsSync(options.currentWorkspaceRoot)) {
5456
- const hasEsedre = fs6.existsSync(path6.join(options.currentWorkspaceRoot, ".esedre")) || fs6.existsSync(path6.join(options.currentWorkspaceRoot, "esedre.json"));
6282
+ if (options.currentWorkspaceRoot && fs7.existsSync(options.currentWorkspaceRoot)) {
6283
+ const hasEsedre = fs7.existsSync(path7.join(options.currentWorkspaceRoot, ".esedre")) || fs7.existsSync(path7.join(options.currentWorkspaceRoot, "esedre.json"));
5457
6284
  if (hasEsedre) {
5458
6285
  const disc = findEsedreConfig(options.currentWorkspaceRoot, { fallbackToGlobal: true });
5459
6286
  workspaceMap.set(
5460
- path6.resolve(options.currentWorkspaceRoot),
5461
- disc.config?.projectCode || path6.basename(options.currentWorkspaceRoot)
6287
+ path7.resolve(options.currentWorkspaceRoot),
6288
+ disc.config?.projectCode || path7.basename(options.currentWorkspaceRoot)
5462
6289
  );
5463
6290
  }
5464
6291
  }
5465
6292
  if (globalCfg?.projects) {
5466
6293
  for (const [code, p] of Object.entries(globalCfg.projects)) {
5467
- const resolved = path6.resolve(expandHome(p));
5468
- if (fs6.existsSync(resolved)) {
6294
+ const resolved = path7.resolve(expandHome(p));
6295
+ if (fs7.existsSync(resolved)) {
5469
6296
  if (!workspaceMap.has(resolved)) {
5470
6297
  workspaceMap.set(resolved, code);
5471
6298
  }
@@ -5516,15 +6343,15 @@ async function upgradeAllWorkspaces(options = {}) {
5516
6343
  }
5517
6344
 
5518
6345
  // src/server/gateway.ts
5519
- import fs9 from "node:fs";
6346
+ import fs10 from "node:fs";
5520
6347
  import http4 from "node:http";
5521
- import path9 from "node:path";
6348
+ import path10 from "node:path";
5522
6349
  import { fileURLToPath as fileURLToPath2 } from "node:url";
5523
6350
 
5524
6351
  // src/server/uiServer.ts
5525
6352
  import http2 from "node:http";
5526
- import fs7 from "node:fs";
5527
- import path7 from "node:path";
6353
+ import fs8 from "node:fs";
6354
+ import path8 from "node:path";
5528
6355
  var MIME_TYPES = {
5529
6356
  ".html": "text/html; charset=utf-8",
5530
6357
  ".js": "application/javascript; charset=utf-8",
@@ -5554,27 +6381,27 @@ function startUiServer(port, webDir) {
5554
6381
  if (!reqPath || reqPath === "/") {
5555
6382
  reqPath = "/index.html";
5556
6383
  }
5557
- const safePath = path7.normalize(reqPath).replace(/^(\.\.[/\\])+/, "");
5558
- let filePath = path7.join(webDir, safePath);
5559
- const resolvedFilePath = path7.resolve(filePath);
5560
- const resolvedWebDir = path7.resolve(webDir);
6384
+ const safePath = path8.normalize(reqPath).replace(/^(\.\.[/\\])+/, "");
6385
+ let filePath = path8.join(webDir, safePath);
6386
+ const resolvedFilePath = path8.resolve(filePath);
6387
+ const resolvedWebDir = path8.resolve(webDir);
5561
6388
  if (!resolvedFilePath.startsWith(resolvedWebDir)) {
5562
6389
  res.writeHead(403, { "Content-Type": "text/plain" });
5563
6390
  res.end("Access Denied: Path escapes web root.");
5564
6391
  return;
5565
6392
  }
5566
- if (!fs7.existsSync(filePath) || fs7.statSync(filePath).isDirectory()) {
5567
- filePath = path7.join(webDir, "index.html");
6393
+ if (!fs8.existsSync(filePath) || fs8.statSync(filePath).isDirectory()) {
6394
+ filePath = path8.join(webDir, "index.html");
5568
6395
  }
5569
- if (!fs7.existsSync(filePath)) {
6396
+ if (!fs8.existsSync(filePath)) {
5570
6397
  res.writeHead(404, { "Content-Type": "text/plain" });
5571
6398
  res.end("Esedre UI: index.html not found. Run npm run build first.");
5572
6399
  return;
5573
6400
  }
5574
- const ext = path7.extname(filePath).toLowerCase();
6401
+ const ext = path8.extname(filePath).toLowerCase();
5575
6402
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
5576
6403
  try {
5577
- const content = fs7.readFileSync(filePath);
6404
+ const content = fs8.readFileSync(filePath);
5578
6405
  res.writeHead(200, { "Content-Type": contentType });
5579
6406
  res.end(content);
5580
6407
  } catch (err) {
@@ -5596,8 +6423,8 @@ function startUiServer(port, webDir) {
5596
6423
 
5597
6424
  // src/server/apiServer.ts
5598
6425
  import http3 from "node:http";
5599
- import fs8 from "node:fs";
5600
- import path8 from "node:path";
6426
+ import fs9 from "node:fs";
6427
+ import path9 from "node:path";
5601
6428
  var MAX_JSON_BODY_BYTES = 10 * 1024 * 1024;
5602
6429
  function readJsonBody(req) {
5603
6430
  return new Promise((resolve, reject) => {
@@ -5739,7 +6566,7 @@ function createApiHandler(storage, workspaceRoot) {
5739
6566
  const numKey = String(t.meta.id);
5740
6567
  const code = t.projectDescriptor?.code || t.meta.project || "Profe";
5741
6568
  const prefixedKey = `${code}-${t.meta.id}`;
5742
- const metaWithLinks = { ...t.meta, links: t.links || t.meta.links || [], isBlocked: t.isBlocked };
6569
+ const metaWithLinks = { ...t.meta, links: t.links || t.meta.links || [], isBlocked: t.isBlocked, lease: t.lease || t.meta.lease || null };
5743
6570
  metasMap[prefixedKey] = metaWithLinks;
5744
6571
  if (!metasMap[numKey] || code.toUpperCase() === "PROF") {
5745
6572
  metasMap[numKey] = metaWithLinks;
@@ -5893,16 +6720,16 @@ function createApiHandler(storage, workspaceRoot) {
5893
6720
  return true;
5894
6721
  }
5895
6722
  const filePath = reqStorage.getAttachmentPath ? reqStorage.getAttachmentPath(ticketId, file) : null;
5896
- if (!filePath || !fs8.existsSync(filePath)) {
6723
+ if (!filePath || !fs9.existsSync(filePath)) {
5897
6724
  sendJson(res, 404, { error: "Attachment not found." });
5898
6725
  return true;
5899
6726
  }
5900
- const stat = fs8.statSync(filePath);
6727
+ const stat = fs9.statSync(filePath);
5901
6728
  if (!stat.isFile()) {
5902
6729
  sendJson(res, 404, { error: "Attachment not found." });
5903
6730
  return true;
5904
6731
  }
5905
- const ext = path8.extname(filePath).toLowerCase();
6732
+ const ext = path9.extname(filePath).toLowerCase();
5906
6733
  const mimeTypes = {
5907
6734
  ".png": "image/png",
5908
6735
  ".jpg": "image/jpeg",
@@ -5920,7 +6747,7 @@ function createApiHandler(storage, workspaceRoot) {
5920
6747
  "Content-Length": stat.size,
5921
6748
  "Cache-Control": "public, max-age=86400"
5922
6749
  });
5923
- fs8.createReadStream(filePath).pipe(res);
6750
+ fs9.createReadStream(filePath).pipe(res);
5924
6751
  return true;
5925
6752
  }
5926
6753
  if (pathname === "/api/planning/plan-history") {
@@ -5943,6 +6770,12 @@ function createApiHandler(storage, workspaceRoot) {
5943
6770
  sendJson(res, 200, { milestones });
5944
6771
  return true;
5945
6772
  }
6773
+ if (pathname === "/api/planning/leases") {
6774
+ const project = url.searchParams.get("project") || void 0;
6775
+ const leases = await reqStorage.listLeases(project);
6776
+ sendJson(res, 200, { leases });
6777
+ return true;
6778
+ }
5946
6779
  }
5947
6780
  if (req.method === "POST") {
5948
6781
  const body = await readJsonBody(req);
@@ -6175,6 +7008,61 @@ function createApiHandler(storage, workspaceRoot) {
6175
7008
  sendJson(res, 200, { success: true, source: result.source, target: result.target });
6176
7009
  return true;
6177
7010
  }
7011
+ if (pathname === "/api/planning/leases/claim") {
7012
+ invalidateApiCache();
7013
+ const { ticketId, holder, agentType, hostname, sessionId, branch, worktree, ttlMinutes, force, project } = body;
7014
+ if (!ticketId) {
7015
+ sendJson(res, 400, { error: "ticketId is required." });
7016
+ return true;
7017
+ }
7018
+ const result = await reqStorage.claimTicket({
7019
+ ticketId,
7020
+ holder,
7021
+ agentType,
7022
+ hostname,
7023
+ sessionId,
7024
+ branch,
7025
+ worktree,
7026
+ ttlMinutes,
7027
+ force: Boolean(force),
7028
+ project
7029
+ });
7030
+ sendJson(res, 200, { success: true, ...result });
7031
+ return true;
7032
+ }
7033
+ if (pathname === "/api/planning/leases/renew") {
7034
+ invalidateApiCache();
7035
+ const { ticketId, holder, ttlMinutes, force, project } = body;
7036
+ if (!ticketId) {
7037
+ sendJson(res, 400, { error: "ticketId is required." });
7038
+ return true;
7039
+ }
7040
+ const lease = await reqStorage.renewLease({
7041
+ ticketId,
7042
+ holder,
7043
+ ttlMinutes,
7044
+ force: Boolean(force),
7045
+ project
7046
+ });
7047
+ sendJson(res, 200, { success: true, lease });
7048
+ return true;
7049
+ }
7050
+ if (pathname === "/api/planning/leases/release") {
7051
+ invalidateApiCache();
7052
+ const { ticketId, holder, force, project } = body;
7053
+ if (!ticketId) {
7054
+ sendJson(res, 400, { error: "ticketId is required." });
7055
+ return true;
7056
+ }
7057
+ const result = await reqStorage.releaseTicket({
7058
+ ticketId,
7059
+ holder,
7060
+ force: Boolean(force),
7061
+ project
7062
+ });
7063
+ sendJson(res, 200, { success: true, ...result });
7064
+ return true;
7065
+ }
6178
7066
  }
6179
7067
  sendJson(res, 404, { error: `Endpoint not found: ${pathname}` });
6180
7068
  return true;
@@ -6183,6 +7071,10 @@ function createApiHandler(storage, workspaceRoot) {
6183
7071
  sendJson(res, 403, { error: err.message });
6184
7072
  return true;
6185
7073
  }
7074
+ if (err.name === "EsedreLeaseConflictError") {
7075
+ sendJson(res, 409, { error: err.message, ticketKey: err.ticketKey, activeLease: err.activeLease });
7076
+ return true;
7077
+ }
6186
7078
  console.error("[Esedre API Error]:", err);
6187
7079
  sendJson(res, 500, { error: err.message });
6188
7080
  return true;
@@ -6211,35 +7103,35 @@ function startApiServer(port, storage, workspaceRoot) {
6211
7103
 
6212
7104
  // src/server/gateway.ts
6213
7105
  var __filename2 = fileURLToPath2(import.meta.url);
6214
- var __dirname2 = path9.dirname(__filename2);
7106
+ var __dirname2 = path10.dirname(__filename2);
6215
7107
  function resolveWebDir(explicitWebDir, workspaceRoot) {
6216
- if (explicitWebDir && fs9.existsSync(path9.join(explicitWebDir, "index.html"))) {
7108
+ if (explicitWebDir && fs10.existsSync(path10.join(explicitWebDir, "index.html"))) {
6217
7109
  return explicitWebDir;
6218
7110
  }
6219
7111
  const candidates = [
6220
7112
  // 1. Packaged layout right next to executable (e.g. tools/web)
6221
- path9.resolve(__dirname2, "web"),
7113
+ path10.resolve(__dirname2, "web"),
6222
7114
  // 2. From workspace root: <workspaceRoot>/dist/web
6223
- workspaceRoot ? path9.resolve(workspaceRoot, "dist", "web") : "",
7115
+ workspaceRoot ? path10.resolve(workspaceRoot, "dist", "web") : "",
6224
7116
  // 3. In-repo legacy subfolder: <workspaceRoot>/esedre/dist/web
6225
- workspaceRoot ? path9.resolve(workspaceRoot, "esedre", "dist", "web") : "",
7117
+ workspaceRoot ? path10.resolve(workspaceRoot, "esedre", "dist", "web") : "",
6226
7118
  // 4. Sibling standalone repository from workspace root: <workspaceRoot>/../esedre/dist/web
6227
- workspaceRoot ? path9.resolve(workspaceRoot, "..", "esedre", "dist", "web") : "",
7119
+ workspaceRoot ? path10.resolve(workspaceRoot, "..", "esedre", "dist", "web") : "",
6228
7120
  // 5. Packaged layout inside dist/web
6229
- path9.resolve(__dirname2, "dist", "web"),
7121
+ path10.resolve(__dirname2, "dist", "web"),
6230
7122
  // 6. Sibling standalone repository from __dirname: <__dirname>/../../esedre/dist/web
6231
- path9.resolve(__dirname2, "..", "..", "esedre", "dist", "web"),
7123
+ path10.resolve(__dirname2, "..", "..", "esedre", "dist", "web"),
6232
7124
  // 7. Source tree layout: esedre/src/server/ -> esedre/dist/web
6233
- path9.resolve(__dirname2, "..", "..", "dist", "web"),
7125
+ path10.resolve(__dirname2, "..", "..", "dist", "web"),
6234
7126
  // 8. Legacy layout from __dirname
6235
- path9.resolve(__dirname2, "..", "esedre", "dist", "web")
7127
+ path10.resolve(__dirname2, "..", "esedre", "dist", "web")
6236
7128
  ].filter(Boolean);
6237
7129
  for (const candidate of candidates) {
6238
- if (fs9.existsSync(path9.join(candidate, "index.html"))) {
7130
+ if (fs10.existsSync(path10.join(candidate, "index.html"))) {
6239
7131
  return candidate;
6240
7132
  }
6241
7133
  }
6242
- return candidates[0] || path9.resolve(__dirname2, "web");
7134
+ return candidates[0] || path10.resolve(__dirname2, "web");
6243
7135
  }
6244
7136
  function proxyRequest(req, res, targetPort, rewritePath) {
6245
7137
  const targetPath = rewritePath ? rewritePath(req.url || "/") : req.url || "/";
@@ -6350,8 +7242,8 @@ ${colors.bold}ROADMAP COMMANDS (Pair Programming & LLM Agents):${colors.reset}
6350
7242
  \u2022 Status values: 'Planned', 'In Development', 'Completed', 'Rejected'
6351
7243
  \u2022 Type values: 'Feature', 'Platform', 'Tools', 'Idea', 'Bug'
6352
7244
 
6353
- ${colors.bold}get${colors.reset} <id> [--json]
6354
- Inspect full ticket specification, feature breakdown, and comments.
7245
+ ${colors.bold}get${colors.reset} <id...> [--json]
7246
+ Inspect full ticket specification, feature breakdown, and comments for one or more tickets.
6355
7247
 
6356
7248
  ${colors.bold}plan${colors.reset} <id> [--file <path> | --set "<markdown>"] [--last-hash <sha1>] [--json]
6357
7249
  View or update implementation plan with optimistic concurrency control.
@@ -6362,7 +7254,7 @@ ${colors.bold}ROADMAP COMMANDS (Pair Programming & LLM Agents):${colors.reset}
6362
7254
  ${colors.bold}update${colors.reset} <id> [-s|--status <status>] [-t|--type <type>] [-P|--priority <prio>] [--title "..."] [--complexity <c>] [--effort "<e>"] [--detail "<md>"] [--file <path>] [--in-dev] [--flag <name>] [--last-hash <sha1>] [--force] [--json]
6363
7255
  Mutate ticket status, type, priority, title, complexity, effort, active state, or feature flag with OCC protection.
6364
7256
 
6365
- ${colors.bold}comment${colors.reset} <id> ["<text>"] [--text "..."] [--author "..."] [--json]
7257
+ ${colors.bold}comment${colors.reset} <id> ["<text>"] [--file <path>] [--text "..."] [--author "..."] [--json]
6366
7258
  Append a research finding, test verification, or note to ticket history.
6367
7259
 
6368
7260
  ${colors.bold}link${colors.reset} <sourceId> <relation> <targetId> [--author "..."] [--json]
@@ -6372,6 +7264,9 @@ ${colors.bold}ROADMAP COMMANDS (Pair Programming & LLM Agents):${colors.reset}
6372
7264
  ${colors.bold}unlink${colors.reset} <sourceId> <targetId> [--json]
6373
7265
  Remove a link between two tickets and its reciprocal link.
6374
7266
 
7267
+ ${colors.bold}lease${colors.reset} [claim <id> | release <id> | renew <id> | list] [--by|--worker-id <holder>] [--ttl <mins>] [--branch <b>] [--status <stat>] [--notes "..."] [--force] [--json]
7268
+ Coordinate multi-agent / multi-machine ticket ownership via TTL-backed advisory leases.
7269
+
6375
7270
  ${colors.bold}snapshot, refresh${colors.reset} [--project <code>] [--json]
6376
7271
  Generate lean read-only projection snapshot (.esedre/snapshot.json) for zero-latency agent context.
6377
7272
 
@@ -6439,7 +7334,7 @@ ${colors.bold}OPTIONS:${colors.reset}
6439
7334
  --blocked Filter tickets that are blocked by uncompleted tickets.
6440
7335
  --linked-to <key> Filter tickets that are linked to a specific ticket.
6441
7336
  --detail "<md>" Specification markdown for ticket detail during create or update.
6442
- --file <path> Path to markdown file for detail (create, update) or implementation plan (plan).
7337
+ --file <path> Path to markdown file for detail (create, update), implementation plan (plan), or comment (comment).
6443
7338
  --json Output raw machine-readable JSON (strongly recommended for autonomous LLM coding agents).
6444
7339
  --last-hash <hash> Optimistic concurrency control: last known sha1 hash of the ticket from 'get'.
6445
7340
  --force Bypass optimistic concurrency last-hash conflict checks on writes.
@@ -6512,7 +7407,7 @@ function printDuplicateProjectWarnings(storage, isJson) {
6512
7407
  const warnings = storage.getDuplicateProjectWarnings ? storage.getDuplicateProjectWarnings() : [];
6513
7408
  for (const warn of warnings) {
6514
7409
  console.error(
6515
- `${colors.yellow}\u26A0\uFE0F Warning: Duplicate project code '${warn.code}' detected across multiple data hubs (${path10.basename(warn.firstHub)} and ${path10.basename(warn.duplicateHub)}). Duplicate project codes across hubs are not supported; only '${path10.basename(warn.firstHub)}' is active.${colors.reset}`
7410
+ `${colors.yellow}\u26A0\uFE0F Warning: Duplicate project code '${warn.code}' detected across multiple data hubs (${path11.basename(warn.firstHub)} and ${path11.basename(warn.duplicateHub)}). Duplicate project codes across hubs are not supported; only '${path11.basename(warn.firstHub)}' is active.${colors.reset}`
6516
7411
  );
6517
7412
  }
6518
7413
  }
@@ -6531,6 +7426,7 @@ async function main() {
6531
7426
  }
6532
7427
  const rawArgs = process.argv.slice(2);
6533
7428
  const { command, positionals, flags } = parseArgs(rawArgs);
7429
+ initLogger({ debug: Boolean(flags["debug"]) });
6534
7430
  try {
6535
7431
  writeGlobalVersion();
6536
7432
  } catch {
@@ -6555,7 +7451,7 @@ async function main() {
6555
7451
  switch (command) {
6556
7452
  case "init": {
6557
7453
  const rawTarget = positionals[0];
6558
- const targetDir = rawTarget ? path10.resolve(rawTarget) : discovered.workspaceRoot;
7454
+ const targetDir = rawTarget ? path11.resolve(rawTarget) : discovered.workspaceRoot;
6559
7455
  const rawHubFlag = flags["hub"];
6560
7456
  const hubArg = typeof rawHubFlag === "string" ? rawHubFlag : void 0;
6561
7457
  const isHubRequested = Boolean(rawHubFlag);
@@ -6634,7 +7530,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
6634
7530
  console.error(`${colors.red}Error: ${nameVal.error}${colors.reset}`);
6635
7531
  process.exit(1);
6636
7532
  }
6637
- const isTargetHub = fs10.existsSync(path10.join(targetDir, "projects.json")) || fs10.existsSync(path10.join(targetDir, "projects")) && !fs10.existsSync(path10.join(targetDir, ".esedre", "esedre.json"));
7533
+ const isTargetHub = fs11.existsSync(path11.join(targetDir, "projects.json")) || fs11.existsSync(path11.join(targetDir, "projects")) && !fs11.existsSync(path11.join(targetDir, ".esedre", "esedre.json"));
6638
7534
  const isWorkspaceless = isHubRequested || isTargetHub || Boolean(discovered.config?.projectCode) && !rawTarget && discovered.config?.projectCode?.toLowerCase() !== projectCode.toLowerCase();
6639
7535
  let projectNewlyRegistered = false;
6640
7536
  let registeredProj;
@@ -6705,7 +7601,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
6705
7601
  const viteConfigPath = findViteConfig(targetDir);
6706
7602
  if (viteConfigPath && setupProxy === void 0 && !isJson) {
6707
7603
  try {
6708
- const rawContent = fs10.readFileSync(viteConfigPath, "utf-8");
7604
+ const rawContent = fs11.readFileSync(viteConfigPath, "utf-8");
6709
7605
  const alreadyConfigured = rawContent.includes("'/esedre'") || rawContent.includes('"/esedre"');
6710
7606
  if (!alreadyConfigured) {
6711
7607
  if (isYes) {
@@ -6715,7 +7611,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
6715
7611
  const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
6716
7612
  try {
6717
7613
  const answer = await rl.question(
6718
- `${colors.cyan}?${colors.reset} Detected ${colors.bold}${path10.basename(viteConfigPath)}${colors.reset}. Would you like to configure a reverse proxy for ${colors.bold}/esedre${colors.reset} targeting http://127.0.0.1:${port}? [Y/n] `
7614
+ `${colors.cyan}?${colors.reset} Detected ${colors.bold}${path11.basename(viteConfigPath)}${colors.reset}. Would you like to configure a reverse proxy for ${colors.bold}/esedre${colors.reset} targeting http://127.0.0.1:${port}? [Y/n] `
6719
7615
  );
6720
7616
  const trimmed = answer.trim().toLowerCase();
6721
7617
  setupProxy = trimmed === "" || trimmed === "y" || trimmed === "yes";
@@ -6731,10 +7627,10 @@ ${colors.bold}Next Steps:${colors.reset}`);
6731
7627
  let hubRelPath = void 0;
6732
7628
  if (rawHub) {
6733
7629
  const singleHub = Array.isArray(rawHub) ? rawHub[0] : rawHub;
6734
- const resolvedHub = path10.resolve(expandHome(singleHub));
6735
- if (fs10.existsSync(resolvedHub)) {
6736
- const rel = path10.relative(targetDir, resolvedHub).replace(/\\/g, "/");
6737
- const isSubdir = !rel.startsWith("..") && !path10.isAbsolute(rel);
7630
+ const resolvedHub = path11.resolve(expandHome(singleHub));
7631
+ if (fs11.existsSync(resolvedHub)) {
7632
+ const rel = path11.relative(targetDir, resolvedHub).replace(/\\/g, "/");
7633
+ const isSubdir = !rel.startsWith("..") && !path11.isAbsolute(rel);
6738
7634
  if (isSubdir || Boolean(flags["data-dir"])) {
6739
7635
  hubRelPath = rel.startsWith(".") ? rel : "./" + rel;
6740
7636
  }
@@ -6790,9 +7686,14 @@ ${colors.bold}Next Steps:${colors.reset}`);
6790
7686
  console.log(` \u2022 In-repo wrappers: .esedre/esedre.cmd, .esedre/ese.cmd, .esedre/esedre, .esedre/ese`);
6791
7687
  console.log(` \u2022 .gitignore: ${res.gitignoreUpdated ? "Added .esedre/snapshot.json" : "Already configured"}`);
6792
7688
  console.log(` \u2022 Snapshot: Generated .esedre/snapshot.json (${snapshotTickets} tickets projected)`);
6793
- console.log(` \u2022 MCP: ${res.mcpConfigured ? "Configured in .agents/mcp_config.json" : "Skipped"}`);
7689
+ console.log(` \u2022 MCP: ${res.mcpConfigured ? "Configured in .agents/mcp_config.json and .agents/mcp.json" : "Skipped"}`);
6794
7690
  console.log(` \u2022 Skill: ${res.skillConfigured ? "Updated .agents/skills/esedre/SKILL.md" : `Retained (${res.skillStatus})`}`);
6795
7691
  console.log(` \u2022 Central Config: Linked into ~/.esedre/config.json`);
7692
+ if (res.ruleSizeWarnings && res.ruleSizeWarnings.length > 0) {
7693
+ for (const rw of res.ruleSizeWarnings) {
7694
+ console.log(` \u2022 ${colors.yellow}\u26A0\uFE0F Rule Size Warning: ${rw.message}${colors.reset}`);
7695
+ }
7696
+ }
6796
7697
  if (res.viteProxyStatus === "CONFIGURED") {
6797
7698
  console.log(` \u2022 ${colors.green}Vite Proxy: Added /esedre proxy to ${res.viteConfigFile} targeting http://127.0.0.1:${effectivePort}${colors.reset}`);
6798
7699
  } else if (res.viteProxyStatus === "ALREADY_CONFIGURED") {
@@ -6908,7 +7809,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
6908
7809
  }
6909
7810
  console.log(`
6910
7811
  ${colors.bold}${colors.cyan}Esedre Central Configuration${colors.reset}`);
6911
- console.log(` \u2022 Config File: ${configPath} ${fs10.existsSync(configPath) ? colors.green + "(active)" + colors.reset : colors.dim + "(default)" + colors.reset}`);
7812
+ console.log(` \u2022 Config File: ${configPath} ${fs11.existsSync(configPath) ? colors.green + "(active)" + colors.reset : colors.dim + "(default)" + colors.reset}`);
6912
7813
  console.log(` \u2022 Default Port: ${globalCfg.port || DEFAULT_ESEDRE_PORT}`);
6913
7814
  console.log(`
6914
7815
  ${colors.bold}Data Hubs (dataDir):${colors.reset}`);
@@ -6974,11 +7875,16 @@ ${colors.bold}${colors.green}\u2714 Esedre multi-workspace upgrade completed acr
6974
7875
  } else if (w.upgrade.skillStatus === "CUSTOMIZED") {
6975
7876
  console.log(` - Preserved customized .agents/skills/esedre/SKILL.md`);
6976
7877
  }
7878
+ if (w.upgrade.ruleSizeWarnings && w.upgrade.ruleSizeWarnings.length > 0) {
7879
+ for (const rw of w.upgrade.ruleSizeWarnings) {
7880
+ console.log(` - ${colors.yellow}\u26A0\uFE0F ${rw.message}${colors.reset}`);
7881
+ }
7882
+ }
6977
7883
  }
6978
7884
  }
6979
7885
  return;
6980
7886
  }
6981
- const targetDir = positionals[0] ? path10.resolve(positionals[0]) : discovered.workspaceRoot;
7887
+ const targetDir = positionals[0] ? path11.resolve(positionals[0]) : discovered.workspaceRoot;
6982
7888
  const targetDiscovered = targetDir === discovered.workspaceRoot ? discovered : findEsedreConfig(targetDir, { fallbackToGlobal: true });
6983
7889
  const res = configureWorkspace(targetDir, {
6984
7890
  projectCode: targetDiscovered.config?.projectCode,
@@ -7005,6 +7911,11 @@ ${colors.bold}${colors.green}\u2714 Esedre multi-workspace upgrade completed acr
7005
7911
  } else if (res.skillStatus === "CUSTOMIZED") {
7006
7912
  console.log(` \u2022 ${colors.yellow}Notice: .agents/skills/esedre/SKILL.md is customized; preserved user modifications (use --force to overwrite).${colors.reset}`);
7007
7913
  }
7914
+ if (res.ruleSizeWarnings && res.ruleSizeWarnings.length > 0) {
7915
+ for (const rw of res.ruleSizeWarnings) {
7916
+ console.log(` \u2022 ${colors.yellow}\u26A0\uFE0F ${rw.message}${colors.reset}`);
7917
+ }
7918
+ }
7008
7919
  }
7009
7920
  return;
7010
7921
  }
@@ -7107,7 +8018,7 @@ ${colors.yellow}\u26A0\uFE0F Warning: Daemon is running v${status.version}, but
7107
8018
  }
7108
8019
  } else {
7109
8020
  console.log(`${colors.yellow}\u25CB Esedre daemon is STOPPED (port ${status.port})${colors.reset}`);
7110
- if (status.logFile && fs10.existsSync(status.logFile)) {
8021
+ if (status.logFile && fs11.existsSync(status.logFile)) {
7111
8022
  console.log(` \u2022 Recent Logs: ${status.logFile}`);
7112
8023
  }
7113
8024
  }
@@ -7238,7 +8149,9 @@ ${colors.yellow}Notice: ${outdatedWrappers.length} workspace(s) have legacy CMD
7238
8149
  const milestone = flags["milestone"] || flags["m"] || void 0;
7239
8150
  const isBlocked = flags["blocked"] ? true : void 0;
7240
8151
  const linkedTo = flags["linked-to"] || flags["linkedTo"] || void 0;
7241
- const tickets = await storage.listTickets({ project, status, type, category, priority, milestone, search, isBlocked, linkedTo });
8152
+ const leased = flags["leased"] !== void 0 ? Boolean(flags["leased"]) : void 0;
8153
+ const leasedBy = flags["leased-by"] || flags["leasedBy"] || void 0;
8154
+ const tickets = await storage.listTickets({ project, status, type, category, priority, milestone, search, isBlocked, linkedTo, leased, leasedBy });
7242
8155
  if (isJson) {
7243
8156
  console.log(JSON.stringify(tickets.map((t) => ({
7244
8157
  id: t.meta.id,
@@ -7255,7 +8168,8 @@ ${colors.yellow}Notice: ${outdatedWrappers.length} workspace(s) have legacy CMD
7255
8168
  project: t.projectDescriptor?.code || t.meta.project || "UNASSIGNED",
7256
8169
  sha1: t.sha1 || t.meta.sha1,
7257
8170
  isBlocked: t.isBlocked,
7258
- links: t.links
8171
+ links: t.links,
8172
+ lease: t.lease || null
7259
8173
  })), null, 2));
7260
8174
  } else {
7261
8175
  if (tickets.length === 0) {
@@ -7278,22 +8192,30 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
7278
8192
  return;
7279
8193
  }
7280
8194
  case "get": {
7281
- const idStr = positionals[0];
7282
- if (!idStr) {
8195
+ if (positionals.length === 0) {
7283
8196
  console.error(`${colors.red}Error: Ticket ID is required (e.g. ese get 1 or ese get Profe-1)${colors.reset}`);
7284
8197
  process.exit(1);
7285
8198
  }
7286
8199
  const projectFlag = flags["project"];
7287
- const lookupKey = projectFlag && /^\d+$/.test(idStr) ? `${projectFlag}-${idStr}` : idStr;
7288
- const ticket = await storage.getTicket(lookupKey);
7289
- if (!ticket) {
7290
- console.error(`${colors.red}Error: Ticket #${lookupKey} not found.${colors.reset}`);
7291
- process.exit(1);
8200
+ const tickets = [];
8201
+ for (const idStr of positionals) {
8202
+ const lookupKey = projectFlag && /^\d+$/.test(idStr) ? `${projectFlag}-${idStr}` : idStr;
8203
+ const ticket = await storage.getTicket(lookupKey);
8204
+ if (!ticket) {
8205
+ console.error(`${colors.red}Error: Ticket #${lookupKey} not found.${colors.reset}`);
8206
+ process.exit(1);
8207
+ }
8208
+ tickets.push(ticket);
7292
8209
  }
7293
8210
  if (isJson) {
7294
- console.log(JSON.stringify(ticket, null, 2));
8211
+ console.log(JSON.stringify(tickets.length === 1 ? tickets[0] : tickets, null, 2));
7295
8212
  } else {
7296
- console.log(formatTicketDetail(ticket));
8213
+ console.log(
8214
+ tickets.map((t) => formatTicketDetail(t)).join(`
8215
+ ${colors.dim}${"\u2500".repeat(60)}${colors.reset}
8216
+
8217
+ `)
8218
+ );
7297
8219
  }
7298
8220
  return;
7299
8221
  }
@@ -7312,11 +8234,11 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
7312
8234
  if (setPlan || filePath) {
7313
8235
  let content = setPlan;
7314
8236
  if (filePath) {
7315
- if (!fs10.existsSync(filePath)) {
8237
+ if (!fs11.existsSync(filePath)) {
7316
8238
  console.error(`${colors.red}Error: Plan file "${filePath}" not found.${colors.reset}`);
7317
8239
  process.exit(1);
7318
8240
  }
7319
- content = fs10.readFileSync(filePath, "utf-8");
8241
+ content = fs11.readFileSync(filePath, "utf-8");
7320
8242
  }
7321
8243
  await storage.savePlan(id, content, lastHash);
7322
8244
  if (isJson) {
@@ -7388,8 +8310,8 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
7388
8310
  const milestone = flags["milestone"] || flags["m"] || void 0;
7389
8311
  let detailMarkdown = detailArg;
7390
8312
  if (fileArg) {
7391
- const filePath = path10.resolve(fileArg);
7392
- if (!fs10.existsSync(filePath)) {
8313
+ const filePath = path11.resolve(fileArg);
8314
+ if (!fs11.existsSync(filePath)) {
7393
8315
  if (isJson) {
7394
8316
  console.error(JSON.stringify({ error: `Detail file "${fileArg}" not found` }));
7395
8317
  } else {
@@ -7397,7 +8319,7 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
7397
8319
  }
7398
8320
  process.exit(1);
7399
8321
  }
7400
- detailMarkdown = fs10.readFileSync(filePath, "utf-8");
8322
+ detailMarkdown = fs11.readFileSync(filePath, "utf-8");
7401
8323
  }
7402
8324
  const created = await storage.createTicket({
7403
8325
  title,
@@ -7467,8 +8389,8 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
7467
8389
  const fileArg = flags["file"];
7468
8390
  let detailMarkdown = detailArg ? normalizeDashesAndMojibake(detailArg) : void 0;
7469
8391
  if (fileArg) {
7470
- const filePath = path10.resolve(fileArg);
7471
- if (!fs10.existsSync(filePath)) {
8392
+ const filePath = path11.resolve(fileArg);
8393
+ if (!fs11.existsSync(filePath)) {
7472
8394
  if (isJson) {
7473
8395
  console.error(JSON.stringify({ error: `Detail file "${fileArg}" not found` }));
7474
8396
  } else {
@@ -7476,7 +8398,7 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
7476
8398
  }
7477
8399
  process.exit(1);
7478
8400
  }
7479
- detailMarkdown = normalizeDashesAndMojibake(fs10.readFileSync(filePath, "utf-8"));
8401
+ detailMarkdown = normalizeDashesAndMojibake(fs11.readFileSync(filePath, "utf-8"));
7480
8402
  }
7481
8403
  if (detailMarkdown !== void 0) updates.detailMarkdown = detailMarkdown;
7482
8404
  const updated = await storage.updateTicket(lookupKey, updates, lastHash);
@@ -7634,10 +8556,30 @@ ${colors.bold}Tickets:${colors.reset}`);
7634
8556
  }
7635
8557
  const projectFlag = flags["project"];
7636
8558
  const lookupKey = projectFlag && /^\d+$/.test(idStr) ? `${projectFlag}-${idStr}` : idStr;
7637
- const positionalText = positionals.slice(1).join(" ").trim();
7638
- const text = flags["text"] || (positionalText || void 0);
8559
+ let text;
8560
+ const filePath = flags["file"];
8561
+ if (filePath) {
8562
+ const resolvedPath = path11.resolve(process.cwd(), filePath);
8563
+ if (!fs11.existsSync(resolvedPath)) {
8564
+ console.error(`${colors.red}Error: File not found: ${filePath}${colors.reset}`);
8565
+ process.exit(1);
8566
+ }
8567
+ if (!fs11.statSync(resolvedPath).isFile()) {
8568
+ console.error(`${colors.red}Error: Path is not a file: ${filePath}${colors.reset}`);
8569
+ process.exit(1);
8570
+ }
8571
+ const fileContent = fs11.readFileSync(resolvedPath, "utf-8");
8572
+ if (!fileContent.trim()) {
8573
+ console.error(`${colors.red}Error: Comment file is empty: ${filePath}${colors.reset}`);
8574
+ process.exit(1);
8575
+ }
8576
+ text = fileContent;
8577
+ } else {
8578
+ const positionalText = positionals.slice(1).join(" ").trim();
8579
+ text = flags["text"] || (positionalText || void 0);
8580
+ }
7639
8581
  if (!text) {
7640
- console.error(`${colors.red}Error: Comment text is required (e.g. ese comment 96 "Verified tests" or --text "...")${colors.reset}`);
8582
+ console.error(`${colors.red}Error: Comment text or --file is required (e.g. ese comment 96 --file note.md or ese comment 96 "Verified tests")${colors.reset}`);
7641
8583
  process.exit(1);
7642
8584
  }
7643
8585
  const author = flags["author"] || "User";
@@ -7701,6 +8643,129 @@ ${colors.bold}Tickets:${colors.reset}`);
7701
8643
  }
7702
8644
  return;
7703
8645
  }
8646
+ case "claim":
8647
+ case "release":
8648
+ case "renew":
8649
+ case "leases":
8650
+ case "lease": {
8651
+ const isTopLevelAlias = command === "claim" || command === "release" || command === "renew" || command === "leases";
8652
+ const subAction = isTopLevelAlias ? command === "leases" ? "list" : command : positionals[0]?.toLowerCase() || "list";
8653
+ const targetArg = isTopLevelAlias ? positionals[0] : positionals[1];
8654
+ const project = flags["project"] || discovered.config?.projectCode;
8655
+ const holder = flags["by"] || flags["holder"] || flags["worker-id"] || flags["workerId"] || flags["author"] || void 0;
8656
+ const agentType = flags["agent"] || flags["agent-type"] || flags["system"] || void 0;
8657
+ const sessionId = flags["session"] || flags["session-id"] || void 0;
8658
+ const branch = flags["branch"] || void 0;
8659
+ const ttlRaw = flags["ttl"] !== void 0 ? Number(flags["ttl"]) : void 0;
8660
+ const isForce = Boolean(flags["force"] || flags["f"]);
8661
+ if (subAction === "list" || subAction === "ls") {
8662
+ const filterProject = flags["all"] || flags["a"] || flags["project"] === "all" ? "all" : project;
8663
+ const leases = await storage.listLeases(filterProject);
8664
+ if (isJson) {
8665
+ console.log(JSON.stringify({ total: leases.length, leases }, null, 2));
8666
+ } else {
8667
+ console.log(formatLeaseListTable(leases));
8668
+ if (leases.length > 0) {
8669
+ console.log(`
8670
+ ${colors.dim}Total: ${leases.length} active lease(s)${colors.reset}`);
8671
+ }
8672
+ }
8673
+ return;
8674
+ }
8675
+ if (subAction === "claim") {
8676
+ if (!targetArg) {
8677
+ console.error(`${colors.red}Error: Ticket ID is required (e.g. ese lease claim 69 --by "agent-1")${colors.reset}`);
8678
+ process.exit(1);
8679
+ }
8680
+ const identity = resolveWorkerIdentity({
8681
+ holder,
8682
+ agentType,
8683
+ sessionId,
8684
+ branch,
8685
+ worktree: discovered.workspaceRoot,
8686
+ configDefaultAuthor: discovered.config?.defaultAuthor
8687
+ });
8688
+ const res = await storage.claimTicket({
8689
+ ticketId: targetArg,
8690
+ holder: identity.holder,
8691
+ agentType: identity.agentType,
8692
+ hostname: identity.hostname,
8693
+ sessionId: identity.sessionId,
8694
+ branch: identity.branch,
8695
+ worktree: identity.worktree,
8696
+ ttlMinutes: ttlRaw,
8697
+ force: isForce,
8698
+ project
8699
+ });
8700
+ if (isJson) {
8701
+ console.log(JSON.stringify(res, null, 2));
8702
+ } else {
8703
+ const actionLabel = res.overridden ? "Overrode & claimed" : res.renewed ? "Renewed lease on" : "Claimed lease on";
8704
+ console.log(
8705
+ `${colors.green}\u2714 ${actionLabel} Ticket ${res.lease.ticketKey}${colors.reset} for ${colors.bold}${res.lease.holder}${colors.reset}${res.lease.hostname ? ` on ${res.lease.hostname}` : ""} (TTL: ${res.lease.ttlMinutes}m, expires ${res.lease.expiresAt})`
8706
+ );
8707
+ }
8708
+ return;
8709
+ }
8710
+ if (subAction === "renew" || subAction === "heartbeat") {
8711
+ if (!targetArg) {
8712
+ console.error(`${colors.red}Error: Ticket ID is required (e.g. ese lease renew 69)${colors.reset}`);
8713
+ process.exit(1);
8714
+ }
8715
+ const lease = await storage.renewLease({
8716
+ ticketId: targetArg,
8717
+ holder,
8718
+ ttlMinutes: ttlRaw,
8719
+ force: isForce,
8720
+ project
8721
+ });
8722
+ if (isJson) {
8723
+ console.log(JSON.stringify({ success: true, lease }, null, 2));
8724
+ } else {
8725
+ console.log(
8726
+ `${colors.green}\u2714 Renewed lease on Ticket ${lease.ticketKey}${colors.reset} for ${colors.bold}${lease.holder}${colors.reset} (expires ${lease.expiresAt})`
8727
+ );
8728
+ }
8729
+ return;
8730
+ }
8731
+ if (subAction === "release" || subAction === "unlock") {
8732
+ if (!targetArg) {
8733
+ console.error(`${colors.red}Error: Ticket ID is required (e.g. ese lease release 69)${colors.reset}`);
8734
+ process.exit(1);
8735
+ }
8736
+ const finalStatus = flags["status"] || void 0;
8737
+ const notes = flags["notes"] || void 0;
8738
+ const res = await storage.releaseTicket({
8739
+ ticketId: targetArg,
8740
+ holder,
8741
+ force: isForce,
8742
+ project
8743
+ });
8744
+ if (finalStatus) {
8745
+ await storage.updateTicket(res.ticketKey || targetArg, { status: finalStatus });
8746
+ }
8747
+ if (notes) {
8748
+ await storage.addComment(res.ticketKey || targetArg, {
8749
+ author: holder || res.previousLease?.holder || discovered.config?.defaultAuthor || "Agent",
8750
+ text: notes
8751
+ });
8752
+ }
8753
+ if (isJson) {
8754
+ console.log(JSON.stringify(res, null, 2));
8755
+ } else {
8756
+ if (res.released) {
8757
+ console.log(
8758
+ `${colors.green}\u2714 Released lease on Ticket ${res.ticketKey}${colors.reset}${res.previousLease ? ` (previously held by ${res.previousLease.holder})` : ""}${finalStatus ? ` -> ${finalStatus}` : ""}`
8759
+ );
8760
+ } else {
8761
+ console.log(`${colors.dim}Ticket ${res.ticketKey} had no active lease.${colors.reset}`);
8762
+ }
8763
+ }
8764
+ return;
8765
+ }
8766
+ console.error(`${colors.red}Error: Unknown lease action "${subAction}". Valid actions: claim, renew, release, list${colors.reset}`);
8767
+ process.exit(1);
8768
+ }
7704
8769
  default: {
7705
8770
  console.error(`${colors.red}Error: Unknown command "${command}"${colors.reset}`);
7706
8771
  printHelp();
@@ -7713,6 +8778,14 @@ ${colors.bold}Tickets:${colors.reset}`);
7713
8778
  console.error(`${colors.dim}Use --force to override if you are certain.${colors.reset}`);
7714
8779
  process.exit(1);
7715
8780
  }
8781
+ if (err instanceof EsedreLeaseConflictError || err.name === "EsedreLeaseConflictError") {
8782
+ if (isJson) {
8783
+ console.error(JSON.stringify({ error: err.message, ticketKey: err.ticketKey, activeLease: err.activeLease }, null, 2));
8784
+ } else {
8785
+ console.error(`${colors.red}${err.message}${colors.reset}`);
8786
+ }
8787
+ process.exit(1);
8788
+ }
7716
8789
  if (err instanceof EsedreAuthorizationError || err.name === "EsedreAuthorizationError") {
7717
8790
  console.error(`${colors.red}Access Denied: ${err.message}${colors.reset}`);
7718
8791
  process.exit(1);