@oevery/rsp 3.1.0-beta.4 → 3.1.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -14
- package/README.zh-CN.md +11 -11
- package/dist/{chunk-D7XDXBFO.mjs → chunk-3X2V6IEW.mjs} +1 -1
- package/dist/{chunk-FJVH7ZYU.mjs → chunk-UVYEXKYS.mjs} +14 -5
- package/dist/{chunk-FI3SYHVH.mjs → chunk-ZKZVWJQ7.mjs} +141 -6
- package/dist/cli.mjs +242 -31
- package/dist/{entry-5OPKUZHF.mjs → entry-BKEJDNOQ.mjs} +2 -2
- package/dist/{entry-FVUG6YW2.mjs → entry-ETZGIFKP.mjs} +2 -2
- package/package.json +1 -1
- package/rules/rsp-rules.md +4 -2
- package/skills/rsp/SKILL.md +5 -15
- package/skills/rsp/references/groups-dependencies.md +1 -0
- package/skills/rsp/references/managed-routing.md +7 -22
- package/skills/rsp/references/release-operations.md +9 -0
- package/skills/rsp/references/reopen-recovery.md +9 -0
- package/skills/rsp-commit/SKILL.md +2 -0
- package/skills/rsp-implement/SKILL.md +5 -5
- package/skills/rsp-manage/SKILL.md +10 -4
- package/skills/rsp-shape/SKILL.md +2 -0
- package/skills/rsp-shape/references/external-issue-input.md +9 -0
package/README.md
CHANGED
|
@@ -34,17 +34,17 @@ RSP derives the next action from the selected Change, repository evidence, verif
|
|
|
34
34
|
## Quick start
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
npx -y @oevery/rsp@3.1.0-beta.
|
|
38
|
-
npx -y @oevery/rsp@3.1.0-beta.
|
|
37
|
+
npx -y @oevery/rsp@3.1.0-beta.5 init
|
|
38
|
+
npx -y @oevery/rsp@3.1.0-beta.5 doctor
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Recommended bootstrap flow:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
npx -y @oevery/rsp@3.1.0-beta.
|
|
44
|
+
npx -y @oevery/rsp@3.1.0-beta.5 init --with-project-setup
|
|
45
45
|
# fill .rsp/changes/project-setup.md
|
|
46
46
|
# fill .rsp/specs/design.md
|
|
47
|
-
npx -y @oevery/rsp@3.1.0-beta.
|
|
47
|
+
npx -y @oevery/rsp@3.1.0-beta.5 doctor
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
## Artifact foundation
|
|
@@ -89,15 +89,16 @@ npx -y @oevery/rsp@3.1.0-beta.4 doctor
|
|
|
89
89
|
- An exact blocker line, `- requires \`<change-work-ref>\`: <reason>`, declares a dependency on another executable Change. Free-form blockers stay external; RSP never guesses edges from prose. Archived prerequisites resolve automatically.
|
|
90
90
|
- Change names are either flat (`<change>`) or one direct grouped child (`<group>/<change>`). Recursive work directories are rejected.
|
|
91
91
|
- A Change Group is optional and is the only composite work shape. Its non-executable, non-focusable logical identity `<group>/brief`, physically stored as `<group>/00-brief.md`, owns the shared goal, constraints, declared slices, completion conditions, durable outcomes, and blockers for at least two direct child Changes.
|
|
92
|
-
- Create a group before its children. Every grouped Change must be declared by the brief, is focused and archived independently, and includes the brief in its context. With no focus, `status` uses Brief declaration order and current blockers to recommend the first executable slice. `status`, `check`, and `doctor` derive group health and completion without persisting another state; `rsp group close` archives only a completed brief.
|
|
92
|
+
- Create a group before its children. Every grouped Change must be declared by the brief, is focused and archived independently, and includes the brief in its context. With no focus, `status` uses Brief declaration order and current blockers to recommend the first executable slice. `status`, `check`, and `doctor` derive group health and completion without persisting another state; `rsp group close` archives only a completed brief. `rsp group create` never reuses an archived identity; `rsp group reopen <group> --reason <text>` explicitly restores one retained Brief snapshot as unfinished, unfocused continuation of that same Group.
|
|
93
93
|
- A Group Brief blocker is inherited by its direct children as an external blocker in the derived execution plan; it does not create guessed dependency edges.
|
|
94
94
|
- A Markdown file and directory cannot claim the same work identity.
|
|
95
95
|
- `rsp status`, `rsp check`, and `rsp doctor` inspect the same complete work tree and dependency facts. `status` derives exact edges with their reasons, ready work, blockers, and stable waves without a graph file; `check` and `doctor` reject incomplete archive inspection, malformed or missing targets, self-dependencies, and cycles. The `changes/` root must exist as a real directory, and existing focus/archive roots and group prefixes must also be real directories. Unsupported directories, non-Markdown entries, symlinks, missing or unreadable current work, incomplete reads, and identity collisions are visible errors. `status` exits non-zero instead of hiding invalid work.
|
|
96
96
|
- `rsp init`, `rsp update`, `rsp add spec`, and the generated Specs index builder apply the same no-follow managed-path checks. Archive discovery accepts only flat archive files or one real group directory; recursively organized Specs accept only real directories and regular files.
|
|
97
97
|
- Final managed files—such as the project `AGENTS.md`, focus markers, fallback/config files, the generated Specs index, and placeholders—must also be regular files; RSP rejects static symlink targets before reading or writing them.
|
|
98
98
|
- Completed changes move to `archives/`. Stable current facts belong in Specs; lasting rationale belongs in Decision Records; stable scoped operating instructions belong in nearest project-owned `AGENTS.md`.
|
|
99
|
-
- When later evidence shows that the original acceptance was not actually met, `rsp reopen <work-ref> --reason <text>` restores the same executable WorkRef as focused open work and retains the selected archive as history. Multiple archives require an exact `--from .rsp/archives/...` path. Use a new corrective Change for genuinely new scope or an independently delivered correction; reopening never rewrites Git or published history.
|
|
99
|
+
- When later evidence shows that the original acceptance was not actually met, `rsp reopen <work-ref> --reason <text>` restores the same executable WorkRef as focused open work and retains the selected archive as history. If its Group is closed, first use `rsp group reopen <group> --reason <text>`, then reopen only the incomplete child. Multiple matching Change or Group archives require an exact `--from .rsp/archives/...` path. Group reopen creates no focus and never reopens children or dependents. Use a new corrective Change for genuinely new scope or an independently delivered correction; reopening never rewrites Git or published history.
|
|
100
100
|
- Do not promote task history, debugging notes, or one-off implementation context into Specs, Decision Records, or project instructions.
|
|
101
|
+
- Managed progress questions report current evidence and do not implicitly pause an authorized run. Explicit pause confirms active work has stopped but preserves the focused WorkRef; only explicit release or `rsp unfocus` clears selection. Blocked or paused runs return a response-only continuation, and resume rereads authority, status, diff, blockers, and verification before requalifying Manage. RSP persists no paused or controller state.
|
|
101
102
|
- Change `Spec` delta markers (`### ADDED`, `### MODIFIED`, `### REMOVED`) are planning aids only. `rsp archive` does not automatically promote them into Specs or Decision Records. Durable writeback remains an explicit semantic decision.
|
|
102
103
|
- `rsp check` performs deterministic hygiene checks. It warns about unfinished template placeholders and unresolved clarification markers, but those warnings do not replace the semantic durable-update decision.
|
|
103
104
|
|
|
@@ -204,7 +205,7 @@ New `rsp init` config templates use the shown `auto` plus `lifecycle` policy. If
|
|
|
204
205
|
|
|
205
206
|
Humans should start with this README. Agents should follow the nearest `AGENTS.md`, prefer `skills/rsp/SKILL.md`, and use `.rsp/rsp-rules.md` only when the Skill is unavailable.
|
|
206
207
|
|
|
207
|
-
When this README shows `rsp <command>`, it assumes the command is already available in your environment. For opt-in beta evaluation, pin the exact prerelease identity, such as `npx -y @oevery/rsp@3.1.0-beta.
|
|
208
|
+
When this README shows `rsp <command>`, it assumes the command is already available in your environment. For opt-in beta evaluation, pin the exact prerelease identity, such as `npx -y @oevery/rsp@3.1.0-beta.5 <command>`; stable users can keep using the unversioned package entrypoint for npm `latest`.
|
|
208
209
|
|
|
209
210
|
Install the exact package's default lifecycle suite into the current project:
|
|
210
211
|
|
|
@@ -232,8 +233,8 @@ rsp skills install rsp-structural-audit
|
|
|
232
233
|
Both forms install from the package that invoked `rsp`, so prerelease dogfooding can pin one exact npm identity:
|
|
233
234
|
|
|
234
235
|
```bash
|
|
235
|
-
npx -y @oevery/rsp@3.1.0-beta.
|
|
236
|
-
npx -y @oevery/rsp@3.1.0-beta.
|
|
236
|
+
npx -y @oevery/rsp@3.1.0-beta.5 skills install --dry-run
|
|
237
|
+
npx -y @oevery/rsp@3.1.0-beta.5 skills install
|
|
237
238
|
```
|
|
238
239
|
|
|
239
240
|
Exact prerelease identities avoid depending on a moving dist-tag. `rsp update` refreshes RSP-managed project files only; run `rsp skills install` separately to refresh the package-owned Skill suite.
|
|
@@ -287,8 +288,8 @@ Durable review makes two independent semantic choices: whether current facts or
|
|
|
287
288
|
|
|
288
289
|
New project:
|
|
289
290
|
|
|
290
|
-
1. `npx -y @oevery/rsp@3.1.0-beta.
|
|
291
|
-
2. Prefer `npx -y @oevery/rsp@3.1.0-beta.
|
|
291
|
+
1. `npx -y @oevery/rsp@3.1.0-beta.5 init`
|
|
292
|
+
2. Prefer `npx -y @oevery/rsp@3.1.0-beta.5 init --with-project-setup`, or run `rsp create project-setup` manually
|
|
292
293
|
3. Fill `.rsp/specs/design.md`
|
|
293
294
|
4. Use `rsp add spec <name>` only when a new durable project doc is needed
|
|
294
295
|
5. Keep lasting rationale in the configured Decision Record directory and stable scoped operating instructions in the nearest project-owned `AGENTS.md`
|
|
@@ -302,14 +303,14 @@ New project:
|
|
|
302
303
|
|
|
303
304
|
Existing project with a rich `AGENTS.md`:
|
|
304
305
|
|
|
305
|
-
1. `npx -y @oevery/rsp@3.1.0-beta.
|
|
306
|
+
1. `npx -y @oevery/rsp@3.1.0-beta.5 init`
|
|
306
307
|
2. Keep the managed block thin
|
|
307
308
|
3. Keep project-wide boundaries and navigation in `.rsp/specs/design.md`, and route cohesive durable facts to the smallest domain Spec
|
|
308
309
|
4. Use `rsp add spec <name>` only when another durable current-fact document is needed
|
|
309
310
|
|
|
310
311
|
AI-assisted setup:
|
|
311
312
|
|
|
312
|
-
1. `npx -y @oevery/rsp@3.1.0-beta.
|
|
313
|
+
1. `npx -y @oevery/rsp@3.1.0-beta.5 init --agents-mode print --with-project-setup`
|
|
313
314
|
2. Keep the managed block as generated and adapt only the surrounding human-owned content if needed
|
|
314
315
|
3. Have the AI review and fill `.rsp/changes/project-setup.md`
|
|
315
316
|
4. Have the AI fill `.rsp/specs/design.md`
|
|
@@ -327,9 +328,11 @@ rsp skills list [--json] List bundled Skills and exact project installati
|
|
|
327
328
|
rsp skills install [name] [--dry-run] [--force]
|
|
328
329
|
Install the default eleven Skills or one exact optional Skill into .agents/skills
|
|
329
330
|
rsp add spec <name> Create .rsp/specs/<name>.md and rebuild specs index
|
|
330
|
-
rsp create <name> [summary] Create .rsp/changes/<name>.md; add --lite
|
|
331
|
+
rsp create <name> [summary] Create .rsp/changes/<name>.md; add --lite or --issue <url> [--issue-relation relates|closes]
|
|
331
332
|
rsp group create <name> [goal] Create an unfocused .rsp/changes/<name>/00-brief.md
|
|
332
333
|
rsp group close <name> Archive a completed Group Brief after every child is archived
|
|
334
|
+
rsp group reopen <name> --reason <text> [--from <archive-path>]
|
|
335
|
+
Restore the same Group Brief as unfinished, unfocused open work and retain history
|
|
333
336
|
rsp focus <name> Mark an open change as currently focused
|
|
334
337
|
rsp unfocus <name> Remove an open change from the current focus set
|
|
335
338
|
rsp archive <name> Archive to .rsp/archives/
|
|
@@ -378,6 +381,8 @@ The JSON-producing inspection commands `status`, `show`, `ready`, `check`, `doct
|
|
|
378
381
|
|
|
379
382
|
`rsp create --lite` is a shorter template for explicitly tracked small changes; simple current-session tasks should not create RSP changes unless tracking is intentionally needed.
|
|
380
383
|
|
|
384
|
+
`rsp create <name> --issue <url>` attaches one validated external issue without network access and defaults to `relation: relates`; `--issue-relation closes` must be explicit and requires `--issue`. Issue options never update an existing Change: that invocation fails without changing the Change or focus. RSP stores only the canonical URL and relationship, never credentials, fetched title/body, or retrieval state. `status --json`, `show`, and `history` preserve valid relationships; immutable archives with older issue metadata remain readable but omit that opaque legacy projection. Shape may use available host access to retrieve real issue content, but failed access degrades to login retry, pasted source data, or a link-only draft rather than fabrication. Commit may add a closing keyword only for a terminal commit with completed acceptance and an owned `closes` relationship; otherwise it keeps a non-closing canonical URL reference.
|
|
385
|
+
|
|
381
386
|
`rsp doctor --fix` runs only safe deterministic repairs. Its `fixed` JSON entries report actual filesystem changes; a healthy project returns `fixed: []` and the human output says no safe fixes are needed.
|
|
382
387
|
|
|
383
388
|
## Platform-agnostic
|
package/README.zh-CN.md
CHANGED
|
@@ -34,17 +34,17 @@ RSP 根据 selected Change、仓库证据、验证结果和 blockers 推导下
|
|
|
34
34
|
## 快速开始
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
npx -y @oevery/rsp@3.1.0-beta.
|
|
38
|
-
npx -y @oevery/rsp@3.1.0-beta.
|
|
37
|
+
npx -y @oevery/rsp@3.1.0-beta.5 init
|
|
38
|
+
npx -y @oevery/rsp@3.1.0-beta.5 doctor
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
推荐启动流程:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
npx -y @oevery/rsp@3.1.0-beta.
|
|
44
|
+
npx -y @oevery/rsp@3.1.0-beta.5 init --with-project-setup
|
|
45
45
|
# 填写 .rsp/changes/project-setup.md
|
|
46
46
|
# 填写 .rsp/specs/design.md
|
|
47
|
-
npx -y @oevery/rsp@3.1.0-beta.
|
|
47
|
+
npx -y @oevery/rsp@3.1.0-beta.5 doctor
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
## Artifact foundation
|
|
@@ -203,7 +203,7 @@ Change 粒度由一个可观察结果及其共享的 acceptance、verification
|
|
|
203
203
|
|
|
204
204
|
人类应从本 README 开始;agent 应遵循 nearest `AGENTS.md`,优先加载 `skills/rsp/SKILL.md`,仅在 Skill 不可用时读取 `.rsp/rsp-rules.md`。
|
|
205
205
|
|
|
206
|
-
如果文档中写的是 `rsp <command>`,默认前提是你的环境里已经能直接运行 `rsp`。进行 opt-in beta 评估时,应固定精确 prerelease 身份,例如 `npx -y @oevery/rsp@3.1.0-beta.
|
|
206
|
+
如果文档中写的是 `rsp <command>`,默认前提是你的环境里已经能直接运行 `rsp`。进行 opt-in beta 评估时,应固定精确 prerelease 身份,例如 `npx -y @oevery/rsp@3.1.0-beta.5 <command>`;stable 用户仍可使用不带版本的入口获取 npm `latest`。
|
|
207
207
|
|
|
208
208
|
将当前精确包内置的默认生命周期 Skill 套件安装到当前项目:
|
|
209
209
|
|
|
@@ -231,8 +231,8 @@ rsp skills install rsp-structural-audit
|
|
|
231
231
|
两种形式都从调用 `rsp` 的同一个包安装,因此 beta 评估可以固定精确 npm 身份:
|
|
232
232
|
|
|
233
233
|
```bash
|
|
234
|
-
npx -y @oevery/rsp@3.1.0-beta.
|
|
235
|
-
npx -y @oevery/rsp@3.1.0-beta.
|
|
234
|
+
npx -y @oevery/rsp@3.1.0-beta.5 skills install --dry-run
|
|
235
|
+
npx -y @oevery/rsp@3.1.0-beta.5 skills install
|
|
236
236
|
```
|
|
237
237
|
|
|
238
238
|
精确 prerelease 身份可以避免依赖持续移动的 dist-tag。`rsp update` 只刷新由 RSP 管理的项目文件;刷新 package-owned Skill 套件需要单独运行 `rsp skills install`。
|
|
@@ -286,8 +286,8 @@ durable review 包含两个独立语义判断:是否更新当前事实或作
|
|
|
286
286
|
|
|
287
287
|
新项目:
|
|
288
288
|
|
|
289
|
-
1. `npx -y @oevery/rsp@3.1.0-beta.
|
|
290
|
-
2. 优先使用 `npx -y @oevery/rsp@3.1.0-beta.
|
|
289
|
+
1. `npx -y @oevery/rsp@3.1.0-beta.5 init`
|
|
290
|
+
2. 优先使用 `npx -y @oevery/rsp@3.1.0-beta.5 init --with-project-setup`,或手动执行 `rsp create project-setup`
|
|
291
291
|
3. 填写 `.rsp/specs/design.md`
|
|
292
292
|
4. 仅在需要新的长期项目文档时使用 `rsp add spec <name>`
|
|
293
293
|
5. 将长期理由写入配置的 Decision Record 目录,将稳定的作用域操作指令写入 nearest project-owned `AGENTS.md`
|
|
@@ -300,14 +300,14 @@ durable review 包含两个独立语义判断:是否更新当前事实或作
|
|
|
300
300
|
|
|
301
301
|
已有复杂 `AGENTS.md` 的项目:
|
|
302
302
|
|
|
303
|
-
1. `npx -y @oevery/rsp@3.1.0-beta.
|
|
303
|
+
1. `npx -y @oevery/rsp@3.1.0-beta.5 init`
|
|
304
304
|
2. 保持受管块尽量薄
|
|
305
305
|
3. 将项目级边界与导航放在 `.rsp/specs/design.md`,把成组的长期事实放入最小领域 Spec
|
|
306
306
|
4. 只有需要额外的 durable current-fact 文档时才使用 `rsp add spec <name>`
|
|
307
307
|
|
|
308
308
|
AI 协助接入:
|
|
309
309
|
|
|
310
|
-
1. `npx -y @oevery/rsp@3.1.0-beta.
|
|
310
|
+
1. `npx -y @oevery/rsp@3.1.0-beta.5 init --agents-mode print --with-project-setup`
|
|
311
311
|
2. 保持受管块原样,只在需要时调整周围由人维护的内容
|
|
312
312
|
3. 让 AI 审阅并填写 `.rsp/changes/project-setup.md`
|
|
313
313
|
4. 让 AI 填写 `.rsp/specs/design.md`
|
|
@@ -472,10 +472,11 @@ function normalizeLogicalPath(pathValue) {
|
|
|
472
472
|
function generateChangeContent(name, summary = "", kind, options = {}) {
|
|
473
473
|
const placeholder = "<\u2026>";
|
|
474
474
|
const proposalSummary = summary || placeholder;
|
|
475
|
+
const issueFrontmatter = renderIssueFrontmatter(options.issues ?? []);
|
|
475
476
|
if (name === "project-setup") {
|
|
476
477
|
return `---
|
|
477
478
|
kind: ops
|
|
478
|
-
---
|
|
479
|
+
${issueFrontmatter}---
|
|
479
480
|
|
|
480
481
|
${renderDocumentTitle(CHANGE_DOCUMENT_SCHEMA, "project-setup")}
|
|
481
482
|
|
|
@@ -533,11 +534,11 @@ ${changeSectionHeading("blockers")}
|
|
|
533
534
|
}
|
|
534
535
|
const frontmatterKind = kind ?? "<choose: feature | fix | refactor | docs | ops | research>";
|
|
535
536
|
if (options.lite)
|
|
536
|
-
return generateLiteChangeContent(name, proposalSummary, frontmatterKind);
|
|
537
|
+
return generateLiteChangeContent(name, proposalSummary, frontmatterKind, issueFrontmatter);
|
|
537
538
|
const template = getChangeTemplateByKind(kind);
|
|
538
539
|
return `---
|
|
539
540
|
kind: "${frontmatterKind}"
|
|
540
|
-
---
|
|
541
|
+
${issueFrontmatter}---
|
|
541
542
|
|
|
542
543
|
${renderDocumentTitle(CHANGE_DOCUMENT_SCHEMA, name)}
|
|
543
544
|
|
|
@@ -582,11 +583,11 @@ ${changeSectionHeading("blockers")}
|
|
|
582
583
|
- none
|
|
583
584
|
`;
|
|
584
585
|
}
|
|
585
|
-
function generateLiteChangeContent(name, proposalSummary, frontmatterKind) {
|
|
586
|
+
function generateLiteChangeContent(name, proposalSummary, frontmatterKind, issueFrontmatter) {
|
|
586
587
|
const placeholder = "<\u2026>";
|
|
587
588
|
return `---
|
|
588
589
|
kind: "${frontmatterKind}"
|
|
589
|
-
---
|
|
590
|
+
${issueFrontmatter}---
|
|
590
591
|
|
|
591
592
|
${renderDocumentTitle(CHANGE_DOCUMENT_SCHEMA, name)}
|
|
592
593
|
|
|
@@ -635,6 +636,14 @@ ${changeSectionHeading("blockers")}
|
|
|
635
636
|
- none
|
|
636
637
|
`;
|
|
637
638
|
}
|
|
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
|
+
`;
|
|
646
|
+
}
|
|
638
647
|
function getChangeTemplateByKind(kind) {
|
|
639
648
|
const placeholder = "<\u2026>";
|
|
640
649
|
const delta = kind === "feature" || kind === "research" || kind === void 0 ? "ADDED" : "MODIFIED";
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
resolveLanguagePolicy,
|
|
30
30
|
resolveManagePolicy,
|
|
31
31
|
toErrorMessage
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-UVYEXKYS.mjs";
|
|
33
33
|
|
|
34
34
|
// src/status/inspect.ts
|
|
35
35
|
import { readFile as readFile5, stat } from "fs/promises";
|
|
@@ -666,6 +666,7 @@ function normalizeLogicalPath2(path) {
|
|
|
666
666
|
|
|
667
667
|
// src/core/change-group.ts
|
|
668
668
|
var GROUP_BRIEF_SECTIONS = getCanonicalSectionHeadings(GROUP_BRIEF_DOCUMENT_SCHEMA);
|
|
669
|
+
var GROUP_REOPEN_COMPLETION_PREFIX = "Resolve reopened concern from";
|
|
669
670
|
async function resolveExecutableChange(name, options = {}) {
|
|
670
671
|
const ref = resolveWorkRef(name, { ...options, executable: true });
|
|
671
672
|
if (ref.kind === "group-change")
|
|
@@ -711,6 +712,8 @@ async function inspectChangeGroups(options = {}) {
|
|
|
711
712
|
const groups = [];
|
|
712
713
|
const archivedSlicesByGroup = /* @__PURE__ */ new Map();
|
|
713
714
|
const archivedGroups = /* @__PURE__ */ new Set();
|
|
715
|
+
const archivedGroupReopenEvidence = /* @__PURE__ */ new Map();
|
|
716
|
+
const archivedGroupBriefPaths = /* @__PURE__ */ new Map();
|
|
714
717
|
const openGroupNames = new Set(workTree.briefs.map((brief) => brief.group));
|
|
715
718
|
for (const path of archiveTree.files) {
|
|
716
719
|
const archivePath = normalizeLogicalPath(relative2(ARCHIVES_DIR, path));
|
|
@@ -736,8 +739,16 @@ async function inspectChangeGroups(options = {}) {
|
|
|
736
739
|
archivedSlicesByGroup.set(archiveGroup, groupSlices);
|
|
737
740
|
}
|
|
738
741
|
}
|
|
739
|
-
if (getDocumentTitle(parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA), "identity") === archiveGroup)
|
|
742
|
+
if (getDocumentTitle(parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA), "identity") === archiveGroup) {
|
|
740
743
|
archivedGroups.add(archiveGroup);
|
|
744
|
+
const evidence = archivedGroupReopenEvidence.get(archiveGroup) ?? /* @__PURE__ */ new Set();
|
|
745
|
+
for (const item of getGroupReopenEvidence(content))
|
|
746
|
+
evidence.add(item.key);
|
|
747
|
+
archivedGroupReopenEvidence.set(archiveGroup, evidence);
|
|
748
|
+
const retainedPaths = archivedGroupBriefPaths.get(archiveGroup) ?? /* @__PURE__ */ new Set();
|
|
749
|
+
retainedPaths.add(`.rsp/archives/${archivePath}`);
|
|
750
|
+
archivedGroupBriefPaths.set(archiveGroup, retainedPaths);
|
|
751
|
+
}
|
|
741
752
|
} catch (error) {
|
|
742
753
|
diagnostics.push({
|
|
743
754
|
severity: "error",
|
|
@@ -764,7 +775,11 @@ async function inspectChangeGroups(options = {}) {
|
|
|
764
775
|
}
|
|
765
776
|
const parsed = parseGroupBrief(brief.group, content);
|
|
766
777
|
diagnostics.push(...parsed.diagnostics.map((diagnostic) => ({ ...diagnostic, path: brief.path })));
|
|
767
|
-
if (archivedGroups.has(brief.group)
|
|
778
|
+
if (archivedGroups.has(brief.group) && !hasExplicitGroupReopenEvidence(
|
|
779
|
+
content,
|
|
780
|
+
archivedGroupReopenEvidence.get(brief.group) ?? /* @__PURE__ */ new Set(),
|
|
781
|
+
archivedGroupBriefPaths.get(brief.group) ?? /* @__PURE__ */ new Set()
|
|
782
|
+
)) {
|
|
768
783
|
diagnostics.push({
|
|
769
784
|
severity: "error",
|
|
770
785
|
code: "group_identity_reopened",
|
|
@@ -855,6 +870,16 @@ async function inspectChangeGroups(options = {}) {
|
|
|
855
870
|
groups.sort((left, right) => left.name.localeCompare(right.name));
|
|
856
871
|
return { groups, diagnostics };
|
|
857
872
|
}
|
|
873
|
+
function getGroupReopenEvidence(content) {
|
|
874
|
+
const completion = getDocumentSectionBody(parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA), "completionConditions");
|
|
875
|
+
return completion.split("\n").flatMap((line) => {
|
|
876
|
+
const match = line.trim().match(/^-\s+\[([ /xX-])\]\s+(Resolve reopened concern from `(\.rsp\/archives\/[^`]+)`: \S.*)$/);
|
|
877
|
+
return match ? [{ active: match[1] !== "-", key: match[2], sourcePath: match[3] }] : [];
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
function hasExplicitGroupReopenEvidence(content, retainedEvidence, retainedGroupBriefPaths) {
|
|
881
|
+
return getGroupReopenEvidence(content).some((item) => item.active && retainedGroupBriefPaths.has(item.sourcePath) && !retainedEvidence.has(item.key));
|
|
882
|
+
}
|
|
858
883
|
async function requireDeclaredGroupSlice(ref) {
|
|
859
884
|
const briefPath = join2(dirname(ref.path), GROUP_BRIEF_FILENAME);
|
|
860
885
|
let content;
|
|
@@ -975,6 +1000,69 @@ import { readFile as readFile3 } from "fs/promises";
|
|
|
975
1000
|
import { readFile as readFile2 } from "fs/promises";
|
|
976
1001
|
import { basename, dirname as dirname2, relative as relative3 } from "path";
|
|
977
1002
|
|
|
1003
|
+
// src/core/issue-relationship.ts
|
|
1004
|
+
var ISSUE_RELATIONS = /* @__PURE__ */ new Set(["relates", "closes"]);
|
|
1005
|
+
var ISSUE_FIELDS = /* @__PURE__ */ new Set(["url", "relation"]);
|
|
1006
|
+
var IssueRelationshipError = class extends Error {
|
|
1007
|
+
constructor(code, message) {
|
|
1008
|
+
super(message);
|
|
1009
|
+
this.code = code;
|
|
1010
|
+
this.name = "IssueRelationshipError";
|
|
1011
|
+
}
|
|
1012
|
+
code;
|
|
1013
|
+
};
|
|
1014
|
+
function parseIssueRelationships(frontmatter) {
|
|
1015
|
+
if (!frontmatter || !("issues" in frontmatter))
|
|
1016
|
+
return [];
|
|
1017
|
+
if (!Array.isArray(frontmatter.issues))
|
|
1018
|
+
throw new IssueRelationshipError("invalid_issue_shape", 'frontmatter field "issues" must be a list');
|
|
1019
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1020
|
+
return frontmatter.issues.map((value, index) => {
|
|
1021
|
+
if (!isPlainObject(value))
|
|
1022
|
+
throw new IssueRelationshipError("invalid_issue_shape", `issues[${index}] must be a mapping with url and relation`);
|
|
1023
|
+
const fields = Object.keys(value);
|
|
1024
|
+
const unsupported = fields.filter((field) => !ISSUE_FIELDS.has(field));
|
|
1025
|
+
if (unsupported.length > 0)
|
|
1026
|
+
throw new IssueRelationshipError("unsupported_issue_field", `issues[${index}] contains unsupported field(s): ${unsupported.sort().join(", ")}`);
|
|
1027
|
+
if (fields.length !== 2 || !("url" in value) || !("relation" in value))
|
|
1028
|
+
throw new IssueRelationshipError("invalid_issue_shape", `issues[${index}] must contain exactly url and relation`);
|
|
1029
|
+
if (typeof value.url !== "string")
|
|
1030
|
+
throw new IssueRelationshipError("invalid_issue_url", `issues[${index}].url must be an absolute http or https URL`);
|
|
1031
|
+
if (typeof value.relation !== "string" || !ISSUE_RELATIONS.has(value.relation))
|
|
1032
|
+
throw new IssueRelationshipError("unsupported_issue_relation", `issues[${index}].relation must be relates or closes`);
|
|
1033
|
+
const url = normalizeIssueUrl(value.url, `issues[${index}].url`);
|
|
1034
|
+
if (seen.has(url))
|
|
1035
|
+
throw new IssueRelationshipError("duplicate_issue_url", `issues contains duplicate normalized URL: ${url}`);
|
|
1036
|
+
seen.add(url);
|
|
1037
|
+
return { url, relation: value.relation };
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
function createIssueRelationship(url, relation = "relates") {
|
|
1041
|
+
return parseIssueRelationships({ issues: [{ url, relation }] })[0];
|
|
1042
|
+
}
|
|
1043
|
+
function normalizeIssueUrl(raw, label) {
|
|
1044
|
+
if (raw.trim() !== raw || raw === "")
|
|
1045
|
+
throw new IssueRelationshipError("invalid_issue_url", `${label} must be an absolute http or https URL without surrounding whitespace`);
|
|
1046
|
+
let parsed;
|
|
1047
|
+
try {
|
|
1048
|
+
parsed = new URL(raw);
|
|
1049
|
+
} catch {
|
|
1050
|
+
throw new IssueRelationshipError("invalid_issue_url", `${label} must be an absolute http or https URL`);
|
|
1051
|
+
}
|
|
1052
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
1053
|
+
throw new IssueRelationshipError("invalid_issue_url", `${label} must use http or https`);
|
|
1054
|
+
if (!parsed.hostname)
|
|
1055
|
+
throw new IssueRelationshipError("invalid_issue_url", `${label} must include a host`);
|
|
1056
|
+
if (parsed.username || parsed.password)
|
|
1057
|
+
throw new IssueRelationshipError("unsafe_issue_url", `${label} must not contain credentials`);
|
|
1058
|
+
if (parsed.hash)
|
|
1059
|
+
throw new IssueRelationshipError("unsafe_issue_url", `${label} must not contain a fragment`);
|
|
1060
|
+
return parsed.toString();
|
|
1061
|
+
}
|
|
1062
|
+
function isPlainObject(value) {
|
|
1063
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
978
1066
|
// src/history/model.ts
|
|
979
1067
|
var HISTORY_DEFAULT_LIMIT = 20;
|
|
980
1068
|
var HISTORY_MAX_LIMIT = 100;
|
|
@@ -1011,6 +1099,7 @@ async function inspectArchiveHistory(options = {}) {
|
|
|
1011
1099
|
}));
|
|
1012
1100
|
const runtime = [];
|
|
1013
1101
|
const records = [];
|
|
1102
|
+
const groupBriefs = [];
|
|
1014
1103
|
if (!tree.rootExists && tree.diagnostics.length === 0) {
|
|
1015
1104
|
diagnostics.push({
|
|
1016
1105
|
severity: "error",
|
|
@@ -1034,14 +1123,18 @@ async function inspectArchiveHistory(options = {}) {
|
|
|
1034
1123
|
diagnostics.push(parsed.diagnostic);
|
|
1035
1124
|
if (parsed.record)
|
|
1036
1125
|
records.push(parsed.record);
|
|
1126
|
+
if (parsed.groupBrief)
|
|
1127
|
+
groupBriefs.push(parsed.groupBrief);
|
|
1037
1128
|
}
|
|
1038
1129
|
records.sort(compareHistoryRecords);
|
|
1130
|
+
groupBriefs.sort(compareArchivedGroupBriefs);
|
|
1039
1131
|
diagnostics.sort(compareDiagnostics);
|
|
1040
1132
|
const diagnosticTotal = diagnostics.length;
|
|
1041
1133
|
const boundedDiagnostics = diagnostics.slice(0, HISTORY_MAX_DIAGNOSTICS);
|
|
1042
1134
|
return {
|
|
1043
1135
|
rootExists: tree.rootExists,
|
|
1044
1136
|
records,
|
|
1137
|
+
groupBriefs,
|
|
1045
1138
|
diagnostics: boundedDiagnostics,
|
|
1046
1139
|
diagnosticSummary: {
|
|
1047
1140
|
total: diagnosticTotal,
|
|
@@ -1082,7 +1175,15 @@ function parseArchiveEntry(sourcePath, path, archivesDir, content) {
|
|
|
1082
1175
|
if (!archiveGroup || group !== archiveGroup || frontmatter.kind !== "group" || !matchesArchiveStem(archiveStem, "brief")) {
|
|
1083
1176
|
return { diagnostic: errorDiagnostic("archive_identity_mismatch", path, `archived Change Group identity ${group} does not match its archive path`) };
|
|
1084
1177
|
}
|
|
1085
|
-
return {};
|
|
1178
|
+
return { groupBrief: { date, group, path, sourcePath } };
|
|
1179
|
+
}
|
|
1180
|
+
let issues;
|
|
1181
|
+
if ("issues" in frontmatter) {
|
|
1182
|
+
try {
|
|
1183
|
+
issues = parseIssueRelationships(frontmatter);
|
|
1184
|
+
} catch {
|
|
1185
|
+
issues = void 0;
|
|
1186
|
+
}
|
|
1086
1187
|
}
|
|
1087
1188
|
const workRef = changeHeadings[0];
|
|
1088
1189
|
const segments = workRef.split("/");
|
|
@@ -1107,6 +1208,7 @@ function parseArchiveEntry(sourcePath, path, archivesDir, content) {
|
|
|
1107
1208
|
summary: summary.value,
|
|
1108
1209
|
summaryTruncated: summary.truncated,
|
|
1109
1210
|
path,
|
|
1211
|
+
...issues === void 0 ? {} : { issues },
|
|
1110
1212
|
sourcePath,
|
|
1111
1213
|
searchSummary: rawSummary
|
|
1112
1214
|
}
|
|
@@ -1151,6 +1253,9 @@ function escapeRegExp(value) {
|
|
|
1151
1253
|
function compareHistoryRecords(left, right) {
|
|
1152
1254
|
return right.date.localeCompare(left.date) || left.workRef.localeCompare(right.workRef) || left.path.localeCompare(right.path);
|
|
1153
1255
|
}
|
|
1256
|
+
function compareArchivedGroupBriefs(left, right) {
|
|
1257
|
+
return right.date.localeCompare(left.date) || left.group.localeCompare(right.group) || left.path.localeCompare(right.path);
|
|
1258
|
+
}
|
|
1154
1259
|
function compareDiagnostics(left, right) {
|
|
1155
1260
|
return (left.path ?? "").localeCompare(right.path ?? "") || left.code.localeCompare(right.code) || left.message.localeCompare(right.message);
|
|
1156
1261
|
}
|
|
@@ -1205,6 +1310,17 @@ function selectArchiveHistoryRecord(records, selector) {
|
|
|
1205
1310
|
}
|
|
1206
1311
|
return matches[0];
|
|
1207
1312
|
}
|
|
1313
|
+
function selectArchivedGroupBrief(records, selector) {
|
|
1314
|
+
const matches = "path" in selector ? records.filter((record) => record.path === selector.path) : records.filter((record) => record.group === selector.group);
|
|
1315
|
+
const identity = "path" in selector ? selector.path : selector.group;
|
|
1316
|
+
if (matches.length === 0)
|
|
1317
|
+
throw new ArchiveHistoryError("archive_not_found", `archived Change Group not found: ${identity}`);
|
|
1318
|
+
if (matches.length > 1) {
|
|
1319
|
+
const candidates = matches.map((record) => record.path).sort();
|
|
1320
|
+
throw new ArchiveHistoryError("archive_ambiguous", `multiple archives match Change Group ${identity}`, candidates);
|
|
1321
|
+
}
|
|
1322
|
+
return matches[0];
|
|
1323
|
+
}
|
|
1208
1324
|
async function readArchiveHistoryDetail(record) {
|
|
1209
1325
|
let content;
|
|
1210
1326
|
try {
|
|
@@ -1600,6 +1716,7 @@ async function inspectProjectStatus(options = {}) {
|
|
|
1600
1716
|
let done = 0;
|
|
1601
1717
|
let total = 0;
|
|
1602
1718
|
let progressKnown = false;
|
|
1719
|
+
let issues = [];
|
|
1603
1720
|
if (path) {
|
|
1604
1721
|
try {
|
|
1605
1722
|
const content = await readFile5(path, "utf-8");
|
|
@@ -1608,10 +1725,21 @@ async function inspectProjectStatus(options = {}) {
|
|
|
1608
1725
|
try {
|
|
1609
1726
|
const frontmatter = parseFrontmatter(content);
|
|
1610
1727
|
kind = frontmatter?.kind ? String(frontmatter.kind) : "\u2014";
|
|
1728
|
+
issues = parseIssueRelationships(frontmatter);
|
|
1611
1729
|
} catch (error) {
|
|
1612
1730
|
kind = "(invalid)";
|
|
1731
|
+
const issueError = error instanceof IssueRelationshipError;
|
|
1732
|
+
if (issueError) {
|
|
1733
|
+
diagnostics.push({
|
|
1734
|
+
severity: "error",
|
|
1735
|
+
code: error.code,
|
|
1736
|
+
change: name,
|
|
1737
|
+
path,
|
|
1738
|
+
message: error.message
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1613
1741
|
runtime.push({
|
|
1614
|
-
code: "frontmatter_parse_failed",
|
|
1742
|
+
code: issueError ? error.code : "frontmatter_parse_failed",
|
|
1615
1743
|
operation: "parseFrontmatter",
|
|
1616
1744
|
path,
|
|
1617
1745
|
message: toErrorMessage(error)
|
|
@@ -1692,7 +1820,8 @@ async function inspectProjectStatus(options = {}) {
|
|
|
1692
1820
|
ageDays,
|
|
1693
1821
|
isFocused,
|
|
1694
1822
|
isBlocked,
|
|
1695
|
-
path: path ? normalizeLogicalPath(path) : null
|
|
1823
|
+
path: path ? normalizeLogicalPath(path) : null,
|
|
1824
|
+
issues
|
|
1696
1825
|
},
|
|
1697
1826
|
progressKnown,
|
|
1698
1827
|
title,
|
|
@@ -1785,16 +1914,22 @@ export {
|
|
|
1785
1914
|
inspectFocusTree,
|
|
1786
1915
|
inspectArchiveTree,
|
|
1787
1916
|
inspectWorkTree,
|
|
1917
|
+
GROUP_REOPEN_COMPLETION_PREFIX,
|
|
1788
1918
|
resolveExecutableChange,
|
|
1789
1919
|
generateGroupBriefContent,
|
|
1790
1920
|
inspectChangeGroups,
|
|
1921
|
+
getGroupReopenEvidence,
|
|
1791
1922
|
hasArchivedGroupBrief,
|
|
1923
|
+
IssueRelationshipError,
|
|
1924
|
+
parseIssueRelationships,
|
|
1925
|
+
createIssueRelationship,
|
|
1792
1926
|
HISTORY_DEFAULT_LIMIT,
|
|
1793
1927
|
HISTORY_MAX_LIMIT,
|
|
1794
1928
|
ArchiveHistoryError,
|
|
1795
1929
|
inspectArchiveHistory,
|
|
1796
1930
|
queryArchiveHistory,
|
|
1797
1931
|
selectArchiveHistoryRecord,
|
|
1932
|
+
selectArchivedGroupBrief,
|
|
1798
1933
|
readArchiveHistoryDetail,
|
|
1799
1934
|
historyInspectionComplete,
|
|
1800
1935
|
inspectChangeDependencies,
|