@leantli/agent-handoff 0.4.0 → 0.5.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.
package/README.md CHANGED
@@ -1,132 +1,122 @@
1
1
  # agent-handoff
2
2
 
3
- `agent-handoff` gives Codex and Claude Code a shared memory handoff layer across
4
- new sessions, fresh clones, git worktrees, and devices.
3
+ `agent-handoff` gives coding agents a small shared memory layer for new
4
+ sessions, fresh clones, git worktrees, and devices.
5
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.
6
+ Long agent sessions accumulate useful context: project background, current task
7
+ state, decisions, preferences, and repeated corrections. Without a handoff
8
+ layer, the next session starts cold.
9
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.
10
+ `agent-handoff` stores that context under `~/.agent-handoff` by default. It does
11
+ not modify `AGENTS.md`, `CLAUDE.md`, or other project instruction files.
12
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:
13
+ ## Quick Start
23
14
 
24
15
  ```bash
25
16
  npm install -g @leantli/agent-handoff
17
+ agent-handoff enable
26
18
  ```
27
19
 
28
- From a checkout:
20
+ GitHub direct install is also supported:
29
21
 
30
22
  ```bash
31
- npm install
32
- npm run build
33
- npm link
23
+ npm install -g github:leantli/agent-handoff
24
+ agent-handoff enable
34
25
  ```
35
26
 
36
- ## Three-Minute Setup
27
+ `enable` does two things:
37
28
 
38
- Create your local vault once:
29
+ - creates local memory under `~/.agent-handoff`
30
+ - installs the packaged skill under `~/.agents/skills/agent-handoff`
39
31
 
40
- ```bash
41
- agent-handoff setup
42
- agent-handoff install-skill
43
- ```
32
+ Agents that load user-level skills can then discover when to run
33
+ `agent-handoff start`, `checkpoint`, `learn`, and `sync`. Existing agent
34
+ sessions may need to be restarted before they see the new skill.
44
35
 
45
- Optional: sync the vault through a private git repo so another device can share
46
- the same handoff memory:
36
+ The tool never edits `AGENTS.md`, `CLAUDE.md`, or project instruction files.
47
37
 
48
- ```bash
49
- agent-handoff setup --sync git@github.com:you/agent-handoff-vault.git
50
- ```
38
+ ## Daily Workflow
51
39
 
52
- Bootstrap each coding project once:
40
+ Run project-aware commands from inside the real project repository, not from a
41
+ parent workspace that contains many repositories.
53
42
 
54
- ```bash
55
- agent-handoff init
56
- ```
43
+ At the start of a coding session in a repo:
57
44
 
58
- This writes:
59
-
60
- ```text
61
- .agent-handoff.yml
62
- AGENTS.md
63
- CLAUDE.md
45
+ ```bash
46
+ agent-handoff status
47
+ agent-handoff start
64
48
  ```
65
49
 
66
- It does not write private memory into the project repository.
67
-
68
- ## Daily Workflow
50
+ If `status` says sync is configured and you are switching devices or clones, run
51
+ `agent-handoff sync` before `start`.
69
52
 
70
- At the start of a new Codex or Claude Code session:
53
+ Before switching sessions, tasks, clones, or devices:
71
54
 
72
55
  ```bash
73
- agent-handoff sync # only if vault sync is configured
74
- agent-handoff start
56
+ agent-handoff checkpoint --note "Current goal, completed work, open questions, next step."
75
57
  ```
76
58
 
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:
59
+ When the user gives a stable preference or recurring correction:
80
60
 
81
61
  ```bash
82
- agent-handoff checkpoint --note "Implemented vault storage; next step is README polish."
83
- agent-handoff sync # only if vault sync is configured
62
+ agent-handoff learn --kind preference --note "Prefer small focused diffs."
84
63
  ```
85
64
 
86
- When the user corrects a stable preference or recurring rule:
65
+ For project-specific decisions or branch-specific context:
87
66
 
88
67
  ```bash
89
- agent-handoff learn --kind preference --note "Prefer TDD for behavior changes."
68
+ agent-handoff learn --scope project --kind decision --note "Use TypeScript for the CLI."
69
+ agent-handoff learn --scope branch --kind context --note "This branch is testing v0.5."
90
70
  ```
91
71
 
92
- For project-specific decisions or branch-specific context:
72
+ ## Cross-Device Sync
73
+
74
+ Local cross-session memory works immediately after `agent-handoff enable`. No
75
+ git repository is needed for the vault.
76
+
77
+ Cross-device sync is optional. To share memory across devices, create a private
78
+ git repository for the vault, then run:
93
79
 
94
80
  ```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."
81
+ agent-handoff sync init git@github.com:you/agent-handoff-vault.git
82
+ agent-handoff sync
97
83
  ```
98
84
 
99
- When configured, `sync` commits pending vault changes and pushes them to the
100
- private vault repository.
85
+ Run `agent-handoff sync init <same-git-url>` once on each device that should
86
+ share the vault. After that, run `agent-handoff sync` before starting on another
87
+ device and after writing useful checkpoints.
101
88
 
102
- ## Why This Solves Cross-Clone Context
89
+ ## How Projects Are Identified
103
90
 
104
- `agent-handoff` identifies a project from `.agent-handoff.yml` or the git
105
- `origin` remote. These all map to the same vault project:
91
+ By default, `agent-handoff` identifies the current project from the git `origin`
92
+ remote. Different clones, sibling checkouts, or worktrees of the same repository
93
+ map to the same project memory:
106
94
 
107
95
  ```text
