@oevery/rsp 3.1.0-beta.2 → 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 +44 -33
- package/README.zh-CN.md +37 -31
- package/dist/{chunk-EALHA3ZS.mjs → chunk-7KKAUBKN.mjs} +246 -253
- package/dist/{chunk-4K53353L.mjs → chunk-NX55NIC3.mjs} +40 -4
- package/dist/cli.mjs +443 -302
- package/dist/{entry-UWUX3HM7.mjs → entry-WZDVKDUM.mjs} +1 -1
- package/dist/{entry-N7WLWGTI.mjs → entry-X23GPCVS.mjs} +1 -1
- package/package.json +1 -1
- package/rules/rsp-rules.md +9 -5
- package/skills/rsp/SKILL.md +10 -4
- package/skills/rsp/references/durable-review.md +1 -1
- package/skills/rsp/references/managed-routing.md +11 -5
- package/skills/rsp/references/setup-repair.md +2 -2
- package/skills/rsp-design/SKILL.md +2 -2
- package/skills/rsp-manage/SKILL.md +27 -12
- package/skills/{rsp-address-review → rsp-resolve-findings}/SKILL.md +4 -4
- package/skills/rsp-shape/SKILL.md +2 -2
- package/skills/{rsp-codebase-audit → rsp-structural-audit}/SKILL.md +5 -3
- package/skills/{rsp-codebase-audit → rsp-structural-audit}/references/structural-lenses.md +6 -0
|
@@ -22,9 +22,7 @@ import {
|
|
|
22
22
|
} from "./chunk-OTJFL6XH.mjs";
|
|
23
23
|
|
|
24
24
|
// src/status/inspect.ts
|
|
25
|
-
import {
|
|
26
|
-
import { readFile as readFile3, stat } from "fs/promises";
|
|
27
|
-
import { join as join3 } from "path";
|
|
25
|
+
import { readFile as readFile5, stat } from "fs/promises";
|
|
28
26
|
|
|
29
27
|
// src/core/change-group.ts
|
|
30
28
|
import { readFile } from "fs/promises";
|
|
@@ -967,7 +965,7 @@ async function inspectChangeDependencies(options = {}) {
|
|
|
967
965
|
const executable = new Set(graphValid ? waves.flat() : []);
|
|
968
966
|
const nodes = [.../* @__PURE__ */ new Set([...openNames, ...edges.map((edge) => edge.requires)])].sort().map((name) => {
|
|
969
967
|
let state;
|
|
970
|
-
if (archivedNames.has(name))
|
|
968
|
+
if (!openNames.has(name) && archivedNames.has(name))
|
|
971
969
|
state = "archived";
|
|
972
970
|
else if (!openNames.has(name))
|
|
973
971
|
state = "missing";
|
|
@@ -1104,246 +1102,12 @@ function findCycleDiagnostics(edges) {
|
|
|
1104
1102
|
return diagnostics;
|
|
1105
1103
|
}
|
|
1106
1104
|
|
|
1107
|
-
// src/
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
const configInspection = await inspectRspConfig();
|
|
1114
|
-
const configValid = configInspection.issues.length === 0;
|
|
1115
|
-
manage = resolveManagePolicy(configInspection.config, { configValid });
|
|
1116
|
-
if (!configValid) {
|
|
1117
|
-
diagnostics.push({
|
|
1118
|
-
severity: "error",
|
|
1119
|
-
code: "invalid_config",
|
|
1120
|
-
path: CONFIG_PATH,
|
|
1121
|
-
message: configInspection.issues.join("; ")
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
|
-
} catch (error) {
|
|
1125
|
-
manage = resolveManagePolicy({}, { configValid: false });
|
|
1126
|
-
diagnostics.push({
|
|
1127
|
-
severity: "error",
|
|
1128
|
-
code: "invalid_config",
|
|
1129
|
-
path: CONFIG_PATH,
|
|
1130
|
-
message: toErrorMessage(error)
|
|
1131
|
-
});
|
|
1132
|
-
}
|
|
1133
|
-
const focusTree = await inspectFocusTree();
|
|
1134
|
-
for (const diagnostic of focusTree.diagnostics) {
|
|
1135
|
-
diagnostics.push({
|
|
1136
|
-
severity: "error",
|
|
1137
|
-
code: diagnostic.code,
|
|
1138
|
-
change: diagnostic.input,
|
|
1139
|
-
path: diagnostic.path,
|
|
1140
|
-
message: diagnostic.message
|
|
1141
|
-
});
|
|
1142
|
-
}
|
|
1143
|
-
const rawFocusedSet = new Set(focusTree.markers.map((marker) => marker.name));
|
|
1144
|
-
const focusedSet = /* @__PURE__ */ new Set();
|
|
1145
|
-
const focusedPaths = /* @__PURE__ */ new Map();
|
|
1146
|
-
for (const name of rawFocusedSet) {
|
|
1147
|
-
try {
|
|
1148
|
-
const ref = resolveWorkRef(name, { executable: true });
|
|
1149
|
-
focusedSet.add(ref.name);
|
|
1150
|
-
focusedPaths.set(ref.name, ref.path);
|
|
1151
|
-
} catch (error) {
|
|
1152
|
-
if (!(error instanceof WorkRefError))
|
|
1153
|
-
throw error;
|
|
1154
|
-
diagnostics.push({
|
|
1155
|
-
severity: "error",
|
|
1156
|
-
code: error.code,
|
|
1157
|
-
path: `.rsp/focus.d/${name}`,
|
|
1158
|
-
change: name,
|
|
1159
|
-
message: error.message
|
|
1160
|
-
});
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
const workTree = await inspectWorkTree();
|
|
1164
|
-
diagnostics.push(...workTree.diagnostics.map((diagnostic) => ({
|
|
1165
|
-
severity: "error",
|
|
1166
|
-
code: diagnostic.code,
|
|
1167
|
-
change: diagnostic.input,
|
|
1168
|
-
path: diagnostic.path,
|
|
1169
|
-
message: diagnostic.message
|
|
1170
|
-
})));
|
|
1171
|
-
const groupInspection = await inspectChangeGroups({ workTree, focusTree });
|
|
1172
|
-
diagnostics.push(...groupInspection.diagnostics);
|
|
1173
|
-
const dependencyInspection = await inspectChangeDependencies({
|
|
1174
|
-
workTree,
|
|
1175
|
-
preferredOrder: groupInspection.groups.flatMap((group) => group.slices.map((slice) => slice.name))
|
|
1176
|
-
});
|
|
1177
|
-
diagnostics.push(...dependencyInspection.diagnostics);
|
|
1178
|
-
const changeMap = new Map(workTree.changes.map((ref) => [ref.name, ref.path]));
|
|
1179
|
-
const allNames = [.../* @__PURE__ */ new Set([...changeMap.keys(), ...focusedSet])].sort();
|
|
1180
|
-
const records = [];
|
|
1181
|
-
const nowMs = options.nowMs ?? Date.now();
|
|
1182
|
-
for (const name of allNames) {
|
|
1183
|
-
const path = changeMap.get(name);
|
|
1184
|
-
let kind = "\u2014";
|
|
1185
|
-
let title = name;
|
|
1186
|
-
let blockerEntries = [];
|
|
1187
|
-
let readiness = emptyReadiness(false);
|
|
1188
|
-
let ageDays = null;
|
|
1189
|
-
const isFocused = focusedSet.has(name);
|
|
1190
|
-
let isBlocked = false;
|
|
1191
|
-
let done = 0;
|
|
1192
|
-
let total = 0;
|
|
1193
|
-
let progressKnown = false;
|
|
1194
|
-
if (path) {
|
|
1195
|
-
try {
|
|
1196
|
-
const content = await readFile3(path, "utf-8");
|
|
1197
|
-
title = extractChangeTitle(content) ?? name;
|
|
1198
|
-
blockerEntries = extractBlockerEntries(content);
|
|
1199
|
-
try {
|
|
1200
|
-
const frontmatter = parseFrontmatter(content);
|
|
1201
|
-
kind = frontmatter?.kind ? String(frontmatter.kind) : "\u2014";
|
|
1202
|
-
} catch (error) {
|
|
1203
|
-
kind = "(invalid)";
|
|
1204
|
-
runtime.push({
|
|
1205
|
-
code: "frontmatter_parse_failed",
|
|
1206
|
-
operation: "parseFrontmatter",
|
|
1207
|
-
path,
|
|
1208
|
-
message: toErrorMessage(error)
|
|
1209
|
-
});
|
|
1210
|
-
}
|
|
1211
|
-
isBlocked = dependencyInspection.activeBlockers.get(name) ?? hasMeaningfulBlockers(content);
|
|
1212
|
-
const checkboxes = countCheckboxes(content);
|
|
1213
|
-
done = checkboxes.done;
|
|
1214
|
-
total = checkboxes.total;
|
|
1215
|
-
progressKnown = true;
|
|
1216
|
-
const details = collectArchiveReadiness(content, { activeBlockers: isBlocked });
|
|
1217
|
-
readiness = {
|
|
1218
|
-
incompleteTasks: details.taskTodos.length,
|
|
1219
|
-
incompleteVerify: details.verifyTodos.length,
|
|
1220
|
-
activeBlockers: details.activeBlockers,
|
|
1221
|
-
missingScenarios: details.missingScenarios,
|
|
1222
|
-
deterministic: details.deterministic,
|
|
1223
|
-
semantic: details.semantic,
|
|
1224
|
-
archiveReady: details.archiveReady
|
|
1225
|
-
};
|
|
1226
|
-
} catch (error) {
|
|
1227
|
-
diagnostics.push({
|
|
1228
|
-
severity: "error",
|
|
1229
|
-
code: "change_read_failed",
|
|
1230
|
-
change: name,
|
|
1231
|
-
path,
|
|
1232
|
-
message: "unable to read open Change",
|
|
1233
|
-
hint: toErrorMessage(error)
|
|
1234
|
-
});
|
|
1235
|
-
runtime.push({
|
|
1236
|
-
code: "change_read_failed",
|
|
1237
|
-
operation: "readFile",
|
|
1238
|
-
path,
|
|
1239
|
-
message: toErrorMessage(error)
|
|
1240
|
-
});
|
|
1241
|
-
}
|
|
1242
|
-
try {
|
|
1243
|
-
const metadata = await stat(path);
|
|
1244
|
-
ageDays = Math.floor((nowMs - metadata.mtime.getTime()) / (1e3 * 60 * 60 * 24));
|
|
1245
|
-
} catch (error) {
|
|
1246
|
-
diagnostics.push({
|
|
1247
|
-
severity: "error",
|
|
1248
|
-
code: "change_stat_failed",
|
|
1249
|
-
change: name,
|
|
1250
|
-
path,
|
|
1251
|
-
message: "unable to inspect open Change metadata",
|
|
1252
|
-
hint: toErrorMessage(error)
|
|
1253
|
-
});
|
|
1254
|
-
runtime.push({
|
|
1255
|
-
code: "change_stat_failed",
|
|
1256
|
-
operation: "stat",
|
|
1257
|
-
path,
|
|
1258
|
-
message: toErrorMessage(error)
|
|
1259
|
-
});
|
|
1260
|
-
}
|
|
1261
|
-
} else {
|
|
1262
|
-
kind = "(missing)";
|
|
1263
|
-
const path2 = focusedPaths.get(name);
|
|
1264
|
-
diagnostics.push({
|
|
1265
|
-
severity: "error",
|
|
1266
|
-
code: "focused_change_missing",
|
|
1267
|
-
change: name,
|
|
1268
|
-
path: path2,
|
|
1269
|
-
message: "focus marker points to a missing Change file"
|
|
1270
|
-
});
|
|
1271
|
-
runtime.push({
|
|
1272
|
-
code: "focused_change_missing",
|
|
1273
|
-
operation: "resolveChange",
|
|
1274
|
-
path: path2,
|
|
1275
|
-
message: "focus marker points to a missing change file"
|
|
1276
|
-
});
|
|
1277
|
-
}
|
|
1278
|
-
records.push({
|
|
1279
|
-
output: {
|
|
1280
|
-
name,
|
|
1281
|
-
kind,
|
|
1282
|
-
progress: { done, total },
|
|
1283
|
-
ageDays,
|
|
1284
|
-
isFocused,
|
|
1285
|
-
isBlocked,
|
|
1286
|
-
path: path ? normalizeLogicalPath(path) : null
|
|
1287
|
-
},
|
|
1288
|
-
progressKnown,
|
|
1289
|
-
title,
|
|
1290
|
-
blockerEntries,
|
|
1291
|
-
readiness
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1294
|
-
return {
|
|
1295
|
-
manage,
|
|
1296
|
-
focused: [...focusedSet].sort(),
|
|
1297
|
-
records,
|
|
1298
|
-
groups: groupInspection.groups,
|
|
1299
|
-
plan: dependencyInspection.plan,
|
|
1300
|
-
archiveTrend: await readArchiveTrend(runtime),
|
|
1301
|
-
diagnostics,
|
|
1302
|
-
runtime
|
|
1303
|
-
};
|
|
1304
|
-
}
|
|
1305
|
-
function emptyReadiness(activeBlockers) {
|
|
1306
|
-
return {
|
|
1307
|
-
incompleteTasks: 0,
|
|
1308
|
-
incompleteVerify: 0,
|
|
1309
|
-
activeBlockers,
|
|
1310
|
-
missingScenarios: false,
|
|
1311
|
-
deterministic: "pass",
|
|
1312
|
-
semantic: "needs-review",
|
|
1313
|
-
archiveReady: activeBlockers ? "no" : "judgment"
|
|
1314
|
-
};
|
|
1315
|
-
}
|
|
1316
|
-
function extractChangeTitle(content) {
|
|
1317
|
-
const heading = content.split(/\r?\n/).find((line) => line.startsWith("# Change: "));
|
|
1318
|
-
return heading?.slice("# Change: ".length).trim() || null;
|
|
1319
|
-
}
|
|
1320
|
-
function extractBlockerEntries(content) {
|
|
1321
|
-
return extractSection(content, "Blockers").split("\n").map((line) => line.trim()).filter((line) => /^[-*]\s+/.test(line)).map((line) => line.replace(/^[-*]\s+/, "").trim()).filter((line) => line.toLowerCase() !== "none");
|
|
1322
|
-
}
|
|
1323
|
-
async function readArchiveTrend(runtime) {
|
|
1324
|
-
const indexPath = join3(ARCHIVES_DIR, "INDEX.md");
|
|
1325
|
-
if (!existsSync(indexPath))
|
|
1326
|
-
return [];
|
|
1327
|
-
try {
|
|
1328
|
-
const content = await readFile3(indexPath, "utf-8");
|
|
1329
|
-
const dateRe = /^\| (\d{4}-\d{2})-\d{2} \|/gm;
|
|
1330
|
-
const counts = {};
|
|
1331
|
-
let match;
|
|
1332
|
-
while ((match = dateRe.exec(content)) !== null) {
|
|
1333
|
-
const month = match[1];
|
|
1334
|
-
counts[month] = (counts[month] || 0) + 1;
|
|
1335
|
-
}
|
|
1336
|
-
return Object.keys(counts).sort().map((month) => ({ month, count: counts[month] }));
|
|
1337
|
-
} catch (error) {
|
|
1338
|
-
runtime.push({
|
|
1339
|
-
code: "archive_index_read_failed",
|
|
1340
|
-
operation: "readFile",
|
|
1341
|
-
path: indexPath,
|
|
1342
|
-
message: toErrorMessage(error)
|
|
1343
|
-
});
|
|
1344
|
-
return [];
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1105
|
+
// src/history/query.ts
|
|
1106
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1107
|
+
|
|
1108
|
+
// src/history/inspect.ts
|
|
1109
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1110
|
+
import { basename, dirname as dirname2, relative as relative3 } from "path";
|
|
1347
1111
|
|
|
1348
1112
|
// src/history/model.ts
|
|
1349
1113
|
var HISTORY_DEFAULT_LIMIT = 20;
|
|
@@ -1369,14 +1133,12 @@ var ArchiveHistoryError = class extends Error {
|
|
|
1369
1133
|
};
|
|
1370
1134
|
|
|
1371
1135
|
// src/history/inspect.ts
|
|
1372
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
1373
|
-
import { basename, dirname as dirname2, relative as relative3 } from "path";
|
|
1374
1136
|
var EXECUTABLE_WORK_REF_RE2 = /^[a-z0-9-]+(?:\/[a-z0-9-]+)?$/;
|
|
1375
1137
|
var GROUP_NAME_RE = /^[a-z0-9-]+$/;
|
|
1376
1138
|
var ARCHIVE_NAME_RE = /^(\d{4}-\d{2}-\d{2})_(.+)\.md$/;
|
|
1377
1139
|
async function inspectArchiveHistory(options = {}) {
|
|
1378
1140
|
const archivesDir = options.archivesDir ?? ARCHIVES_DIR;
|
|
1379
|
-
const tree = await inspectArchiveTree({ archivesDir });
|
|
1141
|
+
const tree = options.archiveTree ?? await inspectArchiveTree({ archivesDir });
|
|
1380
1142
|
const diagnostics = tree.diagnostics.map((diagnostic) => ({
|
|
1381
1143
|
severity: "error",
|
|
1382
1144
|
code: diagnostic.code,
|
|
@@ -1397,7 +1159,7 @@ async function inspectArchiveHistory(options = {}) {
|
|
|
1397
1159
|
const path = archiveOutputPath(sourcePath, archivesDir);
|
|
1398
1160
|
let content;
|
|
1399
1161
|
try {
|
|
1400
|
-
content = await
|
|
1162
|
+
content = await readFile3(sourcePath, "utf-8");
|
|
1401
1163
|
} catch (error) {
|
|
1402
1164
|
const message = toErrorMessage(error);
|
|
1403
1165
|
diagnostics.push({ severity: "error", code: "archive_read_failed", path, message: `unable to read archived work: ${message}` });
|
|
@@ -1479,7 +1241,8 @@ function parseArchiveEntry(sourcePath, path, archivesDir, content) {
|
|
|
1479
1241
|
summary: summary.value,
|
|
1480
1242
|
summaryTruncated: summary.truncated,
|
|
1481
1243
|
path,
|
|
1482
|
-
sourcePath
|
|
1244
|
+
sourcePath,
|
|
1245
|
+
searchSummary: rawSummary
|
|
1483
1246
|
}
|
|
1484
1247
|
};
|
|
1485
1248
|
}
|
|
@@ -1527,11 +1290,11 @@ function compareDiagnostics(left, right) {
|
|
|
1527
1290
|
}
|
|
1528
1291
|
|
|
1529
1292
|
// src/history/query.ts
|
|
1530
|
-
import { readFile as readFile5 } from "fs/promises";
|
|
1531
1293
|
function queryArchiveHistory(records, query = {}) {
|
|
1532
1294
|
validateArchiveHistoryQuery(query);
|
|
1533
1295
|
const limit = query.limit ?? HISTORY_DEFAULT_LIMIT;
|
|
1534
|
-
const matched = records.filter((record) => (!query.since || record.date >= query.since) && (!query.until || record.date <= query.until) && (!query.kind || record.kind === query.kind) && (!query.group || record.group === query.group)
|
|
1296
|
+
const matched = records.filter((record) => (!query.since || record.date >= query.since) && (!query.until || record.date <= query.until) && (!query.kind || record.kind === query.kind) && (!query.group || record.group === query.group) && (!query.search || `${record.workRef}
|
|
1297
|
+
${record.searchSummary ?? record.summary}`.toLowerCase().includes(query.search.toLowerCase()))).sort(compareHistoryRecords2);
|
|
1535
1298
|
const selected = matched.slice(0, limit);
|
|
1536
1299
|
return {
|
|
1537
1300
|
records: selected.map(toOutputRecord),
|
|
@@ -1556,6 +1319,8 @@ function validateArchiveHistoryQuery(query) {
|
|
|
1556
1319
|
throw new ArchiveHistoryError("invalid_history_kind", "history kind must be non-empty");
|
|
1557
1320
|
if (query.group !== void 0 && !/^[a-z0-9-]+$/.test(query.group))
|
|
1558
1321
|
throw new ArchiveHistoryError("invalid_history_group", "history group must be one lowercase kebab-case segment");
|
|
1322
|
+
if (query.search !== void 0 && query.search.trim() === "")
|
|
1323
|
+
throw new ArchiveHistoryError("invalid_history_search", "history search must be non-empty");
|
|
1559
1324
|
}
|
|
1560
1325
|
function selectArchiveHistoryRecord(records, selector) {
|
|
1561
1326
|
const matches = "path" in selector ? records.filter((record) => record.path === selector.path) : records.filter((record) => record.workRef === selector.workRef);
|
|
@@ -1571,7 +1336,7 @@ function selectArchiveHistoryRecord(records, selector) {
|
|
|
1571
1336
|
async function readArchiveHistoryDetail(record) {
|
|
1572
1337
|
let content;
|
|
1573
1338
|
try {
|
|
1574
|
-
content = await
|
|
1339
|
+
content = await readFile4(record.sourcePath, "utf-8");
|
|
1575
1340
|
} catch (error) {
|
|
1576
1341
|
throw new ArchiveHistoryError("archive_read_failed", `unable to read archived Change ${record.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1577
1342
|
}
|
|
@@ -1596,7 +1361,7 @@ function historyInspectionComplete(inspection) {
|
|
|
1596
1361
|
return inspection.diagnostics.every((diagnostic) => diagnostic.severity !== "error");
|
|
1597
1362
|
}
|
|
1598
1363
|
function toOutputRecord(record) {
|
|
1599
|
-
const { sourcePath: _sourcePath, ...output } = record;
|
|
1364
|
+
const { searchSummary: _searchSummary, sourcePath: _sourcePath, ...output } = record;
|
|
1600
1365
|
return output;
|
|
1601
1366
|
}
|
|
1602
1367
|
function boundEvidence(section, omitNone = false) {
|
|
@@ -1617,6 +1382,234 @@ function compareHistoryRecords2(left, right) {
|
|
|
1617
1382
|
return right.date.localeCompare(left.date) || left.workRef.localeCompare(right.workRef) || left.path.localeCompare(right.path);
|
|
1618
1383
|
}
|
|
1619
1384
|
|
|
1385
|
+
// src/status/inspect.ts
|
|
1386
|
+
async function inspectProjectStatus(options = {}) {
|
|
1387
|
+
const runtime = [];
|
|
1388
|
+
const diagnostics = [];
|
|
1389
|
+
let manage = resolveManagePolicy({});
|
|
1390
|
+
try {
|
|
1391
|
+
const configInspection = await inspectRspConfig();
|
|
1392
|
+
const configValid = configInspection.issues.length === 0;
|
|
1393
|
+
manage = resolveManagePolicy(configInspection.config, { configValid });
|
|
1394
|
+
if (!configValid) {
|
|
1395
|
+
diagnostics.push({
|
|
1396
|
+
severity: "error",
|
|
1397
|
+
code: "invalid_config",
|
|
1398
|
+
path: CONFIG_PATH,
|
|
1399
|
+
message: configInspection.issues.join("; ")
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
} catch (error) {
|
|
1403
|
+
manage = resolveManagePolicy({}, { configValid: false });
|
|
1404
|
+
diagnostics.push({
|
|
1405
|
+
severity: "error",
|
|
1406
|
+
code: "invalid_config",
|
|
1407
|
+
path: CONFIG_PATH,
|
|
1408
|
+
message: toErrorMessage(error)
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
const focusTree = await inspectFocusTree();
|
|
1412
|
+
for (const diagnostic of focusTree.diagnostics) {
|
|
1413
|
+
diagnostics.push({
|
|
1414
|
+
severity: "error",
|
|
1415
|
+
code: diagnostic.code,
|
|
1416
|
+
change: diagnostic.input,
|
|
1417
|
+
path: diagnostic.path,
|
|
1418
|
+
message: diagnostic.message
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
const rawFocusedSet = new Set(focusTree.markers.map((marker) => marker.name));
|
|
1422
|
+
const focusedSet = /* @__PURE__ */ new Set();
|
|
1423
|
+
const focusedPaths = /* @__PURE__ */ new Map();
|
|
1424
|
+
for (const name of rawFocusedSet) {
|
|
1425
|
+
try {
|
|
1426
|
+
const ref = resolveWorkRef(name, { executable: true });
|
|
1427
|
+
focusedSet.add(ref.name);
|
|
1428
|
+
focusedPaths.set(ref.name, ref.path);
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
if (!(error instanceof WorkRefError))
|
|
1431
|
+
throw error;
|
|
1432
|
+
diagnostics.push({
|
|
1433
|
+
severity: "error",
|
|
1434
|
+
code: error.code,
|
|
1435
|
+
path: `.rsp/focus.d/${name}`,
|
|
1436
|
+
change: name,
|
|
1437
|
+
message: error.message
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
const workTree = await inspectWorkTree();
|
|
1442
|
+
const archiveTree = await inspectArchiveTree();
|
|
1443
|
+
const historyInspection = await inspectArchiveHistory({ archiveTree });
|
|
1444
|
+
diagnostics.push(...workTree.diagnostics.map((diagnostic) => ({
|
|
1445
|
+
severity: "error",
|
|
1446
|
+
code: diagnostic.code,
|
|
1447
|
+
change: diagnostic.input,
|
|
1448
|
+
path: diagnostic.path,
|
|
1449
|
+
message: diagnostic.message
|
|
1450
|
+
})));
|
|
1451
|
+
const groupInspection = await inspectChangeGroups({ workTree, focusTree, archiveTree });
|
|
1452
|
+
diagnostics.push(...groupInspection.diagnostics);
|
|
1453
|
+
const dependencyInspection = await inspectChangeDependencies({
|
|
1454
|
+
workTree,
|
|
1455
|
+
archiveTree,
|
|
1456
|
+
preferredOrder: groupInspection.groups.flatMap((group) => group.slices.map((slice) => slice.name))
|
|
1457
|
+
});
|
|
1458
|
+
diagnostics.push(...dependencyInspection.diagnostics);
|
|
1459
|
+
const changeMap = new Map(workTree.changes.map((ref) => [ref.name, ref.path]));
|
|
1460
|
+
const allNames = [.../* @__PURE__ */ new Set([...changeMap.keys(), ...focusedSet])].sort();
|
|
1461
|
+
const records = [];
|
|
1462
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
1463
|
+
for (const name of allNames) {
|
|
1464
|
+
const path = changeMap.get(name);
|
|
1465
|
+
let kind = "\u2014";
|
|
1466
|
+
let title = name;
|
|
1467
|
+
let blockerEntries = [];
|
|
1468
|
+
let readiness = emptyReadiness(false);
|
|
1469
|
+
let ageDays = null;
|
|
1470
|
+
const isFocused = focusedSet.has(name);
|
|
1471
|
+
let isBlocked = false;
|
|
1472
|
+
let done = 0;
|
|
1473
|
+
let total = 0;
|
|
1474
|
+
let progressKnown = false;
|
|
1475
|
+
if (path) {
|
|
1476
|
+
try {
|
|
1477
|
+
const content = await readFile5(path, "utf-8");
|
|
1478
|
+
title = extractChangeTitle(content) ?? name;
|
|
1479
|
+
blockerEntries = extractBlockerEntries(content);
|
|
1480
|
+
try {
|
|
1481
|
+
const frontmatter = parseFrontmatter(content);
|
|
1482
|
+
kind = frontmatter?.kind ? String(frontmatter.kind) : "\u2014";
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
kind = "(invalid)";
|
|
1485
|
+
runtime.push({
|
|
1486
|
+
code: "frontmatter_parse_failed",
|
|
1487
|
+
operation: "parseFrontmatter",
|
|
1488
|
+
path,
|
|
1489
|
+
message: toErrorMessage(error)
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
isBlocked = dependencyInspection.activeBlockers.get(name) ?? hasMeaningfulBlockers(content);
|
|
1493
|
+
const checkboxes = countCheckboxes(content);
|
|
1494
|
+
done = checkboxes.done;
|
|
1495
|
+
total = checkboxes.total;
|
|
1496
|
+
progressKnown = true;
|
|
1497
|
+
const details = collectArchiveReadiness(content, { activeBlockers: isBlocked });
|
|
1498
|
+
readiness = {
|
|
1499
|
+
incompleteTasks: details.taskTodos.length,
|
|
1500
|
+
incompleteVerify: details.verifyTodos.length,
|
|
1501
|
+
activeBlockers: details.activeBlockers,
|
|
1502
|
+
missingScenarios: details.missingScenarios,
|
|
1503
|
+
deterministic: details.deterministic,
|
|
1504
|
+
semantic: details.semantic,
|
|
1505
|
+
archiveReady: details.archiveReady
|
|
1506
|
+
};
|
|
1507
|
+
} catch (error) {
|
|
1508
|
+
diagnostics.push({
|
|
1509
|
+
severity: "error",
|
|
1510
|
+
code: "change_read_failed",
|
|
1511
|
+
change: name,
|
|
1512
|
+
path,
|
|
1513
|
+
message: "unable to read open Change",
|
|
1514
|
+
hint: toErrorMessage(error)
|
|
1515
|
+
});
|
|
1516
|
+
runtime.push({
|
|
1517
|
+
code: "change_read_failed",
|
|
1518
|
+
operation: "readFile",
|
|
1519
|
+
path,
|
|
1520
|
+
message: toErrorMessage(error)
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
try {
|
|
1524
|
+
const metadata = await stat(path);
|
|
1525
|
+
ageDays = Math.floor((nowMs - metadata.mtime.getTime()) / (1e3 * 60 * 60 * 24));
|
|
1526
|
+
} catch (error) {
|
|
1527
|
+
diagnostics.push({
|
|
1528
|
+
severity: "error",
|
|
1529
|
+
code: "change_stat_failed",
|
|
1530
|
+
change: name,
|
|
1531
|
+
path,
|
|
1532
|
+
message: "unable to inspect open Change metadata",
|
|
1533
|
+
hint: toErrorMessage(error)
|
|
1534
|
+
});
|
|
1535
|
+
runtime.push({
|
|
1536
|
+
code: "change_stat_failed",
|
|
1537
|
+
operation: "stat",
|
|
1538
|
+
path,
|
|
1539
|
+
message: toErrorMessage(error)
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
} else {
|
|
1543
|
+
kind = "(missing)";
|
|
1544
|
+
const path2 = focusedPaths.get(name);
|
|
1545
|
+
diagnostics.push({
|
|
1546
|
+
severity: "error",
|
|
1547
|
+
code: "focused_change_missing",
|
|
1548
|
+
change: name,
|
|
1549
|
+
path: path2,
|
|
1550
|
+
message: "focus marker points to a missing Change file"
|
|
1551
|
+
});
|
|
1552
|
+
runtime.push({
|
|
1553
|
+
code: "focused_change_missing",
|
|
1554
|
+
operation: "resolveChange",
|
|
1555
|
+
path: path2,
|
|
1556
|
+
message: "focus marker points to a missing change file"
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
records.push({
|
|
1560
|
+
output: {
|
|
1561
|
+
name,
|
|
1562
|
+
kind,
|
|
1563
|
+
progress: { done, total },
|
|
1564
|
+
ageDays,
|
|
1565
|
+
isFocused,
|
|
1566
|
+
isBlocked,
|
|
1567
|
+
path: path ? normalizeLogicalPath(path) : null
|
|
1568
|
+
},
|
|
1569
|
+
progressKnown,
|
|
1570
|
+
title,
|
|
1571
|
+
blockerEntries,
|
|
1572
|
+
readiness
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
return {
|
|
1576
|
+
manage,
|
|
1577
|
+
focused: [...focusedSet].sort(),
|
|
1578
|
+
records,
|
|
1579
|
+
groups: groupInspection.groups,
|
|
1580
|
+
plan: dependencyInspection.plan,
|
|
1581
|
+
archiveTrend: historyInspectionComplete(historyInspection) ? deriveArchiveTrend(historyInspection.records) : [],
|
|
1582
|
+
diagnostics: [...diagnostics, ...historyInspection.rootExists ? historyInspection.diagnostics : []],
|
|
1583
|
+
runtime: [...runtime, ...historyInspection.runtime]
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
function emptyReadiness(activeBlockers) {
|
|
1587
|
+
return {
|
|
1588
|
+
incompleteTasks: 0,
|
|
1589
|
+
incompleteVerify: 0,
|
|
1590
|
+
activeBlockers,
|
|
1591
|
+
missingScenarios: false,
|
|
1592
|
+
deterministic: "pass",
|
|
1593
|
+
semantic: "needs-review",
|
|
1594
|
+
archiveReady: activeBlockers ? "no" : "judgment"
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
function extractChangeTitle(content) {
|
|
1598
|
+
const heading = content.split(/\r?\n/).find((line) => line.startsWith("# Change: "));
|
|
1599
|
+
return heading?.slice("# Change: ".length).trim() || null;
|
|
1600
|
+
}
|
|
1601
|
+
function extractBlockerEntries(content) {
|
|
1602
|
+
return extractSection(content, "Blockers").split("\n").map((line) => line.trim()).filter((line) => /^[-*]\s+/.test(line)).map((line) => line.replace(/^[-*]\s+/, "").trim()).filter((line) => line.toLowerCase() !== "none");
|
|
1603
|
+
}
|
|
1604
|
+
function deriveArchiveTrend(records) {
|
|
1605
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1606
|
+
for (const record of records) {
|
|
1607
|
+
const month = record.date.slice(0, 7);
|
|
1608
|
+
counts.set(month, (counts.get(month) ?? 0) + 1);
|
|
1609
|
+
}
|
|
1610
|
+
return [...counts].sort(([left], [right]) => left.localeCompare(right)).map(([month, count]) => ({ month, count }));
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1620
1613
|
// src/status/dependency-forest.ts
|
|
1621
1614
|
function defaultRoots(plan) {
|
|
1622
1615
|
const selected = new Set(plan.nodes.filter((node) => node.selection === "selected").map((node) => node.name));
|
|
@@ -14,17 +14,21 @@ import { chmod, copyFile, mkdir, readdir, readFile, rename, rm } from "fs/promis
|
|
|
14
14
|
import { dirname, join, relative } from "path";
|
|
15
15
|
var DEFAULT_PACKAGED_SKILL_NAMES = [
|
|
16
16
|
"rsp",
|
|
17
|
-
"rsp-address-review",
|
|
18
17
|
"rsp-commit",
|
|
19
18
|
"rsp-design",
|
|
20
19
|
"rsp-diagnose",
|
|
21
20
|
"rsp-implement",
|
|
22
21
|
"rsp-manage",
|
|
23
22
|
"rsp-release-docs",
|
|
23
|
+
"rsp-resolve-findings",
|
|
24
24
|
"rsp-review",
|
|
25
25
|
"rsp-shape",
|
|
26
26
|
"rsp-tdd"
|
|
27
27
|
];
|
|
28
|
+
var OBSOLETE_PACKAGED_SKILL_RENAMES = {
|
|
29
|
+
"rsp-address-review": "rsp-resolve-findings",
|
|
30
|
+
"rsp-codebase-audit": "rsp-structural-audit"
|
|
31
|
+
};
|
|
28
32
|
function unsupportedEntry(path, label) {
|
|
29
33
|
return new ManagedPathError(path, `unsupported entry in the ${label} tree: ${path}`);
|
|
30
34
|
}
|
|
@@ -199,8 +203,9 @@ async function installPackagedSkills(args = {}, options = {}) {
|
|
|
199
203
|
}
|
|
200
204
|
const skills = requestedNames.map((name) => packagedSkillsByName.get(name)).sort((a, b) => a.name.localeCompare(b.name));
|
|
201
205
|
const targetRoot = resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
|
|
202
|
-
const result = { installed: [], unchanged: [], replaced: [] };
|
|
206
|
+
const result = { installed: [], removed: [], unchanged: [], replaced: [] };
|
|
203
207
|
const conflicts = [];
|
|
208
|
+
const obsoleteConflicts = [];
|
|
204
209
|
const targetTreeIdentities = /* @__PURE__ */ new Map();
|
|
205
210
|
for (const skill of skills) {
|
|
206
211
|
const target = join(targetRoot, skill.name);
|
|
@@ -221,8 +226,30 @@ async function installPackagedSkills(args = {}, options = {}) {
|
|
|
221
226
|
conflicts.push(skill.name);
|
|
222
227
|
}
|
|
223
228
|
}
|
|
229
|
+
const selectedNames = new Set(skills.map((skill) => skill.name));
|
|
230
|
+
const obsoleteTreeIdentities = /* @__PURE__ */ new Map();
|
|
231
|
+
for (const [obsolete, replacement] of Object.entries(OBSOLETE_PACKAGED_SKILL_RENAMES)) {
|
|
232
|
+
if (!selectedNames.has(replacement))
|
|
233
|
+
continue;
|
|
234
|
+
const target = join(targetRoot, obsolete);
|
|
235
|
+
if (inspectTarget(target, obsolete) === "missing")
|
|
236
|
+
continue;
|
|
237
|
+
const targetIdentity = inspectDirectoryIdentity(target, `obsolete installed Skill ${obsolete}`);
|
|
238
|
+
assertDirectoryIdentities([targetIdentity], `obsolete installed Skill ${obsolete}`);
|
|
239
|
+
if (args.force) {
|
|
240
|
+
result.removed.push(obsolete);
|
|
241
|
+
obsoleteTreeIdentities.set(obsolete, targetIdentity);
|
|
242
|
+
} else {
|
|
243
|
+
obsoleteConflicts.push(`${obsolete} -> ${replacement}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const conflictMessages = [];
|
|
224
247
|
if (conflicts.length > 0)
|
|
225
|
-
|
|
248
|
+
conflictMessages.push(`conflicting packaged Skills: ${conflicts.join(", ")}`);
|
|
249
|
+
if (obsoleteConflicts.length > 0)
|
|
250
|
+
conflictMessages.push(`obsolete packaged Skill renames: ${obsoleteConflicts.join(", ")}`);
|
|
251
|
+
if (conflictMessages.length > 0)
|
|
252
|
+
throw new Error(`${conflictMessages.join("; ")}; rerun with --force to replace or remove only these package-owned directories`);
|
|
226
253
|
if (args.dryRun)
|
|
227
254
|
return result;
|
|
228
255
|
await options.onMutationStep?.({ phase: "before-target-root-mutation", targetRoot });
|
|
@@ -252,6 +279,15 @@ async function installPackagedSkills(args = {}, options = {}) {
|
|
|
252
279
|
for (const name of result.replaced) {
|
|
253
280
|
assertDirectoryIdentities([targetTreeIdentities.get(name)], `installed Skill ${name}`);
|
|
254
281
|
}
|
|
282
|
+
for (const name of result.removed) {
|
|
283
|
+
assertDirectoryIdentities([obsoleteTreeIdentities.get(name)], `obsolete installed Skill ${name}`);
|
|
284
|
+
}
|
|
285
|
+
for (const name of result.removed) {
|
|
286
|
+
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
287
|
+
assertDirectoryIdentities([obsoleteTreeIdentities.get(name)], `obsolete installed Skill ${name}`);
|
|
288
|
+
await renamePath(join(targetRoot, name), join(previousRoot, name));
|
|
289
|
+
movedPrevious.push(name);
|
|
290
|
+
}
|
|
255
291
|
for (const name of result.replaced) {
|
|
256
292
|
assertDirectoryIdentities(targetIdentities, "project Skills root");
|
|
257
293
|
assertDirectoryIdentities([targetTreeIdentities.get(name)], `installed Skill ${name}`);
|
|
@@ -308,7 +344,7 @@ async function installPackagedSkills(args = {}, options = {}) {
|
|
|
308
344
|
}
|
|
309
345
|
function printSkillInstallResult(result, dryRun = false) {
|
|
310
346
|
const prefix = dryRun ? "would be " : "";
|
|
311
|
-
for (const status of ["installed", "unchanged", "replaced"]) {
|
|
347
|
+
for (const status of ["installed", "unchanged", "replaced", "removed"]) {
|
|
312
348
|
if (result[status].length > 0)
|
|
313
349
|
console.log(` ${prefix}${status}: ${result[status].join(", ")}`);
|
|
314
350
|
}
|