coding-agent-skills 0.2.13 → 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 (69) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +9 -1
  3. package/ROADMAP.md +7 -3
  4. package/bin/coding-agent-skills +14 -0
  5. package/docs/adapters/README.md +39 -0
  6. package/docs/adapters/project-installation.md +26 -0
  7. package/docs/adapters/real-project-adoption.md +2 -1
  8. package/docs/architecture/README.md +5 -3
  9. package/docs/release/README.md +3 -2
  10. package/docs/release/npm-package.md +12 -2
  11. package/docs/safety/README.md +11 -1
  12. package/docs/testing/README.md +16 -0
  13. package/docs/usage/README.md +25 -5
  14. package/examples/command-policies/deployment-preflight.json +70 -0
  15. package/examples/command-policies/github-handoff.json +74 -0
  16. package/examples/evidence-packs/deployment-preflight.json +60 -0
  17. package/examples/evidence-packs/github-handoff.json +67 -0
  18. package/examples/manifests/deployment-preflight.json +14 -0
  19. package/examples/manifests/github-handoff.json +14 -0
  20. package/examples/workflows/deployment-preflight.md +8 -0
  21. package/examples/workflows/github-handoff.md +5 -0
  22. package/package.json +3 -1
  23. package/runs/skill-runs.md +35 -0
  24. package/schemas/project-adapter-installation.schema.json +4 -0
  25. package/schemas/project-adapter.schema.json +4 -0
  26. package/scripts/lib/deployment-preflight.mjs +655 -0
  27. package/scripts/lib/github-handoff.mjs +446 -0
  28. package/scripts/lib/pack-rules.mjs +20 -2
  29. package/scripts/render-deployment-preflight.mjs +9 -0
  30. package/scripts/render-github-handoff.mjs +7 -0
  31. package/scripts/test-pack.mjs +148 -1
  32. package/scripts/validate-pack.mjs +8 -2
  33. package/skills/deployment-preflight/SKILL.md +89 -0
  34. package/skills/deployment-preflight/adapter-interface.md +17 -0
  35. package/skills/deployment-preflight/agents/openai.yaml +3 -0
  36. package/skills/deployment-preflight/checklist.md +7 -0
  37. package/skills/deployment-preflight/evidence-template.md +19 -0
  38. package/skills/deployment-preflight/examples.md +11 -0
  39. package/skills/deployment-preflight/failure-modes.md +11 -0
  40. package/skills/github-handoff/SKILL.md +95 -0
  41. package/skills/github-handoff/adapter-interface.md +18 -0
  42. package/skills/github-handoff/agents/openai.yaml +3 -0
  43. package/skills/github-handoff/checklist.md +10 -0
  44. package/skills/github-handoff/evidence-template.md +16 -0
  45. package/skills/github-handoff/examples.md +19 -0
  46. package/skills/github-handoff/failure-modes.md +8 -0
  47. package/tests/fixtures/deployment-preflight/adapter-project/.coding-agent/adapters/deployment-preflight-fixture/adapter.json +56 -0
  48. package/tests/fixtures/deployment-preflight/adapter-project/.coding-agent/skills.json +23 -0
  49. package/tests/fixtures/deployment-preflight/adapter-project/README.md +3 -0
  50. package/tests/fixtures/deployment-preflight/adapter-project/deploy/netlify.toml +3 -0
  51. package/tests/fixtures/deployment-preflight/adapter-project/ignored/render.yaml +3 -0
  52. package/tests/fixtures/deployment-preflight/adapter-project/package.json +5 -0
  53. package/tests/fixtures/deployment-preflight/static-project/Dockerfile +2 -0
  54. package/tests/fixtures/deployment-preflight/static-project/README.md +3 -0
  55. package/tests/fixtures/deployment-preflight/static-project/docs/deployment.md +4 -0
  56. package/tests/fixtures/deployment-preflight/static-project/package.json +6 -0
  57. package/tests/fixtures/deployment-preflight/static-project/src/index.js +1 -0
  58. package/tests/fixtures/deployment-preflight/static-project/wrangler.toml +3 -0
  59. package/tests/fixtures/github-handoff/adapter-project/.coding-agent/adapters/github-handoff-fixture/adapter.json +56 -0
  60. package/tests/fixtures/github-handoff/adapter-project/.coding-agent/skills.json +23 -0
  61. package/tests/fixtures/github-handoff/adapter-project/README.md +3 -0
  62. package/tests/fixtures/github-handoff/adapter-project/package.json +4 -0
  63. package/tests/fixtures/github-handoff/adapter-project/src/index.js +1 -0
  64. package/tests/fixtures/github-handoff/static-project/README.md +3 -0
  65. package/tests/fixtures/github-handoff/static-project/package.json +4 -0
  66. package/tests/fixtures/github-handoff/static-project/src/index.js +1 -0
  67. package/tests/fixtures/triggers/cases.json +26 -2
  68. package/tests/trigger/README.md +4 -0
  69. package/work-ledger.md +28 -6
