dsh-pstack 0.2.0-beta.3 → 0.2.1

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 (39) hide show
  1. package/README.md +35 -16
  2. package/README.zh.md +36 -17
  3. package/assets/pstack-config-entry.png +0 -0
  4. package/cordis.patch.yml +12 -0
  5. package/dist/client.mjs +2448 -0
  6. package/dist/client.mjs.map +1 -0
  7. package/dist/index.mjs +1 -1
  8. package/dist/{paths-BT77khwm.mjs → paths-yji9SNak.mjs} +0 -1
  9. package/dist/pstack/docs/harness/dsh.md +7 -7
  10. package/dist/remote.d.mts +27 -0
  11. package/dist/remote.mjs +235 -0
  12. package/dist/role-catalog.d.mts +35 -0
  13. package/dist/role-catalog.mjs +122 -0
  14. package/dist/role-config-a4JpVdc-.mjs +379 -0
  15. package/dist/role-manifest-79cnFfDM.mjs +112 -0
  16. package/dist/role-manifest-8OERX9-s.d.mts +90 -0
  17. package/dist/role-wire-CAJsVA6e.d.mts +54 -0
  18. package/dist/roles.d.mts +18 -0
  19. package/dist/roles.mjs +293 -0
  20. package/dist/script.mjs +1 -1
  21. package/dist/skills/authored/decision-audit/SKILL.md +21 -17
  22. package/dist/skills/authored/decision-audit/SKILL.zh.md +21 -17
  23. package/dist/skills/authored/decision-audit/references/recover-commit.md +3 -3
  24. package/dist/skills/authored/decision-audit/references/recover-commit.zh.md +3 -3
  25. package/dist/skills/authored/decision-write/SKILL.md +11 -9
  26. package/dist/skills/authored/decision-write/SKILL.zh.md +11 -9
  27. package/dist/skills/authored/decision-write/references/land.md +3 -3
  28. package/dist/skills/authored/decision-write/references/land.zh.md +3 -3
  29. package/dist/skills/authored/decision-write/references/scope.md +16 -0
  30. package/dist/skills/authored/decision-write/references/scope.zh.md +16 -0
  31. package/dist/skills/authored/docs-drift/SKILL.md +120 -0
  32. package/dist/skills/authored/docs-drift/SKILL.zh.md +120 -0
  33. package/dist/skills/authored/setup-project-docs/SKILL.md +14 -9
  34. package/dist/skills/authored/setup-project-docs/SKILL.zh.md +14 -9
  35. package/dist/skills/authored/setup-project-docs/references/decision-conventions.md +3 -1
  36. package/dist/skills/authored/setup-project-docs/references/decision-conventions.zh.md +3 -1
  37. package/dist/subagent.d.mts +27 -20
  38. package/dist/subagent.mjs +71 -448
  39. package/package.json +34 -3
