@sduck/sduck-cli 0.1.3 → 0.1.5

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.
package/dist/cli.js CHANGED
@@ -3,8 +3,8 @@
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
5
 
6
- // src/core/done.ts
7
- import { readFile as readFile2, writeFile } from "fs/promises";
6
+ // src/core/archive.ts
7
+ import { readFile as readFile2, rename } from "fs/promises";
8
8
  import { join as join3 } from "path";
9
9
 
10
10
  // src/core/fs.ts
@@ -39,9 +39,11 @@ import { join, relative } from "path";
39
39
  var SDUCK_HOME_DIR = ".sduck";
40
40
  var SDUCK_ASSETS_DIR = "sduck-assets";
41
41
  var SDUCK_WORKSPACE_DIR = "sduck-workspace";
42
+ var SDUCK_ARCHIVE_DIR = "sduck-archive";
42
43
  var PROJECT_SDUCK_HOME_RELATIVE_PATH = SDUCK_HOME_DIR;
43
44
  var PROJECT_SDUCK_ASSETS_RELATIVE_PATH = join(SDUCK_HOME_DIR, SDUCK_ASSETS_DIR);
44
45
  var PROJECT_SDUCK_WORKSPACE_RELATIVE_PATH = join(SDUCK_HOME_DIR, SDUCK_WORKSPACE_DIR);
46
+ var PROJECT_SDUCK_ARCHIVE_RELATIVE_PATH = join(SDUCK_HOME_DIR, SDUCK_ARCHIVE_DIR);
45
47
  function getProjectSduckHomePath(projectRoot) {
46
48
  return join(projectRoot, PROJECT_SDUCK_HOME_RELATIVE_PATH);
47
49
  }
