deuk-agent-flow 5.0.2 → 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 (98) hide show
  1. package/CHANGELOG.ko.md +18 -0
  2. package/CHANGELOG.md +25 -0
  3. package/bundled/deuk-agent-flow.vsix +0 -0
  4. package/core-rules/AGENTS.md +5 -5
  5. package/package.json +20 -2
  6. package/scripts/bundle-vscode-vsix.ts +1 -1
  7. package/scripts/cli-context-commands.ts +35 -0
  8. package/scripts/cli-init-claude.ts +179 -0
  9. package/scripts/cli-init-core.ts +89 -0
  10. package/scripts/cli-init-instructions.ts +489 -0
  11. package/scripts/cli-init-migrate.ts +504 -0
  12. package/scripts/cli-init-workspace.ts +63 -0
  13. package/scripts/cli-locales.ts +85 -0
  14. package/scripts/cli-skill-commands.ts +3 -1
  15. package/scripts/cli-ticket-commands.ts +58 -0
  16. package/scripts/cli-ticket-docmeta.ts +103 -0
  17. package/scripts/cli-ticket-document.ts +153 -0
  18. package/scripts/cli-ticket-evidence.ts +312 -0
  19. package/scripts/cli-ticket-home.ts +449 -0
  20. package/scripts/cli-ticket-scan.ts +102 -0
  21. package/scripts/cli-ticket-surface.ts +56 -0
  22. package/scripts/oss-prepack.js +32 -0
  23. package/scripts/out/scripts/bundle-vscode-vsix.js +68 -0
  24. package/scripts/out/scripts/bundle-vscode-vsix.js.map +1 -0
  25. package/scripts/out/scripts/cli-args.js +374 -0
  26. package/scripts/out/scripts/cli-args.js.map +1 -0
  27. package/scripts/out/scripts/cli-context-commands.js +31 -0
  28. package/scripts/out/scripts/cli-context-commands.js.map +1 -0
  29. package/scripts/out/scripts/cli-init-claude.js +176 -0
  30. package/scripts/out/scripts/cli-init-claude.js.map +1 -0
  31. package/scripts/out/scripts/cli-init-commands.js +87 -0
  32. package/scripts/out/scripts/cli-init-commands.js.map +1 -0
  33. package/scripts/out/scripts/cli-init-core.js +89 -0
  34. package/scripts/out/scripts/cli-init-core.js.map +1 -0
  35. package/scripts/out/scripts/cli-init-instructions.js +460 -0
  36. package/scripts/out/scripts/cli-init-instructions.js.map +1 -0
  37. package/scripts/out/scripts/cli-init-logic.js +41 -0
  38. package/scripts/out/scripts/cli-init-logic.js.map +1 -0
  39. package/scripts/out/scripts/cli-init-migrate.js +497 -0
  40. package/scripts/out/scripts/cli-init-migrate.js.map +1 -0
  41. package/scripts/out/scripts/cli-init-workspace.js +57 -0
  42. package/scripts/out/scripts/cli-init-workspace.js.map +1 -0
  43. package/scripts/out/scripts/cli-locales.js +83 -0
  44. package/scripts/out/scripts/cli-locales.js.map +1 -0
  45. package/scripts/out/scripts/cli-prompts.js +101 -0
  46. package/scripts/out/scripts/cli-prompts.js.map +1 -0
  47. package/scripts/out/scripts/cli-rule-compiler.js +121 -0
  48. package/scripts/out/scripts/cli-rule-compiler.js.map +1 -0
  49. package/scripts/out/scripts/cli-skill-commands.js +708 -0
  50. package/scripts/out/scripts/cli-skill-commands.js.map +1 -0
  51. package/scripts/out/scripts/cli-telemetry-commands.js +604 -0
  52. package/scripts/out/scripts/cli-telemetry-commands.js.map +1 -0
  53. package/scripts/out/scripts/cli-ticket-command-shared.js +2 -0
  54. package/scripts/out/scripts/cli-ticket-command-shared.js.map +1 -0
  55. package/scripts/out/scripts/cli-ticket-commands.js +3530 -0
  56. package/scripts/out/scripts/cli-ticket-commands.js.map +1 -0
  57. package/scripts/out/scripts/cli-ticket-docmeta.js +90 -0
  58. package/scripts/out/scripts/cli-ticket-docmeta.js.map +1 -0
  59. package/scripts/out/scripts/cli-ticket-document.js +121 -0
  60. package/scripts/out/scripts/cli-ticket-document.js.map +1 -0
  61. package/scripts/out/scripts/cli-ticket-evidence.js +286 -0
  62. package/scripts/out/scripts/cli-ticket-evidence.js.map +1 -0
  63. package/scripts/out/scripts/cli-ticket-home.js +440 -0
  64. package/scripts/out/scripts/cli-ticket-home.js.map +1 -0
  65. package/scripts/out/scripts/cli-ticket-index.js +272 -0
  66. package/scripts/out/scripts/cli-ticket-index.js.map +1 -0
  67. package/scripts/out/scripts/cli-ticket-migration.js +261 -0
  68. package/scripts/out/scripts/cli-ticket-migration.js.map +1 -0
  69. package/scripts/out/scripts/cli-ticket-parser.js +90 -0
  70. package/scripts/out/scripts/cli-ticket-parser.js.map +1 -0
  71. package/scripts/out/scripts/cli-ticket-scan.js +93 -0
  72. package/scripts/out/scripts/cli-ticket-scan.js.map +1 -0
  73. package/scripts/out/scripts/cli-ticket-surface.js +53 -0
  74. package/scripts/out/scripts/cli-ticket-surface.js.map +1 -0
  75. package/scripts/out/scripts/cli-usage-commands.js +310 -0
  76. package/scripts/out/scripts/cli-usage-commands.js.map +1 -0
  77. package/scripts/out/scripts/cli-utils.js +1544 -0
  78. package/scripts/out/scripts/cli-utils.js.map +1 -0
  79. package/scripts/out/scripts/cli.js +696 -0
  80. package/scripts/out/scripts/cli.js.map +1 -0
  81. package/scripts/out/scripts/lint-md.js +238 -0
  82. package/scripts/out/scripts/lint-md.js.map +1 -0
  83. package/scripts/out/scripts/lint-rules.js +193 -0
  84. package/scripts/out/scripts/lint-rules.js.map +1 -0
  85. package/scripts/out/scripts/merge-logic.js +38 -0
  86. package/scripts/out/scripts/merge-logic.js.map +1 -0
  87. package/scripts/out/scripts/plan-parser.js +52 -0
  88. package/scripts/out/scripts/plan-parser.js.map +1 -0
  89. package/scripts/out/scripts/ticket-machine.js +94 -0
  90. package/scripts/out/scripts/ticket-machine.js.map +1 -0
  91. package/scripts/out/scripts/ticket-workflow.js +498 -0
  92. package/scripts/out/scripts/ticket-workflow.js.map +1 -0
  93. package/scripts/ticket-machine.ts +99 -0
  94. package/scripts/ticket-workflow.ts +544 -0
  95. package/templates/skills/role-closer/SKILL.md +27 -0
  96. package/templates/skills/role-implementer/SKILL.md +30 -0
  97. package/templates/skills/role-planner/SKILL.md +29 -0
  98. package/templates/skills/role-qa-verifier/SKILL.md +30 -0
