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.
- package/CHANGELOG.ko.md +7 -0
- package/CHANGELOG.md +70 -0
- package/README.ko.md +55 -12
- package/README.md +55 -12
- package/bundle/.cursorrules +8 -4
- package/bundle/AGENTS.md +94 -21
- package/bundle/gemini.md +26 -0
- package/bundle/rules/multi-ai-workflow.mdc +2 -2
- package/bundle/rules.d/core-workflow.md +48 -0
- package/bundle/rules.d/deukrag-mcp.md +31 -0
- package/bundle/templates/TICKET_TEMPLATE.md +30 -15
- package/package.json +2 -4
- package/scripts/cli-args.mjs +1 -0
- package/scripts/cli-init-commands.mjs +244 -44
- package/scripts/cli-init-logic.mjs +27 -6
- package/scripts/cli-rule-compiler.mjs +102 -0
- package/scripts/cli-ticket-commands.mjs +49 -17
- package/scripts/cli-ticket-logic.mjs +137 -132
- package/scripts/cli-utils.mjs +77 -5
- package/scripts/cli.mjs +9 -1
- package/scripts/merge-logic.mjs +0 -28
- package/scripts/sync-bundle.mjs +15 -0
|
@@ -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:
|
|
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 (
|
|
21
|
-
-
|
|
22
|
-
- [
|
|
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.,
|
|
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.
|
|
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
|
-
- [
|
|
37
|
-
- [
|
|
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.
|
|
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"
|
package/scripts/cli-args.mjs
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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:
|
|
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-
|
|
3
|
+
import { AGENT_ROOT_DIR, TICKET_DIR_NAME } from "./cli-utils.mjs";
|
|
4
4
|
|
|
5
|
-
const
|
|
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 =
|
|
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 ${
|
|
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
|
-
|
|
22
|
-
|
|
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
|
+
}
|