deuk-agent-rule 2.5.13 → 3.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.ko.md +74 -0
- package/CHANGELOG.md +138 -316
- package/README.ko.md +134 -154
- package/README.md +121 -153
- package/package.json +29 -7
- package/scripts/cli-args.mjs +87 -3
- package/scripts/cli-init-commands.mjs +1382 -223
- package/scripts/cli-init-logic.mjs +28 -16
- package/scripts/cli-prompts.mjs +13 -4
- package/scripts/cli-rule-compiler.mjs +44 -34
- package/scripts/cli-skill-commands.mjs +172 -0
- package/scripts/cli-telemetry-commands.mjs +429 -0
- package/scripts/cli-ticket-commands.mjs +1934 -161
- package/scripts/cli-ticket-index.mjs +298 -0
- package/scripts/cli-ticket-migration.mjs +320 -0
- package/scripts/cli-ticket-parser.mjs +207 -0
- package/scripts/cli-utils.mjs +381 -59
- package/scripts/cli.mjs +99 -19
- package/scripts/lint-md.mjs +247 -0
- package/scripts/lint-rules.mjs +143 -0
- package/scripts/merge-logic.mjs +13 -306
- package/scripts/plan-parser.mjs +53 -0
- package/templates/MODULE_RULE_TEMPLATE.md +11 -0
- package/templates/PROJECT_RULE.md +47 -0
- package/templates/TICKET_TEMPLATE.ko.md +21 -0
- package/templates/TICKET_TEMPLATE.md +21 -0
- package/templates/rules.d/deukcontext-mcp.md +31 -0
- package/templates/rules.d/platform-coexistence.md +29 -0
- package/templates/skills/context-recall/SKILL.md +25 -0
- package/templates/skills/generated-file-guard/SKILL.md +25 -0
- package/templates/skills/safe-refactor/SKILL.md +25 -0
- package/bundle/.cursorrules +0 -11
- package/bundle/AGENTS.md +0 -146
- package/bundle/gemini.md +0 -26
- package/bundle/rules/delivery-and-parallel-work.mdc +0 -26
- package/bundle/rules/git-commit.mdc +0 -24
- package/bundle/rules/multi-ai-workflow.mdc +0 -104
- package/bundle/rules.d/core-workflow.md +0 -48
- package/bundle/rules.d/deukrag-mcp.md +0 -37
- package/bundle/templates/MODULE_RULE_TEMPLATE.md +0 -24
- package/bundle/templates/TICKET_TEMPLATE.md +0 -58
- package/scripts/cli-ticket-logic.mjs +0 -568
- package/scripts/sync-bundle.mjs +0 -77
- package/scripts/sync-oss.mjs +0 -126
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 {
|
|
9
|
-
import {
|
|
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
|
|
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,
|
|
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,
|
|
83
|
-
const configFile = join(opts.cwd,
|
|
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 && !
|
|
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
|
-
|
|
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
|
|
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>
|
|
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
|
-
--
|
|
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
|
+
}
|