package/dist/roles.mjs ADDED
@@ -0,0 +1,293 @@
1
+ import { collectCatalogue } from "./models.mjs";
2
+ import { i as findRoleSpec, o as normalizeRoleName, r as ROLE_MANIFEST, t as INHERIT_PARENT_ALIASES } from "./role-manifest-79cnFfDM.mjs";
3
+ import { REMOTE_PACKAGE, ROLES_SERVICE, pstackRolesRemote } from "./remote.mjs";
4
+ import { i as resolveDshHome, n as readRoleConfiguration, o as updateRoleDocument, t as modelsFilePath } from "./role-config-a4JpVdc-.mjs";
5
+ import { dirname } from "node:path";
6
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
7
+ import { Service } from "@deepseek-ai/cordis";
8
+ import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
9
+ import { randomUUID } from "node:crypto";
10
+ // src/internal/role-editor.ts
11
+ /**
12
+ * Role-file editing behind the Plugins configuration page.
13
+ *
14
+ * The writer changes exactly one role line and leaves every other byte of the
15
+ * file alone: unknown lines, malformed lines, comments, and `# budget` survive a
16
+ * save. Saves are validated against the live `pstack_models` catalogue before
17
+ * the first byte is written, so a rejected save leaves the file unchanged.
18
+ */
19
+ function routeView(route) {
20
+ if (route.kind === "inherit") return {
21
+ kind: "inherit",
22
+ alias: route.alias ?? "inherit-parent"
23
+ };
24
+ return {
25
+ kind: "route",
26
+ provider: route.provider,
27
+ model: route.model,
28
+ ...route.effort === void 0 ? {} : { effort: route.effort }
29
+ };
30
+ }
31
+ function roleView(spec, entry) {
32
+ return {
33
+ key: spec.key,
34
+ shape: spec.shape,
35
+ configured: entry !== void 0,
36
+ ...entry === void 0 ? {} : { line: entry.line },
37
+ routes: entry === void 0 ? [] : entry.routes.map(routeView),
38
+ issues: entry === void 0 ? [] : [...entry.issues]
39
+ };
40
+ }
41
+ /** Project a parsed file onto the manifest, preserving order and per-line issues. */
42
+ function snapshotFromConfiguration(config) {
43
+ if (config.status === "unreadable") return {
44
+ path: config.path,
45
+ status: "unreadable",
46
+ reason: config.reason,
47
+ roles: ROLE_MANIFEST.map((spec) => roleView(spec, void 0)),
48
+ issues: [],
49
+ canSave: false
50
+ };
51
+ if (config.status === "missing") return {
52
+ path: config.path,
53
+ status: "missing",
54
+ roles: ROLE_MANIFEST.map((spec) => roleView(spec, void 0)),
55
+ issues: [],
56
+ canSave: true
57
+ };
58
+ return {
59
+ path: config.path,
60
+ status: "loaded",
61
+ ...config.budget === void 0 ? {} : { budget: config.budget },
62
+ roles: ROLE_MANIFEST.map((spec) => roleView(spec, config.roles.get(spec.key))),
63
+ issues: config.issues.map((issue) => ({
64
+ ...issue.line === void 0 ? {} : { line: issue.line },
65
+ message: issue.message
66
+ })),
67
+ canSave: true
68
+ };
69
+ }
70
+ /** Validate one save against the manifest and the live catalogue. */
71
+ function validateSaveRequest(request, catalogue) {
72
+ if (typeof request !== "object" || request === null || Array.isArray(request)) return {
73
+ ok: false,
74
+ reason: "the save request must be an object"
75
+ };
76
+ if (typeof request.key !== "string") return {
77
+ ok: false,
78
+ reason: "the save request must name a role"
79
+ };
80
+ const spec = findRoleSpec(normalizeRoleName(request.key));
81
+ if (spec === void 0) return {
82
+ ok: false,
83
+ reason: `unknown role ${JSON.stringify(request.key)}`
84
+ };
85
+ if (!Array.isArray(request.routes)) return {
86
+ ok: false,
87
+ reason: `role "${spec.key}" must carry a route list`
88
+ };
89
+ if (spec.shape === "single" && request.routes.length !== 1) return {
90
+ ok: false,
91
+ reason: `role "${spec.key}" holds a single route; save exactly one`
92
+ };
93
+ if (request.routes.length === 0) return {
94
+ ok: false,
95
+ reason: `role "${spec.key}" must keep at least one route`
96
+ };
97
+ const routes = [];
98
+ for (let index = 0; index < request.routes.length; index += 1) {
99
+ const route = request.routes[index];
100
+ const position = request.routes.length === 1 ? "the route" : `route ${index + 1}`;
101
+ if (typeof route !== "object" || route === null || Array.isArray(route)) return {
102
+ ok: false,
103
+ reason: `${position} must be an object`
104
+ };
105
+ if (route.kind === "inherit") {
106
+ const alias = route.alias ?? "inherit-parent";
107
+ if (!INHERIT_PARENT_ALIASES.includes(alias)) return {
108
+ ok: false,
109
+ reason: `${position} has unknown inheritance alias ${JSON.stringify(alias)}; use \`inherit-parent\` or \`auto\``
110
+ };
111
+ routes.push({
112
+ kind: "inherit",
113
+ alias
114
+ });
115
+ continue;
116
+ }
117
+ const unknownKind = route.kind;
118
+ if (unknownKind !== "route") return {
119
+ ok: false,
120
+ reason: `${position} has unknown kind ${JSON.stringify(unknownKind)}`
121
+ };
122
+ const provider = typeof route.provider === "string" ? route.provider.trim() : "";
123
+ const model = typeof route.model === "string" ? route.model.trim() : "";
124
+ if (provider === "" || model === "") return {
125
+ ok: false,
126
+ reason: `${position} needs both a provider and a model`
127
+ };
128
+ const providerEntry = catalogue.providers.find((candidate) => candidate.provider === provider);
129
+ if (providerEntry === void 0) return {
130
+ ok: false,
131
+ reason: `provider ${JSON.stringify(provider)} is not in the live model directory; run \`pstack_models\` and pick a registered provider`
132
+ };
133
+ if (providerEntry.error !== void 0) return {
134
+ ok: false,
135
+ reason: `provider ${JSON.stringify(provider)} could not be discovered: ${providerEntry.error}`
136
+ };
137
+ const modelEntry = providerEntry.models.find((candidate) => candidate.id === model);
138
+ if (modelEntry === void 0) return {
139
+ ok: false,
140
+ reason: `provider ${JSON.stringify(provider)} does not advertise model ${JSON.stringify(model)}`
141
+ };
142
+ if (modelEntry.error !== void 0) return {
143
+ ok: false,
144
+ reason: `model ${JSON.stringify(`${provider}/${model}`)} could not be resolved: ${modelEntry.error}`
145
+ };
146
+ const effort = typeof route.effort === "string" ? route.effort.trim() : route.effort;
147
+ if (effort !== void 0 && effort !== "") {
148
+ if (!modelEntry.efforts.includes(effort)) return {
149
+ ok: false,
150
+ reason: `model ${JSON.stringify(`${provider}/${model}`)} does not declare effort ${JSON.stringify(effort)}; declared efforts: ${modelEntry.efforts.length === 0 ? "none" : modelEntry.efforts.join(", ")}`
151
+ };
152
+ routes.push({
153
+ kind: "route",
154
+ provider,
155
+ model,
156
+ effort
157
+ });
158
+ continue;
159
+ }
160
+ if (effort === "") return {
161
+ ok: false,
162
+ reason: `${position} has an empty effort; drop the effort or pick one`
163
+ };
164
+ routes.push({
165
+ kind: "route",
166
+ provider,
167
+ model
168
+ });
169
+ }
170
+ return {
171
+ ok: true,
172
+ value: {
173
+ spec,
174
+ routes
175
+ }
176
+ };
177
+ }
178
+ /** Render one role's routes in the role-file grammar. */
179
+ function renderRoleValue(routes) {
180
+ return routes.map((route) => route.kind === "inherit" ? route.alias ?? "inherit-parent" : route.effort === void 0 ? `${route.provider}/${route.model}` : `${route.provider}/${route.model} [${route.effort}]`).join(", ");
181
+ }
182
+ /** Update the winning role through the same line grammar used by the parser. */
183
+ function updateRoleText(text, spec, value) {
184
+ return updateRoleDocument(text, spec, value);
185
+ }
186
+ function writeAtomically(path, text) {
187
+ mkdirSync(dirname(path), { recursive: true });
188
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
189
+ try {
190
+ writeFileSync(temporary, text, "utf8");
191
+ renameSync(temporary, path);
192
+ } catch (error) {
193
+ rmSync(temporary, { force: true });
194
+ throw error;
195
+ }
196
+ }
197
+ /**
198
+ * Save one role and return the reloaded snapshot.
199
+ *
200
+ * A missing file is created with just this role. An unreadable file is never
201
+ * overwritten. Every refusal leaves the existing bytes untouched.
202
+ */
203
+ function saveRoleConfiguration(path, request, catalogue) {
204
+ const current = readRoleConfiguration(path);
205
+ if (current.status === "unreadable") return {
206
+ ok: false,
207
+ reason: `the role file is unreadable: ${current.reason}`
208
+ };
209
+ const validation = validateSaveRequest(request, catalogue);
210
+ if (!validation.ok) return validation;
211
+ let existing = "";
212
+ if (current.status === "loaded") try {
213
+ existing = readFileSync(path, "utf8");
214
+ } catch (error) {
215
+ return {
216
+ ok: false,
217
+ reason: `the role file could not be read: ${describeError(error)}`
218
+ };
219
+ }
220
+ const next = updateRoleText(existing, validation.value.spec, renderRoleValue(validation.value.routes));
221
+ try {
222
+ writeAtomically(path, next);
223
+ } catch (error) {
224
+ return {
225
+ ok: false,
226
+ reason: `the role file could not be written: ${describeError(error)}`
227
+ };
228
+ }
229
+ return {
230
+ ok: true,
231
+ snapshot: snapshotFromConfiguration(readRoleConfiguration(path))
232
+ };
233
+ }
234
+ function describeError(error) {
235
+ if (error instanceof Error) {
236
+ const code = error.code;
237
+ return typeof code === "string" ? `${error.message} (${code})` : error.message;
238
+ }
239
+ return String(error);
240
+ }
241
+ // src/roles.ts
242
+ /**
243
+ * `pstack-roles`: the Host half of the role model configuration page.
244
+ *
245
+ * It owns no cached policy: every read and write goes to
246
+ * `$DSH_HOME/pstack-models.mdc`, which `pstack_subagent` also re-reads on every
247
+ * delegation. Saving therefore takes effect on the next delegation without a
248
+ * restart, and an already running child keeps the route it started with.
249
+ */
250
+ /** Read and write the role file for the Plugins configuration page. */
251
+ var PstackRolesService = class extends TypertRemoteService {
252
+ static inject = ["llm", "typert"];
253
+ constructor(ctx) {
254
+ super(ctx, ROLES_SERVICE);
255
+ }
256
+ [Service.init]() {
257
+ const typert = this.ctx.typert;
258
+ if (typert === void 0) throw new Error("pstack-roles requires the Typert registry service");
259
+ typert.register({
260
+ package: REMOTE_PACKAGE,
261
+ face: "host",
262
+ schemas: [],
263
+ model: {
264
+ services: [],
265
+ events: [],
266
+ objects: []
267
+ },
268
+ invocations: pstackRolesRemote.descriptors
269
+ });
270
+ }
271
+ /** Current path, status, values, and per-line issues. */
272
+ status() {
273
+ return snapshotFromConfiguration(readRoleConfiguration(roleFilePath()));
274
+ }
275
+ /** The live directory the page offers for its provider/model/effort selects. */
276
+ async catalogue(signal) {
277
+ return collectCatalogue(this.llm, void 0, signal);
278
+ }
279
+ /** Validate and save one role, then return the reloaded snapshot. */
280
+ async save(request, signal) {
281
+ signal.throwIfAborted();
282
+ const catalogue = await collectCatalogue(this.llm, void 0, signal);
283
+ signal.throwIfAborted();
284
+ return saveRoleConfiguration(roleFilePath(), request, catalogue);
285
+ }
286
+ get llm() {
287
+ return this.ctx.llm;
288
+ }
289
+ };
290
+ function roleFilePath() {
291
+ return modelsFilePath(resolveDshHome());
292
+ }
293
+ export { PstackRolesService, PstackRolesService as default };
package/dist/script.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as PORT_ROOT } from "./paths-BT77khwm.mjs";
1
+ import { i as PORT_ROOT } from "./paths-yji9SNak.mjs";
2
2
  import { isAbsolute, join, resolve } from "node:path";
