DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins

dsh-global-task-list

Global Task List

全局任务库,配备持久化交互面板:任务 CRUD 工具、关联子代理终端状态同步,以及适用于 DeepSeek Harness 的生成式 UI 界面渲染器

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add dsh-global-task-list@0.4.2
README兼容性版本

兼容性与来源证明

Global Task List 以 dsh-global-task-list 发布,当前版本为 0.4.2。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
web
发布来源
npm
Registry 更新时间
2026/11/5

版本

0.4.2stable
2026/11/5
0.2.12stable
2026/10/5
0.2.11stable
2026/10/5
查看其余 7 个版本收起版本
0.2.10stable
2026/9/30
0.2.7stable
2026/9/30
0.2.5stable
2026/9/30
0.2.0stable
2026/9/29
0.1.5stable
2026/9/11
0.1.4stable
2026/8/22
0.1.3stable
2026/8/21

相关插件

正在加载相关插件…

最新版
0.4.2
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
189 kB
文件数
8
Surface
web
许可证
MIT
发布源
npm
GitHub
★ 2
周下载
0
最近提交
2026/11/5
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 productivity-workflow 分类下经过校验的插件。

Archive Manager@michengai/dsh-archive-manager归档、恢复、收藏和永久删除会话,并诊断损坏的会话日志。Remotedsh-remoteDeepSeek Harness 的远程工作助手:连接 SSH(密码/密钥/代理/键盘交互、多跳 ProxyJump),从 ~/.ssh/config 列出主机,选择远程工作区,使用 21 个 rw_* 工具对其进行操作,支持感知冲突的镜像同步、内嵌 PTY tDeepseek Ipptdeepseek-ipptiPolloWork PPT Studio 及其精选幻灯片模板,作为原生 DeepSeek Harness 对话视图。Rewind Plugindsh-rewind-plugin窗口内对话回退并恢复工作区文件

README

dsh-global-task-list

A global task library for the DeepSeek Harness: six model-facing tools, a persistent browser panel, subagent job status synchronization, and a generative-UI surface renderer. Tasks persist across sessions in a storageDomain unit and survive restarts.

全局任务列表插件:为 DeepSeek Harness 提供一个跨会话持久化的任务库,附右下角常驻面板(轨迹等非对话界面自动隐藏)。主 agent 用 task_add 创建任务(带描述与进度),spawn 子 agent 后关联 jobId 自动同步终态;面板支持手动改状态、删除、拆分,并实时(SSE)刷新。任务可携带 surface 结构化文档渲染生成式 UI(进度条、时间线、表格、DAG 等)。

