@leantli/agent-handoff 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 agent-handoff contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,209 @@
1
+ # agent-handoff
2
+
3
+ `agent-handoff` gives Codex and Claude Code a shared memory handoff layer across
4
+ new sessions, fresh clones, git worktrees, and devices.
5
+
6
+ A long agent session often accumulates useful context: project background,
7
+ current task state, decisions, preferences, and repeated corrections. Without a
8
+ handoff layer, a new session or another device starts cold.
9
+
10
+ `agent-handoff` stores that context in a user-level vault, then lets any clone or
11
+ worktree of the same repository recover it by project identity.
12
+
13
+ ```text
14
+ ~/.agent-handoff/vault/ # private user memory
15
+ repo/.agent-handoff.yml # lightweight project identity
16
+ repo/AGENTS.md # Codex bootstrap instruction
17
+ repo/CLAUDE.md # Claude Code bootstrap instruction
18
+ ```
19
+
20
+ ## Install
21
+
22
+ From npm:
23
+
24
+ ```bash
25
+ npm install -g @leantli/agent-handoff
26
+ ```
27
+
28
+ From a checkout:
29
+
30
+ ```bash
31
+ npm install
32
+ npm run build
33
+ npm link
34
+ ```
35
+
36
+ ## Three-Minute Setup
37
+
38
+ Create your local vault once:
39
+
40
+ ```bash
41
+ agent-handoff setup
42
+ agent-handoff install-skill
43
+ ```
44
+
45
+ Optional: sync the vault through a private git repo so another device can share
46
+ the same handoff memory:
47
+
48
+ ```bash
49
+ agent-handoff setup --sync git@github.com:you/agent-handoff-vault.git
50
+ ```
51
+
52
+ Bootstrap each coding project once:
53
+
54
+ ```bash
55
+ agent-handoff init
56
+ ```
57
+
58
+ This writes:
59
+
60
+ ```text
61
+ .agent-handoff.yml
62
+ AGENTS.md
63
+ CLAUDE.md
64
+ ```
65
+
66
+ It does not write private memory into the project repository.
67
+
68
+ ## Daily Workflow
69
+
70
+ At the start of a new Codex or Claude Code session:
71
+
72
+ ```bash
73
+ agent-handoff sync # only if vault sync is configured
74
+ agent-handoff start
75
+ ```
76
+
77
+ Paste or let the agent read the start packet before it works.
78
+
79
+ When a useful session is about to end, or before switching devices:
80
+
81
+ ```bash
82
+ agent-handoff checkpoint --note "Implemented vault storage; next step is README polish."
83
+ agent-handoff sync # only if vault sync is configured
84
+ ```
85
+
86
+ When the user corrects a stable preference or recurring rule:
87
+
88
+ ```bash
89
+ agent-handoff learn --kind preference --note "Prefer TDD for behavior changes."
90
+ ```
91
+
92
+ For project-specific decisions or branch-specific context:
93
+
94
+ ```bash
95
+ agent-handoff learn --scope project --kind decision --note "Use vault-first storage."
96
+ agent-handoff learn --scope branch --kind context --note "Main is preparing v0.3."
97
+ ```
98
+
99
+ When configured, `sync` commits pending vault changes and pushes them to the
100
+ private vault repository.
101
+
102
+ ## Why This Solves Cross-Clone Context
103
+
104
+ `agent-handoff` identifies a project from `.agent-handoff.yml` or the git
105
+ `origin` remote. These all map to the same vault project:
106
+
107
+ ```text
108
+ ~/code/repo
109
+ ~/tmp/repo
110
+ ~/worktrees/repo-feature
111
+ another device's ~/projects/repo
112
+ ```
113
+
114
+ For example, both remotes below normalize to the same project id:
115
+
116
+ ```text
117
+ https://github.com/leantli/agent-handoff.git
118
+ git@github.com:leantli/agent-handoff.git
119
+
120
+ github.com__leantli__agent-handoff
121
+ ```
122
+
123
+ That means A session can checkpoint context into the vault, and B session can
124
+ recover it from any clone or worktree that resolves to the same project id.
125
+
126
+ ## What Gets Stored
127
+
128
+ The vault is private user state:
129
+
130
+ ```text
131
+ ~/.agent-handoff/
132
+ config.json
133
+ vault/
134
+ global/
135
+ preferences.md
136
+ lessons.md
137
+ projects/
138
+ github.com__owner__repo/
139
+ project.md
140
+ decisions.md
141
+ preferences.md
142
+ branches/
143
+ main.md
144
+ feature-demo.md
145
+ checkpoints/
146
+ 20260508T103000Z-laptop-codex-main.md
147
+ ```
148
+
149
+ The project repository gets only bootstrap files:
150
+
151
+ ```text
152
+ .agent-handoff.yml
153
+ AGENTS.md
154
+ CLAUDE.md
155
+ ```
156
+
157
+ ## Commands
158
+
159
+ ```bash
160
+ agent-handoff setup # create/configure the user vault
161
+ agent-handoff install-skill # install the agent workflow skill
162
+ agent-handoff init # bootstrap the current repo
163
+ agent-handoff start # print the context packet for a new session
164
+ agent-handoff checkpoint # write a session checkpoint
165
+ agent-handoff learn # write durable global/project/branch memory
166
+ agent-handoff sync # git pull/rebase + push the vault
167
+ agent-handoff status # quick readiness check
168
+ agent-handoff doctor # detailed health check
169
+ ```
170
+
171
+ ## Agent Skill
172
+
173
+ This repo includes a Codex-compatible skill:
174
+
175
+ ```text
176
+ .agents/skills/agent-handoff/SKILL.md
177
+ ```
178
+
179
+ The skill tells an agent when to run `start`, `checkpoint`, `learn`, and `sync`.
180
+ Install it into your user skills directory to make the workflow available across
181
+ all repositories:
182
+
183
+ ```bash
184
+ agent-handoff install-skill
185
+ ```
186
+
187
+ The repo also keeps a copy at `.agents/skills/agent-handoff/SKILL.md` for
188
+ project-local use.
189
+
190
+ ## Status
191
+
192
+ This is an early prototype. It does not read proprietary chat transcripts or
193
+ client-internal state. Agents must still call `start`, `checkpoint`, and `learn`
194
+ at the right moments, guided by `AGENTS.md` and `CLAUDE.md`.
195
+
196
+ ## Development
197
+
198
+ Run tests:
199
+
200
+ ```bash
201
+ npm test
202
+ ```
203
+
204
+ Run type checking and build:
205
+
206
+ ```bash
207
+ npm run typecheck
208
+ npm run build
209
+ ```
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "./cli.js";
3
+ process.exitCode = main();
package/dist/cli.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export interface MainOptions {
2
+ cwd?: string;
3
+ stdout?: NodeJS.WritableStream;
4
+ stderr?: NodeJS.WritableStream;
5
+ stdin?: {
6
+ isTTY?: boolean;
7
+ };
8
+ }
9
+ export declare function main(argv?: string[], opts?: MainOptions): number;
package/dist/cli.js ADDED
@@ -0,0 +1,177 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { Command, CommanderError, InvalidArgumentError, Option } from "commander";
3
+ import { HandoffError, buildStartPacket, doctor, getStatus, initRepo, installSkill, learn, setupHome, syncVault, writeCheckpoint, } from "./core.js";
4
+ export function main(argv = process.argv.slice(2), opts = {}) {
5
+ const stdout = opts.stdout ?? process.stdout;
6
+ const stderr = opts.stderr ?? process.stderr;
7
+ const program = buildProgram(stdout, stderr, opts.stdin, opts.cwd);
8
+ try {
9
+ program.parse(argv, { from: "user" });
10
+ return 0;
11
+ }
12
+ catch (error) {
13
+ if (error instanceof CommanderError) {
14
+ return error.exitCode;
15
+ }
16
+ if (error instanceof HandoffError) {
17
+ stderr.write(`${error.message}\n`);
18
+ return 1;
19
+ }
20
+ throw error;
21
+ }
22
+ }
23
+ function buildProgram(stdout, stderr, stdin, cwd) {
24
+ const program = new Command();
25
+ program
26
+ .name("agent-handoff")
27
+ .description("Shared vault handoff memory for Codex and Claude Code.")
28
+ .version("agent-handoff 0.4.0")
29
+ .exitOverride()
30
+ .configureOutput({
31
+ writeOut: (str) => stdout.write(str),
32
+ writeErr: (str) => stderr.write(str),
33
+ })
34
+ .option("--home <path>", "Agent handoff home directory. Defaults to ~/.agent-handoff.");
35
+ program
36
+ .command("setup")
37
+ .description("Create or configure the user vault.")
38
+ .option("--vault <path>", "Vault directory. Defaults to HOME/vault.")
39
+ .option("--sync <url>", "Optional git remote URL for vault sync.")
40
+ .action((options) => {
41
+ const result = setupHome({ home: globalHome(program), vault: options.vault, syncUrl: options.sync });
42
+ stdout.write(`Agent handoff home: ${result.home}\n`);
43
+ stdout.write(`Vault: ${result.vault}\n`);
44
+ });
45
+ program
46
+ .command("install-skill")
47
+ .description("Install the agent-handoff skill into a user skills directory.")
48
+ .option("--skills-home <path>", "Skills home directory. Defaults to ~/.agents/skills.")
49
+ .action((options) => {
50
+ const result = installSkill({ skillsHome: options.skillsHome });
51
+ const verb = result.updated ? "Installed skill" : "Skill already installed";
52
+ stdout.write(`${verb}: ${result.path}\n`);
53
+ });
54
+ program
55
+ .command("init")
56
+ .description("Bootstrap this repo for agent handoff.")
57
+ .option("--project-id <id>", "Override detected project id.")
58
+ .option("--branch <branch>", "Override detected branch.")
59
+ .addOption(new Option("--client <client>", "Client bootstrap to install. Repeat to install multiple. Defaults to both.")
60
+ .choices(["codex", "claude"])
61
+ .argParser(collect))
62
+ .action((options) => {
63
+ const result = initRepo({
64
+ root: cwd,
65
+ home: globalHome(program),
66
+ projectId: options.projectId,
67
+ branch: options.branch,
68
+ clients: options.client,
69
+ });
70
+ stdout.write(`Initialized agent handoff for ${result.projectId}.\n`);
71
+ stdout.write(`Vault project: ${result.vaultProject}\n`);
72
+ });
73
+ program
74
+ .command("start")
75
+ .description("Print context for a new agent session.")
76
+ .option("--branch <branch>", "Override detected branch.")
77
+ .action((options) => {
78
+ stdout.write(`${buildStartPacket({ root: cwd, home: globalHome(program), branch: options.branch })}\n`);
79
+ });
80
+ program
81
+ .command("checkpoint")
82
+ .description("Write a session checkpoint.")
83
+ .option("--note <text>", "Note text. If omitted, stdin is used.")
84
+ .option("--file <path>", "Read note text from a file.")
85
+ .option("--device <name>", "Device name for the checkpoint.")
86
+ .option("--agent <name>", "Agent/client name, such as codex or claude.")
87
+ .option("--branch <branch>", "Override detected branch.")
88
+ .action((options) => {
89
+ const result = writeCheckpoint({
90
+ root: cwd,
91
+ home: globalHome(program),
92
+ note: readNote(options, stdin),
93
+ device: options.device,
94
+ agent: options.agent,
95
+ branch: options.branch,
96
+ });
97
+ stdout.write(`Wrote checkpoint: ${result.path}\n`);
98
+ });
99
+ program
100
+ .command("learn")
101
+ .description("Store durable handoff memory.")
102
+ .option("--note <text>", "Note text. If omitted, stdin is used.")
103
+ .option("--file <path>", "Read note text from a file.")
104
+ .addOption(new Option("--scope <scope>", "Where to store the learned memory.").choices(["global", "project", "branch"]).default("global"))
105
+ .addOption(new Option("--kind <kind>", "Kind of durable memory to write.").choices(["preference", "lesson", "decision", "context"]).default("preference"))
106
+ .option("--branch <branch>", "Branch to use with --scope branch.")
107
+ .action((options) => {
108
+ const result = learn(readNote(options, stdin), {
109
+ root: cwd,
110
+ home: globalHome(program),
111
+ scope: options.scope,
112
+ kind: options.kind,
113
+ branch: options.branch,
114
+ });
115
+ stdout.write(`Learned ${result.kind}: ${result.path}\n`);
116
+ });
117
+ program
118
+ .command("sync")
119
+ .description("Pull and push the vault git repository.")
120
+ .action(() => {
121
+ for (const output of syncVault({ home: globalHome(program) })) {
122
+ if (output)
123
+ stdout.write(`${output}\n`);
124
+ }
125
+ });
126
+ program
127
+ .command("status")
128
+ .description("Show whether handoff is ready here.")
129
+ .action(() => {
130
+ const status = getStatus({ root: cwd, home: globalHome(program) });
131
+ if (status.initialized) {
132
+ stdout.write(`Agent handoff is ready for ${status.projectId}.\n`);
133
+ }
134
+ else {
135
+ printProblems(status.problems, stdout);
136
+ throw new CommanderError(1, "agent-handoff.status", "status failed");
137
+ }
138
+ });
139
+ program
140
+ .command("doctor")
141
+ .description("Check bootstrap and vault health.")
142
+ .action(() => {
143
+ const report = doctor({ root: cwd, home: globalHome(program) });
144
+ if (report.ok) {
145
+ stdout.write(`Agent handoff is healthy for ${report.projectId}.\n`);
146
+ }
147
+ else {
148
+ printProblems(report.problems, stdout);
149
+ throw new CommanderError(1, "agent-handoff.doctor", "doctor failed");
150
+ }
151
+ });
152
+ return program;
153
+ }
154
+ function globalHome(program) {
155
+ return program.opts().home;
156
+ }
157
+ function collect(value, previous) {
158
+ if (value !== "codex" && value !== "claude") {
159
+ throw new InvalidArgumentError("client must be codex or claude");
160
+ }
161
+ return [...(previous ?? []), value];
162
+ }
163
+ function readNote(options, stdin) {
164
+ if (options.note)
165
+ return options.note;
166
+ if (options.file)
167
+ return readFileSync(options.file, "utf8");
168
+ if (stdin?.isTTY ?? process.stdin.isTTY) {
169
+ throw new HandoffError("provide --note or --file, or pipe note text on stdin");
170
+ }
171
+ return readFileSync(0, "utf8");
172
+ }
173
+ function printProblems(problems, stdout) {
174
+ for (const problem of problems) {
175
+ stdout.write(`- ${problem}\n`);
176
+ }
177
+ }
package/dist/core.d.ts ADDED
@@ -0,0 +1,103 @@
1
+ export declare const DEFAULT_HOME: string;
2
+ export declare const CONFIG_FILE = "config.json";
3
+ export declare const BOOTSTRAP_FILE = ".agent-handoff.yml";
4
+ type Client = "codex" | "claude";
5
+ type LearnScope = "global" | "project" | "branch";
6
+ type LearnKind = "preference" | "lesson" | "decision" | "context";
7
+ export declare class HandoffError extends Error {
8
+ constructor(message: string);
9
+ }
10
+ export interface SetupResult {
11
+ home: string;
12
+ vault: string;
13
+ created: number;
14
+ updated: number;
15
+ }
16
+ export interface InitResult {
17
+ created: number;
18
+ updated: number;
19
+ root: string;
20
+ projectId: string;
21
+ vaultProject: string;
22
+ clients: Client[];
23
+ }
24
+ export interface CheckpointResult {
25
+ path: string;
26
+ projectId: string;
27
+ branch: string;
28
+ createdAt: string;
29
+ }
30
+ export interface LearnResult {
31
+ path: string;
32
+ kind: string;
33
+ createdAt: string;
34
+ }
35
+ export interface InstallSkillResult {
36
+ path: string;
37
+ updated: boolean;
38
+ }
39
+ export interface Status {
40
+ initialized: boolean;
41
+ problems: string[];
42
+ root: string;
43
+ projectId: string | null;
44
+ }
45
+ export interface DoctorReport {
46
+ ok: boolean;
47
+ problems: string[];
48
+ root: string;
49
+ projectId: string | null;
50
+ }
51
+ export declare function setupHome(opts?: {
52
+ home?: string;
53
+ vault?: string;
54
+ syncUrl?: string;
55
+ }): SetupResult;
56
+ export declare function normalizeProjectId(value: string): string;
57
+ export declare function installSkill(opts?: {
58
+ skillsHome?: string;
59
+ }): InstallSkillResult;
60
+ export declare function initRepo(opts?: {
61
+ root?: string;
62
+ home?: string;
63
+ projectId?: string;
64
+ branch?: string;
65
+ clients?: string[];
66
+ }): InitResult;
67
+ export declare function deriveProjectId(root?: string, projectId?: string): string;
68
+ export declare function currentBranch(root?: string): string;
69
+ export declare function buildStartPacket(opts?: {
70
+ root?: string;
71
+ home?: string;
72
+ branch?: string;
73
+ maxCheckpoints?: number;
74
+ }): string;
75
+ export declare function writeCheckpoint(opts: {
76
+ root?: string;
77
+ note: string;
78
+ home?: string;
79
+ now?: Date;
80
+ device?: string;
81
+ agent?: string;
82
+ branch?: string;
83
+ }): CheckpointResult;
84
+ export declare function learn(note: string, opts?: {
85
+ home?: string;
86
+ root?: string;
87
+ scope?: LearnScope;
88
+ kind?: LearnKind;
89
+ branch?: string;
90
+ now?: Date;
91
+ }): LearnResult;
92
+ export declare function syncVault(opts?: {
93
+ home?: string;
94
+ }): string[];
95
+ export declare function getStatus(opts?: {
96
+ root?: string;
97
+ home?: string;
98
+ }): Status;
99
+ export declare function doctor(opts?: {
100
+ root?: string;
101
+ home?: string;
102
+ }): DoctorReport;
103
+ export {};
package/dist/core.js ADDED
@@ -0,0 +1,678 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
3
+ import { hostname, homedir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ export const DEFAULT_HOME = join(homedir(), ".agent-handoff");
7
+ export const CONFIG_FILE = "config.json";
8
+ export const BOOTSTRAP_FILE = ".agent-handoff.yml";
9
+ const MANAGED_BEGIN = "<!-- BEGIN AGENT-HANDOFF -->";
10
+ const MANAGED_END = "<!-- END AGENT-HANDOFF -->";
11
+ const SECRET_PATTERNS = [
12
+ /(api[_-]?key|token|secret|password)\s*[:=]/i,
13
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
14
+ /\bsk-[A-Za-z0-9_-]{8,}\b/,
15
+ ];
16
+ export class HandoffError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = "HandoffError";
20
+ }
21
+ }
22
+ export function setupHome(opts = {}) {
23
+ const homePath = resolveHome(opts.home);
24
+ const vaultPath = opts.vault ? resolve(opts.vault) : join(homePath, "vault");
25
+ let created = 0;
26
+ let updated = 0;
27
+ if (!existsSync(homePath)) {
28
+ mkdirSync(homePath, { recursive: true });
29
+ created += 1;
30
+ }
31
+ if (opts.syncUrl && cloneVaultIfNeeded(homePath, vaultPath, opts.syncUrl)) {
32
+ created += 1;
33
+ }
34
+ for (const directory of [vaultPath, join(vaultPath, "global"), join(vaultPath, "projects")]) {
35
+ if (!existsSync(directory)) {
36
+ mkdirSync(directory, { recursive: true });
37
+ created += 1;
38
+ }
39
+ }
40
+ for (const [filename, contents] of Object.entries(globalSeedFiles())) {
41
+ const path = join(vaultPath, "global", filename);
42
+ if (!existsSync(path)) {
43
+ writeFileSync(path, contents, "utf8");
44
+ created += 1;
45
+ }
46
+ }
47
+ const configPath = join(homePath, CONFIG_FILE);
48
+ const desired = { version: 2, vault: vaultPath };
49
+ if (opts.syncUrl)
50
+ desired.sync_url = opts.syncUrl;
51
+ if (existsSync(configPath)) {
52
+ const existing = JSON.parse(readFileSync(configPath, "utf8"));
53
+ let changed = false;
54
+ if (existing.version !== 2) {
55
+ existing.version = 2;
56
+ changed = true;
57
+ }
58
+ if (existing.vault !== vaultPath) {
59
+ existing.vault = vaultPath;
60
+ changed = true;
61
+ }
62
+ if (opts.syncUrl && existing.sync_url !== opts.syncUrl) {
63
+ existing.sync_url = opts.syncUrl;
64
+ changed = true;
65
+ }
66
+ if (changed) {
67
+ writeFileSync(configPath, json(existing), "utf8");
68
+ updated += 1;
69
+ }
70
+ }
71
+ else {
72
+ writeFileSync(configPath, json(desired), "utf8");
73
+ created += 1;
74
+ }
75
+ if (opts.syncUrl) {
76
+ ensureGitRemote(vaultPath, opts.syncUrl);
77
+ }
78
+ return { home: homePath, vault: vaultPath, created, updated };
79
+ }
80
+ export function normalizeProjectId(value) {
81
+ const raw = value.trim();
82
+ let host = "";
83
+ let path = "";
84
+ if (raw.startsWith("git@") && raw.includes(":")) {
85
+ const [userHost, remotePath] = raw.split(":", 2);
86
+ host = userHost.split("@", 2)[1] ?? "";
87
+ path = remotePath;
88
+ }
89
+ else if (raw.includes("://")) {
90
+ const parsed = new URL(raw);
91
+ host = parsed.hostname || parsed.host;
92
+ path = parsed.pathname;
93
+ }
94
+ else {
95
+ return safeProjectId(raw);
96
+ }
97
+ path = path.replace(/^\/+|\/+$/g, "");
98
+ if (path.endsWith(".git")) {
99
+ path = path.slice(0, -4);
100
+ }
101
+ return safeProjectId([host.toLowerCase(), ...path.split("/").filter(Boolean)].join("__"));
102
+ }
103
+ export function installSkill(opts = {}) {
104
+ const skillsHome = opts.skillsHome
105
+ ? resolve(opts.skillsHome)
106
+ : resolve(join(homedir(), ".agents", "skills"));
107
+ const skillDir = join(skillsHome, "agent-handoff");
108
+ mkdirSync(skillDir, { recursive: true });
109
+ const path = join(skillDir, "SKILL.md");
110
+ const content = readResource("agent-handoff.SKILL.md");
111
+ const updated = !existsSync(path) || readFileSync(path, "utf8") !== content;
112
+ if (updated) {
113
+ writeFileSync(path, content, "utf8");
114
+ }
115
+ return { path, updated };
116
+ }
117
+ export function initRepo(opts = {}) {
118
+ const rootPath = resolve(opts.root ?? ".");
119
+ const setup = setupHome({ home: opts.home });
120
+ const pid = deriveProjectId(rootPath, opts.projectId);
121
+ const branchName = opts.branch ?? currentBranch(rootPath);
122
+ const selectedClients = normalizeClients(opts.clients);
123
+ let created = 0;
124
+ let updated = 0;
125
+ const bootstrapPath = join(rootPath, BOOTSTRAP_FILE);
126
+ const bootstrapContents = `version: 2\nproject_id: ${pid}\nclients: ${selectedClients.join(",")}\n`;
127
+ if (!existsSync(bootstrapPath)) {
128
+ writeFileSync(bootstrapPath, bootstrapContents, "utf8");
129
+ created += 1;
130
+ }
131
+ else {
132
+ const data = readBootstrap(bootstrapPath);
133
+ const existingClients = clientsFromBootstrap(data);
134
+ if (data.project_id !== pid ||
135
+ data.version !== "2" ||
136
+ existingClients.join(",") !== selectedClients.join(",")) {
137
+ writeFileSync(bootstrapPath, bootstrapContents, "utf8");
138
+ updated += 1;
139
+ }
140
+ }
141
+ for (const filename of clientInstructionFiles(selectedClients)) {
142
+ const changed = ensureManagedBlock(join(rootPath, filename));
143
+ if (changed.changed) {
144
+ if (changed.created)
145
+ created += 1;
146
+ else
147
+ updated += 1;
148
+ }
149
+ }
150
+ const projectPath = vaultProjectPath(setup.vault, pid);
151
+ created += ensureProjectFiles(projectPath, branchName);
152
+ return {
153
+ created,
154
+ updated,
155
+ root: rootPath,
156
+ projectId: pid,
157
+ vaultProject: projectPath,
158
+ clients: selectedClients,
159
+ };
160
+ }
161
+ export function deriveProjectId(root = ".", projectId) {
162
+ const rootPath = resolve(root);
163
+ if (projectId)
164
+ return coerceProjectId(projectId);
165
+ const bootstrapPath = join(rootPath, BOOTSTRAP_FILE);
166
+ if (existsSync(bootstrapPath)) {
167
+ const data = readBootstrap(bootstrapPath);
168
+ if (data.project_id)
169
+ return coerceProjectId(data.project_id);
170
+ }
171
+ const remote = gitOutput(rootPath, ["remote", "get-url", "origin"]);
172
+ if (remote)
173
+ return normalizeProjectId(remote);
174
+ return safeProjectId(rootPath.split(/[\\/]/).pop() ?? "unknown-project");
175
+ }
176
+ export function currentBranch(root = ".") {
177
+ return gitOutput(resolve(root), ["branch", "--show-current"]) ?? "default";
178
+ }
179
+ export function buildStartPacket(opts = {}) {
180
+ const rootPath = resolve(opts.root ?? ".");
181
+ const status = getStatus({ root: rootPath, home: opts.home });
182
+ if (!status.initialized) {
183
+ throw new HandoffError(statusError(status));
184
+ }
185
+ const setup = loadSetup(opts.home);
186
+ const pid = status.projectId ?? deriveProjectId(rootPath);
187
+ const branchName = opts.branch ?? currentBranch(rootPath);
188
+ const projectPath = vaultProjectPath(setup.vault, pid);
189
+ const branchFile = join(projectPath, "branches", `${safeName(branchName)}.md`);
190
+ const sections = [
191
+ ["Global Preferences", join(setup.vault, "global", "preferences.md")],
192
+ ["Global Lessons", join(setup.vault, "global", "lessons.md")],
193
+ ["Project Context", join(projectPath, "project.md")],
194
+ ["Project Preferences", join(projectPath, "preferences.md")],
195
+ ["Project Decisions", join(projectPath, "decisions.md")],
196
+ ["Branch Context", branchFile],
197
+ ];
198
+ const lines = [
199
+ "# Agent Handoff Start Packet",
200
+ "",
201
+ `Project: \`${pid}\``,
202
+ `Branch: \`${branchName}\``,
203
+ "",
204
+ "Read this packet before making changes. Use it to recover context from previous Codex and Claude Code sessions.",
205
+ ];
206
+ for (const [title, path] of sections) {
207
+ lines.push(...renderSection(title, path));
208
+ }
209
+ const checkpoints = latestCheckpoints(projectPath, opts.maxCheckpoints ?? 5, branchName);
210
+ if (checkpoints.length > 0) {
211
+ lines.push("", "## Recent Checkpoints");
212
+ for (const path of checkpoints) {
213
+ lines.push(...renderSection(path.split(/[\\/]/).pop() ?? path, path, 3));
214
+ }
215
+ }
216
+ lines.push("");
217
+ return lines.join("\n");
218
+ }
219
+ export function writeCheckpoint(opts) {
220
+ const rootPath = resolve(opts.root ?? ".");
221
+ const status = getStatus({ root: rootPath, home: opts.home });
222
+ if (!status.initialized) {
223
+ throw new HandoffError(statusError(status));
224
+ }
225
+ const cleanedNote = cleanNote(opts.note);
226
+ if (!cleanedNote)
227
+ throw new HandoffError("checkpoint note cannot be empty");
228
+ rejectLikelySecret(cleanedNote);
229
+ const setup = loadSetup(opts.home);
230
+ const pid = status.projectId ?? deriveProjectId(rootPath);
231
+ const branchName = opts.branch ?? currentBranch(rootPath);
232
+ const createdAt = timestamp(opts.now);
233
+ const projectPath = vaultProjectPath(setup.vault, pid);
234
+ const checkpoints = join(projectPath, "checkpoints");
235
+ mkdirSync(checkpoints, { recursive: true });
236
+ const deviceLabel = opts.device ?? hostname() ?? "device";
237
+ const agentLabel = opts.agent ?? "agent";
238
+ const filename = `${compactTimestamp(createdAt)}-${safeName(deviceLabel)}-${safeName(agentLabel)}-${safeName(branchName)}.md`;
239
+ const path = join(checkpoints, filename);
240
+ const contents = [
241
+ "# Checkpoint",
242
+ "",
243
+ `created_at: ${createdAt}`,
244
+ `project_id: ${pid}`,
245
+ `branch: ${branchName}`,
246
+ `device: ${deviceLabel}`,
247
+ `agent: ${agentLabel}`,
248
+ "",
249
+ "## Notes",
250
+ "",
251
+ cleanedNote,
252
+ "",
253
+ ].join("\n");
254
+ writeFileSync(path, contents, "utf8");
255
+ return { path, projectId: pid, branch: branchName, createdAt };
256
+ }
257
+ export function learn(note, opts = {}) {
258
+ const clean = cleanNote(note);
259
+ if (!clean)
260
+ throw new HandoffError("learn note cannot be empty");
261
+ rejectLikelySecret(clean);
262
+ const scope = opts.scope ?? "global";
263
+ const kind = opts.kind ?? "preference";
264
+ if (!["global", "project", "branch"].includes(scope)) {
265
+ throw new HandoffError("learn scope must be 'global', 'project', or 'branch'");
266
+ }
267
+ if (!["preference", "lesson", "decision", "context"].includes(kind)) {
268
+ throw new HandoffError("learn kind must be 'preference', 'lesson', 'decision', or 'context'");
269
+ }
270
+ const setup = setupHome({ home: opts.home });
271
+ const path = learnTargetPath(setup, resolve(opts.root ?? "."), scope, kind, opts.branch);
272
+ const createdAt = timestamp(opts.now);
273
+ appendFile(path, `\n- ${createdAt}: ${clean}\n`);
274
+ return { path, kind: scope === "global" ? kind : `${scope} ${kind}`, createdAt };
275
+ }
276
+ export function syncVault(opts = {}) {
277
+ const setup = loadSetup(opts.home);
278
+ if (!existsSync(join(setup.vault, ".git"))) {
279
+ throw new HandoffError("vault is not a git repository; run setup --sync first");
280
+ }
281
+ const outputs = [];
282
+ gitChecked(setup.vault, ["add", "-A"]);
283
+ const staged = gitRun(setup.vault, ["diff", "--cached", "--quiet"]).status !== 0;
284
+ if (staged) {
285
+ outputs.push(gitChecked(setup.vault, [
286
+ "-c",
287
+ "user.name=agent-handoff",
288
+ "-c",
289
+ "user.email=agent-handoff@local",
290
+ "commit",
291
+ "-m",
292
+ "chore: sync agent handoff vault",
293
+ ]));
294
+ }
295
+ const branch = gitOutput(setup.vault, ["branch", "--show-current"]) ?? "main";
296
+ const pull = gitRun(setup.vault, ["pull", "--rebase", "--autostash", "origin", branch]);
297
+ if (pull.status !== 0 && !isEmptyRemotePull(pull.output)) {
298
+ throw new HandoffError(pull.output.trim() || "git pull failed");
299
+ }
300
+ if (pull.output.trim())
301
+ outputs.push(pull.output.trim());
302
+ const push = gitRun(setup.vault, ["push", "-u", "origin", branch]);
303
+ if (push.status !== 0) {
304
+ throw new HandoffError(push.output.trim() || "git push failed");
305
+ }
306
+ if (push.output.trim())
307
+ outputs.push(push.output.trim());
308
+ return outputs;
309
+ }
310
+ export function getStatus(opts = {}) {
311
+ const rootPath = resolve(opts.root ?? ".");
312
+ const problems = [];
313
+ let projectId = null;
314
+ let clients = ["codex", "claude"];
315
+ const bootstrapPath = join(rootPath, BOOTSTRAP_FILE);
316
+ if (!existsSync(bootstrapPath)) {
317
+ problems.push(`${BOOTSTRAP_FILE} is missing`);
318
+ }
319
+ else {
320
+ const data = readBootstrap(bootstrapPath);
321
+ projectId = data.project_id ?? null;
322
+ if (!projectId)
323
+ problems.push(`${BOOTSTRAP_FILE} is missing project_id`);
324
+ clients = clientsFromBootstrap(data);
325
+ }
326
+ for (const filename of clientInstructionFiles(clients)) {
327
+ if (!hasManagedBlock(join(rootPath, filename))) {
328
+ problems.push(`${filename} is missing the managed handoff block`);
329
+ }
330
+ }
331
+ const config = readConfig(opts.home);
332
+ if (!config) {
333
+ problems.push("vault config is missing; run agent-handoff setup");
334
+ }
335
+ else if (!existsSync(config.vault)) {
336
+ problems.push(`vault directory is missing: ${config.vault}`);
337
+ }
338
+ else if (projectId) {
339
+ const projectPath = vaultProjectPath(config.vault, projectId);
340
+ if (!existsSync(projectPath)) {
341
+ problems.push(`vault project is missing: ${projectId}`);
342
+ }
343
+ }
344
+ return { initialized: problems.length === 0, problems, root: rootPath, projectId };
345
+ }
346
+ export function doctor(opts = {}) {
347
+ const status = getStatus(opts);
348
+ return {
349
+ ok: status.initialized,
350
+ problems: status.problems,
351
+ root: status.root,
352
+ projectId: status.projectId,
353
+ };
354
+ }
355
+ function globalSeedFiles() {
356
+ return {
357
+ "preferences.md": "# Global Preferences\n\n",
358
+ "lessons.md": "# Global Lessons\n\n",
359
+ };
360
+ }
361
+ function projectSeedFiles() {
362
+ return {
363
+ "project.md": "# Project Context\n\n",
364
+ "decisions.md": "# Decisions\n\n",
365
+ "preferences.md": "# Project Preferences\n\n",
366
+ };
367
+ }
368
+ function ensureProjectFiles(projectPath, branch) {
369
+ let created = 0;
370
+ for (const directory of [projectPath, join(projectPath, "branches"), join(projectPath, "checkpoints")]) {
371
+ if (!existsSync(directory)) {
372
+ mkdirSync(directory, { recursive: true });
373
+ created += 1;
374
+ }
375
+ }
376
+ for (const [filename, contents] of Object.entries(projectSeedFiles())) {
377
+ const path = join(projectPath, filename);
378
+ if (!existsSync(path)) {
379
+ writeFileSync(path, contents, "utf8");
380
+ created += 1;
381
+ }
382
+ }
383
+ const branchPath = join(projectPath, "branches", `${safeName(branch)}.md`);
384
+ if (!existsSync(branchPath)) {
385
+ writeFileSync(branchPath, `# Branch Context: ${branch}\n\n`, "utf8");
386
+ created += 1;
387
+ }
388
+ return created;
389
+ }
390
+ function learnTargetPath(setup, root, scope, kind, branch) {
391
+ if (scope === "global") {
392
+ if (!["preference", "lesson"].includes(kind)) {
393
+ throw new HandoffError("global learn kind must be 'preference' or 'lesson'");
394
+ }
395
+ return join(setup.vault, "global", kind === "preference" ? "preferences.md" : "lessons.md");
396
+ }
397
+ const status = getStatus({ root, home: setup.home });
398
+ if (!status.initialized) {
399
+ throw new HandoffError(statusError(status));
400
+ }
401
+ const pid = status.projectId ?? deriveProjectId(root);
402
+ const projectPath = vaultProjectPath(setup.vault, pid);
403
+ if (scope === "project") {
404
+ if (kind === "preference")
405
+ return join(projectPath, "preferences.md");
406
+ if (kind === "decision")
407
+ return join(projectPath, "decisions.md");
408
+ return join(projectPath, "project.md");
409
+ }
410
+ const branchName = branch ?? currentBranch(root);
411
+ const branchPath = join(projectPath, "branches", `${safeName(branchName)}.md`);
412
+ if (!existsSync(branchPath)) {
413
+ writeFileSync(branchPath, `# Branch Context: ${branchName}\n\n`, "utf8");
414
+ }
415
+ return branchPath;
416
+ }
417
+ function normalizeClients(clients) {
418
+ const selected = clients && clients.length > 0 ? clients : ["codex", "claude"];
419
+ const deduped = [];
420
+ for (const client of selected) {
421
+ if (client !== "codex" && client !== "claude") {
422
+ throw new HandoffError(`unsupported client(s): ${client}`);
423
+ }
424
+ if (!deduped.includes(client))
425
+ deduped.push(client);
426
+ }
427
+ return deduped;
428
+ }
429
+ function clientsFromBootstrap(data) {
430
+ if (!data.clients)
431
+ return ["codex", "claude"];
432
+ return normalizeClients(data.clients.split(",").map((client) => client.trim()).filter(Boolean));
433
+ }
434
+ function clientInstructionFiles(clients) {
435
+ const files = [];
436
+ if (clients.includes("codex"))
437
+ files.push("AGENTS.md");
438
+ if (clients.includes("claude"))
439
+ files.push("CLAUDE.md");
440
+ return files;
441
+ }
442
+ function resolveHome(home) {
443
+ return resolve(home ? expandHome(home) : DEFAULT_HOME);
444
+ }
445
+ function expandHome(path) {
446
+ if (path === "~")
447
+ return homedir();
448
+ if (path.startsWith("~/"))
449
+ return join(homedir(), path.slice(2));
450
+ return path;
451
+ }
452
+ function readConfig(home) {
453
+ const configPath = join(resolveHome(home), CONFIG_FILE);
454
+ if (!existsSync(configPath))
455
+ return null;
456
+ return JSON.parse(readFileSync(configPath, "utf8"));
457
+ }
458
+ function loadSetup(home) {
459
+ const config = readConfig(home);
460
+ if (!config)
461
+ return setupHome({ home });
462
+ return { home: resolveHome(home), vault: resolve(config.vault), created: 0, updated: 0 };
463
+ }
464
+ function vaultProjectPath(vault, projectId) {
465
+ return join(vault, "projects", coerceProjectId(projectId));
466
+ }
467
+ function coerceProjectId(value) {
468
+ if (value.includes("://") || value.startsWith("git@"))
469
+ return normalizeProjectId(value);
470
+ return safeProjectId(value);
471
+ }
472
+ function safeProjectId(value) {
473
+ let normalized = value.trim().replace(/^\/+|\/+$/g, "");
474
+ if (normalized.endsWith(".git"))
475
+ normalized = normalized.slice(0, -4);
476
+ normalized = normalized.replace(/[/:]/g, "__").replace(/[^A-Za-z0-9._-]+/g, "__");
477
+ normalized = normalized.replace(/__+/g, "__").replace(/^_+|_+$/g, "");
478
+ return normalized || "unknown-project";
479
+ }
480
+ function safeName(value) {
481
+ const normalized = value.trim().replace(/[^A-Za-z0-9._-]+/g, "__").replace(/__+/g, "__").replace(/^_+|_+$/g, "");
482
+ return normalized || "default";
483
+ }
484
+ function readBootstrap(path) {
485
+ const data = {};
486
+ for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
487
+ if (!line.includes(":") || line.trimStart().startsWith("#"))
488
+ continue;
489
+ const index = line.indexOf(":");
490
+ data[line.slice(0, index).trim()] = line.slice(index + 1).trim().replace(/^["']|["']$/g, "");
491
+ }
492
+ return data;
493
+ }
494
+ function managedBlock() {
495
+ return [
496
+ MANAGED_BEGIN,
497
+ "Agent handoff is enabled for this repository.",
498
+ "",
499
+ "At the start of a new Codex or Claude Code session, run `agent-handoff sync` if vault sync is configured, then run:",
500
+ "",
501
+ "```bash",
502
+ "agent-handoff start",
503
+ "```",
504
+ "",
505
+ "Read the returned packet before making changes.",
506
+ "",
507
+ "Before pausing work, switching devices, or ending a useful session, run:",
508
+ "",
509
+ "```bash",
510
+ 'agent-handoff checkpoint --note "<current goal, progress, open questions, next step>"',
511
+ "```",
512
+ "",
513
+ "Then run `agent-handoff sync` if vault sync is configured.",
514
+ "",
515
+ 'When the user corrects a stable preference or recurring rule, run `agent-handoff learn --kind preference --note "..."`.',
516
+ MANAGED_END,
517
+ "",
518
+ ].join("\n");
519
+ }
520
+ function ensureManagedBlock(path) {
521
+ const block = managedBlock();
522
+ if (!existsSync(path)) {
523
+ writeFileSync(path, block, "utf8");
524
+ return { changed: true, created: true };
525
+ }
526
+ const original = readFileSync(path, "utf8");
527
+ let updated;
528
+ if (original.includes(MANAGED_BEGIN) && original.includes(MANAGED_END)) {
529
+ const before = original.split(MANAGED_BEGIN, 1)[0];
530
+ const after = original.split(MANAGED_END, 2)[1] ?? "";
531
+ const parts = [];
532
+ if (before.trimEnd())
533
+ parts.push(before.trimEnd());
534
+ parts.push(block.trimEnd());
535
+ if (after.trimStart())
536
+ parts.push(after.trimStart());
537
+ updated = `${parts.join("\n\n")}\n`;
538
+ }
539
+ else {
540
+ updated = `${original.trimEnd()}\n\n${block}`;
541
+ }
542
+ if (updated !== original) {
543
+ writeFileSync(path, updated, "utf8");
544
+ return { changed: true, created: false };
545
+ }
546
+ return { changed: false, created: false };
547
+ }
548
+ function hasManagedBlock(path) {
549
+ if (!existsSync(path))
550
+ return false;
551
+ const contents = readFileSync(path, "utf8");
552
+ return contents.includes(MANAGED_BEGIN) && contents.includes(MANAGED_END);
553
+ }
554
+ function renderSection(title, path, headingLevel = 2) {
555
+ const heading = "#".repeat(headingLevel);
556
+ if (!existsSync(path)) {
557
+ return ["", `${heading} ${title}`, "", "_Missing._"];
558
+ }
559
+ return ["", `${heading} ${title}`, "", readFileSync(path, "utf8").trimEnd()];
560
+ }
561
+ function latestCheckpoints(projectPath, limit, branch) {
562
+ const checkpointDir = join(projectPath, "checkpoints");
563
+ if (!existsSync(checkpointDir))
564
+ return [];
565
+ let paths = readdirSync(checkpointDir)
566
+ .filter((name) => name.endsWith(".md"))
567
+ .sort()
568
+ .map((name) => join(checkpointDir, name));
569
+ if (branch !== undefined) {
570
+ paths = paths.filter((path) => checkpointBranch(path) === branch);
571
+ }
572
+ return paths.slice(-limit);
573
+ }
574
+ function checkpointBranch(path) {
575
+ for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
576
+ if (line.startsWith("branch:"))
577
+ return line.split(":", 2)[1].trim();
578
+ }
579
+ return null;
580
+ }
581
+ function timestamp(now) {
582
+ const value = now ?? new Date();
583
+ const year = value.getUTCFullYear();
584
+ const month = pad(value.getUTCMonth() + 1);
585
+ const day = pad(value.getUTCDate());
586
+ const hour = pad(value.getUTCHours());
587
+ const minute = pad(value.getUTCMinutes());
588
+ const second = pad(value.getUTCSeconds());
589
+ return `${year}-${month}-${day}T${hour}:${minute}:${second}+00:00`;
590
+ }
591
+ function compactTimestamp(value) {
592
+ return value.replace("+00:00", "Z").replace(/[:.-]/g, "");
593
+ }
594
+ function pad(value) {
595
+ return value.toString().padStart(2, "0");
596
+ }
597
+ function cleanNote(note) {
598
+ return note
599
+ .trim()
600
+ .split(/\r?\n/)
601
+ .map((line) => line.trimEnd())
602
+ .join("\n")
603
+ .trim();
604
+ }
605
+ function rejectLikelySecret(note) {
606
+ if (SECRET_PATTERNS.some((pattern) => pattern.test(note))) {
607
+ throw new HandoffError("handoff notes look like they contain a secret; remove it and try again");
608
+ }
609
+ }
610
+ function statusError(status) {
611
+ return `agent handoff is not ready:\n${status.problems.map((problem) => `- ${problem}`).join("\n")}`;
612
+ }
613
+ function json(data) {
614
+ return `${JSON.stringify(data, null, 2)}\n`;
615
+ }
616
+ function appendFile(path, contents) {
617
+ writeFileSync(path, contents, { encoding: "utf8", flag: "a" });
618
+ }
619
+ function cloneVaultIfNeeded(home, vault, syncUrl) {
620
+ if (existsSync(join(vault, ".git")))
621
+ return false;
622
+ if (existsSync(vault)) {
623
+ if (readdirSync(vault).length === 0) {
624
+ rmSync(vault, { recursive: true, force: true });
625
+ }
626
+ else {
627
+ return false;
628
+ }
629
+ }
630
+ const clone = gitRun(home, ["clone", syncUrl, vault]);
631
+ return clone.status === 0;
632
+ }
633
+ function ensureGitRemote(vault, syncUrl) {
634
+ if (!existsSync(join(vault, ".git"))) {
635
+ gitRun(vault, ["init"]);
636
+ gitRun(vault, ["branch", "-M", "main"]);
637
+ }
638
+ const remotes = gitOutput(vault, ["remote"]);
639
+ if (remotes?.split(/\r?\n/).includes("origin")) {
640
+ gitRun(vault, ["remote", "set-url", "origin", syncUrl]);
641
+ }
642
+ else {
643
+ gitRun(vault, ["remote", "add", "origin", syncUrl]);
644
+ }
645
+ }
646
+ function gitOutput(root, args) {
647
+ const result = gitRun(root, args);
648
+ if (result.status !== 0)
649
+ return null;
650
+ const output = result.output.trim();
651
+ return output || null;
652
+ }
653
+ function gitChecked(root, args) {
654
+ const result = gitRun(root, args);
655
+ if (result.status !== 0) {
656
+ throw new HandoffError(result.output.trim() || `git ${args.join(" ")} failed`);
657
+ }
658
+ return result.output.trim();
659
+ }
660
+ function gitRun(root, args) {
661
+ const result = spawnSync("git", args, {
662
+ cwd: root,
663
+ encoding: "utf8",
664
+ stdio: ["ignore", "pipe", "pipe"],
665
+ });
666
+ return {
667
+ status: result.status ?? 1,
668
+ output: `${result.stdout ?? ""}${result.stderr ?? ""}`,
669
+ };
670
+ }
671
+ function isEmptyRemotePull(output) {
672
+ const lowered = output.toLowerCase();
673
+ return lowered.includes("couldn't find remote ref") || lowered.includes("could not find remote ref");
674
+ }
675
+ function readResource(name) {
676
+ const path = fileURLToPath(new URL(`../resources/${name}`, import.meta.url));
677
+ return readFileSync(path, "utf8");
678
+ }
@@ -0,0 +1 @@
1
+ export * from "./core.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from "./core.js";
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@leantli/agent-handoff",
3
+ "version": "0.4.0",
4
+ "description": "Shared vault handoff memory for Codex and Claude Code.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "agent-handoff contributors",
8
+ "bin": {
9
+ "agent-handoff": "dist/bin.js"
10
+ },
11
+ "types": "dist/index.d.ts",
12
+ "files": [
13
+ "dist",
14
+ "resources",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "prepack": "npm run build",
21
+ "test": "vitest run",
22
+ "typecheck": "tsc --noEmit"
23
+ },
24
+ "keywords": [
25
+ "ai",
26
+ "agents",
27
+ "codex",
28
+ "claude-code",
29
+ "handoff",
30
+ "context"
31
+ ],
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "dependencies": {
36
+ "commander": "^12.1.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^20.11.0",
40
+ "typescript": "^5.4.0",
41
+ "vitest": "^1.6.0"
42
+ }
43
+ }
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: agent-handoff
3
+ description: Use when starting, resuming, pausing, checkpointing, or transferring Codex/Claude Code work across sessions, clones, worktrees, or devices with the agent-handoff CLI.
4
+ ---
5
+
6
+ # Agent Handoff
7
+
8
+ Use `agent-handoff` to restore and preserve coding-agent context.
9
+
10
+ ## Start Of Session
11
+
12
+ When beginning work in a repository:
13
+
14
+ 1. Run `agent-handoff status`.
15
+ 2. If status says the repo is not ready, run `agent-handoff setup` if the vault is missing, then `agent-handoff init`.
16
+ 3. If vault sync is configured, run `agent-handoff sync`.
17
+ 4. Run `agent-handoff start`.
18
+ 5. Read the returned packet before changing files.
19
+
20
+ ## During Work
21
+
22
+ Use `learn` only for stable facts that should survive future sessions, clones,
23
+ worktrees, and devices.
24
+
25
+ For a durable user preference or recurring correction:
26
+
27
+ ```bash
28
+ agent-handoff learn --kind preference --note "<stable preference>"
29
+ ```
30
+
31
+ For a durable lesson about project or agent behavior:
32
+
33
+ ```bash
34
+ agent-handoff learn --kind lesson --note "<stable lesson>"
35
+ ```
36
+
37
+ For project-specific decisions:
38
+
39
+ ```bash
40
+ agent-handoff learn --scope project --kind decision --note "<project decision>"
41
+ ```
42
+
43
+ For branch-specific current context:
44
+
45
+ ```bash
46
+ agent-handoff learn --scope branch --kind context --note "<branch context>"
47
+ ```
48
+
49
+ ## Before Pausing
50
+
51
+ Before ending a useful session, switching devices, or handing work to another
52
+ agent, write a concise checkpoint:
53
+
54
+ ```bash
55
+ agent-handoff checkpoint --note "<current goal, completed work, open questions, next step>"
56
+ ```
57
+
58
+ If vault sync is configured, run:
59
+
60
+ ```bash
61
+ agent-handoff sync
62
+ ```
63
+
64
+ If sync fails, keep the local checkpoint and report the error.
65
+
66
+ ## Rules
67
+
68
+ - Do not store secrets, tokens, credentials, or private customer data in handoff notes.
69
+ - Keep checkpoints factual and concise.
70
+ - Do not use `learn` for temporary task state; use `checkpoint` instead.
71
+ - Prefer project or branch scope for project-specific facts instead of global memory.
72
+ - If `agent-handoff` is not installed, tell the user the CLI is missing and continue without pretending context was saved.