@leantli/agent-handoff 0.5.3 → 0.6.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/README.md CHANGED
@@ -66,7 +66,7 @@ For project-specific decisions or branch-specific context:
66
66
 
67
67
  ```bash
68
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."
69
+ agent-handoff learn --scope branch --kind context --note "This branch is testing the sync workflow."
70
70
  ```
71
71
 
72
72
  ## Cross-Device Sync
@@ -74,27 +74,17 @@ agent-handoff learn --scope branch --kind context --note "This branch is testing
74
74
  Local cross-session memory works immediately after `agent-handoff enable`. No
75
75
  git repository is needed for the vault.
76
76
 
77
- Cross-device sync is optional. The vault repository should be private because it
78
- can contain project background, preferences, decisions, and handoff notes.
79
-
80
- If GitHub CLI (`gh`) is installed and authenticated, let `agent-handoff` create
81
- the private repository:
82
-
83
- ```bash
84
- agent-handoff sync create you/agent-handoff-vault
85
- agent-handoff sync
86
- ```
87
-
88
- `sync create` always uses `gh repo create --private`. If the repository already
89
- exists and is public, `agent-handoff` refuses to use it.
90
-
91
- If you create the repository manually, make it private, then run:
77
+ Cross-device sync is optional. Create a dedicated private repository for the
78
+ vault. Do not use a project code repository as the vault. Then run:
92
79
 
93
80
  ```bash
94
81
  agent-handoff sync init git@github.com:you/agent-handoff-vault.git
95
82
  agent-handoff sync
96
83
  ```
97
84
 
85
+ Use a private repository because the vault can contain project background,
86
+ preferences, decisions, and handoff notes.
87
+
98
88
  Run `agent-handoff sync init <same-git-url>` once on each device that should
99
89
  share the vault. After that, run `agent-handoff sync` before starting on another
100
90
  device and after writing useful checkpoints.
@@ -102,6 +92,10 @@ device and after writing useful checkpoints.
102
92
  By default the vault lives at `~/.agent-handoff/vault`. Use `--home` or
103
93
  `--vault` when you need a different location. `agent-handoff sync` commits local
104
94
  vault changes, pulls/rebases remote vault changes, then pushes the result.
95
+ If `sync init` says the local vault has unsynced memory and the remote already
96
+ has data, back up or manually merge the local vault before joining that remote.
97
+ If git reports a conflict or active operation, resolve the conflict in the vault,
98
+ finish or abort the active git operation, then run `agent-handoff sync` again.
105
99
 
106
100
  ## How Projects Are Identified
107
101
 
@@ -167,7 +161,6 @@ agent-handoff enable # create local memory and install the user skill
167
161
  agent-handoff start # print context for the current project and branch
168
162
  agent-handoff checkpoint # write a session checkpoint
169
163
  agent-handoff learn # store durable global/project/branch memory
170
- agent-handoff sync create # create a private GitHub vault repo with gh
171
164
  agent-handoff sync init # enable optional cross-device sync
172
165
  agent-handoff sync # pull/rebase and push the vault