@@ -51,6 +53,9 @@ function getProjectSduckAssetsPath(projectRoot) {
51
53
  function getProjectSduckWorkspacePath(projectRoot) {
52
54
  return join(projectRoot, PROJECT_SDUCK_WORKSPACE_RELATIVE_PATH);
53
55
  }
56
+ function getProjectSduckArchivePath(projectRoot) {
57
+ return join(projectRoot, PROJECT_SDUCK_ARCHIVE_RELATIVE_PATH);
58
+ }
54
59
  function getProjectRelativeSduckAssetPath(...segments) {
55
60
  return join(PROJECT_SDUCK_ASSETS_RELATIVE_PATH, ...segments);
56
61
  }
@@ -141,6 +146,128 @@ async function findActiveTask(projectRoot) {
141
146
  return null;
142
147
  }
143
148
 
149
+ // src/core/archive.ts
150
+ function extractCompletedAt(metaContent) {
151
+ const match = /^completed_at:\s+(.+)$/m.exec(metaContent);
152
+ const value = match?.[1]?.trim();
153
+ if (value === void 0 || value === "null") {
154
+ return null;
155
+ }
156
+ return value;
157
+ }
158
+ function deriveArchiveMonth(completedAt) {
159
+ return completedAt.slice(0, 7);
160
+ }
161
+ function filterArchiveCandidates(tasks) {
162
+ return tasks.filter((task) => task.status === "DONE");
163
+ }
164
+ async function isAlreadyArchived(archivePath, taskDirName) {
165
+ const targetPath = join3(archivePath, taskDirName);
166
+ return await getFsEntryKind(targetPath) === "directory";
167
+ }
168
+ async function loadArchiveTargets(projectRoot, input) {
169
+ const tasks = await listWorkspaceTasks(projectRoot);
170
+ const doneTasks = filterArchiveCandidates(tasks);
171
+ const targets = [];
172
+ for (const task of doneTasks) {
173
+ const metaPath = join3(projectRoot, task.path, "meta.yml");
174
+ const metaContent = await readFile2(metaPath, "utf8");
175
+ const completedAt = extractCompletedAt(metaContent);
176
+ if (completedAt === null) {
177
+ continue;
178
+ }
179
+ targets.push({
180
+ completedAt,
181
+ id: task.id,
182
+ month: deriveArchiveMonth(completedAt),
183
+ path: task.path,
184
+ slug: task.slug
185
+ });
186
+ }
187
+ targets.sort((a, b) => a.completedAt.localeCompare(b.completedAt));
188
+ const keep = input.keep ?? 0;
189
+ if (keep > 0 && targets.length > keep) {
190
+ return targets.slice(0, targets.length - keep);
191
+ }
192
+ return targets;
193
+ }
194
+ async function runArchiveWorkflow(projectRoot, targets) {
195
+ const archiveRoot = getProjectSduckArchivePath(projectRoot);
196
+ const archived = [];
197
+ const skipped = [];
198
+ for (const target of targets) {
199
+ const monthDir = join3(archiveRoot, target.month);
200
+ await ensureDirectory(monthDir);
201
+ const segments = target.path.split("/");
202
+ const taskDirName = segments.at(-1) ?? target.id;
203
+ if (await isAlreadyArchived(monthDir, taskDirName)) {
204
+ skipped.push({ reason: "already archived", taskId: target.id });
205
+ continue;
206
+ }
207
+ const sourcePath = join3(projectRoot, target.path);
208
+ const destPath = join3(monthDir, taskDirName);
209
+ await rename(sourcePath, destPath);
210
+ archived.push({ month: target.month, taskId: target.id });
211
+ }
212
+ return { archived, skipped };
213
+ }
214
+
215
+ // src/commands/archive.ts
216
+ function padCell(value, width) {
217
+ return value.padEnd(width, " ");
218
+ }
219
+ function buildResultTable(result) {
220
+ const rows = [
221
+ ...result.archived.map((row) => ({
222
+ month: row.month,
223
+ result: "archived",
224
+ task: row.taskId
225
+ })),
226
+ ...result.skipped.map((row) => ({
227
+ month: "",
228
+ result: "skipped",
229
+ task: row.taskId
230
+ }))
231
+ ];
232
+ const resultWidth = Math.max("Result".length, ...rows.map((row) => row.result.length));
233
+ const taskWidth = Math.max("Task".length, ...rows.map((row) => row.task.length));
234
+ const monthWidth = Math.max("Month".length, ...rows.map((row) => row.month.length));
235
+ const border = `+-${"-".repeat(resultWidth)}-+-${"-".repeat(taskWidth)}-+-${"-".repeat(monthWidth)}-+`;
236
+ const header = `| ${padCell("Result", resultWidth)} | ${padCell("Task", taskWidth)} | ${padCell("Month", monthWidth)} |`;
237
+ const body = rows.map(
238
+ (row) => `| ${padCell(row.result, resultWidth)} | ${padCell(row.task, taskWidth)} | ${padCell(row.month, monthWidth)} |`
239
+ );
240
+ return [border, header, border, ...body, border].join("\n");
241
+ }
242
+ async function runArchiveCommand(input, projectRoot) {
243
+ try {
244
+ const targets = await loadArchiveTargets(projectRoot, input);
245
+ if (targets.length === 0) {
246
+ return {
247
+ exitCode: 0,
248
+ stderr: "",
249
+ stdout: "\uC544\uCE74\uC774\uBE0C \uB300\uC0C1\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."
250
+ };
251
+ }
252
+ const result = await runArchiveWorkflow(projectRoot, targets);
253
+ return {
254
+ exitCode: 0,
255
+ stderr: "",
256
+ stdout: buildResultTable(result)
257
+ };
258
+ } catch (error) {
259
+ return {
260
+ exitCode: 1,
261
+ stderr: error instanceof Error ? error.message : "Unknown archive failure.",
262
+ stdout: ""
263
+ };
264
+ }
265
+ }
266
+
267
+ // src/core/done.ts
268
+ import { readFile as readFile3, writeFile } from "fs/promises";
269
+ import { join as join4 } from "path";
270
+
144
271
  // src/utils/utc-date.ts
145
272
  function pad2(value) {
146
273
  return String(value).padStart(2, "0");
@@ -243,11 +370,11 @@ function validateDoneTarget(task) {
243
370
  }
244
371
  }
245
372
  async function loadTaskEvalCriteria(projectRoot) {
246
- const taskEvalPath = join3(projectRoot, TASK_EVAL_ASSET_PATH);
373
+ const taskEvalPath = join4(projectRoot, TASK_EVAL_ASSET_PATH);
247
374
  if (await getFsEntryKind(taskEvalPath) !== "file") {
248
375
  throw new Error(`Missing task evaluation asset at ${TASK_EVAL_ASSET_PATH}.`);
249
376
  }
250
- const taskEvalContent = await readFile2(taskEvalPath, "utf8");
377
+ const taskEvalContent = await readFile3(taskEvalPath, "utf8");
251
378
  const labels = extractTaskEvalCriteriaLabels(taskEvalContent);
252
379
  if (labels.length === 0) {
253
380
  throw new Error(`Task evaluation asset has no criteria labels: ${TASK_EVAL_ASSET_PATH}.`);
@@ -256,17 +383,17 @@ async function loadTaskEvalCriteria(projectRoot) {
256
383
  }
257
384
  async function completeTask(projectRoot, task, completedAt, taskEvalCriteria) {
258
385
  validateDoneTarget(task);
259
- const metaPath = join3(projectRoot, task.path, "meta.yml");
260
- const specPath = join3(projectRoot, task.path, "spec.md");
386
+ const metaPath = join4(projectRoot, task.path, "meta.yml");
387
+ const specPath = join4(projectRoot, task.path, "spec.md");
261
388
  if (await getFsEntryKind(metaPath) !== "file") {
262
389
  throw new Error(`Missing meta.yml for task ${task.id}.`);
263
390
  }
264
391
  if (await getFsEntryKind(specPath) !== "file") {
265
392
  throw new Error(`Missing spec.md for task ${task.id}.`);
266
393
  }
267
- const metaContent = await readFile2(metaPath, "utf8");
394
+ const metaContent = await readFile3(metaPath, "utf8");
268
395
  validateDoneMetaContent(metaContent);
269
- const specContent = await readFile2(specPath, "utf8");
396
+ const specContent = await readFile3(specPath, "utf8");
270
397
  const uncheckedItems = extractUncheckedChecklistItems(specContent);
271
398
  if (uncheckedItems.length > 0) {
272
399
  throw new Error(`Spec checklist is incomplete: ${uncheckedItems.join("; ")}`);
@@ -332,10 +459,10 @@ function createTaskCompletedAt(date = /* @__PURE__ */ new Date()) {
332
459
  }
333
460
 
334
461
  // src/commands/done.ts
335
- function padCell(value, width) {
462
+ function padCell2(value, width) {
336
463
  return value.padEnd(width, " ");
337
464
  }
338
- function buildResultTable(result) {
465
+ function buildResultTable2(result) {
339
466
  const rows = [
340
467
  ...result.succeeded.map((row) => ({
341
468
  note: row.note,
@@ -352,9 +479,9 @@ function buildResultTable(result) {
352
479
  const taskWidth = Math.max("Task".length, ...rows.map((row) => row.task.length));
353
480
  const noteWidth = Math.max("Note".length, ...rows.map((row) => row.note.length));
354
481
  const border = `+-${"-".repeat(resultWidth)}-+-${"-".repeat(taskWidth)}-+-${"-".repeat(noteWidth)}-+`;
355
- const header = `| ${padCell("Result", resultWidth)} | ${padCell("Task", taskWidth)} | ${padCell("Note", noteWidth)} |`;
482
+ const header = `| ${padCell2("Result", resultWidth)} | ${padCell2("Task", taskWidth)} | ${padCell2("Note", noteWidth)} |`;
356
483
  const body = rows.map(
357
- (row) => `| ${padCell(row.result, resultWidth)} | ${padCell(row.task, taskWidth)} | ${padCell(row.note, noteWidth)} |`
484
+ (row) => `| ${padCell2(row.result, resultWidth)} | ${padCell2(row.task, taskWidth)} | ${padCell2(row.note, noteWidth)} |`
358
485
  );
359
486
  return [border, header, border, ...body, border].join("\n");
360
487
  }
@@ -372,7 +499,7 @@ function formatFailureDetails(failed) {
372
499
  return lines;
373
500
  }
374
501
  function formatSuccess(result) {
375
- const lines = [buildResultTable(result)];
502
+ const lines = [buildResultTable2(result)];
376
503
  if (result.succeeded.length > 0) {
377
504
  const criteriaLabels = result.succeeded[0]?.taskEvalCriteria ?? [];
378
505
  lines.push("", "\uC0C1\uD0DC: DONE");
@@ -413,10 +540,10 @@ import { confirm } from "@inquirer/prompts";
413
540
 
414
541
  // src/core/fast-track.ts
415
542
  import { writeFile as writeFile5 } from "fs/promises";
416
- import { join as join8 } from "path";
543
+ import { join as join9 } from "path";
417
544
 
418
545
  // src/core/assets.ts
419
- import { dirname, join as join4 } from "path";
546
+ import { dirname, join as join5 } from "path";
420
547
  import { fileURLToPath } from "url";
421
548
  var SUPPORTED_TASK_TYPES = [
422
549
  "build",
@@ -426,16 +553,16 @@ var SUPPORTED_TASK_TYPES = [
426
553
  "chore"
427
554
  ];
428
555
  var EVAL_ASSET_RELATIVE_PATHS = {
429
- task: join4("eval", "task.yml"),
430
- plan: join4("eval", "plan.yml"),
431
- spec: join4("eval", "spec.yml")
556
+ task: join5("eval", "task.yml"),
557
+ plan: join5("eval", "plan.yml"),
558
+ spec: join5("eval", "spec.yml")
432
559
  };
433
560
  var SPEC_TEMPLATE_RELATIVE_PATHS = {
434
- build: join4("types", "build.md"),
435
- feature: join4("types", "feature.md"),
436
- fix: join4("types", "fix.md"),
437
- refactor: join4("types", "refactor.md"),
438
- chore: join4("types", "chore.md")
561
+ build: join5("types", "build.md"),
562
+ feature: join5("types", "feature.md"),
563
+ fix: join5("types", "fix.md"),
564
+ refactor: join5("types", "refactor.md"),
565
+ chore: join5("types", "chore.md")
439
566
  };
440
567
  var INIT_ASSET_RELATIVE_PATHS = [
441
568
  EVAL_ASSET_RELATIVE_PATHS.spec,
@@ -446,8 +573,8 @@ var INIT_ASSET_RELATIVE_PATHS = [
446
573
  async function getBundledAssetsRoot() {
447
574
  const currentDirectoryPath = dirname(fileURLToPath(import.meta.url));
448
575
  const candidatePaths = [
449
- join4(currentDirectoryPath, "..", "..", ".sduck", "sduck-assets"),
450
- join4(currentDirectoryPath, "..", ".sduck", "sduck-assets")
576
+ join5(currentDirectoryPath, "..", "..", ".sduck", "sduck-assets"),
577
+ join5(currentDirectoryPath, "..", ".sduck", "sduck-assets")
451
578
  ];
452
579
  for (const candidatePath of candidatePaths) {
453
580
  if (await getFsEntryKind(candidatePath) === "directory") {
@@ -464,8 +591,8 @@ function resolveSpecTemplateRelativePath(type) {
464
591
  }
465
592
 
466
593
  // src/core/plan-approve.ts
467
- import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
468
- import { join as join5 } from "path";
594
+ import { readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
595
+ import { join as join6 } from "path";
469
596
  function filterPlanApprovalCandidates(tasks) {
470
597
  return tasks.filter((task) => task.status === "SPEC_APPROVED");
471
598
  }
@@ -507,8 +634,8 @@ async function approvePlans(projectRoot, tasks, approvedAt) {
507
634
  });
508
635
  continue;
509
636
  }
510
- const metaPath = join5(projectRoot, task.path, "meta.yml");
511
- const planPath = join5(projectRoot, task.path, "plan.md");
637
+ const metaPath = join6(projectRoot, task.path, "meta.yml");
638
+ const planPath = join6(projectRoot, task.path, "plan.md");
512
639
  if (await getFsEntryKind(metaPath) !== "file") {
513
640
  failed.push({ note: "missing meta.yml", taskId: task.id });
514
641
  continue;
@@ -517,14 +644,14 @@ async function approvePlans(projectRoot, tasks, approvedAt) {
517
644
  failed.push({ note: "missing plan.md", taskId: task.id });
518
645
  continue;
519
646
  }
520
- const planContent = await readFile3(planPath, "utf8");
647
+ const planContent = await readFile4(planPath, "utf8");
521
648
  const totalSteps = countPlanSteps(planContent);
522
649
  if (totalSteps === 0) {
523
650
  failed.push({ note: "missing valid Step headers", taskId: task.id });
524
651
  continue;
525
652
  }
526
653
  const updatedMeta = updatePlanApprovalBlock(
527
- await readFile3(metaPath, "utf8"),
654
+ await readFile4(metaPath, "utf8"),
528
655
  approvedAt,
529
656
  totalSteps
530
657
  );
@@ -547,8 +674,8 @@ function createPlanApprovedAt(date = /* @__PURE__ */ new Date()) {
547
674
  }
548
675
 
549
676
  // src/core/spec-approve.ts
550
- import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
551
- import { join as join6 } from "path";
677
+ import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
678
+ import { join as join7 } from "path";
552
679
  function filterApprovalCandidates(tasks) {
553
680
  return tasks.filter((task) => task.status === "PENDING_SPEC_APPROVAL");
554
681
  }
@@ -583,11 +710,11 @@ function updateSpecApprovalBlock(metaContent, approvedAt) {
583
710
  async function approveSpecs(projectRoot, tasks, approvedAt) {
584
711
  validateSpecApprovalTargets(tasks);
585
712
  for (const task of tasks) {
586
- const metaPath = join6(projectRoot, task.path, "meta.yml");
713
+ const metaPath = join7(projectRoot, task.path, "meta.yml");
587
714
  if (await getFsEntryKind(metaPath) !== "file") {
588
715
  throw new Error(`Missing meta.yml for task ${task.id}.`);
589
716
  }
590
- const updatedContent = updateSpecApprovalBlock(await readFile4(metaPath, "utf8"), approvedAt);
717
+ const updatedContent = updateSpecApprovalBlock(await readFile5(metaPath, "utf8"), approvedAt);
591
718
  await writeFile3(metaPath, updatedContent, "utf8");
592
719
  }
593
720
  return {
@@ -605,8 +732,8 @@ function createSpecApprovedAt(date = /* @__PURE__ */ new Date()) {
605
732
  }
606
733
 
607
734
  // src/core/start.ts
608
- import { mkdir as mkdir2, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
609
- import { join as join7 } from "path";
735
+ import { mkdir as mkdir2, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
736
+ import { join as join8 } from "path";
610
737
  function normalizeSlug(input) {
611
738
  return input.trim().toLowerCase().replace(/[_\s]+/g, "-").replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
612
739
  }
@@ -653,7 +780,7 @@ function renderInitialMeta(input) {
653
780
  }
654
781
  async function resolveSpecTemplatePath(type) {
655
782
  const assetsRoot = await getBundledAssetsRoot();
656
- return join7(assetsRoot, resolveSpecTemplateRelativePath(type));
783
+ return join8(assetsRoot, resolveSpecTemplateRelativePath(type));
657
784
  }
658
785
  function applyTemplateDefaults(template, type, slug, currentDate) {
659
786
  const displayName = slug.replace(/-/g, " ");
@@ -673,7 +800,7 @@ async function startTask(rawType, rawSlug, projectRoot, currentDate = /* @__PURE
673
800
  }
674
801
  const workspaceId = createWorkspaceId(currentDate, rawType, slug);
675
802
  const workspacePath = getProjectRelativeSduckWorkspacePath(workspaceId);
676
- const absoluteWorkspacePath = join7(projectRoot, workspacePath);
803
+ const absoluteWorkspacePath = join8(projectRoot, workspacePath);
677
804
  if (await getFsEntryKind(absoluteWorkspacePath) !== "missing") {
678
805
  throw new Error(`Workspace already exists: ${workspacePath}`);
679
806
  }
@@ -684,7 +811,7 @@ async function startTask(rawType, rawSlug, projectRoot, currentDate = /* @__PURE
684
811
  if (await getFsEntryKind(templatePath) !== "file") {
685
812
  throw new Error(`Missing spec template for type '${rawType}' at ${templatePath}`);
686
813
  }
687
- const specTemplate = await readFile5(templatePath, "utf8");
814
+ const specTemplate = await readFile6(templatePath, "utf8");
688
815
  const specContent = applyTemplateDefaults(specTemplate, rawType, slug, currentDate);
689
816
  const metaContent = renderInitialMeta({
690
817
  createdAt: formatUtcTimestamp(currentDate),
@@ -692,9 +819,9 @@ async function startTask(rawType, rawSlug, projectRoot, currentDate = /* @__PURE
692
819
  slug,
693
820
  type: rawType
694
821
  });
695
- await writeFile4(join7(absoluteWorkspacePath, "meta.yml"), metaContent, "utf8");
696
- await writeFile4(join7(absoluteWorkspacePath, "spec.md"), specContent, "utf8");
697
- await writeFile4(join7(absoluteWorkspacePath, "plan.md"), "", "utf8");
822
+ await writeFile4(join8(absoluteWorkspacePath, "meta.yml"), metaContent, "utf8");
823
+ await writeFile4(join8(absoluteWorkspacePath, "spec.md"), specContent, "utf8");
824
+ await writeFile4(join8(absoluteWorkspacePath, "plan.md"), "", "utf8");
698
825
  return {
699
826
  workspaceId,
700
827
  workspacePath,
@@ -761,9 +888,9 @@ async function createFastTrackTask(input, projectRoot) {
761
888
  throw new Error(`Unsupported type: ${input.type}`);
762
889
  }
763
890
  const startedTask = await startTask(input.type, input.slug, projectRoot);
764
- const taskPath = join8(projectRoot, startedTask.workspacePath);
765
- const specPath = join8(taskPath, "spec.md");
766
- const planPath = join8(taskPath, "plan.md");
891
+ const taskPath = join9(projectRoot, startedTask.workspacePath);
892
+ const specPath = join9(taskPath, "spec.md");
893
+ const planPath = join9(taskPath, "plan.md");
767
894
  if (await getFsEntryKind(specPath) !== "file") {
768
895
  throw new Error(`Missing spec.md for fast-track task ${startedTask.workspaceId}.`);
769
896
  }
@@ -900,9 +1027,15 @@ async function runFastTrackCommand(input, projectRoot) {
900
1027
  import { checkbox } from "@inquirer/prompts";
901
1028
 
902
1029
  // src/core/agent-rules.ts
903
- import { readFile as readFile6 } from "fs/promises";
904
- import { dirname as dirname2, join as join9 } from "path";
1030
+ import { readFile as readFile7 } from "fs/promises";
1031
+ import { dirname as dirname2, join as join10 } from "path";
905
1032
  import { fileURLToPath as fileURLToPath2 } from "url";
1033
+ var CLAUDE_CODE_HOOK_SETTINGS_PATH = join10(".claude", "settings.json");
1034
+ var CLAUDE_CODE_HOOK_SCRIPT_PATH = join10(".claude", "hooks", "sdd-guard.sh");
1035
+ var CLAUDE_CODE_HOOK_SOURCE_PATH = join10("hooks", "sdd-guard.sh");
1036
+ function needsClaudeCodeHook(agents) {
1037
+ return agents.includes("claude-code");
1038
+ }
906
1039
  var SDD_RULES_BEGIN = "<!-- sduck:begin -->";
907
1040
  var SDD_RULES_END = "<!-- sduck:end -->";
908
1041
  var SUPPORTED_AGENTS = [
@@ -920,12 +1053,12 @@ var AGENT_RULE_TARGETS = [
920
1053
  { agentId: "gemini-cli", outputPath: "GEMINI.md", kind: "root-file" },
921
1054
  {
922
1055
  agentId: "cursor",
923
- outputPath: join9(".cursor", "rules", "sduck-core.mdc"),
1056
+ outputPath: join10(".cursor", "rules", "sduck-core.mdc"),
924
1057
  kind: "managed-file"
925
1058
  },
926
1059
  {
927
1060
  agentId: "antigravity",
928
- outputPath: join9(".agents", "rules", "sduck-core.md"),
1061
+ outputPath: join10(".agents", "rules", "sduck-core.md"),
929
1062
  kind: "managed-file"
930
1063
  }
931
1064
  ];
@@ -986,8 +1119,8 @@ function renderManagedBlock(lines) {
986
1119
  async function getAgentRulesAssetRoot() {
987
1120
  const currentDirectoryPath = dirname2(fileURLToPath2(import.meta.url));
988
1121
  const candidatePaths = [
989
- join9(currentDirectoryPath, "..", "..", ".sduck", "sduck-assets", "agent-rules"),
990
- join9(currentDirectoryPath, "..", ".sduck", "sduck-assets", "agent-rules")
1122
+ join10(currentDirectoryPath, "..", "..", ".sduck", "sduck-assets", "agent-rules"),
1123
+ join10(currentDirectoryPath, "..", ".sduck", "sduck-assets", "agent-rules")
991
1124
  ];
992
1125
  for (const candidatePath of candidatePaths) {
993
1126
  if (await getFsEntryKind(candidatePath) === "directory") {
@@ -997,7 +1130,7 @@ async function getAgentRulesAssetRoot() {
997
1130
  throw new Error("Unable to locate bundled sduck agent rule assets.");
998
1131
  }
999
1132
  async function readAssetFile(assetRoot, fileName) {
1000
- return await readFile6(join9(assetRoot, fileName), "utf8");
1133
+ return await readFile7(join10(assetRoot, fileName), "utf8");
1001
1134
  }
1002
1135
  function buildRootFileLines(agentIds, agentSpecificContent) {
1003
1136
  const labels = SUPPORTED_AGENTS.filter((agent) => agentIds.includes(agent.id)).map(
@@ -1059,8 +1192,8 @@ function planAgentRuleActions(mode, targets, existingEntries, existingContents)
1059
1192
  }
1060
1193
 
1061
1194
  // src/core/init.ts
1062
- import { mkdir as mkdir3, readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
1063
- import { dirname as dirname3, join as join10 } from "path";
1195
+ import { chmod, mkdir as mkdir3, readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
1196
+ import { dirname as dirname3, join as join11 } from "path";
1064
1197
  var ASSET_TEMPLATE_DEFINITIONS = [
1065
1198
  {
1066
1199
  key: "eval-spec",
@@ -1173,7 +1306,7 @@ async function collectExistingEntries(projectRoot) {
1173
1306
  for (const definition of ASSET_TEMPLATE_DEFINITIONS) {
1174
1307
  existingEntries.set(
1175
1308
  definition.relativePath,
1176
- await getFsEntryKind(join10(projectRoot, definition.relativePath))
1309
+ await getFsEntryKind(join11(projectRoot, definition.relativePath))
1177
1310
  );
1178
1311
  }
1179
1312
  return existingEntries;
@@ -1181,9 +1314,9 @@ async function collectExistingEntries(projectRoot) {
1181
1314
  async function collectExistingFileContents(projectRoot, targets) {
1182
1315
  const contents = /* @__PURE__ */ new Map();
1183
1316
  for (const target of targets) {
1184
- const targetPath = join10(projectRoot, target.outputPath);
1317
+ const targetPath = join11(projectRoot, target.outputPath);
1185
1318
  if (await getFsEntryKind(targetPath) === "file") {
1186
- contents.set(target.outputPath, await readFile7(targetPath, "utf8"));
1319
+ contents.set(target.outputPath, await readFile8(targetPath, "utf8"));
1187
1320
  }
1188
1321
  }
1189
1322
  return contents;
@@ -1246,8 +1379,8 @@ async function initProject(options, projectRoot) {
1246
1379
  continue;
1247
1380
  }
1248
1381
  const definition = ASSET_TEMPLATE_MAP[action.key];
1249
- const sourcePath = join10(assetSourceRoot, toBundledAssetRelativePath(definition.relativePath));
1250
- const targetPath = join10(projectRoot, definition.relativePath);
1382
+ const sourcePath = join11(assetSourceRoot, toBundledAssetRelativePath(definition.relativePath));
1383
+ const targetPath = join11(projectRoot, definition.relativePath);
1251
1384
  await ensureReadableFile(sourcePath);
1252
1385
  await mkdir3(dirname3(targetPath), { recursive: true });
1253
1386
  await copyFileIntoPlace(sourcePath, targetPath);
@@ -1257,7 +1390,7 @@ async function initProject(options, projectRoot) {
1257
1390
  for (const target of agentTargets) {
1258
1391
  agentEntryKinds.set(
1259
1392
  target.outputPath,
1260
- await getFsEntryKind(join10(projectRoot, target.outputPath))
1393
+ await getFsEntryKind(join11(projectRoot, target.outputPath))
1261
1394
  );
1262
1395
  }
1263
1396
  const existingContents = await collectExistingFileContents(projectRoot, agentTargets);
@@ -1269,6 +1402,9 @@ async function initProject(options, projectRoot) {
1269
1402
  summary,
1270
1403
  resolvedOptions.agents
1271
1404
  );
1405
+ if (needsClaudeCodeHook(resolvedOptions.agents)) {
1406
+ await installClaudeCodeHook(projectRoot, summary);
1407
+ }
1272
1408
  return {
1273
1409
  mode,
1274
1410
  agents: resolvedOptions.agents,
@@ -1276,9 +1412,54 @@ async function initProject(options, projectRoot) {
1276
1412
  didChange: summary.created.length > 0 || summary.prepended.length > 0 || summary.overwritten.length > 0
1277
1413
  };
1278
1414
  }
1415
+ async function installClaudeCodeHook(projectRoot, summary) {
1416
+ const assetRoot = await getBundledAssetsRoot();
1417
+ const hookSourcePath = join11(assetRoot, "agent-rules", CLAUDE_CODE_HOOK_SOURCE_PATH);
1418
+ const hookTargetPath = join11(projectRoot, CLAUDE_CODE_HOOK_SCRIPT_PATH);
1419
+ const settingsPath = join11(projectRoot, CLAUDE_CODE_HOOK_SETTINGS_PATH);
1420
+ await mkdir3(dirname3(hookTargetPath), { recursive: true });
1421
+ await copyFileIntoPlace(hookSourcePath, hookTargetPath);
1422
+ await chmod(hookTargetPath, 493);
1423
+ summary.created.push(CLAUDE_CODE_HOOK_SCRIPT_PATH);
1424
+ summary.rows.push({ path: CLAUDE_CODE_HOOK_SCRIPT_PATH, status: "created" });
1425
+ const hookConfig = {
1426
+ hooks: {
1427
+ PreToolUse: [
1428
+ {
1429
+ matcher: "Edit|Write",
1430
+ hooks: [
1431
+ {
1432
+ type: "command",
1433
+ command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/sdd-guard.sh'
1434
+ }
1435
+ ]
1436
+ }
1437
+ ]
1438
+ }
1439
+ };
1440
+ if (await getFsEntryKind(settingsPath) === "file") {
1441
+ const existingContent = await readFile8(settingsPath, "utf8");
1442
+ try {
1443
+ const existing = JSON.parse(existingContent);
1444
+ existing["hooks"] = hookConfig.hooks;
1445
+ await writeFile6(settingsPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
1446
+ summary.overwritten.push(CLAUDE_CODE_HOOK_SETTINGS_PATH);
1447
+ summary.rows.push({ path: CLAUDE_CODE_HOOK_SETTINGS_PATH, status: "overwritten" });
1448
+ } catch {
1449
+ await writeFile6(settingsPath, JSON.stringify(hookConfig, null, 2) + "\n", "utf8");
1450
+ summary.overwritten.push(CLAUDE_CODE_HOOK_SETTINGS_PATH);
1451
+ summary.rows.push({ path: CLAUDE_CODE_HOOK_SETTINGS_PATH, status: "overwritten" });
1452
+ }
1453
+ } else {
1454
+ await mkdir3(dirname3(settingsPath), { recursive: true });
1455
+ await writeFile6(settingsPath, JSON.stringify(hookConfig, null, 2) + "\n", "utf8");
1456
+ summary.created.push(CLAUDE_CODE_HOOK_SETTINGS_PATH);
1457
+ summary.rows.push({ path: CLAUDE_CODE_HOOK_SETTINGS_PATH, status: "created" });
1458
+ }
1459
+ }
1279
1460
  async function applyAgentRuleActions(projectRoot, actions, existingContents, summary, selectedAgents) {
1280
1461
  for (const action of actions) {
1281
- const targetPath = join10(projectRoot, action.outputPath);
1462
+ const targetPath = join11(projectRoot, action.outputPath);
1282
1463
  const content = await renderAgentRuleContent(action, selectedAgents);
1283
1464
  if (action.mergeMode === "create") {
1284
1465
  await mkdir3(dirname3(targetPath), { recursive: true });
@@ -1323,16 +1504,16 @@ async function applyAgentRuleActions(projectRoot, actions, existingContents, sum
1323
1504
  var AGENT_PROMPT_MESSAGE = "Select AI agents to generate repository rule files for";
1324
1505
  var AGENT_PROMPT_INSTRUCTIONS = "Use space to toggle agents, arrow keys to move, and enter to submit.";
1325
1506
  var AGENT_PROMPT_REQUIRED_MESSAGE = "Select at least one agent. Use space to toggle and enter to submit.";
1326
- function padCell2(value, width) {
1507
+ function padCell3(value, width) {
1327
1508
  return value.padEnd(width, " ");
1328
1509
  }
1329
1510
  function buildSummaryTable(rows) {
1330
1511
  const statusWidth = Math.max("Status".length, ...rows.map((row) => row.status.length));
1331
1512
  const pathWidth = Math.max("Path".length, ...rows.map((row) => row.path.length));
1332
1513
  const border = `+-${"-".repeat(statusWidth)}-+-${"-".repeat(pathWidth)}-+`;
1333
- const header = `| ${padCell2("Status", statusWidth)} | ${padCell2("Path", pathWidth)} |`;
1514
+ const header = `| ${padCell3("Status", statusWidth)} | ${padCell3("Path", pathWidth)} |`;
1334
1515
  const body = rows.map(
1335
- (row) => `| ${padCell2(row.status, statusWidth)} | ${padCell2(row.path, pathWidth)} |`
1516
+ (row) => `| ${padCell3(row.status, statusWidth)} | ${padCell3(row.path, pathWidth)} |`
1336
1517
  );
1337
1518
  return [border, header, border, ...body, border].join("\n");
1338
1519
  }
@@ -1399,10 +1580,10 @@ async function runInitCommand(options, projectRoot) {
1399
1580
 
1400
1581
  // src/commands/plan-approve.ts
1401
1582
  import { checkbox as checkbox2 } from "@inquirer/prompts";
1402
- function padCell3(value, width) {
1583
+ function padCell4(value, width) {
1403
1584
  return value.padEnd(width, " ");
1404
1585
  }
1405
- function buildResultTable2(result) {
1586
+ function buildResultTable3(result) {
1406
1587
  const rows = [
1407
1588
  ...result.succeeded.map((row) => ({
1408
1589
  note: row.note,
@@ -1422,9 +1603,9 @@ function buildResultTable2(result) {
1422
1603
  const stepsWidth = Math.max("Steps".length, ...rows.map((row) => row.steps.length));
1423
1604
  const noteWidth = Math.max("Note".length, ...rows.map((row) => row.note.length));
1424
1605
  const border = `+-${"-".repeat(resultWidth)}-+-${"-".repeat(taskWidth)}-+-${"-".repeat(stepsWidth)}-+-${"-".repeat(noteWidth)}-+`;
1425
- const header = `| ${padCell3("Result", resultWidth)} | ${padCell3("Task", taskWidth)} | ${padCell3("Steps", stepsWidth)} | ${padCell3("Note", noteWidth)} |`;
1606
+ const header = `| ${padCell4("Result", resultWidth)} | ${padCell4("Task", taskWidth)} | ${padCell4("Steps", stepsWidth)} | ${padCell4("Note", noteWidth)} |`;
1426
1607
  const body = rows.map(
1427
- (row) => `| ${padCell3(row.result, resultWidth)} | ${padCell3(row.task, taskWidth)} | ${padCell3(row.steps, stepsWidth)} | ${padCell3(row.note, noteWidth)} |`
1608
+ (row) => `| ${padCell4(row.result, resultWidth)} | ${padCell4(row.task, taskWidth)} | ${padCell4(row.steps, stepsWidth)} | ${padCell4(row.note, noteWidth)} |`
1428
1609
  );
1429
1610
  return [border, header, border, ...body, border].join("\n");
1430
1611
  }
@@ -1432,7 +1613,7 @@ function formatTaskLabel(task) {
1432
1613
  return `${task.id} (${task.status})`;
1433
1614
  }
1434
1615
  function formatSuccess2(result) {
1435
- const lines = [buildResultTable2(result)];
1616
+ const lines = [buildResultTable3(result)];
1436
1617
  if (result.succeeded.length > 0) {
1437
1618
  lines.push("", "\uC0C1\uD0DC: IN_PROGRESS \u2192 \uC791\uC5C5\uC744 \uC2DC\uC791\uD569\uB2C8\uB2E4.");
1438
1619
  }
@@ -1467,7 +1648,7 @@ async function runPlanApproveCommand(input, projectRoot) {
1467
1648
  if (result.succeeded.length === 0) {
1468
1649
  return {
1469
1650
  exitCode: 1,
1470
- stderr: buildResultTable2(result),
1651
+ stderr: buildResultTable3(result),
1471
1652
  stdout: ""
1472
1653
  };
1473
1654
  }
@@ -1567,7 +1748,7 @@ async function runStartCommand(type, slug, projectRoot) {
1567
1748
  // package.json
1568
1749
  var package_default = {
1569
1750
  name: "@sduck/sduck-cli",
1570
- version: "0.1.3",
1751
+ version: "0.1.5",
1571
1752
  description: "Spec-Driven Development CLI bootstrap",
1572
1753
  type: "module",
1573
1754
  bin: {
@@ -1719,6 +1900,19 @@ program.command("done [target]").description("Complete an in-progress task after
1719
1900
  process.exitCode = result.exitCode;
1720
1901
  }
1721
1902
  });
1903
+ program.command("archive").description("Archive completed tasks into monthly directories").option("--keep <n>", "Keep the N most recently completed tasks in workspace", "0").action(async (options) => {
1904
+ const keep = Number(options.keep);
1905
+ const result = await runArchiveCommand({ keep }, process.cwd());
1906
+ if (result.stdout !== "") {
1907
+ console.log(result.stdout);
1908
+ }
1909
+ if (result.stderr !== "") {
1910
+ console.error(result.stderr);
1911
+ }
1912
+ if (result.exitCode !== 0) {
1913
+ process.exitCode = result.exitCode;
1914
+ }
1915
+ });
1722
1916
  program.command("roadmap").description("Show the current bootstrap status").action(() => {
1723
1917
  console.log(PLACEHOLDER_MESSAGE);
1724
1918
  });