deuk-agent-rule 1.0.13 → 2.2.1

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.
@@ -1,55 +1,105 @@
1
1
  ---
2
- description: Handoff format rules for multi-AI workflow
2
+ description: Ticket format rules for multi-AI workflow
3
3
  alwaysApply: true
4
4
  ---
5
5
 
6
6
  # Multi-AI Workflow
7
7
 
8
+ ## HIGHEST PRIORITY RULE (Tone, Documentation & Formatting)
9
+ Never use dramatic, exaggerated, or absolute language. When authoring any document or ticket, you MUST NEVER use expressions that emphasize dramatic effects or imply absolute completeness/perfection. Maintain a strictly factual, calm, dry tone at all times.
10
+ HARD RULE: Never use Markdown word emphasis (bold **, italic *, etc.) in any generated content, including tickets, reports, and chat responses. Keep the text uniformly plain.
11
+
8
12
  ## Before starting work
9
13
 
10
- Before implementation, fixes, or other **substantive** changes to the repo:
14
+ Before implementation, fixes, or other substantive changes to the repo:
11
15
 
12
- 1. **Check persisted handoffs** in the locations described in `AGENTS.md` (**Handoff persistence**) — at minimum inspect **`.deuk-agent-handoff/`** and any project-specific internal paths for files related to the current task.
13
- 2. **If `DeukAgentRules/handoff/LATEST.md` exists** (repo root–relative path: rules package cloned as top-level folder `DeukAgentRules`), **read it** before editing code, in addition to step 1.
14
- 3. **Read** matching documents before editing code. If the user **pasted** a handoff in the chat, treat that as primary unless they say to follow on-disk files instead.
15
- 4. Skip the scan only when no handoff locations exist, nothing matches the task, or the user tells you to ignore stored handoffs.
16
+ 1. Check persisted tickets in the locations described in AGENTS.md (Ticket persistence) — at minimum inspect .deuk-agent-ticket/ and any project-specific internal paths for files related to the current task.
17
+ 2. If .deuk-agent-ticket/TICKET_LIST.md exists, read it before editing code, then open only relevant topic files, in addition to step 1.
18
+ 3. Read matching documents before editing code. If the user pasted a ticket in the chat, treat that as primary unless they say to follow on-disk files instead.
19
+ 4. Skip the scan only when no ticket locations exist, nothing matches the task, or the user tells you to ignore stored tickets.
16
20
 
17
- ## Receiving Handoff from Other Tools
21
+ ## Receiving Ticket from Other Tools
18
22
 
19
23
  When the user pastes a design or plan from another assistant or planning tool:
20
24
 
21
- 1. Parse the **handoff format** (see `AGENTS.md`, heading **Handoff format**).
25
+ 1. Parse the ticket format (see AGENTS.md, heading Ticket format).
22
26
  2. Validate file paths exist before modifying.
23
27
  3. Execute changes as specified — do not redesign unless the user asks.
24
- 4. Report back in the same handoff format for further iteration.
28
+ 4. Report back in the same ticket format for further iteration.
25
29
 
26
- ## Producing Handoff for Other Tools
30
+ ## Producing Ticket for Other Tools
27
31
 
28
32
  When the user asks to prepare work for another tool:
29
33
 
30
- 1. Output the handoff format with concrete file paths and change descriptions. When linking handoff files (Markdown `[label](path)` or path citations), use the **full path from the repository root** — never a bare filename.
34
+ 1. Output the ticket format with concrete file paths and change descriptions. When linking ticket files (Markdown [label](path) or path citations), use the full path from the repository root — never a bare filename.
31
35
  2. Include any constraints or dependencies.
32
36
  3. Do not include editor-specific instructions (token budgets, local rule file paths).
