deuk-agent-flow 4.2.7 → 5.0.4

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 (170) hide show
  1. package/CHANGELOG.ko.md +277 -0
  2. package/CHANGELOG.md +794 -124
  3. package/LICENSE +0 -0
  4. package/README.ko.md +97 -17
  5. package/README.md +108 -25
  6. package/bin/deuk-agent-flow.js +11 -13
  7. package/bin/deuk-agent-rule.js +1 -1
  8. package/bundled/README.md +3 -0
  9. package/bundled/deuk-agent-flow.vsix +0 -0
  10. package/core-rules/AGENTS.md +30 -120
  11. package/docs/architecture.ko.md +155 -2
  12. package/docs/architecture.md +155 -2
  13. package/docs/assets/agentflow-panel-skills.png +0 -0
  14. package/docs/assets/agentflow-panel.png +0 -0
  15. package/docs/how-it-works.ko.md +109 -52
  16. package/docs/how-it-works.md +128 -71
  17. package/docs/principles.ko.md +68 -68
  18. package/docs/principles.md +68 -68
  19. package/docs/usage-guide.ko.md +251 -212
  20. package/package.json +60 -45
  21. package/scripts/bundle-vscode-vsix.ts +67 -0
  22. package/scripts/{cli-args.mjs → cli-args.ts} +49 -12
  23. package/scripts/cli-context-commands.ts +35 -0
  24. package/scripts/cli-init-claude.ts +179 -0
  25. package/scripts/cli-init-commands.ts +99 -0
  26. package/scripts/cli-init-core.ts +89 -0
  27. package/scripts/cli-init-instructions.ts +489 -0
  28. package/scripts/cli-init-logic.ts +46 -0
  29. package/scripts/cli-init-migrate.ts +504 -0
  30. package/scripts/cli-init-workspace.ts +63 -0
  31. package/scripts/cli-locales.ts +85 -0
  32. package/scripts/{cli-prompts.mjs → cli-prompts.ts} +19 -34
  33. package/scripts/{cli-rule-compiler.mjs → cli-rule-compiler.ts} +128 -112
  34. package/scripts/cli-skill-commands.ts +709 -0
  35. package/scripts/{cli-telemetry-commands.mjs → cli-telemetry-commands.ts} +25 -22
  36. package/scripts/cli-ticket-command-shared.ts +4 -0
  37. package/scripts/cli-ticket-commands.ts +3781 -0
  38. package/scripts/cli-ticket-docmeta.ts +103 -0
  39. package/scripts/cli-ticket-document.ts +153 -0
  40. package/scripts/cli-ticket-evidence.ts +312 -0
  41. package/scripts/cli-ticket-home.ts +449 -0
  42. package/scripts/cli-ticket-index.ts +283 -0
  43. package/scripts/{cli-ticket-migration.mjs → cli-ticket-migration.ts} +44 -68
  44. package/scripts/cli-ticket-parser.ts +100 -0
  45. package/scripts/cli-ticket-scan.ts +102 -0
  46. package/scripts/cli-ticket-surface.ts +56 -0
  47. package/scripts/{cli-usage-commands.mjs → cli-usage-commands.ts} +325 -326
  48. package/scripts/cli-utils.ts +1560 -0
  49. package/scripts/cli.ts +695 -0
  50. package/scripts/{lint-md.mjs → lint-md.ts} +32 -42
  51. package/scripts/lint-rules.ts +203 -0
  52. package/scripts/{merge-logic.mjs → merge-logic.ts} +44 -44
  53. package/scripts/oss-prepack.js +32 -0
  54. package/scripts/out/scripts/bundle-vscode-vsix.js +68 -0
  55. package/scripts/out/scripts/bundle-vscode-vsix.js.map +1 -0
  56. package/scripts/out/scripts/cli-args.js +374 -0
  57. package/scripts/out/scripts/cli-args.js.map +1 -0
  58. package/scripts/out/scripts/cli-context-commands.js +31 -0
  59. package/scripts/out/scripts/cli-context-commands.js.map +1 -0
  60. package/scripts/out/scripts/cli-init-claude.js +176 -0
  61. package/scripts/out/scripts/cli-init-claude.js.map +1 -0
  62. package/scripts/out/scripts/cli-init-commands.js +87 -0
  63. package/scripts/out/scripts/cli-init-commands.js.map +1 -0
  64. package/scripts/out/scripts/cli-init-core.js +89 -0
  65. package/scripts/out/scripts/cli-init-core.js.map +1 -0
  66. package/scripts/out/scripts/cli-init-instructions.js +460 -0
  67. package/scripts/out/scripts/cli-init-instructions.js.map +1 -0
  68. package/scripts/out/scripts/cli-init-logic.js +41 -0
  69. package/scripts/out/scripts/cli-init-logic.js.map +1 -0
  70. package/scripts/out/scripts/cli-init-migrate.js +497 -0
  71. package/scripts/out/scripts/cli-init-migrate.js.map +1 -0
  72. package/scripts/out/scripts/cli-init-workspace.js +57 -0
  73. package/scripts/out/scripts/cli-init-workspace.js.map +1 -0
  74. package/scripts/out/scripts/cli-locales.js +83 -0
  75. package/scripts/out/scripts/cli-locales.js.map +1 -0
  76. package/scripts/out/scripts/cli-prompts.js +101 -0
  77. package/scripts/out/scripts/cli-prompts.js.map +1 -0
  78. package/scripts/out/scripts/cli-rule-compiler.js +121 -0
  79. package/scripts/out/scripts/cli-rule-compiler.js.map +1 -0
  80. package/scripts/out/scripts/cli-skill-commands.js +708 -0
  81. package/scripts/out/scripts/cli-skill-commands.js.map +1 -0
  82. package/scripts/out/scripts/cli-telemetry-commands.js +604 -0
  83. package/scripts/out/scripts/cli-telemetry-commands.js.map +1 -0
  84. package/scripts/out/scripts/cli-ticket-command-shared.js +2 -0
  85. package/scripts/out/scripts/cli-ticket-command-shared.js.map +1 -0
  86. package/scripts/out/scripts/cli-ticket-commands.js +3530 -0
  87. package/scripts/out/scripts/cli-ticket-commands.js.map +1 -0
  88. package/scripts/out/scripts/cli-ticket-docmeta.js +90 -0
  89. package/scripts/out/scripts/cli-ticket-docmeta.js.map +1 -0
  90. package/scripts/out/scripts/cli-ticket-document.js +121 -0
  91. package/scripts/out/scripts/cli-ticket-document.js.map +1 -0
  92. package/scripts/out/scripts/cli-ticket-evidence.js +286 -0
  93. package/scripts/out/scripts/cli-ticket-evidence.js.map +1 -0
  94. package/scripts/out/scripts/cli-ticket-home.js +440 -0
  95. package/scripts/out/scripts/cli-ticket-home.js.map +1 -0
  96. package/scripts/out/scripts/cli-ticket-index.js +272 -0
  97. package/scripts/out/scripts/cli-ticket-index.js.map +1 -0
  98. package/scripts/out/scripts/cli-ticket-migration.js +261 -0
  99. package/scripts/out/scripts/cli-ticket-migration.js.map +1 -0
  100. package/scripts/out/scripts/cli-ticket-parser.js +90 -0
  101. package/scripts/out/scripts/cli-ticket-parser.js.map +1 -0
  102. package/scripts/out/scripts/cli-ticket-scan.js +93 -0
  103. package/scripts/out/scripts/cli-ticket-scan.js.map +1 -0
  104. package/scripts/out/scripts/cli-ticket-surface.js +53 -0
  105. package/scripts/out/scripts/cli-ticket-surface.js.map +1 -0
  106. package/scripts/out/scripts/cli-usage-commands.js +310 -0
  107. package/scripts/out/scripts/cli-usage-commands.js.map +1 -0
  108. package/scripts/out/scripts/cli-utils.js +1544 -0
  109. package/scripts/out/scripts/cli-utils.js.map +1 -0
  110. package/scripts/out/scripts/cli.js +696 -0
  111. package/scripts/out/scripts/cli.js.map +1 -0
  112. package/scripts/out/scripts/lint-md.js +238 -0
  113. package/scripts/out/scripts/lint-md.js.map +1 -0
  114. package/scripts/out/scripts/lint-rules.js +193 -0
  115. package/scripts/out/scripts/lint-rules.js.map +1 -0
  116. package/scripts/out/scripts/merge-logic.js +38 -0
  117. package/scripts/out/scripts/merge-logic.js.map +1 -0
  118. package/scripts/out/scripts/plan-parser.js +52 -0
  119. package/scripts/out/scripts/plan-parser.js.map +1 -0
  120. package/scripts/out/scripts/ticket-machine.js +94 -0
  121. package/scripts/out/scripts/ticket-machine.js.map +1 -0
  122. package/scripts/out/scripts/ticket-workflow.js +498 -0
  123. package/scripts/out/scripts/ticket-workflow.js.map +1 -0
  124. package/scripts/{plan-parser.mjs → plan-parser.ts} +53 -53
  125. package/scripts/ticket-machine.ts +99 -0
  126. package/scripts/ticket-workflow.ts +544 -0
  127. package/templates/MODULE_RULE_TEMPLATE.md +11 -11
  128. package/templates/PROJECT_RULE.md +46 -47
  129. package/templates/TICKET_TEMPLATE.ko.md +48 -44
  130. package/templates/TICKET_TEMPLATE.md +48 -44
  131. package/templates/project-memory.md +19 -0
  132. package/templates/project-pilot/CONFORMANCE_GATE_TEMPLATE.md +25 -25
  133. package/templates/project-pilot/DRIFT_CHECKLIST.md +27 -27
  134. package/templates/project-pilot/FLOW_CONTRACT_TEMPLATE.md +26 -26
  135. package/templates/project-pilot/IMPLEMENTATION_MATRIX_TEMPLATE.md +30 -30
  136. package/templates/project-pilot/INTEGRATION_CONTRACT_TEMPLATE.md +26 -26
  137. package/templates/project-pilot/OWNER_MAP_TEMPLATE.md +15 -15
  138. package/templates/project-pilot/PROJECT_PILOT_RULE_TEMPLATE.md +34 -34
  139. package/templates/project-pilot/REFACTOR_CONTRACT_TEMPLATE.md +32 -32
  140. package/templates/project-pilot/REMEDIATION_PLAN_TEMPLATE.md +33 -33
  141. package/templates/rules.d/deukcontext-mcp.md +31 -31
  142. package/templates/rules.d/platform-coexistence.md +29 -29
  143. package/templates/skills/context-recall/SKILL.md +3 -1
  144. package/templates/skills/doc-sync/SKILL.md +111 -0
  145. package/templates/skills/generated-file-guard/SKILL.md +3 -1
  146. package/templates/skills/persona-maid/SKILL.md +65 -0
  147. package/templates/skills/project-pilot/SKILL.md +14 -63
  148. package/templates/skills/role-closer/SKILL.md +27 -0
  149. package/templates/skills/role-implementer/SKILL.md +30 -0
  150. package/templates/skills/role-planner/SKILL.md +29 -0
  151. package/templates/skills/role-qa-verifier/SKILL.md +30 -0
  152. package/templates/skills/safe-refactor/SKILL.md +3 -1
  153. package/templates/skills/ticket-status-surface/SKILL.md +17 -0
  154. package/NOTICE.md +0 -19
  155. package/core-rules/GEMINI.md +0 -7
  156. package/docs/npm-publish-guide.ko.md +0 -70
  157. package/scripts/cli-init-commands.mjs +0 -1759
  158. package/scripts/cli-init-logic.mjs +0 -64
  159. package/scripts/cli-skill-commands.mjs +0 -212
  160. package/scripts/cli-ticket-command-shared.mjs +0 -60
  161. package/scripts/cli-ticket-commands.mjs +0 -2474
  162. package/scripts/cli-ticket-index.mjs +0 -322
  163. package/scripts/cli-ticket-parser.mjs +0 -210
  164. package/scripts/cli-utils.mjs +0 -602
  165. package/scripts/cli.mjs +0 -256
  166. package/scripts/lint-rules.mjs +0 -197
  167. package/scripts/publish-dual-npm.mjs +0 -141
  168. package/scripts/smoke-npm-docker.mjs +0 -102
  169. package/scripts/smoke-npm-local.mjs +0 -110
  170. package/scripts/update-download-badge.mjs +0 -103
