@oevery/rsp 3.1.1 → 3.3.0

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.
Files changed (40) hide show
  1. package/README.md +4 -3
  2. package/README.zh-CN.md +3 -2
  3. package/dist/{chunk-WJXKLLIF.mjs → chunk-7HXQNBIG.mjs} +447 -734
  4. package/dist/{chunk-3Z4IO2TD.mjs → chunk-QM2UVPQK.mjs} +47 -30
  5. package/dist/{chunk-MUNLLVVH.mjs → chunk-TSUDEX5Y.mjs} +1205 -117
  6. package/dist/cli.mjs +5336 -3045
  7. package/dist/entry-GIDNR7CJ.mjs +1846 -0
  8. package/dist/{entry-KAWA2AS4.mjs → entry-YAW3PYFA.mjs} +4 -4
  9. package/package.json +12 -7
  10. package/rules/rsp-rules.md +30 -58
  11. package/skills/rsp/SKILL.md +28 -45
  12. package/skills/rsp/references/control-outcome.md +31 -0
  13. package/skills/rsp/references/durable-review.md +23 -4
  14. package/skills/rsp/references/focus-continuation.md +9 -0
  15. package/skills/rsp/references/implementation-evidence.md +12 -0
  16. package/skills/rsp/references/managed-routing.md +23 -18
  17. package/skills/rsp/references/response-language.md +11 -0
  18. package/skills/rsp/references/setup-repair.md +3 -3
  19. package/skills/rsp-commit/SKILL.md +26 -9
  20. package/skills/rsp-design/SKILL.md +1 -1
  21. package/skills/rsp-diagnose/SKILL.md +4 -2
  22. package/skills/rsp-implement/SKILL.md +18 -6
  23. package/skills/rsp-manage/SKILL.md +61 -37
  24. package/skills/rsp-manage/references/closeout.md +13 -0
  25. package/skills/rsp-manage/references/delegation.md +47 -0
  26. package/skills/rsp-manage/references/interruption-recovery.md +19 -0
  27. package/skills/rsp-manage/references/review-convergence.md +9 -0
  28. package/skills/rsp-release-docs/SKILL.md +15 -13
  29. package/skills/rsp-release-docs/references/publication-lifecycle.md +4 -0
  30. package/skills/rsp-resolve-findings/SKILL.md +3 -3
  31. package/skills/rsp-review/SKILL.md +10 -36
  32. package/skills/rsp-review/references/code-review.md +15 -0
  33. package/skills/rsp-review/references/document-review.md +15 -0
  34. package/skills/rsp-shape/SKILL.md +14 -4
  35. package/skills/rsp-shape/references/deep-clarification.md +2 -0
  36. package/skills/rsp-structural-audit/SKILL.md +1 -1
  37. package/skills/rsp-tdd/SKILL.md +11 -3
  38. package/skills/rsp-verify/SKILL.md +40 -0
  39. package/dist/entry-IPHJBI53.mjs +0 -882
  40. package/skills/rsp/references/release-operations.md +0 -9
@@ -1,219 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/core/config.ts
4
- import { readFile as readFile2 } from "fs/promises";
5
- import { dirname as dirname3, join as join4 } from "path";
4
+ import { readFile } from "fs/promises";
5
+ import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import pc from "picocolors";
8
+ import { parseDocument, stringify, visit } from "yaml";
8
9
 
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
- }
10
+ // src/core/content.ts
11
+ import { parse } from "yaml";
217
12
 
218
13
  // src/core/document-model.ts
