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,9 +1,24 @@
1
1
  import { existsSync, appendFileSync, writeFileSync, mkdirSync, readFileSync } from "fs";
2
2
  import { join } from "path";
3
- import { AGENT_ROOT_DIR, TICKET_DIR_NAME } from "./cli-utils.mjs";
3
+ import { AGENT_ROOT_DIR, LEGACY_IGNORE_DIR, LEGACY_TICKET_DIR, LEGACY_TICKET_DIR_PLURAL, TICKET_DIR_NAME } from "./cli-utils.mjs";
4
4
 
5
5
  const GITIGNORE_AGENT_MARKER = "# deuk-agent-rule: agent hub directory (local, not committed by default)";
6
6
 
7
+ function hasExactGitignoreLine(content, line) {
8
+ return content
9
+ .split(/\r?\n/)
10
+ .map(s => s.trim())
11
+ .includes(line);
12
+ }
13
+
14
+ function removeExactGitignoreLines(content, lines) {
15
+ const remove = new Set(lines);
16
+ return content
17
+ .split(/\r?\n/)
18
+ .filter(line => !remove.has(line.trim()))
19
+ .join("\n");
20
+ }
21
+
7
22
  export function ensureTicketDirAndGitignore(opts) {
8
23
  const ticketPath = join(opts.cwd, TICKET_DIR_NAME);
9
24
  const gitignorePath = join(opts.cwd, ".gitignore");
@@ -21,24 +36,21 @@ export function ensureTicketDirAndGitignore(opts) {
21
36
 
22
37
  // 1. Create document directories
23
38
  const docsPath = join(opts.cwd, AGENT_ROOT_DIR, "docs");
24
- mkdirSync(join(docsPath, "plans"), { recursive: true });
25
- mkdirSync(join(docsPath, "walkthroughs"), { recursive: true });
26
- mkdirSync(join(docsPath, "scratch"), { recursive: true });
27
-
28
- // 2. Ignore docs/scratch/ always
29
- const scratchIgnoreLine = `${AGENT_ROOT_DIR}/docs/scratch/`;
30
- if (!gi.includes(scratchIgnoreLine)) {
31
- appendFileSync(gitignorePath, "\n" + scratchIgnoreLine + "\n", "utf8");
32
- console.log(`[INIT] Added ${scratchIgnoreLine} to .gitignore`);
33
- gi += "\n" + scratchIgnoreLine + "\n";
34
- }
39
+ mkdirSync(join(docsPath, "plan"), { recursive: true });
40
+ mkdirSync(join(docsPath, "archive"), { recursive: true });
35
41
 
36
42
  // Also check for legacy ignore lines to clean up or at least check presence
37
- const legacyIgnore1 = ".deuk-agent-ticket/";
38
- const legacyIgnore2 = ".deuk-agent-tickets/";
39
- const legacyIgnore3 = ".deuk-agent/tickets/";
43
+ const legacyIgnore1 = `${LEGACY_TICKET_DIR}/`;
44
+ const legacyIgnore2 = `${LEGACY_TICKET_DIR_PLURAL}/`;
45
+ const legacyIgnore3 = LEGACY_IGNORE_DIR;
46
+ const cleanedGi = removeExactGitignoreLines(gi, [legacyIgnore1, legacyIgnore2, legacyIgnore3]);
47
+ if (cleanedGi !== gi) {
48
+ writeFileSync(gitignorePath, cleanedGi.replace(/\n*$/, "\n"), "utf8");
49
+ gi = cleanedGi;
50
+ console.log("[INIT] Removed legacy deuk-agent-rule .gitignore entries");
51
+ }
40
52
 
41
- if (!gi.includes(ignoreLine) && !opts.shareTickets) {
53
+ if (!hasExactGitignoreLine(gi, ignoreLine) && !opts.shareTickets) {
42
54
  const block = "\n" + GITIGNORE_AGENT_MARKER + "\n" + ignoreLine + "\n";
43
55
  appendFileSync(gitignorePath, block, "utf8");
44
56
  console.log(`[INIT] Added ${ignoreLine} to .gitignore`);
@@ -1,7 +1,7 @@
1
1
  import { createInterface } from "readline";
2
2
  import { existsSync, readFileSync } from "fs";
3
3
  import { join } from "path";
4
- import { loadInitConfig, writeInitConfig, STACKS, AGENT_TOOLS } from "./cli-utils.mjs";
4
+ import { STACKS, AGENT_TOOLS, DOC_LANGUAGE_CHOICES, resolveDocsLanguage, normalizeWorkflowMode, WORKFLOW_MODE_EXECUTE, WORKFLOW_MODE_PLAN } from "./cli-utils.mjs";
5
5
 
6
6
  export async function ask(rl, question) {
7
7
  return new Promise((resolve) => rl.question(question, resolve));
@@ -48,6 +48,13 @@ export async function runInteractive(opts) {
48
48
 
49
49
  const stack = await selectOne(rl, "What is your primary tech stack?", STACKS);
50
50
  const tools = await selectMany(rl, "Which agent tools do you use?", AGENT_TOOLS);
51
+ const docsLanguage = await selectOne(rl, "What document language should generated tickets/plans use?", DOC_LANGUAGE_CHOICES);
52
+ const workflowMode = opts.workflowMode
53
+ ? normalizeWorkflowMode(opts.workflowMode)
54
+ : await selectOne(rl, "What workflow mode should be saved?", [
55
+ { label: "Plan mode (prepare only)", value: WORKFLOW_MODE_PLAN },
56
+ { label: "Execute mode (apply changes)", value: WORKFLOW_MODE_EXECUTE },
57
+ ]);
51
58
  const shareTickets = await askYesNo("Do you want to share (git-track) tickets for this repository?", false);
52
59
 
53
60
  const targetAgents = join(opts.cwd, "AGENTS.md");
@@ -57,7 +64,7 @@ export async function runInteractive(opts) {
57
64
  console.log("\n No AGENTS.md found — will create with markers.");
58
65
  } else {
59
66
  const content = readFileSync(targetAgents, "utf8");
60
- const hasMarkers = content.includes("deuk-agent-rule:begin");
67
+ const hasMarkers = content.includes("deuk-agent-rule:begin") || content.includes("## DeukAgentRules");
61
68
  if (!hasMarkers) {
62
69
  const choice = await selectOne(rl, "AGENTS.md exists but has no markers. How to apply?", [
63
70
  { label: "Append managed block at the end (safe)", value: "inject" },
@@ -77,14 +84,16 @@ export async function runInteractive(opts) {
77
84
  opts.agents = opts.agents ?? agentsDefault;
78
85
  opts.stack = stack;
79
86
  opts.agentTools = tools;
87
+ opts.docsLanguage = resolveDocsLanguage(docsLanguage);
88
+ opts.workflowMode = normalizeWorkflowMode(opts.workflowMode || workflowMode);
80
89
  opts.shareTickets = shareTickets;
81
90
  opts.remoteSync = remoteSync;
82
91
  opts.pipelineUrl = pipelineUrl;
83
92
 
84
- writeInitConfig(opts.cwd, opts);
85
-
86
93
  console.log("\n Stack : " + stack);
87
94
  console.log(" Tools : " + (tools.join(", ") || "none"));
95
+ console.log(" Docs Language: " + opts.docsLanguage);
96
+ console.log(" Workflow Mode: " + opts.workflowMode);
88
97
  console.log(" Share Tickets: " + (opts.shareTickets ? "Yes (Shared)" : "No (Private)"));
89
98
  console.log(" Remote Sync: " + (opts.remoteSync ? "Enabled" : "Disabled"));
90
99
  if (opts.remoteSync) console.log(" Pipeline URL: " + opts.pipelineUrl);
@@ -8,8 +8,8 @@ import YAML from "yaml";
8
8
  * and compiles a single rule string for injection.
9
9
  */
10
10
  export function compileDynamicRules(cwd, bundleRoot, targetFileName) {
11
- const bundleRulesDir = join(bundleRoot, "rules.d");
12
- const localRulesDir = join(cwd, AGENT_ROOT_DIR, "domain-rules");
11
+ const bundleRulesDir = join(bundleRoot, "templates", "rules.d");
12
+ const localRulesDir = join(cwd, AGENT_ROOT_DIR, "project-rules");
13
13
 
14
14
  const allRuleFiles = [];
15
15
 
@@ -30,24 +30,29 @@ export function compileDynamicRules(cwd, bundleRoot, targetFileName) {
30
30
  let compiledContent = "";
31
31
 
32
32
  for (const filePath of allRuleFiles) {
33
- const rawContent = readFileSync(filePath, "utf8");
34
- const { meta, content } = parseFrontMatter(rawContent);
35
-
36
- // Check if this rule is intended for the current target file (e.g., AGENTS.md)
37
- if (meta.inject_target && !meta.inject_target.includes(targetFileName)) {
38
- continue;
39
- }
33
+ try {
34
+ const rawContent = readFileSync(filePath, "utf8");
35
+ const { meta, content } = parseFrontMatter(rawContent);
36
+
37
+ // Check if this rule is intended for the current target file (e.g., AGENTS.md)
38
+ if (meta.inject_target && !meta.inject_target.includes(targetFileName)) {
39
+ continue;
40
+ }
40
41
 
41
- // Evaluate conditions
42
- let shouldInclude = true;
43
- if (meta.condition) {
44
- shouldInclude = evaluateCondition(meta.condition, cwd);
45
- }
46
-
47
- if (shouldInclude) {
48
- const sourceId = meta.id || basename(filePath);
49
- compiledContent += `\n<!-- RULE MODULE: ${sourceId} -->\n`;
50
- compiledContent += content.trim() + "\n";
42
+ // Evaluate conditions
43
+ let shouldInclude = true;
44
+ if (meta.condition) {
45
+ shouldInclude = evaluateCondition(meta.condition, cwd);
46
+ }
47
+
48
+ if (shouldInclude) {
49
+ const sourceId = meta.id || basename(filePath);
50
+ compiledContent += `\n<!-- RULE MODULE: ${sourceId} -->\n`;
51
+ compiledContent += content.trim() + "\n";
52
+ }
53
+ } catch (err) {
54
+ console.warn(`[WARNING] Skipping malformed rule file at ${filePath}:`, err.message);
55
+ continue;
51
56
  }
52
57
  }
53
58
 
@@ -55,20 +60,25 @@ export function compileDynamicRules(cwd, bundleRoot, targetFileName) {
55
60
  }
56
61
 
57
62
  /**
58
- * Attempts to locate and parse the DeukRag config.yaml from the workspace root.
63
+ * Attempts to locate and parse the DeukContext config.yaml from the workspace root.
59
64
  */
60
- function resolveDeukRagConfig(cwd) {
61
- // Go up directories until we find a sibling DeukRag folder, or hit root
65
+ function resolveDeukContextConfig(cwd) {
66
+ // Go up directories until we find a sibling DeukAgentContext folder, or hit root
62
67
  let current = resolve(cwd);
63
68
  while (current && current !== "/") {
64
- const candidatePath = join(current, "DeukRag", ".local", "config.yaml");
65
- if (existsSync(candidatePath)) {
66
- try {
67
- const raw = readFileSync(candidatePath, "utf8");
68
- return YAML.parse(raw);
69
- } catch (e) {
70
- console.error("Failed to parse DeukRag config.yaml:", e);
71
- return null;
69
+ const candidates = [
70
+ join(current, "DeukAgentContext", ".local", "config.yaml"),
71
+ join(current, "DeukContext", ".local", "config.yaml") // Legacy fallback
72
+ ];
73
+ for (const candidatePath of candidates) {
74
+ if (existsSync(candidatePath)) {
75
+ try {
76
+ const raw = readFileSync(candidatePath, "utf8");
77
+ return YAML.parse(raw);
78
+ } catch (e) {
79
+ console.error("Failed to parse DeukContext config.yaml:", e);
80
+ return null;
81
+ }
72
82
  }
73
83
  }
74
84
  const parent = dirname(current);
@@ -84,12 +94,12 @@ function resolveDeukRagConfig(cwd) {
84
94
  function evaluateCondition(condition, cwd) {
85
95
  if (!condition) return true;
86
96
 
87
- // Example: condition: { mcp: "deukrag" }
88
- if (condition.mcp === "deukrag") {
89
- const ragConfig = resolveDeukRagConfig(cwd);
97
+ // Example: condition: { mcp: "deuk-agent-context" }
98
+ if (condition.mcp === "deuk-agent-context" || condition.mcp === "deuk_agent_context") {
99
+ const ragConfig = resolveDeukContextConfig(cwd);
90
100
  if (!ragConfig || !ragConfig.projects) return false;
91
101
 
92
- // Check if the current cwd is managed by DeukRag
102
+ // Check if the current cwd is managed by DeukContext
93
103
  const isManaged = ragConfig.projects.some(p => {
94
104
  // If the project path is a prefix of cwd, it's managed
95
105
  return cwd.startsWith(p.path);
@@ -0,0 +1,172 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ import { AGENT_ROOT_DIR } from "./cli-utils.mjs";
4
+
5
+ const SKILL_IDS = ["safe-refactor", "generated-file-guard", "context-recall"];
6
+ const SKILL_ROOT = "templates/skills";
7
+ const CONFIG_FILE = `${AGENT_ROOT_DIR}/skills.json`;
8
+ const REPO_SKILL_TEMPLATE_ROOT = `${AGENT_ROOT_DIR}/skill-templates`;
9
+
10
+ function repoSkillPath(cwd, id) {
11
+ return join(cwd, AGENT_ROOT_DIR, "skills", id, "SKILL.md");
12
+ }
13
+
14
+ function sourceSkillPath(cwd, id) {
15
+ const localTemplate = join(cwd, REPO_SKILL_TEMPLATE_ROOT, id, "SKILL.md");
16
+ if (existsSync(localTemplate)) return localTemplate;
17
+ return join(cwd, SKILL_ROOT, id, "SKILL.md");
18
+ }
19
+
20
+ function loadSkillSource(cwd, id) {
21
+ const source = sourceSkillPath(cwd, id);
22
+ if (!existsSync(source)) throw new Error(`skill not found: ${id}`);
23
+ return readFileSync(source, "utf8");
24
+ }
25
+
26
+ function loadSkillConfig(cwd) {
27
+ const path = join(cwd, CONFIG_FILE);
28
+ if (!existsSync(path)) return { version: 1, installed: [], exposed: {} };
29
+ return JSON.parse(readFileSync(path, "utf8"));
30
+ }
31
+
32
+ function writeSkillConfig(cwd, config, dryRun) {
33
+ if (dryRun) return;
34
+ const path = join(cwd, CONFIG_FILE);
35
+ mkdirSync(dirname(path), { recursive: true });
36
+ writeFileSync(path, JSON.stringify(config, null, 2), "utf8");
37
+ }
38
+
39
+ function ensureKnownSkill(id) {
40
+ if (!SKILL_IDS.includes(id)) throw new Error(`unknown skill: ${id}`);
41
+ }
42
+
43
+ export function listSkills(cwd = process.cwd()) {
44
+ const config = loadSkillConfig(cwd);
45
+ return SKILL_IDS.map(id => ({
46
+ id,
47
+ installed: config.installed.includes(id),
48
+ exposed: Object.entries(config.exposed || {})
49
+ .filter(([, ids]) => Array.isArray(ids) && ids.includes(id))
50
+ .map(([platform]) => platform)
51
+ }));
52
+ }
53
+
54
+ export function addSkill(opts = {}) {
55
+ const cwd = opts.cwd || process.cwd();
56
+ const id = opts.skill;
57
+ ensureKnownSkill(id);
58
+ const body = loadSkillSource(cwd, id);
59
+ const target = repoSkillPath(cwd, id);
60
+ const config = loadSkillConfig(cwd);
61
+
62
+ if (!opts.dryRun) {
63
+ mkdirSync(dirname(target), { recursive: true });
64
+ writeFileSync(target, body, "utf8");
65
+ }
66
+
67
+ if (!config.installed.includes(id)) config.installed.push(id);
68
+ writeSkillConfig(cwd, config, opts.dryRun);
69
+ return { id, target };
70
+ }
71
+
72
+ function exposeClaude(cwd, ids, dryRun) {
73
+ for (const id of ids) {
74
+ const body = readFileSync(repoSkillPath(cwd, id), "utf8");
75
+ const target = join(cwd, ".claude", "skills", id, "SKILL.md");
76
+ if (!dryRun) {
77
+ mkdirSync(dirname(target), { recursive: true });
78
+ writeFileSync(target, body, "utf8");
79
+ }
80
+ }
81
+ }
82
+
83
+ function exposeCursor(cwd, ids, dryRun) {
84
+ const target = join(cwd, ".cursor", "rules", "deuk-agent-skills.mdc");
85
+ const body = [
86
+ "---",
87
+ "description: \"DeukAgentRules skill pointers\"",
88
+ "globs: [\"**/*\"]",
89
+ "alwaysApply: false",
90
+ "---",
91
+ "# DeukAgentRules Skills",
92
+ "",
93
+ "These are thin behavior playbooks. They do not override `core-rules/AGENTS.md`, TDW, APC, Phase Gate, or PROJECT_RULE.md.",
94
+ "",
95
+ ...ids.map(id => `- ${id}: .deuk-agent/skills/${id}/SKILL.md`),
96
+ ""
97
+ ].join("\n");
98
+ if (!dryRun) {
99
+ mkdirSync(dirname(target), { recursive: true });
100
+ writeFileSync(target, body, "utf8");
101
+ }
102
+ }
103
+
104
+ export function exposeSkills(opts = {}) {
105
+ const cwd = opts.cwd || process.cwd();
106
+ const platform = String(opts.platform || "").toLowerCase();
107
+ if (!["claude", "cursor"].includes(platform)) throw new Error("skill expose requires --platform claude|cursor");
108
+
109
+ const config = loadSkillConfig(cwd);
110
+ const ids = config.installed || [];
111
+ if (ids.length === 0) throw new Error("no skills installed; run skill add first");
112
+ for (const id of ids) {
113
+ if (!existsSync(repoSkillPath(cwd, id))) addSkill({ cwd, skill: id, dryRun: opts.dryRun });
114
+ }
115
+
116
+ if (platform === "claude") exposeClaude(cwd, ids, opts.dryRun);
117
+ if (platform === "cursor") exposeCursor(cwd, ids, opts.dryRun);
118
+
119
+ config.exposed = config.exposed || {};
120
+ config.exposed[platform] = Array.from(new Set([...(config.exposed[platform] || []), ...ids]));
121
+ writeSkillConfig(cwd, config, opts.dryRun);
122
+ return { platform, ids };
123
+ }
124
+
125
+ export function lintSkills(cwd = process.cwd()) {
126
+ const paths = [];
127
+ for (const root of [join(cwd, SKILL_ROOT), join(cwd, REPO_SKILL_TEMPLATE_ROOT), join(cwd, AGENT_ROOT_DIR, "skills")]) {
128
+ if (!existsSync(root)) continue;
129
+ for (const id of readdirSync(root)) {
130
+ const path = join(root, id, "SKILL.md");
131
+ if (existsSync(path)) paths.push(path);
132
+ }
133
+ }
134
+
135
+ const violations = [];
136
+ const forbidden = [/ignore ticket/i, /skip verification/i, /override (tdw|apc|phase gate)/i, /edit generated output directly/i];
137
+ for (const path of paths) {
138
+ const body = readFileSync(path, "utf8");
139
+ if (!/Authority:.*core-rules\/AGENTS\.md/i.test(body)) violations.push(`${path}: missing authority line`);
140
+ for (const pattern of forbidden) {
141
+ if (pattern.test(body)) violations.push(`${path}: forbidden phrase ${pattern}`);
142
+ }
143
+ }
144
+ return { ok: violations.length === 0, paths, violations };
145
+ }
146
+
147
+ export async function runSkill(action, opts = {}) {
148
+ if (action === "list") {
149
+ const rows = listSkills(opts.cwd);
150
+ if (opts.json) console.log(JSON.stringify(rows, null, 2));
151
+ else rows.forEach(row => console.log(`${row.id} installed=${row.installed ? "yes" : "no"} exposed=${row.exposed.join(",") || "-"}`));
152
+ return;
153
+ }
154
+ if (action === "add") {
155
+ const result = addSkill(opts);
156
+ console.log(`skill added: ${result.id}`);
157
+ return;
158
+ }
159
+ if (action === "expose") {
160
+ const result = exposeSkills(opts);
161
+ console.log(`skills exposed: ${result.platform} ${result.ids.join(",")}`);
162
+ return;
163
+ }
164
+ if (action === "lint") {
165
+ const result = lintSkills(opts.cwd);
166
+ if (opts.json) console.log(JSON.stringify(result, null, 2));
167
+ else console.log(result.ok ? "skill:lint ok" : `skill:lint failed ${result.violations.length}`);
168
+ if (!result.ok) throw new Error(result.violations.join("\n"));
169
+ return;
170
+ }
171
+ throw new Error("Unknown skill action: " + action);
172
+ }