@@ -0,0 +1,99 @@
1
+ import { removeClaudeUserPromptSubmitHook, upsertClaudeUserPromptSubmitHook } from "./cli-init-claude.js";
2
+ import { CANONICAL_INIT_CLEANUP_PATHS, CLAUDE_SETTINGS_PATH, CLAUDE_USERPROMPT_SUBMIT_COMMAND, CLAUDE_USERPROMPT_SUBMIT_MATCHER, GLOBAL_POINTER_BEGIN_PREFIX, GLOBAL_POINTER_END, MANAGED_BLOCK_BEGIN, MANAGED_BLOCK_END, hashManagedContent, mergeManagedBlock, safeReadText, splitManagedBlock, wrapManagedBlock } from "./cli-init-core.js";
3
+ // #638: updateSiblingWorkspaceRegistry (B / ~/.agent-flow) retired — no longer called.
4
+ import { dirname, basename, relative, resolve } from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync, unlinkSync, rmSync, renameSync, chmodSync, statSync, symlinkSync } from "fs";
7
+ import { createHash } from "crypto";
8
+
9
+ import { ensureTicketDirAndGitignore } from "./cli-init-logic.js";
10
+ import { normalizeTicketPaths } from "./cli-ticket-migration.js";
11
+ import { rebuildTicketIndexFromTopicFilesIfNeeded } from "./cli-ticket-parser.js";
12
+ import { readTicketIndexJson, syncActiveTicketId, writeTicketIndexJson } from "./cli-ticket-index.js";
13
+ import { ensureWorkspaceMigrated } from "./cli-ticket-home.js";
14
+ import { syncExposedNativeSkills } from "./cli-skill-commands.js";
15
+
16
+ // #623/#622-D: init-time migration of a workspace's tickets to ~/.deuk-agent/tickets/
17
+ // and sync of native skills from the SSOT. Idempotent and best-effort — a failure
18
+ // here must not abort workspace setup.
19
+ function ensureTicketsMigratedOnInit(cwd, dryRun) {
20
+ try {
21
+ // #645: init is the ONLY entry point allowed to mint a brand-new workspace
22
+ // (marker + workspace.json). allowCreate gates that — without it, reconcile
23
+ // self-heals an already-registered workspace but never registers a new one.
24
+ ensureWorkspaceMigrated(cwd, { dryRun, allowCreate: true });
25
+ } catch (err) {
26
+ console.warn(`[INIT] ticket home migration skipped for ${basename(cwd)}: ${err.message}`);
27
+ }
28
+ try {
29
+ syncExposedNativeSkills(cwd, { dryRun });
30
+ } catch (err) {
31
+ console.warn(`[INIT] skill sync skipped for ${basename(cwd)}: ${err.message}`);
32
+ }
33
+ }
34
+ import { cliText, resolveCliLocale } from "./cli-locales.js";
35
+
36
+ import { DEUK_ROOT_DIR, CliOpts, SPOKE_REGISTRY, TICKET_INDEX_FILENAME, TICKET_LIST_FILENAME, TICKET_SUBDIR, buildWorkspaceAliases, discoverAllWorkspaces, getWorkspaceInitConfigPath, isWorkflowExecute, loadInitConfig, makePath, normalizeRegistryPath, normalizeTicketGroup, normalizeWorkflowMode, parseFrontMatter, pruneRuleModules, resolveDocsLanguage, resolvePackageRoot, resolveUserHome, resolveWorkflowMode, stringifyFrontMatter, toPosixPath, toRepoRelativePath, writeFileLF } from "./cli-utils.js";
37
+
38
+ import { removeFragmentedLocalAgentSurfaces, removeLegacyWorkspaceConfig, removeLocalSkillCopies, removeNestedLegacyWorkspaceConfigs, removeRuntimeTemplateCopies } from "./cli-init-migrate.js";
39
+ import { buildGlobalClaudeInstructions, buildGlobalCodexInstructions, canonicalizeGeneratedCommandReferences, generateSpokeContent, mergeManagedRuleContent, removeDuplicateRuleCopies, runGlobalAgentInstructionSync, runSingleWorkspaceMaintenance, syncCodexMcpEndpointFromWorkspace } from "./cli-init-instructions.js";
40
+
41
+ export function runWorkspaceMaintenance(cwd = process.cwd(), dryRun = false, bundleRoot = resolvePackageRoot({ fromUrl: import.meta.url }), opts: CliOpts = {}) {
42
+ dryRun = Boolean(dryRun);
43
+ if (!cwd || !existsSync(cwd)) return { removed: 0 };
44
+ if (opts.silent) {
45
+ const originalLog = console.log;
46
+ console.log = () => {};
47
+ try {
48
+ return runWorkspaceMaintenance(cwd, dryRun, bundleRoot, { ...opts, silent: false });
49
+ } finally {
50
+ console.log = originalLog;
51
+ }
52
+ }
53
+ const ignoreDirs = [];
54
+ const recursive = Boolean(opts.recursive);
55
+
56
+ const homeDir = resolveUserHome(opts);
57
+ const saved = loadInitConfig(cwd, { ...opts, homeDir }) || {};
58
+ const maintenanceOptions = { ...saved, ...opts };
59
+ runGlobalAgentInstructionSync(dryRun, bundleRoot, homeDir);
60
+ const codexMcpResult = syncCodexMcpEndpointFromWorkspace(cwd, { dryRun, homeDir });
61
+ const claudeSettingsResult = upsertClaudeUserPromptSubmitHook({ dryRun, homeDir });
62
+ // #638: the ~/.agent-flow sibling registry (B) is retired. Workspace candidates now
63
+ // come solely from ~/.deuk-agent/tickets/{uuid}/workspace.json (written by reconcile),
64
+ // so we no longer write B here — it only went stale and spawned ambiguous ghosts.
65
+ removeNestedLegacyWorkspaceConfigs(cwd, dryRun, ignoreDirs);
66
+ removeRuntimeTemplateCopies(cwd, dryRun);
67
+ removeLocalSkillCopies(cwd, dryRun);
68
+ removeDuplicateRuleCopies(cwd, dryRun);
69
+ canonicalizeGeneratedCommandReferences(cwd, bundleRoot, dryRun);
70
+ const removed = removeFragmentedLocalAgentSurfaces(cwd, dryRun);
71
+
72
+ const submodules = recursive ? discoverAllWorkspaces(cwd, ignoreDirs) : [cwd];
73
+ if (recursive && !submodules.includes(cwd)) submodules.push(cwd);
74
+
75
+ for (const subCwd of submodules) {
76
+ try {
77
+ runSingleWorkspaceMaintenance(subCwd, dryRun, bundleRoot, maintenanceOptions);
78
+ // #623: init is one of the two migration entry points (the other is ticket
79
+ // command entry). Move this workspace's tickets to home as part of setup.
80
+ ensureTicketsMigratedOnInit(subCwd, dryRun);
81
+ } catch (err) {
82
+ console.error(`[ERROR] Failed to initialize workspace ${basename(subCwd)}: ${(err as Error).message}`);
83
+ }
84
+ }
85
+
86
+ return {
87
+ removed,
88
+ codexMcpChanged: Boolean(codexMcpResult?.changed),
89
+ codexMcpUrl: codexMcpResult?.url || null,
90
+ claudeSettingsChanged: Boolean(claudeSettingsResult?.changed),
91
+ claudeSettingsHash: claudeSettingsResult?.hash || null
92
+ };
93
+ }
94
+
95
+ export function formatInitCompletionMessage(cwd, dryRun = false) {
96
+ const label = dryRun ? "Dry-run complete" : "Init complete";
97
+ return `[DONE] ${label} for ${basename(cwd)}. Rules and pointers are ready. \`이슈분석 티켓\`이라고 해보세요.`;
98
+ }
99
+
@@ -0,0 +1,89 @@
1
+ import { readFileSync } from "fs";
2
+ import { createHash } from "crypto";
3
+ import { LEGACY_DEUK, DEUK_ROOT_DIR, CliOpts, makePath } from "./cli-utils.js";
4
+
5
+ export function safeReadText(absPath, fallback = "") {
6
+ try {
7
+ return readFileSync(absPath, "utf8");
8
+ } catch {
9
+ return fallback;
10
+ }
11
+ }
12
+
13
+ export const MANAGED_BLOCK_BEGIN = "<!-- deuk-agent-managed:begin -->";
14
+ export const MANAGED_BLOCK_END = "<!-- deuk-agent-managed:end -->";
15
+ export const GLOBAL_POINTER_BEGIN_PREFIX = "--- DEUK_AGENT_FLOW_BEGIN :";
16
+ export const GLOBAL_POINTER_END = "--- DEUK_AGENT_FLOW_END";
17
+ export const CLAUDE_SETTINGS_PATH = makePath(".claude", "settings.json");
18
+ export const CLAUDE_USERPROMPT_SUBMIT_MATCHER = "";
19
+ // #871-followup: drop `ticket use --latest` — it stole focus to the cwd index's
20
+ // newest ticket every turn, ignoring the per-session workspace cookie (this is the
21
+ // "지멋대로 아무 워크스페이스를 잡는" bug). `rules` already derives the active
22
+ // ticket from the session cookie when given --session-id, so just forward the
23
+ // session id and let rules resolve the last-used workspace. Claude exposes the
24
+ // turn's session id as $CLAUDE_SESSION_ID to UserPromptSubmit hooks.
25
+ export const CLAUDE_USERPROMPT_SUBMIT_COMMAND = "deuk-agent-flow rules --session-id \"$CLAUDE_SESSION_ID\" 2>/dev/null || deuk-agent-flow rules";
26
+ export const CANONICAL_INIT_CLEANUP_PATHS = {
27
+ runtimeTemplateCopies: [makePath(DEUK_ROOT_DIR, "templates"), LEGACY_DEUK.templateDir],
28
+ localSkillCopies: [
29
+ makePath(DEUK_ROOT_DIR, "skill-templates"),
30
+ makePath(".claude", "skills"),
31
+ makePath(".cursor", "rules", "deuk-agent-skills.mdc")
32
+ ],
33
+ duplicateRuleCopies: [makePath(DEUK_ROOT_DIR, "rules"), makePath(".cursor", "rules", "deuk-agent-rule-multi-ai-workflow.mdc")],
34
+ // Per-workspace agent surfaces are removed to keep repos clean — the rule lives
35
+ // once in the agent-flow-designated home, not duplicated into every workspace.
36
+ // deuk-EXCLUSIVE files (filename identifies deuk) — safe to delete the whole file.
37
+ localAgentSurfaces: [
38
+ makePath(".cursor", "rules", "deuk-agent.mdc"),
39
+ makePath(".claude", "rules", "deuk-agent.md"),
40
+ makePath(".windsurf", "rules", "deuk-agent.md"),
41
+ makePath(".aiassistant", "rules", "deuk-agent.md"),
42
+ ".cursorrules",
43
+ ".windsurfrules"
44
+ ],
45
+ // SHARED-name files that may also carry the user's own content — strip only the
46
+ // deuk-managed block (preserve user settings); delete only if nothing else remains.
47
+ sharedAgentSurfaces: [
48
+ makePath(".codex", "AGENTS.md"),
49
+ makePath(".github", "copilot-instructions.md"),
50
+ "CLAUDE.md",
51
+ "GEMINI.md"
52
+ ],
53
+ };
54
+
55
+ // ─── Managed Block 헬퍼 ──────────────────────────────────────────────────────
56
+ export function wrapManagedBlock(content) {
57
+ return `${MANAGED_BLOCK_BEGIN}\n${String(content || "").trimEnd()}\n${MANAGED_BLOCK_END}`;
58
+ }
59
+
60
+ export function hashManagedContent(content) {
61
+ return createHash("sha256").update(String(content || ""), "utf8").digest("hex");
62
+ }
63
+
64
+ export function splitManagedBlock(content) {
65
+ const current = String(content || "");
66
+ if (!current.includes(MANAGED_BLOCK_BEGIN) || !current.includes(MANAGED_BLOCK_END)) return null;
67
+ const beginIdx = current.indexOf(MANAGED_BLOCK_BEGIN);
68
+ const endIdx = current.indexOf(MANAGED_BLOCK_END, beginIdx);
69
+ if (beginIdx === -1 || endIdx === -1) return null;
70
+ return {
71
+ before: current.slice(0, beginIdx).trimEnd(),
72
+ managed: current.slice(beginIdx + MANAGED_BLOCK_BEGIN.length, endIdx).trim(),
73
+ after: current.slice(endIdx + MANAGED_BLOCK_END.length).trimStart(),
74
+ };
75
+ }
76
+
77
+ export function mergeManagedBlock(existing, managedContent) {
78
+ const current = String(existing || "");
79
+ const nextBlock = wrapManagedBlock(managedContent);
80
+ if (!current.trim()) return `${nextBlock}\n`;
81
+ const currentBlock = splitManagedBlock(current);
82
+ if (currentBlock) {
83
+ return [currentBlock.before, nextBlock, currentBlock.after].filter(Boolean).join("\n\n").trimEnd() + "\n";
84
+ }
85
+ const cleaned = current.trimEnd();
86
+ const managedBody = String(managedContent || "").trim();
87
+ if (managedBody && cleaned.includes(managedBody)) return cleaned + "\n";
88
+ return `${cleaned}\n\n${nextBlock}\n`;
89
+ }
@@ -0,0 +1,489 @@
1
+ import { dirname, basename } from "path";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, unlinkSync, renameSync } from "fs";
3
+ import { fileURLToPath } from "url";
4
+ import { DEUK_ROOT_DIR, CliOpts, SPOKE_REGISTRY, makePath, resolvePackageRoot, resolveUserHome, toFileUri, toPosixPath, toRepoRelativePath } from "./cli-utils.js";
5
+ import { CANONICAL_INIT_CLEANUP_PATHS, GLOBAL_POINTER_BEGIN_PREFIX, GLOBAL_POINTER_END, MANAGED_BLOCK_BEGIN, MANAGED_BLOCK_END, hashManagedContent, mergeManagedBlock, safeReadText, splitManagedBlock, wrapManagedBlock } from "./cli-init-core.js";
6
+ import { upsertClaudeUserPromptSubmitHook } from "./cli-init-claude.js";
7
+ import { ensureTicketDirAndGitignore } from "./cli-init-logic.js";
8
+ import { resolveHomeTicketDirForWorkspace } from "./cli-ticket-home.js";
9
+ import { normalizeTicketPaths } from "./cli-ticket-migration.js";
10
+ import { rebuildTicketIndexFromTopicFilesIfNeeded } from "./cli-ticket-parser.js";
11
+ import { syncActiveTicketId, writeTicketIndexJson } from "./cli-ticket-index.js";
12
+ import { cleanupGeneratedAgentPointerSurface, cleanupNestedGeneratedAgentPointers, ensureWritableDirectory, migrateDeukRootDir, migrateHtmlMarkersToHeadings, migrateLegacyStructure, removeLegacyPaths, removeLocalAgentSurfaces, removeLocalSkillCopies, removeRuntimeTemplateCopies, sameFileContent } from "./cli-init-migrate.js";
13
+ import { ensureSystemSkills } from "./cli-skill-commands.js";
14
+
15
+ function readWorkspaceDeukContextMcpUrl(cwd) {
16
+ const mcpPath = makePath(cwd, ".mcp.json");
17
+ if (!existsSync(mcpPath)) return "";
18
+
19
+ try {
20
+ const config = JSON.parse(readFileSync(mcpPath, "utf8"));
21
+ const servers = config.mcpServers || config.servers || {};
22
+ const deukContextServer = servers["deuk-agent-context"] || servers["deuk_agent_context"];
23
+ return String(deukContextServer?.url || "").trim();
24
+ } catch (err) {
25
+ if (process.env.DEBUG) console.warn(`[DEBUG] Failed to parse ${mcpPath}: ${err.message}`);
26
+ return "";
27
+ }
28
+ }
29
+
30
+ function upsertCodexMcpTomlUrl(existingContent, nextUrl) {
31
+ const sectionHeader = "[mcp_servers.deuk_agent_context]";
32
+ const normalizedUrlLine = `url = ${JSON.stringify(nextUrl)}`;
33
+ const content = String(existingContent || "").trimEnd();
34
+ const sectionPattern = /(^|\n)\[mcp_servers\.deuk_agent_context\]\n([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/;
35
+ const sectionMatch = content.match(sectionPattern);
36
+
37
+ if (!sectionMatch) {
38
+ const prefix = content ? `${content}\n\n` : "";
39
+ return `${prefix}${sectionHeader}\n${normalizedUrlLine}\n`;
40
+ }
41
+
42
+ const sectionBody = sectionMatch[2];
43
+ const nextSectionBody = /^url\s*=.*$/m.test(sectionBody)
44
+ ? sectionBody.replace(/^url\s*=.*$/m, normalizedUrlLine)
45
+ : `${sectionBody.trimEnd()}${sectionBody.trimEnd() ? "\n" : ""}${normalizedUrlLine}`;
46
+
47
+ return `${content.slice(0, sectionMatch.index)}${sectionMatch[1]}${sectionHeader}\n${nextSectionBody}${content.slice(sectionMatch.index + sectionMatch[0].length)}\n`;
48
+ }
49
+
50
+ export function syncCodexMcpEndpointFromWorkspace(cwd, opts: CliOpts = {}) {
51
+ const url = readWorkspaceDeukContextMcpUrl(cwd);
52
+ if (!url) return { changed: false, url: "" };
53
+
54
+ const homeDir = resolveUserHome(opts);
55
+ const dryRun = Boolean(opts.dryRun);
56
+ const codexConfigPath = makePath(homeDir, ".codex", "config.toml");
57
+ const existingContent = existsSync(codexConfigPath) ? readFileSync(codexConfigPath, "utf8") : "";
58
+ const nextContent = upsertCodexMcpTomlUrl(existingContent, url);
59
+ const changed = existingContent !== nextContent;
60
+ if (!changed) return { changed: false, url, path: codexConfigPath };
61
+
62
+ if (!dryRun) {
63
+ mkdirSync(dirname(codexConfigPath), { recursive: true });
64
+ writeFileSync(codexConfigPath, nextContent, "utf8");
65
+ }
66
+ console.log(`[MIGRATE] ${dryRun ? "Would sync" : "Synced"} Codex MCP endpoint: deuk_agent_context -> ${url}`);
67
+ return { changed: true, url, path: codexConfigPath };
68
+ }
69
+
70
+ function canonicalizeLegacyDeukAgentText(content, bundleRoot) {
71
+ const coreRulesPath = makePath(bundleRoot, "core-rules", "AGENTS.md");
72
+ const coreRulesUri = toFileUri(coreRulesPath);
73
+ return String(content || "")
74
+ .replace(/DeukAgentRules/g, "DeukAgentFlow")
75
+ .replace(/deuk-agent-rule/g, "deuk-agent-flow")
76
+ .replace(/Deuk Agent Rules/g, "Deuk Agent Flow")
77
+ .replace(/\bdeuk-agent-flow rules path --path-only\b/g, "deuk-agent-flow rules")
78
+ .replace(/file:\/\/[^)\s>\]"'`]+\/DeukAgentRules\/core-rules\/AGENTS\.md/g, coreRulesUri)
79
+ .replace(/\/home\/joy\/workspace\/DeukAgentRules\/core-rules\/AGENTS\.md/g, coreRulesPath)
80
+ .replace(/file:\/\/[^)\s>\]"'`]+\/deuk-agent-flow\/core-rules\/AGENTS\.md/g, coreRulesUri)
81
+ .replace(/(?:[a-zA-Z]:[\\/]|(?:\/|\\\\)[^\n\\/]+)[^\n)\s>\]"'`]+\/deuk-agent-flow\/core-rules\/AGENTS\.md/g, coreRulesPath);
82
+ }
83
+
84
+ function normalizeExistingSpokeContent(existingContent, managedContent, bundleRoot) {
85
+ const current = String(existingContent || "");
86
+ if (!current.trim()) return current;
87
+ if (isGeneratedDeukPointer(current)) return "";
88
+
89
+ const normalizedManaged = String(managedContent || "").trim();
90
+ const canonicalize = (value) => canonicalizeLegacyDeukAgentText(value, bundleRoot).trim();
91
+
92
+ if (canonicalize(current) === normalizedManaged) return "";
93
+
94
+ const currentBlock = splitManagedBlock(current);
95
+ if (!currentBlock) return current;
96
+
97
+ const before = canonicalize(currentBlock.before) === normalizedManaged ? "" : currentBlock.before;
98
+ const after = canonicalize(currentBlock.after) === normalizedManaged ? "" : currentBlock.after;
99
+ return [before, wrapManagedBlock(managedContent), after].filter(Boolean).join("\n\n").trimEnd() + "\n";
100
+ }
101
+
102
+ export function canonicalizeTextFile(absPath, cwd, bundleRoot, dryRun, label) {
103
+ if (!existsSync(absPath)) return false;
104
+ const before = readFileSync(absPath, "utf8");
105
+ const after = canonicalizeLegacyDeukAgentText(before, bundleRoot);
106
+ if (before === after) return false;
107
+ if (!dryRun) writeFileSync(absPath, after, "utf8");
108
+ console.log(`[MIGRATE] ${dryRun ? "Would canonicalize" : "Canonicalized"} ${label}: ${toRepoRelativePath(cwd, absPath)}`);
109
+ return true;
110
+ }
111
+
112
+ export function splitProjectDoc(content) {
113
+ const marker = "--- project-doc ---";
114
+ const idx = String(content || "").indexOf(marker);
115
+ if (idx === -1) return { pointer: content, projectDoc: "" };
116
+ return {
117
+ pointer: content.slice(0, idx),
118
+ projectDoc: content.slice(idx).trimStart()
119
+ };
120
+ }
121
+
122
+ export function isGeneratedDeukPointer(content) {
123
+ const src = String(content || "");
124
+ const hasHeader = /Managed by DeukAgent(?:Rules|Flow)/.test(src) || /# Deuk Agent Rules\b|# Deuk Agent Flow\b|# DeukAgentFlow\b/.test(src);
125
+ const hasCoreRulesRef = /Core rules (?:are at|path is resolved by|are printed by):/i.test(src) || /deuk-agent-flow rules/i.test(src);
126
+ if (!hasHeader || !hasCoreRulesRef) return false;
127
+ // Old format required "thin bootstrap" line; new format omits it — both are valid generated pointers
128
+ return true;
129
+ }
130
+
131
+ export function canonicalizeGeneratedCommandReferences(cwd, bundleRoot, dryRun) {
132
+ const targets = [
133
+ makePath(cwd, "AGENTS.md"),
134
+ makePath(cwd, "PROJECT_RULE.md"),
135
+ makePath(cwd, DEUK_ROOT_DIR, "PROJECT_RULE.md"),
136
+ makePath(cwd, "docs", "project", "AGENTS.md"),
137
+ makePath(cwd, ".github", "copilot-instructions.md"),
138
+ makePath(cwd, ".codex", "AGENTS.md"),
139
+ ];
140
+
141
+ for (const target of targets) {
142
+ canonicalizeTextFile(target, cwd, bundleRoot, dryRun, "legacy command reference");
143
+ }
144
+ }
145
+
146
+
147
+ export function buildGlobalCodexInstructions() {
148
+ return buildGlobalAgentPointer();
149
+ }
150
+
151
+ export function buildGlobalClaudeInstructions() {
152
+ return buildGlobalAgentPointer();
153
+ }
154
+
155
+ function extractMarkdownHeadingSection(content = "", heading = "") {
156
+ const normalizeHeading = value => String(value || "")
157
+ .trim()
158
+ .replace(/^#+\s*/, "")
159
+ .replace(/^\d+[\).\s-]+/, "")
160
+ .toLowerCase();
161
+ const target = normalizeHeading(heading);
162
+ if (!target) return "";
163
+ const lines = String(content || "").split(/\r?\n/);
164
+ let start = -1;
165
+ let level = 0;
166
+ for (const [index, line] of lines.entries()) {
167
+ const match = line.match(/^(#{1,6})\s+(.+?)\s*$/);
168
+ if (!match) continue;
169
+ if (start === -1) {
170
+ if (normalizeHeading(match[2]) === target) {
171
+ start = index;
172
+ level = match[1].length;
173
+ }
174
+ continue;
175
+ }
176
+ if (match[1].length <= level) {
177
+ return lines.slice(start, index).join("\n").trim();
178
+ }
179
+ }
180
+ return start === -1 ? "" : lines.slice(start).join("\n").trim();
181
+ }
182
+
183
+ function readCoreRulesSection(bundleRoot, heading) {
184
+ const rulesPath = makePath([bundleRoot, "core-rules", "AGENTS.md"]);
185
+ if (!existsSync(rulesPath)) return "";
186
+ return extractMarkdownHeadingSection(readFileSync(rulesPath, "utf8"), heading);
187
+ }
188
+
189
+ function readCliSurfaceDocument(bundleRoot, name) {
190
+ const surfacePath = makePath(bundleRoot, "docs", "cli-surfaces", `${name}.md`);
191
+ if (!existsSync(surfacePath)) {
192
+ throw new Error(`CLI surface document not found: ${surfacePath}`);
193
+ }
194
+ return readFileSync(surfacePath, "utf8");
195
+ }
196
+
197
+ function renderCliSurfaceDocument(bundleRoot, name, replacements = {}) {
198
+ let content = readCliSurfaceDocument(bundleRoot, name);
199
+ content = content.replace(/\{\{CORE:([^}]+)\}\}/g, (_match, heading) => readCoreRulesSection(bundleRoot, String(heading || "").trim()).trimEnd());
200
+ for (const [key, value] of Object.entries(replacements)) {
201
+ content = content.replaceAll(`{{${key}}}`, String(value ?? ""));
202
+ }
203
+ return content.trimEnd();
204
+ }
205
+
206
+ function buildGlobalAgentPointer(bundleRoot = resolvePackageRoot({ fromUrl: import.meta.url }), format = "markdown", agentId = "default") {
207
+ let body = "";
208
+ try {
209
+ body = renderCliSurfaceDocument(bundleRoot, `global-pointer-${agentId}`);
210
+ } catch(e) {
211
+ body = renderCliSurfaceDocument(bundleRoot, "global-pointer");
212
+ }
213
+
214
+ if (format === "mdc") {
215
+ return `---
216
+ description: "DeukAgentFlow global pointer"
217
+ globs: ["**/*"]
218
+ alwaysApply: true
219
+ ---
220
+ ${body}`;
221
+ }
222
+
223
+ return body;
224
+ }
225
+
226
+ function splitMdcFrontMatter(content) {
227
+ const text = String(content || "");
228
+ if (!text.startsWith("---\n")) return { frontMatter: "", body: text };
229
+ const endIdx = text.indexOf("\n---\n", 4);
230
+ if (endIdx === -1) return { frontMatter: "", body: text };
231
+ return {
232
+ frontMatter: stripPathKeysFromFrontMatter(text.slice(0, endIdx + 5)).trimEnd(),
233
+ body: text.slice(endIdx + 5).trimStart()
234
+ };
235
+ }
236
+
237
+ function stripPathKeysFromFrontMatter(frontMatter) {
238
+ return String(frontMatter || "")
239
+ .split(/\r?\n/)
240
+ .filter(line => !/^\s*(path|absPath|absolutePath|relativePath|sourcePath|targetPath|workspacePath|rulesPath|ticketPath)\s*:/i.test(line))
241
+ .join("\n");
242
+ }
243
+
244
+ function wrapGlobalPointerBlock(content) {
245
+ const normalizedContent = String(content || "").trimEnd();
246
+ const { frontMatter, body } = splitMdcFrontMatter(normalizedContent);
247
+ const pointerBlock = [
248
+ `${GLOBAL_POINTER_BEGIN_PREFIX} ${hashManagedContent(normalizedContent)}`,
249
+ body.trimEnd(),
250
+ GLOBAL_POINTER_END
251
+ ].join("\n");
252
+ return frontMatter ? `${frontMatter}\n${pointerBlock}` : pointerBlock;
253
+ }
254
+
255
+ function stripGlobalPointerBlocks(content) {
256
+ let text = String(content || "");
257
+ // Strip <!-- deuk-agent-managed --> wrapped blocks (legacy)
258
+ let currentBlock = splitManagedBlock(text);
259
+ while (currentBlock) {
260
+ text = [currentBlock.before, currentBlock.after].filter(Boolean).join("\n\n").trim();
261
+ currentBlock = splitManagedBlock(text);
262
+ }
263
+ // Strip --- DEUK_AGENT_FLOW_BEGIN/END blocks
264
+ text = text.replace(/--- DEUK_AGENT_FLOW_BEGIN :[^\n]*\n[\s\S]*?--- DEUK_AGENT_FLOW_END\n?/g, "");
265
+ text = text.replace(/DEUK_AGENT_GLOBAL_POINTER_BEGIN[\s\S]*?DEUK_AGENT_GLOBAL_POINTER_END\n?/g, "");
266
+ text = text.replace(/^---\ndescription: "DeukAgentFlow global pointer"\nglobs: \["\*\*\/\*"\]\nalwaysApply: true\n---\n# DeukAgentFlow Global Pointer\n\nManaged by DeukAgentFlow\.\n\nRead this file first:\n[^\n]+\n\nKeep personal notes in the sibling `\.user` file, not in this managed pointer\.\n?/gm, "");
267
+ text = text.replace(/^# DeukAgentFlow Global Pointer\n\nManaged by DeukAgentFlow\.\n\nRead this file first:\n[^\n]+\n\nKeep personal notes in the sibling `\.user` file, not in this managed pointer\.\n?/gm, "");
268
+ text = text.replace(/^---\ndescription: "DeukAgentFlow global pointer"\nglobs: \["\*\*\/\*"\]\nalwaysApply: true\n---\n?/gm, "");
269
+ // Strip plain (tag-free) pointer blocks left by previous init versions
270
+ text = text.replace(/^# DeukAgentFlow Global Pointer\n[\s\S]*$/m, "");
271
+ return text.trim();
272
+ }
273
+
274
+ function mergeGlobalAgentPointer(existingContent, managedContent) {
275
+ const nextBlock = wrapGlobalPointerBlock(managedContent);
276
+ const current = String(existingContent || "");
277
+ const remainder = stripGlobalPointerBlocks(current);
278
+ return remainder ? `${nextBlock}\n\n${remainder}\n` : `${nextBlock}\n`;
279
+ }
280
+
281
+ function getGlobalAgentInstructionTargets(homeDir = resolveUserHome(), bundleRoot = resolvePackageRoot({ fromUrl: import.meta.url })) {
282
+ return [
283
+ {
284
+ id: "codex",
285
+ target: makePath(homeDir, ".codex", "AGENTS.md"),
286
+ content: buildGlobalAgentPointer(bundleRoot, "markdown", "codex")
287
+ },
288
+ {
289
+ id: "claude",
290
+ target: makePath(homeDir, ".claude", "CLAUDE.md"),
291
+ content: buildGlobalAgentPointer(bundleRoot, "markdown", "claude")
292
+ },
293
+ {
294
+ id: "gemini",
295
+ target: makePath(homeDir, ".gemini", "GEMINI.md"),
296
+ content: buildGlobalAgentPointer(bundleRoot, "markdown", "gemini")
297
+ },
298
+ {
299
+ id: "cursor",
300
+ target: makePath(homeDir, ".cursor", "rules", "deuk-agent.mdc"),
301
+ content: buildGlobalAgentPointer(bundleRoot, "mdc", "cursor")
302
+ },
303
+ {
304
+ id: "copilot",
305
+ target: makePath(homeDir, ".config", "Code", "User", "copilot-instructions.md"),
306
+ content: buildGlobalAgentPointer(bundleRoot, "markdown", "copilot")
307
+ },
308
+ {
309
+ id: "antigravity",
310
+ target: makePath(homeDir, ".config", "Antigravity", "User", "AGENTS.md"),
311
+ content: buildGlobalAgentPointer(bundleRoot, "markdown", "antigravity")
312
+ },
313
+ ];
314
+ }
315
+
316
+ function syncGlobalAgentInstructions(dryRun, bundleRoot = resolvePackageRoot({ fromUrl: import.meta.url }), homeDir = resolveUserHome()) {
317
+ for (const entry of getGlobalAgentInstructionTargets(homeDir, bundleRoot)) {
318
+ if (dryRun) continue;
319
+ const nextContent = entry.content;
320
+ const existingContent = existsSync(entry.target) ? safeReadText(entry.target) : "";
321
+ const mergedContent = mergeGlobalAgentPointer(existingContent, nextContent);
322
+ if (existingContent === mergedContent) continue;
323
+
324
+ mkdirSync(dirname(entry.target), { recursive: true });
325
+ writeFileSync(entry.target, mergedContent, "utf8");
326
+ }
327
+ // Claude settings.json 훅도 rules 실행 시마다 최신 커맨드로 자동 동기화.
328
+ upsertClaudeUserPromptSubmitHook({ dryRun, homeDir });
329
+ }
330
+
331
+ export function runGlobalAgentInstructionSync(dryRun = false, bundleRoot = resolvePackageRoot({ fromUrl: import.meta.url }), homeDir = resolveUserHome()) {
332
+ syncGlobalAgentInstructions(dryRun, bundleRoot, homeDir);
333
+ }
334
+
335
+ export function generateSpokeContent(spoke, bundleRoot) {
336
+ let content = "";
337
+ try {
338
+ content = renderCliSurfaceDocument(bundleRoot, `spoke-pointer-${spoke.id}`);
339
+ } catch (err) {
340
+ content = renderCliSurfaceDocument(bundleRoot, "spoke-pointer");
341
+ }
342
+
343
+ if (spoke.format === "mdc") {
344
+ return `---
345
+ description: "Deuk Agent Flow - Project conventions and ticket workflow"
346
+ globs: ["**/*"]
347
+ alwaysApply: true
348
+ ---
349
+ ${content}`;
350
+ }
351
+ return `---\n\n## DeukAgentFlow\n\n> Managed by DeukAgentFlow. Remove this section if not installed.\n\n${content}\n`;
352
+ }
353
+
354
+ export function mergeManagedRuleContent(existingContent, managedContent) {
355
+ return mergeManagedBlock(existingContent, managedContent);
356
+ }
357
+
358
+ function deploySpokePointers(cwd, bundleRoot, dryRun) {
359
+ for (const spoke of SPOKE_REGISTRY) {
360
+ const legacyPath = spoke.legacy ? makePath(cwd, spoke.legacy) : null;
361
+ const legacyExisted = Boolean(legacyPath && existsSync(legacyPath));
362
+
363
+ // Legacy root files are replaced by canonical spoke targets.
364
+ if (legacyPath) {
365
+ if (legacyExisted) {
366
+ if (!dryRun) unlinkSync(legacyPath);
367
+ console.log(`[CLEANUP] removed legacy: ${spoke.legacy}`);
368
+ }
369
+ }
370
+
371
+ const shouldInstallDefaultHub = spoke.id === "antigravity" && existsSync(makePath(cwd, DEUK_ROOT_DIR));
372
+ if (!spoke.detect(cwd) && !legacyExisted && !shouldInstallDefaultHub) continue;
373
+
374
+ const targetPath = makePath(cwd, spoke.target);
375
+ const targetDir = dirname(targetPath);
376
+ const managedContent = generateSpokeContent(spoke, bundleRoot);
377
+ const existingContent = existsSync(targetPath) ? safeReadText(targetPath) : "";
378
+ const normalizedContent = normalizeExistingSpokeContent(existingContent, managedContent, bundleRoot);
379
+ const nextContent = mergeManagedBlock(normalizedContent, managedContent);
380
+ if (existingContent === nextContent) {
381
+ console.log(`spoke synced: ${spoke.target} (${spoke.id})`);
382
+ continue;
383
+ }
384
+
385
+ if (!dryRun) {
386
+ ensureWritableDirectory(targetDir, cwd, dryRun, `spoke target conflict resolved for ${spoke.id}`);
387
+ mkdirSync(targetDir, { recursive: true });
388
+ writeFileSync(targetPath, nextContent, "utf8");
389
+ }
390
+ console.log(`spoke synced: ${spoke.target} (${spoke.id})`);
391
+ }
392
+ }
393
+
394
+ export function removeDuplicateRuleCopies(cwd, dryRun) {
395
+ // Note: AGENTS.md is now the Antigravity spoke target — do NOT delete it here.
396
+ // CLAUDE.md/GEMINI.md legacy cleanup is handled by deploySpokePointers (spoke.legacy field).
397
+ // .gemini is the Antigravity platform directory — preserve it.
398
+ removeLegacyPaths(cwd, dryRun, CANONICAL_INIT_CLEANUP_PATHS.duplicateRuleCopies, "legacy/duplicate");
399
+ }
400
+
401
+ function migrateProjectRuleToAgentRoot(cwd, dryRun, opts: CliOpts = {}) {
402
+ const sourcePath = makePath(cwd, "PROJECT_RULE.md");
403
+ if (!existsSync(sourcePath)) return false;
404
+
405
+ // Workspace-level PROJECT_RULE.md is legacy — remove it (content lives in home ticket store).
406
+ const homeDir = resolveHomeTicketDirForWorkspace(cwd, opts);
407
+ const targetPath = makePath(homeDir, "PROJECT_RULE.md");
408
+
409
+ if (existsSync(targetPath)) {
410
+ if (sameFileContent(sourcePath, targetPath)) {
411
+ if (!dryRun) unlinkSync(sourcePath);
412
+ console.log(`[MIGRATE] removed duplicate root PROJECT_RULE.md: ${toRepoRelativePath(cwd, sourcePath)}`);
413
+ return true;
414
+ }
415
+ console.warn(`[WARNING] PROJECT_RULE.md conflict: home store version exists with different content, kept ${toRepoRelativePath(cwd, sourcePath)}`);
416
+ return false;
417
+ }
418
+
419
+ if (!dryRun) {
420
+ mkdirSync(dirname(targetPath), { recursive: true });
421
+ renameSync(sourcePath, targetPath);
422
+ }
423
+ console.log(`[MIGRATE] ${dryRun ? "Would move" : "Moved"} project rules: ${toRepoRelativePath(cwd, sourcePath)} -> home ticket store`);
424
+ return true;
425
+ }
426
+
427
+ function ensureProjectRule(cwd, bundleRoot, dryRun, opts: CliOpts = {}) {
428
+ const homeDir = resolveHomeTicketDirForWorkspace(cwd, opts);
429
+ const projectRulePath = makePath(homeDir, "PROJECT_RULE.md");
430
+ if (existsSync(projectRulePath)) return false;
431
+
432
+ const templatePath = makePath(bundleRoot, "templates", "PROJECT_RULE.md");
433
+ if (!existsSync(templatePath)) return false;
434
+
435
+ if (!dryRun) {
436
+ mkdirSync(dirname(projectRulePath), { recursive: true });
437
+ copyFileSync(templatePath, projectRulePath);
438
+ }
439
+ console.log(`PROJECT_RULE.md: created from template in home ticket store`);
440
+ return true;
441
+ }
442
+
443
+ function ensureProjectMemory(cwd, bundleRoot, dryRun, opts: CliOpts = {}) {
444
+ const homeDir = resolveHomeTicketDirForWorkspace(cwd, opts);
445
+ const memoryPath = makePath(homeDir, "project-memory.md");
446
+ if (existsSync(memoryPath)) return false;
447
+
448
+ const templatePath = makePath(bundleRoot, "templates", "project-memory.md");
449
+ if (!existsSync(templatePath)) return false;
450
+
451
+ if (!dryRun) {
452
+ mkdirSync(dirname(memoryPath), { recursive: true });
453
+ copyFileSync(templatePath, memoryPath);
454
+ }
455
+ console.log(`project-memory.md: created from template in home ticket store`);
456
+ return true;
457
+ }
458
+
459
+ export function runSingleWorkspaceMaintenance(cwd, dryRun, bundleRoot, opts: CliOpts = {}) {
460
+ console.log(`\nInitializing ${basename(cwd)}...`);
461
+
462
+ // #713: self-healing home root migration (~/.deuk-agent → ~/.deuk). Idempotent, runs every init.
463
+ migrateDeukRootDir({ ...opts, dryRun });
464
+ migrateLegacyStructure(cwd, dryRun);
465
+ migrateHtmlMarkersToHeadings(cwd, dryRun);
466
+ ensureTicketDirAndGitignore({ ...opts, cwd, dryRun });
467
+ normalizeTicketPaths(cwd, { silent: false });
468
+ if (!dryRun) {
469
+ const rebuiltIndex = rebuildTicketIndexFromTopicFilesIfNeeded(cwd, { force: true });
470
+ writeTicketIndexJson(cwd, rebuiltIndex, { force: true });
471
+ syncActiveTicketId(cwd);
472
+ }
473
+
474
+ removeDuplicateRuleCopies(cwd, dryRun);
475
+ removeLocalAgentSurfaces(cwd, dryRun);
476
+ cleanupGeneratedAgentPointerSurface(makePath(cwd, "AGENTS.md"), cwd, dryRun);
477
+ migrateProjectRuleToAgentRoot(cwd, dryRun, opts);
478
+ ensureProjectRule(cwd, bundleRoot, dryRun, opts);
479
+ // #685: project-memory.md 자동 생성 제거 — 무의미한 진행-상태 레이어(언니 지시).
480
+
481
+ removeRuntimeTemplateCopies(cwd, dryRun);
482
+ removeLocalSkillCopies(cwd, dryRun);
483
+ // #697: 시스템 필수 스킬(ticket-status-surface 등 system:true)을 항상 보장.
484
+ try { ensureSystemSkills(cwd, { dryRun }); } catch { /* best-effort */ }
485
+ removeLocalAgentSurfaces(cwd, dryRun);
486
+ canonicalizeGeneratedCommandReferences(cwd, bundleRoot, dryRun);
487
+ cleanupNestedGeneratedAgentPointers(cwd, dryRun);
488
+ }
489
+