173
166
  agent-handoff status # quick readiness and sync-state check
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { Command, CommanderError, Option } from "commander";
3
- import { HandoffError, buildStartPacket, createGitHubSyncRepo, enableHandoff, enableSync, getStatus, learn, syncVault, writeCheckpoint, } from "./core.js";
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;
@@ -25,7 +25,7 @@ function buildProgram(stdout, stderr, stdin, cwd) {
25
25
  program
26
26
  .name("agent-handoff")
27
27
  .description("Shared vault handoff memory for coding agents.")
28
- .version("agent-handoff 0.5.3")
28
+ .version(`agent-handoff ${packageVersion()}`)
29
29
  .exitOverride()
30
30
  .configureOutput({
31
31
  writeOut: (str) => stdout.write(str),
@@ -92,26 +92,9 @@ function buildProgram(stdout, stderr, stdin, cwd) {
92
92
  stdout.write(`Learned ${result.kind}: ${result.path}\n`);
93
93
  });
94
94
  const sync = program.command("sync").description("Sync the handoff vault.");
95
- sync
96
- .command("create <repository>")
97
- .description("Create a private GitHub repository with gh and enable cross-device sync.")
98
- .option("--vault <path>", "Vault directory. Defaults to HOME/vault.")
99
- .addOption(new Option("--protocol <protocol>", "Git remote protocol to store in config.").choices(["https", "ssh"]).default("https"))
100
- .action((repository, options) => {
101
- const result = createGitHubSyncRepo({
102
- home: globalHome(program),
103
- vault: options.vault,
104
- repository,
105
- protocol: options.protocol,
106
- });
107
- stdout.write(`${result.created ? "Created" : "Using existing"} private GitHub repository: ${result.repository}\n`);
108
- stdout.write(`Cross-device sync enabled: ${result.syncUrl}\n`);
109
- stdout.write(`Vault: ${result.setup.vault}\n`);
110
- stdout.write("Run agent-handoff sync to push local memory.\n");
111
- });
112
95
  sync
113
96
  .command("init <git-url>")
114
- .description("Enable cross-device sync with a private git repository.")
97
+ .description("Enable cross-device sync with a dedicated private vault repository.")
115
98
  .option("--vault <path>", "Vault directory. Defaults to HOME/vault.")
116
99
  .action((gitUrl, options) => {
117
100
  const result = enableSync({ home: globalHome(program), vault: options.vault, syncUrl: gitUrl });
@@ -158,6 +141,13 @@ function readNote(options, stdin) {
158
141
  }
159
142
  return readFileSync(0, "utf8");
160
143
  }
144
+ function packageVersion() {
145
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
146
+ if (typeof pkg.version !== "string") {
147
+ throw new HandoffError("package.json is missing a string version");
148
+ }
149
+ return pkg.version;
150
+ }
161
151
  function printProblems(problems, stdout) {
162
152
  for (const problem of problems) {
163
153
  stdout.write(`- ${problem}\n`);
package/dist/core.d.ts CHANGED
@@ -16,13 +16,6 @@ export interface EnableResult {
16
16
  setup: SetupResult;
17
17
  skill: InstallSkillResult;
18
18
  }
19
- export interface CreateGitHubSyncRepoResult {
20
- repository: string;
21
- created: boolean;
22
- isPrivate: boolean;
23
- syncUrl: string;
24
- setup: SetupResult;
25
- }
26
19
  export interface CheckpointResult {
27
20
  path: string;
28
21
  projectId: string;
@@ -46,7 +39,6 @@ export interface Status {
46
39
  syncConfigured: boolean;
47
40
  syncUrl?: string;
48
41
  }
49
- type GitHubRemoteProtocol = "https" | "ssh";
50
42
  export declare function setupHome(opts?: {
51
43
  home?: string;
52
44
  vault?: string;
@@ -66,12 +58,6 @@ export declare function enableSync(opts: {
66
58
  vault?: string;
67
59
  syncUrl: string;
68
60
  }): SetupResult;
69
- export declare function createGitHubSyncRepo(opts: {
70
- home?: string;
71
- vault?: string;
72
- repository: string;
73
- protocol?: GitHubRemoteProtocol;
74
- }): CreateGitHubSyncRepoResult;
75
61
  export declare function deriveProjectId(root?: string, projectId?: string): string;
76
62
  export declare function currentBranch(root?: string): string;
77
63
  export declare function buildStartPacket(opts?: {
package/dist/core.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
3
3
  import { hostname, homedir } from "node:os";
4
- import { join, resolve } from "node:path";
4
+ import { isAbsolute, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  export const DEFAULT_HOME = join(homedir(), ".agent-handoff");
7
7
  export const CONFIG_FILE = "config.json";
@@ -130,39 +130,6 @@ export function enableHandoff(opts = {}) {
130
130
  export function enableSync(opts) {
131
131
  return setupHome({ home: opts.home, vault: opts.vault, syncUrl: opts.syncUrl });
132
132
  }
133
- export function createGitHubSyncRepo(opts) {
134
- const repository = normalizeGitHubRepository(opts.repository);
135
- const protocol = opts.protocol ?? "https";
136
- let created = false;
137
- let info = inspectGitHubRepo(repository);
138
- if (!info) {
139
- ghChecked([
140
- "repo",
141
- "create",
142
- repository,
143
- "--private",
144
- "--description",
145
- "Private vault for agent-handoff shared coding-agent context.",
146
- ]);
147
- created = true;
148
- info = inspectGitHubRepo(repository);
149
- if (!info) {
150
- throw new HandoffError(`created GitHub repository ${repository} but could not inspect it with gh`);
151
- }
152
- }
153
- if (!info.isPrivate) {
154
- throw new HandoffError(`GitHub repository ${repository} is public; use a private repository for agent-handoff sync`);
155
- }
156
- const syncUrl = githubRepoSyncUrl(info, protocol);
157
- const setup = enableSync({ home: opts.home, vault: opts.vault, syncUrl });
158
- return {
159
- repository: info.nameWithOwner ?? repository,
160
- created,
161
- isPrivate: info.isPrivate,
162
- syncUrl,
163
- setup,
164
- };
165
- }
166
133
  export function deriveProjectId(root = ".", projectId) {
167
134
  const rootPath = resolve(root);
168
135
  if (projectId)
@@ -282,6 +249,9 @@ export function syncVault(opts = {}) {
282
249
  if (syncProblem)
283
250
  throw new HandoffError(syncProblem);
284
251
  const outputs = [];
252
+ if (hasActiveGitOperation(setup.vault) || hasUnmergedFiles(setup.vault)) {
253
+ throw syncConflictError(setup.vault);
254
+ }
285
255
  gitChecked(setup.vault, ["add", "-A"]);
286
256
  const staged = gitRun(setup.vault, ["diff", "--cached", "--quiet"]).status !== 0;
287
257
  if (staged) {
@@ -299,6 +269,9 @@ export function syncVault(opts = {}) {
299
269
  const pull = gitRun(setup.vault, ["pull", "--rebase", "--autostash", "origin", branch]);
300
270
  const ignoredEmptyRemotePull = pull.status !== 0 && isEmptyRemotePull(pull.output);
301
271
  if (pull.status !== 0 && !ignoredEmptyRemotePull) {
272
+ if (hasActiveGitOperation(setup.vault) || hasUnmergedFiles(setup.vault)) {
273
+ throw syncConflictError(setup.vault, pull.output);
274
+ }
302
275
  throw new HandoffError(pull.output.trim() || "git pull failed");
303
276
  }
304
277
  if (!ignoredEmptyRemotePull && pull.output.trim())
@@ -482,55 +455,6 @@ function syncConfigProblem(vault, syncUrl) {
482
455
  }
483
456
  return null;
484
457
  }
485
- function normalizeGitHubRepository(value) {
486
- const repository = value.trim().replace(/\/+$/g, "").replace(/\.git$/i, "");
487
- if (repository.includes("://") || repository.startsWith("git@")) {
488
- throw new HandoffError("GitHub repository must be in owner/name form, for example leantli/agent-handoff-vault");
489
- }
490
- if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) {
491
- throw new HandoffError("GitHub repository must be in owner/name form, for example leantli/agent-handoff-vault");
492
- }
493
- return repository;
494
- }
495
- function inspectGitHubRepo(repository) {
496
- const result = ghRun(["repo", "view", repository, "--json", "nameWithOwner,isPrivate,url,sshUrl"]);
497
- if (result.status !== 0) {
498
- if (/could not resolve|not found|repository not found/i.test(result.output))
499
- return null;
500
- throw new HandoffError(result.output.trim() || `cannot inspect GitHub repository ${repository} with gh`);
501
- }
502
- let raw;
503
- try {
504
- raw = JSON.parse(result.output);
505
- }
506
- catch (error) {
507
- const detail = error instanceof Error ? error.message : String(error);
508
- throw new HandoffError(`gh repo view returned invalid JSON: ${detail}`);
509
- }
510
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
511
- throw new HandoffError("gh repo view returned invalid repository data");
512
- }
513
- const data = raw;
514
- if (typeof data.isPrivate !== "boolean") {
515
- throw new HandoffError("gh repo view returned invalid repository privacy data");
516
- }
517
- return {
518
- nameWithOwner: typeof data.nameWithOwner === "string" ? data.nameWithOwner : undefined,
519
- isPrivate: data.isPrivate,
520
- url: typeof data.url === "string" ? data.url : undefined,
521
- sshUrl: typeof data.sshUrl === "string" ? data.sshUrl : undefined,
522
- };
523
- }
524
- function githubRepoSyncUrl(info, protocol) {
525
- const raw = protocol === "ssh" ? info.sshUrl : info.url;
526
- if (!raw) {
527
- throw new HandoffError(`GitHub repository is missing a ${protocol} clone URL`);
528
- }
529
- if (raw.startsWith("https://github.com/") && !raw.endsWith(".git")) {
530
- return `${raw}.git`;
531
- }
532
- return raw;
533
- }
534
458
  function loadSetup(home) {
535
459
  const config = readConfig(home);
536
460
  if (!config)
@@ -789,26 +713,6 @@ function gitChecked(root, args) {
789
713
  }
790
714
  return result.output.trim();
791
715
  }
792
- function ghChecked(args) {
793
- const result = ghRun(args);
794
- if (result.status !== 0) {
795
- throw new HandoffError(result.output.trim() || `gh ${args.join(" ")} failed`);
796
- }
797
- return result.output.trim();
798
- }
799
- function ghRun(args) {
800
- const result = spawnSync("gh", args, {
801
- encoding: "utf8",
802
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
803
- stdio: ["ignore", "pipe", "pipe"],
804
- timeout: GIT_TIMEOUT_MS,
805
- });
806
- const error = result.error ? `\n${result.error.message}` : "";
807
- return {
808
- status: result.status ?? 1,
809
- output: `${result.stdout ?? ""}${result.stderr ?? ""}${error}`,
810
- };
811
- }
812
716
  function gitRun(root, args) {
813
717
  const result = spawnSync("git", args, {
814
718
  cwd: root,
@@ -823,6 +727,24 @@ function gitRun(root, args) {
823
727
  output: `${result.stdout ?? ""}${result.stderr ?? ""}${error}`,
824
728
  };
825
729
  }
730
+ function hasUnmergedFiles(vault) {
731
+ const result = gitRun(vault, ["diff", "--name-only", "--diff-filter=U"]);
732
+ return result.status === 0 && result.output.trim().length > 0;
733
+ }
734
+ function hasActiveGitOperation(vault) {
735
+ return ["rebase-merge", "rebase-apply", "MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"].some((name) => gitPathExists(vault, name));
736
+ }
737
+ function gitPathExists(vault, name) {
738
+ const path = gitOutput(vault, ["rev-parse", "--git-path", name]);
739
+ if (!path)
740
+ return false;
741
+ return existsSync(isAbsolute(path) ? path : join(vault, path));
742
+ }
743
+ function syncConflictError(vault, detail) {
744
+ const message = `sync conflict in ${vault}; resolve conflicts in that vault, finish or abort the active git operation, then run agent-handoff sync again`;
745
+ const trimmed = detail?.trim();
746
+ return new HandoffError(trimmed ? `${message}\n\n${trimmed}` : message);
747
+ }
826
748
  function isEmptyRemotePull(output) {
827
749
  const lowered = output.toLowerCase();
828
750
  return lowered.includes("couldn't find remote ref") || lowered.includes("could not find remote ref");
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { HandoffError, buildStartPacket, createGitHubSyncRepo, enableHandoff, enableSync, getStatus, learn, normalizeProjectId, syncVault, writeCheckpoint, } from "./core.js";
2
- export type { CheckpointResult, CreateGitHubSyncRepoResult, EnableResult, LearnResult, Status, } 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 { HandoffError, buildStartPacket, createGitHubSyncRepo, enableHandoff, enableSync, getStatus, learn, normalizeProjectId, syncVault, writeCheckpoint, } from "./core.js";
1
+ export { HandoffError, buildStartPacket, enableHandoff, enableSync, getStatus, learn, normalizeProjectId, syncVault, writeCheckpoint, } from "./core.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leantli/agent-handoff",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Shared vault handoff memory for coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,20 +24,12 @@ agent-handoff.
24
24
 
25
25
  ## Cross-Device Sync Setup
26
26
 
27
- Use a private vault repository for sync. The vault can contain project context,
28
- preferences, decisions, and handoff notes.
27
+ Use a dedicated private vault repository for sync. Do not use a project code
28
+ repository as the vault. The vault can contain project context, preferences,
29
+ decisions, and handoff notes.
29
30
 
30
- If the user asks you to create the GitHub sync repository, run:
31
-
32
- ```bash
33
- agent-handoff sync create <owner>/<repo>
34
- agent-handoff sync
35
- ```
36
-
37
- `sync create` uses GitHub CLI (`gh`) and creates the repository as private. If an
38
- existing repository is public, do not use it for sync.
39
-
40
- If the user already has a private git repository for the vault, run:
31
+ If the user asks you to set up cross-device sync, make sure the vault repository
32
+ they created is private, then run:
41
33
 
42
34
  ```bash
43
35
  agent-handoff sync init <private-git-url>
@@ -47,6 +39,15 @@ agent-handoff sync
47
39
  On another device, install and enable the CLI, then run `sync init` with the same
48
40
  private git URL, `agent-handoff sync`, and `agent-handoff start`.
49
41
 
42
+ If `sync init` says the local vault has unsynced memory and the remote already
43
+ has data, keep the local files and report it. The user or agent should back up
44
+ or manually merge the local vault before joining that remote.
45
+
46
+ If sync fails because git reports a conflict or active operation, keep the local
47
+ files and report the conflict. The user or agent should resolve it inside the
48
+ vault, finish or abort the active git operation, then run `agent-handoff sync`
49
+ again.
50
+
50
51
  ## During Work
51
52
 
52
53
  Use `learn` only for stable facts that should survive future sessions, clones,