coding-agent-skills 0.2.12 → 0.2.14

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 (68) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +8 -0
  3. package/ROADMAP.md +8 -4
  4. package/bin/coding-agent-skills +14 -0
  5. package/docs/adapters/README.md +38 -0
  6. package/docs/adapters/project-installation.md +25 -0
  7. package/docs/adapters/real-project-adoption.md +3 -2
  8. package/docs/architecture/README.md +4 -2
  9. package/docs/release/README.md +5 -3
  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/github-handoff.json +74 -0
  15. package/examples/command-policies/migration-review.json +70 -0
  16. package/examples/evidence-packs/github-handoff.json +67 -0
  17. package/examples/evidence-packs/migration-review.json +60 -0
  18. package/examples/manifests/github-handoff.json +14 -0
  19. package/examples/manifests/migration-review.json +14 -0
  20. package/examples/workflows/github-handoff.md +5 -0
  21. package/examples/workflows/migration-review.md +7 -0
  22. package/package.json +3 -1
  23. package/runs/skill-runs.md +32 -0
  24. package/schemas/project-adapter-installation.schema.json +4 -0
  25. package/schemas/project-adapter.schema.json +4 -0
  26. package/scripts/lib/github-handoff.mjs +446 -0
  27. package/scripts/lib/migration-review.mjs +641 -0
  28. package/scripts/lib/pack-rules.mjs +20 -2
  29. package/scripts/render-github-handoff.mjs +7 -0
  30. package/scripts/render-migration-review.mjs +8 -0
  31. package/scripts/test-pack.mjs +147 -1
  32. package/scripts/validate-pack.mjs +8 -2
  33. package/skills/github-handoff/SKILL.md +95 -0
  34. package/skills/github-handoff/adapter-interface.md +18 -0
  35. package/skills/github-handoff/agents/openai.yaml +3 -0
  36. package/skills/github-handoff/checklist.md +10 -0
  37. package/skills/github-handoff/evidence-template.md +16 -0
  38. package/skills/github-handoff/examples.md +19 -0
  39. package/skills/github-handoff/failure-modes.md +8 -0
  40. package/skills/migration-review/SKILL.md +87 -0
  41. package/skills/migration-review/adapter-interface.md +16 -0
  42. package/skills/migration-review/agents/openai.yaml +3 -0
  43. package/skills/migration-review/checklist.md +8 -0
  44. package/skills/migration-review/evidence-template.md +12 -0
  45. package/skills/migration-review/examples.md +20 -0
  46. package/skills/migration-review/failure-modes.md +5 -0
  47. package/tests/fixtures/github-handoff/adapter-project/.coding-agent/adapters/github-handoff-fixture/adapter.json +56 -0
  48. package/tests/fixtures/github-handoff/adapter-project/.coding-agent/skills.json +23 -0
  49. package/tests/fixtures/github-handoff/adapter-project/README.md +3 -0
  50. package/tests/fixtures/github-handoff/adapter-project/package.json +4 -0
  51. package/tests/fixtures/github-handoff/adapter-project/src/index.js +1 -0
  52. package/tests/fixtures/github-handoff/static-project/README.md +3 -0
  53. package/tests/fixtures/github-handoff/static-project/package.json +4 -0
  54. package/tests/fixtures/github-handoff/static-project/src/index.js +1 -0
  55. package/tests/fixtures/migration-review/adapter-project/.coding-agent/adapters/migration-review-fixture/adapter.json +56 -0
  56. package/tests/fixtures/migration-review/adapter-project/.coding-agent/skills.json +23 -0
  57. package/tests/fixtures/migration-review/adapter-project/README.md +3 -0
  58. package/tests/fixtures/migration-review/adapter-project/db/migrations/001_create_accounts.sql +3 -0
  59. package/tests/fixtures/migration-review/adapter-project/ignored/migrations/999_ignore.sql +1 -0
  60. package/tests/fixtures/migration-review/adapter-project/package.json +3 -0
  61. package/tests/fixtures/migration-review/static-project/README.md +3 -0
  62. package/tests/fixtures/migration-review/static-project/drizzle.config.ts +4 -0
  63. package/tests/fixtures/migration-review/static-project/package.json +7 -0
  64. package/tests/fixtures/migration-review/static-project/prisma/migrations/20260703010101_init/migration.sql +6 -0
  65. package/tests/fixtures/migration-review/static-project/prisma/schema.prisma +4 -0
  66. package/tests/fixtures/triggers/cases.json +26 -2
  67. package/tests/trigger/README.md +4 -0
  68. package/work-ledger.md +27 -5
