reffy-cli 0.7.0 → 1.1.1
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 +35 -24
- package/dist/cli.js +539 -36
- package/dist/diagram.js +2 -2
- package/dist/doctor.d.ts +1 -4
- package/dist/doctor.js +8 -12
- 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 +76 -0
- package/dist/refs-paths.d.ts +13 -0
- package/dist/refs-paths.js +34 -9
- 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 +50 -0
- package/dist/types.d.ts +3 -0
- package/dist/workspace.d.ts +8 -0
- package/dist/workspace.js +51 -0
- package/package.json +14 -2
package/dist/cli.js
CHANGED
|
@@ -4,9 +4,16 @@ import { existsSync, promises as fs, statSync } from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { renderDiagram } from "./diagram.js";
|
|
6
6
|
import { runDoctor } from "./doctor.js";
|
|
7
|
-
import {
|
|
7
|
+
import { archivePlanningChange } from "./plan-archive.js";
|
|
8
|
+
import { createPlanScaffold } from "./plan.js";
|
|
9
|
+
import { DEFAULT_PLANNING_DIRNAME } from "./planning-paths.js";
|
|
10
|
+
import { listPlanningChanges, showPlanningChange, validatePlanningChange } from "./plan-runtime.js";
|
|
11
|
+
import { DEFAULT_REFS_DIRNAME, looksLikeRefsDir } from "./refs-paths.js";
|
|
12
|
+
import { prepareCanonicalPlanningLayout } from "./planning-workspace.js";
|
|
8
13
|
import { ReferencesStore } from "./storage.js";
|
|
14
|
+
import { listSpecs, showSpec } from "./spec-runtime.js";
|
|
9
15
|
import { summarizeArtifacts } from "./summarize.js";
|
|
16
|
+
import { prepareCanonicalWorkspace } from "./workspace.js";
|
|
10
17
|
const require = createRequire(import.meta.url);
|
|
11
18
|
const { version: packageVersion } = require("../package.json");
|
|
12
19
|
const REFFY_ASCII = [
|
|
@@ -17,6 +24,10 @@ const REFFY_ASCII = [
|
|
|
17
24
|
"|_| \\___||_| |_| \\__, |",
|
|
18
25
|
" |___/ ",
|
|
19
26
|
].join("\n");
|
|
27
|
+
const BOOTSTRAP_AGENT_INSTRUCTION = [
|
|
28
|
+
'Please read `AGENTS.md` and help me fill out the project context template in `reffyspec/project.md`',
|
|
29
|
+
"with details about my project, tech stack, architecture, and conventions.",
|
|
30
|
+
].join(" ");
|
|
20
31
|
function buildReffyBlock(refsDirName) {
|
|
21
32
|
return `<!-- REFFY:START -->
|
|
22
33
|
# Reffy Instructions
|
|
@@ -29,14 +40,34 @@ Always open \`@/${refsDirName}/AGENTS.md\` when the request:
|
|
|
29
40
|
- Refers to "reffy", "references", "explore", or "context layer"
|
|
30
41
|
|
|
31
42
|
Use \`@/${refsDirName}/AGENTS.md\` to learn:
|
|
32
|
-
- Reffy workflow
|
|
33
|
-
- How Reffy
|
|
43
|
+
- Reffy workflow for ideation, artifact indexing, and planning scaffolds
|
|
44
|
+
- How Reffy owns the runtime while preserving ReffySpec planning files
|
|
34
45
|
- How to store and consume ideation context in \`${refsDirName}/\`
|
|
35
46
|
|
|
36
47
|
Keep this managed block so \`reffy init\` can refresh the instructions.
|
|
37
48
|
|
|
38
49
|
<!-- REFFY:END -->`;
|
|
39
50
|
}
|
|
51
|
+
function buildReffySpecBlock() {
|
|
52
|
+
return `<!-- REFFYSPEC:START -->
|
|
53
|
+
# ReffySpec Instructions
|
|
54
|
+
|
|
55
|
+
These instructions are for AI assistants working in this project.
|
|
56
|
+
|
|
57
|
+
Always open \`@/${DEFAULT_PLANNING_DIRNAME}/AGENTS.md\` when the request:
|
|
58
|
+
- Mentions planning or proposals (words like proposal, spec, change, plan)
|
|
59
|
+
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
|
|
60
|
+
- Needs the authoritative planning/spec workflow for this repo
|
|
61
|
+
|
|
62
|
+
Use \`@/${DEFAULT_PLANNING_DIRNAME}/AGENTS.md\` to learn:
|
|
63
|
+
- How to create and apply ReffySpec change proposals
|
|
64
|
+
- ReffySpec format and conventions
|
|
65
|
+
- Project structure and planning guidelines
|
|
66
|
+
|
|
67
|
+
Keep this managed block so \`reffy init\` can refresh the instructions.
|
|
68
|
+
|
|
69
|
+
<!-- REFFYSPEC:END -->`;
|
|
70
|
+
}
|
|
40
71
|
function buildReffyAgentsContent(refsDirName) {
|
|
41
72
|
return `# Reffy Instructions
|
|
42
73
|
|
|
@@ -48,7 +79,7 @@ These instructions are for AI assistants working in this project.
|
|
|
48
79
|
- If needed, read existing context in \`${refsDirName}/artifacts/\`.
|
|
49
80
|
- Add/update exploratory artifacts and keep them concise.
|
|
50
81
|
- Run \`reffy reindex\` and \`reffy validate\` after artifact changes.
|
|
51
|
-
-
|
|
82
|
+
- Use \`reffy summarize --output json\` and \`reffy plan create\` to turn artifact context into planning scaffolds.
|
|
52
83
|
|
|
53
84
|
## When To Use Reffy
|
|
54
85
|
|
|
@@ -70,17 +101,20 @@ You can skip Reffy when the request is:
|
|
|
70
101
|
2. Add or update artifacts to capture exploratory context.
|
|
71
102
|
3. Run \`reffy reindex\` to index newly added files into \`${refsDirName}/manifest.json\`.
|
|
72
103
|
4. Run \`reffy validate\` to verify manifest contract compliance.
|
|
104
|
+
5. Run \`reffy plan create\` to generate proposal/tasks/spec scaffolds from selected artifacts when planning is ready.
|
|
73
105
|
|
|
74
|
-
## Relationship To
|
|
106
|
+
## Relationship To ReffySpec
|
|
75
107
|
|
|
76
|
-
- Reffy
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
108
|
+
- Reffy owns ideation artifacts, manifest metadata, and native planning scaffolds.
|
|
109
|
+
- ReffySpec is the planning subsystem inside Reffy.
|
|
110
|
+
- The vendored fork at \`/.vendor/ReffySpec\` is reference-only for v1; first-party behavior lives in this repo.
|
|
111
|
+
- Reffy is the primary runtime authority for this project.
|
|
112
|
+
- ReffySpec files live under \`${DEFAULT_PLANNING_DIRNAME}/\` as the canonical planning layout.
|
|
113
|
+
- Do not duplicate full proposal/spec content in Reffy artifacts; generate and link planning outputs from them.
|
|
80
114
|
|
|
81
|
-
##
|
|
115
|
+
## ReffySpec Citation Rules
|
|
82
116
|
|
|
83
|
-
When
|
|
117
|
+
When a ReffySpec proposal is informed by Reffy artifacts:
|
|
84
118
|
- After ideation approval, run \`reffy summarize --output json\` to shortlist candidate artifacts.
|
|
85
119
|
- Include a short "Reffy References" subsection in \`proposal.md\` (or design notes if more appropriate).
|
|
86
120
|
- Cite only artifact filenames that directly informed the proposal's problem, scope, decisions, or constraints.
|
|
@@ -92,7 +126,7 @@ When an OpenSpec proposal is informed by Reffy artifacts:
|
|
|
92
126
|
|
|
93
127
|
### Reusable Proposal Snippet
|
|
94
128
|
|
|
95
|
-
Use this in
|
|
129
|
+
Use this in \`${DEFAULT_PLANNING_DIRNAME}/changes/<change-id>/proposal.md\`:
|
|
96
130
|
|
|
97
131
|
\`\`\`md
|
|
98
132
|
## Reffy References
|
|
@@ -114,9 +148,59 @@ No Reffy references used.
|
|
|
114
148
|
- Keep manifests machine-readable and schema-compliant (version 1).
|
|
115
149
|
`;
|
|
116
150
|
}
|
|
151
|
+
function buildReffySpecAgentsContent() {
|
|
152
|
+
return `# ReffySpec Instructions
|
|
153
|
+
|
|
154
|
+
These instructions are for AI assistants working in this project.
|
|
155
|
+
|
|
156
|
+
## TL;DR Checklist
|
|
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.
|
|
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/\`.
|
|
162
|
+
|
|
163
|
+
## ReffySpec Workflow
|
|
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>/\`.
|
|
169
|
+
5. Use native Reffy commands for routine planning workflow:
|
|
170
|
+
- \`reffy plan create\`
|
|
171
|
+
- \`reffy plan validate\`
|
|
172
|
+
- \`reffy plan list\`
|
|
173
|
+
- \`reffy plan show\`
|
|
174
|
+
- \`reffy plan archive\`
|
|
175
|
+
- \`reffy spec list\`
|
|
176
|
+
- \`reffy spec show\`
|
|
177
|
+
|
|
178
|
+
## Directory Model
|
|
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.
|
|
183
|
+
|
|
184
|
+
## Proposal Rules
|
|
185
|
+
|
|
186
|
+
- Use a unique verb-led \`change-id\` in kebab-case.
|
|
187
|
+
- Include \`proposal.md\`, \`tasks.md\`, optional \`design.md\`, and delta specs per affected capability.
|
|
188
|
+
- Delta specs must use \`## ADDED|MODIFIED|REMOVED|RENAMED Requirements\`.
|
|
189
|
+
- Each requirement must include at least one \`#### Scenario:\`.
|
|
190
|
+
|
|
191
|
+
## Reffy Relationship
|
|
192
|
+
|
|
193
|
+
- Reffy owns the runtime and artifact workflow.
|
|
194
|
+
- ReffySpec is the canonical planning/spec surface.
|
|
195
|
+
- Reffy artifacts in \`.reffy/\` should inform proposal/design content without duplicating the full planning files.
|
|
196
|
+
`;
|
|
197
|
+
}
|
|
117
198
|
const REFFY_START = "<!-- REFFY:START -->";
|
|
118
199
|
const REFFY_END = "<!-- REFFY:END -->";
|
|
200
|
+
const REFFYSPEC_START = "<!-- REFFYSPEC:START -->";
|
|
201
|
+
const REFFYSPEC_END = "<!-- REFFYSPEC:END -->";
|
|
119
202
|
const OPENSPEC_START = "<!-- OPENSPEC:START -->";
|
|
203
|
+
const OPENSPEC_END = "<!-- OPENSPEC:END -->";
|
|
120
204
|
function upsertReffyBlock(content) {
|
|
121
205
|
return upsertReffyBlockForDir(content, DEFAULT_REFS_DIRNAME);
|
|
122
206
|
}
|
|
@@ -134,10 +218,27 @@ function upsertReffyBlockForDir(content, refsDirName) {
|
|
|
134
218
|
}
|
|
135
219
|
return content.trim().length > 0 ? `${reffyBlock}\n\n${content.trimStart()}` : `${reffyBlock}\n`;
|
|
136
220
|
}
|
|
221
|
+
function upsertPlanningBlock(content) {
|
|
222
|
+
const reffyspecBlock = buildReffySpecBlock();
|
|
223
|
+
if (content.includes(REFFYSPEC_START) && content.includes(REFFYSPEC_END)) {
|
|
224
|
+
const prefix = content.split(REFFYSPEC_START)[0] ?? "";
|
|
225
|
+
const suffix = content.split(REFFYSPEC_END, 2)[1] ?? "";
|
|
226
|
+
const trimmedSuffix = suffix.trimStart();
|
|
227
|
+
return trimmedSuffix.length > 0 ? `${prefix}${reffyspecBlock}\n\n${trimmedSuffix}` : `${prefix}${reffyspecBlock}\n`;
|
|
228
|
+
}
|
|
229
|
+
if (content.includes(OPENSPEC_START) && content.includes(OPENSPEC_END)) {
|
|
230
|
+
const prefix = content.split(OPENSPEC_START)[0] ?? "";
|
|
231
|
+
const suffix = content.split(OPENSPEC_END, 2)[1] ?? "";
|
|
232
|
+
const trimmedSuffix = suffix.trimStart();
|
|
233
|
+
return trimmedSuffix.length > 0 ? `${prefix}${reffyspecBlock}\n\n${trimmedSuffix}` : `${prefix}${reffyspecBlock}\n`;
|
|
234
|
+
}
|
|
235
|
+
return content.trim().length > 0 ? `${content.trimEnd()}\n\n${reffyspecBlock}\n` : `${reffyspecBlock}\n`;
|
|
236
|
+
}
|
|
137
237
|
async function initAgents(repoRoot) {
|
|
138
|
-
const refsDirName =
|
|
238
|
+
const refsDirName = DEFAULT_REFS_DIRNAME;
|
|
139
239
|
const agentsPath = path.join(repoRoot, "AGENTS.md");
|
|
140
240
|
const reffyAgentsPath = path.join(repoRoot, refsDirName, "AGENTS.md");
|
|
241
|
+
const reffyspecAgentsPath = path.join(repoRoot, DEFAULT_PLANNING_DIRNAME, "AGENTS.md");
|
|
141
242
|
let content = "";
|
|
142
243
|
try {
|
|
143
244
|
content = await fs.readFile(agentsPath, "utf8");
|
|
@@ -145,11 +246,13 @@ async function initAgents(repoRoot) {
|
|
|
145
246
|
catch {
|
|
146
247
|
content = "";
|
|
147
248
|
}
|
|
148
|
-
const updated = upsertReffyBlockForDir(content, refsDirName);
|
|
249
|
+
const updated = upsertPlanningBlock(upsertReffyBlockForDir(content, refsDirName));
|
|
149
250
|
await fs.mkdir(path.dirname(reffyAgentsPath), { recursive: true });
|
|
251
|
+
await fs.mkdir(path.dirname(reffyspecAgentsPath), { recursive: true });
|
|
150
252
|
await fs.writeFile(agentsPath, updated, "utf8");
|
|
151
253
|
await fs.writeFile(reffyAgentsPath, buildReffyAgentsContent(refsDirName), "utf8");
|
|
152
|
-
|
|
254
|
+
await fs.writeFile(reffyspecAgentsPath, buildReffySpecAgentsContent(), "utf8");
|
|
255
|
+
return { root_agents_path: agentsPath, reffy_agents_path: reffyAgentsPath, reffyspec_agents_path: reffyspecAgentsPath };
|
|
153
256
|
}
|
|
154
257
|
function pathExists(targetPath) {
|
|
155
258
|
return existsSync(targetPath);
|
|
@@ -199,6 +302,30 @@ function parseRepoArg(argv) {
|
|
|
199
302
|
}
|
|
200
303
|
return discoverRepoRoot(process.cwd());
|
|
201
304
|
}
|
|
305
|
+
function getPlanPositionalArgs(argv) {
|
|
306
|
+
const positionals = [];
|
|
307
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
308
|
+
const arg = argv[i];
|
|
309
|
+
if (arg === "--repo" || arg === "--output" || arg === "--change-id" || arg === "--title" || arg === "--artifacts") {
|
|
310
|
+
i += 1;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (arg.startsWith("--repo=") ||
|
|
314
|
+
arg.startsWith("--output=") ||
|
|
315
|
+
arg.startsWith("--change-id=") ||
|
|
316
|
+
arg.startsWith("--title=") ||
|
|
317
|
+
arg.startsWith("--artifacts=") ||
|
|
318
|
+
arg === "--json" ||
|
|
319
|
+
arg === "--all" ||
|
|
320
|
+
arg === "--force") {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (arg.startsWith("--"))
|
|
324
|
+
continue;
|
|
325
|
+
positionals.push(arg);
|
|
326
|
+
}
|
|
327
|
+
return positionals;
|
|
328
|
+
}
|
|
202
329
|
function parseOutputMode(argv) {
|
|
203
330
|
for (let i = 0; i < argv.length; i += 1) {
|
|
204
331
|
const arg = argv[i];
|
|
@@ -232,6 +359,56 @@ function printBanner(mode) {
|
|
|
232
359
|
console.log("");
|
|
233
360
|
}
|
|
234
361
|
}
|
|
362
|
+
function shouldPrintBootstrapOnboarding(workspaceCreated, workspaceMigrated, planningCreated, planningMigrated) {
|
|
363
|
+
return workspaceCreated || workspaceMigrated || planningCreated || planningMigrated;
|
|
364
|
+
}
|
|
365
|
+
function printBootstrapOnboarding() {
|
|
366
|
+
console.log("");
|
|
367
|
+
console.log("Next step for your agent harness:");
|
|
368
|
+
console.log("Copy and paste this into your conversation with your chosen agent:");
|
|
369
|
+
console.log("");
|
|
370
|
+
console.log(BOOTSTRAP_AGENT_INSTRUCTION);
|
|
371
|
+
}
|
|
372
|
+
async function runSetupCommand(commandName, output, repoRoot) {
|
|
373
|
+
const workspace = await prepareCanonicalWorkspace(repoRoot);
|
|
374
|
+
const planning = await prepareCanonicalPlanningLayout(repoRoot);
|
|
375
|
+
const agents = await initAgents(repoRoot);
|
|
376
|
+
const store = new ReferencesStore(repoRoot);
|
|
377
|
+
const reindex = await store.reindexArtifacts();
|
|
378
|
+
const payload = {
|
|
379
|
+
status: "ok",
|
|
380
|
+
command: commandName,
|
|
381
|
+
workspace_mode: workspace.state.mode,
|
|
382
|
+
migrated_workspace: workspace.migrated,
|
|
383
|
+
created_workspace: workspace.created,
|
|
384
|
+
planning_mode: planning.state.mode,
|
|
385
|
+
migrated_planning_layout: planning.migrated,
|
|
386
|
+
created_planning_layout: planning.created,
|
|
387
|
+
...agents,
|
|
388
|
+
refs_dir: store.refsDir,
|
|
389
|
+
manifest_path: store.manifestPath,
|
|
390
|
+
reindex,
|
|
391
|
+
};
|
|
392
|
+
if (output === "json") {
|
|
393
|
+
printResult(output, payload);
|
|
394
|
+
return 0;
|
|
395
|
+
}
|
|
396
|
+
if (workspace.message) {
|
|
397
|
+
console.log(workspace.message);
|
|
398
|
+
}
|
|
399
|
+
if (planning.message) {
|
|
400
|
+
console.log(planning.message);
|
|
401
|
+
}
|
|
402
|
+
console.log(`${commandName === "init" ? "Initialized" : "Bootstrapped"} ${store.refsDir}`);
|
|
403
|
+
console.log(`Updated ${agents.root_agents_path}`);
|
|
404
|
+
console.log(`Updated ${agents.reffy_agents_path}`);
|
|
405
|
+
console.log(`Updated ${agents.reffyspec_agents_path}`);
|
|
406
|
+
console.log(`Reindex: added=${String(reindex.added)} removed=${String(reindex.removed)} total=${String(reindex.total)}`);
|
|
407
|
+
if (shouldPrintBootstrapOnboarding(workspace.created, workspace.migrated, planning.created, planning.migrated)) {
|
|
408
|
+
printBootstrapOnboarding();
|
|
409
|
+
}
|
|
410
|
+
return 0;
|
|
411
|
+
}
|
|
235
412
|
function usage() {
|
|
236
413
|
return [
|
|
237
414
|
"Usage: reffy <command> [--repo PATH] [--output text|json]",
|
|
@@ -240,12 +417,15 @@ function usage() {
|
|
|
240
417
|
" --version Print the installed reffy package version.",
|
|
241
418
|
"",
|
|
242
419
|
"Commands:",
|
|
243
|
-
" init
|
|
244
|
-
" bootstrap
|
|
420
|
+
" init Run the canonical first-run setup flow and refresh managed instructions.",
|
|
421
|
+
" bootstrap Compatibility alias for init.",
|
|
422
|
+
" migrate Migrate a legacy .references workspace to the canonical .reffy layout.",
|
|
245
423
|
" doctor Diagnose required Reffy setup and optional tool availability.",
|
|
246
424
|
" reindex Scan .reffy/artifacts and add missing files to manifest.",
|
|
247
425
|
" validate Validate .reffy/manifest.json against manifest v1 contract.",
|
|
248
426
|
" summarize Generate a read-only summary of indexed Reffy artifacts.",
|
|
427
|
+
" plan Generate and manage ReffySpec planning scaffolds from indexed Reffy artifacts.",
|
|
428
|
+
" spec Inspect current specs from the ReffySpec layout.",
|
|
249
429
|
" diagram Render Mermaid diagrams (supports SVG and ASCII).",
|
|
250
430
|
].join("\n");
|
|
251
431
|
}
|
|
@@ -254,7 +434,7 @@ function diagramUsage() {
|
|
|
254
434
|
"Usage: reffy diagram render [--repo PATH] [--input PATH|--stdin] [--format svg|ascii] [--output PATH]",
|
|
255
435
|
"",
|
|
256
436
|
"Options:",
|
|
257
|
-
" --input PATH Read Mermaid (or
|
|
437
|
+
" --input PATH Read Mermaid (or ReffySpec spec.md) from file",
|
|
258
438
|
" --stdin Read Mermaid text from stdin",
|
|
259
439
|
" --format VALUE Output format: svg (default) or ascii",
|
|
260
440
|
" --output PATH Write rendered result to file instead of stdout",
|
|
@@ -269,6 +449,30 @@ function diagramUsage() {
|
|
|
269
449
|
" --font NAME SVG font family override",
|
|
270
450
|
].join("\n");
|
|
271
451
|
}
|
|
452
|
+
function specUsage() {
|
|
453
|
+
return [
|
|
454
|
+
"Usage:",
|
|
455
|
+
" reffy spec list [--repo PATH] [--output text|json]",
|
|
456
|
+
" reffy spec show <spec-id> [--repo PATH] [--output text|json]",
|
|
457
|
+
].join("\n");
|
|
458
|
+
}
|
|
459
|
+
function planUsage() {
|
|
460
|
+
return [
|
|
461
|
+
"Usage:",
|
|
462
|
+
" reffy plan create --change-id ID [--repo PATH] [--artifacts file1.md,file2.md|--all] [--title TEXT]",
|
|
463
|
+
" reffy plan validate <change-id> [--repo PATH] [--output text|json]",
|
|
464
|
+
" reffy plan list [--repo PATH] [--output text|json]",
|
|
465
|
+
" reffy plan show <change-id> [--repo PATH] [--output text|json]",
|
|
466
|
+
" reffy plan archive <change-id> [--repo PATH] [--output text|json]",
|
|
467
|
+
"",
|
|
468
|
+
"Options:",
|
|
469
|
+
` --change-id ID ${DEFAULT_PLANNING_DIRNAME} change id to create`,
|
|
470
|
+
" --title TEXT Human-readable proposal title",
|
|
471
|
+
" --artifacts LIST Comma-separated artifact filenames or ids to link",
|
|
472
|
+
" --all Use all indexed artifacts as planning inputs",
|
|
473
|
+
" --force Overwrite an existing non-empty change directory",
|
|
474
|
+
].join("\n");
|
|
475
|
+
}
|
|
272
476
|
function parseDiagramArgs(argv) {
|
|
273
477
|
const repoRoot = parseRepoArg(argv);
|
|
274
478
|
const args = {
|
|
@@ -368,6 +572,83 @@ function parseDiagramArgs(argv) {
|
|
|
368
572
|
}
|
|
369
573
|
return args;
|
|
370
574
|
}
|
|
575
|
+
function parsePlanArgs(argv) {
|
|
576
|
+
const repoRoot = parseRepoArg(argv);
|
|
577
|
+
const args = {
|
|
578
|
+
repoRoot,
|
|
579
|
+
changeId: "",
|
|
580
|
+
artifactFilters: [],
|
|
581
|
+
includeAllArtifacts: false,
|
|
582
|
+
overwrite: false,
|
|
583
|
+
};
|
|
584
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
585
|
+
const arg = argv[i];
|
|
586
|
+
if (arg === "--repo") {
|
|
587
|
+
i += 1;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (arg.startsWith("--repo="))
|
|
591
|
+
continue;
|
|
592
|
+
if (arg === "--json")
|
|
593
|
+
continue;
|
|
594
|
+
if (arg === "--output") {
|
|
595
|
+
i += 1;
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (arg.startsWith("--output="))
|
|
599
|
+
continue;
|
|
600
|
+
if (arg === "--change-id") {
|
|
601
|
+
const value = argv[i + 1];
|
|
602
|
+
if (!value)
|
|
603
|
+
throw new Error("--change-id requires a value");
|
|
604
|
+
args.changeId = value;
|
|
605
|
+
i += 1;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (arg.startsWith("--change-id=")) {
|
|
609
|
+
args.changeId = arg.split("=", 2)[1] ?? "";
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (arg === "--title") {
|
|
613
|
+
const value = argv[i + 1];
|
|
614
|
+
if (!value)
|
|
615
|
+
throw new Error("--title requires a value");
|
|
616
|
+
args.title = value;
|
|
617
|
+
i += 1;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (arg.startsWith("--title=")) {
|
|
621
|
+
args.title = arg.split("=", 2)[1];
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (arg === "--artifacts") {
|
|
625
|
+
const value = argv[i + 1];
|
|
626
|
+
if (!value)
|
|
627
|
+
throw new Error("--artifacts requires a comma-separated value");
|
|
628
|
+
args.artifactFilters = value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
629
|
+
i += 1;
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (arg.startsWith("--artifacts=")) {
|
|
633
|
+
const value = arg.split("=", 2)[1] ?? "";
|
|
634
|
+
args.artifactFilters = value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (arg === "--all") {
|
|
638
|
+
args.includeAllArtifacts = true;
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
if (arg === "--force") {
|
|
642
|
+
args.overwrite = true;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
throw new Error(`Unknown plan option: ${arg}`);
|
|
646
|
+
}
|
|
647
|
+
if (!args.changeId) {
|
|
648
|
+
throw new Error("--change-id is required");
|
|
649
|
+
}
|
|
650
|
+
return args;
|
|
651
|
+
}
|
|
371
652
|
function printSection(title, values) {
|
|
372
653
|
console.log(`${title}:`);
|
|
373
654
|
if (values.length === 0) {
|
|
@@ -400,6 +681,7 @@ async function main() {
|
|
|
400
681
|
return 1;
|
|
401
682
|
}
|
|
402
683
|
const parsed = parseDiagramArgs(diagramArgs);
|
|
684
|
+
await prepareCanonicalPlanningLayout(parsed.repoRoot);
|
|
403
685
|
const rendered = await renderDiagram({
|
|
404
686
|
repoRoot: parsed.repoRoot,
|
|
405
687
|
inputPath: parsed.inputPath,
|
|
@@ -426,47 +708,265 @@ async function main() {
|
|
|
426
708
|
}
|
|
427
709
|
return 0;
|
|
428
710
|
}
|
|
711
|
+
if (command === "plan") {
|
|
712
|
+
const [subcommand, ...planArgs] = rest;
|
|
713
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
714
|
+
console.error(planUsage());
|
|
715
|
+
return 1;
|
|
716
|
+
}
|
|
717
|
+
if (subcommand === "create") {
|
|
718
|
+
const output = parseOutputMode(planArgs);
|
|
719
|
+
const parsed = parsePlanArgs(planArgs);
|
|
720
|
+
await prepareCanonicalPlanningLayout(parsed.repoRoot);
|
|
721
|
+
const store = new ReferencesStore(parsed.repoRoot);
|
|
722
|
+
const validation = await store.validateManifest();
|
|
723
|
+
if (!validation.ok) {
|
|
724
|
+
const payload = { status: "error", command: "plan", subcommand: "create", ...validation };
|
|
725
|
+
if (output === "json") {
|
|
726
|
+
printResult(output, payload);
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
console.error(`Cannot create plan: manifest invalid (${String(validation.errors.length)} error(s))`);
|
|
730
|
+
}
|
|
731
|
+
return 1;
|
|
732
|
+
}
|
|
733
|
+
const result = await createPlanScaffold(store, {
|
|
734
|
+
changeId: parsed.changeId,
|
|
735
|
+
title: parsed.title,
|
|
736
|
+
artifactFilters: parsed.artifactFilters,
|
|
737
|
+
includeAllArtifacts: parsed.includeAllArtifacts,
|
|
738
|
+
overwrite: parsed.overwrite,
|
|
739
|
+
});
|
|
740
|
+
const payload = { status: "ok", command: "plan", subcommand: "create", ...result };
|
|
741
|
+
if (output === "json") {
|
|
742
|
+
printResult(output, payload);
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
console.log(`Created ${result.change_dir}`);
|
|
746
|
+
console.log(`Artifacts linked: ${String(result.linked_artifacts)}`);
|
|
747
|
+
for (const file of result.written_files) {
|
|
748
|
+
console.log(`- ${file}`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return 0;
|
|
752
|
+
}
|
|
753
|
+
const output = parseOutputMode(planArgs);
|
|
754
|
+
const repoRoot = parseRepoArg(planArgs);
|
|
755
|
+
await prepareCanonicalPlanningLayout(repoRoot);
|
|
756
|
+
const positionals = getPlanPositionalArgs(planArgs);
|
|
757
|
+
if (subcommand === "validate") {
|
|
758
|
+
const changeId = positionals[0];
|
|
759
|
+
if (!changeId) {
|
|
760
|
+
console.error("reffy plan validate requires a change id");
|
|
761
|
+
console.error(planUsage());
|
|
762
|
+
return 1;
|
|
763
|
+
}
|
|
764
|
+
const result = await validatePlanningChange(repoRoot, changeId);
|
|
765
|
+
const payload = { status: result.ok ? "ok" : "error", command: "plan", subcommand: "validate", ...result };
|
|
766
|
+
if (output === "json") {
|
|
767
|
+
printResult(output, payload);
|
|
768
|
+
}
|
|
769
|
+
else if (result.ok) {
|
|
770
|
+
console.log(`Change "${changeId}" is valid`);
|
|
771
|
+
console.log(`Tasks: ${String(result.task_status.completed)}/${String(result.task_status.total)}`);
|
|
772
|
+
console.log(`Delta specs: ${String(result.delta_count)}`);
|
|
773
|
+
for (const warning of result.warnings) {
|
|
774
|
+
console.log(`warn: ${warning}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
console.error(`Change "${changeId}" has issues`);
|
|
779
|
+
for (const error of result.errors) {
|
|
780
|
+
console.error(`error: ${error}`);
|
|
781
|
+
}
|
|
782
|
+
for (const warning of result.warnings) {
|
|
783
|
+
console.error(`warn: ${warning}`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return result.ok ? 0 : 1;
|
|
787
|
+
}
|
|
788
|
+
if (subcommand === "list") {
|
|
789
|
+
const changes = await listPlanningChanges(repoRoot);
|
|
790
|
+
const payload = { status: "ok", command: "plan", subcommand: "list", changes };
|
|
791
|
+
if (output === "json") {
|
|
792
|
+
printResult(output, payload);
|
|
793
|
+
}
|
|
794
|
+
else if (changes.length === 0) {
|
|
795
|
+
console.log("Changes:");
|
|
796
|
+
console.log("- (none)");
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
console.log("Changes:");
|
|
800
|
+
for (const change of changes) {
|
|
801
|
+
console.log(`- ${change.id} - ${change.title} [deltas ${String(change.delta_count)}] [tasks ${String(change.task_status.completed)}/${String(change.task_status.total)}]`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return 0;
|
|
805
|
+
}
|
|
806
|
+
if (subcommand === "show") {
|
|
807
|
+
const changeId = positionals[0];
|
|
808
|
+
if (!changeId) {
|
|
809
|
+
console.error("reffy plan show requires a change id");
|
|
810
|
+
console.error(planUsage());
|
|
811
|
+
return 1;
|
|
812
|
+
}
|
|
813
|
+
const result = await showPlanningChange(repoRoot, changeId);
|
|
814
|
+
const payload = { status: "ok", command: "plan", subcommand: "show", change: result };
|
|
815
|
+
if (output === "json") {
|
|
816
|
+
printResult(output, payload);
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
console.log(`# ${result.title}`);
|
|
820
|
+
console.log("");
|
|
821
|
+
console.log(result.proposal.trim());
|
|
822
|
+
console.log("");
|
|
823
|
+
console.log("## Tasks");
|
|
824
|
+
console.log(result.tasks.trim());
|
|
825
|
+
if (result.design) {
|
|
826
|
+
console.log("");
|
|
827
|
+
console.log("## Design");
|
|
828
|
+
console.log(result.design.trim());
|
|
829
|
+
}
|
|
830
|
+
if (result.specs.length > 0) {
|
|
831
|
+
console.log("");
|
|
832
|
+
console.log("## Specs");
|
|
833
|
+
for (const spec of result.specs) {
|
|
834
|
+
console.log(`### ${spec.capability}`);
|
|
835
|
+
console.log(spec.content.trim());
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return 0;
|
|
840
|
+
}
|
|
841
|
+
if (subcommand === "archive") {
|
|
842
|
+
const changeId = positionals[0];
|
|
843
|
+
if (!changeId) {
|
|
844
|
+
console.error("reffy plan archive requires a change id");
|
|
845
|
+
console.error(planUsage());
|
|
846
|
+
return 1;
|
|
847
|
+
}
|
|
848
|
+
const result = await archivePlanningChange(repoRoot, changeId);
|
|
849
|
+
const payload = { status: "ok", command: "plan", subcommand: "archive", ...result };
|
|
850
|
+
if (output === "json") {
|
|
851
|
+
printResult(output, payload);
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
console.log(`Archived ${changeId} to ${result.archive_dir}`);
|
|
855
|
+
console.log(`Updated specs: ${String(result.updated_specs.length)}`);
|
|
856
|
+
console.log(`Linked artifacts updated: ${String(result.linked_artifacts)}`);
|
|
857
|
+
}
|
|
858
|
+
return 0;
|
|
859
|
+
}
|
|
860
|
+
{
|
|
861
|
+
console.error(`Unknown plan subcommand: ${subcommand}`);
|
|
862
|
+
console.error(planUsage());
|
|
863
|
+
return 1;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (command === "spec") {
|
|
867
|
+
const [subcommand, ...specArgs] = rest;
|
|
868
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
869
|
+
console.error(specUsage());
|
|
870
|
+
return 1;
|
|
871
|
+
}
|
|
872
|
+
const output = parseOutputMode(specArgs);
|
|
873
|
+
const repoRoot = parseRepoArg(specArgs);
|
|
874
|
+
await prepareCanonicalPlanningLayout(repoRoot);
|
|
875
|
+
const positionals = getPlanPositionalArgs(specArgs);
|
|
876
|
+
if (subcommand === "list") {
|
|
877
|
+
const specs = await listSpecs(repoRoot);
|
|
878
|
+
const payload = { status: "ok", command: "spec", subcommand: "list", specs };
|
|
879
|
+
if (output === "json") {
|
|
880
|
+
printResult(output, payload);
|
|
881
|
+
}
|
|
882
|
+
else if (specs.length === 0) {
|
|
883
|
+
console.log("Specs:");
|
|
884
|
+
console.log("- (none)");
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
console.log("Specs:");
|
|
888
|
+
for (const spec of specs) {
|
|
889
|
+
console.log(`- ${spec.id} - ${spec.title} [requirements ${String(spec.requirement_count)}]`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return 0;
|
|
893
|
+
}
|
|
894
|
+
if (subcommand === "show") {
|
|
895
|
+
const specId = positionals[0];
|
|
896
|
+
if (!specId) {
|
|
897
|
+
console.error("reffy spec show requires a spec id");
|
|
898
|
+
console.error(specUsage());
|
|
899
|
+
return 1;
|
|
900
|
+
}
|
|
901
|
+
const result = await showSpec(repoRoot, specId);
|
|
902
|
+
const payload = { status: "ok", command: "spec", subcommand: "show", spec: result };
|
|
903
|
+
if (output === "json") {
|
|
904
|
+
printResult(output, payload);
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
console.log(`# ${result.title}`);
|
|
908
|
+
if (result.purpose) {
|
|
909
|
+
console.log("");
|
|
910
|
+
console.log(`Purpose: ${result.purpose}`);
|
|
911
|
+
}
|
|
912
|
+
console.log("");
|
|
913
|
+
console.log(result.spec.trim());
|
|
914
|
+
if (result.design) {
|
|
915
|
+
console.log("");
|
|
916
|
+
console.log("## Design");
|
|
917
|
+
console.log(result.design.trim());
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return 0;
|
|
921
|
+
}
|
|
922
|
+
console.error(`Unknown spec subcommand: ${subcommand}`);
|
|
923
|
+
console.error(specUsage());
|
|
924
|
+
return 1;
|
|
925
|
+
}
|
|
429
926
|
const output = parseOutputMode(rest);
|
|
430
927
|
if (command === "init") {
|
|
431
928
|
printBanner(output);
|
|
432
929
|
const repoRoot = parseRepoArg(rest);
|
|
433
|
-
|
|
434
|
-
const payload = { status: "ok", command: "init", ...agents };
|
|
435
|
-
if (output === "json") {
|
|
436
|
-
printResult(output, payload);
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
console.log(`Updated ${agents.root_agents_path}`);
|
|
440
|
-
console.log(`Updated ${agents.reffy_agents_path}`);
|
|
441
|
-
}
|
|
442
|
-
return 0;
|
|
930
|
+
return await runSetupCommand("init", output, repoRoot);
|
|
443
931
|
}
|
|
444
932
|
if (command === "bootstrap") {
|
|
445
933
|
const repoRoot = parseRepoArg(rest);
|
|
934
|
+
return await runSetupCommand("bootstrap", output, repoRoot);
|
|
935
|
+
}
|
|
936
|
+
if (command === "migrate") {
|
|
937
|
+
const repoRoot = parseRepoArg(rest);
|
|
938
|
+
const workspace = await prepareCanonicalWorkspace(repoRoot);
|
|
939
|
+
const planning = await prepareCanonicalPlanningLayout(repoRoot);
|
|
446
940
|
const agents = await initAgents(repoRoot);
|
|
447
|
-
const store = new ReferencesStore(repoRoot);
|
|
448
|
-
const reindex = await store.reindexArtifacts();
|
|
449
941
|
const payload = {
|
|
450
942
|
status: "ok",
|
|
451
|
-
command: "
|
|
943
|
+
command: "migrate",
|
|
944
|
+
workspace_mode: workspace.state.mode,
|
|
945
|
+
migrated_workspace: workspace.migrated,
|
|
946
|
+
created_workspace: workspace.created,
|
|
947
|
+
planning_mode: planning.state.mode,
|
|
948
|
+
migrated_planning_layout: planning.migrated,
|
|
949
|
+
created_planning_layout: planning.created,
|
|
950
|
+
refs_dir: workspace.state.canonicalDir,
|
|
452
951
|
...agents,
|
|
453
|
-
refs_dir: store.refsDir,
|
|
454
|
-
manifest_path: store.manifestPath,
|
|
455
|
-
reindex,
|
|
456
952
|
};
|
|
457
953
|
if (output === "json") {
|
|
458
954
|
printResult(output, payload);
|
|
459
955
|
}
|
|
460
956
|
else {
|
|
461
|
-
console.log(`
|
|
957
|
+
console.log(workspace.message ?? `Workspace ready at ${workspace.state.canonicalDir}`);
|
|
958
|
+
if (planning.message) {
|
|
959
|
+
console.log(planning.message);
|
|
960
|
+
}
|
|
462
961
|
console.log(`Updated ${agents.root_agents_path}`);
|
|
463
962
|
console.log(`Updated ${agents.reffy_agents_path}`);
|
|
464
|
-
console.log(`
|
|
963
|
+
console.log(`Updated ${agents.reffyspec_agents_path}`);
|
|
465
964
|
}
|
|
466
965
|
return 0;
|
|
467
966
|
}
|
|
468
967
|
if (command === "reindex") {
|
|
469
968
|
const repoRoot = parseRepoArg(rest);
|
|
969
|
+
await prepareCanonicalPlanningLayout(repoRoot);
|
|
470
970
|
const store = new ReferencesStore(repoRoot);
|
|
471
971
|
const reindex = await store.reindexArtifacts();
|
|
472
972
|
const payload = { status: "ok", command: "reindex", ...reindex };
|
|
@@ -480,6 +980,7 @@ async function main() {
|
|
|
480
980
|
}
|
|
481
981
|
if (command === "doctor") {
|
|
482
982
|
const repoRoot = parseRepoArg(rest);
|
|
983
|
+
await prepareCanonicalPlanningLayout(repoRoot);
|
|
483
984
|
const report = await runDoctor(repoRoot);
|
|
484
985
|
const status = report.summary.required_failed > 0 ? "error" : "ok";
|
|
485
986
|
const payload = { status, command: "doctor", ...report };
|
|
@@ -505,6 +1006,7 @@ async function main() {
|
|
|
505
1006
|
}
|
|
506
1007
|
if (command === "validate") {
|
|
507
1008
|
const repoRoot = parseRepoArg(rest);
|
|
1009
|
+
await prepareCanonicalPlanningLayout(repoRoot);
|
|
508
1010
|
const store = new ReferencesStore(repoRoot);
|
|
509
1011
|
const result = await store.validateManifest();
|
|
510
1012
|
const payload = { status: result.ok ? "ok" : "error", command: "validate", ...result };
|
|
@@ -532,6 +1034,7 @@ async function main() {
|
|
|
532
1034
|
}
|
|
533
1035
|
if (command === "summarize") {
|
|
534
1036
|
const repoRoot = parseRepoArg(rest);
|
|
1037
|
+
await prepareCanonicalPlanningLayout(repoRoot);
|
|
535
1038
|
const store = new ReferencesStore(repoRoot);
|
|
536
1039
|
const validation = await store.validateManifest();
|
|
537
1040
|
if (!validation.ok) {
|