@oevery/rsp 3.1.0-beta.1 → 3.1.0-beta.3
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 +101 -87
- package/README.zh-CN.md +88 -64
- package/dist/{chunk-QLXIDVKK.mjs → chunk-7KKAUBKN.mjs} +309 -1255
- package/dist/chunk-JOMFFQRG.mjs +99 -0
- package/dist/chunk-NX55NIC3.mjs +358 -0
- package/dist/chunk-OTJFL6XH.mjs +1022 -0
- package/dist/cli.mjs +533 -561
- package/dist/entry-WZDVKDUM.mjs +174 -0
- package/dist/{entry-LAPR3TNI.mjs → entry-X23GPCVS.mjs} +8 -92
- package/package.json +11 -10
- package/rules/rsp-rules.md +16 -14
- package/skills/rsp/SKILL.md +48 -176
- package/skills/rsp/references/conflict-handling.md +11 -0
- package/skills/rsp/references/durable-review.md +17 -0
- package/skills/rsp/references/groups-dependencies.md +11 -0
- package/skills/rsp/references/managed-routing.md +51 -0
- package/skills/rsp/references/setup-repair.md +11 -0
- package/skills/rsp-commit/SKILL.md +42 -0
- package/skills/rsp-design/SKILL.md +14 -9
- package/skills/rsp-manage/SKILL.md +38 -26
- package/skills/rsp-release-docs/SKILL.md +36 -128
- package/skills/rsp-release-docs/references/evidence-and-surfaces.md +21 -0
- package/skills/rsp-release-docs/references/output-contracts.md +0 -18
- package/skills/rsp-release-docs/references/publication-lifecycle.md +24 -0
- package/skills/{rsp-address-review → rsp-resolve-findings}/SKILL.md +7 -7
- package/skills/rsp-shape/SKILL.md +19 -17
- package/skills/rsp-shape/references/complex-shaping.md +2 -2
- package/skills/rsp-structural-audit/SKILL.md +71 -0
- package/skills/rsp-structural-audit/references/structural-lenses.md +41 -0
package/dist/cli.mjs
CHANGED
|
@@ -1,22 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
inspectPackagedSkillInventory,
|
|
4
|
+
installPackagedSkills,
|
|
5
|
+
printPackagedSkillInventory,
|
|
6
|
+
printSkillInstallResult
|
|
7
|
+
} from "./chunk-NX55NIC3.mjs";
|
|
8
|
+
import {
|
|
4
9
|
ArchiveHistoryError,
|
|
10
|
+
GROUP_BRIEF_FILENAME,
|
|
11
|
+
HISTORY_DEFAULT_LIMIT,
|
|
12
|
+
HISTORY_MAX_LIMIT,
|
|
13
|
+
WorkRefError,
|
|
14
|
+
generateGroupBriefContent,
|
|
15
|
+
hasArchivedGroupBrief,
|
|
16
|
+
historyInspectionComplete,
|
|
17
|
+
inspectArchiveHistory,
|
|
18
|
+
inspectArchiveTree,
|
|
19
|
+
inspectChangeDependencies,
|
|
20
|
+
inspectChangeGroups,
|
|
21
|
+
inspectFocusTree,
|
|
22
|
+
inspectProjectStatus,
|
|
23
|
+
inspectWorkTree,
|
|
24
|
+
projectDependencyForest,
|
|
25
|
+
queryArchiveHistory,
|
|
26
|
+
readArchiveHistoryDetail,
|
|
27
|
+
resolveArchiveDirectory,
|
|
28
|
+
resolveExecutableChange,
|
|
29
|
+
resolveFocusMarkerPath,
|
|
30
|
+
resolveWorkRef,
|
|
31
|
+
selectArchiveHistoryRecord
|
|
32
|
+
} from "./chunk-7KKAUBKN.mjs";
|
|
33
|
+
import {
|
|
34
|
+
ARCHIVES_DIR,
|
|
5
35
|
CHANGES_DIR,
|
|
6
36
|
CONFIG_PATH,
|
|
7
37
|
DEFAULT_DECISION_RECORDS_PATH,
|
|
8
38
|
FOCUS_DIR,
|
|
9
|
-
GROUP_BRIEF_FILENAME,
|
|
10
|
-
HISTORY_DEFAULT_LIMIT,
|
|
11
|
-
HISTORY_MAX_LIMIT,
|
|
12
39
|
LOCK_PATH,
|
|
13
|
-
ManagedPathError,
|
|
14
40
|
OBSOLETE_RSP_RULES_PATH,
|
|
15
41
|
PKG_ROOT,
|
|
16
42
|
RSP_DIR,
|
|
17
43
|
RSP_RULES_PATH,
|
|
18
44
|
VALID_KINDS,
|
|
19
|
-
WorkRefError,
|
|
20
45
|
buildDurableReviewGuidance,
|
|
21
46
|
cleanupEmptyParentDirs,
|
|
22
47
|
clearConfigCache,
|
|
@@ -28,56 +53,37 @@ import {
|
|
|
28
53
|
emitJson,
|
|
29
54
|
ensureDecisionRecordsDirectory,
|
|
30
55
|
ensureManagedFile,
|
|
31
|
-
extractSection,
|
|
32
56
|
generateChangeContent,
|
|
33
57
|
generateDesignContent,
|
|
34
|
-
generateGroupBriefContent,
|
|
35
58
|
generateSpecContent,
|
|
36
59
|
getDurableReviewCandidateTargets,
|
|
37
60
|
getVersion,
|
|
38
61
|
guardRspInitialized,
|
|
39
|
-
hasArchivedGroupBrief,
|
|
40
62
|
hasMeaningfulBlockers,
|
|
41
63
|
hasRspAgentsBlock,
|
|
42
|
-
historyInspectionComplete,
|
|
43
|
-
inspectArchiveHistory,
|
|
44
|
-
inspectArchiveTree,
|
|
45
|
-
inspectChangeDependencies,
|
|
46
|
-
inspectChangeGroups,
|
|
47
|
-
inspectFocusTree,
|
|
48
64
|
inspectManagedDirectory,
|
|
49
65
|
inspectManagedFile,
|
|
50
66
|
inspectManagedFileTree,
|
|
51
|
-
inspectProjectStatus,
|
|
52
67
|
inspectRspConfig,
|
|
53
68
|
inspectUnsupportedRules,
|
|
54
|
-
inspectWorkTree,
|
|
55
69
|
isValidSpecName,
|
|
56
70
|
loadRspConfig,
|
|
57
71
|
normalizeLogicalPath,
|
|
58
72
|
parseFrontmatter,
|
|
59
73
|
parseScenarios,
|
|
60
74
|
pc,
|
|
61
|
-
projectDependencyForest,
|
|
62
|
-
queryArchiveHistory,
|
|
63
|
-
readArchiveHistoryDetail,
|
|
64
75
|
recordRuntimeDiagnostic,
|
|
65
76
|
requireManagedDirectory,
|
|
66
|
-
resolveArchiveDirectory,
|
|
67
77
|
resolveDecisionRecordsPath,
|
|
68
|
-
resolveExecutableChange,
|
|
69
|
-
resolveFocusMarkerPath,
|
|
70
78
|
resolveKinds,
|
|
71
79
|
resolveManagedDirectoryChain,
|
|
72
80
|
resolveRequiredSections,
|
|
73
|
-
resolveWorkRef,
|
|
74
|
-
selectArchiveHistoryRecord,
|
|
75
81
|
toErrorMessage,
|
|
76
82
|
upsertRspAgentsBlock,
|
|
77
83
|
validateDecisionRecordsFilesystemPath,
|
|
78
84
|
walkMarkdownFiles,
|
|
79
85
|
writeManagedFile
|
|
80
|
-
} from "./chunk-
|
|
86
|
+
} from "./chunk-OTJFL6XH.mjs";
|
|
81
87
|
|
|
82
88
|
// src/cli.ts
|
|
83
89
|
import { resolve } from "path";
|
|
@@ -85,7 +91,7 @@ import { fileURLToPath } from "url";
|
|
|
85
91
|
import { defineCommand, runMain } from "citty";
|
|
86
92
|
|
|
87
93
|
// src/commands/add-spec.ts
|
|
88
|
-
import { mkdir as mkdir2, unlink as
|
|
94
|
+
import { mkdir as mkdir2, unlink as unlink3, writeFile as writeFile2 } from "fs/promises";
|
|
89
95
|
import { join as join2 } from "path";
|
|
90
96
|
|
|
91
97
|
// src/core/lock.ts
|
|
@@ -150,12 +156,51 @@ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
// src/commands/specs-index.ts
|
|
153
|
-
import {
|
|
154
|
-
import {
|
|
155
|
-
|
|
156
|
-
|
|
159
|
+
import { readFile as readFile2, unlink as unlink2 } from "fs/promises";
|
|
160
|
+
import { basename, dirname, isAbsolute, join, relative, sep } from "path";
|
|
161
|
+
var SPECS_INDEX_FILENAME = "00-index.md";
|
|
162
|
+
var LEGACY_SPECS_INDEX_FILENAME = "INDEX.md";
|
|
163
|
+
async function buildSpecsIndex(options = {}) {
|
|
164
|
+
const { acquireLock = true, quiet = false, affectedDirectory } = options;
|
|
157
165
|
if (acquireLock)
|
|
158
|
-
return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false, quiet }));
|
|
166
|
+
return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false, quiet, affectedDirectory }));
|
|
167
|
+
const plan = await planSpecsIndexes(affectedDirectory);
|
|
168
|
+
for (const write of plan.writes)
|
|
169
|
+
await writeManagedFile(write.path, write.content, "Specs index");
|
|
170
|
+
for (const path of plan.removals)
|
|
171
|
+
await unlink2(path);
|
|
172
|
+
const changed = plan.writes.length > 0 || plan.removals.length > 0;
|
|
173
|
+
if (!quiet) {
|
|
174
|
+
if (changed)
|
|
175
|
+
console.log(` ${pc.green("Specs indexes updated:")} ${plan.writes.length} written, ${plan.removals.length} removed.
|
|
176
|
+
`);
|
|
177
|
+
else
|
|
178
|
+
console.log(` ${pc.dim("Specs indexes already up to date.")}
|
|
179
|
+
`);
|
|
180
|
+
}
|
|
181
|
+
return changed;
|
|
182
|
+
}
|
|
183
|
+
async function inspectSpecsIndexHealth() {
|
|
184
|
+
try {
|
|
185
|
+
const plan = await planSpecsIndexes();
|
|
186
|
+
return {
|
|
187
|
+
ok: plan.writes.length === 0 && plan.removals.length === 0,
|
|
188
|
+
expectedPaths: plan.expectedPaths.map(toProjectPath),
|
|
189
|
+
stalePaths: plan.writes.map((write) => toProjectPath(write.path)),
|
|
190
|
+
obsoletePaths: plan.removals.map(toProjectPath),
|
|
191
|
+
issues: []
|
|
192
|
+
};
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
expectedPaths: [],
|
|
197
|
+
stalePaths: [],
|
|
198
|
+
obsoletePaths: [],
|
|
199
|
+
issues: [error instanceof Error ? error.message : String(error)]
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function planSpecsIndexes(affectedDirectory) {
|
|
159
204
|
const specsDir = join(RSP_DIR, "specs");
|
|
160
205
|
const configInspection = await inspectRspConfig();
|
|
161
206
|
if (configInspection.issues.length > 0)
|
|
@@ -164,91 +209,154 @@ async function buildSpecsIndex({ acquireLock = true, quiet = false } = {}) {
|
|
|
164
209
|
const inspection = await inspectManagedFileTree(specsDir, "Specs", { allowMissing: true });
|
|
165
210
|
if (inspection.issues.length > 0)
|
|
166
211
|
throw inspection.issues[0];
|
|
167
|
-
if (!inspection.rootExists)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
212
|
+
if (!inspection.rootExists)
|
|
213
|
+
return { writes: [], removals: [], expectedPaths: [] };
|
|
214
|
+
const excludedDirectories = [DEFAULT_DECISION_RECORDS_PATH, decisionRecordsPath].filter((path) => isInside(specsDir, path));
|
|
215
|
+
const isExcluded = (path) => excludedDirectories.some((directory) => isInside(directory, path));
|
|
216
|
+
const managedIndexFiles = inspection.files.filter((path) => !isExcluded(path)).filter((path) => basename(path) === SPECS_INDEX_FILENAME || path === join(specsDir, LEGACY_SPECS_INDEX_FILENAME));
|
|
217
|
+
const recognizedIndexes = /* @__PURE__ */ new Set();
|
|
218
|
+
for (const path of managedIndexFiles) {
|
|
219
|
+
let metadata;
|
|
220
|
+
try {
|
|
221
|
+
metadata = parseFrontmatter(await readFile2(path, "utf-8"));
|
|
222
|
+
} catch (error) {
|
|
223
|
+
throw new Error(`unable to inspect reserved Specs index ${toProjectPath(path)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
224
|
+
}
|
|
225
|
+
const expectedSourceDirectory = path === join(specsDir, LEGACY_SPECS_INDEX_FILENAME) ? toProjectPath(specsDir) : toProjectPath(dirname(path));
|
|
226
|
+
if (metadata?.kind !== "generated-index" || metadata.index_type !== "specs" || metadata.source_dir !== expectedSourceDirectory) {
|
|
227
|
+
throw new Error(`reserved Specs index is not recognized generated content: ${toProjectPath(path)}`);
|
|
228
|
+
}
|
|
229
|
+
recognizedIndexes.add(path);
|
|
230
|
+
}
|
|
231
|
+
const specFiles = inspection.files.filter((path) => path.endsWith(".md")).filter((path) => !isExcluded(path)).filter((path) => basename(path) !== SPECS_INDEX_FILENAME).filter((path) => path !== join(specsDir, LEGACY_SPECS_INDEX_FILENAME)).sort();
|
|
232
|
+
const directories = /* @__PURE__ */ new Set([specsDir]);
|
|
233
|
+
for (const file of specFiles) {
|
|
234
|
+
let current = dirname(file);
|
|
235
|
+
while (isInside(specsDir, current)) {
|
|
236
|
+
directories.add(current);
|
|
237
|
+
if (current === specsDir)
|
|
238
|
+
break;
|
|
239
|
+
current = dirname(current);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const orderedDirectories = [...directories].sort();
|
|
243
|
+
const expectedPaths = orderedDirectories.map((directory) => join(directory, SPECS_INDEX_FILENAME));
|
|
244
|
+
const expectedSet = new Set(expectedPaths);
|
|
245
|
+
const writableDirectories = affectedDirectory ? new Set(ancestorDirectories(specsDir, affectedDirectory)) : directories;
|
|
246
|
+
const writes = [];
|
|
247
|
+
for (const directory of orderedDirectories) {
|
|
248
|
+
if (!writableDirectories.has(directory))
|
|
249
|
+
continue;
|
|
250
|
+
const directFiles = specFiles.filter((path2) => dirname(path2) === directory);
|
|
251
|
+
const directDirectories = orderedDirectories.filter((path2) => path2 !== directory && dirname(path2) === directory);
|
|
252
|
+
const content = await renderSpecsIndex(specsDir, directory, directFiles, directDirectories);
|
|
253
|
+
const path = join(directory, SPECS_INDEX_FILENAME);
|
|
254
|
+
const existingContent = recognizedIndexes.has(path) ? await readFile2(path, "utf-8") : null;
|
|
255
|
+
if (existingContent !== content)
|
|
256
|
+
writes.push({ path, content });
|
|
257
|
+
}
|
|
258
|
+
const removals = [...recognizedIndexes].filter((path) => path === join(specsDir, LEGACY_SPECS_INDEX_FILENAME) || !expectedSet.has(path) && !affectedDirectory).sort();
|
|
259
|
+
return { writes, removals, expectedPaths };
|
|
260
|
+
}
|
|
261
|
+
async function renderSpecsIndex(specsDir, directory, files, directories) {
|
|
262
|
+
const sourceDir = toProjectPath(directory);
|
|
263
|
+
const scope = normalizeLogicalPath(relative(specsDir, directory));
|
|
264
|
+
const title = scope === "" ? "Specs Index" : `Specs: ${scope}`;
|
|
265
|
+
const entries = [];
|
|
266
|
+
for (const childDirectory of directories) {
|
|
267
|
+
const name = basename(childDirectory);
|
|
268
|
+
entries.push({
|
|
269
|
+
sortKey: `${name}/`,
|
|
270
|
+
line: `| [${escapeLabel(`${name}/`)}](${encodeLink(`${name}/${SPECS_INDEX_FILENAME}`)}) | directory | ${escapeCell(name)} | \u2014 |`
|
|
271
|
+
});
|
|
172
272
|
}
|
|
173
|
-
const
|
|
174
|
-
const name = basename(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
lines
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
273
|
+
for (const path of files) {
|
|
274
|
+
const name = basename(path);
|
|
275
|
+
const content = await readFile2(path, "utf-8");
|
|
276
|
+
const metadata = parseFrontmatter(content);
|
|
277
|
+
const metadataTitle = metadata && typeof metadata.title === "string" ? metadata.title : "";
|
|
278
|
+
const entryTitle = metadataTitle || extractTitle(content) || name.replace(/\.md$/, "");
|
|
279
|
+
const summary = extractSummary(content, metadata?.summary);
|
|
280
|
+
entries.push({
|
|
281
|
+
sortKey: name,
|
|
282
|
+
line: `| [${escapeLabel(name)}](${encodeLink(name)}) | spec | ${escapeCell(entryTitle)} | ${escapeCell(summary || "\u2014")} |`
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
entries.sort((left, right) => left.sortKey < right.sortKey ? -1 : left.sortKey > right.sortKey ? 1 : 0);
|
|
286
|
+
const lines = [
|
|
287
|
+
"---",
|
|
288
|
+
`title: ${JSON.stringify(title)}`,
|
|
289
|
+
`summary: ${JSON.stringify(`Direct Specs and child Spec directories under ${sourceDir}.`)}`,
|
|
290
|
+
"kind: generated-index",
|
|
291
|
+
"index_type: specs",
|
|
292
|
+
`source_dir: ${JSON.stringify(sourceDir)}`,
|
|
293
|
+
`entry_count: ${entries.length}`,
|
|
294
|
+
"---",
|
|
295
|
+
"",
|
|
296
|
+
`# ${title}`,
|
|
297
|
+
"",
|
|
298
|
+
"_Generated direct-child navigation. Edit the linked Specs, then run `rsp update`._",
|
|
299
|
+
""
|
|
300
|
+
];
|
|
301
|
+
if (entries.length === 0) {
|
|
302
|
+
lines.push("_No Specs or child Spec directories yet._");
|
|
193
303
|
} else {
|
|
194
|
-
lines.push("|
|
|
195
|
-
lines.push("
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
`);
|
|
215
|
-
return changed;
|
|
216
|
-
}
|
|
217
|
-
function isInside(directory, filePath) {
|
|
218
|
-
const rel = relative(directory, filePath);
|
|
304
|
+
lines.push("| Entry | Kind | Title | Summary |");
|
|
305
|
+
lines.push("|-------|------|-------|---------|");
|
|
306
|
+
lines.push(...entries.map((entry) => entry.line));
|
|
307
|
+
}
|
|
308
|
+
return lines.join("\n");
|
|
309
|
+
}
|
|
310
|
+
function ancestorDirectories(root, directory) {
|
|
311
|
+
if (!isInside(root, directory))
|
|
312
|
+
throw new Error(`affected Spec directory must stay inside ${toProjectPath(root)}: ${directory}`);
|
|
313
|
+
const result = [];
|
|
314
|
+
let current = directory;
|
|
315
|
+
while (true) {
|
|
316
|
+
result.push(current);
|
|
317
|
+
if (current === root)
|
|
318
|
+
return result;
|
|
319
|
+
current = dirname(current);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function isInside(directory, path) {
|
|
323
|
+
const rel = relative(directory, path);
|
|
219
324
|
return rel === "" || !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
220
325
|
}
|
|
326
|
+
function toProjectPath(path) {
|
|
327
|
+
return normalizeLogicalPath(relative(".", path));
|
|
328
|
+
}
|
|
221
329
|
function extractTitle(content) {
|
|
222
330
|
for (const rawLine of content.split("\n")) {
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
return rawLine.slice(2).trim();
|
|
331
|
+
if (rawLine.startsWith("# "))
|
|
332
|
+
return rawLine.slice(2).trim();
|
|
226
333
|
}
|
|
227
334
|
return "";
|
|
228
335
|
}
|
|
229
336
|
function extractSummary(content, frontmatterSummary) {
|
|
230
337
|
if (typeof frontmatterSummary === "string" && frontmatterSummary.trim() !== "")
|
|
231
338
|
return frontmatterSummary.trim();
|
|
232
|
-
const lines = content.split("\n");
|
|
233
339
|
let inFrontmatter = false;
|
|
234
|
-
for (const rawLine of
|
|
340
|
+
for (const rawLine of content.split("\n")) {
|
|
235
341
|
const line = rawLine.trim();
|
|
236
342
|
if (line === "---" || line === "...") {
|
|
237
343
|
inFrontmatter = !inFrontmatter;
|
|
238
344
|
continue;
|
|
239
345
|
}
|
|
240
|
-
if (inFrontmatter || line === "")
|
|
241
|
-
continue;
|
|
242
|
-
if (line.startsWith("#"))
|
|
243
|
-
continue;
|
|
244
|
-
if (line.startsWith("##"))
|
|
346
|
+
if (inFrontmatter || line === "" || line.startsWith("#"))
|
|
245
347
|
continue;
|
|
246
348
|
return line.replace(/^[-*]\s*/, "").trim();
|
|
247
349
|
}
|
|
248
350
|
return "";
|
|
249
351
|
}
|
|
250
352
|
function escapeCell(value) {
|
|
251
|
-
return value.replace(/\|/g, "\\|");
|
|
353
|
+
return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
354
|
+
}
|
|
355
|
+
function escapeLabel(value) {
|
|
356
|
+
return value.replace(/([\\[\]])/g, "\\$1");
|
|
357
|
+
}
|
|
358
|
+
function encodeLink(value) {
|
|
359
|
+
return encodeURI(value).replace(/\(/g, "%28").replace(/\)/g, "%29");
|
|
252
360
|
}
|
|
253
361
|
|
|
254
362
|
// src/commands/add-spec.ts
|
|
@@ -261,6 +369,10 @@ async function addSpec(name, projectName = "project") {
|
|
|
261
369
|
console.error(` ${pc.red("Error:")} .rsp/specs/decisions/ is reserved for Decision Records`);
|
|
262
370
|
process.exit(1);
|
|
263
371
|
}
|
|
372
|
+
if (["index", "00-index"].includes(name.split("/").at(-1) || "")) {
|
|
373
|
+
console.error(` ${pc.red("Error:")} spec name "index" is reserved for generated local navigation`);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
264
376
|
guardRspInitialized();
|
|
265
377
|
try {
|
|
266
378
|
const configInspection = await inspectRspConfig();
|
|
@@ -287,11 +399,11 @@ async function addSpec(name, projectName = "project") {
|
|
|
287
399
|
try {
|
|
288
400
|
await writeFile2(path, content, { flag: "wx" });
|
|
289
401
|
created = true;
|
|
290
|
-
await buildSpecsIndex({ acquireLock: false });
|
|
402
|
+
await buildSpecsIndex({ acquireLock: false, affectedDirectory: parent });
|
|
291
403
|
} catch (error) {
|
|
292
404
|
if (created) {
|
|
293
405
|
try {
|
|
294
|
-
await
|
|
406
|
+
await unlink3(path);
|
|
295
407
|
await cleanupEmptyParentDirs(path, specsDir);
|
|
296
408
|
} catch {
|
|
297
409
|
}
|
|
@@ -306,126 +418,22 @@ async function addSpec(name, projectName = "project") {
|
|
|
306
418
|
}
|
|
307
419
|
|
|
308
420
|
// src/commands/archive.ts
|
|
309
|
-
import { existsSync
|
|
310
|
-
import { mkdir as mkdir3, readFile as
|
|
311
|
-
import { basename as
|
|
312
|
-
|
|
313
|
-
// src/commands/archive-index.ts
|
|
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 } = {}) {
|
|
318
|
-
if (acquireLock)
|
|
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.")}
|
|
328
|
-
`);
|
|
329
|
-
return false;
|
|
330
|
-
}
|
|
331
|
-
const archiveFiles = inspection.files;
|
|
332
|
-
const lines = [];
|
|
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("---");
|
|
341
|
-
lines.push("");
|
|
342
|
-
lines.push("# Archive Index");
|
|
343
|
-
lines.push("");
|
|
344
|
-
if (archiveFiles.length === 0) {
|
|
345
|
-
lines.push("_No archived changes yet._");
|
|
346
|
-
} else {
|
|
347
|
-
lines.push("| Date | Change | Kind | Summary |");
|
|
348
|
-
lines.push("|------|--------|------|---------|");
|
|
349
|
-
}
|
|
350
|
-
for (const fp of archiveFiles) {
|
|
351
|
-
const base = basename2(fp);
|
|
352
|
-
const archiveRel = normalizeLogicalPath(relative2(ARCHIVES_DIR, fp));
|
|
353
|
-
const archiveDirPart = dirname(archiveRel);
|
|
354
|
-
const dateMatch = base.match(/^(\d{4}-\d{2}-\d{2})_(.+)\.md$/);
|
|
355
|
-
const date = dateMatch ? dateMatch[1] : "\u2014";
|
|
356
|
-
const baseName = dateMatch ? dateMatch[2] : base.replace(/\.md$/, "");
|
|
357
|
-
const changeName = archiveDirPart !== "." ? `${archiveDirPart}/${baseName}` : baseName;
|
|
358
|
-
let summary = "";
|
|
359
|
-
let kind = "";
|
|
360
|
-
try {
|
|
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();
|
|
367
|
-
const proposal = extractSection(content, "Proposal");
|
|
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();
|
|
374
|
-
} catch {
|
|
375
|
-
}
|
|
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).
|
|
386
|
-
`);
|
|
387
|
-
return changed;
|
|
388
|
-
}
|
|
389
|
-
function escapeCell2(value) {
|
|
390
|
-
return value.replace(/\|/g, "\\|");
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// src/commands/archive.ts
|
|
394
|
-
async function archiveChange(name, options = {}) {
|
|
421
|
+
import { existsSync } from "fs";
|
|
422
|
+
import { mkdir as mkdir3, readFile as readFile3, rename, unlink as unlink4 } from "fs/promises";
|
|
423
|
+
import { basename as basename2, join as join3 } from "path";
|
|
424
|
+
async function archiveChange(name) {
|
|
395
425
|
if (!name) {
|
|
396
426
|
console.error(` ${pc.red("Usage:")} rsp archive <name>`);
|
|
397
427
|
process.exit(1);
|
|
398
428
|
}
|
|
399
429
|
guardRspInitialized();
|
|
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;
|
|
421
|
-
}
|
|
422
430
|
try {
|
|
423
431
|
return await withRspLock("archive-change", async () => {
|
|
424
432
|
const workRef = await resolveExecutableChange(name, { mustExist: true });
|
|
425
433
|
const srcPath = workRef.path;
|
|
426
434
|
const archiveSubdir = resolveArchiveDirectory(workRef);
|
|
427
435
|
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
428
|
-
const content = await
|
|
436
|
+
const content = await readFile3(srcPath, "utf-8");
|
|
429
437
|
const dependencyInspection = await inspectChangeDependencies();
|
|
430
438
|
const checklist = collectArchiveChecklist(content, {
|
|
431
439
|
activeBlockers: dependencyInspection.activeBlockers.get(name)
|
|
@@ -436,15 +444,15 @@ async function archiveChange(name, options = {}) {
|
|
|
436
444
|
console.log(` ${pc.dim("Archive will continue. Review the warnings above before treating this work as fully closed.")}
|
|
437
445
|
`);
|
|
438
446
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
439
|
-
const base =
|
|
447
|
+
const base = basename2(workRef.name);
|
|
440
448
|
await mkdir3(archiveSubdir, { recursive: true });
|
|
441
449
|
const archiveName = resolveArchiveName(archiveSubdir, date, base);
|
|
442
|
-
await rename(srcPath,
|
|
450
|
+
await rename(srcPath, join3(archiveSubdir, archiveName));
|
|
443
451
|
const postArchiveWarnings = [];
|
|
444
452
|
let focusCleared = false;
|
|
445
|
-
if (
|
|
453
|
+
if (existsSync(focusEntry)) {
|
|
446
454
|
try {
|
|
447
|
-
await
|
|
455
|
+
await unlink4(focusEntry);
|
|
448
456
|
await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
|
|
449
457
|
focusCleared = true;
|
|
450
458
|
} catch (error) {
|
|
@@ -460,17 +468,12 @@ async function archiveChange(name, options = {}) {
|
|
|
460
468
|
` : "";
|
|
461
469
|
console.log(` ${pc.green("Archived:")} ${archiveName}
|
|
462
470
|
${clearedMsg}`);
|
|
463
|
-
try {
|
|
464
|
-
await buildArchiveIndex({ acquireLock: false });
|
|
465
|
-
} catch (error) {
|
|
466
|
-
postArchiveWarnings.push(`archive index rebuild failed: ${toErrorMessage(error)}`);
|
|
467
|
-
}
|
|
468
471
|
for (const warning of postArchiveWarnings)
|
|
469
472
|
console.log(` ${pc.yellow("\u26A0")} ${warning}`);
|
|
470
473
|
if (postArchiveWarnings.length > 0)
|
|
471
474
|
console.log(` ${pc.dim("Archive completed, but follow-up cleanup was only partially successful.")}
|
|
472
475
|
`);
|
|
473
|
-
if (
|
|
476
|
+
if (existsSync(".git")) {
|
|
474
477
|
console.log(` ${pc.cyan("Git delivery:")}
|
|
475
478
|
`);
|
|
476
479
|
console.log(` git status --short`);
|
|
@@ -486,28 +489,14 @@ ${clearedMsg}`);
|
|
|
486
489
|
throw error;
|
|
487
490
|
}
|
|
488
491
|
}
|
|
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
|
-
}
|
|
502
|
-
}
|
|
503
492
|
function resolveArchiveName(archiveSubdir, date, base) {
|
|
504
493
|
const initialName = `${date}_${base}.md`;
|
|
505
|
-
if (!
|
|
494
|
+
if (!existsSync(join3(archiveSubdir, initialName)))
|
|
506
495
|
return initialName;
|
|
507
496
|
const MAX_SUFFIX = 9999;
|
|
508
497
|
for (let index = 2; index <= MAX_SUFFIX; index++) {
|
|
509
498
|
const candidate = `${date}_${base}-${index}.md`;
|
|
510
|
-
if (!
|
|
499
|
+
if (!existsSync(join3(archiveSubdir, candidate)))
|
|
511
500
|
return candidate;
|
|
512
501
|
}
|
|
513
502
|
throw new WorkRefError(
|
|
@@ -518,7 +507,7 @@ function resolveArchiveName(archiveSubdir, date, base) {
|
|
|
518
507
|
}
|
|
519
508
|
|
|
520
509
|
// src/commands/check.ts
|
|
521
|
-
import { readFile as
|
|
510
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
522
511
|
async function runCheck(options = {}) {
|
|
523
512
|
const diagnostics = [];
|
|
524
513
|
const runtime = [];
|
|
@@ -561,7 +550,7 @@ async function runCheck(options = {}) {
|
|
|
561
550
|
const entryContent = marker.name;
|
|
562
551
|
let markerContent = "";
|
|
563
552
|
try {
|
|
564
|
-
markerContent = (await
|
|
553
|
+
markerContent = (await readFile4(entryPath, "utf-8")).trim();
|
|
565
554
|
} catch (error) {
|
|
566
555
|
addDiagnostic({
|
|
567
556
|
severity: "error",
|
|
@@ -672,7 +661,7 @@ async function runCheck(options = {}) {
|
|
|
672
661
|
const { name, path: fp } = ref;
|
|
673
662
|
let content;
|
|
674
663
|
try {
|
|
675
|
-
content = await
|
|
664
|
+
content = await readFile4(fp, "utf-8");
|
|
676
665
|
} catch (error) {
|
|
677
666
|
addDiagnostic({
|
|
678
667
|
severity: "error",
|
|
@@ -901,8 +890,8 @@ function collectMatchingLines(content, predicate) {
|
|
|
901
890
|
}
|
|
902
891
|
|
|
903
892
|
// src/commands/create.ts
|
|
904
|
-
import { existsSync as
|
|
905
|
-
import { mkdir as mkdir4, unlink as
|
|
893
|
+
import { existsSync as existsSync2 } from "fs";
|
|
894
|
+
import { mkdir as mkdir4, unlink as unlink5, writeFile as writeFile3 } from "fs/promises";
|
|
906
895
|
import { dirname as dirname2 } from "path";
|
|
907
896
|
async function createChange(name, summary = "", kind, options = {}) {
|
|
908
897
|
if (!name) {
|
|
@@ -914,7 +903,7 @@ async function createChange(name, summary = "", kind, options = {}) {
|
|
|
914
903
|
return await withRspLock("create-change", async () => {
|
|
915
904
|
const workRef = await resolveExecutableChange(name);
|
|
916
905
|
const changePath = workRef.path;
|
|
917
|
-
const existed =
|
|
906
|
+
const existed = existsSync2(changePath);
|
|
918
907
|
if (!existed) {
|
|
919
908
|
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
920
909
|
const content = generateChangeContent(name, summary, kind, { lite: Boolean(options.lite) });
|
|
@@ -929,7 +918,7 @@ async function createChange(name, summary = "", kind, options = {}) {
|
|
|
929
918
|
} catch (error) {
|
|
930
919
|
if (changeCreated) {
|
|
931
920
|
try {
|
|
932
|
-
await
|
|
921
|
+
await unlink5(changePath);
|
|
933
922
|
await cleanupEmptyParentDirs(changePath, CHANGES_DIR);
|
|
934
923
|
} catch {
|
|
935
924
|
}
|
|
@@ -956,17 +945,39 @@ async function createChange(name, summary = "", kind, options = {}) {
|
|
|
956
945
|
}
|
|
957
946
|
|
|
958
947
|
// src/commands/doctor.ts
|
|
959
|
-
import { existsSync as
|
|
948
|
+
import { existsSync as existsSync4 } from "fs";
|
|
960
949
|
import { readdir, readFile as readFile7, stat } from "fs/promises";
|
|
961
|
-
import { basename as
|
|
950
|
+
import { basename as basename3, join as join6, relative as relative2 } from "path";
|
|
962
951
|
|
|
963
952
|
// src/commands/update.ts
|
|
964
|
-
import { existsSync as
|
|
953
|
+
import { existsSync as existsSync3 } from "fs";
|
|
965
954
|
import { mkdir as mkdir5, readFile as readFile6, rm, rmdir } from "fs/promises";
|
|
966
955
|
import { join as join5 } from "path";
|
|
956
|
+
|
|
957
|
+
// src/commands/archive-index-migration.ts
|
|
958
|
+
import { readFile as readFile5, unlink as unlink6 } from "fs/promises";
|
|
959
|
+
import { join as join4 } from "path";
|
|
960
|
+
async function removeLegacyArchiveIndex() {
|
|
961
|
+
const path = join4(ARCHIVES_DIR, "INDEX.md");
|
|
962
|
+
const inspection = inspectManagedFile(path, "legacy archive index", { allowMissing: true });
|
|
963
|
+
if (!inspection.exists || inspection.issue)
|
|
964
|
+
return false;
|
|
965
|
+
let frontmatter;
|
|
966
|
+
try {
|
|
967
|
+
frontmatter = parseFrontmatter(await readFile5(path, "utf-8"));
|
|
968
|
+
} catch {
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
if (frontmatter?.kind !== "generated-index" || frontmatter.index_type !== "archives")
|
|
972
|
+
return false;
|
|
973
|
+
await unlink6(path);
|
|
974
|
+
return true;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// src/commands/update.ts
|
|
967
978
|
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
979
|
async function updateProject(options = {}) {
|
|
969
|
-
if (!
|
|
980
|
+
if (!existsSync3(RSP_DIR)) {
|
|
970
981
|
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
971
982
|
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
972
983
|
process.exit(1);
|
|
@@ -997,6 +1008,9 @@ async function updateProject(options = {}) {
|
|
|
997
1008
|
console.log(` ${pc.green("\u2713")} changes/ directory restored`);
|
|
998
1009
|
updated = true;
|
|
999
1010
|
}
|
|
1011
|
+
const archivesInspection = inspectManagedDirectory(join5(RSP_DIR, "archives"), "archive root", { allowMissing: true });
|
|
1012
|
+
if (archivesInspection.issue)
|
|
1013
|
+
throw archivesInspection.issue;
|
|
1000
1014
|
const bundledRules = await readFile6(join5(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
|
|
1001
1015
|
const rulesInspection = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
1002
1016
|
if (rulesInspection.issue)
|
|
@@ -1009,7 +1023,7 @@ async function updateProject(options = {}) {
|
|
|
1009
1023
|
console.log(` ${pc.green("\u2713")} rsp-rules.md updated`);
|
|
1010
1024
|
updated = true;
|
|
1011
1025
|
}
|
|
1012
|
-
if (
|
|
1026
|
+
if (existsSync3(OBSOLETE_RSP_RULES_PATH)) {
|
|
1013
1027
|
await rm(OBSOLETE_RSP_RULES_PATH);
|
|
1014
1028
|
actions.push("obsolete rules/rsp-rules.md removed");
|
|
1015
1029
|
if (!options.quiet)
|
|
@@ -1071,9 +1085,14 @@ async function updateProject(options = {}) {
|
|
|
1071
1085
|
updated = true;
|
|
1072
1086
|
}
|
|
1073
1087
|
const specsIndexChanged = await buildSpecsIndex({ acquireLock: false, quiet: options.quiet });
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1088
|
+
if (specsIndexChanged) {
|
|
1089
|
+
actions.push("generated Specs indexes reconciled");
|
|
1090
|
+
updated = true;
|
|
1091
|
+
}
|
|
1092
|
+
if (await removeLegacyArchiveIndex()) {
|
|
1093
|
+
actions.push("legacy Archive Index removed");
|
|
1094
|
+
if (!options.quiet)
|
|
1095
|
+
console.log(` ${pc.green("\u2713")} legacy Archive Index removed`);
|
|
1077
1096
|
updated = true;
|
|
1078
1097
|
}
|
|
1079
1098
|
if (!options.quiet && inspection.diagnostics.length > 0) {
|
|
@@ -1109,7 +1128,7 @@ async function updateProject(options = {}) {
|
|
|
1109
1128
|
async function runDoctor(options = {}) {
|
|
1110
1129
|
const fixed = [];
|
|
1111
1130
|
let repairIssue = null;
|
|
1112
|
-
if (options.fix &&
|
|
1131
|
+
if (options.fix && existsSync4(RSP_DIR)) {
|
|
1113
1132
|
try {
|
|
1114
1133
|
const update = await updateProject({ quiet: Boolean(options.json) });
|
|
1115
1134
|
fixed.push(...update.actions);
|
|
@@ -1129,17 +1148,12 @@ async function runDoctor(options = {}) {
|
|
|
1129
1148
|
const runtime = [];
|
|
1130
1149
|
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
|
|
1131
1150
|
const designPath = join6(RSP_DIR, "specs", "design.md");
|
|
1132
|
-
|
|
1133
|
-
const archivesIndexPath = join6(RSP_DIR, "archives", "INDEX.md");
|
|
1134
|
-
reportCheck(checks, ".rsp exists", existsSync6(RSP_DIR), "Run: rsp init");
|
|
1151
|
+
reportCheck(checks, ".rsp exists", existsSync4(RSP_DIR), "Run: rsp init");
|
|
1135
1152
|
reportFallbackProtocol(checks);
|
|
1136
1153
|
await reportUnsupportedRules(checks, reportRuntime);
|
|
1137
|
-
reportCheck(checks, "specs/design.md exists",
|
|
1138
|
-
reportCheck(checks, "specs/INDEX.md exists", existsSync6(specsIndexPath), "Run: rsp update");
|
|
1139
|
-
reportCheck(checks, "archives/INDEX.md exists", existsSync6(archivesIndexPath), "Run: rsp update");
|
|
1154
|
+
reportCheck(checks, "specs/design.md exists", existsSync4(designPath), "Run: rsp init");
|
|
1140
1155
|
await checkAgents(checks, reportRuntime);
|
|
1141
|
-
await
|
|
1142
|
-
await checkGeneratedIndex(checks, reportRuntime, archivesIndexPath, "archives", "archives/INDEX.md has generated-index metadata", "Run: rsp update");
|
|
1156
|
+
await checkSpecsIndexes(checks, reportRuntime);
|
|
1143
1157
|
await checkArchiveNaming(checks);
|
|
1144
1158
|
const decisionRecordsConfigValid = await checkConfigSemantics(checks, reportRuntime);
|
|
1145
1159
|
if (decisionRecordsConfigValid) {
|
|
@@ -1167,7 +1181,7 @@ async function runDoctor(options = {}) {
|
|
|
1167
1181
|
if (fixed.length > 0) {
|
|
1168
1182
|
console.log(` ${pc.green("Fixed:")} ${fixed.join(", ")}`);
|
|
1169
1183
|
console.log();
|
|
1170
|
-
} else if (options.fix && !repairIssue &&
|
|
1184
|
+
} else if (options.fix && !repairIssue && existsSync4(RSP_DIR)) {
|
|
1171
1185
|
console.log(` ${pc.dim("No safe fixes needed.")}`);
|
|
1172
1186
|
console.log();
|
|
1173
1187
|
}
|
|
@@ -1185,7 +1199,7 @@ async function runDoctor(options = {}) {
|
|
|
1185
1199
|
}
|
|
1186
1200
|
function reportFallbackProtocol(checks) {
|
|
1187
1201
|
const canonical = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
1188
|
-
const obsoleteExists =
|
|
1202
|
+
const obsoleteExists = existsSync4(OBSOLETE_RSP_RULES_PATH);
|
|
1189
1203
|
if (canonical.issue) {
|
|
1190
1204
|
checks.push({
|
|
1191
1205
|
status: "issue",
|
|
@@ -1199,7 +1213,7 @@ function reportFallbackProtocol(checks) {
|
|
|
1199
1213
|
checks.push({
|
|
1200
1214
|
status: "issue",
|
|
1201
1215
|
label: "rsp-rules.md exists",
|
|
1202
|
-
hint:
|
|
1216
|
+
hint: existsSync4(RSP_DIR) ? "Run: rsp update" : "Run: rsp init"
|
|
1203
1217
|
});
|
|
1204
1218
|
}
|
|
1205
1219
|
if (obsoleteExists) {
|
|
@@ -1290,44 +1304,31 @@ async function checkAgents(checks, reportRuntime) {
|
|
|
1290
1304
|
}
|
|
1291
1305
|
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing managed RSP block", hint: "Run: rsp init" });
|
|
1292
1306
|
}
|
|
1293
|
-
async function
|
|
1294
|
-
const
|
|
1295
|
-
if (
|
|
1296
|
-
checks.push({ status: "
|
|
1297
|
-
return;
|
|
1298
|
-
}
|
|
1299
|
-
if (!indexFile.exists)
|
|
1300
|
-
return;
|
|
1301
|
-
let content;
|
|
1302
|
-
try {
|
|
1303
|
-
content = await readFile7(path, "utf-8");
|
|
1304
|
-
} catch (error) {
|
|
1305
|
-
reportRuntime({
|
|
1306
|
-
code: "generated_index_read_failed",
|
|
1307
|
-
operation: "readFile",
|
|
1308
|
-
path,
|
|
1309
|
-
message: toErrorMessage(error)
|
|
1310
|
-
});
|
|
1311
|
-
checks.push({ status: "issue", label, message: `${label} could not be read`, hint: fixHint });
|
|
1307
|
+
async function checkSpecsIndexes(checks, reportRuntime) {
|
|
1308
|
+
const health = await inspectSpecsIndexHealth();
|
|
1309
|
+
if (health.ok) {
|
|
1310
|
+
checks.push({ status: "ok", label: "hierarchical Specs indexes are current generated files" });
|
|
1312
1311
|
return;
|
|
1313
1312
|
}
|
|
1314
|
-
|
|
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) {
|
|
1313
|
+
for (const message of health.issues) {
|
|
1321
1314
|
reportRuntime({
|
|
1322
|
-
code: "
|
|
1323
|
-
operation: "
|
|
1324
|
-
path,
|
|
1325
|
-
message
|
|
1315
|
+
code: "specs_index_inspection_failed",
|
|
1316
|
+
operation: "inspectSpecsIndexHealth",
|
|
1317
|
+
path: ".rsp/specs",
|
|
1318
|
+
message
|
|
1326
1319
|
});
|
|
1327
|
-
checks.push({ status: "issue", label, message: `${label} could not parse metadata`, hint: fixHint });
|
|
1328
|
-
return;
|
|
1329
1320
|
}
|
|
1330
|
-
|
|
1321
|
+
const details = [
|
|
1322
|
+
...health.issues,
|
|
1323
|
+
health.stalePaths.length > 0 ? `missing or stale: ${health.stalePaths.join(", ")}` : "",
|
|
1324
|
+
health.obsoletePaths.length > 0 ? `recognized obsolete indexes: ${health.obsoletePaths.join(", ")}` : ""
|
|
1325
|
+
].filter(Boolean);
|
|
1326
|
+
checks.push({
|
|
1327
|
+
status: "issue",
|
|
1328
|
+
label: "hierarchical Specs indexes are current generated files",
|
|
1329
|
+
message: details.join("; "),
|
|
1330
|
+
hint: "Run: rsp update"
|
|
1331
|
+
});
|
|
1331
1332
|
}
|
|
1332
1333
|
async function checkArchiveNaming(checks) {
|
|
1333
1334
|
const archivesDir = join6(RSP_DIR, "archives");
|
|
@@ -1342,7 +1343,7 @@ async function checkArchiveNaming(checks) {
|
|
|
1342
1343
|
return;
|
|
1343
1344
|
}
|
|
1344
1345
|
checks.push({ status: "ok", label: "archives use supported managed paths" });
|
|
1345
|
-
const invalidFiles = inspection.files.map((fp) => normalizeLogicalPath(
|
|
1346
|
+
const invalidFiles = inspection.files.map((fp) => normalizeLogicalPath(relative2(archivesDir, fp))).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(basename3(fp)) === false);
|
|
1346
1347
|
if (invalidFiles.length === 0) {
|
|
1347
1348
|
checks.push({ status: "ok", label: "archived change files follow the date_name.md convention" });
|
|
1348
1349
|
return;
|
|
@@ -1430,7 +1431,7 @@ async function checkDecisionRecordsDirectory(checks, reportRuntime) {
|
|
|
1430
1431
|
}
|
|
1431
1432
|
async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
|
|
1432
1433
|
const decisionRecordsPath = resolveDecisionRecordsPath(await loadRspConfig());
|
|
1433
|
-
if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !
|
|
1434
|
+
if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !existsSync4(DEFAULT_DECISION_RECORDS_PATH))
|
|
1434
1435
|
return;
|
|
1435
1436
|
let inspectionFailed = false;
|
|
1436
1437
|
const inactiveRecords = (await walkMarkdownFiles(DEFAULT_DECISION_RECORDS_PATH, {
|
|
@@ -1438,7 +1439,7 @@ async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
|
|
|
1438
1439
|
inspectionFailed = true;
|
|
1439
1440
|
reportRuntime(diagnostic);
|
|
1440
1441
|
}
|
|
1441
|
-
})).map((path) => normalizeLogicalPath(
|
|
1442
|
+
})).map((path) => normalizeLogicalPath(relative2(DEFAULT_DECISION_RECORDS_PATH, path))).sort();
|
|
1442
1443
|
if (inspectionFailed) {
|
|
1443
1444
|
checks.push({
|
|
1444
1445
|
status: "issue",
|
|
@@ -1548,8 +1549,8 @@ async function checkActiveChangeConsistency(checks) {
|
|
|
1548
1549
|
}
|
|
1549
1550
|
|
|
1550
1551
|
// src/commands/focus.ts
|
|
1551
|
-
import { existsSync as
|
|
1552
|
-
import { mkdir as mkdir6, unlink as
|
|
1552
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1553
|
+
import { mkdir as mkdir6, unlink as unlink7 } from "fs/promises";
|
|
1553
1554
|
import { dirname as dirname3 } from "path";
|
|
1554
1555
|
async function focusChange(name) {
|
|
1555
1556
|
if (!name) {
|
|
@@ -1582,9 +1583,9 @@ async function unfocusChange(name) {
|
|
|
1582
1583
|
return await withRspLock("unfocus-change", async () => {
|
|
1583
1584
|
const workRef = resolveWorkRef(name, { executable: false, mustExist: false });
|
|
1584
1585
|
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
1585
|
-
if (!
|
|
1586
|
+
if (!existsSync5(focusEntry))
|
|
1586
1587
|
throw new WorkRefError("focus_marker_not_found", `.rsp/focus.d/${name}`, name);
|
|
1587
|
-
await
|
|
1588
|
+
await unlink7(focusEntry);
|
|
1588
1589
|
await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
|
|
1589
1590
|
console.log(` ${pc.green("Unfocused:")} ${name}`);
|
|
1590
1591
|
console.log(` ${pc.dim("focus.d cleared")} \u2192 ${name}`);
|
|
@@ -1675,11 +1676,6 @@ async function closeChangeGroup(name) {
|
|
|
1675
1676
|
} catch (error) {
|
|
1676
1677
|
warnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
|
|
1677
1678
|
}
|
|
1678
|
-
try {
|
|
1679
|
-
await buildArchiveIndex({ acquireLock: false });
|
|
1680
|
-
} catch (error) {
|
|
1681
|
-
warnings.push(`archive index rebuild failed: ${toErrorMessage(error)}`);
|
|
1682
|
-
}
|
|
1683
1679
|
console.log(` ${pc.green("Closed Change Group:")} ${name}`);
|
|
1684
1680
|
console.log(` ${pc.dim("Archived Group Brief:")} ${join7(archiveDirectory, archiveName)}
|
|
1685
1681
|
`);
|
|
@@ -1773,7 +1769,8 @@ async function showHistory(input = {}, options = {}) {
|
|
|
1773
1769
|
since: query.since ?? null,
|
|
1774
1770
|
until: query.until ?? null,
|
|
1775
1771
|
kind: query.kind ?? null,
|
|
1776
|
-
group: query.group ?? null
|
|
1772
|
+
group: query.group ?? null,
|
|
1773
|
+
search: query.search ?? null
|
|
1777
1774
|
},
|
|
1778
1775
|
records: listed.records,
|
|
1779
1776
|
summary: listed.summary,
|
|
@@ -1793,7 +1790,7 @@ function validateHistoryQuery(input) {
|
|
|
1793
1790
|
if (input.workRef) {
|
|
1794
1791
|
if (!WORK_REF_RE.test(input.workRef) || input.workRef.endsWith("/brief") || input.workRef.endsWith("/00-brief"))
|
|
1795
1792
|
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)
|
|
1793
|
+
if (input.limit !== void 0 || input.since !== void 0 || input.until !== void 0 || input.kind !== void 0 || input.group !== void 0 || input.search !== void 0)
|
|
1797
1794
|
return invalid("history_detail_filters_unsupported", "list filters cannot be combined with an exact history detail lookup");
|
|
1798
1795
|
return { ok: true, query: {} };
|
|
1799
1796
|
}
|
|
@@ -1810,6 +1807,8 @@ function validateHistoryQuery(input) {
|
|
|
1810
1807
|
return invalid("invalid_history_kind", "--kind must be a non-empty exact historical kind");
|
|
1811
1808
|
if (input.group !== void 0 && !GROUP_RE.test(input.group))
|
|
1812
1809
|
return invalid("invalid_history_group", "--group must be one lowercase kebab-case Group name");
|
|
1810
|
+
if (input.search !== void 0 && input.search.trim() === "")
|
|
1811
|
+
return invalid("invalid_history_search", "--search must be non-empty");
|
|
1813
1812
|
return {
|
|
1814
1813
|
ok: true,
|
|
1815
1814
|
query: {
|
|
@@ -1817,7 +1816,8 @@ function validateHistoryQuery(input) {
|
|
|
1817
1816
|
since: input.since,
|
|
1818
1817
|
until: input.until,
|
|
1819
1818
|
kind: input.kind?.trim(),
|
|
1820
|
-
group: input.group
|
|
1819
|
+
group: input.group,
|
|
1820
|
+
search: input.search?.trim()
|
|
1821
1821
|
}
|
|
1822
1822
|
};
|
|
1823
1823
|
}
|
|
@@ -1909,7 +1909,7 @@ function printHistoryDetail(result) {
|
|
|
1909
1909
|
}
|
|
1910
1910
|
|
|
1911
1911
|
// src/commands/init.ts
|
|
1912
|
-
import { existsSync as
|
|
1912
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1913
1913
|
import { mkdir as mkdir8, readFile as readFile8 } from "fs/promises";
|
|
1914
1914
|
import { join as join8 } from "path";
|
|
1915
1915
|
function generateConfigTemplate() {
|
|
@@ -1923,7 +1923,14 @@ function generateConfigTemplate() {
|
|
|
1923
1923
|
# kinds: ${VALID_KINDS.join(", ")}
|
|
1924
1924
|
#
|
|
1925
1925
|
# kinds:
|
|
1926
|
-
${fmtList(VALID_KINDS,
|
|
1926
|
+
${fmtList(VALID_KINDS, 0)}
|
|
1927
|
+
|
|
1928
|
+
# New projects default to automatic Manage routing with lifecycle-only closeout.
|
|
1929
|
+
# Use activation: explicit to require a named managed request.
|
|
1930
|
+
# closeout accepts manual, lifecycle, or local; local routes a qualified clean terminal non-small boundary to one local commit but never push or publication.
|
|
1931
|
+
manage:
|
|
1932
|
+
activation: auto
|
|
1933
|
+
closeout: lifecycle
|
|
1927
1934
|
|
|
1928
1935
|
# Decision Records default to .rsp/specs/decisions.
|
|
1929
1936
|
# Set exactly one project-relative authoritative directory when the Host Project already owns Decision Records elsewhere.
|
|
@@ -1938,7 +1945,7 @@ function toTitle(projectName) {
|
|
|
1938
1945
|
return projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
|
|
1939
1946
|
}
|
|
1940
1947
|
async function initProject(args = {}) {
|
|
1941
|
-
const isNew = !
|
|
1948
|
+
const isNew = !existsSync6(RSP_DIR);
|
|
1942
1949
|
const projectName = await detectProjectName();
|
|
1943
1950
|
const mode = args.agentsMode ?? "managed";
|
|
1944
1951
|
if (mode !== "managed" && mode !== "print") {
|
|
@@ -1953,7 +1960,7 @@ async function initProject(args = {}) {
|
|
|
1953
1960
|
const agentsInspection = inspectManagedFile(agentsPath, "AGENTS.md", { allowMissing: true });
|
|
1954
1961
|
if (agentsInspection.issue)
|
|
1955
1962
|
throw agentsInspection.issue;
|
|
1956
|
-
if (
|
|
1963
|
+
if (existsSync6(CONFIG_PATH)) {
|
|
1957
1964
|
clearConfigCache();
|
|
1958
1965
|
const existingConfig = await inspectRspConfig();
|
|
1959
1966
|
if (existingConfig.issues.length > 0)
|
|
@@ -1983,12 +1990,9 @@ async function initProject(args = {}) {
|
|
|
1983
1990
|
throw new Error(configInspection.issues.join("; "));
|
|
1984
1991
|
const decisionRecordsPath = resolveDecisionRecordsPath(configInspection.config);
|
|
1985
1992
|
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");
|
|
1987
|
-
created = createdSpecsIndex || created;
|
|
1988
1993
|
const createdDesign = await ensureFile(join8(RSP_DIR, "specs", "design.md"), generateDesignContent(projectName));
|
|
1989
1994
|
created = createdDesign || created;
|
|
1990
|
-
|
|
1991
|
-
created = createdArchivesIndex || created;
|
|
1995
|
+
created = await removeLegacyArchiveIndex() || created;
|
|
1992
1996
|
created = await ensureFile(join8(CHANGES_DIR, ".gitkeep"), "") || created;
|
|
1993
1997
|
created = await ensureFile(join8(FOCUS_DIR, ".gitkeep"), "") || created;
|
|
1994
1998
|
created = await ensureFile(join8(RSP_DIR, ".gitignore"), "# Transient files that should not be committed\n.lock\n") || created;
|
|
@@ -1999,10 +2003,7 @@ async function initProject(args = {}) {
|
|
|
1999
2003
|
created = await ensureFile(changePath, generateChangeContent("project-setup")) || created;
|
|
2000
2004
|
created = await ensureFile(focusMarker, "") || created;
|
|
2001
2005
|
}
|
|
2002
|
-
|
|
2003
|
-
await buildSpecsIndex({ acquireLock: false });
|
|
2004
|
-
if (createdArchivesIndex)
|
|
2005
|
-
await buildArchiveIndex({ acquireLock: false });
|
|
2006
|
+
created = await buildSpecsIndex({ acquireLock: false }) || created;
|
|
2006
2007
|
const title = toTitle(projectName);
|
|
2007
2008
|
let agentsUpdated = false;
|
|
2008
2009
|
if (!agentsInspection.exists) {
|
|
@@ -2178,8 +2179,126 @@ function formatArchiveReady(value) {
|
|
|
2178
2179
|
}
|
|
2179
2180
|
}
|
|
2180
2181
|
|
|
2182
|
+
// src/commands/reopen.ts
|
|
2183
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2184
|
+
import { mkdir as mkdir9, readFile as readFile10, unlink as unlink8, writeFile as writeFile5 } from "fs/promises";
|
|
2185
|
+
import { dirname as dirname4 } from "path";
|
|
2186
|
+
async function reopenChange(name, options) {
|
|
2187
|
+
if (!name) {
|
|
2188
|
+
console.error(` ${pc.red("Usage:")} rsp reopen <name> --reason <text> [--from <archive-path>]`);
|
|
2189
|
+
process.exit(1);
|
|
2190
|
+
}
|
|
2191
|
+
const reason = options.reason?.trim();
|
|
2192
|
+
if (!reason || /[\r\n]/.test(reason)) {
|
|
2193
|
+
console.error(` ${pc.red("Error:")} --reason must be one non-empty line`);
|
|
2194
|
+
process.exit(1);
|
|
2195
|
+
}
|
|
2196
|
+
guardRspInitialized();
|
|
2197
|
+
try {
|
|
2198
|
+
await withRspLock("reopen-change", async () => {
|
|
2199
|
+
const inspection = await inspectArchiveHistory();
|
|
2200
|
+
if (!historyInspectionComplete(inspection)) {
|
|
2201
|
+
throw new ArchiveHistoryError(
|
|
2202
|
+
"archive_inspection_incomplete",
|
|
2203
|
+
"archive history inspection is incomplete; resolve the reported diagnostics before reopening work"
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
const record = selectArchiveHistoryRecord(
|
|
2207
|
+
inspection.records,
|
|
2208
|
+
options.from ? { path: options.from } : { workRef: name }
|
|
2209
|
+
);
|
|
2210
|
+
if (record.workRef !== name) {
|
|
2211
|
+
throw new ArchiveHistoryError(
|
|
2212
|
+
"archive_identity_mismatch",
|
|
2213
|
+
`selected archive belongs to ${record.workRef}, not ${name}`
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
let workRef;
|
|
2217
|
+
try {
|
|
2218
|
+
workRef = resolveWorkRef(name, { executable: true });
|
|
2219
|
+
} catch (error) {
|
|
2220
|
+
if (error instanceof WorkRefError && error.code === "group_brief_missing") {
|
|
2221
|
+
throw new ArchiveHistoryError(
|
|
2222
|
+
"archived_group_closed",
|
|
2223
|
+
`cannot reopen ${name} because its Change Group is closed; reopen the Group through a separately authorized lifecycle operation first`
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
throw error;
|
|
2227
|
+
}
|
|
2228
|
+
if (existsSync7(workRef.path))
|
|
2229
|
+
throw new ArchiveHistoryError("open_change_exists", `open Change already exists: ${name}`);
|
|
2230
|
+
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
2231
|
+
if (existsSync7(focusEntry))
|
|
2232
|
+
throw new ArchiveHistoryError("focus_marker_exists", `focus marker already exists for non-open Change: ${name}; run rsp doctor`);
|
|
2233
|
+
const archivedContent = await readFile10(record.sourcePath, "utf-8");
|
|
2234
|
+
const reopenedContent = appendChecklistItem(
|
|
2235
|
+
appendChecklistItem(archivedContent, "Tasks", `- [ ] Resolve reopened concern: ${reason}`),
|
|
2236
|
+
"Verify",
|
|
2237
|
+
`- [ ] Verify reopened concern: ${reason}`
|
|
2238
|
+
);
|
|
2239
|
+
await mkdir9(dirname4(workRef.path), { recursive: true });
|
|
2240
|
+
let changeCreated = false;
|
|
2241
|
+
try {
|
|
2242
|
+
await writeFile5(workRef.path, reopenedContent, { flag: "wx" });
|
|
2243
|
+
changeCreated = true;
|
|
2244
|
+
await mkdir9(FOCUS_DIR, { recursive: true });
|
|
2245
|
+
await mkdir9(dirname4(focusEntry), { recursive: true });
|
|
2246
|
+
await writeManagedFile(focusEntry, "", "focus marker");
|
|
2247
|
+
} catch (error) {
|
|
2248
|
+
if (changeCreated) {
|
|
2249
|
+
try {
|
|
2250
|
+
await unlink8(workRef.path);
|
|
2251
|
+
await cleanupEmptyParentDirs(workRef.path, CHANGES_DIR);
|
|
2252
|
+
} catch {
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
throw error;
|
|
2256
|
+
}
|
|
2257
|
+
console.log(` ${pc.green("Reopened:")} ${name}`);
|
|
2258
|
+
console.log(` ${pc.dim("retained archive")} \u2192 ${record.path}`);
|
|
2259
|
+
console.log(` ${pc.dim("focused via focus.d")} \u2192 ${name}`);
|
|
2260
|
+
console.log(` ${pc.cyan("Next:")} refine the reopened Task and Verify evidence, then resolve the concern
|
|
2261
|
+
`);
|
|
2262
|
+
if (existsSync7(".git")) {
|
|
2263
|
+
console.log(` ${pc.cyan("Git delivery:")}`);
|
|
2264
|
+
console.log(` git status --short`);
|
|
2265
|
+
console.log(` Inspect the complete reopen transition; stage and commit only with separate Git authority.`);
|
|
2266
|
+
console.log();
|
|
2267
|
+
}
|
|
2268
|
+
});
|
|
2269
|
+
} catch (error) {
|
|
2270
|
+
if (error instanceof ArchiveHistoryError || error instanceof WorkRefError) {
|
|
2271
|
+
console.error(` ${pc.red("Error:")} ${error.message}`);
|
|
2272
|
+
if (error instanceof ArchiveHistoryError) {
|
|
2273
|
+
for (const candidate of error.candidates)
|
|
2274
|
+
console.error(` ${candidate}`);
|
|
2275
|
+
if (error.candidatesTruncated)
|
|
2276
|
+
console.error(` ... ${error.candidateTotal - error.candidates.length} more`);
|
|
2277
|
+
}
|
|
2278
|
+
process.exit(1);
|
|
2279
|
+
}
|
|
2280
|
+
throw error;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
function appendChecklistItem(content, heading, item) {
|
|
2284
|
+
const headings = [...content.matchAll(new RegExp(`^## ${heading}[ \\t]*\\r?$`, "gm"))];
|
|
2285
|
+
if (headings.length !== 1) {
|
|
2286
|
+
throw new ArchiveHistoryError(
|
|
2287
|
+
"archive_section_invalid",
|
|
2288
|
+
`archived Change must contain exactly one required section: ${heading}`
|
|
2289
|
+
);
|
|
2290
|
+
}
|
|
2291
|
+
const sectionBodyStart = headings[0].index + headings[0][0].length;
|
|
2292
|
+
const nextHeading = content.indexOf("\n## ", sectionBodyStart);
|
|
2293
|
+
const insertAt = nextHeading < 0 ? content.length : nextHeading;
|
|
2294
|
+
const before = content.slice(0, insertAt);
|
|
2295
|
+
const separator = before.endsWith("\n\n") ? "" : before.endsWith("\n") ? "\n" : "\n\n";
|
|
2296
|
+
return `${before}${separator}${item}
|
|
2297
|
+
${content.slice(insertAt)}`;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2181
2300
|
// src/commands/show.ts
|
|
2182
|
-
import { readFile as
|
|
2301
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
2183
2302
|
function exitShowError(error, options) {
|
|
2184
2303
|
const nextActions = error.code === "no_focused_change" ? ["Run: rsp status", "Run: rsp focus <name>", "Or run: rsp create <name>"] : [];
|
|
2185
2304
|
if (options.json) {
|
|
@@ -2237,7 +2356,7 @@ async function showChange(nameOrFocused, options = {}) {
|
|
|
2237
2356
|
const srcPath = workRef.path;
|
|
2238
2357
|
let content;
|
|
2239
2358
|
try {
|
|
2240
|
-
content = await
|
|
2359
|
+
content = await readFile11(srcPath, "utf-8");
|
|
2241
2360
|
} catch {
|
|
2242
2361
|
exitShowError({ code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, options);
|
|
2243
2362
|
}
|
|
@@ -2348,239 +2467,6 @@ function formatArchiveReady2(value) {
|
|
|
2348
2467
|
}
|
|
2349
2468
|
}
|
|
2350
2469
|
|
|
2351
|
-
// src/commands/skills.ts
|
|
2352
|
-
import { randomUUID } from "crypto";
|
|
2353
|
-
import { lstatSync } from "fs";
|
|
2354
|
-
import { chmod, copyFile, mkdir as mkdir9, readdir as readdir2, readFile as readFile11, rename as rename3, rm as rm2 } from "fs/promises";
|
|
2355
|
-
import { dirname as dirname4, join as join9, relative as relative4 } from "path";
|
|
2356
|
-
function unsupportedEntry(path, label) {
|
|
2357
|
-
return new ManagedPathError(path, `unsupported entry in the ${label} tree: ${path}`);
|
|
2358
|
-
}
|
|
2359
|
-
function inspectDirectoryIdentity(path, label) {
|
|
2360
|
-
const inspection = inspectManagedDirectory(path, label);
|
|
2361
|
-
if (inspection.issue)
|
|
2362
|
-
throw inspection.issue;
|
|
2363
|
-
const stats = lstatSync(path);
|
|
2364
|
-
return { path, dev: stats.dev, ino: stats.ino };
|
|
2365
|
-
}
|
|
2366
|
-
function assertDirectoryIdentities(identities, label) {
|
|
2367
|
-
for (const identity of identities) {
|
|
2368
|
-
const current = inspectDirectoryIdentity(identity.path, label);
|
|
2369
|
-
if (current.dev !== identity.dev || current.ino !== identity.ino)
|
|
2370
|
-
throw new ManagedPathError(identity.path, `${label} changed after validation: ${identity.path}`);
|
|
2371
|
-
}
|
|
2372
|
-
}
|
|
2373
|
-
async function prepareTargetRoot(projectRoot, targetRoot) {
|
|
2374
|
-
const projectIdentity = inspectDirectoryIdentity(projectRoot, "project Skills root");
|
|
2375
|
-
const agentsRoot = join9(projectRoot, ".agents");
|
|
2376
|
-
const identities = [projectIdentity];
|
|
2377
|
-
assertDirectoryIdentities(identities, "project Skills root");
|
|
2378
|
-
await mkdir9(agentsRoot).catch((error) => {
|
|
2379
|
-
if (error.code !== "EEXIST")
|
|
2380
|
-
throw error;
|
|
2381
|
-
});
|
|
2382
|
-
assertDirectoryIdentities(identities, "project Skills root");
|
|
2383
|
-
identities.push(inspectDirectoryIdentity(agentsRoot, "project Skills root"));
|
|
2384
|
-
assertDirectoryIdentities(identities, "project Skills root");
|
|
2385
|
-
await mkdir9(targetRoot).catch((error) => {
|
|
2386
|
-
if (error.code !== "EEXIST")
|
|
2387
|
-
throw error;
|
|
2388
|
-
});
|
|
2389
|
-
assertDirectoryIdentities(identities, "project Skills root");
|
|
2390
|
-
identities.push(inspectDirectoryIdentity(targetRoot, "project Skills root"));
|
|
2391
|
-
return identities;
|
|
2392
|
-
}
|
|
2393
|
-
async function inspectPackagedSkills(packageRoot) {
|
|
2394
|
-
const skillsRoot = join9(packageRoot, "skills");
|
|
2395
|
-
const rootInspection = inspectManagedDirectory(skillsRoot, "packaged Skills root");
|
|
2396
|
-
if (rootInspection.issue)
|
|
2397
|
-
throw rootInspection.issue;
|
|
2398
|
-
const entries = await readdir2(skillsRoot, { withFileTypes: true });
|
|
2399
|
-
const skills = [];
|
|
2400
|
-
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2401
|
-
if (entry.name.startsWith("."))
|
|
2402
|
-
continue;
|
|
2403
|
-
const root = join9(skillsRoot, entry.name);
|
|
2404
|
-
if (!entry.isDirectory())
|
|
2405
|
-
throw unsupportedEntry(root, "packaged Skills");
|
|
2406
|
-
const inspection = await inspectManagedFileTree(root, `packaged Skill ${entry.name}`);
|
|
2407
|
-
if (inspection.issues.length > 0)
|
|
2408
|
-
throw inspection.issues[0];
|
|
2409
|
-
skills.push({
|
|
2410
|
-
name: entry.name,
|
|
2411
|
-
root,
|
|
2412
|
-
files: inspection.files.map((path) => relative4(root, path))
|
|
2413
|
-
});
|
|
2414
|
-
}
|
|
2415
|
-
return skills;
|
|
2416
|
-
}
|
|
2417
|
-
function inspectTarget(path, name) {
|
|
2418
|
-
try {
|
|
2419
|
-
const stats = lstatSync(path);
|
|
2420
|
-
if (stats.isDirectory())
|
|
2421
|
-
return "directory";
|
|
2422
|
-
throw unsupportedEntry(path, `installed Skill ${name}`);
|
|
2423
|
-
} catch (error) {
|
|
2424
|
-
if (error.code === "ENOENT")
|
|
2425
|
-
return "missing";
|
|
2426
|
-
throw error;
|
|
2427
|
-
}
|
|
2428
|
-
}
|
|
2429
|
-
async function treesEqual(source, targetRoot) {
|
|
2430
|
-
const target = await inspectManagedFileTree(targetRoot, `installed Skill ${source.name}`);
|
|
2431
|
-
if (target.issues.length > 0)
|
|
2432
|
-
throw target.issues[0];
|
|
2433
|
-
const targetFiles = target.files.map((path) => relative4(targetRoot, path));
|
|
2434
|
-
if (source.files.length !== targetFiles.length || source.files.some((path, index) => path !== targetFiles[index]))
|
|
2435
|
-
return false;
|
|
2436
|
-
for (const path of source.files) {
|
|
2437
|
-
const sourcePath = join9(source.root, path);
|
|
2438
|
-
const targetPath = join9(targetRoot, path);
|
|
2439
|
-
const [sourceStats, targetStats] = [lstatSync(sourcePath), lstatSync(targetPath)];
|
|
2440
|
-
if (!sourceStats.isFile() || !targetStats.isFile())
|
|
2441
|
-
throw unsupportedEntry(!sourceStats.isFile() ? sourcePath : targetPath, `Skill ${source.name}`);
|
|
2442
|
-
if ((sourceStats.mode & 511) !== (targetStats.mode & 511))
|
|
2443
|
-
return false;
|
|
2444
|
-
const [sourceContent, targetContent] = await Promise.all([readFile11(sourcePath), readFile11(targetPath)]);
|
|
2445
|
-
if (!sourceContent.equals(targetContent))
|
|
2446
|
-
return false;
|
|
2447
|
-
}
|
|
2448
|
-
return true;
|
|
2449
|
-
}
|
|
2450
|
-
async function copySkillTree(source, targetRoot) {
|
|
2451
|
-
await mkdir9(targetRoot, { recursive: true });
|
|
2452
|
-
for (const path of source.files) {
|
|
2453
|
-
const sourcePath = join9(source.root, path);
|
|
2454
|
-
const targetPath = join9(targetRoot, path);
|
|
2455
|
-
const stats = lstatSync(sourcePath);
|
|
2456
|
-
if (!stats.isFile())
|
|
2457
|
-
throw unsupportedEntry(sourcePath, `packaged Skill ${source.name}`);
|
|
2458
|
-
await mkdir9(dirname4(targetPath), { recursive: true });
|
|
2459
|
-
await copyFile(sourcePath, targetPath);
|
|
2460
|
-
await chmod(targetPath, stats.mode & 511);
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
|
-
async function installPackagedSkills(args = {}, options = {}) {
|
|
2464
|
-
const packageRoot = options.packageRoot ?? PKG_ROOT;
|
|
2465
|
-
const projectRoot = options.projectRoot ?? process.cwd();
|
|
2466
|
-
const renamePath = options.renamePath ?? rename3;
|
|
2467
|
-
const skills = await inspectPackagedSkills(packageRoot);
|
|
2468
|
-
const targetRoot = resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
|
|
2469
|
-
const result = { installed: [], unchanged: [], replaced: [] };
|
|
2470
|
-
const conflicts = [];
|
|
2471
|
-
const targetTreeIdentities = /* @__PURE__ */ new Map();
|
|
2472
|
-
for (const skill of skills) {
|
|
2473
|
-
const target = join9(targetRoot, skill.name);
|
|
2474
|
-
if (inspectTarget(target, skill.name) === "missing") {
|
|
2475
|
-
result.installed.push(skill.name);
|
|
2476
|
-
continue;
|
|
2477
|
-
}
|
|
2478
|
-
const targetIdentity = inspectDirectoryIdentity(target, `installed Skill ${skill.name}`);
|
|
2479
|
-
if (await treesEqual(skill, target)) {
|
|
2480
|
-
result.unchanged.push(skill.name);
|
|
2481
|
-
continue;
|
|
2482
|
-
}
|
|
2483
|
-
assertDirectoryIdentities([targetIdentity], `installed Skill ${skill.name}`);
|
|
2484
|
-
if (args.force) {
|
|
2485
|
-
result.replaced.push(skill.name);
|
|
2486
|
-
targetTreeIdentities.set(skill.name, targetIdentity);
|
|
2487
|
-
} else {
|
|
2488
|
-
conflicts.push(skill.name);
|
|
2489
|
-
}
|
|
2490
|
-
}
|
|
2491
|
-
if (conflicts.length > 0)
|
|
2492
|
-
throw new Error(`conflicting packaged Skills: ${conflicts.join(", ")}; rerun with --force to replace only these package-owned directories`);
|
|
2493
|
-
if (args.dryRun)
|
|
2494
|
-
return result;
|
|
2495
|
-
await options.onMutationStep?.({ phase: "before-target-root-mutation", targetRoot });
|
|
2496
|
-
const targetIdentities = await prepareTargetRoot(projectRoot, targetRoot);
|
|
2497
|
-
const stagingRoot = join9(targetRoot, `.rsp-skills-install-${randomUUID()}`);
|
|
2498
|
-
const nextRoot = join9(stagingRoot, "next");
|
|
2499
|
-
const previousRoot = join9(stagingRoot, "previous");
|
|
2500
|
-
const failedRoot = join9(stagingRoot, "failed");
|
|
2501
|
-
const movedPrevious = [];
|
|
2502
|
-
const activated = [];
|
|
2503
|
-
let removeStaging = true;
|
|
2504
|
-
try {
|
|
2505
|
-
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
2506
|
-
await mkdir9(nextRoot, { recursive: true });
|
|
2507
|
-
await mkdir9(previousRoot);
|
|
2508
|
-
await mkdir9(failedRoot);
|
|
2509
|
-
for (const skill of skills) {
|
|
2510
|
-
if (result.unchanged.includes(skill.name))
|
|
2511
|
-
continue;
|
|
2512
|
-
await copySkillTree(skill, join9(nextRoot, skill.name));
|
|
2513
|
-
}
|
|
2514
|
-
for (const name of result.installed) {
|
|
2515
|
-
const target = join9(targetRoot, name);
|
|
2516
|
-
if (inspectTarget(target, name) !== "missing")
|
|
2517
|
-
throw new ManagedPathError(target, `installed Skill changed after preflight: ${target}`);
|
|
2518
|
-
}
|
|
2519
|
-
for (const name of result.replaced) {
|
|
2520
|
-
assertDirectoryIdentities([targetTreeIdentities.get(name)], `installed Skill ${name}`);
|
|
2521
|
-
}
|
|
2522
|
-
for (const name of result.replaced) {
|
|
2523
|
-
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
2524
|
-
assertDirectoryIdentities([targetTreeIdentities.get(name)], `installed Skill ${name}`);
|
|
2525
|
-
await renamePath(join9(targetRoot, name), join9(previousRoot, name));
|
|
2526
|
-
movedPrevious.push(name);
|
|
2527
|
-
}
|
|
2528
|
-
for (const name of [...result.installed, ...result.replaced].sort()) {
|
|
2529
|
-
const target = join9(targetRoot, name);
|
|
2530
|
-
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
2531
|
-
await options.onMutationStep?.({ phase: "before-activate", name, target });
|
|
2532
|
-
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
2533
|
-
if (inspectTarget(target, name) !== "missing")
|
|
2534
|
-
throw new ManagedPathError(target, `installed Skill changed after preflight: ${target}`);
|
|
2535
|
-
await renamePath(join9(nextRoot, name), target);
|
|
2536
|
-
activated.push(name);
|
|
2537
|
-
}
|
|
2538
|
-
} catch (error) {
|
|
2539
|
-
const rollbackErrors = [];
|
|
2540
|
-
for (const name of activated.reverse()) {
|
|
2541
|
-
const target = join9(targetRoot, name);
|
|
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
2470
|
// src/status/derive.ts
|
|
2585
2471
|
function deriveStatusView(snapshot, options = {}) {
|
|
2586
2472
|
const query = {
|
|
@@ -2601,6 +2487,7 @@ function deriveStatusView(snapshot, options = {}) {
|
|
|
2601
2487
|
const ok = snapshot.diagnostics.every((diagnostic) => diagnostic.severity !== "error");
|
|
2602
2488
|
const nextActions = ok ? buildStatusNextActions(snapshot.focused.length, snapshot.records.map((record) => record.output), snapshot.groups, filtered) : ["Run: rsp doctor"];
|
|
2603
2489
|
return {
|
|
2490
|
+
manage: snapshot.manage,
|
|
2604
2491
|
query,
|
|
2605
2492
|
focused: snapshot.focused,
|
|
2606
2493
|
records,
|
|
@@ -2696,6 +2583,8 @@ function printStatusPlain(view) {
|
|
|
2696
2583
|
console.log();
|
|
2697
2584
|
console.log(` ${pc.bold("RSP status")}`);
|
|
2698
2585
|
console.log();
|
|
2586
|
+
console.log(` ${pc.bold("Manage:")} activation ${view.manage.activation} \xB7 closeout ${view.manage.closeout}`);
|
|
2587
|
+
console.log();
|
|
2699
2588
|
for (const diagnostic of view.diagnostics) {
|
|
2700
2589
|
const label = diagnostic.change ?? diagnostic.path;
|
|
2701
2590
|
console.log(` ${pc.red("\u2717")} ${label ? `${label} \u2014 ` : ""}${diagnostic.message}`);
|
|
@@ -2813,6 +2702,7 @@ function toStatusJson(view) {
|
|
|
2813
2702
|
return {
|
|
2814
2703
|
command: "status",
|
|
2815
2704
|
ok: view.ok,
|
|
2705
|
+
manage: view.manage,
|
|
2816
2706
|
filters: view.query,
|
|
2817
2707
|
focused: view.focused,
|
|
2818
2708
|
records: view.records.map((record) => record.output),
|
|
@@ -2829,6 +2719,7 @@ function toStatusJsonError(error, options) {
|
|
|
2829
2719
|
return {
|
|
2830
2720
|
command: "status",
|
|
2831
2721
|
ok: false,
|
|
2722
|
+
manage: { activation: "explicit", closeout: "manual" },
|
|
2832
2723
|
filters: {
|
|
2833
2724
|
focused: options.focused,
|
|
2834
2725
|
blocked: options.blocked,
|
|
@@ -2878,6 +2769,9 @@ function isInteractiveTerminal(environment) {
|
|
|
2878
2769
|
function shouldAutoLaunchUi(args, environment) {
|
|
2879
2770
|
return args.length === 0 && isInteractiveTerminal(environment) && (environment.ci === void 0 || environment.ci === "false");
|
|
2880
2771
|
}
|
|
2772
|
+
function shouldLaunchSkillsUi(args, environment) {
|
|
2773
|
+
return args.length === 1 && args[0] === "skills" && isInteractiveTerminal(environment) && (environment.ci === void 0 || environment.ci === "false");
|
|
2774
|
+
}
|
|
2881
2775
|
function validateUiArgs(args) {
|
|
2882
2776
|
let lang = "auto";
|
|
2883
2777
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -2999,7 +2893,7 @@ var groupCommand = defineCommand({
|
|
|
2999
2893
|
var addSpecCommand = defineCommand({
|
|
3000
2894
|
meta: {
|
|
3001
2895
|
name: "spec",
|
|
3002
|
-
description: "Create .rsp/specs/<name>.md and
|
|
2896
|
+
description: "Create .rsp/specs/<name>.md and refresh local Specs indexes"
|
|
3003
2897
|
},
|
|
3004
2898
|
args: {
|
|
3005
2899
|
name: {
|
|
@@ -3034,12 +2928,42 @@ var archiveCommand = defineCommand({
|
|
|
3034
2928
|
},
|
|
3035
2929
|
"dry-run": {
|
|
3036
2930
|
type: "boolean",
|
|
3037
|
-
description: "
|
|
2931
|
+
description: "Deprecated compatibility route to rsp ready; never moves the Change",
|
|
3038
2932
|
default: false
|
|
3039
2933
|
}
|
|
3040
2934
|
},
|
|
3041
2935
|
async run({ args }) {
|
|
3042
|
-
|
|
2936
|
+
if (args["dry-run"]) {
|
|
2937
|
+
console.error(" Deprecated: use `rsp ready <name>` for read-only archive readiness.");
|
|
2938
|
+
await showReady(args.name);
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
await archiveChange(args.name);
|
|
2942
|
+
}
|
|
2943
|
+
});
|
|
2944
|
+
var reopenCommand = defineCommand({
|
|
2945
|
+
meta: {
|
|
2946
|
+
name: "reopen",
|
|
2947
|
+
description: "Restore one archived Change as focused open work while retaining history"
|
|
2948
|
+
},
|
|
2949
|
+
args: {
|
|
2950
|
+
name: {
|
|
2951
|
+
type: "positional",
|
|
2952
|
+
description: "Archived Change WorkRef",
|
|
2953
|
+
required: true
|
|
2954
|
+
},
|
|
2955
|
+
from: {
|
|
2956
|
+
type: "string",
|
|
2957
|
+
description: "Exact .rsp/archives/... path when the WorkRef has multiple archives"
|
|
2958
|
+
},
|
|
2959
|
+
reason: {
|
|
2960
|
+
type: "string",
|
|
2961
|
+
description: "One-line concern that makes the restored Change unfinished",
|
|
2962
|
+
required: true
|
|
2963
|
+
}
|
|
2964
|
+
},
|
|
2965
|
+
async run({ args }) {
|
|
2966
|
+
await reopenChange(args.name, { from: args.from, reason: args.reason });
|
|
3043
2967
|
}
|
|
3044
2968
|
});
|
|
3045
2969
|
var focusCommand = defineCommand({
|
|
@@ -3180,7 +3104,7 @@ var checkCommand = defineCommand({
|
|
|
3180
3104
|
var updateCommand = defineCommand({
|
|
3181
3105
|
meta: {
|
|
3182
3106
|
name: "update",
|
|
3183
|
-
description: "Refresh RSP project
|
|
3107
|
+
description: "Refresh RSP-managed project files after upgrade; does not update packaged Skills"
|
|
3184
3108
|
},
|
|
3185
3109
|
async run() {
|
|
3186
3110
|
const result = await updateProject();
|
|
@@ -3194,9 +3118,14 @@ var skillsInstallCommand = defineCommand({
|
|
|
3194
3118
|
description: "Install this package's bundled Skills into .agents/skills"
|
|
3195
3119
|
},
|
|
3196
3120
|
args: {
|
|
3121
|
+
"name": {
|
|
3122
|
+
type: "positional",
|
|
3123
|
+
description: "Exact packaged Skill name (default: lifecycle suite)",
|
|
3124
|
+
required: false
|
|
3125
|
+
},
|
|
3197
3126
|
"dry-run": {
|
|
3198
3127
|
type: "boolean",
|
|
3199
|
-
description: "Preflight
|
|
3128
|
+
description: "Preflight selected packaged Skills without changing files",
|
|
3200
3129
|
default: false
|
|
3201
3130
|
},
|
|
3202
3131
|
"force": {
|
|
@@ -3206,20 +3135,44 @@ var skillsInstallCommand = defineCommand({
|
|
|
3206
3135
|
}
|
|
3207
3136
|
},
|
|
3208
3137
|
async run({ args }) {
|
|
3138
|
+
if (args._.length > 1)
|
|
3139
|
+
throw new Error("rsp skills install accepts at most one Skill name");
|
|
3209
3140
|
const result = await installPackagedSkills({
|
|
3210
3141
|
dryRun: Boolean(args["dry-run"]),
|
|
3211
|
-
force: Boolean(args.force)
|
|
3142
|
+
force: Boolean(args.force),
|
|
3143
|
+
names: args.name ? [args.name] : void 0
|
|
3212
3144
|
});
|
|
3213
3145
|
printSkillInstallResult(result, Boolean(args["dry-run"]));
|
|
3214
3146
|
}
|
|
3215
3147
|
});
|
|
3148
|
+
var skillsListCommand = defineCommand({
|
|
3149
|
+
meta: {
|
|
3150
|
+
name: "list",
|
|
3151
|
+
description: "List package-bundled Skills and their project status"
|
|
3152
|
+
},
|
|
3153
|
+
args: {
|
|
3154
|
+
json: {
|
|
3155
|
+
type: "boolean",
|
|
3156
|
+
description: "Print machine-readable JSON output",
|
|
3157
|
+
default: false
|
|
3158
|
+
}
|
|
3159
|
+
},
|
|
3160
|
+
async run({ args }) {
|
|
3161
|
+
const inventory = await inspectPackagedSkillInventory();
|
|
3162
|
+
if (args.json)
|
|
3163
|
+
emitJson(inventory);
|
|
3164
|
+
else
|
|
3165
|
+
printPackagedSkillInventory(inventory);
|
|
3166
|
+
}
|
|
3167
|
+
});
|
|
3216
3168
|
var skillsCommand = defineCommand({
|
|
3217
3169
|
meta: {
|
|
3218
3170
|
name: "skills",
|
|
3219
3171
|
description: "Manage package-bundled project Skills"
|
|
3220
3172
|
},
|
|
3221
3173
|
subCommands: {
|
|
3222
|
-
install: skillsInstallCommand
|
|
3174
|
+
install: skillsInstallCommand,
|
|
3175
|
+
list: skillsListCommand
|
|
3223
3176
|
}
|
|
3224
3177
|
});
|
|
3225
3178
|
var doctorCommand = defineCommand({
|
|
@@ -3358,6 +3311,10 @@ var historyCommand = defineCommand({
|
|
|
3358
3311
|
type: "string",
|
|
3359
3312
|
description: "Match one exact Change Group"
|
|
3360
3313
|
},
|
|
3314
|
+
search: {
|
|
3315
|
+
type: "string",
|
|
3316
|
+
description: "Case-insensitive literal match over WorkRef and summary"
|
|
3317
|
+
},
|
|
3361
3318
|
json: {
|
|
3362
3319
|
type: "boolean",
|
|
3363
3320
|
description: "Print machine-readable JSON output",
|
|
@@ -3377,6 +3334,7 @@ var historyCommand = defineCommand({
|
|
|
3377
3334
|
until: args.until,
|
|
3378
3335
|
kind: args.kind,
|
|
3379
3336
|
group: args.group,
|
|
3337
|
+
search: args.search,
|
|
3380
3338
|
positionalCount: args._?.length ?? (args.name ? 1 : 0)
|
|
3381
3339
|
}, { json: Boolean(args.json), compact: Boolean(args.compact) });
|
|
3382
3340
|
if (!result.ok)
|
|
@@ -3391,6 +3349,19 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
3391
3349
|
ci: process.env.CI
|
|
3392
3350
|
};
|
|
3393
3351
|
const explicitUi = rawArgs[0] === "ui";
|
|
3352
|
+
if (shouldLaunchSkillsUi(rawArgs, terminalEnvironment)) {
|
|
3353
|
+
const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
|
|
3354
|
+
const locale = resolveUiLocale("auto", process.env.RSP_UI_LANG, process.env.LC_ALL ?? process.env.LC_MESSAGES ?? process.env.LANG ?? new Intl.DateTimeFormat().resolvedOptions().locale);
|
|
3355
|
+
const { runSkillsTui } = await import("./entry-WZDVKDUM.mjs");
|
|
3356
|
+
const selection = await runSkillsTui(locale);
|
|
3357
|
+
if (selection.kind === "confirmed") {
|
|
3358
|
+
const result = await installPackagedSkills({ names: selection.names, force: selection.force });
|
|
3359
|
+
printSkillInstallResult(result);
|
|
3360
|
+
} else if (selection.kind === "error") {
|
|
3361
|
+
process.exitCode = 1;
|
|
3362
|
+
}
|
|
3363
|
+
return;
|
|
3364
|
+
}
|
|
3394
3365
|
if (explicitUi || shouldAutoLaunchUi(rawArgs, terminalEnvironment)) {
|
|
3395
3366
|
const { lang } = validateUiArgs(explicitUi ? rawArgs.slice(1) : []);
|
|
3396
3367
|
if (!isInteractiveTerminal(terminalEnvironment)) {
|
|
@@ -3398,8 +3369,8 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
3398
3369
|
throw new Error("rsp ui requires an interactive terminal; use rsp status or rsp status --json instead");
|
|
3399
3370
|
} else {
|
|
3400
3371
|
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-
|
|
3372
|
+
const locale = resolveUiLocale(lang, process.env.RSP_UI_LANG, process.env.LC_ALL ?? process.env.LC_MESSAGES ?? process.env.LANG ?? new Intl.DateTimeFormat().resolvedOptions().locale);
|
|
3373
|
+
const { runTui } = await import("./entry-X23GPCVS.mjs");
|
|
3403
3374
|
const exitCode = await runTui(locale);
|
|
3404
3375
|
if (exitCode !== 0)
|
|
3405
3376
|
process.exitCode = exitCode;
|
|
@@ -3422,6 +3393,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
3422
3393
|
focus: focusCommand,
|
|
3423
3394
|
unfocus: unfocusCommand,
|
|
3424
3395
|
archive: archiveCommand,
|
|
3396
|
+
reopen: reopenCommand,
|
|
3425
3397
|
ready: readyCommand,
|
|
3426
3398
|
show: showCommand,
|
|
3427
3399
|
history: historyCommand,
|