esedre 1.2.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/CHANGELOG.md +30 -0
- package/README.md +24 -9
- package/dist/esedre.mjs +1168 -155
- package/dist/web/assets/{index-CqLRSVQd.js → index-Bx4_5X7t.js} +9 -7
- package/dist/web/assets/index-lVT1j3Rg.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-hrxboO8S.css +0 -2
package/dist/esedre.mjs
CHANGED
|
@@ -7,10 +7,10 @@ import path11 from "node:path";
|
|
|
7
7
|
// src/storage/filesystem.ts
|
|
8
8
|
import fs3 from "node:fs";
|
|
9
9
|
import path3 from "node:path";
|
|
10
|
-
import
|
|
10
|
+
import os3 from "node:os";
|
|
11
11
|
|
|
12
12
|
// src/types.ts
|
|
13
|
-
var CURRENT_ESEDRE_VERSION = "1.
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
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
|
|
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
|
|
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
|
|
2499
|
-
if (
|
|
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
|
|
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
|
|
2508
|
-
if (itemMatch)
|
|
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
|
|
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
|
|
2517
|
-
if (itemMatch)
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
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,6 +3997,59 @@ 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
|
|
@@ -3691,6 +4165,20 @@ var EsedreMcpServer = class {
|
|
|
3691
4165
|
}
|
|
3692
4166
|
};
|
|
3693
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
|
+
}
|
|
3694
4182
|
return {
|
|
3695
4183
|
jsonrpc: "2.0",
|
|
3696
4184
|
id,
|
|
@@ -3987,6 +4475,71 @@ Status: ${ticket.meta.status}`;
|
|
|
3987
4475
|
},
|
|
3988
4476
|
required: ["sourceTicketId", "targetTicketId"]
|
|
3989
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
|
+
}
|
|
3990
4543
|
}
|
|
3991
4544
|
];
|
|
3992
4545
|
}
|
|
@@ -4179,12 +4732,103 @@ Status: ${ticket.meta.status}`;
|
|
|
4179
4732
|
target: result.target
|
|
4180
4733
|
};
|
|
4181
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
|
+
}
|
|
4182
4811
|
default:
|
|
4183
4812
|
throw new Error(`Unknown tool: ${name}`);
|
|
4184
4813
|
}
|
|
4185
4814
|
}
|
|
4186
4815
|
};
|
|
4187
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
|
+
|
|
4188
4832
|
// src/utils/manifestDetector.ts
|
|
4189
4833
|
import fs5 from "node:fs";
|
|
4190
4834
|
import path5 from "node:path";
|
|
@@ -4373,12 +5017,13 @@ function detectProjectManifest(targetDir) {
|
|
|
4373
5017
|
// src/upgrade.ts
|
|
4374
5018
|
import crypto2 from "node:crypto";
|
|
4375
5019
|
import fs7 from "node:fs";
|
|
5020
|
+
import os5 from "node:os";
|
|
4376
5021
|
import path7 from "node:path";
|
|
4377
5022
|
|
|
4378
5023
|
// src/server/daemon.ts
|
|
4379
5024
|
import fs6 from "node:fs";
|
|
4380
5025
|
import path6 from "node:path";
|
|
4381
|
-
import
|
|
5026
|
+
import os4 from "node:os";
|
|
4382
5027
|
import http from "node:http";
|
|
4383
5028
|
import { spawn, spawnSync } from "node:child_process";
|
|
4384
5029
|
import { fileURLToPath } from "node:url";
|
|
@@ -4388,7 +5033,7 @@ function getGlobalEsedreDir() {
|
|
|
4388
5033
|
if (process.env.ESEDRE_GLOBAL_DIR) {
|
|
4389
5034
|
return process.env.ESEDRE_GLOBAL_DIR;
|
|
4390
5035
|
}
|
|
4391
|
-
return path6.join(
|
|
5036
|
+
return path6.join(os4.homedir(), ".esedre");
|
|
4392
5037
|
}
|
|
4393
5038
|
function ensureGlobalEsedreStore() {
|
|
4394
5039
|
const globalDir = getGlobalEsedreDir();
|
|
@@ -4970,7 +5615,11 @@ var HISTORIC_SKILL_HASHES = [
|
|
|
4970
5615
|
"8611116c276fdfb598b965c716b251ce7c77c7f7",
|
|
4971
5616
|
"05f129fb6e06a18df00215af105567abcbf0bc67",
|
|
4972
5617
|
"5d330ecf8fc96f383946e0351adb60378afa70f0",
|
|
4973
|
-
"23716c9db561e52e5115dc364fe154d3a7bc1e23"
|
|
5618
|
+
"23716c9db561e52e5115dc364fe154d3a7bc1e23",
|
|
5619
|
+
"75d50d5f9cf9348cca9d9b2466e04cf7992ef17e",
|
|
5620
|
+
"d0bd99ea93d9c65245b3be51188d4bc153024092",
|
|
5621
|
+
"e5d4fe4f2ae0bd464bad877ea1e5ab63cef747fa",
|
|
5622
|
+
"d25597e8c2c26cdc5873e5dbf02335032db07580"
|
|
4974
5623
|
];
|
|
4975
5624
|
var ESEDRE_SKILL_TEMPLATE = `---
|
|
4976
5625
|
name: esedre
|
|
@@ -4979,7 +5628,7 @@ description: Tooling and workflow reference for interacting with the Esedre deve
|
|
|
4979
5628
|
|
|
4980
5629
|
# Esedre Planning & Roadmap Workflow
|
|
4981
5630
|
|
|
4982
|
-
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.
|
|
4983
5632
|
|
|
4984
5633
|
## 0. Authoritative Ingress Hierarchy & Zero-Latency Context (\`.esedre/snapshot.json\`)
|
|
4985
5634
|
|
|
@@ -4988,38 +5637,49 @@ All ticket discovery, inspection, and state management strictly follow this posi
|
|
|
4988
5637
|
1. **Step 1 (Default for Read-Only Inspection) - Local Snapshot Projection**:
|
|
4989
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\`).
|
|
4990
5639
|
2. **Step 2 (Dynamic Queries & State Mutations) - Esedre MCP Tools**:
|
|
4991
|
-
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\`).
|
|
4992
5641
|
3. **Step 3 (Terminal & Script Fallback) - In-Repo CLI Wrapper**:
|
|
4993
|
-
|
|
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).
|
|
4994
5644
|
4. **Storage Boundary**:
|
|
4995
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.
|
|
4996
5646
|
|
|
4997
5647
|
## 1. Model Context Protocol (MCP) Tools
|
|
4998
5648
|
|
|
4999
|
-
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:
|
|
5000
5650
|
|
|
5001
5651
|
| Tool | Purpose | Key Arguments |
|
|
5002
5652
|
|---|---|---|
|
|
5003
5653
|
| \`esedre_list_tickets\` | List roadmap tickets with filters | \`project\` (e.g. \`Profe\`, \`Esedre\`, \`Alce\`), \`status\`, \`type\`, \`priority\`, \`milestone\`, \`isBlocked\`, \`linkedTo\`, \`search\`, \`includePlan\` |
|
|
5004
|
-
| \`esedre_get_ticket\` | Full specification, summary, comments, revision & sha1 | \`ticketId\` (numeric e.g. \`96\` or compound e.g. \`Profe-96\`) |
|
|
5654
|
+
| \`esedre_get_ticket\` | Full specification, summary, comments, links, lease, revision & sha1 | \`ticketId\` (numeric e.g. \`96\` or compound e.g. \`Profe-96\`) |
|
|
5005
5655
|
| \`esedre_get_plan\` | Active implementation plan markdown | \`ticketId\` (numeric or compound) |
|
|
5006
5656
|
| \`esedre_save_plan\` | Save implementation plan markdown with OCC | \`ticketId\`, \`planMarkdown\`, \`lastHash\` |
|
|
5007
|
-
| \`esedre_create_ticket\` | Mint a new ticket with auto sequential ID | \`title\` (max 48 chars), \`type\`, \`project\`, \`effort\`, \`summary\`, \`detail\` |
|
|
5008
|
-
| \`esedre_update_ticket\` | Update ticket attributes with OCC | \`ticketId\`, \`status\`, \`type\`, \`title\`, \`complexity\`, \`effort\`, \`inDevelopment\`, \`featureFlag\`, \`lastHash\` |
|
|
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\` |
|
|
5009
5669
|
| \`esedre_add_comment\` | Append developer or agent comment | \`ticketId\`, \`text\`, \`file\`, \`author\` |
|
|
5010
5670
|
|
|
5011
5671
|
## 2. Core CLI Commands (Fallback via \`run_command\`)
|
|
5012
5672
|
|
|
5013
|
-
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>\`):
|
|
5014
5674
|
|
|
5015
5675
|
### List Tickets
|
|
5016
5676
|
\`\`\`bash
|
|
5017
|
-
.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]
|
|
5018
5678
|
\`\`\`
|
|
5019
5679
|
|
|
5020
5680
|
### Read Ticket Details
|
|
5021
5681
|
\`\`\`bash
|
|
5022
|
-
.esedre/ese get <ticketId
|
|
5682
|
+
.esedre/ese get <ticketId...> [--json]
|
|
5023
5683
|
\`\`\`
|
|
5024
5684
|
|
|
5025
5685
|
### Read or Update Implementation Plans
|
|
@@ -5031,14 +5691,35 @@ Both \`esedre\` and \`ese\` work interchangeably:
|
|
|
5031
5691
|
|
|
5032
5692
|
### Create a Ticket
|
|
5033
5693
|
\`\`\`bash
|
|
5034
|
-
.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]
|
|
5035
5695
|
\`\`\`
|
|
5036
5696
|
|
|
5037
5697
|
### Update Ticket Status & Attributes
|
|
5038
5698
|
\`\`\`bash
|
|
5039
5699
|
.esedre/ese update <ticketId> --status "In Development"
|
|
5040
5700
|
.esedre/ese update <ticketId> --status "Completed"
|
|
5041
|
-
.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
|
|
5042
5723
|
\`\`\`
|
|
5043
5724
|
|
|
5044
5725
|
### Append Comments or Notes
|
|
@@ -5048,6 +5729,23 @@ Both \`esedre\` and \`ese\` work interchangeably:
|
|
|
5048
5729
|
.esedre/ese comment <ticketId> --text "Verified implementation." --author "Agent"
|
|
5049
5730
|
\`\`\`
|
|
5050
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
|
+
|
|
5051
5749
|
### Regenerate Snapshot
|
|
5052
5750
|
\`\`\`bash
|
|
5053
5751
|
.esedre/ese snapshot
|
|
@@ -5090,7 +5788,10 @@ If the host application declares any or all of the 12 core design tokens on \`:r
|
|
|
5090
5788
|
## 4. Safety & Invariants
|
|
5091
5789
|
- **Always use \`--json\`** for CLI programmatic inspection.
|
|
5092
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.
|
|
5093
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.
|
|
5094
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}\`.
|
|
5095
5796
|
- **Never delete tickets** directly via filesystem.
|
|
5096
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.
|
|
@@ -5099,6 +5800,35 @@ If the host application declares any or all of the 12 core design tokens on \`:r
|
|
|
5099
5800
|
- **SEO Invariant**: These developer routes are private and MUST NEVER be exposed in \`sitemap.xml\`, \`robots.txt\`, \`llms.txt\`, or public navigation links.
|
|
5100
5801
|
- The URL hash is preserved across hard page refreshes (F5) without falling back to \`#/chat\` or other views.
|
|
5101
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
|
+
}
|
|
5102
5832
|
function plantWrappers(targetDir) {
|
|
5103
5833
|
const esedreDir = path7.join(targetDir, ".esedre");
|
|
5104
5834
|
fs7.mkdirSync(esedreDir, { recursive: true });
|
|
@@ -5121,36 +5851,91 @@ function plantWrappers(targetDir) {
|
|
|
5121
5851
|
}
|
|
5122
5852
|
}
|
|
5123
5853
|
}
|
|
5124
|
-
function
|
|
5125
|
-
const
|
|
5126
|
-
fs7.
|
|
5127
|
-
|
|
5128
|
-
let configObj = { mcpServers: {} };
|
|
5129
|
-
if (fs7.existsSync(mcpConfigPath)) {
|
|
5130
|
-
try {
|
|
5131
|
-
configObj = JSON.parse(fs7.readFileSync(mcpConfigPath, "utf-8"));
|
|
5132
|
-
if (!configObj.mcpServers) configObj.mcpServers = {};
|
|
5133
|
-
} catch {
|
|
5134
|
-
}
|
|
5135
|
-
}
|
|
5136
|
-
if (process.platform === "win32") {
|
|
5137
|
-
configObj.mcpServers.esedre = {
|
|
5138
|
-
command: "cmd.exe",
|
|
5139
|
-
args: ["/c", ".esedre\\esedre.cmd", "mcp"]
|
|
5140
|
-
};
|
|
5141
|
-
} else {
|
|
5142
|
-
configObj.mcpServers.esedre = {
|
|
5143
|
-
command: ".esedre/esedre",
|
|
5144
|
-
args: ["mcp"]
|
|
5145
|
-
};
|
|
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;
|
|
5146
5858
|
}
|
|
5147
5859
|
try {
|
|
5148
|
-
|
|
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
|
+
}
|
|
5149
5898
|
return true;
|
|
5150
5899
|
} catch {
|
|
5151
5900
|
return false;
|
|
5152
5901
|
}
|
|
5153
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
|
+
}
|
|
5154
5939
|
function setupSkill(targetDir, options = {}) {
|
|
5155
5940
|
const skillDir = path7.join(targetDir, ".agents", "skills", "esedre");
|
|
5156
5941
|
fs7.mkdirSync(skillDir, { recursive: true });
|
|
@@ -5164,7 +5949,7 @@ function setupSkill(targetDir, options = {}) {
|
|
|
5164
5949
|
if (status === "LATEST" && !options.force) {
|
|
5165
5950
|
return { status, updated: false };
|
|
5166
5951
|
}
|
|
5167
|
-
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...>"));
|
|
5168
5953
|
if (options.force || status === "HISTORIC_DEFAULT" || status === "MISSING" || hasBannedOrDeprecatedTerms) {
|
|
5169
5954
|
fs7.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
|
|
5170
5955
|
return { status: "LATEST", updated: true };
|
|
@@ -5385,6 +6170,7 @@ function configureWorkspace(targetDir, options = {}) {
|
|
|
5385
6170
|
if (!globalStoreRes.ok) {
|
|
5386
6171
|
console.warn(`\x1B[33m\u26A0\uFE0F Warning: Could not initialize global store at ${globalStoreRes.path}: ${globalStoreRes.error}\x1B[0m`);
|
|
5387
6172
|
}
|
|
6173
|
+
const ruleSizeWarnings = checkAgentRuleSizes(targetDir);
|
|
5388
6174
|
return {
|
|
5389
6175
|
esedreJsonCreatedOrUpdated: true,
|
|
5390
6176
|
projectRegistered,
|
|
@@ -5399,7 +6185,8 @@ function configureWorkspace(targetDir, options = {}) {
|
|
|
5399
6185
|
recommendedProxySnippet: proxyRes.recommendedProxySnippet,
|
|
5400
6186
|
globalStoreStatus: globalStoreRes.ok ? "ready" : "warning",
|
|
5401
6187
|
globalStorePath: globalStoreRes.path,
|
|
5402
|
-
globalStoreError: globalStoreRes.error
|
|
6188
|
+
globalStoreError: globalStoreRes.error,
|
|
6189
|
+
ruleSizeWarnings
|
|
5403
6190
|
};
|
|
5404
6191
|
}
|
|
5405
6192
|
var initWorkspace = configureWorkspace;
|
|
@@ -5409,8 +6196,8 @@ var ESEDRE_HUB_AGENTS_TEMPLATE = `# Agent Guidelines: esedre-data
|
|
|
5409
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.
|
|
5410
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:
|
|
5411
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.
|
|
5412
|
-
2. **Esedre MCP Tools**: Use \`esedre_get_ticket\`, \`esedre_list_tickets\`, \`esedre_get_plan\`, \`esedre_save_plan\`, and \`
|
|
5413
|
-
3. **Esedre CLI**: Execute \`.esedre/ese get <id>\`, \`.esedre/ese list\`, or \`.esedre/ese
|
|
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.
|
|
5414
6201
|
|
|
5415
6202
|
## 2. Ingress Redirection Protocol
|
|
5416
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.
|
|
@@ -5779,7 +6566,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5779
6566
|
const numKey = String(t.meta.id);
|
|
5780
6567
|
const code = t.projectDescriptor?.code || t.meta.project || "Profe";
|
|
5781
6568
|
const prefixedKey = `${code}-${t.meta.id}`;
|
|
5782
|
-
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 };
|
|
5783
6570
|
metasMap[prefixedKey] = metaWithLinks;
|
|
5784
6571
|
if (!metasMap[numKey] || code.toUpperCase() === "PROF") {
|
|
5785
6572
|
metasMap[numKey] = metaWithLinks;
|
|
@@ -5983,6 +6770,12 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5983
6770
|
sendJson(res, 200, { milestones });
|
|
5984
6771
|
return true;
|
|
5985
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
|
+
}
|
|
5986
6779
|
}
|
|
5987
6780
|
if (req.method === "POST") {
|
|
5988
6781
|
const body = await readJsonBody(req);
|
|
@@ -6215,6 +7008,61 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
6215
7008
|
sendJson(res, 200, { success: true, source: result.source, target: result.target });
|
|
6216
7009
|
return true;
|
|
6217
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
|
+
}
|
|
6218
7066
|
}
|
|
6219
7067
|
sendJson(res, 404, { error: `Endpoint not found: ${pathname}` });
|
|
6220
7068
|
return true;
|
|
@@ -6223,6 +7071,10 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
6223
7071
|
sendJson(res, 403, { error: err.message });
|
|
6224
7072
|
return true;
|
|
6225
7073
|
}
|
|
7074
|
+
if (err.name === "EsedreLeaseConflictError") {
|
|
7075
|
+
sendJson(res, 409, { error: err.message, ticketKey: err.ticketKey, activeLease: err.activeLease });
|
|
7076
|
+
return true;
|
|
7077
|
+
}
|
|
6226
7078
|
console.error("[Esedre API Error]:", err);
|
|
6227
7079
|
sendJson(res, 500, { error: err.message });
|
|
6228
7080
|
return true;
|
|
@@ -6390,8 +7242,8 @@ ${colors.bold}ROADMAP COMMANDS (Pair Programming & LLM Agents):${colors.reset}
|
|
|
6390
7242
|
\u2022 Status values: 'Planned', 'In Development', 'Completed', 'Rejected'
|
|
6391
7243
|
\u2022 Type values: 'Feature', 'Platform', 'Tools', 'Idea', 'Bug'
|
|
6392
7244
|
|
|
6393
|
-
${colors.bold}get${colors.reset} <id
|
|
6394
|
-
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.
|
|
6395
7247
|
|
|
6396
7248
|
${colors.bold}plan${colors.reset} <id> [--file <path> | --set "<markdown>"] [--last-hash <sha1>] [--json]
|
|
6397
7249
|
View or update implementation plan with optimistic concurrency control.
|
|
@@ -6412,6 +7264,9 @@ ${colors.bold}ROADMAP COMMANDS (Pair Programming & LLM Agents):${colors.reset}
|
|
|
6412
7264
|
${colors.bold}unlink${colors.reset} <sourceId> <targetId> [--json]
|
|
6413
7265
|
Remove a link between two tickets and its reciprocal link.
|
|
6414
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
|
+
|
|
6415
7270
|
${colors.bold}snapshot, refresh${colors.reset} [--project <code>] [--json]
|
|
6416
7271
|
Generate lean read-only projection snapshot (.esedre/snapshot.json) for zero-latency agent context.
|
|
6417
7272
|
|
|
@@ -6571,6 +7426,7 @@ async function main() {
|
|
|
6571
7426
|
}
|
|
6572
7427
|
const rawArgs = process.argv.slice(2);
|
|
6573
7428
|
const { command, positionals, flags } = parseArgs(rawArgs);
|
|
7429
|
+
initLogger({ debug: Boolean(flags["debug"]) });
|
|
6574
7430
|
try {
|
|
6575
7431
|
writeGlobalVersion();
|
|
6576
7432
|
} catch {
|
|
@@ -6830,9 +7686,14 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
6830
7686
|
console.log(` \u2022 In-repo wrappers: .esedre/esedre.cmd, .esedre/ese.cmd, .esedre/esedre, .esedre/ese`);
|
|
6831
7687
|
console.log(` \u2022 .gitignore: ${res.gitignoreUpdated ? "Added .esedre/snapshot.json" : "Already configured"}`);
|
|
6832
7688
|
console.log(` \u2022 Snapshot: Generated .esedre/snapshot.json (${snapshotTickets} tickets projected)`);
|
|
6833
|
-
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"}`);
|
|
6834
7690
|
console.log(` \u2022 Skill: ${res.skillConfigured ? "Updated .agents/skills/esedre/SKILL.md" : `Retained (${res.skillStatus})`}`);
|
|
6835
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
|
+
}
|
|
6836
7697
|
if (res.viteProxyStatus === "CONFIGURED") {
|
|
6837
7698
|
console.log(` \u2022 ${colors.green}Vite Proxy: Added /esedre proxy to ${res.viteConfigFile} targeting http://127.0.0.1:${effectivePort}${colors.reset}`);
|
|
6838
7699
|
} else if (res.viteProxyStatus === "ALREADY_CONFIGURED") {
|
|
@@ -7014,6 +7875,11 @@ ${colors.bold}${colors.green}\u2714 Esedre multi-workspace upgrade completed acr
|
|
|
7014
7875
|
} else if (w.upgrade.skillStatus === "CUSTOMIZED") {
|
|
7015
7876
|
console.log(` - Preserved customized .agents/skills/esedre/SKILL.md`);
|
|
7016
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
|
+
}
|
|
7017
7883
|
}
|
|
7018
7884
|
}
|
|
7019
7885
|
return;
|
|
@@ -7045,6 +7911,11 @@ ${colors.bold}${colors.green}\u2714 Esedre multi-workspace upgrade completed acr
|
|
|
7045
7911
|
} else if (res.skillStatus === "CUSTOMIZED") {
|
|
7046
7912
|
console.log(` \u2022 ${colors.yellow}Notice: .agents/skills/esedre/SKILL.md is customized; preserved user modifications (use --force to overwrite).${colors.reset}`);
|
|
7047
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
|
+
}
|
|
7048
7919
|
}
|
|
7049
7920
|
return;
|
|
7050
7921
|
}
|
|
@@ -7278,7 +8149,9 @@ ${colors.yellow}Notice: ${outdatedWrappers.length} workspace(s) have legacy CMD
|
|
|
7278
8149
|
const milestone = flags["milestone"] || flags["m"] || void 0;
|
|
7279
8150
|
const isBlocked = flags["blocked"] ? true : void 0;
|
|
7280
8151
|
const linkedTo = flags["linked-to"] || flags["linkedTo"] || void 0;
|
|
7281
|
-
const
|
|
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 });
|
|
7282
8155
|
if (isJson) {
|
|
7283
8156
|
console.log(JSON.stringify(tickets.map((t) => ({
|
|
7284
8157
|
id: t.meta.id,
|
|
@@ -7295,7 +8168,8 @@ ${colors.yellow}Notice: ${outdatedWrappers.length} workspace(s) have legacy CMD
|
|
|
7295
8168
|
project: t.projectDescriptor?.code || t.meta.project || "UNASSIGNED",
|
|
7296
8169
|
sha1: t.sha1 || t.meta.sha1,
|
|
7297
8170
|
isBlocked: t.isBlocked,
|
|
7298
|
-
links: t.links
|
|
8171
|
+
links: t.links,
|
|
8172
|
+
lease: t.lease || null
|
|
7299
8173
|
})), null, 2));
|
|
7300
8174
|
} else {
|
|
7301
8175
|
if (tickets.length === 0) {
|
|
@@ -7318,22 +8192,30 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
7318
8192
|
return;
|
|
7319
8193
|
}
|
|
7320
8194
|
case "get": {
|
|
7321
|
-
|
|
7322
|
-
if (!idStr) {
|
|
8195
|
+
if (positionals.length === 0) {
|
|
7323
8196
|
console.error(`${colors.red}Error: Ticket ID is required (e.g. ese get 1 or ese get Profe-1)${colors.reset}`);
|
|
7324
8197
|
process.exit(1);
|
|
7325
8198
|
}
|
|
7326
8199
|
const projectFlag = flags["project"];
|
|
7327
|
-
const
|
|
7328
|
-
const
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
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);
|
|
7332
8209
|
}
|
|
7333
8210
|
if (isJson) {
|
|
7334
|
-
console.log(JSON.stringify(
|
|
8211
|
+
console.log(JSON.stringify(tickets.length === 1 ? tickets[0] : tickets, null, 2));
|
|
7335
8212
|
} else {
|
|
7336
|
-
console.log(
|
|
8213
|
+
console.log(
|
|
8214
|
+
tickets.map((t) => formatTicketDetail(t)).join(`
|
|
8215
|
+
${colors.dim}${"\u2500".repeat(60)}${colors.reset}
|
|
8216
|
+
|
|
8217
|
+
`)
|
|
8218
|
+
);
|
|
7337
8219
|
}
|
|
7338
8220
|
return;
|
|
7339
8221
|
}
|
|
@@ -7761,6 +8643,129 @@ ${colors.bold}Tickets:${colors.reset}`);
|
|
|
7761
8643
|
}
|
|
7762
8644
|
return;
|
|
7763
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
|
+
}
|
|
7764
8769
|
default: {
|
|
7765
8770
|
console.error(`${colors.red}Error: Unknown command "${command}"${colors.reset}`);
|
|
7766
8771
|
printHelp();
|
|
@@ -7773,6 +8778,14 @@ ${colors.bold}Tickets:${colors.reset}`);
|
|
|
7773
8778
|
console.error(`${colors.dim}Use --force to override if you are certain.${colors.reset}`);
|
|
7774
8779
|
process.exit(1);
|
|
7775
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
|
+
}
|
|
7776
8789
|
if (err instanceof EsedreAuthorizationError || err.name === "EsedreAuthorizationError") {
|
|
7777
8790
|
console.error(`${colors.red}Access Denied: ${err.message}${colors.reset}`);
|
|
7778
8791
|
process.exit(1);
|