lockstep-cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/adapters/claude.d.ts +2 -0
  2. package/dist/adapters/claude.js +56 -0
  3. package/dist/adapters/claude.js.map +1 -0
  4. package/dist/adapters/fsutil.d.ts +7 -0
  5. package/dist/adapters/fsutil.js +41 -0
  6. package/dist/adapters/fsutil.js.map +1 -0
  7. package/dist/adapters/install.test.d.ts +1 -0
  8. package/dist/adapters/install.test.js +32 -0
  9. package/dist/adapters/install.test.js.map +1 -0
  10. package/dist/adapters/merge.d.ts +27 -0
  11. package/dist/adapters/merge.js +49 -0
  12. package/dist/adapters/merge.js.map +1 -0
  13. package/dist/adapters/merge.test.d.ts +1 -0
  14. package/dist/adapters/merge.test.js +41 -0
  15. package/dist/adapters/merge.test.js.map +1 -0
  16. package/dist/adapters/registry.d.ts +2 -0
  17. package/dist/adapters/registry.js +17 -0
  18. package/dist/adapters/registry.js.map +1 -0
  19. package/dist/adapters/templates.d.ts +5 -0
  20. package/dist/adapters/templates.js +40 -0
  21. package/dist/adapters/templates.js.map +1 -0
  22. package/dist/adapters/types.d.ts +14 -0
  23. package/dist/adapters/types.js +2 -0
  24. package/dist/adapters/types.js.map +1 -0
  25. package/dist/auth/token-store.d.ts +3 -0
  26. package/dist/auth/token-store.js +74 -0
  27. package/dist/auth/token-store.js.map +1 -0
  28. package/dist/capture/classify.d.ts +12 -0
  29. package/dist/capture/classify.js +26 -0
  30. package/dist/capture/classify.js.map +1 -0
  31. package/dist/capture/classify.test.d.ts +1 -0
  32. package/dist/capture/classify.test.js +25 -0
  33. package/dist/capture/classify.test.js.map +1 -0
  34. package/dist/capture/diff.d.ts +2 -0
  35. package/dist/capture/diff.js +19 -0
  36. package/dist/capture/diff.js.map +1 -0
  37. package/dist/capture/index.d.ts +6 -0
  38. package/dist/capture/index.js +85 -0
  39. package/dist/capture/index.js.map +1 -0
  40. package/dist/cloud.d.ts +8 -0
  41. package/dist/cloud.js +38 -0
  42. package/dist/cloud.js.map +1 -0
  43. package/dist/config.d.ts +8 -0
  44. package/dist/config.js +26 -0
  45. package/dist/config.js.map +1 -0
  46. package/dist/connect.d.ts +10 -0
  47. package/dist/connect.js +60 -0
  48. package/dist/connect.js.map +1 -0
  49. package/dist/index.d.ts +2 -0
  50. package/dist/index.js +98 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/init.d.ts +8 -0
  53. package/dist/init.js +46 -0
  54. package/dist/init.js.map +1 -0
  55. package/dist/login.d.ts +6 -0
  56. package/dist/login.js +32 -0
  57. package/dist/login.js.map +1 -0
  58. package/dist/mcp/api.d.ts +2 -0
  59. package/dist/mcp/api.js +22 -0
  60. package/dist/mcp/api.js.map +1 -0
  61. package/dist/mcp/git.d.ts +3 -0
  62. package/dist/mcp/git.js +20 -0
  63. package/dist/mcp/git.js.map +1 -0
  64. package/dist/mcp/server.d.ts +6 -0
  65. package/dist/mcp/server.js +39 -0
  66. package/dist/mcp/server.js.map +1 -0
  67. package/dist/mcp/session.d.ts +9 -0
  68. package/dist/mcp/session.js +12 -0
  69. package/dist/mcp/session.js.map +1 -0
  70. package/package.json +43 -0
