@oevery/rsp 2.0.4 → 3.1.0-beta.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 +195 -62
- package/README.zh-CN.md +177 -60
- package/bin/rsp.mjs +2 -2
- package/dist/chunk-QLXIDVKK.mjs +2618 -0
- package/dist/cli.mjs +1997 -1355
- package/dist/entry-LAPR3TNI.mjs +966 -0
- package/dist/locale-HT44UIUP.mjs +21 -0
- package/package.json +13 -4
- package/rules/rsp-rules.md +63 -91
- package/skills/rsp/SKILL.md +142 -60
- package/skills/rsp-address-review/SKILL.md +61 -0
- package/skills/rsp-design/SKILL.md +48 -0
- package/skills/rsp-design/references/domain-modeling.md +11 -0
- package/skills/rsp-design/references/module-seams.md +11 -0
- package/skills/rsp-design/references/reversible-exploration.md +19 -0
- package/skills/rsp-diagnose/SKILL.md +46 -0
- package/skills/rsp-implement/NOTICE.md +27 -0
- package/skills/rsp-implement/SKILL.md +52 -0
- package/skills/rsp-manage/SKILL.md +53 -0
- package/skills/rsp-release-docs/SKILL.md +120 -0
- package/skills/rsp-release-docs/references/convention-discovery.md +65 -0
- package/skills/rsp-release-docs/references/output-contracts.md +115 -0
- package/skills/rsp-review/SKILL.md +98 -0
- package/skills/rsp-shape/SKILL.md +65 -0
- package/skills/rsp-shape/references/complex-shaping.md +30 -0
- package/skills/rsp-shape/references/deep-clarification.md +29 -0
- package/skills/rsp-tdd/SKILL.md +48 -0
package/dist/cli.mjs
CHANGED
|
@@ -1,714 +1,97 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ARCHIVES_DIR,
|
|
4
|
+
ArchiveHistoryError,
|
|
5
|
+
CHANGES_DIR,
|
|
6
|
+
CONFIG_PATH,
|
|
7
|
+
DEFAULT_DECISION_RECORDS_PATH,
|
|
8
|
+
FOCUS_DIR,
|
|
9
|
+
GROUP_BRIEF_FILENAME,
|
|
10
|
+
HISTORY_DEFAULT_LIMIT,
|
|
11
|
+
HISTORY_MAX_LIMIT,
|
|
12
|
+
LOCK_PATH,
|
|
13
|
+
ManagedPathError,
|
|
14
|
+
OBSOLETE_RSP_RULES_PATH,
|
|
15
|
+
PKG_ROOT,
|
|
16
|
+
RSP_DIR,
|
|
17
|
+
RSP_RULES_PATH,
|
|
18
|
+
VALID_KINDS,
|
|
19
|
+
WorkRefError,
|
|
20
|
+
buildDurableReviewGuidance,
|
|
21
|
+
cleanupEmptyParentDirs,
|
|
22
|
+
clearConfigCache,
|
|
23
|
+
collectArchiveChecklist,
|
|
24
|
+
collectArchiveReadiness,
|
|
25
|
+
countCheckboxes,
|
|
26
|
+
detectDeltaSections,
|
|
27
|
+
detectProjectName,
|
|
28
|
+
emitJson,
|
|
29
|
+
ensureDecisionRecordsDirectory,
|
|
30
|
+
ensureManagedFile,
|
|
31
|
+
extractSection,
|
|
32
|
+
generateChangeContent,
|
|
33
|
+
generateDesignContent,
|
|
34
|
+
generateGroupBriefContent,
|
|
35
|
+
generateSpecContent,
|
|
36
|
+
getDurableReviewCandidateTargets,
|
|
37
|
+
getVersion,
|
|
38
|
+
guardRspInitialized,
|
|
39
|
+
hasArchivedGroupBrief,
|
|
40
|
+
hasMeaningfulBlockers,
|
|
41
|
+
hasRspAgentsBlock,
|
|
42
|
+
historyInspectionComplete,
|
|
43
|
+
inspectArchiveHistory,
|
|
44
|
+
inspectArchiveTree,
|
|
45
|
+
inspectChangeDependencies,
|
|
46
|
+
inspectChangeGroups,
|
|
47
|
+
inspectFocusTree,
|
|
48
|
+
inspectManagedDirectory,
|
|
49
|
+
inspectManagedFile,
|
|
50
|
+
inspectManagedFileTree,
|
|
51
|
+
inspectProjectStatus,
|
|
52
|
+
inspectRspConfig,
|
|
53
|
+
inspectUnsupportedRules,
|
|
54
|
+
inspectWorkTree,
|
|
55
|
+
isValidSpecName,
|
|
56
|
+
loadRspConfig,
|
|
57
|
+
normalizeLogicalPath,
|
|
58
|
+
parseFrontmatter,
|
|
59
|
+
parseScenarios,
|
|
60
|
+
pc,
|
|
61
|
+
projectDependencyForest,
|
|
62
|
+
queryArchiveHistory,
|
|
63
|
+
readArchiveHistoryDetail,
|
|
64
|
+
recordRuntimeDiagnostic,
|
|
65
|
+
requireManagedDirectory,
|
|
66
|
+
resolveArchiveDirectory,
|
|
67
|
+
resolveDecisionRecordsPath,
|
|
68
|
+
resolveExecutableChange,
|
|
69
|
+
resolveFocusMarkerPath,
|
|
70
|
+
resolveKinds,
|
|
71
|
+
resolveManagedDirectoryChain,
|
|
72
|
+
resolveRequiredSections,
|
|
73
|
+
resolveWorkRef,
|
|
74
|
+
selectArchiveHistoryRecord,
|
|
75
|
+
toErrorMessage,
|
|
76
|
+
upsertRspAgentsBlock,
|
|
77
|
+
validateDecisionRecordsFilesystemPath,
|
|
78
|
+
walkMarkdownFiles,
|
|
79
|
+
writeManagedFile
|
|
80
|
+
} from "./chunk-QLXIDVKK.mjs";
|
|
2
81
|
|
|
3
82
|
// src/cli.ts
|
|
4
83
|
import { resolve } from "path";
|
|
5
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
|
-
import { defineCommand, runMain } from "citty";
|
|
7
|
-
|
|
8
|
-
// src/commands/add-rules.ts
|
|
9
|
-
import { existsSync as existsSync3 } from "fs";
|
|
10
|
-
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
11
|
-
import { dirname as dirname3, join as join3 } from "path";
|
|
12
|
-
|
|
13
|
-
// src/core/config.ts
|
|
14
|
-
import { existsSync as existsSync2 } from "fs";
|
|
15
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
16
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
17
84
|
import { fileURLToPath } from "url";
|
|
18
|
-
import
|
|
19
|
-
|
|
20
|
-
// src/core/helpers.ts
|
|
21
|
-
import { existsSync } from "fs";
|
|
22
|
-
import { readdir, readFile, rmdir } 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
|
|
63
|
-
var RSP_AGENTS_BEGIN = "<!-- rsp:begin -->";
|
|
64
|
-
var RSP_AGENTS_END = "<!-- rsp:end -->";
|
|
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;
|
|
72
|
-
}
|
|
73
|
-
function parseFrontmatter(content) {
|
|
74
|
-
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)\r?\n/);
|
|
75
|
-
if (!m)
|
|
76
|
-
return null;
|
|
77
|
-
return parseYamlText(m[1]);
|
|
78
|
-
}
|
|
79
|
-
function countCheckboxes(content) {
|
|
80
|
-
const todo = (content.match(/\[ \]/g) || []).length;
|
|
81
|
-
const progress = (content.match(/\[\/\]/g) || []).length;
|
|
82
|
-
const done = (content.match(/\[x\]/g) || []).length;
|
|
83
|
-
const dropped = (content.match(/\[-\]/g) || []).length;
|
|
84
|
-
return { todo, progress, done, dropped, total: todo + progress + done + dropped };
|
|
85
|
-
}
|
|
86
|
-
async function walkMarkdownFiles(dir, options = {}) {
|
|
87
|
-
const all = await walkFiles(dir, options);
|
|
88
|
-
return all.filter((f) => f.endsWith(".md"));
|
|
89
|
-
}
|
|
90
|
-
async function walkFiles(dir, options = {}) {
|
|
91
|
-
const files = [];
|
|
92
|
-
try {
|
|
93
|
-
const items = await readdir(dir, { withFileTypes: true });
|
|
94
|
-
for (const item of items) {
|
|
95
|
-
if (item.name.startsWith("."))
|
|
96
|
-
continue;
|
|
97
|
-
const filePath = join(dir, item.name);
|
|
98
|
-
if (item.isDirectory())
|
|
99
|
-
files.push(...await walkFiles(filePath, options));
|
|
100
|
-
else
|
|
101
|
-
files.push(filePath);
|
|
102
|
-
}
|
|
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
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return files;
|
|
114
|
-
}
|
|
115
|
-
function changeNameFromPath(changesDir, filePath) {
|
|
116
|
-
const rel = normalizeLogicalPath(relative(changesDir, filePath));
|
|
117
|
-
return rel.replace(/\.md$/, "");
|
|
118
|
-
}
|
|
119
|
-
function normalizeLogicalPath(pathValue) {
|
|
120
|
-
return pathValue.split(sep).join("/").replace(/\\/g, "/");
|
|
121
|
-
}
|
|
122
|
-
function generateChangeContent(name, summary = "", kind, options = {}) {
|
|
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
|
|
127
|
-
---
|
|
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
|
|
141
|
-
|
|
142
|
-
## Spec
|
|
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
|
|
164
|
-
- Constraints:
|
|
165
|
-
- Keep the setup lightweight and avoid duplicating durable project facts
|
|
166
|
-
|
|
167
|
-
## Tasks
|
|
168
|
-
- [ ] Review the repository structure, entrypoints, and primary outputs
|
|
169
|
-
- [ ] Review AGENTS.md and confirm the RSP entry points to the right project files
|
|
170
|
-
- [ ] Fill .rsp/specs/design.md with durable architecture facts
|
|
171
|
-
- [ ] Add .rsp/rules/project-rules.md if stable local rules or validation steps exist
|
|
172
|
-
|
|
173
|
-
## Verify
|
|
174
|
-
- Automated:
|
|
175
|
-
- [ ] Run rsp doctor
|
|
176
|
-
- Manual:
|
|
177
|
-
- [ ] Review .rsp/specs/design.md and confirm it matches the repository
|
|
178
|
-
- Durable updates:
|
|
179
|
-
- [ ] Decide whether this change produced durable knowledge that belongs in \`.rsp/specs/\` or \`.rsp/rules/\`
|
|
180
|
-
- [ ] 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
|
|
181
|
-
|
|
182
|
-
## Blockers
|
|
183
|
-
- none
|
|
184
|
-
`;
|
|
185
|
-
}
|
|
186
|
-
const frontmatterKind = kind ?? "<choose: feature | fix | refactor | docs | ops | research>";
|
|
187
|
-
if (options.lite)
|
|
188
|
-
return generateLiteChangeContent(name, proposalSummary, frontmatterKind);
|
|
189
|
-
const template = getChangeTemplateByKind(kind);
|
|
190
|
-
return `---
|
|
191
|
-
kind: "${frontmatterKind}"
|
|
192
|
-
---
|
|
193
|
-
|
|
194
|
-
# Change: ${name}
|
|
195
|
-
|
|
196
|
-
## Proposal
|
|
197
|
-
- Summary: ${proposalSummary}
|
|
198
|
-
- Why:
|
|
199
|
-
- ${template.why}
|
|
200
|
-
- Scope:
|
|
201
|
-
- ${template.scope}
|
|
202
|
-
- Non-goals:
|
|
203
|
-
- ${template.nonGoals}
|
|
204
|
-
|
|
205
|
-
## Spec
|
|
206
|
-
${template.specSection}
|
|
207
|
-
|
|
208
|
-
### Acceptance
|
|
209
|
-
${template.acceptanceSection}
|
|
210
|
-
|
|
211
|
-
## Design
|
|
212
|
-
- Approach:
|
|
213
|
-
- ${template.approach}
|
|
214
|
-
- Affected areas:
|
|
215
|
-
- ${template.affectedArea1}
|
|
216
|
-
- ${template.affectedArea2}
|
|
217
|
-
- Constraints:
|
|
218
|
-
- ${template.constraints}
|
|
219
|
-
|
|
220
|
-
## Tasks
|
|
221
|
-
- [ ] Finalize the proposal, spec, and design details for this change
|
|
222
|
-
- [ ] ${template.task}
|
|
223
|
-
- [ ] Verify the result and update any required durable docs or rules
|
|
224
|
-
|
|
225
|
-
## Verify
|
|
226
|
-
- Automated:
|
|
227
|
-
- [ ] ${template.automatedVerify}
|
|
228
|
-
- Manual:
|
|
229
|
-
- [ ] ${template.manualVerify}
|
|
230
|
-
- Durable updates:
|
|
231
|
-
- [ ] Decide whether this change produced durable knowledge that belongs in \`.rsp/specs/\` or \`.rsp/rules/\`
|
|
232
|
-
- [ ] 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
|
|
233
|
-
|
|
234
|
-
## Blockers
|
|
235
|
-
- none
|
|
236
|
-
`;
|
|
237
|
-
}
|
|
238
|
-
function generateLiteChangeContent(name, proposalSummary, frontmatterKind) {
|
|
239
|
-
return `---
|
|
240
|
-
kind: "${frontmatterKind}"
|
|
241
|
-
---
|
|
242
|
-
|
|
243
|
-
# Change: ${name}
|
|
244
|
-
|
|
245
|
-
## Proposal
|
|
246
|
-
- Summary: ${proposalSummary}
|
|
247
|
-
- Why:
|
|
248
|
-
- <why this small change matters>
|
|
249
|
-
- Scope:
|
|
250
|
-
- <what will change>
|
|
251
|
-
- Non-goals:
|
|
252
|
-
- none
|
|
253
|
-
|
|
254
|
-
## Spec
|
|
255
|
-
### MODIFIED
|
|
256
|
-
- Requirement: <observable outcome>
|
|
257
|
-
- <what should be true after this change>
|
|
258
|
-
|
|
259
|
-
### Acceptance
|
|
260
|
-
#### Scenario: change is complete
|
|
261
|
-
- GIVEN <current context>
|
|
262
|
-
- WHEN <the change is applied>
|
|
263
|
-
- THEN <expected outcome>
|
|
264
|
-
|
|
265
|
-
## Design
|
|
266
|
-
- Approach:
|
|
267
|
-
- <minimal implementation approach>
|
|
268
|
-
- Affected areas:
|
|
269
|
-
- <primary file, directory, or doc path>
|
|
270
|
-
- Constraints:
|
|
271
|
-
- <important constraint, or none>
|
|
272
|
-
|
|
273
|
-
## Tasks
|
|
274
|
-
- [ ] Implement the small change
|
|
275
|
-
- [ ] Verify the result
|
|
276
|
-
|
|
277
|
-
## Verify
|
|
278
|
-
- Automated:
|
|
279
|
-
- [ ] <exact command, or not needed: reason>
|
|
280
|
-
- Manual:
|
|
281
|
-
- [ ] <exact scenario, or not needed: reason>
|
|
282
|
-
- Durable updates:
|
|
283
|
-
- [ ] Decide whether this change produced durable knowledge that belongs in ".rsp/specs/" or ".rsp/rules/"
|
|
284
|
-
- [ ] If yes, write only stable facts to the smallest correct target file before archive
|
|
285
|
-
|
|
286
|
-
## Blockers
|
|
287
|
-
- none
|
|
288
|
-
`;
|
|
289
|
-
}
|
|
290
|
-
function getChangeTemplateByKind(kind) {
|
|
291
|
-
switch (kind) {
|
|
292
|
-
case "feature":
|
|
293
|
-
return {
|
|
294
|
-
why: "<what user need or capability gap this addresses>",
|
|
295
|
-
scope: "<what new behavior or capability will be delivered>",
|
|
296
|
-
nonGoals: "<what related capabilities are explicitly out of scope>",
|
|
297
|
-
specSection: "<!-- Describe observable behavior and requirements. Implementation notes belong in ## Design. -->\n### ADDED\n- Requirement: <new user-facing behavior>\n - <verifiable requirement for the new capability>",
|
|
298
|
-
acceptanceSection: "#### Scenario: user exercises the new capability\n- GIVEN <context>\n- WHEN <user action>\n- THEN <expected new behavior>",
|
|
299
|
-
approach: "<how the new capability will be implemented>",
|
|
300
|
-
affectedArea1: "<concrete file path, directory, module, or subsystem 1>",
|
|
301
|
-
affectedArea2: "<concrete file path, directory, module, or subsystem 2 if needed>",
|
|
302
|
-
constraints: "<behavior, compatibility, performance, safety, or scope constraint that must hold>",
|
|
303
|
-
task: "<implement the new behavior and its acceptance scenario in the affected areas>",
|
|
304
|
-
automatedVerify: "<exact test, lint, build, or check command for the new behavior>",
|
|
305
|
-
manualVerify: "<exact end-to-end user scenario to validate the new capability>"
|
|
306
|
-
};
|
|
307
|
-
case "fix":
|
|
308
|
-
return {
|
|
309
|
-
why: "<what current behavior is wrong or broken>",
|
|
310
|
-
scope: "<what specific defect is being corrected>",
|
|
311
|
-
nonGoals: "<what related but separate issues are out of scope>",
|
|
312
|
-
specSection: "<!-- Describe expected correct behavior. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: correct behavior\n - <expected correct behavior after the fix>",
|
|
313
|
-
acceptanceSection: "#### Scenario: defect is resolved\n- GIVEN <conditions that trigger the defect>\n- WHEN <action that previously failed>\n- THEN <expected correct outcome>\n- AND the original broken outcome no longer occurs",
|
|
314
|
-
approach: "<root cause analysis and fix strategy>",
|
|
315
|
-
affectedArea1: "<concrete file path, directory, module, or subsystem containing the defect>",
|
|
316
|
-
affectedArea2: "<secondary path, dependency, or test file if needed>",
|
|
317
|
-
constraints: "<regression, backward-compatibility, safety, or scope constraint that must hold>",
|
|
318
|
-
task: "<identify the root cause and apply the minimal fix>",
|
|
319
|
-
automatedVerify: "<exact regression test, lint, build, or check command that proves the fix>",
|
|
320
|
-
manualVerify: "<exact steps to reproduce the original issue and confirm it no longer occurs>"
|
|
321
|
-
};
|
|
322
|
-
case "refactor":
|
|
323
|
-
return {
|
|
324
|
-
why: "<what maintainability, readability, or structural issue motivates this>",
|
|
325
|
-
scope: "<what code areas are being restructured>",
|
|
326
|
-
nonGoals: "<no behavior change is expected or intended>",
|
|
327
|
-
specSection: "<!-- Describe the desired structural outcome. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: internal structure improvement\n - <desired structural outcome without observable behavior change>",
|
|
328
|
-
acceptanceSection: "#### Scenario: behavior is preserved after restructuring\n- GIVEN the existing codebase before the refactor\n- WHEN the refactored code runs against existing tests\n- THEN all existing tests pass unchanged",
|
|
329
|
-
approach: "<how the code will be restructured (rename, extract, move, etc.)>",
|
|
330
|
-
affectedArea1: "<concrete file path, directory, module, or subsystem being refactored>",
|
|
331
|
-
affectedArea2: "<secondary path, dependency, or test file if needed>",
|
|
332
|
-
constraints: "<observable behavior must remain unchanged; compatibility, performance, and safety constraints must still hold>",
|
|
333
|
-
task: "<restructure the code while keeping behavior identical>",
|
|
334
|
-
automatedVerify: "<exact existing test, lint, build, or check command that must continue to pass>",
|
|
335
|
-
manualVerify: "<exact scenario to spot-check that behavior is unchanged after the refactor>"
|
|
336
|
-
};
|
|
337
|
-
case "docs":
|
|
338
|
-
return {
|
|
339
|
-
why: "<why the documentation change matters>",
|
|
340
|
-
scope: "<which docs, readers, or workflows will be updated>",
|
|
341
|
-
nonGoals: "<what documentation behavior or audience is out of scope>",
|
|
342
|
-
specSection: "<!-- Describe what the reader should see or experience. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: documentation accuracy\n - <what durable reader-facing behavior or explanation changes>",
|
|
343
|
-
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",
|
|
344
|
-
approach: "<how the documentation will be updated and organized>",
|
|
345
|
-
affectedArea1: "<concrete doc path, directory, module doc, or documentation surface 1>",
|
|
346
|
-
affectedArea2: "<secondary doc path or reference file if needed>",
|
|
347
|
-
constraints: "<durable wording, consistency, scope, or audience constraint that must hold>",
|
|
348
|
-
task: "<update the target documentation and supporting references>",
|
|
349
|
-
automatedVerify: "<exact markdown, docs, link, lint, build, or check command if applicable>",
|
|
350
|
-
manualVerify: "<exact reader or operator scenario to confirm the updated guidance is accurate>"
|
|
351
|
-
};
|
|
352
|
-
case "research":
|
|
353
|
-
return {
|
|
354
|
-
why: "<what question or uncertainty this research resolves>",
|
|
355
|
-
scope: "<what system area, option set, or hypothesis is being examined>",
|
|
356
|
-
nonGoals: "<what implementation work is explicitly out of scope>",
|
|
357
|
-
specSection: "<!-- Describe what finding or decision must be captured. Implementation notes belong in ## Design. -->\n### ADDED\n- Requirement: research outcome recording\n - <what finding, option, or decision must be captured clearly>",
|
|
358
|
-
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",
|
|
359
|
-
approach: "<how the investigation will be performed and what evidence will be gathered>",
|
|
360
|
-
affectedArea1: "<concrete file path, directory, module, subsystem, or source under investigation>",
|
|
361
|
-
affectedArea2: "<secondary path, dependency, environment, or evidence source if needed>",
|
|
362
|
-
constraints: "<time, evidence, safety, or scope constraint for the investigation>",
|
|
363
|
-
task: "<gather evidence, record the recommendation or finding, and note any follow-up implementation work if needed>",
|
|
364
|
-
automatedVerify: "<exact command, script, query, or check used to confirm the observed behavior>",
|
|
365
|
-
manualVerify: "<exact review of the findings to confirm they answer the original question and identify follow-up work if needed>"
|
|
366
|
-
};
|
|
367
|
-
case "ops":
|
|
368
|
-
return {
|
|
369
|
-
why: "<why this operational or environment change matters>",
|
|
370
|
-
scope: "<what environment, workflow, or operational path changes>",
|
|
371
|
-
nonGoals: "<what product or feature behavior will not change>",
|
|
372
|
-
specSection: "<!-- Describe the reliable operational outcome. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: operational behavior\n - <what reliable operational outcome should change or stay true>",
|
|
373
|
-
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",
|
|
374
|
-
approach: "<how the operational change will be applied safely>",
|
|
375
|
-
affectedArea1: "<concrete script, config path, workflow, environment path, or operational surface 1>",
|
|
376
|
-
affectedArea2: "<secondary script, config path, dependency, or environment surface if needed>",
|
|
377
|
-
constraints: "<rollback, safety, reliability, environment, or scope constraint that must hold>",
|
|
378
|
-
task: "<apply and verify the operational change>",
|
|
379
|
-
automatedVerify: "<exact operational validation, lint, build, deploy, or check command>",
|
|
380
|
-
manualVerify: "<exact operator workflow or environment scenario to confirm the operational result>"
|
|
381
|
-
};
|
|
382
|
-
default:
|
|
383
|
-
return {
|
|
384
|
-
why: "<why this change matters>",
|
|
385
|
-
scope: "<what this change will do>",
|
|
386
|
-
nonGoals: "<what this change will not do>",
|
|
387
|
-
specSection: "<!-- Describe observable behavior and requirements. Implementation notes belong in ## Design. -->\n### ADDED\n- Requirement: <new or updated behavior>\n - <verifiable requirement>",
|
|
388
|
-
acceptanceSection: "#### Scenario: <name>\n- GIVEN <context>\n- WHEN <action>\n- THEN <expected outcome>",
|
|
389
|
-
approach: "<how the change will be implemented>",
|
|
390
|
-
affectedArea1: "<concrete file path, directory, module, or subsystem 1>",
|
|
391
|
-
affectedArea2: "<concrete file path, directory, module, or subsystem 2 if needed>",
|
|
392
|
-
constraints: "<behavior, compatibility, performance, safety, or scope constraint that must hold>",
|
|
393
|
-
task: "<implement the core change in the affected areas>",
|
|
394
|
-
automatedVerify: "<exact test, lint, build, or check command>",
|
|
395
|
-
manualVerify: "<exact end-to-end scenario to validate>"
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
function renderRspAgentsBlock() {
|
|
400
|
-
return `${RSP_AGENTS_BEGIN}
|
|
401
|
-
## RSP Entry
|
|
402
|
-
|
|
403
|
-
RSP keeps durable rules, specs, and current work under \`.rsp/\`.
|
|
404
|
-
Treat AGENTS.md as navigation only; keep durable rules and design in \`.rsp/\`.
|
|
405
|
-
|
|
406
|
-
Read in order:
|
|
407
|
-
1. .rsp/rules/rsp-rules.md
|
|
408
|
-
2. .rsp/focus.d/
|
|
409
|
-
3. matching .rsp/changes/*.md for the focused entries
|
|
410
|
-
4. .rsp/specs/design.md
|
|
411
|
-
5. .rsp/specs/INDEX.md
|
|
412
|
-
6. only the relevant additional .rsp/rules/*.md and .rsp/specs/*.md files
|
|
413
|
-
|
|
414
|
-
If \`.rsp/focus.d/\` is empty and the user has not provided a concrete task, ask what to work on or suggest \`npx -y @oevery/rsp create <name>\` for tracked work.
|
|
415
|
-
If your agent supports Agent Skills, load \`rsp\` for setup, repair, and durable-decision tasks.
|
|
416
|
-
${RSP_AGENTS_END}`;
|
|
417
|
-
}
|
|
418
|
-
function upsertRspAgentsBlock(content) {
|
|
419
|
-
const block = renderRspAgentsBlock();
|
|
420
|
-
const managedRe = /<!-- rsp:begin -->[\s\S]*?<!-- rsp:end -->/;
|
|
421
|
-
if (managedRe.test(content)) {
|
|
422
|
-
const next2 = content.replace(managedRe, block);
|
|
423
|
-
return { content: next2, changed: next2 !== content };
|
|
424
|
-
}
|
|
425
|
-
const trimmed = content.trimStart();
|
|
426
|
-
const next = trimmed ? `${block}
|
|
427
|
-
|
|
428
|
-
${trimmed}` : `${block}
|
|
429
|
-
`;
|
|
430
|
-
return { content: next, changed: true };
|
|
431
|
-
}
|
|
432
|
-
function hasRspAgentsBlock(content) {
|
|
433
|
-
return content.includes(RSP_AGENTS_BEGIN) && content.includes(RSP_AGENTS_END);
|
|
434
|
-
}
|
|
435
|
-
var CHANGE_NAME_RE = /^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
|
|
436
|
-
function isValidChangeName(name) {
|
|
437
|
-
return CHANGE_NAME_RE.test(name);
|
|
438
|
-
}
|
|
439
|
-
function guardRspInitialized() {
|
|
440
|
-
const rspRulesPath = join(RSP_DIR, "rules", "rsp-rules.md");
|
|
441
|
-
const designPath = join(RSP_DIR, "specs", "design.md");
|
|
442
|
-
if (!existsSync(rspRulesPath) || !existsSync(designPath)) {
|
|
443
|
-
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
444
|
-
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
445
|
-
process.exit(1);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
async function getFocusedChangeNames(options = {}) {
|
|
449
|
-
const names = /* @__PURE__ */ new Set();
|
|
450
|
-
const focusDir = join(RSP_DIR, "focus.d");
|
|
451
|
-
if (!existsSync(focusDir))
|
|
452
|
-
return names;
|
|
453
|
-
const entries = await walkFiles(focusDir, options);
|
|
454
|
-
for (const entryPath of entries)
|
|
455
|
-
names.add(normalizeLogicalPath(relative(focusDir, entryPath)));
|
|
456
|
-
return names;
|
|
457
|
-
}
|
|
458
|
-
function getOpenCheckboxes(sectionText) {
|
|
459
|
-
return sectionText.split("\n").map((line) => line.trim()).filter((line) => /^- \[[ /]\]/.test(line));
|
|
460
|
-
}
|
|
461
|
-
async function cleanupEmptyParentDirs(path, stopDir) {
|
|
462
|
-
let parent = dirname(path);
|
|
463
|
-
while (parent !== stopDir) {
|
|
464
|
-
try {
|
|
465
|
-
const remaining = await readdir(parent);
|
|
466
|
-
if (remaining.length === 0)
|
|
467
|
-
await rmdir(parent);
|
|
468
|
-
else
|
|
469
|
-
break;
|
|
470
|
-
} catch {
|
|
471
|
-
break;
|
|
472
|
-
}
|
|
473
|
-
parent = dirname(parent);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
async function detectProjectName() {
|
|
477
|
-
try {
|
|
478
|
-
if (existsSync("package.json")) {
|
|
479
|
-
const raw = await readFile("package.json", "utf-8");
|
|
480
|
-
const pkg = JSON.parse(raw);
|
|
481
|
-
if (pkg.name && pkg.name !== "")
|
|
482
|
-
return pkg.name;
|
|
483
|
-
}
|
|
484
|
-
} catch {
|
|
485
|
-
}
|
|
486
|
-
return basename(process.cwd());
|
|
487
|
-
}
|
|
488
|
-
function generateDesignContent(projectName) {
|
|
489
|
-
return `# Project Design: ${projectName}
|
|
490
|
-
|
|
491
|
-
## Purpose
|
|
492
|
-
- <what this project is responsible for>
|
|
493
|
-
- <who or what it serves>
|
|
494
|
-
|
|
495
|
-
## Stable Facts
|
|
496
|
-
- <durable fact that future agents or developers must know>
|
|
497
|
-
- <key architectural invariant>
|
|
498
|
-
|
|
499
|
-
## Boundaries
|
|
500
|
-
- In scope:
|
|
501
|
-
- <major capability or boundary>
|
|
502
|
-
- Out of scope:
|
|
503
|
-
- <intentional non-goal>
|
|
504
|
-
|
|
505
|
-
## Structure
|
|
506
|
-
- <important directory or subsystem> \u2014 <responsibility>
|
|
507
|
-
|
|
508
|
-
## Constraints
|
|
509
|
-
- <cross-cutting technical or operational constraint, when it affects architecture>
|
|
510
|
-
`;
|
|
511
|
-
}
|
|
512
|
-
function generateProjectRulesContent(projectName) {
|
|
513
|
-
return `---
|
|
514
|
-
name: project-rules
|
|
515
|
-
description: Project-specific rules for ${projectName}
|
|
516
|
-
---
|
|
517
|
-
|
|
518
|
-
# Project Rules
|
|
519
|
-
|
|
520
|
-
## Scope
|
|
521
|
-
- <stable local rules, workflow constraints, or validation expectations>
|
|
522
|
-
- Do not put temporary debugging steps here.
|
|
523
|
-
|
|
524
|
-
## Validation
|
|
525
|
-
- <preferred stable validation commands, not temporary troubleshooting steps>
|
|
526
|
-
|
|
527
|
-
## Conventions
|
|
528
|
-
- <project-specific conventions>
|
|
529
|
-
`;
|
|
530
|
-
}
|
|
531
|
-
function generateRulesContent(name) {
|
|
532
|
-
const title = toTitleCase(name);
|
|
533
|
-
return `---
|
|
534
|
-
name: ${name}
|
|
535
|
-
description: Durable rules for ${title}
|
|
536
|
-
---
|
|
537
|
-
|
|
538
|
-
# ${title}
|
|
539
|
-
|
|
540
|
-
## Scope
|
|
541
|
-
- <what this rules file governs>
|
|
542
|
-
|
|
543
|
-
## Rules
|
|
544
|
-
- <stable local rule>
|
|
545
|
-
|
|
546
|
-
## Validation
|
|
547
|
-
- <relevant validation command or check>
|
|
548
|
-
`;
|
|
549
|
-
}
|
|
550
|
-
function generateSpecContent(name) {
|
|
551
|
-
const title = toTitleCase(name);
|
|
552
|
-
return `# ${title}
|
|
553
|
-
|
|
554
|
-
## Purpose
|
|
555
|
-
- <why this project-level spec exists>
|
|
556
|
-
|
|
557
|
-
## Stable Facts
|
|
558
|
-
- <durable fact that future work must know>
|
|
559
|
-
|
|
560
|
-
## Boundaries
|
|
561
|
-
- In scope:
|
|
562
|
-
- <what this spec covers>
|
|
563
|
-
- Out of scope:
|
|
564
|
-
- <what this spec does not cover>
|
|
565
|
-
|
|
566
|
-
## Constraints
|
|
567
|
-
- <stable constraint, if any>
|
|
568
|
-
`;
|
|
569
|
-
}
|
|
570
|
-
function toTitleCase(value) {
|
|
571
|
-
return value.split(/[-/]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
572
|
-
}
|
|
573
|
-
function extractSection(content, heading) {
|
|
574
|
-
const lines = content.split("\n");
|
|
575
|
-
const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
|
|
576
|
-
if (start === -1)
|
|
577
|
-
return "";
|
|
578
|
-
const body = [];
|
|
579
|
-
for (let index = start + 1; index < lines.length; index++) {
|
|
580
|
-
const line = lines[index];
|
|
581
|
-
if (line.startsWith("## "))
|
|
582
|
-
break;
|
|
583
|
-
body.push(line);
|
|
584
|
-
}
|
|
585
|
-
return body.join("\n").trim();
|
|
586
|
-
}
|
|
587
|
-
function hasMeaningfulBlockers(content) {
|
|
588
|
-
const blockers = extractSection(content, "Blockers");
|
|
589
|
-
if (!blockers)
|
|
590
|
-
return false;
|
|
591
|
-
const lines = blockers.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
592
|
-
if (lines.length === 0)
|
|
593
|
-
return false;
|
|
594
|
-
return lines.some((line) => !/^[-*]\s*(?:none)?$/i.test(line) && !/^none$/i.test(line));
|
|
595
|
-
}
|
|
596
|
-
function detectDeltaSections(content) {
|
|
597
|
-
const body = extractSection(content, "Spec");
|
|
598
|
-
return {
|
|
599
|
-
added: /^###\s*ADDED/im.test(body),
|
|
600
|
-
modified: /^###\s*MODIFIED/im.test(body),
|
|
601
|
-
removed: /^###\s*REMOVED/im.test(body)
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
function collectArchiveReadiness(content) {
|
|
605
|
-
const warnings = [];
|
|
606
|
-
const tasksSection = extractSection(content, "Tasks");
|
|
607
|
-
const verifySection = extractSection(content, "Verify");
|
|
608
|
-
const taskTodos = getOpenCheckboxes(tasksSection);
|
|
609
|
-
if (taskTodos.length > 0)
|
|
610
|
-
warnings.push(`${taskTodos.length} task item(s) still incomplete`);
|
|
611
|
-
const verifyTodos = getOpenCheckboxes(verifySection);
|
|
612
|
-
if (verifyTodos.length > 0)
|
|
613
|
-
warnings.push(`${verifyTodos.length} Verify checklist item(s) are still incomplete`);
|
|
614
|
-
const activeBlockers = hasMeaningfulBlockers(content);
|
|
615
|
-
if (activeBlockers)
|
|
616
|
-
warnings.push("active blockers are present in the change file");
|
|
617
|
-
const scenarios = parseScenarios(content);
|
|
618
|
-
const missingScenarios = scenarios.length === 0;
|
|
619
|
-
if (missingScenarios)
|
|
620
|
-
warnings.push("no Scenario blocks found (some changes do not need them)");
|
|
621
|
-
const archiveReady = activeBlockers ? "no" : warnings.length === 0 ? "yes" : "judgment";
|
|
622
|
-
return {
|
|
623
|
-
taskTodos,
|
|
624
|
-
verifyTodos,
|
|
625
|
-
activeBlockers,
|
|
626
|
-
scenarioCount: scenarios.length,
|
|
627
|
-
missingScenarios,
|
|
628
|
-
deterministic: warnings.length === 0 ? "pass" : "warnings",
|
|
629
|
-
semantic: "needs-review",
|
|
630
|
-
archiveReady,
|
|
631
|
-
warnings
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
function buildDurableReviewGuidance(candidateTargets) {
|
|
635
|
-
return {
|
|
636
|
-
required: true,
|
|
637
|
-
decisions: [
|
|
638
|
-
"No durable update needed",
|
|
639
|
-
"Update existing spec or rule",
|
|
640
|
-
"Create a new durable spec"
|
|
641
|
-
],
|
|
642
|
-
candidateTargets,
|
|
643
|
-
note: "Semantic review decides whether stable facts belong in specs or rules; the CLI never merges delta specs automatically."
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
function getDurableReviewCandidateTargets(options = {}) {
|
|
647
|
-
const targets = [".rsp/specs/design.md"];
|
|
648
|
-
if (options.projectRulesExists)
|
|
649
|
-
targets.push(".rsp/rules/project-rules.md");
|
|
650
|
-
return targets;
|
|
651
|
-
}
|
|
652
|
-
function collectArchiveChecklist(content) {
|
|
653
|
-
return collectArchiveReadiness(content).warnings;
|
|
654
|
-
}
|
|
655
|
-
function parseScenarios(content) {
|
|
656
|
-
const scenarios = [];
|
|
657
|
-
const regex = /^#{3,4}\s*Scenario:\s*(.+)$/gim;
|
|
658
|
-
let match;
|
|
659
|
-
while ((match = regex.exec(content)) !== null) {
|
|
660
|
-
const heading = match[1].trim();
|
|
661
|
-
const startPos = match.index + match[0].length;
|
|
662
|
-
const restContent = content.slice(startPos);
|
|
663
|
-
const nextSection = restContent.match(/^#{3,4}\s/m);
|
|
664
|
-
const block = nextSection ? restContent.slice(0, nextSection.index) : restContent;
|
|
665
|
-
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*/, ""));
|
|
666
|
-
if (steps.length > 0)
|
|
667
|
-
scenarios.push({ heading, steps });
|
|
668
|
-
}
|
|
669
|
-
return scenarios;
|
|
670
|
-
}
|
|
85
|
+
import { defineCommand, runMain } from "citty";
|
|
671
86
|
|
|
672
|
-
// src/
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
var RSP_DIR = ".rsp";
|
|
676
|
-
var CHANGES_DIR = join2(RSP_DIR, "changes");
|
|
677
|
-
var FOCUS_DIR = join2(RSP_DIR, "focus.d");
|
|
678
|
-
var ARCHIVES_DIR = join2(RSP_DIR, "archives");
|
|
679
|
-
var LOCK_PATH = join2(RSP_DIR, ".lock");
|
|
680
|
-
var CONFIG_PATH = join2(RSP_DIR, "config.yaml");
|
|
681
|
-
var VALID_KINDS = ["feature", "fix", "refactor", "docs", "ops", "research"];
|
|
682
|
-
var DEFAULT_REQUIRED_SECTIONS = ["Proposal", "Spec", "Design", "Tasks", "Verify", "Blockers"];
|
|
683
|
-
var _configCache = null;
|
|
684
|
-
async function loadRspConfig() {
|
|
685
|
-
if (_configCache)
|
|
686
|
-
return _configCache;
|
|
687
|
-
if (!existsSync2(CONFIG_PATH)) {
|
|
688
|
-
_configCache = {};
|
|
689
|
-
return _configCache;
|
|
690
|
-
}
|
|
691
|
-
const raw = await readFile2(CONFIG_PATH, "utf-8");
|
|
692
|
-
const parsed = parseYamlText(raw);
|
|
693
|
-
_configCache = {
|
|
694
|
-
kinds: Array.isArray(parsed.kinds) ? parsed.kinds.map(String) : void 0
|
|
695
|
-
};
|
|
696
|
-
return _configCache;
|
|
697
|
-
}
|
|
698
|
-
function resolveKinds(config) {
|
|
699
|
-
return config.kinds && config.kinds.length > 0 ? config.kinds : [...VALID_KINDS];
|
|
700
|
-
}
|
|
701
|
-
function resolveRequiredSections(_config) {
|
|
702
|
-
return [...DEFAULT_REQUIRED_SECTIONS];
|
|
703
|
-
}
|
|
704
|
-
async function getVersion() {
|
|
705
|
-
const { version } = JSON.parse(await readFile2(join2(PKG_ROOT, "package.json"), "utf-8"));
|
|
706
|
-
return version;
|
|
707
|
-
}
|
|
87
|
+
// src/commands/add-spec.ts
|
|
88
|
+
import { mkdir as mkdir2, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
|
|
89
|
+
import { join as join2 } from "path";
|
|
708
90
|
|
|
709
91
|
// src/core/lock.ts
|
|
710
|
-
import { mkdir, readFile
|
|
92
|
+
import { mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
711
93
|
async function withRspLock(operation, fn) {
|
|
94
|
+
requireManagedDirectory(RSP_DIR, "RSP root", { allowMissing: true });
|
|
712
95
|
await mkdir(RSP_DIR, { recursive: true });
|
|
713
96
|
try {
|
|
714
97
|
await writeFile(LOCK_PATH, `${process.pid}
|
|
@@ -731,7 +114,7 @@ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
|
731
114
|
}
|
|
732
115
|
async function handleStaleLock(operation) {
|
|
733
116
|
try {
|
|
734
|
-
const content = await
|
|
117
|
+
const content = await readFile(LOCK_PATH, "utf-8");
|
|
735
118
|
const lines = content.trim().split("\n");
|
|
736
119
|
const pid = Number(lines[0]);
|
|
737
120
|
if (pid && Number.isFinite(pid)) {
|
|
@@ -766,50 +149,30 @@ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
|
766
149
|
}
|
|
767
150
|
}
|
|
768
151
|
|
|
769
|
-
// src/commands/add-rules.ts
|
|
770
|
-
async function addRules(name, projectName = "project") {
|
|
771
|
-
if (!name || !isValidChangeName(name)) {
|
|
772
|
-
console.error(` ${pc.red("Error:")} rule name must be kebab-case with optional subdirectory`);
|
|
773
|
-
process.exit(1);
|
|
774
|
-
}
|
|
775
|
-
guardRspInitialized();
|
|
776
|
-
const path = join3(RSP_DIR, "rules", `${name}.md`);
|
|
777
|
-
if (existsSync3(path)) {
|
|
778
|
-
console.error(` ${pc.red("Error:")} rule file already exists: .rsp/rules/${name}.md`);
|
|
779
|
-
process.exit(1);
|
|
780
|
-
}
|
|
781
|
-
return withRspLock("add-rules", async () => {
|
|
782
|
-
await mkdir2(dirname3(path), { recursive: true });
|
|
783
|
-
const resolvedProjectName = projectName === "project" ? await detectProjectName() : projectName;
|
|
784
|
-
const content = name === "project-rules" ? generateProjectRulesContent(resolvedProjectName) : generateRulesContent(name);
|
|
785
|
-
await writeFile2(path, content);
|
|
786
|
-
console.log(` ${pc.green("Created:")} .rsp/rules/${name}.md
|
|
787
|
-
`);
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// src/commands/add-spec.ts
|
|
792
|
-
import { existsSync as existsSync5 } from "fs";
|
|
793
|
-
import { mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
|
|
794
|
-
import { dirname as dirname4, join as join5 } from "path";
|
|
795
|
-
|
|
796
152
|
// src/commands/specs-index.ts
|
|
797
|
-
import { existsSync
|
|
798
|
-
import { readFile as
|
|
799
|
-
import { basename
|
|
153
|
+
import { existsSync } from "fs";
|
|
154
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
155
|
+
import { basename, isAbsolute, join, relative, sep } from "path";
|
|
800
156
|
async function buildSpecsIndex({ acquireLock = true, quiet = false } = {}) {
|
|
801
157
|
if (acquireLock)
|
|
802
158
|
return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false, quiet }));
|
|
803
|
-
const specsDir =
|
|
804
|
-
|
|
159
|
+
const specsDir = join(RSP_DIR, "specs");
|
|
160
|
+
const configInspection = await inspectRspConfig();
|
|
161
|
+
if (configInspection.issues.length > 0)
|
|
162
|
+
throw new Error(configInspection.issues.join("; "));
|
|
163
|
+
const decisionRecordsPath = resolveDecisionRecordsPath(configInspection.config);
|
|
164
|
+
const inspection = await inspectManagedFileTree(specsDir, "Specs", { allowMissing: true });
|
|
165
|
+
if (inspection.issues.length > 0)
|
|
166
|
+
throw inspection.issues[0];
|
|
167
|
+
if (!inspection.rootExists) {
|
|
805
168
|
if (!quiet)
|
|
806
169
|
console.log(` ${pc.dim("No specs directory.")}
|
|
807
170
|
`);
|
|
808
171
|
return false;
|
|
809
172
|
}
|
|
810
|
-
const specFiles = (
|
|
811
|
-
const name =
|
|
812
|
-
return name !== "INDEX.md" && name !== "design.md";
|
|
173
|
+
const specFiles = inspection.files.filter((fp) => fp.endsWith(".md")).filter((fp) => {
|
|
174
|
+
const name = basename(fp);
|
|
175
|
+
return name !== "INDEX.md" && name !== "design.md" && !isInside(DEFAULT_DECISION_RECORDS_PATH, fp) && !isInside(decisionRecordsPath, fp);
|
|
813
176
|
}).sort();
|
|
814
177
|
const lines = [];
|
|
815
178
|
lines.push("---");
|
|
@@ -831,8 +194,8 @@ async function buildSpecsIndex({ acquireLock = true, quiet = false } = {}) {
|
|
|
831
194
|
lines.push("| File | Title | Summary |");
|
|
832
195
|
lines.push("|------|-------|---------|");
|
|
833
196
|
for (const fp of specFiles) {
|
|
834
|
-
const rel = normalizeLogicalPath(
|
|
835
|
-
const content = await
|
|
197
|
+
const rel = normalizeLogicalPath(relative(specsDir, fp));
|
|
198
|
+
const content = await readFile2(fp, "utf-8");
|
|
836
199
|
const fm = parseFrontmatter(content);
|
|
837
200
|
const fmTitle = fm && typeof fm.title === "string" ? fm.title : "";
|
|
838
201
|
const title = fmTitle || extractTitle(content) || rel.replace(/\.md$/, "");
|
|
@@ -840,17 +203,21 @@ async function buildSpecsIndex({ acquireLock = true, quiet = false } = {}) {
|
|
|
840
203
|
lines.push(`| ${escapeCell(rel)} | ${escapeCell(title)} | ${escapeCell(summary || "\u2014")} |`);
|
|
841
204
|
}
|
|
842
205
|
}
|
|
843
|
-
const indexPath =
|
|
206
|
+
const indexPath = join(specsDir, "INDEX.md");
|
|
844
207
|
const nextContent = lines.join("\n");
|
|
845
|
-
const existingContent =
|
|
208
|
+
const existingContent = existsSync(indexPath) ? await readFile2(indexPath, "utf-8") : null;
|
|
846
209
|
const changed = existingContent !== nextContent;
|
|
847
210
|
if (changed)
|
|
848
|
-
await
|
|
211
|
+
await writeManagedFile(indexPath, nextContent, "Specs index");
|
|
849
212
|
if (!quiet)
|
|
850
213
|
console.log(` ${pc.green("INDEX.md updated:")} ${specFiles.length} spec file(s).
|
|
851
214
|
`);
|
|
852
215
|
return changed;
|
|
853
216
|
}
|
|
217
|
+
function isInside(directory, filePath) {
|
|
218
|
+
const rel = relative(directory, filePath);
|
|
219
|
+
return rel === "" || !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
220
|
+
}
|
|
854
221
|
function extractTitle(content) {
|
|
855
222
|
for (const rawLine of content.split("\n")) {
|
|
856
223
|
if (!rawLine.startsWith("# "))
|
|
@@ -886,46 +253,82 @@ function escapeCell(value) {
|
|
|
886
253
|
|
|
887
254
|
// src/commands/add-spec.ts
|
|
888
255
|
async function addSpec(name, projectName = "project") {
|
|
889
|
-
if (!name || !
|
|
256
|
+
if (!name || !isValidSpecName(name)) {
|
|
890
257
|
console.error(` ${pc.red("Error:")} spec name must be kebab-case with optional subdirectory`);
|
|
891
258
|
process.exit(1);
|
|
892
259
|
}
|
|
260
|
+
if (name.startsWith("decisions/")) {
|
|
261
|
+
console.error(` ${pc.red("Error:")} .rsp/specs/decisions/ is reserved for Decision Records`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
893
264
|
guardRspInitialized();
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
265
|
+
try {
|
|
266
|
+
const configInspection = await inspectRspConfig();
|
|
267
|
+
if (configInspection.issues.length > 0) {
|
|
268
|
+
console.error(` ${pc.red("Error:")} ${configInspection.issues.join("; ")}`);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error(` ${pc.red("Error:")} .rsp/config.yaml could not be parsed: ${toErrorMessage(error)}`);
|
|
897
273
|
process.exit(1);
|
|
898
274
|
}
|
|
899
275
|
return withRspLock("add-spec", async () => {
|
|
900
|
-
|
|
276
|
+
const specsDir = join2(RSP_DIR, "specs");
|
|
277
|
+
const segments = name.split("/");
|
|
278
|
+
const parent = resolveManagedDirectoryChain(specsDir, segments.slice(0, -1), "spec path");
|
|
279
|
+
const path = join2(parent, `${segments.at(-1)}.md`);
|
|
280
|
+
const inspection = await inspectManagedFileTree(specsDir, "Specs");
|
|
281
|
+
if (inspection.issues.length > 0)
|
|
282
|
+
throw inspection.issues[0];
|
|
283
|
+
await mkdir2(parent, { recursive: true });
|
|
901
284
|
const resolvedProjectName = projectName === "project" ? await detectProjectName() : projectName;
|
|
902
285
|
const content = name === "design" ? generateDesignContent(resolvedProjectName) : generateSpecContent(name);
|
|
903
|
-
|
|
904
|
-
|
|
286
|
+
let created = false;
|
|
287
|
+
try {
|
|
288
|
+
await writeFile2(path, content, { flag: "wx" });
|
|
289
|
+
created = true;
|
|
290
|
+
await buildSpecsIndex({ acquireLock: false });
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (created) {
|
|
293
|
+
try {
|
|
294
|
+
await unlink2(path);
|
|
295
|
+
await cleanupEmptyParentDirs(path, specsDir);
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (error.code === "EEXIST")
|
|
300
|
+
throw new Error(`spec file already exists: .rsp/specs/${name}.md`);
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
905
303
|
console.log(` ${pc.green("Created:")} .rsp/specs/${name}.md
|
|
906
304
|
`);
|
|
907
305
|
});
|
|
908
306
|
}
|
|
909
307
|
|
|
910
308
|
// src/commands/archive.ts
|
|
911
|
-
import { existsSync as
|
|
912
|
-
import { mkdir as
|
|
913
|
-
import { basename as
|
|
309
|
+
import { existsSync as existsSync3 } from "fs";
|
|
310
|
+
import { mkdir as mkdir3, readFile as readFile4, rename, unlink as unlink3 } from "fs/promises";
|
|
311
|
+
import { basename as basename3, join as join4 } from "path";
|
|
914
312
|
|
|
915
313
|
// src/commands/archive-index.ts
|
|
916
|
-
import { existsSync as
|
|
917
|
-
import { readFile as
|
|
918
|
-
import { basename as
|
|
314
|
+
import { existsSync as existsSync2 } from "fs";
|
|
315
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
316
|
+
import { basename as basename2, dirname, join as join3, relative as relative2 } from "path";
|
|
919
317
|
async function buildArchiveIndex({ acquireLock = true, quiet = false } = {}) {
|
|
920
318
|
if (acquireLock)
|
|
921
319
|
return withRspLock("archive-index", async () => buildArchiveIndex({ acquireLock: false, quiet }));
|
|
922
|
-
|
|
320
|
+
const inspection = await inspectArchiveTree();
|
|
321
|
+
if (inspection.diagnostics.length > 0) {
|
|
322
|
+
const diagnostic = inspection.diagnostics[0];
|
|
323
|
+
throw new WorkRefError(diagnostic.code, diagnostic.message, diagnostic.input);
|
|
324
|
+
}
|
|
325
|
+
if (!inspection.rootExists) {
|
|
923
326
|
if (!quiet)
|
|
924
327
|
console.log(` ${pc.dim("No archives directory.")}
|
|
925
328
|
`);
|
|
926
329
|
return false;
|
|
927
330
|
}
|
|
928
|
-
const archiveFiles =
|
|
331
|
+
const archiveFiles = inspection.files;
|
|
929
332
|
const lines = [];
|
|
930
333
|
lines.push("---");
|
|
931
334
|
lines.push("title: Archive Index");
|
|
@@ -945,9 +348,9 @@ async function buildArchiveIndex({ acquireLock = true, quiet = false } = {}) {
|
|
|
945
348
|
lines.push("|------|--------|------|---------|");
|
|
946
349
|
}
|
|
947
350
|
for (const fp of archiveFiles) {
|
|
948
|
-
const base =
|
|
949
|
-
const archiveRel = normalizeLogicalPath(
|
|
950
|
-
const archiveDirPart =
|
|
351
|
+
const base = basename2(fp);
|
|
352
|
+
const archiveRel = normalizeLogicalPath(relative2(ARCHIVES_DIR, fp));
|
|
353
|
+
const archiveDirPart = dirname(archiveRel);
|
|
951
354
|
const dateMatch = base.match(/^(\d{4}-\d{2}-\d{2})_(.+)\.md$/);
|
|
952
355
|
const date = dateMatch ? dateMatch[1] : "\u2014";
|
|
953
356
|
const baseName = dateMatch ? dateMatch[2] : base.replace(/\.md$/, "");
|
|
@@ -955,26 +358,29 @@ async function buildArchiveIndex({ acquireLock = true, quiet = false } = {}) {
|
|
|
955
358
|
let summary = "";
|
|
956
359
|
let kind = "";
|
|
957
360
|
try {
|
|
958
|
-
const content = await
|
|
361
|
+
const content = await readFile3(fp, "utf-8");
|
|
959
362
|
const fm = parseFrontmatter(content);
|
|
960
363
|
if (typeof fm?.kind === "string")
|
|
961
364
|
kind = fm.kind;
|
|
962
365
|
if (typeof fm?.summary === "string")
|
|
963
366
|
summary = fm.summary.trim();
|
|
964
367
|
const proposal = extractSection(content, "Proposal");
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
368
|
+
const outcomeLine = proposal.split("\n").map((line) => line.trim()).find((line) => line.startsWith("- Outcome:"));
|
|
369
|
+
const legacySummaryLine = proposal.split("\n").map((line) => line.trim()).find((line) => line.startsWith("- Summary:"));
|
|
370
|
+
if (!summary && outcomeLine)
|
|
371
|
+
summary = outcomeLine.slice("- Outcome:".length).trim();
|
|
372
|
+
if (!summary && legacySummaryLine)
|
|
373
|
+
summary = legacySummaryLine.slice("- Summary:".length).trim();
|
|
968
374
|
} catch {
|
|
969
375
|
}
|
|
970
376
|
lines.push(`| ${date} | ${escapeCell2(changeName)} | ${escapeCell2(kind || "\u2014")} | ${escapeCell2(summary || "\u2014")} |`);
|
|
971
377
|
}
|
|
972
|
-
const indexPath =
|
|
378
|
+
const indexPath = join3(ARCHIVES_DIR, "INDEX.md");
|
|
973
379
|
const nextContent = lines.join("\n");
|
|
974
|
-
const existingContent =
|
|
380
|
+
const existingContent = existsSync2(indexPath) ? await readFile3(indexPath, "utf-8") : null;
|
|
975
381
|
const changed = existingContent !== nextContent;
|
|
976
382
|
if (changed)
|
|
977
|
-
await
|
|
383
|
+
await writeManagedFile(indexPath, nextContent, "archive index");
|
|
978
384
|
if (!quiet)
|
|
979
385
|
console.log(` ${pc.green("INDEX.md updated:")} ${archiveFiles.length} archived change(s).
|
|
980
386
|
`);
|
|
@@ -990,19 +396,14 @@ async function archiveChange(name, options = {}) {
|
|
|
990
396
|
console.error(` ${pc.red("Usage:")} rsp archive <name>`);
|
|
991
397
|
process.exit(1);
|
|
992
398
|
}
|
|
993
|
-
if (!isValidChangeName(name)) {
|
|
994
|
-
console.error(` ${pc.red("Error:")} change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
995
|
-
process.exit(1);
|
|
996
|
-
}
|
|
997
399
|
guardRspInitialized();
|
|
998
|
-
const srcPath = join7(CHANGES_DIR, `${name}.md`);
|
|
999
|
-
if (!existsSync7(srcPath)) {
|
|
1000
|
-
console.error(` ${pc.red("Change not found:")} .rsp/changes/${name}.md`);
|
|
1001
|
-
process.exit(1);
|
|
1002
|
-
}
|
|
1003
400
|
if (options.dryRun) {
|
|
1004
|
-
const
|
|
1005
|
-
const
|
|
401
|
+
const workRef = await resolveArchiveWorkRefOrExit(name);
|
|
402
|
+
const content = await readFile4(workRef.path, "utf-8");
|
|
403
|
+
const dependencyInspection = await inspectChangeDependencies();
|
|
404
|
+
const checklist = collectArchiveChecklist(content, {
|
|
405
|
+
activeBlockers: dependencyInspection.activeBlockers.get(name)
|
|
406
|
+
});
|
|
1006
407
|
console.log();
|
|
1007
408
|
console.log(` ${pc.bold("Archive dry-run for")} ${pc.cyan(name)}`);
|
|
1008
409
|
console.log();
|
|
@@ -1018,140 +419,214 @@ async function archiveChange(name, options = {}) {
|
|
|
1018
419
|
}
|
|
1019
420
|
return;
|
|
1020
421
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
422
|
+
try {
|
|
423
|
+
return await withRspLock("archive-change", async () => {
|
|
424
|
+
const workRef = await resolveExecutableChange(name, { mustExist: true });
|
|
425
|
+
const srcPath = workRef.path;
|
|
426
|
+
const archiveSubdir = resolveArchiveDirectory(workRef);
|
|
427
|
+
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
428
|
+
const content = await readFile4(srcPath, "utf-8");
|
|
429
|
+
const dependencyInspection = await inspectChangeDependencies();
|
|
430
|
+
const checklist = collectArchiveChecklist(content, {
|
|
431
|
+
activeBlockers: dependencyInspection.activeBlockers.get(name)
|
|
432
|
+
});
|
|
433
|
+
for (const line of checklist)
|
|
434
|
+
console.log(` ${pc.yellow("\u26A0")} ${line}`);
|
|
435
|
+
if (checklist.length > 0)
|
|
436
|
+
console.log(` ${pc.dim("Archive will continue. Review the warnings above before treating this work as fully closed.")}
|
|
1028
437
|
`);
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
438
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
439
|
+
const base = basename3(workRef.name);
|
|
440
|
+
await mkdir3(archiveSubdir, { recursive: true });
|
|
441
|
+
const archiveName = resolveArchiveName(archiveSubdir, date, base);
|
|
442
|
+
await rename(srcPath, join4(archiveSubdir, archiveName));
|
|
443
|
+
const postArchiveWarnings = [];
|
|
444
|
+
let focusCleared = false;
|
|
445
|
+
if (existsSync3(focusEntry)) {
|
|
446
|
+
try {
|
|
447
|
+
await unlink3(focusEntry);
|
|
448
|
+
await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
|
|
449
|
+
focusCleared = true;
|
|
450
|
+
} catch (error) {
|
|
451
|
+
postArchiveWarnings.push(`focus marker cleanup failed: ${toErrorMessage(error)}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
1040
454
|
try {
|
|
1041
|
-
await
|
|
1042
|
-
await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
|
|
1043
|
-
focusCleared = true;
|
|
455
|
+
await cleanupEmptyParentDirs(srcPath, CHANGES_DIR);
|
|
1044
456
|
} catch (error) {
|
|
1045
|
-
postArchiveWarnings.push(`
|
|
457
|
+
postArchiveWarnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
|
|
1046
458
|
}
|
|
1047
|
-
|
|
1048
|
-
try {
|
|
1049
|
-
await cleanupEmptyParentDirs(srcPath, CHANGES_DIR);
|
|
1050
|
-
} catch (error) {
|
|
1051
|
-
postArchiveWarnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
|
|
1052
|
-
}
|
|
1053
|
-
const clearedMsg = focusCleared ? ` ${pc.dim("focus marker cleared")}
|
|
459
|
+
const clearedMsg = focusCleared ? ` ${pc.dim("focus marker cleared")}
|
|
1054
460
|
` : "";
|
|
1055
|
-
|
|
461
|
+
console.log(` ${pc.green("Archived:")} ${archiveName}
|
|
1056
462
|
${clearedMsg}`);
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
463
|
+
try {
|
|
464
|
+
await buildArchiveIndex({ acquireLock: false });
|
|
465
|
+
} catch (error) {
|
|
466
|
+
postArchiveWarnings.push(`archive index rebuild failed: ${toErrorMessage(error)}`);
|
|
467
|
+
}
|
|
468
|
+
for (const warning of postArchiveWarnings)
|
|
469
|
+
console.log(` ${pc.yellow("\u26A0")} ${warning}`);
|
|
470
|
+
if (postArchiveWarnings.length > 0)
|
|
471
|
+
console.log(` ${pc.dim("Archive completed, but follow-up cleanup was only partially successful.")}
|
|
1066
472
|
`);
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
console.log(` ${pc.cyan("Git workflow:")}
|
|
473
|
+
if (existsSync3(".git")) {
|
|
474
|
+
console.log(` ${pc.cyan("Git delivery:")}
|
|
1070
475
|
`);
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
476
|
+
console.log(` git status --short`);
|
|
477
|
+
console.log(` Inspect the complete archive transition; stage and commit only with separate Git authority.`);
|
|
478
|
+
console.log();
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
} catch (error) {
|
|
482
|
+
if (error instanceof WorkRefError) {
|
|
483
|
+
console.error(` ${pc.red("Error:")} ${error.message}`);
|
|
484
|
+
process.exit(1);
|
|
1074
485
|
}
|
|
1075
|
-
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function resolveArchiveWorkRefOrExit(name) {
|
|
490
|
+
try {
|
|
491
|
+
const workRef = await resolveExecutableChange(name, { mustExist: true });
|
|
492
|
+
resolveArchiveDirectory(workRef);
|
|
493
|
+
resolveFocusMarkerPath(workRef);
|
|
494
|
+
return workRef;
|
|
495
|
+
} catch (error) {
|
|
496
|
+
if (error instanceof WorkRefError) {
|
|
497
|
+
console.error(` ${pc.red("Error:")} ${error.message}`);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
throw error;
|
|
501
|
+
}
|
|
1076
502
|
}
|
|
1077
503
|
function resolveArchiveName(archiveSubdir, date, base) {
|
|
1078
504
|
const initialName = `${date}_${base}.md`;
|
|
1079
|
-
if (!
|
|
505
|
+
if (!existsSync3(join4(archiveSubdir, initialName)))
|
|
1080
506
|
return initialName;
|
|
1081
507
|
const MAX_SUFFIX = 9999;
|
|
1082
508
|
for (let index = 2; index <= MAX_SUFFIX; index++) {
|
|
1083
509
|
const candidate = `${date}_${base}-${index}.md`;
|
|
1084
|
-
if (!
|
|
510
|
+
if (!existsSync3(join4(archiveSubdir, candidate)))
|
|
1085
511
|
return candidate;
|
|
1086
512
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
513
|
+
throw new WorkRefError(
|
|
514
|
+
"archive_name_exhausted",
|
|
515
|
+
`archive name collision exceeded ${MAX_SUFFIX} attempts for ${base}`,
|
|
516
|
+
base
|
|
517
|
+
);
|
|
1089
518
|
}
|
|
1090
519
|
|
|
1091
520
|
// src/commands/check.ts
|
|
1092
|
-
import {
|
|
1093
|
-
import { readFile as readFile7 } from "fs/promises";
|
|
1094
|
-
import { join as join8, relative as relative4 } from "path";
|
|
521
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1095
522
|
async function runCheck(options = {}) {
|
|
1096
|
-
const
|
|
523
|
+
const diagnostics = [];
|
|
524
|
+
const runtime = [];
|
|
525
|
+
let config;
|
|
526
|
+
try {
|
|
527
|
+
config = await loadRspConfig();
|
|
528
|
+
} catch (error) {
|
|
529
|
+
const result2 = {
|
|
530
|
+
command: "check",
|
|
531
|
+
ok: false,
|
|
532
|
+
focused: Boolean(options.focused),
|
|
533
|
+
diagnostics: [{ severity: "error", code: "invalid_config", path: ".rsp/config.yaml", message: toErrorMessage(error) }],
|
|
534
|
+
runtime,
|
|
535
|
+
summary: { changeFiles: 0, errors: 1, warnings: 0 }
|
|
536
|
+
};
|
|
537
|
+
if (options.json)
|
|
538
|
+
emitJson(result2, options);
|
|
539
|
+
else
|
|
540
|
+
console.error(` ${pc.red("Error:")} ${toErrorMessage(error)}`);
|
|
541
|
+
return result2;
|
|
542
|
+
}
|
|
1097
543
|
const validKinds = resolveKinds(config);
|
|
1098
544
|
const requiredSections = resolveRequiredSections(config);
|
|
1099
545
|
const choosePlaceholderRe = /^<choose:/i;
|
|
1100
|
-
const diagnostics = [];
|
|
1101
|
-
const runtime = [];
|
|
1102
546
|
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
|
|
1103
547
|
const addDiagnostic = (diagnostic) => diagnostics.push(diagnostic);
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
548
|
+
const focusTree = await inspectFocusTree();
|
|
549
|
+
for (const diagnostic of focusTree.diagnostics) {
|
|
550
|
+
addDiagnostic({
|
|
551
|
+
severity: "error",
|
|
552
|
+
code: diagnostic.code,
|
|
553
|
+
change: diagnostic.input,
|
|
554
|
+
path: diagnostic.path,
|
|
555
|
+
message: diagnostic.message
|
|
556
|
+
});
|
|
1107
557
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
}
|
|
1149
|
-
|
|
558
|
+
const focusedSet = options.focused ? new Set(focusTree.markers.map((marker) => marker.name)) : /* @__PURE__ */ new Set();
|
|
559
|
+
for (const marker of focusTree.markers) {
|
|
560
|
+
const entryPath = marker.path;
|
|
561
|
+
const entryContent = marker.name;
|
|
562
|
+
let markerContent = "";
|
|
563
|
+
try {
|
|
564
|
+
markerContent = (await readFile5(entryPath, "utf-8")).trim();
|
|
565
|
+
} catch (error) {
|
|
566
|
+
addDiagnostic({
|
|
567
|
+
severity: "error",
|
|
568
|
+
code: "focus_marker_read_failed",
|
|
569
|
+
path: `focus.d/${entryContent}`,
|
|
570
|
+
change: entryContent,
|
|
571
|
+
message: "unable to read focus marker file",
|
|
572
|
+
hint: toErrorMessage(error)
|
|
573
|
+
});
|
|
574
|
+
reportRuntime({
|
|
575
|
+
code: "focus_marker_read_failed",
|
|
576
|
+
operation: "readFile",
|
|
577
|
+
path: entryPath,
|
|
578
|
+
message: toErrorMessage(error)
|
|
579
|
+
});
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (markerContent) {
|
|
583
|
+
addDiagnostic({
|
|
584
|
+
severity: "warning",
|
|
585
|
+
code: "focus_marker_not_empty",
|
|
586
|
+
path: `focus.d/${entryContent}`,
|
|
587
|
+
message: "should be an empty marker file; path is the source of truth"
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
resolveWorkRef(entryContent, { executable: true, mustExist: true });
|
|
592
|
+
} catch (error) {
|
|
593
|
+
if (!(error instanceof WorkRefError))
|
|
594
|
+
throw error;
|
|
595
|
+
addDiagnostic({
|
|
596
|
+
severity: "error",
|
|
597
|
+
code: error.code === "work_ref_not_found" ? "focused_change_missing" : error.code,
|
|
598
|
+
path: `focus.d/${entryContent}`,
|
|
599
|
+
change: entryContent,
|
|
600
|
+
message: error.code === "work_ref_not_found" ? `focuses "${entryContent}" but changes/${entryContent}.md not found` : error.message
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const workTree = await inspectWorkTree();
|
|
605
|
+
for (const diagnostic of workTree.diagnostics) {
|
|
606
|
+
const inspectionIncomplete = diagnostic.code === "invalid_work_root" || diagnostic.code === "work_tree_read_failed";
|
|
607
|
+
if (inspectionIncomplete || !options.focused || focusedSet.has(diagnostic.input)) {
|
|
608
|
+
addDiagnostic({
|
|
609
|
+
severity: "error",
|
|
610
|
+
code: diagnostic.code,
|
|
611
|
+
change: diagnostic.input,
|
|
612
|
+
path: diagnostic.path,
|
|
613
|
+
message: diagnostic.message
|
|
614
|
+
});
|
|
1150
615
|
}
|
|
1151
616
|
}
|
|
1152
|
-
const
|
|
1153
|
-
const
|
|
1154
|
-
|
|
617
|
+
const changeRefs = options.focused ? workTree.changes.filter((ref) => focusedSet.has(ref.name)) : workTree.changes;
|
|
618
|
+
const groupInspection = await inspectChangeGroups({ workTree, focusTree });
|
|
619
|
+
const focusedGroups = new Set(changeRefs.flatMap((ref) => ref.group ? [ref.group] : []));
|
|
620
|
+
for (const diagnostic of groupInspection.diagnostics) {
|
|
621
|
+
if (!options.focused || !diagnostic.change || focusedGroups.has(diagnostic.change))
|
|
622
|
+
addDiagnostic(diagnostic);
|
|
623
|
+
}
|
|
624
|
+
const dependencyInspection = await inspectChangeDependencies({ workTree });
|
|
625
|
+
for (const diagnostic of dependencyInspection.diagnostics) {
|
|
626
|
+
if (!options.focused || !diagnostic.change || focusedSet.has(diagnostic.change))
|
|
627
|
+
addDiagnostic(diagnostic);
|
|
628
|
+
}
|
|
629
|
+
if (changeRefs.length === 0) {
|
|
1155
630
|
const result2 = {
|
|
1156
631
|
command: "check",
|
|
1157
632
|
ok: diagnostics.every((d) => d.severity !== "error"),
|
|
@@ -1165,7 +640,7 @@ async function runCheck(options = {}) {
|
|
|
1165
640
|
}
|
|
1166
641
|
};
|
|
1167
642
|
if (options.json) {
|
|
1168
|
-
emitJson(result2);
|
|
643
|
+
emitJson(result2, options);
|
|
1169
644
|
return result2;
|
|
1170
645
|
}
|
|
1171
646
|
console.log();
|
|
@@ -1193,11 +668,11 @@ async function runCheck(options = {}) {
|
|
|
1193
668
|
`);
|
|
1194
669
|
return result2;
|
|
1195
670
|
}
|
|
1196
|
-
for (const
|
|
1197
|
-
const name =
|
|
671
|
+
for (const ref of changeRefs) {
|
|
672
|
+
const { name, path: fp } = ref;
|
|
1198
673
|
let content;
|
|
1199
674
|
try {
|
|
1200
|
-
content = await
|
|
675
|
+
content = await readFile5(fp, "utf-8");
|
|
1201
676
|
} catch (error) {
|
|
1202
677
|
addDiagnostic({
|
|
1203
678
|
severity: "error",
|
|
@@ -1368,13 +843,13 @@ async function runCheck(options = {}) {
|
|
|
1368
843
|
diagnostics,
|
|
1369
844
|
runtime,
|
|
1370
845
|
summary: {
|
|
1371
|
-
changeFiles:
|
|
846
|
+
changeFiles: changeRefs.length,
|
|
1372
847
|
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
1373
848
|
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
1374
849
|
}
|
|
1375
850
|
};
|
|
1376
851
|
if (options.json) {
|
|
1377
|
-
emitJson(result);
|
|
852
|
+
emitJson(result, options);
|
|
1378
853
|
return result;
|
|
1379
854
|
}
|
|
1380
855
|
console.log();
|
|
@@ -1394,10 +869,10 @@ async function runCheck(options = {}) {
|
|
|
1394
869
|
}
|
|
1395
870
|
console.log();
|
|
1396
871
|
if (result.summary.errors === 0 && result.summary.warnings === 0)
|
|
1397
|
-
console.log(` ${pc.green("\u2713")} All ${
|
|
872
|
+
console.log(` ${pc.green("\u2713")} All ${changeRefs.length} change file(s) valid.
|
|
1398
873
|
`);
|
|
1399
874
|
else
|
|
1400
|
-
console.log(` ${pc.red(String(result.summary.errors))} error(s), ${pc.yellow(String(result.summary.warnings))} warning(s) in ${
|
|
875
|
+
console.log(` ${pc.red(String(result.summary.errors))} error(s), ${pc.yellow(String(result.summary.warnings))} warning(s) in ${changeRefs.length} change file(s).
|
|
1401
876
|
`);
|
|
1402
877
|
return result;
|
|
1403
878
|
}
|
|
@@ -1426,92 +901,190 @@ function collectMatchingLines(content, predicate) {
|
|
|
1426
901
|
}
|
|
1427
902
|
|
|
1428
903
|
// src/commands/create.ts
|
|
1429
|
-
import { existsSync as
|
|
1430
|
-
import { mkdir as
|
|
1431
|
-
import { dirname as
|
|
904
|
+
import { existsSync as existsSync4 } from "fs";
|
|
905
|
+
import { mkdir as mkdir4, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
|
|
906
|
+
import { dirname as dirname2 } from "path";
|
|
1432
907
|
async function createChange(name, summary = "", kind, options = {}) {
|
|
1433
908
|
if (!name) {
|
|
1434
909
|
console.error(` ${pc.red("Usage:")} rsp create <name> [summary]`);
|
|
1435
910
|
process.exit(1);
|
|
1436
911
|
}
|
|
1437
|
-
if (!isValidChangeName(name)) {
|
|
1438
|
-
console.error(` ${pc.red("Error:")} change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
1439
|
-
process.exit(1);
|
|
1440
|
-
}
|
|
1441
912
|
guardRspInitialized();
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
913
|
+
try {
|
|
914
|
+
return await withRspLock("create-change", async () => {
|
|
915
|
+
const workRef = await resolveExecutableChange(name);
|
|
916
|
+
const changePath = workRef.path;
|
|
917
|
+
const existed = existsSync4(changePath);
|
|
918
|
+
if (!existed) {
|
|
919
|
+
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
920
|
+
const content = generateChangeContent(name, summary, kind, { lite: Boolean(options.lite) });
|
|
921
|
+
await mkdir4(dirname2(changePath), { recursive: true });
|
|
922
|
+
let changeCreated = false;
|
|
923
|
+
try {
|
|
924
|
+
await writeFile3(changePath, content, { flag: "wx" });
|
|
925
|
+
changeCreated = true;
|
|
926
|
+
await mkdir4(FOCUS_DIR, { recursive: true });
|
|
927
|
+
await mkdir4(dirname2(focusEntry), { recursive: true });
|
|
928
|
+
await writeManagedFile(focusEntry, "", "focus marker");
|
|
929
|
+
} catch (error) {
|
|
930
|
+
if (changeCreated) {
|
|
931
|
+
try {
|
|
932
|
+
await unlink4(changePath);
|
|
933
|
+
await cleanupEmptyParentDirs(changePath, CHANGES_DIR);
|
|
934
|
+
} catch {
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
throw error;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
const label = existed ? "Using" : pc.green("Created");
|
|
941
|
+
console.log(` ${label}: ${changePath}`);
|
|
942
|
+
if (existed)
|
|
943
|
+
console.log(` ${pc.dim("Unchanged focus.")} Run: rsp focus ${name}`);
|
|
944
|
+
else
|
|
945
|
+
console.log(` ${pc.dim("focused via focus.d")} \u2192 ${name}`);
|
|
946
|
+
console.log(` ${pc.cyan("Next:")} ${options.lite ? "fill the lite change details, then implement and verify" : "fill proposal/spec/design first, then implement and complete the tasks"}
|
|
1461
947
|
`);
|
|
1462
|
-
|
|
948
|
+
});
|
|
949
|
+
} catch (error) {
|
|
950
|
+
if (error instanceof WorkRefError) {
|
|
951
|
+
console.error(` ${pc.red("Error:")} ${error.message}`);
|
|
952
|
+
process.exit(1);
|
|
953
|
+
}
|
|
954
|
+
throw error;
|
|
955
|
+
}
|
|
1463
956
|
}
|
|
1464
957
|
|
|
1465
958
|
// src/commands/doctor.ts
|
|
1466
|
-
import { existsSync as
|
|
1467
|
-
import { readFile as
|
|
1468
|
-
import { basename as
|
|
959
|
+
import { existsSync as existsSync6 } from "fs";
|
|
960
|
+
import { readdir, readFile as readFile7, stat } from "fs/promises";
|
|
961
|
+
import { basename as basename4, join as join6, relative as relative3 } from "path";
|
|
1469
962
|
|
|
1470
963
|
// src/commands/update.ts
|
|
1471
|
-
import { existsSync as
|
|
1472
|
-
import { mkdir as
|
|
1473
|
-
import { join as
|
|
1474
|
-
var SKILL_REFRESH_HINT = " Note: if you use
|
|
964
|
+
import { existsSync as existsSync5 } from "fs";
|
|
965
|
+
import { mkdir as mkdir5, readFile as readFile6, rm, rmdir } from "fs/promises";
|
|
966
|
+
import { join as join5 } from "path";
|
|
967
|
+
var SKILL_REFRESH_HINT = " Note: if you use package-bundled RSP Skills, refresh them too:\n rsp skills install --dry-run\n rsp skills install --force\n";
|
|
1475
968
|
async function updateProject(options = {}) {
|
|
1476
|
-
if (!
|
|
969
|
+
if (!existsSync5(RSP_DIR)) {
|
|
1477
970
|
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
1478
971
|
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
1479
972
|
process.exit(1);
|
|
1480
973
|
}
|
|
1481
974
|
return withRspLock("update", async () => {
|
|
975
|
+
const agentsPath = "AGENTS.md";
|
|
976
|
+
const agentsInspection = inspectManagedFile(agentsPath, "AGENTS.md", { allowMissing: true });
|
|
977
|
+
if (agentsInspection.issue)
|
|
978
|
+
throw agentsInspection.issue;
|
|
1482
979
|
let updated = false;
|
|
1483
980
|
const actions = [];
|
|
1484
|
-
|
|
1485
|
-
const
|
|
1486
|
-
|
|
1487
|
-
|
|
981
|
+
clearConfigCache();
|
|
982
|
+
const configInspection = await inspectRspConfig();
|
|
983
|
+
if (configInspection.issues.length > 0)
|
|
984
|
+
throw new Error(configInspection.issues.join("; "));
|
|
985
|
+
const decisionRecordsPath = resolveDecisionRecordsPath(configInspection.config);
|
|
986
|
+
const decisionRecordsFilesystemIssue = await validateDecisionRecordsFilesystemPath(decisionRecordsPath);
|
|
987
|
+
if (decisionRecordsFilesystemIssue)
|
|
988
|
+
throw new Error(decisionRecordsFilesystemIssue);
|
|
989
|
+
const changesInspection = inspectManagedDirectory(CHANGES_DIR, "open work root", { allowMissing: true });
|
|
990
|
+
if (changesInspection.issue)
|
|
991
|
+
throw changesInspection.issue;
|
|
992
|
+
if (!changesInspection.exists) {
|
|
993
|
+
await mkdir5(CHANGES_DIR, { recursive: true });
|
|
994
|
+
await ensureManagedFile(join5(CHANGES_DIR, ".gitkeep"), "", "changes placeholder");
|
|
995
|
+
actions.push("changes/ directory restored");
|
|
996
|
+
if (!options.quiet)
|
|
997
|
+
console.log(` ${pc.green("\u2713")} changes/ directory restored`);
|
|
998
|
+
updated = true;
|
|
999
|
+
}
|
|
1000
|
+
const bundledRules = await readFile6(join5(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
|
|
1001
|
+
const rulesInspection = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
1002
|
+
if (rulesInspection.issue)
|
|
1003
|
+
throw rulesInspection.issue;
|
|
1004
|
+
const existingRules = rulesInspection.exists ? await readFile6(RSP_RULES_PATH, "utf-8") : null;
|
|
1488
1005
|
if (existingRules !== bundledRules) {
|
|
1489
|
-
await
|
|
1490
|
-
actions.push("
|
|
1006
|
+
await writeManagedFile(RSP_RULES_PATH, bundledRules, "fallback protocol");
|
|
1007
|
+
actions.push("rsp-rules.md updated");
|
|
1491
1008
|
if (!options.quiet)
|
|
1492
|
-
console.log(` ${pc.green("\u2713")}
|
|
1009
|
+
console.log(` ${pc.green("\u2713")} rsp-rules.md updated`);
|
|
1493
1010
|
updated = true;
|
|
1494
1011
|
}
|
|
1495
|
-
|
|
1012
|
+
if (existsSync5(OBSOLETE_RSP_RULES_PATH)) {
|
|
1013
|
+
await rm(OBSOLETE_RSP_RULES_PATH);
|
|
1014
|
+
actions.push("obsolete rules/rsp-rules.md removed");
|
|
1015
|
+
if (!options.quiet)
|
|
1016
|
+
console.log(` ${pc.green("\u2713")} obsolete rules/rsp-rules.md removed`);
|
|
1017
|
+
updated = true;
|
|
1018
|
+
}
|
|
1019
|
+
try {
|
|
1020
|
+
await rmdir(join5(RSP_DIR, "rules"));
|
|
1021
|
+
actions.push("empty rules directory removed");
|
|
1022
|
+
updated = true;
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
if (!["ENOENT", "ENOTEMPTY"].includes(error.code || ""))
|
|
1025
|
+
throw error;
|
|
1026
|
+
}
|
|
1027
|
+
let inspection = await inspectUnsupportedRules();
|
|
1028
|
+
if (inspection.diagnostics.length === 0 && inspection.directoryRemaining && inspection.entries.every((entry) => entry.kind === "directory")) {
|
|
1029
|
+
const directories = inspection.entries.map((entry) => entry.path).sort((left, right) => right.split("/").length - left.split("/").length);
|
|
1030
|
+
for (const path of directories)
|
|
1031
|
+
await rmdir(join5(RSP_DIR, "rules", path));
|
|
1032
|
+
await rmdir(join5(RSP_DIR, "rules"));
|
|
1033
|
+
actions.push("empty rules directory tree removed");
|
|
1034
|
+
if (!options.quiet)
|
|
1035
|
+
console.log(` ${pc.green("\u2713")} empty rules directory tree removed`);
|
|
1036
|
+
updated = true;
|
|
1037
|
+
inspection = await inspectUnsupportedRules();
|
|
1038
|
+
}
|
|
1039
|
+
const residualEntries = inspection.entries.map((entry) => entry.path);
|
|
1040
|
+
if (!options.quiet && inspection.diagnostics.length > 0) {
|
|
1041
|
+
console.log(` ${pc.red("Error:")} migration inspection incomplete:`);
|
|
1042
|
+
for (const diagnostic of inspection.diagnostics)
|
|
1043
|
+
console.log(` ${diagnostic.path}: ${diagnostic.message}`);
|
|
1044
|
+
console.log(` ${pc.dim("Fix access to .rsp/rules/, then run rsp update again.")}`);
|
|
1045
|
+
console.log();
|
|
1046
|
+
}
|
|
1047
|
+
if (!options.quiet && residualEntries.length > 0) {
|
|
1048
|
+
console.log(` ${pc.yellow("Warning:")} these entries are no longer read by RSP:`);
|
|
1049
|
+
for (const path of residualEntries)
|
|
1050
|
+
console.log(` .rsp/rules/${path}`);
|
|
1051
|
+
console.log(` ${pc.dim("Move stable scoped instructions to the nearest project-owned AGENTS.md, then remove the old files.")}`);
|
|
1052
|
+
console.log(` ${pc.dim("Run: rsp doctor")}`);
|
|
1053
|
+
console.log();
|
|
1054
|
+
}
|
|
1496
1055
|
const projectName = await detectProjectName();
|
|
1497
|
-
const baseAgents =
|
|
1056
|
+
const baseAgents = agentsInspection.exists ? await readFile6(agentsPath, "utf-8") : `# ${projectName}
|
|
1498
1057
|
|
|
1499
1058
|
`;
|
|
1500
1059
|
const nextAgents = upsertRspAgentsBlock(baseAgents);
|
|
1501
|
-
if (!
|
|
1502
|
-
await
|
|
1060
|
+
if (!agentsInspection.exists || nextAgents.changed) {
|
|
1061
|
+
await writeManagedFile(agentsPath, nextAgents.content, "AGENTS.md");
|
|
1503
1062
|
actions.push("AGENTS.md managed block refreshed");
|
|
1504
1063
|
if (!options.quiet)
|
|
1505
1064
|
console.log(` ${pc.green("\u2713")} AGENTS.md managed block refreshed`);
|
|
1506
1065
|
updated = true;
|
|
1507
1066
|
}
|
|
1067
|
+
if (await ensureDecisionRecordsDirectory(decisionRecordsPath)) {
|
|
1068
|
+
actions.push(`Decision Record directory ensured: ${decisionRecordsPath}`);
|
|
1069
|
+
if (!options.quiet)
|
|
1070
|
+
console.log(` ${pc.green("\u2713")} Decision Record directory ensured: ${decisionRecordsPath}`);
|
|
1071
|
+
updated = true;
|
|
1072
|
+
}
|
|
1508
1073
|
const specsIndexChanged = await buildSpecsIndex({ acquireLock: false, quiet: options.quiet });
|
|
1509
1074
|
const archiveIndexChanged = await buildArchiveIndex({ acquireLock: false, quiet: options.quiet });
|
|
1510
1075
|
if (specsIndexChanged || archiveIndexChanged) {
|
|
1511
1076
|
actions.push("generated indexes rebuilt");
|
|
1512
1077
|
updated = true;
|
|
1513
1078
|
}
|
|
1514
|
-
if (!options.quiet &&
|
|
1079
|
+
if (!options.quiet && inspection.diagnostics.length > 0) {
|
|
1080
|
+
console.log(` ${pc.red("Managed update incomplete; migration inspection failed.")}
|
|
1081
|
+
`);
|
|
1082
|
+
console.log(SKILL_REFRESH_HINT);
|
|
1083
|
+
} else if (!options.quiet && residualEntries.length > 0) {
|
|
1084
|
+
console.log(` ${pc.yellow(updated ? "Managed update complete; manual migration remains." : "Managed files are up to date; manual migration remains.")}
|
|
1085
|
+
`);
|
|
1086
|
+
console.log(SKILL_REFRESH_HINT);
|
|
1087
|
+
} else if (!options.quiet && !updated) {
|
|
1515
1088
|
console.log(` ${pc.dim("Already up to date.")}
|
|
1516
1089
|
`);
|
|
1517
1090
|
console.log(SKILL_REFRESH_HINT);
|
|
@@ -1520,34 +1093,60 @@ async function updateProject(options = {}) {
|
|
|
1520
1093
|
`);
|
|
1521
1094
|
console.log(SKILL_REFRESH_HINT);
|
|
1522
1095
|
}
|
|
1523
|
-
return
|
|
1096
|
+
return {
|
|
1097
|
+
actions,
|
|
1098
|
+
migration: {
|
|
1099
|
+
inspectionComplete: inspection.diagnostics.length === 0,
|
|
1100
|
+
manualMigrationRequired: inspection.directoryRemaining,
|
|
1101
|
+
residualEntries,
|
|
1102
|
+
diagnostics: inspection.diagnostics
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1524
1105
|
});
|
|
1525
1106
|
}
|
|
1526
1107
|
|
|
1527
1108
|
// src/commands/doctor.ts
|
|
1528
1109
|
async function runDoctor(options = {}) {
|
|
1529
1110
|
const fixed = [];
|
|
1530
|
-
|
|
1531
|
-
|
|
1111
|
+
let repairIssue = null;
|
|
1112
|
+
if (options.fix && existsSync6(RSP_DIR)) {
|
|
1113
|
+
try {
|
|
1114
|
+
const update = await updateProject({ quiet: Boolean(options.json) });
|
|
1115
|
+
fixed.push(...update.actions);
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
repairIssue = `safe deterministic repair could not run: ${toErrorMessage(error)}`;
|
|
1118
|
+
}
|
|
1532
1119
|
}
|
|
1533
1120
|
const checks = [];
|
|
1121
|
+
if (repairIssue) {
|
|
1122
|
+
checks.push({
|
|
1123
|
+
status: "issue",
|
|
1124
|
+
label: "safe deterministic repairs completed",
|
|
1125
|
+
message: repairIssue,
|
|
1126
|
+
hint: "Resolve the reported configuration or filesystem issue, then run rsp doctor --fix again."
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1534
1129
|
const runtime = [];
|
|
1535
1130
|
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
|
|
1536
|
-
const
|
|
1537
|
-
const
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
reportCheck(checks, "specs/design.md exists",
|
|
1543
|
-
reportCheck(checks, "specs/INDEX.md exists",
|
|
1544
|
-
reportCheck(checks, "archives/INDEX.md exists",
|
|
1131
|
+
const designPath = join6(RSP_DIR, "specs", "design.md");
|
|
1132
|
+
const specsIndexPath = join6(RSP_DIR, "specs", "INDEX.md");
|
|
1133
|
+
const archivesIndexPath = join6(RSP_DIR, "archives", "INDEX.md");
|
|
1134
|
+
reportCheck(checks, ".rsp exists", existsSync6(RSP_DIR), "Run: rsp init");
|
|
1135
|
+
reportFallbackProtocol(checks);
|
|
1136
|
+
await reportUnsupportedRules(checks, reportRuntime);
|
|
1137
|
+
reportCheck(checks, "specs/design.md exists", existsSync6(designPath), "Run: rsp init");
|
|
1138
|
+
reportCheck(checks, "specs/INDEX.md exists", existsSync6(specsIndexPath), "Run: rsp update");
|
|
1139
|
+
reportCheck(checks, "archives/INDEX.md exists", existsSync6(archivesIndexPath), "Run: rsp update");
|
|
1545
1140
|
await checkAgents(checks, reportRuntime);
|
|
1546
1141
|
await checkGeneratedIndex(checks, reportRuntime, specsIndexPath, "specs", "specs/INDEX.md has generated-index metadata", "Run: rsp update");
|
|
1547
1142
|
await checkGeneratedIndex(checks, reportRuntime, archivesIndexPath, "archives", "archives/INDEX.md has generated-index metadata", "Run: rsp update");
|
|
1548
|
-
await checkArchiveNaming(checks
|
|
1549
|
-
await checkConfigSemantics(checks, reportRuntime);
|
|
1550
|
-
|
|
1143
|
+
await checkArchiveNaming(checks);
|
|
1144
|
+
const decisionRecordsConfigValid = await checkConfigSemantics(checks, reportRuntime);
|
|
1145
|
+
if (decisionRecordsConfigValid) {
|
|
1146
|
+
await checkDecisionRecordsDirectory(checks, reportRuntime);
|
|
1147
|
+
await checkInactiveDefaultDecisionRecords(checks, reportRuntime);
|
|
1148
|
+
}
|
|
1149
|
+
await checkActiveChangeConsistency(checks);
|
|
1551
1150
|
const result = {
|
|
1552
1151
|
command: "doctor",
|
|
1553
1152
|
ok: checks.every((check) => check.status !== "issue"),
|
|
@@ -1559,7 +1158,7 @@ async function runDoctor(options = {}) {
|
|
|
1559
1158
|
}
|
|
1560
1159
|
};
|
|
1561
1160
|
if (options.json) {
|
|
1562
|
-
emitJson(result);
|
|
1161
|
+
emitJson(result, options);
|
|
1563
1162
|
return result;
|
|
1564
1163
|
}
|
|
1565
1164
|
console.log();
|
|
@@ -1568,21 +1167,72 @@ async function runDoctor(options = {}) {
|
|
|
1568
1167
|
if (fixed.length > 0) {
|
|
1569
1168
|
console.log(` ${pc.green("Fixed:")} ${fixed.join(", ")}`);
|
|
1570
1169
|
console.log();
|
|
1571
|
-
} else if (options.fix) {
|
|
1170
|
+
} else if (options.fix && !repairIssue && existsSync6(RSP_DIR)) {
|
|
1572
1171
|
console.log(` ${pc.dim("No safe fixes needed.")}`);
|
|
1573
1172
|
console.log();
|
|
1574
1173
|
}
|
|
1575
|
-
for (const check of checks)
|
|
1576
|
-
printDoctorCheck(check);
|
|
1577
|
-
if (result.summary.issues === 0)
|
|
1578
|
-
console.log(`
|
|
1579
|
-
${pc.green("\u2713")} RSP setup looks healthy.
|
|
1580
|
-
`);
|
|
1581
|
-
else
|
|
1582
|
-
console.log(`
|
|
1583
|
-
${pc.yellow(String(result.summary.issues))} issue(s) detected.
|
|
1584
|
-
`);
|
|
1585
|
-
return result;
|
|
1174
|
+
for (const check of checks)
|
|
1175
|
+
printDoctorCheck(check);
|
|
1176
|
+
if (result.summary.issues === 0)
|
|
1177
|
+
console.log(`
|
|
1178
|
+
${pc.green("\u2713")} RSP setup looks healthy.
|
|
1179
|
+
`);
|
|
1180
|
+
else
|
|
1181
|
+
console.log(`
|
|
1182
|
+
${pc.yellow(String(result.summary.issues))} issue(s) detected.
|
|
1183
|
+
`);
|
|
1184
|
+
return result;
|
|
1185
|
+
}
|
|
1186
|
+
function reportFallbackProtocol(checks) {
|
|
1187
|
+
const canonical = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
1188
|
+
const obsoleteExists = existsSync6(OBSOLETE_RSP_RULES_PATH);
|
|
1189
|
+
if (canonical.issue) {
|
|
1190
|
+
checks.push({
|
|
1191
|
+
status: "issue",
|
|
1192
|
+
label: "rsp-rules.md is a regular managed file",
|
|
1193
|
+
message: canonical.issue.message,
|
|
1194
|
+
hint: "Replace the unsupported entry with a project-local regular file, then run: rsp update"
|
|
1195
|
+
});
|
|
1196
|
+
} else if (canonical.exists) {
|
|
1197
|
+
checks.push({ status: "ok", label: "rsp-rules.md exists" });
|
|
1198
|
+
} else if (!obsoleteExists) {
|
|
1199
|
+
checks.push({
|
|
1200
|
+
status: "issue",
|
|
1201
|
+
label: "rsp-rules.md exists",
|
|
1202
|
+
hint: existsSync6(RSP_DIR) ? "Run: rsp update" : "Run: rsp init"
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
if (obsoleteExists) {
|
|
1206
|
+
checks.push({
|
|
1207
|
+
status: "issue",
|
|
1208
|
+
label: "obsolete fallback protocol path detected",
|
|
1209
|
+
message: "obsolete fallback protocol path detected",
|
|
1210
|
+
hint: "Run: rsp update"
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
async function reportUnsupportedRules(checks, reportRuntime) {
|
|
1215
|
+
const inspection = await inspectUnsupportedRules();
|
|
1216
|
+
for (const diagnostic of inspection.diagnostics)
|
|
1217
|
+
reportRuntime(diagnostic);
|
|
1218
|
+
if (inspection.diagnostics.length > 0) {
|
|
1219
|
+
checks.push({
|
|
1220
|
+
status: "issue",
|
|
1221
|
+
label: "unable to inspect .rsp/rules/",
|
|
1222
|
+
message: "unable to inspect .rsp/rules/; migration status is unknown",
|
|
1223
|
+
hint: "Fix directory access, then run: rsp doctor"
|
|
1224
|
+
});
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
if (!inspection.directoryRemaining)
|
|
1228
|
+
return;
|
|
1229
|
+
const entries = inspection.entries.map((entry) => entry.path);
|
|
1230
|
+
checks.push({
|
|
1231
|
+
status: "issue",
|
|
1232
|
+
label: "unsupported .rsp/rules/ entries",
|
|
1233
|
+
message: entries.length > 0 ? `unsupported .rsp/rules/ entries: ${entries.join(", ")}` : "unsupported empty .rsp/rules/ directory remains",
|
|
1234
|
+
hint: "Move stable scoped instructions to the nearest project-owned AGENTS.md, remove obsolete entries, then run rsp update"
|
|
1235
|
+
});
|
|
1586
1236
|
}
|
|
1587
1237
|
function reportCheck(checks, label, ok, fixHint) {
|
|
1588
1238
|
if (ok) {
|
|
@@ -1607,13 +1257,23 @@ function printDoctorCheck(check) {
|
|
|
1607
1257
|
console.log(` ${pc.dim(check.hint)}`);
|
|
1608
1258
|
}
|
|
1609
1259
|
async function checkAgents(checks, reportRuntime) {
|
|
1610
|
-
|
|
1260
|
+
const inspection = inspectManagedFile("AGENTS.md", "AGENTS.md", { allowMissing: true });
|
|
1261
|
+
if (inspection.issue) {
|
|
1262
|
+
checks.push({
|
|
1263
|
+
status: "issue",
|
|
1264
|
+
label: "AGENTS.md is a regular managed file",
|
|
1265
|
+
message: inspection.issue.message,
|
|
1266
|
+
hint: "Replace the unsupported entry with a project-local regular file, then run: rsp update"
|
|
1267
|
+
});
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
if (!inspection.exists) {
|
|
1611
1271
|
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing", hint: "Run: rsp init" });
|
|
1612
1272
|
return;
|
|
1613
1273
|
}
|
|
1614
1274
|
let agents;
|
|
1615
1275
|
try {
|
|
1616
|
-
agents = await
|
|
1276
|
+
agents = await readFile7("AGENTS.md", "utf-8");
|
|
1617
1277
|
} catch (error) {
|
|
1618
1278
|
reportRuntime({
|
|
1619
1279
|
code: "agents_read_failed",
|
|
@@ -1631,11 +1291,16 @@ async function checkAgents(checks, reportRuntime) {
|
|
|
1631
1291
|
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing managed RSP block", hint: "Run: rsp init" });
|
|
1632
1292
|
}
|
|
1633
1293
|
async function checkGeneratedIndex(checks, reportRuntime, path, expectedIndexType, label, fixHint) {
|
|
1634
|
-
|
|
1294
|
+
const indexFile = inspectManagedFile(path, label, { allowMissing: true });
|
|
1295
|
+
if (indexFile.issue) {
|
|
1296
|
+
checks.push({ status: "issue", label, message: indexFile.issue.message, hint: fixHint });
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
if (!indexFile.exists)
|
|
1635
1300
|
return;
|
|
1636
1301
|
let content;
|
|
1637
1302
|
try {
|
|
1638
|
-
content = await
|
|
1303
|
+
content = await readFile7(path, "utf-8");
|
|
1639
1304
|
} catch (error) {
|
|
1640
1305
|
reportRuntime({
|
|
1641
1306
|
code: "generated_index_read_failed",
|
|
@@ -1664,10 +1329,20 @@ async function checkGeneratedIndex(checks, reportRuntime, path, expectedIndexTyp
|
|
|
1664
1329
|
}
|
|
1665
1330
|
checks.push({ status: "issue", label, hint: fixHint });
|
|
1666
1331
|
}
|
|
1667
|
-
async function checkArchiveNaming(checks
|
|
1668
|
-
const archivesDir =
|
|
1669
|
-
const
|
|
1670
|
-
|
|
1332
|
+
async function checkArchiveNaming(checks) {
|
|
1333
|
+
const archivesDir = join6(RSP_DIR, "archives");
|
|
1334
|
+
const inspection = await inspectArchiveTree({ archivesDir });
|
|
1335
|
+
if (inspection.diagnostics.length > 0) {
|
|
1336
|
+
checks.push({
|
|
1337
|
+
status: "issue",
|
|
1338
|
+
label: "archives use supported managed paths",
|
|
1339
|
+
message: inspection.diagnostics.map((diagnostic) => diagnostic.message).join("; "),
|
|
1340
|
+
hint: "Keep archives flat or one real group directory deep; remove symlinks and unsupported entries."
|
|
1341
|
+
});
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
checks.push({ status: "ok", label: "archives use supported managed paths" });
|
|
1345
|
+
const invalidFiles = inspection.files.map((fp) => normalizeLogicalPath(relative3(archivesDir, fp))).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(basename4(fp)) === false);
|
|
1671
1346
|
if (invalidFiles.length === 0) {
|
|
1672
1347
|
checks.push({ status: "ok", label: "archived change files follow the date_name.md convention" });
|
|
1673
1348
|
return;
|
|
@@ -1680,69 +1355,172 @@ async function checkArchiveNaming(checks, reportRuntime) {
|
|
|
1680
1355
|
});
|
|
1681
1356
|
}
|
|
1682
1357
|
async function checkConfigSemantics(checks, reportRuntime) {
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
let content;
|
|
1358
|
+
clearConfigCache();
|
|
1359
|
+
let inspection;
|
|
1686
1360
|
try {
|
|
1687
|
-
|
|
1361
|
+
inspection = await inspectRspConfig();
|
|
1688
1362
|
} catch (error) {
|
|
1363
|
+
const message = toErrorMessage(error);
|
|
1689
1364
|
reportRuntime({
|
|
1690
|
-
code: "
|
|
1691
|
-
operation: "
|
|
1692
|
-
path:
|
|
1693
|
-
message
|
|
1365
|
+
code: "invalid_config",
|
|
1366
|
+
operation: "inspectRspConfig",
|
|
1367
|
+
path: ".rsp/config.yaml",
|
|
1368
|
+
message
|
|
1369
|
+
});
|
|
1370
|
+
checks.push({
|
|
1371
|
+
status: "issue",
|
|
1372
|
+
code: "invalid_config",
|
|
1373
|
+
label: "config.yaml semantic checks",
|
|
1374
|
+
message,
|
|
1375
|
+
hint: "Fix the reported config file, YAML, or managed-path issue."
|
|
1376
|
+
});
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
for (const issue of inspection.issues) {
|
|
1380
|
+
checks.push({
|
|
1381
|
+
status: "issue",
|
|
1382
|
+
code: "invalid_config",
|
|
1383
|
+
label: "config.yaml semantic checks",
|
|
1384
|
+
message: issue,
|
|
1385
|
+
hint: issue.includes("decisions") ? "Use .rsp/specs/decisions or one project-relative path outside .rsp/." : "Remove the unsupported field or provide the documented value type."
|
|
1694
1386
|
});
|
|
1695
|
-
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" });
|
|
1696
|
-
return;
|
|
1697
1387
|
}
|
|
1698
|
-
|
|
1388
|
+
if (inspection.issues.length === 0 && inspection.config.kinds !== void 0)
|
|
1389
|
+
checks.push({ status: "ok", label: 'config.yaml field "kinds" has valid list semantics' });
|
|
1390
|
+
return inspection.issues.length === 0;
|
|
1391
|
+
}
|
|
1392
|
+
async function checkDecisionRecordsDirectory(checks, reportRuntime) {
|
|
1393
|
+
const decisionRecordsPath = resolveDecisionRecordsPath(await loadRspConfig());
|
|
1699
1394
|
try {
|
|
1700
|
-
|
|
1395
|
+
const filesystemError = await validateDecisionRecordsFilesystemPath(decisionRecordsPath);
|
|
1396
|
+
if (filesystemError) {
|
|
1397
|
+
checks.push({
|
|
1398
|
+
status: "issue",
|
|
1399
|
+
label: `${decisionRecordsPath} is the authoritative Decision Record directory`,
|
|
1400
|
+
message: filesystemError,
|
|
1401
|
+
hint: "Choose a project-relative directory that does not escape through a symlink."
|
|
1402
|
+
});
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
const info = await stat(decisionRecordsPath);
|
|
1406
|
+
if (info.isDirectory())
|
|
1407
|
+
await readdir(decisionRecordsPath);
|
|
1408
|
+
reportCheck(
|
|
1409
|
+
checks,
|
|
1410
|
+
`${decisionRecordsPath} is the authoritative Decision Record directory`,
|
|
1411
|
+
info.isDirectory(),
|
|
1412
|
+
"Run: rsp update"
|
|
1413
|
+
);
|
|
1701
1414
|
} catch (error) {
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1415
|
+
if (error.code !== "ENOENT") {
|
|
1416
|
+
reportRuntime({
|
|
1417
|
+
code: "decision_records_path_stat_failed",
|
|
1418
|
+
operation: "stat",
|
|
1419
|
+
path: decisionRecordsPath,
|
|
1420
|
+
message: toErrorMessage(error)
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1708
1423
|
checks.push({
|
|
1709
1424
|
status: "issue",
|
|
1710
|
-
label:
|
|
1711
|
-
message:
|
|
1712
|
-
hint:
|
|
1425
|
+
label: `${decisionRecordsPath} is the authoritative Decision Record directory`,
|
|
1426
|
+
message: `${decisionRecordsPath} Decision Record directory is missing or unreadable`,
|
|
1427
|
+
hint: "Run: rsp update"
|
|
1713
1428
|
});
|
|
1714
1429
|
}
|
|
1715
1430
|
}
|
|
1716
|
-
function
|
|
1717
|
-
|
|
1431
|
+
async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
|
|
1432
|
+
const decisionRecordsPath = resolveDecisionRecordsPath(await loadRspConfig());
|
|
1433
|
+
if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !existsSync6(DEFAULT_DECISION_RECORDS_PATH))
|
|
1718
1434
|
return;
|
|
1719
|
-
|
|
1720
|
-
|
|
1435
|
+
let inspectionFailed = false;
|
|
1436
|
+
const inactiveRecords = (await walkMarkdownFiles(DEFAULT_DECISION_RECORDS_PATH, {
|
|
1437
|
+
onError: (diagnostic) => {
|
|
1438
|
+
inspectionFailed = true;
|
|
1439
|
+
reportRuntime(diagnostic);
|
|
1440
|
+
}
|
|
1441
|
+
})).map((path) => normalizeLogicalPath(relative3(DEFAULT_DECISION_RECORDS_PATH, path))).sort();
|
|
1442
|
+
if (inspectionFailed) {
|
|
1721
1443
|
checks.push({
|
|
1722
1444
|
status: "issue",
|
|
1723
|
-
label: "
|
|
1724
|
-
message: `
|
|
1725
|
-
hint:
|
|
1445
|
+
label: "inactive default Decision Records are inspectable",
|
|
1446
|
+
message: `unable to inspect inactive default Decision Records under ${DEFAULT_DECISION_RECORDS_PATH}`,
|
|
1447
|
+
hint: "Restore directory access, then run rsp doctor again."
|
|
1726
1448
|
});
|
|
1727
1449
|
return;
|
|
1728
1450
|
}
|
|
1729
|
-
|
|
1730
|
-
const duplicates = [...new Set(items.filter((item, index) => items.indexOf(item) !== index))];
|
|
1731
|
-
if (duplicates.length === 0) {
|
|
1732
|
-
checks.push({ status: "ok", label: `config.yaml field "${key}" has valid list semantics` });
|
|
1451
|
+
if (inactiveRecords.length === 0)
|
|
1733
1452
|
return;
|
|
1734
|
-
}
|
|
1735
1453
|
checks.push({
|
|
1736
1454
|
status: "issue",
|
|
1737
|
-
label: "
|
|
1738
|
-
message: `
|
|
1739
|
-
hint: `
|
|
1455
|
+
label: "inactive default Decision Records are migrated",
|
|
1456
|
+
message: `inactive default Decision Records: ${inactiveRecords.join(", ")}`,
|
|
1457
|
+
hint: `Move lasting rationale to ${decisionRecordsPath}, then remove the inactive files from ${DEFAULT_DECISION_RECORDS_PATH}.`
|
|
1740
1458
|
});
|
|
1741
1459
|
}
|
|
1742
|
-
async function checkActiveChangeConsistency(checks
|
|
1743
|
-
const
|
|
1744
|
-
const
|
|
1745
|
-
const
|
|
1460
|
+
async function checkActiveChangeConsistency(checks) {
|
|
1461
|
+
const workTree = await inspectWorkTree();
|
|
1462
|
+
const focusTree = await inspectFocusTree();
|
|
1463
|
+
const groupInspection = await inspectChangeGroups({ workTree, focusTree });
|
|
1464
|
+
const dependencyInspection = await inspectChangeDependencies({ workTree });
|
|
1465
|
+
const changeNames = new Set(workTree.changes.map((ref) => ref.name));
|
|
1466
|
+
const structureIssues = workTree.diagnostics.map((diagnostic) => diagnostic.message);
|
|
1467
|
+
structureIssues.push(...focusTree.diagnostics.map((diagnostic) => diagnostic.message));
|
|
1468
|
+
const rawFocusNames = new Set(focusTree.markers.map((marker) => marker.name));
|
|
1469
|
+
const focusNames = /* @__PURE__ */ new Set();
|
|
1470
|
+
for (const name of rawFocusNames) {
|
|
1471
|
+
try {
|
|
1472
|
+
focusNames.add(resolveWorkRef(name, { executable: true }).name);
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
if (!(error instanceof WorkRefError))
|
|
1475
|
+
throw error;
|
|
1476
|
+
structureIssues.push(`focus.d/${name}: ${error.message}`);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
const uniqueStructureIssues = [...new Set(structureIssues)];
|
|
1480
|
+
if (uniqueStructureIssues.length === 0) {
|
|
1481
|
+
checks.push({ status: "ok", label: "open work uses supported WorkRef shapes" });
|
|
1482
|
+
} else {
|
|
1483
|
+
checks.push({
|
|
1484
|
+
status: "issue",
|
|
1485
|
+
label: "open work uses supported WorkRef shapes",
|
|
1486
|
+
message: uniqueStructureIssues.join("; "),
|
|
1487
|
+
hint: "Keep work flat or use one group directory with direct Change files. Use rsp unfocus <group>/brief to clear an accidental Group Brief marker."
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
const groupStructureIssues = workTree.diagnostics.filter((diagnostic) => diagnostic.code === "group_brief_missing" || diagnostic.path.endsWith(`/${GROUP_BRIEF_FILENAME}`)).map((diagnostic) => diagnostic.message);
|
|
1491
|
+
const groupIssues = [
|
|
1492
|
+
...groupInspection.diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message),
|
|
1493
|
+
...groupStructureIssues
|
|
1494
|
+
];
|
|
1495
|
+
const groupWarnings = groupInspection.diagnostics.filter((diagnostic) => diagnostic.severity === "warning");
|
|
1496
|
+
if (groupIssues.length === 0) {
|
|
1497
|
+
checks.push({ status: "ok", label: "Change Group contracts are valid" });
|
|
1498
|
+
} else {
|
|
1499
|
+
checks.push({
|
|
1500
|
+
status: "issue",
|
|
1501
|
+
label: "Change Group contracts are valid",
|
|
1502
|
+
message: [...new Set(groupIssues)].join("; "),
|
|
1503
|
+
hint: `Keep one ${GROUP_BRIEF_FILENAME} per group and make its Slices list match direct open or archived child Changes.`
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
if (groupWarnings.length > 0) {
|
|
1507
|
+
checks.push({
|
|
1508
|
+
status: "info",
|
|
1509
|
+
label: "Change Group completion guidance",
|
|
1510
|
+
message: [...new Set(groupWarnings.map((diagnostic) => diagnostic.message))].join("; ")
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
const dependencyIssues = dependencyInspection.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1514
|
+
if (dependencyIssues.length === 0) {
|
|
1515
|
+
checks.push({ status: "ok", label: "Change dependency graph is valid" });
|
|
1516
|
+
} else {
|
|
1517
|
+
checks.push({
|
|
1518
|
+
status: "issue",
|
|
1519
|
+
label: "Change dependency graph is valid",
|
|
1520
|
+
message: [...new Set(dependencyIssues.map((diagnostic) => diagnostic.message))].join("; "),
|
|
1521
|
+
hint: "Use exact existing Change WorkRefs, remove self-dependencies, and break dependency cycles."
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1746
1524
|
const missingChangeFiles = [...focusNames].filter((name) => !changeNames.has(name));
|
|
1747
1525
|
const unfocusedChangeFiles = [...changeNames].filter((name) => !focusNames.has(name));
|
|
1748
1526
|
if (missingChangeFiles.length === 0) {
|
|
@@ -1770,119 +1548,454 @@ async function checkActiveChangeConsistency(checks, reportRuntime) {
|
|
|
1770
1548
|
}
|
|
1771
1549
|
|
|
1772
1550
|
// src/commands/focus.ts
|
|
1773
|
-
import { existsSync as
|
|
1774
|
-
import { mkdir as
|
|
1775
|
-
import { dirname as
|
|
1551
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1552
|
+
import { mkdir as mkdir6, unlink as unlink5 } from "fs/promises";
|
|
1553
|
+
import { dirname as dirname3 } from "path";
|
|
1776
1554
|
async function focusChange(name) {
|
|
1777
1555
|
if (!name) {
|
|
1778
1556
|
console.error(` ${pc.red("Usage:")} rsp focus <name>`);
|
|
1779
1557
|
process.exit(1);
|
|
1780
1558
|
}
|
|
1781
|
-
if (!isValidChangeName(name)) {
|
|
1782
|
-
console.error(` ${pc.red("Error:")} change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
1783
|
-
process.exit(1);
|
|
1784
|
-
}
|
|
1785
1559
|
guardRspInitialized();
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1560
|
+
try {
|
|
1561
|
+
return await withRspLock("focus-change", async () => {
|
|
1562
|
+
const workRef = await resolveExecutableChange(name, { mustExist: true });
|
|
1563
|
+
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
1564
|
+
await mkdir6(FOCUS_DIR, { recursive: true });
|
|
1565
|
+
await mkdir6(dirname3(focusEntry), { recursive: true });
|
|
1566
|
+
await writeManagedFile(focusEntry, "", "focus marker");
|
|
1567
|
+
console.log(` ${pc.green("Focused:")} ${name}`);
|
|
1568
|
+
console.log(` ${pc.dim("focus.d")} \u2192 ${name}`);
|
|
1569
|
+
console.log();
|
|
1570
|
+
});
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
exitWorkRefError(error);
|
|
1790
1573
|
}
|
|
1791
|
-
return withRspLock("focus-change", async () => {
|
|
1792
|
-
await mkdir7(FOCUS_DIR, { recursive: true });
|
|
1793
|
-
const focusEntry = join12(FOCUS_DIR, name);
|
|
1794
|
-
await mkdir7(dirname8(focusEntry), { recursive: true });
|
|
1795
|
-
await writeFile8(focusEntry, "");
|
|
1796
|
-
console.log(` ${pc.green("Focused:")} ${name}`);
|
|
1797
|
-
console.log(` ${pc.dim("focus.d")} \u2192 ${name}`);
|
|
1798
|
-
console.log();
|
|
1799
|
-
});
|
|
1800
1574
|
}
|
|
1801
1575
|
async function unfocusChange(name) {
|
|
1802
1576
|
if (!name) {
|
|
1803
1577
|
console.error(` ${pc.red("Usage:")} rsp unfocus <name>`);
|
|
1804
1578
|
process.exit(1);
|
|
1805
1579
|
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1580
|
+
guardRspInitialized();
|
|
1581
|
+
try {
|
|
1582
|
+
return await withRspLock("unfocus-change", async () => {
|
|
1583
|
+
const workRef = resolveWorkRef(name, { executable: false, mustExist: false });
|
|
1584
|
+
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
1585
|
+
if (!existsSync7(focusEntry))
|
|
1586
|
+
throw new WorkRefError("focus_marker_not_found", `.rsp/focus.d/${name}`, name);
|
|
1587
|
+
await unlink5(focusEntry);
|
|
1588
|
+
await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
|
|
1589
|
+
console.log(` ${pc.green("Unfocused:")} ${name}`);
|
|
1590
|
+
console.log(` ${pc.dim("focus.d cleared")} \u2192 ${name}`);
|
|
1591
|
+
console.log();
|
|
1592
|
+
});
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
exitWorkRefError(error);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
function exitWorkRefError(error) {
|
|
1598
|
+
if (error instanceof WorkRefError) {
|
|
1599
|
+
if (error.code === "focus_marker_not_found")
|
|
1600
|
+
console.error(` ${pc.red("Focus marker not found:")} ${error.message}`);
|
|
1601
|
+
else
|
|
1602
|
+
console.error(` ${pc.red("Error:")} ${error.message}`);
|
|
1603
|
+
process.exit(1);
|
|
1604
|
+
}
|
|
1605
|
+
throw error;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// src/commands/group.ts
|
|
1609
|
+
import { mkdir as mkdir7, rename as rename2, writeFile as writeFile4 } from "fs/promises";
|
|
1610
|
+
import { join as join7 } from "path";
|
|
1611
|
+
var ChangeGroupError = class extends Error {
|
|
1612
|
+
name = "ChangeGroupError";
|
|
1613
|
+
};
|
|
1614
|
+
async function createChangeGroup(name, goal = "") {
|
|
1615
|
+
if (!name) {
|
|
1616
|
+
console.error(` ${pc.red("Usage:")} rsp group create <name> [goal]`);
|
|
1808
1617
|
process.exit(1);
|
|
1809
1618
|
}
|
|
1810
1619
|
guardRspInitialized();
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1620
|
+
try {
|
|
1621
|
+
await withRspLock("create-change-group", async () => {
|
|
1622
|
+
const ref = resolveWorkRef(`${name}/brief`);
|
|
1623
|
+
if (ref.kind !== "group-brief")
|
|
1624
|
+
throw new WorkRefError("invalid_work_ref", `invalid Change Group name: ${name}`, name);
|
|
1625
|
+
if (await hasArchivedGroupBrief(name))
|
|
1626
|
+
throw new ChangeGroupError(`archived Change Group cannot be reopened: ${name}`);
|
|
1627
|
+
const groupDirectory = resolveManagedDirectoryChain(CHANGES_DIR, [name], "Change Group path");
|
|
1628
|
+
await mkdir7(groupDirectory, { recursive: true });
|
|
1629
|
+
try {
|
|
1630
|
+
await writeFile4(ref.path, generateGroupBriefContent(name, goal), { flag: "wx" });
|
|
1631
|
+
} catch (error) {
|
|
1632
|
+
if (error.code === "EEXIST")
|
|
1633
|
+
throw new ChangeGroupError(`Change Group already exists: ${name}`);
|
|
1634
|
+
throw error;
|
|
1635
|
+
}
|
|
1636
|
+
console.log(` ${pc.green("Created Change Group:")} ${name}`);
|
|
1637
|
+
console.log(` ${pc.dim("Unfocused Group Brief:")} ${ref.path}`);
|
|
1638
|
+
console.log(` ${pc.cyan("Next:")} fill the brief, then create direct child Changes with rsp create ${name}/<change>
|
|
1639
|
+
`);
|
|
1640
|
+
});
|
|
1641
|
+
} catch (error) {
|
|
1642
|
+
if (error instanceof WorkRefError || error instanceof ChangeGroupError) {
|
|
1643
|
+
console.error(` ${pc.red("Error:")} ${error.message}`);
|
|
1644
|
+
process.exit(1);
|
|
1645
|
+
}
|
|
1646
|
+
throw error;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
async function closeChangeGroup(name) {
|
|
1650
|
+
if (!name) {
|
|
1651
|
+
console.error(` ${pc.red("Usage:")} rsp group close <name>`);
|
|
1814
1652
|
process.exit(1);
|
|
1815
1653
|
}
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
await
|
|
1819
|
-
|
|
1820
|
-
|
|
1654
|
+
guardRspInitialized();
|
|
1655
|
+
try {
|
|
1656
|
+
await withRspLock("close-change-group", async () => {
|
|
1657
|
+
const ref = resolveWorkRef(`${name}/brief`, { mustExist: true });
|
|
1658
|
+
if (ref.kind !== "group-brief")
|
|
1659
|
+
throw new ChangeGroupError(`invalid Change Group name: ${name}`);
|
|
1660
|
+
const inspection = await inspectChangeGroups();
|
|
1661
|
+
const group = inspection.groups.find((candidate) => candidate.name === name);
|
|
1662
|
+
if (!group)
|
|
1663
|
+
throw new ChangeGroupError(`Change Group not found: ${name}`);
|
|
1664
|
+
const reasons = collectCloseReasons(group, inspection.diagnostics.filter((diagnostic) => diagnostic.change === name && diagnostic.severity === "error").map((diagnostic) => diagnostic.message));
|
|
1665
|
+
if (reasons.length > 0)
|
|
1666
|
+
throw new ChangeGroupError(`Change Group "${name}" is not ready to close: ${reasons.join("; ")}`);
|
|
1667
|
+
const archiveDirectory = resolveArchiveDirectory(ref);
|
|
1668
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1669
|
+
const archiveName = resolveArchiveName(archiveDirectory, date, "brief");
|
|
1670
|
+
await mkdir7(archiveDirectory, { recursive: true });
|
|
1671
|
+
await rename2(ref.path, join7(archiveDirectory, archiveName));
|
|
1672
|
+
const warnings = [];
|
|
1673
|
+
try {
|
|
1674
|
+
await cleanupEmptyParentDirs(ref.path, CHANGES_DIR);
|
|
1675
|
+
} catch (error) {
|
|
1676
|
+
warnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
|
|
1677
|
+
}
|
|
1678
|
+
try {
|
|
1679
|
+
await buildArchiveIndex({ acquireLock: false });
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
warnings.push(`archive index rebuild failed: ${toErrorMessage(error)}`);
|
|
1682
|
+
}
|
|
1683
|
+
console.log(` ${pc.green("Closed Change Group:")} ${name}`);
|
|
1684
|
+
console.log(` ${pc.dim("Archived Group Brief:")} ${join7(archiveDirectory, archiveName)}
|
|
1685
|
+
`);
|
|
1686
|
+
for (const warning of warnings)
|
|
1687
|
+
console.log(` ${pc.yellow("\u26A0")} ${warning}`);
|
|
1688
|
+
if (warnings.length > 0)
|
|
1689
|
+
console.log(` ${pc.yellow("Group close completed, but follow-up cleanup was only partially successful.")}
|
|
1690
|
+
`);
|
|
1691
|
+
});
|
|
1692
|
+
} catch (error) {
|
|
1693
|
+
if (error instanceof WorkRefError || error instanceof ChangeGroupError) {
|
|
1694
|
+
console.error(` ${pc.red("Error:")} ${error.message}`);
|
|
1695
|
+
process.exit(1);
|
|
1696
|
+
}
|
|
1697
|
+
throw error;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
function collectCloseReasons(group, errors) {
|
|
1701
|
+
const reasons = [...errors, ...group.warnings];
|
|
1702
|
+
const open = group.slices.filter((slice) => slice.state === "open").map((slice) => slice.name);
|
|
1703
|
+
const missing = group.slices.filter((slice) => slice.state === "missing").map((slice) => slice.name);
|
|
1704
|
+
if (open.length > 0)
|
|
1705
|
+
reasons.push(`open slices: ${open.join(", ")}`);
|
|
1706
|
+
if (missing.length > 0)
|
|
1707
|
+
reasons.push(`missing slices: ${missing.join(", ")}`);
|
|
1708
|
+
if (group.completion.total === 0)
|
|
1709
|
+
reasons.push("no completion conditions are declared");
|
|
1710
|
+
else if (group.completion.done !== group.completion.total)
|
|
1711
|
+
reasons.push(`completion conditions incomplete: ${group.completion.done}/${group.completion.total}`);
|
|
1712
|
+
if (group.blockers)
|
|
1713
|
+
reasons.push("active group blockers remain");
|
|
1714
|
+
return [...new Set(reasons)];
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// src/commands/history.ts
|
|
1718
|
+
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1719
|
+
var GROUP_RE = /^[a-z0-9-]+$/;
|
|
1720
|
+
var WORK_REF_RE = /^[a-z0-9-]+(?:\/[a-z0-9-]+)?$/;
|
|
1721
|
+
async function showHistory(input = {}, options = {}) {
|
|
1722
|
+
guardRspInitialized();
|
|
1723
|
+
const mode = input.workRef ? "detail" : "list";
|
|
1724
|
+
const validation = validateHistoryQuery(input);
|
|
1725
|
+
if (!validation.ok)
|
|
1726
|
+
return emitHistoryError(mode, input, validation.error, [], emptyCollectionSummary(), [], options);
|
|
1727
|
+
const inspection = await inspectArchiveHistory();
|
|
1728
|
+
if (!historyInspectionComplete(inspection)) {
|
|
1729
|
+
return emitHistoryError(mode, input, {
|
|
1730
|
+
code: "archive_inspection_incomplete",
|
|
1731
|
+
message: "archive history inspection is incomplete; resolve the reported diagnostics before querying history"
|
|
1732
|
+
}, inspection.diagnostics, inspection.diagnosticSummary, inspection.runtime, options);
|
|
1733
|
+
}
|
|
1734
|
+
if (input.workRef) {
|
|
1735
|
+
try {
|
|
1736
|
+
const selected = selectArchiveHistoryRecord(inspection.records, { workRef: input.workRef });
|
|
1737
|
+
const record = await readArchiveHistoryDetail(selected);
|
|
1738
|
+
const result2 = {
|
|
1739
|
+
command: "history",
|
|
1740
|
+
ok: true,
|
|
1741
|
+
mode: "detail",
|
|
1742
|
+
query: { workRef: input.workRef },
|
|
1743
|
+
record,
|
|
1744
|
+
diagnostics: [],
|
|
1745
|
+
diagnosticSummary: emptyCollectionSummary(),
|
|
1746
|
+
runtime: inspection.runtime
|
|
1747
|
+
};
|
|
1748
|
+
if (options.json)
|
|
1749
|
+
emitJson(result2, options);
|
|
1750
|
+
else
|
|
1751
|
+
printHistoryDetail(result2);
|
|
1752
|
+
return result2;
|
|
1753
|
+
} catch (error) {
|
|
1754
|
+
if (error instanceof ArchiveHistoryError) {
|
|
1755
|
+
return emitHistoryError(mode, input, {
|
|
1756
|
+
code: error.code,
|
|
1757
|
+
message: error.message,
|
|
1758
|
+
candidates: error.candidates.length > 0 ? error.candidates : void 0,
|
|
1759
|
+
candidateSummary: error.candidateTotal > 0 ? { total: error.candidateTotal, returned: error.candidates.length, hasMore: error.candidatesTruncated } : void 0
|
|
1760
|
+
}, [], emptyCollectionSummary(), inspection.runtime, options);
|
|
1761
|
+
}
|
|
1762
|
+
throw error;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
const query = validation.query;
|
|
1766
|
+
const listed = queryArchiveHistory(inspection.records, query);
|
|
1767
|
+
const result = {
|
|
1768
|
+
command: "history",
|
|
1769
|
+
ok: true,
|
|
1770
|
+
mode: "list",
|
|
1771
|
+
query: {
|
|
1772
|
+
limit: query.limit ?? HISTORY_DEFAULT_LIMIT,
|
|
1773
|
+
since: query.since ?? null,
|
|
1774
|
+
until: query.until ?? null,
|
|
1775
|
+
kind: query.kind ?? null,
|
|
1776
|
+
group: query.group ?? null
|
|
1777
|
+
},
|
|
1778
|
+
records: listed.records,
|
|
1779
|
+
summary: listed.summary,
|
|
1780
|
+
diagnostics: [],
|
|
1781
|
+
diagnosticSummary: emptyCollectionSummary(),
|
|
1782
|
+
runtime: inspection.runtime
|
|
1783
|
+
};
|
|
1784
|
+
if (options.json)
|
|
1785
|
+
emitJson(result, options);
|
|
1786
|
+
else
|
|
1787
|
+
printHistoryList(result);
|
|
1788
|
+
return result;
|
|
1789
|
+
}
|
|
1790
|
+
function validateHistoryQuery(input) {
|
|
1791
|
+
if ((input.positionalCount ?? (input.workRef ? 1 : 0)) > 1)
|
|
1792
|
+
return invalid("history_positional_args_unsupported", "history accepts at most one positional WorkRef");
|
|
1793
|
+
if (input.workRef) {
|
|
1794
|
+
if (!WORK_REF_RE.test(input.workRef) || input.workRef.endsWith("/brief") || input.workRef.endsWith("/00-brief"))
|
|
1795
|
+
return invalid("invalid_history_work_ref", "history WorkRef must be a flat or one-Group-level executable Change identity");
|
|
1796
|
+
if (input.limit !== void 0 || input.since !== void 0 || input.until !== void 0 || input.kind !== void 0 || input.group !== void 0)
|
|
1797
|
+
return invalid("history_detail_filters_unsupported", "list filters cannot be combined with an exact history detail lookup");
|
|
1798
|
+
return { ok: true, query: {} };
|
|
1799
|
+
}
|
|
1800
|
+
const limit = input.limit === void 0 ? HISTORY_DEFAULT_LIMIT : Number(input.limit);
|
|
1801
|
+
if (input.limit !== void 0 && !/^[1-9]\d*$/.test(input.limit) || !Number.isInteger(limit) || limit < 1 || limit > HISTORY_MAX_LIMIT)
|
|
1802
|
+
return invalid("invalid_history_limit", `--limit must be an integer from 1 through ${HISTORY_MAX_LIMIT}`);
|
|
1803
|
+
if (input.since && !isCalendarDate(input.since))
|
|
1804
|
+
return invalid("invalid_history_since", "--since must be a valid YYYY-MM-DD date");
|
|
1805
|
+
if (input.until && !isCalendarDate(input.until))
|
|
1806
|
+
return invalid("invalid_history_until", "--until must be a valid YYYY-MM-DD date");
|
|
1807
|
+
if (input.since && input.until && input.since > input.until)
|
|
1808
|
+
return invalid("invalid_history_date_range", "--since must be on or before --until");
|
|
1809
|
+
if (input.kind !== void 0 && input.kind.trim() === "")
|
|
1810
|
+
return invalid("invalid_history_kind", "--kind must be a non-empty exact historical kind");
|
|
1811
|
+
if (input.group !== void 0 && !GROUP_RE.test(input.group))
|
|
1812
|
+
return invalid("invalid_history_group", "--group must be one lowercase kebab-case Group name");
|
|
1813
|
+
return {
|
|
1814
|
+
ok: true,
|
|
1815
|
+
query: {
|
|
1816
|
+
limit,
|
|
1817
|
+
since: input.since,
|
|
1818
|
+
until: input.until,
|
|
1819
|
+
kind: input.kind?.trim(),
|
|
1820
|
+
group: input.group
|
|
1821
|
+
}
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
function invalid(code, message) {
|
|
1825
|
+
return { ok: false, error: { code, message } };
|
|
1826
|
+
}
|
|
1827
|
+
function isCalendarDate(value) {
|
|
1828
|
+
if (!DATE_RE.test(value))
|
|
1829
|
+
return false;
|
|
1830
|
+
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
1831
|
+
return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
|
|
1832
|
+
}
|
|
1833
|
+
function emitHistoryError(mode, query, error, diagnostics, diagnosticSummary, runtime, options) {
|
|
1834
|
+
const result = {
|
|
1835
|
+
command: "history",
|
|
1836
|
+
ok: false,
|
|
1837
|
+
mode,
|
|
1838
|
+
query,
|
|
1839
|
+
records: [],
|
|
1840
|
+
record: null,
|
|
1841
|
+
diagnostics,
|
|
1842
|
+
diagnosticSummary,
|
|
1843
|
+
runtime,
|
|
1844
|
+
error: {
|
|
1845
|
+
code: error.code,
|
|
1846
|
+
message: error.message,
|
|
1847
|
+
...error.candidates && { candidates: error.candidates },
|
|
1848
|
+
...error.candidateSummary && { candidateSummary: error.candidateSummary }
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
if (options.json) {
|
|
1852
|
+
emitJson(result, options);
|
|
1853
|
+
} else {
|
|
1854
|
+
console.error(` ${pc.red("Error:")} ${error.message}`);
|
|
1855
|
+
for (const diagnostic of diagnostics)
|
|
1856
|
+
console.error(` ${pc.dim(`${diagnostic.path ?? "archive"} \u2014 ${diagnostic.message}`)}`);
|
|
1857
|
+
if (diagnosticSummary.hasMore)
|
|
1858
|
+
console.error(` ${pc.dim(`${diagnosticSummary.total - diagnosticSummary.returned} additional diagnostic(s) omitted`)}`);
|
|
1859
|
+
for (const candidate of error.candidates ?? [])
|
|
1860
|
+
console.error(` ${pc.dim(candidate)}`);
|
|
1861
|
+
if (error.candidateSummary?.hasMore)
|
|
1862
|
+
console.error(` ${pc.dim(`${error.candidateSummary.total - error.candidateSummary.returned} additional candidate(s) omitted`)}`);
|
|
1863
|
+
}
|
|
1864
|
+
return result;
|
|
1865
|
+
}
|
|
1866
|
+
function emptyCollectionSummary() {
|
|
1867
|
+
return { total: 0, returned: 0, hasMore: false };
|
|
1868
|
+
}
|
|
1869
|
+
function printHistoryList(result) {
|
|
1870
|
+
console.log();
|
|
1871
|
+
console.log(` ${pc.bold("Archived Changes")}`);
|
|
1872
|
+
console.log();
|
|
1873
|
+
if (result.records.length === 0) {
|
|
1874
|
+
console.log(` ${pc.dim("No archived Changes match the query.")}
|
|
1875
|
+
`);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
for (const record of result.records) {
|
|
1879
|
+
console.log(` ${pc.dim(record.date)} ${pc.cyan(record.workRef)} ${pc.dim(`[${record.kind}]`)}`);
|
|
1880
|
+
console.log(` ${record.summary}${record.summaryTruncated ? "\u2026" : ""}`);
|
|
1881
|
+
console.log(` ${pc.dim(record.path)}`);
|
|
1882
|
+
}
|
|
1883
|
+
console.log();
|
|
1884
|
+
console.log(` ${pc.dim(`${result.summary.returned}/${result.summary.matched} matching archive(s) returned${result.summary.hasMore ? "; narrow filters or raise --limit for more" : ""}.`)}
|
|
1885
|
+
`);
|
|
1886
|
+
}
|
|
1887
|
+
function printHistoryDetail(result) {
|
|
1888
|
+
const record = result.record;
|
|
1889
|
+
console.log();
|
|
1890
|
+
console.log(` ${pc.bold("Archived Change:")} ${pc.cyan(record.workRef)}`);
|
|
1891
|
+
console.log(` ${pc.dim("Date:")} ${record.date}`);
|
|
1892
|
+
console.log(` ${pc.dim("Kind:")} ${record.kind}`);
|
|
1893
|
+
console.log(` ${pc.dim("Path:")} ${record.path}`);
|
|
1894
|
+
console.log(` ${pc.dim("Summary:")} ${record.summary}${record.summaryTruncated ? "\u2026" : ""}`);
|
|
1895
|
+
console.log(` ${pc.dim("Scenarios:")} ${record.scenarioCount}`);
|
|
1896
|
+
console.log(` ${pc.dim("Tasks:")} ${record.checkboxes.tasks.done}/${record.checkboxes.tasks.total}`);
|
|
1897
|
+
console.log(` ${pc.dim("Verify:")} ${record.checkboxes.verify.done}/${record.checkboxes.verify.total}`);
|
|
1898
|
+
for (const [label, evidence] of Object.entries(record.evidence)) {
|
|
1821
1899
|
console.log();
|
|
1822
|
-
});
|
|
1900
|
+
console.log(` ${pc.bold(`${label[0].toUpperCase()}${label.slice(1)}:`)}`);
|
|
1901
|
+
if (evidence.items.length === 0)
|
|
1902
|
+
console.log(` ${pc.dim("none")}`);
|
|
1903
|
+
else
|
|
1904
|
+
evidence.items.forEach((item) => console.log(` ${item}`));
|
|
1905
|
+
if (evidence.truncated)
|
|
1906
|
+
console.log(` ${pc.dim("(truncated)")}`);
|
|
1907
|
+
}
|
|
1908
|
+
console.log();
|
|
1823
1909
|
}
|
|
1824
1910
|
|
|
1825
1911
|
// src/commands/init.ts
|
|
1826
|
-
import { existsSync as
|
|
1827
|
-
import { mkdir as mkdir8, readFile as
|
|
1828
|
-
import { join as
|
|
1912
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1913
|
+
import { mkdir as mkdir8, readFile as readFile8 } from "fs/promises";
|
|
1914
|
+
import { join as join8 } from "path";
|
|
1829
1915
|
function generateConfigTemplate() {
|
|
1830
1916
|
const fmtList = (items, indent) => items.map((i) => `${" ".repeat(indent)}# - ${i}`).join("\n");
|
|
1831
1917
|
return `# RSP project configuration
|
|
1832
|
-
#
|
|
1918
|
+
# Omit kinds or use [] to retain the built-in defaults.
|
|
1919
|
+
# A non-empty kinds list replaces the built-in defaults; it does not extend them.
|
|
1920
|
+
# Every entry must be a unique non-empty string.
|
|
1833
1921
|
#
|
|
1834
1922
|
# Built-in defaults:
|
|
1835
1923
|
# kinds: ${VALID_KINDS.join(", ")}
|
|
1836
1924
|
#
|
|
1837
1925
|
# kinds:
|
|
1838
1926
|
${fmtList(VALID_KINDS, 2)}
|
|
1927
|
+
|
|
1928
|
+
# Decision Records default to .rsp/specs/decisions.
|
|
1929
|
+
# Set exactly one project-relative authoritative directory when the Host Project already owns Decision Records elsewhere.
|
|
1930
|
+
# decisions:
|
|
1931
|
+
# path: docs/adr
|
|
1839
1932
|
`;
|
|
1840
1933
|
}
|
|
1841
1934
|
async function ensureFile(path, content) {
|
|
1842
|
-
|
|
1843
|
-
return false;
|
|
1844
|
-
await writeFile9(path, content);
|
|
1845
|
-
return true;
|
|
1935
|
+
return ensureManagedFile(path, content, "managed file");
|
|
1846
1936
|
}
|
|
1847
1937
|
function toTitle(projectName) {
|
|
1848
1938
|
return projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
|
|
1849
1939
|
}
|
|
1850
1940
|
async function initProject(args = {}) {
|
|
1851
|
-
const isNew = !
|
|
1941
|
+
const isNew = !existsSync8(RSP_DIR);
|
|
1852
1942
|
const projectName = await detectProjectName();
|
|
1853
1943
|
const mode = args.agentsMode ?? "managed";
|
|
1854
1944
|
if (mode !== "managed" && mode !== "print") {
|
|
1855
1945
|
console.error(` ${pc.red("Error:")} unsupported --agents-mode "${mode}". Use: managed or print`);
|
|
1856
1946
|
process.exit(1);
|
|
1857
1947
|
}
|
|
1948
|
+
const agentsPath = "AGENTS.md";
|
|
1949
|
+
const initialAgentsInspection = inspectManagedFile(agentsPath, "AGENTS.md", { allowMissing: true });
|
|
1950
|
+
if (initialAgentsInspection.issue)
|
|
1951
|
+
throw initialAgentsInspection.issue;
|
|
1858
1952
|
return withRspLock("init", async () => {
|
|
1953
|
+
const agentsInspection = inspectManagedFile(agentsPath, "AGENTS.md", { allowMissing: true });
|
|
1954
|
+
if (agentsInspection.issue)
|
|
1955
|
+
throw agentsInspection.issue;
|
|
1956
|
+
if (existsSync8(CONFIG_PATH)) {
|
|
1957
|
+
clearConfigCache();
|
|
1958
|
+
const existingConfig = await inspectRspConfig();
|
|
1959
|
+
if (existingConfig.issues.length > 0)
|
|
1960
|
+
throw new Error(existingConfig.issues.join("; "));
|
|
1961
|
+
const existingDecisionRecordsPath = resolveDecisionRecordsPath(existingConfig.config);
|
|
1962
|
+
const filesystemIssue = await validateDecisionRecordsFilesystemPath(existingDecisionRecordsPath);
|
|
1963
|
+
if (filesystemIssue)
|
|
1964
|
+
throw new Error(filesystemIssue);
|
|
1965
|
+
}
|
|
1859
1966
|
const dirs = [
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
join13(RSP_DIR, "archives")
|
|
1967
|
+
{ path: join8(RSP_DIR, "specs"), label: "specs root" },
|
|
1968
|
+
{ path: CHANGES_DIR, label: "open work root" },
|
|
1969
|
+
{ path: FOCUS_DIR, label: "focus root" },
|
|
1970
|
+
{ path: join8(RSP_DIR, "archives"), label: "archive root" }
|
|
1865
1971
|
];
|
|
1866
|
-
for (const
|
|
1867
|
-
|
|
1972
|
+
for (const directory of dirs)
|
|
1973
|
+
requireManagedDirectory(directory.path, directory.label, { allowMissing: true });
|
|
1974
|
+
for (const directory of dirs)
|
|
1975
|
+
await mkdir8(directory.path, { recursive: true });
|
|
1868
1976
|
let created = false;
|
|
1869
|
-
const bundledRules = await
|
|
1870
|
-
created = await ensureFile(
|
|
1871
|
-
created = await ensureFile(
|
|
1872
|
-
|
|
1977
|
+
const bundledRules = await readFile8(join8(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
|
|
1978
|
+
created = await ensureFile(RSP_RULES_PATH, bundledRules) || created;
|
|
1979
|
+
created = await ensureFile(join8(RSP_DIR, "config.yaml"), generateConfigTemplate()) || created;
|
|
1980
|
+
clearConfigCache();
|
|
1981
|
+
const configInspection = await inspectRspConfig();
|
|
1982
|
+
if (configInspection.issues.length > 0)
|
|
1983
|
+
throw new Error(configInspection.issues.join("; "));
|
|
1984
|
+
const decisionRecordsPath = resolveDecisionRecordsPath(configInspection.config);
|
|
1985
|
+
created = await ensureDecisionRecordsDirectory(decisionRecordsPath) || created;
|
|
1986
|
+
const createdSpecsIndex = await ensureFile(join8(RSP_DIR, "specs", "INDEX.md"), "# Specs Index\n\n_Additional project-level specs beyond `design.md`._\n");
|
|
1873
1987
|
created = createdSpecsIndex || created;
|
|
1874
|
-
const createdDesign = await ensureFile(
|
|
1988
|
+
const createdDesign = await ensureFile(join8(RSP_DIR, "specs", "design.md"), generateDesignContent(projectName));
|
|
1875
1989
|
created = createdDesign || created;
|
|
1876
|
-
const createdArchivesIndex = await ensureFile(
|
|
1990
|
+
const createdArchivesIndex = await ensureFile(join8(RSP_DIR, "archives", "INDEX.md"), "# Archive Index\n");
|
|
1877
1991
|
created = createdArchivesIndex || created;
|
|
1878
|
-
created = await ensureFile(
|
|
1879
|
-
created = await ensureFile(
|
|
1880
|
-
created = await ensureFile(
|
|
1881
|
-
if (args.withProjectRules)
|
|
1882
|
-
created = await ensureFile(join13(RSP_DIR, "rules", "project-rules.md"), generateProjectRulesContent(projectName)) || created;
|
|
1992
|
+
created = await ensureFile(join8(CHANGES_DIR, ".gitkeep"), "") || created;
|
|
1993
|
+
created = await ensureFile(join8(FOCUS_DIR, ".gitkeep"), "") || created;
|
|
1994
|
+
created = await ensureFile(join8(RSP_DIR, ".gitignore"), "# Transient files that should not be committed\n.lock\n") || created;
|
|
1883
1995
|
if (args.withProjectSetup) {
|
|
1884
|
-
const
|
|
1885
|
-
const
|
|
1996
|
+
const projectSetupRef = resolveWorkRef("project-setup", { executable: true });
|
|
1997
|
+
const changePath = projectSetupRef.path;
|
|
1998
|
+
const focusMarker = resolveFocusMarkerPath(projectSetupRef);
|
|
1886
1999
|
created = await ensureFile(changePath, generateChangeContent("project-setup")) || created;
|
|
1887
2000
|
created = await ensureFile(focusMarker, "") || created;
|
|
1888
2001
|
}
|
|
@@ -1890,27 +2003,26 @@ async function initProject(args = {}) {
|
|
|
1890
2003
|
await buildSpecsIndex({ acquireLock: false });
|
|
1891
2004
|
if (createdArchivesIndex)
|
|
1892
2005
|
await buildArchiveIndex({ acquireLock: false });
|
|
1893
|
-
const agentsPath = "AGENTS.md";
|
|
1894
2006
|
const title = toTitle(projectName);
|
|
1895
2007
|
let agentsUpdated = false;
|
|
1896
|
-
if (!
|
|
1897
|
-
await
|
|
2008
|
+
if (!agentsInspection.exists) {
|
|
2009
|
+
await writeManagedFile(agentsPath, `# ${title}
|
|
1898
2010
|
|
|
1899
2011
|
${upsertRspAgentsBlock("").content}
|
|
1900
|
-
|
|
2012
|
+
`, "AGENTS.md");
|
|
1901
2013
|
created = true;
|
|
1902
2014
|
agentsUpdated = true;
|
|
1903
2015
|
} else {
|
|
1904
|
-
const existing = await
|
|
2016
|
+
const existing = await readFile8(agentsPath, "utf-8");
|
|
1905
2017
|
const next = upsertRspAgentsBlock(existing);
|
|
1906
2018
|
if (next.changed) {
|
|
1907
|
-
await
|
|
2019
|
+
await writeManagedFile(agentsPath, next.content, "AGENTS.md");
|
|
1908
2020
|
created = true;
|
|
1909
2021
|
agentsUpdated = true;
|
|
1910
2022
|
}
|
|
1911
2023
|
}
|
|
1912
2024
|
if (mode === "print") {
|
|
1913
|
-
const managed = await
|
|
2025
|
+
const managed = await readFile8(agentsPath, "utf-8");
|
|
1914
2026
|
console.log(`
|
|
1915
2027
|
${managed}
|
|
1916
2028
|
`);
|
|
@@ -1922,8 +2034,9 @@ ${managed}
|
|
|
1922
2034
|
console.log(`
|
|
1923
2035
|
${pc.green("RSP scaffolded.")}
|
|
1924
2036
|
`);
|
|
1925
|
-
console.log(` Created: .rsp/rules
|
|
2037
|
+
console.log(` Created: .rsp/rsp-rules.md
|
|
1926
2038
|
.rsp/specs/
|
|
2039
|
+
${decisionRecordsPath}/
|
|
1927
2040
|
.rsp/changes/
|
|
1928
2041
|
.rsp/focus.d/
|
|
1929
2042
|
.rsp/archives/
|
|
@@ -1948,33 +2061,50 @@ ${managed}
|
|
|
1948
2061
|
}
|
|
1949
2062
|
|
|
1950
2063
|
// src/commands/ready.ts
|
|
1951
|
-
import {
|
|
1952
|
-
|
|
1953
|
-
|
|
2064
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
2065
|
+
function exitReadyError(name, error, options) {
|
|
2066
|
+
if (options.json) {
|
|
2067
|
+
emitJson({
|
|
2068
|
+
command: "ready",
|
|
2069
|
+
ok: false,
|
|
2070
|
+
change: name || null,
|
|
2071
|
+
path: null,
|
|
2072
|
+
warnings: [],
|
|
2073
|
+
runtime: [],
|
|
2074
|
+
error
|
|
2075
|
+
}, options);
|
|
2076
|
+
} else {
|
|
2077
|
+
console.error(` ${pc.red("Error:")} ${error.message}`);
|
|
2078
|
+
}
|
|
2079
|
+
process.exit(1);
|
|
2080
|
+
}
|
|
1954
2081
|
async function showReady(name, options = {}) {
|
|
1955
2082
|
if (!name) {
|
|
1956
2083
|
console.error(` ${pc.red("Usage:")} rsp ready <name>`);
|
|
1957
2084
|
process.exit(1);
|
|
1958
2085
|
}
|
|
1959
|
-
if (!isValidChangeName(name)) {
|
|
1960
|
-
console.error(` ${pc.red("Error:")} change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
1961
|
-
process.exit(1);
|
|
1962
|
-
}
|
|
1963
2086
|
guardRspInitialized();
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
2087
|
+
let workRef;
|
|
2088
|
+
try {
|
|
2089
|
+
workRef = await resolveExecutableChange(name, { mustExist: true });
|
|
2090
|
+
} catch (error) {
|
|
2091
|
+
if (error instanceof WorkRefError)
|
|
2092
|
+
exitReadyError(name, { code: error.code, message: error.message }, options);
|
|
2093
|
+
throw error;
|
|
1968
2094
|
}
|
|
2095
|
+
const srcPath = workRef.path;
|
|
1969
2096
|
const runtime = [];
|
|
1970
2097
|
let content;
|
|
1971
2098
|
try {
|
|
1972
|
-
content = await
|
|
2099
|
+
content = await readFile9(srcPath, "utf-8");
|
|
1973
2100
|
} catch {
|
|
1974
2101
|
console.error(` ${pc.red("Error:")} unable to read .rsp/changes/${name}.md`);
|
|
1975
2102
|
process.exit(1);
|
|
1976
2103
|
}
|
|
1977
|
-
const
|
|
2104
|
+
const dependencyInspection = await inspectChangeDependencies();
|
|
2105
|
+
const readinessDetails = collectArchiveReadiness(content, {
|
|
2106
|
+
activeBlockers: dependencyInspection.activeBlockers.get(name)
|
|
2107
|
+
});
|
|
1978
2108
|
const checklist = readinessDetails.warnings;
|
|
1979
2109
|
const readiness = {
|
|
1980
2110
|
incompleteTasks: readinessDetails.taskTodos.length,
|
|
@@ -1985,7 +2115,19 @@ async function showReady(name, options = {}) {
|
|
|
1985
2115
|
semantic: readinessDetails.semantic,
|
|
1986
2116
|
archiveReady: readinessDetails.archiveReady
|
|
1987
2117
|
};
|
|
1988
|
-
|
|
2118
|
+
let decisionRecordsPath;
|
|
2119
|
+
try {
|
|
2120
|
+
const configInspection = await inspectRspConfig();
|
|
2121
|
+
if (configInspection.issues.length > 0)
|
|
2122
|
+
exitReadyError(name, { code: "invalid_config", message: configInspection.issues.join("; ") }, options);
|
|
2123
|
+
decisionRecordsPath = resolveDecisionRecordsPath(configInspection.config);
|
|
2124
|
+
const filesystemIssue = await validateDecisionRecordsFilesystemPath(decisionRecordsPath);
|
|
2125
|
+
if (filesystemIssue)
|
|
2126
|
+
exitReadyError(name, { code: "invalid_decision_records_path", message: filesystemIssue }, options);
|
|
2127
|
+
} catch (error) {
|
|
2128
|
+
exitReadyError(name, { code: "invalid_config", message: `.rsp/config.yaml could not be parsed: ${toErrorMessage(error)}` }, options);
|
|
2129
|
+
}
|
|
2130
|
+
const durableReview = buildDurableReviewGuidance(getDurableReviewCandidateTargets(), decisionRecordsPath);
|
|
1989
2131
|
const result = {
|
|
1990
2132
|
command: "ready",
|
|
1991
2133
|
ok: true,
|
|
@@ -1997,7 +2139,7 @@ async function showReady(name, options = {}) {
|
|
|
1997
2139
|
runtime
|
|
1998
2140
|
};
|
|
1999
2141
|
if (options.json) {
|
|
2000
|
-
emitJson(result);
|
|
2142
|
+
emitJson(result, options);
|
|
2001
2143
|
return result;
|
|
2002
2144
|
}
|
|
2003
2145
|
console.log();
|
|
@@ -2011,25 +2153,20 @@ async function showReady(name, options = {}) {
|
|
|
2011
2153
|
console.log(` ${pc.yellow("\u26A0")} ${line}`);
|
|
2012
2154
|
console.log();
|
|
2013
2155
|
console.log(` ${pc.dim("Review the warnings above before treating this work as fully closed.")}`);
|
|
2014
|
-
console.log(` ${pc.dim("Run:")} rsp archive ${name}
|
|
2015
|
-
`);
|
|
2016
2156
|
}
|
|
2017
2157
|
console.log(` ${pc.dim("Deterministic readiness:")} ${readiness.deterministic === "pass" ? pc.green("pass") : pc.yellow("warnings")}`);
|
|
2018
2158
|
console.log(` ${pc.dim("Semantic review:")} ${pc.yellow("needed")}`);
|
|
2019
2159
|
console.log(` ${pc.dim("Archive ready:")} ${formatArchiveReady(readiness.archiveReady)}
|
|
2020
2160
|
`);
|
|
2021
2161
|
console.log(` ${pc.bold("Durable review:")}`);
|
|
2022
|
-
console.log(` ${pc.dim("
|
|
2023
|
-
console.log(` ${pc.dim("
|
|
2162
|
+
console.log(` ${pc.dim("Current-fact options:")} ${durableReview.factDecisions.join(" | ")}`);
|
|
2163
|
+
console.log(` ${pc.dim("Rationale options:")} ${durableReview.rationaleDecisions.join(" | ")}`);
|
|
2164
|
+
console.log(` ${pc.dim("Current-fact targets:")} ${durableReview.factCandidateTargets.join(", ")}`);
|
|
2165
|
+
console.log(` ${pc.dim("Decision Record path:")} ${durableReview.decisionRecordsPath}`);
|
|
2024
2166
|
console.log(` ${pc.dim(durableReview.note)}
|
|
2025
2167
|
`);
|
|
2026
2168
|
return result;
|
|
2027
2169
|
}
|
|
2028
|
-
function getReadyDurableReviewCandidateTargets() {
|
|
2029
|
-
return getDurableReviewCandidateTargets({
|
|
2030
|
-
projectRulesExists: existsSync14(join14(RSP_DIR, "rules", "project-rules.md"))
|
|
2031
|
-
});
|
|
2032
|
-
}
|
|
2033
2170
|
function formatArchiveReady(value) {
|
|
2034
2171
|
switch (value) {
|
|
2035
2172
|
case "yes":
|
|
@@ -2042,9 +2179,7 @@ function formatArchiveReady(value) {
|
|
|
2042
2179
|
}
|
|
2043
2180
|
|
|
2044
2181
|
// src/commands/show.ts
|
|
2045
|
-
import {
|
|
2046
|
-
import { readFile as readFile12 } from "fs/promises";
|
|
2047
|
-
import { join as join15 } from "path";
|
|
2182
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2048
2183
|
function exitShowError(error, options) {
|
|
2049
2184
|
const nextActions = error.code === "no_focused_change" ? ["Run: rsp status", "Run: rsp focus <name>", "Or run: rsp create <name>"] : [];
|
|
2050
2185
|
if (options.json) {
|
|
@@ -2056,7 +2191,7 @@ function exitShowError(error, options) {
|
|
|
2056
2191
|
runtime: [],
|
|
2057
2192
|
nextActions,
|
|
2058
2193
|
error
|
|
2059
|
-
});
|
|
2194
|
+
}, options);
|
|
2060
2195
|
} else {
|
|
2061
2196
|
console.error(` ${pc.red("Error:")} ${error.message}`);
|
|
2062
2197
|
for (const action of nextActions)
|
|
@@ -2068,9 +2203,14 @@ async function showChange(nameOrFocused, options = {}) {
|
|
|
2068
2203
|
const runtime = [];
|
|
2069
2204
|
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
|
|
2070
2205
|
guardRspInitialized();
|
|
2206
|
+
const focusTree = await inspectFocusTree();
|
|
2207
|
+
if (focusTree.diagnostics.length > 0) {
|
|
2208
|
+
const diagnostic = focusTree.diagnostics[0];
|
|
2209
|
+
exitShowError({ code: diagnostic.code, message: diagnostic.message }, options);
|
|
2210
|
+
}
|
|
2211
|
+
const focused = new Set(focusTree.markers.map((marker) => marker.name));
|
|
2071
2212
|
let name;
|
|
2072
2213
|
if (options.focused) {
|
|
2073
|
-
const focused = await getFocusedChangeNames({ onError: reportRuntime });
|
|
2074
2214
|
if (focused.size === 0) {
|
|
2075
2215
|
exitShowError({ code: "no_focused_change", message: "no focused change exists" }, options);
|
|
2076
2216
|
}
|
|
@@ -2082,23 +2222,22 @@ async function showChange(nameOrFocused, options = {}) {
|
|
|
2082
2222
|
}
|
|
2083
2223
|
name = [...focused][0];
|
|
2084
2224
|
} else if (nameOrFocused) {
|
|
2085
|
-
if (!isValidChangeName(nameOrFocused)) {
|
|
2086
|
-
exitShowError({
|
|
2087
|
-
code: "invalid_change_name",
|
|
2088
|
-
message: "change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)"
|
|
2089
|
-
}, options);
|
|
2090
|
-
}
|
|
2091
2225
|
name = nameOrFocused;
|
|
2092
2226
|
} else {
|
|
2093
2227
|
exitShowError({ code: "missing_change_name", message: "Usage: rsp show <name|--focused> [--json] [--verbose]" }, options);
|
|
2094
2228
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2229
|
+
let workRef;
|
|
2230
|
+
try {
|
|
2231
|
+
workRef = await resolveExecutableChange(name, { mustExist: true });
|
|
2232
|
+
} catch (error) {
|
|
2233
|
+
if (error instanceof WorkRefError)
|
|
2234
|
+
exitShowError({ code: error.code, message: error.message }, options);
|
|
2235
|
+
throw error;
|
|
2098
2236
|
}
|
|
2237
|
+
const srcPath = workRef.path;
|
|
2099
2238
|
let content;
|
|
2100
2239
|
try {
|
|
2101
|
-
content = await
|
|
2240
|
+
content = await readFile10(srcPath, "utf-8");
|
|
2102
2241
|
} catch {
|
|
2103
2242
|
exitShowError({ code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, options);
|
|
2104
2243
|
}
|
|
@@ -2115,12 +2254,12 @@ async function showChange(nameOrFocused, options = {}) {
|
|
|
2115
2254
|
message: toErrorMessage(error)
|
|
2116
2255
|
});
|
|
2117
2256
|
}
|
|
2118
|
-
const
|
|
2119
|
-
const
|
|
2257
|
+
const isFocused = focused.has(name);
|
|
2258
|
+
const dependencyInspection = await inspectChangeDependencies();
|
|
2120
2259
|
const cb = countCheckboxes(content);
|
|
2121
|
-
const blockers = hasMeaningfulBlockers(content);
|
|
2260
|
+
const blockers = dependencyInspection.activeBlockers.get(name) ?? hasMeaningfulBlockers(content);
|
|
2122
2261
|
const scenarios = parseScenarios(content);
|
|
2123
|
-
const readinessDetails = collectArchiveReadiness(content);
|
|
2262
|
+
const readinessDetails = collectArchiveReadiness(content, { activeBlockers: blockers });
|
|
2124
2263
|
const readiness = {
|
|
2125
2264
|
incompleteTasks: readinessDetails.taskTodos.length,
|
|
2126
2265
|
incompleteVerify: readinessDetails.verifyTodos.length,
|
|
@@ -2130,10 +2269,22 @@ async function showChange(nameOrFocused, options = {}) {
|
|
|
2130
2269
|
semantic: readinessDetails.semantic,
|
|
2131
2270
|
archiveReady: readinessDetails.archiveReady
|
|
2132
2271
|
};
|
|
2133
|
-
const
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2272
|
+
const factCandidateTargets = getDurableReviewCandidateTargets();
|
|
2273
|
+
let decisionRecordsPath;
|
|
2274
|
+
try {
|
|
2275
|
+
const configInspection = await inspectRspConfig();
|
|
2276
|
+
if (configInspection.issues.length > 0)
|
|
2277
|
+
exitShowError({ code: "invalid_config", message: configInspection.issues.join("; ") }, options);
|
|
2278
|
+
decisionRecordsPath = resolveDecisionRecordsPath(configInspection.config);
|
|
2279
|
+
const filesystemIssue = await validateDecisionRecordsFilesystemPath(decisionRecordsPath);
|
|
2280
|
+
if (filesystemIssue)
|
|
2281
|
+
exitShowError({ code: "invalid_decision_records_path", message: filesystemIssue }, options);
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
exitShowError({ code: "invalid_config", message: `.rsp/config.yaml could not be parsed: ${toErrorMessage(error)}` }, options);
|
|
2284
|
+
}
|
|
2285
|
+
const groupContextPaths = workRef.group ? [normalizeLogicalPath(resolveWorkRef(`${workRef.group}/brief`, { mustExist: true }).path)] : [];
|
|
2286
|
+
const contextPaths = [...groupContextPaths, ...factCandidateTargets, decisionRecordsPath];
|
|
2287
|
+
const durableReview = buildDurableReviewGuidance(factCandidateTargets, decisionRecordsPath);
|
|
2137
2288
|
const result = {
|
|
2138
2289
|
command: "show",
|
|
2139
2290
|
ok: true,
|
|
@@ -2152,7 +2303,7 @@ async function showChange(nameOrFocused, options = {}) {
|
|
|
2152
2303
|
runtime
|
|
2153
2304
|
};
|
|
2154
2305
|
if (options.json) {
|
|
2155
|
-
emitJson(result);
|
|
2306
|
+
emitJson(result, options);
|
|
2156
2307
|
return result;
|
|
2157
2308
|
}
|
|
2158
2309
|
console.log();
|
|
@@ -2178,8 +2329,10 @@ async function showChange(nameOrFocused, options = {}) {
|
|
|
2178
2329
|
console.log(` ${pc.dim(cp)}`);
|
|
2179
2330
|
console.log();
|
|
2180
2331
|
console.log(` ${pc.bold("Durable review:")}`);
|
|
2181
|
-
console.log(` ${pc.dim("
|
|
2182
|
-
console.log(` ${pc.dim("
|
|
2332
|
+
console.log(` ${pc.dim("Current-fact options:")} ${durableReview.factDecisions.join(" | ")}`);
|
|
2333
|
+
console.log(` ${pc.dim("Rationale options:")} ${durableReview.rationaleDecisions.join(" | ")}`);
|
|
2334
|
+
console.log(` ${pc.dim("Current-fact targets:")} ${durableReview.factCandidateTargets.join(", ")}`);
|
|
2335
|
+
console.log(` ${pc.dim("Decision Record path:")} ${durableReview.decisionRecordsPath}`);
|
|
2183
2336
|
console.log(` ${pc.dim(durableReview.note)}`);
|
|
2184
2337
|
console.log();
|
|
2185
2338
|
return result;
|
|
@@ -2195,191 +2348,286 @@ function formatArchiveReady2(value) {
|
|
|
2195
2348
|
}
|
|
2196
2349
|
}
|
|
2197
2350
|
|
|
2198
|
-
// src/commands/
|
|
2199
|
-
import {
|
|
2200
|
-
import {
|
|
2201
|
-
import {
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
const
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
const
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
const
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
if (
|
|
2351
|
+
// src/commands/skills.ts
|
|
2352
|
+
import { randomUUID } from "crypto";
|
|
2353
|
+
import { lstatSync } from "fs";
|
|
2354
|
+
import { chmod, copyFile, mkdir as mkdir9, readdir as readdir2, readFile as readFile11, rename as rename3, rm as rm2 } from "fs/promises";
|
|
2355
|
+
import { dirname as dirname4, join as join9, relative as relative4 } from "path";
|
|
2356
|
+
function unsupportedEntry(path, label) {
|
|
2357
|
+
return new ManagedPathError(path, `unsupported entry in the ${label} tree: ${path}`);
|
|
2358
|
+
}
|
|
2359
|
+
function inspectDirectoryIdentity(path, label) {
|
|
2360
|
+
const inspection = inspectManagedDirectory(path, label);
|
|
2361
|
+
if (inspection.issue)
|
|
2362
|
+
throw inspection.issue;
|
|
2363
|
+
const stats = lstatSync(path);
|
|
2364
|
+
return { path, dev: stats.dev, ino: stats.ino };
|
|
2365
|
+
}
|
|
2366
|
+
function assertDirectoryIdentities(identities, label) {
|
|
2367
|
+
for (const identity of identities) {
|
|
2368
|
+
const current = inspectDirectoryIdentity(identity.path, label);
|
|
2369
|
+
if (current.dev !== identity.dev || current.ino !== identity.ino)
|
|
2370
|
+
throw new ManagedPathError(identity.path, `${label} changed after validation: ${identity.path}`);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
async function prepareTargetRoot(projectRoot, targetRoot) {
|
|
2374
|
+
const projectIdentity = inspectDirectoryIdentity(projectRoot, "project Skills root");
|
|
2375
|
+
const agentsRoot = join9(projectRoot, ".agents");
|
|
2376
|
+
const identities = [projectIdentity];
|
|
2377
|
+
assertDirectoryIdentities(identities, "project Skills root");
|
|
2378
|
+
await mkdir9(agentsRoot).catch((error) => {
|
|
2379
|
+
if (error.code !== "EEXIST")
|
|
2380
|
+
throw error;
|
|
2381
|
+
});
|
|
2382
|
+
assertDirectoryIdentities(identities, "project Skills root");
|
|
2383
|
+
identities.push(inspectDirectoryIdentity(agentsRoot, "project Skills root"));
|
|
2384
|
+
assertDirectoryIdentities(identities, "project Skills root");
|
|
2385
|
+
await mkdir9(targetRoot).catch((error) => {
|
|
2386
|
+
if (error.code !== "EEXIST")
|
|
2387
|
+
throw error;
|
|
2388
|
+
});
|
|
2389
|
+
assertDirectoryIdentities(identities, "project Skills root");
|
|
2390
|
+
identities.push(inspectDirectoryIdentity(targetRoot, "project Skills root"));
|
|
2391
|
+
return identities;
|
|
2392
|
+
}
|
|
2393
|
+
async function inspectPackagedSkills(packageRoot) {
|
|
2394
|
+
const skillsRoot = join9(packageRoot, "skills");
|
|
2395
|
+
const rootInspection = inspectManagedDirectory(skillsRoot, "packaged Skills root");
|
|
2396
|
+
if (rootInspection.issue)
|
|
2397
|
+
throw rootInspection.issue;
|
|
2398
|
+
const entries = await readdir2(skillsRoot, { withFileTypes: true });
|
|
2399
|
+
const skills = [];
|
|
2400
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2401
|
+
if (entry.name.startsWith("."))
|
|
2402
|
+
continue;
|
|
2403
|
+
const root = join9(skillsRoot, entry.name);
|
|
2404
|
+
if (!entry.isDirectory())
|
|
2405
|
+
throw unsupportedEntry(root, "packaged Skills");
|
|
2406
|
+
const inspection = await inspectManagedFileTree(root, `packaged Skill ${entry.name}`);
|
|
2407
|
+
if (inspection.issues.length > 0)
|
|
2408
|
+
throw inspection.issues[0];
|
|
2409
|
+
skills.push({
|
|
2410
|
+
name: entry.name,
|
|
2411
|
+
root,
|
|
2412
|
+
files: inspection.files.map((path) => relative4(root, path))
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
return skills;
|
|
2416
|
+
}
|
|
2417
|
+
function inspectTarget(path, name) {
|
|
2418
|
+
try {
|
|
2419
|
+
const stats = lstatSync(path);
|
|
2420
|
+
if (stats.isDirectory())
|
|
2421
|
+
return "directory";
|
|
2422
|
+
throw unsupportedEntry(path, `installed Skill ${name}`);
|
|
2423
|
+
} catch (error) {
|
|
2424
|
+
if (error.code === "ENOENT")
|
|
2425
|
+
return "missing";
|
|
2426
|
+
throw error;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
async function treesEqual(source, targetRoot) {
|
|
2430
|
+
const target = await inspectManagedFileTree(targetRoot, `installed Skill ${source.name}`);
|
|
2431
|
+
if (target.issues.length > 0)
|
|
2432
|
+
throw target.issues[0];
|
|
2433
|
+
const targetFiles = target.files.map((path) => relative4(targetRoot, path));
|
|
2434
|
+
if (source.files.length !== targetFiles.length || source.files.some((path, index) => path !== targetFiles[index]))
|
|
2435
|
+
return false;
|
|
2436
|
+
for (const path of source.files) {
|
|
2437
|
+
const sourcePath = join9(source.root, path);
|
|
2438
|
+
const targetPath = join9(targetRoot, path);
|
|
2439
|
+
const [sourceStats, targetStats] = [lstatSync(sourcePath), lstatSync(targetPath)];
|
|
2440
|
+
if (!sourceStats.isFile() || !targetStats.isFile())
|
|
2441
|
+
throw unsupportedEntry(!sourceStats.isFile() ? sourcePath : targetPath, `Skill ${source.name}`);
|
|
2442
|
+
if ((sourceStats.mode & 511) !== (targetStats.mode & 511))
|
|
2443
|
+
return false;
|
|
2444
|
+
const [sourceContent, targetContent] = await Promise.all([readFile11(sourcePath), readFile11(targetPath)]);
|
|
2445
|
+
if (!sourceContent.equals(targetContent))
|
|
2446
|
+
return false;
|
|
2447
|
+
}
|
|
2448
|
+
return true;
|
|
2449
|
+
}
|
|
2450
|
+
async function copySkillTree(source, targetRoot) {
|
|
2451
|
+
await mkdir9(targetRoot, { recursive: true });
|
|
2452
|
+
for (const path of source.files) {
|
|
2453
|
+
const sourcePath = join9(source.root, path);
|
|
2454
|
+
const targetPath = join9(targetRoot, path);
|
|
2455
|
+
const stats = lstatSync(sourcePath);
|
|
2456
|
+
if (!stats.isFile())
|
|
2457
|
+
throw unsupportedEntry(sourcePath, `packaged Skill ${source.name}`);
|
|
2458
|
+
await mkdir9(dirname4(targetPath), { recursive: true });
|
|
2459
|
+
await copyFile(sourcePath, targetPath);
|
|
2460
|
+
await chmod(targetPath, stats.mode & 511);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
async function installPackagedSkills(args = {}, options = {}) {
|
|
2464
|
+
const packageRoot = options.packageRoot ?? PKG_ROOT;
|
|
2465
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
2466
|
+
const renamePath = options.renamePath ?? rename3;
|
|
2467
|
+
const skills = await inspectPackagedSkills(packageRoot);
|
|
2468
|
+
const targetRoot = resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
|
|
2469
|
+
const result = { installed: [], unchanged: [], replaced: [] };
|
|
2470
|
+
const conflicts = [];
|
|
2471
|
+
const targetTreeIdentities = /* @__PURE__ */ new Map();
|
|
2472
|
+
for (const skill of skills) {
|
|
2473
|
+
const target = join9(targetRoot, skill.name);
|
|
2474
|
+
if (inspectTarget(target, skill.name) === "missing") {
|
|
2475
|
+
result.installed.push(skill.name);
|
|
2476
|
+
continue;
|
|
2477
|
+
}
|
|
2478
|
+
const targetIdentity = inspectDirectoryIdentity(target, `installed Skill ${skill.name}`);
|
|
2479
|
+
if (await treesEqual(skill, target)) {
|
|
2480
|
+
result.unchanged.push(skill.name);
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
assertDirectoryIdentities([targetIdentity], `installed Skill ${skill.name}`);
|
|
2484
|
+
if (args.force) {
|
|
2485
|
+
result.replaced.push(skill.name);
|
|
2486
|
+
targetTreeIdentities.set(skill.name, targetIdentity);
|
|
2487
|
+
} else {
|
|
2488
|
+
conflicts.push(skill.name);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
if (conflicts.length > 0)
|
|
2492
|
+
throw new Error(`conflicting packaged Skills: ${conflicts.join(", ")}; rerun with --force to replace only these package-owned directories`);
|
|
2493
|
+
if (args.dryRun)
|
|
2494
|
+
return result;
|
|
2495
|
+
await options.onMutationStep?.({ phase: "before-target-root-mutation", targetRoot });
|
|
2496
|
+
const targetIdentities = await prepareTargetRoot(projectRoot, targetRoot);
|
|
2497
|
+
const stagingRoot = join9(targetRoot, `.rsp-skills-install-${randomUUID()}`);
|
|
2498
|
+
const nextRoot = join9(stagingRoot, "next");
|
|
2499
|
+
const previousRoot = join9(stagingRoot, "previous");
|
|
2500
|
+
const failedRoot = join9(stagingRoot, "failed");
|
|
2501
|
+
const movedPrevious = [];
|
|
2502
|
+
const activated = [];
|
|
2503
|
+
let removeStaging = true;
|
|
2504
|
+
try {
|
|
2505
|
+
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
2506
|
+
await mkdir9(nextRoot, { recursive: true });
|
|
2507
|
+
await mkdir9(previousRoot);
|
|
2508
|
+
await mkdir9(failedRoot);
|
|
2509
|
+
for (const skill of skills) {
|
|
2510
|
+
if (result.unchanged.includes(skill.name))
|
|
2511
|
+
continue;
|
|
2512
|
+
await copySkillTree(skill, join9(nextRoot, skill.name));
|
|
2513
|
+
}
|
|
2514
|
+
for (const name of result.installed) {
|
|
2515
|
+
const target = join9(targetRoot, name);
|
|
2516
|
+
if (inspectTarget(target, name) !== "missing")
|
|
2517
|
+
throw new ManagedPathError(target, `installed Skill changed after preflight: ${target}`);
|
|
2518
|
+
}
|
|
2519
|
+
for (const name of result.replaced) {
|
|
2520
|
+
assertDirectoryIdentities([targetTreeIdentities.get(name)], `installed Skill ${name}`);
|
|
2521
|
+
}
|
|
2522
|
+
for (const name of result.replaced) {
|
|
2523
|
+
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
2524
|
+
assertDirectoryIdentities([targetTreeIdentities.get(name)], `installed Skill ${name}`);
|
|
2525
|
+
await renamePath(join9(targetRoot, name), join9(previousRoot, name));
|
|
2526
|
+
movedPrevious.push(name);
|
|
2527
|
+
}
|
|
2528
|
+
for (const name of [...result.installed, ...result.replaced].sort()) {
|
|
2529
|
+
const target = join9(targetRoot, name);
|
|
2530
|
+
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
2531
|
+
await options.onMutationStep?.({ phase: "before-activate", name, target });
|
|
2532
|
+
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
2533
|
+
if (inspectTarget(target, name) !== "missing")
|
|
2534
|
+
throw new ManagedPathError(target, `installed Skill changed after preflight: ${target}`);
|
|
2535
|
+
await renamePath(join9(nextRoot, name), target);
|
|
2536
|
+
activated.push(name);
|
|
2537
|
+
}
|
|
2538
|
+
} catch (error) {
|
|
2539
|
+
const rollbackErrors = [];
|
|
2540
|
+
for (const name of activated.reverse()) {
|
|
2541
|
+
const target = join9(targetRoot, name);
|
|
2227
2542
|
try {
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
kind = "(invalid)";
|
|
2234
|
-
reportRuntime({
|
|
2235
|
-
code: "frontmatter_parse_failed",
|
|
2236
|
-
operation: "parseFrontmatter",
|
|
2237
|
-
path: fp,
|
|
2238
|
-
message: toErrorMessage(error)
|
|
2239
|
-
});
|
|
2240
|
-
}
|
|
2241
|
-
isBlocked = hasMeaningfulBlockers(content);
|
|
2242
|
-
const cb = countCheckboxes(content);
|
|
2243
|
-
done = cb.done;
|
|
2244
|
-
total = cb.total;
|
|
2245
|
-
progress = `${done}/${total}`;
|
|
2246
|
-
} catch (error) {
|
|
2247
|
-
reportRuntime({
|
|
2248
|
-
code: "change_read_failed",
|
|
2249
|
-
operation: "readFile",
|
|
2250
|
-
path: fp,
|
|
2251
|
-
message: toErrorMessage(error)
|
|
2252
|
-
});
|
|
2543
|
+
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
2544
|
+
if (inspectTarget(target, name) === "directory")
|
|
2545
|
+
await renamePath(target, join9(failedRoot, name));
|
|
2546
|
+
} catch (rollbackError) {
|
|
2547
|
+
rollbackErrors.push(rollbackError instanceof Error ? rollbackError.message : String(rollbackError));
|
|
2253
2548
|
}
|
|
2549
|
+
}
|
|
2550
|
+
for (const name of movedPrevious.reverse()) {
|
|
2551
|
+
const target = join9(targetRoot, name);
|
|
2552
|
+
const previous = join9(previousRoot, name);
|
|
2254
2553
|
try {
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
});
|
|
2554
|
+
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
2555
|
+
await options.onMutationStep?.({ phase: "before-rollback-restore", name, target, previous });
|
|
2556
|
+
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
2557
|
+
if (inspectTarget(target, name) !== "missing")
|
|
2558
|
+
throw new ManagedPathError(target, `cannot restore installed Skill because the target is occupied: ${target}`);
|
|
2559
|
+
await renamePath(previous, target);
|
|
2560
|
+
} catch (rollbackError) {
|
|
2561
|
+
rollbackErrors.push(rollbackError instanceof Error ? rollbackError.message : String(rollbackError));
|
|
2264
2562
|
}
|
|
2265
|
-
} else {
|
|
2266
|
-
kind = "(missing)";
|
|
2267
|
-
reportRuntime({
|
|
2268
|
-
code: "focused_change_missing",
|
|
2269
|
-
operation: "resolveChange",
|
|
2270
|
-
path: join16(CHANGES_DIR, `${name}.md`),
|
|
2271
|
-
message: "focus marker points to a missing change file"
|
|
2272
|
-
});
|
|
2273
2563
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
path: fp ? normalizeLogicalPath(fp) : null
|
|
2284
|
-
});
|
|
2564
|
+
if (rollbackErrors.length > 0) {
|
|
2565
|
+
removeStaging = false;
|
|
2566
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2567
|
+
throw new Error(`${message}; rollback incomplete; recover original Skills from ${previousRoot}; ${rollbackErrors.join("; ")}`);
|
|
2568
|
+
}
|
|
2569
|
+
throw error;
|
|
2570
|
+
} finally {
|
|
2571
|
+
if (removeStaging)
|
|
2572
|
+
await rm2(stagingRoot, { recursive: true, force: true });
|
|
2285
2573
|
}
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2574
|
+
return result;
|
|
2575
|
+
}
|
|
2576
|
+
function printSkillInstallResult(result, dryRun = false) {
|
|
2577
|
+
const prefix = dryRun ? "would be " : "";
|
|
2578
|
+
for (const status of ["installed", "unchanged", "replaced"]) {
|
|
2579
|
+
if (result[status].length > 0)
|
|
2580
|
+
console.log(` ${prefix}${status}: ${result[status].join(", ")}`);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// src/status/derive.ts
|
|
2585
|
+
function deriveStatusView(snapshot, options = {}) {
|
|
2586
|
+
const query = {
|
|
2587
|
+
focused: Boolean(options.focused),
|
|
2588
|
+
blocked: Boolean(options.blocked),
|
|
2589
|
+
stale: typeof options.stale === "number" ? options.stale : null
|
|
2590
|
+
};
|
|
2591
|
+
const records = snapshot.records.filter(({ output: record }) => {
|
|
2592
|
+
if (query.focused && !record.isFocused)
|
|
2290
2593
|
return false;
|
|
2291
|
-
if (
|
|
2292
|
-
const ageDays = Number(record.age);
|
|
2293
|
-
if (!Number.isFinite(ageDays) || ageDays < options.stale)
|
|
2294
|
-
return false;
|
|
2295
|
-
}
|
|
2296
|
-
return true;
|
|
2297
|
-
});
|
|
2298
|
-
const filteredOutputRecords = outputRecords.filter((record) => {
|
|
2299
|
-
if (options.focused && !record.isFocused)
|
|
2594
|
+
if (query.blocked && !record.isBlocked)
|
|
2300
2595
|
return false;
|
|
2301
|
-
if (
|
|
2596
|
+
if (query.stale !== null && (record.ageDays === null || record.ageDays < query.stale))
|
|
2302
2597
|
return false;
|
|
2303
|
-
if (typeof options.stale === "number") {
|
|
2304
|
-
if (record.ageDays === null || record.ageDays < options.stale)
|
|
2305
|
-
return false;
|
|
2306
|
-
}
|
|
2307
2598
|
return true;
|
|
2308
2599
|
});
|
|
2309
|
-
const
|
|
2310
|
-
const
|
|
2311
|
-
const
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
},
|
|
2319
|
-
focused: [...focusedSet].sort(),
|
|
2320
|
-
records: filteredOutputRecords,
|
|
2600
|
+
const filtered = query.focused || query.blocked || query.stale !== null;
|
|
2601
|
+
const ok = snapshot.diagnostics.every((diagnostic) => diagnostic.severity !== "error");
|
|
2602
|
+
const nextActions = ok ? buildStatusNextActions(snapshot.focused.length, snapshot.records.map((record) => record.output), snapshot.groups, filtered) : ["Run: rsp doctor"];
|
|
2603
|
+
return {
|
|
2604
|
+
query,
|
|
2605
|
+
focused: snapshot.focused,
|
|
2606
|
+
records,
|
|
2607
|
+
groups: snapshot.groups,
|
|
2608
|
+
plan: filterDependencyPlan(snapshot.plan, new Set(records.map((record) => record.output.name))),
|
|
2321
2609
|
summary: {
|
|
2322
|
-
total:
|
|
2323
|
-
focused:
|
|
2324
|
-
blocked:
|
|
2610
|
+
total: records.length,
|
|
2611
|
+
focused: records.filter((record) => record.output.isFocused).length,
|
|
2612
|
+
blocked: records.filter((record) => record.output.isBlocked).length
|
|
2325
2613
|
},
|
|
2326
2614
|
nextActions,
|
|
2327
|
-
archiveTrend:
|
|
2328
|
-
|
|
2615
|
+
archiveTrend: snapshot.archiveTrend,
|
|
2616
|
+
diagnostics: snapshot.diagnostics,
|
|
2617
|
+
runtime: snapshot.runtime,
|
|
2618
|
+
hasExecutableChanges: snapshot.records.length > 0,
|
|
2619
|
+
ok
|
|
2329
2620
|
};
|
|
2330
|
-
if (runOptions.json) {
|
|
2331
|
-
emitJson(statusResult);
|
|
2332
|
-
return statusResult;
|
|
2333
|
-
}
|
|
2334
|
-
console.log();
|
|
2335
|
-
console.log(` ${pc.bold("RSP status")}`);
|
|
2336
|
-
console.log();
|
|
2337
|
-
if (focusedSet.size > 0) {
|
|
2338
|
-
console.log(` ${pc.cyan("Focused:")} ${[...focusedSet].join(", ")}`);
|
|
2339
|
-
console.log();
|
|
2340
|
-
} else {
|
|
2341
|
-
console.log(` ${pc.dim("No focused change.")}`);
|
|
2342
|
-
for (const action of nextActions)
|
|
2343
|
-
console.log(` ${pc.dim(action)}`);
|
|
2344
|
-
console.log();
|
|
2345
|
-
}
|
|
2346
|
-
if (allNames.length === 0) {
|
|
2347
|
-
console.log(` ${pc.dim("No changes found.")} Run: rsp create <name>
|
|
2348
|
-
`);
|
|
2349
|
-
return statusResult;
|
|
2350
|
-
}
|
|
2351
|
-
if (filteredRecords.length === 0) {
|
|
2352
|
-
console.log(` ${pc.dim("No changes match the current filters.")}
|
|
2353
|
-
`);
|
|
2354
|
-
return statusResult;
|
|
2355
|
-
}
|
|
2356
|
-
const pad = (s, w) => s.padEnd(w);
|
|
2357
|
-
console.log(` ${pad("Change", COL_CHANGE)} ${pad("Kind", COL_KIND)} ${pad("Age(d)", COL_AGE)} ${pad("Progress", COL_PROGRESS)}`);
|
|
2358
|
-
console.log(` ${"\u2500".repeat(COL_CHANGE)} ${"\u2500".repeat(COL_KIND)} ${"\u2500".repeat(COL_AGE)} ${"\u2500".repeat(COL_PROGRESS)}`);
|
|
2359
|
-
for (const r of filteredRecords) {
|
|
2360
|
-
const marker = r.isFocused ? pc.cyan("*") : " ";
|
|
2361
|
-
const namePadded = r.name.padEnd(COL_CHANGE - 1);
|
|
2362
|
-
const kindPadded = r.kind.padEnd(COL_KIND);
|
|
2363
|
-
const agePadded = r.age.padEnd(COL_AGE);
|
|
2364
|
-
const progressPadded = r.progress.padEnd(COL_PROGRESS);
|
|
2365
|
-
const nameDisplay = r.isBlocked ? pc.yellow(namePadded) : namePadded;
|
|
2366
|
-
const ageDisplay = r.isFocused ? pc.cyan(agePadded) : agePadded;
|
|
2367
|
-
console.log(` ${marker}${nameDisplay} ${kindPadded} ${ageDisplay} ${progressPadded}`);
|
|
2368
|
-
}
|
|
2369
|
-
console.log();
|
|
2370
|
-
console.log(` ${pc.bold("Summary:")} ${statusResult.summary.total} change(s), ${statusResult.summary.focused} focused, ${pc.yellow(String(statusResult.summary.blocked))} blocked`);
|
|
2371
|
-
console.log();
|
|
2372
|
-
printArchiveTrend(statusResult.archiveTrend);
|
|
2373
|
-
if (statusResult.summary.blocked > 0) {
|
|
2374
|
-
const blocked = filteredRecords.filter((r) => r.isBlocked);
|
|
2375
|
-
console.log(` ${pc.yellow("Blocked:")} ${blocked.map((r) => r.name).join(", ")}`);
|
|
2376
|
-
console.log();
|
|
2377
|
-
}
|
|
2378
|
-
return statusResult;
|
|
2379
2621
|
}
|
|
2380
|
-
function buildStatusNextActions(focusedCount,
|
|
2622
|
+
function buildStatusNextActions(focusedCount, records, groups, filtered) {
|
|
2623
|
+
const openChanges = records.map((record) => record.name).sort();
|
|
2381
2624
|
if (focusedCount > 0)
|
|
2382
2625
|
return [];
|
|
2626
|
+
const readyGroup = groups.find((group) => group.readyToClose);
|
|
2627
|
+
if (readyGroup)
|
|
2628
|
+
return [`Run: rsp group close ${readyGroup.name}`];
|
|
2629
|
+
if (openChanges.length === 0 && groups.length > 0)
|
|
2630
|
+
return [`Review Group Brief: ${groups[0].path}`];
|
|
2383
2631
|
if (openChanges.length === 0)
|
|
2384
2632
|
return ["Run: rsp create <name>"];
|
|
2385
2633
|
if (filtered) {
|
|
@@ -2391,38 +2639,165 @@ function buildStatusNextActions(focusedCount, openChanges, filtered) {
|
|
|
2391
2639
|
}
|
|
2392
2640
|
const firstChanges = openChanges.slice(0, 3).join(", ");
|
|
2393
2641
|
const extra = openChanges.length > 3 ? ` (+${openChanges.length - 3} more)` : "";
|
|
2642
|
+
const recommendation = recommendOpenChange(records, groups);
|
|
2394
2643
|
return [
|
|
2395
2644
|
`Open changes: ${firstChanges}${extra}`,
|
|
2396
|
-
`Run: rsp focus ${
|
|
2645
|
+
recommendation ? `Run: rsp focus ${recommendation}` : "Resolve blockers before focusing an open change.",
|
|
2397
2646
|
"Or run: rsp create <name>"
|
|
2398
2647
|
];
|
|
2399
2648
|
}
|
|
2400
|
-
|
|
2401
|
-
const
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
const
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2649
|
+
function recommendOpenChange(records, groups) {
|
|
2650
|
+
const recordsByName = new Map(records.map((record) => [record.name, record]));
|
|
2651
|
+
const candidates = records.filter((record) => !record.name.includes("/") && !record.isBlocked).map((record) => ({ name: record.name, owner: record.name }));
|
|
2652
|
+
for (const group of groups) {
|
|
2653
|
+
if (group.blockers)
|
|
2654
|
+
continue;
|
|
2655
|
+
const slice = group.slices.find((candidate) => {
|
|
2656
|
+
const record = recordsByName.get(candidate.name);
|
|
2657
|
+
return candidate.state === "open" && record !== void 0 && !record.isBlocked;
|
|
2658
|
+
});
|
|
2659
|
+
if (slice)
|
|
2660
|
+
candidates.push({ name: slice.name, owner: group.name });
|
|
2661
|
+
}
|
|
2662
|
+
return candidates.sort((left, right) => left.owner.localeCompare(right.owner) || left.name.localeCompare(right.name))[0]?.name ?? null;
|
|
2663
|
+
}
|
|
2664
|
+
function filterDependencyPlan(plan, names) {
|
|
2665
|
+
const included = new Set(names);
|
|
2666
|
+
let changed = true;
|
|
2667
|
+
while (changed) {
|
|
2668
|
+
changed = false;
|
|
2669
|
+
for (const edge of plan.edges) {
|
|
2670
|
+
if (included.has(edge.change) && !included.has(edge.requires)) {
|
|
2671
|
+
included.add(edge.requires);
|
|
2672
|
+
changed = true;
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
const waves = plan.waves.map((wave) => wave.filter((name) => included.has(name))).filter((wave) => wave.length > 0);
|
|
2677
|
+
return {
|
|
2678
|
+
nodes: plan.nodes.filter((node) => included.has(node.name)).map((node) => ({ ...node, selection: names.has(node.name) ? "selected" : "prerequisite" })),
|
|
2679
|
+
ready: waves[0] ?? [],
|
|
2680
|
+
edges: plan.edges.filter((edge) => included.has(edge.change)),
|
|
2681
|
+
blocked: plan.blocked.filter((blocker) => included.has(blocker.change)),
|
|
2682
|
+
waves
|
|
2683
|
+
};
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
// src/status/plain.ts
|
|
2687
|
+
var COL_CHANGE = 32;
|
|
2688
|
+
var COL_KIND = 10;
|
|
2689
|
+
var COL_AGE = 7;
|
|
2690
|
+
var COL_PROGRESS = 10;
|
|
2691
|
+
function printStatusRuntimeDiagnostics(runtime) {
|
|
2692
|
+
for (const diagnostic of runtime)
|
|
2693
|
+
console.error(` ${pc.dim(`[verbose] ${diagnostic.operation} ${diagnostic.path}: ${diagnostic.message}`)}`);
|
|
2694
|
+
}
|
|
2695
|
+
function printStatusPlain(view) {
|
|
2696
|
+
console.log();
|
|
2697
|
+
console.log(` ${pc.bold("RSP status")}`);
|
|
2698
|
+
console.log();
|
|
2699
|
+
for (const diagnostic of view.diagnostics) {
|
|
2700
|
+
const label = diagnostic.change ?? diagnostic.path;
|
|
2701
|
+
console.log(` ${pc.red("\u2717")} ${label ? `${label} \u2014 ` : ""}${diagnostic.message}`);
|
|
2702
|
+
}
|
|
2703
|
+
if (view.diagnostics.length > 0)
|
|
2704
|
+
console.log();
|
|
2705
|
+
if (view.focused.length > 0) {
|
|
2706
|
+
console.log(` ${pc.cyan("Focused:")} ${view.focused.join(", ")}`);
|
|
2707
|
+
console.log();
|
|
2708
|
+
} else {
|
|
2709
|
+
console.log(` ${pc.dim("No focused change.")}`);
|
|
2710
|
+
for (const action of view.nextActions)
|
|
2711
|
+
console.log(` ${pc.dim(action)}`);
|
|
2712
|
+
console.log();
|
|
2713
|
+
}
|
|
2714
|
+
printChangeGroups(view.groups);
|
|
2715
|
+
printDependencyPlan(view.plan, view.records);
|
|
2716
|
+
if (!view.hasExecutableChanges) {
|
|
2717
|
+
if (view.ok)
|
|
2718
|
+
console.log(` ${pc.dim("No executable changes found.")} ${view.groups.length === 0 ? "Run: rsp create <name>" : "Review the Change Group guidance above."}
|
|
2719
|
+
`);
|
|
2720
|
+
else
|
|
2721
|
+
console.log(` ${pc.dim("No executable changes can be shown until the work-tree issues are resolved.")}
|
|
2722
|
+
`);
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
if (view.records.length === 0) {
|
|
2726
|
+
console.log(` ${pc.dim("No changes match the current filters.")}
|
|
2727
|
+
`);
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
if ((process.stdout.columns ?? 100) < 88 || view.records.some((record) => record.output.name.length >= COL_CHANGE)) {
|
|
2731
|
+
for (const { output: record, progressKnown } of view.records) {
|
|
2732
|
+
const state = [record.isFocused ? "focused" : "open", record.isBlocked ? "blocked" : null].filter(Boolean).join(" \xB7 ");
|
|
2733
|
+
console.log(` ${pc.bold("Change:")} ${record.isBlocked ? pc.yellow(record.name) : record.name}`);
|
|
2734
|
+
console.log(` ${state} \xB7 ${record.kind} \xB7 age ${record.ageDays ?? "\u2014"}d \xB7 progress ${progressKnown ? `${record.progress.done}/${record.progress.total}` : "\u2014"}`);
|
|
2735
|
+
}
|
|
2736
|
+
} else {
|
|
2737
|
+
const pad = (value, width) => value.padEnd(width);
|
|
2738
|
+
console.log(` ${pad("Change", COL_CHANGE)} ${pad("Kind", COL_KIND)} ${pad("Age(d)", COL_AGE)} ${pad("Progress", COL_PROGRESS)}`);
|
|
2739
|
+
console.log(` ${"\u2500".repeat(COL_CHANGE)} ${"\u2500".repeat(COL_KIND)} ${"\u2500".repeat(COL_AGE)} ${"\u2500".repeat(COL_PROGRESS)}`);
|
|
2740
|
+
for (const { output: record, progressKnown } of view.records) {
|
|
2741
|
+
const marker = record.isFocused ? pc.cyan("*") : " ";
|
|
2742
|
+
const namePadded = record.name.padEnd(COL_CHANGE - 1);
|
|
2743
|
+
const kindPadded = record.kind.padEnd(COL_KIND);
|
|
2744
|
+
const agePadded = (record.ageDays === null ? "\u2014" : String(record.ageDays)).padEnd(COL_AGE);
|
|
2745
|
+
const progressPadded = (progressKnown ? `${record.progress.done}/${record.progress.total}` : "\u2014").padEnd(COL_PROGRESS);
|
|
2746
|
+
const nameDisplay = record.isBlocked ? pc.yellow(namePadded) : namePadded;
|
|
2747
|
+
const ageDisplay = record.isFocused ? pc.cyan(agePadded) : agePadded;
|
|
2748
|
+
console.log(` ${marker}${nameDisplay} ${kindPadded} ${ageDisplay} ${progressPadded}`);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
console.log();
|
|
2752
|
+
console.log(` ${pc.bold("Summary:")} ${view.summary.total} change(s), ${view.summary.focused} focused, ${pc.yellow(String(view.summary.blocked))} blocked`);
|
|
2753
|
+
console.log();
|
|
2754
|
+
printArchiveTrend(view.archiveTrend);
|
|
2755
|
+
}
|
|
2756
|
+
function printChangeGroups(groups) {
|
|
2757
|
+
if (groups.length === 0)
|
|
2758
|
+
return;
|
|
2759
|
+
console.log(` ${pc.bold("Change Groups")}`);
|
|
2760
|
+
for (const group of groups) {
|
|
2761
|
+
const archived = group.slices.filter((slice) => slice.state === "archived").length;
|
|
2762
|
+
const readiness = group.readyToClose ? pc.green("ready to close") : pc.dim("not ready");
|
|
2763
|
+
const blocked = group.blockers ? ` ${pc.yellow("blocked")}` : "";
|
|
2764
|
+
console.log(` ${group.name}: ${archived}/${group.slices.length} archived, completion ${group.completion.done}/${group.completion.total}, ${readiness}${blocked}`);
|
|
2425
2765
|
}
|
|
2766
|
+
console.log();
|
|
2767
|
+
}
|
|
2768
|
+
function printDependencyPlan(plan, records) {
|
|
2769
|
+
console.log(` ${pc.bold("Dependency graph")}`);
|
|
2770
|
+
console.log(` ${pc.dim("(parent requires children)")}`);
|
|
2771
|
+
const roots = projectDependencyForest(plan);
|
|
2772
|
+
const detailColumn = Math.max(40, ...plan.nodes.map((node) => node.name.length + 8));
|
|
2773
|
+
const renderNode = (node, prefix, last) => {
|
|
2774
|
+
const connector = last === null ? "" : last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
2775
|
+
const symbol = node.selection === "selected" ? "\u25CE" : node.state === "ready" ? "\u25CF" : node.state === "archived" ? "\u2713" : node.state === "missing" || node.state === "blocked" ? "!" : "\u25CB";
|
|
2776
|
+
const identity = `${prefix}${connector}${symbol} ${node.name}`.padEnd(detailColumn);
|
|
2777
|
+
const record = records.find((candidate) => candidate.output.name === node.name);
|
|
2778
|
+
const ownership = node.selection === "prerequisite" ? "prerequisite" : record?.output.isFocused ? "focused" : "open";
|
|
2779
|
+
const state = node.state === "archived" ? "resolved" : node.state;
|
|
2780
|
+
console.log(` ${identity} ${ownership} \xB7 ${state}${node.shared ? " \xB7 \u21A9 shared" : ""}${node.reason ? ` \u2014 ${node.reason}` : ""}`);
|
|
2781
|
+
const childPrefix = prefix + (last === null ? "" : last ? " " : "\u2502 ");
|
|
2782
|
+
for (const [index, child] of node.children.entries())
|
|
2783
|
+
renderNode(child, childPrefix, index === node.children.length - 1);
|
|
2784
|
+
};
|
|
2785
|
+
if (roots.length === 0)
|
|
2786
|
+
console.log(` ${pc.dim("none")}`);
|
|
2787
|
+
else
|
|
2788
|
+
roots.forEach((node) => renderNode(node, "", null));
|
|
2789
|
+
const external = plan.blocked.filter((blocker) => blocker.external).map((blocker) => blocker.change);
|
|
2790
|
+
if (external.length > 0)
|
|
2791
|
+
console.log(` ${pc.dim("External blockers:")} ${external.join(", ")}`);
|
|
2792
|
+
const implementationReady = plan.ready.filter((name) => {
|
|
2793
|
+
const record = records.find((candidate) => candidate.output.name === name);
|
|
2794
|
+
return !record || record.readiness.incompleteTasks > 0 || record.readiness.incompleteVerify > 0;
|
|
2795
|
+
});
|
|
2796
|
+
const completedReady = plan.ready.filter((name) => !implementationReady.includes(name));
|
|
2797
|
+
const next = implementationReady.length > 0 ? implementationReady.join(", ") : completedReady.length > 0 ? `review for durable update/archive: ${completedReady.join(", ")}` : pc.dim("none");
|
|
2798
|
+
console.log(` ${pc.dim("Next action:")} ${next}`);
|
|
2799
|
+
console.log(` ${pc.dim("Legend:")} \u25CE focused/open \u25CF ready \u25CB waiting \u2713 resolved prerequisite ! blocked`);
|
|
2800
|
+
console.log();
|
|
2426
2801
|
}
|
|
2427
2802
|
function printArchiveTrend(trend) {
|
|
2428
2803
|
if (trend.length === 0)
|
|
@@ -2433,7 +2808,102 @@ function printArchiveTrend(trend) {
|
|
|
2433
2808
|
console.log();
|
|
2434
2809
|
}
|
|
2435
2810
|
|
|
2811
|
+
// src/status/v3-json.ts
|
|
2812
|
+
function toStatusJson(view) {
|
|
2813
|
+
return {
|
|
2814
|
+
command: "status",
|
|
2815
|
+
ok: view.ok,
|
|
2816
|
+
filters: view.query,
|
|
2817
|
+
focused: view.focused,
|
|
2818
|
+
records: view.records.map((record) => record.output),
|
|
2819
|
+
groups: view.groups,
|
|
2820
|
+
plan: view.plan,
|
|
2821
|
+
summary: view.summary,
|
|
2822
|
+
nextActions: view.nextActions,
|
|
2823
|
+
archiveTrend: view.archiveTrend,
|
|
2824
|
+
diagnostics: view.diagnostics,
|
|
2825
|
+
runtime: view.runtime
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
function toStatusJsonError(error, options) {
|
|
2829
|
+
return {
|
|
2830
|
+
command: "status",
|
|
2831
|
+
ok: false,
|
|
2832
|
+
filters: {
|
|
2833
|
+
focused: options.focused,
|
|
2834
|
+
blocked: options.blocked,
|
|
2835
|
+
stale: null
|
|
2836
|
+
},
|
|
2837
|
+
focused: [],
|
|
2838
|
+
records: [],
|
|
2839
|
+
groups: [],
|
|
2840
|
+
plan: {
|
|
2841
|
+
nodes: [],
|
|
2842
|
+
ready: [],
|
|
2843
|
+
edges: [],
|
|
2844
|
+
blocked: [],
|
|
2845
|
+
waves: []
|
|
2846
|
+
},
|
|
2847
|
+
summary: {
|
|
2848
|
+
total: 0,
|
|
2849
|
+
focused: 0,
|
|
2850
|
+
blocked: 0
|
|
2851
|
+
},
|
|
2852
|
+
archiveTrend: [],
|
|
2853
|
+
nextActions: [],
|
|
2854
|
+
diagnostics: [],
|
|
2855
|
+
runtime: [],
|
|
2856
|
+
error
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
// src/commands/status.ts
|
|
2861
|
+
async function showStatus(options = {}, runOptions = {}) {
|
|
2862
|
+
const snapshot = await inspectProjectStatus();
|
|
2863
|
+
if (runOptions.verbose && !runOptions.json)
|
|
2864
|
+
printStatusRuntimeDiagnostics(snapshot.runtime);
|
|
2865
|
+
const view = deriveStatusView(snapshot, options);
|
|
2866
|
+
const result = toStatusJson(view);
|
|
2867
|
+
if (runOptions.json)
|
|
2868
|
+
emitJson(result, runOptions);
|
|
2869
|
+
else
|
|
2870
|
+
printStatusPlain(view);
|
|
2871
|
+
return result;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
// src/tui/route.ts
|
|
2875
|
+
function isInteractiveTerminal(environment) {
|
|
2876
|
+
return environment.stdinTty && environment.stdoutTty && environment.term !== "dumb";
|
|
2877
|
+
}
|
|
2878
|
+
function shouldAutoLaunchUi(args, environment) {
|
|
2879
|
+
return args.length === 0 && isInteractiveTerminal(environment) && (environment.ci === void 0 || environment.ci === "false");
|
|
2880
|
+
}
|
|
2881
|
+
function validateUiArgs(args) {
|
|
2882
|
+
let lang = "auto";
|
|
2883
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2884
|
+
const argument = args[index];
|
|
2885
|
+
if (argument !== "--lang")
|
|
2886
|
+
throw new Error(`Unknown rsp ui option: ${argument}`);
|
|
2887
|
+
const value = args[index + 1];
|
|
2888
|
+
if (value !== "auto" && value !== "en" && value !== "zh-CN")
|
|
2889
|
+
throw new Error("--lang must be auto, en, or zh-CN");
|
|
2890
|
+
lang = value;
|
|
2891
|
+
index += 1;
|
|
2892
|
+
}
|
|
2893
|
+
return { lang };
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2436
2896
|
// src/cli.ts
|
|
2897
|
+
var COMPACT_JSON_COMMANDS = /* @__PURE__ */ new Set(["status", "show", "ready", "check", "doctor", "history"]);
|
|
2898
|
+
function validateCompactInvocation(rawArgs) {
|
|
2899
|
+
if (!rawArgs.includes("--compact"))
|
|
2900
|
+
return;
|
|
2901
|
+
const command = rawArgs[0];
|
|
2902
|
+
if (!command || !COMPACT_JSON_COMMANDS.has(command))
|
|
2903
|
+
throw new Error(`--compact is unsupported for rsp${command ? ` ${command}` : ""}`);
|
|
2904
|
+
if (!rawArgs.includes("--json"))
|
|
2905
|
+
throw new Error("--compact requires --json");
|
|
2906
|
+
}
|
|
2437
2907
|
var initCommand = defineCommand({
|
|
2438
2908
|
meta: {
|
|
2439
2909
|
name: "init",
|
|
@@ -2445,11 +2915,6 @@ var initCommand = defineCommand({
|
|
|
2445
2915
|
description: "How to handle AGENTS.md output: managed or print",
|
|
2446
2916
|
default: "managed"
|
|
2447
2917
|
},
|
|
2448
|
-
"with-project-rules": {
|
|
2449
|
-
type: "boolean",
|
|
2450
|
-
description: "Create .rsp/rules/project-rules.md",
|
|
2451
|
-
default: false
|
|
2452
|
-
},
|
|
2453
2918
|
"with-project-setup": {
|
|
2454
2919
|
type: "boolean",
|
|
2455
2920
|
description: "Create .rsp/changes/project-setup.md and focus it",
|
|
@@ -2459,7 +2924,6 @@ var initCommand = defineCommand({
|
|
|
2459
2924
|
async run({ args }) {
|
|
2460
2925
|
await initProject({
|
|
2461
2926
|
agentsMode: args["agents-mode"] === "print" ? "print" : "managed",
|
|
2462
|
-
withProjectRules: Boolean(args["with-project-rules"]),
|
|
2463
2927
|
withProjectSetup: Boolean(args["with-project-setup"])
|
|
2464
2928
|
});
|
|
2465
2929
|
}
|
|
@@ -2490,20 +2954,46 @@ var createCommand = defineCommand({
|
|
|
2490
2954
|
await createChange(args.name, summary, args.kind, { lite: Boolean(args.lite) });
|
|
2491
2955
|
}
|
|
2492
2956
|
});
|
|
2493
|
-
var
|
|
2957
|
+
var groupCreateCommand = defineCommand({
|
|
2494
2958
|
meta: {
|
|
2495
|
-
name: "
|
|
2496
|
-
description: "Create .rsp/
|
|
2959
|
+
name: "create",
|
|
2960
|
+
description: "Create an unfocused .rsp/changes/<group>/00-brief.md"
|
|
2497
2961
|
},
|
|
2498
2962
|
args: {
|
|
2499
2963
|
name: {
|
|
2500
2964
|
type: "positional",
|
|
2501
|
-
description: "
|
|
2965
|
+
description: "Change Group name",
|
|
2502
2966
|
required: true
|
|
2503
2967
|
}
|
|
2504
2968
|
},
|
|
2505
2969
|
async run({ args }) {
|
|
2506
|
-
|
|
2970
|
+
const goal = Array.isArray(args._) && args._.length > 1 ? args._.slice(1).join(" ") : "";
|
|
2971
|
+
await createChangeGroup(args.name, goal);
|
|
2972
|
+
}
|
|
2973
|
+
});
|
|
2974
|
+
var groupCommand = defineCommand({
|
|
2975
|
+
meta: {
|
|
2976
|
+
name: "group",
|
|
2977
|
+
description: "Create and close shallow Change Groups"
|
|
2978
|
+
},
|
|
2979
|
+
subCommands: {
|
|
2980
|
+
create: groupCreateCommand,
|
|
2981
|
+
close: defineCommand({
|
|
2982
|
+
meta: {
|
|
2983
|
+
name: "close",
|
|
2984
|
+
description: "Archive a completed Group Brief after all child Changes are archived"
|
|
2985
|
+
},
|
|
2986
|
+
args: {
|
|
2987
|
+
name: {
|
|
2988
|
+
type: "positional",
|
|
2989
|
+
description: "Change Group name",
|
|
2990
|
+
required: true
|
|
2991
|
+
}
|
|
2992
|
+
},
|
|
2993
|
+
async run({ args }) {
|
|
2994
|
+
await closeChangeGroup(args.name);
|
|
2995
|
+
}
|
|
2996
|
+
})
|
|
2507
2997
|
}
|
|
2508
2998
|
});
|
|
2509
2999
|
var addSpecCommand = defineCommand({
|
|
@@ -2525,10 +3015,9 @@ var addSpecCommand = defineCommand({
|
|
|
2525
3015
|
var addCommand = defineCommand({
|
|
2526
3016
|
meta: {
|
|
2527
3017
|
name: "add",
|
|
2528
|
-
description: "Add optional
|
|
3018
|
+
description: "Add an optional durable spec file"
|
|
2529
3019
|
},
|
|
2530
3020
|
subCommands: {
|
|
2531
|
-
rules: addRulesCommand,
|
|
2532
3021
|
spec: addSpecCommand
|
|
2533
3022
|
}
|
|
2534
3023
|
});
|
|
@@ -2610,6 +3099,11 @@ var statusCommand = defineCommand({
|
|
|
2610
3099
|
description: "Print machine-readable JSON output",
|
|
2611
3100
|
default: false
|
|
2612
3101
|
},
|
|
3102
|
+
compact: {
|
|
3103
|
+
type: "boolean",
|
|
3104
|
+
description: "Print JSON without indentation (requires --json)",
|
|
3105
|
+
default: false
|
|
3106
|
+
},
|
|
2613
3107
|
verbose: {
|
|
2614
3108
|
type: "boolean",
|
|
2615
3109
|
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
@@ -2618,28 +3112,31 @@ var statusCommand = defineCommand({
|
|
|
2618
3112
|
},
|
|
2619
3113
|
async run({ args }) {
|
|
2620
3114
|
const stale = args.stale === void 0 ? void 0 : Number(args.stale);
|
|
2621
|
-
if (
|
|
3115
|
+
if (stale !== void 0 && (Number.isFinite(stale) === false || Number.isInteger(stale) === false || stale < 0)) {
|
|
2622
3116
|
if (args.json) {
|
|
2623
|
-
|
|
3117
|
+
emitJson(toStatusJsonError({
|
|
2624
3118
|
code: "invalid_stale_filter",
|
|
2625
3119
|
message: "--stale must be a non-negative integer number of days"
|
|
2626
3120
|
}, {
|
|
2627
3121
|
focused: Boolean(args.focused),
|
|
2628
3122
|
blocked: Boolean(args.blocked)
|
|
2629
|
-
});
|
|
3123
|
+
}), { compact: Boolean(args.compact) });
|
|
2630
3124
|
} else {
|
|
2631
3125
|
console.error(` Error: --stale must be a non-negative integer number of days`);
|
|
2632
3126
|
}
|
|
2633
3127
|
process.exit(1);
|
|
2634
3128
|
}
|
|
2635
|
-
await showStatus({
|
|
3129
|
+
const result = await showStatus({
|
|
2636
3130
|
focused: Boolean(args.focused),
|
|
2637
3131
|
blocked: Boolean(args.blocked),
|
|
2638
3132
|
stale
|
|
2639
3133
|
}, {
|
|
2640
3134
|
json: Boolean(args.json),
|
|
3135
|
+
compact: Boolean(args.compact),
|
|
2641
3136
|
verbose: Boolean(args.verbose)
|
|
2642
3137
|
});
|
|
3138
|
+
if (!result.ok)
|
|
3139
|
+
process.exit(1);
|
|
2643
3140
|
}
|
|
2644
3141
|
});
|
|
2645
3142
|
var checkCommand = defineCommand({
|
|
@@ -2658,6 +3155,11 @@ var checkCommand = defineCommand({
|
|
|
2658
3155
|
description: "Print machine-readable JSON output",
|
|
2659
3156
|
default: false
|
|
2660
3157
|
},
|
|
3158
|
+
compact: {
|
|
3159
|
+
type: "boolean",
|
|
3160
|
+
description: "Print JSON without indentation (requires --json)",
|
|
3161
|
+
default: false
|
|
3162
|
+
},
|
|
2661
3163
|
verbose: {
|
|
2662
3164
|
type: "boolean",
|
|
2663
3165
|
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
@@ -2668,6 +3170,7 @@ var checkCommand = defineCommand({
|
|
|
2668
3170
|
const result = await runCheck({
|
|
2669
3171
|
focused: Boolean(args.focused),
|
|
2670
3172
|
json: Boolean(args.json),
|
|
3173
|
+
compact: Boolean(args.compact),
|
|
2671
3174
|
verbose: Boolean(args.verbose)
|
|
2672
3175
|
});
|
|
2673
3176
|
if (!result.ok)
|
|
@@ -2677,10 +3180,46 @@ var checkCommand = defineCommand({
|
|
|
2677
3180
|
var updateCommand = defineCommand({
|
|
2678
3181
|
meta: {
|
|
2679
3182
|
name: "update",
|
|
2680
|
-
description: "Refresh RSP project structure after upgrade (
|
|
3183
|
+
description: "Refresh RSP project structure after upgrade (fallback protocol, AGENTS, indices)"
|
|
2681
3184
|
},
|
|
2682
3185
|
async run() {
|
|
2683
|
-
await updateProject();
|
|
3186
|
+
const result = await updateProject();
|
|
3187
|
+
if (!result.migration.inspectionComplete)
|
|
3188
|
+
process.exit(1);
|
|
3189
|
+
}
|
|
3190
|
+
});
|
|
3191
|
+
var skillsInstallCommand = defineCommand({
|
|
3192
|
+
meta: {
|
|
3193
|
+
name: "install",
|
|
3194
|
+
description: "Install this package's bundled Skills into .agents/skills"
|
|
3195
|
+
},
|
|
3196
|
+
args: {
|
|
3197
|
+
"dry-run": {
|
|
3198
|
+
type: "boolean",
|
|
3199
|
+
description: "Preflight all packaged Skills without changing files",
|
|
3200
|
+
default: false
|
|
3201
|
+
},
|
|
3202
|
+
"force": {
|
|
3203
|
+
type: "boolean",
|
|
3204
|
+
description: "Replace divergent package-owned Skill directories",
|
|
3205
|
+
default: false
|
|
3206
|
+
}
|
|
3207
|
+
},
|
|
3208
|
+
async run({ args }) {
|
|
3209
|
+
const result = await installPackagedSkills({
|
|
3210
|
+
dryRun: Boolean(args["dry-run"]),
|
|
3211
|
+
force: Boolean(args.force)
|
|
3212
|
+
});
|
|
3213
|
+
printSkillInstallResult(result, Boolean(args["dry-run"]));
|
|
3214
|
+
}
|
|
3215
|
+
});
|
|
3216
|
+
var skillsCommand = defineCommand({
|
|
3217
|
+
meta: {
|
|
3218
|
+
name: "skills",
|
|
3219
|
+
description: "Manage package-bundled project Skills"
|
|
3220
|
+
},
|
|
3221
|
+
subCommands: {
|
|
3222
|
+
install: skillsInstallCommand
|
|
2684
3223
|
}
|
|
2685
3224
|
});
|
|
2686
3225
|
var doctorCommand = defineCommand({
|
|
@@ -2694,6 +3233,11 @@ var doctorCommand = defineCommand({
|
|
|
2694
3233
|
description: "Print machine-readable JSON output",
|
|
2695
3234
|
default: false
|
|
2696
3235
|
},
|
|
3236
|
+
compact: {
|
|
3237
|
+
type: "boolean",
|
|
3238
|
+
description: "Print JSON without indentation (requires --json)",
|
|
3239
|
+
default: false
|
|
3240
|
+
},
|
|
2697
3241
|
verbose: {
|
|
2698
3242
|
type: "boolean",
|
|
2699
3243
|
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
@@ -2706,7 +3250,7 @@ var doctorCommand = defineCommand({
|
|
|
2706
3250
|
}
|
|
2707
3251
|
},
|
|
2708
3252
|
async run({ args }) {
|
|
2709
|
-
const result = await runDoctor({ json: Boolean(args.json), verbose: Boolean(args.verbose), fix: Boolean(args.fix) });
|
|
3253
|
+
const result = await runDoctor({ json: Boolean(args.json), compact: Boolean(args.compact), verbose: Boolean(args.verbose), fix: Boolean(args.fix) });
|
|
2710
3254
|
if (!result.ok)
|
|
2711
3255
|
process.exit(1);
|
|
2712
3256
|
}
|
|
@@ -2727,6 +3271,11 @@ var readyCommand = defineCommand({
|
|
|
2727
3271
|
description: "Print machine-readable JSON output",
|
|
2728
3272
|
default: false
|
|
2729
3273
|
},
|
|
3274
|
+
compact: {
|
|
3275
|
+
type: "boolean",
|
|
3276
|
+
description: "Print JSON without indentation (requires --json)",
|
|
3277
|
+
default: false
|
|
3278
|
+
},
|
|
2730
3279
|
verbose: {
|
|
2731
3280
|
type: "boolean",
|
|
2732
3281
|
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
@@ -2734,7 +3283,7 @@ var readyCommand = defineCommand({
|
|
|
2734
3283
|
}
|
|
2735
3284
|
},
|
|
2736
3285
|
async run({ args }) {
|
|
2737
|
-
await showReady(args.name, { json: Boolean(args.json), verbose: Boolean(args.verbose) });
|
|
3286
|
+
await showReady(args.name, { json: Boolean(args.json), compact: Boolean(args.compact), verbose: Boolean(args.verbose) });
|
|
2738
3287
|
}
|
|
2739
3288
|
});
|
|
2740
3289
|
var showCommand = defineCommand({
|
|
@@ -2758,6 +3307,11 @@ var showCommand = defineCommand({
|
|
|
2758
3307
|
description: "Print machine-readable JSON output",
|
|
2759
3308
|
default: false
|
|
2760
3309
|
},
|
|
3310
|
+
compact: {
|
|
3311
|
+
type: "boolean",
|
|
3312
|
+
description: "Print JSON without indentation (requires --json)",
|
|
3313
|
+
default: false
|
|
3314
|
+
},
|
|
2761
3315
|
verbose: {
|
|
2762
3316
|
type: "boolean",
|
|
2763
3317
|
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
@@ -2768,43 +3322,131 @@ var showCommand = defineCommand({
|
|
|
2768
3322
|
await showChange(args.name, {
|
|
2769
3323
|
focused: Boolean(args.focused),
|
|
2770
3324
|
json: Boolean(args.json),
|
|
3325
|
+
compact: Boolean(args.compact),
|
|
2771
3326
|
verbose: Boolean(args.verbose)
|
|
2772
3327
|
});
|
|
2773
3328
|
}
|
|
2774
3329
|
});
|
|
3330
|
+
var historyCommand = defineCommand({
|
|
3331
|
+
meta: {
|
|
3332
|
+
name: "history",
|
|
3333
|
+
description: "Query bounded archived Change summaries or one exact archived WorkRef"
|
|
3334
|
+
},
|
|
3335
|
+
args: {
|
|
3336
|
+
name: {
|
|
3337
|
+
type: "positional",
|
|
3338
|
+
description: "Optional exact archived Change WorkRef for detail",
|
|
3339
|
+
required: false
|
|
3340
|
+
},
|
|
3341
|
+
limit: {
|
|
3342
|
+
type: "string",
|
|
3343
|
+
description: "Maximum list records from 1 through 100 (default: 20)"
|
|
3344
|
+
},
|
|
3345
|
+
since: {
|
|
3346
|
+
type: "string",
|
|
3347
|
+
description: "Include archives on or after YYYY-MM-DD"
|
|
3348
|
+
},
|
|
3349
|
+
until: {
|
|
3350
|
+
type: "string",
|
|
3351
|
+
description: "Include archives on or before YYYY-MM-DD"
|
|
3352
|
+
},
|
|
3353
|
+
kind: {
|
|
3354
|
+
type: "string",
|
|
3355
|
+
description: "Match one exact historical Change kind"
|
|
3356
|
+
},
|
|
3357
|
+
group: {
|
|
3358
|
+
type: "string",
|
|
3359
|
+
description: "Match one exact Change Group"
|
|
3360
|
+
},
|
|
3361
|
+
json: {
|
|
3362
|
+
type: "boolean",
|
|
3363
|
+
description: "Print machine-readable JSON output",
|
|
3364
|
+
default: false
|
|
3365
|
+
},
|
|
3366
|
+
compact: {
|
|
3367
|
+
type: "boolean",
|
|
3368
|
+
description: "Print JSON without indentation (requires --json)",
|
|
3369
|
+
default: false
|
|
3370
|
+
}
|
|
3371
|
+
},
|
|
3372
|
+
async run({ args }) {
|
|
3373
|
+
const result = await showHistory({
|
|
3374
|
+
workRef: args.name,
|
|
3375
|
+
limit: args.limit,
|
|
3376
|
+
since: args.since,
|
|
3377
|
+
until: args.until,
|
|
3378
|
+
kind: args.kind,
|
|
3379
|
+
group: args.group,
|
|
3380
|
+
positionalCount: args._?.length ?? (args.name ? 1 : 0)
|
|
3381
|
+
}, { json: Boolean(args.json), compact: Boolean(args.compact) });
|
|
3382
|
+
if (!result.ok)
|
|
3383
|
+
process.exitCode = 1;
|
|
3384
|
+
}
|
|
3385
|
+
});
|
|
2775
3386
|
async function runCli(rawArgs = process.argv.slice(2)) {
|
|
3387
|
+
const terminalEnvironment = {
|
|
3388
|
+
stdinTty: Boolean(process.stdin.isTTY),
|
|
3389
|
+
stdoutTty: Boolean(process.stdout.isTTY),
|
|
3390
|
+
term: process.env.TERM,
|
|
3391
|
+
ci: process.env.CI
|
|
3392
|
+
};
|
|
3393
|
+
const explicitUi = rawArgs[0] === "ui";
|
|
3394
|
+
if (explicitUi || shouldAutoLaunchUi(rawArgs, terminalEnvironment)) {
|
|
3395
|
+
const { lang } = validateUiArgs(explicitUi ? rawArgs.slice(1) : []);
|
|
3396
|
+
if (!isInteractiveTerminal(terminalEnvironment)) {
|
|
3397
|
+
if (explicitUi)
|
|
3398
|
+
throw new Error("rsp ui requires an interactive terminal; use rsp status or rsp status --json instead");
|
|
3399
|
+
} else {
|
|
3400
|
+
const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
|
|
3401
|
+
const locale = resolveUiLocale(lang, process.env.RSP_UI_LANG, process.env.LC_ALL ?? process.env.LC_MESSAGES ?? process.env.LANG ?? Intl.DateTimeFormat().resolvedOptions().locale);
|
|
3402
|
+
const { runTui } = await import("./entry-LAPR3TNI.mjs");
|
|
3403
|
+
const exitCode = await runTui(locale);
|
|
3404
|
+
if (exitCode !== 0)
|
|
3405
|
+
process.exitCode = exitCode;
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
validateCompactInvocation(rawArgs);
|
|
2776
3410
|
const version = await getVersion();
|
|
2777
3411
|
const main = defineCommand({
|
|
2778
3412
|
meta: {
|
|
2779
3413
|
name: "rsp",
|
|
2780
3414
|
version,
|
|
2781
|
-
description: "RSP (
|
|
3415
|
+
description: "RSP (Reliable Software Practice), a repository-native engineering workflow for humans and AI agents"
|
|
2782
3416
|
},
|
|
2783
3417
|
subCommands: {
|
|
2784
3418
|
init: initCommand,
|
|
2785
3419
|
add: addCommand,
|
|
2786
3420
|
create: createCommand,
|
|
3421
|
+
group: groupCommand,
|
|
2787
3422
|
focus: focusCommand,
|
|
2788
3423
|
unfocus: unfocusCommand,
|
|
2789
3424
|
archive: archiveCommand,
|
|
2790
3425
|
ready: readyCommand,
|
|
2791
3426
|
show: showCommand,
|
|
3427
|
+
history: historyCommand,
|
|
2792
3428
|
status: statusCommand,
|
|
2793
3429
|
check: checkCommand,
|
|
2794
3430
|
update: updateCommand,
|
|
2795
|
-
doctor: doctorCommand
|
|
3431
|
+
doctor: doctorCommand,
|
|
3432
|
+
skills: skillsCommand
|
|
2796
3433
|
}
|
|
2797
3434
|
});
|
|
2798
3435
|
await runMain(main, { rawArgs });
|
|
2799
3436
|
}
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
3437
|
+
async function runCliMain(rawArgs = process.argv.slice(2)) {
|
|
3438
|
+
try {
|
|
3439
|
+
await runCli(rawArgs);
|
|
3440
|
+
} catch (error) {
|
|
3441
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2804
3442
|
console.error(` Error: ${message}`);
|
|
2805
|
-
process.
|
|
2806
|
-
}
|
|
3443
|
+
process.exitCode = 1;
|
|
3444
|
+
}
|
|
2807
3445
|
}
|
|
3446
|
+
var isMain = process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
3447
|
+
if (isMain)
|
|
3448
|
+
await runCliMain();
|
|
2808
3449
|
export {
|
|
2809
|
-
runCli
|
|
3450
|
+
runCli,
|
|
3451
|
+
runCliMain
|
|
2810
3452
|
};
|