What it does

  • Global task library — task_add / task_list / task_get / task_update / task_delete model tools over a cross-session storageDomain unit (task_ui). Tasks carry a title, status (pending / running / done / blocked / failed), description, nextStep, an ordered steps flow, parentId, dependsOn, an optional progress ({ text, percent? }), and an optional surface document.
  • Persistent panel — a bottom-right floating panel (mounted in the session-scoped conversation.input.dock slot) subscribes to a Server-Sent Events channel (/task-ui/events) and refreshes on every task change, letting the user change status, delete (with confirmation), and trigger a generative split loop. Clicking a card expands a secondary card under it with the description, the step flow, dependencies, and timestamps. Styled with --dsw-* theme tokens and localized zh/en. It disappears on non-conversation views (e.g. trajectory).
  • Step flow — the detail card describes progress as a vertical flow rather than a bar: finished and pending steps share the plain frame, while the two that matter are drawn differently — current in a solid brand frame with a faint tint, next in a dashed one. Steps come from the owner's steps (done / current / next / todo); when it wrote none the panel derives a node from progress, and a list naming no next inherits nextStep as one. A step's state is drawn, so it is also announced to screen readers as text.
  • Panel motion — arrivals and removals are mirror images. An added card's row opens a place for it (the slot grows from no height) and the card slides in from the right; a deleted card slides out to the right first, and only once it is gone does its slot collapse, so the cards below glide up into the gap rather than jumping. Both are two-beat sequences with the timers in the component and the keyframes in the CSS reading the same constants. Expansion is CSS-only (grid-template-rows: 0fr → 1fr). All of it is skipped under prefers-reduced-motion, which keeps every state change and both folds. A removed card is re-adopted as a ghost row inside a layout effect, so it never leaves the DOM for a painted frame — that gap was what made the whole list flash and the panel run a shrink/grow/shrink cycle on delete. A task that lands in a folded row cannot animate in, so the row's count pulses instead.
  • Resizable — drag the panel's left edge (or focus it and use ←/→) to set its width between 260px and 760px; the width is remembered in localStorage. The gesture follows ui-layout's own DragHandle: pointer capture for the whole drag, one throttled update per animation frame, and the width computed from the drag origin rather than accumulated per move.
  • Running focus — when anything is running, those cards are the panel: each opens its own detail region without being asked, because running work is always a handful of tasks and the space they would otherwise share is better spent on what the reader came to see. With nothing running, the open work is the live list instead — the fold exists to get idle work out of the way of work in flight, so it does not appear when there is nothing to get out of the way of.
  • Folds — everything that is not running folds into a named row: open-but-idle work under "other open", finished work under "done". While a row is closed its cards are not rendered at all: a long-lived library would otherwise keep every such card in the DOM, and the reader is almost never looking at them. Opening one renders its cards with a short cascade.
  • The finish beat — a card that becomes done says so before it leaves. Over 620ms the status dot blooms and settles into the success colour, a thin ring expands out of it and dissolves, and a soft success wash crosses the card once. Then its row folds away — the slot's height collapses to 0fr, so the cards below glide up instead of jumping, with no FLIP measurement pass — and the finished row's count pulses once to acknowledge where it went.
  • The wake — a task that starts again (resumed out of the finished fold, or started after sitting idle) has to be seen arriving in the live list, or it simply appears there. Its row opens a place for it (the slot grows from no height) while the card slides in from the right, and then its detail unfolds — the component holds it shut for the length of the wake, so the card does not arrive already open.
  • Live motion — a running card carries a brand-coloured left edge and breathes on a slow 2.6s halo, and the step it is currently on turns a thin ring. That ring is the one place in the panel where "something is happening right now" is literally true, so it is scoped to the running card: a blocked task's current step does not pretend to move. Layout is never touched — the dot scales inside its own box, so a title cannot twitch — and prefers-reduced-motion drops the glow and the movement while keeping every state change and the folds.
  • Subagent job sync — a task linked to a background job via task_update(id, { jobId }) auto-advances its terminal status through the JOB_STATUS_MAP (completed→done, failed→failed, killed→blocked). Tasks are created explicitly by the master agent, never auto-registered from arbitrary background jobs.
  • Generative UI surface — a task's surface field renders a whitelisted, recursive component tree: section, metric, statusBadge, progress, table, list, timeline, dag, disclosure.

Install

Installing from npm is the recommended path — the package ships prebuilt lib/, so no build step runs at install time. The GitHub alternative pulls source and builds on install (see below).

# npm (recommended, prebuilt — no build step at install)
dsh plugin --profile web add dsh-global-task-list

# GitHub (alternative: source + prepare build)
dsh plugin --profile web add github:sgzxs/dsh-global-task-list#v0.2.0

The package declares dsh.bundle, so dsh adds it to the profile's bundles automatically. Requires a DSH installation with @deepseek-ai/dsh-base and the client surface (dsh-web-app) present.

Use at least 0.2.0 for a 0.2.x DSH runtime (the desktop app is 0.2.0-rc.2): earlier releases declare 0.1.x-only DSH peer ranges, and the runtime rejects them before pnpm runs, with installation rejected: Plugin dsh-global-task-list@0.1.5 is incompatible with dsh 0.2.0-rc.2. The desktop app additionally manages its profile itself — see Installing on the desktop app.

Which version did I actually get?

Not necessarily the one that was displayed. A marketplace shows the version a registry reports, while the install is a package-manager run that can resolve differently — an exact pin in the profile's package.json, a pnpm-lock.yaml entry, or a registry asked in turn (DSH's plugin manager falls back to registry.npmmirror.com when the first one is unreachable or answers that it has no such version, and a mirror lags npm). Nothing reconciles the two, so "the marketplace said 2.10 but I got 2.3" is a resolution result, not a stale download.

