deuk-agent-rule 2.5.13 → 3.3.2

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.
Files changed (44) hide show
  1. package/CHANGELOG.ko.md +74 -0
  2. package/CHANGELOG.md +138 -316
  3. package/README.ko.md +134 -154
  4. package/README.md +121 -153
  5. package/package.json +29 -7
  6. package/scripts/cli-args.mjs +87 -3
  7. package/scripts/cli-init-commands.mjs +1382 -223
  8. package/scripts/cli-init-logic.mjs +28 -16
  9. package/scripts/cli-prompts.mjs +13 -4
  10. package/scripts/cli-rule-compiler.mjs +44 -34
  11. package/scripts/cli-skill-commands.mjs +172 -0
  12. package/scripts/cli-telemetry-commands.mjs +429 -0
  13. package/scripts/cli-ticket-commands.mjs +1934 -161
  14. package/scripts/cli-ticket-index.mjs +298 -0
  15. package/scripts/cli-ticket-migration.mjs +320 -0
  16. package/scripts/cli-ticket-parser.mjs +207 -0
  17. package/scripts/cli-utils.mjs +381 -59
  18. package/scripts/cli.mjs +99 -19
  19. package/scripts/lint-md.mjs +247 -0
  20. package/scripts/lint-rules.mjs +143 -0
  21. package/scripts/merge-logic.mjs +13 -306
  22. package/scripts/plan-parser.mjs +53 -0
  23. package/templates/MODULE_RULE_TEMPLATE.md +11 -0
  24. package/templates/PROJECT_RULE.md +47 -0
  25. package/templates/TICKET_TEMPLATE.ko.md +21 -0
  26. package/templates/TICKET_TEMPLATE.md +21 -0
  27. package/templates/rules.d/deukcontext-mcp.md +31 -0
  28. package/templates/rules.d/platform-coexistence.md +29 -0
  29. package/templates/skills/context-recall/SKILL.md +25 -0
  30. package/templates/skills/generated-file-guard/SKILL.md +25 -0
  31. package/templates/skills/safe-refactor/SKILL.md +25 -0
  32. package/bundle/.cursorrules +0 -11
  33. package/bundle/AGENTS.md +0 -146
  34. package/bundle/gemini.md +0 -26
  35. package/bundle/rules/delivery-and-parallel-work.mdc +0 -26
  36. package/bundle/rules/git-commit.mdc +0 -24
  37. package/bundle/rules/multi-ai-workflow.mdc +0 -104
  38. package/bundle/rules.d/core-workflow.md +0 -48
  39. package/bundle/rules.d/deukrag-mcp.md +0 -37
  40. package/bundle/templates/MODULE_RULE_TEMPLATE.md +0 -24
  41. package/bundle/templates/TICKET_TEMPLATE.md +0 -58
  42. package/scripts/cli-ticket-logic.mjs +0 -568
  43. package/scripts/sync-bundle.mjs +0 -77
  44. package/scripts/sync-oss.mjs +0 -126
