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 +10 -10
- package/dist/cli.js +27 -23
- package/dist/plan-archive.js +4 -4
- package/dist/plan-runtime.js +3 -3
- package/dist/plan.js +6 -6
- package/dist/planning-paths.d.ts +9 -2
- package/dist/planning-paths.js +28 -5
- package/dist/planning-workspace.js +22 -2
- package/dist/spec-runtime.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](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
|
|
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
|
|
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
|
|
35
|
-
- `reffy spec list|show`: inspects current spec state under
|
|
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
|
|
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
|
|
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
|
-
-
|
|
78
|
-
-
|
|
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 {
|
|
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
|
-
|
|
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 \`@/${
|
|
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 \`@/${
|
|
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 \`${
|
|
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 \`${
|
|
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 \`${
|
|
159
|
-
- Review active changes in \`${
|
|
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 \`${
|
|
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 \`${
|
|
166
|
-
2. Inspect current specs in \`${
|
|
167
|
-
3. Inspect active changes in \`${
|
|
168
|
-
4. Draft or update proposal/design/tasks/spec files under \`${
|
|
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
|
-
- \`${
|
|
181
|
-
- \`${
|
|
182
|
-
- \`${
|
|
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 =
|
|
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 ${
|
|
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
|
-
|
|
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) {
|
package/dist/plan-archive.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
}
|
package/dist/plan-runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
];
|
package/dist/planning-paths.d.ts
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
export declare const DEFAULT_PLANNING_DIRNAME = "reffyspec";
|
|
2
|
-
export declare const
|
|
3
|
-
export
|
|
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;
|
package/dist/planning-paths.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 ===
|
|
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 {
|
|
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
|
|
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);
|
package/dist/spec-runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
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 =
|
|
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 =
|
|
63
|
+
const specsRoot = resolvePlanningPath(repoRoot, "specs");
|
|
64
64
|
const specIds = await listDirectories(specsRoot);
|
|
65
65
|
const summaries = [];
|
|
66
66
|
for (const specId of specIds) {
|