dsh-taskboard 0.4.5 → 0.5.1

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 (50) hide show
  1. package/README.md +24 -1
  2. package/lib/client.js +434 -193
  3. package/lib/host/execution.js +80 -33
  4. package/lib/host/execution.js.map +1 -1
  5. package/lib/host/git.js +49 -5
  6. package/lib/host/git.js.map +1 -1
  7. package/lib/host/routes.js +210 -112
  8. package/lib/host/routes.js.map +1 -1
  9. package/lib/host/scheduler.js +50 -28
  10. package/lib/host/scheduler.js.map +1 -1
  11. package/lib/host/sdk.js +7 -2
  12. package/lib/host/sdk.js.map +1 -1
  13. package/lib/host/store.js +41 -8
  14. package/lib/host/store.js.map +1 -1
  15. package/lib/host/templates.js +10 -3
  16. package/lib/host/templates.js.map +1 -1
  17. package/lib/host/tools.js +128 -97
  18. package/lib/host/tools.js.map +1 -1
  19. package/lib/index.js +3 -1
  20. package/lib/index.js.map +1 -1
  21. package/lib/shared/api.js.map +1 -1
  22. package/lib/shared/protocol.js +48 -5
  23. package/lib/shared/protocol.js.map +1 -1
  24. package/package.json +9 -8
  25. package/src/client/api.ts +26 -8
  26. package/src/client/board/ImportModal.tsx +1 -1
  27. package/src/client/board/SettingsModal.tsx +84 -0
  28. package/src/client/board/TaskBoard.tsx +47 -40
  29. package/src/client/board/TaskCard.tsx +3 -5
  30. package/src/client/board/TaskDetail.tsx +30 -21
  31. package/src/client/board/TaskFormModal.tsx +39 -31
  32. package/src/client/board/format.ts +26 -0
  33. package/src/client/board/labels.ts +44 -0
  34. package/src/client/controller.ts +86 -34
  35. package/src/client/index.ts +7 -5
  36. package/src/client/sidebar-entry.ts +5 -1
  37. package/src/client/styles.ts +4 -0
  38. package/src/host/execution.ts +90 -16
  39. package/src/host/git.ts +39 -10
  40. package/src/host/routes.ts +263 -128
  41. package/src/host/scheduler.ts +62 -36
  42. package/src/host/sdk.ts +12 -1
  43. package/src/host/store.ts +53 -7
  44. package/src/host/templates.ts +12 -3
  45. package/src/host/tools.ts +187 -126
  46. package/src/index.ts +10 -1
  47. package/src/shared/api.ts +11 -2
  48. package/src/shared/protocol.ts +83 -6
  49. package/src/shared/version.ts +1 -1
  50. package/src/client/board/NewTaskModal.tsx +0 -8
