lockage 0.0.0-stage → 0.9.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 (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 +42 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kihara, Takuya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,356 @@
1
- # Temporary Holding Version
1
+ # lockage
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > 日本語版: [README_ja.md](README_ja.md)
4
+
5
+ An MCP server that lets multiple code-development LLMs (Kiro, Claude Code, and others) **collaborate on development centered on a local git repository**.
6
+
7
+ When you run several agents on the same repository at once, you normally hit problems: "two agents write the same file and break it," "nobody knows who is touching what," and "integration (merging) turns into chaos." lockage solves this by combining **physical isolation via git worktree** with **a lightweight coordination layer on top of SQLite**.
8
+
9
+ It is a tool aimed at collaboration on a single host (one personal machine).
10
+
11
+ > This document is for people getting started with lockage. For how to develop, build, and test lockage itself, see [`docs/development.md`](docs/development.md); for the operational details of publishing PRs (`lockage-publish`), see [`docs/publishing.md`](docs/publishing.md).
12
+
13
+ ---
14
+
15
+ ## What makes it distinctive
16
+
17
+ - **Two layers: physical isolation (worktree) + logical visibility (claim / dirty set)**
18
+ Each agent is physically separated by a git worktree, which makes it impossible for two agents to write the same file at the same time. On top of that sits an advisory coordination layer where agents declare "who is responsible for what." The coordination layer **only warns; it never blocks** (warn-never-block). No hard lock that could cause deadlock is used.
19
+
20
+ - **Atomic task claim (CAS)**
21
+ A task claim is serialized with SQLite's `BEGIN IMMEDIATE` + version CAS, mechanically preventing two agents from taking the same task (the later one receives `conflict` and is not blocked).
22
+
23
+ - **Integration merge gate**
24
+ The path that integrates worktree results is serialized through a single gate. **A task under a ticket, regardless of how many tasks there are (even just one), goes through the serial merge gate (1-B) and is aggregated into the ticket integration branch `lockage/ticket/<short-id>`.** Only a minor task with no parent ticket (a task not linked to a ticket) that has no divergence is merged straight into the base branch on the spot (1-C) (if there is divergence or a conflict candidate, it also goes through the serial gate 1-B). Conflict candidates are detected by a 3-way pre-check and **sent back without auto-resolving**, leaving the decision to a human or the assignee.
25
+
26
+ - **A2A wake eliminates polling**
27
+ Events such as another agent stepping into your area, a dependency task completing, or a message addressed to you — all of these can be awaited with a single `wait_for_unblock` long-poll. The agent no longer needs to busy-poll.
28
+
29
+ - **SQLite is the single source of truth for state**
30
+ The source of truth for state such as tasks, claims, worktrees, and the board is SQLite. The default location is `~/.lockage/lockage.sqlite` in the home directory (changeable via `LOCKAGE_DB`).
31
+
32
+ lockage is an MCP server that provides atomic claim, dependency DAG, ticket management, worktree physical isolation, advisory claim/dirty set, integration merge gate, board/A2A wake, and the blocked-interrupt lifecycle.
33
+
34
+ ---
35
+
36
+ ## Requirements
37
+
38
+ - **Node.js 22.5 or later** (SQLite uses Node's built-in `node:sqlite`, so no native build or extra DB is needed)
39
+ - **git** (used for worktree / merge operations)
40
+
41
+ ---
42
+
43
+ ## Setup
44
+
45
+ No clone or build is required. You only add a definition to your MCP client's configuration that launches the npm-published artifact with `npx` (for the concrete configuration, see "[Registering with an MCP client](#registering-with-an-mcp-client)" below).
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "lockage": { "command": "npx", "args": ["-y", "lockage"] }
51
+ }
52
+ }
53
+ ```
54
+
55
+ The first run may take a little time to fetch. The DB (ledger) is created at `~/.lockage/lockage.sqlite` by default, so no extra configuration is needed.
56
+
57
+ To get started you need just two things: **registering with an MCP client** and **distributing the coordination rules** (next section). Line these two up and multi-agent coordination starts working.
58
+
59
+ ### Registering with an MCP client
60
+
61
+ Add a server definition like the following to the MCP configuration of Kiro / Claude Code / Antigravity, and so on (follow each client's configuration file format. Antigravity uses `~/.gemini/config/mcp_config.json`, and you can also register with `agy mcp add`).
62
+
63
+ To Kiro's `mcp.json`, add the following definition that launches the npm-published artifact with `npx` (no clone or `npm run build` is needed; the first run may take a little time to fetch).
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "lockage": {
69
+ "command": "npx",
70
+ "args": ["-y", "lockage"],
71
+ "disabled": false
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ The DB (ledger) is stored at `~/.lockage/lockage.sqlite` by default (the parent directory is created automatically). The default is fine; only those who want to change the location add `"env": { "LOCKAGE_DB": "/absolute/path/to/your-workspace/.lockage/lockage.sqlite" }` to `lockage`.
78
+
79
+ When multiple agents coordinate, line everyone up so they **point at the same `LOCKAGE_DB`**. If you leave the default, everyone points at the same default ledger, so no extra configuration is needed. This becomes the shared coordination ledger.
80
+
81
+ For clients such as Claude Code / Antigravity where launching from the npm-published artifact has not been confirmed, clone and `npm run build`, then register with the traditional definition that points `command:node`, `args:["/absolute/path/to/lockage/dist/index.js"]`, and `env.LOCKAGE_DB` (for Antigravity, `agy mcp add --env LOCKAGE_DB=<absolute path to the ledger> lockage node <path to the lockage repository>/dist/index.js`).
82
+
83
+ Do not write `LOCKAGE_TOOLSETS` in an agent's configuration (only the default `core` is visible). Only when a human uses the maintenance/diagnostic tools, add `"LOCKAGE_TOOLSETS": "maintenance"` to the `env` of that MCP client.
84
+
85
+ ---
86
+
87
+ ## Distributing the coordination rules
88
+
89
+ Once both registering with an MCP client and distributing these coordination rules are in place, you can get started. After writing the registration in the previous section, go on to deliver the coordination rules to each agent.
90
+
91
+ **The main path is installation via a plugin / Power.** You can load the coordination rules and hooks on the client side without placing anything in the target repository.
92
+
93
+ - **Claude Code**: register this repository as a marketplace and install the lockage plugin (`plugins/claude`).
94
+ - **Antigravity**: install with `agy plugin install <local path where you cloned lockage>/plugins/antigravity`.
95
+ - **Kiro**: register `plugins/kiro` as a Power and place the hooks into the target's `.kiro/hooks/` (`npx -y lockage setup kiro`).
96
+
97
+ For the detailed procedure of each, see [`plugins/README.md`](plugins/README.md).
98
+
99
+ **Manual install for environments that cannot use a plugin / Power**: the canonical source of the coordination rules is [`plugins/shared/AGENTS.md`](plugins/shared/AGENTS.md). Copy it to the root of the target project as `AGENTS.md` and you can have the same coordination rules read (keep a single canonical source and do not transcribe the full text here, to avoid double maintenance).
100
+
101
+ ### Why distribution is needed
102
+
103
+ Merely registering this server in the MCP configuration does not tell an agent "when, which tool, in what order to call." Only once the instructions required for coordination (the coordination rules in `AGENTS.md` and the lockage-workflow skill) are delivered to each agent does coordination start running as the rules describe.
104
+
105
+ > 💡 There are only two prerequisites for coordinating multiple agents. **(a)** Everyone's MCP configuration points at **the same absolute `LOCKAGE_DB` path**, and **(b)** each agent names **its own unique `agent_id`** (since a shared `AGENTS.md` cannot write them out in advance, each agent decides its own at startup and confirms uniqueness on the board). Line these two up and coordination runs as the rules describe.
106
+
107
+ ### Mapping of the coordination flow to tools
108
+
109
+ This is the mapping of which lockage tool the coordination rules (`plugins/shared/AGENTS.md`) call in which situation.
110
+
111
+ | Flow | Tool to call |
112
+ |---|---|
113
+ | Task planning (including dependencies) | `create_task` (`depends_on`) / `add_dependency` |
114
+ | Find and take a task | `list_tasks(status:"open")` → `claim_task` |
115
+ | Get your own workspace | `assign_worktree` (work in the returned `worktree_path`/`branch`; if you cannot move there, request it via `post_board`) |
116
+ | Change pre-announcement / conflict detection | `publish_dirty` (watch `overlaps`) |
117
+ | Communication / handoff | `post_board` / `read_board` |
118
+ | Cannot complete; interrupt / resume | `post_board(kind:"question")` → `block_task` → (if the human says continue) `resume_task` / (if a different worker is named) `handoff_task` |
119
+ | Completion / integration | `task_done` (if `needs_review`, look at the returned `contested` to resolve → re-run `task_done`) |
120
+ | Publishing a PR (per ticket, agent-driven) | Look at `publisher_status` and launch `lockage-publish` to publish your own ticket (do not run `gh pr create` by hand). For the trigger timing, `--mode`, and operational notes, see [`docs/publishing.md`](docs/publishing.md) |
121
+ | Idle | `wait_for_unblock` |
122
+
123
+ ---
124
+
125
+ ## How to set priority
126
+
127
+ Tasks (`create_task`) and tickets (`open_ticket` / `update_ticket`) can specify a `priority`. For this value, which determines processing order, the following consistent rule is adopted.
128
+
129
+ - **Larger means higher priority.** `priority` is an integer, default `0`. A task/ticket with a larger value is selected earlier (`list_tasks` / `list_tickets` return results `ORDER BY priority DESC`).
130
+ - **Ties at the same priority break by `created_at` ascending** (older first). At the same priority, the one created earlier is processed first.
131
+ - **Negative values are allowed.** Since `0` is the default (normal), attach a negative value (e.g. `-10`) to things you explicitly want to defer. There is no upper or lower bound.
132
+
133
+ ### Why "larger means higher"
134
+
135
+ This tool assumes AI agents pass `priority` over MCP. "Bigger number = stronger (higher priority)" is the most intuitive and least error-prone, so this direction is adopted (with a "`0` is highest" scheme, every new task that defaults to `0` would come to the front, and raising the number would lower the priority — a counterintuitive result).
136
+
137
+ ### Recommended range (a guide)
138
+
139
+ This is not a strict partition, just a guide to align how values are assigned.
140
+
141
+ | Value | Meaning |
142
+ |---|---|
143
+ | `100` | Interrupt (highest; when you want to jump ahead of everything) |
144
+ | `10` | High |
145
+ | `0` | Normal (default) |
146
+ | `-10` | Low (explicitly deferred) |
147
+
148
+ ---
149
+
150
+ ## Provided tools (37)
151
+
152
+ The following are exposed as MCP tools. `agent_id` is the identifier each agent names (within the single-host trust boundary).
153
+
154
+ ### Tool sets (`LOCKAGE_TOOLSETS`)
155
+
156
+ By default only the `core` tools that agents use (28) are exposed. The other sets are enabled at server startup with the environment variable `LOCKAGE_TOOLSETS` (comma-separated). For a tool that belongs to a set, the role column in the table below begins with "Set: …".
157
+
158
+ | Set | How to enable | Tools |
159
+ |---|---|---|
160
+ | core | Always enabled | Those with no "Set" marked in the table below |
161
+ | publisher | Enabled on the server that `lockage-publish` launches itself (no configuration needed) | `acquire_publish_lock` `release_publish_lock` `clear_integration_branch` `close_ticket_on_merge` `release_ticket_worktrees` `release_ticket_claims` |
162
+ | maintenance | Write `"LOCKAGE_TOOLSETS": "maintenance"` in the `env` of the MCP client a human uses for maintenance/diagnosis | `reclaim_orphan_worktrees` `process_merge_queue` `list_merge_queue` |
163
+
164
+ The `publisher` set is for cleanup that, if an agent called it, could close a ticket or clear an integration branch name, so it is not shown to agents.
165
+
166
+ ### Task operations
167
+
168
+ | Tool | Role |
169
+ |---|---|
170
+ | `create_task` | Create a task (`ticket_id` is optional; set dependencies with `depends_on`. Cycles are rejected) |
171
+ | `add_dependency` | Set a dependency (blocks) between existing tasks (`from_task` blocks `to_task`. Cycles are rejected) |
172
+ | `list_tasks` | List tasks (with the `version` to pass to claim. Filter by `status`/`assignee`/`ticket_id`/`repo`. Unassigned is `status:"open"`. `ready_only` for only dependency-resolved ones. For `status:"blocked"` it attaches `blocked_age_sec`/`agent_silent_sec`/`last_question` as material for judging orphans (no auto-reclaim)) |
173
+ | `claim_task` | Atomically claim a task (CAS. A conflict returns `conflict` and does not block) |
174
+ | `release_task` | Release a task (`outcome="open"` to make it claimable again/`"abandoned"` to abandon) |
175
+ | `block_task` | Interrupt pending a human decision (`in_progress`→`blocked`). Keeps the assignee/worktree and is excluded from the lease sweep = held indefinitely. Leave the reason you are stuck first via `post_board(kind="question")` |
176
+ | `resume_task` | The same agent resumes when a human instructs "continue" (`blocked`→`in_progress`). Routing to another worker is handoff (only when a human explicitly says so) |
177
+ | `handoff_task` | Transfer a `blocked` task to another worker **as a whole worktree unit** (§4.7). In a single CAS transaction, reassign the task/its active worktree/the active claim whose `holder=from` and `task_id` match/the dirty_set of the transferred worktree from `from`→`to` (branch/path unchanged). Double handoff is prevented by a CAS on `assignee=from` and `blocked`. An `in_progress` task cannot be snatched (must be `blocked` first). Making it another worker's is only on explicit human instruction |
178
+ | `task_done` | Complete → to the integration merge gate (1-C immediate merge / 1-B serial gate). On merge success, `done` |
179
+ | `heartbeat` | Liveness report. Extends the lease of held tasks/claims. Also auto-computes dirty from the git state of the assigned worktree and returns anything outside the claim's globs as `unclaimed` (§52) |
180
+
181
+ ### Ticket operations (an aggregation unit for issues; model 3)
182
+
183
+ A ticket is a cross-cutting, long-lived, un-claimed aggregation unit for issues. Linked 1 ticket → N tasks via `tasks.ticket_id`.
184
+
185
+ | Tool | Role |
186
+ |---|---|
187
+ | `open_ticket` | Open a ticket (`repos` omitted/empty = cross-cutting over all. The body is FTS5-indexed) |
188
+ | `update_ticket` | Update a ticket (`version-CAS`. A conflict returns `conflict` and does not block) |
189
+ | `list_tickets` | List/search (descending `priority`, older first on ties. `status`/`repo`/`query` for FTS5 full-text search. `limit` defaults to 100. When `repo` is given, cross-cutting ones are included too) |
190
+ | `read_ticket` | Get one ticket (includes `integration_branch`/`ticket_ready_at`/`repos`. For the publisher) |
191
+ | `clear_integration_branch` | Set: publisher. Reset `integration_branch` to NULL (post-PR-merge cleanup. system column, version-invariant, idempotent) |
192
+ | `close_ticket_on_merge` | Set: publisher. Flip a ticket to `closed` after a PR merge (reconcile-only. Following the same idea as `clear_integration_branch`, it is separated from `update_ticket` (version-CAS) and leaves version invariant. Idempotent and one-way via a local CAS on `status != closed`) |
193
+ | `link_task_to_ticket` | Re-link a task to its parent ticket (`ticket_id=null` to unlink) |
194
+
195
+ ### File coordination (advisory)
196
+
197
+ | Tool | Role |
198
+ |---|---|
199
+ | `claim_files` | Declare your area with globs (overlaps warn only, do not block) |
200
+ | `release_claim` | Explicitly release a claim |
201
+ | `list_claims` | List currently valid claims |
202
+ | `publish_dirty` | Publish changed files and A2A-wake the overlapping party. In addition to explicit publication, lockage auto-computes dirty from the git state of the worktree (`git status` + committed diff from `base_sha`) and includes it in the union, returning anything outside the claim's globs as `unclaimed` (§52. Debounced per worktree at a minimum interval of 30 seconds) |
203
+ | `list_dirty` | List the dirty set (the union of explicit `publish_dirty` entries and the git-derived changes of the worktree auto-computed by `heartbeat` and so on) |
204
+
205
+ ### Context sharing (board)
206
+
207
+ | Tool | Role |
208
+ |---|---|
209
+ | `post_board` | Post to the shared board (specify `to_agent` for a directed handoff) |
210
+ | `read_board` | Read the board (incremental fetch with a `since` cursor. `unresolved_only:true` excludes resolved questions. Combine with `kind:"question"` to see only unresolved questions) |
211
+ | `resolve_question` | Mark a question resolved (a complement to auto-resolution. When a task is set done/abandoned, the linked unresolved questions are auto-resolved, so this is for explicitly closing a question with no `ref_task_id` or one a human only answered and wants to close. Idempotent) |
212
+
213
+ ### worktree
214
+
215
+ | Tool | Role |
216
+ |---|---|
217
+ | `assign_worktree` | Assign a git worktree for physical isolation (idempotent reuse) |
218
+ | `release_worktree` | Release a worktree (keeps the branch by default. On a remove failure it does not set released but keeps active and returns a reason) |
219
+ | `list_worktrees` | List valid worktree assignments (filter by `agent_id`, `ticket_id`). With `stale:true`, return only worktrees that remain active even though they are linked to a done task (= candidates whose release failed) (read-only) |
220
+ | `reclaim_orphan_worktrees` | Set: maintenance. Enumerate/reclaim orphaned worktrees (`dry_run` enumerates only) |
221
+ | `release_ticket_worktrees` | Set: publisher. Release the done-task worktrees under a ticket in bulk (called from reconcile's MERGED path) |
222
+ | `release_ticket_claims` | Set: publisher. Release in bulk the claims linked to done/abandoned tasks under a ticket (called from reconcile's MERGED path. A recovery path for leftovers) |
223
+
224
+ ### Merge gate
225
+
226
+ | Tool | Role |
227
+ |---|---|
228
+ | `process_merge_queue` | Set: maintenance. Drive the serial merge gate (normally `task_done` drives it automatically. For explicit driving) |
229
+ | `list_merge_queue` | Set: maintenance. List the merge queue (filter by `needs_review` and so on) |
230
+ | `publisher_status` | Observe from the board the counts and lists of unprocessed `ticket_ready` (awaiting PR), updates awaiting an existing PR, awaiting merge (`pr_opened` done but no `pr_merged` = awaiting reconcile) (`awaiting_merge_count`), the publisher's last-run trace (times of `pr_opened`/`pr_merged`), and each ticket (read-only. Does not launch publish). The three counts are separate observation axes and are not exclusive (in particular, awaiting-update and awaiting-merge can both show the same ticket, so do not simply add them) |
231
+
232
+ ### Publish lock (per-ticket advisory lock)
233
+
234
+ An advisory lock so that multiple `lockage-publish` processes launched at once do not double-process the same ticket. Following the same idea as the lease/TTL of `claims`, PK=`ticket_id`, so other tickets are not blocked (default TTL 300 seconds. Even if a crash leaves it unreleased, another process can steal it after TTL expiry). Normally `lockage-publish` acquires and releases it internally and automatically, so agents do not need to call it directly.
235
+
236
+ | Tool | Role |
237
+ |---|---|
238
+ | `acquire_publish_lock` | Set: publisher. Acquire the per-ticket publish advisory lock (TTL settable with `ttl_sec`. Default 300. On success `{ ok:true, holder }`; if a live lock exists `{ ok:false }`. PK=`ticket_id`, so other tickets are not blocked) |
239
+ | `release_publish_lock` | Set: publisher. Release the publish advisory lock (deletes only on a `holder` match = does not delete a lock that is not yours, stolen after TTL expiry) |
240
+
241
+ ### Waiting (A2A wake)
242
+
243
+ | Tool | Role |
244
+ |---|---|
245
+ | `wait_for_unblock` | Long-poll. Blocks until one of "dependency task ready / board addressed to you or to all / overlap / timeout," and returns the reason (`woke_by`) |
246
+
247
+ ---
248
+
249
+ ## A typical coordination flow
250
+
251
+ Everything starts from a ticket. You open what you want to do (an issue or feature) as a ticket, split it into multiple tasks, and agents process them.
252
+
253
+ 1. `open_ticket(body, repos)` opens a ticket (the unit of what you want to do).
254
+ 2. `create_task(ticket_id, depends_on …)` splits that ticket into one or more tasks (set dependencies with `depends_on`).
255
+ 3. Agents claim and process each task (see "Processing one task" below).
256
+ 4. A task under a ticket (even just one) does not get its `task_done` merged straight to the mainline; it is **aggregated into the ticket integration branch `lockage/ticket/<short-id>`** (no special treatment by task count). Only a minor task not linked to a ticket is merged straight into the base branch.
257
+ 5. When all tasks under the same ticket become `done`, `ticket_ready` is auto-issued (idempotent via CAS), and the publisher (`lockage-publish`) pushes the integration branch → opens a PR to the default branch.
258
+
259
+ The flow of one agent processing one task:
260
+
261
+ 1. `list_tasks(status:"open")` gets unassigned tasks and their `version`
262
+ 2. `claim_task(task_id, agent_id, expected_version)` claims it (if there is a conflict, move to another task)
263
+ 3. `assign_worktree(agent_id, repo_path, task_id)` gets your own dedicated worktree
264
+ 4. Work in that worktree, publishing files you are about to change with `publish_dirty` as you go (if you overlap with someone else, they notice)
265
+ 5. After finishing, commit in the worktree, then `task_done(agent_id, worktree_path, branch, repo, task_id)`
266
+ - A task under a ticket (even just one) → if there are no conflict candidates, aggregated into the ticket integration branch via the serial gate (1-B)
267
+ - A task not linked to a ticket → if there are no conflict candidates, merged straight into the base branch (1-C)
268
+ - A conflict candidate exists → sent back to `needs_review`. Look at the returned `needs_review[].contested` (files likely to conflict), resolve in the worktree, and re-run `task_done`
269
+ 6. When idle, wait for the next event (dependency resolved, communication, overlap) with `wait_for_unblock(agent_id)`
270
+
271
+ ---
272
+
273
+ ## Intended usage flow (user → agent → PR)
274
+
275
+ This shows the whole lifecycle of how a user uses lockage as a sequence. There are 5 actors:
276
+
277
+ - **User** (developer): the human who feeds in what they want (a ticket) and reviews/merges the PR.
278
+ - **Orchestrator**: the coordinator who wakes multiple workers. May be a human or a dedicated agent (for solo operation it may be absent, in which case the user wakes one worker directly).
279
+ - **Worker (agent)**: Claude Code / Kiro, etc. The one that actually writes code. Several run in parallel.
280
+ - **lockage**: the MCP server. With SQLite as the single source of truth for state, it provides claim / worktree / board / merge gate.
281
+ - **publisher**: `lockage-publish`. A 1-shot command that picks up `ticket_ready`, pushes the integration branch, and opens a PR. It is not resident; it **runs once only when an agent (or the coordinator) invokes it**. The main path is "the agent that set the last task of the same ticket to `task_done` checks `publisher_status` and launches it limited to `--ticket`," and the safety net is "each agent, at session end, looks at `publisher_status` and if anything is unprocessed launches a full sweep once."
282
+
283
+ ```mermaid
284
+ sequenceDiagram
285
+ actor User as User
286
+ participant Orch as Orchestrator<br/>(human / dedicated agent)
287
+ participant Agent as Worker<br/>(Claude Code / Kiro)
288
+ participant Lockage as lockage (MCP)
289
+ participant Git as git / worktree
290
+ participant Pub as publisher<br/>(lockage-publish / 1-shot)
291
+ actor Forge as GitHub / GitLab
292
+
293
+ Note over User,Lockage: ① Open — turn what you want into a ticket
294
+ User->>Lockage: open_ticket(body, repos)
295
+ User->>Lockage: create_task(ticket_id, depends_on …)
296
+
297
+ Note over Orch,Agent: ② Launch — wake a worker (for solo operation the user does it directly)
298
+ User->>Orch: launch a worker (assign an agent_id)
299
+ Orch->>Agent: launch in a worktree (have it read the coordination rules = AGENTS.md)
300
+
301
+ Note over Agent,Git: ③ Name & claim — fix your own agent_id and take a task
302
+ Agent->>Lockage: check for duplicates with read_board/list_claims → post_board(note "joined")
303
+ Agent->>Lockage: list_tasks(status:open) → claim_task(CAS)
304
+ Agent->>Lockage: assign_worktree(agent_id, repo, task)
305
+ Lockage->>Git: git worktree add (physical isolation)
306
+
307
+ Note over Agent,Lockage: ④ Work — write while making it visible via the coordination layer
308
+ Agent->>Lockage: claim_files(glob) / publish_dirty (wake the party if you overlap)
309
+ Agent->>Git: edit, commit in that worktree
310
+
311
+ Note over Agent,Git: ⑤ Complete — to the integration merge gate
312
+ Agent->>Lockage: task_done(worktree, branch, repo, task)
313
+ alt no conflict candidate
314
+ Lockage->>Git: under a ticket → 1-B merge into the integration branch / no ticket → 1-C immediate merge to base → done
315
+ else conflict candidate exists
316
+ Lockage-->>Agent: sent back to needs_review (3-way pre-check)
317
+ Agent->>Git: resolve in the worktree → re-run task_done
318
+ end
319
+ Note over Lockage: all tasks under the ticket done → auto-issue ticket_ready (idempotent CAS)
320
+
321
+ Note over Agent,Forge: ⑥ Publish — the agent drives the publisher (does not open PRs by hand)
322
+ Agent->>Lockage: after the last task_done, publisher_status (confirm pending_count>=1)
323
+ Agent->>Pub: lockage-publish --repo-path <abs> --ticket <id> (limited to your own ticket)
324
+ Pub->>Lockage: read_board(ticket_ready) / acquire_publish_lock (per ticket)
325
+ Pub->>Forge: push the integration branch → open a PR (gh / glab)
326
+ Pub->>Lockage: release_publish_lock → post_board(pr_opened)
327
+ Note over Agent,Pub: Safety net: at session end look at publisher_status; if pending_count>0, lockage-publish --repo-path <abs> (full sweep, default both); if awaiting_merge_count>0, --mode reconcile once. Concurrent launches are safe via the advisory lock (a ticket another process is handling is skipped=locked)
328
+
329
+ Note over User,Forge: ⑦ Review & merge — the human gate
330
+ User->>Forge: review the PR → merge
331
+ Agent->>Pub: lockage-publish (default both. Use --mode reconcile for a previous session's missed cleanup)
332
+ Pub->>Forge: check prState (MERGED)
333
+ Pub->>Git: delete the remote/local integration branch
334
+ Pub->>Lockage: clear_integration_branch → close_ticket_on_merge(status=closed) → post_board(pr_merged)
335
+
336
+ Note over Agent,Lockage: ⑧ Next — when idle, wait via long-poll
337
+ Agent->>Lockage: wait_for_unblock(agent_id)
338
+ Lockage-->>Agent: wake on dependency resolved / board addressed to you / overlap
339
+ ```
340
+
341
+ **Key points**:
342
+
343
+ - **The human touches only the two ends**: the entrance (`open_ticket` / `create_task`) and the exit (reviewing, merging the PR). The middle — claim → worktree → merge gate → publishing the PR — is run automatically by agents and the publisher.
344
+ - **Agents do not open PRs by hand**: run through `task_done` and, once all tasks are done, `ticket_ready` is raised automatically. Publishing a PR is not done by running `gh pr create` yourself; instead, **the agent that set the last task to done looks at `publisher_status` and launches `lockage-publish --ticket` once** to drive it (as a safety net, if anything is unprocessed at session end, a full sweep). No resident polling is needed, and even if multiple agents launch at once, the per-ticket advisory lock prevents double processing (a ticket being handled is `skipped`=`locked`).
345
+ - **Merging the PR is the human gate**: the integration branch is created automatically, but reflecting it into the mainline (merging the PR) goes through the user's approval (branch protection + merge rights).
346
+ - **Solo operation**: if no orchestrator is needed, the user just wakes one worker in any MCP client and the same flow runs.
347
+
348
+ Publishing a PR is handled per ticket by `lockage-publish` (a separate bin). It is a 1-shot command that picks up `ticket_ready` (all tasks under a ticket already aggregated into the integration branch), pushes the integration branch, and opens a PR to the default branch. For the command's `--mode`, putting it on PATH, the timing at which agents invoke it, and operational notes, see [`docs/publishing.md`](docs/publishing.md).
349
+
350
+ ---
351
+
352
+ ## License
353
+
354
+ MIT License
355
+
356
+ Existing projects referenced were used only for design ideas (patterns); no code was reused.
@@ -0,0 +1,52 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ export interface OpenOptions {
3
+ /** DB file path. ':memory:' for in-memory (for tests). */
4
+ path: string;
5
+ /** This host's identifier. Defaults to os.hostname(). Phase 1 is pinned to the own host. */
6
+ hostId?: string;
7
+ }
8
+ export interface CoordDb {
9
+ readonly db: DatabaseSync;
10
+ readonly hostId: string;
11
+ close(): void;
12
+ }
13
+ /**
14
+ * Open SQLite, issue PRAGMAs, and apply the schema idempotently.
15
+ * - journal_mode=WAL: concurrent read/write (but WAL is unavailable for :memory:, so skip)
16
+ * - foreign_keys=ON: enable FK constraints
17
+ * - busy_timeout=5000: wait up to 5s on lock contention (both BEGIN IMMEDIATE and the following PRAGMAs). Set it first
18
+ */
19
+ export declare function openDb(opts: OpenOptions): CoordDb;
20
+ /**
21
+ * #70: in step with renaming lockage's work unit from issue to ticket, move the existing DB's
22
+ * tables, columns, and board kinds to the new names (idempotent). Does nothing if issues is absent
23
+ * (new DB) or tickets already exists.
24
+ *
25
+ * FTS5's external-content setting (content='issues') does not follow a table rename, so drop
26
+ * issues_fts and its triggers, re-create it as tickets_fts via schema.sql, and rebuild the index.
27
+ * Also drop the old indexes (they would remain rewritten to point at the new column names after the
28
+ * rename and duplicate the new-named ones that schema.sql creates).
29
+ *
30
+ * The rename, applying schema.sql, and rebuilding the index are committed in a single transaction.
31
+ * If it stops midway, the next startup sees tickets so migration does not run, leaving an empty
32
+ * tickets_fts. Updating a pre-migration ticket then would make FTS5's delete look for a row absent
33
+ * from the index and raise SQLITE_CORRUPT_VTAB.
34
+ *
35
+ * @param schema the contents of schema.sql (read by the caller).
36
+ * @returns true if a rename was performed.
37
+ */
38
+ export declare function migrateIssuesToTickets(db: DatabaseSync, schema: string): boolean;
39
+ /**
40
+ * Backward-compatible migration for an existing DB (idempotent).
41
+ * - worktree_assignments.repo_path: the FS root of the repo that merge uses for `git -C`. Absent in
42
+ * an old DB, so ADD COLUMN (existing rows stay NULL = fall back to the argument at merge time).
43
+ * - worktree_assignments.base_sha: the fork-point commit SHA, to compute the diff from base later.
44
+ * Absent in an old DB, so ADD COLUMN (existing rows stay NULL = the diff side falls back to status only).
45
+ * - Per-ticket PR (A-1〜A-3): ADD COLUMN for tickets.integration_branch / tickets.ticket_ready_at /
46
+ * merge_queue.target_branch / board_messages.ref_ticket_id (all nullable).
47
+ * - Remove the CHECK (note|handoff|question) on board_messages.kind (option 3). SQLite requires a
48
+ * table rebuild to change a CHECK, so if the old CHECK remains in the DDL string, copy to _new and swap.
49
+ */
50
+ export declare function migrate(db: DatabaseSync): void;
51
+ /** unix seconds (integer). All tables' *_at are unified as unix seconds. */
52
+ export declare function unixNow(): number;