3
3
  import { spawn } from "node:child_process";
4
4
  // src/internal/script-registry.ts
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: decision-audit
3
- description: Audit decision records against a change set or the whole repository; report routing, status, scope, anchor, supersession, and review findings with command evidence.
3
+ description: Read-only audit of decision records for a PR, local changes, or the whole repository. Report routing, status, scope, anchor, supersession, and overdue-review findings with command evidence.
4
4
  disable-model-invocation: true
5
5
  ---
6
6
 
@@ -12,13 +12,15 @@ Produce a read-only report of records needing human attention, without judging w
12
12
 
13
13
  Accept `mode` (`pr`, `local`, or `full`; default `pr`), optional `base`, and optional `scope` paths replacing the computed change set.
14
14
 
15
- For `pr`, resolve the PR base; otherwise try `origin/main`. When absent, try `origin/master`, `main`, `master`, the newest tag, then `HEAD~20`. Report the selected ref, or stop and list every attempted ref.
15
+ For `pr`, resolve the PR base. For `local` or `full`, try `origin/main`. When absent, try `origin/master`, `main`, `master`, the newest tag, then `HEAD~20`. Report the selected ref. An unresolved base blocks base-relative change sets and commit-anchor checks; continue the other checks and list every attempted ref under `Unverified`.
16
16
 