Two ways to see the truth:

  • ask the agent to run taskui_probe — it answers [task-ui] v<version> host alive, tasks=N; the host log line [task-ui] host plugin loaded v<version> says the same at startup;
  • read the profile: <dshHome>/profiles/<profile>/node_modules/dsh-global-task-list/package.json.

To make an update actually land, name the version explicitly — an exact version also lets the manager's registry plan move past a mirror that does not have it yet:

dsh plugin --profile web add dsh-global-task-list@0.2.11

And to keep receiving updates, depend on a range rather than an exact version: a profile pinned to 0.2.2 will keep installing 0.2.2 no matter what the marketplace shows.

GitHub install: allowBuilds

Installing from GitHub pulls source, so pnpm must run the package's prepare script to build lib/. pnpm blocks that by default for git-hosted packages, and its allowBuilds key matches the exact commit (a package-name-only entry does not match — see pnpm#12367). The first add therefore fails with an ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED message that prints the exact key to allow. Copy that exact key into the profile's pnpm-workspace.yaml, then re-run:

allowBuilds:
  dsh-global-task-list@git+https://github.com/sgzxs/dsh-global-task-list.git#<commit>: true

Pin a tag (e.g. #v0.2.0) in the install command so the commit hash — and therefore the allowBuilds key — stays stable.

Requirements

  • DeepSeek Harness (@deepseek-ai/dsh) at 0.1.2-alpha.2 or later: the client bundle imports defineStore from @deepseek-ai/dsh-client-store, a browser module-table entry that does not exist on earlier hosts (there the panel fails to load with Failed to load plugins). Verified against 0.1.5-rc.2 (CLI/web profiles) and 0.2.0-rc.2 (the DeepSeek Harness desktop app).
  • The Host half resolves @deepseek-ai/dsh-tools, @deepseek-ai/dsh-llm, @deepseek-ai/dsh-storage-domain, and zod from the profile's node_modules.
  • The client bundle is prebuilt (lib/client.js) and ships with the package; no build step runs at install time.

DSH version compatibility

The Host half is written against two DSH generations because the jobs service changed shape in 0.2:

Surface0.1.x0.2.xThis plugin
Job change feedctx.jobs.onJobsChanged(listener) (listener receives the owner Agent)ctx.jobs.events.subscribe({ owners: 'all' }, listener) (each commit carries the job snapshot)feature-detects jobs.events.subscribe first, falls back to onJobsChanged, and disables the bridge with a log line if neither exists
ctx.jobs.list(caller)caller is the owner Agentcaller is the owner's session idonly used on the 0.1.x path, which is the path that needs it

Everything else the plugin touches is unchanged between the two: defineTool / tools.register, createUserMessage, defineDomain / domainTable / storageDomain.open, webServer.register({ kind, path, handler }), agents.list() / agent.followup / session.header.origin, and the domain/changed event. The client half needs no version split at all — the browser platform module table (react, react/jsx-runtime, react-dom, react-dom/client, @deepseek-ai/cordis, dsh-client-store, dsh-client-ui-slots, dsh-client-ui-primitives, dsh-client-ui-dockkit) and the conversation.input.dock slot contract are identical in both.

peerDependencies therefore admit both lines (^0.1.5-rc.2 || ^0.2.0-rc.2). DSH enforces every @deepseek-ai/dsh* peer range against the running runtime at install time and again at profile composition, so a range that excludes the running version blocks the plugin instead of degrading.

Installing on the desktop app

The desktop app owns its profile: dsh --profile desktop is refused outside the Electron application, and dsh plugin --profile desktop <args> expects the app to be fully quit first. Either install through the app's own Plugins page, or place the package and declare it by hand:

// $DSH_HOME/profiles/desktop/package.json
{
  "dependencies": { "dsh-global-task-list": "link:../../plugins/dsh-global-task-list" },
  "dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-global-task-list"] } }
}

Profiles reload their configuration by default (dsh-hmr), so a running desktop app mounts the plugin on its next recompose — no restart required.

Build (development)

