@nickmeriano/task 0.5.0 → 0.7.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 (53) hide show
  1. package/README.md +104 -8
  2. package/dist/claim.d.ts +76 -0
  3. package/dist/claim.d.ts.map +1 -0
  4. package/dist/claim.js +237 -0
  5. package/dist/claim.js.map +1 -0
  6. package/dist/claim.test.d.ts +15 -0
  7. package/dist/claim.test.d.ts.map +1 -0
  8. package/dist/claim.test.js +185 -0
  9. package/dist/claim.test.js.map +1 -0
  10. package/dist/cli.js +304 -17
  11. package/dist/cli.js.map +1 -1
  12. package/dist/file-store.d.ts +113 -0
  13. package/dist/file-store.d.ts.map +1 -0
  14. package/dist/file-store.js +604 -0
  15. package/dist/file-store.js.map +1 -0
  16. package/dist/index.d.ts +7 -4
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +4 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/server.d.ts.map +1 -1
  21. package/dist/server.js +11 -7
  22. package/dist/server.js.map +1 -1
  23. package/dist/store.d.ts +55 -15
  24. package/dist/store.d.ts.map +1 -1
  25. package/dist/store.js +63 -29
  26. package/dist/store.js.map +1 -1
  27. package/dist/store.test.d.ts +12 -0
  28. package/dist/store.test.d.ts.map +1 -0
  29. package/dist/store.test.js +252 -0
  30. package/dist/store.test.js.map +1 -0
  31. package/dist/ticket-doc.d.ts +57 -0
  32. package/dist/ticket-doc.d.ts.map +1 -0
  33. package/dist/ticket-doc.js +197 -0
  34. package/dist/ticket-doc.js.map +1 -0
  35. package/dist/types.d.ts +22 -1
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/types.js.map +1 -1
  38. package/package.json +4 -4
  39. package/skill/SKILL.md +45 -8
  40. package/src/claim.test.ts +242 -0
  41. package/src/claim.ts +302 -0
  42. package/src/cli.ts +313 -20
  43. package/src/file-store.ts +693 -0
  44. package/src/index.ts +42 -4
  45. package/src/server.ts +15 -11
  46. package/src/store.test.ts +305 -0
  47. package/src/store.ts +99 -40
  48. package/src/ticket-doc.ts +226 -0
  49. package/src/types.ts +22 -1
  50. package/ui/dist/assets/index-CXW8uT5f.css +1 -0
  51. package/ui/dist/assets/{index-D_qmmh3D.js → index-oJzomUDL.js} +58 -58
  52. package/ui/dist/index.html +2 -2
  53. package/ui/dist/assets/index-Da14ye1f.css +0 -1
package/README.md CHANGED
@@ -3,10 +3,12 @@
3
3
  A task manager that lives in your repo.
4
4
 
5
5
  Tasks and their status are coupled to the code — so keep them next to it.
6
- State is a SQLite database in `.task/` at your project root: no accounts, no
7
- connectors, no drift between the board and the branch. Built for the way
8
- projects work now you *and* your AI agents share one backlog, and the board
9
- updates live while agents move tickets from another terminal.
6
+ State is plain text in `.task/` at your project root one markdown file per
7
+ ticket and per comment: no accounts, no connectors, no drift between the board
8
+ and the branch. Ticket changes show up as readable diffs in PRs, and two
9
+ branches editing different tickets merge cleanly. Built for the way projects
10
+ work now — you *and* your AI agents share one backlog, and the board updates
11
+ live while agents move tickets from another terminal.
10
12
 
11
13
  ```bash
12
14
  npx @nickmeriano/task init
@@ -46,10 +48,53 @@ task done 1 # → done
46
48
  task link 2 --blocked-by 1 # dependency, visible from both tasks
47
49
  task update 1 --pr https://github.com/you/repo/pull/42
48
50
  task comment 1 "shipped in #42" --author claude
51
+ task boards # every board in the repo, with prefixes
52
+ task archive --all # move finished tickets to .task/archive/
49
53
  task serve # opens the kanban + table UI, live
50
54
  task publish # same board, at a URL you can open on a phone
51
55
  ```
52
56
 
