@haiyangbg/buildbeat 2.0.2 → 3.0.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 (91) hide show
  1. package/CHANGELOG.md +18 -301
  2. package/README.en.md +6 -19
  3. package/README.md +6 -19
  4. package/SKILL.md +168 -219
  5. package/bin/buildbeat.js +14 -2
  6. package/docs/CAPABILITY-MATRIX.md +13 -55
  7. package/docs/README.md +12 -14
  8. package/docs/RELEASING.md +8 -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.md +12 -12
  13. package/docs/v2/guide/03-policy-guide.md +1 -1
  14. package/docs/v2/guide/06-evidence-guide.md +3 -3
  15. package/docs/v2/guide/07-approval-guide.md +10 -10
  16. package/docs/v2/guide/10-recovery.md +6 -6
  17. package/docs/v2/guide/11-session-handoff.en.md +2 -2
  18. package/docs/v2/guide/11-session-handoff.md +2 -2
  19. package/docs/v2/guide/README.md +0 -6
  20. package/lessons.md +52 -71
  21. package/package.json +3 -8
  22. package/src/v2/cli/run.js +33 -23
  23. package/src/v2/engine/risk-preset.js +1 -1
  24. package/src/v2/runtime/notify.js +5 -5
  25. package/src/v2/runtime/overview.js +7 -7
  26. package/templates/ARCHITECTURE.md +1 -1
  27. package/templates/contracts/PROTOCOL.md +2 -10
  28. package/templates/gitignore.template +0 -3
  29. package/templates/pm/adr/README.md +1 -1
  30. package/templates/pm/decisions.md +4 -5
  31. package/templates/standards/CODE.md +1 -1
  32. package/templates/standards/DESIGN.md +1 -1
  33. package/templates/standards/REVIEW.md +2 -2
  34. package/templates/standards/STACK.md +2 -8
  35. package/templates/v2/AGENTS.md +18 -18
  36. package/templates/v2/BUILDBEAT.md +2 -3
  37. package/templates/v2/CLAUDE.md +1 -1
  38. package/templates/v2/run-config.example.yaml +1 -1
  39. package/templates/v2//346/214/207/346/214/245/345/217/260.md +6 -6
  40. package/bin/buildbeat-v2.js +0 -18
  41. package/bin/solobaton.js +0 -6
  42. package/docs/CHECKS.md +0 -326
  43. package/docs/CLI.md +0 -245
  44. package/docs/LEGACY-V1.16-MIGRATION.md +0 -54
  45. package/docs/v2/guide/08-migration-v1.md +0 -72
  46. package/example/.buildbeat/manifest.json +0 -45
  47. package/example/AGENTS.md +0 -19
  48. package/example/ARCHITECTURE.md +0 -39
  49. package/example/BUILDBEAT.md +0 -17
  50. package/example/CLAUDE.md +0 -7
  51. package/example/README.md +0 -75
  52. package/example/contracts/PROTOCOL.md +0 -38
  53. package/example/pm/NOW.md +0 -22
  54. package/example/pm/adr/ADR-0001-local-first-sqlite.md +0 -25
  55. package/example/pm/adr/README.md +0 -7
  56. package/example/pm/archive//344/270/200/346/234/237/evidence/gate1.md +0 -5
  57. package/example/pm/archive//344/270/200/346/234/237/evidence/gate2.md +0 -5
  58. package/example/pm/archive//344/270/200/346/234/237/evidence/gate3.md +0 -5
  59. package/example/pm/archive//344/270/200/346/234/237/evidence/gate4.md +0 -5
  60. package/example/pm/archive//344/270/200/346/234/237/evidence/implementation.md +0 -5
  61. package/example/pm/decisions.md +0 -20
  62. package/example/pm/status//344/272/247/345/223/201.md +0 -20
  63. package/example/pm/status//345/205/250/346/240/210.md +0 -15
  64. package/example/pm/status//346/265/213/350/257/225.md +0 -15
  65. package/example/pm//344/270/200/346/234/237-/347/234/213/346/235/277.md +0 -97
  66. package/example/standards/CODE.md +0 -18
  67. package/example/standards/DESIGN.md +0 -34
  68. package/example/standards/REVIEW.md +0 -16
  69. package/example/standards/STACK.md +0 -31
  70. package/src/cli.js +0 -323
  71. package/src/constants.js +0 -202
  72. package/src/doctor.js +0 -267
  73. package/src/planner.js +0 -251
  74. package/src/project.js +0 -844
  75. package/src/upgrader.js +0 -1249
  76. package/src/v2/presets/risk/legacy-four-gates.yaml +0 -44
  77. package/src/writer.js +0 -534
  78. package/templates/.claude/agents/reviewer.md +0 -62
  79. package/templates/AGENTS.md +0 -85
  80. package/templates/BUILDBEAT.md +0 -13
  81. package/templates/CLAUDE.md +0 -7
  82. package/templates/pm/NOW.md +0 -26
  83. package/templates/pm/changes/README.md +0 -44
  84. package/templates/pm/status/README.md +0 -32
  85. package/templates/pm//345/275/223/346/234/237/347/234/213/346/235/277.md +0 -62
  86. package/templates/scripts/bus-check.sh +0 -1875
  87. package/templates/scripts/design-preview.sh +0 -44
  88. package/templates/scripts/drift-check.sh +0 -112
  89. package/templates/scripts/pre-commit.sh +0 -74
  90. package/templates/scripts/verify-status.sh +0 -105
  91. package/templates//346/214/207/346/214/245/345/217/260.md +0 -58