@@ -0,0 +1,696 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
3
+ import { basename, dirname } from "path";
4
+ import { parseArgs, parseSkillArgs, parseTelemetryArgs, parseTicketArgs, parseUsageArgs } from "./cli-args.js";
5
+ import { formatInitCompletionMessage, runWorkspaceMaintenance } from "./cli-init-commands.js";
6
+ import { removeClaudeUserPromptSubmitHook } from "./cli-init-claude.js";
7
+ import { runGlobalAgentInstructionSync } from "./cli-init-instructions.js";
8
+ import { buildTicketCreateGuide, buildTicketRulesGuide, runTicketArchive, runTicketClose, runTicketConnect, runTicketCreate, runTicketDiscard, runTicketDoctor, runTicketEvidenceCheck, runTicketEvidenceReport, runTicketGuard, runTicketHandoff, runTicketHotfix, runTicketList, runTicketMeta, runTicketMigrateHome, runTicketMove, runTicketNext, runTicketRebuild, runTicketRules, runTicketSanitize, runTicketStatus, runTicketUse } from "./cli-ticket-commands.js";
9
+ import { runTelemetry } from "./cli-telemetry-commands.js";
10
+ import { runUsage } from "./cli-usage-commands.js";
11
+ import { runContextCommand } from "./cli-context-commands.js";
12
+ import { performUpgradeMigration } from "./cli-ticket-migration.js";
13
+ import { pruneGhostWorkspaces, resolveHomeTicketDirForWorkspace } from "./cli-ticket-home.js";
14
+ import { collectTicketMarkdownFiles } from "./cli-ticket-scan.js";
15
+ import { LEGACY_DEUK, DEUK_ROOT_DIR, INIT_CONFIG_VERSION, WORKFLOW_MODE_EXECUTE, checkUpdateNotifier, findWorkspaceRoot, getUserInitConfigPath, loadInitConfig, loadWorkspaceCandidates, makePath, resolveDocsLanguage, resolvePackageRoot, resolveSessionId, resolveWorkflowMode, resolveWorkspaceTarget } from "./cli-utils.js";
16
+ void checkUpdateNotifier();
17
+ const pkgRoot = resolvePackageRoot({ fromUrl: import.meta.url });
18
+ function parseEntryOptions(sub, rest) {
19
+ if (sub === "ticket")
20
+ return parseTicketArgs(rest.slice(1));
21
+ if (sub === "telemetry")
22
+ return parseTelemetryArgs(rest);
23
+ if (sub === "usage")
24
+ return parseUsageArgs(rest.slice(1));
25
+ if (sub === "skill")
26
+ return parseSkillArgs(rest.slice(1));
27
+ if (sub === "workspace")
28
+ return parseArgs(rest.slice(1));
29
+ if (sub === "rules") {
30
+ const action = rest[0];
31
+ return parseArgs(action && !String(action).startsWith("-") ? rest.slice(1) : rest);
32
+ }
33
+ if (sub === "init" || sub === "merge")
34
+ return parseArgs(rest);
35
+ if (sub === "context" || sub === "config" || sub === "lint:md" || sub === "lint-md")
36
+ return parseArgs(rest);
37
+ return parseArgs(rest);
38
+ }
39
+ function resolveAgentWorkspaceRoot(startDir) {
40
+ // Must NOT use resolveWorkspaceContext here: it falls back to resolve(cwd) when
41
+ // no .deuk-agent marker exists, so the home dir (e.g. C:\Users\joy) would look
42
+ // like a workspace and trigger maintenance that crashes on null paths. Use the
43
+ // marker-based finder (with its home guard) so only real workspaces qualify.
44
+ return findWorkspaceRoot(startDir || process.cwd()) || "";
45
+ }
46
+ function hasManagedAgentWorkspace(cwd) {
47
+ return Boolean(resolveAgentWorkspaceRoot(cwd));
48
+ }
49
+ async function main() {
50
+ const argv = process.argv.slice(2);
51
+ const sub = argv[0];
52
+ if (!sub || sub === "-h" || sub === "--help" || sub === "help") {
53
+ printAugmentedHelp();
54
+ return;
55
+ }
56
+ if (sub === "--version" || sub === "-v" || sub === "version") {
57
+ try {
58
+ const pkg = JSON.parse(readFileSync(makePath(pkgRoot, "package.json"), "utf8"));
59
+ console.log(pkg.version);
60
+ }
61
+ catch {
62
+ console.error("unable to read package version");
63
+ process.exitCode = 1;
64
+ }
65
+ return;
66
+ }
67
+ const rest = argv.slice(1);
68
+ const entryOpts = parseEntryOptions(sub, rest);
69
+ const shouldRunAutoMigration = shouldRunEntryAutoMigration(sub, rest);
70
+ const entryAutoMigrationCwd = shouldRunAutoMigration ? resolveEntryAutoMigrationCwd(sub, entryOpts) : entryOpts.cwd;
71
+ const shouldSkipAutoMaintenance = shouldRunAutoMigration && !hasManagedAgentWorkspace(entryAutoMigrationCwd);
72
+ if (shouldRunAutoMigration && !shouldSkipAutoMaintenance) {
73
+ runWorkspaceMaintenance(entryAutoMigrationCwd, entryOpts.dryRun, pkgRoot, {
74
+ ...entryOpts,
75
+ silent: true
76
+ });
77
+ }
78
+ if (sub === "ticket") {
79
+ const action = rest[0];
80
+ const opts = entryOpts;
81
+ if (opts.cwdExplicit) {
82
+ throw new Error("ticket commands no longer accept --cwd. Use --workspace <id> for target selection, or --id for existing tickets.");
83
+ }
84
+ // #638: prune-ghosts is a global home-store operation — it spans all
85
+ // workspaces and needs no --workspace, so handle it before workspace
86
+ // resolution (which would otherwise reject it for lacking --workspace).
87
+ if (action === "prune-ghosts") {
88
+ await runTicketPruneGhosts(opts);
89
+ return;
90
+ }
91
+ if (!action || action === "-h" || action === "--help" || action === "help" || opts.help) {
92
+ printTicketGuide(opts.help && action && !String(action).startsWith("-") ? action : "", opts);
93
+ return;
94
+ }
95
+ opts.cwd = resolveTicketWorkspaceCwd(opts);
96
+ // #623: migrate this workspace's tickets to home once, here at the single ticket
97
+ // command entry point. detect itself is now a pure read, so this is the only
98
+ // place (besides init) that performs the move/registry write. Idempotent.
99
+ try {
100
+ const { resolveRealWorkspaceRoot } = await import("./cli-utils.js");
101
+ const home = await import("./cli-ticket-home.js");
102
+ // #626: never fall back to opts.cwd — a non-workspace cwd (temp/subfolder) must
103
+ // NOT be registered as a ghost workspace. null → migration is simply skipped.
104
+ // #645: no allowCreate here — a ticket command running in some cwd only ever
105
+ // self-heals an ALREADY-registered workspace (marker present). It must NEVER
106
+ // mint a new one; that auto-on-cwd behavior is what spawned the ghosts. New
107
+ // workspaces are registered exclusively by `deuk-agent-flow init`.
108
+ const wsRoot = resolveRealWorkspaceRoot(opts.cwd, opts);
109
+ if (wsRoot)
110
+ home.ensureWorkspaceMigrated(wsRoot);
111
+ }
112
+ catch { /* migration is best-effort; commands still work via fallback */ }
113
+ // #622-D: keep native (Claude) skills in sync with the SSOT on ticket entry —
114
+ // package-template updates propagate to ~/.claude/skills without manual re-expose.
115
+ try {
116
+ const { syncExposedNativeSkills } = await import("./cli-skill-commands.js");
117
+ syncExposedNativeSkills(opts.cwd);
118
+ }
119
+ catch { /* skill sync is best-effort */ }
120
+ if (action === "create")
121
+ await runTicketCreate(opts);
122
+ else if (action === "list")
123
+ await runTicketList(opts);
124
+ else if (action === "use")
125
+ await runTicketUse(opts);
126
+ else if (action === "next")
127
+ await runTicketNext(opts);
128
+ else if (action === "evidence")
129
+ await runTicketEvidenceCheck(opts);
130
+ else if (action === "close")
131
+ await runTicketClose(opts);
132
+ else if (action === "archive")
133
+ await runTicketArchive(opts);
134
+ else if (action === "discard" || action === "delete")
135
+ await runTicketDiscard(opts);
136
+ else if (action === "meta")
137
+ await runTicketMeta(opts);
138
+ else if (action === "connect")
139
+ await runTicketConnect(opts);
140
+ else if (action === "rebuild")
141
+ await runTicketRebuild(opts);
142
+ else if (action === "sanitize")
143
+ await runTicketSanitize(opts);
144
+ else if (action === "doctor")
145
+ await runTicketDoctor(opts);
146
+ else if (action === "move" || action === "step")
147
+ await runTicketMove(opts);
148
+ else if (action === "hotfix")
149
+ await runTicketHotfix(opts);
150
+ else if (action === "status")
151
+ await runTicketStatus(opts);
152
+ else if (action === "migrate-home" || action === "migrate-tickets-to-home")
153
+ await runTicketMigrateHome(opts);
154
+ else if (action === "guard") {
155
+ await runTicketGuard(opts);
156
+ }
157
+ else if (action === "context") {
158
+ throw new Error("ticket context has been removed from the approval surface. Use: deuk-agent-flow ticket move --id <ticket-id> --phase 2 --approval approved --non-interactive");
159
+ }
160
+ else if (action === "handoff" || action === "continue")
161
+ await runTicketHandoff(opts);
162
+ else if (action === "report") {
163
+ // #727: report/reports(레거시 cwd 기록)는 제거. evidence-report(claim)만 유지.
164
+ if (opts.claim) {
165
+ await runTicketEvidenceReport(opts);
166
+ }
167
+ else {
168
+ throw new Error("ticket report/reports has been removed (#727). Reports were a legacy cwd artifact.");
169
+ }
170
+ }
171
+ else if (action === "upgrade" || action === "migrate") {
172
+ const count = performUpgradeMigration(opts.cwd, opts);
173
+ console.log(`Migration complete: ${count} tickets upgraded.`);
174
+ }
175
+ else {
176
+ console.error("Unknown ticket action: " + action);
177
+ // #648: unknown subcommand → plain usage, not the rules gate.
178
+ printTicketGuide("", { ...opts, help: true });
179
+ }
180
+ return;
181
+ }
182
+ if (sub === "telemetry") {
183
+ const opts = entryOpts;
184
+ await runTelemetry(opts);
185
+ return;
186
+ }
187
+ if (sub === "usage") {
188
+ const action = rest[0];
189
+ const opts = entryOpts;
190
+ await runUsage(action, opts);
191
+ return;
192
+ }
193
+ if (sub === "workspace") {
194
+ const action = rest[0];
195
+ const opts = entryOpts;
196
+ runWorkspace(action, opts, rest.slice(1));
197
+ return;
198
+ }
199
+ if (sub === "context") {
200
+ runContextCommand(rest);
201
+ return;
202
+ }
203
+ if (sub === "config") {
204
+ runConfig(rest[0], entryOpts, rest.slice(1));
205
+ return;
206
+ }
207
+ if (sub === "skill") {
208
+ const action = rest[0];
209
+ const opts = entryOpts;
210
+ const { runSkill } = await import("./cli-skill-commands.js");
211
+ await runSkill(action, opts);
212
+ return;
213
+ }
214
+ if (sub === "lint:md" || sub === "lint-md") {
215
+ const { runMarkdownLint } = await import("./lint-md.js");
216
+ runMarkdownLint(rest);
217
+ return;
218
+ }
219
+ if (sub === "rules") {
220
+ const action = rest[0] && !String(rest[0]).startsWith("-") ? rest[0] : undefined;
221
+ const opts = entryOpts;
222
+ if (!action) {
223
+ runRulesShow(opts);
224
+ return;
225
+ }
226
+ if (action === "audit") {
227
+ const { runRulesAudit } = await import("./lint-rules.js");
228
+ runRulesAudit(opts);
229
+ return;
230
+ }
231
+ if (action === "ticket") {
232
+ if (opts.cwdExplicit) {
233
+ throw new Error("rules ticket no longer accepts --cwd. Load the ticket surface with `deuk-agent-flow rules ticket`.");
234
+ }
235
+ opts.cwd = resolveTicketWorkspaceCwd(opts);
236
+ opts.ruleSurface = "ticket";
237
+ await runTicketRules(opts);
238
+ return;
239
+ }
240
+ if (action === "show") {
241
+ runRulesShow(opts);
242
+ return;
243
+ }
244
+ if (action === "path") {
245
+ runRulesPath(opts);
246
+ return;
247
+ }
248
+ console.error("Unknown rules action: " + action);
249
+ printAugmentedHelp("rules");
250
+ return;
251
+ }
252
+ if (sub === "init" || sub === "merge") {
253
+ const opts = entryOpts;
254
+ if (opts.help) {
255
+ printAugmentedHelp(sub);
256
+ return;
257
+ }
258
+ // Explicit init/merge is allowed to bootstrap a new workspace even when
259
+ // entry auto-maintenance would skip the same path for lacking .deuk-agent.
260
+ const saved = loadInitConfig(opts.cwd, opts);
261
+ if (saved && !opts.interactive) {
262
+ // CLI flags (opts) take precedence over saved config
263
+ for (const key in saved) {
264
+ if (opts[key] === undefined)
265
+ opts[key] = saved[key];
266
+ }
267
+ console.log(`Using saved ${saved.configScope || "workspace"} config from ${saved.configPath || `${DEUK_ROOT_DIR}/config.json`} (CLI overrides applied)`);
268
+ }
269
+ await handleInit(opts, saved, sub);
270
+ return;
271
+ }
272
+ console.error("Unknown command: " + sub);
273
+ printAugmentedHelp();
274
+ }
275
+ function shouldRunEntryAutoMigration(sub, rest = []) {
276
+ if (sub === "workspace")
277
+ return false;
278
+ if (sub === "config")
279
+ return false;
280
+ if (rest.some(arg => arg === "-h" || arg === "--help" || arg === "help"))
281
+ return false;
282
+ if (sub === "rules" && (rest[0] === "path" || rest[0] === "show"))
283
+ return false;
284
+ return true;
285
+ }
286
+ function resolveEntryAutoMigrationCwd(sub, opts = {}) {
287
+ // Auto-migration is best-effort: resolve workspace from registry when possible.
288
+ // rules and ticket are the primary entry points that always know a workspace.
289
+ if (sub === "ticket" || sub === "rules") {
290
+ try {
291
+ return resolveTicketWorkspaceCwd(opts);
292
+ }
293
+ catch {
294
+ return opts.cwd;
295
+ }
296
+ }
297
+ return opts.cwd;
298
+ }
299
+ function resolveTicketWorkspaceCwd(opts = {}) {
300
+ const query = opts.workspace || "";
301
+ const candidates = loadWorkspaceCandidates("", { platform: "any", homeDir: opts.homeDir });
302
+ // #652: Workspace is resolved ONLY from registry mapping — explicit --workspace,
303
+ // or sessionId cookie (also registry-backed). cwd is never passed in or read.
304
+ const resolution = candidates ? resolveWorkspaceTarget("", query, { candidates, sessionId: opts.sessionId, homeDir: opts.homeDir }) : null;
305
+ if (resolution?.target?.path)
306
+ return resolution.target.path;
307
+ // #633: for commands targeting an existing ticket (move/use/close/status), the
308
+ // workspace can be derived from the ticket id by scanning each registered
309
+ // workspace's INDEX.json — this is still registry-only (cwd is never read), so
310
+ // it satisfies the no-cwd-fallback rule while keeping --id ergonomic.
311
+ if (!query && opts.ticketId) {
312
+ const byId = resolveTicketWorkspaceById(opts);
313
+ if (byId)
314
+ return byId;
315
+ }
316
+ const hint = query
317
+ ? `--workspace "${query}" did not match any registered workspace.`
318
+ : opts.ticketId
319
+ ? `--id "${opts.ticketId}" was not found in any registered workspace, and no --workspace was given.`
320
+ : `no --workspace was given and no session cookie matched.`;
321
+ throw new Error(`Cannot resolve workspace from the registry: ${hint}\n` +
322
+ `Workspace is determined only by --workspace or a session cookie (registry-backed); ` +
323
+ `cwd is never used. Pass --workspace <name> or register the workspace first.`);
324
+ }
325
+ // #638: prune empty ghost workspace folders (name-{idSuffix} leftovers with no
326
+ // .md) from the home store, plus their dead registry entries. Dry-run by default;
327
+ // --apply performs deletion. Global (home-store) op, no --workspace needed.
328
+ async function runTicketPruneGhosts(opts = {}) {
329
+ const result = pruneGhostWorkspaces({ ...opts, apply: Boolean(opts.apply) });
330
+ if (result.ghosts.length === 0) {
331
+ console.log("No ghost (empty) workspace folders found in the home store.");
332
+ return;
333
+ }
334
+ console.log(`Ghost (empty, no .md) workspace folders: ${result.ghosts.length}`);
335
+ for (const key of result.ghosts) {
336
+ console.log(opts.apply ? `[REMOVE] ${key}` : `[DRY-RUN] would remove ${key}`);
337
+ }
338
+ if (opts.apply) {
339
+ console.log(`Done: removed ${result.removed.length} folder(s), dropped ${result.registryDropped?.length ?? 0} registry entr(y/ies).`);
340
+ }
341
+ else {
342
+ console.log("Re-run with --apply to delete these folders and their dead registry entries.");
343
+ }
344
+ }
345
+ function resolveTicketWorkspaceById(opts = {}) {
346
+ const selector = String(opts.ticketId || "").trim();
347
+ if (!selector)
348
+ return "";
349
+ const candidates = loadWorkspaceCandidates(opts.invocationCwd || opts.cwd);
350
+ if (!candidates?.workspaces?.length)
351
+ return "";
352
+ const matches = [];
353
+ for (const workspace of candidates.workspaces) {
354
+ // #622/#633: tickets live in the home store (~/.deuk-agent/tickets/{ticketKey}/).
355
+ // Resolve the dir ONLY through the path util (resolveHomeTicketDirForWorkspace) and
356
+ // scan the actual .md files (collectTicketMarkdownFiles) — never hand-assemble paths
357
+ // or trust INDEX.json, which can drift out of sync with the real files on disk.
358
+ const homeDir = resolveHomeTicketDirForWorkspace(workspace.path, opts);
359
+ const found = collectTicketMarkdownFiles(homeDir)
360
+ .some(file => basename(file, ".md").includes(selector));
361
+ if (found)
362
+ matches.push(workspace);
363
+ }
364
+ if (matches.length === 1)
365
+ return matches[0].path;
366
+ if (matches.length > 1) {
367
+ throw new Error(`ticket id is ambiguous across registered workspaces: ${matches.map(workspace => workspace.id).join(", ")}`);
368
+ }
369
+ return "";
370
+ }
371
+ function runConfig(action, opts, argv = []) {
372
+ const configPath = getUserInitConfigPath(opts);
373
+ if (!action || action === "path") {
374
+ console.log(configPath);
375
+ return;
376
+ }
377
+ if (action === "show") {
378
+ if (!existsSync(configPath)) {
379
+ console.log("{}");
380
+ return;
381
+ }
382
+ console.log(JSON.stringify(JSON.parse(readFileSync(configPath, "utf8")), null, 2));
383
+ return;
384
+ }
385
+ if (action === "set") {
386
+ const key = argv[0];
387
+ const value = argv[1];
388
+ if (!key || value === undefined)
389
+ throw new Error("config set requires <key> <value>");
390
+ const existing = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {};
391
+ const next = { ...existing, version: INIT_CONFIG_VERSION, [key]: parseConfigValue(value), updatedAt: new Date().toISOString() };
392
+ mkdirSync(dirname(configPath), { recursive: true });
393
+ writeFileSync(configPath, JSON.stringify(next, null, 2), "utf8");
394
+ console.log(`config set: ${key}`);
395
+ return;
396
+ }
397
+ if (action === "unset") {
398
+ const key = argv[0];
399
+ if (!key)
400
+ throw new Error("config unset requires <key>");
401
+ const existing = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {};
402
+ delete existing[key];
403
+ existing.updatedAt = new Date().toISOString();
404
+ mkdirSync(dirname(configPath), { recursive: true });
405
+ writeFileSync(configPath, JSON.stringify(existing, null, 2), "utf8");
406
+ console.log(`config unset: ${key}`);
407
+ return;
408
+ }
409
+ throw new Error("Unknown config action: " + action);
410
+ }
411
+ function parseConfigValue(value) {
412
+ if (value === "true")
413
+ return true;
414
+ if (value === "false")
415
+ return false;
416
+ if (value === "null")
417
+ return null;
418
+ if (/^-?\d+(\.\d+)?$/.test(value))
419
+ return Number(value);
420
+ if (String(value).includes(","))
421
+ return String(value).split(",").map(part => part.trim()).filter(Boolean);
422
+ return value;
423
+ }
424
+ function runWorkspace(action, opts, argv = []) {
425
+ // #636: use the same global-registry-first loader as ticket commands, so
426
+ // `workspace list/resolve` work from any cwd (incl. neutral dirs) instead of
427
+ // only seeing cwd-sibling candidates.
428
+ const candidates = loadWorkspaceCandidates(opts.invocationCwd || opts.cwd, { platform: "any" });
429
+ if (!candidates) {
430
+ throw new Error("workspace candidates not found");
431
+ }
432
+ if (action === "list" || !action) {
433
+ for (const workspace of candidates.workspaces) {
434
+ console.log(`${workspace.id}\t${workspace.path}`);
435
+ }
436
+ return;
437
+ }
438
+ if (action === "resolve") {
439
+ const query = findWorkspaceQuery(argv) || opts.workspace || opts.project;
440
+ const result = resolveWorkspaceTarget(opts.cwd, query, { candidates, sessionId: opts.sessionId });
441
+ if (!result?.target) {
442
+ const suffix = result?.reason ? ` (${result.reason})` : "";
443
+ throw new Error(`workspace target not found${suffix}: ${query || ""}`.trim());
444
+ }
445
+ console.log(result.target.path);
446
+ return;
447
+ }
448
+ throw new Error("Unknown workspace action: " + action);
449
+ }
450
+ function findWorkspaceQuery(argv = []) {
451
+ const valueOptions = new Set(["--cwd", "--workspace", "--project"]);
452
+ for (let i = 0; i < argv.length; i++) {
453
+ const arg = argv[i];
454
+ if (arg === "resolve")
455
+ continue;
456
+ if (valueOptions.has(arg)) {
457
+ i++;
458
+ continue;
459
+ }
460
+ if (arg.startsWith("--"))
461
+ continue;
462
+ return arg;
463
+ }
464
+ return "";
465
+ }
466
+ // Removed legacy migration runTicketMigrate
467
+ async function handleInit(opts, saved, sub = "init") {
468
+ if (opts.clean && !opts.dryRun) {
469
+ console.log(`[CLEAN] Removing runtime template copies, legacy templates, and config...`);
470
+ const templatesDir = makePath(opts.cwd, LEGACY_DEUK.templateDir);
471
+ const runtimeTemplatesDir = makePath(opts.cwd, DEUK_ROOT_DIR, "templates");
472
+ const configFile = makePath(opts.cwd, LEGACY_DEUK.configFile);
473
+ if (existsSync(templatesDir))
474
+ rmSync(templatesDir, { recursive: true, force: true });
475
+ if (existsSync(runtimeTemplatesDir))
476
+ rmSync(runtimeTemplatesDir, { recursive: true, force: true });
477
+ if (existsSync(configFile))
478
+ rmSync(configFile, { force: true });
479
+ }
480
+ const workflowMode = resolveWorkflowMode(opts, saved);
481
+ if (!opts.dryRun && workflowMode !== WORKFLOW_MODE_EXECUTE) {
482
+ console.log(`[WORKFLOW] Plan mode active. Re-run with --workflow execute or --approval approved to apply file mutations.`);
483
+ return;
484
+ }
485
+ runWorkspaceMaintenance(opts.cwd, opts.dryRun, pkgRoot, opts);
486
+ if (opts.clean && !opts.dryRun) {
487
+ removeClaudeUserPromptSubmitHook({ homeDir: opts.homeDir });
488
+ }
489
+ if (sub === "init")
490
+ console.log(formatInitCompletionMessage(opts.cwd, opts.dryRun));
491
+ }
492
+ function runRulesPath(opts = {}) {
493
+ runGlobalAgentInstructionSync(Boolean(opts.dryRun), pkgRoot, opts.homeDir);
494
+ const rulesPath = makePath([pkgRoot, "core-rules", "AGENTS.md"]);
495
+ const payload = { path: rulesPath, exists: existsSync(rulesPath) };
496
+ if (opts.json) {
497
+ console.log(JSON.stringify(payload));
498
+ }
499
+ else {
500
+ console.log(rulesPath);
501
+ }
502
+ if (!payload.exists) {
503
+ throw new Error(`core rules not found: ${rulesPath}`);
504
+ }
505
+ }
506
+ function runRulesShow(opts = {}) {
507
+ const locale = resolveDocsLanguage(opts.docsLanguage || "auto");
508
+ const msg = locale === "ko" ? "📦 📜 규칙 확인 중..." : "📦 📜 Checking rules...";
509
+ console.error(msg);
510
+ runGlobalAgentInstructionSync(Boolean(opts.dryRun), pkgRoot, opts.homeDir);
511
+ const rulesPath = makePath([pkgRoot, "core-rules", "AGENTS.md"]);
512
+ if (!existsSync(rulesPath)) {
513
+ throw new Error(`core rules not found: ${rulesPath}`);
514
+ }
515
+ const rawContent = readFileSync(rulesPath, "utf8");
516
+ const isWslEnv = !!(process.env.WSLENV || process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP);
517
+ const content = process.platform === "win32" && !isWslEnv
518
+ ? rawContent.replace(/```bash\n[\s\S]*?```/, "```powershell\n$planBody = @'\n## Scope & Approval\n범위: <어떤 파일/함수까지>\n요구사항: <사용자가 원하는 결과>\n접근법: <구체적 방법>\n## Plan\n1. <실행 단계>\n## Analysis\n원인: <근본 원인>\n- <file:line — 관찰 증거>\n가설: 1. <가설>\n## Direction\n<개선 방향>\n'@\ndeuk-agent-flow ticket create `\n --title \"<short-title>\" `\n --summary \"<one-line summary>\" `\n --plan-body $planBody --non-interactive\n```")
519
+ : rawContent;
520
+ const sectionContent = opts.section ? extractMarkdownHeadingSection(content, opts.section) : content;
521
+ if (opts.section && !sectionContent) {
522
+ throw new Error(`core rules section not found: ${opts.section}`);
523
+ }
524
+ if (opts.json) {
525
+ console.log(JSON.stringify({ path: rulesPath, section: opts.section || null, content: sectionContent }, null, 2));
526
+ return;
527
+ }
528
+ console.log(sectionContent.trimEnd());
529
+ // #643: --workspace flag를 우선 처리 — opts.cwd를 대상 워크스페이스 경로로 교체
530
+ // #871-followup: also resolve via the session cookie when no --workspace is given.
531
+ // The cookie is a home-level file keyed by sessionId (cwd-independent) recording
532
+ // the last-used workspace absolute path. Without this, a plain
533
+ // `deuk-agent-flow rules --session-id <s>` fell through to process.cwd() and showed
534
+ // the cwd workspace's active ticket instead of the session's last-used one — the
535
+ // root cause of "rules keeps pointing at a different workspace's ticket".
536
+ // #652: the workspace is resolved ONLY from --workspace + the session cookie
537
+ // (both registry-backed). It is NEVER inferred from cwd. effectiveCwd starts EMPTY
538
+ // and is filled solely by a successful registry resolution — if neither a
539
+ // --workspace nor a session cookie resolves, effectiveCwd stays "" and the block
540
+ // below routes the agent to pick a workspace from the registry instead of letting
541
+ // a bare cwd masquerade as a managed workspace.
542
+ let effectiveCwd = "";
543
+ let effectiveWorkspaceName = opts.workspace || "";
544
+ // #673: always attempt cookie-based workspace resolution — resolveSessionId() auto-detects
545
+ // the agent session UUID from env vars, so --session-id is no longer required.
546
+ if (!opts.section) {
547
+ try {
548
+ // startDir arg is ignored by loadWorkspaceCandidates (#645 registry-only).
549
+ const candidates = loadWorkspaceCandidates("", { platform: "any", homeDir: opts.homeDir });
550
+ const resolution = resolveWorkspaceTarget("", opts.workspace, { candidates, sessionId: resolveSessionId(opts.sessionId), homeDir: opts.homeDir });
551
+ if (resolution?.target) {
552
+ effectiveCwd = resolution.target.path;
553
+ effectiveWorkspaceName = resolution.target.id || opts.workspace;
554
+ }
555
+ }
556
+ catch { /* workspace not resolved → effectiveCwd stays "" (no cwd fallback) */ }
557
+ }
558
+ // Auto-compose ticket rules when a managed workspace is detected
559
+ if (!opts.section && !hasManagedAgentWorkspace(effectiveCwd)) {
560
+ // #633: cwd is not a managed workspace, but DON'T ignore the registry. If
561
+ // workspaces are registered, tell the agent to pick one with --workspace
562
+ // instead of declaring "no workspace, work freely" from cwd alone.
563
+ let registered = [];
564
+ try {
565
+ const candidates = loadWorkspaceCandidates("", { platform: "any", homeDir: opts.homeDir });
566
+ registered = (candidates?.workspaces || []).map(w => w.id).filter(Boolean).sort();
567
+ }
568
+ catch { /* registry unreadable */ }
569
+ if (registered.length > 0) {
570
+ const list = registered.map(id => ` - ${id}`).join("\n");
571
+ console.log(`\n## Next Action\nNo workspace selected. Workspace is resolved ONLY from the registry via --workspace ` +
572
+ `or the session cookie (never from cwd). ${registered.length} workspace(s) registered — pick one:\n${list}\n\n` +
573
+ `\`\`\`bash\ndeuk-agent-flow rules ticket --workspace ${registered[0]}\n\`\`\``);
574
+ }
575
+ else {
576
+ console.log("\n## Next Action\nNo managed workspace detected and none registered. Work freely without a ticket.");
577
+ }
578
+ return;
579
+ }
580
+ if (!opts.section && hasManagedAgentWorkspace(effectiveCwd)) {
581
+ // #652: effectiveCwd is already the registry-resolved workspace root — use it
582
+ // directly, no findWorkspaceRoot ancestor walk (that was a cwd-inference path).
583
+ const ticketOpts = { ...opts, cwd: effectiveCwd, workspace: effectiveWorkspaceName, ruleSurface: "ticket" };
584
+ const ticketRules = buildTicketRulesGuide(ticketOpts);
585
+ if (ticketRules && ticketRules.trim()) {
586
+ console.log("\n" + ticketRules.trimEnd());
587
+ }
588
+ }
589
+ }
590
+ function extractMarkdownHeadingSection(content = "", heading = "") {
591
+ const normalizeHeading = value => String(value || "")
592
+ .trim()
593
+ .replace(/^#+\s*/, "")
594
+ .replace(/^\d+[\).\s-]+/, "")
595
+ .toLowerCase();
596
+ const target = normalizeHeading(heading);
597
+ if (!target)
598
+ return "";
599
+ const lines = String(content || "").split(/\r?\n/);
600
+ let start = -1;
601
+ let level = 0;
602
+ for (const [index, line] of lines.entries()) {
603
+ const match = line.match(/^(#{1,6})\s+(.+?)\s*$/);
604
+ if (!match)
605
+ continue;
606
+ const title = normalizeHeading(match[2]);
607
+ if (title === target) {
608
+ start = index;
609
+ level = match[1].length;
610
+ break;
611
+ }
612
+ }
613
+ if (start < 0)
614
+ return "";
615
+ let end = lines.length;
616
+ for (let index = start + 1; index < lines.length; index++) {
617
+ const match = lines[index].match(/^(#{1,6})\s+/);
618
+ if (match && match[1].length <= level) {
619
+ end = index;
620
+ break;
621
+ }
622
+ }
623
+ return lines.slice(start, end).join("\n");
624
+ }
625
+ function readCoreRulesSection(heading) {
626
+ const rulesPath = makePath([pkgRoot, "core-rules", "AGENTS.md"]);
627
+ if (!existsSync(rulesPath))
628
+ return "";
629
+ return extractMarkdownHeadingSection(readFileSync(rulesPath, "utf8"), heading);
630
+ }
631
+ function readCliSurfaceDocument(name) {
632
+ const surfacePath = makePath(pkgRoot, "docs", "cli-surfaces", `${name}.md`);
633
+ if (!existsSync(surfacePath)) {
634
+ throw new Error(`CLI surface document not found: ${surfacePath}`);
635
+ }
636
+ return readFileSync(surfacePath, "utf8");
637
+ }
638
+ function renderCliSurfaceDocument(name, replacements = {}) {
639
+ let content = readCliSurfaceDocument(name);
640
+ content = content.replace(/\{\{CORE:([^}]+)\}\}/g, (_match, heading) => readCoreRulesSection(String(heading || "").trim()).trimEnd());
641
+ for (const [key, value] of Object.entries(replacements)) {
642
+ content = content.replaceAll(`{{${key}}}`, String(value ?? ""));
643
+ }
644
+ return content.trimEnd();
645
+ }
646
+ function printAugmentedHelp(surface = "root") {
647
+ console.log(renderCliSurfaceDocument("root", { SURFACE: surface }));
648
+ }
649
+ function printTicketGuide(action = "", opts = {}) {
650
+ if (action === "create") {
651
+ console.log(buildTicketCreateGuide(opts));
652
+ return;
653
+ }
654
+ // #648: `ticket --help` / `-h` / unknown subcommand must print plain usage —
655
+ // NOT the rules gate (which injects project-memory + Next Action). The rules
656
+ // surface stays reachable only via `deuk-agent-flow rules`.
657
+ if (opts.help || !action) {
658
+ console.log(buildTicketUsageGuide());
659
+ return;
660
+ }
661
+ console.log(buildTicketRulesGuide({ ...opts, ruleSurface: "ticket" }));
662
+ }
663
+ function buildTicketUsageGuide() {
664
+ return [
665
+ "deuk-agent-flow ticket — manage workflow tickets",
666
+ "",
667
+ "Usage:",
668
+ " deuk-agent-flow ticket <command> --workspace <name> [options]",
669
+ "",
670
+ "Commands:",
671
+ " create Create a ticket (requires --title, --summary, --plan-body)",
672
+ " use Set a ticket as the active ticket (--id)",
673
+ " move Advance/rollback phase (--id, --phase <n> | --to <state>, --approval)",
674
+ " status Show ticket status / active ticket link",
675
+ " close Close a ticket (--id, --status closed)",
676
+ " discard Discard a ticket without approval (--id)",
677
+ " list List tickets in the workspace",
678
+ "",
679
+ "Common options:",
680
+ " --workspace <name> Target workspace (registry-backed; required)",
681
+ " --id <ticket-id> Ticket id to act on",
682
+ " --phase <n> Phase number for `move`",
683
+ " --to <state> Target state for `move` (e.g. end)",
684
+ " --approval approved Confirm approval for a gated phase move",
685
+ " --non-interactive No prompts (for scripted/agent use)",
686
+ " -h, --help Show this help",
687
+ "",
688
+ "For the full rules gate (project rules, memory, next action), run:",
689
+ " deuk-agent-flow rules"
690
+ ].join("\n");
691
+ }
692
+ main().catch(async (err) => {
693
+ console.error(err.message || err);
694
+ process.exit(1);
695
+ });
696
+ //# sourceMappingURL=cli.js.map