@nickmeriano/task 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -22
- package/dist/asks.test.d.ts +17 -0
- package/dist/asks.test.d.ts.map +1 -0
- package/dist/asks.test.js +278 -0
- package/dist/asks.test.js.map +1 -0
- package/dist/check.d.ts +3 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +99 -60
- package/dist/check.js.map +1 -1
- package/dist/check.test.js +99 -60
- package/dist/check.test.js.map +1 -1
- package/dist/claim-io.d.ts +73 -0
- package/dist/claim-io.d.ts.map +1 -0
- package/dist/claim-io.js +344 -0
- package/dist/claim-io.js.map +1 -0
- package/dist/claim.d.ts +108 -23
- package/dist/claim.d.ts.map +1 -1
- package/dist/claim.js +320 -86
- package/dist/claim.js.map +1 -1
- package/dist/claim.test.d.ts +10 -2
- package/dist/claim.test.d.ts.map +1 -1
- package/dist/claim.test.js +282 -96
- package/dist/claim.test.js.map +1 -1
- package/dist/cli.js +346 -68
- package/dist/cli.js.map +1 -1
- package/dist/file-store.d.ts +70 -24
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +319 -141
- package/dist/file-store.js.map +1 -1
- package/dist/git-serve.d.ts +183 -0
- package/dist/git-serve.d.ts.map +1 -0
- package/dist/git-serve.js +503 -0
- package/dist/git-serve.js.map +1 -0
- package/dist/git-serve.test.d.ts +16 -0
- package/dist/git-serve.test.d.ts.map +1 -0
- package/dist/git-serve.test.js +183 -0
- package/dist/git-serve.test.js.map +1 -0
- package/dist/git.d.ts +65 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +114 -0
- package/dist/git.js.map +1 -0
- package/dist/id.d.ts +39 -0
- package/dist/id.d.ts.map +1 -0
- package/dist/id.js +67 -0
- package/dist/id.js.map +1 -0
- package/dist/inbox.d.ts +41 -0
- package/dist/inbox.d.ts.map +1 -0
- package/dist/inbox.js +56 -0
- package/dist/inbox.js.map +1 -0
- package/dist/overview.d.ts +9 -0
- package/dist/overview.d.ts.map +1 -1
- package/dist/overview.js +19 -11
- package/dist/overview.js.map +1 -1
- package/dist/overview.test.js +6 -5
- package/dist/overview.test.js.map +1 -1
- package/dist/promote.test.d.ts +3 -2
- package/dist/promote.test.d.ts.map +1 -1
- package/dist/promote.test.js +29 -31
- package/dist/promote.test.js.map +1 -1
- package/dist/publish.d.ts +2 -17
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +4 -49
- package/dist/publish.js.map +1 -1
- package/dist/search.d.ts +3 -1
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +15 -2
- package/dist/search.js.map +1 -1
- package/dist/search.test.js +9 -9
- package/dist/search.test.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +218 -21
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +28 -14
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js.map +1 -1
- package/dist/store.test.js +88 -75
- package/dist/store.test.js.map +1 -1
- package/dist/ticket-doc.d.ts +26 -10
- package/dist/ticket-doc.d.ts.map +1 -1
- package/dist/ticket-doc.js +71 -27
- package/dist/ticket-doc.js.map +1 -1
- package/dist/types.d.ts +51 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +96 -24
- package/src/asks.test.ts +355 -0
- package/src/check.test.ts +129 -72
- package/src/check.ts +101 -58
- package/src/claim-io.ts +401 -0
- package/src/claim.test.ts +372 -101
- package/src/claim.ts +418 -99
- package/src/cli.ts +370 -67
- package/src/file-store.ts +357 -145
- package/src/git-serve.test.ts +240 -0
- package/src/git-serve.ts +595 -0
- package/src/git.ts +141 -0
- package/src/id.ts +68 -0
- package/src/inbox.ts +77 -0
- package/src/overview.test.ts +6 -5
- package/src/overview.ts +25 -10
- package/src/promote.test.ts +40 -28
- package/src/publish.ts +6 -53
- package/src/search.test.ts +11 -11
- package/src/search.ts +17 -3
- package/src/server.ts +221 -19
- package/src/store.test.ts +95 -77
- package/src/store.ts +28 -13
- package/src/ticket-doc.ts +93 -30
- package/src/types.ts +52 -17
- package/ui/dist/assets/index-BjsorZOU.js +229 -0
- package/ui/dist/assets/index-CoKCUYic.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-BJmOsOdR.js +0 -229
- package/ui/dist/assets/index-BoqQlqSU.css +0 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The git surface's contract (TAS-42), checked over real HTTP against real
|
|
3
|
+
* repositories — the properties that must hold before a board UI is allowed
|
|
4
|
+
* to run git:
|
|
5
|
+
*
|
|
6
|
+
* 1. Board commits are pathspec-scoped: with board AND non-board files dirty,
|
|
7
|
+
* committing from the API commits exactly the board files.
|
|
8
|
+
* 2. Checkout is refused while tracked non-board files are dirty — no
|
|
9
|
+
* stashing magic, the refusal says why.
|
|
10
|
+
* 3. The CSRF gate: a cross-origin browser write is 403 before any handler
|
|
11
|
+
* runs; the board's own origin and origin-less clients pass.
|
|
12
|
+
* 4. Degradation: a repo with no origin advertises no `git` capability and
|
|
13
|
+
* 404s the whole route group — the self-hiding rule the UI builds on.
|
|
14
|
+
*/
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
import { after, test } from "node:test";
|
|
17
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { spawnSync } from "node:child_process";
|
|
21
|
+
import { initProject } from "./file-store.js";
|
|
22
|
+
import { compareUrl } from "./git-serve.js";
|
|
23
|
+
import { createTaskServer } from "./server.js";
|
|
24
|
+
function tempDir() {
|
|
25
|
+
const dir = mkdtempSync(join(tmpdir(), "task-git-serve-test-"));
|
|
26
|
+
process.on("exit", () => rmSync(dir, { recursive: true, force: true }));
|
|
27
|
+
return dir;
|
|
28
|
+
}
|
|
29
|
+
function sh(cwd, command, ...args) {
|
|
30
|
+
const result = spawnSync(command, args, { cwd, encoding: "utf8" });
|
|
31
|
+
if (result.status !== 0) {
|
|
32
|
+
throw new Error(`${command} ${args.join(" ")} failed: ${result.stderr}`);
|
|
33
|
+
}
|
|
34
|
+
return result.stdout.trim();
|
|
35
|
+
}
|
|
36
|
+
/** A clone with a board, its origin bare — the shape `task serve` runs in. */
|
|
37
|
+
function repoWithOrigin() {
|
|
38
|
+
const home = tempDir();
|
|
39
|
+
const bare = join(home, "origin.git");
|
|
40
|
+
sh(home, "git", "init", "--quiet", "--bare", "-b", "main", bare);
|
|
41
|
+
const clone = join(home, "clone");
|
|
42
|
+
sh(home, "git", "clone", "--quiet", bare, clone);
|
|
43
|
+
sh(clone, "git", "config", "user.name", "Test");
|
|
44
|
+
sh(clone, "git", "config", "user.email", "test@example.com");
|
|
45
|
+
const store = initProject(clone, { name: "git board", prefix: "GIT" });
|
|
46
|
+
const first = store.create({ title: "First", status: "todo" }).key;
|
|
47
|
+
writeFileSync(join(clone, "code.txt"), "original\n");
|
|
48
|
+
sh(clone, "git", "add", "-A");
|
|
49
|
+
sh(clone, "git", "commit", "--quiet", "-m", "board");
|
|
50
|
+
sh(clone, "git", "push", "--quiet", "-u", "origin", "main");
|
|
51
|
+
return { clone, first };
|
|
52
|
+
}
|
|
53
|
+
const servers = [];
|
|
54
|
+
after(() => {
|
|
55
|
+
for (const server of servers)
|
|
56
|
+
server.close();
|
|
57
|
+
});
|
|
58
|
+
async function serve(root) {
|
|
59
|
+
const server = createTaskServer(root);
|
|
60
|
+
servers.push(server);
|
|
61
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
62
|
+
const { port } = server.address();
|
|
63
|
+
return `http://127.0.0.1:${port}`;
|
|
64
|
+
}
|
|
65
|
+
async function call(base, path, init) {
|
|
66
|
+
const res = await fetch(`${base}${path}`, init);
|
|
67
|
+
return { status: res.status, body: (await res.json()) };
|
|
68
|
+
}
|
|
69
|
+
function post(body, headers = {}) {
|
|
70
|
+
return {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
73
|
+
body: JSON.stringify(body),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
test("board commits are pathspec-scoped: mixed dirty tree, only board files land", async () => {
|
|
77
|
+
const { clone: repo, first } = repoWithOrigin();
|
|
78
|
+
const base = await serve(repo);
|
|
79
|
+
// Board dirt: edit a ticket and add a brand-new (untracked) one. Non-board
|
|
80
|
+
// dirt: a tracked file edited and an untracked scratch file.
|
|
81
|
+
const status = await call(base, "/api/git/status");
|
|
82
|
+
assert.equal(status.status, 200);
|
|
83
|
+
const patch = await call(base, `/api/tasks/${first}`, Object.assign(post({ status: "in_progress" }), { method: "PATCH" }));
|
|
84
|
+
assert.equal(patch.status, 200);
|
|
85
|
+
const created = await call(base, "/api/tasks", post({ title: "Second" }));
|
|
86
|
+
assert.equal(created.status, 201);
|
|
87
|
+
const second = created.body.task.key;
|
|
88
|
+
writeFileSync(join(repo, "code.txt"), "changed\n");
|
|
89
|
+
writeFileSync(join(repo, "scratch.txt"), "notes\n");
|
|
90
|
+
const before = await call(base, "/api/git/status");
|
|
91
|
+
const dirty = before.body.dirty;
|
|
92
|
+
assert.ok(dirty.length >= 2, JSON.stringify(before.body));
|
|
93
|
+
assert.ok(dirty.every((f) => f.path.startsWith(".task/")));
|
|
94
|
+
assert.deepEqual([...new Set(dirty.map((f) => f.ticket))].sort(), [first, second].sort());
|
|
95
|
+
assert.equal(before.body.otherDirty, 1); // code.txt — tracked; scratch.txt is untracked noise
|
|
96
|
+
const commit = await call(base, "/api/git/commit", post({ message: "chore(board): test" }));
|
|
97
|
+
assert.equal(commit.status, 200, JSON.stringify(commit.body));
|
|
98
|
+
// The commit holds board files only; the tree keeps its non-board dirt.
|
|
99
|
+
const committed = sh(repo, "git", "show", "--name-only", "--format=", "HEAD")
|
|
100
|
+
.split("\n")
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
assert.ok(committed.length >= 2);
|
|
103
|
+
assert.ok(committed.every((f) => f.startsWith(".task/")), committed.join(","));
|
|
104
|
+
const remaining = sh(repo, "git", "status", "--porcelain");
|
|
105
|
+
assert.ok(remaining.includes("code.txt"));
|
|
106
|
+
assert.ok(remaining.includes("scratch.txt"));
|
|
107
|
+
assert.ok(!remaining.includes(".task/"));
|
|
108
|
+
const after = await call(base, "/api/git/status");
|
|
109
|
+
assert.deepEqual(after.body.dirty, []);
|
|
110
|
+
assert.equal(after.body.ahead, 1); // committed, not pushed
|
|
111
|
+
// Nothing left to commit → clean refusal, not an empty commit.
|
|
112
|
+
const again = await call(base, "/api/git/commit", post({ message: "chore(board): empty" }));
|
|
113
|
+
assert.equal(again.status, 400);
|
|
114
|
+
assert.match(String(again.body.error), /nothing to commit/);
|
|
115
|
+
});
|
|
116
|
+
test("commit with newBranch cuts the branch first; checkout back is refused while code is dirty", async () => {
|
|
117
|
+
const { clone: repo, first } = repoWithOrigin();
|
|
118
|
+
const base = await serve(repo);
|
|
119
|
+
const patch = await call(base, `/api/tasks/${first}`, Object.assign(post({ tags: ["x"] }), { method: "PATCH" }));
|
|
120
|
+
assert.equal(patch.status, 200);
|
|
121
|
+
writeFileSync(join(repo, "code.txt"), "work in flight\n");
|
|
122
|
+
const commit = await call(base, "/api/git/commit", post({ message: "chore(board): branch it", newBranch: "board/test-1", base: "main" }));
|
|
123
|
+
assert.equal(commit.status, 200, JSON.stringify(commit.body));
|
|
124
|
+
assert.equal(commit.body.branch, "board/test-1");
|
|
125
|
+
assert.equal(sh(repo, "git", "branch", "--show-current"), "board/test-1");
|
|
126
|
+
// code.txt (tracked, dirty, outside every board dir) blocks the switch.
|
|
127
|
+
const refused = await call(base, "/api/git/checkout", post({ branch: "main" }));
|
|
128
|
+
assert.equal(refused.status, 409, JSON.stringify(refused.body));
|
|
129
|
+
assert.match(String(refused.body.error), /code\.txt/);
|
|
130
|
+
assert.equal(sh(repo, "git", "branch", "--show-current"), "board/test-1");
|
|
131
|
+
// Clean the code file and the same switch goes through.
|
|
132
|
+
sh(repo, "git", "checkout", "--", "code.txt");
|
|
133
|
+
const ok = await call(base, "/api/git/checkout", post({ branch: "main" }));
|
|
134
|
+
assert.equal(ok.status, 200, JSON.stringify(ok.body));
|
|
135
|
+
assert.equal(sh(repo, "git", "branch", "--show-current"), "main");
|
|
136
|
+
});
|
|
137
|
+
test("cross-origin writes are refused before any handler runs", async () => {
|
|
138
|
+
const { clone: repo } = repoWithOrigin();
|
|
139
|
+
const base = await serve(repo);
|
|
140
|
+
// A drive-by page POSTing at localhost: Origin names the attacker.
|
|
141
|
+
const evil = await call(base, "/api/git/commit", post({ message: "pwn" }, { Origin: "http://evil.example" }));
|
|
142
|
+
assert.equal(evil.status, 403);
|
|
143
|
+
// The board's own pages pass — Origin matches the Host it was served from.
|
|
144
|
+
const own = await call(base, "/api/tasks", post({ title: "From the board" }, { Origin: base }));
|
|
145
|
+
assert.equal(own.status, 201);
|
|
146
|
+
// And so do origin-less clients (curl, scripts) — the local-trust case.
|
|
147
|
+
const cli = await call(base, "/api/tasks", post({ title: "From a script" }));
|
|
148
|
+
assert.equal(cli.status, 201);
|
|
149
|
+
// Reads are uncontested either way.
|
|
150
|
+
const read = await call(base, "/api/git/status", { headers: { Origin: "http://evil.example" } });
|
|
151
|
+
assert.equal(read.status, 200);
|
|
152
|
+
});
|
|
153
|
+
test("a repo with no origin serves boards but no git surface at all", async () => {
|
|
154
|
+
const root = tempDir();
|
|
155
|
+
sh(root, "git", "init", "--quiet", "-b", "main", root);
|
|
156
|
+
sh(root, "git", "config", "user.name", "Test");
|
|
157
|
+
sh(root, "git", "config", "user.email", "test@example.com");
|
|
158
|
+
const store = initProject(root, { name: "loner", prefix: "LON" });
|
|
159
|
+
store.create({ title: "Only", status: "todo" });
|
|
160
|
+
const base = await serve(root);
|
|
161
|
+
const project = await call(base, "/api/project");
|
|
162
|
+
assert.equal(project.status, 200);
|
|
163
|
+
assert.equal("git" in project.body, false, JSON.stringify(project.body));
|
|
164
|
+
const status = await call(base, "/api/git/status");
|
|
165
|
+
assert.equal(status.status, 404);
|
|
166
|
+
const branches = await call(base, "/api/git/branches");
|
|
167
|
+
assert.equal(branches.status, 404);
|
|
168
|
+
});
|
|
169
|
+
test("a plain directory (no git at all) behaves the same", async () => {
|
|
170
|
+
const root = tempDir();
|
|
171
|
+
mkdirSync(join(root, "inner"));
|
|
172
|
+
const store = initProject(join(root, "inner"), { name: "bare", prefix: "BAR" });
|
|
173
|
+
store.create({ title: "Only", status: "todo" });
|
|
174
|
+
// Guard against the test machine's own enclosing repo: this only proves
|
|
175
|
+
// something when the temp dir is genuinely outside one, which tmpdir() is.
|
|
176
|
+
const base = await serve(join(root, "inner"));
|
|
177
|
+
const project = await call(base, "/api/project");
|
|
178
|
+
assert.equal("git" in project.body, false);
|
|
179
|
+
});
|
|
180
|
+
test("the gh-less PR fallback is the exact GitHub compare URL, branches escaped", () => {
|
|
181
|
+
assert.equal(compareUrl({ owner: "nicmeriano", repo: "nickmeriano.com" }, "main", "board/2026-09-01"), "https://github.com/nicmeriano/nickmeriano.com/compare/main...board%2F2026-09-01?expand=1");
|
|
182
|
+
});
|
|
183
|
+
//# sourceMappingURL=git-serve.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-serve.test.js","sourceRoot":"","sources":["../src/git-serve.test.ts"],"names":[],"mappings":"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"}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every git spawn in the CLI goes through here (TAS-40, folded into TAS-42):
|
|
3
|
+
* one place that guarantees a spawn can never wedge the process. Each call is
|
|
4
|
+
* non-interactive — `GIT_TERMINAL_PROMPT=0` turns a would-be credential
|
|
5
|
+
* prompt into a fast failure — and bounded by a timeout, because several of
|
|
6
|
+
* these run inside `task serve` request handlers, where a hung prompt or a
|
|
7
|
+
* wedged remote would hang the board for everyone looking at it.
|
|
8
|
+
*
|
|
9
|
+
* Three calling styles, because callers genuinely differ on what failure is:
|
|
10
|
+
* `git` returns the full result (claim flows branch on exit codes), `gitMust`
|
|
11
|
+
* throws (steps with no soft outcome), `gitOut` returns null (read paths that
|
|
12
|
+
* degrade to "can't ask git" — the serve/inbox rule).
|
|
13
|
+
*/
|
|
14
|
+
export interface GitResult {
|
|
15
|
+
status: number;
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
}
|
|
19
|
+
export interface GitOptions {
|
|
20
|
+
/** Extra env on top of the non-interactive baseline — e.g. GIT_INDEX_FILE. */
|
|
21
|
+
env?: Record<string, string>;
|
|
22
|
+
/** Piped to stdin — e.g. `hash-object -w --stdin`. */
|
|
23
|
+
input?: string;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
}
|
|
26
|
+
/** Local operations answer fast or something is genuinely wrong. */
|
|
27
|
+
export declare const GIT_TIMEOUT_MS = 30000;
|
|
28
|
+
/** Network operations (a full fetch, a push) get real-world headroom. */
|
|
29
|
+
export declare const GIT_NETWORK_TIMEOUT_MS = 120000;
|
|
30
|
+
/**
|
|
31
|
+
* The one spawn. A failed spawn (git missing, timeout hit) reads as status 1
|
|
32
|
+
* with the reason in stderr — callers treat it exactly like a failed command,
|
|
33
|
+
* which for every caller here is the truth that matters.
|
|
34
|
+
*/
|
|
35
|
+
export declare function runGit(cwd: string, args: string[], options?: GitOptions): GitResult;
|
|
36
|
+
export declare function git(cwd: string, ...args: string[]): GitResult;
|
|
37
|
+
/** Run git and throw on failure — for the steps that have no soft outcome. */
|
|
38
|
+
export declare function gitMust(cwd: string, ...args: string[]): string;
|
|
39
|
+
/** Soft git: stdout on success, null on any failure — callers decide if that's an error. */
|
|
40
|
+
export declare function gitOut(cwd: string, ...args: string[]): string | null;
|
|
41
|
+
/**
|
|
42
|
+
* The base a claim or board branch starts from: origin's default branch.
|
|
43
|
+
* Resolved from `origin/HEAD` when the clone recorded it, with a main/master
|
|
44
|
+
* fallback for repos wired up by hand (`git remote add` + push never sets
|
|
45
|
+
* origin/HEAD).
|
|
46
|
+
*/
|
|
47
|
+
export declare function defaultBase(cwd: string): string;
|
|
48
|
+
export interface RepoRef {
|
|
49
|
+
owner: string;
|
|
50
|
+
repo: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The GitHub repo this working copy pushes to.
|
|
54
|
+
*
|
|
55
|
+
* `origin` first, then the only remote if there's exactly one — a clone with
|
|
56
|
+
* `upstream` and a fork remote should not silently pick the wrong one, so
|
|
57
|
+
* ambiguity reads as "no repo" and the caller asks the user.
|
|
58
|
+
*/
|
|
59
|
+
export declare function detectRepo(cwd: string): RepoRef | null;
|
|
60
|
+
/**
|
|
61
|
+
* Every shape git writes a GitHub remote in: https, ssh, `git@`-scp, with or
|
|
62
|
+
* without `.git`, and with or without credentials embedded in the host.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseRemote(url: string): RepoRef | null;
|
|
65
|
+
//# sourceMappingURL=git.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,UAAU;IACzB,8EAA8E;IAC9E,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,oEAAoE;AACpE,eAAO,MAAM,cAAc,QAAS,CAAA;AACpC,yEAAyE;AACzE,eAAO,MAAM,sBAAsB,SAAU,CAAA;AAE7C;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,UAAe,GAAG,SAAS,CAgBvF;AAED,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,SAAS,CAE7D;AAED,8EAA8E;AAC9E,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAM9D;AAED,4FAA4F;AAC5F,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAGpE;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAW/C;AAID,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAItD;AAaD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CASvD"}
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every git spawn in the CLI goes through here (TAS-40, folded into TAS-42):
|
|
3
|
+
* one place that guarantees a spawn can never wedge the process. Each call is
|
|
4
|
+
* non-interactive — `GIT_TERMINAL_PROMPT=0` turns a would-be credential
|
|
5
|
+
* prompt into a fast failure — and bounded by a timeout, because several of
|
|
6
|
+
* these run inside `task serve` request handlers, where a hung prompt or a
|
|
7
|
+
* wedged remote would hang the board for everyone looking at it.
|
|
8
|
+
*
|
|
9
|
+
* Three calling styles, because callers genuinely differ on what failure is:
|
|
10
|
+
* `git` returns the full result (claim flows branch on exit codes), `gitMust`
|
|
11
|
+
* throws (steps with no soft outcome), `gitOut` returns null (read paths that
|
|
12
|
+
* degrade to "can't ask git" — the serve/inbox rule).
|
|
13
|
+
*/
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
/** Local operations answer fast or something is genuinely wrong. */
|
|
16
|
+
export const GIT_TIMEOUT_MS = 30_000;
|
|
17
|
+
/** Network operations (a full fetch, a push) get real-world headroom. */
|
|
18
|
+
export const GIT_NETWORK_TIMEOUT_MS = 120_000;
|
|
19
|
+
/**
|
|
20
|
+
* The one spawn. A failed spawn (git missing, timeout hit) reads as status 1
|
|
21
|
+
* with the reason in stderr — callers treat it exactly like a failed command,
|
|
22
|
+
* which for every caller here is the truth that matters.
|
|
23
|
+
*/
|
|
24
|
+
export function runGit(cwd, args, options = {}) {
|
|
25
|
+
const result = spawnSync("git", args, {
|
|
26
|
+
cwd,
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
input: options.input,
|
|
29
|
+
timeout: options.timeoutMs ?? GIT_TIMEOUT_MS,
|
|
30
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", ...options.env },
|
|
31
|
+
});
|
|
32
|
+
if (result.error) {
|
|
33
|
+
return { status: 1, stdout: "", stderr: result.error.message };
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
status: result.status ?? 1,
|
|
37
|
+
stdout: (result.stdout ?? "").trim(),
|
|
38
|
+
stderr: (result.stderr ?? "").trim(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function git(cwd, ...args) {
|
|
42
|
+
return runGit(cwd, args);
|
|
43
|
+
}
|
|
44
|
+
/** Run git and throw on failure — for the steps that have no soft outcome. */
|
|
45
|
+
export function gitMust(cwd, ...args) {
|
|
46
|
+
const result = runGit(cwd, args);
|
|
47
|
+
if (result.status !== 0) {
|
|
48
|
+
throw new Error(`git ${args[0]} failed: ${result.stderr || result.stdout}`);
|
|
49
|
+
}
|
|
50
|
+
return result.stdout;
|
|
51
|
+
}
|
|
52
|
+
/** Soft git: stdout on success, null on any failure — callers decide if that's an error. */
|
|
53
|
+
export function gitOut(cwd, ...args) {
|
|
54
|
+
const result = runGit(cwd, args);
|
|
55
|
+
return result.status === 0 ? result.stdout : null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The base a claim or board branch starts from: origin's default branch.
|
|
59
|
+
* Resolved from `origin/HEAD` when the clone recorded it, with a main/master
|
|
60
|
+
* fallback for repos wired up by hand (`git remote add` + push never sets
|
|
61
|
+
* origin/HEAD).
|
|
62
|
+
*/
|
|
63
|
+
export function defaultBase(cwd) {
|
|
64
|
+
const head = git(cwd, "symbolic-ref", "--quiet", "refs/remotes/origin/HEAD");
|
|
65
|
+
if (head.status === 0)
|
|
66
|
+
return head.stdout.replace(/^refs\/remotes\//, "");
|
|
67
|
+
for (const name of ["main", "master"]) {
|
|
68
|
+
if (git(cwd, "show-ref", "--verify", "--quiet", `refs/remotes/origin/${name}`).status === 0) {
|
|
69
|
+
return `origin/${name}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw new Error("couldn't resolve origin's default branch — origin/HEAD is unset and neither origin/main nor origin/master exists");
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The GitHub repo this working copy pushes to.
|
|
76
|
+
*
|
|
77
|
+
* `origin` first, then the only remote if there's exactly one — a clone with
|
|
78
|
+
* `upstream` and a fork remote should not silently pick the wrong one, so
|
|
79
|
+
* ambiguity reads as "no repo" and the caller asks the user.
|
|
80
|
+
*/
|
|
81
|
+
export function detectRepo(cwd) {
|
|
82
|
+
const remotes = gitRemotes(cwd);
|
|
83
|
+
const url = remotes.get("origin") ?? (remotes.size === 1 ? [...remotes.values()][0] : null);
|
|
84
|
+
return url ? parseRemote(url) : null;
|
|
85
|
+
}
|
|
86
|
+
function gitRemotes(cwd) {
|
|
87
|
+
const out = new Map();
|
|
88
|
+
const raw = gitOut(cwd, "remote", "-v");
|
|
89
|
+
if (raw === null)
|
|
90
|
+
return out;
|
|
91
|
+
for (const line of raw.split("\n")) {
|
|
92
|
+
const match = /^(\S+)\s+(\S+)\s+\(fetch\)$/.exec(line.trim());
|
|
93
|
+
if (match)
|
|
94
|
+
out.set(match[1], match[2]);
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Every shape git writes a GitHub remote in: https, ssh, `git@`-scp, with or
|
|
100
|
+
* without `.git`, and with or without credentials embedded in the host.
|
|
101
|
+
*/
|
|
102
|
+
export function parseRemote(url) {
|
|
103
|
+
const cleaned = url.trim().replace(/\.git$/, "");
|
|
104
|
+
const match = /^(?:https?:\/\/|ssh:\/\/)?(?:[^@/]+@)?github\.com[:/]([^/]+)\/([^/]+?)\/?$/.exec(cleaned);
|
|
105
|
+
if (!match)
|
|
106
|
+
return null;
|
|
107
|
+
const [, owner, repo] = match;
|
|
108
|
+
if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(owner))
|
|
109
|
+
return null;
|
|
110
|
+
if (!/^[A-Za-z0-9._-]{1,100}$/.test(repo))
|
|
111
|
+
return null;
|
|
112
|
+
return { owner, repo };
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=git.js.map
|
package/dist/git.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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"}
|
package/dist/id.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticket keys — the branch-safe identity scheme (TAS-42).
|
|
3
|
+
*
|
|
4
|
+
* A ticket's key is 5 random characters from a 27-letter alphabet, minted at
|
|
5
|
+
* `task add` with no coordination: two checkouts that have never seen each
|
|
6
|
+
* other's commits can't mint the same key except by 1-in-14M chance per pair,
|
|
7
|
+
* and `task check`'s duplicate lint catches the miracle at merge. The key IS
|
|
8
|
+
* the ticket's directory name under `.task/tickets/`, so allocation is safe by
|
|
9
|
+
* construction — git can't merge two different tickets into one directory.
|
|
10
|
+
*
|
|
11
|
+
* The alphabet drops vowels (keys can't spell words), 0/1/i/l/o/u (no
|
|
12
|
+
* confusable glyphs when a key is read aloud or typed from a screen), and is
|
|
13
|
+
* all-lowercase (keys land in claim branch names, which are case-sensitive
|
|
14
|
+
* refs). A key always contains a letter — an all-digit roll is re-rolled — so
|
|
15
|
+
* a purely numeric directory is unambiguously a pre-migration ticket, which
|
|
16
|
+
* is exactly how the check lint tells the two apart.
|
|
17
|
+
*
|
|
18
|
+
* Keys deliberately carry no timestamp and no sequence: identity must be
|
|
19
|
+
* dumber than metadata. Creation order lives in `created:`; board order lives
|
|
20
|
+
* in `position:`. Anything the key "said" beyond naming the ticket could be
|
|
21
|
+
* wrong one day, and a wrong id breaks every reference to it.
|
|
22
|
+
*/
|
|
23
|
+
export declare const KEY_ALPHABET = "23456789bcdfghjkmnpqrstvwxz";
|
|
24
|
+
export declare const KEY_LENGTH = 5;
|
|
25
|
+
/**
|
|
26
|
+
* What a ticket key may look like — the shape readers accept, wider than what
|
|
27
|
+
* minting produces so a future length change doesn't strand old boards. Must
|
|
28
|
+
* contain a letter: an all-digit name is a pre-migration ticket number, and
|
|
29
|
+
* the two must never be confusable.
|
|
30
|
+
*/
|
|
31
|
+
export declare const KEY_PATTERN: RegExp;
|
|
32
|
+
export declare function isTicketKey(value: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Mint a key no ticket on this checkout uses. The local-collision retry is
|
|
35
|
+
* near-free insurance; the cross-branch case has no lock by design and rides
|
|
36
|
+
* on the odds (27^5 ≈ 14M keys) plus the check lint.
|
|
37
|
+
*/
|
|
38
|
+
export declare function mintKey(taken: ReadonlySet<string>): string;
|
|
39
|
+
//# sourceMappingURL=id.d.ts.map
|
package/dist/id.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../src/id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,eAAO,MAAM,YAAY,gCAAgC,CAAA;AACzD,eAAO,MAAM,UAAU,IAAI,CAAA;AAE3B;;;;;GAKG;AACH,eAAO,MAAM,WAAW,QAAgC,CAAA;AAExD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAElD;AAoBD;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,MAAM,CAM1D"}
|
package/dist/id.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticket keys — the branch-safe identity scheme (TAS-42).
|
|
3
|
+
*
|
|
4
|
+
* A ticket's key is 5 random characters from a 27-letter alphabet, minted at
|
|
5
|
+
* `task add` with no coordination: two checkouts that have never seen each
|
|
6
|
+
* other's commits can't mint the same key except by 1-in-14M chance per pair,
|
|
7
|
+
* and `task check`'s duplicate lint catches the miracle at merge. The key IS
|
|
8
|
+
* the ticket's directory name under `.task/tickets/`, so allocation is safe by
|
|
9
|
+
* construction — git can't merge two different tickets into one directory.
|
|
10
|
+
*
|
|
11
|
+
* The alphabet drops vowels (keys can't spell words), 0/1/i/l/o/u (no
|
|
12
|
+
* confusable glyphs when a key is read aloud or typed from a screen), and is
|
|
13
|
+
* all-lowercase (keys land in claim branch names, which are case-sensitive
|
|
14
|
+
* refs). A key always contains a letter — an all-digit roll is re-rolled — so
|
|
15
|
+
* a purely numeric directory is unambiguously a pre-migration ticket, which
|
|
16
|
+
* is exactly how the check lint tells the two apart.
|
|
17
|
+
*
|
|
18
|
+
* Keys deliberately carry no timestamp and no sequence: identity must be
|
|
19
|
+
* dumber than metadata. Creation order lives in `created:`; board order lives
|
|
20
|
+
* in `position:`. Anything the key "said" beyond naming the ticket could be
|
|
21
|
+
* wrong one day, and a wrong id breaks every reference to it.
|
|
22
|
+
*/
|
|
23
|
+
export const KEY_ALPHABET = "23456789bcdfghjkmnpqrstvwxz";
|
|
24
|
+
export const KEY_LENGTH = 5;
|
|
25
|
+
/**
|
|
26
|
+
* What a ticket key may look like — the shape readers accept, wider than what
|
|
27
|
+
* minting produces so a future length change doesn't strand old boards. Must
|
|
28
|
+
* contain a letter: an all-digit name is a pre-migration ticket number, and
|
|
29
|
+
* the two must never be confusable.
|
|
30
|
+
*/
|
|
31
|
+
export const KEY_PATTERN = /^(?=.*[a-z])[a-z0-9]{4,12}$/;
|
|
32
|
+
export function isTicketKey(value) {
|
|
33
|
+
return KEY_PATTERN.test(value);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* One random roll — Web Crypto (global in Node ≥19 and in Workers, so this
|
|
37
|
+
* module stays importable by the hosted board), with rejection sampling so
|
|
38
|
+
* the 27-letter alphabet gets a uniform draw rather than a modulo bias.
|
|
39
|
+
*/
|
|
40
|
+
function roll() {
|
|
41
|
+
const limit = 256 - (256 % KEY_ALPHABET.length);
|
|
42
|
+
let key = "";
|
|
43
|
+
while (key.length < KEY_LENGTH) {
|
|
44
|
+
const bytes = crypto.getRandomValues(new Uint8Array(KEY_LENGTH * 2));
|
|
45
|
+
for (const byte of bytes) {
|
|
46
|
+
if (byte >= limit || key.length >= KEY_LENGTH)
|
|
47
|
+
continue;
|
|
48
|
+
key += KEY_ALPHABET[byte % KEY_ALPHABET.length];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return key;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Mint a key no ticket on this checkout uses. The local-collision retry is
|
|
55
|
+
* near-free insurance; the cross-branch case has no lock by design and rides
|
|
56
|
+
* on the odds (27^5 ≈ 14M keys) plus the check lint.
|
|
57
|
+
*/
|
|
58
|
+
export function mintKey(taken) {
|
|
59
|
+
for (;;) {
|
|
60
|
+
const key = roll();
|
|
61
|
+
if (!KEY_PATTERN.test(key))
|
|
62
|
+
continue; // all-digit roll — must stay pre-migration-only
|
|
63
|
+
if (!taken.has(key))
|
|
64
|
+
return key;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=id.js.map
|
package/dist/id.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"id.js","sourceRoot":"","sources":["../src/id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,6BAA6B,CAAA;AACzD,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAA;AAE3B;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,6BAA6B,CAAA;AAExD,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,OAAO,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AAChC,CAAC;AAED;;;;GAIG;AACH,SAAS,IAAI;IACX,MAAM,KAAK,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,CAAA;IAC/C,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,OAAO,GAAG,CAAC,MAAM,GAAG,UAAU,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAA;QACpE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,IAAI,IAAI,KAAK,IAAI,GAAG,CAAC,MAAM,IAAI,UAAU;gBAAE,SAAQ;YACvD,GAAG,IAAI,YAAY,CAAC,IAAI,GAAG,YAAY,CAAC,MAAM,CAAC,CAAA;QACjD,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,KAA0B;IAChD,SAAS,CAAC;QACR,MAAM,GAAG,GAAG,IAAI,EAAE,CAAA;QAClB,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,SAAQ,CAAC,gDAAgD;QACrF,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,GAAG,CAAA;IACjC,CAAC;AACH,CAAC"}
|
package/dist/inbox.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `task inbox` — every open ask on effectively-in-progress tickets, repo-wide.
|
|
3
|
+
* The one list a person checks to unblock work NOW (TAS-38): each entry is a
|
|
4
|
+
* stalled worker or stalled work, nothing else.
|
|
5
|
+
*
|
|
6
|
+
* "Effectively in progress" is TAS-26's read core: between claim and merge the
|
|
7
|
+
* default branch still says `todo` — the claim branch is the truth about
|
|
8
|
+
* in-flight work, and the branch *name* carries the ticket id, so resolving it
|
|
9
|
+
* costs one fetch of the claim refs, never a general branch scan. The asks a
|
|
10
|
+
* worker files mid-run live on its claim branch too, so the inbox reads each
|
|
11
|
+
* claimed ticket's ask files from the branch and unions them with the local
|
|
12
|
+
* ones by id (asks are one file each, so the union is exact; see claim-io.ts,
|
|
13
|
+
* which also owns the write side of the same rule). Offline, or with no
|
|
14
|
+
* origin at all, everything degrades silently to file truth — a board you
|
|
15
|
+
* can't fetch is still a board.
|
|
16
|
+
*
|
|
17
|
+
* Backlog and todo asks are deliberately out of scope: unstarted work owes
|
|
18
|
+
* nobody anything yet, and claiming is what promotes an ask into the inbox.
|
|
19
|
+
*/
|
|
20
|
+
import type { Store } from "./store.ts";
|
|
21
|
+
import type { Ask, Task } from "./types.ts";
|
|
22
|
+
/** A board handed to buildInbox: the store plus the id the UI navigates with. */
|
|
23
|
+
export interface InboxBoard {
|
|
24
|
+
/** Path relative to the repo's board scope — "." for the root board. */
|
|
25
|
+
id: string;
|
|
26
|
+
store: Store;
|
|
27
|
+
}
|
|
28
|
+
export interface InboxEntry {
|
|
29
|
+
board: string;
|
|
30
|
+
prefix: string;
|
|
31
|
+
/** File truth — a claimed ticket may still read `todo` here; `claim` says why it counts. */
|
|
32
|
+
task: Task;
|
|
33
|
+
/** Present when the ticket's claim branch exists on origin. */
|
|
34
|
+
claim: {
|
|
35
|
+
branch: string;
|
|
36
|
+
} | null;
|
|
37
|
+
/** Open asks only, oldest first; ordinals match `task show`'s (the union's, when claimed). */
|
|
38
|
+
asks: Ask[];
|
|
39
|
+
}
|
|
40
|
+
export declare function buildInbox(boards: InboxBoard[]): InboxEntry[];
|
|
41
|
+
//# sourceMappingURL=inbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../src/inbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAE3C,iFAAiF;AACjF,MAAM,WAAW,UAAU;IACzB,wEAAwE;IACxE,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,KAAK,CAAA;CACb;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,4FAA4F;IAC5F,IAAI,EAAE,IAAI,CAAA;IACV,+DAA+D;IAC/D,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAChC,8FAA8F;IAC9F,IAAI,EAAE,GAAG,EAAE,CAAA;CACZ;AAOD,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CA4B7D"}
|
package/dist/inbox.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `task inbox` — every open ask on effectively-in-progress tickets, repo-wide.
|
|
3
|
+
* The one list a person checks to unblock work NOW (TAS-38): each entry is a
|
|
4
|
+
* stalled worker or stalled work, nothing else.
|
|
5
|
+
*
|
|
6
|
+
* "Effectively in progress" is TAS-26's read core: between claim and merge the
|
|
7
|
+
* default branch still says `todo` — the claim branch is the truth about
|
|
8
|
+
* in-flight work, and the branch *name* carries the ticket id, so resolving it
|
|
9
|
+
* costs one fetch of the claim refs, never a general branch scan. The asks a
|
|
10
|
+
* worker files mid-run live on its claim branch too, so the inbox reads each
|
|
11
|
+
* claimed ticket's ask files from the branch and unions them with the local
|
|
12
|
+
* ones by id (asks are one file each, so the union is exact; see claim-io.ts,
|
|
13
|
+
* which also owns the write side of the same rule). Offline, or with no
|
|
14
|
+
* origin at all, everything degrades silently to file truth — a board you
|
|
15
|
+
* can't fetch is still a board.
|
|
16
|
+
*
|
|
17
|
+
* Backlog and todo asks are deliberately out of scope: unstarted work owes
|
|
18
|
+
* nobody anything yet, and claiming is what promotes an ask into the inbox.
|
|
19
|
+
*/
|
|
20
|
+
import { claimBranch, claimNamespace } from "./claim.js";
|
|
21
|
+
import { mergedAsks, remoteClaimTips, syncClaimRefs } from "./claim-io.js";
|
|
22
|
+
/** Oldest open ask decides an entry's urgency — and the whole list's order. */
|
|
23
|
+
function oldestAsk(entry) {
|
|
24
|
+
return entry.asks[0]?.createdAt ?? "";
|
|
25
|
+
}
|
|
26
|
+
export function buildInbox(boards) {
|
|
27
|
+
const entries = [];
|
|
28
|
+
for (const { id, store } of boards) {
|
|
29
|
+
// One bounded ls-remote per board; branches are then fetched only when
|
|
30
|
+
// their tip moved (syncClaimRefs), so a refetch-happy UI costs one round
|
|
31
|
+
// trip, not a fetch storm. Null = origin unreachable = file truth only.
|
|
32
|
+
const tips = remoteClaimTips(store);
|
|
33
|
+
const claimed = tips ? new Set(tips.keys()) : null;
|
|
34
|
+
if (tips && tips.size > 0) {
|
|
35
|
+
try {
|
|
36
|
+
syncClaimRefs(store, claimNamespace(store.config), tips);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Namespace validated by remoteClaimTips already; nothing to do.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// in_progress is in by file truth; todo is in only via a live claim.
|
|
43
|
+
for (const task of store.list({ statuses: ["in_progress", "todo"] })) {
|
|
44
|
+
const branch = claimBranch(store.config, task.key);
|
|
45
|
+
const claim = claimed?.has(branch) ? { branch } : null;
|
|
46
|
+
if (task.status !== "in_progress" && !claim)
|
|
47
|
+
continue;
|
|
48
|
+
const asks = (claim ? mergedAsks(store, branch, task) : task.asks).filter((a) => !a.resolvedAt);
|
|
49
|
+
if (asks.length === 0)
|
|
50
|
+
continue;
|
|
51
|
+
entries.push({ board: id, prefix: store.config.prefix, task, claim, asks });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return entries.sort((a, b) => oldestAsk(a).localeCompare(oldestAsk(b)));
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=inbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbox.js","sourceRoot":"","sources":["../src/inbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAsB1E,+EAA+E;AAC/E,SAAS,SAAS,CAAC,KAAiB;IAClC,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,SAAS,IAAI,EAAE,CAAA;AACvC,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAoB;IAC7C,MAAM,OAAO,GAAiB,EAAE,CAAA;IAChC,KAAK,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,MAAM,EAAE,CAAC;QACnC,uEAAuE;QACvE,yEAAyE;QACzE,wEAAwE;QACxE,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,CAAC,CAAA;QACnC,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QAClD,IAAI,IAAI,IAAI,IAAI,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAC1B,IAAI,CAAC;gBACH,aAAa,CAAC,KAAK,EAAE,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAA;YAC1D,CAAC;YAAC,MAAM,CAAC;gBACP,iEAAiE;YACnE,CAAC;QACH,CAAC;QACD,qEAAqE;QACrE,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC,aAAa,EAAE,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;YACrE,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;YAClD,MAAM,KAAK,GAAG,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;YACtD,IAAI,IAAI,CAAC,MAAM,KAAK,aAAa,IAAI,CAAC,KAAK;gBAAE,SAAQ;YACrD,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CACvE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CACrB,CAAA;YACD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAQ;YAC/B,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAC7E,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACzE,CAAC"}
|
package/dist/overview.d.ts
CHANGED
|
@@ -24,7 +24,16 @@ export interface Overview {
|
|
|
24
24
|
/** Live board only — the archive is history, not health. */
|
|
25
25
|
statuses: Record<Status, number>;
|
|
26
26
|
open: number;
|
|
27
|
+
/** Open tickets a person owes something — i.e. carrying open asks. */
|
|
27
28
|
needsHuman: number;
|
|
29
|
+
/** Open asks across the live board's open tickets. */
|
|
30
|
+
openAsks: number;
|
|
31
|
+
/** The ask that's been owed longest — the standup's guilty number. */
|
|
32
|
+
oldestAsk: {
|
|
33
|
+
taskId: string;
|
|
34
|
+
text: string;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
} | null;
|
|
28
37
|
/**
|
|
29
38
|
* Open tickets whose claim branch exists on origin right now; null when
|
|
30
39
|
* origin can't be asked (no remote, offline) — unknown, not zero.
|