17
- Inspect `docs/decisions/`, `docs/adr/`, and `doc/adr/`:
17
+ Resolve `<dir>` in this order:
18
18
 
19
- - One exists: call it `<dir>` and report its path.
20
- - Several exist: ask which is current and wait.
21
- - None exists or it contains no records: report that normal result and stop.
19
+ 1. Use a caller-supplied record or directory path.
20
+ 2. Otherwise, resolve the decision pointer in root `AGENTS.md` under `## Project docs` when it points to an existing `<dir>/AGENTS.md`.
21
+ 3. Otherwise, inspect `docs/decisions/`, `docs/adr/`, and `doc/adr/`.
22
+
23
+ Report the selected path. If several candidates remain, ask which is current and wait. If none exists or it contains no records, report that normal result and stop.
22
24
 
23
25
  ## 2. Collect the evidence
24
26
 
@@ -33,24 +35,26 @@ git diff --name-only "$(git merge-base HEAD <base>)"..HEAD
33
35
  git status --porcelain=v1 --untracked-files=all
34
36
  ```
35
37
 
36
- Decode `git status` paths, including the destination of a rename. For `full`, use all tracked files. Exclude lock files, generated files, snapshots, and pure assets from the computed set; record these exclusions and the final set.
38
+ Decode `git status` paths, including the destination of a rename. For `full`, use all tracked files. Exclude lock files, generated files, snapshots, and pure assets from the computed set; record these exclusions and the final set. An explicit `scope` replaces the computed set but does not change base-dependent anchor checks.
39
+
40
+ Read `<dir>/AGENTS.md` and `<dir>/README.md` when present; these are conventions, not records. Apply their conventions to every other `*.md`. Parse each header through its first `## ` heading. List records missing any of `Status`, `Date`, `Scope`, `Implemented-by`, or `Review` separately as `Legacy`; they receive no further checks until the required fields are restored. `Superseded-by` is optional for current records; check it only under E.
37
41
 
38
- Read `<dir>/AGENTS.md` and `<dir>/README.md` when present; these are conventions, not records. Apply their conventions to every other `*.md`. Parse each header through its first `## ` heading. List records missing any of `Status`, `Date`, `Scope`, `Implemented-by`, or `Review` separately as `Legacy`, exempt from all six checks.
42
+ Apply every applicable check to every current record; a record may have multiple findings. Use [Match Scope paths](../decision-write/references/scope.md) for every glob/path check and retain the matches as evidence.
39
43
 
40
- Apply every applicable check to every current record; a record may have multiple findings.
44
+ ## 3. Run the checks
41
45
 
42
- ## 3. Apply all six checks
46
+ Apply A, B, B′, and D only to non-`superseded` records; apply C and E as specified below.
43
47
 
44
- **A — Route.** For each non-`superseded` record whose `Scope` matches the change set, report its title, path, matched paths, and change-set evidence in one line.
48
+ **A — Route.** For each record whose `Scope` matches the change set, report its title, path, matched paths, and change-set evidence in one line.
45
49
 
46
50
  **B — Status pair.** Use the anchor results below and report each matching row:
47
51
 
48
52
  | Status | Anchor result | Finding |
49
53
  |---|---|---|
50
54
  | `proposed` | verified | Should be `accepted` |
51
- | `accepted` | `-`, missing, pending, or unverified | Landing lacks verified evidence |
55
+ | `accepted` | `-` or missing | Landing lacks verified evidence |
52
56
 
53
- For `proposed`, `-` or a pending anchor is not a finding; an unverified anchor belongs in `Unverified`, outside B and C.
57
+ For `proposed`, `-` is pending. For either `proposed` or `accepted`, a pending anchor is not a finding; report the record as pending. An unverified anchor belongs in `Unverified`, outside B and C.
54
58
 
55
59
  **B′ — Dead scope.** For each `accepted` record with no `Scope` glob matching an existing file, report its path and glob-check evidence. Ask whether the decision is obsolete or its scope is wrong.
56
60
 
@@ -67,14 +71,14 @@ For a landed PR or ancestral commit, list its paths with `gh pr view N --json fi
67
71
 
68
72
  Before reporting a commit as lost, read [Recover a rewritten commit](references/recover-commit.md) and attempt patch-identity recovery while its object remains available.
69
73
 
70
- **D — Overdue review.** In `full` mode only, report each record whose `Review` date is before the current date. Collect the changes since the decision date:
74
+ **D — Overdue review.** In `full` mode only, report each record whose `Review` date is before the current date. Collect matching changes since the decision date:
71
75
 
72
76
  ```bash
73
77
  date +%F
74
- git log --since=<Date> --oneline -- <Scope paths>
78
+ git log --since=<Date> --format=%H <base> --
75
79
  ```
76
80
 
77
- Include the count and latest subject when present; suggest human review and a new `Review` date.
81
+ List each commit's paths and filter them with the shared matcher before counting it; the record's `Scope` value is matcher input, not a Git pathspec. Include the matched-commit count and latest subject when present. An overdue date is the finding even when there are no matching changes; suggest human review and a new `Review` date.
78
82
 
79
83
  **E — Superseded record.** For each current `superseded` record, report when `Superseded-by` is missing, does not resolve to a record in `<dir>`, or carries no ` — <reason>` text. Suggest adding the replacing record path and a one-line reason.
80
84
 
@@ -96,4 +100,4 @@ Use this structure for findings; include `Evidence` for every class:
96
100
  Suggest: replace the anchor with commit:8ff9f762 (matching patch-id d064e03f4e53)
97
101
  ```
