lockage 0.0.0-stage → 0.9.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +355 -2
  3. package/dist/db/index.d.ts +52 -0
  4. package/dist/db/index.js +238 -0
  5. package/dist/db/schema.sql +238 -0
  6. package/dist/hook/clients.d.ts +56 -0
  7. package/dist/hook/clients.js +258 -0
  8. package/dist/hook/context.d.ts +22 -0
  9. package/dist/hook/context.js +40 -0
  10. package/dist/hook/dispatch.d.ts +21 -0
  11. package/dist/hook/dispatch.js +212 -0
  12. package/dist/hook/guard.d.ts +34 -0
  13. package/dist/hook/guard.js +106 -0
  14. package/dist/hook/repo.d.ts +40 -0
  15. package/dist/hook/repo.js +173 -0
  16. package/dist/hook/setup-kiro.d.ts +30 -0
  17. package/dist/hook/setup-kiro.js +118 -0
  18. package/dist/index.d.ts +47 -0
  19. package/dist/index.js +694 -0
  20. package/dist/launcher/client.d.ts +22 -0
  21. package/dist/launcher/client.js +57 -0
  22. package/dist/launcher/publish-adapter.d.ts +88 -0
  23. package/dist/launcher/publish-adapter.js +233 -0
  24. package/dist/launcher/publish-cli.d.ts +2 -0
  25. package/dist/launcher/publish-cli.js +123 -0
  26. package/dist/launcher/publish.d.ts +140 -0
  27. package/dist/launcher/publish.js +522 -0
  28. package/dist/lockage/board.d.ts +247 -0
  29. package/dist/lockage/board.js +407 -0
  30. package/dist/lockage/claim.d.ts +307 -0
  31. package/dist/lockage/claim.js +580 -0
  32. package/dist/lockage/files.d.ts +229 -0
  33. package/dist/lockage/files.js +403 -0
  34. package/dist/lockage/git.d.ts +92 -0
  35. package/dist/lockage/git.js +183 -0
  36. package/dist/lockage/globs.d.ts +26 -0
  37. package/dist/lockage/globs.js +125 -0
  38. package/dist/lockage/merge.d.ts +132 -0
  39. package/dist/lockage/merge.js +831 -0
  40. package/dist/lockage/publish-lock.d.ts +38 -0
  41. package/dist/lockage/publish-lock.js +73 -0
  42. package/dist/lockage/tickets.d.ts +213 -0
  43. package/dist/lockage/tickets.js +340 -0
  44. package/dist/lockage/worktree.d.ts +212 -0
  45. package/dist/lockage/worktree.js +563 -0
  46. package/package.json +43 -4
