reffy-cli 0.6.7 → 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/README.md +55 -15
- package/dist/cli.js +553 -32
- package/dist/diagram.js +2 -2
- package/dist/doctor.d.ts +1 -4
- package/dist/doctor.js +17 -17
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/manifest.js +13 -0
- package/dist/plan-archive.d.ts +8 -0
- package/dist/plan-archive.js +169 -0
- package/dist/plan-runtime.d.ts +35 -0
- package/dist/plan-runtime.js +218 -0
- package/dist/plan.d.ts +25 -0
- package/dist/plan.js +298 -0
- package/dist/planning-paths.d.ts +18 -0
- package/dist/planning-paths.js +56 -0
- package/dist/planning-workspace.d.ts +8 -0
- package/dist/planning-workspace.js +40 -0
- package/dist/refs-paths.d.ts +18 -0
- package/dist/refs-paths.js +57 -0
- package/dist/spec-runtime.d.ts +14 -0
- package/dist/spec-runtime.js +82 -0
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +52 -1
- package/dist/types.d.ts +3 -0
- package/dist/workspace.d.ts +8 -0
- package/dist/workspace.js +51 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -4,8 +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 { 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";
|
|
7
13
|
import { ReferencesStore } from "./storage.js";
|
|
14
|
+
import { listSpecs, showSpec } from "./spec-runtime.js";
|
|
8
15
|
import { summarizeArtifacts } from "./summarize.js";
|
|
16
|
+
import { prepareCanonicalWorkspace } from "./workspace.js";
|
|
9
17
|
const require = createRequire(import.meta.url);
|
|
10
18
|
const { version: packageVersion } = require("../package.json");
|
|
11
19
|
const REFFY_ASCII = [
|
|
@@ -16,36 +24,58 @@ const REFFY_ASCII = [
|
|
|
16
24
|
"|_| \\___||_| |_| \\__, |",
|
|
17
25
|
" |___/ ",
|
|
18
26
|
].join("\n");
|
|
19
|
-
|
|
27
|
+
function buildReffyBlock(refsDirName) {
|
|
28
|
+
return `<!-- REFFY:START -->
|
|
20
29
|
# Reffy Instructions
|
|
21
30
|
|
|
22
31
|
These instructions are for AI assistants working in this project.
|
|
23
32
|
|
|
24
|
-
Always open
|
|
33
|
+
Always open \`@/${refsDirName}/AGENTS.md\` when the request:
|
|
25
34
|
- Mentions early-stage ideation, exploration, brainstorming, or raw notes
|
|
26
35
|
- Needs context before drafting specs or proposals
|
|
27
36
|
- Refers to "reffy", "references", "explore", or "context layer"
|
|
28
37
|
|
|
29
|
-
Use
|
|
30
|
-
- Reffy workflow
|
|
31
|
-
- How Reffy
|
|
32
|
-
- How to store and consume ideation context in
|
|
38
|
+
Use \`@/${refsDirName}/AGENTS.md\` to learn:
|
|
39
|
+
- Reffy workflow for ideation, artifact indexing, and planning scaffolds
|
|
40
|
+
- How Reffy owns the runtime while preserving ReffySpec planning files
|
|
41
|
+
- How to store and consume ideation context in \`${refsDirName}/\`
|
|
33
42
|
|
|
34
43
|
Keep this managed block so \`reffy init\` can refresh the instructions.
|
|
35
44
|
|
|
36
45
|
<!-- REFFY:END -->`;
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
}
|
|
67
|
+
function buildReffyAgentsContent(refsDirName) {
|
|
68
|
+
return `# Reffy Instructions
|
|
39
69
|
|
|
40
70
|
These instructions are for AI assistants working in this project.
|
|
41
71
|
|
|
42
72
|
## TL;DR Checklist
|
|
43
73
|
|
|
44
74
|
- Decide whether Reffy ideation is needed for this request.
|
|
45
|
-
- If needed, read existing context in
|
|
75
|
+
- If needed, read existing context in \`${refsDirName}/artifacts/\`.
|
|
46
76
|
- Add/update exploratory artifacts and keep them concise.
|
|
47
77
|
- Run \`reffy reindex\` and \`reffy validate\` after artifact changes.
|
|
48
|
-
-
|
|
78
|
+
- Use \`reffy summarize --output json\` and \`reffy plan create\` to turn artifact context into planning scaffolds.
|
|
49
79
|
|
|
50
80
|
## When To Use Reffy
|
|
51
81
|
|
|
@@ -63,21 +93,24 @@ You can skip Reffy when the request is:
|
|
|
63
93
|
|
|
64
94
|
## Reffy Workflow
|
|
65
95
|
|
|
66
|
-
1. Read existing artifacts in
|
|
96
|
+
1. Read existing artifacts in \`${refsDirName}/artifacts/\`.
|
|
67
97
|
2. Add or update artifacts to capture exploratory context.
|
|
68
|
-
3. Run \`reffy reindex\` to index newly added files into
|
|
98
|
+
3. Run \`reffy reindex\` to index newly added files into \`${refsDirName}/manifest.json\`.
|
|
69
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.
|
|
70
101
|
|
|
71
|
-
## Relationship To
|
|
102
|
+
## Relationship To ReffySpec
|
|
72
103
|
|
|
73
|
-
- Reffy
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
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.
|
|
77
110
|
|
|
78
|
-
##
|
|
111
|
+
## ReffySpec Citation Rules
|
|
79
112
|
|
|
80
|
-
When
|
|
113
|
+
When a ReffySpec proposal is informed by Reffy artifacts:
|
|
81
114
|
- After ideation approval, run \`reffy summarize --output json\` to shortlist candidate artifacts.
|
|
82
115
|
- Include a short "Reffy References" subsection in \`proposal.md\` (or design notes if more appropriate).
|
|
83
116
|
- Cite only artifact filenames that directly informed the proposal's problem, scope, decisions, or constraints.
|
|
@@ -89,7 +122,7 @@ When an OpenSpec proposal is informed by Reffy artifacts:
|
|
|
89
122
|
|
|
90
123
|
### Reusable Proposal Snippet
|
|
91
124
|
|
|
92
|
-
Use this in
|
|
125
|
+
Use this in \`${DEFAULT_PLANNING_DIRNAME}/changes/<change-id>/proposal.md\`:
|
|
93
126
|
|
|
94
127
|
\`\`\`md
|
|
95
128
|
## Reffy References
|
|
@@ -105,30 +138,103 @@ No Reffy references used.
|
|
|
105
138
|
|
|
106
139
|
## Artifact Conventions
|
|
107
140
|
|
|
108
|
-
- Treat
|
|
141
|
+
- Treat \`${refsDirName}/\` as a repository-local guidance and ideation context layer.
|
|
109
142
|
- Keep artifact names clear and stable.
|
|
110
143
|
- Prefer markdown notes for exploratory content.
|
|
111
144
|
- Keep manifests machine-readable and schema-compliant (version 1).
|
|
112
145
|
`;
|
|
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
|
+
}
|
|
113
194
|
const REFFY_START = "<!-- REFFY:START -->";
|
|
114
195
|
const REFFY_END = "<!-- REFFY:END -->";
|
|
196
|
+
const REFFYSPEC_START = "<!-- REFFYSPEC:START -->";
|
|
197
|
+
const REFFYSPEC_END = "<!-- REFFYSPEC:END -->";
|
|
115
198
|
const OPENSPEC_START = "<!-- OPENSPEC:START -->";
|
|
199
|
+
const OPENSPEC_END = "<!-- OPENSPEC:END -->";
|
|
116
200
|
function upsertReffyBlock(content) {
|
|
201
|
+
return upsertReffyBlockForDir(content, DEFAULT_REFS_DIRNAME);
|
|
202
|
+
}
|
|
203
|
+
function upsertReffyBlockForDir(content, refsDirName) {
|
|
204
|
+
const reffyBlock = buildReffyBlock(refsDirName);
|
|
117
205
|
if (content.includes(REFFY_START) && content.includes(REFFY_END)) {
|
|
118
206
|
const prefix = content.split(REFFY_START)[0] ?? "";
|
|
119
207
|
const suffix = content.split(REFFY_END, 2)[1] ?? "";
|
|
120
208
|
const trimmedSuffix = suffix.trimStart();
|
|
121
|
-
return trimmedSuffix.length > 0 ? `${prefix}${
|
|
209
|
+
return trimmedSuffix.length > 0 ? `${prefix}${reffyBlock}\n\n${trimmedSuffix}` : `${prefix}${reffyBlock}\n`;
|
|
122
210
|
}
|
|
123
211
|
if (content.includes(OPENSPEC_START)) {
|
|
124
212
|
const [before, after] = content.split(OPENSPEC_START, 2);
|
|
125
|
-
return `${before.trimEnd()}\n\n${
|
|
213
|
+
return `${before.trimEnd()}\n\n${reffyBlock}\n\n${OPENSPEC_START}${after}`;
|
|
126
214
|
}
|
|
127
|
-
return content.trim().length > 0 ? `${
|
|
215
|
+
return content.trim().length > 0 ? `${reffyBlock}\n\n${content.trimStart()}` : `${reffyBlock}\n`;
|
|
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`;
|
|
128
232
|
}
|
|
129
233
|
async function initAgents(repoRoot) {
|
|
234
|
+
const refsDirName = DEFAULT_REFS_DIRNAME;
|
|
130
235
|
const agentsPath = path.join(repoRoot, "AGENTS.md");
|
|
131
|
-
const reffyAgentsPath = path.join(repoRoot,
|
|
236
|
+
const reffyAgentsPath = path.join(repoRoot, refsDirName, "AGENTS.md");
|
|
237
|
+
const reffyspecAgentsPath = path.join(repoRoot, DEFAULT_PLANNING_DIRNAME, "AGENTS.md");
|
|
132
238
|
let content = "";
|
|
133
239
|
try {
|
|
134
240
|
content = await fs.readFile(agentsPath, "utf8");
|
|
@@ -136,11 +242,13 @@ async function initAgents(repoRoot) {
|
|
|
136
242
|
catch {
|
|
137
243
|
content = "";
|
|
138
244
|
}
|
|
139
|
-
const updated =
|
|
245
|
+
const updated = upsertPlanningBlock(upsertReffyBlockForDir(content, refsDirName));
|
|
140
246
|
await fs.mkdir(path.dirname(reffyAgentsPath), { recursive: true });
|
|
247
|
+
await fs.mkdir(path.dirname(reffyspecAgentsPath), { recursive: true });
|
|
141
248
|
await fs.writeFile(agentsPath, updated, "utf8");
|
|
142
|
-
await fs.writeFile(reffyAgentsPath,
|
|
143
|
-
|
|
249
|
+
await fs.writeFile(reffyAgentsPath, buildReffyAgentsContent(refsDirName), "utf8");
|
|
250
|
+
await fs.writeFile(reffyspecAgentsPath, buildReffySpecAgentsContent(), "utf8");
|
|
251
|
+
return { root_agents_path: agentsPath, reffy_agents_path: reffyAgentsPath, reffyspec_agents_path: reffyspecAgentsPath };
|
|
144
252
|
}
|
|
145
253
|
function pathExists(targetPath) {
|
|
146
254
|
return existsSync(targetPath);
|
|
@@ -156,10 +264,10 @@ function isDirectory(targetPath) {
|
|
|
156
264
|
function discoverRepoRoot(startDir) {
|
|
157
265
|
let current = path.resolve(startDir);
|
|
158
266
|
while (true) {
|
|
159
|
-
if (
|
|
267
|
+
if (looksLikeRefsDir(current)) {
|
|
160
268
|
return path.dirname(current);
|
|
161
269
|
}
|
|
162
|
-
if (isDirectory(path.join(current, ".
|
|
270
|
+
if (isDirectory(path.join(current, DEFAULT_REFS_DIRNAME)) || isDirectory(path.join(current, ".references"))) {
|
|
163
271
|
return current;
|
|
164
272
|
}
|
|
165
273
|
if (pathExists(path.join(current, "AGENTS.md")) || pathExists(path.join(current, ".git"))) {
|
|
@@ -190,6 +298,30 @@ function parseRepoArg(argv) {
|
|
|
190
298
|
}
|
|
191
299
|
return discoverRepoRoot(process.cwd());
|
|
192
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
|
+
}
|
|
193
325
|
function parseOutputMode(argv) {
|
|
194
326
|
for (let i = 0; i < argv.length; i += 1) {
|
|
195
327
|
const arg = argv[i];
|
|
@@ -233,10 +365,13 @@ function usage() {
|
|
|
233
365
|
"Commands:",
|
|
234
366
|
" init Ensure root AGENTS.md block and .reffy/AGENTS.md are up to date.",
|
|
235
367
|
" bootstrap Run init, ensure .reffy structure exists, then reindex artifacts.",
|
|
368
|
+
" migrate Migrate a legacy .references workspace to the canonical .reffy layout.",
|
|
236
369
|
" doctor Diagnose required Reffy setup and optional tool availability.",
|
|
237
370
|
" reindex Scan .reffy/artifacts and add missing files to manifest.",
|
|
238
371
|
" validate Validate .reffy/manifest.json against manifest v1 contract.",
|
|
239
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.",
|
|
240
375
|
" diagram Render Mermaid diagrams (supports SVG and ASCII).",
|
|
241
376
|
].join("\n");
|
|
242
377
|
}
|
|
@@ -245,7 +380,7 @@ function diagramUsage() {
|
|
|
245
380
|
"Usage: reffy diagram render [--repo PATH] [--input PATH|--stdin] [--format svg|ascii] [--output PATH]",
|
|
246
381
|
"",
|
|
247
382
|
"Options:",
|
|
248
|
-
" --input PATH Read Mermaid (or
|
|
383
|
+
" --input PATH Read Mermaid (or ReffySpec spec.md) from file",
|
|
249
384
|
" --stdin Read Mermaid text from stdin",
|
|
250
385
|
" --format VALUE Output format: svg (default) or ascii",
|
|
251
386
|
" --output PATH Write rendered result to file instead of stdout",
|
|
@@ -260,6 +395,30 @@ function diagramUsage() {
|
|
|
260
395
|
" --font NAME SVG font family override",
|
|
261
396
|
].join("\n");
|
|
262
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
|
+
}
|
|
263
422
|
function parseDiagramArgs(argv) {
|
|
264
423
|
const repoRoot = parseRepoArg(argv);
|
|
265
424
|
const args = {
|
|
@@ -359,6 +518,83 @@ function parseDiagramArgs(argv) {
|
|
|
359
518
|
}
|
|
360
519
|
return args;
|
|
361
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
|
+
}
|
|
362
598
|
function printSection(title, values) {
|
|
363
599
|
console.log(`${title}:`);
|
|
364
600
|
if (values.length === 0) {
|
|
@@ -391,6 +627,7 @@ async function main() {
|
|
|
391
627
|
return 1;
|
|
392
628
|
}
|
|
393
629
|
const parsed = parseDiagramArgs(diagramArgs);
|
|
630
|
+
await prepareCanonicalPlanningLayout(parsed.repoRoot);
|
|
394
631
|
const rendered = await renderDiagram({
|
|
395
632
|
repoRoot: parsed.repoRoot,
|
|
396
633
|
inputPath: parsed.inputPath,
|
|
@@ -417,29 +654,271 @@ async function main() {
|
|
|
417
654
|
}
|
|
418
655
|
return 0;
|
|
419
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
|
+
}
|
|
420
872
|
const output = parseOutputMode(rest);
|
|
421
873
|
if (command === "init") {
|
|
422
874
|
printBanner(output);
|
|
423
875
|
const repoRoot = parseRepoArg(rest);
|
|
876
|
+
const workspace = await prepareCanonicalWorkspace(repoRoot);
|
|
877
|
+
const planning = await prepareCanonicalPlanningLayout(repoRoot);
|
|
424
878
|
const agents = await initAgents(repoRoot);
|
|
425
|
-
const payload = {
|
|
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
|
+
};
|
|
426
890
|
if (output === "json") {
|
|
427
891
|
printResult(output, payload);
|
|
428
892
|
}
|
|
429
893
|
else {
|
|
894
|
+
if (workspace.message) {
|
|
895
|
+
console.log(workspace.message);
|
|
896
|
+
}
|
|
897
|
+
if (planning.message) {
|
|
898
|
+
console.log(planning.message);
|
|
899
|
+
}
|
|
430
900
|
console.log(`Updated ${agents.root_agents_path}`);
|
|
431
901
|
console.log(`Updated ${agents.reffy_agents_path}`);
|
|
902
|
+
console.log(`Updated ${agents.reffyspec_agents_path}`);
|
|
432
903
|
}
|
|
433
904
|
return 0;
|
|
434
905
|
}
|
|
435
906
|
if (command === "bootstrap") {
|
|
436
907
|
const repoRoot = parseRepoArg(rest);
|
|
908
|
+
const workspace = await prepareCanonicalWorkspace(repoRoot);
|
|
909
|
+
const planning = await prepareCanonicalPlanningLayout(repoRoot);
|
|
437
910
|
const agents = await initAgents(repoRoot);
|
|
438
911
|
const store = new ReferencesStore(repoRoot);
|
|
439
912
|
const reindex = await store.reindexArtifacts();
|
|
440
913
|
const payload = {
|
|
441
914
|
status: "ok",
|
|
442
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,
|
|
443
922
|
...agents,
|
|
444
923
|
refs_dir: store.refsDir,
|
|
445
924
|
manifest_path: store.manifestPath,
|
|
@@ -449,15 +928,54 @@ async function main() {
|
|
|
449
928
|
printResult(output, payload);
|
|
450
929
|
}
|
|
451
930
|
else {
|
|
931
|
+
if (workspace.message) {
|
|
932
|
+
console.log(workspace.message);
|
|
933
|
+
}
|
|
934
|
+
if (planning.message) {
|
|
935
|
+
console.log(planning.message);
|
|
936
|
+
}
|
|
452
937
|
console.log(`Bootstrapped ${store.refsDir}`);
|
|
453
938
|
console.log(`Updated ${agents.root_agents_path}`);
|
|
454
939
|
console.log(`Updated ${agents.reffy_agents_path}`);
|
|
940
|
+
console.log(`Updated ${agents.reffyspec_agents_path}`);
|
|
455
941
|
console.log(`Reindex: added=${String(reindex.added)} removed=${String(reindex.removed)} total=${String(reindex.total)}`);
|
|
456
942
|
}
|
|
457
943
|
return 0;
|
|
458
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
|
+
}
|
|
459
976
|
if (command === "reindex") {
|
|
460
977
|
const repoRoot = parseRepoArg(rest);
|
|
978
|
+
await prepareCanonicalPlanningLayout(repoRoot);
|
|
461
979
|
const store = new ReferencesStore(repoRoot);
|
|
462
980
|
const reindex = await store.reindexArtifacts();
|
|
463
981
|
const payload = { status: "ok", command: "reindex", ...reindex };
|
|
@@ -471,6 +989,7 @@ async function main() {
|
|
|
471
989
|
}
|
|
472
990
|
if (command === "doctor") {
|
|
473
991
|
const repoRoot = parseRepoArg(rest);
|
|
992
|
+
await prepareCanonicalPlanningLayout(repoRoot);
|
|
474
993
|
const report = await runDoctor(repoRoot);
|
|
475
994
|
const status = report.summary.required_failed > 0 ? "error" : "ok";
|
|
476
995
|
const payload = { status, command: "doctor", ...report };
|
|
@@ -496,6 +1015,7 @@ async function main() {
|
|
|
496
1015
|
}
|
|
497
1016
|
if (command === "validate") {
|
|
498
1017
|
const repoRoot = parseRepoArg(rest);
|
|
1018
|
+
await prepareCanonicalPlanningLayout(repoRoot);
|
|
499
1019
|
const store = new ReferencesStore(repoRoot);
|
|
500
1020
|
const result = await store.validateManifest();
|
|
501
1021
|
const payload = { status: result.ok ? "ok" : "error", command: "validate", ...result };
|
|
@@ -523,6 +1043,7 @@ async function main() {
|
|
|
523
1043
|
}
|
|
524
1044
|
if (command === "summarize") {
|
|
525
1045
|
const repoRoot = parseRepoArg(rest);
|
|
1046
|
+
await prepareCanonicalPlanningLayout(repoRoot);
|
|
526
1047
|
const store = new ReferencesStore(repoRoot);
|
|
527
1048
|
const validation = await store.validateManifest();
|
|
528
1049
|
if (!validation.ok) {
|