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,46 @@
1
+ import { existsSync, appendFileSync, writeFileSync, mkdirSync, readFileSync } from "fs";
2
+ import { LEGACY_DEUK, DEUK_ROOT_DIR, CliOpts, isInsideGitWorkTree, makePath } from "./cli-utils.js";
3
+
4
+ const GITIGNORE_AGENT_MARKER = "# deuk-agent-flow: agent hub directory (local, not committed by default)";
5
+ const LEGACY_GITIGNORE_AGENT_MARKER = "# deuk-agent-rule: agent hub directory (local, not committed by default)";
6
+
7
+ function hasExactGitignoreLine(content, line) {
8
+ return content
9
+ .split(/\r?\n/)
10
+ .map(s => s.trim())
11
+ .includes(line);
12
+ }
13
+
14
+ function removeExactGitignoreLines(content, lines) {
15
+ const remove = new Set(lines);
16
+ return content
17
+ .split(/\r?\n/)
18
+ .filter(line => !remove.has(line.trim()))
19
+ .join("\n");
20
+ }
21
+
22
+ export function ensureTicketDirAndGitignore(opts) {
23
+ const gitignorePath = makePath(opts.cwd, ".gitignore");
24
+
25
+ if (opts.dryRun) return;
26
+
27
+ if (!isInsideGitWorkTree(opts.cwd)) return;
28
+
29
+ let gi = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf8") : "";
30
+
31
+ // Remove all legacy/old deuk ignore lines. The new .deuk-workspace-id file is committed
32
+ // (not ignored), so no new ignore entry is added. Legacy dir names come from the single
33
+ // LEGACY_DEUK.dirs SSOT — for each we strip both "<dir>/" and "<dir>/tickets/". DEUK_ROOT_DIR
34
+ // ("<.deuk>/") is also stripped in case an older flow ever added it.
35
+ const legacyIgnoreLines = LEGACY_DEUK.dirs.flatMap(dir => [`${dir}/`, `${dir}/tickets/`]);
36
+ const cleanedGi = removeExactGitignoreLines(gi, [
37
+ ...legacyIgnoreLines,
38
+ GITIGNORE_AGENT_MARKER,
39
+ LEGACY_GITIGNORE_AGENT_MARKER,
40
+ `${DEUK_ROOT_DIR}/`, // new .deuk/ root line (if ever added)
41
+ ]);
42
+ if (cleanedGi !== gi) {
43
+ writeFileSync(gitignorePath, cleanedGi.replace(/\n*$/, "\n"), "utf8");
44
+ console.log("[INIT] Removed legacy .deuk-agent/ .gitignore entries");
45
+ }
46
+ }
@@ -0,0 +1,504 @@
1
+ import { basename } from "path";
2
+ import { existsSync, readFileSync, writeFileSync, readdirSync, unlinkSync, rmSync, renameSync, statSync, copyFileSync, cpSync } from "fs";
3
+ import type { Dirent } from "fs";
4
+ import { randomUUID } from "crypto";
5
+ import { LEGACY_DEUK, DEUK_ROOT_DIR, CliOpts, WORKSPACE_MARKER_FILE, getWorkspaceInitConfigPath, makePath, resolveDocsLanguage, resolveUserHome, toRepoRelativePath } from "./cli-utils.js";
6
+ import { CANONICAL_INIT_CLEANUP_PATHS, safeReadText, splitManagedBlock } from "./cli-init-core.js";
7
+ import { isGeneratedDeukPointer, splitProjectDoc } from "./cli-init-instructions.js";
8
+ import { cliText, resolveCliLocale } from "./cli-locales.js";
9
+
10
+ function resolveInitLocale(docsLanguage = "auto") {
11
+ return resolveCliLocale(resolveDocsLanguage(docsLanguage));
12
+ }
13
+
14
+ // #713: home root directory migration ~/.deuk-agent → ~/.deuk. The literal legacy name is
15
+ // fixed here (DEUK_ROOT_DIR now resolves to ".deuk", so it can't name the OLD dir). Runs on
16
+ // every init entry and is self-healing/idempotent:
17
+ // (A) legacy exists && new absent → try rename (fast); on failure (another agent holds
18
+ // .deuk-agent, EBUSY/EPERM) fall back to recursive copy, preserving the original so
19
+ // the older agent keeps working.
20
+ // (B) BOTH exist (copy left a leftover) → keep retrying rmSync(.deuk-agent) each init;
21
+ // once the other agent releases it, it gets removed. Safety guard: never delete the
22
+ // legacy dir while the new ~/.deuk is empty (would risk data loss).
23
+ function dirHasEntries(dirAbs: string): boolean {
24
+ try {
25
+ return readdirSync(dirAbs).length > 0;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ export function migrateDeukRootDir(opts: CliOpts = {}) {
32
+ const home = resolveUserHome(opts);
33
+ const dryRun = Boolean(opts.dryRun);
34
+ const legacyAbs = makePath(home, LEGACY_DEUK.rootDir);
35
+ const nextAbs = makePath(home, DEUK_ROOT_DIR); // ~/.deuk
36
+
37
+ const legacyExists = existsSync(legacyAbs);
38
+ const nextExists = existsSync(nextAbs);
39
+
40
+ // (A) move legacy → new when only legacy is present.
41
+ if (legacyExists && !nextExists) {
42
+ if (dryRun) {
43
+ console.log(`[MIGRATE] Would migrate ${legacyAbs} -> ${nextAbs} (rename, copy fallback)`);
44
+ return { migrated: false, mode: "dry-run" };
45
+ }
46
+ try {
47
+ renameSync(legacyAbs, nextAbs);
48
+ console.log(`[MIGRATE] Moved ${LEGACY_DEUK.rootDir} -> ${DEUK_ROOT_DIR}`);
49
+ return { migrated: true, mode: "rename" };
50
+ } catch {
51
+ // Another agent likely holds the legacy dir — copy instead, keep the original intact.
52
+ cpSync(legacyAbs, nextAbs, { recursive: true });
53
+ console.log(`[MIGRATE] Copied ${LEGACY_DEUK.rootDir} -> ${DEUK_ROOT_DIR} (legacy kept for compatibility)`);
54
+ return { migrated: true, mode: "copy" };
55
+ }
56
+ }
57
+
58
+ // (B) both present (copy leftover) → persistently try to remove the legacy dir, but only
59
+ // once the new dir actually holds data, so a half-finished copy never deletes the source.
60
+ if (legacyExists && nextExists) {
61
+ if (!dirHasEntries(nextAbs)) {
62
+ return { migrated: false, mode: "guard-new-empty" };
63
+ }
64
+ if (dryRun) {
65
+ console.log(`[MIGRATE] Would remove leftover ${legacyAbs} (new ${DEUK_ROOT_DIR} populated)`);
66
+ return { migrated: false, mode: "dry-run" };
67
+ }
68
+ try {
69
+ rmSync(legacyAbs, { recursive: true, force: true });
70
+ console.log(`[MIGRATE] Removed leftover ${LEGACY_DEUK.rootDir} (migration complete)`);
71
+ return { migrated: true, mode: "cleanup" };
72
+ } catch {
73
+ // Still held by another agent — leave it; the next init retries.
74
+ return { migrated: false, mode: "cleanup-deferred" };
75
+ }
76
+ }
77
+
78
+ return { migrated: false, mode: "noop" };
79
+ }
80
+
81
+ export function removeLegacyPaths(cwd, dryRun, paths, label) {
82
+ let removed = 0;
83
+ for (const relPath of paths) {
84
+ const target = makePath(cwd, relPath);
85
+ if (!existsSync(target)) continue;
86
+ if (!dryRun) rmSync(target, { recursive: true, force: true });
87
+ console.log(`[CLEANUP] removed ${label}: ${toRepoRelativePath(cwd, target)}`);
88
+ removed += 1;
89
+ }
90
+ return removed;
91
+ }
92
+
93
+ export function ensureWritableDirectory(dirAbs, cwd, dryRun, label) {
94
+ if (!existsSync(dirAbs)) return;
95
+
96
+ try {
97
+ if (statSync(dirAbs).isDirectory()) return;
98
+ } catch (err) {
99
+ if (process.env.DEBUG) console.warn(`[DEBUG] Failed to inspect ${dirAbs}:`, err);
100
+ return;
101
+ }
102
+
103
+ const backupBase = `${dirAbs}.bak`;
104
+ let backupAbs = backupBase;
105
+ let index = 1;
106
+ while (existsSync(backupAbs)) {
107
+ backupAbs = `${backupBase}.${index}`;
108
+ index += 1;
109
+ }
110
+
111
+ const relDir = toRepoRelativePath(cwd, dirAbs);
112
+ const relBackup = toRepoRelativePath(cwd, backupAbs);
113
+ if (!dryRun) {
114
+ renameSync(dirAbs, backupAbs);
115
+ }
116
+ console.log(`[MIGRATE] ${label}: ${relDir} -> ${relBackup}`);
117
+ }
118
+
119
+ function removeEmptyDirsBottomUp(dir, cwd, dryRun) {
120
+ if (!existsSync(dir)) return;
121
+ for (const entry of sortedDirEntries(dir, { withFileTypes: true })) {
122
+ if (entry.isDirectory()) removeEmptyDirsBottomUp(makePath(dir, entry.name), cwd, dryRun);
123
+ }
124
+
125
+ try {
126
+ if (sortedDirEntries(dir).length > 0) return;
127
+ if (!dryRun) rmSync(dir, { recursive: true, force: true });
128
+ console.log(`[CLEANUP] removed empty directory: ${toRepoRelativePath(cwd, dir)}`);
129
+ } catch (err) {
130
+ if (process.env.DEBUG) console.warn(`[DEBUG] Failed to prune ${dir}:`, err);
131
+ }
132
+ }
133
+
134
+ export function migrateLegacyStructure(cwd, dryRun) {
135
+ const legacyTemplates = makePath(cwd, LEGACY_DEUK.templateDir);
136
+ if (existsSync(legacyTemplates)) {
137
+ console.log(`[CLEANUP] removing legacy runtime templates: ${LEGACY_DEUK.templateDir}`);
138
+ if (!dryRun && existsSync(legacyTemplates)) rmSync(legacyTemplates, { recursive: true, force: true });
139
+ }
140
+
141
+ const legacyConfig = makePath(cwd, LEGACY_DEUK.configFile);
142
+ if (existsSync(legacyConfig)) {
143
+ console.log(`[MIGRATE] Removing legacy config: ${LEGACY_DEUK.configFile}`);
144
+ if (!dryRun) unlinkSync(legacyConfig);
145
+ }
146
+
147
+ removeLegacyWorkspaceConfig(cwd, dryRun);
148
+
149
+ // Purge workspace .deuk-agent/ — only .deuk-workspace-id marker should remain.
150
+ migrateWorkspaceMarker(cwd, dryRun);
151
+ }
152
+
153
+ // Migrates the workspace marker from the old .deuk-agent/workspace-id scheme to
154
+ // the new .deuk-workspace-id single-file scheme. 4 cases:
155
+ // A: .deuk-workspace-id already exists → skip (already migrated)
156
+ // B: .deuk-agent/workspace-id exists → copy UUID, delete .deuk-agent dir
157
+ // C: .deuk-agent/ exists but no workspace-id → mint new UUID, write file, delete .deuk-agent dir
158
+ // D: neither exists → skip (not a workspace)
159
+ export function migrateWorkspaceMarker(cwd, dryRun) {
160
+ const newMarker = makePath(cwd, WORKSPACE_MARKER_FILE);
161
+ const legacyDir = makePath(cwd, LEGACY_DEUK.rootDir);
162
+ const legacyIdFile = makePath(legacyDir, "workspace-id");
163
+
164
+ // Case A: marker already exists — still purge any leftover .deuk-agent/ dir
165
+ if (existsSync(newMarker)) {
166
+ if (existsSync(legacyDir)) {
167
+ if (!dryRun) rmSync(legacyDir, { recursive: true, force: true });
168
+ console.log(`[MIGRATE] ${dryRun ? "Would remove" : "Removed"} leftover ${LEGACY_DEUK.rootDir}/ directory`);
169
+ }
170
+ return;
171
+ }
172
+
173
+ // Case D: no .deuk-agent dir at all → not a managed workspace
174
+ if (!existsSync(legacyDir)) return;
175
+
176
+ let uuid;
177
+ if (existsSync(legacyIdFile)) {
178
+ // Case B: read existing UUID
179
+ uuid = String(readFileSync(legacyIdFile, "utf8") || "").trim();
180
+ if (!uuid) uuid = randomUUID();
181
+ } else {
182
+ // Case C: .deuk-agent exists but no workspace-id → mint new UUID
183
+ uuid = randomUUID();
184
+ }
185
+
186
+ if (!dryRun) {
187
+ writeFileSync(newMarker, uuid + "\n", "utf8");
188
+ rmSync(legacyDir, { recursive: true, force: true });
189
+ }
190
+ console.log(`[MIGRATE] ${dryRun ? "Would migrate" : "Migrated"} workspace marker → ${WORKSPACE_MARKER_FILE} (uuid: ${uuid})`);
191
+ }
192
+
193
+ export function removeLegacyWorkspaceConfig(cwd, dryRun) {
194
+ const workspaceConfig = getWorkspaceInitConfigPath(cwd);
195
+ if (!existsSync(workspaceConfig)) return false;
196
+
197
+ if (!dryRun) unlinkSync(workspaceConfig);
198
+ console.log(`[CLEANUP] ${dryRun ? "Would remove" : "Removed"} legacy workspace config: ${toRepoRelativePath(cwd, workspaceConfig)}`);
199
+ return true;
200
+ }
201
+
202
+ export function removeNestedLegacyWorkspaceConfigs(cwd, dryRun, ignoreDirs = []) {
203
+ const ignored = new Set([...(ignoreDirs || []), ".git", "node_modules", ".cache"]);
204
+ const roots = [];
205
+
206
+ function visit(dir) {
207
+ let entries = [];
208
+ try {
209
+ entries = sortedDirEntries(dir, { withFileTypes: true });
210
+ } catch {
211
+ return;
212
+ }
213
+
214
+ if (existsSync(getWorkspaceInitConfigPath(dir))) roots.push(dir);
215
+
216
+ for (const entry of entries) {
217
+ if (!entry.isDirectory()) continue;
218
+ if (ignored.has(entry.name)) continue;
219
+ if (entry.name === DEUK_ROOT_DIR || LEGACY_DEUK.dirs.includes(entry.name)) continue;
220
+ visit(makePath(dir, entry.name));
221
+ }
222
+ }
223
+
224
+ visit(cwd);
225
+
226
+ let migrated = 0;
227
+ for (const root of roots) {
228
+ if (removeLegacyWorkspaceConfig(root, dryRun)) migrated += 1;
229
+ }
230
+ return migrated;
231
+ }
232
+
233
+
234
+
235
+ function sortedDirEntries(dir: string, options: { withFileTypes: true }): Dirent[];
236
+ function sortedDirEntries(dir: string, options?: Record<string, any>): string[];
237
+ function sortedDirEntries(dir: string, options: Record<string, any> = {}): (string | Dirent)[] {
238
+ const entries = readdirSync(dir, options as any) as (string | Dirent)[];
239
+ return entries.sort((a, b) => {
240
+ const aName = typeof a === "string" ? a : (a as Dirent).name;
241
+ const bName = typeof b === "string" ? b : (b as Dirent).name;
242
+ return String(aName).localeCompare(String(bName));
243
+ });
244
+ }
245
+
246
+ export function sameFileContent(leftPath, rightPath) {
247
+ return safeReadText(leftPath) === safeReadText(rightPath);
248
+ }
249
+
250
+ export function walkMdFiles(dir, callback) {
251
+ if (!existsSync(dir)) return;
252
+ for (const entry of sortedDirEntries(dir, { withFileTypes: true })) {
253
+ const p = makePath(dir, entry.name);
254
+ if (entry.isDirectory()) {
255
+ walkMdFiles(p, callback);
256
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
257
+ callback(p);
258
+ }
259
+ }
260
+ }
261
+
262
+ function escapeRegExp(value) {
263
+ return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
264
+ }
265
+
266
+ export function migrateHtmlMarkersToHeadings(cwd, dryRun) {
267
+ const locale = resolveInitLocale();
268
+ const agentsPath = makePath(cwd, "AGENTS.md");
269
+ if (!existsSync(agentsPath)) return;
270
+
271
+ const content = readFileSync(agentsPath, "utf8");
272
+ const oldBegin = "<!-- deuk-agent-rule:begin -->";
273
+ const oldEnd = "<!-- deuk-agent-rule:end -->";
274
+
275
+ if (!content.includes(oldBegin)) return;
276
+
277
+ const beginIdx = content.indexOf(oldBegin);
278
+ const endIdx = content.lastIndexOf(oldEnd);
279
+ if (endIdx <= beginIdx) return;
280
+
281
+ const managedContent = content.slice(beginIdx + oldBegin.length, endIdx).trim();
282
+ const userContent = content.slice(0, beginIdx).trim();
283
+ const afterContent = content.slice(endIdx + oldEnd.length).trim();
284
+
285
+ let newContent = "";
286
+ if (userContent) newContent += userContent + "\n\n";
287
+ if (afterContent) newContent += afterContent + "\n\n";
288
+ newContent += "---\n\n";
289
+ newContent += "## DeukAgentFlow\n\n";
290
+ newContent += "> Managed by DeukAgentFlow. Remove this section if not installed.\n\n";
291
+ newContent += managedContent + "\n";
292
+
293
+ if (!dryRun) {
294
+ copyFileSync(agentsPath, agentsPath + ".pre-v2.bak");
295
+ writeFileSync(agentsPath, newContent, "utf8");
296
+ console.log(cliText("init.migrate.convertedMarkers", { locale }));
297
+ console.log(cliText("init.migrate.backupSaved", { locale, vars: { path: "AGENTS.md.pre-v2.bak" } }));
298
+ } else {
299
+ console.log(cliText("init.migrate.dryRunConvertedMarkers", { locale }));
300
+ }
301
+ }
302
+
303
+ const INIT_SURFACE_IGNORE_DIRS = new Set([
304
+ ".git",
305
+ ".hg",
306
+ ".svn",
307
+ DEUK_ROOT_DIR,
308
+ ...LEGACY_DEUK.dirs,
309
+ "node_modules",
310
+ "dist",
311
+ "build",
312
+ ".next",
313
+ ".nuxt",
314
+ "coverage"
315
+ ]);
316
+
317
+ function walkInitTextSurfaces(dir, callback) {
318
+ if (!existsSync(dir)) return;
319
+ for (const entry of sortedDirEntries(dir, { withFileTypes: true })) {
320
+ const absPath = makePath(dir, entry.name);
321
+ if (entry.isDirectory()) {
322
+ if (INIT_SURFACE_IGNORE_DIRS.has(entry.name)) continue;
323
+ walkInitTextSurfaces(absPath, callback);
324
+ continue;
325
+ }
326
+ if (!entry.isFile()) continue;
327
+ if (entry.name === "AGENTS.md" || entry.name === "PROJECT_RULE.md") callback(absPath);
328
+ }
329
+ }
330
+
331
+ function removeLegacyHtmlManagedBlock(content) {
332
+ const oldBegin = "<!-- deuk-agent-rule:begin -->";
333
+ const oldEnd = "<!-- deuk-agent-rule:end -->";
334
+ const src = String(content || "");
335
+ const beginIdx = src.indexOf(oldBegin);
336
+ const endIdx = src.lastIndexOf(oldEnd);
337
+ if (beginIdx === -1 || endIdx <= beginIdx) return null;
338
+ return [
339
+ src.slice(0, beginIdx).trim(),
340
+ src.slice(endIdx + oldEnd.length).trim()
341
+ ].filter(Boolean).join("\n\n");
342
+ }
343
+
344
+ export function cleanupGeneratedAgentPointerSurface(absPath, cwd, dryRun) {
345
+ const locale = resolveInitLocale();
346
+ if (!existsSync(absPath)) return false;
347
+ if (basename(absPath) !== "AGENTS.md") return false;
348
+
349
+ const before = readFileSync(absPath, "utf8");
350
+ if (!/DeukAgentRules|DeukAgentFlow|deuk-agent-rule/.test(before)) return false;
351
+
352
+ const unmanagedContent = removeLegacyHtmlManagedBlock(before);
353
+ if (unmanagedContent !== null) {
354
+ const next = unmanagedContent.trim();
355
+ if (!dryRun) {
356
+ if (next) writeFileSync(absPath, `${next}\n`, "utf8");
357
+ else unlinkSync(absPath);
358
+ }
359
+ const action = dryRun
360
+ ? `would ${cliText(next ? "init.cleanup.strippedBlock" : "init.cleanup.removedPointer", { locale })}`
361
+ : cliText(next ? "init.cleanup.strippedBlock" : "init.cleanup.removedPointer", { locale });
362
+ console.log(cliText("init.cleanup.message", { locale, vars: { action, path: toRepoRelativePath(cwd, absPath) } }));
363
+ return true;
364
+ }
365
+
366
+ const { pointer, projectDoc } = splitProjectDoc(before);
367
+ if (!isGeneratedDeukPointer(pointer)) return false;
368
+
369
+ const next = projectDoc.trim();
370
+ if (!dryRun) {
371
+ if (next) writeFileSync(absPath, `${next}\n`, "utf8");
372
+ else unlinkSync(absPath);
373
+ }
374
+ const action = dryRun
375
+ ? `would ${cliText(next ? "init.cleanup.strippedPointer" : "init.cleanup.removedPointer", { locale })}`
376
+ : cliText(next ? "init.cleanup.strippedPointer" : "init.cleanup.removedPointer", { locale });
377
+ console.log(cliText("init.cleanup.message", { locale, vars: { action, path: toRepoRelativePath(cwd, absPath) } }));
378
+ return true;
379
+ }
380
+
381
+ export function cleanupNestedGeneratedAgentPointers(cwd, dryRun) {
382
+ const locale = resolveInitLocale();
383
+ if (!existsSync(makePath(cwd, ".git"))) return 0;
384
+
385
+ let count = 0;
386
+ walkInitTextSurfaces(cwd, (absPath) => {
387
+ if (cleanupGeneratedAgentPointerSurface(absPath, cwd, dryRun)) count += 1;
388
+ });
389
+ if (count > 0) {
390
+ console.log(cliText("init.cleanup.summary", { locale, vars: { action: dryRun ? "would remove/strip" : "removed/stripped", count, workspace: basename(cwd) } }));
391
+ }
392
+ return count;
393
+ }
394
+
395
+ export function removeRuntimeTemplateCopies(cwd, dryRun) {
396
+ removeLegacyPaths(cwd, dryRun, CANONICAL_INIT_CLEANUP_PATHS.runtimeTemplateCopies, "runtime template copy");
397
+ }
398
+
399
+ export function removeLocalSkillCopies(cwd, dryRun) {
400
+ removeLegacyPaths(cwd, dryRun, CANONICAL_INIT_CLEANUP_PATHS.localSkillCopies, "local skill copy");
401
+ }
402
+
403
+ // Surgically remove the deuk-managed block from a shared-name surface (CLAUDE.md,
404
+ // AGENTS.md, copilot-instructions.md, …), preserving any user content. Deletes the
405
+ // file only when nothing of the user's remains. Never blunt-deletes a file that may
406
+ // hold the user's own Claude/agent settings.
407
+ function stripDeukManagedSurface(absPath, cwd, dryRun) {
408
+ if (!existsSync(absPath)) return;
409
+ const before = readFileSync(absPath, "utf8");
410
+ if (!/deuk-agent-managed|DeukAgentFlow|deuk-agent-rule/.test(before)) return; // not ours — leave untouched
411
+
412
+ let rest = null;
413
+ const block = splitManagedBlock(before); // current HTML-marked form
414
+ if (block) {
415
+ rest = [block.before, block.after].map(s => String(s || "").trim()).filter(Boolean).join("\n\n");
416
+ } else {
417
+ const legacy = removeLegacyHtmlManagedBlock(before); // older deuk-agent-rule markers
418
+ if (legacy !== null) {
419
+ rest = legacy.trim();
420
+ } else {
421
+ const { pointer, projectDoc } = splitProjectDoc(before); // bare "## DeukAgentFlow" section form
422
+ if (!isGeneratedDeukPointer(pointer)) return; // can't isolate deuk content safely — leave
423
+ rest = String(projectDoc || "").trim();
424
+ }
425
+ }
426
+
427
+ if (dryRun) {
428
+ console.log(`[CLEANUP] would ${rest ? "strip deuk block from" : "remove deuk surface"}: ${toRepoRelativePath(cwd, absPath)}`);
429
+ return;
430
+ }
431
+ if (rest) {
432
+ writeFileSync(absPath, `${rest}\n`, "utf8");
433
+ console.log(`[CLEANUP] stripped deuk block, kept user content: ${toRepoRelativePath(cwd, absPath)}`);
434
+ } else {
435
+ rmSync(absPath, { force: true });
436
+ console.log(`[CLEANUP] removed deuk-managed surface: ${toRepoRelativePath(cwd, absPath)}`);
437
+ }
438
+ }
439
+
440
+ export function removeLocalAgentSurfaces(cwd, dryRun) {
441
+ removeLegacyPaths(cwd, dryRun, CANONICAL_INIT_CLEANUP_PATHS.localAgentSurfaces, "local agent surface");
442
+ for (const rel of CANONICAL_INIT_CLEANUP_PATHS.sharedAgentSurfaces) {
443
+ stripDeukManagedSurface(makePath(cwd, rel), cwd, dryRun);
444
+ }
445
+ for (const relDir of [".cursor", ".claude", ".codex", ".windsurf", ".aiassistant"]) {
446
+ removeEmptyDirsBottomUp(makePath(cwd, relDir), cwd, dryRun);
447
+ }
448
+ }
449
+
450
+ const AUTO_MIGRATION_SKIP_DIRS = new Set([
451
+ ".git",
452
+ ".hg",
453
+ ".svn",
454
+ DEUK_ROOT_DIR,
455
+ ...LEGACY_DEUK.dirs,
456
+ "node_modules",
457
+ "dist",
458
+ "build",
459
+ "coverage",
460
+ ".next",
461
+ ".turbo",
462
+ ".venv",
463
+ "__pycache__"
464
+ ]);
465
+
466
+ export function removeFragmentedLocalAgentSurfaces(cwd, dryRun) {
467
+ let removed = removeLegacyPaths(cwd, dryRun, CANONICAL_INIT_CLEANUP_PATHS.localAgentSurfaces, "local agent surface");
468
+ const stack = [cwd];
469
+ const visited = new Set();
470
+
471
+ while (stack.length > 0) {
472
+ const current = stack.pop();
473
+ if (!current || visited.has(current) || !existsSync(current)) continue;
474
+ visited.add(current);
475
+
476
+ let entries = [];
477
+ try {
478
+ entries = sortedDirEntries(current, { withFileTypes: true });
479
+ } catch {
480
+ continue;
481
+ }
482
+
483
+ for (const relPath of CANONICAL_INIT_CLEANUP_PATHS.localAgentSurfaces) {
484
+ const target = makePath(current, relPath);
485
+ if (!existsSync(target)) continue;
486
+ if (!dryRun) rmSync(target, { recursive: true, force: true });
487
+ console.log(`[CLEANUP] removed fragmented local agent surface: ${toRepoRelativePath(cwd, target)}`);
488
+ removed += 1;
489
+ }
490
+
491
+ for (const entry of entries) {
492
+ if (!entry.isDirectory()) continue;
493
+ if (AUTO_MIGRATION_SKIP_DIRS.has(entry.name)) continue;
494
+ stack.push(makePath(current, entry.name));
495
+ }
496
+
497
+ for (const relDir of [".cursor", ".claude", ".codex", ".windsurf", ".aiassistant"]) {
498
+ removeEmptyDirsBottomUp(makePath(current, relDir), cwd, dryRun);
499
+ }
500
+ }
501
+
502
+ return removed;
503
+ }
504
+
@@ -0,0 +1,63 @@
1
+ import { dirname, basename } from "path";
2
+ import { existsSync, writeFileSync, mkdirSync } from "fs";
3
+ import { createHash } from "crypto";
4
+ import { DEUK_ROOT_DIR, CliOpts, buildWorkspaceAliases, hasWorkspaceMarker, makePath, normalizeRegistryPath, resolveUserHome, writeFileLF } from "./cli-utils.js";
5
+
6
+ // ─── 내부 헬퍼: 워크스페이스 레지스트리 ─────────────────────────────────
7
+ function hashPath(value) {
8
+ return createHash("sha256").update(normalizeRegistryPath(String(value || "")), "utf8").digest("hex");
9
+ }
10
+
11
+ function workspaceRegistryRoot(homeDir = resolveUserHome()) {
12
+ return makePath(homeDir, ".agent-flow", "workspace");
13
+ }
14
+
15
+ function safeRegistryFileName(name) {
16
+ const cleaned = String(name || "workspace")
17
+ .replace(/[^A-Za-z0-9._-]+/g, "-")
18
+ .replace(/^-+|-+$/g, "");
19
+ return `${cleaned || "workspace"}.json`;
20
+ }
21
+
22
+ // ─── 공개 API ─────────────────────────────────────────────────────────────
23
+
24
+ export function updateSiblingWorkspaceRegistry(cwd, opts: CliOpts = {}) {
25
+ const workspacePath = normalizeRegistryPath(cwd || "");
26
+ if (!hasWorkspaceMarker(workspacePath)) return false;
27
+
28
+ const homeDir = resolveUserHome(opts);
29
+ // #623: never register the user home or anything under ~/.deuk-agent as a
30
+ // workspace — that store holds relocated tickets, not a sibling workspace, and
31
+ // registering it makes ticket ids look ambiguous across "workspaces".
32
+ const homeNorm = normalizeRegistryPath(homeDir);
33
+ const storeNorm = normalizeRegistryPath(makePath(homeDir, DEUK_ROOT_DIR));
34
+ if (workspacePath === homeNorm
35
+ || workspacePath === storeNorm
36
+ || workspacePath.startsWith(storeNorm + "/")) {
37
+ return false;
38
+ }
39
+ const dryRun = Boolean(opts.dryRun);
40
+ const parentRoot = dirname(workspacePath);
41
+ const workspaceName = basename(workspacePath);
42
+ const parentRootHash = hashPath(parentRoot);
43
+ const pathHash = hashPath(workspacePath);
44
+ const registryDir = makePath(workspaceRegistryRoot(homeDir), parentRootHash);
45
+ const registryPath = makePath(registryDir, safeRegistryFileName(workspaceName));
46
+ const payload = {
47
+ version: 1,
48
+ name: workspaceName,
49
+ path: workspacePath,
50
+ aliases: buildWorkspaceAliases(workspaceName, workspacePath),
51
+ parentRoot,
52
+ parentRootHash,
53
+ pathHash,
54
+ updatedAt: new Date().toISOString(),
55
+ };
56
+
57
+ if (!dryRun) {
58
+ mkdirSync(registryDir, { recursive: true });
59
+ writeFileLF(registryPath, JSON.stringify(payload, null, 2) + "\n");
60
+ }
61
+ return { registryPath, payload };
62
+ }
63
+