219
14
  var CHANGE_DOCUMENT_SCHEMA = {
@@ -371,13 +166,7 @@ function lineBreakStart(source, headingStart) {
371
166
  return headingStart;
372
167
  }
373
168
 
374
- // src/core/helpers.ts
375
- import { existsSync as existsSync2 } from "fs";
376
- import { readdir as readdir3, readFile, rmdir } from "fs/promises";
377
- import { basename, dirname as dirname2, join as join3, relative as relative2, sep as sep2 } from "path";
378
- import { parse } from "yaml";
379
- var RSP_AGENTS_BEGIN = "<!-- rsp:begin -->";
380
- var RSP_AGENTS_END = "<!-- rsp:end -->";
169
+ // src/core/content.ts
381
170
  function parseYamlText(text) {
382
171
  const parsed = parse(text);
383
172
  if (parsed === null || parsed === void 0)
@@ -399,518 +188,457 @@ function countCheckboxes(content) {
399
188
  const dropped = (content.match(/\[-\]/g) || []).length;
400
189
  return { todo, progress, done, dropped, total: todo + progress + done + dropped };
401
190
  }
402
- async function walkMarkdownFiles(dir, options = {}) {
403
- const all = await walkFiles(dir, options);
404
- return all.filter((f) => f.endsWith(".md"));
405
- }
406
- async function inspectUnsupportedRules() {
407
- const rulesDir = join3(RSP_DIR, "rules");
408
- const diagnostics = [];
409
- const rawEntries = await walkAllEntries(rulesDir, diagnostics);
410
- const entries = rawEntries.filter((entry) => entry.path !== OBSOLETE_RSP_RULES_PATH).map((entry) => ({
411
- path: normalizeLogicalPath(relative2(rulesDir, entry.path)),
412
- kind: entry.kind
413
- }));
414
- const obsoleteFallbackPresent = existsSync2(OBSOLETE_RSP_RULES_PATH);
191
+ function classifyVerifyCheckboxes(sectionText) {
192
+ const buckets = collectVerifyCheckboxLines(sectionText);
415
193
  return {
416
- entries,
417
- diagnostics,
418
- directoryRemaining: existsSync2(rulesDir) && (!obsoleteFallbackPresent || entries.length > 0)
194
+ required: countCheckboxes([...buckets.required, ...buckets.unclassified].join("\n")),
195
+ optional: countCheckboxes(buckets.optional.join("\n")),
196
+ unclassified: countCheckboxes(buckets.unclassified.join("\n")),
197
+ legacy: buckets.legacy
419
198
  };
420
199
  }
421
- async function walkAllEntries(dir, diagnostics) {
422
- const entries = [];
423
- try {
424
- const items = await readdir3(dir, { withFileTypes: true });
425
- for (const item of items) {
426
- const path = join3(dir, item.name);
427
- const kind = item.isDirectory() ? "directory" : "file";
428
- entries.push({ path, kind });
429
- if (kind === "directory")
430
- entries.push(...await walkAllEntries(path, diagnostics));
431
- }
432
- } catch (error) {
433
- if (error.code !== "ENOENT") {
434
- diagnostics.push({
435
- code: "walk_failed",
436
- operation: "readdir",
437
- path: dir,
438
- message: toErrorMessage(error)
439
- });
200
+ function collectVerifyCheckboxLines(sectionText) {
201
+ const required = [];
202
+ const optional = [];
203
+ const unclassified = [];
204
+ let current = "unclassified";
205
+ let sawClassificationHeading = false;
206
+ for (const line of sectionText.split("\n")) {
207
+ const heading = line.match(/^###\s+(Required|Optional)\s*$/i);
208
+ if (heading) {
209
+ current = heading[1].toLowerCase();
210
+ sawClassificationHeading = true;
211
+ continue;
440
212
  }
213
+ if (!/^- \[[ /x-]\]/i.test(line.trim()))
214
+ continue;
215
+ if (current === "required")
216
+ required.push(line);
217
+ else if (current === "optional")
218
+ optional.push(line);
219
+ else
220
+ unclassified.push(line);
441
221
  }
442
- return entries;
222
+ return { required, optional, unclassified, legacy: unclassified.length > 0 || !sawClassificationHeading };
443
223
  }
444
- async function walkFiles(dir, options = {}) {
445
- const files = [];
446
- try {
447
- const items = await readdir3(dir, { withFileTypes: true });
448
- for (const item of items) {
449
- if (item.name.startsWith("."))
450
- continue;
451
- const filePath = join3(dir, item.name);
452
- if (item.isDirectory())
453
- files.push(...await walkFiles(filePath, options));
454
- else
455
- files.push(filePath);
456
- }
457
- } catch (error) {
458
- if (error.code !== "ENOENT") {
459
- options.onError?.({
460
- code: "walk_failed",
461
- operation: "readdir",
462
- path: dir,
463
- message: toErrorMessage(error)
464
- });
465
- }
466
- }
467
- return files;
224
+ function getOpenCheckboxes(sectionText) {
225
+ return sectionText.split("\n").map((line) => line.trim()).filter((line) => /^- \[[ /]\]/.test(line));
468
226
  }
469
- function normalizeLogicalPath(pathValue) {
470
- return pathValue.split(sep2).join("/").replace(/\\/g, "/");
227
+ function extractBlockerLines(content) {
228
+ const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
229
+ return normalizeBlockerBody(getDocumentSectionBody(document, "blockers"));
471
230
  }
472
- function generateChangeContent(name, summary = "", kind, options = {}) {
473
- const placeholder = "<\u2026>";
474
- const proposalSummary = summary || placeholder;
475
- const issueFrontmatter = renderIssueFrontmatter(options.issues ?? []);
476
- if (name === "project-setup") {
477
- return `---
478
- kind: ops
479
- ${issueFrontmatter}---
480
-
481
- ${renderDocumentTitle(CHANGE_DOCUMENT_SCHEMA, "project-setup")}
482
-
483
- ${changeSectionHeading("proposal")}
484
- - Outcome: ${proposalSummary}
485
- - Why:
486
- - ${placeholder}
487
- - Scope:
488
- - ${placeholder}
489
- - Non-goals:
490
- - ${placeholder}
491
-
492
- ${changeSectionHeading("spec")}
493
- ### ADDED
494
- - Requirement: ${placeholder}
495
- - ${placeholder}
496
-
497
- ### Acceptance
498
- #### Scenario: ${placeholder}
499
- - GIVEN ${placeholder}
500
- - WHEN ${placeholder}
501
- - THEN ${placeholder}
502
-
503
- ${changeSectionHeading("design")}
504
- - Approach:
505
- - ${placeholder}
506
- - Boundaries:
507
- - ${placeholder}
508
- - Affected areas:
509
- - .rsp/specs/design.md
510
- - CONTEXT.md
511
- - AGENTS.md
512
- - Durable outcome targets:
513
- - Current facts: ${placeholder}
514
- - Lasting rationale: ${placeholder}
515
- - Constraints:
516
- - ${placeholder}
517
-
518
- ${changeSectionHeading("tasks")}
519
- - [ ] .rsp/specs/design.md: ${placeholder}
520
- - [ ] CONTEXT.md: ${placeholder}
521
- - [ ] AGENTS.md: ${placeholder}
522
-
523
- ${changeSectionHeading("verify")}
524
- - Automated:
525
- - [ ] rsp doctor \u2014 proves: ${placeholder}
526
- - Manual or environment:
527
- - [ ] ${placeholder}
528
- - Coverage:
529
- - ${placeholder}
530
-
531
- ${changeSectionHeading("blockers")}
532
- - none
533
- `;
534
- }
535
- const frontmatterKind = kind ?? "<choose: feature | fix | refactor | docs | ops | research>";
536
- if (options.lite)
537
- return generateLiteChangeContent(name, proposalSummary, frontmatterKind, issueFrontmatter);
538
- const template = getChangeTemplateByKind(kind);
539
- return `---
540
- kind: "${frontmatterKind}"
541
- ${issueFrontmatter}---
542
-
543
- ${renderDocumentTitle(CHANGE_DOCUMENT_SCHEMA, name)}
544
-
545
- ${changeSectionHeading("proposal")}
546
- - Outcome: ${proposalSummary}
547
- - Why:
548
- - ${template.why}
549
- - Scope:
550
- - ${template.scope}
551
- - Non-goals:
552
- - ${template.nonGoals}
553
-
554
- ${changeSectionHeading("spec")}
555
- ${template.specSection}
556
-
557
- ### Acceptance
558
- ${template.acceptanceSection}
559
-
560
- ${changeSectionHeading("design")}
561
- - Approach:
562
- - ${template.approach}
563
- - Boundaries:
564
- - ${placeholder}
565
- - Affected areas:
566
- - ${template.affectedArea1}
567
- - ${template.affectedArea2}
568
- - Constraints:
569
- - ${template.constraints}
570
-
571
- ${changeSectionHeading("tasks")}
572
- - [ ] ${template.task}
573
-
574
- ${changeSectionHeading("verify")}
575
- - Automated:
576
- - [ ] ${template.automatedVerify} \u2014 proves: ${placeholder}
577
- - Manual or environment:
578
- - [ ] ${template.manualVerify}
579
- - Coverage:
580
- - ${placeholder}
581
-
582
- ${changeSectionHeading("blockers")}
583
- - none
584
- `;
231
+ function hasMeaningfulBlockerBody(body) {
232
+ return hasMeaningfulBlockerLines(normalizeBlockerBody(body));
585
233
  }
586
- function generateLiteChangeContent(name, proposalSummary, frontmatterKind, issueFrontmatter) {
587
- const placeholder = "<\u2026>";
588
- return `---
589
- kind: "${frontmatterKind}"
590
- ${issueFrontmatter}---
591
-
592
- ${renderDocumentTitle(CHANGE_DOCUMENT_SCHEMA, name)}
593
-
594
- ${changeSectionHeading("proposal")}
595
- - Outcome: ${proposalSummary}
596
- - Why:
597
- - ${placeholder}
598
- - Scope:
599
- - ${placeholder}
600
- - Non-goals:
601
- - none
602
-
603
- ${changeSectionHeading("spec")}
604
- ### MODIFIED
605
- - Requirement: ${placeholder}
606
- - ${placeholder}
607
-
608
- ### Acceptance
609
- #### Scenario: ${placeholder}
610
- - GIVEN ${placeholder}
611
- - WHEN ${placeholder}
612
- - THEN ${placeholder}
613
-
614
- ${changeSectionHeading("design")}
615
- - Approach:
616
- - ${placeholder}
617
- - Boundaries:
618
- - ${placeholder}
619
- - Affected areas:
620
- - ${placeholder}
621
- - Constraints:
622
- - ${placeholder}
623
-
624
- ${changeSectionHeading("tasks")}
625
- - [ ] ${placeholder}
626
-
627
- ${changeSectionHeading("verify")}
628
- - Automated:
629
- - [ ] ${placeholder} \u2014 proves: ${placeholder}
630
- - Manual or environment:
631
- - [ ] ${placeholder}
632
- - Coverage:
633
- - ${placeholder}
634
-
635
- ${changeSectionHeading("blockers")}
636
- - none
637
- `;
234
+ function normalizeBlockerBody(body) {
235
+ return body.replace(/<!--[\s\S]*?-->/g, "").split("\n").map((line) => line.trim()).filter(Boolean);
638
236
  }
639
- function renderIssueFrontmatter(issues) {
640
- if (issues.length === 0)
641
- return "";
642
- return `issues:
643
- ${issues.map((issue) => ` - url: ${JSON.stringify(issue.url)}
644
- relation: ${issue.relation}`).join("\n")}
645
- `;
237
+ function hasMeaningfulBlockers(content) {
238
+ return hasMeaningfulBlockerLines(extractBlockerLines(content));
239
+ }
240
+ function isEmptyBlockerLine(line) {
241
+ const normalized = line.trim();
242
+ return /^none[.。]?$/i.test(normalized) || /^[-*]\s*(?:none[.。]?)?$/i.test(normalized);
243
+ }
244
+ function hasMeaningfulBlockerLines(lines) {
245
+ if (lines.length === 0)
246
+ return false;
247
+ return lines.some((line) => !isEmptyBlockerLine(line));
646
248
  }
647
- function getChangeTemplateByKind(kind) {
648
- const placeholder = "<\u2026>";
649
- const delta = kind === "feature" || kind === "research" || kind === void 0 ? "ADDED" : "MODIFIED";
249
+ function detectDeltaSections(content) {
250
+ const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
251
+ const body = getDocumentSectionBody(document, "spec");
650
252
  return {
651
- why: placeholder,
652
- scope: placeholder,
653
- nonGoals: placeholder,
654
- specSection: `### ${delta}
655
- - Requirement: ${placeholder}
656
- - ${placeholder}`,
657
- acceptanceSection: `#### Scenario: ${placeholder}
658
- - GIVEN ${placeholder}
659
- - WHEN ${placeholder}
660
- - THEN ${placeholder}`,
661
- approach: placeholder,
662
- affectedArea1: placeholder,
663
- affectedArea2: placeholder,
664
- constraints: placeholder,
665
- task: placeholder,
666
- automatedVerify: placeholder,
667
- manualVerify: placeholder
253
+ added: /^###\s*ADDED/im.test(body),
254
+ modified: /^###\s*MODIFIED/im.test(body),
255
+ removed: /^###\s*REMOVED/im.test(body)
668
256
  };
669
257
  }
670
- function changeSectionHeading(sectionId) {
671
- return renderDocumentSectionHeading(CHANGE_DOCUMENT_SCHEMA, sectionId);
258
+ function parseScenarios(content) {
259
+ const scenarios = [];
260
+ const regex = /^#{3,4}\s*Scenario:\s*(.+)$/gim;
261
+ let match;
262
+ while ((match = regex.exec(content)) !== null) {
263
+ const heading = match[1].trim();
264
+ const startPos = match.index + match[0].length;
265
+ const restContent = content.slice(startPos);
266
+ const nextSection = restContent.match(/^#{3,4}\s/m);
267
+ const block = nextSection ? restContent.slice(0, nextSection.index) : restContent;
268
+ 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*/, ""));
269
+ if (steps.length > 0)
270
+ scenarios.push({ heading, steps });
271
+ }
272
+ return scenarios;
273
+ }
274
+
275
+ // src/core/decisions.ts
276
+ import { existsSync } from "fs";
277
+ import { mkdir, readdir as readdir2, realpath, stat } from "fs/promises";
278
+ import { dirname, isAbsolute, join as join2, posix, relative, resolve, sep } from "path";
279
+
280
+ // src/core/managed-path.ts
281
+ import { lstatSync } from "fs";
282
+ import { readdir, writeFile } from "fs/promises";
283
+ import { join } from "path";
284
+ var ManagedPathError = class extends Error {
285
+ constructor(path, message) {
286
+ super(message);
287
+ this.path = path;
288
+ this.name = "ManagedPathError";
289
+ }
290
+ path;
291
+ };
292
+ function inspectManagedDirectory(path, label, options = {}) {
293
+ try {
294
+ const stats = lstatSync(path);
295
+ if (stats.isDirectory())
296
+ return { exists: true, issue: null };
297
+ return {
298
+ exists: true,
299
+ issue: new ManagedPathError(path, `${label} must be a real directory: ${path}`)
300
+ };
301
+ } catch (error) {
302
+ if (error.code === "ENOENT") {
303
+ if (options.allowMissing)
304
+ return { exists: false, issue: null };
305
+ return {
306
+ exists: false,
307
+ issue: new ManagedPathError(path, `${label} must be a real directory: ${path}`)
308
+ };
309
+ }
310
+ const message = error instanceof Error ? error.message : String(error);
311
+ return {
312
+ exists: false,
313
+ issue: new ManagedPathError(path, `unable to inspect ${label} ${path}: ${message}`)
314
+ };
315
+ }
672
316
  }
673
- function renderRspAgentsBlock() {
674
- return `${RSP_AGENTS_BEGIN}
675
- ## RSP Entry
676
-
677
- RSP tracks current work, stable specs, and archives under \`.rsp/\`.
678
-
679
- Read in order:
680
- 1. Nearest \`AGENTS.md\` for project or module instructions.
681
- 2. Root \`CONTEXT-MAP.md\` if present, then the relevant nearest \`CONTEXT.md\`.
682
- 3. The \`rsp\` skill; if unavailable, read \`.rsp/rsp-rules.md\` as the fallback protocol.
683
- 4. \`.rsp/focus.d/\`; for grouped work read the sibling Group Brief, then the explicitly selected focused Change.
684
- 5. Only the relevant Specs and Decision Records under the configured authoritative path.
685
-
686
- 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.
687
- Do not treat \`.rsp/specs/\` or \`.rsp/changes/\` as replacements for nearest \`AGENTS.md\` or \`CONTEXT.md\`.
688
- ${RSP_AGENTS_END}`;
317
+ function requireManagedDirectory(path, label, options = {}) {
318
+ const inspection = inspectManagedDirectory(path, label, options);
319
+ if (inspection.issue)
320
+ throw inspection.issue;
321
+ return inspection.exists;
689
322
  }
690
- function upsertRspAgentsBlock(content) {
691
- const block = renderRspAgentsBlock();
692
- const managedRe = /<!-- rsp:begin -->[\s\S]*?<!-- rsp:end -->/;
693
- if (managedRe.test(content)) {
694
- const next2 = content.replace(managedRe, block);
695
- return { content: next2, changed: next2 !== content };
323
+ function inspectManagedFile(path, label, options = {}) {
324
+ try {
325
+ const stats = lstatSync(path);
326
+ if (stats.isFile())
327
+ return { exists: true, issue: null };
328
+ return {
329
+ exists: true,
330
+ issue: new ManagedPathError(path, `${label} must be a regular file: ${path}`)
331
+ };
332
+ } catch (error) {
333
+ if (error.code === "ENOENT") {
334
+ if (options.allowMissing)
335
+ return { exists: false, issue: null };
336
+ return {
337
+ exists: false,
338
+ issue: new ManagedPathError(path, `${label} must be a regular file: ${path}`)
339
+ };
340
+ }
341
+ const message = error instanceof Error ? error.message : String(error);
342
+ return {
343
+ exists: false,
344
+ issue: new ManagedPathError(path, `unable to inspect ${label} ${path}: ${message}`)
345
+ };
696
346
  }
697
- const trimmed = content.trimStart();
698
- const next = trimmed ? `${block}
699
-
700
- ${trimmed}` : `${block}
701
- `;
702
- return { content: next, changed: true };
703
347
  }
704
- function hasRspAgentsBlock(content) {
705
- return content.includes(RSP_AGENTS_BEGIN) && content.includes(RSP_AGENTS_END);
348
+ function requireManagedFile(path, label, options = {}) {
349
+ const inspection = inspectManagedFile(path, label, options);
350
+ if (inspection.issue)
351
+ throw inspection.issue;
352
+ return inspection.exists;
706
353
  }
707
- var SPEC_NAME_RE = /^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
708
- function isValidSpecName(name) {
709
- return SPEC_NAME_RE.test(name);
354
+ async function ensureManagedFile(path, content, label) {
355
+ if (requireManagedFile(path, label, { allowMissing: true }))
356
+ return false;
357
+ await writeFile(path, content, { flag: "wx" });
358
+ return true;
710
359
  }
711
- function guardRspInitialized() {
712
- const designPath = join3(RSP_DIR, "specs", "design.md");
713
- const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
714
- const design = inspectManagedFile(designPath, "design Spec", { allowMissing: true });
715
- if (rules.issue || design.issue || !rules.exists || !design.exists) {
716
- const initialized = existsSync2(RSP_DIR);
717
- console.error(` ${pc.red("Error:")} ${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}`);
718
- console.error(` ${pc.dim(initialized ? "Run: rsp update" : "Run: rsp init")}`);
719
- process.exit(1);
720
- }
360
+ async function writeManagedFile(path, content, label) {
361
+ const exists = requireManagedFile(path, label, { allowMissing: true });
362
+ if (exists)
363
+ await writeFile(path, content);
364
+ else
365
+ await writeFile(path, content, { flag: "wx" });
721
366
  }
722
- function getOpenCheckboxes(sectionText) {
723
- return sectionText.split("\n").map((line) => line.trim()).filter((line) => /^- \[[ /]\]/.test(line));
367
+ function resolveManagedDirectoryChain(root, segments, label) {
368
+ requireManagedDirectory(root, `${label} root`);
369
+ let current = root;
370
+ for (const segment of segments) {
371
+ if (!segment || segment === "." || segment === ".." || segment.includes("/") || segment.includes("\\"))
372
+ throw new ManagedPathError(current, `${label} contains an invalid directory segment: ${segment}`);
373
+ current = join(current, segment);
374
+ requireManagedDirectory(current, label, { allowMissing: true });
375
+ }
376
+ return current;
724
377
  }
725
- async function cleanupEmptyParentDirs(path, stopDir) {
726
- let parent = dirname2(path);
727
- while (parent !== stopDir) {
728
- try {
729
- const remaining = await readdir3(parent);
730
- if (remaining.length === 0)
731
- await rmdir(parent);
732
- else
733
- break;
734
- } catch {
735
- break;
736
- }
737
- parent = dirname2(parent);
378
+ async function inspectManagedFileTree(root, label, options = {}) {
379
+ const result = { rootExists: false, files: [], issues: [] };
380
+ const rootInspection = inspectManagedDirectory(root, `${label} root`, options);
381
+ if (rootInspection.issue) {
382
+ result.issues.push(rootInspection.issue);
383
+ return result;
738
384
  }
385
+ if (!rootInspection.exists)
386
+ return result;
387
+ result.rootExists = true;
388
+ await collectManagedFiles(root, label, result);
389
+ result.files.sort();
390
+ return result;
739
391
  }
740
- async function detectProjectName() {
392
+ async function collectManagedFiles(directory, label, result) {
393
+ let entries;
741
394
  try {
742
- if (existsSync2("package.json")) {
743
- const raw = await readFile("package.json", "utf-8");
744
- const pkg = JSON.parse(raw);
745
- if (pkg.name && pkg.name !== "")
746
- return pkg.name;
395
+ entries = await readdir(directory, { withFileTypes: true });
396
+ } catch (error) {
397
+ const message = error instanceof Error ? error.message : String(error);
398
+ result.issues.push(new ManagedPathError(directory, `unable to inspect the ${label} tree at ${directory}: ${message}`));
399
+ return;
400
+ }
401
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
402
+ if (entry.name.startsWith("."))
403
+ continue;
404
+ const path = join(directory, entry.name);
405
+ if (entry.isDirectory()) {
406
+ await collectManagedFiles(path, label, result);
407
+ continue;
747
408
  }
748
- } catch {
409
+ if (entry.isFile()) {
410
+ result.files.push(path);
411
+ continue;
412
+ }
413
+ result.issues.push(new ManagedPathError(path, `unsupported entry in the ${label} tree: ${path}`));
749
414
  }
750
- return basename(process.cwd());
751
- }
752
- function generateDesignContent(projectName) {
753
- const placeholder = "<\u2026>";
754
- return `# Project Design: ${projectName}
755
-
756
- ## Purpose
757
- - ${placeholder}
758
-
759
- ## Stable Facts
760
- - ${placeholder}
761
-
762
- ## Boundaries
763
- - In scope:
764
- - ${placeholder}
765
- - Out of scope:
766
- - ${placeholder}
767
-
768
- ## Structure
769
- - ${placeholder}
770
-
771
- ## Constraints
772
- - ${placeholder}
773
- `;
774
415
  }
775
- function generateSpecContent(name) {
776
- const title = toTitleCase(name);
777
- const placeholder = "<\u2026>";
778
- return `# ${title}
779
416
 
780
- ## Purpose
781
- - ${placeholder}
782
-
783
- ## Stable Facts
784
- - ${placeholder}
785
-
786
- ## Boundaries
787
- - In scope:
788
- - ${placeholder}
789
- - Out of scope:
790
- - ${placeholder}
791
-
792
- ## Constraints
793
- - ${placeholder}
794
- `;
795
- }
796
- function toTitleCase(value) {
797
- return value.split(/[-/]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
798
- }
799
- function extractBlockerLines(content) {
800
- const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
801
- return normalizeBlockerBody(getDocumentSectionBody(document, "blockers"));
802
- }
803
- function hasMeaningfulBlockerBody(body) {
804
- return hasMeaningfulBlockerLines(normalizeBlockerBody(body));
805
- }
806
- function normalizeBlockerBody(body) {
807
- return body.replace(/<!--[\s\S]*?-->/g, "").split("\n").map((line) => line.trim()).filter(Boolean);
808
- }
809
- function hasMeaningfulBlockers(content) {
810
- return hasMeaningfulBlockerLines(extractBlockerLines(content));
811
- }
812
- function hasMeaningfulBlockerLines(lines) {
813
- if (lines.length === 0)
814
- return false;
815
- return lines.some((line) => !/^[-*]\s*(?:none)?$/i.test(line) && !/^none$/i.test(line));
816
- }
817
- function detectDeltaSections(content) {
818
- const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
819
- const body = getDocumentSectionBody(document, "spec");
820
- return {
821
- added: /^###\s*ADDED/im.test(body),
822
- modified: /^###\s*MODIFIED/im.test(body),
823
- removed: /^###\s*REMOVED/im.test(body)
824
- };
417
+ // src/core/decisions.ts
418
+ var DEFAULT_DECISION_RECORDS_PATH = ".rsp/specs/decisions";
419
+ function normalizeDecisionRecordsPath(pathValue) {
420
+ return posix.normalize(pathValue.trim().replace(/\\/g, "/")).replace(/^\.\//, "").replace(/\/$/, "");
825
421
  }
826
- function collectArchiveReadiness(content, options = {}) {
827
- const warnings = [];
828
- const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
829
- const tasksSection = getDocumentSectionBody(document, "tasks");
830
- const verifySection = getDocumentSectionBody(document, "verify");
831
- const taskTodos = getOpenCheckboxes(tasksSection);
832
- if (taskTodos.length > 0)
833
- warnings.push(`${taskTodos.length} task item(s) still incomplete`);
834
- const verifyTodos = getOpenCheckboxes(verifySection);
835
- if (verifyTodos.length > 0)
836
- warnings.push(`${verifyTodos.length} Verify checklist item(s) are still incomplete`);
837
- const activeBlockers = options.activeBlockers ?? hasMeaningfulBlockers(content);
838
- if (activeBlockers)
839
- warnings.push("active blockers are present in the change file");
840
- const scenarios = parseScenarios(content);
841
- const missingScenarios = scenarios.length === 0;
842
- if (missingScenarios)
843
- warnings.push("no Scenario blocks found (some changes do not need them)");
844
- const archiveReady = activeBlockers ? "no" : warnings.length === 0 ? "yes" : "judgment";
845
- return {
846
- taskTodos,
847
- verifyTodos,
848
- activeBlockers,
849
- scenarioCount: scenarios.length,
850
- missingScenarios,
851
- deterministic: warnings.length === 0 ? "pass" : "warnings",
852
- semantic: "needs-review",
853
- archiveReady,
854
- warnings
855
- };
422
+ function validateDecisionRecordsPath(pathValue) {
423
+ if (typeof pathValue !== "string" || pathValue.trim() === "")
424
+ return 'config.yaml field "decisions.path" must be a non-empty string';
425
+ const raw = pathValue.trim();
426
+ const portable = raw.replace(/\\/g, "/");
427
+ if (isAbsolute(raw) || posix.isAbsolute(portable) || /^[a-z]:[\\/]/i.test(raw))
428
+ return 'config.yaml field "decisions.path" must be project-relative';
429
+ const normalized = normalizeDecisionRecordsPath(raw);
430
+ if (normalized === "." || normalized === ".." || normalized.startsWith("../"))
431
+ return 'config.yaml field "decisions.path" must stay inside the Host Project';
432
+ if ((normalized === ".rsp" || normalized.startsWith(".rsp/")) && normalized !== DEFAULT_DECISION_RECORDS_PATH)
433
+ return `config.yaml field "decisions.path" must use ${DEFAULT_DECISION_RECORDS_PATH} or a path outside .rsp/`;
434
+ return null;
856
435
  }
857
- function buildDurableReviewGuidance(factCandidateTargets, decisionRecordsPath) {
858
- return {
859
- required: true,
860
- factDecisions: [
861
- "No current-fact update needed",
862
- "Update existing spec or scoped instruction",
863
- "Create a new durable spec"
864
- ],
865
- rationaleDecisions: [
866
- "No Decision Record needed",
867
- "Create or update a Decision Record"
868
- ],
869
- factCandidateTargets,
870
- decisionRecordsPath,
871
- 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."
872
- };
436
+ function getDecisionRecordsConfigIssue(config) {
437
+ if (!("decisions" in config))
438
+ return null;
439
+ const decisions = config.decisions;
440
+ if (!decisions || typeof decisions !== "object" || Array.isArray(decisions))
441
+ return 'config.yaml field "decisions" must be a mapping with one path';
442
+ const keys = Object.keys(decisions);
443
+ if (keys.length !== 1 || keys[0] !== "path")
444
+ return 'config.yaml field "decisions" supports only "path"';
445
+ return validateDecisionRecordsPath(decisions.path);
873
446
  }
874
- function getDurableReviewCandidateTargets() {
875
- return [".rsp/specs/design.md"];
447
+ function resolveDecisionRecordsPath(config) {
448
+ const configured = config.decisions?.path;
449
+ return configured && validateDecisionRecordsPath(configured) === null ? normalizeDecisionRecordsPath(configured) : DEFAULT_DECISION_RECORDS_PATH;
876
450
  }
877
- function collectArchiveChecklist(content, options = {}) {
878
- return collectArchiveReadiness(content, options).warnings;
451
+ async function ensureDecisionRecordsDirectory(pathValue) {
452
+ const filesystemError = await validateDecisionRecordsFilesystemPath(pathValue);
453
+ if (filesystemError)
454
+ throw new Error(filesystemError);
455
+ const existed = existsSync(pathValue);
456
+ await mkdir(pathValue, { recursive: true });
457
+ const entries = await readdir2(pathValue);
458
+ if (entries.length === 0) {
459
+ await ensureManagedFile(join2(pathValue, ".gitkeep"), "", "Decision Record placeholder");
460
+ return true;
461
+ }
462
+ if (entries.includes(".gitkeep"))
463
+ requireManagedFile(join2(pathValue, ".gitkeep"), "Decision Record placeholder");
464
+ return !existed;
879
465
  }
880
- function parseScenarios(content) {
881
- const scenarios = [];
882
- const regex = /^#{3,4}\s*Scenario:\s*(.+)$/gim;
883
- let match;
884
- while ((match = regex.exec(content)) !== null) {
885
- const heading = match[1].trim();
886
- const startPos = match.index + match[0].length;
887
- const restContent = content.slice(startPos);
888
- const nextSection = restContent.match(/^#{3,4}\s/m);
889
- const block = nextSection ? restContent.slice(0, nextSection.index) : restContent;
890
- 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*/, ""));
891
- if (steps.length > 0)
892
- scenarios.push({ heading, steps });
466
+ async function validateDecisionRecordsFilesystemPath(pathValue, cwd = process.cwd()) {
467
+ const projectRoot = await realpath(cwd);
468
+ const targetPath = resolve(projectRoot, pathValue);
469
+ let existingAncestor = targetPath;
470
+ while (!existsSync(existingAncestor)) {
471
+ const parent = dirname(existingAncestor);
472
+ if (parent === existingAncestor)
473
+ return `Decision Record path "${pathValue}" has no accessible Host Project ancestor`;
474
+ existingAncestor = parent;
893
475
  }
894
- return scenarios;
476
+ const resolvedAncestor = await realpath(existingAncestor);
477
+ const rel = relative(projectRoot, resolvedAncestor);
478
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
479
+ return `Decision Record path "${pathValue}" resolves outside the Host Project through ${existingAncestor}`;
480
+ if (existsSync(targetPath) && !(await stat(targetPath)).isDirectory())
481
+ return `Decision Record path "${pathValue}" must be a directory`;
482
+ return null;
895
483
  }
896
484
 
897
485
  // src/core/config.ts
898
- var __dirname = dirname3(fileURLToPath(import.meta.url));
899
- var PKG_ROOT = join4(__dirname, "..");
486
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
487
+ var PKG_ROOT = join3(__dirname, "..");
900
488
  var RSP_DIR = ".rsp";
901
- var RSP_RULES_PATH = join4(RSP_DIR, "rsp-rules.md");
902
- var OBSOLETE_RSP_RULES_PATH = join4(RSP_DIR, "rules", "rsp-rules.md");
903
- var CHANGES_DIR = join4(RSP_DIR, "changes");
904
- var FOCUS_DIR = join4(RSP_DIR, "focus.d");
905
- var ARCHIVES_DIR = join4(RSP_DIR, "archives");
906
- var LOCK_PATH = join4(RSP_DIR, ".lock");
907
- var CONFIG_PATH = join4(RSP_DIR, "config.yaml");
489
+ var RSP_RULES_PATH = join3(RSP_DIR, "rsp-rules.md");
490
+ var OBSOLETE_RSP_RULES_PATH = join3(RSP_DIR, "rules", "rsp-rules.md");
491
+ var CHANGES_DIR = join3(RSP_DIR, "changes");
492
+ var FOCUS_DIR = join3(RSP_DIR, "focus.d");
493
+ var MAX_FOCUS_CAPSULE_BYTES = 4096;
494
+ var ARCHIVES_DIR = join3(RSP_DIR, "archives");
495
+ var LOCK_PATH = join3(RSP_DIR, ".lock");
496
+ var CONFIG_PATH = join3(RSP_DIR, "config.yaml");
908
497
  var VALID_KINDS = ["feature", "fix", "refactor", "docs", "ops", "research"];
909
498
  var DEFAULT_REQUIRED_SECTIONS = getCanonicalSectionHeadings(CHANGE_DOCUMENT_SCHEMA);
910
- var DEFAULT_MANAGE_POLICY = { activation: "explicit", closeout: "local" };
911
- var FAILED_CLOSED_MANAGE_POLICY = { activation: "explicit", closeout: "manual" };
499
+ var LEGACY_MISSING_CONFIG_MANAGE_POLICY = { activation: "explicit", closeout: "local" };
500
+ var INVALID_CONFIG_MANAGE_POLICY = { activation: "explicit", closeout: "manual" };
501
+ var CONFIG_DEFAULTS = {
502
+ kinds: [],
503
+ decisions: { path: ".rsp/specs/decisions" },
504
+ language: { default: "en" },
505
+ manage: { activation: "auto", closeout: "local" }
506
+ };
507
+ function generateConfigTemplate(config = {}) {
508
+ const kinds = config.kinds ?? CONFIG_DEFAULTS.kinds;
509
+ const decisions = { ...CONFIG_DEFAULTS.decisions, ...config.decisions };
510
+ const manage = { ...CONFIG_DEFAULTS.manage, ...config.manage };
511
+ const language = { ...CONFIG_DEFAULTS.language, ...config.language };
512
+ const kindsYaml = stringify(kinds).trimEnd();
513
+ const renderedKinds = kinds.length === 0 ? `kinds: ${kindsYaml}` : `kinds:
514
+ ${kindsYaml.split("\n").map((line) => ` ${line}`).join("\n")}`;
515
+ const artifactsLine = language.artifacts ? ` artifacts: ${stringify(language.artifacts).trim()}` : " # artifacts: zh-CN";
516
+ const commitLine = language.commit ? ` commit: ${stringify(language.commit).trim()}` : " # commit: zh-CN";
517
+ return `# RSP project configuration
518
+ # An empty kinds list retains the built-in defaults.
519
+ # A non-empty kinds list replaces the built-in defaults; it does not extend them.
520
+ # Every entry must be a unique non-empty string.
521
+ #
522
+ # Built-in defaults:
523
+ # kinds: ${VALID_KINDS.join(", ")}
524
+ #
525
+ ${renderedKinds}
526
+
527
+ # Decision Records default to .rsp/specs/decisions.
528
+ decisions:
529
+ path: ${stringify(decisions.path).trim()}
530
+
531
+ # New projects default to automatic Manage routing with local closeout.
532
+ # Use activation: explicit to require a named managed request.
533
+ # closeout accepts manual, lifecycle, or local; local routes a qualified clean terminal non-small boundary to one local commit but never push or publication.
534
+ manage:
535
+ activation: ${manage.activation}
536
+ closeout: ${manage.closeout}
537
+
538
+ # Set one shared project default for durable artifact and commit prose.
539
+ # Response language remains user/session-owned and is never read from this project mapping.
540
+ language:
541
+ default: ${stringify(language.default).trim()}
542
+ ${artifactsLine}
543
+ ${commitLine}
544
+ `;
545
+ }
912
546
  var MANAGE_ACTIVATIONS = ["explicit", "auto"];
913
547
  var MANAGE_CLOSEOUTS = ["manual", "lifecycle", "local"];
548
+ function reconcileRspConfigDefaults(raw) {
549
+ const document = parseDocument(raw);
550
+ if (document.errors.length > 0)
551
+ throw new Error(document.errors.map((error) => error.message).join("; "));
552
+ const parsed = parseYamlText(raw);
553
+ const issues = validateRspConfig(parsed);
554
+ if (issues.length > 0)
555
+ throw new Error(issues.join("; "));
556
+ const added = collectMissingConfigDefaults(parsed);
557
+ const customComments = hasCustomConfigComments(document, parsed);
558
+ if (customComments) {
559
+ backfillConfigDocument(document, parsed);
560
+ }
561
+ if (customComments && added.length === 0)
562
+ return { content: raw, added, changed: false };
563
+ const content = customComments ? String(document) : generateConfigTemplate(parsed);
564
+ return { content, added, changed: content !== raw };
565
+ }
566
+ function collectMissingConfigDefaults(parsed) {
567
+ const added = [];
568
+ if (!("kinds" in parsed))
569
+ added.push("kinds");
570
+ if (!("decisions" in parsed) || !hasNestedKey(parsed.decisions, "path"))
571
+ added.push("decisions.path");
572
+ if (!("language" in parsed))
573
+ added.push("language.default");
574
+ if (!("manage" in parsed)) {
575
+ added.push("manage.activation", "manage.closeout");
576
+ } else {
577
+ if (!hasNestedKey(parsed.manage, "activation"))
578
+ added.push("manage.activation");
579
+ if (!hasNestedKey(parsed.manage, "closeout"))
580
+ added.push("manage.closeout");
581
+ }
582
+ return added;
583
+ }
584
+ function backfillConfigDocument(document, parsed) {
585
+ if (!("kinds" in parsed))
586
+ document.set("kinds", [...CONFIG_DEFAULTS.kinds]);
587
+ if (!("decisions" in parsed))
588
+ document.set("decisions", { path: CONFIG_DEFAULTS.decisions.path });
589
+ else if (!hasNestedKey(parsed.decisions, "path"))
590
+ document.setIn(["decisions", "path"], CONFIG_DEFAULTS.decisions.path);
591
+ if (!("language" in parsed))
592
+ document.set("language", { default: CONFIG_DEFAULTS.language.default });
593
+ if (!("manage" in parsed)) {
594
+ document.set("manage", { ...CONFIG_DEFAULTS.manage });
595
+ } else {
596
+ if (!hasNestedKey(parsed.manage, "activation"))
597
+ document.setIn(["manage", "activation"], CONFIG_DEFAULTS.manage.activation);
598
+ if (!hasNestedKey(parsed.manage, "closeout"))
599
+ document.setIn(["manage", "closeout"], CONFIG_DEFAULTS.manage.closeout);
600
+ }
601
+ }
602
+ function hasNestedKey(value, key) {
603
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && key in value);
604
+ }
605
+ var LEGACY_GENERATED_CONFIG_COMMENTS = /* @__PURE__ */ new Set([
606
+ "Omit kinds or use [] to retain the built-in defaults.",
607
+ "New projects default to automatic Manage routing with lifecycle-only closeout.",
608
+ "Self-host automatic Manage routing with bounded deterministic local commits; push and publication stay explicit.",
609
+ "Set exactly one project-relative authoritative directory when the Host Project already owns Decision Records elsewhere.",
610
+ "kinds:",
611
+ "decisions:",
612
+ "path: docs/adr",
613
+ ...VALID_KINDS.map((kind) => `- ${kind}`)
614
+ ]);
615
+ function hasCustomConfigComments(document, parsed) {
616
+ const generated = new Set(collectConfigComments(parseDocument(generateConfigTemplate(parsed))));
617
+ for (const comment of LEGACY_GENERATED_CONFIG_COMMENTS)
618
+ generated.add(comment);
619
+ return collectConfigComments(document).some((comment) => !generated.has(comment));
620
+ }
621
+ function collectConfigComments(document) {
622
+ const comments = [];
623
+ const append = (value) => {
624
+ if (!value)
625
+ return;
626
+ for (const line of value.split("\n")) {
627
+ const comment = line.trim();
628
+ if (comment)
629
+ comments.push(comment);
630
+ }
631
+ };
632
+ append(document.commentBefore);
633
+ append(document.comment);
634
+ visit(document, {
635
+ Node(_key, node) {
636
+ append(node.commentBefore);
637
+ append(node.comment);
638
+ }
639
+ });
640
+ return comments;
641
+ }
914
642
  var _configCache = null;
915
643
  async function loadRspConfig() {
916
644
  const inspection = await inspectRspConfig();
@@ -918,11 +646,12 @@ async function loadRspConfig() {
918
646
  throw new Error(inspection.issues.join("; "));
919
647
  return inspection.config;
920
648
  }
921
- async function inspectRspConfig() {
922
- const cwd = process.cwd();
649
+ async function inspectRspConfig(cwd = process.cwd()) {
650
+ cwd = resolve2(cwd);
923
651
  if (_configCache?.cwd === cwd)
924
652
  return _configCache.inspection;
925
- const configFile = inspectManagedFile(CONFIG_PATH, "config file", { allowMissing: true });
653
+ const configPath = join3(cwd, CONFIG_PATH);
654
+ const configFile = inspectManagedFile(configPath, "config file", { allowMissing: true });
926
655
  if (configFile.issue)
927
656
  throw configFile.issue;
928
657
  if (!configFile.exists) {
@@ -930,7 +659,7 @@ async function inspectRspConfig() {
930
659
  _configCache = { cwd, inspection: inspection2 };
931
660
  return inspection2;
932
661
  }
933
- const raw = await readFile2(CONFIG_PATH, "utf-8");
662
+ const raw = await readFile(configPath, "utf-8");
934
663
  const parsed = parseYamlText(raw);
935
664
  const issues = validateRspConfig(parsed);
936
665
  const decisions = parsed.decisions && typeof parsed.decisions === "object" && !Array.isArray(parsed.decisions) ? parsed.decisions : void 0;
@@ -1025,10 +754,10 @@ function resolveRequiredSections(_config) {
1025
754
  }
1026
755
  function resolveManagePolicy(config, options = {}) {
1027
756
  if (options.configValid === false)
1028
- return { ...FAILED_CLOSED_MANAGE_POLICY };
757
+ return { ...INVALID_CONFIG_MANAGE_POLICY };
1029
758
  return {
1030
- activation: config.manage?.activation ?? DEFAULT_MANAGE_POLICY.activation,
1031
- closeout: config.manage?.closeout ?? DEFAULT_MANAGE_POLICY.closeout
759
+ activation: config.manage?.activation ?? LEGACY_MISSING_CONFIG_MANAGE_POLICY.activation,
760
+ closeout: config.manage?.closeout ?? LEGACY_MISSING_CONFIG_MANAGE_POLICY.closeout
1032
761
  };
1033
762
  }
1034
763
  function resolveLanguagePolicy(config, options = {}) {
@@ -1063,11 +792,12 @@ function canonicalizeLanguageTag(value) {
1063
792
  }
1064
793
  }
1065
794
  async function getVersion() {
1066
- const { version } = JSON.parse(await readFile2(join4(PKG_ROOT, "package.json"), "utf-8"));
795
+ const { version } = JSON.parse(await readFile(join3(PKG_ROOT, "package.json"), "utf-8"));
1067
796
  return version;
1068
797
  }
1069
798
 
1070
799
  // src/core/output.ts
800
+ import pc2 from "picocolors";
1071
801
  function toErrorMessage(error) {
1072
802
  return error instanceof Error ? error.message : String(error);
1073
803
  }
@@ -1076,27 +806,8 @@ function emitJson(data, options = {}) {
1076
806
  process.stdout.write(`${JSON.stringify(data, null, indentation)}
1077
807
  `);
1078
808
  }
1079
- function recordRuntimeDiagnostic(runtime, diagnostic, verbose = false) {
1080
- runtime.push(diagnostic);
1081
- if (verbose) {
1082
- console.error(` ${pc.dim(`[verbose] ${diagnostic.operation} ${diagnostic.path}: ${diagnostic.message}`)}`);
1083
- }
1084
- }
1085
809
 
1086
810
  export {
1087
- ManagedPathError,
1088
- inspectManagedDirectory,
1089
- requireManagedDirectory,
1090
- inspectManagedFile,
1091
- requireManagedFile,
1092
- ensureManagedFile,
1093
- writeManagedFile,
1094
- resolveManagedDirectoryChain,
1095
- inspectManagedFileTree,
1096
- DEFAULT_DECISION_RECORDS_PATH,
1097
- resolveDecisionRecordsPath,
1098
- ensureDecisionRecordsDirectory,
1099
- validateDecisionRecordsFilesystemPath,
1100
811
  CHANGE_DOCUMENT_SCHEMA,
1101
812
  GROUP_BRIEF_DOCUMENT_SCHEMA,
1102
813
  DocumentSectionCardinalityError,
@@ -1110,32 +821,30 @@ export {
1110
821
  renderDocumentSectionHeading,
1111
822
  renderDocumentTitle,
1112
823
  appendDocumentSectionItem,
1113
- toErrorMessage,
1114
- emitJson,
1115
- recordRuntimeDiagnostic,
1116
824
  parseFrontmatter,
1117
825
  countCheckboxes,
1118
- walkMarkdownFiles,
1119
- inspectUnsupportedRules,
1120
- normalizeLogicalPath,
1121
- generateChangeContent,
1122
- upsertRspAgentsBlock,
1123
- hasRspAgentsBlock,
1124
- isValidSpecName,
1125
- guardRspInitialized,
1126
- cleanupEmptyParentDirs,
1127
- detectProjectName,
1128
- generateDesignContent,
1129
- generateSpecContent,
826
+ classifyVerifyCheckboxes,
827
+ collectVerifyCheckboxLines,
828
+ getOpenCheckboxes,
1130
829
  extractBlockerLines,
1131
830
  hasMeaningfulBlockerBody,
1132
831
  hasMeaningfulBlockers,
832
+ isEmptyBlockerLine,
1133
833
  detectDeltaSections,
1134
- collectArchiveReadiness,
1135
- buildDurableReviewGuidance,
1136
- getDurableReviewCandidateTargets,
1137
- collectArchiveChecklist,
1138
834
  parseScenarios,
835
+ ManagedPathError,
836
+ inspectManagedDirectory,
837
+ requireManagedDirectory,
838
+ inspectManagedFile,
839
+ requireManagedFile,
840
+ ensureManagedFile,
841
+ writeManagedFile,
842
+ resolveManagedDirectoryChain,
843
+ inspectManagedFileTree,
844
+ DEFAULT_DECISION_RECORDS_PATH,
845
+ resolveDecisionRecordsPath,
846
+ ensureDecisionRecordsDirectory,
847
+ validateDecisionRecordsFilesystemPath,
1139
848
  pc,
1140
849
  PKG_ROOT,
1141
850
  RSP_DIR,
@@ -1143,10 +852,12 @@ export {
1143
852
  OBSOLETE_RSP_RULES_PATH,
1144
853
  CHANGES_DIR,
1145
854
  FOCUS_DIR,
855
+ MAX_FOCUS_CAPSULE_BYTES,
1146
856
  ARCHIVES_DIR,
1147
857
  LOCK_PATH,
1148
858
  CONFIG_PATH,
1149
- VALID_KINDS,
859
+ generateConfigTemplate,
860
+ reconcileRspConfigDefaults,
1150
861
  loadRspConfig,
1151
862
  inspectRspConfig,
1152
863
  clearConfigCache,
@@ -1154,5 +865,7 @@ export {
1154
865
  resolveRequiredSections,
1155
866
  resolveManagePolicy,
1156
867
  resolveLanguagePolicy,
1157
- getVersion
868
+ getVersion,
869
+ toErrorMessage,
870
+ emitJson
1158
871
  };