@oevery/rsp 2.0.3 → 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 +214 -61
- package/README.zh-CN.md +196 -59
- package/bin/rsp.mjs +2 -2
- package/dist/chunk-QLXIDVKK.mjs +2618 -0
- package/dist/cli.mjs +2466 -1230
- 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 -95
- package/skills/rsp/SKILL.md +164 -119
- 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,609 +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) {
|
|
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
|
-
const template = getChangeTemplateByKind(kind);
|
|
188
|
-
return `---
|
|
189
|
-
kind: "${frontmatterKind}"
|
|
190
|
-
---
|
|
191
|
-
|
|
192
|
-
# Change: ${name}
|
|
193
|
-
|
|
194
|
-
## Proposal
|
|
195
|
-
- Summary: ${proposalSummary}
|
|
196
|
-
- Why:
|
|
197
|
-
- ${template.why}
|
|
198
|
-
- Scope:
|
|
199
|
-
- ${template.scope}
|
|
200
|
-
- Non-goals:
|
|
201
|
-
- ${template.nonGoals}
|
|
202
|
-
|
|
203
|
-
## Spec
|
|
204
|
-
${template.specSection}
|
|
205
|
-
|
|
206
|
-
### Acceptance
|
|
207
|
-
${template.acceptanceSection}
|
|
208
|
-
|
|
209
|
-
## Design
|
|
210
|
-
- Approach:
|
|
211
|
-
- ${template.approach}
|
|
212
|
-
- Affected areas:
|
|
213
|
-
- ${template.affectedArea1}
|
|
214
|
-
- ${template.affectedArea2}
|
|
215
|
-
- Constraints:
|
|
216
|
-
- ${template.constraints}
|
|
217
|
-
|
|
218
|
-
## Tasks
|
|
219
|
-
- [ ] Finalize the proposal, spec, and design details for this change
|
|
220
|
-
- [ ] ${template.task}
|
|
221
|
-
- [ ] Verify the result and update any required durable docs or rules
|
|
222
|
-
|
|
223
|
-
## Verify
|
|
224
|
-
- Automated:
|
|
225
|
-
- [ ] ${template.automatedVerify}
|
|
226
|
-
- Manual:
|
|
227
|
-
- [ ] ${template.manualVerify}
|
|
228
|
-
- Durable updates:
|
|
229
|
-
- [ ] Decide whether this change produced durable knowledge that belongs in \`.rsp/specs/\` or \`.rsp/rules/\`
|
|
230
|
-
- [ ] 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
|
|
231
|
-
|
|
232
|
-
## Blockers
|
|
233
|
-
- none
|
|
234
|
-
`;
|
|
235
|
-
}
|
|
236
|
-
function getChangeTemplateByKind(kind) {
|
|
237
|
-
switch (kind) {
|
|
238
|
-
case "feature":
|
|
239
|
-
return {
|
|
240
|
-
why: "<what user need or capability gap this addresses>",
|
|
241
|
-
scope: "<what new behavior or capability will be delivered>",
|
|
242
|
-
nonGoals: "<what related capabilities are explicitly out of scope>",
|
|
243
|
-
specSection: "### ADDED\n- Requirement: <new user-facing behavior>\n - <verifiable requirement for the new capability>",
|
|
244
|
-
acceptanceSection: "#### Scenario: user exercises the new capability\n- GIVEN <context>\n- WHEN <user action>\n- THEN <expected new behavior>",
|
|
245
|
-
approach: "<how the new capability will be implemented>",
|
|
246
|
-
affectedArea1: "<concrete file path, directory, module, or subsystem 1>",
|
|
247
|
-
affectedArea2: "<concrete file path, directory, module, or subsystem 2 if needed>",
|
|
248
|
-
constraints: "<behavior, compatibility, performance, safety, or scope constraint that must hold>",
|
|
249
|
-
task: "<implement the new behavior and its acceptance scenario in the affected areas>",
|
|
250
|
-
automatedVerify: "<exact test, lint, build, or check command for the new behavior>",
|
|
251
|
-
manualVerify: "<exact end-to-end user scenario to validate the new capability>"
|
|
252
|
-
};
|
|
253
|
-
case "fix":
|
|
254
|
-
return {
|
|
255
|
-
why: "<what current behavior is wrong or broken>",
|
|
256
|
-
scope: "<what specific defect is being corrected>",
|
|
257
|
-
nonGoals: "<what related but separate issues are out of scope>",
|
|
258
|
-
specSection: "### MODIFIED\n- Requirement: correct behavior\n - <expected correct behavior after the fix>",
|
|
259
|
-
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",
|
|
260
|
-
approach: "<root cause analysis and fix strategy>",
|
|
261
|
-
affectedArea1: "<concrete file path, directory, module, or subsystem containing the defect>",
|
|
262
|
-
affectedArea2: "<secondary path, dependency, or test file if needed>",
|
|
263
|
-
constraints: "<regression, backward-compatibility, safety, or scope constraint that must hold>",
|
|
264
|
-
task: "<identify the root cause and apply the minimal fix>",
|
|
265
|
-
automatedVerify: "<exact regression test, lint, build, or check command that proves the fix>",
|
|
266
|
-
manualVerify: "<exact steps to reproduce the original issue and confirm it no longer occurs>"
|
|
267
|
-
};
|
|
268
|
-
case "refactor":
|
|
269
|
-
return {
|
|
270
|
-
why: "<what maintainability, readability, or structural issue motivates this>",
|
|
271
|
-
scope: "<what code areas are being restructured>",
|
|
272
|
-
nonGoals: "<no behavior change is expected or intended>",
|
|
273
|
-
specSection: "### MODIFIED\n- Requirement: internal structure improvement\n - <desired structural outcome without observable behavior change>",
|
|
274
|
-
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",
|
|
275
|
-
approach: "<how the code will be restructured (rename, extract, move, etc.)>",
|
|
276
|
-
affectedArea1: "<concrete file path, directory, module, or subsystem being refactored>",
|
|
277
|
-
affectedArea2: "<secondary path, dependency, or test file if needed>",
|
|
278
|
-
constraints: "<observable behavior must remain unchanged; compatibility, performance, and safety constraints must still hold>",
|
|
279
|
-
task: "<restructure the code while keeping behavior identical>",
|
|
280
|
-
automatedVerify: "<exact existing test, lint, build, or check command that must continue to pass>",
|
|
281
|
-
manualVerify: "<exact scenario to spot-check that behavior is unchanged after the refactor>"
|
|
282
|
-
};
|
|
283
|
-
case "docs":
|
|
284
|
-
return {
|
|
285
|
-
why: "<why the documentation change matters>",
|
|
286
|
-
scope: "<which docs, readers, or workflows will be updated>",
|
|
287
|
-
nonGoals: "<what documentation behavior or audience is out of scope>",
|
|
288
|
-
specSection: "### MODIFIED\n- Requirement: documentation accuracy\n - <what durable reader-facing behavior or explanation changes>",
|
|
289
|
-
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",
|
|
290
|
-
approach: "<how the documentation will be updated and organized>",
|
|
291
|
-
affectedArea1: "<concrete doc path, directory, module doc, or documentation surface 1>",
|
|
292
|
-
affectedArea2: "<secondary doc path or reference file if needed>",
|
|
293
|
-
constraints: "<durable wording, consistency, scope, or audience constraint that must hold>",
|
|
294
|
-
task: "<update the target documentation and supporting references>",
|
|
295
|
-
automatedVerify: "<exact markdown, docs, link, lint, build, or check command if applicable>",
|
|
296
|
-
manualVerify: "<exact reader or operator scenario to confirm the updated guidance is accurate>"
|
|
297
|
-
};
|
|
298
|
-
case "research":
|
|
299
|
-
return {
|
|
300
|
-
why: "<what question or uncertainty this research resolves>",
|
|
301
|
-
scope: "<what system area, option set, or hypothesis is being examined>",
|
|
302
|
-
nonGoals: "<what implementation work is explicitly out of scope>",
|
|
303
|
-
specSection: "### ADDED\n- Requirement: research outcome recording\n - <what finding, option, or decision must be captured clearly>",
|
|
304
|
-
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",
|
|
305
|
-
approach: "<how the investigation will be performed and what evidence will be gathered>",
|
|
306
|
-
affectedArea1: "<concrete file path, directory, module, subsystem, or source under investigation>",
|
|
307
|
-
affectedArea2: "<secondary path, dependency, environment, or evidence source if needed>",
|
|
308
|
-
constraints: "<time, evidence, safety, or scope constraint for the investigation>",
|
|
309
|
-
task: "<gather evidence, record the recommendation or finding, and note any follow-up implementation work if needed>",
|
|
310
|
-
automatedVerify: "<exact command, script, query, or check used to confirm the observed behavior>",
|
|
311
|
-
manualVerify: "<exact review of the findings to confirm they answer the original question and identify follow-up work if needed>"
|
|
312
|
-
};
|
|
313
|
-
case "ops":
|
|
314
|
-
return {
|
|
315
|
-
why: "<why this operational or environment change matters>",
|
|
316
|
-
scope: "<what environment, workflow, or operational path changes>",
|
|
317
|
-
nonGoals: "<what product or feature behavior will not change>",
|
|
318
|
-
specSection: "### MODIFIED\n- Requirement: operational behavior\n - <what reliable operational outcome should change or stay true>",
|
|
319
|
-
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",
|
|
320
|
-
approach: "<how the operational change will be applied safely>",
|
|
321
|
-
affectedArea1: "<concrete script, config path, workflow, environment path, or operational surface 1>",
|
|
322
|
-
affectedArea2: "<secondary script, config path, dependency, or environment surface if needed>",
|
|
323
|
-
constraints: "<rollback, safety, reliability, environment, or scope constraint that must hold>",
|
|
324
|
-
task: "<apply and verify the operational change>",
|
|
325
|
-
automatedVerify: "<exact operational validation, lint, build, deploy, or check command>",
|
|
326
|
-
manualVerify: "<exact operator workflow or environment scenario to confirm the operational result>"
|
|
327
|
-
};
|
|
328
|
-
default:
|
|
329
|
-
return {
|
|
330
|
-
why: "<why this change matters>",
|
|
331
|
-
scope: "<what this change will do>",
|
|
332
|
-
nonGoals: "<what this change will not do>",
|
|
333
|
-
specSection: "### ADDED\n- Requirement: <new or updated behavior>\n - <verifiable requirement>",
|
|
334
|
-
acceptanceSection: "#### Scenario: <name>\n- GIVEN <context>\n- WHEN <action>\n- THEN <expected outcome>",
|
|
335
|
-
approach: "<how the change will be implemented>",
|
|
336
|
-
affectedArea1: "<concrete file path, directory, module, or subsystem 1>",
|
|
337
|
-
affectedArea2: "<concrete file path, directory, module, or subsystem 2 if needed>",
|
|
338
|
-
constraints: "<behavior, compatibility, performance, safety, or scope constraint that must hold>",
|
|
339
|
-
task: "<implement the core change in the affected areas>",
|
|
340
|
-
automatedVerify: "<exact test, lint, build, or check command>",
|
|
341
|
-
manualVerify: "<exact end-to-end scenario to validate>"
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
function renderRspAgentsBlock() {
|
|
346
|
-
return `${RSP_AGENTS_BEGIN}
|
|
347
|
-
## RSP Entry
|
|
348
|
-
|
|
349
|
-
RSP keeps durable rules, specs, and current work under \`.rsp/\`.
|
|
350
|
-
Treat AGENTS.md as navigation only; keep durable rules and design in \`.rsp/\`.
|
|
351
|
-
|
|
352
|
-
Read in order:
|
|
353
|
-
1. .rsp/rules/rsp-rules.md
|
|
354
|
-
2. .rsp/focus.d/
|
|
355
|
-
3. matching .rsp/changes/*.md for the focused entries
|
|
356
|
-
4. .rsp/specs/design.md
|
|
357
|
-
5. .rsp/specs/INDEX.md
|
|
358
|
-
6. only the relevant additional .rsp/rules/*.md and .rsp/specs/*.md files
|
|
359
|
-
|
|
360
|
-
If \`.rsp/focus.d/\` is empty, ask what to work on or suggest \`npx -y @oevery/rsp create <name>\`.
|
|
361
|
-
If your agent supports Agent Skills, load \`rsp\` for setup, repair, and durable-decision tasks.
|
|
362
|
-
${RSP_AGENTS_END}`;
|
|
363
|
-
}
|
|
364
|
-
function upsertRspAgentsBlock(content) {
|
|
365
|
-
const block = renderRspAgentsBlock();
|
|
366
|
-
const managedRe = /<!-- rsp:begin -->[\s\S]*?<!-- rsp:end -->/;
|
|
367
|
-
if (managedRe.test(content)) {
|
|
368
|
-
const next2 = content.replace(managedRe, block);
|
|
369
|
-
return { content: next2, changed: next2 !== content };
|
|
370
|
-
}
|
|
371
|
-
const trimmed = content.trimStart();
|
|
372
|
-
const next = trimmed ? `${block}
|
|
373
|
-
|
|
374
|
-
${trimmed}` : `${block}
|
|
375
|
-
`;
|
|
376
|
-
return { content: next, changed: true };
|
|
377
|
-
}
|
|
378
|
-
function hasRspAgentsBlock(content) {
|
|
379
|
-
return content.includes(RSP_AGENTS_BEGIN) && content.includes(RSP_AGENTS_END);
|
|
380
|
-
}
|
|
381
|
-
var CHANGE_NAME_RE = /^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
|
|
382
|
-
function isValidChangeName(name) {
|
|
383
|
-
return CHANGE_NAME_RE.test(name);
|
|
384
|
-
}
|
|
385
|
-
function guardRspInitialized() {
|
|
386
|
-
const rspRulesPath = join(RSP_DIR, "rules", "rsp-rules.md");
|
|
387
|
-
const designPath = join(RSP_DIR, "specs", "design.md");
|
|
388
|
-
if (!existsSync(rspRulesPath) || !existsSync(designPath)) {
|
|
389
|
-
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
390
|
-
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
391
|
-
process.exit(1);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
async function getFocusedChangeNames(options = {}) {
|
|
395
|
-
const names = /* @__PURE__ */ new Set();
|
|
396
|
-
const focusDir = join(RSP_DIR, "focus.d");
|
|
397
|
-
if (!existsSync(focusDir))
|
|
398
|
-
return names;
|
|
399
|
-
const entries = await walkFiles(focusDir, options);
|
|
400
|
-
for (const entryPath of entries)
|
|
401
|
-
names.add(normalizeLogicalPath(relative(focusDir, entryPath)));
|
|
402
|
-
return names;
|
|
403
|
-
}
|
|
404
|
-
function getOpenCheckboxes(sectionText) {
|
|
405
|
-
return sectionText.split("\n").map((line) => line.trim()).filter((line) => /^- \[[ /]\]/.test(line));
|
|
406
|
-
}
|
|
407
|
-
async function cleanupEmptyParentDirs(path, stopDir) {
|
|
408
|
-
let parent = dirname(path);
|
|
409
|
-
while (parent !== stopDir) {
|
|
410
|
-
try {
|
|
411
|
-
const remaining = await readdir(parent);
|
|
412
|
-
if (remaining.length === 0)
|
|
413
|
-
await rmdir(parent);
|
|
414
|
-
else
|
|
415
|
-
break;
|
|
416
|
-
} catch {
|
|
417
|
-
break;
|
|
418
|
-
}
|
|
419
|
-
parent = dirname(parent);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
async function detectProjectName() {
|
|
423
|
-
try {
|
|
424
|
-
if (existsSync("package.json")) {
|
|
425
|
-
const raw = await readFile("package.json", "utf-8");
|
|
426
|
-
const pkg = JSON.parse(raw);
|
|
427
|
-
if (pkg.name && pkg.name !== "")
|
|
428
|
-
return pkg.name;
|
|
429
|
-
}
|
|
430
|
-
} catch {
|
|
431
|
-
}
|
|
432
|
-
return basename(process.cwd());
|
|
433
|
-
}
|
|
434
|
-
function generateDesignContent(projectName) {
|
|
435
|
-
return `# Project Design: ${projectName}
|
|
436
|
-
|
|
437
|
-
## Purpose
|
|
438
|
-
- <what this project is responsible for>
|
|
439
|
-
- <who or what it serves>
|
|
440
|
-
|
|
441
|
-
## Stable Facts
|
|
442
|
-
- <durable fact that future agents or developers must know>
|
|
443
|
-
- <key architectural invariant>
|
|
444
|
-
|
|
445
|
-
## Boundaries
|
|
446
|
-
- In scope:
|
|
447
|
-
- <major capability or boundary>
|
|
448
|
-
- Out of scope:
|
|
449
|
-
- <intentional non-goal>
|
|
450
|
-
|
|
451
|
-
## Structure
|
|
452
|
-
- <important directory or subsystem> \u2014 <responsibility>
|
|
453
|
-
|
|
454
|
-
## Constraints
|
|
455
|
-
- <cross-cutting technical or operational constraint, when it affects architecture>
|
|
456
|
-
`;
|
|
457
|
-
}
|
|
458
|
-
function generateProjectRulesContent(projectName) {
|
|
459
|
-
return `---
|
|
460
|
-
name: project-rules
|
|
461
|
-
description: Project-specific rules for ${projectName}
|
|
462
|
-
---
|
|
463
|
-
|
|
464
|
-
# Project Rules
|
|
465
|
-
|
|
466
|
-
## Scope
|
|
467
|
-
- <stable local rules, workflow constraints, or validation expectations>
|
|
468
|
-
- Do not put temporary debugging steps here.
|
|
469
|
-
|
|
470
|
-
## Validation
|
|
471
|
-
- <preferred stable validation commands, not temporary troubleshooting steps>
|
|
472
|
-
|
|
473
|
-
## Conventions
|
|
474
|
-
- <project-specific conventions>
|
|
475
|
-
`;
|
|
476
|
-
}
|
|
477
|
-
function generateRulesContent(name) {
|
|
478
|
-
const title = toTitleCase(name);
|
|
479
|
-
return `---
|
|
480
|
-
name: ${name}
|
|
481
|
-
description: Durable rules for ${title}
|
|
482
|
-
---
|
|
483
|
-
|
|
484
|
-
# ${title}
|
|
485
|
-
|
|
486
|
-
## Scope
|
|
487
|
-
- <what this rules file governs>
|
|
488
|
-
|
|
489
|
-
## Rules
|
|
490
|
-
- <stable local rule>
|
|
491
|
-
|
|
492
|
-
## Validation
|
|
493
|
-
- <relevant validation command or check>
|
|
494
|
-
`;
|
|
495
|
-
}
|
|
496
|
-
function generateSpecContent(name) {
|
|
497
|
-
const title = toTitleCase(name);
|
|
498
|
-
return `# ${title}
|
|
499
|
-
|
|
500
|
-
## Purpose
|
|
501
|
-
- <why this project-level spec exists>
|
|
502
|
-
|
|
503
|
-
## Stable Facts
|
|
504
|
-
- <durable fact that future work must know>
|
|
505
|
-
|
|
506
|
-
## Boundaries
|
|
507
|
-
- In scope:
|
|
508
|
-
- <what this spec covers>
|
|
509
|
-
- Out of scope:
|
|
510
|
-
- <what this spec does not cover>
|
|
511
|
-
|
|
512
|
-
## Constraints
|
|
513
|
-
- <stable constraint, if any>
|
|
514
|
-
`;
|
|
515
|
-
}
|
|
516
|
-
function toTitleCase(value) {
|
|
517
|
-
return value.split(/[-/]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
518
|
-
}
|
|
519
|
-
function extractSection(content, heading) {
|
|
520
|
-
const lines = content.split("\n");
|
|
521
|
-
const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
|
|
522
|
-
if (start === -1)
|
|
523
|
-
return "";
|
|
524
|
-
const body = [];
|
|
525
|
-
for (let index = start + 1; index < lines.length; index++) {
|
|
526
|
-
const line = lines[index];
|
|
527
|
-
if (line.startsWith("## "))
|
|
528
|
-
break;
|
|
529
|
-
body.push(line);
|
|
530
|
-
}
|
|
531
|
-
return body.join("\n").trim();
|
|
532
|
-
}
|
|
533
|
-
function hasMeaningfulBlockers(content) {
|
|
534
|
-
const blockers = extractSection(content, "Blockers");
|
|
535
|
-
if (!blockers)
|
|
536
|
-
return false;
|
|
537
|
-
const lines = blockers.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
538
|
-
if (lines.length === 0)
|
|
539
|
-
return false;
|
|
540
|
-
return lines.some((line) => !/^[-*]\s*(?:none)?$/i.test(line) && !/^none$/i.test(line));
|
|
541
|
-
}
|
|
542
|
-
function detectDeltaSections(content) {
|
|
543
|
-
const body = extractSection(content, "Spec");
|
|
544
|
-
return {
|
|
545
|
-
added: /^###\s*ADDED/im.test(body),
|
|
546
|
-
modified: /^###\s*MODIFIED/im.test(body),
|
|
547
|
-
removed: /^###\s*REMOVED/im.test(body)
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
function parseScenarios(content) {
|
|
551
|
-
const scenarios = [];
|
|
552
|
-
const regex = /^#{3,4}\s*Scenario:\s*(.+)$/gim;
|
|
553
|
-
let match;
|
|
554
|
-
while ((match = regex.exec(content)) !== null) {
|
|
555
|
-
const heading = match[1].trim();
|
|
556
|
-
const startPos = match.index + match[0].length;
|
|
557
|
-
const restContent = content.slice(startPos);
|
|
558
|
-
const nextSection = restContent.match(/^#{3,4}\s/m);
|
|
559
|
-
const block = nextSection ? restContent.slice(0, nextSection.index) : restContent;
|
|
560
|
-
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*/, ""));
|
|
561
|
-
if (steps.length > 0)
|
|
562
|
-
scenarios.push({ heading, steps });
|
|
563
|
-
}
|
|
564
|
-
return scenarios;
|
|
565
|
-
}
|
|
85
|
+
import { defineCommand, runMain } from "citty";
|
|
566
86
|
|
|
567
|
-
// src/
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
var RSP_DIR = ".rsp";
|
|
571
|
-
var CHANGES_DIR = join2(RSP_DIR, "changes");
|
|
572
|
-
var FOCUS_DIR = join2(RSP_DIR, "focus.d");
|
|
573
|
-
var ARCHIVES_DIR = join2(RSP_DIR, "archives");
|
|
574
|
-
var LOCK_PATH = join2(RSP_DIR, ".lock");
|
|
575
|
-
var CONFIG_PATH = join2(RSP_DIR, "config.yaml");
|
|
576
|
-
var VALID_KINDS = ["feature", "fix", "refactor", "docs", "ops", "research"];
|
|
577
|
-
var DEFAULT_REQUIRED_SECTIONS = ["Proposal", "Spec", "Design", "Tasks", "Verify", "Blockers"];
|
|
578
|
-
var _configCache = null;
|
|
579
|
-
async function loadRspConfig() {
|
|
580
|
-
if (_configCache)
|
|
581
|
-
return _configCache;
|
|
582
|
-
if (!existsSync2(CONFIG_PATH)) {
|
|
583
|
-
_configCache = {};
|
|
584
|
-
return _configCache;
|
|
585
|
-
}
|
|
586
|
-
const raw = await readFile2(CONFIG_PATH, "utf-8");
|
|
587
|
-
const parsed = parseYamlText(raw);
|
|
588
|
-
_configCache = {
|
|
589
|
-
kinds: Array.isArray(parsed.kinds) ? parsed.kinds.map(String) : void 0
|
|
590
|
-
};
|
|
591
|
-
return _configCache;
|
|
592
|
-
}
|
|
593
|
-
function resolveKinds(config) {
|
|
594
|
-
return config.kinds && config.kinds.length > 0 ? config.kinds : [...VALID_KINDS];
|
|
595
|
-
}
|
|
596
|
-
function resolveRequiredSections(_config) {
|
|
597
|
-
return [...DEFAULT_REQUIRED_SECTIONS];
|
|
598
|
-
}
|
|
599
|
-
async function getVersion() {
|
|
600
|
-
const { version } = JSON.parse(await readFile2(join2(PKG_ROOT, "package.json"), "utf-8"));
|
|
601
|
-
return version;
|
|
602
|
-
}
|
|
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";
|
|
603
90
|
|
|
604
91
|
// src/core/lock.ts
|
|
605
|
-
import { mkdir, readFile
|
|
92
|
+
import { mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
606
93
|
async function withRspLock(operation, fn) {
|
|
94
|
+
requireManagedDirectory(RSP_DIR, "RSP root", { allowMissing: true });
|
|
607
95
|
await mkdir(RSP_DIR, { recursive: true });
|
|
608
96
|
try {
|
|
609
97
|
await writeFile(LOCK_PATH, `${process.pid}
|
|
@@ -626,7 +114,7 @@ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
|
626
114
|
}
|
|
627
115
|
async function handleStaleLock(operation) {
|
|
628
116
|
try {
|
|
629
|
-
const content = await
|
|
117
|
+
const content = await readFile(LOCK_PATH, "utf-8");
|
|
630
118
|
const lines = content.trim().split("\n");
|
|
631
119
|
const pid = Number(lines[0]);
|
|
632
120
|
if (pid && Number.isFinite(pid)) {
|
|
@@ -661,51 +149,41 @@ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
|
661
149
|
}
|
|
662
150
|
}
|
|
663
151
|
|
|
664
|
-
// src/commands/add-rules.ts
|
|
665
|
-
async function addRules(name, projectName = "project") {
|
|
666
|
-
if (!name || !isValidChangeName(name)) {
|
|
667
|
-
console.error(` ${pc.red("Error:")} rule name must be kebab-case with optional subdirectory`);
|
|
668
|
-
process.exit(1);
|
|
669
|
-
}
|
|
670
|
-
guardRspInitialized();
|
|
671
|
-
const path = join3(RSP_DIR, "rules", `${name}.md`);
|
|
672
|
-
if (existsSync3(path)) {
|
|
673
|
-
console.error(` ${pc.red("Error:")} rule file already exists: .rsp/rules/${name}.md`);
|
|
674
|
-
process.exit(1);
|
|
675
|
-
}
|
|
676
|
-
return withRspLock("add-rules", async () => {
|
|
677
|
-
await mkdir2(dirname3(path), { recursive: true });
|
|
678
|
-
const resolvedProjectName = projectName === "project" ? await detectProjectName() : projectName;
|
|
679
|
-
const content = name === "project-rules" ? generateProjectRulesContent(resolvedProjectName) : generateRulesContent(name);
|
|
680
|
-
await writeFile2(path, content);
|
|
681
|
-
console.log(` ${pc.green("Created:")} .rsp/rules/${name}.md
|
|
682
|
-
`);
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// src/commands/add-spec.ts
|
|
687
|
-
import { existsSync as existsSync5 } from "fs";
|
|
688
|
-
import { mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
|
|
689
|
-
import { dirname as dirname4, join as join5 } from "path";
|
|
690
|
-
|
|
691
152
|
// src/commands/specs-index.ts
|
|
692
|
-
import { existsSync
|
|
693
|
-
import { readFile as
|
|
694
|
-
import { basename
|
|
695
|
-
async function buildSpecsIndex({ acquireLock = true } = {}) {
|
|
153
|
+
import { existsSync } from "fs";
|
|
154
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
155
|
+
import { basename, isAbsolute, join, relative, sep } from "path";
|
|
156
|
+
async function buildSpecsIndex({ acquireLock = true, quiet = false } = {}) {
|
|
696
157
|
if (acquireLock)
|
|
697
|
-
return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false }));
|
|
698
|
-
const specsDir =
|
|
699
|
-
|
|
700
|
-
|
|
158
|
+
return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false, quiet }));
|
|
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) {
|
|
168
|
+
if (!quiet)
|
|
169
|
+
console.log(` ${pc.dim("No specs directory.")}
|
|
701
170
|
`);
|
|
702
|
-
return;
|
|
171
|
+
return false;
|
|
703
172
|
}
|
|
704
|
-
const specFiles = (
|
|
705
|
-
const name =
|
|
706
|
-
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);
|
|
707
176
|
}).sort();
|
|
708
177
|
const lines = [];
|
|
178
|
+
lines.push("---");
|
|
179
|
+
lines.push("title: Specs Index");
|
|
180
|
+
lines.push("summary: Additional project-level specs beyond design.md.");
|
|
181
|
+
lines.push("kind: generated-index");
|
|
182
|
+
lines.push("index_type: specs");
|
|
183
|
+
lines.push("source_dir: .rsp/specs");
|
|
184
|
+
lines.push(`entry_count: ${specFiles.length}`);
|
|
185
|
+
lines.push("---");
|
|
186
|
+
lines.push("");
|
|
709
187
|
lines.push("# Specs Index");
|
|
710
188
|
lines.push("");
|
|
711
189
|
lines.push("_Additional project-level specs beyond `design.md`._");
|
|
@@ -716,8 +194,8 @@ async function buildSpecsIndex({ acquireLock = true } = {}) {
|
|
|
716
194
|
lines.push("| File | Title | Summary |");
|
|
717
195
|
lines.push("|------|-------|---------|");
|
|
718
196
|
for (const fp of specFiles) {
|
|
719
|
-
const rel = normalizeLogicalPath(
|
|
720
|
-
const content = await
|
|
197
|
+
const rel = normalizeLogicalPath(relative(specsDir, fp));
|
|
198
|
+
const content = await readFile2(fp, "utf-8");
|
|
721
199
|
const fm = parseFrontmatter(content);
|
|
722
200
|
const fmTitle = fm && typeof fm.title === "string" ? fm.title : "";
|
|
723
201
|
const title = fmTitle || extractTitle(content) || rel.replace(/\.md$/, "");
|
|
@@ -725,11 +203,20 @@ async function buildSpecsIndex({ acquireLock = true } = {}) {
|
|
|
725
203
|
lines.push(`| ${escapeCell(rel)} | ${escapeCell(title)} | ${escapeCell(summary || "\u2014")} |`);
|
|
726
204
|
}
|
|
727
205
|
}
|
|
728
|
-
|
|
729
|
-
lines.
|
|
730
|
-
await
|
|
731
|
-
|
|
206
|
+
const indexPath = join(specsDir, "INDEX.md");
|
|
207
|
+
const nextContent = lines.join("\n");
|
|
208
|
+
const existingContent = existsSync(indexPath) ? await readFile2(indexPath, "utf-8") : null;
|
|
209
|
+
const changed = existingContent !== nextContent;
|
|
210
|
+
if (changed)
|
|
211
|
+
await writeManagedFile(indexPath, nextContent, "Specs index");
|
|
212
|
+
if (!quiet)
|
|
213
|
+
console.log(` ${pc.green("INDEX.md updated:")} ${specFiles.length} spec file(s).
|
|
732
214
|
`);
|
|
215
|
+
return changed;
|
|
216
|
+
}
|
|
217
|
+
function isInside(directory, filePath) {
|
|
218
|
+
const rel = relative(directory, filePath);
|
|
219
|
+
return rel === "" || !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
733
220
|
}
|
|
734
221
|
function extractTitle(content) {
|
|
735
222
|
for (const rawLine of content.split("\n")) {
|
|
@@ -766,249 +253,384 @@ function escapeCell(value) {
|
|
|
766
253
|
|
|
767
254
|
// src/commands/add-spec.ts
|
|
768
255
|
async function addSpec(name, projectName = "project") {
|
|
769
|
-
if (!name || !
|
|
256
|
+
if (!name || !isValidSpecName(name)) {
|
|
770
257
|
console.error(` ${pc.red("Error:")} spec name must be kebab-case with optional subdirectory`);
|
|
771
258
|
process.exit(1);
|
|
772
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
|
+
}
|
|
773
264
|
guardRspInitialized();
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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)}`);
|
|
777
273
|
process.exit(1);
|
|
778
274
|
}
|
|
779
275
|
return withRspLock("add-spec", async () => {
|
|
780
|
-
|
|
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 });
|
|
781
284
|
const resolvedProjectName = projectName === "project" ? await detectProjectName() : projectName;
|
|
782
285
|
const content = name === "design" ? generateDesignContent(resolvedProjectName) : generateSpecContent(name);
|
|
783
|
-
|
|
784
|
-
|
|
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
|
+
}
|
|
785
303
|
console.log(` ${pc.green("Created:")} .rsp/specs/${name}.md
|
|
786
304
|
`);
|
|
787
305
|
});
|
|
788
306
|
}
|
|
789
307
|
|
|
790
308
|
// src/commands/archive.ts
|
|
791
|
-
import { existsSync as
|
|
792
|
-
import { mkdir as
|
|
793
|
-
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";
|
|
794
312
|
|
|
795
313
|
// src/commands/archive-index.ts
|
|
796
|
-
import { existsSync as
|
|
797
|
-
import { readFile as
|
|
798
|
-
import { basename as
|
|
799
|
-
async function buildArchiveIndex({ acquireLock = true } = {}) {
|
|
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";
|
|
317
|
+
async function buildArchiveIndex({ acquireLock = true, quiet = false } = {}) {
|
|
800
318
|
if (acquireLock)
|
|
801
|
-
return withRspLock("archive-index", async () => buildArchiveIndex({ acquireLock: false }));
|
|
802
|
-
|
|
803
|
-
|
|
319
|
+
return withRspLock("archive-index", async () => buildArchiveIndex({ acquireLock: false, quiet }));
|
|
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) {
|
|
326
|
+
if (!quiet)
|
|
327
|
+
console.log(` ${pc.dim("No archives directory.")}
|
|
804
328
|
`);
|
|
805
|
-
return;
|
|
329
|
+
return false;
|
|
806
330
|
}
|
|
807
|
-
const archiveFiles =
|
|
331
|
+
const archiveFiles = inspection.files;
|
|
808
332
|
const lines = [];
|
|
809
|
-
lines.push("
|
|
333
|
+
lines.push("---");
|
|
334
|
+
lines.push("title: Archive Index");
|
|
335
|
+
lines.push("summary: Completed RSP changes.");
|
|
336
|
+
lines.push("kind: generated-index");
|
|
337
|
+
lines.push("index_type: archives");
|
|
338
|
+
lines.push("source_dir: .rsp/archives");
|
|
339
|
+
lines.push(`entry_count: ${archiveFiles.length}`);
|
|
340
|
+
lines.push("---");
|
|
810
341
|
lines.push("");
|
|
811
|
-
lines.push(
|
|
342
|
+
lines.push("# Archive Index");
|
|
812
343
|
lines.push("");
|
|
813
344
|
if (archiveFiles.length === 0) {
|
|
814
345
|
lines.push("_No archived changes yet._");
|
|
815
346
|
} else {
|
|
816
|
-
lines.push("| Date | Change | Summary |");
|
|
817
|
-
lines.push("
|
|
347
|
+
lines.push("| Date | Change | Kind | Summary |");
|
|
348
|
+
lines.push("|------|--------|------|---------|");
|
|
818
349
|
}
|
|
819
350
|
for (const fp of archiveFiles) {
|
|
820
|
-
const base =
|
|
821
|
-
const archiveRel = normalizeLogicalPath(
|
|
822
|
-
const archiveDirPart =
|
|
351
|
+
const base = basename2(fp);
|
|
352
|
+
const archiveRel = normalizeLogicalPath(relative2(ARCHIVES_DIR, fp));
|
|
353
|
+
const archiveDirPart = dirname(archiveRel);
|
|
823
354
|
const dateMatch = base.match(/^(\d{4}-\d{2}-\d{2})_(.+)\.md$/);
|
|
824
355
|
const date = dateMatch ? dateMatch[1] : "\u2014";
|
|
825
356
|
const baseName = dateMatch ? dateMatch[2] : base.replace(/\.md$/, "");
|
|
826
357
|
const changeName = archiveDirPart !== "." ? `${archiveDirPart}/${baseName}` : baseName;
|
|
827
358
|
let summary = "";
|
|
359
|
+
let kind = "";
|
|
828
360
|
try {
|
|
829
|
-
const content = await
|
|
361
|
+
const content = await readFile3(fp, "utf-8");
|
|
362
|
+
const fm = parseFrontmatter(content);
|
|
363
|
+
if (typeof fm?.kind === "string")
|
|
364
|
+
kind = fm.kind;
|
|
365
|
+
if (typeof fm?.summary === "string")
|
|
366
|
+
summary = fm.summary.trim();
|
|
830
367
|
const proposal = extractSection(content, "Proposal");
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
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();
|
|
834
374
|
} catch {
|
|
835
375
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
lines.
|
|
840
|
-
|
|
841
|
-
const
|
|
842
|
-
|
|
843
|
-
|
|
376
|
+
lines.push(`| ${date} | ${escapeCell2(changeName)} | ${escapeCell2(kind || "\u2014")} | ${escapeCell2(summary || "\u2014")} |`);
|
|
377
|
+
}
|
|
378
|
+
const indexPath = join3(ARCHIVES_DIR, "INDEX.md");
|
|
379
|
+
const nextContent = lines.join("\n");
|
|
380
|
+
const existingContent = existsSync2(indexPath) ? await readFile3(indexPath, "utf-8") : null;
|
|
381
|
+
const changed = existingContent !== nextContent;
|
|
382
|
+
if (changed)
|
|
383
|
+
await writeManagedFile(indexPath, nextContent, "archive index");
|
|
384
|
+
if (!quiet)
|
|
385
|
+
console.log(` ${pc.green("INDEX.md updated:")} ${archiveFiles.length} archived change(s).
|
|
844
386
|
`);
|
|
387
|
+
return changed;
|
|
388
|
+
}
|
|
389
|
+
function escapeCell2(value) {
|
|
390
|
+
return value.replace(/\|/g, "\\|");
|
|
845
391
|
}
|
|
846
392
|
|
|
847
393
|
// src/commands/archive.ts
|
|
848
|
-
async function archiveChange(name) {
|
|
394
|
+
async function archiveChange(name, options = {}) {
|
|
849
395
|
if (!name) {
|
|
850
396
|
console.error(` ${pc.red("Usage:")} rsp archive <name>`);
|
|
851
397
|
process.exit(1);
|
|
852
398
|
}
|
|
853
|
-
if (!isValidChangeName(name)) {
|
|
854
|
-
console.error(` ${pc.red("Error:")} change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
855
|
-
process.exit(1);
|
|
856
|
-
}
|
|
857
399
|
guardRspInitialized();
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
400
|
+
if (options.dryRun) {
|
|
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
|
+
});
|
|
407
|
+
console.log();
|
|
408
|
+
console.log(` ${pc.bold("Archive dry-run for")} ${pc.cyan(name)}`);
|
|
409
|
+
console.log();
|
|
410
|
+
if (checklist.length === 0) {
|
|
411
|
+
console.log(` ${pc.green("\u2713")} Ready to archive. No deterministic warnings found.
|
|
412
|
+
`);
|
|
413
|
+
} else {
|
|
414
|
+
for (const line of checklist)
|
|
415
|
+
console.log(` ${pc.yellow("\u26A0")} ${line}`);
|
|
416
|
+
console.log();
|
|
417
|
+
console.log(` ${pc.dim("Review the warnings above before treating this work as fully closed.")}
|
|
418
|
+
`);
|
|
419
|
+
}
|
|
420
|
+
return;
|
|
862
421
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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.")}
|
|
870
437
|
`);
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
|
+
}
|
|
882
454
|
try {
|
|
883
|
-
await
|
|
884
|
-
await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
|
|
885
|
-
focusCleared = true;
|
|
455
|
+
await cleanupEmptyParentDirs(srcPath, CHANGES_DIR);
|
|
886
456
|
} catch (error) {
|
|
887
|
-
postArchiveWarnings.push(`
|
|
457
|
+
postArchiveWarnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
|
|
888
458
|
}
|
|
889
|
-
|
|
890
|
-
try {
|
|
891
|
-
await cleanupEmptyParentDirs(srcPath, CHANGES_DIR);
|
|
892
|
-
} catch (error) {
|
|
893
|
-
postArchiveWarnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
|
|
894
|
-
}
|
|
895
|
-
const clearedMsg = focusCleared ? ` ${pc.dim("focus marker cleared")}
|
|
459
|
+
const clearedMsg = focusCleared ? ` ${pc.dim("focus marker cleared")}
|
|
896
460
|
` : "";
|
|
897
|
-
|
|
461
|
+
console.log(` ${pc.green("Archived:")} ${archiveName}
|
|
898
462
|
${clearedMsg}`);
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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.")}
|
|
908
472
|
`);
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
console.log(` ${pc.cyan("Git workflow:")}
|
|
473
|
+
if (existsSync3(".git")) {
|
|
474
|
+
console.log(` ${pc.cyan("Git delivery:")}
|
|
912
475
|
`);
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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);
|
|
916
485
|
}
|
|
917
|
-
|
|
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
|
+
}
|
|
918
502
|
}
|
|
919
503
|
function resolveArchiveName(archiveSubdir, date, base) {
|
|
920
504
|
const initialName = `${date}_${base}.md`;
|
|
921
|
-
if (!
|
|
505
|
+
if (!existsSync3(join4(archiveSubdir, initialName)))
|
|
922
506
|
return initialName;
|
|
923
507
|
const MAX_SUFFIX = 9999;
|
|
924
508
|
for (let index = 2; index <= MAX_SUFFIX; index++) {
|
|
925
509
|
const candidate = `${date}_${base}-${index}.md`;
|
|
926
|
-
if (!
|
|
510
|
+
if (!existsSync3(join4(archiveSubdir, candidate)))
|
|
927
511
|
return candidate;
|
|
928
512
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
const tasksSection = extractSection(content, "Tasks");
|
|
935
|
-
const verifySection = extractSection(content, "Verify");
|
|
936
|
-
const taskTodos = getOpenCheckboxes(tasksSection);
|
|
937
|
-
if (taskTodos.length > 0)
|
|
938
|
-
warnings.push(`${taskTodos.length} task item(s) still incomplete`);
|
|
939
|
-
const verifyTodos = getOpenCheckboxes(verifySection);
|
|
940
|
-
if (verifyTodos.length > 0)
|
|
941
|
-
warnings.push(`${verifyTodos.length} Verify checklist item(s) are still incomplete`);
|
|
942
|
-
if (hasMeaningfulBlockers(content))
|
|
943
|
-
warnings.push("active blockers are present in the change file");
|
|
944
|
-
const scenarios = parseScenarios(content);
|
|
945
|
-
if (scenarios.length === 0)
|
|
946
|
-
warnings.push("no Scenario blocks found (some changes do not need them)");
|
|
947
|
-
return warnings;
|
|
513
|
+
throw new WorkRefError(
|
|
514
|
+
"archive_name_exhausted",
|
|
515
|
+
`archive name collision exceeded ${MAX_SUFFIX} attempts for ${base}`,
|
|
516
|
+
base
|
|
517
|
+
);
|
|
948
518
|
}
|
|
949
519
|
|
|
950
520
|
// src/commands/check.ts
|
|
951
|
-
import {
|
|
952
|
-
import { readFile as readFile7 } from "fs/promises";
|
|
953
|
-
import { join as join8, relative as relative4 } from "path";
|
|
521
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
954
522
|
async function runCheck(options = {}) {
|
|
955
|
-
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
|
+
}
|
|
956
543
|
const validKinds = resolveKinds(config);
|
|
957
544
|
const requiredSections = resolveRequiredSections(config);
|
|
958
545
|
const choosePlaceholderRe = /^<choose:/i;
|
|
959
|
-
const diagnostics = [];
|
|
960
|
-
const runtime = [];
|
|
961
546
|
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
|
|
962
547
|
const addDiagnostic = (diagnostic) => diagnostics.push(diagnostic);
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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
|
+
});
|
|
557
|
+
}
|
|
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
|
+
});
|
|
1005
615
|
}
|
|
1006
616
|
}
|
|
1007
|
-
const
|
|
1008
|
-
|
|
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) {
|
|
1009
630
|
const result2 = {
|
|
1010
631
|
command: "check",
|
|
1011
632
|
ok: diagnostics.every((d) => d.severity !== "error"),
|
|
633
|
+
focused: Boolean(options.focused),
|
|
1012
634
|
diagnostics,
|
|
1013
635
|
runtime,
|
|
1014
636
|
summary: {
|
|
@@ -1018,11 +640,13 @@ async function runCheck(options = {}) {
|
|
|
1018
640
|
}
|
|
1019
641
|
};
|
|
1020
642
|
if (options.json) {
|
|
1021
|
-
emitJson(result2);
|
|
643
|
+
emitJson(result2, options);
|
|
1022
644
|
return result2;
|
|
1023
645
|
}
|
|
1024
646
|
console.log();
|
|
1025
647
|
console.log(` ${pc.bold("RSP check")}`);
|
|
648
|
+
if (options.focused)
|
|
649
|
+
console.log(` ${pc.dim("(focused only)")}`);
|
|
1026
650
|
console.log();
|
|
1027
651
|
if (diagnostics.length === 0) {
|
|
1028
652
|
console.log(` ${pc.dim("No change files to check.")}
|
|
@@ -1044,11 +668,11 @@ async function runCheck(options = {}) {
|
|
|
1044
668
|
`);
|
|
1045
669
|
return result2;
|
|
1046
670
|
}
|
|
1047
|
-
for (const
|
|
1048
|
-
const name =
|
|
671
|
+
for (const ref of changeRefs) {
|
|
672
|
+
const { name, path: fp } = ref;
|
|
1049
673
|
let content;
|
|
1050
674
|
try {
|
|
1051
|
-
content = await
|
|
675
|
+
content = await readFile5(fp, "utf-8");
|
|
1052
676
|
} catch (error) {
|
|
1053
677
|
addDiagnostic({
|
|
1054
678
|
severity: "error",
|
|
@@ -1159,6 +783,30 @@ async function runCheck(options = {}) {
|
|
|
1159
783
|
message: `delta markers found: ${parts.join(", ")}`
|
|
1160
784
|
});
|
|
1161
785
|
}
|
|
786
|
+
const placeholderLines = findUnfinishedTemplateLines(content);
|
|
787
|
+
if (placeholderLines.length > 0) {
|
|
788
|
+
addDiagnostic({
|
|
789
|
+
severity: "warning",
|
|
790
|
+
code: "unfinished_template_placeholders",
|
|
791
|
+
change: name,
|
|
792
|
+
path: fp,
|
|
793
|
+
message: "unfinished template placeholders found",
|
|
794
|
+
details: placeholderLines,
|
|
795
|
+
hint: "Replace angle-bracket template text with concrete change details before archiving."
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
const clarificationLines = findUnresolvedClarificationLines(content);
|
|
799
|
+
if (clarificationLines.length > 0) {
|
|
800
|
+
addDiagnostic({
|
|
801
|
+
severity: "warning",
|
|
802
|
+
code: "unresolved_clarifications",
|
|
803
|
+
change: name,
|
|
804
|
+
path: fp,
|
|
805
|
+
message: "unresolved clarification markers found",
|
|
806
|
+
details: clarificationLines,
|
|
807
|
+
hint: "Resolve or remove clarification markers once the open question is answered."
|
|
808
|
+
});
|
|
809
|
+
}
|
|
1162
810
|
const scenarios = parseScenarios(content);
|
|
1163
811
|
if (scenarios.length > 0) {
|
|
1164
812
|
const scenarioIssues = [];
|
|
@@ -1191,20 +839,23 @@ async function runCheck(options = {}) {
|
|
|
1191
839
|
const result = {
|
|
1192
840
|
command: "check",
|
|
1193
841
|
ok: diagnostics.every((d) => d.severity !== "error"),
|
|
842
|
+
focused: Boolean(options.focused),
|
|
1194
843
|
diagnostics,
|
|
1195
844
|
runtime,
|
|
1196
845
|
summary: {
|
|
1197
|
-
changeFiles:
|
|
846
|
+
changeFiles: changeRefs.length,
|
|
1198
847
|
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
1199
848
|
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
1200
849
|
}
|
|
1201
850
|
};
|
|
1202
851
|
if (options.json) {
|
|
1203
|
-
emitJson(result);
|
|
852
|
+
emitJson(result, options);
|
|
1204
853
|
return result;
|
|
1205
854
|
}
|
|
1206
855
|
console.log();
|
|
1207
856
|
console.log(` ${pc.bold("RSP check")}`);
|
|
857
|
+
if (options.focused)
|
|
858
|
+
console.log(` ${pc.dim("(focused only)")}`);
|
|
1208
859
|
console.log();
|
|
1209
860
|
for (const diagnostic of diagnostics) {
|
|
1210
861
|
const icon = diagnostic.severity === "error" ? pc.red("\u2717") : diagnostic.severity === "warning" ? pc.yellow("\u26A0") : pc.dim("\u2139");
|
|
@@ -1218,77 +869,288 @@ async function runCheck(options = {}) {
|
|
|
1218
869
|
}
|
|
1219
870
|
console.log();
|
|
1220
871
|
if (result.summary.errors === 0 && result.summary.warnings === 0)
|
|
1221
|
-
console.log(` ${pc.green("\u2713")} All ${
|
|
872
|
+
console.log(` ${pc.green("\u2713")} All ${changeRefs.length} change file(s) valid.
|
|
1222
873
|
`);
|
|
1223
874
|
else
|
|
1224
|
-
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).
|
|
1225
876
|
`);
|
|
1226
877
|
return result;
|
|
1227
878
|
}
|
|
879
|
+
function findUnfinishedTemplateLines(content) {
|
|
880
|
+
const placeholderRe = /<[^>\n]*(?:choose:|one-line|what |why |which |who |how |new |updated |expected |actual |specific |concrete |exact |verifiable |user |capability |behavior |context |action |path|directory|module|subsystem|command|scenario|constraint|requirement|question|uncertainty|implementation)[^>\n]*>/i;
|
|
881
|
+
return collectMatchingLines(content, (line) => {
|
|
882
|
+
const trimmed = stripInlineCodeSpans(line).trim();
|
|
883
|
+
return trimmed.startsWith("<!--") === false && placeholderRe.test(trimmed);
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
function findUnresolvedClarificationLines(content) {
|
|
887
|
+
return collectMatchingLines(content, (line) => /\[(?:NEEDS|TODO|TBD)\s+CLARIFICATION(?::[^\]]*)?\]/i.test(stripInlineCodeSpans(line)));
|
|
888
|
+
}
|
|
889
|
+
function stripInlineCodeSpans(line) {
|
|
890
|
+
return line.replace(/`[^`]*`/g, "");
|
|
891
|
+
}
|
|
892
|
+
function collectMatchingLines(content, predicate) {
|
|
893
|
+
const matches = [];
|
|
894
|
+
const lines = content.split("\n");
|
|
895
|
+
for (let index = 0; index < lines.length; index++) {
|
|
896
|
+
const line = lines[index];
|
|
897
|
+
if (predicate(line))
|
|
898
|
+
matches.push(`line ${index + 1}: ${line.trim()}`);
|
|
899
|
+
}
|
|
900
|
+
return matches.slice(0, 10);
|
|
901
|
+
}
|
|
1228
902
|
|
|
1229
903
|
// src/commands/create.ts
|
|
1230
|
-
import { existsSync as
|
|
1231
|
-
import { mkdir as
|
|
1232
|
-
import { dirname as
|
|
1233
|
-
async function createChange(name, summary = "", kind) {
|
|
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";
|
|
907
|
+
async function createChange(name, summary = "", kind, options = {}) {
|
|
1234
908
|
if (!name) {
|
|
1235
909
|
console.error(` ${pc.red("Usage:")} rsp create <name> [summary]`);
|
|
1236
910
|
process.exit(1);
|
|
1237
911
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
912
|
+
guardRspInitialized();
|
|
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"}
|
|
947
|
+
`);
|
|
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
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/commands/doctor.ts
|
|
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";
|
|
962
|
+
|
|
963
|
+
// src/commands/update.ts
|
|
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";
|
|
968
|
+
async function updateProject(options = {}) {
|
|
969
|
+
if (!existsSync5(RSP_DIR)) {
|
|
970
|
+
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
971
|
+
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
1240
972
|
process.exit(1);
|
|
1241
973
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
const
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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;
|
|
979
|
+
let updated = false;
|
|
980
|
+
const actions = [];
|
|
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;
|
|
1005
|
+
if (existingRules !== bundledRules) {
|
|
1006
|
+
await writeManagedFile(RSP_RULES_PATH, bundledRules, "fallback protocol");
|
|
1007
|
+
actions.push("rsp-rules.md updated");
|
|
1008
|
+
if (!options.quiet)
|
|
1009
|
+
console.log(` ${pc.green("\u2713")} rsp-rules.md updated`);
|
|
1010
|
+
updated = true;
|
|
1011
|
+
}
|
|
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
|
+
}
|
|
1055
|
+
const projectName = await detectProjectName();
|
|
1056
|
+
const baseAgents = agentsInspection.exists ? await readFile6(agentsPath, "utf-8") : `# ${projectName}
|
|
1057
|
+
|
|
1058
|
+
`;
|
|
1059
|
+
const nextAgents = upsertRspAgentsBlock(baseAgents);
|
|
1060
|
+
if (!agentsInspection.exists || nextAgents.changed) {
|
|
1061
|
+
await writeManagedFile(agentsPath, nextAgents.content, "AGENTS.md");
|
|
1062
|
+
actions.push("AGENTS.md managed block refreshed");
|
|
1063
|
+
if (!options.quiet)
|
|
1064
|
+
console.log(` ${pc.green("\u2713")} AGENTS.md managed block refreshed`);
|
|
1065
|
+
updated = true;
|
|
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
|
+
}
|
|
1073
|
+
const specsIndexChanged = await buildSpecsIndex({ acquireLock: false, quiet: options.quiet });
|
|
1074
|
+
const archiveIndexChanged = await buildArchiveIndex({ acquireLock: false, quiet: options.quiet });
|
|
1075
|
+
if (specsIndexChanged || archiveIndexChanged) {
|
|
1076
|
+
actions.push("generated indexes rebuilt");
|
|
1077
|
+
updated = true;
|
|
1078
|
+
}
|
|
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) {
|
|
1088
|
+
console.log(` ${pc.dim("Already up to date.")}
|
|
1089
|
+
`);
|
|
1090
|
+
console.log(SKILL_REFRESH_HINT);
|
|
1091
|
+
} else if (!options.quiet) {
|
|
1092
|
+
console.log(` ${pc.green("Update complete.")}
|
|
1262
1093
|
`);
|
|
1094
|
+
console.log(SKILL_REFRESH_HINT);
|
|
1095
|
+
}
|
|
1096
|
+
return {
|
|
1097
|
+
actions,
|
|
1098
|
+
migration: {
|
|
1099
|
+
inspectionComplete: inspection.diagnostics.length === 0,
|
|
1100
|
+
manualMigrationRequired: inspection.directoryRemaining,
|
|
1101
|
+
residualEntries,
|
|
1102
|
+
diagnostics: inspection.diagnostics
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1263
1105
|
});
|
|
1264
1106
|
}
|
|
1265
1107
|
|
|
1266
1108
|
// src/commands/doctor.ts
|
|
1267
|
-
import { existsSync as existsSync10 } from "fs";
|
|
1268
|
-
import { readFile as readFile8 } from "fs/promises";
|
|
1269
|
-
import { basename as basename5, join as join10, relative as relative5 } from "path";
|
|
1270
1109
|
async function runDoctor(options = {}) {
|
|
1110
|
+
const fixed = [];
|
|
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
|
+
}
|
|
1119
|
+
}
|
|
1271
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
|
+
}
|
|
1272
1129
|
const runtime = [];
|
|
1273
1130
|
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
|
|
1274
|
-
const
|
|
1275
|
-
const
|
|
1276
|
-
const
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
reportCheck(checks, "specs/design.md exists",
|
|
1281
|
-
reportCheck(checks, "specs/INDEX.md exists",
|
|
1282
|
-
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");
|
|
1283
1140
|
await checkAgents(checks, reportRuntime);
|
|
1284
|
-
await checkGeneratedIndex(checks, reportRuntime, specsIndexPath, "
|
|
1285
|
-
await checkGeneratedIndex(checks, reportRuntime, archivesIndexPath, "
|
|
1286
|
-
await checkArchiveNaming(checks
|
|
1287
|
-
await checkConfigSemantics(checks, reportRuntime);
|
|
1288
|
-
|
|
1141
|
+
await checkGeneratedIndex(checks, reportRuntime, specsIndexPath, "specs", "specs/INDEX.md has generated-index metadata", "Run: rsp update");
|
|
1142
|
+
await checkGeneratedIndex(checks, reportRuntime, archivesIndexPath, "archives", "archives/INDEX.md has generated-index metadata", "Run: rsp update");
|
|
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);
|
|
1289
1150
|
const result = {
|
|
1290
1151
|
command: "doctor",
|
|
1291
1152
|
ok: checks.every((check) => check.status !== "issue"),
|
|
1153
|
+
fixed,
|
|
1292
1154
|
checks,
|
|
1293
1155
|
runtime,
|
|
1294
1156
|
summary: {
|
|
@@ -1296,12 +1158,19 @@ async function runDoctor(options = {}) {
|
|
|
1296
1158
|
}
|
|
1297
1159
|
};
|
|
1298
1160
|
if (options.json) {
|
|
1299
|
-
emitJson(result);
|
|
1161
|
+
emitJson(result, options);
|
|
1300
1162
|
return result;
|
|
1301
1163
|
}
|
|
1302
1164
|
console.log();
|
|
1303
1165
|
console.log(` ${pc.bold("RSP doctor")}`);
|
|
1304
1166
|
console.log();
|
|
1167
|
+
if (fixed.length > 0) {
|
|
1168
|
+
console.log(` ${pc.green("Fixed:")} ${fixed.join(", ")}`);
|
|
1169
|
+
console.log();
|
|
1170
|
+
} else if (options.fix && !repairIssue && existsSync6(RSP_DIR)) {
|
|
1171
|
+
console.log(` ${pc.dim("No safe fixes needed.")}`);
|
|
1172
|
+
console.log();
|
|
1173
|
+
}
|
|
1305
1174
|
for (const check of checks)
|
|
1306
1175
|
printDoctorCheck(check);
|
|
1307
1176
|
if (result.summary.issues === 0)
|
|
@@ -1314,6 +1183,57 @@ async function runDoctor(options = {}) {
|
|
|
1314
1183
|
`);
|
|
1315
1184
|
return result;
|
|
1316
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
|
+
});
|
|
1236
|
+
}
|
|
1317
1237
|
function reportCheck(checks, label, ok, fixHint) {
|
|
1318
1238
|
if (ok) {
|
|
1319
1239
|
checks.push({ status: "ok", label });
|
|
@@ -1337,13 +1257,23 @@ function printDoctorCheck(check) {
|
|
|
1337
1257
|
console.log(` ${pc.dim(check.hint)}`);
|
|
1338
1258
|
}
|
|
1339
1259
|
async function checkAgents(checks, reportRuntime) {
|
|
1340
|
-
|
|
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) {
|
|
1341
1271
|
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing", hint: "Run: rsp init" });
|
|
1342
1272
|
return;
|
|
1343
1273
|
}
|
|
1344
1274
|
let agents;
|
|
1345
1275
|
try {
|
|
1346
|
-
agents = await
|
|
1276
|
+
agents = await readFile7("AGENTS.md", "utf-8");
|
|
1347
1277
|
} catch (error) {
|
|
1348
1278
|
reportRuntime({
|
|
1349
1279
|
code: "agents_read_failed",
|
|
@@ -1360,12 +1290,17 @@ async function checkAgents(checks, reportRuntime) {
|
|
|
1360
1290
|
}
|
|
1361
1291
|
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing managed RSP block", hint: "Run: rsp init" });
|
|
1362
1292
|
}
|
|
1363
|
-
async function checkGeneratedIndex(checks, reportRuntime, path,
|
|
1364
|
-
|
|
1293
|
+
async function checkGeneratedIndex(checks, reportRuntime, path, expectedIndexType, label, fixHint) {
|
|
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)
|
|
1365
1300
|
return;
|
|
1366
1301
|
let content;
|
|
1367
1302
|
try {
|
|
1368
|
-
content = await
|
|
1303
|
+
content = await readFile7(path, "utf-8");
|
|
1369
1304
|
} catch (error) {
|
|
1370
1305
|
reportRuntime({
|
|
1371
1306
|
code: "generated_index_read_failed",
|
|
@@ -1376,16 +1311,38 @@ async function checkGeneratedIndex(checks, reportRuntime, path, signature, label
|
|
|
1376
1311
|
checks.push({ status: "issue", label, message: `${label} could not be read`, hint: fixHint });
|
|
1377
1312
|
return;
|
|
1378
1313
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1314
|
+
try {
|
|
1315
|
+
const fm = parseFrontmatter(content);
|
|
1316
|
+
if (fm?.kind === "generated-index" && fm.index_type === expectedIndexType) {
|
|
1317
|
+
checks.push({ status: "ok", label });
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
reportRuntime({
|
|
1322
|
+
code: "generated_index_frontmatter_parse_failed",
|
|
1323
|
+
operation: "parseFrontmatter",
|
|
1324
|
+
path,
|
|
1325
|
+
message: toErrorMessage(error)
|
|
1326
|
+
});
|
|
1327
|
+
checks.push({ status: "issue", label, message: `${label} could not parse metadata`, hint: fixHint });
|
|
1381
1328
|
return;
|
|
1382
1329
|
}
|
|
1383
1330
|
checks.push({ status: "issue", label, hint: fixHint });
|
|
1384
1331
|
}
|
|
1385
|
-
async function checkArchiveNaming(checks
|
|
1386
|
-
const archivesDir =
|
|
1387
|
-
const
|
|
1388
|
-
|
|
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);
|
|
1389
1346
|
if (invalidFiles.length === 0) {
|
|
1390
1347
|
checks.push({ status: "ok", label: "archived change files follow the date_name.md convention" });
|
|
1391
1348
|
return;
|
|
@@ -1398,69 +1355,172 @@ async function checkArchiveNaming(checks, reportRuntime) {
|
|
|
1398
1355
|
});
|
|
1399
1356
|
}
|
|
1400
1357
|
async function checkConfigSemantics(checks, reportRuntime) {
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
let content;
|
|
1358
|
+
clearConfigCache();
|
|
1359
|
+
let inspection;
|
|
1404
1360
|
try {
|
|
1405
|
-
|
|
1361
|
+
inspection = await inspectRspConfig();
|
|
1406
1362
|
} catch (error) {
|
|
1363
|
+
const message = toErrorMessage(error);
|
|
1407
1364
|
reportRuntime({
|
|
1408
|
-
code: "
|
|
1409
|
-
operation: "
|
|
1410
|
-
path:
|
|
1411
|
-
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."
|
|
1412
1386
|
});
|
|
1413
|
-
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" });
|
|
1414
|
-
return;
|
|
1415
1387
|
}
|
|
1416
|
-
|
|
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());
|
|
1417
1394
|
try {
|
|
1418
|
-
|
|
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
|
+
);
|
|
1419
1414
|
} catch (error) {
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
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
|
+
}
|
|
1426
1423
|
checks.push({
|
|
1427
1424
|
status: "issue",
|
|
1428
|
-
label:
|
|
1429
|
-
message:
|
|
1430
|
-
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"
|
|
1431
1428
|
});
|
|
1432
1429
|
}
|
|
1433
1430
|
}
|
|
1434
|
-
function
|
|
1435
|
-
|
|
1431
|
+
async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
|
|
1432
|
+
const decisionRecordsPath = resolveDecisionRecordsPath(await loadRspConfig());
|
|
1433
|
+
if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !existsSync6(DEFAULT_DECISION_RECORDS_PATH))
|
|
1436
1434
|
return;
|
|
1437
|
-
|
|
1438
|
-
|
|
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) {
|
|
1439
1443
|
checks.push({
|
|
1440
1444
|
status: "issue",
|
|
1441
|
-
label: "
|
|
1442
|
-
message: `
|
|
1443
|
-
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."
|
|
1444
1448
|
});
|
|
1445
1449
|
return;
|
|
1446
1450
|
}
|
|
1447
|
-
|
|
1448
|
-
const duplicates = [...new Set(items.filter((item, index) => items.indexOf(item) !== index))];
|
|
1449
|
-
if (duplicates.length === 0) {
|
|
1450
|
-
checks.push({ status: "ok", label: `config.yaml field "${key}" has valid list semantics` });
|
|
1451
|
+
if (inactiveRecords.length === 0)
|
|
1451
1452
|
return;
|
|
1452
|
-
}
|
|
1453
1453
|
checks.push({
|
|
1454
1454
|
status: "issue",
|
|
1455
|
-
label: "
|
|
1456
|
-
message: `
|
|
1457
|
-
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}.`
|
|
1458
1458
|
});
|
|
1459
1459
|
}
|
|
1460
|
-
async function checkActiveChangeConsistency(checks
|
|
1461
|
-
const
|
|
1462
|
-
const
|
|
1463
|
-
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
|
+
}
|
|
1464
1524
|
const missingChangeFiles = [...focusNames].filter((name) => !changeNames.has(name));
|
|
1465
1525
|
const unfocusedChangeFiles = [...changeNames].filter((name) => !focusNames.has(name));
|
|
1466
1526
|
if (missingChangeFiles.length === 0) {
|
|
@@ -1488,119 +1548,454 @@ async function checkActiveChangeConsistency(checks, reportRuntime) {
|
|
|
1488
1548
|
}
|
|
1489
1549
|
|
|
1490
1550
|
// src/commands/focus.ts
|
|
1491
|
-
import { existsSync as
|
|
1492
|
-
import { mkdir as mkdir6, unlink as
|
|
1493
|
-
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";
|
|
1494
1554
|
async function focusChange(name) {
|
|
1495
1555
|
if (!name) {
|
|
1496
1556
|
console.error(` ${pc.red("Usage:")} rsp focus <name>`);
|
|
1497
1557
|
process.exit(1);
|
|
1498
1558
|
}
|
|
1499
|
-
if (!isValidChangeName(name)) {
|
|
1500
|
-
console.error(` ${pc.red("Error:")} change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
1501
|
-
process.exit(1);
|
|
1502
|
-
}
|
|
1503
1559
|
guardRspInitialized();
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
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);
|
|
1508
1573
|
}
|
|
1509
|
-
return withRspLock("focus-change", async () => {
|
|
1510
|
-
await mkdir6(FOCUS_DIR, { recursive: true });
|
|
1511
|
-
const focusEntry = join11(FOCUS_DIR, name);
|
|
1512
|
-
await mkdir6(dirname8(focusEntry), { recursive: true });
|
|
1513
|
-
await writeFile7(focusEntry, "");
|
|
1514
|
-
console.log(` ${pc.green("Focused:")} ${name}`);
|
|
1515
|
-
console.log(` ${pc.dim("focus.d")} \u2192 ${name}`);
|
|
1516
|
-
console.log();
|
|
1517
|
-
});
|
|
1518
1574
|
}
|
|
1519
1575
|
async function unfocusChange(name) {
|
|
1520
1576
|
if (!name) {
|
|
1521
1577
|
console.error(` ${pc.red("Usage:")} rsp unfocus <name>`);
|
|
1522
1578
|
process.exit(1);
|
|
1523
1579
|
}
|
|
1524
|
-
if (!isValidChangeName(name)) {
|
|
1525
|
-
console.error(` ${pc.red("Error:")} change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
1526
|
-
process.exit(1);
|
|
1527
|
-
}
|
|
1528
1580
|
guardRspInitialized();
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
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);
|
|
1533
1595
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
});
|
|
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]`);
|
|
1617
|
+
process.exit(1);
|
|
1618
|
+
}
|
|
1619
|
+
guardRspInitialized();
|
|
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>`);
|
|
1652
|
+
process.exit(1);
|
|
1653
|
+
}
|
|
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)) {
|
|
1899
|
+
console.log();
|
|
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();
|
|
1541
1909
|
}
|
|
1542
1910
|
|
|
1543
1911
|
// src/commands/init.ts
|
|
1544
|
-
import { existsSync as
|
|
1545
|
-
import { mkdir as
|
|
1546
|
-
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";
|
|
1547
1915
|
function generateConfigTemplate() {
|
|
1548
1916
|
const fmtList = (items, indent) => items.map((i) => `${" ".repeat(indent)}# - ${i}`).join("\n");
|
|
1549
1917
|
return `# RSP project configuration
|
|
1550
|
-
#
|
|
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.
|
|
1551
1921
|
#
|
|
1552
1922
|
# Built-in defaults:
|
|
1553
1923
|
# kinds: ${VALID_KINDS.join(", ")}
|
|
1554
1924
|
#
|
|
1555
1925
|
# kinds:
|
|
1556
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
|
|
1557
1932
|
`;
|
|
1558
1933
|
}
|
|
1559
1934
|
async function ensureFile(path, content) {
|
|
1560
|
-
|
|
1561
|
-
return false;
|
|
1562
|
-
await writeFile8(path, content);
|
|
1563
|
-
return true;
|
|
1935
|
+
return ensureManagedFile(path, content, "managed file");
|
|
1564
1936
|
}
|
|
1565
1937
|
function toTitle(projectName) {
|
|
1566
1938
|
return projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
|
|
1567
1939
|
}
|
|
1568
1940
|
async function initProject(args = {}) {
|
|
1569
|
-
const isNew = !
|
|
1941
|
+
const isNew = !existsSync8(RSP_DIR);
|
|
1570
1942
|
const projectName = await detectProjectName();
|
|
1571
1943
|
const mode = args.agentsMode ?? "managed";
|
|
1572
1944
|
if (mode !== "managed" && mode !== "print") {
|
|
1573
1945
|
console.error(` ${pc.red("Error:")} unsupported --agents-mode "${mode}". Use: managed or print`);
|
|
1574
1946
|
process.exit(1);
|
|
1575
1947
|
}
|
|
1948
|
+
const agentsPath = "AGENTS.md";
|
|
1949
|
+
const initialAgentsInspection = inspectManagedFile(agentsPath, "AGENTS.md", { allowMissing: true });
|
|
1950
|
+
if (initialAgentsInspection.issue)
|
|
1951
|
+
throw initialAgentsInspection.issue;
|
|
1576
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
|
+
}
|
|
1577
1966
|
const dirs = [
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
join12(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" }
|
|
1583
1971
|
];
|
|
1584
|
-
for (const
|
|
1585
|
-
|
|
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 });
|
|
1586
1976
|
let created = false;
|
|
1587
|
-
const bundledRules = await
|
|
1588
|
-
created = await ensureFile(
|
|
1589
|
-
created = await ensureFile(
|
|
1590
|
-
|
|
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");
|
|
1591
1987
|
created = createdSpecsIndex || created;
|
|
1592
|
-
const createdDesign = await ensureFile(
|
|
1988
|
+
const createdDesign = await ensureFile(join8(RSP_DIR, "specs", "design.md"), generateDesignContent(projectName));
|
|
1593
1989
|
created = createdDesign || created;
|
|
1594
|
-
const createdArchivesIndex = await ensureFile(
|
|
1990
|
+
const createdArchivesIndex = await ensureFile(join8(RSP_DIR, "archives", "INDEX.md"), "# Archive Index\n");
|
|
1595
1991
|
created = createdArchivesIndex || created;
|
|
1596
|
-
created = await ensureFile(
|
|
1597
|
-
created = await ensureFile(
|
|
1598
|
-
created = await ensureFile(
|
|
1599
|
-
if (args.withProjectRules)
|
|
1600
|
-
created = await ensureFile(join12(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;
|
|
1601
1995
|
if (args.withProjectSetup) {
|
|
1602
|
-
const
|
|
1603
|
-
const
|
|
1996
|
+
const projectSetupRef = resolveWorkRef("project-setup", { executable: true });
|
|
1997
|
+
const changePath = projectSetupRef.path;
|
|
1998
|
+
const focusMarker = resolveFocusMarkerPath(projectSetupRef);
|
|
1604
1999
|
created = await ensureFile(changePath, generateChangeContent("project-setup")) || created;
|
|
1605
2000
|
created = await ensureFile(focusMarker, "") || created;
|
|
1606
2001
|
}
|
|
@@ -1608,27 +2003,26 @@ async function initProject(args = {}) {
|
|
|
1608
2003
|
await buildSpecsIndex({ acquireLock: false });
|
|
1609
2004
|
if (createdArchivesIndex)
|
|
1610
2005
|
await buildArchiveIndex({ acquireLock: false });
|
|
1611
|
-
const agentsPath = "AGENTS.md";
|
|
1612
2006
|
const title = toTitle(projectName);
|
|
1613
2007
|
let agentsUpdated = false;
|
|
1614
|
-
if (!
|
|
1615
|
-
await
|
|
2008
|
+
if (!agentsInspection.exists) {
|
|
2009
|
+
await writeManagedFile(agentsPath, `# ${title}
|
|
1616
2010
|
|
|
1617
2011
|
${upsertRspAgentsBlock("").content}
|
|
1618
|
-
|
|
2012
|
+
`, "AGENTS.md");
|
|
1619
2013
|
created = true;
|
|
1620
2014
|
agentsUpdated = true;
|
|
1621
2015
|
} else {
|
|
1622
|
-
const existing = await
|
|
2016
|
+
const existing = await readFile8(agentsPath, "utf-8");
|
|
1623
2017
|
const next = upsertRspAgentsBlock(existing);
|
|
1624
2018
|
if (next.changed) {
|
|
1625
|
-
await
|
|
2019
|
+
await writeManagedFile(agentsPath, next.content, "AGENTS.md");
|
|
1626
2020
|
created = true;
|
|
1627
2021
|
agentsUpdated = true;
|
|
1628
2022
|
}
|
|
1629
2023
|
}
|
|
1630
2024
|
if (mode === "print") {
|
|
1631
|
-
const managed = await
|
|
2025
|
+
const managed = await readFile8(agentsPath, "utf-8");
|
|
1632
2026
|
console.log(`
|
|
1633
2027
|
${managed}
|
|
1634
2028
|
`);
|
|
@@ -1640,8 +2034,9 @@ ${managed}
|
|
|
1640
2034
|
console.log(`
|
|
1641
2035
|
${pc.green("RSP scaffolded.")}
|
|
1642
2036
|
`);
|
|
1643
|
-
console.log(` Created: .rsp/rules
|
|
2037
|
+
console.log(` Created: .rsp/rsp-rules.md
|
|
1644
2038
|
.rsp/specs/
|
|
2039
|
+
${decisionRecordsPath}/
|
|
1645
2040
|
.rsp/changes/
|
|
1646
2041
|
.rsp/focus.d/
|
|
1647
2042
|
.rsp/archives/
|
|
@@ -1665,210 +2060,744 @@ ${managed}
|
|
|
1665
2060
|
});
|
|
1666
2061
|
}
|
|
1667
2062
|
|
|
1668
|
-
// src/commands/
|
|
1669
|
-
import {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
2063
|
+
// src/commands/ready.ts
|
|
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
|
+
}
|
|
2081
|
+
async function showReady(name, options = {}) {
|
|
2082
|
+
if (!name) {
|
|
2083
|
+
console.error(` ${pc.red("Usage:")} rsp ready <name>`);
|
|
2084
|
+
process.exit(1);
|
|
2085
|
+
}
|
|
2086
|
+
guardRspInitialized();
|
|
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;
|
|
2094
|
+
}
|
|
2095
|
+
const srcPath = workRef.path;
|
|
1677
2096
|
const runtime = [];
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
const
|
|
1686
|
-
const
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
2097
|
+
let content;
|
|
2098
|
+
try {
|
|
2099
|
+
content = await readFile9(srcPath, "utf-8");
|
|
2100
|
+
} catch {
|
|
2101
|
+
console.error(` ${pc.red("Error:")} unable to read .rsp/changes/${name}.md`);
|
|
2102
|
+
process.exit(1);
|
|
2103
|
+
}
|
|
2104
|
+
const dependencyInspection = await inspectChangeDependencies();
|
|
2105
|
+
const readinessDetails = collectArchiveReadiness(content, {
|
|
2106
|
+
activeBlockers: dependencyInspection.activeBlockers.get(name)
|
|
2107
|
+
});
|
|
2108
|
+
const checklist = readinessDetails.warnings;
|
|
2109
|
+
const readiness = {
|
|
2110
|
+
incompleteTasks: readinessDetails.taskTodos.length,
|
|
2111
|
+
incompleteVerify: readinessDetails.verifyTodos.length,
|
|
2112
|
+
activeBlockers: readinessDetails.activeBlockers,
|
|
2113
|
+
missingScenarios: readinessDetails.missingScenarios,
|
|
2114
|
+
deterministic: readinessDetails.deterministic,
|
|
2115
|
+
semantic: readinessDetails.semantic,
|
|
2116
|
+
archiveReady: readinessDetails.archiveReady
|
|
2117
|
+
};
|
|
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);
|
|
2131
|
+
const result = {
|
|
2132
|
+
command: "ready",
|
|
2133
|
+
ok: true,
|
|
2134
|
+
change: name,
|
|
2135
|
+
path: normalizeLogicalPath(srcPath),
|
|
2136
|
+
readiness,
|
|
2137
|
+
durableReview,
|
|
2138
|
+
warnings: checklist,
|
|
2139
|
+
runtime
|
|
2140
|
+
};
|
|
2141
|
+
if (options.json) {
|
|
2142
|
+
emitJson(result, options);
|
|
2143
|
+
return result;
|
|
2144
|
+
}
|
|
2145
|
+
console.log();
|
|
2146
|
+
console.log(` ${pc.bold("Archive readiness for")} ${pc.cyan(name)}`);
|
|
2147
|
+
console.log();
|
|
2148
|
+
if (checklist.length === 0) {
|
|
2149
|
+
console.log(` ${pc.green("\u2713")} Ready to archive. No deterministic warnings found.
|
|
2150
|
+
`);
|
|
2151
|
+
} else {
|
|
2152
|
+
for (const line of checklist)
|
|
2153
|
+
console.log(` ${pc.yellow("\u26A0")} ${line}`);
|
|
2154
|
+
console.log();
|
|
2155
|
+
console.log(` ${pc.dim("Review the warnings above before treating this work as fully closed.")}`);
|
|
2156
|
+
}
|
|
2157
|
+
console.log(` ${pc.dim("Deterministic readiness:")} ${readiness.deterministic === "pass" ? pc.green("pass") : pc.yellow("warnings")}`);
|
|
2158
|
+
console.log(` ${pc.dim("Semantic review:")} ${pc.yellow("needed")}`);
|
|
2159
|
+
console.log(` ${pc.dim("Archive ready:")} ${formatArchiveReady(readiness.archiveReady)}
|
|
2160
|
+
`);
|
|
2161
|
+
console.log(` ${pc.bold("Durable review:")}`);
|
|
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}`);
|
|
2166
|
+
console.log(` ${pc.dim(durableReview.note)}
|
|
2167
|
+
`);
|
|
2168
|
+
return result;
|
|
2169
|
+
}
|
|
2170
|
+
function formatArchiveReady(value) {
|
|
2171
|
+
switch (value) {
|
|
2172
|
+
case "yes":
|
|
2173
|
+
return pc.green("yes");
|
|
2174
|
+
case "no":
|
|
2175
|
+
return pc.yellow("no");
|
|
2176
|
+
case "judgment":
|
|
2177
|
+
return pc.yellow("judgment");
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// src/commands/show.ts
|
|
2182
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2183
|
+
function exitShowError(error, options) {
|
|
2184
|
+
const nextActions = error.code === "no_focused_change" ? ["Run: rsp status", "Run: rsp focus <name>", "Or run: rsp create <name>"] : [];
|
|
2185
|
+
if (options.json) {
|
|
2186
|
+
emitJson({
|
|
2187
|
+
command: "show",
|
|
2188
|
+
ok: false,
|
|
2189
|
+
change: null,
|
|
2190
|
+
contextPaths: [],
|
|
2191
|
+
runtime: [],
|
|
2192
|
+
nextActions,
|
|
2193
|
+
error
|
|
2194
|
+
}, options);
|
|
2195
|
+
} else {
|
|
2196
|
+
console.error(` ${pc.red("Error:")} ${error.message}`);
|
|
2197
|
+
for (const action of nextActions)
|
|
2198
|
+
console.error(` ${pc.dim(action)}`);
|
|
2199
|
+
}
|
|
2200
|
+
process.exit(1);
|
|
2201
|
+
}
|
|
2202
|
+
async function showChange(nameOrFocused, options = {}) {
|
|
2203
|
+
const runtime = [];
|
|
2204
|
+
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
|
|
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));
|
|
2212
|
+
let name;
|
|
2213
|
+
if (options.focused) {
|
|
2214
|
+
if (focused.size === 0) {
|
|
2215
|
+
exitShowError({ code: "no_focused_change", message: "no focused change exists" }, options);
|
|
1743
2216
|
}
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
2217
|
+
if (focused.size > 1) {
|
|
2218
|
+
exitShowError({
|
|
2219
|
+
code: "multiple_focused_changes",
|
|
2220
|
+
message: `multiple focused changes exist (${[...focused].join(", ")}). Specify a name or focus exactly one change.`
|
|
2221
|
+
}, options);
|
|
2222
|
+
}
|
|
2223
|
+
name = [...focused][0];
|
|
2224
|
+
} else if (nameOrFocused) {
|
|
2225
|
+
name = nameOrFocused;
|
|
2226
|
+
} else {
|
|
2227
|
+
exitShowError({ code: "missing_change_name", message: "Usage: rsp show <name|--focused> [--json] [--verbose]" }, options);
|
|
2228
|
+
}
|
|
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;
|
|
2236
|
+
}
|
|
2237
|
+
const srcPath = workRef.path;
|
|
2238
|
+
let content;
|
|
2239
|
+
try {
|
|
2240
|
+
content = await readFile10(srcPath, "utf-8");
|
|
2241
|
+
} catch {
|
|
2242
|
+
exitShowError({ code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, options);
|
|
2243
|
+
}
|
|
2244
|
+
let kind = "\u2014";
|
|
2245
|
+
try {
|
|
2246
|
+
const fm = parseFrontmatter(content);
|
|
2247
|
+
kind = fm?.kind ? String(fm.kind) : "\u2014";
|
|
2248
|
+
} catch (error) {
|
|
2249
|
+
kind = "(invalid)";
|
|
2250
|
+
reportRuntime({
|
|
2251
|
+
code: "frontmatter_parse_failed",
|
|
2252
|
+
operation: "parseFrontmatter",
|
|
2253
|
+
path: srcPath,
|
|
2254
|
+
message: toErrorMessage(error)
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
const isFocused = focused.has(name);
|
|
2258
|
+
const dependencyInspection = await inspectChangeDependencies();
|
|
2259
|
+
const cb = countCheckboxes(content);
|
|
2260
|
+
const blockers = dependencyInspection.activeBlockers.get(name) ?? hasMeaningfulBlockers(content);
|
|
2261
|
+
const scenarios = parseScenarios(content);
|
|
2262
|
+
const readinessDetails = collectArchiveReadiness(content, { activeBlockers: blockers });
|
|
2263
|
+
const readiness = {
|
|
2264
|
+
incompleteTasks: readinessDetails.taskTodos.length,
|
|
2265
|
+
incompleteVerify: readinessDetails.verifyTodos.length,
|
|
2266
|
+
activeBlockers: readinessDetails.activeBlockers,
|
|
2267
|
+
missingScenarios: readinessDetails.missingScenarios,
|
|
2268
|
+
deterministic: readinessDetails.deterministic,
|
|
2269
|
+
semantic: readinessDetails.semantic,
|
|
2270
|
+
archiveReady: readinessDetails.archiveReady
|
|
2271
|
+
};
|
|
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);
|
|
2288
|
+
const result = {
|
|
2289
|
+
command: "show",
|
|
2290
|
+
ok: true,
|
|
2291
|
+
change: {
|
|
1747
2292
|
name,
|
|
2293
|
+
path: normalizeLogicalPath(srcPath),
|
|
1748
2294
|
kind,
|
|
1749
|
-
progress: { done, total },
|
|
1750
|
-
ageDays,
|
|
1751
2295
|
isFocused,
|
|
1752
|
-
|
|
1753
|
-
|
|
2296
|
+
progress: { done: cb.done, total: cb.total },
|
|
2297
|
+
blockers,
|
|
2298
|
+
scenarioCount: scenarios.length,
|
|
2299
|
+
readiness
|
|
2300
|
+
},
|
|
2301
|
+
contextPaths,
|
|
2302
|
+
durableReview,
|
|
2303
|
+
runtime
|
|
2304
|
+
};
|
|
2305
|
+
if (options.json) {
|
|
2306
|
+
emitJson(result, options);
|
|
2307
|
+
return result;
|
|
2308
|
+
}
|
|
2309
|
+
console.log();
|
|
2310
|
+
console.log(` ${pc.bold("Change:")} ${pc.cyan(name)}`);
|
|
2311
|
+
console.log(` ${pc.dim("Path:")} ${normalizeLogicalPath(srcPath)}`);
|
|
2312
|
+
console.log(` ${pc.dim("Kind:")} ${kind}`);
|
|
2313
|
+
console.log(` ${pc.dim("Focused:")} ${isFocused ? pc.green("yes") : pc.dim("no")}`);
|
|
2314
|
+
console.log(` ${pc.dim("Progress:")} ${cb.done}/${cb.total}`);
|
|
2315
|
+
console.log(` ${pc.dim("Blockers:")} ${blockers ? pc.yellow("yes") : pc.green("no")}`);
|
|
2316
|
+
console.log(` ${pc.dim("Scenarios:")} ${scenarios.length}`);
|
|
2317
|
+
console.log();
|
|
2318
|
+
console.log(` ${pc.bold("Readiness:")}`);
|
|
2319
|
+
console.log(` ${pc.dim("Incomplete tasks:")} ${readiness.incompleteTasks > 0 ? pc.yellow(String(readiness.incompleteTasks)) : pc.green("0")}`);
|
|
2320
|
+
console.log(` ${pc.dim("Incomplete verify:")} ${readiness.incompleteVerify > 0 ? pc.yellow(String(readiness.incompleteVerify)) : pc.green("0")}`);
|
|
2321
|
+
console.log(` ${pc.dim("Active blockers:")} ${readiness.activeBlockers ? pc.yellow("yes") : pc.green("no")}`);
|
|
2322
|
+
console.log(` ${pc.dim("Missing scenarios:")} ${readiness.missingScenarios ? pc.yellow("yes") : pc.green("no")}`);
|
|
2323
|
+
console.log(` ${pc.dim("Deterministic:")} ${readiness.deterministic === "pass" ? pc.green("pass") : pc.yellow("warnings")}`);
|
|
2324
|
+
console.log(` ${pc.dim("Semantic review:")} ${pc.yellow("needed")}`);
|
|
2325
|
+
console.log(` ${pc.dim("Archive ready:")} ${formatArchiveReady2(readiness.archiveReady)}`);
|
|
2326
|
+
console.log();
|
|
2327
|
+
console.log(` ${pc.bold("Context paths:")}`);
|
|
2328
|
+
for (const cp of contextPaths)
|
|
2329
|
+
console.log(` ${pc.dim(cp)}`);
|
|
2330
|
+
console.log();
|
|
2331
|
+
console.log(` ${pc.bold("Durable review:")}`);
|
|
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}`);
|
|
2336
|
+
console.log(` ${pc.dim(durableReview.note)}`);
|
|
2337
|
+
console.log();
|
|
2338
|
+
return result;
|
|
2339
|
+
}
|
|
2340
|
+
function formatArchiveReady2(value) {
|
|
2341
|
+
switch (value) {
|
|
2342
|
+
case "yes":
|
|
2343
|
+
return pc.green("yes");
|
|
2344
|
+
case "no":
|
|
2345
|
+
return pc.yellow("no");
|
|
2346
|
+
case "judgment":
|
|
2347
|
+
return pc.yellow("judgment");
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
|
|
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))
|
|
1754
2413
|
});
|
|
1755
2414
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
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))
|
|
1758
2443
|
return false;
|
|
1759
|
-
|
|
2444
|
+
const [sourceContent, targetContent] = await Promise.all([readFile11(sourcePath), readFile11(targetPath)]);
|
|
2445
|
+
if (!sourceContent.equals(targetContent))
|
|
1760
2446
|
return false;
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
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;
|
|
1765
2477
|
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
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);
|
|
2542
|
+
try {
|
|
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));
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
for (const name of movedPrevious.reverse()) {
|
|
2551
|
+
const target = join9(targetRoot, name);
|
|
2552
|
+
const previous = join9(previousRoot, name);
|
|
2553
|
+
try {
|
|
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));
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
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 });
|
|
2573
|
+
}
|
|
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)
|
|
1770
2593
|
return false;
|
|
1771
|
-
if (
|
|
2594
|
+
if (query.blocked && !record.isBlocked)
|
|
2595
|
+
return false;
|
|
2596
|
+
if (query.stale !== null && (record.ageDays === null || record.ageDays < query.stale))
|
|
1772
2597
|
return false;
|
|
1773
|
-
if (typeof options.stale === "number") {
|
|
1774
|
-
if (record.ageDays === null || record.ageDays < options.stale)
|
|
1775
|
-
return false;
|
|
1776
|
-
}
|
|
1777
2598
|
return true;
|
|
1778
2599
|
});
|
|
1779
|
-
const
|
|
1780
|
-
const
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
focused: [...focusedSet].sort(),
|
|
1789
|
-
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))),
|
|
1790
2609
|
summary: {
|
|
1791
|
-
total:
|
|
1792
|
-
focused:
|
|
1793
|
-
blocked:
|
|
2610
|
+
total: records.length,
|
|
2611
|
+
focused: records.filter((record) => record.output.isFocused).length,
|
|
2612
|
+
blocked: records.filter((record) => record.output.isBlocked).length
|
|
1794
2613
|
},
|
|
1795
|
-
|
|
1796
|
-
|
|
2614
|
+
nextActions,
|
|
2615
|
+
archiveTrend: snapshot.archiveTrend,
|
|
2616
|
+
diagnostics: snapshot.diagnostics,
|
|
2617
|
+
runtime: snapshot.runtime,
|
|
2618
|
+
hasExecutableChanges: snapshot.records.length > 0,
|
|
2619
|
+
ok
|
|
1797
2620
|
};
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
2621
|
+
}
|
|
2622
|
+
function buildStatusNextActions(focusedCount, records, groups, filtered) {
|
|
2623
|
+
const openChanges = records.map((record) => record.name).sort();
|
|
2624
|
+
if (focusedCount > 0)
|
|
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}`];
|
|
2631
|
+
if (openChanges.length === 0)
|
|
2632
|
+
return ["Run: rsp create <name>"];
|
|
2633
|
+
if (filtered) {
|
|
2634
|
+
return [
|
|
2635
|
+
"Run: rsp status",
|
|
2636
|
+
"Run: rsp focus <name>",
|
|
2637
|
+
"Or run: rsp create <name>"
|
|
2638
|
+
];
|
|
1801
2639
|
}
|
|
2640
|
+
const firstChanges = openChanges.slice(0, 3).join(", ");
|
|
2641
|
+
const extra = openChanges.length > 3 ? ` (+${openChanges.length - 3} more)` : "";
|
|
2642
|
+
const recommendation = recommendOpenChange(records, groups);
|
|
2643
|
+
return [
|
|
2644
|
+
`Open changes: ${firstChanges}${extra}`,
|
|
2645
|
+
recommendation ? `Run: rsp focus ${recommendation}` : "Resolve blockers before focusing an open change.",
|
|
2646
|
+
"Or run: rsp create <name>"
|
|
2647
|
+
];
|
|
2648
|
+
}
|
|
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) {
|
|
1802
2696
|
console.log();
|
|
1803
2697
|
console.log(` ${pc.bold("RSP status")}`);
|
|
1804
2698
|
console.log();
|
|
1805
|
-
|
|
1806
|
-
|
|
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(", ")}`);
|
|
1807
2707
|
console.log();
|
|
1808
2708
|
} else {
|
|
1809
|
-
console.log(` ${pc.dim("No focused change.")}
|
|
2709
|
+
console.log(` ${pc.dim("No focused change.")}`);
|
|
2710
|
+
for (const action of view.nextActions)
|
|
2711
|
+
console.log(` ${pc.dim(action)}`);
|
|
1810
2712
|
console.log();
|
|
1811
2713
|
}
|
|
1812
|
-
|
|
1813
|
-
|
|
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.")}
|
|
1814
2722
|
`);
|
|
1815
|
-
return
|
|
2723
|
+
return;
|
|
1816
2724
|
}
|
|
1817
|
-
if (
|
|
2725
|
+
if (view.records.length === 0) {
|
|
1818
2726
|
console.log(` ${pc.dim("No changes match the current filters.")}
|
|
1819
2727
|
`);
|
|
1820
|
-
return
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
const
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
const
|
|
1833
|
-
|
|
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
|
+
}
|
|
1834
2750
|
}
|
|
1835
2751
|
console.log();
|
|
1836
|
-
console.log(` ${pc.bold("Summary:")} ${
|
|
2752
|
+
console.log(` ${pc.bold("Summary:")} ${view.summary.total} change(s), ${view.summary.focused} focused, ${pc.yellow(String(view.summary.blocked))} blocked`);
|
|
1837
2753
|
console.log();
|
|
1838
|
-
printArchiveTrend(
|
|
1839
|
-
if (statusResult.summary.blocked > 0) {
|
|
1840
|
-
const blocked = filteredRecords.filter((r) => r.isBlocked);
|
|
1841
|
-
console.log(` ${pc.yellow("Blocked:")} ${blocked.map((r) => r.name).join(", ")}`);
|
|
1842
|
-
console.log();
|
|
1843
|
-
}
|
|
1844
|
-
return statusResult;
|
|
2754
|
+
printArchiveTrend(view.archiveTrend);
|
|
1845
2755
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
const
|
|
1852
|
-
const
|
|
1853
|
-
const
|
|
1854
|
-
|
|
1855
|
-
while ((match = dateRe.exec(content)) !== null) {
|
|
1856
|
-
const month = match[1];
|
|
1857
|
-
counts[month] = (counts[month] || 0) + 1;
|
|
1858
|
-
}
|
|
1859
|
-
const months = Object.keys(counts).sort();
|
|
1860
|
-
if (months.length === 0)
|
|
1861
|
-
return [];
|
|
1862
|
-
return months.map((month) => ({ month, count: counts[month] }));
|
|
1863
|
-
} catch (error) {
|
|
1864
|
-
recordRuntimeDiagnostic(runtime, {
|
|
1865
|
-
code: "archive_index_read_failed",
|
|
1866
|
-
operation: "readFile",
|
|
1867
|
-
path: indexPath,
|
|
1868
|
-
message: toErrorMessage(error)
|
|
1869
|
-
}, Boolean(runOptions.verbose) && !runOptions.json);
|
|
1870
|
-
return [];
|
|
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}`);
|
|
1871
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();
|
|
1872
2801
|
}
|
|
1873
2802
|
function printArchiveTrend(trend) {
|
|
1874
2803
|
if (trend.length === 0)
|
|
@@ -1879,54 +2808,102 @@ function printArchiveTrend(trend) {
|
|
|
1879
2808
|
console.log();
|
|
1880
2809
|
}
|
|
1881
2810
|
|
|
1882
|
-
// src/
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
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
|
+
}
|
|
1907
2859
|
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
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 };
|
|
1927
2894
|
}
|
|
1928
2895
|
|
|
1929
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
|
+
}
|
|
1930
2907
|
var initCommand = defineCommand({
|
|
1931
2908
|
meta: {
|
|
1932
2909
|
name: "init",
|
|
@@ -1938,11 +2915,6 @@ var initCommand = defineCommand({
|
|
|
1938
2915
|
description: "How to handle AGENTS.md output: managed or print",
|
|
1939
2916
|
default: "managed"
|
|
1940
2917
|
},
|
|
1941
|
-
"with-project-rules": {
|
|
1942
|
-
type: "boolean",
|
|
1943
|
-
description: "Create .rsp/rules/project-rules.md",
|
|
1944
|
-
default: false
|
|
1945
|
-
},
|
|
1946
2918
|
"with-project-setup": {
|
|
1947
2919
|
type: "boolean",
|
|
1948
2920
|
description: "Create .rsp/changes/project-setup.md and focus it",
|
|
@@ -1952,7 +2924,6 @@ var initCommand = defineCommand({
|
|
|
1952
2924
|
async run({ args }) {
|
|
1953
2925
|
await initProject({
|
|
1954
2926
|
agentsMode: args["agents-mode"] === "print" ? "print" : "managed",
|
|
1955
|
-
withProjectRules: Boolean(args["with-project-rules"]),
|
|
1956
2927
|
withProjectSetup: Boolean(args["with-project-setup"])
|
|
1957
2928
|
});
|
|
1958
2929
|
}
|
|
@@ -1971,27 +2942,58 @@ var createCommand = defineCommand({
|
|
|
1971
2942
|
kind: {
|
|
1972
2943
|
type: "string",
|
|
1973
2944
|
description: "Optional kind for a kind-aware template (feature, fix, refactor, docs, ops, research)"
|
|
2945
|
+
},
|
|
2946
|
+
lite: {
|
|
2947
|
+
type: "boolean",
|
|
2948
|
+
description: "Use a shorter change template while keeping the required RSP sections",
|
|
2949
|
+
default: false
|
|
1974
2950
|
}
|
|
1975
2951
|
},
|
|
1976
2952
|
async run({ args }) {
|
|
1977
2953
|
const summary = Array.isArray(args._) && args._.length > 1 ? args._.slice(1).join(" ") : "";
|
|
1978
|
-
await createChange(args.name, summary, args.kind);
|
|
2954
|
+
await createChange(args.name, summary, args.kind, { lite: Boolean(args.lite) });
|
|
1979
2955
|
}
|
|
1980
2956
|
});
|
|
1981
|
-
var
|
|
2957
|
+
var groupCreateCommand = defineCommand({
|
|
1982
2958
|
meta: {
|
|
1983
|
-
name: "
|
|
1984
|
-
description: "Create .rsp/
|
|
2959
|
+
name: "create",
|
|
2960
|
+
description: "Create an unfocused .rsp/changes/<group>/00-brief.md"
|
|
1985
2961
|
},
|
|
1986
2962
|
args: {
|
|
1987
2963
|
name: {
|
|
1988
2964
|
type: "positional",
|
|
1989
|
-
description: "
|
|
2965
|
+
description: "Change Group name",
|
|
1990
2966
|
required: true
|
|
1991
2967
|
}
|
|
1992
2968
|
},
|
|
1993
2969
|
async run({ args }) {
|
|
1994
|
-
|
|
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
|
+
})
|
|
1995
2997
|
}
|
|
1996
2998
|
});
|
|
1997
2999
|
var addSpecCommand = defineCommand({
|
|
@@ -2013,10 +3015,9 @@ var addSpecCommand = defineCommand({
|
|
|
2013
3015
|
var addCommand = defineCommand({
|
|
2014
3016
|
meta: {
|
|
2015
3017
|
name: "add",
|
|
2016
|
-
description: "Add optional
|
|
3018
|
+
description: "Add an optional durable spec file"
|
|
2017
3019
|
},
|
|
2018
3020
|
subCommands: {
|
|
2019
|
-
rules: addRulesCommand,
|
|
2020
3021
|
spec: addSpecCommand
|
|
2021
3022
|
}
|
|
2022
3023
|
});
|
|
@@ -2026,14 +3027,19 @@ var archiveCommand = defineCommand({
|
|
|
2026
3027
|
description: "Archive an open change into .rsp/archives/"
|
|
2027
3028
|
},
|
|
2028
3029
|
args: {
|
|
2029
|
-
name: {
|
|
3030
|
+
"name": {
|
|
2030
3031
|
type: "positional",
|
|
2031
3032
|
description: "Change name",
|
|
2032
3033
|
required: true
|
|
3034
|
+
},
|
|
3035
|
+
"dry-run": {
|
|
3036
|
+
type: "boolean",
|
|
3037
|
+
description: "Preview archive readiness without moving the change",
|
|
3038
|
+
default: false
|
|
2033
3039
|
}
|
|
2034
3040
|
},
|
|
2035
3041
|
async run({ args }) {
|
|
2036
|
-
await archiveChange(args.name);
|
|
3042
|
+
await archiveChange(args.name, { dryRun: Boolean(args["dry-run"]) });
|
|
2037
3043
|
}
|
|
2038
3044
|
});
|
|
2039
3045
|
var focusCommand = defineCommand({
|
|
@@ -2093,6 +3099,11 @@ var statusCommand = defineCommand({
|
|
|
2093
3099
|
description: "Print machine-readable JSON output",
|
|
2094
3100
|
default: false
|
|
2095
3101
|
},
|
|
3102
|
+
compact: {
|
|
3103
|
+
type: "boolean",
|
|
3104
|
+
description: "Print JSON without indentation (requires --json)",
|
|
3105
|
+
default: false
|
|
3106
|
+
},
|
|
2096
3107
|
verbose: {
|
|
2097
3108
|
type: "boolean",
|
|
2098
3109
|
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
@@ -2101,41 +3112,54 @@ var statusCommand = defineCommand({
|
|
|
2101
3112
|
},
|
|
2102
3113
|
async run({ args }) {
|
|
2103
3114
|
const stale = args.stale === void 0 ? void 0 : Number(args.stale);
|
|
2104
|
-
if (
|
|
3115
|
+
if (stale !== void 0 && (Number.isFinite(stale) === false || Number.isInteger(stale) === false || stale < 0)) {
|
|
2105
3116
|
if (args.json) {
|
|
2106
|
-
|
|
3117
|
+
emitJson(toStatusJsonError({
|
|
2107
3118
|
code: "invalid_stale_filter",
|
|
2108
3119
|
message: "--stale must be a non-negative integer number of days"
|
|
2109
3120
|
}, {
|
|
2110
3121
|
focused: Boolean(args.focused),
|
|
2111
3122
|
blocked: Boolean(args.blocked)
|
|
2112
|
-
});
|
|
3123
|
+
}), { compact: Boolean(args.compact) });
|
|
2113
3124
|
} else {
|
|
2114
3125
|
console.error(` Error: --stale must be a non-negative integer number of days`);
|
|
2115
3126
|
}
|
|
2116
3127
|
process.exit(1);
|
|
2117
3128
|
}
|
|
2118
|
-
await showStatus({
|
|
3129
|
+
const result = await showStatus({
|
|
2119
3130
|
focused: Boolean(args.focused),
|
|
2120
3131
|
blocked: Boolean(args.blocked),
|
|
2121
3132
|
stale
|
|
2122
3133
|
}, {
|
|
2123
3134
|
json: Boolean(args.json),
|
|
3135
|
+
compact: Boolean(args.compact),
|
|
2124
3136
|
verbose: Boolean(args.verbose)
|
|
2125
3137
|
});
|
|
3138
|
+
if (!result.ok)
|
|
3139
|
+
process.exit(1);
|
|
2126
3140
|
}
|
|
2127
3141
|
});
|
|
2128
3142
|
var checkCommand = defineCommand({
|
|
2129
3143
|
meta: {
|
|
2130
3144
|
name: "check",
|
|
2131
|
-
description: "Validate changes (frontmatter, sections, deltas, scenarios)"
|
|
3145
|
+
description: "Validate changes (frontmatter, sections, template text, deltas, scenarios)"
|
|
2132
3146
|
},
|
|
2133
3147
|
args: {
|
|
3148
|
+
focused: {
|
|
3149
|
+
type: "boolean",
|
|
3150
|
+
description: "Only validate currently focused changes",
|
|
3151
|
+
default: false
|
|
3152
|
+
},
|
|
2134
3153
|
json: {
|
|
2135
3154
|
type: "boolean",
|
|
2136
3155
|
description: "Print machine-readable JSON output",
|
|
2137
3156
|
default: false
|
|
2138
3157
|
},
|
|
3158
|
+
compact: {
|
|
3159
|
+
type: "boolean",
|
|
3160
|
+
description: "Print JSON without indentation (requires --json)",
|
|
3161
|
+
default: false
|
|
3162
|
+
},
|
|
2139
3163
|
verbose: {
|
|
2140
3164
|
type: "boolean",
|
|
2141
3165
|
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
@@ -2143,7 +3167,12 @@ var checkCommand = defineCommand({
|
|
|
2143
3167
|
}
|
|
2144
3168
|
},
|
|
2145
3169
|
async run({ args }) {
|
|
2146
|
-
const result = await runCheck({
|
|
3170
|
+
const result = await runCheck({
|
|
3171
|
+
focused: Boolean(args.focused),
|
|
3172
|
+
json: Boolean(args.json),
|
|
3173
|
+
compact: Boolean(args.compact),
|
|
3174
|
+
verbose: Boolean(args.verbose)
|
|
3175
|
+
});
|
|
2147
3176
|
if (!result.ok)
|
|
2148
3177
|
process.exit(1);
|
|
2149
3178
|
}
|
|
@@ -2151,10 +3180,46 @@ var checkCommand = defineCommand({
|
|
|
2151
3180
|
var updateCommand = defineCommand({
|
|
2152
3181
|
meta: {
|
|
2153
3182
|
name: "update",
|
|
2154
|
-
description: "Refresh RSP project structure after upgrade (
|
|
3183
|
+
description: "Refresh RSP project structure after upgrade (fallback protocol, AGENTS, indices)"
|
|
2155
3184
|
},
|
|
2156
3185
|
async run() {
|
|
2157
|
-
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
|
|
2158
3223
|
}
|
|
2159
3224
|
});
|
|
2160
3225
|
var doctorCommand = defineCommand({
|
|
@@ -2168,49 +3233,220 @@ var doctorCommand = defineCommand({
|
|
|
2168
3233
|
description: "Print machine-readable JSON output",
|
|
2169
3234
|
default: false
|
|
2170
3235
|
},
|
|
3236
|
+
compact: {
|
|
3237
|
+
type: "boolean",
|
|
3238
|
+
description: "Print JSON without indentation (requires --json)",
|
|
3239
|
+
default: false
|
|
3240
|
+
},
|
|
2171
3241
|
verbose: {
|
|
2172
3242
|
type: "boolean",
|
|
2173
3243
|
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
2174
3244
|
default: false
|
|
3245
|
+
},
|
|
3246
|
+
fix: {
|
|
3247
|
+
type: "boolean",
|
|
3248
|
+
description: "Run safe deterministic repairs before reporting diagnostics",
|
|
3249
|
+
default: false
|
|
2175
3250
|
}
|
|
2176
3251
|
},
|
|
2177
3252
|
async run({ args }) {
|
|
2178
|
-
const result = await runDoctor({ json: Boolean(args.json), verbose: Boolean(args.verbose) });
|
|
3253
|
+
const result = await runDoctor({ json: Boolean(args.json), compact: Boolean(args.compact), verbose: Boolean(args.verbose), fix: Boolean(args.fix) });
|
|
2179
3254
|
if (!result.ok)
|
|
2180
3255
|
process.exit(1);
|
|
2181
3256
|
}
|
|
2182
3257
|
});
|
|
3258
|
+
var readyCommand = defineCommand({
|
|
3259
|
+
meta: {
|
|
3260
|
+
name: "ready",
|
|
3261
|
+
description: "Preview archive readiness for a change without moving it"
|
|
3262
|
+
},
|
|
3263
|
+
args: {
|
|
3264
|
+
name: {
|
|
3265
|
+
type: "positional",
|
|
3266
|
+
description: "Change name",
|
|
3267
|
+
required: true
|
|
3268
|
+
},
|
|
3269
|
+
json: {
|
|
3270
|
+
type: "boolean",
|
|
3271
|
+
description: "Print machine-readable JSON output",
|
|
3272
|
+
default: false
|
|
3273
|
+
},
|
|
3274
|
+
compact: {
|
|
3275
|
+
type: "boolean",
|
|
3276
|
+
description: "Print JSON without indentation (requires --json)",
|
|
3277
|
+
default: false
|
|
3278
|
+
},
|
|
3279
|
+
verbose: {
|
|
3280
|
+
type: "boolean",
|
|
3281
|
+
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
3282
|
+
default: false
|
|
3283
|
+
}
|
|
3284
|
+
},
|
|
3285
|
+
async run({ args }) {
|
|
3286
|
+
await showReady(args.name, { json: Boolean(args.json), compact: Boolean(args.compact), verbose: Boolean(args.verbose) });
|
|
3287
|
+
}
|
|
3288
|
+
});
|
|
3289
|
+
var showCommand = defineCommand({
|
|
3290
|
+
meta: {
|
|
3291
|
+
name: "show",
|
|
3292
|
+
description: "Show change context with readiness signals and relevant paths"
|
|
3293
|
+
},
|
|
3294
|
+
args: {
|
|
3295
|
+
name: {
|
|
3296
|
+
type: "positional",
|
|
3297
|
+
description: "Change name (use --focused instead to show the currently focused change)",
|
|
3298
|
+
required: false
|
|
3299
|
+
},
|
|
3300
|
+
focused: {
|
|
3301
|
+
type: "boolean",
|
|
3302
|
+
description: "Show the currently focused change",
|
|
3303
|
+
default: false
|
|
3304
|
+
},
|
|
3305
|
+
json: {
|
|
3306
|
+
type: "boolean",
|
|
3307
|
+
description: "Print machine-readable JSON output",
|
|
3308
|
+
default: false
|
|
3309
|
+
},
|
|
3310
|
+
compact: {
|
|
3311
|
+
type: "boolean",
|
|
3312
|
+
description: "Print JSON without indentation (requires --json)",
|
|
3313
|
+
default: false
|
|
3314
|
+
},
|
|
3315
|
+
verbose: {
|
|
3316
|
+
type: "boolean",
|
|
3317
|
+
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
3318
|
+
default: false
|
|
3319
|
+
}
|
|
3320
|
+
},
|
|
3321
|
+
async run({ args }) {
|
|
3322
|
+
await showChange(args.name, {
|
|
3323
|
+
focused: Boolean(args.focused),
|
|
3324
|
+
json: Boolean(args.json),
|
|
3325
|
+
compact: Boolean(args.compact),
|
|
3326
|
+
verbose: Boolean(args.verbose)
|
|
3327
|
+
});
|
|
3328
|
+
}
|
|
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
|
+
});
|
|
2183
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);
|
|
2184
3410
|
const version = await getVersion();
|
|
2185
3411
|
const main = defineCommand({
|
|
2186
3412
|
meta: {
|
|
2187
3413
|
name: "rsp",
|
|
2188
3414
|
version,
|
|
2189
|
-
description: "RSP (
|
|
3415
|
+
description: "RSP (Reliable Software Practice), a repository-native engineering workflow for humans and AI agents"
|
|
2190
3416
|
},
|
|
2191
3417
|
subCommands: {
|
|
2192
3418
|
init: initCommand,
|
|
2193
3419
|
add: addCommand,
|
|
2194
3420
|
create: createCommand,
|
|
3421
|
+
group: groupCommand,
|
|
2195
3422
|
focus: focusCommand,
|
|
2196
3423
|
unfocus: unfocusCommand,
|
|
2197
3424
|
archive: archiveCommand,
|
|
3425
|
+
ready: readyCommand,
|
|
3426
|
+
show: showCommand,
|
|
3427
|
+
history: historyCommand,
|
|
2198
3428
|
status: statusCommand,
|
|
2199
3429
|
check: checkCommand,
|
|
2200
3430
|
update: updateCommand,
|
|
2201
|
-
doctor: doctorCommand
|
|
3431
|
+
doctor: doctorCommand,
|
|
3432
|
+
skills: skillsCommand
|
|
2202
3433
|
}
|
|
2203
3434
|
});
|
|
2204
3435
|
await runMain(main, { rawArgs });
|
|
2205
3436
|
}
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
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);
|
|
2210
3442
|
console.error(` Error: ${message}`);
|
|
2211
|
-
process.
|
|
2212
|
-
}
|
|
3443
|
+
process.exitCode = 1;
|
|
3444
|
+
}
|
|
2213
3445
|
}
|
|
3446
|
+
var isMain = process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
3447
|
+
if (isMain)
|
|
3448
|
+
await runCliMain();
|
|
2214
3449
|
export {
|
|
2215
|
-
runCli
|
|
3450
|
+
runCli,
|
|
3451
|
+
runCliMain
|
|
2216
3452
|
};
|