dsh-pstack 0.2.0-beta.3 → 0.2.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 (32) hide show
  1. package/README.md +16 -3
  2. package/README.zh.md +18 -5
  3. package/assets/pstack-config-entry.png +0 -0
  4. package/cordis.patch.yml +8 -0
  5. package/dist/client.mjs +2341 -0
  6. package/dist/client.mjs.map +1 -0
  7. package/dist/remote.d.mts +27 -0
  8. package/dist/remote.mjs +234 -0
  9. package/dist/role-config-NNn5r1JX.mjs +415 -0
  10. package/dist/role-manifest-C3fUzjLI.d.mts +19 -0
  11. package/dist/role-wire-DZwTn2wF.d.mts +55 -0
  12. package/dist/roles.d.mts +18 -0
  13. package/dist/roles.mjs +323 -0
  14. package/dist/skills/authored/decision-audit/SKILL.md +21 -17
  15. package/dist/skills/authored/decision-audit/SKILL.zh.md +21 -17
  16. package/dist/skills/authored/decision-audit/references/recover-commit.md +3 -3
  17. package/dist/skills/authored/decision-audit/references/recover-commit.zh.md +3 -3
  18. package/dist/skills/authored/decision-write/SKILL.md +11 -9
  19. package/dist/skills/authored/decision-write/SKILL.zh.md +11 -9
  20. package/dist/skills/authored/decision-write/references/land.md +3 -3
  21. package/dist/skills/authored/decision-write/references/land.zh.md +3 -3
  22. package/dist/skills/authored/decision-write/references/scope.md +16 -0
  23. package/dist/skills/authored/decision-write/references/scope.zh.md +16 -0
  24. package/dist/skills/authored/docs-drift/SKILL.md +120 -0
  25. package/dist/skills/authored/docs-drift/SKILL.zh.md +120 -0
  26. package/dist/skills/authored/setup-project-docs/SKILL.md +14 -9
  27. package/dist/skills/authored/setup-project-docs/SKILL.zh.md +14 -9
  28. package/dist/skills/authored/setup-project-docs/references/decision-conventions.md +3 -1
  29. package/dist/skills/authored/setup-project-docs/references/decision-conventions.zh.md +3 -1
  30. package/dist/subagent.d.mts +3 -18
  31. package/dist/subagent.mjs +5 -412
  32. package/package.json +32 -2
