@kingkyylian/handoffkit 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +16 -3
- package/ROADMAP.md +2 -2
- package/dist/index.js +486 -151
- package/dist/index.js.map +1 -1
- package/docs/CACHE.md +34 -0
- package/docs/RELEASE.md +10 -7
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -7,6 +7,64 @@ import { Command as Command6 } from "commander";
|
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import { z as z2 } from "zod";
|
|
9
9
|
|
|
10
|
+
// src/core/cache.ts
|
|
11
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
|
|
14
|
+
// src/core/redact.ts
|
|
15
|
+
var REDACTION = "[REDACTED]";
|
|
16
|
+
var SECRET_KEY_PATTERN = /(\b(?:[A-Z0-9]+[_.-])*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE[_-]?KEY|CLIENT[_-]?SECRET|ACCESS[_-]?TOKEN|REFRESH[_-]?TOKEN|COOKIE|SESSION|JWT|AUTH_TOKEN)(?:[_.-][A-Z0-9]+)*\b\s*(?:=|:)\s*)(["']?)([^\s"',}]+)/gi;
|
|
17
|
+
var TOKEN_PATTERNS = [
|
|
18
|
+
/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi,
|
|
19
|
+
/\bsk-[A-Za-z0-9_-]{16,}/g,
|
|
20
|
+
/\bgh[pousr]_[A-Za-z0-9_]{16,}/g,
|
|
21
|
+
/\bnpm_[A-Za-z0-9_-]{16,}/g,
|
|
22
|
+
/\bxox[baprs]-[A-Za-z0-9-]{16,}/g,
|
|
23
|
+
/\bAIza[0-9A-Za-z_-]{20,}/g,
|
|
24
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
25
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
|
|
26
|
+
/\/\/([^/\s:@]+):([^@\s/]+)@/g
|
|
27
|
+
];
|
|
28
|
+
function redactText(input) {
|
|
29
|
+
let output = input.replace(
|
|
30
|
+
/-----BEGIN ([A-Z ]*PRIVATE KEY)-----[\s\S]*?-----END \1-----/g,
|
|
31
|
+
(_match, keyType) => `-----BEGIN ${keyType}-----
|
|
32
|
+
${REDACTION}
|
|
33
|
+
-----END ${keyType}-----`
|
|
34
|
+
);
|
|
35
|
+
output = output.replace(SECRET_KEY_PATTERN, (_match, prefix, quote) => {
|
|
36
|
+
return `${prefix}${quote}${REDACTION}${quote}`;
|
|
37
|
+
});
|
|
38
|
+
output = output.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, "Bearer [REDACTED]");
|
|
39
|
+
output = output.replace(/\/\/([^/\s:@]+):([^@\s/]+)@/g, "//[REDACTED]@");
|
|
40
|
+
for (const pattern of TOKEN_PATTERNS.slice(1, -1)) {
|
|
41
|
+
output = output.replace(pattern, REDACTION);
|
|
42
|
+
}
|
|
43
|
+
return output;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/core/cache.ts
|
|
47
|
+
async function writeCacheArtifact(root, kind, data, options = {}) {
|
|
48
|
+
const createdAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
49
|
+
const envelope = {
|
|
50
|
+
version: 1,
|
|
51
|
+
kind,
|
|
52
|
+
createdAt,
|
|
53
|
+
data
|
|
54
|
+
};
|
|
55
|
+
const cacheDir = join(root, ".handoffkit", kind);
|
|
56
|
+
const artifactPath = join(cacheDir, `${cacheTimestamp(createdAt)}.json`);
|
|
57
|
+
const latestPath = join(cacheDir, "latest.json");
|
|
58
|
+
const contents = `${redactText(JSON.stringify(envelope, null, 2))}
|
|
59
|
+
`;
|
|
60
|
+
await mkdir(cacheDir, { recursive: true });
|
|
61
|
+
await Promise.all([writeFile(artifactPath, contents, "utf8"), writeFile(latestPath, contents, "utf8")]);
|
|
62
|
+
return { artifactPath, latestPath };
|
|
63
|
+
}
|
|
64
|
+
function cacheTimestamp(timestamp) {
|
|
65
|
+
return timestamp.replace(/[:.]/g, "-");
|
|
66
|
+
}
|
|
67
|
+
|
|
10
68
|
// src/core/git.ts
|
|
11
69
|
import { readFile } from "fs/promises";
|
|
12
70
|
import { basename } from "path";
|
|
@@ -26,7 +84,7 @@ function formatCliError(error) {
|
|
|
26
84
|
|
|
27
85
|
// src/core/git.ts
|
|
28
86
|
var UNTRACKED_PATCH_CHAR_LIMIT = 2e4;
|
|
29
|
-
var IGNORED_CHANGED_PATH_PREFIXES = ["node_modules/", "dist/", "coverage/", ".git/"];
|
|
87
|
+
var IGNORED_CHANGED_PATH_PREFIXES = ["node_modules/", "dist/", "coverage/", ".git/", ".handoffkit/"];
|
|
30
88
|
async function findGitRoot(cwd) {
|
|
31
89
|
const result = await execa("git", ["rev-parse", "--show-toplevel"], {
|
|
32
90
|
cwd,
|
|
@@ -160,40 +218,6 @@ function isIgnoredChangedPath(file) {
|
|
|
160
218
|
// src/core/instructions.ts
|
|
161
219
|
import { readFile as readFile2, stat } from "fs/promises";
|
|
162
220
|
import fg from "fast-glob";
|
|
163
|
-
|
|
164
|
-
// src/core/redact.ts
|
|
165
|
-
var REDACTION = "[REDACTED]";
|
|
166
|
-
var SECRET_KEY_PATTERN = /(\b[A-Z0-9_.-]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE[_-]?KEY|CLIENT[_-]?SECRET|ACCESS[_-]?TOKEN|REFRESH[_-]?TOKEN|COOKIE|SESSION|JWT|AUTH_TOKEN)[A-Z0-9_.-]*\b\s*(?:=|:)\s*)(["']?)([^\s"',}]+)/gi;
|
|
167
|
-
var TOKEN_PATTERNS = [
|
|
168
|
-
/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi,
|
|
169
|
-
/\bsk-[A-Za-z0-9_-]{16,}/g,
|
|
170
|
-
/\bgh[pousr]_[A-Za-z0-9_]{16,}/g,
|
|
171
|
-
/\bnpm_[A-Za-z0-9_-]{16,}/g,
|
|
172
|
-
/\bxox[baprs]-[A-Za-z0-9-]{16,}/g,
|
|
173
|
-
/\bAIza[0-9A-Za-z_-]{20,}/g,
|
|
174
|
-
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
175
|
-
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
|
|
176
|
-
/\/\/([^/\s:@]+):([^@\s/]+)@/g
|
|
177
|
-
];
|
|
178
|
-
function redactText(input) {
|
|
179
|
-
let output = input.replace(
|
|
180
|
-
/-----BEGIN ([A-Z ]*PRIVATE KEY)-----[\s\S]*?-----END \1-----/g,
|
|
181
|
-
(_match, keyType) => `-----BEGIN ${keyType}-----
|
|
182
|
-
${REDACTION}
|
|
183
|
-
-----END ${keyType}-----`
|
|
184
|
-
);
|
|
185
|
-
output = output.replace(SECRET_KEY_PATTERN, (_match, prefix, quote) => {
|
|
186
|
-
return `${prefix}${quote}${REDACTION}${quote}`;
|
|
187
|
-
});
|
|
188
|
-
output = output.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, "Bearer [REDACTED]");
|
|
189
|
-
output = output.replace(/\/\/([^/\s:@]+):([^@\s/]+)@/g, "//[REDACTED]@");
|
|
190
|
-
for (const pattern of TOKEN_PATTERNS.slice(1, -1)) {
|
|
191
|
-
output = output.replace(pattern, REDACTION);
|
|
192
|
-
}
|
|
193
|
-
return output;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// src/core/instructions.ts
|
|
197
221
|
var INSTRUCTION_PATTERNS = [
|
|
198
222
|
"**/AGENTS.md",
|
|
199
223
|
"**/CLAUDE.md",
|
|
@@ -253,7 +277,7 @@ function instructionKind(path) {
|
|
|
253
277
|
|
|
254
278
|
// src/core/package-json.ts
|
|
255
279
|
import { access, readFile as readFile3 } from "fs/promises";
|
|
256
|
-
import { join } from "path";
|
|
280
|
+
import { join as join2 } from "path";
|
|
257
281
|
import { z } from "zod";
|
|
258
282
|
var PackageJsonSchema = z.object({
|
|
259
283
|
name: z.string().optional(),
|
|
@@ -263,7 +287,7 @@ var PackageJsonSchema = z.object({
|
|
|
263
287
|
var VERIFY_SCRIPT_ORDER = ["build", "test", "typecheck", "lint", "check", "verify", "ci"];
|
|
264
288
|
var VERIFY_SCRIPT_PREFIX = /^(build|test|typecheck|lint|check|verify|ci)(:|$)/;
|
|
265
289
|
async function detectPackageInfo(root) {
|
|
266
|
-
const packageJsonPath =
|
|
290
|
+
const packageJsonPath = join2(root, "package.json");
|
|
267
291
|
if (!await pathExists(packageJsonPath)) {
|
|
268
292
|
return void 0;
|
|
269
293
|
}
|
|
@@ -289,7 +313,7 @@ async function detectPackageManager(root, packageManagerField) {
|
|
|
289
313
|
["bun.lockb", "bun"]
|
|
290
314
|
];
|
|
291
315
|
for (const [lockfile, manager] of lockfiles) {
|
|
292
|
-
if (await pathExists(
|
|
316
|
+
if (await pathExists(join2(root, lockfile))) {
|
|
293
317
|
return manager;
|
|
294
318
|
}
|
|
295
319
|
}
|
|
@@ -320,29 +344,73 @@ function orderIndex(name) {
|
|
|
320
344
|
}
|
|
321
345
|
|
|
322
346
|
// src/core/risk.ts
|
|
347
|
+
var RISK_RULES = [
|
|
348
|
+
{
|
|
349
|
+
severity: "high",
|
|
350
|
+
title: "Security-sensitive code changed",
|
|
351
|
+
detail: "Review redaction, auth, token, or secret-handling changes carefully before handoff.",
|
|
352
|
+
matches: (file) => /(^|\/)(redact|secret|auth|token|security)/i.test(file)
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
severity: "high",
|
|
356
|
+
title: "Release or package publishing path changed",
|
|
357
|
+
detail: "Release and package changes can break install, provenance, or publish flow; run pnpm pack:dry-run and pnpm smoke:release before tagging or publishing.",
|
|
358
|
+
matches: isReleaseOrPackageFile
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
severity: "medium",
|
|
362
|
+
title: "CI workflow changed",
|
|
363
|
+
detail: "Workflow changes can fail only after push; confirm GitHub Actions still passes on the target branch.",
|
|
364
|
+
matches: (file) => file.startsWith(".github/workflows/")
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
severity: "medium",
|
|
368
|
+
title: "Build tooling or TypeScript config changed",
|
|
369
|
+
detail: "Tooling changes can break typecheck, lint, build output, or package entrypoints; run the full local check command.",
|
|
370
|
+
matches: isBuildToolingFile
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
severity: "medium",
|
|
374
|
+
title: "CLI behavior changed",
|
|
375
|
+
detail: "CLI entrypoint or command changes can break user-facing flags and output contracts; cover the changed command with unit or integration tests.",
|
|
376
|
+
matches: (file) => file.startsWith("src/cli/")
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
severity: "medium",
|
|
380
|
+
title: "Resume parsing changed",
|
|
381
|
+
detail: "Resume parser changes can drop handoff context; verify completed work, next steps, failures, and open questions are still extracted.",
|
|
382
|
+
matches: (file) => file === "src/core/resume.ts" || file.includes("/resume")
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
severity: "medium",
|
|
386
|
+
title: "Handoff report rendering changed",
|
|
387
|
+
detail: "Report rendering changes can hide critical context; verify Markdown and JSON output still include repository, verification, risk, and next-step sections.",
|
|
388
|
+
matches: (file) => file.startsWith("src/report/")
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
severity: "medium",
|
|
392
|
+
title: "Generated artifact or ignore policy changed",
|
|
393
|
+
detail: "Ignore/cache policy changes can pollute changedFiles or published packages; verify generated directories remain ignored and excluded from reports.",
|
|
394
|
+
matches: isGeneratedOrIgnorePolicyFile
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
severity: "low",
|
|
398
|
+
title: "Documentation changed",
|
|
399
|
+
detail: "Documentation-only changes still need examples, command names, and release instructions checked against the current CLI behavior.",
|
|
400
|
+
matches: isDocumentationFile
|
|
401
|
+
}
|
|
402
|
+
];
|
|
323
403
|
function analyzeRisk(report) {
|
|
324
404
|
const files = report.repository.changedFiles;
|
|
325
405
|
const notes = [];
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
notes.push({
|
|
335
|
-
severity: "medium",
|
|
336
|
-
title: "Dependency or package metadata changed",
|
|
337
|
-
detail: "Run install/build verification and check package publishing metadata."
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
if (files.some((file) => file.startsWith(".github/workflows/"))) {
|
|
341
|
-
notes.push({
|
|
342
|
-
severity: "medium",
|
|
343
|
-
title: "CI workflow changed",
|
|
344
|
-
detail: "Confirm GitHub Actions still passes after push."
|
|
345
|
-
});
|
|
406
|
+
for (const rule of RISK_RULES) {
|
|
407
|
+
if (files.some(rule.matches)) {
|
|
408
|
+
notes.push({
|
|
409
|
+
severity: rule.severity,
|
|
410
|
+
title: rule.title,
|
|
411
|
+
detail: rule.detail
|
|
412
|
+
});
|
|
413
|
+
}
|
|
346
414
|
}
|
|
347
415
|
const sourceFiles = files.filter((file) => file.startsWith("src/") && file.endsWith(".ts"));
|
|
348
416
|
const testFiles = files.filter((file) => file.startsWith("tests/") && file.endsWith(".test.ts"));
|
|
@@ -362,21 +430,34 @@ function analyzeRisk(report) {
|
|
|
362
430
|
}
|
|
363
431
|
return { notes };
|
|
364
432
|
}
|
|
433
|
+
function isReleaseOrPackageFile(file) {
|
|
434
|
+
return file === "package.json" || file === "CHANGELOG.md" || file === "docs/RELEASE.md" || file === "scripts/release-smoke.mjs" || /^\.github\/workflows\/.*release.*\.ya?ml$/i.test(file) || /(^|\/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lockb?)$/i.test(file);
|
|
435
|
+
}
|
|
436
|
+
function isBuildToolingFile(file) {
|
|
437
|
+
return /(^|\/)(tsconfig(?:\.[^/]*)?\.json|tsup\.config\.ts|vitest\.config\.ts|eslint\.config\.[cm]?[jt]s|pnpm-workspace\.yaml)$/i.test(file) || file.startsWith("scripts/");
|
|
438
|
+
}
|
|
439
|
+
function isGeneratedOrIgnorePolicyFile(file) {
|
|
440
|
+
return file === ".gitignore" || file === ".npmignore" || file.startsWith(".handoffkit/") || file.startsWith("docs/checkpoints/") || /(^|\/)(dist|coverage|node_modules|\.tmp-tests)\//.test(file);
|
|
441
|
+
}
|
|
442
|
+
function isDocumentationFile(file) {
|
|
443
|
+
return file === "README.md" || file === "ROADMAP.md" || file === "CONTRIBUTING.md" || file === "SECURITY.md" || file.startsWith("docs/");
|
|
444
|
+
}
|
|
365
445
|
|
|
366
446
|
// src/core/scanners.ts
|
|
367
447
|
import { mkdtemp, readFile as readFile4 } from "fs/promises";
|
|
368
448
|
import { tmpdir } from "os";
|
|
369
|
-
import { relative, join as
|
|
449
|
+
import { relative, join as join3 } from "path";
|
|
370
450
|
import { performance } from "perf_hooks";
|
|
371
451
|
import { execa as execa2 } from "execa";
|
|
452
|
+
import fg2 from "fast-glob";
|
|
372
453
|
var MAX_FINDINGS = 20;
|
|
373
454
|
var ERROR_LIMIT = 2e3;
|
|
374
|
-
async function detectSecretScanners() {
|
|
375
|
-
const [gitleaks, secretlint] = await Promise.all([scannerStatus("gitleaks"), scannerStatus("secretlint")]);
|
|
455
|
+
async function detectSecretScanners(root = process.cwd()) {
|
|
456
|
+
const [gitleaks, secretlint] = await Promise.all([scannerStatus("gitleaks", root), scannerStatus("secretlint", root)]);
|
|
376
457
|
return { scanners: [gitleaks, secretlint] };
|
|
377
458
|
}
|
|
378
459
|
async function runSecretScanners(root) {
|
|
379
|
-
const report = await detectSecretScanners();
|
|
460
|
+
const report = await detectSecretScanners(root);
|
|
380
461
|
const scans = await Promise.all(report.scanners.map((scanner) => runScanner(root, scanner)));
|
|
381
462
|
return { ...report, scans };
|
|
382
463
|
}
|
|
@@ -422,14 +503,18 @@ function normalizeSecretlintFindings(rawJson, limit = MAX_FINDINGS, root) {
|
|
|
422
503
|
}
|
|
423
504
|
return findings;
|
|
424
505
|
}
|
|
425
|
-
async function scannerStatus(name) {
|
|
506
|
+
async function scannerStatus(name, root) {
|
|
426
507
|
const result = await execa2(name, ["--version"], {
|
|
427
508
|
reject: false
|
|
428
509
|
}).catch(() => void 0);
|
|
510
|
+
const configFiles = await scannerConfigFiles(name, root);
|
|
429
511
|
return {
|
|
430
512
|
name,
|
|
431
513
|
available: Boolean(result && result.exitCode === 0),
|
|
432
|
-
...result?.stdout ? { version: result.stdout.trim() } : {}
|
|
514
|
+
...result?.stdout ? { version: result.stdout.trim() } : {},
|
|
515
|
+
configFiles,
|
|
516
|
+
configHint: configHint(name, configFiles),
|
|
517
|
+
installHint: installHint(name)
|
|
433
518
|
};
|
|
434
519
|
}
|
|
435
520
|
async function runScanner(root, scanner) {
|
|
@@ -447,8 +532,8 @@ async function runScanner(root, scanner) {
|
|
|
447
532
|
}
|
|
448
533
|
async function runGitleaks(root) {
|
|
449
534
|
const started = performance.now();
|
|
450
|
-
const tempDir = await mkdtemp(
|
|
451
|
-
const reportPath =
|
|
535
|
+
const tempDir = await mkdtemp(join3(tmpdir(), "handoffkit-gitleaks-"));
|
|
536
|
+
const reportPath = join3(tempDir, "report.json");
|
|
452
537
|
const result = await execa2(
|
|
453
538
|
"gitleaks",
|
|
454
539
|
["dir", root, "--no-banner", "--no-color", "--redact=100", "--report-format", "json", "--report-path", reportPath, "--max-target-megabytes", "2"],
|
|
@@ -518,6 +603,25 @@ function trimError(output) {
|
|
|
518
603
|
return trimmed.length > ERROR_LIMIT ? `${trimmed.slice(0, ERROR_LIMIT)}
|
|
519
604
|
[truncated]` : trimmed;
|
|
520
605
|
}
|
|
606
|
+
async function scannerConfigFiles(name, root) {
|
|
607
|
+
const patterns = name === "gitleaks" ? ["gitleaks.toml", ".gitleaks.toml", ".gitleaksignore", ".config/gitleaks/*.toml"] : [".secretlintrc", ".secretlintrc.*", "secretlint.config.*"];
|
|
608
|
+
const matches = await fg2(patterns, {
|
|
609
|
+
cwd: root,
|
|
610
|
+
dot: true,
|
|
611
|
+
onlyFiles: true,
|
|
612
|
+
unique: true
|
|
613
|
+
});
|
|
614
|
+
return matches.sort();
|
|
615
|
+
}
|
|
616
|
+
function configHint(name, configFiles) {
|
|
617
|
+
if (configFiles.length > 0) {
|
|
618
|
+
return `config: ${configFiles.join(", ")}`;
|
|
619
|
+
}
|
|
620
|
+
return name === "gitleaks" ? "config: none detected; optional files include .gitleaks.toml, gitleaks.toml, or .config/gitleaks/*.toml" : "config: none detected; optional files include .secretlintrc.*, .secretlintrc, or secretlint.config.*";
|
|
621
|
+
}
|
|
622
|
+
function installHint(name) {
|
|
623
|
+
return name === "gitleaks" ? "Install gitleaks from https://github.com/gitleaks/gitleaks, then rerun with --scan-secrets." : "Install secretlint from https://github.com/secretlint/secretlint, then rerun with --scan-secrets.";
|
|
624
|
+
}
|
|
521
625
|
|
|
522
626
|
// src/core/verify.ts
|
|
523
627
|
import { performance as performance2 } from "perf_hooks";
|
|
@@ -572,7 +676,7 @@ async function collectHandoffReport(options) {
|
|
|
572
676
|
}),
|
|
573
677
|
detectInstructionFiles(root),
|
|
574
678
|
detectPackageInfo(root),
|
|
575
|
-
options.scanSecrets ? runSecretScanners(root) : detectSecretScanners()
|
|
679
|
+
options.scanSecrets ? runSecretScanners(root) : detectSecretScanners(root)
|
|
576
680
|
]);
|
|
577
681
|
const report = {
|
|
578
682
|
goal: options.goal,
|
|
@@ -594,7 +698,7 @@ async function collectHandoffReport(options) {
|
|
|
594
698
|
}
|
|
595
699
|
|
|
596
700
|
// src/cli/output.ts
|
|
597
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
701
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
598
702
|
import { dirname, resolve } from "path";
|
|
599
703
|
|
|
600
704
|
// src/core/budget.ts
|
|
@@ -625,84 +729,176 @@ function renderJsonReport(report) {
|
|
|
625
729
|
`;
|
|
626
730
|
}
|
|
627
731
|
|
|
732
|
+
// src/report/profiles.ts
|
|
733
|
+
var genericOrder = [
|
|
734
|
+
"goal",
|
|
735
|
+
"repository",
|
|
736
|
+
"gitStatus",
|
|
737
|
+
"recentCommits",
|
|
738
|
+
"changedFiles",
|
|
739
|
+
"branchDelta",
|
|
740
|
+
"diffSummary",
|
|
741
|
+
"includedBranchDelta",
|
|
742
|
+
"includedDiff",
|
|
743
|
+
"instructionFiles",
|
|
744
|
+
"package",
|
|
745
|
+
"resume",
|
|
746
|
+
"verification",
|
|
747
|
+
"risk",
|
|
748
|
+
"secretScanning"
|
|
749
|
+
];
|
|
750
|
+
var profiles = {
|
|
751
|
+
generic: {
|
|
752
|
+
title: "Handoff Packet",
|
|
753
|
+
sectionOrder: genericOrder,
|
|
754
|
+
nextAgentNotes: [
|
|
755
|
+
"Use this packet as the starting context for the next coding session.",
|
|
756
|
+
"Verify commands locally before claiming completion."
|
|
757
|
+
]
|
|
758
|
+
},
|
|
759
|
+
codex: {
|
|
760
|
+
title: "Codex Handoff Packet",
|
|
761
|
+
sectionOrder: [
|
|
762
|
+
"goal",
|
|
763
|
+
"repository",
|
|
764
|
+
"gitStatus",
|
|
765
|
+
"changedFiles",
|
|
766
|
+
"verification",
|
|
767
|
+
"risk",
|
|
768
|
+
"branchDelta",
|
|
769
|
+
"diffSummary",
|
|
770
|
+
"includedBranchDelta",
|
|
771
|
+
"includedDiff",
|
|
772
|
+
"instructionFiles",
|
|
773
|
+
"package",
|
|
774
|
+
"resume",
|
|
775
|
+
"secretScanning",
|
|
776
|
+
"recentCommits"
|
|
777
|
+
],
|
|
778
|
+
nextAgentNotes: [
|
|
779
|
+
"Start by reading the goal, repository status, changed files, and verification state.",
|
|
780
|
+
"Use local tools to inspect files before editing; do not assume hidden context.",
|
|
781
|
+
"Keep edits scoped and rerun the relevant verification before reporting completion."
|
|
782
|
+
]
|
|
783
|
+
},
|
|
784
|
+
claude: {
|
|
785
|
+
title: "Claude Code Handoff Packet",
|
|
786
|
+
sectionOrder: [
|
|
787
|
+
"goal",
|
|
788
|
+
"resume",
|
|
789
|
+
"repository",
|
|
790
|
+
"verification",
|
|
791
|
+
"risk",
|
|
792
|
+
"changedFiles",
|
|
793
|
+
"gitStatus",
|
|
794
|
+
"branchDelta",
|
|
795
|
+
"diffSummary",
|
|
796
|
+
"includedBranchDelta",
|
|
797
|
+
"includedDiff",
|
|
798
|
+
"instructionFiles",
|
|
799
|
+
"package",
|
|
800
|
+
"secretScanning",
|
|
801
|
+
"recentCommits"
|
|
802
|
+
],
|
|
803
|
+
nextAgentNotes: [
|
|
804
|
+
"Treat this as concise project memory plus current branch state.",
|
|
805
|
+
"Use the resume state to separate completed work from remaining work.",
|
|
806
|
+
"Ask for clarification only when the packet leaves a blocking ambiguity."
|
|
807
|
+
]
|
|
808
|
+
},
|
|
809
|
+
cursor: {
|
|
810
|
+
title: "Cursor Handoff Packet",
|
|
811
|
+
sectionOrder: [
|
|
812
|
+
"goal",
|
|
813
|
+
"repository",
|
|
814
|
+
"changedFiles",
|
|
815
|
+
"gitStatus",
|
|
816
|
+
"includedDiff",
|
|
817
|
+
"diffSummary",
|
|
818
|
+
"branchDelta",
|
|
819
|
+
"includedBranchDelta",
|
|
820
|
+
"instructionFiles",
|
|
821
|
+
"package",
|
|
822
|
+
"verification",
|
|
823
|
+
"risk",
|
|
824
|
+
"resume",
|
|
825
|
+
"secretScanning",
|
|
826
|
+
"recentCommits"
|
|
827
|
+
],
|
|
828
|
+
nextAgentNotes: [
|
|
829
|
+
"Open the changed files first to build editor context.",
|
|
830
|
+
"Use instruction files and package scripts to keep edits aligned with the workspace.",
|
|
831
|
+
"Prefer small edits and rerun the detected verification scripts."
|
|
832
|
+
]
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
function profileForTarget(target) {
|
|
836
|
+
return profiles[target] ?? profiles.generic;
|
|
837
|
+
}
|
|
838
|
+
|
|
628
839
|
// src/report/markdown.ts
|
|
629
840
|
function renderMarkdownReport(report) {
|
|
841
|
+
const profile = profileForTarget(report.target);
|
|
630
842
|
const lines = [
|
|
631
|
-
`# ${
|
|
632
|
-
"",
|
|
633
|
-
"## Goal",
|
|
634
|
-
report.goal,
|
|
635
|
-
"",
|
|
636
|
-
"## Repository",
|
|
637
|
-
`- Repository: \`${report.repository.name}\``,
|
|
638
|
-
`- Branch: \`${report.repository.branch}\``,
|
|
639
|
-
`- Changed files: ${report.repository.changedFiles.length}`,
|
|
640
|
-
"",
|
|
641
|
-
"## Git Status",
|
|
642
|
-
codeBlock(report.repository.status || "Clean working tree."),
|
|
643
|
-
"",
|
|
644
|
-
"## Recent Commits",
|
|
645
|
-
listOrNone(report.repository.recentCommits.map((commit) => `- ${commit}`)),
|
|
646
|
-
"",
|
|
647
|
-
"## Changed Files",
|
|
648
|
-
listOrNone(report.repository.changedFiles.map((file) => `- \`${file}\``)),
|
|
649
|
-
"",
|
|
650
|
-
...renderBaseDiffSummary(report),
|
|
651
|
-
"## Diff Summary",
|
|
652
|
-
"### Staged",
|
|
653
|
-
codeBlock(report.repository.stagedDiffSummary || "No staged diff."),
|
|
654
|
-
"",
|
|
655
|
-
"### Unstaged",
|
|
656
|
-
codeBlock(report.repository.unstagedDiffSummary || "No unstaged diff."),
|
|
657
|
-
"",
|
|
658
|
-
"## Instruction Files",
|
|
659
|
-
renderInstructionFiles(report.instructionFiles),
|
|
843
|
+
`# ${profile.title}`,
|
|
660
844
|
"",
|
|
661
|
-
|
|
662
|
-
renderPackage(report.packageInfo),
|
|
663
|
-
"",
|
|
664
|
-
...renderResumeSource(report),
|
|
665
|
-
...renderVerification(report),
|
|
666
|
-
...renderRisk(report),
|
|
667
|
-
...renderSecretScanning(report),
|
|
845
|
+
...profile.sectionOrder.flatMap((section) => renderSection(section, report)),
|
|
668
846
|
"## Next Agent Notes",
|
|
847
|
+
...profile.nextAgentNotes.map((note) => `- ${note}`),
|
|
669
848
|
"- This packet was generated from local git and filesystem state.",
|
|
670
849
|
"- Likely secrets were redacted from generated output.",
|
|
671
850
|
"- No LLM APIs were called."
|
|
672
851
|
];
|
|
673
|
-
if (report.repository.includeDiff && report.repository.diff) {
|
|
674
|
-
lines.splice(
|
|
675
|
-
lines.indexOf("## Instruction Files"),
|
|
676
|
-
0,
|
|
677
|
-
"## Included Diff",
|
|
678
|
-
"### Staged Patch",
|
|
679
|
-
codeBlock(report.repository.diff.staged || "No staged patch."),
|
|
680
|
-
"",
|
|
681
|
-
"### Unstaged Patch",
|
|
682
|
-
codeBlock(report.repository.diff.unstaged || "No unstaged patch."),
|
|
683
|
-
""
|
|
684
|
-
);
|
|
685
|
-
}
|
|
686
|
-
if (report.repository.includeDiff && report.repository.baseDiff) {
|
|
687
|
-
lines.splice(
|
|
688
|
-
lines.indexOf("## Included Diff"),
|
|
689
|
-
0,
|
|
690
|
-
`## Included Branch Delta Since \`${report.repository.baseRef}\``,
|
|
691
|
-
codeBlock(report.repository.baseDiff),
|
|
692
|
-
""
|
|
693
|
-
);
|
|
694
|
-
}
|
|
695
852
|
return `${lines.join("\n")}
|
|
696
853
|
`;
|
|
697
854
|
}
|
|
698
|
-
function
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
855
|
+
function renderSection(section, report) {
|
|
856
|
+
switch (section) {
|
|
857
|
+
case "goal":
|
|
858
|
+
return ["## Goal", report.goal, ""];
|
|
859
|
+
case "repository":
|
|
860
|
+
return [
|
|
861
|
+
"## Repository",
|
|
862
|
+
`- Repository: \`${report.repository.name}\``,
|
|
863
|
+
`- Branch: \`${report.repository.branch}\``,
|
|
864
|
+
`- Changed files: ${report.repository.changedFiles.length}`,
|
|
865
|
+
""
|
|
866
|
+
];
|
|
867
|
+
case "gitStatus":
|
|
868
|
+
return ["## Git Status", codeBlock(report.repository.status || "Clean working tree."), ""];
|
|
869
|
+
case "recentCommits":
|
|
870
|
+
return ["## Recent Commits", listOrNone(report.repository.recentCommits.map((commit) => `- ${commit}`)), ""];
|
|
871
|
+
case "changedFiles":
|
|
872
|
+
return ["## Changed Files", listOrNone(report.repository.changedFiles.map((file) => `- \`${file}\``)), ""];
|
|
873
|
+
case "branchDelta":
|
|
874
|
+
return renderBaseDiffSummary(report);
|
|
875
|
+
case "diffSummary":
|
|
876
|
+
return [
|
|
877
|
+
"## Diff Summary",
|
|
878
|
+
"### Staged",
|
|
879
|
+
codeBlock(report.repository.stagedDiffSummary || "No staged diff."),
|
|
880
|
+
"",
|
|
881
|
+
"### Unstaged",
|
|
882
|
+
codeBlock(report.repository.unstagedDiffSummary || "No unstaged diff."),
|
|
883
|
+
""
|
|
884
|
+
];
|
|
885
|
+
case "includedBranchDelta":
|
|
886
|
+
return renderIncludedBranchDelta(report);
|
|
887
|
+
case "includedDiff":
|
|
888
|
+
return renderIncludedDiff(report);
|
|
889
|
+
case "instructionFiles":
|
|
890
|
+
return ["## Instruction Files", renderInstructionFiles(report.instructionFiles), ""];
|
|
891
|
+
case "package":
|
|
892
|
+
return ["## Package", renderPackage(report.packageInfo), ""];
|
|
893
|
+
case "resume":
|
|
894
|
+
return renderResumeSource(report);
|
|
895
|
+
case "verification":
|
|
896
|
+
return renderVerification(report);
|
|
897
|
+
case "risk":
|
|
898
|
+
return renderRisk(report);
|
|
899
|
+
case "secretScanning":
|
|
900
|
+
return renderSecretScanning(report);
|
|
901
|
+
}
|
|
706
902
|
}
|
|
707
903
|
function renderBaseDiffSummary(report) {
|
|
708
904
|
if (!report.repository.baseRef) {
|
|
@@ -714,6 +910,30 @@ function renderBaseDiffSummary(report) {
|
|
|
714
910
|
""
|
|
715
911
|
];
|
|
716
912
|
}
|
|
913
|
+
function renderIncludedBranchDelta(report) {
|
|
914
|
+
if (!report.repository.includeDiff || !report.repository.baseDiff) {
|
|
915
|
+
return [];
|
|
916
|
+
}
|
|
917
|
+
return [
|
|
918
|
+
`## Included Branch Delta Since \`${report.repository.baseRef}\``,
|
|
919
|
+
codeBlock(report.repository.baseDiff),
|
|
920
|
+
""
|
|
921
|
+
];
|
|
922
|
+
}
|
|
923
|
+
function renderIncludedDiff(report) {
|
|
924
|
+
if (!report.repository.includeDiff || !report.repository.diff) {
|
|
925
|
+
return [];
|
|
926
|
+
}
|
|
927
|
+
return [
|
|
928
|
+
"## Included Diff",
|
|
929
|
+
"### Staged Patch",
|
|
930
|
+
codeBlock(report.repository.diff.staged || "No staged patch."),
|
|
931
|
+
"",
|
|
932
|
+
"### Unstaged Patch",
|
|
933
|
+
codeBlock(report.repository.diff.unstaged || "No unstaged patch."),
|
|
934
|
+
""
|
|
935
|
+
];
|
|
936
|
+
}
|
|
717
937
|
function renderPackage(packageInfo) {
|
|
718
938
|
if (!packageInfo) {
|
|
719
939
|
return "No package.json detected.";
|
|
@@ -801,12 +1021,16 @@ function renderSecretScanning(report) {
|
|
|
801
1021
|
}
|
|
802
1022
|
function renderSecretScannerReport(secretScanning) {
|
|
803
1023
|
if (!secretScanning.scans) {
|
|
804
|
-
return secretScanning.scanners.map((scanner) => `- ${scanner
|
|
1024
|
+
return secretScanning.scanners.map((scanner) => `- ${scannerStatusLine(scanner)}`).join("\n");
|
|
805
1025
|
}
|
|
806
1026
|
return secretScanning.scans.map((scan) => {
|
|
1027
|
+
const status = secretScanning.scanners.find((scanner) => scanner.name === scan.name);
|
|
807
1028
|
const lines = [
|
|
808
1029
|
`- ${scan.name}: ${scan.ran ? `${scan.findings.length} finding(s), exit ${scan.exitCode}` : scan.error ?? "not run"}`
|
|
809
1030
|
];
|
|
1031
|
+
if (status) {
|
|
1032
|
+
lines.push(...scannerGuidanceLines(status));
|
|
1033
|
+
}
|
|
810
1034
|
for (const finding of scan.findings) {
|
|
811
1035
|
lines.push(` - ${finding.ruleId ? `${finding.ruleId}: ` : ""}${finding.message}${finding.file ? ` (${finding.file}${finding.line ? `:${finding.line}` : ""})` : ""}`);
|
|
812
1036
|
}
|
|
@@ -816,6 +1040,23 @@ function renderSecretScannerReport(secretScanning) {
|
|
|
816
1040
|
return lines.join("\n");
|
|
817
1041
|
}).join("\n");
|
|
818
1042
|
}
|
|
1043
|
+
function scannerStatusLine(scanner) {
|
|
1044
|
+
const config = scanner.configFiles.length > 0 ? `; config: ${scanner.configFiles.join(", ")}` : scanner.available ? "" : `; ${scanner.configHint}`;
|
|
1045
|
+
const install = scanner.available ? "" : `; ${scanner.installHint}`;
|
|
1046
|
+
return `${scanner.name}: ${scanner.available ? "available" : "not found"}${config}${install}`;
|
|
1047
|
+
}
|
|
1048
|
+
function scannerGuidanceLines(scanner) {
|
|
1049
|
+
const lines = [];
|
|
1050
|
+
if (scanner.configFiles.length > 0) {
|
|
1051
|
+
lines.push(` - config: ${scanner.configFiles.join(", ")}`);
|
|
1052
|
+
} else if (!scanner.available) {
|
|
1053
|
+
lines.push(` - ${scanner.configHint}`);
|
|
1054
|
+
}
|
|
1055
|
+
if (!scanner.available) {
|
|
1056
|
+
lines.push(` - ${scanner.installHint}`);
|
|
1057
|
+
}
|
|
1058
|
+
return lines;
|
|
1059
|
+
}
|
|
819
1060
|
function codeBlock(text) {
|
|
820
1061
|
return ["```text", text, "```"].join("\n");
|
|
821
1062
|
}
|
|
@@ -828,8 +1069,8 @@ async function writeRenderedReport(report, format, budget, output) {
|
|
|
828
1069
|
const rendered = redactText(renderOutput(report, format, budget));
|
|
829
1070
|
if (output) {
|
|
830
1071
|
const outputPath = resolve(process.cwd(), output);
|
|
831
|
-
await
|
|
832
|
-
await
|
|
1072
|
+
await mkdir2(dirname(outputPath), { recursive: true });
|
|
1073
|
+
await writeFile2(outputPath, rendered, "utf8");
|
|
833
1074
|
process.stderr.write(`Wrote handoff packet to ${outputPath}
|
|
834
1075
|
`);
|
|
835
1076
|
return;
|
|
@@ -863,11 +1104,13 @@ var PackCliOptionsSchema = z2.object({
|
|
|
863
1104
|
diff: z2.boolean().default(true),
|
|
864
1105
|
since: z2.string().trim().min(1).optional(),
|
|
865
1106
|
verify: z2.boolean().default(false),
|
|
866
|
-
scanSecrets: z2.boolean().default(false)
|
|
1107
|
+
scanSecrets: z2.boolean().default(false),
|
|
1108
|
+
cache: z2.boolean().default(false)
|
|
867
1109
|
});
|
|
868
1110
|
function createPackCommand() {
|
|
869
|
-
return new Command("pack").description("Create a safe local handoff packet for another AI assistant.").summary("Create a Markdown or JSON packet from the current git state.").option("--goal <text>", "handoff goal", "Make your own goal").option("--output <path>", "write output to a file instead of stdout").option("--format <format>", "output format: markdown or json", "markdown").option("--for <agent>", "target output: generic, codex, claude, or cursor", "generic").option("--budget <tokens>", "rough output token budget", parseBudget, 4e3).option("--since <ref>", "focus committed branch delta on a base ref").option("--verify", "run safe verification scripts and include results").option("--scan-secrets", "run optional local secret scanners and include bounded results").option("--include-diff", "include full staged and unstaged patches", false).option("--no-diff", "omit diff summaries and full patches").action(async (rawOptions) => {
|
|
1111
|
+
return new Command("pack").description("Create a safe local handoff packet for another AI assistant.").summary("Create a Markdown or JSON packet from the current git state.").option("--goal <text>", "handoff goal", "Make your own goal").option("--output <path>", "write output to a file instead of stdout").option("--format <format>", "output format: markdown or json", "markdown").option("--for <agent>", "target output: generic, codex, claude, or cursor", "generic").option("--budget <tokens>", "rough output token budget", parseBudget, 4e3).option("--since <ref>", "focus committed branch delta on a base ref").option("--verify", "run safe verification scripts and include results").option("--scan-secrets", "run optional local secret scanners and include bounded results").option("--cache", "write explicit local cache artifacts under .handoffkit when available").option("--include-diff", "include full staged and unstaged patches", false).option("--no-diff", "omit diff summaries and full patches").action(async (rawOptions) => {
|
|
870
1112
|
const options = parseOptions(rawOptions);
|
|
1113
|
+
const root = options.cache ? await findGitRoot(process.cwd()) : void 0;
|
|
871
1114
|
const report = await collectHandoffReport({
|
|
872
1115
|
goal: options.goal,
|
|
873
1116
|
cwd: process.cwd(),
|
|
@@ -881,6 +1124,15 @@ function createPackCommand() {
|
|
|
881
1124
|
includeVerification: options.verify,
|
|
882
1125
|
scanSecrets: options.scanSecrets
|
|
883
1126
|
});
|
|
1127
|
+
if (options.cache && root && report.verification) {
|
|
1128
|
+
const cache = await writeCacheArtifact(root, "verification", {
|
|
1129
|
+
goal: report.goal,
|
|
1130
|
+
target: report.target,
|
|
1131
|
+
verification: report.verification
|
|
1132
|
+
});
|
|
1133
|
+
process.stderr.write(`Wrote verification cache to ${cache.latestPath}
|
|
1134
|
+
`);
|
|
1135
|
+
}
|
|
884
1136
|
await writeRenderedReport(report, options.format, options.budget, options.output);
|
|
885
1137
|
});
|
|
886
1138
|
}
|
|
@@ -909,11 +1161,20 @@ import { z as z3 } from "zod";
|
|
|
909
1161
|
// src/core/resume.ts
|
|
910
1162
|
var RESUME_PREVIEW_LIMIT = 3e3;
|
|
911
1163
|
var SECTION_ALIASES = {
|
|
912
|
-
completed: [/^completed$/i, /^done$/i, /^done this session$/i, /^what changed$/i, /^implemented$/i],
|
|
913
|
-
remaining: [/^remaining$/i, /^next steps$/i, /^todo$/i, /^to do$/i],
|
|
914
|
-
failedCommands: [/^failed commands$/i, /^failures$/i, /^errors$/i],
|
|
915
|
-
openQuestions: [
|
|
916
|
-
|
|
1164
|
+
completed: [/^completed$/i, /^completed work$/i, /^done$/i, /^done this session$/i, /^what changed$/i, /^what i changed$/i, /^implemented$/i],
|
|
1165
|
+
remaining: [/^remaining$/i, /^remaining work$/i, /^next steps$/i, /^next action$/i, /^next safest action$/i, /^todo$/i, /^to do$/i],
|
|
1166
|
+
failedCommands: [/^failed command$/i, /^failed commands$/i, /^command failed$/i, /^commands failed$/i, /^failure$/i, /^failures$/i, /^error$/i, /^errors$/i],
|
|
1167
|
+
openQuestions: [
|
|
1168
|
+
/^open question$/i,
|
|
1169
|
+
/^open questions$/i,
|
|
1170
|
+
/^open questions \/ risks$/i,
|
|
1171
|
+
/^open questions and risks$/i,
|
|
1172
|
+
/^question$/i,
|
|
1173
|
+
/^questions$/i,
|
|
1174
|
+
/^blocker$/i,
|
|
1175
|
+
/^blockers$/i
|
|
1176
|
+
],
|
|
1177
|
+
verification: [/^verification$/i, /^tests$/i, /^tests run$/i, /^validation$/i]
|
|
917
1178
|
};
|
|
918
1179
|
function createResumeSource(path, content) {
|
|
919
1180
|
const normalized = content.replace(/\r\n/g, "\n").trim();
|
|
@@ -948,12 +1209,22 @@ function parseResumeState(content) {
|
|
|
948
1209
|
section = sectionForHeading(heading);
|
|
949
1210
|
continue;
|
|
950
1211
|
}
|
|
1212
|
+
const transcriptLine = stripTranscriptPrefix(line);
|
|
1213
|
+
const labeled = parseLabeledTranscriptLine(transcriptLine);
|
|
1214
|
+
if (labeled) {
|
|
1215
|
+
heading = labeled.heading;
|
|
1216
|
+
section = labeled.section;
|
|
1217
|
+
if (labeled.item) {
|
|
1218
|
+
appendResumeItem(state, section, labeled.item, heading);
|
|
1219
|
+
}
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
951
1222
|
if (!section) {
|
|
952
1223
|
continue;
|
|
953
1224
|
}
|
|
954
|
-
const item = normalizeListItem(
|
|
1225
|
+
const item = normalizeListItem(transcriptLine);
|
|
955
1226
|
if (item) {
|
|
956
|
-
state
|
|
1227
|
+
appendResumeItem(state, section, item, heading);
|
|
957
1228
|
}
|
|
958
1229
|
}
|
|
959
1230
|
const next = state.remaining[0] ?? state.openQuestions[0] ?? state.failedCommands[0];
|
|
@@ -971,10 +1242,41 @@ function sectionForHeading(heading) {
|
|
|
971
1242
|
}
|
|
972
1243
|
return void 0;
|
|
973
1244
|
}
|
|
1245
|
+
function parseLabeledTranscriptLine(line) {
|
|
1246
|
+
const match = line.match(/^([^:]{1,80}):(?:\s*(.*))?$/);
|
|
1247
|
+
if (!match?.[1]) {
|
|
1248
|
+
return void 0;
|
|
1249
|
+
}
|
|
1250
|
+
const heading = match[1].trim();
|
|
1251
|
+
const section = sectionForHeading(heading);
|
|
1252
|
+
if (!section) {
|
|
1253
|
+
return void 0;
|
|
1254
|
+
}
|
|
1255
|
+
const item = match[2]?.trim();
|
|
1256
|
+
return {
|
|
1257
|
+
section,
|
|
1258
|
+
heading,
|
|
1259
|
+
...item ? { item } : {}
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
function appendResumeItem(state, section, item, heading) {
|
|
1263
|
+
state[section].push({ text: redactText(item), ...heading ? { sourceHeading: redactText(heading) } : {} });
|
|
1264
|
+
}
|
|
974
1265
|
function normalizeListItem(line) {
|
|
975
1266
|
const match = line.match(/^[-*]\s+(.+)$/) ?? line.match(/^\d+\.\s+(.+)$/);
|
|
976
1267
|
return match?.[1]?.trim();
|
|
977
1268
|
}
|
|
1269
|
+
function stripTranscriptPrefix(line) {
|
|
1270
|
+
let current = line.trim();
|
|
1271
|
+
for (let i = 0; i < 4; i += 1) {
|
|
1272
|
+
const next = current.replace(/^\[[^\]\n]{1,60}\]\s*/, "").replace(/^(?:user|assistant|system|developer|tool|terminal|command|cmd|result|codex|claude|cursor|gemini)(?:\s*\([^)]*\))?\s*[:>]\s*/i, "").trim();
|
|
1273
|
+
if (next === current) {
|
|
1274
|
+
return current;
|
|
1275
|
+
}
|
|
1276
|
+
current = next;
|
|
1277
|
+
}
|
|
1278
|
+
return current;
|
|
1279
|
+
}
|
|
978
1280
|
function normalizeHeading(heading) {
|
|
979
1281
|
return heading.trim().replace(/:$/, "").replace(/\s*\/\s*/g, " / ").replace(/\s+/g, " ");
|
|
980
1282
|
}
|
|
@@ -988,12 +1290,14 @@ var ResumeOptionsSchema = z3.object({
|
|
|
988
1290
|
output: z3.string().optional(),
|
|
989
1291
|
format: z3.enum(["markdown", "json"]).default("markdown"),
|
|
990
1292
|
for: z3.enum(["generic", "codex", "claude", "cursor"]).default("generic"),
|
|
991
|
-
budget: z3.number().int().positive().default(4e3)
|
|
1293
|
+
budget: z3.number().int().positive().default(4e3),
|
|
1294
|
+
cache: z3.boolean().default(false)
|
|
992
1295
|
});
|
|
993
1296
|
function createResumeCommand() {
|
|
994
|
-
return new Command2("resume").description("Create a fresh handoff packet using a previous handoff as resume context.").summary("Merge a previous handoff or transcript with fresh repo state.").argument("<path>", "previous handoff or transcript file").option("--goal <text>", "new handoff goal", "Resume interrupted AI coding session").option("--output <path>", "write output to a file instead of stdout").option("--format <format>", "output format: markdown or json", "markdown").option("--for <agent>", "target output: generic, codex, claude, or cursor", "generic").option("--budget <tokens>", "rough output token budget", parseBudget2, 4e3).action(async (path, rawOptions) => {
|
|
1297
|
+
return new Command2("resume").description("Create a fresh handoff packet using a previous handoff as resume context.").summary("Merge a previous handoff or transcript with fresh repo state.").argument("<path>", "previous handoff or transcript file").option("--goal <text>", "new handoff goal", "Resume interrupted AI coding session").option("--output <path>", "write output to a file instead of stdout").option("--format <format>", "output format: markdown or json", "markdown").option("--for <agent>", "target output: generic, codex, claude, or cursor", "generic").option("--budget <tokens>", "rough output token budget", parseBudget2, 4e3).option("--cache", "write a local resume artifact under .handoffkit/resume").action(async (path, rawOptions) => {
|
|
995
1298
|
const options = ResumeOptionsSchema.parse(rawOptions);
|
|
996
1299
|
const source = createResumeSource(path, await readFile5(path, "utf8"));
|
|
1300
|
+
const root = options.cache ? await findGitRoot(process.cwd()) : void 0;
|
|
997
1301
|
const report = await collectHandoffReport({
|
|
998
1302
|
goal: options.goal,
|
|
999
1303
|
cwd: process.cwd(),
|
|
@@ -1007,6 +1311,15 @@ function createResumeCommand() {
|
|
|
1007
1311
|
scanSecrets: false,
|
|
1008
1312
|
resumeSource: source
|
|
1009
1313
|
});
|
|
1314
|
+
if (options.cache && root) {
|
|
1315
|
+
const cache = await writeCacheArtifact(root, "resume", {
|
|
1316
|
+
goal: report.goal,
|
|
1317
|
+
target: report.target,
|
|
1318
|
+
source
|
|
1319
|
+
});
|
|
1320
|
+
process.stderr.write(`Wrote resume cache to ${cache.latestPath}
|
|
1321
|
+
`);
|
|
1322
|
+
}
|
|
1010
1323
|
await writeRenderedReport(report, options.format, options.budget, options.output);
|
|
1011
1324
|
});
|
|
1012
1325
|
}
|
|
@@ -1072,7 +1385,11 @@ function createScanSecretsCommand() {
|
|
|
1072
1385
|
function renderScanMarkdown(report) {
|
|
1073
1386
|
const lines = ["# Secret Scan Results", ""];
|
|
1074
1387
|
for (const scan of report.scans ?? []) {
|
|
1388
|
+
const status = report.scanners.find((scanner) => scanner.name === scan.name);
|
|
1075
1389
|
lines.push(`- ${scan.name}: ${scan.ran ? `${scan.findings.length} finding(s), exit ${scan.exitCode}` : scan.error ?? "not run"}`);
|
|
1390
|
+
if (status) {
|
|
1391
|
+
lines.push(...scannerGuidanceLines2(status));
|
|
1392
|
+
}
|
|
1076
1393
|
for (const finding of scan.findings) {
|
|
1077
1394
|
lines.push(` - ${finding.ruleId ? `${finding.ruleId}: ` : ""}${finding.message}${finding.file ? ` (${finding.file}${finding.line ? `:${finding.line}` : ""})` : ""}`);
|
|
1078
1395
|
}
|
|
@@ -1080,18 +1397,36 @@ function renderScanMarkdown(report) {
|
|
|
1080
1397
|
return `${lines.join("\n")}
|
|
1081
1398
|
`;
|
|
1082
1399
|
}
|
|
1400
|
+
function scannerGuidanceLines2(scanner) {
|
|
1401
|
+
const lines = [];
|
|
1402
|
+
if (scanner.configFiles.length > 0) {
|
|
1403
|
+
lines.push(` - config: ${scanner.configFiles.join(", ")}`);
|
|
1404
|
+
} else if (!scanner.available) {
|
|
1405
|
+
lines.push(` - ${scanner.configHint}`);
|
|
1406
|
+
}
|
|
1407
|
+
if (!scanner.available) {
|
|
1408
|
+
lines.push(` - ${scanner.installHint}`);
|
|
1409
|
+
}
|
|
1410
|
+
return lines;
|
|
1411
|
+
}
|
|
1083
1412
|
|
|
1084
1413
|
// src/cli/commands/verify.ts
|
|
1085
1414
|
import { Command as Command5 } from "commander";
|
|
1086
1415
|
import { z as z6 } from "zod";
|
|
1087
1416
|
var VerifyOptionsSchema = z6.object({
|
|
1088
|
-
format: z6.enum(["markdown", "json"]).default("markdown")
|
|
1417
|
+
format: z6.enum(["markdown", "json"]).default("markdown"),
|
|
1418
|
+
cache: z6.boolean().default(false)
|
|
1089
1419
|
});
|
|
1090
1420
|
function createVerifyCommand() {
|
|
1091
|
-
return new Command5("verify").description("Run safe local verification scripts.").summary("Run safe detected verification scripts.").option("--format <format>", "output format: markdown or json", "markdown").action(async (rawOptions) => {
|
|
1421
|
+
return new Command5("verify").description("Run safe local verification scripts.").summary("Run safe detected verification scripts.").option("--format <format>", "output format: markdown or json", "markdown").option("--cache", "write a local verification artifact under .handoffkit/verification").action(async (rawOptions) => {
|
|
1092
1422
|
const options = VerifyOptionsSchema.parse(rawOptions);
|
|
1093
1423
|
const root = await findGitRoot(process.cwd());
|
|
1094
1424
|
const verification = await runVerification(root);
|
|
1425
|
+
if (options.cache) {
|
|
1426
|
+
const cache = await writeCacheArtifact(root, "verification", verification);
|
|
1427
|
+
process.stderr.write(`Wrote verification cache to ${cache.latestPath}
|
|
1428
|
+
`);
|
|
1429
|
+
}
|
|
1095
1430
|
if (options.format === "json") {
|
|
1096
1431
|
process.stdout.write(redactText(`${JSON.stringify(verification, null, 2)}
|
|
1097
1432
|
`));
|
|
@@ -1114,7 +1449,7 @@ function renderVerificationMarkdown(commands) {
|
|
|
1114
1449
|
}
|
|
1115
1450
|
|
|
1116
1451
|
// src/cli/index.ts
|
|
1117
|
-
var program = new Command6().name("handoffkit").description("Create safe local handoff packets for AI-assisted coding sessions.").summary("Create local-first AI coding session handoff packets.").showHelpAfterError("(run with --help for usage)").version("0.
|
|
1452
|
+
var program = new Command6().name("handoffkit").description("Create safe local handoff packets for AI-assisted coding sessions.").summary("Create local-first AI coding session handoff packets.").showHelpAfterError("(run with --help for usage)").version("0.3.0");
|
|
1118
1453
|
program.addCommand(createPackCommand());
|
|
1119
1454
|
program.addCommand(createVerifyCommand());
|
|
1120
1455
|
program.addCommand(createRiskCommand());
|