agent-merge-broker 0.3.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.
Files changed (75) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +352 -0
  3. package/dist/broker.d.ts +110 -0
  4. package/dist/broker.d.ts.map +1 -0
  5. package/dist/broker.js +1023 -0
  6. package/dist/broker.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +558 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/config.d.ts +19 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +312 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/errors.d.ts +16 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +36 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/git.d.ts +46 -0
  20. package/dist/git.d.ts.map +1 -0
  21. package/dist/git.js +195 -0
  22. package/dist/git.js.map +1 -0
  23. package/dist/hooks.d.ts +22 -0
  24. package/dist/hooks.d.ts.map +1 -0
  25. package/dist/hooks.js +110 -0
  26. package/dist/hooks.js.map +1 -0
  27. package/dist/index.d.ts +11 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +10 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/patterns.d.ts +9 -0
  32. package/dist/patterns.d.ts.map +1 -0
  33. package/dist/patterns.js +66 -0
  34. package/dist/patterns.js.map +1 -0
  35. package/dist/process.d.ts +30 -0
  36. package/dist/process.d.ts.map +1 -0
  37. package/dist/process.js +91 -0
  38. package/dist/process.js.map +1 -0
  39. package/dist/provenance.d.ts +10 -0
  40. package/dist/provenance.d.ts.map +1 -0
  41. package/dist/provenance.js +36 -0
  42. package/dist/provenance.js.map +1 -0
  43. package/dist/publisher.d.ts +37 -0
  44. package/dist/publisher.d.ts.map +1 -0
  45. package/dist/publisher.js +144 -0
  46. package/dist/publisher.js.map +1 -0
  47. package/dist/scheduler.d.ts +6 -0
  48. package/dist/scheduler.d.ts.map +1 -0
  49. package/dist/scheduler.js +96 -0
  50. package/dist/scheduler.js.map +1 -0
  51. package/dist/store.d.ts +73 -0
  52. package/dist/store.d.ts.map +1 -0
  53. package/dist/store.js +323 -0
  54. package/dist/store.js.map +1 -0
  55. package/dist/types.d.ts +219 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +3 -0
  58. package/dist/types.js.map +1 -0
  59. package/dist/validation.d.ts +13 -0
  60. package/dist/validation.d.ts.map +1 -0
  61. package/dist/validation.js +75 -0
  62. package/dist/validation.js.map +1 -0
  63. package/dist/verify.d.ts +40 -0
  64. package/dist/verify.d.ts.map +1 -0
  65. package/dist/verify.js +177 -0
  66. package/dist/verify.js.map +1 -0
  67. package/docs/ARCHITECTURE.md +144 -0
  68. package/docs/PROTOCOL.md +127 -0
  69. package/docs/RELEASING.md +23 -0
  70. package/docs/SECURITY.md +57 -0
  71. package/package.json +68 -0
  72. package/schemas/config.schema.json +149 -0
  73. package/schemas/provenance.schema.json +56 -0
  74. package/schemas/receipt.schema.json +39 -0
  75. package/templates/AGENTS.snippet.md +5 -0
