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.
- package/README.md +35 -16
- package/README.zh.md +36 -17
- package/assets/pstack-config-entry.png +0 -0
- package/cordis.patch.yml +12 -0
- package/dist/client.mjs +2448 -0
- package/dist/client.mjs.map +1 -0
- package/dist/index.mjs +1 -1
- package/dist/{paths-BT77khwm.mjs → paths-yji9SNak.mjs} +0 -1
- package/dist/pstack/docs/harness/dsh.md +7 -7
- package/dist/remote.d.mts +27 -0
- package/dist/remote.mjs +235 -0
- package/dist/role-catalog.d.mts +35 -0
- package/dist/role-catalog.mjs +122 -0
- package/dist/role-config-a4JpVdc-.mjs +379 -0
- package/dist/role-manifest-79cnFfDM.mjs +112 -0
- package/dist/role-manifest-8OERX9-s.d.mts +90 -0
- package/dist/role-wire-CAJsVA6e.d.mts +54 -0
- package/dist/roles.d.mts +18 -0
- package/dist/roles.mjs +293 -0
- package/dist/script.mjs +1 -1
- package/dist/skills/authored/decision-audit/SKILL.md +21 -17
- package/dist/skills/authored/decision-audit/SKILL.zh.md +21 -17
- package/dist/skills/authored/decision-audit/references/recover-commit.md +3 -3
- package/dist/skills/authored/decision-audit/references/recover-commit.zh.md +3 -3
- package/dist/skills/authored/decision-write/SKILL.md +11 -9
- package/dist/skills/authored/decision-write/SKILL.zh.md +11 -9
- package/dist/skills/authored/decision-write/references/land.md +3 -3
- package/dist/skills/authored/decision-write/references/land.zh.md +3 -3
- package/dist/skills/authored/decision-write/references/scope.md +16 -0
- package/dist/skills/authored/decision-write/references/scope.zh.md +16 -0
- package/dist/skills/authored/docs-drift/SKILL.md +120 -0
- package/dist/skills/authored/docs-drift/SKILL.zh.md +120 -0
- package/dist/skills/authored/setup-project-docs/SKILL.md +14 -9
- package/dist/skills/authored/setup-project-docs/SKILL.zh.md +14 -9
- package/dist/skills/authored/setup-project-docs/references/decision-conventions.md +3 -1
- package/dist/skills/authored/setup-project-docs/references/decision-conventions.zh.md +3 -1
- package/dist/subagent.d.mts +27 -20
- package/dist/subagent.mjs +71 -448
- 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,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: decision-audit
|
|
3
|
-
description:
|
|
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
|
|
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
|
-
|
|
17
|
+
Resolve `<dir>` in this order:
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
## 3. Run the checks
|
|
41
45
|
|
|
42
|
-
|
|
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
|
|
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` |
|
|
55
|
+
| `accepted` | `-` or missing | Landing lacks verified evidence |
|
|
52
56
|
|
|
53
|
-
For `proposed`, `-` or a pending anchor is not a finding;
|
|
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
|
|
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> --
|
|
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.
|
|
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
|
|
15
|
+
`pr` 解析 PR 基线,`local` 或 `full` 先尝试 `origin/main`。不存在时依次尝试 `origin/master`、`main`、`master`、最新 tag、`HEAD~20`。报告实际选中的 ref。基线无法解析会阻止相对基线的改动集合计算与提交锚点校验;继续其他检查,并在 `Unverified` 中列出每个尝试过的 ref。
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
按以下顺序定位 `<dir>`:
|
|
18
18
|
|
|
19
|
-
|
|
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
|
-
|
|
42
|
+
对每条现行记录执行所有适用的检查;同一记录可以产生多项发现。所有 glob/路径检查都使用[匹配 Scope 路径](../decision-write/references/scope.zh.md),并保留匹配结果作为证据。
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
## 3. 执行检查
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
A、B、B′ 和 D 只用于非 `superseded` 记录;C 和 E 按下方说明执行。
|
|
43
47
|
|
|
44
|
-
**A——路由。**
|
|
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` 带 `-`
|
|
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> --
|
|
78
|
+
git log --since=<Date> --format=%H <base> --
|
|
75
79
|
```
|
|
76
80
|
|
|
77
|
-
|
|
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`
|
|
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
|
|
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
|
|
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
|
-
|
|
17
|
+
比较 patch 标识前,先用共享 `Scope` 匹配器筛选每个候选提交的路径。命中可支持建议改用替代 SHA;未命中则留给人判断替代项。使用 squash 合并的仓库优先采用 PR 锚点。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: decision-write
|
|
3
|
-
description: Write, update, land, or supersede
|
|
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
|
-
|
|
16
|
+
Resolve `<dir>` in this order:
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
16
|
+
按以下顺序定位 `<dir>`:
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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`
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|