reffy-cli 1.1.2 → 1.2.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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![MIT License](https://img.shields.io/github/license/RoskiDeluge/reffy-ts.svg)](LICENSE)
5
5
  [![CI](https://github.com/RoskiDeluge/reffy-ts/actions/workflows/ci.yml/badge.svg)](https://github.com/RoskiDeluge/reffy-ts/actions/workflows/ci.yml)
6
6
 
7
- Reffy is a CLI-first planning system for agent-friendly development workflows. It keeps ideation artifacts in straightforward version-controlled markdown files and manages formal planning files under `reffyspec/`.
7
+ Reffy is a CLI-first planning system for agent-friendly development workflows. It keeps ideation artifacts in straightforward version-controlled markdown files and manages formal planning files under `.reffy/reffyspec/`.
8
8
 
9
9
  ## Install
10
10
 
@@ -23,7 +23,7 @@ reffy init
23
23
 
24
24
  Command summary:
25
25
 
26
- - `reffy init`: runs the canonical first-run setup flow, creates the `.reffy/` and `reffyspec/` structure, writes managed instructions, reindexes artifacts, and on first-run text output prints a copy/paste instruction for your agent harness.
26
+ - `reffy init`: runs the canonical first-run setup flow, creates the `.reffy/` and `.reffy/reffyspec/` structure, writes managed instructions, reindexes artifacts, and on first-run text output prints a copy/paste instruction for your agent harness.
27
27
  - `reffy bootstrap`: compatibility alias for `reffy init`.
28
28
  - `reffy migrate`: migrates a legacy `.references/` workspace into `.reffy/`.
29
29
  - `reffy doctor`: diagnoses required Reffy setup and workspace health.
@@ -31,8 +31,8 @@ Command summary:
31
31
  - `reffy validate`: validates `.reffy/manifest.json` against manifest v1 contract.
32
32
  - `reffy summarize`: generates a read-only handoff summary from indexed artifacts.
33
33
  - `reffy plan create`: generates proposal, task, design, and spec scaffolds from indexed Reffy artifacts.
34
- - `reffy plan validate|list|show|archive`: manages the planning lifecycle under `reffyspec/`.
35
- - `reffy spec list|show`: inspects current spec state under `reffyspec/`.
34
+ - `reffy plan validate|list|show|archive`: manages the planning lifecycle under `.reffy/reffyspec/`.
35
+ - `reffy spec list|show`: inspects current spec state under `.reffy/reffyspec/`.
36
36
  - `reffy diagram render`: renders Mermaid diagrams as SVG or ASCII, including spec-aware generation from compatible `spec.md` files.
37
37
 
38
38
  Output modes:
@@ -55,8 +55,8 @@ reffy plan list --output json
55
55
  reffy plan archive add-login-flow
56
56
  reffy spec show auth --output json
57
57
  reffy diagram render --stdin --format svg < diagram.mmd
58
- reffy diagram render --input reffyspec/specs/auth/spec.md --format ascii
59
- reffy diagram render --input reffyspec/specs/auth/spec.md --format svg --output .reffy/artifacts/auth-spec.svg
58
+ reffy diagram render --input .reffy/reffyspec/specs/auth/spec.md --format ascii
59
+ reffy diagram render --input .reffy/reffyspec/specs/auth/spec.md --format svg --output .reffy/artifacts/auth-spec.svg
60
60
  ```
61
61
 
62
62
  ## Using Reffy With ReffySpec
@@ -64,7 +64,7 @@ reffy diagram render --input reffyspec/specs/auth/spec.md --format svg --output
64
64
  A practical pattern is:
65
65
 
66
66
  1. Use Reffy for ideation and context capture in `.reffy/artifacts/`.
67
- 2. Use Reffy to scaffold and manage planning files in `reffyspec/`.
67
+ 2. Use Reffy to scaffold and manage planning files in `.reffy/reffyspec/`.
68
68
  3. Keep a clear traceable path from exploratory artifacts to formal specs.
69
69
  4. Use Reffy commands for day-to-day workflow.
70
70
 
@@ -72,10 +72,10 @@ Reference implementation in this repo:
72
72
 
73
73
  - `AGENTS.md`: contains both managed instruction blocks and encodes sequencing.
74
74
  - `AGENTS.md`: Reffy block routes ideation/exploration requests to `@/.reffy/AGENTS.md`.
75
- - `AGENTS.md`: ReffySpec block routes planning/proposal/spec requests to `@/reffyspec/AGENTS.md`.
75
+ - `AGENTS.md`: ReffySpec block routes planning/proposal/spec requests to `@/.reffy/reffyspec/AGENTS.md`.
76
76
  - `.reffy/AGENTS.md`: defines the artifact and ideation workflow.
77
- - `reffyspec/AGENTS.md`: defines the ReffySpec planning/spec workflow conventions used in this repo.
78
- - `reffyspec/project.md`: captures durable project context for agents, including purpose, stack, architecture, conventions, and constraints.
77
+ - `.reffy/reffyspec/AGENTS.md`: defines the ReffySpec planning/spec workflow conventions used in this repo.
78
+ - `.reffy/reffyspec/project.md`: captures durable project context for agents, including purpose, stack, architecture, conventions, and constraints.
79
79
 
80
80
  ## Develop
81
81
 
package/dist/cli.js CHANGED
@@ -6,9 +6,9 @@ import { renderDiagram } from "./diagram.js";
6
6
  import { runDoctor } from "./doctor.js";
7
7
  import { archivePlanningChange } from "./plan-archive.js";
8
8
  import { createPlanScaffold } from "./plan.js";
9
- import { DEFAULT_PLANNING_DIRNAME } from "./planning-paths.js";
9
+ import { DEFAULT_PLANNING_RELATIVE_DIR, looksLikePlanningDir, resolveCanonicalPlanningPath } from "./planning-paths.js";
10
10
  import { listPlanningChanges, showPlanningChange, validatePlanningChange } from "./plan-runtime.js";
11
- import { DEFAULT_REFS_DIRNAME, looksLikeRefsDir } from "./refs-paths.js";
11
+ import { DEFAULT_REFS_DIRNAME, detectWorkspaceState, looksLikeRefsDir } from "./refs-paths.js";
12
12
  import { prepareCanonicalPlanningLayout } from "./planning-workspace.js";
13
13
  import { ReferencesStore } from "./storage.js";
14
14
  import { listSpecs, showSpec } from "./spec-runtime.js";
@@ -25,7 +25,7 @@ const REFFY_ASCII = [
25
25
  " |___/ ",
26
26
  ].join("\n");
27
27
  const BOOTSTRAP_AGENT_INSTRUCTION = [
28
- 'Please read `AGENTS.md` and help me fill out the project context template in `reffyspec/project.md`',
28
+ `Please read \`AGENTS.md\` and help me fill out the project context template in \`${DEFAULT_PLANNING_RELATIVE_DIR}/project.md\``,
29
29
  "with details about my project, tech stack, architecture, and conventions.",
30
30
  ].join(" ");
31
31
  function buildReffyBlock(refsDirName) {
@@ -54,12 +54,12 @@ function buildReffySpecBlock() {
54
54
 
55
55
  These instructions are for AI assistants working in this project.
56
56
 
57
- Always open \`@/${DEFAULT_PLANNING_DIRNAME}/AGENTS.md\` when the request:
57
+ Always open \`@/${DEFAULT_PLANNING_RELATIVE_DIR}/AGENTS.md\` when the request:
58
58
  - Mentions planning or proposals (words like proposal, spec, change, plan)
59
59
  - Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
60
60
  - Needs the authoritative planning/spec workflow for this repo
61
61
 
62
- Use \`@/${DEFAULT_PLANNING_DIRNAME}/AGENTS.md\` to learn:
62
+ Use \`@/${DEFAULT_PLANNING_RELATIVE_DIR}/AGENTS.md\` to learn:
63
63
  - How to create and apply ReffySpec change proposals
64
64
  - ReffySpec format and conventions
65
65
  - Project structure and planning guidelines
@@ -109,7 +109,7 @@ You can skip Reffy when the request is:
109
109
  - ReffySpec is the planning subsystem inside Reffy.
110
110
  - The vendored fork at \`/.vendor/ReffySpec\` is reference-only for v1; first-party behavior lives in this repo.
111
111
  - Reffy is the primary runtime authority for this project.
112
- - ReffySpec files live under \`${DEFAULT_PLANNING_DIRNAME}/\` as the canonical planning layout.
112
+ - ReffySpec files live under \`${DEFAULT_PLANNING_RELATIVE_DIR}/\` as the canonical planning layout.
113
113
  - Do not duplicate full proposal/spec content in Reffy artifacts; generate and link planning outputs from them.
114
114
 
115
115
  ## ReffySpec Citation Rules
@@ -126,7 +126,7 @@ When a ReffySpec proposal is informed by Reffy artifacts:
126
126
 
127
127
  ### Reusable Proposal Snippet
128
128
 
129
- Use this in \`${DEFAULT_PLANNING_DIRNAME}/changes/<change-id>/proposal.md\`:
129
+ Use this in \`${DEFAULT_PLANNING_RELATIVE_DIR}/changes/<change-id>/proposal.md\`:
130
130
 
131
131
  \`\`\`md
132
132
  ## Reffy References
@@ -155,17 +155,17 @@ These instructions are for AI assistants working in this project.
155
155
 
156
156
  ## TL;DR Checklist
157
157
 
158
- - Read the relevant current specs in \`${DEFAULT_PLANNING_DIRNAME}/specs/\` before changing behavior.
159
- - Review active changes in \`${DEFAULT_PLANNING_DIRNAME}/changes/\` before drafting or implementing new work.
158
+ - Read the relevant current specs in \`${DEFAULT_PLANNING_RELATIVE_DIR}/specs/\` before changing behavior.
159
+ - Review active changes in \`${DEFAULT_PLANNING_RELATIVE_DIR}/changes/\` before drafting or implementing new work.
160
160
  - Use ReffySpec change proposals for new capabilities, breaking changes, or architecture shifts.
161
- - Keep current truth in \`${DEFAULT_PLANNING_DIRNAME}/specs/\` and proposed deltas in \`${DEFAULT_PLANNING_DIRNAME}/changes/\`.
161
+ - Keep current truth in \`${DEFAULT_PLANNING_RELATIVE_DIR}/specs/\` and proposed deltas in \`${DEFAULT_PLANNING_RELATIVE_DIR}/changes/\`.
162
162
 
163
163
  ## ReffySpec Workflow
164
164
 
165
- 1. Read \`${DEFAULT_PLANNING_DIRNAME}/project.md\` for project conventions.
166
- 2. Inspect current specs in \`${DEFAULT_PLANNING_DIRNAME}/specs/\`.
167
- 3. Inspect active changes in \`${DEFAULT_PLANNING_DIRNAME}/changes/\`.
168
- 4. Draft or update proposal/design/tasks/spec files under \`${DEFAULT_PLANNING_DIRNAME}/changes/<change-id>/\`.
165
+ 1. Read \`${DEFAULT_PLANNING_RELATIVE_DIR}/project.md\` for project conventions.
166
+ 2. Inspect current specs in \`${DEFAULT_PLANNING_RELATIVE_DIR}/specs/\`.
167
+ 3. Inspect active changes in \`${DEFAULT_PLANNING_RELATIVE_DIR}/changes/\`.
168
+ 4. Draft or update proposal/design/tasks/spec files under \`${DEFAULT_PLANNING_RELATIVE_DIR}/changes/<change-id>/\`.
169
169
  5. Use native Reffy commands for routine planning workflow:
170
170
  - \`reffy plan create\`
171
171
  - \`reffy plan validate\`
@@ -177,9 +177,9 @@ These instructions are for AI assistants working in this project.
177
177
 
178
178
  ## Directory Model
179
179
 
180
- - \`${DEFAULT_PLANNING_DIRNAME}/changes/\` contains active proposed changes.
181
- - \`${DEFAULT_PLANNING_DIRNAME}/changes/archive/\` contains historical archived changes.
182
- - \`${DEFAULT_PLANNING_DIRNAME}/specs/\` contains current truth for each capability.
180
+ - \`${DEFAULT_PLANNING_RELATIVE_DIR}/changes/\` contains active proposed changes.
181
+ - \`${DEFAULT_PLANNING_RELATIVE_DIR}/changes/archive/\` contains historical archived changes.
182
+ - \`${DEFAULT_PLANNING_RELATIVE_DIR}/specs/\` contains current truth for each capability.
183
183
 
184
184
  ## Proposal Rules
185
185
 
@@ -238,7 +238,7 @@ async function initAgents(repoRoot) {
238
238
  const refsDirName = DEFAULT_REFS_DIRNAME;
239
239
  const agentsPath = path.join(repoRoot, "AGENTS.md");
240
240
  const reffyAgentsPath = path.join(repoRoot, refsDirName, "AGENTS.md");
241
- const reffyspecAgentsPath = path.join(repoRoot, DEFAULT_PLANNING_DIRNAME, "AGENTS.md");
241
+ const reffyspecAgentsPath = resolveCanonicalPlanningPath(repoRoot, "AGENTS.md");
242
242
  let content = "";
243
243
  try {
244
244
  content = await fs.readFile(agentsPath, "utf8");
@@ -271,6 +271,11 @@ function discoverRepoRoot(startDir) {
271
271
  if (looksLikeRefsDir(current)) {
272
272
  return path.dirname(current);
273
273
  }
274
+ if (looksLikePlanningDir(current)) {
275
+ return path.basename(path.dirname(current)) === DEFAULT_REFS_DIRNAME
276
+ ? path.dirname(path.dirname(current))
277
+ : path.dirname(current);
278
+ }
274
279
  if (isDirectory(path.join(current, DEFAULT_REFS_DIRNAME)) || isDirectory(path.join(current, ".references"))) {
275
280
  return current;
276
281
  }
@@ -466,7 +471,7 @@ function planUsage() {
466
471
  " reffy plan archive <change-id> [--repo PATH] [--output text|json]",
467
472
  "",
468
473
  "Options:",
469
- ` --change-id ID ${DEFAULT_PLANNING_DIRNAME} change id to create`,
474
+ ` --change-id ID ${DEFAULT_PLANNING_RELATIVE_DIR} change id to create`,
470
475
  " --title TEXT Human-readable proposal title",
471
476
  " --artifacts LIST Comma-separated artifact filenames or ids to link",
472
477
  " --all Use all indexed artifacts as planning inputs",
@@ -966,7 +971,6 @@ async function main() {
966
971
  }
967
972
  if (command === "reindex") {
968
973
  const repoRoot = parseRepoArg(rest);
969
- await prepareCanonicalPlanningLayout(repoRoot);
970
974
  const store = new ReferencesStore(repoRoot);
971
975
  const reindex = await store.reindexArtifacts();
972
976
  const payload = { status: "ok", command: "reindex", ...reindex };
@@ -980,7 +984,9 @@ async function main() {
980
984
  }
981
985
  if (command === "doctor") {
982
986
  const repoRoot = parseRepoArg(rest);
983
- await prepareCanonicalPlanningLayout(repoRoot);
987
+ if (detectWorkspaceState(repoRoot).mode !== "legacy") {
988
+ await prepareCanonicalPlanningLayout(repoRoot);
989
+ }
984
990
  const report = await runDoctor(repoRoot);
985
991
  const status = report.summary.required_failed > 0 ? "error" : "ok";
986
992
  const payload = { status, command: "doctor", ...report };
@@ -1006,7 +1012,6 @@ async function main() {
1006
1012
  }
1007
1013
  if (command === "validate") {
1008
1014
  const repoRoot = parseRepoArg(rest);
1009
- await prepareCanonicalPlanningLayout(repoRoot);
1010
1015
  const store = new ReferencesStore(repoRoot);
1011
1016
  const result = await store.validateManifest();
1012
1017
  const payload = { status: result.ok ? "ok" : "error", command: "validate", ...result };
@@ -1034,7 +1039,6 @@ async function main() {
1034
1039
  }
1035
1040
  if (command === "summarize") {
1036
1041
  const repoRoot = parseRepoArg(rest);
1037
- await prepareCanonicalPlanningLayout(repoRoot);
1038
1042
  const store = new ReferencesStore(repoRoot);
1039
1043
  const validation = await store.validateManifest();
1040
1044
  if (!validation.ok) {
@@ -1,6 +1,6 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
- import { DEFAULT_PLANNING_DIRNAME } from "./planning-paths.js";
3
+ import { resolvePlanningPath } from "./planning-paths.js";
4
4
  import { validatePlanningChange } from "./plan-runtime.js";
5
5
  import { ReferencesStore } from "./storage.js";
6
6
  const DELTA_SECTION_PATTERN = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/;
@@ -185,7 +185,7 @@ async function buildSpecUpdates(repoRoot, changeId, changeDir) {
185
185
  const deltaContent = await fs.readFile(deltaPath, "utf8");
186
186
  const relDeltaPath = path.relative(repoRoot, deltaPath).split(path.sep).join("/");
187
187
  const delta = parseArchiveableRequirements(deltaContent, relDeltaPath);
188
- const currentSpecPath = path.join(repoRoot, DEFAULT_PLANNING_DIRNAME, "specs", capability, "spec.md");
188
+ const currentSpecPath = resolvePlanningPath(repoRoot, "specs", capability, "spec.md");
189
189
  const currentSpecExists = await pathExists(currentSpecPath);
190
190
  const nextContent = currentSpecExists
191
191
  ? mergeRequirementsIntoCurrentSpec(await fs.readFile(currentSpecPath, "utf8"), delta, path.relative(repoRoot, currentSpecPath).split(path.sep).join("/"))
@@ -204,11 +204,11 @@ export async function archivePlanningChange(repoRoot, changeId) {
204
204
  if (!validation.ok) {
205
205
  throw new Error(`cannot archive invalid change "${changeId}": ${validation.errors.join("; ")}`);
206
206
  }
207
- const changeDir = path.join(repoRoot, DEFAULT_PLANNING_DIRNAME, "changes", changeId);
207
+ const changeDir = resolvePlanningPath(repoRoot, "changes", changeId);
208
208
  if (!(await pathExists(changeDir))) {
209
209
  throw new Error(`change not found: ${changeId}`);
210
210
  }
211
- const archiveDir = path.join(repoRoot, DEFAULT_PLANNING_DIRNAME, "changes", "archive", `${archiveDatePrefix()}-${changeId}`);
211
+ const archiveDir = resolvePlanningPath(repoRoot, "changes", "archive", `${archiveDatePrefix()}-${changeId}`);
212
212
  if (await pathExists(archiveDir)) {
213
213
  throw new Error(`archive destination already exists: ${path.relative(repoRoot, archiveDir).split(path.sep).join("/")}`);
214
214
  }
@@ -1,6 +1,6 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
- import { DEFAULT_PLANNING_DIRNAME } from "./planning-paths.js";
3
+ import { resolvePlanningPath } from "./planning-paths.js";
4
4
  const CHANGE_HEADING_PREFIX = "# Change:";
5
5
  const REQUIREMENT_SECTION_PATTERN = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/;
6
6
  const REQUIREMENT_HEADING_PATTERN = /^###\s+Requirement:\s+(.+)$/;
@@ -20,7 +20,7 @@ async function listDirectories(dir) {
20
20
  return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
21
21
  }
22
22
  function getChangePaths(repoRoot, changeId) {
23
- const changeDir = path.join(repoRoot, DEFAULT_PLANNING_DIRNAME, "changes", changeId);
23
+ const changeDir = resolvePlanningPath(repoRoot, "changes", changeId);
24
24
  return {
25
25
  id: changeId,
26
26
  changeDir,
@@ -130,7 +130,7 @@ async function buildChangeSummary(repoRoot, changeId) {
130
130
  };
131
131
  }
132
132
  export async function listPlanningChanges(repoRoot) {
133
- const changesRoot = path.join(repoRoot, DEFAULT_PLANNING_DIRNAME, "changes");
133
+ const changesRoot = resolvePlanningPath(repoRoot, "changes");
134
134
  const changeIds = (await listDirectories(changesRoot)).filter((id) => id !== "archive");
135
135
  const summaries = [];
136
136
  for (const changeId of changeIds) {
package/dist/plan.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
- import { DEFAULT_PLANNING_DIRNAME } from "./planning-paths.js";
3
+ import { DEFAULT_PLANNING_RELATIVE_DIR, resolveCanonicalPlanningPath } from "./planning-paths.js";
4
4
  function normalizeKebab(value) {
5
5
  return value
6
6
  .trim()
@@ -259,25 +259,25 @@ export async function createPlanScaffold(store, input) {
259
259
  const title = input.title?.trim() || deriveTitle(changeId);
260
260
  const capabilityId = normalizeKebab(deriveCapabilityId(changeId));
261
261
  const artifactInputs = await loadArtifactPlanningInputs(store, selected);
262
- const changeDir = path.join(store.repoRoot, DEFAULT_PLANNING_DIRNAME, "changes", changeId);
262
+ const changeDir = resolveCanonicalPlanningPath(store.repoRoot, "changes", changeId);
263
263
  const specDir = path.join(changeDir, "specs", capabilityId);
264
264
  await ensureEmptyChangeDir(changeDir, input.overwrite ?? false);
265
265
  await fs.mkdir(specDir, { recursive: true });
266
266
  const files = [
267
267
  {
268
- rel: path.join(DEFAULT_PLANNING_DIRNAME, "changes", changeId, "proposal.md"),
268
+ rel: path.join(DEFAULT_PLANNING_RELATIVE_DIR, "changes", changeId, "proposal.md"),
269
269
  content: buildProposal(title, capabilityId, artifactInputs),
270
270
  },
271
271
  {
272
- rel: path.join(DEFAULT_PLANNING_DIRNAME, "changes", changeId, "tasks.md"),
272
+ rel: path.join(DEFAULT_PLANNING_RELATIVE_DIR, "changes", changeId, "tasks.md"),
273
273
  content: buildTasks(artifactInputs),
274
274
  },
275
275
  {
276
- rel: path.join(DEFAULT_PLANNING_DIRNAME, "changes", changeId, "design.md"),
276
+ rel: path.join(DEFAULT_PLANNING_RELATIVE_DIR, "changes", changeId, "design.md"),
277
277
  content: buildDesign(artifactInputs),
278
278
  },
279
279
  {
280
- rel: path.join(DEFAULT_PLANNING_DIRNAME, "changes", changeId, "specs", capabilityId, "spec.md"),
280
+ rel: path.join(DEFAULT_PLANNING_RELATIVE_DIR, "changes", changeId, "specs", capabilityId, "spec.md"),
281
281
  content: buildSpecDelta(deriveRequirementTitle(title), artifactInputs),
282
282
  },
283
283
  ];
@@ -1,17 +1,24 @@
1
1
  export declare const DEFAULT_PLANNING_DIRNAME = "reffyspec";
2
- export declare const LEGACY_PLANNING_DIRNAME = "openspec";
3
- export type PlanningMode = "canonical" | "legacy" | "dual" | "new";
2
+ export declare const DEFAULT_PLANNING_RELATIVE_DIR = ".reffy/reffyspec";
3
+ export declare const LEGACY_PLANNING_DIRNAME = "reffyspec";
4
+ export declare const COMPAT_PLANNING_DIRNAME = "openspec";
5
+ export type PlanningMode = "canonical" | "legacy" | "compat" | "dual" | "new";
4
6
  export interface PlanningState {
5
7
  canonicalDir: string;
6
8
  legacyDir: string;
9
+ compatDir: string;
7
10
  canonicalExists: boolean;
8
11
  legacyExists: boolean;
12
+ compatExists: boolean;
9
13
  mode: PlanningMode;
10
14
  activeDirName: string;
11
15
  activeDir: string;
12
16
  }
13
17
  export declare function resolveCanonicalPlanningDir(repoRoot: string): string;
14
18
  export declare function resolveLegacyPlanningDir(repoRoot: string): string;
19
+ export declare function resolveCompatPlanningDir(repoRoot: string): string;
20
+ export declare function resolveCanonicalPlanningPath(repoRoot: string, ...parts: string[]): string;
21
+ export declare function resolvePlanningPath(repoRoot: string, ...parts: string[]): string;
15
22
  export declare function detectPlanningState(repoRoot: string): PlanningState;
16
23
  export declare function resolvePlanningDirName(repoRoot: string): string;
17
24
  export declare function resolvePlanningDir(repoRoot: string): string;
@@ -1,7 +1,10 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
+ import { DEFAULT_REFS_DIRNAME } from "./refs-paths.js";
3
4
  export const DEFAULT_PLANNING_DIRNAME = "reffyspec";
4
- export const LEGACY_PLANNING_DIRNAME = "openspec";
5
+ export const DEFAULT_PLANNING_RELATIVE_DIR = `${DEFAULT_REFS_DIRNAME}/${DEFAULT_PLANNING_DIRNAME}`;
6
+ export const LEGACY_PLANNING_DIRNAME = "reffyspec";
7
+ export const COMPAT_PLANNING_DIRNAME = "openspec";
5
8
  function isDirectory(targetPath) {
6
9
  try {
7
10
  return statSync(targetPath).isDirectory();
@@ -11,18 +14,29 @@ function isDirectory(targetPath) {
11
14
  }
12
15
  }
13
16
  export function resolveCanonicalPlanningDir(repoRoot) {
14
- return path.join(repoRoot, DEFAULT_PLANNING_DIRNAME);
17
+ return path.join(repoRoot, DEFAULT_REFS_DIRNAME, DEFAULT_PLANNING_DIRNAME);
15
18
  }
16
19
  export function resolveLegacyPlanningDir(repoRoot) {
17
20
  return path.join(repoRoot, LEGACY_PLANNING_DIRNAME);
18
21
  }
22
+ export function resolveCompatPlanningDir(repoRoot) {
23
+ return path.join(repoRoot, COMPAT_PLANNING_DIRNAME);
24
+ }
25
+ export function resolveCanonicalPlanningPath(repoRoot, ...parts) {
26
+ return path.join(resolveCanonicalPlanningDir(repoRoot), ...parts);
27
+ }
28
+ export function resolvePlanningPath(repoRoot, ...parts) {
29
+ return path.join(resolvePlanningDir(repoRoot), ...parts);
30
+ }
19
31
  export function detectPlanningState(repoRoot) {
20
32
  const canonicalDir = resolveCanonicalPlanningDir(repoRoot);
21
33
  const legacyDir = resolveLegacyPlanningDir(repoRoot);
34
+ const compatDir = resolveCompatPlanningDir(repoRoot);
22
35
  const canonicalExists = isDirectory(canonicalDir);
23
36
  const legacyExists = isDirectory(legacyDir);
37
+ const compatExists = isDirectory(compatDir);
24
38
  let mode = "new";
25
- if (canonicalExists && legacyExists) {
39
+ if (canonicalExists && (legacyExists || compatExists)) {
26
40
  mode = "dual";
27
41
  }
28
42
  else if (canonicalExists) {
@@ -31,12 +45,21 @@ export function detectPlanningState(repoRoot) {
31
45
  else if (legacyExists) {
32
46
  mode = "legacy";
33
47
  }
34
- const activeDirName = mode === "legacy" ? LEGACY_PLANNING_DIRNAME : DEFAULT_PLANNING_DIRNAME;
48
+ else if (compatExists) {
49
+ mode = "compat";
50
+ }
51
+ const activeDirName = mode === "legacy"
52
+ ? LEGACY_PLANNING_DIRNAME
53
+ : mode === "compat"
54
+ ? COMPAT_PLANNING_DIRNAME
55
+ : DEFAULT_PLANNING_RELATIVE_DIR;
35
56
  return {
36
57
  canonicalDir,
37
58
  legacyDir,
59
+ compatDir,
38
60
  canonicalExists,
39
61
  legacyExists,
62
+ compatExists,
40
63
  mode,
41
64
  activeDirName,
42
65
  activeDir: path.join(repoRoot, activeDirName),
@@ -50,7 +73,7 @@ export function resolvePlanningDir(repoRoot) {
50
73
  }
51
74
  export function looksLikePlanningDir(targetPath) {
52
75
  const base = path.basename(targetPath);
53
- return ((base === DEFAULT_PLANNING_DIRNAME || base === LEGACY_PLANNING_DIRNAME) &&
76
+ return ((base === DEFAULT_PLANNING_DIRNAME || base === COMPAT_PLANNING_DIRNAME) &&
54
77
  existsSync(path.join(targetPath, "changes")) &&
55
78
  existsSync(path.join(targetPath, "specs")));
56
79
  }
@@ -1,6 +1,6 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
- import { DEFAULT_PLANNING_DIRNAME, LEGACY_PLANNING_DIRNAME, detectPlanningState, resolveCanonicalPlanningDir, } from "./planning-paths.js";
3
+ import { COMPAT_PLANNING_DIRNAME, DEFAULT_PLANNING_DIRNAME, detectPlanningState, resolveCanonicalPlanningDir, } from "./planning-paths.js";
4
4
  const DEFAULT_PROJECT_CONTEXT_TEMPLATE = `# Project Context
5
5
 
6
6
  ## Purpose
@@ -59,12 +59,32 @@ async function ensureCanonicalPlanningStructure(repoRoot) {
59
59
  export async function prepareCanonicalPlanningLayout(repoRoot) {
60
60
  const current = detectPlanningState(repoRoot);
61
61
  if (current.mode === "legacy") {
62
+ await fs.mkdir(path.dirname(current.canonicalDir), { recursive: true });
62
63
  await fs.rename(current.legacyDir, current.canonicalDir);
63
64
  return {
64
65
  state: detectPlanningState(repoRoot),
65
66
  migrated: true,
66
67
  created: false,
67
- message: `Migrated ${LEGACY_PLANNING_DIRNAME}/ to ${DEFAULT_PLANNING_DIRNAME}/`,
68
+ message: `Migrated reffyspec/ to ${DEFAULT_PLANNING_DIRNAME}/ within .reffy/`,
69
+ };
70
+ }
71
+ if (current.mode === "compat") {
72
+ await fs.mkdir(path.dirname(current.canonicalDir), { recursive: true });
73
+ await fs.rename(current.compatDir, current.canonicalDir);
74
+ return {
75
+ state: detectPlanningState(repoRoot),
76
+ migrated: true,
77
+ created: false,
78
+ message: `Migrated ${COMPAT_PLANNING_DIRNAME}/ to .reffy/${DEFAULT_PLANNING_DIRNAME}/`,
79
+ };
80
+ }
81
+ if (current.mode === "dual") {
82
+ await ensureCanonicalPlanningStructure(repoRoot);
83
+ return {
84
+ state: detectPlanningState(repoRoot),
85
+ migrated: false,
86
+ created: false,
87
+ message: `Using canonical .reffy/${DEFAULT_PLANNING_DIRNAME}/; legacy planning directories remain for manual cleanup`,
68
88
  };
69
89
  }
70
90
  const created = await ensureCanonicalPlanningStructure(repoRoot);
@@ -1,6 +1,6 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
- import { DEFAULT_PLANNING_DIRNAME } from "./planning-paths.js";
3
+ import { resolvePlanningPath } from "./planning-paths.js";
4
4
  const SPEC_HEADING_SUFFIX = " Specification";
5
5
  const PURPOSE_HEADING = "## Purpose";
6
6
  const REQUIREMENT_HEADING_PATTERN = /^###\s+Requirement:\s+(.+)$/;
@@ -46,7 +46,7 @@ function extractPurpose(content) {
46
46
  return purposeLines.length > 0 ? purposeLines.join(" ") : undefined;
47
47
  }
48
48
  async function buildSpecSummary(repoRoot, specId) {
49
- const specDir = path.join(repoRoot, DEFAULT_PLANNING_DIRNAME, "specs", specId);
49
+ const specDir = resolvePlanningPath(repoRoot, "specs", specId);
50
50
  const specPath = path.join(specDir, "spec.md");
51
51
  const designPath = path.join(specDir, "design.md");
52
52
  const content = await fs.readFile(specPath, "utf8");
@@ -60,7 +60,7 @@ async function buildSpecSummary(repoRoot, specId) {
60
60
  };
61
61
  }
62
62
  export async function listSpecs(repoRoot) {
63
- const specsRoot = path.join(repoRoot, DEFAULT_PLANNING_DIRNAME, "specs");
63
+ const specsRoot = resolvePlanningPath(repoRoot, "specs");
64
64
  const specIds = await listDirectories(specsRoot);
65
65
  const summaries = [];
66
66
  for (const specId of specIds) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reffy-cli",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "CLI-first, framework-agnostic references workflow for any repo (TypeScript)",
5
5
  "keywords": [
6
6
  "reffy",