33
- 4. When the handoff must **persist** (user asks to save or document it, or it is the canonical next-step spec), **create or update** a Markdown file under **internal implementation documentation** (see `AGENTS.md`, **Handoff persistence**). Prefer **`DeukAgentRules/handoff/LATEST.md`** when this repo has a top-level **`DeukAgentRules/`** folder; otherwise prefer **`.deuk-agent-handoff/`**; `init` gitignores that directory by default. Use the same handoff template; do not place this in public README/landing unless the project explicitly treats this as its internal log. In Markdown links and backtick citations, use the **full path from the repository root** — never a bare filename.
34
- 5. **After writing a persisted handoff**, in chat include **one dedicated line** with the full repo-root-relative path so the next session can open it unambiguously, for example: `Path: \`.deuk-agent-handoff/LATEST.md\`` (same path you used in the file). Repeat the full path in links elsewhere in the same message if helpful.
35
- 6. **Optional first line inside the persisted file** (improves search and skim): `**Handoff (repo-relative):** \`path/from/repo/root.md\`` or the same path in an HTML comment on line 1.
36
- 7. Write persisted handoff **content** in the **user's conversation language** unless they specify another language.
37
+ 4. When the ticket must persist (user asks to save or document it, or it is the canonical next-step spec), create or update a Markdown file under internal implementation documentation (see AGENTS.md, Ticket persistence). Prefer the unified .deuk-agent-ticket/ directory for both the index (TICKET_LIST.md) and the topic files; init gitignores .deuk-agent-ticket/ by default. Use the same ticket template; do not place this in public README/landing unless the project explicitly treats this as its internal log. In Markdown links and backtick citations, use the full path from the repository root — never a bare filename.
38
+ 5. After writing a persisted ticket, in chat include one dedicated line with the full repo-root-relative path so the next session can open it unambiguously, for example: `Path: .deuk-agent-ticket/sub/container-unified-20260329-120000.md` (same path you used in the file). Repeat the full path in links elsewhere in the same message if helpful.
39
+ 6. Optional last line inside the persisted file (improves search and skim): `<!-- Ticket (repo-relative): path/from/repo/root.md -->` on the very last line.
40
+ 7. Write persisted ticket content in the user's conversation language (e.g., Korean) unless they specify another language. When using Korean, follow the concise polite style (-요) as described in AGENTS.md.
41
+
42
+ ## Context Preservation (New Issues)
43
+
44
+ If a new bug, unexpected error, or structural scope change arises during your work, DO NOT attempt to fix it within an endless conversational loop. This degrades the context window.
45
+ Instead, immediately pause and Prioritize Creating a Ticket.
46
+ - Document your findings and create a new .deuk-agent-ticket/ Markdown file.
47
+ - This preserves the context safely and allows transparent multi-agent handoffs or manual rollbacks.
48
+
49
+ ## Custom Slash Commands & Chat Selection UI
50
+
51
+ If the user's prompt starts with /ticket or /티켓 (with an optional keyword like DeukPack), immediately pause and perform the following:
52
+ 1. Fetch & Present (Rich UI Selection): Do not just list text. Automatically read .deuk-agent-ticket/TICKET_LIST.md and present the available open tickets using a Carousel (preferred for compatible agents like Antigravity) or a Markdown Table (fallback).
53
+ - Carousel Slide Format:
54
+ ```markdown
55
+ ## [Ticket ID] Title
56
+ Group: [Group] | Project: [Project]
57
+ [Open Ticket](full/path/to/ticket.md)
58
+ ```
59
+ - Table Format: Use a clearly formatted table with a separate column for the [Open](path) link.
60
+ 2. Read & Contextualize: If the user selects a ticket (by number or clicking the link), immediately read the ticket content and summarize the latest constraints and objectives.
61
+ 3. Wait for command: Briefly summarize the active ticket state and wait for the user's next command.
62
+
63
+ ## Selection Box (Agent Chat Interaction)
64
+
65
+ When the user asks to "choose" or "select" a ticket in the chat, the agent must provide a Selection Box-like experience:
66
+ - Interactive Listing: Provide a numbered listing using the Carousel or Table format above.
67
+ - Direct Linkage: Ensure every listed item has a clickable repository-root-relative path.
68
+ - No CLI output only: Never just point to ticket list CLI output; always render the human-readable selection UI directly in the chat window.
69
+
70
+ ## Persisted ticket vs plan UI (optional)
71
+
72
+ Some environments show plan-style documents in a separate panel from ordinary Markdown tabs. Behavior varies by product version; when the user wants that experience in Cursor, you may mirror the same ticket body (Ticket format) under the workspace .cursor/plans/ directory using a .plan.md suffix, for example .cursor/plans/deuk-ticket.plan.md or .cursor/plans/deuk-ticket-<topic>.plan.md.
73
+
74
+ - Canonical copy remains the unified .deuk-agent-ticket/ directory, where both the topic files and the TICKET_LIST.md index pointer are located. The .cursor/plans/ file is an optional duplicate for UI only.
75
+ - Single source of truth: If both exist, keep their content in sync; updating one should update the other in the same turn when possible.
76
+ - Version control: Treat .cursor/plans/ like other local-only tickets unless the team commits plans on purpose — align with .gitignore team policy.
77
+ - For temporary tickets within a single session, write them inline in chat; when repeatedly referenced, shared across multiple agents, or recording risks, convert to an internal .md file. The default storage path is the local .deuk-agent-ticket/ directory; commit to the repository only when requested by the user or following established team conventions.
78
+
79
+ ## Definitive Multi-AI Workflow Cycles
37
80
 
