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
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
2
|
+
import { basename, dirname, join } from "path";
|
|
3
|
+
import {
|
|
4
|
+
AGENT_ROOT_DIR, TICKET_SUBDIR, TICKET_LIST_FILENAME,
|
|
5
|
+
toPosixPath, toRepoRelativePath, detectProjectFromBody, deriveTopicFromBaseName, normalizeTicketGroup,
|
|
6
|
+
parseFrontMatter, stringifyFrontMatter, discoverAllWorkspaces, detectConsumerTicketDir,
|
|
7
|
+
ARCHIVE_YEAR_MONTH_RE, ARCHIVE_DAY_RE
|
|
8
|
+
} from "./cli-utils.mjs";
|
|
9
|
+
import { readTicketIndexJson, writeTicketIndexJson } from "./cli-ticket-index.mjs";
|
|
10
|
+
|
|
11
|
+
export function collectTicketMarkdownFiles(dir, out = []) {
|
|
12
|
+
if (!existsSync(dir)) return out;
|
|
13
|
+
for (const ent of readdirSync(dir, { withFileTypes: true })) {
|
|
14
|
+
const abs = join(dir, ent.name);
|
|
15
|
+
if (ent.name === "node_modules" || ent.name === ".git") continue;
|
|
16
|
+
if (ent.isDirectory()) collectTicketMarkdownFiles(abs, out);
|
|
17
|
+
else if (ent.isFile() && /\.md$/i.test(ent.name)) {
|
|
18
|
+
const base = ent.name;
|
|
19
|
+
if (base === "LATEST.md" || base === TICKET_LIST_FILENAME || base === "ACTIVE_TICKET.md") continue;
|
|
20
|
+
out.push(abs);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function discoverAllTicketDirs(baseCwd, out = []) {
|
|
27
|
+
return discoverAllWorkspaces(baseCwd, undefined, new Set(out));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function rebuildTicketIndexFromTopicFilesIfNeeded(cwd, opts = {}) {
|
|
31
|
+
const indexJson = readTicketIndexJson(cwd);
|
|
32
|
+
|
|
33
|
+
if (indexJson.entries.length > 0 && !opts.force && !opts.rebuild) {
|
|
34
|
+
return indexJson;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ticketDir = detectConsumerTicketDir(cwd);
|
|
38
|
+
if (!ticketDir) return indexJson;
|
|
39
|
+
|
|
40
|
+
// Strictly scan only the official ticket system directory and its subdirectories
|
|
41
|
+
const ticketDirs = [ticketDir];
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if (ticketDirs.length === 0) return indexJson;
|
|
45
|
+
|
|
46
|
+
const files = [];
|
|
47
|
+
for (const dir of ticketDirs) {
|
|
48
|
+
collectTicketMarkdownFiles(dir, files);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let dirty = false;
|
|
52
|
+
const newEntries = [];
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < files.length; i++) {
|
|
55
|
+
const entry = processTicketFile(files[i], cwd, indexJson, opts);
|
|
56
|
+
if (entry) newEntries.push(entry);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (JSON.stringify(indexJson.entries) !== JSON.stringify(newEntries)) {
|
|
60
|
+
dirty = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (dirty || opts.force || opts.rebuild) {
|
|
64
|
+
newEntries.sort((a,b) => String(b.createdAt||"").localeCompare(String(a.createdAt||"")));
|
|
65
|
+
const next = { version: 1, updatedAt: new Date().toISOString(), entries: newEntries };
|
|
66
|
+
writeTicketIndexJson(cwd, next, opts);
|
|
67
|
+
if (opts.rebuild) console.log(`[REBUILD] INDEX.json rebuilt with ${newEntries.length} entries.`);
|
|
68
|
+
return next;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return indexJson;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function processTicketFile(abs, cwd, indexJson, opts) {
|
|
75
|
+
const rel = toPosixPath(toRepoRelativePath(cwd, abs));
|
|
76
|
+
const filename = basename(abs);
|
|
77
|
+
const idFromFilename = filename.replace(/\.md$/i, "");
|
|
78
|
+
const isAlreadyInArchive = rel.includes("/archive/");
|
|
79
|
+
const storage = parseTicketStorage(rel, isAlreadyInArchive, abs);
|
|
80
|
+
const group = storage.group;
|
|
81
|
+
|
|
82
|
+
// Optimization: If entry already exists in index and not forced, reuse metadata to save I/O & tokens
|
|
83
|
+
const existing = indexJson.entries.find(e => e.id === idFromFilename);
|
|
84
|
+
if (existing && !opts.force) {
|
|
85
|
+
return {
|
|
86
|
+
...existing,
|
|
87
|
+
group,
|
|
88
|
+
archiveYearMonth: storage.archiveYearMonth || existing.archiveYearMonth,
|
|
89
|
+
archiveDay: storage.archiveDay || existing.archiveDay,
|
|
90
|
+
status: isAlreadyInArchive ? "archived" : (existing.status === "archived" ? "open" : existing.status),
|
|
91
|
+
updatedAt: statSync(abs).mtime.toISOString()
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// New or forced entry: Parse file content
|
|
96
|
+
let meta = {}, content = "";
|
|
97
|
+
try {
|
|
98
|
+
const body = readFileSync(abs, "utf8");
|
|
99
|
+
const parsed = parseFrontMatter(body);
|
|
100
|
+
meta = parsed.meta;
|
|
101
|
+
content = parsed.content;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.warn(`[WARNING] Failed to parse ${rel}: ${err.message}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const title = meta.title || idFromFilename;
|
|
107
|
+
const project = meta.project || detectProjectFromBody(content);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
id: meta.id || idFromFilename,
|
|
111
|
+
title,
|
|
112
|
+
topic: deriveTopicFromBaseName(filename),
|
|
113
|
+
group,
|
|
114
|
+
fileName: filename,
|
|
115
|
+
project,
|
|
116
|
+
submodule: meta.submodule || (rel.startsWith(AGENT_ROOT_DIR) ? "" : rel.split("/")[0]),
|
|
117
|
+
createdAt: meta.createdAt || statSync(abs).mtime.toISOString(),
|
|
118
|
+
updatedAt: statSync(abs).mtime.toISOString(),
|
|
119
|
+
source: "ticket-sync",
|
|
120
|
+
status: isAlreadyInArchive ? "archived" : (meta.status || "open"),
|
|
121
|
+
archiveYearMonth: storage.archiveYearMonth,
|
|
122
|
+
archiveDay: storage.archiveDay,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseTicketStorage(rel, isArchived, abs) {
|
|
127
|
+
if (!isArchived) {
|
|
128
|
+
return { group: basename(dirname(abs)) };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const parts = rel.split("/");
|
|
132
|
+
const archiveIdx = parts.indexOf("archive");
|
|
133
|
+
const group = parts[archiveIdx + 1] || basename(dirname(abs));
|
|
134
|
+
const maybeYearMonth = parts[archiveIdx + 2];
|
|
135
|
+
const maybeDay = parts[archiveIdx + 3];
|
|
136
|
+
|
|
137
|
+
if (ARCHIVE_YEAR_MONTH_RE.test(String(maybeYearMonth || "")) && ARCHIVE_DAY_RE.test(String(maybeDay || ""))) {
|
|
138
|
+
return { group: normalizeTicketGroup(group), archiveYearMonth: maybeYearMonth, archiveDay: maybeDay };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { group: normalizeTicketGroup(group) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function appendTicketEntry(cwd, entry, opts = {}) {
|
|
145
|
+
const indexJson = readTicketIndexJson(cwd);
|
|
146
|
+
entry.status = entry.status || "open";
|
|
147
|
+
// We no longer store 'path' snapshots in INDEX.json
|
|
148
|
+
const { path, ...cleanEntry } = entry;
|
|
149
|
+
const next = {
|
|
150
|
+
version: indexJson.version || 1,
|
|
151
|
+
updatedAt: new Date().toISOString(),
|
|
152
|
+
activeTicketId: indexJson.activeTicketId,
|
|
153
|
+
entries: [cleanEntry, ...indexJson.entries]
|
|
154
|
+
};
|
|
155
|
+
writeTicketIndexJson(cwd, next, opts);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function updateTicketEntryStatus(cwd, opts = {}) {
|
|
159
|
+
const indexJson = rebuildTicketIndexFromTopicFilesIfNeeded(cwd, opts);
|
|
160
|
+
let foundIndex = -1;
|
|
161
|
+
const targetTopic = opts.topic ? String(opts.topic).toLowerCase() : null;
|
|
162
|
+
|
|
163
|
+
if (opts.latest) {
|
|
164
|
+
foundIndex = 0;
|
|
165
|
+
} else if (targetTopic) {
|
|
166
|
+
// Match against both topic AND id for consistency with pickTicketEntry
|
|
167
|
+
foundIndex = indexJson.entries.findIndex(e =>
|
|
168
|
+
String(e.topic || "").toLowerCase().includes(targetTopic) ||
|
|
169
|
+
String(e.id || "").toLowerCase().includes(targetTopic)
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (foundIndex === -1) {
|
|
174
|
+
throw new Error("No matching ticket found to update status");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const entry = indexJson.entries[foundIndex];
|
|
178
|
+
const newStatus = opts.status || "closed";
|
|
179
|
+
entry.status = newStatus;
|
|
180
|
+
if (newStatus === "closed" || newStatus === "cancelled" || newStatus === "wontfix") {
|
|
181
|
+
entry.phase = 4;
|
|
182
|
+
}
|
|
183
|
+
entry.updatedAt = new Date().toISOString();
|
|
184
|
+
|
|
185
|
+
// Sync status back to .md frontmatter to prevent rebuild reversion
|
|
186
|
+
const absPath = join(cwd, entry.path);
|
|
187
|
+
if (existsSync(absPath)) {
|
|
188
|
+
try {
|
|
189
|
+
const body = readFileSync(absPath, "utf8");
|
|
190
|
+
const parsed = parseFrontMatter(body);
|
|
191
|
+
if (parsed.meta.status !== newStatus) {
|
|
192
|
+
parsed.meta.status = newStatus;
|
|
193
|
+
if (newStatus === "closed" || newStatus === "cancelled" || newStatus === "wontfix") {
|
|
194
|
+
parsed.meta.phase = 4;
|
|
195
|
+
}
|
|
196
|
+
const newBody = stringifyFrontMatter(parsed.meta, parsed.content);
|
|
197
|
+
writeFileSync(absPath, newBody, "utf8");
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.warn(`[WARNING] Failed to sync status to ${entry.path}: ${err.message}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const next = { version: indexJson.version, updatedAt: new Date().toISOString(), entries: indexJson.entries };
|
|
205
|
+
writeTicketIndexJson(cwd, next, opts);
|
|
206
|
+
return entry;
|
|
207
|
+
}
|