@@ -0,0 +1,55 @@
1
+ import "./models.mjs";
2
+ import { n as RoleValueShape } from "./role-manifest-C3fUzjLI.mjs";
3
+ /** Which spelling of "inherit the parent route" a role line uses. */
4
+ type InheritAlias = "inherit-parent" | "auto";
5
+ /** One role value as the configuration page sees it. */
6
+ type RouteView = {
7
+ readonly kind: "inherit";
8
+ readonly alias: InheritAlias;
9
+ } | {
10
+ readonly kind: "route";
11
+ readonly provider: string;
12
+ readonly model: string;
13
+ readonly effort?: string;
14
+ };
15
+ /** One of the 17 roles in manifest order. */
16
+ interface RoleView {
17
+ readonly key: string;
18
+ readonly shape: RoleValueShape;
19
+ /** Whether the role file configures this role. */
20
+ readonly configured: boolean;
21
+ /** The winning line, when the role file configures this role. */
22
+ readonly line?: number;
23
+ readonly routes: readonly RouteView[];
24
+ readonly issues: readonly string[];
25
+ }
26
+ /** One file-level problem. Unknown and malformed lines are reported, never dropped. */
27
+ interface RoleFileIssue {
28
+ readonly line?: number;
29
+ readonly message: string;
30
+ }
31
+ /** Everything the configuration page renders about `$DSH_HOME/pstack-models.mdc`. */
32
+ interface RoleFileSnapshot {
33
+ readonly path: string;
34
+ readonly status: "missing" | "loaded" | "unreadable";
35
+ readonly reason?: string;
36
+ readonly budget?: string;
37
+ readonly roles: readonly RoleView[];
38
+ readonly issues: readonly RoleFileIssue[];
39
+ /** False only when the file cannot be read at all. */
40
+ readonly canSave: boolean;
41
+ }
42
+ /** One role save. List roles carry every route, never only the edited one. */
43
+ interface SaveRoleRequest {
44
+ readonly key: string;
45
+ readonly routes: readonly RouteView[];
46
+ }
47
+ /** A refused save leaves the file untouched and names the reason. */
48
+ type SaveRoleResult = {
49
+ readonly ok: true;
50
+ readonly snapshot: RoleFileSnapshot;
51
+ } | {
52
+ readonly ok: false;
53
+ readonly reason: string;
54
+ };
55
+ export { SaveRoleRequest as n, SaveRoleResult as r, RoleFileSnapshot as t };
@@ -0,0 +1,18 @@
1
+ import { CatalogueResult } from "./models.mjs";
2
+ import { n as SaveRoleRequest, r as SaveRoleResult, t as RoleFileSnapshot } from "./role-wire-DZwTn2wF.mjs";
3
+ import { Context, Service } from "@deepseek-ai/cordis";
4
+ import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
5
+ /** Read and write the role file for the Plugins configuration page. */
6
+ export declare class PstackRolesService extends TypertRemoteService {
7
+ static inject: string[];
8
+ constructor(ctx: Context);
9
+ protected [Service.init](): void;
10
+ /** Current path, status, values, and per-line issues. */
11
+ status(): RoleFileSnapshot;
12
+ /** The live directory the page offers for its provider/model/effort selects. */
13
+ catalogue(signal: AbortSignal): Promise<CatalogueResult>;
14
+ /** Validate and save one role, then return the reloaded snapshot. */
15
+ save(request: SaveRoleRequest, signal: AbortSignal): Promise<SaveRoleResult>;
16
+ private get llm();
17
+ }
18
+ export { PstackRolesService as default };
package/dist/roles.mjs ADDED
@@ -0,0 +1,323 @@
1
+ import { collectCatalogue } from "./models.mjs";
2
+ import { REMOTE_PACKAGE, ROLES_SERVICE, pstackRolesRemote } from "./remote.mjs";
3
+ import { a as resolveDshHome, c as ROLE_MANIFEST, l as findRoleSpec, n as normalizeRoleName, r as readRoleConfiguration, s as INHERIT_PARENT_ALIASES, t as modelsFilePath } from "./role-config-NNn5r1JX.mjs";
4
+ import { dirname } from "node:path";
5
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
+ import { Service } from "@deepseek-ai/cordis";
7
+ import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
8
+ import { randomUUID } from "node:crypto";
9
+ // src/internal/role-editor.ts
10
+ /**
11
+ * Role-file editing behind the Plugins configuration page.
12
+ *
13
+ * The writer changes exactly one role line and leaves every other byte of the
14
+ * file alone: unknown lines, malformed lines, comments, and `# budget` survive a
15
+ * save. Saves are validated against the live `pstack_models` catalogue before
16
+ * the first byte is written, so a rejected save leaves the file unchanged.
17
+ */
18
+ function routeView(route) {
19
+ if (route.kind === "inherit") return {
20
+ kind: "inherit",
21
+ alias: route.alias ?? "inherit-parent"
22
+ };
23
+ return {
24
+ kind: "route",
25
+ provider: route.provider,
26
+ model: route.model,
27
+ ...route.effort === void 0 ? {} : { effort: route.effort }
28
+ };
29
+ }
30
+ function roleView(spec, entry) {
31
+ return {
32
+ key: spec.key,
33
+ shape: spec.shape,
34
+ configured: entry !== void 0,
35
+ ...entry === void 0 ? {} : { line: entry.line },
36
+ routes: entry === void 0 ? [] : entry.routes.map(routeView),
37
+ issues: entry === void 0 ? [] : [...entry.issues]
38
+ };
39
+ }
40
+ /** Project a parsed file onto the manifest, preserving order and per-line issues. */
41
+ function snapshotFromConfiguration(config) {
42
+ if (config.status === "unreadable") return {
43
+ path: config.path,
44
+ status: "unreadable",
45
+ reason: config.reason,
46
+ roles: ROLE_MANIFEST.map((spec) => roleView(spec, void 0)),
47
+ issues: [],
48
+ canSave: false
49
+ };
50
+ if (config.status === "missing") return {
51
+ path: config.path,
52
+ status: "missing",
53
+ roles: ROLE_MANIFEST.map((spec) => roleView(spec, void 0)),
54
+ issues: [],
55
+ canSave: true
56
+ };
57
+ return {
58
+ path: config.path,
59
+ status: "loaded",
60
+ ...config.budget === void 0 ? {} : { budget: config.budget },
61
+ roles: ROLE_MANIFEST.map((spec) => roleView(spec, config.roles.get(spec.key))),
62
+ issues: config.issues.map((issue) => ({
63
+ ...issue.line === void 0 ? {} : { line: issue.line },
64
+ message: issue.message
65
+ })),
66
+ canSave: true
67
+ };
68
+ }
69
+ /** Validate one save against the manifest and the live catalogue. */
70
+ function validateSaveRequest(request, catalogue) {
71
+ if (typeof request !== "object" || request === null || Array.isArray(request)) return {
72
+ ok: false,
73
+ reason: "the save request must be an object"
74
+ };
75
+ if (typeof request.key !== "string") return {
76
+ ok: false,
77
+ reason: "the save request must name a role"
78
+ };
79
+ const spec = findRoleSpec(normalizeRoleName(request.key));
80
+ if (spec === void 0) return {
81
+ ok: false,
82
+ reason: `unknown role ${JSON.stringify(request.key)}`
83
+ };
84
+ if (!Array.isArray(request.routes)) return {
85
+ ok: false,
86
+ reason: `role "${spec.key}" must carry a route list`
87
+ };
88
+ if (spec.shape === "single" && request.routes.length !== 1) return {
89
+ ok: false,
90
+ reason: `role "${spec.key}" holds a single route; save exactly one`
91
+ };
92
+ if (request.routes.length === 0) return {
93
+ ok: false,
94
+ reason: `role "${spec.key}" must keep at least one route`
95
+ };
96
+ const routes = [];
97
+ for (let index = 0; index < request.routes.length; index += 1) {
98
+ const route = request.routes[index];
99
+ const position = request.routes.length === 1 ? "the route" : `route ${index + 1}`;
100
+ if (typeof route !== "object" || route === null || Array.isArray(route)) return {
101
+ ok: false,
102
+ reason: `${position} must be an object`
103
+ };
104
+ if (route.kind === "inherit") {
105
+ const alias = route.alias ?? "inherit-parent";
106
+ if (!INHERIT_PARENT_ALIASES.includes(alias)) return {
107
+ ok: false,
108
+ reason: `${position} has unknown inheritance alias ${JSON.stringify(alias)}; use \`inherit-parent\` or \`auto\``
109
+ };
110
+ routes.push({
111
+ kind: "inherit",
112
+ alias
113
+ });
114
+ continue;
115
+ }
116
+ const unknownKind = route.kind;
117
+ if (unknownKind !== "route") return {
118
+ ok: false,
119
+ reason: `${position} has unknown kind ${JSON.stringify(unknownKind)}`
120
+ };
121
+ const provider = typeof route.provider === "string" ? route.provider.trim() : "";
122
+ const model = typeof route.model === "string" ? route.model.trim() : "";
123
+ if (provider === "" || model === "") return {
124
+ ok: false,
125
+ reason: `${position} needs both a provider and a model`
126
+ };
127
+ const providerEntry = catalogue.providers.find((candidate) => candidate.provider === provider);
128
+ if (providerEntry === void 0) return {
129
+ ok: false,
130
+ reason: `provider ${JSON.stringify(provider)} is not in the live model directory; run \`pstack_models\` and pick a registered provider`
131
+ };
132
+ if (providerEntry.error !== void 0) return {
133
+ ok: false,
134
+ reason: `provider ${JSON.stringify(provider)} could not be discovered: ${providerEntry.error}`
135
+ };
136
+ const modelEntry = providerEntry.models.find((candidate) => candidate.id === model);
137
+ if (modelEntry === void 0) return {
138
+ ok: false,
139
+ reason: `provider ${JSON.stringify(provider)} does not advertise model ${JSON.stringify(model)}`
140
+ };
141
+ if (modelEntry.error !== void 0) return {
142
+ ok: false,
143
+ reason: `model ${JSON.stringify(`${provider}/${model}`)} could not be resolved: ${modelEntry.error}`
144
+ };
145
+ const effort = typeof route.effort === "string" ? route.effort.trim() : route.effort;
146
+ if (effort !== void 0 && effort !== "") {
147
+ if (!modelEntry.efforts.includes(effort)) return {
148
+ ok: false,
149
+ reason: `model ${JSON.stringify(`${provider}/${model}`)} does not declare effort ${JSON.stringify(effort)}; declared efforts: ${modelEntry.efforts.length === 0 ? "none" : modelEntry.efforts.join(", ")}`
150
+ };
151
+ routes.push({
152
+ kind: "route",
153
+ provider,
154
+ model,
155
+ effort
156
+ });
157
+ continue;
158
+ }
159
+ if (effort === "") return {
160
+ ok: false,
161
+ reason: `${position} has an empty effort; drop the effort or pick one`
162
+ };
163
+ routes.push({
164
+ kind: "route",
165
+ provider,
166
+ model
167
+ });
168
+ }
169
+ return {
170
+ ok: true,
171
+ value: {
172
+ spec,
173
+ routes
174
+ }
175
+ };
176
+ }
177
+ /** Render one role's routes in the role-file grammar. */
178
+ function renderRoleValue(routes) {
179
+ 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(", ");
180
+ }
181
+ function bodyStart(lines) {
182
+ if (lines[0]?.trim() !== "---") return 0;
183
+ for (let index = 1; index < lines.length; index += 1) if (lines[index]?.trim() === "---") return index + 1;
184
+ return 0;
185
+ }
186
+ /**
187
+ * Replace the winning line for one role, or append one when absent.
188
+ *
189
+ * Every other line is returned verbatim. A role configured twice keeps its
190
+ * earlier line and replaces the last one, which is the line the parser treats
191
+ * as winning; the duplicate remains visible as a per-line issue.
192
+ */
193
+ function updateRoleText(text, spec, value) {
194
+ const lines = text.split("\n");
195
+ const crlf = text.includes("\r\n");
196
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
197
+ let match = -1;
198
+ for (let index = bodyStart(lines); index < lines.length; index += 1) {
199
+ const line = lines[index];
200
+ const trimmed = line.endsWith("\r") ? line.slice(0, -1).trim() : line.trim();
201
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
202
+ const separator = trimmed.indexOf(":");
203
+ if (separator === -1) continue;
204
+ if (normalizeRoleName(trimmed.slice(0, separator)) !== spec.key) continue;
205
+ match = index;
206
+ }
207
+ const rendered = `${spec.key}: ${value}`;
208
+ if (match >= 0) {
209
+ const original = lines[match];
210
+ const indent = /^\s*/.exec(original)?.[0] ?? "";
211
+ const ending = original.endsWith("\r") ? "\r" : "";
212
+ lines[match] = `${indent}${rendered}${ending}`;
213
+ } else lines.push(`${rendered}${crlf ? "\r" : ""}`);
214
+ return `${lines.join("\n")}\n`;
215
+ }
216
+ function writeAtomically(path, text) {
217
+ mkdirSync(dirname(path), { recursive: true });
218
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
219
+ try {
220
+ writeFileSync(temporary, text, "utf8");
221
+ renameSync(temporary, path);
222
+ } catch (error) {
223
+ rmSync(temporary, { force: true });
224
+ throw error;
225
+ }
226
+ }
227
+ /**
228
+ * Save one role and return the reloaded snapshot.
229
+ *
230
+ * A missing file is created with just this role. An unreadable file is never
231
+ * overwritten. Every refusal leaves the existing bytes untouched.
232
+ */
233
+ function saveRoleConfiguration(path, request, catalogue) {
234
+ const current = readRoleConfiguration(path);
235
+ if (current.status === "unreadable") return {
236
+ ok: false,
237
+ reason: `the role file is unreadable: ${current.reason}`
238
+ };
239
+ const validation = validateSaveRequest(request, catalogue);
240
+ if (!validation.ok) return validation;
241
+ let existing = "";
242
+ if (current.status === "loaded") try {
243
+ existing = readFileSync(path, "utf8");
244
+ } catch (error) {
245
+ return {
246
+ ok: false,
247
+ reason: `the role file could not be read: ${describeError(error)}`
248
+ };
249
+ }
250
+ const next = updateRoleText(existing, validation.value.spec, renderRoleValue(validation.value.routes));
251
+ try {
252
+ writeAtomically(path, next);
253
+ } catch (error) {
254
+ return {
255
+ ok: false,
256
+ reason: `the role file could not be written: ${describeError(error)}`
257
+ };
258
+ }
259
+ return {
260
+ ok: true,
261
+ snapshot: snapshotFromConfiguration(readRoleConfiguration(path))
262
+ };
263
+ }
264
+ function describeError(error) {
265
+ if (error instanceof Error) {
266
+ const code = error.code;
267
+ return typeof code === "string" ? `${error.message} (${code})` : error.message;
268
+ }
269
+ return String(error);
270
+ }
271
+ // src/roles.ts
272
+ /**
273
+ * `pstack-roles`: the Host half of the role model configuration page.
274
+ *
275
+ * It owns no cached policy: every read and write goes to
276
+ * `$DSH_HOME/pstack-models.mdc`, which `pstack_subagent` also re-reads on every
277
+ * delegation. Saving therefore takes effect on the next delegation without a
278
+ * restart, and an already running child keeps the route it started with.
279
+ */
280
+ /** Read and write the role file for the Plugins configuration page. */
281
+ var PstackRolesService = class extends TypertRemoteService {
282
+ static inject = ["llm", "typert"];
283
+ constructor(ctx) {
284
+ super(ctx, ROLES_SERVICE);
285
+ }
286
+ [Service.init]() {
287
+ const typert = this.ctx.typert;
288
+ if (typert === void 0) throw new Error("pstack-roles requires the Typert registry service");
289
+ typert.register({
290
+ package: REMOTE_PACKAGE,
291
+ face: "host",
292
+ schemas: [],
293
+ model: {
294
+ services: [],
295
+ events: [],
296
+ objects: []
297
+ },
298
+ invocations: pstackRolesRemote.descriptors
299
+ });
300
+ }
301
+ /** Current path, status, values, and per-line issues. */
302
+ status() {
303
+ return snapshotFromConfiguration(readRoleConfiguration(roleFilePath()));
304
+ }
305
+ /** The live directory the page offers for its provider/model/effort selects. */
306
+ async catalogue(signal) {
307
+ return collectCatalogue(this.llm, void 0, signal);
308
+ }
309
+ /** Validate and save one role, then return the reloaded snapshot. */
310
+ async save(request, signal) {
311
+ signal.throwIfAborted();
312
+ const catalogue = await collectCatalogue(this.llm, void 0, signal);
313
+ signal.throwIfAborted();
314
+ return saveRoleConfiguration(roleFilePath(), request, catalogue);
315
+ }
316
+ get llm() {
317
+ return this.ctx.llm;
318
+ }
319
+ };
320
+ function roleFilePath() {
321
+ return modelsFilePath(resolveDshHome());
322
+ }
323
+ export { PstackRolesService, PstackRolesService as default };
@@ -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 锚点。