98
102
 
99
- List `Legacy` and `Unverified` separately from findings. When nothing needs attention, say so in one line. An unavailable or unauthenticated `gh` leaves PR anchors unverified while the other checks continue.
103
+ List `Legacy` and `Unverified` separately from findings. An unavailable or unauthenticated `gh` leaves PR anchors unverified while the other checks continue. Say when no current record carries a finding. Route all edits to `decision-write`; suggest `/setup-project-docs` only when the decision directory or its root pointer is missing.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: decision-audit
3
- description: 按改动集合或整个仓库审计决策记录,报告路由、状态、范围、锚点、取代与复核发现,并附命令证据。
3
+ description: 只读审计 PR、本地改动或整个仓库的决策记录,报告路由、状态、范围、锚点、取代与复核逾期发现,并附命令证据。
4
4
  disable-model-invocation: true
5
5
  ---
6
6
 
@@ -12,13 +12,15 @@ disable-model-invocation: true
12
12
 
13
13
  接受 `mode`(`pr`、`local` 或 `full`,默认 `pr`)、可选的 `base`,以及用于替代计算所得改动集合的可选 `scope` 路径。
14
14
 
15
- `pr` 解析 PR 基线,其他模式先尝试 `origin/main`。不存在时依次尝试 `origin/master`、`main`、`master`、最新 tag、`HEAD~20`。报告实际选中的 ref;全部无法解析时停止并列出每个尝试过的 ref。
15
+ `pr` 解析 PR 基线,`local` 或 `full` 先尝试 `origin/main`。不存在时依次尝试 `origin/master`、`main`、`master`、最新 tag、`HEAD~20`。报告实际选中的 ref。基线无法解析会阻止相对基线的改动集合计算与提交锚点校验;继续其他检查,并在 `Unverified` 中列出每个尝试过的 ref。
16
16
 
17
- 检查 `docs/decisions/`、`docs/adr/` 和 `doc/adr/`:
17
+ 按以下顺序定位 `<dir>`:
18
18
 
19
- - 只有一个存在:记为 `<dir>` 并报告路径。
20
- - 多个存在:询问哪个是当前目录并等待。
21
- - 都不存在或没有记录:报告这一正常结果并停止。
19
+ 1. 使用调用方提供的记录或目录路径。
20
+ 2. 否则,解析根目录 `AGENTS.md` 的 `## Project docs` 中的决策指针;目标为现存的 `<dir>/AGENTS.md` 时使用该目录。
21
+ 3. 否则,检查 `docs/decisions/`、`docs/adr/` 和 `doc/adr/`。
22
+
23
+ 报告选中的路径。仍有多个候选时,询问哪个是当前目录并等待。都不存在或没有记录时,报告这一正常结果并停止。
22
24
 
23
25
  ## 2. 收集证据
24
26
 
@@ -33,24 +35,26 @@ git diff --name-only "$(git merge-base HEAD <base>)"..HEAD
33
35
  git status --porcelain=v1 --untracked-files=all
34
36
  ```
35
37
 
36
- 解析 `git status` 中的路径,重命名取目标路径。`full` 使用所有受 Git 跟踪的文件。从计算结果中剔除锁文件、生成文件、快照和纯资源文件;记录这些排除项与最终集合。
38
+ 解析 `git status` 中的路径,重命名取目标路径。`full` 使用所有受 Git 跟踪的文件。从计算结果中剔除锁文件、生成文件、快照和纯资源文件;记录这些排除项与最终集合。显式 `scope` 替换计算所得集合,但不改变依赖基线的锚点校验。
39
+
40
+ 存在 `<dir>/AGENTS.md` 和 `<dir>/README.md` 时读取;它们是约定而非记录。将其约定应用到其他所有 `*.md`。读取每条记录至第一个 `## ` 标题并解析头部。缺少 `Status`、`Date`、`Scope`、`Implemented-by`、`Review` 中任一项的记录单列为 `Legacy`,补齐必需字段前不执行后续检查。现行记录的 `Superseded-by` 可选,只在 E 类检查中处理。
37
41
 
38
- 存在 `<dir>/AGENTS.md` 和 `<dir>/README.md` 时读取;它们是约定而非记录。将其约定应用到其他所有 `*.md`。读取每条记录至第一个 `## ` 标题并解析头部。缺少 `Status`、`Date`、`Scope`、`Implemented-by`、`Review` 中任一项的记录单列为 `Legacy`,不参与以下六项检查。
42
+ 对每条现行记录执行所有适用的检查;同一记录可以产生多项发现。所有 glob/路径检查都使用[匹配 Scope 路径](../decision-write/references/scope.zh.md),并保留匹配结果作为证据。
39
43
 
40
- 对每条现行记录执行所有适用的检查;同一记录可以产生多项发现。
44
+ ## 3. 执行检查
41
45
 
42
- ## 3. 执行全部六项检查
46
+ A、B、B′ 和 D 只用于非 `superseded` 记录;C 和 E 按下方说明执行。
43
47
 
44
- **A——路由。** 对每条非 `superseded` 且 `Scope` 匹配改动集合的记录,用一行报告标题、路径、匹配路径及改动集合证据。
48
+ **A——路由。** 对每条 `Scope` 匹配改动集合的记录,用一行报告标题、路径、匹配路径及改动集合证据。
45
49
 
46
50
  **B——状态成对。** 使用下方的锚点结果,报告每个匹配行:
47
51
 
48
52
  | Status | 锚点结果 | 发现 |
49
53
  |---|---|---|
