@kingkyylian/handoffkit 0.2.0 → 0.4.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 +15 -0
- package/README.md +22 -1
- package/dist/index.js +471 -127
- package/dist/index.js.map +1 -1
- package/docs/CACHE.md +57 -0
- package/docs/RELEASE.md +3 -1
- package/examples/cache-backed-handoff.md +26 -0
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -1,15 +1,154 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command7 } from "commander";
|
|
5
5
|
|
|
6
|
-
// src/cli/commands/
|
|
6
|
+
// src/cli/commands/cache.ts
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
-
import { z
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
// src/core/cache.ts
|
|
11
|
+
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
|
|
12
|
+
import { basename, 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
|
+
var CACHE_KINDS = ["resume", "verification"];
|
|
48
|
+
async function writeCacheArtifact(root, kind, data, options = {}) {
|
|
49
|
+
const createdAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
50
|
+
const envelope = {
|
|
51
|
+
version: 1,
|
|
52
|
+
kind,
|
|
53
|
+
createdAt,
|
|
54
|
+
data
|
|
55
|
+
};
|
|
56
|
+
const cacheDir = join(root, ".handoffkit", kind);
|
|
57
|
+
const artifactPath = join(cacheDir, `${cacheTimestamp(createdAt)}.json`);
|
|
58
|
+
const latestPath = join(cacheDir, "latest.json");
|
|
59
|
+
const contents = `${redactText(JSON.stringify(envelope, null, 2))}
|
|
60
|
+
`;
|
|
61
|
+
await mkdir(cacheDir, { recursive: true });
|
|
62
|
+
await Promise.all([writeFile(artifactPath, contents, "utf8"), writeFile(latestPath, contents, "utf8")]);
|
|
63
|
+
return { artifactPath, latestPath };
|
|
64
|
+
}
|
|
65
|
+
async function listCacheArtifacts(root) {
|
|
66
|
+
const summaries = await Promise.all(CACHE_KINDS.map((kind) => listCacheKind(root, kind)));
|
|
67
|
+
return summaries.flat().sort((a, b) => b.createdAt.localeCompare(a.createdAt) || a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name));
|
|
68
|
+
}
|
|
69
|
+
async function readCacheArtifact(root, kind, name = "latest") {
|
|
70
|
+
const normalizedName = normalizeArtifactName(name);
|
|
71
|
+
const artifactPath = join(root, ".handoffkit", kind, `${normalizedName}.json`);
|
|
72
|
+
const envelope = JSON.parse(await readFile(artifactPath, "utf8"));
|
|
73
|
+
if (envelope.version !== 1 || envelope.kind !== kind || typeof envelope.createdAt !== "string") {
|
|
74
|
+
throw new Error(`Invalid cache artifact: .handoffkit/${kind}/${normalizedName}.json`);
|
|
75
|
+
}
|
|
76
|
+
return envelope;
|
|
77
|
+
}
|
|
78
|
+
async function readResumeSourceFromCache(root, ref) {
|
|
79
|
+
const { kind, name } = parseCacheRef(ref, "resume");
|
|
80
|
+
if (kind !== "resume") {
|
|
81
|
+
throw new Error("resume --from-cache only supports resume cache artifacts.");
|
|
82
|
+
}
|
|
83
|
+
const artifact = await readCacheArtifact(root, kind, name);
|
|
84
|
+
const source = resumeSourceFromArtifact(artifact);
|
|
85
|
+
if (!source) {
|
|
86
|
+
throw new Error(`Cache artifact does not contain a resume source: .handoffkit/${kind}/${name}.json`);
|
|
87
|
+
}
|
|
88
|
+
return source;
|
|
89
|
+
}
|
|
90
|
+
function parseCacheRef(ref, defaultKind) {
|
|
91
|
+
const normalized = ref.trim();
|
|
92
|
+
const parts = normalized.split("/");
|
|
93
|
+
if (parts.length === 1 && defaultKind) {
|
|
94
|
+
return { kind: defaultKind, name: normalizeArtifactName(parts[0] || "latest") };
|
|
95
|
+
}
|
|
96
|
+
const [kind, name] = parts;
|
|
97
|
+
if (parts.length === 2 && kind && isCacheArtifactKind(kind)) {
|
|
98
|
+
return { kind, name: normalizeArtifactName(name || "latest") };
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`Invalid cache ref: ${ref}`);
|
|
101
|
+
}
|
|
102
|
+
function resumeSourceFromArtifact(artifact) {
|
|
103
|
+
const data = artifact.data;
|
|
104
|
+
return data.source;
|
|
105
|
+
}
|
|
106
|
+
async function listCacheKind(root, kind) {
|
|
107
|
+
const cacheDir = join(root, ".handoffkit", kind);
|
|
108
|
+
let entries;
|
|
109
|
+
try {
|
|
110
|
+
entries = await readdir(cacheDir);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error.code === "ENOENT") {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
const artifacts = await Promise.all(
|
|
118
|
+
entries.filter((entry) => entry.endsWith(".json")).map(async (entry) => {
|
|
119
|
+
const name = basename(entry, ".json");
|
|
120
|
+
try {
|
|
121
|
+
const artifact = await readCacheArtifact(root, kind, name);
|
|
122
|
+
return {
|
|
123
|
+
kind,
|
|
124
|
+
name,
|
|
125
|
+
createdAt: artifact.createdAt,
|
|
126
|
+
path: `.handoffkit/${kind}/${entry}`
|
|
127
|
+
};
|
|
128
|
+
} catch {
|
|
129
|
+
return void 0;
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
return artifacts.filter((artifact) => Boolean(artifact));
|
|
134
|
+
}
|
|
135
|
+
function isCacheArtifactKind(value) {
|
|
136
|
+
return CACHE_KINDS.includes(value);
|
|
137
|
+
}
|
|
138
|
+
function normalizeArtifactName(name) {
|
|
139
|
+
const withoutExtension = name.replace(/\.json$/i, "");
|
|
140
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(withoutExtension)) {
|
|
141
|
+
throw new Error(`Invalid cache artifact name: ${name}`);
|
|
142
|
+
}
|
|
143
|
+
return withoutExtension;
|
|
144
|
+
}
|
|
145
|
+
function cacheTimestamp(timestamp) {
|
|
146
|
+
return timestamp.replace(/[:.]/g, "-");
|
|
147
|
+
}
|
|
9
148
|
|
|
10
149
|
// src/core/git.ts
|
|
11
|
-
import { readFile } from "fs/promises";
|
|
12
|
-
import { basename } from "path";
|
|
150
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
151
|
+
import { basename as basename2 } from "path";
|
|
13
152
|
import { execa } from "execa";
|
|
14
153
|
|
|
15
154
|
// src/cli/errors.ts
|
|
@@ -26,7 +165,7 @@ function formatCliError(error) {
|
|
|
26
165
|
|
|
27
166
|
// src/core/git.ts
|
|
28
167
|
var UNTRACKED_PATCH_CHAR_LIMIT = 2e4;
|
|
29
|
-
var IGNORED_CHANGED_PATH_PREFIXES = ["node_modules/", "dist/", "coverage/", ".git/"];
|
|
168
|
+
var IGNORED_CHANGED_PATH_PREFIXES = ["node_modules/", "dist/", "coverage/", ".git/", ".handoffkit/"];
|
|
30
169
|
async function findGitRoot(cwd) {
|
|
31
170
|
const result = await execa("git", ["rev-parse", "--show-toplevel"], {
|
|
32
171
|
cwd,
|
|
@@ -55,7 +194,7 @@ async function collectGitInfo(root, options) {
|
|
|
55
194
|
const unstagedDiffSummary = options.includeDiffSummary ? joinSections([trackedUnstagedDiffSummary, renderUntrackedSummary(untrackedFiles)]) : "";
|
|
56
195
|
const diff = options.includeDiff ? await collectDiff(root, untrackedFiles) : void 0;
|
|
57
196
|
return {
|
|
58
|
-
name:
|
|
197
|
+
name: basename2(root),
|
|
59
198
|
branch,
|
|
60
199
|
...options.since ? { baseRef: options.since } : {},
|
|
61
200
|
status,
|
|
@@ -139,7 +278,7 @@ function renderUntrackedSummary(files) {
|
|
|
139
278
|
async function untrackedPatch(root, files) {
|
|
140
279
|
const patches = await Promise.all(
|
|
141
280
|
files.map(async (file) => {
|
|
142
|
-
const content = await
|
|
281
|
+
const content = await readFile2(`${root}/${file}`, "utf8");
|
|
143
282
|
const trimmedContent = content.length > UNTRACKED_PATCH_CHAR_LIMIT ? `${content.slice(0, UNTRACKED_PATCH_CHAR_LIMIT).trimEnd()}
|
|
144
283
|
[truncated]` : content.trimEnd();
|
|
145
284
|
return [`Untracked file: ${file}`, "```text", trimmedContent, "```"].join("\n");
|
|
@@ -157,43 +296,74 @@ function isIgnoredChangedPath(file) {
|
|
|
157
296
|
return IGNORED_CHANGED_PATH_PREFIXES.some((prefix) => file === prefix.slice(0, -1) || file.startsWith(prefix));
|
|
158
297
|
}
|
|
159
298
|
|
|
160
|
-
// src/
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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}`;
|
|
299
|
+
// src/cli/commands/cache.ts
|
|
300
|
+
var CacheFormatOptionsSchema = z.object({
|
|
301
|
+
format: z.enum(["markdown", "json"]).default("markdown")
|
|
302
|
+
});
|
|
303
|
+
var CacheKindSchema = z.enum(["verification", "resume"]);
|
|
304
|
+
function createCacheCommand() {
|
|
305
|
+
return new Command("cache").description("Inspect local .handoffkit cache artifacts.").summary("List and show explicit local cache artifacts.").addCommand(createCacheListCommand()).addCommand(createCacheShowCommand());
|
|
306
|
+
}
|
|
307
|
+
function createCacheListCommand() {
|
|
308
|
+
return new Command("list").description("List local cache artifacts.").option("--format <format>", "output format: markdown or json", "markdown").action(async (rawOptions) => {
|
|
309
|
+
const options = CacheFormatOptionsSchema.parse(rawOptions);
|
|
310
|
+
const root = await findGitRoot(process.cwd());
|
|
311
|
+
const artifacts = await listCacheArtifacts(root);
|
|
312
|
+
if (options.format === "json") {
|
|
313
|
+
process.stdout.write(`${JSON.stringify({ artifacts }, null, 2)}
|
|
314
|
+
`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
process.stdout.write(renderCacheListMarkdown(artifacts));
|
|
187
318
|
});
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
319
|
+
}
|
|
320
|
+
function createCacheShowCommand() {
|
|
321
|
+
return new Command("show").description("Show one local cache artifact.").argument("<kind>", "cache kind: verification or resume").argument("[name]", "artifact name, defaults to latest", "latest").option("--format <format>", "output format: markdown or json", "markdown").action(async (kindInput, name, rawOptions) => {
|
|
322
|
+
const options = CacheFormatOptionsSchema.parse(rawOptions);
|
|
323
|
+
const kind = CacheKindSchema.parse(kindInput);
|
|
324
|
+
const root = await findGitRoot(process.cwd());
|
|
325
|
+
const artifact = await readCacheArtifact(root, kind, name);
|
|
326
|
+
if (options.format === "json") {
|
|
327
|
+
process.stdout.write(`${JSON.stringify(artifact, null, 2)}
|
|
328
|
+
`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
process.stdout.write(renderCacheArtifactMarkdown(artifact, kind, name));
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
function renderCacheListMarkdown(artifacts) {
|
|
335
|
+
const lines = ["# Cache Artifacts", ""];
|
|
336
|
+
if (artifacts.length === 0) {
|
|
337
|
+
lines.push("No cache artifacts found.");
|
|
338
|
+
} else {
|
|
339
|
+
for (const artifact of artifacts) {
|
|
340
|
+
lines.push(`- ${artifact.kind}/${artifact.name} (${artifact.createdAt}) - \`${artifact.path}\``);
|
|
341
|
+
}
|
|
192
342
|
}
|
|
193
|
-
return
|
|
343
|
+
return `${lines.join("\n")}
|
|
344
|
+
`;
|
|
194
345
|
}
|
|
346
|
+
function renderCacheArtifactMarkdown(artifact, kind, name) {
|
|
347
|
+
return [
|
|
348
|
+
"# Cache Artifact",
|
|
349
|
+
"",
|
|
350
|
+
`- Artifact: ${kind}/${name}`,
|
|
351
|
+
`- Created: ${artifact.createdAt}`,
|
|
352
|
+
"",
|
|
353
|
+
"```json",
|
|
354
|
+
JSON.stringify(artifact, null, 2),
|
|
355
|
+
"```",
|
|
356
|
+
""
|
|
357
|
+
].join("\n");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/cli/commands/pack.ts
|
|
361
|
+
import { Command as Command2 } from "commander";
|
|
362
|
+
import { z as z3 } from "zod";
|
|
195
363
|
|
|
196
364
|
// src/core/instructions.ts
|
|
365
|
+
import { readFile as readFile3, stat } from "fs/promises";
|
|
366
|
+
import fg from "fast-glob";
|
|
197
367
|
var INSTRUCTION_PATTERNS = [
|
|
198
368
|
"**/AGENTS.md",
|
|
199
369
|
"**/CLAUDE.md",
|
|
@@ -226,7 +396,7 @@ async function readPreview(root, path) {
|
|
|
226
396
|
if (!metadata.isFile()) {
|
|
227
397
|
return "Directory rule set detected.";
|
|
228
398
|
}
|
|
229
|
-
const content = await
|
|
399
|
+
const content = await readFile3(fullPath, "utf8");
|
|
230
400
|
const normalized = content.replace(/\r\n/g, "\n").trim();
|
|
231
401
|
const preview = normalized.length > PREVIEW_CHAR_LIMIT ? `${normalized.slice(0, PREVIEW_CHAR_LIMIT).trimEnd()}
|
|
232
402
|
[truncated]` : normalized;
|
|
@@ -252,22 +422,22 @@ function instructionKind(path) {
|
|
|
252
422
|
}
|
|
253
423
|
|
|
254
424
|
// src/core/package-json.ts
|
|
255
|
-
import { access, readFile as
|
|
256
|
-
import { join } from "path";
|
|
257
|
-
import { z } from "zod";
|
|
258
|
-
var PackageJsonSchema =
|
|
259
|
-
name:
|
|
260
|
-
packageManager:
|
|
261
|
-
scripts:
|
|
425
|
+
import { access, readFile as readFile4 } from "fs/promises";
|
|
426
|
+
import { join as join2 } from "path";
|
|
427
|
+
import { z as z2 } from "zod";
|
|
428
|
+
var PackageJsonSchema = z2.object({
|
|
429
|
+
name: z2.string().optional(),
|
|
430
|
+
packageManager: z2.string().optional(),
|
|
431
|
+
scripts: z2.record(z2.string(), z2.string()).optional()
|
|
262
432
|
});
|
|
263
433
|
var VERIFY_SCRIPT_ORDER = ["build", "test", "typecheck", "lint", "check", "verify", "ci"];
|
|
264
434
|
var VERIFY_SCRIPT_PREFIX = /^(build|test|typecheck|lint|check|verify|ci)(:|$)/;
|
|
265
435
|
async function detectPackageInfo(root) {
|
|
266
|
-
const packageJsonPath =
|
|
436
|
+
const packageJsonPath = join2(root, "package.json");
|
|
267
437
|
if (!await pathExists(packageJsonPath)) {
|
|
268
438
|
return void 0;
|
|
269
439
|
}
|
|
270
|
-
const rawPackageJson = await
|
|
440
|
+
const rawPackageJson = await readFile4(packageJsonPath, "utf8");
|
|
271
441
|
const packageJson = PackageJsonSchema.parse(JSON.parse(rawPackageJson));
|
|
272
442
|
const packageManager = await detectPackageManager(root, packageJson.packageManager);
|
|
273
443
|
const verificationScripts = detectVerificationScripts(packageJson.scripts ?? {});
|
|
@@ -289,7 +459,7 @@ async function detectPackageManager(root, packageManagerField) {
|
|
|
289
459
|
["bun.lockb", "bun"]
|
|
290
460
|
];
|
|
291
461
|
for (const [lockfile, manager] of lockfiles) {
|
|
292
|
-
if (await pathExists(
|
|
462
|
+
if (await pathExists(join2(root, lockfile))) {
|
|
293
463
|
return manager;
|
|
294
464
|
}
|
|
295
465
|
}
|
|
@@ -320,29 +490,73 @@ function orderIndex(name) {
|
|
|
320
490
|
}
|
|
321
491
|
|
|
322
492
|
// src/core/risk.ts
|
|
493
|
+
var RISK_RULES = [
|
|
494
|
+
{
|
|
495
|
+
severity: "high",
|
|
496
|
+
title: "Security-sensitive code changed",
|
|
497
|
+
detail: "Review redaction, auth, token, or secret-handling changes carefully before handoff.",
|
|
498
|
+
matches: (file) => /(^|\/)(redact|secret|auth|token|security)/i.test(file)
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
severity: "high",
|
|
502
|
+
title: "Release or package publishing path changed",
|
|
503
|
+
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.",
|
|
504
|
+
matches: isReleaseOrPackageFile
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
severity: "medium",
|
|
508
|
+
title: "CI workflow changed",
|
|
509
|
+
detail: "Workflow changes can fail only after push; confirm GitHub Actions still passes on the target branch.",
|
|
510
|
+
matches: (file) => file.startsWith(".github/workflows/")
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
severity: "medium",
|
|
514
|
+
title: "Build tooling or TypeScript config changed",
|
|
515
|
+
detail: "Tooling changes can break typecheck, lint, build output, or package entrypoints; run the full local check command.",
|
|
516
|
+
matches: isBuildToolingFile
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
severity: "medium",
|
|
520
|
+
title: "CLI behavior changed",
|
|
521
|
+
detail: "CLI entrypoint or command changes can break user-facing flags and output contracts; cover the changed command with unit or integration tests.",
|
|
522
|
+
matches: (file) => file.startsWith("src/cli/")
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
severity: "medium",
|
|
526
|
+
title: "Resume parsing changed",
|
|
527
|
+
detail: "Resume parser changes can drop handoff context; verify completed work, next steps, failures, and open questions are still extracted.",
|
|
528
|
+
matches: (file) => file === "src/core/resume.ts" || file.includes("/resume")
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
severity: "medium",
|
|
532
|
+
title: "Handoff report rendering changed",
|
|
533
|
+
detail: "Report rendering changes can hide critical context; verify Markdown and JSON output still include repository, verification, risk, and next-step sections.",
|
|
534
|
+
matches: (file) => file.startsWith("src/report/")
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
severity: "medium",
|
|
538
|
+
title: "Generated artifact or ignore policy changed",
|
|
539
|
+
detail: "Ignore/cache policy changes can pollute changedFiles or published packages; verify generated directories remain ignored and excluded from reports.",
|
|
540
|
+
matches: isGeneratedOrIgnorePolicyFile
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
severity: "low",
|
|
544
|
+
title: "Documentation changed",
|
|
545
|
+
detail: "Documentation-only changes still need examples, command names, and release instructions checked against the current CLI behavior.",
|
|
546
|
+
matches: isDocumentationFile
|
|
547
|
+
}
|
|
548
|
+
];
|
|
323
549
|
function analyzeRisk(report) {
|
|
324
550
|
const files = report.repository.changedFiles;
|
|
325
551
|
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
|
-
});
|
|
552
|
+
for (const rule of RISK_RULES) {
|
|
553
|
+
if (files.some(rule.matches)) {
|
|
554
|
+
notes.push({
|
|
555
|
+
severity: rule.severity,
|
|
556
|
+
title: rule.title,
|
|
557
|
+
detail: rule.detail
|
|
558
|
+
});
|
|
559
|
+
}
|
|
346
560
|
}
|
|
347
561
|
const sourceFiles = files.filter((file) => file.startsWith("src/") && file.endsWith(".ts"));
|
|
348
562
|
const testFiles = files.filter((file) => file.startsWith("tests/") && file.endsWith(".test.ts"));
|
|
@@ -362,11 +576,23 @@ function analyzeRisk(report) {
|
|
|
362
576
|
}
|
|
363
577
|
return { notes };
|
|
364
578
|
}
|
|
579
|
+
function isReleaseOrPackageFile(file) {
|
|
580
|
+
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);
|
|
581
|
+
}
|
|
582
|
+
function isBuildToolingFile(file) {
|
|
583
|
+
return /(^|\/)(tsconfig(?:\.[^/]*)?\.json|tsup\.config\.ts|vitest\.config\.ts|eslint\.config\.[cm]?[jt]s|pnpm-workspace\.yaml)$/i.test(file) || file.startsWith("scripts/");
|
|
584
|
+
}
|
|
585
|
+
function isGeneratedOrIgnorePolicyFile(file) {
|
|
586
|
+
return file === ".gitignore" || file === ".npmignore" || file.startsWith(".handoffkit/") || file.startsWith("docs/checkpoints/") || /(^|\/)(dist|coverage|node_modules|\.tmp-tests)\//.test(file);
|
|
587
|
+
}
|
|
588
|
+
function isDocumentationFile(file) {
|
|
589
|
+
return file === "README.md" || file === "ROADMAP.md" || file === "CONTRIBUTING.md" || file === "SECURITY.md" || file.startsWith("docs/");
|
|
590
|
+
}
|
|
365
591
|
|
|
366
592
|
// src/core/scanners.ts
|
|
367
|
-
import { mkdtemp, readFile as
|
|
593
|
+
import { mkdtemp, readFile as readFile5 } from "fs/promises";
|
|
368
594
|
import { tmpdir } from "os";
|
|
369
|
-
import { relative, join as
|
|
595
|
+
import { relative, join as join3 } from "path";
|
|
370
596
|
import { performance } from "perf_hooks";
|
|
371
597
|
import { execa as execa2 } from "execa";
|
|
372
598
|
import fg2 from "fast-glob";
|
|
@@ -452,14 +678,14 @@ async function runScanner(root, scanner) {
|
|
|
452
678
|
}
|
|
453
679
|
async function runGitleaks(root) {
|
|
454
680
|
const started = performance.now();
|
|
455
|
-
const tempDir = await mkdtemp(
|
|
456
|
-
const reportPath =
|
|
681
|
+
const tempDir = await mkdtemp(join3(tmpdir(), "handoffkit-gitleaks-"));
|
|
682
|
+
const reportPath = join3(tempDir, "report.json");
|
|
457
683
|
const result = await execa2(
|
|
458
684
|
"gitleaks",
|
|
459
685
|
["dir", root, "--no-banner", "--no-color", "--redact=100", "--report-format", "json", "--report-path", reportPath, "--max-target-megabytes", "2"],
|
|
460
686
|
{ reject: false, all: true }
|
|
461
687
|
);
|
|
462
|
-
const rawReport = await
|
|
688
|
+
const rawReport = await readFile5(reportPath, "utf8").catch(() => "[]");
|
|
463
689
|
const findings = normalizeGitleaksFindings(rawReport);
|
|
464
690
|
return {
|
|
465
691
|
name: "gitleaks",
|
|
@@ -606,6 +832,7 @@ async function collectHandoffReport(options) {
|
|
|
606
832
|
...packageInfo ? { packageInfo } : {},
|
|
607
833
|
...options.resumeSource ? { resumeSource: options.resumeSource } : {},
|
|
608
834
|
...options.includeVerification ? { verification: await runVerification(root) } : {},
|
|
835
|
+
...options.includeCache ? { cache: { artifacts: await listCacheArtifacts(root) } } : {},
|
|
609
836
|
secretScanning,
|
|
610
837
|
budget: {
|
|
611
838
|
requestedTokens: options.budget,
|
|
@@ -618,7 +845,7 @@ async function collectHandoffReport(options) {
|
|
|
618
845
|
}
|
|
619
846
|
|
|
620
847
|
// src/cli/output.ts
|
|
621
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
848
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
622
849
|
import { dirname, resolve } from "path";
|
|
623
850
|
|
|
624
851
|
// src/core/budget.ts
|
|
@@ -664,6 +891,7 @@ var genericOrder = [
|
|
|
664
891
|
"package",
|
|
665
892
|
"resume",
|
|
666
893
|
"verification",
|
|
894
|
+
"cache",
|
|
667
895
|
"risk",
|
|
668
896
|
"secretScanning"
|
|
669
897
|
];
|
|
@@ -684,6 +912,7 @@ var profiles = {
|
|
|
684
912
|
"gitStatus",
|
|
685
913
|
"changedFiles",
|
|
686
914
|
"verification",
|
|
915
|
+
"cache",
|
|
687
916
|
"risk",
|
|
688
917
|
"branchDelta",
|
|
689
918
|
"diffSummary",
|
|
@@ -708,6 +937,7 @@ var profiles = {
|
|
|
708
937
|
"resume",
|
|
709
938
|
"repository",
|
|
710
939
|
"verification",
|
|
940
|
+
"cache",
|
|
711
941
|
"risk",
|
|
712
942
|
"changedFiles",
|
|
713
943
|
"gitStatus",
|
|
@@ -740,6 +970,7 @@ var profiles = {
|
|
|
740
970
|
"instructionFiles",
|
|
741
971
|
"package",
|
|
742
972
|
"verification",
|
|
973
|
+
"cache",
|
|
743
974
|
"risk",
|
|
744
975
|
"resume",
|
|
745
976
|
"secretScanning",
|
|
@@ -814,6 +1045,8 @@ function renderSection(section, report) {
|
|
|
814
1045
|
return renderResumeSource(report);
|
|
815
1046
|
case "verification":
|
|
816
1047
|
return renderVerification(report);
|
|
1048
|
+
case "cache":
|
|
1049
|
+
return renderCache(report);
|
|
817
1050
|
case "risk":
|
|
818
1051
|
return renderRisk(report);
|
|
819
1052
|
case "secretScanning":
|
|
@@ -919,6 +1152,18 @@ function renderVerification(report) {
|
|
|
919
1152
|
""
|
|
920
1153
|
];
|
|
921
1154
|
}
|
|
1155
|
+
function renderCache(report) {
|
|
1156
|
+
if (!report.cache) {
|
|
1157
|
+
return [];
|
|
1158
|
+
}
|
|
1159
|
+
return ["## Cache Artifacts", renderCacheArtifacts(report.cache.artifacts), ""];
|
|
1160
|
+
}
|
|
1161
|
+
function renderCacheArtifacts(artifacts) {
|
|
1162
|
+
if (artifacts.length === 0) {
|
|
1163
|
+
return "No cache artifacts found.";
|
|
1164
|
+
}
|
|
1165
|
+
return artifacts.map((artifact) => `- ${artifact.kind}/${artifact.name} (${artifact.createdAt}) - \`${artifact.path}\``).join("\n");
|
|
1166
|
+
}
|
|
922
1167
|
function renderRisk(report) {
|
|
923
1168
|
if (!report.risk) {
|
|
924
1169
|
return [];
|
|
@@ -989,8 +1234,8 @@ async function writeRenderedReport(report, format, budget, output) {
|
|
|
989
1234
|
const rendered = redactText(renderOutput(report, format, budget));
|
|
990
1235
|
if (output) {
|
|
991
1236
|
const outputPath = resolve(process.cwd(), output);
|
|
992
|
-
await
|
|
993
|
-
await
|
|
1237
|
+
await mkdir2(dirname(outputPath), { recursive: true });
|
|
1238
|
+
await writeFile2(outputPath, rendered, "utf8");
|
|
994
1239
|
process.stderr.write(`Wrote handoff packet to ${outputPath}
|
|
995
1240
|
`);
|
|
996
1241
|
return;
|
|
@@ -1014,21 +1259,24 @@ function renderOutput(report, format, budget) {
|
|
|
1014
1259
|
}
|
|
1015
1260
|
|
|
1016
1261
|
// src/cli/commands/pack.ts
|
|
1017
|
-
var PackCliOptionsSchema =
|
|
1018
|
-
goal:
|
|
1019
|
-
output:
|
|
1020
|
-
format:
|
|
1021
|
-
for:
|
|
1022
|
-
budget:
|
|
1023
|
-
includeDiff:
|
|
1024
|
-
diff:
|
|
1025
|
-
since:
|
|
1026
|
-
verify:
|
|
1027
|
-
scanSecrets:
|
|
1262
|
+
var PackCliOptionsSchema = z3.object({
|
|
1263
|
+
goal: z3.string().trim().min(1).default("Make your own goal"),
|
|
1264
|
+
output: z3.string().optional(),
|
|
1265
|
+
format: z3.enum(["markdown", "json"]).default("markdown"),
|
|
1266
|
+
for: z3.enum(["generic", "codex", "claude", "cursor"]).default("generic"),
|
|
1267
|
+
budget: z3.number().int().positive().default(4e3),
|
|
1268
|
+
includeDiff: z3.boolean().default(false),
|
|
1269
|
+
diff: z3.boolean().default(true),
|
|
1270
|
+
since: z3.string().trim().min(1).optional(),
|
|
1271
|
+
verify: z3.boolean().default(false),
|
|
1272
|
+
scanSecrets: z3.boolean().default(false),
|
|
1273
|
+
cache: z3.boolean().default(false),
|
|
1274
|
+
includeCache: z3.boolean().default(false)
|
|
1028
1275
|
});
|
|
1029
1276
|
function createPackCommand() {
|
|
1030
|
-
return new
|
|
1277
|
+
return new Command2("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-cache", "include recent .handoffkit artifact summaries").option("--include-diff", "include full staged and unstaged patches", false).option("--no-diff", "omit diff summaries and full patches").action(async (rawOptions) => {
|
|
1031
1278
|
const options = parseOptions(rawOptions);
|
|
1279
|
+
const root = options.cache ? await findGitRoot(process.cwd()) : void 0;
|
|
1032
1280
|
const report = await collectHandoffReport({
|
|
1033
1281
|
goal: options.goal,
|
|
1034
1282
|
cwd: process.cwd(),
|
|
@@ -1040,8 +1288,18 @@ function createPackCommand() {
|
|
|
1040
1288
|
includeDiffSummary: options.diff,
|
|
1041
1289
|
...options.since ? { since: options.since } : {},
|
|
1042
1290
|
includeVerification: options.verify,
|
|
1043
|
-
scanSecrets: options.scanSecrets
|
|
1291
|
+
scanSecrets: options.scanSecrets,
|
|
1292
|
+
includeCache: options.includeCache
|
|
1044
1293
|
});
|
|
1294
|
+
if (options.cache && root && report.verification) {
|
|
1295
|
+
const cache = await writeCacheArtifact(root, "verification", {
|
|
1296
|
+
goal: report.goal,
|
|
1297
|
+
target: report.target,
|
|
1298
|
+
verification: report.verification
|
|
1299
|
+
});
|
|
1300
|
+
process.stderr.write(`Wrote verification cache to ${cache.latestPath}
|
|
1301
|
+
`);
|
|
1302
|
+
}
|
|
1045
1303
|
await writeRenderedReport(report, options.format, options.budget, options.output);
|
|
1046
1304
|
});
|
|
1047
1305
|
}
|
|
@@ -1063,18 +1321,27 @@ function parseBudget(value) {
|
|
|
1063
1321
|
}
|
|
1064
1322
|
|
|
1065
1323
|
// src/cli/commands/resume.ts
|
|
1066
|
-
import { readFile as
|
|
1067
|
-
import { Command as
|
|
1068
|
-
import { z as
|
|
1324
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1325
|
+
import { Command as Command3 } from "commander";
|
|
1326
|
+
import { z as z4 } from "zod";
|
|
1069
1327
|
|
|
1070
1328
|
// src/core/resume.ts
|
|
1071
1329
|
var RESUME_PREVIEW_LIMIT = 3e3;
|
|
1072
1330
|
var SECTION_ALIASES = {
|
|
1073
|
-
completed: [/^completed$/i, /^done$/i, /^done this session$/i, /^what changed$/i, /^implemented$/i],
|
|
1074
|
-
remaining: [/^remaining$/i, /^next steps$/i, /^todo$/i, /^to do$/i],
|
|
1075
|
-
failedCommands: [/^failed commands$/i, /^failures$/i, /^errors$/i],
|
|
1076
|
-
openQuestions: [
|
|
1077
|
-
|
|
1331
|
+
completed: [/^completed$/i, /^completed work$/i, /^done$/i, /^done this session$/i, /^what changed$/i, /^what i changed$/i, /^implemented$/i],
|
|
1332
|
+
remaining: [/^remaining$/i, /^remaining work$/i, /^next steps$/i, /^next action$/i, /^next safest action$/i, /^todo$/i, /^to do$/i],
|
|
1333
|
+
failedCommands: [/^failed command$/i, /^failed commands$/i, /^command failed$/i, /^commands failed$/i, /^failure$/i, /^failures$/i, /^error$/i, /^errors$/i],
|
|
1334
|
+
openQuestions: [
|
|
1335
|
+
/^open question$/i,
|
|
1336
|
+
/^open questions$/i,
|
|
1337
|
+
/^open questions \/ risks$/i,
|
|
1338
|
+
/^open questions and risks$/i,
|
|
1339
|
+
/^question$/i,
|
|
1340
|
+
/^questions$/i,
|
|
1341
|
+
/^blocker$/i,
|
|
1342
|
+
/^blockers$/i
|
|
1343
|
+
],
|
|
1344
|
+
verification: [/^verification$/i, /^tests$/i, /^tests run$/i, /^validation$/i]
|
|
1078
1345
|
};
|
|
1079
1346
|
function createResumeSource(path, content) {
|
|
1080
1347
|
const normalized = content.replace(/\r\n/g, "\n").trim();
|
|
@@ -1109,12 +1376,22 @@ function parseResumeState(content) {
|
|
|
1109
1376
|
section = sectionForHeading(heading);
|
|
1110
1377
|
continue;
|
|
1111
1378
|
}
|
|
1379
|
+
const transcriptLine = stripTranscriptPrefix(line);
|
|
1380
|
+
const labeled = parseLabeledTranscriptLine(transcriptLine);
|
|
1381
|
+
if (labeled) {
|
|
1382
|
+
heading = labeled.heading;
|
|
1383
|
+
section = labeled.section;
|
|
1384
|
+
if (labeled.item) {
|
|
1385
|
+
appendResumeItem(state, section, labeled.item, heading);
|
|
1386
|
+
}
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1112
1389
|
if (!section) {
|
|
1113
1390
|
continue;
|
|
1114
1391
|
}
|
|
1115
|
-
const item = normalizeListItem(
|
|
1392
|
+
const item = normalizeListItem(transcriptLine);
|
|
1116
1393
|
if (item) {
|
|
1117
|
-
state
|
|
1394
|
+
appendResumeItem(state, section, item, heading);
|
|
1118
1395
|
}
|
|
1119
1396
|
}
|
|
1120
1397
|
const next = state.remaining[0] ?? state.openQuestions[0] ?? state.failedCommands[0];
|
|
@@ -1132,10 +1409,41 @@ function sectionForHeading(heading) {
|
|
|
1132
1409
|
}
|
|
1133
1410
|
return void 0;
|
|
1134
1411
|
}
|
|
1412
|
+
function parseLabeledTranscriptLine(line) {
|
|
1413
|
+
const match = line.match(/^([^:]{1,80}):(?:\s*(.*))?$/);
|
|
1414
|
+
if (!match?.[1]) {
|
|
1415
|
+
return void 0;
|
|
1416
|
+
}
|
|
1417
|
+
const heading = match[1].trim();
|
|
1418
|
+
const section = sectionForHeading(heading);
|
|
1419
|
+
if (!section) {
|
|
1420
|
+
return void 0;
|
|
1421
|
+
}
|
|
1422
|
+
const item = match[2]?.trim();
|
|
1423
|
+
return {
|
|
1424
|
+
section,
|
|
1425
|
+
heading,
|
|
1426
|
+
...item ? { item } : {}
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
function appendResumeItem(state, section, item, heading) {
|
|
1430
|
+
state[section].push({ text: redactText(item), ...heading ? { sourceHeading: redactText(heading) } : {} });
|
|
1431
|
+
}
|
|
1135
1432
|
function normalizeListItem(line) {
|
|
1136
1433
|
const match = line.match(/^[-*]\s+(.+)$/) ?? line.match(/^\d+\.\s+(.+)$/);
|
|
1137
1434
|
return match?.[1]?.trim();
|
|
1138
1435
|
}
|
|
1436
|
+
function stripTranscriptPrefix(line) {
|
|
1437
|
+
let current = line.trim();
|
|
1438
|
+
for (let i = 0; i < 4; i += 1) {
|
|
1439
|
+
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();
|
|
1440
|
+
if (next === current) {
|
|
1441
|
+
return current;
|
|
1442
|
+
}
|
|
1443
|
+
current = next;
|
|
1444
|
+
}
|
|
1445
|
+
return current;
|
|
1446
|
+
}
|
|
1139
1447
|
function normalizeHeading(heading) {
|
|
1140
1448
|
return heading.trim().replace(/:$/, "").replace(/\s*\/\s*/g, " / ").replace(/\s+/g, " ");
|
|
1141
1449
|
}
|
|
@@ -1144,17 +1452,20 @@ function hasResumeState(state) {
|
|
|
1144
1452
|
}
|
|
1145
1453
|
|
|
1146
1454
|
// src/cli/commands/resume.ts
|
|
1147
|
-
var ResumeOptionsSchema =
|
|
1148
|
-
goal:
|
|
1149
|
-
output:
|
|
1150
|
-
format:
|
|
1151
|
-
for:
|
|
1152
|
-
budget:
|
|
1455
|
+
var ResumeOptionsSchema = z4.object({
|
|
1456
|
+
goal: z4.string().trim().min(1).default("Resume interrupted AI coding session"),
|
|
1457
|
+
output: z4.string().optional(),
|
|
1458
|
+
format: z4.enum(["markdown", "json"]).default("markdown"),
|
|
1459
|
+
for: z4.enum(["generic", "codex", "claude", "cursor"]).default("generic"),
|
|
1460
|
+
budget: z4.number().int().positive().default(4e3),
|
|
1461
|
+
cache: z4.boolean().default(false),
|
|
1462
|
+
fromCache: z4.string().trim().min(1).optional()
|
|
1153
1463
|
});
|
|
1154
1464
|
function createResumeCommand() {
|
|
1155
|
-
return new
|
|
1465
|
+
return new Command3("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").option("--from-cache <ref>", "load resume source from .handoffkit/resume, for example latest or resume/latest").action(async (path, rawOptions) => {
|
|
1156
1466
|
const options = ResumeOptionsSchema.parse(rawOptions);
|
|
1157
|
-
const
|
|
1467
|
+
const root = options.cache || options.fromCache ? await findGitRoot(process.cwd()) : void 0;
|
|
1468
|
+
const source = await resolveResumeSource(path, options.fromCache, root);
|
|
1158
1469
|
const report = await collectHandoffReport({
|
|
1159
1470
|
goal: options.goal,
|
|
1160
1471
|
cwd: process.cwd(),
|
|
@@ -1166,11 +1477,36 @@ function createResumeCommand() {
|
|
|
1166
1477
|
includeDiffSummary: true,
|
|
1167
1478
|
includeVerification: false,
|
|
1168
1479
|
scanSecrets: false,
|
|
1480
|
+
includeCache: false,
|
|
1169
1481
|
resumeSource: source
|
|
1170
1482
|
});
|
|
1483
|
+
if (options.cache && root) {
|
|
1484
|
+
const cache = await writeCacheArtifact(root, "resume", {
|
|
1485
|
+
goal: report.goal,
|
|
1486
|
+
target: report.target,
|
|
1487
|
+
source
|
|
1488
|
+
});
|
|
1489
|
+
process.stderr.write(`Wrote resume cache to ${cache.latestPath}
|
|
1490
|
+
`);
|
|
1491
|
+
}
|
|
1171
1492
|
await writeRenderedReport(report, options.format, options.budget, options.output);
|
|
1172
1493
|
});
|
|
1173
1494
|
}
|
|
1495
|
+
async function resolveResumeSource(path, fromCache, root) {
|
|
1496
|
+
if (path && fromCache) {
|
|
1497
|
+
throw new Error("Pass either <path> or --from-cache, not both.");
|
|
1498
|
+
}
|
|
1499
|
+
if (fromCache) {
|
|
1500
|
+
if (!root) {
|
|
1501
|
+
throw new Error("--from-cache requires a git repository.");
|
|
1502
|
+
}
|
|
1503
|
+
return readResumeSourceFromCache(root, fromCache);
|
|
1504
|
+
}
|
|
1505
|
+
if (!path) {
|
|
1506
|
+
throw new Error("resume requires <path> or --from-cache <ref>.");
|
|
1507
|
+
}
|
|
1508
|
+
return createResumeSource(path, await readFile6(path, "utf8"));
|
|
1509
|
+
}
|
|
1174
1510
|
function parseBudget2(value) {
|
|
1175
1511
|
const parsed = Number.parseInt(value, 10);
|
|
1176
1512
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
@@ -1180,13 +1516,13 @@ function parseBudget2(value) {
|
|
|
1180
1516
|
}
|
|
1181
1517
|
|
|
1182
1518
|
// src/cli/commands/risk.ts
|
|
1183
|
-
import { Command as
|
|
1184
|
-
import { z as
|
|
1185
|
-
var RiskOptionsSchema =
|
|
1186
|
-
format:
|
|
1519
|
+
import { Command as Command4 } from "commander";
|
|
1520
|
+
import { z as z5 } from "zod";
|
|
1521
|
+
var RiskOptionsSchema = z5.object({
|
|
1522
|
+
format: z5.enum(["markdown", "json"]).default("markdown")
|
|
1187
1523
|
});
|
|
1188
1524
|
function createRiskCommand() {
|
|
1189
|
-
return new
|
|
1525
|
+
return new Command4("risk").description("Show deterministic risk notes for the current handoff.").summary("Show deterministic risk notes from changed files.").option("--format <format>", "output format: markdown or json", "markdown").action(async (rawOptions) => {
|
|
1190
1526
|
const options = RiskOptionsSchema.parse(rawOptions);
|
|
1191
1527
|
const report = await collectHandoffReport({
|
|
1192
1528
|
goal: "Review local risk",
|
|
@@ -1197,7 +1533,8 @@ function createRiskCommand() {
|
|
|
1197
1533
|
includeDiff: false,
|
|
1198
1534
|
includeDiffSummary: false,
|
|
1199
1535
|
includeVerification: false,
|
|
1200
|
-
scanSecrets: false
|
|
1536
|
+
scanSecrets: false,
|
|
1537
|
+
includeCache: false
|
|
1201
1538
|
});
|
|
1202
1539
|
if (options.format === "json") {
|
|
1203
1540
|
process.stdout.write(`${JSON.stringify(report.risk, null, 2)}
|
|
@@ -1212,13 +1549,13 @@ ${report.risk?.notes.map((note) => `- **${note.severity}**: ${note.title} - ${no
|
|
|
1212
1549
|
}
|
|
1213
1550
|
|
|
1214
1551
|
// src/cli/commands/scan-secrets.ts
|
|
1215
|
-
import { Command as
|
|
1216
|
-
import { z as
|
|
1217
|
-
var ScanSecretsOptionsSchema =
|
|
1218
|
-
format:
|
|
1552
|
+
import { Command as Command5 } from "commander";
|
|
1553
|
+
import { z as z6 } from "zod";
|
|
1554
|
+
var ScanSecretsOptionsSchema = z6.object({
|
|
1555
|
+
format: z6.enum(["markdown", "json"]).default("markdown")
|
|
1219
1556
|
});
|
|
1220
1557
|
function createScanSecretsCommand() {
|
|
1221
|
-
return new
|
|
1558
|
+
return new Command5("scan-secrets").description("Run optional local secret scanners and print bounded redacted results.").summary("Run optional local secret scanners.").option("--format <format>", "output format: markdown or json", "markdown").action(async (rawOptions) => {
|
|
1222
1559
|
const options = ScanSecretsOptionsSchema.parse(rawOptions);
|
|
1223
1560
|
const root = await findGitRoot(process.cwd());
|
|
1224
1561
|
const report = await runSecretScanners(root);
|
|
@@ -1259,16 +1596,22 @@ function scannerGuidanceLines2(scanner) {
|
|
|
1259
1596
|
}
|
|
1260
1597
|
|
|
1261
1598
|
// src/cli/commands/verify.ts
|
|
1262
|
-
import { Command as
|
|
1263
|
-
import { z as
|
|
1264
|
-
var VerifyOptionsSchema =
|
|
1265
|
-
format:
|
|
1599
|
+
import { Command as Command6 } from "commander";
|
|
1600
|
+
import { z as z7 } from "zod";
|
|
1601
|
+
var VerifyOptionsSchema = z7.object({
|
|
1602
|
+
format: z7.enum(["markdown", "json"]).default("markdown"),
|
|
1603
|
+
cache: z7.boolean().default(false)
|
|
1266
1604
|
});
|
|
1267
1605
|
function createVerifyCommand() {
|
|
1268
|
-
return new
|
|
1606
|
+
return new Command6("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) => {
|
|
1269
1607
|
const options = VerifyOptionsSchema.parse(rawOptions);
|
|
1270
1608
|
const root = await findGitRoot(process.cwd());
|
|
1271
1609
|
const verification = await runVerification(root);
|
|
1610
|
+
if (options.cache) {
|
|
1611
|
+
const cache = await writeCacheArtifact(root, "verification", verification);
|
|
1612
|
+
process.stderr.write(`Wrote verification cache to ${cache.latestPath}
|
|
1613
|
+
`);
|
|
1614
|
+
}
|
|
1272
1615
|
if (options.format === "json") {
|
|
1273
1616
|
process.stdout.write(redactText(`${JSON.stringify(verification, null, 2)}
|
|
1274
1617
|
`));
|
|
@@ -1291,12 +1634,13 @@ function renderVerificationMarkdown(commands) {
|
|
|
1291
1634
|
}
|
|
1292
1635
|
|
|
1293
1636
|
// src/cli/index.ts
|
|
1294
|
-
var program = new
|
|
1637
|
+
var program = new Command7().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.4.0");
|
|
1295
1638
|
program.addCommand(createPackCommand());
|
|
1296
1639
|
program.addCommand(createVerifyCommand());
|
|
1297
1640
|
program.addCommand(createRiskCommand());
|
|
1298
1641
|
program.addCommand(createScanSecretsCommand());
|
|
1299
1642
|
program.addCommand(createResumeCommand());
|
|
1643
|
+
program.addCommand(createCacheCommand());
|
|
1300
1644
|
try {
|
|
1301
1645
|
await program.parseAsync(process.argv);
|
|
1302
1646
|
} catch (error) {
|