deuk-agent-rule 2.2.1 → 2.3.1

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/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "deuk-agent-rule",
3
- "version": "2.2.1",
4
- "type": "module",
5
- "description": "DeukAgentRules",
3
+ "version": "2.3.1",
4
+ "description": "DeukAgentRules: generic AGENTS.md + .cursor rule templates with init/merge CLI (npm name: deuk-agent-rule).",
6
5
  "keywords": [
7
6
  "agents-md",
8
7
  "cursor-rules",
@@ -49,5 +48,8 @@
49
48
  },
50
49
  "devDependencies": {
51
50
  "commit-and-tag-version": "^12.7.1"
51
+ },
52
+ "dependencies": {
53
+ "ejs": "^5.0.2"
52
54
  }
53
55
  }
File without changes
@@ -12,11 +12,18 @@ export function parseTicketArgs(argv) {
12
12
  else if (a === "--from") out.from = argv[++i];
13
13
  else if (a === "--ref") out.ref = argv[++i];
14
14
  else if (a === "--limit") out.limit = Number(argv[++i]);
15
+ else if (a === "--submodule") out.submodule = argv[++i];
15
16
  else if (a === "--latest") out.latest = true;
16
17
  else if (a === "--path-only") out.pathOnly = true;
17
18
  else if (a === "--print-content") out.printContent = true;
18
19
  else if (a === "--all") out.all = true;
19
20
  else if (a === "--status") out.status = argv[++i];
21
+ else if (a === "--archived") out.archived = true;
22
+ else if (a === "--report") out.report = argv[++i];
23
+ else if (a === "--json") out.json = true;
24
+ else if (a === "--remote") out.remote = argv[++i];
25
+ else if (a === "--sync") out.sync = true;
26
+ else if (a === "--no-sync") out.sync = false;
20
27
  }
21
28
  return out;
22
29
  }
@@ -37,6 +44,10 @@ export function parseArgs(argv) {
37
44
  else if (a === "--rules") out.rules = argv[++i];
38
45
  else if (a === "--cursorrules") out.cursorrules = argv[++i];
39
46
  else if (a === "--append-if-no-markers") out.appendIfNoMarkers = true;
47
+ else if (a === "--json") out.json = true;
48
+ else if (a === "--remote") out.remote = argv[++i];
49
+ else if (a === "--sync") out.sync = true;
50
+ else if (a === "--no-sync") out.sync = false;
40
51
  else if (a === "-h" || a === "--help") out.help = true;
41
52
  }
42
53
  return out;
@@ -1,8 +1,25 @@
1
1
  import { join } from "path";
2
- import { existsSync, readFileSync, writeFileSync } from "fs";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync } from "fs";
3
3
  import { resolveMarkers, resolveCursorrulesMarkers, applyAgents, applyRules, applyCursorrules, readBundleAgents } from "./merge-logic.mjs";
4
4
  import { ensureTicketDirAndGitignore } from "./cli-init-logic.mjs";
5
- import { loadInitConfig, writeInitConfig } from "./cli-prompts.mjs";
5
+ import { loadInitConfig, writeInitConfig } from "./cli-utils.mjs";
6
+ import { runInteractive } from "./cli-prompts.mjs";
7
+
8
+ function syncTemplates(cwd, bundleRoot, dryRun) {
9
+ const tplSrcDir = join(bundleRoot, "templates");
10
+ const tplDestDir = join(cwd, ".deuk-agent-templates");
11
+ if (!existsSync(tplSrcDir)) return;
12
+ if (!dryRun) mkdirSync(tplDestDir, { recursive: true });
13
+
14
+ for (const name of readdirSync(tplSrcDir)) {
15
+ if (!name.endsWith(".md")) continue;
16
+ const src = join(tplSrcDir, name);
17
+ const dest = join(tplDestDir, name);
18
+ if (!dryRun) copyFileSync(src, dest);
19
+ console.log(`template synced: ${dest}`);
20
+ }
21
+ }
22
+
6
23
 
