coding-agent-skills 0.2.14 → 0.2.15

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 (48) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +5 -1
  3. package/ROADMAP.md +4 -2
  4. package/bin/coding-agent-skills +7 -0
  5. package/docs/adapters/README.md +21 -0
  6. package/docs/adapters/project-installation.md +14 -0
  7. package/docs/adapters/real-project-adoption.md +2 -2
  8. package/docs/architecture/README.md +3 -2
  9. package/docs/release/README.md +1 -1
  10. package/docs/release/npm-package.md +7 -2
  11. package/docs/safety/README.md +6 -1
  12. package/docs/testing/README.md +8 -0
  13. package/docs/usage/README.md +15 -5
  14. package/examples/command-policies/deployment-preflight.json +70 -0
  15. package/examples/evidence-packs/deployment-preflight.json +60 -0
  16. package/examples/manifests/deployment-preflight.json +14 -0
  17. package/examples/workflows/deployment-preflight.md +8 -0
  18. package/package.json +2 -1
  19. package/runs/skill-runs.md +19 -0
  20. package/schemas/project-adapter-installation.schema.json +2 -0
  21. package/schemas/project-adapter.schema.json +2 -0
  22. package/scripts/lib/deployment-preflight.mjs +655 -0
  23. package/scripts/lib/pack-rules.mjs +11 -2
  24. package/scripts/render-deployment-preflight.mjs +9 -0
  25. package/scripts/test-pack.mjs +60 -1
  26. package/scripts/validate-pack.mjs +5 -2
  27. package/skills/deployment-preflight/SKILL.md +89 -0
  28. package/skills/deployment-preflight/adapter-interface.md +17 -0
  29. package/skills/deployment-preflight/agents/openai.yaml +3 -0
  30. package/skills/deployment-preflight/checklist.md +7 -0
  31. package/skills/deployment-preflight/evidence-template.md +19 -0
  32. package/skills/deployment-preflight/examples.md +11 -0
  33. package/skills/deployment-preflight/failure-modes.md +11 -0
  34. package/tests/fixtures/deployment-preflight/adapter-project/.coding-agent/adapters/deployment-preflight-fixture/adapter.json +56 -0
  35. package/tests/fixtures/deployment-preflight/adapter-project/.coding-agent/skills.json +23 -0
  36. package/tests/fixtures/deployment-preflight/adapter-project/README.md +3 -0
  37. package/tests/fixtures/deployment-preflight/adapter-project/deploy/netlify.toml +3 -0
  38. package/tests/fixtures/deployment-preflight/adapter-project/ignored/render.yaml +3 -0
  39. package/tests/fixtures/deployment-preflight/adapter-project/package.json +5 -0
  40. package/tests/fixtures/deployment-preflight/static-project/Dockerfile +2 -0
  41. package/tests/fixtures/deployment-preflight/static-project/README.md +3 -0
  42. package/tests/fixtures/deployment-preflight/static-project/docs/deployment.md +4 -0
  43. package/tests/fixtures/deployment-preflight/static-project/package.json +6 -0
  44. package/tests/fixtures/deployment-preflight/static-project/src/index.js +1 -0
  45. package/tests/fixtures/deployment-preflight/static-project/wrangler.toml +3 -0
  46. package/tests/fixtures/triggers/cases.json +13 -1
  47. package/tests/trigger/README.md +2 -0
  48. package/work-ledger.md +18 -6