npm install
npm run build:host    # tsdown bundles src/index.ts → lib/index.js
npm run build:client  # tsdown bundles src/client → lib/client.js
npm run typecheck     # tsc --noEmit, against the real DSH 0.2.0-rc.2 types
npm test              # builds, then typecheck + smoke + render + CSS-classes
npm run sync web      # copy the build into a profile's node_modules (dev helper)

Both halves are TypeScript: the Host half (src/index.ts) compiles to plain ESM, and the client half (TSX) compiles with CSS Modules (lightningcss) into a __ModuleLoader__ closure-factory bundle.

Why the test suite exists

The build only transpiles — tsdown runs with dts: false and never type-checks — so nothing in the build path catches a wrong identifier. npm test closes that with six layers, each of which caught something the others missed:

LayerCatches
tsc --noEmitundefined identifiers, wrong argument types, stale locale keys (the dictionaries are the key-set source of truth)
test/host-smoke.mjswhat the Host half actually contributes: the six tools, their converted JSON Schema (including the object-typed steps items), both HTTP routes, the job-bridge subscription, and the system-prompt section. It also drives the storage lifecycle: a transient already-open is retried, and disposing the plugin closes the handle it owns.
test/client-smoke.mjsmodule-eval and apply() throws — what a browser reports as "Failed to load plugins"
test/render-check.mjsrender-path throws across every task state. DSH isolates each slot entry, so a component that throws renders nothing: the panel disappears while the rest of the UI stays healthy. The harness runs the real component through a minimal hook runtime and renders child components too.
test/class-check.mjsa css.foo with no .foo rule (and the reverse) — the class map is a plain object, so neither side is typed
test/bundle-css-check.mjsthe same split in the built artifact: every class in the bundle's exported map must have a rule in the CSS text that bundle injects. Source and build can disagree.

The typecheck needs the DSH packages as devDependencies; they are pinned exactly to the runtime line the plugin targets, and they never ship (files lists only lib/ and the patch).

Model Experience

Request context and condition

The package contributes six model-facing tools to the agent's tool catalog.

What the model sees

  • taskui_probe — reports the plugin's host-alive status and current task count.
  • task_list(all?, limit?) — one triage line per task: - [status] title (id[, job=...]) — next: .... Every unfinished task is listed; finished ones are capped to the most recent 5 with a trailing count, because finished work is history rather than work in flight and the library has no archival. all: true lists them all, limit bounds the lines (default 40).
  • task_get(id) — the full record of one task, ordered the way a session picking up someone else's work needs it: status and title, progress (with percent), next, the whole steps flow with its states, dependencies, parent, job, description, and timestamps.
  • task_add(title, description?, nextStep?, steps?, parentId?, dependsOn?, surface?, progress?) — creates a task (initial pending).
  • task_update(id, title?/status?/description?/nextStep?/steps?/parentId?/dependsOn?/surface?/progress?/jobId?) — patches one task; progress is { text, percent? }, nextStep is the one-line "what happens next", steps is the ordered [{ text, state }] flow the detail card draws, surface accepts a structured generative-UI document, and jobId links the task to a background job for terminal-status sync.
  • task_delete(id) — deletes one task.

The list is deliberately short and the detail lives in task_get, so the model's read path is "filter with task_list, then read the one task you are about to work on". The durable record carries far more than any single line could, and a later session has no other way to reach it.

nextStep and steps postdate the original record schema. Both are optional and defaulted, and the domain's backend descriptor is derived from the domain name, version, and table names rather than the zod schemas — so adding a column needs no version bump, and records written before it existed still load. steps items are validated by the durable zod boundary on write, so the tool parameter declares them as loose objects (additionalProperties: true) instead of duplicating that enum in JSON Schema.

The surface document is a recursive, whitelisted component tree (section / metric / statusBadge / progress / table / list / timeline / dag / disclosure) rendered by the browser panel. Unknown kinds are ignored by the renderer; the Host stores the document as opaque JSON.

How the agent is made to use it

