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,64 @@
1
+ import { existsSync, appendFileSync, writeFileSync, mkdirSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { AGENT_ROOT_DIR, LEGACY_IGNORE_DIR, LEGACY_TICKET_DIR, LEGACY_TICKET_DIR_PLURAL, TICKET_DIR_NAME } from "./cli-utils.mjs";
4
+
5
+ const GITIGNORE_AGENT_MARKER = "# deuk-agent-flow: agent hub directory (local, not committed by default)";
6
+ const LEGACY_GITIGNORE_AGENT_MARKER = "# deuk-agent-rule: agent hub directory (local, not committed by default)";
7
+
8
+ function hasExactGitignoreLine(content, line) {
9
+ return content
10
+ .split(/\r?\n/)
11
+ .map(s => s.trim())
12
+ .includes(line);
13
+ }
14
+
15
+ function removeExactGitignoreLines(content, lines) {
16
+ const remove = new Set(lines);
17
+ return content
18
+ .split(/\r?\n/)
19
+ .filter(line => !remove.has(line.trim()))
20
+ .join("\n");
21
+ }
22
+
23
+ export function ensureTicketDirAndGitignore(opts) {
24
+ const ticketPath = join(opts.cwd, TICKET_DIR_NAME);
25
+ const gitignorePath = join(opts.cwd, ".gitignore");
26
+ const ignoreLine = AGENT_ROOT_DIR + "/";
27
+
28
+ if (opts.dryRun) return;
29
+
30
+ mkdirSync(ticketPath, { recursive: true });
31
+ if (opts.shareTickets) {
32
+ console.log(`[INIT] Ticket sharing enabled. Skipping .gitignore entry for ${AGENT_ROOT_DIR}/`);
33
+ return;
34
+ }
35
+
36
+ let gi = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf8") : "";
37
+
38
+ // 1. Create document directories
39
+ const docsPath = join(opts.cwd, AGENT_ROOT_DIR, "docs");
40
+ mkdirSync(join(docsPath, "plan"), { recursive: true });
41
+ mkdirSync(join(docsPath, "archive"), { recursive: true });
42
+
43
+ // Also check for legacy ignore lines to clean up or at least check presence
44
+ const legacyIgnore1 = `${LEGACY_TICKET_DIR}/`;
45
+ const legacyIgnore2 = `${LEGACY_TICKET_DIR_PLURAL}/`;
46
+ const legacyIgnore3 = LEGACY_IGNORE_DIR;
47
+ const cleanedGi = removeExactGitignoreLines(gi, [legacyIgnore1, legacyIgnore2, legacyIgnore3, LEGACY_GITIGNORE_AGENT_MARKER]);
48
+ if (cleanedGi !== gi) {
49
+ writeFileSync(gitignorePath, cleanedGi.replace(/\n*$/, "\n"), "utf8");
50
+ gi = cleanedGi;
51
+ console.log("[INIT] Removed legacy deuk-agent-rule .gitignore entries");
52
+ }
53
+
54
+ const hasIgnoreLine = hasExactGitignoreLine(gi, ignoreLine);
55
+ const hasMarkerLine = hasExactGitignoreLine(gi, GITIGNORE_AGENT_MARKER);
56
+ if ((!hasIgnoreLine || !hasMarkerLine) && !opts.shareTickets) {
57
+ const linesToAdd = [];
58
+ if (!hasMarkerLine) linesToAdd.push(GITIGNORE_AGENT_MARKER);
59
+ if (!hasIgnoreLine) linesToAdd.push(ignoreLine);
60
+ const block = "\n" + linesToAdd.join("\n") + "\n";
61
+ appendFileSync(gitignorePath, block, "utf8");
62
+ console.log(`[INIT] Added ${ignoreLine} to .gitignore`);
63
+ }
64
+ }
@@ -0,0 +1,104 @@
1
+ import { createInterface } from "readline";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { STACKS, AGENT_TOOLS, DOC_LANGUAGE_CHOICES, resolveDocsLanguage, normalizeWorkflowMode, WORKFLOW_MODE_EXECUTE, WORKFLOW_MODE_PLAN } from "./cli-utils.mjs";
5
+
6
+ export async function ask(rl, question) {
7
+ return new Promise((resolve) => rl.question(question, resolve));
8
+ }
9
+
10
+ export async function askYesNo(question, defaultYes = true) {
11
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
12
+ try {
13
+ const ans = (await ask(rl, question + (defaultYes ? " [Y/n]: " : " [y/N]: "))).trim().toLowerCase();
14
+ if (!ans) return defaultYes;
15
+ return ans === "y" || ans === "yes";
16
+ } finally {
17
+ rl.close();
18
+ }
19
+ }
20
+
21
+ export async function selectOne(rl, prompt, choices) {
22
+ console.log("\n" + prompt);
23
+ choices.forEach((c, i) => console.log(` ${i + 1}) ${c.label}`));
24
+ while (true) {
25
+ const ans = (await ask(rl, ` Choice [1-${choices.length}]: `)).trim();
26
+ const idx = parseInt(ans, 10) - 1;
27
+ if (idx >= 0 && idx < choices.length) return choices[idx].value;
28
+ }
29
+ }
30
+
31
+ export async function selectMany(rl, prompt, choices) {
32
+ console.log("\n" + prompt + " (comma-separated numbers, or 'all')");
33
+ choices.forEach((c, i) => console.log(` ${i + 1}) ${c.label}`));
34
+ while (true) {
35
+ const ans = (await ask(rl, ` Choices: `)).trim().toLowerCase();
36
+ if (ans === "all" || ans === "") return choices.map((c) => c.value);
37
+ const parts = ans.split(/[,\s]+/).map((s) => parseInt(s, 10) - 1);
38
+ if (parts.every((i) => i >= 0 && i < choices.length)) return parts.map((i) => choices[i].value);
39
+ }
40
+ }
41
+
42
+
43
+
44
+ export async function runInteractive(opts) {
45
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
46
+ try {
47
+ console.log("\nDeukAgentFlow init — let's configure your workspace.\n");
48
+
49
+ const stack = await selectOne(rl, "What is your primary tech stack?", STACKS);
50
+ const tools = await selectMany(rl, "Which agent tools do you use?", AGENT_TOOLS);
51
+ const docsLanguage = await selectOne(rl, "What document language should generated tickets/plans use?", DOC_LANGUAGE_CHOICES);
52
+ const workflowMode = opts.workflowMode
53
+ ? normalizeWorkflowMode(opts.workflowMode)
54
+ : await selectOne(rl, "What workflow mode should be saved?", [
55
+ { label: "Plan mode (prepare only)", value: WORKFLOW_MODE_PLAN },
56
+ { label: "Execute mode (apply changes)", value: WORKFLOW_MODE_EXECUTE },
57
+ ]);
58
+ const shareTickets = await askYesNo("Do you want to share (git-track) tickets for this repository?", false);
59
+
60
+ const targetAgents = join(opts.cwd, "AGENTS.md");
61
+ let agentsDefault = "inject";
62
+ if (!existsSync(targetAgents)) {
63
+ agentsDefault = "inject"; // will append markers
64
+ console.log("\n No AGENTS.md found — will create with markers.");
65
+ } else {
66
+ const content = readFileSync(targetAgents, "utf8");
67
+ const hasMarkers = content.includes("deuk-agent-rule:begin") || content.includes("## DeukAgentFlow");
68
+ if (!hasMarkers) {
69
+ const choice = await selectOne(rl, "AGENTS.md exists but has no markers. How to apply?", [
70
+ { label: "Append managed block at the end (safe)", value: "inject" },
71
+ { label: "Overwrite entire AGENTS.md", value: "overwrite" },
72
+ { label: "Skip AGENTS.md", value: "skip" },
73
+ ]);
74
+ agentsDefault = choice;
75
+ }
76
+ }
77
+
78
+ const remoteSync = opts.remoteSync !== undefined ? opts.remoteSync : (await askYesNo("Enable AI Pipeline remote synchronization? (Advanced)", false));
79
+ let pipelineUrl = opts.pipelineUrl || "";
80
+ if (remoteSync && !pipelineUrl) {
81
+ pipelineUrl = (await ask(rl, "Enter AI Pipeline Endpoint URL: ")).trim();
82
+ }
83
+
84
+ opts.agents = opts.agents ?? agentsDefault;
85
+ opts.stack = stack;
86
+ opts.agentTools = tools;
87
+ opts.docsLanguage = resolveDocsLanguage(docsLanguage);
88
+ opts.workflowMode = normalizeWorkflowMode(opts.workflowMode || workflowMode);
89
+ opts.shareTickets = shareTickets;
90
+ opts.remoteSync = remoteSync;
91
+ opts.pipelineUrl = pipelineUrl;
92
+
93
+ console.log("\n Stack : " + stack);
94
+ console.log(" Tools : " + (tools.join(", ") || "none"));
95
+ console.log(" Docs Language: " + opts.docsLanguage);
96
+ console.log(" Workflow Mode: " + opts.workflowMode);
97
+ console.log(" Share Tickets: " + (opts.shareTickets ? "Yes (Shared)" : "No (Private)"));
98
+ console.log(" Remote Sync: " + (opts.remoteSync ? "Enabled" : "Disabled"));
99
+ if (opts.remoteSync) console.log(" Pipeline URL: " + opts.pipelineUrl);
100
+ console.log(" AGENTS: " + opts.agents + "\n");
101
+ } finally {
102
+ rl.close();
103
+ }
104
+ }
@@ -0,0 +1,112 @@
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, "templates", "rules.d");
12
+ const localRulesDir = join(cwd, AGENT_ROOT_DIR, "project-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
+ try {
34
+ const rawContent = readFileSync(filePath, "utf8");
35
+ const { meta, content } = parseFrontMatter(rawContent);
36
+
37
+ // Check if this rule is intended for the current target file (e.g., AGENTS.md)
38
+ if (meta.inject_target && !meta.inject_target.includes(targetFileName)) {
39
+ continue;
40
+ }
41
+
42
+ // Evaluate conditions
43
+ let shouldInclude = true;
44
+ if (meta.condition) {
45
+ shouldInclude = evaluateCondition(meta.condition, cwd);
46
+ }
47
+
48
+ if (shouldInclude) {
49
+ const sourceId = meta.id || basename(filePath);
50
+ compiledContent += `\n<!-- RULE MODULE: ${sourceId} -->\n`;
51
+ compiledContent += content.trim() + "\n";
52
+ }
53
+ } catch (err) {
54
+ console.warn(`[WARNING] Skipping malformed rule file at ${filePath}:`, err.message);
55
+ continue;
56
+ }
57
+ }
58
+
59
+ return compiledContent.trim();
60
+ }
61
+
62
+ /**
63
+ * Attempts to locate and parse the DeukContext config.yaml from the workspace root.
64
+ */
65
+ function resolveDeukContextConfig(cwd) {
66
+ // Go up directories until we find a sibling DeukAgentContext folder, or hit root
67
+ let current = resolve(cwd);
68
+ while (current && current !== "/") {
69
+ const candidates = [
70
+ join(current, "DeukAgentContext", ".local", "config.yaml"),
71
+ join(current, "DeukContext", ".local", "config.yaml") // Legacy fallback
72
+ ];
73
+ for (const candidatePath of candidates) {
74
+ if (existsSync(candidatePath)) {
75
+ try {
76
+ const raw = readFileSync(candidatePath, "utf8");
77
+ return YAML.parse(raw);
78
+ } catch (e) {
79
+ console.error("Failed to parse DeukContext config.yaml:", e);
80
+ return null;
81
+ }
82
+ }
83
+ }
84
+ const parent = dirname(current);
85
+ if (parent === current) break;
86
+ current = parent;
87
+ }
88
+ return null;
89
+ }
90
+
91
+ /**
92
+ * Evaluates Frontmatter conditions to determine if a rule should be included.
93
+ */
94
+ function evaluateCondition(condition, cwd) {
95
+ if (!condition) return true;
96
+
97
+ // Example: condition: { mcp: "deuk-agent-context" }
98
+ if (condition.mcp === "deuk-agent-context" || condition.mcp === "deuk_agent_context") {
99
+ const ragConfig = resolveDeukContextConfig(cwd);
100
+ if (!ragConfig || !ragConfig.projects) return false;
101
+
102
+ // Check if the current cwd is managed by DeukContext
103
+ const isManaged = ragConfig.projects.some(p => {
104
+ // If the project path is a prefix of cwd, it's managed
105
+ return cwd.startsWith(p.path);
106
+ });
107
+
108
+ return isManaged;
109
+ }
110
+
111
+ return true;
112
+ }
@@ -0,0 +1,201 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { AGENT_ROOT_DIR } from "./cli-utils.mjs";
5
+
6
+ const SKILL_IDS = ["safe-refactor", "generated-file-guard", "context-recall", "project-pilot"];
7
+ const SKILL_ROOT = "templates/skills";
8
+ const CONFIG_FILE = `${AGENT_ROOT_DIR}/skills.json`;
9
+ const REPO_SKILL_TEMPLATE_ROOT = `${AGENT_ROOT_DIR}/skill-templates`;
10
+ const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
11
+
12
+ function repoSkillPath(cwd, id) {
13
+ return join(cwd, AGENT_ROOT_DIR, "skills", id, "SKILL.md");
14
+ }
15
+
16
+ function sourceSkillPath(cwd, id) {
17
+ const localTemplate = join(cwd, REPO_SKILL_TEMPLATE_ROOT, id, "SKILL.md");
18
+ if (existsSync(localTemplate)) return localTemplate;
19
+ const repoTemplate = join(cwd, SKILL_ROOT, id, "SKILL.md");
20
+ if (existsSync(repoTemplate)) return repoTemplate;
21
+ return join(PACKAGE_ROOT, SKILL_ROOT, id, "SKILL.md");
22
+ }
23
+
24
+ function loadSkillSource(cwd, id) {
25
+ const source = sourceSkillPath(cwd, id);
26
+ if (!existsSync(source)) throw new Error(`skill not found: ${id}`);
27
+ return readFileSync(source, "utf8");
28
+ }
29
+
30
+ function loadSkillConfig(cwd) {
31
+ const path = join(cwd, CONFIG_FILE);
32
+ if (!existsSync(path)) return { version: 1, installed: [], exposed: {} };
33
+ return JSON.parse(readFileSync(path, "utf8"));
34
+ }
35
+
36
+ function detectExposedPlatforms(cwd, id) {
37
+ const platforms = [];
38
+ const claudePath = join(cwd, ".claude", "skills", id, "SKILL.md");
39
+ if (existsSync(claudePath)) {
40
+ platforms.push("claude");
41
+ }
42
+
43
+ const cursorPath = join(cwd, ".cursor", "rules", "deuk-agent-skills.mdc");
44
+ if (existsSync(cursorPath)) {
45
+ try {
46
+ const body = readFileSync(cursorPath, "utf8");
47
+ if (body.includes(`.deuk-agent/skills/${id}/SKILL.md`)) {
48
+ platforms.push("cursor");
49
+ }
50
+ } catch {
51
+ // Ignore pointer read failures and fall back to config-based exposure only.
52
+ }
53
+ }
54
+
55
+ return platforms;
56
+ }
57
+
58
+ function writeSkillConfig(cwd, config, dryRun) {
59
+ if (dryRun) return;
60
+ const path = join(cwd, CONFIG_FILE);
61
+ mkdirSync(dirname(path), { recursive: true });
62
+ writeFileSync(path, JSON.stringify(config, null, 2), "utf8");
63
+ }
64
+
65
+ function ensureKnownSkill(id) {
66
+ if (!SKILL_IDS.includes(id)) throw new Error(`unknown skill: ${id}`);
67
+ }
68
+
69
+ export function listSkills(cwd = process.cwd()) {
70
+ const config = loadSkillConfig(cwd);
71
+ return SKILL_IDS.map(id => ({
72
+ id,
73
+ installed: config.installed.includes(id) || existsSync(repoSkillPath(cwd, id)),
74
+ exposed: Array.from(new Set([
75
+ ...Object.entries(config.exposed || {})
76
+ .filter(([, ids]) => Array.isArray(ids) && ids.includes(id))
77
+ .map(([platform]) => platform),
78
+ ...detectExposedPlatforms(cwd, id)
79
+ ]))
80
+ }));
81
+ }
82
+
83
+ export function addSkill(opts = {}) {
84
+ const cwd = opts.cwd || process.cwd();
85
+ const id = opts.skill;
86
+ ensureKnownSkill(id);
87
+ const body = loadSkillSource(cwd, id);
88
+ const target = repoSkillPath(cwd, id);
89
+ const config = loadSkillConfig(cwd);
90
+
91
+ if (!opts.dryRun) {
92
+ mkdirSync(dirname(target), { recursive: true });
93
+ writeFileSync(target, body, "utf8");
94
+ }
95
+
96
+ if (!config.installed.includes(id)) config.installed.push(id);
97
+ writeSkillConfig(cwd, config, opts.dryRun);
98
+ return { id, target };
99
+ }
100
+
101
+ function exposeClaude(cwd, ids, dryRun) {
102
+ for (const id of ids) {
103
+ const body = readFileSync(repoSkillPath(cwd, id), "utf8");
104
+ const target = join(cwd, ".claude", "skills", id, "SKILL.md");
105
+ if (!dryRun) {
106
+ mkdirSync(dirname(target), { recursive: true });
107
+ writeFileSync(target, body, "utf8");
108
+ }
109
+ }
110
+ }
111
+
112
+ function exposeCursor(cwd, ids, dryRun) {
113
+ const target = join(cwd, ".cursor", "rules", "deuk-agent-skills.mdc");
114
+ const body = [
115
+ "---",
116
+ "description: \"DeukAgentFlow skill pointers\"",
117
+ "globs: [\"**/*\"]",
118
+ "alwaysApply: false",
119
+ "---",
120
+ "# DeukAgentFlow Skills",
121
+ "",
122
+ "These are thin behavior playbooks. They do not override `core-rules/AGENTS.md`, TDW, APC, Phase Gate, or PROJECT_RULE.md.",
123
+ "",
124
+ ...ids.map(id => `- ${id}: .deuk-agent/skills/${id}/SKILL.md`),
125
+ ""
126
+ ].join("\n");
127
+ if (!dryRun) {
128
+ mkdirSync(dirname(target), { recursive: true });
129
+ writeFileSync(target, body, "utf8");
130
+ }
131
+ }
132
+
133
+ export function exposeSkills(opts = {}) {
134
+ const cwd = opts.cwd || process.cwd();
135
+ const platform = String(opts.platform || "").toLowerCase();
136
+ if (!["claude", "cursor"].includes(platform)) throw new Error("skill expose requires --platform claude|cursor");
137
+
138
+ const config = loadSkillConfig(cwd);
139
+ const ids = config.installed || [];
140
+ if (ids.length === 0) throw new Error("no skills installed; run skill add first");
141
+ for (const id of ids) {
142
+ if (!existsSync(repoSkillPath(cwd, id))) addSkill({ cwd, skill: id, dryRun: opts.dryRun });
143
+ }
144
+
145
+ if (platform === "claude") exposeClaude(cwd, ids, opts.dryRun);
146
+ if (platform === "cursor") exposeCursor(cwd, ids, opts.dryRun);
147
+
148
+ config.exposed = config.exposed || {};
149
+ config.exposed[platform] = Array.from(new Set([...(config.exposed[platform] || []), ...ids]));
150
+ writeSkillConfig(cwd, config, opts.dryRun);
151
+ return { platform, ids };
152
+ }
153
+
154
+ export function lintSkills(cwd = process.cwd()) {
155
+ const paths = [];
156
+ for (const root of [join(cwd, SKILL_ROOT), join(cwd, REPO_SKILL_TEMPLATE_ROOT), join(cwd, AGENT_ROOT_DIR, "skills")]) {
157
+ if (!existsSync(root)) continue;
158
+ for (const id of readdirSync(root)) {
159
+ const path = join(root, id, "SKILL.md");
160
+ if (existsSync(path)) paths.push(path);
161
+ }
162
+ }
163
+
164
+ const violations = [];
165
+ const forbidden = [/ignore ticket/i, /skip verification/i, /override (tdw|apc|phase gate)/i, /edit generated output directly/i];
166
+ for (const path of paths) {
167
+ const body = readFileSync(path, "utf8");
168
+ if (!/Authority:.*core-rules\/AGENTS\.md/i.test(body)) violations.push(`${path}: missing authority line`);
169
+ for (const pattern of forbidden) {
170
+ if (pattern.test(body)) violations.push(`${path}: forbidden phrase ${pattern}`);
171
+ }
172
+ }
173
+ return { ok: violations.length === 0, paths, violations };
174
+ }
175
+
176
+ export async function runSkill(action, opts = {}) {
177
+ if (action === "list") {
178
+ const rows = listSkills(opts.cwd);
179
+ if (opts.json) console.log(JSON.stringify(rows, null, 2));
180
+ else rows.forEach(row => console.log(`${row.id} installed=${row.installed ? "yes" : "no"} exposed=${row.exposed.join(",") || "-"}`));
181
+ return;
182
+ }
183
+ if (action === "add") {
184
+ const result = addSkill(opts);
185
+ console.log(`skill added: ${result.id}`);
186
+ return;
187
+ }
188
+ if (action === "expose") {
189
+ const result = exposeSkills(opts);
190
+ console.log(`skills exposed: ${result.platform} ${result.ids.join(",")}`);
191
+ return;
192
+ }
193
+ if (action === "lint") {
194
+ const result = lintSkills(opts.cwd);
195
+ if (opts.json) console.log(JSON.stringify(result, null, 2));
196
+ else console.log(result.ok ? "skill:lint ok" : `skill:lint failed ${result.violations.length}`);
197
+ if (!result.ok) throw new Error(result.violations.join("\n"));
198
+ return;
199
+ }
200
+ throw new Error("Unknown skill action: " + action);
201
+ }