@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 +10 -17
- package/dist/cli.js +10 -20
- package/dist/core.d.ts +0 -14
- package/dist/core.js +25 -103
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/resources/agent-handoff.SKILL.md +14 -13
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
|
|
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.
|
|
78
|
-
|
|
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,
|
|
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(
|
|
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
|
|
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,
|
|
2
|
-
export type { CheckpointResult,
|
|
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,
|
|
1
|
+
export { HandoffError, buildStartPacket, enableHandoff, enableSync, getStatus, learn, normalizeProjectId, syncVault, writeCheckpoint, } from "./core.js";
|
package/package.json
CHANGED
|
@@ -24,20 +24,12 @@ agent-handoff.
|
|
|
24
24
|
|
|
25
25
|
## Cross-Device Sync Setup
|
|
26
26
|
|
|
27
|
-
Use a private vault repository for sync.
|
|
28
|
-
|
|
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
|
|
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,
|