@oevery/rsp 2.0.3 → 2.0.4
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 +25 -5
- package/README.zh-CN.md +25 -5
- package/bin/rsp.mjs +0 -0
- package/dist/cli.mjs +774 -180
- package/package.json +1 -1
- package/rules/rsp-rules.md +15 -19
- package/skills/rsp/SKILL.md +56 -93
package/dist/cli.mjs
CHANGED
|
@@ -119,7 +119,7 @@ function changeNameFromPath(changesDir, filePath) {
|
|
|
119
119
|
function normalizeLogicalPath(pathValue) {
|
|
120
120
|
return pathValue.split(sep).join("/").replace(/\\/g, "/");
|
|
121
121
|
}
|
|
122
|
-
function generateChangeContent(name, summary = "", kind) {
|
|
122
|
+
function generateChangeContent(name, summary = "", kind, options = {}) {
|
|
123
123
|
const proposalSummary = summary || (name === "project-setup" ? "Capture the project model, boundaries, and stable local constraints" : "<one-line summary>");
|
|
124
124
|
if (name === "project-setup") {
|
|
125
125
|
return `---
|
|
@@ -184,6 +184,8 @@ kind: ops
|
|
|
184
184
|
`;
|
|
185
185
|
}
|
|
186
186
|
const frontmatterKind = kind ?? "<choose: feature | fix | refactor | docs | ops | research>";
|
|
187
|
+
if (options.lite)
|
|
188
|
+
return generateLiteChangeContent(name, proposalSummary, frontmatterKind);
|
|
187
189
|
const template = getChangeTemplateByKind(kind);
|
|
188
190
|
return `---
|
|
189
191
|
kind: "${frontmatterKind}"
|
|
@@ -229,6 +231,58 @@ ${template.acceptanceSection}
|
|
|
229
231
|
- [ ] Decide whether this change produced durable knowledge that belongs in \`.rsp/specs/\` or \`.rsp/rules/\`
|
|
230
232
|
- [ ] If yes, write only stable facts to the smallest correct target file before archive; do not promote task history, debugging notes, or one-off implementation context
|
|
231
233
|
|
|
234
|
+
## Blockers
|
|
235
|
+
- none
|
|
236
|
+
`;
|
|
237
|
+
}
|
|
238
|
+
function generateLiteChangeContent(name, proposalSummary, frontmatterKind) {
|
|
239
|
+
return `---
|
|
240
|
+
kind: "${frontmatterKind}"
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
# Change: ${name}
|
|
244
|
+
|
|
245
|
+
## Proposal
|
|
246
|
+
- Summary: ${proposalSummary}
|
|
247
|
+
- Why:
|
|
248
|
+
- <why this small change matters>
|
|
249
|
+
- Scope:
|
|
250
|
+
- <what will change>
|
|
251
|
+
- Non-goals:
|
|
252
|
+
- none
|
|
253
|
+
|
|
254
|
+
## Spec
|
|
255
|
+
### MODIFIED
|
|
256
|
+
- Requirement: <observable outcome>
|
|
257
|
+
- <what should be true after this change>
|
|
258
|
+
|
|
259
|
+
### Acceptance
|
|
260
|
+
#### Scenario: change is complete
|
|
261
|
+
- GIVEN <current context>
|
|
262
|
+
- WHEN <the change is applied>
|
|
263
|
+
- THEN <expected outcome>
|
|
264
|
+
|
|
265
|
+
## Design
|
|
266
|
+
- Approach:
|
|
267
|
+
- <minimal implementation approach>
|
|
268
|
+
- Affected areas:
|
|
269
|
+
- <primary file, directory, or doc path>
|
|
270
|
+
- Constraints:
|
|
271
|
+
- <important constraint, or none>
|
|
272
|
+
|
|
273
|
+
## Tasks
|
|
274
|
+
- [ ] Implement the small change
|
|
275
|
+
- [ ] Verify the result
|
|
276
|
+
|
|
277
|
+
## Verify
|
|
278
|
+
- Automated:
|
|
279
|
+
- [ ] <exact command, or not needed: reason>
|
|
280
|
+
- Manual:
|
|
281
|
+
- [ ] <exact scenario, or not needed: reason>
|
|
282
|
+
- Durable updates:
|
|
283
|
+
- [ ] Decide whether this change produced durable knowledge that belongs in ".rsp/specs/" or ".rsp/rules/"
|
|
284
|
+
- [ ] If yes, write only stable facts to the smallest correct target file before archive
|
|
285
|
+
|
|
232
286
|
## Blockers
|
|
233
287
|
- none
|
|
234
288
|
`;
|
|
@@ -240,7 +294,7 @@ function getChangeTemplateByKind(kind) {
|
|
|
240
294
|
why: "<what user need or capability gap this addresses>",
|
|
241
295
|
scope: "<what new behavior or capability will be delivered>",
|
|
242
296
|
nonGoals: "<what related capabilities are explicitly out of scope>",
|
|
243
|
-
specSection: "### ADDED\n- Requirement: <new user-facing behavior>\n - <verifiable requirement for the new capability>",
|
|
297
|
+
specSection: "<!-- Describe observable behavior and requirements. Implementation notes belong in ## Design. -->\n### ADDED\n- Requirement: <new user-facing behavior>\n - <verifiable requirement for the new capability>",
|
|
244
298
|
acceptanceSection: "#### Scenario: user exercises the new capability\n- GIVEN <context>\n- WHEN <user action>\n- THEN <expected new behavior>",
|
|
245
299
|
approach: "<how the new capability will be implemented>",
|
|
246
300
|
affectedArea1: "<concrete file path, directory, module, or subsystem 1>",
|
|
@@ -255,7 +309,7 @@ function getChangeTemplateByKind(kind) {
|
|
|
255
309
|
why: "<what current behavior is wrong or broken>",
|
|
256
310
|
scope: "<what specific defect is being corrected>",
|
|
257
311
|
nonGoals: "<what related but separate issues are out of scope>",
|
|
258
|
-
specSection: "### MODIFIED\n- Requirement: correct behavior\n - <expected correct behavior after the fix>",
|
|
312
|
+
specSection: "<!-- Describe expected correct behavior. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: correct behavior\n - <expected correct behavior after the fix>",
|
|
259
313
|
acceptanceSection: "#### Scenario: defect is resolved\n- GIVEN <conditions that trigger the defect>\n- WHEN <action that previously failed>\n- THEN <expected correct outcome>\n- AND the original broken outcome no longer occurs",
|
|
260
314
|
approach: "<root cause analysis and fix strategy>",
|
|
261
315
|
affectedArea1: "<concrete file path, directory, module, or subsystem containing the defect>",
|
|
@@ -270,7 +324,7 @@ function getChangeTemplateByKind(kind) {
|
|
|
270
324
|
why: "<what maintainability, readability, or structural issue motivates this>",
|
|
271
325
|
scope: "<what code areas are being restructured>",
|
|
272
326
|
nonGoals: "<no behavior change is expected or intended>",
|
|
273
|
-
specSection: "### MODIFIED\n- Requirement: internal structure improvement\n - <desired structural outcome without observable behavior change>",
|
|
327
|
+
specSection: "<!-- Describe the desired structural outcome. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: internal structure improvement\n - <desired structural outcome without observable behavior change>",
|
|
274
328
|
acceptanceSection: "#### Scenario: behavior is preserved after restructuring\n- GIVEN the existing codebase before the refactor\n- WHEN the refactored code runs against existing tests\n- THEN all existing tests pass unchanged",
|
|
275
329
|
approach: "<how the code will be restructured (rename, extract, move, etc.)>",
|
|
276
330
|
affectedArea1: "<concrete file path, directory, module, or subsystem being refactored>",
|
|
@@ -285,7 +339,7 @@ function getChangeTemplateByKind(kind) {
|
|
|
285
339
|
why: "<why the documentation change matters>",
|
|
286
340
|
scope: "<which docs, readers, or workflows will be updated>",
|
|
287
341
|
nonGoals: "<what documentation behavior or audience is out of scope>",
|
|
288
|
-
specSection: "### MODIFIED\n- Requirement: documentation accuracy\n - <what durable reader-facing behavior or explanation changes>",
|
|
342
|
+
specSection: "<!-- Describe what the reader should see or experience. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: documentation accuracy\n - <what durable reader-facing behavior or explanation changes>",
|
|
289
343
|
acceptanceSection: "#### Scenario: reader follows the updated guidance\n- GIVEN the updated documentation\n- WHEN a reader follows the documented workflow\n- THEN the steps are accurate and sufficient",
|
|
290
344
|
approach: "<how the documentation will be updated and organized>",
|
|
291
345
|
affectedArea1: "<concrete doc path, directory, module doc, or documentation surface 1>",
|
|
@@ -300,7 +354,7 @@ function getChangeTemplateByKind(kind) {
|
|
|
300
354
|
why: "<what question or uncertainty this research resolves>",
|
|
301
355
|
scope: "<what system area, option set, or hypothesis is being examined>",
|
|
302
356
|
nonGoals: "<what implementation work is explicitly out of scope>",
|
|
303
|
-
specSection: "### ADDED\n- Requirement: research outcome recording\n - <what finding, option, or decision must be captured clearly>",
|
|
357
|
+
specSection: "<!-- Describe what finding or decision must be captured. Implementation notes belong in ## Design. -->\n### ADDED\n- Requirement: research outcome recording\n - <what finding, option, or decision must be captured clearly>",
|
|
304
358
|
acceptanceSection: "#### Scenario: research question is resolved\n- GIVEN the current uncertainty or open question\n- WHEN the investigation is completed\n- THEN the result is recorded clearly enough to guide follow-up work",
|
|
305
359
|
approach: "<how the investigation will be performed and what evidence will be gathered>",
|
|
306
360
|
affectedArea1: "<concrete file path, directory, module, subsystem, or source under investigation>",
|
|
@@ -315,7 +369,7 @@ function getChangeTemplateByKind(kind) {
|
|
|
315
369
|
why: "<why this operational or environment change matters>",
|
|
316
370
|
scope: "<what environment, workflow, or operational path changes>",
|
|
317
371
|
nonGoals: "<what product or feature behavior will not change>",
|
|
318
|
-
specSection: "### MODIFIED\n- Requirement: operational behavior\n - <what reliable operational outcome should change or stay true>",
|
|
372
|
+
specSection: "<!-- Describe the reliable operational outcome. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: operational behavior\n - <what reliable operational outcome should change or stay true>",
|
|
319
373
|
acceptanceSection: "#### Scenario: operational path succeeds\n- GIVEN the target environment or workflow\n- WHEN the operational change is applied\n- THEN the expected operational outcome is reliable",
|
|
320
374
|
approach: "<how the operational change will be applied safely>",
|
|
321
375
|
affectedArea1: "<concrete script, config path, workflow, environment path, or operational surface 1>",
|
|
@@ -330,7 +384,7 @@ function getChangeTemplateByKind(kind) {
|
|
|
330
384
|
why: "<why this change matters>",
|
|
331
385
|
scope: "<what this change will do>",
|
|
332
386
|
nonGoals: "<what this change will not do>",
|
|
333
|
-
specSection: "### ADDED\n- Requirement: <new or updated behavior>\n - <verifiable requirement>",
|
|
387
|
+
specSection: "<!-- Describe observable behavior and requirements. Implementation notes belong in ## Design. -->\n### ADDED\n- Requirement: <new or updated behavior>\n - <verifiable requirement>",
|
|
334
388
|
acceptanceSection: "#### Scenario: <name>\n- GIVEN <context>\n- WHEN <action>\n- THEN <expected outcome>",
|
|
335
389
|
approach: "<how the change will be implemented>",
|
|
336
390
|
affectedArea1: "<concrete file path, directory, module, or subsystem 1>",
|
|
@@ -357,7 +411,7 @@ Read in order:
|
|
|
357
411
|
5. .rsp/specs/INDEX.md
|
|
358
412
|
6. only the relevant additional .rsp/rules/*.md and .rsp/specs/*.md files
|
|
359
413
|
|
|
360
|
-
If \`.rsp/focus.d/\` is empty, ask what to work on or suggest \`npx -y @oevery/rsp create <name
|
|
414
|
+
If \`.rsp/focus.d/\` is empty and the user has not provided a concrete task, ask what to work on or suggest \`npx -y @oevery/rsp create <name>\` for tracked work.
|
|
361
415
|
If your agent supports Agent Skills, load \`rsp\` for setup, repair, and durable-decision tasks.
|
|
362
416
|
${RSP_AGENTS_END}`;
|
|
363
417
|
}
|
|
@@ -547,6 +601,57 @@ function detectDeltaSections(content) {
|
|
|
547
601
|
removed: /^###\s*REMOVED/im.test(body)
|
|
548
602
|
};
|
|
549
603
|
}
|
|
604
|
+
function collectArchiveReadiness(content) {
|
|
605
|
+
const warnings = [];
|
|
606
|
+
const tasksSection = extractSection(content, "Tasks");
|
|
607
|
+
const verifySection = extractSection(content, "Verify");
|
|
608
|
+
const taskTodos = getOpenCheckboxes(tasksSection);
|
|
609
|
+
if (taskTodos.length > 0)
|
|
610
|
+
warnings.push(`${taskTodos.length} task item(s) still incomplete`);
|
|
611
|
+
const verifyTodos = getOpenCheckboxes(verifySection);
|
|
612
|
+
if (verifyTodos.length > 0)
|
|
613
|
+
warnings.push(`${verifyTodos.length} Verify checklist item(s) are still incomplete`);
|
|
614
|
+
const activeBlockers = hasMeaningfulBlockers(content);
|
|
615
|
+
if (activeBlockers)
|
|
616
|
+
warnings.push("active blockers are present in the change file");
|
|
617
|
+
const scenarios = parseScenarios(content);
|
|
618
|
+
const missingScenarios = scenarios.length === 0;
|
|
619
|
+
if (missingScenarios)
|
|
620
|
+
warnings.push("no Scenario blocks found (some changes do not need them)");
|
|
621
|
+
const archiveReady = activeBlockers ? "no" : warnings.length === 0 ? "yes" : "judgment";
|
|
622
|
+
return {
|
|
623
|
+
taskTodos,
|
|
624
|
+
verifyTodos,
|
|
625
|
+
activeBlockers,
|
|
626
|
+
scenarioCount: scenarios.length,
|
|
627
|
+
missingScenarios,
|
|
628
|
+
deterministic: warnings.length === 0 ? "pass" : "warnings",
|
|
629
|
+
semantic: "needs-review",
|
|
630
|
+
archiveReady,
|
|
631
|
+
warnings
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function buildDurableReviewGuidance(candidateTargets) {
|
|
635
|
+
return {
|
|
636
|
+
required: true,
|
|
637
|
+
decisions: [
|
|
638
|
+
"No durable update needed",
|
|
639
|
+
"Update existing spec or rule",
|
|
640
|
+
"Create a new durable spec"
|
|
641
|
+
],
|
|
642
|
+
candidateTargets,
|
|
643
|
+
note: "Semantic review decides whether stable facts belong in specs or rules; the CLI never merges delta specs automatically."
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function getDurableReviewCandidateTargets(options = {}) {
|
|
647
|
+
const targets = [".rsp/specs/design.md"];
|
|
648
|
+
if (options.projectRulesExists)
|
|
649
|
+
targets.push(".rsp/rules/project-rules.md");
|
|
650
|
+
return targets;
|
|
651
|
+
}
|
|
652
|
+
function collectArchiveChecklist(content) {
|
|
653
|
+
return collectArchiveReadiness(content).warnings;
|
|
654
|
+
}
|
|
550
655
|
function parseScenarios(content) {
|
|
551
656
|
const scenarios = [];
|
|
552
657
|
const regex = /^#{3,4}\s*Scenario:\s*(.+)$/gim;
|
|
@@ -692,20 +797,30 @@ import { dirname as dirname4, join as join5 } from "path";
|
|
|
692
797
|
import { existsSync as existsSync4 } from "fs";
|
|
693
798
|
import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
694
799
|
import { basename as basename2, join as join4, relative as relative2 } from "path";
|
|
695
|
-
async function buildSpecsIndex({ acquireLock = true } = {}) {
|
|
800
|
+
async function buildSpecsIndex({ acquireLock = true, quiet = false } = {}) {
|
|
696
801
|
if (acquireLock)
|
|
697
|
-
return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false }));
|
|
802
|
+
return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false, quiet }));
|
|
698
803
|
const specsDir = join4(RSP_DIR, "specs");
|
|
699
804
|
if (!existsSync4(specsDir)) {
|
|
700
|
-
|
|
805
|
+
if (!quiet)
|
|
806
|
+
console.log(` ${pc.dim("No specs directory.")}
|
|
701
807
|
`);
|
|
702
|
-
return;
|
|
808
|
+
return false;
|
|
703
809
|
}
|
|
704
810
|
const specFiles = (await walkMarkdownFiles(specsDir)).filter((fp) => {
|
|
705
811
|
const name = basename2(fp);
|
|
706
812
|
return name !== "INDEX.md" && name !== "design.md";
|
|
707
813
|
}).sort();
|
|
708
814
|
const lines = [];
|
|
815
|
+
lines.push("---");
|
|
816
|
+
lines.push("title: Specs Index");
|
|
817
|
+
lines.push("summary: Additional project-level specs beyond design.md.");
|
|
818
|
+
lines.push("kind: generated-index");
|
|
819
|
+
lines.push("index_type: specs");
|
|
820
|
+
lines.push("source_dir: .rsp/specs");
|
|
821
|
+
lines.push(`entry_count: ${specFiles.length}`);
|
|
822
|
+
lines.push("---");
|
|
823
|
+
lines.push("");
|
|
709
824
|
lines.push("# Specs Index");
|
|
710
825
|
lines.push("");
|
|
711
826
|
lines.push("_Additional project-level specs beyond `design.md`._");
|
|
@@ -725,11 +840,16 @@ async function buildSpecsIndex({ acquireLock = true } = {}) {
|
|
|
725
840
|
lines.push(`| ${escapeCell(rel)} | ${escapeCell(title)} | ${escapeCell(summary || "\u2014")} |`);
|
|
726
841
|
}
|
|
727
842
|
}
|
|
728
|
-
|
|
729
|
-
lines.
|
|
730
|
-
await
|
|
731
|
-
|
|
843
|
+
const indexPath = join4(specsDir, "INDEX.md");
|
|
844
|
+
const nextContent = lines.join("\n");
|
|
845
|
+
const existingContent = existsSync4(indexPath) ? await readFile4(indexPath, "utf-8") : null;
|
|
846
|
+
const changed = existingContent !== nextContent;
|
|
847
|
+
if (changed)
|
|
848
|
+
await writeFile3(indexPath, nextContent);
|
|
849
|
+
if (!quiet)
|
|
850
|
+
console.log(` ${pc.green("INDEX.md updated:")} ${specFiles.length} spec file(s).
|
|
732
851
|
`);
|
|
852
|
+
return changed;
|
|
733
853
|
}
|
|
734
854
|
function extractTitle(content) {
|
|
735
855
|
for (const rawLine of content.split("\n")) {
|
|
@@ -796,25 +916,33 @@ import { basename as basename4, dirname as dirname6, join as join7 } from "path"
|
|
|
796
916
|
import { existsSync as existsSync6 } from "fs";
|
|
797
917
|
import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
798
918
|
import { basename as basename3, dirname as dirname5, join as join6, relative as relative3 } from "path";
|
|
799
|
-
async function buildArchiveIndex({ acquireLock = true } = {}) {
|
|
919
|
+
async function buildArchiveIndex({ acquireLock = true, quiet = false } = {}) {
|
|
800
920
|
if (acquireLock)
|
|
801
|
-
return withRspLock("archive-index", async () => buildArchiveIndex({ acquireLock: false }));
|
|
921
|
+
return withRspLock("archive-index", async () => buildArchiveIndex({ acquireLock: false, quiet }));
|
|
802
922
|
if (!existsSync6(ARCHIVES_DIR)) {
|
|
803
|
-
|
|
923
|
+
if (!quiet)
|
|
924
|
+
console.log(` ${pc.dim("No archives directory.")}
|
|
804
925
|
`);
|
|
805
|
-
return;
|
|
926
|
+
return false;
|
|
806
927
|
}
|
|
807
928
|
const archiveFiles = (await walkMarkdownFiles(ARCHIVES_DIR)).filter((fp) => basename3(fp) !== "INDEX.md").sort();
|
|
808
929
|
const lines = [];
|
|
809
|
-
lines.push("
|
|
930
|
+
lines.push("---");
|
|
931
|
+
lines.push("title: Archive Index");
|
|
932
|
+
lines.push("summary: Completed RSP changes.");
|
|
933
|
+
lines.push("kind: generated-index");
|
|
934
|
+
lines.push("index_type: archives");
|
|
935
|
+
lines.push("source_dir: .rsp/archives");
|
|
936
|
+
lines.push(`entry_count: ${archiveFiles.length}`);
|
|
937
|
+
lines.push("---");
|
|
810
938
|
lines.push("");
|
|
811
|
-
lines.push(
|
|
939
|
+
lines.push("# Archive Index");
|
|
812
940
|
lines.push("");
|
|
813
941
|
if (archiveFiles.length === 0) {
|
|
814
942
|
lines.push("_No archived changes yet._");
|
|
815
943
|
} else {
|
|
816
|
-
lines.push("| Date | Change | Summary |");
|
|
817
|
-
lines.push("
|
|
944
|
+
lines.push("| Date | Change | Kind | Summary |");
|
|
945
|
+
lines.push("|------|--------|------|---------|");
|
|
818
946
|
}
|
|
819
947
|
for (const fp of archiveFiles) {
|
|
820
948
|
const base = basename3(fp);
|
|
@@ -825,27 +953,39 @@ async function buildArchiveIndex({ acquireLock = true } = {}) {
|
|
|
825
953
|
const baseName = dateMatch ? dateMatch[2] : base.replace(/\.md$/, "");
|
|
826
954
|
const changeName = archiveDirPart !== "." ? `${archiveDirPart}/${baseName}` : baseName;
|
|
827
955
|
let summary = "";
|
|
956
|
+
let kind = "";
|
|
828
957
|
try {
|
|
829
958
|
const content = await readFile5(fp, "utf-8");
|
|
959
|
+
const fm = parseFrontmatter(content);
|
|
960
|
+
if (typeof fm?.kind === "string")
|
|
961
|
+
kind = fm.kind;
|
|
962
|
+
if (typeof fm?.summary === "string")
|
|
963
|
+
summary = fm.summary.trim();
|
|
830
964
|
const proposal = extractSection(content, "Proposal");
|
|
831
965
|
const summaryLine = proposal.split("\n").map((line) => line.trim()).find((line) => line.startsWith("- Summary:"));
|
|
832
|
-
if (summaryLine)
|
|
966
|
+
if (!summary && summaryLine)
|
|
833
967
|
summary = summaryLine.slice("- Summary:".length).trim();
|
|
834
968
|
} catch {
|
|
835
969
|
}
|
|
836
|
-
|
|
837
|
-
lines.push(`| ${date} | ${changeName} | ${summaryCell} |`);
|
|
970
|
+
lines.push(`| ${date} | ${escapeCell2(changeName)} | ${escapeCell2(kind || "\u2014")} | ${escapeCell2(summary || "\u2014")} |`);
|
|
838
971
|
}
|
|
839
|
-
lines.push("");
|
|
840
|
-
lines.push("_Auto-generated by `rsp update`._");
|
|
841
972
|
const indexPath = join6(ARCHIVES_DIR, "INDEX.md");
|
|
842
|
-
|
|
843
|
-
|
|
973
|
+
const nextContent = lines.join("\n");
|
|
974
|
+
const existingContent = existsSync6(indexPath) ? await readFile5(indexPath, "utf-8") : null;
|
|
975
|
+
const changed = existingContent !== nextContent;
|
|
976
|
+
if (changed)
|
|
977
|
+
await writeFile5(indexPath, nextContent);
|
|
978
|
+
if (!quiet)
|
|
979
|
+
console.log(` ${pc.green("INDEX.md updated:")} ${archiveFiles.length} archived change(s).
|
|
844
980
|
`);
|
|
981
|
+
return changed;
|
|
982
|
+
}
|
|
983
|
+
function escapeCell2(value) {
|
|
984
|
+
return value.replace(/\|/g, "\\|");
|
|
845
985
|
}
|
|
846
986
|
|
|
847
987
|
// src/commands/archive.ts
|
|
848
|
-
async function archiveChange(name) {
|
|
988
|
+
async function archiveChange(name, options = {}) {
|
|
849
989
|
if (!name) {
|
|
850
990
|
console.error(` ${pc.red("Usage:")} rsp archive <name>`);
|
|
851
991
|
process.exit(1);
|
|
@@ -860,6 +1000,24 @@ async function archiveChange(name) {
|
|
|
860
1000
|
console.error(` ${pc.red("Change not found:")} .rsp/changes/${name}.md`);
|
|
861
1001
|
process.exit(1);
|
|
862
1002
|
}
|
|
1003
|
+
if (options.dryRun) {
|
|
1004
|
+
const content = await readFile6(srcPath, "utf-8");
|
|
1005
|
+
const checklist = collectArchiveChecklist(content);
|
|
1006
|
+
console.log();
|
|
1007
|
+
console.log(` ${pc.bold("Archive dry-run for")} ${pc.cyan(name)}`);
|
|
1008
|
+
console.log();
|
|
1009
|
+
if (checklist.length === 0) {
|
|
1010
|
+
console.log(` ${pc.green("\u2713")} Ready to archive. No deterministic warnings found.
|
|
1011
|
+
`);
|
|
1012
|
+
} else {
|
|
1013
|
+
for (const line of checklist)
|
|
1014
|
+
console.log(` ${pc.yellow("\u26A0")} ${line}`);
|
|
1015
|
+
console.log();
|
|
1016
|
+
console.log(` ${pc.dim("Review the warnings above before treating this work as fully closed.")}
|
|
1017
|
+
`);
|
|
1018
|
+
}
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
863
1021
|
return withRspLock("archive-change", async () => {
|
|
864
1022
|
const content = await readFile6(srcPath, "utf-8");
|
|
865
1023
|
const checklist = collectArchiveChecklist(content);
|
|
@@ -929,23 +1087,6 @@ function resolveArchiveName(archiveSubdir, date, base) {
|
|
|
929
1087
|
console.error(` ${pc.red("Error:")} archive name collision exceeded ${MAX_SUFFIX} attempts for ${base}`);
|
|
930
1088
|
process.exit(1);
|
|
931
1089
|
}
|
|
932
|
-
function collectArchiveChecklist(content) {
|
|
933
|
-
const warnings = [];
|
|
934
|
-
const tasksSection = extractSection(content, "Tasks");
|
|
935
|
-
const verifySection = extractSection(content, "Verify");
|
|
936
|
-
const taskTodos = getOpenCheckboxes(tasksSection);
|
|
937
|
-
if (taskTodos.length > 0)
|
|
938
|
-
warnings.push(`${taskTodos.length} task item(s) still incomplete`);
|
|
939
|
-
const verifyTodos = getOpenCheckboxes(verifySection);
|
|
940
|
-
if (verifyTodos.length > 0)
|
|
941
|
-
warnings.push(`${verifyTodos.length} Verify checklist item(s) are still incomplete`);
|
|
942
|
-
if (hasMeaningfulBlockers(content))
|
|
943
|
-
warnings.push("active blockers are present in the change file");
|
|
944
|
-
const scenarios = parseScenarios(content);
|
|
945
|
-
if (scenarios.length === 0)
|
|
946
|
-
warnings.push("no Scenario blocks found (some changes do not need them)");
|
|
947
|
-
return warnings;
|
|
948
|
-
}
|
|
949
1090
|
|
|
950
1091
|
// src/commands/check.ts
|
|
951
1092
|
import { existsSync as existsSync8 } from "fs";
|
|
@@ -960,6 +1101,10 @@ async function runCheck(options = {}) {
|
|
|
960
1101
|
const runtime = [];
|
|
961
1102
|
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
|
|
962
1103
|
const addDiagnostic = (diagnostic) => diagnostics.push(diagnostic);
|
|
1104
|
+
let focusedSet = /* @__PURE__ */ new Set();
|
|
1105
|
+
if (options.focused) {
|
|
1106
|
+
focusedSet = await getFocusedChangeNames({ onError: reportRuntime });
|
|
1107
|
+
}
|
|
963
1108
|
if (existsSync8(FOCUS_DIR)) {
|
|
964
1109
|
const entries = await walkFiles(FOCUS_DIR, { onError: reportRuntime });
|
|
965
1110
|
for (const entryPath of entries) {
|
|
@@ -1004,11 +1149,13 @@ async function runCheck(options = {}) {
|
|
|
1004
1149
|
}
|
|
1005
1150
|
}
|
|
1006
1151
|
}
|
|
1007
|
-
const
|
|
1152
|
+
const allChangeFiles = existsSync8(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
|
|
1153
|
+
const changeFiles = options.focused ? allChangeFiles.filter((fp) => focusedSet.has(changeNameFromPath(CHANGES_DIR, fp))) : allChangeFiles;
|
|
1008
1154
|
if (changeFiles.length === 0) {
|
|
1009
1155
|
const result2 = {
|
|
1010
1156
|
command: "check",
|
|
1011
1157
|
ok: diagnostics.every((d) => d.severity !== "error"),
|
|
1158
|
+
focused: Boolean(options.focused),
|
|
1012
1159
|
diagnostics,
|
|
1013
1160
|
runtime,
|
|
1014
1161
|
summary: {
|
|
@@ -1023,6 +1170,8 @@ async function runCheck(options = {}) {
|
|
|
1023
1170
|
}
|
|
1024
1171
|
console.log();
|
|
1025
1172
|
console.log(` ${pc.bold("RSP check")}`);
|
|
1173
|
+
if (options.focused)
|
|
1174
|
+
console.log(` ${pc.dim("(focused only)")}`);
|
|
1026
1175
|
console.log();
|
|
1027
1176
|
if (diagnostics.length === 0) {
|
|
1028
1177
|
console.log(` ${pc.dim("No change files to check.")}
|
|
@@ -1159,6 +1308,30 @@ async function runCheck(options = {}) {
|
|
|
1159
1308
|
message: `delta markers found: ${parts.join(", ")}`
|
|
1160
1309
|
});
|
|
1161
1310
|
}
|
|
1311
|
+
const placeholderLines = findUnfinishedTemplateLines(content);
|
|
1312
|
+
if (placeholderLines.length > 0) {
|
|
1313
|
+
addDiagnostic({
|
|
1314
|
+
severity: "warning",
|
|
1315
|
+
code: "unfinished_template_placeholders",
|
|
1316
|
+
change: name,
|
|
1317
|
+
path: fp,
|
|
1318
|
+
message: "unfinished template placeholders found",
|
|
1319
|
+
details: placeholderLines,
|
|
1320
|
+
hint: "Replace angle-bracket template text with concrete change details before archiving."
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
const clarificationLines = findUnresolvedClarificationLines(content);
|
|
1324
|
+
if (clarificationLines.length > 0) {
|
|
1325
|
+
addDiagnostic({
|
|
1326
|
+
severity: "warning",
|
|
1327
|
+
code: "unresolved_clarifications",
|
|
1328
|
+
change: name,
|
|
1329
|
+
path: fp,
|
|
1330
|
+
message: "unresolved clarification markers found",
|
|
1331
|
+
details: clarificationLines,
|
|
1332
|
+
hint: "Resolve or remove clarification markers once the open question is answered."
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1162
1335
|
const scenarios = parseScenarios(content);
|
|
1163
1336
|
if (scenarios.length > 0) {
|
|
1164
1337
|
const scenarioIssues = [];
|
|
@@ -1191,6 +1364,7 @@ async function runCheck(options = {}) {
|
|
|
1191
1364
|
const result = {
|
|
1192
1365
|
command: "check",
|
|
1193
1366
|
ok: diagnostics.every((d) => d.severity !== "error"),
|
|
1367
|
+
focused: Boolean(options.focused),
|
|
1194
1368
|
diagnostics,
|
|
1195
1369
|
runtime,
|
|
1196
1370
|
summary: {
|
|
@@ -1205,6 +1379,8 @@ async function runCheck(options = {}) {
|
|
|
1205
1379
|
}
|
|
1206
1380
|
console.log();
|
|
1207
1381
|
console.log(` ${pc.bold("RSP check")}`);
|
|
1382
|
+
if (options.focused)
|
|
1383
|
+
console.log(` ${pc.dim("(focused only)")}`);
|
|
1208
1384
|
console.log();
|
|
1209
1385
|
for (const diagnostic of diagnostics) {
|
|
1210
1386
|
const icon = diagnostic.severity === "error" ? pc.red("\u2717") : diagnostic.severity === "warning" ? pc.yellow("\u26A0") : pc.dim("\u2139");
|
|
@@ -1225,12 +1401,35 @@ async function runCheck(options = {}) {
|
|
|
1225
1401
|
`);
|
|
1226
1402
|
return result;
|
|
1227
1403
|
}
|
|
1404
|
+
function findUnfinishedTemplateLines(content) {
|
|
1405
|
+
const placeholderRe = /<[^>\n]*(?:choose:|one-line|what |why |which |who |how |new |updated |expected |actual |specific |concrete |exact |verifiable |user |capability |behavior |context |action |path|directory|module|subsystem|command|scenario|constraint|requirement|question|uncertainty|implementation)[^>\n]*>/i;
|
|
1406
|
+
return collectMatchingLines(content, (line) => {
|
|
1407
|
+
const trimmed = stripInlineCodeSpans(line).trim();
|
|
1408
|
+
return trimmed.startsWith("<!--") === false && placeholderRe.test(trimmed);
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
function findUnresolvedClarificationLines(content) {
|
|
1412
|
+
return collectMatchingLines(content, (line) => /\[(?:NEEDS|TODO|TBD)\s+CLARIFICATION(?::[^\]]*)?\]/i.test(stripInlineCodeSpans(line)));
|
|
1413
|
+
}
|
|
1414
|
+
function stripInlineCodeSpans(line) {
|
|
1415
|
+
return line.replace(/`[^`]*`/g, "");
|
|
1416
|
+
}
|
|
1417
|
+
function collectMatchingLines(content, predicate) {
|
|
1418
|
+
const matches = [];
|
|
1419
|
+
const lines = content.split("\n");
|
|
1420
|
+
for (let index = 0; index < lines.length; index++) {
|
|
1421
|
+
const line = lines[index];
|
|
1422
|
+
if (predicate(line))
|
|
1423
|
+
matches.push(`line ${index + 1}: ${line.trim()}`);
|
|
1424
|
+
}
|
|
1425
|
+
return matches.slice(0, 10);
|
|
1426
|
+
}
|
|
1228
1427
|
|
|
1229
1428
|
// src/commands/create.ts
|
|
1230
1429
|
import { existsSync as existsSync9 } from "fs";
|
|
1231
1430
|
import { mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
|
|
1232
1431
|
import { dirname as dirname7, join as join9 } from "path";
|
|
1233
|
-
async function createChange(name, summary = "", kind) {
|
|
1432
|
+
async function createChange(name, summary = "", kind, options = {}) {
|
|
1234
1433
|
if (!name) {
|
|
1235
1434
|
console.error(` ${pc.red("Usage:")} rsp create <name> [summary]`);
|
|
1236
1435
|
process.exit(1);
|
|
@@ -1245,7 +1444,7 @@ async function createChange(name, summary = "", kind) {
|
|
|
1245
1444
|
await mkdir5(dirname7(changePath), { recursive: true });
|
|
1246
1445
|
const existed = existsSync9(changePath);
|
|
1247
1446
|
if (!existed) {
|
|
1248
|
-
const content = generateChangeContent(name, summary, kind);
|
|
1447
|
+
const content = generateChangeContent(name, summary, kind, { lite: Boolean(options.lite) });
|
|
1249
1448
|
await writeFile6(changePath, content);
|
|
1250
1449
|
await mkdir5(FOCUS_DIR, { recursive: true });
|
|
1251
1450
|
const focusEntry = join9(FOCUS_DIR, name);
|
|
@@ -1258,37 +1457,101 @@ async function createChange(name, summary = "", kind) {
|
|
|
1258
1457
|
console.log(` ${pc.dim("Unchanged focus.")} Run: rsp focus ${name}`);
|
|
1259
1458
|
else
|
|
1260
1459
|
console.log(` ${pc.dim("focused via focus.d")} \u2192 ${name}`);
|
|
1261
|
-
console.log(` ${pc.cyan("Next:")} fill proposal/spec/design first, then implement and complete the tasks
|
|
1460
|
+
console.log(` ${pc.cyan("Next:")} ${options.lite ? "fill the lite change details, then implement and verify" : "fill proposal/spec/design first, then implement and complete the tasks"}
|
|
1262
1461
|
`);
|
|
1263
1462
|
});
|
|
1264
1463
|
}
|
|
1265
1464
|
|
|
1266
1465
|
// src/commands/doctor.ts
|
|
1466
|
+
import { existsSync as existsSync11 } from "fs";
|
|
1467
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1468
|
+
import { basename as basename5, join as join11, relative as relative5 } from "path";
|
|
1469
|
+
|
|
1470
|
+
// src/commands/update.ts
|
|
1267
1471
|
import { existsSync as existsSync10 } from "fs";
|
|
1268
|
-
import { readFile as readFile8 } from "fs/promises";
|
|
1269
|
-
import {
|
|
1472
|
+
import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
|
|
1473
|
+
import { join as join10 } from "path";
|
|
1474
|
+
var SKILL_REFRESH_HINT = " Note: if you use the published RSP skill, refresh it too:\n npx skills add oevery/rsp\n";
|
|
1475
|
+
async function updateProject(options = {}) {
|
|
1476
|
+
if (!existsSync10(RSP_DIR)) {
|
|
1477
|
+
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
1478
|
+
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
1479
|
+
process.exit(1);
|
|
1480
|
+
}
|
|
1481
|
+
return withRspLock("update", async () => {
|
|
1482
|
+
let updated = false;
|
|
1483
|
+
const actions = [];
|
|
1484
|
+
const bundledRules = await readFile8(join10(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
|
|
1485
|
+
const rulesPath = join10(RSP_DIR, "rules", "rsp-rules.md");
|
|
1486
|
+
await mkdir6(join10(RSP_DIR, "rules"), { recursive: true });
|
|
1487
|
+
const existingRules = existsSync10(rulesPath) ? await readFile8(rulesPath, "utf-8") : null;
|
|
1488
|
+
if (existingRules !== bundledRules) {
|
|
1489
|
+
await writeFile7(rulesPath, bundledRules);
|
|
1490
|
+
actions.push("rules/rsp-rules.md updated");
|
|
1491
|
+
if (!options.quiet)
|
|
1492
|
+
console.log(` ${pc.green("\u2713")} rules/rsp-rules.md updated`);
|
|
1493
|
+
updated = true;
|
|
1494
|
+
}
|
|
1495
|
+
const agentsPath = "AGENTS.md";
|
|
1496
|
+
const projectName = await detectProjectName();
|
|
1497
|
+
const baseAgents = existsSync10(agentsPath) ? await readFile8(agentsPath, "utf-8") : `# ${projectName}
|
|
1498
|
+
|
|
1499
|
+
`;
|
|
1500
|
+
const nextAgents = upsertRspAgentsBlock(baseAgents);
|
|
1501
|
+
if (!existsSync10(agentsPath) || nextAgents.changed) {
|
|
1502
|
+
await writeFile7(agentsPath, nextAgents.content);
|
|
1503
|
+
actions.push("AGENTS.md managed block refreshed");
|
|
1504
|
+
if (!options.quiet)
|
|
1505
|
+
console.log(` ${pc.green("\u2713")} AGENTS.md managed block refreshed`);
|
|
1506
|
+
updated = true;
|
|
1507
|
+
}
|
|
1508
|
+
const specsIndexChanged = await buildSpecsIndex({ acquireLock: false, quiet: options.quiet });
|
|
1509
|
+
const archiveIndexChanged = await buildArchiveIndex({ acquireLock: false, quiet: options.quiet });
|
|
1510
|
+
if (specsIndexChanged || archiveIndexChanged) {
|
|
1511
|
+
actions.push("generated indexes rebuilt");
|
|
1512
|
+
updated = true;
|
|
1513
|
+
}
|
|
1514
|
+
if (!options.quiet && !updated) {
|
|
1515
|
+
console.log(` ${pc.dim("Already up to date.")}
|
|
1516
|
+
`);
|
|
1517
|
+
console.log(SKILL_REFRESH_HINT);
|
|
1518
|
+
} else if (!options.quiet) {
|
|
1519
|
+
console.log(` ${pc.green("Update complete.")}
|
|
1520
|
+
`);
|
|
1521
|
+
console.log(SKILL_REFRESH_HINT);
|
|
1522
|
+
}
|
|
1523
|
+
return actions;
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// src/commands/doctor.ts
|
|
1270
1528
|
async function runDoctor(options = {}) {
|
|
1529
|
+
const fixed = [];
|
|
1530
|
+
if (options.fix && existsSync11(RSP_DIR)) {
|
|
1531
|
+
fixed.push(...await updateProject({ quiet: Boolean(options.json) }));
|
|
1532
|
+
}
|
|
1271
1533
|
const checks = [];
|
|
1272
1534
|
const runtime = [];
|
|
1273
1535
|
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
|
|
1274
|
-
const rspRulesPath =
|
|
1275
|
-
const designPath =
|
|
1276
|
-
const specsIndexPath =
|
|
1277
|
-
const archivesIndexPath =
|
|
1278
|
-
reportCheck(checks, ".rsp exists",
|
|
1279
|
-
reportCheck(checks, "rules/rsp-rules.md exists",
|
|
1280
|
-
reportCheck(checks, "specs/design.md exists",
|
|
1281
|
-
reportCheck(checks, "specs/INDEX.md exists",
|
|
1282
|
-
reportCheck(checks, "archives/INDEX.md exists",
|
|
1536
|
+
const rspRulesPath = join11(RSP_DIR, "rules", "rsp-rules.md");
|
|
1537
|
+
const designPath = join11(RSP_DIR, "specs", "design.md");
|
|
1538
|
+
const specsIndexPath = join11(RSP_DIR, "specs", "INDEX.md");
|
|
1539
|
+
const archivesIndexPath = join11(RSP_DIR, "archives", "INDEX.md");
|
|
1540
|
+
reportCheck(checks, ".rsp exists", existsSync11(RSP_DIR), "Run: rsp init");
|
|
1541
|
+
reportCheck(checks, "rules/rsp-rules.md exists", existsSync11(rspRulesPath), "Run: rsp init");
|
|
1542
|
+
reportCheck(checks, "specs/design.md exists", existsSync11(designPath), "Run: rsp init");
|
|
1543
|
+
reportCheck(checks, "specs/INDEX.md exists", existsSync11(specsIndexPath), "Run: rsp update");
|
|
1544
|
+
reportCheck(checks, "archives/INDEX.md exists", existsSync11(archivesIndexPath), "Run: rsp update");
|
|
1283
1545
|
await checkAgents(checks, reportRuntime);
|
|
1284
|
-
await checkGeneratedIndex(checks, reportRuntime, specsIndexPath, "
|
|
1285
|
-
await checkGeneratedIndex(checks, reportRuntime, archivesIndexPath, "
|
|
1546
|
+
await checkGeneratedIndex(checks, reportRuntime, specsIndexPath, "specs", "specs/INDEX.md has generated-index metadata", "Run: rsp update");
|
|
1547
|
+
await checkGeneratedIndex(checks, reportRuntime, archivesIndexPath, "archives", "archives/INDEX.md has generated-index metadata", "Run: rsp update");
|
|
1286
1548
|
await checkArchiveNaming(checks, reportRuntime);
|
|
1287
1549
|
await checkConfigSemantics(checks, reportRuntime);
|
|
1288
1550
|
await checkActiveChangeConsistency(checks, reportRuntime);
|
|
1289
1551
|
const result = {
|
|
1290
1552
|
command: "doctor",
|
|
1291
1553
|
ok: checks.every((check) => check.status !== "issue"),
|
|
1554
|
+
fixed,
|
|
1292
1555
|
checks,
|
|
1293
1556
|
runtime,
|
|
1294
1557
|
summary: {
|
|
@@ -1302,6 +1565,13 @@ async function runDoctor(options = {}) {
|
|
|
1302
1565
|
console.log();
|
|
1303
1566
|
console.log(` ${pc.bold("RSP doctor")}`);
|
|
1304
1567
|
console.log();
|
|
1568
|
+
if (fixed.length > 0) {
|
|
1569
|
+
console.log(` ${pc.green("Fixed:")} ${fixed.join(", ")}`);
|
|
1570
|
+
console.log();
|
|
1571
|
+
} else if (options.fix) {
|
|
1572
|
+
console.log(` ${pc.dim("No safe fixes needed.")}`);
|
|
1573
|
+
console.log();
|
|
1574
|
+
}
|
|
1305
1575
|
for (const check of checks)
|
|
1306
1576
|
printDoctorCheck(check);
|
|
1307
1577
|
if (result.summary.issues === 0)
|
|
@@ -1337,13 +1607,13 @@ function printDoctorCheck(check) {
|
|
|
1337
1607
|
console.log(` ${pc.dim(check.hint)}`);
|
|
1338
1608
|
}
|
|
1339
1609
|
async function checkAgents(checks, reportRuntime) {
|
|
1340
|
-
if (!
|
|
1610
|
+
if (!existsSync11("AGENTS.md")) {
|
|
1341
1611
|
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing", hint: "Run: rsp init" });
|
|
1342
1612
|
return;
|
|
1343
1613
|
}
|
|
1344
1614
|
let agents;
|
|
1345
1615
|
try {
|
|
1346
|
-
agents = await
|
|
1616
|
+
agents = await readFile9("AGENTS.md", "utf-8");
|
|
1347
1617
|
} catch (error) {
|
|
1348
1618
|
reportRuntime({
|
|
1349
1619
|
code: "agents_read_failed",
|
|
@@ -1360,12 +1630,12 @@ async function checkAgents(checks, reportRuntime) {
|
|
|
1360
1630
|
}
|
|
1361
1631
|
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing managed RSP block", hint: "Run: rsp init" });
|
|
1362
1632
|
}
|
|
1363
|
-
async function checkGeneratedIndex(checks, reportRuntime, path,
|
|
1364
|
-
if (!
|
|
1633
|
+
async function checkGeneratedIndex(checks, reportRuntime, path, expectedIndexType, label, fixHint) {
|
|
1634
|
+
if (!existsSync11(path))
|
|
1365
1635
|
return;
|
|
1366
1636
|
let content;
|
|
1367
1637
|
try {
|
|
1368
|
-
content = await
|
|
1638
|
+
content = await readFile9(path, "utf-8");
|
|
1369
1639
|
} catch (error) {
|
|
1370
1640
|
reportRuntime({
|
|
1371
1641
|
code: "generated_index_read_failed",
|
|
@@ -1376,15 +1646,27 @@ async function checkGeneratedIndex(checks, reportRuntime, path, signature, label
|
|
|
1376
1646
|
checks.push({ status: "issue", label, message: `${label} could not be read`, hint: fixHint });
|
|
1377
1647
|
return;
|
|
1378
1648
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1649
|
+
try {
|
|
1650
|
+
const fm = parseFrontmatter(content);
|
|
1651
|
+
if (fm?.kind === "generated-index" && fm.index_type === expectedIndexType) {
|
|
1652
|
+
checks.push({ status: "ok", label });
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
reportRuntime({
|
|
1657
|
+
code: "generated_index_frontmatter_parse_failed",
|
|
1658
|
+
operation: "parseFrontmatter",
|
|
1659
|
+
path,
|
|
1660
|
+
message: toErrorMessage(error)
|
|
1661
|
+
});
|
|
1662
|
+
checks.push({ status: "issue", label, message: `${label} could not parse metadata`, hint: fixHint });
|
|
1381
1663
|
return;
|
|
1382
1664
|
}
|
|
1383
1665
|
checks.push({ status: "issue", label, hint: fixHint });
|
|
1384
1666
|
}
|
|
1385
1667
|
async function checkArchiveNaming(checks, reportRuntime) {
|
|
1386
|
-
const archivesDir =
|
|
1387
|
-
const archiveFiles =
|
|
1668
|
+
const archivesDir = join11(RSP_DIR, "archives");
|
|
1669
|
+
const archiveFiles = existsSync11(archivesDir) ? (await walkMarkdownFiles(archivesDir, { onError: reportRuntime })).filter((fp) => relative5(archivesDir, fp) !== "INDEX.md") : [];
|
|
1388
1670
|
const invalidFiles = archiveFiles.map((fp) => normalizeLogicalPath(relative5(archivesDir, fp))).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(basename5(fp)) === false);
|
|
1389
1671
|
if (invalidFiles.length === 0) {
|
|
1390
1672
|
checks.push({ status: "ok", label: "archived change files follow the date_name.md convention" });
|
|
@@ -1398,11 +1680,11 @@ async function checkArchiveNaming(checks, reportRuntime) {
|
|
|
1398
1680
|
});
|
|
1399
1681
|
}
|
|
1400
1682
|
async function checkConfigSemantics(checks, reportRuntime) {
|
|
1401
|
-
if (!
|
|
1683
|
+
if (!existsSync11(CONFIG_PATH))
|
|
1402
1684
|
return;
|
|
1403
1685
|
let content;
|
|
1404
1686
|
try {
|
|
1405
|
-
content = await
|
|
1687
|
+
content = await readFile9(CONFIG_PATH, "utf-8");
|
|
1406
1688
|
} catch (error) {
|
|
1407
1689
|
reportRuntime({
|
|
1408
1690
|
code: "config_read_failed",
|
|
@@ -1458,7 +1740,7 @@ function reportConfigListField(checks, parsed, key) {
|
|
|
1458
1740
|
});
|
|
1459
1741
|
}
|
|
1460
1742
|
async function checkActiveChangeConsistency(checks, reportRuntime) {
|
|
1461
|
-
const changeFiles =
|
|
1743
|
+
const changeFiles = existsSync11(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
|
|
1462
1744
|
const changeNames = new Set(changeFiles.map((fp) => changeNameFromPath(CHANGES_DIR, fp)));
|
|
1463
1745
|
const focusNames = await getFocusedChangeNames({ onError: reportRuntime });
|
|
1464
1746
|
const missingChangeFiles = [...focusNames].filter((name) => !changeNames.has(name));
|
|
@@ -1488,9 +1770,9 @@ async function checkActiveChangeConsistency(checks, reportRuntime) {
|
|
|
1488
1770
|
}
|
|
1489
1771
|
|
|
1490
1772
|
// src/commands/focus.ts
|
|
1491
|
-
import { existsSync as
|
|
1492
|
-
import { mkdir as
|
|
1493
|
-
import { dirname as dirname8, join as
|
|
1773
|
+
import { existsSync as existsSync12 } from "fs";
|
|
1774
|
+
import { mkdir as mkdir7, unlink as unlink3, writeFile as writeFile8 } from "fs/promises";
|
|
1775
|
+
import { dirname as dirname8, join as join12 } from "path";
|
|
1494
1776
|
async function focusChange(name) {
|
|
1495
1777
|
if (!name) {
|
|
1496
1778
|
console.error(` ${pc.red("Usage:")} rsp focus <name>`);
|
|
@@ -1501,16 +1783,16 @@ async function focusChange(name) {
|
|
|
1501
1783
|
process.exit(1);
|
|
1502
1784
|
}
|
|
1503
1785
|
guardRspInitialized();
|
|
1504
|
-
const changePath =
|
|
1505
|
-
if (!
|
|
1786
|
+
const changePath = join12(CHANGES_DIR, `${name}.md`);
|
|
1787
|
+
if (!existsSync12(changePath)) {
|
|
1506
1788
|
console.error(` ${pc.red("Change not found:")} .rsp/changes/${name}.md`);
|
|
1507
1789
|
process.exit(1);
|
|
1508
1790
|
}
|
|
1509
1791
|
return withRspLock("focus-change", async () => {
|
|
1510
|
-
await
|
|
1511
|
-
const focusEntry =
|
|
1512
|
-
await
|
|
1513
|
-
await
|
|
1792
|
+
await mkdir7(FOCUS_DIR, { recursive: true });
|
|
1793
|
+
const focusEntry = join12(FOCUS_DIR, name);
|
|
1794
|
+
await mkdir7(dirname8(focusEntry), { recursive: true });
|
|
1795
|
+
await writeFile8(focusEntry, "");
|
|
1514
1796
|
console.log(` ${pc.green("Focused:")} ${name}`);
|
|
1515
1797
|
console.log(` ${pc.dim("focus.d")} \u2192 ${name}`);
|
|
1516
1798
|
console.log();
|
|
@@ -1526,8 +1808,8 @@ async function unfocusChange(name) {
|
|
|
1526
1808
|
process.exit(1);
|
|
1527
1809
|
}
|
|
1528
1810
|
guardRspInitialized();
|
|
1529
|
-
const focusEntry =
|
|
1530
|
-
if (!
|
|
1811
|
+
const focusEntry = join12(FOCUS_DIR, name);
|
|
1812
|
+
if (!existsSync12(focusEntry)) {
|
|
1531
1813
|
console.error(` ${pc.red("Focus marker not found:")} .rsp/focus.d/${name}`);
|
|
1532
1814
|
process.exit(1);
|
|
1533
1815
|
}
|
|
@@ -1541,9 +1823,9 @@ async function unfocusChange(name) {
|
|
|
1541
1823
|
}
|
|
1542
1824
|
|
|
1543
1825
|
// src/commands/init.ts
|
|
1544
|
-
import { existsSync as
|
|
1545
|
-
import { mkdir as
|
|
1546
|
-
import { join as
|
|
1826
|
+
import { existsSync as existsSync13 } from "fs";
|
|
1827
|
+
import { mkdir as mkdir8, readFile as readFile10, writeFile as writeFile9 } from "fs/promises";
|
|
1828
|
+
import { join as join13 } from "path";
|
|
1547
1829
|
function generateConfigTemplate() {
|
|
1548
1830
|
const fmtList = (items, indent) => items.map((i) => `${" ".repeat(indent)}# - ${i}`).join("\n");
|
|
1549
1831
|
return `# RSP project configuration
|
|
@@ -1557,16 +1839,16 @@ ${fmtList(VALID_KINDS, 2)}
|
|
|
1557
1839
|
`;
|
|
1558
1840
|
}
|
|
1559
1841
|
async function ensureFile(path, content) {
|
|
1560
|
-
if (
|
|
1842
|
+
if (existsSync13(path))
|
|
1561
1843
|
return false;
|
|
1562
|
-
await
|
|
1844
|
+
await writeFile9(path, content);
|
|
1563
1845
|
return true;
|
|
1564
1846
|
}
|
|
1565
1847
|
function toTitle(projectName) {
|
|
1566
1848
|
return projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
|
|
1567
1849
|
}
|
|
1568
1850
|
async function initProject(args = {}) {
|
|
1569
|
-
const isNew = !
|
|
1851
|
+
const isNew = !existsSync13(RSP_DIR);
|
|
1570
1852
|
const projectName = await detectProjectName();
|
|
1571
1853
|
const mode = args.agentsMode ?? "managed";
|
|
1572
1854
|
if (mode !== "managed" && mode !== "print") {
|
|
@@ -1575,32 +1857,32 @@ async function initProject(args = {}) {
|
|
|
1575
1857
|
}
|
|
1576
1858
|
return withRspLock("init", async () => {
|
|
1577
1859
|
const dirs = [
|
|
1578
|
-
|
|
1579
|
-
|
|
1860
|
+
join13(RSP_DIR, "rules"),
|
|
1861
|
+
join13(RSP_DIR, "specs"),
|
|
1580
1862
|
CHANGES_DIR,
|
|
1581
1863
|
FOCUS_DIR,
|
|
1582
|
-
|
|
1864
|
+
join13(RSP_DIR, "archives")
|
|
1583
1865
|
];
|
|
1584
1866
|
for (const d of dirs)
|
|
1585
|
-
await
|
|
1867
|
+
await mkdir8(d, { recursive: true });
|
|
1586
1868
|
let created = false;
|
|
1587
|
-
const bundledRules = await
|
|
1588
|
-
created = await ensureFile(
|
|
1589
|
-
created = await ensureFile(
|
|
1590
|
-
const createdSpecsIndex = await ensureFile(
|
|
1869
|
+
const bundledRules = await readFile10(join13(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
|
|
1870
|
+
created = await ensureFile(join13(RSP_DIR, "rules", "rsp-rules.md"), bundledRules) || created;
|
|
1871
|
+
created = await ensureFile(join13(RSP_DIR, "config.yaml"), generateConfigTemplate()) || created;
|
|
1872
|
+
const createdSpecsIndex = await ensureFile(join13(RSP_DIR, "specs", "INDEX.md"), "# Specs Index\n\n_Additional project-level specs beyond `design.md`._\n");
|
|
1591
1873
|
created = createdSpecsIndex || created;
|
|
1592
|
-
const createdDesign = await ensureFile(
|
|
1874
|
+
const createdDesign = await ensureFile(join13(RSP_DIR, "specs", "design.md"), generateDesignContent(projectName));
|
|
1593
1875
|
created = createdDesign || created;
|
|
1594
|
-
const createdArchivesIndex = await ensureFile(
|
|
1876
|
+
const createdArchivesIndex = await ensureFile(join13(RSP_DIR, "archives", "INDEX.md"), "# Archive Index\n");
|
|
1595
1877
|
created = createdArchivesIndex || created;
|
|
1596
|
-
created = await ensureFile(
|
|
1597
|
-
created = await ensureFile(
|
|
1598
|
-
created = await ensureFile(
|
|
1878
|
+
created = await ensureFile(join13(CHANGES_DIR, ".gitkeep"), "") || created;
|
|
1879
|
+
created = await ensureFile(join13(FOCUS_DIR, ".gitkeep"), "") || created;
|
|
1880
|
+
created = await ensureFile(join13(RSP_DIR, ".gitignore"), "# Transient files that should not be committed\n.lock\n") || created;
|
|
1599
1881
|
if (args.withProjectRules)
|
|
1600
|
-
created = await ensureFile(
|
|
1882
|
+
created = await ensureFile(join13(RSP_DIR, "rules", "project-rules.md"), generateProjectRulesContent(projectName)) || created;
|
|
1601
1883
|
if (args.withProjectSetup) {
|
|
1602
|
-
const changePath =
|
|
1603
|
-
const focusMarker =
|
|
1884
|
+
const changePath = join13(CHANGES_DIR, "project-setup.md");
|
|
1885
|
+
const focusMarker = join13(FOCUS_DIR, "project-setup");
|
|
1604
1886
|
created = await ensureFile(changePath, generateChangeContent("project-setup")) || created;
|
|
1605
1887
|
created = await ensureFile(focusMarker, "") || created;
|
|
1606
1888
|
}
|
|
@@ -1611,24 +1893,24 @@ async function initProject(args = {}) {
|
|
|
1611
1893
|
const agentsPath = "AGENTS.md";
|
|
1612
1894
|
const title = toTitle(projectName);
|
|
1613
1895
|
let agentsUpdated = false;
|
|
1614
|
-
if (!
|
|
1615
|
-
await
|
|
1896
|
+
if (!existsSync13(agentsPath)) {
|
|
1897
|
+
await writeFile9(agentsPath, `# ${title}
|
|
1616
1898
|
|
|
1617
1899
|
${upsertRspAgentsBlock("").content}
|
|
1618
1900
|
`);
|
|
1619
1901
|
created = true;
|
|
1620
1902
|
agentsUpdated = true;
|
|
1621
1903
|
} else {
|
|
1622
|
-
const existing = await
|
|
1904
|
+
const existing = await readFile10(agentsPath, "utf-8");
|
|
1623
1905
|
const next = upsertRspAgentsBlock(existing);
|
|
1624
1906
|
if (next.changed) {
|
|
1625
|
-
await
|
|
1907
|
+
await writeFile9(agentsPath, next.content);
|
|
1626
1908
|
created = true;
|
|
1627
1909
|
agentsUpdated = true;
|
|
1628
1910
|
}
|
|
1629
1911
|
}
|
|
1630
1912
|
if (mode === "print") {
|
|
1631
|
-
const managed = await
|
|
1913
|
+
const managed = await readFile10(agentsPath, "utf-8");
|
|
1632
1914
|
console.log(`
|
|
1633
1915
|
${managed}
|
|
1634
1916
|
`);
|
|
@@ -1665,10 +1947,258 @@ ${managed}
|
|
|
1665
1947
|
});
|
|
1666
1948
|
}
|
|
1667
1949
|
|
|
1950
|
+
// src/commands/ready.ts
|
|
1951
|
+
import { existsSync as existsSync14 } from "fs";
|
|
1952
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
1953
|
+
import { join as join14 } from "path";
|
|
1954
|
+
async function showReady(name, options = {}) {
|
|
1955
|
+
if (!name) {
|
|
1956
|
+
console.error(` ${pc.red("Usage:")} rsp ready <name>`);
|
|
1957
|
+
process.exit(1);
|
|
1958
|
+
}
|
|
1959
|
+
if (!isValidChangeName(name)) {
|
|
1960
|
+
console.error(` ${pc.red("Error:")} change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
1961
|
+
process.exit(1);
|
|
1962
|
+
}
|
|
1963
|
+
guardRspInitialized();
|
|
1964
|
+
const srcPath = join14(CHANGES_DIR, `${name}.md`);
|
|
1965
|
+
if (!existsSync14(srcPath)) {
|
|
1966
|
+
console.error(` ${pc.red("Change not found:")} .rsp/changes/${name}.md`);
|
|
1967
|
+
process.exit(1);
|
|
1968
|
+
}
|
|
1969
|
+
const runtime = [];
|
|
1970
|
+
let content;
|
|
1971
|
+
try {
|
|
1972
|
+
content = await readFile11(srcPath, "utf-8");
|
|
1973
|
+
} catch {
|
|
1974
|
+
console.error(` ${pc.red("Error:")} unable to read .rsp/changes/${name}.md`);
|
|
1975
|
+
process.exit(1);
|
|
1976
|
+
}
|
|
1977
|
+
const readinessDetails = collectArchiveReadiness(content);
|
|
1978
|
+
const checklist = readinessDetails.warnings;
|
|
1979
|
+
const readiness = {
|
|
1980
|
+
incompleteTasks: readinessDetails.taskTodos.length,
|
|
1981
|
+
incompleteVerify: readinessDetails.verifyTodos.length,
|
|
1982
|
+
activeBlockers: readinessDetails.activeBlockers,
|
|
1983
|
+
missingScenarios: readinessDetails.missingScenarios,
|
|
1984
|
+
deterministic: readinessDetails.deterministic,
|
|
1985
|
+
semantic: readinessDetails.semantic,
|
|
1986
|
+
archiveReady: readinessDetails.archiveReady
|
|
1987
|
+
};
|
|
1988
|
+
const durableReview = buildDurableReviewGuidance(getReadyDurableReviewCandidateTargets());
|
|
1989
|
+
const result = {
|
|
1990
|
+
command: "ready",
|
|
1991
|
+
ok: true,
|
|
1992
|
+
change: name,
|
|
1993
|
+
path: normalizeLogicalPath(srcPath),
|
|
1994
|
+
readiness,
|
|
1995
|
+
durableReview,
|
|
1996
|
+
warnings: checklist,
|
|
1997
|
+
runtime
|
|
1998
|
+
};
|
|
1999
|
+
if (options.json) {
|
|
2000
|
+
emitJson(result);
|
|
2001
|
+
return result;
|
|
2002
|
+
}
|
|
2003
|
+
console.log();
|
|
2004
|
+
console.log(` ${pc.bold("Archive readiness for")} ${pc.cyan(name)}`);
|
|
2005
|
+
console.log();
|
|
2006
|
+
if (checklist.length === 0) {
|
|
2007
|
+
console.log(` ${pc.green("\u2713")} Ready to archive. No deterministic warnings found.
|
|
2008
|
+
`);
|
|
2009
|
+
} else {
|
|
2010
|
+
for (const line of checklist)
|
|
2011
|
+
console.log(` ${pc.yellow("\u26A0")} ${line}`);
|
|
2012
|
+
console.log();
|
|
2013
|
+
console.log(` ${pc.dim("Review the warnings above before treating this work as fully closed.")}`);
|
|
2014
|
+
console.log(` ${pc.dim("Run:")} rsp archive ${name}
|
|
2015
|
+
`);
|
|
2016
|
+
}
|
|
2017
|
+
console.log(` ${pc.dim("Deterministic readiness:")} ${readiness.deterministic === "pass" ? pc.green("pass") : pc.yellow("warnings")}`);
|
|
2018
|
+
console.log(` ${pc.dim("Semantic review:")} ${pc.yellow("needed")}`);
|
|
2019
|
+
console.log(` ${pc.dim("Archive ready:")} ${formatArchiveReady(readiness.archiveReady)}
|
|
2020
|
+
`);
|
|
2021
|
+
console.log(` ${pc.bold("Durable review:")}`);
|
|
2022
|
+
console.log(` ${pc.dim("Decision options:")} ${durableReview.decisions.join(" | ")}`);
|
|
2023
|
+
console.log(` ${pc.dim("Candidate targets:")} ${durableReview.candidateTargets.join(", ")}`);
|
|
2024
|
+
console.log(` ${pc.dim(durableReview.note)}
|
|
2025
|
+
`);
|
|
2026
|
+
return result;
|
|
2027
|
+
}
|
|
2028
|
+
function getReadyDurableReviewCandidateTargets() {
|
|
2029
|
+
return getDurableReviewCandidateTargets({
|
|
2030
|
+
projectRulesExists: existsSync14(join14(RSP_DIR, "rules", "project-rules.md"))
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
function formatArchiveReady(value) {
|
|
2034
|
+
switch (value) {
|
|
2035
|
+
case "yes":
|
|
2036
|
+
return pc.green("yes");
|
|
2037
|
+
case "no":
|
|
2038
|
+
return pc.yellow("no");
|
|
2039
|
+
case "judgment":
|
|
2040
|
+
return pc.yellow("judgment");
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
// src/commands/show.ts
|
|
2045
|
+
import { existsSync as existsSync15 } from "fs";
|
|
2046
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
2047
|
+
import { join as join15 } from "path";
|
|
2048
|
+
function exitShowError(error, options) {
|
|
2049
|
+
const nextActions = error.code === "no_focused_change" ? ["Run: rsp status", "Run: rsp focus <name>", "Or run: rsp create <name>"] : [];
|
|
2050
|
+
if (options.json) {
|
|
2051
|
+
emitJson({
|
|
2052
|
+
command: "show",
|
|
2053
|
+
ok: false,
|
|
2054
|
+
change: null,
|
|
2055
|
+
contextPaths: [],
|
|
2056
|
+
runtime: [],
|
|
2057
|
+
nextActions,
|
|
2058
|
+
error
|
|
2059
|
+
});
|
|
2060
|
+
} else {
|
|
2061
|
+
console.error(` ${pc.red("Error:")} ${error.message}`);
|
|
2062
|
+
for (const action of nextActions)
|
|
2063
|
+
console.error(` ${pc.dim(action)}`);
|
|
2064
|
+
}
|
|
2065
|
+
process.exit(1);
|
|
2066
|
+
}
|
|
2067
|
+
async function showChange(nameOrFocused, options = {}) {
|
|
2068
|
+
const runtime = [];
|
|
2069
|
+
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
|
|
2070
|
+
guardRspInitialized();
|
|
2071
|
+
let name;
|
|
2072
|
+
if (options.focused) {
|
|
2073
|
+
const focused = await getFocusedChangeNames({ onError: reportRuntime });
|
|
2074
|
+
if (focused.size === 0) {
|
|
2075
|
+
exitShowError({ code: "no_focused_change", message: "no focused change exists" }, options);
|
|
2076
|
+
}
|
|
2077
|
+
if (focused.size > 1) {
|
|
2078
|
+
exitShowError({
|
|
2079
|
+
code: "multiple_focused_changes",
|
|
2080
|
+
message: `multiple focused changes exist (${[...focused].join(", ")}). Specify a name or focus exactly one change.`
|
|
2081
|
+
}, options);
|
|
2082
|
+
}
|
|
2083
|
+
name = [...focused][0];
|
|
2084
|
+
} else if (nameOrFocused) {
|
|
2085
|
+
if (!isValidChangeName(nameOrFocused)) {
|
|
2086
|
+
exitShowError({
|
|
2087
|
+
code: "invalid_change_name",
|
|
2088
|
+
message: "change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)"
|
|
2089
|
+
}, options);
|
|
2090
|
+
}
|
|
2091
|
+
name = nameOrFocused;
|
|
2092
|
+
} else {
|
|
2093
|
+
exitShowError({ code: "missing_change_name", message: "Usage: rsp show <name|--focused> [--json] [--verbose]" }, options);
|
|
2094
|
+
}
|
|
2095
|
+
const srcPath = join15(CHANGES_DIR, `${name}.md`);
|
|
2096
|
+
if (!existsSync15(srcPath)) {
|
|
2097
|
+
exitShowError({ code: "change_not_found", message: `.rsp/changes/${name}.md not found` }, options);
|
|
2098
|
+
}
|
|
2099
|
+
let content;
|
|
2100
|
+
try {
|
|
2101
|
+
content = await readFile12(srcPath, "utf-8");
|
|
2102
|
+
} catch {
|
|
2103
|
+
exitShowError({ code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, options);
|
|
2104
|
+
}
|
|
2105
|
+
let kind = "\u2014";
|
|
2106
|
+
try {
|
|
2107
|
+
const fm = parseFrontmatter(content);
|
|
2108
|
+
kind = fm?.kind ? String(fm.kind) : "\u2014";
|
|
2109
|
+
} catch (error) {
|
|
2110
|
+
kind = "(invalid)";
|
|
2111
|
+
reportRuntime({
|
|
2112
|
+
code: "frontmatter_parse_failed",
|
|
2113
|
+
operation: "parseFrontmatter",
|
|
2114
|
+
path: srcPath,
|
|
2115
|
+
message: toErrorMessage(error)
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
const focusedSet = await getFocusedChangeNames({ onError: reportRuntime });
|
|
2119
|
+
const isFocused = focusedSet.has(name);
|
|
2120
|
+
const cb = countCheckboxes(content);
|
|
2121
|
+
const blockers = hasMeaningfulBlockers(content);
|
|
2122
|
+
const scenarios = parseScenarios(content);
|
|
2123
|
+
const readinessDetails = collectArchiveReadiness(content);
|
|
2124
|
+
const readiness = {
|
|
2125
|
+
incompleteTasks: readinessDetails.taskTodos.length,
|
|
2126
|
+
incompleteVerify: readinessDetails.verifyTodos.length,
|
|
2127
|
+
activeBlockers: readinessDetails.activeBlockers,
|
|
2128
|
+
missingScenarios: readinessDetails.missingScenarios,
|
|
2129
|
+
deterministic: readinessDetails.deterministic,
|
|
2130
|
+
semantic: readinessDetails.semantic,
|
|
2131
|
+
archiveReady: readinessDetails.archiveReady
|
|
2132
|
+
};
|
|
2133
|
+
const contextPaths = getDurableReviewCandidateTargets({
|
|
2134
|
+
projectRulesExists: existsSync15(join15(RSP_DIR, "rules", "project-rules.md"))
|
|
2135
|
+
});
|
|
2136
|
+
const durableReview = buildDurableReviewGuidance(contextPaths);
|
|
2137
|
+
const result = {
|
|
2138
|
+
command: "show",
|
|
2139
|
+
ok: true,
|
|
2140
|
+
change: {
|
|
2141
|
+
name,
|
|
2142
|
+
path: normalizeLogicalPath(srcPath),
|
|
2143
|
+
kind,
|
|
2144
|
+
isFocused,
|
|
2145
|
+
progress: { done: cb.done, total: cb.total },
|
|
2146
|
+
blockers,
|
|
2147
|
+
scenarioCount: scenarios.length,
|
|
2148
|
+
readiness
|
|
2149
|
+
},
|
|
2150
|
+
contextPaths,
|
|
2151
|
+
durableReview,
|
|
2152
|
+
runtime
|
|
2153
|
+
};
|
|
2154
|
+
if (options.json) {
|
|
2155
|
+
emitJson(result);
|
|
2156
|
+
return result;
|
|
2157
|
+
}
|
|
2158
|
+
console.log();
|
|
2159
|
+
console.log(` ${pc.bold("Change:")} ${pc.cyan(name)}`);
|
|
2160
|
+
console.log(` ${pc.dim("Path:")} ${normalizeLogicalPath(srcPath)}`);
|
|
2161
|
+
console.log(` ${pc.dim("Kind:")} ${kind}`);
|
|
2162
|
+
console.log(` ${pc.dim("Focused:")} ${isFocused ? pc.green("yes") : pc.dim("no")}`);
|
|
2163
|
+
console.log(` ${pc.dim("Progress:")} ${cb.done}/${cb.total}`);
|
|
2164
|
+
console.log(` ${pc.dim("Blockers:")} ${blockers ? pc.yellow("yes") : pc.green("no")}`);
|
|
2165
|
+
console.log(` ${pc.dim("Scenarios:")} ${scenarios.length}`);
|
|
2166
|
+
console.log();
|
|
2167
|
+
console.log(` ${pc.bold("Readiness:")}`);
|
|
2168
|
+
console.log(` ${pc.dim("Incomplete tasks:")} ${readiness.incompleteTasks > 0 ? pc.yellow(String(readiness.incompleteTasks)) : pc.green("0")}`);
|
|
2169
|
+
console.log(` ${pc.dim("Incomplete verify:")} ${readiness.incompleteVerify > 0 ? pc.yellow(String(readiness.incompleteVerify)) : pc.green("0")}`);
|
|
2170
|
+
console.log(` ${pc.dim("Active blockers:")} ${readiness.activeBlockers ? pc.yellow("yes") : pc.green("no")}`);
|
|
2171
|
+
console.log(` ${pc.dim("Missing scenarios:")} ${readiness.missingScenarios ? pc.yellow("yes") : pc.green("no")}`);
|
|
2172
|
+
console.log(` ${pc.dim("Deterministic:")} ${readiness.deterministic === "pass" ? pc.green("pass") : pc.yellow("warnings")}`);
|
|
2173
|
+
console.log(` ${pc.dim("Semantic review:")} ${pc.yellow("needed")}`);
|
|
2174
|
+
console.log(` ${pc.dim("Archive ready:")} ${formatArchiveReady2(readiness.archiveReady)}`);
|
|
2175
|
+
console.log();
|
|
2176
|
+
console.log(` ${pc.bold("Context paths:")}`);
|
|
2177
|
+
for (const cp of contextPaths)
|
|
2178
|
+
console.log(` ${pc.dim(cp)}`);
|
|
2179
|
+
console.log();
|
|
2180
|
+
console.log(` ${pc.bold("Durable review:")}`);
|
|
2181
|
+
console.log(` ${pc.dim("Decision options:")} ${durableReview.decisions.join(" | ")}`);
|
|
2182
|
+
console.log(` ${pc.dim("Candidate targets:")} ${durableReview.candidateTargets.join(", ")}`);
|
|
2183
|
+
console.log(` ${pc.dim(durableReview.note)}`);
|
|
2184
|
+
console.log();
|
|
2185
|
+
return result;
|
|
2186
|
+
}
|
|
2187
|
+
function formatArchiveReady2(value) {
|
|
2188
|
+
switch (value) {
|
|
2189
|
+
case "yes":
|
|
2190
|
+
return pc.green("yes");
|
|
2191
|
+
case "no":
|
|
2192
|
+
return pc.yellow("no");
|
|
2193
|
+
case "judgment":
|
|
2194
|
+
return pc.yellow("judgment");
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
|
|
1668
2198
|
// src/commands/status.ts
|
|
1669
|
-
import { existsSync as
|
|
1670
|
-
import { readFile as
|
|
1671
|
-
import { join as
|
|
2199
|
+
import { existsSync as existsSync16 } from "fs";
|
|
2200
|
+
import { readFile as readFile13, stat } from "fs/promises";
|
|
2201
|
+
import { join as join16 } from "path";
|
|
1672
2202
|
var COL_CHANGE = 32;
|
|
1673
2203
|
var COL_KIND = 10;
|
|
1674
2204
|
var COL_AGE = 7;
|
|
@@ -1677,7 +2207,7 @@ async function showStatus(options = {}, runOptions = {}) {
|
|
|
1677
2207
|
const runtime = [];
|
|
1678
2208
|
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(runOptions.verbose) && !runOptions.json);
|
|
1679
2209
|
const focusedSet = await getFocusedChangeNames({ onError: reportRuntime });
|
|
1680
|
-
const changeFiles =
|
|
2210
|
+
const changeFiles = existsSync16(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
|
|
1681
2211
|
const changeMap = {};
|
|
1682
2212
|
for (const fp of changeFiles)
|
|
1683
2213
|
changeMap[changeNameFromPath(CHANGES_DIR, fp)] = fp;
|
|
@@ -1695,7 +2225,7 @@ async function showStatus(options = {}, runOptions = {}) {
|
|
|
1695
2225
|
let total = 0;
|
|
1696
2226
|
if (fp) {
|
|
1697
2227
|
try {
|
|
1698
|
-
const content = await
|
|
2228
|
+
const content = await readFile13(fp, "utf-8");
|
|
1699
2229
|
try {
|
|
1700
2230
|
const fm = parseFrontmatter(content);
|
|
1701
2231
|
kind = fm?.kind ? String(fm.kind) : "\u2014";
|
|
@@ -1737,7 +2267,7 @@ async function showStatus(options = {}, runOptions = {}) {
|
|
|
1737
2267
|
reportRuntime({
|
|
1738
2268
|
code: "focused_change_missing",
|
|
1739
2269
|
operation: "resolveChange",
|
|
1740
|
-
path:
|
|
2270
|
+
path: join16(CHANGES_DIR, `${name}.md`),
|
|
1741
2271
|
message: "focus marker points to a missing change file"
|
|
1742
2272
|
});
|
|
1743
2273
|
}
|
|
@@ -1777,6 +2307,7 @@ async function showStatus(options = {}, runOptions = {}) {
|
|
|
1777
2307
|
return true;
|
|
1778
2308
|
});
|
|
1779
2309
|
const blockedCount = filteredOutputRecords.filter((r) => r.isBlocked).length;
|
|
2310
|
+
const nextActions = buildStatusNextActions(focusedSet.size, Object.keys(changeMap).sort(), Boolean(options.focused || options.blocked || typeof options.stale === "number"));
|
|
1780
2311
|
const statusResult = {
|
|
1781
2312
|
command: "status",
|
|
1782
2313
|
ok: true,
|
|
@@ -1792,6 +2323,7 @@ async function showStatus(options = {}, runOptions = {}) {
|
|
|
1792
2323
|
focused: filteredOutputRecords.filter((r) => r.isFocused).length,
|
|
1793
2324
|
blocked: blockedCount
|
|
1794
2325
|
},
|
|
2326
|
+
nextActions,
|
|
1795
2327
|
archiveTrend: await readArchiveTrend(runtime, runOptions),
|
|
1796
2328
|
runtime
|
|
1797
2329
|
};
|
|
@@ -1806,7 +2338,9 @@ async function showStatus(options = {}, runOptions = {}) {
|
|
|
1806
2338
|
console.log(` ${pc.cyan("Focused:")} ${[...focusedSet].join(", ")}`);
|
|
1807
2339
|
console.log();
|
|
1808
2340
|
} else {
|
|
1809
|
-
console.log(` ${pc.dim("No focused change.")}
|
|
2341
|
+
console.log(` ${pc.dim("No focused change.")}`);
|
|
2342
|
+
for (const action of nextActions)
|
|
2343
|
+
console.log(` ${pc.dim(action)}`);
|
|
1810
2344
|
console.log();
|
|
1811
2345
|
}
|
|
1812
2346
|
if (allNames.length === 0) {
|
|
@@ -1843,12 +2377,32 @@ async function showStatus(options = {}, runOptions = {}) {
|
|
|
1843
2377
|
}
|
|
1844
2378
|
return statusResult;
|
|
1845
2379
|
}
|
|
2380
|
+
function buildStatusNextActions(focusedCount, openChanges, filtered) {
|
|
2381
|
+
if (focusedCount > 0)
|
|
2382
|
+
return [];
|
|
2383
|
+
if (openChanges.length === 0)
|
|
2384
|
+
return ["Run: rsp create <name>"];
|
|
2385
|
+
if (filtered) {
|
|
2386
|
+
return [
|
|
2387
|
+
"Run: rsp status",
|
|
2388
|
+
"Run: rsp focus <name>",
|
|
2389
|
+
"Or run: rsp create <name>"
|
|
2390
|
+
];
|
|
2391
|
+
}
|
|
2392
|
+
const firstChanges = openChanges.slice(0, 3).join(", ");
|
|
2393
|
+
const extra = openChanges.length > 3 ? ` (+${openChanges.length - 3} more)` : "";
|
|
2394
|
+
return [
|
|
2395
|
+
`Open changes: ${firstChanges}${extra}`,
|
|
2396
|
+
`Run: rsp focus ${openChanges[0]}`,
|
|
2397
|
+
"Or run: rsp create <name>"
|
|
2398
|
+
];
|
|
2399
|
+
}
|
|
1846
2400
|
async function readArchiveTrend(runtime, runOptions) {
|
|
1847
|
-
const indexPath =
|
|
1848
|
-
if (!
|
|
2401
|
+
const indexPath = join16(ARCHIVES_DIR, "INDEX.md");
|
|
2402
|
+
if (!existsSync16(indexPath))
|
|
1849
2403
|
return [];
|
|
1850
2404
|
try {
|
|
1851
|
-
const content = await
|
|
2405
|
+
const content = await readFile13(indexPath, "utf-8");
|
|
1852
2406
|
const dateRe = /^\| (\d{4}-\d{2})-\d{2} \|/gm;
|
|
1853
2407
|
const counts = {};
|
|
1854
2408
|
let match;
|
|
@@ -1879,53 +2433,6 @@ function printArchiveTrend(trend) {
|
|
|
1879
2433
|
console.log();
|
|
1880
2434
|
}
|
|
1881
2435
|
|
|
1882
|
-
// src/commands/update.ts
|
|
1883
|
-
import { existsSync as existsSync14 } from "fs";
|
|
1884
|
-
import { mkdir as mkdir8, readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
|
|
1885
|
-
import { join as join14 } from "path";
|
|
1886
|
-
var SKILL_REFRESH_HINT = " Note: if you use the published RSP skill, refresh it too:\n npx skills add oevery/rsp\n";
|
|
1887
|
-
async function updateProject() {
|
|
1888
|
-
if (!existsSync14(RSP_DIR)) {
|
|
1889
|
-
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
1890
|
-
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
1891
|
-
process.exit(1);
|
|
1892
|
-
}
|
|
1893
|
-
return withRspLock("update", async () => {
|
|
1894
|
-
let updated = false;
|
|
1895
|
-
const bundledRules = await readFile11(join14(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
|
|
1896
|
-
const rulesPath = join14(RSP_DIR, "rules", "rsp-rules.md");
|
|
1897
|
-
await mkdir8(join14(RSP_DIR, "rules"), { recursive: true });
|
|
1898
|
-
const existingRules = existsSync14(rulesPath) ? await readFile11(rulesPath, "utf-8") : null;
|
|
1899
|
-
if (existingRules !== bundledRules) {
|
|
1900
|
-
await writeFile9(rulesPath, bundledRules);
|
|
1901
|
-
console.log(` ${pc.green("\u2713")} rules/rsp-rules.md updated`);
|
|
1902
|
-
updated = true;
|
|
1903
|
-
}
|
|
1904
|
-
const agentsPath = "AGENTS.md";
|
|
1905
|
-
const projectName = await detectProjectName();
|
|
1906
|
-
const baseAgents = existsSync14(agentsPath) ? await readFile11(agentsPath, "utf-8") : `# ${projectName}
|
|
1907
|
-
|
|
1908
|
-
`;
|
|
1909
|
-
const nextAgents = upsertRspAgentsBlock(baseAgents);
|
|
1910
|
-
if (!existsSync14(agentsPath) || nextAgents.changed) {
|
|
1911
|
-
await writeFile9(agentsPath, nextAgents.content);
|
|
1912
|
-
console.log(` ${pc.green("\u2713")} AGENTS.md managed block refreshed`);
|
|
1913
|
-
updated = true;
|
|
1914
|
-
}
|
|
1915
|
-
await buildSpecsIndex({ acquireLock: false });
|
|
1916
|
-
await buildArchiveIndex({ acquireLock: false });
|
|
1917
|
-
if (!updated) {
|
|
1918
|
-
console.log(` ${pc.dim("Already up to date.")}
|
|
1919
|
-
`);
|
|
1920
|
-
console.log(SKILL_REFRESH_HINT);
|
|
1921
|
-
} else {
|
|
1922
|
-
console.log(` ${pc.green("Update complete.")}
|
|
1923
|
-
`);
|
|
1924
|
-
console.log(SKILL_REFRESH_HINT);
|
|
1925
|
-
}
|
|
1926
|
-
});
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
2436
|
// src/cli.ts
|
|
1930
2437
|
var initCommand = defineCommand({
|
|
1931
2438
|
meta: {
|
|
@@ -1971,11 +2478,16 @@ var createCommand = defineCommand({
|
|
|
1971
2478
|
kind: {
|
|
1972
2479
|
type: "string",
|
|
1973
2480
|
description: "Optional kind for a kind-aware template (feature, fix, refactor, docs, ops, research)"
|
|
2481
|
+
},
|
|
2482
|
+
lite: {
|
|
2483
|
+
type: "boolean",
|
|
2484
|
+
description: "Use a shorter change template while keeping the required RSP sections",
|
|
2485
|
+
default: false
|
|
1974
2486
|
}
|
|
1975
2487
|
},
|
|
1976
2488
|
async run({ args }) {
|
|
1977
2489
|
const summary = Array.isArray(args._) && args._.length > 1 ? args._.slice(1).join(" ") : "";
|
|
1978
|
-
await createChange(args.name, summary, args.kind);
|
|
2490
|
+
await createChange(args.name, summary, args.kind, { lite: Boolean(args.lite) });
|
|
1979
2491
|
}
|
|
1980
2492
|
});
|
|
1981
2493
|
var addRulesCommand = defineCommand({
|
|
@@ -2026,14 +2538,19 @@ var archiveCommand = defineCommand({
|
|
|
2026
2538
|
description: "Archive an open change into .rsp/archives/"
|
|
2027
2539
|
},
|
|
2028
2540
|
args: {
|
|
2029
|
-
name: {
|
|
2541
|
+
"name": {
|
|
2030
2542
|
type: "positional",
|
|
2031
2543
|
description: "Change name",
|
|
2032
2544
|
required: true
|
|
2545
|
+
},
|
|
2546
|
+
"dry-run": {
|
|
2547
|
+
type: "boolean",
|
|
2548
|
+
description: "Preview archive readiness without moving the change",
|
|
2549
|
+
default: false
|
|
2033
2550
|
}
|
|
2034
2551
|
},
|
|
2035
2552
|
async run({ args }) {
|
|
2036
|
-
await archiveChange(args.name);
|
|
2553
|
+
await archiveChange(args.name, { dryRun: Boolean(args["dry-run"]) });
|
|
2037
2554
|
}
|
|
2038
2555
|
});
|
|
2039
2556
|
var focusCommand = defineCommand({
|
|
@@ -2128,9 +2645,14 @@ var statusCommand = defineCommand({
|
|
|
2128
2645
|
var checkCommand = defineCommand({
|
|
2129
2646
|
meta: {
|
|
2130
2647
|
name: "check",
|
|
2131
|
-
description: "Validate changes (frontmatter, sections, deltas, scenarios)"
|
|
2648
|
+
description: "Validate changes (frontmatter, sections, template text, deltas, scenarios)"
|
|
2132
2649
|
},
|
|
2133
2650
|
args: {
|
|
2651
|
+
focused: {
|
|
2652
|
+
type: "boolean",
|
|
2653
|
+
description: "Only validate currently focused changes",
|
|
2654
|
+
default: false
|
|
2655
|
+
},
|
|
2134
2656
|
json: {
|
|
2135
2657
|
type: "boolean",
|
|
2136
2658
|
description: "Print machine-readable JSON output",
|
|
@@ -2143,7 +2665,11 @@ var checkCommand = defineCommand({
|
|
|
2143
2665
|
}
|
|
2144
2666
|
},
|
|
2145
2667
|
async run({ args }) {
|
|
2146
|
-
const result = await runCheck({
|
|
2668
|
+
const result = await runCheck({
|
|
2669
|
+
focused: Boolean(args.focused),
|
|
2670
|
+
json: Boolean(args.json),
|
|
2671
|
+
verbose: Boolean(args.verbose)
|
|
2672
|
+
});
|
|
2147
2673
|
if (!result.ok)
|
|
2148
2674
|
process.exit(1);
|
|
2149
2675
|
}
|
|
@@ -2172,14 +2698,80 @@ var doctorCommand = defineCommand({
|
|
|
2172
2698
|
type: "boolean",
|
|
2173
2699
|
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
2174
2700
|
default: false
|
|
2701
|
+
},
|
|
2702
|
+
fix: {
|
|
2703
|
+
type: "boolean",
|
|
2704
|
+
description: "Run safe deterministic repairs before reporting diagnostics",
|
|
2705
|
+
default: false
|
|
2175
2706
|
}
|
|
2176
2707
|
},
|
|
2177
2708
|
async run({ args }) {
|
|
2178
|
-
const result = await runDoctor({ json: Boolean(args.json), verbose: Boolean(args.verbose) });
|
|
2709
|
+
const result = await runDoctor({ json: Boolean(args.json), verbose: Boolean(args.verbose), fix: Boolean(args.fix) });
|
|
2179
2710
|
if (!result.ok)
|
|
2180
2711
|
process.exit(1);
|
|
2181
2712
|
}
|
|
2182
2713
|
});
|
|
2714
|
+
var readyCommand = defineCommand({
|
|
2715
|
+
meta: {
|
|
2716
|
+
name: "ready",
|
|
2717
|
+
description: "Preview archive readiness for a change without moving it"
|
|
2718
|
+
},
|
|
2719
|
+
args: {
|
|
2720
|
+
name: {
|
|
2721
|
+
type: "positional",
|
|
2722
|
+
description: "Change name",
|
|
2723
|
+
required: true
|
|
2724
|
+
},
|
|
2725
|
+
json: {
|
|
2726
|
+
type: "boolean",
|
|
2727
|
+
description: "Print machine-readable JSON output",
|
|
2728
|
+
default: false
|
|
2729
|
+
},
|
|
2730
|
+
verbose: {
|
|
2731
|
+
type: "boolean",
|
|
2732
|
+
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
2733
|
+
default: false
|
|
2734
|
+
}
|
|
2735
|
+
},
|
|
2736
|
+
async run({ args }) {
|
|
2737
|
+
await showReady(args.name, { json: Boolean(args.json), verbose: Boolean(args.verbose) });
|
|
2738
|
+
}
|
|
2739
|
+
});
|
|
2740
|
+
var showCommand = defineCommand({
|
|
2741
|
+
meta: {
|
|
2742
|
+
name: "show",
|
|
2743
|
+
description: "Show change context with readiness signals and relevant paths"
|
|
2744
|
+
},
|
|
2745
|
+
args: {
|
|
2746
|
+
name: {
|
|
2747
|
+
type: "positional",
|
|
2748
|
+
description: "Change name (use --focused instead to show the currently focused change)",
|
|
2749
|
+
required: false
|
|
2750
|
+
},
|
|
2751
|
+
focused: {
|
|
2752
|
+
type: "boolean",
|
|
2753
|
+
description: "Show the currently focused change",
|
|
2754
|
+
default: false
|
|
2755
|
+
},
|
|
2756
|
+
json: {
|
|
2757
|
+
type: "boolean",
|
|
2758
|
+
description: "Print machine-readable JSON output",
|
|
2759
|
+
default: false
|
|
2760
|
+
},
|
|
2761
|
+
verbose: {
|
|
2762
|
+
type: "boolean",
|
|
2763
|
+
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
2764
|
+
default: false
|
|
2765
|
+
}
|
|
2766
|
+
},
|
|
2767
|
+
async run({ args }) {
|
|
2768
|
+
await showChange(args.name, {
|
|
2769
|
+
focused: Boolean(args.focused),
|
|
2770
|
+
json: Boolean(args.json),
|
|
2771
|
+
verbose: Boolean(args.verbose)
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
});
|
|
2183
2775
|
async function runCli(rawArgs = process.argv.slice(2)) {
|
|
2184
2776
|
const version = await getVersion();
|
|
2185
2777
|
const main = defineCommand({
|
|
@@ -2195,6 +2787,8 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
2195
2787
|
focus: focusCommand,
|
|
2196
2788
|
unfocus: unfocusCommand,
|
|
2197
2789
|
archive: archiveCommand,
|
|
2790
|
+
ready: readyCommand,
|
|
2791
|
+
show: showCommand,
|
|
2198
2792
|
status: statusCommand,
|
|
2199
2793
|
check: checkCommand,
|
|
2200
2794
|
update: updateCommand,
|