50
54
  | `proposed` | 已核验 | 应改为 `accepted` |
51
- | `accepted` | `-`、缺失、待落地或未核验 | 落地状态缺少已核验证据 |
55
+ | `accepted` | `-` 或缺失 | 落地状态缺少已核验证据 |
52
56
 
53
- `proposed` 带 `-` 或待落地锚点不是发现;带未核验锚点时归入 `Unverified`,不归入 B 或 C。
57
+ `proposed` 带 `-` 时归为待落地。`proposed` 或 `accepted` 带待落地锚点都不是发现;将记录归为待落地。带未核验锚点时归入 `Unverified`,不归入 B 或 C。
54
58
 
55
59
  **B′——范围失效。** 对 `Scope` 中所有 glob 都不匹配现存文件的每条 `accepted` 记录,报告路径与 glob 检查证据,并询问是决策已过时还是范围写错。
56
60
 
@@ -67,14 +71,14 @@ git status --porcelain=v1 --untracked-files=all
67
71
 
68
72
  报告提交丢失前,读取[恢复被改写的提交](references/recover-commit.zh.md);只要其对象仍可用,就尝试恢复 patch 标识。
69
73
 
70
- **D——复核逾期。** 仅在 `full` 模式,报告 `Review` 日期早于当前日期的每条记录,并收集决策日期以来的改动:
74
+ **D——复核逾期。** 仅在 `full` 模式,报告 `Review` 日期早于当前日期的每条记录,并收集决策日期以来匹配 `Scope` 的改动:
71
75
 
72
76
  ```bash
73
77
  date +%F
74
- git log --since=<Date> --oneline -- <Scope paths>
78
+ git log --since=<Date> --format=%H <base> --
75
79
  ```
76
80
 
77
- 附上提交数量和最新主题(如有),建议人工复核并更新 `Review` 日期。
81
+ 列出每个提交的路径,先用共享匹配器筛选,再计数;记录的 `Scope` 是匹配器输入,不是 Git pathspec。附上匹配提交的数量和最新主题(如有)。日期逾期本身就是发现,即使没有匹配改动;建议人工复核并更新 `Review` 日期。
78
82
 
79
83
  **E——取代记录。** 对每条现行 `superseded` 记录,在 `Superseded-by` 缺失、无法解析到 `<dir>` 内的记录,或没有 ` — <原因>` 文本时报告。建议补上替代记录路径和一句原因。
80
84
 
@@ -96,4 +100,4 @@ git log --since=<Date> --oneline -- <Scope paths>
96
100
  Suggest: replace the anchor with commit:8ff9f762 (matching patch-id d064e03f4e53)
97
101
  ```
98
102
 
99
- `Legacy` 与 `Unverified` 分别单列,不计为发现。无需关注时用一行说明。`gh` 缺失或未认证时,将 PR 锚点标为未核验,继续其他检查。
103
+ `Legacy` 与 `Unverified` 分别单列,不计为发现。`gh` 缺失或未认证时,将 PR 锚点标为未核验,继续其他检查。没有现行记录带发现时明确说明。所有修改都交给 `decision-write`;仅在决策目录或根指针缺失时建议使用 `/setup-project-docs`。
@@ -9,9 +9,9 @@ git show <dead-sha> | git patch-id --stable
9
9
  Search plausible replacements within the record's date and scope:
10
10
 
11
11
  ```bash
12
- git log --since=<Date> --format=%H -- <Scope paths> |
13
- while read -r sha; do git show "$sha" | git patch-id --stable; done |
12
+ git log --since=<Date> --format=%H <base> -- |
13
+ while read -r sha; do git show "$sha" | git patch-id --stable | awk -v sha="$sha" '{print $1, sha}'; done |
14
14
  grep -F "<patch-id>"
15
15
  ```
16
16
 
17
- A match supports suggesting the replacement SHA; otherwise leave replacement to a human. Prefer PR anchors in squash-merged repositories.
17
+ Filter each candidate commit's paths with the shared `Scope` matcher before comparing its patch identity. A match supports suggesting the replacement SHA; otherwise leave replacement to a human. Prefer PR anchors in squash-merged repositories.
@@ -9,9 +9,9 @@ git show <dead-sha> | git patch-id --stable
9
9
  在记录的日期与范围内搜索可能的替代提交:
10
10
 
11
11
  ```bash
12
- git log --since=<Date> --format=%H -- <Scope paths> |
13
- while read -r sha; do git show "$sha" | git patch-id --stable; done |
12
+ git log --since=<Date> --format=%H <base> -- |
13
+ while read -r sha; do git show "$sha" | git patch-id --stable | awk -v sha="$sha" '{print $1, sha}'; done |
14
14
  grep -F "<patch-id>"
15
15
  ```
16
16
 
17
- 命中可支持建议改用替代 SHA;未命中则留给人判断替代项。使用 squash 合并的仓库优先采用 PR 锚点。
17
+ 比较 patch 标识前,先用共享 `Scope` 匹配器筛选每个候选提交的路径。命中可支持建议改用替代 SHA;未命中则留给人判断替代项。使用 squash 合并的仓库优先采用 PR 锚点。
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: decision-write
3
- description: Write, update, land, or supersede a decision record. Use for resolved alternatives, missing design rationale, reversed decisions, or any record-file change. Land discovers and verifies the implementation.
3
+ description: Write, update, land, or supersede decision records. Use for resolved alternatives, missing rationale, reversed decisions, or record-file changes. Land finds and verifies the implementation; Review-only updates keep the current status.
4
4
  ---
5
5
 
6
6
  # Write or update a decision record
@@ -13,18 +13,20 @@ Use this procedure for every change to `Status`, `Implemented-by`, `Superseded-b
13
13
 
