deuk-agent-rule 2.4.4 β†’ 2.5.11

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.
@@ -0,0 +1,31 @@
1
+ ---
2
+ id: deukrag-mcp
3
+ condition:
4
+ mcp: deukrag
5
+ inject_target: ["AGENTS.md", "gemini.md"]
6
+ ---
7
+ ## 🧠 DeukRag Knowledge Engine & RAG-FIRST HARD LOCK
8
+
9
+ - **[ABSOLUTE PRIORITY: RAG-FIRST ACTION] (ν•˜λ“œλ£°)**:
10
+ - ν”„λ‘¬ν”„νŠΈμ— μ‘λ‹΅ν•˜λŠ” λͺ¨λ“  μ•‘μ…˜μ—μ„œ MCP(`mcp_deukrag_search_*`) ν™œμš©μ΄ **0μˆœμœ„**μž…λ‹ˆλ‹€.
11
+ - 둜컬 파일(`grep_search`, `view_file`)을 λ’€μ§€κ±°λ‚˜ μ½”λ“œλ₯Ό μˆ˜μ •ν•˜κΈ° 전에, **무쑰건** κ΄€λ ¨λœ λ£°, ν‹°μΌ“, μ½”λ“œλ₯Ό MCP둜 λ¨Όμ € κ²€μƒ‰ν•˜μ‹­μ‹œμ˜€. MCPλŠ” λ‹Ήμ‹ μ˜ μ£Ό 감각 κΈ°κ΄€μž…λ‹ˆλ‹€.
12
+ - **[CONTINUOUS RAG POLICY (ν•˜λ“œλ£°)]**:
13
+ - ν‹°μΌ“ μ‹œμž‘(Phase 0) μ‹œμ μ—λ§Œ MCPλ₯Ό 1회 ν˜ΈμΆœν•˜κ³  끝내지 λ§ˆμ‹­μ‹œμ˜€.
14
+ - μ½”λ“œλ₯Ό μž‘μ„±ν•˜λŠ” 도쀑(Phase 2) μƒˆλ‘œμš΄ 파일/클래슀λ₯Ό λ§ˆμ£ΌμΉ˜κ±°λ‚˜, λΉŒλ“œ μ—λŸ¬κ°€ λ°œμƒν•˜κ±°λ‚˜, API μŠ€νŽ™ 확인이 ν•„μš”ν•  λ•Œλ§ˆλ‹€ **μž‘μ—… 쀑간에도 μˆ˜μ‹œλ‘œ** `mcp_deukrag_search_*` 도ꡬλ₯Ό 적극 ν˜ΈμΆœν•΄μ•Ό ν•©λ‹ˆλ‹€.
15
+ - **둜컬 파일 검색에 μ˜μ‘΄ν•˜μ§€ 말고 RAGλ₯Ό 반볡 ν˜ΈμΆœν•˜μ‹­μ‹œμ˜€.**
16
+
17
+ ## πŸ“‘ RAG Connectivity & Fallback Protocol (RAG μƒνƒœλ³„ 행동 μ§€μΉ¨)
18
+
19
+ μ—μ΄μ „νŠΈλŠ” ν˜„μž¬ ν™˜κ²½μ˜ RAG(MCP) μ—°κ²° μƒνƒœμ— 따라 μ•„λž˜μ™€ 같이 행동 μœ ν˜•μ„ μ¦‰μ‹œ μ „ν™˜ν•΄μ•Ό ν•©λ‹ˆλ‹€.
20
+
21
+ ### State A: RAG-Normal (Strict RAG-First)
22
+ - **μƒνƒœ**: MCP 도ꡬ(`mcp_deukrag_search_*`)κ°€ 정상 μž‘λ™ν•¨.
23
+ - **행동**: λͺ¨λ“  뢄석과 μ½”λ“œ μž‘μ„± μ „ λ°˜λ“œμ‹œ RAGλ₯Ό ν˜ΈμΆœν•˜μ‹­μ‹œμ˜€. 둜컬 파일 검색(`grep`, `ls`)보닀 RAG κ²°κ³Όλ₯Ό μš°μ„  μ‹ λ’°ν•˜μ‹­μ‹œμ˜€.
24
+
25
+ ### State B: RAG-Missing (Graceful Fallback)
26
+ - **μƒνƒœ**: ν™˜κ²½μ— MCP 도ꡬ가 μ„€μΉ˜λ˜μ–΄ μžˆμ§€ μ•Šκ±°λ‚˜ μ„€μ •λ˜μ§€ μ•ŠμŒ.
27
+ - **행동**: μ¦‰μ‹œ 둜컬 파일 기반 검색(`grep_search`, `list_dir`, `view_file`)으둜 μ „ν™˜ν•˜μ‹­μ‹œμ˜€. RAG λΆ€μž¬λ₯Ό μ‚¬μš©μžμ—κ²Œ λΆˆν‰ν•˜μ§€ 말고, κ°€μš©ν•œ 둜컬 μ»¨ν…μŠ€νŠΈ λ‚΄μ—μ„œ μ΅œμ„ μ„ λ‹€ν•΄ μž‘μ—…μ„ μ™„μˆ˜ν•˜μ‹­μ‹œμ˜€.
28
+
29
+ ### State C: RAG-Error (Loop Prevention)
30
+ - **μƒνƒœ**: MCP 호좜 μ‹œ νƒ€μž„μ•„μ›ƒ, μ—°κ²° 였λ₯˜, λ˜λŠ” 동일 μ—λŸ¬κ°€ 2회 이상 반볡됨.
31
+ - **행동**: **μ¦‰μ‹œ RAG ν˜ΈμΆœμ„ 쀑단**ν•˜κ³  State B(둜컬 검색)둜 κ°•μ œ μ „ν™˜ν•˜μ‹­μ‹œμ˜€. μ—λŸ¬ 루프에 λΉ μ Έ μž‘μ—… μ‹œκ°„μ„ λ‚­λΉ„ν•˜μ§€ λ§ˆμ‹­μ‹œμ˜€. 문제 상황을 `## πŸ“œ Execution Report`에 κΈ°λ‘ν•˜μ—¬ μΆ”ν›„ 볡ꡬ 티켓을 λ°œν–‰ν•  수 있게 ν•˜μ‹­μ‹œμ˜€.
@@ -1,37 +1,52 @@
1
- # Task: [Task Title] | Ticket: [TICKET-XXX]
1
+ # [Execution] Task: <%= meta.title %> | ID: <%= meta.id %>
2
2
 
