@design-ai/cli 4.55.0 → 4.56.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 (52) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +39 -0
  3. package/README.ko.md +7 -7
  4. package/README.md +9 -9
  5. package/cli/lib/mcp-server.mjs +208 -10
  6. package/cli/lib/site-analysis.mjs +297 -0
  7. package/cli/lib/site-args.mjs +433 -0
  8. package/cli/lib/site-bundle-build.mjs +127 -0
  9. package/cli/lib/site-bundle-check.mjs +454 -0
  10. package/cli/lib/site-bundle-commands.mjs +95 -0
  11. package/cli/lib/site-bundle-compare.mjs +157 -0
  12. package/cli/lib/site-bundle-contract.mjs +79 -0
  13. package/cli/lib/site-bundle-files.mjs +87 -0
  14. package/cli/lib/site-bundle-handoff-runbook-actions.mjs +113 -0
  15. package/cli/lib/site-bundle-handoff-runbook-evidence-fields.mjs +164 -0
  16. package/cli/lib/site-bundle-handoff-runbook-evidence.mjs +334 -0
  17. package/cli/lib/site-bundle-handoff-runbook-format.mjs +31 -0
  18. package/cli/lib/site-bundle-handoff-runbook-stage-metadata.mjs +84 -0
  19. package/cli/lib/site-bundle-handoff-runbook.mjs +1331 -0
  20. package/cli/lib/site-bundle-handoff-summary.mjs +183 -0
  21. package/cli/lib/site-bundle-handoff.mjs +271 -0
  22. package/cli/lib/site-bundle-readme.mjs +98 -0
  23. package/cli/lib/site-bundle-repair-report.mjs +143 -0
  24. package/cli/lib/site-bundle-repair.mjs +68 -0
  25. package/cli/lib/site-content.mjs +399 -0
  26. package/cli/lib/site-evidence.mjs +35 -0
  27. package/cli/lib/site-mcp-commands.mjs +28 -0
  28. package/cli/lib/site-mcp-probes.mjs +159 -0
  29. package/cli/lib/site-mcp-readiness.mjs +157 -0
  30. package/cli/lib/site-mcp-report.mjs +324 -0
  31. package/cli/lib/site-next-actions.mjs +333 -0
  32. package/cli/lib/site-options.mjs +104 -0
  33. package/cli/lib/site-prompts.mjs +332 -0
  34. package/cli/lib/site-starter.mjs +153 -0
  35. package/cli/lib/site-strings.mjs +23 -0
  36. package/cli/lib/site-tasks.mjs +93 -0
  37. package/cli/lib/site-workflow-graph.mjs +309 -0
  38. package/cli/lib/site-workspace.mjs +492 -0
  39. package/cli/lib/site.mjs +108 -6617
  40. package/docs/DISTRIBUTION.ko.md +35 -6
  41. package/docs/DISTRIBUTION.md +35 -8
  42. package/docs/RELEASE-CHECKLIST.md +20 -3
  43. package/docs/ROADMAP.md +2179 -0
  44. package/docs/external-status.md +22 -7
  45. package/docs/integrations/design-ai-mcp-server.md +32 -0
  46. package/docs/integrations/vscode-walkthrough.ko.md +3 -3
  47. package/docs/integrations/vscode-walkthrough.md +3 -3
  48. package/docs/site-overrides/main.html +1 -1
  49. package/package.json +1 -1
  50. package/tools/audit/package-smoke.py +106 -0
  51. package/tools/audit/registry-smoke.py +378 -10
  52. package/tools/audit/smoke_assertions.py +83 -22
