esedre 1.1.0 → 1.2.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 CHANGED
@@ -5,6 +5,20 @@ All notable changes to Esedre are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.0] - 2026-10-09
9
+
10
+ ### Added
11
+ - **MCP Ticket Listing Implementation Plan Hydration (`includePlan` and `hasPlan`) (Ticket #58)**:
12
+ - Extended `esedre_list_tickets` MCP tool to return `hasPlan: boolean` on all listed tickets, indicating whether an implementation plan exists on disk.
13
+ - Added optional `includePlan: boolean` parameter to `esedre_list_tickets` to hydrate full `planMarkdown` directly in ticket listings, eliminating redundant sequential `esedre_get_plan` tool calls when agents review multiple active tickets.
14
+ - Added unit test suite in `tests/mcpServer.test.ts` verifying `hasPlan` and `includePlan` behavior across multiple tickets.
15
+ - **Comment File Input Option for CLI & MCP Tool (Ticket #62)**:
16
+ - Added `--file <path>` option to `ese comment` CLI command, reading verification notes directly from UTF-8 markdown or text files on disk.
17
+ - Bypasses Windows `cmd.exe` `%VAR%` parameter expansion and stripping, preserving percent signs (`100%`) and literal characters verbatim.
18
+ - Added priority precedence for `--file` over inline `--text` and positional arguments, with existence, regular file, and non-empty content validation.
19
+ - Added parity `file?: string` property to `esedre_add_comment` MCP tool, with automatic local file resolution and relaxed `required: ['ticketId']` schema constraint.
20
+ - Added comprehensive automated test suite in `tests/cliComment.test.ts` covering error handling, priority precedence, percent preservation, and MCP execution.
21
+
8
22
  ## [1.1.0] - 2026-10-02
9
23
 
10
24
  ### Added
package/README.md CHANGED
@@ -122,7 +122,7 @@ Core day-to-day workflow commands for scoping, viewing, planning, and verifying
122
122
  | `link` | `ese link <sourceId> <relation> <targetId> [--author "..."]` | Establish a bi-directional link between two tickets. Relations: `relates-to`, `blocks`, `parent-of`, `duplicates`. |
123
123
  | `unlink` | `ese unlink <sourceId> <targetId> [--relation <relation>]` | Remove a bi-directional link between two tickets. |
124
124
  | `milestone` | `ese milestone [list\|get\|create\|update\|delete] [args...]` | Manage project milestones and umbrella feature flags with deliverables tracking. |
125
- | `comment` | `ese comment <id> ["<text>"] [--text "..."] [--author "..."]` | Append a developer or LLM agent note to ticket history. |
125
+ | `comment` | `ese comment <id> ["<text>"] [--file <path>] [--text "..."] [--author "..."]` | Append a developer or LLM agent note to ticket history. |
126
126
  | `snapshot`, `refresh` | `ese snapshot [--project <code>] [--json]` | Generate or refresh projection `.esedre/snapshot.json` for zero-latency agent context. |
127
127
  | `projects` | `ese projects [--json]` | List registered projects within authorized scope. |
128
128
 
@@ -199,7 +199,7 @@ To connect Esedre to **Google Antigravity**, **Claude Code**, **Cursor**, or any
199
199
  ```
200
200
 
201
201
  ### Registered Tools
202
- - `esedre_list_tickets`: List tickets with optional project, status, type, priority, milestone, isBlocked, linkedTo, or search filter.
202
+ - `esedre_list_tickets`: List tickets with optional project, status, type, priority, milestone, isBlocked, linkedTo, search, or includePlan filter.
203
203
  - `esedre_get_ticket`: Retrieve full specification, summary, comments, revision, links, blocker status, and content hash (`sha1`).
204
204
  - `esedre_get_plan` & `esedre_save_plan`: Inspect and update implementation plans with optimistic concurrency (`lastHash`).
205
205
  - `esedre_create_ticket`: Mint new roadmap tickets with project code validation (up to 8 chars), optional priority, optional milestone, and specification detail markdown.
@@ -210,7 +210,7 @@ To connect Esedre to **Google Antigravity**, **Claude Code**, **Cursor**, or any
210
210
  - `esedre_get_milestone`: Retrieve milestone specifications, umbrella feature flag, and member tickets.
211
211
  - `esedre_create_milestone`: Create a project milestone with optional umbrella feature flag and target date.
212
212
  - `esedre_update_milestone`: Update milestone status, title, description, or umbrella feature flag.
213
- - `esedre_add_comment`: Append developer or LLM agent verification notes.
213
+ - `esedre_add_comment`: Append developer or LLM agent verification notes with optional file path or inline text.
214
214
 
215
215
  ### Cross-Project Linked Issues & Dependencies
216
216
 
package/dist/esedre.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // bin/esedre.ts
4
- import fs10 from "node:fs";
5
- import path10 from "node:path";
4
+ import fs11 from "node:fs";
5
+ import path11 from "node:path";
6
6
 
7
7
  // src/storage/filesystem.ts
8
8
  import fs3 from "node:fs";
@@ -10,7 +10,7 @@ import path3 from "node:path";
10
10
  import os2 from "node:os";
11
11
 
12
12
  // src/types.ts
13
- var CURRENT_ESEDRE_VERSION = "1.1.0";
13
+ var CURRENT_ESEDRE_VERSION = "1.2.0";
14
14
  var INVERSE_RELATIONS = {
15
15
  "relates-to": "relates-to",
16
16
  "blocks": "blocked-by",
@@ -3579,6 +3579,8 @@ var SecurityFilter = class {
3579
3579
  };
3580
3580
 
3581
3581
  // src/mcp/server.ts
3582
+ import fs4 from "node:fs";
3583
+ import path4 from "node:path";
3582
3584
  import readline from "node:readline";
3583
3585
  var EsedreMcpServer = class {
3584
3586
  storage;
@@ -3800,7 +3802,8 @@ Status: ${ticket.meta.status}`;
3800
3802
  milestone: { type: "string", description: "Filter tickets by milestone name or ID" },
3801
3803
  search: { type: "string", description: "Search keywords in title or ID" },
3802
3804
  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" }
3805
+ linkedTo: { type: "string", description: "Filter tickets linked to a specific ticket ID or key" },
3806
+ includePlan: { type: "boolean", description: "When true, includes implementation plan markdown (planMarkdown) for each ticket" }
3804
3807
  }
3805
3808
  }
3806
3809
  },