108
- ~/code/repo
109
- ~/tmp/repo
110
- ~/worktrees/repo-feature
111
- another device's ~/projects/repo
96
+ https://github.com/p1cn/loop.git
97
+ git@github.com:p1cn/loop.git
98
+
99
+ github.com__p1cn__loop
112
100
  ```
113
101
 
114
- For example, both remotes below normalize to the same project id:
102
+ If `.agent-handoff.yml` exists, its `project_id` is used as an override. The
103
+ tool does not require this file for normal git repositories.
115
104
 
116
- ```text
117
- https://github.com/leantli/agent-handoff.git
118
- git@github.com:leantli/agent-handoff.git
105
+ In a workspace like this:
119
106
 
120
- github.com__leantli__agent-handoff
107
+ ```text
108
+ ~/workspace/
109
+ app-one/
110
+ app-two/
111
+ app-three/
121
112
  ```
122
113
 
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.
114
+ run `agent-handoff start`, `checkpoint`, and project or branch `learn` commands
115
+ from `~/workspace/app-one`, `~/workspace/app-two`, or whichever repository is
116
+ actually being edited. Global preferences can be written from anywhere.
125
117
 
126
118
  ## What Gets Stored
127
119
 
128
- The vault is private user state:
129
-
130
120
  ```text
131
121
  ~/.agent-handoff/
132
122
  config.json
@@ -146,64 +136,30 @@ The vault is private user state:
146
136
  20260508T103000Z-laptop-codex-main.md
147
137
  ```
148
138
 
149
- The project repository gets only bootstrap files:
139
+ The layers are:
150
140
 
151
- ```text
152
- .agent-handoff.yml
153
- AGENTS.md
154
- CLAUDE.md
155
- ```
141
+ - `global`: preferences and lessons that apply across projects.
142
+ - `project`: durable background, decisions, and preferences for one repository.
143
+ - `branch`: task or branch-specific context.
144
+ - `checkpoints`: recent session handoff notes.
156
145
 
157
146
  ## Commands
158
147
 
159
148
  ```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
149
+ agent-handoff enable # create local memory and install the user skill
150
+ agent-handoff start # print context for the current project and branch
164
151
  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
152
+ agent-handoff learn # store durable global/project/branch memory
153
+ agent-handoff sync init # enable optional cross-device sync
154
+ agent-handoff sync # pull/rebase and push the vault
155
+ agent-handoff status # quick readiness and sync-state check
185
156
  ```
186
157
 
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
158
  ## Development
197
159
 
198
- Run tests:
199
-
200
160
  ```bash
161
+ npm install
201
162
  npm test
202
- ```
203
-
204
- Run type checking and build:
205
-
206
- ```bash
207
163
  npm run typecheck
208
164
  npm run build
209
165
  ```
