@oevery/rsp 2.0.2 → 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 +30 -4
- package/README.zh-CN.md +30 -4
- package/bin/rsp.mjs +0 -0
- package/dist/cli.mjs +863 -204
- package/package.json +1 -1
- package/rules/rsp-rules.md +20 -18
- package/skills/rsp/SKILL.md +73 -72
package/dist/cli.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import pc from "picocolors";
|
|
|
19
19
|
|
|
20
20
|
// src/core/helpers.ts
|
|
21
21
|
import { existsSync } from "fs";
|
|
22
|
-
import { readdir, readFile, rmdir
|
|
22
|
+
import { readdir, readFile, rmdir } from "fs/promises";
|
|
23
23
|
import { basename, dirname, join, relative, sep } from "path";
|
|
24
24
|
import { parse } from "yaml";
|
|
25
25
|
|
|
@@ -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 `---
|
|
@@ -166,6 +166,7 @@ kind: ops
|
|
|
166
166
|
|
|
167
167
|
## Tasks
|
|
168
168
|
- [ ] Review the repository structure, entrypoints, and primary outputs
|
|
169
|
+
- [ ] Review AGENTS.md and confirm the RSP entry points to the right project files
|
|
169
170
|
- [ ] Fill .rsp/specs/design.md with durable architecture facts
|
|
170
171
|
- [ ] Add .rsp/rules/project-rules.md if stable local rules or validation steps exist
|
|
171
172
|
|
|
@@ -183,6 +184,8 @@ kind: ops
|
|
|
183
184
|
`;
|
|
184
185
|
}
|
|
185
186
|
const frontmatterKind = kind ?? "<choose: feature | fix | refactor | docs | ops | research>";
|
|
187
|
+
if (options.lite)
|
|
188
|
+
return generateLiteChangeContent(name, proposalSummary, frontmatterKind);
|
|
186
189
|
const template = getChangeTemplateByKind(kind);
|
|
187
190
|
return `---
|
|
188
191
|
kind: "${frontmatterKind}"
|
|
@@ -209,12 +212,15 @@ ${template.acceptanceSection}
|
|
|
209
212
|
- Approach:
|
|
210
213
|
- ${template.approach}
|
|
211
214
|
- Affected areas:
|
|
212
|
-
- ${template.
|
|
215
|
+
- ${template.affectedArea1}
|
|
216
|
+
- ${template.affectedArea2}
|
|
213
217
|
- Constraints:
|
|
214
218
|
- ${template.constraints}
|
|
215
219
|
|
|
216
220
|
## Tasks
|
|
221
|
+
- [ ] Finalize the proposal, spec, and design details for this change
|
|
217
222
|
- [ ] ${template.task}
|
|
223
|
+
- [ ] Verify the result and update any required durable docs or rules
|
|
218
224
|
|
|
219
225
|
## Verify
|
|
220
226
|
- Automated:
|
|
@@ -225,67 +231,168 @@ ${template.acceptanceSection}
|
|
|
225
231
|
- [ ] Decide whether this change produced durable knowledge that belongs in \`.rsp/specs/\` or \`.rsp/rules/\`
|
|
226
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
|
|
227
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
|
+
|
|
228
286
|
## Blockers
|
|
229
287
|
- none
|
|
230
288
|
`;
|
|
231
289
|
}
|
|
232
290
|
function getChangeTemplateByKind(kind) {
|
|
233
291
|
switch (kind) {
|
|
292
|
+
case "feature":
|
|
293
|
+
return {
|
|
294
|
+
why: "<what user need or capability gap this addresses>",
|
|
295
|
+
scope: "<what new behavior or capability will be delivered>",
|
|
296
|
+
nonGoals: "<what related capabilities are explicitly out of scope>",
|
|
297
|
+
specSection: "<!-- Describe observable behavior and requirements. Implementation notes belong in ## Design. -->\n### ADDED\n- Requirement: <new user-facing behavior>\n - <verifiable requirement for the new capability>",
|
|
298
|
+
acceptanceSection: "#### Scenario: user exercises the new capability\n- GIVEN <context>\n- WHEN <user action>\n- THEN <expected new behavior>",
|
|
299
|
+
approach: "<how the new capability will be implemented>",
|
|
300
|
+
affectedArea1: "<concrete file path, directory, module, or subsystem 1>",
|
|
301
|
+
affectedArea2: "<concrete file path, directory, module, or subsystem 2 if needed>",
|
|
302
|
+
constraints: "<behavior, compatibility, performance, safety, or scope constraint that must hold>",
|
|
303
|
+
task: "<implement the new behavior and its acceptance scenario in the affected areas>",
|
|
304
|
+
automatedVerify: "<exact test, lint, build, or check command for the new behavior>",
|
|
305
|
+
manualVerify: "<exact end-to-end user scenario to validate the new capability>"
|
|
306
|
+
};
|
|
307
|
+
case "fix":
|
|
308
|
+
return {
|
|
309
|
+
why: "<what current behavior is wrong or broken>",
|
|
310
|
+
scope: "<what specific defect is being corrected>",
|
|
311
|
+
nonGoals: "<what related but separate issues are out of scope>",
|
|
312
|
+
specSection: "<!-- Describe expected correct behavior. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: correct behavior\n - <expected correct behavior after the fix>",
|
|
313
|
+
acceptanceSection: "#### Scenario: defect is resolved\n- GIVEN <conditions that trigger the defect>\n- WHEN <action that previously failed>\n- THEN <expected correct outcome>\n- AND the original broken outcome no longer occurs",
|
|
314
|
+
approach: "<root cause analysis and fix strategy>",
|
|
315
|
+
affectedArea1: "<concrete file path, directory, module, or subsystem containing the defect>",
|
|
316
|
+
affectedArea2: "<secondary path, dependency, or test file if needed>",
|
|
317
|
+
constraints: "<regression, backward-compatibility, safety, or scope constraint that must hold>",
|
|
318
|
+
task: "<identify the root cause and apply the minimal fix>",
|
|
319
|
+
automatedVerify: "<exact regression test, lint, build, or check command that proves the fix>",
|
|
320
|
+
manualVerify: "<exact steps to reproduce the original issue and confirm it no longer occurs>"
|
|
321
|
+
};
|
|
322
|
+
case "refactor":
|
|
323
|
+
return {
|
|
324
|
+
why: "<what maintainability, readability, or structural issue motivates this>",
|
|
325
|
+
scope: "<what code areas are being restructured>",
|
|
326
|
+
nonGoals: "<no behavior change is expected or intended>",
|
|
327
|
+
specSection: "<!-- Describe the desired structural outcome. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: internal structure improvement\n - <desired structural outcome without observable behavior change>",
|
|
328
|
+
acceptanceSection: "#### Scenario: behavior is preserved after restructuring\n- GIVEN the existing codebase before the refactor\n- WHEN the refactored code runs against existing tests\n- THEN all existing tests pass unchanged",
|
|
329
|
+
approach: "<how the code will be restructured (rename, extract, move, etc.)>",
|
|
330
|
+
affectedArea1: "<concrete file path, directory, module, or subsystem being refactored>",
|
|
331
|
+
affectedArea2: "<secondary path, dependency, or test file if needed>",
|
|
332
|
+
constraints: "<observable behavior must remain unchanged; compatibility, performance, and safety constraints must still hold>",
|
|
333
|
+
task: "<restructure the code while keeping behavior identical>",
|
|
334
|
+
automatedVerify: "<exact existing test, lint, build, or check command that must continue to pass>",
|
|
335
|
+
manualVerify: "<exact scenario to spot-check that behavior is unchanged after the refactor>"
|
|
336
|
+
};
|
|
234
337
|
case "docs":
|
|
235
338
|
return {
|
|
236
339
|
why: "<why the documentation change matters>",
|
|
237
340
|
scope: "<which docs, readers, or workflows will be updated>",
|
|
238
341
|
nonGoals: "<what documentation behavior or audience is out of scope>",
|
|
239
|
-
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>",
|
|
240
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",
|
|
241
344
|
approach: "<how the documentation will be updated and organized>",
|
|
242
|
-
|
|
243
|
-
|
|
345
|
+
affectedArea1: "<concrete doc path, directory, module doc, or documentation surface 1>",
|
|
346
|
+
affectedArea2: "<secondary doc path or reference file if needed>",
|
|
347
|
+
constraints: "<durable wording, consistency, scope, or audience constraint that must hold>",
|
|
244
348
|
task: "<update the target documentation and supporting references>",
|
|
245
|
-
automatedVerify: "<
|
|
246
|
-
manualVerify: "<
|
|
349
|
+
automatedVerify: "<exact markdown, docs, link, lint, build, or check command if applicable>",
|
|
350
|
+
manualVerify: "<exact reader or operator scenario to confirm the updated guidance is accurate>"
|
|
247
351
|
};
|
|
248
352
|
case "research":
|
|
249
353
|
return {
|
|
250
354
|
why: "<what question or uncertainty this research resolves>",
|
|
251
355
|
scope: "<what system area, option set, or hypothesis is being examined>",
|
|
252
356
|
nonGoals: "<what implementation work is explicitly out of scope>",
|
|
253
|
-
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>",
|
|
254
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",
|
|
255
359
|
approach: "<how the investigation will be performed and what evidence will be gathered>",
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
360
|
+
affectedArea1: "<concrete file path, directory, module, subsystem, or source under investigation>",
|
|
361
|
+
affectedArea2: "<secondary path, dependency, environment, or evidence source if needed>",
|
|
362
|
+
constraints: "<time, evidence, safety, or scope constraint for the investigation>",
|
|
363
|
+
task: "<gather evidence, record the recommendation or finding, and note any follow-up implementation work if needed>",
|
|
364
|
+
automatedVerify: "<exact command, script, query, or check used to confirm the observed behavior>",
|
|
365
|
+
manualVerify: "<exact review of the findings to confirm they answer the original question and identify follow-up work if needed>"
|
|
261
366
|
};
|
|
262
367
|
case "ops":
|
|
263
368
|
return {
|
|
264
369
|
why: "<why this operational or environment change matters>",
|
|
265
370
|
scope: "<what environment, workflow, or operational path changes>",
|
|
266
371
|
nonGoals: "<what product or feature behavior will not change>",
|
|
267
|
-
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>",
|
|
268
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",
|
|
269
374
|
approach: "<how the operational change will be applied safely>",
|
|
270
|
-
|
|
271
|
-
|
|
375
|
+
affectedArea1: "<concrete script, config path, workflow, environment path, or operational surface 1>",
|
|
376
|
+
affectedArea2: "<secondary script, config path, dependency, or environment surface if needed>",
|
|
377
|
+
constraints: "<rollback, safety, reliability, environment, or scope constraint that must hold>",
|
|
272
378
|
task: "<apply and verify the operational change>",
|
|
273
|
-
automatedVerify: "<
|
|
274
|
-
manualVerify: "<
|
|
379
|
+
automatedVerify: "<exact operational validation, lint, build, deploy, or check command>",
|
|
380
|
+
manualVerify: "<exact operator workflow or environment scenario to confirm the operational result>"
|
|
275
381
|
};
|
|
276
382
|
default:
|
|
277
383
|
return {
|
|
278
384
|
why: "<why this change matters>",
|
|
279
385
|
scope: "<what this change will do>",
|
|
280
386
|
nonGoals: "<what this change will not do>",
|
|
281
|
-
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>",
|
|
282
388
|
acceptanceSection: "#### Scenario: <name>\n- GIVEN <context>\n- WHEN <action>\n- THEN <expected outcome>",
|
|
283
389
|
approach: "<how the change will be implemented>",
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
390
|
+
affectedArea1: "<concrete file path, directory, module, or subsystem 1>",
|
|
391
|
+
affectedArea2: "<concrete file path, directory, module, or subsystem 2 if needed>",
|
|
392
|
+
constraints: "<behavior, compatibility, performance, safety, or scope constraint that must hold>",
|
|
393
|
+
task: "<implement the core change in the affected areas>",
|
|
394
|
+
automatedVerify: "<exact test, lint, build, or check command>",
|
|
395
|
+
manualVerify: "<exact end-to-end scenario to validate>"
|
|
289
396
|
};
|
|
290
397
|
}
|
|
291
398
|
}
|
|
@@ -293,6 +400,9 @@ function renderRspAgentsBlock() {
|
|
|
293
400
|
return `${RSP_AGENTS_BEGIN}
|
|
294
401
|
## RSP Entry
|
|
295
402
|
|
|
403
|
+
RSP keeps durable rules, specs, and current work under \`.rsp/\`.
|
|
404
|
+
Treat AGENTS.md as navigation only; keep durable rules and design in \`.rsp/\`.
|
|
405
|
+
|
|
296
406
|
Read in order:
|
|
297
407
|
1. .rsp/rules/rsp-rules.md
|
|
298
408
|
2. .rsp/focus.d/
|
|
@@ -300,6 +410,9 @@ Read in order:
|
|
|
300
410
|
4. .rsp/specs/design.md
|
|
301
411
|
5. .rsp/specs/INDEX.md
|
|
302
412
|
6. only the relevant additional .rsp/rules/*.md and .rsp/specs/*.md files
|
|
413
|
+
|
|
414
|
+
If \`.rsp/focus.d/\` is empty and the user has not provided a concrete task, ask what to work on or suggest \`npx -y @oevery/rsp create <name>\` for tracked work.
|
|
415
|
+
If your agent supports Agent Skills, load \`rsp\` for setup, repair, and durable-decision tasks.
|
|
303
416
|
${RSP_AGENTS_END}`;
|
|
304
417
|
}
|
|
305
418
|
function upsertRspAgentsBlock(content) {
|
|
@@ -332,6 +445,16 @@ function guardRspInitialized() {
|
|
|
332
445
|
process.exit(1);
|
|
333
446
|
}
|
|
334
447
|
}
|
|
448
|
+
async function getFocusedChangeNames(options = {}) {
|
|
449
|
+
const names = /* @__PURE__ */ new Set();
|
|
450
|
+
const focusDir = join(RSP_DIR, "focus.d");
|
|
451
|
+
if (!existsSync(focusDir))
|
|
452
|
+
return names;
|
|
453
|
+
const entries = await walkFiles(focusDir, options);
|
|
454
|
+
for (const entryPath of entries)
|
|
455
|
+
names.add(normalizeLogicalPath(relative(focusDir, entryPath)));
|
|
456
|
+
return names;
|
|
457
|
+
}
|
|
335
458
|
function getOpenCheckboxes(sectionText) {
|
|
336
459
|
return sectionText.split("\n").map((line) => line.trim()).filter((line) => /^- \[[ /]\]/.test(line));
|
|
337
460
|
}
|
|
@@ -478,6 +601,57 @@ function detectDeltaSections(content) {
|
|
|
478
601
|
removed: /^###\s*REMOVED/im.test(body)
|
|
479
602
|
};
|
|
480
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
|
+
}
|
|
481
655
|
function parseScenarios(content) {
|
|
482
656
|
const scenarios = [];
|
|
483
657
|
const regex = /^#{3,4}\s*Scenario:\s*(.+)$/gim;
|
|
@@ -623,20 +797,30 @@ import { dirname as dirname4, join as join5 } from "path";
|
|
|
623
797
|
import { existsSync as existsSync4 } from "fs";
|
|
624
798
|
import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
625
799
|
import { basename as basename2, join as join4, relative as relative2 } from "path";
|
|
626
|
-
async function buildSpecsIndex({ acquireLock = true } = {}) {
|
|
800
|
+
async function buildSpecsIndex({ acquireLock = true, quiet = false } = {}) {
|
|
627
801
|
if (acquireLock)
|
|
628
|
-
return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false }));
|
|
802
|
+
return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false, quiet }));
|
|
629
803
|
const specsDir = join4(RSP_DIR, "specs");
|
|
630
804
|
if (!existsSync4(specsDir)) {
|
|
631
|
-
|
|
805
|
+
if (!quiet)
|
|
806
|
+
console.log(` ${pc.dim("No specs directory.")}
|
|
632
807
|
`);
|
|
633
|
-
return;
|
|
808
|
+
return false;
|
|
634
809
|
}
|
|
635
810
|
const specFiles = (await walkMarkdownFiles(specsDir)).filter((fp) => {
|
|
636
811
|
const name = basename2(fp);
|
|
637
812
|
return name !== "INDEX.md" && name !== "design.md";
|
|
638
813
|
}).sort();
|
|
639
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("");
|
|
640
824
|
lines.push("# Specs Index");
|
|
641
825
|
lines.push("");
|
|
642
826
|
lines.push("_Additional project-level specs beyond `design.md`._");
|
|
@@ -656,11 +840,16 @@ async function buildSpecsIndex({ acquireLock = true } = {}) {
|
|
|
656
840
|
lines.push(`| ${escapeCell(rel)} | ${escapeCell(title)} | ${escapeCell(summary || "\u2014")} |`);
|
|
657
841
|
}
|
|
658
842
|
}
|
|
659
|
-
|
|
660
|
-
lines.
|
|
661
|
-
await
|
|
662
|
-
|
|
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).
|
|
663
851
|
`);
|
|
852
|
+
return changed;
|
|
664
853
|
}
|
|
665
854
|
function extractTitle(content) {
|
|
666
855
|
for (const rawLine of content.split("\n")) {
|
|
@@ -727,25 +916,33 @@ import { basename as basename4, dirname as dirname6, join as join7 } from "path"
|
|
|
727
916
|
import { existsSync as existsSync6 } from "fs";
|
|
728
917
|
import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
729
918
|
import { basename as basename3, dirname as dirname5, join as join6, relative as relative3 } from "path";
|
|
730
|
-
async function buildArchiveIndex({ acquireLock = true } = {}) {
|
|
919
|
+
async function buildArchiveIndex({ acquireLock = true, quiet = false } = {}) {
|
|
731
920
|
if (acquireLock)
|
|
732
|
-
return withRspLock("archive-index", async () => buildArchiveIndex({ acquireLock: false }));
|
|
921
|
+
return withRspLock("archive-index", async () => buildArchiveIndex({ acquireLock: false, quiet }));
|
|
733
922
|
if (!existsSync6(ARCHIVES_DIR)) {
|
|
734
|
-
|
|
923
|
+
if (!quiet)
|
|
924
|
+
console.log(` ${pc.dim("No archives directory.")}
|
|
735
925
|
`);
|
|
736
|
-
return;
|
|
926
|
+
return false;
|
|
737
927
|
}
|
|
738
928
|
const archiveFiles = (await walkMarkdownFiles(ARCHIVES_DIR)).filter((fp) => basename3(fp) !== "INDEX.md").sort();
|
|
739
929
|
const lines = [];
|
|
740
|
-
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("---");
|
|
741
938
|
lines.push("");
|
|
742
|
-
lines.push(
|
|
939
|
+
lines.push("# Archive Index");
|
|
743
940
|
lines.push("");
|
|
744
941
|
if (archiveFiles.length === 0) {
|
|
745
942
|
lines.push("_No archived changes yet._");
|
|
746
943
|
} else {
|
|
747
|
-
lines.push("| Date | Change | Summary |");
|
|
748
|
-
lines.push("
|
|
944
|
+
lines.push("| Date | Change | Kind | Summary |");
|
|
945
|
+
lines.push("|------|--------|------|---------|");
|
|
749
946
|
}
|
|
750
947
|
for (const fp of archiveFiles) {
|
|
751
948
|
const base = basename3(fp);
|
|
@@ -756,27 +953,39 @@ async function buildArchiveIndex({ acquireLock = true } = {}) {
|
|
|
756
953
|
const baseName = dateMatch ? dateMatch[2] : base.replace(/\.md$/, "");
|
|
757
954
|
const changeName = archiveDirPart !== "." ? `${archiveDirPart}/${baseName}` : baseName;
|
|
758
955
|
let summary = "";
|
|
956
|
+
let kind = "";
|
|
759
957
|
try {
|
|
760
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();
|
|
761
964
|
const proposal = extractSection(content, "Proposal");
|
|
762
965
|
const summaryLine = proposal.split("\n").map((line) => line.trim()).find((line) => line.startsWith("- Summary:"));
|
|
763
|
-
if (summaryLine)
|
|
966
|
+
if (!summary && summaryLine)
|
|
764
967
|
summary = summaryLine.slice("- Summary:".length).trim();
|
|
765
968
|
} catch {
|
|
766
969
|
}
|
|
767
|
-
|
|
768
|
-
lines.push(`| ${date} | ${changeName} | ${summaryCell} |`);
|
|
970
|
+
lines.push(`| ${date} | ${escapeCell2(changeName)} | ${escapeCell2(kind || "\u2014")} | ${escapeCell2(summary || "\u2014")} |`);
|
|
769
971
|
}
|
|
770
|
-
lines.push("");
|
|
771
|
-
lines.push("_Auto-generated by `rsp update`._");
|
|
772
972
|
const indexPath = join6(ARCHIVES_DIR, "INDEX.md");
|
|
773
|
-
|
|
774
|
-
|
|
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).
|
|
775
980
|
`);
|
|
981
|
+
return changed;
|
|
982
|
+
}
|
|
983
|
+
function escapeCell2(value) {
|
|
984
|
+
return value.replace(/\|/g, "\\|");
|
|
776
985
|
}
|
|
777
986
|
|
|
778
987
|
// src/commands/archive.ts
|
|
779
|
-
async function archiveChange(name) {
|
|
988
|
+
async function archiveChange(name, options = {}) {
|
|
780
989
|
if (!name) {
|
|
781
990
|
console.error(` ${pc.red("Usage:")} rsp archive <name>`);
|
|
782
991
|
process.exit(1);
|
|
@@ -785,11 +994,30 @@ async function archiveChange(name) {
|
|
|
785
994
|
console.error(` ${pc.red("Error:")} change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
786
995
|
process.exit(1);
|
|
787
996
|
}
|
|
997
|
+
guardRspInitialized();
|
|
788
998
|
const srcPath = join7(CHANGES_DIR, `${name}.md`);
|
|
789
999
|
if (!existsSync7(srcPath)) {
|
|
790
1000
|
console.error(` ${pc.red("Change not found:")} .rsp/changes/${name}.md`);
|
|
791
1001
|
process.exit(1);
|
|
792
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
|
+
}
|
|
793
1021
|
return withRspLock("archive-change", async () => {
|
|
794
1022
|
const content = await readFile6(srcPath, "utf-8");
|
|
795
1023
|
const checklist = collectArchiveChecklist(content);
|
|
@@ -859,23 +1087,6 @@ function resolveArchiveName(archiveSubdir, date, base) {
|
|
|
859
1087
|
console.error(` ${pc.red("Error:")} archive name collision exceeded ${MAX_SUFFIX} attempts for ${base}`);
|
|
860
1088
|
process.exit(1);
|
|
861
1089
|
}
|
|
862
|
-
function collectArchiveChecklist(content) {
|
|
863
|
-
const warnings = [];
|
|
864
|
-
const tasksSection = extractSection(content, "Tasks");
|
|
865
|
-
const verifySection = extractSection(content, "Verify");
|
|
866
|
-
const taskTodos = getOpenCheckboxes(tasksSection);
|
|
867
|
-
if (taskTodos.length > 0)
|
|
868
|
-
warnings.push(`${taskTodos.length} task item(s) still incomplete`);
|
|
869
|
-
const verifyTodos = getOpenCheckboxes(verifySection);
|
|
870
|
-
if (verifyTodos.length > 0)
|
|
871
|
-
warnings.push(`${verifyTodos.length} Verify checklist item(s) are still incomplete`);
|
|
872
|
-
if (hasMeaningfulBlockers(content))
|
|
873
|
-
warnings.push("active blockers are present in the change file");
|
|
874
|
-
const scenarios = parseScenarios(content);
|
|
875
|
-
if (scenarios.length === 0)
|
|
876
|
-
warnings.push("no Scenario blocks found (some changes do not need them)");
|
|
877
|
-
return warnings;
|
|
878
|
-
}
|
|
879
1090
|
|
|
880
1091
|
// src/commands/check.ts
|
|
881
1092
|
import { existsSync as existsSync8 } from "fs";
|
|
@@ -890,6 +1101,10 @@ async function runCheck(options = {}) {
|
|
|
890
1101
|
const runtime = [];
|
|
891
1102
|
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
|
|
892
1103
|
const addDiagnostic = (diagnostic) => diagnostics.push(diagnostic);
|
|
1104
|
+
let focusedSet = /* @__PURE__ */ new Set();
|
|
1105
|
+
if (options.focused) {
|
|
1106
|
+
focusedSet = await getFocusedChangeNames({ onError: reportRuntime });
|
|
1107
|
+
}
|
|
893
1108
|
if (existsSync8(FOCUS_DIR)) {
|
|
894
1109
|
const entries = await walkFiles(FOCUS_DIR, { onError: reportRuntime });
|
|
895
1110
|
for (const entryPath of entries) {
|
|
@@ -934,11 +1149,13 @@ async function runCheck(options = {}) {
|
|
|
934
1149
|
}
|
|
935
1150
|
}
|
|
936
1151
|
}
|
|
937
|
-
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;
|
|
938
1154
|
if (changeFiles.length === 0) {
|
|
939
1155
|
const result2 = {
|
|
940
1156
|
command: "check",
|
|
941
1157
|
ok: diagnostics.every((d) => d.severity !== "error"),
|
|
1158
|
+
focused: Boolean(options.focused),
|
|
942
1159
|
diagnostics,
|
|
943
1160
|
runtime,
|
|
944
1161
|
summary: {
|
|
@@ -953,6 +1170,8 @@ async function runCheck(options = {}) {
|
|
|
953
1170
|
}
|
|
954
1171
|
console.log();
|
|
955
1172
|
console.log(` ${pc.bold("RSP check")}`);
|
|
1173
|
+
if (options.focused)
|
|
1174
|
+
console.log(` ${pc.dim("(focused only)")}`);
|
|
956
1175
|
console.log();
|
|
957
1176
|
if (diagnostics.length === 0) {
|
|
958
1177
|
console.log(` ${pc.dim("No change files to check.")}
|
|
@@ -1089,6 +1308,30 @@ async function runCheck(options = {}) {
|
|
|
1089
1308
|
message: `delta markers found: ${parts.join(", ")}`
|
|
1090
1309
|
});
|
|
1091
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
|
+
}
|
|
1092
1335
|
const scenarios = parseScenarios(content);
|
|
1093
1336
|
if (scenarios.length > 0) {
|
|
1094
1337
|
const scenarioIssues = [];
|
|
@@ -1121,6 +1364,7 @@ async function runCheck(options = {}) {
|
|
|
1121
1364
|
const result = {
|
|
1122
1365
|
command: "check",
|
|
1123
1366
|
ok: diagnostics.every((d) => d.severity !== "error"),
|
|
1367
|
+
focused: Boolean(options.focused),
|
|
1124
1368
|
diagnostics,
|
|
1125
1369
|
runtime,
|
|
1126
1370
|
summary: {
|
|
@@ -1135,6 +1379,8 @@ async function runCheck(options = {}) {
|
|
|
1135
1379
|
}
|
|
1136
1380
|
console.log();
|
|
1137
1381
|
console.log(` ${pc.bold("RSP check")}`);
|
|
1382
|
+
if (options.focused)
|
|
1383
|
+
console.log(` ${pc.dim("(focused only)")}`);
|
|
1138
1384
|
console.log();
|
|
1139
1385
|
for (const diagnostic of diagnostics) {
|
|
1140
1386
|
const icon = diagnostic.severity === "error" ? pc.red("\u2717") : diagnostic.severity === "warning" ? pc.yellow("\u26A0") : pc.dim("\u2139");
|
|
@@ -1155,12 +1401,35 @@ async function runCheck(options = {}) {
|
|
|
1155
1401
|
`);
|
|
1156
1402
|
return result;
|
|
1157
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
|
+
}
|
|
1158
1427
|
|
|
1159
1428
|
// src/commands/create.ts
|
|
1160
1429
|
import { existsSync as existsSync9 } from "fs";
|
|
1161
1430
|
import { mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
|
|
1162
1431
|
import { dirname as dirname7, join as join9 } from "path";
|
|
1163
|
-
async function createChange(name, summary = "", kind) {
|
|
1432
|
+
async function createChange(name, summary = "", kind, options = {}) {
|
|
1164
1433
|
if (!name) {
|
|
1165
1434
|
console.error(` ${pc.red("Usage:")} rsp create <name> [summary]`);
|
|
1166
1435
|
process.exit(1);
|
|
@@ -1175,7 +1444,7 @@ async function createChange(name, summary = "", kind) {
|
|
|
1175
1444
|
await mkdir5(dirname7(changePath), { recursive: true });
|
|
1176
1445
|
const existed = existsSync9(changePath);
|
|
1177
1446
|
if (!existed) {
|
|
1178
|
-
const content = generateChangeContent(name, summary, kind);
|
|
1447
|
+
const content = generateChangeContent(name, summary, kind, { lite: Boolean(options.lite) });
|
|
1179
1448
|
await writeFile6(changePath, content);
|
|
1180
1449
|
await mkdir5(FOCUS_DIR, { recursive: true });
|
|
1181
1450
|
const focusEntry = join9(FOCUS_DIR, name);
|
|
@@ -1188,37 +1457,101 @@ async function createChange(name, summary = "", kind) {
|
|
|
1188
1457
|
console.log(` ${pc.dim("Unchanged focus.")} Run: rsp focus ${name}`);
|
|
1189
1458
|
else
|
|
1190
1459
|
console.log(` ${pc.dim("focused via focus.d")} \u2192 ${name}`);
|
|
1191
|
-
console.log(` ${pc.cyan("Next:")}
|
|
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"}
|
|
1192
1461
|
`);
|
|
1193
1462
|
});
|
|
1194
1463
|
}
|
|
1195
1464
|
|
|
1196
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
|
|
1197
1471
|
import { existsSync as existsSync10 } from "fs";
|
|
1198
|
-
import { readFile as readFile8 } from "fs/promises";
|
|
1199
|
-
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
|
|
1200
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
|
+
}
|
|
1201
1533
|
const checks = [];
|
|
1202
1534
|
const runtime = [];
|
|
1203
1535
|
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
|
|
1204
|
-
const rspRulesPath =
|
|
1205
|
-
const designPath =
|
|
1206
|
-
const specsIndexPath =
|
|
1207
|
-
const archivesIndexPath =
|
|
1208
|
-
reportCheck(checks, ".rsp exists",
|
|
1209
|
-
reportCheck(checks, "rules/rsp-rules.md exists",
|
|
1210
|
-
reportCheck(checks, "specs/design.md exists",
|
|
1211
|
-
reportCheck(checks, "specs/INDEX.md exists",
|
|
1212
|
-
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");
|
|
1213
1545
|
await checkAgents(checks, reportRuntime);
|
|
1214
|
-
await checkGeneratedIndex(checks, reportRuntime, specsIndexPath, "
|
|
1215
|
-
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");
|
|
1216
1548
|
await checkArchiveNaming(checks, reportRuntime);
|
|
1217
1549
|
await checkConfigSemantics(checks, reportRuntime);
|
|
1218
1550
|
await checkActiveChangeConsistency(checks, reportRuntime);
|
|
1219
1551
|
const result = {
|
|
1220
1552
|
command: "doctor",
|
|
1221
1553
|
ok: checks.every((check) => check.status !== "issue"),
|
|
1554
|
+
fixed,
|
|
1222
1555
|
checks,
|
|
1223
1556
|
runtime,
|
|
1224
1557
|
summary: {
|
|
@@ -1232,6 +1565,13 @@ async function runDoctor(options = {}) {
|
|
|
1232
1565
|
console.log();
|
|
1233
1566
|
console.log(` ${pc.bold("RSP doctor")}`);
|
|
1234
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
|
+
}
|
|
1235
1575
|
for (const check of checks)
|
|
1236
1576
|
printDoctorCheck(check);
|
|
1237
1577
|
if (result.summary.issues === 0)
|
|
@@ -1267,13 +1607,13 @@ function printDoctorCheck(check) {
|
|
|
1267
1607
|
console.log(` ${pc.dim(check.hint)}`);
|
|
1268
1608
|
}
|
|
1269
1609
|
async function checkAgents(checks, reportRuntime) {
|
|
1270
|
-
if (!
|
|
1610
|
+
if (!existsSync11("AGENTS.md")) {
|
|
1271
1611
|
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing", hint: "Run: rsp init" });
|
|
1272
1612
|
return;
|
|
1273
1613
|
}
|
|
1274
1614
|
let agents;
|
|
1275
1615
|
try {
|
|
1276
|
-
agents = await
|
|
1616
|
+
agents = await readFile9("AGENTS.md", "utf-8");
|
|
1277
1617
|
} catch (error) {
|
|
1278
1618
|
reportRuntime({
|
|
1279
1619
|
code: "agents_read_failed",
|
|
@@ -1290,12 +1630,12 @@ async function checkAgents(checks, reportRuntime) {
|
|
|
1290
1630
|
}
|
|
1291
1631
|
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing managed RSP block", hint: "Run: rsp init" });
|
|
1292
1632
|
}
|
|
1293
|
-
async function checkGeneratedIndex(checks, reportRuntime, path,
|
|
1294
|
-
if (!
|
|
1633
|
+
async function checkGeneratedIndex(checks, reportRuntime, path, expectedIndexType, label, fixHint) {
|
|
1634
|
+
if (!existsSync11(path))
|
|
1295
1635
|
return;
|
|
1296
1636
|
let content;
|
|
1297
1637
|
try {
|
|
1298
|
-
content = await
|
|
1638
|
+
content = await readFile9(path, "utf-8");
|
|
1299
1639
|
} catch (error) {
|
|
1300
1640
|
reportRuntime({
|
|
1301
1641
|
code: "generated_index_read_failed",
|
|
@@ -1306,15 +1646,27 @@ async function checkGeneratedIndex(checks, reportRuntime, path, signature, label
|
|
|
1306
1646
|
checks.push({ status: "issue", label, message: `${label} could not be read`, hint: fixHint });
|
|
1307
1647
|
return;
|
|
1308
1648
|
}
|
|
1309
|
-
|
|
1310
|
-
|
|
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 });
|
|
1311
1663
|
return;
|
|
1312
1664
|
}
|
|
1313
1665
|
checks.push({ status: "issue", label, hint: fixHint });
|
|
1314
1666
|
}
|
|
1315
1667
|
async function checkArchiveNaming(checks, reportRuntime) {
|
|
1316
|
-
const archivesDir =
|
|
1317
|
-
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") : [];
|
|
1318
1670
|
const invalidFiles = archiveFiles.map((fp) => normalizeLogicalPath(relative5(archivesDir, fp))).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(basename5(fp)) === false);
|
|
1319
1671
|
if (invalidFiles.length === 0) {
|
|
1320
1672
|
checks.push({ status: "ok", label: "archived change files follow the date_name.md convention" });
|
|
@@ -1328,11 +1680,11 @@ async function checkArchiveNaming(checks, reportRuntime) {
|
|
|
1328
1680
|
});
|
|
1329
1681
|
}
|
|
1330
1682
|
async function checkConfigSemantics(checks, reportRuntime) {
|
|
1331
|
-
if (!
|
|
1683
|
+
if (!existsSync11(CONFIG_PATH))
|
|
1332
1684
|
return;
|
|
1333
1685
|
let content;
|
|
1334
1686
|
try {
|
|
1335
|
-
content = await
|
|
1687
|
+
content = await readFile9(CONFIG_PATH, "utf-8");
|
|
1336
1688
|
} catch (error) {
|
|
1337
1689
|
reportRuntime({
|
|
1338
1690
|
code: "config_read_failed",
|
|
@@ -1388,10 +1740,9 @@ function reportConfigListField(checks, parsed, key) {
|
|
|
1388
1740
|
});
|
|
1389
1741
|
}
|
|
1390
1742
|
async function checkActiveChangeConsistency(checks, reportRuntime) {
|
|
1391
|
-
const changeFiles =
|
|
1743
|
+
const changeFiles = existsSync11(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
|
|
1392
1744
|
const changeNames = new Set(changeFiles.map((fp) => changeNameFromPath(CHANGES_DIR, fp)));
|
|
1393
|
-
const
|
|
1394
|
-
const focusNames = new Set(focusEntries.map((fp) => normalizeLogicalPath(relative5(FOCUS_DIR, fp))));
|
|
1745
|
+
const focusNames = await getFocusedChangeNames({ onError: reportRuntime });
|
|
1395
1746
|
const missingChangeFiles = [...focusNames].filter((name) => !changeNames.has(name));
|
|
1396
1747
|
const unfocusedChangeFiles = [...changeNames].filter((name) => !focusNames.has(name));
|
|
1397
1748
|
if (missingChangeFiles.length === 0) {
|
|
@@ -1419,9 +1770,9 @@ async function checkActiveChangeConsistency(checks, reportRuntime) {
|
|
|
1419
1770
|
}
|
|
1420
1771
|
|
|
1421
1772
|
// src/commands/focus.ts
|
|
1422
|
-
import { existsSync as
|
|
1423
|
-
import { mkdir as
|
|
1424
|
-
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";
|
|
1425
1776
|
async function focusChange(name) {
|
|
1426
1777
|
if (!name) {
|
|
1427
1778
|
console.error(` ${pc.red("Usage:")} rsp focus <name>`);
|
|
@@ -1432,16 +1783,16 @@ async function focusChange(name) {
|
|
|
1432
1783
|
process.exit(1);
|
|
1433
1784
|
}
|
|
1434
1785
|
guardRspInitialized();
|
|
1435
|
-
const changePath =
|
|
1436
|
-
if (!
|
|
1786
|
+
const changePath = join12(CHANGES_DIR, `${name}.md`);
|
|
1787
|
+
if (!existsSync12(changePath)) {
|
|
1437
1788
|
console.error(` ${pc.red("Change not found:")} .rsp/changes/${name}.md`);
|
|
1438
1789
|
process.exit(1);
|
|
1439
1790
|
}
|
|
1440
1791
|
return withRspLock("focus-change", async () => {
|
|
1441
|
-
await
|
|
1442
|
-
const focusEntry =
|
|
1443
|
-
await
|
|
1444
|
-
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, "");
|
|
1445
1796
|
console.log(` ${pc.green("Focused:")} ${name}`);
|
|
1446
1797
|
console.log(` ${pc.dim("focus.d")} \u2192 ${name}`);
|
|
1447
1798
|
console.log();
|
|
@@ -1457,8 +1808,8 @@ async function unfocusChange(name) {
|
|
|
1457
1808
|
process.exit(1);
|
|
1458
1809
|
}
|
|
1459
1810
|
guardRspInitialized();
|
|
1460
|
-
const focusEntry =
|
|
1461
|
-
if (!
|
|
1811
|
+
const focusEntry = join12(FOCUS_DIR, name);
|
|
1812
|
+
if (!existsSync12(focusEntry)) {
|
|
1462
1813
|
console.error(` ${pc.red("Focus marker not found:")} .rsp/focus.d/${name}`);
|
|
1463
1814
|
process.exit(1);
|
|
1464
1815
|
}
|
|
@@ -1472,9 +1823,9 @@ async function unfocusChange(name) {
|
|
|
1472
1823
|
}
|
|
1473
1824
|
|
|
1474
1825
|
// src/commands/init.ts
|
|
1475
|
-
import { existsSync as
|
|
1476
|
-
import { mkdir as
|
|
1477
|
-
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";
|
|
1478
1829
|
function generateConfigTemplate() {
|
|
1479
1830
|
const fmtList = (items, indent) => items.map((i) => `${" ".repeat(indent)}# - ${i}`).join("\n");
|
|
1480
1831
|
return `# RSP project configuration
|
|
@@ -1488,16 +1839,16 @@ ${fmtList(VALID_KINDS, 2)}
|
|
|
1488
1839
|
`;
|
|
1489
1840
|
}
|
|
1490
1841
|
async function ensureFile(path, content) {
|
|
1491
|
-
if (
|
|
1842
|
+
if (existsSync13(path))
|
|
1492
1843
|
return false;
|
|
1493
|
-
await
|
|
1844
|
+
await writeFile9(path, content);
|
|
1494
1845
|
return true;
|
|
1495
1846
|
}
|
|
1496
1847
|
function toTitle(projectName) {
|
|
1497
1848
|
return projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
|
|
1498
1849
|
}
|
|
1499
1850
|
async function initProject(args = {}) {
|
|
1500
|
-
const isNew = !
|
|
1851
|
+
const isNew = !existsSync13(RSP_DIR);
|
|
1501
1852
|
const projectName = await detectProjectName();
|
|
1502
1853
|
const mode = args.agentsMode ?? "managed";
|
|
1503
1854
|
if (mode !== "managed" && mode !== "print") {
|
|
@@ -1506,32 +1857,32 @@ async function initProject(args = {}) {
|
|
|
1506
1857
|
}
|
|
1507
1858
|
return withRspLock("init", async () => {
|
|
1508
1859
|
const dirs = [
|
|
1509
|
-
|
|
1510
|
-
|
|
1860
|
+
join13(RSP_DIR, "rules"),
|
|
1861
|
+
join13(RSP_DIR, "specs"),
|
|
1511
1862
|
CHANGES_DIR,
|
|
1512
1863
|
FOCUS_DIR,
|
|
1513
|
-
|
|
1864
|
+
join13(RSP_DIR, "archives")
|
|
1514
1865
|
];
|
|
1515
1866
|
for (const d of dirs)
|
|
1516
|
-
await
|
|
1867
|
+
await mkdir8(d, { recursive: true });
|
|
1517
1868
|
let created = false;
|
|
1518
|
-
const bundledRules = await
|
|
1519
|
-
created = await ensureFile(
|
|
1520
|
-
created = await ensureFile(
|
|
1521
|
-
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");
|
|
1522
1873
|
created = createdSpecsIndex || created;
|
|
1523
|
-
const createdDesign = await ensureFile(
|
|
1874
|
+
const createdDesign = await ensureFile(join13(RSP_DIR, "specs", "design.md"), generateDesignContent(projectName));
|
|
1524
1875
|
created = createdDesign || created;
|
|
1525
|
-
const createdArchivesIndex = await ensureFile(
|
|
1876
|
+
const createdArchivesIndex = await ensureFile(join13(RSP_DIR, "archives", "INDEX.md"), "# Archive Index\n");
|
|
1526
1877
|
created = createdArchivesIndex || created;
|
|
1527
|
-
created = await ensureFile(
|
|
1528
|
-
created = await ensureFile(
|
|
1529
|
-
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;
|
|
1530
1881
|
if (args.withProjectRules)
|
|
1531
|
-
created = await ensureFile(
|
|
1882
|
+
created = await ensureFile(join13(RSP_DIR, "rules", "project-rules.md"), generateProjectRulesContent(projectName)) || created;
|
|
1532
1883
|
if (args.withProjectSetup) {
|
|
1533
|
-
const changePath =
|
|
1534
|
-
const focusMarker =
|
|
1884
|
+
const changePath = join13(CHANGES_DIR, "project-setup.md");
|
|
1885
|
+
const focusMarker = join13(FOCUS_DIR, "project-setup");
|
|
1535
1886
|
created = await ensureFile(changePath, generateChangeContent("project-setup")) || created;
|
|
1536
1887
|
created = await ensureFile(focusMarker, "") || created;
|
|
1537
1888
|
}
|
|
@@ -1542,24 +1893,24 @@ async function initProject(args = {}) {
|
|
|
1542
1893
|
const agentsPath = "AGENTS.md";
|
|
1543
1894
|
const title = toTitle(projectName);
|
|
1544
1895
|
let agentsUpdated = false;
|
|
1545
|
-
if (!
|
|
1546
|
-
await
|
|
1896
|
+
if (!existsSync13(agentsPath)) {
|
|
1897
|
+
await writeFile9(agentsPath, `# ${title}
|
|
1547
1898
|
|
|
1548
1899
|
${upsertRspAgentsBlock("").content}
|
|
1549
1900
|
`);
|
|
1550
1901
|
created = true;
|
|
1551
1902
|
agentsUpdated = true;
|
|
1552
1903
|
} else {
|
|
1553
|
-
const existing = await
|
|
1904
|
+
const existing = await readFile10(agentsPath, "utf-8");
|
|
1554
1905
|
const next = upsertRspAgentsBlock(existing);
|
|
1555
1906
|
if (next.changed) {
|
|
1556
|
-
await
|
|
1907
|
+
await writeFile9(agentsPath, next.content);
|
|
1557
1908
|
created = true;
|
|
1558
1909
|
agentsUpdated = true;
|
|
1559
1910
|
}
|
|
1560
1911
|
}
|
|
1561
1912
|
if (mode === "print") {
|
|
1562
|
-
const managed = await
|
|
1913
|
+
const managed = await readFile10(agentsPath, "utf-8");
|
|
1563
1914
|
console.log(`
|
|
1564
1915
|
${managed}
|
|
1565
1916
|
`);
|
|
@@ -1596,24 +1947,267 @@ ${managed}
|
|
|
1596
1947
|
});
|
|
1597
1948
|
}
|
|
1598
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
|
+
|
|
1599
2198
|
// src/commands/status.ts
|
|
1600
|
-
import { existsSync as
|
|
1601
|
-
import { readFile as
|
|
1602
|
-
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";
|
|
1603
2202
|
var COL_CHANGE = 32;
|
|
1604
2203
|
var COL_KIND = 10;
|
|
1605
2204
|
var COL_AGE = 7;
|
|
1606
2205
|
var COL_PROGRESS = 10;
|
|
1607
2206
|
async function showStatus(options = {}, runOptions = {}) {
|
|
1608
|
-
const focusedSet = /* @__PURE__ */ new Set();
|
|
1609
2207
|
const runtime = [];
|
|
1610
2208
|
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(runOptions.verbose) && !runOptions.json);
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
for (const entryPath of entries)
|
|
1614
|
-
focusedSet.add(normalizeLogicalPath(relative6(FOCUS_DIR, entryPath)));
|
|
1615
|
-
}
|
|
1616
|
-
const changeFiles = existsSync13(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
|
|
2209
|
+
const focusedSet = await getFocusedChangeNames({ onError: reportRuntime });
|
|
2210
|
+
const changeFiles = existsSync16(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
|
|
1617
2211
|
const changeMap = {};
|
|
1618
2212
|
for (const fp of changeFiles)
|
|
1619
2213
|
changeMap[changeNameFromPath(CHANGES_DIR, fp)] = fp;
|
|
@@ -1631,7 +2225,7 @@ async function showStatus(options = {}, runOptions = {}) {
|
|
|
1631
2225
|
let total = 0;
|
|
1632
2226
|
if (fp) {
|
|
1633
2227
|
try {
|
|
1634
|
-
const content = await
|
|
2228
|
+
const content = await readFile13(fp, "utf-8");
|
|
1635
2229
|
try {
|
|
1636
2230
|
const fm = parseFrontmatter(content);
|
|
1637
2231
|
kind = fm?.kind ? String(fm.kind) : "\u2014";
|
|
@@ -1658,7 +2252,7 @@ async function showStatus(options = {}, runOptions = {}) {
|
|
|
1658
2252
|
});
|
|
1659
2253
|
}
|
|
1660
2254
|
try {
|
|
1661
|
-
const s = await
|
|
2255
|
+
const s = await stat(fp);
|
|
1662
2256
|
ageDays = Math.floor((Date.now() - s.mtime.getTime()) / (1e3 * 60 * 60 * 24));
|
|
1663
2257
|
} catch (error) {
|
|
1664
2258
|
reportRuntime({
|
|
@@ -1673,7 +2267,7 @@ async function showStatus(options = {}, runOptions = {}) {
|
|
|
1673
2267
|
reportRuntime({
|
|
1674
2268
|
code: "focused_change_missing",
|
|
1675
2269
|
operation: "resolveChange",
|
|
1676
|
-
path:
|
|
2270
|
+
path: join16(CHANGES_DIR, `${name}.md`),
|
|
1677
2271
|
message: "focus marker points to a missing change file"
|
|
1678
2272
|
});
|
|
1679
2273
|
}
|
|
@@ -1713,6 +2307,7 @@ async function showStatus(options = {}, runOptions = {}) {
|
|
|
1713
2307
|
return true;
|
|
1714
2308
|
});
|
|
1715
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"));
|
|
1716
2311
|
const statusResult = {
|
|
1717
2312
|
command: "status",
|
|
1718
2313
|
ok: true,
|
|
@@ -1728,6 +2323,7 @@ async function showStatus(options = {}, runOptions = {}) {
|
|
|
1728
2323
|
focused: filteredOutputRecords.filter((r) => r.isFocused).length,
|
|
1729
2324
|
blocked: blockedCount
|
|
1730
2325
|
},
|
|
2326
|
+
nextActions,
|
|
1731
2327
|
archiveTrend: await readArchiveTrend(runtime, runOptions),
|
|
1732
2328
|
runtime
|
|
1733
2329
|
};
|
|
@@ -1742,7 +2338,9 @@ async function showStatus(options = {}, runOptions = {}) {
|
|
|
1742
2338
|
console.log(` ${pc.cyan("Focused:")} ${[...focusedSet].join(", ")}`);
|
|
1743
2339
|
console.log();
|
|
1744
2340
|
} else {
|
|
1745
|
-
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)}`);
|
|
1746
2344
|
console.log();
|
|
1747
2345
|
}
|
|
1748
2346
|
if (allNames.length === 0) {
|
|
@@ -1779,12 +2377,32 @@ async function showStatus(options = {}, runOptions = {}) {
|
|
|
1779
2377
|
}
|
|
1780
2378
|
return statusResult;
|
|
1781
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
|
+
}
|
|
1782
2400
|
async function readArchiveTrend(runtime, runOptions) {
|
|
1783
|
-
const indexPath =
|
|
1784
|
-
if (!
|
|
2401
|
+
const indexPath = join16(ARCHIVES_DIR, "INDEX.md");
|
|
2402
|
+
if (!existsSync16(indexPath))
|
|
1785
2403
|
return [];
|
|
1786
2404
|
try {
|
|
1787
|
-
const content = await
|
|
2405
|
+
const content = await readFile13(indexPath, "utf-8");
|
|
1788
2406
|
const dateRe = /^\| (\d{4}-\d{2})-\d{2} \|/gm;
|
|
1789
2407
|
const counts = {};
|
|
1790
2408
|
let match;
|
|
@@ -1815,52 +2433,6 @@ function printArchiveTrend(trend) {
|
|
|
1815
2433
|
console.log();
|
|
1816
2434
|
}
|
|
1817
2435
|
|
|
1818
|
-
// src/commands/update.ts
|
|
1819
|
-
import { existsSync as existsSync14 } from "fs";
|
|
1820
|
-
import { mkdir as mkdir8, readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
|
|
1821
|
-
import { join as join14 } from "path";
|
|
1822
|
-
var SKILL_REFRESH_HINT = " Note: if you use the published RSP skill, refresh it too:\n npx skills add oevery/rsp\n";
|
|
1823
|
-
async function updateProject() {
|
|
1824
|
-
if (!existsSync14(RSP_DIR)) {
|
|
1825
|
-
console.log(` ${pc.red("Error:")} RSP is not initialized. Run: rsp init`);
|
|
1826
|
-
process.exit(1);
|
|
1827
|
-
}
|
|
1828
|
-
return withRspLock("update", async () => {
|
|
1829
|
-
let updated = false;
|
|
1830
|
-
const bundledRules = await readFile11(join14(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
|
|
1831
|
-
const rulesPath = join14(RSP_DIR, "rules", "rsp-rules.md");
|
|
1832
|
-
await mkdir8(join14(RSP_DIR, "rules"), { recursive: true });
|
|
1833
|
-
const existingRules = existsSync14(rulesPath) ? await readFile11(rulesPath, "utf-8") : null;
|
|
1834
|
-
if (existingRules !== bundledRules) {
|
|
1835
|
-
await writeFile9(rulesPath, bundledRules);
|
|
1836
|
-
console.log(` ${pc.green("\u2713")} rules/rsp-rules.md updated`);
|
|
1837
|
-
updated = true;
|
|
1838
|
-
}
|
|
1839
|
-
const agentsPath = "AGENTS.md";
|
|
1840
|
-
const projectName = await detectProjectName();
|
|
1841
|
-
const baseAgents = existsSync14(agentsPath) ? await readFile11(agentsPath, "utf-8") : `# ${projectName}
|
|
1842
|
-
|
|
1843
|
-
`;
|
|
1844
|
-
const nextAgents = upsertRspAgentsBlock(baseAgents);
|
|
1845
|
-
if (!existsSync14(agentsPath) || nextAgents.changed) {
|
|
1846
|
-
await writeFile9(agentsPath, nextAgents.content);
|
|
1847
|
-
console.log(` ${pc.green("\u2713")} AGENTS.md managed block refreshed`);
|
|
1848
|
-
updated = true;
|
|
1849
|
-
}
|
|
1850
|
-
await buildSpecsIndex({ acquireLock: false });
|
|
1851
|
-
await buildArchiveIndex({ acquireLock: false });
|
|
1852
|
-
if (!updated) {
|
|
1853
|
-
console.log(` ${pc.dim("Already up to date.")}
|
|
1854
|
-
`);
|
|
1855
|
-
console.log(SKILL_REFRESH_HINT);
|
|
1856
|
-
} else {
|
|
1857
|
-
console.log(` ${pc.green("Update complete.")}
|
|
1858
|
-
`);
|
|
1859
|
-
console.log(SKILL_REFRESH_HINT);
|
|
1860
|
-
}
|
|
1861
|
-
});
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
2436
|
// src/cli.ts
|
|
1865
2437
|
var initCommand = defineCommand({
|
|
1866
2438
|
meta: {
|
|
@@ -1906,11 +2478,16 @@ var createCommand = defineCommand({
|
|
|
1906
2478
|
kind: {
|
|
1907
2479
|
type: "string",
|
|
1908
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
|
|
1909
2486
|
}
|
|
1910
2487
|
},
|
|
1911
2488
|
async run({ args }) {
|
|
1912
2489
|
const summary = Array.isArray(args._) && args._.length > 1 ? args._.slice(1).join(" ") : "";
|
|
1913
|
-
await createChange(args.name, summary, args.kind);
|
|
2490
|
+
await createChange(args.name, summary, args.kind, { lite: Boolean(args.lite) });
|
|
1914
2491
|
}
|
|
1915
2492
|
});
|
|
1916
2493
|
var addRulesCommand = defineCommand({
|
|
@@ -1961,14 +2538,19 @@ var archiveCommand = defineCommand({
|
|
|
1961
2538
|
description: "Archive an open change into .rsp/archives/"
|
|
1962
2539
|
},
|
|
1963
2540
|
args: {
|
|
1964
|
-
name: {
|
|
2541
|
+
"name": {
|
|
1965
2542
|
type: "positional",
|
|
1966
2543
|
description: "Change name",
|
|
1967
2544
|
required: true
|
|
2545
|
+
},
|
|
2546
|
+
"dry-run": {
|
|
2547
|
+
type: "boolean",
|
|
2548
|
+
description: "Preview archive readiness without moving the change",
|
|
2549
|
+
default: false
|
|
1968
2550
|
}
|
|
1969
2551
|
},
|
|
1970
2552
|
async run({ args }) {
|
|
1971
|
-
await archiveChange(args.name);
|
|
2553
|
+
await archiveChange(args.name, { dryRun: Boolean(args["dry-run"]) });
|
|
1972
2554
|
}
|
|
1973
2555
|
});
|
|
1974
2556
|
var focusCommand = defineCommand({
|
|
@@ -2063,9 +2645,14 @@ var statusCommand = defineCommand({
|
|
|
2063
2645
|
var checkCommand = defineCommand({
|
|
2064
2646
|
meta: {
|
|
2065
2647
|
name: "check",
|
|
2066
|
-
description: "Validate changes (frontmatter, sections, deltas, scenarios)"
|
|
2648
|
+
description: "Validate changes (frontmatter, sections, template text, deltas, scenarios)"
|
|
2067
2649
|
},
|
|
2068
2650
|
args: {
|
|
2651
|
+
focused: {
|
|
2652
|
+
type: "boolean",
|
|
2653
|
+
description: "Only validate currently focused changes",
|
|
2654
|
+
default: false
|
|
2655
|
+
},
|
|
2069
2656
|
json: {
|
|
2070
2657
|
type: "boolean",
|
|
2071
2658
|
description: "Print machine-readable JSON output",
|
|
@@ -2078,7 +2665,11 @@ var checkCommand = defineCommand({
|
|
|
2078
2665
|
}
|
|
2079
2666
|
},
|
|
2080
2667
|
async run({ args }) {
|
|
2081
|
-
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
|
+
});
|
|
2082
2673
|
if (!result.ok)
|
|
2083
2674
|
process.exit(1);
|
|
2084
2675
|
}
|
|
@@ -2107,14 +2698,80 @@ var doctorCommand = defineCommand({
|
|
|
2107
2698
|
type: "boolean",
|
|
2108
2699
|
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
2109
2700
|
default: false
|
|
2701
|
+
},
|
|
2702
|
+
fix: {
|
|
2703
|
+
type: "boolean",
|
|
2704
|
+
description: "Run safe deterministic repairs before reporting diagnostics",
|
|
2705
|
+
default: false
|
|
2110
2706
|
}
|
|
2111
2707
|
},
|
|
2112
2708
|
async run({ args }) {
|
|
2113
|
-
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) });
|
|
2114
2710
|
if (!result.ok)
|
|
2115
2711
|
process.exit(1);
|
|
2116
2712
|
}
|
|
2117
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
|
+
});
|
|
2118
2775
|
async function runCli(rawArgs = process.argv.slice(2)) {
|
|
2119
2776
|
const version = await getVersion();
|
|
2120
2777
|
const main = defineCommand({
|
|
@@ -2130,6 +2787,8 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
2130
2787
|
focus: focusCommand,
|
|
2131
2788
|
unfocus: unfocusCommand,
|
|
2132
2789
|
archive: archiveCommand,
|
|
2790
|
+
ready: readyCommand,
|
|
2791
|
+
show: showCommand,
|
|
2133
2792
|
status: statusCommand,
|
|
2134
2793
|
check: checkCommand,
|
|
2135
2794
|
update: updateCommand,
|