38
- ## Persisted handoff vs plan UI (optional)
81
+ The core philosophy of this project dictates a strict separation of 'Reasoning' and 'Execution' via boundary tickets. The definitive workflow cycle is:
39
82
 
40
- Some environments show **plan-style documents** in a separate panel from ordinary Markdown tabs. Behavior varies by product version; when the user wants that experience in **Cursor**, you may **mirror** the same handoff body (Handoff format) under the workspace **`.cursor/plans/`** directory using a **`.plan.md`** suffix, for example `.cursor/plans/deuk-handoff.plan.md` or `.cursor/plans/deuk-handoff-<topic>.plan.md`.
83
+ 1. Explore & Plan (Reasoning AI): High-reasoning agents (e.g., Gemini, Claude) explore the codebase, research constraints, and propose an implementation plan.
84
+ 2. Decision (User): The user actively reviews and approves the plan.
85
+ 3. Persist Ticket (Handoff): The reasoning agent explicitly writes the approved plan into a .deuk-agent-ticket/ Markdown file.
86
+ 4. Execution (Coding AI): IDE execution agents (e.g., Cursor, Windsurf) read the single ticket and strictly execute the code without deviating from the constraints.
87
+ 5. Self-Verification (Post-Test Artifact Risk Analysis - MANDATORY): Testing is NOT the final step. After tests are completed, the agent MUST automatically perform a risk analysis on the resulting artifacts (e.g., compiled binaries, generated bundles, final configurations) before reporting to the user:
88
+ - Do the generated artifacts introduce missing schemas, memory leaks, or unintended coupling?
89
+ - Are there structural corruptions or unintended side effects in the generated output?
90
+ - This step must be executed proactively; do not wait for the user to prompt for post-test analysis.
91
+ - If any risk is identified, document it in the ticket before reporting — do not silently suppress it.
92
+ 6. Report & Close: Present the final artifact risk analysis to the user. If verified successful, clear the queue by running npx deuk-agent-rule ticket close --topic <topic>. If unexpected issues arise, revert to Phase 1 (Explore) to amend the ticket.
41
93
 
42
- - **Canonical copy** remains **`.deuk-agent-handoff/`** or **`DeukAgentRules/handoff/LATEST.md`** (portable, matches `AGENTS.md`). The `.cursor/plans/` file is an **optional duplicate** for UI only.
43
- - **Single source of truth:** If both exist, keep their **content in sync**; updating one should update the other in the same turn when possible.
44
- - **Version control:** Treat `.cursor/plans/` like other local-only handoffs unless the team commits plans on purpose — align with `.gitignore` team policy.
45
94
 
46
- ## Role: Execution
95
+ ## Agent Role Awareness
47
96
 
48
- In this workflow the agent's primary role is **execution** applying concrete changes across multiple files. For design, analysis, or planning, defer to the user's chosen tool when they indicate.
97
+ If you are operating inside an IDE (Cursor, Windsurf), you are strictly in Phase 4 (Execution). Do not attempt to over-engineer or explore completely out of bounds. Follow the Markdown ticket constraints strictly. If large-scale exploration or breaking structural changes are needed during execution, pause and instruct the user to revert to Phase 1 (Explore & Plan).
49
98
 
50
99
  ## Cost awareness (effective use of context)
51
100
 