package/dist/bin.js CHANGED
File without changes
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
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";
2
+ import { Command, CommanderError, Option } from "commander";
3
+ import { HandoffError, buildStartPacket, enableHandoff, enableSync, getStatus, learn, syncVault, writeCheckpoint, } from "./core.js";
4
4
  export function main(argv = process.argv.slice(2), opts = {}) {
5
5
  const stdout = opts.stdout ?? process.stdout;
6
6
  const stderr = opts.stderr ?? process.stderr;
@@ -24,8 +24,8 @@ function buildProgram(stdout, stderr, stdin, cwd) {
24
24
  const program = new Command();
25
25
  program
26
26
  .name("agent-handoff")
27
- .description("Shared vault handoff memory for Codex and Claude Code.")
28
- .version("agent-handoff 0.4.0")
27
+ .description("Shared vault handoff memory for coding agents.")
28
+ .version("agent-handoff 0.5.1")
29
29
  .exitOverride()
30
30
  .configureOutput({
31
31
  writeOut: (str) => stdout.write(str),
@@ -33,42 +33,19 @@ function buildProgram(stdout, stderr, stdin, cwd) {
33
33
  })
34
34
  .option("--home <path>", "Agent handoff home directory. Defaults to ~/.agent-handoff.");
35
35
  program
36
- .command("setup")
37
- .description("Create or configure the user vault.")
36
+ .command("enable")
37
+ .description("Enable local handoff memory and install the user skill.")
38
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
39
  .option("--skills-home <path>", "Skills home directory. Defaults to ~/.agents/skills.")
49
40
  .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,
41
+ const result = enableHandoff({
65
42
  home: globalHome(program),
66
- projectId: options.projectId,
67
- branch: options.branch,
68
- clients: options.client,
43
+ vault: options.vault,
44
+ skillsHome: options.skillsHome,
69
45
  });
70
- stdout.write(`Initialized agent handoff for ${result.projectId}.\n`);
71
- stdout.write(`Vault project: ${result.vaultProject}\n`);
46
+ stdout.write(`Agent handoff enabled: ${result.setup.home}\n`);
47
+ stdout.write(`Vault: ${result.setup.vault}\n`);
48
+ stdout.write(`Skill: ${result.skill.path}\n`);
72
49
  });
73
50
  program
74
51
  .command("start")
@@ -83,7 +60,7 @@ function buildProgram(stdout, stderr, stdin, cwd) {
83
60
  .option("--note <text>", "Note text. If omitted, stdin is used.")
84
61
  .option("--file <path>", "Read note text from a file.")
85
62
  .option("--device <name>", "Device name for the checkpoint.")
86
- .option("--agent <name>", "Agent/client name, such as codex or claude.")
63
+ .option("--agent <name>", "Agent/client label for checkpoint metadata.")
87
64
  .option("--branch <branch>", "Override detected branch.")
88
65
  .action((options) => {
89
66
  const result = writeCheckpoint({
@@ -114,10 +91,17 @@ function buildProgram(stdout, stderr, stdin, cwd) {
114
91
  });
115
92
  stdout.write(`Learned ${result.kind}: ${result.path}\n`);
116
93
  });
117
- program
118
- .command("sync")
119
- .description("Pull and push the vault git repository.")
120
- .action(() => {
94
+ const sync = program.command("sync").description("Sync the handoff vault.");
95
+ sync
96
+ .command("init <git-url>")
97
+ .description("Enable cross-device sync with a private git repository.")
98
+ .option("--vault <path>", "Vault directory. Defaults to HOME/vault.")
99
+ .action((gitUrl, options) => {
100
+ const result = enableSync({ home: globalHome(program), vault: options.vault, syncUrl: gitUrl });
101
+ stdout.write(`Cross-device sync enabled: ${gitUrl}\n`);
102
+ stdout.write(`Vault: ${result.vault}\n`);
103
+ });
104
+ sync.action(() => {
121
105
  for (const output of syncVault({ home: globalHome(program) })) {
122
106
  if (output)
123
107
  stdout.write(`${output}\n`);
@@ -130,36 +114,23 @@ function buildProgram(stdout, stderr, stdin, cwd) {
130
114
  const status = getStatus({ root: cwd, home: globalHome(program) });
131
115
  if (status.initialized) {
132
116
  stdout.write(`Agent handoff is ready for ${status.projectId}.\n`);
117
+ if (status.syncConfigured) {
118
+ stdout.write(`Sync: configured (${status.syncUrl})\n`);
119
+ }
120
+ else {
121
+ stdout.write("Sync: not configured\n");
122
+ }
133
123
  }
134
124
  else {
135
125
  printProblems(status.problems, stdout);
136
126
  throw new CommanderError(1, "agent-handoff.status", "status failed");
137
127
  }
138
128
  });
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
129
  return program;
153
130
  }
154
131
  function globalHome(program) {
155
132
  return program.opts().home;
156
133
  }
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
134
  function readNote(options, stdin) {
164
135
  if (options.note)
165
136
  return options.note;
package/dist/core.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  export declare const DEFAULT_HOME: string;
2
2
  export declare const CONFIG_FILE = "config.json";
3
3
  export declare const BOOTSTRAP_FILE = ".agent-handoff.yml";
4
- type Client = "codex" | "claude";
5
4
  type LearnScope = "global" | "project" | "branch";
6
5
  type LearnKind = "preference" | "lesson" | "decision" | "context";
7
6
  export declare class HandoffError extends Error {
@@ -13,13 +12,9 @@ export interface SetupResult {
13
12
  created: number;
14
13
  updated: number;
15
14
  }
16
- export interface InitResult {
17
- created: number;
18
- updated: number;
19
- root: string;
20
- projectId: string;
21
- vaultProject: string;
22
- clients: Client[];
15
+ export interface EnableResult {
16
+ setup: SetupResult;
17
+ skill: InstallSkillResult;
23
18
  }
24
19
  export interface CheckpointResult {
25
20
  path: string;
@@ -41,12 +36,8 @@ export interface Status {
41
36
  problems: string[];
42
37
  root: string;
43
38
  projectId: string | null;
44
- }
45
- export interface DoctorReport {
46
- ok: boolean;
47
- problems: string[];
48
- root: string;
49
- projectId: string | null;
39
+ syncConfigured: boolean;
40
+ syncUrl?: string;
50
41
  }
51
42
  export declare function setupHome(opts?: {
52
43
  home?: string;
@@ -57,13 +48,16 @@ export declare function normalizeProjectId(value: string): string;
57
48
  export declare function installSkill(opts?: {
58
49
  skillsHome?: string;
59
50
  }): InstallSkillResult;
60
- export declare function initRepo(opts?: {
61
- root?: string;
51
+ export declare function enableHandoff(opts?: {
62
52
  home?: string;
63
- projectId?: string;
64
- branch?: string;
65
- clients?: string[];
66
- }): InitResult;
53
+ vault?: string;
54
+ skillsHome?: string;
55
+ }): EnableResult;
56
+ export declare function enableSync(opts: {
57
+ home?: string;
58
+ vault?: string;
59
+ syncUrl: string;
60
+ }): SetupResult;
67
61
  export declare function deriveProjectId(root?: string, projectId?: string): string;
68
62
  export declare function currentBranch(root?: string): string;
69
63
  export declare function buildStartPacket(opts?: {
@@ -96,8 +90,4 @@ export declare function getStatus(opts?: {
96
90
  root?: string;
97
91
  home?: string;
98
92
  }): Status;
99
- export declare function doctor(opts?: {
100
- root?: string;
101
- home?: string;
102
- }): DoctorReport;
103
93
  export {};
package/dist/core.js CHANGED
@@ -6,13 +6,12 @@ import { fileURLToPath } from "node:url";
6
6
  export const DEFAULT_HOME = join(homedir(), ".agent-handoff");
7
7
  export const CONFIG_FILE = "config.json";
8
8
  export const BOOTSTRAP_FILE = ".agent-handoff.yml";
9
- const MANAGED_BEGIN = "<!-- BEGIN AGENT-HANDOFF -->";
10
- const MANAGED_END = "<!-- END AGENT-HANDOFF -->";
11
9
  const SECRET_PATTERNS = [
12
10
  /(api[_-]?key|token|secret|password)\s*[:=]/i,
13
11
  /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
14
12
  /\bsk-[A-Za-z0-9_-]{8,}\b/,
15
13
  ];
14
+ const GIT_TIMEOUT_MS = 30_000;
16
15
  export class HandoffError extends Error {
17
16
  constructor(message) {
18
17
  super(message);
@@ -28,6 +27,13 @@ export function setupHome(opts = {}) {
28
27
  mkdirSync(homePath, { recursive: true });
29
28
  created += 1;
30
29
  }
30
+ if (opts.syncUrl) {
31
+ validateSyncRemote(homePath, opts.syncUrl);
32
+ if (hasUnsyncedLocalVault(vaultPath) && remoteHasRefs(homePath, opts.syncUrl)) {
33
+ throw new HandoffError("local vault has unsynced memory and the sync remote already has data; move or back up the local vault before joining this remote");
34
+ }
35
+ ensureExistingGitVaultCompatible(vaultPath, opts.syncUrl);
36
+ }
31
37
  if (opts.syncUrl && cloneVaultIfNeeded(homePath, vaultPath, opts.syncUrl)) {
32
38
  created += 1;
33
39
  }
@@ -49,7 +55,9 @@ export function setupHome(opts = {}) {
49
55
  if (opts.syncUrl)
50
56
  desired.sync_url = opts.syncUrl;
51
57
  if (existsSync(configPath)) {
52
- const existing = JSON.parse(readFileSync(configPath, "utf8"));
58
+ const existing = readConfig(opts.home);
59
+ if (!existing)
60
+ throw new HandoffError(`${CONFIG_FILE} is missing`);
53
61
  let changed = false;
54
62
  if (existing.version !== 2) {
55
63
  existing.version = 2;
@@ -114,49 +122,13 @@ export function installSkill(opts = {}) {
114
122
  }
115
123
  return { path, updated };
116
124
  }
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
- };
125
+ export function enableHandoff(opts = {}) {
126
+ const setup = setupHome({ home: opts.home, vault: opts.vault });
127
+ const skill = installSkill({ skillsHome: opts.skillsHome });
128
+ return { setup, skill };
129
+ }
130
+ export function enableSync(opts) {
131
+ return setupHome({ home: opts.home, vault: opts.vault, syncUrl: opts.syncUrl });
160
132
  }
161
133
  export function deriveProjectId(root = ".", projectId) {
162
134
  const rootPath = resolve(root);
@@ -178,14 +150,11 @@ export function currentBranch(root = ".") {
178
150
  }
179
151
  export function buildStartPacket(opts = {}) {
180
152
  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
153
  const setup = loadSetup(opts.home);
186
- const pid = status.projectId ?? deriveProjectId(rootPath);
154
+ const pid = deriveProjectId(rootPath);
187
155
  const branchName = opts.branch ?? currentBranch(rootPath);
188
156
  const projectPath = vaultProjectPath(setup.vault, pid);
157
+ ensureProjectFiles(projectPath, branchName);
189
158
  const branchFile = join(projectPath, "branches", `${safeName(branchName)}.md`);
190
159
  const sections = [
191
160
  ["Global Preferences", join(setup.vault, "global", "preferences.md")],
@@ -201,7 +170,7 @@ export function buildStartPacket(opts = {}) {
201
170
  `Project: \`${pid}\``,
202
171
  `Branch: \`${branchName}\``,
203
172
  "",
204
- "Read this packet before making changes. Use it to recover context from previous Codex and Claude Code sessions.",
173
+ "Read this packet before making changes. Use it to recover context from previous coding-agent sessions.",
205
174
  ];
206
175
  for (const [title, path] of sections) {
207
176
  lines.push(...renderSection(title, path));
@@ -218,25 +187,22 @@ export function buildStartPacket(opts = {}) {
218
187
  }
219
188
  export function writeCheckpoint(opts) {
220
189
  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
190
  const cleanedNote = cleanNote(opts.note);
226
191
  if (!cleanedNote)
227
192
  throw new HandoffError("checkpoint note cannot be empty");
228
193
  rejectLikelySecret(cleanedNote);
229
194
  const setup = loadSetup(opts.home);
230
- const pid = status.projectId ?? deriveProjectId(rootPath);
195
+ const pid = deriveProjectId(rootPath);
231
196
  const branchName = opts.branch ?? currentBranch(rootPath);
232
197
  const createdAt = timestamp(opts.now);
233
198
  const projectPath = vaultProjectPath(setup.vault, pid);
199
+ ensureProjectFiles(projectPath, branchName);
234
200
  const checkpoints = join(projectPath, "checkpoints");
235
201
  mkdirSync(checkpoints, { recursive: true });
236
202
  const deviceLabel = opts.device ?? hostname() ?? "device";
237
203
  const agentLabel = opts.agent ?? "agent";
238
204
  const filename = `${compactTimestamp(createdAt)}-${safeName(deviceLabel)}-${safeName(agentLabel)}-${safeName(branchName)}.md`;
239
- const path = join(checkpoints, filename);
205
+ const path = uniquePath(checkpoints, filename);
240
206
  const contents = [
241
207
  "# Checkpoint",
242
208
  "",
@@ -267,7 +233,7 @@ export function learn(note, opts = {}) {
267
233
  if (!["preference", "lesson", "decision", "context"].includes(kind)) {
268
234
  throw new HandoffError("learn kind must be 'preference', 'lesson', 'decision', or 'context'");
269
235
  }
270
- const setup = setupHome({ home: opts.home });
236
+ const setup = loadSetup(opts.home);
271
237
  const path = learnTargetPath(setup, resolve(opts.root ?? "."), scope, kind, opts.branch);
272
238
  const createdAt = timestamp(opts.now);
273
239
  appendFile(path, `\n- ${createdAt}: ${clean}\n`);
@@ -275,9 +241,13 @@ export function learn(note, opts = {}) {
275
241
  }
276
242
  export function syncVault(opts = {}) {
277
243
  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");
244
+ const config = readConfig(opts.home);
245
+ if (!config?.sync_url) {
246
+ throw new HandoffError("sync is not configured; run agent-handoff sync init <git-url> first");
280
247
  }
248
+ const syncProblem = syncConfigProblem(setup.vault, config.sync_url);
249
+ if (syncProblem)
250
+ throw new HandoffError(syncProblem);
281
251
  const outputs = [];
282
252
  gitChecked(setup.vault, ["add", "-A"]);
283
253
  const staged = gitRun(setup.vault, ["diff", "--cached", "--quiet"]).status !== 0;
@@ -310,46 +280,49 @@ export function syncVault(opts = {}) {
310
280
  export function getStatus(opts = {}) {
311
281
  const rootPath = resolve(opts.root ?? ".");
312
282
  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`);
283
+ const projectId = deriveProjectId(rootPath);
284
+ let syncUrl;
285
+ let config = null;
286
+ try {
287
+ config = readConfig(opts.home);
288
+ }
289
+ catch (error) {
290
+ if (error instanceof HandoffError) {
291
+ problems.push(error.message);
292
+ }
293
+ else {
294
+ throw error;
329
295
  }
330
296
  }
331
- const config = readConfig(opts.home);
332
297
  if (!config) {
333
- problems.push("vault config is missing; run agent-handoff setup");
298
+ if (problems.length === 0) {
299
+ problems.push("agent-handoff is not enabled; run agent-handoff enable");
300
+ }
334
301
  }
335
302
  else if (!existsSync(config.vault)) {
336
303
  problems.push(`vault directory is missing: ${config.vault}`);
337
304
  }
338
- else if (projectId) {
339
- const projectPath = vaultProjectPath(config.vault, projectId);
340
- if (!existsSync(projectPath)) {
341
- problems.push(`vault project is missing: ${projectId}`);
305
+ else {
306
+ if (config.sync_url) {
307
+ const syncProblem = syncConfigProblem(config.vault, config.sync_url);
308
+ if (syncProblem) {
309
+ problems.push(syncProblem);
310
+ }
311
+ else {
312
+ syncUrl = config.sync_url;
313
+ }
314
+ }
315
+ else {
316
+ syncUrl = undefined;
342
317
  }
343
318
  }
344
- return { initialized: problems.length === 0, problems, root: rootPath, projectId };
345
- }
346
- export function doctor(opts = {}) {
347
- const status = getStatus(opts);
348
319
  return {
349
- ok: status.initialized,
350
- problems: status.problems,
351
- root: status.root,
352
- projectId: status.projectId,
320
+ initialized: problems.length === 0,
321
+ problems,
322
+ root: rootPath,
323
+ projectId,
324
+ syncConfigured: Boolean(syncUrl),
325
+ syncUrl,
353
326
  };
354
327
  }
355
328
  function globalSeedFiles() {
@@ -398,8 +371,9 @@ function learnTargetPath(setup, root, scope, kind, branch) {
398
371
  if (!status.initialized) {
399
372
  throw new HandoffError(statusError(status));
400
373
  }
401
- const pid = status.projectId ?? deriveProjectId(root);
374
+ const pid = deriveProjectId(root);
402
375
  const projectPath = vaultProjectPath(setup.vault, pid);
376
+ ensureProjectFiles(projectPath, branch ?? currentBranch(root));
403
377
  if (scope === "project") {
404
378
  if (kind === "preference")
405
379
  return join(projectPath, "preferences.md");
@@ -414,31 +388,6 @@ function learnTargetPath(setup, root, scope, kind, branch) {
414
388
  }
415
389
  return branchPath;
416
390
  }
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
391
  function resolveHome(home) {
443
392
  return resolve(home ? expandHome(home) : DEFAULT_HOME);
444
393
  }
@@ -453,12 +402,58 @@ function readConfig(home) {
453
402
  const configPath = join(resolveHome(home), CONFIG_FILE);
454
403
  if (!existsSync(configPath))
455
404
  return null;
456
- return JSON.parse(readFileSync(configPath, "utf8"));
405
+ let raw;
406
+ try {
407
+ raw = JSON.parse(readFileSync(configPath, "utf8"));
408
+ }
409
+ catch (error) {
410
+ const detail = error instanceof Error ? error.message : String(error);
411
+ throw new HandoffError(`${CONFIG_FILE} is invalid: ${detail}`);
412
+ }
413
+ return validateConfig(raw);
414
+ }
415
+ function validateConfig(raw) {
416
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
417
+ throw new HandoffError(`${CONFIG_FILE} is invalid: expected an object`);
418
+ }
419
+ const data = raw;
420
+ if (typeof data.vault !== "string" || data.vault.trim().length === 0) {
421
+ throw new HandoffError(`${CONFIG_FILE} is invalid: vault must be a non-empty string`);
422
+ }
423
+ if (data.version !== undefined && typeof data.version !== "number") {
424
+ throw new HandoffError(`${CONFIG_FILE} is invalid: version must be a number`);
425
+ }
426
+ if (data.sync_url !== undefined && (typeof data.sync_url !== "string" || data.sync_url.trim().length === 0)) {
427
+ throw new HandoffError(`${CONFIG_FILE} is invalid: sync_url must be a non-empty string`);
428
+ }
429
+ const config = {
430
+ version: typeof data.version === "number" ? data.version : 0,
431
+ vault: data.vault,
432
+ };
433
+ if (typeof data.sync_url === "string") {
434
+ config.sync_url = data.sync_url;
435
+ }
436
+ return config;
437
+ }
438
+ function syncConfigProblem(vault, syncUrl) {
439
+ if (!existsSync(join(vault, ".git"))) {
440
+ return `sync is configured for ${syncUrl} but vault is not a git repository; run agent-handoff sync init ${syncUrl}`;
441
+ }
442
+ const origin = gitOutput(vault, ["remote", "get-url", "origin"]);
443
+ if (!origin) {
444
+ return `sync is configured for ${syncUrl} but vault git remote origin is missing; run agent-handoff sync init ${syncUrl}`;
445
+ }
446
+ if (origin !== syncUrl) {
447
+ return `sync is configured for ${syncUrl} but vault origin is ${origin}; run agent-handoff sync init ${syncUrl}`;
448
+ }
449
+ return null;
457
450
  }
458
451
  function loadSetup(home) {
459
452
  const config = readConfig(home);
460
453
  if (!config)
461
- return setupHome({ home });
454
+ throw new HandoffError("agent-handoff is not enabled; run agent-handoff enable");
455
+ if (!existsSync(config.vault))
456
+ throw new HandoffError(`vault directory is missing: ${config.vault}`);
462
457
  return { home: resolveHome(home), vault: resolve(config.vault), created: 0, updated: 0 };
463
458
  }
464
459
  function vaultProjectPath(vault, projectId) {
@@ -491,66 +486,6 @@ function readBootstrap(path) {
491
486
  }
492
487
  return data;
493
488
  }
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
489
  function renderSection(title, path, headingLevel = 2) {
555
490
  const heading = "#".repeat(headingLevel);
556
491
  if (!existsSync(path)) {
@@ -616,11 +551,72 @@ function json(data) {
616
551
  function appendFile(path, contents) {
617
552
  writeFileSync(path, contents, { encoding: "utf8", flag: "a" });
618
553
  }
554
+ function uniquePath(directory, filename) {
555
+ const dot = filename.lastIndexOf(".");
556
+ const stem = dot === -1 ? filename : filename.slice(0, dot);
557
+ const extension = dot === -1 ? "" : filename.slice(dot);
558
+ let path = join(directory, filename);
559
+ let index = 2;
560
+ while (existsSync(path)) {
561
+ path = join(directory, `${stem}-${index}${extension}`);
562
+ index += 1;
563
+ }
564
+ return path;
565
+ }
566
+ function validateSyncRemote(home, syncUrl) {
567
+ const result = gitRun(home, ["ls-remote", syncUrl]);
568
+ if (result.status !== 0) {
569
+ throw new HandoffError(result.output.trim() || `cannot access sync remote: ${syncUrl}`);
570
+ }
571
+ }
572
+ function hasUnsyncedLocalVault(vault) {
573
+ return existsSync(vault) && !existsSync(join(vault, ".git")) && !isSeedOnlyVault(vault);
574
+ }
575
+ function remoteHasRefs(home, syncUrl) {
576
+ const result = gitRun(home, ["ls-remote", "--heads", syncUrl]);
577
+ if (result.status !== 0) {
578
+ throw new HandoffError(result.output.trim() || `cannot inspect sync remote: ${syncUrl}`);
579
+ }
580
+ return result.output.trim().length > 0;
581
+ }
582
+ function ensureExistingGitVaultCompatible(vault, syncUrl) {
583
+ if (!existsSync(join(vault, ".git")))
584
+ return;
585
+ if (!remoteHasRefs(vault, syncUrl))
586
+ return;
587
+ const localHead = gitOutput(vault, ["rev-parse", "HEAD"]);
588
+ if (!localHead)
589
+ return;
590
+ const namespace = "refs/remotes/agent-handoff-check";
591
+ const fetch = gitRun(vault, ["fetch", "--no-tags", syncUrl, `+refs/heads/*:${namespace}/*`]);
592
+ if (fetch.status !== 0) {
593
+ throw new HandoffError(fetch.output.trim() || `cannot fetch sync remote: ${syncUrl}`);
594
+ }
595
+ const refs = listRefs(vault, namespace);
596
+ try {
597
+ if (refs.length > 0 && !refs.some((ref) => sharesHistory(vault, localHead, ref))) {
598
+ throw new HandoffError("existing git vault and sync remote do not share history; use a fresh vault or keep the current sync remote");
599
+ }
600
+ }
601
+ finally {
602
+ for (const ref of refs) {
603
+ gitRun(vault, ["update-ref", "-d", ref]);
604
+ }
605
+ }
606
+ }
607
+ function listRefs(vault, namespace) {
608
+ const refs = gitOutput(vault, ["for-each-ref", "--format=%(refname)", namespace]);
609
+ return refs?.split(/\r?\n/).filter(Boolean) ?? [];
610
+ }
611
+ function sharesHistory(vault, localHead, remoteRef) {
612
+ return (gitRun(vault, ["merge-base", "--is-ancestor", localHead, remoteRef]).status === 0 ||
613
+ gitRun(vault, ["merge-base", "--is-ancestor", remoteRef, localHead]).status === 0);
614
+ }
619
615
  function cloneVaultIfNeeded(home, vault, syncUrl) {
620
616
  if (existsSync(join(vault, ".git")))
621
617
  return false;
622
618
  if (existsSync(vault)) {
623
- if (readdirSync(vault).length === 0) {
619
+ if (readdirSync(vault).length === 0 || isSeedOnlyVault(vault)) {
624
620
  rmSync(vault, { recursive: true, force: true });
625
621
  }
626
622
  else {
@@ -628,19 +624,72 @@ function cloneVaultIfNeeded(home, vault, syncUrl) {
628
624
  }
629
625
  }
630
626
  const clone = gitRun(home, ["clone", syncUrl, vault]);
627
+ if (clone.status !== 0) {
628
+ throw new HandoffError(clone.output.trim() || `git clone ${syncUrl} failed`);
629
+ }
631
630
  return clone.status === 0;
632
631
  }
632
+ function isSeedOnlyVault(vault) {
633
+ const entries = readdirSync(vault).sort();
634
+ if (entries.some((entry) => !["global", "projects"].includes(entry)))
635
+ return false;
636
+ const projects = join(vault, "projects");
637
+ if (existsSync(projects) && !isSeedOnlyProjects(projects))
638
+ return false;
639
+ const global = join(vault, "global");
640
+ if (!existsSync(global))
641
+ return entries.length === 0 || entries.every((entry) => entry === "projects");
642
+ const seeds = globalSeedFiles();
643
+ for (const entry of readdirSync(global)) {
644
+ if (!(entry in seeds))
645
+ return false;
646
+ if (readFileSync(join(global, entry), "utf8") !== seeds[entry])
647
+ return false;
648
+ }
649
+ return true;
650
+ }
651
+ function isSeedOnlyProjects(projects) {
652
+ for (const name of readdirSync(projects)) {
653
+ if (!isSeedOnlyProject(join(projects, name)))
654
+ return false;
655
+ }
656
+ return true;
657
+ }
658
+ function isSeedOnlyProject(projectPath) {
659
+ const allowed = new Set(["project.md", "decisions.md", "preferences.md", "branches", "checkpoints"]);
660
+ for (const entry of readdirSync(projectPath)) {
661
+ if (!allowed.has(entry))
662
+ return false;
663
+ }
664
+ const seeds = projectSeedFiles();
665
+ for (const [filename, contents] of Object.entries(seeds)) {
666
+ const path = join(projectPath, filename);
667
+ if (existsSync(path) && readFileSync(path, "utf8") !== contents)
668
+ return false;
669
+ }
670
+ const checkpoints = join(projectPath, "checkpoints");
671
+ if (existsSync(checkpoints) && readdirSync(checkpoints).length > 0)
672
+ return false;
673
+ const branches = join(projectPath, "branches");
674
+ if (!existsSync(branches))
675
+ return true;
676
+ return readdirSync(branches).every((name) => {
677
+ if (!name.endsWith(".md"))
678
+ return false;
679
+ return /^# Branch Context: .+\n\n$/.test(readFileSync(join(branches, name), "utf8"));
680
+ });
681
+ }
633
682
  function ensureGitRemote(vault, syncUrl) {
634
683
  if (!existsSync(join(vault, ".git"))) {
635
- gitRun(vault, ["init"]);
636
- gitRun(vault, ["branch", "-M", "main"]);
684
+ gitChecked(vault, ["init"]);
685
+ gitChecked(vault, ["branch", "-M", "main"]);
637
686
  }
638
687
  const remotes = gitOutput(vault, ["remote"]);
639
688
  if (remotes?.split(/\r?\n/).includes("origin")) {
640
- gitRun(vault, ["remote", "set-url", "origin", syncUrl]);
689
+ gitChecked(vault, ["remote", "set-url", "origin", syncUrl]);
641
690
  }
642
691
  else {
643
- gitRun(vault, ["remote", "add", "origin", syncUrl]);
692
+ gitChecked(vault, ["remote", "add", "origin", syncUrl]);
644
693
  }
645
694
  }
646
695
  function gitOutput(root, args) {
@@ -661,11 +710,14 @@ function gitRun(root, args) {
661
710
  const result = spawnSync("git", args, {
662
711
  cwd: root,
663
712
  encoding: "utf8",
713
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
664
714
  stdio: ["ignore", "pipe", "pipe"],
715
+ timeout: GIT_TIMEOUT_MS,
665
716
  });
717
+ const error = result.error ? `\n${result.error.message}` : "";
666
718
  return {
667
719
  status: result.status ?? 1,
668
- output: `${result.stdout ?? ""}${result.stderr ?? ""}`,
720
+ output: `${result.stdout ?? ""}${result.stderr ?? ""}${error}`,
669
721
  };
670
722
  }
671
723
  function isEmptyRemotePull(output) {
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
- export * from "./core.js";
1
+ export { HandoffError, buildStartPacket, enableHandoff, enableSync, getStatus, learn, normalizeProjectId, syncVault, writeCheckpoint, } from "./core.js";
2
+ export type { CheckpointResult, EnableResult, LearnResult, Status, } from "./core.js";
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export * from "./core.js";
1
+ export { HandoffError, buildStartPacket, enableHandoff, enableSync, getStatus, learn, normalizeProjectId, syncVault, writeCheckpoint, } from "./core.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@leantli/agent-handoff",
3
- "version": "0.4.0",
4
- "description": "Shared vault handoff memory for Codex and Claude Code.",
3
+ "version": "0.5.1",
4
+ "description": "Shared vault handoff memory for coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "agent-handoff contributors",
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "scripts": {
19
19
  "build": "tsc",
20
- "prepack": "npm run build",
20
+ "prepare": "npm run build",
21
21
  "test": "vitest run",
22
22
  "typecheck": "tsc --noEmit"
23
23
  },
@@ -1,6 +1,6 @@
1
1
  ---
2
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.
3
+ description: Use when starting, resuming, pausing, checkpointing, or transferring coding-agent work across sessions, clones, worktrees, or devices with the agent-handoff CLI.
4
4
  ---
5
5
 
6
6
  # Agent Handoff
@@ -12,11 +12,16 @@ Use `agent-handoff` to restore and preserve coding-agent context.
12
12
  When beginning work in a repository:
13
13
 
14
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`.
15
+ 2. If status reports a problem, stop and report it. If the problem is that
16
+ agent-handoff is not enabled, tell the user to run `agent-handoff enable`.
17
+ 3. If status says `Sync: configured` and the user is switching devices or clones,
18
+ run `agent-handoff sync`.
17
19
  4. Run `agent-handoff start`.
18
20
  5. Read the returned packet before changing files.
19
21
 
22
+ Do not edit `AGENTS.md`, `CLAUDE.md`, or other instruction files to install
23
+ agent-handoff.
24
+
20
25
  ## During Work
21
26
 
22
27
  Use `learn` only for stable facts that should survive future sessions, clones,
@@ -55,7 +60,7 @@ agent, write a concise checkpoint:
55
60
  agent-handoff checkpoint --note "<current goal, completed work, open questions, next step>"
56
61
  ```
57
62
 
58
- If vault sync is configured, run:
63
+ If sync is configured, run:
59
64
 
60
65
  ```bash
61
66
  agent-handoff sync
@@ -69,4 +74,4 @@ If sync fails, keep the local checkpoint and report the error.
69
74
  - Keep checkpoints factual and concise.
70
75
  - Do not use `learn` for temporary task state; use `checkpoint` instead.
71
76
  - 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.
77
+ - Do not modify repository instruction files as part of agent-handoff installation or use.