@@ -0,0 +1,60 @@
1
+ {
2
+ "contractVersion": "1.0.0",
3
+ "skill": {"name": "migration-review", "version": "0.2.3"},
4
+ "invocation": {
5
+ "id": "example-migration-review",
6
+ "startedAt": "2026-07-03T12:00:00Z",
7
+ "endedAt": "2026-07-03T12:01:00Z"
8
+ },
9
+ "repository": {
10
+ "root": "/workspace/example-project",
11
+ "branch": "main",
12
+ "head": "0123456789abcdef",
13
+ "workingTreeState": "clean"
14
+ },
15
+ "userIntent": "Map static migration and schema evidence before database handoff.",
16
+ "declaredScope": ["/workspace/example-project"],
17
+ "projectAdapter": "example-migration-review-adapter",
18
+ "environmentSummary": {"platform": "linux", "shell": "bash"},
19
+ "status": "complete",
20
+ "confidence": {
21
+ "level": "medium",
22
+ "reason": "Static migration and schema files were inspected, but database state was not verified."
23
+ },
24
+ "commands": [
25
+ {
26
+ "command": "coding-agent-skills migration-review /workspace/example-project",
27
+ "family": "migration-review-renderer",
28
+ "workingDirectory": "/workspace/example-project",
29
+ "startedAt": "2026-07-03T12:00:20Z",
30
+ "endedAt": "2026-07-03T12:00:21Z",
31
+ "exitStatus": 0,
32
+ "resultStatus": "success",
33
+ "safetyClass": "allowed",
34
+ "approvalReference": null,
35
+ "purpose": "Render a static migration review report.",
36
+ "outputSummary": "Reported static migration files, schema files, config files, risk indicators, skipped items, and not-verified database areas."
37
+ }
38
+ ],
39
+ "skippedChecks": [],
40
+ "findings": [
41
+ {
42
+ "summary": "Static migration evidence was mapped without database access.",
43
+ "evidence": ["prisma/migrations/example/migration.sql", "prisma/schema.prisma"]
44
+ }
45
+ ],
46
+ "risks": [
47
+ {
48
+ "summary": "Database state may differ from static migration evidence.",
49
+ "evidence": ["applied migration history was not queried", "runtime database connectivity was not tested"]
50
+ }
51
+ ],
52
+ "failures": [],
53
+ "unresolvedQuestions": [],
54
+ "changedState": {
55
+ "changed": false,
56
+ "summary": "No project, Git, dependency, runtime, database, generated-code, service, or remote state changed."
57
+ },
58
+ "handoffSummary": "Static migration evidence is mapped; applied database state remains unverified.",
59
+ "recommendedNextAction": "Review reported migration surfaces before editing database schema or migration files."
60
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "github-handoff",
3
+ "version": "0.2.3",
4
+ "mode": "audit-only",
5
+ "evidenceContract": "../../contracts/evidence-pack/evidence-pack.schema.json",
6
+ "commandPolicy": "../command-policies/github-handoff.json",
7
+ "adapterSchema": "../../schemas/project-adapter.schema.json",
8
+ "adapterCompatibility": {
9
+ "contractVersion": "1.0.0",
10
+ "compatibleAdapterVersions": ["1.0.0"]
11
+ },
12
+ "adapterInterface": "../../skills/github-handoff/adapter-interface.md",
13
+ "description": "Prepare local Git handoff evidence without GitHub mutation."
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "migration-review",
3
+ "version": "0.2.3",
4
+ "mode": "audit-only",
5
+ "evidenceContract": "../../contracts/evidence-pack/evidence-pack.schema.json",
6
+ "commandPolicy": "../command-policies/migration-review.json",
7
+ "adapterSchema": "../../schemas/project-adapter.schema.json",
8
+ "adapterCompatibility": {
9
+ "contractVersion": "1.0.0",
10
+ "compatibleAdapterVersions": ["1.0.0"]
11
+ },
12
+ "adapterInterface": "../../skills/migration-review/adapter-interface.md",
13
+ "description": "Map static database migration and schema evidence without database access."
14
+ }
@@ -0,0 +1,5 @@
1
+ # GitHub Handoff Workflow
2
+
3
+ Run `coding-agent-skills github-handoff /path/to/project` to summarize local Git metadata and changed-file status before a separately approved GitHub action.
4
+
5
+ The workflow is audit-only: it does not commit, push, tag, create pull requests, call GitHub APIs, read tokens, read secret files, or write project files.
@@ -0,0 +1,7 @@
1
+ # Migration Review Workflow
2
+
3
+ Use `coding-agent-skills migration-review <project-root>` to map static migration, schema,
4
+ config, package-script-key, and risk-indicator evidence before database work.
5
+
6
+ The workflow remains read-only: no database connections, migration execution, ORM generation,
7
+ package installation, builds, tests, deployments, secret-file reads, or project writes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-agent-skills",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "Evidence-first, read-only coding-agent skills and project adapter tooling.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -13,6 +13,8 @@
13
13
  "env-audit",
