dsh-taskboard 0.6.2 → 0.6.4
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 +18 -3
- package/lib/client.js +13 -4
- package/lib/host/execution.js +160 -78
- package/lib/host/execution.js.map +1 -1
- package/lib/host/git.js +41 -13
- package/lib/host/git.js.map +1 -1
- package/lib/host/isolation.js +192 -0
- package/lib/host/isolation.js.map +1 -0
- package/lib/host/repos.js +91 -0
- package/lib/host/repos.js.map +1 -0
- package/lib/host/routes.js +205 -60
- package/lib/host/routes.js.map +1 -1
- package/lib/host/tools.js +1 -1
- package/lib/host/tools.js.map +1 -1
- package/lib/index.js +4 -0
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +62 -1
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +5 -2
- package/src/client/api.ts +2 -1
- package/src/client/board/TaskDetail.tsx +100 -32
- package/src/client/board/TaskFormModal.tsx +7 -0
- package/src/client/controller.ts +15 -6
- package/src/client/i18n/en.ts +5 -1
- package/src/client/i18n/runtime.ts +71 -5
- package/src/client/i18n/zh.ts +5 -1
- package/src/client/index.ts +5 -2
- package/src/client/styles.ts +7 -0
- package/src/host/execution.ts +199 -115
- package/src/host/git.ts +84 -18
- package/src/host/isolation.ts +268 -0
- package/src/host/repos.ts +146 -0
- package/src/host/routes.ts +211 -73
- package/src/host/tools.ts +2 -2
- package/src/index.ts +6 -1
- package/src/shared/api.ts +29 -5
- package/src/shared/protocol.ts +124 -0
- package/src/shared/version.ts +1 -1
package/src/shared/protocol.ts
CHANGED
|
@@ -350,6 +350,61 @@ export type CommentRecord = {
|
|
|
350
350
|
/** One commit produced by an isolated execution (hash + subject). */
|
|
351
351
|
export type CommitInfo = { hash: string; subject: string }
|
|
352
352
|
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// Multi-repo mirror isolation (0.6.3)
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Max repos ONE task mirror may cover (0.6.3): the workspace root repo plus
|
|
359
|
+
* its parallel nested repos. A workspace discovering more degrades the whole
|
|
360
|
+
* worktree run to the original directory (plan §4.1 — predictable, and never
|
|
361
|
+
* a half-built mirror).
|
|
362
|
+
*/
|
|
363
|
+
export const MAX_MIRROR_REPOS = 8
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Whether `rel` is a legal repo-path key (0.6.3): `''` = the workspace root
|
|
367
|
+
* repo; otherwise a relative forward-slash path with no traversal/absolute
|
|
368
|
+
* shape and no dot segments. These keys ride into filesystem joins
|
|
369
|
+
* (`<workspace>/<rel>`) and ledger maps — the same R4 paranoia as task ids.
|
|
370
|
+
*/
|
|
371
|
+
export function isValidRelRepoPath(rel: string): boolean {
|
|
372
|
+
if (rel === '') return true
|
|
373
|
+
if (rel.length === 0 || rel.length > 300) return false
|
|
374
|
+
if (/^[A-Za-z]:[\\/]/.test(rel) || rel.startsWith('\\\\') || rel.startsWith('/')) return false
|
|
375
|
+
const parts = rel.split('/')
|
|
376
|
+
return parts.every(p =>
|
|
377
|
+
p.length > 0 && p !== '.' && p !== '..' && !p.startsWith('.') && !/[\\:*?"<>|]/.test(p))
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Per-repo isolation + evidence facts for one execution (0.6.3 multi-repo
|
|
382
|
+
* mirror). Present only when the mirror covers MORE than the workspace root
|
|
383
|
+
* repo alone (or has skipped repos) — a plain single-repo run keeps the
|
|
384
|
+
* legacy flat fields above, untouched, so old ledgers/clients stay blind to
|
|
385
|
+
* this. The root repo (when mirrored) ALSO dual-writes the flat fields.
|
|
386
|
+
*/
|
|
387
|
+
export type ExecutionRepoEvidence = {
|
|
388
|
+
/** Repo path relative to the workspace ('' = the workspace root repo). */
|
|
389
|
+
repo: string
|
|
390
|
+
/** The task branch checked out in this repo's worktree. */
|
|
391
|
+
branch: string
|
|
392
|
+
/** Absolute path of the repo's worktree inside the task mirror. */
|
|
393
|
+
worktreePath: string
|
|
394
|
+
/** HEAD of the task branch before the execution started. */
|
|
395
|
+
baseCommit?: string
|
|
396
|
+
/** HEAD at settlement. */
|
|
397
|
+
headCommit?: string
|
|
398
|
+
/** Commits between baseCommit and headCommit (capped, newest first). */
|
|
399
|
+
commits?: CommitInfo[]
|
|
400
|
+
commitsTotal?: number
|
|
401
|
+
/** Uncommitted changes at settlement (`status --porcelain` lines, capped). */
|
|
402
|
+
dirtyFiles?: string[]
|
|
403
|
+
dirtyFilesTotal?: number
|
|
404
|
+
diffStat?: string
|
|
405
|
+
changedFiles?: number
|
|
406
|
+
}
|
|
407
|
+
|
|
353
408
|
/**
|
|
354
409
|
* The structured execution report an agent submits at handoff (0.4.0).
|
|
355
410
|
* Commits/dirty/diff facts are host-collected git evidence — the report
|
|
@@ -417,6 +472,8 @@ export type ExecutionRecord = {
|
|
|
417
472
|
diffStat?: string
|
|
418
473
|
/** How many files differ between baseCommit and headCommit. */
|
|
419
474
|
changedFiles?: number
|
|
475
|
+
/** Per-repo facts of a multi-repo mirror run (0.6.3; absent on single-repo runs). */
|
|
476
|
+
repos?: ExecutionRepoEvidence[]
|
|
420
477
|
/** The agent's structured report, submitted via taskboard_execution_report. */
|
|
421
478
|
report?: ExecutionReport
|
|
422
479
|
}
|
|
@@ -466,8 +523,17 @@ export type TaskRecord = {
|
|
|
466
523
|
/**
|
|
467
524
|
* The task branch fixed at the FIRST worktree creation (`task/<标题>+<taskId>`).
|
|
468
525
|
* Renaming the task afterwards never changes it (history preservation).
|
|
526
|
+
* Multi-repo mirror runs (0.6.3): this stays the WORKSPACE ROOT repo's
|
|
527
|
+
* branch; every nested repo pins its own under {@link branches}.
|
|
469
528
|
*/
|
|
470
529
|
branch?: string
|
|
530
|
+
/**
|
|
531
|
+
* Per-repo task branches of a multi-repo mirror task (0.6.3): repo path
|
|
532
|
+
* relative to the workspace (never `''` — the root uses {@link branch})
|
|
533
|
+
* → branch name. Pinned at the repo's FIRST successful worktree creation,
|
|
534
|
+
* same rename-proof semantics as {@link branch}.
|
|
535
|
+
*/
|
|
536
|
+
branches?: Record<string, string>
|
|
471
537
|
/**
|
|
472
538
|
* The session currently holding the in-progress claim (explicit claim or a
|
|
473
539
|
* live execution). Present only while `status === 'in_progress'`: any move
|
|
@@ -863,6 +929,61 @@ function numOr(raw: Record<string, unknown>, key: string, fallback: number): num
|
|
|
863
929
|
return typeof v === 'number' && Number.isFinite(v) ? v : fallback
|
|
864
930
|
}
|
|
865
931
|
|
|
932
|
+
/** Evidence caps for imported per-repo facts (aligns the host's collect caps). */
|
|
933
|
+
const IMPORT_COMMIT_CAP = 50
|
|
934
|
+
const IMPORT_DIRTY_CAP = 100
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Sanitize an imported per-task branches map (0.6.3): legal repo keys only,
|
|
938
|
+
* non-empty branch strings, capped at {@link MAX_MIRROR_REPOS} entries.
|
|
939
|
+
* @returns undefined when nothing legal remains.
|
|
940
|
+
*/
|
|
941
|
+
export function normalizeBranchesMap(raw: unknown): Record<string, string> | undefined {
|
|
942
|
+
if (typeof raw !== 'object' || raw === null) return undefined
|
|
943
|
+
const out: Record<string, string> = {}
|
|
944
|
+
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
945
|
+
if (Object.keys(out).length >= MAX_MIRROR_REPOS) break
|
|
946
|
+
if (typeof value !== 'string' || value.trim().length === 0 || value.length > 200) continue
|
|
947
|
+
if (key === '' || !isValidRelRepoPath(key)) continue
|
|
948
|
+
out[key] = value.trim()
|
|
949
|
+
}
|
|
950
|
+
return Object.keys(out).length > 0 ? out : undefined
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Sanitize ONE imported per-repo evidence entry (0.6.3): rebuilds the record
|
|
955
|
+
* field by field, capping evidence arrays like the host's own collection.
|
|
956
|
+
* @returns undefined for a structurally illegal entry (dropped, not fatal).
|
|
957
|
+
*/
|
|
958
|
+
export function normalizeRepoEvidence(raw: unknown): ExecutionRepoEvidence | undefined {
|
|
959
|
+
if (typeof raw !== 'object' || raw === null) return undefined
|
|
960
|
+
const e = raw as Record<string, unknown>
|
|
961
|
+
if (typeof e.repo !== 'string' || !isValidRelRepoPath(e.repo)) return undefined
|
|
962
|
+
if (typeof e.branch !== 'string' || e.branch.length === 0 || e.branch.length > 200) return undefined
|
|
963
|
+
if (typeof e.worktreePath !== 'string' || e.worktreePath.length === 0 || e.worktreePath.length > 1000) return undefined
|
|
964
|
+
const commits = Array.isArray(e.commits)
|
|
965
|
+
? e.commits.filter((c): c is CommitInfo =>
|
|
966
|
+
typeof c === 'object' && c !== null && typeof (c as CommitInfo).hash === 'string'
|
|
967
|
+
&& typeof (c as CommitInfo).subject === 'string').slice(0, IMPORT_COMMIT_CAP)
|
|
968
|
+
: undefined
|
|
969
|
+
const dirtyFiles = Array.isArray(e.dirtyFiles)
|
|
970
|
+
? e.dirtyFiles.filter((l): l is string => typeof l === 'string').slice(0, IMPORT_DIRTY_CAP)
|
|
971
|
+
: undefined
|
|
972
|
+
return {
|
|
973
|
+
repo: e.repo,
|
|
974
|
+
branch: e.branch,
|
|
975
|
+
worktreePath: e.worktreePath,
|
|
976
|
+
...(typeof e.baseCommit === 'string' ? { baseCommit: e.baseCommit.slice(0, 100) } : {}),
|
|
977
|
+
...(typeof e.headCommit === 'string' ? { headCommit: e.headCommit.slice(0, 100) } : {}),
|
|
978
|
+
...(commits !== undefined && commits.length > 0 ? { commits } : {}),
|
|
979
|
+
...(typeof e.commitsTotal === 'number' && Number.isFinite(e.commitsTotal) ? { commitsTotal: e.commitsTotal } : {}),
|
|
980
|
+
...(dirtyFiles !== undefined && dirtyFiles.length > 0 ? { dirtyFiles } : {}),
|
|
981
|
+
...(typeof e.dirtyFilesTotal === 'number' && Number.isFinite(e.dirtyFilesTotal) ? { dirtyFilesTotal: e.dirtyFilesTotal } : {}),
|
|
982
|
+
...(typeof e.diffStat === 'string' ? { diffStat: e.diffStat.slice(0, 500) } : {}),
|
|
983
|
+
...(typeof e.changedFiles === 'number' && Number.isFinite(e.changedFiles) ? { changedFiles: e.changedFiles } : {}),
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
866
987
|
/**
|
|
867
988
|
* Validate ONE imported task record (pure): rebuilds it field by field with
|
|
868
989
|
* the normal validators, minting missing ids and re-arming cron. Executions
|
|
@@ -936,6 +1057,7 @@ export function validateImportedTask(raw: unknown, now: number): { ok: true; tas
|
|
|
936
1057
|
...(typeof xe.dirtyFilesTotal === 'number' ? { dirtyFilesTotal: xe.dirtyFilesTotal } : {}),
|
|
937
1058
|
...(typeof xe.diffStat === 'string' ? { diffStat: xe.diffStat } : {}),
|
|
938
1059
|
...(typeof xe.changedFiles === 'number' ? { changedFiles: xe.changedFiles } : {}),
|
|
1060
|
+
...(Array.isArray(xe.repos) ? { repos: xe.repos.slice(0, MAX_MIRROR_REPOS).map(normalizeRepoEvidence).filter((r): r is ExecutionRepoEvidence => r !== undefined) } : {}),
|
|
939
1061
|
...(typeof xe.report === 'object' && xe.report !== null ? { report: normalizeExecutionReport(xe.report) } : {}),
|
|
940
1062
|
})
|
|
941
1063
|
}
|
|
@@ -944,6 +1066,7 @@ export function validateImportedTask(raw: unknown, now: number): { ok: true; tas
|
|
|
944
1066
|
const actorOf = (v: unknown): Actor => (typeof v === 'object' && v !== null && (v as Actor).kind === 'agent' && typeof (v as { sessionId?: unknown }).sessionId === 'string'
|
|
945
1067
|
? { kind: 'agent', sessionId: (v as { sessionId: string }).sessionId }
|
|
946
1068
|
: { kind: 'user' })
|
|
1069
|
+
const branchesMap = normalizeBranchesMap(e.branches)
|
|
947
1070
|
const task: TaskRecord = {
|
|
948
1071
|
id,
|
|
949
1072
|
title: normalizeTitle(strOr(e, 'title', '')),
|
|
@@ -960,6 +1083,7 @@ export function validateImportedTask(raw: unknown, now: number): { ok: true; tas
|
|
|
960
1083
|
...(typeof e.permission === 'string' ? { permission: asPermission(e.permission) } : {}),
|
|
961
1084
|
...(Array.isArray(e.checklist) ? { checklist: normalizeChecklist(e.checklist) } : {}),
|
|
962
1085
|
...(typeof e.branch === 'string' ? { branch: e.branch } : {}),
|
|
1086
|
+
...(branchesMap !== undefined ? { branches: branchesMap } : {}),
|
|
963
1087
|
...(status === 'in_progress' && typeof e.claimedBy === 'string' ? { claimedBy: e.claimedBy } : {}),
|
|
964
1088
|
...(status === 'in_progress' && typeof e.claimedAt === 'number' ? { claimedAt: e.claimedAt } : {}),
|
|
965
1089
|
version: Math.max(1, Math.trunc(numOr(e, 'version', 1))),
|
package/src/shared/version.ts
CHANGED