package/src/doctor.js DELETED
@@ -1,267 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import path from "node:path";
3
-
4
- import {
5
- CLI_VERSION,
6
- COMMON_REQUIRED_FILES,
7
- OUTPUT_SCHEMA_VERSION,
8
- SCRIPT_NAMES,
9
- SUPPORTED_MANIFEST_SCHEMA_VERSIONS,
10
- } from "./constants.js";
11
- import { inspectProject } from "./project.js";
12
-
13
- const LEVEL_ORDER = { error: 0, warning: 1, info: 2 };
14
-
15
- function add(findings, level, code, message, relativePath = null) {
16
- findings.push({ level, code, message, path: relativePath });
17
- }
18
-
19
- function requiredFiles(installation) {
20
- const scriptBase = installation.layout === "compact" ? "pm/scripts" : "scripts";
21
- const operatorCard = installation.layout === "compact" ? "pm/指挥台.md" : "指挥台.md";
22
- const localFiles = [installation.markerPath, operatorCard].filter(Boolean);
23
- return [
24
- ...COMMON_REQUIRED_FILES,
25
- ...localFiles,
26
- ...SCRIPT_NAMES.map((name) => `${scriptBase}/${name}`),
27
- ];
28
- }
29
-
30
- function verifyStatusHasPlaceholders(target, layout) {
31
- const relative = layout === "compact"
32
- ? "pm/scripts/verify-status.sh"
33
- : "scripts/verify-status.sh";
34
- const filename = path.join(target, relative);
35
- if (!existsSync(filename)) {
36
- return { relative, unconfigured: false };
37
- }
38
- const text = readFileSync(filename, "utf8");
39
- return { relative, unconfigured: text.includes("<套件1>") || text.includes("<代码子仓1>") };
40
- }
41
-
42
- export function runDoctor(targetInput) {
43
- const inspection = inspectProject(targetInput);
44
- const findings = [];
45
- const { installation } = inspection;
46
-
47
- if (!inspection.exists) {
48
- add(findings, "error", "target.not_found", "Target directory does not exist.");
49
- } else if (installation.state === "not-installed") {
50
- add(findings, "error", "install.not_found", "No BuildBeat or legacy Solobaton installation was detected.");
51
- } else if (installation.state === "mixed") {
52
- add(
53
- findings,
54
- "error",
55
- "install.mixed_layout",
56
- "Multiple BuildBeat/legacy markers or default/compact layout signals coexist; ownership is ambiguous.",
57
- );
58
- } else if (installation.state === "partial") {
59
- add(
60
- findings,
61
- "error",
62
- "install.partial",
63
- `A partial ${installation.layout} layout was detected without a version marker.`,
64
- );
65
- }
66
-
67
- if (installation.state === "installed") {
68
- if (!installation.version) {
69
- add(findings, "error", "version.unreadable", `${installation.markerPath} has no readable installed version.`);
70
- }
71
- for (const relative of requiredFiles(installation)) {
72
- if (!existsSync(path.join(inspection.target, relative))) {
73
- add(findings, "error", "file.missing", "Required scaffold file is missing.", relative);
74
- }
75
- }
76
- if (installation.namespace === "solobaton") {
77
- add(
78
- findings,
79
- "info",
80
- "install.legacy_namespace",
81
- "A legacy Solobaton marker was detected; it remains readable, while new scaffolds use BuildBeat identifiers.",
82
- installation.markerPath,
83
- );
84
- }
85
- const verify = verifyStatusHasPlaceholders(inspection.target, installation.layout);
86
- if (verify.unconfigured) {
87
- add(
88
- findings,
89
- "warning",
90
- "verification.unconfigured",
91
- "verify-status still contains suite placeholders; L3 automation is not configured.",
92
- verify.relative,
93
- );
94
- }
95
- }
96
-
97
- if (inspection.manifest.state === "missing" && installation.state === "installed") {
98
- add(
99
- findings,
100
- "warning",
101
- "manifest.missing",
102
- "This is a legacy/unmanaged installation; safe automated upgrade and uninstall are unavailable.",
103
- inspection.manifest.path,
104
- );
105
- } else if (inspection.manifest.state === "ambiguous") {
106
- add(
107
- findings,
108
- "error",
109
- "manifest.ambiguous",
110
- "Both BuildBeat and legacy Solobaton lifecycle manifests exist; ownership is ambiguous.",
111
- inspection.manifest.path,
112
- );
113
- } else if (inspection.manifest.state === "invalid") {
114
- add(findings, "error", "manifest.invalid", "The lifecycle manifest is unreadable or invalid JSON.", inspection.manifest.path);
115
- } else if (
116
- inspection.manifest.state === "present" &&
117
- inspection.manifest.schemaVersion !== null &&
118
- !SUPPORTED_MANIFEST_SCHEMA_VERSIONS.includes(inspection.manifest.schemaVersion)
119
- ) {
120
- add(
121
- findings,
122
- "error",
123
- "manifest.unsupported_schema",
124
- `Manifest schema ${inspection.manifest.schemaVersion ?? "unknown"} is not supported.`,
125
- inspection.manifest.path,
126
- );
127
- } else if (inspection.manifest.state === "present") {
128
- for (const issue of inspection.manifest.validationIssues) {
129
- add(findings, "error", issue.code, issue.message, inspection.manifest.path);
130
- }
131
- if (
132
- installation.state === "installed" &&
133
- ["default", "compact"].includes(inspection.manifest.layout) &&
134
- inspection.manifest.layout !== installation.layout
135
- ) {
136
- add(
137
- findings,
138
- "error",
139
- "manifest.layout_mismatch",
140
- "Manifest layout does not match the detected installation layout.",
141
- inspection.manifest.path,
142
- );
143
- }
144
- if (
145
- installation.state === "installed" &&
146
- installation.version &&
147
- /^v\d+\.\d+(?:\.\d+)?$/.test(inspection.manifest.scaffoldVersion || "") &&
148
- inspection.manifest.scaffoldVersion !== installation.version
149
- ) {
150
- add(
151
- findings,
152
- "error",
153
- "manifest.version_mismatch",
154
- `Manifest scaffoldVersion does not match ${installation.markerPath}.`,
155
- inspection.manifest.path,
156
- );
157
- }
158
- }
159
-
160
- for (const item of inspection.placeholders) {
161
- add(
162
- findings,
163
- "warning",
164
- "placeholder.remaining",
165
- `Canonical placeholders remain: ${item.tokens.join(", ")}`,
166
- item.path,
167
- );
168
- }
169
-
170
- if (inspection.exists && !inspection.signals.hasGit) {
171
- add(findings, "error", "git.not_initialized", "The target has no root .git entry.");
172
- }
173
- if (inspection.exists && inspection.hook.mode === "direct-non-executable") {
174
- add(
175
- findings,
176
- "warning",
177
- "hook.non_executable",
178
- "The root pre-commit hook exists but is not executable.",
179
- inspection.hook.path,
180
- );
181
- } else if (inspection.exists && !inspection.hook.configured) {
182
- add(
183
- findings,
184
- "warning",
185
- "hook.missing",
186
- "No root pre-commit hook or core.hooksPath configuration was detected.",
187
- );
188
- }
189
-
190
- for (const command of ["git", "bash"]) {
191
- if (!inspection.dependencies[command].available) {
192
- add(findings, "error", `dependency.${command}_missing`, `${command} is required but unavailable.`);
193
- }
194
- }
195
- if (!inspection.dependencies.gitleaks.available) {
196
- add(
197
- findings,
198
- "warning",
199
- "dependency.gitleaks_missing",
200
- "gitleaks is unavailable; the local Secret gate degrades to a warning.",
201
- );
202
- }
203
- if (!inspection.dependencies.python3.available) {
204
- add(findings, "info", "dependency.python_missing", "Python 3 is unavailable; design preview cannot run.");
205
- }
206
- if (!inspection.dependencies.jq.available) {
207
- add(findings, "info", "dependency.jq_missing", "jq is unavailable; production drift checks cannot run.");
208
- }
209
- if (inspection.scan.truncated) {
210
- add(
211
- findings,
212
- "warning",
213
- "scan.truncated",
214
- "Project scan hit the 5,000-entry safety limit; results are incomplete.",
215
- );
216
- }
217
- for (const item of inspection.scan.warnings) {
218
- add(findings, "warning", "scan.unreadable", `Path could not be inspected: ${item.reason}.`, item.path);
219
- }
220
-
221
- findings.sort((a, b) =>
222
- LEVEL_ORDER[a.level] - LEVEL_ORDER[b.level] ||
223
- a.code.localeCompare(b.code) ||
224
- (a.path || "").localeCompare(b.path || ""),
225
- );
226
- const summary = {
227
- errors: findings.filter((item) => item.level === "error").length,
228
- warnings: findings.filter((item) => item.level === "warning").length,
229
- info: findings.filter((item) => item.level === "info").length,
230
- };
231
-
232
- return {
233
- schemaVersion: OUTPUT_SCHEMA_VERSION,
234
- command: "doctor",
235
- cliVersion: CLI_VERSION,
236
- target: inspection.target,
237
- installation: inspection.installation,
238
- manifest: inspection.manifest,
239
- capabilities: inspection.dependencies,
240
- findings,
241
- summary,
242
- ok: summary.errors === 0,
243
- };
244
- }
245
-
246
- export function formatDoctor(report) {
247
- const lines = [
248
- "BuildBeat doctor",
249
- `Target: ${report.target}`,
250
- `Installation: ${report.installation.state}${report.installation.layout ? ` (${report.installation.layout})` : ""}${report.installation.version ? ` ${report.installation.version}` : ""}`,
251
- `Manifest: ${report.manifest.state}`,
252
- "",
253
- ];
254
- if (report.findings.length === 0) {
255
- lines.push("OK No findings.");
256
- } else {
257
- for (const finding of report.findings) {
258
- const location = finding.path ? ` [${finding.path}]` : "";
259
- lines.push(`${finding.level.toUpperCase()} ${finding.code}${location}: ${finding.message}`);
260
- }
261
- }
262
- lines.push(
263
- "",
264
- `Result: ${report.summary.errors} error(s), ${report.summary.warnings} warning(s), ${report.summary.info} info item(s).`,
265
- );
266
- return lines.join("\n");
267
- }
package/src/planner.js DELETED
@@ -1,251 +0,0 @@
1
- import {
2
- CLI_VERSION,
3
- MANIFEST_PATH,
4
- OUTPUT_SCHEMA_VERSION,
5
- SCAFFOLD_VERSION,
6
- } from "./constants.js";
7
- import { inspectProject, plannedFiles } from "./project.js";
8
- import { prepareScaffold } from "./writer.js";
9
-
10
- export function buildPlan({ mode, target, layout, preview = true, now = new Date() }) {
11
- const inspection = inspectProject(target, {
12
- collisionLayout: layout,
13
- includeDependencies: false,
14
- });
15
- const rendered = prepareScaffold({
16
- layout,
17
- projectName: inspection.projectName.value,
18
- now,
19
- });
20
- const operations = plannedFiles(layout).map((item) => ({
21
- action: item.policy === "merge-only" ? "merge" : "seed",
22
- source: `templates/${item.template}`,
23
- target: item.target,
24
- policy: item.policy,
25
- collision: inspection.collisions.includes(item.target),
26
- }));
27
- const blockers = [];
28
- const warnings = [];
29
-
30
- if (inspection.installation.state === "installed") {
31
- blockers.push({
32
- code: "install.already_present",
33
- message: "BuildBeat or a legacy Solobaton scaffold is already installed; a future upgrade/adoption path must own this transition.",
34
- });
35
- } else if (inspection.installation.state === "mixed") {
36
- blockers.push({
37
- code: "install.mixed_layout",
38
- message: "Multiple BuildBeat/legacy markers or layout signals are present; reconcile ownership before any lifecycle write.",
39
- });
40
- } else if (inspection.installation.state === "partial") {
41
- blockers.push({
42
- code: "install.partial",
43
- message: "A partial installation exists; the CLI will not guess which files are user-owned.",
44
- });
45
- }
46
-
47
- if (inspection.manifest.state !== "missing") {
48
- blockers.push({
49
- code: "manifest.already_present",
50
- message: "A lifecycle manifest already exists or is unreadable; reconcile it before scaffolding.",
51
- });
52
- }
53
-
54
- if (mode === "adopt" && !inspection.exists) {
55
- blockers.push({
56
- code: "target.not_found",
57
- message: "Brownfield adoption requires an existing project directory.",
58
- });
59
- }
60
-
61
- if (mode === "init" && inspection.exists && inspection.signals.nonEmpty) {
62
- warnings.push({
63
- code: "target.non_empty",
64
- message: "The target is not empty; confirm whether the brownfield adopt flow is more appropriate.",
65
- });
66
- }
67
- if (mode === "adopt" && inspection.exists && !inspection.signals.nonEmpty) {
68
- warnings.push({
69
- code: "target.empty",
70
- message: "The target is empty; the new-project init flow is probably more appropriate.",
71
- });
72
- }
73
- if (mode === "adopt" && layout === "default") {
74
- warnings.push({
75
- code: "layout.default_for_adopt",
76
- message: "Brownfield adoption normally uses compact layout; default layout needs an explicit collision review.",
77
- });
78
- }
79
- if (inspection.collisions.length > 0) {
80
- blockers.push({
81
- code: "files.collide",
82
- message: `${inspection.collisions.length} planned target path(s) collide; Wave 1 never overwrites project files.`,
83
- });
84
- }
85
- if (inspection.gitWorktree.state === "dirty") {
86
- blockers.push({
87
- code: "git.dirty",
88
- message: `The target-root Git worktree has ${inspection.gitWorktree.changes} visible change(s); commit or otherwise clean it before writing.`,
89
- });
90
- } else if (inspection.gitWorktree.state === "unavailable") {
91
- blockers.push({
92
- code: "git.status_unavailable",
93
- message: "A root .git entry exists, but the fixed read-only Git status check failed.",
94
- });
95
- }
96
- if (inspection.gitignore.state === "unsafe") {
97
- blockers.push({
98
- code: "integration.gitignore_unsafe",
99
- message: ".gitignore exists but is not a readable regular file.",
100
- });
101
- } else if (inspection.gitignore.beginMarkers > 0 || inspection.gitignore.endMarkers > 0) {
102
- blockers.push({
103
- code: "integration.gitignore_fragment_present",
104
- message: "BuildBeat or legacy Solobaton fragment markers already exist without schema 2 ownership metadata.",
105
- });
106
- }
107
- if (inspection.scan.truncated) {
108
- warnings.push({
109
- code: "scan.truncated",
110
- message: "The project scan hit its safety limit; inspect the omitted area before Gate 1.",
111
- });
112
- }
113
- if (inspection.scan.symlinks.length > 0) {
114
- warnings.push({
115
- code: "scan.symlinks_skipped",
116
- message: `${inspection.scan.symlinks.length} symbolic link(s) were skipped and require separate inspection.`,
117
- });
118
- }
119
- if (inspection.scan.warnings.length > 0) {
120
- warnings.push({
121
- code: "scan.unreadable",
122
- message: `${inspection.scan.warnings.length} path(s) could not be inspected.`,
123
- });
124
- }
125
-
126
- const questions = [
127
- "Is this project expected to finish in a few days, or will it be maintained long term?",
128
- "Will anyone else open AI coding sessions for this project?",
129
- "Keep the default Product / Fullstack / Testing sessions, or change them?",
130
- ];
131
- if (inspection.signals.hasUi) {
132
- questions.push("Who decides the UI result: an existing design source, or your rendered review?");
133
- }
134
-
135
- const steps = mode === "adopt"
136
- ? [
137
- "Review the detected repositories, deployment markers, tests, and high-risk boundaries.",
138
- "Approve the old/new strangler boundary and establish a minimum L3 verification suite.",
139
- "Seed the compact coordination layout without replacing the project's existing scripts directory.",
140
- "Render project facts, record the first decision package, then run bus-check.",
141
- "Install hooks only after detecting and preserving any existing hook chain.",
142
- ]
143
- : [
144
- "Confirm the detected project facts and the small remaining question set.",
145
- "Seed the selected coordination layout and render every canonical placeholder.",
146
- "Merge ignore rules instead of replacing .gitignore.",
147
- "Record the first decision package, configure verification, then run bus-check.",
148
- "Install hooks only after detecting and preserving any existing hook chain.",
149
- ];
150
-
151
- return {
152
- schemaVersion: OUTPUT_SCHEMA_VERSION,
153
- command: mode,
154
- cliVersion: CLI_VERSION,
155
- scaffoldVersion: SCAFFOLD_VERSION,
156
- preview,
157
- writesPerformed: false,
158
- target: inspection.target,
159
- targetExists: inspection.exists,
160
- layout,
161
- detected: {
162
- projectName: inspection.projectName,
163
- rootGit: inspection.signals.hasGit,
164
- repositories: inspection.repositories,
165
- deploymentMarkers: inspection.deploymentMarkers,
166
- hasTests: inspection.signals.hasTests,
167
- hasUi: inspection.signals.hasUi,
168
- symlinksSkipped: inspection.scan.symlinks.length,
169
- scanTruncated: inspection.scan.truncated,
170
- },
171
- operations,
172
- collisions: inspection.collisions,
173
- writtenPaths: [],
174
- renderedPlaceholders: rendered.renderedPlaceholders,
175
- pendingPlaceholders: rendered.pendingPlaceholders,
176
- manifestPath: MANIFEST_PATH,
177
- nextAction: mode === "adopt"
178
- ? "Continue with SKILL.md §8.5 to verify the brownfield boundary and render project-owned facts."
179
- : "Continue with SKILL.md §8 to render project-owned facts and complete Bootstrap.",
180
- blockers,
181
- warnings,
182
- questions,
183
- steps,
184
- ready: blockers.length === 0,
185
- };
186
- }
187
-
188
- export function formatPlan(plan) {
189
- if (plan.writesPerformed) {
190
- const lines = [
191
- `BuildBeat ${plan.command} complete`,
192
- `Target: ${plan.target}`,
193
- `Layout: ${plan.layout}`,
194
- `Written paths: ${plan.writtenPaths.length}`,
195
- `Manifest: ${plan.manifestPath}`,
196
- `Pending placeholder files: ${plan.pendingPlaceholders.length}`,
197
- ];
198
- if (plan.pendingPlaceholders.length > 0) {
199
- lines.push("", "Pending placeholders:");
200
- plan.pendingPlaceholders.forEach((item) => {
201
- lines.push(`- ${item.path}: ${item.tokens.join(", ")}`);
202
- });
203
- }
204
- lines.push("", plan.nextAction);
205
- return lines.join("\n");
206
- }
207
- const lines = [
208
- `BuildBeat ${plan.command} ${plan.preview ? "dry run" : "write plan"}`,
209
- `Target: ${plan.target}${plan.targetExists ? "" : plan.preview ? " (does not exist)" : " (will be created)"}`,
210
- `Layout: ${plan.layout}`,
211
- `Detected project: ${plan.detected.projectName.value} (${plan.detected.projectName.source})`,
212
- `Repositories: ${plan.detected.repositories.length}; deployment markers: ${plan.detected.deploymentMarkers.length}; tests: ${plan.detected.hasTests ? "detected" : "not detected"}; UI: ${plan.detected.hasUi ? "detected" : "not detected"}`,
213
- `Planned paths: ${plan.operations.length}; collisions: ${plan.collisions.length}`,
214
- "",
215
- ];
216
- for (const blocker of plan.blockers) {
217
- lines.push(`BLOCKER ${blocker.code}: ${blocker.message}`);
218
- }
219
- for (const warning of plan.warnings) {
220
- lines.push(`WARNING ${warning.code}: ${warning.message}`);
221
- }
222
- if (plan.blockers.length > 0 || plan.warnings.length > 0) {
223
- lines.push("");
224
- }
225
- lines.push("Planned operations:");
226
- plan.operations.forEach((operation, index) => {
227
- lines.push(
228
- `${index + 1}. ${operation.action} ${operation.target} (${operation.policy})${operation.collision ? " [collision]" : ""}`,
229
- );
230
- });
231
- lines.push("", `Deterministic replacements: ${plan.renderedPlaceholders.length}`);
232
- plan.renderedPlaceholders.forEach((item) => {
233
- lines.push(`- ${item.path}: ${item.token} -> ${item.value}`);
234
- });
235
- lines.push("", `Pending placeholder files: ${plan.pendingPlaceholders.length}`);
236
- plan.pendingPlaceholders.forEach((item) => {
237
- lines.push(`- ${item.path}: ${item.tokens.join(", ")}`);
238
- });
239
- lines.push("");
240
- lines.push("Remaining human questions:");
241
- plan.questions.forEach((question, index) => lines.push(`${index + 1}. ${question}`));
242
- lines.push("", "Proposed sequence:");
243
- plan.steps.forEach((step, index) => lines.push(`${index + 1}. ${step}`));
244
- lines.push(
245
- "",
246
- plan.preview
247
- ? "No files changed. This was a dry run."
248
- : "No files changed yet. Apply only after the confirmation prompt.",
249
- );
250
- return lines.join("\n");
251
- }