@@ -0,0 +1,2 @@
1
+ import type { VendorAdapter } from "./types.js";
2
+ export declare const claudeAdapter: VendorAdapter;
@@ -0,0 +1,56 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { access } from "node:fs/promises";
4
+ import { applyFile, readIfExists } from "./fsutil.js";
5
+ import { mergeHooks, mergeMcp, upsertManagedBlock } from "./merge.js";
6
+ import { captureHooks, mcpSpec, SKILL_MD, CLAUDE_BLOCK } from "./templates.js";
7
+ function paths(cwd, scope) {
8
+ if (scope === "user") {
9
+ const h = homedir();
10
+ return {
11
+ mcp: join(h, ".mcp.json"),
12
+ hooks: join(h, ".claude", "settings.json"),
13
+ skill: join(h, ".claude", "skills", "lockstep", "SKILL.md"),
14
+ instructions: join(h, ".claude", "CLAUDE.md"),
15
+ };
16
+ }
17
+ return {
18
+ mcp: join(cwd, ".mcp.json"),
19
+ hooks: join(cwd, ".claude", "settings.json"),
20
+ skill: join(cwd, ".claude", "skills", "lockstep", "SKILL.md"),
21
+ instructions: join(cwd, "CLAUDE.md"),
22
+ };
23
+ }
24
+ export const claudeAdapter = {
25
+ id: "claude",
26
+ async detect() {
27
+ try {
28
+ await access(join(homedir(), ".claude"));
29
+ return true;
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ },
35
+ async install(cwd, scope, dryRun) {
36
+ const p = paths(cwd, scope);
37
+ return [
38
+ await applyFile(p.mcp, (cur) => mergeMcp(cur, "lockstep", mcpSpec("claude")), dryRun),
39
+ await applyFile(p.hooks, (cur) => mergeHooks(cur, captureHooks, "lockstep"), dryRun),
40
+ await applyFile(p.skill, () => SKILL_MD, dryRun),
41
+ await applyFile(p.instructions, (cur) => upsertManagedBlock(cur, CLAUDE_BLOCK), dryRun),
42
+ ];
43
+ },
44
+ async verify(cwd, scope) {
45
+ const p = paths(cwd, scope);
46
+ const mcp = await readIfExists(p.mcp);
47
+ const hooks = await readIfExists(p.hooks);
48
+ const mcpOk = !!mcp && mcp.includes('"lockstep"');
49
+ const hooksOk = !!hooks && hooks.includes("lockstep");
50
+ return {
51
+ ok: mcpOk && hooksOk,
52
+ details: [`${mcpOk ? "✓" : "✗"} mcp server (${p.mcp})`, `${hooksOk ? "✓" : "✗"} hooks (${p.hooks})`],
53
+ };
54
+ },
55
+ };
56
+ //# sourceMappingURL=claude.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../src/adapters/claude.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export declare function readIfExists(p: string): Promise<string | null>;
2
+ /**
3
+ * Read → transform → atomic write (temp + rename), backing up the original once.
4
+ * Returns a human-readable status line. No-op (and reports "unchanged") if the
5
+ * transform produces identical content — this is what makes init idempotent.
6
+ */
7
+ export declare function applyFile(p: string, transform: (cur: string | null) => string, dryRun: boolean): Promise<string>;
@@ -0,0 +1,41 @@
1
+ import { mkdir, readFile, writeFile, rename, access } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ export async function readIfExists(p) {
4
+ try {
5
+ return await readFile(p, "utf8");
6
+ }
7
+ catch {
8
+ return null;
9
+ }
10
+ }
11
+ async function exists(p) {
12
+ try {
13
+ await access(p);
14
+ return true;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ /**
21
+ * Read → transform → atomic write (temp + rename), backing up the original once.
22
+ * Returns a human-readable status line. No-op (and reports "unchanged") if the
23
+ * transform produces identical content — this is what makes init idempotent.
24
+ */
25
+ export async function applyFile(p, transform, dryRun) {
26
+ const cur = await readIfExists(p);
27
+ const next = transform(cur);
28
+ if (cur === next)
29
+ return `unchanged ${p}`;
30
+ if (dryRun)
31
+ return `would write ${p}`;
32
+ await mkdir(dirname(p), { recursive: true });
33
+ if (cur !== null && !(await exists(`${p}.lockstep.bak`))) {
34
+ await writeFile(`${p}.lockstep.bak`, cur);
35
+ }
36
+ const tmp = `${p}.tmp-${process.pid}`;
37
+ await writeFile(tmp, next);
38
+ await rename(tmp, p);
39
+ return `wrote ${p}`;
40
+ }
41
+ //# sourceMappingURL=fsutil.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fsutil.js","sourceRoot":"","sources":["../../src/adapters/fsutil.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,CAAS;IAC1C,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,CAAS;IAC7B,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,CAAC,CAAC,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,CAAS,EACT,SAAyC,EACzC,MAAe;IAEf,MAAM,GAAG,GAAG,MAAM,YAAY,CAAC,CAAC,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;IAC5B,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,cAAc,CAAC,EAAE,CAAC;IAC3C,IAAI,MAAM;QAAE,OAAO,eAAe,CAAC,EAAE,CAAC;IACtC,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC;QACzD,MAAM,SAAS,CAAC,GAAG,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC;IAC5C,CAAC;IACD,MAAM,GAAG,GAAG,GAAG,CAAC,QAAQ,OAAO,CAAC,GAAG,EAAE,CAAC;IACtC,MAAM,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC3B,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACrB,OAAO,cAAc,CAAC,EAAE,CAAC;AAC3B,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { claudeAdapter } from "./claude.js";
7
+ test("init writes config, is idempotent on disk, and preserves foreign config", async () => {
8
+ const dir = await mkdtemp(join(tmpdir(), "lockstep-init-"));
9
+ await mkdir(join(dir, ".claude"), { recursive: true });
10
+ await writeFile(join(dir, ".claude", "settings.json"), JSON.stringify({ hooks: { PostToolUse: [{ matcher: "Bash", hooks: [{ type: "command", command: "my-linter" }] }] } }, null, 2));
11
+ await writeFile(join(dir, ".mcp.json"), JSON.stringify({ mcpServers: { other: { command: "x", args: [] } } }, null, 2));
12
+ await writeFile(join(dir, "CLAUDE.md"), "# Project\n\nhand-written notes\n");
13
+ const read = async () => ({
14
+ settings: await readFile(join(dir, ".claude", "settings.json"), "utf8"),
15
+ mcp: await readFile(join(dir, ".mcp.json"), "utf8"),
16
+ claude: await readFile(join(dir, "CLAUDE.md"), "utf8"),
17
+ skill: await readFile(join(dir, ".claude", "skills", "lockstep", "SKILL.md"), "utf8"),
18
+ });
19
+ await claudeAdapter.install(dir, "project", false);
20
+ const after1 = await read();
21
+ await claudeAdapter.install(dir, "project", false);
22
+ const after2 = await read();
23
+ assert.deepEqual(after1, after2, "second init is byte-identical (idempotent)");
24
+ assert.ok(after1.settings.includes("my-linter"), "foreign hook preserved");
25
+ assert.ok(after1.mcp.includes('"other"'), "foreign mcp server preserved");
26
+ assert.ok(after1.claude.includes("hand-written notes"), "user CLAUDE.md content preserved");
27
+ assert.ok(after1.settings.includes("capture"), "our hooks installed");
28
+ assert.ok(after1.mcp.includes('"lockstep"'), "our mcp server installed");
29
+ const v = await claudeAdapter.verify(dir, "project");
30
+ assert.ok(v.ok, "verify passes after install");
31
+ });
32
+ //# sourceMappingURL=install.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.test.js","sourceRoot":"","sources":["../../src/adapters/install.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ export interface HookCommand {
2
+ type: "command";
3
+ command: string;
4
+ args?: string[];
5
+ timeout?: number;
6
+ }
7
+ export interface HookEntry {
8
+ matcher?: string;
9
+ hooks: HookCommand[];
10
+ }
11
+ export interface ManagedHook {
12
+ event: string;
13
+ matcher: string;
14
+ args: string[];
15
+ timeout: number;
16
+ }
17
+ export interface McpServerSpec {
18
+ command: string;
19
+ args: string[];
20
+ env?: Record<string, string>;
21
+ }
22
+ /** Merge our capture hooks into an existing settings JSON, preserving foreign hooks. */
23
+ export declare function mergeHooks(existing: string | null, managed: ManagedHook[], command?: string): string;
24
+ /** Upsert our MCP server under its stable key; leave sibling servers untouched. */
25
+ export declare function mergeMcp(existing: string | null, name: string, spec: McpServerSpec): string;
26
+ /** Replace only the delimited managed block in a markdown file; append if absent. */
27
+ export declare function upsertManagedBlock(existing: string | null, block: string): string;
@@ -0,0 +1,49 @@
1
+ // Identifies lockstep-owned hook entries (matches both the global-bin form `lockstep …`
2
+ // and the legacy `npx @lockstep/cli …` form, so re-running init migrates old configs).
3
+ const MARKER = "lockstep";
4
+ function isOurs(entry) {
5
+ return (entry.hooks ?? []).some((h) => `${h.command} ${(h.args ?? []).join(" ")}`.includes(MARKER));
6
+ }
7
+ /** Merge our capture hooks into an existing settings JSON, preserving foreign hooks. */
8
+ export function mergeHooks(existing, managed, command = "npx") {
9
+ const obj = existing ? JSON.parse(existing) : {};
10
+ const hooks = obj.hooks ?? {};
11
+ const byEvent = new Map();
12
+ for (const m of managed) {
13
+ const list = byEvent.get(m.event) ?? [];
14
+ list.push(m);
15
+ byEvent.set(m.event, list);
16
+ }
17
+ for (const [event, ms] of byEvent) {
18
+ const arr = Array.isArray(hooks[event]) ? hooks[event] : [];
19
+ const foreign = arr.filter((e) => !isOurs(e));
20
+ const ours = ms.map((m) => ({
21
+ matcher: m.matcher,
22
+ hooks: [{ type: "command", command, args: m.args, timeout: m.timeout }],
23
+ }));
24
+ hooks[event] = [...foreign, ...ours];
25
+ }
26
+ obj.hooks = hooks;
27
+ return JSON.stringify(obj, null, 2) + "\n";
28
+ }
29
+ /** Upsert our MCP server under its stable key; leave sibling servers untouched. */
30
+ export function mergeMcp(existing, name, spec) {
31
+ const obj = existing ? JSON.parse(existing) : {};
32
+ const servers = obj.mcpServers ?? {};
33
+ servers[name] = spec;
34
+ obj.mcpServers = servers;
35
+ return JSON.stringify(obj, null, 2) + "\n";
36
+ }
37
+ const START = "<!-- lockstep:start -->";
38
+ const END = "<!-- lockstep:end -->";
39
+ /** Replace only the delimited managed block in a markdown file; append if absent. */
40
+ export function upsertManagedBlock(existing, block) {
41
+ const wrapped = `${START}\n${block}\n${END}`;
42
+ if (!existing)
43
+ return wrapped + "\n";
44
+ const re = new RegExp(`${START}[\\s\\S]*?${END}`);
45
+ if (re.test(existing))
46
+ return existing.replace(re, wrapped);
47
+ return existing.trimEnd() + "\n\n" + wrapped + "\n";
48
+ }
49
+ //# sourceMappingURL=merge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.js","sourceRoot":"","sources":["../../src/adapters/merge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mergeHooks, mergeMcp, upsertManagedBlock } from "./merge.js";
4
+ const HOOKS = [
5
+ { event: "SessionStart", matcher: "*", args: ["-y", "@lockstep/cli", "capture", "--event", "SessionStart"], timeout: 20 },
6
+ { event: "PostToolUse", matcher: "Edit|Write", args: ["-y", "@lockstep/cli", "capture", "--event", "PostToolUse"], timeout: 30 },
7
+ ];
8
+ test("mergeHooks is idempotent (run twice = byte-identical)", () => {
9
+ const once = mergeHooks(null, HOOKS);
10
+ const twice = mergeHooks(once, HOOKS);
11
+ assert.equal(once, twice);
12
+ });
13
+ test("mergeHooks preserves foreign hooks and stays idempotent", () => {
14
+ const existing = JSON.stringify({
15
+ hooks: { PostToolUse: [{ matcher: "Bash", hooks: [{ type: "command", command: "my-linter" }] }] },
16
+ });
17
+ const merged = mergeHooks(existing, HOOKS);
18
+ const obj = JSON.parse(merged);
19
+ const cmds = obj.hooks.PostToolUse.map((e) => e.hooks[0].command);
20
+ assert.ok(cmds.includes("my-linter"), "foreign hook preserved");
21
+ assert.ok(obj.hooks.PostToolUse.some((e) => (e.hooks[0].args ?? []).join(" ").includes("@lockstep/cli")), "our hook added");
22
+ assert.equal(mergeHooks(merged, HOOKS), merged, "idempotent with foreign present");
23
+ });
24
+ test("mergeMcp upserts our key, preserves siblings, idempotent", () => {
25
+ const existing = JSON.stringify({ mcpServers: { other: { command: "x", args: [] } } });
26
+ const spec = { command: "npx", args: ["-y", "@lockstep/cli", "mcp"] };
27
+ const m1 = mergeMcp(existing, "lockstep", spec);
28
+ const obj = JSON.parse(m1);
29
+ assert.ok(obj.mcpServers.other, "sibling preserved");
30
+ assert.ok(obj.mcpServers.lockstep, "our server added");
31
+ assert.equal(mergeMcp(m1, "lockstep", spec), m1, "idempotent");
32
+ });
33
+ test("upsertManagedBlock replaces only the managed region, idempotent", () => {
34
+ const md = "# My project\n\nsome notes\n";
35
+ const b1 = upsertManagedBlock(md, "LOCKSTEP RULES v1");
36
+ assert.ok(b1.includes("My project") && b1.includes("LOCKSTEP RULES v1"));
37
+ const b2 = upsertManagedBlock(b1, "LOCKSTEP RULES v2");
38
+ assert.ok(b2.includes("v2") && !b2.includes("v1"), "managed block replaced, user content kept");
39
+ assert.equal(upsertManagedBlock(b2, "LOCKSTEP RULES v2"), b2, "idempotent");
40
+ });
41
+ //# sourceMappingURL=merge.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.test.js","sourceRoot":"","sources":["../../src/adapters/merge.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { VendorAdapter } from "./types.js";
2
+ export declare function getAdapters(vendor?: string): Promise<VendorAdapter[]>;
@@ -0,0 +1,17 @@
1
+ import { claudeAdapter } from "./claude.js";
2
+ // Codex + Gemini adapters slot in here later — same interface, zero core change.
3
+ const ALL = [claudeAdapter];
4
+ export async function getAdapters(vendor) {
5
+ if (vendor && vendor !== "all") {
6
+ return ALL.filter((a) => a.id === vendor);
7
+ }
8
+ const detected = [];
9
+ for (const a of ALL) {
10
+ if (await a.detect())
11
+ detected.push(a);
12
+ }
13
+ // Project-committed config is harmless even if the CLI isn't detected locally,
14
+ // so fall back to all known adapters when nothing is detected.
15
+ return detected.length > 0 ? detected : ALL;
16
+ }
17
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/adapters/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C,iFAAiF;AACjF,MAAM,GAAG,GAAoB,CAAC,aAAa,CAAC,CAAC;AAE7C,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAe;IAC/C,IAAI,MAAM,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;QAC/B,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,CAAC;IAC5C,CAAC;IACD,MAAM,QAAQ,GAAoB,EAAE,CAAC;IACrC,KAAK,MAAM,CAAC,IAAI,GAAG,EAAE,CAAC;QACpB,IAAI,MAAM,CAAC,CAAC,MAAM,EAAE;YAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACzC,CAAC;IACD,+EAA+E;IAC/E,+DAA+D;IAC/D,OAAO,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC;AAC9C,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { ManagedHook, McpServerSpec } from "./merge.js";
2
+ export declare const mcpSpec: (vendor: string) => McpServerSpec;
3
+ export declare const captureHooks: ManagedHook[];
4
+ export declare const SKILL_MD = "---\nname: lockstep\ndescription: Keep this repo's coding agents in lockstep \u2014 read the shared ledger before coding, publish changes after.\n---\n\n# Lockstep\n\nThis project uses Lockstep to coordinate multiple developers' coding agents on the same codebase.\n\n## On session start\n- Call `inbox` to see what changed, what's newly binding, and what's delegated to you.\n- Call `decisions` to load the binding rules for the areas you'll touch.\n\n## Before coding a shared/contract surface\n- Call `query` to check the ledger for existing decisions/contracts (answer instantly if known).\n- Respect any `binding` decision in scope.\n\n## After making a change\n- Summarize the change and call `notify` (include a contract delta for interface changes).\n- For any surface you call, record the dependency with `register_dependency`.\n\n## Coordinating\n- Use `ask` for code/repo questions (set `urgent` if you're blocked).\n- Use `delegate` / `complete` for handoffs. Propose binding rules with `propose_decision`.\n";
5
+ export declare const CLAUDE_BLOCK = "## Lockstep (team coordination)\nOn session start, read your `inbox` and current `decisions`. Before coding a shared/contract surface, `query` the ledger and obey binding decisions. After a change, summarize it, `register_dependency` for surfaces you call, and `notify`. Ask code/repo questions with `ask` (urgent if blocking). See the `lockstep` skill for detail.";
@@ -0,0 +1,40 @@
1
+ // Uses the globally-installed `lockstep` bin. (When the package is published to npm,
2
+ // an installer flag can switch these to `npx @lockstep/cli` for zero-install teammates.)
3
+ export const mcpSpec = (vendor) => ({
4
+ command: "lockstep",
5
+ args: ["mcp"],
6
+ env: { LOCKSTEP_VENDOR: vendor },
7
+ });
8
+ export const captureHooks = [
9
+ { event: "SessionStart", matcher: "*", args: ["capture", "--event", "SessionStart"], timeout: 20 },
10
+ { event: "PostToolUse", matcher: "Edit|Write|MultiEdit|NotebookEdit", args: ["capture", "--event", "PostToolUse"], timeout: 30 },
11
+ { event: "Stop", matcher: "*", args: ["capture", "--event", "Stop"], timeout: 45 },
12
+ ];
13
+ export const SKILL_MD = `---
14
+ name: lockstep
15
+ description: Keep this repo's coding agents in lockstep — read the shared ledger before coding, publish changes after.
16
+ ---
17
+
18
+ # Lockstep
19
+
20
+ This project uses Lockstep to coordinate multiple developers' coding agents on the same codebase.
21
+
22
+ ## On session start
23
+ - Call \`inbox\` to see what changed, what's newly binding, and what's delegated to you.
24
+ - Call \`decisions\` to load the binding rules for the areas you'll touch.
25
+
26
+ ## Before coding a shared/contract surface
27
+ - Call \`query\` to check the ledger for existing decisions/contracts (answer instantly if known).
28
+ - Respect any \`binding\` decision in scope.
29
+
30
+ ## After making a change
31
+ - Summarize the change and call \`notify\` (include a contract delta for interface changes).
32
+ - For any surface you call, record the dependency with \`register_dependency\`.
33
+
34
+ ## Coordinating
35
+ - Use \`ask\` for code/repo questions (set \`urgent\` if you're blocked).
36
+ - Use \`delegate\` / \`complete\` for handoffs. Propose binding rules with \`propose_decision\`.
37
+ `;
38
+ export const CLAUDE_BLOCK = `## Lockstep (team coordination)
39
+ On session start, read your \`inbox\` and current \`decisions\`. Before coding a shared/contract surface, \`query\` the ledger and obey binding decisions. After a change, summarize it, \`register_dependency\` for surfaces you call, and \`notify\`. Ask code/repo questions with \`ask\` (urgent if blocking). See the \`lockstep\` skill for detail.`;
40
+ //# sourceMappingURL=templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../../src/adapters/templates.ts"],"names":[],"mappings":"AAEA,qFAAqF;AACrF,yFAAyF;AACzF,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,MAAc,EAAiB,EAAE,CAAC,CAAC;IACzD,OAAO,EAAE,UAAU;IACnB,IAAI,EAAE,CAAC,KAAK,CAAC;IACb,GAAG,EAAE,EAAE,eAAe,EAAE,MAAM,EAAE;CACjC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,YAAY,GAAkB;IACzC,EAAE,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,cAAc,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE;IAClG,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,mCAAmC,EAAE,IAAI,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,aAAa,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE;IAChI,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE;CACnF,CAAC;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;;CAwBvB,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAG;0VAC8T,CAAC"}
@@ -0,0 +1,14 @@
1
+ export type Scope = "project" | "user";
2
+ /**
3
+ * The vendor seam. Core/MCP tools are identical across vendors; only config writing
4
+ * and event-name mapping differ. Adding Codex/Gemini = a new implementation, zero core change.
5
+ */
6
+ export interface VendorAdapter {
7
+ id: string;
8
+ detect(): Promise<boolean>;
9
+ install(cwd: string, scope: Scope, dryRun: boolean): Promise<string[]>;
10
+ verify(cwd: string, scope: Scope): Promise<{
11
+ ok: boolean;
12
+ details: string[];
13
+ }>;
14
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/adapters/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ export declare function setToken(token: string): Promise<void>;
2
+ export declare function getToken(): Promise<string | null>;
3
+ export declare function clearToken(): Promise<void>;
@@ -0,0 +1,74 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { mkdir, readFile, writeFile, rm, chmod } from "node:fs/promises";
4
+ /**
5
+ * Stores the Lockstep session token. Prefers the OS keychain (keytar) but falls back to a
6
+ * 0600 file under ~/.lockstep whenever keytar is missing OR throws (e.g. headless/no keychain).
7
+ * Never stores the GitHub token.
8
+ */
9
+ const SERVICE = "lockstep";
10
+ const ACCOUNT = "session-token";
11
+ const fileDir = join(homedir(), ".lockstep");
12
+ const filePath = join(fileDir, "credentials.json");
13
+ async function keychain() {
14
+ try {
15
+ const mod = await import("keytar");
16
+ return (mod.default ?? mod);
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ async function fileSet(token) {
23
+ await mkdir(fileDir, { recursive: true });
24
+ await writeFile(filePath, JSON.stringify({ token }), { mode: 0o600 });
25
+ await chmod(filePath, 0o600);
26
+ }
27
+ async function fileGet() {
28
+ try {
29
+ return JSON.parse(await readFile(filePath, "utf8")).token ?? null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ export async function setToken(token) {
36
+ const kc = await keychain();
37
+ if (kc) {
38
+ try {
39
+ await kc.setPassword(SERVICE, ACCOUNT, token);
40
+ return;
41
+ }
42
+ catch {
43
+ /* keychain unavailable → file fallback */
44
+ }
45
+ }
46
+ await fileSet(token);
47
+ }
48
+ export async function getToken() {
49
+ const kc = await keychain();
50
+ if (kc) {
51
+ try {
52
+ const v = await kc.getPassword(SERVICE, ACCOUNT);
53
+ if (v)
54
+ return v;
55
+ }
56
+ catch {
57
+ /* fall through to file */
58
+ }
59
+ }
60
+ return fileGet();
61
+ }
62
+ export async function clearToken() {
63
+ const kc = await keychain();
64
+ if (kc) {
65
+ try {
66
+ await kc.deletePassword(SERVICE, ACCOUNT);
67
+ }
68
+ catch {
69
+ /* ignore */
70
+ }
71
+ }
72
+ await rm(filePath, { force: true });
73
+ }
74
+ //# sourceMappingURL=token-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-store.js","sourceRoot":"","sources":["../../src/auth/token-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAEzE;;;;GAIG;AACH,MAAM,OAAO,GAAG,UAAU,CAAC;AAC3B,MAAM,OAAO,GAAG,eAAe,CAAC;AAChC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC;AAC7C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,kBAAkB,CAAC,CAAC;AAQnD,KAAK,UAAU,QAAQ;IACrB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;QACnC,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,GAAG,CAAwB,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,OAAO,CAAC,KAAa;IAClC,MAAM,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1C,MAAM,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACtE,MAAM,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;AAC/B,CAAC;AACD,KAAK,UAAU,OAAO;IACpB,IAAI,CAAC;QACH,OAAQ,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAwB,CAAC,KAAK,IAAI,IAAI,CAAC;IAC5F,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,KAAa;IAC1C,MAAM,EAAE,GAAG,MAAM,QAAQ,EAAE,CAAC;IAC5B,IAAI,EAAE,EAAE,CAAC;QACP,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;YAC9C,OAAO;QACT,CAAC;QAAC,MAAM,CAAC;YACP,0CAA0C;QAC5C,CAAC;IACH,CAAC;IACD,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ;IAC5B,MAAM,EAAE,GAAG,MAAM,QAAQ,EAAE,CAAC;IAC5B,IAAI,EAAE,EAAE,CAAC;QACP,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,MAAM,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YACjD,IAAI,CAAC;gBAAE,OAAO,CAAC,CAAC;QAClB,CAAC;QAAC,MAAM,CAAC;YACP,0BAA0B;QAC5B,CAAC;IACH,CAAC;IACD,OAAO,OAAO,EAAE,CAAC;AACnB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,MAAM,EAAE,GAAG,MAAM,QAAQ,EAAE,CAAC;IAC5B,IAAI,EAAE,EAAE,CAAC;QACP,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC5C,CAAC;QAAC,MAAM,CAAC;YACP,YAAY;QACd,CAAC;IACH,CAAC;IACD,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACtC,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Contract-surface detection (PRD open-Q1, a safety gate). Pragmatic heuristic:
3
+ * does a changed file expose a public interface others code against? Bias to "shared"
4
+ * when in doubt — a false-negative in the cross-org wedge is a confidentiality leak.
5
+ * Pure + testable.
6
+ */
7
+ export declare function isContractSurface(path: string, content?: string): boolean;
8
+ /** owned (only you own all changed paths, none are contract surfaces) vs shared. */
9
+ export declare function riskTierFor(opts: {
10
+ anyContractSurface: boolean;
11
+ allOwnedByMe: boolean;
12
+ }): "owned" | "shared";
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Contract-surface detection (PRD open-Q1, a safety gate). Pragmatic heuristic:
3
+ * does a changed file expose a public interface others code against? Bias to "shared"
4
+ * when in doubt — a false-negative in the cross-org wedge is a confidentiality leak.
5
+ * Pure + testable.
6
+ */
7
+ export function isContractSurface(path, content) {
8
+ if (/(openapi|swagger)/i.test(path) && /\.(ya?ml|json)$/i.test(path))
9
+ return true;
10
+ if (/\.(proto|graphql|gql)$/i.test(path))
11
+ return true;
12
+ if (/(^|\/)(routes?|controllers?|api|handlers?|endpoints?|contracts?)(\/|\.)/i.test(path))
13
+ return true;
14
+ if (content && /\.(ts|tsx|js|mjs)$/i.test(path)) {
15
+ if (/\bexport\s+(async\s+)?(function|const|class|interface|type)\b/.test(content))
16
+ return true;
17
+ }
18
+ return false;
19
+ }
20
+ /** owned (only you own all changed paths, none are contract surfaces) vs shared. */
21
+ export function riskTierFor(opts) {
22
+ if (opts.anyContractSurface)
23
+ return "shared";
24
+ return opts.allOwnedByMe ? "owned" : "shared";
25
+ }
26
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../../src/capture/classify.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,OAAgB;IAC9D,IAAI,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAClF,IAAI,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACtD,IAAI,0EAA0E,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACvG,IAAI,OAAO,IAAI,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAChD,IAAI,+DAA+D,CAAC,IAAI,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;IACjG,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,WAAW,CAAC,IAA4D;IACtF,IAAI,IAAI,CAAC,kBAAkB;QAAE,OAAO,QAAQ,CAAC;IAC7C,OAAO,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC;AAChD,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { isContractSurface, riskTierFor } from "./classify.js";
4
+ test("detects OpenAPI / schema files as contract surfaces", () => {
5
+ assert.ok(isContractSurface("api/openapi.yaml"));
6
+ assert.ok(isContractSurface("schema.graphql"));
7
+ assert.ok(isContractSurface("proto/orders.proto"));
8
+ });
9
+ test("detects route/controller paths", () => {
10
+ assert.ok(isContractSurface("src/routes/orders.ts"));
11
+ assert.ok(isContractSurface("app/controllers/cart.rb"));
12
+ });
13
+ test("detects exported TS API by content", () => {
14
+ assert.ok(isContractSurface("src/orders.ts", "export function createOrder() {}"));
15
+ assert.ok(!isContractSurface("src/util.ts", "function localHelper() {}"));
16
+ });
17
+ test("plain internal file is not a contract surface", () => {
18
+ assert.ok(!isContractSurface("src/internal/helpers.go", "func helper() {}"));
19
+ });
20
+ test("riskTier biases to shared on contract surfaces", () => {
21
+ assert.equal(riskTierFor({ anyContractSurface: true, allOwnedByMe: true }), "shared");
22
+ assert.equal(riskTierFor({ anyContractSurface: false, allOwnedByMe: true }), "owned");
23
+ assert.equal(riskTierFor({ anyContractSurface: false, allOwnedByMe: false }), "shared");
24
+ });
25
+ //# sourceMappingURL=classify.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.test.js","sourceRoot":"","sources":["../../src/capture/classify.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE/D,IAAI,CAAC,qDAAqD,EAAE,GAAG,EAAE;IAC/D,MAAM,CAAC,EAAE,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,CAAC,CAAC;IACjD,MAAM,CAAC,EAAE,CAAC,iBAAiB,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAC/C,MAAM,CAAC,EAAE,CAAC,iBAAiB,CAAC,oBAAoB,CAAC,CAAC,CAAC;AACrD,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,gCAAgC,EAAE,GAAG,EAAE;IAC1C,MAAM,CAAC,EAAE,CAAC,iBAAiB,CAAC,sBAAsB,CAAC,CAAC,CAAC;IACrD,MAAM,CAAC,EAAE,CAAC,iBAAiB,CAAC,yBAAyB,CAAC,CAAC,CAAC;AAC1D,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,oCAAoC,EAAE,GAAG,EAAE;IAC9C,MAAM,CAAC,EAAE,CAAC,iBAAiB,CAAC,eAAe,EAAE,kCAAkC,CAAC,CAAC,CAAC;IAClF,MAAM,CAAC,EAAE,CAAC,CAAC,iBAAiB,CAAC,aAAa,EAAE,2BAA2B,CAAC,CAAC,CAAC;AAC5E,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,+CAA+C,EAAE,GAAG,EAAE;IACzD,MAAM,CAAC,EAAE,CAAC,CAAC,iBAAiB,CAAC,yBAAyB,EAAE,kBAAkB,CAAC,CAAC,CAAC;AAC/E,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,gDAAgD,EAAE,GAAG,EAAE;IAC1D,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,kBAAkB,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC;IACtF,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,kBAAkB,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;IACtF,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,kBAAkB,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC;AAC1F,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ /** Changed files (tracked vs HEAD + untracked), read-only. Source never leaves the machine. */
2
+ export declare function changedFiles(cwd: string): string[];
@@ -0,0 +1,19 @@
1
+ import { execFileSync } from "node:child_process";
2
+ /** Changed files (tracked vs HEAD + untracked), read-only. Source never leaves the machine. */
3
+ export function changedFiles(cwd) {
4
+ try {
5
+ const tracked = execFileSync("git", ["-C", cwd, "diff", "--name-only", "HEAD"], { encoding: "utf8" })
6
+ .split("\n")
7
+ .filter(Boolean);
8
+ const untracked = execFileSync("git", ["-C", cwd, "ls-files", "--others", "--exclude-standard"], {
9
+ encoding: "utf8",
10
+ })
11
+ .split("\n")
12
+ .filter(Boolean);
13
+ return [...new Set([...tracked, ...untracked])];
14
+ }
15
+ catch {
16
+ return [];
17
+ }
18
+ }
19
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/capture/diff.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,+FAA+F;AAC/F,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;aAClG,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,OAAO,CAAC,CAAC;QACnB,MAAM,SAAS,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,UAAU,EAAE,oBAAoB,CAAC,EAAE;YAC/F,QAAQ,EAAE,MAAM;SACjB,CAAC;aACC,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,OAAO,CAAC,CAAC;QACnB,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Hook entrypoint. Resilient by design — never break the agent: on any error it exits 0.
3
+ * SessionStart → replay inbox + binding decisions as additionalContext.
4
+ * PostToolUse/Stop → diff → classify surface → risk-tiered publish via notify.
5
+ */
6
+ export declare function runCapture(event: string): Promise<void>;