14
14
  "secret-audit",
15
15
  "api-contract-audit",
16
+ "migration-review",
17
+ "github-handoff",
16
18
  "project-adapters",
17
19
  "code-validation",
18
20
  "cli"
@@ -229,3 +229,35 @@ This file records bounded maintainer-loop runs. Entries must not contain secrets
229
229
  - Validation commands: pending final release validation matrix.
230
230
  - Result: pass pending final publication evidence.
231
231
  - Commit/tag/push status: pending approved release workflow.
232
+
233
+ ## implementation-v0.2.13-migration-review
234
+
235
+ - Run ID: `implementation-v0.2.13-migration-review`
236
+ - Repository: `/home/oneclickwebsitedesignfactory/coding-agent-skills`
237
+ - Command used: `builder-mode approval for migration-review-skill implementation and release`
238
+ - Files changed: `migration-review` skill, migration review renderer and library,
239
+ CLI wrapper, adapter schemas, pack rules, release tests, synthetic migration fixtures,
240
+ usage/release/safety/adapter docs, changelog, roadmap, work ledger, run log, and package
241
+ metadata.
242
+ - Safety boundary: read-only, static-analysis only, no `.env` reads, no database
243
+ connections, no migration execution, no ORM generation, no package scripts, no builds,
244
+ no tests in target projects, no deploys, and no target-project mutation.
245
+ - Validation commands: pending final release validation matrix.
246
+ - Result: passed; `v0.2.13` was committed, pushed, tagged, published to npm, smoke-tested
247
+ from the registry, and released on GitHub.
248
+ - Commit/tag/push status: complete.
249
+
250
+ ## implementation-v0.2.14-github-handoff
251
+
252
+ - Run ID: `implementation-v0.2.14-github-handoff`
253
+ - Repository: `/home/oneclickwebsitedesignfactory/coding-agent-skills`
254
+ - Command used: `builder-mode approval for github-handoff-skill implementation and release`
255
+ - Files changed: `github-handoff` skill, GitHub handoff renderer and library, CLI wrapper,
256
+ adapter schemas, pack rules, release tests, synthetic handoff fixtures, usage/release/
257
+ safety/adapter docs, changelog, roadmap, work ledger, run log, and package metadata.
258
+ - Safety boundary: read-only local Git metadata only, no `.env` reads, no token reads, no
259
+ remote URL printing, no commits, no pushes, no tags, no pull request creation, no GitHub
260
+ API calls, and no target-project mutation.
261
+ - Validation commands: pending final release validation matrix.
262
+ - Result: pass pending final publication evidence.
263
+ - Commit/tag/push status: pending approved release workflow.
@@ -69,6 +69,8 @@
69
69
  "env-audit",
70
70
  "secret-audit",
71
71
  "api-contract-audit",
72
+ "migration-review",
73
+ "github-handoff",
72
74
  "build-verify",
73
75
  "git-preflight",
74
76
  "runtime-truth",
@@ -105,6 +107,8 @@
105
107
  "env-audit",
106
108
  "secret-audit",
107
109
  "api-contract-audit",
110
+ "migration-review",
111
+ "github-handoff",
108
112
  "build-verify",
109
113
  "git-preflight",
110
114
  "runtime-truth",
@@ -93,6 +93,8 @@
93
93
  "env-audit",
94
94
  "secret-audit",
95
95
  "api-contract-audit",
96
+ "migration-review",
97
+ "github-handoff",
96
98
  "build-verify",
97
99
  "git-preflight",
98
100
  "runtime-truth",
@@ -173,6 +175,8 @@
173
175
  "env-audit",
174
176
  "secret-audit",
175
177
  "api-contract-audit",
178
+ "migration-review",
179
+ "github-handoff",
176
180
  "build-verify",
177
181
  "git-preflight",
178
182
  "runtime-truth",
@@ -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
+ }