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
package/scripts/cli.mjs CHANGED
@@ -2,18 +2,18 @@
2
2
  import { existsSync, rmSync } from "fs";
3
3
  import { dirname, join } from "path";
4
4
  import { fileURLToPath } from "url";
5
- import { parseArgs, parseTicketArgs } from "./cli-args.mjs";
5
+ import { parseArgs, parseTicketArgs, parseSkillArgs, parseTelemetryArgs } from "./cli-args.mjs";
6
6
  import { runInit, runMerge } from "./cli-init-commands.mjs";
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, checkUpdateNotifier } from "./cli-utils.mjs";
7
+ import { runTicketCreate, runTicketList, runTicketUse, runTicketClose, runTicketArchive, runTicketReports, runTicketMeta, runTicketConnect, runTicketRebuild, runTicketReportAttach, runTicketMove, runTicketNext, runTicketHotfix, runTicketStatus, runTicketHandoff, runTicketEvidenceCheck, runTicketEvidenceReport } from "./cli-ticket-commands.mjs";
8
+ import { runTelemetry } from "./cli-telemetry-commands.mjs";
9
+ import { performUpgradeMigration } from "./cli-ticket-migration.mjs";
10
+ import { loadInitConfig, writeInitConfig, checkUpdateNotifier, normalizeWorkflowMode, WORKFLOW_MODE_EXECUTE, AGENT_ROOT_DIR, resolveWorkflowMode, LEGACY_TEMPLATE_DIR, LEGACY_CONFIG_FILE } from "./cli-utils.mjs";
10
11
  import { runInteractive } from "./cli-prompts.mjs";
11
12
 
12
13
  const updatePromise = checkUpdateNotifier();
13
14
 
14
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
16
  const pkgRoot = join(__dirname, "..");