52
- - Keep sessions **focused** — one clear objective per session when possible.
53
- - Prefer **concise** handoffs and replies: concrete paths, decisions, and diffs over narrative padding.
54
- - For read-only analysis, prefer **summaries** and scoped excerpts over dumping whole files; use lightweight review modes when the product offers them.
55
- - When producing handoffs for *other* tools, omit local-only tuning hints (see **Producing Handoff** above); still stay efficient in your own responses here.
101
+ - Keep sessions focused — one clear objective per session when possible.
102
+ - Prefer concise tickets and replies: concrete paths, decisions, and diffs over narrative padding.
103
+ - For read-only analysis, prefer summaries and scoped excerpts over dumping whole files; use lightweight review modes when the product offers them.
104
+ - When producing tickets for other tools, omit local-only tuning hints (see Producing Ticket above); still stay efficient in your own responses here.
105
+ - Tone & Style: No hype, no exaggerations, no fake satisfaction. Keep responses entirely factual and calm. Crucially, when authoring any document or ticket, NEVER use expressions that emphasize dramatic effects or imply absolute completeness/perfection. When using Korean, follow the concise polite style (-요).
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "deuk-agent-rule",
3
- "version": "1.0.13",
4
- "description": "DeukAgentRules: generic AGENTS.md + .cursor rule templates with init/merge CLI (npm name: deuk-agent-rule).",
3
+ "version": "2.2.1",
4
+ "type": "module",
5
+ "description": "DeukAgentRules",
5
6
  "keywords": [
6
7
  "agents-md",
7
8
  "cursor-rules",
@@ -10,7 +11,7 @@
10
11
  "claude",
11
12
  "windsurf",
12
13
  "jetbrains",
13
- "handoff",
14
+ "ticket",
14
15
  "deuk-family",
15
16
  "deukpack-ecosystem"
16
17
  ],