@@ -0,0 +1,75 @@
1
+ import { resolveShell, runShell } from "./process.js";
2
+ import { matchesAny } from "./patterns.js";
3
+ import { ValidationError } from "./errors.js";
4
+ const OUTPUT_LIMIT_BYTES = 64 * 1_024;
5
+ function truncateOutput(value) {
6
+ if (Buffer.byteLength(value, "utf8") <= OUTPUT_LIMIT_BYTES)
7
+ return value;
8
+ const head = value.slice(0, 48 * 1_024);
9
+ const tail = value.slice(-16 * 1_024);
10
+ return `${head}\n... output truncated by Merge Broker ...\n${tail}`;
11
+ }
12
+ function renderCommand(command, taskId, files, shell) {
13
+ return command
14
+ .replaceAll("{taskId}", shell.quote(taskId ?? ""))
15
+ .replaceAll("{files}", files.map(shell.quote).join(" "));
16
+ }
17
+ /**
18
+ * A validator command is repository-trusted, but a lease token is a worker credential that no
19
+ * validator needs. Passing it through would hand every configured command the ability to submit or
20
+ * cancel on that worker's behalf.
21
+ */
22
+ function validatorEnvironment(overrides) {
23
+ const { MERGE_BROKER_TOKEN: _token, ...inherited } = process.env;
24
+ return { ...inherited, ...overrides };
25
+ }
26
+ export async function runValidators(options) {
27
+ const shell = resolveShell(options.shell);
28
+ const results = [];
29
+ for (const validator of options.validators) {
30
+ if (options.scope === "focused" &&
31
+ validator.paths &&
32
+ validator.paths.length > 0 &&
33
+ !options.files.some((file) => matchesAny(file, validator.paths ?? []))) {
34
+ continue;
35
+ }
36
+ const command = renderCommand(validator.command, options.taskId, options.files, shell);
37
+ const startedAt = new Date();
38
+ const result = await runShell(command, {
39
+ cwd: options.cwd,
40
+ allowFailure: true,
41
+ timeoutMs: (validator.timeoutSeconds ?? 900) * 1_000,
42
+ shell,
43
+ env: {
44
+ ...validatorEnvironment(validator.env),
45
+ MERGE_BROKER_TASK_ID: options.taskId ?? "",
46
+ MERGE_BROKER_FILES: options.files.join("\n"),
47
+ MERGE_BROKER_BASE_SHA: options.baseSha,
48
+ MERGE_BROKER_HEAD_SHA: options.headSha,
49
+ MERGE_BROKER_BATCH_ID: options.batchId,
50
+ },
51
+ });
52
+ const finishedAt = new Date();
53
+ const validation = {
54
+ name: validator.name,
55
+ command,
56
+ scope: options.scope,
57
+ ...(options.taskId ? { taskId: options.taskId } : {}),
58
+ startedAt: startedAt.toISOString(),
59
+ finishedAt: finishedAt.toISOString(),
60
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
61
+ exitCode: result.exitCode,
62
+ stdout: truncateOutput(result.stdout),
63
+ stderr: truncateOutput(result.stderr),
64
+ };
65
+ results.push(validation);
66
+ if (result.exitCode !== 0) {
67
+ throw new ValidationError(`Validator \"${validator.name}\" failed.`, {
68
+ validation,
69
+ completedValidations: results,
70
+ });
71
+ }
72
+ }
73
+ return results;
74
+ }
75
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import type { GitRepository } from "./git.js";
2
+ import type { BatchProvenance } from "./types.js";
3
+ export interface VerifyProvenanceOptions {
4
+ repo: GitRepository;
5
+ /** The pull request's head branch, which must be a broker integration branch. */
6
+ branch: string;
7
+ headSha: string;
8
+ /** The current tip of the target branch. */
9
+ baseSha: string;
10
+ baseBranch: string;
11
+ branchPrefix?: string;
12
+ provenanceDirectory?: string;
13
+ }
14
+ export interface ProvenanceVerification {
15
+ batchId: string;
16
+ manifestPath: string;
17
+ manifest: BatchProvenance;
18
+ /** The commit that introduced the manifest; the integrated work is its parent. */
19
+ provenanceSha: string;
20
+ parentSha: string;
21
+ taskIds: string[];
22
+ }
23
+ /**
24
+ * Reads verification policy from the configuration as it exists on the base branch. The checked-out
25
+ * tree belongs to the change being verified, so trusting its configuration would let a pull request
26
+ * widen the rules it is about to be judged by.
27
+ */
28
+ export declare function policyFromBase(repo: GitRepository, baseSha: string): Promise<{
29
+ baseBranch?: string;
30
+ branchPrefix?: string;
31
+ provenanceDirectory?: string;
32
+ }>;
33
+ export declare function batchIdFromBranch(branch: string, prefix: string): string;
34
+ /**
35
+ * Proves that a pull request is an unaltered broker batch: assembled on real base history, carrying
36
+ * every commit its receipts claim, changing exactly the files those receipts account for, and
37
+ * validated. Reads nothing but Git, so it works on any forge and before any dependency is installed.
38
+ */
39
+ export declare function verifyProvenance(options: VerifyProvenanceOptions): Promise<ProvenanceVerification>;
40
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,YAAY,CAAC;AAIhE,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,aAAa,CAAC;IACpB,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,eAAe,CAAC;IAC1B,kFAAkF;IAClF,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAMD;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBvF;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAYxE;AA8CD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA8GxG"}
package/dist/verify.js ADDED
@@ -0,0 +1,177 @@
1
+ import { BrokerError } from "./errors.js";
2
+ import { provenancePath } from "./provenance.js";
3
+ const MAX_BRANCH_UPDATE_MERGES = 50;
4
+ function invalid(message, details) {
5
+ return new BrokerError("PROVENANCE_INVALID", message, details);
6
+ }
7
+ /**
8
+ * Reads verification policy from the configuration as it exists on the base branch. The checked-out
9
+ * tree belongs to the change being verified, so trusting its configuration would let a pull request
10
+ * widen the rules it is about to be judged by.
11
+ */
12
+ export async function policyFromBase(repo, baseSha) {
13
+ const shown = await repo.git(["show", `${baseSha}:.merge-broker/config.json`], repo.root, true);
14
+ if (shown.exitCode !== 0)
15
+ return {};
16
+ try {
17
+ const config = JSON.parse(shown.stdout);
18
+ return {
19
+ ...(typeof config.baseBranch === "string" ? { baseBranch: config.baseBranch } : {}),
20
+ ...(typeof config.integration?.branchPrefix === "string"
21
+ ? { branchPrefix: config.integration.branchPrefix }
22
+ : {}),
23
+ ...(typeof config.integration?.provenance?.directory === "string"
24
+ ? { provenanceDirectory: config.integration.provenance.directory }
25
+ : {}),
26
+ };
27
+ }
28
+ catch {
29
+ return {};
30
+ }
31
+ }
32
+ export function batchIdFromBranch(branch, prefix) {
33
+ if (!branch.startsWith(prefix) || branch.length === prefix.length) {
34
+ throw invalid(`Expected a ${prefix}<batch-id> branch, received ${branch}. Work reaches the base branch through the broker, not directly.`, { branch, prefix });
35
+ }
36
+ const batchId = branch.slice(prefix.length);
37
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/u.test(batchId)) {
38
+ throw invalid(`Unsafe batch id in branch ${branch}.`, { branch });
39
+ }
40
+ return batchId;
41
+ }
42
+ async function isAncestor(repo, ancestor, descendant) {
43
+ const result = await repo.git(["merge-base", "--is-ancestor", ancestor, descendant], repo.root, true);
44
+ return result.exitCode === 0;
45
+ }
46
+ /**
47
+ * Finds the commit that introduced the manifest, stepping over the merges GitHub creates when a
48
+ * protected base requires branches to be up to date. Such a merge is accepted only when its merged
49
+ * side is already contained in the base and it changed nothing the base did not: anything else is a
50
+ * commit somebody pushed onto the branch after the broker assembled it.
51
+ */
52
+ async function findProvenanceCommit(repo, headSha, baseSha) {
53
+ let current = headSha;
54
+ for (let depth = 0; depth < MAX_BRANCH_UPDATE_MERGES; depth += 1) {
55
+ const listed = await repo.git(["rev-list", "--parents", "-n", "1", current]);
56
+ const parents = listed.stdout.trim().split(/\s+/u).slice(1);
57
+ if (parents.length < 2)
58
+ return current;
59
+ if (parents.length > 2)
60
+ throw invalid(`Octopus merge ${current} is not a recognised branch update.`);
61
+ const [firstParent, secondParent] = parents;
62
+ if (!(await isAncestor(repo, secondParent, baseSha))) {
63
+ throw invalid(`Merge ${current} brings in ${secondParent}, which is not part of ${baseSha}. Only base-branch updates may be added to an integration branch.`);
64
+ }
65
+ const conflictEdits = (await repo.git(["diff", "--name-only", firstParent, current])).stdout
66
+ .split("\n")
67
+ .filter(Boolean);
68
+ const baseChanges = new Set((await repo.git(["diff", "--name-only", `${firstParent}...${secondParent}`])).stdout
69
+ .split("\n")
70
+ .filter(Boolean));
71
+ const unexplained = conflictEdits.filter((file) => !baseChanges.has(file));
72
+ if (unexplained.length > 0) {
73
+ throw invalid(`Branch update ${current} changed files the base branch did not: ${unexplained.join(", ")}.`, {
74
+ files: unexplained,
75
+ });
76
+ }
77
+ current = firstParent;
78
+ }
79
+ throw invalid("Integration branch has too many merge commits to verify.");
80
+ }
81
+ /**
82
+ * Proves that a pull request is an unaltered broker batch: assembled on real base history, carrying
83
+ * every commit its receipts claim, changing exactly the files those receipts account for, and
84
+ * validated. Reads nothing but Git, so it works on any forge and before any dependency is installed.
85
+ */
86
+ export async function verifyProvenance(options) {
87
+ const { repo, branch, headSha, baseSha, baseBranch, branchPrefix = "merge-broker/", provenanceDirectory = ".merge-broker/attestations", } = options;
88
+ const batchId = batchIdFromBranch(branch, branchPrefix);
89
+ const manifestPath = provenancePath(provenanceDirectory, batchId);
90
+ const shown = await repo.git(["show", `${headSha}:${manifestPath}`], repo.root, true);
91
+ if (shown.exitCode !== 0) {
92
+ throw invalid(`Integration branch ${branch} does not contain ${manifestPath}.`, { manifestPath });
93
+ }
94
+ let manifest;
95
+ try {
96
+ manifest = JSON.parse(shown.stdout);
97
+ }
98
+ catch (error) {
99
+ throw invalid(`${manifestPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
100
+ }
101
+ if (manifest.version !== 1 || manifest.generator !== "agent-merge-broker") {
102
+ throw invalid(`Unsupported provenance manifest in ${manifestPath}.`);
103
+ }
104
+ if (manifest.batchId !== batchId) {
105
+ throw invalid(`Manifest batch ${manifest.batchId} does not match branch ${batchId}.`);
106
+ }
107
+ if (manifest.baseBranch !== baseBranch) {
108
+ throw invalid(`Batch targets ${manifest.baseBranch}, not ${baseBranch}.`);
109
+ }
110
+ // The batch must sit on real base history, but not necessarily on its current tip. Requiring
111
+ // equality would deadlock every batch: the base advances between integration and this check, and
112
+ // no amount of re-running makes a recorded base match a moving target.
113
+ if (manifest.baseSha !== baseSha && !(await isAncestor(repo, manifest.baseSha, baseSha))) {
114
+ throw invalid(`Batch was assembled on ${manifest.baseSha}, which is not an ancestor of ${baseBranch} at ${baseSha}. Re-integrate the batch.`);
115
+ }
116
+ const provenanceSha = await findProvenanceCommit(repo, headSha, baseSha);
117
+ const parentSha = (await repo.git(["rev-parse", `${provenanceSha}^`])).stdout.trim();
118
+ if (manifest.integratedHeadSha !== parentSha) {
119
+ throw invalid("The manifest is not the final provenance-only commit on this branch.", {
120
+ recorded: manifest.integratedHeadSha,
121
+ actual: parentSha,
122
+ });
123
+ }
124
+ const changedByManifest = (await repo.git(["diff-tree", "--no-commit-id", "--name-only", "-r", provenanceSha])).stdout
125
+ .split("\n")
126
+ .filter(Boolean);
127
+ if (changedByManifest.length !== 1 || changedByManifest[0] !== manifestPath) {
128
+ throw invalid("The final broker commit must change only its provenance manifest.", {
129
+ changed: changedByManifest,
130
+ });
131
+ }
132
+ if (!(await isAncestor(repo, manifest.baseSha, parentSha))) {
133
+ throw invalid("Integrated head does not descend from the recorded base.");
134
+ }
135
+ if (!Array.isArray(manifest.tasks) || manifest.tasks.length === 0) {
136
+ throw invalid("Manifest contains no tasks.");
137
+ }
138
+ const taskIds = manifest.tasks.map((task) => task.id);
139
+ if (JSON.stringify(taskIds) !== JSON.stringify(manifest.taskIds)) {
140
+ throw invalid("Manifest taskIds do not match its task records.");
141
+ }
142
+ // Compared against the base the manifest records, not the current tip: a later unrelated commit on
143
+ // the base branch would otherwise look like an unaccounted change.
144
+ const claimedPaths = [...new Set(manifest.tasks.flatMap((task) => task.actualPaths ?? []))].sort();
145
+ const integratedPaths = [
146
+ ...new Set((await repo.git(["diff", "--name-only", `${manifest.baseSha}..${parentSha}`])).stdout
147
+ .split("\n")
148
+ .filter(Boolean)),
149
+ ].sort();
150
+ if (JSON.stringify(claimedPaths) !== JSON.stringify(integratedPaths)) {
151
+ throw invalid("Manifest paths do not match the integrated diff.", {
152
+ claimed: claimedPaths,
153
+ integrated: integratedPaths,
154
+ });
155
+ }
156
+ // Squashing rewrites the batch into one commit and drops the cherry-pick trail, so submitted
157
+ // commits are only traceable when history was preserved. Manifests written before this field
158
+ // existed always preserved it.
159
+ if ((manifest.history ?? "preserve") === "preserve") {
160
+ const history = (await repo.git(["log", "--format=%B", `${manifest.baseSha}..${parentSha}`])).stdout;
161
+ for (const task of manifest.tasks) {
162
+ if (!Array.isArray(task.commits) || task.commits.length === 0) {
163
+ throw invalid(`Task ${task.id} records no submitted commits.`);
164
+ }
165
+ for (const commit of task.commits) {
166
+ if (!history.includes(`cherry picked from commit ${commit}`)) {
167
+ throw invalid(`Integrated history is missing submitted commit ${commit} from task ${task.id}.`);
168
+ }
169
+ }
170
+ }
171
+ }
172
+ if (!Array.isArray(manifest.validations) || manifest.validations.some((item) => item.exitCode !== 0)) {
173
+ throw invalid("Manifest records a failed validation.");
174
+ }
175
+ return { batchId, manifestPath, manifest, provenanceSha, parentSha, taskIds };
176
+ }
177
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,144 @@
1
+ # Architecture
2
+
3
+ ## Product boundary
4
+
5
+ Agent Merge Broker sits between code-producing workers and the repository's protected integration workflow. Workers own implementation and focused commits. The broker owns ordering, batching, validation, and publication. GitHub or another forge remains the review, policy, and deployment boundary.
6
+
7
+ The core depends on Git rather than a particular agent SDK. CLI JSON output and the exported Node API are the initial adapter surfaces.
8
+
9
+ ## Invariants
10
+
11
+ 1. A worker submits immutable commit IDs, never an uncommitted filesystem snapshot.
12
+ 2. Broker operations never merge into or rewrite the configured base branch.
13
+ 3. Integration happens in a dedicated disposable worktree based on a resolved base SHA.
14
+ 4. The retained branch is created only after every configured validator succeeds.
15
+ 5. A task is `merged` only after reconciliation or an explicit operator completion.
16
+ 6. State changes are atomic and serialized across linked Git worktrees.
17
+ 7. The lease token itself is never persisted; only its SHA-256 digest is stored.
18
+
19
+ ## Portable and runtime state
20
+
21
+ Portable repository policy is committed under `.merge-broker/`.
22
+
23
+ Runtime state is stored at:
24
+
25
+ ```text
26
+ $(git rev-parse --git-common-dir)/merge-broker/
27
+ ├── state.json
28
+ ├── audit.jsonl
29
+ ├── receipts/<task-id>.json
30
+ ├── batches/<batch-id>.json
31
+ ├── state.lock/
32
+ ├── integration.lock/
33
+ ├── archive/
34
+ └── worktrees/<batch-id>/
35
+ ```
36
+
37
+ JSON state writes use a temporary sibling followed by an atomic rename. Lock acquisition uses atomic directory creation. Short state mutations and long integration transactions use separate locks so status reads and heartbeats do not need to hold the integration lock.
38
+
39
+ ## Retention
40
+
41
+ Active state is a working set, not a historical record. Because `state.json` is rewritten in full on every transaction, an unbounded history makes every heartbeat progressively more expensive. `prune` moves completed tasks and batches into `archive/`, and the audit stream rotates into the same directory once the active file grows large. Archived material is never deleted.
42
+
43
+ Two records are deliberately not prunable. A completed task that a retained task still declares as a dependency stays, because `dependencyReady` cannot distinguish a pruned dependency from one that has never merged and would block the dependent forever. A batch stays while any of its tasks does, so a retained task never points at a batch that no longer exists.
44
+
45
+ Audit reads are tolerant by design: they scan a bounded tail and skip records that fail to parse. A crash between writing and flushing leaves a truncated line, which is exactly the moment the audit trail matters most.
46
+
47
+ ## Lock recovery
48
+
49
+ A lock owner records its process ID, hostname, and creation time. A holder on this machine whose process is gone is provably abandoned and is reclaimed after a short grace period. A holder on another machine cannot be probed — the state directory is shared, process IDs are not — so it waits out the full stale window rather than risking two integrations at once. `unlock` exposes that same decision to an operator, and `--force` is the deliberate override for a holder that cannot be proven dead.
50
+
51
+ ## Task lifecycle
52
+
53
+ ```text
54
+ registered → claimed → submitted → integrating → batched → published → merged
55
+ ↑ ↑ │ │ │
56
+ │ │ │ │ │ pull request closed
57
+ │ └─┼──────────┴─────────────────────┘ or batch-mate failed
58
+ └──── retry ←┴─ failed
59
+ ```
60
+
61
+ - `registered`: metadata exists but no active worker lease is required.
62
+ - `claimed`: an agent holds an expiring lease.
63
+ - `submitted`: immutable commits and their actual changed paths were recorded.
64
+ - `integrating`: selected by the process holding the integration lock.
65
+ - `batched`: validation passed and a local integration branch exists.
66
+ - `published`: the remote branch or PR was created.
67
+ - `merged`: the result reached the base workflow and dependencies may proceed.
68
+
69
+ Cancellation is allowed only before batching. After correction, `task submit` can replace the receipt or `task retry` can requeue the existing receipt.
70
+
71
+ A failed attempt is attributed as narrowly as the evidence allows. A cherry-pick conflict or a focused validation failure is attributable to one task: that task moves to `failed` with error evidence and its batch-mates return to `submitted`, so an unrelated agent's work is not held hostage. An authoritative validation failure indicts the entire batch and moves every selected task to `failed`, which prevents a polling broker from repeating the same broken batch.
72
+
73
+ A published batch whose pull request is closed without merging becomes `closed` rather than remaining `published`. Its tasks return to `submitted`, because a closed pull request means the work was rejected, not completed. `integration.maxAttempts` bounds how many times a task may be re-queued automatically before it is left `failed` for a human.
74
+
75
+ ## Scheduling
76
+
77
+ Submitted tasks are sorted by:
78
+
79
+ 1. descending priority;
80
+ 2. ascending submission time;
81
+ 3. lexical task ID.
82
+
83
+ The scheduler walks this list repeatedly so a parent selected in the current batch can unblock a child on the next pass. It rejects or defers candidates that exceed batch limits, conflict on an actual file, share a configured serialized resource, or depend on work that is neither in the batch nor `merged`.
84
+
85
+ This is a deterministic weighted greedy independent-set heuristic. Finding an optimal maximum non-conflicting set is not a project guarantee.
86
+
87
+ Expected path globs coordinate editing leases. Actual paths are derived from Git commits and drive integration batching. Expected scopes are conservative because two arbitrary glob languages cannot always be proven disjoint cheaply.
88
+
89
+ ## Transaction
90
+
91
+ For a selected batch the broker:
92
+
93
+ 1. Resolves the configured integration `baseRef` to a specific SHA while retaining `baseBranch` as the forge target.
94
+ 2. Marks selected tasks `integrating` under the state lock.
95
+ 3. Adds a detached Git worktree at that SHA.
96
+ 4. Cherry-picks each receipt commit with `-x` in dependency order.
97
+ 5. Runs applicable focused validators after each task, in a fixed non-login shell.
98
+ 6. Runs all authoritative validators over the complete batch.
99
+ 7. Optionally squashes the batch while preserving task IDs in the message.
100
+ 8. Optionally commits a provenance manifest whose parent is the integrated task head.
101
+ 9. Creates a uniquely named local branch at the resulting head.
102
+ 10. Removes the disposable worktree.
103
+ 11. Optionally pushes the branch and opens one GitHub PR.
104
+
105
+ A failed cherry-pick is aborted. No retained branch is created after a validation failure. Configurable failed-worktree retention exists for diagnosis, but defaults off because worktrees can contain build products and secrets.
106
+
107
+ ## Publication and reconciliation
108
+
109
+ Publication supports three modes:
110
+
111
+ - `none`: retain a local branch only.
112
+ - `branch`: push one branch.
113
+ - `pull-request`: push and invoke `gh pr create`.
114
+
115
+ When provenance is enabled, the final generated commit records the base SHA,
116
+ integrated parent, task receipts, paths, and completed broker validators. A
117
+ remote workflow can validate this structure before dependency installation and
118
+ then either trust broker-authoritative validation or run the single
119
+ authoritative suite itself.
120
+
121
+ PR reconciliation queries GitHub for merged state. Branch reconciliation fetches the configured base and checks that the batch head is an ancestor. Squash and rebase workflows may require the explicit `batch complete` escape hatch because the local batch head can disappear from final ancestry.
122
+
123
+ ## Remote verification
124
+
125
+ `verify-provenance` is the read-only inverse of integration: given a branch, its head, and the base,
126
+ it re-derives what the broker must have done and rejects anything else. It uses only Git, so it runs
127
+ on any forge, in any language ecosystem, before dependencies are installed — which is what makes it
128
+ cheap enough to require on every pull request.
129
+
130
+ It is deliberately tolerant of one thing: the merges a forge creates when a protected base requires
131
+ branches to be up to date. Such a merge is accepted only when its merged side is already contained
132
+ in the base and it changed nothing the base did not; the manifest commit is then located behind it.
133
+ Every other commit added to an integration branch fails verification.
134
+
135
+ Squashed batches cannot be traced commit by commit, because squashing discards the cherry-pick
136
+ trail. The manifest records which mode produced it so a verifier knows which guarantees apply.
137
+
138
+ ## Failure isolation
139
+
140
+ The first failing cherry-pick identifies a commit and task. Focused validation identifies a task-scoped failure. An authoritative failure identifies the complete batch but may represent an interaction between otherwise valid tasks. Version 0.1 deliberately stops and marks the bounded batch failed rather than automatically guessing a resolution. Operators can requeue the unchanged receipt explicitly. Automatic delta debugging can be layered on without weakening the transaction invariant.
141
+
142
+ ## Future adapters
143
+
144
+ Adapters should translate their native task system into the protocol rather than receive Git administration privileges. Natural additions are MCP, GitHub Actions, Codex, Claude Code, and generic webhook adapters. The core state machine must remain usable without them.
@@ -0,0 +1,127 @@
1
+ # Worker and adapter protocol
2
+
3
+ ## Contract
4
+
5
+ A worker needs only five capabilities:
6
+
7
+ 1. Claim an ID and expected path scope.
8
+ 2. Maintain an expiring lease while editing.
9
+ 3. Create one or more focused Git commits.
10
+ 4. Submit those commit IDs with the lease token.
11
+ 5. Stop performing Git administration after submission.
12
+
13
+ All CLI commands support `--json`. Successful commands write one JSON value to stdout and exit zero. Errors write a stable code, message, and optional details to stderr and exit nonzero.
14
+
15
+ ## Claim
16
+
17
+ ```bash
18
+ merge-broker --json task claim TASK-123 \
19
+ --holder adapter/session-456 \
20
+ --agent codex \
21
+ --base main \
22
+ --path 'src/billing/**' \
23
+ --depends-on TASK-100 \
24
+ --priority 20
25
+ ```
26
+
27
+ The response contains a one-time `token`. Adapters should hold it in process memory or an appropriate secret store. The persisted state contains only a digest.
28
+
29
+ ## Heartbeat
30
+
31
+ Heartbeat before the lease expiry shown in the claim response:
32
+
33
+ ```bash
34
+ MERGE_BROKER_TOKEN=... merge-broker --json task heartbeat TASK-123
35
+ ```
36
+
37
+ An expired lease can be claimed by another worker. A former holder must not continue editing after losing the lease.
38
+
39
+ ## Extend scope
40
+
41
+ When a discovered dependency expands the task, extend the active lease with
42
+ the same token before editing the additional path:
43
+
44
+ ```bash
45
+ MERGE_BROKER_TOKEN=... merge-broker --json task extend TASK-123 \
46
+ --path 'src/shared/contract.ts'
47
+ ```
48
+
49
+ The broker rechecks active and serialized-resource conflicts before accepting
50
+ the larger scope.
51
+
52
+ ## Submit
53
+
54
+ ```bash
55
+ MERGE_BROKER_TOKEN=... merge-broker --json task submit TASK-123 \
56
+ --commit a1b2c3d \
57
+ --commit d4e5f6a
58
+ ```
59
+
60
+ Commit order is significant. The broker resolves every revision to a full immutable commit ID and computes actual paths from Git rather than trusting the caller.
61
+
62
+ The receipt written under Git's common directory conforms to [`../schemas/receipt.schema.json`](../schemas/receipt.schema.json). It contains no lease credential.
63
+
64
+ ## Published batch provenance
65
+
66
+ With `integration.provenance.enabled`, the broker's final integration commit
67
+ adds one JSON manifest under the configured repository-relative directory. Its
68
+ parent is the assembled task head. The record binds the branch to its base,
69
+ task IDs, submitted commits, changed paths, dependencies, and any broker-side
70
+ validators. It conforms to
71
+ [`../schemas/provenance.schema.json`](../schemas/provenance.schema.json).
72
+
73
+ Remote policy should verify the branch prefix, manifest path, batch ID, base
74
+ SHA, final-commit parent, and one-file provenance commit before spending time
75
+ on dependency installation or authoritative CI.
76
+
77
+ ## Path semantics
78
+
79
+ Paths are repository-relative and use forward slashes. Globs use picomatch semantics. Prefer the smallest scope that covers expected work:
80
+
81
+ ```text
82
+ src/billing/**
83
+ test/billing/**
84
+ prisma/schema.prisma
85
+ ```
86
+
87
+ Avoid `**/*` unless the task genuinely owns the repository. A committed file outside the expected scope is rejected by default.
88
+
89
+ ## Dependencies
90
+
91
+ Dependencies refer to broker task IDs. With `requireDependencies` enabled, every referenced task must exist before submission. A dependency is schedulable when it is selected earlier in the same batch or has reached `merged` status.
92
+
93
+ Repository adapters should distinguish `baseRef`, the Git revision used to build transactions, from `baseBranch`, the forge target. For repositories with a passive local checkout, use `baseRef: "origin/main"` and `baseBranch: "main"`.
94
+
95
+ Published or prepared work is not treated as merged. This prevents a child from being integrated against a base that does not contain its parent.
96
+
97
+ ## Stable error categories
98
+
99
+ Adapters should primarily branch on these codes:
100
+
101
+ - `LEASE_CONFLICT`, `LEASE_EXPIRED`, `LEASE_TOKEN`
102
+ - `UNEXPECTED_PATHS`
103
+ - `INVALID_TASK`, `UNKNOWN_TASK`, `UNKNOWN_DEPENDENCY`
104
+ - `UNKNOWN_COMMIT`, `DUPLICATE_COMMIT`, `EMPTY_COMMIT`, `MERGE_COMMIT`
105
+ - `CHERRY_PICK_CONFLICT`
106
+ - `VALIDATION_FAILED`
107
+ - `EMPTY_BATCH`
108
+ - `LOCK_TIMEOUT`, `LOCK_HELD`
109
+ - `PUBLISH_DISABLED`, `PUBLISH_FAILED`, `AUTO_MERGE_FAILED`
110
+ - `PROVENANCE_INVALID`
111
+ - `HOOKS_PATH_CONFLICT`
112
+
113
+ Additional codes may be introduced. Adapters must display unknown errors rather than treating them as success.
114
+
115
+ ## Programmatic use
116
+
117
+ The package exports `MergeBroker`, repository/configuration types, state types, and error classes:
118
+
119
+ ```ts
120
+ import { MergeBroker } from "agent-merge-broker";
121
+
122
+ const broker = await MergeBroker.open("/path/to/worktree");
123
+ const plan = await broker.plan();
124
+ const result = await broker.integrate({ dryRun: true });
125
+ ```
126
+
127
+ Programmatic callers share the same filesystem locks and state machine as CLI callers.
@@ -0,0 +1,23 @@
1
+ # Releasing
2
+
3
+ The package name `agent-merge-broker` was unclaimed on npm when the project was initialized. Registry availability is not reserved until the first publication.
4
+
5
+ ## First release setup
6
+
7
+ 1. Create the public GitHub repository and update package metadata if its owner differs from `WeSpitfire`.
8
+ 2. Confirm the package name and ownership with `npm view agent-merge-broker`.
9
+ 3. Configure npm trusted publishing for the GitHub repository and the `release.yml` workflow, or add an appropriately scoped `NPM_TOKEN` and adjust the workflow.
10
+ 4. Protect `main` and require the CI workflow.
11
+ 5. Add a private vulnerability-reporting contact to `docs/SECURITY.md`.
12
+
13
+ ## Release procedure
14
+
15
+ 1. Update `CHANGELOG.md` and remove the `Unreleased` marker for the target version.
16
+ 2. Update `package.json` with `npm version <major|minor|patch> --no-git-tag-version`.
17
+ 3. Run `npm run verify` and `npm pack --dry-run`.
18
+ 4. Commit the release metadata and merge it through the normal protected workflow.
19
+ 5. Create a GitHub release tagged `v<version>`.
20
+
21
+ Publishing the GitHub release invokes the release workflow, repeats verification, checks that the tag equals the package version, and publishes with npm provenance.
22
+
23
+ Do not reuse or move a published version tag. If a release is incorrect, deprecate it and publish a corrected patch version.