@oevery/rsp 1.1.0 → 2.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 +110 -45
- package/README.zh-CN.md +107 -48
- package/dist/cli.mjs +1372 -920
- package/package.json +8 -5
- package/rules/rsp-rules.md +79 -81
- package/skills/rsp/SKILL.md +137 -0
package/dist/cli.mjs
CHANGED
|
@@ -7,90 +7,87 @@ import { defineCommand, runMain } from "citty";
|
|
|
7
7
|
|
|
8
8
|
// src/commands/add-rules.ts
|
|
9
9
|
import { existsSync as existsSync3 } from "fs";
|
|
10
|
-
import { mkdir as mkdir2,
|
|
11
|
-
import {
|
|
10
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
11
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
12
12
|
|
|
13
13
|
// src/core/config.ts
|
|
14
14
|
import { existsSync as existsSync2 } from "fs";
|
|
15
|
-
import { readFile } from "fs/promises";
|
|
16
|
-
import { dirname, join as join2 } from "path";
|
|
15
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
16
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
17
17
|
import { fileURLToPath } from "url";
|
|
18
18
|
import pc from "picocolors";
|
|
19
19
|
|
|
20
20
|
// src/core/helpers.ts
|
|
21
21
|
import { existsSync } from "fs";
|
|
22
|
-
import { readdir, stat } from "fs/promises";
|
|
23
|
-
import { join, relative } from "path";
|
|
22
|
+
import { readdir, readFile, rmdir, stat } from "fs/promises";
|
|
23
|
+
import { basename, dirname, join, relative, sep } from "path";
|
|
24
|
+
import { parse } from "yaml";
|
|
25
|
+
|
|
26
|
+
// src/core/output.ts
|
|
27
|
+
function toErrorMessage(error) {
|
|
28
|
+
return error instanceof Error ? error.message : String(error);
|
|
29
|
+
}
|
|
30
|
+
function emitJson(data) {
|
|
31
|
+
console.log(JSON.stringify(data, null, 2));
|
|
32
|
+
}
|
|
33
|
+
function emitStatusJsonError(error, options) {
|
|
34
|
+
const payload = {
|
|
35
|
+
command: "status",
|
|
36
|
+
ok: false,
|
|
37
|
+
filters: {
|
|
38
|
+
focused: options.focused,
|
|
39
|
+
blocked: options.blocked,
|
|
40
|
+
stale: null
|
|
41
|
+
},
|
|
42
|
+
focused: [],
|
|
43
|
+
records: [],
|
|
44
|
+
summary: {
|
|
45
|
+
total: 0,
|
|
46
|
+
focused: 0,
|
|
47
|
+
blocked: 0
|
|
48
|
+
},
|
|
49
|
+
archiveTrend: [],
|
|
50
|
+
runtime: [],
|
|
51
|
+
error
|
|
52
|
+
};
|
|
53
|
+
emitJson(payload);
|
|
54
|
+
}
|
|
55
|
+
function recordRuntimeDiagnostic(runtime, diagnostic, verbose = false) {
|
|
56
|
+
runtime.push(diagnostic);
|
|
57
|
+
if (verbose) {
|
|
58
|
+
console.error(` ${pc.dim(`[verbose] ${diagnostic.operation} ${diagnostic.path}: ${diagnostic.message}`)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/core/helpers.ts
|
|
24
63
|
var RSP_AGENTS_BEGIN = "<!-- rsp:begin -->";
|
|
25
64
|
var RSP_AGENTS_END = "<!-- rsp:end -->";
|
|
26
|
-
function
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
currentList = null;
|
|
34
|
-
currentKey = null;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
for (const line of lines) {
|
|
38
|
-
const trimmed = line.trim();
|
|
39
|
-
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
const li = line.match(/^[ \t]+-[ \t]+([^\n]+)$/);
|
|
43
|
-
if (li && currentList !== null) {
|
|
44
|
-
currentList.push(li[1].trim());
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
flushList();
|
|
48
|
-
const kv = line.match(/^([\w-]+):[ \t]*([^\n]*)$/);
|
|
49
|
-
if (kv) {
|
|
50
|
-
currentKey = kv[1];
|
|
51
|
-
const val = kv[2].trim();
|
|
52
|
-
if (!val || val === "[]") {
|
|
53
|
-
currentList = [];
|
|
54
|
-
} else {
|
|
55
|
-
result[currentKey] = val;
|
|
56
|
-
currentKey = null;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
flushList();
|
|
61
|
-
return result;
|
|
65
|
+
function parseYamlText(text) {
|
|
66
|
+
const parsed = parse(text);
|
|
67
|
+
if (parsed === null || parsed === void 0)
|
|
68
|
+
return {};
|
|
69
|
+
if (typeof parsed !== "object" || Array.isArray(parsed))
|
|
70
|
+
throw new Error("YAML document must be a mapping/object");
|
|
71
|
+
return parsed;
|
|
62
72
|
}
|
|
63
73
|
function parseFrontmatter(content) {
|
|
64
74
|
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)\r?\n/);
|
|
65
75
|
if (!m)
|
|
66
76
|
return null;
|
|
67
|
-
return
|
|
77
|
+
return parseYamlText(m[1]);
|
|
68
78
|
}
|
|
69
79
|
function countCheckboxes(content) {
|
|
70
80
|
const todo = (content.match(/\[ \]/g) || []).length;
|
|
71
81
|
const progress = (content.match(/\[\/\]/g) || []).length;
|
|
72
82
|
const done = (content.match(/\[x\]/g) || []).length;
|
|
73
83
|
const dropped = (content.match(/\[-\]/g) || []).length;
|
|
74
|
-
return { todo, progress, done, total: todo + progress + done + dropped };
|
|
84
|
+
return { todo, progress, done, dropped, total: todo + progress + done + dropped };
|
|
75
85
|
}
|
|
76
|
-
async function walkMarkdownFiles(dir) {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
const items = await readdir(dir, { withFileTypes: true });
|
|
80
|
-
for (const item of items) {
|
|
81
|
-
if (item.name.startsWith("."))
|
|
82
|
-
continue;
|
|
83
|
-
const filePath = join(dir, item.name);
|
|
84
|
-
if (item.isDirectory())
|
|
85
|
-
files.push(...await walkMarkdownFiles(filePath));
|
|
86
|
-
else if (item.name.endsWith(".md"))
|
|
87
|
-
files.push(filePath);
|
|
88
|
-
}
|
|
89
|
-
} catch {
|
|
90
|
-
}
|
|
91
|
-
return files;
|
|
86
|
+
async function walkMarkdownFiles(dir, options = {}) {
|
|
87
|
+
const all = await walkFiles(dir, options);
|
|
88
|
+
return all.filter((f) => f.endsWith(".md"));
|
|
92
89
|
}
|
|
93
|
-
async function walkFiles(dir) {
|
|
90
|
+
async function walkFiles(dir, options = {}) {
|
|
94
91
|
const files = [];
|
|
95
92
|
try {
|
|
96
93
|
const items = await readdir(dir, { withFileTypes: true });
|
|
@@ -99,63 +96,210 @@ async function walkFiles(dir) {
|
|
|
99
96
|
continue;
|
|
100
97
|
const filePath = join(dir, item.name);
|
|
101
98
|
if (item.isDirectory())
|
|
102
|
-
files.push(...await walkFiles(filePath));
|
|
99
|
+
files.push(...await walkFiles(filePath, options));
|
|
103
100
|
else
|
|
104
101
|
files.push(filePath);
|
|
105
102
|
}
|
|
106
|
-
} catch {
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (error.code !== "ENOENT") {
|
|
105
|
+
options.onError?.({
|
|
106
|
+
code: "walk_failed",
|
|
107
|
+
operation: "readdir",
|
|
108
|
+
path: dir,
|
|
109
|
+
message: toErrorMessage(error)
|
|
110
|
+
});
|
|
111
|
+
}
|
|
107
112
|
}
|
|
108
113
|
return files;
|
|
109
114
|
}
|
|
110
|
-
function
|
|
111
|
-
const rel = relative(
|
|
115
|
+
function changeNameFromPath(changesDir, filePath) {
|
|
116
|
+
const rel = normalizeLogicalPath(relative(changesDir, filePath));
|
|
112
117
|
return rel.replace(/\.md$/, "");
|
|
113
118
|
}
|
|
114
|
-
function
|
|
115
|
-
return
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
function normalizeLogicalPath(pathValue) {
|
|
120
|
+
return pathValue.split(sep).join("/").replace(/\\/g, "/");
|
|
121
|
+
}
|
|
122
|
+
function generateChangeContent(name, summary = "", kind) {
|
|
123
|
+
const proposalSummary = summary || (name === "project-setup" ? "Capture the project model, boundaries, and stable local constraints" : "<one-line summary>");
|
|
124
|
+
if (name === "project-setup") {
|
|
125
|
+
return `---
|
|
126
|
+
kind: ops
|
|
119
127
|
---
|
|
120
|
-
|
|
128
|
+
|
|
129
|
+
# Change: project-setup
|
|
130
|
+
|
|
131
|
+
## Proposal
|
|
132
|
+
- Summary: ${proposalSummary}
|
|
133
|
+
- Why:
|
|
134
|
+
- Establish a durable project model before normal implementation work starts
|
|
135
|
+
- Scope:
|
|
136
|
+
- Review the repository structure, entrypoints, and primary outputs
|
|
137
|
+
- Fill .rsp/specs/design.md with durable architecture facts
|
|
138
|
+
- Add .rsp/rules/project-rules.md when stable local rules or validation steps exist
|
|
139
|
+
- Non-goals:
|
|
140
|
+
- Do not duplicate durable project facts across this change, specs, and rules
|
|
121
141
|
|
|
122
142
|
## Spec
|
|
123
|
-
|
|
124
|
-
-
|
|
125
|
-
-
|
|
143
|
+
### ADDED
|
|
144
|
+
- Requirement: project bootstrap capture
|
|
145
|
+
- The repository's purpose, scope, and structure are reflected in .rsp/specs/design.md
|
|
146
|
+
|
|
147
|
+
### MODIFIED
|
|
148
|
+
- Requirement: stable local operating constraints
|
|
149
|
+
- Stable validation or workflow constraints are reflected in .rsp/rules/project-rules.md when needed
|
|
150
|
+
|
|
151
|
+
### Acceptance
|
|
152
|
+
#### Scenario: project model captured
|
|
153
|
+
- GIVEN an initialized RSP project
|
|
154
|
+
- WHEN project setup is completed
|
|
155
|
+
- THEN .rsp/specs/design.md reflects durable project facts
|
|
156
|
+
- AND .rsp/rules/project-rules.md exists only when stable local rules are present
|
|
157
|
+
|
|
158
|
+
## Design
|
|
159
|
+
- Approach:
|
|
160
|
+
- Keep bootstrap knowledge in this single change while moving durable facts into specs and rules
|
|
161
|
+
- Affected areas:
|
|
162
|
+
- .rsp/specs/design.md
|
|
163
|
+
- .rsp/rules/project-rules.md
|
|
126
164
|
- Constraints:
|
|
127
|
-
-
|
|
165
|
+
- Keep the setup lightweight and avoid duplicating durable project facts
|
|
166
|
+
|
|
167
|
+
## Tasks
|
|
168
|
+
- [ ] Review the repository structure, entrypoints, and primary outputs
|
|
169
|
+
- [ ] Fill .rsp/specs/design.md with durable architecture facts
|
|
170
|
+
- [ ] Add .rsp/rules/project-rules.md if stable local rules or validation steps exist
|
|
171
|
+
|
|
172
|
+
## Verify
|
|
173
|
+
- Automated:
|
|
174
|
+
- [ ] Run rsp doctor
|
|
175
|
+
- Manual:
|
|
176
|
+
- [ ] Review .rsp/specs/design.md and confirm it matches the repository
|
|
177
|
+
- Durable updates:
|
|
178
|
+
- [ ] Decide whether this change produced durable knowledge that belongs in \`.rsp/specs/\` or \`.rsp/rules/\`
|
|
179
|
+
- [ ] If yes, write only stable facts to the smallest correct target file before archive; do not promote task history, debugging notes, or one-off implementation context
|
|
180
|
+
|
|
181
|
+
## Blockers
|
|
182
|
+
- none
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
const frontmatterKind = kind ?? "<choose: feature | fix | refactor | docs | ops | research>";
|
|
186
|
+
const template = getChangeTemplateByKind(kind);
|
|
187
|
+
return `---
|
|
188
|
+
kind: "${frontmatterKind}"
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
# Change: ${name}
|
|
192
|
+
|
|
193
|
+
## Proposal
|
|
194
|
+
- Summary: ${proposalSummary}
|
|
195
|
+
- Why:
|
|
196
|
+
- ${template.why}
|
|
197
|
+
- Scope:
|
|
198
|
+
- ${template.scope}
|
|
199
|
+
- Non-goals:
|
|
200
|
+
- ${template.nonGoals}
|
|
201
|
+
|
|
202
|
+
## Spec
|
|
203
|
+
${template.specSection}
|
|
128
204
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
- [ ] <task>
|
|
205
|
+
### Acceptance
|
|
206
|
+
${template.acceptanceSection}
|
|
132
207
|
|
|
133
|
-
##
|
|
134
|
-
-
|
|
208
|
+
## Design
|
|
209
|
+
- Approach:
|
|
210
|
+
- ${template.approach}
|
|
211
|
+
- Affected areas:
|
|
212
|
+
- ${template.affectedAreas}
|
|
213
|
+
- Constraints:
|
|
214
|
+
- ${template.constraints}
|
|
215
|
+
|
|
216
|
+
## Tasks
|
|
217
|
+
- [ ] ${template.task}
|
|
135
218
|
|
|
136
|
-
##
|
|
137
|
-
-
|
|
219
|
+
## Verify
|
|
220
|
+
- Automated:
|
|
221
|
+
- [ ] ${template.automatedVerify}
|
|
222
|
+
- Manual:
|
|
223
|
+
- [ ] ${template.manualVerify}
|
|
224
|
+
- Durable updates:
|
|
225
|
+
- [ ] Decide whether this change produced durable knowledge that belongs in \`.rsp/specs/\` or \`.rsp/rules/\`
|
|
226
|
+
- [ ] If yes, write only stable facts to the smallest correct target file before archive; do not promote task history, debugging notes, or one-off implementation context
|
|
138
227
|
|
|
139
228
|
## Blockers
|
|
140
|
-
-
|
|
229
|
+
- none
|
|
141
230
|
`;
|
|
142
231
|
}
|
|
232
|
+
function getChangeTemplateByKind(kind) {
|
|
233
|
+
switch (kind) {
|
|
234
|
+
case "docs":
|
|
235
|
+
return {
|
|
236
|
+
why: "<why the documentation change matters>",
|
|
237
|
+
scope: "<which docs, readers, or workflows will be updated>",
|
|
238
|
+
nonGoals: "<what documentation behavior or audience is out of scope>",
|
|
239
|
+
specSection: "### MODIFIED\n- Requirement: documentation accuracy\n - <what durable reader-facing behavior or explanation changes>",
|
|
240
|
+
acceptanceSection: "#### Scenario: reader follows the updated guidance\n- GIVEN the updated documentation\n- WHEN a reader follows the documented workflow\n- THEN the steps are accurate and sufficient",
|
|
241
|
+
approach: "<how the documentation will be updated and organized>",
|
|
242
|
+
affectedAreas: "<doc path or documentation area>",
|
|
243
|
+
constraints: "<durable wording, consistency, or scope constraint>",
|
|
244
|
+
task: "<update the target documentation and supporting references>",
|
|
245
|
+
automatedVerify: "<run markdown, docs, or link checks if applicable>",
|
|
246
|
+
manualVerify: "<review the updated doc as a reader and confirm it is accurate>"
|
|
247
|
+
};
|
|
248
|
+
case "research":
|
|
249
|
+
return {
|
|
250
|
+
why: "<what question or uncertainty this research resolves>",
|
|
251
|
+
scope: "<what system area, option set, or hypothesis is being examined>",
|
|
252
|
+
nonGoals: "<what implementation work is explicitly out of scope>",
|
|
253
|
+
specSection: "### ADDED\n- Requirement: research outcome recording\n - <what finding, option, or decision must be captured clearly>",
|
|
254
|
+
acceptanceSection: "#### Scenario: research question is resolved\n- GIVEN the current uncertainty or open question\n- WHEN the investigation is completed\n- THEN the result is recorded clearly enough to guide follow-up work",
|
|
255
|
+
approach: "<how the investigation will be performed and what evidence will be gathered>",
|
|
256
|
+
affectedAreas: "<code path, subsystem, or source being investigated>",
|
|
257
|
+
constraints: "<time, evidence, or scope constraint for the investigation>",
|
|
258
|
+
task: "<gather evidence and record the resulting recommendation or finding>",
|
|
259
|
+
automatedVerify: "<run any command needed to confirm the observed behavior>",
|
|
260
|
+
manualVerify: "<review the findings and confirm they answer the original question>"
|
|
261
|
+
};
|
|
262
|
+
case "ops":
|
|
263
|
+
return {
|
|
264
|
+
why: "<why this operational or environment change matters>",
|
|
265
|
+
scope: "<what environment, workflow, or operational path changes>",
|
|
266
|
+
nonGoals: "<what product or feature behavior will not change>",
|
|
267
|
+
specSection: "### MODIFIED\n- Requirement: operational behavior\n - <what reliable operational outcome should change or stay true>",
|
|
268
|
+
acceptanceSection: "#### Scenario: operational path succeeds\n- GIVEN the target environment or workflow\n- WHEN the operational change is applied\n- THEN the expected operational outcome is reliable",
|
|
269
|
+
approach: "<how the operational change will be applied safely>",
|
|
270
|
+
affectedAreas: "<script, config, workflow, or environment path>",
|
|
271
|
+
constraints: "<rollback, safety, or environment constraint>",
|
|
272
|
+
task: "<apply and verify the operational change>",
|
|
273
|
+
automatedVerify: "<run the relevant operational validation command>",
|
|
274
|
+
manualVerify: "<exercise the target workflow and confirm the operational result>"
|
|
275
|
+
};
|
|
276
|
+
default:
|
|
277
|
+
return {
|
|
278
|
+
why: "<why this change matters>",
|
|
279
|
+
scope: "<what this change will do>",
|
|
280
|
+
nonGoals: "<what this change will not do>",
|
|
281
|
+
specSection: "### ADDED\n- Requirement: <new or updated behavior>\n - <verifiable requirement>",
|
|
282
|
+
acceptanceSection: "#### Scenario: <name>\n- GIVEN <context>\n- WHEN <action>\n- THEN <expected outcome>",
|
|
283
|
+
approach: "<how the change will be implemented>",
|
|
284
|
+
affectedAreas: "<path or subsystem>",
|
|
285
|
+
constraints: "<binding technical or product constraint>",
|
|
286
|
+
task: "<implementation task>",
|
|
287
|
+
automatedVerify: "<test command or automated check>",
|
|
288
|
+
manualVerify: "<manual scenario to validate>"
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
143
292
|
function renderRspAgentsBlock() {
|
|
144
293
|
return `${RSP_AGENTS_BEGIN}
|
|
145
294
|
## RSP Entry
|
|
146
295
|
|
|
147
296
|
Read in order:
|
|
148
|
-
1. .rsp/rules
|
|
149
|
-
2. .rsp/
|
|
150
|
-
3. .rsp/
|
|
151
|
-
4. .rsp/
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
- .rsp/rules/rsp-rules.md is required
|
|
155
|
-
- .rsp/rules/project-rules.md is optional
|
|
156
|
-
- Keep project design in .rsp/specs/
|
|
157
|
-
- Add extra spec files only when they have durable value
|
|
158
|
-
- Do not create a feature named 'init'
|
|
297
|
+
1. .rsp/rules/rsp-rules.md
|
|
298
|
+
2. .rsp/focus.d/
|
|
299
|
+
3. matching .rsp/changes/*.md for the focused entries
|
|
300
|
+
4. .rsp/specs/design.md
|
|
301
|
+
5. .rsp/specs/INDEX.md
|
|
302
|
+
6. only the relevant additional .rsp/rules/*.md and .rsp/specs/*.md files
|
|
159
303
|
${RSP_AGENTS_END}`;
|
|
160
304
|
}
|
|
161
305
|
function upsertRspAgentsBlock(content) {
|
|
@@ -175,6 +319,49 @@ ${trimmed}` : `${block}
|
|
|
175
319
|
function hasRspAgentsBlock(content) {
|
|
176
320
|
return content.includes(RSP_AGENTS_BEGIN) && content.includes(RSP_AGENTS_END);
|
|
177
321
|
}
|
|
322
|
+
var CHANGE_NAME_RE = /^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
|
|
323
|
+
function isValidChangeName(name) {
|
|
324
|
+
return CHANGE_NAME_RE.test(name);
|
|
325
|
+
}
|
|
326
|
+
function guardRspInitialized() {
|
|
327
|
+
const rspRulesPath = join(RSP_DIR, "rules", "rsp-rules.md");
|
|
328
|
+
const designPath = join(RSP_DIR, "specs", "design.md");
|
|
329
|
+
if (!existsSync(rspRulesPath) || !existsSync(designPath)) {
|
|
330
|
+
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
331
|
+
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function getOpenCheckboxes(sectionText) {
|
|
336
|
+
return sectionText.split("\n").map((line) => line.trim()).filter((line) => /^- \[[ /]\]/.test(line));
|
|
337
|
+
}
|
|
338
|
+
async function cleanupEmptyParentDirs(path, stopDir) {
|
|
339
|
+
let parent = dirname(path);
|
|
340
|
+
while (parent !== stopDir) {
|
|
341
|
+
try {
|
|
342
|
+
const remaining = await readdir(parent);
|
|
343
|
+
if (remaining.length === 0)
|
|
344
|
+
await rmdir(parent);
|
|
345
|
+
else
|
|
346
|
+
break;
|
|
347
|
+
} catch {
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
parent = dirname(parent);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function detectProjectName() {
|
|
354
|
+
try {
|
|
355
|
+
if (existsSync("package.json")) {
|
|
356
|
+
const raw = await readFile("package.json", "utf-8");
|
|
357
|
+
const pkg = JSON.parse(raw);
|
|
358
|
+
if (pkg.name && pkg.name !== "")
|
|
359
|
+
return pkg.name;
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
return basename(process.cwd());
|
|
364
|
+
}
|
|
178
365
|
function generateDesignContent(projectName) {
|
|
179
366
|
return `# Project Design: ${projectName}
|
|
180
367
|
|
|
@@ -182,7 +369,11 @@ function generateDesignContent(projectName) {
|
|
|
182
369
|
- <what this project is responsible for>
|
|
183
370
|
- <who or what it serves>
|
|
184
371
|
|
|
185
|
-
##
|
|
372
|
+
## Stable Facts
|
|
373
|
+
- <durable fact that future agents or developers must know>
|
|
374
|
+
- <key architectural invariant>
|
|
375
|
+
|
|
376
|
+
## Boundaries
|
|
186
377
|
- In scope:
|
|
187
378
|
- <major capability or boundary>
|
|
188
379
|
- Out of scope:
|
|
@@ -192,7 +383,7 @@ function generateDesignContent(projectName) {
|
|
|
192
383
|
- <important directory or subsystem> \u2014 <responsibility>
|
|
193
384
|
|
|
194
385
|
## Constraints
|
|
195
|
-
- <cross-cutting technical or operational constraint>
|
|
386
|
+
- <cross-cutting technical or operational constraint, when it affects architecture>
|
|
196
387
|
`;
|
|
197
388
|
}
|
|
198
389
|
function generateProjectRulesContent(projectName) {
|
|
@@ -204,10 +395,11 @@ description: Project-specific rules for ${projectName}
|
|
|
204
395
|
# Project Rules
|
|
205
396
|
|
|
206
397
|
## Scope
|
|
207
|
-
- <rules
|
|
398
|
+
- <stable local rules, workflow constraints, or validation expectations>
|
|
399
|
+
- Do not put temporary debugging steps here.
|
|
208
400
|
|
|
209
401
|
## Validation
|
|
210
|
-
- <preferred validation commands>
|
|
402
|
+
- <preferred stable validation commands, not temporary troubleshooting steps>
|
|
211
403
|
|
|
212
404
|
## Conventions
|
|
213
405
|
- <project-specific conventions>
|
|
@@ -217,7 +409,7 @@ function generateRulesContent(name) {
|
|
|
217
409
|
const title = toTitleCase(name);
|
|
218
410
|
return `---
|
|
219
411
|
name: ${name}
|
|
220
|
-
description:
|
|
412
|
+
description: Durable rules for ${title}
|
|
221
413
|
---
|
|
222
414
|
|
|
223
415
|
# ${title}
|
|
@@ -239,27 +431,47 @@ function generateSpecContent(name) {
|
|
|
239
431
|
## Purpose
|
|
240
432
|
- <why this project-level spec exists>
|
|
241
433
|
|
|
242
|
-
##
|
|
243
|
-
- <
|
|
434
|
+
## Stable Facts
|
|
435
|
+
- <durable fact that future work must know>
|
|
436
|
+
|
|
437
|
+
## Boundaries
|
|
438
|
+
- In scope:
|
|
439
|
+
- <what this spec covers>
|
|
440
|
+
- Out of scope:
|
|
441
|
+
- <what this spec does not cover>
|
|
442
|
+
|
|
443
|
+
## Constraints
|
|
444
|
+
- <stable constraint, if any>
|
|
244
445
|
`;
|
|
245
446
|
}
|
|
246
447
|
function toTitleCase(value) {
|
|
247
448
|
return value.split(/[-/]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
248
449
|
}
|
|
249
|
-
function
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
};
|
|
450
|
+
function extractSection(content, heading) {
|
|
451
|
+
const lines = content.split("\n");
|
|
452
|
+
const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
|
|
453
|
+
if (start === -1)
|
|
454
|
+
return "";
|
|
455
|
+
const body = [];
|
|
456
|
+
for (let index = start + 1; index < lines.length; index++) {
|
|
457
|
+
const line = lines[index];
|
|
458
|
+
if (line.startsWith("## "))
|
|
459
|
+
break;
|
|
460
|
+
body.push(line);
|
|
261
461
|
}
|
|
262
|
-
|
|
462
|
+
return body.join("\n").trim();
|
|
463
|
+
}
|
|
464
|
+
function hasMeaningfulBlockers(content) {
|
|
465
|
+
const blockers = extractSection(content, "Blockers");
|
|
466
|
+
if (!blockers)
|
|
467
|
+
return false;
|
|
468
|
+
const lines = blockers.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
469
|
+
if (lines.length === 0)
|
|
470
|
+
return false;
|
|
471
|
+
return lines.some((line) => !/^[-*]\s*(?:none)?$/i.test(line) && !/^none$/i.test(line));
|
|
472
|
+
}
|
|
473
|
+
function detectDeltaSections(content) {
|
|
474
|
+
const body = extractSection(content, "Spec");
|
|
263
475
|
return {
|
|
264
476
|
added: /^###\s*ADDED/im.test(body),
|
|
265
477
|
modified: /^###\s*MODIFIED/im.test(body),
|
|
@@ -268,60 +480,32 @@ function detectDeltaSections(content) {
|
|
|
268
480
|
}
|
|
269
481
|
function parseScenarios(content) {
|
|
270
482
|
const scenarios = [];
|
|
271
|
-
const regex =
|
|
483
|
+
const regex = /^#{3,4}\s*Scenario:\s*(.+)$/gim;
|
|
272
484
|
let match;
|
|
273
485
|
while ((match = regex.exec(content)) !== null) {
|
|
274
486
|
const heading = match[1].trim();
|
|
275
487
|
const startPos = match.index + match[0].length;
|
|
276
488
|
const restContent = content.slice(startPos);
|
|
277
|
-
const nextSection = restContent.match(
|
|
489
|
+
const nextSection = restContent.match(/^#{3,4}\s/m);
|
|
278
490
|
const block = nextSection ? restContent.slice(0, nextSection.index) : restContent;
|
|
279
|
-
const steps = block.split("\n").map((s) => s.trim()).filter((s) => s.startsWith("-") && /GIVEN|WHEN|THEN|AND|BUT/i.test(s)).map((s) => s.replace(
|
|
491
|
+
const steps = block.split("\n").map((s) => s.trim()).filter((s) => s.startsWith("-") && /GIVEN|WHEN|THEN|AND|BUT/i.test(s)).map((s) => s.replace(/^[-*]\s*/, ""));
|
|
280
492
|
if (steps.length > 0)
|
|
281
493
|
scenarios.push({ heading, steps });
|
|
282
494
|
}
|
|
283
495
|
return scenarios;
|
|
284
496
|
}
|
|
285
|
-
function detectCycles(graph) {
|
|
286
|
-
const cycles = [];
|
|
287
|
-
const visited = /* @__PURE__ */ new Set();
|
|
288
|
-
const inStack = /* @__PURE__ */ new Set();
|
|
289
|
-
function dfs(node, path) {
|
|
290
|
-
if (inStack.has(node)) {
|
|
291
|
-
const cycleStart = path.indexOf(node);
|
|
292
|
-
if (cycleStart !== -1)
|
|
293
|
-
cycles.push(path.slice(cycleStart).concat(node));
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
if (visited.has(node))
|
|
297
|
-
return;
|
|
298
|
-
visited.add(node);
|
|
299
|
-
inStack.add(node);
|
|
300
|
-
path.push(node);
|
|
301
|
-
const neighbors = graph.get(node) || [];
|
|
302
|
-
for (const next of neighbors) {
|
|
303
|
-
if (graph.has(next))
|
|
304
|
-
dfs(next, path);
|
|
305
|
-
}
|
|
306
|
-
path.pop();
|
|
307
|
-
inStack.delete(node);
|
|
308
|
-
}
|
|
309
|
-
for (const node of graph.keys())
|
|
310
|
-
dfs(node, []);
|
|
311
|
-
return cycles;
|
|
312
|
-
}
|
|
313
497
|
|
|
314
498
|
// src/core/config.ts
|
|
315
|
-
var __dirname =
|
|
499
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
316
500
|
var PKG_ROOT = join2(__dirname, "..");
|
|
317
501
|
var RSP_DIR = ".rsp";
|
|
318
|
-
var
|
|
502
|
+
var CHANGES_DIR = join2(RSP_DIR, "changes");
|
|
503
|
+
var FOCUS_DIR = join2(RSP_DIR, "focus.d");
|
|
319
504
|
var ARCHIVES_DIR = join2(RSP_DIR, "archives");
|
|
320
505
|
var LOCK_PATH = join2(RSP_DIR, ".lock");
|
|
321
506
|
var CONFIG_PATH = join2(RSP_DIR, "config.yaml");
|
|
322
|
-
var
|
|
323
|
-
var
|
|
324
|
-
var DEFAULT_REQUIRED_SECTIONS = ["Spec", "Plan"];
|
|
507
|
+
var VALID_KINDS = ["feature", "fix", "refactor", "docs", "ops", "research"];
|
|
508
|
+
var DEFAULT_REQUIRED_SECTIONS = ["Proposal", "Spec", "Design", "Tasks", "Verify", "Blockers"];
|
|
325
509
|
var _configCache = null;
|
|
326
510
|
async function loadRspConfig() {
|
|
327
511
|
if (_configCache)
|
|
@@ -330,31 +514,26 @@ async function loadRspConfig() {
|
|
|
330
514
|
_configCache = {};
|
|
331
515
|
return _configCache;
|
|
332
516
|
}
|
|
333
|
-
const raw = await
|
|
334
|
-
const parsed =
|
|
517
|
+
const raw = await readFile2(CONFIG_PATH, "utf-8");
|
|
518
|
+
const parsed = parseYamlText(raw);
|
|
335
519
|
_configCache = {
|
|
336
|
-
|
|
337
|
-
priorities: Array.isArray(parsed.priorities) ? parsed.priorities.map(String) : void 0,
|
|
338
|
-
required_sections: Array.isArray(parsed.required_sections) ? parsed.required_sections.map(String) : void 0
|
|
520
|
+
kinds: Array.isArray(parsed.kinds) ? parsed.kinds.map(String) : void 0
|
|
339
521
|
};
|
|
340
522
|
return _configCache;
|
|
341
523
|
}
|
|
342
|
-
function
|
|
343
|
-
return config.
|
|
524
|
+
function resolveKinds(config) {
|
|
525
|
+
return config.kinds && config.kinds.length > 0 ? config.kinds : [...VALID_KINDS];
|
|
344
526
|
}
|
|
345
|
-
function
|
|
346
|
-
return
|
|
347
|
-
}
|
|
348
|
-
function resolveRequiredSections(config) {
|
|
349
|
-
return config.required_sections && config.required_sections.length > 0 ? config.required_sections : [...DEFAULT_REQUIRED_SECTIONS];
|
|
527
|
+
function resolveRequiredSections(_config) {
|
|
528
|
+
return [...DEFAULT_REQUIRED_SECTIONS];
|
|
350
529
|
}
|
|
351
530
|
async function getVersion() {
|
|
352
|
-
const { version } = JSON.parse(await
|
|
531
|
+
const { version } = JSON.parse(await readFile2(join2(PKG_ROOT, "package.json"), "utf-8"));
|
|
353
532
|
return version;
|
|
354
533
|
}
|
|
355
534
|
|
|
356
535
|
// src/core/lock.ts
|
|
357
|
-
import { mkdir, readFile as
|
|
536
|
+
import { mkdir, readFile as readFile3, unlink, writeFile } from "fs/promises";
|
|
358
537
|
async function withRspLock(operation, fn) {
|
|
359
538
|
await mkdir(RSP_DIR, { recursive: true });
|
|
360
539
|
try {
|
|
@@ -378,7 +557,7 @@ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
|
378
557
|
}
|
|
379
558
|
async function handleStaleLock(operation) {
|
|
380
559
|
try {
|
|
381
|
-
const content = await
|
|
560
|
+
const content = await readFile3(LOCK_PATH, "utf-8");
|
|
382
561
|
const lines = content.trim().split("\n");
|
|
383
562
|
const pid = Number(lines[0]);
|
|
384
563
|
if (pid && Number.isFinite(pid)) {
|
|
@@ -390,10 +569,15 @@ async function handleStaleLock(operation) {
|
|
|
390
569
|
} catch (e) {
|
|
391
570
|
if (e instanceof Error && e.message.startsWith("RSP locked"))
|
|
392
571
|
throw e;
|
|
393
|
-
|
|
394
|
-
|
|
572
|
+
const code = e.code;
|
|
573
|
+
if (code === "ESRCH") {
|
|
574
|
+
await unlink(LOCK_PATH);
|
|
575
|
+
await writeFile(LOCK_PATH, `${process.pid}
|
|
395
576
|
${operation}
|
|
396
577
|
${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
throw e;
|
|
397
581
|
}
|
|
398
582
|
} else {
|
|
399
583
|
await unlink(LOCK_PATH);
|
|
@@ -409,39 +593,19 @@ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
|
409
593
|
}
|
|
410
594
|
|
|
411
595
|
// src/commands/add-rules.ts
|
|
412
|
-
var NAME_RE = /^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
|
|
413
|
-
async function detectProjectName() {
|
|
414
|
-
try {
|
|
415
|
-
if (existsSync3("package.json")) {
|
|
416
|
-
const raw = await readFile3("package.json", "utf-8");
|
|
417
|
-
const pkg = JSON.parse(raw);
|
|
418
|
-
if (pkg.name && pkg.name !== "")
|
|
419
|
-
return pkg.name;
|
|
420
|
-
}
|
|
421
|
-
} catch {
|
|
422
|
-
}
|
|
423
|
-
return basename(process.cwd());
|
|
424
|
-
}
|
|
425
596
|
async function addRules(name, projectName = "project") {
|
|
426
|
-
if (!name || !
|
|
597
|
+
if (!name || !isValidChangeName(name)) {
|
|
427
598
|
console.error(` ${pc.red("Error:")} rule name must be kebab-case with optional subdirectory`);
|
|
428
599
|
process.exit(1);
|
|
429
600
|
}
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
if (
|
|
433
|
-
console.error(` ${pc.red("Error:")}
|
|
434
|
-
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
601
|
+
guardRspInitialized();
|
|
602
|
+
const path = join3(RSP_DIR, "rules", `${name}.md`);
|
|
603
|
+
if (existsSync3(path)) {
|
|
604
|
+
console.error(` ${pc.red("Error:")} rule file already exists: .rsp/rules/${name}.md`);
|
|
435
605
|
process.exit(1);
|
|
436
606
|
}
|
|
437
607
|
return withRspLock("add-rules", async () => {
|
|
438
|
-
|
|
439
|
-
const path = join3(dir, `${name}.md`);
|
|
440
|
-
if (existsSync3(path)) {
|
|
441
|
-
console.error(` ${pc.red("Error:")} rule file already exists: .rsp/rules/${name}.md`);
|
|
442
|
-
process.exit(1);
|
|
443
|
-
}
|
|
444
|
-
await mkdir2(dirname2(path), { recursive: true });
|
|
608
|
+
await mkdir2(dirname3(path), { recursive: true });
|
|
445
609
|
const resolvedProjectName = projectName === "project" ? await detectProjectName() : projectName;
|
|
446
610
|
const content = name === "project-rules" ? generateProjectRulesContent(resolvedProjectName) : generateRulesContent(name);
|
|
447
611
|
await writeFile2(path, content);
|
|
@@ -452,8 +616,8 @@ async function addRules(name, projectName = "project") {
|
|
|
452
616
|
|
|
453
617
|
// src/commands/add-spec.ts
|
|
454
618
|
import { existsSync as existsSync5 } from "fs";
|
|
455
|
-
import { mkdir as mkdir3,
|
|
456
|
-
import {
|
|
619
|
+
import { mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
|
|
620
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
457
621
|
|
|
458
622
|
// src/commands/specs-index.ts
|
|
459
623
|
import { existsSync as existsSync4 } from "fs";
|
|
@@ -480,16 +644,17 @@ async function buildSpecsIndex({ acquireLock = true } = {}) {
|
|
|
480
644
|
lines.push("| File | Title | Summary |");
|
|
481
645
|
lines.push("|------|-------|---------|");
|
|
482
646
|
for (const fp of specFiles) {
|
|
483
|
-
const rel = relative2(specsDir, fp);
|
|
647
|
+
const rel = normalizeLogicalPath(relative2(specsDir, fp));
|
|
484
648
|
const content = await readFile4(fp, "utf-8");
|
|
485
649
|
const fm = parseFrontmatter(content);
|
|
486
|
-
const
|
|
650
|
+
const fmTitle = fm && typeof fm.title === "string" ? fm.title : "";
|
|
651
|
+
const title = fmTitle || extractTitle(content) || rel.replace(/\.md$/, "");
|
|
487
652
|
const summary = extractSummary(content, fm?.summary);
|
|
488
653
|
lines.push(`| ${escapeCell(rel)} | ${escapeCell(title)} | ${escapeCell(summary || "\u2014")} |`);
|
|
489
654
|
}
|
|
490
655
|
}
|
|
491
656
|
lines.push("");
|
|
492
|
-
lines.push("_Auto-generated by `rsp
|
|
657
|
+
lines.push("_Auto-generated by `rsp update`._");
|
|
493
658
|
await writeFile3(join4(specsDir, "INDEX.md"), lines.join("\n"));
|
|
494
659
|
console.log(` ${pc.green("INDEX.md updated:")} ${specFiles.length} spec file(s).
|
|
495
660
|
`);
|
|
@@ -528,40 +693,20 @@ function escapeCell(value) {
|
|
|
528
693
|
}
|
|
529
694
|
|
|
530
695
|
// src/commands/add-spec.ts
|
|
531
|
-
var NAME_RE2 = /^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
|
|
532
|
-
async function detectProjectName2() {
|
|
533
|
-
try {
|
|
534
|
-
if (existsSync5("package.json")) {
|
|
535
|
-
const raw = await readFile5("package.json", "utf-8");
|
|
536
|
-
const pkg = JSON.parse(raw);
|
|
537
|
-
if (pkg.name && pkg.name !== "")
|
|
538
|
-
return pkg.name;
|
|
539
|
-
}
|
|
540
|
-
} catch {
|
|
541
|
-
}
|
|
542
|
-
return basename3(process.cwd());
|
|
543
|
-
}
|
|
544
696
|
async function addSpec(name, projectName = "project") {
|
|
545
|
-
if (!name || !
|
|
697
|
+
if (!name || !isValidChangeName(name)) {
|
|
546
698
|
console.error(` ${pc.red("Error:")} spec name must be kebab-case with optional subdirectory`);
|
|
547
699
|
process.exit(1);
|
|
548
700
|
}
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
if (
|
|
552
|
-
console.error(` ${pc.red("Error:")}
|
|
553
|
-
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
701
|
+
guardRspInitialized();
|
|
702
|
+
const path = join5(RSP_DIR, "specs", `${name}.md`);
|
|
703
|
+
if (existsSync5(path)) {
|
|
704
|
+
console.error(` ${pc.red("Error:")} spec file already exists: .rsp/specs/${name}.md`);
|
|
554
705
|
process.exit(1);
|
|
555
706
|
}
|
|
556
707
|
return withRspLock("add-spec", async () => {
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
if (existsSync5(path)) {
|
|
560
|
-
console.error(` ${pc.red("Error:")} spec file already exists: .rsp/specs/${name}.md`);
|
|
561
|
-
process.exit(1);
|
|
562
|
-
}
|
|
563
|
-
await mkdir3(dirname3(path), { recursive: true });
|
|
564
|
-
const resolvedProjectName = projectName === "project" ? await detectProjectName2() : projectName;
|
|
708
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
709
|
+
const resolvedProjectName = projectName === "project" ? await detectProjectName() : projectName;
|
|
565
710
|
const content = name === "design" ? generateDesignContent(resolvedProjectName) : generateSpecContent(name);
|
|
566
711
|
await writeFile4(path, content);
|
|
567
712
|
await buildSpecsIndex({ acquireLock: false });
|
|
@@ -570,148 +715,359 @@ async function addSpec(name, projectName = "project") {
|
|
|
570
715
|
});
|
|
571
716
|
}
|
|
572
717
|
|
|
718
|
+
// src/commands/archive.ts
|
|
719
|
+
import { existsSync as existsSync7 } from "fs";
|
|
720
|
+
import { mkdir as mkdir4, readFile as readFile6, rename, unlink as unlink2 } from "fs/promises";
|
|
721
|
+
import { basename as basename4, dirname as dirname6, join as join7 } from "path";
|
|
722
|
+
|
|
573
723
|
// src/commands/archive-index.ts
|
|
574
724
|
import { existsSync as existsSync6 } from "fs";
|
|
575
|
-
import { readFile as
|
|
576
|
-
import { basename as
|
|
725
|
+
import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
726
|
+
import { basename as basename3, dirname as dirname5, join as join6, relative as relative3 } from "path";
|
|
577
727
|
async function buildArchiveIndex({ acquireLock = true } = {}) {
|
|
578
728
|
if (acquireLock)
|
|
579
729
|
return withRspLock("archive-index", async () => buildArchiveIndex({ acquireLock: false }));
|
|
580
|
-
|
|
581
|
-
if (!existsSync6(archiveDir)) {
|
|
730
|
+
if (!existsSync6(ARCHIVES_DIR)) {
|
|
582
731
|
console.log(` ${pc.dim("No archives directory.")}
|
|
583
732
|
`);
|
|
584
733
|
return;
|
|
585
734
|
}
|
|
586
|
-
const archiveFiles = (await walkMarkdownFiles(
|
|
735
|
+
const archiveFiles = (await walkMarkdownFiles(ARCHIVES_DIR)).filter((fp) => basename3(fp) !== "INDEX.md").sort();
|
|
587
736
|
const lines = [];
|
|
588
737
|
lines.push("# Archive Index");
|
|
589
738
|
lines.push("");
|
|
590
739
|
lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
|
|
591
740
|
lines.push("");
|
|
592
741
|
if (archiveFiles.length === 0) {
|
|
593
|
-
lines.push("_No archived
|
|
742
|
+
lines.push("_No archived changes yet._");
|
|
594
743
|
} else {
|
|
595
|
-
lines.push("| Date |
|
|
596
|
-
lines.push("
|
|
744
|
+
lines.push("| Date | Change | Summary |");
|
|
745
|
+
lines.push("|------|--------|---------|");
|
|
597
746
|
}
|
|
598
747
|
for (const fp of archiveFiles) {
|
|
599
|
-
const base =
|
|
600
|
-
const archiveRel = relative3(
|
|
601
|
-
const archiveDirPart =
|
|
748
|
+
const base = basename3(fp);
|
|
749
|
+
const archiveRel = normalizeLogicalPath(relative3(ARCHIVES_DIR, fp));
|
|
750
|
+
const archiveDirPart = dirname5(archiveRel);
|
|
602
751
|
const dateMatch = base.match(/^(\d{4}-\d{2}-\d{2})_(.+)\.md$/);
|
|
603
752
|
const date = dateMatch ? dateMatch[1] : "\u2014";
|
|
604
753
|
const baseName = dateMatch ? dateMatch[2] : base.replace(/\.md$/, "");
|
|
605
|
-
const
|
|
754
|
+
const changeName = archiveDirPart !== "." ? `${archiveDirPart}/${baseName}` : baseName;
|
|
606
755
|
let summary = "";
|
|
607
756
|
try {
|
|
608
|
-
const content = await
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const specMatch = content.match(/^## Spec[\s\S]*?\n- Summary:[ \t]*([^\n]+)$/m);
|
|
614
|
-
if (specMatch)
|
|
615
|
-
summary = specMatch[1].trim();
|
|
616
|
-
}
|
|
757
|
+
const content = await readFile5(fp, "utf-8");
|
|
758
|
+
const proposal = extractSection(content, "Proposal");
|
|
759
|
+
const summaryLine = proposal.split("\n").map((line) => line.trim()).find((line) => line.startsWith("- Summary:"));
|
|
760
|
+
if (summaryLine)
|
|
761
|
+
summary = summaryLine.slice("- Summary:".length).trim();
|
|
617
762
|
} catch {
|
|
618
763
|
}
|
|
619
764
|
const summaryCell = summary ? summary.replace(/\|/g, "\\|") : "\u2014";
|
|
620
|
-
lines.push(`| ${date} | ${
|
|
765
|
+
lines.push(`| ${date} | ${changeName} | ${summaryCell} |`);
|
|
621
766
|
}
|
|
622
767
|
lines.push("");
|
|
623
|
-
lines.push("_Auto-generated by `rsp
|
|
624
|
-
const indexPath = join6(
|
|
768
|
+
lines.push("_Auto-generated by `rsp update`._");
|
|
769
|
+
const indexPath = join6(ARCHIVES_DIR, "INDEX.md");
|
|
625
770
|
await writeFile5(indexPath, lines.join("\n"));
|
|
626
|
-
console.log(` ${pc.green("INDEX.md updated:")} ${archiveFiles.length} archived
|
|
771
|
+
console.log(` ${pc.green("INDEX.md updated:")} ${archiveFiles.length} archived change(s).
|
|
772
|
+
`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/commands/archive.ts
|
|
776
|
+
async function archiveChange(name) {
|
|
777
|
+
if (!name) {
|
|
778
|
+
console.error(` ${pc.red("Usage:")} rsp archive <name>`);
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
if (!isValidChangeName(name)) {
|
|
782
|
+
console.error(` ${pc.red("Error:")} change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
783
|
+
process.exit(1);
|
|
784
|
+
}
|
|
785
|
+
const srcPath = join7(CHANGES_DIR, `${name}.md`);
|
|
786
|
+
if (!existsSync7(srcPath)) {
|
|
787
|
+
console.error(` ${pc.red("Change not found:")} .rsp/changes/${name}.md`);
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}
|
|
790
|
+
return withRspLock("archive-change", async () => {
|
|
791
|
+
const content = await readFile6(srcPath, "utf-8");
|
|
792
|
+
const checklist = collectArchiveChecklist(content);
|
|
793
|
+
for (const line of checklist)
|
|
794
|
+
console.log(` ${pc.yellow("\u26A0")} ${line}`);
|
|
795
|
+
if (checklist.length > 0)
|
|
796
|
+
console.log(` ${pc.dim("Archive will continue. Review the warnings above before treating this work as fully closed.")}
|
|
797
|
+
`);
|
|
798
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
799
|
+
const dir = dirname6(name);
|
|
800
|
+
const base = basename4(name);
|
|
801
|
+
const archiveSubdir = dir !== "." ? join7(ARCHIVES_DIR, dir) : ARCHIVES_DIR;
|
|
802
|
+
await mkdir4(archiveSubdir, { recursive: true });
|
|
803
|
+
const archiveName = resolveArchiveName(archiveSubdir, date, base);
|
|
804
|
+
await rename(srcPath, join7(archiveSubdir, archiveName));
|
|
805
|
+
const postArchiveWarnings = [];
|
|
806
|
+
let focusCleared = false;
|
|
807
|
+
const focusEntry = join7(FOCUS_DIR, name);
|
|
808
|
+
if (existsSync7(focusEntry)) {
|
|
809
|
+
try {
|
|
810
|
+
await unlink2(focusEntry);
|
|
811
|
+
await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
|
|
812
|
+
focusCleared = true;
|
|
813
|
+
} catch (error) {
|
|
814
|
+
postArchiveWarnings.push(`focus marker cleanup failed: ${toErrorMessage(error)}`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
await cleanupEmptyParentDirs(srcPath, CHANGES_DIR);
|
|
819
|
+
} catch (error) {
|
|
820
|
+
postArchiveWarnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
|
|
821
|
+
}
|
|
822
|
+
const clearedMsg = focusCleared ? ` ${pc.dim("focus marker cleared")}
|
|
823
|
+
` : "";
|
|
824
|
+
console.log(` ${pc.green("Archived:")} ${archiveName}
|
|
825
|
+
${clearedMsg}`);
|
|
826
|
+
try {
|
|
827
|
+
await buildArchiveIndex({ acquireLock: false });
|
|
828
|
+
} catch (error) {
|
|
829
|
+
postArchiveWarnings.push(`archive index rebuild failed: ${toErrorMessage(error)}`);
|
|
830
|
+
}
|
|
831
|
+
for (const warning of postArchiveWarnings)
|
|
832
|
+
console.log(` ${pc.yellow("\u26A0")} ${warning}`);
|
|
833
|
+
if (postArchiveWarnings.length > 0)
|
|
834
|
+
console.log(` ${pc.dim("Archive completed, but follow-up cleanup was only partially successful.")}
|
|
835
|
+
`);
|
|
836
|
+
if (existsSync7(".git")) {
|
|
837
|
+
const archiveRelPath = dir !== "." ? join7(dir, archiveName) : archiveName;
|
|
838
|
+
console.log(` ${pc.cyan("Git workflow:")}
|
|
627
839
|
`);
|
|
840
|
+
console.log(` git add .rsp/archives/${archiveRelPath}`);
|
|
841
|
+
console.log(` git commit -m "feat: archive ${name}"`);
|
|
842
|
+
console.log();
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
function resolveArchiveName(archiveSubdir, date, base) {
|
|
847
|
+
const initialName = `${date}_${base}.md`;
|
|
848
|
+
if (!existsSync7(join7(archiveSubdir, initialName)))
|
|
849
|
+
return initialName;
|
|
850
|
+
const MAX_SUFFIX = 9999;
|
|
851
|
+
for (let index = 2; index <= MAX_SUFFIX; index++) {
|
|
852
|
+
const candidate = `${date}_${base}-${index}.md`;
|
|
853
|
+
if (!existsSync7(join7(archiveSubdir, candidate)))
|
|
854
|
+
return candidate;
|
|
855
|
+
}
|
|
856
|
+
console.error(` ${pc.red("Error:")} archive name collision exceeded ${MAX_SUFFIX} attempts for ${base}`);
|
|
857
|
+
process.exit(1);
|
|
858
|
+
}
|
|
859
|
+
function collectArchiveChecklist(content) {
|
|
860
|
+
const warnings = [];
|
|
861
|
+
const tasksSection = extractSection(content, "Tasks");
|
|
862
|
+
const verifySection = extractSection(content, "Verify");
|
|
863
|
+
const taskTodos = getOpenCheckboxes(tasksSection);
|
|
864
|
+
if (taskTodos.length > 0)
|
|
865
|
+
warnings.push(`${taskTodos.length} task item(s) still incomplete`);
|
|
866
|
+
const verifyTodos = getOpenCheckboxes(verifySection);
|
|
867
|
+
if (verifyTodos.length > 0)
|
|
868
|
+
warnings.push(`${verifyTodos.length} Verify checklist item(s) are still incomplete`);
|
|
869
|
+
if (hasMeaningfulBlockers(content))
|
|
870
|
+
warnings.push("active blockers are present in the change file");
|
|
871
|
+
const scenarios = parseScenarios(content);
|
|
872
|
+
if (scenarios.length === 0)
|
|
873
|
+
warnings.push("no Scenario blocks found (some changes do not need them)");
|
|
874
|
+
return warnings;
|
|
628
875
|
}
|
|
629
876
|
|
|
630
877
|
// src/commands/check.ts
|
|
631
|
-
import { existsSync as
|
|
878
|
+
import { existsSync as existsSync8 } from "fs";
|
|
632
879
|
import { readFile as readFile7 } from "fs/promises";
|
|
633
|
-
import { join as
|
|
634
|
-
async function runCheck() {
|
|
635
|
-
const featuresDir = join7(RSP_DIR, "features");
|
|
880
|
+
import { join as join8, relative as relative4 } from "path";
|
|
881
|
+
async function runCheck(options = {}) {
|
|
636
882
|
const config = await loadRspConfig();
|
|
637
|
-
const
|
|
638
|
-
const validPriorities = resolvePriorities(config);
|
|
883
|
+
const validKinds = resolveKinds(config);
|
|
639
884
|
const requiredSections = resolveRequiredSections(config);
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if (
|
|
646
|
-
const entries = await walkFiles(
|
|
885
|
+
const choosePlaceholderRe = /^<choose:/i;
|
|
886
|
+
const diagnostics = [];
|
|
887
|
+
const runtime = [];
|
|
888
|
+
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
|
|
889
|
+
const addDiagnostic = (diagnostic) => diagnostics.push(diagnostic);
|
|
890
|
+
if (existsSync8(FOCUS_DIR)) {
|
|
891
|
+
const entries = await walkFiles(FOCUS_DIR, { onError: reportRuntime });
|
|
647
892
|
for (const entryPath of entries) {
|
|
648
|
-
const entryContent = relative4(
|
|
649
|
-
|
|
893
|
+
const entryContent = normalizeLogicalPath(relative4(FOCUS_DIR, entryPath));
|
|
894
|
+
let markerContent = "";
|
|
895
|
+
try {
|
|
896
|
+
markerContent = (await readFile7(entryPath, "utf-8")).trim();
|
|
897
|
+
} catch (error) {
|
|
898
|
+
addDiagnostic({
|
|
899
|
+
severity: "error",
|
|
900
|
+
code: "focus_marker_read_failed",
|
|
901
|
+
path: `focus.d/${entryContent}`,
|
|
902
|
+
change: entryContent,
|
|
903
|
+
message: "unable to read focus marker file",
|
|
904
|
+
hint: toErrorMessage(error)
|
|
905
|
+
});
|
|
906
|
+
reportRuntime({
|
|
907
|
+
code: "focus_marker_read_failed",
|
|
908
|
+
operation: "readFile",
|
|
909
|
+
path: entryPath,
|
|
910
|
+
message: toErrorMessage(error)
|
|
911
|
+
});
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
650
914
|
if (markerContent) {
|
|
651
|
-
|
|
652
|
-
|
|
915
|
+
addDiagnostic({
|
|
916
|
+
severity: "warning",
|
|
917
|
+
code: "focus_marker_not_empty",
|
|
918
|
+
path: `focus.d/${entryContent}`,
|
|
919
|
+
message: "should be an empty marker file; path is the source of truth"
|
|
920
|
+
});
|
|
653
921
|
}
|
|
654
|
-
const fp =
|
|
655
|
-
if (!
|
|
656
|
-
|
|
657
|
-
|
|
922
|
+
const fp = join8(CHANGES_DIR, `${entryContent}.md`);
|
|
923
|
+
if (!existsSync8(fp)) {
|
|
924
|
+
addDiagnostic({
|
|
925
|
+
severity: "error",
|
|
926
|
+
code: "focused_change_missing",
|
|
927
|
+
path: `focus.d/${entryContent}`,
|
|
928
|
+
change: entryContent,
|
|
929
|
+
message: `focuses "${entryContent}" but changes/${entryContent}.md not found`
|
|
930
|
+
});
|
|
658
931
|
}
|
|
659
932
|
}
|
|
660
933
|
}
|
|
661
|
-
const
|
|
662
|
-
if (
|
|
663
|
-
|
|
934
|
+
const changeFiles = existsSync8(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
|
|
935
|
+
if (changeFiles.length === 0) {
|
|
936
|
+
const result2 = {
|
|
937
|
+
command: "check",
|
|
938
|
+
ok: diagnostics.every((d) => d.severity !== "error"),
|
|
939
|
+
diagnostics,
|
|
940
|
+
runtime,
|
|
941
|
+
summary: {
|
|
942
|
+
changeFiles: 0,
|
|
943
|
+
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
944
|
+
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
if (options.json) {
|
|
948
|
+
emitJson(result2);
|
|
949
|
+
return result2;
|
|
950
|
+
}
|
|
951
|
+
console.log();
|
|
952
|
+
console.log(` ${pc.bold("RSP check")}`);
|
|
953
|
+
console.log();
|
|
954
|
+
if (diagnostics.length === 0) {
|
|
955
|
+
console.log(` ${pc.dim("No change files to check.")}
|
|
956
|
+
`);
|
|
957
|
+
return result2;
|
|
958
|
+
}
|
|
959
|
+
for (const diagnostic of diagnostics) {
|
|
960
|
+
const icon = diagnostic.severity === "error" ? pc.red("\u2717") : diagnostic.severity === "warning" ? pc.yellow("\u26A0") : pc.dim("\u2139");
|
|
961
|
+
const label = diagnostic.change ?? diagnostic.path;
|
|
962
|
+
const headline = label ? `${label} \u2014 ${diagnostic.message}` : diagnostic.message;
|
|
963
|
+
console.log(` ${icon} ${headline}`);
|
|
964
|
+
for (const detail of diagnostic.details || [])
|
|
965
|
+
console.log(` ${pc.dim(detail)}`);
|
|
966
|
+
if (diagnostic.hint)
|
|
967
|
+
console.log(` ${pc.dim(diagnostic.hint)}`);
|
|
968
|
+
}
|
|
969
|
+
console.log();
|
|
970
|
+
console.log(` ${pc.red(String(result2.summary.errors))} error(s), ${pc.yellow(String(result2.summary.warnings))} warning(s) in 0 change file(s).
|
|
664
971
|
`);
|
|
665
|
-
return
|
|
972
|
+
return result2;
|
|
666
973
|
}
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
const name = featureNameFromPath(featuresDir, fp);
|
|
974
|
+
for (const fp of changeFiles) {
|
|
975
|
+
const name = changeNameFromPath(CHANGES_DIR, fp);
|
|
670
976
|
let content;
|
|
671
977
|
try {
|
|
672
978
|
content = await readFile7(fp, "utf-8");
|
|
673
|
-
} catch {
|
|
674
|
-
|
|
675
|
-
|
|
979
|
+
} catch (error) {
|
|
980
|
+
addDiagnostic({
|
|
981
|
+
severity: "error",
|
|
982
|
+
code: "change_read_failed",
|
|
983
|
+
change: name,
|
|
984
|
+
path: fp,
|
|
985
|
+
message: "unable to read file",
|
|
986
|
+
hint: toErrorMessage(error)
|
|
987
|
+
});
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
let fm = null;
|
|
991
|
+
try {
|
|
992
|
+
fm = parseFrontmatter(content);
|
|
993
|
+
} catch (error) {
|
|
994
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
995
|
+
addDiagnostic({
|
|
996
|
+
severity: "error",
|
|
997
|
+
code: "invalid_frontmatter",
|
|
998
|
+
change: name,
|
|
999
|
+
path: fp,
|
|
1000
|
+
message: `invalid YAML frontmatter (${message})`
|
|
1001
|
+
});
|
|
676
1002
|
continue;
|
|
677
1003
|
}
|
|
678
|
-
const fm = parseFrontmatter(content);
|
|
679
1004
|
if (!fm) {
|
|
680
|
-
|
|
681
|
-
|
|
1005
|
+
addDiagnostic({
|
|
1006
|
+
severity: "error",
|
|
1007
|
+
code: "missing_frontmatter",
|
|
1008
|
+
change: name,
|
|
1009
|
+
path: fp,
|
|
1010
|
+
message: "missing YAML frontmatter"
|
|
1011
|
+
});
|
|
682
1012
|
}
|
|
683
1013
|
for (const section of requiredSections) {
|
|
684
1014
|
if (!new RegExp(`^## ${section}$`, "m").test(content)) {
|
|
685
|
-
|
|
686
|
-
|
|
1015
|
+
addDiagnostic({
|
|
1016
|
+
severity: "error",
|
|
1017
|
+
code: "missing_section",
|
|
1018
|
+
change: name,
|
|
1019
|
+
path: fp,
|
|
1020
|
+
message: `missing "## ${section}" section`
|
|
1021
|
+
});
|
|
687
1022
|
}
|
|
688
1023
|
}
|
|
689
1024
|
if (fm) {
|
|
690
|
-
if (!("
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1025
|
+
if (!("kind" in fm)) {
|
|
1026
|
+
addDiagnostic({
|
|
1027
|
+
severity: "error",
|
|
1028
|
+
code: "missing_kind",
|
|
1029
|
+
change: name,
|
|
1030
|
+
path: fp,
|
|
1031
|
+
message: "missing frontmatter field: kind"
|
|
1032
|
+
});
|
|
1033
|
+
} else if (choosePlaceholderRe.test(String(fm.kind))) {
|
|
1034
|
+
addDiagnostic({
|
|
1035
|
+
severity: "error",
|
|
1036
|
+
code: "placeholder_kind",
|
|
1037
|
+
change: name,
|
|
1038
|
+
path: fp,
|
|
1039
|
+
message: `kind still uses the template placeholder; choose one of: ${validKinds.join(", ")}`
|
|
1040
|
+
});
|
|
1041
|
+
} else if (!validKinds.includes(String(fm.kind))) {
|
|
1042
|
+
addDiagnostic({
|
|
1043
|
+
severity: "error",
|
|
1044
|
+
code: "invalid_kind",
|
|
1045
|
+
change: name,
|
|
1046
|
+
path: fp,
|
|
1047
|
+
message: `invalid kind "${fm.kind}" (valid: ${validKinds.join(", ")})`
|
|
1048
|
+
});
|
|
703
1049
|
}
|
|
704
1050
|
}
|
|
705
|
-
const
|
|
706
|
-
if (
|
|
707
|
-
const heading =
|
|
1051
|
+
const headingLine = content.split("\n").map((line) => line.trim()).find((line) => line.startsWith("# Change:"));
|
|
1052
|
+
if (headingLine) {
|
|
1053
|
+
const heading = headingLine.slice("# Change:".length).trim();
|
|
708
1054
|
if (heading !== name) {
|
|
709
|
-
|
|
710
|
-
|
|
1055
|
+
addDiagnostic({
|
|
1056
|
+
severity: "error",
|
|
1057
|
+
code: "heading_mismatch",
|
|
1058
|
+
change: name,
|
|
1059
|
+
path: fp,
|
|
1060
|
+
message: `# Change: heading "${heading}" differs from change name`
|
|
1061
|
+
});
|
|
711
1062
|
}
|
|
712
1063
|
} else {
|
|
713
|
-
|
|
714
|
-
|
|
1064
|
+
addDiagnostic({
|
|
1065
|
+
severity: "error",
|
|
1066
|
+
code: "missing_heading",
|
|
1067
|
+
change: name,
|
|
1068
|
+
path: fp,
|
|
1069
|
+
message: 'missing "# Change:" heading'
|
|
1070
|
+
});
|
|
715
1071
|
}
|
|
716
1072
|
const deltas = detectDeltaSections(content);
|
|
717
1073
|
if (deltas.added || deltas.modified || deltas.removed) {
|
|
@@ -722,7 +1078,13 @@ async function runCheck() {
|
|
|
722
1078
|
parts.push("MODIFIED");
|
|
723
1079
|
if (deltas.removed)
|
|
724
1080
|
parts.push("REMOVED");
|
|
725
|
-
|
|
1081
|
+
addDiagnostic({
|
|
1082
|
+
severity: "info",
|
|
1083
|
+
code: "delta_markers_found",
|
|
1084
|
+
change: name,
|
|
1085
|
+
path: fp,
|
|
1086
|
+
message: `delta markers found: ${parts.join(", ")}`
|
|
1087
|
+
});
|
|
726
1088
|
}
|
|
727
1089
|
const scenarios = parseScenarios(content);
|
|
728
1090
|
if (scenarios.length > 0) {
|
|
@@ -742,509 +1104,482 @@ async function runCheck() {
|
|
|
742
1104
|
scenarioIssues.push(`"${s.heading}" missing ${missing.join("/")}`);
|
|
743
1105
|
}
|
|
744
1106
|
if (scenarioIssues.length > 0) {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1107
|
+
addDiagnostic({
|
|
1108
|
+
severity: "warning",
|
|
1109
|
+
code: "scenario_format_issues",
|
|
1110
|
+
change: name,
|
|
1111
|
+
path: fp,
|
|
1112
|
+
message: "scenario format issues",
|
|
1113
|
+
details: scenarioIssues
|
|
1114
|
+
});
|
|
749
1115
|
}
|
|
750
1116
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
console.log(` ${pc.red("\u2717")} ${name} \u2014 duplicate dependency "${dep}"`);
|
|
762
|
-
errors++;
|
|
763
|
-
continue;
|
|
764
|
-
}
|
|
765
|
-
seen.add(dep);
|
|
766
|
-
allDeps.set(name, [...allDeps.get(name) || [], dep]);
|
|
767
|
-
const depFp = join7(featuresDir, `${dep}.md`);
|
|
768
|
-
if (!existsSync7(depFp)) {
|
|
769
|
-
console.log(` ${pc.red("\u2717")} ${name} \u2014 depends on "${dep}" but features/${dep}.md not found`);
|
|
770
|
-
errors++;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
1117
|
+
}
|
|
1118
|
+
const result = {
|
|
1119
|
+
command: "check",
|
|
1120
|
+
ok: diagnostics.every((d) => d.severity !== "error"),
|
|
1121
|
+
diagnostics,
|
|
1122
|
+
runtime,
|
|
1123
|
+
summary: {
|
|
1124
|
+
changeFiles: changeFiles.length,
|
|
1125
|
+
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
1126
|
+
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
773
1127
|
}
|
|
1128
|
+
};
|
|
1129
|
+
if (options.json) {
|
|
1130
|
+
emitJson(result);
|
|
1131
|
+
return result;
|
|
774
1132
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
1133
|
+
console.log();
|
|
1134
|
+
console.log(` ${pc.bold("RSP check")}`);
|
|
1135
|
+
console.log();
|
|
1136
|
+
for (const diagnostic of diagnostics) {
|
|
1137
|
+
const icon = diagnostic.severity === "error" ? pc.red("\u2717") : diagnostic.severity === "warning" ? pc.yellow("\u26A0") : pc.dim("\u2139");
|
|
1138
|
+
const label = diagnostic.change ?? diagnostic.path;
|
|
1139
|
+
const headline = label ? `${label} \u2014 ${diagnostic.message}` : diagnostic.message;
|
|
1140
|
+
console.log(` ${icon} ${headline}`);
|
|
1141
|
+
for (const detail of diagnostic.details || [])
|
|
1142
|
+
console.log(` ${pc.dim(detail)}`);
|
|
1143
|
+
if (diagnostic.hint)
|
|
1144
|
+
console.log(` ${pc.dim(diagnostic.hint)}`);
|
|
779
1145
|
}
|
|
780
1146
|
console.log();
|
|
781
|
-
if (errors === 0 && warnings === 0)
|
|
782
|
-
console.log(` ${pc.green("\u2713")} All ${
|
|
1147
|
+
if (result.summary.errors === 0 && result.summary.warnings === 0)
|
|
1148
|
+
console.log(` ${pc.green("\u2713")} All ${changeFiles.length} change file(s) valid.
|
|
783
1149
|
`);
|
|
784
1150
|
else
|
|
785
|
-
console.log(` ${pc.red(String(errors))} error(s), ${pc.yellow(String(warnings))} warning(s) in ${
|
|
1151
|
+
console.log(` ${pc.red(String(result.summary.errors))} error(s), ${pc.yellow(String(result.summary.warnings))} warning(s) in ${changeFiles.length} change file(s).
|
|
786
1152
|
`);
|
|
787
|
-
return
|
|
1153
|
+
return result;
|
|
788
1154
|
}
|
|
789
1155
|
|
|
790
|
-
// src/commands/
|
|
791
|
-
import { existsSync as
|
|
792
|
-
import { mkdir as
|
|
793
|
-
import {
|
|
794
|
-
async function
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
816
|
-
const dir = dirname5(name);
|
|
817
|
-
const base = basename5(name);
|
|
818
|
-
const archiveSubdir = dir !== "." ? join8(ARCHIVES_DIR, dir) : ARCHIVES_DIR;
|
|
819
|
-
const archiveName = `${date}_${base}.md`;
|
|
820
|
-
await mkdir4(archiveSubdir, { recursive: true });
|
|
821
|
-
await rename(srcPath, join8(archiveSubdir, archiveName));
|
|
822
|
-
let activeCleared = false;
|
|
823
|
-
const activeEntry = join8(ACTIVE_DIR, name);
|
|
824
|
-
if (existsSync8(activeEntry)) {
|
|
825
|
-
await unlink2(activeEntry);
|
|
826
|
-
await cleanupEmptyParentDirs(activeEntry, ACTIVE_DIR);
|
|
827
|
-
activeCleared = true;
|
|
1156
|
+
// src/commands/create.ts
|
|
1157
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1158
|
+
import { mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
|
|
1159
|
+
import { dirname as dirname7, join as join9 } from "path";
|
|
1160
|
+
async function createChange(name, summary = "", kind) {
|
|
1161
|
+
if (!name) {
|
|
1162
|
+
console.error(` ${pc.red("Usage:")} rsp create <name> [summary]`);
|
|
1163
|
+
process.exit(1);
|
|
1164
|
+
}
|
|
1165
|
+
if (!isValidChangeName(name)) {
|
|
1166
|
+
console.error(` ${pc.red("Error:")} change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
1167
|
+
process.exit(1);
|
|
1168
|
+
}
|
|
1169
|
+
guardRspInitialized();
|
|
1170
|
+
return withRspLock("create-change", async () => {
|
|
1171
|
+
const changePath = join9(CHANGES_DIR, `${name}.md`);
|
|
1172
|
+
await mkdir5(dirname7(changePath), { recursive: true });
|
|
1173
|
+
const existed = existsSync9(changePath);
|
|
1174
|
+
if (!existed) {
|
|
1175
|
+
const content = generateChangeContent(name, summary, kind);
|
|
1176
|
+
await writeFile6(changePath, content);
|
|
1177
|
+
await mkdir5(FOCUS_DIR, { recursive: true });
|
|
1178
|
+
const focusEntry = join9(FOCUS_DIR, name);
|
|
1179
|
+
await mkdir5(dirname7(focusEntry), { recursive: true });
|
|
1180
|
+
await writeFile6(focusEntry, "");
|
|
828
1181
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
if (existsSync8(".git")) {
|
|
837
|
-
const archiveRelPath = dir !== "." ? join8(dir, archiveName) : archiveName;
|
|
838
|
-
console.log(` ${pc.cyan("Git workflow:")}
|
|
1182
|
+
const label = existed ? "Using" : pc.green("Created");
|
|
1183
|
+
console.log(` ${label}: ${changePath}`);
|
|
1184
|
+
if (existed)
|
|
1185
|
+
console.log(` ${pc.dim("Unchanged focus.")} Run: rsp focus ${name}`);
|
|
1186
|
+
else
|
|
1187
|
+
console.log(` ${pc.dim("focused via focus.d")} \u2192 ${name}`);
|
|
1188
|
+
console.log(` ${pc.cyan("Next:")} edit the change file and complete the tasks
|
|
839
1189
|
`);
|
|
840
|
-
console.log(` git add .rsp/archives/${archiveRelPath}`);
|
|
841
|
-
console.log(` git commit -m "feat: complete ${name}"`);
|
|
842
|
-
console.log();
|
|
843
|
-
}
|
|
844
1190
|
});
|
|
845
1191
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1192
|
+
|
|
1193
|
+
// src/commands/doctor.ts
|
|
1194
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1195
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1196
|
+
import { basename as basename5, join as join10, relative as relative5 } from "path";
|
|
1197
|
+
async function runDoctor(options = {}) {
|
|
1198
|
+
const checks = [];
|
|
1199
|
+
const runtime = [];
|
|
1200
|
+
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
|
|
1201
|
+
const rspRulesPath = join10(RSP_DIR, "rules", "rsp-rules.md");
|
|
1202
|
+
const designPath = join10(RSP_DIR, "specs", "design.md");
|
|
1203
|
+
const specsIndexPath = join10(RSP_DIR, "specs", "INDEX.md");
|
|
1204
|
+
const archivesIndexPath = join10(RSP_DIR, "archives", "INDEX.md");
|
|
1205
|
+
reportCheck(checks, ".rsp exists", existsSync10(RSP_DIR), "Run: rsp init");
|
|
1206
|
+
reportCheck(checks, "rules/rsp-rules.md exists", existsSync10(rspRulesPath), "Run: rsp init");
|
|
1207
|
+
reportCheck(checks, "specs/design.md exists", existsSync10(designPath), "Run: rsp init");
|
|
1208
|
+
reportCheck(checks, "specs/INDEX.md exists", existsSync10(specsIndexPath), "Run: rsp update");
|
|
1209
|
+
reportCheck(checks, "archives/INDEX.md exists", existsSync10(archivesIndexPath), "Run: rsp update");
|
|
1210
|
+
await checkAgents(checks, reportRuntime);
|
|
1211
|
+
await checkGeneratedIndex(checks, reportRuntime, specsIndexPath, "_Auto-generated by `rsp update`._", "specs/INDEX.md has generated signature", "Run: rsp update");
|
|
1212
|
+
await checkGeneratedIndex(checks, reportRuntime, archivesIndexPath, "_Auto-generated by `rsp update`._", "archives/INDEX.md has generated signature", "Run: rsp update");
|
|
1213
|
+
await checkArchiveNaming(checks, reportRuntime);
|
|
1214
|
+
await checkConfigSemantics(checks, reportRuntime);
|
|
1215
|
+
await checkActiveChangeConsistency(checks, reportRuntime);
|
|
1216
|
+
const result = {
|
|
1217
|
+
command: "doctor",
|
|
1218
|
+
ok: checks.every((check) => check.status !== "issue"),
|
|
1219
|
+
checks,
|
|
1220
|
+
runtime,
|
|
1221
|
+
summary: {
|
|
1222
|
+
issues: checks.filter((check) => check.status === "issue").length
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
if (options.json) {
|
|
1226
|
+
emitJson(result);
|
|
1227
|
+
return result;
|
|
861
1228
|
}
|
|
862
|
-
|
|
1229
|
+
console.log();
|
|
1230
|
+
console.log(` ${pc.bold("RSP doctor")}`);
|
|
1231
|
+
console.log();
|
|
1232
|
+
for (const check of checks)
|
|
1233
|
+
printDoctorCheck(check);
|
|
1234
|
+
if (result.summary.issues === 0)
|
|
1235
|
+
console.log(`
|
|
1236
|
+
${pc.green("\u2713")} RSP setup looks healthy.
|
|
1237
|
+
`);
|
|
1238
|
+
else
|
|
1239
|
+
console.log(`
|
|
1240
|
+
${pc.yellow(String(result.summary.issues))} issue(s) detected.
|
|
1241
|
+
`);
|
|
1242
|
+
return result;
|
|
863
1243
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
if (hasDependents)
|
|
868
|
-
await runCheck();
|
|
869
|
-
} catch {
|
|
870
|
-
console.warn(` ${pc.dim("\u26A0 could not verify dependents for")} "${name}"`);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
async function cleanupEmptyParentDirs(path, stopDir) {
|
|
874
|
-
let parent = dirname5(path);
|
|
875
|
-
while (parent !== stopDir) {
|
|
876
|
-
try {
|
|
877
|
-
const remaining = await readdir2(parent);
|
|
878
|
-
if (remaining.length === 0)
|
|
879
|
-
await rmdir(parent);
|
|
880
|
-
else
|
|
881
|
-
break;
|
|
882
|
-
} catch {
|
|
883
|
-
break;
|
|
884
|
-
}
|
|
885
|
-
parent = dirname5(parent);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// src/commands/deps.ts
|
|
890
|
-
import { existsSync as existsSync9 } from "fs";
|
|
891
|
-
import { readFile as readFile9 } from "fs/promises";
|
|
892
|
-
import { join as join9 } from "path";
|
|
893
|
-
async function showDependencies(options = false) {
|
|
894
|
-
const resolved = typeof options === "boolean" ? { mermaid: options } : options;
|
|
895
|
-
const mermaid = Boolean(resolved.mermaid);
|
|
896
|
-
const focus = resolved.focus?.trim();
|
|
897
|
-
const reverse = resolved.reverse?.trim();
|
|
898
|
-
if (focus && reverse)
|
|
899
|
-
throw new Error("Use either --focus or --reverse, not both");
|
|
900
|
-
const featuresDir = join9(RSP_DIR, "features");
|
|
901
|
-
const featureFiles = existsSync9(featuresDir) ? await walkMarkdownFiles(featuresDir) : [];
|
|
902
|
-
console.log();
|
|
903
|
-
console.log(` ${pc.bold("RSP dependencies")}`);
|
|
904
|
-
console.log();
|
|
905
|
-
if (featureFiles.length === 0) {
|
|
906
|
-
console.log(` ${pc.dim("No features found.")}
|
|
907
|
-
`);
|
|
1244
|
+
function reportCheck(checks, label, ok, fixHint) {
|
|
1245
|
+
if (ok) {
|
|
1246
|
+
checks.push({ status: "ok", label });
|
|
908
1247
|
return;
|
|
909
1248
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
try {
|
|
916
|
-
content = await readFile9(fp, "utf-8");
|
|
917
|
-
} catch {
|
|
918
|
-
continue;
|
|
919
|
-
}
|
|
920
|
-
const fm = parseFrontmatter(content);
|
|
921
|
-
const depsRaw = fm?.["depends-on"] ? Array.isArray(fm["depends-on"]) ? fm["depends-on"] : [fm["depends-on"]] : [];
|
|
922
|
-
const deps = depsRaw.filter(Boolean);
|
|
923
|
-
records.push({ name, deps });
|
|
924
|
-
for (const dep of deps) {
|
|
925
|
-
const list = dependents.get(dep) || [];
|
|
926
|
-
list.push(name);
|
|
927
|
-
dependents.set(dep, list);
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
let visibleRecords = [...records];
|
|
931
|
-
if (focus) {
|
|
932
|
-
const related = /* @__PURE__ */ new Set([focus]);
|
|
933
|
-
const target = records.find((record) => record.name === focus);
|
|
934
|
-
if (target) {
|
|
935
|
-
for (const dep of target.deps)
|
|
936
|
-
related.add(dep);
|
|
937
|
-
}
|
|
938
|
-
for (const dependent of dependents.get(focus) || [])
|
|
939
|
-
related.add(dependent);
|
|
940
|
-
visibleRecords = records.filter((record) => related.has(record.name));
|
|
941
|
-
} else if (reverse) {
|
|
942
|
-
const related = new Set(dependents.get(reverse) || []);
|
|
943
|
-
visibleRecords = records.filter((record) => related.has(record.name));
|
|
944
|
-
}
|
|
945
|
-
if ((focus || reverse) && visibleRecords.length === 0) {
|
|
946
|
-
console.log(` ${pc.dim("No dependency matches found for the current filter.")}
|
|
947
|
-
`);
|
|
1249
|
+
checks.push({ status: "issue", label, hint: fixHint });
|
|
1250
|
+
}
|
|
1251
|
+
function printDoctorCheck(check) {
|
|
1252
|
+
if (check.status === "ok") {
|
|
1253
|
+
console.log(` ${pc.green("\u2713")} ${check.label}`);
|
|
948
1254
|
return;
|
|
949
1255
|
}
|
|
950
|
-
if (
|
|
951
|
-
console.log(
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
for (const { name } of visibleRecords) {
|
|
955
|
-
const id = safeId(name);
|
|
956
|
-
const label = name.replace(/"/g, "#quot;");
|
|
957
|
-
console.log(` ${id}["${label}"]`);
|
|
958
|
-
}
|
|
959
|
-
const visibleNames = new Set(visibleRecords.map((record) => record.name));
|
|
960
|
-
for (const { name, deps } of visibleRecords) {
|
|
961
|
-
const fromId = safeId(name);
|
|
962
|
-
for (const dep of deps) {
|
|
963
|
-
if (!visibleNames.has(dep))
|
|
964
|
-
continue;
|
|
965
|
-
const toId = safeId(dep);
|
|
966
|
-
console.log(` ${fromId} --> ${toId}`);
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
console.log("```");
|
|
970
|
-
console.log();
|
|
1256
|
+
if (check.status === "info") {
|
|
1257
|
+
console.log(` ${pc.dim(`Info: ${check.message || check.label}`)}`);
|
|
1258
|
+
if (check.hint)
|
|
1259
|
+
console.log(` ${pc.dim(check.hint)}`);
|
|
971
1260
|
return;
|
|
972
1261
|
}
|
|
973
|
-
console.log(` ${"
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
const depText = deps.length > 0 ? deps.join(", ") : "\u2014";
|
|
977
|
-
const dependentText = (dependents.get(name) || []).length > 0 ? pc.dim((dependents.get(name) || []).join(", ")) : "\u2014";
|
|
978
|
-
console.log(` ${name.padEnd(28)} ${depText.padEnd(28)} ${dependentText}`);
|
|
979
|
-
}
|
|
980
|
-
console.log();
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// src/commands/doctor.ts
|
|
984
|
-
import { existsSync as existsSync10 } from "fs";
|
|
985
|
-
import { readFile as readFile10 } from "fs/promises";
|
|
986
|
-
import { join as join10, relative as relative5 } from "path";
|
|
987
|
-
async function runDoctor() {
|
|
988
|
-
let issues = 0;
|
|
989
|
-
console.log();
|
|
990
|
-
console.log(` ${pc.bold("RSP doctor")}`);
|
|
991
|
-
console.log();
|
|
992
|
-
const rspRulesPath = join10(RSP_DIR, "rules", "rsp-rules.md");
|
|
993
|
-
const designPath = join10(RSP_DIR, "specs", "design.md");
|
|
994
|
-
const specsIndexPath = join10(RSP_DIR, "specs", "INDEX.md");
|
|
995
|
-
const archivesIndexPath = join10(RSP_DIR, "archives", "INDEX.md");
|
|
996
|
-
issues += reportCheck(".rsp exists", existsSync10(RSP_DIR), "Run: rsp init");
|
|
997
|
-
issues += reportCheck("rules/rsp-rules.md exists", existsSync10(rspRulesPath), "Run: rsp init");
|
|
998
|
-
issues += reportCheck("specs/design.md exists", existsSync10(designPath), "Run: rsp init");
|
|
999
|
-
issues += reportCheck("specs/INDEX.md exists", existsSync10(specsIndexPath), "Run: rsp specs-index");
|
|
1000
|
-
issues += reportCheck("archives/INDEX.md exists", existsSync10(archivesIndexPath), "Run: rsp archive-index");
|
|
1001
|
-
issues += await checkAgents();
|
|
1002
|
-
issues += await checkGeneratedIndex(specsIndexPath, "_Auto-generated by `rsp specs-index`._", "specs/INDEX.md has generated signature", "Run: rsp specs-index");
|
|
1003
|
-
issues += await checkGeneratedIndex(archivesIndexPath, "_Auto-generated by `rsp archive-index`._", "archives/INDEX.md has generated signature", "Run: rsp archive-index");
|
|
1004
|
-
issues += await checkArchiveNaming();
|
|
1005
|
-
issues += await checkConfigSemantics();
|
|
1006
|
-
issues += await checkActiveFeatureConsistency();
|
|
1007
|
-
if (issues === 0)
|
|
1008
|
-
console.log(`
|
|
1009
|
-
${pc.green("\u2713")} RSP setup looks healthy.
|
|
1010
|
-
`);
|
|
1011
|
-
else
|
|
1012
|
-
console.log(`
|
|
1013
|
-
${pc.yellow(String(issues))} issue(s) detected.
|
|
1014
|
-
`);
|
|
1015
|
-
return issues;
|
|
1016
|
-
}
|
|
1017
|
-
function reportCheck(label, ok, fixHint) {
|
|
1018
|
-
if (ok) {
|
|
1019
|
-
console.log(` ${pc.green("\u2713")} ${label}`);
|
|
1020
|
-
return 0;
|
|
1021
|
-
}
|
|
1022
|
-
console.log(` ${pc.yellow("!")} ${label}`);
|
|
1023
|
-
if (fixHint)
|
|
1024
|
-
console.log(` ${pc.dim(fixHint)}`);
|
|
1025
|
-
return 1;
|
|
1262
|
+
console.log(` ${pc.yellow("!")} ${check.message || check.label}`);
|
|
1263
|
+
if (check.hint)
|
|
1264
|
+
console.log(` ${pc.dim(check.hint)}`);
|
|
1026
1265
|
}
|
|
1027
|
-
async function checkAgents() {
|
|
1266
|
+
async function checkAgents(checks, reportRuntime) {
|
|
1028
1267
|
if (!existsSync10("AGENTS.md")) {
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1268
|
+
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing", hint: "Run: rsp init" });
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
let agents;
|
|
1272
|
+
try {
|
|
1273
|
+
agents = await readFile8("AGENTS.md", "utf-8");
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
reportRuntime({
|
|
1276
|
+
code: "agents_read_failed",
|
|
1277
|
+
operation: "readFile",
|
|
1278
|
+
path: "AGENTS.md",
|
|
1279
|
+
message: toErrorMessage(error)
|
|
1280
|
+
});
|
|
1281
|
+
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md could not be read", hint: "Check AGENTS.md permissions or recreate it with rsp init" });
|
|
1282
|
+
return;
|
|
1032
1283
|
}
|
|
1033
|
-
const agents = await readFile10("AGENTS.md", "utf-8");
|
|
1034
1284
|
if (hasRspAgentsBlock(agents)) {
|
|
1035
|
-
|
|
1036
|
-
return
|
|
1285
|
+
checks.push({ status: "ok", label: "AGENTS.md contains managed RSP block" });
|
|
1286
|
+
return;
|
|
1037
1287
|
}
|
|
1038
|
-
|
|
1039
|
-
console.log(` ${pc.dim("Run: rsp init --agents-mode managed")}`);
|
|
1040
|
-
return 1;
|
|
1288
|
+
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing managed RSP block", hint: "Run: rsp init" });
|
|
1041
1289
|
}
|
|
1042
|
-
async function checkGeneratedIndex(path, signature, label, fixHint) {
|
|
1290
|
+
async function checkGeneratedIndex(checks, reportRuntime, path, signature, label, fixHint) {
|
|
1043
1291
|
if (!existsSync10(path))
|
|
1044
|
-
return
|
|
1045
|
-
|
|
1292
|
+
return;
|
|
1293
|
+
let content;
|
|
1294
|
+
try {
|
|
1295
|
+
content = await readFile8(path, "utf-8");
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
reportRuntime({
|
|
1298
|
+
code: "generated_index_read_failed",
|
|
1299
|
+
operation: "readFile",
|
|
1300
|
+
path,
|
|
1301
|
+
message: toErrorMessage(error)
|
|
1302
|
+
});
|
|
1303
|
+
checks.push({ status: "issue", label, message: `${label} could not be read`, hint: fixHint });
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1046
1306
|
if (content.includes(signature)) {
|
|
1047
|
-
|
|
1048
|
-
return
|
|
1307
|
+
checks.push({ status: "ok", label });
|
|
1308
|
+
return;
|
|
1049
1309
|
}
|
|
1050
|
-
|
|
1051
|
-
console.log(` ${pc.dim(fixHint)}`);
|
|
1052
|
-
return 1;
|
|
1310
|
+
checks.push({ status: "issue", label, hint: fixHint });
|
|
1053
1311
|
}
|
|
1054
|
-
async function checkArchiveNaming() {
|
|
1312
|
+
async function checkArchiveNaming(checks, reportRuntime) {
|
|
1055
1313
|
const archivesDir = join10(RSP_DIR, "archives");
|
|
1056
|
-
const archiveFiles = existsSync10(archivesDir) ? (await walkMarkdownFiles(archivesDir)).filter((fp) => relative5(archivesDir, fp) !== "INDEX.md") : [];
|
|
1057
|
-
const invalidFiles = archiveFiles.map((fp) => relative5(archivesDir, fp)).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(fp
|
|
1314
|
+
const archiveFiles = existsSync10(archivesDir) ? (await walkMarkdownFiles(archivesDir, { onError: reportRuntime })).filter((fp) => relative5(archivesDir, fp) !== "INDEX.md") : [];
|
|
1315
|
+
const invalidFiles = archiveFiles.map((fp) => normalizeLogicalPath(relative5(archivesDir, fp))).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(basename5(fp)) === false);
|
|
1058
1316
|
if (invalidFiles.length === 0) {
|
|
1059
|
-
|
|
1060
|
-
return
|
|
1317
|
+
checks.push({ status: "ok", label: "archived change files follow the date_name.md convention" });
|
|
1318
|
+
return;
|
|
1061
1319
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1320
|
+
checks.push({
|
|
1321
|
+
status: "issue",
|
|
1322
|
+
label: "archived change files follow the date_name.md convention",
|
|
1323
|
+
message: `archived change files with invalid names: ${invalidFiles.join(", ")}`,
|
|
1324
|
+
hint: "Rename archive files to YYYY-MM-DD_name.md and run: rsp update"
|
|
1325
|
+
});
|
|
1065
1326
|
}
|
|
1066
|
-
async function checkConfigSemantics() {
|
|
1327
|
+
async function checkConfigSemantics(checks, reportRuntime) {
|
|
1067
1328
|
if (!existsSync10(CONFIG_PATH))
|
|
1068
|
-
return
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1329
|
+
return;
|
|
1330
|
+
let content;
|
|
1331
|
+
try {
|
|
1332
|
+
content = await readFile8(CONFIG_PATH, "utf-8");
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
reportRuntime({
|
|
1335
|
+
code: "config_read_failed",
|
|
1336
|
+
operation: "readFile",
|
|
1337
|
+
path: CONFIG_PATH,
|
|
1338
|
+
message: toErrorMessage(error)
|
|
1339
|
+
});
|
|
1340
|
+
checks.push({ status: "issue", label: "config.yaml semantic checks", message: "config.yaml could not be read", hint: "Check .rsp/config.yaml permissions or recreate it with rsp init" });
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
let parsed;
|
|
1344
|
+
try {
|
|
1345
|
+
parsed = parseYamlText(content);
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1348
|
+
checks.push({ status: "issue", label: "config.yaml semantic checks", message: "config.yaml is not valid YAML", hint: message });
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
reportConfigListField(checks, parsed, "kinds");
|
|
1352
|
+
if ("required_sections" in parsed) {
|
|
1353
|
+
checks.push({
|
|
1354
|
+
status: "issue",
|
|
1355
|
+
label: "config.yaml semantic checks",
|
|
1356
|
+
message: 'config.yaml field "required_sections" is no longer supported',
|
|
1357
|
+
hint: 'RSP keeps the core change section structure fixed. Remove "required_sections" from .rsp/config.yaml.'
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
function reportConfigListField(checks, parsed, key) {
|
|
1078
1362
|
if (!(key in parsed))
|
|
1079
|
-
return
|
|
1363
|
+
return;
|
|
1080
1364
|
const value = parsed[key];
|
|
1081
1365
|
if (!Array.isArray(value)) {
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1366
|
+
checks.push({
|
|
1367
|
+
status: "issue",
|
|
1368
|
+
label: "config.yaml semantic checks",
|
|
1369
|
+
message: `config.yaml field "${key}" must be a YAML list`,
|
|
1370
|
+
hint: `Update .rsp/config.yaml so "${key}" is a YAML sequence (for example: ${key}: [item])`
|
|
1371
|
+
});
|
|
1372
|
+
return;
|
|
1085
1373
|
}
|
|
1086
1374
|
const items = value.map(String);
|
|
1087
1375
|
const duplicates = [...new Set(items.filter((item, index) => items.indexOf(item) !== index))];
|
|
1088
1376
|
if (duplicates.length === 0) {
|
|
1089
|
-
|
|
1090
|
-
return
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
const
|
|
1101
|
-
const
|
|
1102
|
-
const
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1377
|
+
checks.push({ status: "ok", label: `config.yaml field "${key}" has valid list semantics` });
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
checks.push({
|
|
1381
|
+
status: "issue",
|
|
1382
|
+
label: "config.yaml semantic checks",
|
|
1383
|
+
message: `config.yaml field "${key}" contains duplicate entries: ${duplicates.join(", ")}`,
|
|
1384
|
+
hint: `Remove duplicate values from .rsp/config.yaml:${key}`
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
async function checkActiveChangeConsistency(checks, reportRuntime) {
|
|
1388
|
+
const changeFiles = existsSync10(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
|
|
1389
|
+
const changeNames = new Set(changeFiles.map((fp) => changeNameFromPath(CHANGES_DIR, fp)));
|
|
1390
|
+
const focusEntries = existsSync10(FOCUS_DIR) ? await walkFiles(FOCUS_DIR, { onError: reportRuntime }) : [];
|
|
1391
|
+
const focusNames = new Set(focusEntries.map((fp) => normalizeLogicalPath(relative5(FOCUS_DIR, fp))));
|
|
1392
|
+
const missingChangeFiles = [...focusNames].filter((name) => !changeNames.has(name));
|
|
1393
|
+
const unfocusedChangeFiles = [...changeNames].filter((name) => !focusNames.has(name));
|
|
1394
|
+
if (missingChangeFiles.length === 0) {
|
|
1395
|
+
checks.push({ status: "ok", label: "every focus marker maps to a change file" });
|
|
1107
1396
|
} else {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1397
|
+
checks.push({
|
|
1398
|
+
status: "issue",
|
|
1399
|
+
label: "every focus marker maps to a change file",
|
|
1400
|
+
message: `focus markers without matching change files: ${missingChangeFiles.join(", ")}`,
|
|
1401
|
+
hint: "Run: remove stale files in .rsp/focus.d/ or recreate the change with rsp create <name>"
|
|
1402
|
+
});
|
|
1111
1403
|
}
|
|
1112
|
-
if (
|
|
1113
|
-
|
|
1404
|
+
if (unfocusedChangeFiles.length === 0) {
|
|
1405
|
+
checks.push({ status: "ok", label: "focus markers are consistent with open changes" });
|
|
1114
1406
|
} else {
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1407
|
+
const preview = unfocusedChangeFiles.slice(0, 5).join(", ");
|
|
1408
|
+
const extra = unfocusedChangeFiles.length > 5 ? ` (+${unfocusedChangeFiles.length - 5} more)` : "";
|
|
1409
|
+
checks.push({
|
|
1410
|
+
status: "info",
|
|
1411
|
+
label: "focus markers are consistent with open changes",
|
|
1412
|
+
message: `unfocused open changes: ${preview}${extra}`,
|
|
1413
|
+
hint: "Only changes listed in .rsp/focus.d/ are currently focused. Use rsp focus <name> to foreground one."
|
|
1414
|
+
});
|
|
1118
1415
|
}
|
|
1119
|
-
return issues;
|
|
1120
1416
|
}
|
|
1121
1417
|
|
|
1122
|
-
// src/commands/
|
|
1418
|
+
// src/commands/focus.ts
|
|
1123
1419
|
import { existsSync as existsSync11 } from "fs";
|
|
1124
|
-
import { mkdir as
|
|
1125
|
-
import {
|
|
1126
|
-
|
|
1127
|
-
|
|
1420
|
+
import { mkdir as mkdir6, unlink as unlink3, writeFile as writeFile7 } from "fs/promises";
|
|
1421
|
+
import { dirname as dirname8, join as join11 } from "path";
|
|
1422
|
+
async function focusChange(name) {
|
|
1423
|
+
if (!name) {
|
|
1424
|
+
console.error(` ${pc.red("Usage:")} rsp focus <name>`);
|
|
1425
|
+
process.exit(1);
|
|
1426
|
+
}
|
|
1427
|
+
if (!isValidChangeName(name)) {
|
|
1428
|
+
console.error(` ${pc.red("Error:")} change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
1429
|
+
process.exit(1);
|
|
1430
|
+
}
|
|
1431
|
+
guardRspInitialized();
|
|
1432
|
+
const changePath = join11(CHANGES_DIR, `${name}.md`);
|
|
1433
|
+
if (!existsSync11(changePath)) {
|
|
1434
|
+
console.error(` ${pc.red("Change not found:")} .rsp/changes/${name}.md`);
|
|
1435
|
+
process.exit(1);
|
|
1436
|
+
}
|
|
1437
|
+
return withRspLock("focus-change", async () => {
|
|
1438
|
+
await mkdir6(FOCUS_DIR, { recursive: true });
|
|
1439
|
+
const focusEntry = join11(FOCUS_DIR, name);
|
|
1440
|
+
await mkdir6(dirname8(focusEntry), { recursive: true });
|
|
1441
|
+
await writeFile7(focusEntry, "");
|
|
1442
|
+
console.log(` ${pc.green("Focused:")} ${name}`);
|
|
1443
|
+
console.log(` ${pc.dim("focus.d")} \u2192 ${name}`);
|
|
1444
|
+
console.log();
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
async function unfocusChange(name) {
|
|
1448
|
+
if (!name) {
|
|
1449
|
+
console.error(` ${pc.red("Usage:")} rsp unfocus <name>`);
|
|
1450
|
+
process.exit(1);
|
|
1451
|
+
}
|
|
1452
|
+
if (!isValidChangeName(name)) {
|
|
1453
|
+
console.error(` ${pc.red("Error:")} change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
1454
|
+
process.exit(1);
|
|
1455
|
+
}
|
|
1456
|
+
guardRspInitialized();
|
|
1457
|
+
const focusEntry = join11(FOCUS_DIR, name);
|
|
1458
|
+
if (!existsSync11(focusEntry)) {
|
|
1459
|
+
console.error(` ${pc.red("Focus marker not found:")} .rsp/focus.d/${name}`);
|
|
1460
|
+
process.exit(1);
|
|
1461
|
+
}
|
|
1462
|
+
return withRspLock("unfocus-change", async () => {
|
|
1463
|
+
await unlink3(focusEntry);
|
|
1464
|
+
await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
|
|
1465
|
+
console.log(` ${pc.green("Unfocused:")} ${name}`);
|
|
1466
|
+
console.log(` ${pc.dim("focus.d cleared")} \u2192 ${name}`);
|
|
1467
|
+
console.log();
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// src/commands/init.ts
|
|
1472
|
+
import { existsSync as existsSync12 } from "fs";
|
|
1473
|
+
import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
|
|
1474
|
+
import { join as join12 } from "path";
|
|
1475
|
+
function generateConfigTemplate() {
|
|
1476
|
+
const fmtList = (items, indent) => items.map((i) => `${" ".repeat(indent)}# - ${i}`).join("\n");
|
|
1477
|
+
return `# RSP project configuration
|
|
1478
|
+
# Uncomment any section below to customize classification values.
|
|
1128
1479
|
#
|
|
1129
1480
|
# Built-in defaults:
|
|
1130
|
-
#
|
|
1131
|
-
# priorities: low, medium, high, critical
|
|
1132
|
-
# required_sections: Spec, Plan
|
|
1481
|
+
# kinds: ${VALID_KINDS.join(", ")}
|
|
1133
1482
|
#
|
|
1134
|
-
#
|
|
1135
|
-
|
|
1136
|
-
# - ready
|
|
1137
|
-
# - in-progress
|
|
1138
|
-
# - blocked
|
|
1139
|
-
# - done
|
|
1140
|
-
#
|
|
1141
|
-
# priorities:
|
|
1142
|
-
# - low
|
|
1143
|
-
# - medium
|
|
1144
|
-
# - high
|
|
1145
|
-
# - critical
|
|
1146
|
-
#
|
|
1147
|
-
# required_sections:
|
|
1148
|
-
# - Spec
|
|
1149
|
-
# - Plan
|
|
1483
|
+
# kinds:
|
|
1484
|
+
${fmtList(VALID_KINDS, 2)}
|
|
1150
1485
|
`;
|
|
1151
|
-
async function detectProjectName3() {
|
|
1152
|
-
try {
|
|
1153
|
-
if (existsSync11("package.json")) {
|
|
1154
|
-
const raw = await readFile11("package.json", "utf-8");
|
|
1155
|
-
const pkg = JSON.parse(raw);
|
|
1156
|
-
if (pkg.name && pkg.name !== "")
|
|
1157
|
-
return pkg.name;
|
|
1158
|
-
}
|
|
1159
|
-
} catch {
|
|
1160
|
-
}
|
|
1161
|
-
return basename6(process.cwd());
|
|
1162
1486
|
}
|
|
1163
1487
|
async function ensureFile(path, content) {
|
|
1164
|
-
if (
|
|
1488
|
+
if (existsSync12(path))
|
|
1165
1489
|
return false;
|
|
1166
|
-
await
|
|
1490
|
+
await writeFile8(path, content);
|
|
1167
1491
|
return true;
|
|
1168
1492
|
}
|
|
1169
1493
|
function toTitle(projectName) {
|
|
1170
1494
|
return projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
|
|
1171
1495
|
}
|
|
1172
1496
|
async function initProject(args = {}) {
|
|
1173
|
-
const isNew = !
|
|
1174
|
-
const projectName = await
|
|
1497
|
+
const isNew = !existsSync12(RSP_DIR);
|
|
1498
|
+
const projectName = await detectProjectName();
|
|
1499
|
+
const mode = args.agentsMode ?? "managed";
|
|
1500
|
+
if (mode !== "managed" && mode !== "print") {
|
|
1501
|
+
console.error(` ${pc.red("Error:")} unsupported --agents-mode "${mode}". Use: managed or print`);
|
|
1502
|
+
process.exit(1);
|
|
1503
|
+
}
|
|
1175
1504
|
return withRspLock("init", async () => {
|
|
1176
1505
|
const dirs = [
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1506
|
+
join12(RSP_DIR, "rules"),
|
|
1507
|
+
join12(RSP_DIR, "specs"),
|
|
1508
|
+
CHANGES_DIR,
|
|
1509
|
+
FOCUS_DIR,
|
|
1510
|
+
join12(RSP_DIR, "archives")
|
|
1182
1511
|
];
|
|
1183
1512
|
for (const d of dirs)
|
|
1184
|
-
await
|
|
1513
|
+
await mkdir7(d, { recursive: true });
|
|
1185
1514
|
let created = false;
|
|
1186
|
-
const bundledRules = await
|
|
1187
|
-
created = await ensureFile(
|
|
1188
|
-
created = await ensureFile(
|
|
1189
|
-
const createdSpecsIndex = await ensureFile(
|
|
1515
|
+
const bundledRules = await readFile9(join12(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
|
|
1516
|
+
created = await ensureFile(join12(RSP_DIR, "rules", "rsp-rules.md"), bundledRules) || created;
|
|
1517
|
+
created = await ensureFile(join12(RSP_DIR, "config.yaml"), generateConfigTemplate()) || created;
|
|
1518
|
+
const createdSpecsIndex = await ensureFile(join12(RSP_DIR, "specs", "INDEX.md"), "# Specs Index\n\n_Project-level specs and design notes._\n");
|
|
1190
1519
|
created = createdSpecsIndex || created;
|
|
1191
|
-
const createdDesign = await ensureFile(
|
|
1520
|
+
const createdDesign = await ensureFile(join12(RSP_DIR, "specs", "design.md"), generateDesignContent(projectName));
|
|
1192
1521
|
created = createdDesign || created;
|
|
1193
|
-
const createdArchivesIndex = await ensureFile(
|
|
1522
|
+
const createdArchivesIndex = await ensureFile(join12(RSP_DIR, "archives", "INDEX.md"), "# Archive Index\n");
|
|
1194
1523
|
created = createdArchivesIndex || created;
|
|
1195
|
-
created = await ensureFile(
|
|
1196
|
-
created = await ensureFile(
|
|
1197
|
-
created = await ensureFile(
|
|
1524
|
+
created = await ensureFile(join12(CHANGES_DIR, ".gitkeep"), "") || created;
|
|
1525
|
+
created = await ensureFile(join12(FOCUS_DIR, ".gitkeep"), "") || created;
|
|
1526
|
+
created = await ensureFile(join12(RSP_DIR, ".gitignore"), "# Transient files that should not be committed\n.lock\n") || created;
|
|
1198
1527
|
if (args.withProjectRules)
|
|
1199
|
-
created = await ensureFile(
|
|
1528
|
+
created = await ensureFile(join12(RSP_DIR, "rules", "project-rules.md"), generateProjectRulesContent(projectName)) || created;
|
|
1529
|
+
if (args.withProjectSetup) {
|
|
1530
|
+
const changePath = join12(CHANGES_DIR, "project-setup.md");
|
|
1531
|
+
const focusMarker = join12(FOCUS_DIR, "project-setup");
|
|
1532
|
+
created = await ensureFile(changePath, generateChangeContent("project-setup")) || created;
|
|
1533
|
+
created = await ensureFile(focusMarker, "") || created;
|
|
1534
|
+
}
|
|
1200
1535
|
if (createdSpecsIndex || createdDesign)
|
|
1201
1536
|
await buildSpecsIndex({ acquireLock: false });
|
|
1202
1537
|
if (createdArchivesIndex)
|
|
1203
1538
|
await buildArchiveIndex({ acquireLock: false });
|
|
1204
1539
|
const agentsPath = "AGENTS.md";
|
|
1205
1540
|
const title = toTitle(projectName);
|
|
1206
|
-
|
|
1207
|
-
if (!
|
|
1208
|
-
|
|
1209
|
-
await writeFile6(agentsPath, `# ${title}
|
|
1541
|
+
let agentsUpdated = false;
|
|
1542
|
+
if (!existsSync12(agentsPath)) {
|
|
1543
|
+
await writeFile8(agentsPath, `# ${title}
|
|
1210
1544
|
|
|
1211
1545
|
${upsertRspAgentsBlock("").content}
|
|
1212
1546
|
`);
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
} else
|
|
1216
|
-
const existing = await
|
|
1547
|
+
created = true;
|
|
1548
|
+
agentsUpdated = true;
|
|
1549
|
+
} else {
|
|
1550
|
+
const existing = await readFile9(agentsPath, "utf-8");
|
|
1217
1551
|
const next = upsertRspAgentsBlock(existing);
|
|
1218
1552
|
if (next.changed) {
|
|
1219
|
-
await
|
|
1553
|
+
await writeFile8(agentsPath, next.content);
|
|
1220
1554
|
created = true;
|
|
1555
|
+
agentsUpdated = true;
|
|
1221
1556
|
}
|
|
1222
1557
|
}
|
|
1223
1558
|
if (mode === "print") {
|
|
1224
|
-
const managed =
|
|
1225
|
-
|
|
1226
|
-
${renderRspAgentsBlock()}
|
|
1227
|
-
`;
|
|
1559
|
+
const managed = await readFile9(agentsPath, "utf-8");
|
|
1228
1560
|
console.log(`
|
|
1229
1561
|
${managed}
|
|
1230
1562
|
`);
|
|
1231
1563
|
}
|
|
1232
1564
|
if (isNew) {
|
|
1565
|
+
const createdProjectSetup = args.withProjectSetup ? "\n .rsp/changes/project-setup.md\n .rsp/focus.d/project-setup" : "";
|
|
1566
|
+
const createdAgents = agentsUpdated ? "\n AGENTS.md" : "";
|
|
1567
|
+
const next = args.withProjectSetup ? "fill .rsp/changes/project-setup.md" : "rsp create project-setup";
|
|
1233
1568
|
console.log(`
|
|
1234
1569
|
${pc.green("RSP scaffolded.")}
|
|
1235
1570
|
`);
|
|
1236
1571
|
console.log(` Created: .rsp/rules/
|
|
1237
1572
|
.rsp/specs/
|
|
1238
|
-
.rsp/
|
|
1239
|
-
.rsp/
|
|
1573
|
+
.rsp/changes/
|
|
1574
|
+
.rsp/focus.d/
|
|
1240
1575
|
.rsp/archives/
|
|
1241
1576
|
.rsp/.gitignore
|
|
1242
1577
|
.rsp/config.yaml
|
|
1243
|
-
.rsp/specs/design.md
|
|
1244
|
-
AGENTS.md
|
|
1578
|
+
.rsp/specs/design.md${createdProjectSetup}${createdAgents}
|
|
1245
1579
|
`);
|
|
1246
|
-
console.log(` ${pc.cyan("Next:")}
|
|
1247
|
-
${pc.dim("
|
|
1580
|
+
console.log(` ${pc.cyan("Next:")} ${next}
|
|
1581
|
+
${pc.dim("Then:")} fill .rsp/specs/design.md
|
|
1582
|
+
${pc.dim("Also:")} rsp status rsp check
|
|
1248
1583
|
`);
|
|
1249
1584
|
} else if (created) {
|
|
1250
1585
|
console.log(`
|
|
@@ -1258,129 +1593,101 @@ ${managed}
|
|
|
1258
1593
|
});
|
|
1259
1594
|
}
|
|
1260
1595
|
|
|
1261
|
-
// src/commands/new-feature.ts
|
|
1262
|
-
import { existsSync as existsSync12 } from "fs";
|
|
1263
|
-
import { mkdir as mkdir6, writeFile as writeFile7 } from "fs/promises";
|
|
1264
|
-
import { dirname as dirname6, join as join12 } from "path";
|
|
1265
|
-
async function newFeature(name, summary = "") {
|
|
1266
|
-
if (!name) {
|
|
1267
|
-
console.error(` ${pc.red("Usage:")} rsp new <name> [summary]`);
|
|
1268
|
-
process.exit(1);
|
|
1269
|
-
}
|
|
1270
|
-
if (/^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/.test(name) === false) {
|
|
1271
|
-
console.error(` ${pc.red("Error:")} feature name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
1272
|
-
process.exit(1);
|
|
1273
|
-
}
|
|
1274
|
-
if (name === "init") {
|
|
1275
|
-
console.error(` ${pc.red("Error:")} "init" is a reserved workflow name, not a feature name`);
|
|
1276
|
-
process.exit(1);
|
|
1277
|
-
}
|
|
1278
|
-
const rspRulesPath = join12(RSP_DIR, "rules", "rsp-rules.md");
|
|
1279
|
-
const designPath = join12(RSP_DIR, "specs", "design.md");
|
|
1280
|
-
if (!existsSync12(rspRulesPath) || !existsSync12(designPath)) {
|
|
1281
|
-
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
1282
|
-
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
1283
|
-
process.exit(1);
|
|
1284
|
-
}
|
|
1285
|
-
return withRspLock("new-feature", async () => {
|
|
1286
|
-
const featurePath = join12(RSP_DIR, "features", `${name}.md`);
|
|
1287
|
-
await mkdir6(dirname6(featurePath), { recursive: true });
|
|
1288
|
-
const existed = existsSync12(featurePath);
|
|
1289
|
-
if (!existed) {
|
|
1290
|
-
const content = generateFeatureContent(name, summary);
|
|
1291
|
-
await writeFile7(featurePath, content);
|
|
1292
|
-
}
|
|
1293
|
-
await mkdir6(ACTIVE_DIR, { recursive: true });
|
|
1294
|
-
const activeEntry = join12(ACTIVE_DIR, name);
|
|
1295
|
-
await mkdir6(dirname6(activeEntry), { recursive: true });
|
|
1296
|
-
await writeFile7(activeEntry, "");
|
|
1297
|
-
if (!existed) {
|
|
1298
|
-
const archiveIndex = join12(ARCHIVES_DIR, "INDEX.md");
|
|
1299
|
-
if (existsSync12(archiveIndex))
|
|
1300
|
-
await buildArchiveIndex({ acquireLock: false });
|
|
1301
|
-
}
|
|
1302
|
-
const label = existed ? "Using" : pc.green("Created");
|
|
1303
|
-
console.log(` ${label}: ${featurePath}
|
|
1304
|
-
${pc.dim("active.d")} \u2192 ${name}
|
|
1305
|
-
`);
|
|
1306
|
-
});
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
1596
|
// src/commands/status.ts
|
|
1310
1597
|
import { existsSync as existsSync13 } from "fs";
|
|
1311
|
-
import { readFile as
|
|
1598
|
+
import { readFile as readFile10, stat as stat2 } from "fs/promises";
|
|
1312
1599
|
import { join as join13, relative as relative6 } from "path";
|
|
1313
|
-
var
|
|
1314
|
-
var
|
|
1600
|
+
var COL_CHANGE = 32;
|
|
1601
|
+
var COL_KIND = 10;
|
|
1315
1602
|
var COL_AGE = 7;
|
|
1316
1603
|
var COL_PROGRESS = 10;
|
|
1317
|
-
async function showStatus(options = {}) {
|
|
1318
|
-
const
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
const
|
|
1331
|
-
const featureMap = {};
|
|
1332
|
-
for (const fp of featureFiles) {
|
|
1333
|
-
const name = featureNameFromPath(featuresDir, fp);
|
|
1334
|
-
featureMap[name] = fp;
|
|
1335
|
-
}
|
|
1336
|
-
console.log();
|
|
1337
|
-
console.log(` ${pc.bold("RSP status")}`);
|
|
1338
|
-
console.log();
|
|
1339
|
-
if (activeSet.size > 0) {
|
|
1340
|
-
console.log(` ${pc.cyan("Active:")} ${[...activeSet].join(", ")}`);
|
|
1341
|
-
console.log();
|
|
1342
|
-
} else {
|
|
1343
|
-
console.log(` ${pc.dim("No active feature.")} Run: rsp new <name>`);
|
|
1344
|
-
console.log();
|
|
1345
|
-
}
|
|
1346
|
-
const allNames = [.../* @__PURE__ */ new Set([...Object.keys(featureMap), ...activeSet])].sort();
|
|
1347
|
-
if (allNames.length === 0) {
|
|
1348
|
-
console.log(` ${pc.dim("No features found.")} Run: rsp new <name>
|
|
1349
|
-
`);
|
|
1350
|
-
return;
|
|
1351
|
-
}
|
|
1604
|
+
async function showStatus(options = {}, runOptions = {}) {
|
|
1605
|
+
const focusedSet = /* @__PURE__ */ new Set();
|
|
1606
|
+
const runtime = [];
|
|
1607
|
+
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(runOptions.verbose) && !runOptions.json);
|
|
1608
|
+
if (existsSync13(FOCUS_DIR)) {
|
|
1609
|
+
const entries = await walkFiles(FOCUS_DIR, { onError: reportRuntime });
|
|
1610
|
+
for (const entryPath of entries)
|
|
1611
|
+
focusedSet.add(normalizeLogicalPath(relative6(FOCUS_DIR, entryPath)));
|
|
1612
|
+
}
|
|
1613
|
+
const changeFiles = existsSync13(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
|
|
1614
|
+
const changeMap = {};
|
|
1615
|
+
for (const fp of changeFiles)
|
|
1616
|
+
changeMap[changeNameFromPath(CHANGES_DIR, fp)] = fp;
|
|
1617
|
+
const allNames = [.../* @__PURE__ */ new Set([...Object.keys(changeMap), ...focusedSet])].sort();
|
|
1352
1618
|
const records = [];
|
|
1619
|
+
const outputRecords = [];
|
|
1353
1620
|
for (const name of allNames) {
|
|
1354
|
-
const fp =
|
|
1355
|
-
let
|
|
1621
|
+
const fp = changeMap[name];
|
|
1622
|
+
let kind = "\u2014";
|
|
1356
1623
|
let progress = "\u2014";
|
|
1357
|
-
let
|
|
1358
|
-
|
|
1359
|
-
const isActive = activeSet.has(name);
|
|
1624
|
+
let ageDays = null;
|
|
1625
|
+
const isFocused = focusedSet.has(name);
|
|
1360
1626
|
let isBlocked = false;
|
|
1627
|
+
let done = 0;
|
|
1628
|
+
let total = 0;
|
|
1361
1629
|
if (fp) {
|
|
1362
1630
|
try {
|
|
1363
|
-
const content = await
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1631
|
+
const content = await readFile10(fp, "utf-8");
|
|
1632
|
+
try {
|
|
1633
|
+
const fm = parseFrontmatter(content);
|
|
1634
|
+
kind = fm?.kind ? String(fm.kind) : "\u2014";
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
kind = "(invalid)";
|
|
1637
|
+
reportRuntime({
|
|
1638
|
+
code: "frontmatter_parse_failed",
|
|
1639
|
+
operation: "parseFrontmatter",
|
|
1640
|
+
path: fp,
|
|
1641
|
+
message: toErrorMessage(error)
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
isBlocked = hasMeaningfulBlockers(content);
|
|
1369
1645
|
const cb = countCheckboxes(content);
|
|
1370
|
-
|
|
1646
|
+
done = cb.done;
|
|
1647
|
+
total = cb.total;
|
|
1648
|
+
progress = `${done}/${total}`;
|
|
1649
|
+
} catch (error) {
|
|
1650
|
+
reportRuntime({
|
|
1651
|
+
code: "change_read_failed",
|
|
1652
|
+
operation: "readFile",
|
|
1653
|
+
path: fp,
|
|
1654
|
+
message: toErrorMessage(error)
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
try {
|
|
1371
1658
|
const s = await stat2(fp);
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1659
|
+
ageDays = Math.floor((Date.now() - s.mtime.getTime()) / (1e3 * 60 * 60 * 24));
|
|
1660
|
+
} catch (error) {
|
|
1661
|
+
reportRuntime({
|
|
1662
|
+
code: "change_stat_failed",
|
|
1663
|
+
operation: "stat",
|
|
1664
|
+
path: fp,
|
|
1665
|
+
message: toErrorMessage(error)
|
|
1666
|
+
});
|
|
1376
1667
|
}
|
|
1377
1668
|
} else {
|
|
1378
|
-
|
|
1669
|
+
kind = "(missing)";
|
|
1670
|
+
reportRuntime({
|
|
1671
|
+
code: "focused_change_missing",
|
|
1672
|
+
operation: "resolveChange",
|
|
1673
|
+
path: join13(CHANGES_DIR, `${name}.md`),
|
|
1674
|
+
message: "focus marker points to a missing change file"
|
|
1675
|
+
});
|
|
1379
1676
|
}
|
|
1380
|
-
|
|
1677
|
+
const age = ageDays === null ? "\u2014" : String(ageDays);
|
|
1678
|
+
records.push({ name, kind, progress, age, isFocused, isBlocked });
|
|
1679
|
+
outputRecords.push({
|
|
1680
|
+
name,
|
|
1681
|
+
kind,
|
|
1682
|
+
progress: { done, total },
|
|
1683
|
+
ageDays,
|
|
1684
|
+
isFocused,
|
|
1685
|
+
isBlocked,
|
|
1686
|
+
path: fp ? normalizeLogicalPath(fp) : null
|
|
1687
|
+
});
|
|
1381
1688
|
}
|
|
1382
1689
|
const filteredRecords = records.filter((record) => {
|
|
1383
|
-
if (options.
|
|
1690
|
+
if (options.focused && !record.isFocused)
|
|
1384
1691
|
return false;
|
|
1385
1692
|
if (options.blocked && !record.isBlocked)
|
|
1386
1693
|
return false;
|
|
@@ -1391,48 +1698,90 @@ async function showStatus(options = {}) {
|
|
|
1391
1698
|
}
|
|
1392
1699
|
return true;
|
|
1393
1700
|
});
|
|
1394
|
-
|
|
1395
|
-
|
|
1701
|
+
const filteredOutputRecords = outputRecords.filter((record) => {
|
|
1702
|
+
if (options.focused && !record.isFocused)
|
|
1703
|
+
return false;
|
|
1704
|
+
if (options.blocked && !record.isBlocked)
|
|
1705
|
+
return false;
|
|
1706
|
+
if (typeof options.stale === "number") {
|
|
1707
|
+
if (record.ageDays === null || record.ageDays < options.stale)
|
|
1708
|
+
return false;
|
|
1709
|
+
}
|
|
1710
|
+
return true;
|
|
1711
|
+
});
|
|
1712
|
+
const blockedCount = filteredOutputRecords.filter((r) => r.isBlocked).length;
|
|
1713
|
+
const statusResult = {
|
|
1714
|
+
command: "status",
|
|
1715
|
+
ok: true,
|
|
1716
|
+
filters: {
|
|
1717
|
+
focused: Boolean(options.focused),
|
|
1718
|
+
blocked: Boolean(options.blocked),
|
|
1719
|
+
stale: typeof options.stale === "number" ? options.stale : null
|
|
1720
|
+
},
|
|
1721
|
+
focused: [...focusedSet].sort(),
|
|
1722
|
+
records: filteredOutputRecords,
|
|
1723
|
+
summary: {
|
|
1724
|
+
total: filteredOutputRecords.length,
|
|
1725
|
+
focused: filteredOutputRecords.filter((r) => r.isFocused).length,
|
|
1726
|
+
blocked: blockedCount
|
|
1727
|
+
},
|
|
1728
|
+
archiveTrend: await readArchiveTrend(runtime, runOptions),
|
|
1729
|
+
runtime
|
|
1730
|
+
};
|
|
1731
|
+
if (runOptions.json) {
|
|
1732
|
+
emitJson(statusResult);
|
|
1733
|
+
return statusResult;
|
|
1734
|
+
}
|
|
1735
|
+
console.log();
|
|
1736
|
+
console.log(` ${pc.bold("RSP status")}`);
|
|
1737
|
+
console.log();
|
|
1738
|
+
if (focusedSet.size > 0) {
|
|
1739
|
+
console.log(` ${pc.cyan("Focused:")} ${[...focusedSet].join(", ")}`);
|
|
1740
|
+
console.log();
|
|
1741
|
+
} else {
|
|
1742
|
+
console.log(` ${pc.dim("No focused change.")} Run: rsp create <name> or rsp focus <name>`);
|
|
1396
1743
|
console.log();
|
|
1397
|
-
|
|
1744
|
+
}
|
|
1745
|
+
if (allNames.length === 0) {
|
|
1746
|
+
console.log(` ${pc.dim("No changes found.")} Run: rsp create <name>
|
|
1747
|
+
`);
|
|
1748
|
+
return statusResult;
|
|
1749
|
+
}
|
|
1750
|
+
if (filteredRecords.length === 0) {
|
|
1751
|
+
console.log(` ${pc.dim("No changes match the current filters.")}
|
|
1752
|
+
`);
|
|
1753
|
+
return statusResult;
|
|
1398
1754
|
}
|
|
1399
1755
|
const pad = (s, w) => s.padEnd(w);
|
|
1400
|
-
console.log(` ${pad("
|
|
1401
|
-
console.log(` ${"\u2500".repeat(
|
|
1402
|
-
let blockedCount = 0;
|
|
1756
|
+
console.log(` ${pad("Change", COL_CHANGE)} ${pad("Kind", COL_KIND)} ${pad("Age(d)", COL_AGE)} ${pad("Progress", COL_PROGRESS)}`);
|
|
1757
|
+
console.log(` ${"\u2500".repeat(COL_CHANGE)} ${"\u2500".repeat(COL_KIND)} ${"\u2500".repeat(COL_AGE)} ${"\u2500".repeat(COL_PROGRESS)}`);
|
|
1403
1758
|
for (const r of filteredRecords) {
|
|
1404
|
-
const marker = r.
|
|
1405
|
-
const namePadded = r.name.padEnd(
|
|
1406
|
-
const
|
|
1759
|
+
const marker = r.isFocused ? pc.cyan("*") : " ";
|
|
1760
|
+
const namePadded = r.name.padEnd(COL_CHANGE - 1);
|
|
1761
|
+
const kindPadded = r.kind.padEnd(COL_KIND);
|
|
1407
1762
|
const agePadded = r.age.padEnd(COL_AGE);
|
|
1408
1763
|
const progressPadded = r.progress.padEnd(COL_PROGRESS);
|
|
1409
1764
|
const nameDisplay = r.isBlocked ? pc.yellow(namePadded) : namePadded;
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
console.log(` ${marker}${nameDisplay} ${statusDisplay} ${ageDisplay} ${progressPadded} ${r.depends}`);
|
|
1413
|
-
if (r.isBlocked)
|
|
1414
|
-
blockedCount++;
|
|
1765
|
+
const ageDisplay = r.isFocused ? pc.cyan(agePadded) : agePadded;
|
|
1766
|
+
console.log(` ${marker}${nameDisplay} ${kindPadded} ${ageDisplay} ${progressPadded}`);
|
|
1415
1767
|
}
|
|
1416
1768
|
console.log();
|
|
1417
|
-
|
|
1418
|
-
const doneCount = filteredRecords.filter((r) => r.status === "done").length;
|
|
1419
|
-
const activeCount = filteredRecords.filter((r) => r.isActive).length;
|
|
1420
|
-
console.log(` ${pc.bold("Summary:")} ${total} feature(s), ${activeCount} active, ${doneCount} done, ${pc.yellow(String(blockedCount))} blocked`);
|
|
1769
|
+
console.log(` ${pc.bold("Summary:")} ${statusResult.summary.total} change(s), ${statusResult.summary.focused} focused, ${pc.yellow(String(statusResult.summary.blocked))} blocked`);
|
|
1421
1770
|
console.log();
|
|
1422
|
-
|
|
1423
|
-
if (
|
|
1771
|
+
printArchiveTrend(statusResult.archiveTrend);
|
|
1772
|
+
if (statusResult.summary.blocked > 0) {
|
|
1424
1773
|
const blocked = filteredRecords.filter((r) => r.isBlocked);
|
|
1425
1774
|
console.log(` ${pc.yellow("Blocked:")} ${blocked.map((r) => r.name).join(", ")}`);
|
|
1426
1775
|
console.log();
|
|
1427
1776
|
}
|
|
1777
|
+
return statusResult;
|
|
1428
1778
|
}
|
|
1429
|
-
async function
|
|
1430
|
-
const
|
|
1431
|
-
const indexPath = join13(archiveDir, "INDEX.md");
|
|
1779
|
+
async function readArchiveTrend(runtime, runOptions) {
|
|
1780
|
+
const indexPath = join13(ARCHIVES_DIR, "INDEX.md");
|
|
1432
1781
|
if (!existsSync13(indexPath))
|
|
1433
|
-
return;
|
|
1782
|
+
return [];
|
|
1434
1783
|
try {
|
|
1435
|
-
const content = await
|
|
1784
|
+
const content = await readFile10(indexPath, "utf-8");
|
|
1436
1785
|
const dateRe = /^\| (\d{4}-\d{2})-\d{2} \|/gm;
|
|
1437
1786
|
const counts = {};
|
|
1438
1787
|
let match;
|
|
@@ -1442,58 +1791,119 @@ async function showArchiveTrend() {
|
|
|
1442
1791
|
}
|
|
1443
1792
|
const months = Object.keys(counts).sort();
|
|
1444
1793
|
if (months.length === 0)
|
|
1445
|
-
return;
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1794
|
+
return [];
|
|
1795
|
+
return months.map((month) => ({ month, count: counts[month] }));
|
|
1796
|
+
} catch (error) {
|
|
1797
|
+
recordRuntimeDiagnostic(runtime, {
|
|
1798
|
+
code: "archive_index_read_failed",
|
|
1799
|
+
operation: "readFile",
|
|
1800
|
+
path: indexPath,
|
|
1801
|
+
message: toErrorMessage(error)
|
|
1802
|
+
}, Boolean(runOptions.verbose) && !runOptions.json);
|
|
1803
|
+
return [];
|
|
1454
1804
|
}
|
|
1455
1805
|
}
|
|
1806
|
+
function printArchiveTrend(trend) {
|
|
1807
|
+
if (trend.length === 0)
|
|
1808
|
+
return;
|
|
1809
|
+
console.log(` ${pc.bold("Archive trend:")}`);
|
|
1810
|
+
const bar = trend.map(({ month, count }) => pc.dim(`${month} ${"\u2588".repeat(Math.min(count, 20))} ${count}`)).join("\n ");
|
|
1811
|
+
console.log(` ${bar}`);
|
|
1812
|
+
console.log();
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// src/commands/update.ts
|
|
1816
|
+
import { existsSync as existsSync14 } from "fs";
|
|
1817
|
+
import { mkdir as mkdir8, readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
|
|
1818
|
+
import { join as join14 } from "path";
|
|
1819
|
+
async function updateProject() {
|
|
1820
|
+
if (!existsSync14(RSP_DIR)) {
|
|
1821
|
+
console.log(` ${pc.red("Error:")} RSP is not initialized. Run: rsp init`);
|
|
1822
|
+
process.exit(1);
|
|
1823
|
+
}
|
|
1824
|
+
return withRspLock("update", async () => {
|
|
1825
|
+
let updated = false;
|
|
1826
|
+
const bundledRules = await readFile11(join14(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
|
|
1827
|
+
const rulesPath = join14(RSP_DIR, "rules", "rsp-rules.md");
|
|
1828
|
+
await mkdir8(join14(RSP_DIR, "rules"), { recursive: true });
|
|
1829
|
+
const existingRules = existsSync14(rulesPath) ? await readFile11(rulesPath, "utf-8") : null;
|
|
1830
|
+
if (existingRules !== bundledRules) {
|
|
1831
|
+
await writeFile9(rulesPath, bundledRules);
|
|
1832
|
+
console.log(` ${pc.green("\u2713")} rules/rsp-rules.md updated`);
|
|
1833
|
+
updated = true;
|
|
1834
|
+
}
|
|
1835
|
+
const agentsPath = "AGENTS.md";
|
|
1836
|
+
const projectName = await detectProjectName();
|
|
1837
|
+
const baseAgents = existsSync14(agentsPath) ? await readFile11(agentsPath, "utf-8") : `# ${projectName}
|
|
1838
|
+
|
|
1839
|
+
`;
|
|
1840
|
+
const nextAgents = upsertRspAgentsBlock(baseAgents);
|
|
1841
|
+
if (!existsSync14(agentsPath) || nextAgents.changed) {
|
|
1842
|
+
await writeFile9(agentsPath, nextAgents.content);
|
|
1843
|
+
console.log(` ${pc.green("\u2713")} AGENTS.md managed block refreshed`);
|
|
1844
|
+
updated = true;
|
|
1845
|
+
}
|
|
1846
|
+
await buildSpecsIndex({ acquireLock: false });
|
|
1847
|
+
await buildArchiveIndex({ acquireLock: false });
|
|
1848
|
+
if (!updated)
|
|
1849
|
+
console.log(` ${pc.dim("Already up to date.")}
|
|
1850
|
+
`);
|
|
1851
|
+
else
|
|
1852
|
+
console.log(` ${pc.green("Update complete.")}
|
|
1853
|
+
`);
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1456
1856
|
|
|
1457
1857
|
// src/cli.ts
|
|
1458
1858
|
var initCommand = defineCommand({
|
|
1459
1859
|
meta: {
|
|
1460
1860
|
name: "init",
|
|
1461
|
-
description: "Scaffold .rsp/
|
|
1861
|
+
description: "Scaffold .rsp/ and ensure AGENTS.md contains the RSP entry block"
|
|
1462
1862
|
},
|
|
1463
1863
|
args: {
|
|
1464
1864
|
"agents-mode": {
|
|
1465
1865
|
type: "string",
|
|
1466
|
-
description: "How to
|
|
1866
|
+
description: "How to handle AGENTS.md output: managed or print",
|
|
1467
1867
|
default: "managed"
|
|
1468
1868
|
},
|
|
1469
1869
|
"with-project-rules": {
|
|
1470
1870
|
type: "boolean",
|
|
1471
1871
|
description: "Create .rsp/rules/project-rules.md",
|
|
1472
1872
|
default: false
|
|
1873
|
+
},
|
|
1874
|
+
"with-project-setup": {
|
|
1875
|
+
type: "boolean",
|
|
1876
|
+
description: "Create .rsp/changes/project-setup.md and focus it",
|
|
1877
|
+
default: false
|
|
1473
1878
|
}
|
|
1474
1879
|
},
|
|
1475
1880
|
async run({ args }) {
|
|
1476
1881
|
await initProject({
|
|
1477
|
-
agentsMode: args["agents-mode"] === "
|
|
1478
|
-
withProjectRules: Boolean(args["with-project-rules"])
|
|
1882
|
+
agentsMode: args["agents-mode"] === "print" ? "print" : "managed",
|
|
1883
|
+
withProjectRules: Boolean(args["with-project-rules"]),
|
|
1884
|
+
withProjectSetup: Boolean(args["with-project-setup"])
|
|
1479
1885
|
});
|
|
1480
1886
|
}
|
|
1481
1887
|
});
|
|
1482
|
-
var
|
|
1888
|
+
var createCommand = defineCommand({
|
|
1483
1889
|
meta: {
|
|
1484
|
-
name: "
|
|
1485
|
-
description: "Create .rsp/
|
|
1890
|
+
name: "create",
|
|
1891
|
+
description: "Create .rsp/changes/<name>.md [summary]"
|
|
1486
1892
|
},
|
|
1487
1893
|
args: {
|
|
1488
1894
|
name: {
|
|
1489
1895
|
type: "positional",
|
|
1490
|
-
description: "
|
|
1896
|
+
description: "Change name",
|
|
1491
1897
|
required: true
|
|
1898
|
+
},
|
|
1899
|
+
kind: {
|
|
1900
|
+
type: "string",
|
|
1901
|
+
description: "Optional kind for a kind-aware template (feature, fix, refactor, docs, ops, research)"
|
|
1492
1902
|
}
|
|
1493
1903
|
},
|
|
1494
1904
|
async run({ args }) {
|
|
1495
1905
|
const summary = Array.isArray(args._) && args._.length > 1 ? args._.slice(1).join(" ") : "";
|
|
1496
|
-
await
|
|
1906
|
+
await createChange(args.name, summary, args.kind);
|
|
1497
1907
|
}
|
|
1498
1908
|
});
|
|
1499
1909
|
var addRulesCommand = defineCommand({
|
|
@@ -1538,111 +1948,141 @@ var addCommand = defineCommand({
|
|
|
1538
1948
|
spec: addSpecCommand
|
|
1539
1949
|
}
|
|
1540
1950
|
});
|
|
1541
|
-
var
|
|
1951
|
+
var archiveCommand = defineCommand({
|
|
1952
|
+
meta: {
|
|
1953
|
+
name: "archive",
|
|
1954
|
+
description: "Archive an open change into .rsp/archives/"
|
|
1955
|
+
},
|
|
1956
|
+
args: {
|
|
1957
|
+
name: {
|
|
1958
|
+
type: "positional",
|
|
1959
|
+
description: "Change name",
|
|
1960
|
+
required: true
|
|
1961
|
+
}
|
|
1962
|
+
},
|
|
1963
|
+
async run({ args }) {
|
|
1964
|
+
await archiveChange(args.name);
|
|
1965
|
+
}
|
|
1966
|
+
});
|
|
1967
|
+
var focusCommand = defineCommand({
|
|
1968
|
+
meta: {
|
|
1969
|
+
name: "focus",
|
|
1970
|
+
description: "Mark an existing change as currently focused in .rsp/focus.d/"
|
|
1971
|
+
},
|
|
1972
|
+
args: {
|
|
1973
|
+
name: {
|
|
1974
|
+
type: "positional",
|
|
1975
|
+
description: "Change name",
|
|
1976
|
+
required: true
|
|
1977
|
+
}
|
|
1978
|
+
},
|
|
1979
|
+
async run({ args }) {
|
|
1980
|
+
await focusChange(args.name);
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
var unfocusCommand = defineCommand({
|
|
1542
1984
|
meta: {
|
|
1543
|
-
name: "
|
|
1544
|
-
description: "
|
|
1985
|
+
name: "unfocus",
|
|
1986
|
+
description: "Remove a change from the current focus set in .rsp/focus.d/"
|
|
1545
1987
|
},
|
|
1546
1988
|
args: {
|
|
1547
1989
|
name: {
|
|
1548
1990
|
type: "positional",
|
|
1549
|
-
description: "
|
|
1991
|
+
description: "Change name",
|
|
1550
1992
|
required: true
|
|
1551
1993
|
}
|
|
1552
1994
|
},
|
|
1553
1995
|
async run({ args }) {
|
|
1554
|
-
await
|
|
1996
|
+
await unfocusChange(args.name);
|
|
1555
1997
|
}
|
|
1556
1998
|
});
|
|
1557
1999
|
var statusCommand = defineCommand({
|
|
1558
2000
|
meta: {
|
|
1559
2001
|
name: "status",
|
|
1560
|
-
description: "Show project status summary"
|
|
2002
|
+
description: "Show project status summary with current focus information"
|
|
1561
2003
|
},
|
|
1562
2004
|
args: {
|
|
1563
|
-
|
|
2005
|
+
focused: {
|
|
1564
2006
|
type: "boolean",
|
|
1565
|
-
description: "Show only
|
|
2007
|
+
description: "Show only currently focused changes",
|
|
1566
2008
|
default: false
|
|
1567
2009
|
},
|
|
1568
2010
|
blocked: {
|
|
1569
2011
|
type: "boolean",
|
|
1570
|
-
description: "Show only blocked
|
|
2012
|
+
description: "Show only blocked changes",
|
|
1571
2013
|
default: false
|
|
1572
2014
|
},
|
|
1573
2015
|
stale: {
|
|
1574
2016
|
type: "string",
|
|
1575
|
-
description: "Show only
|
|
2017
|
+
description: "Show only changes with age >= days"
|
|
2018
|
+
},
|
|
2019
|
+
json: {
|
|
2020
|
+
type: "boolean",
|
|
2021
|
+
description: "Print machine-readable JSON output",
|
|
2022
|
+
default: false
|
|
2023
|
+
},
|
|
2024
|
+
verbose: {
|
|
2025
|
+
type: "boolean",
|
|
2026
|
+
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
2027
|
+
default: false
|
|
1576
2028
|
}
|
|
1577
2029
|
},
|
|
1578
2030
|
async run({ args }) {
|
|
1579
2031
|
const stale = args.stale === void 0 ? void 0 : Number(args.stale);
|
|
1580
2032
|
if (args.stale !== void 0 && (Number.isFinite(stale) === false || Number.isInteger(stale) === false || stale < 0)) {
|
|
1581
|
-
|
|
2033
|
+
if (args.json) {
|
|
2034
|
+
emitStatusJsonError({
|
|
2035
|
+
code: "invalid_stale_filter",
|
|
2036
|
+
message: "--stale must be a non-negative integer number of days"
|
|
2037
|
+
}, {
|
|
2038
|
+
focused: Boolean(args.focused),
|
|
2039
|
+
blocked: Boolean(args.blocked)
|
|
2040
|
+
});
|
|
2041
|
+
} else {
|
|
2042
|
+
console.error(` Error: --stale must be a non-negative integer number of days`);
|
|
2043
|
+
}
|
|
1582
2044
|
process.exit(1);
|
|
1583
2045
|
}
|
|
1584
2046
|
await showStatus({
|
|
1585
|
-
|
|
2047
|
+
focused: Boolean(args.focused),
|
|
1586
2048
|
blocked: Boolean(args.blocked),
|
|
1587
2049
|
stale
|
|
2050
|
+
}, {
|
|
2051
|
+
json: Boolean(args.json),
|
|
2052
|
+
verbose: Boolean(args.verbose)
|
|
1588
2053
|
});
|
|
1589
2054
|
}
|
|
1590
2055
|
});
|
|
1591
2056
|
var checkCommand = defineCommand({
|
|
1592
2057
|
meta: {
|
|
1593
2058
|
name: "check",
|
|
1594
|
-
description: "Validate
|
|
1595
|
-
},
|
|
1596
|
-
async run() {
|
|
1597
|
-
const errors = await runCheck();
|
|
1598
|
-
if (errors > 0)
|
|
1599
|
-
process.exit(1);
|
|
1600
|
-
}
|
|
1601
|
-
});
|
|
1602
|
-
var depsCommand = defineCommand({
|
|
1603
|
-
meta: {
|
|
1604
|
-
name: "deps",
|
|
1605
|
-
description: "Show dependency summary (--mermaid for graph)"
|
|
2059
|
+
description: "Validate changes (frontmatter, sections, deltas, scenarios)"
|
|
1606
2060
|
},
|
|
1607
2061
|
args: {
|
|
1608
|
-
|
|
2062
|
+
json: {
|
|
1609
2063
|
type: "boolean",
|
|
1610
|
-
description: "
|
|
2064
|
+
description: "Print machine-readable JSON output",
|
|
1611
2065
|
default: false
|
|
1612
2066
|
},
|
|
1613
|
-
|
|
1614
|
-
type: "
|
|
1615
|
-
description: "
|
|
1616
|
-
|
|
1617
|
-
reverse: {
|
|
1618
|
-
type: "string",
|
|
1619
|
-
description: "Show only features that depend on the given feature"
|
|
2067
|
+
verbose: {
|
|
2068
|
+
type: "boolean",
|
|
2069
|
+
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
2070
|
+
default: false
|
|
1620
2071
|
}
|
|
1621
2072
|
},
|
|
1622
2073
|
async run({ args }) {
|
|
1623
|
-
await
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
reverse: typeof args.reverse === "string" ? args.reverse : void 0
|
|
1627
|
-
});
|
|
1628
|
-
}
|
|
1629
|
-
});
|
|
1630
|
-
var archiveIndexCommand = defineCommand({
|
|
1631
|
-
meta: {
|
|
1632
|
-
name: "archive-index",
|
|
1633
|
-
description: "Regenerate archives INDEX.md"
|
|
1634
|
-
},
|
|
1635
|
-
async run() {
|
|
1636
|
-
await buildArchiveIndex();
|
|
2074
|
+
const result = await runCheck({ json: Boolean(args.json), verbose: Boolean(args.verbose) });
|
|
2075
|
+
if (!result.ok)
|
|
2076
|
+
process.exit(1);
|
|
1637
2077
|
}
|
|
1638
2078
|
});
|
|
1639
|
-
var
|
|
2079
|
+
var updateCommand = defineCommand({
|
|
1640
2080
|
meta: {
|
|
1641
|
-
name: "
|
|
1642
|
-
description: "
|
|
2081
|
+
name: "update",
|
|
2082
|
+
description: "Refresh RSP project structure after upgrade (rules, AGENTS, indices)"
|
|
1643
2083
|
},
|
|
1644
2084
|
async run() {
|
|
1645
|
-
await
|
|
2085
|
+
await updateProject();
|
|
1646
2086
|
}
|
|
1647
2087
|
});
|
|
1648
2088
|
var doctorCommand = defineCommand({
|
|
@@ -1650,9 +2090,21 @@ var doctorCommand = defineCommand({
|
|
|
1650
2090
|
name: "doctor",
|
|
1651
2091
|
description: "Check RSP setup health and common integration issues"
|
|
1652
2092
|
},
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
2093
|
+
args: {
|
|
2094
|
+
json: {
|
|
2095
|
+
type: "boolean",
|
|
2096
|
+
description: "Print machine-readable JSON output",
|
|
2097
|
+
default: false
|
|
2098
|
+
},
|
|
2099
|
+
verbose: {
|
|
2100
|
+
type: "boolean",
|
|
2101
|
+
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
2102
|
+
default: false
|
|
2103
|
+
}
|
|
2104
|
+
},
|
|
2105
|
+
async run({ args }) {
|
|
2106
|
+
const result = await runDoctor({ json: Boolean(args.json), verbose: Boolean(args.verbose) });
|
|
2107
|
+
if (!result.ok)
|
|
1656
2108
|
process.exit(1);
|
|
1657
2109
|
}
|
|
1658
2110
|
});
|
|
@@ -1662,19 +2114,19 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
1662
2114
|
meta: {
|
|
1663
2115
|
name: "rsp",
|
|
1664
2116
|
version,
|
|
1665
|
-
description: "RSP (Rules, Specs, Plans) workflow for AI-assisted
|
|
2117
|
+
description: "RSP (Rules, Specs, Plans) workflow for lightweight AI-assisted change management"
|
|
1666
2118
|
},
|
|
1667
2119
|
subCommands: {
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
2120
|
+
init: initCommand,
|
|
2121
|
+
add: addCommand,
|
|
2122
|
+
create: createCommand,
|
|
2123
|
+
focus: focusCommand,
|
|
2124
|
+
unfocus: unfocusCommand,
|
|
2125
|
+
archive: archiveCommand,
|
|
2126
|
+
status: statusCommand,
|
|
2127
|
+
check: checkCommand,
|
|
2128
|
+
update: updateCommand,
|
|
2129
|
+
doctor: doctorCommand
|
|
1678
2130
|
}
|
|
1679
2131
|
});
|
|
1680
2132
|
await runMain(main, { rawArgs });
|