16
- const bundleRoot = join(pkgRoot, "bundle");
17
17
  async function main() {
18
18
  const argv = process.argv.slice(2);
19
19
  const sub = argv[0];
@@ -30,11 +30,29 @@ async function main() {
30
30
  if (action === "create") await runTicketCreate(opts);
31
31
  else if (action === "list") await runTicketList(opts);
32
32
  else if (action === "use") await runTicketUse(opts);
33
+ else if (action === "next") await runTicketNext(opts);
34
+ else if (action === "evidence") await runTicketEvidenceCheck(opts);
33
35
  else if (action === "close") await runTicketClose(opts);
34
36
  else if (action === "archive") await runTicketArchive(opts);
35
37
  else if (action === "reports") await runTicketReports(opts);
36
38
  else if (action === "meta") await runTicketMeta(opts);
37
39
  else if (action === "connect") await runTicketConnect(opts);
40
+ else if (action === "rebuild") await runTicketRebuild(opts);
41
+ else if (action === "move" || action === "step") await runTicketMove(opts);
42
+ else if (action === "hotfix") await runTicketHotfix(opts);
43
+ else if (action === "status") await runTicketStatus(opts);
44
+ else if (action === "handoff" || action === "continue") await runTicketHandoff(opts);
45
+ else if (action === "report") {
46
+ const subAction = rest[1];
47
+ if (subAction === "attach") {
48
+ const attachOpts = parseTicketArgs(rest.slice(2));
49
+ await runTicketReportAttach(attachOpts);
50
+ } else if (opts.claim) {
51
+ await runTicketEvidenceReport(opts);
52
+ } else {
53
+ await runTicketReports(opts);
54
+ }
55
+ }
38
56
  else if (action === "upgrade" || action === "migrate") {
39
57
  const count = performUpgradeMigration(opts.cwd, opts);
40
58
  console.log(`Migration complete: ${count} tickets upgraded.`);
@@ -46,6 +64,39 @@ async function main() {
46
64
  return;
47
65
  }
48
66
 
67
+ if (sub === "telemetry") {
68
+ const opts = parseTelemetryArgs(rest);
69
+ await runTelemetry(opts);
70
+ return;
71
+ }
72
+
73
+ if (sub === "skill") {
74
+ const action = rest[0];
75
+ const opts = parseSkillArgs(rest.slice(1));
76
+ const { runSkill } = await import("./cli-skill-commands.mjs");
77
+ await runSkill(action, opts);
78
+ return;
79
+ }
80
+
81
+ if (sub === "lint:md" || sub === "lint-md") {
82
+ const { runMarkdownLint } = await import("./lint-md.mjs");
83
+ runMarkdownLint(rest);
84
+ return;
85
+ }
86
+
87
+ if (sub === "rules") {
88
+ const action = rest[0];
89
+ const opts = parseArgs(rest.slice(1));
90
+ if (action === "audit") {
91
+ const { runRulesAudit } = await import("./lint-rules.mjs");
92
+ runRulesAudit(opts);
93
+ return;
94
+ }
95
+ console.error("Unknown rules action: " + action);
96
+ printHelp();
97
+ return;
98
+ }
99
+
49
100
  if (sub === "init" || sub === "merge") {
50
101
  const opts = parseArgs(rest);
51
102
  if (opts.help) {
@@ -59,13 +110,13 @@ async function main() {
59
110
  for (const key in saved) {
60
111
  if (opts[key] === undefined) opts[key] = saved[key];
61
112
  }
62
- console.log(`Using saved config from .deuk-agent-rule.config.json (CLI overrides applied)`);
113
+ console.log(`Using saved config from ${AGENT_ROOT_DIR}/config.json (CLI overrides applied)`);
63
114
  }
64
115
 
65
116
  if (sub === "init") {
66
- await handleInit(opts);
117
+ await handleInit(opts, saved);
67
118
  } else {
68
- runMerge(opts, bundleRoot);
119
+ runMerge(opts, pkgRoot);
69
120
  }
70
121
  return;
71
122
  }
@@ -76,24 +127,31 @@ async function main() {
76
127
 
77
128
  // Removed legacy migration runTicketMigrate
78
129
 
79
- async function handleInit(opts) {
130
+ async function handleInit(opts, saved) {
80
131
  if (opts.clean && !opts.dryRun) {
81
132
  console.log(`[CLEAN] Removing legacy templates and config...`);
82
- const templatesDir = join(opts.cwd, ".deuk-agent-templates");
83
- const configFile = join(opts.cwd, ".deuk-agent-rule.config.json");
133
+ const templatesDir = join(opts.cwd, LEGACY_TEMPLATE_DIR);
134
+ const configFile = join(opts.cwd, LEGACY_CONFIG_FILE);
84
135
  if (existsSync(templatesDir)) rmSync(templatesDir, { recursive: true, force: true });
85
136
  if (existsSync(configFile)) rmSync(configFile, { force: true });
86
137
  }
87
138
 
88
- if (!opts.interactive && !opts.nonInteractive && !loadInitConfig(opts.cwd)) {
139
+ if (!opts.interactive && !opts.nonInteractive && !saved) {
89
140
  // If no config and not interactive, prompt unless non-interactive
90
141
  await runInteractive(opts);
91
- if (!opts.dryRun) writeInitConfig(opts.cwd, opts);
92
142
  } else if (opts.interactive) {
93
143
  await runInteractive(opts);
94
- if (!opts.dryRun) writeInitConfig(opts.cwd, opts);
95
144
  }
96
- await runInit(opts, bundleRoot);
145
+
146
+ if (!opts.dryRun) writeInitConfig(opts.cwd, opts);
147
+
148
+ const workflowMode = resolveWorkflowMode(opts, saved);
149
+ if (!opts.dryRun && workflowMode !== WORKFLOW_MODE_EXECUTE) {
150
+ console.log(`[WORKFLOW] Plan mode active. Re-run with --workflow execute or --approval approved to apply file mutations.`);
151
+ return;
152
+ }
153
+
154
+ await runInit(opts, pkgRoot);
97
155
  }
98
156
 
99
157
  function printHelp() {
@@ -102,7 +160,11 @@ function printHelp() {
102
160
  Usage:
103
161
  npx deuk-agent-rule init [options]
104
162
  npx deuk-agent-rule merge [options]
105
- npx deuk-agent-rule ticket <create|list|use|close|archive|reports|migrate|upgrade|meta|connect> [options]
163
+ npx deuk-agent-rule lint:md [--cwd <path>] [files...]
164
+ npx deuk-agent-rule rules audit [--compact|--json]
165
+ npx deuk-agent-rule skill <list|add|expose|lint> [options]
166
+ npx deuk-agent-rule ticket <create|evidence|list|status|handoff|continue|use|close|archive|reports|migrate|upgrade|meta|connect|move> [options]
167
+ npx deuk-agent-rule telemetry <log|sync|summary|migrate> [options]
106
168
 
107
169
  Options:
108
170
  --cwd <path> Target repo root
@@ -110,19 +172,37 @@ Options:
110
172
  --non-interactive CI/scripts mode: no prompts
111
173
  --tag <id> Custom marker ID (default: deuk-agent-rule)
112
174
  --agents <mode> inject | skip | overwrite
113
- --rules <mode> prefix | skip | overwrite
114
175
  --cursorrules <mode> inject | skip | overwrite
176
+ --workflow <mode> plan | execute
177
+ --approval <state> pending | approved (alias for workflow)
178
+ --docs-language <lang> auto | ko | en
115
179
  --json Output result in JSON format
116
180
  --remote <url> Temporary pipeline URL
117
181
  --sync Force enable remote sync
118
182
  --no-sync Force disable remote sync
119
183
 
184
+ Skill Options:
185
+ --skill, --id <name> Skill ID (safe-refactor|generated-file-guard|context-recall)
186
+ --platform <name> Exposure target (claude|cursor)
187
+
120
188
  Ticket Options:
121
- --topic <name> Ticket topic slug
189
+ --topic, --id <name> Ticket topic slug or ID
122
190
  --group <name> Ticket group (sub|main|discussion)
123
191
  --project <name> Project filter (DeukUI|DeukAgentRules)
124
192
  --submodule <name> Submodule filter (DeukPack|DeukUI)
125
- --latest Use most recent ticket (default if no topic)
193
+ --docs-language <lang> auto | ko | en
194
+ --evidence <text> Provide Phase 0 RAG evidence summary
195
+ --claim <text> Validate or report only from ticket evidence matching this claim
196
+ --skip-phase0 Bypass Phase 0 RAG validation
197
+ --plan-body <text> Create ticket with filled Phase 1 markdown body
198
+ --require-filled Enforce non-placeholder APC and compact plan content before create succeeds
199
+ --allow-placeholder Opt out of strict create guard (legacy behavior)
200
+ --compact Prefer one-line ticket outputs in automation flows
201
+ --status-detail Include detailed reasons in ticket status output
202
+ --handoff Emit compact handoff output for session continuation
203
+ --phase <number> Explicitly set the phase number (e.g., --phase 2)
204
+ --next Move to the next phase
205
+ --latest, -l Use most recent ticket (default if no topic)
126
206
  --path-only Print only the file path
127
207
  --json Output result in JSON format
128
208
  `);
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, readdirSync, statSync } from "fs";
3
+ import { join, relative, dirname, resolve } from "path";
4
+ import { spawnSync } from "child_process";
5
+ import YAML from "yaml";
6
+ import { AGENT_ROOT_DIR, TICKET_DIR_NAME } from "./cli-utils.mjs";
7
+
8
+ const ignoredDirs = new Set([".git", "node_modules"]);
9
+
10
+ export function collectChangedFiles(repoRoot) {
11
+ const changed = new Set();
12
+
13
+ const gitArgs = ["-C", repoRoot, "diff", "--name-only", "--diff-filter=ACMRTUXB"];
14
+ const gitDiff = spawnSync("git", gitArgs, { encoding: "utf8" });
15
+ if (gitDiff.status === 0) {
16
+ gitDiff.stdout
17
+ .split("\n")
18
+ .map(s => s.trim())
19
+ .filter(Boolean)
20
+ .forEach(f => changed.add(f));
21
+ }
22
+
23
+ const gitUntracked = spawnSync("git", ["-C", repoRoot, "ls-files", "--others", "--exclude-standard"], { encoding: "utf8" });
24
+ if (gitUntracked.status === 0) {
25
+ gitUntracked.stdout
26
+ .split("\n")
27
+ .map(s => s.trim())
28
+ .filter(Boolean)
29
+ .forEach(f => changed.add(f));
30
+ }
31
+
32
+ return Array.from(changed).sort();
33
+ }
34
+
35
+ export function collectChangedMarkdownFiles(repoRoot) {
36
+ return collectChangedFiles(repoRoot).filter(isMarkdownFile);
37
+ }
38
+
39
+ function isMarkdownFile(filePath) {
40
+ const lower = filePath.toLowerCase();
41
+ return lower.endsWith(".md") || lower.endsWith(".mdx") || lower.endsWith(".markdown");
42
+ }
43
+
44
+ function isPlanReport(relPath) {
45
+ return relPath.includes(`${AGENT_ROOT_DIR}/docs/plan/`) && relPath.endsWith("-report.md");
46
+ }
47
+
48
+ const LEGACY_ARCHIVED_TEMPLATE_NAMES = new Set([
49
+ "module-rule-template.md",
50
+ "ticket-list-template.md",
51
+ "ticket-template-ko.md",
52
+ "ticket-template.md"
53
+ ]);
54
+
55
+ function isLegacyArchivedTemplateArtifact(relPath, content) {
56
+ const normalized = relPath.replace(/\\/g, "/");
57
+ if (!normalized.includes(`${TICKET_DIR_NAME}/archive/`)) return false;
58
+ if (!LEGACY_ARCHIVED_TEMPLATE_NAMES.has(normalized.split("/").pop())) return false;
59
+
60
+ const src = String(content || "");
61
+ return src.includes("<%=") || src.includes("<%-") || /^id:\s*(module-rule-template|ticket-list-template|ticket-template-ko|ticket-template)\s*$/m.test(src);
62
+ }
63
+
64
+ function lintWalkthroughReportStructure(relPath, content) {
65
+ const errors = [];
66
+ const hasSummary = /##\s+(Summary|요약)(?:\s|$)/i.test(content);
67
+ const hasVerification = /##\s+(Verification|검증)(?:\s|$)/i.test(content);
68
+ const hasOutcome = /##\s+(Verification Outcome|Verification Results|검증 결과)(?:\s|$)/i.test(content);
69
+
70
+ if (!hasSummary) {
71
+ errors.push(`${relPath}: report missing Summary/요약 section`);
72
+ }
73
+ if (!hasVerification) {
74
+ errors.push(`${relPath}: report missing Verification/검증 section`);
75
+ }
76
+ if (!hasOutcome) {
77
+ errors.push(`${relPath}: report missing Verification Outcome/Verification Results/검증 결과 section`);
78
+ }
79
+
80
+ return errors;
81
+ }
82
+
83
+ function walkMarkdownFiles(rootDir, out = []) {
84
+ for (const entry of readdirSync(rootDir, { withFileTypes: true })) {
85
+ if (entry.isDirectory()) {
86
+ if (ignoredDirs.has(entry.name)) continue;
87
+ walkMarkdownFiles(join(rootDir, entry.name), out);
88
+ continue;
89
+ }
90
+ const filePath = join(rootDir, entry.name);
91
+ if (isMarkdownFile(filePath)) out.push(filePath);
92
+ }
93
+ return out;
94
+ }
95
+
96
+ function lintFile(absPath, repoRoot) {
97
+ const rel = relative(repoRoot, absPath);
98
+ const content = readFileSync(absPath, "utf8");
99
+ const errors = [];
100
+
101
+ if (isLegacyArchivedTemplateArtifact(rel, content)) {
102
+ return errors;
103
+ }
104
+
105
+ const lines = content.split(/\r?\n/);
106
+ lines.forEach((line, idx) => {
107
+ if (/\s+$/.test(line)) {
108
+ errors.push(`${rel}:${idx + 1}: trailing whitespace`);
109
+ }
110
+ });
111
+
112
+ const fenceCount = lines.filter(line => /^```/.test(line.trim())).length;
113
+ if (fenceCount % 2 !== 0) {
114
+ errors.push(`${rel}: unmatched fenced code block`);
115
+ }
116
+
117
+ if (content.startsWith("---\n") || content.startsWith("---\r\n")) {
118
+ const match = content.match(/^---\r?\n([\s\S]+?)\r?\n---\r?\n?/);
119
+ if (!match) {
120
+ const afterOpeningRule = content.replace(/^---\r?\n/, "");
121
+ if (!/^\s*\r?\n#{1,6}\s/.test(afterOpeningRule)) {
122
+ errors.push(`${rel}: invalid or unterminated frontmatter`);
123
+ }
124
+ } else {
125
+ try {
126
+ const parsed = YAML.parse(match[1]);
127
+ const isDeukAgentDoc = rel.includes(`${AGENT_ROOT_DIR}/docs/`) || rel.includes(`${AGENT_ROOT_DIR}/tickets/`);
128
+ const isArchive = rel.includes("archive/");
129
+ const isTemplate = rel.includes("templates/");
130
+ if (isDeukAgentDoc && !isArchive && !isTemplate) {
131
+ const requiredKeys = ["summary", "status", "priority", "tags"];
132
+ for (const key of requiredKeys) {
133
+ if (!parsed || parsed[key] === undefined || parsed[key] === null || parsed[key] === "") {
134
+ errors.push(`${rel}: missing required frontmatter key: ${key}`);
135
+ }
136
+ }
137
+ }
138
+ } catch (err) {
139
+ errors.push(`${rel}: invalid frontmatter YAML (${err.message})`);
140
+ }
141
+ }
142
+ } else {
143
+ const isDeukAgentDoc = rel.includes(`${AGENT_ROOT_DIR}/docs/`) || rel.includes(`${AGENT_ROOT_DIR}/tickets/`);
144
+ const isArchive = rel.includes("archive/");
145
+ const isTemplate = rel.includes("templates/");
146
+ if (isDeukAgentDoc && !isArchive && !isTemplate) {
147
+ errors.push(`${rel}: missing required frontmatter`);
148
+ }
149
+ }
150
+
151
+ const linkPattern = /!?\[[^\]]+\]\(([^)]+)\)/g;
152
+ let match;
153
+ while ((match = linkPattern.exec(content)) !== null) {
154
+ const target = String(match[1] || "").trim();
155
+ if (!target || target.startsWith("#") || /^[a-z]+:\/\//i.test(target) || target.startsWith("mailto:")) {
156
+ continue;
157
+ }
158
+ const pathOnly = target.split("#")[0].split("?")[0];
159
+ if (!pathOnly) continue;
160
+ const resolved = join(dirname(absPath), pathOnly);
161
+ if (!statExists(resolved)) {
162
+ errors.push(`${rel}: broken relative link -> ${target}`);
163
+ }
164
+ }
165
+
166
+ if (isPlanReport(rel)) {
167
+ errors.push(...lintWalkthroughReportStructure(rel, content));
168
+ }
169
+
170
+ return errors;
171
+ }
172
+
173
+ function statExists(absPath) {
174
+ try {
175
+ return statSync(absPath).isFile() || statSync(absPath).isDirectory();
176
+ } catch {
177
+ return false;
178
+ }
179
+ }
180
+
181
+ function resolveMarkdownLintTargets(repoRoot, explicitPaths = []) {
182
+ const files = explicitPaths.length > 0
183
+ ? explicitPaths.map(p => resolve(repoRoot, p)).filter(statExists).filter(isMarkdownFile)
184
+ : collectChangedMarkdownFiles(repoRoot).map(p => join(repoRoot, p));
185
+
186
+ const targets = files.length > 0 ? files : walkMarkdownFiles(repoRoot);
187
+ return Array.from(new Set(targets));
188
+ }
189
+
190
+ function parseLintArgs(argv) {
191
+ const out = { cwd: process.cwd(), paths: [] };
192
+ for (let i = 0; i < argv.length; i++) {
193
+ const arg = argv[i];
194
+ if (arg === "--cwd") {
195
+ out.cwd = argv[++i] || out.cwd;
196
+ } else if (arg.startsWith("--cwd=")) {
197
+ out.cwd = arg.slice("--cwd=".length);
198
+ } else if (!arg.startsWith("-")) {
199
+ out.paths.push(arg);
200
+ }
201
+ }
202
+ return out;
203
+ }
204
+
205
+ export function lintMarkdownPaths(paths, cwd = process.cwd()) {
206
+ const repoRoot = resolve(cwd);
207
+ const targets = Array.from(new Set(
208
+ (Array.isArray(paths) ? paths : [])
209
+ .map(p => resolve(repoRoot, p))
210
+ .filter(statExists)
211
+ .filter(isMarkdownFile)
212
+ ));
213
+ const errors = [];
214
+ for (const filePath of targets) {
215
+ errors.push(...lintFile(filePath, repoRoot));
216
+ }
217
+ return { repoRoot, targets, errors };
218
+ }
219
+
220
+ export function runMarkdownLint(argv = process.argv.slice(2)) {
221
+ const opts = parseLintArgs(argv);
222
+ const repoRoot = resolve(opts.cwd);
223
+ const targets = opts.paths.length > 0
224
+ ? lintMarkdownPaths(opts.paths, opts.cwd).targets
225
+ : resolveMarkdownLintTargets(repoRoot);
226
+ const errors = [];
227
+ for (const filePath of targets) {
228
+ errors.push(...lintFile(filePath, repoRoot));
229
+ }
230
+
231
+ if (targets.length === 0) {
232
+ console.log("lint:md: no markdown files found");
233
+ return;
234
+ }
235
+
236
+ if (errors.length > 0) {
237
+ console.error("lint:md failed");
238
+ for (const err of errors) console.error(`- ${err}`);
239
+ process.exit(1);
240
+ }
241
+
242
+ console.log(`lint:md passed (${targets.length} file${targets.length === 1 ? "" : "s"})`);
243
+ }
244
+
245
+ if (import.meta.url === `file://${process.argv[1]}`) {
246
+ runMarkdownLint();
247
+ }
@@ -0,0 +1,143 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+
4
+ const RULE_CHECKS = [
5
+ {
6
+ code: "DR-KERNEL-01",
7
+ message: "Compact kernel must keep ticket-first and tool-contract invariants at the top of the core rules.",
8
+ test: (rules) => /## Compact Kernel/i.test(rules)
9
+ && /Tools own detail/i.test(rules)
10
+ && /No ticket, no writes/i.test(rules)
11
+ && /Every phase must request and satisfy the tool-provided contract/i.test(rules)
12
+ && /Phase state has two records/i.test(rules)
13
+ && /Verification is mandatory/i.test(rules)
14
+ && /never bypass ticket, scope, generated-file, or verification gates/i.test(rules)
15
+ },
16
+ {
17
+ code: "DR-TOKEN-01",
18
+ message: "Low-token mode must stay quiet and compact.",
19
+ test: (rules) => /Silent-by-default is mandatory/i.test(rules)
20
+ && /Keep chat compact/i.test(rules)
21
+ && /Final answers must be short but complete enough/i.test(rules)
22
+ && /짧게|매우 짧게|한 줄로|간단히/i.test(rules)
23
+ && /one-sentence or bullet-only/i.test(rules)
24
+ && /avoid repeating them in chat/i.test(rules)
25
+ },
26
+ {
27
+ code: "DR-PRIORITY-01",
28
+ message: "Rules must define pointer/core/project instruction precedence.",
29
+ test: (rules) => /## 0\. Priority/i.test(rules)
30
+ && /Global DeukAgentRules pointer/i.test(rules)
31
+ && /Local generated pointer\/spoke/i.test(rules)
32
+ && /core-rules\/AGENTS\.md/i.test(rules)
33
+ && /PROJECT_RULE\.md/i.test(rules)
34
+ },
35
+ {
36
+ code: "DR-BOOT-01",
37
+ message: "Boot sequence must load core rules, project rules, and ticket context.",
38
+ test: (rules) => /Boot Sequence \(run once\)/i.test(rules)
39
+ && /Read this file \(AGENTS\.md\)/i.test(rules)
40
+ && /Read `PROJECT_RULE\.md`/i.test(rules)
41
+ && /set_workflow_context\(project, ticket_id, phase\)/i.test(rules)
42
+ && /clickable ticket-start line/i.test(rules)
43
+ },
44
+ {
45
+ code: "DR-TICKET-01",
46
+ message: "First-turn and discovery behavior must stay ticket-first.",
47
+ test: (rules) => /First-Turn Invariant/i.test(rules)
48
+ && /Ticket Discovery \(1-CALL RULE\)/i.test(rules)
49
+ && /create the ticket first/i.test(rules)
50
+ && /Do not use `ticket list` for discovery/i.test(rules)
51
+ },
52
+ {
53
+ code: "DR-CHANGE-01",
54
+ message: "Phase contract must require tool-provided requirements and block shortcuts.",
55
+ test: (rules) => /Phase Contract/i.test(rules)
56
+ && /complete requirement bundle/i.test(rules)
57
+ && /Required ticket fields\/tasks/i.test(rules)
58
+ && /Scope boundaries, generated\/source mapping/i.test(rules)
59
+ && /Do not invent a shortcut/i.test(rules)
60
+ },
61
+ {
62
+ code: "DR-LIFECYCLE-01",
63
+ message: "Lifecycle must cover phases, durable records, and compact chat.",
64
+ test: (rules) => /Ticket Lifecycle/i.test(rules)
65
+ && /Phase 0/i.test(rules)
66
+ && /Phase 4/i.test(rules)
67
+ && /findings, hypotheses, scope, compact plan, and phase contract/i.test(rules)
68
+ && /affected files, and residual risk/i.test(rules)
69
+ && /Keep chat compact once the ticket carries the durable record/i.test(rules)
70
+ },
71
+ {
72
+ code: "DR-GATE-01",
73
+ message: "Hard stops must block missing contracts, unsafe scope, and premature execution.",
74
+ test: (rules) => /Hard Stops/i.test(rules)
75
+ && /missing phase contract/i.test(rules)
76
+ && /generated\/source uncertainty/i.test(rules)
77
+ && /shared-interface changes/i.test(rules)
78
+ && /read-only until the ticket records findings/i.test(rules)
79
+ },
80
+ {
81
+ code: "DR-HALT-01",
82
+ message: "Kernel must still define halt conditions and file guards.",
83
+ test: (rules) => /Hard Stops/i.test(rules)
84
+ && /generated\/source uncertainty/i.test(rules)
85
+ && /missing tests/i.test(rules)
86
+ },
87
+ {
88
+ code: "DR-CHURN-01",
89
+ message: "Repeated symptom fixes must trigger stabilization instead of ticket churn.",
90
+ test: (rules) => /Hard Stops/i.test(rules)
91
+ && /stabilization or root-cause ticket/i.test(rules)
92
+ && /same failure family/i.test(rules)
93
+ },
94
+ {
95
+ code: "DR-CLI-01",
96
+ message: "Tool delegation and CLI ownership must stay explicit.",
97
+ test: (rules) => /Tool Delegation/i.test(rules)
98
+ && /Use `rg`\/`rg --files` first/i.test(rules)
99
+ && /Use MCP\/RAG only when local evidence is insufficient/i.test(rules)
100
+ && /Let CLI own lifecycle enforcement, claim checks, reports, and audits/i.test(rules)
101
+ && /rules audit/i.test(rules)
102
+ }
103
+ ];
104
+
105
+ export function auditRules(cwd = process.cwd()) {
106
+ const rulesPath = join(cwd, "core-rules", "AGENTS.md");
107
+ if (!existsSync(rulesPath)) {
108
+ return {
109
+ ok: false,
110
+ path: rulesPath,
111
+ violations: [{ code: "DR-RULES-00", message: "core-rules/AGENTS.md not found" }]
112
+ };
113
+ }
114
+
115
+ const rules = readFileSync(rulesPath, "utf8");
116
+ const violations = RULE_CHECKS
117
+ .filter((check) => !check.test(rules))
118
+ .map(({ code, message }) => ({ code, message }));
119
+
120
+ return { ok: violations.length === 0, path: rulesPath, violations };
121
+ }
122
+
123
+ export function runRulesAudit(opts = {}) {
124
+ const result = auditRules(opts.cwd || process.cwd());
125
+ if (opts.json) {
126
+ console.log(JSON.stringify(result, null, 2));
127
+ } else if (opts.compact) {
128
+ console.log(result.ok ? "rules:audit ok" : `rules:audit failed ${result.violations.length}`);
129
+ } else if (result.ok) {
130
+ console.log("rules:audit ok");
131
+ } else {
132
+ console.error("rules:audit failed");
133
+ for (const violation of result.violations) {
134
+ console.error(`${violation.code}: ${violation.message}`);
135
+ }
136
+ }
137
+
138
+ if (!result.ok) {
139
+ throw new Error(`rules audit failed: ${result.violations.map((v) => v.code).join(", ")}`);
140
+ }
141
+
142
+ return result;
143
+ }