deuk-agent-flow 4.2.7 → 5.0.2

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