@@ -0,0 +1,43 @@
1
+ export function parseTicketArgs(argv) {
2
+ const out = { cwd: process.cwd(), dryRun: false, nonInteractive: false, limit: 20 };
3
+ for (let i = 0; i < argv.length; i++) {
4
+ const a = argv[i];
5
+ if (a === "--cwd") out.cwd = argv[++i];
6
+ else if (a === "--dry-run") out.dryRun = true;
7
+ else if (a === "--non-interactive") out.nonInteractive = true;
8
+ else if (a === "--topic") out.topic = argv[++i];
9
+ else if (a === "--group") out.group = argv[++i];
10
+ else if (a === "--project") out.project = argv[++i];
11
+ else if (a === "--content") out.content = argv[++i];
12
+ else if (a === "--from") out.from = argv[++i];
13
+ else if (a === "--ref") out.ref = argv[++i];
14
+ else if (a === "--limit") out.limit = Number(argv[++i]);
15
+ else if (a === "--latest") out.latest = true;
16
+ else if (a === "--path-only") out.pathOnly = true;
17
+ else if (a === "--print-content") out.printContent = true;
18
+ else if (a === "--all") out.all = true;
19
+ else if (a === "--status") out.status = argv[++i];
20
+ }
21
+ return out;
22
+ }
23
+
24
+ export function parseArgs(argv) {
25
+ const out = { cwd: process.cwd(), dryRun: false, backup: false };
26
+ for (let i = 0; i < argv.length; i++) {
27
+ const a = argv[i];
28
+ if (a === "--cwd") out.cwd = argv[++i];
29
+ else if (a === "--dry-run") out.dryRun = true;
30
+ else if (a === "--backup") out.backup = true;
31
+ else if (a === "--non-interactive") out.nonInteractive = true;
32
+ else if (a === "--interactive") out.interactive = true;
33
+ else if (a === "--tag") out.tag = argv[++i];
34
+ else if (a === "--marker-begin") out.markerBegin = argv[++i];
35
+ else if (a === "--marker-end") out.markerEnd = argv[++i];
36
+ else if (a === "--agents") out.agents = argv[++i];
37
+ else if (a === "--rules") out.rules = argv[++i];
38
+ else if (a === "--cursorrules") out.cursorrules = argv[++i];
39
+ else if (a === "--append-if-no-markers") out.appendIfNoMarkers = true;
40
+ else if (a === "-h" || a === "--help") out.help = true;
41
+ }
42
+ return out;
43
+ }
@@ -0,0 +1,65 @@
1
+ import { join } from "path";
2
+ import { existsSync, readFileSync, writeFileSync } from "fs";
3
+ import { resolveMarkers, resolveCursorrulesMarkers, applyAgents, applyRules, applyCursorrules, readBundleAgents } from "./merge-logic.mjs";
4
+ import { ensureTicketDirAndGitignore } from "./cli-init-logic.mjs";
5
+ import { loadInitConfig, writeInitConfig } from "./cli-prompts.mjs";
6
+
7
+ export async function runInit(opts, bundleRoot) {
8
+ const markers = resolveMarkers(opts);
9
+ const agentsResult = applyAgents({
10
+ targetPath: join(opts.cwd, "AGENTS.md"),
11
+ bundleContent: readBundleAgents(bundleRoot),
12
+ markers, flavor: "init",
13
+ appendIfNoMarkers: opts.appendIfNoMarkers,
14
+ dryRun: opts.dryRun, backup: opts.backup,
15
+ agentsMode: opts.agents || "inject"
16
+ });
17
+ console.log(`AGENTS.md: ${agentsResult.action} (${agentsResult.mode || ""})`);
18
+
19
+ const ruleActions = applyRules({
20
+ bundleRulesDir: join(bundleRoot, "rules"),
21
+ targetRulesDir: join(opts.cwd, ".cursor", "rules"),
22
+ rulesMode: opts.rules || "prefix",
23
+ dryRun: opts.dryRun, backup: opts.backup
24
+ });
25
+ ruleActions.forEach(r => console.log(`rule ${r.action}: ${r.dest || r.src}`));
26
+
27
+ const crResult = applyCursorrules({
28
+ bundleRoot, cwd: opts.cwd,
29
+ markers: resolveCursorrulesMarkers({}),
30
+ cursorrulesMode: opts.cursorrules || "inject",
31
+ dryRun: opts.dryRun, backup: opts.backup
32
+ });
33
+ console.log(`.cursorrules: ${crResult.action} (${crResult.mode || ""})`);
34
+
35
+ ensureTicketDirAndGitignore(opts);
36
+ }
37
+
38
+ export function runMerge(opts, bundleRoot) {
39
+ const markers = resolveMarkers(opts);
40
+ const agentsResult = applyAgents({
41
+ targetPath: join(opts.cwd, "AGENTS.md"),
42
+ bundleContent: readBundleAgents(bundleRoot),
43
+ markers, flavor: "merge",
44
+ appendIfNoMarkers: opts.appendIfNoMarkers,
45
+ dryRun: opts.dryRun, backup: opts.backup,
46
+ agentsMode: opts.agents || "inject"
47
+ });
48
+ console.log(`AGENTS.md: ${agentsResult.action} (${agentsResult.mode || ""})`);
49
+
50
+ const ruleActions = applyRules({
51
+ bundleRulesDir: join(bundleRoot, "rules"),
52
+ targetRulesDir: join(opts.cwd, ".cursor", "rules"),
53
+ rulesMode: opts.rules || "skip",
54
+ dryRun: opts.dryRun, backup: opts.backup
55
+ });
56
+ ruleActions.forEach(r => console.log(`rule ${r.action}: ${r.dest || r.src}`));
57
+
58
+ const crResult = applyCursorrules({
59
+ bundleRoot, cwd: opts.cwd,
60
+ markers: resolveCursorrulesMarkers({}),
61
+ cursorrulesMode: opts.cursorrules || "inject",
62
+ dryRun: opts.dryRun, backup: opts.backup
63
+ });
64
+ console.log(`.cursorrules: ${crResult.action} (${crResult.mode || ""})`);
65
+ }
@@ -0,0 +1,21 @@
1
+ import { existsSync, appendFileSync, writeFileSync, mkdirSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { TICKET_DIR_NAME } from "./cli-ticket-logic.mjs";
4
+
5
+ const GITIGNORE_TICKET_MARKER = "# deuk-agent-rule: ticket directory (local, not committed by default)";
6
+
7
+ export function ensureTicketDirAndGitignore(opts) {
8
+ const ticketPath = join(opts.cwd, TICKET_DIR_NAME);
9
+ const gitignorePath = join(opts.cwd, ".gitignore");
10
+ const ignoreLine = TICKET_DIR_NAME + "/";
11
+
12
+ if (opts.dryRun) return;
13
+
14
+ mkdirSync(ticketPath, { recursive: true });
15
+
16
+ let gi = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf8") : "";
17
+ if (!gi.includes(ignoreLine)) {
18
+ const block = "\n" + GITIGNORE_TICKET_MARKER + "\n" + ignoreLine + "\n";
19
+ appendFileSync(gitignorePath, block, "utf8");
20
+ }
21
+ }
@@ -0,0 +1,123 @@
1
+ import { createInterface } from "readline";
2
+ import { existsSync, readFileSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+
5
+ const INIT_CONFIG_FILENAME = ".deuk-agent-rule.config.json";
6
+ const INIT_CONFIG_VERSION = 1;
7
+
8
+ export async function ask(rl, question) {
9
+ return new Promise((resolve) => rl.question(question, resolve));
10
+ }
11
+
12
+ export async function askYesNo(question, defaultYes = true) {
13
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
14
+ try {
15
+ const ans = (await ask(rl, question + (defaultYes ? " [Y/n]: " : " [y/N]: "))).trim().toLowerCase();
16
+ if (!ans) return defaultYes;
17
+ return ans === "y" || ans === "yes";
18
+ } finally {
19
+ rl.close();
20
+ }
21
+ }
22
+
23
+ export async function selectOne(rl, prompt, choices) {
24
+ console.log("\n" + prompt);
25
+ choices.forEach((c, i) => console.log(` ${i + 1}) ${c.label}`));
26
+ while (true) {
27
+ const ans = (await ask(rl, ` Choice [1-${choices.length}]: `)).trim();
28
+ const idx = parseInt(ans, 10) - 1;
29
+ if (idx >= 0 && idx < choices.length) return choices[idx].value;
30
+ }
31
+ }
32
+
33
+ export async function selectMany(rl, prompt, choices) {
34
+ console.log("\n" + prompt + " (comma-separated numbers, or 'all')");
35
+ choices.forEach((c, i) => console.log(` ${i + 1}) ${c.label}`));
36
+ while (true) {
37
+ const ans = (await ask(rl, ` Choices: `)).trim().toLowerCase();
38
+ if (ans === "all" || ans === "") return choices.map((c) => c.value);
39
+ const parts = ans.split(/[,\s]+/).map((s) => parseInt(s, 10) - 1);
40
+ if (parts.every((i) => i >= 0 && i < choices.length)) return parts.map((i) => choices[i].value);
41
+ }
42
+ }
43
+
44
+ export function loadInitConfig(cwd) {
45
+ const p = join(cwd, INIT_CONFIG_FILENAME);
46
+ if (!existsSync(p)) return null;
47
+ try {
48
+ const j = JSON.parse(readFileSync(p, "utf8"));
49
+ if (j.version !== INIT_CONFIG_VERSION) return null;
50
+ return j;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ export function writeInitConfig(cwd, opts) {
57
+ const p = join(cwd, INIT_CONFIG_FILENAME);
58
+ const body = {
59
+ version: INIT_CONFIG_VERSION,
60
+ stack: opts.stack,
61
+ agentTools: opts.agentTools,
62
+ agentsMode: opts.agents ?? "inject",
63
+ updatedAt: new Date().toISOString(),
64
+ };
65
+ writeFileSync(p, JSON.stringify(body, null, 2) + "\n", "utf8");
66
+ }
67
+
68
+ export const STACKS = [
69
+ { label: "Unity / C#", value: "unity" },
70
+ { label: "Next.js + C#", value: "nextjs-dotnet" },
71
+ { label: "Web (React / Vue / general)", value: "web" },
72
+ { label: "Java / Spring Boot", value: "java" },
73
+ { label: "Other / skip", value: "other" },
74
+ ];
75
+
76
+ export const AGENT_TOOLS = [
77
+ { label: "Cursor", value: "cursor" },
78
+ { label: "GitHub Copilot", value: "copilot" },
79
+ { label: "Gemini / Antigravity", value: "gemini" },
80
+ { label: "Claude (Cursor / Claude Code)", value: "claude" },
81
+ { label: "Windsurf", value: "windsurf" },
82
+ { label: "JetBrains AI Assistant", value: "jetbrains" },
83
+ { label: "All of the above", value: "all" },
84
+ { label: "Other / skip", value: "other" },
85
+ ];
86
+
87
+ export async function runInteractive(opts) {
88
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
89
+ try {
90
+ console.log("\nDeukAgentRules init — let's configure your workspace.\n");
91
+
92
+ const stack = await selectOne(rl, "What is your primary tech stack?", STACKS);
93
+ const tools = await selectMany(rl, "Which agent tools do you use?", AGENT_TOOLS);
94
+
95
+ const targetAgents = join(opts.cwd, "AGENTS.md");
96
+ let agentsDefault = "inject";
97
+ if (!existsSync(targetAgents)) {
98
+ agentsDefault = "inject"; // will append markers
99
+ console.log("\n No AGENTS.md found — will create with markers.");
100
+ } else {
101
+ const content = readFileSync(targetAgents, "utf8");
102
+ const hasMarkers = content.includes("deuk-agent-rule:begin");
103
+ if (!hasMarkers) {
104
+ const choice = await selectOne(rl, "AGENTS.md exists but has no markers. How to apply?", [
105
+ { label: "Append managed block at the end (safe)", value: "inject" },
106
+ { label: "Overwrite entire AGENTS.md", value: "overwrite" },
107
+ { label: "Skip AGENTS.md", value: "skip" },
108
+ ]);
109
+ agentsDefault = choice;
110
+ }
111
+ }
112
+
113
+ opts.agents = opts.agents ?? agentsDefault;
114
+ opts.stack = stack;
115
+ opts.agentTools = tools;
116
+
117
+ console.log("\n Stack : " + stack);
118
+ console.log(" Tools : " + (tools.join(", ") || "none"));
119
+ console.log(" AGENTS: " + opts.agents + "\n");
120
+ } finally {
121
+ rl.close();
122
+ }
123
+ }
@@ -0,0 +1,159 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
2
+ import { basename, join } from "path";
3
+ import { toSlug, toRepoRelativePath, inferRefTitleAndTopic, resolveReferencedTicketPath } from "./cli-utils.mjs";
4
+ import { TICKET_DIR_NAME, appendTicketEntry, rebuildTicketIndexFromTopicFilesIfNeeded, detectConsumerTicketDir } from "./cli-ticket-logic.mjs";
5
+
6
+ import { createInterface } from "readline";
7
+ import { selectOne } from "./cli-prompts.mjs";
8
+
9
+ export async function runTicketCreate(opts) {
10
+ if (!opts.topic && !opts.ref) throw new Error("ticket create requires --topic or --ref");
11
+
12
+ const inferred = opts.ref ? inferRefTitleAndTopic(opts) : null;
13
+ const topic = toSlug(opts.topic || inferred?.topic || "ticket");
14
+ const title = opts.topic || inferred?.title || "ticket";
15
+ const group = toSlug(opts.group || "sub");
16
+
17
+ let path, source;
18
+ if (opts.ref) {
19
+ path = resolveReferencedTicketPath(opts);
20
+ source = "ticket-reference";
21
+ } else {
22
+ let body = opts.content ? String(opts.content).replace(/\\n/g, '\n') : "";
23
+ if (!body && opts.from) body = readFileSync(join(opts.cwd, opts.from), "utf8");
24
+ const abs = join(opts.cwd, TICKET_DIR_NAME, group, `${topic}-${Date.now()}.md`);
25
+ mkdirSync(join(opts.cwd, TICKET_DIR_NAME, group), { recursive: true });
26
+ path = toRepoRelativePath(opts.cwd, abs);
27
+ const marker = `\n\n<!-- Ticket (repo-relative): ${path} -->\n`;
28
+ writeFileSync(abs, body.trimEnd() + marker, "utf8");
29
+ source = "ticket-create";
30
+ }
31
+
32
+ appendTicketEntry(opts.cwd, {
33
+ id: `ticket_${Date.now()}`,
34
+ title, topic, group, project: opts.project || "global",
35
+ createdAt: new Date().toISOString(), path, source
36
+ }, opts);
37
+ }
38
+
39
+ export async function runTicketList(opts) {
40
+ const ticketDir = detectConsumerTicketDir(opts.cwd);
41
+ if (!ticketDir) {
42
+ throw new Error("No ticket system found. Please run 'npx deuk-agent-rule init' first.");
43
+ }
44
+ const index = rebuildTicketIndexFromTopicFilesIfNeeded(opts.cwd, opts);
45
+ let rows = index.entries;
46
+
47
+ if (!opts.all) {
48
+ const targetStatus = opts.status || "open";
49
+ rows = rows.filter(e => e.status === targetStatus);
50
+ }
51
+
52
+ if (opts.group) rows = rows.filter(e => e.group === opts.group);
53
+ if (opts.project) rows = rows.filter(e => e.project === opts.project);
54
+
55
+ console.log("# STATUS GROUP PROJECT CREATED TITLE");
56
+ rows.slice(0, opts.limit).forEach((e, idx) => {
57
+ const stat = (e.status === "closed" ? "[x]" : "[ ]").padEnd(7);
58
+ const safeTitle = String(e.title || e.topic || "").replace(/(\n|\\n)+/g, " ").slice(0, 50);
59
+ console.log(`${String(idx+1).padEnd(2)} ${stat} ${e.group.padEnd(10)} ${e.project.padEnd(11)} ${e.createdAt.padEnd(24)} ${safeTitle}`);
60
+ });
61
+ }
62
+
63
+ import { updateTicketEntryStatus } from "./cli-ticket-logic.mjs";
64
+
65
+ export async function runTicketClose(opts) {
66
+ if (!opts.topic && !opts.latest) {
67
+ if (process.stdout.isTTY) {
68
+ const index = rebuildTicketIndexFromTopicFilesIfNeeded(opts.cwd, opts);
69
+ const choices = index.entries
70
+ .filter(e => e.status !== "closed")
71
+ .map(e => ({ label: `[${e.group}] ${e.title}`, value: e.topic }));
72
+ if (choices.length > 0) {
73
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
74
+ try {
75
+ opts.topic = await selectOne(rl, "Choose a ticket to close:", choices);
76
+ } finally {
77
+ rl.close();
78
+ }
79
+ } else {
80
+ throw new Error("No open tickets found to close.");
81
+ }
82
+ } else {
83
+ throw new Error("ticket close requires --topic or --latest");
84
+ }
85
+ }
86
+ opts.status = "closed";
87
+ const entry = updateTicketEntryStatus(opts.cwd, opts);
88
+ console.log(`ticket: closed -> ${entry.topic} (${entry.path})`);
89
+ }
90
+
91
+ export async function runTicketUse(opts) {
92
+ const index = rebuildTicketIndexFromTopicFilesIfNeeded(opts.cwd, opts);
93
+
94
+ let targetTopic = opts.topic;
95
+ if (!targetTopic && !opts.latest) {
96
+ if (process.stdout.isTTY) {
97
+ const choices = index.entries
98
+ .map(e => ({ label: `${e.status === 'closed' ? '✓ ' : ''}[${e.group}] ${e.title}`, value: e.topic }));
99
+ if (choices.length > 0) {
100
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
101
+ try {
102
+ targetTopic = await selectOne(rl, "Choose a ticket to use:", choices);
103
+ } finally {
104
+ rl.close();
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ const found = opts.latest ? index.entries[0] : index.entries.find(e => e.topic.includes(targetTopic));
111
+ if (!found) throw new Error("No matching ticket found");
112
+
113
+ if (opts.pathOnly) console.log(found.path);
114
+ else {
115
+ console.log(`Path: ${found.path}`);
116
+ if (opts.printContent) console.log("\n" + readFileSync(join(opts.cwd, found.path), "utf8"));
117
+ }
118
+ }
119
+
120
+ import { getLegacyMigrationCandidate, parseLegacyTicketMeta } from "./cli-ticket-logic.mjs";
121
+ import { dirname } from "path";
122
+
123
+ export async function runTicketMigrate(opts) {
124
+ const candidate = getLegacyMigrationCandidate(opts.cwd);
125
+ if (!candidate) {
126
+ console.log("ticket: no legacy LATEST.md migration candidate found");
127
+ return;
128
+ }
129
+
130
+ const { title, group, project } = parseLegacyTicketMeta(candidate.body);
131
+ const topic = toSlug(title);
132
+ const stamp = Date.now();
133
+ const relPath = join(TICKET_DIR_NAME, group, `${topic}-${stamp}.md`);
134
+ const absPath = join(opts.cwd, relPath);
135
+
136
+ if (opts.dryRun) {
137
+ console.log("ticket: would migrate -> " + relPath);
138
+ } else {
139
+ mkdirSync(dirname(absPath), { recursive: true });
140
+ const marker = `\n\n<!-- Ticket (repo-relative): ${relPath} -->\n`;
141
+ writeFileSync(absPath, candidate.body.trimEnd() + marker, "utf8");
142
+ console.log("ticket: migrated body -> " + relPath);
143
+
144
+ appendTicketEntry(opts.cwd, {
145
+ id: `ticket_migrated_${stamp}`,
146
+ title,
147
+ topic,
148
+ group,
149
+ project,
150
+ createdAt: new Date().toISOString(),
151
+ path: relPath,
152
+ source: "ticket-migrate",
153
+ }, opts);
154
+ if (existsSync(candidate.latestPath)) {
155
+ unlinkSync(candidate.latestPath);
156
+ console.log("ticket: deleted legacy LATEST.md");
157
+ }
158
+ }
159
+ }