@@ -1,568 +0,0 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, unlinkSync, copyFileSync } from "fs";
2
- import { basename, dirname, join, relative, resolve } from "path";
3
- import { createHash } from "crypto";
4
- import { hostname as osHostname } from "os";
5
- import {
6
- toPosixPath, toRepoRelativePath, toSlug, formatTimestampForFile, makeEntryId,
7
- detectProjectFromBody, deriveTopicFromBaseName, parseFrontMatter, stringifyFrontMatter,
8
- loadInitConfig, findFileRecursively,
9
- AGENT_ROOT_DIR, TICKET_SUBDIR, TEMPLATE_SUBDIR, RULES_SUBDIR,
10
- TICKET_DIR_NAME, TICKET_INDEX_FILENAME, TICKET_LIST_FILENAME, TICKET_LIST_TEMPLATE_FILENAME
11
- } from "./cli-utils.mjs";
12
-
13
- const DEFAULT_TICKET_LIST_TEMPLATE = `# Ticket List
14
-
15
- > Source index: \`{{SOURCE_INDEX}}\`
16
-
17
- ## Latest
18
-
19
- {{LATEST_BLOCK}}
20
-
21
- ## Entries
22
-
23
- | # | Title | Group | Project | Created | Path |
24
- |---|---|---|---|---|---|
25
- {{ENTRIES_ROWS}}
26
-
27
- ## Commands
28
-
29
- \`\`\`bash
30
- {{CMD_LIST}}
31
- {{CMD_USE_LATEST}}
32
- \`\`\`
33
- `;
34
-
35
- export function detectConsumerTicketDir(startDir, opts = {}) {
36
- let curr = resolve(startDir);
37
- while (curr && curr !== dirname(curr)) {
38
- // Priority 1: New consolidated path
39
- const newPath = join(curr, AGENT_ROOT_DIR, TICKET_SUBDIR);
40
- if (existsSync(newPath)) return newPath;
41
-
42
- // Priority 2: Legacy path
43
- const legacyPath = join(curr, ".deuk-agent-ticket");
44
- if (existsSync(legacyPath)) return legacyPath;
45
-
46
- curr = dirname(curr);
47
- }
48
- // If not found and creation allowed (init), return default new local path.
49
- return opts.createIfMissing ? join(startDir, AGENT_ROOT_DIR, TICKET_SUBDIR) : null;
50
- }
51
-
52
- export function readTicketIndexJson(cwd) {
53
- const dir = detectConsumerTicketDir(cwd);
54
- if (!dir) return { version: 1, updatedAt: null, entries: [] };
55
- const p = join(dir, TICKET_INDEX_FILENAME);
56
- if (!existsSync(p)) {
57
- return { version: 1, updatedAt: null, entries: [] };
58
- }
59
- try {
60
- const j = JSON.parse(readFileSync(p, "utf8"));
61
- const entries = Array.isArray(j.entries) ? j.entries.map(e => ({ ...e, status: e.status || "open" })) : [];
62
- return { version: 1, updatedAt: j.updatedAt ?? null, activeTicketId: j.activeTicketId ?? null, entries };
63
- } catch (err) {
64
- console.error(`[ERROR] Failed to parse ${TICKET_INDEX_FILENAME} at ${p}:`, err.message);
65
- // Return empty but do NOT overwrite immediately unless forced
66
- return { version: 1, updatedAt: null, activeTicketId: null, entries: [], _corrupt: true };
67
- }
68
- }
69
-
70
- export function writeTicketIndexJson(cwd, indexJson, opts = {}) {
71
- if (indexJson._corrupt && !opts.force) {
72
- console.error(`[ABORT] Refusing to overwrite potentially corrupt ${TICKET_INDEX_FILENAME}. Use --force to override.`);
73
- return;
74
- }
75
- const dir = detectConsumerTicketDir(cwd, { createIfMissing: true });
76
- const p = join(dir, TICKET_INDEX_FILENAME);
77
- if (opts.dryRun) return;
78
- mkdirSync(dir, { recursive: true });
79
- const out = { ...indexJson };
80
- delete out._corrupt;
81
- writeFileSync(p, JSON.stringify(out, null, 2) + "\n", "utf8");
82
- }
83
-
84
- export function renderTicketListMarkdown(cwd, entries) {
85
- const sorted = [...entries].sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")));
86
- const latest = sorted.find(e => e.status !== "archived") || sorted[0] || null;
87
-
88
- const ticketDir = detectConsumerTicketDir(cwd, { createIfMissing: true });
89
- const templatePath = join(ticketDir, TICKET_LIST_TEMPLATE_FILENAME);
90
- const template = existsSync(templatePath) ? readFileSync(templatePath, "utf8") : DEFAULT_TICKET_LIST_TEMPLATE;
91
-
92
- let latestBlock = "- No active ticket entries yet.";
93
- if (latest) {
94
- const absPath = join(cwd, latest.path);
95
- const fileUri = `file://${toPosixPath(absPath)}`;
96
- const safeLatestTitle = String(latest.title || "").replace(/\[|\]/g, '').replace(/\n/g, ' ');
97
- latestBlock = `- [${safeLatestTitle}](${fileUri})\n- status: \`${latest.status}\` / group: \`${latest.group}\` / project: \`${latest.project}\``;
98
- }
99
-
100
- const activeRows = sorted.filter(e => e.status !== "archived").map((e, i) => renderLine(e, i, ticketDir, cwd));
101
- const archivedRows = sorted.filter(e => e.status === "archived").slice(0, 50).map((e, i) => renderLine(e, i, ticketDir, cwd));
102
-
103
- let combinedRows = "### 🚀 Active Tickets\n\n| # | Status | Pri | Title | Group | Project | Created | Path |\n|---|---|---|---|---|---|---|---|\n" +
104
- (activeRows.join("\n") || "| - | - | - | No active tickets | - | - | - | - |") +
105
- "\n\n### 📦 Archived Tickets\n\n| # | Status | Pri | Title | Group | Project | Created | Path |\n|---|---|---|---|---|---|---|---|\n" +
106
- (archivedRows.join("\n") || "| - | - | - | No archived tickets | - | - | - | - |");
107
-
108
- return template
109
- .replaceAll("{{SOURCE_INDEX}}", `${toRepoRelativePath(cwd, ticketDir)}/${TICKET_INDEX_FILENAME}`)
110
- .replaceAll("{{LATEST_BLOCK}}", latestBlock)
111
- .replaceAll("{{ENTRIES_ROWS}}", combinedRows)
112
- .replaceAll("{{CMD_LIST}}", "npx deuk-agent-rule ticket list")
113
- .replaceAll("{{CMD_USE_LATEST}}", "npx deuk-agent-rule ticket use --latest");
114
- }
115
-
116
- function renderLine(e, i, ticketDir, cwd) {
117
- const absPath = join(cwd, e.path);
118
- const fileUri = `file://${toPosixPath(absPath)}`;
119
- const statusIcon = e.status === "active" ? "🔥 " : (e.status === "archived" ? "📦 " : "[ ] ");
120
- const safeTitle = String(e.title || "").replace(/\|/g, '|').replace(/(\n|\\n)+/g, ' ');
121
- const prio = e.priority || "P2";
122
- return `| ${i + 1} | ${statusIcon}${e.status} | ${prio} | ${safeTitle} | ${e.group} | ${e.project} | ${e.createdAt.split('T')[0]} | [open](${fileUri}) |`;
123
- }
124
-
125
- export function writeTicketListFile(cwd, entries, opts = {}) {
126
- if (!opts.render) return; // Make it on-demand
127
- const ticketDir = detectConsumerTicketDir(cwd, { createIfMissing: true });
128
- const p = join(ticketDir, TICKET_LIST_FILENAME);
129
- if (opts.dryRun) return;
130
- const body = renderTicketListMarkdown(cwd, entries);
131
- mkdirSync(ticketDir, { recursive: true });
132
- writeFileSync(p, body, "utf8");
133
- }
134
-
135
- export function appendTicketEntry(cwd, entry, opts = {}) {
136
- const indexJson = readTicketIndexJson(cwd);
137
- entry.status = entry.status || "open";
138
- const next = { version: 1, updatedAt: new Date().toISOString(), activeTicketId: indexJson.activeTicketId, entries: [entry, ...indexJson.entries] };
139
- writeTicketIndexJson(cwd, next, opts);
140
- if (opts.render) writeTicketListFile(cwd, next.entries, opts);
141
- }
142
-
143
- export function updateTicketEntryStatus(cwd, opts = {}) {
144
- const indexJson = rebuildTicketIndexFromTopicFilesIfNeeded(cwd, opts);
145
- let foundIndex = -1;
146
- const targetTopic = opts.topic ? String(opts.topic).toLowerCase() : null;
147
-
148
- if (opts.latest) {
149
- foundIndex = 0;
150
- } else if (targetTopic) {
151
- foundIndex = indexJson.entries.findIndex(e => String(e.topic).toLowerCase().includes(targetTopic));
152
- }
153
-
154
- if (foundIndex === -1) {
155
- throw new Error("No matching ticket found to update status");
156
- }
157
-
158
- const entry = indexJson.entries[foundIndex];
159
- entry.status = opts.status || "closed";
160
- entry.updatedAt = new Date().toISOString(); // optional metadata update
161
-
162
- const next = { version: indexJson.version, updatedAt: new Date().toISOString(), entries: indexJson.entries };
163
- writeTicketIndexJson(cwd, next, opts);
164
- writeTicketListFile(cwd, next.entries, opts);
165
- return entry;
166
- }
167
-
168
- export function performUpgradeMigration(cwd, opts = {}) {
169
- const root = detectConsumerTicketDir(cwd, { createIfMissing: true });
170
- const archiveDir = join(root, "archive");
171
-
172
- const files = collectTicketMarkdownFiles(root).filter(p => {
173
- const base = basename(p);
174
- return base !== "LATEST.md" && base !== TICKET_LIST_FILENAME && base !== TICKET_LIST_TEMPLATE_FILENAME && base !== "ACTIVE_TICKET.md";
175
- });
176
-
177
- console.log(`[UPGRADE] Scanning ${files.length} tickets for V2 migration...`);
178
-
179
- let count = 0;
180
- for (const abs of files) {
181
- const rel = toRepoRelativePath(cwd, abs);
182
- const body = readFileSync(abs, "utf8");
183
- const { meta, content } = parseFrontMatter(body);
184
-
185
- if (meta.id && meta.status) {
186
- // Already V2, but check if it needs archiving
187
- const isAlreadyInArchive = rel.includes("/archive/");
188
- if (meta.status === "archived" && !isAlreadyInArchive && !opts.dryRun) {
189
- // Move to archive if status is archived but file is in root
190
- moveFileToArchive(cwd, abs, meta.group || "sub");
191
- }
192
- continue;
193
- }
194
-
195
- // V1 -> V2 Migration
196
- const titleMatch = content.match(/^##\s+Task:\s*(.+)$/m);
197
- const title = meta.title || titleMatch?.[1]?.trim() || basename(abs).replace(/\.md$/i, "");
198
-
199
- // Check if finished (all phases [x])
200
- const phases = content.match(/\[[ x/]]/g);
201
- const finished = phases && phases.length > 0 && phases.every(p => p.includes("x"));
202
- const isAlreadyInArchive = rel.includes("/archive/");
203
-
204
- let status = meta.status || "open";
205
- if (finished || isAlreadyInArchive) {
206
- status = "archived";
207
- }
208
-
209
- const project = meta.project || detectProjectFromBody(content);
210
-
211
- const newMeta = {
212
- id: meta.id || `000-legacy-${statSync(abs).mtimeMs}`,
213
- title,
214
- status,
215
- submodule: meta.submodule || (content.includes("DeukPack") ? "DeukPack" : ""),
216
- project,
217
- createdAt: meta.createdAt || statSync(abs).birthtime.toISOString(),
218
- updatedAt: new Date().toISOString()
219
- };
220
-
221
- const migratedBody = stringifyFrontMatter(newMeta, content);
222
-
223
- if (opts.dryRun) {
224
- console.log(`[DRY-RUN] Would upgrade: ${rel} (status: ${status})`);
225
- } else {
226
- let finalAbs = abs;
227
- if (status === "archived" && !isAlreadyInArchive) {
228
- finalAbs = moveFileToArchive(cwd, abs, basename(dirname(abs)));
229
- }
230
- writeFileSync(finalAbs, migratedBody, "utf8");
231
- console.log(`[OK] Upgraded: ${toRepoRelativePath(cwd, finalAbs)}`);
232
- count++;
233
- }
234
- }
235
-
236
- if (!opts.dryRun) {
237
- rebuildTicketIndexFromTopicFilesIfNeeded(cwd, { ...opts, force: true });
238
- performDefragmentation(cwd, opts); // NEW: Split to submodules
239
- syncActiveTicketPointer(cwd);
240
- }
241
-
242
- return count;
243
- }
244
-
245
- export function performDefragmentation(cwd, opts = {}) {
246
- const rootTicketDir = detectConsumerTicketDir(cwd);
247
- if (!rootTicketDir) return;
248
- const tickets = collectTicketMarkdownFiles(rootTicketDir).filter(p => {
249
- const base = basename(p);
250
- return base !== "LATEST.md" && base !== TICKET_LIST_FILENAME && base !== TICKET_LIST_TEMPLATE_FILENAME && base !== "ACTIVE_TICKET.md";
251
- });
252
-
253
- console.log(`[DEFRAG] Checking ${tickets.length} tickets for submodule placement...`);
254
-
255
- const modifiedSubmodules = new Set();
256
-
257
- for (const abs of tickets) {
258
- const { meta } = parseFrontMatter(readFileSync(abs, "utf8"));
259
- if (meta.submodule && meta.submodule !== "global") {
260
- const subPath = join(cwd, meta.submodule);
261
- if (existsSync(subPath) && statSync(subPath).isDirectory()) {
262
- const subTicketDir = join(subPath, AGENT_ROOT_DIR, TICKET_SUBDIR);
263
- mkdirSync(subTicketDir, { recursive: true });
264
-
265
- const relToRoot = relative(rootTicketDir, abs);
266
- const destAbs = join(subTicketDir, relToRoot);
267
-
268
- if (opts.dryRun) {
269
- console.log(`[DRY-RUN] Would move to submodule: ${relToRoot} -> ${meta.submodule}/${AGENT_ROOT_DIR}/${TICKET_SUBDIR}/`);
270
- } else {
271
- mkdirSync(dirname(destAbs), { recursive: true });
272
- copyFileSync(abs, destAbs);
273
- unlinkSync(abs);
274
- console.log(`[DEFRAG] Moved: ${meta.submodule}/${AGENT_ROOT_DIR}/${TICKET_SUBDIR}/${relToRoot}`);
275
- modifiedSubmodules.add(subPath);
276
- }
277
- }
278
- }
279
- }
280
-
281
- // Re-index all touched submodules
282
- if (!opts.dryRun) {
283
- for (const subCwd of modifiedSubmodules) {
284
- rebuildTicketIndexFromTopicFilesIfNeeded(subCwd, { ...opts, force: true });
285
- syncActiveTicketId(subCwd);
286
- }
287
- }
288
- }
289
-
290
- function moveFileToArchive(cwd, abs, group) {
291
- const ticketDir = detectConsumerTicketDir(cwd);
292
- const archiveBase = join(ticketDir, "archive");
293
- const targetSubDir = (basename(ticketDir) === TICKET_SUBDIR || !group) ? "sub" : group;
294
- const targetDir = join(archiveBase, targetSubDir);
295
- mkdirSync(targetDir, { recursive: true });
296
- const finalAbs = join(targetDir, basename(abs));
297
- if (finalAbs !== abs) {
298
- if (existsSync(finalAbs)) {
299
- unlinkSync(abs); // Already exists in archive
300
- } else {
301
- writeFileSync(finalAbs, readFileSync(abs, "utf8"), "utf8");
302
- unlinkSync(abs);
303
- }
304
- }
305
- return finalAbs;
306
- }
307
-
308
- export function collectTicketMarkdownFiles(dir, out = []) {
309
- if (!existsSync(dir)) return out;
310
- for (const ent of readdirSync(dir, { withFileTypes: true })) {
311
- const abs = join(dir, ent.name);
312
- // Ignore common noise
313
- if (ent.name === "node_modules" || ent.name === ".git") continue;
314
-
315
- if (ent.isDirectory()) collectTicketMarkdownFiles(abs, out);
316
- else if (ent.isFile() && /\.md$/i.test(ent.name)) {
317
- const base = ent.name;
318
- if (base === "LATEST.md" || base === TICKET_LIST_FILENAME || base === TICKET_LIST_TEMPLATE_FILENAME || base === "ACTIVE_TICKET.md") continue;
319
- out.push(abs);
320
- }
321
- }
322
- return out;
323
- }
324
-
325
- /**
326
- * Finds all ticket directories recursively, skipping node_modules/.git
327
- */
328
- export function discoverAllTicketDirs(baseCwd, out = []) {
329
- if (!existsSync(baseCwd)) return out;
330
- const entries = readdirSync(baseCwd, { withFileTypes: true });
331
-
332
- // New path check
333
- const localNew = join(baseCwd, AGENT_ROOT_DIR, TICKET_SUBDIR);
334
- if (existsSync(localNew) && statSync(localNew).isDirectory()) {
335
- out.push(localNew);
336
- }
337
- // Legacy path check (singular and plural)
338
- const localLegacy1 = join(baseCwd, ".deuk-agent-ticket");
339
- if (existsSync(localLegacy1) && statSync(localLegacy1).isDirectory()) {
340
- out.push(localLegacy1);
341
- }
342
- const localLegacy2 = join(baseCwd, ".deuk-agent-tickets");
343
- if (existsSync(localLegacy2) && statSync(localLegacy2).isDirectory()) {
344
- out.push(localLegacy2);
345
- }
346
-
347
- for (const ent of entries) {
348
- if (!ent.isDirectory()) continue;
349
- if (ent.name === "node_modules" || ent.name === ".git" || ent.name === AGENT_ROOT_DIR || ent.name === ".deuk-agent-ticket" || ent.name === ".deuk-agent-tickets") continue;
350
- discoverAllTicketDirs(join(baseCwd, ent.name), out);
351
- }
352
- return out;
353
- }
354
-
355
- export function rebuildTicketIndexFromTopicFilesIfNeeded(cwd, opts = {}) {
356
- const indexJson = readTicketIndexJson(cwd);
357
- // Hierarchical Scan: If we are at root (has AGENT_ROOT_DIR), discover all sub-dirs.
358
- const isRoot = existsSync(join(cwd, AGENT_ROOT_DIR)) || existsSync(join(cwd, ".git"));
359
-
360
- let ticketDirs = [];
361
- if (opts.recursive !== false && isRoot) {
362
- ticketDirs = discoverAllTicketDirs(cwd);
363
- } else {
364
- const local = detectConsumerTicketDir(cwd);
365
- if (local) {
366
- ticketDirs = [local];
367
- }
368
- }
369
-
370
- if (ticketDirs.length === 0) return indexJson;
371
-
372
- const files = [];
373
- for (const dir of ticketDirs) {
374
- collectTicketMarkdownFiles(dir, files);
375
- }
376
-
377
- let dirty = false;
378
- const newEntries = [];
379
-
380
- for (let i = 0; i < files.length; i++) {
381
- const abs = files[i];
382
- const rel = toPosixPath(toRepoRelativePath(cwd, abs));
383
- const body = readFileSync(abs, "utf8");
384
- const { meta, content } = parseFrontMatter(body);
385
- const titleMatch = content.match(/^##\s+Task:\s*(.+)$/m);
386
-
387
- const title = meta.title || titleMatch?.[1]?.trim() || basename(abs).replace(/\.md$/i, "");
388
- const isAlreadyInArchive = rel.includes("/archive/");
389
- const status = isAlreadyInArchive ? "archived" : (meta.status || "open");
390
- const project = meta.project || detectProjectFromBody(content);
391
- const submodule = meta.submodule || "";
392
-
393
- newEntries.push({
394
- id: meta.id || makeEntryId(),
395
- title,
396
- topic: deriveTopicFromBaseName(basename(abs)),
397
- group: basename(dirname(abs)),
398
- project,
399
- submodule: meta.submodule || (rel.startsWith(AGENT_ROOT_DIR) ? "" : rel.split("/")[0]),
400
- createdAt: meta.createdAt || statSync(abs).mtime.toISOString(),
401
- updatedAt: meta.updatedAt || statSync(abs).mtime.toISOString(),
402
- path: rel,
403
- source: "ticket-sync",
404
- status,
405
- });
406
- }
407
-
408
- // Compare with old index to see if dirty
409
- if (JSON.stringify(indexJson.entries) !== JSON.stringify(newEntries)) {
410
- dirty = true;
411
- }
412
-
413
- if (dirty || opts.force) {
414
- newEntries.sort((a,b) => String(b.createdAt||"").localeCompare(String(a.createdAt||"")));
415
- const next = { version: 1, updatedAt: new Date().toISOString(), entries: newEntries };
416
- writeTicketIndexJson(cwd, next, opts);
417
- writeTicketListFile(cwd, next.entries, opts);
418
- return next;
419
- }
420
-
421
- return indexJson;
422
- }
423
-
424
-
425
- export function syncActiveTicketId(cwd) {
426
- const index = readTicketIndexJson(cwd);
427
- // Find the single "active" ticket, or the most recent "open" ticket.
428
- const activeEntry = index.entries.find(e => e.status === "active") ||
429
- index.entries.find(e => e.status === "open");
430
-
431
- const ticketDir = detectConsumerTicketDir(cwd);
432
- if (!ticketDir) return;
433
-
434
- const activeId = activeEntry ? activeEntry.id : null;
435
- if (index.activeTicketId !== activeId) {
436
- writeTicketIndexJson(cwd, { ...index, activeTicketId: activeId });
437
- }
438
-
439
- // Cleanup redundant pointers from legacy approach
440
- const legacyLatestPath = join(ticketDir, "LATEST.md");
441
- const pointerPathMd = join(ticketDir, "ACTIVE_TICKET.md");
442
- const pointerPathJson = join(ticketDir, "ACTIVE_TICKET.json");
443
-
444
- for (const p of [legacyLatestPath, pointerPathMd, pointerPathJson]) {
445
- if (existsSync(p)) {
446
- unlinkSync(p);
447
- }
448
- }
449
- }
450
-
451
-
452
- /**
453
- * Returns the machine hostname slug (lowercase, alphanumeric + hyphen only).
454
- */
455
- export function getHostnameSlug() {
456
- try {
457
- const slug = osHostname().toLowerCase().replace(/[^a-z0-9\-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
458
- return slug.slice(0, 8).replace(/-$/, '') || 'local';
459
- } catch {
460
- return 'local';
461
- }
462
- }
463
-
464
- /**
465
- * Normalizes all paths in INDEX.json by finding the actual files in the ticket directory.
466
- * Useful after migration or manual folder restructuring.
467
- */
468
- export function normalizeTicketPaths(cwd, opts = {}) {
469
- const index = readTicketIndexJson(cwd);
470
- const ticketDir = detectConsumerTicketDir(cwd);
471
- const entries = index.entries || [];
472
- let modified = false;
473
-
474
- for (const entry of entries) {
475
- if (!entry.path) continue;
476
-
477
- const currentAbs = join(cwd, entry.path);
478
- if (!existsSync(currentAbs)) {
479
- const fileName = basename(entry.path);
480
- const found = findFileRecursively(ticketDir, fileName);
481
- if (found) {
482
- const newRel = toRepoRelativePath(cwd, found);
483
- if (entry.path !== newRel) {
484
- entry.path = newRel;
485
- modified = true;
486
- }
487
- }
488
- }
489
- }
490
-
491
- if (modified) {
492
- index.updatedAt = new Date().toISOString();
493
- writeTicketIndexJson(cwd, index);
494
- if (!opts.silent) console.log(`[NORMALIZE] Corrected stale paths in ${basename(cwd)}/INDEX.json`);
495
- }
496
- return modified;
497
- }
498
-
499
- /**
500
- * Computes next sequential 3-digit ticket number by scanning all entries
501
- * in the current INDEX.json. Parses new (`NNN-topic-hostname`) format.
502
- *
503
- * @param {object[]} existingEntries - entries array from INDEX.json
504
- * @returns {{ num: number, hostname: string }}
505
- */
506
- export function computeNextTicketNumber(existingEntries) {
507
- const hostname = getHostnameSlug();
508
- const newRe = /^(\d{3,4})-/;
509
- let max = 0;
510
- for (const e of (existingEntries || [])) {
511
- const id = String(e.id || '');
512
- const m = id.match(newRe);
513
- if (m) {
514
- const n = parseInt(m[1], 10);
515
- if (n > max && n < 10000) max = n; // Sanity check for 4-digit limit
516
- }
517
- }
518
- return { num: max + 1, hostname };
519
- }
520
-
521
- /**
522
- * Sequential hostname-aware ticket ID.
523
- * Format: NNN-<topic-slug>-<hostname>
524
- * Example: 001-add-feature-joy-nucb
525
- * NNN starts at 001 and increments per local INDEX.json state.
526
- *
527
- * @param {string} topicSlug
528
- * @param {object[]} existingEntries - entries array from INDEX.json (may be empty)
529
- */
530
- export function generateTicketId(topicSlug, existingEntries) {
531
- const hostname = getHostnameSlug();
532
- const slug = toSlug(topicSlug || 'ticket');
533
-
534
- // If topicSlug already starts with NNN-, respect it
535
- const match = slug.match(/^(\d{3,4})-(.*)/);
536
- if (match) {
537
- const numStr = match[1];
538
- const restSlug = match[2].slice(0, 32);
539
- return `${numStr}-${restSlug}-${hostname}`;
540
- }
541
-
542
- const { num } = computeNextTicketNumber(existingEntries);
543
- const numStr = String(num).padStart(3, '0');
544
- const finalSlug = slug.slice(0, 32);
545
- return `${numStr}-${finalSlug}-${hostname}`;
546
- }
547
-
548
- /**
549
- * Async background sync to AI Pipeline.
550
- * Returning true on success, false on failure (for connect check).
551
- */
552
- export async function syncToPipeline(url, data) {
553
- if (typeof fetch === "undefined") {
554
- // Node.js version < 18 or no fetch polyfill
555
- return false;
556
- }
557
- try {
558
- const response = await fetch(url, {
559
- method: "POST",
560
- headers: { "Content-Type": "application/json" },
561
- body: JSON.stringify(data),
562
- signal: AbortSignal?.timeout ? AbortSignal.timeout(3000) : undefined
563
- });
564
- return response.ok;
565
- } catch (err) {
566
- return false;
567
- }
568
- }
@@ -1,77 +0,0 @@
1
- import {
2
- copyFileSync,
3
- existsSync,
4
- mkdirSync,
5
- readFileSync,
6
- readdirSync,
7
- rmSync,
8
- writeFileSync,
9
- } from "fs";
10
- import { join, dirname } from "path";
11
- import { fileURLToPath } from "url";
12
-
13
- const __dirname = dirname(fileURLToPath(import.meta.url));
14
- const pkgRoot = join(__dirname, "..");
15
-
16
- /** Copy publish/ templates into bundle/ for npm packaging. */
17
- const publishDir = join(pkgRoot, "publish");
18
- const publishRulesDir = join(publishDir, "rules");
19
- const rulesDest = join(pkgRoot, "bundle", "rules");
20
- const agentsSrc = join(publishDir, "AGENTS.md");
21
- const agentsDest = join(pkgRoot, "bundle", "AGENTS.md");
22
- const cursorrulesSrc = join(publishDir, ".cursorrules");
23
- const cursorrulesDest = join(pkgRoot, "bundle", ".cursorrules");
24
- const geminiSrc = join(publishDir, "gemini.md");
25
- const geminiDest = join(pkgRoot, "bundle", "gemini.md");
26
-
27
- if (!existsSync(publishDir)) {
28
- throw new Error("Missing publish template dir: " + publishDir);
29
- }
30
- if (!existsSync(publishRulesDir)) {
31
- throw new Error("Missing publish/rules: " + publishRulesDir);
32
- }
33
- if (!existsSync(agentsSrc)) {
34
- throw new Error("Missing publish/AGENTS.md: " + agentsSrc);
35
- }
36
- if (!existsSync(cursorrulesSrc)) {
37
- throw new Error("Missing publish/.cursorrules: " + cursorrulesSrc);
38
- }
39
-
40
- if (existsSync(rulesDest)) {
41
- rmSync(rulesDest, { recursive: true });
42
- }
43
- mkdirSync(rulesDest, { recursive: true });
44
- for (const name of readdirSync(publishRulesDir)) {
45
- if (!name.endsWith(".mdc")) continue;
46
- copyFileSync(join(publishRulesDir, name), join(rulesDest, name));
47
- }
48
-
49
- const templatesSrc = join(publishDir, "templates");
50
- const templatesDest = join(pkgRoot, "bundle", "templates");
51
- if (existsSync(templatesSrc)) {
52
- if (existsSync(templatesDest)) {
53
- rmSync(templatesDest, { recursive: true });
54
- }
55
- mkdirSync(templatesDest, { recursive: true });
56
- for (const name of readdirSync(templatesSrc)) {
57
- copyFileSync(join(templatesSrc, name), join(templatesDest, name));
58
- }
59
- }
60
-
61
- const dynamicRulesSrc = join(publishDir, "rules.d");
62
- const dynamicRulesDest = join(pkgRoot, "bundle", "rules.d");
63
- if (existsSync(dynamicRulesSrc)) {
64
- if (existsSync(dynamicRulesDest)) {
65
- rmSync(dynamicRulesDest, { recursive: true });
66
- }
67
- mkdirSync(dynamicRulesDest, { recursive: true });
68
- for (const name of readdirSync(dynamicRulesSrc)) {
69
- copyFileSync(join(dynamicRulesSrc, name), join(dynamicRulesDest, name));
70
- }
71
- }
72
-
73
- const agentsBody = readFileSync(agentsSrc, "utf8");
74
- writeFileSync(agentsDest, agentsBody, "utf8");
75
- copyFileSync(cursorrulesSrc, cursorrulesDest);
76
- if (existsSync(geminiSrc)) copyFileSync(geminiSrc, geminiDest);
77
- console.log("deuk-agent-rule: synced bundle from publish/ template.");