@@ -107,11 +107,20 @@ export const URGENCY_COLOR: Readonly<Record<Urgency, string>> = {
107
107
  * - `worktree`: each execution runs in a fresh `git worktree` on a dedicated
108
108
  * task branch (`task/<标题>+<taskId>`) under `<workspace>/.dsh-worktrees/`.
109
109
  * - `none`: run in the workspace directory as before, zero git interaction.
110
- * Omitted = the default `worktree`; non-git projects auto-degrade at run
111
- * time (the execution record carries an `isolationNote` explaining why).
110
+ * Omitted = {@link DEFAULT_ISOLATION}; since 0.5.0 creation materializes the
111
+ * board default onto the record (看板设置 执行隔离), and non-git projects
112
+ * still auto-degrade at run time (the execution record carries an
113
+ * `isolationNote` explaining why).
112
114
  */
113
115
  export type IsolationMode = 'worktree' | 'none'
114
116
 
117
+ /**
118
+ * Factory-default isolation (0.5.0): 原目录执行. Applies when neither the
119
+ * task record nor the board setting (`BoardSettings.defaultIsolation`)
120
+ * says otherwise. Before 0.5.0 the implicit default was 'worktree'.
121
+ */
122
+ export const DEFAULT_ISOLATION: IsolationMode = 'none'
123
+
115
124
  /** Validate an isolation value. */
116
125
  export function asIsolation(raw: string): IsolationMode {
117
126
  if (raw !== 'worktree' && raw !== 'none') {
@@ -120,9 +129,39 @@ export function asIsolation(raw: string): IsolationMode {
120
129
  return raw
121
130
  }
122
131
 
123
- /** Resolve a task's effective isolation (omitted → the worktree default). */
132
+ /** Resolve a task's effective isolation (omitted → the factory default). */
124
133
  export function effectiveIsolation(task: Pick<TaskRecord, 'isolation'>): IsolationMode {
125
- return task.isolation === undefined ? 'worktree' : task.isolation
134
+ return task.isolation === undefined ? DEFAULT_ISOLATION : task.isolation
135
+ }
136
+
137
+ /**
138
+ * Board-level settings persisted with the ledger (0.5.0). Only fields the
139
+ * user explicitly set are present; absent fields follow factory defaults.
140
+ */
141
+ export type BoardSettings = {
142
+ /** Default code isolation applied when a NEW task is created without an explicit choice. */
143
+ defaultIsolation?: IsolationMode
144
+ }
145
+
146
+ /** Validate raw input into sanitized {@link BoardSettings} (unknown fields dropped). */
147
+ export function asBoardSettings(raw: unknown): BoardSettings {
148
+ if (typeof raw !== 'object' || raw === null) {
149
+ throw new Error('board settings must be an object')
150
+ }
151
+ const e = raw as Record<string, unknown>
152
+ const out: BoardSettings = {}
153
+ if (e.defaultIsolation !== undefined) {
154
+ if (typeof e.defaultIsolation !== 'string') {
155
+ throw new Error("defaultIsolation must be 'worktree' or 'none'")
156
+ }
157
+ out.defaultIsolation = asIsolation(e.defaultIsolation)
158
+ }
159
+ return out
160
+ }
161
+
162
+ /** The effective default isolation for NEW tasks (board setting → factory default). */
163
+ export function defaultIsolationOf(settings?: BoardSettings): IsolationMode {
164
+ return settings?.defaultIsolation ?? DEFAULT_ISOLATION
126
165
  }
127
166
 
128
167
  /** How a task may run. */
@@ -247,6 +286,7 @@ export function nextCronTime(match: CronMatch, from: number): number | null {
247
286
  export type Actor =
248
287
  | { kind: 'user' }
249
288
  | { kind: 'agent'; sessionId: string }
289
+ | { kind: 'system' }
250
290
 
251
291
  /** A progress/report comment on a task. */
252
292
  export type CommentRecord = {
@@ -419,6 +459,8 @@ export type TaskLedger = {
419
459
  /** Global monotonic revision; every mutation bumps it. */
420
460
  revision: number
421
461
  tasks: TaskRecord[]
462
+ /** Board-level settings (0.5.0); absent on ledgers never touched by 设置. */
463
+ settings?: BoardSettings
422
464
  }
423
465
 
424
466
  /** Current ledger format version. */
@@ -438,6 +480,16 @@ function suffix(): string {
438
480
  return Math.random().toString(36).slice(2, 8)
439
481
  }
440
482
 
483
+ /**
484
+ * Legal task id charset (R4): `t-<base36>-<base36>` from {@link newTaskId},
485
+ * and the ONLY shape accepted from the outside (import) or used to build
486
+ * filesystem paths (worktree dirs). Ids ride into `join(ws, '.dsh-worktrees',
487
+ * id)` — a lax charset here is an arbitrary-directory delete primitive.
488
+ */
489
+ export function isValidTaskId(id: string): boolean {
490
+ return /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/.test(id)
491
+ }
492
+
441
493
  /** Mint a task id. */
442
494
  export function newTaskId(): string {
443
495
  return `t-${Date.now().toString(36)}-${suffix()}`
@@ -740,6 +792,8 @@ export type ImportPlan = {
740
792
  overwrite: TaskRecord[]
741
793
  /** Invalid entries with a human-readable reason (never imported). */
742
794
  invalid: Array<{ id?: string; reason: string }>
795
+ /** The file's board settings (0.5.0); replace-mode swaps them, merge keeps the live ones. */
796
+ settings?: BoardSettings
743
797
  }
744
798
 
745
799
  /** One unknown-value read helper: string fields with defaults. */
@@ -768,7 +822,10 @@ export function validateImportedTask(raw: unknown, now: number): { ok: true; tas
768
822
  const e = raw as Record<string, unknown>
769
823
  const id = typeof e.id === 'string' ? e.id.trim() : ''
770
824
  const fail = (reason: string): { ok: false; reason: string } => ({ ok: false, reason })
771
- if (id.length === 0 || id.length > 100) return fail('missing/invalid id')
825
+ // R4①: length alone let traversal-shaped ids (`../../x`, `..\..\x`) into
826
+ // the ledger; the charset gate is the primary defense for every downstream
827
+ // filesystem use of a task id.
828
+ if (!isValidTaskId(id)) return fail('missing/invalid id (must match ^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$)')
772
829
  try {
773
830
  const execution = normalizeExecution(
774
831
  typeof e.execution === 'object' && e.execution !== null ? e.execution as { mode?: string; cron?: string } : {},
@@ -866,6 +923,26 @@ export function validateImportedTask(raw: unknown, now: number): { ok: true; tas
866
923
  }
867
924
  }
868
925
 
926
+ /**
927
+ * Minimal structural check for ONE ledger record at load time (S11): unlike
928
+ * {@link validateImportedTask} this REBUILDS NOTHING (cron state, ids and
929
+ * timestamps must survive a load untouched) — it only rejects entries whose
930
+ * shape would break downstream consumers, including the R4 id charset.
931
+ * @param raw - the untyped record.
932
+ */
933
+ export function isPlausibleTaskRecord(raw: unknown): boolean {
934
+ if (typeof raw !== 'object' || raw === null) return false
935
+ const t = raw as Record<string, unknown>
936
+ return typeof t.id === 'string' && isValidTaskId(t.id)
937
+ && typeof t.title === 'string' && t.title.length > 0
938
+ && typeof t.workspaceId === 'string' && t.workspaceId.length > 0
939
+ && ALL_STATUSES.includes(t.status as TaskStatus)
940
+ && typeof t.version === 'number' && Number.isFinite(t.version) && t.version >= 1
941
+ && Array.isArray(t.comments) && Array.isArray(t.executions)
942
+ && typeof t.execution === 'object' && t.execution !== null
943
+ && (t.execution as { mode?: unknown }).mode !== undefined
944
+ }
945
+
869
946
  /**
870
947
  * Validate a whole imported ledger and classify its tasks against the live
871
948
  * one (pure). Duplicate ids INSIDE the file are invalid (first wins, later
@@ -882,7 +959,7 @@ export function validateLedgerImport(raw: unknown, knownIds: ReadonlySet<string>
882
959
  throw new Error(`不支持的 schemaVersion ${String(e.schemaVersion)}(当前支持 ${LEDGER_SCHEMA_VERSION})`)
883
960
  }
884
961
  if (!Array.isArray(e.tasks)) throw new Error('导入文件的 tasks 不是数组')
885
- const plan: ImportPlan = { create: [], overwrite: [], invalid: [] }
962
+ const plan: ImportPlan = { create: [], overwrite: [], invalid: [], ...(e.settings !== undefined ? { settings: asBoardSettings(e.settings) } : {}) }
886
963
  const seen = new Set<string>()
887
964
  for (const entry of e.tasks) {
888
965
  const id = typeof (entry as { id?: unknown })?.id === 'string' ? (entry as { id: string }).id : undefined
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  /** The package version (must equal package.json "version"). */
9
- export const PLUGIN_VERSION = '0.4.5'
9
+ export const PLUGIN_VERSION = '0.5.1'
@@ -1,8 +0,0 @@
1
- /**
2
- * Compatibility shim: the composer moved into TaskFormModal (create + edit
3
- * in one dialog). Kept so existing imports keep working.
4
- *
5
- * @module dsh-taskboard/client/board/NewTaskModal
6
- */
7
- export { TaskFormModal as NewTaskModal } from './TaskFormModal.tsx'
8
- export type { CatalogModel } from './TaskFormModal.tsx'