dsh-taskboard 0.2.2 → 0.4.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 (43) hide show
  1. package/README.md +62 -6
  2. package/lib/client.js +1839 -74
  3. package/lib/host/execution.js +199 -54
  4. package/lib/host/execution.js.map +1 -1
  5. package/lib/host/git.js +327 -0
  6. package/lib/host/git.js.map +1 -0
  7. package/lib/host/protocol-text.js +5 -3
  8. package/lib/host/protocol-text.js.map +1 -1
  9. package/lib/host/routes.js +435 -5
  10. package/lib/host/routes.js.map +1 -1
  11. package/lib/host/store.js +12 -0
  12. package/lib/host/store.js.map +1 -1
  13. package/lib/host/templates.js +166 -0
  14. package/lib/host/templates.js.map +1 -0
  15. package/lib/host/tools.js +217 -3
  16. package/lib/host/tools.js.map +1 -1
  17. package/lib/index.js +23 -3
  18. package/lib/index.js.map +1 -1
  19. package/lib/shared/api.js.map +1 -1
  20. package/lib/shared/protocol.js +286 -1
  21. package/lib/shared/protocol.js.map +1 -1
  22. package/package.json +74 -74
  23. package/src/client/api.ts +47 -3
  24. package/src/client/board/ImportModal.tsx +182 -0
  25. package/src/client/board/TaskBoard.tsx +118 -3
  26. package/src/client/board/TaskCard.tsx +9 -0
  27. package/src/client/board/TaskDetail.tsx +360 -4
  28. package/src/client/board/TaskFormModal.tsx +193 -12
  29. package/src/client/board/TemplateManager.tsx +121 -0
  30. package/src/client/controller.ts +238 -11
  31. package/src/client/index.ts +18 -1
  32. package/src/client/styles.ts +198 -0
  33. package/src/host/execution.ts +301 -67
  34. package/src/host/git.ts +370 -0
  35. package/src/host/protocol-text.ts +5 -3
  36. package/src/host/routes.ts +483 -5
  37. package/src/host/store.ts +13 -0
  38. package/src/host/templates.ts +143 -0
  39. package/src/host/tools.ts +215 -2
  40. package/src/index.ts +30 -1
  41. package/src/shared/api.ts +89 -3
  42. package/src/shared/protocol.ts +408 -0
  43. package/src/shared/version.ts +1 -1