@@ -0,0 +1,454 @@
1
+ // Handoff bundle validation for Website Improvement.
2
+
3
+ import {
4
+ existsSync,
5
+ readFileSync,
6
+ readdirSync,
7
+ statSync,
8
+ } from "node:fs";
9
+ import path from "node:path";
10
+
11
+ import { buildSiteMcpProbeReport } from "./site-mcp-probes.mjs";
12
+ import { buildSiteMcpCheckReport } from "./site-mcp-report.mjs";
13
+ import {
14
+ IMPLEMENTATION_EVIDENCE_KEYS,
15
+ countImplementationEvidence,
16
+ } from "./site-evidence.mjs";
17
+ import {
18
+ SITE_BUNDLE_CHECKSUM_FILES,
19
+ SITE_BUNDLE_FILES,
20
+ } from "./site-content.mjs";
21
+ import { normalizeObject } from "./site-workspace.mjs";
22
+ import {
23
+ addIssue,
24
+ analyzeSiteWorkspace,
25
+ statusFromIssues,
26
+ } from "./site-analysis.mjs";
27
+ import {
28
+ buildBundleRepairGuidance,
29
+ formatBundleRepairGuidanceLines,
30
+ } from "./site-bundle-repair.mjs";
31
+ import {
32
+ addBundleMarkdownIssue,
33
+ arraysEqual,
34
+ buildBundleDigest,
35
+ parseBundleJson,
36
+ sha256Hex,
37
+ } from "./site-bundle-files.mjs";
38
+ import {
39
+ addBundleGeneratedContractIssues,
40
+ buildBundleGeneratedContract,
41
+ emptyBundleGeneratedContract,
42
+ formatGeneratedContractDriftLines,
43
+ formatGeneratedContractDriftSummary,
44
+ } from "./site-bundle-contract.mjs";
45
+ import { SITE_TARGET_REPO_EXECUTION_CHECKLIST } from "./site-bundle-handoff-summary.mjs";
46
+
47
+ function summarizeBundlePayload(summaryPayload) {
48
+ const taskGeneration = normalizeObject(summaryPayload?.taskGeneration);
49
+ const site = normalizeObject(summaryPayload?.site);
50
+ const mcp = normalizeObject(summaryPayload?.mcp);
51
+ const probeCounts = normalizeObject(mcp.probeCounts);
52
+ const checksums = normalizeObject(summaryPayload?.checksums);
53
+ const handoff = normalizeObject(summaryPayload?.handoff);
54
+ return {
55
+ source: String(summaryPayload?.source || ""),
56
+ status: String(summaryPayload?.status || "unknown"),
57
+ workspaceStatus: String(summaryPayload?.workspaceStatus || "unknown"),
58
+ siteName: String(site.name || ""),
59
+ totalTasks: Number.isFinite(taskGeneration.totalTasks) ? taskGeneration.totalTasks : 0,
60
+ implementationEvidence: countImplementationEvidence(summaryPayload?.implementationEvidence),
61
+ mcpStatus: String(mcp.status || "unknown"),
62
+ mcpProbeStatus: String(mcp.probeStatus || "unknown"),
63
+ mcpProbeCounts: {
64
+ count: Number.isInteger(probeCounts.count) && probeCounts.count >= 0 ? probeCounts.count : 0,
65
+ pass: Number.isInteger(probeCounts.pass) && probeCounts.pass >= 0 ? probeCounts.pass : 0,
66
+ warn: Number.isInteger(probeCounts.warn) && probeCounts.warn >= 0 ? probeCounts.warn : 0,
67
+ fail: Number.isInteger(probeCounts.fail) && probeCounts.fail >= 0 ? probeCounts.fail : 0,
68
+ },
69
+ files: Array.isArray(summaryPayload?.files) ? summaryPayload.files.map(String) : [],
70
+ checksumAlgorithm: String(checksums.algorithm || ""),
71
+ checksumBundleDigest: String(checksums.bundleDigest || ""),
72
+ checksumFiles: normalizeObject(checksums.files),
73
+ handoffExecutionChecklist: Array.isArray(handoff.executionChecklist)
74
+ ? handoff.executionChecklist.map((item) => normalizeObject(item))
75
+ : [],
76
+ };
77
+ }
78
+
79
+ function validateBundleHandoffExecutionChecklist(summary, issues) {
80
+ const expectedIds = SITE_TARGET_REPO_EXECUTION_CHECKLIST.map((item) => item.id);
81
+ const actual = Array.isArray(summary.handoffExecutionChecklist) ? summary.handoffExecutionChecklist : [];
82
+ const actualIds = actual.map((item) => String(item.id || ""));
83
+ if (!arraysEqual(actualIds, expectedIds)) {
84
+ addIssue(issues, "fail", "bundle-handoff-execution-checklist", "summary.json handoff.executionChecklist must match the target-repo execution checklist contract");
85
+ return;
86
+ }
87
+ for (const [index, expected] of SITE_TARGET_REPO_EXECUTION_CHECKLIST.entries()) {
88
+ const actualItem = actual[index] || {};
89
+ if (actualItem.label !== expected.label) {
90
+ addIssue(issues, "fail", `bundle-handoff-execution-checklist-${expected.id}-label`, `summary.json handoff.executionChecklist.${expected.id}.label changed`);
91
+ }
92
+ if (actualItem.required !== expected.required) {
93
+ addIssue(issues, "fail", `bundle-handoff-execution-checklist-${expected.id}-required`, `summary.json handoff.executionChecklist.${expected.id}.required changed`);
94
+ }
95
+ if (actualItem.evidence !== expected.evidence) {
96
+ addIssue(issues, "fail", `bundle-handoff-execution-checklist-${expected.id}-evidence`, `summary.json handoff.executionChecklist.${expected.id}.evidence changed`);
97
+ }
98
+ }
99
+ }
100
+
101
+ function summarizeBundleBoundaries(summaryPayload) {
102
+ const boundaries = Array.isArray(summaryPayload?.boundaries)
103
+ ? summaryPayload.boundaries.map(String)
104
+ : [];
105
+ return {
106
+ boundaries,
107
+ externalCalls: false,
108
+ targetRepoMutation: false,
109
+ };
110
+ }
111
+
112
+ export function buildSiteBundleCheckReport({
113
+ target,
114
+ cwd = process.cwd(),
115
+ } = {}) {
116
+ const directory = path.resolve(cwd, String(target || ""));
117
+ const issues = [];
118
+
119
+ if (!target) {
120
+ addIssue(issues, "fail", "bundle-directory-required", "A handoff bundle directory path is required");
121
+ } else if (!existsSync(directory)) {
122
+ addIssue(issues, "fail", "bundle-directory-missing", `Bundle directory does not exist: ${directory}`);
123
+ } else if (!statSync(directory).isDirectory()) {
124
+ addIssue(issues, "fail", "bundle-directory-type", `Bundle path must be a directory: ${directory}`);
125
+ }
126
+
127
+ const canReadDirectory = issues.every((issue) => !issue.id.startsWith("bundle-directory"));
128
+ const expected = new Set(SITE_BUNDLE_FILES);
129
+ const directEntries = canReadDirectory ? readdirSync(directory) : [];
130
+ const directFiles = directEntries.filter((entry) => {
131
+ const targetPath = path.join(directory, entry);
132
+ return existsSync(targetPath) && statSync(targetPath).isFile();
133
+ });
134
+ const unexpectedFiles = directFiles.filter((entry) => !expected.has(entry)).sort();
135
+ const files = SITE_BUNDLE_FILES.map((relativePath) => {
136
+ const targetPath = path.join(directory, relativePath);
137
+ const present = canReadDirectory && existsSync(targetPath) && statSync(targetPath).isFile();
138
+ return {
139
+ path: relativePath,
140
+ present,
141
+ };
142
+ });
143
+
144
+ if (canReadDirectory) {
145
+ for (const file of SITE_BUNDLE_FILES) {
146
+ const targetPath = path.join(directory, file);
147
+ if (!existsSync(targetPath)) {
148
+ addIssue(issues, "fail", `bundle-missing-${file}`, `Bundle file is missing: ${file}`);
149
+ } else if (!statSync(targetPath).isFile()) {
150
+ addIssue(issues, "fail", `bundle-file-${file}`, `Bundle path must be a file: ${file}`);
151
+ }
152
+ }
153
+ }
154
+
155
+ const summaryPayload = canReadDirectory ? parseBundleJson(directory, "summary.json", issues) : null;
156
+ const workspacePayload = canReadDirectory ? parseBundleJson(directory, "website-workspace.tasks.json", issues) : null;
157
+ const mcpPayload = canReadDirectory ? parseBundleJson(directory, "mcp-check.json", issues) : null;
158
+ const mcpProbePayload = canReadDirectory ? parseBundleJson(directory, "mcp-probes.json", issues) : null;
159
+ const summary = summarizeBundlePayload(summaryPayload);
160
+ const boundarySummary = summarizeBundleBoundaries(summaryPayload);
161
+
162
+ let workspaceSummary = null;
163
+ let recomputedMcp = null;
164
+ let recomputedMcpProbes = null;
165
+ let generatedContract = emptyBundleGeneratedContract(summary.source);
166
+
167
+ if (summaryPayload) {
168
+ if (summaryPayload.version !== 1) {
169
+ addIssue(issues, "fail", "bundle-summary-version", "summary.json version must be 1");
170
+ }
171
+ if (!["pass", "warn", "fail"].includes(summary.status)) {
172
+ addIssue(issues, "fail", "bundle-summary-status", "summary.json status must be pass, warn, or fail");
173
+ } else if (summary.status === "fail") {
174
+ addIssue(issues, "fail", "bundle-readiness-fail", "summary.json reports a failing handoff bundle");
175
+ } else if (summary.status === "warn") {
176
+ addIssue(issues, "warn", "bundle-readiness-warn", "summary.json reports readiness warnings");
177
+ }
178
+ if (!arraysEqual(summary.files, SITE_BUNDLE_FILES)) {
179
+ addIssue(issues, "fail", "bundle-summary-files", "summary.json files must match the expected handoff bundle manifest");
180
+ }
181
+ validateBundleHandoffExecutionChecklist(summary, issues);
182
+ if (summaryPayload.implementationEvidence !== undefined) {
183
+ const evidenceCounts = normalizeObject(summaryPayload.implementationEvidence);
184
+ if (summaryPayload.implementationEvidence === null || typeof summaryPayload.implementationEvidence !== "object" || Array.isArray(summaryPayload.implementationEvidence)) {
185
+ addIssue(issues, "fail", "bundle-summary-implementation-evidence", "summary.json implementationEvidence must be an object when provided");
186
+ } else {
187
+ for (const key of IMPLEMENTATION_EVIDENCE_KEYS) {
188
+ if (!Number.isInteger(evidenceCounts[key]) || evidenceCounts[key] < 0) {
189
+ addIssue(issues, "fail", `bundle-summary-implementation-evidence-${key}`, `summary.json implementationEvidence.${key} must be a non-negative integer`);
190
+ }
191
+ }
192
+ }
193
+ }
194
+ const boundaries = Array.isArray(summaryPayload.boundaries) ? summaryPayload.boundaries : [];
195
+ for (const boundary of ["deterministic-local", "no-external-mcp-calls", "no-target-repo-mutation"]) {
196
+ if (!boundaries.includes(boundary)) {
197
+ addIssue(issues, "warn", `bundle-boundary-${boundary}`, `summary.json boundaries should include ${boundary}`);
198
+ }
199
+ }
200
+
201
+ if (!summaryPayload.checksums) {
202
+ addIssue(issues, "warn", "bundle-checksums-missing", "summary.json should include SHA-256 checksums; regenerate the bundle with the current CLI");
203
+ } else if (summary.checksumAlgorithm !== "sha256") {
204
+ addIssue(issues, "fail", "bundle-checksum-algorithm", "summary.json checksums.algorithm must be sha256");
205
+ } else {
206
+ const checksumFiles = summary.checksumFiles;
207
+ const checksumKeys = Object.keys(checksumFiles).sort();
208
+ const expectedChecksumKeys = SITE_BUNDLE_CHECKSUM_FILES;
209
+ if (!summary.checksumBundleDigest) {
210
+ addIssue(issues, "warn", "bundle-checksum-bundle-digest-missing", "summary.json should include checksums.bundleDigest; regenerate the bundle with the current CLI");
211
+ } else if (!/^[a-f0-9]{64}$/.test(summary.checksumBundleDigest)) {
212
+ addIssue(issues, "fail", "bundle-checksum-bundle-digest-format", "summary.json checksums.bundleDigest must be a SHA-256 hex digest");
213
+ } else {
214
+ const manifestBundleDigest = buildBundleDigest(checksumFiles);
215
+ if (manifestBundleDigest !== summary.checksumBundleDigest) {
216
+ addIssue(issues, "fail", "bundle-checksum-bundle-digest-manifest", "summary.json checksums.bundleDigest does not match the checksum file manifest");
217
+ }
218
+ }
219
+ for (const expectedPath of expectedChecksumKeys) {
220
+ const expectedDigest = checksumFiles[expectedPath];
221
+ if (!expectedDigest) {
222
+ addIssue(issues, "fail", `bundle-checksum-missing-${expectedPath}`, `summary.json is missing a checksum for ${expectedPath}`);
223
+ continue;
224
+ }
225
+ if (!/^[a-f0-9]{64}$/.test(String(expectedDigest))) {
226
+ addIssue(issues, "fail", `bundle-checksum-format-${expectedPath}`, `summary.json checksum for ${expectedPath} must be a SHA-256 hex digest`);
227
+ }
228
+ }
229
+ for (const checksumPath of checksumKeys) {
230
+ if (!expectedChecksumKeys.includes(checksumPath)) {
231
+ addIssue(issues, "fail", `bundle-checksum-unexpected-${checksumPath}`, `summary.json includes an unexpected checksum entry: ${checksumPath}`);
232
+ }
233
+ }
234
+ if (canReadDirectory) {
235
+ for (const expectedPath of expectedChecksumKeys) {
236
+ const targetPath = path.join(directory, expectedPath);
237
+ if (!existsSync(targetPath) || !statSync(targetPath).isFile()) continue;
238
+ const expectedDigest = checksumFiles[expectedPath];
239
+ if (!expectedDigest || !/^[a-f0-9]{64}$/.test(String(expectedDigest))) continue;
240
+ const actualDigest = sha256Hex(readFileSync(targetPath, "utf8"));
241
+ if (actualDigest !== expectedDigest) {
242
+ addIssue(issues, "fail", `bundle-checksum-${expectedPath}`, `${expectedPath} checksum does not match summary.json`);
243
+ }
244
+ }
245
+ if (summary.checksumBundleDigest && /^[a-f0-9]{64}$/.test(summary.checksumBundleDigest)) {
246
+ const actualChecksumFiles = Object.fromEntries(
247
+ expectedChecksumKeys
248
+ .filter((filePath) => {
249
+ const targetPath = path.join(directory, filePath);
250
+ return existsSync(targetPath) && statSync(targetPath).isFile();
251
+ })
252
+ .map((filePath) => [filePath, sha256Hex(readFileSync(path.join(directory, filePath), "utf8"))]),
253
+ );
254
+ if (
255
+ expectedChecksumKeys.every((filePath) => actualChecksumFiles[filePath])
256
+ && buildBundleDigest(actualChecksumFiles) !== summary.checksumBundleDigest
257
+ ) {
258
+ addIssue(issues, "fail", "bundle-checksum-bundle-digest", "Current bundle files do not match summary.json checksums.bundleDigest");
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+
265
+ if (workspacePayload) {
266
+ const analyzed = analyzeSiteWorkspace(workspacePayload, {
267
+ filePath: path.join(directory, "website-workspace.tasks.json"),
268
+ });
269
+ workspaceSummary = analyzed.summary;
270
+ for (const issue of workspaceSummary.issues.filter((item) => item.level !== "pass")) {
271
+ addIssue(issues, issue.level, `workspace-${issue.id}`, issue.message);
272
+ }
273
+ if (summaryPayload && summary.siteName && summary.siteName !== analyzed.workspace.siteProfile.name) {
274
+ addIssue(issues, "fail", "bundle-site-name", "summary.json site name does not match website-workspace.tasks.json");
275
+ }
276
+ if (summaryPayload && summary.totalTasks !== analyzed.workspace.refactorTasks.length) {
277
+ addIssue(issues, "fail", "bundle-task-count", "summary.json taskGeneration.totalTasks does not match website-workspace.tasks.json");
278
+ }
279
+ const workspaceEvidenceCounts = countImplementationEvidence(analyzed.workspace.implementationEvidence);
280
+ if (summaryPayload && summaryPayload.implementationEvidence !== undefined) {
281
+ for (const key of IMPLEMENTATION_EVIDENCE_KEYS) {
282
+ if (summary.implementationEvidence[key] !== workspaceEvidenceCounts[key]) {
283
+ addIssue(issues, "fail", `bundle-implementation-evidence-${key}`, `summary.json implementationEvidence.${key} does not match website-workspace.tasks.json`);
284
+ }
285
+ }
286
+ } else {
287
+ summary.implementationEvidence = workspaceEvidenceCounts;
288
+ }
289
+ if (canReadDirectory && workspaceSummary.status !== "fail") {
290
+ generatedContract = buildBundleGeneratedContract(directory, analyzed.workspace, summary.source);
291
+ addBundleGeneratedContractIssues(generatedContract, issues);
292
+ }
293
+ recomputedMcp = buildSiteMcpCheckReport(analyzed.workspace, analyzed.summary);
294
+ recomputedMcpProbes = buildSiteMcpProbeReport(analyzed.workspace);
295
+ }
296
+
297
+ if (mcpPayload && recomputedMcp) {
298
+ if (mcpPayload.status !== recomputedMcp.status) {
299
+ addIssue(issues, "fail", "bundle-mcp-status", "mcp-check.json status does not match recomputed MCP readiness");
300
+ }
301
+ if (!arraysEqual((mcpPayload.items || []).map((item) => item.key), recomputedMcp.items.map((item) => item.key))) {
302
+ addIssue(issues, "fail", "bundle-mcp-items", "mcp-check.json item order does not match the current MCP readiness contract");
303
+ }
304
+ if (JSON.stringify(mcpPayload.counts || {}) !== JSON.stringify(recomputedMcp.counts)) {
305
+ addIssue(issues, "fail", "bundle-mcp-counts", "mcp-check.json counts do not match recomputed MCP readiness");
306
+ }
307
+ if (summaryPayload && summary.mcpStatus !== String(mcpPayload.status || "")) {
308
+ addIssue(issues, "fail", "bundle-summary-mcp-status", "summary.json mcp.status does not match mcp-check.json");
309
+ }
310
+ }
311
+
312
+ if (mcpProbePayload && recomputedMcpProbes) {
313
+ if (mcpProbePayload.status !== recomputedMcpProbes.status) {
314
+ addIssue(issues, "fail", "bundle-mcp-probe-status", "mcp-probes.json status does not match recomputed MCP probe readiness");
315
+ }
316
+ if (!arraysEqual((mcpProbePayload.items || []).map((item) => item.id), recomputedMcpProbes.items.map((item) => item.id))) {
317
+ addIssue(issues, "fail", "bundle-mcp-probe-items", "mcp-probes.json item order does not match the current MCP probe contract");
318
+ }
319
+ for (const key of ["count", "pass", "warn", "fail"]) {
320
+ if (mcpProbePayload[key] !== recomputedMcpProbes[key]) {
321
+ addIssue(issues, "fail", `bundle-mcp-probe-${key}`, `mcp-probes.json ${key} does not match recomputed MCP probe readiness`);
322
+ }
323
+ }
324
+ if (summaryPayload && summary.mcpProbeStatus !== String(mcpProbePayload.status || "")) {
325
+ addIssue(issues, "fail", "bundle-summary-mcp-probe-status", "summary.json mcp.probeStatus does not match mcp-probes.json");
326
+ }
327
+ if (summaryPayload) {
328
+ for (const key of ["count", "pass", "warn", "fail"]) {
329
+ if (summary.mcpProbeCounts[key] !== mcpProbePayload[key]) {
330
+ addIssue(issues, "fail", `bundle-summary-mcp-probe-counts-${key}`, `summary.json mcp.probeCounts.${key} does not match mcp-probes.json`);
331
+ }
332
+ }
333
+ }
334
+ }
335
+
336
+ if (canReadDirectory) {
337
+ addBundleMarkdownIssue(directory, "README.md", [
338
+ "Website improvement handoff bundle",
339
+ "does not call external MCPs",
340
+ "Target Repo Execution Checklist",
341
+ "Confirm target repo working directory",
342
+ ], issues);
343
+ addBundleMarkdownIssue(directory, "mcp-action-plan.md", [
344
+ "# Website improvement MCP action plan",
345
+ ], issues);
346
+ addBundleMarkdownIssue(directory, "mcp-probes.json", [
347
+ "\"mode\": \"read-only-local\"",
348
+ "\"externalCalls\": false",
349
+ ], issues);
350
+ addBundleMarkdownIssue(directory, "website-handoff.md", [
351
+ "# Website improvement handoff",
352
+ ], issues);
353
+ addBundleMarkdownIssue(directory, "website-prompts.md", [
354
+ "# Website improvement prompt bundle",
355
+ ], issues);
356
+ addBundleMarkdownIssue(directory, "codex-implementation.md", [
357
+ "# Codex implementation prompt",
358
+ "Task ID:",
359
+ "Work in the target website repository, not in this design-ai repository.",
360
+ ], issues);
361
+ }
362
+
363
+ if (issues.length === 0) {
364
+ addIssue(issues, "pass", "bundle-ready", "Handoff bundle is complete and internally consistent");
365
+ }
366
+
367
+ const status = statusFromIssues(issues);
368
+ const repairGuidance = buildBundleRepairGuidance(directory, generatedContract);
369
+ return {
370
+ directory,
371
+ valid: status !== "fail",
372
+ status,
373
+ counts: {
374
+ expectedFiles: SITE_BUNDLE_FILES.length,
375
+ presentFiles: files.filter((file) => file.present).length,
376
+ missingFiles: files.filter((file) => !file.present).length,
377
+ unexpectedFiles: unexpectedFiles.length,
378
+ expectedChecksumFiles: SITE_BUNDLE_CHECKSUM_FILES.length,
379
+ verifiedChecksumFiles: SITE_BUNDLE_CHECKSUM_FILES.filter((filePath) => {
380
+ const expectedDigest = summary.checksumFiles[filePath];
381
+ const targetPath = path.join(directory, filePath);
382
+ if (!expectedDigest || !/^[a-f0-9]{64}$/.test(String(expectedDigest))) return false;
383
+ if (!canReadDirectory || !existsSync(targetPath) || !statSync(targetPath).isFile()) return false;
384
+ return sha256Hex(readFileSync(targetPath, "utf8")) === expectedDigest;
385
+ }).length,
386
+ checksumFailures: issues.filter((issue) => issue.level === "fail" && issue.id.startsWith("bundle-checksum-")).length,
387
+ expectedGeneratedFiles: generatedContract.expectedFiles,
388
+ verifiedGeneratedFiles: generatedContract.verifiedFiles,
389
+ generatedFailures: issues.filter((issue) => issue.level === "fail" && issue.id.startsWith("bundle-generated-")).length,
390
+ issues: issues.length,
391
+ warnings: issues.filter((issue) => issue.level === "warn").length,
392
+ failures: issues.filter((issue) => issue.level === "fail").length,
393
+ },
394
+ summary,
395
+ workspaceStatus: workspaceSummary?.status || "unknown",
396
+ mcpStatus: mcpPayload?.status || "unknown",
397
+ mcpProbeStatus: mcpProbePayload?.status || "unknown",
398
+ mcpProbeCounts: {
399
+ count: Number.isInteger(mcpProbePayload?.count) && mcpProbePayload.count >= 0 ? mcpProbePayload.count : summary.mcpProbeCounts.count,
400
+ pass: Number.isInteger(mcpProbePayload?.pass) && mcpProbePayload.pass >= 0 ? mcpProbePayload.pass : summary.mcpProbeCounts.pass,
401
+ warn: Number.isInteger(mcpProbePayload?.warn) && mcpProbePayload.warn >= 0 ? mcpProbePayload.warn : summary.mcpProbeCounts.warn,
402
+ fail: Number.isInteger(mcpProbePayload?.fail) && mcpProbePayload.fail >= 0 ? mcpProbePayload.fail : summary.mcpProbeCounts.fail,
403
+ },
404
+ boundaries: boundarySummary.boundaries,
405
+ externalCalls: boundarySummary.externalCalls,
406
+ targetRepoMutation: boundarySummary.targetRepoMutation,
407
+ files,
408
+ unexpectedFiles,
409
+ generatedContract,
410
+ repairGuidance,
411
+ issues,
412
+ };
413
+ }
414
+
415
+ export function formatSiteBundleCheckJson(report) {
416
+ return JSON.stringify(report, null, 2);
417
+ }
418
+
419
+ export function formatSiteBundleCheckHuman(report) {
420
+ return [
421
+ `Website Improvement handoff bundle check: ${report.directory}`,
422
+ "",
423
+ `Status: ${report.status}`,
424
+ `Files: ${report.counts.presentFiles}/${report.counts.expectedFiles}`,
425
+ `Checksums: ${report.counts.verifiedChecksumFiles}/${report.counts.expectedChecksumFiles} verified`,
426
+ `Generated contract: ${report.counts.verifiedGeneratedFiles}/${report.counts.expectedGeneratedFiles} verified`,
427
+ `Bundle digest: ${report.summary.checksumBundleDigest || "not recorded"}`,
428
+ `Unexpected files: ${report.unexpectedFiles.length ? report.unexpectedFiles.join(", ") : "none"}`,
429
+ `Generated drift files: ${formatGeneratedContractDriftSummary(report.generatedContract)}`,
430
+ `Source: ${report.summary.source || "unknown"}`,
431
+ `Site: ${report.summary.siteName || "unknown"}`,
432
+ `Tasks: ${report.summary.totalTasks}`,
433
+ `Evidence: executed work ${report.summary.implementationEvidence.executedWork}, verification ${report.summary.implementationEvidence.verificationResults}, risks ${report.summary.implementationEvidence.remainingRisks}, next actions ${report.summary.implementationEvidence.nextActions}`,
434
+ `MCP status: ${report.mcpStatus}`,
435
+ `MCP probe status: ${report.mcpProbeStatus}`,
436
+ `MCP probes: ${report.mcpProbeCounts.pass}/${report.mcpProbeCounts.count} passing, ${report.mcpProbeCounts.warn} warning, ${report.mcpProbeCounts.fail} failing`,
437
+ `Boundary flags: external calls ${report.externalCalls ? "yes" : "no"}; target repo mutation ${report.targetRepoMutation ? "yes" : "no"}`,
438
+ "",
439
+ "Files:",
440
+ ...report.files.map((file) => `- [${file.present ? "pass" : "fail"}] ${file.path}`),
441
+ "",
442
+ "Generated contract drift:",
443
+ ...formatGeneratedContractDriftLines(report.generatedContract),
444
+ "",
445
+ "Repair guidance:",
446
+ ...formatBundleRepairGuidanceLines(report.repairGuidance),
447
+ "",
448
+ "Bundle boundaries:",
449
+ ...(report.boundaries.length ? report.boundaries.map((boundary) => `- ${boundary}`) : ["- none recorded"]),
450
+ "",
451
+ "Issues:",
452
+ ...report.issues.map((issue) => `- [${issue.level}] ${issue.id}: ${issue.message}`),
453
+ ].join("\n");
454
+ }
@@ -0,0 +1,95 @@
1
+ // Command and safety helpers for Website Improvement handoff bundles.
2
+
3
+ export function shellQuote(value) {
4
+ const text = String(value || "");
5
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(text)) return text;
6
+ return `'${text.replaceAll("'", "'\"'\"'")}'`;
7
+ }
8
+
9
+ export function commandFromArgs(args) {
10
+ return args.map((arg) => shellQuote(arg)).join(" ");
11
+ }
12
+
13
+ export function taskHandoffOutFile(task) {
14
+ return `target-repo-${task.id}-handoff.md`;
15
+ }
16
+
17
+ export function buildBundleTaskHandoffCommandArgs(directory, task, { strict = false } = {}) {
18
+ const args = [
19
+ "design-ai",
20
+ "site",
21
+ String(directory || ""),
22
+ "--bundle-handoff",
23
+ "--task",
24
+ String(task.id || ""),
25
+ ];
26
+ if (strict) args.push("--strict");
27
+ args.push("--out", taskHandoffOutFile(task));
28
+ return args;
29
+ }
30
+
31
+ export function buildBundleTaskHandoffCommand(directory, task, options = {}) {
32
+ return commandFromArgs(buildBundleTaskHandoffCommandArgs(directory, task, options));
33
+ }
34
+
35
+ export function buildBundleTaskHandoffCommandSafety(task, { strict = false } = {}) {
36
+ return {
37
+ runPolicy: "writes-local-file",
38
+ safetyLevel: "local-output-file",
39
+ writesLocalFile: true,
40
+ outputFile: taskHandoffOutFile(task),
41
+ mutates: "local-output-file-only",
42
+ externalCalls: false,
43
+ targetRepoMutation: false,
44
+ requiresCleanWorkspace: false,
45
+ requiresReviewBeforeMutation: false,
46
+ strict: Boolean(strict),
47
+ };
48
+ }
49
+
50
+ export function buildBundleSourceCommandSafety({ strict = false } = {}) {
51
+ return {
52
+ runPolicy: "read-only",
53
+ safetyLevel: "local-read-only",
54
+ writesLocalFile: false,
55
+ outputFile: "",
56
+ mutates: "none",
57
+ externalCalls: false,
58
+ targetRepoMutation: false,
59
+ requiresCleanWorkspace: false,
60
+ requiresReviewBeforeMutation: false,
61
+ strict: Boolean(strict),
62
+ };
63
+ }
64
+
65
+ export function buildBundleCheckCommandArgs(directory, { strict = false } = {}) {
66
+ const args = [
67
+ "design-ai",
68
+ "site",
69
+ String(directory || ""),
70
+ "--bundle-check",
71
+ ];
72
+ if (strict) args.push("--strict");
73
+ args.push("--json");
74
+ return args;
75
+ }
76
+
77
+ export function buildBundleCheckCommand(directory, options = {}) {
78
+ return commandFromArgs(buildBundleCheckCommandArgs(directory, options));
79
+ }
80
+
81
+ export function buildBundleHandoffCommandArgs(directory, { strict = false } = {}) {
82
+ const args = [
83
+ "design-ai",
84
+ "site",
85
+ String(directory || ""),
86
+ "--bundle-handoff",
87
+ ];
88
+ if (strict) args.push("--strict");
89
+ args.push("--json");
90
+ return args;
91
+ }
92
+
93
+ export function buildBundleHandoffCommand(directory, options = {}) {
94
+ return commandFromArgs(buildBundleHandoffCommandArgs(directory, options));
95
+ }