A skill is model-invoked: the catalog the model sees carries only each skill's name and description, so task-ui loads exactly when the model decides that description matches — which is not when ordinary work starts. That is the wrong lever for behaviour that must happen unprompted, so the plugin uses the levers that are always present instead:

  1. Tool descriptions — the schemas sit in the system prompt's tool catalog on every request. Each one states its purpose, the moment to act, and its boundary (the same shape the harness uses for its own todo_write, which is proactive for exactly this reason). task_add says to record multi-step work before starting it and always before delegating — and to record it late rather than not at all; task_update says to keep the entry moving, to link a delegated job, and how to finish one properly (clear nextStep, leave steps all done, write the outcome into description).

    The skip boundary is deliberately a shape test, not a length test. It first read "skip trivial single-step requests / one-shot answers", and a repo analysis walked straight through it: "analyze X" is one exchange with one deliverable, so a model could reasonably file it under "one-shot". It now names the shapes that count — analyze / investigate / compare / implement / review, and anything the user calls a task — and keeps exactly one exemption: a reply you can give without looking anything up.

  2. One system-prompt section (task-ui:library, at the subagent tool's own order) — one sentence in the harness's own register, so a model that never reaches for a tool still knows the library exists. It covers the whole obligation, not just the write half: record before starting, keep current while working, read the record back before continuing what an earlier session started, name the shapes that count as multi-step, and record late rather than not at all. It renders two variants: the session that owns the work gets all of it, while a subagent (read from AssembleContext.agent.session.header.origin) is told only to keep its task's record true — it does not plan, delegate, or create parallel entries its spawner cannot see. It renders empty in any scope where task_add is not callable, and config.promptSection: false drops it for a deployment that prefers to rely on the descriptions alone.

  3. Just-in-time notes in tool results — the write tools report a derived inconsistency in their own result, and say nothing otherwise. task_add names the fields a title-only entry is missing; task_update on a task that just became done names the specific gaps (a nextStep still set, steps not marked done, an empty description). These restate no rule and repeat no guidance — they read the record they just wrote and report what is wrong with one — so they cost nothing until something is actually wrong, and they land at the moment it can still be fixed. A tool result is appended to the conversation, so unlike a section it never touches the cached prompt prefix.

Sections are merged by name, so this one — a unique name no DSH package or preset uses — is never shadowed by, and never shadows, the deployment's persona: all of them simply render in order sequence (deployment:persona-prefix at 0, this at the subagent tool's order, deployment:persona-suffix at 10200).

The bundled skill remains the place for the detailed workflow (the subagent supervision loop, the flow-authoring rules, the cross-session handoff read order, the surface component catalog). It is not injected into the prompt — a model needs to decide to use the library before that detail is worth its tokens, and the always-on levers are what make it decide — but levers 4 and 5 put the body in front of it once the session is actually arranging work, which is the gap between "decided to use it" and "knows how to use it well".

Token effect

Six tool schemas plus their descriptions are injected into the system-prompt tool catalog, and — unless config.promptSection is false — one fixed sentence into the system prompt. Fixed cost; no per-task or data-dependent prompt growth.

KV Cache effect

Independent of the agent's request-context assembly. The tool schemas and the prompt section are static, so the prompt prefix is stable across requests; task-list content does not enter the prompt. A plugin version change (or toggling promptSection) replaces the section, or the tool schemas, and invalidates reuse.

Design notes

Tasks are session-independent by design. The record deliberately does not name the session that created it or last touched it, and no field ties an entry to a conversation. The library exists so that work hands off seamlessly: any session — a later one, a different preset, a subagent — picks an entry up and continues, and what it needs is what is true now and what to do next, both of which the record carries (progress, steps, nextStep). Recording an origin would make every entry an artifact of the conversation that produced it, which is the opposite of the point; it would also invite a later session to defer to an owner that no longer exists. This is why task_get leads with state rather than history, and why nothing has to be rewritten when a task outlives the session it started in.

