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.
- package/CHANGELOG.ko.md +74 -0
- package/CHANGELOG.md +138 -316
- package/README.ko.md +134 -154
- package/README.md +121 -153
- package/package.json +29 -7
- package/scripts/cli-args.mjs +87 -3
- package/scripts/cli-init-commands.mjs +1382 -223
- package/scripts/cli-init-logic.mjs +28 -16
- package/scripts/cli-prompts.mjs +13 -4
- package/scripts/cli-rule-compiler.mjs +44 -34
- package/scripts/cli-skill-commands.mjs +172 -0
- package/scripts/cli-telemetry-commands.mjs +429 -0
- package/scripts/cli-ticket-commands.mjs +1934 -161
- package/scripts/cli-ticket-index.mjs +298 -0
- package/scripts/cli-ticket-migration.mjs +320 -0
- package/scripts/cli-ticket-parser.mjs +207 -0
- package/scripts/cli-utils.mjs +381 -59
- package/scripts/cli.mjs +99 -19
- package/scripts/lint-md.mjs +247 -0
- package/scripts/lint-rules.mjs +143 -0
- package/scripts/merge-logic.mjs +13 -306
- package/scripts/plan-parser.mjs +53 -0
- package/templates/MODULE_RULE_TEMPLATE.md +11 -0
- package/templates/PROJECT_RULE.md +47 -0
- package/templates/TICKET_TEMPLATE.ko.md +21 -0
- package/templates/TICKET_TEMPLATE.md +21 -0
- package/templates/rules.d/deukcontext-mcp.md +31 -0
- package/templates/rules.d/platform-coexistence.md +29 -0
- package/templates/skills/context-recall/SKILL.md +25 -0
- package/templates/skills/generated-file-guard/SKILL.md +25 -0
- package/templates/skills/safe-refactor/SKILL.md +25 -0
- package/bundle/.cursorrules +0 -11
- package/bundle/AGENTS.md +0 -146
- package/bundle/gemini.md +0 -26
- package/bundle/rules/delivery-and-parallel-work.mdc +0 -26
- package/bundle/rules/git-commit.mdc +0 -24
- package/bundle/rules/multi-ai-workflow.mdc +0 -104
- package/bundle/rules.d/core-workflow.md +0 -48
- package/bundle/rules.d/deukrag-mcp.md +0 -37
- package/bundle/templates/MODULE_RULE_TEMPLATE.md +0 -24
- package/bundle/templates/TICKET_TEMPLATE.md +0 -58
- package/scripts/cli-ticket-logic.mjs +0 -568
- package/scripts/sync-bundle.mjs +0 -77
- 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
|
-
}
|
package/scripts/sync-bundle.mjs
DELETED
|
@@ -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.");
|