@@ -0,0 +1,446 @@
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 = "github-handoff";
23
+
24
+ const REFUSED_BEHAVIOR = [
25
+ "no commits",
26
+ "no pushes",
27
+ "no tags",
28
+ "no branch changes",
29
+ "no pull request creation",
30
+ "no GitHub API mutations",
31
+ "no token reads",
32
+ "no secret-file reads",
33
+ "no project writes",
34
+ ];
35
+
36
+ const NOT_VERIFIED = [
37
+ "remote repository permissions",
38
+ "GitHub pull request state",
39
+ "GitHub issue state",
40
+ "GitHub Actions or CI status",
41
+ "whether local commits have been pushed",
42
+ "reviewer assignment or approval state",
43
+ "release publication state",
44
+ ];
45
+
46
+ function inside(root, candidate) {
47
+ const relative = path.relative(root, candidate);
48
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
49
+ }
50
+
51
+ function toPosix(relativePath) {
52
+ return relativePath.split(path.sep).join("/");
53
+ }
54
+
55
+ function safeRelativePath(candidate) {
56
+ return (
57
+ typeof candidate === "string" &&
58
+ candidate.length > 0 &&
59
+ !candidate.startsWith("/") &&
60
+ !candidate.split(/[\\/]+/).includes("..")
61
+ );
62
+ }
63
+
64
+ function secretBearingPath(relativePath) {
65
+ const normalized = toPosix(relativePath);
66
+ const basename = path.posix.basename(normalized);
67
+ return (
68
+ basename === ".env" ||
69
+ basename.startsWith(".env.") ||
70
+ basename === ".npmrc" ||
71
+ /\.(?:pem|key|p12|pfx)$/i.test(basename) ||
72
+ /(?:^|\/)(?:secrets?|credentials?|private-key|service-role|tokens?)(?:\/|$)/i.test(normalized)
73
+ );
74
+ }
75
+
76
+ function runGit(projectRoot, args) {
77
+ const result = spawnSync("git", args, {
78
+ cwd: projectRoot,
79
+ encoding: "utf8",
80
+ stdio: ["ignore", "pipe", "pipe"],
81
+ });
82
+ return {
83
+ ok: result.status === 0,
84
+ stdout: result.stdout.trim(),
85
+ };
86
+ }
87
+
88
+ function parseStatusEntry(line) {
89
+ const code = line.slice(0, 2);
90
+ const rawPath = line.slice(3).trim();
91
+ const paths = rawPath.split(" -> ");
92
+ const redacted = paths.some(secretBearingPath);
93
+ return {
94
+ code,
95
+ path: redacted ? "[REDACTED:secret-bearing-path]" : rawPath,
96
+ redacted,
97
+ category: categorizeStatus(code),
98
+ };
99
+ }
100
+
101
+ function categorizeStatus(code) {
102
+ if (code.includes("U") || code === "AA" || code === "DD") return "conflicted";
103
+ if (code === "??") return "untracked";
104
+ if (code.includes("A")) return "added";
105
+ if (code.includes("D")) return "deleted";
106
+ if (code.includes("R")) return "renamed";
107
+ if (code.includes("C")) return "copied";
108
+ if (code.includes("M")) return "modified";
109
+ return "other";
110
+ }
111
+
112
+ function summarizeChanges(entries) {
113
+ const summary = {
114
+ total: entries.length,
115
+ added: 0,
116
+ copied: 0,
117
+ deleted: 0,
118
+ modified: 0,
119
+ renamed: 0,
120
+ untracked: 0,
121
+ conflicted: 0,
122
+ other: 0,
123
+ redacted: 0,
124
+ };
125
+ for (const entry of entries) {
126
+ summary[entry.category] += 1;
127
+ if (entry.redacted) summary.redacted += 1;
128
+ }
129
+ return summary;
130
+ }
131
+
132
+ function gitSummary(projectRoot) {
133
+ const summary = {
134
+ isGitRepo: false,
135
+ root: null,
136
+ branchState: null,
137
+ branch: null,
138
+ head: null,
139
+ headShort: null,
140
+ headSubject: null,
141
+ tagsAtHead: [],
142
+ remoteNames: [],
143
+ changedFiles: [],
144
+ changeSummary: summarizeChanges([]),
145
+ warnings: [],
146
+ };
147
+
148
+ const revParse = runGit(projectRoot, ["rev-parse", "--show-toplevel"]);
149
+ if (!revParse.ok) {
150
+ summary.warnings.push("git repository not detected");
151
+ return summary;
152
+ }
153
+ summary.isGitRepo = true;
154
+ summary.root = revParse.stdout;
155
+
156
+ const status = runGit(projectRoot, ["status", "--short", "--branch"]);
157
+ if (!status.ok) {
158
+ summary.warnings.push("git status unavailable");
159
+ } else {
160
+ const lines = status.stdout.split(/\r?\n/).filter(Boolean);
161
+ summary.branchState = lines[0] ?? null;
162
+ summary.changedFiles = lines.slice(1).map(parseStatusEntry);
163
+ summary.changeSummary = summarizeChanges(summary.changedFiles);
164
+ if (summary.changedFiles.length) summary.warnings.push("working tree has local changes");
165
+ if (summary.changeSummary.redacted) {
166
+ summary.warnings.push("one or more changed paths were redacted because they look secret-bearing");
167
+ }
168
+ if (/\[(?:ahead|behind|gone|diverged)[^\]]*\]/i.test(summary.branchState ?? "")) {
169
+ summary.warnings.push("branch state indicates remote divergence; revalidate before handoff");
170
+ }
171
+ }
172
+
173
+ const branch = runGit(projectRoot, ["rev-parse", "--abbrev-ref", "HEAD"]);
174
+ if (branch.ok) summary.branch = branch.stdout;
175
+
176
+ const head = runGit(projectRoot, ["rev-parse", "HEAD"]);
177
+ if (head.ok) {
178
+ summary.head = head.stdout;
179
+ summary.headShort = head.stdout.slice(0, 12);
180
+ }
181
+
182
+ const subject = runGit(projectRoot, ["log", "-1", "--format=%s"]);
183
+ if (subject.ok) summary.headSubject = subject.stdout;
184
+
185
+ const tags = runGit(projectRoot, ["tag", "--points-at", "HEAD"]);
186
+ if (tags.ok) summary.tagsAtHead = tags.stdout ? tags.stdout.split(/\r?\n/).filter(Boolean).sort() : [];
187
+
188
+ const remotes = runGit(projectRoot, ["remote"]);
189
+ if (remotes.ok) summary.remoteNames = remotes.stdout ? remotes.stdout.split(/\r?\n/).filter(Boolean).sort() : [];
190
+
191
+ return summary;
192
+ }
193
+
194
+ function adapterContext(projectRootInput, coreRoot) {
195
+ const loaded = readProjectAdapterDeclaration(projectRootInput);
196
+ if (!loaded.ok) {
197
+ if (loaded.codes.length === 1 && loaded.codes[0] === "missing-project-declaration") {
198
+ return {
199
+ ok: true,
200
+ present: false,
201
+ enabled: false,
202
+ projectRoot: path.resolve(projectRootInput),
203
+ mode: "none",
204
+ codes: [],
205
+ };
206
+ }
207
+ return { ok: false, present: false, enabled: false, status: "failed", codes: loaded.codes };
208
+ }
209
+
210
+ const validation = validateProjectAdapters(loaded.projectRoot, { coreRoot });
211
+ if (!validation.ok) {
212
+ return {
213
+ ok: false,
214
+ present: true,
215
+ enabled: false,
216
+ status: "failed",
217
+ codes: validation.codes,
218
+ validation,
219
+ };
220
+ }
221
+
222
+ if (!validation.acceptedSkills.includes(SKILL_ID)) {
223
+ return {
224
+ ok: true,
225
+ present: true,
226
+ enabled: false,
227
+ projectRoot: loaded.projectRoot,
228
+ mode: "adapter-present-github-handoff-not-enabled",
229
+ declarationPath: loaded.declarationPath,
230
+ declaration: loaded.declaration,
231
+ validation,
232
+ codes: [`${SKILL_ID}-not-enabled-by-adapter`],
233
+ };
234
+ }
235
+
236
+ const adapters = [];
237
+ const errors = [];
238
+ const container = path.resolve(loaded.projectRoot, loaded.declaration.adapterRoot);
239
+ if (!inside(loaded.projectRoot, container) || !fs.existsSync(container)) {
240
+ errors.push("adapter-root-not-found");
241
+ } else {
242
+ for (const declaration of loaded.declaration.adapters ?? []) {
243
+ if (!(declaration.skillIds ?? []).includes(SKILL_ID)) continue;
244
+ const manifestPath = path.join(container, declaration.id, ADAPTER_MANIFEST_FILENAME);
245
+ const record = readSafeJsonFile(manifestPath);
246
+ if (!record.value) {
247
+ errors.push(...record.codes);
248
+ continue;
249
+ }
250
+ adapters.push({
251
+ declaration,
252
+ manifestPath: path.relative(loaded.projectRoot, manifestPath),
253
+ manifest: record.value,
254
+ });
255
+ }
256
+ }
257
+ if (errors.length) {
258
+ return { ok: false, present: true, enabled: false, status: "failed", codes: errors, validation };
259
+ }
260
+ return {
261
+ ok: true,
262
+ present: true,
263
+ enabled: true,
264
+ projectRoot: loaded.projectRoot,
265
+ mode: "adapter-enabled",
266
+ declarationPath: loaded.declarationPath,
267
+ declaration: loaded.declaration,
268
+ validation,
269
+ adapters,
270
+ codes: [],
271
+ };
272
+ }
273
+
274
+ function adapterEvidence(context) {
275
+ const ignoredPaths = new Set([".git", "node_modules", "dist", "build", "coverage", "validation-output"]);
276
+ const requiredEvidence = new Set(["branch state", "HEAD", "working-tree state", "changed-file summary"]);
277
+ for (const adapter of context.adapters ?? []) {
278
+ for (const candidate of adapter.manifest.extensions?.ignoredPaths ?? []) {
279
+ if (safeRelativePath(candidate)) ignoredPaths.add(candidate);
280
+ }
281
+ for (const candidate of adapter.manifest.extensions?.requiredEvidence ?? []) {
282
+ if (typeof candidate === "string" && candidate.trim()) requiredEvidence.add(candidate);
283
+ }
284
+ }
285
+ return {
286
+ ignoredPaths: [...ignoredPaths].sort(),
287
+ requiredEvidence: [...requiredEvidence].sort(),
288
+ };
289
+ }
290
+
291
+ export function buildGithubHandoffReport(projectRootInput, options = {}) {
292
+ const coreRoot = options.coreRoot ?? DEFAULT_CORE_ROOT;
293
+ const context = adapterContext(projectRootInput, coreRoot);
294
+ const projectRoot = context.projectRoot ?? path.resolve(projectRootInput ?? ".");
295
+ const git = gitSummary(projectRoot);
296
+ const base = {
297
+ coreVersion: PILOT_VERSION,
298
+ projectRoot,
299
+ adapter: context,
300
+ git,
301
+ ignoredPaths: [".git", "node_modules", "dist", "build", "coverage", "validation-output"],
302
+ requiredEvidence: ["branch state", "HEAD", "working-tree state", "changed-file summary"],
303
+ changedFiles: [],
304
+ changeSummary: summarizeChanges([]),
305
+ skipped: [],
306
+ warnings: [...git.warnings],
307
+ notVerified: NOT_VERIFIED,
308
+ refusedBehavior: REFUSED_BEHAVIOR,
309
+ };
310
+
311
+ if (!context.ok) {
312
+ return {
313
+ ...base,
314
+ status: "failed",
315
+ warnings: [...(context.codes ?? []), ...git.warnings],
316
+ };
317
+ }
318
+
319
+ if (!git.isGitRepo) {
320
+ return {
321
+ ...base,
322
+ status: "failed",
323
+ skipped: [{ path: ".", reason: "project root is not a Git repository" }],
324
+ };
325
+ }
326
+
327
+ if (context.present && !context.enabled) {
328
+ return {
329
+ ...base,
330
+ status: "partial",
331
+ skipped: [{ path: ".", reason: "project adapter is present but does not enable github-handoff" }],
332
+ warnings: [
333
+ "github-handoff is not enabled by the project adapter; changed-file details were not listed",
334
+ ...git.warnings,
335
+ ],
336
+ };
337
+ }
338
+
339
+ const evidence = context.enabled ? adapterEvidence(context) : {
340
+ ignoredPaths: base.ignoredPaths,
341
+ requiredEvidence: base.requiredEvidence,
342
+ };
343
+
344
+ return {
345
+ ...base,
346
+ status: "complete",
347
+ ignoredPaths: evidence.ignoredPaths,
348
+ requiredEvidence: evidence.requiredEvidence,
349
+ changedFiles: git.changedFiles,
350
+ changeSummary: git.changeSummary,
351
+ warnings: [
352
+ ...git.warnings,
353
+ ...(context.enabled ? ["github-handoff used adapter-declared handoff evidence metadata"] : []),
354
+ "remote URLs were not printed to avoid credential exposure",
355
+ ],
356
+ };
357
+ }
358
+
359
+ function listOrNone(items) {
360
+ return items.length ? items.join(", ") : "none";
361
+ }
362
+
363
+ function renderChangeSummary(summary) {
364
+ return [
365
+ `- Total changed entries: ${summary.total}`,
366
+ `- Added: ${summary.added}`,
367
+ `- Modified: ${summary.modified}`,
368
+ `- Deleted: ${summary.deleted}`,
369
+ `- Renamed: ${summary.renamed}`,
370
+ `- Copied: ${summary.copied}`,
371
+ `- Untracked: ${summary.untracked}`,
372
+ `- Conflicted: ${summary.conflicted}`,
373
+ `- Redacted paths: ${summary.redacted}`,
374
+ ];
375
+ }
376
+
377
+ export function renderGithubHandoffReport(report) {
378
+ const lines = [
379
+ "# GitHub Handoff Report",
380
+ "",
381
+ `Status: ${report.status}`,
382
+ `Core version: ${report.coreVersion}`,
383
+ `Project root: ${report.projectRoot}`,
384
+ "",
385
+ "## Git State",
386
+ `- Git root: ${report.git.root ?? "not detected"}`,
387
+ `- Branch state: ${report.git.branchState ?? "not detected"}`,
388
+ `- Current branch: ${report.git.branch ?? "not detected"}`,
389
+ `- HEAD: ${report.git.headShort ?? "not detected"}`,
390
+ `- HEAD subject: ${report.git.headSubject ?? "not detected"}`,
391
+ `- Tags at HEAD: ${listOrNone(report.git.tagsAtHead)}`,
392
+ `- Remote names: ${listOrNone(report.git.remoteNames)}`,
393
+ "",
394
+ "## Adapter Scope",
395
+ `- Adapter present: ${report.adapter.present ? "yes" : "no"}`,
396
+ `- Github-handoff enabled: ${report.adapter.enabled ? "yes" : "no"}`,
397
+ `- Mode: ${report.adapter.mode ?? "unknown"}`,
398
+ "",
399
+ "## Required Evidence",
400
+ ...report.requiredEvidence.map((item) => `- ${item}`),
401
+ "",
402
+ "## Ignored Paths",
403
+ ...report.ignoredPaths.map((item) => `- ${item}`),
404
+ "",
405
+ "## Change Summary",
406
+ ...renderChangeSummary(report.changeSummary),
407
+ "",
408
+ "## Changed Files",
409
+ ];
410
+
411
+ if (report.changedFiles.length) {
412
+ for (const entry of report.changedFiles) {
413
+ lines.push(`- ${entry.code} ${entry.path}`);
414
+ }
415
+ } else {
416
+ lines.push("- none listed");
417
+ }
418
+
419
+ lines.push(
420
+ "",
421
+ "## Skipped",
422
+ ...(report.skipped.length ? report.skipped.map((item) => `- ${item.path}: ${item.reason}`) : ["- none"]),
423
+ "",
424
+ "## Not Verified",
425
+ ...report.notVerified.map((item) => `- ${item}`),
426
+ "",
427
+ "## Warnings",
428
+ ...(report.warnings.length ? report.warnings.map((item) => `- ${item}`) : ["- none"]),
429
+ "",
430
+ "## Refused Behavior",
431
+ ...report.refusedBehavior.map((item) => `- ${item}`),
432
+ "",
433
+ "No commit, push, tag, branch change, pull request creation, GitHub API mutation, token read, secret-file read, or project write was performed.",
434
+ );
435
+
436
+ return `${lines.join("\n")}\n`;
437
+ }
438
+
439
+ export function githubHandoffCliResult(projectRootInput, options = {}) {
440
+ const report = buildGithubHandoffReport(projectRootInput, options);
441
+ return {
442
+ exitCode: report.status === "failed" ? 1 : 0,
443
+ report,
444
+ lines: renderGithubHandoffReport(report).trimEnd().split("\n"),
445
+ };
446
+ }
@@ -5,6 +5,8 @@ export const PILOT_SKILLS = [
5
5
  "secret-audit",
6
6
  "api-contract-audit",
7
7
  "migration-review",
8
+ "github-handoff",
9
+ "deployment-preflight",
8
10
  "build-verify",
9
11
  "git-preflight",
10
12
  "runtime-truth",
@@ -21,6 +23,8 @@ export const AUDIT_ONLY_SKILLS = [
21
23
  "secret-audit",
22
24
  "api-contract-audit",
23
25
  "migration-review",
26
+ "github-handoff",
27
+ "deployment-preflight",
24
28
  "git-preflight",
25
29
  "runtime-truth",
26
30
  "llm-drift-control",
@@ -337,6 +341,13 @@ export function adapterIssues(adapter, options = {}) {
337
341
  export function classifyTrigger(prompt) {
338
342
  const text = prompt.toLowerCase();
339
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
+ }
340
351
  if (
341
352
  /\b(?:deploy|install|update the lockfile|commit these|publish the branch|restart|enable it|rewrite the documentation)\b/.test(
342
353
  text,
@@ -407,6 +418,13 @@ export function classifyTrigger(prompt) {
407
418
  ) {
408
419
  return "migration-review";
409
420
  }
421
+ if (
422
+ /\b(?:github handoff|handoff report|handoff evidence|pull request handoff|pr handoff|changed files summary|git handoff|release handoff)\b/.test(
423
+ text,
424
+ )
425
+ ) {
426
+ return "github-handoff";
427
+ }
410
428
  if (
411
429
  /\b(?:unfamiliar repository|canonical repository root|canonical repo|map the current packages|map this repository|identify its entry points|nested directory)\b/.test(
412
430
  text,
@@ -548,7 +566,7 @@ function classifySegment(segment, options = {}) {
548
566
  }
549
567
  if (
550
568
  executable === "node" &&
551
- !/^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|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(
552
570
  segment,
553
571
  )
554
572
  ) {
@@ -558,7 +576,7 @@ function classifySegment(segment, options = {}) {
558
576
  ["coding-agent-skills", "bin/coding-agent-skills", "./bin/coding-agent-skills"].includes(
559
577
  executable,
560
578
  ) &&
561
- !/^(?:\.\/)?(?: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+|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(
562
580
  segment,
563
581
  )
564
582
  ) {
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ deploymentPreflightCliResult,
4
+ } from "./lib/deployment-preflight.mjs";
5
+
6
+ const result = deploymentPreflightCliResult(process.argv[2]);
7
+ const stream = result.stream === "stderr" ? process.stderr : process.stdout;
8
+ stream.write(`${result.lines.join("\n")}\n`);
9
+ process.exitCode = result.exitCode;
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { githubHandoffCliResult } from "./lib/github-handoff.mjs";
3
+
4
+ const projectRoot = process.argv[2];
5
+ const result = githubHandoffCliResult(projectRoot);
6
+ process.stdout.write(`${result.lines.join("\n")}\n`);
7
+ process.exitCode = result.exitCode;