reffy-cli 0.7.0 → 1.0.0

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
@@ -4,9 +4,16 @@ import { existsSync, promises as fs, statSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { renderDiagram } from "./diagram.js";
6
6
  import { runDoctor } from "./doctor.js";
7
- import { DEFAULT_REFS_DIRNAME, looksLikeRefsDir, resolveRefsDirName } from "./refs-paths.js";
7
+ import { archivePlanningChange } from "./plan-archive.js";
8
+ import { createPlanScaffold } from "./plan.js";
9
+ import { DEFAULT_PLANNING_DIRNAME } from "./planning-paths.js";
10
+ import { listPlanningChanges, showPlanningChange, validatePlanningChange } from "./plan-runtime.js";
11
+ import { DEFAULT_REFS_DIRNAME, looksLikeRefsDir } from "./refs-paths.js";
12
+ import { prepareCanonicalPlanningLayout } from "./planning-workspace.js";
8
13
  import { ReferencesStore } from "./storage.js";
14
+ import { listSpecs, showSpec } from "./spec-runtime.js";
9
15
  import { summarizeArtifacts } from "./summarize.js";
16
+ import { prepareCanonicalWorkspace } from "./workspace.js";
10
17
  const require = createRequire(import.meta.url);
11
18
  const { version: packageVersion } = require("../package.json");
12
19
  const REFFY_ASCII = [
@@ -29,14 +36,34 @@ Always open \`@/${refsDirName}/AGENTS.md\` when the request:
29
36
  - Refers to "reffy", "references", "explore", or "context layer"
30
37
 
31
38
  Use \`@/${refsDirName}/AGENTS.md\` to learn:
32
- - Reffy workflow and artifact conventions
33
- - How Reffy and OpenSpec should be sequenced
39
+ - Reffy workflow for ideation, artifact indexing, and planning scaffolds
40
+ - How Reffy owns the runtime while preserving ReffySpec planning files
34
41
  - How to store and consume ideation context in \`${refsDirName}/\`
35
42
 
36
43
  Keep this managed block so \`reffy init\` can refresh the instructions.
37
44
 
38
45
  <!-- REFFY:END -->`;
39
46
  }
47
+ function buildReffySpecBlock() {
48
+ return `<!-- REFFYSPEC:START -->
49
+ # ReffySpec Instructions
50
+
51
+ These instructions are for AI assistants working in this project.
52
+
53
+ Always open \`@/${DEFAULT_PLANNING_DIRNAME}/AGENTS.md\` when the request:
54
+ - Mentions planning or proposals (words like proposal, spec, change, plan)
55
+ - Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
56
+ - Needs the authoritative planning/spec workflow for this repo
57
+
58
+ Use \`@/${DEFAULT_PLANNING_DIRNAME}/AGENTS.md\` to learn:
59
+ - How to create and apply ReffySpec change proposals
60
+ - ReffySpec format and conventions
61
+ - Project structure and planning guidelines
62
+
63
+ Keep this managed block so \`reffy init\` can refresh the instructions.
64
+
65
+ <!-- REFFYSPEC:END -->`;
66
+ }
40
67
  function buildReffyAgentsContent(refsDirName) {
41
68
  return `# Reffy Instructions
42
69
 
@@ -48,7 +75,7 @@ These instructions are for AI assistants working in this project.
48
75
  - If needed, read existing context in \`${refsDirName}/artifacts/\`.
49
76
  - Add/update exploratory artifacts and keep them concise.
50
77
  - Run \`reffy reindex\` and \`reffy validate\` after artifact changes.
51
- - After ideation approval, run \`reffy summarize --output json\` and pick only directly relevant artifacts for proposal citations.
78
+ - Use \`reffy summarize --output json\` and \`reffy plan create\` to turn artifact context into planning scaffolds.
52
79
 
53
80
  ## When To Use Reffy
54
81
 
@@ -70,17 +97,20 @@ You can skip Reffy when the request is:
70
97
  2. Add or update artifacts to capture exploratory context.
71
98
  3. Run \`reffy reindex\` to index newly added files into \`${refsDirName}/manifest.json\`.
72
99
  4. Run \`reffy validate\` to verify manifest contract compliance.
100
+ 5. Run \`reffy plan create\` to generate proposal/tasks/spec scaffolds from selected artifacts when planning is ready.
73
101
 
74
- ## Relationship To OpenSpec
102
+ ## Relationship To ReffySpec
75
103
 
76
- - Reffy is the ideation/context layer.
77
- - OpenSpec is the formal planning/spec layer.
78
- - After ideation stabilizes, hand off to OpenSpec by following \`@/openspec/AGENTS.md\`.
79
- - Do not duplicate full proposal/spec content in Reffy artifacts; summarize and link to OpenSpec outputs.
104
+ - Reffy owns ideation artifacts, manifest metadata, and native planning scaffolds.
105
+ - ReffySpec is the planning subsystem inside Reffy.
106
+ - The vendored fork at \`/.vendor/ReffySpec\` is reference-only for v1; first-party behavior lives in this repo.
107
+ - Reffy is the primary runtime authority for this project.
108
+ - ReffySpec files live under \`${DEFAULT_PLANNING_DIRNAME}/\` as the canonical planning layout.
109
+ - Do not duplicate full proposal/spec content in Reffy artifacts; generate and link planning outputs from them.
80
110
 
81
- ## OpenSpec Citation Rules
111
+ ## ReffySpec Citation Rules
82
112
 
83
- When an OpenSpec proposal is informed by Reffy artifacts:
113
+ When a ReffySpec proposal is informed by Reffy artifacts:
84
114
  - After ideation approval, run \`reffy summarize --output json\` to shortlist candidate artifacts.
85
115
  - Include a short "Reffy References" subsection in \`proposal.md\` (or design notes if more appropriate).
86
116
  - Cite only artifact filenames that directly informed the proposal's problem, scope, decisions, or constraints.
@@ -92,7 +122,7 @@ When an OpenSpec proposal is informed by Reffy artifacts:
92
122
 
93
123
  ### Reusable Proposal Snippet
94
124
 
95
- Use this in \`openspec/changes/<change-id>/proposal.md\`:
125
+ Use this in \`${DEFAULT_PLANNING_DIRNAME}/changes/<change-id>/proposal.md\`:
96
126
 
97
127
  \`\`\`md
98
128
  ## Reffy References
@@ -114,9 +144,59 @@ No Reffy references used.
114
144
  - Keep manifests machine-readable and schema-compliant (version 1).
115
145
  `;
116
146
  }
147
+ function buildReffySpecAgentsContent() {
148
+ return `# ReffySpec Instructions
149
+
150
+ These instructions are for AI assistants working in this project.
151
+
152
+ ## TL;DR Checklist
153
+
154
+ - Read the relevant current specs in \`${DEFAULT_PLANNING_DIRNAME}/specs/\` before changing behavior.
155
+ - Review active changes in \`${DEFAULT_PLANNING_DIRNAME}/changes/\` before drafting or implementing new work.
156
+ - Use ReffySpec change proposals for new capabilities, breaking changes, or architecture shifts.
157
+ - Keep current truth in \`${DEFAULT_PLANNING_DIRNAME}/specs/\` and proposed deltas in \`${DEFAULT_PLANNING_DIRNAME}/changes/\`.
158
+
159
+ ## ReffySpec Workflow
160
+
161
+ 1. Read \`${DEFAULT_PLANNING_DIRNAME}/project.md\` for project conventions.
162
+ 2. Inspect current specs in \`${DEFAULT_PLANNING_DIRNAME}/specs/\`.
163
+ 3. Inspect active changes in \`${DEFAULT_PLANNING_DIRNAME}/changes/\`.
164
+ 4. Draft or update proposal/design/tasks/spec files under \`${DEFAULT_PLANNING_DIRNAME}/changes/<change-id>/\`.
165
+ 5. Use native Reffy commands for routine planning workflow:
166
+ - \`reffy plan create\`
167
+ - \`reffy plan validate\`
168
+ - \`reffy plan list\`
169
+ - \`reffy plan show\`
170
+ - \`reffy plan archive\`
171
+ - \`reffy spec list\`
172
+ - \`reffy spec show\`
173
+
174
+ ## Directory Model
175
+
176
+ - \`${DEFAULT_PLANNING_DIRNAME}/changes/\` contains active proposed changes.
177
+ - \`${DEFAULT_PLANNING_DIRNAME}/changes/archive/\` contains historical archived changes.
178
+ - \`${DEFAULT_PLANNING_DIRNAME}/specs/\` contains current truth for each capability.
179
+
180
+ ## Proposal Rules
181
+
182
+ - Use a unique verb-led \`change-id\` in kebab-case.
183
+ - Include \`proposal.md\`, \`tasks.md\`, optional \`design.md\`, and delta specs per affected capability.
184
+ - Delta specs must use \`## ADDED|MODIFIED|REMOVED|RENAMED Requirements\`.
185
+ - Each requirement must include at least one \`#### Scenario:\`.
186
+
187
+ ## Reffy Relationship
188
+
189
+ - Reffy owns the runtime and artifact workflow.
190
+ - ReffySpec is the canonical planning/spec surface.
191
+ - Reffy artifacts in \`.reffy/\` should inform proposal/design content without duplicating the full planning files.
192
+ `;
193
+ }
117
194
  const REFFY_START = "<!-- REFFY:START -->";
118
195
  const REFFY_END = "<!-- REFFY:END -->";
196
+ const REFFYSPEC_START = "<!-- REFFYSPEC:START -->";
197
+ const REFFYSPEC_END = "<!-- REFFYSPEC:END -->";
119
198
  const OPENSPEC_START = "<!-- OPENSPEC:START -->";
199
+ const OPENSPEC_END = "<!-- OPENSPEC:END -->";
120
200
  function upsertReffyBlock(content) {
121
201
  return upsertReffyBlockForDir(content, DEFAULT_REFS_DIRNAME);
122
202
  }
@@ -134,10 +214,27 @@ function upsertReffyBlockForDir(content, refsDirName) {
134
214
  }
135
215
  return content.trim().length > 0 ? `${reffyBlock}\n\n${content.trimStart()}` : `${reffyBlock}\n`;
136
216
  }
