deuk-agent-flow 4.0.19

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 (59) hide show
  1. package/CHANGELOG.ko.md +223 -0
  2. package/CHANGELOG.md +227 -0
  3. package/LICENSE +184 -0
  4. package/README.ko.md +282 -0
  5. package/README.md +270 -0
  6. package/bin/deuk-agent-flow.js +50 -0
  7. package/bin/deuk-agent-rule.js +2 -0
  8. package/core-rules/AGENTS.md +153 -0
  9. package/core-rules/GEMINI.md +7 -0
  10. package/docs/architecture.ko.md +34 -0
  11. package/docs/architecture.md +33 -0
  12. package/docs/assets/architecture-v3.png +0 -0
  13. package/docs/how-it-works.ko.md +52 -0
  14. package/docs/how-it-works.md +71 -0
  15. package/docs/principles.ko.md +68 -0
  16. package/docs/principles.md +68 -0
  17. package/docs/usage-guide.ko.md +212 -0
  18. package/package.json +96 -0
  19. package/scripts/cli-args.mjs +200 -0
  20. package/scripts/cli-init-commands.mjs +1799 -0
  21. package/scripts/cli-init-logic.mjs +64 -0
  22. package/scripts/cli-prompts.mjs +104 -0
  23. package/scripts/cli-rule-compiler.mjs +112 -0
  24. package/scripts/cli-skill-commands.mjs +201 -0
  25. package/scripts/cli-telemetry-commands.mjs +599 -0
  26. package/scripts/cli-ticket-commands.mjs +2393 -0
  27. package/scripts/cli-ticket-index.mjs +298 -0
  28. package/scripts/cli-ticket-migration.mjs +320 -0
  29. package/scripts/cli-ticket-parser.mjs +209 -0
  30. package/scripts/cli-usage-commands.mjs +326 -0
  31. package/scripts/cli-utils.mjs +587 -0
  32. package/scripts/cli.mjs +246 -0
  33. package/scripts/lint-md.mjs +267 -0
  34. package/scripts/lint-rules.mjs +186 -0
  35. package/scripts/merge-logic.mjs +44 -0
  36. package/scripts/plan-parser.mjs +53 -0
  37. package/scripts/publish-dual-npm.mjs +141 -0
  38. package/scripts/smoke-npm-docker.mjs +102 -0
  39. package/scripts/smoke-npm-local.mjs +109 -0
  40. package/scripts/update-download-badge.mjs +107 -0
  41. package/templates/MODULE_RULE_TEMPLATE.md +11 -0
  42. package/templates/PROJECT_RULE.md +47 -0
  43. package/templates/TICKET_TEMPLATE.ko.md +44 -0
  44. package/templates/TICKET_TEMPLATE.md +44 -0
  45. package/templates/project-pilot/CONFORMANCE_GATE_TEMPLATE.md +23 -0
  46. package/templates/project-pilot/DRIFT_CHECKLIST.md +19 -0
  47. package/templates/project-pilot/FLOW_CONTRACT_TEMPLATE.md +26 -0
  48. package/templates/project-pilot/IMPLEMENTATION_MATRIX_TEMPLATE.md +30 -0
  49. package/templates/project-pilot/INTEGRATION_CONTRACT_TEMPLATE.md +26 -0
  50. package/templates/project-pilot/OWNER_MAP_TEMPLATE.md +15 -0
  51. package/templates/project-pilot/PROJECT_PILOT_RULE_TEMPLATE.md +34 -0
  52. package/templates/project-pilot/REFACTOR_CONTRACT_TEMPLATE.md +32 -0
  53. package/templates/project-pilot/REMEDIATION_PLAN_TEMPLATE.md +33 -0
  54. package/templates/rules.d/deukcontext-mcp.md +31 -0
  55. package/templates/rules.d/platform-coexistence.md +29 -0
  56. package/templates/skills/context-recall/SKILL.md +25 -0
  57. package/templates/skills/generated-file-guard/SKILL.md +25 -0
  58. package/templates/skills/project-pilot/SKILL.md +63 -0
  59. package/templates/skills/safe-refactor/SKILL.md +25 -0
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, rmSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { parseArgs, parseTicketArgs, parseSkillArgs, parseTelemetryArgs, parseUsageArgs } from "./cli-args.mjs";
6
+ import { runInit, runMerge } from "./cli-init-commands.mjs";
7
+ import { runTicketCreate, runTicketList, runTicketUse, runTicketClose, runTicketArchive, runTicketDiscard, runTicketReports, runTicketMeta, runTicketConnect, runTicketRebuild, runTicketReportAttach, runTicketMove, runTicketNext, runTicketHotfix, runTicketStatus, runTicketGuard, runTicketHandoff, runTicketEvidenceCheck, runTicketEvidenceReport } from "./cli-ticket-commands.mjs";
8
+ import { runTelemetry } from "./cli-telemetry-commands.mjs";
9
+ import { runUsage } from "./cli-usage-commands.mjs";
10
+ import { performUpgradeMigration } from "./cli-ticket-migration.mjs";
11
+ import { loadInitConfig, writeInitConfig, checkUpdateNotifier, normalizeWorkflowMode, WORKFLOW_MODE_EXECUTE, AGENT_ROOT_DIR, resolveWorkflowMode, LEGACY_TEMPLATE_DIR, LEGACY_CONFIG_FILE } from "./cli-utils.mjs";
12
+ import { runInteractive } from "./cli-prompts.mjs";
13
+
14
+ const updatePromise = checkUpdateNotifier();
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const pkgRoot = join(__dirname, "..");
18
+ async function main() {
19
+ const argv = process.argv.slice(2);
20
+ const sub = argv[0];
21
+ if (!sub || sub === "-h" || sub === "--help" || sub === "help") {
22
+ printHelp();
23
+ return;
24
+ }
25
+
26
+ const rest = argv.slice(1);
27
+
28
+ if (sub === "ticket") {
29
+ const action = rest[0];
30
+ const opts = parseTicketArgs(rest.slice(1));
31
+ if (action === "create") await runTicketCreate(opts);
32
+ else if (action === "list") await runTicketList(opts);
33
+ else if (action === "use") await runTicketUse(opts);
34
+ else if (action === "next") await runTicketNext(opts);
35
+ else if (action === "evidence") await runTicketEvidenceCheck(opts);
36
+ else if (action === "close") await runTicketClose(opts);
37
+ else if (action === "archive") await runTicketArchive(opts);
38
+ else if (action === "discard" || action === "delete") await runTicketDiscard(opts);
39
+ else if (action === "reports") await runTicketReports(opts);
40
+ else if (action === "meta") await runTicketMeta(opts);
41
+ else if (action === "connect") await runTicketConnect(opts);
42
+ else if (action === "rebuild") await runTicketRebuild(opts);
43
+ else if (action === "move" || action === "step") await runTicketMove(opts);
44
+ else if (action === "hotfix") await runTicketHotfix(opts);
45
+ else if (action === "status") await runTicketStatus(opts);
46
+ else if (action === "guard" || action === "context") await runTicketGuard(opts);
47
+ else if (action === "handoff" || action === "continue") await runTicketHandoff(opts);
48
+ else if (action === "report") {
49
+ const subAction = rest[1];
50
+ if (subAction === "attach") {
51
+ const attachOpts = parseTicketArgs(rest.slice(2));
52
+ await runTicketReportAttach(attachOpts);
53
+ } else if (opts.claim) {
54
+ await runTicketEvidenceReport(opts);
55
+ } else {
56
+ await runTicketReports(opts);
57
+ }
58
+ }
59
+ else if (action === "upgrade" || action === "migrate") {
60
+ const count = performUpgradeMigration(opts.cwd, opts);
61
+ console.log(`Migration complete: ${count} tickets upgraded.`);
62
+ }
63
+ else {
64
+ console.error("Unknown ticket action: " + action);
65
+ printHelp();
66
+ }
67
+ return;
68
+ }
69
+
70
+ if (sub === "telemetry") {
71
+ const opts = parseTelemetryArgs(rest);
72
+ await runTelemetry(opts);
73
+ return;
74
+ }
75
+
76
+ if (sub === "usage") {
77
+ const action = rest[0];
78
+ const opts = parseUsageArgs(rest.slice(1));
79
+ await runUsage(action, opts);
80
+ return;
81
+ }
82
+
83
+ if (sub === "skill") {
84
+ const action = rest[0];
85
+ const opts = parseSkillArgs(rest.slice(1));
86
+ const { runSkill } = await import("./cli-skill-commands.mjs");
87
+ await runSkill(action, opts);
88
+ return;
89
+ }
90
+
91
+ if (sub === "lint:md" || sub === "lint-md") {
92
+ const { runMarkdownLint } = await import("./lint-md.mjs");
93
+ runMarkdownLint(rest);
94
+ return;
95
+ }
96
+
97
+ if (sub === "rules") {
98
+ const action = rest[0];
99
+ const opts = parseArgs(rest.slice(1));
100
+ if (action === "audit") {
101
+ const { runRulesAudit } = await import("./lint-rules.mjs");
102
+ runRulesAudit(opts);
103
+ return;
104
+ }
105
+ console.error("Unknown rules action: " + action);
106
+ printHelp();
107
+ return;
108
+ }
109
+
110
+ if (sub === "init" || sub === "merge") {
111
+ const opts = parseArgs(rest);
112
+ if (opts.help) {
113
+ printHelp();
114
+ return;
115
+ }
116
+
117
+ const saved = loadInitConfig(opts.cwd);
118
+ if (saved && !opts.interactive) {
119
+ // CLI flags (opts) take precedence over saved config
120
+ for (const key in saved) {
121
+ if (opts[key] === undefined) opts[key] = saved[key];
122
+ }
123
+ console.log(`Using saved config from ${AGENT_ROOT_DIR}/config.json (CLI overrides applied)`);
124
+ }
125
+
126
+ if (sub === "init") {
127
+ await handleInit(opts, saved);
128
+ } else {
129
+ runMerge(opts, pkgRoot);
130
+ }
131
+ return;
132
+ }
133
+
134
+ console.error("Unknown command: " + sub);
135
+ printHelp();
136
+ }
137
+
138
+ // Removed legacy migration runTicketMigrate
139
+
140
+ async function handleInit(opts, saved) {
141
+ if (opts.clean && !opts.dryRun) {
142
+ console.log(`[CLEAN] Removing legacy templates and config...`);
143
+ const templatesDir = join(opts.cwd, LEGACY_TEMPLATE_DIR);
144
+ const configFile = join(opts.cwd, LEGACY_CONFIG_FILE);
145
+ if (existsSync(templatesDir)) rmSync(templatesDir, { recursive: true, force: true });
146
+ if (existsSync(configFile)) rmSync(configFile, { force: true });
147
+ }
148
+
149
+ if (!opts.interactive && !opts.nonInteractive && !saved) {
150
+ // If no config and not interactive, prompt unless non-interactive
151
+ await runInteractive(opts);
152
+ } else if (opts.interactive) {
153
+ await runInteractive(opts);
154
+ }
155
+
156
+ if (!opts.dryRun) writeInitConfig(opts.cwd, opts);
157
+
158
+ const workflowMode = resolveWorkflowMode(opts, saved);
159
+ if (!opts.dryRun && workflowMode !== WORKFLOW_MODE_EXECUTE) {
160
+ console.log(`[WORKFLOW] Plan mode active. Re-run with --workflow execute or --approval approved to apply file mutations.`);
161
+ return;
162
+ }
163
+
164
+ await runInit(opts, pkgRoot);
165
+ }
166
+
167
+ function printHelp() {
168
+ console.log(`DeukAgentFlow CLI - Generalization Rules & Ticket Management
169
+
170
+ Usage:
171
+ npx deuk-agent-flow init [options]
172
+ npx deuk-agent-flow merge [options]
173
+ npx deuk-agent-flow lint:md [--cwd <path>] [files...]
174
+ npx deuk-agent-flow rules audit [--compact|--json]
175
+ npx deuk-agent-flow skill <list|add|expose|lint> [options]
176
+ npx deuk-agent-flow ticket <create|evidence|list|status|guard|context|handoff|continue|use|close|archive|discard|delete|reports|migrate|upgrade|meta|connect|move> [options]
177
+ npx deuk-agent-flow telemetry <log|sync|summary|migrate> [options]
178
+ npx deuk-agent-flow usage <set|status|advise> [options]
179
+
180
+ Options:
181
+ --cwd <path> Target repo root
182
+ --dry-run Print actions without writing
183
+ --non-interactive CI/scripts mode: no prompts
184
+ --tag <id> Custom marker ID (default: deuk-agent-rule)
185
+ --agents <mode> inject | skip | overwrite
186
+ --cursorrules <mode> inject | skip | overwrite
187
+ --workflow <mode> plan | execute
188
+ --approval <state> pending | approved (alias for workflow)
189
+ --docs-language <lang> auto | ko | en
190
+ --json Output result in JSON format
191
+ --remote <url> Temporary pipeline URL
192
+ --sync Force enable remote sync
193
+ --no-sync Force disable remote sync
194
+
195
+ Skill Options:
196
+ --skill, --id <name> Skill ID (safe-refactor|generated-file-guard|context-recall|project-pilot)
197
+ --platform <name> Exposure target (claude|cursor)
198
+
199
+ Ticket Options:
200
+ --topic, --id <name> Ticket topic slug or ID
201
+ --group <name> Ticket group (sub|main|discussion)
202
+ --project <name> Project filter (DeukUI|DeukAgentFlow)
203
+ --submodule <name> Submodule filter (DeukPack|DeukUI)
204
+ --docs-language <lang> auto | ko | en
205
+ --evidence <text> Provide Phase 0 RAG evidence summary
206
+ --claim <text> Validate or report only from ticket evidence matching this claim
207
+ --skip-phase0 Bypass Phase 0 RAG validation
208
+ --plan-body <text> Create ticket with filled Phase 1 markdown body
209
+ --plan-body-file <p> Read filled Phase 1 markdown from a file, or '-' for stdin
210
+ --content-file <path> Read extra ticket context from a file, or '-' for stdin
211
+ --require-filled Enforce non-placeholder APC and compact plan content before create succeeds
212
+ --allow-placeholder Opt out of strict create guard (legacy behavior)
213
+ --ticket-started Confirm the clickable ticket-start line was shared before guard/context
214
+ --ticket-reviewed Confirm the durable ticket body was reopened and reviewed before guard/context
215
+ --compact Prefer one-line ticket outputs in automation flows
216
+ --status-detail Include detailed reasons in ticket status output
217
+ --handoff Emit compact handoff output for session continuation
218
+ --phase <number> Explicitly set the phase number (e.g., --phase 2)
219
+ --next Move to the next phase
220
+ --latest, -l Use most recent ticket (default if no topic)
221
+ --path-only Print only the file path
222
+ --json Output result in JSON format
223
+
224
+ Usage Options:
225
+ --platform <name> Platform name (Codex/Copilot supported)
226
+ --client <name> Client label for usage state/output
227
+ --agent-id <id> Agent identifier for per-agent tracking
228
+ --weekly-remaining <pct> Remaining weekly percentage (0-100)
229
+ --five-hour-remaining <pct> Remaining 5-hour percentage (0-100)
230
+ --weekly-reset <text> Weekly reset time label
231
+ --five-hour-reset <text> 5-hour reset time label
232
+ --task-grade <grade> Task grade (S|A|B|C)
233
+ --task <label> Optional task label for advice output
234
+ --turn-count <n> Conversation turn count for split advice
235
+ --linked-ticket-count <n> Linked ticket count for split advice
236
+ --cross-workspace Mark the conversation as cross-workspace
237
+ `);
238
+ }
239
+
240
+ main().then(async () => {
241
+ await updatePromise;
242
+ }).catch(async err => {
243
+ await updatePromise;
244
+ console.error(err.message || err);
245
+ process.exit(1);
246
+ });
@@ -0,0 +1,267 @@
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 looksLikeYamlFrontmatter(content) {
65
+ if (!(content.startsWith("---\n") || content.startsWith("---\r\n"))) return false;
66
+ const afterOpening = content.replace(/^---\r?\n/, "");
67
+ const lines = afterOpening.split(/\r?\n/);
68
+ const frontmatterLines = [];
69
+
70
+ for (const line of lines) {
71
+ if (line.trim() === "---") break;
72
+ frontmatterLines.push(line);
73
+ }
74
+
75
+ if (frontmatterLines.length === 0) return false;
76
+
77
+ const firstNonEmpty = frontmatterLines.find(line => line.trim());
78
+ if (!firstNonEmpty) return false;
79
+ if (/^(#|>|- |\* |\d+\.)/.test(firstNonEmpty.trim())) return false;
80
+
81
+ return frontmatterLines.some(line => /^[A-Za-z0-9_-]+\s*:/.test(line.trim()));
82
+ }
83
+
84
+ function lintWalkthroughReportStructure(relPath, content) {
85
+ const errors = [];
86
+ const hasSummary = /##\s+(Summary|요약)(?:\s|$)/i.test(content);
87
+ const hasVerification = /##\s+(Verification|검증)(?:\s|$)/i.test(content);
88
+ const hasOutcome = /##\s+(Verification Outcome|Verification Results|검증 결과)(?:\s|$)/i.test(content);
89
+
90
+ if (!hasSummary) {
91
+ errors.push(`${relPath}: report missing Summary/요약 section`);
92
+ }
93
+ if (!hasVerification) {
94
+ errors.push(`${relPath}: report missing Verification/검증 section`);
95
+ }
96
+ if (!hasOutcome) {
97
+ errors.push(`${relPath}: report missing Verification Outcome/Verification Results/검증 결과 section`);
98
+ }
99
+
100
+ return errors;
101
+ }
102
+
103
+ function walkMarkdownFiles(rootDir, out = []) {
104
+ for (const entry of readdirSync(rootDir, { withFileTypes: true })) {
105
+ if (entry.isDirectory()) {
106
+ if (ignoredDirs.has(entry.name)) continue;
107
+ walkMarkdownFiles(join(rootDir, entry.name), out);
108
+ continue;
109
+ }
110
+ const filePath = join(rootDir, entry.name);
111
+ if (isMarkdownFile(filePath)) out.push(filePath);
112
+ }
113
+ return out;
114
+ }
115
+
116
+ function lintFile(absPath, repoRoot) {
117
+ const rel = relative(repoRoot, absPath);
118
+ const content = readFileSync(absPath, "utf8");
119
+ const errors = [];
120
+
121
+ if (isLegacyArchivedTemplateArtifact(rel, content)) {
122
+ return errors;
123
+ }
124
+
125
+ const lines = content.split(/\r?\n/);
126
+ lines.forEach((line, idx) => {
127
+ if (/\s+$/.test(line)) {
128
+ errors.push(`${rel}:${idx + 1}: trailing whitespace`);
129
+ }
130
+ });
131
+
132
+ const fenceCount = lines.filter(line => /^```/.test(line.trim())).length;
133
+ if (fenceCount % 2 !== 0) {
134
+ errors.push(`${rel}: unmatched fenced code block`);
135
+ }
136
+
137
+ if (looksLikeYamlFrontmatter(content)) {
138
+ const match = content.match(/^---\r?\n([\s\S]+?)\r?\n---\r?\n?/);
139
+ if (!match) {
140
+ const afterOpeningRule = content.replace(/^---\r?\n/, "");
141
+ if (!/^\s*\r?\n#{1,6}\s/.test(afterOpeningRule)) {
142
+ errors.push(`${rel}: invalid or unterminated frontmatter`);
143
+ }
144
+ } else {
145
+ try {
146
+ const parsed = YAML.parse(match[1]);
147
+ const isDeukAgentDoc = rel.includes(`${AGENT_ROOT_DIR}/docs/`) || rel.includes(`${AGENT_ROOT_DIR}/tickets/`);
148
+ const isArchive = rel.includes("archive/");
149
+ const isTemplate = rel.includes("templates/");
150
+ if (isDeukAgentDoc && !isArchive && !isTemplate) {
151
+ const requiredKeys = ["summary", "status", "priority", "tags"];
152
+ for (const key of requiredKeys) {
153
+ if (!parsed || parsed[key] === undefined || parsed[key] === null || parsed[key] === "") {
154
+ errors.push(`${rel}: missing required frontmatter key: ${key}`);
155
+ }
156
+ }
157
+ }
158
+ } catch (err) {
159
+ errors.push(`${rel}: invalid frontmatter YAML (${err.message})`);
160
+ }
161
+ }
162
+ } else {
163
+ const isDeukAgentDoc = rel.includes(`${AGENT_ROOT_DIR}/docs/`) || rel.includes(`${AGENT_ROOT_DIR}/tickets/`);
164
+ const isArchive = rel.includes("archive/");
165
+ const isTemplate = rel.includes("templates/");
166
+ if (isDeukAgentDoc && !isArchive && !isTemplate) {
167
+ errors.push(`${rel}: missing required frontmatter`);
168
+ }
169
+ }
170
+
171
+ const linkPattern = /!?\[[^\]]+\]\(([^)]+)\)/g;
172
+ let match;
173
+ while ((match = linkPattern.exec(content)) !== null) {
174
+ const target = String(match[1] || "").trim();
175
+ if (!target || target.startsWith("#") || /^[a-z]+:\/\//i.test(target) || target.startsWith("mailto:")) {
176
+ continue;
177
+ }
178
+ const pathOnly = target.split("#")[0].split("?")[0];
179
+ if (!pathOnly) continue;
180
+ const resolved = join(dirname(absPath), pathOnly);
181
+ if (!statExists(resolved)) {
182
+ errors.push(`${rel}: broken relative link -> ${target}`);
183
+ }
184
+ }
185
+
186
+ if (isPlanReport(rel)) {
187
+ errors.push(...lintWalkthroughReportStructure(rel, content));
188
+ }
189
+
190
+ return errors;
191
+ }
192
+
193
+ function statExists(absPath) {
194
+ try {
195
+ return statSync(absPath).isFile() || statSync(absPath).isDirectory();
196
+ } catch {
197
+ return false;
198
+ }
199
+ }
200
+
201
+ function resolveMarkdownLintTargets(repoRoot, explicitPaths = []) {
202
+ const files = explicitPaths.length > 0
203
+ ? explicitPaths.map(p => resolve(repoRoot, p)).filter(statExists).filter(isMarkdownFile)
204
+ : collectChangedMarkdownFiles(repoRoot).map(p => join(repoRoot, p));
205
+
206
+ const targets = files.length > 0 ? files : walkMarkdownFiles(repoRoot);
207
+ return Array.from(new Set(targets));
208
+ }
209
+
210
+ function parseLintArgs(argv) {
211
+ const out = { cwd: process.cwd(), paths: [] };
212
+ for (let i = 0; i < argv.length; i++) {
213
+ const arg = argv[i];
214
+ if (arg === "--cwd") {
215
+ out.cwd = argv[++i] || out.cwd;
216
+ } else if (arg.startsWith("--cwd=")) {
217
+ out.cwd = arg.slice("--cwd=".length);
218
+ } else if (!arg.startsWith("-")) {
219
+ out.paths.push(arg);
220
+ }
221
+ }
222
+ return out;
223
+ }
224
+
225
+ export function lintMarkdownPaths(paths, cwd = process.cwd()) {
226
+ const repoRoot = resolve(cwd);
227
+ const targets = Array.from(new Set(
228
+ (Array.isArray(paths) ? paths : [])
229
+ .map(p => resolve(repoRoot, p))
230
+ .filter(statExists)
231
+ .filter(isMarkdownFile)
232
+ ));
233
+ const errors = [];
234
+ for (const filePath of targets) {
235
+ errors.push(...lintFile(filePath, repoRoot));
236
+ }
237
+ return { repoRoot, targets, errors };
238
+ }
239
+
240
+ export function runMarkdownLint(argv = process.argv.slice(2)) {
241
+ const opts = parseLintArgs(argv);
242
+ const repoRoot = resolve(opts.cwd);
243
+ const targets = opts.paths.length > 0
244
+ ? lintMarkdownPaths(opts.paths, opts.cwd).targets
245
+ : resolveMarkdownLintTargets(repoRoot);
246
+ const errors = [];
247
+ for (const filePath of targets) {
248
+ errors.push(...lintFile(filePath, repoRoot));
249
+ }
250
+
251
+ if (targets.length === 0) {
252
+ console.log("lint:md: no markdown files found");
253
+ return;
254
+ }
255
+
256
+ if (errors.length > 0) {
257
+ console.error("lint:md failed");
258
+ for (const err of errors) console.error(`- ${err}`);
259
+ process.exit(1);
260
+ }
261
+
262
+ console.log(`lint:md passed (${targets.length} file${targets.length === 1 ? "" : "s"})`);
263
+ }
264
+
265
+ if (import.meta.url === `file://${process.argv[1]}`) {
266
+ runMarkdownLint();
267
+ }