coding-agent-skills 0.2.13 → 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.
- package/CHANGELOG.md +20 -0
- package/README.md +4 -0
- package/ROADMAP.md +5 -3
- package/bin/coding-agent-skills +7 -0
- package/docs/adapters/README.md +18 -0
- package/docs/adapters/project-installation.md +12 -0
- package/docs/adapters/real-project-adoption.md +3 -2
- package/docs/architecture/README.md +3 -2
- package/docs/release/README.md +3 -2
- package/docs/release/npm-package.md +7 -2
- package/docs/safety/README.md +6 -1
- package/docs/testing/README.md +8 -0
- package/docs/usage/README.md +15 -5
- package/examples/command-policies/github-handoff.json +74 -0
- package/examples/evidence-packs/github-handoff.json +67 -0
- package/examples/manifests/github-handoff.json +14 -0
- package/examples/workflows/github-handoff.md +5 -0
- package/package.json +2 -1
- package/runs/skill-runs.md +16 -0
- package/schemas/project-adapter-installation.schema.json +2 -0
- package/schemas/project-adapter.schema.json +2 -0
- package/scripts/lib/github-handoff.mjs +446 -0
- package/scripts/lib/pack-rules.mjs +11 -2
- package/scripts/render-github-handoff.mjs +7 -0
- package/scripts/test-pack.mjs +89 -1
- package/scripts/validate-pack.mjs +5 -2
- package/skills/github-handoff/SKILL.md +95 -0
- package/skills/github-handoff/adapter-interface.md +18 -0
- package/skills/github-handoff/agents/openai.yaml +3 -0
- package/skills/github-handoff/checklist.md +10 -0
- package/skills/github-handoff/evidence-template.md +16 -0
- package/skills/github-handoff/examples.md +19 -0
- package/skills/github-handoff/failure-modes.md +8 -0
- package/tests/fixtures/github-handoff/adapter-project/.coding-agent/adapters/github-handoff-fixture/adapter.json +56 -0
- package/tests/fixtures/github-handoff/adapter-project/.coding-agent/skills.json +23 -0
- package/tests/fixtures/github-handoff/adapter-project/README.md +3 -0
- package/tests/fixtures/github-handoff/adapter-project/package.json +4 -0
- package/tests/fixtures/github-handoff/adapter-project/src/index.js +1 -0
- package/tests/fixtures/github-handoff/static-project/README.md +3 -0
- package/tests/fixtures/github-handoff/static-project/package.json +4 -0
- package/tests/fixtures/github-handoff/static-project/src/index.js +1 -0
- package/tests/fixtures/triggers/cases.json +14 -2
- package/tests/trigger/README.md +2 -0
- package/work-ledger.md +16 -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,7 @@ export const PILOT_SKILLS = [
|
|
|
5
5
|
"secret-audit",
|
|
6
6
|
"api-contract-audit",
|
|
7
7
|
"migration-review",
|
|
8
|
+
"github-handoff",
|
|
8
9
|
"build-verify",
|
|
9
10
|
"git-preflight",
|
|
10
11
|
"runtime-truth",
|
|
@@ -21,6 +22,7 @@ export const AUDIT_ONLY_SKILLS = [
|
|
|
21
22
|
"secret-audit",
|
|
22
23
|
"api-contract-audit",
|
|
23
24
|
"migration-review",
|
|
25
|
+
"github-handoff",
|
|
24
26
|
"git-preflight",
|
|
25
27
|
"runtime-truth",
|
|
26
28
|
"llm-drift-control",
|
|
@@ -407,6 +409,13 @@ export function classifyTrigger(prompt) {
|
|
|
407
409
|
) {
|
|
408
410
|
return "migration-review";
|
|
409
411
|
}
|
|
412
|
+
if (
|
|
413
|
+
/\b(?:github handoff|handoff report|handoff evidence|pull request handoff|pr handoff|changed files summary|git handoff|release handoff)\b/.test(
|
|
414
|
+
text,
|
|
415
|
+
)
|
|
416
|
+
) {
|
|
417
|
+
return "github-handoff";
|
|
418
|
+
}
|
|
410
419
|
if (
|
|
411
420
|
/\b(?:unfamiliar repository|canonical repository root|canonical repo|map the current packages|map this repository|identify its entry points|nested directory)\b/.test(
|
|
412
421
|
text,
|
|
@@ -548,7 +557,7 @@ function classifySegment(segment, options = {}) {
|
|
|
548
557
|
}
|
|
549
558
|
if (
|
|
550
559
|
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(
|
|
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(
|
|
552
561
|
segment,
|
|
553
562
|
)
|
|
554
563
|
) {
|
|
@@ -558,7 +567,7 @@ function classifySegment(segment, options = {}) {
|
|
|
558
567
|
["coding-agent-skills", "bin/coding-agent-skills", "./bin/coding-agent-skills"].includes(
|
|
559
568
|
executable,
|
|
560
569
|
) &&
|
|
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(
|
|
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(
|
|
562
571
|
segment,
|
|
563
572
|
)
|
|
564
573
|
) {
|
|
@@ -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;
|
package/scripts/test-pack.mjs
CHANGED
|
@@ -67,6 +67,11 @@ import {
|
|
|
67
67
|
migrationReviewCliResult,
|
|
68
68
|
renderMigrationReviewReport,
|
|
69
69
|
} from "./lib/migration-review.mjs";
|
|
70
|
+
import {
|
|
71
|
+
buildGithubHandoffReport,
|
|
72
|
+
githubHandoffCliResult,
|
|
73
|
+
renderGithubHandoffReport,
|
|
74
|
+
} from "./lib/github-handoff.mjs";
|
|
70
75
|
import {
|
|
71
76
|
adapterUpgradeCliResult,
|
|
72
77
|
checkAdapterUpgrade,
|
|
@@ -133,6 +138,27 @@ function readJson(relativePath) {
|
|
|
133
138
|
return JSON.parse(read(relativePath));
|
|
134
139
|
}
|
|
135
140
|
|
|
141
|
+
function runGitFixtureCommand(cwd, args) {
|
|
142
|
+
const result = spawnSync("git", args, {
|
|
143
|
+
cwd,
|
|
144
|
+
encoding: "utf8",
|
|
145
|
+
stdio: "pipe",
|
|
146
|
+
});
|
|
147
|
+
assert.equal(result.status, 0, `git ${args.join(" ")}\n${result.stderr}`);
|
|
148
|
+
return result.stdout.trim();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function createGitFixture(sourceRelativePath) {
|
|
152
|
+
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "github-handoff-fixture-"));
|
|
153
|
+
fs.cpSync(path.join(root, sourceRelativePath), temporary, { recursive: true });
|
|
154
|
+
runGitFixtureCommand(temporary, ["init", "-b", "main"]);
|
|
155
|
+
runGitFixtureCommand(temporary, ["config", "user.name", "Fixture User"]);
|
|
156
|
+
runGitFixtureCommand(temporary, ["config", "user.email", "fixture@example.invalid"]);
|
|
157
|
+
runGitFixtureCommand(temporary, ["add", "."]);
|
|
158
|
+
runGitFixtureCommand(temporary, ["commit", "-m", "initial fixture commit"]);
|
|
159
|
+
return temporary;
|
|
160
|
+
}
|
|
161
|
+
|
|
136
162
|
function walk(directory, output = []) {
|
|
137
163
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
138
164
|
if ([".git", "node_modules", "validation-output"].includes(entry.name)) continue;
|
|
@@ -286,10 +312,16 @@ test("local CLI maps approved commands to existing safe scripts", () => {
|
|
|
286
312
|
assert.ok(cliText.includes("scripts/render-secret-audit.mjs"));
|
|
287
313
|
assert.ok(cliText.includes("scripts/render-api-contract-audit.mjs"));
|
|
288
314
|
assert.ok(cliText.includes("scripts/render-migration-review.mjs"));
|
|
315
|
+
assert.ok(cliText.includes("scripts/render-github-handoff.mjs"));
|
|
289
316
|
assert.ok(cliText.includes("scripts/validate-adapters.mjs"));
|
|
290
317
|
assert.ok(!cliText.includes(".env"));
|
|
291
318
|
|
|
292
319
|
const fixtureRoot = path.join(root, "tests", "fixtures");
|
|
320
|
+
const githubHandoffFixture = createGitFixture(
|
|
321
|
+
path.join("tests", "fixtures", "github-handoff", "static-project"),
|
|
322
|
+
);
|
|
323
|
+
fs.appendFileSync(path.join(githubHandoffFixture, "README.md"), "\nLocal handoff change.\n");
|
|
324
|
+
|
|
293
325
|
const commands = [
|
|
294
326
|
[["validate-pack"], /pilot pack valid/],
|
|
295
327
|
[
|
|
@@ -327,6 +359,10 @@ test("local CLI maps approved commands to existing safe scripts", () => {
|
|
|
327
359
|
["migration-review", path.join(fixtureRoot, "migration-review", "static-project")],
|
|
328
360
|
/# Migration Review Report/,
|
|
329
361
|
],
|
|
362
|
+
[
|
|
363
|
+
["github-handoff", githubHandoffFixture],
|
|
364
|
+
/# GitHub Handoff Report/,
|
|
365
|
+
],
|
|
330
366
|
];
|
|
331
367
|
|
|
332
368
|
for (const [args, expected] of commands) {
|
|
@@ -351,7 +387,7 @@ test("local CLI maps approved commands to existing safe scripts", () => {
|
|
|
351
387
|
test("npm package metadata is public-ready and dependency-free", () => {
|
|
352
388
|
const packageJson = readJson("package.json");
|
|
353
389
|
assert.equal(packageJson.name, "coding-agent-skills");
|
|
354
|
-
assert.equal(packageJson.version, "0.2.
|
|
390
|
+
assert.equal(packageJson.version, "0.2.14");
|
|
355
391
|
assert.equal(
|
|
356
392
|
packageJson.description,
|
|
357
393
|
"Evidence-first, read-only coding-agent skills and project adapter tooling.",
|
|
@@ -368,6 +404,7 @@ test("npm package metadata is public-ready and dependency-free", () => {
|
|
|
368
404
|
"secret-audit",
|
|
369
405
|
"api-contract-audit",
|
|
370
406
|
"migration-review",
|
|
407
|
+
"github-handoff",
|
|
371
408
|
"project-adapters",
|
|
372
409
|
"code-validation",
|
|
373
410
|
"cli",
|
|
@@ -708,6 +745,57 @@ test("migration-review does not broaden a repo-map-only project adapter", () =>
|
|
|
708
745
|
assert.match(renderMigrationReviewReport(result), /migration-review is not enabled/);
|
|
709
746
|
});
|
|
710
747
|
|
|
748
|
+
test("github-handoff summarizes local git state without mutating remotes", () => {
|
|
749
|
+
const fixture = createGitFixture(path.join("tests", "fixtures", "github-handoff", "static-project"));
|
|
750
|
+
runGitFixtureCommand(fixture, ["tag", "v0.0.0"]);
|
|
751
|
+
fs.appendFileSync(path.join(fixture, "README.md"), "\nChanged for handoff.\n");
|
|
752
|
+
fs.writeFileSync(path.join(fixture, "src", "new-file.js"), "export const handoff = true;\n");
|
|
753
|
+
|
|
754
|
+
const result = buildGithubHandoffReport(fixture, { coreRoot: root });
|
|
755
|
+
|
|
756
|
+
assert.equal(result.status, "complete");
|
|
757
|
+
assert.equal(result.git.branch, "main");
|
|
758
|
+
assert.ok(result.git.head);
|
|
759
|
+
assert.ok(result.git.tagsAtHead.includes("v0.0.0"));
|
|
760
|
+
assert.equal(result.changeSummary.total, 2);
|
|
761
|
+
assert.equal(result.changeSummary.modified, 1);
|
|
762
|
+
assert.equal(result.changeSummary.untracked, 1);
|
|
763
|
+
assert.ok(result.changedFiles.some((record) => record.path === "README.md"));
|
|
764
|
+
assert.ok(result.changedFiles.some((record) => record.path === "src/new-file.js"));
|
|
765
|
+
assert.match(renderGithubHandoffReport(result), /No commit, push, tag/);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
test("github-handoff respects adapter-declared handoff metadata", () => {
|
|
769
|
+
const fixture = createGitFixture(path.join("tests", "fixtures", "github-handoff", "adapter-project"));
|
|
770
|
+
fs.appendFileSync(path.join(fixture, "src", "index.js"), "\nexport const changed = true;\n");
|
|
771
|
+
|
|
772
|
+
const result = buildGithubHandoffReport(fixture, { coreRoot: root });
|
|
773
|
+
|
|
774
|
+
assert.equal(result.status, "complete");
|
|
775
|
+
assert.equal(result.adapter.enabled, true);
|
|
776
|
+
assert.ok(result.requiredEvidence.includes("handoff summary"));
|
|
777
|
+
assert.ok(result.ignoredPaths.includes("tmp"));
|
|
778
|
+
assert.equal(result.changeSummary.modified, 1);
|
|
779
|
+
assert.ok(result.warnings.includes("github-handoff used adapter-declared handoff evidence metadata"));
|
|
780
|
+
const cli = githubHandoffCliResult(fixture, { coreRoot: root });
|
|
781
|
+
assert.equal(cli.exitCode, 0);
|
|
782
|
+
assert.match(cli.lines.join("\n"), /Github-handoff enabled: yes/);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
test("github-handoff does not broaden a repo-map-only project adapter", () => {
|
|
786
|
+
const fixture = createGitFixture(
|
|
787
|
+
path.join("tests", "fixtures", "project-adapter-installation", "valid-exact-pin"),
|
|
788
|
+
);
|
|
789
|
+
fs.appendFileSync(path.join(fixture, "README.md"), "\nShould not be listed.\n");
|
|
790
|
+
|
|
791
|
+
const result = buildGithubHandoffReport(fixture, { coreRoot: root });
|
|
792
|
+
|
|
793
|
+
assert.equal(result.status, "partial");
|
|
794
|
+
assert.equal(result.changedFiles.length, 0);
|
|
795
|
+
assert.equal(result.changeSummary.total, 0);
|
|
796
|
+
assert.match(renderGithubHandoffReport(result), /github-handoff is not enabled/);
|
|
797
|
+
});
|
|
798
|
+
|
|
711
799
|
test("validate-pack accepts installed package trees without source-only gitignore", () => {
|
|
712
800
|
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "installed-package-"));
|
|
713
801
|
const installedRoot = path.join(temporaryRoot, "coding-agent-skills");
|
|
@@ -100,6 +100,7 @@ const requiredRootFiles = [
|
|
|
100
100
|
"scripts/render-secret-audit.mjs",
|
|
101
101
|
"scripts/render-api-contract-audit.mjs",
|
|
102
102
|
"scripts/render-migration-review.mjs",
|
|
103
|
+
"scripts/render-github-handoff.mjs",
|
|
103
104
|
"scripts/check-adapter-upgrade.mjs",
|
|
104
105
|
"scripts/check-adapter-upgrade-chain.mjs",
|
|
105
106
|
"scripts/validate-adapters.mjs",
|
|
@@ -110,6 +111,7 @@ const requiredRootFiles = [
|
|
|
110
111
|
"scripts/lib/secret-audit.mjs",
|
|
111
112
|
"scripts/lib/api-contract-audit.mjs",
|
|
112
113
|
"scripts/lib/migration-review.mjs",
|
|
114
|
+
"scripts/lib/github-handoff.mjs",
|
|
113
115
|
"scripts/lib/adapter-upgrade.mjs",
|
|
114
116
|
"scripts/lib/adapter-upgrade-chain.mjs",
|
|
115
117
|
"scripts/lib/adapter-discovery.mjs",
|
|
@@ -683,8 +685,8 @@ if (packageJson) {
|
|
|
683
685
|
if (packageJson.name !== "coding-agent-skills") {
|
|
684
686
|
failures.push("package.json has unexpected package name");
|
|
685
687
|
}
|
|
686
|
-
if (packageJson.version !== "0.2.
|
|
687
|
-
failures.push("package.json version must be 0.2.
|
|
688
|
+
if (packageJson.version !== "0.2.14") {
|
|
689
|
+
failures.push("package.json version must be 0.2.14 for public package validation");
|
|
688
690
|
}
|
|
689
691
|
if (packageJson.type !== "module") failures.push("package.json must preserve ESM mode");
|
|
690
692
|
if (packageJson.private !== false) {
|
|
@@ -707,6 +709,7 @@ if (packageJson) {
|
|
|
707
709
|
"secret-audit",
|
|
708
710
|
"api-contract-audit",
|
|
709
711
|
"migration-review",
|
|
712
|
+
"github-handoff",
|
|
710
713
|
"project-adapters",
|
|
711
714
|
"code-validation",
|
|
712
715
|
"cli",
|