217
+ function upsertPlanningBlock(content) {
218
+ const reffyspecBlock = buildReffySpecBlock();
219
+ if (content.includes(REFFYSPEC_START) && content.includes(REFFYSPEC_END)) {
220
+ const prefix = content.split(REFFYSPEC_START)[0] ?? "";
221
+ const suffix = content.split(REFFYSPEC_END, 2)[1] ?? "";
222
+ const trimmedSuffix = suffix.trimStart();
223
+ return trimmedSuffix.length > 0 ? `${prefix}${reffyspecBlock}\n\n${trimmedSuffix}` : `${prefix}${reffyspecBlock}\n`;
224
+ }
225
+ if (content.includes(OPENSPEC_START) && content.includes(OPENSPEC_END)) {
226
+ const prefix = content.split(OPENSPEC_START)[0] ?? "";
227
+ const suffix = content.split(OPENSPEC_END, 2)[1] ?? "";
228
+ const trimmedSuffix = suffix.trimStart();
229
+ return trimmedSuffix.length > 0 ? `${prefix}${reffyspecBlock}\n\n${trimmedSuffix}` : `${prefix}${reffyspecBlock}\n`;
230
+ }
231
+ return content.trim().length > 0 ? `${content.trimEnd()}\n\n${reffyspecBlock}\n` : `${reffyspecBlock}\n`;
232
+ }
137
233
  async function initAgents(repoRoot) {
138
- const refsDirName = resolveRefsDirName(repoRoot);
234
+ const refsDirName = DEFAULT_REFS_DIRNAME;
139
235
  const agentsPath = path.join(repoRoot, "AGENTS.md");
140
236
  const reffyAgentsPath = path.join(repoRoot, refsDirName, "AGENTS.md");
237
+ const reffyspecAgentsPath = path.join(repoRoot, DEFAULT_PLANNING_DIRNAME, "AGENTS.md");
141
238
  let content = "";
142
239
  try {
143
240
  content = await fs.readFile(agentsPath, "utf8");
@@ -145,11 +242,13 @@ async function initAgents(repoRoot) {
145
242
  catch {
146
243
  content = "";
147
244
  }
148
- const updated = upsertReffyBlockForDir(content, refsDirName);
245
+ const updated = upsertPlanningBlock(upsertReffyBlockForDir(content, refsDirName));
149
246
  await fs.mkdir(path.dirname(reffyAgentsPath), { recursive: true });
247
+ await fs.mkdir(path.dirname(reffyspecAgentsPath), { recursive: true });
150
248
  await fs.writeFile(agentsPath, updated, "utf8");
151
249
  await fs.writeFile(reffyAgentsPath, buildReffyAgentsContent(refsDirName), "utf8");
152
- return { root_agents_path: agentsPath, reffy_agents_path: reffyAgentsPath };
250
+ await fs.writeFile(reffyspecAgentsPath, buildReffySpecAgentsContent(), "utf8");
251
+ return { root_agents_path: agentsPath, reffy_agents_path: reffyAgentsPath, reffyspec_agents_path: reffyspecAgentsPath };
153
252
  }
154
253
  function pathExists(targetPath) {
155
254
  return existsSync(targetPath);
@@ -199,6 +298,30 @@ function parseRepoArg(argv) {
199
298
  }
200
299
  return discoverRepoRoot(process.cwd());
201
300
  }
301
+ function getPlanPositionalArgs(argv) {
302
+ const positionals = [];
303
+ for (let i = 0; i < argv.length; i += 1) {
304
+ const arg = argv[i];
305
+ if (arg === "--repo" || arg === "--output" || arg === "--change-id" || arg === "--title" || arg === "--artifacts") {
306
+ i += 1;
307
+ continue;
308
+ }
309
+ if (arg.startsWith("--repo=") ||
310
+ arg.startsWith("--output=") ||
311
+ arg.startsWith("--change-id=") ||
312
+ arg.startsWith("--title=") ||
313
+ arg.startsWith("--artifacts=") ||
314
+ arg === "--json" ||
315
+ arg === "--all" ||
316
+ arg === "--force") {
317
+ continue;
318
+ }
319
+ if (arg.startsWith("--"))
320
+ continue;
321
+ positionals.push(arg);
322
+ }
323
+ return positionals;
324
+ }
202
325
  function parseOutputMode(argv) {
203
326
  for (let i = 0; i < argv.length; i += 1) {
204
327
  const arg = argv[i];
@@ -242,10 +365,13 @@ function usage() {
242
365
  "Commands:",
243
366
  " init Ensure root AGENTS.md block and .reffy/AGENTS.md are up to date.",
244
367
  " bootstrap Run init, ensure .reffy structure exists, then reindex artifacts.",
368
+ " migrate Migrate a legacy .references workspace to the canonical .reffy layout.",
245
369
  " doctor Diagnose required Reffy setup and optional tool availability.",
246
370
  " reindex Scan .reffy/artifacts and add missing files to manifest.",
247
371
  " validate Validate .reffy/manifest.json against manifest v1 contract.",
248
372
  " summarize Generate a read-only summary of indexed Reffy artifacts.",
373
+ " plan Generate and manage ReffySpec planning scaffolds from indexed Reffy artifacts.",
374
+ " spec Inspect current specs from the ReffySpec layout.",
249
375
  " diagram Render Mermaid diagrams (supports SVG and ASCII).",
250
376
  ].join("\n");
251
377
  }
@@ -254,7 +380,7 @@ function diagramUsage() {
254
380
  "Usage: reffy diagram render [--repo PATH] [--input PATH|--stdin] [--format svg|ascii] [--output PATH]",
255
381
  "",
256
382
  "Options:",
257
- " --input PATH Read Mermaid (or OpenSpec spec.md) from file",
383
+ " --input PATH Read Mermaid (or ReffySpec spec.md) from file",
258
384
  " --stdin Read Mermaid text from stdin",
259
385
  " --format VALUE Output format: svg (default) or ascii",
260
386
  " --output PATH Write rendered result to file instead of stdout",
@@ -269,6 +395,30 @@ function diagramUsage() {
269
395
  " --font NAME SVG font family override",
270
396
  ].join("\n");
271
397
  }
398
+ function specUsage() {
399
+ return [
400
+ "Usage:",
401
+ " reffy spec list [--repo PATH] [--output text|json]",
402
+ " reffy spec show <spec-id> [--repo PATH] [--output text|json]",
403
+ ].join("\n");
404
+ }
405
+ function planUsage() {
406
+ return [
407
+ "Usage:",
408
+ " reffy plan create --change-id ID [--repo PATH] [--artifacts file1.md,file2.md|--all] [--title TEXT]",
409
+ " reffy plan validate <change-id> [--repo PATH] [--output text|json]",
410
+ " reffy plan list [--repo PATH] [--output text|json]",
411
+ " reffy plan show <change-id> [--repo PATH] [--output text|json]",
412
+ " reffy plan archive <change-id> [--repo PATH] [--output text|json]",
413
+ "",
414
+ "Options:",
415
+ ` --change-id ID ${DEFAULT_PLANNING_DIRNAME} change id to create`,
416
+ " --title TEXT Human-readable proposal title",
417
+ " --artifacts LIST Comma-separated artifact filenames or ids to link",
418
+ " --all Use all indexed artifacts as planning inputs",
419
+ " --force Overwrite an existing non-empty change directory",
420
+ ].join("\n");
421
+ }
272
422
  function parseDiagramArgs(argv) {
273
423
  const repoRoot = parseRepoArg(argv);
274
424
  const args = {
@@ -368,6 +518,83 @@ function parseDiagramArgs(argv) {
368
518
  }
369
519
  return args;
370
520
  }
521
+ function parsePlanArgs(argv) {
522
+ const repoRoot = parseRepoArg(argv);
523
+ const args = {
524
+ repoRoot,
525
+ changeId: "",
526
+ artifactFilters: [],
527
+ includeAllArtifacts: false,
528
+ overwrite: false,
529
+ };
530
+ for (let i = 0; i < argv.length; i += 1) {
531
+ const arg = argv[i];
532
+ if (arg === "--repo") {
533
+ i += 1;
534
+ continue;
535
+ }
536
+ if (arg.startsWith("--repo="))
537
+ continue;
538
+ if (arg === "--json")
539
+ continue;
540
+ if (arg === "--output") {
541
+ i += 1;
542
+ continue;
543
+ }
544
+ if (arg.startsWith("--output="))
545
+ continue;
546
+ if (arg === "--change-id") {
547
+ const value = argv[i + 1];
548
+ if (!value)
549
+ throw new Error("--change-id requires a value");
550
+ args.changeId = value;
551
+ i += 1;
552
+ continue;
553
+ }
554
+ if (arg.startsWith("--change-id=")) {
555
+ args.changeId = arg.split("=", 2)[1] ?? "";
556
+ continue;
557
+ }
558
+ if (arg === "--title") {
559
+ const value = argv[i + 1];
560
+ if (!value)
561
+ throw new Error("--title requires a value");
562
+ args.title = value;
563
+ i += 1;
564
+ continue;
565
+ }
566
+ if (arg.startsWith("--title=")) {
567
+ args.title = arg.split("=", 2)[1];
568
+ continue;
569
+ }
570
+ if (arg === "--artifacts") {
571
+ const value = argv[i + 1];
572
+ if (!value)
573
+ throw new Error("--artifacts requires a comma-separated value");
574
+ args.artifactFilters = value.split(",").map((entry) => entry.trim()).filter(Boolean);
575
+ i += 1;
576
+ continue;
577
+ }
578
+ if (arg.startsWith("--artifacts=")) {
579
+ const value = arg.split("=", 2)[1] ?? "";
580
+ args.artifactFilters = value.split(",").map((entry) => entry.trim()).filter(Boolean);
581
+ continue;
582
+ }
583
+ if (arg === "--all") {
584
+ args.includeAllArtifacts = true;
585
+ continue;
586
+ }
587
+ if (arg === "--force") {
588
+ args.overwrite = true;
589
+ continue;
590
+ }
591
+ throw new Error(`Unknown plan option: ${arg}`);
592
+ }
593
+ if (!args.changeId) {
594
+ throw new Error("--change-id is required");
595
+ }
596
+ return args;
597
+ }
371
598
  function printSection(title, values) {
372
599
  console.log(`${title}:`);
373
600
  if (values.length === 0) {
@@ -400,6 +627,7 @@ async function main() {
400
627
  return 1;
401
628
  }
402
629
  const parsed = parseDiagramArgs(diagramArgs);
630
+ await prepareCanonicalPlanningLayout(parsed.repoRoot);
403
631
  const rendered = await renderDiagram({
404
632
  repoRoot: parsed.repoRoot,
405
633
  inputPath: parsed.inputPath,
@@ -426,29 +654,271 @@ async function main() {
426
654
  }
427
655
  return 0;
428
656
  }
657
+ if (command === "plan") {
658
+ const [subcommand, ...planArgs] = rest;
659
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
660
+ console.error(planUsage());
661
+ return 1;
662
+ }
663
+ if (subcommand === "create") {
664
+ const output = parseOutputMode(planArgs);
665
+ const parsed = parsePlanArgs(planArgs);
666
+ await prepareCanonicalPlanningLayout(parsed.repoRoot);
667
+ const store = new ReferencesStore(parsed.repoRoot);
668
+ const validation = await store.validateManifest();
669
+ if (!validation.ok) {
670
+ const payload = { status: "error", command: "plan", subcommand: "create", ...validation };
671
+ if (output === "json") {
672
+ printResult(output, payload);
673
+ }
674
+ else {
675
+ console.error(`Cannot create plan: manifest invalid (${String(validation.errors.length)} error(s))`);
676
+ }
677
+ return 1;
678
+ }
679
+ const result = await createPlanScaffold(store, {
680
+ changeId: parsed.changeId,
681
+ title: parsed.title,
682
+ artifactFilters: parsed.artifactFilters,
683
+ includeAllArtifacts: parsed.includeAllArtifacts,
684
+ overwrite: parsed.overwrite,
685
+ });
686
+ const payload = { status: "ok", command: "plan", subcommand: "create", ...result };
687
+ if (output === "json") {
688
+ printResult(output, payload);
689
+ }
690
+ else {
691
+ console.log(`Created ${result.change_dir}`);
692
+ console.log(`Artifacts linked: ${String(result.linked_artifacts)}`);
693
+ for (const file of result.written_files) {
694
+ console.log(`- ${file}`);
695
+ }
696
+ }
697
+ return 0;
698
+ }
699
+ const output = parseOutputMode(planArgs);
700
+ const repoRoot = parseRepoArg(planArgs);
701
+ await prepareCanonicalPlanningLayout(repoRoot);
702
+ const positionals = getPlanPositionalArgs(planArgs);
703
+ if (subcommand === "validate") {
704
+ const changeId = positionals[0];
705
+ if (!changeId) {
706
+ console.error("reffy plan validate requires a change id");
707
+ console.error(planUsage());
708
+ return 1;
709
+ }
710
+ const result = await validatePlanningChange(repoRoot, changeId);
711
+ const payload = { status: result.ok ? "ok" : "error", command: "plan", subcommand: "validate", ...result };
712
+ if (output === "json") {
713
+ printResult(output, payload);
714
+ }
715
+ else if (result.ok) {
716
+ console.log(`Change "${changeId}" is valid`);
717
+ console.log(`Tasks: ${String(result.task_status.completed)}/${String(result.task_status.total)}`);
718
+ console.log(`Delta specs: ${String(result.delta_count)}`);
719
+ for (const warning of result.warnings) {
720
+ console.log(`warn: ${warning}`);
721
+ }
722
+ }
723
+ else {
724
+ console.error(`Change "${changeId}" has issues`);
725
+ for (const error of result.errors) {
726
+ console.error(`error: ${error}`);
727
+ }
728
+ for (const warning of result.warnings) {
729
+ console.error(`warn: ${warning}`);
730
+ }
731
+ }
732
+ return result.ok ? 0 : 1;
733
+ }
734
+ if (subcommand === "list") {
735
+ const changes = await listPlanningChanges(repoRoot);
736
+ const payload = { status: "ok", command: "plan", subcommand: "list", changes };
737
+ if (output === "json") {
738
+ printResult(output, payload);
739
+ }
740
+ else if (changes.length === 0) {
741
+ console.log("Changes:");
742
+ console.log("- (none)");
743
+ }
744
+ else {
745
+ console.log("Changes:");
746
+ for (const change of changes) {
747
+ console.log(`- ${change.id} - ${change.title} [deltas ${String(change.delta_count)}] [tasks ${String(change.task_status.completed)}/${String(change.task_status.total)}]`);
748
+ }
749
+ }
750
+ return 0;
751
+ }
752
+ if (subcommand === "show") {
753
+ const changeId = positionals[0];
754
+ if (!changeId) {
755
+ console.error("reffy plan show requires a change id");
756
+ console.error(planUsage());
757
+ return 1;
758
+ }
759
+ const result = await showPlanningChange(repoRoot, changeId);
760
+ const payload = { status: "ok", command: "plan", subcommand: "show", change: result };
761
+ if (output === "json") {
762
+ printResult(output, payload);
763
+ }
764
+ else {
765
+ console.log(`# ${result.title}`);
766
+ console.log("");
767
+ console.log(result.proposal.trim());
768
+ console.log("");
769
+ console.log("## Tasks");
770
+ console.log(result.tasks.trim());
771
+ if (result.design) {
772
+ console.log("");
773
+ console.log("## Design");
774
+ console.log(result.design.trim());
775
+ }
776
+ if (result.specs.length > 0) {
777
+ console.log("");
778
+ console.log("## Specs");
779
+ for (const spec of result.specs) {
780
+ console.log(`### ${spec.capability}`);
781
+ console.log(spec.content.trim());
782
+ }
783
+ }
784
+ }
785
+ return 0;
786
+ }
787
+ if (subcommand === "archive") {
788
+ const changeId = positionals[0];
789
+ if (!changeId) {
790
+ console.error("reffy plan archive requires a change id");
791
+ console.error(planUsage());
792
+ return 1;
793
+ }
794
+ const result = await archivePlanningChange(repoRoot, changeId);
795
+ const payload = { status: "ok", command: "plan", subcommand: "archive", ...result };
796
+ if (output === "json") {
797
+ printResult(output, payload);
798
+ }
799
+ else {
800
+ console.log(`Archived ${changeId} to ${result.archive_dir}`);
801
+ console.log(`Updated specs: ${String(result.updated_specs.length)}`);
802
+ console.log(`Linked artifacts updated: ${String(result.linked_artifacts)}`);
803
+ }
804
+ return 0;
805
+ }
806
+ {
807
+ console.error(`Unknown plan subcommand: ${subcommand}`);
808
+ console.error(planUsage());
809
+ return 1;
810
+ }
811
+ }
812
+ if (command === "spec") {
813
+ const [subcommand, ...specArgs] = rest;
814
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
815
+ console.error(specUsage());
816
+ return 1;
817
+ }
818
+ const output = parseOutputMode(specArgs);
819
+ const repoRoot = parseRepoArg(specArgs);
820
+ await prepareCanonicalPlanningLayout(repoRoot);
821
+ const positionals = getPlanPositionalArgs(specArgs);
822
+ if (subcommand === "list") {
823
+ const specs = await listSpecs(repoRoot);
824
+ const payload = { status: "ok", command: "spec", subcommand: "list", specs };
825
+ if (output === "json") {
826
+ printResult(output, payload);
827
+ }
828
+ else if (specs.length === 0) {
829
+ console.log("Specs:");
830
+ console.log("- (none)");
831
+ }
832
+ else {
833
+ console.log("Specs:");
834
+ for (const spec of specs) {
835
+ console.log(`- ${spec.id} - ${spec.title} [requirements ${String(spec.requirement_count)}]`);
836
+ }
837
+ }
838
+ return 0;
839
+ }
840
+ if (subcommand === "show") {
841
+ const specId = positionals[0];
842
+ if (!specId) {
843
+ console.error("reffy spec show requires a spec id");
844
+ console.error(specUsage());
845
+ return 1;
846
+ }
847
+ const result = await showSpec(repoRoot, specId);
848
+ const payload = { status: "ok", command: "spec", subcommand: "show", spec: result };
849
+ if (output === "json") {
850
+ printResult(output, payload);
851
+ }
852
+ else {
853
+ console.log(`# ${result.title}`);
854
+ if (result.purpose) {
855
+ console.log("");
856
+ console.log(`Purpose: ${result.purpose}`);
857
+ }
858
+ console.log("");
859
+ console.log(result.spec.trim());
860
+ if (result.design) {
861
+ console.log("");
862
+ console.log("## Design");
863
+ console.log(result.design.trim());
864
+ }
865
+ }
866
+ return 0;
867
+ }
868
+ console.error(`Unknown spec subcommand: ${subcommand}`);
869
+ console.error(specUsage());
870
+ return 1;
871
+ }
429
872
  const output = parseOutputMode(rest);
430
873
  if (command === "init") {
431
874
  printBanner(output);
432
875
  const repoRoot = parseRepoArg(rest);
876
+ const workspace = await prepareCanonicalWorkspace(repoRoot);
877
+ const planning = await prepareCanonicalPlanningLayout(repoRoot);
433
878
  const agents = await initAgents(repoRoot);
434
- const payload = { status: "ok", command: "init", ...agents };
879
+ const payload = {
880
+ status: "ok",
881
+ command: "init",
882
+ workspace_mode: workspace.state.mode,
883
+ migrated_workspace: workspace.migrated,
884
+ created_workspace: workspace.created,
885
+ planning_mode: planning.state.mode,
886
+ migrated_planning_layout: planning.migrated,
887
+ created_planning_layout: planning.created,
888
+ ...agents,
889
+ };
435
890
  if (output === "json") {
436
891
  printResult(output, payload);
437
892
  }
438
893
  else {
894
+ if (workspace.message) {
895
+ console.log(workspace.message);
896
+ }
897
+ if (planning.message) {
898
+ console.log(planning.message);
899
+ }
439
900
  console.log(`Updated ${agents.root_agents_path}`);
440
901
  console.log(`Updated ${agents.reffy_agents_path}`);
902
+ console.log(`Updated ${agents.reffyspec_agents_path}`);
441
903
  }
442
904
  return 0;
443
905
  }
444
906
  if (command === "bootstrap") {
445
907
  const repoRoot = parseRepoArg(rest);
908
+ const workspace = await prepareCanonicalWorkspace(repoRoot);
909
+ const planning = await prepareCanonicalPlanningLayout(repoRoot);
446
910
  const agents = await initAgents(repoRoot);
447
911
  const store = new ReferencesStore(repoRoot);
448
912
  const reindex = await store.reindexArtifacts();
449
913
  const payload = {
450
914
  status: "ok",
451
915
  command: "bootstrap",
916
+ workspace_mode: workspace.state.mode,
917
+ migrated_workspace: workspace.migrated,
918
+ created_workspace: workspace.created,
919
+ planning_mode: planning.state.mode,
920
+ migrated_planning_layout: planning.migrated,
921
+ created_planning_layout: planning.created,
452
922
  ...agents,
453
923
  refs_dir: store.refsDir,
454
924
  manifest_path: store.manifestPath,
@@ -458,15 +928,54 @@ async function main() {
458
928
  printResult(output, payload);
459
929
  }
460
930
  else {
931
+ if (workspace.message) {
932
+ console.log(workspace.message);
933
+ }
934
+ if (planning.message) {
935
+ console.log(planning.message);
936
+ }
461
937
  console.log(`Bootstrapped ${store.refsDir}`);
462
938
  console.log(`Updated ${agents.root_agents_path}`);
463
939
  console.log(`Updated ${agents.reffy_agents_path}`);
940
+ console.log(`Updated ${agents.reffyspec_agents_path}`);
464
941
  console.log(`Reindex: added=${String(reindex.added)} removed=${String(reindex.removed)} total=${String(reindex.total)}`);
465
942
  }
466
943
  return 0;
467
944
  }
945
+ if (command === "migrate") {
946
+ const repoRoot = parseRepoArg(rest);
947
+ const workspace = await prepareCanonicalWorkspace(repoRoot);
948
+ const planning = await prepareCanonicalPlanningLayout(repoRoot);
949
+ const agents = await initAgents(repoRoot);
950
+ const payload = {
951
+ status: "ok",
952
+ command: "migrate",
953
+ workspace_mode: workspace.state.mode,
954
+ migrated_workspace: workspace.migrated,
955
+ created_workspace: workspace.created,
956
+ planning_mode: planning.state.mode,
957
+ migrated_planning_layout: planning.migrated,
958
+ created_planning_layout: planning.created,
959
+ refs_dir: workspace.state.canonicalDir,
960
+ ...agents,
961
+ };
962
+ if (output === "json") {
963
+ printResult(output, payload);
964
+ }
965
+ else {
966
+ console.log(workspace.message ?? `Workspace ready at ${workspace.state.canonicalDir}`);
967
+ if (planning.message) {
968
+ console.log(planning.message);
969
+ }
970
+ console.log(`Updated ${agents.root_agents_path}`);
971
+ console.log(`Updated ${agents.reffy_agents_path}`);
972
+ console.log(`Updated ${agents.reffyspec_agents_path}`);
973
+ }
974
+ return 0;
975
+ }
468
976
  if (command === "reindex") {
469
977
  const repoRoot = parseRepoArg(rest);
978
+ await prepareCanonicalPlanningLayout(repoRoot);
470
979
  const store = new ReferencesStore(repoRoot);
471
980
  const reindex = await store.reindexArtifacts();
472
981
  const payload = { status: "ok", command: "reindex", ...reindex };
@@ -480,6 +989,7 @@ async function main() {
480
989
  }
481
990
  if (command === "doctor") {
482
991
  const repoRoot = parseRepoArg(rest);
992
+ await prepareCanonicalPlanningLayout(repoRoot);
483
993
  const report = await runDoctor(repoRoot);
484
994
  const status = report.summary.required_failed > 0 ? "error" : "ok";
485
995
  const payload = { status, command: "doctor", ...report };
@@ -505,6 +1015,7 @@ async function main() {
505
1015
  }
506
1016
  if (command === "validate") {
507
1017
  const repoRoot = parseRepoArg(rest);
1018
+ await prepareCanonicalPlanningLayout(repoRoot);
508
1019
  const store = new ReferencesStore(repoRoot);
509
1020
  const result = await store.validateManifest();
510
1021
  const payload = { status: result.ok ? "ok" : "error", command: "validate", ...result };
@@ -532,6 +1043,7 @@ async function main() {
532
1043
  }
533
1044
  if (command === "summarize") {
534
1045
  const repoRoot = parseRepoArg(rest);
1046
+ await prepareCanonicalPlanningLayout(repoRoot);
535
1047
  const store = new ReferencesStore(repoRoot);
536
1048
  const validation = await store.validateManifest();
537
1049
  if (!validation.ok) {