@@ -3892,9 +3895,10 @@ Status: ${ticket.meta.status}`;
3892
3895
  properties: {
3893
3896
  ticketId: { type: ["integer", "string"], description: "Numeric ticket ID or compound key" },
3894
3897
  text: { type: "string", description: "Comment message" },
3898
+ file: { type: "string", description: "Path to local file containing comment message markdown" },
3895
3899
  author: { type: "string", description: "Author name (e.g. Antigravity, Developer)" }
3896
3900
  },
3897
- required: ["ticketId", "text"]
3901
+ required: ["ticketId"]
3898
3902
  }
3899
3903
  },
3900
3904
  {
@@ -3991,6 +3995,7 @@ Status: ${ticket.meta.status}`;
3991
3995
  switch (canonical) {
3992
3996
  case "list_tickets": {
3993
3997
  const type = args.type || args.category;
3998
+ const includePlan = Boolean(args.includePlan);
3994
3999
  const tickets = await this.storage.listTickets({
3995
4000
  project: args.project,
3996
4001
  status: args.status,
@@ -4002,23 +4007,39 @@ Status: ${ticket.meta.status}`;
4002
4007
  isBlocked: args.isBlocked !== void 0 ? Boolean(args.isBlocked) : void 0,
4003
4008
  linkedTo: args.linkedTo
4004
4009
  });
4005
- return tickets.map((t) => ({
4006
- id: t.meta.id,
4007
- title: t.meta.title,
4008
- type: t.meta.type || t.meta.category,
4009
- category: t.meta.type || t.meta.category,
4010
- status: t.meta.status,
4011
- priority: t.meta.priority,
4012
- complexity: t.meta.complexity,
4013
- effort: t.meta.estimatedEffort,
4014
- milestone: t.meta.milestone,
4015
- featureFlag: t.meta.featureFlag,
4016
- inheritedFeatureFlag: t.meta.inheritedFeatureFlag,
4017
- project: t.projectDescriptor?.code || t.meta.project || "UNASSIGNED",
4018
- sha1: t.sha1 || t.meta.sha1,
4019
- isBlocked: t.isBlocked,
4020
- links: t.links
4021
- }));
4010
+ const results = await Promise.all(
4011
+ tickets.map(async (t) => {
4012
+ let planMarkdown = t.planMarkdown;
4013
+ if (includePlan && planMarkdown === void 0) {
4014
+ const lookupKey = t.projectDescriptor?.code ? `${t.projectDescriptor.code}-${t.meta.id}` : t.meta.id;
4015
+ planMarkdown = await this.storage.getPlan(lookupKey) ?? null;
4016
+ }
4017
+ const hasPlan = Boolean(
4018
+ t.planMarkdown && t.planMarkdown.trim().length > 0 || planMarkdown && planMarkdown.trim().length > 0
4019
+ );
4020
+ return {
4021
+ id: t.meta.id,
4022
+ title: t.meta.title,
4023
+ type: t.meta.type || t.meta.category,
4024
+ category: t.meta.type || t.meta.category,
4025
+ status: t.meta.status,
4026
+ priority: t.meta.priority,
4027
+ complexity: t.meta.complexity,
4028
+ effort: t.meta.estimatedEffort,
4029
+ milestone: t.meta.milestone,
4030
+ featureFlag: t.meta.featureFlag,
4031
+ inheritedFeatureFlag: t.meta.inheritedFeatureFlag,
4032
+ project: t.projectDescriptor?.code || t.meta.project || "UNASSIGNED",
4033
+ summary: t.detail?.summary,
4034
+ hasPlan,
4035
+ ...includePlan ? { planMarkdown: planMarkdown || null } : {},
4036
+ sha1: t.sha1 || t.meta.sha1,
4037
+ isBlocked: t.isBlocked,
4038
+ links: t.links
4039
+ };
4040
+ })
4041
+ );
4042
+ return results;
4022
4043
  }
4023
4044
  case "get_ticket": {
4024
4045
  const id = this.parseTicketId(args.ticketId);
@@ -4077,8 +4098,26 @@ Status: ${ticket.meta.status}`;
4077
4098
  }
4078
4099
  case "add_comment": {
4079
4100
  const id = this.parseTicketId(args.ticketId);
4101
+ let text = args.text;
4102
+ if (!text && args.file) {
4103
+ const resolvedPath = path4.resolve(process.cwd(), args.file);
4104
+ if (!fs4.existsSync(resolvedPath)) {
4105
+ throw new Error(`File not found: ${args.file}`);
4106
+ }
4107
+ if (!fs4.statSync(resolvedPath).isFile()) {
4108
+ throw new Error(`Path is not a file: ${args.file}`);
4109
+ }
4110
+ const fileContent = fs4.readFileSync(resolvedPath, "utf-8");
4111
+ if (!fileContent.trim()) {
4112
+ throw new Error(`Comment file is empty: ${args.file}`);
4113
+ }
4114
+ text = fileContent;
4115
+ }
4116
+ if (!text || !text.trim()) {
4117
+ throw new Error("Comment text or file is required");
4118
+ }
4080
4119
  const comment = await this.storage.addComment(id, {
4081
- text: args.text,
4120
+ text,
4082
4121
  author: args.author || "Agent"
4083
4122
  });
4084
4123
  return comment;
@@ -4147,19 +4186,19 @@ Status: ${ticket.meta.status}`;
4147
4186
  };
4148
4187
 
4149
4188
  // src/utils/manifestDetector.ts
4150
- import fs4 from "node:fs";
4151
- import path4 from "node:path";
4189
+ import fs5 from "node:fs";
4190
+ import path5 from "node:path";
4152
4191
  function sanitizeCode(input) {
4153
4192
  const clean = input.replace(/[^a-zA-Z0-9]/g, "");
4154
4193
  return clean.slice(0, MAX_PROJECT_CODE_LENGTH) || "APP";
4155
4194
  }
4156
4195
  function detectProjectManifest(targetDir) {
4157
- const dirBase = path4.basename(targetDir);
4196
+ const dirBase = path5.basename(targetDir);
4158
4197
  for (const settingsFile of ["settings.gradle.kts", "settings.gradle"]) {
4159
- const fullPath = path4.join(targetDir, settingsFile);
4160
- if (fs4.existsSync(fullPath)) {
4198
+ const fullPath = path5.join(targetDir, settingsFile);
4199
+ if (fs5.existsSync(fullPath)) {
4161
4200
  try {
4162
- const content = fs4.readFileSync(fullPath, "utf-8");
4201
+ const content = fs5.readFileSync(fullPath, "utf-8");
4163
4202
  const match = content.match(/rootProject\.name\s*=\s*["']([^"']+)["']/);
4164
4203
  if (match && match[1].trim()) {
4165
4204
  const rawName = match[1].trim();
@@ -4175,15 +4214,15 @@ function detectProjectManifest(targetDir) {
4175
4214
  }
4176
4215
  }
4177
4216
  for (const buildFile of [
4178
- path4.join("app", "build.gradle.kts"),
4217
+ path5.join("app", "build.gradle.kts"),
4179
4218
  "build.gradle.kts",
4180
- path4.join("app", "build.gradle"),
4219
+ path5.join("app", "build.gradle"),
4181
4220
  "build.gradle"
4182
4221
  ]) {
4183
- const fullPath = path4.join(targetDir, buildFile);
4184
- if (fs4.existsSync(fullPath)) {
4222
+ const fullPath = path5.join(targetDir, buildFile);
4223
+ if (fs5.existsSync(fullPath)) {
4185
4224
  try {
4186
- const content = fs4.readFileSync(fullPath, "utf-8");
4225
+ const content = fs5.readFileSync(fullPath, "utf-8");
4187
4226
  const match = content.match(/(?:namespace|applicationId)\s*=\s*["']([^"']+)["']/);
4188
4227
  if (match && match[1].trim()) {
4189
4228
  const ns = match[1].trim();
@@ -4200,10 +4239,10 @@ function detectProjectManifest(targetDir) {
4200
4239
  }
4201
4240
  }
4202
4241
  }
4203
- const pomPath = path4.join(targetDir, "pom.xml");
4204
- if (fs4.existsSync(pomPath)) {
4242
+ const pomPath = path5.join(targetDir, "pom.xml");
4243
+ if (fs5.existsSync(pomPath)) {
4205
4244
  try {
4206
- const content = fs4.readFileSync(pomPath, "utf-8");
4245
+ const content = fs5.readFileSync(pomPath, "utf-8");
4207
4246
  const stripped = content.replace(/<!--[\s\S]*?-->/g, "").replace(/<parent>[\s\S]*?<\/parent>/g, "");
4208
4247
  const nameMatch = stripped.match(/<name>\s*([^<]+)\s*<\/name>/);
4209
4248
  const artifactMatch = stripped.match(/<artifactId>\s*([^<]+)\s*<\/artifactId>/) || content.match(/<artifactId>\s*([^<]+)\s*<\/artifactId>/);
@@ -4222,10 +4261,10 @@ function detectProjectManifest(targetDir) {
4222
4261
  } catch {
4223
4262
  }
4224
4263
  }
4225
- const pkgPath = path4.join(targetDir, "package.json");
4226
- if (fs4.existsSync(pkgPath)) {
4264
+ const pkgPath = path5.join(targetDir, "package.json");
4265
+ if (fs5.existsSync(pkgPath)) {
4227
4266
  try {
4228
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
4267
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
4229
4268
  if (pkg.name && typeof pkg.name === "string") {
4230
4269
  const baseName = pkg.name.replace(/^@[\w-]+\//, "");
4231
4270
  return {
@@ -4238,10 +4277,10 @@ function detectProjectManifest(targetDir) {
4238
4277
  } catch {
4239
4278
  }
4240
4279
  }
4241
- const cargoPath = path4.join(targetDir, "Cargo.toml");
4242
- if (fs4.existsSync(cargoPath)) {
4280
+ const cargoPath = path5.join(targetDir, "Cargo.toml");
4281
+ if (fs5.existsSync(cargoPath)) {
4243
4282
  try {
4244
- const content = fs4.readFileSync(cargoPath, "utf-8");
4283
+ const content = fs5.readFileSync(cargoPath, "utf-8");
4245
4284
  const match = content.match(/\[package\][\s\S]*?name\s*=\s*["']([^"']+)["']/);
4246
4285
  if (match && match[1].trim()) {
4247
4286
  const rawName = match[1].trim();
@@ -4255,10 +4294,10 @@ function detectProjectManifest(targetDir) {
4255
4294
  } catch {
4256
4295
  }
4257
4296
  }
4258
- const pyprojectPath = path4.join(targetDir, "pyproject.toml");
4259
- if (fs4.existsSync(pyprojectPath)) {
4297
+ const pyprojectPath = path5.join(targetDir, "pyproject.toml");
4298
+ if (fs5.existsSync(pyprojectPath)) {
4260
4299
  try {
4261
- const content = fs4.readFileSync(pyprojectPath, "utf-8");
4300
+ const content = fs5.readFileSync(pyprojectPath, "utf-8");
4262
4301
  const match = content.match(/name\s*=\s*["']([^"']+)["']/);
4263
4302
  if (match && match[1].trim()) {
4264
4303
  const rawName = match[1].trim();
@@ -4272,10 +4311,10 @@ function detectProjectManifest(targetDir) {
4272
4311
  } catch {
4273
4312
  }
4274
4313
  }
4275
- const setupCfgPath = path4.join(targetDir, "setup.cfg");
4276
- if (fs4.existsSync(setupCfgPath)) {
4314
+ const setupCfgPath = path5.join(targetDir, "setup.cfg");
4315
+ if (fs5.existsSync(setupCfgPath)) {
4277
4316
  try {
4278
- const content = fs4.readFileSync(setupCfgPath, "utf-8");
4317
+ const content = fs5.readFileSync(setupCfgPath, "utf-8");
4279
4318
  const match = content.match(/^name\s*=\s*([^\r\n]+)/m);
4280
4319
  if (match && match[1].trim()) {
4281
4320
  const rawName = match[1].trim();
@@ -4289,10 +4328,10 @@ function detectProjectManifest(targetDir) {
4289
4328
  } catch {
4290
4329
  }
4291
4330
  }
4292
- const setupPyPath = path4.join(targetDir, "setup.py");
4293
- if (fs4.existsSync(setupPyPath)) {
4331
+ const setupPyPath = path5.join(targetDir, "setup.py");
4332
+ if (fs5.existsSync(setupPyPath)) {
4294
4333
  try {
4295
- const content = fs4.readFileSync(setupPyPath, "utf-8");
4334
+ const content = fs5.readFileSync(setupPyPath, "utf-8");
4296
4335
  const match = content.match(/name\s*=\s*["']([^"']+)["']/);
4297
4336
  if (match && match[1].trim()) {
4298
4337
  const rawName = match[1].trim();
@@ -4306,10 +4345,10 @@ function detectProjectManifest(targetDir) {
4306
4345
  } catch {
4307
4346
  }
4308
4347
  }
4309
- const goModPath = path4.join(targetDir, "go.mod");
4310
- if (fs4.existsSync(goModPath)) {
4348
+ const goModPath = path5.join(targetDir, "go.mod");
4349
+ if (fs5.existsSync(goModPath)) {
4311
4350
  try {
4312
- const content = fs4.readFileSync(goModPath, "utf-8");
4351
+ const content = fs5.readFileSync(goModPath, "utf-8");
4313
4352
  const match = content.match(/^module\s+([^\r\n\s]+)/m);
4314
4353
  if (match && match[1].trim()) {
4315
4354
  const modPath = match[1].trim();
@@ -4333,48 +4372,48 @@ function detectProjectManifest(targetDir) {
4333
4372
 
4334
4373
  // src/upgrade.ts
4335
4374
  import crypto2 from "node:crypto";
4336
- import fs6 from "node:fs";
4337
- import path6 from "node:path";
4375
+ import fs7 from "node:fs";
4376
+ import path7 from "node:path";
4338
4377
 
4339
4378
  // src/server/daemon.ts
4340
- import fs5 from "node:fs";
4341
- import path5 from "node:path";
4379
+ import fs6 from "node:fs";
4380
+ import path6 from "node:path";
4342
4381
  import os3 from "node:os";
4343
4382
  import http from "node:http";
4344
4383
  import { spawn, spawnSync } from "node:child_process";
4345
4384
  import { fileURLToPath } from "node:url";
4346
4385
  var __filename = fileURLToPath(import.meta.url);
4347
- var __dirname = path5.dirname(__filename);
4386
+ var __dirname = path6.dirname(__filename);
4348
4387
  function getGlobalEsedreDir() {
4349
4388
  if (process.env.ESEDRE_GLOBAL_DIR) {
4350
4389
  return process.env.ESEDRE_GLOBAL_DIR;
4351
4390
  }
4352
- return path5.join(os3.homedir(), ".esedre");
4391
+ return path6.join(os3.homedir(), ".esedre");
4353
4392
  }
4354
4393
  function ensureGlobalEsedreStore() {
4355
4394
  const globalDir = getGlobalEsedreDir();
4356
- const runDir = path5.join(globalDir, "run");
4357
- const logsDir = path5.join(globalDir, "logs");
4395
+ const runDir = path6.join(globalDir, "run");
4396
+ const logsDir = path6.join(globalDir, "logs");
4358
4397
  try {
4359
- fs5.mkdirSync(runDir, { recursive: true });
4360
- fs5.mkdirSync(logsDir, { recursive: true });
4398
+ fs6.mkdirSync(runDir, { recursive: true });
4399
+ fs6.mkdirSync(logsDir, { recursive: true });
4361
4400
  try {
4362
- const runEntries = fs5.readdirSync(runDir);
4401
+ const runEntries = fs6.readdirSync(runDir);
4363
4402
  for (const entry of runEntries) {
4364
4403
  if (entry.startsWith(".probe-")) {
4365
- fs5.unlinkSync(path5.join(runDir, entry));
4404
+ fs6.unlinkSync(path6.join(runDir, entry));
4366
4405
  }
4367
4406
  }
4368
4407
  } catch {
4369
4408
  }
4370
- const testProbe = path5.join(runDir, `.probe-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`);
4409
+ const testProbe = path6.join(runDir, `.probe-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`);
4371
4410
  try {
4372
- fs5.writeFileSync(testProbe, "ok", "utf-8");
4411
+ fs6.writeFileSync(testProbe, "ok", "utf-8");
4373
4412
  return { ok: true, path: globalDir };
4374
4413
  } finally {
4375
- if (fs5.existsSync(testProbe)) {
4414
+ if (fs6.existsSync(testProbe)) {
4376
4415
  try {
4377
- fs5.unlinkSync(testProbe);
4416
+ fs6.unlinkSync(testProbe);
4378
4417
  } catch {
4379
4418
  }
4380
4419
  }
@@ -4389,10 +4428,10 @@ function resolveStoreDir(workspaceRoot) {
4389
4428
  return globalStore.path;
4390
4429
  }
4391
4430
  if (workspaceRoot) {
4392
- const localStore = path5.join(workspaceRoot, ".esedre");
4431
+ const localStore = path6.join(workspaceRoot, ".esedre");
4393
4432
  try {
4394
- fs5.mkdirSync(path5.join(localStore, "run"), { recursive: true });
4395
- fs5.mkdirSync(path5.join(localStore, "logs"), { recursive: true });
4433
+ fs6.mkdirSync(path6.join(localStore, "run"), { recursive: true });
4434
+ fs6.mkdirSync(path6.join(localStore, "logs"), { recursive: true });
4396
4435
  return localStore;
4397
4436
  } catch {
4398
4437
  }
@@ -4401,32 +4440,32 @@ function resolveStoreDir(workspaceRoot) {
4401
4440
  }
4402
4441
  function getDaemonStateFile(port, workspaceRoot) {
4403
4442
  const store = resolveStoreDir(workspaceRoot);
4404
- return path5.join(store, "run", `daemon-${port}.json`);
4443
+ return path6.join(store, "run", `daemon-${port}.json`);
4405
4444
  }
4406
4445
  function getDaemonLogFile(port, workspaceRoot) {
4407
4446
  const store = resolveStoreDir(workspaceRoot);
4408
- return path5.join(store, "logs", `daemon-${port}.log`);
4447
+ return path6.join(store, "logs", `daemon-${port}.log`);
4409
4448
  }
4410
4449
  function getDaemonState(port, workspaceRoot) {
4411
4450
  const stateFile = getDaemonStateFile(port, workspaceRoot);
4412
- if (!fs5.existsSync(stateFile)) {
4451
+ if (!fs6.existsSync(stateFile)) {
4413
4452
  return null;
4414
4453
  }
4415
4454
  try {
4416
- const raw = fs5.readFileSync(stateFile, "utf-8").trim();
4455
+ const raw = fs6.readFileSync(stateFile, "utf-8").trim();
4417
4456
  if (!raw) {
4418
- fs5.unlinkSync(stateFile);
4457
+ fs6.unlinkSync(stateFile);
4419
4458
  return null;
4420
4459
  }
4421
4460
  const state = JSON.parse(raw);
4422
4461
  if (!state || typeof state.pid !== "number" || typeof state.port !== "number") {
4423
- fs5.unlinkSync(stateFile);
4462
+ fs6.unlinkSync(stateFile);
4424
4463
  return null;
4425
4464
  }
4426
4465
  return state;
4427
4466
  } catch {
4428
4467
  try {
4429
- fs5.unlinkSync(stateFile);
4468
+ fs6.unlinkSync(stateFile);
4430
4469
  } catch {
4431
4470
  }
4432
4471
  return null;
@@ -4497,7 +4536,7 @@ async function startDaemon(options = {}) {
4497
4536
  }
4498
4537
  }
4499
4538
  try {
4500
- fs5.unlinkSync(stateFile);
4539
+ fs6.unlinkSync(stateFile);
4501
4540
  } catch {
4502
4541
  }
4503
4542
  }
@@ -4516,40 +4555,40 @@ async function startDaemon(options = {}) {
4516
4555
  };
4517
4556
  }
4518
4557
  resolveStoreDir(workspaceRoot);
4519
- fs5.mkdirSync(path5.dirname(logFile), { recursive: true });
4520
- fs5.mkdirSync(path5.dirname(stateFile), { recursive: true });
4558
+ fs6.mkdirSync(path6.dirname(logFile), { recursive: true });
4559
+ fs6.mkdirSync(path6.dirname(stateFile), { recursive: true });
4521
4560
  const MAX_LOG_SIZE_BYTES = 10 * 1024 * 1024;
4522
4561
  try {
4523
- if (fs5.existsSync(logFile)) {
4524
- const stats = fs5.statSync(logFile);
4562
+ if (fs6.existsSync(logFile)) {
4563
+ const stats = fs6.statSync(logFile);
4525
4564
  if (stats.size > MAX_LOG_SIZE_BYTES) {
4526
4565
  const oldLogFile = `${logFile}.old`;
4527
4566
  try {
4528
- if (fs5.existsSync(oldLogFile)) fs5.unlinkSync(oldLogFile);
4529
- fs5.renameSync(logFile, oldLogFile);
4567
+ if (fs6.existsSync(oldLogFile)) fs6.unlinkSync(oldLogFile);
4568
+ fs6.renameSync(logFile, oldLogFile);
4530
4569
  } catch {
4531
- fs5.truncateSync(logFile, 0);
4570
+ fs6.truncateSync(logFile, 0);
4532
4571
  }
4533
4572
  }
4534
4573
  }
4535
4574
  } catch {
4536
4575
  }
4537
- const logFd = fs5.openSync(logFile, "a");
4538
- const isEsedreArgv1 = process.argv[1] && /esedre/i.test(path5.basename(process.argv[1]));
4576
+ const logFd = fs6.openSync(logFile, "a");
4577
+ const isEsedreArgv1 = process.argv[1] && /esedre/i.test(path6.basename(process.argv[1]));
4539
4578
  const cliCandidates = [
4540
- isEsedreArgv1 ? path5.resolve(process.argv[1]) : void 0,
4541
- path5.resolve(workspaceRoot, "dist", "esedre.mjs"),
4542
- path5.resolve(workspaceRoot, "../esedre/dist", "esedre.mjs"),
4543
- path5.resolve(__dirname, "esedre.mjs"),
4544
- path5.resolve(__dirname, "..", "dist", "esedre.mjs"),
4545
- path5.resolve(__dirname, "..", "..", "dist", "esedre.mjs"),
4546
- path5.resolve(__dirname, "..", "..", "bin", "esedre.js"),
4547
- path5.resolve(__dirname, "..", "..", "bin", "esedre.ts"),
4548
- path5.resolve(__dirname, "..", "esedre.mjs"),
4549
- path5.resolve(__dirname, "../../../esedre/dist", "esedre.mjs")
4579
+ isEsedreArgv1 ? path6.resolve(process.argv[1]) : void 0,
4580
+ path6.resolve(workspaceRoot, "dist", "esedre.mjs"),
4581
+ path6.resolve(workspaceRoot, "../esedre/dist", "esedre.mjs"),
4582
+ path6.resolve(__dirname, "esedre.mjs"),
4583
+ path6.resolve(__dirname, "..", "dist", "esedre.mjs"),
4584
+ path6.resolve(__dirname, "..", "..", "dist", "esedre.mjs"),
4585
+ path6.resolve(__dirname, "..", "..", "bin", "esedre.js"),
4586
+ path6.resolve(__dirname, "..", "..", "bin", "esedre.ts"),
4587
+ path6.resolve(__dirname, "..", "esedre.mjs"),
4588
+ path6.resolve(__dirname, "../../../esedre/dist", "esedre.mjs")
4550
4589
  ].filter(Boolean);
4551
- const cliPath = cliCandidates.find((c) => fs5.existsSync(c) && (c.endsWith(".mjs") || c.endsWith(".js") || c.endsWith(".ts"))) || cliCandidates[0];
4552
- if (!fs5.existsSync(cliPath)) {
4590
+ const cliPath = cliCandidates.find((c) => fs6.existsSync(c) && (c.endsWith(".mjs") || c.endsWith(".js") || c.endsWith(".ts"))) || cliCandidates[0];
4591
+ if (!fs6.existsSync(cliPath)) {
4553
4592
  throw new Error(`Could not locate Esedre executable. Searched candidates: ${cliCandidates.join(", ")}`);
4554
4593
  }
4555
4594
  const child = spawn(process.execPath, [cliPath, "start", "--foreground", "--port", String(port)], {
@@ -4561,7 +4600,7 @@ async function startDaemon(options = {}) {
4561
4600
  });
4562
4601
  child.unref();
4563
4602
  try {
4564
- fs5.closeSync(logFd);
4603
+ fs6.closeSync(logFd);
4565
4604
  } catch {
4566
4605
  }
4567
4606
  const pid = child.pid;
@@ -4602,13 +4641,13 @@ async function startDaemon(options = {}) {
4602
4641
  logFile
4603
4642
  };
4604
4643
  const tmpState = `${stateFile}.tmp.${Date.now()}`;
4605
- fs5.writeFileSync(tmpState, JSON.stringify(state, null, 2) + "\n", "utf-8");
4644
+ fs6.writeFileSync(tmpState, JSON.stringify(state, null, 2) + "\n", "utf-8");
4606
4645
  try {
4607
- fs5.renameSync(tmpState, stateFile);
4646
+ fs6.renameSync(tmpState, stateFile);
4608
4647
  } catch {
4609
- fs5.writeFileSync(stateFile, JSON.stringify(state, null, 2) + "\n", "utf-8");
4648
+ fs6.writeFileSync(stateFile, JSON.stringify(state, null, 2) + "\n", "utf-8");
4610
4649
  try {
4611
- fs5.unlinkSync(tmpState);
4650
+ fs6.unlinkSync(tmpState);
4612
4651
  } catch {
4613
4652
  }
4614
4653
  }
@@ -4671,8 +4710,8 @@ async function stopDaemon(options = {}) {
4671
4710
  }
4672
4711
  }
4673
4712
  try {
4674
- if (fs5.existsSync(stateFile)) {
4675
- fs5.unlinkSync(stateFile);
4713
+ if (fs6.existsSync(stateFile)) {
4714
+ fs6.unlinkSync(stateFile);
4676
4715
  }
4677
4716
  } catch {
4678
4717
  }
@@ -4709,7 +4748,7 @@ async function getDaemonStatus(options = {}) {
4709
4748
  }
4710
4749
  if (state && !isProcessAlive(state.pid)) {
4711
4750
  try {
4712
- fs5.unlinkSync(stateFile);
4751
+ fs6.unlinkSync(stateFile);
4713
4752
  } catch {
4714
4753
  }
4715
4754
  }
@@ -4727,26 +4766,26 @@ function printDaemonLogs(options = {}) {
4727
4766
  const port = resolvePorts(void 0, { port: options.port }).gateway;
4728
4767
  const workspaceRoot = options.workspaceRoot || process.cwd();
4729
4768
  const logFile = getDaemonLogFile(port, workspaceRoot);
4730
- if (!fs5.existsSync(logFile)) {
4769
+ if (!fs6.existsSync(logFile)) {
4731
4770
  console.log(`\x1B[33mNo logs found at: ${logFile}\x1B[0m`);
4732
4771
  return;
4733
4772
  }
4734
4773
  const count = options.lines || 40;
4735
4774
  let content = "";
4736
4775
  try {
4737
- const stats = fs5.statSync(logFile);
4776
+ const stats = fs6.statSync(logFile);
4738
4777
  const maxReadBytes = Math.min(stats.size, Math.max(256 * 1024, count * 2048));
4739
4778
  if (stats.size <= maxReadBytes) {
4740
- content = fs5.readFileSync(logFile, "utf-8");
4779
+ content = fs6.readFileSync(logFile, "utf-8");
4741
4780
  } else {
4742
- const fd = fs5.openSync(logFile, "r");
4781
+ const fd = fs6.openSync(logFile, "r");
4743
4782
  const buffer = Buffer.alloc(maxReadBytes);
4744
- fs5.readSync(fd, buffer, 0, maxReadBytes, stats.size - maxReadBytes);
4745
- fs5.closeSync(fd);
4783
+ fs6.readSync(fd, buffer, 0, maxReadBytes, stats.size - maxReadBytes);
4784
+ fs6.closeSync(fd);
4746
4785
  content = buffer.toString("utf-8");
4747
4786
  }
4748
4787
  } catch {
4749
- content = fs5.readFileSync(logFile, "utf-8");
4788
+ content = fs6.readFileSync(logFile, "utf-8");
4750
4789
  }
4751
4790
  const allLines = content.split(/\r?\n/);
4752
4791
  const sanitizedLines = allLines.slice(-count).map((line) => {
@@ -4961,13 +5000,13 @@ When Esedre MCP is active in your agent session (\`.agents/mcp_config.json\`), u
4961
5000
 
4962
5001
  | Tool | Purpose | Key Arguments |
4963
5002
  |---|---|---|
4964
- | \`esedre_list_tickets\` | List roadmap tickets with filters | \`project\` (e.g. \`Profe\`, \`Esedre\`, \`Alce\`), \`status\`, \`type\`, \`search\` |
5003
+ | \`esedre_list_tickets\` | List roadmap tickets with filters | \`project\` (e.g. \`Profe\`, \`Esedre\`, \`Alce\`), \`status\`, \`type\`, \`priority\`, \`milestone\`, \`isBlocked\`, \`linkedTo\`, \`search\`, \`includePlan\` |
4965
5004
  | \`esedre_get_ticket\` | Full specification, summary, comments, revision & sha1 | \`ticketId\` (numeric e.g. \`96\` or compound e.g. \`Profe-96\`) |
4966
5005
  | \`esedre_get_plan\` | Active implementation plan markdown | \`ticketId\` (numeric or compound) |
4967
5006
  | \`esedre_save_plan\` | Save implementation plan markdown with OCC | \`ticketId\`, \`planMarkdown\`, \`lastHash\` |
4968
5007
  | \`esedre_create_ticket\` | Mint a new ticket with auto sequential ID | \`title\` (max 48 chars), \`type\`, \`project\`, \`effort\`, \`summary\`, \`detail\` |
4969
5008
  | \`esedre_update_ticket\` | Update ticket attributes with OCC | \`ticketId\`, \`status\`, \`type\`, \`title\`, \`complexity\`, \`effort\`, \`inDevelopment\`, \`featureFlag\`, \`lastHash\` |
4970
- | \`esedre_add_comment\` | Append developer or agent comment | \`ticketId\`, \`text\`, \`author\` |
5009
+ | \`esedre_add_comment\` | Append developer or agent comment | \`ticketId\`, \`text\`, \`file\`, \`author\` |
4971
5010
 
4972
5011
  ## 2. Core CLI Commands (Fallback via \`run_command\`)
4973
5012
 
@@ -5005,6 +5044,7 @@ Both \`esedre\` and \`ese\` work interchangeably:
5005
5044
  ### Append Comments or Notes
5006
5045
  \`\`\`bash
5007
5046
  .esedre/ese comment <ticketId> "Verified implementation." --author "Agent"
5047
+ .esedre/ese comment <ticketId> --file <commentFilePath> --author "Agent"
5008
5048
  .esedre/ese comment <ticketId> --text "Verified implementation." --author "Agent"
5009
5049
  \`\`\`
5010
5050
 
@@ -5060,8 +5100,8 @@ If the host application declares any or all of the 12 core design tokens on \`:r
5060
5100
  - The URL hash is preserved across hard page refreshes (F5) without falling back to \`#/chat\` or other views.
5061
5101
  `;
5062
5102
  function plantWrappers(targetDir) {
5063
- const esedreDir = path6.join(targetDir, ".esedre");
5064
- fs6.mkdirSync(esedreDir, { recursive: true });
5103
+ const esedreDir = path7.join(targetDir, ".esedre");
5104
+ fs7.mkdirSync(esedreDir, { recursive: true });
5065
5105
  const wrappers = [
5066
5106
  { name: "esedre.cmd", content: WRAPPER_CMD, exec: false },
5067
5107
  { name: "esedre.ps1", content: WRAPPER_PS1, exec: false },
@@ -5071,24 +5111,24 @@ function plantWrappers(targetDir) {
5071
5111
  { name: "ese", content: ESE_WRAPPER_BASH, exec: true }
5072
5112
  ];
5073
5113
  for (const w of wrappers) {
5074
- const fullPath = path6.join(esedreDir, w.name);
5075
- fs6.writeFileSync(fullPath, w.content, "utf-8");
5114
+ const fullPath = path7.join(esedreDir, w.name);
5115
+ fs7.writeFileSync(fullPath, w.content, "utf-8");
5076
5116
  if (w.exec) {
5077
5117
  try {
5078
- fs6.chmodSync(fullPath, 493);
5118
+ fs7.chmodSync(fullPath, 493);
5079
5119
  } catch {
5080
5120
  }
5081
5121
  }
5082
5122
  }
5083
5123
  }
5084
5124
  function setupMcpConfig(targetDir) {
5085
- const agentsDir = path6.join(targetDir, ".agents");
5086
- fs6.mkdirSync(agentsDir, { recursive: true });
5087
- const mcpConfigPath = path6.join(agentsDir, "mcp_config.json");
5125
+ const agentsDir = path7.join(targetDir, ".agents");
5126
+ fs7.mkdirSync(agentsDir, { recursive: true });
5127
+ const mcpConfigPath = path7.join(agentsDir, "mcp_config.json");
5088
5128
  let configObj = { mcpServers: {} };
5089
- if (fs6.existsSync(mcpConfigPath)) {
5129
+ if (fs7.existsSync(mcpConfigPath)) {
5090
5130
  try {
5091
- configObj = JSON.parse(fs6.readFileSync(mcpConfigPath, "utf-8"));
5131
+ configObj = JSON.parse(fs7.readFileSync(mcpConfigPath, "utf-8"));
5092
5132
  if (!configObj.mcpServers) configObj.mcpServers = {};
5093
5133
  } catch {
5094
5134
  }
@@ -5105,28 +5145,28 @@ function setupMcpConfig(targetDir) {
5105
5145
  };
5106
5146
  }
5107
5147
  try {
5108
- fs6.writeFileSync(mcpConfigPath, JSON.stringify(configObj, null, 2) + "\n", "utf-8");
5148
+ fs7.writeFileSync(mcpConfigPath, JSON.stringify(configObj, null, 2) + "\n", "utf-8");
5109
5149
  return true;
5110
5150
  } catch {
5111
5151
  return false;
5112
5152
  }
5113
5153
  }
5114
5154
  function setupSkill(targetDir, options = {}) {
5115
- const skillDir = path6.join(targetDir, ".agents", "skills", "esedre");
5116
- fs6.mkdirSync(skillDir, { recursive: true });
5117
- const skillFile = path6.join(skillDir, "SKILL.md");
5118
- if (!fs6.existsSync(skillFile)) {
5119
- fs6.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
5155
+ const skillDir = path7.join(targetDir, ".agents", "skills", "esedre");
5156
+ fs7.mkdirSync(skillDir, { recursive: true });
5157
+ const skillFile = path7.join(skillDir, "SKILL.md");
5158
+ if (!fs7.existsSync(skillFile)) {
5159
+ fs7.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
5120
5160
  return { status: "MISSING", updated: true };
5121
5161
  }
5122
- const currentContent = fs6.readFileSync(skillFile, "utf-8");
5162
+ const currentContent = fs7.readFileSync(skillFile, "utf-8");
5123
5163
  const status = classifyContent(currentContent, ESEDRE_SKILL_TEMPLATE, HISTORIC_SKILL_HASHES);
5124
5164
  if (status === "LATEST" && !options.force) {
5125
5165
  return { status, updated: false };
5126
5166
  }
5127
5167
  const hasBannedOrDeprecatedTerms = currentContent.includes("companion agent") || currentContent.includes("companion LLM") || currentContent.includes("tools/esedre.mjs");
5128
5168
  if (options.force || status === "HISTORIC_DEFAULT" || status === "MISSING" || hasBannedOrDeprecatedTerms) {
5129
- fs6.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
5169
+ fs7.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
5130
5170
  return { status: "LATEST", updated: true };
5131
5171
  }
5132
5172
  return { status, updated: false };
@@ -5140,8 +5180,8 @@ var VITE_CONFIG_FILES = [
5140
5180
  ];
5141
5181
  function findViteConfig(targetDir) {
5142
5182
  for (const name of VITE_CONFIG_FILES) {
5143
- const fullPath = path6.join(targetDir, name);
5144
- if (fs6.existsSync(fullPath)) {
5183
+ const fullPath = path7.join(targetDir, name);
5184
+ if (fs7.existsSync(fullPath)) {
5145
5185
  return fullPath;
5146
5186
  }
5147
5187
  }
@@ -5205,10 +5245,10 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
5205
5245
  recommendedProxySnippet: snippet
5206
5246
  };
5207
5247
  }
5208
- const relativeName = path6.basename(configPath);
5248
+ const relativeName = path7.basename(configPath);
5209
5249
  let content = "";
5210
5250
  try {
5211
- content = fs6.readFileSync(configPath, "utf-8");
5251
+ content = fs7.readFileSync(configPath, "utf-8");
5212
5252
  } catch {
5213
5253
  return {
5214
5254
  status: "MANUAL_REQUIRED",
@@ -5239,7 +5279,7 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
5239
5279
  };
5240
5280
  }
5241
5281
  try {
5242
- fs6.writeFileSync(configPath, injected, "utf-8");
5282
+ fs7.writeFileSync(configPath, injected, "utf-8");
5243
5283
  return {
5244
5284
  status: "CONFIGURED",
5245
5285
  configFile: relativeName,
@@ -5254,15 +5294,15 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
5254
5294
  }
5255
5295
  }
5256
5296
  function configureWorkspace(targetDir, options = {}) {
5257
- const esedreDir = path6.join(targetDir, ".esedre");
5258
- if (!fs6.existsSync(esedreDir)) {
5259
- fs6.mkdirSync(esedreDir, { recursive: true });
5297
+ const esedreDir = path7.join(targetDir, ".esedre");
5298
+ if (!fs7.existsSync(esedreDir)) {
5299
+ fs7.mkdirSync(esedreDir, { recursive: true });
5260
5300
  }
5261
- const esedreJsonPath = path6.join(esedreDir, "esedre.json");
5301
+ const esedreJsonPath = path7.join(esedreDir, "esedre.json");
5262
5302
  let existingRaw = null;
5263
- if (fs6.existsSync(esedreJsonPath)) {
5303
+ if (fs7.existsSync(esedreJsonPath)) {
5264
5304
  try {
5265
- existingRaw = JSON.parse(fs6.readFileSync(esedreJsonPath, "utf-8"));
5305
+ existingRaw = JSON.parse(fs7.readFileSync(esedreJsonPath, "utf-8"));
5266
5306
  } catch {
5267
5307
  }
5268
5308
  }
@@ -5298,16 +5338,16 @@ function configureWorkspace(targetDir, options = {}) {
5298
5338
  port: options.port || void 0
5299
5339
  };
5300
5340
  }
5301
- fs6.writeFileSync(esedreJsonPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
5341
+ fs7.writeFileSync(esedreJsonPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
5302
5342
  let projectRegistered = false;
5303
5343
  if (projectCode) {
5304
- const hasExistingTicketsDir = fs6.existsSync(path6.join(targetDir, "tickets")) || fs6.existsSync(path6.join(targetDir, "src", "data", "planning", "tickets"));
5305
- const ticketsDir = path6.join(esedreDir, "tickets");
5306
- if (!hasExistingTicketsDir && !fs6.existsSync(ticketsDir)) {
5307
- fs6.mkdirSync(ticketsDir, { recursive: true });
5344
+ const hasExistingTicketsDir = fs7.existsSync(path7.join(targetDir, "tickets")) || fs7.existsSync(path7.join(targetDir, "src", "data", "planning", "tickets"));
5345
+ const ticketsDir = path7.join(esedreDir, "tickets");
5346
+ if (!hasExistingTicketsDir && !fs7.existsSync(ticketsDir)) {
5347
+ fs7.mkdirSync(ticketsDir, { recursive: true });
5308
5348
  }
5309
- const inRepoPJson = path6.join(esedreDir, "project.json");
5310
- if (!fs6.existsSync(inRepoPJson)) {
5349
+ const inRepoPJson = path7.join(esedreDir, "project.json");
5350
+ if (!fs7.existsSync(inRepoPJson)) {
5311
5351
  const projDesc = {
5312
5352
  id: 1,
5313
5353
  code: projectCode.toUpperCase(),
@@ -5319,14 +5359,14 @@ function configureWorkspace(targetDir, options = {}) {
5319
5359
  border: "border-cyan-500/40"
5320
5360
  }
5321
5361
  };
5322
- fs6.writeFileSync(inRepoPJson, JSON.stringify(projDesc, null, 2) + "\n", "utf-8");
5362
+ fs7.writeFileSync(inRepoPJson, JSON.stringify(projDesc, null, 2) + "\n", "utf-8");
5323
5363
  projectRegistered = true;
5324
5364
  } else if (projectName) {
5325
5365
  try {
5326
- const existing = JSON.parse(fs6.readFileSync(inRepoPJson, "utf-8"));
5366
+ const existing = JSON.parse(fs7.readFileSync(inRepoPJson, "utf-8"));
5327
5367
  if (existing.name !== projectName) {
5328
5368
  existing.name = projectName;
5329
- fs6.writeFileSync(inRepoPJson, JSON.stringify(existing, null, 2) + "\n", "utf-8");
5369
+ fs7.writeFileSync(inRepoPJson, JSON.stringify(existing, null, 2) + "\n", "utf-8");
5330
5370
  }
5331
5371
  } catch {
5332
5372
  }
@@ -5376,26 +5416,26 @@ var ESEDRE_HUB_AGENTS_TEMPLATE = `# Agent Guidelines: esedre-data
5376
5416
  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
5417
  `;
5378
5418
  function initHub(targetDir) {
5379
- const resolved = path6.resolve(targetDir);
5380
- if (!fs6.existsSync(resolved)) {
5381
- fs6.mkdirSync(resolved, { recursive: true });
5419
+ const resolved = path7.resolve(targetDir);
5420
+ if (!fs7.existsSync(resolved)) {
5421
+ fs7.mkdirSync(resolved, { recursive: true });
5382
5422
  }
5383
- const projectsDir = path6.join(resolved, "projects");
5423
+ const projectsDir = path7.join(resolved, "projects");
5384
5424
  let projectsDirCreated = false;
5385
- if (!fs6.existsSync(projectsDir)) {
5386
- fs6.mkdirSync(projectsDir, { recursive: true });
5425
+ if (!fs7.existsSync(projectsDir)) {
5426
+ fs7.mkdirSync(projectsDir, { recursive: true });
5387
5427
  projectsDirCreated = true;
5388
5428
  }
5389
- const projectsJsonPath = path6.join(resolved, "projects.json");
5429
+ const projectsJsonPath = path7.join(resolved, "projects.json");
5390
5430
  let projectsJsonCreated = false;
5391
- if (!fs6.existsSync(projectsJsonPath)) {
5392
- fs6.writeFileSync(projectsJsonPath, "[]\n", "utf-8");
5431
+ if (!fs7.existsSync(projectsJsonPath)) {
5432
+ fs7.writeFileSync(projectsJsonPath, "[]\n", "utf-8");
5393
5433
  projectsJsonCreated = true;
5394
5434
  }
5395
- const agentsMdPath = path6.join(resolved, "AGENTS.md");
5435
+ const agentsMdPath = path7.join(resolved, "AGENTS.md");
5396
5436
  let agentsMdCreated = false;
5397
- if (!fs6.existsSync(agentsMdPath)) {
5398
- fs6.writeFileSync(agentsMdPath, ESEDRE_HUB_AGENTS_TEMPLATE, "utf-8");
5437
+ if (!fs7.existsSync(agentsMdPath)) {
5438
+ fs7.writeFileSync(agentsMdPath, ESEDRE_HUB_AGENTS_TEMPLATE, "utf-8");
5399
5439
  agentsMdCreated = true;
5400
5440
  }
5401
5441
  return {
@@ -5424,14 +5464,14 @@ function findOutdatedWrappers(currentWorkspaceRoot, globalConfig) {
5424
5464
  const outdated = [];
5425
5465
  const checkedDirs = /* @__PURE__ */ new Set();
5426
5466
  const checkDir = (dir, name) => {
5427
- const resolved = path6.resolve(expandHome(dir));
5467
+ const resolved = path7.resolve(expandHome(dir));
5428
5468
  if (checkedDirs.has(resolved.toLowerCase())) return;
5429
5469
  checkedDirs.add(resolved.toLowerCase());
5430
- if (!fs6.existsSync(resolved)) return;
5431
- const cmdPath = path6.join(resolved, ".esedre", "esedre.cmd");
5432
- if (fs6.existsSync(cmdPath)) {
5470
+ if (!fs7.existsSync(resolved)) return;
5471
+ const cmdPath = path7.join(resolved, ".esedre", "esedre.cmd");
5472
+ if (fs7.existsSync(cmdPath)) {
5433
5473
  try {
5434
- const content = fs6.readFileSync(cmdPath, "utf-8");
5474
+ const content = fs7.readFileSync(cmdPath, "utf-8");
5435
5475
  if (isWrapperOutdated(content)) {
5436
5476
  outdated.push({ name, dir: resolved, wrapperPath: cmdPath });
5437
5477
  }
@@ -5440,7 +5480,7 @@ function findOutdatedWrappers(currentWorkspaceRoot, globalConfig) {
5440
5480
  }
5441
5481
  };
5442
5482
  if (currentWorkspaceRoot) {
5443
- checkDir(currentWorkspaceRoot, path6.basename(currentWorkspaceRoot));
5483
+ checkDir(currentWorkspaceRoot, path7.basename(currentWorkspaceRoot));
5444
5484
  }
5445
5485
  if (globalConfig?.projects) {
5446
5486
  for (const [code, p] of Object.entries(globalConfig.projects)) {
@@ -5452,20 +5492,20 @@ function findOutdatedWrappers(currentWorkspaceRoot, globalConfig) {
5452
5492
  async function upgradeAllWorkspaces(options = {}) {
5453
5493
  const globalCfg = options.globalConfig !== void 0 ? options.globalConfig : readGlobalConfig();
5454
5494
  const workspaceMap = /* @__PURE__ */ new Map();
5455
- if (options.currentWorkspaceRoot && fs6.existsSync(options.currentWorkspaceRoot)) {
5456
- const hasEsedre = fs6.existsSync(path6.join(options.currentWorkspaceRoot, ".esedre")) || fs6.existsSync(path6.join(options.currentWorkspaceRoot, "esedre.json"));
5495
+ if (options.currentWorkspaceRoot && fs7.existsSync(options.currentWorkspaceRoot)) {
5496
+ const hasEsedre = fs7.existsSync(path7.join(options.currentWorkspaceRoot, ".esedre")) || fs7.existsSync(path7.join(options.currentWorkspaceRoot, "esedre.json"));
5457
5497
  if (hasEsedre) {
5458
5498
  const disc = findEsedreConfig(options.currentWorkspaceRoot, { fallbackToGlobal: true });
5459
5499
  workspaceMap.set(
5460
- path6.resolve(options.currentWorkspaceRoot),
5461
- disc.config?.projectCode || path6.basename(options.currentWorkspaceRoot)
5500
+ path7.resolve(options.currentWorkspaceRoot),
5501
+ disc.config?.projectCode || path7.basename(options.currentWorkspaceRoot)
5462
5502
  );
5463
5503
  }
5464
5504
  }
5465
5505
  if (globalCfg?.projects) {
5466
5506
  for (const [code, p] of Object.entries(globalCfg.projects)) {
5467
- const resolved = path6.resolve(expandHome(p));
5468
- if (fs6.existsSync(resolved)) {
5507
+ const resolved = path7.resolve(expandHome(p));
5508
+ if (fs7.existsSync(resolved)) {
5469
5509
  if (!workspaceMap.has(resolved)) {
5470
5510
  workspaceMap.set(resolved, code);
5471
5511
  }
@@ -5516,15 +5556,15 @@ async function upgradeAllWorkspaces(options = {}) {
5516
5556
  }
5517
5557
 
5518
5558
  // src/server/gateway.ts
5519
- import fs9 from "node:fs";
5559
+ import fs10 from "node:fs";
5520
5560
  import http4 from "node:http";
5521
- import path9 from "node:path";
5561
+ import path10 from "node:path";
5522
5562
  import { fileURLToPath as fileURLToPath2 } from "node:url";
5523
5563
 
5524
5564
  // src/server/uiServer.ts
5525
5565
  import http2 from "node:http";
5526
- import fs7 from "node:fs";
5527
- import path7 from "node:path";
5566
+ import fs8 from "node:fs";
5567
+ import path8 from "node:path";
5528
5568
  var MIME_TYPES = {
5529
5569
  ".html": "text/html; charset=utf-8",
5530
5570
  ".js": "application/javascript; charset=utf-8",
@@ -5554,27 +5594,27 @@ function startUiServer(port, webDir) {
5554
5594
  if (!reqPath || reqPath === "/") {
5555
5595
  reqPath = "/index.html";
5556
5596
  }
5557
- const safePath = path7.normalize(reqPath).replace(/^(\.\.[/\\])+/, "");
5558
- let filePath = path7.join(webDir, safePath);
5559
- const resolvedFilePath = path7.resolve(filePath);
5560
- const resolvedWebDir = path7.resolve(webDir);
5597
+ const safePath = path8.normalize(reqPath).replace(/^(\.\.[/\\])+/, "");
5598
+ let filePath = path8.join(webDir, safePath);
5599
+ const resolvedFilePath = path8.resolve(filePath);
5600
+ const resolvedWebDir = path8.resolve(webDir);
5561
5601
  if (!resolvedFilePath.startsWith(resolvedWebDir)) {
5562
5602
  res.writeHead(403, { "Content-Type": "text/plain" });
5563
5603
  res.end("Access Denied: Path escapes web root.");
5564
5604
  return;
5565
5605
  }
5566
- if (!fs7.existsSync(filePath) || fs7.statSync(filePath).isDirectory()) {
5567
- filePath = path7.join(webDir, "index.html");
5606
+ if (!fs8.existsSync(filePath) || fs8.statSync(filePath).isDirectory()) {
5607
+ filePath = path8.join(webDir, "index.html");
5568
5608
  }
5569
- if (!fs7.existsSync(filePath)) {
5609
+ if (!fs8.existsSync(filePath)) {
5570
5610
  res.writeHead(404, { "Content-Type": "text/plain" });
5571
5611
  res.end("Esedre UI: index.html not found. Run npm run build first.");
5572
5612
  return;
5573
5613
  }
5574
- const ext = path7.extname(filePath).toLowerCase();
5614
+ const ext = path8.extname(filePath).toLowerCase();
5575
5615
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
5576
5616
  try {
5577
- const content = fs7.readFileSync(filePath);
5617
+ const content = fs8.readFileSync(filePath);
5578
5618
  res.writeHead(200, { "Content-Type": contentType });
5579
5619
  res.end(content);
5580
5620
  } catch (err) {
@@ -5596,8 +5636,8 @@ function startUiServer(port, webDir) {
5596
5636
 
5597
5637
  // src/server/apiServer.ts
5598
5638
  import http3 from "node:http";
5599
- import fs8 from "node:fs";
5600
- import path8 from "node:path";
5639
+ import fs9 from "node:fs";
5640
+ import path9 from "node:path";
5601
5641
  var MAX_JSON_BODY_BYTES = 10 * 1024 * 1024;
5602
5642
  function readJsonBody(req) {
5603
5643
  return new Promise((resolve, reject) => {
@@ -5893,16 +5933,16 @@ function createApiHandler(storage, workspaceRoot) {
5893
5933
  return true;
5894
5934
  }
5895
5935
  const filePath = reqStorage.getAttachmentPath ? reqStorage.getAttachmentPath(ticketId, file) : null;
5896
- if (!filePath || !fs8.existsSync(filePath)) {
5936
+ if (!filePath || !fs9.existsSync(filePath)) {
5897
5937
  sendJson(res, 404, { error: "Attachment not found." });
5898
5938
  return true;
5899
5939
  }
5900
- const stat = fs8.statSync(filePath);
5940
+ const stat = fs9.statSync(filePath);
5901
5941
  if (!stat.isFile()) {
5902
5942
  sendJson(res, 404, { error: "Attachment not found." });
5903
5943
  return true;
5904
5944
  }
5905
- const ext = path8.extname(filePath).toLowerCase();
5945
+ const ext = path9.extname(filePath).toLowerCase();
5906
5946
  const mimeTypes = {
5907
5947
  ".png": "image/png",
5908
5948
  ".jpg": "image/jpeg",
@@ -5920,7 +5960,7 @@ function createApiHandler(storage, workspaceRoot) {
5920
5960
  "Content-Length": stat.size,
5921
5961
  "Cache-Control": "public, max-age=86400"
5922
5962
  });
5923
- fs8.createReadStream(filePath).pipe(res);
5963
+ fs9.createReadStream(filePath).pipe(res);
5924
5964
  return true;
5925
5965
  }
5926
5966
  if (pathname === "/api/planning/plan-history") {
@@ -6211,35 +6251,35 @@ function startApiServer(port, storage, workspaceRoot) {
6211
6251
 
6212
6252
  // src/server/gateway.ts
6213
6253
  var __filename2 = fileURLToPath2(import.meta.url);
6214
- var __dirname2 = path9.dirname(__filename2);
6254
+ var __dirname2 = path10.dirname(__filename2);
6215
6255
  function resolveWebDir(explicitWebDir, workspaceRoot) {
6216
- if (explicitWebDir && fs9.existsSync(path9.join(explicitWebDir, "index.html"))) {
6256
+ if (explicitWebDir && fs10.existsSync(path10.join(explicitWebDir, "index.html"))) {
6217
6257
  return explicitWebDir;
6218
6258
  }
6219
6259
  const candidates = [
6220
6260
  // 1. Packaged layout right next to executable (e.g. tools/web)
6221
- path9.resolve(__dirname2, "web"),
6261
+ path10.resolve(__dirname2, "web"),
6222
6262
  // 2. From workspace root: <workspaceRoot>/dist/web
6223
- workspaceRoot ? path9.resolve(workspaceRoot, "dist", "web") : "",
6263
+ workspaceRoot ? path10.resolve(workspaceRoot, "dist", "web") : "",
6224
6264
  // 3. In-repo legacy subfolder: <workspaceRoot>/esedre/dist/web
6225
- workspaceRoot ? path9.resolve(workspaceRoot, "esedre", "dist", "web") : "",
6265
+ workspaceRoot ? path10.resolve(workspaceRoot, "esedre", "dist", "web") : "",
6226
6266
  // 4. Sibling standalone repository from workspace root: <workspaceRoot>/../esedre/dist/web
6227
- workspaceRoot ? path9.resolve(workspaceRoot, "..", "esedre", "dist", "web") : "",
6267
+ workspaceRoot ? path10.resolve(workspaceRoot, "..", "esedre", "dist", "web") : "",
6228
6268
  // 5. Packaged layout inside dist/web
6229
- path9.resolve(__dirname2, "dist", "web"),
6269
+ path10.resolve(__dirname2, "dist", "web"),
6230
6270
  // 6. Sibling standalone repository from __dirname: <__dirname>/../../esedre/dist/web
6231
- path9.resolve(__dirname2, "..", "..", "esedre", "dist", "web"),
6271
+ path10.resolve(__dirname2, "..", "..", "esedre", "dist", "web"),
6232
6272
  // 7. Source tree layout: esedre/src/server/ -> esedre/dist/web
6233
- path9.resolve(__dirname2, "..", "..", "dist", "web"),
6273
+ path10.resolve(__dirname2, "..", "..", "dist", "web"),
6234
6274
  // 8. Legacy layout from __dirname
6235
- path9.resolve(__dirname2, "..", "esedre", "dist", "web")
6275
+ path10.resolve(__dirname2, "..", "esedre", "dist", "web")
6236
6276
  ].filter(Boolean);
6237
6277
  for (const candidate of candidates) {
6238
- if (fs9.existsSync(path9.join(candidate, "index.html"))) {
6278
+ if (fs10.existsSync(path10.join(candidate, "index.html"))) {
6239
6279
  return candidate;
6240
6280
  }
6241
6281
  }
6242
- return candidates[0] || path9.resolve(__dirname2, "web");
6282
+ return candidates[0] || path10.resolve(__dirname2, "web");
6243
6283
  }
6244
6284
  function proxyRequest(req, res, targetPort, rewritePath) {
6245
6285
  const targetPath = rewritePath ? rewritePath(req.url || "/") : req.url || "/";
@@ -6362,7 +6402,7 @@ ${colors.bold}ROADMAP COMMANDS (Pair Programming & LLM Agents):${colors.reset}
6362
6402
  ${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
6403
  Mutate ticket status, type, priority, title, complexity, effort, active state, or feature flag with OCC protection.
6364
6404
 
6365
- ${colors.bold}comment${colors.reset} <id> ["<text>"] [--text "..."] [--author "..."] [--json]
6405
+ ${colors.bold}comment${colors.reset} <id> ["<text>"] [--file <path>] [--text "..."] [--author "..."] [--json]
6366
6406
  Append a research finding, test verification, or note to ticket history.
6367
6407
 
6368
6408
  ${colors.bold}link${colors.reset} <sourceId> <relation> <targetId> [--author "..."] [--json]
@@ -6439,7 +6479,7 @@ ${colors.bold}OPTIONS:${colors.reset}
6439
6479
  --blocked Filter tickets that are blocked by uncompleted tickets.
6440
6480
  --linked-to <key> Filter tickets that are linked to a specific ticket.
6441
6481
  --detail "<md>" Specification markdown for ticket detail during create or update.
6442
- --file <path> Path to markdown file for detail (create, update) or implementation plan (plan).
6482
+ --file <path> Path to markdown file for detail (create, update), implementation plan (plan), or comment (comment).
6443
6483
  --json Output raw machine-readable JSON (strongly recommended for autonomous LLM coding agents).
6444
6484
  --last-hash <hash> Optimistic concurrency control: last known sha1 hash of the ticket from 'get'.
6445
6485
  --force Bypass optimistic concurrency last-hash conflict checks on writes.
@@ -6512,7 +6552,7 @@ function printDuplicateProjectWarnings(storage, isJson) {
6512
6552
  const warnings = storage.getDuplicateProjectWarnings ? storage.getDuplicateProjectWarnings() : [];
6513
6553
  for (const warn of warnings) {
6514
6554
  console.error(
6515
- `${colors.yellow}\u26A0\uFE0F Warning: Duplicate project code '${warn.code}' detected across multiple data hubs (${path10.basename(warn.firstHub)} and ${path10.basename(warn.duplicateHub)}). Duplicate project codes across hubs are not supported; only '${path10.basename(warn.firstHub)}' is active.${colors.reset}`
6555
+ `${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
6556
  );
6517
6557
  }
6518
6558
  }
@@ -6555,7 +6595,7 @@ async function main() {
6555
6595
  switch (command) {
6556
6596
  case "init": {
6557
6597
  const rawTarget = positionals[0];
6558
- const targetDir = rawTarget ? path10.resolve(rawTarget) : discovered.workspaceRoot;
6598
+ const targetDir = rawTarget ? path11.resolve(rawTarget) : discovered.workspaceRoot;
6559
6599
  const rawHubFlag = flags["hub"];
6560
6600
  const hubArg = typeof rawHubFlag === "string" ? rawHubFlag : void 0;
6561
6601
  const isHubRequested = Boolean(rawHubFlag);
@@ -6634,7 +6674,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
6634
6674
  console.error(`${colors.red}Error: ${nameVal.error}${colors.reset}`);
6635
6675
  process.exit(1);
6636
6676
  }
6637
- const isTargetHub = fs10.existsSync(path10.join(targetDir, "projects.json")) || fs10.existsSync(path10.join(targetDir, "projects")) && !fs10.existsSync(path10.join(targetDir, ".esedre", "esedre.json"));
6677
+ const isTargetHub = fs11.existsSync(path11.join(targetDir, "projects.json")) || fs11.existsSync(path11.join(targetDir, "projects")) && !fs11.existsSync(path11.join(targetDir, ".esedre", "esedre.json"));
6638
6678
  const isWorkspaceless = isHubRequested || isTargetHub || Boolean(discovered.config?.projectCode) && !rawTarget && discovered.config?.projectCode?.toLowerCase() !== projectCode.toLowerCase();
6639
6679
  let projectNewlyRegistered = false;
6640
6680
  let registeredProj;
@@ -6705,7 +6745,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
6705
6745
  const viteConfigPath = findViteConfig(targetDir);
6706
6746
  if (viteConfigPath && setupProxy === void 0 && !isJson) {
6707
6747
  try {
6708
- const rawContent = fs10.readFileSync(viteConfigPath, "utf-8");
6748
+ const rawContent = fs11.readFileSync(viteConfigPath, "utf-8");
6709
6749
  const alreadyConfigured = rawContent.includes("'/esedre'") || rawContent.includes('"/esedre"');
6710
6750
  if (!alreadyConfigured) {
6711
6751
  if (isYes) {
@@ -6715,7 +6755,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
6715
6755
  const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
6716
6756
  try {
6717
6757
  const answer = await rl.question(
6718
- `${colors.cyan}?${colors.reset} Detected ${colors.bold}${path10.basename(viteConfigPath)}${colors.reset}. Would you like to configure a reverse proxy for ${colors.bold}/esedre${colors.reset} targeting http://127.0.0.1:${port}? [Y/n] `
6758
+ `${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
6759
  );
6720
6760
  const trimmed = answer.trim().toLowerCase();
6721
6761
  setupProxy = trimmed === "" || trimmed === "y" || trimmed === "yes";
@@ -6731,10 +6771,10 @@ ${colors.bold}Next Steps:${colors.reset}`);
6731
6771
  let hubRelPath = void 0;
6732
6772
  if (rawHub) {
6733
6773
  const singleHub = Array.isArray(rawHub) ? rawHub[0] : rawHub;
6734
- const resolvedHub = path10.resolve(expandHome(singleHub));
6735
- if (fs10.existsSync(resolvedHub)) {
6736
- const rel = path10.relative(targetDir, resolvedHub).replace(/\\/g, "/");
6737
- const isSubdir = !rel.startsWith("..") && !path10.isAbsolute(rel);
6774
+ const resolvedHub = path11.resolve(expandHome(singleHub));
6775
+ if (fs11.existsSync(resolvedHub)) {
6776
+ const rel = path11.relative(targetDir, resolvedHub).replace(/\\/g, "/");
6777
+ const isSubdir = !rel.startsWith("..") && !path11.isAbsolute(rel);
6738
6778
  if (isSubdir || Boolean(flags["data-dir"])) {
6739
6779
  hubRelPath = rel.startsWith(".") ? rel : "./" + rel;
6740
6780
  }
@@ -6908,7 +6948,7 @@ ${colors.bold}Next Steps:${colors.reset}`);
6908
6948
  }
6909
6949
  console.log(`
6910
6950
  ${colors.bold}${colors.cyan}Esedre Central Configuration${colors.reset}`);
6911
- console.log(` \u2022 Config File: ${configPath} ${fs10.existsSync(configPath) ? colors.green + "(active)" + colors.reset : colors.dim + "(default)" + colors.reset}`);
6951
+ console.log(` \u2022 Config File: ${configPath} ${fs11.existsSync(configPath) ? colors.green + "(active)" + colors.reset : colors.dim + "(default)" + colors.reset}`);
6912
6952
  console.log(` \u2022 Default Port: ${globalCfg.port || DEFAULT_ESEDRE_PORT}`);
6913
6953
  console.log(`
6914
6954
  ${colors.bold}Data Hubs (dataDir):${colors.reset}`);
@@ -6978,7 +7018,7 @@ ${colors.bold}${colors.green}\u2714 Esedre multi-workspace upgrade completed acr
6978
7018
  }
6979
7019
  return;
6980
7020
  }
6981
- const targetDir = positionals[0] ? path10.resolve(positionals[0]) : discovered.workspaceRoot;
7021
+ const targetDir = positionals[0] ? path11.resolve(positionals[0]) : discovered.workspaceRoot;
6982
7022
  const targetDiscovered = targetDir === discovered.workspaceRoot ? discovered : findEsedreConfig(targetDir, { fallbackToGlobal: true });
6983
7023
  const res = configureWorkspace(targetDir, {
6984
7024
  projectCode: targetDiscovered.config?.projectCode,
@@ -7107,7 +7147,7 @@ ${colors.yellow}\u26A0\uFE0F Warning: Daemon is running v${status.version}, but
7107
7147
  }
7108
7148
  } else {
7109
7149
  console.log(`${colors.yellow}\u25CB Esedre daemon is STOPPED (port ${status.port})${colors.reset}`);
7110
- if (status.logFile && fs10.existsSync(status.logFile)) {
7150
+ if (status.logFile && fs11.existsSync(status.logFile)) {
7111
7151
  console.log(` \u2022 Recent Logs: ${status.logFile}`);
7112
7152
  }
7113
7153
  }
@@ -7312,11 +7352,11 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
7312
7352
  if (setPlan || filePath) {
7313
7353
  let content = setPlan;
7314
7354
  if (filePath) {
7315
- if (!fs10.existsSync(filePath)) {
7355
+ if (!fs11.existsSync(filePath)) {
7316
7356
  console.error(`${colors.red}Error: Plan file "${filePath}" not found.${colors.reset}`);
7317
7357
  process.exit(1);
7318
7358
  }
7319
- content = fs10.readFileSync(filePath, "utf-8");
7359
+ content = fs11.readFileSync(filePath, "utf-8");
7320
7360
  }
7321
7361
  await storage.savePlan(id, content, lastHash);
7322
7362
  if (isJson) {
@@ -7388,8 +7428,8 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
7388
7428
  const milestone = flags["milestone"] || flags["m"] || void 0;
7389
7429
  let detailMarkdown = detailArg;
7390
7430
  if (fileArg) {
7391
- const filePath = path10.resolve(fileArg);
7392
- if (!fs10.existsSync(filePath)) {
7431
+ const filePath = path11.resolve(fileArg);
7432
+ if (!fs11.existsSync(filePath)) {
7393
7433
  if (isJson) {
7394
7434
  console.error(JSON.stringify({ error: `Detail file "${fileArg}" not found` }));
7395
7435
  } else {
@@ -7397,7 +7437,7 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
7397
7437
  }
7398
7438
  process.exit(1);
7399
7439
  }
7400
- detailMarkdown = fs10.readFileSync(filePath, "utf-8");
7440
+ detailMarkdown = fs11.readFileSync(filePath, "utf-8");
7401
7441
  }
7402
7442
  const created = await storage.createTicket({
7403
7443
  title,
@@ -7467,8 +7507,8 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
7467
7507
  const fileArg = flags["file"];
7468
7508
  let detailMarkdown = detailArg ? normalizeDashesAndMojibake(detailArg) : void 0;
7469
7509
  if (fileArg) {
7470
- const filePath = path10.resolve(fileArg);
7471
- if (!fs10.existsSync(filePath)) {
7510
+ const filePath = path11.resolve(fileArg);
7511
+ if (!fs11.existsSync(filePath)) {
7472
7512
  if (isJson) {
7473
7513
  console.error(JSON.stringify({ error: `Detail file "${fileArg}" not found` }));
7474
7514
  } else {
@@ -7476,7 +7516,7 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
7476
7516
  }
7477
7517
  process.exit(1);
7478
7518
  }
7479
- detailMarkdown = normalizeDashesAndMojibake(fs10.readFileSync(filePath, "utf-8"));
7519
+ detailMarkdown = normalizeDashesAndMojibake(fs11.readFileSync(filePath, "utf-8"));
7480
7520
  }
7481
7521
  if (detailMarkdown !== void 0) updates.detailMarkdown = detailMarkdown;
7482
7522
  const updated = await storage.updateTicket(lookupKey, updates, lastHash);
@@ -7634,10 +7674,30 @@ ${colors.bold}Tickets:${colors.reset}`);
7634
7674
  }
7635
7675
  const projectFlag = flags["project"];
7636
7676
  const lookupKey = projectFlag && /^\d+$/.test(idStr) ? `${projectFlag}-${idStr}` : idStr;
7637
- const positionalText = positionals.slice(1).join(" ").trim();
7638
- const text = flags["text"] || (positionalText || void 0);
7677
+ let text;
7678
+ const filePath = flags["file"];
7679
+ if (filePath) {
7680
+ const resolvedPath = path11.resolve(process.cwd(), filePath);
7681
+ if (!fs11.existsSync(resolvedPath)) {
7682
+ console.error(`${colors.red}Error: File not found: ${filePath}${colors.reset}`);
7683
+ process.exit(1);
7684
+ }
7685
+ if (!fs11.statSync(resolvedPath).isFile()) {
7686
+ console.error(`${colors.red}Error: Path is not a file: ${filePath}${colors.reset}`);
7687
+ process.exit(1);
7688
+ }
7689
+ const fileContent = fs11.readFileSync(resolvedPath, "utf-8");
7690
+ if (!fileContent.trim()) {
7691
+ console.error(`${colors.red}Error: Comment file is empty: ${filePath}${colors.reset}`);
7692
+ process.exit(1);
7693
+ }
7694
+ text = fileContent;
7695
+ } else {
7696
+ const positionalText = positionals.slice(1).join(" ").trim();
7697
+ text = flags["text"] || (positionalText || void 0);
7698
+ }
7639
7699
  if (!text) {
7640
- console.error(`${colors.red}Error: Comment text is required (e.g. ese comment 96 "Verified tests" or --text "...")${colors.reset}`);
7700
+ 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
7701
  process.exit(1);
7642
7702
  }
7643
7703
  const author = flags["author"] || "User";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esedre",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Esedre: Developer roadmap, ticketing, and LLM coding partner coordination platform",
5
5
  "homepage": "https://esedre.com",
6
6
  "repository": {