automagik-si/genie--plugins-dsh-genie-board ↗★ 352

@automagik/genie-dsh-board

Wishes in, PRs out. CLI agent that interviews you, plans the work, dispatches parallel agents in isolated worktrees, and reviews code before you see it.

패키지
@automagik/genie-dsh-board
호환성
미검증
Harness peer 범위
>=0.1.2-rc.1 <0.3.0-0
버전
5.260901.3
라이선스
Apache-2.0
최근 업데이트
2026. 11. 12.

설치

$npx -p @deepseek-ai/dsh dsh plugin --profile web add 'github:automagik-si/genie#b8441276d680d9cdef114d2f3854551e517100dc&path:plugins/dsh-genie-board'

Genie board for DSH Web

The plugin adds two entries to the DSH Web sidebar, each a global panel in the main column: Genie board and Workflows. Every panel starts with a registered repository workspace picker.

  • Genie board shows a board's lanes, cards, owners, liveness, blocks, dependencies, comments and history. Select a card to move, comment, block/hold, unblock, claim, release or complete it; Add creates a task on the selected board. Refresh and returning to the visible tab read a complete board again; there is no polling.
  • Workflows lists the repository's .claude/workflows/.js catalog (the canonical saved-workflow format), each script's phases and when-to-use guidance, and the script body. The catalog is read-only: nothing is executed from the panel.

The panels are React components rendered through DSH's own slot system (sidebar.panellist + main), styled with DSH alias tokens so they follow the active theme, and built against the frozen browser module table (React and @deepseek-ai/dsh-client-ui-primitives).

The skills panel that used to live here (retired 2026-09-20)

This package carried a third panel, Skills, listing the repository's skills//SKILL.md catalog with its shipped resources and document body, grouped under category headers. It is deleted, not disabled: @namastexlabs/dsh-skills owns every skills surface now, including the workspace-scoped read this row served (GET /dsh-skills/api/workspace?workspace=… answers with the same inventory), so the row, its two routes (/api/genie-board/skills, /api/genie-board/skills/document), its panel, its ./skills export and its dist/skills.js bundle no longer exist. There is no second skills surface beside the replacement, and nothing here reads skills/ any more.

What the row's grouping and workspace read carried lives on where it belongs: the category / mutates taxonomy stays declared and enforced by this repository's own scripts/skills-inventory-parity.ts and scripts/skills-lint.ts (in bun run check), and @namastexlabs/dsh-skills is the surface that shows it. The taxonomy mirror this package kept (src/taxonomy.ts) existed only to render that panel, so it went with it.

Three rows: one manager, two sub-plugins

The Host half is three cordis rows, all from this one package:

Row idPackage specifierRegisters
genie-dsh-board@automagik/genie-dsh-boardthe genieRuntime service and /api/genie-board/health
genie-dsh-board-board@automagik/genie-dsh-board/board/workspaces and the one mutating /action
genie-dsh-board-workflows@automagik/genie-dsh-board/workflows/workflows and /workflows/document

The manager row owns everything the sub-rows must not each own a copy of: the Genie executable resolved once with its compatibility verdict, workspace resolution, and the whole trust fence — loopback address, loopback Host, exact Origin on POST, Sec-Fetch-Site, the 415 content-type check, the 16 KiB body cap, the derived socket deadline and the single error shape. It publishes them as the cordis service genieRuntime, and a sub-row registers every route through genieRuntime.route. A sub-row that cannot resolve the service registers nothing at all and never resolves an executable of its own.

A sub-row waits for that service through cordis' deferred ctx.inject(['genieRuntime'], …) (the whenRuntime helper in src/runtime.ts), never a row-level export const inject. A row-level inject is a hard dependency: cordis parks the row's own fiber in PENDING while the service is missing, and DSH's boot audit fails the whole Host on any enabled loader entry that is still pending once the tree settles. That is why disabling only the manager row used to kill DSH with 3 entries did not activate (one pending (waiting for service: genieRuntime) line per sub-row) instead of simply dropping the Genie panels. With the deferred form each sub-row activates immediately and its callback runs in a child fiber — invisible to the boot audit — if and when the manager provides the service.

The fence stays singular on purpose. Three copies drift, and the first row that forgot Origin-on-POST would reopen CSRF against a loopback service that can spawn the Genie binary.