@@ -0,0 +1,327 @@
1
+ //#region src/host/git.ts
2
+ /** Timeout for quick read-only queries (rev-parse / status / log / diff). */
3
+ const QUICK_TIMEOUT_MS = 2e3;
4
+ /** Timeout for structural operations (worktree add/remove, merge, branch). */
5
+ const HEAVY_TIMEOUT_MS = 15e3;
6
+ /** Directory under a workspace where task worktrees live. */
7
+ const WORKTREE_DIR = ".dsh-worktrees";
8
+ /** Diff viewer caps (0.4.0): raw text kept per view. */
9
+ const MAX_DIFF_BYTES = 128 * 1024;
10
+ /** Diff viewer caps: lines kept per view. */
11
+ const MAX_DIFF_LINES = 2e3;
12
+ /** Cap one diff payload by bytes and lines (in order, marking truncation). */
13
+ function capDiff(out) {
14
+ let text = out;
15
+ let truncated = false;
16
+ if (text.length > 131072) {
17
+ text = text.slice(0, MAX_DIFF_BYTES);
18
+ truncated = true;
19
+ }
20
+ const lines = text.split("\n");
21
+ if (lines.length > 2e3) {
22
+ text = lines.slice(0, MAX_DIFF_LINES).join("\n");
23
+ truncated = true;
24
+ }
25
+ return {
26
+ text,
27
+ truncated
28
+ };
29
+ }
30
+ /** A plausible git object hash (defense against option injection). */
31
+ function isHash(hash) {
32
+ return /^[0-9a-f]{4,64}$/i.test(hash);
33
+ }
34
+ /**
35
+ * Build the task branch name `task/<标题>+<taskId>` (plan §9 拍板).
36
+ *
37
+ * Title sanitizing: whitespace runs collapse to `-`; git-illegal characters
38
+ * (`~ ^ : ? * [ \ / @ { }` and friends) are stripped; `..` collapses; the
39
+ * segment is trimmed of leading/trailing `.-` and truncated to ~20 code
40
+ * points; an empty result falls back to the bare `task/<taskId>`.
41
+ * @param title - the task title (already normalized 1..200 chars).
42
+ * @param taskId - the task id (stable suffix).
43
+ * @returns the branch name.
44
+ */
45
+ function sanitizeBranchName(title, taskId) {
46
+ const segment = title.trim().replace(/\s+/g, "-").replace(/[/\\~^:?*[\]@{}"'<>|#%&;$!`'=,;()]+/g, "").replace(/\.\.+/g, ".").replace(/^[-.\s]+|[-.\s]+$/g, "");
47
+ const head = Array.from(segment).slice(0, 20).join("").replace(/^[-.]+|[-.]+$/g, "");
48
+ return head.length === 0 ? `task/${taskId}` : `task/${head}+${taskId}`;
49
+ }
50
+ /** The canonical worktree path of a task inside its workspace (forward slashes). */
51
+ function worktreePathOf(workspacePath, taskId) {
52
+ return `${workspacePath.replace(/[\\/]+$/, "").replaceAll("\\", "/")}/${WORKTREE_DIR}/${taskId}`;
53
+ }
54
+ /** Real exec layer over child_process.execFile (windowsHide, timeout, maxBuffer). */
55
+ const realExec = (args, options) => new Promise((resolve) => {
56
+ (async () => {
57
+ const { execFile } = await import("node:child_process");
58
+ execFile("git", args, {
59
+ cwd: options.cwd,
60
+ timeout: options.timeout ?? QUICK_TIMEOUT_MS,
61
+ windowsHide: true,
62
+ maxBuffer: 4 * 1024 * 1024,
63
+ encoding: "utf8"
64
+ }, (error, stdout, stderr) => {
65
+ resolve({
66
+ ok: error === null,
67
+ stdout: String(stdout ?? ""),
68
+ stderr: String(stderr ?? "")
69
+ });
70
+ });
71
+ })().catch(() => resolve({
72
+ ok: false,
73
+ stdout: "",
74
+ stderr: "exec unavailable"
75
+ }));
76
+ });
77
+ /**
78
+ * Build a {@link GitFace} over an injectable exec layer.
79
+ * @param exec - the exec function (real `git` when omitted).
80
+ */
81
+ function createGitFace(exec = realExec) {
82
+ const quick = (args, cwd) => exec(args, {
83
+ cwd,
84
+ timeout: QUICK_TIMEOUT_MS
85
+ });
86
+ const heavy = (args, cwd) => exec(args, {
87
+ cwd,
88
+ timeout: HEAVY_TIMEOUT_MS
89
+ });
90
+ const locks = /* @__PURE__ */ new Map();
91
+ const withRootLock = (root, fn) => {
92
+ const next = (locks.get(root) ?? Promise.resolve()).then(fn, fn);
93
+ locks.set(root, next.catch(() => {}));
94
+ return next;
95
+ };
96
+ return {
97
+ async detect(root) {
98
+ const r = await quick(["rev-parse", "--is-inside-work-tree"], root);
99
+ return r.ok && r.stdout.trim() === "true";
100
+ },
101
+ async binaryAvailable() {
102
+ const r = await quick(["--version"]);
103
+ return r.ok && r.stdout.startsWith("git version");
104
+ },
105
+ prepareWorktree: (root, path, branch, mode = "fresh") => withRootLock(root, async () => {
106
+ if (mode === "reuse") {
107
+ const wtHead = await quick(["rev-parse", "HEAD"], path);
108
+ if (wtHead.ok && wtHead.stdout.trim().length > 0) return {
109
+ path,
110
+ branch,
111
+ baseCommit: wtHead.stdout.trim(),
112
+ reused: true
113
+ };
114
+ }
115
+ const head = await quick(["rev-parse", "HEAD"], root);
116
+ if (!head.ok) return void 0;
117
+ const baseCommit = head.stdout.trim();
118
+ if ((await quick([
119
+ "show-ref",
120
+ "--verify",
121
+ `refs/heads/${branch}`
122
+ ], root)).ok) {
123
+ await heavy([
124
+ "worktree",
125
+ "remove",
126
+ "--force",
127
+ path
128
+ ], root);
129
+ await heavy(["worktree", "prune"], root);
130
+ if (!(await heavy([
131
+ "branch",
132
+ "-f",
133
+ branch,
134
+ "HEAD"
135
+ ], root)).ok) return void 0;
136
+ if (!(await heavy([
137
+ "worktree",
138
+ "add",
139
+ path,
140
+ branch
141
+ ], root)).ok) return void 0;
142
+ } else if (!(await heavy([
143
+ "worktree",
144
+ "add",
145
+ "-b",
146
+ branch,
147
+ path
148
+ ], root)).ok) return void 0;
149
+ return {
150
+ path,
151
+ branch,
152
+ baseCommit
153
+ };
154
+ }),
155
+ async collect(worktreePath, baseCommit) {
156
+ const facts = {
157
+ commits: [],
158
+ commitsTotal: 0,
159
+ dirtyFiles: [],
160
+ dirtyFilesTotal: 0,
161
+ changedFiles: 0
162
+ };
163
+ const range = `${baseCommit}..HEAD`;
164
+ const head = await quick(["rev-parse", "HEAD"], worktreePath);
165
+ if (head.ok) facts.headCommit = head.stdout.trim();
166
+ const log = await quick([
167
+ "log",
168
+ "--pretty=format:%h %s",
169
+ range
170
+ ], worktreePath);
171
+ if (log.ok) {
172
+ const commits = log.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).map((line) => {
173
+ const space = line.indexOf(" ");
174
+ return space === -1 ? {
175
+ hash: line,
176
+ subject: ""
177
+ } : {
178
+ hash: line.slice(0, space),
179
+ subject: line.slice(space + 1)
180
+ };
181
+ });
182
+ facts.commitsTotal = commits.length;
183
+ facts.commits = commits.slice(0, 50);
184
+ }
185
+ const status = await quick(["status", "--porcelain"], worktreePath);
186
+ if (status.ok) {
187
+ const dirty = status.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
188
+ facts.dirtyFilesTotal = dirty.length;
189
+ facts.dirtyFiles = dirty.slice(0, 100);
190
+ }
191
+ const shortstat = await quick([
192
+ "diff",
193
+ "--shortstat",
194
+ range
195
+ ], worktreePath);
196
+ if (shortstat.ok && shortstat.stdout.trim().length > 0) facts.diffStat = shortstat.stdout.trim();
197
+ const names = await quick([
198
+ "diff",
199
+ "--name-only",
200
+ range
201
+ ], worktreePath);
202
+ if (names.ok) facts.changedFiles = names.stdout.split("\n").filter((l) => l.trim().length > 0).length;
203
+ return facts;
204
+ },
205
+ merge: (root, branch) => withRootLock(root, async () => {
206
+ const status = await quick(["status", "--porcelain"], root);
207
+ if (status.ok) {
208
+ const dirtyLines = status.stdout.split("\n").map((l) => l.trim()).filter((l) => {
209
+ if (l.length === 0) return false;
210
+ const path = l.slice(3);
211
+ return path !== ".dsh-worktrees" && !path.startsWith(`.dsh-worktrees/`);
212
+ });
213
+ if (dirtyLines.length > 0) throw new Error(`主工作区有 ${dirtyLines.length} 处未提交修改,请先提交或暂存后再合并`);
214
+ }
215
+ const merged = await heavy([
216
+ "merge",
217
+ "--no-ff",
218
+ "--no-edit",
219
+ branch
220
+ ], root);
221
+ if (!merged.ok) {
222
+ await heavy(["merge", "--abort"], root);
223
+ throw new Error(`合并失败:${merged.stderr.trim().slice(0, 300)}`);
224
+ }
225
+ }),
226
+ async isAncestor(root, branch) {
227
+ return (await quick([
228
+ "merge-base",
229
+ "--is-ancestor",
230
+ branch,
231
+ "HEAD"
232
+ ], root)).ok;
233
+ },
234
+ removeWorktree: (root, worktreePath) => withRootLock(root, async () => {
235
+ const status = await quick(["status", "--porcelain"], worktreePath);
236
+ if (status.ok && status.stdout.trim().length > 0) {
237
+ const lines = status.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
238
+ throw new Error(`worktree 有 ${lines.length} 处未提交修改,拒绝删除:\n${lines.slice(0, 10).join("\n")}`);
239
+ }
240
+ const removed = await heavy([
241
+ "worktree",
242
+ "remove",
243
+ worktreePath
244
+ ], root);
245
+ if (!removed.ok) throw new Error(`删除 worktree 失败:${(removed.stderr.trim() || removed.stdout.trim()).slice(0, 300)}`);
246
+ }),
247
+ deleteBranch: (root, branch) => withRootLock(root, async () => {
248
+ const deleted = await heavy([
249
+ "branch",
250
+ "-D",
251
+ branch
252
+ ], root);
253
+ if (!deleted.ok) throw new Error(`删除分支失败:${deleted.stderr.trim().slice(0, 300)}`);
254
+ }),
255
+ async showCommit(cwd, hash) {
256
+ if (!isHash(hash)) return void 0;
257
+ const r = await quick([
258
+ "show",
259
+ "--no-color",
260
+ "--format=medium",
261
+ hash
262
+ ], cwd);
263
+ if (!r.ok || r.stdout.trim().length === 0) return void 0;
264
+ return capDiff(r.stdout);
265
+ },
266
+ async showPathDiff(cwd, path, baseCommit) {
267
+ const target = path.trim();
268
+ if (target.length === 0) return void 0;
269
+ if (baseCommit !== void 0 && isHash(baseCommit)) {
270
+ const r = await quick([
271
+ "diff",
272
+ "--no-color",
273
+ `${baseCommit}..HEAD`,
274
+ "--",
275
+ target
276
+ ], cwd);
277
+ if (!r.ok) return void 0;
278
+ if (r.stdout.trim().length === 0) return {
279
+ text: "(该文件无差异)",
280
+ truncated: false
281
+ };
282
+ return capDiff(r.stdout);
283
+ }
284
+ const r = await quick([
285
+ "diff",
286
+ "--no-color",
287
+ "HEAD",
288
+ "--",
289
+ target
290
+ ], cwd);
291
+ if (r.ok && r.stdout.trim().length > 0) return capDiff(r.stdout);
292
+ const st = await quick([
293
+ "status",
294
+ "--porcelain",
295
+ "--",
296
+ target
297
+ ], cwd);
298
+ if (r.ok && st.ok && st.stdout.trim().startsWith("??")) {
299
+ const ni = await exec([
300
+ "diff",
301
+ "--no-color",
302
+ "--no-index",
303
+ "--",
304
+ "/dev/null",
305
+ target
306
+ ], {
307
+ cwd,
308
+ timeout: QUICK_TIMEOUT_MS
309
+ });
310
+ if (ni.stdout.includes("diff --git")) return capDiff(ni.stdout);
311
+ return {
312
+ text: `(未跟踪新文件:${target})`,
313
+ truncated: false
314
+ };
315
+ }
316
+ if (!r.ok) return void 0;
317
+ return {
318
+ text: "(该文件无差异)",
319
+ truncated: false
320
+ };
321
+ }
322
+ };
323
+ }
324
+ //#endregion
325
+ export { MAX_DIFF_BYTES, MAX_DIFF_LINES, WORKTREE_DIR, createGitFace, sanitizeBranchName, worktreePathOf };
326
+
327
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","names":[],"sources":["../../src/host/git.ts"],"sourcesContent":["/**\n * Host git face (0.3.0): the ONLY place dsh-taskboard shells out to git.\n * 0.3.1: per-repo serialization of structural operations, binary probing,\n * no-op merge detection, worktree REUSE mode, and evidence size caps.\n *\n * Design invariants (plan §3.4/§3.5):\n * - NARROW interface: detect / binaryAvailable / prepareWorktree / collect /\n * merge / isAncestor / removeWorktree / deleteBranch — nothing else leaks\n * into the plugin.\n * - FAIL-SOFT: every call has a timeout and resolves to a benign result\n * (false / undefined / empty facts) on ANY git failure — a missing git,\n * a locked worktree, or a damaged repo degrades execution to the original\n * directory and NEVER fails the ledger or the run pipeline. Only the\n * explicit user actions (merge / remove / deleteBranch) throw, with a\n * readable message the GUI surfaces as-is.\n * - SERIALIZED structural ops: concurrent isolated executions on the SAME\n * repository would race on git's index/worktree locks, so every structural\n * operation (prepareWorktree / merge / removeWorktree / deleteBranch) runs\n * inside a per-root in-process mutex. Read-only collects stay concurrent.\n * - INJECTABLE runner: the exec layer is a single function so unit tests\n * script every path without a real git.\n *\n * @module dsh-taskboard/host/git\n */\nimport type { CommitInfo } from '../shared/protocol.ts'\n\n/** Timeout for quick read-only queries (rev-parse / status / log / diff). */\nconst QUICK_TIMEOUT_MS = 2_000\n\n/** Timeout for structural operations (worktree add/remove, merge, branch). */\nconst HEAVY_TIMEOUT_MS = 15_000\n\n/** Directory under a workspace where task worktrees live. */\nexport const WORKTREE_DIR = '.dsh-worktrees'\n\n/** Evidence caps: commits kept per execution record (newest first). */\nexport const MAX_COMMIT_EVIDENCE = 50\n\n/** Evidence caps: uncommitted-change lines kept per execution record. */\nexport const MAX_DIRTY_EVIDENCE = 100\n\n/** Diff viewer caps (0.4.0): raw text kept per view. */\nexport const MAX_DIFF_BYTES = 128 * 1024\n\n/** Diff viewer caps: lines kept per view. */\nexport const MAX_DIFF_LINES = 2_000\n\n/** One capped, read-only diff view (diff viewer, 0.4.0). */\nexport interface DiffResult {\n text: string\n truncated: boolean\n}\n\n/** Cap one diff payload by bytes and lines (in order, marking truncation). */\nfunction capDiff(out: string): DiffResult {\n let text = out\n let truncated = false\n if (text.length > MAX_DIFF_BYTES) {\n text = text.slice(0, MAX_DIFF_BYTES)\n truncated = true\n }\n const lines = text.split('\\n')\n if (lines.length > MAX_DIFF_LINES) {\n text = lines.slice(0, MAX_DIFF_LINES).join('\\n')\n truncated = true\n }\n return { text, truncated }\n}\n\n/** A plausible git object hash (defense against option injection). */\nfunction isHash(hash: string): boolean {\n return /^[0-9a-f]{4,64}$/i.test(hash)\n}\n\n/** Result of one underlying exec: `ok` is exit-0, output never null. */\nexport interface ExecResult { ok: boolean; stdout: string; stderr: string }\n\n/** The injectable exec layer: run `git <args>` under a cwd with a timeout. */\nexport type ExecFn = (args: string[], options: { cwd?: string; timeout?: number }) => Promise<ExecResult>\n\n/** Facts needed to open an isolated execution. */\nexport interface WorktreeInfo {\n /** Absolute worktree path (the session's cwd). */\n path: string\n /** The task branch checked out there. */\n branch: string\n /** Baseline for evidence collection: main HEAD (fresh) or worktree HEAD (reuse). */\n baseCommit: string\n /** True when an existing live worktree was kept as-is (续跑). */\n reused?: boolean\n}\n\n/** Settlement facts collected from a worktree (partial on best-effort basis). */\nexport interface SettlementFacts {\n headCommit?: string\n commits: CommitInfo[]\n /** Total commits before capping (equals commits.length when under the cap). */\n commitsTotal: number\n dirtyFiles: string[]\n /** Total uncommitted lines before capping. */\n dirtyFilesTotal: number\n diffStat?: string\n changedFiles: number\n}\n\n/** The narrow git face the rest of the plugin depends on. */\nexport interface GitFace {\n /** Whether `root` sits inside a usable git work tree (fail-soft → false). */\n detect(root: string): Promise<boolean>\n /** Whether a usable git binary answers at all (distinguishes 未装 git vs 非 git 仓库). */\n binaryAvailable(): Promise<boolean>\n /**\n * Ensure a worktree at `path` on `branch`. Default mode `'fresh'` resets to\n * the main worktree's current HEAD (每次全新); mode `'reuse'` keeps a live\n * worktree exactly as-is (续跑 — agent's commits and uncommitted changes\n * survive) and falls back to a fresh creation when none is alive. Resolves\n * undefined on any failure — callers degrade to the original directory.\n */\n prepareWorktree(root: string, path: string, branch: string, mode?: 'fresh' | 'reuse'): Promise<WorktreeInfo | undefined>\n /** Collect settlement facts (never throws; missing pieces stay unset). */\n collect(worktreePath: string, baseCommit: string): Promise<SettlementFacts>\n /** Merge `branch` into the main worktree (`--no-ff`); THROWS with a readable reason. */\n merge(root: string, branch: string): Promise<void>\n /** Whether `branch` is already an ancestor of HEAD (a merge would be a no-op). */\n isAncestor(root: string, branch: string): Promise<boolean>\n /** Remove a worktree; THROWS when it still has uncommitted changes. */\n removeWorktree(root: string, worktreePath: string): Promise<void>\n /** Delete a branch; THROWS (e.g. still checked out in a worktree). */\n deleteBranch(root: string, branch: string): Promise<void>\n /**\n * Show one commit (message + patch) — diff viewer (0.4.0). Fail-soft:\n * undefined on any git failure; the payload is capped.\n */\n showCommit(cwd: string, hash: string): Promise<DiffResult | undefined>\n /**\n * Show the diff of one path — diff viewer (0.4.0). Without `baseCommit`:\n * the working-tree view (staged + unstaged vs HEAD, e.g. uncommitted\n * changes in a live worktree); with `baseCommit`: the range\n * base..HEAD restricted to the path. Fail-soft: undefined on failure.\n */\n showPathDiff(cwd: string, path: string, baseCommit?: string): Promise<DiffResult | undefined>\n}\n\n/**\n * Build the task branch name `task/<标题>+<taskId>` (plan §9 拍板).\n *\n * Title sanitizing: whitespace runs collapse to `-`; git-illegal characters\n * (`~ ^ : ? * [ \\ / @ { }` and friends) are stripped; `..` collapses; the\n * segment is trimmed of leading/trailing `.-` and truncated to ~20 code\n * points; an empty result falls back to the bare `task/<taskId>`.\n * @param title - the task title (already normalized 1..200 chars).\n * @param taskId - the task id (stable suffix).\n * @returns the branch name.\n */\nexport function sanitizeBranchName(title: string, taskId: string): string {\n const segment = title.trim()\n .replace(/\\s+/g, '-')\n .replace(/[/\\\\~^:?*[\\]@{}\"'<>|#%&;$!`'=,;()]+/g, '')\n .replace(/\\.\\.+/g, '.')\n .replace(/^[-.\\s]+|[-.\\s]+$/g, '')\n const head = Array.from(segment).slice(0, 20).join('').replace(/^[-.]+|[-.]+$/g, '')\n return head.length === 0 ? `task/${taskId}` : `task/${head}+${taskId}`\n}\n\n/** The canonical worktree path of a task inside its workspace (forward slashes). */\nexport function worktreePathOf(workspacePath: string, taskId: string): string {\n const root = workspacePath.replace(/[\\\\/]+$/, '').replaceAll('\\\\', '/')\n return `${root}/${WORKTREE_DIR}/${taskId}`\n}\n\n/** Real exec layer over child_process.execFile (windowsHide, timeout, maxBuffer). */\nconst realExec: ExecFn = (args, options) => new Promise(resolve => {\n void (async () => {\n const { execFile } = await import('node:child_process')\n execFile('git', args, {\n cwd: options.cwd,\n timeout: options.timeout ?? QUICK_TIMEOUT_MS,\n windowsHide: true,\n maxBuffer: 4 * 1024 * 1024,\n encoding: 'utf8',\n }, (error, stdout, stderr) => {\n resolve({ ok: error === null, stdout: String(stdout ?? ''), stderr: String(stderr ?? '') })\n })\n })().catch(() => resolve({ ok: false, stdout: '', stderr: 'exec unavailable' }))\n})\n\n/**\n * Build a {@link GitFace} over an injectable exec layer.\n * @param exec - the exec function (real `git` when omitted).\n */\nexport function createGitFace(exec: ExecFn = realExec): GitFace {\n const quick = (args: string[], cwd?: string): Promise<ExecResult> => exec(args, { cwd, timeout: QUICK_TIMEOUT_MS })\n const heavy = (args: string[], cwd?: string): Promise<ExecResult> => exec(args, { cwd, timeout: HEAVY_TIMEOUT_MS })\n\n // Per-root mutex (0.3.1): structural git ops on the SAME repository run one\n // at a time — concurrent isolated executions must not race on git's locks.\n const locks = new Map<string, Promise<unknown>>()\n const withRootLock = <T>(root: string, fn: () => Promise<T>): Promise<T> => {\n const prev = locks.get(root) ?? Promise.resolve()\n const next = prev.then(fn, fn)\n locks.set(root, next.catch(() => { /* the chain never blocks later ops */ }))\n return next\n }\n\n return {\n async detect(root) {\n const r = await quick(['rev-parse', '--is-inside-work-tree'], root)\n return r.ok && r.stdout.trim() === 'true'\n },\n\n async binaryAvailable() {\n const r = await quick(['--version'])\n return r.ok && r.stdout.startsWith('git version')\n },\n\n prepareWorktree: (root, path, branch, mode = 'fresh') => withRootLock(root, async () => {\n // 续跑: a live worktree at the path is kept EXACTLY as-is — the agent's\n // commits and uncommitted changes survive; the baseline becomes the\n // worktree's own HEAD so evidence covers only the new run.\n if (mode === 'reuse') {\n const wtHead = await quick(['rev-parse', 'HEAD'], path)\n if (wtHead.ok && wtHead.stdout.trim().length > 0) {\n return { path, branch, baseCommit: wtHead.stdout.trim(), reused: true }\n }\n // No live worktree → fall through to a fresh preparation.\n }\n\n // Baseline: the main worktree's current HEAD (also validates the repo).\n const head = await quick(['rev-parse', 'HEAD'], root)\n if (!head.ok) return undefined\n const baseCommit = head.stdout.trim()\n\n const exists = await quick(['show-ref', '--verify', `refs/heads/${branch}`], root)\n if (exists.ok) {\n // Reuse the fixed branch name, but guarantee a FRESH baseline: drop\n // any stale worktree at the path, move the branch to the current\n // HEAD, then check the branch out again (每次全新,复用仅作选项保留).\n await heavy(['worktree', 'remove', '--force', path], root)\n await heavy(['worktree', 'prune'], root)\n const moved = await heavy(['branch', '-f', branch, 'HEAD'], root)\n if (!moved.ok) return undefined\n const added = await heavy(['worktree', 'add', path, branch], root)\n if (!added.ok) return undefined\n } else {\n const added = await heavy(['worktree', 'add', '-b', branch, path], root)\n if (!added.ok) return undefined\n }\n return { path, branch, baseCommit }\n }),\n\n async collect(worktreePath, baseCommit) {\n const facts: SettlementFacts = { commits: [], commitsTotal: 0, dirtyFiles: [], dirtyFilesTotal: 0, changedFiles: 0 }\n const range = `${baseCommit}..HEAD`\n\n const head = await quick(['rev-parse', 'HEAD'], worktreePath)\n if (head.ok) facts.headCommit = head.stdout.trim()\n\n const log = await quick(['log', '--pretty=format:%h %s', range], worktreePath)\n if (log.ok) {\n const commits = log.stdout.split('\\n')\n .map(line => line.trim())\n .filter(line => line.length > 0)\n .map(line => {\n const space = line.indexOf(' ')\n return space === -1\n ? { hash: line, subject: '' }\n : { hash: line.slice(0, space), subject: line.slice(space + 1) }\n })\n // Evidence caps (0.3.1): the ledger is rewritten whole on every\n // mutation — cap what a huge branch/status dump can add to it.\n facts.commitsTotal = commits.length\n facts.commits = commits.slice(0, MAX_COMMIT_EVIDENCE)\n }\n\n const status = await quick(['status', '--porcelain'], worktreePath)\n if (status.ok) {\n const dirty = status.stdout.split('\\n').map(l => l.trim()).filter(l => l.length > 0)\n facts.dirtyFilesTotal = dirty.length\n facts.dirtyFiles = dirty.slice(0, MAX_DIRTY_EVIDENCE)\n }\n\n const shortstat = await quick(['diff', '--shortstat', range], worktreePath)\n if (shortstat.ok && shortstat.stdout.trim().length > 0) facts.diffStat = shortstat.stdout.trim()\n\n const names = await quick(['diff', '--name-only', range], worktreePath)\n if (names.ok) facts.changedFiles = names.stdout.split('\\n').filter(l => l.trim().length > 0).length\n\n return facts\n },\n\n merge: (root, branch) => withRootLock(root, async () => {\n // Main-clean check. The plugin's own worktree directory\n // (<root>/.dsh-worktrees) shows up as untracked noise and is EXEMPT —\n // otherwise merging would be impossible without gitignoring it first.\n const status = await quick(['status', '--porcelain'], root)\n if (status.ok) {\n const dirtyLines = status.stdout.split('\\n')\n .map(l => l.trim())\n .filter(l => {\n if (l.length === 0) return false\n const path = l.slice(3)\n return path !== WORKTREE_DIR && !path.startsWith(`${WORKTREE_DIR}/`)\n })\n if (dirtyLines.length > 0) {\n throw new Error(`主工作区有 ${dirtyLines.length} 处未提交修改,请先提交或暂存后再合并`)\n }\n }\n const merged = await heavy(['merge', '--no-ff', '--no-edit', branch], root)\n if (!merged.ok) {\n // Roll the half-finished merge back so the main worktree stays usable;\n // report the ORIGINAL failure verbatim (不自动解决冲突).\n await heavy(['merge', '--abort'], root)\n throw new Error(`合并失败:${merged.stderr.trim().slice(0, 300)}`)\n }\n }),\n\n async isAncestor(root, branch) {\n // exit 0 = branch is an ancestor of (or equal to) HEAD → merge no-op.\n const r = await quick(['merge-base', '--is-ancestor', branch, 'HEAD'], root)\n return r.ok\n },\n\n removeWorktree: (root, worktreePath) => withRootLock(root, async () => {\n const status = await quick(['status', '--porcelain'], worktreePath)\n if (status.ok && status.stdout.trim().length > 0) {\n const lines = status.stdout.split('\\n').map(l => l.trim()).filter(l => l.length > 0)\n throw new Error(`worktree 有 ${lines.length} 处未提交修改,拒绝删除:\\n${lines.slice(0, 10).join('\\n')}`)\n }\n const removed = await heavy(['worktree', 'remove', worktreePath], root)\n if (!removed.ok) throw new Error(`删除 worktree 失败:${(removed.stderr.trim() || removed.stdout.trim()).slice(0, 300)}`)\n }),\n\n deleteBranch: (root, branch) => withRootLock(root, async () => {\n const deleted = await heavy(['branch', '-D', branch], root)\n if (!deleted.ok) throw new Error(`删除分支失败:${deleted.stderr.trim().slice(0, 300)}`)\n }),\n\n async showCommit(cwd, hash) {\n if (!isHash(hash)) return undefined\n const r = await quick(['show', '--no-color', '--format=medium', hash], cwd)\n if (!r.ok || r.stdout.trim().length === 0) return undefined\n return capDiff(r.stdout)\n },\n\n async showPathDiff(cwd, path, baseCommit) {\n const target = path.trim()\n if (target.length === 0) return undefined\n if (baseCommit !== undefined && isHash(baseCommit)) {\n const r = await quick(['diff', '--no-color', `${baseCommit}..HEAD`, '--', target], cwd)\n if (!r.ok) return undefined\n if (r.stdout.trim().length === 0) return { text: '(该文件无差异)', truncated: false }\n return capDiff(r.stdout)\n }\n // Working-tree view: staged + unstaged vs HEAD.\n const r = await quick(['diff', '--no-color', 'HEAD', '--', target], cwd)\n if (r.ok && r.stdout.trim().length > 0) return capDiff(r.stdout)\n // Untracked files never appear in `git diff` — detect one and synthesize\n // its new-file patch via --no-index (which exits 1 on differences, so\n // its stdout is trusted whenever it carries a diff header).\n const st = await quick(['status', '--porcelain', '--', target], cwd)\n if (r.ok && st.ok && st.stdout.trim().startsWith('??')) {\n const ni = await exec(['diff', '--no-color', '--no-index', '--', '/dev/null', target], { cwd, timeout: QUICK_TIMEOUT_MS })\n if (ni.stdout.includes('diff --git')) return capDiff(ni.stdout)\n return { text: `(未跟踪新文件:${target})`, truncated: false }\n }\n if (!r.ok) return undefined\n return { text: '(该文件无差异)', truncated: false }\n },\n }\n}\n"],"mappings":"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"}
@@ -14,7 +14,8 @@
14
14
  const TASKBOARD_PROTOCOL = [
15
15
  "本机已安装 dsh-taskboard 插件(DSH 任务看板):任务挂在项目(DSH workspace)上,",
16
16
  "用 taskboard_* 工具读写;人在 Web GUI 看板上实时看到同样数据。能力:查板(list/get)、",
17
- "建卡(create)、改卡(update)、移卡(move)、评论(comment_add/comments)、删除(delete=仅标记)",
17
+ "建卡(create)、改卡(update)、移卡(move)、评论(comment_add/comments)、删除(delete=仅标记)",
18
+ "验收清单(checklist)、执行报告(execution_report)。",
18
19
  "任务带紧急度(urgent红/normal紫/relaxed蓝)、执行方式(claim认领/scheduled定时)与可选指定模型。",
19
20
  "工作纪律:",
20
21
  "1. 开工先查板:开始工作前先 taskboard_list(按本项目过滤、status=todo),有可认领任务时按纪律认领。",
@@ -22,8 +23,9 @@ const TASKBOARD_PROTOCOL = [
22
23
  "3. 先认领再干活:把 todo→in_progress(带 ifVersion)成功后,才开始读代码/分析实现;",
23
24
  " 认领失败(版本冲突/项目边界不符/已被他人持有)就停止并报告,绝不循环重试或接管他人任务。",
24
25
  "4. 版本冲突只重试一次:ifVersion 冲突时重新读卡,仅当状态仍可认领且需求未变时用新版本号重试一次,再失败即停止报告。",
25
- "5. 验收交接:实现并自验后,评论记录(改动/验证结果/剩余风险),再把 in_progress→in_review。",
26
- "6. 完成须用户确认:你永远不能把任务移到 done——那是用户的确认动作;blocked=无法继续,canceled=不再继续。",
26
+ "5. 验收交接:实现并自验后,taskboard_execution_report 提交结构化报告(摘要/改动文件/自验/风险),",
27
+ " 评论记录补充细节,再把 in_progress→in_review;任务带验收清单时用 taskboard_checklist 逐项勾选(附证据)。",
28
+ "6. 完成须用户确认:你永远不能把任务移到 done——那是用户的确认动作;清单全勾也不等于完成;blocked=无法继续,canceled=不再继续。",
27
29
  "7. backlog=未授权:backlog 任务不算批准执行,被指派也不是授权,除非用户明确要求。",
28
30
  "8. 模型与定时只读:任务的 model 与 execution 配置归创建者/用户所有,update 工具不允许你修改这两个字段。",
29
31
  "项目边界:只有属于任务所在项目的会话才能认领(todo→in_progress)或执行它。",
@@ -1 +1 @@
1
- {"version":3,"file":"protocol-text.js","names":[],"sources":["../../src/host/protocol-text.ts"],"sourcesContent":["/**\n * The agent workflow protocol text — the single source the system-prompt\n * section serves. This is a behavioral contract, not a feature ad: claiming\n * discipline, optimistic-version retry rules, review handoff, and the\n * user-only completion gate.\n *\n * The regression test (tests/protocol.spec.ts) locks the discipline\n * sentences, so editing the text without revisiting the test fails loud.\n *\n * @module dsh-taskboard/host/protocol-text\n */\n\n/** The protocol section served to every agent (Chinese UI deployment). */\nexport const TASKBOARD_PROTOCOL = [\n '本机已安装 dsh-taskboard 插件(DSH 任务看板):任务挂在项目(DSH workspace)上,',\n '用 taskboard_* 工具读写;人在 Web GUI 看板上实时看到同样数据。能力:查板(list/get)、',\n '建卡(create)、改卡(update)、移卡(move)、评论(comment_add/comments)、删除(delete=仅标记)。',\n '任务带紧急度(urgent红/normal紫/relaxed蓝)、执行方式(claim认领/scheduled定时)与可选指定模型。',\n '工作纪律:',\n '1. 开工先查板:开始工作前先 taskboard_list(按本项目过滤、status=todo),有可认领任务时按纪律认领。',\n '2. 先读后动:动卡前先 taskboard_get 并读评论;评论视为最新需求,若要求等待/暂缓,停下汇报,不改状态。',\n '3. 先认领再干活:把 todo→in_progress(带 ifVersion)成功后,才开始读代码/分析实现;',\n ' 认领失败(版本冲突/项目边界不符/已被他人持有)就停止并报告,绝不循环重试或接管他人任务。',\n '4. 版本冲突只重试一次:ifVersion 冲突时重新读卡,仅当状态仍可认领且需求未变时用新版本号重试一次,再失败即停止报告。',\n '5. 验收交接:实现并自验后,评论记录(改动/验证结果/剩余风险),再把 in_progress→in_review',\n '6. 完成须用户确认:你永远不能把任务移到 done——那是用户的确认动作;blocked=无法继续,canceled=不再继续。',\n '7. backlog=未授权:backlog 任务不算批准执行,被指派也不是授权,除非用户明确要求。',\n '8. 模型与定时只读:任务的 model 与 execution 配置归创建者/用户所有,update 工具不允许你修改这两个字段。',\n '项目边界:只有属于任务所在项目的会话才能认领(todo→in_progress)或执行它。',\n '用户提到「任务看板/看板/认领任务」时即指本插件,请据此协作。',\n].join('\\n')\n\n/** Section order inside the tool-guidance band (100–199). */\nexport const PROTOCOL_SECTION_ORDER = 180\n\n/** Registered section name. */\nexport const PROTOCOL_SECTION_NAME = 'plugin:dsh-taskboard'\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,qBAAqB;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,IAAI;;AAMX,MAAa,wBAAwB"}
1
+ {"version":3,"file":"protocol-text.js","names":[],"sources":["../../src/host/protocol-text.ts"],"sourcesContent":["/**\n * The agent workflow protocol text — the single source the system-prompt\n * section serves. This is a behavioral contract, not a feature ad: claiming\n * discipline, optimistic-version retry rules, review handoff, and the\n * user-only completion gate.\n *\n * The regression test (tests/protocol.spec.ts) locks the discipline\n * sentences, so editing the text without revisiting the test fails loud.\n *\n * @module dsh-taskboard/host/protocol-text\n */\n\n/** The protocol section served to every agent (Chinese UI deployment). */\nexport const TASKBOARD_PROTOCOL = [\n '本机已安装 dsh-taskboard 插件(DSH 任务看板):任务挂在项目(DSH workspace)上,',\n '用 taskboard_* 工具读写;人在 Web GUI 看板上实时看到同样数据。能力:查板(list/get)、',\n '建卡(create)、改卡(update)、移卡(move)、评论(comment_add/comments)、删除(delete=仅标记)、',\n '验收清单(checklist)、执行报告(execution_report)。',\n '任务带紧急度(urgent红/normal紫/relaxed蓝)、执行方式(claim认领/scheduled定时)与可选指定模型。',\n '工作纪律:',\n '1. 开工先查板:开始工作前先 taskboard_list(按本项目过滤、status=todo),有可认领任务时按纪律认领。',\n '2. 先读后动:动卡前先 taskboard_get 并读评论;评论视为最新需求,若要求等待/暂缓,停下汇报,不改状态。',\n '3. 先认领再干活:把 todo→in_progress(带 ifVersion)成功后,才开始读代码/分析实现;',\n ' 认领失败(版本冲突/项目边界不符/已被他人持有)就停止并报告,绝不循环重试或接管他人任务。',\n '4. 版本冲突只重试一次:ifVersion 冲突时重新读卡,仅当状态仍可认领且需求未变时用新版本号重试一次,再失败即停止报告。',\n '5. 验收交接:实现并自验后,taskboard_execution_report 提交结构化报告(摘要/改动文件/自验/风险),',\n ' 评论记录补充细节,再把 in_progress→in_review;任务带验收清单时用 taskboard_checklist 逐项勾选(附证据)。',\n '6. 完成须用户确认:你永远不能把任务移到 done——那是用户的确认动作;清单全勾也不等于完成;blocked=无法继续,canceled=不再继续。',\n '7. backlog=未授权:backlog 任务不算批准执行,被指派也不是授权,除非用户明确要求。',\n '8. 模型与定时只读:任务的 model 与 execution 配置归创建者/用户所有,update 工具不允许你修改这两个字段。',\n '项目边界:只有属于任务所在项目的会话才能认领(todo→in_progress)或执行它。',\n '用户提到「任务看板/看板/认领任务」时即指本插件,请据此协作。',\n].join('\\n')\n\n/** Section order inside the tool-guidance band (100–199). */\nexport const PROTOCOL_SECTION_ORDER = 180\n\n/** Registered section name. */\nexport const PROTOCOL_SECTION_NAME = 'plugin:dsh-taskboard'\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,qBAAqB;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,IAAI;;AAMX,MAAa,wBAAwB"}