Known Limitations and Deferred Work

  • No graph layout — the dag surface renders nodes as chips plus an edges list, not a positioned graph.

  • Surface is opaque — the Host stores surface as unvalidated JSON (z.record); malformed kinds fall back to "ignored" in the renderer rather than a load-time error.

  • sync-profile.mjs is a local dev helper — it copies the built package into $DSH_HOME/profiles/<profile>/node_modules (profile defaults to web) and warns when that profile's dsh.profile.bundles does not list the plugin. It is not published (excluded via files); use dsh plugin add for installation.

  • Job-status sync degrades, never blocks — a runtime exposing neither jobs.events.subscribe nor jobs.onJobsChanged keeps the tools, the panel, and the HTTP API, and logs jobs service exposes no change feed; job status sync disabled.

  • The prompt section cannot force the behaviour — descriptions and one sentence raise the odds that the model records its work; nothing in the harness compels it. A deployment that needs a guarantee should say so in its own persona or preset instructions, which outrank a plugin's contribution.

  • Skill updates are seeded, and can also be shadowed — the Host half writes skills/task-ui/SKILL.md into <dshHome>/skills/task-ui/ and keeps it current, but it will not clobber a copy the user edited: an edited copy is left alone and this version's text is written beside it as SKILL.md.new. More importantly, the seeded copy is only one of several possible roots, and dsh-skill-filesystem ranks them (lowest wins): project .dsh 100, project .agents 200, customSkillDirs 300, $DSH_HOME/skills 400, user .agents 500, and a package's own bundled copy last at 600. A deployment whose customSkillDirs holds its own task-ui copy therefore shadows the seeded one — an agent preset copied from another machine can silently pin a months-old manual while the plugin's copy is current. The plugin cannot see or fix a shadowing copy; if the loaded skill looks stale, check those roots first.

  • Blocked reasons have no field of their own — dependsOn covers task-to-task blocking; a reason that is not another task ("waiting on an external service") has only to live in.

Acknowledgements

Inspired by HomeRail — its generative-UI (A2UI) idea of "the agent emits a bounded, whitelisted component tree that a host renders into a glanceable view" shaped this plugin's surface document and component catalog (section / metric / statusBadge / progress / table / list / timeline / dag / disclosure), as well as the principle of preferring built-in rendering over dumping raw logs.

this
  • The operator manual rides the first library call of a session — the bundled skill's body is attached to the result of the first task_* call, once per session, so a model that never calls the skill tool still reads the manual at the moment it decides to use the library. Delivering it in a tool result matters for three reasons: it lands in the same turn (a section or context could only take effect on the next request), it costs nothing in sessions that never touch the library, and it is appended to the conversation rather than injected into the prompt, so the cached prefix is untouched. The body is read from this package's own bundled copy rather than through ctx.skills, because a host-plane plugin cannot rely on seeing a provider that an agent preset mounted in its own scope; front matter is stripped, and the text stays under the tool-result pruner's threshold.

  • An orchestration hook delivers it earlier — a tools/post-execute listener watches for the calls that reveal the model is organising multi-step work: todo_write (it decided to track steps), any subagent* spawn (it is delegating), workflow / ralph. The first one in a session attaches the manual to that call through additionalContexts — the harness's own channel for "attach context for the next request" — so the called tool's result is left exactly as its owner wrote it. This is the difference between explaining the library after the model already recorded something and explaining it as the model starts arranging the work. The message is attributed with this plugin's own MessageSourceMap kind and declares form: 'instructions' (the semantic vocabulary's term for instructions read out of a file), per the harness's rule that each producer declares its own kind rather than sharing a catch-all.

    Both delivery channels go through one once-per-session set, so exactly one of them wins and the manual is never sent twice.

  • nextStep
  • A running task linked to a job is flagged, not corrected — the job registry is in-process, so a jobId written before the running Host started cannot refer to anything alive. Both read paths say so: task_list appends [expired], task_get leads with a warning: line, and the panel colours the job line with --dsw-alias-state-warn-primary. The record is annotated rather than rewritten, because the plugin cannot know whether the work finished, was abandoned, or is simply unrecorded. The signal is the task's own updatedAt compared against Host start (jobs exposes no call that enumerates every owner's jobs), so a write after boot clears the flag — a false negative if a session sets running by hand on a task that still carries an old jobId.

  • The client bundle is not byte-reproducible — npm run build:client emits the CSS-module class map in a different key order on every run, so the bundle hash changes even with identical sources. The key→hash pairs themselves are stable and nothing at runtime observes the order; the only casualty is comparing a local build against a published tarball by hash, which is why lib/client.js never matches its published copy byte for byte.