dsh-taskboard 0.1.2 → 0.2.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 +30 -3
- package/lib/client.js +743 -142
- package/lib/host/execution.js +156 -28
- package/lib/host/execution.js.map +1 -1
- package/lib/host/routes.js +30 -3
- package/lib/host/routes.js.map +1 -1
- package/lib/host/scheduler.js +10 -1
- package/lib/host/scheduler.js.map +1 -1
- package/lib/host/store.js +31 -18
- package/lib/host/store.js.map +1 -1
- package/lib/host/tools.js +14 -4
- package/lib/host/tools.js.map +1 -1
- package/lib/index.js +19 -4
- package/lib/index.js.map +1 -1
- package/lib/shared/protocol.js +53 -2
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +1 -1
- package/src/client/api.ts +3 -0
- package/src/client/board/TaskBoard.tsx +95 -13
- package/src/client/board/TaskCard.tsx +6 -3
- package/src/client/board/TaskDetail.tsx +81 -7
- package/src/client/board/TaskFormModal.tsx +1 -1
- package/src/client/controller.ts +163 -4
- package/src/client/index.ts +10 -0
- package/src/client/session-jump.ts +93 -0
- package/src/client/sidebar-entry.ts +98 -1
- package/src/client/styles.ts +56 -2
- package/src/host/execution.ts +190 -16
- package/src/host/routes.ts +38 -11
- package/src/host/scheduler.ts +20 -4
- package/src/host/store.ts +34 -13
- package/src/host/tools.ts +30 -8
- package/src/index.ts +27 -3
- package/src/shared/protocol.ts +72 -3
- package/src/shared/version.ts +9 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduler.js","names":[],"sources":["../../src/host/scheduler.ts"],"sourcesContent":["/**\n * Host-side cron scheduler: one tick per minute over the ledger's scheduled\n * tasks. A due task (nextRunAt reached, not running, not trashed) first has\n * its next run advanced to the next cron match — then it executes through\n * the same path as the manual button. Missed windows (host was down, tab\n * closed — irrelevant here, this is the host process) simply advance: a\n * nextRunAt more than one window in the past is skipped, not caught up.\n *\n * @module dsh-taskboard/host/scheduler\n */\nimport { nextCronTime, parseCron, type TaskLedger } from '../shared/protocol.ts'\nimport type
|
|
1
|
+
{"version":3,"file":"scheduler.js","names":[],"sources":["../../src/host/scheduler.ts"],"sourcesContent":["/**\n * Host-side cron scheduler: one tick per minute over the ledger's scheduled\n * tasks. A due task (nextRunAt reached, not running, not trashed) first has\n * its next run advanced to the next cron match — then it executes through\n * the same path as the manual button. Missed windows (host was down, tab\n * closed — irrelevant here, this is the host process) simply advance: a\n * nextRunAt more than one window in the past is skipped, not caught up.\n *\n * @module dsh-taskboard/host/scheduler\n */\nimport { nextCronTime, parseCron, type TaskLedger } from '../shared/protocol.ts'\nimport { DEFAULT_MAX_CONCURRENT, type ExecutionService } from './execution.ts'\nimport type { TaskStore } from './store.ts'\n\n/** Tick cadence. */\nconst TICK_MS = 60_000\n\n/** A due window older than this is skipped (missed while the host was down). */\nconst SKIP_AFTER_MS = 5 * 60_000\n\n/** Everything the scheduler needs. */\nexport interface SchedulerDeps {\n store: TaskStore\n execution: Pick<ExecutionService, 'run' | 'inFlight'>\n now: () => number\n /** Max concurrently running executions (default 3; must match the execution service). */\n maxConcurrent?: number\n /** Timer face (injectable for tests). */\n timers?: {\n setInterval(fn: () => void, ms: number): unknown\n clearInterval(handle: unknown): void\n }\n}\n\n/**\n * The cron scheduler.\n */\nexport class SchedulerService {\n private handle: unknown\n private catchup: ReturnType<typeof setTimeout> | undefined\n\n /** @param deps - store + execution + clock. */\n constructor(private readonly deps: SchedulerDeps) {}\n\n /** Start ticking. */\n start(): void {\n const timers = this.deps.timers ?? {\n setInterval: (fn: () => void, ms: number) => setInterval(fn, ms),\n clearInterval: (handle: unknown) => clearInterval(handle as ReturnType<typeof setInterval>),\n }\n this.handle = timers.setInterval(() => { void this.tick() }, TICK_MS)\n // Catch up promptly on host restart: run one tick soon after start. The\n // handle is cleared on dispose so a torn-down scheduler never fires.\n this.catchup = setTimeout(() => { void this.tick() }, 3_000)\n }\n\n /** Stop ticking. */\n dispose(): void {\n if (this.catchup !== undefined) {\n clearTimeout(this.catchup)\n this.catchup = undefined\n }\n if (this.handle === undefined) return\n const timers = this.deps.timers ?? { clearInterval: (h: unknown) => clearInterval(h as ReturnType<typeof setInterval>) }\n timers.clearInterval(this.handle)\n this.handle = undefined\n }\n\n /** One scheduler pass (exported for tests). */\n async tick(): Promise<void> {\n // Load once before reading: snapshot() does not trigger a load, and the\n // scheduler may be the first consumer after a host restart (otherwise it\n // would tick over an empty ledger until something else loads it).\n await this.deps.store.load()\n const now = this.deps.now()\n const ledger: TaskLedger = this.deps.store.snapshot()\n const atCapacity = this.deps.execution.inFlight() >= (this.deps.maxConcurrent ?? DEFAULT_MAX_CONCURRENT)\n for (const task of ledger.tasks) {\n if (task.execution.mode !== 'scheduled' || task.execution.cron === undefined) continue\n if (task.execution.nextRunAt === undefined) continue\n if (task.status === 'in_progress' || task.trashedAt !== undefined) continue\n if (task.execution.nextRunAt > now) continue\n // At the concurrency cap: leave nextRunAt in the past and retry next\n // tick — advancing here would silently burn this window.\n if (atCapacity) continue\n const missed = now - task.execution.nextRunAt > SKIP_AFTER_MS\n\n // Advance the schedule FIRST (idempotent under re-ticks), then run\n // unless the window was missed entirely.\n await this.advance(task.id, now)\n if (missed) continue\n const lastTriggeredAt = task.execution.nextRunAt\n await this.markTriggered(task.id, lastTriggeredAt)\n await this.deps.execution.run(task.id, 'scheduled').catch(error => {\n console.error('[dsh-taskboard] scheduled run failed:', error)\n })\n }\n }\n\n /** Recompute and persist the next run for one scheduled task. */\n private async advance(taskId: string, now: number): Promise<void> {\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(t => t.id === taskId)\n if (task === undefined || task.execution.cron === undefined) return undefined\n const match = parseCron(task.execution.cron)\n const next = match === null ? undefined : nextCronTime(match, now) ?? undefined\n if (next === undefined) return undefined\n task.execution.nextRunAt = next\n return [task]\n })\n }\n\n /** Record the trigger instant on the task. */\n private async markTriggered(taskId: string, at: number | undefined): Promise<void> {\n if (at === undefined) return\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(t => t.id === taskId)\n if (task === undefined) return undefined\n task.execution.lastTriggeredAt = at\n return [task]\n })\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAeA,MAAM,UAAU;;AAGhB,MAAM,gBAAgB,IAAI;;;;AAmB1B,IAAa,mBAAb,MAA8B;CAKC;CAJ7B;CACA;;CAGA,YAAY,MAAsC;EAArB,KAAA,OAAA;CAAsB;;CAGnD,QAAc;EACZ,MAAM,SAAS,KAAK,KAAK,UAAU;GACjC,cAAc,IAAgB,OAAe,YAAY,IAAI,EAAE;GAC/D,gBAAgB,WAAoB,cAAc,MAAwC;EAC5F;EACA,KAAK,SAAS,OAAO,kBAAkB;GAAE,KAAU,KAAK;EAAE,GAAG,OAAO;EAGpE,KAAK,UAAU,iBAAiB;GAAE,KAAU,KAAK;EAAE,GAAG,GAAK;CAC7D;;CAGA,UAAgB;EACd,IAAI,KAAK,YAAY,KAAA,GAAW;GAC9B,aAAa,KAAK,OAAO;GACzB,KAAK,UAAU,KAAA;EACjB;EACA,IAAI,KAAK,WAAW,KAAA,GAAW;EAE/B,CADe,KAAK,KAAK,UAAU,EAAE,gBAAgB,MAAe,cAAc,CAAmC,EAAE,EAAA,CAChH,cAAc,KAAK,MAAM;EAChC,KAAK,SAAS,KAAA;CAChB;;CAGA,MAAM,OAAsB;EAI1B,MAAM,KAAK,KAAK,MAAM,KAAK;EAC3B,MAAM,MAAM,KAAK,KAAK,IAAI;EAC1B,MAAM,SAAqB,KAAK,KAAK,MAAM,SAAS;EACpD,MAAM,aAAa,KAAK,KAAK,UAAU,SAAS,MAAM,KAAK,KAAK,iBAAA;EAChE,KAAK,MAAM,QAAQ,OAAO,OAAO;GAC/B,IAAI,KAAK,UAAU,SAAS,eAAe,KAAK,UAAU,SAAS,KAAA,GAAW;GAC9E,IAAI,KAAK,UAAU,cAAc,KAAA,GAAW;GAC5C,IAAI,KAAK,WAAW,iBAAiB,KAAK,cAAc,KAAA,GAAW;GACnE,IAAI,KAAK,UAAU,YAAY,KAAK;GAGpC,IAAI,YAAY;GAChB,MAAM,SAAS,MAAM,KAAK,UAAU,YAAY;GAIhD,MAAM,KAAK,QAAQ,KAAK,IAAI,GAAG;GAC/B,IAAI,QAAQ;GACZ,MAAM,kBAAkB,KAAK,UAAU;GACvC,MAAM,KAAK,cAAc,KAAK,IAAI,eAAe;GACjD,MAAM,KAAK,KAAK,UAAU,IAAI,KAAK,IAAI,WAAW,CAAC,CAAC,OAAM,UAAS;IACjE,QAAQ,MAAM,yCAAyC,KAAK;GAC9D,CAAC;EACH;CACF;;CAGA,MAAc,QAAQ,QAAgB,KAA4B;EAChE,MAAM,KAAK,KAAK,MAAM,OAAO,iBAAiB,WAAW;GACvD,MAAM,OAAO,OAAO,MAAM,MAAK,MAAK,EAAE,OAAO,MAAM;GACnD,IAAI,SAAS,KAAA,KAAa,KAAK,UAAU,SAAS,KAAA,GAAW,OAAO,KAAA;GACpE,MAAM,QAAQ,UAAU,KAAK,UAAU,IAAI;GAC3C,MAAM,OAAO,UAAU,OAAO,KAAA,IAAY,aAAa,OAAO,GAAG,KAAK,KAAA;GACtE,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;GAC/B,KAAK,UAAU,YAAY;GAC3B,OAAO,CAAC,IAAI;EACd,CAAC;CACH;;CAGA,MAAc,cAAc,QAAgB,IAAuC;EACjF,IAAI,OAAO,KAAA,GAAW;EACtB,MAAM,KAAK,KAAK,MAAM,OAAO,iBAAiB,WAAW;GACvD,MAAM,OAAO,OAAO,MAAM,MAAK,MAAK,EAAE,OAAO,MAAM;GACnD,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;GAC/B,KAAK,UAAU,kBAAkB;GACjC,OAAO,CAAC,IAAI;EACd,CAAC;CACH;AACF"}
|
package/lib/host/store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { emptyLedger } from "../shared/protocol.js";
|
|
1
|
+
import { emptyLedger, pruneExecutions } from "../shared/protocol.js";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
4
4
|
//#region src/host/store.ts
|
|
@@ -31,11 +31,18 @@ var TaskStore = class {
|
|
|
31
31
|
try {
|
|
32
32
|
const raw = await readFile(this.file, "utf8");
|
|
33
33
|
const parsed = JSON.parse(raw);
|
|
34
|
-
if (typeof parsed.revision === "number" && Array.isArray(parsed.tasks))
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
if (typeof parsed.revision === "number" && Array.isArray(parsed.tasks)) {
|
|
35
|
+
const tasks = parsed.tasks;
|
|
36
|
+
for (const task of tasks) if (task.status === "in_progress" && task.claimedBy === void 0 && task.updatedBy?.kind === "agent" && typeof task.updatedBy.sessionId === "string") {
|
|
37
|
+
task.claimedBy = task.updatedBy.sessionId;
|
|
38
|
+
task.claimedAt = task.updatedAt;
|
|
39
|
+
}
|
|
40
|
+
this.ledger = {
|
|
41
|
+
schemaVersion: 1,
|
|
42
|
+
revision: parsed.revision,
|
|
43
|
+
tasks
|
|
44
|
+
};
|
|
45
|
+
}
|
|
39
46
|
} catch (error) {
|
|
40
47
|
if (error.code !== "ENOENT") try {
|
|
41
48
|
await rename(this.file, `${this.file}.corrupt-${Date.now()}`);
|
|
@@ -43,13 +50,18 @@ var TaskStore = class {
|
|
|
43
50
|
}
|
|
44
51
|
this.loaded = true;
|
|
45
52
|
}
|
|
46
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* The current snapshot — a deep-frozen clone. Mutating the returned value
|
|
55
|
+
* throws (strict mode) instead of silently bypassing the revision/persist
|
|
56
|
+
* path; internal state is never handed out.
|
|
57
|
+
*/
|
|
47
58
|
snapshot() {
|
|
48
|
-
return this.ledger;
|
|
59
|
+
return deepFreeze(structuredClone(this.ledger));
|
|
49
60
|
}
|
|
50
|
-
/** Find a task by id. */
|
|
61
|
+
/** Find a task by id (frozen clone; internal state is never handed out). */
|
|
51
62
|
get(id) {
|
|
52
|
-
|
|
63
|
+
const task = this.ledger.tasks.find((t) => t.id === id);
|
|
64
|
+
return task === void 0 ? void 0 : deepFreeze(structuredClone(task));
|
|
53
65
|
}
|
|
54
66
|
/** Subscribe to committed changes; returns the unsubscribe. */
|
|
55
67
|
subscribe(fn) {
|
|
@@ -71,6 +83,7 @@ var TaskStore = class {
|
|
|
71
83
|
ledger: this.ledger,
|
|
72
84
|
changed: []
|
|
73
85
|
};
|
|
86
|
+
for (const task of changed) pruneExecutions(task);
|
|
74
87
|
draft.revision += 1;
|
|
75
88
|
const json = JSON.stringify(draft);
|
|
76
89
|
await persistAtomic(this.file, json);
|
|
@@ -90,15 +103,15 @@ var TaskStore = class {
|
|
|
90
103
|
};
|
|
91
104
|
return this.queue = this.queue.then(run, run);
|
|
92
105
|
}
|
|
93
|
-
/** Persist the current ledger now (used after external reconciliation). */
|
|
94
|
-
async flush(kind, changed) {
|
|
95
|
-
await this.mutate(kind, (ledger) => {
|
|
96
|
-
const byId = new Map(this.ledger.tasks.map((t) => [t.id, t]));
|
|
97
|
-
ledger.tasks = ledger.tasks.map((t) => byId.get(t.id) ?? t);
|
|
98
|
-
return [...changed];
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
106
|
};
|
|
107
|
+
/** Recursively freeze a plain-data value (defense in depth for handed-out snapshots). */
|
|
108
|
+
function deepFreeze(value) {
|
|
109
|
+
if (value !== null && typeof value === "object") {
|
|
110
|
+
if (!Object.isFrozen(value)) Object.freeze(value);
|
|
111
|
+
for (const key of Object.keys(value)) deepFreeze(value[key]);
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
102
115
|
/** Atomic file persist: write temp, then rename over the target. */
|
|
103
116
|
async function persistAtomic(file, contents) {
|
|
104
117
|
await mkdir(dirname(file), { recursive: true });
|
package/lib/host/store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","names":[],"sources":["../../src/host/store.ts"],"sourcesContent":["/**\n * Host-side task ledger: one JSON file under the DSH home, mutated through a\n * serial write queue, published as immutable snapshots with a global\n * monotonic revision. Change subscribers (P2: SSE route) observe every\n * committed mutation.\n *\n * @module dsh-taskboard/host/store\n */\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport {\n LEDGER_SCHEMA_VERSION,\n emptyLedger,\n type TaskLedger,\n type TaskRecord,\n} from '../shared/protocol.ts'\n\n/** One committed ledger mutation, handed to change subscribers. */\nexport interface LedgerChange {\n /** Revision after the mutation. */\n revision: number\n /** The mutated tasks, if any (a comment purge may touch none). */\n tasks: readonly TaskRecord[]\n /** What kind of mutation this was (for SSE event naming later). */\n kind: 'task-created' | 'task-updated' | 'task-moved' | 'task-deleted' | 'comment-added' | 'execution-recorded'\n}\n\n/** Options for {@link TaskStore}. */\nexport interface TaskStoreOptions {\n /** Absolute ledger file path. */\n file: string\n}\n\n/**\n * The durable ledger. All mutations run through {@link mutate}, which:\n * validates the resulting document, bumps the global revision, persists\n * atomically (temp file + rename), and only then notifies subscribers.\n */\nexport class TaskStore {\n private readonly file: string\n private ledger: TaskLedger = emptyLedger()\n private readonly subscribers = new Set<(change: LedgerChange) => void>()\n private queue: Promise<unknown> = Promise.resolve()\n private loaded = false\n\n /** @param options - file location. */\n constructor(options: TaskStoreOptions) {\n this.file = options.file\n }\n\n /** Load (once) from disk; a missing file starts empty; a corrupt file is quarantined, not thrown. */\n async load(): Promise<void> {\n if (this.loaded) return\n try {\n const raw = await readFile(this.file, 'utf8')\n const parsed = JSON.parse(raw) as TaskLedger\n if (typeof parsed.revision === 'number' && Array.isArray(parsed.tasks)) {\n this.ledger = { schemaVersion: LEDGER_SCHEMA_VERSION, revision: parsed.revision, tasks
|
|
1
|
+
{"version":3,"file":"store.js","names":[],"sources":["../../src/host/store.ts"],"sourcesContent":["/**\n * Host-side task ledger: one JSON file under the DSH home, mutated through a\n * serial write queue, published as immutable snapshots with a global\n * monotonic revision. Change subscribers (P2: SSE route) observe every\n * committed mutation.\n *\n * @module dsh-taskboard/host/store\n */\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport {\n LEDGER_SCHEMA_VERSION,\n emptyLedger,\n pruneExecutions,\n type TaskLedger,\n type TaskRecord,\n} from '../shared/protocol.ts'\n\n/** One committed ledger mutation, handed to change subscribers. */\nexport interface LedgerChange {\n /** Revision after the mutation. */\n revision: number\n /** The mutated tasks, if any (a comment purge may touch none). */\n tasks: readonly TaskRecord[]\n /** What kind of mutation this was (for SSE event naming later). */\n kind: 'task-created' | 'task-updated' | 'task-moved' | 'task-deleted' | 'comment-added' | 'execution-recorded'\n}\n\n/** Options for {@link TaskStore}. */\nexport interface TaskStoreOptions {\n /** Absolute ledger file path. */\n file: string\n}\n\n/**\n * The durable ledger. All mutations run through {@link mutate}, which:\n * validates the resulting document, bumps the global revision, persists\n * atomically (temp file + rename), and only then notifies subscribers.\n */\nexport class TaskStore {\n private readonly file: string\n private ledger: TaskLedger = emptyLedger()\n private readonly subscribers = new Set<(change: LedgerChange) => void>()\n private queue: Promise<unknown> = Promise.resolve()\n private loaded = false\n\n /** @param options - file location. */\n constructor(options: TaskStoreOptions) {\n this.file = options.file\n }\n\n /** Load (once) from disk; a missing file starts empty; a corrupt file is quarantined, not thrown. */\n async load(): Promise<void> {\n if (this.loaded) return\n try {\n const raw = await readFile(this.file, 'utf8')\n const parsed = JSON.parse(raw) as TaskLedger\n if (typeof parsed.revision === 'number' && Array.isArray(parsed.tasks)) {\n const tasks = parsed.tasks as TaskRecord[]\n // Migration from pre-claim-field ledgers: an agent-held in_progress\n // task carried its holder in updatedBy — backfill the explicit claim\n // fields so the hold survives user edits (updatedBy is audit-only).\n for (const task of tasks) {\n if (task.status === 'in_progress' && task.claimedBy === undefined\n && task.updatedBy?.kind === 'agent' && typeof task.updatedBy.sessionId === 'string') {\n task.claimedBy = task.updatedBy.sessionId\n task.claimedAt = task.updatedAt\n }\n }\n this.ledger = { schemaVersion: LEDGER_SCHEMA_VERSION, revision: parsed.revision, tasks }\n }\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code\n if (code !== 'ENOENT') {\n // Quarantine a corrupt ledger: rename it aside, start fresh. Never\n // take the host down over ledger damage.\n try {\n await rename(this.file, `${this.file}.corrupt-${Date.now()}`)\n } catch { /* best effort */ }\n }\n }\n this.loaded = true\n }\n\n /**\n * The current snapshot — a deep-frozen clone. Mutating the returned value\n * throws (strict mode) instead of silently bypassing the revision/persist\n * path; internal state is never handed out.\n */\n snapshot(): TaskLedger {\n return deepFreeze(structuredClone(this.ledger))\n }\n\n /** Find a task by id (frozen clone; internal state is never handed out). */\n get(id: string): TaskRecord | undefined {\n const task = this.ledger.tasks.find(t => t.id === id)\n return task === undefined ? undefined : deepFreeze(structuredClone(task))\n }\n\n /** Subscribe to committed changes; returns the unsubscribe. */\n subscribe(fn: (change: LedgerChange) => void): () => void {\n this.subscribers.add(fn)\n return () => this.subscribers.delete(fn)\n }\n\n /**\n * Run one mutation inside the serial queue. The mutator works on a\n * structured clone; returning `undefined` aborts with no write.\n * @param kind - change kind for subscribers.\n * @param mutator - receives the cloned ledger; mutate tasks in place; return the touched tasks.\n */\n async mutate(\n kind: LedgerChange['kind'],\n mutator: (ledger: TaskLedger) => TaskRecord[] | undefined,\n ): Promise<{ ledger: TaskLedger; changed: readonly TaskRecord[] }> {\n const run = async (): Promise<{ ledger: TaskLedger; changed: readonly TaskRecord[] }> => {\n await this.load()\n const draft: TaskLedger = structuredClone(this.ledger)\n const changed = mutator(draft)\n if (changed === undefined) {\n return { ledger: this.ledger, changed: [] }\n }\n // Retention cap: every committed mutation re-checks the touched tasks,\n // so execution history can never grow unbounded (SSE state payload).\n for (const task of changed) pruneExecutions(task)\n draft.revision += 1\n const json = JSON.stringify(draft)\n await persistAtomic(this.file, json)\n this.ledger = draft\n const change: LedgerChange = { revision: draft.revision, tasks: changed, kind }\n for (const fn of this.subscribers) {\n try {\n fn(change)\n } catch { /* subscriber errors never abort the write */ }\n }\n return { ledger: draft, changed }\n }\n const result = (this.queue = this.queue.then(run, run)) as ReturnType<typeof run>\n return result\n }\n}\n\n/** Recursively freeze a plain-data value (defense in depth for handed-out snapshots). */\nfunction deepFreeze<T>(value: T): T {\n if (value !== null && typeof value === 'object') {\n if (!Object.isFrozen(value)) Object.freeze(value)\n for (const key of Object.keys(value as Record<string, unknown>)) {\n deepFreeze((value as Record<string, unknown>)[key])\n }\n }\n return value\n}\n\n/** Atomic file persist: write temp, then rename over the target. */\nasync function persistAtomic(file: string, contents: string): Promise<void> {\n await mkdir(dirname(file), { recursive: true })\n const temp = join(dirname(file), `.${Math.random().toString(36).slice(2)}.tmp`)\n await writeFile(temp, contents, 'utf8')\n await rename(temp, file)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAuCA,IAAa,YAAb,MAAuB;CACrB;CACA,SAA6B,YAAY;CACzC,8BAA+B,IAAI,IAAoC;CACvE,QAAkC,QAAQ,QAAQ;CAClD,SAAiB;;CAGjB,YAAY,SAA2B;EACrC,KAAK,OAAO,QAAQ;CACtB;;CAGA,MAAM,OAAsB;EAC1B,IAAI,KAAK,QAAQ;EACjB,IAAI;GACF,MAAM,MAAM,MAAM,SAAS,KAAK,MAAM,MAAM;GAC5C,MAAM,SAAS,KAAK,MAAM,GAAG;GAC7B,IAAI,OAAO,OAAO,aAAa,YAAY,MAAM,QAAQ,OAAO,KAAK,GAAG;IACtE,MAAM,QAAQ,OAAO;IAIrB,KAAK,MAAM,QAAQ,OACjB,IAAI,KAAK,WAAW,iBAAiB,KAAK,cAAc,KAAA,KACnD,KAAK,WAAW,SAAS,WAAW,OAAO,KAAK,UAAU,cAAc,UAAU;KACrF,KAAK,YAAY,KAAK,UAAU;KAChC,KAAK,YAAY,KAAK;IACxB;IAEF,KAAK,SAAS;KAAE,eAAA;KAAsC,UAAU,OAAO;KAAU;IAAM;GACzF;EACF,SAAS,OAAO;GAEd,IADc,MAAgC,SACjC,UAGX,IAAI;IACF,MAAM,OAAO,KAAK,MAAM,GAAG,KAAK,KAAK,WAAW,KAAK,IAAI,GAAG;GAC9D,QAAQ,CAAoB;EAEhC;EACA,KAAK,SAAS;CAChB;;;;;;CAOA,WAAuB;EACrB,OAAO,WAAW,gBAAgB,KAAK,MAAM,CAAC;CAChD;;CAGA,IAAI,IAAoC;EACtC,MAAM,OAAO,KAAK,OAAO,MAAM,MAAK,MAAK,EAAE,OAAO,EAAE;EACpD,OAAO,SAAS,KAAA,IAAY,KAAA,IAAY,WAAW,gBAAgB,IAAI,CAAC;CAC1E;;CAGA,UAAU,IAAgD;EACxD,KAAK,YAAY,IAAI,EAAE;EACvB,aAAa,KAAK,YAAY,OAAO,EAAE;CACzC;;;;;;;CAQA,MAAM,OACJ,MACA,SACiE;EACjE,MAAM,MAAM,YAA6E;GACvF,MAAM,KAAK,KAAK;GAChB,MAAM,QAAoB,gBAAgB,KAAK,MAAM;GACrD,MAAM,UAAU,QAAQ,KAAK;GAC7B,IAAI,YAAY,KAAA,GACd,OAAO;IAAE,QAAQ,KAAK;IAAQ,SAAS,CAAC;GAAE;GAI5C,KAAK,MAAM,QAAQ,SAAS,gBAAgB,IAAI;GAChD,MAAM,YAAY;GAClB,MAAM,OAAO,KAAK,UAAU,KAAK;GACjC,MAAM,cAAc,KAAK,MAAM,IAAI;GACnC,KAAK,SAAS;GACd,MAAM,SAAuB;IAAE,UAAU,MAAM;IAAU,OAAO;IAAS;GAAK;GAC9E,KAAK,MAAM,MAAM,KAAK,aACpB,IAAI;IACF,GAAG,MAAM;GACX,QAAQ,CAAgD;GAE1D,OAAO;IAAE,QAAQ;IAAO;GAAQ;EAClC;EAEA,OAAO,KADc,QAAQ,KAAK,MAAM,KAAK,KAAK,GAAG;CAEvD;AACF;;AAGA,SAAS,WAAc,OAAa;CAClC,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;EAC/C,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG,OAAO,OAAO,KAAK;EAChD,KAAK,MAAM,OAAO,OAAO,KAAK,KAAgC,GAC5D,WAAY,MAAkC,IAAI;CAEtD;CACA,OAAO;AACT;;AAGA,eAAe,cAAc,MAAc,UAAiC;CAC1E,MAAM,MAAM,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;CAC9C,MAAM,OAAO,KAAK,QAAQ,IAAI,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK;CAC9E,MAAM,UAAU,MAAM,UAAU,MAAM;CACtC,MAAM,OAAO,MAAM,IAAI;AACzB"}
|
package/lib/host/tools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { asStatus, asUrgency, canTransition, effectivePrompt, isClaim, newCommentId, newTaskId, normalizeBody, normalizeExecution, normalizePrompt, normalizeTitle, summarize } from "../shared/protocol.js";
|
|
1
|
+
import { asStatus, asUrgency, canTransition, effectivePrompt, isClaim, isClaimedBy, newCommentId, newTaskId, normalizeBody, normalizeExecution, normalizeModel, normalizePrompt, normalizeTitle, summarize, syncClaim } from "../shared/protocol.js";
|
|
2
2
|
import { defineTool } from "./sdk.js";
|
|
3
3
|
//#region src/host/tools.ts
|
|
4
4
|
/** Render side: one compact task line (id/status/version are load-bearing). */
|
|
@@ -18,6 +18,8 @@ function taskDetail(t) {
|
|
|
18
18
|
`状态: ${t.status} (v${t.version}) · 紧急度: ${t.urgency} · 项目: ${t.workspaceId}${t.blocked ? " · 受阻" : ""}`,
|
|
19
19
|
`执行方式: ${t.execution.mode}${t.execution.cron !== void 0 ? ` cron=${t.execution.cron}` : ""}`
|
|
20
20
|
];
|
|
21
|
+
const holder = isClaimedBy(t);
|
|
22
|
+
if (holder !== void 0) lines.push(`认领: agent ${String(holder).slice(0, 24)}(持有期间其他会话不可移动)`);
|
|
21
23
|
if (t.execution.nextRunAt !== void 0) lines.push(`下次触发: ${new Date(t.execution.nextRunAt).toISOString()}`);
|
|
22
24
|
if (t.model !== void 0) lines.push(`固定模型: ${t.model.provider}/${t.model.model}`);
|
|
23
25
|
lines.push(`描述: ${t.description.length > 0 ? t.description : "(无)"}`);
|
|
@@ -81,6 +83,13 @@ function workspaceFace(registry) {
|
|
|
81
83
|
}))
|
|
82
84
|
};
|
|
83
85
|
}
|
|
86
|
+
/** Validate a pinned model: structural check always, provider route when known. */
|
|
87
|
+
function checkModel(deps, raw) {
|
|
88
|
+
const model = normalizeModel(raw);
|
|
89
|
+
const providers = deps.modelProviders?.();
|
|
90
|
+
if (providers !== void 0 && !providers.includes(model.provider)) throw new ToolError(ERR.invalidInput, `model provider "${model.provider}" has no registered route (available: ${providers.join(", ")})`);
|
|
91
|
+
return model;
|
|
92
|
+
}
|
|
84
93
|
/** Resolve the calling agent's actor and session id. */
|
|
85
94
|
function caller(exec) {
|
|
86
95
|
if (!exec.agent) throw new ToolError(ERR.requiresAgent, "taskboard tools require a calling agent session");
|
|
@@ -305,7 +314,7 @@ function registerTaskboardTools(ctx, deps) {
|
|
|
305
314
|
const status = args.status === void 0 ? "todo" : asStatus(args.status);
|
|
306
315
|
if (status === "done" || status === "archived") throw new ToolError(ERR.invalidTransition, "a new task cannot start as done/archived");
|
|
307
316
|
const execution = normalizeExecution(args.execution ?? {}, deps.now());
|
|
308
|
-
|
|
317
|
+
const model = args.model !== void 0 ? checkModel(deps, args.model) : void 0;
|
|
309
318
|
const now = deps.now();
|
|
310
319
|
const task = {
|
|
311
320
|
id: newTaskId(),
|
|
@@ -317,7 +326,7 @@ function registerTaskboardTools(ctx, deps) {
|
|
|
317
326
|
status,
|
|
318
327
|
blocked: false,
|
|
319
328
|
execution,
|
|
320
|
-
model
|
|
329
|
+
model,
|
|
321
330
|
version: 1,
|
|
322
331
|
createdAt: now,
|
|
323
332
|
updatedAt: now,
|
|
@@ -447,7 +456,7 @@ function registerTaskboardTools(ctx, deps) {
|
|
|
447
456
|
versionGuard(task, args.ifVersion);
|
|
448
457
|
if (to === "done") throw new ToolError(ERR.forbidden, "moving a task to done requires explicit user confirmation (GUI); agents cannot do it");
|
|
449
458
|
if (!canTransition(task.status, to)) throw new ToolError(ERR.invalidTransition, `illegal transition ${task.status} → ${to}`);
|
|
450
|
-
if (task.status === "in_progress" && task.
|
|
459
|
+
if (task.status === "in_progress" && task.claimedBy !== void 0 && task.claimedBy !== actor.sessionId) throw new ToolError(ERR.forbidden, `task is held by session ${task.claimedBy}; never take over another session's claim`);
|
|
451
460
|
if (isClaim(task.status, to)) {
|
|
452
461
|
if (await callerWorkspace(deps, exec) !== task.workspaceId) throw new ToolError(ERR.workspaceMismatch, "only a session inside this task's project may claim it");
|
|
453
462
|
}
|
|
@@ -457,6 +466,7 @@ function registerTaskboardTools(ctx, deps) {
|
|
|
457
466
|
next.updatedAt = deps.now();
|
|
458
467
|
next.updatedBy = actor;
|
|
459
468
|
if (isClaim(task.status, to)) next.blocked = false;
|
|
469
|
+
syncClaim(next, to, deps.now(), isClaim(task.status, to) ? actor.sessionId : void 0);
|
|
460
470
|
await store.mutate("task-moved", (ledger) => {
|
|
461
471
|
const i = ledger.tasks.findIndex((t) => t.id === args.id);
|
|
462
472
|
ledger.tasks[i] = next;
|
package/lib/host/tools.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.js","names":["v"],"sources":["../../src/host/tools.ts"],"sourcesContent":["/**\n * The eight `taskboard_*` agent tools. All writes require a calling agent\n * session (ownership audit), carry optimistic-version checks, and enforce\n * the protocol gates in CODE, not in prompt text:\n *\n * - `move → done` is rejected for agent callers (user confirmation only)\n * - `move todo → in_progress` requires the calling session's workspace to\n * match the task's project (claim boundary)\n * - taking over a task held by another session is rejected\n * - `delete` for agent callers only sets the soft-delete marker\n *\n * OUTPUT CONTRACT (lesson: registry `createSuccessResult` renders\n * `output.render(args, value)` into `result.content`, and the loop feeds\n * exactly that content to the model — the raw JSON `value` never reaches\n * the model): render() IS the model-facing tool result. Every render must\n * carry the complete facts an agent needs to act (ids, versions, statuses);\n * a \"terse UI summary\" here starves the agent.\n *\n * @module dsh-taskboard/host/tools\n */\nimport type { WorkspaceRegistry } from '@deepseek-ai/dsh-workspace'\nimport { defineTool } from './sdk.ts'\nimport {\n asStatus,\n asUrgency,\n canTransition,\n effectivePrompt,\n isClaim,\n newCommentId,\n newTaskId,\n normalizeBody,\n normalizeExecution,\n normalizePrompt,\n normalizeTitle,\n summarize,\n type Actor,\n type TaskModel,\n type TaskRecord,\n} from '../shared/protocol.ts'\nimport type { TaskStore } from './store.ts'\n\n/** Render side: one compact task line (id/status/version are load-bearing). */\nfunction taskLine(t: {\n id: string\n title: string\n status: string\n urgency: string\n version: number\n workspaceId: string\n blocked: boolean\n executionMode: string\n commentCount?: number\n lastExecutionOutcome?: string\n trashed?: boolean\n}): string {\n const parts = [\n `- ${t.id} [${t.status}] v${t.version} · ${t.urgency} · 项目 ${t.workspaceId}`,\n `「${t.title}」`,\n ]\n if (t.blocked) parts.push('·受阻')\n if (t.executionMode === 'scheduled') parts.push('·定时')\n if (t.commentCount !== undefined && t.commentCount > 0) parts.push(`·评论${t.commentCount}`)\n if (t.lastExecutionOutcome !== undefined) parts.push(`·上次执行${t.lastExecutionOutcome}`)\n if (t.trashed === true) parts.push('·已删')\n return parts.join(' ')\n}\n\n/** Render side: the full task detail block (everything an executor needs). */\nfunction taskDetail(t: TaskRecord & { effectivePrompt?: string }): string {\n const lines: string[] = [\n `任务 ${t.id} 「${t.title}」`,\n `状态: ${t.status} (v${t.version}) · 紧急度: ${t.urgency} · 项目: ${t.workspaceId}${t.blocked ? ' · 受阻' : ''}`,\n `执行方式: ${t.execution.mode}${t.execution.cron !== undefined ? ` cron=${t.execution.cron}` : ''}`,\n ]\n if (t.execution.nextRunAt !== undefined) lines.push(`下次触发: ${new Date(t.execution.nextRunAt).toISOString()}`)\n if (t.model !== undefined) lines.push(`固定模型: ${t.model.provider}/${t.model.model}`)\n lines.push(`描述: ${t.description.length > 0 ? t.description : '(无)'}`)\n lines.push(`执行 Prompt: ${t.effectivePrompt ?? effectivePrompt(t)}`)\n if (t.comments.length > 0) {\n lines.push(`评论 (${t.comments.length}):`)\n for (const c of t.comments) {\n const who = c.threadId !== undefined ? `agent ${String(c.threadId).slice(0, 24)}` : 'user'\n lines.push(` - [${who} ${new Date(c.createdAt).toISOString()}] ${c.body}`)\n }\n } else {\n lines.push('评论: 无')\n }\n if (t.executions.length > 0) {\n lines.push(`执行记录 (${t.executions.length}):`)\n for (const e of t.executions) {\n const at = e.startedAt !== undefined ? new Date(e.startedAt).toISOString() : '?'\n const err = e.error !== undefined ? ` 错误: ${e.error}` : ''\n lines.push(` - [${e.trigger} ${at}] ${e.outcome}${err}`)\n }\n } else {\n lines.push('执行记录: 无')\n }\n const updatedBy = t.updatedBy.kind === 'agent' ? `agent ${String(t.updatedBy.sessionId).slice(0, 24)}` : 'user'\n lines.push(`更新: ${new Date(t.updatedAt).toISOString()} 由 ${updatedBy}`)\n return lines.join('\\n')\n}\n\n/** Stable error codes surfaced at the head of tool error messages. */\nexport const ERR = {\n notFound: 'not_found',\n versionConflict: 'version_conflict',\n workspaceMismatch: 'workspace_mismatch',\n invalidTransition: 'invalid_transition',\n forbidden: 'forbidden',\n requiresAgent: 'unauthorized_actor',\n invalidInput: 'invalid_input',\n} as const\n\n/** Tool failure: an Error whose message starts with a stable code. */\nclass ToolError extends Error {\n constructor(readonly code: string, detail: string) {\n super(`Error: ${code}: ${detail}`)\n }\n}\n\n/** The workspace face the tools need (narrow for tests). */\nexport interface WorkspaceFace {\n /** Resolve the workspace owning a canonical cwd, if any. */\n resolveByPath(path: string): Promise<{ id: string } | undefined>\n /** Get a workspace by id. */\n get(id: string): { id: string; path: string; title: string } | undefined\n /** List all workspaces. */\n list(): Array<{ id: string; path: string; title: string }>\n}\n\n/** Adapt the real registry to the narrow face. */\nexport function workspaceFace(registry: WorkspaceRegistry): WorkspaceFace {\n // Explicit field mapping: Workspace entities expose path/title as prototype\n // getters, which JSON.stringify skips (own enumerable properties only).\n return {\n resolveByPath: async (path) => {\n const ws = await registry.resolveByPath(path as never)\n return ws === undefined ? undefined : { id: ws.id }\n },\n get: id => {\n const ws = registry.get(id as never)\n return ws === undefined ? undefined : { id: ws.id, path: ws.path, title: ws.title }\n },\n list: () => registry.list().map(ws => ({ id: ws.id, path: ws.path, title: ws.title })),\n }\n}\n\n/** Everything the tool set needs. */\nexport interface ToolDeps {\n store: TaskStore\n workspaces: WorkspaceFace\n /** Current epoch ms (injectable for tests). */\n now: () => number\n}\n\n/** Resolve the calling agent's actor and session id. */\nfunction caller(exec: ToolRunContext): { actor: Actor & { kind: 'agent' }; sessionId: string } {\n if (!exec.agent) throw new ToolError(ERR.requiresAgent, 'taskboard tools require a calling agent session')\n const sessionId = exec.agent.id\n return { actor: { kind: 'agent', sessionId }, sessionId }\n}\n\n/** The calling session's workspace id (undefined when unaffiliated). */\nasync function callerWorkspace(deps: ToolDeps, exec: ToolRunContext): Promise<string | undefined> {\n const cwd = exec.agent?.session.header.cwd\n if (typeof cwd !== 'string' || cwd.length === 0) return undefined\n const ws = await deps.workspaces.resolveByPath(cwd)\n return ws?.id\n}\n\n/** Guard: version match. */\nfunction versionGuard(task: TaskRecord, ifVersion: number | undefined): void {\n if (ifVersion === undefined) {\n throw new ToolError(ERR.versionConflict, 'this write requires ifVersion; read the task first')\n }\n if (ifVersion !== task.version) {\n throw new ToolError(ERR.versionConflict, `stale version ${ifVersion} (current ${task.version}); re-read the task and retry once`)\n }\n}\n\n/** Re-throw with a stable code; non-ToolErrors become invalid_input. */\nfunction fail(error: unknown): never {\n if (error instanceof ToolError) throw error\n const message = error instanceof Error ? error.message : String(error)\n throw new ToolError(ERR.invalidInput, message)\n}\n\n/** Loose json output schema shared by every taskboard tool. */\nconst JSON_OUT = { type: 'json' } as const\n\n/** Deep-JSON a value for a json-rooted tool output (spread results lose implicit index signatures). */\nfunction json<T>(value: T): Record<string, unknown> {\n return JSON.parse(JSON.stringify(value)) as Record<string, unknown>\n}\n\n/** The exec context face the tools read (agent identity + session cwd). */\nexport interface ToolRunContext {\n agent?: { id: string; session: { header: { cwd?: string } } }\n}\n\n/** Registry-like context face (tests stub this). */\nexport interface ToolContextFace {\n tools: { register(tool: { name: string }): unknown }\n}\n\n/**\n * Register all eight tools.\n * @param ctx - a context exposing `tools.register`.\n * @param deps - store + workspaces + clock.\n * @returns dispose functions, one per tool.\n */\nexport function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Array<() => void> {\n const disposers: Array<() => void> = []\n const { store, workspaces } = deps\n\n // Env-gated tool-call tracing (ATB_TRACE=1) — evidence for protocol E2E.\n const register = (tool: { name: string; execute?: unknown }) => {\n if (process.env.ATB_TRACE === '1' && typeof tool.execute === 'function') {\n const orig = tool.execute as (args: unknown, exec: unknown) => Promise<unknown>\n tool.execute = async (args: unknown, exec: unknown) => {\n console.error(`[atb ▶] ${tool.name}`, JSON.stringify(args).slice(0, 300))\n try {\n const result = await orig(args, exec)\n console.error(`[atb ✓] ${tool.name}`, JSON.stringify(result).slice(0, 300))\n return result\n } catch (error) {\n console.error(`[atb ✗] ${tool.name}`, String(error).slice(0, 400))\n throw error\n }\n }\n }\n return ctx.tools.register(tool as { name: string })\n }\n\n // ------------------------------------------------------------------ list\n disposers.push(register(defineTool({\n name: 'taskboard_list',\n description:\n 'List task-board tasks. Filter by project (workspaceId), status, or urgency. '\n + 'Returns compact summaries (id, title, status, urgency, version, claim owner). '\n + 'Check this before starting work to find claimable todo tasks in your project.',\n parameters: {\n workspaceId: { type: 'string', description: 'Filter by project (DSH workspace id).' },\n status: { type: 'string', description: 'Filter by exact status (backlog/todo/in_progress/in_review/done/canceled/archived).' },\n urgency: { type: 'string', description: 'Filter by urgency (urgent/normal/relaxed).' },\n includeTrashed: { type: 'boolean', description: 'Include soft-deleted tasks (default false).' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { revision?: number; tasks?: Array<Record<string, unknown>> }\n const tasks = v.tasks ?? []\n const head = `任务 ${tasks.length} 条(台账 rev ${v.revision ?? '?'})`\n if (tasks.length === 0) return [{ type: 'text', text: `${head}:无匹配任务。` }]\n return [{ type: 'text', text: [head, ...tasks.map(t => taskLine(t as never))].join('\\n') }]\n },\n },\n async execute(args) {\n try {\n const a = args as { workspaceId?: string; status?: string; urgency?: string; includeTrashed?: boolean }\n const tasks = store.snapshot().tasks.filter(t =>\n (a.workspaceId === undefined || t.workspaceId === a.workspaceId)\n && (a.status === undefined || t.status === a.status)\n && (a.urgency === undefined || t.urgency === a.urgency)\n && (a.includeTrashed === true || t.trashedAt === undefined))\n return json({ revision: store.snapshot().revision, tasks: tasks.map(summarize) })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ------------------------------------------------------------------- get\n disposers.push(register(defineTool({\n name: 'taskboard_get',\n description:\n 'Read one task in full: description, prompt, project, urgency, status, comments, executions, version. '\n + 'Read this (and the comments) BEFORE claiming or starting work on a task.',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id from the board.' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { task?: TaskRecord & { effectivePrompt?: string } }\n return [{ type: 'text', text: v.task === undefined ? '任务不存在。' : taskDetail(v.task) }]\n },\n },\n async execute(args: { id: string }) {\n try {\n const { id } = args\n const task = store.get(id)\n if (task === undefined || task.trashedAt !== undefined) throw new ToolError(ERR.notFound, `no task ${id}`)\n return json({ task: { ...task, effectivePrompt: effectivePrompt(task) } })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ---------------------------------------------------------------- create\n disposers.push(register(defineTool({\n name: 'taskboard_create',\n description:\n 'Create a task on the board. Required: title, workspaceId (project), urgency (urgent/normal/relaxed). '\n + 'Optional: description, prompt (sent to a fresh session on execution), status (default todo), '\n + 'execution mode (claim|scheduled + cron), model {provider, model} to pin executions to a model. '\n + 'Do not track trivial requests as tasks.',\n parameters: {\n title: { type: 'string', required: true, description: 'Short imperative line (1..200 chars).' },\n workspaceId: { type: 'string', required: true, description: 'Project (DSH workspace id) this task belongs to.' },\n urgency: { type: 'string', required: true, description: 'urgent (red) | normal (purple) | relaxed (blue).' },\n description: { type: 'string', description: 'What the task involves (plain text).' },\n prompt: { type: 'string', description: 'Prompt sent to a fresh session when executed; default = title+description.' },\n status: { type: 'string', description: 'Initial status; default todo. backlog = not approved for execution.' },\n execution: {\n type: 'object',\n additionalProperties: false,\n description: 'Execution config: { mode: \"claim\" } (default) or { mode: \"scheduled\", cron: \"m h dom mon dow\" }.',\n properties: {\n mode: { type: 'string', description: 'claim | scheduled.' },\n cron: { type: 'string', description: 'Five-field cron expression (scheduled only).' },\n },\n },\n model: {\n type: 'object',\n additionalProperties: false,\n description: 'Pin executions to one configured model: { provider, model }. Omit to use the default model.',\n properties: {\n provider: { type: 'string', description: 'Provider route id.' },\n model: { type: 'string', description: 'Provider-owned model id.' },\n },\n },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { task?: { id?: string; status?: string; version?: number } }\n const t = v.task\n return [{ type: 'text', text: t === undefined ? '创建失败。' : `已创建任务 ${t.id} [${t.status}] v${t.version}。写入前先 taskboard_get 读取。` }]\n },\n },\n async execute(args: {\n title: string\n workspaceId: string\n urgency: string\n status?: string\n description?: string\n prompt?: string\n execution?: { mode?: string; cron?: string }\n model?: { provider?: string; model?: string }\n }, exec: unknown) {\n try {\n const { actor } = caller(exec as ToolRunContext)\n const title = normalizeTitle(args.title)\n if (workspaces.get(args.workspaceId) === undefined) {\n throw new ToolError(ERR.notFound, `unknown workspaceId ${args.workspaceId}`)\n }\n const urgency = asUrgency(args.urgency)\n const status = args.status === undefined ? 'todo' as const : asStatus(args.status)\n if (status === 'done' || status === 'archived') {\n throw new ToolError(ERR.invalidTransition, 'a new task cannot start as done/archived')\n }\n const execution = normalizeExecution(args.execution ?? {}, deps.now())\n if (args.model !== undefined && (typeof args.model.provider !== 'string' || typeof args.model.model !== 'string')) {\n throw new ToolError(ERR.invalidInput, 'model must be { provider: string, model: string }')\n }\n const now = deps.now()\n const task: TaskRecord = {\n id: newTaskId(),\n title,\n description: (args.description ?? '').trim(),\n prompt: normalizePrompt(args.prompt),\n workspaceId: args.workspaceId,\n urgency,\n status,\n blocked: false,\n execution,\n model: args.model as TaskModel | undefined,\n version: 1,\n createdAt: now,\n updatedAt: now,\n createdBy: actor,\n updatedBy: actor,\n comments: [],\n executions: [],\n }\n await store.mutate('task-created', ledger => {\n ledger.tasks.push(task)\n return [task]\n })\n return json({ task: summarize(task) })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ---------------------------------------------------------------- update\n disposers.push(register(defineTool({\n name: 'taskboard_update',\n description:\n 'Update a task\\'s title/description/prompt/urgency/blocked. Requires ifVersion (read first). '\n + 'The model and execution config are read-only through this tool (they belong to the task owner/user).',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id.' },\n ifVersion: { type: 'number', required: true, description: 'The task version you read; the write fails on mismatch.' },\n title: { type: 'string', description: 'New title.' },\n description: { type: 'string', description: 'New description.' },\n prompt: { type: 'string', description: 'New execution prompt.' },\n urgency: { type: 'string', description: 'urgent | normal | relaxed.' },\n blocked: { type: 'boolean', description: 'Blocked marker (work cannot continue right now).' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { task?: { id?: string; status?: string; version?: number } }\n const t = v.task\n return [{ type: 'text', text: t === undefined ? '更新失败。' : `已更新任务 ${t.id},当前 v${t.version} [${t.status}]。` }]\n },\n },\n async execute(args: {\n id: string\n ifVersion: number\n title?: string\n description?: string\n prompt?: string\n urgency?: string\n blocked?: boolean\n }, exec: unknown) {\n try {\n const { actor } = caller(exec as ToolRunContext)\n const task = store.get(args.id)\n if (task === undefined || task.trashedAt !== undefined) throw new ToolError(ERR.notFound, `no task ${args.id}`)\n versionGuard(task, args.ifVersion)\n if (task.status === 'archived') throw new ToolError(ERR.invalidTransition, 'archived tasks are immutable')\n const next: TaskRecord = structuredClone(task)\n if (args.title !== undefined) next.title = normalizeTitle(args.title)\n if (args.description !== undefined) next.description = args.description.trim()\n if (args.prompt !== undefined) next.prompt = normalizePrompt(args.prompt)\n if (args.urgency !== undefined) next.urgency = asUrgency(args.urgency)\n if (args.blocked !== undefined) next.blocked = args.blocked\n next.version = task.version + 1\n next.updatedAt = deps.now()\n next.updatedBy = actor\n await store.mutate('task-updated', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === args.id)\n ledger.tasks[i] = next\n return [next]\n })\n return json({ task: summarize(next) })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ------------------------------------------------------------------ move\n disposers.push(register(defineTool({\n name: 'taskboard_move',\n description:\n 'Move a task between statuses (requires ifVersion). Claim = todo→in_progress (only a session '\n + 'inside the task\\'s project may claim; never take over a task held by another session). '\n + 'After implementing and self-verifying: comment, then in_progress→in_review. '\n + 'You can NEVER move a task to done — that requires explicit user confirmation.',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id.' },\n status: { type: 'string', required: true, description: 'Target status.' },\n ifVersion: { type: 'number', required: true, description: 'Task version you read; fails on mismatch.' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { task?: { id?: string; status?: string; version?: number } }\n const t = v.task\n return [{ type: 'text', text: t === undefined ? '移动失败。' : `任务 ${t.id} 已移到 ${t.status},当前 v${t.version}。` }]\n },\n },\n async execute(args: { id: string; status: string; ifVersion: number }, exec: unknown) {\n try {\n const { actor } = caller(exec as ToolRunContext)\n const to = asStatus(args.status)\n const task = store.get(args.id)\n if (task === undefined || task.trashedAt !== undefined) throw new ToolError(ERR.notFound, `no task ${args.id}`)\n versionGuard(task, args.ifVersion)\n\n // Code-level gate: agents never complete a task.\n if (to === 'done') {\n throw new ToolError(ERR.forbidden, 'moving a task to done requires explicit user confirmation (GUI); agents cannot do it')\n }\n if (!canTransition(task.status, to)) {\n throw new ToolError(ERR.invalidTransition, `illegal transition ${task.status} → ${to}`)\n }\n // Exclusive hold: while a task is in_progress under an agent, no other\n // session may move it at all (that would be a takeover).\n if (task.status === 'in_progress' && task.updatedBy.kind === 'agent' && task.updatedBy.sessionId !== actor.sessionId) {\n throw new ToolError(ERR.forbidden, `task is held by session ${task.updatedBy.sessionId}; never take over another session's claim`)\n }\n // Claim boundary: the calling session must belong to the task's project.\n if (isClaim(task.status, to)) {\n const wsId = await callerWorkspace(deps, exec as ToolRunContext)\n if (wsId !== task.workspaceId) {\n throw new ToolError(ERR.workspaceMismatch, 'only a session inside this task\\'s project may claim it')\n }\n }\n const next: TaskRecord = structuredClone(task)\n next.status = to\n next.version = task.version + 1\n next.updatedAt = deps.now()\n next.updatedBy = actor\n if (isClaim(task.status, to)) next.blocked = false\n await store.mutate('task-moved', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === args.id)\n ledger.tasks[i] = next\n return [next]\n })\n return json({ task: summarize(next) })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ----------------------------------------------------------- comment_add\n disposers.push(register(defineTool({\n name: 'taskboard_comment_add',\n description:\n 'Append a progress/report comment to a task. When handing off to review, the comment should cover: '\n + 'what changed, how it was verified, outcome, and remaining risks.',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id.' },\n body: { type: 'string', required: true, description: 'Comment text (1..4000 chars).' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { comment?: { id?: string }; task?: { id?: string; version?: number; status?: string } }\n const c = v.comment\n const t = v.task\n if (c === undefined || t === undefined) return [{ type: 'text', text: '评论失败。' }]\n // The comment bumped the version — echo it so the agent can chain the\n // next write (e.g. move → in_review) WITHOUT re-reading.\n return [{\n type: 'text',\n text: `评论 ${c.id} 已添加;任务 ${t.id} 当前 v${t.version} [${t.status}](后续写操作用此版本号).`,\n }]\n },\n },\n async execute(args: { id: string; body: string }, exec: unknown) {\n try {\n const { sessionId } = caller(exec as ToolRunContext)\n const task = store.get(args.id)\n if (task === undefined || task.trashedAt !== undefined) throw new ToolError(ERR.notFound, `no task ${args.id}`)\n const comment = {\n id: newCommentId(),\n body: normalizeBody(args.body),\n version: 1,\n createdAt: deps.now(),\n threadId: sessionId,\n }\n const next: TaskRecord = structuredClone(task)\n next.comments.push(comment)\n next.version = task.version + 1\n next.updatedAt = deps.now()\n await store.mutate('comment-added', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === args.id)\n ledger.tasks[i] = next\n return [next]\n })\n return json({ comment, task: { id: next.id, version: next.version, status: next.status } })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // -------------------------------------------------------------- comments\n disposers.push(register(defineTool({\n name: 'taskboard_comments',\n description: 'List a task\\'s comments, oldest first. Read them before deciding to start work.',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id.' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { comments?: unknown[] }\n const list = v.comments as Array<{ body: string; createdAt: number; threadId?: string }> | undefined\n if (list === undefined || list.length === 0) return [{ type: 'text', text: '无评论。' }]\n const lines = list.map(c => {\n const who = c.threadId !== undefined ? `agent ${String(c.threadId).slice(0, 24)}` : 'user'\n return `- [${who} ${new Date(c.createdAt).toISOString()}] ${c.body}`\n })\n return [{ type: 'text', text: `评论 ${list.length} 条:\\n${lines.join('\\n')}` }]\n },\n },\n async execute(args: { id: string }) {\n try {\n const task = store.get(args.id)\n if (task === undefined || task.trashedAt !== undefined) throw new ToolError(ERR.notFound, `no task ${args.id}`)\n return json({ comments: task.comments })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ---------------------------------------------------------------- delete\n disposers.push(register(defineTool({\n name: 'taskboard_delete',\n description:\n 'Soft-delete a task (marks it trashed; the user confirms the purge in the GUI). '\n + 'Requires ifVersion. Prefer canceled/archived over delete unless the task was a mistake.',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id.' },\n ifVersion: { type: 'number', required: true, description: 'Task version you read.' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { trashed?: boolean }\n return [{ type: 'text', text: v.trashed === true ? '任务已标记删除(等待用户在 GUI 清除)。' : '删除失败。' }]\n },\n },\n async execute(args: { id: string; ifVersion: number }, exec: unknown) {\n try {\n caller(exec as ToolRunContext)\n const task = store.get(args.id)\n if (task === undefined) throw new ToolError(ERR.notFound, `no task ${args.id}`)\n versionGuard(task, args.ifVersion)\n const next: TaskRecord = structuredClone(task)\n next.trashedAt = deps.now()\n next.version = task.version + 1\n await store.mutate('task-deleted', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === args.id)\n ledger.tasks[i] = next\n return [next]\n })\n return { trashed: true }\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n return disposers\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tools.js","names":["v"],"sources":["../../src/host/tools.ts"],"sourcesContent":["/**\n * The eight `taskboard_*` agent tools. All writes require a calling agent\n * session (ownership audit), carry optimistic-version checks, and enforce\n * the protocol gates in CODE, not in prompt text:\n *\n * - `move → done` is rejected for agent callers (user confirmation only)\n * - `move todo → in_progress` requires the calling session's workspace to\n * match the task's project (claim boundary)\n * - taking over a task held by another session is rejected\n * - `delete` for agent callers only sets the soft-delete marker\n *\n * OUTPUT CONTRACT (lesson: registry `createSuccessResult` renders\n * `output.render(args, value)` into `result.content`, and the loop feeds\n * exactly that content to the model — the raw JSON `value` never reaches\n * the model): render() IS the model-facing tool result. Every render must\n * carry the complete facts an agent needs to act (ids, versions, statuses);\n * a \"terse UI summary\" here starves the agent.\n *\n * @module dsh-taskboard/host/tools\n */\nimport type { WorkspaceRegistry } from '@deepseek-ai/dsh-workspace'\nimport { defineTool } from './sdk.ts'\nimport {\n asStatus,\n asUrgency,\n canTransition,\n effectivePrompt,\n isClaim,\n isClaimedBy,\n newCommentId,\n newTaskId,\n normalizeBody,\n normalizeExecution,\n normalizeModel,\n normalizePrompt,\n normalizeTitle,\n summarize,\n syncClaim,\n type Actor,\n type TaskModel,\n type TaskRecord,\n} from '../shared/protocol.ts'\nimport type { TaskStore } from './store.ts'\n\n/** Render side: one compact task line (id/status/version are load-bearing). */\nfunction taskLine(t: {\n id: string\n title: string\n status: string\n urgency: string\n version: number\n workspaceId: string\n blocked: boolean\n executionMode: string\n commentCount?: number\n lastExecutionOutcome?: string\n trashed?: boolean\n}): string {\n const parts = [\n `- ${t.id} [${t.status}] v${t.version} · ${t.urgency} · 项目 ${t.workspaceId}`,\n `「${t.title}」`,\n ]\n if (t.blocked) parts.push('·受阻')\n if (t.executionMode === 'scheduled') parts.push('·定时')\n if (t.commentCount !== undefined && t.commentCount > 0) parts.push(`·评论${t.commentCount}`)\n if (t.lastExecutionOutcome !== undefined) parts.push(`·上次执行${t.lastExecutionOutcome}`)\n if (t.trashed === true) parts.push('·已删')\n return parts.join(' ')\n}\n\n/** Render side: the full task detail block (everything an executor needs). */\nfunction taskDetail(t: TaskRecord & { effectivePrompt?: string }): string {\n const lines: string[] = [\n `任务 ${t.id} 「${t.title}」`,\n `状态: ${t.status} (v${t.version}) · 紧急度: ${t.urgency} · 项目: ${t.workspaceId}${t.blocked ? ' · 受阻' : ''}`,\n `执行方式: ${t.execution.mode}${t.execution.cron !== undefined ? ` cron=${t.execution.cron}` : ''}`,\n ]\n const holder = isClaimedBy(t)\n if (holder !== undefined) lines.push(`认领: agent ${String(holder).slice(0, 24)}(持有期间其他会话不可移动)`)\n if (t.execution.nextRunAt !== undefined) lines.push(`下次触发: ${new Date(t.execution.nextRunAt).toISOString()}`)\n if (t.model !== undefined) lines.push(`固定模型: ${t.model.provider}/${t.model.model}`)\n lines.push(`描述: ${t.description.length > 0 ? t.description : '(无)'}`)\n lines.push(`执行 Prompt: ${t.effectivePrompt ?? effectivePrompt(t)}`)\n if (t.comments.length > 0) {\n lines.push(`评论 (${t.comments.length}):`)\n for (const c of t.comments) {\n const who = c.threadId !== undefined ? `agent ${String(c.threadId).slice(0, 24)}` : 'user'\n lines.push(` - [${who} ${new Date(c.createdAt).toISOString()}] ${c.body}`)\n }\n } else {\n lines.push('评论: 无')\n }\n if (t.executions.length > 0) {\n lines.push(`执行记录 (${t.executions.length}):`)\n for (const e of t.executions) {\n const at = e.startedAt !== undefined ? new Date(e.startedAt).toISOString() : '?'\n const err = e.error !== undefined ? ` 错误: ${e.error}` : ''\n lines.push(` - [${e.trigger} ${at}] ${e.outcome}${err}`)\n }\n } else {\n lines.push('执行记录: 无')\n }\n const updatedBy = t.updatedBy.kind === 'agent' ? `agent ${String(t.updatedBy.sessionId).slice(0, 24)}` : 'user'\n lines.push(`更新: ${new Date(t.updatedAt).toISOString()} 由 ${updatedBy}`)\n return lines.join('\\n')\n}\n\n/** Stable error codes surfaced at the head of tool error messages. */\nexport const ERR = {\n notFound: 'not_found',\n versionConflict: 'version_conflict',\n workspaceMismatch: 'workspace_mismatch',\n invalidTransition: 'invalid_transition',\n forbidden: 'forbidden',\n requiresAgent: 'unauthorized_actor',\n invalidInput: 'invalid_input',\n} as const\n\n/** Tool failure: an Error whose message starts with a stable code. */\nclass ToolError extends Error {\n constructor(readonly code: string, detail: string) {\n super(`Error: ${code}: ${detail}`)\n }\n}\n\n/** The workspace face the tools need (narrow for tests). */\nexport interface WorkspaceFace {\n /** Resolve the workspace owning a canonical cwd, if any. */\n resolveByPath(path: string): Promise<{ id: string } | undefined>\n /** Get a workspace by id. */\n get(id: string): { id: string; path: string; title: string } | undefined\n /** List all workspaces. */\n list(): Array<{ id: string; path: string; title: string }>\n}\n\n/** Adapt the real registry to the narrow face. */\nexport function workspaceFace(registry: WorkspaceRegistry): WorkspaceFace {\n // Explicit field mapping: Workspace entities expose path/title as prototype\n // getters, which JSON.stringify skips (own enumerable properties only).\n return {\n resolveByPath: async (path) => {\n const ws = await registry.resolveByPath(path as never)\n return ws === undefined ? undefined : { id: ws.id }\n },\n get: id => {\n const ws = registry.get(id as never)\n return ws === undefined ? undefined : { id: ws.id, path: ws.path, title: ws.title }\n },\n list: () => registry.list().map(ws => ({ id: ws.id, path: ws.path, title: ws.title })),\n }\n}\n\n/** Everything the tool set needs. */\nexport interface ToolDeps {\n store: TaskStore\n workspaces: WorkspaceFace\n /** Current epoch ms (injectable for tests). */\n now: () => number\n /**\n * Registered model provider routes (from the host llm runtime), for\n * advisory validation of pinned models; undefined = runtime unavailable,\n * in which case only the structural check applies.\n */\n modelProviders?: () => string[] | undefined\n}\n\n/** Validate a pinned model: structural check always, provider route when known. */\nfunction checkModel(deps: ToolDeps, raw: unknown): TaskModel {\n const model = normalizeModel(raw)\n const providers = deps.modelProviders?.()\n if (providers !== undefined && !providers.includes(model.provider)) {\n throw new ToolError(ERR.invalidInput, `model provider \"${model.provider}\" has no registered route (available: ${providers.join(', ')})`)\n }\n return model\n}\n\n/** Resolve the calling agent's actor and session id. */\nfunction caller(exec: ToolRunContext): { actor: Actor & { kind: 'agent' }; sessionId: string } {\n if (!exec.agent) throw new ToolError(ERR.requiresAgent, 'taskboard tools require a calling agent session')\n const sessionId = exec.agent.id\n return { actor: { kind: 'agent', sessionId }, sessionId }\n}\n\n/** The calling session's workspace id (undefined when unaffiliated). */\nasync function callerWorkspace(deps: ToolDeps, exec: ToolRunContext): Promise<string | undefined> {\n const cwd = exec.agent?.session.header.cwd\n if (typeof cwd !== 'string' || cwd.length === 0) return undefined\n const ws = await deps.workspaces.resolveByPath(cwd)\n return ws?.id\n}\n\n/** Guard: version match. */\nfunction versionGuard(task: TaskRecord, ifVersion: number | undefined): void {\n if (ifVersion === undefined) {\n throw new ToolError(ERR.versionConflict, 'this write requires ifVersion; read the task first')\n }\n if (ifVersion !== task.version) {\n throw new ToolError(ERR.versionConflict, `stale version ${ifVersion} (current ${task.version}); re-read the task and retry once`)\n }\n}\n\n/** Re-throw with a stable code; non-ToolErrors become invalid_input. */\nfunction fail(error: unknown): never {\n if (error instanceof ToolError) throw error\n const message = error instanceof Error ? error.message : String(error)\n throw new ToolError(ERR.invalidInput, message)\n}\n\n/** Loose json output schema shared by every taskboard tool. */\nconst JSON_OUT = { type: 'json' } as const\n\n/** Deep-JSON a value for a json-rooted tool output (spread results lose implicit index signatures). */\nfunction json<T>(value: T): Record<string, unknown> {\n return JSON.parse(JSON.stringify(value)) as Record<string, unknown>\n}\n\n/** The exec context face the tools read (agent identity + session cwd). */\nexport interface ToolRunContext {\n agent?: { id: string; session: { header: { cwd?: string } } }\n}\n\n/** Registry-like context face (tests stub this). */\nexport interface ToolContextFace {\n tools: { register(tool: { name: string }): unknown }\n}\n\n/**\n * Register all eight tools.\n * @param ctx - a context exposing `tools.register`.\n * @param deps - store + workspaces + clock.\n * @returns dispose functions, one per tool.\n */\nexport function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Array<() => void> {\n const disposers: Array<() => void> = []\n const { store, workspaces } = deps\n\n // Env-gated tool-call tracing (ATB_TRACE=1) — evidence for protocol E2E.\n const register = (tool: { name: string; execute?: unknown }) => {\n if (process.env.ATB_TRACE === '1' && typeof tool.execute === 'function') {\n const orig = tool.execute as (args: unknown, exec: unknown) => Promise<unknown>\n tool.execute = async (args: unknown, exec: unknown) => {\n console.error(`[atb ▶] ${tool.name}`, JSON.stringify(args).slice(0, 300))\n try {\n const result = await orig(args, exec)\n console.error(`[atb ✓] ${tool.name}`, JSON.stringify(result).slice(0, 300))\n return result\n } catch (error) {\n console.error(`[atb ✗] ${tool.name}`, String(error).slice(0, 400))\n throw error\n }\n }\n }\n return ctx.tools.register(tool as { name: string })\n }\n\n // ------------------------------------------------------------------ list\n disposers.push(register(defineTool({\n name: 'taskboard_list',\n description:\n 'List task-board tasks. Filter by project (workspaceId), status, or urgency. '\n + 'Returns compact summaries (id, title, status, urgency, version, claim owner). '\n + 'Check this before starting work to find claimable todo tasks in your project.',\n parameters: {\n workspaceId: { type: 'string', description: 'Filter by project (DSH workspace id).' },\n status: { type: 'string', description: 'Filter by exact status (backlog/todo/in_progress/in_review/done/canceled/archived).' },\n urgency: { type: 'string', description: 'Filter by urgency (urgent/normal/relaxed).' },\n includeTrashed: { type: 'boolean', description: 'Include soft-deleted tasks (default false).' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { revision?: number; tasks?: Array<Record<string, unknown>> }\n const tasks = v.tasks ?? []\n const head = `任务 ${tasks.length} 条(台账 rev ${v.revision ?? '?'})`\n if (tasks.length === 0) return [{ type: 'text', text: `${head}:无匹配任务。` }]\n return [{ type: 'text', text: [head, ...tasks.map(t => taskLine(t as never))].join('\\n') }]\n },\n },\n async execute(args) {\n try {\n const a = args as { workspaceId?: string; status?: string; urgency?: string; includeTrashed?: boolean }\n const tasks = store.snapshot().tasks.filter(t =>\n (a.workspaceId === undefined || t.workspaceId === a.workspaceId)\n && (a.status === undefined || t.status === a.status)\n && (a.urgency === undefined || t.urgency === a.urgency)\n && (a.includeTrashed === true || t.trashedAt === undefined))\n return json({ revision: store.snapshot().revision, tasks: tasks.map(summarize) })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ------------------------------------------------------------------- get\n disposers.push(register(defineTool({\n name: 'taskboard_get',\n description:\n 'Read one task in full: description, prompt, project, urgency, status, comments, executions, version. '\n + 'Read this (and the comments) BEFORE claiming or starting work on a task.',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id from the board.' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { task?: TaskRecord & { effectivePrompt?: string } }\n return [{ type: 'text', text: v.task === undefined ? '任务不存在。' : taskDetail(v.task) }]\n },\n },\n async execute(args: { id: string }) {\n try {\n const { id } = args\n const task = store.get(id)\n if (task === undefined || task.trashedAt !== undefined) throw new ToolError(ERR.notFound, `no task ${id}`)\n return json({ task: { ...task, effectivePrompt: effectivePrompt(task) } })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ---------------------------------------------------------------- create\n disposers.push(register(defineTool({\n name: 'taskboard_create',\n description:\n 'Create a task on the board. Required: title, workspaceId (project), urgency (urgent/normal/relaxed). '\n + 'Optional: description, prompt (sent to a fresh session on execution), status (default todo), '\n + 'execution mode (claim|scheduled + cron), model {provider, model} to pin executions to a model. '\n + 'Do not track trivial requests as tasks.',\n parameters: {\n title: { type: 'string', required: true, description: 'Short imperative line (1..200 chars).' },\n workspaceId: { type: 'string', required: true, description: 'Project (DSH workspace id) this task belongs to.' },\n urgency: { type: 'string', required: true, description: 'urgent (red) | normal (purple) | relaxed (blue).' },\n description: { type: 'string', description: 'What the task involves (plain text).' },\n prompt: { type: 'string', description: 'Prompt sent to a fresh session when executed; default = title+description.' },\n status: { type: 'string', description: 'Initial status; default todo. backlog = not approved for execution.' },\n execution: {\n type: 'object',\n additionalProperties: false,\n description: 'Execution config: { mode: \"claim\" } (default) or { mode: \"scheduled\", cron: \"m h dom mon dow\" }.',\n properties: {\n mode: { type: 'string', description: 'claim | scheduled.' },\n cron: { type: 'string', description: 'Five-field cron expression (scheduled only).' },\n },\n },\n model: {\n type: 'object',\n additionalProperties: false,\n description: 'Pin executions to one configured model: { provider, model }. Omit to use the default model.',\n properties: {\n provider: { type: 'string', description: 'Provider route id.' },\n model: { type: 'string', description: 'Provider-owned model id.' },\n },\n },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { task?: { id?: string; status?: string; version?: number } }\n const t = v.task\n return [{ type: 'text', text: t === undefined ? '创建失败。' : `已创建任务 ${t.id} [${t.status}] v${t.version}。写入前先 taskboard_get 读取。` }]\n },\n },\n async execute(args: {\n title: string\n workspaceId: string\n urgency: string\n status?: string\n description?: string\n prompt?: string\n execution?: { mode?: string; cron?: string }\n model?: { provider?: string; model?: string }\n }, exec: unknown) {\n try {\n const { actor } = caller(exec as ToolRunContext)\n const title = normalizeTitle(args.title)\n if (workspaces.get(args.workspaceId) === undefined) {\n throw new ToolError(ERR.notFound, `unknown workspaceId ${args.workspaceId}`)\n }\n const urgency = asUrgency(args.urgency)\n const status = args.status === undefined ? 'todo' as const : asStatus(args.status)\n if (status === 'done' || status === 'archived') {\n throw new ToolError(ERR.invalidTransition, 'a new task cannot start as done/archived')\n }\n const execution = normalizeExecution(args.execution ?? {}, deps.now())\n const model = args.model !== undefined ? checkModel(deps, args.model) : undefined\n const now = deps.now()\n const task: TaskRecord = {\n id: newTaskId(),\n title,\n description: (args.description ?? '').trim(),\n prompt: normalizePrompt(args.prompt),\n workspaceId: args.workspaceId,\n urgency,\n status,\n blocked: false,\n execution,\n model,\n version: 1,\n createdAt: now,\n updatedAt: now,\n createdBy: actor,\n updatedBy: actor,\n comments: [],\n executions: [],\n }\n await store.mutate('task-created', ledger => {\n ledger.tasks.push(task)\n return [task]\n })\n return json({ task: summarize(task) })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ---------------------------------------------------------------- update\n disposers.push(register(defineTool({\n name: 'taskboard_update',\n description:\n 'Update a task\\'s title/description/prompt/urgency/blocked. Requires ifVersion (read first). '\n + 'The model and execution config are read-only through this tool (they belong to the task owner/user).',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id.' },\n ifVersion: { type: 'number', required: true, description: 'The task version you read; the write fails on mismatch.' },\n title: { type: 'string', description: 'New title.' },\n description: { type: 'string', description: 'New description.' },\n prompt: { type: 'string', description: 'New execution prompt.' },\n urgency: { type: 'string', description: 'urgent | normal | relaxed.' },\n blocked: { type: 'boolean', description: 'Blocked marker (work cannot continue right now).' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { task?: { id?: string; status?: string; version?: number } }\n const t = v.task\n return [{ type: 'text', text: t === undefined ? '更新失败。' : `已更新任务 ${t.id},当前 v${t.version} [${t.status}]。` }]\n },\n },\n async execute(args: {\n id: string\n ifVersion: number\n title?: string\n description?: string\n prompt?: string\n urgency?: string\n blocked?: boolean\n }, exec: unknown) {\n try {\n const { actor } = caller(exec as ToolRunContext)\n const task = store.get(args.id)\n if (task === undefined || task.trashedAt !== undefined) throw new ToolError(ERR.notFound, `no task ${args.id}`)\n versionGuard(task, args.ifVersion)\n if (task.status === 'archived') throw new ToolError(ERR.invalidTransition, 'archived tasks are immutable')\n const next: TaskRecord = structuredClone(task)\n if (args.title !== undefined) next.title = normalizeTitle(args.title)\n if (args.description !== undefined) next.description = args.description.trim()\n if (args.prompt !== undefined) next.prompt = normalizePrompt(args.prompt)\n if (args.urgency !== undefined) next.urgency = asUrgency(args.urgency)\n if (args.blocked !== undefined) next.blocked = args.blocked\n next.version = task.version + 1\n next.updatedAt = deps.now()\n next.updatedBy = actor\n await store.mutate('task-updated', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === args.id)\n ledger.tasks[i] = next\n return [next]\n })\n return json({ task: summarize(next) })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ------------------------------------------------------------------ move\n disposers.push(register(defineTool({\n name: 'taskboard_move',\n description:\n 'Move a task between statuses (requires ifVersion). Claim = todo→in_progress (only a session '\n + 'inside the task\\'s project may claim; never take over a task held by another session). '\n + 'After implementing and self-verifying: comment, then in_progress→in_review. '\n + 'You can NEVER move a task to done — that requires explicit user confirmation.',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id.' },\n status: { type: 'string', required: true, description: 'Target status.' },\n ifVersion: { type: 'number', required: true, description: 'Task version you read; fails on mismatch.' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { task?: { id?: string; status?: string; version?: number } }\n const t = v.task\n return [{ type: 'text', text: t === undefined ? '移动失败。' : `任务 ${t.id} 已移到 ${t.status},当前 v${t.version}。` }]\n },\n },\n async execute(args: { id: string; status: string; ifVersion: number }, exec: unknown) {\n try {\n const { actor } = caller(exec as ToolRunContext)\n const to = asStatus(args.status)\n const task = store.get(args.id)\n if (task === undefined || task.trashedAt !== undefined) throw new ToolError(ERR.notFound, `no task ${args.id}`)\n versionGuard(task, args.ifVersion)\n\n // Code-level gate: agents never complete a task.\n if (to === 'done') {\n throw new ToolError(ERR.forbidden, 'moving a task to done requires explicit user confirmation (GUI); agents cannot do it')\n }\n if (!canTransition(task.status, to)) {\n throw new ToolError(ERR.invalidTransition, `illegal transition ${task.status} → ${to}`)\n }\n // Exclusive hold: while a task is in_progress under a session\n // (explicit claimedBy — an agent claim or a live execution), no other\n // session may move it (that would be a takeover).\n if (task.status === 'in_progress' && task.claimedBy !== undefined && task.claimedBy !== actor.sessionId) {\n throw new ToolError(ERR.forbidden, `task is held by session ${task.claimedBy}; never take over another session's claim`)\n }\n // Claim boundary: the calling session must belong to the task's project.\n if (isClaim(task.status, to)) {\n const wsId = await callerWorkspace(deps, exec as ToolRunContext)\n if (wsId !== task.workspaceId) {\n throw new ToolError(ERR.workspaceMismatch, 'only a session inside this task\\'s project may claim it')\n }\n }\n const next: TaskRecord = structuredClone(task)\n next.status = to\n next.version = task.version + 1\n next.updatedAt = deps.now()\n next.updatedBy = actor\n if (isClaim(task.status, to)) next.blocked = false\n // Record the holder on a claim; every move out of in_progress releases it.\n syncClaim(next, to, deps.now(), isClaim(task.status, to) ? actor.sessionId : undefined)\n await store.mutate('task-moved', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === args.id)\n ledger.tasks[i] = next\n return [next]\n })\n return json({ task: summarize(next) })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ----------------------------------------------------------- comment_add\n disposers.push(register(defineTool({\n name: 'taskboard_comment_add',\n description:\n 'Append a progress/report comment to a task. When handing off to review, the comment should cover: '\n + 'what changed, how it was verified, outcome, and remaining risks.',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id.' },\n body: { type: 'string', required: true, description: 'Comment text (1..4000 chars).' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { comment?: { id?: string }; task?: { id?: string; version?: number; status?: string } }\n const c = v.comment\n const t = v.task\n if (c === undefined || t === undefined) return [{ type: 'text', text: '评论失败。' }]\n // The comment bumped the version — echo it so the agent can chain the\n // next write (e.g. move → in_review) WITHOUT re-reading.\n return [{\n type: 'text',\n text: `评论 ${c.id} 已添加;任务 ${t.id} 当前 v${t.version} [${t.status}](后续写操作用此版本号).`,\n }]\n },\n },\n async execute(args: { id: string; body: string }, exec: unknown) {\n try {\n const { sessionId } = caller(exec as ToolRunContext)\n const task = store.get(args.id)\n if (task === undefined || task.trashedAt !== undefined) throw new ToolError(ERR.notFound, `no task ${args.id}`)\n const comment = {\n id: newCommentId(),\n body: normalizeBody(args.body),\n version: 1,\n createdAt: deps.now(),\n threadId: sessionId,\n }\n const next: TaskRecord = structuredClone(task)\n next.comments.push(comment)\n next.version = task.version + 1\n next.updatedAt = deps.now()\n await store.mutate('comment-added', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === args.id)\n ledger.tasks[i] = next\n return [next]\n })\n return json({ comment, task: { id: next.id, version: next.version, status: next.status } })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // -------------------------------------------------------------- comments\n disposers.push(register(defineTool({\n name: 'taskboard_comments',\n description: 'List a task\\'s comments, oldest first. Read them before deciding to start work.',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id.' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { comments?: unknown[] }\n const list = v.comments as Array<{ body: string; createdAt: number; threadId?: string }> | undefined\n if (list === undefined || list.length === 0) return [{ type: 'text', text: '无评论。' }]\n const lines = list.map(c => {\n const who = c.threadId !== undefined ? `agent ${String(c.threadId).slice(0, 24)}` : 'user'\n return `- [${who} ${new Date(c.createdAt).toISOString()}] ${c.body}`\n })\n return [{ type: 'text', text: `评论 ${list.length} 条:\\n${lines.join('\\n')}` }]\n },\n },\n async execute(args: { id: string }) {\n try {\n const task = store.get(args.id)\n if (task === undefined || task.trashedAt !== undefined) throw new ToolError(ERR.notFound, `no task ${args.id}`)\n return json({ comments: task.comments })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ---------------------------------------------------------------- delete\n disposers.push(register(defineTool({\n name: 'taskboard_delete',\n description:\n 'Soft-delete a task (marks it trashed; the user confirms the purge in the GUI). '\n + 'Requires ifVersion. Prefer canceled/archived over delete unless the task was a mistake.',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id.' },\n ifVersion: { type: 'number', required: true, description: 'Task version you read.' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { trashed?: boolean }\n return [{ type: 'text', text: v.trashed === true ? '任务已标记删除(等待用户在 GUI 清除)。' : '删除失败。' }]\n },\n },\n async execute(args: { id: string; ifVersion: number }, exec: unknown) {\n try {\n caller(exec as ToolRunContext)\n const task = store.get(args.id)\n if (task === undefined) throw new ToolError(ERR.notFound, `no task ${args.id}`)\n versionGuard(task, args.ifVersion)\n const next: TaskRecord = structuredClone(task)\n next.trashedAt = deps.now()\n next.version = task.version + 1\n await store.mutate('task-deleted', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === args.id)\n ledger.tasks[i] = next\n return [next]\n })\n return { trashed: true }\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n return disposers\n}\n"],"mappings":"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"}
|
package/lib/index.js
CHANGED
|
@@ -19,6 +19,7 @@ const inject = ["tools", "systemPrompt"];
|
|
|
19
19
|
function apply(ctx) {
|
|
20
20
|
const store = new TaskStore({ file: dshHomePath(LEDGER_FILE) });
|
|
21
21
|
const now = () => Date.now();
|
|
22
|
+
const maxConcurrent = Math.max(1, Number.parseInt(process.env.DSH_TASKBOARD_MAX_CONCURRENT ?? "", 10) || 3);
|
|
22
23
|
const disposeSection = ctx.systemPrompt.section({
|
|
23
24
|
name: PROTOCOL_SECTION_NAME,
|
|
24
25
|
order: 180,
|
|
@@ -27,10 +28,19 @@ function apply(ctx) {
|
|
|
27
28
|
ctx.effect(() => disposeSection, "dsh-taskboard: protocol section");
|
|
28
29
|
ctx.inject(["workspaceRegistry"], (wsCtx) => {
|
|
29
30
|
const disposers = [];
|
|
31
|
+
const modelProviders = () => {
|
|
32
|
+
try {
|
|
33
|
+
const llm = wsCtx.get("llm");
|
|
34
|
+
return llm === void 0 || typeof llm.listProviders !== "function" ? void 0 : llm.listProviders().map((p) => p.id);
|
|
35
|
+
} catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
30
39
|
disposers.push(...registerTaskboardTools(wsCtx, {
|
|
31
40
|
store,
|
|
32
41
|
workspaces: workspaceFace(wsCtx.workspaceRegistry),
|
|
33
|
-
now
|
|
42
|
+
now,
|
|
43
|
+
modelProviders
|
|
34
44
|
}));
|
|
35
45
|
const events = { onSessionEvent: (listener) => wsCtx.on("session/event", (session, event) => {
|
|
36
46
|
listener(session.id, event);
|
|
@@ -64,7 +74,8 @@ function apply(ctx) {
|
|
|
64
74
|
} catch {
|
|
65
75
|
return;
|
|
66
76
|
}
|
|
67
|
-
}
|
|
77
|
+
},
|
|
78
|
+
maxConcurrent
|
|
68
79
|
});
|
|
69
80
|
let disposeRoutes;
|
|
70
81
|
agentCtx.inject(["webServer"], (webCtx) => {
|
|
@@ -72,14 +83,18 @@ function apply(ctx) {
|
|
|
72
83
|
store,
|
|
73
84
|
workspaces: workspaceFace(wsCtx.workspaceRegistry),
|
|
74
85
|
now,
|
|
75
|
-
run: (taskId) => execution.run(taskId, "manual")
|
|
86
|
+
run: (taskId) => execution.run(taskId, "manual"),
|
|
87
|
+
cancel: (taskId) => execution.cancel(taskId),
|
|
88
|
+
modelProviders
|
|
76
89
|
});
|
|
77
90
|
return () => disposeRoutes?.();
|
|
78
91
|
});
|
|
92
|
+
execution.reconcile();
|
|
79
93
|
const scheduler = new SchedulerService({
|
|
80
94
|
store,
|
|
81
95
|
execution,
|
|
82
|
-
now
|
|
96
|
+
now,
|
|
97
|
+
maxConcurrent
|
|
83
98
|
});
|
|
84
99
|
scheduler.start();
|
|
85
100
|
disposers.push(() => scheduler.dispose());
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Host loader entry for dsh-taskboard.\n *\n * Wiring: the ledger store (one JSON file under the DSH home), the eight\n * `taskboard_*` agent tools, the agent workflow-protocol system-prompt\n * section, the /taskboard JSON+SSE routes (when a webServer is served),\n * the host execution service (fresh in-project sessions, pinned models), and\n * the host-side cron scheduler for scheduled tasks.\n *\n * Export shape follows the dsh-tool-todo lesson: a function/namespace plugin —\n * `name` / `inject` / `apply`, NO default export.\n *\n * @module dsh-taskboard\n */\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only module imports: they load the cordis Context augmentations\n// (ctx.tools / ctx.systemPrompt / ctx.agents) and vanish at compile time —\n// the built host half keeps ZERO runtime @deepseek-ai imports.\nimport type {} from '@deepseek-ai/dsh-tools'\nimport type {} from '@deepseek-ai/dsh-system-prompt'\nimport type {} from '@deepseek-ai/dsh-agent'\nimport { PROTOCOL_SECTION_NAME, PROTOCOL_SECTION_ORDER, TASKBOARD_PROTOCOL } from './host/protocol-text.ts'\nimport { ExecutionService, type EventsFace } from './host/execution.ts'\nimport { registerTaskboardRoutes } from './host/routes.ts'\nimport { SchedulerService } from './host/scheduler.ts'\nimport { dshHomePath } from './host/sdk.ts'\nimport { TaskStore } from './host/store.ts'\nimport { registerTaskboardTools, workspaceFace } from './host/tools.ts'\n\n/** Ledger file name under the DSH home. */\nexport const LEDGER_FILE = 'dsh-taskboard.json'\n\n/** Cordis plugin name. */\nexport const name = 'dsh-taskboard'\n\n/** Required host services (tool registry + prompt assembly). */\nexport const inject = ['tools', 'systemPrompt']\n\n/**\n * Mount the host half.\n * @param ctx - the plugin context (tools + systemPrompt injected).\n */\nexport function apply(ctx: Context): void {\n const store = new TaskStore({ file: dshHomePath(LEDGER_FILE) })\n const now = () => Date.now()\n\n // Agent workflow protocol (claim discipline, retry rules, done-gate).\n const disposeSection = ctx.systemPrompt.section({\n name: PROTOCOL_SECTION_NAME,\n order: PROTOCOL_SECTION_ORDER,\n text: TASKBOARD_PROTOCOL,\n })\n ctx.effect(() => disposeSection, 'dsh-taskboard: protocol section')\n\n // Tools, routes, execution, and the scheduler all come up with the\n // workspace registry (claim boundary + project execution need it).\n ctx.inject(['workspaceRegistry'], (wsCtx: Context) => {\n const disposers: Array<() => void> = []\n disposers.push(...registerTaskboardTools(wsCtx, {\n store,\n workspaces: workspaceFace(wsCtx.workspaceRegistry),\n now,\n }))\n\n // Settlement listener over the session event bus.\n const events: EventsFace = {\n onSessionEvent: (listener) => wsCtx.on('session/event', (session, event) => {\n listener(session.id, event as { type: string; data?: unknown })\n }),\n }\n\n wsCtx.inject(['agents'], (agentCtx: Context) => {\n const execution = new ExecutionService({\n store,\n agents: {\n create: (options): Promise<never> => agentCtx.agents.create(options as never) as Promise<never>,\n },\n workspaces: {\n get: id => workspaceFace(wsCtx.workspaceRegistry).get(id),\n attach: async (workspaceId, sessionId) => {\n const ws = wsCtx.workspaceRegistry.get(workspaceId as never)\n if (ws !== undefined) await ws.attachSession(sessionId as never)\n },\n },\n events,\n now,\n renameSession: (sessionId, title) => {\n // Best-effort: pin the execution session's title to the task title\n // through the log-backed session-title service (user-sourced rename).\n try {\n const sessions = agentCtx.get('sessions') as { get(id: string): unknown } | undefined\n const sessionTitle = agentCtx.get('sessionTitle') as { rename(session: unknown, title: string): unknown } | undefined\n const session = sessions?.get(sessionId)\n if (session !== undefined && sessionTitle !== undefined) sessionTitle.rename(session, title)\n } catch { /* cosmetic */ }\n },\n defaultModel: () => {\n try {\n const selection = agentCtx.get('agentDefaultModel') as { currentSelection?: () => { provider: string; model: string } | undefined } | undefined\n const read = selection?.currentSelection\n return read === undefined ? undefined : read.call(selection)\n } catch { return undefined }\n },\n })\n\n // /dsh-taskboard routes (the run action reaches the execution service).\n let disposeRoutes: (() => void) | undefined\n agentCtx.inject(['webServer'], (webCtx: Context) => {\n disposeRoutes = registerTaskboardRoutes(webCtx, {\n store,\n workspaces: workspaceFace(wsCtx.workspaceRegistry),\n now,\n run: (taskId: string) => execution.run(taskId, 'manual'),\n })\n return () => disposeRoutes?.()\n })\n\n // Host-side cron scheduler: due scheduled tasks execute even with no\n // browser open.\n const scheduler = new SchedulerService({ store, execution, now })\n scheduler.start()\n disposers.push(() => scheduler.dispose())\n\n return () => {\n disposeRoutes?.()\n for (const dispose of disposers.splice(0)) dispose()\n }\n })\n\n return () => {\n for (const dispose of disposers.splice(0)) dispose()\n }\n })\n}\n"],"mappings":";;;;;;;;;AA8BA,MAAa,cAAc;;AAG3B,MAAa,OAAO;;AAGpB,MAAa,SAAS,CAAC,SAAS,cAAc;;;;;AAM9C,SAAgB,MAAM,KAAoB;CACxC,MAAM,QAAQ,IAAI,UAAU,EAAE,MAAM,YAAY,WAAW,EAAE,CAAC;CAC9D,MAAM,YAAY,KAAK,IAAI;
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Host loader entry for dsh-taskboard.\n *\n * Wiring: the ledger store (one JSON file under the DSH home), the eight\n * `taskboard_*` agent tools, the agent workflow-protocol system-prompt\n * section, the /taskboard JSON+SSE routes (when a webServer is served),\n * the host execution service (fresh in-project sessions, pinned models), and\n * the host-side cron scheduler for scheduled tasks.\n *\n * Export shape follows the dsh-tool-todo lesson: a function/namespace plugin —\n * `name` / `inject` / `apply`, NO default export.\n *\n * @module dsh-taskboard\n */\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only module imports: they load the cordis Context augmentations\n// (ctx.tools / ctx.systemPrompt / ctx.agents) and vanish at compile time —\n// the built host half keeps ZERO runtime @deepseek-ai imports.\nimport type {} from '@deepseek-ai/dsh-tools'\nimport type {} from '@deepseek-ai/dsh-system-prompt'\nimport type {} from '@deepseek-ai/dsh-agent'\nimport { PROTOCOL_SECTION_NAME, PROTOCOL_SECTION_ORDER, TASKBOARD_PROTOCOL } from './host/protocol-text.ts'\nimport { DEFAULT_MAX_CONCURRENT, ExecutionService, type EventsFace } from './host/execution.ts'\nimport { registerTaskboardRoutes } from './host/routes.ts'\nimport { SchedulerService } from './host/scheduler.ts'\nimport { dshHomePath } from './host/sdk.ts'\nimport { TaskStore } from './host/store.ts'\nimport { registerTaskboardTools, workspaceFace } from './host/tools.ts'\n\n/** Ledger file name under the DSH home. */\nexport const LEDGER_FILE = 'dsh-taskboard.json'\n\n/** Cordis plugin name. */\nexport const name = 'dsh-taskboard'\n\n/** Required host services (tool registry + prompt assembly). */\nexport const inject = ['tools', 'systemPrompt']\n\n/**\n * Mount the host half.\n * @param ctx - the plugin context (tools + systemPrompt injected).\n */\nexport function apply(ctx: Context): void {\n const store = new TaskStore({ file: dshHomePath(LEDGER_FILE) })\n const now = () => Date.now()\n // Global execution concurrency cap (DSH_TASKBOARD_MAX_CONCURRENT overrides).\n const maxConcurrent = Math.max(1, Number.parseInt(process.env.DSH_TASKBOARD_MAX_CONCURRENT ?? '', 10) || DEFAULT_MAX_CONCURRENT)\n\n // Agent workflow protocol (claim discipline, retry rules, done-gate).\n const disposeSection = ctx.systemPrompt.section({\n name: PROTOCOL_SECTION_NAME,\n order: PROTOCOL_SECTION_ORDER,\n text: TASKBOARD_PROTOCOL,\n })\n ctx.effect(() => disposeSection, 'dsh-taskboard: protocol section')\n\n // Tools, routes, execution, and the scheduler all come up with the\n // workspace registry (claim boundary + project execution need it).\n ctx.inject(['workspaceRegistry'], (wsCtx: Context) => {\n const disposers: Array<() => void> = []\n\n // Registered model provider routes (from the host llm runtime), read\n // lazily at call time so late availability still applies; undefined when\n // the runtime is absent → only structural model validation runs.\n const modelProviders = (): string[] | undefined => {\n try {\n const llm = wsCtx.get('llm') as { listProviders?: () => Array<{ id: string }> } | undefined\n return llm === undefined || typeof llm.listProviders !== 'function'\n ? undefined\n : llm.listProviders().map(p => p.id)\n } catch { return undefined }\n }\n\n disposers.push(...registerTaskboardTools(wsCtx, {\n store,\n workspaces: workspaceFace(wsCtx.workspaceRegistry),\n now,\n modelProviders,\n }))\n\n // Settlement listener over the session event bus.\n const events: EventsFace = {\n onSessionEvent: (listener) => wsCtx.on('session/event', (session, event) => {\n listener(session.id, event as { type: string; data?: unknown })\n }),\n }\n\n wsCtx.inject(['agents'], (agentCtx: Context) => {\n const execution = new ExecutionService({\n store,\n agents: {\n create: (options): Promise<never> => agentCtx.agents.create(options as never) as Promise<never>,\n },\n workspaces: {\n get: id => workspaceFace(wsCtx.workspaceRegistry).get(id),\n attach: async (workspaceId, sessionId) => {\n const ws = wsCtx.workspaceRegistry.get(workspaceId as never)\n if (ws !== undefined) await ws.attachSession(sessionId as never)\n },\n },\n events,\n now,\n renameSession: (sessionId, title) => {\n // Best-effort: pin the execution session's title to the task title\n // through the log-backed session-title service (user-sourced rename).\n try {\n const sessions = agentCtx.get('sessions') as { get(id: string): unknown } | undefined\n const sessionTitle = agentCtx.get('sessionTitle') as { rename(session: unknown, title: string): unknown } | undefined\n const session = sessions?.get(sessionId)\n if (session !== undefined && sessionTitle !== undefined) sessionTitle.rename(session, title)\n } catch { /* cosmetic */ }\n },\n defaultModel: () => {\n try {\n const selection = agentCtx.get('agentDefaultModel') as { currentSelection?: () => { provider: string; model: string } | undefined } | undefined\n const read = selection?.currentSelection\n return read === undefined ? undefined : read.call(selection)\n } catch { return undefined }\n },\n maxConcurrent,\n })\n\n // /dsh-taskboard routes (the run action reaches the execution service).\n let disposeRoutes: (() => void) | undefined\n agentCtx.inject(['webServer'], (webCtx: Context) => {\n disposeRoutes = registerTaskboardRoutes(webCtx, {\n store,\n workspaces: workspaceFace(wsCtx.workspaceRegistry),\n now,\n run: (taskId: string) => execution.run(taskId, 'manual'),\n cancel: (taskId: string) => execution.cancel(taskId),\n modelProviders,\n })\n return () => disposeRoutes?.()\n })\n\n // Startup reconciliation: executions left 'running' by a previous host\n // process are marked failed and their tasks handed back to todo (their\n // settlement watchers died with that process).\n void execution.reconcile()\n\n // Host-side cron scheduler: due scheduled tasks execute even with no\n // browser open. Shares the execution concurrency cap.\n const scheduler = new SchedulerService({ store, execution, now, maxConcurrent })\n scheduler.start()\n disposers.push(() => scheduler.dispose())\n\n return () => {\n disposeRoutes?.()\n for (const dispose of disposers.splice(0)) dispose()\n }\n })\n\n return () => {\n for (const dispose of disposers.splice(0)) dispose()\n }\n })\n}\n"],"mappings":"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"}
|
package/lib/shared/protocol.js
CHANGED
|
@@ -142,6 +142,18 @@ function nextCronTime(match, from) {
|
|
|
142
142
|
}
|
|
143
143
|
return null;
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Enforce the execution-record retention cap on one task (in place): keep the
|
|
147
|
+
* newest {@link MAX_EXECUTIONS} records, count the dropped ones in
|
|
148
|
+
* `executionsPruned`. Running records are always the newest, never dropped.
|
|
149
|
+
* @param task - the task to prune.
|
|
150
|
+
*/
|
|
151
|
+
function pruneExecutions(task) {
|
|
152
|
+
if (task.executions.length <= 20) return;
|
|
153
|
+
const dropped = task.executions.length - 20;
|
|
154
|
+
task.executions = task.executions.slice(-20);
|
|
155
|
+
task.executionsPruned = (task.executionsPruned ?? 0) + dropped;
|
|
156
|
+
}
|
|
145
157
|
/** An empty ledger. */
|
|
146
158
|
function emptyLedger() {
|
|
147
159
|
return {
|
|
@@ -248,7 +260,46 @@ function effectivePrompt(task) {
|
|
|
248
260
|
* @param task - the task.
|
|
249
261
|
*/
|
|
250
262
|
function isClaimedBy(task) {
|
|
251
|
-
return task.status === "in_progress" && task.
|
|
263
|
+
return task.status === "in_progress" && task.claimedBy !== void 0 ? task.claimedBy : void 0;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Maintain the explicit claim fields around a status change: entering
|
|
267
|
+
* in_progress under a session records the holder (an execution-start or an
|
|
268
|
+
* agent claim); every move out of in_progress releases the claim (handoff,
|
|
269
|
+
* give-back, cancel). A user-driven move into in_progress records no holder —
|
|
270
|
+
* no session works on it yet.
|
|
271
|
+
* @param task - the task being written (mutated in place).
|
|
272
|
+
* @param to - the target status.
|
|
273
|
+
* @param now - current epoch ms.
|
|
274
|
+
* @param holder - the session id claiming the task, when applicable.
|
|
275
|
+
*/
|
|
276
|
+
function syncClaim(task, to, now, holder) {
|
|
277
|
+
if (to !== "in_progress") {
|
|
278
|
+
delete task.claimedBy;
|
|
279
|
+
delete task.claimedAt;
|
|
280
|
+
} else if (holder !== void 0) {
|
|
281
|
+
task.claimedBy = holder;
|
|
282
|
+
task.claimedAt = now;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Validate and normalize a pinned model: `{ provider, model }`, both
|
|
287
|
+
* non-empty trimmed strings.
|
|
288
|
+
* @param raw - the raw input.
|
|
289
|
+
* @returns the normalized model.
|
|
290
|
+
* @throws when the shape or the fields are invalid.
|
|
291
|
+
*/
|
|
292
|
+
function normalizeModel(raw) {
|
|
293
|
+
if (typeof raw !== "object" || raw === null) throw new Error("model must be { provider: string, model: string }");
|
|
294
|
+
const { provider, model } = raw;
|
|
295
|
+
if (typeof provider !== "string" || typeof model !== "string") throw new Error("model must be { provider: string, model: string }");
|
|
296
|
+
const p = provider.trim();
|
|
297
|
+
const m = model.trim();
|
|
298
|
+
if (p.length === 0 || m.length === 0) throw new Error("model.provider and model.model must be non-empty strings");
|
|
299
|
+
return {
|
|
300
|
+
provider: p,
|
|
301
|
+
model: m
|
|
302
|
+
};
|
|
252
303
|
}
|
|
253
304
|
/**
|
|
254
305
|
* Build the compact summary of a task.
|
|
@@ -274,6 +325,6 @@ function summarize(task) {
|
|
|
274
325
|
};
|
|
275
326
|
}
|
|
276
327
|
//#endregion
|
|
277
|
-
export { ALL_STATUSES, MAIN_STATUSES, SECONDARY_STATUSES, URGENCIES, asStatus, asUrgency, canTransition, effectivePrompt, emptyLedger, isClaim, isClaimedBy, newCommentId, newExecutionId, newTaskId, nextCronTime, normalizeBody, normalizeExecution, normalizePrompt, normalizeTitle, parseCron, summarize };
|
|
328
|
+
export { ALL_STATUSES, MAIN_STATUSES, SECONDARY_STATUSES, URGENCIES, asStatus, asUrgency, canTransition, effectivePrompt, emptyLedger, isClaim, isClaimedBy, newCommentId, newExecutionId, newTaskId, nextCronTime, normalizeBody, normalizeExecution, normalizeModel, normalizePrompt, normalizeTitle, parseCron, pruneExecutions, summarize, syncClaim };
|
|
278
329
|
|
|
279
330
|
//# sourceMappingURL=protocol.js.map
|