Health reports which sub-rows are mounted and the resolved config of each:

{
  "compatible": true, "version": "5.x.y", "executable": "/…/genie",
  "mounted": { "board": true, "workflows": true },
  "config": {
    "manager": { "deadlineMs": 10000, "outputBudgetBytes": 4194304 },
    "board": { "order": 10 },
    "workflows": { "order": 12 }
  }
}

Degraded mode is unchanged: a missing or incompatible binary records the error, health stays up, the read-only catalog still serves, and only the mutating route is withheld.

Per-row config, and disabling a row

Every row takes a config block, and every key has a default, so a row inserted with no config behaves exactly as it did before the split. Each schema is a Standard Schema object the DSH loader validates against (Config), wrapping the same pure resolveConfig function the unit tests call — so a test and a live profile can never disagree about what a config means. The validator is hand-written rather than schemastery: the Host bundle is fully bundled by esbuild from a dependency-free source tree, and cordis requires only a Standard Schema object, not that library specifically.

RowKeyDefaultRefinement
managerdeadlineMs10000the derived socket deadline is deadlineMs + 5000 and must stay at or under the 120 s ceiling, so the socket timer always strictly outlives the handler budget
manageroutputBudgetBytes4194304hard maximum of 4 MiB; a larger budget cannot help, because the whole answer is buffered before the browser sees any of it
boardorder100–1000
workflowsorder120–1000

Any row can be turned off from a profile patch by its id:

- id: genie-dsh-board-workflows
  disabled: true

That row then registers no route, and health stops reporting it as mounted — so the browser drops its panel instead of leaving one that 404s on first use.

The manager row is disableable too, and it is the one whose absence takes the whole suite with it:

- id: genie-dsh-board
  disabled: true

DSH then boots normally with no Genie surface at all: the two sub-rows activate cleanly and register nothing, so /api/genie-board/health is absent along with /workspaces, /action, /workflows and /workflows/document — every one of them a plain DSH 404 — and the browser half shows no Genie panels because it has no health to gate them on. This is the supported way to keep the plugin installed while turning it off.

scripts/dsh-genie-board-smoke.ts mounts both patches as its acceptance proof: phase two disables genie-dsh-board-workflows and phase three disables genie-dsh-board, holding the Host up past the loader audit (a printed authenticated URL is not a boot — dsh web prints one before it audits the settled tree) and then proving every route 404s. src/board.test.ts asserts the same two shapes against a fake cordis context, plus the rule that keeps the manager-disabled case working: no sub-row module exports a row-level inject.

The retired skills row must not be re-added behind a flag. bun test scripts/dsh-genie-board-smoke.test.ts pins that: the disable-by-id phase targets a row the shipped cordis.patch.yml still inserts, and the skills row is absent from the patch's insert list, the bundle list and the probed routes. A profile patch that still carries - id: genie-dsh-board-skills / disabled: true is harmless — DSH's applyEntryPatches warns patch: entry … not found and skips a patch whose id no longer exists, so the already-switched-off profile row can stay exactly as the operator wrote it.

Why the client half is NOT split (2026-09-15)

The Host is three rows; the browser bundle is deliberately still one, with a single exports["./client"]. DSH's own loader makes per-sub-plugin client entry points impossible today:

  • dsh-client-modules/lib/index.js l.825 keys the client module table by package name, so rows of one package cannot own separate client halves.
  • clientExportOf (same file, l.155-165) resolves only exports["./client"] and nothing else.
  • exactPackageSpecifier (l.132-138) returns undefined for a three-segment scoped specifier, so on the non-internal path a subpath-only row resolves no client half at all.

So src/client/index.ts splits its panel loop into labelled ctx.effect registrations inside the one bundle, and gates each on what the manager reports as mounted — never on loader row state. Each registration also refuses to own a panel id another plugin already holds: it checks slot occupancy first and warns instead of overwriting. Revisit per-row client entries only when the loader gains a subpath client contract; do not re-derive the idea from a blueprint without re-reading those three call sites.

The package is not renamed to @automagik/genie-dsh either. That is a decision, not an omission: subpath exports deliver the whole sub-plugin structure at zero release cost, while a rename would have to move ~/.dsh/profiles/web/package.json (both the link: dependency key and dsh.profile.bundles) and DSH_PLUGIN_MEMBERS in the same change.

Build and install