57
+ ## Claiming — so two workers never pick up the same ticket
58
+
59
+ When agents (or agents *and* you) work one backlog, picking up a ticket needs
60
+ a lock. `task claim` is that lock, built out of things git already guarantees:
61
+
62
+ ```bash
63
+ task claim TAS-21 # claim it: branch + status flip + push
64
+ task list --claimable # the queue: what a worker may pick up next
65
+ task claim --release TAS-21 # abandon a claim cleanly
66
+ ```
67
+
68
+ - **The claim is the work branch.** `task claim TAS-21` branches
69
+ `task/claim/tas-21` off origin's default branch, flips the ticket to
70
+ `in_progress` as the branch's first commit, and pushes. The branch existing
71
+ on origin *is* the claim — nothing side-band to clean up, because the branch
72
+ was needed anyway and dies at merge.
73
+ - **The namespace is yours.** `task/claim/` is only the default —
74
+ `"claimPrefix"` in `.task/config.json` moves the whole namespace, and it
75
+ lives in the committed config on purpose: every worker and every clone must
76
+ agree on what "claimed" looks like, or there is no lock. If your workers are
77
+ Claude Code cloud sessions, set `"claimPrefix": "claude/task/"` — that
78
+ runtime can push `claude/`-prefixed branches without extra ceremony. Pick
79
+ the value before the first claim; renaming later strands in-flight claims.
80
+ - **Atomic by construction.** The push only succeeds if the branch doesn't
81
+ exist yet (a compare-and-swap on the ref), so two concurrent claimers of the
82
+ same ticket get exactly one winner. Exit codes are the contract: `0` claimed,
83
+ `1` already claimed (pick the next ticket), `2` not claimable — not `todo`,
84
+ blocked, `--needs-human`, or a dirty working tree.
85
+ - **The lifecycle rides the branch.** Claim = `in_progress`; before the PR is
86
+ marked ready, `task done` + `task update --pr` on the branch; the merge lands
87
+ code and `done` together, and the branch auto-deletes. Between claim and
88
+ merge the default branch still says `todo` — the branch is the truth about
89
+ in-flight work, and `git ls-remote origin 'task/claim/*'` (or your
90
+ configured namespace) lists all of it.
91
+ - **Claim before you work — humans too.** The same verb covers manual work:
92
+ run `task claim <id>` before starting a ticket yourself and any scheduled
93
+ agent will skip it. `task list --claimable` shows `todo` tickets in position
94
+ order minus blocked, needs-human, and already-claimed — the top entry is
95
+ what an autonomous worker takes next, which makes column order your priority
96
+ queue.
97
+
53
98
  ## From anywhere
54
99
 
55
100
  `task serve` is localhost, which is the right answer right up until you're not
@@ -83,9 +128,55 @@ one committing.
83
128
  immediately; your tasks are untouched, because they were only ever read from
84
129
  `.task/`. Changing your mind about *visibility* doesn't need that at all —
85
130
  `task publish --private` is enough.