7
24
  export async function runInit(opts, bundleRoot) {
8
25
  const markers = resolveMarkers(opts);
@@ -33,6 +50,12 @@ export async function runInit(opts, bundleRoot) {
33
50
  console.log(`.cursorrules: ${crResult.action} (${crResult.mode || ""})`);
34
51
 
35
52
  ensureTicketDirAndGitignore(opts);
53
+ syncTemplates(opts.cwd, bundleRoot, opts.dryRun);
54
+
55
+ // If no config exists, save the derived/default config to ensure persistency
56
+ if (!loadInitConfig(opts.cwd)) {
57
+ writeInitConfig(opts.cwd, opts);
58
+ }
36
59
  }
37
60
 
38
61
  export function runMerge(opts, bundleRoot) {
@@ -62,4 +85,6 @@ export function runMerge(opts, bundleRoot) {
62
85
  dryRun: opts.dryRun, backup: opts.backup
63
86
  });
64
87
  console.log(`.cursorrules: ${crResult.action} (${crResult.mode || ""})`);
88
+
89
+ syncTemplates(opts.cwd, bundleRoot, opts.dryRun);
65
90
  }
@@ -12,6 +12,10 @@ export function ensureTicketDirAndGitignore(opts) {
12
12
  if (opts.dryRun) return;
13
13
 
14
14
  mkdirSync(ticketPath, { recursive: true });
15
+ if (opts.shareTickets) {
16
+ console.log(`[INIT] Ticket sharing enabled. Skipping .gitignore entry for ${TICKET_DIR_NAME}/`);
17
+ return;
18
+ }
15
19
 
16
20
  let gi = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf8") : "";
17
21
  if (!gi.includes(ignoreLine)) {
@@ -1,9 +1,7 @@
1
1
  import { createInterface } from "readline";
2
- import { existsSync, readFileSync, writeFileSync } from "fs";
2
+ import { existsSync, readFileSync } from "fs";
3
3
  import { join } from "path";
4
-
5
- const INIT_CONFIG_FILENAME = ".deuk-agent-rule.config.json";
6
- const INIT_CONFIG_VERSION = 1;
4
+ import { loadInitConfig, writeInitConfig, STACKS, AGENT_TOOLS } from "./cli-utils.mjs";
7
5
 
8
6
  export async function ask(rl, question) {
9
7
  return new Promise((resolve) => rl.question(question, resolve));
@@ -41,48 +39,7 @@ export async function selectMany(rl, prompt, choices) {
41
39
  }
42
40
  }
43
41
 
44
- export function loadInitConfig(cwd) {
45
- const p = join(cwd, INIT_CONFIG_FILENAME);
46
- if (!existsSync(p)) return null;
47
- try {
48
- const j = JSON.parse(readFileSync(p, "utf8"));
49
- if (j.version !== INIT_CONFIG_VERSION) return null;
50
- return j;
51
- } catch {
52
- return null;
53
- }
54
- }
55
-
56
- export function writeInitConfig(cwd, opts) {
57
- const p = join(cwd, INIT_CONFIG_FILENAME);
58
- const body = {
59
- version: INIT_CONFIG_VERSION,
60
- stack: opts.stack,
61
- agentTools: opts.agentTools,
62
- agentsMode: opts.agents ?? "inject",
63
- updatedAt: new Date().toISOString(),
64
- };
65
- writeFileSync(p, JSON.stringify(body, null, 2) + "\n", "utf8");
66
- }
67
-
68
- export const STACKS = [
69
- { label: "Unity / C#", value: "unity" },
70
- { label: "Next.js + C#", value: "nextjs-dotnet" },
71
- { label: "Web (React / Vue / general)", value: "web" },
72
- { label: "Java / Spring Boot", value: "java" },
73
- { label: "Other / skip", value: "other" },
74
- ];
75
42
 
76
- export const AGENT_TOOLS = [
77
- { label: "Cursor", value: "cursor" },
78
- { label: "GitHub Copilot", value: "copilot" },
79
- { label: "Gemini / Antigravity", value: "gemini" },
80
- { label: "Claude (Cursor / Claude Code)", value: "claude" },
81
- { label: "Windsurf", value: "windsurf" },
82
- { label: "JetBrains AI Assistant", value: "jetbrains" },
83
- { label: "All of the above", value: "all" },
84
- { label: "Other / skip", value: "other" },
85
- ];
86
43
 
87
44
  export async function runInteractive(opts) {
88
45
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -91,6 +48,7 @@ export async function runInteractive(opts) {
91
48
 
92
49
  const stack = await selectOne(rl, "What is your primary tech stack?", STACKS);
93
50
  const tools = await selectMany(rl, "Which agent tools do you use?", AGENT_TOOLS);
51
+ const shareTickets = await askYesNo("Do you want to share (git-track) tickets for this repository?", false);
94
52
 
95
53
  const targetAgents = join(opts.cwd, "AGENTS.md");
96
54
  let agentsDefault = "inject";
@@ -110,12 +68,26 @@ export async function runInteractive(opts) {
110
68
  }
111
69
  }
112
70
 
71
+ const remoteSync = opts.remoteSync !== undefined ? opts.remoteSync : (await askYesNo("Enable AI Pipeline remote synchronization? (Advanced)", false));
72
+ let pipelineUrl = opts.pipelineUrl || "";
73
+ if (remoteSync && !pipelineUrl) {
74
+ pipelineUrl = (await ask(rl, "Enter AI Pipeline Endpoint URL: ")).trim();
75
+ }
76
+
113
77
  opts.agents = opts.agents ?? agentsDefault;
114
78
  opts.stack = stack;
115
79
  opts.agentTools = tools;
80
+ opts.shareTickets = shareTickets;
81
+ opts.remoteSync = remoteSync;
82
+ opts.pipelineUrl = pipelineUrl;
83
+
84
+ writeInitConfig(opts.cwd, opts);
116
85
 
117
86
  console.log("\n Stack : " + stack);
118
87
  console.log(" Tools : " + (tools.join(", ") || "none"));
88
+ console.log(" Share Tickets: " + (opts.shareTickets ? "Yes (Shared)" : "No (Private)"));
89
+ console.log(" Remote Sync: " + (opts.remoteSync ? "Enabled" : "Disabled"));
90
+ if (opts.remoteSync) console.log(" Pipeline URL: " + opts.pipelineUrl);
119
91
  console.log(" AGENTS: " + opts.agents + "\n");
120
92
  } finally {
121
93
  rl.close();
@@ -1,7 +1,9 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
2
- import { basename, join } from "path";
3
- import { toSlug, toRepoRelativePath, inferRefTitleAndTopic, resolveReferencedTicketPath } from "./cli-utils.mjs";
4
- import { TICKET_DIR_NAME, appendTicketEntry, rebuildTicketIndexFromTopicFilesIfNeeded, detectConsumerTicketDir } from "./cli-ticket-logic.mjs";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, copyFileSync, readdirSync, rmSync, statSync } from "fs";
2
+ import { basename, join, dirname, relative, resolve } from "path";
3
+ import { toSlug, toRepoRelativePath, inferRefTitleAndTopic, resolveReferencedTicketPath, toPosixPath, stringifyFrontMatter } from "./cli-utils.mjs";
4
+ import { TICKET_DIR_NAME, appendTicketEntry, rebuildTicketIndexFromTopicFilesIfNeeded, detectConsumerTicketDir, readTicketIndexJson, writeTicketIndexJson, writeTicketListFile, syncActiveTicketPointer, generateTicketId, syncToPipeline } from "./cli-ticket-logic.mjs";
5
+ import { loadInitConfig } from "./cli-utils.mjs";
6
+ import ejs from "ejs";
5
7
 
6
8
  import { createInterface } from "readline";
7
9
  import { selectOne } from "./cli-prompts.mjs";
@@ -19,21 +21,60 @@ export async function runTicketCreate(opts) {
19
21
  path = resolveReferencedTicketPath(opts);
20
22
  source = "ticket-reference";
21
23
  } else {
22
- let body = opts.content ? String(opts.content).replace(/\\n/g, '\n') : "";
23
- if (!body && opts.from) body = readFileSync(join(opts.cwd, opts.from), "utf8");
24
- const abs = join(opts.cwd, TICKET_DIR_NAME, group, `${topic}-${Date.now()}.md`);
25
- mkdirSync(join(opts.cwd, TICKET_DIR_NAME, group), { recursive: true });
24
+ let tplText = "";
25
+ const consumerTplPath = join(opts.cwd, ".deuk-agent-templates", "TICKET_TEMPLATE.md");
26
+ const bundleTplPath = join(new URL('.', import.meta.url).pathname, "..", "bundle", "templates", "TICKET_TEMPLATE.md");
27
+
28
+ if (existsSync(consumerTplPath)) tplText = readFileSync(consumerTplPath, "utf8");
29
+ else if (existsSync(bundleTplPath)) tplText = readFileSync(bundleTplPath, "utf8");
30
+ else throw new Error("ticket create: Template not found. Refusing to create an empty ticket.");
31
+
32
+ // Find nearest or create in CWD if missing
33
+ const ticketDir = detectConsumerTicketDir(opts.cwd, { createIfMissing: true });
34
+ const abs = join(ticketDir, group, `${topic}-${Date.now()}.md`);
35
+ mkdirSync(join(ticketDir, group), { recursive: true });
26
36
  path = toRepoRelativePath(opts.cwd, abs);
27
- const marker = `\n\n<!-- Ticket (repo-relative): ${path} -->\n`;
28
- writeFileSync(abs, body.trimEnd() + marker, "utf8");
37
+
38
+ const ticketId = generateTicketId(title);
39
+ const meta = {
40
+ id: ticketId,
41
+ title,
42
+ topic,
43
+ status: "open",
44
+ submodule: opts.submodule || "",
45
+ project: opts.project || "global",
46
+ createdAt: new Date().toISOString(),
47
+ };
48
+
49
+ const ejsFrontMatter = `---
50
+ id: <%= meta.id %>
51
+ title: "<%- meta.title.replace(/"/g, '\\"') %>"
52
+ topic: <%= meta.topic %>
53
+ status: open
54
+ submodule: <%= meta.submodule %>
55
+ project: <%= meta.project %>
56
+ createdAt: <%= meta.createdAt %>
57
+ ---
58
+
59
+ `;
60
+ const finalContent = ejs.render(ejsFrontMatter + tplText, { meta });
61
+ writeFileSync(abs, finalContent, "utf8");
29
62
  source = "ticket-create";
63
+
64
+ // Remote Sync Hook
65
+ const config = loadInitConfig(opts.cwd);
66
+ if (config && config.remoteSync && config.pipelineUrl) {
67
+ syncToPipeline(config.pipelineUrl, { action: "create", ticket: meta });
68
+ }
69
+
70
+ appendTicketEntry(opts.cwd, {
71
+ id: ticketId,
72
+ title, topic, group, project: opts.project || "global",
73
+ createdAt: new Date().toISOString(), path, source
74
+ }, opts);
30
75
  }
31
76
 
32
- appendTicketEntry(opts.cwd, {
33
- id: `ticket_${Date.now()}`,
34
- title, topic, group, project: opts.project || "global",
35
- createdAt: new Date().toISOString(), path, source
36
- }, opts);
77
+ syncActiveTicketPointer(opts.cwd);
37
78
  }
38
79
 
39
80
  export async function runTicketList(opts) {
@@ -42,24 +83,64 @@ export async function runTicketList(opts) {
42
83
  throw new Error("No ticket system found. Please run 'npx deuk-agent-rule init' first.");
43
84
  }
44
85
  const index = rebuildTicketIndexFromTopicFilesIfNeeded(opts.cwd, opts);
86
+ syncActiveTicketPointer(opts.cwd);
45
87
  let rows = index.entries;
88
+
46
89
 
47
- if (!opts.all) {
48
- const targetStatus = opts.status || "open";
49
- rows = rows.filter(e => e.status === targetStatus);
90
+ if (opts.active) {
91
+ rows = rows.filter(e => e.status === "active" || e.status === "open");
92
+ } else if (opts.archived) {
93
+ rows = rows.filter(e => e.status === "archived");
94
+ } else if (!opts.all) {
95
+ // Default: major/active list (open or active)
96
+ rows = rows.filter(e => e.status === "open" || e.status === "active");
50
97
  }
51
98
 
52
99
  if (opts.group) rows = rows.filter(e => e.group === opts.group);
53
100
  if (opts.project) rows = rows.filter(e => e.project === opts.project);
101
+ if (opts.submodule) rows = rows.filter(e => e.submodule === opts.submodule);
54
102
 
55
- console.log("# STATUS GROUP PROJECT CREATED TITLE");
103
+ if (opts.json) {
104
+ console.log(JSON.stringify(rows, null, 2));
105
+ return;
106
+ }
107
+
108
+ console.log("# STATUS SUBMODULE GROUP PROJECT CREATED TITLE");
56
109
  rows.slice(0, opts.limit).forEach((e, idx) => {
57
110
  const stat = (e.status === "closed" ? "[x]" : "[ ]").padEnd(7);
111
+ const sub = (e.submodule || "-").padEnd(11);
58
112
  const safeTitle = String(e.title || e.topic || "").replace(/(\n|\\n)+/g, " ").slice(0, 50);
59
- console.log(`${String(idx+1).padEnd(2)} ${stat} ${e.group.padEnd(10)} ${e.project.padEnd(11)} ${e.createdAt.padEnd(24)} ${safeTitle}`);
113
+ console.log(`${String(idx+1).padEnd(2)} ${stat} ${sub} ${String(e.group||"").padEnd(10)} ${String(e.project||"").padEnd(11)} ${String(e.createdAt||"").padEnd(24)} ${safeTitle}`);
60
114
  });
61
115
  }
62
116
 
117
+ export async function runTicketMeta(opts) {
118
+ const index = rebuildTicketIndexFromTopicFilesIfNeeded(opts.cwd, opts);
119
+ const found = pickTicketEntry(opts, index);
120
+ if (!found) throw new Error("ticket meta: no matching ticket found");
121
+
122
+ if (opts.json) {
123
+ console.log(JSON.stringify(found, null, 2));
124
+ } else {
125
+ console.log(`Ticket Meta [${found.topic}]`);
126
+ Object.entries(found).forEach(([k, v]) => console.log(` ${k}: ${v}`));
127
+ }
128
+ }
129
+
130
+ export async function runTicketConnect(opts) {
131
+ const config = loadInitConfig(opts.cwd);
132
+ const url = opts.remote || config?.pipelineUrl;
133
+ if (!url) throw new Error("ticket connect: no pipeline URL configured or provided via --remote");
134
+
135
+ console.log(`Connecting to AI Pipeline at ${url} ...`);
136
+ const success = await syncToPipeline(url, { action: "ping", timestamp: new Date().toISOString() });
137
+ if (success) {
138
+ console.log("SUCCESS: Pipeline is reachable.");
139
+ } else {
140
+ console.error("FAILED: Could not connect to pipeline or returned non-OK status.");
141
+ }
142
+ }
143
+
63
144
  import { updateTicketEntryStatus } from "./cli-ticket-logic.mjs";
64
145
 
65
146
  export async function runTicketClose(opts) {
@@ -85,6 +166,7 @@ export async function runTicketClose(opts) {
85
166
  }
86
167
  opts.status = "closed";
87
168
  const entry = updateTicketEntryStatus(opts.cwd, opts);
169
+ syncActiveTicketPointer(opts.cwd);
88
170
  console.log(`ticket: closed -> ${entry.topic} (${entry.path})`);
89
171
  }
90
172
 
@@ -117,43 +199,115 @@ export async function runTicketUse(opts) {
117
199
  }
118
200
  }
119
201
 
120
- import { getLegacyMigrationCandidate, parseLegacyTicketMeta } from "./cli-ticket-logic.mjs";
121
- import { dirname } from "path";
122
202
 
123
- export async function runTicketMigrate(opts) {
124
- const candidate = getLegacyMigrationCandidate(opts.cwd);
125
- if (!candidate) {
126
- console.log("ticket: no legacy LATEST.md migration candidate found");
127
- return;
203
+
204
+ export function pickTicketEntry(opts, indexJson) {
205
+ const rows = [...indexJson.entries].sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")));
206
+ if (rows.length === 0) return null;
207
+ if (opts.topic) {
208
+ const key = String(opts.topic).toLowerCase();
209
+ return rows.find(e => String(e.topic || "").toLowerCase().includes(key)) || null;
128
210
  }
211
+ return rows[0];
212
+ }
129
213
 
130
- const { title, group, project } = parseLegacyTicketMeta(candidate.body);
131
- const topic = toSlug(title);
132
- const stamp = Date.now();
133
- const relPath = join(TICKET_DIR_NAME, group, `${topic}-${stamp}.md`);
134
- const absPath = join(opts.cwd, relPath);
214
+ export async function runTicketArchive(opts) {
215
+ if (!opts.latest && !opts.topic) {
216
+ if (process.stdout.isTTY) {
217
+ const index = rebuildTicketIndexFromTopicFilesIfNeeded(opts.cwd, opts);
218
+ const choices = index.entries
219
+ .filter(e => e.status !== "archived")
220
+ .map(e => ({ label: `[${e.group}] ${e.title}`, value: e.topic }));
221
+ if (choices.length > 0) {
222
+ const { createInterface } = await import("readline");
223
+ const { selectOne } = await import("./cli-prompts.mjs");
224
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
225
+ try {
226
+ opts.topic = await selectOne(rl, "Choose a ticket to archive (this will move the file to archive/):", choices);
227
+ } finally {
228
+ rl.close();
229
+ }
230
+ } else {
231
+ throw new Error("No active tickets found to archive.");
232
+ }
233
+ } else {
234
+ throw new Error("ticket archive requires --latest or --topic <prefix>");
235
+ }
236
+ }
237
+
238
+ const indexJson = rebuildTicketIndexFromTopicFilesIfNeeded(opts.cwd, opts);
239
+ const found = pickTicketEntry(opts, indexJson);
240
+ if (!found) throw new Error("ticket archive: no matching entry");
135
241
 
136
- if (opts.dryRun) {
137
- console.log("ticket: would migrate -> " + relPath);
138
- } else {
139
- mkdirSync(dirname(absPath), { recursive: true });
140
- const marker = `\n\n<!-- Ticket (repo-relative): ${relPath} -->\n`;
141
- writeFileSync(absPath, candidate.body.trimEnd() + marker, "utf8");
142
- console.log("ticket: migrated body -> " + relPath);
242
+ const absPath = join(opts.cwd, found.path);
243
+ if (!existsSync(absPath)) {
244
+ throw new Error("ticket archive: file not found " + found.path);
245
+ }
143
246
 
144
- appendTicketEntry(opts.cwd, {
145
- id: `ticket_migrated_${stamp}`,
146
- title,
147
- topic,
148
- group,
149
- project,
150
- createdAt: new Date().toISOString(),
151
- path: relPath,
152
- source: "ticket-migrate",
153
- }, opts);
154
- if (existsSync(candidate.latestPath)) {
155
- unlinkSync(candidate.latestPath);
156
- console.log("ticket: deleted legacy LATEST.md");
247
+ const archiveDir = join(opts.cwd, TICKET_DIR_NAME, "archive", found.group || "sub");
248
+ if (!opts.dryRun) mkdirSync(archiveDir, { recursive: true });
249
+
250
+ const fileName = found.path.split(/[/\\]/).pop();
251
+ const newAbsPath = join(archiveDir, fileName);
252
+ const bodyLines = readFileSync(absPath, "utf8").trimEnd().split(/\r?\n/);
253
+
254
+ if (opts.report) {
255
+ const reportSrc = resolve(opts.cwd, opts.report);
256
+ if (!existsSync(reportSrc)) {
257
+ throw new Error("ticket archive: report file not found " + opts.report);
157
258
  }
259
+ const reportDir = join(opts.cwd, TICKET_DIR_NAME, "reports");
260
+ if (!opts.dryRun) mkdirSync(reportDir, { recursive: true });
261
+
262
+ const reportDest = join(reportDir, `REPORT-${fileName}`);
263
+ if (!opts.dryRun) copyFileSync(reportSrc, reportDest);
264
+ console.log("ticket archive: copied report to " + toRepoRelativePath(opts.cwd, reportDest));
265
+
266
+ bodyLines.push("");
267
+ bodyLines.push("## šŸ“„ Attached Report");
268
+ const relativeLink = toPosixPath(relative(dirname(newAbsPath), reportDest));
269
+ bodyLines.push(`- [View Report](${relativeLink})`);
158
270
  }
271
+
272
+ if (opts.dryRun) {
273
+ console.log("ticket archive: would move " + toRepoRelativePath(opts.cwd, absPath) + " to " + toRepoRelativePath(opts.cwd, newAbsPath));
274
+ return;
275
+ }
276
+
277
+ writeFileSync(newAbsPath, bodyLines.join("\n") + "\n", "utf8");
278
+ rmSync(absPath);
279
+ console.log("ticket archive: moved ticket to " + toRepoRelativePath(opts.cwd, newAbsPath));
280
+
281
+ const entryIdx = indexJson.entries.findIndex(e => e.id === found.id);
282
+ if (entryIdx >= 0) {
283
+ indexJson.entries[entryIdx].status = "archived";
284
+ indexJson.entries[entryIdx].path = toRepoRelativePath(opts.cwd, newAbsPath);
285
+ indexJson.entries[entryIdx].updatedAt = new Date().toISOString();
286
+ }
287
+
288
+ writeTicketIndexJson(opts.cwd, indexJson, opts);
289
+ writeTicketListFile(opts.cwd, indexJson.entries, opts);
290
+ }
291
+
292
+ export async function runTicketReports(opts) {
293
+ const reportDir = join(opts.cwd, TICKET_DIR_NAME, "reports");
294
+ console.log("\nšŸ“„ Agent Reports:");
295
+ if (!existsSync(reportDir)) {
296
+ console.log(" No reports found.");
297
+ return;
298
+ }
299
+ const files = readdirSync(reportDir).filter(f => f.startsWith("REPORT-") && f.endsWith(".md"));
300
+ if (files.length === 0) {
301
+ console.log(" No reports found.");
302
+ return;
303
+ }
304
+
305
+ const sorted = files.sort((a, b) => {
306
+ return statSync(join(reportDir, b)).mtime.getTime() - statSync(join(reportDir, a)).mtime.getTime();
307
+ });
308
+
309
+ sorted.slice(0, opts.limit || 30).forEach(f => {
310
+ console.log(` - [${f}]`);
311
+ });
312
+ console.log("");
159
313
  }