@@ -0,0 +1,655 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import {
7
+ ADAPTER_MANIFEST_FILENAME,
8
+ readSafeJsonFile,
9
+ } from "./adapter-discovery.mjs";
10
+ import { PILOT_VERSION } from "./pack-rules.mjs";
11
+ import {
12
+ readProjectAdapterDeclaration,
13
+ validateProjectAdapters,
14
+ } from "./project-adapter-installation.mjs";
15
+
16
+ const DEFAULT_CORE_ROOT = path.resolve(
17
+ path.dirname(fileURLToPath(import.meta.url)),
18
+ "..",
19
+ "..",
20
+ );
21
+
22
+ const SKILL_ID = "deployment-preflight";
23
+
24
+ const DEFAULT_IGNORED_PATHS = [
25
+ ".git",
26
+ ".next",
27
+ "node_modules",
28
+ "dist",
29
+ "build",
30
+ "coverage",
31
+ "out",
32
+ "validation-output",
33
+ ];
34
+
35
+ const TEXT_EXTENSIONS = new Set([
36
+ ".cjs",
37
+ ".cts",
38
+ ".dockerfile",
39
+ ".js",
40
+ ".json",
41
+ ".jsonc",
42
+ ".jsx",
43
+ ".md",
44
+ ".mjs",
45
+ ".mts",
46
+ ".toml",
47
+ ".ts",
48
+ ".tsx",
49
+ ".txt",
50
+ ".yaml",
51
+ ".yml",
52
+ ]);
53
+
54
+ const DEPLOYMENT_CONFIG_BASENAMES = new Set([
55
+ "Dockerfile",
56
+ "Procfile",
57
+ "app.yaml",
58
+ "app.yml",
59
+ "amplify.yml",
60
+ "cloudbuild.yaml",
61
+ "cloudbuild.yml",
62
+ "docker-compose.yaml",
63
+ "docker-compose.yml",
64
+ "firebase.json",
65
+ "fly.toml",
66
+ "netlify.toml",
67
+ "railway.json",
68
+ "railway.toml",
69
+ "render.yaml",
70
+ "render.yml",
71
+ "sst.config.js",
72
+ "sst.config.mjs",
73
+ "sst.config.ts",
74
+ "vercel.json",
75
+ "wrangler.json",
76
+ "wrangler.jsonc",
77
+ "wrangler.toml",
78
+ ]);
79
+
80
+ const DEPLOYMENT_TERMS = [
81
+ ["cloudflare", /\b(?:cloudflare|wrangler|pages_build_output_dir|compatibility_date)\b/i],
82
+ ["vercel", /\b(?:vercel|buildCommand|outputDirectory|rewrites|redirects)\b/i],
83
+ ["netlify", /\b(?:netlify|publish\s*=|functions\s*=|\[\[redirects\]\])\b/i],
84
+ ["docker", /\b(?:FROM\s+\S+|docker-compose|services:|image:)\b/im],
85
+ ["fly", /\b(?:app\s*=|primary_region|fly\.io)\b/i],
86
+ ["railway", /\b(?:railway|startCommand|buildCommand)\b/i],
87
+ ["render", /\b(?:render|services:|envVars:)\b/i],
88
+ ["firebase", /\b(?:firebase|hosting|functions)\b/i],
89
+ ["generic-ci-deploy", /\b(?:deploy|deployment|release|production)\b/i],
90
+ ];
91
+
92
+ const REFUSED_BEHAVIOR = [
93
+ "no deployments",
94
+ "no cloud provider API calls",
95
+ "no package installs",
96
+ "no target project builds",
97
+ "no target project tests",
98
+ "no runtime checks",
99
+ "no service or process mutation",
100
+ "no database connections",
101
+ "no migrations",
102
+ "no secret-file reads",
103
+ "no project writes",
104
+ ];
105
+
106
+ const NOT_VERIFIED = [
107
+ "provider authentication",
108
+ "cloud project permissions",
109
+ "deployed service state",
110
+ "DNS or domain configuration",
111
+ "environment variable values",
112
+ "build output correctness",
113
+ "CI/CD job results",
114
+ "runtime health checks",
115
+ "rollback behavior",
116
+ ];
117
+
118
+ function inside(root, candidate) {
119
+ const relative = path.relative(root, candidate);
120
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
121
+ }
122
+
123
+ function toPosix(relativePath) {
124
+ return relativePath.split(path.sep).join("/");
125
+ }
126
+
127
+ function safeRelativePath(candidate) {
128
+ return (
129
+ typeof candidate === "string" &&
130
+ candidate.length > 0 &&
131
+ !candidate.startsWith("/") &&
132
+ !candidate.split(/[\\/]+/).includes("..")
133
+ );
134
+ }
135
+
136
+ function secretBearingPath(relativePath) {
137
+ const normalized = toPosix(relativePath);
138
+ const basename = path.posix.basename(normalized);
139
+ return (
140
+ basename === ".env" ||
141
+ basename.startsWith(".env.") ||
142
+ basename === ".npmrc" ||
143
+ /\.(?:pem|key|p12|pfx)$/i.test(basename) ||
144
+ /(?:^|\/)(?:secrets?|credentials?|private-key|service-role|tokens?)(?:\/|$)/i.test(normalized)
145
+ );
146
+ }
147
+
148
+ function ignoredBy(relativePath, ignoredPaths) {
149
+ const normalized = toPosix(relativePath);
150
+ return ignoredPaths.some((ignored) => {
151
+ const clean = toPosix(ignored).replace(/\/+$/g, "");
152
+ return normalized === clean || normalized.startsWith(`${clean}/`);
153
+ });
154
+ }
155
+
156
+ function gitSummary(projectRoot) {
157
+ const summary = {
158
+ root: null,
159
+ branchState: null,
160
+ hasUncommittedChanges: false,
161
+ warnings: [],
162
+ };
163
+ const revParse = spawnSync("git", ["rev-parse", "--show-toplevel"], {
164
+ cwd: projectRoot,
165
+ encoding: "utf8",
166
+ stdio: ["ignore", "pipe", "pipe"],
167
+ });
168
+ if (revParse.status === 0) summary.root = revParse.stdout.trim();
169
+ const status = spawnSync("git", ["status", "--short", "--branch"], {
170
+ cwd: projectRoot,
171
+ encoding: "utf8",
172
+ stdio: ["ignore", "pipe", "pipe"],
173
+ });
174
+ if (status.status !== 0) {
175
+ summary.warnings.push("git status unavailable");
176
+ return summary;
177
+ }
178
+ const lines = status.stdout.split(/\r?\n/).filter(Boolean);
179
+ summary.branchState = lines[0] ?? null;
180
+ summary.hasUncommittedChanges = lines.length > 1;
181
+ if (summary.hasUncommittedChanges) summary.warnings.push("working tree has local changes; filenames omitted");
182
+ if (/\[(?:ahead|behind|gone|diverged)[^\]]*\]/i.test(summary.branchState ?? "")) {
183
+ summary.warnings.push("branch state indicates remote divergence; revalidate after update");
184
+ }
185
+ return summary;
186
+ }
187
+
188
+ function adapterContext(projectRootInput, coreRoot) {
189
+ const loaded = readProjectAdapterDeclaration(projectRootInput);
190
+ if (!loaded.ok) {
191
+ if (loaded.codes.length === 1 && loaded.codes[0] === "missing-project-declaration") {
192
+ return {
193
+ ok: true,
194
+ present: false,
195
+ enabled: false,
196
+ projectRoot: path.resolve(projectRootInput),
197
+ mode: "none",
198
+ codes: [],
199
+ };
200
+ }
201
+ return { ok: false, present: false, enabled: false, status: "failed", codes: loaded.codes };
202
+ }
203
+
204
+ const validation = validateProjectAdapters(loaded.projectRoot, { coreRoot });
205
+ if (!validation.ok) {
206
+ return {
207
+ ok: false,
208
+ present: true,
209
+ enabled: false,
210
+ status: "failed",
211
+ codes: validation.codes,
212
+ validation,
213
+ };
214
+ }
215
+
216
+ if (!validation.acceptedSkills.includes(SKILL_ID)) {
217
+ return {
218
+ ok: true,
219
+ present: true,
220
+ enabled: false,
221
+ projectRoot: loaded.projectRoot,
222
+ mode: "adapter-present-deployment-preflight-not-enabled",
223
+ declarationPath: loaded.declarationPath,
224
+ declaration: loaded.declaration,
225
+ validation,
226
+ codes: [`${SKILL_ID}-not-enabled-by-adapter`],
227
+ };
228
+ }
229
+
230
+ const adapters = [];
231
+ const errors = [];
232
+ const container = path.resolve(loaded.projectRoot, loaded.declaration.adapterRoot);
233
+ if (!inside(loaded.projectRoot, container) || !fs.existsSync(container)) {
234
+ errors.push("adapter-root-not-found");
235
+ } else {
236
+ for (const declaration of loaded.declaration.adapters ?? []) {
237
+ if (!(declaration.skillIds ?? []).includes(SKILL_ID)) continue;
238
+ const manifestPath = path.join(container, declaration.id, ADAPTER_MANIFEST_FILENAME);
239
+ const record = readSafeJsonFile(manifestPath);
240
+ if (!record.value) {
241
+ errors.push(...record.codes);
242
+ continue;
243
+ }
244
+ adapters.push({
245
+ declaration,
246
+ manifestPath: path.relative(loaded.projectRoot, manifestPath),
247
+ manifest: record.value,
248
+ });
249
+ }
250
+ }
251
+ if (errors.length) {
252
+ return { ok: false, present: true, enabled: false, status: "failed", codes: errors, validation };
253
+ }
254
+ return {
255
+ ok: true,
256
+ present: true,
257
+ enabled: true,
258
+ projectRoot: loaded.projectRoot,
259
+ mode: "adapter-enabled",
260
+ declarationPath: loaded.declarationPath,
261
+ declaration: loaded.declaration,
262
+ validation,
263
+ adapters,
264
+ codes: [],
265
+ };
266
+ }
267
+
268
+ function scopeFromAdapter(context) {
269
+ const safeReadPaths = new Set();
270
+ const ignoredPaths = new Set(DEFAULT_IGNORED_PATHS);
271
+ for (const adapter of context.adapters ?? []) {
272
+ for (const candidate of adapter.manifest.extensions?.safeReadPaths ?? []) {
273
+ if (safeRelativePath(candidate)) safeReadPaths.add(candidate);
274
+ }
275
+ for (const candidate of adapter.manifest.extensions?.ignoredPaths ?? []) {
276
+ if (safeRelativePath(candidate)) ignoredPaths.add(candidate);
277
+ }
278
+ }
279
+ return {
280
+ scopePaths: safeReadPaths.size ? [...safeReadPaths].sort() : ["."],
281
+ ignoredPaths: [...ignoredPaths].sort(),
282
+ };
283
+ }
284
+
285
+ function candidateFile(relativePath) {
286
+ const normalized = toPosix(relativePath);
287
+ const basename = path.posix.basename(normalized);
288
+ const lowerBasename = basename.toLowerCase();
289
+ return (
290
+ TEXT_EXTENSIONS.has(path.extname(lowerBasename)) ||
291
+ DEPLOYMENT_CONFIG_BASENAMES.has(basename) ||
292
+ DEPLOYMENT_CONFIG_BASENAMES.has(lowerBasename) ||
293
+ /(?:^|\/)(?:deploy|deployment|hosting|docker|k8s|kubernetes|helm|terraform|infra|ops|ci|workflows)(?:\/|$)/i.test(normalized)
294
+ );
295
+ }
296
+
297
+ function collectFiles(projectRoot, scopePaths, ignoredPaths) {
298
+ const files = [];
299
+ const skipped = [];
300
+ for (const scopePath of scopePaths) {
301
+ if (!safeRelativePath(scopePath) && scopePath !== ".") {
302
+ skipped.push({ path: scopePath, reason: "unsafe scope path" });
303
+ continue;
304
+ }
305
+ const absolute = path.resolve(projectRoot, scopePath);
306
+ if (!inside(projectRoot, absolute)) {
307
+ skipped.push({ path: scopePath, reason: "scope escapes project root" });
308
+ continue;
309
+ }
310
+ if (!fs.existsSync(absolute)) {
311
+ skipped.push({ path: scopePath, reason: "scope path not found" });
312
+ continue;
313
+ }
314
+ walkPath(projectRoot, absolute, ignoredPaths, files, skipped);
315
+ }
316
+ return { files: [...new Set(files)].sort(), skipped };
317
+ }
318
+
319
+ function walkPath(projectRoot, absolute, ignoredPaths, files, skipped) {
320
+ const relative = toPosix(path.relative(projectRoot, absolute)) || ".";
321
+ if (relative !== "." && ignoredBy(relative, ignoredPaths)) {
322
+ skipped.push({ path: relative, reason: "ignored path" });
323
+ return;
324
+ }
325
+ if (relative !== "." && secretBearingPath(relative)) {
326
+ skipped.push({ path: relative, reason: "secret-bearing path excluded" });
327
+ return;
328
+ }
329
+ const stat = fs.lstatSync(absolute);
330
+ if (stat.isSymbolicLink()) {
331
+ skipped.push({ path: relative, reason: "symbolic link skipped" });
332
+ return;
333
+ }
334
+ if (stat.isDirectory()) {
335
+ for (const entry of fs.readdirSync(absolute)) {
336
+ walkPath(projectRoot, path.join(absolute, entry), ignoredPaths, files, skipped);
337
+ }
338
+ return;
339
+ }
340
+ if (!stat.isFile()) return;
341
+ if (stat.size > 512_000) {
342
+ skipped.push({ path: relative, reason: "file larger than bounded read limit" });
343
+ return;
344
+ }
345
+ if (!candidateFile(relative)) {
346
+ skipped.push({ path: relative, reason: "not a deployment-preflight candidate file" });
347
+ return;
348
+ }
349
+ files.push(relative);
350
+ }
351
+
352
+ function detectDeploymentConfig(relativePath, text) {
353
+ const normalized = toPosix(relativePath);
354
+ const basename = path.posix.basename(normalized);
355
+ const lowerBasename = basename.toLowerCase();
356
+ const reasons = [];
357
+ if (DEPLOYMENT_CONFIG_BASENAMES.has(basename) || DEPLOYMENT_CONFIG_BASENAMES.has(lowerBasename)) {
358
+ reasons.push("deployment-config-filename");
359
+ }
360
+ if (/(?:^|\/)\.github\/workflows\/[^/]+\.ya?ml$/i.test(normalized)) {
361
+ reasons.push("github-actions-workflow");
362
+ }
363
+ if (/(?:^|\/)(?:Dockerfile|docker-compose\.ya?ml)$/i.test(normalized)) {
364
+ reasons.push("container-deployment-config");
365
+ }
366
+ for (const [platform, pattern] of DEPLOYMENT_TERMS) {
367
+ if (pattern.test(text)) reasons.push(`${platform}-indicator`);
368
+ }
369
+ return [...new Set(reasons)].sort();
370
+ }
371
+
372
+ function detectDeploymentDoc(relativePath, text) {
373
+ const normalized = toPosix(relativePath);
374
+ const reasons = [];
375
+ if (/\b(?:deploy|deployment|release|hosting|rollback|production)\b/i.test(text)) {
376
+ reasons.push("deployment-wording");
377
+ }
378
+ if (/(?:^|\/)(?:deploy|deployment|release|hosting|ops|runbook)(?:\/|\.|$)/i.test(normalized)) {
379
+ reasons.push("deployment-doc-path");
380
+ }
381
+ return [...new Set(reasons)].sort();
382
+ }
383
+
384
+ function detectPackageScriptKeys(relativePath, text) {
385
+ if (path.posix.basename(toPosix(relativePath)) !== "package.json") return [];
386
+ try {
387
+ const parsed = JSON.parse(text);
388
+ const scripts = parsed && typeof parsed === "object" ? parsed.scripts : null;
389
+ if (!scripts || typeof scripts !== "object") return [];
390
+ return Object.entries(scripts)
391
+ .filter(([key, value]) =>
392
+ /\b(?:deploy|release|publish|preview|wrangler|vercel|netlify|fly|railway|render|firebase|sst|docker)\b/i.test(
393
+ `${key} ${value}`,
394
+ ),
395
+ )
396
+ .map(([key]) => ({ path: relativePath, key }));
397
+ } catch {
398
+ return [{ path: relativePath, key: "unparseable-package-json" }];
399
+ }
400
+ }
401
+
402
+ function detectPlatformIndicators(relativePath, text) {
403
+ const indicators = [];
404
+ for (const [platform, pattern] of DEPLOYMENT_TERMS) {
405
+ if (pattern.test(text)) indicators.push({ path: relativePath, platform });
406
+ }
407
+ return indicators;
408
+ }
409
+
410
+ function detectRiskIndicators(relativePath, text) {
411
+ const indicators = [];
412
+ const checks = [
413
+ ["deploy-command-reference", /\b(?:deploy|publish|release)\b/i],
414
+ ["production-reference", /\b(?:production|prod|live)\b/i],
415
+ ["force-or-unsafe-flag", /\b(?:--force|--yes|-y|--confirm|--unsafe)\b/i],
416
+ ["secret-like-setting-name", /\b(?:token|secret|password|private[_-]?key|api[_-]?key|service[_-]?role)\b/i],
417
+ ["environment-value-reference", /\b(?:process\.env|import\.meta\.env|\$\{?[A-Z][A-Z0-9_]{2,}\}?|envVars)\b/i],
418
+ ];
419
+ for (const [type, pattern] of checks) {
420
+ if (pattern.test(text)) indicators.push({ path: relativePath, type });
421
+ }
422
+ return indicators;
423
+ }
424
+
425
+ function scanDeploymentPreflight(projectRoot, files) {
426
+ const configFiles = [];
427
+ const deploymentDocs = [];
428
+ const packageScriptKeys = [];
429
+ const platformIndicators = [];
430
+ const riskIndicators = [];
431
+ const warnings = [];
432
+
433
+ for (const relative of files) {
434
+ let text = "";
435
+ try {
436
+ text = fs.readFileSync(path.join(projectRoot, relative), "utf8");
437
+ } catch {
438
+ warnings.push(`could not read ${relative}`);
439
+ continue;
440
+ }
441
+
442
+ const configReasons = detectDeploymentConfig(relative, text);
443
+ if (configReasons.length) {
444
+ configFiles.push({ path: relative, reasons: configReasons });
445
+ }
446
+ const docReasons = detectDeploymentDoc(relative, text);
447
+ if (docReasons.length) {
448
+ deploymentDocs.push({ path: relative, reasons: docReasons });
449
+ }
450
+ packageScriptKeys.push(...detectPackageScriptKeys(relative, text));
451
+ platformIndicators.push(...detectPlatformIndicators(relative, text));
452
+ riskIndicators.push(...detectRiskIndicators(relative, text));
453
+ }
454
+
455
+ const byPath = (left, right) =>
456
+ `${left.path}:${left.type ?? left.key ?? left.platform ?? ""}`.localeCompare(
457
+ `${right.path}:${right.type ?? right.key ?? right.platform ?? ""}`,
458
+ );
459
+
460
+ return {
461
+ configFiles: configFiles.sort(byPath),
462
+ deploymentDocs: deploymentDocs.sort(byPath),
463
+ packageScriptKeys: packageScriptKeys.sort(byPath),
464
+ platformIndicators: platformIndicators.sort(byPath),
465
+ riskIndicators: riskIndicators.sort(byPath),
466
+ warnings,
467
+ };
468
+ }
469
+
470
+ export function buildDeploymentPreflightReport(projectRootInput, options = {}) {
471
+ const coreRoot = options.coreRoot ?? DEFAULT_CORE_ROOT;
472
+ const context = adapterContext(projectRootInput, coreRoot);
473
+ if (!context.ok) {
474
+ return {
475
+ status: "failed",
476
+ coreVersion: PILOT_VERSION,
477
+ projectRoot: path.resolve(projectRootInput ?? "."),
478
+ adapter: context,
479
+ git: gitSummary(path.resolve(projectRootInput ?? ".")),
480
+ scopePaths: [],
481
+ ignoredPaths: DEFAULT_IGNORED_PATHS,
482
+ filesScanned: [],
483
+ configFiles: [],
484
+ deploymentDocs: [],
485
+ packageScriptKeys: [],
486
+ platformIndicators: [],
487
+ riskIndicators: [],
488
+ skipped: [],
489
+ warnings: context.codes ?? [],
490
+ notVerified: NOT_VERIFIED,
491
+ refusedBehavior: REFUSED_BEHAVIOR,
492
+ };
493
+ }
494
+
495
+ const projectRoot = context.projectRoot;
496
+ const git = gitSummary(projectRoot);
497
+ if (context.present && !context.enabled) {
498
+ return {
499
+ status: "partial",
500
+ coreVersion: PILOT_VERSION,
501
+ projectRoot,
502
+ adapter: context,
503
+ git,
504
+ scopePaths: [],
505
+ ignoredPaths: DEFAULT_IGNORED_PATHS,
506
+ filesScanned: [],
507
+ configFiles: [],
508
+ deploymentDocs: [],
509
+ packageScriptKeys: [],
510
+ platformIndicators: [],
511
+ riskIndicators: [],
512
+ skipped: [{ path: ".", reason: "project adapter is present but does not enable deployment-preflight" }],
513
+ warnings: [
514
+ "deployment-preflight is not enabled by the project adapter; target files were not read",
515
+ ...git.warnings,
516
+ ],
517
+ notVerified: NOT_VERIFIED,
518
+ refusedBehavior: REFUSED_BEHAVIOR,
519
+ };
520
+ }
521
+
522
+ const scope = context.enabled
523
+ ? scopeFromAdapter(context)
524
+ : { scopePaths: ["."], ignoredPaths: DEFAULT_IGNORED_PATHS };
525
+ const collected = collectFiles(projectRoot, scope.scopePaths, scope.ignoredPaths);
526
+ const scanned = scanDeploymentPreflight(projectRoot, collected.files);
527
+ return {
528
+ status: "complete",
529
+ coreVersion: PILOT_VERSION,
530
+ projectRoot,
531
+ adapter: context,
532
+ git,
533
+ scopePaths: scope.scopePaths,
534
+ ignoredPaths: scope.ignoredPaths,
535
+ filesScanned: collected.files,
536
+ configFiles: scanned.configFiles,
537
+ deploymentDocs: scanned.deploymentDocs,
538
+ packageScriptKeys: scanned.packageScriptKeys,
539
+ platformIndicators: scanned.platformIndicators,
540
+ riskIndicators: scanned.riskIndicators,
541
+ skipped: collected.skipped,
542
+ warnings: [
543
+ ...git.warnings,
544
+ ...scanned.warnings,
545
+ ...(context.enabled ? ["deployment-preflight used adapter-declared safe read paths only"] : []),
546
+ ...(context.present ? [] : ["no project adapter declaration found; deployment-preflight used generic bounded static scan"]),
547
+ ],
548
+ notVerified: NOT_VERIFIED,
549
+ refusedBehavior: REFUSED_BEHAVIOR,
550
+ };
551
+ }
552
+
553
+ function renderRecords(records, formatter, empty = "- none found") {
554
+ if (!records.length) return [empty];
555
+ return records.slice(0, 80).map(formatter).concat(
556
+ records.length > 80 ? [`- ${records.length - 80} additional records omitted`] : [],
557
+ );
558
+ }
559
+
560
+ export function renderDeploymentPreflightReport(report) {
561
+ const lines = [
562
+ "# Deployment Preflight Report",
563
+ "",
564
+ `Status: ${report.status}`,
565
+ `Core version: ${report.coreVersion}`,
566
+ `Project root: ${report.projectRoot}`,
567
+ "",
568
+ "## Git State",
569
+ `- Git root: ${report.git.root ?? "not detected"}`,
570
+ `- Branch state: ${report.git.branchState ?? "not detected"}`,
571
+ `- Local changes: ${report.git.hasUncommittedChanges ? "detected" : "not detected"}`,
572
+ "",
573
+ "## Adapter Scope",
574
+ `- Adapter present: ${report.adapter.present ? "yes" : "no"}`,
575
+ `- Deployment-preflight enabled: ${report.adapter.enabled ? "yes" : "no"}`,
576
+ `- Mode: ${report.adapter.mode}`,
577
+ "",
578
+ "## Scope Paths",
579
+ ...(report.scopePaths.length ? report.scopePaths.map((item) => `- ${item}`) : ["- none"]),
580
+ "",
581
+ "## Ignored Paths",
582
+ ...report.ignoredPaths.map((item) => `- ${item}`),
583
+ "",
584
+ "## Summary",
585
+ `- Static files scanned: ${report.filesScanned.length}`,
586
+ `- Deployment config files: ${report.configFiles.length}`,
587
+ `- Deployment docs: ${report.deploymentDocs.length}`,
588
+ `- Package script keys mentioning deployment: ${report.packageScriptKeys.length}`,
589
+ `- Platform indicators: ${report.platformIndicators.length}`,
590
+ `- Risk indicators: ${report.riskIndicators.length}`,
591
+ `- Skipped items: ${report.skipped.length}`,
592
+ "",
593
+ "## Deployment Config Files",
594
+ ...renderRecords(report.configFiles, (record) => `- ${record.path}: ${record.reasons.join(", ")}`),
595
+ "",
596
+ "## Deployment Docs",
597
+ ...renderRecords(report.deploymentDocs, (record) => `- ${record.path}: ${record.reasons.join(", ")}`),
598
+ "",
599
+ "## Package Script Keys",
600
+ ...renderRecords(
601
+ report.packageScriptKeys,
602
+ (record) => `- ${record.path}: ${record.key}`,
603
+ "- none found; command values are not printed",
604
+ ),
605
+ "",
606
+ "## Platform Indicators",
607
+ ...renderRecords(report.platformIndicators, (record) => `- ${record.path}: ${record.platform}`),
608
+ "",
609
+ "## Risk Indicators",
610
+ ...renderRecords(report.riskIndicators, (record) => `- ${record.path}: ${record.type}`),
611
+ "",
612
+ "## Skipped",
613
+ ];
614
+ if (report.skipped.length) {
615
+ for (const skipped of report.skipped.slice(0, 40)) {
616
+ lines.push(`- ${skipped.path}: ${skipped.reason}`);
617
+ }
618
+ if (report.skipped.length > 40) lines.push(`- ${report.skipped.length - 40} additional skipped items omitted`);
619
+ } else {
620
+ lines.push("- none");
621
+ }
622
+
623
+ lines.push("", "## Not Verified");
624
+ for (const item of report.notVerified) lines.push(`- ${item}`);
625
+ lines.push("", "## Warnings");
626
+ if (report.warnings.length) {
627
+ for (const warning of report.warnings) lines.push(`- ${warning}`);
628
+ } else {
629
+ lines.push("- none");
630
+ }
631
+ lines.push("", "## Refused Behavior");
632
+ for (const item of report.refusedBehavior) lines.push(`- ${item}`);
633
+ lines.push(
634
+ "",
635
+ "No deployment, cloud provider API call, package installation, target project build, test, runtime check, service mutation, database connection, migration, secret-file read, or project write was performed.",
636
+ );
637
+ return lines.join("\n");
638
+ }
639
+
640
+ export function deploymentPreflightCliResult(projectRootInput, options = {}) {
641
+ if (!projectRootInput) {
642
+ return {
643
+ exitCode: 2,
644
+ stream: "stderr",
645
+ lines: ["usage: node scripts/render-deployment-preflight.mjs <project-root>"],
646
+ };
647
+ }
648
+ const report = buildDeploymentPreflightReport(projectRootInput, options);
649
+ return {
650
+ exitCode: report.status === "failed" ? 1 : 0,
651
+ stream: report.status === "failed" ? "stderr" : "stdout",
652
+ lines: renderDeploymentPreflightReport(report).split("\n"),
653
+ report,
654
+ };
655
+ }
@@ -6,6 +6,7 @@ export const PILOT_SKILLS = [
6
6
  "api-contract-audit",
7
7
  "migration-review",
8
8
  "github-handoff",
9
+ "deployment-preflight",
9
10
  "build-verify",
10
11
  "git-preflight",
11
12
  "runtime-truth",
@@ -23,6 +24,7 @@ export const AUDIT_ONLY_SKILLS = [
23
24
  "api-contract-audit",
24
25
  "migration-review",
25
26
  "github-handoff",
27
+ "deployment-preflight",
26
28
  "git-preflight",
27
29
  "runtime-truth",
28
30
  "llm-drift-control",
@@ -339,6 +341,13 @@ export function adapterIssues(adapter, options = {}) {
339
341
  export function classifyTrigger(prompt) {
340
342
  const text = prompt.toLowerCase();
341
343
 
344
+ if (
345
+ /\b(?:deployment preflight|deploy preflight|pre-deploy audit|deployment readiness|deployment surface|deploy surface|static deployment evidence)\b/.test(
346
+ text,
347
+ )
348
+ ) {
349
+ return "deployment-preflight";
350
+ }
342
351
  if (
343
352
  /\b(?:deploy|install|update the lockfile|commit these|publish the branch|restart|enable it|rewrite the documentation)\b/.test(
344
353
  text,
@@ -557,7 +566,7 @@ function classifySegment(segment, options = {}) {
557
566
  }
558
567
  if (
559
568
  executable === "node" &&
560
- !/^node\s+(?:--check\b|--test\b|scripts\/(?:validate-pack|validate-maintainer-loop|validate-adapters|validate-project-adapters|check-adapter-upgrade|check-adapter-upgrade-chain|verify-evidence-bundle|render-evidence-archive-report|render-adapter-repo-map|render-route-trace|render-env-audit|render-secret-audit|render-api-contract-audit|render-migration-review|render-github-handoff|test-pack)\.mjs\b)/.test(
569
+ !/^node\s+(?:--check\b|--test\b|scripts\/(?:validate-pack|validate-maintainer-loop|validate-adapters|validate-project-adapters|check-adapter-upgrade|check-adapter-upgrade-chain|verify-evidence-bundle|render-evidence-archive-report|render-adapter-repo-map|render-route-trace|render-env-audit|render-secret-audit|render-api-contract-audit|render-migration-review|render-github-handoff|render-deployment-preflight|test-pack)\.mjs\b)/.test(
561
570
  segment,
562
571
  )
563
572
  ) {
@@ -567,7 +576,7 @@ function classifySegment(segment, options = {}) {
567
576
  ["coding-agent-skills", "bin/coding-agent-skills", "./bin/coding-agent-skills"].includes(
568
577
  executable,
569
578
  ) &&
570
- !/^(?:\.\/)?(?:bin\/)?coding-agent-skills\s+(?:validate-pack|validate-project\s+\S+|repo-map\s+\S+|route-trace\s+\S+|env-audit\s+\S+|secret-audit\s+\S+|api-contract-audit\s+\S+|migration-review\s+\S+|github-handoff\s+\S+|validate-adapters\s+\S+|help|--help|-h)\s*$/.test(
579
+ !/^(?:\.\/)?(?:bin\/)?coding-agent-skills\s+(?:validate-pack|validate-project\s+\S+|repo-map\s+\S+|route-trace\s+\S+|env-audit\s+\S+|secret-audit\s+\S+|api-contract-audit\s+\S+|migration-review\s+\S+|github-handoff\s+\S+|deployment-preflight\s+\S+|validate-adapters\s+\S+|help|--help|-h)\s*$/.test(
571
580
  segment,
572
581
  )
573
582
  ) {