14
14
  ## Locate the records
15
15
 
16
- Inspect `docs/decisions/`, `docs/adr/`, and `doc/adr/`:
16
+ Resolve `<dir>` in this order:
17
17
 
18
- - One exists: call it `<dir>`, follow its naming convention, and read its `AGENTS.md` when present.
19
- - Several exist: ask which is current and wait.
20
- - None exists: report that this repository keeps no decision records, suggest `/setup-project-docs`, and stop.
18
+ 1. Use a caller-supplied record or directory path.
19
+ 2. Otherwise, resolve the decision pointer in root `AGENTS.md` under `## Project docs` when it points to an existing `<dir>/AGENTS.md`.
20
+ 3. Otherwise, inspect `docs/decisions/`, `docs/adr/`, and `doc/adr/`.
21
+
22
+ Use the selected directory, follow its naming convention, and read its `AGENTS.md` and `README.md` conventions when present. If several candidates remain, ask which is current and wait. If none exists, report that no decision directory was found, suggest `/setup-project-docs`, and stop.
21
23
 
22
24
  `AGENTS.md` and `README.md` are conventions, not records; exclude both from searches, classification, and updates.
23
25
 
24
26
  ## Choose the mode
25
27
 
26
28
  - **Draft:** no record owns this decision, a new record replaces an old one, or an existing record needs substantive body or relationship updates. Duplicate and overlapping dispositions stay in Draft; explicit status-field updates use Land. Do steps 1–5, present the approval gate in step 6, then write only after confirmation and report with step 7.
27
- - **Land:** the user explicitly asks to mark an existing record `accepted` or update its status fields. Do steps 4, 5, and 7. A record path plus `accepted` is sufficient input; never enter Land autonomously. Without a path, list records whose `Scope` matches the landed change and ask which to update.
29
+ - **Land:** the user explicitly asks to mark an existing record `accepted` or update its status fields. Read the full target record, then do steps 4, 5, and 7. A record path plus `accepted` is sufficient input; never enter Land autonomously. Without a path, list records whose `Scope` matches the landed change and ask which to update. For a Review-only update, preserve `Status` and `Implemented-by`, skip steps 4 and 5, and report with step 7.
28
30
 
29
31
  ## 1. Establish the choice
30
32
 
@@ -91,13 +93,13 @@ A new record stays `proposed` until it lands. `Implemented-by` identifies the im
91
93
 
92
94
  For Land without an anchor, or the record-only exception, read [Find and verify the implementation](references/land.md) before editing. For any other proposed anchor, use that reference's verification procedure without its discovery steps.
93
95
 
94
- Resolve `<base>` as the repository's default branch and report the selected ref. Verify every anchor; unavailable evidence or an unresolved base leaves a new record `proposed` + `-`. If an existing `accepted` record's anchor fails verification, stop and report it rather than silently demoting it. Change only fields required by evidence; advancing `Review` alone leaves status unchanged.
96
+ Resolve `<base>` as the repository's default branch and report the selected ref. Verify every anchor. In Draft, unavailable evidence or an unresolved base leaves a new record `proposed` + `-`; in Land, stop and report without editing. If an existing `accepted` record's anchor fails verification, stop and report it rather than silently demoting it. Change only fields required by evidence; advancing `Review` alone leaves status unchanged.
95
97
 
96
98
  When a new draft replaces an old record, set the old record to `superseded` in the same approved edit set, even when it was `accepted`. Keep its body and anchor, add the cross-link, and write `Superseded-by: <new record path> — <one-line reason>` below `Review`. Ask for a missing reason and wait. A `proposed` old record keeps `Implemented-by: -`.
97
99
 
98
100
  ## 5. Check scope covers the change
99
101
 
100
- Use caller-supplied `scope` paths when present. For Land with a verified implementing change, list its paths with:
102
+ Use [Match Scope paths](references/scope.md) for every glob/path check, including anchor discovery in Land. Use caller-supplied `scope` paths when present. For Land with a verified implementing change, list its paths with:
101
103
 
102
104
  ```bash
103
105
  gh pr view N --json files --jq '.files[].path'
@@ -119,7 +121,7 @@ For every Draft write, including a Duplicate update, present the complete result
119
121
 
120
122
  ## 7. Report
121
123
 
122
- Account for every search hit, changed record, relationship, anchor verification, scope match, and unresolved question. A `proposed` record awaiting landing needs a Land update after merge. Name any discovered implementation and its changed paths; mark a record-only anchor as a confirmed exception. Include every `Superseded-by` path and reason.
124
+ When root `AGENTS.md` has no resolving decision pointer for the selected `<dir>`, suggest `/setup-project-docs` to register it. Account for every search hit, changed record, relationship, anchor verification, scope match, and unresolved question. A `proposed` record awaiting landing needs a Land update after merge. Name any discovered implementation and its changed paths; mark a record-only anchor as a confirmed exception. Include every `Superseded-by` path and reason.
123
125
 
124
126
  ```text
125
127
  Record: docs/decisions/2026-10-27-use-postgres.md
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: decision-write
3
- description: 撰写、更新、落地或取代决策记录。用于已定取舍、缺失设计理由、推翻决策或任何记录文件改动;落地模式查找并校验实现。
3
+ description: 撰写、更新、落地或取代决策记录。用于已定取舍、缺失设计理由、推翻决策或记录文件改动;落地模式查找并校验实现,仅更新 `Review` 时保留当前状态。
4
4
  ---
5
5
 
6
6
  # 撰写或更新决策记录
