@haiyangbg/buildbeat 2.0.2 → 3.0.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 (113) hide show
  1. package/CHANGELOG.md +25 -301
  2. package/README.en.md +9 -22
  3. package/README.md +6 -19
  4. package/SKILL.md +169 -220
  5. package/bin/buildbeat.js +14 -2
  6. package/docs/CAPABILITY-MATRIX.md +13 -55
  7. package/docs/README.md +13 -14
  8. package/docs/RELEASING.md +10 -9
  9. package/docs/v2/RFC-0001-product-definition.md +2 -0
  10. package/docs/v2/RFC-0003-workflow-policy.md +2 -0
  11. package/docs/v2/guide/00-how-to-talk.md +3 -3
  12. package/docs/v2/guide/01-quickstart.en.md +163 -0
  13. package/docs/v2/guide/01-quickstart.md +15 -13
  14. package/docs/v2/guide/03-policy-guide.md +1 -1
  15. package/docs/v2/guide/06-evidence-guide.en.md +51 -0
  16. package/docs/v2/guide/06-evidence-guide.md +5 -3
  17. package/docs/v2/guide/07-approval-guide.en.md +115 -0
  18. package/docs/v2/guide/07-approval-guide.md +12 -10
  19. package/docs/v2/guide/10-recovery.en.md +83 -0
  20. package/docs/v2/guide/10-recovery.md +8 -6
  21. package/docs/v2/guide/11-session-handoff.en.md +2 -2
  22. package/docs/v2/guide/11-session-handoff.md +2 -2
  23. package/docs/v2/guide/README.md +4 -10
  24. package/example/.buildbeat/notify.yaml +13 -0
  25. package/example/.buildbeat/observe.yaml +31 -0
  26. package/example/AGENTS.md +67 -13
  27. package/example/BUILDBEAT.md +8 -11
  28. package/example/CLAUDE.md +1 -1
  29. package/example/README.md +17 -67
  30. package/example/delivery/envelope/prompts/builder.md +10 -0
  31. package/example/delivery/envelope/prompts/fixer.md +10 -0
  32. package/example/delivery/envelope/prompts/reviewer.md +13 -0
  33. package/example/delivery/envelope/worker.sh +70 -0
  34. package/example/delivery/work/WORK-EXPORT-DATE-FILTER/decisions.jsonl +3 -0
  35. package/example/delivery/work/WORK-EXPORT-DATE-FILTER/intent.md +24 -0
  36. package/example/delivery/work/WORK-EXPORT-DATE-FILTER/plan.md +20 -0
  37. package/example/delivery/work/WORK-EXPORT-DATE-FILTER/run-config.yaml +66 -0
  38. package/example/delivery/work/WORK-EXPORT-DATE-FILTER/runs/RUN-EXPORT-01/run-record.json +108 -0
  39. package/example/delivery/work/WORK-EXPORT-DATE-FILTER/workflow.yaml +44 -0
  40. package/example/gitignore.template +20 -0
  41. package/example/package.json +13 -0
  42. package/example/pm/decisions.md +3 -15
  43. package/example/src/export.js +25 -0
  44. package/example/src/ledger.js +16 -0
  45. package/example/tests/export.test.js +35 -0
  46. package/example//346/214/207/346/214/245/345/217/260.md +40 -0
  47. package/lessons.md +52 -71
  48. package/package.json +3 -7
  49. package/src/v2/cli/run.js +33 -23
  50. package/src/v2/engine/risk-preset.js +1 -1
  51. package/src/v2/runtime/notify.js +5 -5
  52. package/src/v2/runtime/overview.js +7 -7
  53. package/templates/ARCHITECTURE.md +1 -1
  54. package/templates/contracts/PROTOCOL.md +2 -10
  55. package/templates/gitignore.template +0 -3
  56. package/templates/pm/adr/README.md +1 -1
  57. package/templates/pm/decisions.md +4 -5
  58. package/templates/standards/CODE.md +1 -1
  59. package/templates/standards/DESIGN.md +1 -1
  60. package/templates/standards/REVIEW.md +2 -2
  61. package/templates/standards/STACK.md +2 -8
  62. package/templates/v2/AGENTS.md +18 -18
  63. package/templates/v2/BUILDBEAT.md +2 -3
  64. package/templates/v2/CLAUDE.md +1 -1
  65. package/templates/v2/run-config.example.yaml +1 -1
  66. package/templates/v2//346/214/207/346/214/245/345/217/260.md +6 -6
  67. package/bin/buildbeat-v2.js +0 -18
  68. package/bin/solobaton.js +0 -6
  69. package/docs/CHECKS.md +0 -326
  70. package/docs/CLI.md +0 -245
  71. package/docs/LEGACY-V1.16-MIGRATION.md +0 -54
  72. package/docs/v2/guide/08-migration-v1.md +0 -72
  73. package/example/.buildbeat/manifest.json +0 -45
  74. package/example/ARCHITECTURE.md +0 -39
  75. package/example/contracts/PROTOCOL.md +0 -38
  76. package/example/pm/NOW.md +0 -22
  77. package/example/pm/adr/ADR-0001-local-first-sqlite.md +0 -25
  78. package/example/pm/adr/README.md +0 -7
  79. package/example/pm/archive//344/270/200/346/234/237/evidence/gate1.md +0 -5
  80. package/example/pm/archive//344/270/200/346/234/237/evidence/gate2.md +0 -5
  81. package/example/pm/archive//344/270/200/346/234/237/evidence/gate3.md +0 -5
  82. package/example/pm/archive//344/270/200/346/234/237/evidence/gate4.md +0 -5
  83. package/example/pm/archive//344/270/200/346/234/237/evidence/implementation.md +0 -5
  84. package/example/pm/status//344/272/247/345/223/201.md +0 -20
  85. package/example/pm/status//345/205/250/346/240/210.md +0 -15
  86. package/example/pm/status//346/265/213/350/257/225.md +0 -15
  87. package/example/pm//344/270/200/346/234/237-/347/234/213/346/235/277.md +0 -97
  88. package/example/standards/CODE.md +0 -18
  89. package/example/standards/DESIGN.md +0 -34
  90. package/example/standards/REVIEW.md +0 -16
  91. package/example/standards/STACK.md +0 -31
  92. package/src/cli.js +0 -323
  93. package/src/constants.js +0 -202
  94. package/src/doctor.js +0 -267
  95. package/src/planner.js +0 -251
  96. package/src/project.js +0 -844
  97. package/src/upgrader.js +0 -1249
  98. package/src/v2/presets/risk/legacy-four-gates.yaml +0 -44
  99. package/src/writer.js +0 -534
  100. package/templates/.claude/agents/reviewer.md +0 -62
  101. package/templates/AGENTS.md +0 -85
  102. package/templates/BUILDBEAT.md +0 -13
  103. package/templates/CLAUDE.md +0 -7
  104. package/templates/pm/NOW.md +0 -26
  105. package/templates/pm/changes/README.md +0 -44
  106. package/templates/pm/status/README.md +0 -32
  107. package/templates/pm//345/275/223/346/234/237/347/234/213/346/235/277.md +0 -62
  108. package/templates/scripts/bus-check.sh +0 -1875
  109. package/templates/scripts/design-preview.sh +0 -44
  110. package/templates/scripts/drift-check.sh +0 -112
  111. package/templates/scripts/pre-commit.sh +0 -74
  112. package/templates/scripts/verify-status.sh +0 -105
  113. package/templates//346/214/207/346/214/245/345/217/260.md +0 -58