Requires Bun (checkout builds), a Host-installed Genie CLI, and DSH 0.1.2-rc.1 up to, not including, 0.3.0 — top-level engines.dsh in package.json (where DSH's manifest contract puts it) reads >=0.1.2-rc.1 =22.12.0 — the oldest Node the prepare was actually run under, not a floor on anything older. bun run test:node-floor (plugins/dsh-prepare-node-floor.test.ts) runs the real prepare under 22.12.0. It is kept out of the default bun test, which stays network-free, and CI runs it in its own node-floor job on a pinned Node 22.12.0. It fails, never skips, when that Node cannot be resolved: point GENIE_NODE_FLOOR_BIN at a 22.12.0 binary, or let it fetch the official build (checked against a pinned sha256). Bun is needed only for the checkout build below, which runs the same file.

bun run build:plugin
dsh plugin --profile web add link:/absolute/path/to/genie/plugins/dsh-genie-board
dsh plugin --profile web list --depth 0
# Restart DSH after installation.
dsh web --no-open --host 127.0.0.1 --port 0

The immutable artifacts are the three Host bundles — dist/index.js, dist/board.js, dist/workflows.js — and dist/client.js (browser). Each is enumerated explicitly in DSH_PLUGIN_MEMBERS; the attested tarball gaining or losing a member is a reviewed change, never a build detail. Source builds use the root checkout version as their compatibility floor. Release packaging stamps minimumGenieVersion and package version together and rebuilds the Host with that exact candidate without changing checkout metadata. Startup checks strict SemVer using the fixed executable's version; incompatible or invalid versions expose health only, with no board operation routes.

Trust and consistency

The Host resolves Genie once at startup: every PATH entry first, then $GENIE_HOME/bin, ~/.genie/bin and ~/.local/bin, so a DSH launched from a desktop session that never sourced a shell profile still finds an installed Genie. /api/genie-board/health reports the executable it picked. The Host canonicalizes it and runs only fixed argv with shell: false. Its environment is limited to PATH, HOME, GENIE_HOME, NO_COLOR, GENIE_AGENT_NAME and GENIE_AGENT_KIND. The Host identity is derived from its OS user and hostname. Browser requests contain only registry IDs, selected board/task IDs and validated action fields. They never supply paths, worker identity, executables, environment or commands. Positional comment arguments use the standard -- boundary so option-shaped text is literal. All routes first apply DSH connection authentication (its signed browser-session cookie), then require a loopback address and a loopback Host header, and reject any cross-origin Origin or Sec-Fetch-Site. A mutation additionally requires an exact Origin and application/json; a read does not, because a same-origin fetch sends no Origin and older Safari/Firefox and embedded WebViews send no Sec-Fetch-Site either. Every route answers exactly once, including when its handler throws: an unexpected failure is a 500 with a generic message, never an unanswered request and never a Host path. A failure the CALLER caused is separated from that: on every route, read ones included, an invalid document name, an unknown document and an unknown or missing workspace answer 400 with one vetted sentence (Invalid name, Document not found, Unknown workspace, workspaceId required), so the panel and an operator can tell a bad request from a broken Host. Those four sentences are constants; no other message reaches a read route's body. Remote/reverse-proxy operation is intentionally unsupported.

The Host retains bounded selection evidence, not board content: registry ID, canonical repository path, listed board IDs, and the selected board's task IDs and lane names. Explicit list then load establishes this evidence. One request per workspace may run at a time; concurrent selection/mutation requests reject. Every list/load still invokes Genie once, and mutations invoke the fixed action then one complete aggregate read. No previous aggregate is served or optimistic change rendered. Workspace removal, path changes, a killed child (deadline or output overflow) and a failed refresh invalidate evidence. A command that ran and refused does not: Genie applies a verb in one transaction, so a non-zero exit changed nothing, the selection stands and the browser keeps the board and the open card it acted on while showing the refusal. That refusal is Genie's own first Error: line, or the tail of its stderr — never a bare exit code. A mutation followed by a failed refresh reports that the operation may have completed and requires reload; never automatically retry it. The selection authorizes the last confirmed board; direct external database edits or task imports racing a request are outside this snapshot guarantee.

CLI work has a shared 10-second deadline and 4 MiB combined stdout/stderr budget. The active child is killed on timeout/output overflow/error. The request's own answer deadline is strictly longer than that bu