esedre 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +26 -11
- package/dist/esedre.mjs +1459 -386
- 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
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// bin/esedre.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs11 from "node:fs";
|
|
5
|
+
import path11 from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/storage/filesystem.ts
|
|
8
8
|
import fs3 from "node:fs";
|
|
9
9
|
import path3 from "node:path";
|
|
10
|
-
import
|
|
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,9 +3997,64 @@ var SecurityFilter = class {
|
|
|
3576
3997
|
}
|
|
3577
3998
|
return this.target.getAttachmentPath ? this.target.getAttachmentPath(id, filename) : null;
|
|
3578
3999
|
}
|
|
4000
|
+
async claimTicket(input) {
|
|
4001
|
+
if (input.project && !isProjectAuthorized(input.project, this.config.allowedProjects)) {
|
|
4002
|
+
throw new EsedreAuthorizationError(input.project);
|
|
4003
|
+
}
|
|
4004
|
+
const ticket = await this.getTicket(
|
|
4005
|
+
input.project && /^\d+$/.test(String(input.ticketId).trim()) ? `${input.project}-${input.ticketId}` : input.ticketId
|
|
4006
|
+
);
|
|
4007
|
+
if (!ticket) {
|
|
4008
|
+
throw new Error(`Ticket #${input.ticketId} not found`);
|
|
4009
|
+
}
|
|
4010
|
+
return this.target.claimTicket(input);
|
|
4011
|
+
}
|
|
4012
|
+
async renewLease(input) {
|
|
4013
|
+
if (input.project && !isProjectAuthorized(input.project, this.config.allowedProjects)) {
|
|
4014
|
+
throw new EsedreAuthorizationError(input.project);
|
|
4015
|
+
}
|
|
4016
|
+
const ticket = await this.getTicket(
|
|
4017
|
+
input.project && /^\d+$/.test(String(input.ticketId).trim()) ? `${input.project}-${input.ticketId}` : input.ticketId
|
|
4018
|
+
);
|
|
4019
|
+
if (!ticket) {
|
|
4020
|
+
throw new Error(`Ticket #${input.ticketId} not found`);
|
|
4021
|
+
}
|
|
4022
|
+
return this.target.renewLease(input);
|
|
4023
|
+
}
|
|
4024
|
+
async releaseTicket(input) {
|
|
4025
|
+
if (input.project && !isProjectAuthorized(input.project, this.config.allowedProjects)) {
|
|
4026
|
+
throw new EsedreAuthorizationError(input.project);
|
|
4027
|
+
}
|
|
4028
|
+
const ticket = await this.getTicket(
|
|
4029
|
+
input.project && /^\d+$/.test(String(input.ticketId).trim()) ? `${input.project}-${input.ticketId}` : input.ticketId
|
|
4030
|
+
);
|
|
4031
|
+
if (!ticket) {
|
|
4032
|
+
throw new Error(`Ticket #${input.ticketId} not found`);
|
|
4033
|
+
}
|
|
4034
|
+
return this.target.releaseTicket(input);
|
|
4035
|
+
}
|
|
4036
|
+
async getTicketLease(id, projectCode) {
|
|
4037
|
+
if (projectCode && !isProjectAuthorized(projectCode, this.config.allowedProjects)) {
|
|
4038
|
+
throw new EsedreAuthorizationError(projectCode);
|
|
4039
|
+
}
|
|
4040
|
+
const ticket = await this.getTicket(
|
|
4041
|
+
projectCode && /^\d+$/.test(String(id).trim()) ? `${projectCode}-${id}` : id
|
|
4042
|
+
);
|
|
4043
|
+
if (!ticket) return null;
|
|
4044
|
+
return this.target.getTicketLease(id, projectCode);
|
|
4045
|
+
}
|
|
4046
|
+
async listLeases(projectCode) {
|
|
4047
|
+
if (projectCode && projectCode !== "all" && !isProjectAuthorized(projectCode, this.config.allowedProjects)) {
|
|
4048
|
+
throw new EsedreAuthorizationError(projectCode);
|
|
4049
|
+
}
|
|
4050
|
+
const allLeases = await this.target.listLeases(projectCode);
|
|
4051
|
+
return allLeases.filter((l) => isProjectAuthorized(l.project, this.config.allowedProjects));
|
|
4052
|
+
}
|
|
3579
4053
|
};
|
|
3580
4054
|
|
|
3581
4055
|
// src/mcp/server.ts
|
|
4056
|
+
import fs4 from "node:fs";
|
|
4057
|
+
import path4 from "node:path";
|
|
3582
4058
|
import readline from "node:readline";
|
|
3583
4059
|
var EsedreMcpServer = class {
|
|
3584
4060
|
storage;
|
|
@@ -3689,6 +4165,20 @@ var EsedreMcpServer = class {
|
|
|
3689
4165
|
}
|
|
3690
4166
|
};
|
|
3691
4167
|
}
|
|
4168
|
+
if (err instanceof EsedreLeaseConflictError || err.name === "EsedreLeaseConflictError") {
|
|
4169
|
+
return {
|
|
4170
|
+
jsonrpc: "2.0",
|
|
4171
|
+
id,
|
|
4172
|
+
error: {
|
|
4173
|
+
code: -32001,
|
|
4174
|
+
message: err.message,
|
|
4175
|
+
data: {
|
|
4176
|
+
ticketKey: err.ticketKey,
|
|
4177
|
+
activeLease: err.activeLease
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
};
|
|
4181
|
+
}
|
|
3692
4182
|
return {
|
|
3693
4183
|
jsonrpc: "2.0",
|
|
3694
4184
|
id,
|
|
@@ -3800,7 +4290,8 @@ Status: ${ticket.meta.status}`;
|
|
|
3800
4290
|
milestone: { type: "string", description: "Filter tickets by milestone name or ID" },
|
|
3801
4291
|
search: { type: "string", description: "Search keywords in title or ID" },
|
|
3802
4292
|
isBlocked: { type: "boolean", description: "Filter tickets blocked by uncompleted tickets" },
|
|
3803
|
-
linkedTo: { type: "string", description: "Filter tickets linked to a specific ticket ID or key" }
|
|
4293
|
+
linkedTo: { type: "string", description: "Filter tickets linked to a specific ticket ID or key" },
|
|
4294
|
+
includePlan: { type: "boolean", description: "When true, includes implementation plan markdown (planMarkdown) for each ticket" }
|
|
3804
4295
|
}
|
|
3805
4296
|
}
|
|
3806
4297
|
},
|
|
@@ -3892,9 +4383,10 @@ Status: ${ticket.meta.status}`;
|
|
|
3892
4383
|
properties: {
|
|
3893
4384
|
ticketId: { type: ["integer", "string"], description: "Numeric ticket ID or compound key" },
|
|
3894
4385
|
text: { type: "string", description: "Comment message" },
|
|
4386
|
+
file: { type: "string", description: "Path to local file containing comment message markdown" },
|
|
3895
4387
|
author: { type: "string", description: "Author name (e.g. Antigravity, Developer)" }
|
|
3896
4388
|
},
|
|
3897
|
-
required: ["ticketId"
|
|
4389
|
+
required: ["ticketId"]
|
|
3898
4390
|
}
|
|
3899
4391
|
},
|
|
3900
4392
|
{
|
|
@@ -3983,6 +4475,71 @@ Status: ${ticket.meta.status}`;
|
|
|
3983
4475
|
},
|
|
3984
4476
|
required: ["sourceTicketId", "targetTicketId"]
|
|
3985
4477
|
}
|
|
4478
|
+
},
|
|
4479
|
+
{
|
|
4480
|
+
name: "esedre_claim_ticket",
|
|
4481
|
+
description: "Claim an advisory TTL-backed lease on a ticket to coordinate multi-agent and multi-machine work and prevent duplicate effort.",
|
|
4482
|
+
inputSchema: {
|
|
4483
|
+
type: "object",
|
|
4484
|
+
properties: {
|
|
4485
|
+
ticketId: { type: ["number", "string"], description: 'Ticket ID or prefixed key (e.g. 69 or "Esedre-69")' },
|
|
4486
|
+
holder: { type: "string", description: 'Agent or developer identity claiming the ticket (e.g. "agy-subagent-1" or "desktop-worker")' },
|
|
4487
|
+
workerId: { type: "string", description: "Alias for holder: agent or worker identity claiming the ticket" },
|
|
4488
|
+
agentType: { type: "string", description: 'Optional agent runtime identifier (e.g. "antigravity", "claude-code", "codex")' },
|
|
4489
|
+
system: { type: "string", description: "Alias for agentType: system or runtime identifier" },
|
|
4490
|
+
sessionId: { type: "string", description: "Optional conversation or session identifier" },
|
|
4491
|
+
branch: { type: "string", description: "Optional Git branch associated with the lease" },
|
|
4492
|
+
ttlMinutes: { type: "number", description: "Lease time-to-live in minutes (default: 60)" },
|
|
4493
|
+
force: { type: "boolean", description: "Override an existing active lease held by another worker" },
|
|
4494
|
+
project: { type: "string", description: "Optional project code when numeric ticketId is used" }
|
|
4495
|
+
},
|
|
4496
|
+
required: ["ticketId"]
|
|
4497
|
+
}
|
|
4498
|
+
},
|
|
4499
|
+
{
|
|
4500
|
+
name: "esedre_renew_lease",
|
|
4501
|
+
description: "Renew (heartbeat) an active ticket lease to extend its expiration timestamp.",
|
|
4502
|
+
inputSchema: {
|
|
4503
|
+
type: "object",
|
|
4504
|
+
properties: {
|
|
4505
|
+
ticketId: { type: ["number", "string"], description: "Ticket ID or prefixed key" },
|
|
4506
|
+
holder: { type: "string", description: "Holder identity verifying lease ownership" },
|
|
4507
|
+
workerId: { type: "string", description: "Alias for holder: worker identity verifying lease ownership" },
|
|
4508
|
+
system: { type: "string", description: "Optional system or agent runtime identifier" },
|
|
4509
|
+
ttlMinutes: { type: "number", description: "New TTL in minutes from now (default: 60)" },
|
|
4510
|
+
force: { type: "boolean", description: "Force renewal even if held by another identity" },
|
|
4511
|
+
project: { type: "string", description: "Optional project code" }
|
|
4512
|
+
},
|
|
4513
|
+
required: ["ticketId"]
|
|
4514
|
+
}
|
|
4515
|
+
},
|
|
4516
|
+
{
|
|
4517
|
+
name: "esedre_release_ticket",
|
|
4518
|
+
description: "Release an active lease on a ticket when work is paused, handed off, or completed.",
|
|
4519
|
+
inputSchema: {
|
|
4520
|
+
type: "object",
|
|
4521
|
+
properties: {
|
|
4522
|
+
ticketId: { type: ["number", "string"], description: "Ticket ID or prefixed key" },
|
|
4523
|
+
holder: { type: "string", description: "Holder identity verifying lease ownership" },
|
|
4524
|
+
workerId: { type: "string", description: "Alias for holder: worker identity verifying lease ownership" },
|
|
4525
|
+
system: { type: "string", description: "Optional system or agent runtime identifier" },
|
|
4526
|
+
finalStatus: { type: "string", enum: ["Planned", "In Development", "Completed", "Rejected"], description: "Optional status to transition the ticket to upon release" },
|
|
4527
|
+
notes: { type: "string", description: "Optional verification or handoff comment to append upon release" },
|
|
4528
|
+
force: { type: "boolean", description: "Force release even if held by another identity" },
|
|
4529
|
+
project: { type: "string", description: "Optional project code" }
|
|
4530
|
+
},
|
|
4531
|
+
required: ["ticketId"]
|
|
4532
|
+
}
|
|
4533
|
+
},
|
|
4534
|
+
{
|
|
4535
|
+
name: "esedre_list_leases",
|
|
4536
|
+
description: "List all currently active (non-expired) ticket leases across authorized projects.",
|
|
4537
|
+
inputSchema: {
|
|
4538
|
+
type: "object",
|
|
4539
|
+
properties: {
|
|
4540
|
+
project: { type: "string", description: 'Optional project code filter (or "all")' }
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
3986
4543
|
}
|
|
3987
4544
|
];
|
|
3988
4545
|
}
|
|
@@ -3991,6 +4548,7 @@ Status: ${ticket.meta.status}`;
|
|
|
3991
4548
|
switch (canonical) {
|
|
3992
4549
|
case "list_tickets": {
|
|
3993
4550
|
const type = args.type || args.category;
|
|
4551
|
+
const includePlan = Boolean(args.includePlan);
|
|
3994
4552
|
const tickets = await this.storage.listTickets({
|
|
3995
4553
|
project: args.project,
|
|
3996
4554
|
status: args.status,
|
|
@@ -4002,23 +4560,39 @@ Status: ${ticket.meta.status}`;
|
|
|
4002
4560
|
isBlocked: args.isBlocked !== void 0 ? Boolean(args.isBlocked) : void 0,
|
|
4003
4561
|
linkedTo: args.linkedTo
|
|
4004
4562
|
});
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4563
|
+
const results = await Promise.all(
|
|
4564
|
+
tickets.map(async (t) => {
|
|
4565
|
+
let planMarkdown = t.planMarkdown;
|
|
4566
|
+
if (includePlan && planMarkdown === void 0) {
|
|
4567
|
+
const lookupKey = t.projectDescriptor?.code ? `${t.projectDescriptor.code}-${t.meta.id}` : t.meta.id;
|
|
4568
|
+
planMarkdown = await this.storage.getPlan(lookupKey) ?? null;
|
|
4569
|
+
}
|
|
4570
|
+
const hasPlan = Boolean(
|
|
4571
|
+
t.planMarkdown && t.planMarkdown.trim().length > 0 || planMarkdown && planMarkdown.trim().length > 0
|
|
4572
|
+
);
|
|
4573
|
+
return {
|
|
4574
|
+
id: t.meta.id,
|
|
4575
|
+
title: t.meta.title,
|
|
4576
|
+
type: t.meta.type || t.meta.category,
|
|
4577
|
+
category: t.meta.type || t.meta.category,
|
|
4578
|
+
status: t.meta.status,
|
|
4579
|
+
priority: t.meta.priority,
|
|
4580
|
+
complexity: t.meta.complexity,
|
|
4581
|
+
effort: t.meta.estimatedEffort,
|
|
4582
|
+
milestone: t.meta.milestone,
|
|
4583
|
+
featureFlag: t.meta.featureFlag,
|
|
4584
|
+
inheritedFeatureFlag: t.meta.inheritedFeatureFlag,
|
|
4585
|
+
project: t.projectDescriptor?.code || t.meta.project || "UNASSIGNED",
|
|
4586
|
+
summary: t.detail?.summary,
|
|
4587
|
+
hasPlan,
|
|
4588
|
+
...includePlan ? { planMarkdown: planMarkdown || null } : {},
|
|
4589
|
+
sha1: t.sha1 || t.meta.sha1,
|
|
4590
|
+
isBlocked: t.isBlocked,
|
|
4591
|
+
links: t.links
|
|
4592
|
+
};
|
|
4593
|
+
})
|
|
4594
|
+
);
|
|
4595
|
+
return results;
|
|
4022
4596
|
}
|
|
4023
4597
|
case "get_ticket": {
|
|
4024
4598
|
const id = this.parseTicketId(args.ticketId);
|
|
@@ -4077,8 +4651,26 @@ Status: ${ticket.meta.status}`;
|
|
|
4077
4651
|
}
|
|
4078
4652
|
case "add_comment": {
|
|
4079
4653
|
const id = this.parseTicketId(args.ticketId);
|
|
4654
|
+
let text = args.text;
|
|
4655
|
+
if (!text && args.file) {
|
|
4656
|
+
const resolvedPath = path4.resolve(process.cwd(), args.file);
|
|
4657
|
+
if (!fs4.existsSync(resolvedPath)) {
|
|
4658
|
+
throw new Error(`File not found: ${args.file}`);
|
|
4659
|
+
}
|
|
4660
|
+
if (!fs4.statSync(resolvedPath).isFile()) {
|
|
4661
|
+
throw new Error(`Path is not a file: ${args.file}`);
|
|
4662
|
+
}
|
|
4663
|
+
const fileContent = fs4.readFileSync(resolvedPath, "utf-8");
|
|
4664
|
+
if (!fileContent.trim()) {
|
|
4665
|
+
throw new Error(`Comment file is empty: ${args.file}`);
|
|
4666
|
+
}
|
|
4667
|
+
text = fileContent;
|
|
4668
|
+
}
|
|
4669
|
+
if (!text || !text.trim()) {
|
|
4670
|
+
throw new Error("Comment text or file is required");
|
|
4671
|
+
}
|
|
4080
4672
|
const comment = await this.storage.addComment(id, {
|
|
4081
|
-
text
|
|
4673
|
+
text,
|
|
4082
4674
|
author: args.author || "Agent"
|
|
4083
4675
|
});
|
|
4084
4676
|
return comment;
|
|
@@ -4140,26 +4732,117 @@ Status: ${ticket.meta.status}`;
|
|
|
4140
4732
|
target: result.target
|
|
4141
4733
|
};
|
|
4142
4734
|
}
|
|
4735
|
+
case "claim_ticket": {
|
|
4736
|
+
const identity = resolveWorkerIdentity({
|
|
4737
|
+
holder: args.holder || args.workerId,
|
|
4738
|
+
agentType: args.agentType || args.system || "mcp-agent",
|
|
4739
|
+
sessionId: args.sessionId,
|
|
4740
|
+
branch: args.branch
|
|
4741
|
+
});
|
|
4742
|
+
const res = await this.storage.claimTicket({
|
|
4743
|
+
ticketId: this.parseTicketId(args.ticketId),
|
|
4744
|
+
holder: identity.holder,
|
|
4745
|
+
agentType: identity.agentType,
|
|
4746
|
+
hostname: identity.hostname,
|
|
4747
|
+
sessionId: identity.sessionId,
|
|
4748
|
+
branch: identity.branch,
|
|
4749
|
+
worktree: identity.worktree,
|
|
4750
|
+
ttlMinutes: args.ttlMinutes,
|
|
4751
|
+
force: Boolean(args.force),
|
|
4752
|
+
project: args.project
|
|
4753
|
+
});
|
|
4754
|
+
return {
|
|
4755
|
+
success: true,
|
|
4756
|
+
renewed: res.renewed,
|
|
4757
|
+
overridden: res.overridden || false,
|
|
4758
|
+
lease: res.lease,
|
|
4759
|
+
ticket: {
|
|
4760
|
+
id: res.ticket.meta.id,
|
|
4761
|
+
project: res.ticket.meta.project,
|
|
4762
|
+
title: res.ticket.meta.title,
|
|
4763
|
+
status: res.ticket.meta.status,
|
|
4764
|
+
sha1: res.ticket.sha1
|
|
4765
|
+
}
|
|
4766
|
+
};
|
|
4767
|
+
}
|
|
4768
|
+
case "renew_lease": {
|
|
4769
|
+
const lease = await this.storage.renewLease({
|
|
4770
|
+
ticketId: this.parseTicketId(args.ticketId),
|
|
4771
|
+
holder: args.holder || args.workerId,
|
|
4772
|
+
ttlMinutes: args.ttlMinutes,
|
|
4773
|
+
force: Boolean(args.force),
|
|
4774
|
+
project: args.project
|
|
4775
|
+
});
|
|
4776
|
+
return {
|
|
4777
|
+
success: true,
|
|
4778
|
+
lease
|
|
4779
|
+
};
|
|
4780
|
+
}
|
|
4781
|
+
case "release_ticket": {
|
|
4782
|
+
const holder = args.holder || args.workerId;
|
|
4783
|
+
const parsedId = this.parseTicketId(args.ticketId);
|
|
4784
|
+
const res = await this.storage.releaseTicket({
|
|
4785
|
+
ticketId: parsedId,
|
|
4786
|
+
holder,
|
|
4787
|
+
force: Boolean(args.force),
|
|
4788
|
+
project: args.project
|
|
4789
|
+
});
|
|
4790
|
+
if (args.finalStatus) {
|
|
4791
|
+
await this.storage.updateTicket(res.ticketKey || parsedId, { status: args.finalStatus });
|
|
4792
|
+
}
|
|
4793
|
+
if (args.notes) {
|
|
4794
|
+
await this.storage.addComment(res.ticketKey || parsedId, {
|
|
4795
|
+
author: holder || res.previousLease?.holder || "mcp-agent",
|
|
4796
|
+
text: args.notes
|
|
4797
|
+
});
|
|
4798
|
+
}
|
|
4799
|
+
return {
|
|
4800
|
+
success: true,
|
|
4801
|
+
...res
|
|
4802
|
+
};
|
|
4803
|
+
}
|
|
4804
|
+
case "list_leases": {
|
|
4805
|
+
const leases = await this.storage.listLeases(args.project);
|
|
4806
|
+
return {
|
|
4807
|
+
total: leases.length,
|
|
4808
|
+
leases
|
|
4809
|
+
};
|
|
4810
|
+
}
|
|
4143
4811
|
default:
|
|
4144
4812
|
throw new Error(`Unknown tool: ${name}`);
|
|
4145
4813
|
}
|
|
4146
4814
|
}
|
|
4147
4815
|
};
|
|
4148
4816
|
|
|
4817
|
+
// src/utils/logger.ts
|
|
4818
|
+
var MAX_LOG_FILE_BYTES = 5 * 1024 * 1024;
|
|
4819
|
+
var debugEnabled = Boolean(process.env.ESEDRE_DEBUG);
|
|
4820
|
+
var logFileOverride;
|
|
4821
|
+
function initLogger(options) {
|
|
4822
|
+
if (options?.debug !== void 0) {
|
|
4823
|
+
debugEnabled = options.debug;
|
|
4824
|
+
} else if (process.env.ESEDRE_DEBUG) {
|
|
4825
|
+
debugEnabled = true;
|
|
4826
|
+
}
|
|
4827
|
+
if (options?.logFile) {
|
|
4828
|
+
logFileOverride = options.logFile;
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
|
|
4149
4832
|
// src/utils/manifestDetector.ts
|
|
4150
|
-
import
|
|
4151
|
-
import
|
|
4833
|
+
import fs5 from "node:fs";
|
|
4834
|
+
import path5 from "node:path";
|
|
4152
4835
|
function sanitizeCode(input) {
|
|
4153
4836
|
const clean = input.replace(/[^a-zA-Z0-9]/g, "");
|
|
4154
4837
|
return clean.slice(0, MAX_PROJECT_CODE_LENGTH) || "APP";
|
|
4155
4838
|
}
|
|
4156
4839
|
function detectProjectManifest(targetDir) {
|
|
4157
|
-
const dirBase =
|
|
4840
|
+
const dirBase = path5.basename(targetDir);
|
|
4158
4841
|
for (const settingsFile of ["settings.gradle.kts", "settings.gradle"]) {
|
|
4159
|
-
const fullPath =
|
|
4160
|
-
if (
|
|
4842
|
+
const fullPath = path5.join(targetDir, settingsFile);
|
|
4843
|
+
if (fs5.existsSync(fullPath)) {
|
|
4161
4844
|
try {
|
|
4162
|
-
const content =
|
|
4845
|
+
const content = fs5.readFileSync(fullPath, "utf-8");
|
|
4163
4846
|
const match = content.match(/rootProject\.name\s*=\s*["']([^"']+)["']/);
|
|
4164
4847
|
if (match && match[1].trim()) {
|
|
4165
4848
|
const rawName = match[1].trim();
|
|
@@ -4175,15 +4858,15 @@ function detectProjectManifest(targetDir) {
|
|
|
4175
4858
|
}
|
|
4176
4859
|
}
|
|
4177
4860
|
for (const buildFile of [
|
|
4178
|
-
|
|
4861
|
+
path5.join("app", "build.gradle.kts"),
|
|
4179
4862
|
"build.gradle.kts",
|
|
4180
|
-
|
|
4863
|
+
path5.join("app", "build.gradle"),
|
|
4181
4864
|
"build.gradle"
|
|
4182
4865
|
]) {
|
|
4183
|
-
const fullPath =
|
|
4184
|
-
if (
|
|
4866
|
+
const fullPath = path5.join(targetDir, buildFile);
|
|
4867
|
+
if (fs5.existsSync(fullPath)) {
|
|
4185
4868
|
try {
|
|
4186
|
-
const content =
|
|
4869
|
+
const content = fs5.readFileSync(fullPath, "utf-8");
|
|
4187
4870
|
const match = content.match(/(?:namespace|applicationId)\s*=\s*["']([^"']+)["']/);
|
|
4188
4871
|
if (match && match[1].trim()) {
|
|
4189
4872
|
const ns = match[1].trim();
|
|
@@ -4200,10 +4883,10 @@ function detectProjectManifest(targetDir) {
|
|
|
4200
4883
|
}
|
|
4201
4884
|
}
|
|
4202
4885
|
}
|
|
4203
|
-
const pomPath =
|
|
4204
|
-
if (
|
|
4886
|
+
const pomPath = path5.join(targetDir, "pom.xml");
|
|
4887
|
+
if (fs5.existsSync(pomPath)) {
|
|
4205
4888
|
try {
|
|
4206
|
-
const content =
|
|
4889
|
+
const content = fs5.readFileSync(pomPath, "utf-8");
|
|
4207
4890
|
const stripped = content.replace(/<!--[\s\S]*?-->/g, "").replace(/<parent>[\s\S]*?<\/parent>/g, "");
|
|
4208
4891
|
const nameMatch = stripped.match(/<name>\s*([^<]+)\s*<\/name>/);
|
|
4209
4892
|
const artifactMatch = stripped.match(/<artifactId>\s*([^<]+)\s*<\/artifactId>/) || content.match(/<artifactId>\s*([^<]+)\s*<\/artifactId>/);
|
|
@@ -4222,10 +4905,10 @@ function detectProjectManifest(targetDir) {
|
|
|
4222
4905
|
} catch {
|
|
4223
4906
|
}
|
|
4224
4907
|
}
|
|
4225
|
-
const pkgPath =
|
|
4226
|
-
if (
|
|
4908
|
+
const pkgPath = path5.join(targetDir, "package.json");
|
|
4909
|
+
if (fs5.existsSync(pkgPath)) {
|
|
4227
4910
|
try {
|
|
4228
|
-
const pkg = JSON.parse(
|
|
4911
|
+
const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
4229
4912
|
if (pkg.name && typeof pkg.name === "string") {
|
|
4230
4913
|
const baseName = pkg.name.replace(/^@[\w-]+\//, "");
|
|
4231
4914
|
return {
|
|
@@ -4238,10 +4921,10 @@ function detectProjectManifest(targetDir) {
|
|
|
4238
4921
|
} catch {
|
|
4239
4922
|
}
|
|
4240
4923
|
}
|
|
4241
|
-
const cargoPath =
|
|
4242
|
-
if (
|
|
4924
|
+
const cargoPath = path5.join(targetDir, "Cargo.toml");
|
|
4925
|
+
if (fs5.existsSync(cargoPath)) {
|
|
4243
4926
|
try {
|
|
4244
|
-
const content =
|
|
4927
|
+
const content = fs5.readFileSync(cargoPath, "utf-8");
|
|
4245
4928
|
const match = content.match(/\[package\][\s\S]*?name\s*=\s*["']([^"']+)["']/);
|
|
4246
4929
|
if (match && match[1].trim()) {
|
|
4247
4930
|
const rawName = match[1].trim();
|
|
@@ -4255,10 +4938,10 @@ function detectProjectManifest(targetDir) {
|
|
|
4255
4938
|
} catch {
|
|
4256
4939
|
}
|
|
4257
4940
|
}
|
|
4258
|
-
const pyprojectPath =
|
|
4259
|
-
if (
|
|
4941
|
+
const pyprojectPath = path5.join(targetDir, "pyproject.toml");
|
|
4942
|
+
if (fs5.existsSync(pyprojectPath)) {
|
|
4260
4943
|
try {
|
|
4261
|
-
const content =
|
|
4944
|
+
const content = fs5.readFileSync(pyprojectPath, "utf-8");
|
|
4262
4945
|
const match = content.match(/name\s*=\s*["']([^"']+)["']/);
|
|
4263
4946
|
if (match && match[1].trim()) {
|
|
4264
4947
|
const rawName = match[1].trim();
|
|
@@ -4272,10 +4955,10 @@ function detectProjectManifest(targetDir) {
|
|
|
4272
4955
|
} catch {
|
|
4273
4956
|
}
|
|
4274
4957
|
}
|
|
4275
|
-
const setupCfgPath =
|
|
4276
|
-
if (
|
|
4958
|
+
const setupCfgPath = path5.join(targetDir, "setup.cfg");
|
|
4959
|
+
if (fs5.existsSync(setupCfgPath)) {
|
|
4277
4960
|
try {
|
|
4278
|
-
const content =
|
|
4961
|
+
const content = fs5.readFileSync(setupCfgPath, "utf-8");
|
|
4279
4962
|
const match = content.match(/^name\s*=\s*([^\r\n]+)/m);
|
|
4280
4963
|
if (match && match[1].trim()) {
|
|
4281
4964
|
const rawName = match[1].trim();
|
|
@@ -4289,10 +4972,10 @@ function detectProjectManifest(targetDir) {
|
|
|
4289
4972
|
} catch {
|
|
4290
4973
|
}
|
|
4291
4974
|
}
|
|
4292
|
-
const setupPyPath =
|
|
4293
|
-
if (
|
|
4975
|
+
const setupPyPath = path5.join(targetDir, "setup.py");
|
|
4976
|
+
if (fs5.existsSync(setupPyPath)) {
|
|
4294
4977
|
try {
|
|
4295
|
-
const content =
|
|
4978
|
+
const content = fs5.readFileSync(setupPyPath, "utf-8");
|
|
4296
4979
|
const match = content.match(/name\s*=\s*["']([^"']+)["']/);
|
|
4297
4980
|
if (match && match[1].trim()) {
|
|
4298
4981
|
const rawName = match[1].trim();
|
|
@@ -4306,10 +4989,10 @@ function detectProjectManifest(targetDir) {
|
|
|
4306
4989
|
} catch {
|
|
4307
4990
|
}
|
|
4308
4991
|
}
|
|
4309
|
-
const goModPath =
|
|
4310
|
-
if (
|
|
4992
|
+
const goModPath = path5.join(targetDir, "go.mod");
|
|
4993
|
+
if (fs5.existsSync(goModPath)) {
|
|
4311
4994
|
try {
|
|
4312
|
-
const content =
|
|
4995
|
+
const content = fs5.readFileSync(goModPath, "utf-8");
|
|
4313
4996
|
const match = content.match(/^module\s+([^\r\n\s]+)/m);
|
|
4314
4997
|
if (match && match[1].trim()) {
|
|
4315
4998
|
const modPath = match[1].trim();
|
|
@@ -4333,48 +5016,49 @@ function detectProjectManifest(targetDir) {
|
|
|
4333
5016
|
|
|
4334
5017
|
// src/upgrade.ts
|
|
4335
5018
|
import crypto2 from "node:crypto";
|
|
4336
|
-
import
|
|
4337
|
-
import
|
|
5019
|
+
import fs7 from "node:fs";
|
|
5020
|
+
import os5 from "node:os";
|
|
5021
|
+
import path7 from "node:path";
|
|
4338
5022
|
|
|
4339
5023
|
// src/server/daemon.ts
|
|
4340
|
-
import
|
|
4341
|
-
import
|
|
4342
|
-
import
|
|
5024
|
+
import fs6 from "node:fs";
|
|
5025
|
+
import path6 from "node:path";
|
|
5026
|
+
import os4 from "node:os";
|
|
4343
5027
|
import http from "node:http";
|
|
4344
5028
|
import { spawn, spawnSync } from "node:child_process";
|
|
4345
5029
|
import { fileURLToPath } from "node:url";
|
|
4346
5030
|
var __filename = fileURLToPath(import.meta.url);
|
|
4347
|
-
var __dirname =
|
|
5031
|
+
var __dirname = path6.dirname(__filename);
|
|
4348
5032
|
function getGlobalEsedreDir() {
|
|
4349
5033
|
if (process.env.ESEDRE_GLOBAL_DIR) {
|
|
4350
5034
|
return process.env.ESEDRE_GLOBAL_DIR;
|
|
4351
5035
|
}
|
|
4352
|
-
return
|
|
5036
|
+
return path6.join(os4.homedir(), ".esedre");
|
|
4353
5037
|
}
|
|
4354
5038
|
function ensureGlobalEsedreStore() {
|
|
4355
5039
|
const globalDir = getGlobalEsedreDir();
|
|
4356
|
-
const runDir =
|
|
4357
|
-
const logsDir =
|
|
5040
|
+
const runDir = path6.join(globalDir, "run");
|
|
5041
|
+
const logsDir = path6.join(globalDir, "logs");
|
|
4358
5042
|
try {
|
|
4359
|
-
|
|
4360
|
-
|
|
5043
|
+
fs6.mkdirSync(runDir, { recursive: true });
|
|
5044
|
+
fs6.mkdirSync(logsDir, { recursive: true });
|
|
4361
5045
|
try {
|
|
4362
|
-
const runEntries =
|
|
5046
|
+
const runEntries = fs6.readdirSync(runDir);
|
|
4363
5047
|
for (const entry of runEntries) {
|
|
4364
5048
|
if (entry.startsWith(".probe-")) {
|
|
4365
|
-
|
|
5049
|
+
fs6.unlinkSync(path6.join(runDir, entry));
|
|
4366
5050
|
}
|
|
4367
5051
|
}
|
|
4368
5052
|
} catch {
|
|
4369
5053
|
}
|
|
4370
|
-
const testProbe =
|
|
5054
|
+
const testProbe = path6.join(runDir, `.probe-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`);
|
|
4371
5055
|
try {
|
|
4372
|
-
|
|
5056
|
+
fs6.writeFileSync(testProbe, "ok", "utf-8");
|
|
4373
5057
|
return { ok: true, path: globalDir };
|
|
4374
5058
|
} finally {
|
|
4375
|
-
if (
|
|
5059
|
+
if (fs6.existsSync(testProbe)) {
|
|
4376
5060
|
try {
|
|
4377
|
-
|
|
5061
|
+
fs6.unlinkSync(testProbe);
|
|
4378
5062
|
} catch {
|
|
4379
5063
|
}
|
|
4380
5064
|
}
|
|
@@ -4389,10 +5073,10 @@ function resolveStoreDir(workspaceRoot) {
|
|
|
4389
5073
|
return globalStore.path;
|
|
4390
5074
|
}
|
|
4391
5075
|
if (workspaceRoot) {
|
|
4392
|
-
const localStore =
|
|
5076
|
+
const localStore = path6.join(workspaceRoot, ".esedre");
|
|
4393
5077
|
try {
|
|
4394
|
-
|
|
4395
|
-
|
|
5078
|
+
fs6.mkdirSync(path6.join(localStore, "run"), { recursive: true });
|
|
5079
|
+
fs6.mkdirSync(path6.join(localStore, "logs"), { recursive: true });
|
|
4396
5080
|
return localStore;
|
|
4397
5081
|
} catch {
|
|
4398
5082
|
}
|
|
@@ -4401,32 +5085,32 @@ function resolveStoreDir(workspaceRoot) {
|
|
|
4401
5085
|
}
|
|
4402
5086
|
function getDaemonStateFile(port, workspaceRoot) {
|
|
4403
5087
|
const store = resolveStoreDir(workspaceRoot);
|
|
4404
|
-
return
|
|
5088
|
+
return path6.join(store, "run", `daemon-${port}.json`);
|
|
4405
5089
|
}
|
|
4406
5090
|
function getDaemonLogFile(port, workspaceRoot) {
|
|
4407
5091
|
const store = resolveStoreDir(workspaceRoot);
|
|
4408
|
-
return
|
|
5092
|
+
return path6.join(store, "logs", `daemon-${port}.log`);
|
|
4409
5093
|
}
|
|
4410
5094
|
function getDaemonState(port, workspaceRoot) {
|
|
4411
5095
|
const stateFile = getDaemonStateFile(port, workspaceRoot);
|
|
4412
|
-
if (!
|
|
5096
|
+
if (!fs6.existsSync(stateFile)) {
|
|
4413
5097
|
return null;
|
|
4414
5098
|
}
|
|
4415
5099
|
try {
|
|
4416
|
-
const raw =
|
|
5100
|
+
const raw = fs6.readFileSync(stateFile, "utf-8").trim();
|
|
4417
5101
|
if (!raw) {
|
|
4418
|
-
|
|
5102
|
+
fs6.unlinkSync(stateFile);
|
|
4419
5103
|
return null;
|
|
4420
5104
|
}
|
|
4421
5105
|
const state = JSON.parse(raw);
|
|
4422
5106
|
if (!state || typeof state.pid !== "number" || typeof state.port !== "number") {
|
|
4423
|
-
|
|
5107
|
+
fs6.unlinkSync(stateFile);
|
|
4424
5108
|
return null;
|
|
4425
5109
|
}
|
|
4426
5110
|
return state;
|
|
4427
5111
|
} catch {
|
|
4428
5112
|
try {
|
|
4429
|
-
|
|
5113
|
+
fs6.unlinkSync(stateFile);
|
|
4430
5114
|
} catch {
|
|
4431
5115
|
}
|
|
4432
5116
|
return null;
|
|
@@ -4497,7 +5181,7 @@ async function startDaemon(options = {}) {
|
|
|
4497
5181
|
}
|
|
4498
5182
|
}
|
|
4499
5183
|
try {
|
|
4500
|
-
|
|
5184
|
+
fs6.unlinkSync(stateFile);
|
|
4501
5185
|
} catch {
|
|
4502
5186
|
}
|
|
4503
5187
|
}
|
|
@@ -4516,40 +5200,40 @@ async function startDaemon(options = {}) {
|
|
|
4516
5200
|
};
|
|
4517
5201
|
}
|
|
4518
5202
|
resolveStoreDir(workspaceRoot);
|
|
4519
|
-
|
|
4520
|
-
|
|
5203
|
+
fs6.mkdirSync(path6.dirname(logFile), { recursive: true });
|
|
5204
|
+
fs6.mkdirSync(path6.dirname(stateFile), { recursive: true });
|
|
4521
5205
|
const MAX_LOG_SIZE_BYTES = 10 * 1024 * 1024;
|
|
4522
5206
|
try {
|
|
4523
|
-
if (
|
|
4524
|
-
const stats =
|
|
5207
|
+
if (fs6.existsSync(logFile)) {
|
|
5208
|
+
const stats = fs6.statSync(logFile);
|
|
4525
5209
|
if (stats.size > MAX_LOG_SIZE_BYTES) {
|
|
4526
5210
|
const oldLogFile = `${logFile}.old`;
|
|
4527
5211
|
try {
|
|
4528
|
-
if (
|
|
4529
|
-
|
|
5212
|
+
if (fs6.existsSync(oldLogFile)) fs6.unlinkSync(oldLogFile);
|
|
5213
|
+
fs6.renameSync(logFile, oldLogFile);
|
|
4530
5214
|
} catch {
|
|
4531
|
-
|
|
5215
|
+
fs6.truncateSync(logFile, 0);
|
|
4532
5216
|
}
|
|
4533
5217
|
}
|
|
4534
5218
|
}
|
|
4535
5219
|
} catch {
|
|
4536
5220
|
}
|
|
4537
|
-
const logFd =
|
|
4538
|
-
const isEsedreArgv1 = process.argv[1] && /esedre/i.test(
|
|
5221
|
+
const logFd = fs6.openSync(logFile, "a");
|
|
5222
|
+
const isEsedreArgv1 = process.argv[1] && /esedre/i.test(path6.basename(process.argv[1]));
|
|
4539
5223
|
const cliCandidates = [
|
|
4540
|
-
isEsedreArgv1 ?
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
5224
|
+
isEsedreArgv1 ? path6.resolve(process.argv[1]) : void 0,
|
|
5225
|
+
path6.resolve(workspaceRoot, "dist", "esedre.mjs"),
|
|
5226
|
+
path6.resolve(workspaceRoot, "../esedre/dist", "esedre.mjs"),
|
|
5227
|
+
path6.resolve(__dirname, "esedre.mjs"),
|
|
5228
|
+
path6.resolve(__dirname, "..", "dist", "esedre.mjs"),
|
|
5229
|
+
path6.resolve(__dirname, "..", "..", "dist", "esedre.mjs"),
|
|
5230
|
+
path6.resolve(__dirname, "..", "..", "bin", "esedre.js"),
|
|
5231
|
+
path6.resolve(__dirname, "..", "..", "bin", "esedre.ts"),
|
|
5232
|
+
path6.resolve(__dirname, "..", "esedre.mjs"),
|
|
5233
|
+
path6.resolve(__dirname, "../../../esedre/dist", "esedre.mjs")
|
|
4550
5234
|
].filter(Boolean);
|
|
4551
|
-
const cliPath = cliCandidates.find((c) =>
|
|
4552
|
-
if (!
|
|
5235
|
+
const cliPath = cliCandidates.find((c) => fs6.existsSync(c) && (c.endsWith(".mjs") || c.endsWith(".js") || c.endsWith(".ts"))) || cliCandidates[0];
|
|
5236
|
+
if (!fs6.existsSync(cliPath)) {
|
|
4553
5237
|
throw new Error(`Could not locate Esedre executable. Searched candidates: ${cliCandidates.join(", ")}`);
|
|
4554
5238
|
}
|
|
4555
5239
|
const child = spawn(process.execPath, [cliPath, "start", "--foreground", "--port", String(port)], {
|
|
@@ -4561,7 +5245,7 @@ async function startDaemon(options = {}) {
|
|
|
4561
5245
|
});
|
|
4562
5246
|
child.unref();
|
|
4563
5247
|
try {
|
|
4564
|
-
|
|
5248
|
+
fs6.closeSync(logFd);
|
|
4565
5249
|
} catch {
|
|
4566
5250
|
}
|
|
4567
5251
|
const pid = child.pid;
|
|
@@ -4602,13 +5286,13 @@ async function startDaemon(options = {}) {
|
|
|
4602
5286
|
logFile
|
|
4603
5287
|
};
|
|
4604
5288
|
const tmpState = `${stateFile}.tmp.${Date.now()}`;
|
|
4605
|
-
|
|
5289
|
+
fs6.writeFileSync(tmpState, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
4606
5290
|
try {
|
|
4607
|
-
|
|
5291
|
+
fs6.renameSync(tmpState, stateFile);
|
|
4608
5292
|
} catch {
|
|
4609
|
-
|
|
5293
|
+
fs6.writeFileSync(stateFile, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
4610
5294
|
try {
|
|
4611
|
-
|
|
5295
|
+
fs6.unlinkSync(tmpState);
|
|
4612
5296
|
} catch {
|
|
4613
5297
|
}
|
|
4614
5298
|
}
|
|
@@ -4671,8 +5355,8 @@ async function stopDaemon(options = {}) {
|
|
|
4671
5355
|
}
|
|
4672
5356
|
}
|
|
4673
5357
|
try {
|
|
4674
|
-
if (
|
|
4675
|
-
|
|
5358
|
+
if (fs6.existsSync(stateFile)) {
|
|
5359
|
+
fs6.unlinkSync(stateFile);
|
|
4676
5360
|
}
|
|
4677
5361
|
} catch {
|
|
4678
5362
|
}
|
|
@@ -4709,7 +5393,7 @@ async function getDaemonStatus(options = {}) {
|
|
|
4709
5393
|
}
|
|
4710
5394
|
if (state && !isProcessAlive(state.pid)) {
|
|
4711
5395
|
try {
|
|
4712
|
-
|
|
5396
|
+
fs6.unlinkSync(stateFile);
|
|
4713
5397
|
} catch {
|
|
4714
5398
|
}
|
|
4715
5399
|
}
|
|
@@ -4727,26 +5411,26 @@ function printDaemonLogs(options = {}) {
|
|
|
4727
5411
|
const port = resolvePorts(void 0, { port: options.port }).gateway;
|
|
4728
5412
|
const workspaceRoot = options.workspaceRoot || process.cwd();
|
|
4729
5413
|
const logFile = getDaemonLogFile(port, workspaceRoot);
|
|
4730
|
-
if (!
|
|
5414
|
+
if (!fs6.existsSync(logFile)) {
|
|
4731
5415
|
console.log(`\x1B[33mNo logs found at: ${logFile}\x1B[0m`);
|
|
4732
5416
|
return;
|
|
4733
5417
|
}
|
|
4734
5418
|
const count = options.lines || 40;
|
|
4735
5419
|
let content = "";
|
|
4736
5420
|
try {
|
|
4737
|
-
const stats =
|
|
5421
|
+
const stats = fs6.statSync(logFile);
|
|
4738
5422
|
const maxReadBytes = Math.min(stats.size, Math.max(256 * 1024, count * 2048));
|
|
4739
5423
|
if (stats.size <= maxReadBytes) {
|
|
4740
|
-
content =
|
|
5424
|
+
content = fs6.readFileSync(logFile, "utf-8");
|
|
4741
5425
|
} else {
|
|
4742
|
-
const fd =
|
|
5426
|
+
const fd = fs6.openSync(logFile, "r");
|
|
4743
5427
|
const buffer = Buffer.alloc(maxReadBytes);
|
|
4744
|
-
|
|
4745
|
-
|
|
5428
|
+
fs6.readSync(fd, buffer, 0, maxReadBytes, stats.size - maxReadBytes);
|
|
5429
|
+
fs6.closeSync(fd);
|
|
4746
5430
|
content = buffer.toString("utf-8");
|
|
4747
5431
|
}
|
|
4748
5432
|
} catch {
|
|
4749
|
-
content =
|
|
5433
|
+
content = fs6.readFileSync(logFile, "utf-8");
|
|
4750
5434
|
}
|
|
4751
5435
|
const allLines = content.split(/\r?\n/);
|
|
4752
5436
|
const sanitizedLines = allLines.slice(-count).map((line) => {
|
|
@@ -4931,7 +5615,11 @@ var HISTORIC_SKILL_HASHES = [
|
|
|
4931
5615
|
"8611116c276fdfb598b965c716b251ce7c77c7f7",
|
|
4932
5616
|
"05f129fb6e06a18df00215af105567abcbf0bc67",
|
|
4933
5617
|
"5d330ecf8fc96f383946e0351adb60378afa70f0",
|
|
4934
|
-
"23716c9db561e52e5115dc364fe154d3a7bc1e23"
|
|
5618
|
+
"23716c9db561e52e5115dc364fe154d3a7bc1e23",
|
|
5619
|
+
"75d50d5f9cf9348cca9d9b2466e04cf7992ef17e",
|
|
5620
|
+
"d0bd99ea93d9c65245b3be51188d4bc153024092",
|
|
5621
|
+
"e5d4fe4f2ae0bd464bad877ea1e5ab63cef747fa",
|
|
5622
|
+
"d25597e8c2c26cdc5873e5dbf02335032db07580"
|
|
4935
5623
|
];
|
|
4936
5624
|
var ESEDRE_SKILL_TEMPLATE = `---
|
|
4937
5625
|
name: esedre
|
|
@@ -4940,7 +5628,7 @@ description: Tooling and workflow reference for interacting with the Esedre deve
|
|
|
4940
5628
|
|
|
4941
5629
|
# Esedre Planning & Roadmap Workflow
|
|
4942
5630
|
|
|
4943
|
-
Esedre is the developer ticketing and LLM coding partner coordination platform. The engine codebase resides in the standalone repository \`aellinsar/esedre\` (\`../esedre\`), and operates on decoupled ticket repositories (such as \`aellinsar/esedre-data\` configured in \`.esedre/esedre.json\`). Interact with tickets via in-repo shell wrappers in \`.esedre/\` (\`.esedre/ese\`), or the global \`ese\` CLI.
|
|
5631
|
+
Esedre is the developer ticketing and LLM coding partner coordination platform. The engine codebase resides in the standalone repository \`aellinsar/esedre\` (\`../esedre\`), and operates on decoupled ticket repositories (such as \`aellinsar/esedre-data\` configured in \`~/.esedre/config.json\` or \`.esedre/esedre.json\`). Interact with tickets via in-repo shell wrappers in \`.esedre/\` (\`.esedre/ese\` on POSIX/macOS/Linux/Git Bash, or \`.\\.esedre\\ese.cmd\` on Windows PowerShell/CMD), or the global \`ese\` CLI.
|
|
4944
5632
|
|
|
4945
5633
|
## 0. Authoritative Ingress Hierarchy & Zero-Latency Context (\`.esedre/snapshot.json\`)
|
|
4946
5634
|
|
|
@@ -4949,38 +5637,49 @@ All ticket discovery, inspection, and state management strictly follow this posi
|
|
|
4949
5637
|
1. **Step 1 (Default for Read-Only Inspection) - Local Snapshot Projection**:
|
|
4950
5638
|
Always read \`.esedre/snapshot.json\` in the workspace root first. It provides an immediate, zero-latency local projection containing all active and completed tickets, summaries, implementation plans, revision numbers, completion dates (\`completedAt\`), and staleness metrics (\`daysSinceUpdate\`).
|
|
4951
5639
|
2. **Step 2 (Dynamic Queries & State Mutations) - Esedre MCP Tools**:
|
|
4952
|
-
Use first-class MCP tools when active in the session (\`esedre_list_tickets\`, \`esedre_get_ticket\`, \`esedre_get_plan\`, \`esedre_save_plan\`, \`esedre_update_ticket\`, \`esedre_add_comment\`).
|
|
5640
|
+
Use first-class MCP tools when active in the session (\`esedre_list_tickets\`, \`esedre_get_ticket\`, \`esedre_get_plan\`, \`esedre_save_plan\`, \`esedre_update_ticket\`, \`esedre_claim_ticket\`, \`esedre_renew_lease\`, \`esedre_release_ticket\`, \`esedre_list_leases\`, \`esedre_add_comment\`).
|
|
4953
5641
|
3. **Step 3 (Terminal & Script Fallback) - In-Repo CLI Wrapper**:
|
|
4954
|
-
|
|
5642
|
+
- **POSIX (macOS, Linux, Git Bash, WSL)**: \`.esedre/ese <command>\` (e.g. \`.esedre/ese get\`, \`.esedre/ese plan\`, \`.esedre/ese update\`, \`.esedre/ese lease\`, \`.esedre/ese snapshot\`).
|
|
5643
|
+
- **Windows (PowerShell / CMD)**: \`.\\.esedre\\ese.cmd <command>\` or \`.\\.esedre\\ese.ps1 <command>\` (do not invoke the extensionless POSIX script \`.esedre/ese\` directly in PowerShell or \`cmd.exe\`, as Windows will fail or prompt to select an application to open the file).
|
|
4955
5644
|
4. **Storage Boundary**:
|
|
4956
5645
|
Backing ticket hubs (such as \`esedre-data\`) represent data storage managed by the engine. All agent interactions with roadmap tickets flow through the snapshot projection, MCP tools, or CLI wrappers.
|
|
4957
5646
|
|
|
4958
5647
|
## 1. Model Context Protocol (MCP) Tools
|
|
4959
5648
|
|
|
4960
|
-
When Esedre MCP is active in your agent session (\`.agents/mcp_config.json\`), use these tools directly:
|
|
5649
|
+
When Esedre MCP is active in your agent session (\`.agents/mcp.json\` or \`.agents/mcp_config.json\`), use these tools directly:
|
|
4961
5650
|
|
|
4962
5651
|
| Tool | Purpose | Key Arguments |
|
|
4963
5652
|
|---|---|---|
|
|
4964
|
-
| \`esedre_list_tickets\` | List roadmap tickets with filters | \`project\` (e.g. \`Profe\`, \`Esedre\`, \`Alce\`), \`status\`, \`type\`, \`search\` |
|
|
4965
|
-
| \`esedre_get_ticket\` | Full specification, summary, comments, revision & sha1 | \`ticketId\` (numeric e.g. \`96\` or compound e.g. \`Profe-96\`) |
|
|
5653
|
+
| \`esedre_list_tickets\` | List roadmap tickets with filters | \`project\` (e.g. \`Profe\`, \`Esedre\`, \`Alce\`), \`status\`, \`type\`, \`priority\`, \`milestone\`, \`isBlocked\`, \`linkedTo\`, \`search\`, \`includePlan\` |
|
|
5654
|
+
| \`esedre_get_ticket\` | Full specification, summary, comments, links, lease, revision & sha1 | \`ticketId\` (numeric e.g. \`96\` or compound e.g. \`Profe-96\`) |
|
|
4966
5655
|
| \`esedre_get_plan\` | Active implementation plan markdown | \`ticketId\` (numeric or compound) |
|
|
4967
5656
|
| \`esedre_save_plan\` | Save implementation plan markdown with OCC | \`ticketId\`, \`planMarkdown\`, \`lastHash\` |
|
|
4968
|
-
| \`esedre_create_ticket\` | Mint a new ticket with auto sequential ID | \`title\` (max 48 chars), \`type\`, \`project\`, \`effort\`, \`summary\`, \`detail\` |
|
|
4969
|
-
| \`esedre_update_ticket\` | Update ticket attributes with OCC | \`ticketId\`, \`status\`, \`type\`, \`title\`, \`complexity\`, \`effort\`, \`inDevelopment\`, \`featureFlag\`, \`lastHash\` |
|
|
4970
|
-
| \`
|
|
5657
|
+
| \`esedre_create_ticket\` | Mint a new ticket with auto sequential ID | \`title\` (max 48 chars), \`type\`, \`project\`, \`priority\`, \`milestone\`, \`effort\`, \`summary\`, \`detail\` |
|
|
5658
|
+
| \`esedre_update_ticket\` | Update ticket attributes with OCC | \`ticketId\`, \`status\`, \`type\`, \`priority\`, \`milestone\`, \`title\`, \`detail\`, \`detailMarkdown\`, \`complexity\`, \`effort\`, \`inDevelopment\`, \`featureFlag\`, \`lastHash\` |
|
|
5659
|
+
| \`esedre_claim_ticket\` | Claim an ephemeral TTL lease on a ticket (\`In Development\`) | \`ticketId\`, \`holder\`/\`workerId\`, \`agentType\`/\`system\`, \`ttlMinutes\`, \`branch\`, \`force\` |
|
|
5660
|
+
| \`esedre_renew_lease\` | Extend an active ticket lease TTL | \`ticketId\`, \`holder\`/\`workerId\`, \`system\`, \`ttlMinutes\` |
|
|
5661
|
+
| \`esedre_release_ticket\` | Release a ticket lease and optionally transition status | \`ticketId\`, \`holder\`/\`workerId\`, \`system\`, \`finalStatus\`, \`notes\`, \`force\` |
|
|
5662
|
+
| \`esedre_list_leases\` | List all active unexpired ticket leases | \`project\` |
|
|
5663
|
+
| \`esedre_link_ticket\` | Establish bi-directional ticket relationship | \`sourceTicketId\`, \`relation\` (\`relates-to\` \\| \`blocks\` \\| \`parent-of\` \\| \`duplicates\`), \`targetTicketId\`, \`author\` |
|
|
5664
|
+
| \`esedre_unlink_ticket\` | Remove bi-directional ticket relationship | \`sourceTicketId\`, \`targetTicketId\`, \`relation\` |
|
|
5665
|
+
| \`esedre_list_milestones\` | List project milestones and deliverables progress | \`project\` |
|
|
5666
|
+
| \`esedre_get_milestone\` | Get milestone specs, umbrella flag, and member tickets | \`milestoneId\` (numeric or title), \`project\` |
|
|
5667
|
+
| \`esedre_create_milestone\` | Create a project milestone with optional umbrella flag | \`title\`, \`project\`, \`featureFlag\`, \`status\`, \`targetDate\`, \`description\` |
|
|
5668
|
+
| \`esedre_update_milestone\` | Update milestone status, umbrella flag, or title | \`milestoneId\`, \`project\`, \`title\`, \`featureFlag\`, \`status\`, \`targetDate\`, \`description\` |
|
|
5669
|
+
| \`esedre_add_comment\` | Append developer or agent comment | \`ticketId\`, \`text\`, \`file\`, \`author\` |
|
|
4971
5670
|
|
|
4972
5671
|
## 2. Core CLI Commands (Fallback via \`run_command\`)
|
|
4973
5672
|
|
|
4974
|
-
Both \`esedre\` and \`ese\` work interchangeably:
|
|
5673
|
+
Both \`esedre\` and \`ese\` work interchangeably. On POSIX shells use \`.esedre/ese <command>\`; on Windows PowerShell or CMD use \`.\\.esedre\\ese.cmd <command>\` (or \`.\\.esedre\\ese.ps1 <command>\`):
|
|
4975
5674
|
|
|
4976
5675
|
### List Tickets
|
|
4977
5676
|
\`\`\`bash
|
|
4978
|
-
.esedre/ese list [-p|--project <code|all>] [-s|--status <status>] [-t|--type <type>] [-q|--search <q>] [--json]
|
|
5677
|
+
.esedre/ese list [-p|--project <code|all>] [-s|--status <status>] [-t|--type <type>] [-P|--priority <priority>] [-m|--milestone <name>] [--blocked] [--linked-to <ticketKey>] [-q|--search <q>] [--json]
|
|
4979
5678
|
\`\`\`
|
|
4980
5679
|
|
|
4981
5680
|
### Read Ticket Details
|
|
4982
5681
|
\`\`\`bash
|
|
4983
|
-
.esedre/ese get <ticketId
|
|
5682
|
+
.esedre/ese get <ticketId...> [--json]
|
|
4984
5683
|
\`\`\`
|
|
4985
5684
|
|
|
4986
5685
|
### Read or Update Implementation Plans
|
|
@@ -4992,22 +5691,61 @@ Both \`esedre\` and \`ese\` work interchangeably:
|
|
|
4992
5691
|
|
|
4993
5692
|
### Create a Ticket
|
|
4994
5693
|
\`\`\`bash
|
|
4995
|
-
.esedre/ese create --title "..." [-p|--project <code>] [-t|--type Feature] [--complexity Medium] [--effort "2.0 - 4.0 hours"] [--detail "<md>"] [--file <path>] [--json]
|
|
5694
|
+
.esedre/ese create --title "..." [-p|--project <code>] [-t|--type Feature] [-P|--priority High] [-m|--milestone <name>] [--complexity Medium] [--effort "2.0 - 4.0 hours"] [--detail "<md>"] [--file <path>] [--json]
|
|
4996
5695
|
\`\`\`
|
|
4997
5696
|
|
|
4998
5697
|
### Update Ticket Status & Attributes
|
|
4999
5698
|
\`\`\`bash
|
|
5000
5699
|
.esedre/ese update <ticketId> --status "In Development"
|
|
5001
5700
|
.esedre/ese update <ticketId> --status "Completed"
|
|
5002
|
-
.esedre/ese update <ticketId> --type Feature --complexity Medium --effort "2.0 - 4.0 hours"
|
|
5701
|
+
.esedre/ese update <ticketId> --type Feature -P High --complexity Medium --effort "2.0 - 4.0 hours"
|
|
5702
|
+
.esedre/ese update <ticketId> -P none # Clears priority
|
|
5703
|
+
.esedre/ese update <ticketId> -m "Release 1.0" # Links ticket to milestone (inherits umbrella feature flag)
|
|
5704
|
+
.esedre/ese update <ticketId> -m none # Unlinks ticket from milestone
|
|
5705
|
+
.esedre/ese update <ticketId> --detail "<markdownContent>" # Updates specification markdown
|
|
5706
|
+
.esedre/ese update <ticketId> --file <detailFilePath> # Overwrites specification from file
|
|
5707
|
+
\`\`\`
|
|
5708
|
+
|
|
5709
|
+
### Ticket Specification (\`detail.md\`) Sections
|
|
5710
|
+
When authoring or updating a ticket's \`detail.md\` (via \`--detail\`, \`--file\`, or \`esedre_update_ticket\`), the snapshot compiler and UI parser recognize the following canonical \`###\` headings:
|
|
5711
|
+
- \`### Summary\`: Concise overview of the problem or feature.
|
|
5712
|
+
- \`### Feature Breakdown\` (or aliases \`### Task Breakdown\`, \`### Fix Breakdown\`, \`### Implementation Breakdown\`): Numbered or bulleted deliverables and fix steps (indented sub-bullets are preserved under their parent item). Use \`### Fix Breakdown\` or \`### Task Breakdown\` naturally for \`Bug\` and \`Tools\` tickets.
|
|
5713
|
+
- \`### Technical Details\` (or \`### Technical Details & Architecture\`): Bold-keyed architectural notes (e.g. \`- **Architecture**: ...\`, \`- **Key Modules**: ...\`, \`- **Root Cause**: ...\`, \`- **Technical Detail**: ...\`).
|
|
5714
|
+
- \`### Open Questions\` (or \`### Open Questions & Decisions\`): Unresolved design questions (\`- **QuestionId**: Question text?\`), which populate \`openQuestions[]\` in \`.esedre/snapshot.json\`.
|
|
5715
|
+
|
|
5716
|
+
### Ticket Leases (\`ese lease\`)
|
|
5717
|
+
\`\`\`bash
|
|
5718
|
+
.esedre/ese lease claim <ticketId> [--worker-id <id>] [--system <name>] [--ttl <minutes>] [--branch <branch>] [--force] [--json]
|
|
5719
|
+
.esedre/ese lease renew <ticketId> [--worker-id <id>] [--system <name>] [--ttl <minutes>] [--json]
|
|
5720
|
+
.esedre/ese lease release <ticketId> [--worker-id <id>] [--system <name>] [--status <Completed|Planned>] [--notes "..."] [--force] [--json]
|
|
5721
|
+
.esedre/ese lease list [-p|--project <code|all>] [--json]
|
|
5722
|
+
# Top-level aliases: ese claim, ese renew, ese release, ese leases
|
|
5003
5723
|
\`\`\`
|
|
5004
5724
|
|
|
5005
5725
|
### Append Comments or Notes
|
|
5006
5726
|
\`\`\`bash
|
|
5007
5727
|
.esedre/ese comment <ticketId> "Verified implementation." --author "Agent"
|
|
5728
|
+
.esedre/ese comment <ticketId> --file <commentFilePath> --author "Agent"
|
|
5008
5729
|
.esedre/ese comment <ticketId> --text "Verified implementation." --author "Agent"
|
|
5009
5730
|
\`\`\`
|
|
5010
5731
|
|
|
5732
|
+
### Link or Unlink Tickets
|
|
5733
|
+
\`\`\`bash
|
|
5734
|
+
.esedre/ese link <sourceId> <relation> <targetId> [--author "..."]
|
|
5735
|
+
.esedre/ese unlink <sourceId> <targetId> [--relation <relation>]
|
|
5736
|
+
# Supported relations: relates-to, blocks, parent-of, duplicates
|
|
5737
|
+
# Inverse reciprocal links are automatically synchronized.
|
|
5738
|
+
\`\`\`
|
|
5739
|
+
|
|
5740
|
+
### Manage Milestones & Umbrella Feature Flags
|
|
5741
|
+
\`\`\`bash
|
|
5742
|
+
.esedre/ese milestone list [-p|--project <code>] [--json]
|
|
5743
|
+
.esedre/ese milestone get <id|title> [-p|--project <code>] [--json]
|
|
5744
|
+
.esedre/ese milestone create --title "..." [-p|--project <code>] [--flag <flag>] [--status <status>] [--target-date <YYYY-MM-DD>] [--description "..."] [--json]
|
|
5745
|
+
.esedre/ese milestone update <id|title> [-p|--project <code>] [--title "..."] [--flag <flag|none>] [--status <status>] [--target-date <YYYY-MM-DD>] [--description "..."] [--json]
|
|
5746
|
+
.esedre/ese milestone delete <id|title> [-p|--project <code>] [--json]
|
|
5747
|
+
\`\`\`
|
|
5748
|
+
|
|
5011
5749
|
### Regenerate Snapshot
|
|
5012
5750
|
\`\`\`bash
|
|
5013
5751
|
.esedre/ese snapshot
|
|
@@ -5050,7 +5788,10 @@ If the host application declares any or all of the 12 core design tokens on \`:r
|
|
|
5050
5788
|
## 4. Safety & Invariants
|
|
5051
5789
|
- **Always use \`--json\`** for CLI programmatic inspection.
|
|
5052
5790
|
- **Optimistic Concurrency**: Writes support \`--last-hash <hash>\` to prevent overwriting concurrent updates.
|
|
5791
|
+
- **Ticket Leases (\`ese lease\`)**: Claim a lease (\`ese lease claim <id>\` or \`esedre_claim_ticket\`) when starting active work so concurrent agent sessions or subagents do not collide on the same ticket, and release it (\`ese lease release <id>\`) upon completion.
|
|
5053
5792
|
- **Universal Type Naming**: Always use \`type\` (\`Feature\`, \`Platform\`, \`Tools\`, \`Idea\`, \`Bug\`). The legacy name \`category\` is deprecated.
|
|
5793
|
+
- **Ticket Priority**: The \`priority\` field is optional (\`Critical\`, \`High\`, \`Medium\`, \`Low\`, or unset/null). Backwards compatible with legacy tickets lacking a priority field. To clear via CLI use \`-P none\` or \`--priority none\` (in code/MCP, pass \`null\` or \`'none'\`).
|
|
5794
|
+
- **Milestones & Umbrella Feature Flags**: Milestones group tickets into deliverables with completion metrics. Milestones can declare an optional umbrella feature flag which member tickets inherit automatically. Unlinking a ticket (\`-m none\`) removes inherited flags.
|
|
5054
5795
|
- **Compound IDs for Multi-Project Portfolios**: In \`project=all\`, ticket IDs are \`\${projectCode}-\${id}\` (e.g. \`Profe-1\`, \`Esedre-1\`). DOM anchors strictly use \`feature-card-\${projectCode}-\${id}\`.
|
|
5055
5796
|
- **Never delete tickets** directly via filesystem.
|
|
5056
5797
|
- **Workspace Scoping**: An LLM coding partner operates strictly within the authorized project scope declared in the local repository configuration. Central repository linking and system-level configuration are managed separately; focus roadmap work, implementation plans, and verification notes on this project.
|
|
@@ -5059,9 +5800,38 @@ If the host application declares any or all of the 12 core design tokens on \`:r
|
|
|
5059
5800
|
- **SEO Invariant**: These developer routes are private and MUST NEVER be exposed in \`sitemap.xml\`, \`robots.txt\`, \`llms.txt\`, or public navigation links.
|
|
5060
5801
|
- The URL hash is preserved across hard page refreshes (F5) without falling back to \`#/chat\` or other views.
|
|
5061
5802
|
`;
|
|
5803
|
+
var AGENT_RULE_MAX_BYTES = 24e3;
|
|
5804
|
+
function checkAgentRuleSizes(targetDir) {
|
|
5805
|
+
const candidates = [
|
|
5806
|
+
"AGENTS.md",
|
|
5807
|
+
"GEMINI.md",
|
|
5808
|
+
".agents/AGENTS.md",
|
|
5809
|
+
".agents/GEMINI.md",
|
|
5810
|
+
".gemini/GEMINI.md"
|
|
5811
|
+
];
|
|
5812
|
+
const warnings = [];
|
|
5813
|
+
for (const rel of candidates) {
|
|
5814
|
+
const fullPath = path7.join(targetDir, rel);
|
|
5815
|
+
if (fs7.existsSync(fullPath)) {
|
|
5816
|
+
try {
|
|
5817
|
+
const stat = fs7.statSync(fullPath);
|
|
5818
|
+
if (stat.isFile() && stat.size > AGENT_RULE_MAX_BYTES) {
|
|
5819
|
+
warnings.push({
|
|
5820
|
+
file: rel,
|
|
5821
|
+
bytes: stat.size,
|
|
5822
|
+
limitBytes: AGENT_RULE_MAX_BYTES,
|
|
5823
|
+
message: `${rel} is ${stat.size.toLocaleString()} bytes (exceeds ${AGENT_RULE_MAX_BYTES.toLocaleString()}-byte agent rule context limit and will be truncated; extract detailed specs into .agents/skills/<name>/SKILL.md).`
|
|
5824
|
+
});
|
|
5825
|
+
}
|
|
5826
|
+
} catch {
|
|
5827
|
+
}
|
|
5828
|
+
}
|
|
5829
|
+
}
|
|
5830
|
+
return warnings;
|
|
5831
|
+
}
|
|
5062
5832
|
function plantWrappers(targetDir) {
|
|
5063
|
-
const esedreDir =
|
|
5064
|
-
|
|
5833
|
+
const esedreDir = path7.join(targetDir, ".esedre");
|
|
5834
|
+
fs7.mkdirSync(esedreDir, { recursive: true });
|
|
5065
5835
|
const wrappers = [
|
|
5066
5836
|
{ name: "esedre.cmd", content: WRAPPER_CMD, exec: false },
|
|
5067
5837
|
{ name: "esedre.ps1", content: WRAPPER_PS1, exec: false },
|
|
@@ -5071,62 +5841,117 @@ function plantWrappers(targetDir) {
|
|
|
5071
5841
|
{ name: "ese", content: ESE_WRAPPER_BASH, exec: true }
|
|
5072
5842
|
];
|
|
5073
5843
|
for (const w of wrappers) {
|
|
5074
|
-
const fullPath =
|
|
5075
|
-
|
|
5844
|
+
const fullPath = path7.join(esedreDir, w.name);
|
|
5845
|
+
fs7.writeFileSync(fullPath, w.content, "utf-8");
|
|
5076
5846
|
if (w.exec) {
|
|
5077
5847
|
try {
|
|
5078
|
-
|
|
5848
|
+
fs7.chmodSync(fullPath, 493);
|
|
5079
5849
|
} catch {
|
|
5080
5850
|
}
|
|
5081
5851
|
}
|
|
5082
5852
|
}
|
|
5083
5853
|
}
|
|
5084
|
-
function
|
|
5085
|
-
const
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
let configObj = { mcpServers: {} };
|
|
5089
|
-
if (fs6.existsSync(mcpConfigPath)) {
|
|
5090
|
-
try {
|
|
5091
|
-
configObj = JSON.parse(fs6.readFileSync(mcpConfigPath, "utf-8"));
|
|
5092
|
-
if (!configObj.mcpServers) configObj.mcpServers = {};
|
|
5093
|
-
} catch {
|
|
5094
|
-
}
|
|
5095
|
-
}
|
|
5096
|
-
if (process.platform === "win32") {
|
|
5097
|
-
configObj.mcpServers.esedre = {
|
|
5098
|
-
command: "cmd.exe",
|
|
5099
|
-
args: ["/c", ".esedre\\esedre.cmd", "mcp"]
|
|
5100
|
-
};
|
|
5101
|
-
} else {
|
|
5102
|
-
configObj.mcpServers.esedre = {
|
|
5103
|
-
command: ".esedre/esedre",
|
|
5104
|
-
args: ["mcp"]
|
|
5105
|
-
};
|
|
5854
|
+
function setupGlobalAntigravityMcpConfig(options = {}) {
|
|
5855
|
+
const geminiDir = options.geminiDir || process.env.ESEDRE_GEMINI_DIR || (process.env.VITEST ? null : path7.join(os5.homedir(), ".gemini"));
|
|
5856
|
+
if (!geminiDir || !fs7.existsSync(geminiDir)) {
|
|
5857
|
+
return false;
|
|
5106
5858
|
}
|
|
5107
5859
|
try {
|
|
5108
|
-
|
|
5860
|
+
const configDir = path7.join(geminiDir, "config");
|
|
5861
|
+
fs7.mkdirSync(configDir, { recursive: true });
|
|
5862
|
+
const mcpConfigPath = path7.join(configDir, "mcp_config.json");
|
|
5863
|
+
let configObj = { mcpServers: {} };
|
|
5864
|
+
if (fs7.existsSync(mcpConfigPath)) {
|
|
5865
|
+
try {
|
|
5866
|
+
configObj = JSON.parse(fs7.readFileSync(mcpConfigPath, "utf-8"));
|
|
5867
|
+
if (!configObj || typeof configObj !== "object") configObj = {};
|
|
5868
|
+
if (!configObj.mcpServers || typeof configObj.mcpServers !== "object") {
|
|
5869
|
+
configObj.mcpServers = {};
|
|
5870
|
+
}
|
|
5871
|
+
} catch {
|
|
5872
|
+
configObj = { mcpServers: {} };
|
|
5873
|
+
}
|
|
5874
|
+
}
|
|
5875
|
+
const existing = configObj.mcpServers.esedre;
|
|
5876
|
+
const existingArgs = Array.isArray(existing?.args) ? existing.args.join(" ") : "";
|
|
5877
|
+
const isBareRelativeWrapper = existing?.command === ".esedre/esedre" || existing?.command === ".esedre\\esedre.cmd" || existing?.command === "cmd.exe" && existingArgs.includes(".esedre\\esedre.cmd") && !existingArgs.toLowerCase().includes("if exist");
|
|
5878
|
+
if (!existing || isBareRelativeWrapper) {
|
|
5879
|
+
if (process.platform === "win32") {
|
|
5880
|
+
const globalNpmEse = path7.join(
|
|
5881
|
+
process.env.APPDATA || path7.join(os5.homedir(), "AppData", "Roaming"),
|
|
5882
|
+
"npm",
|
|
5883
|
+
"ese.cmd"
|
|
5884
|
+
);
|
|
5885
|
+
const fallbackCmd = fs7.existsSync(globalNpmEse) ? `call "${globalNpmEse}" mcp` : "call ese mcp";
|
|
5886
|
+
configObj.mcpServers.esedre = {
|
|
5887
|
+
command: "cmd.exe",
|
|
5888
|
+
args: ["/c", `if exist .esedre\\esedre.cmd (call .esedre\\esedre.cmd mcp) else (${fallbackCmd})`]
|
|
5889
|
+
};
|
|
5890
|
+
} else {
|
|
5891
|
+
configObj.mcpServers.esedre = {
|
|
5892
|
+
command: "sh",
|
|
5893
|
+
args: ["-c", "if [ -x .esedre/esedre ]; then exec .esedre/esedre mcp; else exec ese mcp; fi"]
|
|
5894
|
+
};
|
|
5895
|
+
}
|
|
5896
|
+
fs7.writeFileSync(mcpConfigPath, JSON.stringify(configObj, null, 2) + "\n", "utf-8");
|
|
5897
|
+
}
|
|
5109
5898
|
return true;
|
|
5110
5899
|
} catch {
|
|
5111
5900
|
return false;
|
|
5112
5901
|
}
|
|
5113
5902
|
}
|
|
5903
|
+
function setupMcpConfig(targetDir, options = {}) {
|
|
5904
|
+
const agentsDir = path7.join(targetDir, ".agents");
|
|
5905
|
+
fs7.mkdirSync(agentsDir, { recursive: true });
|
|
5906
|
+
const writeConfigFile = (fileName) => {
|
|
5907
|
+
const mcpConfigPath = path7.join(agentsDir, fileName);
|
|
5908
|
+
let configObj = { mcpServers: {} };
|
|
5909
|
+
if (fs7.existsSync(mcpConfigPath)) {
|
|
5910
|
+
try {
|
|
5911
|
+
configObj = JSON.parse(fs7.readFileSync(mcpConfigPath, "utf-8"));
|
|
5912
|
+
if (!configObj.mcpServers) configObj.mcpServers = {};
|
|
5913
|
+
} catch {
|
|
5914
|
+
}
|
|
5915
|
+
}
|
|
5916
|
+
if (process.platform === "win32") {
|
|
5917
|
+
configObj.mcpServers.esedre = {
|
|
5918
|
+
command: "cmd.exe",
|
|
5919
|
+
args: ["/c", ".esedre\\esedre.cmd", "mcp"]
|
|
5920
|
+
};
|
|
5921
|
+
} else {
|
|
5922
|
+
configObj.mcpServers.esedre = {
|
|
5923
|
+
command: ".esedre/esedre",
|
|
5924
|
+
args: ["mcp"]
|
|
5925
|
+
};
|
|
5926
|
+
}
|
|
5927
|
+
try {
|
|
5928
|
+
fs7.writeFileSync(mcpConfigPath, JSON.stringify(configObj, null, 2) + "\n", "utf-8");
|
|
5929
|
+
return true;
|
|
5930
|
+
} catch {
|
|
5931
|
+
return false;
|
|
5932
|
+
}
|
|
5933
|
+
};
|
|
5934
|
+
const okLegacy = writeConfigFile("mcp_config.json");
|
|
5935
|
+
const okModern = writeConfigFile("mcp.json");
|
|
5936
|
+
setupGlobalAntigravityMcpConfig(options);
|
|
5937
|
+
return okLegacy && okModern;
|
|
5938
|
+
}
|
|
5114
5939
|
function setupSkill(targetDir, options = {}) {
|
|
5115
|
-
const skillDir =
|
|
5116
|
-
|
|
5117
|
-
const skillFile =
|
|
5118
|
-
if (!
|
|
5119
|
-
|
|
5940
|
+
const skillDir = path7.join(targetDir, ".agents", "skills", "esedre");
|
|
5941
|
+
fs7.mkdirSync(skillDir, { recursive: true });
|
|
5942
|
+
const skillFile = path7.join(skillDir, "SKILL.md");
|
|
5943
|
+
if (!fs7.existsSync(skillFile)) {
|
|
5944
|
+
fs7.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
|
|
5120
5945
|
return { status: "MISSING", updated: true };
|
|
5121
5946
|
}
|
|
5122
|
-
const currentContent =
|
|
5947
|
+
const currentContent = fs7.readFileSync(skillFile, "utf-8");
|
|
5123
5948
|
const status = classifyContent(currentContent, ESEDRE_SKILL_TEMPLATE, HISTORIC_SKILL_HASHES);
|
|
5124
5949
|
if (status === "LATEST" && !options.force) {
|
|
5125
5950
|
return { status, updated: false };
|
|
5126
5951
|
}
|
|
5127
|
-
const hasBannedOrDeprecatedTerms = currentContent.includes("companion agent") || currentContent.includes("companion LLM") || currentContent.includes("tools/esedre.mjs");
|
|
5952
|
+
const hasBannedOrDeprecatedTerms = currentContent.includes("companion agent") || currentContent.includes("companion LLM") || currentContent.includes("tools/esedre.mjs") || currentContent.includes("# Esedre Planning & Roadmap Workflow") && (!currentContent.includes("ese.cmd") || !currentContent.includes("Fix Breakdown") || !currentContent.includes("<ticketId...>"));
|
|
5128
5953
|
if (options.force || status === "HISTORIC_DEFAULT" || status === "MISSING" || hasBannedOrDeprecatedTerms) {
|
|
5129
|
-
|
|
5954
|
+
fs7.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
|
|
5130
5955
|
return { status: "LATEST", updated: true };
|
|
5131
5956
|
}
|
|
5132
5957
|
return { status, updated: false };
|
|
@@ -5140,8 +5965,8 @@ var VITE_CONFIG_FILES = [
|
|
|
5140
5965
|
];
|
|
5141
5966
|
function findViteConfig(targetDir) {
|
|
5142
5967
|
for (const name of VITE_CONFIG_FILES) {
|
|
5143
|
-
const fullPath =
|
|
5144
|
-
if (
|
|
5968
|
+
const fullPath = path7.join(targetDir, name);
|
|
5969
|
+
if (fs7.existsSync(fullPath)) {
|
|
5145
5970
|
return fullPath;
|
|
5146
5971
|
}
|
|
5147
5972
|
}
|
|
@@ -5205,10 +6030,10 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
|
|
|
5205
6030
|
recommendedProxySnippet: snippet
|
|
5206
6031
|
};
|
|
5207
6032
|
}
|
|
5208
|
-
const relativeName =
|
|
6033
|
+
const relativeName = path7.basename(configPath);
|
|
5209
6034
|
let content = "";
|
|
5210
6035
|
try {
|
|
5211
|
-
content =
|
|
6036
|
+
content = fs7.readFileSync(configPath, "utf-8");
|
|
5212
6037
|
} catch {
|
|
5213
6038
|
return {
|
|
5214
6039
|
status: "MANUAL_REQUIRED",
|
|
@@ -5239,7 +6064,7 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
|
|
|
5239
6064
|
};
|
|
5240
6065
|
}
|
|
5241
6066
|
try {
|
|
5242
|
-
|
|
6067
|
+
fs7.writeFileSync(configPath, injected, "utf-8");
|
|
5243
6068
|
return {
|
|
5244
6069
|
status: "CONFIGURED",
|
|
5245
6070
|
configFile: relativeName,
|
|
@@ -5254,15 +6079,15 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
|
|
|
5254
6079
|
}
|
|
5255
6080
|
}
|
|
5256
6081
|
function configureWorkspace(targetDir, options = {}) {
|
|
5257
|
-
const esedreDir =
|
|
5258
|
-
if (!
|
|
5259
|
-
|
|
6082
|
+
const esedreDir = path7.join(targetDir, ".esedre");
|
|
6083
|
+
if (!fs7.existsSync(esedreDir)) {
|
|
6084
|
+
fs7.mkdirSync(esedreDir, { recursive: true });
|
|
5260
6085
|
}
|
|
5261
|
-
const esedreJsonPath =
|
|
6086
|
+
const esedreJsonPath = path7.join(esedreDir, "esedre.json");
|
|
5262
6087
|
let existingRaw = null;
|
|
5263
|
-
if (
|
|
6088
|
+
if (fs7.existsSync(esedreJsonPath)) {
|
|
5264
6089
|
try {
|
|
5265
|
-
existingRaw = JSON.parse(
|
|
6090
|
+
existingRaw = JSON.parse(fs7.readFileSync(esedreJsonPath, "utf-8"));
|
|
5266
6091
|
} catch {
|
|
5267
6092
|
}
|
|
5268
6093
|
}
|
|
@@ -5298,16 +6123,16 @@ function configureWorkspace(targetDir, options = {}) {
|
|
|
5298
6123
|
port: options.port || void 0
|
|
5299
6124
|
};
|
|
5300
6125
|
}
|
|
5301
|
-
|
|
6126
|
+
fs7.writeFileSync(esedreJsonPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
5302
6127
|
let projectRegistered = false;
|
|
5303
6128
|
if (projectCode) {
|
|
5304
|
-
const hasExistingTicketsDir =
|
|
5305
|
-
const ticketsDir =
|
|
5306
|
-
if (!hasExistingTicketsDir && !
|
|
5307
|
-
|
|
6129
|
+
const hasExistingTicketsDir = fs7.existsSync(path7.join(targetDir, "tickets")) || fs7.existsSync(path7.join(targetDir, "src", "data", "planning", "tickets"));
|
|
6130
|
+
const ticketsDir = path7.join(esedreDir, "tickets");
|
|
6131
|
+
if (!hasExistingTicketsDir && !fs7.existsSync(ticketsDir)) {
|
|
6132
|
+
fs7.mkdirSync(ticketsDir, { recursive: true });
|
|
5308
6133
|
}
|
|
5309
|
-
const inRepoPJson =
|
|
5310
|
-
if (!
|
|
6134
|
+
const inRepoPJson = path7.join(esedreDir, "project.json");
|
|
6135
|
+
if (!fs7.existsSync(inRepoPJson)) {
|
|
5311
6136
|
const projDesc = {
|
|
5312
6137
|
id: 1,
|
|
5313
6138
|
code: projectCode.toUpperCase(),
|
|
@@ -5319,14 +6144,14 @@ function configureWorkspace(targetDir, options = {}) {
|
|
|
5319
6144
|
border: "border-cyan-500/40"
|
|
5320
6145
|
}
|
|
5321
6146
|
};
|
|
5322
|
-
|
|
6147
|
+
fs7.writeFileSync(inRepoPJson, JSON.stringify(projDesc, null, 2) + "\n", "utf-8");
|
|
5323
6148
|
projectRegistered = true;
|
|
5324
6149
|
} else if (projectName) {
|
|
5325
6150
|
try {
|
|
5326
|
-
const existing = JSON.parse(
|
|
6151
|
+
const existing = JSON.parse(fs7.readFileSync(inRepoPJson, "utf-8"));
|
|
5327
6152
|
if (existing.name !== projectName) {
|
|
5328
6153
|
existing.name = projectName;
|
|
5329
|
-
|
|
6154
|
+
fs7.writeFileSync(inRepoPJson, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
5330
6155
|
}
|
|
5331
6156
|
} catch {
|
|
5332
6157
|
}
|
|
@@ -5345,6 +6170,7 @@ function configureWorkspace(targetDir, options = {}) {
|
|
|
5345
6170
|
if (!globalStoreRes.ok) {
|
|
5346
6171
|
console.warn(`\x1B[33m\u26A0\uFE0F Warning: Could not initialize global store at ${globalStoreRes.path}: ${globalStoreRes.error}\x1B[0m`);
|
|
5347
6172
|
}
|
|
6173
|
+
const ruleSizeWarnings = checkAgentRuleSizes(targetDir);
|
|
5348
6174
|
return {
|
|
5349
6175
|
esedreJsonCreatedOrUpdated: true,
|
|
5350
6176
|
projectRegistered,
|
|
@@ -5359,7 +6185,8 @@ function configureWorkspace(targetDir, options = {}) {
|
|
|
5359
6185
|
recommendedProxySnippet: proxyRes.recommendedProxySnippet,
|
|
5360
6186
|
globalStoreStatus: globalStoreRes.ok ? "ready" : "warning",
|
|
5361
6187
|
globalStorePath: globalStoreRes.path,
|
|
5362
|
-
globalStoreError: globalStoreRes.error
|
|
6188
|
+
globalStoreError: globalStoreRes.error,
|
|
6189
|
+
ruleSizeWarnings
|
|
5363
6190
|
};
|
|
5364
6191
|
}
|
|
5365
6192
|
var initWorkspace = configureWorkspace;
|
|
@@ -5369,33 +6196,33 @@ var ESEDRE_HUB_AGENTS_TEMPLATE = `# Agent Guidelines: esedre-data
|
|
|
5369
6196
|
- **Backing Storage Repository**: \`esedre-data\` is a dedicated, decoupled data hub storing raw tickets, metadata manifests, and implementation plans managed by the Esedre engine.
|
|
5370
6197
|
- **Authorized Ingress Channels**: Ticket discovery, read-only inspection, status updates, and plan authoring strictly ingress through the Esedre application layer in consuming project workspaces:
|
|
5371
6198
|
1. **Local Workspace Snapshot**: Check the consuming workspace's \`.esedre/snapshot.json\` first for instant, zero-latency read access to ticket summaries, plans, and statuses.
|
|
5372
|
-
2. **Esedre MCP Tools**: Use \`esedre_get_ticket\`, \`esedre_list_tickets\`, \`esedre_get_plan\`, \`esedre_save_plan\`, and \`
|
|
5373
|
-
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.
|
|
5374
6201
|
|
|
5375
6202
|
## 2. Ingress Redirection Protocol
|
|
5376
6203
|
When inspecting or managing tickets for any project (such as \`Profe\`, \`Esedre\`, or \`Alce\`), navigate to that project's workspace and utilize its local \`.esedre/snapshot.json\`, Esedre MCP tools, or \`.esedre/ese\` CLI wrapper.
|
|
5377
6204
|
`;
|
|
5378
6205
|
function initHub(targetDir) {
|
|
5379
|
-
const resolved =
|
|
5380
|
-
if (!
|
|
5381
|
-
|
|
6206
|
+
const resolved = path7.resolve(targetDir);
|
|
6207
|
+
if (!fs7.existsSync(resolved)) {
|
|
6208
|
+
fs7.mkdirSync(resolved, { recursive: true });
|
|
5382
6209
|
}
|
|
5383
|
-
const projectsDir =
|
|
6210
|
+
const projectsDir = path7.join(resolved, "projects");
|
|
5384
6211
|
let projectsDirCreated = false;
|
|
5385
|
-
if (!
|
|
5386
|
-
|
|
6212
|
+
if (!fs7.existsSync(projectsDir)) {
|
|
6213
|
+
fs7.mkdirSync(projectsDir, { recursive: true });
|
|
5387
6214
|
projectsDirCreated = true;
|
|
5388
6215
|
}
|
|
5389
|
-
const projectsJsonPath =
|
|
6216
|
+
const projectsJsonPath = path7.join(resolved, "projects.json");
|
|
5390
6217
|
let projectsJsonCreated = false;
|
|
5391
|
-
if (!
|
|
5392
|
-
|
|
6218
|
+
if (!fs7.existsSync(projectsJsonPath)) {
|
|
6219
|
+
fs7.writeFileSync(projectsJsonPath, "[]\n", "utf-8");
|
|
5393
6220
|
projectsJsonCreated = true;
|
|
5394
6221
|
}
|
|
5395
|
-
const agentsMdPath =
|
|
6222
|
+
const agentsMdPath = path7.join(resolved, "AGENTS.md");
|
|
5396
6223
|
let agentsMdCreated = false;
|
|
5397
|
-
if (!
|
|
5398
|
-
|
|
6224
|
+
if (!fs7.existsSync(agentsMdPath)) {
|
|
6225
|
+
fs7.writeFileSync(agentsMdPath, ESEDRE_HUB_AGENTS_TEMPLATE, "utf-8");
|
|
5399
6226
|
agentsMdCreated = true;
|
|
5400
6227
|
}
|
|
5401
6228
|
return {
|
|
@@ -5424,14 +6251,14 @@ function findOutdatedWrappers(currentWorkspaceRoot, globalConfig) {
|
|
|
5424
6251
|
const outdated = [];
|
|
5425
6252
|
const checkedDirs = /* @__PURE__ */ new Set();
|
|
5426
6253
|
const checkDir = (dir, name) => {
|
|
5427
|
-
const resolved =
|
|
6254
|
+
const resolved = path7.resolve(expandHome(dir));
|
|
5428
6255
|
if (checkedDirs.has(resolved.toLowerCase())) return;
|
|
5429
6256
|
checkedDirs.add(resolved.toLowerCase());
|
|
5430
|
-
if (!
|
|
5431
|
-
const cmdPath =
|
|
5432
|
-
if (
|
|
6257
|
+
if (!fs7.existsSync(resolved)) return;
|
|
6258
|
+
const cmdPath = path7.join(resolved, ".esedre", "esedre.cmd");
|
|
6259
|
+
if (fs7.existsSync(cmdPath)) {
|
|
5433
6260
|
try {
|
|
5434
|
-
const content =
|
|
6261
|
+
const content = fs7.readFileSync(cmdPath, "utf-8");
|
|
5435
6262
|
if (isWrapperOutdated(content)) {
|
|
5436
6263
|
outdated.push({ name, dir: resolved, wrapperPath: cmdPath });
|
|
5437
6264
|
}
|
|
@@ -5440,7 +6267,7 @@ function findOutdatedWrappers(currentWorkspaceRoot, globalConfig) {
|
|
|
5440
6267
|
}
|
|
5441
6268
|
};
|
|
5442
6269
|
if (currentWorkspaceRoot) {
|
|
5443
|
-
checkDir(currentWorkspaceRoot,
|
|
6270
|
+
checkDir(currentWorkspaceRoot, path7.basename(currentWorkspaceRoot));
|
|
5444
6271
|
}
|
|
5445
6272
|
if (globalConfig?.projects) {
|
|
5446
6273
|
for (const [code, p] of Object.entries(globalConfig.projects)) {
|
|
@@ -5452,20 +6279,20 @@ function findOutdatedWrappers(currentWorkspaceRoot, globalConfig) {
|
|
|
5452
6279
|
async function upgradeAllWorkspaces(options = {}) {
|
|
5453
6280
|
const globalCfg = options.globalConfig !== void 0 ? options.globalConfig : readGlobalConfig();
|
|
5454
6281
|
const workspaceMap = /* @__PURE__ */ new Map();
|
|
5455
|
-
if (options.currentWorkspaceRoot &&
|
|
5456
|
-
const hasEsedre =
|
|
6282
|
+
if (options.currentWorkspaceRoot && fs7.existsSync(options.currentWorkspaceRoot)) {
|
|
6283
|
+
const hasEsedre = fs7.existsSync(path7.join(options.currentWorkspaceRoot, ".esedre")) || fs7.existsSync(path7.join(options.currentWorkspaceRoot, "esedre.json"));
|
|
5457
6284
|
if (hasEsedre) {
|
|
5458
6285
|
const disc = findEsedreConfig(options.currentWorkspaceRoot, { fallbackToGlobal: true });
|
|
5459
6286
|
workspaceMap.set(
|
|
5460
|
-
|
|
5461
|
-
disc.config?.projectCode ||
|
|
6287
|
+
path7.resolve(options.currentWorkspaceRoot),
|
|
6288
|
+
disc.config?.projectCode || path7.basename(options.currentWorkspaceRoot)
|
|
5462
6289
|
);
|
|
5463
6290
|
}
|
|
5464
6291
|
}
|
|
5465
6292
|
if (globalCfg?.projects) {
|
|
5466
6293
|
for (const [code, p] of Object.entries(globalCfg.projects)) {
|
|
5467
|
-
const resolved =
|
|
5468
|
-
if (
|
|
6294
|
+
const resolved = path7.resolve(expandHome(p));
|
|
6295
|
+
if (fs7.existsSync(resolved)) {
|
|
5469
6296
|
if (!workspaceMap.has(resolved)) {
|
|
5470
6297
|
workspaceMap.set(resolved, code);
|
|
5471
6298
|
}
|
|
@@ -5516,15 +6343,15 @@ async function upgradeAllWorkspaces(options = {}) {
|
|
|
5516
6343
|
}
|
|
5517
6344
|
|
|
5518
6345
|
// src/server/gateway.ts
|
|
5519
|
-
import
|
|
6346
|
+
import fs10 from "node:fs";
|
|
5520
6347
|
import http4 from "node:http";
|
|
5521
|
-
import
|
|
6348
|
+
import path10 from "node:path";
|
|
5522
6349
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5523
6350
|
|
|
5524
6351
|
// src/server/uiServer.ts
|
|
5525
6352
|
import http2 from "node:http";
|
|
5526
|
-
import
|
|
5527
|
-
import
|
|
6353
|
+
import fs8 from "node:fs";
|
|
6354
|
+
import path8 from "node:path";
|
|
5528
6355
|
var MIME_TYPES = {
|
|
5529
6356
|
".html": "text/html; charset=utf-8",
|
|
5530
6357
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -5554,27 +6381,27 @@ function startUiServer(port, webDir) {
|
|
|
5554
6381
|
if (!reqPath || reqPath === "/") {
|
|
5555
6382
|
reqPath = "/index.html";
|
|
5556
6383
|
}
|
|
5557
|
-
const safePath =
|
|
5558
|
-
let filePath =
|
|
5559
|
-
const resolvedFilePath =
|
|
5560
|
-
const resolvedWebDir =
|
|
6384
|
+
const safePath = path8.normalize(reqPath).replace(/^(\.\.[/\\])+/, "");
|
|
6385
|
+
let filePath = path8.join(webDir, safePath);
|
|
6386
|
+
const resolvedFilePath = path8.resolve(filePath);
|
|
6387
|
+
const resolvedWebDir = path8.resolve(webDir);
|
|
5561
6388
|
if (!resolvedFilePath.startsWith(resolvedWebDir)) {
|
|
5562
6389
|
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
5563
6390
|
res.end("Access Denied: Path escapes web root.");
|
|
5564
6391
|
return;
|
|
5565
6392
|
}
|
|
5566
|
-
if (!
|
|
5567
|
-
filePath =
|
|
6393
|
+
if (!fs8.existsSync(filePath) || fs8.statSync(filePath).isDirectory()) {
|
|
6394
|
+
filePath = path8.join(webDir, "index.html");
|
|
5568
6395
|
}
|
|
5569
|
-
if (!
|
|
6396
|
+
if (!fs8.existsSync(filePath)) {
|
|
5570
6397
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
5571
6398
|
res.end("Esedre UI: index.html not found. Run npm run build first.");
|
|
5572
6399
|
return;
|
|
5573
6400
|
}
|
|
5574
|
-
const ext =
|
|
6401
|
+
const ext = path8.extname(filePath).toLowerCase();
|
|
5575
6402
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
5576
6403
|
try {
|
|
5577
|
-
const content =
|
|
6404
|
+
const content = fs8.readFileSync(filePath);
|
|
5578
6405
|
res.writeHead(200, { "Content-Type": contentType });
|
|
5579
6406
|
res.end(content);
|
|
5580
6407
|
} catch (err) {
|
|
@@ -5596,8 +6423,8 @@ function startUiServer(port, webDir) {
|
|
|
5596
6423
|
|
|
5597
6424
|
// src/server/apiServer.ts
|
|
5598
6425
|
import http3 from "node:http";
|
|
5599
|
-
import
|
|
5600
|
-
import
|
|
6426
|
+
import fs9 from "node:fs";
|
|
6427
|
+
import path9 from "node:path";
|
|
5601
6428
|
var MAX_JSON_BODY_BYTES = 10 * 1024 * 1024;
|
|
5602
6429
|
function readJsonBody(req) {
|
|
5603
6430
|
return new Promise((resolve, reject) => {
|
|
@@ -5739,7 +6566,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5739
6566
|
const numKey = String(t.meta.id);
|
|
5740
6567
|
const code = t.projectDescriptor?.code || t.meta.project || "Profe";
|
|
5741
6568
|
const prefixedKey = `${code}-${t.meta.id}`;
|
|
5742
|
-
const metaWithLinks = { ...t.meta, links: t.links || t.meta.links || [], isBlocked: t.isBlocked };
|
|
6569
|
+
const metaWithLinks = { ...t.meta, links: t.links || t.meta.links || [], isBlocked: t.isBlocked, lease: t.lease || t.meta.lease || null };
|
|
5743
6570
|
metasMap[prefixedKey] = metaWithLinks;
|
|
5744
6571
|
if (!metasMap[numKey] || code.toUpperCase() === "PROF") {
|
|
5745
6572
|
metasMap[numKey] = metaWithLinks;
|
|
@@ -5893,16 +6720,16 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5893
6720
|
return true;
|
|
5894
6721
|
}
|
|
5895
6722
|
const filePath = reqStorage.getAttachmentPath ? reqStorage.getAttachmentPath(ticketId, file) : null;
|
|
5896
|
-
if (!filePath || !
|
|
6723
|
+
if (!filePath || !fs9.existsSync(filePath)) {
|
|
5897
6724
|
sendJson(res, 404, { error: "Attachment not found." });
|
|
5898
6725
|
return true;
|
|
5899
6726
|
}
|
|
5900
|
-
const stat =
|
|
6727
|
+
const stat = fs9.statSync(filePath);
|
|
5901
6728
|
if (!stat.isFile()) {
|
|
5902
6729
|
sendJson(res, 404, { error: "Attachment not found." });
|
|
5903
6730
|
return true;
|
|
5904
6731
|
}
|
|
5905
|
-
const ext =
|
|
6732
|
+
const ext = path9.extname(filePath).toLowerCase();
|
|
5906
6733
|
const mimeTypes = {
|
|
5907
6734
|
".png": "image/png",
|
|
5908
6735
|
".jpg": "image/jpeg",
|
|
@@ -5920,7 +6747,7 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5920
6747
|
"Content-Length": stat.size,
|
|
5921
6748
|
"Cache-Control": "public, max-age=86400"
|
|
5922
6749
|
});
|
|
5923
|
-
|
|
6750
|
+
fs9.createReadStream(filePath).pipe(res);
|
|
5924
6751
|
return true;
|
|
5925
6752
|
}
|
|
5926
6753
|
if (pathname === "/api/planning/plan-history") {
|
|
@@ -5943,6 +6770,12 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
5943
6770
|
sendJson(res, 200, { milestones });
|
|
5944
6771
|
return true;
|
|
5945
6772
|
}
|
|
6773
|
+
if (pathname === "/api/planning/leases") {
|
|
6774
|
+
const project = url.searchParams.get("project") || void 0;
|
|
6775
|
+
const leases = await reqStorage.listLeases(project);
|
|
6776
|
+
sendJson(res, 200, { leases });
|
|
6777
|
+
return true;
|
|
6778
|
+
}
|
|
5946
6779
|
}
|
|
5947
6780
|
if (req.method === "POST") {
|
|
5948
6781
|
const body = await readJsonBody(req);
|
|
@@ -6175,6 +7008,61 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
6175
7008
|
sendJson(res, 200, { success: true, source: result.source, target: result.target });
|
|
6176
7009
|
return true;
|
|
6177
7010
|
}
|
|
7011
|
+
if (pathname === "/api/planning/leases/claim") {
|
|
7012
|
+
invalidateApiCache();
|
|
7013
|
+
const { ticketId, holder, agentType, hostname, sessionId, branch, worktree, ttlMinutes, force, project } = body;
|
|
7014
|
+
if (!ticketId) {
|
|
7015
|
+
sendJson(res, 400, { error: "ticketId is required." });
|
|
7016
|
+
return true;
|
|
7017
|
+
}
|
|
7018
|
+
const result = await reqStorage.claimTicket({
|
|
7019
|
+
ticketId,
|
|
7020
|
+
holder,
|
|
7021
|
+
agentType,
|
|
7022
|
+
hostname,
|
|
7023
|
+
sessionId,
|
|
7024
|
+
branch,
|
|
7025
|
+
worktree,
|
|
7026
|
+
ttlMinutes,
|
|
7027
|
+
force: Boolean(force),
|
|
7028
|
+
project
|
|
7029
|
+
});
|
|
7030
|
+
sendJson(res, 200, { success: true, ...result });
|
|
7031
|
+
return true;
|
|
7032
|
+
}
|
|
7033
|
+
if (pathname === "/api/planning/leases/renew") {
|
|
7034
|
+
invalidateApiCache();
|
|
7035
|
+
const { ticketId, holder, ttlMinutes, force, project } = body;
|
|
7036
|
+
if (!ticketId) {
|
|
7037
|
+
sendJson(res, 400, { error: "ticketId is required." });
|
|
7038
|
+
return true;
|
|
7039
|
+
}
|
|
7040
|
+
const lease = await reqStorage.renewLease({
|
|
7041
|
+
ticketId,
|
|
7042
|
+
holder,
|
|
7043
|
+
ttlMinutes,
|
|
7044
|
+
force: Boolean(force),
|
|
7045
|
+
project
|
|
7046
|
+
});
|
|
7047
|
+
sendJson(res, 200, { success: true, lease });
|
|
7048
|
+
return true;
|
|
7049
|
+
}
|
|
7050
|
+
if (pathname === "/api/planning/leases/release") {
|
|
7051
|
+
invalidateApiCache();
|
|
7052
|
+
const { ticketId, holder, force, project } = body;
|
|
7053
|
+
if (!ticketId) {
|
|
7054
|
+
sendJson(res, 400, { error: "ticketId is required." });
|
|
7055
|
+
return true;
|
|
7056
|
+
}
|
|
7057
|
+
const result = await reqStorage.releaseTicket({
|
|
7058
|
+
ticketId,
|
|
7059
|
+
holder,
|
|
7060
|
+
force: Boolean(force),
|
|
7061
|
+
project
|
|
7062
|
+
});
|
|
7063
|
+
sendJson(res, 200, { success: true, ...result });
|
|
7064
|
+
return true;
|
|
7065
|
+
}
|
|
6178
7066
|
}
|
|
6179
7067
|
sendJson(res, 404, { error: `Endpoint not found: ${pathname}` });
|
|
6180
7068
|
return true;
|
|
@@ -6183,6 +7071,10 @@ function createApiHandler(storage, workspaceRoot) {
|
|
|
6183
7071
|
sendJson(res, 403, { error: err.message });
|
|
6184
7072
|
return true;
|
|
6185
7073
|
}
|
|
7074
|
+
if (err.name === "EsedreLeaseConflictError") {
|
|
7075
|
+
sendJson(res, 409, { error: err.message, ticketKey: err.ticketKey, activeLease: err.activeLease });
|
|
7076
|
+
return true;
|
|
7077
|
+
}
|
|
6186
7078
|
console.error("[Esedre API Error]:", err);
|
|
6187
7079
|
sendJson(res, 500, { error: err.message });
|
|
6188
7080
|
return true;
|
|
@@ -6211,35 +7103,35 @@ function startApiServer(port, storage, workspaceRoot) {
|
|
|
6211
7103
|
|
|
6212
7104
|
// src/server/gateway.ts
|
|
6213
7105
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
6214
|
-
var __dirname2 =
|
|
7106
|
+
var __dirname2 = path10.dirname(__filename2);
|
|
6215
7107
|
function resolveWebDir(explicitWebDir, workspaceRoot) {
|
|
6216
|
-
if (explicitWebDir &&
|
|
7108
|
+
if (explicitWebDir && fs10.existsSync(path10.join(explicitWebDir, "index.html"))) {
|
|
6217
7109
|
return explicitWebDir;
|
|
6218
7110
|
}
|
|
6219
7111
|
const candidates = [
|
|
6220
7112
|
// 1. Packaged layout right next to executable (e.g. tools/web)
|
|
6221
|
-
|
|
7113
|
+
path10.resolve(__dirname2, "web"),
|
|
6222
7114
|
// 2. From workspace root: <workspaceRoot>/dist/web
|
|
6223
|
-
workspaceRoot ?
|
|
7115
|
+
workspaceRoot ? path10.resolve(workspaceRoot, "dist", "web") : "",
|
|
6224
7116
|
// 3. In-repo legacy subfolder: <workspaceRoot>/esedre/dist/web
|
|
6225
|
-
workspaceRoot ?
|
|
7117
|
+
workspaceRoot ? path10.resolve(workspaceRoot, "esedre", "dist", "web") : "",
|
|
6226
7118
|
// 4. Sibling standalone repository from workspace root: <workspaceRoot>/../esedre/dist/web
|
|
6227
|
-
workspaceRoot ?
|
|
7119
|
+
workspaceRoot ? path10.resolve(workspaceRoot, "..", "esedre", "dist", "web") : "",
|
|
6228
7120
|
// 5. Packaged layout inside dist/web
|
|
6229
|
-
|
|
7121
|
+
path10.resolve(__dirname2, "dist", "web"),
|
|
6230
7122
|
// 6. Sibling standalone repository from __dirname: <__dirname>/../../esedre/dist/web
|
|
6231
|
-
|
|
7123
|
+
path10.resolve(__dirname2, "..", "..", "esedre", "dist", "web"),
|
|
6232
7124
|
// 7. Source tree layout: esedre/src/server/ -> esedre/dist/web
|
|
6233
|
-
|
|
7125
|
+
path10.resolve(__dirname2, "..", "..", "dist", "web"),
|
|
6234
7126
|
// 8. Legacy layout from __dirname
|
|
6235
|
-
|
|
7127
|
+
path10.resolve(__dirname2, "..", "esedre", "dist", "web")
|
|
6236
7128
|
].filter(Boolean);
|
|
6237
7129
|
for (const candidate of candidates) {
|
|
6238
|
-
if (
|
|
7130
|
+
if (fs10.existsSync(path10.join(candidate, "index.html"))) {
|
|
6239
7131
|
return candidate;
|
|
6240
7132
|
}
|
|
6241
7133
|
}
|
|
6242
|
-
return candidates[0] ||
|
|
7134
|
+
return candidates[0] || path10.resolve(__dirname2, "web");
|
|
6243
7135
|
}
|
|
6244
7136
|
function proxyRequest(req, res, targetPort, rewritePath) {
|
|
6245
7137
|
const targetPath = rewritePath ? rewritePath(req.url || "/") : req.url || "/";
|
|
@@ -6350,8 +7242,8 @@ ${colors.bold}ROADMAP COMMANDS (Pair Programming & LLM Agents):${colors.reset}
|
|
|
6350
7242
|
\u2022 Status values: 'Planned', 'In Development', 'Completed', 'Rejected'
|
|
6351
7243
|
\u2022 Type values: 'Feature', 'Platform', 'Tools', 'Idea', 'Bug'
|
|
6352
7244
|
|
|
6353
|
-
${colors.bold}get${colors.reset} <id
|
|
6354
|
-
Inspect full ticket specification, feature breakdown, and comments.
|
|
7245
|
+
${colors.bold}get${colors.reset} <id...> [--json]
|
|
7246
|
+
Inspect full ticket specification, feature breakdown, and comments for one or more tickets.
|
|
6355
7247
|
|
|
6356
7248
|
${colors.bold}plan${colors.reset} <id> [--file <path> | --set "<markdown>"] [--last-hash <sha1>] [--json]
|
|
6357
7249
|
View or update implementation plan with optimistic concurrency control.
|
|
@@ -6362,7 +7254,7 @@ ${colors.bold}ROADMAP COMMANDS (Pair Programming & LLM Agents):${colors.reset}
|
|
|
6362
7254
|
${colors.bold}update${colors.reset} <id> [-s|--status <status>] [-t|--type <type>] [-P|--priority <prio>] [--title "..."] [--complexity <c>] [--effort "<e>"] [--detail "<md>"] [--file <path>] [--in-dev] [--flag <name>] [--last-hash <sha1>] [--force] [--json]
|
|
6363
7255
|
Mutate ticket status, type, priority, title, complexity, effort, active state, or feature flag with OCC protection.
|
|
6364
7256
|
|
|
6365
|
-
${colors.bold}comment${colors.reset} <id> ["<text>"] [--text "..."] [--author "..."] [--json]
|
|
7257
|
+
${colors.bold}comment${colors.reset} <id> ["<text>"] [--file <path>] [--text "..."] [--author "..."] [--json]
|
|
6366
7258
|
Append a research finding, test verification, or note to ticket history.
|
|
6367
7259
|
|
|
6368
7260
|
${colors.bold}link${colors.reset} <sourceId> <relation> <targetId> [--author "..."] [--json]
|
|
@@ -6372,6 +7264,9 @@ ${colors.bold}ROADMAP COMMANDS (Pair Programming & LLM Agents):${colors.reset}
|
|
|
6372
7264
|
${colors.bold}unlink${colors.reset} <sourceId> <targetId> [--json]
|
|
6373
7265
|
Remove a link between two tickets and its reciprocal link.
|
|
6374
7266
|
|
|
7267
|
+
${colors.bold}lease${colors.reset} [claim <id> | release <id> | renew <id> | list] [--by|--worker-id <holder>] [--ttl <mins>] [--branch <b>] [--status <stat>] [--notes "..."] [--force] [--json]
|
|
7268
|
+
Coordinate multi-agent / multi-machine ticket ownership via TTL-backed advisory leases.
|
|
7269
|
+
|
|
6375
7270
|
${colors.bold}snapshot, refresh${colors.reset} [--project <code>] [--json]
|
|
6376
7271
|
Generate lean read-only projection snapshot (.esedre/snapshot.json) for zero-latency agent context.
|
|
6377
7272
|
|
|
@@ -6439,7 +7334,7 @@ ${colors.bold}OPTIONS:${colors.reset}
|
|
|
6439
7334
|
--blocked Filter tickets that are blocked by uncompleted tickets.
|
|
6440
7335
|
--linked-to <key> Filter tickets that are linked to a specific ticket.
|
|
6441
7336
|
--detail "<md>" Specification markdown for ticket detail during create or update.
|
|
6442
|
-
--file <path> Path to markdown file for detail (create, update)
|
|
7337
|
+
--file <path> Path to markdown file for detail (create, update), implementation plan (plan), or comment (comment).
|
|
6443
7338
|
--json Output raw machine-readable JSON (strongly recommended for autonomous LLM coding agents).
|
|
6444
7339
|
--last-hash <hash> Optimistic concurrency control: last known sha1 hash of the ticket from 'get'.
|
|
6445
7340
|
--force Bypass optimistic concurrency last-hash conflict checks on writes.
|
|
@@ -6512,7 +7407,7 @@ function printDuplicateProjectWarnings(storage, isJson) {
|
|
|
6512
7407
|
const warnings = storage.getDuplicateProjectWarnings ? storage.getDuplicateProjectWarnings() : [];
|
|
6513
7408
|
for (const warn of warnings) {
|
|
6514
7409
|
console.error(
|
|
6515
|
-
`${colors.yellow}\u26A0\uFE0F Warning: Duplicate project code '${warn.code}' detected across multiple data hubs (${
|
|
7410
|
+
`${colors.yellow}\u26A0\uFE0F Warning: Duplicate project code '${warn.code}' detected across multiple data hubs (${path11.basename(warn.firstHub)} and ${path11.basename(warn.duplicateHub)}). Duplicate project codes across hubs are not supported; only '${path11.basename(warn.firstHub)}' is active.${colors.reset}`
|
|
6516
7411
|
);
|
|
6517
7412
|
}
|
|
6518
7413
|
}
|
|
@@ -6531,6 +7426,7 @@ async function main() {
|
|
|
6531
7426
|
}
|
|
6532
7427
|
const rawArgs = process.argv.slice(2);
|
|
6533
7428
|
const { command, positionals, flags } = parseArgs(rawArgs);
|
|
7429
|
+
initLogger({ debug: Boolean(flags["debug"]) });
|
|
6534
7430
|
try {
|
|
6535
7431
|
writeGlobalVersion();
|
|
6536
7432
|
} catch {
|
|
@@ -6555,7 +7451,7 @@ async function main() {
|
|
|
6555
7451
|
switch (command) {
|
|
6556
7452
|
case "init": {
|
|
6557
7453
|
const rawTarget = positionals[0];
|
|
6558
|
-
const targetDir = rawTarget ?
|
|
7454
|
+
const targetDir = rawTarget ? path11.resolve(rawTarget) : discovered.workspaceRoot;
|
|
6559
7455
|
const rawHubFlag = flags["hub"];
|
|
6560
7456
|
const hubArg = typeof rawHubFlag === "string" ? rawHubFlag : void 0;
|
|
6561
7457
|
const isHubRequested = Boolean(rawHubFlag);
|
|
@@ -6634,7 +7530,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
6634
7530
|
console.error(`${colors.red}Error: ${nameVal.error}${colors.reset}`);
|
|
6635
7531
|
process.exit(1);
|
|
6636
7532
|
}
|
|
6637
|
-
const isTargetHub =
|
|
7533
|
+
const isTargetHub = fs11.existsSync(path11.join(targetDir, "projects.json")) || fs11.existsSync(path11.join(targetDir, "projects")) && !fs11.existsSync(path11.join(targetDir, ".esedre", "esedre.json"));
|
|
6638
7534
|
const isWorkspaceless = isHubRequested || isTargetHub || Boolean(discovered.config?.projectCode) && !rawTarget && discovered.config?.projectCode?.toLowerCase() !== projectCode.toLowerCase();
|
|
6639
7535
|
let projectNewlyRegistered = false;
|
|
6640
7536
|
let registeredProj;
|
|
@@ -6705,7 +7601,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
6705
7601
|
const viteConfigPath = findViteConfig(targetDir);
|
|
6706
7602
|
if (viteConfigPath && setupProxy === void 0 && !isJson) {
|
|
6707
7603
|
try {
|
|
6708
|
-
const rawContent =
|
|
7604
|
+
const rawContent = fs11.readFileSync(viteConfigPath, "utf-8");
|
|
6709
7605
|
const alreadyConfigured = rawContent.includes("'/esedre'") || rawContent.includes('"/esedre"');
|
|
6710
7606
|
if (!alreadyConfigured) {
|
|
6711
7607
|
if (isYes) {
|
|
@@ -6715,7 +7611,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
6715
7611
|
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
6716
7612
|
try {
|
|
6717
7613
|
const answer = await rl.question(
|
|
6718
|
-
`${colors.cyan}?${colors.reset} Detected ${colors.bold}${
|
|
7614
|
+
`${colors.cyan}?${colors.reset} Detected ${colors.bold}${path11.basename(viteConfigPath)}${colors.reset}. Would you like to configure a reverse proxy for ${colors.bold}/esedre${colors.reset} targeting http://127.0.0.1:${port}? [Y/n] `
|
|
6719
7615
|
);
|
|
6720
7616
|
const trimmed = answer.trim().toLowerCase();
|
|
6721
7617
|
setupProxy = trimmed === "" || trimmed === "y" || trimmed === "yes";
|
|
@@ -6731,10 +7627,10 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
6731
7627
|
let hubRelPath = void 0;
|
|
6732
7628
|
if (rawHub) {
|
|
6733
7629
|
const singleHub = Array.isArray(rawHub) ? rawHub[0] : rawHub;
|
|
6734
|
-
const resolvedHub =
|
|
6735
|
-
if (
|
|
6736
|
-
const rel =
|
|
6737
|
-
const isSubdir = !rel.startsWith("..") && !
|
|
7630
|
+
const resolvedHub = path11.resolve(expandHome(singleHub));
|
|
7631
|
+
if (fs11.existsSync(resolvedHub)) {
|
|
7632
|
+
const rel = path11.relative(targetDir, resolvedHub).replace(/\\/g, "/");
|
|
7633
|
+
const isSubdir = !rel.startsWith("..") && !path11.isAbsolute(rel);
|
|
6738
7634
|
if (isSubdir || Boolean(flags["data-dir"])) {
|
|
6739
7635
|
hubRelPath = rel.startsWith(".") ? rel : "./" + rel;
|
|
6740
7636
|
}
|
|
@@ -6790,9 +7686,14 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
6790
7686
|
console.log(` \u2022 In-repo wrappers: .esedre/esedre.cmd, .esedre/ese.cmd, .esedre/esedre, .esedre/ese`);
|
|
6791
7687
|
console.log(` \u2022 .gitignore: ${res.gitignoreUpdated ? "Added .esedre/snapshot.json" : "Already configured"}`);
|
|
6792
7688
|
console.log(` \u2022 Snapshot: Generated .esedre/snapshot.json (${snapshotTickets} tickets projected)`);
|
|
6793
|
-
console.log(` \u2022 MCP: ${res.mcpConfigured ? "Configured in .agents/mcp_config.json" : "Skipped"}`);
|
|
7689
|
+
console.log(` \u2022 MCP: ${res.mcpConfigured ? "Configured in .agents/mcp_config.json and .agents/mcp.json" : "Skipped"}`);
|
|
6794
7690
|
console.log(` \u2022 Skill: ${res.skillConfigured ? "Updated .agents/skills/esedre/SKILL.md" : `Retained (${res.skillStatus})`}`);
|
|
6795
7691
|
console.log(` \u2022 Central Config: Linked into ~/.esedre/config.json`);
|
|
7692
|
+
if (res.ruleSizeWarnings && res.ruleSizeWarnings.length > 0) {
|
|
7693
|
+
for (const rw of res.ruleSizeWarnings) {
|
|
7694
|
+
console.log(` \u2022 ${colors.yellow}\u26A0\uFE0F Rule Size Warning: ${rw.message}${colors.reset}`);
|
|
7695
|
+
}
|
|
7696
|
+
}
|
|
6796
7697
|
if (res.viteProxyStatus === "CONFIGURED") {
|
|
6797
7698
|
console.log(` \u2022 ${colors.green}Vite Proxy: Added /esedre proxy to ${res.viteConfigFile} targeting http://127.0.0.1:${effectivePort}${colors.reset}`);
|
|
6798
7699
|
} else if (res.viteProxyStatus === "ALREADY_CONFIGURED") {
|
|
@@ -6908,7 +7809,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
|
|
|
6908
7809
|
}
|
|
6909
7810
|
console.log(`
|
|
6910
7811
|
${colors.bold}${colors.cyan}Esedre Central Configuration${colors.reset}`);
|
|
6911
|
-
console.log(` \u2022 Config File: ${configPath} ${
|
|
7812
|
+
console.log(` \u2022 Config File: ${configPath} ${fs11.existsSync(configPath) ? colors.green + "(active)" + colors.reset : colors.dim + "(default)" + colors.reset}`);
|
|
6912
7813
|
console.log(` \u2022 Default Port: ${globalCfg.port || DEFAULT_ESEDRE_PORT}`);
|
|
6913
7814
|
console.log(`
|
|
6914
7815
|
${colors.bold}Data Hubs (dataDir):${colors.reset}`);
|
|
@@ -6974,11 +7875,16 @@ ${colors.bold}${colors.green}\u2714 Esedre multi-workspace upgrade completed acr
|
|
|
6974
7875
|
} else if (w.upgrade.skillStatus === "CUSTOMIZED") {
|
|
6975
7876
|
console.log(` - Preserved customized .agents/skills/esedre/SKILL.md`);
|
|
6976
7877
|
}
|
|
7878
|
+
if (w.upgrade.ruleSizeWarnings && w.upgrade.ruleSizeWarnings.length > 0) {
|
|
7879
|
+
for (const rw of w.upgrade.ruleSizeWarnings) {
|
|
7880
|
+
console.log(` - ${colors.yellow}\u26A0\uFE0F ${rw.message}${colors.reset}`);
|
|
7881
|
+
}
|
|
7882
|
+
}
|
|
6977
7883
|
}
|
|
6978
7884
|
}
|
|
6979
7885
|
return;
|
|
6980
7886
|
}
|
|
6981
|
-
const targetDir = positionals[0] ?
|
|
7887
|
+
const targetDir = positionals[0] ? path11.resolve(positionals[0]) : discovered.workspaceRoot;
|
|
6982
7888
|
const targetDiscovered = targetDir === discovered.workspaceRoot ? discovered : findEsedreConfig(targetDir, { fallbackToGlobal: true });
|
|
6983
7889
|
const res = configureWorkspace(targetDir, {
|
|
6984
7890
|
projectCode: targetDiscovered.config?.projectCode,
|
|
@@ -7005,6 +7911,11 @@ ${colors.bold}${colors.green}\u2714 Esedre multi-workspace upgrade completed acr
|
|
|
7005
7911
|
} else if (res.skillStatus === "CUSTOMIZED") {
|
|
7006
7912
|
console.log(` \u2022 ${colors.yellow}Notice: .agents/skills/esedre/SKILL.md is customized; preserved user modifications (use --force to overwrite).${colors.reset}`);
|
|
7007
7913
|
}
|
|
7914
|
+
if (res.ruleSizeWarnings && res.ruleSizeWarnings.length > 0) {
|
|
7915
|
+
for (const rw of res.ruleSizeWarnings) {
|
|
7916
|
+
console.log(` \u2022 ${colors.yellow}\u26A0\uFE0F ${rw.message}${colors.reset}`);
|
|
7917
|
+
}
|
|
7918
|
+
}
|
|
7008
7919
|
}
|
|
7009
7920
|
return;
|
|
7010
7921
|
}
|
|
@@ -7107,7 +8018,7 @@ ${colors.yellow}\u26A0\uFE0F Warning: Daemon is running v${status.version}, but
|
|
|
7107
8018
|
}
|
|
7108
8019
|
} else {
|
|
7109
8020
|
console.log(`${colors.yellow}\u25CB Esedre daemon is STOPPED (port ${status.port})${colors.reset}`);
|
|
7110
|
-
if (status.logFile &&
|
|
8021
|
+
if (status.logFile && fs11.existsSync(status.logFile)) {
|
|
7111
8022
|
console.log(` \u2022 Recent Logs: ${status.logFile}`);
|
|
7112
8023
|
}
|
|
7113
8024
|
}
|
|
@@ -7238,7 +8149,9 @@ ${colors.yellow}Notice: ${outdatedWrappers.length} workspace(s) have legacy CMD
|
|
|
7238
8149
|
const milestone = flags["milestone"] || flags["m"] || void 0;
|
|
7239
8150
|
const isBlocked = flags["blocked"] ? true : void 0;
|
|
7240
8151
|
const linkedTo = flags["linked-to"] || flags["linkedTo"] || void 0;
|
|
7241
|
-
const
|
|
8152
|
+
const leased = flags["leased"] !== void 0 ? Boolean(flags["leased"]) : void 0;
|
|
8153
|
+
const leasedBy = flags["leased-by"] || flags["leasedBy"] || void 0;
|
|
8154
|
+
const tickets = await storage.listTickets({ project, status, type, category, priority, milestone, search, isBlocked, linkedTo, leased, leasedBy });
|
|
7242
8155
|
if (isJson) {
|
|
7243
8156
|
console.log(JSON.stringify(tickets.map((t) => ({
|
|
7244
8157
|
id: t.meta.id,
|
|
@@ -7255,7 +8168,8 @@ ${colors.yellow}Notice: ${outdatedWrappers.length} workspace(s) have legacy CMD
|
|
|
7255
8168
|
project: t.projectDescriptor?.code || t.meta.project || "UNASSIGNED",
|
|
7256
8169
|
sha1: t.sha1 || t.meta.sha1,
|
|
7257
8170
|
isBlocked: t.isBlocked,
|
|
7258
|
-
links: t.links
|
|
8171
|
+
links: t.links,
|
|
8172
|
+
lease: t.lease || null
|
|
7259
8173
|
})), null, 2));
|
|
7260
8174
|
} else {
|
|
7261
8175
|
if (tickets.length === 0) {
|
|
@@ -7278,22 +8192,30 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
7278
8192
|
return;
|
|
7279
8193
|
}
|
|
7280
8194
|
case "get": {
|
|
7281
|
-
|
|
7282
|
-
if (!idStr) {
|
|
8195
|
+
if (positionals.length === 0) {
|
|
7283
8196
|
console.error(`${colors.red}Error: Ticket ID is required (e.g. ese get 1 or ese get Profe-1)${colors.reset}`);
|
|
7284
8197
|
process.exit(1);
|
|
7285
8198
|
}
|
|
7286
8199
|
const projectFlag = flags["project"];
|
|
7287
|
-
const
|
|
7288
|
-
const
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
8200
|
+
const tickets = [];
|
|
8201
|
+
for (const idStr of positionals) {
|
|
8202
|
+
const lookupKey = projectFlag && /^\d+$/.test(idStr) ? `${projectFlag}-${idStr}` : idStr;
|
|
8203
|
+
const ticket = await storage.getTicket(lookupKey);
|
|
8204
|
+
if (!ticket) {
|
|
8205
|
+
console.error(`${colors.red}Error: Ticket #${lookupKey} not found.${colors.reset}`);
|
|
8206
|
+
process.exit(1);
|
|
8207
|
+
}
|
|
8208
|
+
tickets.push(ticket);
|
|
7292
8209
|
}
|
|
7293
8210
|
if (isJson) {
|
|
7294
|
-
console.log(JSON.stringify(
|
|
8211
|
+
console.log(JSON.stringify(tickets.length === 1 ? tickets[0] : tickets, null, 2));
|
|
7295
8212
|
} else {
|
|
7296
|
-
console.log(
|
|
8213
|
+
console.log(
|
|
8214
|
+
tickets.map((t) => formatTicketDetail(t)).join(`
|
|
8215
|
+
${colors.dim}${"\u2500".repeat(60)}${colors.reset}
|
|
8216
|
+
|
|
8217
|
+
`)
|
|
8218
|
+
);
|
|
7297
8219
|
}
|
|
7298
8220
|
return;
|
|
7299
8221
|
}
|
|
@@ -7312,11 +8234,11 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
7312
8234
|
if (setPlan || filePath) {
|
|
7313
8235
|
let content = setPlan;
|
|
7314
8236
|
if (filePath) {
|
|
7315
|
-
if (!
|
|
8237
|
+
if (!fs11.existsSync(filePath)) {
|
|
7316
8238
|
console.error(`${colors.red}Error: Plan file "${filePath}" not found.${colors.reset}`);
|
|
7317
8239
|
process.exit(1);
|
|
7318
8240
|
}
|
|
7319
|
-
content =
|
|
8241
|
+
content = fs11.readFileSync(filePath, "utf-8");
|
|
7320
8242
|
}
|
|
7321
8243
|
await storage.savePlan(id, content, lastHash);
|
|
7322
8244
|
if (isJson) {
|
|
@@ -7388,8 +8310,8 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
7388
8310
|
const milestone = flags["milestone"] || flags["m"] || void 0;
|
|
7389
8311
|
let detailMarkdown = detailArg;
|
|
7390
8312
|
if (fileArg) {
|
|
7391
|
-
const filePath =
|
|
7392
|
-
if (!
|
|
8313
|
+
const filePath = path11.resolve(fileArg);
|
|
8314
|
+
if (!fs11.existsSync(filePath)) {
|
|
7393
8315
|
if (isJson) {
|
|
7394
8316
|
console.error(JSON.stringify({ error: `Detail file "${fileArg}" not found` }));
|
|
7395
8317
|
} else {
|
|
@@ -7397,7 +8319,7 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
7397
8319
|
}
|
|
7398
8320
|
process.exit(1);
|
|
7399
8321
|
}
|
|
7400
|
-
detailMarkdown =
|
|
8322
|
+
detailMarkdown = fs11.readFileSync(filePath, "utf-8");
|
|
7401
8323
|
}
|
|
7402
8324
|
const created = await storage.createTicket({
|
|
7403
8325
|
title,
|
|
@@ -7467,8 +8389,8 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
7467
8389
|
const fileArg = flags["file"];
|
|
7468
8390
|
let detailMarkdown = detailArg ? normalizeDashesAndMojibake(detailArg) : void 0;
|
|
7469
8391
|
if (fileArg) {
|
|
7470
|
-
const filePath =
|
|
7471
|
-
if (!
|
|
8392
|
+
const filePath = path11.resolve(fileArg);
|
|
8393
|
+
if (!fs11.existsSync(filePath)) {
|
|
7472
8394
|
if (isJson) {
|
|
7473
8395
|
console.error(JSON.stringify({ error: `Detail file "${fileArg}" not found` }));
|
|
7474
8396
|
} else {
|
|
@@ -7476,7 +8398,7 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
|
|
|
7476
8398
|
}
|
|
7477
8399
|
process.exit(1);
|
|
7478
8400
|
}
|
|
7479
|
-
detailMarkdown = normalizeDashesAndMojibake(
|
|
8401
|
+
detailMarkdown = normalizeDashesAndMojibake(fs11.readFileSync(filePath, "utf-8"));
|
|
7480
8402
|
}
|
|
7481
8403
|
if (detailMarkdown !== void 0) updates.detailMarkdown = detailMarkdown;
|
|
7482
8404
|
const updated = await storage.updateTicket(lookupKey, updates, lastHash);
|
|
@@ -7634,10 +8556,30 @@ ${colors.bold}Tickets:${colors.reset}`);
|
|
|
7634
8556
|
}
|
|
7635
8557
|
const projectFlag = flags["project"];
|
|
7636
8558
|
const lookupKey = projectFlag && /^\d+$/.test(idStr) ? `${projectFlag}-${idStr}` : idStr;
|
|
7637
|
-
|
|
7638
|
-
const
|
|
8559
|
+
let text;
|
|
8560
|
+
const filePath = flags["file"];
|
|
8561
|
+
if (filePath) {
|
|
8562
|
+
const resolvedPath = path11.resolve(process.cwd(), filePath);
|
|
8563
|
+
if (!fs11.existsSync(resolvedPath)) {
|
|
8564
|
+
console.error(`${colors.red}Error: File not found: ${filePath}${colors.reset}`);
|
|
8565
|
+
process.exit(1);
|
|
8566
|
+
}
|
|
8567
|
+
if (!fs11.statSync(resolvedPath).isFile()) {
|
|
8568
|
+
console.error(`${colors.red}Error: Path is not a file: ${filePath}${colors.reset}`);
|
|
8569
|
+
process.exit(1);
|
|
8570
|
+
}
|
|
8571
|
+
const fileContent = fs11.readFileSync(resolvedPath, "utf-8");
|
|
8572
|
+
if (!fileContent.trim()) {
|
|
8573
|
+
console.error(`${colors.red}Error: Comment file is empty: ${filePath}${colors.reset}`);
|
|
8574
|
+
process.exit(1);
|
|
8575
|
+
}
|
|
8576
|
+
text = fileContent;
|
|
8577
|
+
} else {
|
|
8578
|
+
const positionalText = positionals.slice(1).join(" ").trim();
|
|
8579
|
+
text = flags["text"] || (positionalText || void 0);
|
|
8580
|
+
}
|
|
7639
8581
|
if (!text) {
|
|
7640
|
-
console.error(`${colors.red}Error: Comment text is required (e.g. ese comment 96
|
|
8582
|
+
console.error(`${colors.red}Error: Comment text or --file is required (e.g. ese comment 96 --file note.md or ese comment 96 "Verified tests")${colors.reset}`);
|
|
7641
8583
|
process.exit(1);
|
|
7642
8584
|
}
|
|
7643
8585
|
const author = flags["author"] || "User";
|
|
@@ -7701,6 +8643,129 @@ ${colors.bold}Tickets:${colors.reset}`);
|
|
|
7701
8643
|
}
|
|
7702
8644
|
return;
|
|
7703
8645
|
}
|
|
8646
|
+
case "claim":
|
|
8647
|
+
case "release":
|
|
8648
|
+
case "renew":
|
|
8649
|
+
case "leases":
|
|
8650
|
+
case "lease": {
|
|
8651
|
+
const isTopLevelAlias = command === "claim" || command === "release" || command === "renew" || command === "leases";
|
|
8652
|
+
const subAction = isTopLevelAlias ? command === "leases" ? "list" : command : positionals[0]?.toLowerCase() || "list";
|
|
8653
|
+
const targetArg = isTopLevelAlias ? positionals[0] : positionals[1];
|
|
8654
|
+
const project = flags["project"] || discovered.config?.projectCode;
|
|
8655
|
+
const holder = flags["by"] || flags["holder"] || flags["worker-id"] || flags["workerId"] || flags["author"] || void 0;
|
|
8656
|
+
const agentType = flags["agent"] || flags["agent-type"] || flags["system"] || void 0;
|
|
8657
|
+
const sessionId = flags["session"] || flags["session-id"] || void 0;
|
|
8658
|
+
const branch = flags["branch"] || void 0;
|
|
8659
|
+
const ttlRaw = flags["ttl"] !== void 0 ? Number(flags["ttl"]) : void 0;
|
|
8660
|
+
const isForce = Boolean(flags["force"] || flags["f"]);
|
|
8661
|
+
if (subAction === "list" || subAction === "ls") {
|
|
8662
|
+
const filterProject = flags["all"] || flags["a"] || flags["project"] === "all" ? "all" : project;
|
|
8663
|
+
const leases = await storage.listLeases(filterProject);
|
|
8664
|
+
if (isJson) {
|
|
8665
|
+
console.log(JSON.stringify({ total: leases.length, leases }, null, 2));
|
|
8666
|
+
} else {
|
|
8667
|
+
console.log(formatLeaseListTable(leases));
|
|
8668
|
+
if (leases.length > 0) {
|
|
8669
|
+
console.log(`
|
|
8670
|
+
${colors.dim}Total: ${leases.length} active lease(s)${colors.reset}`);
|
|
8671
|
+
}
|
|
8672
|
+
}
|
|
8673
|
+
return;
|
|
8674
|
+
}
|
|
8675
|
+
if (subAction === "claim") {
|
|
8676
|
+
if (!targetArg) {
|
|
8677
|
+
console.error(`${colors.red}Error: Ticket ID is required (e.g. ese lease claim 69 --by "agent-1")${colors.reset}`);
|
|
8678
|
+
process.exit(1);
|
|
8679
|
+
}
|
|
8680
|
+
const identity = resolveWorkerIdentity({
|
|
8681
|
+
holder,
|
|
8682
|
+
agentType,
|
|
8683
|
+
sessionId,
|
|
8684
|
+
branch,
|
|
8685
|
+
worktree: discovered.workspaceRoot,
|
|
8686
|
+
configDefaultAuthor: discovered.config?.defaultAuthor
|
|
8687
|
+
});
|
|
8688
|
+
const res = await storage.claimTicket({
|
|
8689
|
+
ticketId: targetArg,
|
|
8690
|
+
holder: identity.holder,
|
|
8691
|
+
agentType: identity.agentType,
|
|
8692
|
+
hostname: identity.hostname,
|
|
8693
|
+
sessionId: identity.sessionId,
|
|
8694
|
+
branch: identity.branch,
|
|
8695
|
+
worktree: identity.worktree,
|
|
8696
|
+
ttlMinutes: ttlRaw,
|
|
8697
|
+
force: isForce,
|
|
8698
|
+
project
|
|
8699
|
+
});
|
|
8700
|
+
if (isJson) {
|
|
8701
|
+
console.log(JSON.stringify(res, null, 2));
|
|
8702
|
+
} else {
|
|
8703
|
+
const actionLabel = res.overridden ? "Overrode & claimed" : res.renewed ? "Renewed lease on" : "Claimed lease on";
|
|
8704
|
+
console.log(
|
|
8705
|
+
`${colors.green}\u2714 ${actionLabel} Ticket ${res.lease.ticketKey}${colors.reset} for ${colors.bold}${res.lease.holder}${colors.reset}${res.lease.hostname ? ` on ${res.lease.hostname}` : ""} (TTL: ${res.lease.ttlMinutes}m, expires ${res.lease.expiresAt})`
|
|
8706
|
+
);
|
|
8707
|
+
}
|
|
8708
|
+
return;
|
|
8709
|
+
}
|
|
8710
|
+
if (subAction === "renew" || subAction === "heartbeat") {
|
|
8711
|
+
if (!targetArg) {
|
|
8712
|
+
console.error(`${colors.red}Error: Ticket ID is required (e.g. ese lease renew 69)${colors.reset}`);
|
|
8713
|
+
process.exit(1);
|
|
8714
|
+
}
|
|
8715
|
+
const lease = await storage.renewLease({
|
|
8716
|
+
ticketId: targetArg,
|
|
8717
|
+
holder,
|
|
8718
|
+
ttlMinutes: ttlRaw,
|
|
8719
|
+
force: isForce,
|
|
8720
|
+
project
|
|
8721
|
+
});
|
|
8722
|
+
if (isJson) {
|
|
8723
|
+
console.log(JSON.stringify({ success: true, lease }, null, 2));
|
|
8724
|
+
} else {
|
|
8725
|
+
console.log(
|
|
8726
|
+
`${colors.green}\u2714 Renewed lease on Ticket ${lease.ticketKey}${colors.reset} for ${colors.bold}${lease.holder}${colors.reset} (expires ${lease.expiresAt})`
|
|
8727
|
+
);
|
|
8728
|
+
}
|
|
8729
|
+
return;
|
|
8730
|
+
}
|
|
8731
|
+
if (subAction === "release" || subAction === "unlock") {
|
|
8732
|
+
if (!targetArg) {
|
|
8733
|
+
console.error(`${colors.red}Error: Ticket ID is required (e.g. ese lease release 69)${colors.reset}`);
|
|
8734
|
+
process.exit(1);
|
|
8735
|
+
}
|
|
8736
|
+
const finalStatus = flags["status"] || void 0;
|
|
8737
|
+
const notes = flags["notes"] || void 0;
|
|
8738
|
+
const res = await storage.releaseTicket({
|
|
8739
|
+
ticketId: targetArg,
|
|
8740
|
+
holder,
|
|
8741
|
+
force: isForce,
|
|
8742
|
+
project
|
|
8743
|
+
});
|
|
8744
|
+
if (finalStatus) {
|
|
8745
|
+
await storage.updateTicket(res.ticketKey || targetArg, { status: finalStatus });
|
|
8746
|
+
}
|
|
8747
|
+
if (notes) {
|
|
8748
|
+
await storage.addComment(res.ticketKey || targetArg, {
|
|
8749
|
+
author: holder || res.previousLease?.holder || discovered.config?.defaultAuthor || "Agent",
|
|
8750
|
+
text: notes
|
|
8751
|
+
});
|
|
8752
|
+
}
|
|
8753
|
+
if (isJson) {
|
|
8754
|
+
console.log(JSON.stringify(res, null, 2));
|
|
8755
|
+
} else {
|
|
8756
|
+
if (res.released) {
|
|
8757
|
+
console.log(
|
|
8758
|
+
`${colors.green}\u2714 Released lease on Ticket ${res.ticketKey}${colors.reset}${res.previousLease ? ` (previously held by ${res.previousLease.holder})` : ""}${finalStatus ? ` -> ${finalStatus}` : ""}`
|
|
8759
|
+
);
|
|
8760
|
+
} else {
|
|
8761
|
+
console.log(`${colors.dim}Ticket ${res.ticketKey} had no active lease.${colors.reset}`);
|
|
8762
|
+
}
|
|
8763
|
+
}
|
|
8764
|
+
return;
|
|
8765
|
+
}
|
|
8766
|
+
console.error(`${colors.red}Error: Unknown lease action "${subAction}". Valid actions: claim, renew, release, list${colors.reset}`);
|
|
8767
|
+
process.exit(1);
|
|
8768
|
+
}
|
|
7704
8769
|
default: {
|
|
7705
8770
|
console.error(`${colors.red}Error: Unknown command "${command}"${colors.reset}`);
|
|
7706
8771
|
printHelp();
|
|
@@ -7713,6 +8778,14 @@ ${colors.bold}Tickets:${colors.reset}`);
|
|
|
7713
8778
|
console.error(`${colors.dim}Use --force to override if you are certain.${colors.reset}`);
|
|
7714
8779
|
process.exit(1);
|
|
7715
8780
|
}
|
|
8781
|
+
if (err instanceof EsedreLeaseConflictError || err.name === "EsedreLeaseConflictError") {
|
|
8782
|
+
if (isJson) {
|
|
8783
|
+
console.error(JSON.stringify({ error: err.message, ticketKey: err.ticketKey, activeLease: err.activeLease }, null, 2));
|
|
8784
|
+
} else {
|
|
8785
|
+
console.error(`${colors.red}${err.message}${colors.reset}`);
|
|
8786
|
+
}
|
|
8787
|
+
process.exit(1);
|
|
8788
|
+
}
|
|
7716
8789
|
if (err instanceof EsedreAuthorizationError || err.name === "EsedreAuthorizationError") {
|
|
7717
8790
|
console.error(`${colors.red}Access Denied: ${err.message}${colors.reset}`);
|
|
7718
8791
|
process.exit(1);
|