@@ -13,18 +13,20 @@ description: 撰写、更新、落地或取代决策记录。用于已定取舍
13
13
 
14
14
  ## 定位记录目录
15
15
 
16
- 检查 `docs/decisions/`、`docs/adr/` 和 `doc/adr/`:
16
+ 按以下顺序定位 `<dir>`:
17
17
 
18
- - 只有一个存在:记为 `<dir>`,沿用其命名约定,并在存在时读取其 `AGENTS.md`。
19
- - 多个存在:询问哪个是当前目录并等待。
20
- - 都不存在:说明本仓库不保存决策记录,建议使用 `/setup-project-docs`,然后停止。
18
+ 1. 使用调用方提供的记录或目录路径。
19
+ 2. 否则,解析根目录 `AGENTS.md` 的 `## Project docs` 中的决策指针;目标为现存的 `<dir>/AGENTS.md` 时使用该目录。
20
+ 3. 否则,检查 `docs/decisions/`、`docs/adr/` 和 `doc/adr/`。
21
+
22
+ 沿用选中目录的命名约定,并在存在时读取其 `AGENTS.md` 与 `README.md` 约定。仍有多个候选时,询问哪个是当前目录并等待。都不存在时,报告未找到决策目录,建议使用 `/setup-project-docs`,然后停止。
21
23
 
22
24
  `AGENTS.md` 与 `README.md` 是约定而非记录;检索、分类与更新都排除它们。
23
25
 
24
26
  ## 选择模式
25
27
 
26
28
  - **撰写:** 没有既有记录拥有这个决策、新记录取代旧记录,或既有记录需要实质更新正文或关系。重复与部分重叠的归类留在撰写模式;明确的状态字段更新走落地。执行步骤 1–5,在步骤 6 展示确认关口,仅在确认后写入,再执行步骤 7 报告。
27
- - **落地:** 用户明确要求把既有记录标为 `accepted` 或更新其状态字段。执行步骤 4、5、7。记录路径加 `accepted` 就是足够输入;不要自行进入落地。未给出路径时,列出 `Scope` 匹配落地改动的记录并询问更新哪一条。
29
+ - **落地:** 用户明确要求把既有记录标为 `accepted` 或更新其状态字段。完整读取目标记录,再执行步骤 4、5、7。记录路径加 `accepted` 就是足够输入;不要自行进入落地。未给出路径时,列出 `Scope` 匹配落地改动的记录并询问更新哪一条。仅更新 `Review` 时保留 `Status` 与 `Implemented-by`,跳过步骤 4、5,执行步骤 7 报告。
28
30
 
29
31
  ## 1. 确认选择
30
32
 
@@ -91,13 +93,13 @@ Superseded-by: <记录路径> — <一句原因> (仅 Status 为 superseded
91
93
 
92
94
  落地模式没有给定锚点,或需要“仅记录提交”例外时,编辑前读取[查找并校验实现](references/land.zh.md)。校验其他已有锚点时,只执行该参考中的校验流程,不执行查找步骤。
93
95
 
94
- `<base>` 取仓库默认分支,并报告选中的 ref。校验每个锚点;证据不可用或基线无法解析时,新记录保持 `proposed` + `-`。既有 `accepted` 记录的锚点校验失败时,停下报告,不静默降级。只改证据要求的字段;单独推进 `Review` 不改变状态。
96
+ `<base>` 取仓库默认分支,并报告选中的 ref。校验每个锚点。撰写模式中,证据不可用或基线无法解析时,新记录保持 `proposed` + `-`;落地模式中停止并报告,不编辑记录。既有 `accepted` 记录的锚点校验失败时,停下报告,不静默降级。只改证据要求的字段;单独推进 `Review` 不改变状态。
95
97
 
96
98
  新草稿取代旧记录时,在同一次获批的修改中把旧记录改为 `superseded`,即使旧记录原为 `accepted`。保留旧正文与锚点,添加互链,并在 `Review` 下方写 `Superseded-by: <新记录路径> — <一句原因>`。原因缺失时先询问并等待。原为 `proposed` 的旧记录保留 `Implemented-by: -`。
97
99
 
98
100
  ## 5. 检查范围覆盖改动
99
101
 
100
- 调用方提供 `scope` 路径时直接使用。带已核验实现改动的落地分支,用以下命令列出实现路径:
102
+ 所有 glob/路径检查都使用[匹配 Scope 路径](references/scope.zh.md),包括落地模式的锚点查找。调用方提供 `scope` 路径时直接使用。带已核验实现改动的落地分支,用以下命令列出实现路径:
101
103
 
102
104
  ```bash
103
105
  gh pr view N --json files --jq '.files[].path'
@@ -119,7 +121,7 @@ git status --porcelain=v1 --untracked-files=all
119
121
 
120
122
  ## 7. 报告
121
123
 
122
- 覆盖每个检索命中、改动的记录、关系判断、锚点校验、范围匹配和未决问题。等待落地的 `proposed` 记录在合并后需要一次“落地”更新。报告查到的实现及其改动路径;仅记录锚点须标明为经确认的例外。包括每个 `Superseded-by` 路径和原因。
124
+ 根目录 `AGENTS.md` 没有可解析到所选 `<dir>` 的决策指针时,建议用 `/setup-project-docs` 登记。覆盖每个检索命中、改动的记录、关系判断、锚点校验、范围匹配和未决问题。等待落地的 `proposed` 记录在合并后需要一次“落地”更新。报告查到的实现及其改动路径;仅记录锚点须标明为经确认的例外。包括每个 `Superseded-by` 路径和原因。
123
125
 
124
126
  ```text
125
127
  Record: docs/decisions/2026-10-27-use-postgres.md