@oevery/rsp 3.1.0-beta.0 → 3.1.0-beta.2

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.
@@ -1,947 +1,39 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/core/config.ts
4
- import { readFile as readFile2 } from "fs/promises";
5
- import { dirname as dirname3, join as join4 } from "path";
6
- import { fileURLToPath } from "url";
7
- import pc from "picocolors";
8
-
9
- // src/core/decisions.ts
10
- import { existsSync } from "fs";
11
- import { mkdir, readdir as readdir2, realpath, stat } from "fs/promises";
12
- import { dirname, isAbsolute, join as join2, posix, relative, resolve, sep } from "path";
13
-
14
- // src/core/managed-path.ts
15
- import { lstatSync } from "fs";
16
- import { readdir, writeFile } from "fs/promises";
17
- import { join } from "path";
18
- var ManagedPathError = class extends Error {
19
- constructor(path, message) {
20
- super(message);
21
- this.path = path;
22
- this.name = "ManagedPathError";
23
- }
24
- path;
25
- };
26
- function inspectManagedDirectory(path, label, options = {}) {
27
- try {
28
- const stats = lstatSync(path);
29
- if (stats.isDirectory())
30
- return { exists: true, issue: null };
31
- return {
32
- exists: true,
33
- issue: new ManagedPathError(path, `${label} must be a real directory: ${path}`)
34
- };
35
- } catch (error) {
36
- if (error.code === "ENOENT") {
37
- if (options.allowMissing)
38
- return { exists: false, issue: null };
39
- return {
40
- exists: false,
41
- issue: new ManagedPathError(path, `${label} must be a real directory: ${path}`)
42
- };
43
- }
44
- const message = error instanceof Error ? error.message : String(error);
45
- return {
46
- exists: false,
47
- issue: new ManagedPathError(path, `unable to inspect ${label} ${path}: ${message}`)
48
- };
49
- }
50
- }
51
- function requireManagedDirectory(path, label, options = {}) {
52
- const inspection = inspectManagedDirectory(path, label, options);
53
- if (inspection.issue)
54
- throw inspection.issue;
55
- return inspection.exists;
56
- }
57
- function inspectManagedFile(path, label, options = {}) {
58
- try {
59
- const stats = lstatSync(path);
60
- if (stats.isFile())
61
- return { exists: true, issue: null };
62
- return {
63
- exists: true,
64
- issue: new ManagedPathError(path, `${label} must be a regular file: ${path}`)
65
- };
66
- } catch (error) {
67
- if (error.code === "ENOENT") {
68
- if (options.allowMissing)
69
- return { exists: false, issue: null };
70
- return {
71
- exists: false,
72
- issue: new ManagedPathError(path, `${label} must be a regular file: ${path}`)
73
- };
74
- }
75
- const message = error instanceof Error ? error.message : String(error);
76
- return {
77
- exists: false,
78
- issue: new ManagedPathError(path, `unable to inspect ${label} ${path}: ${message}`)
79
- };
80
- }
81
- }
82
- function requireManagedFile(path, label, options = {}) {
83
- const inspection = inspectManagedFile(path, label, options);
84
- if (inspection.issue)
85
- throw inspection.issue;
86
- return inspection.exists;
87
- }
88
- async function ensureManagedFile(path, content, label) {
89
- if (requireManagedFile(path, label, { allowMissing: true }))
90
- return false;
91
- await writeFile(path, content, { flag: "wx" });
92
- return true;
93
- }
94
- async function writeManagedFile(path, content, label) {
95
- const exists = requireManagedFile(path, label, { allowMissing: true });
96
- if (exists)
97
- await writeFile(path, content);
98
- else
99
- await writeFile(path, content, { flag: "wx" });
100
- }
101
- function resolveManagedDirectoryChain(root, segments, label) {
102
- requireManagedDirectory(root, `${label} root`);
103
- let current = root;
104
- for (const segment of segments) {
105
- if (!segment || segment === "." || segment === ".." || segment.includes("/") || segment.includes("\\"))
106
- throw new ManagedPathError(current, `${label} contains an invalid directory segment: ${segment}`);
107
- current = join(current, segment);
108
- requireManagedDirectory(current, label, { allowMissing: true });
109
- }
110
- return current;
111
- }
112
- async function inspectManagedFileTree(root, label, options = {}) {
113
- const result = { rootExists: false, files: [], issues: [] };
114
- const rootInspection = inspectManagedDirectory(root, `${label} root`, options);
115
- if (rootInspection.issue) {
116
- result.issues.push(rootInspection.issue);
117
- return result;
118
- }
119
- if (!rootInspection.exists)
120
- return result;
121
- result.rootExists = true;
122
- await collectManagedFiles(root, label, result);
123
- result.files.sort();
124
- return result;
125
- }
126
- async function collectManagedFiles(directory, label, result) {
127
- let entries;
128
- try {
129
- entries = await readdir(directory, { withFileTypes: true });
130
- } catch (error) {
131
- const message = error instanceof Error ? error.message : String(error);
132
- result.issues.push(new ManagedPathError(directory, `unable to inspect the ${label} tree at ${directory}: ${message}`));
133
- return;
134
- }
135
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
136
- if (entry.name.startsWith("."))
137
- continue;
138
- const path = join(directory, entry.name);
139
- if (entry.isDirectory()) {
140
- await collectManagedFiles(path, label, result);
141
- continue;
142
- }
143
- if (entry.isFile()) {
144
- result.files.push(path);
145
- continue;
146
- }
147
- result.issues.push(new ManagedPathError(path, `unsupported entry in the ${label} tree: ${path}`));
148
- }
149
- }
150
-
151
- // src/core/decisions.ts
152
- var DEFAULT_DECISION_RECORDS_PATH = ".rsp/specs/decisions";
153
- function normalizeDecisionRecordsPath(pathValue) {
154
- return posix.normalize(pathValue.trim().replace(/\\/g, "/")).replace(/^\.\//, "").replace(/\/$/, "");
155
- }
156
- function validateDecisionRecordsPath(pathValue) {
157
- if (typeof pathValue !== "string" || pathValue.trim() === "")
158
- return 'config.yaml field "decisions.path" must be a non-empty string';
159
- const raw = pathValue.trim();
160
- const portable = raw.replace(/\\/g, "/");
161
- if (isAbsolute(raw) || posix.isAbsolute(portable) || /^[a-z]:[\\/]/i.test(raw))
162
- return 'config.yaml field "decisions.path" must be project-relative';
163
- const normalized = normalizeDecisionRecordsPath(raw);
164
- if (normalized === "." || normalized === ".." || normalized.startsWith("../"))
165
- return 'config.yaml field "decisions.path" must stay inside the Host Project';
166
- if ((normalized === ".rsp" || normalized.startsWith(".rsp/")) && normalized !== DEFAULT_DECISION_RECORDS_PATH)
167
- return `config.yaml field "decisions.path" must use ${DEFAULT_DECISION_RECORDS_PATH} or a path outside .rsp/`;
168
- return null;
169
- }
170
- function getDecisionRecordsConfigIssue(config) {
171
- if (!("decisions" in config))
172
- return null;
173
- const decisions = config.decisions;
174
- if (!decisions || typeof decisions !== "object" || Array.isArray(decisions))
175
- return 'config.yaml field "decisions" must be a mapping with one path';
176
- const keys = Object.keys(decisions);
177
- if (keys.length !== 1 || keys[0] !== "path")
178
- return 'config.yaml field "decisions" supports only "path"';
179
- return validateDecisionRecordsPath(decisions.path);
180
- }
181
- function resolveDecisionRecordsPath(config) {
182
- const configured = config.decisions?.path;
183
- return configured && validateDecisionRecordsPath(configured) === null ? normalizeDecisionRecordsPath(configured) : DEFAULT_DECISION_RECORDS_PATH;
184
- }
185
- async function ensureDecisionRecordsDirectory(pathValue) {
186
- const filesystemError = await validateDecisionRecordsFilesystemPath(pathValue);
187
- if (filesystemError)
188
- throw new Error(filesystemError);
189
- const existed = existsSync(pathValue);
190
- await mkdir(pathValue, { recursive: true });
191
- const entries = await readdir2(pathValue);
192
- if (entries.length === 0) {
193
- await ensureManagedFile(join2(pathValue, ".gitkeep"), "", "Decision Record placeholder");
194
- return true;
195
- }
196
- if (entries.includes(".gitkeep"))
197
- requireManagedFile(join2(pathValue, ".gitkeep"), "Decision Record placeholder");
198
- return !existed;
199
- }
200
- async function validateDecisionRecordsFilesystemPath(pathValue) {
201
- const projectRoot = await realpath(".");
202
- let existingAncestor = resolve(pathValue);
203
- while (!existsSync(existingAncestor)) {
204
- const parent = dirname(existingAncestor);
205
- if (parent === existingAncestor)
206
- return `Decision Record path "${pathValue}" has no accessible Host Project ancestor`;
207
- existingAncestor = parent;
208
- }
209
- const resolvedAncestor = await realpath(existingAncestor);
210
- const rel = relative(projectRoot, resolvedAncestor);
211
- if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
212
- return `Decision Record path "${pathValue}" resolves outside the Host Project through ${existingAncestor}`;
213
- if (existsSync(resolve(pathValue)) && !(await stat(resolve(pathValue))).isDirectory())
214
- return `Decision Record path "${pathValue}" must be a directory`;
215
- return null;
216
- }
217
-
218
- // src/core/helpers.ts
219
- import { existsSync as existsSync2 } from "fs";
220
- import { readdir as readdir3, readFile, rmdir } from "fs/promises";
221
- import { basename, dirname as dirname2, join as join3, relative as relative2, sep as sep2 } from "path";
222
- import { parse } from "yaml";
223
- var RSP_AGENTS_BEGIN = "<!-- rsp:begin -->";
224
- var RSP_AGENTS_END = "<!-- rsp:end -->";
225
- function parseYamlText(text) {
226
- const parsed = parse(text);
227
- if (parsed === null || parsed === void 0)
228
- return {};
229
- if (typeof parsed !== "object" || Array.isArray(parsed))
230
- throw new Error("YAML document must be a mapping/object");
231
- return parsed;
232
- }
233
- function parseFrontmatter(content) {
234
- const m = content.match(/^---\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)\r?\n/);
235
- if (!m)
236
- return null;
237
- return parseYamlText(m[1]);
238
- }
239
- function countCheckboxes(content) {
240
- const todo = (content.match(/\[ \]/g) || []).length;
241
- const progress = (content.match(/\[\/\]/g) || []).length;
242
- const done = (content.match(/\[x\]/g) || []).length;
243
- const dropped = (content.match(/\[-\]/g) || []).length;
244
- return { todo, progress, done, dropped, total: todo + progress + done + dropped };
245
- }
246
- async function walkMarkdownFiles(dir, options = {}) {
247
- const all = await walkFiles(dir, options);
248
- return all.filter((f) => f.endsWith(".md"));
249
- }
250
- async function inspectUnsupportedRules() {
251
- const rulesDir = join3(RSP_DIR, "rules");
252
- const diagnostics = [];
253
- const rawEntries = await walkAllEntries(rulesDir, diagnostics);
254
- const entries = rawEntries.filter((entry) => entry.path !== OBSOLETE_RSP_RULES_PATH).map((entry) => ({
255
- path: normalizeLogicalPath(relative2(rulesDir, entry.path)),
256
- kind: entry.kind
257
- }));
258
- const obsoleteFallbackPresent = existsSync2(OBSOLETE_RSP_RULES_PATH);
259
- return {
260
- entries,
261
- diagnostics,
262
- directoryRemaining: existsSync2(rulesDir) && (!obsoleteFallbackPresent || entries.length > 0)
263
- };
264
- }
265
- async function walkAllEntries(dir, diagnostics) {
266
- const entries = [];
267
- try {
268
- const items = await readdir3(dir, { withFileTypes: true });
269
- for (const item of items) {
270
- const path = join3(dir, item.name);
271
- const kind = item.isDirectory() ? "directory" : "file";
272
- entries.push({ path, kind });
273
- if (kind === "directory")
274
- entries.push(...await walkAllEntries(path, diagnostics));
275
- }
276
- } catch (error) {
277
- if (error.code !== "ENOENT") {
278
- diagnostics.push({
279
- code: "walk_failed",
280
- operation: "readdir",
281
- path: dir,
282
- message: toErrorMessage(error)
283
- });
284
- }
285
- }
286
- return entries;
287
- }
288
- async function walkFiles(dir, options = {}) {
289
- const files = [];
290
- try {
291
- const items = await readdir3(dir, { withFileTypes: true });
292
- for (const item of items) {
293
- if (item.name.startsWith("."))
294
- continue;
295
- const filePath = join3(dir, item.name);
296
- if (item.isDirectory())
297
- files.push(...await walkFiles(filePath, options));
298
- else
299
- files.push(filePath);
300
- }
301
- } catch (error) {
302
- if (error.code !== "ENOENT") {
303
- options.onError?.({
304
- code: "walk_failed",
305
- operation: "readdir",
306
- path: dir,
307
- message: toErrorMessage(error)
308
- });
309
- }
310
- }
311
- return files;
312
- }
313
- function normalizeLogicalPath(pathValue) {
314
- return pathValue.split(sep2).join("/").replace(/\\/g, "/");
315
- }
316
- function generateChangeContent(name, summary = "", kind, options = {}) {
317
- const proposalSummary = summary || (name === "project-setup" ? "Capture the project model, boundaries, and stable local constraints" : "<one-line summary>");
318
- if (name === "project-setup") {
319
- return `---
320
- kind: ops
321
- ---
322
-
323
- # Change: project-setup
324
-
325
- ## Proposal
326
- - Outcome: ${proposalSummary}
327
- - Why:
328
- - Establish a durable project model before normal implementation work starts
329
- - Scope:
330
- - Review the repository structure, entrypoints, and primary outputs
331
- - Fill .rsp/specs/design.md with durable architecture facts
332
- - Add stable navigation, context, instructions, or validation steps to project-owned CONTEXT.md or the nearest AGENTS.md when those conventions exist
333
- - Non-goals:
334
- - Do not duplicate durable project facts across this change, specs, and AGENTS.md
335
-
336
- ## Spec
337
- ### ADDED
338
- - Requirement: project bootstrap capture
339
- - The repository's purpose, scope, and structure are reflected in .rsp/specs/design.md
340
-
341
- ### MODIFIED
342
- - Requirement: stable project-owned context and instructions
343
- - Stable navigation and context are reflected in project-owned CONTEXT.md when that convention exists
344
- - Stable agent instructions are reflected in the nearest project-owned AGENTS.md when needed
345
-
346
- ### Acceptance
347
- #### Scenario: project model captured
348
- - GIVEN an initialized RSP project
349
- - WHEN project setup is completed
350
- - THEN .rsp/specs/design.md reflects durable project facts
351
- - AND project-owned CONTEXT.md and the nearest AGENTS.md contain only the stable context and instructions assigned to them by Host Project conventions
352
-
353
- ## Design
354
- - Approach:
355
- - Keep bootstrap work in this single Change while returning durable outcomes to their existing project owners
356
- - Boundaries:
357
- - Specs own stable current facts; project-owned CONTEXT.md and AGENTS.md retain their navigation, context, and instruction roles
358
- - Affected areas:
359
- - .rsp/specs/design.md
360
- - Project-owned CONTEXT.md or AGENTS.md when applicable
361
- - Durable outcome targets:
362
- - Current facts: .rsp/specs/design.md, existing scoped Specs, CONTEXT.md, or AGENTS.md as applicable
363
- - Lasting rationale: one Decision Record under the configured authoritative path, only when a hard-to-reverse tradeoff exists
364
- - Constraints:
365
- - Keep the setup lightweight and avoid duplicating durable project facts
366
-
367
- ## Tasks
368
- - [ ] Review the repository structure, entrypoints, and primary outputs
369
- - [ ] Review project-owned CONTEXT.md and AGENTS.md and confirm the RSP entry points to the right project files
370
- - [ ] Fill .rsp/specs/design.md with durable architecture facts
371
- - [ ] Add stable project-owned context, instructions, or validation steps only where the Host Project convention requires them
372
-
373
- ## Verify
374
- - Automated:
375
- - [ ] Run rsp doctor \u2014 proves: the initialized RSP workspace is structurally valid
376
- - Manual or environment:
377
- - [ ] Review .rsp/specs/design.md and project-owned context or instructions and confirm they match the repository
378
- - Coverage:
379
- - none
380
-
381
- ## Blockers
382
- - none
383
- `;
384
- }
385
- const frontmatterKind = kind ?? "<choose: feature | fix | refactor | docs | ops | research>";
386
- if (options.lite)
387
- return generateLiteChangeContent(name, proposalSummary, frontmatterKind);
388
- const template = getChangeTemplateByKind(kind);
389
- return `---
390
- kind: "${frontmatterKind}"
391
- ---
392
-
393
- # Change: ${name}
394
-
395
- ## Proposal
396
- - Outcome: ${proposalSummary}
397
- - Why:
398
- - ${template.why}
399
- - Scope:
400
- - ${template.scope}
401
- - Non-goals:
402
- - ${template.nonGoals}
403
-
404
- ## Spec
405
- ${template.specSection}
406
-
407
- ### Acceptance
408
- ${template.acceptanceSection}
409
-
410
- ## Design
411
- - Approach:
412
- - ${template.approach}
413
- - Boundaries:
414
- - <module, interface, data, document, or ownership boundary affected>
415
- - Affected areas:
416
- - ${template.affectedArea1}
417
- - ${template.affectedArea2}
418
- - Constraints:
419
- - ${template.constraints}
420
-
421
- ## Tasks
422
- - [ ] ${template.task}
423
-
424
- ## Verify
425
- - Automated:
426
- - [ ] ${template.automatedVerify} \u2014 proves: <behavior or scope covered>
427
- - Manual or environment:
428
- - [ ] ${template.manualVerify}
429
- - Coverage:
430
- - <omitted or unavailable relevant coverage and reason, or none>
431
-
432
- ## Blockers
433
- - none
434
- `;
435
- }
436
- function generateLiteChangeContent(name, proposalSummary, frontmatterKind) {
437
- return `---
438
- kind: "${frontmatterKind}"
439
- ---
440
-
441
- # Change: ${name}
442
-
443
- ## Proposal
444
- - Outcome: ${proposalSummary}
445
- - Why:
446
- - <why this small change matters>
447
- - Scope:
448
- - <what will change>
449
- - Non-goals:
450
- - none
451
-
452
- ## Spec
453
- ### MODIFIED
454
- - Requirement: <observable outcome>
455
- - <what should be true after this change>
456
-
457
- ### Acceptance
458
- #### Scenario: change is complete
459
- - GIVEN <current context>
460
- - WHEN <the change is applied>
461
- - THEN <expected outcome>
462
-
463
- ## Design
464
- - Approach:
465
- - <minimal implementation approach>
466
- - Boundaries:
467
- - <primary ownership or interface boundary>
468
- - Affected areas:
469
- - <primary file, directory, or doc path>
470
- - Constraints:
471
- - <important constraint, or none>
472
-
473
- ## Tasks
474
- - [ ] Implement the small change
475
-
476
- ## Verify
477
- - Automated:
478
- - [ ] <cheapest decisive check; retain a new test only when justified, or not applicable: reason> \u2014 proves: <behavior or scope covered>
479
- - Manual or environment:
480
- - [ ] <exact acceptance scenario, or unavailable/not applicable: owner and reason>
481
- - Coverage:
482
- - <omitted or unavailable relevant coverage and reason, or none>
483
-
484
- ## Blockers
485
- - none
486
- `;
487
- }
488
- function getChangeTemplateByKind(kind) {
489
- const automatedVerify = "<cheapest decisive check; retain a new test only when justified, or not applicable: reason>";
490
- switch (kind) {
491
- case "feature":
492
- return {
493
- why: "<what user need or capability gap this addresses>",
494
- scope: "<what new behavior or capability will be delivered>",
495
- nonGoals: "<what related capabilities are explicitly out of scope>",
496
- 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>",
497
- acceptanceSection: "#### Scenario: user exercises the new capability\n- GIVEN <context>\n- WHEN <user action>\n- THEN <expected new behavior>",
498
- approach: "<how the new capability will be implemented>",
499
- affectedArea1: "<concrete file path, directory, module, or subsystem 1>",
500
- affectedArea2: "<concrete file path, directory, module, or subsystem 2 if needed>",
501
- constraints: "<behavior, compatibility, performance, safety, or scope constraint that must hold>",
502
- task: "<implement the new behavior and its acceptance scenario in the affected areas>",
503
- automatedVerify,
504
- manualVerify: "<exact end-to-end user scenario to validate the new capability, or unavailable/not applicable: owner and reason>"
505
- };
506
- case "fix":
507
- return {
508
- why: "<what current behavior is wrong or broken>",
509
- scope: "<what specific defect is being corrected>",
510
- nonGoals: "<what related but separate issues are out of scope>",
511
- specSection: "<!-- Describe expected correct behavior. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: correct behavior\n - <expected correct behavior after the fix>",
512
- 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",
513
- approach: "<confirmed cause and correction strategy, or exact evidence needed to establish the cause>",
514
- affectedArea1: "<concrete file path, directory, module, or subsystem containing the defect>",
515
- affectedArea2: "<secondary path, dependency, or test file if needed>",
516
- constraints: "<regression, backward-compatibility, safety, or scope constraint that must hold>",
517
- task: "<identify the root cause and apply the minimal fix>",
518
- automatedVerify,
519
- manualVerify: "<exact steps to reproduce the original issue and confirm it no longer occurs, or unavailable/not applicable: owner and reason>"
520
- };
521
- case "refactor":
522
- return {
523
- why: "<what maintainability, readability, or structural issue motivates this>",
524
- scope: "<what code areas are being restructured>",
525
- nonGoals: "<no behavior change is expected or intended>",
526
- 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>",
527
- 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",
528
- approach: "<how the code will be restructured (rename, extract, move, etc.)>",
529
- affectedArea1: "<concrete file path, directory, module, or subsystem being refactored>",
530
- affectedArea2: "<secondary path, dependency, or test file if needed>",
531
- constraints: "<observable behavior must remain unchanged; compatibility, performance, and safety constraints must still hold>",
532
- task: "<restructure the code while keeping behavior identical>",
533
- automatedVerify,
534
- manualVerify: "<exact scenario to spot-check that behavior is unchanged after the refactor, or unavailable/not applicable: owner and reason>"
535
- };
536
- case "docs":
537
- return {
538
- why: "<why the documentation change matters>",
539
- scope: "<which docs, readers, or workflows will be updated>",
540
- nonGoals: "<what documentation behavior or audience is out of scope>",
541
- 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>",
542
- 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",
543
- approach: "<how the documentation will be updated and organized>",
544
- affectedArea1: "<concrete doc path, directory, module doc, or documentation surface 1>",
545
- affectedArea2: "<secondary doc path or reference file if needed>",
546
- constraints: "<durable wording, consistency, scope, or audience constraint that must hold>",
547
- task: "<update the target documentation and supporting references>",
548
- automatedVerify,
549
- manualVerify: "<exact reader or operator scenario to confirm the updated guidance is accurate, or unavailable/not applicable: owner and reason>"
550
- };
551
- case "research":
552
- return {
553
- why: "<what question or uncertainty this research resolves>",
554
- scope: "<what system area, option set, or hypothesis is being examined>",
555
- nonGoals: "<what implementation work is explicitly out of scope>",
556
- 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>",
557
- 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",
558
- approach: "<how the investigation will be performed and what evidence will be gathered>",
559
- affectedArea1: "<concrete file path, directory, module, subsystem, or source under investigation>",
560
- affectedArea2: "<secondary path, dependency, environment, or evidence source if needed>",
561
- constraints: "<time, evidence, safety, or scope constraint for the investigation>",
562
- task: "<gather evidence, record the recommendation or finding, and note any follow-up implementation work if needed>",
563
- automatedVerify,
564
- manualVerify: "<exact review of the findings to confirm they answer the original question and identify follow-up work if needed, or unavailable/not applicable: owner and reason>"
565
- };
566
- case "ops":
567
- return {
568
- why: "<why this operational or environment change matters>",
569
- scope: "<what environment, workflow, or operational path changes>",
570
- nonGoals: "<what product or feature behavior will not change>",
571
- 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>",
572
- 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",
573
- approach: "<how the operational change will be applied safely>",
574
- affectedArea1: "<concrete script, config path, workflow, environment path, or operational surface 1>",
575
- affectedArea2: "<secondary script, config path, dependency, or environment surface if needed>",
576
- constraints: "<rollback, safety, reliability, environment, or scope constraint that must hold>",
577
- task: "<apply and verify the operational change>",
578
- automatedVerify,
579
- manualVerify: "<exact operator workflow or environment scenario to confirm the operational result, or unavailable/not applicable: owner and reason>"
580
- };
581
- default:
582
- return {
583
- why: "<why this change matters>",
584
- scope: "<what this change will do>",
585
- nonGoals: "<what this change will not do>",
586
- specSection: "<!-- Describe observable behavior and requirements. Implementation notes belong in ## Design. -->\n### ADDED\n- Requirement: <new or updated behavior>\n - <verifiable requirement>",
587
- acceptanceSection: "#### Scenario: <name>\n- GIVEN <context>\n- WHEN <action>\n- THEN <expected outcome>",
588
- approach: "<how the change will be implemented>",
589
- affectedArea1: "<concrete file path, directory, module, or subsystem 1>",
590
- affectedArea2: "<concrete file path, directory, module, or subsystem 2 if needed>",
591
- constraints: "<behavior, compatibility, performance, safety, or scope constraint that must hold>",
592
- task: "<implement the core change in the affected areas>",
593
- automatedVerify,
594
- manualVerify: "<exact acceptance scenario, or unavailable/not applicable: owner and reason>"
595
- };
596
- }
597
- }
598
- function renderRspAgentsBlock() {
599
- return `${RSP_AGENTS_BEGIN}
600
- ## RSP Entry
601
-
602
- RSP tracks current work, stable specs, and archives under \`.rsp/\`.
603
-
604
- Read in order:
605
- 1. Nearest \`AGENTS.md\` for project or module instructions.
606
- 2. Root \`CONTEXT-MAP.md\` if present, then the relevant nearest \`CONTEXT.md\`.
607
- 3. The \`rsp\` skill; if unavailable, read \`.rsp/rsp-rules.md\` as the fallback protocol.
608
- 4. \`.rsp/focus.d/\`; for grouped work read the sibling Group Brief, then the explicitly selected focused Change.
609
- 5. Only the relevant Specs and Decision Records under the configured authoritative path.
610
-
611
- 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.
612
- Do not treat \`.rsp/specs/\` or \`.rsp/changes/\` as replacements for nearest \`AGENTS.md\` or \`CONTEXT.md\`.
613
- ${RSP_AGENTS_END}`;
614
- }
615
- function upsertRspAgentsBlock(content) {
616
- const block = renderRspAgentsBlock();
617
- const managedRe = /<!-- rsp:begin -->[\s\S]*?<!-- rsp:end -->/;
618
- if (managedRe.test(content)) {
619
- const next2 = content.replace(managedRe, block);
620
- return { content: next2, changed: next2 !== content };
621
- }
622
- const trimmed = content.trimStart();
623
- const next = trimmed ? `${block}
624
-
625
- ${trimmed}` : `${block}
626
- `;
627
- return { content: next, changed: true };
628
- }
629
- function hasRspAgentsBlock(content) {
630
- return content.includes(RSP_AGENTS_BEGIN) && content.includes(RSP_AGENTS_END);
631
- }
632
- var SPEC_NAME_RE = /^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
633
- function isValidSpecName(name) {
634
- return SPEC_NAME_RE.test(name);
635
- }
636
- function guardRspInitialized() {
637
- const designPath = join3(RSP_DIR, "specs", "design.md");
638
- const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
639
- const design = inspectManagedFile(designPath, "design Spec", { allowMissing: true });
640
- if (rules.issue || design.issue || !rules.exists || !design.exists) {
641
- const initialized = existsSync2(RSP_DIR);
642
- console.error(` ${pc.red("Error:")} ${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}`);
643
- console.error(` ${pc.dim(initialized ? "Run: rsp update" : "Run: rsp init")}`);
644
- process.exit(1);
645
- }
646
- }
647
- function getOpenCheckboxes(sectionText) {
648
- return sectionText.split("\n").map((line) => line.trim()).filter((line) => /^- \[[ /]\]/.test(line));
649
- }
650
- async function cleanupEmptyParentDirs(path, stopDir) {
651
- let parent = dirname2(path);
652
- while (parent !== stopDir) {
653
- try {
654
- const remaining = await readdir3(parent);
655
- if (remaining.length === 0)
656
- await rmdir(parent);
657
- else
658
- break;
659
- } catch {
660
- break;
661
- }
662
- parent = dirname2(parent);
663
- }
664
- }
665
- async function detectProjectName() {
666
- try {
667
- if (existsSync2("package.json")) {
668
- const raw = await readFile("package.json", "utf-8");
669
- const pkg = JSON.parse(raw);
670
- if (pkg.name && pkg.name !== "")
671
- return pkg.name;
672
- }
673
- } catch {
674
- }
675
- return basename(process.cwd());
676
- }
677
- function generateDesignContent(projectName) {
678
- return `# Project Design: ${projectName}
679
-
680
- ## Purpose
681
- - <what this project is responsible for>
682
- - <who or what it serves>
683
-
684
- ## Stable Facts
685
- - <durable fact that future agents or developers must know>
686
- - <key architectural invariant>
687
-
688
- ## Boundaries
689
- - In scope:
690
- - <major capability or boundary>
691
- - Out of scope:
692
- - <intentional non-goal>
693
-
694
- ## Structure
695
- - <important directory or subsystem> \u2014 <responsibility>
696
-
697
- ## Constraints
698
- - <cross-cutting technical or operational constraint, when it affects architecture>
699
- `;
700
- }
701
- function generateSpecContent(name) {
702
- const title = toTitleCase(name);
703
- return `# ${title}
704
-
705
- ## Purpose
706
- - <why this project-level spec exists>
707
-
708
- ## Stable Facts
709
- - <durable fact that future work must know>
710
-
711
- ## Boundaries
712
- - In scope:
713
- - <what this spec covers>
714
- - Out of scope:
715
- - <what this spec does not cover>
716
-
717
- ## Constraints
718
- - <stable constraint, if any>
719
- `;
720
- }
721
- function toTitleCase(value) {
722
- return value.split(/[-/]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
723
- }
724
- function extractSection(content, heading) {
725
- const lines = content.split("\n");
726
- const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
727
- if (start === -1)
728
- return "";
729
- const body = [];
730
- for (let index = start + 1; index < lines.length; index++) {
731
- const line = lines[index];
732
- if (line.startsWith("## "))
733
- break;
734
- body.push(line);
735
- }
736
- return body.join("\n").trim();
737
- }
738
- function extractBlockerLines(content) {
739
- const uncommented = content.replace(/<!--[\s\S]*?-->/g, "");
740
- return extractSection(uncommented, "Blockers").split("\n").map((line) => line.trim()).filter(Boolean);
741
- }
742
- function hasMeaningfulBlockers(content) {
743
- const lines = extractBlockerLines(content);
744
- if (lines.length === 0)
745
- return false;
746
- return lines.some((line) => !/^[-*]\s*(?:none)?$/i.test(line) && !/^none$/i.test(line));
747
- }
748
- function detectDeltaSections(content) {
749
- const body = extractSection(content, "Spec");
750
- return {
751
- added: /^###\s*ADDED/im.test(body),
752
- modified: /^###\s*MODIFIED/im.test(body),
753
- removed: /^###\s*REMOVED/im.test(body)
754
- };
755
- }
756
- function collectArchiveReadiness(content, options = {}) {
757
- const warnings = [];
758
- const tasksSection = extractSection(content, "Tasks");
759
- const verifySection = extractSection(content, "Verify");
760
- const taskTodos = getOpenCheckboxes(tasksSection);
761
- if (taskTodos.length > 0)
762
- warnings.push(`${taskTodos.length} task item(s) still incomplete`);
763
- const verifyTodos = getOpenCheckboxes(verifySection);
764
- if (verifyTodos.length > 0)
765
- warnings.push(`${verifyTodos.length} Verify checklist item(s) are still incomplete`);
766
- const activeBlockers = options.activeBlockers ?? hasMeaningfulBlockers(content);
767
- if (activeBlockers)
768
- warnings.push("active blockers are present in the change file");
769
- const scenarios = parseScenarios(content);
770
- const missingScenarios = scenarios.length === 0;
771
- if (missingScenarios)
772
- warnings.push("no Scenario blocks found (some changes do not need them)");
773
- const archiveReady = activeBlockers ? "no" : warnings.length === 0 ? "yes" : "judgment";
774
- return {
775
- taskTodos,
776
- verifyTodos,
777
- activeBlockers,
778
- scenarioCount: scenarios.length,
779
- missingScenarios,
780
- deterministic: warnings.length === 0 ? "pass" : "warnings",
781
- semantic: "needs-review",
782
- archiveReady,
783
- warnings
784
- };
785
- }
786
- function buildDurableReviewGuidance(factCandidateTargets, decisionRecordsPath) {
787
- return {
788
- required: true,
789
- factDecisions: [
790
- "No current-fact update needed",
791
- "Update existing spec or scoped instruction",
792
- "Create a new durable spec"
793
- ],
794
- rationaleDecisions: [
795
- "No Decision Record needed",
796
- "Create or update a Decision Record"
797
- ],
798
- factCandidateTargets,
799
- decisionRecordsPath,
800
- note: "Semantic review decides current-fact and lasting-rationale updates independently. The CLI never infers scoped instruction or Decision Record filenames and never promotes Change content automatically."
801
- };
802
- }
803
- function getDurableReviewCandidateTargets() {
804
- return [".rsp/specs/design.md"];
805
- }
806
- function collectArchiveChecklist(content, options = {}) {
807
- return collectArchiveReadiness(content, options).warnings;
808
- }
809
- function parseScenarios(content) {
810
- const scenarios = [];
811
- const regex = /^#{3,4}\s*Scenario:\s*(.+)$/gim;
812
- let match;
813
- while ((match = regex.exec(content)) !== null) {
814
- const heading = match[1].trim();
815
- const startPos = match.index + match[0].length;
816
- const restContent = content.slice(startPos);
817
- const nextSection = restContent.match(/^#{3,4}\s/m);
818
- const block = nextSection ? restContent.slice(0, nextSection.index) : restContent;
819
- const steps = block.split("\n").map((s) => s.trim()).filter((s) => s.startsWith("-") && /GIVEN|WHEN|THEN|AND|BUT/i.test(s)).map((s) => s.replace(/^[-*]\s*/, ""));
820
- if (steps.length > 0)
821
- scenarios.push({ heading, steps });
822
- }
823
- return scenarios;
824
- }
825
-
826
- // src/core/config.ts
827
- var __dirname = dirname3(fileURLToPath(import.meta.url));
828
- var PKG_ROOT = join4(__dirname, "..");
829
- var RSP_DIR = ".rsp";
830
- var RSP_RULES_PATH = join4(RSP_DIR, "rsp-rules.md");
831
- var OBSOLETE_RSP_RULES_PATH = join4(RSP_DIR, "rules", "rsp-rules.md");
832
- var CHANGES_DIR = join4(RSP_DIR, "changes");
833
- var FOCUS_DIR = join4(RSP_DIR, "focus.d");
834
- var ARCHIVES_DIR = join4(RSP_DIR, "archives");
835
- var LOCK_PATH = join4(RSP_DIR, ".lock");
836
- var CONFIG_PATH = join4(RSP_DIR, "config.yaml");
837
- var VALID_KINDS = ["feature", "fix", "refactor", "docs", "ops", "research"];
838
- var DEFAULT_REQUIRED_SECTIONS = ["Proposal", "Spec", "Design", "Tasks", "Verify", "Blockers"];
839
- var _configCache = null;
840
- async function loadRspConfig() {
841
- const inspection = await inspectRspConfig();
842
- if (inspection.issues.length > 0)
843
- throw new Error(inspection.issues.join("; "));
844
- return inspection.config;
845
- }
846
- async function inspectRspConfig() {
847
- const cwd = process.cwd();
848
- if (_configCache?.cwd === cwd)
849
- return _configCache.inspection;
850
- const configFile = inspectManagedFile(CONFIG_PATH, "config file", { allowMissing: true });
851
- if (configFile.issue)
852
- throw configFile.issue;
853
- if (!configFile.exists) {
854
- const inspection2 = { config: {}, issues: [] };
855
- _configCache = { cwd, inspection: inspection2 };
856
- return inspection2;
857
- }
858
- const raw = await readFile2(CONFIG_PATH, "utf-8");
859
- const parsed = parseYamlText(raw);
860
- const issues = validateRspConfig(parsed);
861
- const decisions = parsed.decisions && typeof parsed.decisions === "object" && !Array.isArray(parsed.decisions) ? parsed.decisions : void 0;
862
- const decisionPath = decisions?.path;
863
- const kinds = Array.isArray(parsed.kinds) && parsed.kinds.every((value) => typeof value === "string" && value.trim() !== "") ? parsed.kinds.map((value) => value.trim()) : void 0;
864
- const decisionRecordsIssue = getDecisionRecordsConfigIssue(parsed);
865
- const inspection = {
866
- config: {
867
- kinds,
868
- decisions: decisionRecordsIssue === null && validateDecisionRecordsPath(decisionPath) === null ? { path: normalizeDecisionRecordsPath(String(decisionPath)) } : void 0
869
- },
870
- issues
871
- };
872
- _configCache = { cwd, inspection };
873
- return inspection;
874
- }
875
- function validateRspConfig(parsed) {
876
- const issues = [];
877
- const supported = /* @__PURE__ */ new Set(["kinds", "decisions"]);
878
- for (const key of Object.keys(parsed)) {
879
- if (key === "required_sections") {
880
- issues.push('config.yaml field "required_sections" is no longer supported');
881
- } else if (!supported.has(key)) {
882
- issues.push(`config.yaml field "${key}" is not supported`);
883
- }
884
- }
885
- if ("kinds" in parsed) {
886
- if (!Array.isArray(parsed.kinds)) {
887
- issues.push('config.yaml field "kinds" must be a YAML list');
888
- } else if (!parsed.kinds.every((value) => typeof value === "string" && value.trim() !== "")) {
889
- issues.push('config.yaml field "kinds" must contain only non-empty strings');
890
- } else {
891
- const kinds = parsed.kinds.map((value) => value.trim());
892
- const duplicates = [...new Set(kinds.filter((value, index) => kinds.indexOf(value) !== index))];
893
- if (duplicates.length > 0)
894
- issues.push(`config.yaml field "kinds" contains duplicate entries: ${duplicates.join(", ")}`);
895
- }
896
- }
897
- const decisionRecordsIssue = getDecisionRecordsConfigIssue(parsed);
898
- if (decisionRecordsIssue)
899
- issues.push(decisionRecordsIssue);
900
- return issues;
901
- }
902
- function clearConfigCache() {
903
- _configCache = null;
904
- }
905
- function resolveKinds(config) {
906
- return config.kinds && config.kinds.length > 0 ? config.kinds : [...VALID_KINDS];
907
- }
908
- function resolveRequiredSections(_config) {
909
- return [...DEFAULT_REQUIRED_SECTIONS];
910
- }
911
- async function getVersion() {
912
- const { version } = JSON.parse(await readFile2(join4(PKG_ROOT, "package.json"), "utf-8"));
913
- return version;
914
- }
915
-
916
- // src/core/output.ts
917
- function toErrorMessage(error) {
918
- return error instanceof Error ? error.message : String(error);
919
- }
920
- function emitJson(data, options = {}) {
921
- const indentation = options.compact ? void 0 : 2;
922
- process.stdout.write(`${JSON.stringify(data, null, indentation)}
923
- `);
924
- }
925
- function recordRuntimeDiagnostic(runtime, diagnostic, verbose = false) {
926
- runtime.push(diagnostic);
927
- if (verbose) {
928
- console.error(` ${pc.dim(`[verbose] ${diagnostic.operation} ${diagnostic.path}: ${diagnostic.message}`)}`);
929
- }
930
- }
2
+ import {
3
+ ARCHIVES_DIR,
4
+ CHANGES_DIR,
5
+ CONFIG_PATH,
6
+ FOCUS_DIR,
7
+ ManagedPathError,
8
+ collectArchiveReadiness,
9
+ countCheckboxes,
10
+ extractBlockerLines,
11
+ extractSection,
12
+ hasMeaningfulBlockers,
13
+ inspectManagedDirectory,
14
+ inspectRspConfig,
15
+ normalizeLogicalPath,
16
+ parseFrontmatter,
17
+ parseScenarios,
18
+ requireManagedDirectory,
19
+ requireManagedFile,
20
+ resolveManagePolicy,
21
+ toErrorMessage
22
+ } from "./chunk-OTJFL6XH.mjs";
931
23
 
932
24
  // src/status/inspect.ts
933
- import { existsSync as existsSync3 } from "fs";
934
- import { readFile as readFile5, stat as stat2 } from "fs/promises";
935
- import { join as join7 } from "path";
25
+ import { existsSync } from "fs";
26
+ import { readFile as readFile3, stat } from "fs/promises";
27
+ import { join as join3 } from "path";
936
28
 
937
29
  // src/core/change-group.ts
938
- import { readFile as readFile3 } from "fs/promises";
939
- import { dirname as dirname4, join as join6, relative as relative4 } from "path";
30
+ import { readFile } from "fs/promises";
31
+ import { dirname, join as join2, relative as relative2 } from "path";
940
32
 
941
33
  // src/core/work-ref.ts
942
- import { lstatSync as lstatSync2 } from "fs";
943
- import { readdir as readdir4 } from "fs/promises";
944
- import { isAbsolute as isAbsolute2, join as join5, relative as relative3 } from "path";
34
+ import { lstatSync } from "fs";
35
+ import { readdir } from "fs/promises";
36
+ import { isAbsolute, join, relative } from "path";
945
37
  var WORK_SEGMENT_RE = /^[a-z0-9-]+$/;
946
38
  var GROUP_BRIEF_FILENAME = "00-brief.md";
947
39
  var WorkRefError = class extends Error {
@@ -981,7 +73,7 @@ function resolveWorkRef(name, options = {}) {
981
73
  }
982
74
  assertValidWorkRoot(changesDir, name);
983
75
  assertNoIdentityCollision(changesDir, segments, name, isGroupBrief ? GROUP_BRIEF_FILENAME : void 0);
984
- const path = join5(changesDir, ...segments.slice(0, -1), isGroupBrief ? GROUP_BRIEF_FILENAME : `${segments.at(-1)}.md`);
76
+ const path = join(changesDir, ...segments.slice(0, -1), isGroupBrief ? GROUP_BRIEF_FILENAME : `${segments.at(-1)}.md`);
985
77
  const ref = segments.length === 1 ? { kind: "change", name, path, group: null } : isGroupBrief ? { kind: "group-brief", name, path, group: segments[0] } : { kind: "group-change", name, path, group: segments[0] };
986
78
  if (ref.kind === "group-change" && options.executable !== false)
987
79
  assertGroupBrief(changesDir, ref);
@@ -996,7 +88,7 @@ function resolveWorkRef(name, options = {}) {
996
88
  return ref;
997
89
  }
998
90
  function assertGroupBrief(changesDir, ref) {
999
- const briefPath = join5(changesDir, ref.group, GROUP_BRIEF_FILENAME);
91
+ const briefPath = join(changesDir, ref.group, GROUP_BRIEF_FILENAME);
1000
92
  const kind = getPathKind(briefPath, ref.name);
1001
93
  if (kind === "missing") {
1002
94
  throw new WorkRefError(
@@ -1016,7 +108,7 @@ function assertGroupBrief(changesDir, ref) {
1016
108
  function assertRegularWorkFile(ref, mustExist) {
1017
109
  let stats;
1018
110
  try {
1019
- stats = lstatSync2(ref.path);
111
+ stats = lstatSync(ref.path);
1020
112
  } catch (error) {
1021
113
  if (error.code === "ENOENT") {
1022
114
  if (mustExist) {
@@ -1040,8 +132,8 @@ function assertRegularWorkFile(ref, mustExist) {
1040
132
  }
1041
133
  function resolveWorkRefPath(path, options = {}) {
1042
134
  const changesDir = options.changesDir ?? CHANGES_DIR;
1043
- const logicalPath = normalizeLogicalPath2(relative3(changesDir, path));
1044
- if (isAbsolute2(logicalPath) || logicalPath === ".." || logicalPath.startsWith("../") || !logicalPath.endsWith(".md")) {
135
+ const logicalPath = normalizeLogicalPath2(relative(changesDir, path));
136
+ if (isAbsolute(logicalPath) || logicalPath === ".." || logicalPath.startsWith("../") || !logicalPath.endsWith(".md")) {
1045
137
  throw new WorkRefError(
1046
138
  "invalid_work_ref_path",
1047
139
  `work path must be a Markdown file inside ${changesDir}: ${path}`,
@@ -1067,13 +159,13 @@ function resolveFocusMarkerPath(ref, options = {}) {
1067
159
  assertManagedRoot(focusDir, ref.name, "invalid_focus_root", "focus root", true);
1068
160
  if (ref.group) {
1069
161
  assertOptionalManagedDirectory(
1070
- join5(focusDir, ref.group),
162
+ join(focusDir, ref.group),
1071
163
  ref.name,
1072
164
  "invalid_focus_path",
1073
165
  "focus path"
1074
166
  );
1075
167
  }
1076
- const markerPath = join5(focusDir, ...ref.name.split("/"));
168
+ const markerPath = join(focusDir, ...ref.name.split("/"));
1077
169
  try {
1078
170
  requireManagedFile(markerPath, "focus marker", { allowMissing: true });
1079
171
  } catch (error) {
@@ -1088,7 +180,7 @@ function resolveArchiveDirectory(ref, options = {}) {
1088
180
  assertManagedRoot(archivesDir, ref.name, "invalid_archive_root", "archive root", true);
1089
181
  if (!ref.group)
1090
182
  return archivesDir;
1091
- const groupPath = join5(archivesDir, ref.group);
183
+ const groupPath = join(archivesDir, ref.group);
1092
184
  assertOptionalManagedDirectory(groupPath, ref.name, "invalid_archive_path", "archive path");
1093
185
  return groupPath;
1094
186
  }
@@ -1107,7 +199,7 @@ async function inspectFocusTree(options = {}) {
1107
199
  if (!rootEntries)
1108
200
  return result;
1109
201
  for (const entry of rootEntries) {
1110
- const path = join5(focusDir, entry.name);
202
+ const path = join(focusDir, entry.name);
1111
203
  if (entry.name === ".gitkeep" && entry.isFile())
1112
204
  continue;
1113
205
  if (entry.isDirectory()) {
@@ -1150,7 +242,7 @@ async function inspectArchiveTree(options = {}) {
1150
242
  if (!rootEntries)
1151
243
  return result;
1152
244
  for (const entry of rootEntries) {
1153
- const path = join5(archivesDir, entry.name);
245
+ const path = join(archivesDir, entry.name);
1154
246
  if (entry.name === "INDEX.md" && entry.isFile())
1155
247
  continue;
1156
248
  if (entry.isDirectory()) {
@@ -1188,7 +280,7 @@ async function inspectWorkTree(options = {}) {
1188
280
  const rootMarkdownNames = new Set(rootEntries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name.slice(0, -3)));
1189
281
  const collidingGroups = new Set([...rootDirectories].filter((name) => rootMarkdownNames.has(name)));
1190
282
  for (const entry of rootEntries) {
1191
- const path = join5(changesDir, entry.name);
283
+ const path = join(changesDir, entry.name);
1192
284
  if (entry.name === ".gitkeep" && entry.isFile())
1193
285
  continue;
1194
286
  if (entry.isDirectory()) {
@@ -1235,17 +327,17 @@ async function inspectGroupDirectory(group, groupPath, changesDir, result, colli
1235
327
  "group_brief_missing",
1236
328
  `Change Group "${group}" requires a Group Brief; run: rsp group create ${group}`,
1237
329
  group
1238
- ), join5(groupPath, GROUP_BRIEF_FILENAME));
330
+ ), join(groupPath, GROUP_BRIEF_FILENAME));
1239
331
  } else if (!briefEntry.isFile()) {
1240
332
  addDiagnostic(result, new WorkRefError(
1241
333
  "work_ref_not_file",
1242
- `Group Brief must be a regular file: ${join5(groupPath, GROUP_BRIEF_FILENAME)}`,
334
+ `Group Brief must be a regular file: ${join(groupPath, GROUP_BRIEF_FILENAME)}`,
1243
335
  group
1244
- ), join5(groupPath, GROUP_BRIEF_FILENAME));
336
+ ), join(groupPath, GROUP_BRIEF_FILENAME));
1245
337
  }
1246
338
  for (const entry of entries) {
1247
339
  const input = `${group}/${entry.name}`;
1248
- const path = join5(groupPath, entry.name);
340
+ const path = join(groupPath, entry.name);
1249
341
  if (entry.name === GROUP_BRIEF_FILENAME && !entry.isFile())
1250
342
  continue;
1251
343
  if (entry.name === "brief.md") {
@@ -1274,7 +366,7 @@ async function inspectFocusGroup(group, groupPath, changesDir, result) {
1274
366
  return;
1275
367
  for (const entry of entries) {
1276
368
  const input = `${group}/${entry.name}`;
1277
- const path = join5(groupPath, entry.name);
369
+ const path = join(groupPath, entry.name);
1278
370
  if (entry.isDirectory()) {
1279
371
  addDiagnostic(result, new WorkRefError(
1280
372
  "unsupported_work_depth",
@@ -1300,7 +392,7 @@ async function inspectArchiveGroup(group, groupPath, result) {
1300
392
  return;
1301
393
  for (const entry of entries) {
1302
394
  const input = `${group}/${entry.name}`;
1303
- const path = join5(groupPath, entry.name);
395
+ const path = join(groupPath, entry.name);
1304
396
  if (!entry.isFile() || !entry.name.endsWith(".md")) {
1305
397
  addArchivePathDiagnostic(result, input, path);
1306
398
  continue;
@@ -1333,7 +425,7 @@ function collectResolvedPath(path, changesDir, result) {
1333
425
  }
1334
426
  async function readDirectory(path, input, result) {
1335
427
  try {
1336
- return (await readdir4(path, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
428
+ return (await readdir(path, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
1337
429
  } catch (error) {
1338
430
  addDiagnostic(result, asReadFailure(path, input, error), path);
1339
431
  return null;
@@ -1360,8 +452,8 @@ function addDiagnostic(result, error, path) {
1360
452
  }
1361
453
  function assertNoIdentityCollision(changesDir, segments, input, finalFilename) {
1362
454
  for (let depth = 1; depth <= segments.length; depth++) {
1363
- const identityPath = join5(changesDir, ...segments.slice(0, depth));
1364
- const filePath = finalFilename && depth === segments.length ? join5(changesDir, ...segments.slice(0, -1), finalFilename) : `${identityPath}.md`;
455
+ const identityPath = join(changesDir, ...segments.slice(0, depth));
456
+ const filePath = finalFilename && depth === segments.length ? join(changesDir, ...segments.slice(0, -1), finalFilename) : `${identityPath}.md`;
1365
457
  const fileKind = getPathKind(filePath, input);
1366
458
  const identityKind = getPathKind(identityPath, input);
1367
459
  const isPrefix = depth < segments.length;
@@ -1427,7 +519,7 @@ function assertOptionalManagedDirectory(path, input, code, label) {
1427
519
  }
1428
520
  function getPathKind(path, input) {
1429
521
  try {
1430
- const stats = lstatSync2(path);
522
+ const stats = lstatSync(path);
1431
523
  if (stats.isFile())
1432
524
  return "file";
1433
525
  if (stats.isDirectory())
@@ -1499,12 +591,12 @@ async function inspectChangeGroups(options = {}) {
1499
591
  const archivedGroups = /* @__PURE__ */ new Set();
1500
592
  const openGroupNames = new Set(workTree.briefs.map((brief) => brief.group));
1501
593
  for (const path of archiveTree.files) {
1502
- const archivePath = normalizeLogicalPath(relative4(ARCHIVES_DIR, path));
594
+ const archivePath = normalizeLogicalPath(relative2(ARCHIVES_DIR, path));
1503
595
  const archiveGroup = archivePath.includes("/") ? archivePath.split("/")[0] : null;
1504
596
  if (!archiveGroup || !openGroupNames.has(archiveGroup))
1505
597
  continue;
1506
598
  try {
1507
- const content = await readFile3(path, "utf-8");
599
+ const content = await readFile(path, "utf-8");
1508
600
  const heading = content.match(/^# Change:\s+(\S+)\s*$/m);
1509
601
  if (heading?.[1]?.includes("/")) {
1510
602
  const identity = heading[1];
@@ -1537,7 +629,7 @@ async function inspectChangeGroups(options = {}) {
1537
629
  for (const brief of workTree.briefs) {
1538
630
  let content;
1539
631
  try {
1540
- content = await readFile3(brief.path, "utf-8");
632
+ content = await readFile(brief.path, "utf-8");
1541
633
  } catch (error) {
1542
634
  diagnostics.push({
1543
635
  severity: "error",
@@ -1562,7 +654,7 @@ async function inspectChangeGroups(options = {}) {
1562
654
  for (const diagnostic of archiveTree.diagnostics.filter((item) => {
1563
655
  if (item.code === "invalid_archive_root")
1564
656
  return true;
1565
- const archivePath = normalizeLogicalPath(relative4(ARCHIVES_DIR, item.path));
657
+ const archivePath = normalizeLogicalPath(relative2(ARCHIVES_DIR, item.path));
1566
658
  return archivePath === brief.group || archivePath.startsWith(`${brief.group}/`);
1567
659
  })) {
1568
660
  diagnostics.push({
@@ -1642,10 +734,10 @@ async function inspectChangeGroups(options = {}) {
1642
734
  return { groups, diagnostics };
1643
735
  }
1644
736
  async function requireDeclaredGroupSlice(ref) {
1645
- const briefPath = join6(dirname4(ref.path), GROUP_BRIEF_FILENAME);
737
+ const briefPath = join2(dirname(ref.path), GROUP_BRIEF_FILENAME);
1646
738
  let content;
1647
739
  try {
1648
- content = await readFile3(briefPath, "utf-8");
740
+ content = await readFile(briefPath, "utf-8");
1649
741
  } catch (error) {
1650
742
  const message = error instanceof Error ? error.message : String(error);
1651
743
  throw new WorkRefError("work_tree_read_failed", `unable to read Group Brief ${briefPath}: ${message}`, ref.name);
@@ -1675,11 +767,11 @@ async function hasArchivedGroupBrief(group) {
1675
767
  }
1676
768
  const groupPrefix = `${group}/`;
1677
769
  for (const path of archiveTree.files) {
1678
- if (!normalizeLogicalPath(relative4(ARCHIVES_DIR, path)).startsWith(groupPrefix))
770
+ if (!normalizeLogicalPath(relative2(ARCHIVES_DIR, path)).startsWith(groupPrefix))
1679
771
  continue;
1680
772
  let content;
1681
773
  try {
1682
- content = await readFile3(path, "utf-8");
774
+ content = await readFile(path, "utf-8");
1683
775
  } catch (error) {
1684
776
  const message = error instanceof Error ? error.message : String(error);
1685
777
  throw new WorkRefError("work_tree_read_failed", `unable to inspect archived Change Group identity at ${path}: ${message}`, group);
@@ -1751,7 +843,7 @@ function escapeRegExp(value) {
1751
843
  }
1752
844
 
1753
845
  // src/core/dependency-plan.ts
1754
- import { readFile as readFile4 } from "fs/promises";
846
+ import { readFile as readFile2 } from "fs/promises";
1755
847
  var NONE_BLOCKER_RE = /^[-*]\s*(?:none)?$/i;
1756
848
  var REQUIRES_RE = /^[-*]\s+requires\s+`([^`]+)`:[ \t]*(\S.*)$/i;
1757
849
  var EXECUTABLE_WORK_REF_RE = /^[a-z0-9-]+(?:\/(?!(?:brief|00-brief)$)[a-z0-9-]+)?$/;
@@ -1770,7 +862,7 @@ async function inspectChangeDependencies(options = {}) {
1770
862
  }));
1771
863
  for (const path of archiveTree.files) {
1772
864
  try {
1773
- const heading = (await readFile4(path, "utf-8")).match(/^# Change:\s+(\S+)\s*$/m);
865
+ const heading = (await readFile2(path, "utf-8")).match(/^# Change:\s+(\S+)\s*$/m);
1774
866
  if (heading)
1775
867
  archivedNames.add(heading[1]);
1776
868
  } catch (error) {
@@ -1784,7 +876,7 @@ async function inspectChangeDependencies(options = {}) {
1784
876
  }
1785
877
  for (const brief of workTree.briefs) {
1786
878
  try {
1787
- if (hasMeaningfulBlockers(await readFile4(brief.path, "utf-8")))
879
+ if (hasMeaningfulBlockers(await readFile2(brief.path, "utf-8")))
1788
880
  blockedGroups.add(brief.group);
1789
881
  } catch (error) {
1790
882
  diagnostics.push({
@@ -1798,7 +890,7 @@ async function inspectChangeDependencies(options = {}) {
1798
890
  }
1799
891
  for (const ref of workTree.changes) {
1800
892
  try {
1801
- const parsed = parseDependencyBlockers(await readFile4(ref.path, "utf-8"));
893
+ const parsed = parseDependencyBlockers(await readFile2(ref.path, "utf-8"));
1802
894
  if (ref.group && blockedGroups.has(ref.group))
1803
895
  parsed.external = true;
1804
896
  parsedByName.set(ref.name, parsed);
@@ -2016,6 +1108,28 @@ function findCycleDiagnostics(edges) {
2016
1108
  async function inspectProjectStatus(options = {}) {
2017
1109
  const runtime = [];
2018
1110
  const diagnostics = [];
1111
+ let manage = resolveManagePolicy({});
1112
+ try {
1113
+ const configInspection = await inspectRspConfig();
1114
+ const configValid = configInspection.issues.length === 0;
1115
+ manage = resolveManagePolicy(configInspection.config, { configValid });
1116
+ if (!configValid) {
1117
+ diagnostics.push({
1118
+ severity: "error",
1119
+ code: "invalid_config",
1120
+ path: CONFIG_PATH,
1121
+ message: configInspection.issues.join("; ")
1122
+ });
1123
+ }
1124
+ } catch (error) {
1125
+ manage = resolveManagePolicy({}, { configValid: false });
1126
+ diagnostics.push({
1127
+ severity: "error",
1128
+ code: "invalid_config",
1129
+ path: CONFIG_PATH,
1130
+ message: toErrorMessage(error)
1131
+ });
1132
+ }
2019
1133
  const focusTree = await inspectFocusTree();
2020
1134
  for (const diagnostic of focusTree.diagnostics) {
2021
1135
  diagnostics.push({
@@ -2079,7 +1193,7 @@ async function inspectProjectStatus(options = {}) {
2079
1193
  let progressKnown = false;
2080
1194
  if (path) {
2081
1195
  try {
2082
- const content = await readFile5(path, "utf-8");
1196
+ const content = await readFile3(path, "utf-8");
2083
1197
  title = extractChangeTitle(content) ?? name;
2084
1198
  blockerEntries = extractBlockerEntries(content);
2085
1199
  try {
@@ -2126,7 +1240,7 @@ async function inspectProjectStatus(options = {}) {
2126
1240
  });
2127
1241
  }
2128
1242
  try {
2129
- const metadata = await stat2(path);
1243
+ const metadata = await stat(path);
2130
1244
  ageDays = Math.floor((nowMs - metadata.mtime.getTime()) / (1e3 * 60 * 60 * 24));
2131
1245
  } catch (error) {
2132
1246
  diagnostics.push({
@@ -2178,6 +1292,7 @@ async function inspectProjectStatus(options = {}) {
2178
1292
  });
2179
1293
  }
2180
1294
  return {
1295
+ manage,
2181
1296
  focused: [...focusedSet].sort(),
2182
1297
  records,
2183
1298
  groups: groupInspection.groups,
@@ -2206,11 +1321,11 @@ function extractBlockerEntries(content) {
2206
1321
  return extractSection(content, "Blockers").split("\n").map((line) => line.trim()).filter((line) => /^[-*]\s+/.test(line)).map((line) => line.replace(/^[-*]\s+/, "").trim()).filter((line) => line.toLowerCase() !== "none");
2207
1322
  }
2208
1323
  async function readArchiveTrend(runtime) {
2209
- const indexPath = join7(ARCHIVES_DIR, "INDEX.md");
2210
- if (!existsSync3(indexPath))
1324
+ const indexPath = join3(ARCHIVES_DIR, "INDEX.md");
1325
+ if (!existsSync(indexPath))
2211
1326
  return [];
2212
1327
  try {
2213
- const content = await readFile5(indexPath, "utf-8");
1328
+ const content = await readFile3(indexPath, "utf-8");
2214
1329
  const dateRe = /^\| (\d{4}-\d{2})-\d{2} \|/gm;
2215
1330
  const counts = {};
2216
1331
  let match;
@@ -2254,8 +1369,8 @@ var ArchiveHistoryError = class extends Error {
2254
1369
  };
2255
1370
 
2256
1371
  // src/history/inspect.ts
2257
- import { readFile as readFile6 } from "fs/promises";
2258
- import { basename as basename2, dirname as dirname5, relative as relative5 } from "path";
1372
+ import { readFile as readFile4 } from "fs/promises";
1373
+ import { basename, dirname as dirname2, relative as relative3 } from "path";
2259
1374
  var EXECUTABLE_WORK_REF_RE2 = /^[a-z0-9-]+(?:\/[a-z0-9-]+)?$/;
2260
1375
  var GROUP_NAME_RE = /^[a-z0-9-]+$/;
2261
1376
  var ARCHIVE_NAME_RE = /^(\d{4}-\d{2}-\d{2})_(.+)\.md$/;
@@ -2282,7 +1397,7 @@ async function inspectArchiveHistory(options = {}) {
2282
1397
  const path = archiveOutputPath(sourcePath, archivesDir);
2283
1398
  let content;
2284
1399
  try {
2285
- content = await readFile6(sourcePath, "utf-8");
1400
+ content = await readFile4(sourcePath, "utf-8");
2286
1401
  } catch (error) {
2287
1402
  const message = toErrorMessage(error);
2288
1403
  diagnostics.push({ severity: "error", code: "archive_read_failed", path, message: `unable to read archived work: ${message}` });
@@ -2311,13 +1426,13 @@ async function inspectArchiveHistory(options = {}) {
2311
1426
  };
2312
1427
  }
2313
1428
  function parseArchiveEntry(sourcePath, path, archivesDir, content) {
2314
- const nameMatch = basename2(sourcePath).match(ARCHIVE_NAME_RE);
1429
+ const nameMatch = basename(sourcePath).match(ARCHIVE_NAME_RE);
2315
1430
  if (!nameMatch || !isCalendarDate(nameMatch[1])) {
2316
1431
  return { diagnostic: errorDiagnostic("archive_name_invalid", path, "archive filename must use a valid YYYY-MM-DD_name.md date") };
2317
1432
  }
2318
1433
  const date = nameMatch[1];
2319
1434
  const archiveStem = nameMatch[2];
2320
- const relativeDirectory = normalizeLogicalPath(relative5(archivesDir, dirname5(sourcePath)));
1435
+ const relativeDirectory = normalizeLogicalPath(relative3(archivesDir, dirname2(sourcePath)));
2321
1436
  const archiveGroup = relativeDirectory === "." || relativeDirectory === "" ? null : relativeDirectory;
2322
1437
  if (archiveGroup && !GROUP_NAME_RE.test(archiveGroup))
2323
1438
  return { diagnostic: errorDiagnostic("archive_identity_mismatch", path, "archive group path is not a valid one-segment Group identity") };
@@ -2392,10 +1507,10 @@ function isCalendarDate(value) {
2392
1507
  return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
2393
1508
  }
2394
1509
  function archiveOutputPath(sourcePath, archivesDir) {
2395
- return `.rsp/archives/${normalizeLogicalPath(relative5(archivesDir, sourcePath))}`;
1510
+ return `.rsp/archives/${normalizeLogicalPath(relative3(archivesDir, sourcePath))}`;
2396
1511
  }
2397
1512
  function normalizeOutputPath(path, archivesDir) {
2398
- const relativePath = normalizeLogicalPath(relative5(archivesDir, path));
1513
+ const relativePath = normalizeLogicalPath(relative3(archivesDir, path));
2399
1514
  return relativePath === "" ? ".rsp/archives" : `.rsp/archives/${relativePath}`;
2400
1515
  }
2401
1516
  function errorDiagnostic(code, path, message) {
@@ -2412,7 +1527,7 @@ function compareDiagnostics(left, right) {
2412
1527
  }
2413
1528
 
2414
1529
  // src/history/query.ts
2415
- import { readFile as readFile7 } from "fs/promises";
1530
+ import { readFile as readFile5 } from "fs/promises";
2416
1531
  function queryArchiveHistory(records, query = {}) {
2417
1532
  validateArchiveHistoryQuery(query);
2418
1533
  const limit = query.limit ?? HISTORY_DEFAULT_LIMIT;
@@ -2456,7 +1571,7 @@ function selectArchiveHistoryRecord(records, selector) {
2456
1571
  async function readArchiveHistoryDetail(record) {
2457
1572
  let content;
2458
1573
  try {
2459
- content = await readFile7(record.sourcePath, "utf-8");
1574
+ content = await readFile5(record.sourcePath, "utf-8");
2460
1575
  } catch (error) {
2461
1576
  throw new ArchiveHistoryError("archive_read_failed", `unable to read archived Change ${record.path}: ${error instanceof Error ? error.message : String(error)}`);
2462
1577
  }
@@ -2538,60 +1653,6 @@ function projectDependencyForest(plan, requestedRoots = defaultRoots(plan)) {
2538
1653
  }
2539
1654
 
2540
1655
  export {
2541
- ManagedPathError,
2542
- inspectManagedDirectory,
2543
- requireManagedDirectory,
2544
- inspectManagedFile,
2545
- ensureManagedFile,
2546
- writeManagedFile,
2547
- resolveManagedDirectoryChain,
2548
- inspectManagedFileTree,
2549
- DEFAULT_DECISION_RECORDS_PATH,
2550
- resolveDecisionRecordsPath,
2551
- ensureDecisionRecordsDirectory,
2552
- validateDecisionRecordsFilesystemPath,
2553
- toErrorMessage,
2554
- emitJson,
2555
- recordRuntimeDiagnostic,
2556
- parseFrontmatter,
2557
- countCheckboxes,
2558
- walkMarkdownFiles,
2559
- inspectUnsupportedRules,
2560
- normalizeLogicalPath,
2561
- generateChangeContent,
2562
- upsertRspAgentsBlock,
2563
- hasRspAgentsBlock,
2564
- isValidSpecName,
2565
- guardRspInitialized,
2566
- cleanupEmptyParentDirs,
2567
- detectProjectName,
2568
- generateDesignContent,
2569
- generateSpecContent,
2570
- extractSection,
2571
- hasMeaningfulBlockers,
2572
- detectDeltaSections,
2573
- collectArchiveReadiness,
2574
- buildDurableReviewGuidance,
2575
- getDurableReviewCandidateTargets,
2576
- collectArchiveChecklist,
2577
- parseScenarios,
2578
- pc,
2579
- PKG_ROOT,
2580
- RSP_DIR,
2581
- RSP_RULES_PATH,
2582
- OBSOLETE_RSP_RULES_PATH,
2583
- CHANGES_DIR,
2584
- FOCUS_DIR,
2585
- ARCHIVES_DIR,
2586
- LOCK_PATH,
2587
- CONFIG_PATH,
2588
- VALID_KINDS,
2589
- loadRspConfig,
2590
- inspectRspConfig,
2591
- clearConfigCache,
2592
- resolveKinds,
2593
- resolveRequiredSections,
2594
- getVersion,
2595
1656
  GROUP_BRIEF_FILENAME,
2596
1657
  WorkRefError,
2597
1658
  resolveWorkRef,