131
+ - **Any branch.** The board renders your default branch, but `?ref=<branch>`
132
+ renders any other — and the header grows a branch switcher when there's more
133
+ than one. That's PR review for board changes: see the board as the PR would
134
+ leave it, before merging. Locally none of this is needed — `task serve`
135
+ shows your working tree, so previewing a branch is `git checkout`.
136
+
137
+ Since the preview URL is predictable, a small workflow in *your* repo can
138
+ comment it on every PR that touches board state — the App itself stays
139
+ read-only and never writes to your repository:
140
+
141
+ ```yaml
142
+ # .github/workflows/board-preview.yml
143
+ name: Board preview
144
+ on:
145
+ pull_request:
146
+ paths: [".task/**", "**/.task/**"]
147
+ permissions:
148
+ pull-requests: write
149
+ jobs:
150
+ comment:
151
+ if: github.event.pull_request.head.repo.full_name == github.repository
152
+ runs-on: ubuntu-latest
153
+ steps:
154
+ - uses: actions/github-script@v7
155
+ with:
156
+ script: |
157
+ const branch = context.payload.pull_request.head.ref
158
+ const url = `https://task.nickmeriano.com/${context.repo.owner}/${context.repo.repo}?ref=${encodeURIComponent(branch)}`
159
+ const marker = "<!-- board-preview -->"
160
+ const body = `${marker}\n📋 [Preview the board at \`${branch}\`](${url})`
161
+ const comments = await github.paginate(github.rest.issues.listComments, { ...context.repo, issue_number: context.issue.number })
162
+ const existing = comments.find((c) => c.body?.startsWith(marker))
163
+ if (existing) await github.rest.issues.updateComment({ ...context.repo, comment_id: existing.id, body })
164
+ else await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body })
165
+ ```
86
166
 
87
167
  - Ids are `PREFIX-12` or just `12`; the prefix is the first three letters of
88
168
  the project name ("phone" → `PHO-1`), or whatever `task init --prefix` says.
169
+ - Ids route by prefix: a bare `12` means the nearest board, while `TAS-12`
170
+ reaches the TAS board from anywhere in the monorepo. An unknown or ambiguous
171
+ prefix fails loudly instead of guessing. `task boards` lists every board —
172
+ prefix, name, path, open count — with a `*` on the one commands target from
173
+ the current directory.
174
+ - `task archive <id>` moves a done/canceled ticket's directory to
175
+ `.task/archive/` — off the board and out of every hot path, so boards stay
176
+ fast as history accumulates. Archived tickets stay readable (`task show`,
177
+ `task list --archived`), keep their comments, and their numbers stay
178
+ reserved forever. `task archive --all` sweeps everything finished;
179
+ `task unarchive <id>` puts one back.
89
180
  - Statuses: `backlog` `todo` `in_progress` `done` `canceled`.
90
181
  - A task carries tags, at most one milestone, and a `--needs-human` flag for
91
182
  work an agent can't finish alone. `--tag a,b` matches either tag.
@@ -113,13 +204,18 @@ one committing.
113
204
  root and it serves every nested board from one server, with a board switcher
114
205
  in the header. Run it inside a package and you get just that board. Every
115
206
  other command stays scoped to the nearest `.task/`.
116
- - The database self-migrates when it's opened, so pulling a colleague's
117
- `.task/` and running any command brings it up to date in place.
207
+ - Boards from before 0.6 stored their state as a committed SQLite database;
208
+ those keep working as-is, and `task migrate` moves one onto text files
209
+ (the database stays on disk as an ignored backup).
118
210
 
119
211
  ## How it's built
120
212
 
121
- - **Zero dependencies.** Storage is Node's built-in `node:sqlite` (Node
122
- 22.13)nothing to compile, so `npx` starts in about a second.
213
+ - **Zero dependencies.** Storage is markdown files with a tiny frontmatter
214
+ blockreviewable in a PR, mergeable by git, editable by hand. Comments are
215
+ one file each (append-only merges cleanly), and the `blocks`/`blocked-by`
216
+ relation is stored on one side only, so its two views can't disagree.
217
+ Legacy boards read through Node's built-in `node:sqlite` (Node ≥ 22.13) —
218
+ nothing to compile, so `npx` starts in about a second.
123
219
  - **Realtime is a file watch.** `task serve` is one `node:http` process: the
124
220
  prebuilt UI, a JSON API, and a server-sent-events stream that pings whenever
125
221
  anything writes to `.task/` — this UI, another terminal, an agent mid-run.
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `task claim` — atomic ticket claiming, for humans and scheduled agents alike
3
+ * (NIC-7 / TAS-21). The claim *is* the ticket's work branch: one deterministic
4
+ * name per ticket, creating it on origin is claiming it, and git's atomic ref
5
+ * creation is the lock. Branch existence = claimed; the branch dies at merge,
6
+ * so there is no claim state to clean up.
7
+ *
8
+ * The CLI stays board-level and forge-agnostic — everything here is plain git
9
+ * against `origin`. PR/session awareness belongs to whatever drives the claim
10
+ * (the implement-task skill, a human).
11
+ */
12
+ import type { Store } from "./store.ts";
13
+ import type { ProjectConfig, Task } from "./types.ts";
14
+ /**
15
+ * The branch namespace claims live under when the board doesn't configure
16
+ * one. Deliberately vendor-neutral: repos whose workers are Claude Code cloud
17
+ * sessions set `"claimPrefix": "claude/task/"` in `.task/config.json` — the
18
+ * one namespace that runtime can push without extra ceremony — and any other
19
+ * convention is equally valid. The key is committed with the board so every
20
+ * worker and every clone agree on what "claimed" looks like; a lock two
21
+ * sides spell differently is no lock at all.
22
+ */
23
+ export declare const DEFAULT_CLAIM_PREFIX = "task/claim/";
24
+ /**
25
+ * The board's claim namespace: configured `claimPrefix` (a trailing slash is
26
+ * implied) or the default. Validated here because it becomes a git ref and an
27
+ * ls-remote glob — a malformed value must fail the claim, not corrupt it.
28
+ */
29
+ export declare function claimNamespace(config: ProjectConfig): string;
30
+ /** { prefix: "TAS" }, 21 → "task/claim/tas-21" (or under the configured namespace). */
31
+ export declare function claimBranch(config: ProjectConfig, number: number): string;
32
+ /**
33
+ * Why a claim was refused: "claimed" (someone holds the branch — exit 1, pick
34
+ * the next ticket) vs "invalid" (the ticket or the tree isn't claimable —
35
+ * exit 2, fix something).
36
+ */
37
+ export declare class ClaimError extends Error {
38
+ readonly kind: "claimed" | "invalid";
39
+ constructor(message: string, kind: "claimed" | "invalid");
40
+ }
41
+ /** Every claim branch that exists on origin right now — one network call. */
42
+ export declare function remoteClaims(cwd: string, namespace: string): Set<string>;
43
+ export interface ClaimResult {
44
+ task: Task;
45
+ branch: string;
46
+ base: string;
47
+ }
48
+ /**
49
+ * Claim `number`: validate, branch off origin's default branch, flip the
50
+ * ticket to in_progress as the branch's first commit, and push. The push
51
+ * carries `--force-with-lease=<branch>:` (empty expectation = "the ref must
52
+ * not exist"), so creating the remote branch is a compare-and-swap: two
53
+ * concurrent claimers of the same ticket, exactly one wins — and a branch
54
+ * someone pre-created without a claim commit can't be hijacked by a plain
55
+ * fast-forward. Leaves the winner checked out on the claim branch.
56
+ */
57
+ export declare function claim(store: Store, number: number): ClaimResult;
58
+ export interface ReleaseResult {
59
+ branch: string;
60
+ /** Whether a branch was actually there to delete, per side. */
61
+ remote: boolean;
62
+ local: boolean;
63
+ }
64
+ /**
65
+ * Abandon a claim cleanly: delete the branch on origin and locally. The status
66
+ * flip only ever existed as a commit on that branch, so deleting it *is* the
67
+ * revert — the default branch never saw in_progress.
68
+ */
69
+ export declare function release(store: Store, number: number): ReleaseResult;
70
+ /**
71
+ * The dispatcher's queue view: `todo` in position order, minus needs-human,
72
+ * minus blocked, minus tickets whose claim branch already exists on origin
73
+ * (one ls-remote for the whole namespace). The top entry is next up.
74
+ */
75
+ export declare function claimableTasks(store: Store): Task[];
76
+ //# sourceMappingURL=claim.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claim.d.ts","sourceRoot":"","sources":["../src/claim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAErD;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,gBAAgB,CAAA;AAKjD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAU5D;AAED,uFAAuF;AACvF,wBAAgB,WAAW,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEzE;AAED;;;;GAIG;AACH,qBAAa,UAAW,SAAQ,KAAK;IACnC,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,CAAA;gBACxB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,SAAS;CAIzD;AA6CD,6EAA6E;AAC7E,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAQxE;AA4BD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,IAAI,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;;;;;;GAQG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CAkF/D;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,+DAA+D;IAC/D,MAAM,EAAE,OAAO,CAAA;IACf,KAAK,EAAE,OAAO,CAAA;CACf;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CA6BnE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,CAOnD"}
package/dist/claim.js ADDED
@@ -0,0 +1,237 @@
1
+ /**
2
+ * `task claim` — atomic ticket claiming, for humans and scheduled agents alike
3
+ * (NIC-7 / TAS-21). The claim *is* the ticket's work branch: one deterministic
4
+ * name per ticket, creating it on origin is claiming it, and git's atomic ref
5
+ * creation is the lock. Branch existence = claimed; the branch dies at merge,
6
+ * so there is no claim state to clean up.
7
+ *
8
+ * The CLI stays board-level and forge-agnostic — everything here is plain git
9
+ * against `origin`. PR/session awareness belongs to whatever drives the claim
10
+ * (the implement-task skill, a human).
11
+ */
12
+ import { spawnSync } from "node:child_process";
13
+ import { join } from "node:path";
14
+ import { FileStore, TICKETS_DIR } from "./file-store.js";
15
+ /**
16
+ * The branch namespace claims live under when the board doesn't configure
17
+ * one. Deliberately vendor-neutral: repos whose workers are Claude Code cloud
18
+ * sessions set `"claimPrefix": "claude/task/"` in `.task/config.json` — the
19
+ * one namespace that runtime can push without extra ceremony — and any other
20
+ * convention is equally valid. The key is committed with the board so every
21
+ * worker and every clone agree on what "claimed" looks like; a lock two
22
+ * sides spell differently is no lock at all.
23
+ */
24
+ export const DEFAULT_CLAIM_PREFIX = "task/claim/";
25
+ /** Slash-terminated path segments of ref-safe characters. */
26
+ const CLAIM_PREFIX_SHAPE = /^([A-Za-z0-9._-]+\/)+$/;
27
+ /**
28
+ * The board's claim namespace: configured `claimPrefix` (a trailing slash is
29
+ * implied) or the default. Validated here because it becomes a git ref and an
30
+ * ls-remote glob — a malformed value must fail the claim, not corrupt it.
31
+ */
32
+ export function claimNamespace(config) {
33
+ const raw = config.claimPrefix ?? DEFAULT_CLAIM_PREFIX;
34
+ const prefix = raw.endsWith("/") ? raw : `${raw}/`;
35
+ if (!CLAIM_PREFIX_SHAPE.test(prefix) || prefix.includes("..") || /(^|\/)\./.test(prefix)) {
36
+ throw new ClaimError(`invalid claimPrefix in .task/config.json: ${JSON.stringify(raw)} — use slash-separated segments like "task/claim/" or "claude/task/"`, "invalid");
37
+ }
38
+ return prefix;
39
+ }
40
+ /** { prefix: "TAS" }, 21 → "task/claim/tas-21" (or under the configured namespace). */
41
+ export function claimBranch(config, number) {
42
+ return `${claimNamespace(config)}${config.prefix.toLowerCase()}-${number}`;
43
+ }
44
+ /**
45
+ * Why a claim was refused: "claimed" (someone holds the branch — exit 1, pick
46
+ * the next ticket) vs "invalid" (the ticket or the tree isn't claimable —
47
+ * exit 2, fix something).
48
+ */
49
+ export class ClaimError extends Error {
50
+ kind;
51
+ constructor(message, kind) {
52
+ super(message);
53
+ this.kind = kind;
54
+ }
55
+ }
56
+ function git(cwd, ...args) {
57
+ const result = spawnSync("git", args, { cwd, encoding: "utf8" });
58
+ if (result.error)
59
+ throw result.error;
60
+ return {
61
+ status: result.status ?? 1,
62
+ stdout: (result.stdout ?? "").trim(),
63
+ stderr: (result.stderr ?? "").trim(),
64
+ };
65
+ }
66
+ /** Run git and throw on failure — for the steps that have no soft outcome. */
67
+ function gitMust(cwd, ...args) {
68
+ const result = git(cwd, ...args);
69
+ if (result.status !== 0) {
70
+ throw new Error(`git ${args[0]} failed: ${result.stderr || result.stdout}`);
71
+ }
72
+ return result.stdout;
73
+ }
74
+ /**
75
+ * The base every claim branch starts from: origin's default branch. Resolved
76
+ * from `origin/HEAD` when the clone recorded it, with a main/master fallback
77
+ * for repos wired up by hand (`git remote add` + push never sets origin/HEAD).
78
+ */
79
+ function defaultBase(cwd) {
80
+ const head = git(cwd, "symbolic-ref", "--quiet", "refs/remotes/origin/HEAD");
81
+ if (head.status === 0)
82
+ return head.stdout.replace(/^refs\/remotes\//, "");
83
+ for (const name of ["main", "master"]) {
84
+ if (git(cwd, "show-ref", "--verify", "--quiet", `refs/remotes/origin/${name}`).status === 0) {
85
+ return `origin/${name}`;
86
+ }
87
+ }
88
+ throw new Error("couldn't resolve origin's default branch — origin/HEAD is unset and neither origin/main nor origin/master exists");
89
+ }
90
+ /** Every claim branch that exists on origin right now — one network call. */
91
+ export function remoteClaims(cwd, namespace) {
92
+ const out = gitMust(cwd, "ls-remote", "--heads", "origin", `${namespace}*`);
93
+ const names = new Set();
94
+ for (const line of out.split("\n")) {
95
+ const ref = line.split("\t")[1];
96
+ if (ref?.startsWith("refs/heads/"))
97
+ names.add(ref.slice("refs/heads/".length));
98
+ }
99
+ return names;
100
+ }
101
+ function remoteBranchExists(cwd, branch) {
102
+ return gitMust(cwd, "ls-remote", "--heads", "origin", branch) !== "";
103
+ }
104
+ /** Blockers still in the way — anything not done or canceled still blocks. */
105
+ function openBlockers(store, task) {
106
+ return task.blockedBy
107
+ .map((n) => store.get(n))
108
+ .filter((b) => b !== null && b.status !== "done" && b.status !== "canceled")
109
+ .map((b) => b.id);
110
+ }
111
+ /** Throws ClaimError("invalid") unless `task` is claimable right now. */
112
+ function assertClaimable(store, task, number) {
113
+ const id = store.displayId(number);
114
+ if (!task)
115
+ throw new ClaimError(`no such task: ${id}`, "invalid");
116
+ if (task.status !== "todo") {
117
+ throw new ClaimError(`${id} is ${task.status} — only todo tickets can be claimed`, "invalid");
118
+ }
119
+ if (task.needsHuman)
120
+ throw new ClaimError(`${id} needs a human — not claimable`, "invalid");
121
+ const blockers = openBlockers(store, task);
122
+ if (blockers.length) {
123
+ throw new ClaimError(`${id} is blocked by ${blockers.join(", ")} — not claimable`, "invalid");
124
+ }
125
+ }
126
+ /**
127
+ * Claim `number`: validate, branch off origin's default branch, flip the
128
+ * ticket to in_progress as the branch's first commit, and push. The push
129
+ * carries `--force-with-lease=<branch>:` (empty expectation = "the ref must
130
+ * not exist"), so creating the remote branch is a compare-and-swap: two
131
+ * concurrent claimers of the same ticket, exactly one wins — and a branch
132
+ * someone pre-created without a claim commit can't be hijacked by a plain
133
+ * fast-forward. Leaves the winner checked out on the claim branch.
134
+ */
135
+ export function claim(store, number) {
136
+ if (!(store instanceof FileStore)) {
137
+ throw new ClaimError("claiming needs a text-format board — run `task migrate` first", "invalid");
138
+ }
139
+ const cwd = store.root;
140
+ const branch = claimBranch(store.config, number);
141
+ const id = store.displayId(number);
142
+ // The local-branch check comes before ticket validation on purpose: on the
143
+ // claim branch itself the ticket reads in_progress, and "already claimed"
144
+ // (exit 1, move on) is the truthful answer there — not a validation failure.
145
+ const tree = git(cwd, "status", "--porcelain");
146
+ if (tree.status !== 0) {
147
+ throw new ClaimError(`not a git repository: ${cwd}`, "invalid");
148
+ }
149
+ if (git(cwd, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`).status === 0) {
150
+ throw new ClaimError(`${id} is already claimed — ${branch} exists locally (finish it, or \`task claim --release ${id}\`)`, "claimed");
151
+ }
152
+ assertClaimable(store, store.get(number), number);
153
+ if (tree.stdout !== "") {
154
+ throw new ClaimError("working tree is dirty — commit or stash before claiming, the claim switches branches", "invalid");
155
+ }
156
+ gitMust(cwd, "fetch", "--quiet", "origin");
157
+ if (remoteBranchExists(cwd, branch)) {
158
+ throw new ClaimError(`${id} is already claimed — ${branch} exists on origin`, "claimed");
159
+ }
160
+ const base = defaultBase(cwd);
161
+ // So a failed claim can put the checkout back exactly where it was —
162
+ // a branch name usually, a bare sha when HEAD was detached.
163
+ const previous = git(cwd, "symbolic-ref", "--quiet", "--short", "HEAD").stdout ||
164
+ gitMust(cwd, "rev-parse", "HEAD");
165
+ gitMust(cwd, "checkout", "--quiet", "-b", branch, base);
166
+ const undo = () => {
167
+ git(cwd, "checkout", "--quiet", previous);
168
+ git(cwd, "branch", "--quiet", "-D", branch);
169
+ };
170
+ try {
171
+ // Re-validate against the base branch: the pre-checkout validation read
172
+ // whatever happened to be checked out, this one reads the truth the claim
173
+ // will actually be built on.
174
+ assertClaimable(store, store.get(number), number);
175
+ store.update(number, { status: "in_progress" });
176
+ gitMust(cwd, "add", "--", join(store.taskDir, TICKETS_DIR, String(number)));
177
+ gitMust(cwd, "commit", "--quiet", "-m", `chore(board): claim ${id} → in_progress`);
178
+ }
179
+ catch (error) {
180
+ undo();
181
+ throw error;
182
+ }
183
+ const push = git(cwd, "push", "--quiet", "-u", "origin", branch, `--force-with-lease=refs/heads/${branch}:`);
184
+ if (push.status !== 0) {
185
+ undo();
186
+ if (remoteBranchExists(cwd, branch)) {
187
+ throw new ClaimError(`${id} is already claimed — ${branch} was just created on origin`, "claimed");
188
+ }
189
+ throw new ClaimError(`${id}: push of ${branch} was rejected (likely a concurrent claim) — ${push.stderr || "no detail from git"}`, "claimed");
190
+ }
191
+ return { task: store.get(number), branch, base };
192
+ }
193
+ /**
194
+ * Abandon a claim cleanly: delete the branch on origin and locally. The status
195
+ * flip only ever existed as a commit on that branch, so deleting it *is* the
196
+ * revert — the default branch never saw in_progress.
197
+ */
198
+ export function release(store, number) {
199
+ const cwd = store.root;
200
+ const branch = claimBranch(store.config, number);
201
+ const onBranch = git(cwd, "symbolic-ref", "--quiet", "--short", "HEAD").stdout === branch;
202
+ if (onBranch) {
203
+ const tree = gitMust(cwd, "status", "--porcelain");
204
+ if (tree !== "") {
205
+ throw new ClaimError(`working tree on ${branch} is dirty — commit elsewhere or discard before releasing`, "invalid");
206
+ }
207
+ // Step off the branch so it can be deleted: onto the local default branch
208
+ // when there is one, detached onto the remote base otherwise.
209
+ const base = defaultBase(cwd);
210
+ const local = base.replace(/^origin\//, "");
211
+ if (git(cwd, "show-ref", "--verify", "--quiet", `refs/heads/${local}`).status === 0) {
212
+ gitMust(cwd, "checkout", "--quiet", local);
213
+ }
214
+ else {
215
+ gitMust(cwd, "checkout", "--quiet", "--detach", base);
216
+ }
217
+ }
218
+ const local = git(cwd, "branch", "--quiet", "-D", branch).status === 0;
219
+ const remote = remoteBranchExists(cwd, branch);
220
+ if (remote)
221
+ gitMust(cwd, "push", "--quiet", "origin", "--delete", branch);
222
+ return { branch, remote, local };
223
+ }
224
+ /**
225
+ * The dispatcher's queue view: `todo` in position order, minus needs-human,
226
+ * minus blocked, minus tickets whose claim branch already exists on origin
227
+ * (one ls-remote for the whole namespace). The top entry is next up.
228
+ */
229
+ export function claimableTasks(store) {
230
+ const claimed = remoteClaims(store.root, claimNamespace(store.config));
231
+ return store
232
+ .list({ statuses: ["todo"] })
233
+ .filter((t) => !t.needsHuman)
234
+ .filter((t) => openBlockers(store, t).length === 0)
235
+ .filter((t) => !claimed.has(claimBranch(store.config, t.number)));
236
+ }
237
+ //# sourceMappingURL=claim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claim.js","sourceRoot":"","sources":["../src/claim.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The properties TAS-21 exists for, checked from the outside — against a real
3
+ * bare "origin" and real CLI subprocesses, because the whole point of claiming
4
+ * is what happens between two independent checkouts:
5
+ *
6
+ * 1. Two concurrent `task claim <same-id>`: exactly one exits 0, the other
7
+ * exits 1 — git's atomic ref creation is the lock.
8
+ * 2. Validation failures exit 2 before anything touches git.
9
+ * 3. `task list --claimable` is the queue: position order, minus blocked,
10
+ * needs-human, and already-claimed tickets.
11
+ * 4. `task claim --release` deletes the branch both sides and the ticket is
12
+ * claimable again.
13
+ */
14
+ export {};
15
+ //# sourceMappingURL=claim.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claim.test.d.ts","sourceRoot":"","sources":["../src/claim.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}