3
- > **[CAUTION FOR AI AGENTS]**
4
- > You are operating within a locked multi-module monorepo.
3
+ > **[CAUTION FOR AI AGENTS]**
4
+ > You are operating within a locked multi-module monorepo.
5
5
  > 1. Restrict absolutely all analysis, file creation, and modifications to the declared **[Target Submodule]** below.
6
6
  > 2. Read the files listed in **[Context Files]** before doing ANY code generation.
7
7
  > 3. DO NOT leak configuration, logic, or dependencies from other submodules.
8
8
 
9
9
  ## 🎯 Scope Bounds
10
-
11
- - **Target Submodule:** `[e.g., DeukUI | DeukPack | DeukNavigation]`
10
+ - **Target Submodule:** `<%- meta.submodule || '[e.g., DeukUI | DeukPack | DeukNavigation]' %>`
12
11
  - **Context Files:**
13
12
  - `[e.g., DeukAgentRules/templates/MODULE_RULE_TEMPLATE.md]`
14
13
  - `[e.g., path/to/your/specific/rules.md]`
15
14
 
16
15
  ## πŸ“ Files to Modify
17
16
  - `path/from/root/to/target1`: [Specific instructions. Don't write 'refactor', describe WHAT to refactor.]
18
- - `path/from/root/to/target2`: [Instructions...]
19
17
 
20
- ## πŸ—οΈ Design Decisions (For Context)
21
- - [Why are we doing this? E.g., "To isolate the IR Layout bindings from DOM events"]
22
- - [What pattern to use?]
18
+ ## πŸ—οΈ Design Decisions (Refer to Plan)
19
+ - **Plan Reference**: <%- meta.planLink %>
20
+ - [Briefly restate critical decisions if necessary]
23
21
 
24
22
  ## πŸ›‘ Strict Constraints (Rules to never break)
25
- - [e.g., Do NOT remove existing @ts-nocheck headers]
26
- - [e.g., MUST retain C# [SerializeField] directives]
27
- - [e.g., Do NOT import Vue logic into DeukPack]
23
+ - [e.g., No hotpath LINQ, Async Safety, No Raw Pointers]
28
24
 
29
25
  ## πŸ”„ Phased Execution Steps
30
- > Agent: Do NOT attempt to do Phase 3 before Phase 1 is fully tested. Use separate chat messages per phase if the task is large.
26
+ > Agent: Do NOT attempt to do Phase 3 before Phase 1 is fully tested.
27
+
28
+ 0. [Phase 0> RAG Research (MCP)]
29
+ - [ ] `mcp_deukrag_search_rules` 기반 κ·œμ•½ κ²€ν†  μ™„λ£Œ
30
+ - [ ] `mcp_deukrag_search_tickets` κ³Όκ±° μœ μ‚¬ ν‹°μΌ“ 이λ ₯ μ—΄λžŒ μ™„λ£Œ
31
+ - [ ] (ν•„μˆ˜ μž‘μ„±) κ²€μƒ‰λœ 핡심 μ»¨ν…μŠ€νŠΈ μš”μ•½:
32
+ 0.5 [Phase 0.5> Deep Analysis (Optional)]
33
+ - [ ] λ³΅μž‘ν•œ μ•„ν‚€ν…μ²˜ λ³€κ²½ μ‹œ 별도 뢄석 μ•„ν‹°νŒ©νŠΈ μž‘μ„± 및 승인 μ™„λ£Œ
31
34
  1. [Phase 1> Setup / Parsing]
32
35
  2. [Phase 2> Core Logic Change]
36
+ - [ ] (CONTINUOUS RAG) μƒˆλ‘œμš΄ ν•¨μˆ˜/클래슀 μˆ˜μ • μ „ `mcp_deukrag_search_code` 및 `search_rules`둜 κ΄€λ ¨ νŒ¨ν„΄ μˆ˜μ‹œ 검색
33
37
  3. [Phase 3> Cleanup / Verification]
38
+ - [ ] (VERIFY RAG) 디버깅 및 μ—λŸ¬ λ°œμƒ μ‹œ 둜그 덀프 μ „ `mcp_deukrag_search_tickets` 둜 κ³Όκ±° ν•΄κ²°μ±… μš°μ„  탐색
39
+ - [ ] **Potential Issue Table**:
40
+ | 이슈 | 심각도 | μ„€λͺ… | 쑰치 κ³„νš |
41
+ |---|---|---|---|
42
+ | | | | |
43
+ 4. [Phase 4> Follow-up Chaining (MANDATORY if issues exist)]
44
+ - [ ] μœ„ ν‘œμ—μ„œ μ¦‰μ‹œ ν•΄κ²° λΆˆκ°€λŠ₯ν•œ ν•­λͺ©μ— λŒ€ν•΄ 별도 ν‹°μΌ“ λ°œν–‰ μ™„λ£Œ
45
+ > CLI Command Example: `deuk-agent-rule ticket create --topic 048-F1-fix-issue --title "Fix the issue" --group <group>`
46
+ - [ ] (ν•„μˆ˜ μž‘μ„±) λ°œν–‰λœ 후속 ν‹°μΌ“ 번호 리슀트:
34
47
 
35
48
  ## βœ… Verification / QA
36
- - [e.g., Check CLI command output `npm run test`]
37
- - [e.g., Validate Inspector mounts properly in Figma]
49
+ - [ ] **Deep Analysis Verification**: Phase 0.5μ—μ„œ λ„μΆœλœ 핡심 섀계 및 ꡬ쑰적 결정사항이 μ½”λ“œμ— λͺ¨λ‘ μ˜¬λ°”λ₯΄κ²Œ λ°˜μ˜λ˜μ—ˆλŠ”μ§€ 확인.
50
+ - [ ] **Potential Issues Check**: [Identify side effects, edge cases, or performance impacts]
51
+ - [ ] **Strict Constraints Audit**: [No hotpath LINQ, Async Safety, No Raw Pointers, etc.]
52
+ - [ ] `npm run test` λ˜λŠ” κ΄€λ ¨ 검증 λͺ…λ Ή μ‹€ν–‰ κ²°κ³Ό 확인
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deuk-agent-rule",
3
- "version": "2.4.4",
3
+ "version": "2.5.11",
4
4
  "description": "DeukAgentRules: generic AGENTS.md + .cursor rule templates with init/merge CLI (npm name: deuk-agent-rule).",
5
5
  "keywords": [
6
6
  "agents-md",
@@ -39,9 +39,7 @@
39
39
  "engines": {
40
40
  "node": ">=18"
41
41
  },
42
- "bin": {
43
- "deuk-agent-rule": "./scripts/cli.mjs"
44
- },
42
+ "bin": {},
45
43
  "dependencies": {
46
44
  "ejs": "^5.0.2",
47
45
  "yaml": "^2.8.3"
@@ -38,6 +38,7 @@ export function parseArgs(argv) {
38
38
  else if (a === "--backup") out.backup = true;
39
39
  else if (a === "--non-interactive") out.nonInteractive = true;
40
40
  else if (a === "--interactive") out.interactive = true;
41
+ else if (a === "--clean") out.clean = true;
41
42
  else if (a === "--tag") out.tag = argv[++i];
42
43
  else if (a === "--marker-begin") out.markerBegin = argv[++i];
43
44
  else if (a === "--marker-end") out.markerEnd = argv[++i];
@@ -1,20 +1,101 @@
1
- import { join } from "path";
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync } from "fs";
1
+ import { join, dirname, basename } from "path";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync, unlinkSync, rmSync, renameSync } from "fs";
3
3
  import { resolveMarkers, resolveCursorrulesMarkers, applyAgents, applyRules, applyCursorrules, readBundleAgents } from "./merge-logic.mjs";
4
4
  import { ensureTicketDirAndGitignore } from "./cli-init-logic.mjs";
5
+ import { normalizeTicketPaths } from "./cli-ticket-logic.mjs";
6
+ import { compileDynamicRules } from "./cli-rule-compiler.mjs";
5
7
  import { loadInitConfig, writeInitConfig } from "./cli-utils.mjs";
6
8
  import { runInteractive } from "./cli-prompts.mjs";
7
9
 
10
+ import { AGENT_ROOT_DIR, TICKET_SUBDIR, TEMPLATE_SUBDIR, RULES_SUBDIR, discoverAllSubmodules } from "./cli-utils.mjs";
11
+
12
+ function migrateLegacyStructure(cwd, dryRun) {
13
+ const recursiveMerge = (src, dest) => {
14
+ if (!existsSync(src)) return;
15
+ if (!existsSync(dest)) {
16
+ if (!dryRun) {
17
+ mkdirSync(dirname(dest), { recursive: true });
18
+ renameSync(src, dest);
19
+ }
20
+ return;
21
+ }
22
+ // Both exist, merge contents
23
+ const entries = readdirSync(src, { withFileTypes: true });
24
+ for (const ent of entries) {
25
+ const sPath = join(src, ent.name);
26
+ const dPath = join(dest, ent.name);
27
+ if (ent.isDirectory()) {
28
+ recursiveMerge(sPath, dPath);
29
+ } else {
30
+ if (!existsSync(dPath)) {
31
+ if (!dryRun) renameSync(sPath, dPath);
32
+ } else {
33
+ // If destination exists, we could overwrite or skip.
34
+ // For tickets, we skip to avoid data loss, but log it.
35
+ if (basename(sPath) !== "INDEX.json" && basename(sPath) !== "TICKET_LIST.md") {
36
+ // console.warn(`[MIGRATE] Skipping existing file: ${dPath}`);
37
+ }
38
+ if (!dryRun) unlinkSync(sPath); // Remove migrated/redundant file
39
+ }
40
+ }
41
+ }
42
+ // Clean up src if empty
43
+ try {
44
+ if (!dryRun && readdirSync(src).length === 0) rmSync(src, { recursive: true });
45
+ } catch {}
46
+ };
47
+
48
+ const legacyTemplates = join(cwd, ".deuk-agent-templates");
49
+ const newTemplates = join(cwd, AGENT_ROOT_DIR, TEMPLATE_SUBDIR);
50
+ if (existsSync(legacyTemplates)) {
51
+ console.log(`[MIGRATE] Merging legacy templates into ${AGENT_ROOT_DIR}/${TEMPLATE_SUBDIR}`);
52
+ recursiveMerge(legacyTemplates, newTemplates);
53
+ }
54
+
55
+ const legacyTickets = join(cwd, ".deuk-agent-ticket");
56
+ const legacyTicketsPlural = join(cwd, ".deuk-agent-tickets");
57
+ const newTickets = join(cwd, AGENT_ROOT_DIR, TICKET_SUBDIR);
58
+
59
+ if (existsSync(legacyTickets)) {
60
+ console.log(`[MIGRATE] Merging legacy singular ticket directory into ${AGENT_ROOT_DIR}/${TICKET_SUBDIR}`);
61
+ recursiveMerge(legacyTickets, newTickets);
62
+ }
63
+ if (existsSync(legacyTicketsPlural)) {
64
+ console.log(`[MIGRATE] Merging legacy plural tickets directory into ${AGENT_ROOT_DIR}/${TICKET_SUBDIR}`);
65
+ recursiveMerge(legacyTicketsPlural, newTickets);
66
+ }
67
+
68
+ const legacyConfig = join(cwd, ".deuk-agent-rule.config.json");
69
+ const newConfig = join(cwd, AGENT_ROOT_DIR, "config.json");
70
+ if (existsSync(legacyConfig) && !existsSync(newConfig)) {
71
+ console.log(`[MIGRATE] Moving legacy config to ${AGENT_ROOT_DIR}/config.json`);
72
+ if (!dryRun) {
73
+ mkdirSync(join(cwd, AGENT_ROOT_DIR), { recursive: true });
74
+ renameSync(legacyConfig, newConfig);
75
+ }
76
+ }
77
+
78
+ // 3. Clean up redundant legacy pointer files from the target directory
79
+ if (existsSync(newTickets)) {
80
+ for (const file of ["ACTIVE_TICKET.md", "ACTIVE_TICKET.json", "LATEST.md"]) {
81
+ const p = join(newTickets, file);
82
+ if (existsSync(p)) {
83
+ console.log(`[MIGRATE] Removing redundant pointer file: ${file}`);
84
+ if (!dryRun) unlinkSync(p);
85
+ }
86
+ }
87
+ }
88
+ }
89
+
8
90
  function syncTemplates(cwd, bundleRoot, dryRun) {
9
91
  const tplSrcDir = join(bundleRoot, "templates");
10
- const tplDestDir = join(cwd, ".deuk-agent-templates");
92
+ const tplDestDir = join(cwd, AGENT_ROOT_DIR, TEMPLATE_SUBDIR);
11
93
  if (!existsSync(tplSrcDir)) return;
12
94
  if (!dryRun) mkdirSync(tplDestDir, { recursive: true });
13
95
 
14
96
  const srcFiles = readdirSync(tplSrcDir).filter(n => n.endsWith(".md"));
15
97
  const destFiles = existsSync(tplDestDir) ? readdirSync(tplDestDir).filter(n => n.endsWith(".md")) : [];
16
98
 
17
- // 1. Copy/Update new templates
18
99
  for (const name of srcFiles) {
19
100
  const src = join(tplSrcDir, name);
20
101
  const dest = join(tplDestDir, name);
@@ -22,51 +103,177 @@ function syncTemplates(cwd, bundleRoot, dryRun) {
22
103
  console.log(`template synced: ${dest}`);
23
104
  }
24
105
 
25
- // 2. Cleanup obsolete templates (Migration)
26
106
  for (const name of destFiles) {
27
107
  if (!srcFiles.includes(name)) {
28
108
  const obsolete = join(tplDestDir, name);
29
- if (!dryRun) {
30
- import("fs").then(fs => fs.unlinkSync(obsolete));
31
- }
109
+ if (!dryRun) unlinkSync(obsolete);
32
110
  console.log(`template removed (obsolete): ${obsolete}`);
33
111
  }
34
112
  }
35
113
  }
36
114
 
115
+ const SPOKE_REGISTRY = [
116
+ {
117
+ id: "cursor",
118
+ detect: (cwd) => existsSync(join(cwd, ".cursor")),
119
+ legacy: ".cursorrules",
120
+ target: ".cursor/rules/deuk-agent.mdc",
121
+ format: "mdc",
122
+ },
123
+ {
124
+ id: "claude",
125
+ detect: (cwd) => existsSync(join(cwd, "CLAUDE.md")) || existsSync(join(cwd, ".claude")),
126
+ legacy: null,
127
+ target: "CLAUDE.md",
128
+ format: "markdown",
129
+ },
130
+ {
131
+ id: "copilot",
132
+ detect: (cwd) => existsSync(join(cwd, ".github")),
133
+ legacy: null,
134
+ target: ".github/copilot-instructions.md",
135
+ format: "markdown",
136
+ },
137
+ {
138
+ id: "windsurf",
139
+ detect: (cwd) => existsSync(join(cwd, ".windsurf")),
140
+ legacy: ".windsurfrules",
141
+ target: ".windsurf/rules/deuk-agent.md",
142
+ format: "markdown",
143
+ },
144
+ {
145
+ id: "jetbrains",
146
+ detect: (cwd) => existsSync(join(cwd, ".aiassistant")) || existsSync(join(cwd, ".idea")),
147
+ legacy: null,
148
+ target: ".aiassistant/rules/deuk-agent.md",
149
+ format: "markdown",
150
+ },
151
+ {
152
+ id: "antigravity",
153
+ detect: (cwd) => existsSync(join(cwd, "gemini.md")) || existsSync(join(cwd, ".gemini")),
154
+ legacy: null,
155
+ target: "gemini.md",
156
+ format: "markdown",
157
+ },
158
+ ];
159
+
160
+ function generateSpokeContent(spoke) {
161
+ const commonContent = `# Deuk Agent Rules
162
+
163
+ This project follows the Deuk Agent Rules framework.
164
+ - Read the full rules: [AGENTS.md](../../AGENTS.md)
165
+ - Module-specific rules: [.deuk-agent/rules/](../../.deuk-agent/rules/)
166
+
167
+ ## Critical Rules
168
+ - Use \`.deuk-agent/templates/TICKET_TEMPLATE.md\` for multi-step tasks.
169
+ - RAG-First: Use MCP tools before local file search when available.
170
+ - Error Loop Prevention: Stop after 2 repeated errors, create a ticket.
171
+ `;
172
+
173
+ if (spoke.format === "mdc") {
174
+ return `---
175
+ description: "Deuk Agent Rules - Project conventions and ticket workflow"
176
+ globs: ["**/*"]
177
+ alwaysApply: true
178
+ ---
179
+ ${commonContent}`;
180
+ }
181
+ return `<!-- deuk-agent-rule:begin -->\n${commonContent}\n<!-- deuk-agent-rule:end -->\n`;
182
+ }
183
+
184
+ function generateLegacyDeprecationNotice(spoke) {
185
+ return `<!-- deuk-agent-rule:deprecated -->
186
+ This file is deprecated. Rules have moved to:
187
+ - Target: ${spoke.target}
188
+ - All agents: AGENTS.md
189
+ <!-- deuk-agent-rule:deprecated:end -->
190
+ `;
191
+ }
192
+
193
+ function deploySpokePointers(cwd, dryRun) {
194
+ for (const spoke of SPOKE_REGISTRY) {
195
+ if (!spoke.detect(cwd)) continue;
196
+
197
+ const targetPath = join(cwd, spoke.target);
198
+ const targetDir = dirname(targetPath);
199
+
200
+ if (!dryRun) {
201
+ mkdirSync(targetDir, { recursive: true });
202
+ writeFileSync(targetPath, generateSpokeContent(spoke), "utf8");
203
+ }
204
+ console.log(`spoke synced: ${spoke.target} (${spoke.id})`);
205
+
206
+ // Deprecate legacy file if it exists
207
+ if (spoke.legacy) {
208
+ const legacyPath = join(cwd, spoke.legacy);
209
+ if (existsSync(legacyPath)) {
210
+ if (!dryRun) writeFileSync(legacyPath, generateLegacyDeprecationNotice(spoke), "utf8");
211
+ console.log(`spoke deprecated: ${spoke.legacy} -> ${spoke.target}`);
212
+ }
213
+ }
214
+ }
215
+ }
37
216
 
38
217
  export async function runInit(opts, bundleRoot) {
218
+ const submodules = discoverAllSubmodules(opts.cwd);
219
+ if (!submodules.includes(opts.cwd)) submodules.push(opts.cwd);
220
+
39
221
  const markers = resolveMarkers(opts);
40
- const agentsResult = applyAgents({
41
- targetPath: join(opts.cwd, "AGENTS.md"),
42
- bundleContent: readBundleAgents(bundleRoot),
43
- markers, flavor: "init",
44
- appendIfNoMarkers: opts.appendIfNoMarkers,
45
- dryRun: opts.dryRun, backup: opts.backup,
46
- agentsMode: opts.agents || "inject"
47
- });
48
- console.log(`AGENTS.md: ${agentsResult.action} (${agentsResult.mode || ""})`);
222
+ const bundleAgents = readBundleAgents(bundleRoot);
49
223
 
50
- const ruleActions = applyRules({
51
- bundleRulesDir: join(bundleRoot, "rules"),
52
- targetRulesDir: join(opts.cwd, ".cursor", "rules"),
53
- rulesMode: opts.rules || "prefix",
54
- dryRun: opts.dryRun, backup: opts.backup
55
- });
56
- ruleActions.forEach(r => console.log(`rule ${r.action}: ${r.dest || r.src}`));
224
+ for (const subCwd of submodules) {
225
+ console.log(`\nInitializing ${basename(subCwd)}...`);
226
+
227
+ // 1. Migration & Directory Setup
228
+ migrateLegacyStructure(subCwd, opts.dryRun);
229
+ ensureTicketDirAndGitignore({ ...opts, cwd: subCwd });
230
+
231
+ // 2. Normalize INDEX.json paths (fix stale paths)
232
+ normalizeTicketPaths(subCwd, { silent: false });
57
233
 
58
- const crResult = applyCursorrules({
59
- bundleRoot, cwd: opts.cwd,
60
- markers: resolveCursorrulesMarkers({}),
61
- cursorrulesMode: opts.cursorrules || "inject",
62
- dryRun: opts.dryRun, backup: opts.backup
63
- });
64
- console.log(`.cursorrules: ${crResult.action} (${crResult.mode || ""})`);
234
+ // 3. Spoke Pointers (e.g. .cursor/rules/deuk-agent.mdc)
235
+ deploySpokePointers(subCwd, opts.dryRun);
65
236
 
66
- ensureTicketDirAndGitignore(opts);
67
- syncTemplates(opts.cwd, bundleRoot, opts.dryRun);
237
+ // 4. Agents Setup (AGENTS.md)
238
+ const compiledAgentsAdditions = compileDynamicRules(subCwd, bundleRoot, "AGENTS.md");
239
+ const fullBundleAgents = bundleAgents + "\n\n" + compiledAgentsAdditions;
240
+
241
+ const agentsResult = applyAgents({
242
+ targetPath: join(subCwd, "AGENTS.md"),
243
+ bundleContent: fullBundleAgents,
244
+ markers, flavor: "init",
245
+ appendIfNoMarkers: opts.appendIfNoMarkers,
246
+ dryRun: opts.dryRun, backup: opts.backup,
247
+ agentsMode: opts.agents || "inject"
248
+ });
249
+ console.log(`AGENTS.md: ${agentsResult.action}`);
250
+
251
+ // 5. Hub Rules Sync (.deuk-agent/rules/)
252
+ const hubRulesDir = join(subCwd, AGENT_ROOT_DIR, RULES_SUBDIR);
253
+ if (!opts.dryRun) mkdirSync(hubRulesDir, { recursive: true });
254
+ applyRules({
255
+ bundleRulesDir: join(bundleRoot, "rules"),
256
+ targetRulesDir: hubRulesDir,
257
+ rulesMode: opts.rules || "overwrite",
258
+ dryRun: opts.dryRun, backup: opts.backup
259
+ });
260
+
261
+ // 6. Gemini Rule Sync (root rule)
262
+ const geminiBundle = join(bundleRoot, "gemini.md");
263
+ const geminiDest = join(subCwd, "gemini.md");
264
+ if (existsSync(geminiBundle)) {
265
+ const baseGemini = readFileSync(geminiBundle, "utf8");
266
+ const compiledGeminiAdditions = compileDynamicRules(subCwd, bundleRoot, "gemini.md");
267
+ if (!opts.dryRun) {
268
+ writeFileSync(geminiDest, baseGemini + "\n\n" + compiledGeminiAdditions, "utf8");
269
+ }
270
+ console.log(`gemini.md: synced with dynamic rules`);
271
+ }
272
+
273
+ // 7. Templates Sync (.deuk-agent/templates/)
274
+ syncTemplates(subCwd, bundleRoot, opts.dryRun);
275
+ }
68
276
 
69
- // If no config exists, save the derived/default config to ensure persistency
70
277
  if (!loadInitConfig(opts.cwd)) {
71
278
  writeInitConfig(opts.cwd, opts);
72
279
  }
@@ -84,21 +291,14 @@ export function runMerge(opts, bundleRoot) {
84
291
  });
85
292
  console.log(`AGENTS.md: ${agentsResult.action} (${agentsResult.mode || ""})`);
86
293
 
294
+ const hubRulesDir = join(opts.cwd, AGENT_ROOT_DIR, RULES_SUBDIR);
87
295
  const ruleActions = applyRules({
88
296
  bundleRulesDir: join(bundleRoot, "rules"),
89
- targetRulesDir: join(opts.cwd, ".cursor", "rules"),
297
+ targetRulesDir: hubRulesDir,
90
298
  rulesMode: opts.rules || "skip",
91
299
  dryRun: opts.dryRun, backup: opts.backup
92
300
  });
93
- ruleActions.forEach(r => console.log(`rule ${r.action}: ${r.dest || r.src}`));
94
-
95
- const crResult = applyCursorrules({
96
- bundleRoot, cwd: opts.cwd,
97
- markers: resolveCursorrulesMarkers({}),
98
- cursorrulesMode: opts.cursorrules || "inject",
99
- dryRun: opts.dryRun, backup: opts.backup
100
- });
101
- console.log(`.cursorrules: ${crResult.action} (${crResult.mode || ""})`);
301
+ ruleActions.forEach(r => console.log(`hub rule ${r.action}: ${r.dest || r.src}`));
102
302
 
103
303
  syncTemplates(opts.cwd, bundleRoot, opts.dryRun);
104
304
  }
@@ -1,25 +1,46 @@
1
1
  import { existsSync, appendFileSync, writeFileSync, mkdirSync, readFileSync } from "fs";
2
2
  import { join } from "path";
3
- import { TICKET_DIR_NAME } from "./cli-ticket-logic.mjs";
3
+ import { AGENT_ROOT_DIR, TICKET_DIR_NAME } from "./cli-utils.mjs";
4
4
 
5
- const GITIGNORE_TICKET_MARKER = "# deuk-agent-rule: ticket directory (local, not committed by default)";
5
+ const GITIGNORE_AGENT_MARKER = "# deuk-agent-rule: agent hub directory (local, not committed by default)";
6
6
 
7
7
  export function ensureTicketDirAndGitignore(opts) {
8
8
  const ticketPath = join(opts.cwd, TICKET_DIR_NAME);
9
9
  const gitignorePath = join(opts.cwd, ".gitignore");
10
- const ignoreLine = TICKET_DIR_NAME + "/";
10
+ const ignoreLine = AGENT_ROOT_DIR + "/";
11
11
 
12
12
  if (opts.dryRun) return;
13
13
 
14
14
  mkdirSync(ticketPath, { recursive: true });
15
15
  if (opts.shareTickets) {
16
- console.log(`[INIT] Ticket sharing enabled. Skipping .gitignore entry for ${TICKET_DIR_NAME}/`);
16
+ console.log(`[INIT] Ticket sharing enabled. Skipping .gitignore entry for ${AGENT_ROOT_DIR}/`);
17
17
  return;
18
18
  }
19
19
 
20
20
  let gi = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf8") : "";
21
- if (!gi.includes(ignoreLine)) {
22
- const block = "\n" + GITIGNORE_TICKET_MARKER + "\n" + ignoreLine + "\n";
21
+
22
+ // 1. Create document directories
23
+ 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
+ }
35
+
36
+ // 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/";
40
+
41
+ if (!gi.includes(ignoreLine) && !opts.shareTickets) {
42
+ const block = "\n" + GITIGNORE_AGENT_MARKER + "\n" + ignoreLine + "\n";
23
43
  appendFileSync(gitignorePath, block, "utf8");
44
+ console.log(`[INIT] Added ${ignoreLine} to .gitignore`);
24
45
  }
25
46
  }
@@ -0,0 +1,102 @@
1
+ import { join, basename, resolve, dirname } from "path";
2
+ import { existsSync, readdirSync, readFileSync } from "fs";
3
+ import { parseFrontMatter, AGENT_ROOT_DIR } from "./cli-utils.mjs";
4
+ import YAML from "yaml";
5
+
6
+ /**
7
+ * Scans directories for rule markdown files, evaluates their Frontmatter conditions,
8
+ * and compiles a single rule string for injection.
9
+ */
10
+ export function compileDynamicRules(cwd, bundleRoot, targetFileName) {
11
+ const bundleRulesDir = join(bundleRoot, "rules.d");
12
+ const localRulesDir = join(cwd, AGENT_ROOT_DIR, "domain-rules");
13
+
14
+ const allRuleFiles = [];
15
+
16
+ // 1. Gather global rules
17
+ if (existsSync(bundleRulesDir)) {
18
+ readdirSync(bundleRulesDir).forEach(f => {
19
+ if (f.endsWith(".md")) allRuleFiles.push(join(bundleRulesDir, f));
20
+ });
21
+ }
22
+
23
+ // 2. Gather local domain rules
24
+ if (existsSync(localRulesDir)) {
25
+ readdirSync(localRulesDir).forEach(f => {
26
+ if (f.endsWith(".md")) allRuleFiles.push(join(localRulesDir, f));
27
+ });
28
+ }
29
+
30
+ let compiledContent = "";
31
+
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
+ }
40
+
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";
51
+ }
52
+ }
53
+
54
+ return compiledContent.trim();
55
+ }
56
+
57
+ /**
58
+ * Attempts to locate and parse the DeukRag config.yaml from the workspace root.
59
+ */
60
+ function resolveDeukRagConfig(cwd) {
61
+ // Go up directories until we find a sibling DeukRag folder, or hit root
62
+ let current = resolve(cwd);
63
+ 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;
72
+ }
73
+ }
74
+ const parent = dirname(current);
75
+ if (parent === current) break;
76
+ current = parent;
77
+ }
78
+ return null;
79
+ }
80
+
81
+ /**
82
+ * Evaluates Frontmatter conditions to determine if a rule should be included.
83
+ */
84
+ function evaluateCondition(condition, cwd) {
85
+ if (!condition) return true;
86
+
87
+ // Example: condition: { mcp: "deukrag" }
88
+ if (condition.mcp === "deukrag") {
89
+ const ragConfig = resolveDeukRagConfig(cwd);
90
+ if (!ragConfig || !ragConfig.projects) return false;
91
+
92
+ // Check if the current cwd is managed by DeukRag
93
+ const isManaged = ragConfig.projects.some(p => {
94
+ // If the project path is a prefix of cwd, it's managed
95
+ return cwd.startsWith(p.path);
96
+ });
97
+
98
+ return isManaged;
99
+ }
100
+
101
+ return true;
102
+ }