@@ -1,44 +0,0 @@
1
- # Migration preset mapping BuildBeat v1's four fixed gates:
2
- # Gate1 (requirements) -> intent acceptance (digest-bound)
3
- # Gate2 (design) -> plan acceptance + human boundary before build
4
- # Gate3 (merge) -> the built-in final merge decision
5
- # Gate4 (release) -> out of MVP scope (no deploy capability exists)
6
- # The extra human boundary before review preserves v1's late review gate.
7
- kind: risk-preset
8
- version: 1
9
- name: legacy-four-gates
10
- stopAt:
11
- - build
12
- - review
13
- policies:
14
- - kind: policy
15
- version: 1
16
- name: intent-accepted
17
- type: pre
18
- appliesTo: build
19
- enforcement: LOCAL_ENFORCED
20
- rule:
21
- artifact.accepted:
22
- artifact: intent
23
- - kind: policy
24
- version: 1
25
- name: plan-accepted
26
- type: pre
27
- appliesTo: build
28
- enforcement: LOCAL_ENFORCED
29
- rule:
30
- artifact.accepted:
31
- artifact: plan
32
- - kind: policy
33
- version: 1
34
- name: merge-evidence-floor
35
- type: transition
36
- appliesTo: enter-wait-merge
37
- enforcement: LOCAL_ENFORCED
38
- rule:
39
- all:
40
- - evidence.exists:
41
- kind: command
42
- minGrade: L2
43
- - finding.maxSeverity:
44
- atMost: P2
package/src/writer.js DELETED
@@ -1,534 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import {
3
- chmodSync,
4
- closeSync,
5
- existsSync,
6
- fsyncSync,
7
- lstatSync,
8
- mkdirSync,
9
- openSync,
10
- readFileSync,
11
- renameSync,
12
- rmdirSync,
13
- unlinkSync,
14
- writeFileSync,
15
- } from "node:fs";
16
- import path from "node:path";
17
- import { fileURLToPath } from "node:url";
18
-
19
- import {
20
- CLI_VERSION,
21
- FILE_POLICIES,
22
- GITIGNORE_BEGIN_MARKER,
23
- GITIGNORE_END_MARKER,
24
- GITIGNORE_MARKER_PAIRS,
25
- MANIFEST_PATH,
26
- MANIFEST_SCHEMA_VERSION,
27
- RENDER_REQUIRED_PLACEHOLDERS,
28
- SCAFFOLD_VERSION,
29
- } from "./constants.js";
30
- import { inspectProject, plannedFiles } from "./project.js";
31
-
32
- const TEMPLATE_ROOT = fileURLToPath(new URL("../templates/", import.meta.url));
33
- const COMPACT_SCRIPT_REFERENCE_TEMPLATES = new Set([
34
- "AGENTS.md",
35
- "ARCHITECTURE.md",
36
- "contracts/PROTOCOL.md",
37
- "pm/NOW.md",
38
- "pm/status/README.md",
39
- "pm/当期看板.md",
40
- "指挥台.md",
41
- ]);
42
-
43
- export class WriteError extends Error {
44
- constructor(code, message, cause = null) {
45
- super(message, cause ? { cause } : undefined);
46
- this.name = "WriteError";
47
- this.code = code;
48
- }
49
- }
50
-
51
- function sha256(bytes) {
52
- return createHash("sha256").update(bytes).digest("hex");
53
- }
54
-
55
- function replaceAll(text, token, value) {
56
- return text.split(token).join(value);
57
- }
58
-
59
- function stableDate(now) {
60
- const value = now instanceof Date ? now : new Date(now);
61
- if (Number.isNaN(value.getTime())) {
62
- throw new WriteError("render.invalid_date", "The scaffold render date is invalid.");
63
- }
64
- return value;
65
- }
66
-
67
- function localCalendarDate(now) {
68
- const value = stableDate(now);
69
- const year = String(value.getFullYear()).padStart(4, "0");
70
- const month = String(value.getMonth() + 1).padStart(2, "0");
71
- const day = String(value.getDate()).padStart(2, "0");
72
- return `${year}-${month}-${day}`;
73
- }
74
-
75
- function deterministicValues({ layout, projectName, now }) {
76
- return new Map([
77
- ["<项目名>", projectName || "project"],
78
- ["<yyyy-mm-dd>", localCalendarDate(now)],
79
- ["<X.Y>", SCAFFOLD_VERSION.replace(/^v/, "")],
80
- ["<默认|紧凑>", layout === "compact" ? "紧凑" : "默认"],
81
- ]);
82
- }
83
-
84
- function ownedGitignoreFragment(body) {
85
- const normalizedBody = body.replace(/\r\n/g, "\n").replace(/\n*$/, "");
86
- return Buffer.from(
87
- `${GITIGNORE_BEGIN_MARKER}\n${normalizedBody}\n${GITIGNORE_END_MARKER}\n`,
88
- "utf8",
89
- );
90
- }
91
-
92
- export function prepareScaffold({ layout, projectName, now = new Date() }) {
93
- const values = deterministicValues({ layout, projectName, now });
94
- const files = [];
95
- const renderedPlaceholders = [];
96
- const pendingPlaceholders = [];
97
- let gitignore = null;
98
-
99
- for (const item of plannedFiles(layout)) {
100
- const source = path.join(TEMPLATE_ROOT, item.template);
101
- let text = readFileSync(source, "utf8");
102
- for (const [token, value] of values) {
103
- if (text.includes(token)) {
104
- text = replaceAll(text, token, value);
105
- renderedPlaceholders.push({ path: item.target, token, value });
106
- }
107
- }
108
- if (
109
- layout === "compact" &&
110
- COMPACT_SCRIPT_REFERENCE_TEMPLATES.has(item.template) &&
111
- text.includes("scripts/")
112
- ) {
113
- text = replaceAll(text, "scripts/", "pm/scripts/");
114
- renderedPlaceholders.push({
115
- path: item.target,
116
- token: "scripts/",
117
- value: "pm/scripts/",
118
- });
119
- }
120
-
121
- const pendingTokens = (RENDER_REQUIRED_PLACEHOLDERS[item.template] || [])
122
- .filter((token) => text.includes(token));
123
- if (pendingTokens.length > 0) {
124
- pendingPlaceholders.push({ path: item.target, tokens: pendingTokens });
125
- }
126
-
127
- const mode = lstatSync(source).mode & 0o777;
128
- if (item.policy === FILE_POLICIES.MERGE_ONLY) {
129
- const fragment = ownedGitignoreFragment(text);
130
- gitignore = {
131
- ...item,
132
- fragment,
133
- baselineSha256: sha256(fragment),
134
- mode,
135
- };
136
- } else {
137
- const content = Buffer.from(text, "utf8");
138
- files.push({
139
- ...item,
140
- content,
141
- baselineSha256: sha256(content),
142
- mode,
143
- });
144
- }
145
- }
146
-
147
- renderedPlaceholders.sort((a, b) =>
148
- a.path.localeCompare(b.path) || a.token.localeCompare(b.token),
149
- );
150
- pendingPlaceholders.sort((a, b) => a.path.localeCompare(b.path));
151
- if (gitignore === null) {
152
- throw new WriteError("integration.gitignore_missing", "The bundled gitignore template is missing.");
153
- }
154
- return { files, gitignore, renderedPlaceholders, pendingPlaceholders };
155
- }
156
-
157
- function ensureDirectory(directory, createdDirectories) {
158
- if (existsSync(directory)) {
159
- const stat = lstatSync(directory);
160
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
161
- throw new WriteError("path.unsafe", `A required directory path is not a real directory: ${directory}`);
162
- }
163
- return;
164
- }
165
- const parent = path.dirname(directory);
166
- if (parent === directory) {
167
- throw new WriteError("path.unsafe", `Cannot create target directory: ${directory}`);
168
- }
169
- ensureDirectory(parent, createdDirectories);
170
- try {
171
- mkdirSync(directory, { mode: 0o755 });
172
- createdDirectories.push(directory);
173
- } catch (error) {
174
- if (error.code === "EEXIST") {
175
- ensureDirectory(directory, createdDirectories);
176
- return;
177
- }
178
- throw error;
179
- }
180
- }
181
-
182
- function assertRelativeTarget(relative) {
183
- if (
184
- typeof relative !== "string" ||
185
- relative.length === 0 ||
186
- relative.includes("\\") ||
187
- path.posix.isAbsolute(relative) ||
188
- path.win32.isAbsolute(relative) ||
189
- path.posix.normalize(relative) !== relative ||
190
- relative.split("/").some((segment) => segment === "" || segment === "." || segment === "..")
191
- ) {
192
- throw new WriteError("path.unsafe", `Unsafe scaffold target path: ${relative}`);
193
- }
194
- }
195
-
196
- function ensureTargetParent(target, relative, createdDirectories) {
197
- assertRelativeTarget(relative);
198
- ensureDirectory(path.dirname(path.join(target, ...relative.split("/"))), createdDirectories);
199
- }
200
-
201
- function fsyncDirectory(directory) {
202
- if (process.platform === "win32") {
203
- return;
204
- }
205
- const descriptor = openSync(directory, "r");
206
- try {
207
- fsyncSync(descriptor);
208
- } finally {
209
- closeSync(descriptor);
210
- }
211
- }
212
-
213
- function atomicWrite(
214
- filename,
215
- bytes,
216
- mode,
217
- { overwrite = false, nextTempId, onRenamed = null },
218
- ) {
219
- const parent = path.dirname(filename);
220
- const basename = path.basename(filename);
221
- let temp = null;
222
- let descriptor = null;
223
- try {
224
- for (let attempt = 0; attempt < 100; attempt += 1) {
225
- const candidate = path.join(
226
- parent,
227
- `.${basename}.buildbeat-${process.pid}-${nextTempId()}-${attempt}.tmp`,
228
- );
229
- try {
230
- descriptor = openSync(candidate, "wx", mode);
231
- temp = candidate;
232
- break;
233
- } catch (error) {
234
- if (error.code !== "EEXIST") {
235
- throw error;
236
- }
237
- }
238
- }
239
- if (descriptor === null || temp === null) {
240
- throw new WriteError("write.temp_unavailable", `Could not allocate a temporary sibling for ${filename}.`);
241
- }
242
- writeFileSync(descriptor, bytes);
243
- fsyncSync(descriptor);
244
- closeSync(descriptor);
245
- descriptor = null;
246
- chmodSync(temp, mode);
247
-
248
- if (!overwrite && existsSync(filename)) {
249
- throw new WriteError("files.collide", `Destination appeared during the write transaction: ${filename}`);
250
- }
251
- if (overwrite) {
252
- const current = lstatSync(filename);
253
- if (!current.isFile() || current.isSymbolicLink()) {
254
- throw new WriteError("integration.gitignore_unsafe", ".gitignore is no longer a regular file.");
255
- }
256
- }
257
- renameSync(temp, filename);
258
- temp = null;
259
- if (typeof onRenamed === "function") {
260
- onRenamed();
261
- }
262
- fsyncDirectory(parent);
263
- } finally {
264
- if (descriptor !== null) {
265
- closeSync(descriptor);
266
- }
267
- if (temp !== null) {
268
- try {
269
- unlinkSync(temp);
270
- } catch (error) {
271
- if (error.code !== "ENOENT") {
272
- throw error;
273
- }
274
- }
275
- }
276
- }
277
- }
278
-
279
- function markerCount(text, marker) {
280
- return text.split(marker).length - 1;
281
- }
282
-
283
- function assertGitignoreBytesSafe(bytes) {
284
- const text = bytes.toString("utf8");
285
- if (GITIGNORE_MARKER_PAIRS.some(
286
- ([beginMarker, endMarker]) =>
287
- markerCount(text, beginMarker) !== 0 || markerCount(text, endMarker) !== 0,
288
- )) {
289
- throw new WriteError(
290
- "integration.gitignore_fragment_present",
291
- "A BuildBeat or legacy Solobaton .gitignore marker already exists without a valid schema 2 ownership record.",
292
- );
293
- }
294
- }
295
-
296
- function mergeGitignore(existing, fragment) {
297
- if (existing.length === 0) {
298
- return fragment;
299
- }
300
- const separator = existing.at(-1) === 0x0a ? Buffer.from("\n") : Buffer.from("\n\n");
301
- return Buffer.concat([existing, separator, fragment]);
302
- }
303
-
304
- function preflight(plan) {
305
- if (plan.preview || plan.writesPerformed || !["init", "adopt"].includes(plan.command)) {
306
- throw new WriteError("write.invalid_plan", "Only a ready init/adopt apply plan can be written.");
307
- }
308
- let inspection;
309
- try {
310
- inspection = inspectProject(plan.target, {
311
- collisionLayout: plan.layout,
312
- includeDependencies: false,
313
- });
314
- } catch (error) {
315
- throw new WriteError("target.unsafe", error.message, error);
316
- }
317
- if (plan.command === "adopt" && !inspection.exists) {
318
- throw new WriteError("target.not_found", "Brownfield adoption requires an existing project directory.");
319
- }
320
- if (inspection.installation.state !== "not-installed") {
321
- throw new WriteError(
322
- `install.${inspection.installation.state.replaceAll("-", "_")}`,
323
- "The target is already installed, partial, or mixed; no write was attempted.",
324
- );
325
- }
326
- if (inspection.manifest.state !== "missing") {
327
- throw new WriteError(
328
- "manifest.already_present",
329
- "A lifecycle manifest already exists or is unreadable; ownership cannot be inferred.",
330
- );
331
- }
332
- if (inspection.collisions.length > 0) {
333
- throw new WriteError(
334
- "files.collide",
335
- `${inspection.collisions.length} destination path(s) collide with existing project content.`,
336
- );
337
- }
338
- if (inspection.gitWorktree.state === "dirty") {
339
- throw new WriteError("git.dirty", "The target-root Git worktree is not clean.");
340
- }
341
- if (inspection.gitWorktree.state === "unavailable") {
342
- throw new WriteError("git.status_unavailable", "The target has a root .git entry, but Git status failed.");
343
- }
344
- if (inspection.gitignore.state === "unsafe") {
345
- throw new WriteError("integration.gitignore_unsafe", ".gitignore is not a readable regular file.");
346
- }
347
- if (inspection.gitignore.beginMarkers > 0 || inspection.gitignore.endMarkers > 0) {
348
- throw new WriteError(
349
- "integration.gitignore_fragment_present",
350
- "A BuildBeat or legacy Solobaton .gitignore marker already exists without a valid schema 2 ownership record.",
351
- );
352
- }
353
- return inspection;
354
- }
355
-
356
- function rollback({ createdFiles, createdDirectories, gitignoreBackup, gitignoreModified, nextTempId }) {
357
- const failures = [];
358
- for (const filename of [...createdFiles].reverse()) {
359
- try {
360
- unlinkSync(filename);
361
- } catch (error) {
362
- if (error.code !== "ENOENT") {
363
- failures.push(`${filename}: ${error.code || error.message}`);
364
- }
365
- }
366
- }
367
- if (gitignoreModified && gitignoreBackup !== null) {
368
- try {
369
- atomicWrite(
370
- gitignoreBackup.path,
371
- gitignoreBackup.bytes,
372
- gitignoreBackup.mode,
373
- { overwrite: true, nextTempId },
374
- );
375
- } catch (error) {
376
- failures.push(`${gitignoreBackup.path}: ${error.code || error.message}`);
377
- }
378
- }
379
- for (const directory of [...createdDirectories].reverse()) {
380
- try {
381
- rmdirSync(directory);
382
- } catch (error) {
383
- if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY") {
384
- failures.push(`${directory}: ${error.code || error.message}`);
385
- }
386
- }
387
- }
388
- return failures;
389
- }
390
-
391
- export function applyScaffold(plan, { now = new Date(), faultInjector = null } = {}) {
392
- const inspection = preflight(plan);
393
- const rendered = prepareScaffold({
394
- layout: plan.layout,
395
- projectName: inspection.projectName.value,
396
- now,
397
- });
398
- const target = path.resolve(plan.target);
399
- const createdFiles = [];
400
- const createdDirectories = [];
401
- let gitignoreBackup = null;
402
- let gitignoreModified = false;
403
- let tempId = 0;
404
- const nextTempId = () => {
405
- tempId += 1;
406
- return tempId;
407
- };
408
- const maybeFault = (phase, relative) => {
409
- if (typeof faultInjector === "function") {
410
- faultInjector({ phase, path: relative, writes: createdFiles.length });
411
- }
412
- };
413
-
414
- try {
415
- ensureDirectory(target, createdDirectories);
416
-
417
- for (const item of rendered.files) {
418
- ensureTargetParent(target, item.target, createdDirectories);
419
- const filename = path.join(target, ...item.target.split("/"));
420
- atomicWrite(filename, item.content, item.mode, {
421
- nextTempId,
422
- onRenamed: () => createdFiles.push(filename),
423
- });
424
- maybeFault("file", item.target);
425
- }
426
-
427
- const gitignorePath = path.join(target, ".gitignore");
428
- ensureTargetParent(target, ".gitignore", createdDirectories);
429
- let existingGitignore = Buffer.alloc(0);
430
- let gitignoreMode = rendered.gitignore.mode || 0o644;
431
- const gitignoreExisted = existsSync(gitignorePath);
432
- if (gitignoreExisted) {
433
- const stat = lstatSync(gitignorePath);
434
- if (!stat.isFile() || stat.isSymbolicLink()) {
435
- throw new WriteError("integration.gitignore_unsafe", ".gitignore is not a regular file.");
436
- }
437
- existingGitignore = readFileSync(gitignorePath);
438
- assertGitignoreBytesSafe(existingGitignore);
439
- gitignoreMode = stat.mode & 0o777;
440
- gitignoreBackup = { path: gitignorePath, bytes: existingGitignore, mode: gitignoreMode };
441
- }
442
- atomicWrite(
443
- gitignorePath,
444
- mergeGitignore(existingGitignore, rendered.gitignore.fragment),
445
- gitignoreMode,
446
- {
447
- overwrite: gitignoreExisted,
448
- nextTempId,
449
- onRenamed: () => {
450
- if (gitignoreExisted) {
451
- gitignoreModified = true;
452
- } else {
453
- createdFiles.push(gitignorePath);
454
- }
455
- },
456
- },
457
- );
458
- maybeFault("gitignore", ".gitignore");
459
-
460
- const manifest = {
461
- schemaVersion: MANIFEST_SCHEMA_VERSION,
462
- scaffoldVersion: SCAFFOLD_VERSION,
463
- cliVersion: CLI_VERSION,
464
- layout: plan.layout,
465
- installedAt: stableDate(now).toISOString(),
466
- files: Object.fromEntries(
467
- rendered.files.map((item) => [
468
- item.target,
469
- { policy: item.policy, baselineSha256: item.baselineSha256 },
470
- ]),
471
- ),
472
- integrations: {
473
- gitignore: {
474
- path: ".gitignore",
475
- beginMarker: GITIGNORE_BEGIN_MARKER,
476
- endMarker: GITIGNORE_END_MARKER,
477
- baselineSha256: rendered.gitignore.baselineSha256,
478
- },
479
- hooks: null,
480
- },
481
- };
482
- ensureTargetParent(target, MANIFEST_PATH, createdDirectories);
483
- maybeFault("before-manifest", MANIFEST_PATH);
484
- const manifestFilename = path.join(target, ...MANIFEST_PATH.split("/"));
485
- atomicWrite(
486
- manifestFilename,
487
- Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"),
488
- 0o644,
489
- {
490
- nextTempId,
491
- onRenamed: () => createdFiles.push(manifestFilename),
492
- },
493
- );
494
-
495
- return {
496
- ...plan,
497
- preview: false,
498
- writesPerformed: true,
499
- targetExists: true,
500
- detected: {
501
- ...plan.detected,
502
- projectName: inspection.projectName,
503
- },
504
- writtenPaths: [
505
- ...rendered.files.map((item) => item.target),
506
- ".gitignore",
507
- MANIFEST_PATH,
508
- ],
509
- renderedPlaceholders: rendered.renderedPlaceholders,
510
- pendingPlaceholders: rendered.pendingPlaceholders,
511
- manifestPath: MANIFEST_PATH,
512
- ready: true,
513
- };
514
- } catch (error) {
515
- const rollbackFailures = rollback({
516
- createdFiles,
517
- createdDirectories,
518
- gitignoreBackup,
519
- gitignoreModified,
520
- nextTempId,
521
- });
522
- if (rollbackFailures.length > 0) {
523
- throw new WriteError(
524
- "rollback.incomplete",
525
- `Write failed and rollback was incomplete: ${rollbackFailures.join("; ")}`,
526
- error,
527
- );
528
- }
529
- if (error instanceof WriteError) {
530
- throw error;
531
- }
532
- throw new WriteError("write.failed", `Scaffold write failed and was rolled back: ${error.message}`, error);
533
- }
534
- }
@@ -1,62 +0,0 @@
1
- ---
2
- name: reviewer
3
- description: review-ready 后单次静默执行的只读审查专家。按 milestone / risk-delta / closure 三种模式核查;未稳定候选返回 NOT_READY,审查中 hash 改变返回 SUPERSEDED。
4
- tools: Read, Grep, Glob, Bash
5
- ---
6
-
7
- 你是 <项目名> 的资深独立审查者。**写者≠审者**是质量硬约束 —— 你只读、只出问题清单,**绝不修改代码**。
8
-
9
- > `Bash` 只用于读取候选事实(`git status --short` / `git rev-parse` / `git diff` / `git show` / `git log`)和运行项目已经声明的本地验证命令。禁止 checkout / switch / restore / clean / add / commit / push,禁止安装依赖、联网、部署或用 shell 写文件;命令可能改工作树时不要运行。
10
-
11
- > **单次静默核查**:除输入缺失或真实阻塞外,不要向主会话发送中间进度、分批 findings 或“仍在审”更新;完成整个 scope 后,先重新读取每个候选仓的 `HEAD` 与 `git status --short`,确认 candidate 未变且仍干净,再一次返回最终正文。发生变化只回 `SUPERSEDED`,避免一轮审查制造多轮 reviewer 状态噪音。
12
-
13
- ## 背景
14
- - 项目用「协作总线」多域协作;全栈域可能同时持有契约两端 → **你是契约自审风险的补偿防线**。
15
- - 唯一契约入口 = `contracts/PROTOCOL.md`;当期需求/设计在 `pm/NOW.md` 指向的看板 + `pm/changes/`。
16
- - 核查门按**风险批次 + 里程碑候选**运行,不是每个小任务都重跑一次完整审查。轻量机器闸每次提交,受影响测试按变更批次,全量测试绑定里程碑候选。
17
-
18
- ## 调用输入(先核这五项)
19
-
20
- 主会话应给出:
21
-
22
- 1. `mode`: `milestone` / `risk-delta` / `closure`。
23
- 2. `candidate`:精确 commit hash;多仓则给 hash 集。`milestone` 没有 candidate → 只能判「候选未固定」,不得给可合并结论。
24
- 3. `base`:`risk-delta` 必填,审查范围固定为 `base..candidate`;`closure` 改填 finding ID + 修复 hash。
25
- 4. `scope`:需求/设计/契约/提案/证据入口。能从 NOW 自查到的不要反问人。
26
- 5. `review_ready`:`milestone` 必须给出以下四项证据,缺一项就只返回 `NOT_READY`,不展开审查:
27
- - 工作包内实现与写者自查已完成,没有已知待修项或计划中的 candidate 修改;
28
- - 每个候选仓 `HEAD=candidate`,且 `git status --short` 为空;
29
- - 受影响/全量自动化达到项目要求的 L3,有 UI 时真渲染证据已就绪;
30
- - candidate 与证据指针已固定,reviewer 期间主会话不会继续修改候选。
31
-
32
- ### 三种模式
33
-
34
- - **`milestone`**:只对 review-ready 的稳定候选 hash 集做一次完整四方核查;默认唯一会产生完整报告的模式。写者自查尚在发现/修复问题时不得启动。
35
- - **`risk-delta`**:只核修改已冻结对外契约、里程碑完成后新出现的鉴权/租户/Secret/fail-closed/持久化语义或不可逆外部副作用;写者在首次 milestone 前自行发现并收敛的问题不是 risk-delta。结论只覆盖该 delta,**不得外推为整体候选通过**。
36
- - **`closure`**:主会话应先把首轮 P0/P1 全部合并修完,再一次复核指定 finding 集与必要回归;不得按 finding 逐个调用,不得复制首轮整份背景或重新发散无关 P2。
37
-
38
- 若同一 candidate 已有 milestone 结论且 hash 未变、机器证据仍绿,直接指出「复用既有结论」;**不要为再放心一次重复全审**。审查返回前 candidate 有变化,立即返回 `SUPERSEDED: <old> -> <new>` 并停止,不得继续核旧 hash 或把写者自发现的连续修补包装成 delta 链;主会话重新满足 review-ready 后再提交替代 milestone。milestone 已完成后,指定 finding 修复走一次合并 `closure`,真正新增的高风险语义走 `risk-delta`;无关改动混进 closure 时判范围不纯。最终候选由「原 milestone 结论 + 后续必要的 delta/closure」覆盖,不把旧 hash 的结论冒充新 hash 结论。
39
-
40
- ## 审什么(按此顺序,逐条给证据)
41
- 1. **四方一致**:实现 ↔ 设计稿(`design/`)↔ 契约(`contracts/PROTOCOL.md`)↔ 需求(当期 spec/提案)。任一方对不上 = 问题。
42
- 2. **契约两端**:调用方与实现方是否都符合 PROTOCOL(字段名/类型/鉴权/错误码)。同会话改两端最易绕过核查——重点盯。
43
- 3. **架构违规 / 重复造轮子 / 边界条件**。
44
- 4. **安全**:鉴权服务端强制(非按钮隐藏)、凭据不落盘、注入/SSRF、多用户数据隔离。
45
- 5. **四态**(有 UI 时):加载 / 空 / 错误 / 移动端是否都处理。
46
- 6. **界面零元注释**(有 UI 时):可见界面无调试信息 / 口径解释脚注 / 字段说明 / mock 标记 / 开发者自留文案;发现通常判 P1。
47
-
48
- ## 输出
49
- - 头部固定列出:`mode` / `base` / `candidate`(多仓 hash 集)/ scope / 证据状态(已复跑 / 只读核验 / 未复跑及原因)。
50
- - `milestone` 头部另列 review-ready 四项;任一不成立就只输出 `NOT_READY + 缺项`,不产生 P0/P1/P2 报告。
51
- - 按严重度排序:🔴 P0(阻塞)/ 🟡 P1(阻塞)/ 🟢 P2(默认挂账,不触发一轮完整复核;项目立项时显式升格的除外)。
52
- - **每条带可核验证据**:`文件:行` / 契约条目 / 复现步骤。无证据的猜测标「待核」,不混入结论。
53
- - 不照单全收上游说法:宁可标「未确认」也不臆断。
54
- - 首轮 milestone 报告只保留一份问题原文;主会话合并修完 P0/P1 后,closure 一次追加下表,不重述全文:
55
-
56
- | Finding | 修复 hash | 复核范围 | 结果 |
57
- |---|---|---|---|
58
-
59
- - `milestone` 结尾给一句:□ 可合并 □ 修 P0/P1 再合 □ 不通过。
60
- - `risk-delta` / `closure` 结尾给一句:□ 本 delta/finding 已关闭 □ 仍阻塞;并明确「不代表整体候选通过」。
61
-
62
- 交回主会话,由人在 Gate3 决定是否合并。**你不合并、不放行。**