deuk-agent-rule 2.2.2 → 2.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 +48 -0
- package/CHANGELOG.md +192 -149
- package/README.ko.md +40 -16
- package/README.md +30 -7
- package/bundle/AGENTS.md +70 -36
- package/bundle/rules/multi-ai-workflow.mdc +0 -1
- package/bundle/templates/TICKET_TEMPLATE.md +1 -1
- package/package.json +57 -52
- package/scripts/cli-args.mjs +9 -0
- package/scripts/cli-init-commands.mjs +27 -2
- package/scripts/cli-init-logic.mjs +4 -0
- package/scripts/cli-prompts.mjs +17 -45
- package/scripts/cli-ticket-commands.mjs +100 -56
- package/scripts/cli-ticket-logic.mjs +322 -46
- package/scripts/cli-utils.mjs +64 -1
- package/scripts/cli.mjs +18 -6
- package/scripts/sync-oss.mjs +5 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, unlinkSync } from "fs";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, unlinkSync, copyFileSync } from "fs";
|
|
2
2
|
import { basename, dirname, join, relative } from "path";
|
|
3
|
-
import {
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
import { toPosixPath, toRepoRelativePath, toSlug, formatTimestampForFile, makeEntryId, detectProjectFromBody, deriveTopicFromBaseName, parseFrontMatter, stringifyFrontMatter, loadInitConfig } from "./cli-utils.mjs";
|
|
4
5
|
|
|
5
6
|
export const TICKET_DIR_NAME = ".deuk-agent-ticket";
|
|
6
7
|
export const TICKET_INDEX_FILENAME = "INDEX.json";
|
|
@@ -65,34 +66,42 @@ export function writeTicketIndexJson(cwd, indexJson, opts = {}) {
|
|
|
65
66
|
|
|
66
67
|
export function renderTicketListMarkdown(cwd, entries) {
|
|
67
68
|
const sorted = [...entries].sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")));
|
|
68
|
-
const latest = sorted[0] || null;
|
|
69
|
+
const latest = sorted.find(e => e.status !== "archived") || sorted[0] || null;
|
|
69
70
|
|
|
70
71
|
const ticketDir = detectConsumerTicketDir(cwd, { createIfMissing: true });
|
|
71
72
|
const templatePath = join(ticketDir, TICKET_LIST_TEMPLATE_FILENAME);
|
|
72
73
|
const template = existsSync(templatePath) ? readFileSync(templatePath, "utf8") : DEFAULT_TICKET_LIST_TEMPLATE;
|
|
73
74
|
|
|
74
|
-
let latestBlock = "- No ticket entries yet.";
|
|
75
|
+
let latestBlock = "- No active ticket entries yet.";
|
|
75
76
|
if (latest) {
|
|
76
77
|
const relPath = toPosixPath(relative(ticketDir, join(cwd, latest.path)));
|
|
77
78
|
const safeLatestTitle = String(latest.title || "").replace(/\[|\]/g, '').replace(/\n/g, ' ');
|
|
78
|
-
latestBlock = `- [${safeLatestTitle}](${relPath})\n-
|
|
79
|
+
latestBlock = `- [${safeLatestTitle}](${relPath})\n- status: \`${latest.status}\` / group: \`${latest.group}\` / project: \`${latest.project}\``;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
const activeRows = sorted.filter(e => e.status !== "archived").map((e, i) => renderLine(e, i, ticketDir, cwd));
|
|
83
|
+
const archivedRows = sorted.filter(e => e.status === "archived").slice(0, 50).map((e, i) => renderLine(e, i, ticketDir, cwd));
|
|
84
|
+
|
|
85
|
+
let combinedRows = "### 🚀 Active Tickets\n\n| # | Status | Title | Group | Project | Created | Path |\n|---|---|---|---|---|---|---|\n" +
|
|
86
|
+
(activeRows.join("\n") || "| - | - | No active tickets | - | - | - | - |") +
|
|
87
|
+
"\n\n### 📦 Archived Tickets\n\n| # | Status | Title | Group | Project | Created | Path |\n|---|---|---|---|---|---|---|\n" +
|
|
88
|
+
(archivedRows.join("\n") || "| - | - | No archived tickets | - | - | - | - |");
|
|
87
89
|
|
|
88
90
|
return template
|
|
89
91
|
.replaceAll("{{SOURCE_INDEX}}", `${TICKET_DIR_NAME}/${TICKET_INDEX_FILENAME}`)
|
|
90
92
|
.replaceAll("{{LATEST_BLOCK}}", latestBlock)
|
|
91
|
-
.replaceAll("{{ENTRIES_ROWS}}",
|
|
93
|
+
.replaceAll("{{ENTRIES_ROWS}}", combinedRows)
|
|
92
94
|
.replaceAll("{{CMD_LIST}}", "npx deuk-agent-rule ticket list")
|
|
93
95
|
.replaceAll("{{CMD_USE_LATEST}}", "npx deuk-agent-rule ticket use --latest");
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
function renderLine(e, i, ticketDir, cwd) {
|
|
99
|
+
const relPath = toPosixPath(relative(ticketDir, join(cwd, e.path)));
|
|
100
|
+
const statusIcon = e.status === "active" ? "🔥 " : (e.status === "archived" ? "📦 " : "[ ] ");
|
|
101
|
+
const safeTitle = String(e.title || "").replace(/\|/g, '|').replace(/(\n|\\n)+/g, ' ');
|
|
102
|
+
return `| ${i + 1} | ${statusIcon}${e.status} | ${safeTitle} | ${e.group} | ${e.project} | ${e.createdAt.split('T')[0]} | [open](${relPath}) |`;
|
|
103
|
+
}
|
|
104
|
+
|
|
96
105
|
export function writeTicketListFile(cwd, entries, opts = {}) {
|
|
97
106
|
const ticketDir = detectConsumerTicketDir(cwd, { createIfMissing: true });
|
|
98
107
|
const p = join(ticketDir, TICKET_LIST_FILENAME);
|
|
@@ -135,60 +144,243 @@ export function updateTicketEntryStatus(cwd, opts = {}) {
|
|
|
135
144
|
return entry;
|
|
136
145
|
}
|
|
137
146
|
|
|
147
|
+
export function performUpgradeMigration(cwd, opts = {}) {
|
|
148
|
+
const root = join(cwd, TICKET_DIR_NAME);
|
|
149
|
+
const archiveDir = join(root, "archive");
|
|
150
|
+
|
|
151
|
+
const files = collectTicketMarkdownFiles(root).filter(p => {
|
|
152
|
+
const base = basename(p);
|
|
153
|
+
return base !== "LATEST.md" && base !== TICKET_LIST_FILENAME && base !== TICKET_LIST_TEMPLATE_FILENAME && base !== "ACTIVE_TICKET.md";
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
console.log(`[UPGRADE] Scanning ${files.length} tickets for V2 migration...`);
|
|
157
|
+
|
|
158
|
+
let count = 0;
|
|
159
|
+
for (const abs of files) {
|
|
160
|
+
const rel = toRepoRelativePath(cwd, abs);
|
|
161
|
+
const body = readFileSync(abs, "utf8");
|
|
162
|
+
const { meta, content } = parseFrontMatter(body);
|
|
163
|
+
|
|
164
|
+
if (meta.id && meta.status) {
|
|
165
|
+
// Already V2, but check if it needs archiving
|
|
166
|
+
const isAlreadyInArchive = rel.includes("/archive/");
|
|
167
|
+
if (meta.status === "archived" && !isAlreadyInArchive && !opts.dryRun) {
|
|
168
|
+
// Move to archive if status is archived but file is in root
|
|
169
|
+
moveFileToArchive(cwd, abs, meta.group || "sub");
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// V1 -> V2 Migration
|
|
175
|
+
const titleMatch = content.match(/^##\s+Task:\s*(.+)$/m);
|
|
176
|
+
const title = meta.title || titleMatch?.[1]?.trim() || basename(abs).replace(/\.md$/i, "");
|
|
177
|
+
|
|
178
|
+
// Check if finished (all phases [x])
|
|
179
|
+
const phases = content.match(/\[[ x/]]/g);
|
|
180
|
+
const finished = phases && phases.length > 0 && phases.every(p => p.includes("x"));
|
|
181
|
+
const isAlreadyInArchive = rel.includes("/archive/");
|
|
182
|
+
|
|
183
|
+
let status = meta.status || "open";
|
|
184
|
+
if (finished || isAlreadyInArchive) {
|
|
185
|
+
status = "archived";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const project = meta.project || detectProjectFromBody(content);
|
|
189
|
+
|
|
190
|
+
const newMeta = {
|
|
191
|
+
id: meta.id || `ticket_${statSync(abs).mtimeMs}`,
|
|
192
|
+
title,
|
|
193
|
+
status,
|
|
194
|
+
submodule: meta.submodule || (content.includes("DeukPack") ? "DeukPack" : ""),
|
|
195
|
+
project,
|
|
196
|
+
createdAt: meta.createdAt || statSync(abs).birthtime.toISOString(),
|
|
197
|
+
updatedAt: new Date().toISOString()
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const migratedBody = stringifyFrontMatter(newMeta, content);
|
|
201
|
+
|
|
202
|
+
if (opts.dryRun) {
|
|
203
|
+
console.log(`[DRY-RUN] Would upgrade: ${rel} (status: ${status})`);
|
|
204
|
+
} else {
|
|
205
|
+
let finalAbs = abs;
|
|
206
|
+
if (status === "archived" && !isAlreadyInArchive) {
|
|
207
|
+
finalAbs = moveFileToArchive(cwd, abs, basename(dirname(abs)));
|
|
208
|
+
}
|
|
209
|
+
writeFileSync(finalAbs, migratedBody, "utf8");
|
|
210
|
+
console.log(`[OK] Upgraded: ${toRepoRelativePath(cwd, finalAbs)}`);
|
|
211
|
+
count++;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!opts.dryRun) {
|
|
216
|
+
rebuildTicketIndexFromTopicFilesIfNeeded(cwd, { ...opts, force: true });
|
|
217
|
+
performDefragmentation(cwd, opts); // NEW: Split to submodules
|
|
218
|
+
syncActiveTicketPointer(cwd);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return count;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function performDefragmentation(cwd, opts = {}) {
|
|
225
|
+
const rootTicketDir = join(cwd, TICKET_DIR_NAME);
|
|
226
|
+
const tickets = collectTicketMarkdownFiles(rootTicketDir).filter(p => {
|
|
227
|
+
const base = basename(p);
|
|
228
|
+
return base !== "LATEST.md" && base !== TICKET_LIST_FILENAME && base !== TICKET_LIST_TEMPLATE_FILENAME && base !== "ACTIVE_TICKET.md";
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
console.log(`[DEFRAG] Checking ${tickets.length} tickets for submodule placement...`);
|
|
232
|
+
|
|
233
|
+
const modifiedSubmodules = new Set();
|
|
234
|
+
|
|
235
|
+
for (const abs of tickets) {
|
|
236
|
+
const { meta } = parseFrontMatter(readFileSync(abs, "utf8"));
|
|
237
|
+
if (meta.submodule && meta.submodule !== "global") {
|
|
238
|
+
const subPath = join(cwd, meta.submodule);
|
|
239
|
+
if (existsSync(subPath) && statSync(subPath).isDirectory()) {
|
|
240
|
+
const subTicketDir = join(subPath, TICKET_DIR_NAME);
|
|
241
|
+
mkdirSync(subTicketDir, { recursive: true });
|
|
242
|
+
|
|
243
|
+
const relToRoot = relative(rootTicketDir, abs);
|
|
244
|
+
const destAbs = join(subTicketDir, relToRoot);
|
|
245
|
+
|
|
246
|
+
if (opts.dryRun) {
|
|
247
|
+
console.log(`[DRY-RUN] Would move to submodule: ${relToRoot} -> ${meta.submodule}/${TICKET_DIR_NAME}/`);
|
|
248
|
+
} else {
|
|
249
|
+
mkdirSync(dirname(destAbs), { recursive: true });
|
|
250
|
+
copyFileSync(abs, destAbs);
|
|
251
|
+
unlinkSync(abs);
|
|
252
|
+
console.log(`[DEFRAG] Moved: ${meta.submodule}/${TICKET_DIR_NAME}/${relToRoot}`);
|
|
253
|
+
modifiedSubmodules.add(subPath);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Re-index all touched submodules
|
|
260
|
+
if (!opts.dryRun) {
|
|
261
|
+
for (const subCwd of modifiedSubmodules) {
|
|
262
|
+
rebuildTicketIndexFromTopicFilesIfNeeded(subCwd, { ...opts, force: true });
|
|
263
|
+
syncActiveTicketPointer(subCwd);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function moveFileToArchive(cwd, abs, group) {
|
|
269
|
+
const archiveBase = join(cwd, TICKET_DIR_NAME, "archive");
|
|
270
|
+
const targetSubDir = (group === TICKET_DIR_NAME || !group) ? "sub" : group;
|
|
271
|
+
const targetDir = join(archiveBase, targetSubDir);
|
|
272
|
+
mkdirSync(targetDir, { recursive: true });
|
|
273
|
+
const finalAbs = join(targetDir, basename(abs));
|
|
274
|
+
if (finalAbs !== abs) {
|
|
275
|
+
if (existsSync(finalAbs)) {
|
|
276
|
+
unlinkSync(abs); // Already exists in archive
|
|
277
|
+
} else {
|
|
278
|
+
writeFileSync(finalAbs, readFileSync(abs, "utf8"), "utf8");
|
|
279
|
+
unlinkSync(abs);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return finalAbs;
|
|
283
|
+
}
|
|
284
|
+
|
|
138
285
|
export function collectTicketMarkdownFiles(dir, out = []) {
|
|
139
286
|
if (!existsSync(dir)) return out;
|
|
140
287
|
for (const ent of readdirSync(dir, { withFileTypes: true })) {
|
|
141
288
|
const abs = join(dir, ent.name);
|
|
289
|
+
// Ignore common noise
|
|
290
|
+
if (ent.name === "node_modules" || ent.name === ".git") continue;
|
|
291
|
+
|
|
142
292
|
if (ent.isDirectory()) collectTicketMarkdownFiles(abs, out);
|
|
143
|
-
else if (ent.isFile() && /\.md$/i.test(ent.name))
|
|
293
|
+
else if (ent.isFile() && /\.md$/i.test(ent.name)) {
|
|
294
|
+
const base = ent.name;
|
|
295
|
+
if (base === "LATEST.md" || base === TICKET_LIST_FILENAME || base === TICKET_LIST_TEMPLATE_FILENAME || base === "ACTIVE_TICKET.md") continue;
|
|
296
|
+
out.push(abs);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Finds all .deuk-agent-ticket directories recursively, skipping node_modules/.git
|
|
304
|
+
*/
|
|
305
|
+
export function discoverAllTicketDirs(baseCwd, out = []) {
|
|
306
|
+
if (!existsSync(baseCwd)) return out;
|
|
307
|
+
const entries = readdirSync(baseCwd, { withFileTypes: true });
|
|
308
|
+
|
|
309
|
+
// If current dir has .deuk-agent-ticket, add it
|
|
310
|
+
const local = join(baseCwd, TICKET_DIR_NAME);
|
|
311
|
+
if (existsSync(local) && statSync(local).isDirectory()) {
|
|
312
|
+
out.push(local);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (const ent of entries) {
|
|
316
|
+
if (!ent.isDirectory()) continue;
|
|
317
|
+
if (ent.name === "node_modules" || ent.name === ".git" || ent.name === TICKET_DIR_NAME) continue;
|
|
318
|
+
discoverAllTicketDirs(join(baseCwd, ent.name), out);
|
|
144
319
|
}
|
|
145
320
|
return out;
|
|
146
321
|
}
|
|
147
322
|
|
|
148
323
|
export function rebuildTicketIndexFromTopicFilesIfNeeded(cwd, opts = {}) {
|
|
149
324
|
const indexJson = readTicketIndexJson(cwd);
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
325
|
+
// Hierarchical Scan: If we are at root, discover all sub-dirs.
|
|
326
|
+
const isRoot = existsSync(join(cwd, "DeukAgentRules")) || existsSync(join(cwd, "project_i"));
|
|
327
|
+
|
|
328
|
+
let ticketDirs = [];
|
|
329
|
+
if (opts.recursive !== false && isRoot) {
|
|
330
|
+
ticketDirs = discoverAllTicketDirs(cwd);
|
|
331
|
+
} else {
|
|
332
|
+
const local = join(cwd, TICKET_DIR_NAME);
|
|
333
|
+
if (existsSync(local) && statSync(local).isDirectory()) {
|
|
334
|
+
ticketDirs = [local];
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (ticketDirs.length === 0) return indexJson;
|
|
339
|
+
|
|
340
|
+
const files = [];
|
|
341
|
+
for (const dir of ticketDirs) {
|
|
342
|
+
collectTicketMarkdownFiles(dir, files);
|
|
343
|
+
}
|
|
155
344
|
|
|
156
345
|
let dirty = false;
|
|
157
|
-
const
|
|
346
|
+
const newEntries = [];
|
|
158
347
|
|
|
159
348
|
for (let i = 0; i < files.length; i++) {
|
|
160
349
|
const abs = files[i];
|
|
161
350
|
const rel = toPosixPath(toRepoRelativePath(cwd, abs));
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
351
|
+
const body = readFileSync(abs, "utf8");
|
|
352
|
+
const { meta, content } = parseFrontMatter(body);
|
|
353
|
+
const titleMatch = content.match(/^##\s+Task:\s*(.+)$/m);
|
|
354
|
+
|
|
355
|
+
const title = meta.title || titleMatch?.[1]?.trim() || basename(abs).replace(/\.md$/i, "");
|
|
356
|
+
const isAlreadyInArchive = rel.includes("/archive/");
|
|
357
|
+
const status = isAlreadyInArchive ? "archived" : (meta.status || "open");
|
|
358
|
+
const project = meta.project || detectProjectFromBody(content);
|
|
359
|
+
const submodule = meta.submodule || "";
|
|
360
|
+
|
|
361
|
+
newEntries.push({
|
|
362
|
+
id: meta.id || makeEntryId(),
|
|
363
|
+
title,
|
|
364
|
+
topic: deriveTopicFromBaseName(basename(abs)),
|
|
365
|
+
group: basename(dirname(abs)),
|
|
366
|
+
project,
|
|
367
|
+
submodule: meta.submodule || (rel.startsWith(TICKET_DIR_NAME) ? "" : rel.split("/")[0]),
|
|
368
|
+
createdAt: meta.createdAt || statSync(abs).mtime.toISOString(),
|
|
369
|
+
updatedAt: meta.updatedAt || statSync(abs).mtime.toISOString(),
|
|
370
|
+
path: rel,
|
|
371
|
+
source: "ticket-sync",
|
|
372
|
+
status,
|
|
373
|
+
});
|
|
179
374
|
}
|
|
180
375
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
indexJson.entries = indexJson.entries.filter(e => physicalPaths.has(toPosixPath(e.path)));
|
|
184
|
-
|
|
185
|
-
if (indexJson.entries.length !== originalLength) {
|
|
376
|
+
// Compare with old index to see if dirty
|
|
377
|
+
if (JSON.stringify(indexJson.entries) !== JSON.stringify(newEntries)) {
|
|
186
378
|
dirty = true;
|
|
187
379
|
}
|
|
188
380
|
|
|
189
|
-
if (dirty) {
|
|
190
|
-
|
|
191
|
-
const next = { version: 1, updatedAt: new Date().toISOString(), entries:
|
|
381
|
+
if (dirty || opts.force) {
|
|
382
|
+
newEntries.sort((a,b) => String(b.createdAt||"").localeCompare(String(a.createdAt||"")));
|
|
383
|
+
const next = { version: 1, updatedAt: new Date().toISOString(), entries: newEntries };
|
|
192
384
|
writeTicketIndexJson(cwd, next, opts);
|
|
193
385
|
writeTicketListFile(cwd, next.entries, opts);
|
|
194
386
|
return next;
|
|
@@ -211,7 +403,7 @@ export function parseLegacyTicketMeta(legacyBody) {
|
|
|
211
403
|
}
|
|
212
404
|
|
|
213
405
|
export function getLegacyMigrationCandidate(cwd) {
|
|
214
|
-
const candidateFiles = ["LATEST.md"
|
|
406
|
+
const candidateFiles = ["LATEST.md"];
|
|
215
407
|
const candidateDirs = [cwd, join(cwd, TICKET_DIR_NAME), join(cwd, "ticket")];
|
|
216
408
|
for (const dir of candidateDirs) {
|
|
217
409
|
for (const file of candidateFiles) {
|
|
@@ -219,7 +411,7 @@ export function getLegacyMigrationCandidate(cwd) {
|
|
|
219
411
|
if (existsSync(p)) {
|
|
220
412
|
const body = readFileSync(p, "utf8");
|
|
221
413
|
// Check for common plan or task markers
|
|
222
|
-
if (body.length > 50 && (/^##\s+Task:/m.test(body) || /^#\s+Plan:/m.test(body)
|
|
414
|
+
if (body.length > 50 && (/^##\s+Task:/m.test(body) || /^#\s+Plan:/m.test(body))) {
|
|
223
415
|
return { latestPath: p, body };
|
|
224
416
|
}
|
|
225
417
|
}
|
|
@@ -227,3 +419,87 @@ export function getLegacyMigrationCandidate(cwd) {
|
|
|
227
419
|
}
|
|
228
420
|
return null;
|
|
229
421
|
}
|
|
422
|
+
|
|
423
|
+
export function syncActiveTicketPointer(cwd) {
|
|
424
|
+
const index = readTicketIndexJson(cwd);
|
|
425
|
+
// Find the single "active" ticket, or the most recent "open" ticket.
|
|
426
|
+
const activeEntry = index.entries.find(e => e.status === "active") ||
|
|
427
|
+
index.entries.find(e => e.status === "open");
|
|
428
|
+
|
|
429
|
+
const ticketDir = detectConsumerTicketDir(cwd);
|
|
430
|
+
if (!ticketDir) return;
|
|
431
|
+
|
|
432
|
+
// LATEST.md is deprecated. Remove it on every sync to prevent stale reads.
|
|
433
|
+
const legacyLatestPath = join(ticketDir, "LATEST.md");
|
|
434
|
+
if (existsSync(legacyLatestPath)) {
|
|
435
|
+
unlinkSync(legacyLatestPath);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const pointerPathMd = join(ticketDir, "ACTIVE_TICKET.md");
|
|
439
|
+
const pointerPathJson = join(ticketDir, "ACTIVE_TICKET.json");
|
|
440
|
+
|
|
441
|
+
if (activeEntry) {
|
|
442
|
+
const srcAbs = join(cwd, activeEntry.path);
|
|
443
|
+
if (existsSync(srcAbs)) {
|
|
444
|
+
const redirectBody = `# 🚀 Active Ticket Redirect\n\n> **[STOP] DO NOT EDIT THIS FILE!**\n> This is a pointer file. Editing this file will result in data loss because it is not the original ticket.\n\n**Please open and edit the original ticket here:**\n🔗 [${basename(activeEntry.path)}](/${toPosixPath(activeEntry.path)})\n\n---\n*Original Path:* \`${activeEntry.path}\`\n*Topic:* \`${activeEntry.topic}\``;
|
|
445
|
+
const redirectContent = stringifyFrontMatter({
|
|
446
|
+
id: activeEntry.id || "pointer",
|
|
447
|
+
title: activeEntry.title || "ACTIVE_TICKET_POINTER",
|
|
448
|
+
topic: activeEntry.topic || "",
|
|
449
|
+
status: activeEntry.status || "active",
|
|
450
|
+
submodule: activeEntry.submodule || "",
|
|
451
|
+
project: activeEntry.project || "",
|
|
452
|
+
createdAt: activeEntry.createdAt || new Date().toISOString()
|
|
453
|
+
}, redirectBody);
|
|
454
|
+
writeFileSync(pointerPathMd, redirectContent, "utf8");
|
|
455
|
+
writeFileSync(pointerPathJson, JSON.stringify({ ...activeEntry, syncedAt: new Date().toISOString() }, null, 2), "utf8");
|
|
456
|
+
|
|
457
|
+
// Hook: Optional background sync to remote pipeline
|
|
458
|
+
const config = loadInitConfig(cwd);
|
|
459
|
+
if (config && config.remoteSync && config.pipelineUrl) {
|
|
460
|
+
syncToPipeline(config.pipelineUrl, { action: "sync_active", ticket: activeEntry });
|
|
461
|
+
}
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// If no active ticket, clear pointers
|
|
467
|
+
const noTicketMsg = "# No Active Ticket Found\nUse `npx deuk-agent-rule ticket list` to find open tasks.\n";
|
|
468
|
+
writeFileSync(pointerPathMd, noTicketMsg, "utf8");
|
|
469
|
+
writeFileSync(pointerPathJson, JSON.stringify({ status: "none", message: "No active ticket" }), "utf8");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Deterministic or Hash-based ID to prevent collisions in multi-device sync
|
|
475
|
+
*/
|
|
476
|
+
export function generateTicketId(title) {
|
|
477
|
+
const seed = `${title}-${Date.now()}-${Math.random()}`;
|
|
478
|
+
try {
|
|
479
|
+
return "ticket_" + createHash("md5").update(seed).digest("hex").slice(0, 12);
|
|
480
|
+
} catch {
|
|
481
|
+
return "ticket_" + Date.now() + "_" + Math.floor(Math.random() * 1000);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Async background sync to AI Pipeline.
|
|
487
|
+
* Returning true on success, false on failure (for connect check).
|
|
488
|
+
*/
|
|
489
|
+
export async function syncToPipeline(url, data) {
|
|
490
|
+
if (typeof fetch === "undefined") {
|
|
491
|
+
// Node.js version < 18 or no fetch polyfill
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
const response = await fetch(url, {
|
|
496
|
+
method: "POST",
|
|
497
|
+
headers: { "Content-Type": "application/json" },
|
|
498
|
+
body: JSON.stringify(data),
|
|
499
|
+
signal: AbortSignal?.timeout ? AbortSignal.timeout(3000) : undefined
|
|
500
|
+
});
|
|
501
|
+
return response.ok;
|
|
502
|
+
} catch (err) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
}
|
package/scripts/cli-utils.mjs
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "fs";
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2
2
|
import { basename, dirname, join, relative } from "path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
|
|
5
|
+
export const INIT_CONFIG_FILENAME = ".deuk-agent-rule.config.json";
|
|
6
|
+
export const INIT_CONFIG_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
export const STACKS = [
|
|
9
|
+
{ label: "Unity / C#", value: "unity" },
|
|
10
|
+
{ label: "Unity + WebApp + C++ Server (Hybrid)", value: "unity-webapp-cpp" },
|
|
11
|
+
{ label: "Next.js + C#", value: "nextjs-dotnet" },
|
|
12
|
+
{ label: "Web (React / Vue / general)", value: "web" },
|
|
13
|
+
{ label: "Java / Spring Boot", value: "java" },
|
|
14
|
+
{ label: "Other / skip", value: "other" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export const AGENT_TOOLS = [
|
|
18
|
+
{ label: "Cursor (Rule System)", value: "cursor" },
|
|
19
|
+
{ label: "Gemini / Antigravity", value: "gemini" },
|
|
20
|
+
{ label: "Claude / Dev", value: "claude" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function loadInitConfig(cwd) {
|
|
24
|
+
const p = join(cwd, INIT_CONFIG_FILENAME);
|
|
25
|
+
if (!existsSync(p)) return null;
|
|
26
|
+
try {
|
|
27
|
+
const j = JSON.parse(readFileSync(p, "utf8"));
|
|
28
|
+
if (j.version !== INIT_CONFIG_VERSION) return null;
|
|
29
|
+
return j;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function writeInitConfig(cwd, opts) {
|
|
36
|
+
const p = join(cwd, INIT_CONFIG_FILENAME);
|
|
37
|
+
const data = {
|
|
38
|
+
version: INIT_CONFIG_VERSION,
|
|
39
|
+
agentsMode: opts.agents || "inject",
|
|
40
|
+
stack: opts.stack,
|
|
41
|
+
agentTools: opts.agentTools,
|
|
42
|
+
shareTickets: !!opts.shareTickets,
|
|
43
|
+
remoteSync: !!opts.remoteSync,
|
|
44
|
+
pipelineUrl: opts.pipelineUrl,
|
|
45
|
+
updatedAt: new Date().toISOString(),
|
|
46
|
+
};
|
|
47
|
+
writeFileSync(p, JSON.stringify(data, null, 2), "utf8");
|
|
48
|
+
}
|
|
3
49
|
|
|
4
50
|
export function toPosixPath(p) {
|
|
5
51
|
return p.replace(/\\/g, "/");
|
|
@@ -80,3 +126,20 @@ export function inferRefTitleAndTopic(opts) {
|
|
|
80
126
|
topic,
|
|
81
127
|
};
|
|
82
128
|
}
|
|
129
|
+
|
|
130
|
+
export function parseFrontMatter(content) {
|
|
131
|
+
const match = content.match(/^---\r?\n([\s\S]+?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
132
|
+
if (!match) return { meta: {}, content };
|
|
133
|
+
try {
|
|
134
|
+
const meta = YAML.parse(match[1]);
|
|
135
|
+
return { meta: meta || {}, content: match[2] };
|
|
136
|
+
} catch (e) {
|
|
137
|
+
console.error("YAML Parse Error:", e);
|
|
138
|
+
return { meta: {}, content };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function stringifyFrontMatter(meta, content) {
|
|
143
|
+
const yamlStr = YAML.stringify(meta).trim();
|
|
144
|
+
return `---\n${yamlStr}\n---\n\n${content.trim()}\n`;
|
|
145
|
+
}
|
package/scripts/cli.mjs
CHANGED
|
@@ -4,8 +4,9 @@ import { dirname, join } from "path";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { parseArgs, parseTicketArgs } from "./cli-args.mjs";
|
|
6
6
|
import { runInit, runMerge } from "./cli-init-commands.mjs";
|
|
7
|
-
import { runTicketCreate, runTicketList, runTicketUse, runTicketClose, runTicketArchive, runTicketReports } from "./cli-ticket-commands.mjs";
|
|
8
|
-
import {
|
|
7
|
+
import { runTicketCreate, runTicketList, runTicketUse, runTicketClose, runTicketArchive, runTicketReports, runTicketMeta, runTicketConnect } from "./cli-ticket-commands.mjs";
|
|
8
|
+
import { performUpgradeMigration } from "./cli-ticket-logic.mjs";
|
|
9
|
+
import { loadInitConfig, writeInitConfig } from "./cli-utils.mjs";
|
|
9
10
|
import { runInteractive } from "./cli-prompts.mjs";
|
|
10
11
|
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -30,7 +31,12 @@ async function main() {
|
|
|
30
31
|
else if (action === "close") await runTicketClose(opts);
|
|
31
32
|
else if (action === "archive") await runTicketArchive(opts);
|
|
32
33
|
else if (action === "reports") await runTicketReports(opts);
|
|
33
|
-
else if (action === "
|
|
34
|
+
else if (action === "meta") await runTicketMeta(opts);
|
|
35
|
+
else if (action === "connect") await runTicketConnect(opts);
|
|
36
|
+
else if (action === "upgrade" || action === "migrate") {
|
|
37
|
+
const count = performUpgradeMigration(opts.cwd, opts);
|
|
38
|
+
console.log(`Migration complete: ${count} tickets upgraded.`);
|
|
39
|
+
}
|
|
34
40
|
else {
|
|
35
41
|
console.error("Unknown ticket action: " + action);
|
|
36
42
|
printHelp();
|
|
@@ -66,7 +72,7 @@ async function main() {
|
|
|
66
72
|
printHelp();
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
|
|
75
|
+
// Removed legacy migration runTicketMigrate
|
|
70
76
|
|
|
71
77
|
async function handleInit(opts) {
|
|
72
78
|
if (!opts.interactive && !opts.nonInteractive && !loadInitConfig(opts.cwd)) {
|
|
@@ -86,7 +92,7 @@ function printHelp() {
|
|
|
86
92
|
Usage:
|
|
87
93
|
npx deuk-agent-rule init [options]
|
|
88
94
|
npx deuk-agent-rule merge [options]
|
|
89
|
-
npx deuk-agent-rule ticket <create|list|use|close|archive|reports|migrate> [options]
|
|
95
|
+
npx deuk-agent-rule ticket <create|list|use|close|archive|reports|migrate|upgrade|meta|connect> [options]
|
|
90
96
|
|
|
91
97
|
Options:
|
|
92
98
|
--cwd <path> Target repo root
|
|
@@ -96,13 +102,19 @@ Options:
|
|
|
96
102
|
--agents <mode> inject | skip | overwrite
|
|
97
103
|
--rules <mode> prefix | skip | overwrite
|
|
98
104
|
--cursorrules <mode> inject | skip | overwrite
|
|
105
|
+
--json Output result in JSON format
|
|
106
|
+
--remote <url> Temporary pipeline URL
|
|
107
|
+
--sync Force enable remote sync
|
|
108
|
+
--no-sync Force disable remote sync
|
|
99
109
|
|
|
100
110
|
Ticket Options:
|
|
101
111
|
--topic <name> Ticket topic slug
|
|
102
112
|
--group <name> Ticket group (sub|main|discussion)
|
|
103
113
|
--project <name> Project filter (DeukUI|DeukAgentRules)
|
|
104
|
-
--
|
|
114
|
+
--submodule <name> Submodule filter (DeukPack|DeukUI)
|
|
115
|
+
--latest Use most recent ticket (default if no topic)
|
|
105
116
|
--path-only Print only the file path
|
|
117
|
+
--json Output result in JSON format
|
|
106
118
|
`);
|
|
107
119
|
}
|
|
108
120
|
|
package/scripts/sync-oss.mjs
CHANGED
|
@@ -49,6 +49,9 @@ cpSync(join(pkgRoot, "README.ko.md"), join(ossRoot, "README.ko.md"), { force: tr
|
|
|
49
49
|
if (existsSync(join(pkgRoot, "CHANGELOG.md"))) {
|
|
50
50
|
cpSync(join(pkgRoot, "CHANGELOG.md"), join(ossRoot, "CHANGELOG.md"), { force: true });
|
|
51
51
|
}
|
|
52
|
+
if (existsSync(join(pkgRoot, "CHANGELOG.ko.md"))) {
|
|
53
|
+
cpSync(join(pkgRoot, "CHANGELOG.ko.md"), join(ossRoot, "CHANGELOG.ko.md"), { force: true });
|
|
54
|
+
}
|
|
52
55
|
if (existsSync(join(pkgRoot, "package-lock.json"))) {
|
|
53
56
|
cpSync(join(pkgRoot, "package-lock.json"), join(ossRoot, "package-lock.json"), { force: true });
|
|
54
57
|
}
|
|
@@ -91,6 +94,7 @@ const outPkg = {
|
|
|
91
94
|
"README.md",
|
|
92
95
|
"README.ko.md",
|
|
93
96
|
"CHANGELOG.md",
|
|
97
|
+
"CHANGELOG.ko.md",
|
|
94
98
|
],
|
|
95
99
|
};
|
|
96
100
|
delete outPkg.private;
|
|
@@ -119,4 +123,4 @@ if (existsSync(ossPolish)) {
|
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
console.log("deuk-agent-rule: synced OSS tree at " + ossRoot);
|
|
122
|
-
console.log(" Override repo URL: DEUK_AGENT_RULES_OSS_REPO=https://github.com/
|
|
126
|
+
console.log(" Override repo URL: DEUK_AGENT_RULES_OSS_REPO=https://github.com/joygram/DeukAgentRules");
|