@@ -0,0 +1,92 @@
1
+ /**
2
+ * A thin wrapper that runs a git command repo-scoped (`git -C <repo>`).
3
+ * Because of the multi-repo assumption (§3: tasks.repo is required), worktree assignment, the merge
4
+ * gate, and the 3-way precheck all run in this `git -C <repo>` scope.
5
+ *
6
+ * @returns the trimmed stdout string.
7
+ * @throws execFileSync throws on a non-zero exit (caught by the caller).
8
+ *
9
+ * HIGH-2 mitigation: expand maxBuffer from the default (1MB) to 64MB. For many-file changes,
10
+ * `diff --name-only` / `merge-tree --write-tree` / `show --name-only` throwing ENOBUFS past 1MB
11
+ * would (a) permanently stall the merge gate if via threeWayPrecheck, and (b) make gitConflictFiles'
12
+ * catch misparse the truncated stdout. With a sufficient buffer, this throw does not occur for
13
+ * realistic inputs.
14
+ */
15
+ export declare function git(repoPath: string, args: string[]): string;
16
+ /**
17
+ * Resolve the repo's main branch name (§4.6).
18
+ * Rather than hardcoding `main`, allow `dev`/`stg`/`master` etc. per repo.
19
+ *
20
+ * Phase 1 default policy: explicit value > the first of `dev`/`stg`/`main`/`master` that exists in
21
+ * the repo > `main`.
22
+ * (Leaves room to replace with a repo_config table in Phase 2, but for now it is inferred from
23
+ * existing branches.)
24
+ */
25
+ export declare function resolveBaseBranch(repoPath: string, explicit?: string): string;
26
+ /**
27
+ * A best-effort helper that resolves a ref (branch name/tag/commit, etc.) to an existing commit SHA.
28
+ *
29
+ * Using `git rev-parse --verify <ref>^{commit}`, it verifies that "the ref exists and reaches a
30
+ * commit" while folding to a 40-char SHA. Used to record the fork-point SHA at worktree assignment
31
+ * (to later compute the diff from base `git diff --name-only <base_sha>...HEAD`).
32
+ *
33
+ * Like the existing resolveBaseBranch, a best-effort policy that swallows failures: when it cannot
34
+ * resolve (ref absent, empty repo, before the initial commit, etc.), it returns null instead of
35
+ * throwing. The caller is designed to save base_sha as NULL when null, and the diff side falls back
36
+ * to status only when base_sha is absent.
37
+ */
38
+ export declare function resolveRefSha(repoPath: string, ref: string): string | null;
39
+ /**
40
+ * A best-effort helper that lists a worktree's working-tree changed files (Issue #52 proposal 2).
41
+ *
42
+ * Runs `git status --porcelain -z` against worktreePath (same style as classifyRemoveFailure = take
43
+ * git status against the worktree path) and extracts the changed file paths from each entry.
44
+ *
45
+ * Reasons for using `-z` (NUL-separated):
46
+ * - Each entry is terminated with NUL (\0) and the path is not quoted (normal `--porcelain` quotes
47
+ * and escapes paths containing spaces etc. with `"..."`, but -z emits them raw).
48
+ * - We do not trim the output, so there is no column shift from the leading space left in a
49
+ * "worktree-only change ( M path)" or the leading space of the 2nd+ entries (status can be
50
+ * stripped at a fixed width).
51
+ * This way it does not break even with paths containing spaces or multiple ` M` (worktree-only) changes.
52
+ *
53
+ * The format of each porcelain v1 -z entry:
54
+ * - `XY <path>\0` (X=index state, Y=worktree state; the 3rd char is always a space, the path from
55
+ * the 4th char). Untracked is `?? <path>\0`.
56
+ * - A rename/copy (R/C) comes as 2 entries: `XY <new>\0<old>\0` (new first, old second). Take the
57
+ * new side, which is the current path, and skip the immediately following old entry (a raw path with no status).
58
+ *
59
+ * Like the other safe helpers, a best-effort that swallows failures: when git fails (not a repo,
60
+ * etc.) it returns an empty array (dirty auto-compute is an auxiliary feature, so if it cannot be
61
+ * obtained, err toward emitting nothing).
62
+ */
63
+ export declare function workingTreeChangedFiles(worktreePath: string): string[];
64
+ /**
65
+ * A best-effort helper that lists committed diff files from the fork-point SHA (Issue #52 proposal 3).
66
+ *
67
+ * When baseSha is non-NULL, runs `git diff --name-only <baseSha>...HEAD` (3-dot = diff from the merge
68
+ * base) against worktreePath and returns the changed file names. This also lets "changes committed in
69
+ * the worktree but not yet in the working tree" be included in dirty (union with workingTreeChangedFiles).
70
+ *
71
+ * When baseSha is absent (an old row saved as NULL because FEAT-002 could not resolve it, an empty
72
+ * repo, etc.) it returns an empty array and falls back to the working-tree state only. Like the other
73
+ * safe helpers, it also returns an empty array on git failure.
74
+ */
75
+ export declare function committedChangedFiles(worktreePath: string, baseSha: string | null | undefined): string[];
76
+ /**
77
+ * Sanitization when embedding agent_id / ticket_id etc. into a worktree path or branch name.
78
+ * Folds characters problematic for the FS/ref such as slashes and whitespace into '-'.
79
+ */
80
+ export declare function safeSlug(s: string): string;
81
+ /**
82
+ * Shortening of ulid/agent_id/ticket_id (for deterministic branch names).
83
+ *
84
+ * A ULID is 26 chars = the first 10 are a millisecond timestamp and the remaining 16 are a random
85
+ * part. Using only the head would make the slugs of "2 IDs generated in the same millisecond"
86
+ * collide, so distinct things fall into the same deterministic name (worktree path / ticket
87
+ * integration branch name). To avoid this, add a few chars from the trailing random part to the head
88
+ * (which keeps chronological sort order) to ensure uniqueness. Inputs of 16 chars or fewer (agent_id,
89
+ * etc.) are not a collision source, so fold them as-is. Worktree assignment and the ticket
90
+ * integration branch name share the same function, and the root fix (head 8 + tail 6) applies to both.
91
+ */
92
+ export declare function short(s: string): string;
@@ -0,0 +1,183 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ /**
3
+ * A thin wrapper that runs a git command repo-scoped (`git -C <repo>`).
4
+ * Because of the multi-repo assumption (§3: tasks.repo is required), worktree assignment, the merge
5
+ * gate, and the 3-way precheck all run in this `git -C <repo>` scope.
6
+ *
7
+ * @returns the trimmed stdout string.
8
+ * @throws execFileSync throws on a non-zero exit (caught by the caller).
9
+ *
10
+ * HIGH-2 mitigation: expand maxBuffer from the default (1MB) to 64MB. For many-file changes,
11
+ * `diff --name-only` / `merge-tree --write-tree` / `show --name-only` throwing ENOBUFS past 1MB
12
+ * would (a) permanently stall the merge gate if via threeWayPrecheck, and (b) make gitConflictFiles'
13
+ * catch misparse the truncated stdout. With a sufficient buffer, this throw does not occur for
14
+ * realistic inputs.
15
+ */
16
+ export function git(repoPath, args) {
17
+ return execFileSync('git', ['-C', repoPath, ...args], {
18
+ encoding: 'utf8',
19
+ maxBuffer: 64 * 1024 * 1024,
20
+ }).trim();
21
+ }
22
+ /**
23
+ * An internal helper that runs a git command repo-scoped and returns stdout raw without trimming.
24
+ *
25
+ * Normally the trimming `git()` is enough, but when the output itself is meaningful — as with
26
+ * `git status --porcelain -z` (NUL-separated, a fixed-width 2-char status + space at the head, with
27
+ * a trailing NUL) — trimming would strip the leading space of a "worktree-only change ( M path)"
28
+ * line or the trailing NUL and shift parsing. Use this function only on paths that handle raw -z output.
29
+ *
30
+ * @returns the raw, untrimmed stdout string.
31
+ * @throws execFileSync throws on a non-zero exit (caught by the caller).
32
+ */
33
+ function gitRaw(repoPath, args) {
34
+ return execFileSync('git', ['-C', repoPath, ...args], {
35
+ encoding: 'utf8',
36
+ maxBuffer: 64 * 1024 * 1024,
37
+ });
38
+ }
39
+ /**
40
+ * Resolve the repo's main branch name (§4.6).
41
+ * Rather than hardcoding `main`, allow `dev`/`stg`/`master` etc. per repo.
42
+ *
43
+ * Phase 1 default policy: explicit value > the first of `dev`/`stg`/`main`/`master` that exists in
44
+ * the repo > `main`.
45
+ * (Leaves room to replace with a repo_config table in Phase 2, but for now it is inferred from
46
+ * existing branches.)
47
+ */
48
+ export function resolveBaseBranch(repoPath, explicit) {
49
+ if (explicit)
50
+ return explicit;
51
+ for (const cand of ['dev', 'stg', 'main', 'master']) {
52
+ try {
53
+ git(repoPath, ['show-ref', '--verify', '--quiet', `refs/heads/${cand}`]);
54
+ return cand;
55
+ }
56
+ catch {
57
+ /* to the next candidate */
58
+ }
59
+ }
60
+ // Fallback when none of the candidates exist. The head of the search order is `dev`, but returning
61
+ // a nonexistent `dev` would make subsequent git operations fail for sure, so keep `main` as the
62
+ // conventional name most likely to exist.
63
+ return 'main';
64
+ }
65
+ /**
66
+ * A best-effort helper that resolves a ref (branch name/tag/commit, etc.) to an existing commit SHA.
67
+ *
68
+ * Using `git rev-parse --verify <ref>^{commit}`, it verifies that "the ref exists and reaches a
69
+ * commit" while folding to a 40-char SHA. Used to record the fork-point SHA at worktree assignment
70
+ * (to later compute the diff from base `git diff --name-only <base_sha>...HEAD`).
71
+ *
72
+ * Like the existing resolveBaseBranch, a best-effort policy that swallows failures: when it cannot
73
+ * resolve (ref absent, empty repo, before the initial commit, etc.), it returns null instead of
74
+ * throwing. The caller is designed to save base_sha as NULL when null, and the diff side falls back
75
+ * to status only when base_sha is absent.
76
+ */
77
+ export function resolveRefSha(repoPath, ref) {
78
+ try {
79
+ return git(repoPath, ['rev-parse', '--verify', `${ref}^{commit}`]);
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ /**
86
+ * A best-effort helper that lists a worktree's working-tree changed files (Issue #52 proposal 2).
87
+ *
88
+ * Runs `git status --porcelain -z` against worktreePath (same style as classifyRemoveFailure = take
89
+ * git status against the worktree path) and extracts the changed file paths from each entry.
90
+ *
91
+ * Reasons for using `-z` (NUL-separated):
92
+ * - Each entry is terminated with NUL (\0) and the path is not quoted (normal `--porcelain` quotes
93
+ * and escapes paths containing spaces etc. with `"..."`, but -z emits them raw).
94
+ * - We do not trim the output, so there is no column shift from the leading space left in a
95
+ * "worktree-only change ( M path)" or the leading space of the 2nd+ entries (status can be
96
+ * stripped at a fixed width).
97
+ * This way it does not break even with paths containing spaces or multiple ` M` (worktree-only) changes.
98
+ *
99
+ * The format of each porcelain v1 -z entry:
100
+ * - `XY <path>\0` (X=index state, Y=worktree state; the 3rd char is always a space, the path from
101
+ * the 4th char). Untracked is `?? <path>\0`.
102
+ * - A rename/copy (R/C) comes as 2 entries: `XY <new>\0<old>\0` (new first, old second). Take the
103
+ * new side, which is the current path, and skip the immediately following old entry (a raw path with no status).
104
+ *
105
+ * Like the other safe helpers, a best-effort that swallows failures: when git fails (not a repo,
106
+ * etc.) it returns an empty array (dirty auto-compute is an auxiliary feature, so if it cannot be
107
+ * obtained, err toward emitting nothing).
108
+ */
109
+ export function workingTreeChangedFiles(worktreePath) {
110
+ let raw;
111
+ try {
112
+ raw = gitRaw(worktreePath, ['status', '--porcelain', '-z']);
113
+ }
114
+ catch {
115
+ return [];
116
+ }
117
+ // Split on NUL. The trailing NUL appends an empty last element, so skip empty elements.
118
+ const entries = raw.split('\0');
119
+ const out = [];
120
+ for (let i = 0; i < entries.length; i++) {
121
+ const entry = entries[i];
122
+ if (entry.length === 0)
123
+ continue;
124
+ // `XY <path>`: the first 2 chars are the XY status, the 3rd char is the separating space, the path from the 4th char.
125
+ const status = entry.slice(0, 2);
126
+ const path = entry.slice(3);
127
+ if (path.length > 0)
128
+ out.push(path);
129
+ // For a rename/copy (R/C), the old side follows as the next entry with no status. Skip it.
130
+ if (status[0] === 'R' || status[0] === 'C')
131
+ i++;
132
+ }
133
+ return out;
134
+ }
135
+ /**
136
+ * A best-effort helper that lists committed diff files from the fork-point SHA (Issue #52 proposal 3).
137
+ *
138
+ * When baseSha is non-NULL, runs `git diff --name-only <baseSha>...HEAD` (3-dot = diff from the merge
139
+ * base) against worktreePath and returns the changed file names. This also lets "changes committed in
140
+ * the worktree but not yet in the working tree" be included in dirty (union with workingTreeChangedFiles).
141
+ *
142
+ * When baseSha is absent (an old row saved as NULL because FEAT-002 could not resolve it, an empty
143
+ * repo, etc.) it returns an empty array and falls back to the working-tree state only. Like the other
144
+ * safe helpers, it also returns an empty array on git failure.
145
+ */
146
+ export function committedChangedFiles(worktreePath, baseSha) {
147
+ if (!baseSha)
148
+ return [];
149
+ let out;
150
+ try {
151
+ out = git(worktreePath, ['diff', '--name-only', `${baseSha}...HEAD`]);
152
+ }
153
+ catch {
154
+ return [];
155
+ }
156
+ return out.split('\n').filter((l) => l.length > 0);
157
+ }
158
+ /**
159
+ * Sanitization when embedding agent_id / ticket_id etc. into a worktree path or branch name.
160
+ * Folds characters problematic for the FS/ref such as slashes and whitespace into '-'.
161
+ */
162
+ export function safeSlug(s) {
163
+ return s.replace(/[^A-Za-z0-9._-]/g, '-');
164
+ }
165
+ /**
166
+ * Shortening of ulid/agent_id/ticket_id (for deterministic branch names).
167
+ *
168
+ * A ULID is 26 chars = the first 10 are a millisecond timestamp and the remaining 16 are a random
169
+ * part. Using only the head would make the slugs of "2 IDs generated in the same millisecond"
170
+ * collide, so distinct things fall into the same deterministic name (worktree path / ticket
171
+ * integration branch name). To avoid this, add a few chars from the trailing random part to the head
172
+ * (which keeps chronological sort order) to ensure uniqueness. Inputs of 16 chars or fewer (agent_id,
173
+ * etc.) are not a collision source, so fold them as-is. Worktree assignment and the ticket
174
+ * integration branch name share the same function, and the root fix (head 8 + tail 6) applies to both.
175
+ */
176
+ export function short(s) {
177
+ const slug = safeSlug(s);
178
+ if (slug.length <= 16)
179
+ return slug.slice(0, 10);
180
+ // Head 8 (time-derived, sortable) + tail 6 (random part, unique). A hyphen also keeps it readable.
181
+ return `${slug.slice(0, 8)}-${slug.slice(-6)}`;
182
+ }
183
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Path normalization (preprocessing for all functions).
3
+ * - Normalize to a repo-root-relative POSIX path (`/`-separated).
4
+ * - Strip `./`, resolve `..`, collapse consecutive slashes, strip the leading `/`.
5
+ * - Case-sensitive comparison (git basis; macOS's default FS is case-insensitive but git distinguishes).
6
+ * - Do not follow symlinks (compare by the link's own path) = stay a pure string normalization.
7
+ */
8
+ export declare function normalizePath(p: string): string;
9
+ /**
10
+ * Whether 2 glob sets "could point at the same file" (the advisory overlap warning §4.3).
11
+ * glob×glob intersection is generally non-trivial, so Phase 1 uses a conservative approximation
12
+ * (false positive = safe side):
13
+ * (a) overlap if there is a string-identical glob
14
+ * (b) one glob's literal prefix directory is contained in / contains the other (e.g. "src/**" and "src/auth/*.ts")
15
+ * (c) one glob matches the other's literal path
16
+ */
17
+ export declare function globsOverlap(globsA: string[], globsB: string[]): boolean;
18
+ /**
19
+ * Intersection of two file sets (dirty set vs dirty set §4.4 / 3-way precheck §4.6).
20
+ * Taken strictly by exact normalized-path match.
21
+ */
22
+ export declare function fileIntersect(filesA: string[], filesB: string[]): string[];
23
+ /**
24
+ * Whether a glob set matches any of a file set (the overlap of claim(glob) × dirty(file) §4.4).
25
+ */
26
+ export declare function globsMatchAny(globs: string[], files: string[]): boolean;
@@ -0,0 +1,125 @@
1
+ import { minimatch } from 'minimatch';
2
+ /**
3
+ * §4.0 shared helpers: glob / file-set intersection decisions.
4
+ *
5
+ * Shared by the advisory warning (§4.3), dirty-set overlap (§4.4), and 3-way precheck (§4.6).
6
+ * Design policy (the asymmetry is the point):
7
+ * - The advisory side (globsOverlap / globsMatchAny) errs on the safe side of "false positive over
8
+ * miss" (warn-never-block, so the cost of over-warning is small, while a miss directly leads to a
9
+ * late discovery of a conflict).
10
+ * - The merge gate's fileIntersect takes it strictly by exact normalized-path match (a false
11
+ * positive here would be an unnecessary needs_review send-back and would hurt P-1).
12
+ */
13
+ /** Shared minimatch options (§4.0: dot/nocomment/noext). */
14
+ const MM_OPTS = { dot: true, nocomment: true, noext: true };
15
+ /**
16
+ * Path normalization (preprocessing for all functions).
17
+ * - Normalize to a repo-root-relative POSIX path (`/`-separated).
18
+ * - Strip `./`, resolve `..`, collapse consecutive slashes, strip the leading `/`.
19
+ * - Case-sensitive comparison (git basis; macOS's default FS is case-insensitive but git distinguishes).
20
+ * - Do not follow symlinks (compare by the link's own path) = stay a pure string normalization.
21
+ */
22
+ export function normalizePath(p) {
23
+ // Backslash (Windows) → slash, collapse consecutive slashes
24
+ let s = p.replace(/\\/g, '/').replace(/\/+/g, '/');
25
+ // Strip the leading ./ and /
26
+ s = s.replace(/^\.?\//, '');
27
+ // Resolve ./ and ../ per segment
28
+ const out = [];
29
+ for (const seg of s.split('/')) {
30
+ if (seg === '' || seg === '.')
31
+ continue;
32
+ if (seg === '..') {
33
+ // Keep a .. that goes above the root (outside the relative path = kept as-is as a comparison string)
34
+ if (out.length > 0 && out[out.length - 1] !== '..')
35
+ out.pop();
36
+ else
37
+ out.push('..');
38
+ }
39
+ else {
40
+ out.push(seg);
41
+ }
42
+ }
43
+ return out.join('/');
44
+ }
45
+ function normalizeAll(items) {
46
+ return items.map(normalizePath);
47
+ }
48
+ /** If a glob has a literal directory prefix, return that prefix (null if none). */
49
+ function literalDirPrefix(glob) {
50
+ const segs = glob.split('/');
51
+ const literal = [];
52
+ for (const seg of segs) {
53
+ // Cut off at a segment containing a wildcard
54
+ if (/[*?[\]{}]/.test(seg))
55
+ break;
56
+ literal.push(seg);
57
+ }
58
+ if (literal.length === 0)
59
+ return null;
60
+ return literal.join('/');
61
+ }
62
+ /**
63
+ * Whether 2 glob sets "could point at the same file" (the advisory overlap warning §4.3).
64
+ * glob×glob intersection is generally non-trivial, so Phase 1 uses a conservative approximation
65
+ * (false positive = safe side):
66
+ * (a) overlap if there is a string-identical glob
67
+ * (b) one glob's literal prefix directory is contained in / contains the other (e.g. "src/**" and "src/auth/*.ts")
68
+ * (c) one glob matches the other's literal path
69
+ */
70
+ export function globsOverlap(globsA, globsB) {
71
+ const a = normalizeAll(globsA);
72
+ const b = normalizeAll(globsB);
73
+ for (const ga of a) {
74
+ for (const gb of b) {
75
+ // (a) exact match
76
+ if (ga === gb)
77
+ return true;
78
+ // (c) if one is a literal path, test it against the other glob
79
+ const gaLiteral = !/[*?[\]{}]/.test(ga);
80
+ const gbLiteral = !/[*?[\]{}]/.test(gb);
81
+ if (gbLiteral && minimatch(gb, ga, MM_OPTS))
82
+ return true;
83
+ if (gaLiteral && minimatch(ga, gb, MM_OPTS))
84
+ return true;
85
+ // (b) containment relationship of literal prefix directories
86
+ const pa = literalDirPrefix(ga);
87
+ const pb = literalDirPrefix(gb);
88
+ if (pa !== null && pb !== null) {
89
+ if (pa === pb)
90
+ return true;
91
+ if (pa.startsWith(pb + '/') || pb.startsWith(pa + '/'))
92
+ return true;
93
+ }
94
+ }
95
+ }
96
+ return false;
97
+ }
98
+ /**
99
+ * Intersection of two file sets (dirty set vs dirty set §4.4 / 3-way precheck §4.6).
100
+ * Taken strictly by exact normalized-path match.
101
+ */
102
+ export function fileIntersect(filesA, filesB) {
103
+ const setB = new Set(normalizeAll(filesB));
104
+ const out = new Set();
105
+ for (const f of normalizeAll(filesA)) {
106
+ if (setB.has(f))
107
+ out.add(f);
108
+ }
109
+ return [...out];
110
+ }
111
+ /**
112
+ * Whether a glob set matches any of a file set (the overlap of claim(glob) × dirty(file) §4.4).
113
+ */
114
+ export function globsMatchAny(globs, files) {
115
+ const gs = normalizeAll(globs);
116
+ const fs = normalizeAll(files);
117
+ for (const g of gs) {
118
+ for (const f of fs) {
119
+ if (minimatch(f, g, MM_OPTS))
120
+ return true;
121
+ }
122
+ }
123
+ return false;
124
+ }
125
+ //# sourceMappingURL=globs.js.map
@@ -0,0 +1,132 @@
1
+ import { type CoordDb } from '../db/index.js';
2
+ /**
3
+ * §4.6 Integration merge gate (serialization + 3-way precheck, requirement FR-1.6).
4
+ *
5
+ * Serializes the integration of worktree results back into the mainline through a single gate.
6
+ * Small tasks merge-and-discard immediately (1-C); larger work goes through the serial gate (1-B).
7
+ * Same "never let them happen at the same time" idea as the claim CAS serialization.
8
+ * - FR-1.6.1 = serial queue (one row at a time in ascending seq; no concurrent processing while merging)
9
+ * - FR-1.6.2 = 3-way precheck (files touched by mainline ∩ my dirty set) and the auto/review split
10
+ * - FR-1.6.3 = combined use of 1-C (immediate merge) and 1-B (serial gate)
11
+ *
12
+ * Strict repo scoping: merge-base/diff/merge all run as `git -C <repo>`. The mainline branch name is
13
+ * resolved via resolveBaseBranch(repo) (infers dev/stg/main/master from what actually exists), assuming multi-repo.
14
+ *
15
+ * Phase 1 only goes as far as "detect and send back" for conflicts. Automatic conflict resolution is
16
+ * not included (heavy and unstable).
17
+ */
18
+ export interface MergeQueueRow {
19
+ id: string;
20
+ seq: number;
21
+ agent_id: string;
22
+ worktree_path: string;
23
+ branch: string;
24
+ task_id: string | null;
25
+ repo: string;
26
+ state: 'queued' | 'merging' | 'merged' | 'needs_review';
27
+ precheck: string | null;
28
+ enqueued_at: number;
29
+ processed_at: number | null;
30
+ host_id: string;
31
+ origin: string;
32
+ /** Aggregation target branch (A-1). NULL = base (backward compatible). When aggregating onto an integration branch, its name. */
33
+ target_branch: string | null;
34
+ }
35
+ export interface MergeArgs {
36
+ agentId: string;
37
+ worktreePath: string;
38
+ branch: string;
39
+ repo: string;
40
+ taskId?: string | null;
41
+ /** Aggregation target branch (A-1). When omitted, resolveBaseBranch(repo). When aggregating onto an integration branch, its name. */
42
+ targetBranch?: string | null;
43
+ }
44
+ export interface EnqueueResult {
45
+ merge_id: string;
46
+ position: number;
47
+ }
48
+ export interface MergedEntry {
49
+ task_id: string | null;
50
+ branch: string;
51
+ path: '1-B';
52
+ }
53
+ export interface NeedsReviewEntry {
54
+ task_id: string | null;
55
+ branch: string;
56
+ contested: string[];
57
+ /** Error summary when the git merge actually failed (a precheck false-negative slipping through). */
58
+ error?: string;
59
+ }
60
+ export interface ProcessResult {
61
+ merged: MergedEntry[];
62
+ needs_review: NeedsReviewEntry[];
63
+ /** Peers notified of needs_review via A2A wake (kind='board') + board handoff. */
64
+ wake_targets: string[];
65
+ }
66
+ export type OnTaskDoneResult = {
67
+ ok: true;
68
+ merged: Array<{
69
+ task_id: string | null;
70
+ branch: string;
71
+ path: '1-B' | '1-C';
72
+ }>;
73
+ needs_review: NeedsReviewEntry[];
74
+ wake_targets: string[];
75
+ } | {
76
+ ok: false;
77
+ reason: 'no_active_worktree' | 'branch_is_base' | 'not_assignee' | 'lease_expired';
78
+ detail: string;
79
+ };
80
+ /**
81
+ * §4.6 enqueue_merge — push a completed worktree onto the serial merge gate (1-B).
82
+ * seq follows the same numbering discipline as board (MAX+1 inside BEGIN IMMEDIATE): monotonically
83
+ * increasing and unique.
84
+ */
85
+ export declare function enqueueMerge(cdb: CoordDb, item: MergeArgs): EnqueueResult;
86
+ /**
87
+ * §4.6 process_merge_queue — the serial merge gate (1-B).
88
+ * Processes one row at a time in ascending seq. If a row is already merging, does not process concurrently.
89
+ * If the 3-way precheck finds zero conflict candidates, auto-merge; if there are candidates, send it back
90
+ * as needs_review (no hard block — a human/the assignee resolves it in the worktree).
91
+ * Uses a while loop rather than tail recursion for the next item (avoids stack overflow on long queues).
92
+ */
93
+ export declare function processMergeQueue(cdb: CoordDb): ProcessResult;
94
+ /**
95
+ * §4.6 is_small_task — is it a "small task safe to merge immediately" (the 1-C decision)?
96
+ * Phase 1 default policy (conservative): only merge immediately when there is no divergence and zero conflict candidates.
97
+ * (a) no parent ticket (no ticket_id). With a ticket_id it is false regardless of task count
98
+ * (= never take 1-C; always aggregate onto the integration branch via 1-B and always return to base via a PR), and
99
+ * (b) base_branch has not advanced past my branch point (mainline unchanged = zero divergence)
100
+ * If neither holds, route it to 1-B. The real conflict decision is finalized in try_immediate_merge.
101
+ */
102
+ export declare function isSmallTask(cdb: CoordDb, item: MergeArgs): boolean;
103
+ /**
104
+ * §4.6 try_immediate_merge — 1-C merge-and-discard immediately.
105
+ * Runs the same 3-way precheck as the serial gate "on the spot for just one item". If there are zero
106
+ * conflict candidates, merge immediately, discard the worktree immediately, and set the task done. If
107
+ * there are candidates, return ok:false and demote to 1-B.
108
+ * @returns ok = merge success, wake_targets = peers woken by the unblock (empty when ok:false).
109
+ */
110
+ export declare function tryImmediateMerge(cdb: CoordDb, item: MergeArgs): {
111
+ ok: boolean;
112
+ wake_targets: string[];
113
+ };
114
+ /**
115
+ * §4.6 on_task_done — the entry point of the task_done tool (FR-1.6.3).
116
+ *
117
+ * Input validation (misuse prevention; stops accidents where git checkout/merge runs on the main working tree):
118
+ * - no_active_worktree if there is no active worktree assignment for (agent_id, worktree_path)
119
+ * - branch_is_base if branch equals the base branch (dev/stg/main/master)
120
+ * - when task_id is given, not_assignee if the caller is not the assignee (another agent owns it, or
121
+ * the worktree assignment is not tied to that task and is not eligible for rescue). If the
122
+ * lease-expiry rescue re-claim lost a race and the task is still open and unassigned, lease_expired
123
+ *
124
+ * After validation passes: a small task (zero divergence, zero conflict candidates) is merged immediately
125
+ * via try_immediate_merge (1-C). If there is divergence/conflict candidates, enqueue it on the serial gate
126
+ * via enqueue_merge and drive it with process_merge_queue (1-B). If the immediate merge hits candidates, it
127
+ * demotes to 1-B (fall-through). 1-B is always the safety net. On a successful merge, set the task done
128
+ * (finalizeMergedTask) and wake dependent downstream assignees.
129
+ */
130
+ export declare function onTaskDone(cdb: CoordDb, item: MergeArgs): OnTaskDoneResult;
131
+ /** The merge queue listing in needs_review / all states (for observation). */
132
+ export declare function listMergeQueue(cdb: CoordDb, state?: MergeQueueRow['state']): MergeQueueRow[];