From f8d99c4ba11aff5d5d208e1ae6db1b7ab1c2a5c2 Mon Sep 17 00:00:00 2001 From: Sofia Date: Thu, 3 Sep 2026 01:57:35 +0000 Subject: [PATCH 01/24] chore: initialize project brain Co-authored-by: Felipe Rosa --- .brain/config.json | 4 +++ brain/.brain/config.json | 4 +++ brain/.obsidian/app.json | 7 +++++ brain/.obsidian/daily-notes.json | 5 ++++ brain/.obsidian/graph.json | 46 ++++++++++++++++++++++++++++++++ brain/.obsidian/templates.json | 3 +++ brain/_Templates/daily.md | 13 +++++++++ brain/_Templates/domain.md | 18 +++++++++++++ brain/_Templates/entity.md | 19 +++++++++++++ brain/_Templates/intel.md | 18 +++++++++++++ brain/_Templates/playbook.md | 16 +++++++++++ brain/_index.md | 16 +++++++++++ brain/brain.json | 29 ++++++++++++++++++++ 13 files changed, 198 insertions(+) create mode 100644 .brain/config.json create mode 100644 brain/.brain/config.json create mode 100644 brain/.obsidian/app.json create mode 100644 brain/.obsidian/daily-notes.json create mode 100644 brain/.obsidian/graph.json create mode 100644 brain/.obsidian/templates.json create mode 100644 brain/_Templates/daily.md create mode 100644 brain/_Templates/domain.md create mode 100644 brain/_Templates/entity.md create mode 100644 brain/_Templates/intel.md create mode 100644 brain/_Templates/playbook.md create mode 100644 brain/_index.md create mode 100644 brain/brain.json diff --git a/.brain/config.json b/.brain/config.json new file mode 100644 index 000000000..7a73f856d --- /dev/null +++ b/.brain/config.json @@ -0,0 +1,4 @@ +{ + "brainId": "brn_w06loz", + "vaultDir": "brain" +} diff --git a/brain/.brain/config.json b/brain/.brain/config.json new file mode 100644 index 000000000..7a73f856d --- /dev/null +++ b/brain/.brain/config.json @@ -0,0 +1,4 @@ +{ + "brainId": "brn_w06loz", + "vaultDir": "brain" +} diff --git a/brain/.obsidian/app.json b/brain/.obsidian/app.json new file mode 100644 index 000000000..1498fe3b7 --- /dev/null +++ b/brain/.obsidian/app.json @@ -0,0 +1,7 @@ +{ + "alwaysUpdateLinks": true, + "newFileLocation": "folder", + "newFileFolderPath": "_Templates", + "showUnsupportedFiles": false, + "useMarkdownLinks": false +} \ No newline at end of file diff --git a/brain/.obsidian/daily-notes.json b/brain/.obsidian/daily-notes.json new file mode 100644 index 000000000..50818c074 --- /dev/null +++ b/brain/.obsidian/daily-notes.json @@ -0,0 +1,5 @@ +{ + "folder": "Daily", + "format": "YYYY-MM-DD", + "template": "_Templates/daily" +} \ No newline at end of file diff --git a/brain/.obsidian/graph.json b/brain/.obsidian/graph.json new file mode 100644 index 000000000..c06267043 --- /dev/null +++ b/brain/.obsidian/graph.json @@ -0,0 +1,46 @@ +{ + "collapse_filter": false, + "search": "", + "showTags": true, + "showAttachments": false, + "showOrphans": true, + "colorGroups": [ + { + "query": "path:Daily", + "color": { + "a": 1, + "rgb": 5431424 + } + }, + { + "query": "path:Intelligence", + "color": { + "a": 1, + "rgb": 1474816 + } + }, + { + "query": "path:Playbooks", + "color": { + "a": 1, + "rgb": 26112 + } + }, + { + "query": "tag:#decision", + "color": { + "a": 1, + "rgb": 16744448 + } + } + ], + "showArrow": true, + "textFadeMultiplier": 0, + "nodeSizeMultiplier": 1, + "lineSizeMultiplier": 1, + "centerStrength": 0.5, + "repelStrength": 10, + "linkStrength": 1, + "linkDistance": 250, + "scale": 1 +} \ No newline at end of file diff --git a/brain/.obsidian/templates.json b/brain/.obsidian/templates.json new file mode 100644 index 000000000..0c32cb562 --- /dev/null +++ b/brain/.obsidian/templates.json @@ -0,0 +1,3 @@ +{ + "folder": "_Templates" +} \ No newline at end of file diff --git a/brain/_Templates/daily.md b/brain/_Templates/daily.md new file mode 100644 index 000000000..94e268083 --- /dev/null +++ b/brain/_Templates/daily.md @@ -0,0 +1,13 @@ +--- +type: daily +created: 2026-09-03 +tags: [daily] +--- + +# {{date}} + +## Notes + +## Decisions + +## Next diff --git a/brain/_Templates/domain.md b/brain/_Templates/domain.md new file mode 100644 index 000000000..fbf52e29e --- /dev/null +++ b/brain/_Templates/domain.md @@ -0,0 +1,18 @@ +--- +title: "{{title}}" +type: domain +tags: [] +created: 2026-09-03 +updated: 2026-09-03 +confidence: medium +--- + +# {{title}} + +## Overview + +## Key Concepts + +## Open Questions + +## Signals to Watch diff --git a/brain/_Templates/entity.md b/brain/_Templates/entity.md new file mode 100644 index 000000000..8a0fbfb7a --- /dev/null +++ b/brain/_Templates/entity.md @@ -0,0 +1,19 @@ +--- +title: "{{title}}" +type: entity +entity_type: +tags: [] +created: 2026-09-03 +updated: 2026-09-03 +confidence: medium +source_type: direct +aliases: [] +--- + +# {{title}} + +## Overview + +## Context + +## Relations diff --git a/brain/_Templates/intel.md b/brain/_Templates/intel.md new file mode 100644 index 000000000..302ebff07 --- /dev/null +++ b/brain/_Templates/intel.md @@ -0,0 +1,18 @@ +--- +title: "{{title}}" +type: intel +tags: [] +created: 2026-09-03 +updated: 2026-09-03 +confidence: medium +source: +source_type: direct +--- + +# {{title}} + +## Key Findings + +## Analysis + +## Open Questions diff --git a/brain/_Templates/playbook.md b/brain/_Templates/playbook.md new file mode 100644 index 000000000..c18a1df42 --- /dev/null +++ b/brain/_Templates/playbook.md @@ -0,0 +1,16 @@ +--- +title: "{{title}}" +type: playbook +tags: [] +created: 2026-09-03 +updated: 2026-09-03 +confidence: high +--- + +# {{title}} + +## When to Use + +## Steps + +## Notes diff --git a/brain/_index.md b/brain/_index.md new file mode 100644 index 000000000..8b918d7ca --- /dev/null +++ b/brain/_index.md @@ -0,0 +1,16 @@ +--- +title: "genie" +type: moc +created: 2026-09-03 +updated: 2026-09-03 +tags: [moc, root] +--- + +# genie + +Welcome to the **genie** brain. This is the root Map of Content (MOC). + +## Folders + +- [[Daily]] — Daily notes and logs +- [[to_process]] — Raw content awaiting organization diff --git a/brain/brain.json b/brain/brain.json new file mode 100644 index 000000000..a8e2da704 --- /dev/null +++ b/brain/brain.json @@ -0,0 +1,29 @@ +{ + "id": "brn_w06loz", + "slug": "genie", + "name": "genie", + "type": "engineering", + "owner": { + "type": "agent", + "id": "local" + }, + "homePath": ".", + "strategy": { + "default": "rag", + "segments": [] + }, + "embeddings": { + "enabled": true, + "dims": 768 + }, + "extractor": { + "gateRules": { + "r1TemplateProtection": true, + "r2ConversasImmutable": true, + "r3PathWhitelist": true, + "r4CrossActorIdentity": true, + "r5DailyAppendOnly": true, + "actorChatIds": [] + } + } +} From 7e33d7d80204f40f25c1730f9a9d21636c1f2465 Mon Sep 17 00:00:00 2001 From: Sofia Date: Thu, 3 Sep 2026 13:26:49 +0000 Subject: [PATCH 02/24] docs(brainstorm): design DSH Genie board plugin Co-authored-by: Felipe Rosa --- .genie/INDEX.md | 2 +- .genie/brainstorms/dsh-genie-board/DESIGN.md | 90 ++++++++++++++++++++ .genie/brainstorms/dsh-genie-board/DRAFT.md | 66 ++++++++++++++ 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 .genie/brainstorms/dsh-genie-board/DESIGN.md create mode 100644 .genie/brainstorms/dsh-genie-board/DRAFT.md diff --git a/.genie/INDEX.md b/.genie/INDEX.md index d297b4550..5d1ff8e1f 100644 --- a/.genie/INDEX.md +++ b/.genie/INDEX.md @@ -12,12 +12,12 @@ ## Simmering - - khal-app-kit-identity + genie-remote-ssh — **RELOCATED 2026-07-23** with the whole khal-native-desktop track to `khal-os/genie-desktop:.genie/` (branch `wish/khal-native-theme`; Felipe: desktop context lives in the desktop repo). Second and third of the Theme→Identity→SSH order, both WRS 60 simmering there - [genie-boards-ui](brainstorms/genie-boards-ui/DRAFT.md) — WRS 70; genie desktop boards module: ABSORB of the G4 kanban decided by Felipe; tab + split-toggle layout tentatively locked ("that looks good", confirm at crystallize); archive section + per-project .genie-commit checkboxes added to scope; roadmap shape (per-project board + groom-with-agent task) recommended, awaiting Felipe; depended on ui-bridge board-payload extension (protocol 1.1) + genie-ui-dash G5 (2026-07-21) — **both retired 2026-08-30** (#2834, Felipe: Orca integration is the only UI); this draft is superseded unless re-scoped onto Orca - [intent-to-wish-compiler](brainstorms/intent-to-wish-compiler/DRAFT.md) — **LIVE (re-verified 2026-07-21):** WRS 92; Demand→Patch/Standard/Program router + circuit-breaker (flex cuts autonomous, payout cuts human-only); invisible routing compiled from intent — program-scale, splits at pour time - [brainstorm-domain-map](brainstorms/brainstorm-domain-map/DRAFT.md) — **LIVE (re-verified 2026-07-21):** WRS 80; executable spec compiler (intent → requirement-ID → oracle-class → execution → proof-packet); deterministic gates, residual-risk review only; subjective-truth ownership still open (umbrella G8) ## Ready +- [dsh-genie-board — DESIGN](brainstorms/dsh-genie-board/DESIGN.md) — WRS 100; co-located DSH Web kanban backed only by the supported `genie board --json` / `genie task` CLI contracts; independent design review pending - [skills-everywhere-b — WISH](wishes/skills-everywhere-b/WISH.md) · [skills-everywhere-c — WISH](wishes/skills-everywhere-c/WISH.md) — **both APPROVED 2026-08-31** (B: plan SHIP round 3 `e86d3f12…` after two FIX-FIRST loops — the ~20k-line subtractive wish, 7 groups, wave-0 gates: Wish A stable + rebase ≥ 1b34d7d4b; C: plan SHIP round 2 `8e5611226…` — skills-lint tokens, CLAUDE.md/AGENTS.md/docs rewrite, depends-on B). Wish A flipped SHIPPED with the @ref-pin correction (skills@1.5.23 serves the default branch for any ref). - [skills-everywhere — DESIGN](brainstorms/skills-everywhere/DESIGN.md) · [WISH A](wishes/skills-everywhere/WISH.md) — **design SHIP 2026-08-30 (rev. 6, digest `fafaef24…`, re-stamped after rename from `codex-skill-installer`); Wish A SHIPPED 2026-08-31 (all 7 groups; @ref-pin correction recorded) — umbrella A→B→C, base `dev`:** everywhere except Orca, Genie = skills (skills.sh `npx skills add automagik-dev/genie@v --all --copy`) + CLI, nothing else — Codex/Claude/Kimi/Hermes/pi integrations, hooks, role agents, council stamp and `agent-sync` deleted; 4-platform Codex dogfood matrix → skills-install + update-path smokes diff --git a/.genie/brainstorms/dsh-genie-board/DESIGN.md b/.genie/brainstorms/dsh-genie-board/DESIGN.md new file mode 100644 index 000000000..e1efd3529 --- /dev/null +++ b/.genie/brainstorms/dsh-genie-board/DESIGN.md @@ -0,0 +1,90 @@ +# Design: Genie board for DSH Web + +| Field | Value | +|-------|-------| +| **Slug** | `dsh-genie-board` | +| **Date** | 2026-09-03 | +| **WRS** | 100/100 | + +## Problem + +DSH users cannot inspect or operate the authoritative Genie board from DSH Web. The integration must render Genie's real lifecycle lanes without turning `genie.db` into a cross-repository API or reviving the retired `genie mcp` / `genie ui-bridge` surfaces. + +## Scope + +### IN + +- A dual-face DSH Web plugin at `plugins/dsh-genie-board/`, using `dsh-task-board` only as licensed UI, build, and plugin-loading schematics. +- Host-side execution of supported `genie board --json` and `genie task ...` commands in a DSH workspace selected by stable workspace id. +- A Genie-branded kanban that renders board-defined lanes, card metadata, and Host-confirmed mutations. +- Board selection plus task create, move, comment, block/unblock, checkout/release, and done actions supported by the current CLI. +- Fail-closed executable, workspace, argv, timeout, output-size, and JSON validation; same-origin Host routes; focused tests and a local linked-plugin smoke. +- Public installation documentation, reference attribution, release packaging, and the `dsh-plugin` GitHub topic after verification. + +### OUT + +- Direct reads or writes to `.genie/genie.db`. +- Reintroducing `genie mcp`, `genie ui-bridge`, a daemon, a plugin-owned ledger, or another Genie protocol. +- The reference plugin's cron scheduler, execution runner, continuation cards, handover bundles, or sleep inhibitor. +- Automatic task execution by DSH sessions, hard delete, task dependency editing, wish authoring, or promotion/deploy controls in v1. +- Homolog, production, stable release, or external announcement beyond the requested repository topic without a separate gate. + +## Approach + +Co-locate `@automagik/genie-dsh-board` under `plugins/dsh-genie-board/` in the public Genie repository. Its Host half receives a DSH workspace id, resolves that id through `workspaceRegistry`, requires a physical repository containing `.genie`, and invokes the installed `genie` executable with a fixed command allowlist, argv arrays, `shell: false`, a bounded environment, deadline, and output cap. Reads parse and validate `genie board list --json`, `genie board --board --json`, and `genie task status ` outputs; writes map a strict discriminated action union to current `genie task` commands and then return a fresh board snapshot. + +The browser half follows the reference plugin's Cordis manifest, client injection, sidebar mount, CSS, and kanban component patterns while keeping Genie authoritative. It discovers eligible DSH workspaces from a Host endpoint, lets the user choose a workspace and board, re-fetches on explicit refresh, successful mutation, and page visibility recovery, and never applies optimistic task state. Host errors are bounded and visible. + +Alternatives considered: `namastexlabs/dsh-plugins` would reuse DSH tooling but is private and publishing it would expose unrelated packages; a new public repository would add release/version-skew policy immediately; reviving MCP/ui-bridge contradicts their explicit retirement; direct SQLite access freezes a private schema. All lose to co-location plus the supported CLI. + +## Simplicity Case + +- **Simplest complete design:** one dual-face package, one in-memory request mapping, and the existing Genie CLI as the only state authority; browser state is replaced from confirmed snapshots. +- **Added machinery:** a Host route fence, strict action union, child-process deadline/output cap, and schema validation are required because a browser-triggered long-lived Host is crossing into repository mutations. +- **Deferred until measured:** SSE/file watchers, polling, caches, batch mutations, autonomous card execution, cron, and resumable requests stay out until explicit user demand or measured refresh latency makes manual/visibility refresh inadequate. +- **Complexity removed:** no new durable state, migration, socket, daemon, synchronization protocol, duplicate task ledger, or cross-repository compatibility matrix. + +## Decisions + +| # | Decision | Rationale | +|---|----------|-----------| +| 1 | Package lives in `automagik-dev/genie` at `plugins/dsh-genie-board/`. | The adapter and the CLI contract ship together; the existing public repository can carry the requested topic without exposing unrelated private code. | +| 2 | Only current public CLI commands cross the boundary. | `genie mcp` and `genie ui-bridge` are explicit refusal stubs; SQLite is private. | +| 3 | Resolve workspace ids through DSH and never accept arbitrary browser paths. | Keeps path authority on the Host and prevents traversal or mutation of an unselected repository. | +| 4 | Mutations are strict action-to-argv mappings and refresh before response. | Prevents command injection and ensures the browser never displays unconfirmed state. | +| 5 | Copy only MIT-licensed plugin/UI schematics with attribution. | Reuses proven DSH mount mechanics without importing the reference ledger, scheduler, runner, or security assumptions. | +| 6 | v1 is a human board surface, not an execution orchestrator. | Meets the requested Genie-board integration while avoiding a second agent lifecycle and permission model. | +| 7 | Add the `dsh-plugin` topic only after build, tests, and linked-profile smoke succeed. | The topic is an external discoverability claim and should point to a working install path. | + +## Risks & Assumptions + +| # | Risk | Severity | Mitigation | +|---|------|----------|------------| +| 1 | Current CLI output omits data or stable JSON for a desired action. | Medium | Freeze only observed documented JSON reads; make unsupported actions visible and add Genie-side JSON only when a concrete UI criterion requires it. | +| 2 | Browser-triggered CLI processes hang or emit excessive/malformed output. | High | Abort deadline, terminate child, cap stdout/stderr, validate JSON and exit code, and test every failure path. | +| 3 | DSH package APIs differ between local `0.1.1-rc.2` and the newer reference package. | Medium | Target and smoke the installed profile first; use only official injected services available locally and declare the proven engine floor. | +| 4 | Co-locating a DSH package silently breaks Genie's release payload. | Medium | Extend package/build/release manifests intentionally and run the repository's full check plus release verification for the affected platform contract. | +| 5 | Reference code is copied without attribution or drifts into unrelated behavior. | Medium | Preserve MIT attribution in package documentation/NOTICE and use a file-by-file provenance inventory in review. | +| 6 | Topic publication precedes a usable install. | Low | Topic mutation is the final, separately evidenced action after linked-plugin read-back. | + +## Success Criteria + +- [ ] In DSH Web, a user selects an eligible workspace and Genie board and sees lanes/cards semantically matching the same CLI JSON snapshot, including status, assignment, liveness, blocks, and comments where the CLI exposes them. +- [ ] Create and move round-trip through `genie task`, and every supported mutation returns a fresh confirmed snapshot with no optimistic browser state. +- [ ] An unknown workspace id, non-physical or non-Genie directory, missing executable, disallowed action, invalid input, timeout, oversized output, malformed JSON, or non-zero exit produces a bounded visible error and no out-of-scope mutation. +- [ ] The Host never invokes a shell, accepts an executable path or raw argv from the browser, reads SQLite directly, or persists a second task ledger. +- [ ] Package typecheck, unit tests, build, focused security tests, local `dsh plugin --profile web add link:...` smoke, and the Genie full gate pass against the final commit. +- [ ] Installation and compatibility are documented, copied reference portions are attributed, the plugin is included in the supported release payload, and `automagik-dev/genie` has the `dsh-plugin` topic. + +## Next Step + +After an independent design review returns SHIP, persist the evidence below and verify its content digest before running `wish`. + + +## Design Review Evidence + +- **Verdict:** PENDING +- **Reviewed content SHA-256:** PENDING +- **Reviewer:** PENDING +- **Reviewed at:** PENDING + diff --git a/.genie/brainstorms/dsh-genie-board/DRAFT.md b/.genie/brainstorms/dsh-genie-board/DRAFT.md new file mode 100644 index 000000000..74eb9a2a2 --- /dev/null +++ b/.genie/brainstorms/dsh-genie-board/DRAFT.md @@ -0,0 +1,66 @@ +# Draft: Genie board for DSH Web + +| Field | Value | +|-------|-------| +| **Slug** | `dsh-genie-board` | +| **Date** | 2026-09-03 | +| **WRS** | 100/100 | + +## Problem + +DSH users cannot inspect or operate the authoritative Genie board from DSH Web. The integration must render Genie's real lifecycle lanes without turning `genie.db` into a cross-repository API or reviving the retired `genie mcp` / `genie ui-bridge` surfaces. + +## Scope + +### IN + +- A dual-face DSH Web plugin using the `dsh-task-board` package only as UI and plugin-loading schematics. +- Host-side execution of the supported `genie board --json` and `genie task ...` CLI contracts in an explicitly selected DSH workspace. +- A kanban UI that renders Genie's board-defined lanes and refreshes after confirmed Host mutations. +- Focused task actions supported by the current CLI: create, move, comment, block/unblock, checkout/release, and done. +- Fail-closed repository, executable, output-size, timeout, and argument validation. +- Install, build, test, and topic-based discovery documentation. + +### OUT + +- Direct reads or writes to `.genie/genie.db`. +- Reintroducing `genie mcp`, `genie ui-bridge`, a daemon, or a new Genie protocol. +- Copying the reference plugin's independent ledger, cron scheduler, execution runner, continuation cards, or sleep inhibitor. +- Automatic execution of cards by DSH agents in the first release. +- Homolog or production promotion before separate human approval. + +## Candidate Approaches + +1. **Co-located package in `automagik-dev/genie` (chosen).** Keep the DSH adapter next to the CLI contract it consumes, install from the public GitHub repository, and add the `dsh-plugin` topic to that repository. This avoids a second release authority and version-skew policy. +2. **Package in private `namastexlabs/dsh-plugins`.** Reuses its DSH monorepo tooling, but public discoverability would require publishing an existing private repository whose other contents were not placed in scope. +3. **New standalone public repository.** Gives the cleanest marketplace identity, but immediately creates cross-repository release/version-skew work and conflicts with the current registered-project list. + +## Simplest Complete Design + +The DSH Host resolves one workspace root, runs a fixed allowlist of `genie` argv arrays with `shell: false`, parses bounded JSON for reads, and returns confirmed snapshots/actions over same-origin Host routes. The browser mounts a Genie-branded board and re-fetches after each mutation. No plugin-owned task database and no background synchronization state are introduced. + +## Decisions + +- Settled: Genie CLI is the only task-state boundary; SQLite is private. +- Settled: DSH task-board code is a schematic/reference, not a state model to fork wholesale. +- Settled: v1 is human-operated board management, not scheduled autonomous execution. +- Settled: the package lives at `plugins/dsh-genie-board/` in `automagik-dev/genie`; the repository receives the `dsh-plugin` topic after the plugin is verified. + +## Risks + +- CLI JSON/exit contracts may be incomplete for some UI actions; freeze only current documented commands and surface unsupported actions rather than inventing writes. +- DSH Host is long-lived while Genie is zero-daemon; every child process needs timeout, output caps, abort cleanup, and no shell. +- A broad copy of `dsh-task-board` would import an unrelated ledger and security model; copy only mount/build/UI patterns. +- The installed DSH is `0.1.1-rc.2`, while the reference task-board targets newer alpha packages; compatibility must be proven against the local profile. + +## Success Criteria + +- A DSH Web user selects a Genie repository and sees the same boards, lanes, cards, status, assignment, liveness, block, and comment information represented by the CLI JSON output. +- Create and move actions round-trip through `genie task` and the UI shows only Host-confirmed state. +- Invalid workspace paths, missing Genie, timeouts, malformed/oversized output, and non-zero CLI exits produce bounded visible errors and no state mutation outside the selected repository. +- Package typecheck, unit tests, build, a local linked-plugin smoke, and the Genie repository's required validation gates pass. +- The public installation path is documented and its public GitHub repository has the `dsh-plugin` topic. + +## Next Step + +Crystallize the design, obtain an independent cross-family review, and only then scaffold the executable wish. From 016675079ba0bad5bfd7b034d63cc074e7fb227c Mon Sep 17 00:00:00 2001 From: Sofia Date: Thu, 3 Sep 2026 13:31:53 +0000 Subject: [PATCH 03/24] docs(wish): plan DSH Genie board plugin --- .genie/brainstorms/dsh-genie-board/DESIGN.md | 8 +- .genie/wishes/dsh-genie-board/WISH.md | 202 +++++++++++++++++++ 2 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 .genie/wishes/dsh-genie-board/WISH.md diff --git a/.genie/brainstorms/dsh-genie-board/DESIGN.md b/.genie/brainstorms/dsh-genie-board/DESIGN.md index e1efd3529..886337901 100644 --- a/.genie/brainstorms/dsh-genie-board/DESIGN.md +++ b/.genie/brainstorms/dsh-genie-board/DESIGN.md @@ -83,8 +83,8 @@ After an independent design review returns SHIP, persist the evidence below and ## Design Review Evidence -- **Verdict:** PENDING -- **Reviewed content SHA-256:** PENDING -- **Reviewer:** PENDING -- **Reviewed at:** PENDING +- **Verdict:** SHIP +- **Reviewed content SHA-256:** `073cce9acfc9a256756a87440cf752559fc9fd9a1d31d8ca7c36615fcece4aaf` +- **Reviewer:** agent:steve:dashboard:0950bf4d-628a-4797-b387-2d2dcaf3273a +- **Reviewed at:** 2026-09-03T13:29:28.000Z diff --git a/.genie/wishes/dsh-genie-board/WISH.md b/.genie/wishes/dsh-genie-board/WISH.md new file mode 100644 index 000000000..ef1244e60 --- /dev/null +++ b/.genie/wishes/dsh-genie-board/WISH.md @@ -0,0 +1,202 @@ +# Wish: Genie board for DSH Web + +| Field | Value | +|-------|-------| +| **Status** | DRAFT | +| **Slug** | `dsh-genie-board` | +| **Date** | 2026-09-03 | +| **Author** | Sofia with Felipe | +| **Appetite** | medium | +| **Branch** | `wish/dsh-genie-board` | +| **Repos touched** | genie | +| **Design** | [DESIGN.md](../../brainstorms/dsh-genie-board/DESIGN.md) | + +## Summary + +Ship a dual-face DSH Web plugin that displays and operates the authoritative Genie board through supported Genie CLI commands. The plugin lives with Genie, never opens `genie.db`, and keeps browser state subordinate to Host-confirmed snapshots. + +## Scope + +### IN + +- `plugins/dsh-genie-board/`: Host adapter, same-origin routes, browser kanban, DSH manifest, package build, tests, and install docs. +- Workspace/board discovery, board rendering, and supported create/move/comment/block/unblock/checkout/release/done mutations. +- Fixed executable and action-to-argv mappings with no shell, plus deadlines, aggregate subprocess/output budgets, JSON validation, and bounded errors. +- Release-payload integration, reference attribution, linked-profile smoke, and topic publication only after validation. + +### OUT + +- Direct SQLite access; revival of `genie mcp` or `genie ui-bridge`; plugin ledgers, watchers, polling, SSE, cron, or autonomous execution. +- Hard delete, dependency editing, wish authoring, deploy controls, production promotion, and announcements beyond the requested GitHub topic. + +## Decisions + +| # | Decision | Rationale | +|---|----------|-----------| +| 1 | Co-locate the package in public Genie. | Adapter and CLI compatibility ship together without exposing the private DSH monorepo. | +| 2 | Use only current CLI commands and validated JSON. | SQLite is private and former protocol surfaces are retired. | +| 3 | Resolve DSH workspace ids on the Host and map a strict action union to argv. | Browser input never becomes a path, executable, raw command, or arbitrary argv. | +| 4 | Use board JSON as the canonical snapshot; enrich only from structured data. | Human-formatted `task status` is not a compatibility contract. | +| 5 | If needed, add the smallest `genie task status --json` contract. | Supplies liveness/comments without reviving a resident protocol; plugin pins its Genie version floor. | +| 6 | Refresh the selected board after every mutation. | The browser never displays optimistic state. | + +## Simplicity Case + +- **Simplest complete design:** one dual-face plugin, one strict Host adapter, and the existing Genie CLI as sole authority. +- **Added machinery:** route fencing, action validation, process/output caps, and optional structured status enrichment are required by browser-triggered mutations and requested card detail. +- **Deferred until measured:** polling, SSE, caches, deltas, batching, autonomous execution, cron, and resumable requests require explicit demand or measured refresh latency. +- **Complexity removed:** no durable state, daemon, socket, synchronization protocol, human-output parser, duplicate ledger, or configurable command surface. + +## Dependencies + +**depends-on:** none +**blocks:** none + +## Success Criteria + +- [ ] DSH Web selects an eligible workspace/board and renders lanes/cards matching validated CLI JSON, including structured assignment, block, liveness, and comment data. +- [ ] All supported mutations map to fixed Genie argv and return a fresh confirmed snapshot. +- [ ] Unsafe workspace/action/input, timeout, aggregate limit, malformed JSON, missing executable, and non-zero exit fail visibly without out-of-scope mutation. +- [ ] No shell, browser-provided path/executable/argv, direct SQLite access, human-output parser, plugin ledger, watcher, or daemon exists. +- [ ] Plugin tests/typecheck/build, security tests, linked-profile smoke, Genie full gate, and release verification pass. +- [ ] Install/compatibility docs and attribution are complete; only then the repository gains the `dsh-plugin` topic. + +## Execution Strategy + +### Wave 1 (sequential) + +| Group | Agent | Complexity | Model | Description | +|-------|-------|------------|-------|-------------| +| 1 | implementor | 3 — CLI contract plus multi-module surface | implementor-mid / high | Freeze structured reads and compatibility floor. | + +### Wave 2 (after Wave 1) + +| Group | Agent | Complexity | Model | Description | +|-------|-------|------------|-------|-------------| +| 2 | implementor | 5 — DSH integration plus subprocess/security boundaries | implementor-high / high | Build and test the dual-face plugin. | + +### Wave 3 (after Wave 2) + +| Group | Agent | Complexity | Model | Description | +|-------|-------|------------|-------|-------------| +| 3 | implementor | 3 — release payload and external topic gate | implementor-mid / high | Package, smoke, document, and publish discoverability. | + +## Execution Groups + +### Group 1: Structured CLI contract + +**Goal:** Freeze the smallest stable JSON surface the plugin needs without parsing human output. + +**Deliverables:** +1. Fixtures for board-list and lane-board JSON, including assignment and enforced blocks. +2. Bounded enrichment: at most 20 card-detail subprocesses per refresh, concurrency 4, total deadline 10 seconds, aggregate stdout/stderr 4 MiB. +3. If required fields are absent, additive `genie task status --json` with success/not-found/stderr/exit/idempotent-read tests; otherwise document why it is unnecessary. +4. Compatibility floor pinned to the first Genie release providing all consumed fields. + +**Acceptance Criteria:** +- [ ] No human CLI output is parsed; every consumed JSON key has a fixture/schema assertion. +- [ ] The 20-card, concurrency-4, 10-second, and 4-MiB bounds are deterministic and tested. + +**Validation:** +```bash +bun test src/term-commands/v5-board.test.ts src/term-commands/v5-task.test.ts && bun run check +``` + +Full gate is required because this changes a shared CLI contract. + +**depends-on:** none + +--- + +### Group 2: DSH Host adapter and kanban + +**Goal:** Deliver the secure, Host-confirmed DSH Web board experience. + +**Deliverables:** +1. Package/manifest/build scaffold using locally proven official DSH injections and attributed reference schematics. +2. Workspace discovery and fixed CLI runner with `shell: false`, bounded environment, strict actions, process/output limits, validation, and same-origin routes. +3. Browser selectors, lanes/cards/details/errors, mutation controls, refresh, and visibility recovery without optimistic state. +4. Unit/component/security tests, including hostile identifiers and no-command-injection assertions. + +**Acceptance Criteria:** +- [ ] Rendering matches fixture-backed Host snapshots; mutations use fixed argv and refresh. +- [ ] Every unsafe/failure case is bounded and cannot invoke an out-of-scope command. +- [ ] No database access, persistence, watcher, poller, shell, or browser-supplied executable/path/argv exists. + +**Validation:** +```bash +bun run check && bun run build:plugin && bun test plugins/dsh-genie-board +``` + +Full gate plus plugin build covers runtime and trust-boundary risk. + +**depends-on:** Group 1 + +--- + +### Group 3: Install, release, and discoverability + +**Goal:** Prove the packaged plugin can be installed locally before advertising it. + +**Deliverables:** +1. Install/compatibility docs, provenance/NOTICE, and release manifest inclusion with version parity. +2. Linked-profile smoke covering install, restart/read-back, board load, and reversible create/move in an isolated fixture repo. +3. Final clean-checkout release verification; only after it passes, add the GitHub topic. + +**Acceptance Criteria:** +- [ ] Artifact contains the plugin/manifests/docs with matching versions. +- [ ] Linked install/read-back and reversible smoke pass without changing a user repository. +- [ ] Topic publication occurs last and is verified by read-back. + +**Validation:** +```bash +bun install --frozen-lockfile && bun run check && bun run build:plugin && bun run verify:release +``` + +Repository full/release gates cover distribution and external compatibility claims. + +**depends-on:** Group 2 + +--- + +## QA Criteria + +- [ ] Fixture repository loads workspace, board, lanes, details, and errors correctly in DSH Web. +- [ ] Every supported mutation round-trips and only confirmed refreshed state renders. +- [ ] Adversarial route/action/path/identifier cases cannot escape fixed command/workspace boundaries. +- [ ] Existing Genie CLI output and non-DSH release behavior remain compatible. + +--- + +## Assumptions / Risks + +| Risk | Severity | Mitigation | +|------|----------|------------| +| DSH APIs drift from installed `0.1.1-rc.2`. | Medium | Use locally proven injections, linked smoke, and tested engine floor. | +| Per-card detail creates subprocess pressure. | Medium | Hard cap count/concurrency/bytes/time and expose partial-detail warnings. | +| Additive JSON becomes a public contract. | Medium | Fixture every key and pin plugin compatibility floor. | +| Release payload omits co-located files. | High | Update manifests and verify final tarballs from a clean checkout. | + +--- + +## Review Results + +_The read-only reviewer returns evidence; the invoking orchestrator appends a timestamped block here._ + +--- + +## Files to Create/Modify + +``` +plugins/dsh-genie-board/** +src/term-commands/v5-task.ts +src/term-commands/v5-task.test.ts +scripts/build.js +scripts/sync.js +scripts/verify-release.sh +package.json +README.md +.genie/brainstorms/dsh-genie-board/** +.genie/wishes/dsh-genie-board/WISH.md +.genie/INDEX.md +``` From 534bec59cec46d600b5f8abc108af36127ef249f Mon Sep 17 00:00:00 2001 From: Felipe Rosa Date: Thu, 3 Sep 2026 13:45:23 +0000 Subject: [PATCH 04/24] docs(wish): close dsh board review gaps --- .genie/INDEX.md | 2 +- .genie/wishes/dsh-genie-board/WISH.md | 111 +++++++++++++++++++++----- 2 files changed, 90 insertions(+), 23 deletions(-) diff --git a/.genie/INDEX.md b/.genie/INDEX.md index 5d1ff8e1f..c306dfa44 100644 --- a/.genie/INDEX.md +++ b/.genie/INDEX.md @@ -17,7 +17,7 @@ - [intent-to-wish-compiler](brainstorms/intent-to-wish-compiler/DRAFT.md) — **LIVE (re-verified 2026-07-21):** WRS 92; Demand→Patch/Standard/Program router + circuit-breaker (flex cuts autonomous, payout cuts human-only); invisible routing compiled from intent — program-scale, splits at pour time - [brainstorm-domain-map](brainstorms/brainstorm-domain-map/DRAFT.md) — **LIVE (re-verified 2026-07-21):** WRS 80; executable spec compiler (intent → requirement-ID → oracle-class → execution → proof-packet); deterministic gates, residual-risk review only; subjective-truth ownership still open (umbrella G8) ## Ready -- [dsh-genie-board — DESIGN](brainstorms/dsh-genie-board/DESIGN.md) — WRS 100; co-located DSH Web kanban backed only by the supported `genie board --json` / `genie task` CLI contracts; independent design review pending +- [dsh-genie-board — DESIGN](brainstorms/dsh-genie-board/DESIGN.md) → [WISH](wishes/dsh-genie-board/WISH.md) — **DRAFT** (plan FIX-FIRST loop 1/2): co-located DSH Web kanban backed only by exact `genie board --json` and unconditional `genie task status --json` contracts; secure fixed-argv Host adapter, four-platform release payload, and linked-profile smoke specified - [skills-everywhere-b — WISH](wishes/skills-everywhere-b/WISH.md) · [skills-everywhere-c — WISH](wishes/skills-everywhere-c/WISH.md) — **both APPROVED 2026-08-31** (B: plan SHIP round 3 `e86d3f12…` after two FIX-FIRST loops — the ~20k-line subtractive wish, 7 groups, wave-0 gates: Wish A stable + rebase ≥ 1b34d7d4b; C: plan SHIP round 2 `8e5611226…` — skills-lint tokens, CLAUDE.md/AGENTS.md/docs rewrite, depends-on B). Wish A flipped SHIPPED with the @ref-pin correction (skills@1.5.23 serves the default branch for any ref). - [skills-everywhere — DESIGN](brainstorms/skills-everywhere/DESIGN.md) · [WISH A](wishes/skills-everywhere/WISH.md) — **design SHIP 2026-08-30 (rev. 6, digest `fafaef24…`, re-stamped after rename from `codex-skill-installer`); Wish A SHIPPED 2026-08-31 (all 7 groups; @ref-pin correction recorded) — umbrella A→B→C, base `dev`:** everywhere except Orca, Genie = skills (skills.sh `npx skills add automagik-dev/genie@v --all --copy`) + CLI, nothing else — Codex/Claude/Kimi/Hermes/pi integrations, hooks, role agents, council stamp and `agent-sync` deleted; 4-platform Codex dogfood matrix → skills-install + update-path smokes diff --git a/.genie/wishes/dsh-genie-board/WISH.md b/.genie/wishes/dsh-genie-board/WISH.md index ef1244e60..fc915407a 100644 --- a/.genie/wishes/dsh-genie-board/WISH.md +++ b/.genie/wishes/dsh-genie-board/WISH.md @@ -36,8 +36,8 @@ Ship a dual-face DSH Web plugin that displays and operates the authoritative Gen | 1 | Co-locate the package in public Genie. | Adapter and CLI compatibility ship together without exposing the private DSH monorepo. | | 2 | Use only current CLI commands and validated JSON. | SQLite is private and former protocol surfaces are retired. | | 3 | Resolve DSH workspace ids on the Host and map a strict action union to argv. | Browser input never becomes a path, executable, raw command, or arbitrary argv. | -| 4 | Use board JSON as the canonical snapshot; enrich only from structured data. | Human-formatted `task status` is not a compatibility contract. | -| 5 | If needed, add the smallest `genie task status --json` contract. | Supplies liveness/comments without reviving a resident protocol; plugin pins its Genie version floor. | +| 4 | Use board JSON as the canonical lane snapshot and unconditionally enrich rendered cards with `genie task status --json`. | Assignment, heartbeat/liveness, enforced blocks, dependencies, timeline, and comments are all machine-readable; human output is never a compatibility contract. | +| 5 | Add and freeze the exact status JSON contract in Group 1, then enforce its first released Genie version in the plugin manifest and at runtime. | Compatibility fails closed before a board command when the installed Genie is too old; documentation is not the enforcement boundary. | | 6 | Refresh the selected board after every mutation. | The browser never displays optimistic state. | ## Simplicity Case @@ -88,14 +88,42 @@ Ship a dual-face DSH Web plugin that displays and operates the authoritative Gen **Goal:** Freeze the smallest stable JSON surface the plugin needs without parsing human output. **Deliverables:** -1. Fixtures for board-list and lane-board JSON, including assignment and enforced blocks. -2. Bounded enrichment: at most 20 card-detail subprocesses per refresh, concurrency 4, total deadline 10 seconds, aggregate stdout/stderr 4 MiB. -3. If required fields are absent, additive `genie task status --json` with success/not-found/stderr/exit/idempotent-read tests; otherwise document why it is unnecessary. -4. Compatibility floor pinned to the first Genie release providing all consumed fields. +1. Additive, unconditional `genie task status --json` support with success, unknown-id, stderr/exit-code, exact-key, nullability, and idempotent-read tests. Human `task status` output remains unchanged. +2. Freeze this versioned response and reject additional/missing keys in fixtures: + + ```ts + { + schemaVersion: 1, + task: { + id: string, boardId: string | null, title: string, + status: 'blocked' | 'ready' | 'in_progress' | 'done', + claimedBy: string | null, claimedAt: number | null, + wish: string | null, group: string | null, + assignedAgent: string | null, assignedReason: string | null, + createdAt: number, updatedAt: number, lane: string | null, + agentKind: string | null, heartbeatAt: number | null, + liveness: 'running' | 'idle' | 'stale' | null, + blockedBy: string | null, blockedReason: string | null, + enforcedBlock: { reason: string, kind: 'work' | 'hold' } | null + }, + dependencies: Array<{ id: string, title: string, status: 'blocked' | 'ready' | 'in_progress' | 'done' }>, + timeline: Array<{ + id: number, kind: string, note: string | null, + authorKind: string | null, author: string | null, createdAt: number + }>, + comments: Array<{ + id: number, note: string, authorKind: string | null, + author: string | null, createdAt: number + }> + } + ``` + + `liveness` is null when `claimedBy` is null and otherwise is derived from `heartbeatAt`; `comments` is the ordered `kind === 'comment'` projection of `timeline` with non-null comment text. +3. Retain exact fixtures for `genie board list --json` (`id`, `name`, `laneCount`, `cardCount`) and lane-board JSON (`scope`; ordered lanes with `name`, `label`, `action`, and exact card keys). Record the first Genie release containing status schema version 1 as the compatibility floor consumed by Group 2. **Acceptance Criteria:** -- [ ] No human CLI output is parsed; every consumed JSON key has a fixture/schema assertion. -- [ ] The 20-card, concurrency-4, 10-second, and 4-MiB bounds are deterministic and tested. +- [ ] `genie task status --json` is always implemented and returns the exact schema above, including heartbeat/liveness, assignment, enforced block/provenance, dependencies, timeline, and structured comments. +- [ ] No human CLI output is parsed; every consumed board/status JSON key has an exact fixture/schema assertion. **Validation:** ```bash @@ -105,6 +133,7 @@ bun test src/term-commands/v5-board.test.ts src/term-commands/v5-task.test.ts && Full gate is required because this changes a shared CLI contract. **depends-on:** none +**blocks:** Group 2 --- @@ -113,24 +142,47 @@ Full gate is required because this changes a shared CLI contract. **Goal:** Deliver the secure, Host-confirmed DSH Web board experience. **Deliverables:** -1. Package/manifest/build scaffold using locally proven official DSH injections and attributed reference schematics. -2. Workspace discovery and fixed CLI runner with `shell: false`, bounded environment, strict actions, process/output limits, validation, and same-origin routes. -3. Browser selectors, lanes/cards/details/errors, mutation controls, refresh, and visibility recovery without optimistic state. -4. Unit/component/security tests, including hostile identifiers and no-command-injection assertions. +1. Create `plugins/dsh-genie-board/package.json`, `agent.cordis.yml`, `cordis.patch.yml`, TypeScript/build configuration, source/tests, and the package-local `build` script; add root `build:plugin` as `bun --cwd plugins/dsh-genie-board run build`. +2. Put the exact first compatible Genie release from Group 1 in the plugin manifest/package metadata and enforce the same floor at Host startup by running the fixed executable as `genie --no-interactive --version`; an older/unparseable version serves no board route. +3. Resolve a workspace id only through DSH `workspaceRegistry`; canonicalize the registry result with `realpath`, require a physical repository containing `.genie`, and use that canonical path as `cwd`. Never accept a browser path. Resolve the Genie executable once from the Host-owned installation, canonicalize it to an absolute executable regular file, and never search for or override it per request. +4. Spawn with `shell: false` and an exact Host-owned environment allowlist: `PATH`, `HOME`, `GENIE_HOME`, `NO_COLOR=1`, `GENIE_AGENT_NAME=`, and `GENIE_AGENT_KIND=dsh`; drop every other variable and accept no environment value from the browser. +5. Implement this normative action table; every argv vector includes `--no-interactive` and no action may synthesize another vector: + + | Action | Fixed executable argv | + |--------|-----------------------| + | List boards | `genie --no-interactive board list --json` | + | Read board | `genie --no-interactive board --board --json` | + | Read card detail | `genie --no-interactive task status --json` | + | Create | `genie --no-interactive task create --title --board <ref>` | + | Move | `genie --no-interactive task move <id> --to <lane>` | + | Comment | `genie --no-interactive task comment <id> <text>` | + | Block | `genie --no-interactive task block <id> --reason <text> [--hold]` | + | Unblock | `genie --no-interactive task unblock <id>` | + | Checkout | `genie --no-interactive task checkout <id> --worker <host-derived-identity>` | + | Release | `genie --no-interactive task release <id>` | + | Done | `genie --no-interactive task done <id>` | + +6. Validate `workspaceId` by exact registry membership; accept `boardRef` only when it equals an id returned by validated board-list JSON; require task ids matching `^t_[a-z0-9]+$`; require lane to equal a lane name from the selected validated board; trim and bound title to 1–200 UTF-8 bytes, comment text to 1–4000, and block reason to 1–1000; reject NUL/control characters, unknown object keys, non-boolean `hold`, and all browser-supplied worker/path/executable/environment/command/argv fields. +7. Bound each request to at most 20 Genie processes total, concurrency 4, a 10-second aggregate deadline, and 4 MiB aggregate stdout plus stderr. Kill remaining children on the first timeout/limit/error; validate exit code and closed JSON schemas before use; return an explicit partial-detail warning when the process cap leaves cards unenriched. Test every bound deterministically, including hostile identifiers and command-injection attempts. +8. Implement browser selectors, lanes/cards/details/errors, mutations, refresh, and visibility recovery; each mutation response re-reads the board and status JSON and never applies optimistic state. +9. Add `scripts/dsh-genie-board-smoke.ts`: create isolated fixture repo/profile state, run `dsh plugin --profile web add link:<absolute-plugin-dir>`, launch `dsh web --no-open --host 127.0.0.1 --port 0`, stop/relaunch it after install, prove `dsh plugin --profile web list --depth 0` reports `@automagik/genie-dsh-board`, read back the plugin health/compatibility route, perform board list/load plus reversible create/move through the Host route, and in `finally` stop the server, run `dsh plugin --profile web remove @automagik/genie-dsh-board`, and delete only the temporary profile/repository. **Acceptance Criteria:** - [ ] Rendering matches fixture-backed Host snapshots; mutations use fixed argv and refresh. - [ ] Every unsafe/failure case is bounded and cannot invoke an out-of-scope command. +- [ ] The manifest and runtime reject every Genie version below the first release containing status schema version 1. +- [ ] Package build and the linked-profile install/restart/read-back/board-operation/cleanup smoke pass against DSH `0.1.1-rc.2` or a newer explicitly proven floor. - [ ] No database access, persistence, watcher, poller, shell, or browser-supplied executable/path/argv exists. **Validation:** ```bash -bun run check && bun run build:plugin && bun test plugins/dsh-genie-board +bun run check && bun run build:plugin && bun test plugins/dsh-genie-board && bun scripts/dsh-genie-board-smoke.ts ``` Full gate plus plugin build covers runtime and trust-boundary risk. **depends-on:** Group 1 +**blocks:** Group 3 --- @@ -139,23 +191,33 @@ Full gate plus plugin build covers runtime and trust-boundary risk. **Goal:** Prove the packaged plugin can be installed locally before advertising it. **Deliverables:** -1. Install/compatibility docs, provenance/NOTICE, and release manifest inclusion with version parity. -2. Linked-profile smoke covering install, restart/read-back, board load, and reversible create/move in an isolated fixture repo. -3. Final clean-checkout release verification; only after it passes, add the GitHub topic. +1. Install/compatibility docs and provenance/NOTICE with the manifest-enforced Genie floor from Group 2. +2. Include `plugins/dsh-genie-board/` in `scripts/build-binary.sh`; extend `scripts/release-payload-version.ts` so package/manifest versions are stamped and verified in staged and extracted payloads; update `scripts/release-payload-version.test.ts`, `scripts/release-docs.test.ts`, `scripts/version-format.test.ts`, `scripts/version-ci-staging.test.ts`, and payload inventory/parity tests. Do not replace or modify the existing `scripts/verify-release.sh` interface. +3. From a clean checkout, build and inspect `linux-x64-glibc`, `linux-x64-musl`, `linux-arm64`, and `darwin-arm64`; each tarball must contain the built plugin, manifest/patch, docs/NOTICE, and matching root/plugin versions. +4. After the candidate has a signed tag and all four tarballs plus `.bundle` and `.intoto.jsonl` sidecars, run the existing verifier against that concrete tag. Only after all checks pass, add the GitHub topic and verify it by read-back. **Acceptance Criteria:** - [ ] Artifact contains the plugin/manifests/docs with matching versions. -- [ ] Linked install/read-back and reversible smoke pass without changing a user repository. +- [ ] All four supported artifacts contain the plugin and pass inventory/version inspection; the linked smoke from Group 2 changes no user repository. - [ ] Topic publication occurs last and is verified by read-back. **Validation:** ```bash -bun install --frozen-lockfile && bun run check && bun run build:plugin && bun run verify:release +bun install --frozen-lockfile +bun run check +bun run build:plugin +VERSION="$(jq -r .version package.json)" +for PLATFORM in linux-x64-glibc linux-x64-musl linux-arm64 darwin-arm64; do + bun run build:binary -- --platform "$PLATFORM" --version "$VERSION" + tar -tzf "dist/genie-$VERSION-$PLATFORM.tar.gz" | grep -E '^\./plugins/dsh-genie-board/(package.json|agent.cordis.yml|cordis.patch.yml|dist/)' +done +bun run verify:release -- "v$VERSION" ``` -Repository full/release gates cover distribution and external compatibility claims. +The final command is intentionally post-publication: `v$VERSION` must be the concrete signed candidate tag. The live verifier downloads every platform artifact and requires each adjacent `.bundle` and `.intoto.jsonl`; a merely local unsigned build does not satisfy this gate. **depends-on:** Group 2 +**blocks:** release/topic publication --- @@ -191,9 +253,14 @@ _The read-only reviewer returns evidence; the invoking orchestrator appends a ti plugins/dsh-genie-board/** src/term-commands/v5-task.ts src/term-commands/v5-task.test.ts -scripts/build.js -scripts/sync.js -scripts/verify-release.sh +scripts/dsh-genie-board-smoke.ts +scripts/build-binary.sh +scripts/release-payload-version.ts +scripts/release-payload-version.test.ts +scripts/release-docs.test.ts +scripts/version-format.test.ts +scripts/version-ci-staging.test.ts +scripts/orca-manifest-parity.test.ts package.json README.md .genie/brainstorms/dsh-genie-board/** From 8ea3a1ae03778d976654027cb545c4984c41168c Mon Sep 17 00:00:00 2001 From: Sofia <genie@namastex.ai> Date: Thu, 3 Sep 2026 14:30:04 +0000 Subject: [PATCH 05/24] docs(wish): record dsh board plan blockers --- .genie/INDEX.md | 2 +- .genie/wishes/dsh-genie-board/WISH.md | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.genie/INDEX.md b/.genie/INDEX.md index c306dfa44..d9857f119 100644 --- a/.genie/INDEX.md +++ b/.genie/INDEX.md @@ -17,7 +17,7 @@ - [intent-to-wish-compiler](brainstorms/intent-to-wish-compiler/DRAFT.md) — **LIVE (re-verified 2026-07-21):** WRS 92; Demand→Patch/Standard/Program router + circuit-breaker (flex cuts autonomous, payout cuts human-only); invisible routing compiled from intent — program-scale, splits at pour time - [brainstorm-domain-map](brainstorms/brainstorm-domain-map/DRAFT.md) — **LIVE (re-verified 2026-07-21):** WRS 80; executable spec compiler (intent → requirement-ID → oracle-class → execution → proof-packet); deterministic gates, residual-risk review only; subjective-truth ownership still open (umbrella G8) ## Ready -- [dsh-genie-board — DESIGN](brainstorms/dsh-genie-board/DESIGN.md) → [WISH](wishes/dsh-genie-board/WISH.md) — **DRAFT** (plan FIX-FIRST loop 1/2): co-located DSH Web kanban backed only by exact `genie board --json` and unconditional `genie task status <id> --json` contracts; secure fixed-argv Host adapter, four-platform release payload, and linked-profile smoke specified +- [dsh-genie-board — DESIGN](brainstorms/dsh-genie-board/DESIGN.md) → [WISH](wishes/dsh-genie-board/WISH.md) — **FIX-FIRST** (plan loop 2/2 exhausted): three HIGH gaps remain in detail-hydration completeness, compatibility-floor sequencing, and exact four-artifact published-release proof; implementation, release, and publication blocked pending Sofia/Felipe decisions and a fresh independent plan review - [skills-everywhere-b — WISH](wishes/skills-everywhere-b/WISH.md) · [skills-everywhere-c — WISH](wishes/skills-everywhere-c/WISH.md) — **both APPROVED 2026-08-31** (B: plan SHIP round 3 `e86d3f12…` after two FIX-FIRST loops — the ~20k-line subtractive wish, 7 groups, wave-0 gates: Wish A stable + rebase ≥ 1b34d7d4b; C: plan SHIP round 2 `8e5611226…` — skills-lint tokens, CLAUDE.md/AGENTS.md/docs rewrite, depends-on B). Wish A flipped SHIPPED with the @ref-pin correction (skills@1.5.23 serves the default branch for any ref). - [skills-everywhere — DESIGN](brainstorms/skills-everywhere/DESIGN.md) · [WISH A](wishes/skills-everywhere/WISH.md) — **design SHIP 2026-08-30 (rev. 6, digest `fafaef24…`, re-stamped after rename from `codex-skill-installer`); Wish A SHIPPED 2026-08-31 (all 7 groups; @ref-pin correction recorded) — umbrella A→B→C, base `dev`:** everywhere except Orca, Genie = skills (skills.sh `npx skills add automagik-dev/genie@v<ver> --all --copy`) + CLI, nothing else — Codex/Claude/Kimi/Hermes/pi integrations, hooks, role agents, council stamp and `agent-sync` deleted; 4-platform Codex dogfood matrix → skills-install + update-path smokes diff --git a/.genie/wishes/dsh-genie-board/WISH.md b/.genie/wishes/dsh-genie-board/WISH.md index fc915407a..2304d82ea 100644 --- a/.genie/wishes/dsh-genie-board/WISH.md +++ b/.genie/wishes/dsh-genie-board/WISH.md @@ -2,7 +2,7 @@ | Field | Value | |-------|-------| -| **Status** | DRAFT | +| **Status** | FIX-FIRST | | **Slug** | `dsh-genie-board` | | **Date** | 2026-09-03 | | **Author** | Sofia with Felipe | @@ -243,7 +243,21 @@ The final command is intentionally post-publication: `v$VERSION` must be the con ## Review Results -_The read-only reviewer returns evidence; the invoking orchestrator appends a timestamped block here._ +### Plan review round 2 — FIX-FIRST (2026-09-03T14:28:27Z) + +- **Reviewed commit:** `a886373a1cca05578ffd3c5ff503e89801f4ada9` +- **Reviewer:** `agent:steve:dashboard:8dc1921d-9f6b-41d0-9095-8ac7ee731afb` +- **Mode:** independent, read-only, detached snapshot +- **Validation:** `wishes:lint`, design-evidence verification, diff check, shell syntax, CLI/source contract checks all passed; snapshot remained clean. +- **Verdict:** **FIX-FIRST** — 0 CRITICAL, 3 HIGH. + +Remaining HIGH gaps after the second review round: + +1. **Detail hydration is contradictory.** The plan promises unconditional complete detail while also allowing partial/on-demand enrichment under a 20-process cap. Choose either one aggregate complete JSON read or a fully specified partial/on-demand contract, including deterministic ordering and mutation-budget accounting. +2. **Compatibility-floor sequencing is circular.** Group 2 depends on a “first released version” that Group 3 has not released. Define a candidate/version-stamping contract produced by the same release, then prove that exact version in Group 3, or add and reconcile an explicit earlier release gate. +3. **Published-release proof is incomplete.** The tar membership check can pass with only one required member; the verifier does not itself require the four named artifacts or inspect plugin contents, and no executable step creates or identifies the signed candidate and sidecars. Specify the authorized candidate workflow and independently assert every required member in each exact artifact before publication. + +Fix-loop budget is exhausted (`2/2`). Cause: `ambiguous-spec` for the hydration contract and `missing-context` for the release-candidate workflow. Owner: Sofia/Felipe. Next gate: resolve those product/release decisions, amend the plan, and obtain a fresh independent plan review. Implementation, release work, and external publication remain blocked. --- From a5a440d503faeffbc909cabc53d12363d3633f9d Mon Sep 17 00:00:00 2001 From: Sofia <genie@namastex.ai> Date: Thu, 3 Sep 2026 19:45:24 +0000 Subject: [PATCH 06/24] docs(wish): resolve dsh board plan gate Co-authored-by: Felipe Rosa <felipe@namastex.ai> --- .genie/INDEX.md | 2 +- .genie/wishes/dsh-genie-board/WISH.md | 121 +++++++++++++++----------- 2 files changed, 71 insertions(+), 52 deletions(-) diff --git a/.genie/INDEX.md b/.genie/INDEX.md index d9857f119..00bd31fe6 100644 --- a/.genie/INDEX.md +++ b/.genie/INDEX.md @@ -17,7 +17,7 @@ - [intent-to-wish-compiler](brainstorms/intent-to-wish-compiler/DRAFT.md) — **LIVE (re-verified 2026-07-21):** WRS 92; Demand→Patch/Standard/Program router + circuit-breaker (flex cuts autonomous, payout cuts human-only); invisible routing compiled from intent — program-scale, splits at pour time - [brainstorm-domain-map](brainstorms/brainstorm-domain-map/DRAFT.md) — **LIVE (re-verified 2026-07-21):** WRS 80; executable spec compiler (intent → requirement-ID → oracle-class → execution → proof-packet); deterministic gates, residual-risk review only; subjective-truth ownership still open (umbrella G8) ## Ready -- [dsh-genie-board — DESIGN](brainstorms/dsh-genie-board/DESIGN.md) → [WISH](wishes/dsh-genie-board/WISH.md) — **FIX-FIRST** (plan loop 2/2 exhausted): three HIGH gaps remain in detail-hydration completeness, compatibility-floor sequencing, and exact four-artifact published-release proof; implementation, release, and publication blocked pending Sofia/Felipe decisions and a fresh independent plan review +- [dsh-genie-board — DESIGN](brainstorms/dsh-genie-board/DESIGN.md) → [WISH](wishes/dsh-genie-board/WISH.md) — **FIX-FIRST, AMENDED 2026-09-03** after direct Felipe approval: complete aggregate reads, immutable pre-build candidate identity/stamping, and human-approved fail-closed proof for exactly four platform artifacts are now specified; implementation, release, and publication remain blocked pending a fresh independent plan `SHIP` - [skills-everywhere-b — WISH](wishes/skills-everywhere-b/WISH.md) · [skills-everywhere-c — WISH](wishes/skills-everywhere-c/WISH.md) — **both APPROVED 2026-08-31** (B: plan SHIP round 3 `e86d3f12…` after two FIX-FIRST loops — the ~20k-line subtractive wish, 7 groups, wave-0 gates: Wish A stable + rebase ≥ 1b34d7d4b; C: plan SHIP round 2 `8e5611226…` — skills-lint tokens, CLAUDE.md/AGENTS.md/docs rewrite, depends-on B). Wish A flipped SHIPPED with the @ref-pin correction (skills@1.5.23 serves the default branch for any ref). - [skills-everywhere — DESIGN](brainstorms/skills-everywhere/DESIGN.md) · [WISH A](wishes/skills-everywhere/WISH.md) — **design SHIP 2026-08-30 (rev. 6, digest `fafaef24…`, re-stamped after rename from `codex-skill-installer`); Wish A SHIPPED 2026-08-31 (all 7 groups; @ref-pin correction recorded) — umbrella A→B→C, base `dev`:** everywhere except Orca, Genie = skills (skills.sh `npx skills add automagik-dev/genie@v<ver> --all --copy`) + CLI, nothing else — Codex/Claude/Kimi/Hermes/pi integrations, hooks, role agents, council stamp and `agent-sync` deleted; 4-platform Codex dogfood matrix → skills-install + update-path smokes diff --git a/.genie/wishes/dsh-genie-board/WISH.md b/.genie/wishes/dsh-genie-board/WISH.md index 2304d82ea..e8710e6d2 100644 --- a/.genie/wishes/dsh-genie-board/WISH.md +++ b/.genie/wishes/dsh-genie-board/WISH.md @@ -36,14 +36,15 @@ Ship a dual-face DSH Web plugin that displays and operates the authoritative Gen | 1 | Co-locate the package in public Genie. | Adapter and CLI compatibility ship together without exposing the private DSH monorepo. | | 2 | Use only current CLI commands and validated JSON. | SQLite is private and former protocol surfaces are retired. | | 3 | Resolve DSH workspace ids on the Host and map a strict action union to argv. | Browser input never becomes a path, executable, raw command, or arbitrary argv. | -| 4 | Use board JSON as the canonical lane snapshot and unconditionally enrich rendered cards with `genie task status <id> --json`. | Assignment, heartbeat/liveness, enforced blocks, dependencies, timeline, and comments are all machine-readable; human output is never a compatibility contract. | -| 5 | Add and freeze the exact status JSON contract in Group 1, then enforce its first released Genie version in the plugin manifest and at runtime. | Compatibility fails closed before a board command when the installed Genie is too old; documentation is not the enforcement boundary. | -| 6 | Refresh the selected board after every mutation. | The browser never displays optimistic state. | +| 4 | Make `genie board --board <ref> --json` the one complete aggregate read for lanes, cards, assignment, liveness, blocks, dependencies, timeline, and comments. | Every board response is complete and deterministic; there is no per-card, partial, or on-demand hydration path. | +| 5 | Derive one immutable candidate version before release build/sign/publish, then stamp the staged plugin version and minimum Genie version from that same candidate. | Group 2 implements and tests the comparator without depending on an already-published release; every shipped artifact binds compatibility to its own candidate. | +| 6 | Refresh the selected board with the complete aggregate after every mutation. | The browser never displays optimistic or partially hydrated state. | +| 7 | Use the authorized stable Release workflow and its protected human approval, then fail closed unless exactly four platform tarballs individually pass digest, signature, provenance, version, and plugin-member verification. | Publication and the GitHub topic cannot outrun release identity or artifact proof. | ## Simplicity Case - **Simplest complete design:** one dual-face plugin, one strict Host adapter, and the existing Genie CLI as sole authority. -- **Added machinery:** route fencing, action validation, process/output caps, and optional structured status enrichment are required by browser-triggered mutations and requested card detail. +- **Added machinery:** route fencing, action validation, process/output caps, and one complete aggregate schema are required by browser-triggered mutations and requested card detail. - **Deferred until measured:** polling, SSE, caches, deltas, batching, autonomous execution, cron, and resumable requests require explicit demand or measured refresh latency. - **Complexity removed:** no durable state, daemon, socket, synchronization protocol, human-output parser, duplicate ledger, or configurable command surface. @@ -54,7 +55,7 @@ Ship a dual-face DSH Web plugin that displays and operates the authoritative Gen ## Success Criteria -- [ ] DSH Web selects an eligible workspace/board and renders lanes/cards matching validated CLI JSON, including structured assignment, block, liveness, and comment data. +- [ ] DSH Web selects an eligible workspace/board and renders lanes/cards from one complete aggregate CLI read, including structured assignment, block, liveness, dependency, timeline, and comment data. - [ ] All supported mutations map to fixed Genie argv and return a fresh confirmed snapshot. - [ ] Unsafe workspace/action/input, timeout, aggregate limit, malformed JSON, missing executable, and non-zero exit fail visibly without out-of-scope mutation. - [ ] No shell, browser-provided path/executable/argv, direct SQLite access, human-output parser, plugin ledger, watcher, or daemon exists. @@ -83,30 +84,30 @@ Ship a dual-face DSH Web plugin that displays and operates the authoritative Gen ## Execution Groups -### Group 1: Structured CLI contract +### Group 1: Complete aggregate CLI contract -**Goal:** Freeze the smallest stable JSON surface the plugin needs without parsing human output. +**Goal:** Freeze one complete, deterministic board JSON read without parsing human output or hydrating cards separately. **Deliverables:** -1. Additive, unconditional `genie task status <id> --json` support with success, unknown-id, stderr/exit-code, exact-key, nullability, and idempotent-read tests. Human `task status` output remains unchanged. -2. Freeze this versioned response and reject additional/missing keys in fixtures: +1. Extend `genie board --board <ref> --json` additively so one invocation returns `schemaVersion: 1`, board scope, ordered lanes, and every card's complete detail. Keep human board/task output unchanged and retain the exact `genie board list --json` contract (`id`, `name`, `laneCount`, `cardCount`). +2. Freeze this exact per-card aggregate shape and reject additional/missing keys and invalid nullability in fixtures: ```ts { - schemaVersion: 1, - task: { - id: string, boardId: string | null, title: string, - status: 'blocked' | 'ready' | 'in_progress' | 'done', - claimedBy: string | null, claimedAt: number | null, - wish: string | null, group: string | null, - assignedAgent: string | null, assignedReason: string | null, - createdAt: number, updatedAt: number, lane: string | null, - agentKind: string | null, heartbeatAt: number | null, - liveness: 'running' | 'idle' | 'stale' | null, - blockedBy: string | null, blockedReason: string | null, - enforcedBlock: { reason: string, kind: 'work' | 'hold' } | null - }, - dependencies: Array<{ id: string, title: string, status: 'blocked' | 'ready' | 'in_progress' | 'done' }>, + id: string, boardId: string | null, title: string, + status: 'blocked' | 'ready' | 'in_progress' | 'done', + claimedBy: string | null, claimedAt: number | null, + wish: string | null, group: string | null, + assignedAgent: string | null, assignedReason: string | null, + createdAt: number, updatedAt: number, lane: string | null, + agentKind: string | null, heartbeatAt: number | null, + liveness: 'running' | 'idle' | 'stale' | null, + blockedBy: string | null, blockedReason: string | null, + enforcedBlock: { reason: string, kind: 'work' | 'hold' } | null, + dependencies: Array<{ + id: string, title: string, + status: 'blocked' | 'ready' | 'in_progress' | 'done' + }>, timeline: Array<{ id: number, kind: string, note: string | null, authorKind: string | null, author: string | null, createdAt: number @@ -118,19 +119,19 @@ Ship a dual-face DSH Web plugin that displays and operates the authoritative Gen } ``` - `liveness` is null when `claimedBy` is null and otherwise is derived from `heartbeatAt`; `comments` is the ordered `kind === 'comment'` projection of `timeline` with non-null comment text. -3. Retain exact fixtures for `genie board list --json` (`id`, `name`, `laneCount`, `cardCount`) and lane-board JSON (`scope`; ordered lanes with `name`, `label`, `action`, and exact card keys). Record the first Genie release containing status schema version 1 as the compatibility floor consumed by Group 2. + Lanes keep their existing order; cards keep the board's existing order; dependencies sort by task id; timeline sorts by `createdAt` then `id`; comments are the ordered `kind === 'comment'` projection of that timeline with non-null text. `liveness` is null when `claimedBy` is null and otherwise derives from `heartbeatAt`. +3. Fetch and join the aggregate in one repository read transaction/query path so every card belongs to the same snapshot. Missing or malformed detail fails the entire command non-zero; the JSON contract has no `partial`, `truncated`, cursor, hydration, or on-demand state. Add success, unknown-board, empty-board, multi-card ordering, stderr/exit-code, exact-key, nullability, and idempotent-read tests. **Acceptance Criteria:** -- [ ] `genie task status <id> --json` is always implemented and returns the exact schema above, including heartbeat/liveness, assignment, enforced block/provenance, dependencies, timeline, and structured comments. -- [ ] No human CLI output is parsed; every consumed board/status JSON key has an exact fixture/schema assertion. +- [ ] One `genie board --board <ref> --json` invocation returns every rendered card and all required assignment, block, liveness, dependency, timeline, and comment data from one complete snapshot. +- [ ] No human CLI output or per-card `task status` call is consumed; every aggregate key, ordering rule, and fail-closed case has an exact fixture/schema assertion. **Validation:** ```bash -bun test src/term-commands/v5-board.test.ts src/term-commands/v5-task.test.ts && bun run check +bun test src/term-commands/v5-board.test.ts && bun run check ``` -Full gate is required because this changes a shared CLI contract. +Full gate is required because this changes a shared aggregate CLI contract. **depends-on:** none **blocks:** Group 2 @@ -142,8 +143,8 @@ Full gate is required because this changes a shared CLI contract. **Goal:** Deliver the secure, Host-confirmed DSH Web board experience. **Deliverables:** -1. Create `plugins/dsh-genie-board/package.json`, `agent.cordis.yml`, `cordis.patch.yml`, TypeScript/build configuration, source/tests, and the package-local `build` script; add root `build:plugin` as `bun --cwd plugins/dsh-genie-board run build`. -2. Put the exact first compatible Genie release from Group 1 in the plugin manifest/package metadata and enforce the same floor at Host startup by running the fixed executable as `genie --no-interactive --version`; an older/unparseable version serves no board route. +1. Create `plugins/dsh-genie-board/package.json`, `agent.cordis.yml`, `cordis.patch.yml`, `README.md`, `NOTICE`, TypeScript/build configuration, source/tests, and the package-local `build` script. Freeze `dist/index.js` as the Host bundle and `dist/client.js` as the browser bundle; add root `build:plugin` as `bun --cwd plugins/dsh-genie-board run build`. +2. Implement a `minimumGenieVersion` plugin field and strict semver comparator without hard-coding a not-yet-published release. Source and linked-profile tests use the checkout root version; Group 3 stamps both the shipped plugin version and `minimumGenieVersion` from its already-derived immutable candidate. At Host startup run the fixed executable as `genie --no-interactive --version`; an older/unparseable version serves no board route. 3. Resolve a workspace id only through DSH `workspaceRegistry`; canonicalize the registry result with `realpath`, require a physical repository containing `.genie`, and use that canonical path as `cwd`. Never accept a browser path. Resolve the Genie executable once from the Host-owned installation, canonicalize it to an absolute executable regular file, and never search for or override it per request. 4. Spawn with `shell: false` and an exact Host-owned environment allowlist: `PATH`, `HOME`, `GENIE_HOME`, `NO_COLOR=1`, `GENIE_AGENT_NAME=<host-derived-identity>`, and `GENIE_AGENT_KIND=dsh`; drop every other variable and accept no environment value from the browser. 5. Implement this normative action table; every argv vector includes `--no-interactive` and no action may synthesize another vector: @@ -152,7 +153,6 @@ Full gate is required because this changes a shared CLI contract. |--------|-----------------------| | List boards | `genie --no-interactive board list --json` | | Read board | `genie --no-interactive board --board <validated-ref> --json` | - | Read card detail | `genie --no-interactive task status <id> --json` | | Create | `genie --no-interactive task create --title <title> --board <ref>` | | Move | `genie --no-interactive task move <id> --to <lane>` | | Comment | `genie --no-interactive task comment <id> <text>` | @@ -163,14 +163,14 @@ Full gate is required because this changes a shared CLI contract. | Done | `genie --no-interactive task done <id>` | 6. Validate `workspaceId` by exact registry membership; accept `boardRef` only when it equals an id returned by validated board-list JSON; require task ids matching `^t_[a-z0-9]+$`; require lane to equal a lane name from the selected validated board; trim and bound title to 1–200 UTF-8 bytes, comment text to 1–4000, and block reason to 1–1000; reject NUL/control characters, unknown object keys, non-boolean `hold`, and all browser-supplied worker/path/executable/environment/command/argv fields. -7. Bound each request to at most 20 Genie processes total, concurrency 4, a 10-second aggregate deadline, and 4 MiB aggregate stdout plus stderr. Kill remaining children on the first timeout/limit/error; validate exit code and closed JSON schemas before use; return an explicit partial-detail warning when the process cap leaves cards unenriched. Test every bound deterministically, including hostile identifiers and command-injection attempts. -8. Implement browser selectors, lanes/cards/details/errors, mutations, refresh, and visibility recovery; each mutation response re-reads the board and status JSON and never applies optimistic state. +7. Use exactly one Genie process for board list/load and at most two sequential processes for a mutation plus its complete aggregate refresh, with a 10-second aggregate deadline and 4 MiB aggregate stdout plus stderr. Kill the active child on timeout/limit/error; validate exit code and the closed aggregate schema before use; any missing/oversized/malformed detail fails the whole response. Test every bound deterministically, including hostile identifiers and command-injection attempts. +8. Implement browser selectors, lanes/cards/details/errors, mutations, refresh, and visibility recovery; each mutation response performs one complete board aggregate re-read and never applies optimistic or partially hydrated state. 9. Add `scripts/dsh-genie-board-smoke.ts`: create isolated fixture repo/profile state, run `dsh plugin --profile web add link:<absolute-plugin-dir>`, launch `dsh web --no-open --host 127.0.0.1 --port 0`, stop/relaunch it after install, prove `dsh plugin --profile web list --depth 0` reports `@automagik/genie-dsh-board`, read back the plugin health/compatibility route, perform board list/load plus reversible create/move through the Host route, and in `finally` stop the server, run `dsh plugin --profile web remove @automagik/genie-dsh-board`, and delete only the temporary profile/repository. **Acceptance Criteria:** -- [ ] Rendering matches fixture-backed Host snapshots; mutations use fixed argv and refresh. +- [ ] Rendering matches one complete fixture-backed Host aggregate; mutations use fixed argv and perform one complete refresh. - [ ] Every unsafe/failure case is bounded and cannot invoke an out-of-scope command. -- [ ] The manifest and runtime reject every Genie version below the first release containing status schema version 1. +- [ ] The manifest and runtime reject every Genie version below the immutable candidate value stamped by Group 3, while source/linked tests prove the comparator against the checkout version without depending on a prior publication. - [ ] Package build and the linked-profile install/restart/read-back/board-operation/cleanup smoke pass against DSH `0.1.1-rc.2` or a newer explicitly proven floor. - [ ] No database access, persistence, watcher, poller, shell, or browser-supplied executable/path/argv exists. @@ -186,19 +186,30 @@ Full gate plus plugin build covers runtime and trust-boundary risk. --- -### Group 3: Install, release, and discoverability +### Group 3: Immutable candidate, release proof, and discoverability -**Goal:** Prove the packaged plugin can be installed locally before advertising it. +**Goal:** Publish only a human-approved stable candidate whose four platform artifacts prove the complete plugin payload. **Deliverables:** -1. Install/compatibility docs and provenance/NOTICE with the manifest-enforced Genie floor from Group 2. -2. Include `plugins/dsh-genie-board/` in `scripts/build-binary.sh`; extend `scripts/release-payload-version.ts` so package/manifest versions are stamped and verified in staged and extracted payloads; update `scripts/release-payload-version.test.ts`, `scripts/release-docs.test.ts`, `scripts/version-format.test.ts`, `scripts/version-ci-staging.test.ts`, and payload inventory/parity tests. Do not replace or modify the existing `scripts/verify-release.sh` interface. -3. From a clean checkout, build and inspect `linux-x64-glibc`, `linux-x64-musl`, `linux-arm64`, and `darwin-arm64`; each tarball must contain the built plugin, manifest/patch, docs/NOTICE, and matching root/plugin versions. -4. After the candidate has a signed tag and all four tarballs plus `.bundle` and `.intoto.jsonl` sidecars, run the existing verifier against that concrete tag. Only after all checks pass, add the GitHub topic and verify it by read-back. +1. Add install/compatibility docs and provenance/NOTICE. Source and linked builds use the checkout root version; no source file guesses a future release number. +2. Extend `scripts/release-payload-version.ts` and its tests so the release workflow's already-resolved `VERSION` stamps and verifies all version-bearing staged and extracted members: root `VERSION`, existing Genie manifests, `plugins/dsh-genie-board/package.json.version`, and the DSH manifest's plugin version and `minimumGenieVersion`. Stamping happens before tarball creation, and any missing/divergent field fails the build. +3. Preserve the repository's authorized release identity sequence: + - `.github/workflows/version.yml` derives a single candidate `VERSION`, binds it to an immutable tag/source SHA and successful source CI before any release build, and never reuses that identity; + - the final stable release is started by a maintainer through `.github/workflows/release.yml` with that exact version/tag SHA/CI run; + - the protected `production` environment approval must succeed before `authorize`, build, sign/attest, or publish can run. + The same candidate value flows unchanged through build, signature, provenance, release asset names, plugin version, and `minimumGenieVersion`. +4. Add `scripts/verify-dsh-genie-board-release.ts` plus tests with two explicit modes: `--unsigned-artifact-dir` proves local tar inventory/member/version completeness, while `--signed-artifact-dir` and `--release` additionally require cryptographic sidecars and digest binding. Wire signed-artifact mode into `.github/workflows/release-publish.yml` after signed artifacts are downloaded but before draft reconciliation/publication. For the supplied candidate and channel, signed-artifact/release mode must fail closed unless: + - the tarball stem set is exactly `linux-x64-glibc`, `linux-x64-musl`, `linux-arm64`, and `darwin-arm64`, with one nonempty `.bundle` and `.intoto.jsonl` beside each; + - each tarball's recomputed SHA-256 equals its channel delivery descriptor's `artifactSha256`; + - `scripts/verify-release.sh --local <tarball>` passes independently for each tarball, proving its cosign identity and SLSA provenance; + - each extracted tarball contains every required plugin member: `package.json`, `agent.cordis.yml`, `cordis.patch.yml`, `README.md`, `NOTICE`, `dist/index.js`, and `dist/client.js`; + - each extracted root/plugin/manifest version and `minimumGenieVersion` equals the immutable candidate exactly. +5. After the stable release is published, run the same verifier in release-download mode against `v$VERSION` and read back the release tag/source binding. Only that green post-publication proof permits adding the `dsh-plugin` GitHub topic; read the topic back afterward. A dev release, local build, unsigned tarball, missing platform, OR-style member check, or approval from the release initiator does not satisfy this gate. **Acceptance Criteria:** -- [ ] Artifact contains the plugin/manifests/docs with matching versions. -- [ ] All four supported artifacts contain the plugin and pass inventory/version inspection; the linked smoke from Group 2 changes no user repository. +- [ ] Candidate version/tag/source/CI identity exists before build and flows unchanged through all four tarballs, plugin metadata, signatures, provenance, descriptors, and the published stable release. +- [ ] The protected human approval precedes build/sign/publish, and the pre-publication verifier rejects any missing/extra platform stem, digest mismatch, missing/invalid sidecar, missing required plugin member, or version mismatch. +- [ ] All four exact published tarballs independently pass SHA-256, cosign, SLSA, complete-member, and version/floor checks; the linked smoke from Group 2 changes no user repository. - [ ] Topic publication occurs last and is verified by read-back. **Validation:** @@ -209,15 +220,16 @@ bun run build:plugin VERSION="$(jq -r .version package.json)" for PLATFORM in linux-x64-glibc linux-x64-musl linux-arm64 darwin-arm64; do bun run build:binary -- --platform "$PLATFORM" --version "$VERSION" - tar -tzf "dist/genie-$VERSION-$PLATFORM.tar.gz" | grep -E '^\./plugins/dsh-genie-board/(package.json|agent.cordis.yml|cordis.patch.yml|dist/)' done -bun run verify:release -- "v$VERSION" +bun scripts/verify-dsh-genie-board-release.ts --unsigned-artifact-dir dist --version "$VERSION" +# Final gate after the separately approved stable Release workflow publishes: +bun scripts/verify-dsh-genie-board-release.ts --release "v$VERSION" --channel stable ``` -The final command is intentionally post-publication: `v$VERSION` must be the concrete signed candidate tag. The live verifier downloads every platform artifact and requires each adjacent `.bundle` and `.intoto.jsonl`; a merely local unsigned build does not satisfy this gate. +The unsigned verifier mode proves only locally built inventory and member/version completeness and cannot authorize publication. Signed-artifact mode is mandatory inside the approved release workflow; the final release-download run is mandatory after publication and proves exactly four published platform tarballs individually against their digest, signature, provenance, complete plugin inventory, and immutable candidate identity before topic publication. **depends-on:** Group 2 -**blocks:** release/topic publication +**blocks:** stable release/topic publication --- @@ -235,7 +247,7 @@ The final command is intentionally post-publication: `v$VERSION` must be the con | Risk | Severity | Mitigation | |------|----------|------------| | DSH APIs drift from installed `0.1.1-rc.2`. | Medium | Use locally proven injections, linked smoke, and tested engine floor. | -| Per-card detail creates subprocess pressure. | Medium | Hard cap count/concurrency/bytes/time and expose partial-detail warnings. | +| Complete aggregate detail increases one response's size. | Medium | Bound one snapshot by bytes/time and fail the whole response rather than expose partial state. | | Additive JSON becomes a public contract. | Medium | Fixture every key and pin plugin compatibility floor. | | Release payload omits co-located files. | High | Update manifests and verify final tarballs from a clean checkout. | @@ -259,15 +271,21 @@ Remaining HIGH gaps after the second review round: Fix-loop budget is exhausted (`2/2`). Cause: `ambiguous-spec` for the hydration contract and `missing-context` for the release-candidate workflow. Owner: Sofia/Felipe. Next gate: resolve those product/release decisions, amend the plan, and obtain a fresh independent plan review. Implementation, release work, and external publication remain blocked. +### Decision resolution — direct Felipe approval (2026-09-03) + +Felipe directly authorized the bounded plan amendment: complete aggregate views from one structured read; one immutable candidate version derived before build/sign/publish; and the authorized, human-approved stable workflow with fail-closed proof of exactly four platform artifacts and every required plugin member. The amended plan removes partial hydration, makes candidate stamping non-circular, and adds per-artifact digest/signature/provenance/member verification. Status remains `FIX-FIRST` until a fresh independent plan review returns `SHIP`; no implementation, release, push, or topic publication is authorized by this amendment. + --- ## Files to Create/Modify ``` plugins/dsh-genie-board/** -src/term-commands/v5-task.ts -src/term-commands/v5-task.test.ts +src/term-commands/v5-board.ts +src/term-commands/v5-board.test.ts scripts/dsh-genie-board-smoke.ts +scripts/verify-dsh-genie-board-release.ts +scripts/verify-dsh-genie-board-release.test.ts scripts/build-binary.sh scripts/release-payload-version.ts scripts/release-payload-version.test.ts @@ -275,6 +293,7 @@ scripts/release-docs.test.ts scripts/version-format.test.ts scripts/version-ci-staging.test.ts scripts/orca-manifest-parity.test.ts +.github/workflows/release-publish.yml package.json README.md .genie/brainstorms/dsh-genie-board/** From 9a852e3d22b549a6ca701a5a9057f52204d505e0 Mon Sep 17 00:00:00 2001 From: Sofia <genie@namastex.ai> Date: Thu, 3 Sep 2026 19:49:33 +0000 Subject: [PATCH 07/24] docs(wish): approve dsh board plan Co-authored-by: Felipe Rosa <felipe@namastex.ai> --- .genie/INDEX.md | 2 +- .genie/wishes/dsh-genie-board/WISH.md | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.genie/INDEX.md b/.genie/INDEX.md index 00bd31fe6..cd9ffdecb 100644 --- a/.genie/INDEX.md +++ b/.genie/INDEX.md @@ -17,7 +17,7 @@ - [intent-to-wish-compiler](brainstorms/intent-to-wish-compiler/DRAFT.md) — **LIVE (re-verified 2026-07-21):** WRS 92; Demand→Patch/Standard/Program router + circuit-breaker (flex cuts autonomous, payout cuts human-only); invisible routing compiled from intent — program-scale, splits at pour time - [brainstorm-domain-map](brainstorms/brainstorm-domain-map/DRAFT.md) — **LIVE (re-verified 2026-07-21):** WRS 80; executable spec compiler (intent → requirement-ID → oracle-class → execution → proof-packet); deterministic gates, residual-risk review only; subjective-truth ownership still open (umbrella G8) ## Ready -- [dsh-genie-board — DESIGN](brainstorms/dsh-genie-board/DESIGN.md) → [WISH](wishes/dsh-genie-board/WISH.md) — **FIX-FIRST, AMENDED 2026-09-03** after direct Felipe approval: complete aggregate reads, immutable pre-build candidate identity/stamping, and human-approved fail-closed proof for exactly four platform artifacts are now specified; implementation, release, and publication remain blocked pending a fresh independent plan `SHIP` +- [dsh-genie-board — DESIGN](brainstorms/dsh-genie-board/DESIGN.md) → [WISH](wishes/dsh-genie-board/WISH.md) — **APPROVED 2026-09-03** at `9c5ba2714` after direct Felipe amendment and independent cross-family GLM 5.3 plan review `SHIP` (0 CRITICAL/HIGH): complete aggregate reads, immutable pre-build candidate identity/stamping, and human-approved fail-closed proof for exactly four platform artifacts; implementation, stable release, and topic publication remain separately gated - [skills-everywhere-b — WISH](wishes/skills-everywhere-b/WISH.md) · [skills-everywhere-c — WISH](wishes/skills-everywhere-c/WISH.md) — **both APPROVED 2026-08-31** (B: plan SHIP round 3 `e86d3f12…` after two FIX-FIRST loops — the ~20k-line subtractive wish, 7 groups, wave-0 gates: Wish A stable + rebase ≥ 1b34d7d4b; C: plan SHIP round 2 `8e5611226…` — skills-lint tokens, CLAUDE.md/AGENTS.md/docs rewrite, depends-on B). Wish A flipped SHIPPED with the @ref-pin correction (skills@1.5.23 serves the default branch for any ref). - [skills-everywhere — DESIGN](brainstorms/skills-everywhere/DESIGN.md) · [WISH A](wishes/skills-everywhere/WISH.md) — **design SHIP 2026-08-30 (rev. 6, digest `fafaef24…`, re-stamped after rename from `codex-skill-installer`); Wish A SHIPPED 2026-08-31 (all 7 groups; @ref-pin correction recorded) — umbrella A→B→C, base `dev`:** everywhere except Orca, Genie = skills (skills.sh `npx skills add automagik-dev/genie@v<ver> --all --copy`) + CLI, nothing else — Codex/Claude/Kimi/Hermes/pi integrations, hooks, role agents, council stamp and `agent-sync` deleted; 4-platform Codex dogfood matrix → skills-install + update-path smokes diff --git a/.genie/wishes/dsh-genie-board/WISH.md b/.genie/wishes/dsh-genie-board/WISH.md index e8710e6d2..b2c0ecb97 100644 --- a/.genie/wishes/dsh-genie-board/WISH.md +++ b/.genie/wishes/dsh-genie-board/WISH.md @@ -2,7 +2,7 @@ | Field | Value | |-------|-------| -| **Status** | FIX-FIRST | +| **Status** | APPROVED | | **Slug** | `dsh-genie-board` | | **Date** | 2026-09-03 | | **Author** | Sofia with Felipe | @@ -273,7 +273,22 @@ Fix-loop budget is exhausted (`2/2`). Cause: `ambiguous-spec` for the hydration ### Decision resolution — direct Felipe approval (2026-09-03) -Felipe directly authorized the bounded plan amendment: complete aggregate views from one structured read; one immutable candidate version derived before build/sign/publish; and the authorized, human-approved stable workflow with fail-closed proof of exactly four platform artifacts and every required plugin member. The amended plan removes partial hydration, makes candidate stamping non-circular, and adds per-artifact digest/signature/provenance/member verification. Status remains `FIX-FIRST` until a fresh independent plan review returns `SHIP`; no implementation, release, push, or topic publication is authorized by this amendment. +Felipe directly authorized the bounded plan amendment: complete aggregate views from one structured read; one immutable candidate version derived before build/sign/publish; and the authorized, human-approved stable workflow with fail-closed proof of exactly four platform artifacts and every required plugin member. The amended plan removes partial hydration, makes candidate stamping non-circular, and adds per-artifact digest/signature/provenance/member verification. No implementation, release, push, or topic publication was authorized by this amendment. + +### Plan review round 3 — SHIP (2026-09-03T19:48:26Z) + +- **Reviewed commit:** `9c5ba2714c52be97ad1742d7f2f3d1bd6c65a0c0` +- **Reviewer:** Steve, `juice/GLM-5.3` (full non-Flash GLM family; cross-family from Sofia/OpenAI GPT) +- **Mode:** independent, read-only, detached snapshot; runtime exposed no separate reasoning control, so maximum deliberation was required in the brief +- **Validation:** exact HEAD and detached state confirmed; `git status --porcelain` empty before/after; `wishes:lint` passed (86 files); amendment diff and live board/release interfaces inspected +- **Verdict:** **SHIP** — 0 CRITICAL, 0 HIGH; all three prior blockers closed + +Closure evidence: +1. Complete aggregate board detail is one deterministic read with no partial/on-demand hydration and whole-response failure on missing detail. +2. Candidate identity is derived and bound before build; Group 2 tests the comparator without a published-version dependency; Group 3 stamps the same candidate into every shipped compatibility/version field. +3. The protected stable approval precedes build/sign/publish; signed and post-publication verification require exactly four platform stems, per-artifact SHA-256/cosign/SLSA proof, explicit plugin members, and exact candidate versions. + +Non-blocking review notes: document that `minimumGenieVersion` intentionally equals the co-shipped plugin release, and record the exact DSH binary path/version in smoke output. Plan status advances to `APPROVED`; implementation and every release/publication gate remain separately authorized. --- From fef77105405991b2f316626b664abdbcdeb7bd08 Mon Sep 17 00:00:00 2001 From: Sofia <genie@namastex.ai> Date: Thu, 3 Sep 2026 20:28:25 +0000 Subject: [PATCH 08/24] docs(wish): start dsh board execution --- .genie/INDEX.md | 2 +- .genie/wishes/dsh-genie-board/WISH.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.genie/INDEX.md b/.genie/INDEX.md index cd9ffdecb..5b585c3a9 100644 --- a/.genie/INDEX.md +++ b/.genie/INDEX.md @@ -17,7 +17,7 @@ - [intent-to-wish-compiler](brainstorms/intent-to-wish-compiler/DRAFT.md) — **LIVE (re-verified 2026-07-21):** WRS 92; Demand→Patch/Standard/Program router + circuit-breaker (flex cuts autonomous, payout cuts human-only); invisible routing compiled from intent — program-scale, splits at pour time - [brainstorm-domain-map](brainstorms/brainstorm-domain-map/DRAFT.md) — **LIVE (re-verified 2026-07-21):** WRS 80; executable spec compiler (intent → requirement-ID → oracle-class → execution → proof-packet); deterministic gates, residual-risk review only; subjective-truth ownership still open (umbrella G8) ## Ready -- [dsh-genie-board — DESIGN](brainstorms/dsh-genie-board/DESIGN.md) → [WISH](wishes/dsh-genie-board/WISH.md) — **APPROVED 2026-09-03** at `9c5ba2714` after direct Felipe amendment and independent cross-family GLM 5.3 plan review `SHIP` (0 CRITICAL/HIGH): complete aggregate reads, immutable pre-build candidate identity/stamping, and human-approved fail-closed proof for exactly four platform artifacts; implementation, stable release, and topic publication remain separately gated +- [dsh-genie-board — DESIGN](brainstorms/dsh-genie-board/DESIGN.md) → [WISH](wishes/dsh-genie-board/WISH.md) — **IN PROGRESS 2026-09-03** from approved plan `9c5ba2714` after direct Felipe implementation authorization; Group 1 is the sole active wave under WIP=1, while merge, stable release, deploy, topic publication, spend, and credential changes remain separately gated - [skills-everywhere-b — WISH](wishes/skills-everywhere-b/WISH.md) · [skills-everywhere-c — WISH](wishes/skills-everywhere-c/WISH.md) — **both APPROVED 2026-08-31** (B: plan SHIP round 3 `e86d3f12…` after two FIX-FIRST loops — the ~20k-line subtractive wish, 7 groups, wave-0 gates: Wish A stable + rebase ≥ 1b34d7d4b; C: plan SHIP round 2 `8e5611226…` — skills-lint tokens, CLAUDE.md/AGENTS.md/docs rewrite, depends-on B). Wish A flipped SHIPPED with the @ref-pin correction (skills@1.5.23 serves the default branch for any ref). - [skills-everywhere — DESIGN](brainstorms/skills-everywhere/DESIGN.md) · [WISH A](wishes/skills-everywhere/WISH.md) — **design SHIP 2026-08-30 (rev. 6, digest `fafaef24…`, re-stamped after rename from `codex-skill-installer`); Wish A SHIPPED 2026-08-31 (all 7 groups; @ref-pin correction recorded) — umbrella A→B→C, base `dev`:** everywhere except Orca, Genie = skills (skills.sh `npx skills add automagik-dev/genie@v<ver> --all --copy`) + CLI, nothing else — Codex/Claude/Kimi/Hermes/pi integrations, hooks, role agents, council stamp and `agent-sync` deleted; 4-platform Codex dogfood matrix → skills-install + update-path smokes diff --git a/.genie/wishes/dsh-genie-board/WISH.md b/.genie/wishes/dsh-genie-board/WISH.md index b2c0ecb97..1f838d866 100644 --- a/.genie/wishes/dsh-genie-board/WISH.md +++ b/.genie/wishes/dsh-genie-board/WISH.md @@ -2,7 +2,7 @@ | Field | Value | |-------|-------| -| **Status** | APPROVED | +| **Status** | IN_PROGRESS | | **Slug** | `dsh-genie-board` | | **Date** | 2026-09-03 | | **Author** | Sofia with Felipe | From 4928e398869ed1dbdf86255fda6dd961b61316a7 Mon Sep 17 00:00:00 2001 From: Felipe Rosa <felipe@namastex.ai> Date: Mon, 7 Sep 2026 19:15:02 +0000 Subject: [PATCH 09/24] test: stabilize release and subprocess validation Bound release reconciliation while allowing measured integration runtimes. Prevent PID zero cleanup and restore Bun exit status explicitly. Co-authored-by: Felipe Rosa <felipe@namastex.ai> --- scripts/reconcile-release-assets.test.ts | 40 ++++++++++++--------- src/genie-commands/__tests__/update.test.ts | 4 +-- src/lib/runtime-integrations.test.ts | 9 +++-- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/scripts/reconcile-release-assets.test.ts b/scripts/reconcile-release-assets.test.ts index 698fdc333..e024f4ea0 100644 --- a/scripts/reconcile-release-assets.test.ts +++ b/scripts/reconcile-release-assets.test.ts @@ -7,6 +7,10 @@ import { join } from 'node:path'; const SCRIPT = join(import.meta.dir, 'reconcile-release-assets.sh'); const VERSION = '5.260714.3'; const CHANNEL = 'dev'; +// These fixtures execute the real shell pipeline with 20/28 assets and many +// verifier subprocesses. A dev run takes ~7s locally, beyond Bun's 5s default. +// Bound each pipeline itself, then allow grouped scenarios their own budget. +const PIPELINE_TIMEOUT_MS = 30_000; const PLATFORMS = ['linux-x64-glibc', 'linux-x64-musl', 'linux-arm64', 'darwin-arm64']; function namesFor(channel: 'stable' | 'dev'): string[] { const channels = channel === 'stable' ? ['stable', 'dev'] : ['dev']; @@ -299,6 +303,8 @@ save(); const result = Bun.spawnSync(['bash', SCRIPT], { cwd: root, + timeout: PIPELINE_TIMEOUT_MS, + killSignal: 'SIGKILL', env: { ...process.env, PATH: `${root}:${process.env.PATH ?? ''}`, @@ -337,7 +343,7 @@ describe('exact GitHub release asset reconciliation', () => { // One by-id POST per asset — finer resumption than the old batch upload. expect(uploadCalls(state)).toHaveLength(NAMES.length); expect(state.usedClobber).not.toBe(true); - }); + }, 60_000); test('expands exact descriptor inventory by selected-channel fanout', () => { for (const channel of ['dev', 'stable'] as const) { @@ -351,7 +357,7 @@ describe('exact GitHub release asset reconciliation', () => { // stable = 4 * (3 + 2*2) = 28 expect(namesFor('dev')).toHaveLength(20); expect(namesFor('stable')).toHaveLength(28); - }, 15_000); + }, 120_000); test('never mutates a published prerelease; channel promotions require fresh immutable tags', () => { const devAssets = localAssets('dev-release', 'dev'); @@ -360,7 +366,7 @@ describe('exact GitHub release asset reconciliation', () => { expect(stable.result.stderr.toString()).toContain('published immutable release'); expect(stable.state.assets).toEqual(devAssets); expect(uploadCalls(stable.state)).toHaveLength(0); - }); + }, 60_000); test('rejects missing and extra local inventory before any GitHub mutation', () => { const missing = run({ draft: true, assets: {} }, (dist) => rmSync(join(dist, NAMES[0]))); @@ -370,7 +376,7 @@ describe('exact GitHub release asset reconciliation', () => { const extra = run({ draft: true, assets: {} }, (dist) => writeFileSync(join(dist, 'unexpected'), 'x')); expect(extra.result.exitCode).toBe(3); expect(calls(extra.state, 'gh')).toHaveLength(0); - }); + }, 60_000); test('rejects empty, symlinked, and directory local assets before GitHub mutation', () => { const empty = run({ draft: true, assets: {} }, (dist) => writeFileSync(join(dist, NAMES[0]), '')); @@ -390,7 +396,7 @@ describe('exact GitHub release asset reconciliation', () => { }); expect(symlink.result.exitCode).toBe(3); expect(calls(symlink.state, 'gh')).toHaveLength(0); - }); + }, 60_000); test('resumes authenticated partial drafts and rejects a cryptographically inconsistent mix', () => { const local = localAssets(); @@ -404,14 +410,14 @@ describe('exact GitHub release asset reconciliation', () => { const mismatch = run({ draft: true, assets: { [NAMES[0]]: 'different' } }); expect(mismatch.result.exitCode).toBe(3); expect(uploadCalls(mismatch.state)).toHaveLength(0); - }); + }, 60_000); test('a complete authenticated draft reuses prior nondeterministic bundle bytes', () => { const draft = run({ draft: true, assets: localAssets('older-run') }); expect(draft.result.exitCode).toBe(0); expect(draft.result.stdout.toString()).toContain('preserves its complete authenticated draft inventory'); expect(uploadCalls(draft.state)).toHaveLength(0); - }); + }, 60_000); test('a retry rejects authenticated old descriptors bound to different candidate manifest bytes', () => { const stale = localAssets('older-run'); @@ -424,7 +430,7 @@ describe('exact GitHub release asset reconciliation', () => { const retry = run({ draft: true, assets: stale }); expect(retry.result.exitCode).toBe(3); expect(uploadCalls(retry.state)).toHaveLength(0); - }); + }, 60_000); test('an interrupted draft preserves prior bundle bytes while uploading only missing assets', () => { const current = localAssets(); @@ -441,7 +447,7 @@ describe('exact GitHub release asset reconciliation', () => { expect(resumed.state.assets[priorBundle]).toBe(partial[priorBundle]); expect(Object.keys(resumed.state.assets).sort()).toEqual([...NAMES].sort()); expect(uploadCalls(resumed.state)).toHaveLength(NAMES.length - Object.keys(partial).length); - }); + }, 60_000); test('reuses a complete published inventory only after pinned cryptographic verification', () => { const publishedAssets = localAssets('published'); @@ -465,7 +471,7 @@ describe('exact GitHub release asset reconciliation', () => { 'https://github.com/automagik-dev/genie/.github/workflows/release-publish.yml@refs/heads/main', ); } - }); + }, 60_000); test('selects our release predicate when GitHub also attests the immutable release', () => { const publishedAssets = localAssets('published'); @@ -477,7 +483,7 @@ describe('exact GitHub release asset reconciliation', () => { // answered with GitHub's immutable-release attestation ahead of ours. expect(state.attestationBatchSizes).toHaveLength(8); expect(state.attestationBatchSizes?.every((size) => size === 2)).toBe(true); - }); + }, 60_000); test('never repairs a partial published release or accepts remote extras', () => { const partial = run({ draft: false, prerelease: false, assets: { [NAMES[0]]: localAssets()[NAMES[0]] } }); @@ -489,7 +495,7 @@ describe('exact GitHub release asset reconciliation', () => { expect(extra.result.exitCode).toBe(3); expect(extra.result.stderr.toString()).toContain('unexpected assets'); expect(uploadCalls(extra.state)).toHaveLength(0); - }); + }, 60_000); test('rejects duplicate and malformed remote inventory before upload', () => { const duplicate = run({ @@ -506,7 +512,7 @@ describe('exact GitHub release asset reconciliation', () => { const malformed = run({ draft: true, assets: {}, remoteAssets: [{ name: 7 }] }); expect(malformed.result.exitCode).toBe(3); expect(uploadCalls(malformed.state)).toHaveLength(0); - }); + }, 60_000); test('rides out transient upload failures without duplicating or clobbering assets', () => { // One 502 on the first upload POST; the retry succeeds. Every asset lands @@ -516,7 +522,7 @@ describe('exact GitHub release asset reconciliation', () => { expect(Object.keys(state.assets).sort()).toEqual([...NAMES].sort()); expect(state.usedClobber).not.toBe(true); expect((state.calls ?? []).every((call) => !call.args.includes('DELETE'))).toBe(true); - }); + }, 60_000); test('an upload that landed but lost its response is skipped, and byte verification adjudicates', () => { // The first POST stores the bytes server-side but reports a transient @@ -532,7 +538,7 @@ describe('exact GitHub release asset reconciliation', () => { const corrupt = run({ draft: true, assets: {}, uploadLandThenFail: 1, uploadCorruptFirst: true }); expect(corrupt.result.exitCode).toBe(3); expect(corrupt.result.stderr.toString()).toContain('remote release asset verification failed after upload'); - }, 30_000); + }, 120_000); test('fails closed before any mutation when the release cannot be resolved', () => { const unknown = run({ draft: true, assets: {}, failTimes: { 'releases/tags': 99 } }); @@ -544,7 +550,7 @@ describe('exact GitHub release asset reconciliation', () => { expect(absent.result.exitCode).toBe(3); expect(absent.result.stderr.toString()).toContain('run prepare first'); expect(uploadCalls(absent.state)).toHaveLength(0); - }); + }, 60_000); test('propagates upload and verification failures', () => { const upload = run({ draft: true, assets: {}, failOn: 'uploads.github.com' }); @@ -572,5 +578,5 @@ describe('exact GitHub release asset reconciliation', () => { const nativePolicy = run({ draft: false, assets: localAssets('published'), invalidNative: true }); expect(nativePolicy.result.exitCode).not.toBe(0); expect(uploadCalls(nativePolicy.state)).toHaveLength(0); - }, 15_000); + }, 120_000); }); diff --git a/src/genie-commands/__tests__/update.test.ts b/src/genie-commands/__tests__/update.test.ts index 182d5fdf3..b8a95a40c 100644 --- a/src/genie-commands/__tests__/update.test.ts +++ b/src/genie-commands/__tests__/update.test.ts @@ -561,7 +561,7 @@ describe('updateCommand wiring', () => { } finally { logSpy.mockRestore(); errorSpy.mockRestore(); - process.exitCode = priorExitCode; + process.exitCode = priorExitCode ?? 0; if (priorWait === undefined) Reflect.deleteProperty(process.env, 'GENIE_LIFECYCLE_LEASE_WAIT_MS'); else process.env.GENIE_LIFECYCLE_LEASE_WAIT_MS = priorWait; } @@ -2207,7 +2207,7 @@ describe('skills.sh channel in the post-delivery convergence (wish skills-everyw }); afterEach(() => { - process.exitCode = previousExitCode; + process.exitCode = previousExitCode ?? 0; }); test('installs skills BEFORE the plugin-era retirement (decision 2 ordering)', () => { diff --git a/src/lib/runtime-integrations.test.ts b/src/lib/runtime-integrations.test.ts index 18630c671..5358ee97c 100644 --- a/src/lib/runtime-integrations.test.ts +++ b/src/lib/runtime-integrations.test.ts @@ -56,15 +56,18 @@ describe('bounded integration subprocess and Codex plugin state', () => { '-e', [ 'const { spawn } = require("node:child_process");', - 'const child = spawn(process.execPath, ["-e", "process.on(\\"SIGTERM\\",()=>{});setInterval(()=>{},1000)"], { stdio: "ignore" });', - 'process.stdout.write(String(child.pid));', + `const child = spawn(process.execPath, ["-e", ${JSON.stringify('process.on("SIGTERM",()=>{});process.stdout.write("ready");setInterval(()=>{},1000)')}], { stdio: ["ignore", "pipe", "ignore"] });`, + 'child.stdout.once("data", () => process.stdout.write(String(child.pid)));', 'process.on("SIGTERM",()=>{});', 'setInterval(()=>{},1000);', ].join(''), ], - { timeoutMs: 50, maxOutputBytes: 1_024, killGraceMs: 30 }, + // Allow both processes to start; stdout acknowledges the descendant's TERM handler. + { timeoutMs: 1_000, maxOutputBytes: 1_024, killGraceMs: 30 }, ); expect(result.timedOut).toBe(true); + // Empty output must never become PID 0 (our entire process group). + expect(result.stdout).toMatch(/^[1-9][0-9]*$/); const descendantPid = Number(result.stdout); expect(Number.isSafeInteger(descendantPid)).toBe(true); let alive = true; From 9cef95e4ccc5e9c597d16fdf3d7dec1598b8640b Mon Sep 17 00:00:00 2001 From: Felipe Rosa <felipe@namastex.ai> Date: Mon, 7 Sep 2026 19:15:22 +0000 Subject: [PATCH 10/24] feat(board): return complete consistent board snapshots Co-authored-by: Felipe Rosa <felipe@namastex.ai> --- .genie/wishes/dsh-genie-board/WISH.md | 43 ++ src/lib/v5/task-state.ts | 206 ++++++++- src/term-commands/v5-board.test.ts | 631 +++++++++++++++++++++++++- src/term-commands/v5-board.ts | 67 ++- 4 files changed, 886 insertions(+), 61 deletions(-) diff --git a/.genie/wishes/dsh-genie-board/WISH.md b/.genie/wishes/dsh-genie-board/WISH.md index 1f838d866..75de6cf5d 100644 --- a/.genie/wishes/dsh-genie-board/WISH.md +++ b/.genie/wishes/dsh-genie-board/WISH.md @@ -290,6 +290,49 @@ Closure evidence: Non-blocking review notes: document that `minimumGenieVersion` intentionally equals the co-shipped plugin release, and record the exact DSH binary path/version in smoke output. Plan status advances to `APPROVED`; implementation and every release/publication gate remain separately authorized. +### Group 1 execution review round 1 — FIX-FIRST (2026-09-03T20:44:39Z) + +- **Reviewed base:** `7c8b5afef` plus the uncommitted Group 1 diff +- **Diff SHA-256:** `5de022e12e2fbf51c4c99c96b5c0edb304e326598e04be5aa95c189e85c03300` +- **Reviewer:** independent native execution reviewer; read-only working-tree review +- **Validation:** `git diff --check` passed; focused board suite passed (50 tests); `bun run check` reached 1955 pass / 1 skip / 9 fail in untouched release/update/local-delivery tests. +- **Verdict:** **FIX-FIRST** — 0 CRITICAL, 2 HIGH, 2 MEDIUM. + +Blocking gaps: +1. Malformed persisted lane metadata could serialize an invalid lane object with exit 0 instead of failing the whole aggregate. +2. Tests did not prove the constant set-query/single-transaction snapshot contract or the required range of malformed/nullability failures. + +Non-blocking gaps: make equal-timestamp ordering and all liveness states discriminating, and strengthen byte-level compatibility fixtures for unchanged CLI surfaces. Fix loop 1 is active; the task remains `in_progress`. + +### Group 1 execution review round 2 — BLOCKED (2026-09-03T20:59:04Z) + +- **Reviewed base:** `7c8b5afef` plus the corrected uncommitted Group 1 diff +- **Diff SHA-256:** `a1b323b4073221ddb77eed59a8a64a7169837906b4fab66773d5f191498b003d` +- **Reviewer:** independent native execution reviewer; read-only working-tree review +- **Code verdict:** no remaining Group 1 findings; every round-one gap is closed. +- **Validation:** `git diff --check`, focused board suite (70 tests), typecheck, and scoped Biome passed. `bun run check` reached 1975 pass / 1 skip / 9 fail. +- **Verdict:** **BLOCKED** — the wish requires a green full gate, and the same nine release/update/local-delivery failures reproduce on untouched detached `HEAD`. + +Corrective route: resolve or formally clear the repository-baseline failures, then rerun `bun run check`. No further Group 1 code fix is indicated; task `t_mtlkd9ad80ce9781` remains `in_progress`. + + +### Group 1 execution review round 3 — code SHIP (2026-09-07) + +- **Reviewer:** independent Codex native reviewer `/root/g1_review`; not the original GLM implementation author. +- **Reviewed HEAD:** `fef77105405991b2f316626b664abdbcdeb7bd08` plus preserved G1 changes, replayed on current dev without conflict. +- **Full diff SHA-256 before this ledger entry:** `a422d2b135fe31ac9fb1507c8c523c95221cf142f4fb2e0735f2d5abdef3e554`. +- **Code/test diff SHA-256:** `57019e9daf27c7ca212000c8976ce41c4b604a363ab75fc088aac4260a61ae2c`. +- **Verdict:** code **SHIP**, no actionable findings. Snapshot consistency, indexed task-scoped JSON-set reads, 33k-card behavior, ordering/nullability, malformed-detail rejection, sanitized identifiers and unchanged output contracts reviewed. +- **Validation:** independent focused suite 76 pass / 0 fail, 382 assertions; diff check passed. Full repository gate is separate and remains pending recovery of reproduced release-test failures. No task-done or release claim. + +### Group 1 acceptance — full gate green (2026-09-07) + +- Baseline repairs independently reviewed **SHIP** by `/root/g1_review`; three-file diff digest `6c03b3e23eb59098b7554256dfd5d1cf741811a79e1a889204b389dc6cc57cf9`, committed as `4928e3988`. +- Root causes: release integration scenarios exceeded implicit test deadlines; Bun preserved a test-owned exit code when restored to undefined; a descendant-cleanup fixture could interpret empty stdout as PID zero. Assertions remain intact; subprocesses are bounded and cleanup validates a positive PID. +- **Full gate:** `bun run check` exited 0; **1990 pass / 1 skip / 0 fail**, 8407 assertions across 96 test files, 262.56 seconds. Frozen dependency install and build passed. +- **Dogfood:** built `dist/genie.js` against an isolated HOME and repository; board creation, task creation, comment, move and aggregate read returned the expected lane, comment and timeline. No personal profile or repository changed. +- G1 code review, full validation and built-CLI smoke are accepted. G2/G3 and stable publication remain pending; this is not whole-wish release acceptance. + --- ## Files to Create/Modify diff --git a/src/lib/v5/task-state.ts b/src/lib/v5/task-state.ts index a036d1a9f..3eff088b1 100644 --- a/src/lib/v5/task-state.ts +++ b/src/lib/v5/task-state.ts @@ -102,6 +102,8 @@ export interface BoardRow { name: string; /** Ordered lifecycle lanes, or null for a laneless (execution-status) board. */ lanes: Lane[] | null; + /** True only when a non-null stored lane definition is not JSON-array data. */ + laneMetadataMalformed: boolean; createdAt: number; } @@ -205,6 +207,34 @@ export interface TaskEvent { createdAt: number; } +/** A dependency summary embedded in the complete board JSON snapshot. */ +export interface BoardTaskDependency { + id: string; + title: string; + status: TaskStatus; +} + +/** A non-empty comment projection embedded in the complete board JSON snapshot. */ +export interface BoardTaskComment { + id: number; + note: string; + authorKind: string | null; + author: string | null; + createdAt: number; +} + +/** + * Complete, closed card contract for one scoped board JSON snapshot. Unlike + * TaskRow and the human projections, this type deliberately includes every + * detail needed by an external board client. + */ +export interface BoardTaskAggregate extends TaskCardRow { + liveness: Liveness | null; + dependencies: BoardTaskDependency[]; + timeline: Array<Omit<TaskEvent, 'taskId'>>; + comments: BoardTaskComment[]; +} + export interface AppendEventInput { kind: string; note?: string; @@ -555,19 +585,26 @@ interface RawBoardRow { created_at: number; } -/** Parse the stored lanes JSON back into `Lane[]`, tolerating malformed data. */ -function parseLanes(raw: string | null): Lane[] | null { - if (raw == null) return null; +/** Parse stored lane JSON while retaining whether non-null metadata was malformed. */ +function parseLanes(raw: string | null): { lanes: Lane[] | null; malformed: boolean } { + if (raw == null) return { lanes: null, malformed: false }; try { const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? (parsed as Lane[]) : null; + return Array.isArray(parsed) ? { lanes: parsed as Lane[], malformed: false } : { lanes: null, malformed: true }; } catch { - return null; + return { lanes: null, malformed: true }; } } function mapBoard(row: RawBoardRow): BoardRow { - return { id: row.id, name: row.name, lanes: parseLanes(row.lanes), createdAt: row.created_at }; + const parsed = parseLanes(row.lanes); + return { + id: row.id, + name: row.name, + lanes: parsed.lanes, + laneMetadataMalformed: parsed.malformed, + createdAt: row.created_at, + }; } /** @@ -583,7 +620,7 @@ export function createBoard(db: Database, name: string, lanes?: Lane[]): BoardRo const normalizedLanes = lanes && lanes.length > 0 ? lanes : null; const lanesJson = normalizedLanes ? JSON.stringify(normalizedLanes) : null; db.query('INSERT INTO boards (id, name, lanes, created_at) VALUES (?, ?, ?, ?)').run(id, name, lanesJson, createdAt); - return { id, name, lanes: normalizedLanes, createdAt }; + return { id, name, lanes: normalizedLanes, laneMetadataMalformed: false, createdAt }; } export function getBoard(db: Database, id: string): BoardRow | null { @@ -1348,6 +1385,161 @@ export function commentCounts(db: Database): Map<string, number> { return new Map(rows.map((r) => [r.task_id, r.n])); } +/** Bounded display-only identifier; persisted values and payloads stay unchanged. */ +export function boardDetailIdentifier(value: string | number): string { + const text = String(value).replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, '?'); + return text.length > 48 ? `${text.slice(0, 45)}...` : text; +} + +interface RawBoardDependency { + task_id: string; + id: string | null; + title: string | null; + status: string | null; +} + +function requireTaskStatus(value: string | null, context: string): TaskStatus { + if (value === 'blocked' || value === 'ready' || value === 'in_progress' || value === 'done') return value; + throw new Error(`Malformed board detail: ${context} has invalid status.`); +} + +function requireString(value: unknown, context: string): string { + if (typeof value === 'string') return value; + throw new Error(`Malformed board detail: ${context} must be a string.`); +} + +function requireNullableString(value: unknown, context: string): string | null { + if (value === null || typeof value === 'string') return value; + throw new Error(`Malformed board detail: ${context} must be a string or null.`); +} + +function requireNumber(value: unknown, context: string): number { + if (typeof value === 'number' && Number.isFinite(value)) return value; + throw new Error(`Malformed board detail: ${context} must be a finite number.`); +} + +function mapAggregateTask(row: RawTask): TaskCardRow { + const id = requireString(row.id, 'task id'); + const blockedBy = requireNullableString(row.blocked_by, `task ${boardDetailIdentifier(id)} blockedBy`); + const blockedReason = requireNullableString(row.blocked_reason, `task ${boardDetailIdentifier(id)} blockedReason`); + const blockKind = requireNullableString(row.block_kind, `task ${boardDetailIdentifier(id)} block kind`); + if (blockKind !== null && blockKind !== 'work' && blockKind !== 'hold') { + throw new Error(`Malformed board detail: task ${boardDetailIdentifier(id)} has invalid block kind.`); + } + return { + id, + boardId: requireNullableString(row.board_id, `task ${boardDetailIdentifier(id)} boardId`), + title: requireString(row.title, `task ${boardDetailIdentifier(id)} title`), + status: requireTaskStatus(row.status, `task ${boardDetailIdentifier(id)}`), + claimedBy: requireNullableString(row.claimed_by, `task ${boardDetailIdentifier(id)} claimedBy`), + claimedAt: + row.claimed_at === null ? null : requireNumber(row.claimed_at, `task ${boardDetailIdentifier(id)} claimedAt`), + wish: requireNullableString(row.wish, `task ${boardDetailIdentifier(id)} wish`), + group: requireNullableString(row.group_name, `task ${boardDetailIdentifier(id)} group`), + assignedAgent: requireNullableString(row.assigned_agent, `task ${boardDetailIdentifier(id)} assignedAgent`), + assignedReason: requireNullableString(row.assigned_reason, `task ${boardDetailIdentifier(id)} assignedReason`), + createdAt: requireNumber(row.created_at, `task ${boardDetailIdentifier(id)} createdAt`), + updatedAt: requireNumber(row.updated_at, `task ${boardDetailIdentifier(id)} updatedAt`), + lane: requireNullableString(row.lane, `task ${boardDetailIdentifier(id)} lane`), + agentKind: requireNullableString(row.agent_kind, `task ${boardDetailIdentifier(id)} agentKind`), + heartbeatAt: + row.heartbeat_at === null + ? null + : requireNumber(row.heartbeat_at, `task ${boardDetailIdentifier(id)} heartbeatAt`), + blockedBy, + blockedReason, + enforcedBlock: blockedBy === null ? null : { reason: blockedReason ?? '', kind: blockKind ?? 'work' }, + }; +} + +/** + * Read every detail for a board's cards from one SQLite snapshot. All rows are + * fetched in three set queries inside one deferred read transaction: cards, + * their dependency summaries, and their timelines. No caller hydrates cards + * individually, and any malformed joined detail aborts the whole snapshot. + */ +export function readBoardTaskSnapshot( + db: Database, + boardId: string, + filter: Pick<TaskFilter, 'wish'> = {}, + now = Date.now(), +): BoardTaskAggregate[] { + const read = db.transaction((): BoardTaskAggregate[] => { + const params = filter.wish ? [boardId, filter.wish] : [boardId]; + const tasks = db + .query(`SELECT * FROM tasks WHERE board_id = ?${filter.wish ? ' AND wish = ?' : ''} ORDER BY created_at`) + .all(...params) as RawTask[]; + if (tasks.length === 0) return []; + // One JSON binding avoids variable limits while retaining task_id index probes. + const taskIds = JSON.stringify(tasks.map((row) => requireString(row.id, 'task id'))); + const dependencies = db + .query( + `SELECT td.task_id, dep.id, dep.title, dep.status + FROM task_dependencies td + LEFT JOIN tasks dep ON dep.id = td.depends_on_id + WHERE td.task_id IN (SELECT value FROM json_each(?)) + ORDER BY td.task_id, dep.id`, + ) + .all(taskIds) as RawBoardDependency[]; + const events = db + .query( + `SELECT e.* FROM task_events e + WHERE e.task_id IN (SELECT value FROM json_each(?)) + ORDER BY e.task_id, e.created_at, e.id`, + ) + .all(taskIds) as RawTaskEvent[]; + + const dependenciesByTask = new Map<string, BoardTaskDependency[]>(); + for (const dependency of dependencies) { + const taskId = requireString(dependency.task_id, 'dependency owner id'); + const id = requireString(dependency.id, `task ${boardDetailIdentifier(taskId)} dependency id`); + const summaries = dependenciesByTask.get(taskId) ?? []; + summaries.push({ + id, + title: requireString(dependency.title, `dependency ${boardDetailIdentifier(id)} title`), + status: requireTaskStatus(dependency.status, `dependency ${boardDetailIdentifier(id)}`), + }); + dependenciesByTask.set(taskId, summaries); + } + + const timelineByTask = new Map<string, Array<Omit<TaskEvent, 'taskId'>>>(); + for (const row of events) { + const taskId = requireString(row.task_id, 'timeline task id'); + const event = { + id: requireNumber(row.id, `task ${boardDetailIdentifier(taskId)} event id`), + kind: requireString(row.kind, `task ${boardDetailIdentifier(taskId)} event kind`), + note: requireNullableString(row.note, `task ${boardDetailIdentifier(taskId)} event note`), + authorKind: requireNullableString(row.author_kind, `task ${boardDetailIdentifier(taskId)} event authorKind`), + author: requireNullableString(row.author, `task ${boardDetailIdentifier(taskId)} event author`), + createdAt: requireNumber(row.created_at, `task ${boardDetailIdentifier(taskId)} event createdAt`), + }; + if (event.kind === 'comment' && event.note === null) { + throw new Error( + `Malformed board detail: task ${boardDetailIdentifier(taskId)} comment ${boardDetailIdentifier(event.id)} has null text.`, + ); + } + const timeline = timelineByTask.get(taskId) ?? []; + timeline.push(event); + timelineByTask.set(taskId, timeline); + } + + return tasks.map((row) => { + const card = mapAggregateTask(row); + const timeline = timelineByTask.get(card.id) ?? []; + return { + ...card, + liveness: card.claimedBy === null ? null : livenessFromHeartbeat(card.heartbeatAt, now), + dependencies: dependenciesByTask.get(card.id) ?? [], + timeline, + comments: timeline + .filter((event): event is typeof event & { note: string } => event.kind === 'comment' && event.note !== null) + .map(({ id, note, authorKind, author, createdAt }) => ({ id, note, authorKind, author, createdAt })), + }; + }); + }); + return read.deferred() as BoardTaskAggregate[]; +} + // ============================================================================ // Lane moves // ============================================================================ diff --git a/src/term-commands/v5-board.test.ts b/src/term-commands/v5-board.test.ts index 1e19bc75e..5c2d15a29 100644 --- a/src/term-commands/v5-board.test.ts +++ b/src/term-commands/v5-board.test.ts @@ -4,6 +4,7 @@ * on the next render with nothing persisted. Exit codes AND stderr are checked. */ +import type { Database, SQLQueryBindings } from 'bun:sqlite'; import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { execFileSync } from 'node:child_process'; import { @@ -23,6 +24,7 @@ import { DEFAULT_LIFECYCLE_LANES, LIVENESS_RUNNING_MS, LIVENESS_STALE_MS, + addDependency, appendTaskEvent, blockTask, claimTask, @@ -32,6 +34,7 @@ import { getTaskEvents, getTaskLane, moveTask, + readBoardTaskSnapshot, recordHeartbeat, } from '../lib/v5/task-state.js'; @@ -76,6 +79,13 @@ async function board(cwd: string, ...args: string[]): Promise<CliResult> { return boardWithEnv(cwd, {}, ...args); } +function expectMalformedBoard(result: CliResult): void { + expect(result.code).toBe(1); + expect(result.stdout).toBe(''); + expect(result.stderr).toMatch(/^Error: Malformed board detail: [^\n]+\n$/); + expect(result.stderr.length).toBeLessThan(240); +} + async function manualTask(cwd: string, ...args: string[]): Promise<CliResult> { const proc = Bun.spawn(['bun', GENIE, 'task', ...args], { cwd, @@ -161,18 +171,35 @@ describe('board render', () => { test('--json emits columns keyed by status', async () => { const db = openDb({ cwd: repo }); - createTask(db, { title: 'ready-1' }); + const task = createTask(db, { title: 'ready-1' }); db.close(); const r = await board(repo, '--json'); expect(r.code).toBe(0); - const payload = JSON.parse(r.stdout) as { - scope: string; - columns: Record<string, Array<{ title: string }>>; + expect(r.stderr).toBe(''); + const expected = { + scope: 'all tasks', + columns: { + blocked: [], + ready: [ + { + id: task.id, + boardId: null, + title: 'ready-1', + status: 'ready', + claimedBy: null, + claimedAt: null, + wish: null, + group: null, + createdAt: task.createdAt, + updatedAt: task.updatedAt, + }, + ], + in_progress: [], + done: [], + }, }; - expect(payload.columns.ready).toHaveLength(1); - expect(payload.columns.ready[0].title).toBe('ready-1'); - expect(payload.columns.blocked).toHaveLength(0); + expect(r.stdout).toBe(`${JSON.stringify(expected, null, 2)}\n`); }); }); @@ -241,8 +268,10 @@ describe('board list', () => { test('reports lane count and card count per board', async () => { const db = openDb({ cwd: repo }); const road = createBoard(db, 'roadmap', DEFAULT_LIFECYCLE_LANES); - createBoard(db, 'plain'); + const plain = createBoard(db, 'plain'); createTask(db, { title: 'c1', boardId: road.id }); + db.query('UPDATE boards SET created_at = 10 WHERE id = ?').run(road.id); + db.query('UPDATE boards SET created_at = 20 WHERE id = ?').run(plain.id); db.close(); const r = await board(repo, 'list'); @@ -252,11 +281,18 @@ describe('board list', () => { expect(r.stdout).toContain('2 boards'); const j = await board(repo, 'list', '--json'); - const rows = JSON.parse(j.stdout) as Array<{ name: string; laneCount: number; cardCount: number }>; - const road2 = rows.find((x) => x.name === 'roadmap'); - expect(road2?.laneCount).toBe(6); - expect(road2?.cardCount).toBe(1); - expect(rows.find((x) => x.name === 'plain')?.laneCount).toBe(0); + expect(j.code).toBe(0); + expect(j.stderr).toBe(''); + expect(j.stdout).toBe( + `${JSON.stringify( + [ + { id: road.id, name: 'roadmap', laneCount: 6, cardCount: 1 }, + { id: plain.id, name: 'plain', laneCount: 0, cardCount: 0 }, + ], + null, + 2, + )}\n`, + ); }); test('reports "No boards found." on an empty repo', async () => { @@ -267,6 +303,44 @@ describe('board list', () => { }); describe('lane-grouped render', () => { + test('keeps the human empty-lane output byte-exact', async () => { + const db = openDb({ cwd: repo }); + createBoard(db, 'roadmap', DEFAULT_LIFECYCLE_LANES); + db.close(); + + const result = await board(repo, '--board', 'roadmap'); + expect(result).toEqual({ + code: 0, + stderr: '', + stdout: [ + '', + 'Board — board "roadmap"', + '═'.repeat(56), + ' Idea: 0 Brainstorm: 0 Wish: 0 Work: 0 Review: 0 Done: 0', + '', + '── Idea → /brainstorm (0 cards) ──', + ' (empty)', + '', + '── Brainstorm → /wish (0 cards) ──', + ' (empty)', + '', + '── Wish → /work (0 cards) ──', + ' (empty)', + '', + '── Work → /review (0 cards) ──', + ' (empty)', + '', + '── Review (0 cards) ──', + ' (empty)', + '', + '── Done (0 cards) ──', + ' (empty)', + '', + '', + ].join('\n'), + }); + }); + test('groups by lane and prints action hints; a moved card lands in its lane', async () => { const db = openDb({ cwd: repo }); const road = createBoard(db, 'roadmap', DEFAULT_LIFECYCLE_LANES); @@ -319,7 +393,7 @@ describe('lane-grouped render', () => { expect(payload.lanes.map((l) => l.name)).toEqual(['Idea', 'Brainstorm', 'Wish', 'Work', 'Review', 'Done']); }); - test('--json carries enforcedBlock and the declared routing on every lane card and nothing else from the runtime layer', async () => { + test('--json carries the complete exact card aggregate and explicit nullability', async () => { const db = openDb({ cwd: repo }); const road = createBoard(db, 'roadmap', DEFAULT_LIFECYCLE_LANES); createTask(db, { title: 'open card', boardId: road.id, lane: 'Idea' }); @@ -334,7 +408,7 @@ describe('lane-grouped render', () => { }); blockTask(db, held.id, 'parked until Q3', { author: 'felipe', authorKind: 'human' }, 'hold'); blockTask(db, broken.id, 'awaiting a decision', { author: 'felipe', authorKind: 'human' }); - recordHeartbeat(db, held.id); // a runtime field that must NOT reach this shape + recordHeartbeat(db, held.id); db.close(); const r = await board(repo, '--board', 'roadmap', '--json'); @@ -356,23 +430,48 @@ describe('lane-grouped render', () => { expect(cards.get('assigned card')?.assignedAgent).toBe('codex'); expect(cards.get('assigned card')?.assignedReason).toBe('declared routing'); - // The lane shape carries the two declared-routing fields plus exactly one - // runtime field — provenance, identity, and heartbeat siblings stay off it. - for (const leaked of ['agentKind', 'heartbeatAt', 'blockedBy', 'blockedReason']) { - expect(leaked in (cards.get('held card') as Record<string, unknown>)).toBe(false); - } + expect(cards.get('open card')).toMatchObject({ + claimedBy: null, + claimedAt: null, + wish: null, + group: null, + lane: 'Idea', + agentKind: null, + heartbeatAt: null, + liveness: null, + blockedBy: null, + blockedReason: null, + enforcedBlock: null, + dependencies: [], + timeline: [], + comments: [], + }); + expect(cards.get('held card')).toMatchObject({ + blockedBy: 'felipe', + blockedReason: 'parked until Q3', + enforcedBlock: { reason: 'parked until Q3', kind: 'hold' }, + liveness: null, + }); expect(Object.keys(cards.get('held card') as Record<string, unknown>).sort()).toEqual([ + 'agentKind', 'assignedAgent', 'assignedReason', + 'blockedBy', + 'blockedReason', 'boardId', 'claimedAt', 'claimedBy', + 'comments', 'createdAt', + 'dependencies', 'enforcedBlock', 'group', + 'heartbeatAt', 'id', 'lane', + 'liveness', 'status', + 'timeline', 'title', 'updatedAt', 'wish', @@ -380,6 +479,498 @@ describe('lane-grouped render', () => { }); }); +describe('scoped board JSON aggregate v1', () => { + test('freezes the exact outer shape and returns every lane for an empty board', async () => { + const db = openDb({ cwd: repo }); + createBoard(db, 'empty', DEFAULT_LIFECYCLE_LANES); + db.close(); + + const result = await board(repo, '--board', 'empty', '--json'); + expect(result.code).toBe(0); + expect(result.stderr).toBe(''); + const payload = JSON.parse(result.stdout) as Record<string, unknown> & { + lanes: Array<{ name: string; label: string | null; action: string | null; cards: unknown[] }>; + }; + expect(Object.keys(payload)).toEqual(['schemaVersion', 'scope', 'lanes']); + expect(payload.schemaVersion).toBe(1); + expect(payload.scope).toBe('board "empty"'); + expect(payload.lanes.map((lane) => lane.name)).toEqual(['Idea', 'Brainstorm', 'Wish', 'Work', 'Review', 'Done']); + for (const lane of payload.lanes) { + expect(Object.keys(lane)).toEqual(['name', 'label', 'action', 'cards']); + expect(lane.cards).toEqual([]); + } + }); + + test('orders cards, dependencies, timeline, and comment projection deterministically', async () => { + const db = openDb({ cwd: repo }); + const roadmap = createBoard(db, 'roadmap', DEFAULT_LIFECYCLE_LANES); + const depZ = createTask(db, { title: 'dependency z', boardId: roadmap.id, lane: 'Idea' }); + const depA = createTask(db, { title: 'dependency a', boardId: roadmap.id, lane: 'Idea' }); + const cardB = createTask(db, { title: 'card b', boardId: roadmap.id, lane: 'Work' }); + const cardA = createTask(db, { title: 'card a', boardId: roadmap.id, lane: 'Work' }); + // Equal timestamps deliberately oppose lexical id order: existing board + // readers preserve insertion order, so z-card must remain before a-card. + db.query('UPDATE tasks SET id = ? WHERE id = ?').run('z-dependency', depZ.id); + db.query('UPDATE tasks SET id = ? WHERE id = ?').run('a-dependency', depA.id); + db.query('UPDATE tasks SET id = ?, created_at = 10 WHERE id = ?').run('z-card', cardB.id); + db.query('UPDATE tasks SET id = ?, created_at = 10 WHERE id = ?').run('a-card', cardA.id); + addDependency(db, 'a-card', 'z-dependency'); + addDependency(db, 'a-card', 'a-dependency'); + // Insert same-time ids in descending order. Timeline and its comment + // projection must sort by createdAt then id, independently of insertion. + db.query( + `INSERT INTO task_events (id, task_id, kind, note, author_kind, author, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + ).run(90, 'a-card', 'comment', 'second comment', 'human', 'felipe', 20); + db.query( + `INSERT INTO task_events (id, task_id, kind, note, author_kind, author, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + ).run(40, 'a-card', 'move', 'Idea→Work', null, null, 20); + db.query( + `INSERT INTO task_events (id, task_id, kind, note, author_kind, author, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + ).run(10, 'a-card', 'comment', 'first comment', null, null, 20); + db.close(); + + const result = await board(repo, '--board', 'roadmap', '--json'); + expect(result.code).toBe(0); + expect(result.stderr).toBe(''); + const payload = JSON.parse(result.stdout) as { + lanes: Array<{ + name: string; + cards: Array< + Record<string, unknown> & { + id: string; + dependencies: Array<{ id: string; title: string; status: string }>; + timeline: Array<{ id: number; kind: string; note: string | null; createdAt: number }>; + comments: Array<{ + id: number; + note: string; + authorKind: string | null; + author: string | null; + createdAt: number; + }>; + } + >; + }>; + }; + const work = payload.lanes.find((lane) => lane.name === 'Work'); + expect(work?.cards.map((card) => card.id)).toEqual(['z-card', 'a-card']); + const card = work?.cards.find((candidate) => candidate.id === 'a-card'); + expect(card).toBeDefined(); + if (!card) throw new Error('expected card a in Work lane'); + expect(card.dependencies.map((dependency) => dependency.id)).toEqual(['a-dependency', 'z-dependency']); + for (const dependency of card.dependencies) { + expect(Object.keys(dependency)).toEqual(['id', 'title', 'status']); + } + expect(card.timeline.map((event) => event.id)).toEqual([10, 40, 90]); + for (const event of card.timeline) { + expect(Object.keys(event)).toEqual(['id', 'kind', 'note', 'authorKind', 'author', 'createdAt']); + } + expect(card.comments).toEqual([ + { id: 10, note: 'first comment', authorKind: null, author: null, createdAt: 20 }, + { + id: 90, + note: 'second comment', + authorKind: 'human', + author: 'felipe', + createdAt: 20, + }, + ]); + }); + + test('derives running, idle, stale, missing-heartbeat, and unclaimed liveness exactly', async () => { + const db = openDb({ cwd: repo }); + const roadmap = createBoard(db, 'roadmap', DEFAULT_LIFECYCLE_LANES); + const now = Date.now(); + const running = createTask(db, { title: 'running', boardId: roadmap.id, lane: 'Work' }); + const idle = createTask(db, { title: 'idle', boardId: roadmap.id, lane: 'Work' }); + const stale = createTask(db, { title: 'stale', boardId: roadmap.id, lane: 'Work' }); + const missing = createTask(db, { title: 'missing heartbeat', boardId: roadmap.id, lane: 'Work' }); + const open = createTask(db, { title: 'open with heartbeat', boardId: roadmap.id, lane: 'Work' }); + for (const task of [running, idle, stale, missing]) claimTask(db, task.id, 'worker'); + recordHeartbeat(db, running.id, now); + recordHeartbeat(db, idle.id, now - LIVENESS_RUNNING_MS - 60_000); + recordHeartbeat(db, stale.id, now - LIVENESS_STALE_MS - 60_000); + recordHeartbeat(db, open.id, now); + db.close(); + + const result = await board(repo, '--board', 'roadmap', '--json'); + const cards = ( + JSON.parse(result.stdout) as { lanes: Array<{ cards: Array<Record<string, unknown>> }> } + ).lanes.flatMap((lane) => lane.cards); + expect(cards.find((card) => card.id === running.id)?.liveness).toBe('running'); + expect(cards.find((card) => card.id === idle.id)?.liveness).toBe('idle'); + expect(cards.find((card) => card.id === stale.id)?.liveness).toBe('stale'); + expect(cards.find((card) => card.id === missing.id)?.liveness).toBe('stale'); + expect(cards.find((card) => card.id === open.id)?.liveness).toBeNull(); + }); + + test('is byte-idempotent after the complete snapshot has been established', async () => { + const db = openDb({ cwd: repo }); + const roadmap = createBoard(db, 'roadmap', DEFAULT_LIFECYCLE_LANES); + createTask(db, { title: 'stable', boardId: roadmap.id, lane: 'Idea' }); + db.close(); + + const first = await board(repo, '--board', 'roadmap', '--json'); + const second = await board(repo, '--board', 'roadmap', '--json'); + expect(first).toEqual({ code: 0, stderr: '', stdout: second.stdout }); + expect(second.code).toBe(0); + expect(second.stderr).toBe(''); + }); + + const corruptions: Array<{ + name: string; + corrupt: (db: Database, boardId: string, taskId: string) => void; + }> = [ + { + name: 'card title scalar', + corrupt: (db, _boardId, taskId) => db.query("UPDATE tasks SET title = X'01' WHERE id = ?").run(taskId), + }, + { + name: 'card status enum', + corrupt: (db, _boardId, taskId) => { + db.exec('PRAGMA ignore_check_constraints = ON'); + db.query("UPDATE tasks SET status = 'unknown' WHERE id = ?").run(taskId); + }, + }, + { + name: 'nullable card claimant scalar', + corrupt: (db, _boardId, taskId) => db.query("UPDATE tasks SET claimed_by = X'01' WHERE id = ?").run(taskId), + }, + { + name: 'dependency title scalar', + corrupt: (db, _boardId, taskId) => { + const dependency = createTask(db, { title: 'outside dependency' }); + addDependency(db, taskId, dependency.id); + db.query("UPDATE tasks SET title = X'01' WHERE id = ?").run(dependency.id); + }, + }, + { + name: 'dependency status enum', + corrupt: (db, _boardId, taskId) => { + const dependency = createTask(db, { title: 'outside dependency' }); + addDependency(db, taskId, dependency.id); + db.exec('PRAGMA ignore_check_constraints = ON'); + db.query("UPDATE tasks SET status = 'unknown' WHERE id = ?").run(dependency.id); + }, + }, + { + name: 'orphan dependency', + corrupt: (db, _boardId, taskId) => { + db.exec('PRAGMA foreign_keys = OFF'); + db.query('INSERT INTO task_dependencies (task_id, depends_on_id) VALUES (?, ?)').run(taskId, 'missing-task'); + }, + }, + { + name: 'timeline kind scalar', + corrupt: (db, _boardId, taskId) => { + const event = appendTaskEvent(db, taskId, { kind: 'move' }); + db.query("UPDATE task_events SET kind = X'01' WHERE id = ?").run(event.id); + }, + }, + { + name: 'nullable timeline author scalar', + corrupt: (db, _boardId, taskId) => { + const event = appendTaskEvent(db, taskId, { kind: 'move' }); + db.query("UPDATE task_events SET author = X'01' WHERE id = ?").run(event.id); + }, + }, + { + name: 'timeline timestamp scalar', + corrupt: (db, _boardId, taskId) => { + const event = appendTaskEvent(db, taskId, { kind: 'move' }); + db.query("UPDATE task_events SET created_at = X'01' WHERE id = ?").run(event.id); + }, + }, + { + name: 'null comment text', + corrupt: (db, _boardId, taskId) => { + appendTaskEvent(db, taskId, { kind: 'comment' }); + }, + }, + ]; + + for (const fixture of corruptions) { + test(`fails closed for malformed ${fixture.name}`, async () => { + const db = openDb({ cwd: repo }); + const roadmap = createBoard(db, 'roadmap', DEFAULT_LIFECYCLE_LANES); + const task = createTask(db, { title: 'corrupt target', boardId: roadmap.id, lane: 'Idea' }); + fixture.corrupt(db, roadmap.id, task.id); + db.close(); + + expectMalformedBoard(await board(repo, '--board', 'roadmap', '--json')); + }); + } + + const malformedLanes = [ + ['invalid JSON', '{'], + ['non-array JSON', '{}'], + ['non-object entry', '[null]'], + ['missing name', '[{}]'], + ['wrong name', '[{"name":7}]'], + ['wrong label', '[{"name":"Idea","label":7}]'], + ['null label', '[{"name":"Idea","label":null}]'], + ['wrong action', '[{"name":"Idea","action":7}]'], + ['null action', '[{"name":"Idea","action":null}]'], + ] as const; + + for (const [name, lanes] of malformedLanes) { + test(`fails closed for ${name} lane metadata`, async () => { + const db = openDb({ cwd: repo }); + const roadmap = createBoard(db, 'roadmap', DEFAULT_LIFECYCLE_LANES); + db.query('UPDATE boards SET lanes = ? WHERE id = ?').run(lanes, roadmap.id); + db.close(); + + expectMalformedBoard(await board(repo, '--board', 'roadmap', '--json')); + }); + } + + test('an unknown board JSON read fails with exit 1, empty stdout, and clear stderr', async () => { + const result = await board(repo, '--board', 'ghost', '--json'); + expect(result).toEqual({ stdout: '', stderr: 'Error: Board not found: ghost\n', code: 1 }); + }); +}); + +describe('board aggregate repository snapshot', () => { + function instrumentReader( + reader: Database, + afterFirstSetRead?: () => void, + ): { + counts: { queries: number; transactions: number; deferred: number; outsideTransaction: number }; + } { + const counts = { queries: 0, transactions: 0, deferred: 0, outsideTransaction: 0 }; + const originalQuery = reader.query.bind(reader); + const originalTransaction = reader.transaction.bind(reader); + + Object.defineProperty(reader, 'query', { + configurable: true, + value: (sql: string) => { + counts.queries += 1; + if (!reader.inTransaction) counts.outsideTransaction += 1; + const queryNumber = counts.queries; + const statement = originalQuery(sql); + return new Proxy(statement, { + get(target, property) { + const value = Reflect.get(target, property, target); + if (property === 'all') { + return (...args: unknown[]) => { + const rows = Reflect.apply(value as (...values: unknown[]) => unknown, target, args); + if (queryNumber === 1) afterFirstSetRead?.(); + return rows; + }; + } + return typeof value === 'function' ? value.bind(target) : value; + }, + }); + }, + }); + Object.defineProperty(reader, 'transaction', { + configurable: true, + value: (callback: () => unknown) => { + counts.transactions += 1; + const transaction = originalTransaction(callback); + return { + deferred: () => { + counts.deferred += 1; + return transaction.deferred(); + }, + }; + }, + }); + return { counts }; + } + + test('uses three constant set queries in one deferred transaction regardless of card count', () => { + const writer = openDb({ cwd: repo }); + const roadmap = createBoard(writer, 'roadmap', DEFAULT_LIFECYCLE_LANES); + createTask(writer, { title: 'first', boardId: roadmap.id, lane: 'Idea' }); + const reader = openDb({ cwd: repo }); + const { counts } = instrumentReader(reader); + + expect(readBoardTaskSnapshot(reader, roadmap.id)).toHaveLength(1); + expect(counts).toEqual({ queries: 3, transactions: 1, deferred: 1, outsideTransaction: 0 }); + + for (let index = 0; index < 24; index += 1) { + createTask(writer, { title: `card ${index}`, boardId: roadmap.id, lane: 'Idea' }); + } + expect(readBoardTaskSnapshot(reader, roadmap.id)).toHaveLength(25); + expect(counts).toEqual({ queries: 6, transactions: 2, deferred: 2, outsideTransaction: 0 }); + reader.close(); + writer.close(); + }); + + test('retains one SQLite snapshot when a peer writes between set reads', () => { + const writer = openDb({ cwd: repo }); + const roadmap = createBoard(writer, 'roadmap', DEFAULT_LIFECYCLE_LANES); + const dependency = createTask(writer, { title: 'before peer write' }); + const card = createTask(writer, { title: 'snapshot card', boardId: roadmap.id, lane: 'Work' }); + addDependency(writer, card.id, dependency.id); + const reader = openDb({ cwd: repo }); + const { counts } = instrumentReader(reader, () => { + writer.query('UPDATE tasks SET title = ? WHERE id = ?').run('after peer write', dependency.id); + appendTaskEvent(writer, card.id, { kind: 'comment', note: 'after peer write' }); + }); + + const [snapshot] = readBoardTaskSnapshot(reader, roadmap.id); + expect(snapshot.dependencies).toEqual([{ id: dependency.id, title: 'before peer write', status: 'ready' }]); + expect(snapshot.timeline).toEqual([]); + expect(snapshot.comments).toEqual([]); + expect(counts).toEqual({ queries: 3, transactions: 1, deferred: 1, outsideTransaction: 0 }); + expect(writer.query('SELECT title FROM tasks WHERE id = ?').get(dependency.id) as { title: string }).toEqual({ + title: 'after peer write', + }); + expect(getTaskEvents(writer, card.id)).toHaveLength(1); + reader.close(); + writer.close(); + }); + + function captureDetailReads( + reader: Database, + ): Array<{ sql: string; bindings: SQLQueryBindings[]; plan: string[]; inTransaction: boolean }> { + const reads: Array<{ sql: string; bindings: SQLQueryBindings[]; plan: string[]; inTransaction: boolean }> = []; + const originalQuery = reader.query.bind(reader); + Object.defineProperty(reader, 'query', { + configurable: true, + value: (sql: string) => { + const statement = originalQuery(sql); + return { + all: (...bindings: SQLQueryBindings[]) => { + if (sql.startsWith('SELECT')) { + const plan = originalQuery(`EXPLAIN QUERY PLAN ${sql}`).all(...bindings) as Array<{ + detail: string; + }>; + reads.push({ sql, bindings, plan: plan.map((row) => row.detail), inTransaction: reader.inTransaction }); + } + return statement.all(...bindings); + }, + }; + }, + }); + return reads; + } + + test('detail reads probe task_id indexes scoped to the selected card ids', () => { + const writer = openDb({ cwd: repo }); + const selected = createBoard(writer, 'selected', DEFAULT_LIFECYCLE_LANES); + const other = createBoard(writer, 'other', DEFAULT_LIFECYCLE_LANES); + const cardA = createTask(writer, { title: 'card a', boardId: selected.id, lane: 'Idea' }); + const cardB = createTask(writer, { title: 'card b', boardId: selected.id, lane: 'Idea' }); + const dependency = createTask(writer, { title: 'cross-board dependency', boardId: other.id, lane: 'Idea' }); + const foreign = createTask(writer, { title: 'foreign card', boardId: other.id, lane: 'Idea' }); + const otherWish = createTask(writer, { title: 'other wish', boardId: selected.id, lane: 'Idea' }); + writer.query('UPDATE tasks SET wish = ? WHERE id IN (?, ?)').run('slice', cardA.id, cardB.id); + writer.query('UPDATE tasks SET created_at = 1 WHERE id = ?').run(cardA.id); + writer.query('UPDATE tasks SET created_at = 2 WHERE id = ?').run(cardB.id); + addDependency(writer, cardA.id, dependency.id); + // Out-of-scope history: a foreign-board event and a filtered-wish comment + // must never surface in — or fail — the selected board's snapshot. + writer + .query("INSERT INTO task_events (task_id, kind, note, created_at) VALUES (?, 'comment', NULL, 1)") + .run(foreign.id); + writer + .query("INSERT INTO task_events (task_id, kind, note, created_at) VALUES (?, 'comment', NULL, 1)") + .run(otherWish.id); + writer + .query("INSERT INTO task_events (task_id, kind, note, created_at) VALUES (?, 'comment', 'later', 2)") + .run(cardA.id); + writer + .query("INSERT INTO task_events (task_id, kind, note, created_at) VALUES (?, 'comment', 'earlier', 1)") + .run(cardA.id); + writer.close(); + + const reader = openDb({ cwd: repo }); + const reads = captureDetailReads(reader); + const snapshot = readBoardTaskSnapshot(reader, selected.id, { wish: 'slice' }); + expect(snapshot.map((card) => card.id)).toEqual([cardA.id, cardB.id]); + expect(snapshot[0]?.dependencies.map((row) => row.id)).toEqual([dependency.id]); + expect(snapshot[0]?.timeline.map((event) => event.note)).toEqual(['earlier', 'later']); + expect(reads).toHaveLength(3); + for (const read of reads) { + expect(read.inTransaction).toBe(true); + } + expect(reads[1]?.sql).toContain('FROM task_dependencies'); + expect(reads[1]?.bindings).toEqual([JSON.stringify([cardA.id, cardB.id])]); + expect(reads[1]?.plan.join('\n')).toMatch(/SEARCH td .*INDEX.*task_id=\?/); + expect(reads[2]?.sql).toContain('FROM task_events'); + expect(reads[2]?.bindings).toEqual([JSON.stringify([cardA.id, cardB.id])]); + expect(reads[2]?.plan.join('\n')).toMatch(/SEARCH e .*INDEX.*task_id=\?/); + expect( + reads + .slice(1) + .flatMap((read) => read.plan) + .join('\n'), + ).not.toMatch(/SCAN (td|e)(?: |$)/); + reader.close(); + }); + + test('an empty card selection returns before querying detail tables', () => { + const writer = openDb({ cwd: repo }); + const empty = createBoard(writer, 'empty-board', DEFAULT_LIFECYCLE_LANES); + writer.close(); + + const reader = openDb({ cwd: repo }); + const reads = captureDetailReads(reader); + expect(readBoardTaskSnapshot(reader, empty.id)).toEqual([]); + expect(reads).toHaveLength(1); + expect(reads[0]?.inTransaction).toBe(true); + reader.close(); + }); + + const hostileIdentifier = 'bad\n\r\t\x1b\u0085\u2028\u2029\u202e'.concat('x'.repeat(2000)); + for (const kind of ['task', 'dependency', 'event'] as const) { + test(`a malformed ${kind} identifier cannot create multiline or oversized diagnostics`, () => { + const db = openDb({ cwd: repo }); + const roadmap = createBoard(db, 'roadmap', DEFAULT_LIFECYCLE_LANES); + const hostile = createTask(db, { title: 'hostile id', boardId: roadmap.id, lane: 'Idea' }); + const owner = createTask(db, { title: 'owner', boardId: roadmap.id, lane: 'Idea' }); + db.query('UPDATE tasks SET id = ? WHERE id = ?').run(hostileIdentifier, hostile.id); + if (kind === 'event') { + db.query("INSERT INTO task_events (task_id, kind, note, created_at) VALUES (?, 'comment', NULL, 1)").run( + hostileIdentifier, + ); + } else { + if (kind === 'dependency') addDependency(db, owner.id, hostileIdentifier); + db.exec('PRAGMA ignore_check_constraints = ON'); + db.query("UPDATE tasks SET status = 'unknown' WHERE id = ?").run(hostileIdentifier); + } + db.close(); + + const reader = openDb({ cwd: repo }); + let message = ''; + try { + readBoardTaskSnapshot(reader, roadmap.id); + } catch (error) { + message = (error as Error).message; + } + reader.close(); + expect(message.startsWith('Malformed board detail:')).toBe(true); + expect(message).not.toMatch(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u); + expect(message.length).toBeLessThan(200); + }); + } + + test('a large board stays under SQLite bind-variable limits with identifiers intact', () => { + const writer = openDb({ cwd: repo }); + const wide = createBoard(writer, 'wide', DEFAULT_LIFECYCLE_LANES); + const insert = writer.prepare( + "INSERT INTO tasks (id, board_id, title, status, created_at, updated_at) VALUES (?, ?, 'bulk', 'ready', 1, 1)", + ); + writer.transaction(() => { + for (let index = 0; index < 33_000; index += 1) insert.run(`task-${index}`, wide.id); + })(); + writer.close(); + + const reader = openDb({ cwd: repo }); + const snapshot = readBoardTaskSnapshot(reader, wide.id); + reader.close(); + expect(snapshot).toHaveLength(33_000); + expect(new Set(snapshot.map((card) => card.id)).size).toBe(33_000); + expect(snapshot.find((card) => card.id === 'task-0')).toBeDefined(); + expect(snapshot.find((card) => card.id === 'task-32999')).toBeDefined(); + }); +}); + describe('wish-status lane reconciliation on CLI JSON reads', () => { test('maps every ordered status prefix and leaves other untouched', async () => { const cases: Array<[status: string, destination: string]> = [ diff --git a/src/term-commands/v5-board.ts b/src/term-commands/v5-board.ts index dcdc4604c..bcf214b5f 100644 --- a/src/term-commands/v5-board.ts +++ b/src/term-commands/v5-board.ts @@ -16,6 +16,7 @@ import { cardBadges } from '../lib/v5/card-render.js'; import { openDb, resolveRepoRoot } from '../lib/v5/genie-db.js'; import { type BoardRow, + type BoardTaskAggregate, DEFAULT_LIFECYCLE_LANES, type Lane, type LaneTaskRow, @@ -23,6 +24,7 @@ import { type TaskFilter, type TaskRow, type TaskStatus, + boardDetailIdentifier, commentCounts, countBoardTasks, createBoard, @@ -31,6 +33,7 @@ import { listTasks, listTasksWithLane, moveTask, + readBoardTaskSnapshot, resolveBoard, } from '../lib/v5/task-state.js'; import { WISH_SLUG_PATTERN, extractStatusCell, readBoundedWishFile } from '../lib/wish-status.js'; @@ -281,6 +284,13 @@ function handleBoardWithDb(opts: BoardOptions): void { scopeLabel = opts.board ? `${scopeLabel}, wish "${opts.wish}"` : `wish "${opts.wish}"`; } + if (opts.json && board?.laneMetadataMalformed) { + throw new Error( + `Malformed board detail: board ${boardDetailIdentifier(board.id)} lanes must be an array or null.`, + ); + } + if (opts.json && board?.lanes) requireAggregateLanes(board.lanes); + // A scoped board that defines lanes renders on the lifecycle axis. Every // other scope (no board, or a laneless board) falls through to the frozen // status render below — kept byte-identical (Group B owns any rework). @@ -350,49 +360,38 @@ function groupByLane<T extends LaneTaskRow>(lanes: Lane[], tasks: T[]): Map<stri return byLane; } -/** - * One card on the additive lane `--json` path — the frozen ten TaskRow keys - * plus the two declared-routing fields and `lane` + `enforcedBlock`, picked - * explicitly so the lane shape states exactly what it serializes. Key order - * matches the pre-assignment spread, so lane output changes by exactly the two - * added fields; the TaskCardRow runtime layer (identity, heartbeat, block - * provenance) stays off this path. - */ -function toLaneJsonCard(t: LaneTaskRow): LaneTaskRow { - return { - id: t.id, - boardId: t.boardId, - title: t.title, - status: t.status, - claimedBy: t.claimedBy, - claimedAt: t.claimedAt, - wish: t.wish, - group: t.group, - assignedAgent: t.assignedAgent, - assignedReason: t.assignedReason, - createdAt: t.createdAt, - updatedAt: t.updatedAt, - lane: t.lane, - enforcedBlock: t.enforcedBlock, - }; +/** Validate persisted lane metadata at the scoped aggregate serialization boundary. */ +function requireAggregateLanes(lanes: Lane[]): void { + for (const [index, lane] of lanes.entries()) { + if (lane === null || typeof lane !== 'object' || Array.isArray(lane)) { + throw new Error(`Malformed board detail: lane ${index} must be an object.`); + } + if (typeof lane.name !== 'string') { + throw new Error(`Malformed board detail: lane ${index} name must be a string.`); + } + if (lane.label !== undefined && typeof lane.label !== 'string') { + throw new Error(`Malformed board detail: lane ${index} label must be a string when present.`); + } + if (lane.action !== undefined && typeof lane.action !== 'string') { + throw new Error(`Malformed board detail: lane ${index} action must be a string when present.`); + } + } } function renderLaneBoard(db: Database, lanes: Lane[], filter: TaskFilter, scopeLabel: string, json: boolean): void { - // `--json` keeps the additive lane shape. Its cards carry the two declared- - // routing fields (`assignedAgent`/`assignedReason`) plus exactly one runtime - // field beyond the frozen TaskRow — `enforcedBlock` (null when unblocked), so - // a lane consumer can tell a parked card from a live one and read who it is - // routed to. Identity, heartbeat, and block provenance stay off this path, - // and the frozen laneless `--json` remains byte-identical. + // A scoped lane board is the complete v1 aggregate contract. The repository + // reader returns all cards, dependencies, and events from one SQLite read + // transaction; grouping here only preserves the board's declared lane order. if (json) { - const byLane = groupByLane(lanes, listTasksWithLane(db, filter)); + if (!filter.boardId) throw new Error('A board id is required for aggregate JSON output.'); + const byLane = groupByLane<BoardTaskAggregate>(lanes, readBoardTaskSnapshot(db, filter.boardId, filter)); const laneGroups = lanes.map((l) => ({ name: l.name, label: l.label ?? null, action: l.action ?? null, - cards: (byLane.get(l.name) ?? []).map(toLaneJsonCard), + cards: byLane.get(l.name) ?? [], })); - out(JSON.stringify({ scope: scopeLabel, lanes: laneGroups }, null, 2)); + out(JSON.stringify({ schemaVersion: 1, scope: scopeLabel, lanes: laneGroups }, null, 2)); return; } From 6e928b75b9f8cfd7d9267eb657fa959943b686b2 Mon Sep 17 00:00:00 2001 From: Felipe Rosa <felipe@namastex.ai> Date: Mon, 7 Sep 2026 19:43:58 +0000 Subject: [PATCH 11/24] feat(dsh): add authenticated Genie board plugin Expose complete board snapshots and confirmed task actions through DSH Web. Co-authored-by: Felipe Rosa <felipe@namastex.ai> --- .genie/wishes/dsh-genie-board/WISH.md | 45 ++- knip.json | 11 +- package.json | 7 +- plugins/dsh-genie-board/NOTICE | 7 + plugins/dsh-genie-board/README.md | 76 +++++ plugins/dsh-genie-board/agent.cordis.yml | 2 + plugins/dsh-genie-board/build.test.ts | 34 ++ plugins/dsh-genie-board/build.ts | 21 ++ plugins/dsh-genie-board/cordis.patch.yml | 3 + plugins/dsh-genie-board/package.json | 16 + plugins/dsh-genie-board/src/board.test.ts | 395 ++++++++++++++++++++++ plugins/dsh-genie-board/src/client.ts | 277 +++++++++++++++ plugins/dsh-genie-board/src/index.ts | 113 +++++++ plugins/dsh-genie-board/src/process.ts | 94 +++++ plugins/dsh-genie-board/src/schema.ts | 91 +++++ plugins/dsh-genie-board/src/service.ts | 109 ++++++ plugins/dsh-genie-board/tsconfig.json | 10 + scripts/dsh-genie-board-smoke.ts | 172 ++++++++++ 18 files changed, 1472 insertions(+), 11 deletions(-) create mode 100644 plugins/dsh-genie-board/NOTICE create mode 100644 plugins/dsh-genie-board/README.md create mode 100644 plugins/dsh-genie-board/agent.cordis.yml create mode 100644 plugins/dsh-genie-board/build.test.ts create mode 100644 plugins/dsh-genie-board/build.ts create mode 100644 plugins/dsh-genie-board/cordis.patch.yml create mode 100644 plugins/dsh-genie-board/package.json create mode 100644 plugins/dsh-genie-board/src/board.test.ts create mode 100644 plugins/dsh-genie-board/src/client.ts create mode 100644 plugins/dsh-genie-board/src/index.ts create mode 100644 plugins/dsh-genie-board/src/process.ts create mode 100644 plugins/dsh-genie-board/src/schema.ts create mode 100644 plugins/dsh-genie-board/src/service.ts create mode 100644 plugins/dsh-genie-board/tsconfig.json create mode 100644 scripts/dsh-genie-board-smoke.ts diff --git a/.genie/wishes/dsh-genie-board/WISH.md b/.genie/wishes/dsh-genie-board/WISH.md index 75de6cf5d..96d599c29 100644 --- a/.genie/wishes/dsh-genie-board/WISH.md +++ b/.genie/wishes/dsh-genie-board/WISH.md @@ -143,7 +143,7 @@ Full gate is required because this changes a shared aggregate CLI contract. **Goal:** Deliver the secure, Host-confirmed DSH Web board experience. **Deliverables:** -1. Create `plugins/dsh-genie-board/package.json`, `agent.cordis.yml`, `cordis.patch.yml`, `README.md`, `NOTICE`, TypeScript/build configuration, source/tests, and the package-local `build` script. Freeze `dist/index.js` as the Host bundle and `dist/client.js` as the browser bundle; add root `build:plugin` as `bun --cwd plugins/dsh-genie-board run build`. +1. Create `plugins/dsh-genie-board/package.json`, `agent.cordis.yml`, `cordis.patch.yml`, `README.md`, `NOTICE`, TypeScript/build configuration, source/tests, and the package-local `build` script. Freeze `dist/index.js` as the Host bundle and `dist/client.js` as the browser bundle; add root `build:plugin` as `bun run --cwd plugins/dsh-genie-board build` (verified Bun invocation; the earlier flag ordering printed usage without building). 2. Implement a `minimumGenieVersion` plugin field and strict semver comparator without hard-coding a not-yet-published release. Source and linked-profile tests use the checkout root version; Group 3 stamps both the shipped plugin version and `minimumGenieVersion` from its already-derived immutable candidate. At Host startup run the fixed executable as `genie --no-interactive --version`; an older/unparseable version serves no board route. 3. Resolve a workspace id only through DSH `workspaceRegistry`; canonicalize the registry result with `realpath`, require a physical repository containing `.genie`, and use that canonical path as `cwd`. Never accept a browser path. Resolve the Genie executable once from the Host-owned installation, canonicalize it to an absolute executable regular file, and never search for or override it per request. 4. Spawn with `shell: false` and an exact Host-owned environment allowlist: `PATH`, `HOME`, `GENIE_HOME`, `NO_COLOR=1`, `GENIE_AGENT_NAME=<host-derived-identity>`, and `GENIE_AGENT_KIND=dsh`; drop every other variable and accept no environment value from the browser. @@ -155,7 +155,7 @@ Full gate is required because this changes a shared aggregate CLI contract. | Read board | `genie --no-interactive board --board <validated-ref> --json` | | Create | `genie --no-interactive task create --title <title> --board <ref>` | | Move | `genie --no-interactive task move <id> --to <lane>` | - | Comment | `genie --no-interactive task comment <id> <text>` | + | Comment | `genie --no-interactive task comment -- <id> <text>` | | Block | `genie --no-interactive task block <id> --reason <text> [--hold]` | | Unblock | `genie --no-interactive task unblock <id>` | | Checkout | `genie --no-interactive task checkout <id> --worker <host-derived-identity>` | @@ -167,12 +167,24 @@ Full gate is required because this changes a shared aggregate CLI contract. 8. Implement browser selectors, lanes/cards/details/errors, mutations, refresh, and visibility recovery; each mutation response performs one complete board aggregate re-read and never applies optimistic or partially hydrated state. 9. Add `scripts/dsh-genie-board-smoke.ts`: create isolated fixture repo/profile state, run `dsh plugin --profile web add link:<absolute-plugin-dir>`, launch `dsh web --no-open --host 127.0.0.1 --port 0`, stop/relaunch it after install, prove `dsh plugin --profile web list --depth 0` reports `@automagik/genie-dsh-board`, read back the plugin health/compatibility route, perform board list/load plus reversible create/move through the Host route, and in `finally` stop the server, run `dsh plugin --profile web remove @automagik/genie-dsh-board`, and delete only the temporary profile/repository. +**Execution clarification — bounded selection evidence (2026-09-07):** + +The fixed comment vector includes the standard `--` option terminator so valid comment text such as `--help` is stored as prose rather than interpreted as a CLI option. A real-CLI regression must verify the stored comment, not merely a successful exit or mocked argv. This is an owner-approved argument-boundary correction within the existing comment action. + +To satisfy the fixed subprocess budgets while validating selected identifiers, the Host may retain only validated board IDs and the task-ID/lane-name sets of one selected board per registered workspace. This is selection-validation evidence, not authentication or a response cache: no aggregate is stored or served, every returned board is freshly read, and there is no persistence, polling, TTL, queue or capability protocol. + +Bind evidence to workspace ID, canonical repository path and selected board ID. Allow one active operation per workspace (including list/load/mutation/refresh); reject overlaps with a retryable conflict. Replace evidence only after complete validation. Invalidate selected evidence on failures, registry removal, path rebinding and board changes. If mutation succeeds but refresh fails, report that the operation may have completed and require a new load; never retry automatically. Enforce verified DSH authentication and same-origin fencing before accessing evidence or spawning; reject missing/mismatched mutation Origin and unsupported content types. Membership reflects the latest confirmed selection; supported CLI mutations do not reassign tasks between boards, and concurrent state transitions retain CLI checks. + +Required tests cover concurrent board switches, mutation during load, invalidation after failures, workspace path rebinding, foreign task IDs, malformed refresh after successful mutation and cross-origin requests, with process-count assertions. This narrow allowance resolves the read-validation/process-budget tension; the prohibition on general caches and ledgers remains. + +Independent contract review: `/root/g1_review` required these exact concurrency/validity safeguards; owner incorporated them before implementation. Final G2 review must verify their implementation. Independent reread returned **SHIP** for the clarification, reviewed WISH SHA-256 `95f4cbb3cfe904117bece4d1b742591e7e2dc68514427b4200acd3d17b6db143` before this receipt was added; this is contract approval, not implementation acceptance. The installed DSH floor is now 0.1.2-rc.1 and must be proven by the real smoke. When no public sidebar extension exists, a labeled Genie launcher and accessible dialog mounted through public client apply/effect is an acceptable entry point; no DOM observer or private runtime API. + **Acceptance Criteria:** -- [ ] Rendering matches one complete fixture-backed Host aggregate; mutations use fixed argv and perform one complete refresh. -- [ ] Every unsafe/failure case is bounded and cannot invoke an out-of-scope command. +- [x] Rendering matches one complete fixture-backed Host aggregate; mutations use fixed argv and perform one complete refresh. +- [x] Every unsafe/failure case is bounded and cannot invoke an out-of-scope command. - [ ] The manifest and runtime reject every Genie version below the immutable candidate value stamped by Group 3, while source/linked tests prove the comparator against the checkout version without depending on a prior publication. -- [ ] Package build and the linked-profile install/restart/read-back/board-operation/cleanup smoke pass against DSH `0.1.1-rc.2` or a newer explicitly proven floor. -- [ ] No database access, persistence, watcher, poller, shell, or browser-supplied executable/path/argv exists. +- [x] Package build and the linked-profile install/restart/read-back/board-operation/cleanup smoke pass against DSH `0.1.1-rc.2` or a newer explicitly proven floor. +- [x] No database access, persistence, watcher, poller, shell, or browser-supplied executable/path/argv exists. **Validation:** ```bash @@ -333,6 +345,27 @@ Corrective route: resolve or formally clear the repository-baseline failures, th - **Dogfood:** built `dist/genie.js` against an isolated HOME and repository; board creation, task creation, comment, move and aggregate read returned the expected lane, comment and timeline. No personal profile or repository changed. - G1 code review, full validation and built-CLI smoke are accepted. G2/G3 and stable publication remain pending; this is not whole-wish release acceptance. +### G2 implementation review — 2026-09-07T19:33Z — FIX-FIRST, round 1 + +- Independent reviewer `/root/g2_review` inspected all 13 plugin files, smoke and root configuration against HEAD `9cef95e4ccc5e9c597d16fdf3d7dec1598b8640b`; reviewed content SHA-256 `a48a527744ebb5c02f8915aaa3085e7502ca3d31a052433b678dcdbcf2e05ae1`. +- P1: exact Host routes bypassed DSH's authenticated `/api` prefix. Require the installed public `connection.requestRejection(req)` browser-cookie check before every route, in addition to Origin/loopback fencing. The original unauthenticated smoke is not acceptance evidence. +- P2: option-shaped comment text could exit successfully without writing a comment. The owner approved the fixed `--` argument terminator above; regression must read back the literal stored text through the real CLI. +- Focused independent run: 13 pass, 161 assertions. The first browser attempt did not mount the launcher and supplies no visual acceptance. Engineer is correcting both findings and repeating authenticated smoke/rendered validation; final review and current full gate remain pending. + +### G2 implementation re-review — 2026-09-07 — SHIP, fix loop 1 + +- Independent reviewer `/root/g2_review` returned **SHIP**, no unresolved findings, on all 17 files in frozen source manifest SHA-256 `7395d3c0064d65fc8a568527df755859753bfe51336d4d8b978d6eadb328783a`, atop HEAD `9cef95e4ccc5e9c597d16fdf3d7dec1598b8640b`. Owner independently verified every manifest entry. +- P1 closed: every route applies public DSH connection authentication before workspace access or operations; real smoke exchanges the launch token and proves missing/invalid cookies return 401. P2 closed: real CLI writes the literal `--help` comment using the fixed option terminator; rendered details confirm it. +- Independent focused validation: **19 pass, 249 assertions, zero failures**, including regenerated browser factory. Reviewed fixed actions, selection/concurrency invalidation, valid canonical-path rebinding, refresh failure, process/output budgets and authentication boundaries. +- Real installed DSH **0.1.2-rc.1** smoke proves installation, restart, authenticated operations and cleanup. Reviewer and owner inspected wide/narrow/history images; browser evidence also proves card details, comment submission, horizontal lane scroll, history access, Escape and restored focus. +- Parent full gate is running on the frozen repaired files. This receipt accepts code and rendered behavior; group completion still requires that current full gate. + +### G2 owner acceptance — 2026-09-07 + +- Parent `bun run check` on the repaired frozen source exited **0**: **2009 pass / 1 skip / 0 fail**, 8653 assertions across 98 files, 270.63 seconds. Scope is the repository-mandated full gate for runtime, trust-boundary and build/configuration changes. +- Parent `bun run build:plugin`, focused plugin tests (**19 pass / 249 assertions**) and real `bun scripts/dsh-genie-board-smoke.ts` each exited **0**. The owner smoke separately proves authenticated install/list/restart/health/load/create/move and literal option-shaped comment, followed by plugin removal and temporary-state cleanup. +- Combined with independent code/security/quality and rendered **SHIP**, Group 2 implementation is accepted. The comparator is proven for source/linked builds; immutable candidate stamping and extracted runtime-floor proof remain explicitly owned by Group 3, so the combined future-artifact checkbox above remains open. + --- ## Files to Create/Modify diff --git a/knip.json b/knip.json index e4a8267f8..64c7d3921 100644 --- a/knip.json +++ b/knip.json @@ -1,7 +1,14 @@ { "$schema": "https://unpkg.com/knip@6.29.0/schema.json", - "entry": ["skills/genie-orca-work/scripts/*.ts"], - "project": ["skills/genie-orca-work/scripts/**/*.ts", "src/**/*.ts"], + "entry": [ + "skills/genie-orca-work/scripts/*.ts", + "plugins/dsh-genie-board/src/index.ts", + "plugins/dsh-genie-board/src/client.ts", + "plugins/dsh-genie-board/build.ts", + "plugins/dsh-genie-board/src/*.test.ts", + "plugins/dsh-genie-board/build.test.ts" + ], + "project": ["skills/genie-orca-work/scripts/**/*.ts", "src/**/*.ts", "plugins/dsh-genie-board/src/**/*.ts"], "ignoreBinaries": ["ldd", "omni"], "ignoreExportsUsedInFile": true } diff --git a/package.json b/package.json index 45f2ad25e..d093ab15b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@automagik/genie", "version": "5.260901.3", - "description": "Collaborative terminal toolkit for human + AI workflows. NOTE: npm distribution discontinued 2026-05-09 — install via `curl -fsSL https://raw.githubusercontent.com/automagik-dev/genie/main/install.sh | bash` (cosign + SLSA verified). See https://automagik.dev/genie/release-process", + "description": "Collaborative terminal toolkit for human + AI workflows. NOTE: npm distribution discontinued 2026-05-09 \u2014 install via `curl -fsSL https://raw.githubusercontent.com/automagik-dev/genie/main/install.sh | bash` (cosign + SLSA verified). See https://automagik.dev/genie/release-process", "license": "MIT", "type": "module", "bin": { @@ -19,7 +19,7 @@ "lint:docs-markdown": "ls docs/incident-response/canisterworm.mdx docs/installation.mdx docs/release-notes.mdx > /dev/null && markdownlint-cli2 SECURITY.md docs/incident-response/canisterworm.mdx docs/installation.mdx docs/release-notes.mdx", "format": "biome format --write .", "test": "bun test", - "typecheck": "tsc --noEmit", + "typecheck": "tsc --noEmit && tsc --noEmit -p plugins/dsh-genie-board/tsconfig.json", "dead-code": "bunx knip", "skills:lint": "bun run scripts/skills-lint.ts", "wishes:lint": "bun run scripts/wishes-lint.ts", @@ -28,7 +28,8 @@ "lint:orca-bundle": "bun scripts/orca-bundle-parity.ts --check", "check": "bun run typecheck && bun run lint && bun run dead-code && bun run skills:lint && bun run wishes:lint && bun run lint:complexity-budget && bun run lint:orca-bundle && bun test", "check:fast": "bun run typecheck && bun run lint && bun run dead-code && bun run skills:lint && bun run wishes:lint && bun run lint:complexity-budget && bun run lint:orca-bundle", - "verify:release": "scripts/verify-release.sh" + "verify:release": "scripts/verify-release.sh", + "build:plugin": "bun run --cwd plugins/dsh-genie-board build" }, "dependencies": { "@inquirer/prompts": "7.10.1", diff --git a/plugins/dsh-genie-board/NOTICE b/plugins/dsh-genie-board/NOTICE new file mode 100644 index 000000000..a06105d3b --- /dev/null +++ b/plugins/dsh-genie-board/NOTICE @@ -0,0 +1,7 @@ +Genie DSH Board +Copyright 2026 Namastex Labs. Licensed under the repository MIT license. + +Original plugin implementation for the public Cordis lifecycle, DSH workspace +registry and DSH WebServer interfaces. DSH is developed by DeepSeek AI and is +not part of this package. No implementation from the separate dsh-genie project +is included. The Host bundle includes Zod (MIT), copyright Colin McDonnell. diff --git a/plugins/dsh-genie-board/README.md b/plugins/dsh-genie-board/README.md new file mode 100644 index 000000000..83597c1ce --- /dev/null +++ b/plugins/dsh-genie-board/README.md @@ -0,0 +1,76 @@ +# Genie board for DSH Web + +Open **Genie board** from the button in DSH Web. Choose a registered repository +workspace and board to view its lanes, cards, owners, activity, blocks, dependencies, +comments, and history. Select a card to move, comment, block/hold, unblock, claim, +release, or complete it. Create adds a task to the selected board. Refresh and +returning to the visible tab read a complete board again; there is no polling. + +## Build and install + +Requires Bun (checkout builds), a Host-installed Genie CLI, and DSH +**0.1.2-rc.1 or newer**. This floor was exercised with the actual installed Host. + +```sh +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 `dist/index.js` (Host) and `dist/client.js` (browser). +`minimumGenieVersion` in package.json is the checkout version for source builds; +the release packaging step stamps it and the package version together. 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 from its installation PATH, canonicalizes the +executable, 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 loopback and same-origin browser signals; mutations require exact Origin +and application/json. 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. Selection errors, failed operations, workspace removal and path changes +invalidate evidence. 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. Closed schemas reject +incomplete or foreign aggregates. Request bodies are limited to 16 KiB. Laneless +legacy boards do not expose the required complete aggregate and fail explicitly. +The plugin has no database access, persistence, filesystem watcher, task poller, +or autonomous agent runner. + +DSH's installed sidebar package exposes no extension slot. The original client +uses its public apply/effect lifecycle to mount an accessible modal board button +without patching DSH sources or observing its DOM. + +## Validation + +```sh +bun run check +bun run build:plugin +bun test plugins/dsh-genie-board +bun scripts/dsh-genie-board-smoke.ts +``` + +The smoke installs into a disposable DSH_HOME, registers a disposable repository +through the real workspace registry, starts/stops/restarts DSH, verifies plugin +listing and compatibility, creates and moves a task through Host routes, then +removes the plugin and temporary state in `finally`. Personal profiles are not used. diff --git a/plugins/dsh-genie-board/agent.cordis.yml b/plugins/dsh-genie-board/agent.cordis.yml new file mode 100644 index 000000000..bb351ac05 --- /dev/null +++ b/plugins/dsh-genie-board/agent.cordis.yml @@ -0,0 +1,2 @@ +- id: genie-dsh-board + name: '@automagik/genie-dsh-board' diff --git a/plugins/dsh-genie-board/build.test.ts b/plugins/dsh-genie-board/build.test.ts new file mode 100644 index 000000000..8ebd47763 --- /dev/null +++ b/plugins/dsh-genie-board/build.test.ts @@ -0,0 +1,34 @@ +import { expect, test } from 'bun:test'; +import { readFile, rm, stat } from 'node:fs/promises'; +import { join } from 'node:path'; +import { runInNewContext } from 'node:vm'; + +test('build regenerates Host bundle and lazy DSH browser factory', async () => { + const root = import.meta.dir; + const host = join(root, 'dist/index.js'); + const client = join(root, 'dist/client.js'); + await rm(host, { force: true }); + await rm(client, { force: true }); + const process = Bun.spawn(['bun', 'run', 'build'], { cwd: root, stdout: 'pipe', stderr: 'pipe' }); + const [code, error] = await Promise.all([process.exited, new Response(process.stderr).text()]); + expect(error).not.toContain('error:'); + expect(code).toBe(0); + expect((await stat(host)).size).toBeGreaterThan(1000); + let registration: { id: string; factory: (require: unknown) => { apply: unknown } } | undefined; + runInNewContext(await readFile(client, 'utf8'), { + window: { + __ModuleLoader__: { + load(value: typeof registration) { + registration = value; + }, + }, + }, + }); + expect(registration?.id).toBe('@automagik/genie-dsh-board'); + // No DOM is needed until Cordis activates the factory's apply method. + expect( + typeof registration?.factory(() => { + throw new Error('Unexpected browser dependency'); + }).apply, + ).toBe('function'); +}, 20_000); diff --git a/plugins/dsh-genie-board/build.ts b/plugins/dsh-genie-board/build.ts new file mode 100644 index 000000000..3ad342aab --- /dev/null +++ b/plugins/dsh-genie-board/build.ts @@ -0,0 +1,21 @@ +import { build } from 'esbuild'; +await build({ + entryPoints: ['src/index.ts'], + bundle: true, + platform: 'node', + format: 'esm', + target: 'node22', + outfile: 'dist/index.js', +}); +await build({ + entryPoints: ['src/client.ts'], + bundle: true, + platform: 'browser', + format: 'cjs', + banner: { + js: 'window.__ModuleLoader__.load({ id: "@automagik/genie-dsh-board", factory: (require) => { const module = { exports: {} }; const exports = module.exports;', + }, + footer: { js: 'return module.exports; } });' }, + target: 'es2022', + outfile: 'dist/client.js', +}); diff --git a/plugins/dsh-genie-board/cordis.patch.yml b/plugins/dsh-genie-board/cordis.patch.yml new file mode 100644 index 000000000..85488f8e4 --- /dev/null +++ b/plugins/dsh-genie-board/cordis.patch.yml @@ -0,0 +1,3 @@ +- insert: + - id: genie-dsh-board + name: '@automagik/genie-dsh-board' diff --git a/plugins/dsh-genie-board/package.json b/plugins/dsh-genie-board/package.json new file mode 100644 index 000000000..ca86659e6 --- /dev/null +++ b/plugins/dsh-genie-board/package.json @@ -0,0 +1,16 @@ +{ + "name": "@automagik/genie-dsh-board", + "version": "5.260901.3", + "minimumGenieVersion": "5.260901.3", + "license": "MIT", + "type": "module", + "main": "dist/index.js", + "exports": { ".": "./dist/index.js", "./client": "./dist/client.js", "./package.json": "./package.json" }, + "files": ["dist", "agent.cordis.yml", "cordis.patch.yml", "README.md", "NOTICE"], + "scripts": { "build": "bun run build.ts", "typecheck": "tsc --noEmit -p tsconfig.json" }, + "dsh": { + "engines": { "dsh": ">=0.1.2-rc.1" }, + "bundle": { "patch": "./cordis.patch.yml" }, + "client": { "platform": "web" } + } +} diff --git a/plugins/dsh-genie-board/src/board.test.ts b/plugins/dsh-genie-board/src/board.test.ts new file mode 100644 index 000000000..dae788dbb --- /dev/null +++ b/plugins/dsh-genie-board/src/board.test.ts @@ -0,0 +1,395 @@ +import { afterEach, describe, expect, test } from 'bun:test'; +import { mkdir, mkdtemp, rm } from 'node:fs/promises'; +import type { IncomingMessage } from 'node:http'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { minimumGenieVersion, trusted } from './index'; +import { MAX_OUTPUT, compatible, execute, hostEnvironment } from './process'; +import { aggregateSchema, requestSchema } from './schema'; +import { BoardService, actionArgs } from './service'; + +const directories: string[] = []; +afterEach(async () => { + for (const directory of directories.splice(0)) await rm(directory, { recursive: true, force: true }); +}); +const card = { + id: 't_abc', + boardId: 'b_abc', + title: 'Task', + status: 'ready', + claimedBy: null, + claimedAt: null, + wish: null, + group: null, + assignedAgent: null, + assignedReason: null, + createdAt: 1, + updatedAt: 1, + lane: 'Ready', + enforcedBlock: null, + agentKind: null, + heartbeatAt: null, + blockedBy: null, + blockedReason: null, + liveness: null, + dependencies: [], + timeline: [], + comments: [], +}; +const aggregate = { + schemaVersion: 1, + scope: 'Board', + lanes: [ + { name: 'Ready', label: null, action: null, cards: [card] }, + { name: 'Done', label: null, action: null, cards: [] }, + ], +}; +const boards = [{ id: 'b_abc', name: 'Board', laneCount: 2, cardCount: 1 }]; +const selection = { workspaceId: 'workspace', boardRef: 'b_abc' }; +async function fixture() { + const path = await mkdtemp(join(tmpdir(), 'genie-plugin-test-')); + directories.push(path); + await mkdir(join(path, '.git')); + await mkdir(join(path, '.genie')); + const workspaces = [{ id: 'workspace', path, title: 'Workspace' }]; + const calls: string[][] = []; + let output: unknown = aggregate; + let listed = boards; + let failure = false; + const service = new BoardService({ list: () => workspaces }, '/fixed/genie', async (_binary, argv, cwd, env) => { + expect(cwd).toBe(path); + expect(env.GENIE_AGENT_KIND).toBe('dsh'); + calls.push(argv); + if (failure) throw new Error('failure'); + return JSON.stringify(argv[0] === 'board' && argv[1] === 'list' ? listed : output); + }); + const list = () => service.request({ action: 'list', workspaceId: 'workspace' }); + const load = () => service.request({ action: 'load', ...selection }); + return { + service, + calls, + workspaces, + list, + load, + setBoards(value: typeof boards) { + listed = value; + }, + setOutput(value: unknown) { + output = value; + }, + fail() { + failure = true; + }, + }; +} +describe('closed inputs and fixed argv', () => { + test('all mutation argv are fixed; title shell syntax remains a single argument', () => { + expect( + actionArgs( + requestSchema.parse({ action: 'create', ...selection, title: '$(touch /tmp/bad); hi' }) as never, + 'host', + ), + ).toEqual(['task', 'create', '--title', '$(touch /tmp/bad); hi', '--board', 'b_abc']); + const vectors = [ + ['move', { lane: 'Done' }, ['task', 'move', 't_abc', '--to', 'Done']], + ['comment', { text: 'hello' }, ['task', 'comment', '--', 't_abc', 'hello']], + ['block', { text: 'reason', hold: true }, ['task', 'block', 't_abc', '--reason', 'reason', '--hold']], + ['checkout', {}, ['task', 'checkout', 't_abc', '--worker', 'host']], + ...['unblock', 'release', 'done'].map((action) => [action, {}, ['task', action, 't_abc']]), + ]; + for (const [action, extra, argv] of vectors) + expect( + actionArgs(requestSchema.parse({ action, ...selection, id: 't_abc', ...(extra as object) }) as never, 'host'), + ).toEqual(argv as string[]); + }); + test('rejects injected authority, identifiers, hold types and controls', () => { + const input = { action: 'block', ...selection, id: 't_abc', text: 'reason' }; + for (const key of ['worker', 'path', 'executable', 'environment', 'command', 'argv', 'unknown']) + expect(requestSchema.safeParse({ ...input, [key]: 'evil' }).success).toBe(false); + for (const id of ['--help', 't_abc;evil', '../t_abc', 't_ABC', 't_abc\0']) + expect(requestSchema.safeParse({ ...input, id }).success).toBe(false); + for (const hold of ['true', 1, null]) expect(requestSchema.safeParse({ ...input, hold }).success).toBe(false); + for (const text of ['', ' ', 'x\0x', 'x\nx', 'x\tx', '\ntitle', 'title\n']) + expect(requestSchema.safeParse({ ...input, text }).success).toBe(false); + }); + test('byte bounds for title/comment/reason', () => { + for (const [action, field, limit] of [ + ['create', 'title', 200], + ['comment', 'text', 4000], + ['block', 'text', 1000], + ] as const) { + const base = { action, ...selection, ...(action === 'create' ? {} : { id: 't_abc' }) }; + expect(requestSchema.safeParse({ ...base, [field]: 'é'.repeat(limit / 2) }).success).toBe(true); + expect(requestSchema.safeParse({ ...base, [field]: `${'é'.repeat(limit / 2)}x` }).success).toBe(false); + } + }); +}); +test('complete closed aggregate rejects missing detail, unknown keys, duplicate cards/lanes', () => { + expect(aggregateSchema.safeParse(aggregate).success).toBe(true); + for (const field of Object.keys(card)) { + const changed = { ...card }; + delete changed[field as keyof typeof changed]; + expect( + aggregateSchema.safeParse({ ...aggregate, lanes: [{ ...aggregate.lanes[0], cards: [changed] }] }).success, + ).toBe(false); + } + expect(aggregateSchema.safeParse({ ...aggregate, extra: true }).success).toBe(false); + expect(aggregateSchema.safeParse({ ...aggregate, lanes: [aggregate.lanes[0], aggregate.lanes[0]] }).success).toBe( + false, + ); +}); +test('list and load each use one process; all mutations use exactly mutation+aggregate', async () => { + const f = await fixture(); + await f.list(); + expect(f.calls.length).toBe(1); + await f.load(); + expect(f.calls.length).toBe(2); + for (const action of ['checkout', 'release', 'unblock', 'done']) { + const before = f.calls.length; + await f.service.request({ action, ...selection, id: 't_abc' }); + expect(f.calls.length - before).toBe(2); + expect(f.calls.at(-1)).toEqual(['board', '--board', 'b_abc', '--json']); + } +}); +test('foreign workspace, board, task and lane never execute', async () => { + for (const extra of [ + { workspaceId: 'foreign' }, + { boardRef: 'b_foreign' }, + { id: 't_foreign' }, + { lane: 'Foreign' }, + ]) { + const f = await fixture(); + await f.list(); + await f.load(); + const before = f.calls.length; + await expect( + f.service.request({ action: 'move', ...selection, id: 't_abc', lane: 'Done', ...extra }), + ).rejects.toThrow(); + expect(f.calls.length).toBe(before); + } +}); +test('failed mutation refresh invalidates selection and reports possible completion', async () => { + const f = await fixture(); + await f.list(); + await f.load(); + f.setOutput({}); + await expect(f.service.request({ action: 'done', ...selection, id: 't_abc' })).rejects.toThrow('may have completed'); + const before = f.calls.length; + await expect(f.service.request({ action: 'done', ...selection, id: 't_abc' })).rejects.toThrow(); + expect(f.calls.length).toBe(before); +}); +test('registry removal and path rebinding invalidate evidence', async () => { + const f = await fixture(); + await f.list(); + await f.load(); + f.workspaces[0].path = '/nonexistent'; + await expect(f.load()).rejects.toThrow(); + expect(f.calls.length).toBe(2); + f.workspaces.splice(0); + await expect(f.list()).rejects.toThrow('Unknown workspace'); +}); +test('overlapping load and mutation are rejected without spawning', async () => { + const f = await fixture(); + await f.list(); + await f.load(); + const load = f.load(); + await expect(f.service.request({ action: 'done', ...selection, id: 't_abc' })).rejects.toThrow('in progress'); + await load; + expect(f.calls.length).toBe(3); +}); +test('loopback and exact same-origin mutation fence', () => { + const req = { + method: 'POST', + socket: { remoteAddress: '127.0.0.1' }, + headers: { host: '127.0.0.1:1234', origin: 'http://127.0.0.1:1234' }, + }; + expect(trusted(req as IncomingMessage)).toBe(true); + for (const origin of [undefined, 'null', 'https://evil.test', 'http://127.0.0.1:1235']) + expect(trusted({ ...req, headers: { ...req.headers, origin } } as IncomingMessage)).toBe(false); + expect(trusted({ ...req, socket: { remoteAddress: '10.0.0.1' } } as IncomingMessage)).toBe(false); +}); +test('semver strict ordering includes prereleases and rejects malformed versions', () => { + for (const [actual, minimum, result] of [ + ['5.260901.3', '5.260901.3', true], + ['5.260901.2', '5.260901.3', false], + ['5.260901.4', '5.260901.3', true], + ['1.0.0-rc.2', '1.0.0-rc.1', true], + ['1.0.0-rc.2', '1.0.0', false], + ['1.0.0', '1.0.0-rc.2', true], + ['1.0.0-01', '1.0.0', false], + ['v1.0.0', '1.0.0', false], + ['01.0.0', '1.0.0', false], + ] as const) + expect(compatible(actual, minimum)).toBe(result); +}); +test('process environment is an exact allowlist', () => { + expect( + Object.keys(hostEnvironment('host')).every((key) => + ['PATH', 'HOME', 'GENIE_HOME', 'NO_COLOR', 'GENIE_AGENT_NAME', 'GENIE_AGENT_KIND'].includes(key), + ), + ).toBe(true); +}); +test('aggregate deadline and output budgets kill child or reject before spawn', async () => { + await expect(execute('/missing', [], '.', {}, { expires: 0, bytes: 0 })).rejects.toThrow('deadline'); + // Node treats the mandatory Genie flag as invalid; a tiny executable fixture consumes it. + const path = await mkdtemp(join(tmpdir(), 'genie-process-test-')); + directories.push(path); + const { writeFile } = await import('node:fs/promises'); + const file = join(path, 'genie'); + await writeFile(file, '#!/bin/sh\nprintf 1234567890\n', { mode: 0o755 }); + await expect(execute(file, [], path, {}, { expires: Date.now() + 1000, bytes: MAX_OUTPUT - 5 })).rejects.toThrow( + 'output limit', + ); + await writeFile(file, '#!/bin/sh\nexec sleep 5\n', { mode: 0o755 }); + await expect( + execute(file, [], path, { PATH: process.env.PATH }, { expires: Date.now() + 20, bytes: 0 }), + ).rejects.toThrow('deadline'); + await expect(execute('/missing', [], path, {}, { expires: Date.now() + 1000, bytes: 0 })).rejects.toThrow(); +}); + +test('all action service vectors refresh once, including option-shaped comments', async () => { + const f = await fixture(); + await f.list(); + await f.load(); + const requests = [ + { action: 'create', title: 'New task' }, + { action: 'move', id: 't_abc', lane: 'Done' }, + { action: 'comment', id: 't_abc', text: '--help' }, + { action: 'block', id: 't_abc', text: 'reason', hold: true }, + ...['unblock', 'checkout', 'release', 'done'].map((action) => ({ action, id: 't_abc' })), + ]; + for (const input of requests) { + const before = f.calls.length; + await f.service.request({ ...selection, ...input }); + expect(f.calls.slice(before)).toEqual([ + actionArgs(requestSchema.parse({ ...selection, ...input }) as never, f.service.identity), + ['board', '--board', 'b_abc', '--json'], + ]); + } +}); +test('valid canonical directory rebinding rejects and concurrent selection switches cannot race', async () => { + const f = await fixture(); + const second = await fixture(); + await f.list(); + await f.load(); + f.workspaces[0].path = second.workspaces[0].path; + await expect(f.load()).rejects.toThrow('Workspace changed'); + expect(f.calls.length).toBe(2); + const g = await fixture(); + await g.list(); + const pending = g.load(); + await expect(g.list()).rejects.toThrow('in progress'); + await pending; + expect(g.calls.length).toBe(2); +}); +test('competing distinct board loads preserve only the winner task membership', async () => { + const f = await fixture(); + f.setBoards([...boards, { ...boards[0], id: 'b_def' }]); + await f.list(); + await f.load(); + f.setOutput({ + ...aggregate, + lanes: [{ ...aggregate.lanes[0], cards: [{ ...card, id: 't_def', boardId: 'b_def' }] }], + }); + const pending = f.service.request({ action: 'load', workspaceId: 'workspace', boardRef: 'b_def' }); + await expect(f.load()).rejects.toThrow('in progress'); + await pending; + await f.service.request({ action: 'done', workspaceId: 'workspace', boardRef: 'b_def', id: 't_def' }); + const before = f.calls.length; + await expect( + f.service.request({ action: 'done', workspaceId: 'workspace', boardRef: 'b_def', id: 't_abc' }), + ).rejects.toThrow('outside'); + expect(f.calls.length).toBe(before); +}); +test('stdout plus stderr share one budget across sequential processes', async () => { + const path = await mkdtemp(join(tmpdir(), 'genie-budget-test-')); + directories.push(path); + const { writeFile } = await import('node:fs/promises'); + const binary = join(path, 'genie'); + await writeFile(binary, '#!/bin/sh\nprintf 12345\nprintf 12345 >&2\n', { mode: 0o755 }); + const budget = { expires: Date.now() + 1000, bytes: MAX_OUTPUT - 15 }; + expect(await execute(binary, [], path, {}, budget)).toBe('12345'); + expect(budget.bytes).toBe(MAX_OUTPUT - 5); + await expect(execute(binary, [], path, {}, budget)).rejects.toThrow('output limit'); +}); +test('Host routes apply DSH authentication and Origin/content-type fences before reading workspaces or spawning', async () => { + const { apply } = await import('./index'); + const { createServer } = await import('node:http'); + const { writeFile, readFile } = await import('node:fs/promises'); + const path = await mkdtemp(join(tmpdir(), 'genie-auth-test-')); + directories.push(path); + const calls = join(path, 'calls'); + await writeFile(join(path, 'genie'), `#!/bin/sh\nprintf x >> '${calls}'\nprintf '${minimumGenieVersion}\\n'\n`, { + mode: 0o755, + }); + const routes = new Map<string, (req: IncomingMessage, res: import('node:http').ServerResponse) => Promise<void>>(); + let registryReads = 0; + const oldPath = process.env.PATH; + try { + process.env.PATH = path; + await apply({ + workspaceRegistry: { + list() { + registryReads++; + return []; + }, + }, + connection: { + requestRejection(req) { + return req.headers.cookie === 'session=valid' ? undefined : 401; + }, + }, + webServer: { + register(route) { + routes.set(route.path, route.handler); + return () => routes.delete(route.path); + }, + }, + effect(effect) { + effect(); + }, + }); + } finally { + process.env.PATH = oldPath; + } + const server = createServer((req, res) => { + const handler = routes.get(req.url ?? ''); + if (handler) void handler(req, res); + else { + res.writeHead(404); + res.end(); + } + }); + await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve)); + const address = server.address(); + if (!address || typeof address === 'string') throw new Error('No address'); + const origin = `http://127.0.0.1:${address.port}`; + try { + for (const path of ['health', 'workspaces', 'action']) { + for (const cookie of ['', 'session=invalid']) { + const response = await fetch(`${origin}/api/genie-board/${path}`, { + method: path === 'action' ? 'POST' : 'GET', + headers: { origin, cookie, 'content-type': 'application/json' }, + ...(path === 'action' ? { body: '{}' } : {}), + }); + expect(response.status).toBe(401); + } + } + for (const [headers, code] of [ + [{ origin: 'https://evil.test', 'content-type': 'application/json' }, 403], + [{ 'content-type': 'application/json' }, 403], + [{ origin, 'content-type': 'text/plain' }, 415], + ] as const) { + const response = await fetch(`${origin}/api/genie-board/action`, { + method: 'POST', + headers: { cookie: 'session=valid', ...headers }, + body: '{}', + }); + expect(response.status).toBe(code); + } + expect(registryReads).toBe(0); + expect(await readFile(calls, 'utf8')).toBe('x'); + } finally { + await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))); + } +}); diff --git a/plugins/dsh-genie-board/src/client.ts b/plugins/dsh-genie-board/src/client.ts new file mode 100644 index 000000000..33f80aa16 --- /dev/null +++ b/plugins/dsh-genie-board/src/client.ts @@ -0,0 +1,277 @@ +import type { Aggregate } from './schema'; + +interface ClientContext { + effect(effect: () => () => void, label?: string): void; +} +const css = ` +.genie-launch{position:fixed;right:20px;bottom:20px;z-index:9000;padding:12px 18px;background:#185a4e;color:white;border:1px solid #398774;border-radius:8px;font:600 15px system-ui;cursor:pointer} +.genie-board{box-sizing:border-box;width:min(1400px,96vw);height:90vh;padding:0;border:1px solid #687a74;border-radius:10px;color:#1d2925;background:#f5f7f6;font:15px system-ui}.genie-board::backdrop{background:#10251cb0}.genie-board *{box-sizing:border-box}.genie-board header{padding:20px 24px;border-bottom:1px solid #cdd8d2;display:flex;align-items:center;gap:14px;flex-wrap:wrap}.genie-board h1{font-size:22px;margin:0 auto 0 0}.genie-board h2{font-size:16px;margin:0 0 16px}.genie-board button,.genie-board select,.genie-board input,.genie-board textarea{font:inherit;border:1px solid #9bafa4;border-radius:5px;padding:8px;background:white;color:inherit}.genie-board button{cursor:pointer}.genie-board button:hover{background:#e4eee8}.genie-board :focus-visible{outline:3px solid #347a69;outline-offset:2px}.genie-board button:disabled{opacity:.55;cursor:wait}.genie-board label{display:flex;gap:6px;align-items:center}.genie-status{min-height:24px;padding:12px 24px}.genie-status[role=alert]{color:#9d2525}.genie-content{display:flex;min-height:60vh;overflow:auto}.genie-lanes{display:flex;gap:16px;padding:0 24px 24px;flex:1;overflow:auto;align-items:flex-start}.genie-lane{flex:1;min-width:220px}.genie-card{display:block;text-align:left;width:100%;margin:0 0 10px;border-left:3px solid #34816b!important;padding:14px!important}.genie-card small{display:block;color:#4c6258;margin-top:8px}.genie-detail{width:360px;flex-shrink:0;padding:0 24px 24px;border-left:1px solid #cdd8d2;overflow:auto}.genie-detail p{white-space:pre-wrap;overflow-wrap:anywhere}.genie-detail h3{font-size:15px;margin:22px 0 8px}.genie-detail textarea{width:100%;min-height:80px}.genie-actions{display:flex;gap:7px;flex-wrap:wrap;margin:10px 0}.genie-create{display:flex;gap:8px;padding:0 24px 20px}.genie-create input{flex:1;min-width:0}.genie-empty{color:#52665b;line-height:1.6}.genie-detail ol{padding-left:20px}.genie-detail li{margin-bottom:12px;overflow-wrap:anywhere}@media(max-width:720px){.genie-board{width:100vw;height:100dvh;max-height:none;border-radius:0}.genie-content{display:block}.genie-detail{width:100%;border-left:0;border-top:1px solid #cdd8d2;padding-top:20px}.genie-board header{padding:16px}.genie-board label{width:100%}.genie-board select{flex:1;min-width:0}.genie-lanes{padding-left:16px}.genie-lane{min-width:230px}.genie-launch{bottom:12px;right:12px}} +`; +function element<K extends keyof HTMLElementTagNameMap>(tag: K, text?: string): HTMLElementTagNameMap[K] { + const node = document.createElement(tag); + if (text !== undefined) node.textContent = text; + return node; +} +async function api(path: string, body?: unknown): Promise<unknown> { + const response = await fetch( + `/api/genie-board/${path}`, + body === undefined + ? {} + : { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }, + ); + const value = await response.json(); + if (!response.ok) throw new Error(value.error ?? 'Request failed'); + return value; +} +export function apply(ctx: ClientContext): void { + ctx.effect(() => { + const style = element('style', css); + const launch = element('button', 'Genie board'); + launch.className = 'genie-launch'; + const dialog = element('dialog'); + dialog.className = 'genie-board'; + dialog.setAttribute('aria-label', 'Genie board'); + const header = element('header'); + header.append(element('h1', 'Genie board')); + const workspace = element('select'); + workspace.setAttribute('aria-label', 'Workspace'); + const board = element('select'); + board.setAttribute('aria-label', 'Board'); + for (const [name, control] of [ + ['Workspace', workspace], + ['Board', board], + ] as const) { + const label = element('label', name); + label.append(control); + header.append(label); + } + const refresh = element('button', 'Refresh'); + const close = element('button', 'Close'); + header.append(refresh, close); + const status = element('div'); + status.className = 'genie-status'; + status.setAttribute('role', 'status'); + status.setAttribute('aria-live', 'polite'); + const create = element('form'); + create.className = 'genie-create'; + const title = element('input'); + title.placeholder = 'New task title'; + title.setAttribute('aria-label', 'New task title'); + title.required = true; + const add = element('button', 'Create task'); + create.append(title, add); + const content = element('div'); + content.className = 'genie-content'; + const lanes = element('div'); + lanes.className = 'genie-lanes'; + const detail = element('aside'); + detail.className = 'genie-detail'; + content.append(lanes, detail); + dialog.append(header, status, create, content); + document.head.append(style); + document.body.append(launch, dialog); + let snapshot: Aggregate | undefined; + let selected: string | undefined; + let busy = false; + const showStatus = (message: string, error = false) => { + status.textContent = message; + status.setAttribute('role', error ? 'alert' : 'status'); + }; + const run = async (operation: () => Promise<void>) => { + if (busy) return; + busy = true; + for (const control of dialog.querySelectorAll<HTMLButtonElement | HTMLSelectElement>('button,select')) + control.disabled = true; + showStatus('Loading…'); + try { + await operation(); + showStatus('Board confirmed by Genie'); + } catch (error) { + snapshot = undefined; + lanes.replaceChildren(); + detail.replaceChildren(); + showStatus(error instanceof Error ? error.message : 'Request failed', true); + } finally { + busy = false; + for (const control of dialog.querySelectorAll<HTMLButtonElement | HTMLSelectElement>('button,select')) + control.disabled = false; + } + }; + const request = (action: string, extra = {}) => + api('action', { + action, + workspaceId: workspace.value, + ...(action === 'list' ? {} : { boardRef: board.value }), + ...extra, + }); + const mutate = (action: string, extra = {}) => + run(async () => { + snapshot = (await request(action, extra)) as Aggregate; + render(); + }); + const render = () => { + lanes.replaceChildren(); + detail.replaceChildren(); + for (const lane of snapshot?.lanes ?? []) { + const column = element('section'); + column.className = 'genie-lane'; + column.append(element('h2', `${lane.label ?? lane.name} · ${lane.cards.length}`)); + if (!lane.cards.length) { + const empty = element('p', 'No tasks'); + empty.className = 'genie-empty'; + column.append(empty); + } + for (const card of lane.cards) { + const button = element('button', card.title); + button.className = 'genie-card'; + button.append( + element( + 'small', + [ + card.status, + card.claimedBy, + card.liveness, + card.enforcedBlock ? `Blocked: ${card.enforcedBlock.reason}` : null, + ] + .filter(Boolean) + .join(' · '), + ), + ); + button.onclick = () => { + selected = card.id; + render(); + }; + column.append(button); + } + lanes.append(column); + } + const card = snapshot?.lanes.flatMap((lane) => lane.cards).find((entry) => entry.id === selected); + if (!card) { + detail.append(element('p', 'Select a task to see its details and history.')); + return; + } + detail.append( + element('h2', card.title), + element('small', card.id), + element('p', `${card.status} · ${card.liveness ?? 'Unclaimed'}`), + ); + if (card.claimedBy) detail.append(element('p', `Owner: ${card.claimedBy} (${card.agentKind ?? 'unknown'})`)); + if (card.assignedAgent) + detail.append(element('p', `Assigned: ${card.assignedAgent} — ${card.assignedReason ?? ''}`)); + if (card.enforcedBlock) + detail.append( + element('p', `${card.enforcedBlock.kind === 'hold' ? 'On hold' : 'Blocked'}: ${card.enforcedBlock.reason}`), + ); + const destination = element('select'); + destination.setAttribute('aria-label', 'Destination lane'); + for (const lane of snapshot?.lanes ?? []) { + const option = element('option', lane.label ?? lane.name); + option.value = lane.name; + option.selected = lane.name === card.lane; + destination.append(option); + } + const move = element('button', 'Move'); + move.onclick = () => void mutate('move', { id: card.id, lane: destination.value }); + const actions = element('div'); + actions.className = 'genie-actions'; + actions.append(destination, move); + for (const action of ['checkout', 'release', 'unblock', 'done']) { + const button = element( + 'button', + { checkout: 'Claim', release: 'Release', unblock: 'Unblock', done: 'Complete' }[action], + ); + button.onclick = () => void mutate(action, { id: card.id }); + actions.append(button); + } + detail.append(actions); + const note = element('textarea'); + note.setAttribute('aria-label', 'Comment or block reason'); + detail.append(note); + const notes = element('div'); + notes.className = 'genie-actions'; + for (const [label, action, hold] of [ + ['Comment', 'comment', false], + ['Block', 'block', false], + ['Hold', 'block', true], + ] as const) { + const button = element('button', label); + button.onclick = () => + void mutate(action, { id: card.id, text: note.value, ...(action === 'block' ? { hold } : {}) }); + notes.append(button); + } + detail.append(notes); + detail.append(element('h3', 'Dependencies')); + for (const dependency of card.dependencies) + detail.append(element('p', `${dependency.title} · ${dependency.status}`)); + if (!card.dependencies.length) detail.append(element('p', 'No dependencies')); + detail.append(element('h3', 'Comments')); + for (const comment of card.comments) + detail.append(element('p', `${comment.author ?? 'Unknown'}: ${comment.note}`)); + detail.append(element('h3', 'History')); + const history = element('ol'); + for (const event of card.timeline) + history.append( + element( + 'li', + `${new Date(event.createdAt).toLocaleString()} · ${event.kind} · ${event.author ?? 'Unknown'}${event.note ? ` — ${event.note}` : ''}`, + ), + ); + detail.append(history); + }; + const load = async () => { + if (!board.value) { + snapshot = undefined; + render(); + return; + } + snapshot = (await request('load')) as Aggregate; + render(); + }; + const list = async () => { + const previous = board.value; + const entries = (await request('list')) as { id: string; name: string }[]; + board.replaceChildren(); + for (const entry of entries) { + const option = element('option', entry.name); + option.value = entry.id; + board.append(option); + } + if (entries.some((entry) => entry.id === previous)) board.value = previous; + await load(); + }; + launch.onclick = () => { + dialog.showModal(); + void run(async () => { + const health = (await api('health')) as { compatible: boolean; error: string }; + if (!health.compatible) throw new Error(health.error); + const entries = (await api('workspaces')) as { id: string; title: string }[]; + workspace.replaceChildren(); + for (const entry of entries) { + const option = element('option', entry.title); + option.value = entry.id; + workspace.append(option); + } + if (!entries.length) throw new Error('Add a repository workspace in DSH to open its Genie board.'); + await list(); + }); + }; + close.onclick = () => dialog.close(); + workspace.onchange = () => void run(list); + board.onchange = () => void run(load); + refresh.onclick = () => void run(list); + create.onsubmit = (event) => { + event.preventDefault(); + void mutate('create', { title: title.value }); + }; + const visible = () => { + if (document.visibilityState === 'visible' && dialog.open) void run(list); + }; + document.addEventListener('visibilitychange', visible); + return () => { + document.removeEventListener('visibilitychange', visible); + style.remove(); + launch.remove(); + dialog.remove(); + }; + }, 'Genie board view'); +} diff --git a/plugins/dsh-genie-board/src/index.ts b/plugins/dsh-genie-board/src/index.ts new file mode 100644 index 000000000..c76f712b8 --- /dev/null +++ b/plugins/dsh-genie-board/src/index.ts @@ -0,0 +1,113 @@ +import type { IncomingMessage, ServerResponse } from 'node:http'; +import manifest from '../package.json'; +import { DEADLINE_MS, compatible, execute, hostEnvironment, resolveExecutable } from './process'; +import { BoardService, type Registry } from './service'; + +interface Context { + workspaceRegistry: Registry; + connection: { requestRejection(req: IncomingMessage): 401 | 403 | undefined }; + webServer: { + register(route: { + kind: 'exact'; + path: string; + handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>; + }): () => void; + }; + effect(effect: () => () => void, label?: string): void; +} +export const inject = ['workspaceRegistry', 'webServer', 'connection']; +export const minimumGenieVersion = manifest.minimumGenieVersion; +export function trusted(req: IncomingMessage): boolean { + const address = req.socket.remoteAddress; + if (!['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(address ?? '')) return false; + const host = req.headers.host; + if (!host || !/^(127\.0\.0\.1|localhost|\[::1\])(?::\d+)?$/.test(host)) return false; + const origin = req.headers.origin; + if (origin !== undefined && origin !== `http://${host}`) return false; + if (req.headers['sec-fetch-site'] && req.headers['sec-fetch-site'] !== 'same-origin') return false; + return req.method === 'POST' + ? origin === `http://${host}` + : origin === `http://${host}` || req.headers['sec-fetch-site'] === 'same-origin'; +} +async function body(req: IncomingMessage): Promise<unknown> { + const chunks: Buffer[] = []; + let size = 0; + for await (const data of req) { + const chunk = Buffer.from(data); + size += chunk.length; + if (size > 16_384) throw new Error('Request body too large'); + chunks.push(chunk); + } + return JSON.parse(Buffer.concat(chunks).toString('utf8')); +} +function json(res: ServerResponse, code: number, value: unknown) { + res.writeHead(code, { + 'content-type': 'application/json', + 'cache-control': 'no-store', + 'x-content-type-options': 'nosniff', + }); + res.end(JSON.stringify(value)); +} +export async function apply(ctx: Context): Promise<void> { + let service: BoardService | undefined; + let version = ''; + let error = ''; + try { + const binary = resolveExecutable(); + version = ( + await execute(binary, ['--version'], process.cwd(), hostEnvironment('dsh-host'), { + expires: Date.now() + DEADLINE_MS, + bytes: 0, + }) + ).trim(); + if (!compatible(version, minimumGenieVersion)) throw new Error(`Genie ${minimumGenieVersion} or newer is required`); + service = new BoardService(ctx.workspaceRegistry, binary); + } catch (failure) { + error = failure instanceof Error ? failure.message : 'Genie unavailable'; + } + ctx.effect(() => { + const disposers: (() => void)[] = []; + const route = (path: string, method: string, handler: () => unknown) => { + disposers.push( + ctx.webServer.register({ + kind: 'exact', + path, + handler: async (req, res) => { + if (req.method !== method) return json(res, 405, { error: 'Method not allowed' }); + const rejection = ctx.connection.requestRejection(req); + if (rejection) return json(res, rejection, { error: 'DSH browser authentication required' }); + if (!trusted(req)) return json(res, 403, { error: 'Same-origin loopback request required' }); + json(res, 200, handler()); + }, + }), + ); + }; + route('/api/genie-board/health', 'GET', () => ({ compatible: !!service, version, minimumGenieVersion, error })); + if (service) { + route('/api/genie-board/workspaces', 'GET', () => service?.workspaces()); + disposers.push( + ctx.webServer.register({ + kind: 'exact', + path: '/api/genie-board/action', + handler: async (req, res) => { + if (req.method !== 'POST') return json(res, 405, { error: 'Method not allowed' }); + const rejection = ctx.connection.requestRejection(req); + if (rejection) return json(res, rejection, { error: 'DSH browser authentication required' }); + if (!trusted(req)) return json(res, 403, { error: 'Same-origin loopback request required' }); + if (req.headers['content-type'] !== 'application/json') + return json(res, 415, { error: 'application/json required' }); + req.setTimeout(DEADLINE_MS, () => req.destroy()); + try { + json(res, 200, await service?.request(await body(req))); + } catch (failure) { + json(res, 400, { error: failure instanceof Error ? failure.message : 'Board request failed' }); + } + }, + }), + ); + } + return () => { + for (const dispose of disposers) dispose(); + }; + }, 'Genie board routes'); +} diff --git a/plugins/dsh-genie-board/src/process.ts b/plugins/dsh-genie-board/src/process.ts new file mode 100644 index 000000000..1cb880ebd --- /dev/null +++ b/plugins/dsh-genie-board/src/process.ts @@ -0,0 +1,94 @@ +import { spawn } from 'node:child_process'; +import { constants, accessSync, realpathSync, statSync } from 'node:fs'; +import { delimiter, join } from 'node:path'; + +export const MAX_OUTPUT = 4 * 1024 * 1024; +export const DEADLINE_MS = 10_000; +export interface Budget { + expires: number; + bytes: number; +} +export function resolveExecutable(): string { + for (const directory of (process.env.PATH ?? '').split(delimiter)) { + if (!directory) continue; + try { + const path = realpathSync(join(directory, 'genie')); + if (!statSync(path).isFile()) continue; + accessSync(path, constants.X_OK); + return path; + } catch { + /* Continue the one startup-only lookup. */ + } + } + throw new Error('Genie executable is unavailable'); +} +export function hostEnvironment(identity: string): NodeJS.ProcessEnv { + const env: NodeJS.ProcessEnv = { NO_COLOR: '1', GENIE_AGENT_NAME: identity, GENIE_AGENT_KIND: 'dsh' }; + for (const key of ['PATH', 'HOME', 'GENIE_HOME']) if (process.env[key] !== undefined) env[key] = process.env[key]; + return env; +} +export function execute( + binary: string, + argv: string[], + cwd: string, + env: NodeJS.ProcessEnv, + budget: Budget, +): Promise<string> { + return new Promise((resolve, reject) => { + const remaining = budget.expires - Date.now(); + if (remaining <= 0) return reject(new Error('Genie deadline exceeded')); + const child = spawn(binary, ['--no-interactive', ...argv], { + cwd, + env, + shell: false, + stdio: ['ignore', 'pipe', 'pipe'], + }); + const chunks: Buffer[] = []; + let failure: Error | undefined; + const fail = (error: Error) => { + failure ??= error; + child.kill('SIGKILL'); + }; + const timer = setTimeout(() => fail(new Error('Genie deadline exceeded')), remaining); + const read = (chunk: Buffer, stdout: boolean) => { + budget.bytes += chunk.length; + if (budget.bytes > MAX_OUTPUT) return fail(new Error('Genie output limit exceeded')); + if (stdout) chunks.push(chunk); + }; + child.stdout.on('data', (chunk: Buffer) => read(chunk, true)); + child.stderr.on('data', (chunk: Buffer) => read(chunk, false)); + child.on('error', fail); + child.on('close', (code) => { + clearTimeout(timer); + if (failure) reject(failure); + else if (code !== 0) reject(new Error(`Genie exited with code ${code}`)); + else resolve(Buffer.concat(chunks).toString('utf8')); + }); + }); +} +export function compatible(actual: string, minimum: string): boolean { + const parse = (value: string) => + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec( + value, + ); + const a = parse(actual.trim()); + const b = parse(minimum); + if (!a || !b) return false; + for (let i = 1; i <= 3; i++) { + if (BigInt(a[i]) !== BigInt(b[i])) return BigInt(a[i]) > BigInt(b[i]); + } + if (a[4] === b[4]) return true; + if (!a[4]) return true; + if (!b[4]) return false; + const ap = a[4].split('.'); + const bp = b[4].split('.'); + for (let i = 0; i < Math.max(ap.length, bp.length); i++) { + if (ap[i] === bp[i]) continue; + if (ap[i] === undefined) return false; + if (bp[i] === undefined) return true; + const an = /^\d+$/.test(ap[i]); + const bn = /^\d+$/.test(bp[i]); + return an && bn ? BigInt(ap[i]) > BigInt(bp[i]) : an !== bn ? !an : ap[i] > bp[i]; + } + return true; +} diff --git a/plugins/dsh-genie-board/src/schema.ts b/plugins/dsh-genie-board/src/schema.ts new file mode 100644 index 000000000..cc68c5ea5 --- /dev/null +++ b/plugins/dsh-genie-board/src/schema.ts @@ -0,0 +1,91 @@ +import { z } from 'zod'; + +const text = z.string(); +const nullable = text.nullable(); +const time = z.number().finite().nonnegative(); +const id = text.regex(/^t_[a-z0-9]+$/); +const status = z.enum(['blocked', 'ready', 'in_progress', 'done']); +const event = z + .object({ id: time.int(), kind: text, note: nullable, authorKind: nullable, author: nullable, createdAt: time }) + .strict(); +export const cardSchema = z + .object({ + id, + boardId: nullable, + title: text, + status, + claimedBy: nullable, + claimedAt: time.nullable(), + wish: nullable, + group: nullable, + assignedAgent: nullable, + assignedReason: nullable, + createdAt: time, + updatedAt: time, + lane: nullable, + enforcedBlock: z + .object({ reason: text, kind: z.enum(['work', 'hold']) }) + .strict() + .nullable(), + agentKind: nullable, + heartbeatAt: time.nullable(), + blockedBy: nullable, + blockedReason: nullable, + liveness: z.enum(['running', 'idle', 'stale']).nullable(), + dependencies: z.array(z.object({ id, title: text, status }).strict()), + timeline: z.array(event), + comments: z.array( + event + .omit({ kind: true }) + .extend({ note: text.min(1) }) + .strict(), + ), + }) + .strict(); +export const aggregateSchema = z + .object({ + schemaVersion: z.literal(1), + scope: text, + lanes: z + .array(z.object({ name: text.min(1), label: nullable, action: nullable, cards: z.array(cardSchema) }).strict()) + .min(1), + }) + .strict() + .superRefine((board, ctx) => { + const names = board.lanes.map((lane) => lane.name); + const ids = board.lanes.flatMap((lane) => lane.cards.map((card) => card.id)); + if (new Set(names).size !== names.length || new Set(ids).size !== ids.length) + ctx.addIssue({ code: 'custom', message: 'Duplicate lane or card' }); + }); +export const boardsSchema = z.array( + z.object({ id: text.regex(/^b_[a-z0-9]+$/), name: text, laneCount: time.int(), cardCount: time.int() }).strict(), +); +const bounded = (max: number) => + text + .refine( + (value) => + !Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127), + 'Control characters are not allowed', + ) + .transform((value) => value.trim()) + .refine( + (value) => + value.length > 0 && + Buffer.byteLength(value) <= max && + !Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127), + 'Invalid text', + ); +const base = { workspaceId: text.min(1).max(200), boardRef: text.regex(/^b_[a-z0-9]+$/) }; +export const requestSchema = z.discriminatedUnion('action', [ + z.object({ action: z.literal('list'), workspaceId: base.workspaceId }).strict(), + z.object({ action: z.literal('load'), ...base }).strict(), + z.object({ action: z.literal('create'), ...base, title: bounded(200) }).strict(), + z.object({ action: z.literal('move'), ...base, id, lane: text.min(1).max(200) }).strict(), + z.object({ action: z.literal('comment'), ...base, id, text: bounded(4000) }).strict(), + z.object({ action: z.literal('block'), ...base, id, text: bounded(1000), hold: z.boolean().optional() }).strict(), + ...(['unblock', 'checkout', 'release', 'done'] as const).map((action) => + z.object({ action: z.literal(action), ...base, id }).strict(), + ), +]); +export type Request = z.infer<typeof requestSchema>; +export type Aggregate = z.infer<typeof aggregateSchema>; diff --git a/plugins/dsh-genie-board/src/service.ts b/plugins/dsh-genie-board/src/service.ts new file mode 100644 index 000000000..1f0b8a89c --- /dev/null +++ b/plugins/dsh-genie-board/src/service.ts @@ -0,0 +1,109 @@ +import { realpath, stat } from 'node:fs/promises'; +import { hostname, userInfo } from 'node:os'; +import { join } from 'node:path'; +import { type Budget, DEADLINE_MS, execute, hostEnvironment } from './process'; +import { type Request, aggregateSchema, boardsSchema, requestSchema } from './schema'; + +export interface Workspace { + id: string; + path: string; + title: string; +} +export interface Registry { + list(): Workspace[]; +} +interface Selection { + path: string; + boards: Set<string>; + board?: string; + tasks?: Set<string>; + lanes?: Set<string>; +} +export function actionArgs(request: Exclude<Request, { action: 'list' | 'load' }>, identity: string): string[] { + switch (request.action) { + case 'create': + return ['task', 'create', '--title', request.title, '--board', request.boardRef]; + case 'move': + return ['task', 'move', request.id, '--to', request.lane]; + case 'comment': + return ['task', 'comment', '--', request.id, request.text]; + case 'block': + return ['task', 'block', request.id, '--reason', request.text, ...(request.hold ? ['--hold'] : [])]; + case 'checkout': + return ['task', 'checkout', request.id, '--worker', identity]; + default: + return ['task', request.action, request.id]; + } +} +export class BoardService { + private readonly selections = new Map<string, Selection>(); + private readonly active = new Set<string>(); + readonly identity = `dsh:${userInfo().username}@${hostname()}`; + private readonly environment = hostEnvironment(this.identity); + constructor( + private readonly registry: Registry, + private readonly binary: string, + private readonly run = execute, + ) {} + workspaces() { + const current = this.registry.list(); + const ids = new Set(current.map((workspace) => workspace.id)); + for (const id of this.selections.keys()) if (!ids.has(id)) this.selections.delete(id); + return current.map(({ id, title }) => ({ id, title })); + } + async request(input: unknown): Promise<unknown> { + const request = requestSchema.parse(input); + this.workspaces(); + if (this.active.has(request.workspaceId)) throw new Error('Workspace operation in progress; wait and reload'); + this.active.add(request.workspaceId); + let mutationCompleted = false; + try { + const workspace = this.registry.list().find((entry) => entry.id === request.workspaceId); + if (!workspace) throw new Error('Unknown workspace'); + const path = await realpath(workspace.path); + if (!(await stat(path)).isDirectory() || !(await stat(join(path, '.genie'))).isDirectory()) + throw new Error('Workspace must contain .genie'); + // Worktrees have a regular .git file; ordinary repositories have a directory. + const git = await stat(join(path, '.git')); + if (!git.isDirectory() && !git.isFile()) throw new Error('Workspace must be a physical repository'); + const previous = this.selections.get(workspace.id); + if (previous && previous.path !== path) { + this.selections.delete(workspace.id); + throw new Error('Workspace changed; list boards again'); + } + const budget: Budget = { expires: Date.now() + DEADLINE_MS, bytes: 0 }; + const run = (args: string[]) => this.run(this.binary, args, path, this.environment, budget); + if (request.action === 'list') { + const boards = boardsSchema.parse(JSON.parse(await run(['board', 'list', '--json']))); + this.selections.set(workspace.id, { path, boards: new Set(boards.map((board) => board.id)) }); + return boards; + } + if (!previous?.boards.has(request.boardRef)) throw new Error('Unknown board; list boards first'); + if (request.action !== 'load') { + if (previous.board !== request.boardRef) throw new Error('Board selection changed; reload'); + if ('id' in request && !previous.tasks?.has(request.id)) throw new Error('Task is outside the selected board'); + if (request.action === 'move' && !previous.lanes?.has(request.lane)) throw new Error('Unknown lane'); + await run(actionArgs(request, this.identity)); + mutationCompleted = true; + } + const aggregate = aggregateSchema.parse(JSON.parse(await run(['board', '--board', request.boardRef, '--json']))); + const cards = aggregate.lanes.flatMap((lane) => lane.cards); + if (cards.some((card) => card.boardId !== request.boardRef)) throw new Error('Foreign board card in aggregate'); + this.selections.set(workspace.id, { + path, + boards: previous.boards, + board: request.boardRef, + tasks: new Set(cards.map((card) => card.id)), + lanes: new Set(aggregate.lanes.map((lane) => lane.name)), + }); + return aggregate; + } catch (error) { + this.selections.delete(request.workspaceId); + if (mutationCompleted) + throw new Error('Operation may have completed, but refresh failed. Reload before making another change.'); + throw error; + } finally { + this.active.delete(request.workspaceId); + } + } +} diff --git a/plugins/dsh-genie-board/tsconfig.json b/plugins/dsh-genie-board/tsconfig.json new file mode 100644 index 000000000..60de6fec8 --- /dev/null +++ b/plugins/dsh-genie-board/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "resolveJsonModule": true, + "noEmit": true + }, + "include": ["src/**/*.ts", "build.ts", "build.test.ts"], + "exclude": ["dist"] +} diff --git a/scripts/dsh-genie-board-smoke.ts b/scripts/dsh-genie-board-smoke.ts new file mode 100644 index 000000000..c21af9cfb --- /dev/null +++ b/scripts/dsh-genie-board-smoke.ts @@ -0,0 +1,172 @@ +import { type ChildProcess, spawn } from 'node:child_process'; +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { delimiter, join, resolve } from 'node:path'; + +const root = resolve(import.meta.dir, '..'); +const temporary = await mkdtemp(join(tmpdir(), 'genie-dsh-smoke-')); +const repo = join(temporary, 'repo'); +const home = join(temporary, 'dsh'); +const bin = join(temporary, 'bin'); +const env = { + ...process.env, + DSH_HOME: home, + GENIE_HOME: join(temporary, 'genie-home'), + HOME: join(temporary, 'home'), + PATH: `${bin}${delimiter}${process.env.PATH}`, +}; +let server: ChildProcess | undefined; +let installed = false; +async function command(binary: string, args: string[], cwd = root): Promise<string> { + const proc = Bun.spawn([binary, ...args], { cwd, env, stdout: 'pipe', stderr: 'pipe' }); + const [stdout, stderr, code] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + if (code) throw new Error(`${binary} ${args.join(' ')} exited ${code}\n${stdout}\n${stderr}`); + console.log(`${binary} ${args.join(' ')}: OK`); + return stdout; +} +async function stop() { + const child = server; + server = undefined; + if (!child || child.exitCode !== null) return; + await new Promise<void>((resolveStop) => { + child.once('close', () => { + clearTimeout(timer); + resolveStop(); + }); + const timer = setTimeout(() => child.kill('SIGKILL'), 5000); + child.kill('SIGTERM'); + }); +} +async function start(): Promise<string> { + server = spawn( + 'dsh', + ['web', '--patch', join(temporary, 'fixture.patch.yml'), '--no-open', '--host', '127.0.0.1', '--port', '0'], + { cwd: repo, env, stdio: ['ignore', 'pipe', 'pipe'] }, + ); + const child = server; + return new Promise((resolveStart, reject) => { + let output = ''; + const timer = setTimeout(() => reject(new Error(`DSH startup timeout\n${output}`)), 45000); + const receive = (data: Buffer) => { + output += data.toString(); + const url = /http:\/\/127\.0\.0\.1:\d+\/\?token=[A-Za-z0-9_-]+/.exec(output)?.[0]; + if (url) { + clearTimeout(timer); + resolveStart(url); + } + }; + child.stdout?.on('data', receive); + child.stderr?.on('data', receive); + child.once('exit', (code) => { + clearTimeout(timer); + reject(new Error(`DSH exited ${code}\n${output}`)); + }); + }); +} +try { + for (const directory of [repo, bin, env.HOME]) await mkdir(directory, { recursive: true }); + await command('git', ['init', '--quiet'], repo); + await command('bun', ['run', 'build']); + await writeFile( + join(bin, 'genie'), + `#!/bin/sh\nexec '${process.execPath.replaceAll("'", "'\\''")}' '${join(root, 'dist/genie.js').replaceAll("'", "'\\''")}' "$@"\n`, + { mode: 0o755 }, + ); + await command('genie', ['--no-interactive', 'board', 'create', 'Smoke'], repo); + // Fixture-only Host plugin calls the actual installed registry's public API. + const fixture = join(temporary, 'fixture.mjs'); + await writeFile( + fixture, + `export const inject=['workspaceRegistry']; export async function apply(ctx){await ctx.workspaceRegistry.create(${JSON.stringify(repo)},'Smoke workspace');}`, + ); + await writeFile( + join(temporary, 'fixture.patch.yml'), + `- insert:\n - id: smoke-workspace\n name: ${JSON.stringify(fixture)}\n`, + ); + await command('dsh', ['plugin', '--profile', 'web', 'add', `link:${join(root, 'plugins/dsh-genie-board')}`]); + installed = true; + await start(); + console.log('First launch authenticated URL received'); + await stop(); + const listed = await command('dsh', ['plugin', '--profile', 'web', 'list', '--depth', '0']); + if (!listed.includes('@automagik/genie-dsh-board')) throw new Error('Installed plugin missing from list'); + const launchUrl = await start(); + const origin = new URL(launchUrl).origin; + const exchange = await fetch(launchUrl, { redirect: 'manual' }); + const cookie = exchange.headers.get('set-cookie')?.split(';')[0]; + if (!cookie) throw new Error('DSH token exchange did not set cookie'); + for (const invalidCookie of ['', 'dsh_session=invalid']) { + const denied = await fetch(`${origin}/api/genie-board/health`, { + headers: { origin, cookie: invalidCookie, 'sec-fetch-site': 'same-origin' }, + }); + if (denied.status !== 401) throw new Error('Missing/invalid DSH cookie was accepted'); + } + const request = async (path: string, data?: unknown) => { + const response = await fetch(`${origin}/api/genie-board/${path}`, { + method: data === undefined ? 'GET' : 'POST', + headers: { origin, cookie, 'content-type': 'application/json', 'sec-fetch-site': 'same-origin' }, + ...(data === undefined ? {} : { body: JSON.stringify(data) }), + }); + const result = await response.json(); + if (!response.ok) throw new Error(JSON.stringify(result)); + return result; + }; + let health: { compatible?: boolean } = {}; + for (let attempt = 0; attempt < 100; attempt++) { + try { + health = await request('health'); + break; + } catch { + await Bun.sleep(100); + } + } + if (!health.compatible) throw new Error(`Incompatible health: ${JSON.stringify(health)}`); + const workspaces = await request('workspaces'); + const workspaceId = workspaces[0]?.id; + const boards = await request('action', { action: 'list', workspaceId }); + const boardRef = boards[0]?.id; + await request('action', { action: 'load', workspaceId, boardRef }); + const created = await request('action', { action: 'create', workspaceId, boardRef, title: 'DSH smoke task' }); + const id = created.lanes + .flatMap((lane: { cards: { id: string; title: string }[] }) => lane.cards) + .find((card: { title: string }) => card.title === 'DSH smoke task')?.id; + if (!id) throw new Error('Create did not return complete task'); + const lane = created.lanes[1].name; + const moved = await request('action', { action: 'move', workspaceId, boardRef, id, lane }); + if ( + !moved.lanes + .find((entry: { name: string }) => entry.name === lane) + .cards.some((card: { id: string }) => card.id === id) + ) + throw new Error('Move not confirmed'); + const commented = await request('action', { action: 'comment', workspaceId, boardRef, id, text: '--help' }); + const commentCard = commented.lanes + .flatMap((entry: { cards: { id: string; comments: { note: string }[] }[] }) => entry.cards) + .find((entry: { id: string }) => entry.id === id); + if (!commentCard?.comments.some((entry: { note: string }) => entry.note === '--help')) + throw new Error('Option-shaped comment was not stored literally'); + console.log( + JSON.stringify({ + dsh: await command('dsh', ['--version']), + health, + origin, + workspaceId, + boardRef, + id, + lane, + result: 'PASS: install/list/restart/health/load/create/move', + }), + ); +} finally { + await stop(); + try { + if (installed) await command('dsh', ['plugin', '--profile', 'web', 'remove', '@automagik/genie-dsh-board']); + } finally { + await rm(temporary, { recursive: true, force: true }); + console.log('Temporary profile/repository removed'); + } +} From 04a523c349daa71ac3a82bdc939dce33d59ac460 Mon Sep 17 00:00:00 2001 From: Felipe Rosa <felipe@namastex.ai> Date: Mon, 7 Sep 2026 20:10:28 +0000 Subject: [PATCH 12/24] test: budget the multi-process roadmap sync scenario Allow a bounded 20 seconds for 13 real CLI invocations under the full suite. Keep every sync, conflict and recovery assertion unchanged. Co-authored-by: Felipe Rosa <felipe@namastex.ai> --- src/term-commands/v5-task.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/term-commands/v5-task.test.ts b/src/term-commands/v5-task.test.ts index 6987a3562..c2353456f 100644 --- a/src/term-commands/v5-task.test.ts +++ b/src/term-commands/v5-task.test.ts @@ -1277,6 +1277,8 @@ describe('roadmap.json canonical sync', () => { expect(listed.stdout).toContain('keeper'); }); + // This round-trip intentionally runs 13 real CLI subprocesses. Their startup + // cost exceeds Bun's default 5s under the full suite; keep a bounded 20s gate. test('pulled snapshot imports on sync; local mutation exports; divergence is refused then resolvable', async () => { // Machine A (repo): publish F1, then F2 with one more card. const db = openDb({ cwd: repo }); @@ -1336,7 +1338,7 @@ describe('roadmap.json canonical sync', () => { } finally { rmSync(clone, { recursive: true, force: true }); } - }); + }, 20_000); }); // Same subprocess invocation as `cli`, but with extra env vars layered on — used From 875e750431eda9ec86e800d1f70fd37d5657a4e5 Mon Sep 17 00:00:00 2001 From: Felipe Rosa <felipe@namastex.ai> Date: Mon, 7 Sep 2026 20:11:00 +0000 Subject: [PATCH 13/24] feat(release): verify and package DSH board payloads Bind staged metadata and compiled compatibility checks to one candidate version. Require complete four-platform payloads and authenticated delivery provenance before either publication path can reconcile release assets. Co-authored-by: Felipe Rosa <felipe@namastex.ai> --- .genie/wishes/dsh-genie-board/WISH.md | 24 ++- .github/workflows/release-publish.yml | 10 + AGENTS.md | 2 +- plugins/dsh-genie-board/NOTICE | 24 +++ plugins/dsh-genie-board/README.md | 26 ++- plugins/dsh-genie-board/build.test.ts | 27 +++ plugins/dsh-genie-board/build.ts | 10 +- plugins/dsh-genie-board/src/index.ts | 6 +- scripts/build-binary.sh | 10 +- scripts/release-payload-version.test.ts | 4 + scripts/release-payload-version.ts | 15 +- .../verify-dsh-genie-board-release.test.ts | 204 ++++++++++++++++++ scripts/verify-dsh-genie-board-release.ts | 181 ++++++++++++++++ 13 files changed, 533 insertions(+), 10 deletions(-) create mode 100644 scripts/verify-dsh-genie-board-release.test.ts create mode 100644 scripts/verify-dsh-genie-board-release.ts diff --git a/.genie/wishes/dsh-genie-board/WISH.md b/.genie/wishes/dsh-genie-board/WISH.md index 96d599c29..ab25ccedb 100644 --- a/.genie/wishes/dsh-genie-board/WISH.md +++ b/.genie/wishes/dsh-genie-board/WISH.md @@ -182,7 +182,7 @@ Independent contract review: `/root/g1_review` required these exact concurrency/ **Acceptance Criteria:** - [x] Rendering matches one complete fixture-backed Host aggregate; mutations use fixed argv and perform one complete refresh. - [x] Every unsafe/failure case is bounded and cannot invoke an out-of-scope command. -- [ ] The manifest and runtime reject every Genie version below the immutable candidate value stamped by Group 3, while source/linked tests prove the comparator against the checkout version without depending on a prior publication. +- [x] The manifest and runtime reject every Genie version below the immutable candidate value stamped by Group 3, while source/linked tests prove the comparator against the checkout version without depending on a prior publication. - [x] Package build and the linked-profile install/restart/read-back/board-operation/cleanup smoke pass against DSH `0.1.1-rc.2` or a newer explicitly proven floor. - [x] No database access, persistence, watcher, poller, shell, or browser-supplied executable/path/argv exists. @@ -366,6 +366,28 @@ Corrective route: resolve or formally clear the repository-baseline failures, th - Parent `bun run build:plugin`, focused plugin tests (**19 pass / 249 assertions**) and real `bun scripts/dsh-genie-board-smoke.ts` each exited **0**. The owner smoke separately proves authenticated install/list/restart/health/load/create/move and literal option-shaped comment, followed by plugin removal and temporary-state cleanup. - Combined with independent code/security/quality and rendered **SHIP**, Group 2 implementation is accepted. The comparator is proven for source/linked builds; immutable candidate stamping and extracted runtime-floor proof remain explicitly owned by Group 3, so the combined future-artifact checkbox above remains open. +### G3 implementation review — 2026-09-07 — SHIP + +- Independent reviewer `/root/g3_review` inspected all 12 changed/new release, build, runtime metadata, test and documentation files, including both publication paths and the updated contributor release contract. **SHIP**, no remaining findings. +- Frozen patch SHA-256 `8924c120504f9239e455b61472d583164d3356b41c9bc4cd4a2596d748b8b435` (33399 bytes): ten tracked-file diffs, followed by new verifier test and source diffs. Parent independently reproduced the hash before adding this ledger. +- Review closed P1 unauthenticated descriptor source binding by requiring pinned delivery attestation and exact predicate equality. It closed P2 false-positive trust tests with separate cosign/SLSA failure stages and all-four-platform invocation checks; missing executables do not satisfy these tests. +- Independent focused validation: **30 pass / 0 fail / 70 assertions**, including payload versions, existing trust helper, descriptor tampering, release tag mismatch, required members and unsafe archive links; diff check passed. +- Parent current full gate and final rebuilt artifact verification remain implementation-acceptance gates. Deterministic trust fixtures prove orchestration, not real signed publication. Protected stable publication, release-download proof and topic publication remain pending the human-approved release. + +### G3 live approval-policy correction and aggregate validation — 2026-09-07 + +- Owner read back the actual GitHub `production` environment: its two required reviewers were configured, but `prevent_self_review` was false. Independent reviewer confirmed this contradicted the approved non-initiator release gate. The owner enabled `prevent_self_review` under that approved requirement, preserving both reviewer IDs/types and deployment branch policy. Fresh environment and branch-policy reads confirm the setting is true, the same two reviewers remain, and `main` remains the sole allowed branch. No release or deployment was started. +- First parent full gate: **2023 pass / 1 skip / 1 fail / 1 error**. The one failure was the existing 13-subprocess roadmap round-trip exceeding Bun's default 5-second test timeout; teardown killed the final child, causing the secondary exit-143 assertion. The unchanged focused case passed in 3.04 seconds. +- Minimal repair gives only that 13-subprocess test a bounded 20-second budget with an explanatory comment; every behavior assertion remains unchanged. The focused canonical-sync group then passed **7 tests / 62 assertions**, affected case 2.96 seconds. Independent review and a fresh parent aggregate gate will close this repair. +- Final four locally built candidate artifacts passed owner extraction checks: all required plugin files, root/Orca/DSH version metadata, compiled Host floor `5.260907.99`, and current README/NOTICE. Native Linux glibc binary version also matched. Owner unsigned verifier and source/linked DSH smoke each exited 0; these remain local premerge proofs, not published signature evidence. +- Independent follow-up accepted the scoped timing repair: **SHIP**, diff SHA-256 `eeae081f438858a42c526b7686a244b18031c96be012a84de9b78efc8de74633`, all assertions retained. Reviewer also independently compared production environment before/after evidence and confirmed the only protection change was `prevent_self_review: false → true`; reviewers and the sole `main` policy are identical. + +### G3 premerge implementation acceptance — 2026-09-07 + +- Fresh parent `bun run check` exited **0**: **2024 pass / 1 skip / 0 fail**, 8692 assertions across 99 files, 284.78 seconds. Full gate covers release/CI, shared runtime metadata, build and test-fixture changes. +- Parent source plugin build, real authenticated DSH source/linked smoke, final four-artifact unsigned verifier and independent extraction/runtime-floor checks all exited **0**. Independent code/security review and the scoped fixture repair are **SHIP**. +- Group 3's implementation is accepted for the requested reviewed/dogfooded `dev → main` PR. Actual stable publication, published signature/provenance verification, and topic publication remain unperformed and open. The Group 3 release task and whole wish therefore remain in progress; this is not a released/SHIPPED wish. Other approved independent queue items may continue while that post-main gate awaits the human release. + --- ## Files to Create/Modify diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 553423723..eef23e4b9 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -1457,6 +1457,10 @@ jobs: - name: Install slsa-verifier for published-asset reuse verification uses: slsa-framework/slsa-verifier/actions/installer@ea584f4502babc6f60d9bc799dbbb13c1caa9ee6 # v2.7.1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.3.11 + - name: Prepare draft and reconcile exact endorsed assets env: GH_TOKEN: ${{ github.token }} @@ -1475,6 +1479,7 @@ jobs: # byte; partial published releases and mismatches fail closed. The # repository setting is an externally verified cutover prerequisite: # GITHUB_TOKEN cannot read the Administration API that exposes it. + bun scripts/verify-dsh-genie-board-release.ts --signed-artifact-dir dist --version "$VERSION" --channel "$CHANNEL" bash scripts/reconcile-release-note.sh prepare bash scripts/reconcile-release-assets.sh @@ -1697,6 +1702,10 @@ jobs: - name: Install slsa-verifier for final remote verification uses: slsa-framework/slsa-verifier/actions/installer@ea584f4502babc6f60d9bc799dbbb13c1caa9ee6 # v2.7.1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.3.11 + - name: Publish, lock, and reverify the complete remote inventory shell: bash env: @@ -1708,6 +1717,7 @@ jobs: CANDIDATE_MANIFEST_DIR: ${{ runner.temp }}/candidate-manifests run: | set -euo pipefail + bun scripts/verify-dsh-genie-board-release.ts --signed-artifact-dir dist --version "$VERSION" --channel "$CHANNEL" bash scripts/reconcile-release-note.sh finalize # Every channel owns a fresh tag. Publish and lock the exact verified # inventory before any public manifest can name it. diff --git a/AGENTS.md b/AGENTS.md index d0ecc9091..1043f3598 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,7 +54,7 @@ Biome enforces single quotes, two-space indentation, 120-column lines, and trail ## Release contract -Release tarballs contain the binary, the `plugins/genie` Orca payload, `skills/`, `templates/`, and `VERSION`. Three version files are stamped and must agree with `package.json`: `package.json`, `plugins/genie/package.json`, and `plugins/genie/orca-plugin.json`. The root `orca-marketplace.json` is a source-only, versionless index that no tarball carries. Stable is the default channel; dev requires explicit selection. Build and verify every supported release tarball before promotion. +Release tarballs contain the binary, the `plugins/genie` Orca payload, the `plugins/dsh-genie-board` DSH payload, `skills/`, `templates/`, and `VERSION`. Committed root and Orca package versions must agree. Staging stamps the immutable candidate into `VERSION`, both plugin packages, the Orca manifest, and the DSH package compatibility floor; the DSH Host bundle is built with the same candidate. Source/linked Host builds use the checkout root version. The root `orca-marketplace.json` is a source-only, versionless index that no tarball carries. Stable is the default channel; dev requires explicit selection. Build and verify every supported release tarball before promotion. ## Runtime-specific notes diff --git a/plugins/dsh-genie-board/NOTICE b/plugins/dsh-genie-board/NOTICE index a06105d3b..fdb859cae 100644 --- a/plugins/dsh-genie-board/NOTICE +++ b/plugins/dsh-genie-board/NOTICE @@ -5,3 +5,27 @@ Original plugin implementation for the public Cordis lifecycle, DSH workspace registry and DSH WebServer interfaces. DSH is developed by DeepSeek AI and is not part of this package. No implementation from the separate dsh-genie project is included. The Host bundle includes Zod (MIT), copyright Colin McDonnell. + +Zod 3.25.76 license text: + +MIT License + +Copyright (c) 2025 Colin McDonnell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/dsh-genie-board/README.md b/plugins/dsh-genie-board/README.md index 83597c1ce..4f9e98bbf 100644 --- a/plugins/dsh-genie-board/README.md +++ b/plugins/dsh-genie-board/README.md @@ -20,8 +20,9 @@ dsh web --no-open --host 127.0.0.1 --port 0 ``` The immutable artifacts are `dist/index.js` (Host) and `dist/client.js` (browser). -`minimumGenieVersion` in package.json is the checkout version for source builds; -the release packaging step stamps it and the package version together. Startup +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. @@ -74,3 +75,24 @@ The smoke installs into a disposable DSH_HOME, registers a disposable repository through the real workspace registry, starts/stops/restarts DSH, verifies plugin listing and compatibility, creates and moves a task through Host routes, then removes the plugin and temporary state in `finally`. Personal profiles are not used. + +## Release verification + +The release payload includes this document, NOTICE, both Cordis manifests, +package metadata and both Host/browser bundles. The repository verifier requires +all seven members independently on all four supported platforms: + +```sh +bun scripts/verify-dsh-genie-board-release.ts --unsigned-artifact-dir dist --version VERSION +bun scripts/verify-dsh-genie-board-release.ts --signed-artifact-dir dist --version VERSION --channel stable +bun scripts/verify-dsh-genie-board-release.ts --release vVERSION --channel stable +``` + +Unsigned mode proves packaging only. Signed modes require `cosign`, +`slsa-verifier`, and `gh` in PATH. They verify descriptor digests, cosign identity, +SLSA provenance, and the signed delivery endorsement binding the descriptor to +its candidate/source identity for every artifact. Release mode +reads back the published tag/source binding. Stable publication still requires +the protected production approval in the existing Release workflow. Only after +that publication and the release-mode proof may a maintainer add the `dsh-plugin` +repository topic and read it back. No local build authorizes that action. diff --git a/plugins/dsh-genie-board/build.test.ts b/plugins/dsh-genie-board/build.test.ts index 8ebd47763..a42608152 100644 --- a/plugins/dsh-genie-board/build.test.ts +++ b/plugins/dsh-genie-board/build.test.ts @@ -32,3 +32,30 @@ test('build regenerates Host bundle and lazy DSH browser factory', async () => { }).apply, ).toBe('function'); }, 20_000); + +test('candidate build embeds override floor without changing source metadata', async () => { + const { mkdtemp, readFile, rm } = await import('node:fs/promises'); + const { tmpdir } = await import('node:os'); + const root = import.meta.dir; + const output = await mkdtemp(join(tmpdir(), 'genie-candidate-host-')); + const before = await readFile(join(root, 'package.json'), 'utf8'); + const sourceVersion = JSON.parse(await readFile(join(root, '../../package.json'), 'utf8')).version; + const candidate = `${sourceVersion}-candidate-proof`; + try { + const process = Bun.spawn(['bun', 'run', 'build', candidate, output], { + cwd: root, + stdout: 'pipe', + stderr: 'pipe', + }); + const [code, stderr] = await Promise.all([process.exited, new Response(process.stderr).text()]); + expect(code).toBe(0); + expect(stderr).not.toContain('error:'); + const host = await import(join(output, 'index.js')); + expect(host.minimumGenieVersion).toBe(candidate); + expect(await readFile(join(root, 'package.json'), 'utf8')).toBe(before); + const source = await import('./src/index'); + expect(source.minimumGenieVersion).toBe(sourceVersion); + } finally { + await rm(output, { recursive: true, force: true }); + } +}, 20_000); diff --git a/plugins/dsh-genie-board/build.ts b/plugins/dsh-genie-board/build.ts index 3ad342aab..2c84ff55a 100644 --- a/plugins/dsh-genie-board/build.ts +++ b/plugins/dsh-genie-board/build.ts @@ -1,11 +1,17 @@ +import { resolve } from 'node:path'; import { build } from 'esbuild'; +import sourcePackage from '../../package.json'; +const [version = sourcePackage.version, output = 'dist', ...extra] = process.argv.slice(2); +if (extra.length || !/^[0-9A-Za-z][0-9A-Za-z.+-]{0,127}$/.test(version)) throw new Error('invalid build version'); +const outdir = resolve(output); await build({ entryPoints: ['src/index.ts'], bundle: true, platform: 'node', format: 'esm', target: 'node22', - outfile: 'dist/index.js', + outfile: resolve(outdir, 'index.js'), + define: { __GENIE_BUILD_VERSION__: JSON.stringify(version) }, }); await build({ entryPoints: ['src/client.ts'], @@ -17,5 +23,5 @@ await build({ }, footer: { js: 'return module.exports; } });' }, target: 'es2022', - outfile: 'dist/client.js', + outfile: resolve(outdir, 'client.js'), }); diff --git a/plugins/dsh-genie-board/src/index.ts b/plugins/dsh-genie-board/src/index.ts index c76f712b8..5f98d7ce0 100644 --- a/plugins/dsh-genie-board/src/index.ts +++ b/plugins/dsh-genie-board/src/index.ts @@ -1,5 +1,6 @@ import type { IncomingMessage, ServerResponse } from 'node:http'; -import manifest from '../package.json'; +import sourcePackage from '../../../package.json'; +declare const __GENIE_BUILD_VERSION__: string; import { DEADLINE_MS, compatible, execute, hostEnvironment, resolveExecutable } from './process'; import { BoardService, type Registry } from './service'; @@ -16,7 +17,8 @@ interface Context { effect(effect: () => () => void, label?: string): void; } export const inject = ['workspaceRegistry', 'webServer', 'connection']; -export const minimumGenieVersion = manifest.minimumGenieVersion; +export const minimumGenieVersion = + typeof __GENIE_BUILD_VERSION__ === 'undefined' ? sourcePackage.version : __GENIE_BUILD_VERSION__; export function trusted(req: IncomingMessage): boolean { const address = req.socket.remoteAddress; if (!['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(address ?? '')) return false; diff --git a/scripts/build-binary.sh b/scripts/build-binary.sh index 975d50e00..10275f7b4 100755 --- a/scripts/build-binary.sh +++ b/scripts/build-binary.sh @@ -68,6 +68,7 @@ bun build --compile \ --outfile "${STAGE}/genie" cp -R "${REPO_ROOT}/plugins" "${STAGE}/plugins" +bun run --cwd "${REPO_ROOT}/plugins/dsh-genie-board" build "${VERSION}" "${STAGE}/plugins/dsh-genie-board/dist" cp -R "${REPO_ROOT}/skills" "${STAGE}/skills" cp -R "${REPO_ROOT}/templates" "${STAGE}/templates" cp "${REPO_ROOT}/LICENSE" "${STAGE}/LICENSE" @@ -125,7 +126,14 @@ bun "${REPO_ROOT}/scripts/release-payload-version.ts" --stamp "${STAGE}" "${VERS for required in \ "LICENSE" \ "plugins/genie/orca-plugin.json" \ - "plugins/genie/orca-entrypoint.min.js"; do + "plugins/genie/orca-entrypoint.min.js" \ + "plugins/dsh-genie-board/package.json" \ + "plugins/dsh-genie-board/agent.cordis.yml" \ + "plugins/dsh-genie-board/cordis.patch.yml" \ + "plugins/dsh-genie-board/README.md" \ + "plugins/dsh-genie-board/NOTICE" \ + "plugins/dsh-genie-board/dist/index.js" \ + "plugins/dsh-genie-board/dist/client.js"; do [[ -f "${STAGE}/${required}" ]] || { echo "error: release payload missing ${required}" >&2; exit 1; } done diff --git a/scripts/release-payload-version.test.ts b/scripts/release-payload-version.test.ts index fa3b1effd..b8e419922 100644 --- a/scripts/release-payload-version.test.ts +++ b/scripts/release-payload-version.test.ts @@ -29,6 +29,10 @@ describe('release payload version contract', () => { for (const path of ['plugins/genie/package.json', 'plugins/genie/orca-plugin.json']) { writeJson(root, path, { name: 'genie', version: '5.000000.0' }); } + writeJson(root, 'plugins/dsh-genie-board/package.json', { + version: '5.000000.0', + minimumGenieVersion: '5.000000.0', + }); return root; } diff --git a/scripts/release-payload-version.ts b/scripts/release-payload-version.ts index cdccb06fb..de4d3f6fc 100644 --- a/scripts/release-payload-version.ts +++ b/scripts/release-payload-version.ts @@ -13,7 +13,11 @@ import { replaceTopLevelStringProperty } from './json-top-level-string.js'; const VERSION_PATTERN = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,127}$/; -const TOP_LEVEL_VERSION_FILES = ['plugins/genie/package.json', 'plugins/genie/orca-plugin.json'] as const; +const TOP_LEVEL_VERSION_FILES = [ + 'plugins/genie/package.json', + 'plugins/genie/orca-plugin.json', + 'plugins/dsh-genie-board/package.json', +] as const; /** * Committed files whose version must already equal package.json before a @@ -82,12 +86,21 @@ export function stampReleasePayloadVersion(payloadRoot: string, version: string) replaceTopLevelVersion(join(payloadRoot, relativePath), version); } + const manifestPath = join(payloadRoot, 'plugins/dsh-genie-board/package.json'); + if (typeof readObject(manifestPath).minimumGenieVersion !== 'string') throw new Error('missing minimumGenieVersion'); + writeFileSync( + manifestPath, + replaceTopLevelStringProperty(readFileSync(manifestPath, 'utf8'), 'minimumGenieVersion', version), + ); writeFileSync(join(payloadRoot, 'VERSION'), `${version}\n`); } /** Fail closed if any copied release metadata disagrees with VERSION. */ export function verifyReleasePayloadVersion(payloadRoot: string, expectedVersion: string): void { assertVersion(expectedVersion); + const floor = readObject(join(payloadRoot, 'plugins/dsh-genie-board/package.json')).minimumGenieVersion; + if (floor !== expectedVersion) + throw new Error(`minimumGenieVersion mismatch: expected ${expectedVersion}, got ${floor}`); const stampPath = join(payloadRoot, 'VERSION'); if (!existsSync(stampPath)) throw new Error(`release payload metadata is missing: ${stampPath}`); const stamp = readFileSync(stampPath, 'utf8').trim(); diff --git a/scripts/verify-dsh-genie-board-release.test.ts b/scripts/verify-dsh-genie-board-release.test.ts new file mode 100644 index 000000000..8178ab6e1 --- /dev/null +++ b/scripts/verify-dsh-genie-board-release.test.ts @@ -0,0 +1,204 @@ +import { afterEach, expect, test } from 'bun:test'; +import { createHash } from 'node:crypto'; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { stampReleasePayloadVersion } from './release-payload-version'; +import { verifyArtifacts } from './verify-dsh-genie-board-release'; +const roots: string[] = []; +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); +const version = '5.260907.99'; +const platforms = ['linux-x64-glibc', 'linux-x64-musl', 'linux-arm64', 'darwin-arm64']; +function fixture() { + const root = mkdtempSync(join(tmpdir(), 'dsh-release-test-')); + roots.push(root); + const payload = join(root, 'payload'); + const artifacts = join(root, 'artifacts'); + mkdirSync(artifacts); + function put(path: string, data: string) { + mkdirSync(dirname(join(payload, path)), { recursive: true }); + writeFileSync(join(payload, path), data); + } + for (const path of [ + 'plugins/genie/package.json', + 'plugins/genie/orca-plugin.json', + 'plugins/dsh-genie-board/package.json', + ]) + put(path, JSON.stringify({ version, minimumGenieVersion: version })); + for (const path of ['agent.cordis.yml', 'cordis.patch.yml', 'README.md', 'NOTICE', 'dist/index.js', 'dist/client.js']) + put(`plugins/dsh-genie-board/${path}`, 'fixture'); + stampReleasePayloadVersion(payload, version); + function pack() { + for (const platform of platforms) { + const path = join(artifacts, `genie-${version}-${platform}.tar.gz`); + const process = Bun.spawnSync(['tar', '-czf', path, '-C', payload, '.']); + if (process.exitCode) throw new Error('tar failed'); + writeFileSync(`${path}.bundle`, 'invalid signature'); + writeFileSync(`${path}.intoto.jsonl`, 'invalid provenance'); + writeFileSync( + `${path}.stable.delivery.json`, + JSON.stringify({ + artifactSha256: createHash('sha256').update(readFileSync(path)).digest('hex'), + version, + channel: 'stable', + releaseName: `genie-${version}-${platform}.tar.gz`, + releaseTag: `v${version}`, + repository: 'automagik-dev/genie', + platformId: platform, + sourceSha: 'a'.repeat(40), + sourceBranch: 'main', + }), + ); + } + } + pack(); + return { root, payload, artifacts, pack, put }; +} +test('all four complete unsigned payloads pass; missing and extra platforms fail', () => { + const f = fixture(); + expect(() => verifyArtifacts(f.artifacts, version)).not.toThrow(); + writeFileSync(join(f.artifacts, 'extra.tar.gz'), 'x'); + expect(() => verifyArtifacts(f.artifacts, version)).toThrow('exactly four'); + rmSync(join(f.artifacts, 'extra.tar.gz')); + rmSync(join(f.artifacts, `genie-${version}-linux-arm64.tar.gz`)); + expect(() => verifyArtifacts(f.artifacts, version)).toThrow('exactly four'); +}); +for (const member of [ + 'package.json', + 'agent.cordis.yml', + 'cordis.patch.yml', + 'README.md', + 'NOTICE', + 'dist/index.js', + 'dist/client.js', +]) + test(`every platform requires ${member}`, () => { + const f = fixture(); + rmSync(join(f.payload, 'plugins/dsh-genie-board', member)); + f.pack(); + expect(() => verifyArtifacts(f.artifacts, version)).toThrow(); + }); +test('candidate floor and version drift fail independently', () => { + const f = fixture(); + f.put('plugins/dsh-genie-board/package.json', JSON.stringify({ version, minimumGenieVersion: '5.0.0' })); + f.pack(); + expect(() => verifyArtifacts(f.artifacts, version)).toThrow('minimumGenieVersion'); + f.put('plugins/dsh-genie-board/package.json', JSON.stringify({ version: '5.0.0', minimumGenieVersion: version })); + f.pack(); + expect(() => verifyArtifacts(f.artifacts, version)).toThrow('version mismatch'); +}); +test('signed mode rejects missing/empty sidecars, descriptor mismatch and invalid trust material', () => { + const f = fixture(); + const first = join(f.artifacts, `genie-${version}-darwin-arm64.tar.gz`); + writeFileSync(`${first}.bundle`, ''); + expect(() => verifyArtifacts(f.artifacts, version, 'stable')).toThrow('empty'); + f.pack(); + rmSync(`${first}.intoto.jsonl`); + expect(() => verifyArtifacts(f.artifacts, version, 'stable')).toThrow(); + f.pack(); + const descriptor = JSON.parse(readFileSync(`${first}.stable.delivery.json`, 'utf8')); + descriptor.artifactSha256 = '0'.repeat(64); + writeFileSync(`${first}.stable.delivery.json`, JSON.stringify(descriptor)); + expect(() => verifyArtifacts(f.artifacts, version, 'stable')).toThrow('descriptor'); + f.pack(); + const tools = join(f.root, 'tools'); + mkdirSync(tools); + writeFileSync(join(tools, 'cosign'), '#!/bin/sh\necho rejected-signature >&2\nexit 1\n', { mode: 0o755 }); + writeFileSync(join(tools, 'slsa-verifier'), '#!/bin/sh\nexit 0\n', { mode: 0o755 }); + const previousPath = process.env.PATH; + try { + process.env.PATH = `${tools}:${previousPath}`; + expect(() => verifyArtifacts(f.artifacts, version, 'stable')).toThrow('cosign signature verification failed'); + writeFileSync(join(tools, 'cosign'), '#!/bin/sh\nexit 0\n', { mode: 0o755 }); + writeFileSync(join(tools, 'slsa-verifier'), '#!/bin/sh\necho rejected-provenance >&2\nexit 1\n', { mode: 0o755 }); + expect(() => verifyArtifacts(f.artifacts, version, 'stable')).toThrow('SLSA provenance verification failed'); + } finally { + process.env.PATH = previousPath; + } +}); +test('both publication paths verify signed payloads before reconciliation', () => { + const workflow = readFileSync(join(import.meta.dir, '../.github/workflows/release-publish.yml'), 'utf8'); + expect(workflow.match(/verify-dsh-genie-board-release.ts --signed-artifact-dir/g)?.length).toBe(2); +}); + +test('signed orchestration verifies all four signatures, provenance and endorsed descriptors; rejects rewritten source', () => { + // These deterministic executables prove invocation and failure propagation, not cryptographic validity. + const f = fixture(); + const tools = join(f.root, 'tools'); + mkdirSync(tools); + const log = join(f.root, 'calls'); + for (const name of ['cosign', 'slsa-verifier']) + writeFileSync(join(tools, name), `#!/bin/sh\necho ${name} >> '${log}'\nexit 0\n`, { mode: 0o755 }); + writeFileSync( + join(tools, 'gh'), + `#!/usr/bin/env bun +import { readFileSync, appendFileSync, readdirSync, copyFileSync } from 'node:fs'; +const args = process.argv.slice(2); +if (args.includes('--help')) process.exit(1); +if (args[0] === 'release' && args[1] === 'download') { + const directory = args[args.indexOf('--dir') + 1]; + for (const name of readdirSync(${JSON.stringify(f.artifacts)})) copyFileSync(${JSON.stringify(f.artifacts)} + '/' + name, directory + '/' + name); + process.exit(0); +} +if (args[0] === 'release' && args[1] === 'view') { console.log(JSON.stringify({ tagName: 'v${version}', isDraft: false, isPrerelease: false })); process.exit(0); } +if (args[0] === 'api') { console.log(JSON.stringify({ sha: process.env.DSH_TEST_TAG_SHA || '${'a'.repeat(40)}' })); process.exit(0); } +appendFileSync(${JSON.stringify(log)}, 'descriptor\\n'); +const bundle = args[args.indexOf('--bundle') + 1]; +console.log(JSON.stringify([{ verificationResult: { statement: { predicate: JSON.parse(readFileSync(bundle, 'utf8')) } } }])); +`, + { mode: 0o755 }, + ); + for (const platform of platforms) { + const descriptor = join(f.artifacts, `genie-${version}-${platform}.tar.gz.stable.delivery.json`); + writeFileSync(`${descriptor}.sigstore.json`, readFileSync(descriptor)); + } + const previousPath = process.env.PATH; + try { + process.env.PATH = `${tools}:${previousPath}`; + expect(verifyArtifacts(f.artifacts, version, 'stable')).toBe('a'.repeat(40)); + const calls = readFileSync(log, 'utf8').trim().split('\n'); + for (const name of ['cosign', 'slsa-verifier', 'descriptor']) + expect(calls.filter((call) => call === name)).toHaveLength(4); + const cli = join(import.meta.dir, 'verify-dsh-genie-board-release.ts'); + const release = Bun.spawnSync(['bun', cli, '--release', `v${version}`, '--channel', 'stable'], { + env: { ...process.env }, + stdout: 'pipe', + stderr: 'pipe', + }); + expect(release.exitCode).toBe(0); + const wrongTag = Bun.spawnSync(['bun', cli, '--release', `v${version}`, '--channel', 'stable'], { + env: { ...process.env, DSH_TEST_TAG_SHA: 'b'.repeat(40) }, + stdout: 'pipe', + stderr: 'pipe', + }); + expect(wrongTag.exitCode).toBe(1); + expect(wrongTag.stderr.toString()).toContain('published tag/source binding mismatch'); + // Tamper all descriptors consistently; the cryptographically endorsed predicate still binds original source. + for (const platform of platforms) { + const descriptorPath = join(f.artifacts, `genie-${version}-${platform}.tar.gz.stable.delivery.json`); + const descriptor = JSON.parse(readFileSync(descriptorPath, 'utf8')); + descriptor.sourceSha = 'b'.repeat(40); + writeFileSync(descriptorPath, JSON.stringify(descriptor)); + } + expect(() => verifyArtifacts(f.artifacts, version, 'stable')).toThrow('verified delivery predicate'); + } finally { + process.env.PATH = previousPath; + } +}); + +test('a missing member on only the final platform fails after the other three complete', () => { + const f = fixture(); + rmSync(join(f.payload, 'plugins/dsh-genie-board/dist/client.js')); + const path = join(f.artifacts, `genie-${version}-linux-x64-musl.tar.gz`); + expect(Bun.spawnSync(['tar', '-czf', path, '-C', f.payload, '.']).exitCode).toBe(0); + expect(() => verifyArtifacts(f.artifacts, version)).toThrow(); +}); + +test('archive symlinks fail before extraction', () => { + const f = fixture(); + symlinkSync('/tmp', join(f.payload, 'outside')); + f.pack(); + expect(() => verifyArtifacts(f.artifacts, version)).toThrow('unsafe archive entry type'); +}); diff --git a/scripts/verify-dsh-genie-board-release.ts b/scripts/verify-dsh-genie-board-release.ts new file mode 100644 index 000000000..52b932eea --- /dev/null +++ b/scripts/verify-dsh-genie-board-release.ts @@ -0,0 +1,181 @@ +#!/usr/bin/env bun +import { createHash } from 'node:crypto'; +import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { isDeepStrictEqual } from 'node:util'; +import { verifyReleasePayloadVersion } from './release-payload-version'; + +const platforms = ['linux-x64-glibc', 'linux-x64-musl', 'linux-arm64', 'darwin-arm64']; +const members = [ + 'package.json', + 'agent.cordis.yml', + 'cordis.patch.yml', + 'README.md', + 'NOTICE', + 'dist/index.js', + 'dist/client.js', +]; +const repository = 'automagik-dev/genie'; +function run(argv: string[]): string { + const result = Bun.spawnSync(argv, { stdout: 'pipe', stderr: 'pipe', timeout: 120_000, env: { ...process.env } }); + if (result.exitCode !== 0) throw new Error(`${argv[0]} failed: ${result.stderr.toString()}`); + return result.stdout.toString(); +} +function nonempty(path: string): void { + if (!statSync(path).isFile() || !statSync(path).size) throw new Error(`missing/empty regular file: ${path}`); +} +function verifyTarball(path: string, version: string): void { + // Reject links, special entries and escaping/duplicate names before extraction. + const names = run(['tar', '-tzf', path]).trim().split('\n'); + const seen = new Set<string>(); + for (const name of names) { + const normalized = name.replace(/^\.\//, '').replace(/\/$/, ''); + if (name.startsWith('/') || normalized.split('/').includes('..') || seen.has(normalized)) + throw new Error('unsafe archive path'); + seen.add(normalized); + } + if ( + run(['tar', '-tvzf', path]) + .trim() + .split('\n') + .some((line) => !['-', 'd'].includes(line[0])) + ) + throw new Error('unsafe archive entry type'); + const root = mkdtempSync(join(tmpdir(), 'genie-dsh-verify-')); + try { + run(['tar', '-xzf', path, '-C', root, '--no-same-owner']); + for (const member of members) nonempty(join(root, 'plugins/dsh-genie-board', member)); + verifyReleasePayloadVersion(root, version); + } finally { + rmSync(root, { recursive: true, force: true }); + } +} +export function verifyArtifacts(directory: string, version: string, channel?: 'stable' | 'dev'): string | undefined { + if (!/^[0-9A-Za-z][0-9A-Za-z.+-]{0,127}$/.test(version)) throw new Error('invalid version'); + const expected = platforms.map((platform) => `genie-${version}-${platform}.tar.gz`).sort(); + const actual = readdirSync(directory) + .filter((name) => name.endsWith('.tar.gz')) + .sort(); + if (JSON.stringify(actual) !== JSON.stringify(expected)) + throw new Error('expected exactly four candidate platform tarballs'); + let sourceSha: string | undefined; + for (const [index, name] of expected.entries()) { + const path = resolve(directory, name); + nonempty(path); + if (channel) { + nonempty(`${path}.bundle`); + nonempty(`${path}.intoto.jsonl`); + const descriptor = JSON.parse(readFileSync(`${path}.${channel}.delivery.json`, 'utf8')); + const platform = platforms.find((value) => name === `genie-${version}-${value}.tar.gz`); + const digest = createHash('sha256').update(readFileSync(path)).digest('hex'); + if ( + descriptor.artifactSha256 !== digest || + descriptor.version !== version || + descriptor.channel !== channel || + descriptor.releaseName !== name || + descriptor.releaseTag !== `v${version}` || + descriptor.repository !== repository || + descriptor.platformId !== platform || + !/^[a-f0-9]{40}$/.test(descriptor.sourceSha) || + (channel === 'stable' && descriptor.sourceBranch !== 'main') + ) + throw new Error(`descriptor binding mismatch: ${name}`); + if (index && sourceSha !== descriptor.sourceSha) throw new Error('candidate source mismatch'); + sourceSha = descriptor.sourceSha; + run(['bash', join(import.meta.dir, 'verify-release.sh'), '--local', path]); + const descriptorPath = `${path}.${channel}.delivery.json`; + nonempty(`${descriptorPath}.sigstore.json`); + const verified = JSON.parse( + run([ + 'gh', + 'attestation', + 'verify', + descriptorPath, + '--bundle', + `${descriptorPath}.sigstore.json`, + '--repo', + repository, + '--predicate-type', + `https://github.com/${repository}/delivery-evidence/v1`, + '--cert-identity', + `https://github.com/${repository}/.github/workflows/release-publish.yml@refs/heads/main`, + '--source-ref', + 'refs/heads/main', + '--format', + 'json', + ]), + ); + if ( + !Array.isArray(verified) || + verified.length !== 1 || + !isDeepStrictEqual(verified[0]?.verificationResult?.statement?.predicate, descriptor) + ) { + throw new Error('verified delivery predicate does not equal descriptor'); + } + } + verifyTarball(path, version); + } + return sourceSha; +} +function main(): void { + const args = process.argv.slice(2); + const options = new Map<string, string>(); + for (let i = 0; i < args.length; i += 2) { + const key = args[i]; + const value = args[i + 1]; + if ( + !['--unsigned-artifact-dir', '--signed-artifact-dir', '--release', '--version', '--channel'].includes(key) || + !value || + options.has(key) + ) + throw new Error('invalid arguments'); + options.set(key, value); + } + const modes = ['--unsigned-artifact-dir', '--signed-artifact-dir', '--release'].filter((key) => options.has(key)); + if (modes.length !== 1) throw new Error('choose exactly one artifact/release mode'); + const mode = modes[0]; + const input = options.get(mode) as string; + const version = mode === '--release' ? input.replace(/^v/, '') : options.get('--version'); + const channel = options.get('--channel') ?? 'stable'; + if ( + !version || + !['stable', 'dev'].includes(channel) || + (mode === '--release' && input !== `v${version}`) || + (options.has('--version') && options.get('--version') !== version) + ) + throw new Error('invalid candidate/channel'); + const directory = mode === '--release' ? mkdtempSync(join(tmpdir(), 'genie-dsh-release-')) : resolve(input); + try { + if (mode === '--release') run(['gh', 'release', 'download', input, '--repo', repository, '--dir', directory]); + const source = verifyArtifacts( + directory, + version, + mode === '--unsigned-artifact-dir' ? undefined : (channel as 'stable' | 'dev'), + ); + if (mode === '--release') { + const release = JSON.parse( + run(['gh', 'release', 'view', input, '--repo', repository, '--json', 'tagName,isDraft,isPrerelease']), + ); + const commit = JSON.parse(run(['gh', 'api', `repos/${repository}/commits/${input}`])); + if ( + release.tagName !== input || + release.isDraft || + (channel === 'stable' && release.isPrerelease) || + commit.sha !== source + ) + throw new Error('published tag/source binding mismatch'); + } + console.log(`DSH payload verified (${mode.slice(2)}, ${version}, four platforms)`); + } finally { + if (mode === '--release') rmSync(directory, { recursive: true, force: true }); + } +} +if (import.meta.main) { + try { + main(); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + } +} From c2fc02596f8416c9bb7e8a057c482ab652188c16 Mon Sep 17 00:00:00 2001 From: Felipe Rosa <felipe@namastex.ai> Date: Mon, 7 Sep 2026 20:24:20 +0000 Subject: [PATCH 14/24] fix(roadmap): ignore JSON object key ordering during sync Recover the preserved key-order fix with deterministic lexical ordering. Keep real content and array order changes detectable and preserve pending edits when legacy markers require explicit resolution. Co-authored-by: Felipe Rosa <felipe@namastex.ai> --- src/lib/v5/roadmap-sync.ts | 12 +++++-- src/lib/v5/task-state.test.ts | 55 ++++++++++++++++++++++++++-- src/term-commands/v5-task.test.ts | 59 +++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 4 deletions(-) diff --git a/src/lib/v5/roadmap-sync.ts b/src/lib/v5/roadmap-sync.ts index 5e1c0a449..7f6f1aa8b 100644 --- a/src/lib/v5/roadmap-sync.ts +++ b/src/lib/v5/roadmap-sync.ts @@ -40,9 +40,17 @@ export function resolveSyncMarkerPath(cwd?: string): string { return join(resolveRepoRoot(cwd), '.genie', 'roadmap-sync'); } -/** Content hash over the canonical (whitespace-independent) JSON form. */ +/** Content hash over the canonical JSON form, independent of whitespace and object-key order. */ function canonicalHash(value: unknown): string { - return createHash('sha256').update(JSON.stringify(value)).digest('hex'); + const canonical = JSON.stringify(value, (_key, current: unknown) => { + if (current === null || Array.isArray(current) || typeof current !== 'object') return current; + return Object.fromEntries( + Object.keys(current) + .sort() + .map((key) => [key, (current as Record<string, unknown>)[key]]), + ); + }); + return createHash('sha256').update(canonical).digest('hex'); } /** diff --git a/src/lib/v5/task-state.test.ts b/src/lib/v5/task-state.test.ts index d90a067e6..565610ee1 100644 --- a/src/lib/v5/task-state.test.ts +++ b/src/lib/v5/task-state.test.ts @@ -1061,11 +1061,62 @@ describe('declared routing — roster allowlist + assignment state API (W1)', () describe('declared routing — roadmap snapshot round-trip (roadmap-sync lockstep)', () => { // Mirrors roadmap-sync's canonicalHash: sha256 over the parsed JSON form, so - // whitespace/formatting differences never count as content changes. + // whitespace and object-key order never count as content changes. function canonicalHash(value: unknown): string { - return createHash('sha256').update(JSON.stringify(value)).digest('hex'); + const canonical = JSON.stringify(value, (_key, current: unknown) => { + if (current === null || Array.isArray(current) || typeof current !== 'object') return current; + return Object.fromEntries( + Object.keys(current) + .sort() + .map((key) => [key, (current as Record<string, unknown>)[key]]), + ); + }); + return createHash('sha256').update(canonical).digest('hex'); } + test('an equal file/db pair refreshes an old order-sensitive marker without rewriting the snapshot', () => { + const repo = join(dir, 'hash-upgrade'); + mkdirSync(join(repo, '.genie'), { recursive: true }); + createTask(db, { title: 'existing card' }); + const snapshot = roadmapSnapshot(db); + const legacyHash = createHash('sha256').update(JSON.stringify(snapshot)).digest('hex'); + const filePath = join(repo, '.genie', 'roadmap.json'); + const markerPath = join(repo, '.genie', 'roadmap-sync'); + const content = `${JSON.stringify(snapshot, null, 2)}\n`; + writeFileSync(filePath, content); + writeFileSync(markerPath, JSON.stringify({ fileHash: legacyHash, dbHash: legacyHash })); + + expect(syncRoadmap(db, repo).action).toBe('none'); + expect(readFileSync(filePath, 'utf-8')).toBe(content); + const marker = JSON.parse(readFileSync(markerPath, 'utf-8')); + expect(marker.fileHash).not.toBe(legacyHash); + expect(marker.fileHash).toBe(marker.dbHash); + expect(syncRoadmap(db, repo).action).toBe('none'); + }); + + test('an old order-sensitive marker with pending edits refuses to overwrite either side', () => { + const repo = join(dir, 'hash-upgrade-pending'); + mkdirSync(join(repo, '.genie'), { recursive: true }); + createTask(db, { title: 'existing card' }); + const snapshot = roadmapSnapshot(db); + const legacyHash = createHash('sha256').update(JSON.stringify(snapshot)).digest('hex'); + const filePath = join(repo, '.genie', 'roadmap.json'); + const markerPath = join(repo, '.genie', 'roadmap-sync'); + const content = `${JSON.stringify(snapshot, null, 2)}\n`; + const marker = JSON.stringify({ fileHash: legacyHash, dbHash: legacyHash }); + writeFileSync(filePath, content); + writeFileSync(markerPath, marker); + const pending = createTask(db, { title: 'unpublished card' }); + + const result = syncRoadmap(db, repo); + expect(result.action).toBe('diverged'); + expect(result.message).toContain('genie task import --replace'); + expect(result.message).toContain('genie task export --write'); + expect(readFileSync(filePath, 'utf-8')).toBe(content); + expect(readFileSync(markerPath, 'utf-8')).toBe(marker); + expect(getTask(db, pending.id)?.title).toBe('unpublished card'); + }); + test('export carries assigned_agent/assigned_reason (SELECT *) and round-trips them through import', () => { const a = createTask(db, { title: 'a', assignedAgent: 'codex', assignedReason: 'dissent on parser' }); const snapshot = exportState(db); diff --git a/src/term-commands/v5-task.test.ts b/src/term-commands/v5-task.test.ts index c2353456f..4eb6b1169 100644 --- a/src/term-commands/v5-task.test.ts +++ b/src/term-commands/v5-task.test.ts @@ -1215,6 +1215,65 @@ describe('roadmap.json canonical sync', () => { expect(settled.code).toBe(0); }); + test('imported snapshots with reordered object keys remain in sync', async () => { + const db = openDb({ cwd: repo }); + createTask(db, { title: 'canonical card' }); + db.close(); + + const published = await cli(repo, 'export', '--write'); + expect(published.stderr).toBe(''); + expect(published.code).toBe(0); + const snapshotPath = join(repo, '.genie', 'roadmap.json'); + const snapshot = JSON.parse(readFileSync(snapshotPath, 'utf-8')) as unknown; + const reorderKeys = (value: unknown): unknown => { + if (Array.isArray(value)) return value.map(reorderKeys); + if (value !== null && typeof value === 'object') { + return Object.fromEntries( + Object.entries(value as Record<string, unknown>) + .reverse() + .map(([key, child]) => [key, reorderKeys(child)]), + ); + } + return value; + }; + writeFileSync(snapshotPath, `${JSON.stringify(reorderKeys(snapshot), null, 2)}\n`); + + const imported = await cli(repo, 'import', '--replace'); + expect(imported.code).toBe(0); + expect(imported.stderr).toBe(''); + const settled = await cli(repo, 'sync'); + expect(settled.code).toBe(0); + expect(settled.stdout).toContain('in sync (none)'); + expect(settled.stderr).toBe(''); + }); + + test.each(['content', 'array order'])('sync detects changed %s after a canonical baseline', async (change) => { + const db = openDb({ cwd: repo }); + createTask(db, { title: 'first card' }); + createTask(db, { title: 'second card' }); + db.close(); + const published = await cli(repo, 'export', '--write'); + expect(published.code).toBe(0); + expect(published.stderr).toBe(''); + + const snapshotPath = join(repo, '.genie', 'roadmap.json'); + const snapshot = JSON.parse(readFileSync(snapshotPath, 'utf-8')) as StateExport; + if (change === 'content') snapshot.tasks[0].title = 'changed card'; + else snapshot.tasks.reverse(); + writeFileSync(snapshotPath, `${JSON.stringify(snapshot, null, 2)}\n`); + + const synced = await cli(repo, 'sync'); + expect(synced.code).toBe(0); + expect(synced.stderr).toBe(''); + expect(synced.stdout).toContain('Board refreshed'); + const imported = openDb({ cwd: repo }); + try { + expect(getTask(imported, snapshot.tasks[0].id)?.title).toBe(snapshot.tasks[0].title); + } finally { + imported.close(); + } + }); + test('a subdirectory spelling of roadmap.json is roadmap-sliced, and is not the canonical baseline', async () => { const db = openDb({ cwd: repo }); createTask(db, { title: 'card' }); From a4d4426df043fefd36558961ac15c0c9f1a268d1 Mon Sep 17 00:00:00 2001 From: Felipe Rosa <felipe@namastex.ai> Date: Mon, 7 Sep 2026 20:30:32 +0000 Subject: [PATCH 15/24] docs(skills): resolve per-group fix-loop budgets explicitly Re-author the budget guidance proposed in Liraz Siri's PR #2738. Keep the default of two and all repair safeguards across surviving guides. Preserve separate quality, CI and model-escalation limits. Co-authored-by: Felipe Rosa <felipe@namastex.ai> --- scripts/release-docs.test.ts | 32 ++++++++++++++++++++++++++++- skills/dream/SKILL.md | 4 +++- skills/fix/SKILL.md | 18 ++++++++-------- skills/genie-orca-review/SKILL.md | 4 +++- skills/genie-orca-work/SKILL.md | 4 +++- skills/genie/reference/lifecycle.md | 4 +++- skills/review/SKILL.md | 7 +++++-- skills/work/SKILL.md | 6 ++++-- 8 files changed, 62 insertions(+), 17 deletions(-) diff --git a/scripts/release-docs.test.ts b/scripts/release-docs.test.ts index abae40576..36e2110ed 100644 --- a/scripts/release-docs.test.ts +++ b/scripts/release-docs.test.ts @@ -982,10 +982,40 @@ describe('Group E release and documentation contracts', () => { expect(review).toContain('unjustified stateful machinery'); expect(review).toContain('a HIGH gap'); for (const lifecycleSkill of [review, fix, work]) expect(lifecycleSkill).toContain('`overdesigned-plan`'); - expect(fix).toContain('up to 2 loops'); + expect(fix).toContain('up to `B` loops'); expect(work).toContain('A user-approved simplification invalidates the superseded plan/review evidence'); }); + test('fix-loop budgets preserve explicit override authority and repair safeguards across lifecycle routes', () => { + for (const path of [ + 'skills/fix/SKILL.md', + 'skills/review/SKILL.md', + 'skills/work/SKILL.md', + 'skills/dream/SKILL.md', + 'skills/genie/reference/lifecycle.md', + 'skills/genie-orca-review/SKILL.md', + 'skills/genie-orca-work/SKILL.md', + ]) { + const skill = read(path); + expect(skill).toContain('budget `B` once per group: default 2'); + expect(skill).toContain('only an explicit higher-priority user/workspace instruction'); + expect(skill).toContain('another positive integer'); + expect(skill).toContain('Carry `B` and the attempts already used across handoffs'); + expect(skill).toContain( + 'Overrides never expand scope, permit unchanged retries, or skip diagnosis or independent re-review', + ); + expect(skill).not.toMatch(/(?:max|at most|up to|Cap) (?:2|\*\*2\*\*) (?:fix )?loops|after 2 (?:failed )?loops/); + } + for (const skill of ['fix', 'review', 'work']) { + expect(read(`skills/${skill}/SKILL.md`)).toContain('at most two escalation attempts per group'); + } + expect(read('skills/fix/SKILL.md')).toContain('effort_escalations=<used>/2'); + expect(read('skills/work/SKILL.md')).toContain('On FIX-FIRST, one fix loop.'); + expect(read('skills/work/SKILL.md')).toContain('This separate quality-pass cap is not expanded by `B`.'); + expect(read('skills/dream/SKILL.md')).toContain('max 3 attempts; poll CI status'); + expect(read('skills/genie-orca-work/SKILL.md')).toContain('re-dispatch one tier up, once.'); + }); + test('router pays Genie lifecycle cost only when it adds value', () => { const router = read('skills/genie/SKILL.md'); const lifecycle = read('skills/genie/reference/lifecycle.md'); diff --git a/skills/dream/SKILL.md b/skills/dream/SKILL.md index 7c4b2f114..79f857861 100644 --- a/skills/dream/SKILL.md +++ b/skills/dream/SKILL.md @@ -57,11 +57,13 @@ Each worker, independently: ## Phase 2: Review + PR +Resolve the fix-loop budget `B` once per group: default 2; only an explicit higher-priority user/workspace instruction may set another positive integer. Carry `B` and the attempts already used across handoffs; do not reset the budget when switching skills. Overrides never expand scope, permit unchanged retries, or skip diagnosis or independent re-review. + **Trigger:** all workers in the layer reported done or blocked. 1. Dispatch one reviewer subagent per PR via the native delegation surface (reviewer ≠ worker) to run `review` against the wish's acceptance criteria. 2. Read bot comments critically — never blindly accept automated findings. -3. On FIX-FIRST: diagnose first; return an overdesigned plan to wish/design review, otherwise dispatch `fix` for valid gaps (max 2 loops per PR). On another architectural issue: escalate in the report, no fix attempt. +3. On FIX-FIRST: diagnose first; return an overdesigned plan to wish/design review, otherwise dispatch `fix` for valid gaps (at most `B` loops per group, including attempts already used during execution). On another architectural issue: escalate in the report, no fix attempt. 4. CI must be green before proceeding — poll status, do not sleep. 5. On SHIP: mark the PR review-complete. diff --git a/skills/fix/SKILL.md b/skills/fix/SKILL.md index 8832fbb37..0f67f97cd 100644 --- a/skills/fix/SKILL.md +++ b/skills/fix/SKILL.md @@ -1,13 +1,15 @@ --- name: fix -description: "Dispatch fix subagent for FIX-FIRST gaps from review, re-review, then diagnose unresolved failures after 2 loops." +description: "Dispatch fix subagent for FIX-FIRST gaps from review, re-review, then diagnose unresolved failures at the resolved budget (default 2)." --- # fix — Fix-Review Loop **Runtime syntax:** invoke the plugin copy through the active runtime's owner-qualified skill selector; use a bare selector only when intentionally selecting a user-tier copy (a separately installed personal copy; Genie no longer seeds this tier). Cross-skill prose below uses bare names as portable semantic routes; the orchestrator resolves the selector for the active runtime. -Resolve FIX-FIRST gaps from `review`: dispatch a fix subagent, re-review, repeat up to 2 loops, then diagnose and route any unresolved failure. +Resolve FIX-FIRST gaps from `review`: dispatch a fix subagent, re-review, repeat up to `B` loops, then diagnose and route any unresolved failure. + +Resolve the fix-loop budget `B` once per group: default 2; only an explicit higher-priority user/workspace instruction may set another positive integer. Carry `B` and the attempts already used across handoffs; do not reset the budget when switching skills. Overrides never expand scope, permit unchanged retries, or skip diagnosis or independent re-review. ## When to Use - `review` returned a **FIX-FIRST** verdict with CRITICAL or HIGH gaps @@ -22,8 +24,8 @@ Resolve FIX-FIRST gaps from `review`: dispatch a fix subagent, re-review, repeat | Verdict | Condition | Action | |---------|-----------|--------| | SHIP | — | Done. Return to orchestrator. | -| FIX-FIRST | loop < 2 | Increment loop, go to step 2. | -| FIX-FIRST | loop = 2 | Stop fixing and run Escalation Diagnosis; max loops reached. | +| FIX-FIRST | loop < B | Increment loop, go to step 2. | +| FIX-FIRST | loop = B | Stop fixing and run Escalation Diagnosis; max loops reached. | | BLOCKED | — | Run Escalation Diagnosis and take the cause-specific route. | 5. **Route the diagnosis:** report the remaining gaps with exact files, failing checks, cause class, and corrective route; the group's task stays `in_progress`. @@ -59,14 +61,14 @@ The fix loop never mutates task state. The group's task stays `in_progress` thro ## Diagnosis / Appeal Format ``` -Fix loop exhausted (2/2). Group remains in progress. +Fix loop exhausted (<used>/B). Group remains in progress. Remaining gaps: - [CRITICAL] <gap description> — <file> - [HIGH] <gap description> — <file> Cause: <model-capacity|missing-context|ambiguous-spec|env-tool-failure|overdesigned-plan> New evidence: <new output/diagnosis, or "none — model/effort escalation prohibited"> Corrective route: <one cause-specific next step> -Budget: attempts=<used>/2; effort_escalations=<used>/2 +Budget: attempts=<used>/B; effort_escalations=<used>/2 Appeal: <reviewer/final-gate disagreement record, or "none"> ``` @@ -79,12 +81,12 @@ Appeal: <reviewer/final-gate disagreement record, or "none"> - [HIGH] sendMessage result not checked — dispatch.ts:541 ``` -Loop 1: native delegation surface → fixer briefed with both gaps, the wish criteria, and `bun test` as validation. The fixer edits, runs the validation, reports its changes with outcomes, and ends `done`. Then native delegation surface → a fresh reviewer briefed to re-run `review` against the same criteria. SHIP → report success to the orchestrator. FIX-FIRST again → loop 2; after that, classify the cause and take its corrective route. A model or effort raise is permitted only for evidenced `model-capacity` within both caps. An `overdesigned-plan` diagnosis stops immediately and returns to planning instead. +With the default `B = 2`, loop 1: native delegation surface → fixer briefed with both gaps, the wish criteria, and `bun test` as validation. The fixer edits, runs the validation, reports its changes with outcomes, and ends `done`. Then native delegation surface → a fresh reviewer briefed to re-run `review` against the same criteria. SHIP → report success to the orchestrator. FIX-FIRST again → loop 2; after that, classify the cause and take its corrective route. A model or effort raise is permitted only for evidenced `model-capacity` within both caps. An `overdesigned-plan` diagnosis stops immediately and returns to planning instead. ## Rules - Tight scope: fix exactly the tagged gaps — no unrequested refactors, features, or drive-by cleanups. - Never fix and review in the same session — always separate subagents. -- Never exceed 2 fix loops — stop, diagnose, and take the cause-specific route. +- Never exceed `B` fix loops — stop, diagnose, and take the cause-specific route. - Never use a fix loop to preserve optional machinery when a simpler plan satisfies the user stories. - Include the original wish criteria in every fix dispatch. - Identical gaps across loops = no progress; classify the cause. Repetition is not new evidence and never authorizes a model or effort raise. diff --git a/skills/genie-orca-review/SKILL.md b/skills/genie-orca-review/SKILL.md index 9c5e553d3..6548f5a3c 100644 --- a/skills/genie-orca-review/SKILL.md +++ b/skills/genie-orca-review/SKILL.md @@ -29,7 +29,9 @@ A reviewer is a **read-only worker dispatched by the coordinator**, never the en ## Fix loop -Coordinator re-dispatches a **fast** worker into the same worktree with the findings quoted verbatim and "apply exactly this, nothing else". Cap 2 loops per group; the coordinator may verify a trivial delta itself instead of a second review. After the cap → human gate. +Resolve the fix-loop budget `B` once per group: default 2; only an explicit higher-priority user/workspace instruction may set another positive integer. Carry `B` and the attempts already used across handoffs; do not reset the budget when switching skills. Overrides never expand scope, permit unchanged retries, or skip diagnosis or independent re-review. + +Coordinator re-dispatches a **fast** worker into the same worktree with the findings quoted verbatim and "apply exactly this, nothing else". Cap `B` loops per group; the coordinator may verify a trivial delta itself instead of a second review. After the cap → human gate. ## What the integrated gate catches that group review does not diff --git a/skills/genie-orca-work/SKILL.md b/skills/genie-orca-work/SKILL.md index e8c3e06f9..d57594d08 100644 --- a/skills/genie-orca-work/SKILL.md +++ b/skills/genie-orca-work/SKILL.md @@ -19,6 +19,8 @@ description: "Coordinator loop for an approved wish on Orca — one Run per wish ## The loop (verbatim command shapes) +Resolve the fix-loop budget `B` once per group: default 2; only an explicit higher-priority user/workspace instruction may set another positive integer. Carry `B` and the attempts already used across handoffs; do not reset the budget when switching skills. Overrides never expand scope, permit unchanged retries, or skip diagnosis or independent re-review. + ```bash ORCA orchestration run-create --objective "<wish slug> (<LINEAR-ID>): <one line>" --json # one task per group; spec = self-contained engineer brief (template below); deps = the wish's depends_on @@ -36,7 +38,7 @@ Per message in a Delivery: - `worker_done` (payload carries taskId/dispatchId/outcome/filesModified): 1. `ORCA orchestration worker-release --dispatch <id> --json` (always, success or failure; keep live only on explicit user request via `worker-retain`). 2. **Review**: `task-create` a read-only review brief (template below), `worker-start --task <review> --worktree name:<slug>-g<n> --agent <reviewer>` — a different agent/model than the engineer. - 3. On review `worker_done`: parse `VERDICT:` — `SHIP` → mark group done in your head and in Linear (below); `FIX-FIRST` → `task-create` a fix brief quoting the findings, dispatch a fast worker into the same worktree (`--terminal <engineer handle>` if still live, else `--worktree name:…`), max **2** loops, then escalate to the human gate; `BLOCKED` → stop the group, post the blocker on the Linear child, continue other groups. + 3. On review `worker_done`: parse `VERDICT:` — `SHIP` → mark group done in your head and in Linear (below); `FIX-FIRST` → `task-create` a fix brief quoting the findings, dispatch a fast worker into the same worktree (`--terminal <engineer handle>` if still live, else `--worktree name:…`), at most `B` loops per group, then escalate to the human gate; `BLOCKED` → stop the group, post the blocker on the Linear child, continue other groups. - Acknowledge only after every message is handled: `ORCA orchestration check --ack <delivery_id> --wait … --json`. Dependent groups become `ready` automatically when their deps complete; start them on the next sweep (`task-list --ready --brief --json`). An integrator group (full gate, docs, tripwires) runs last on the integrated wish branch: merge each group branch into the wish branch **yourself** (coordinator-owned git), then dispatch. diff --git a/skills/genie/reference/lifecycle.md b/skills/genie/reference/lifecycle.md index 5401ccc69..8d61b24a8 100644 --- a/skills/genie/reference/lifecycle.md +++ b/skills/genie/reference/lifecycle.md @@ -46,6 +46,8 @@ external blocker is recorded; authorized merge plus required QA → `SHIPPED`. The reviewer remains read-only and never edits WISH.md or task state. A chat verdict that was not persisted does not advance the lifecycle. +Resolve the fix-loop budget `B` once per group: default 2; only an explicit higher-priority user/workspace instruction may set another positive integer. Carry `B` and the attempts already used across handoffs; do not reset the budget when switching skills. Overrides never expand scope, permit unchanged retries, or skip diagnosis or independent re-review. + ## Skill Catalog | Skill | Purpose | When to use | @@ -54,7 +56,7 @@ verdict that was not persisted does not advance the lifecycle. | `wish` | Convert a design into a structured plan at `.genie/wishes/<slug>/WISH.md` — scope, execution groups, acceptance criteria, validation | Idea is concrete, needs a plan | | `review` | Genie criteria gate — SHIP / FIX-FIRST / BLOCKED with severity-tagged gaps | Before and after `work`, or any plan/PR | | `work` | Execute an approved wish — dispatch native subagents per group in waves, fix loops, validation | Wish is SHIP-approved | -| `fix` | Resolve FIX-FIRST gaps, re-review, escalate after 2 failed loops | Review returned FIX-FIRST | +| `fix` | Resolve FIX-FIRST gaps, re-review, diagnose unresolved gaps after `B` loops (default 2) | Review returned FIX-FIRST | | `council` | Multi-perspective deliberation with specialist viewpoints | Major design decisions, tradeoffs | | `refine` | Transform a brief into a production-ready prompt | Prompt needs sharpening | | `report` | Investigate bugs — trace, capture evidence, open a GitHub issue with confirmation | Bug reports | diff --git a/skills/review/SKILL.md b/skills/review/SKILL.md index 5c63f582c..e44a70765 100644 --- a/skills/review/SKILL.md +++ b/skills/review/SKILL.md @@ -140,10 +140,13 @@ write. Never edit WISH.md, the brainstorm jar, or task state as the reviewer. | PR review (before merge) | Merge to `dev` (agents) or approve for human merge | ### FIX-FIRST loop + +Resolve the fix-loop budget `B` once per group: default 2; only an explicit higher-priority user/workspace instruction may set another positive integer. Carry `B` and the attempts already used across handoffs; do not reset the budget when switching skills. Overrides never expand scope, permit unchanged retries, or skip diagnosis or independent re-review. + 1. Diagnose first. For `overdesigned-plan`, return to `brainstorm`/`wish` without consuming a fix attempt. 2. Otherwise auto-invoke `fix` with the severity-tagged gap list. -3. After `fix` completes, re-run `review` (max 2 fix loops). -4. Still FIX-FIRST after 2 loops → return BLOCKED with an Escalation Diagnosis; never raise model or effort automatically. +3. After `fix` completes, re-run `review` (at most `B` fix loops per group). +4. Still FIX-FIRST after `B` loops → return BLOCKED with an Escalation Diagnosis; never raise model or effort automatically. When a failure's root cause is unclear, invoke `trace` before dispatching `fix` — `fix` then applies the cause-specific correction from the trace report. An unclear cause is not evidence of `model-capacity`. diff --git a/skills/work/SKILL.md b/skills/work/SKILL.md index 7329cd901..383005b21 100644 --- a/skills/work/SKILL.md +++ b/skills/work/SKILL.md @@ -17,6 +17,8 @@ When you are spawned as a subagent for a group, your dispatch prompt carries the - An approved wish exists and `review` returned SHIP on the plan - Orchestrator needs to dispatch implementation to subagents +Resolve the fix-loop budget `B` once per group: default 2; only an explicit higher-priority user/workspace instruction may set another positive integer. Carry `B` and the attempts already used across handoffs; do not reset the budget when switching skills. Overrides never expand scope, permit unchanged retries, or skip diagnosis or independent re-review. + ## Flow 1. **Load and enter execution:** read `.genie/wishes/<slug>/WISH.md` and require persisted status `APPROVED` (or `IN_PROGRESS` when resuming). Before the first dispatch, the orchestrator sets `APPROVED` → `IN_PROGRESS`; read group state with `genie task list --wish <slug>` (or `genie board --wish <slug>`). 2. **Pick the wave:** every group whose `depends-on` groups are done, per the wish's Execution Strategy. @@ -26,8 +28,8 @@ When you are spawned as a subagent for a group, your dispatch prompt carries the ``` If two agents race one task, exactly one wins; the loser gets a conflict error and stands down. 4. **Await completion — never poll:** background subagents notify you when they finish. Inspect `genie board --wish <slug>` on demand; completion is push, not poll. -5. **Local review:** per finished group, dispatch a reviewer subagent (reviewer ≠ engineer) to run `review` against that group's acceptance criteria. The orchestrator appends each returned evidence block under `## Review Results`; the reviewer never edits it. Diagnose before fixing: `overdesigned-plan` returns to wish/design review without consuming a fix attempt; other FIX-FIRST gaps may use at most 2 fix loops. -6. **Quality review:** dispatch a reviewer for a quality pass (security, maintainability, perf). On FIX-FIRST, one fix loop. +5. **Local review:** per finished group, dispatch a reviewer subagent (reviewer ≠ engineer) to run `review` against that group's acceptance criteria. The orchestrator appends each returned evidence block under `## Review Results`; the reviewer never edits it. Diagnose before fixing: `overdesigned-plan` returns to wish/design review without consuming a fix attempt; other FIX-FIRST gaps may use at most `B` fix loops. +6. **Quality review:** dispatch a reviewer for a quality pass (security, maintainability, perf). On FIX-FIRST, one fix loop. This separate quality-pass cap is not expanded by `B`. 7. **Validate:** run the group's validation command yourself through the active runtime's shell surface; record the output and the scope rationale as evidence. Confirm it remains proportional to the actual diff, widening it when implementation reached beyond the plan: documentation-only changes, including deterministic generated documentation or plugin skill mirrors, use From ce6fd7acceea60e82f941abccd1c1e68d178fd3c Mon Sep 17 00:00:00 2001 From: Felipe Rosa <felipe@namastex.ai> Date: Mon, 7 Sep 2026 20:47:58 +0000 Subject: [PATCH 16/24] fix(state): return hires atomically under concurrent removal Capture the roster row in the upsert with RETURNING, preserving defaults and original hired_at while eliminating the post-write read race. Exercise two processes hiring and removing the same row against one real SQLite database. Reauthored from the hireAgent race identified in Liraz Siri's PR #2737 review. Validated with the full gate (2031 pass, 1 skip, 0 fail) and isolated API plus built CLI backup/import dogfood. Co-authored-by: Felipe Rosa <felipe@namastex.ai> --- src/lib/v5/task-state.test.ts | 83 +++++++++++++++++++++++++++++++++++ src/lib/v5/task-state.ts | 20 +++++---- 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/src/lib/v5/task-state.test.ts b/src/lib/v5/task-state.test.ts index 565610ee1..076be19d6 100644 --- a/src/lib/v5/task-state.test.ts +++ b/src/lib/v5/task-state.test.ts @@ -1383,6 +1383,89 @@ describe('hire roster (single-row upsert / delete)', () => { }); }); +describe('multi-process hire/unhire return race', () => { + test('every hire returns its complete row even when another process unhires it', async () => { + const dbPath = join(dir, 'hire-unhire.db'); + const seed = openDb({ path: dbPath }); + hireAgent(seed, { wish: 'race', agentAdapterId: 'adapter', worktree: '/wt/seed' }); + seed.close(); + const workerPath = join(dir, 'hire-unhire-worker.ts'); + writeFileSync( + workerPath, + ` +import { openDb } from ${JSON.stringify(join(import.meta.dir, 'genie-db.ts'))}; +import { hireAgent, unhireAgent } from ${JSON.stringify(join(import.meta.dir, 'task-state.ts'))}; +const [dbPath, op] = process.argv.slice(2); +const db = openDb({ path: dbPath }); +process.stdout.write('ready'); +await Bun.stdin.text(); +let invalid = 0; +let removed = 0; +try { + for (let i = 0; i < 3000; i++) { + if (op === 'unhire') { + if (unhireAgent(db, 'race', 'adapter')) removed++; + } else { + const row = hireAgent(db, { + wish: 'race', agentAdapterId: 'adapter', profile: 'profile-' + i, + worktree: '/wt/' + i, state: 'active', + }); + if (!row || row.wish !== 'race' || row.agentAdapterId !== 'adapter' || + row.profile !== 'profile-' + i || row.worktree !== '/wt/' + i || + row.state !== 'active' || !Number.isInteger(row.hiredAt) || row.hiredAt <= 0) invalid++; + } + } + process.stdout.write(JSON.stringify({ invalid, removed })); +} finally { + db.close(); +} +`, + ); + const workers = ['hire', 'unhire'].map((op) => + Bun.spawn(['bun', 'run', workerPath, dbPath, op], { + stdin: 'pipe', + stdout: 'pipe', + stderr: 'pipe', + env: { ...process.env, HOME: dir, GENIE_HOME: join(dir, '.genie') }, + }), + ); + try { + // Both handles are open before either loop begins, so startup cannot + // serialize away the race. The child waits for stdin EOF after readiness. + await Promise.all( + workers.map(async (worker) => { + const reader = worker.stdout.getReader(); + const ready = await reader.read(); + reader.releaseLock(); + expect(new TextDecoder().decode(ready.value)).toBe('ready'); + }), + ); + for (const worker of workers) worker.stdin.end(); + const results = await Promise.all( + workers.map(async (worker) => { + const reader = worker.stdout.getReader(); + let output = ''; + while (true) { + const { value, done } = await reader.read(); + if (done) break; + output += new TextDecoder().decode(value); + } + return { output, stderr: await new Response(worker.stderr).text(), code: await worker.exited }; + }), + ); + for (const result of results) { + expect(result.code).toBe(0); + expect(result.stderr).toBe(''); + } + expect(JSON.parse(results[0].output).invalid).toBe(0); + expect(JSON.parse(results[1].output).removed).toBeGreaterThan(0); + } finally { + for (const worker of workers) worker.kill(); + await Promise.all(workers.map((worker) => worker.exited)); + } + }, 30_000); +}); + // --------------------------------------------------------------------------- // Multi-PROCESS roster write vs task-create race: concurrent bun processes hire // agents and create tasks against the same on-disk WAL database. Every writer diff --git a/src/lib/v5/task-state.ts b/src/lib/v5/task-state.ts index 3eff088b1..eedda62ba 100644 --- a/src/lib/v5/task-state.ts +++ b/src/lib/v5/task-state.ts @@ -1712,22 +1712,26 @@ function mapHire(row: RawHire): HireRosterRow { * `(wish, agent_adapter_id)`: a re-hire refreshes profile/worktree/state but * preserves the original `hired_at` by OMITTING `hired_at` from the `ON CONFLICT * DO UPDATE SET` list — an unset column keeps its stored value, so the first - * hire's timestamp survives every re-hire and the call converges on one row. A - * single statement is atomic on its own; the WAL + busy_timeout the handle - * carries (see sqlite-open.ts) serializes it against concurrent writers. + * hire's timestamp survives every re-hire and the call converges on one row. + * RETURNING captures the result inside that same write statement, so a racing + * unhire cannot remove the row between the upsert and a separate result read. + * WAL + busy_timeout (see sqlite-open.ts) serializes concurrent writers. */ export function hireAgent(db: Database, input: HireAgentInput): HireRosterRow { const now = Date.now(); const state = input.state ?? 'hired'; - db.query( - `INSERT INTO hire_roster (wish, agent_adapter_id, profile, worktree, hired_at, state) + const row = db + .query( + `INSERT INTO hire_roster (wish, agent_adapter_id, profile, worktree, hired_at, state) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(wish, agent_adapter_id) DO UPDATE SET profile = excluded.profile, worktree = excluded.worktree, - state = excluded.state`, - ).run(input.wish, input.agentAdapterId, input.profile ?? null, input.worktree, now, state); - return getHire(db, input.wish, input.agentAdapterId) as HireRosterRow; + state = excluded.state + RETURNING *`, + ) + .get(input.wish, input.agentAdapterId, input.profile ?? null, input.worktree, now, state) as RawHire; + return mapHire(row); } /** From a1574d13ea34a65f6a29c02101c0c2ea26918e2d Mon Sep 17 00:00:00 2001 From: Felipe Rosa <felipe@namastex.ai> Date: Mon, 7 Sep 2026 20:54:09 +0000 Subject: [PATCH 17/24] test: set explicit modes in delivery fixtures Port the surviving fixture creation modes from Liraz Siri's PR #2737, commit 9711b1597d6a9b7a4605925579fcd4d2a18d5cfe. Keep retired agent-sync and obsolete plugin payload hunks absent. Process umask still applies. Independent review SHIP; surviving suites pass under default and 027 umasks with 12 tests and 73 assertions each. B2 selection determinism is already fixed on main by 01598ec8d3b259f99f77ad83a0f71b92c0c8eb78. Co-authored-by: Felipe Rosa <felipe@namastex.ai> --- .../__tests__/update-command-publication.test.ts | 14 +++++++------- src/genie-commands/local-delivery-repair.test.ts | 2 +- tests/support/update-current-boundary-runner.ts | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/genie-commands/__tests__/update-command-publication.test.ts b/src/genie-commands/__tests__/update-command-publication.test.ts index 4f4eb3be7..cf68d2573 100644 --- a/src/genie-commands/__tests__/update-command-publication.test.ts +++ b/src/genie-commands/__tests__/update-command-publication.test.ts @@ -46,13 +46,13 @@ function buildReleasePayload( } { const payload = join(root, 'payload'); for (const directory of ['.agents', '.claude-plugin', 'plugins/genie', 'skills/review', 'templates']) { - mkdirSync(join(payload, directory), { recursive: true }); + mkdirSync(join(payload, directory), { recursive: true, mode: 0o755 }); } - writeFileSync(join(payload, 'LICENSE'), 'test fixture\n'); - writeFileSync(join(payload, 'VERSION'), `${version}\n`); - writeFileSync(join(payload, 'plugins', 'genie', 'plugin.txt'), 'authenticated plugin payload\n'); - writeFileSync(join(payload, 'skills', 'review', 'SKILL.md'), '# Review\n'); - writeFileSync(join(payload, 'templates', 'template.txt'), 'template\n'); + writeFileSync(join(payload, 'LICENSE'), 'test fixture\n', { mode: 0o644 }); + writeFileSync(join(payload, 'VERSION'), `${version}\n`, { mode: 0o644 }); + writeFileSync(join(payload, 'plugins', 'genie', 'plugin.txt'), 'authenticated plugin payload\n', { mode: 0o644 }); + writeFileSync(join(payload, 'skills', 'review', 'SKILL.md'), '# Review\n', { mode: 0o644 }); + writeFileSync(join(payload, 'templates', 'template.txt'), 'template\n', { mode: 0o644 }); writeExecutable( join(payload, 'genie'), `#!/bin/sh\nif [ "\${1:-}" = "--version" ]; then printf 'genie ${version}\\n'; exit 0; fi\nexit 0\n`, @@ -75,7 +75,7 @@ describe('updateCommand publication boundary', () => { const bin = join(genieHome, 'bin'); const fakeBin = join(root, 'fake-bin'); const fixture = join(root, 'fixture'); - mkdirSync(bin, { recursive: true }); + mkdirSync(bin, { recursive: true, mode: 0o755 }); mkdirSync(fakeBin); mkdirSync(fixture); diff --git a/src/genie-commands/local-delivery-repair.test.ts b/src/genie-commands/local-delivery-repair.test.ts index 05c3da7cc..0a9dace72 100644 --- a/src/genie-commands/local-delivery-repair.test.ts +++ b/src/genie-commands/local-delivery-repair.test.ts @@ -112,7 +112,7 @@ function isolatedEnv(root: string, overrides: Record<string, string> = {}): Reco const genieHome = join(root, 'genie-home'); const codexHome = join(root, 'codex-home'); const temp = join(root, 'tmp'); - for (const path of [home, genieHome, codexHome, temp]) mkdirSync(path, { recursive: true }); + for (const path of [home, genieHome, codexHome, temp]) mkdirSync(path, { recursive: true, mode: 0o700 }); return { ...env, HOME: home, diff --git a/tests/support/update-current-boundary-runner.ts b/tests/support/update-current-boundary-runner.ts index f62cfa8a0..4d6a41207 100644 --- a/tests/support/update-current-boundary-runner.ts +++ b/tests/support/update-current-boundary-runner.ts @@ -12,7 +12,7 @@ if (genieHome === undefined || scenario !== 'already-current') { const bin = join(genieHome, 'bin'); for (const directory of ['.agents', '.claude-plugin', 'plugins/genie', 'skills/review', 'templates']) { - mkdirSync(join(bin, directory), { recursive: true }); + mkdirSync(join(bin, directory), { recursive: true, mode: 0o755 }); } writeFileSync(join(bin, 'LICENSE'), 'fixture\n'); writeFileSync(join(bin, 'VERSION'), `${VERSION}\n`); @@ -22,8 +22,8 @@ writeFileSync(join(bin, 'templates', 'fixture.txt'), 'fixture\n'); const executable = join(bin, 'genie'); writeFileSync(executable, `#!/bin/sh\nif [ "\${1:-}" = "--version" ]; then printf 'genie ${VERSION}\\n'; fi\nexit 0\n`); chmodSync(executable, 0o755); -mkdirSync(process.env.HOME as string, { recursive: true }); -mkdirSync(process.env.CODEX_HOME as string, { recursive: true }); +mkdirSync(process.env.HOME as string, { recursive: true, mode: 0o755 }); +mkdirSync(process.env.CODEX_HOME as string, { recursive: true, mode: 0o755 }); const marker = join(genieHome, '.install-version'); writeFileSync(marker, 'prior-marker\n'); From a79ec048be2a19512283ab85bfa16aa9741f6864 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:16:15 +0000 Subject: [PATCH 18/24] chore(version): bump to 5.260907.1 [auto-version] --- package.json | 2 +- plugins/genie/orca-plugin.json | 2 +- plugins/genie/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d093ab15b..441650913 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@automagik/genie", - "version": "5.260901.3", + "version": "5.260907.1", "description": "Collaborative terminal toolkit for human + AI workflows. NOTE: npm distribution discontinued 2026-05-09 \u2014 install via `curl -fsSL https://raw.githubusercontent.com/automagik-dev/genie/main/install.sh | bash` (cosign + SLSA verified). See https://automagik.dev/genie/release-process", "license": "MIT", "type": "module", diff --git a/plugins/genie/orca-plugin.json b/plugins/genie/orca-plugin.json index 4742e809e..2306aa05c 100644 --- a/plugins/genie/orca-plugin.json +++ b/plugins/genie/orca-plugin.json @@ -3,7 +3,7 @@ "id": "genie", "publisher": "automagik", "name": "Genie", - "version": "5.260901.3", + "version": "5.260907.1", "description": "Genie workflows backed by Orca as the sole lifecycle authority.", "author": { "name": "Namastex Labs", diff --git a/plugins/genie/package.json b/plugins/genie/package.json index c8fa476ac..54e93127d 100644 --- a/plugins/genie/package.json +++ b/plugins/genie/package.json @@ -1,6 +1,6 @@ { "name": "genie-plugin", - "version": "5.260901.3", + "version": "5.260907.1", "private": true, "description": "Runtime dependencies for genie bundled CLIs", "license": "MIT", From 5f97e6629a4bc54bef68b389e0aa1c5d4af55de1 Mon Sep 17 00:00:00 2001 From: Felipe Rosa <felipe@namastex.ai> Date: Mon, 7 Sep 2026 21:33:54 +0000 Subject: [PATCH 19/24] fix(dsh): reject Unicode controls in board input Reject Unicode control, format and line/paragraph separators before trimming or forwarding titles, comments and block reasons to the CLI. Preserve normal Unicode text and UTF-8 limits, and remove the redundant post-trim check. Independent review SHIP; 28 plugin tests pass with 461 assertions, including 81 action/position rejection vectors that invoke no CLI command. The final aggregate gate follows the remaining validated promotion-review corrections. Co-authored-by: Felipe Rosa <felipe@namastex.ai> --- plugins/dsh-genie-board/src/board.test.ts | 43 +++++++++++++++++++++++ plugins/dsh-genie-board/src/schema.ts | 14 ++------ 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/plugins/dsh-genie-board/src/board.test.ts b/plugins/dsh-genie-board/src/board.test.ts index dae788dbb..eed6f4dc2 100644 --- a/plugins/dsh-genie-board/src/board.test.ts +++ b/plugins/dsh-genie-board/src/board.test.ts @@ -112,6 +112,49 @@ describe('closed inputs and fixed argv', () => { for (const text of ['', ' ', 'x\0x', 'x\nx', 'x\tx', '\ntitle', 'title\n']) expect(requestSchema.safeParse({ ...input, text }).success).toBe(false); }); + test.each([ + ['C1 next line', '\u0085'], + ['C1 control sequence introducer', '\u009b'], + ['bidi override', '\u202e'], + ['bidi isolate', '\u2066'], + ['zero width space', '\u200b'], + ['byte order mark', '\ufeff'], + ['line separator', '\u2028'], + ['paragraph separator', '\u2029'], + ['supplementary format control', '\u{e0001}'], + ])('rejects %s at every text boundary before any CLI call', async (_name, control) => { + const f = await fixture(); + await f.list(); + await f.load(); + const before = f.calls.length; + for (const value of [`${control}text`, `te${control}xt`, `text${control}`]) { + for (const input of [ + { action: 'create', ...selection, title: value }, + { action: 'comment', ...selection, id: 't_abc', text: value }, + { action: 'block', ...selection, id: 't_abc', text: value }, + ]) { + await expect(f.service.request(input)).rejects.toThrow('Control characters are not allowed'); + expect(f.calls.length).toBe(before); + } + } + }); + test('ordinary Unicode text is trimmed and forwarded unchanged for all three actions', async () => { + const f = await fixture(); + await f.list(); + await f.load(); + const value = 'café 漢字 🙂 e\u0301'; + const padded = ` ${value} `; + const vectors = [ + [{ action: 'create', title: padded }, ['task', 'create', '--title', value, '--board', 'b_abc']], + [{ action: 'comment', id: 't_abc', text: padded }, ['task', 'comment', '--', 't_abc', value]], + [{ action: 'block', id: 't_abc', text: padded }, ['task', 'block', 't_abc', '--reason', value]], + ] as const; + for (const [input, argv] of vectors) { + const before = f.calls.length; + await f.service.request({ ...selection, ...input }); + expect(f.calls.slice(before)).toEqual([[...argv], ['board', '--board', 'b_abc', '--json']]); + } + }); test('byte bounds for title/comment/reason', () => { for (const [action, field, limit] of [ ['create', 'title', 200], diff --git a/plugins/dsh-genie-board/src/schema.ts b/plugins/dsh-genie-board/src/schema.ts index cc68c5ea5..efec3f0dd 100644 --- a/plugins/dsh-genie-board/src/schema.ts +++ b/plugins/dsh-genie-board/src/schema.ts @@ -62,19 +62,9 @@ export const boardsSchema = z.array( ); const bounded = (max: number) => text - .refine( - (value) => - !Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127), - 'Control characters are not allowed', - ) + .refine((value) => !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(value), 'Control characters are not allowed') .transform((value) => value.trim()) - .refine( - (value) => - value.length > 0 && - Buffer.byteLength(value) <= max && - !Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127), - 'Invalid text', - ); + .refine((value) => value.length > 0 && Buffer.byteLength(value) <= max, 'Invalid text'); const base = { workspaceId: text.min(1).max(200), boardRef: text.regex(/^b_[a-z0-9]+$/) }; export const requestSchema = z.discriminatedUnion('action', [ z.object({ action: z.literal('list'), workspaceId: base.workspaceId }).strict(), From eeafaeace761018d416d9a7539bd8af1cf25b7d3 Mon Sep 17 00:00:00 2001 From: Felipe Rosa <felipe@namastex.ai> Date: Mon, 7 Sep 2026 21:45:12 +0000 Subject: [PATCH 20/24] fix: close promotion review gaps Make tied board ordering explicit and bound smoke subprocesses. Require independent fix reviews and strengthen build and Host contract coverage. Co-authored-by: Felipe Rosa <felipe@namastex.ai> --- plugins/dsh-genie-board/build.test.ts | 94 ++++++++++++++++------- plugins/dsh-genie-board/src/board.test.ts | 6 +- scripts/dsh-genie-board-smoke.ts | 9 ++- skills/genie-orca-review/SKILL.md | 2 +- src/lib/v5/task-state.ts | 2 +- src/term-commands/v5-board.test.ts | 3 + 6 files changed, 83 insertions(+), 33 deletions(-) diff --git a/plugins/dsh-genie-board/build.test.ts b/plugins/dsh-genie-board/build.test.ts index a42608152..70f9675a2 100644 --- a/plugins/dsh-genie-board/build.test.ts +++ b/plugins/dsh-genie-board/build.test.ts @@ -1,41 +1,77 @@ import { expect, test } from 'bun:test'; -import { readFile, rm, stat } from 'node:fs/promises'; +import { mkdtemp, readFile, readdir, rm, stat } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { runInNewContext } from 'node:vm'; -test('build regenerates Host bundle and lazy DSH browser factory', async () => { +test('valid repeated builds regenerate identical Host and lazy browser bundles', async () => { const root = import.meta.dir; - const host = join(root, 'dist/index.js'); - const client = join(root, 'dist/client.js'); - await rm(host, { force: true }); - await rm(client, { force: true }); - const process = Bun.spawn(['bun', 'run', 'build'], { cwd: root, stdout: 'pipe', stderr: 'pipe' }); - const [code, error] = await Promise.all([process.exited, new Response(process.stderr).text()]); - expect(error).not.toContain('error:'); - expect(code).toBe(0); - expect((await stat(host)).size).toBeGreaterThan(1000); - let registration: { id: string; factory: (require: unknown) => { apply: unknown } } | undefined; - runInNewContext(await readFile(client, 'utf8'), { - window: { - __ModuleLoader__: { - load(value: typeof registration) { - registration = value; + const output = await mkdtemp(join(tmpdir(), 'genie-repeat-host-')); + const version = JSON.parse(await readFile(join(root, '../../package.json'), 'utf8')).version; + let previous: string[] | undefined; + try { + for (let attempt = 0; attempt < 2; attempt++) { + const proc = Bun.spawn(['bun', 'run', 'build', version, output], { + cwd: root, + stdout: 'pipe', + stderr: 'pipe', + }); + const [code, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]); + expect(code).toBe(0); + expect(stderr).not.toContain('error:'); + const host = join(output, 'index.js'); + const client = join(output, 'client.js'); + expect((await stat(host)).size).toBeGreaterThan(1000); + expect((await import(`${host}?attempt=${attempt}`)).minimumGenieVersion).toBe(version); + const bytes = [await readFile(host, 'utf8'), await readFile(client, 'utf8')]; + if (previous) expect(bytes).toEqual(previous); + previous = bytes; + let registration: { id: string; factory: (require: unknown) => { apply: unknown } } | undefined; + runInNewContext(bytes[1], { + window: { + __ModuleLoader__: { + load(value: typeof registration) { + registration = value; + }, + }, }, - }, - }, - }); - expect(registration?.id).toBe('@automagik/genie-dsh-board'); - // No DOM is needed until Cordis activates the factory's apply method. - expect( - typeof registration?.factory(() => { - throw new Error('Unexpected browser dependency'); - }).apply, - ).toBe('function'); + }); + expect(registration?.id).toBe('@automagik/genie-dsh-board'); + // No DOM is needed until Cordis activates the factory's apply method. + expect( + typeof registration?.factory(() => { + throw new Error('Unexpected browser dependency'); + }).apply, + ).toBe('function'); + } + } finally { + await rm(output, { recursive: true, force: true }); + } +}, 20_000); + +test('invalid build version exits with an error and writes no bundle', async () => { + const output = await mkdtemp(join(tmpdir(), 'genie-invalid-host-')); + try { + const proc = Bun.spawn(['bun', 'run', 'build', 'invalid version', output], { + cwd: import.meta.dir, + stdout: 'pipe', + stderr: 'pipe', + }); + const [code, stdout, stderr] = await Promise.all([ + proc.exited, + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]); + expect(code).toBe(1); + expect(stdout).toBe(''); + expect(stderr).toContain('invalid build version'); + expect(await readdir(output)).toEqual([]); + } finally { + await rm(output, { recursive: true, force: true }); + } }, 20_000); test('candidate build embeds override floor without changing source metadata', async () => { - const { mkdtemp, readFile, rm } = await import('node:fs/promises'); - const { tmpdir } = await import('node:os'); const root = import.meta.dir; const output = await mkdtemp(join(tmpdir(), 'genie-candidate-host-')); const before = await readFile(join(root, 'package.json'), 'utf8'); diff --git a/plugins/dsh-genie-board/src/board.test.ts b/plugins/dsh-genie-board/src/board.test.ts index eed6f4dc2..3528d74dd 100644 --- a/plugins/dsh-genie-board/src/board.test.ts +++ b/plugins/dsh-genie-board/src/board.test.ts @@ -266,8 +266,12 @@ test('semver strict ordering includes prereleases and rejects malformed versions expect(compatible(actual, minimum)).toBe(result); }); test('process environment is an exact allowlist', () => { + const env = hostEnvironment('host'); + expect(env.GENIE_AGENT_NAME).toBe('host'); + expect(env.GENIE_AGENT_KIND).toBe('dsh'); + expect(env.NO_COLOR).toBe('1'); expect( - Object.keys(hostEnvironment('host')).every((key) => + Object.keys(env).every((key) => ['PATH', 'HOME', 'GENIE_HOME', 'NO_COLOR', 'GENIE_AGENT_NAME', 'GENIE_AGENT_KIND'].includes(key), ), ).toBe(true); diff --git a/scripts/dsh-genie-board-smoke.ts b/scripts/dsh-genie-board-smoke.ts index c21af9cfb..2ee782911 100644 --- a/scripts/dsh-genie-board-smoke.ts +++ b/scripts/dsh-genie-board-smoke.ts @@ -18,7 +18,14 @@ const env = { let server: ChildProcess | undefined; let installed = false; async function command(binary: string, args: string[], cwd = root): Promise<string> { - const proc = Bun.spawn([binary, ...args], { cwd, env, stdout: 'pipe', stderr: 'pipe' }); + const proc = Bun.spawn([binary, ...args], { + cwd, + env, + stdout: 'pipe', + stderr: 'pipe', + timeout: 120_000, + killSignal: 'SIGKILL', + }); const [stdout, stderr, code] = await Promise.all([ new Response(proc.stdout).text(), new Response(proc.stderr).text(), diff --git a/skills/genie-orca-review/SKILL.md b/skills/genie-orca-review/SKILL.md index 6548f5a3c..8b077440d 100644 --- a/skills/genie-orca-review/SKILL.md +++ b/skills/genie-orca-review/SKILL.md @@ -31,7 +31,7 @@ A reviewer is a **read-only worker dispatched by the coordinator**, never the en Resolve the fix-loop budget `B` once per group: default 2; only an explicit higher-priority user/workspace instruction may set another positive integer. Carry `B` and the attempts already used across handoffs; do not reset the budget when switching skills. Overrides never expand scope, permit unchanged retries, or skip diagnosis or independent re-review. -Coordinator re-dispatches a **fast** worker into the same worktree with the findings quoted verbatim and "apply exactly this, nothing else". Cap `B` loops per group; the coordinator may verify a trivial delta itself instead of a second review. After the cap → human gate. +Coordinator re-dispatches a **fast** worker into the same worktree with the findings quoted verbatim and "apply exactly this, nothing else". Cap `B` loops per group. After every fix, dispatch an independent re-reviewer who is not the fixer. If the re-review is still not SHIP after `B` attempts, stop fixing and follow [Escalation Diagnosis](../fix/SKILL.md#escalation-diagnosis), then its cause-specific route or human gate as applicable. Diagnosis does not reset the budget or grant additional fix attempts. ## What the integrated gate catches that group review does not diff --git a/src/lib/v5/task-state.ts b/src/lib/v5/task-state.ts index eedda62ba..2a68447d5 100644 --- a/src/lib/v5/task-state.ts +++ b/src/lib/v5/task-state.ts @@ -1467,7 +1467,7 @@ export function readBoardTaskSnapshot( const read = db.transaction((): BoardTaskAggregate[] => { const params = filter.wish ? [boardId, filter.wish] : [boardId]; const tasks = db - .query(`SELECT * FROM tasks WHERE board_id = ?${filter.wish ? ' AND wish = ?' : ''} ORDER BY created_at`) + .query(`SELECT * FROM tasks WHERE board_id = ?${filter.wish ? ' AND wish = ?' : ''} ORDER BY created_at, rowid`) .all(...params) as RawTask[]; if (tasks.length === 0) return []; // One JSON binding avoids variable limits while retaining task_id index probes. diff --git a/src/term-commands/v5-board.test.ts b/src/term-commands/v5-board.test.ts index 5c2d15a29..a110ca6e1 100644 --- a/src/term-commands/v5-board.test.ts +++ b/src/term-commands/v5-board.test.ts @@ -514,6 +514,9 @@ describe('scoped board JSON aggregate v1', () => { db.query('UPDATE tasks SET id = ? WHERE id = ?').run('a-dependency', depA.id); db.query('UPDATE tasks SET id = ?, created_at = 10 WHERE id = ?').run('z-card', cardB.id); db.query('UPDATE tasks SET id = ?, created_at = 10 WHERE id = ?').run('a-card', cardA.id); + // This access path sorts equal timestamps by id unless the aggregate + // explicitly requests its insertion-order tie-break. + db.run('CREATE INDEX test_board_created_id ON tasks(board_id, created_at, id)'); addDependency(db, 'a-card', 'z-dependency'); addDependency(db, 'a-card', 'a-dependency'); // Insert same-time ids in descending order. Timeline and its comment From e5cac713e62df68bf25981b7e79493c6b3476901 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:56:47 +0000 Subject: [PATCH 21/24] chore(version): bump to 5.260907.2 [auto-version] --- package.json | 2 +- plugins/genie/orca-plugin.json | 2 +- plugins/genie/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 441650913..5236bebe3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@automagik/genie", - "version": "5.260907.1", + "version": "5.260907.2", "description": "Collaborative terminal toolkit for human + AI workflows. NOTE: npm distribution discontinued 2026-05-09 \u2014 install via `curl -fsSL https://raw.githubusercontent.com/automagik-dev/genie/main/install.sh | bash` (cosign + SLSA verified). See https://automagik.dev/genie/release-process", "license": "MIT", "type": "module", diff --git a/plugins/genie/orca-plugin.json b/plugins/genie/orca-plugin.json index 2306aa05c..71f939a4c 100644 --- a/plugins/genie/orca-plugin.json +++ b/plugins/genie/orca-plugin.json @@ -3,7 +3,7 @@ "id": "genie", "publisher": "automagik", "name": "Genie", - "version": "5.260907.1", + "version": "5.260907.2", "description": "Genie workflows backed by Orca as the sole lifecycle authority.", "author": { "name": "Namastex Labs", diff --git a/plugins/genie/package.json b/plugins/genie/package.json index 54e93127d..379cd851c 100644 --- a/plugins/genie/package.json +++ b/plugins/genie/package.json @@ -1,6 +1,6 @@ { "name": "genie-plugin", - "version": "5.260907.1", + "version": "5.260907.2", "private": true, "description": "Runtime dependencies for genie bundled CLIs", "license": "MIT", From 1bb35f98c2b879208731047a6b4baa0e1aa58810 Mon Sep 17 00:00:00 2001 From: Felipe Rosa <felipe@namastex.ai> Date: Tue, 8 Sep 2026 19:43:59 +0000 Subject: [PATCH 22/24] chore(roadmap): publish the local board snapshot The committed snapshot had been stale since 2026-08-13. The 2026-08-29 export only reached the unmerged recovery/orca-drain/genie-main-20260830 branch, and the 2026-08-30 Orca-drain reset reverted the working file while the gitignored sync baseline kept that export's hash, so every `task sync` since read as diverged and the hooks (fail-open) never re-exported. This publishes the real board from this host's genie.db, 84 cards, untouched, and re-stamps the baseline. A follow-up commit prunes stale planning cards, so this revision is the full pre-prune record. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012RU32tvczV84Ys9kHVtEQD --- .genie/roadmap.json | 2368 ++++++++++++++++++++++++++++++++----------- 1 file changed, 1791 insertions(+), 577 deletions(-) diff --git a/.genie/roadmap.json b/.genie/roadmap.json index 315b13c84..d93ce1950 100644 --- a/.genie/roadmap.json +++ b/.genie/roadmap.json @@ -4,6 +4,18 @@ { "key": "stage_log_backfill_v1", "value": "1784758341375" + }, + { + "key": "wish_base:skills-everywhere", + "value": "{\"branch\":\"wish/skills-everywhere\",\"base\":\"b52dfff2b3c5d5d06fd2b0a08b5dfb4cbd6453a9\",\"recordedAt\":1788109092518}" + }, + { + "key": "wish_base:skills-everywhere-b", + "value": "{\"branch\":\"wish/skills-everywhere-b\",\"base\":\"6b00a9807b14bb8cf1d6402ffe4d3e24c18849ba\",\"recordedAt\":1788200054235}" + }, + { + "key": "wish_base:skills-everywhere-c", + "value": "{\"branch\":\"wish/skills-everywhere-c\",\"base\":\"6b00a9807b14bb8cf1d6402ffe4d3e24c18849ba\",\"recordedAt\":1788229756107}" } ], "boards": [], @@ -17,6 +29,8 @@ "claimed_at": 1784623891565, "wish": "genie-ui", "group_name": "group-1", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784618699261, "updated_at": 1784625442704, "lane": null, @@ -24,9 +38,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mrubw9bxa7f28dd8", @@ -37,6 +49,8 @@ "claimed_at": 1784626730518, "wish": "genie-ui", "group_name": "group-2", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784618699325, "updated_at": 1784627382271, "lane": null, @@ -44,9 +58,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mrubw9do9fc563c4", @@ -57,6 +69,8 @@ "claimed_at": 1784629252019, "wish": "genie-ui", "group_name": "group-3", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784618699388, "updated_at": 1784629987524, "lane": null, @@ -64,9 +78,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mrubw9ft4d20d980", @@ -77,6 +89,8 @@ "claimed_at": 1784630070055, "wish": "genie-ui", "group_name": "group-4", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784618699465, "updated_at": 1784630608759, "lane": null, @@ -84,9 +98,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mruxc5gvd0c00325", @@ -97,6 +109,8 @@ "claimed_at": 1784660541667, "wish": "genie-ui-dash", "group_name": "group-1", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784654712751, "updated_at": 1784661266675, "lane": null, @@ -104,9 +118,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mruxc5iv51303ef4", @@ -117,6 +129,8 @@ "claimed_at": 1784661307439, "wish": "genie-ui-dash", "group_name": "group-2", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784654712823, "updated_at": 1784664027865, "lane": null, @@ -124,9 +138,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mruxc5l659685893", @@ -137,6 +149,8 @@ "claimed_at": 1784664068582, "wish": "genie-ui-dash", "group_name": "group-3", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784654712906, "updated_at": 1784666301881, "lane": null, @@ -144,9 +158,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mruxc5mz0a9ee3f5", @@ -157,6 +169,8 @@ "claimed_at": 1784666347308, "wish": "genie-ui-dash", "group_name": "group-4", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784654712971, "updated_at": 1784669563300, "lane": null, @@ -164,9 +178,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mruxc5ora3add7b8", @@ -177,6 +189,8 @@ "claimed_at": 1784669605355, "wish": "genie-ui-dash", "group_name": "group-5", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784654713035, "updated_at": 1784672089341, "lane": null, @@ -184,9 +198,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mruzpyww3e2a8b8b", @@ -197,6 +209,8 @@ "claimed_at": null, "wish": null, "group_name": null, + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784658716672, "updated_at": 1784658716672, "lane": null, @@ -204,9 +218,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mrv087jca37acdd9", @@ -217,6 +229,8 @@ "claimed_at": 1784660523509, "wish": "genie-ui-bridge", "group_name": "group-1", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784659567656, "updated_at": 1784661184531, "lane": null, @@ -224,9 +238,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mrv087ldf5bdb175", @@ -237,6 +249,8 @@ "claimed_at": 1784661223928, "wish": "genie-ui-bridge", "group_name": "group-2", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784659567729, "updated_at": 1784663117984, "lane": null, @@ -244,9 +258,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mrveq6oe95cb9141", @@ -257,6 +269,8 @@ "claimed_at": null, "wish": null, "group_name": null, + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784683920978, "updated_at": 1784683920978, "lane": null, @@ -264,9 +278,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mrvf2p7za43e792d", @@ -277,6 +289,8 @@ "claimed_at": 1784694196822, "wish": "live-dev-loop", "group_name": "group-1", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784684504879, "updated_at": 1784695589899, "lane": null, @@ -284,9 +298,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mrvf2pam274f1741", @@ -297,6 +309,8 @@ "claimed_at": 1784697640859, "wish": "live-dev-loop", "group_name": "group-2", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784684504974, "updated_at": 1784698734359, "lane": null, @@ -304,9 +318,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mrvf2pd39e9349c2", @@ -317,6 +329,8 @@ "claimed_at": 1784695629392, "wish": "live-dev-loop", "group_name": "group-3", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784684505063, "updated_at": 1784697606035, "lane": null, @@ -324,129 +338,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null - }, - { - "id": "t_mrwwknwl195358a3", - "board_id": null, - "title": "Core identity — package.json, app name, publish target", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "khal-rebrand", - "group_name": "group-1", - "created_at": 1784774362629, - "updated_at": 1784774362629, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null - }, - { - "id": "t_mrwwknz76a0d4a64", - "board_id": null, - "title": "Release feeds — update check, release notes, toast", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "khal-rebrand", - "group_name": "group-2", - "created_at": 1784774362723, - "updated_at": 1784774362723, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null - }, - { - "id": "t_mrwwko1x1cae87af", - "board_id": null, - "title": "Per-project dir — .dash → .khal via one constant", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "khal-rebrand", - "group_name": "group-3", - "created_at": 1784774362821, - "updated_at": 1784774362821, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null - }, - { - "id": "t_mrwwko4i254384c6", - "board_id": null, - "title": "User-facing Dash string sweep", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "khal-rebrand", - "group_name": "group-4", - "created_at": 1784774362914, - "updated_at": 1784774362914, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null - }, - { - "id": "t_mrwwko6w4d889a11", - "board_id": null, - "title": "Docs, LICENSE, upstream detachment", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "khal-rebrand", - "group_name": "group-5", - "created_at": 1784774363000, - "updated_at": 1784774363000, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null - }, - { - "id": "t_mrwwko9jc20398ca", - "board_id": null, - "title": "Icon and visual assets (blocked-on-asset)", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "khal-rebrand", - "group_name": "group-6", - "created_at": 1784774363096, - "updated_at": 1784774363096, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mrxqzo6s8fbb27ec", @@ -457,6 +349,8 @@ "claimed_at": null, "wish": null, "group_name": null, + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784825451316, "updated_at": 1784827974072, "lane": null, @@ -464,9 +358,7 @@ "heartbeat_at": null, "blocked_by": "claude-code", "blocked_reason": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mrxr9wzrda10d35b", @@ -477,6 +369,8 @@ "claimed_at": null, "wish": "khal-native-theme", "group_name": "group-1", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784825929289, "updated_at": 1784827974160, "lane": null, @@ -484,9 +378,7 @@ "heartbeat_at": null, "blocked_by": "claude-code", "blocked_reason": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mrxr9x1pbce3a16b", @@ -497,6 +389,8 @@ "claimed_at": null, "wish": "khal-native-theme", "group_name": "group-2", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784825929357, "updated_at": 1784827974238, "lane": null, @@ -504,9 +398,7 @@ "heartbeat_at": null, "blocked_by": "claude-code", "blocked_reason": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mrxr9x49eb19d0ad", @@ -517,6 +409,8 @@ "claimed_at": null, "wish": "khal-native-theme", "group_name": "group-3", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784825929449, "updated_at": 1784827974322, "lane": null, @@ -524,9 +418,7 @@ "heartbeat_at": null, "blocked_by": "claude-code", "blocked_reason": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mrxr9x6sb3dfb668", @@ -537,6 +429,8 @@ "claimed_at": null, "wish": "khal-native-theme", "group_name": "group-4", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784825929540, "updated_at": 1784827974396, "lane": null, @@ -544,9 +438,7 @@ "heartbeat_at": null, "blocked_by": "claude-code", "blocked_reason": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mrxr9x8t711ce43e", @@ -557,6 +449,8 @@ "claimed_at": null, "wish": "khal-native-theme", "group_name": "group-5", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784825929613, "updated_at": 1784827974462, "lane": null, @@ -564,9 +458,7 @@ "heartbeat_at": null, "blocked_by": "claude-code", "blocked_reason": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mrxr9xbb010c4f81", @@ -577,6 +469,8 @@ "claimed_at": null, "wish": "khal-native-theme", "group_name": "group-6", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1784825929703, "updated_at": 1784827974556, "lane": null, @@ -584,9 +478,7 @@ "heartbeat_at": null, "blocked_by": "claude-code", "blocked_reason": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_ms58w0yqa195fbf8", @@ -597,6 +489,8 @@ "claimed_at": null, "wish": "genie-official-roadmap", "group_name": null, + "assigned_agent": null, + "assigned_reason": null, "created_at": 1785278777572, "updated_at": 1785278777572, "lane": null, @@ -604,9 +498,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_ms58y58z0b45759f", @@ -617,6 +509,8 @@ "claimed_at": null, "wish": "genie-official-roadmap", "group_name": "group-1", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1785278876435, "updated_at": 1785278876435, "lane": null, @@ -624,9 +518,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_ms58y5bc8853df95", @@ -637,6 +529,8 @@ "claimed_at": null, "wish": "genie-official-roadmap", "group_name": "group-2", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1785278876520, "updated_at": 1785278876520, "lane": null, @@ -644,9 +538,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_ms58y5e19f6e9757", @@ -657,6 +549,8 @@ "claimed_at": null, "wish": "genie-official-roadmap", "group_name": "group-3", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1785278876617, "updated_at": 1785278876617, "lane": null, @@ -664,9 +558,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mscebl889535b59e", @@ -677,6 +569,8 @@ "claimed_at": 1785712701024, "wish": "lifecycle-lease-busy-grace", "group_name": "group-1", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1785711164984, "updated_at": 1785714161658, "lane": null, @@ -684,9 +578,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_mscebla86e45ab77", @@ -697,6 +589,8 @@ "claimed_at": 1785714210396, "wish": "lifecycle-lease-busy-grace", "group_name": "group-2", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1785711165056, "updated_at": 1785716639547, "lane": null, @@ -704,9 +598,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msi18po2a63fe11f", @@ -717,6 +609,8 @@ "claimed_at": 1786056837590, "wish": "harness-audit-landing", "group_name": "ready-fixes", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786051992818, "updated_at": 1786129435207, "lane": null, @@ -724,9 +618,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msi18prp35883ce7", @@ -737,6 +629,8 @@ "claimed_at": 1786057120710, "wish": "harness-audit-landing", "group_name": "completetask-fence", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786051992949, "updated_at": 1786129435338, "lane": null, @@ -744,9 +638,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msi18pv9b29465c2", @@ -757,6 +649,8 @@ "claimed_at": 1786057120255, "wish": "harness-audit-landing", "group_name": "wishgroup-deletion", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786051993077, "updated_at": 1786129435459, "lane": null, @@ -764,9 +658,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msi2nv2gcf9c97af", @@ -777,6 +669,8 @@ "claimed_at": null, "wish": "roadmap-truth", "group_name": null, + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786054379272, "updated_at": 1786054379272, "lane": null, @@ -784,9 +678,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msiyp3ua4e038e63", @@ -797,6 +689,8 @@ "claimed_at": 1786118248009, "wish": "remotty-board-asks", "group_name": "block-model", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786108185010, "updated_at": 1786120597563, "lane": null, @@ -804,9 +698,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msiyp3x10655eab9", @@ -817,6 +709,8 @@ "claimed_at": 1786118261192, "wish": "remotty-board-asks", "group_name": "index-lane-links", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786108185109, "updated_at": 1786121037959, "lane": null, @@ -824,9 +718,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msiyp3zr6f65f0fc", @@ -837,6 +729,8 @@ "claimed_at": 1786121077747, "wish": "remotty-board-asks", "group_name": "task-wish-verb", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786108185207, "updated_at": 1786122063091, "lane": null, @@ -844,9 +738,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msiyp42ib277f60a", @@ -857,6 +749,8 @@ "claimed_at": 1786121655148, "wish": "remotty-board-asks", "group_name": "task-delete", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786108185306, "updated_at": 1786122699029, "lane": null, @@ -864,9 +758,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msnw1wva78ca40b7", @@ -877,6 +769,8 @@ "claimed_at": null, "wish": "cross-agent-delegate", "group_name": null, + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786406074534, "updated_at": 1786413384910, "lane": null, @@ -884,9 +778,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msnwyz4qff08ad29", @@ -897,6 +789,8 @@ "claimed_at": 1786411501612, "wish": "cross-agent-delegate", "group_name": "group-b", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786407617114, "updated_at": 1786412348073, "lane": null, @@ -904,9 +798,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msnwyz7136c34cb2", @@ -917,6 +809,8 @@ "claimed_at": 1786411499996, "wish": "cross-agent-delegate", "group_name": "group-c", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786407617197, "updated_at": 1786412796523, "lane": null, @@ -924,9 +818,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msnx6kz8c52d4762", @@ -937,6 +829,8 @@ "claimed_at": 1786408694769, "wish": "cross-agent-delegate", "group_name": "group-a1", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786407972020, "updated_at": 1786411453387, "lane": null, @@ -944,9 +838,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msnx6l1k7cddfcc0", @@ -957,6 +849,8 @@ "claimed_at": 1786411502191, "wish": "cross-agent-delegate", "group_name": "group-a2", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786407972104, "updated_at": 1786412198104, "lane": null, @@ -964,9 +858,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msoqsd8sdb8df48b", @@ -977,6 +869,8 @@ "claimed_at": 1786460670279, "wish": "mcp-write-tools", "group_name": "group-1", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786457697292, "updated_at": 1786558209313, "lane": null, @@ -984,9 +878,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msoqsdb5429d06e8", @@ -997,6 +889,8 @@ "claimed_at": 1786558267434, "wish": "mcp-write-tools", "group_name": "group-2", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786457697377, "updated_at": 1786568148732, "lane": null, @@ -1004,9 +898,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msoqsdde40b31ee9", @@ -1017,6 +909,8 @@ "claimed_at": 1786568193561, "wish": "mcp-write-tools", "group_name": "group-3", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786457697458, "updated_at": 1786628093499, "lane": null, @@ -1024,9 +918,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msp4fp6ce714c8be", @@ -1037,6 +929,8 @@ "claimed_at": null, "wish": "delegate-bridge", "group_name": null, + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786480620852, "updated_at": 1786657262650, "lane": null, @@ -1044,9 +938,7 @@ "heartbeat_at": null, "blocked_by": "dream-scc-reversals", "blocked_reason": "re-targeted by spawn-context-contract group reversals (2026-08-13): gating moves from headless-turn-open to herdr-swap (0.3) + engine-turn-verbs (remotty board card t_msqkkjt221792465); S-F turn transport re-bases from one-shot headless launches to live herdr sessions (agent start -> prompt --wait --until -> read -> report/release)", - "block_kind": "work", - "assigned_agent": null, - "assigned_reason": null + "block_kind": "work" }, { "id": "t_msp6a1qlaaa8acdc", @@ -1057,6 +949,8 @@ "claimed_at": 1786554116904, "wish": "delegate-bridge", "group_name": "group-a", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786483716429, "updated_at": 1786554590852, "lane": null, @@ -1064,9 +958,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msp6a1sz195f0ea9", @@ -1077,6 +969,8 @@ "claimed_at": null, "wish": "delegate-bridge", "group_name": "group-s", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786483716515, "updated_at": 1786483716515, "lane": null, @@ -1084,9 +978,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msp6a1vd3c103d09", @@ -1097,6 +989,8 @@ "claimed_at": null, "wish": "delegate-bridge", "group_name": "group-b", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786483716601, "updated_at": 1786483716601, "lane": null, @@ -1104,9 +998,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msp6a1xzca62d364", @@ -1117,6 +1009,8 @@ "claimed_at": null, "wish": "delegate-bridge", "group_name": "group-c", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786483716695, "updated_at": 1786483716695, "lane": null, @@ -1124,9 +1018,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msp6a20k0a1df8a8", @@ -1137,6 +1029,8 @@ "claimed_at": null, "wish": "delegate-bridge", "group_name": "group-d", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786483716788, "updated_at": 1786483716788, "lane": null, @@ -1144,9 +1038,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msp6a23b3c681369", @@ -1157,6 +1049,8 @@ "claimed_at": null, "wish": "delegate-bridge", "group_name": "group-e", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786483716887, "updated_at": 1786483716887, "lane": null, @@ -1164,9 +1058,7 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null + "block_kind": null }, { "id": "t_msp6a25sef5b7e40", @@ -1177,6 +1069,8 @@ "claimed_at": null, "wish": "delegate-bridge", "group_name": "group-f", + "assigned_agent": null, + "assigned_reason": null, "created_at": 1786483716976, "updated_at": 1786483716976, "lane": null, @@ -1184,499 +1078,1819 @@ "heartbeat_at": null, "blocked_by": null, "blocked_reason": null, - "block_kind": null, - "assigned_agent": null, - "assigned_reason": null - } - ], - "task_dependencies": [], - "stage_log": [], - "task_events": [ + "block_kind": null + }, { - "id": 1, - "task_id": "t_mrxqzo6s8fbb27ec", - "kind": "block", - "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "author_kind": "claude-code", - "author": null, - "created_at": 1784827974073 + "id": "t_mtdb642cdb6f9f10", + "board_id": null, + "title": "Genie cleanup release — standalone contained + native Orca plugin", + "status": "ready", + "claimed_by": null, + "claimed_at": null, + "wish": null, + "group_name": null, + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1787943119124, + "updated_at": 1787943119124, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null }, { - "id": 2, - "task_id": "t_mrxr9wzrda10d35b", - "kind": "block", - "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "author_kind": "claude-code", - "author": null, - "created_at": 1784827974160 + "id": "t_mtdns9r623748cfd", + "board_id": null, + "title": "Build bundled /quick and daily delivery cadence", + "status": "done", + "claimed_by": null, + "claimed_at": null, + "wish": null, + "group_name": null, + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1787964308322, + "updated_at": 1787965629293, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null }, { - "id": 3, - "task_id": "t_mrxr9x1pbce3a16b", - "kind": "block", - "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "author_kind": "claude-code", - "author": null, - "created_at": 1784827974238 + "id": "t_mtfzsu4ee1390c48", + "board_id": null, + "title": "codex-skill-installer: skills via skills.sh, retire Codex+Claude plugin delivery", + "status": "ready", + "claimed_by": null, + "claimed_at": null, + "wish": "skills-everywhere", + "group_name": null, + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788105422462, + "updated_at": 1788108622793, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null }, { - "id": 4, - "task_id": "t_mrxr9x49eb19d0ad", - "kind": "block", - "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", + "id": "t_mtg14v70f2fb54ba", + "board_id": null, + "title": "Wish A G1: skills installer + install/update/uninstall wiring", + "status": "done", + "claimed_by": "engineer-g1", + "claimed_at": 1788111374068, + "wish": "skills-everywhere", + "group_name": "group-1", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788107663340, + "updated_at": 1788113339912, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mtg14vc4e18dcc51", + "board_id": null, + "title": "Wish A G2: legacy integration retirement", + "status": "done", + "claimed_by": "engineer-g2", + "claimed_at": 1788113384174, + "wish": "skills-everywhere", + "group_name": "group-2", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788107663524, + "updated_at": 1788118209073, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mtg14vhpa6d08b44", + "board_id": null, + "title": "Wish A G3: doctor skills + retirement surface", + "status": "done", + "claimed_by": "engineer-g3", + "claimed_at": 1788113405613, + "wish": "skills-everywhere", + "group_name": "group-3", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788107663725, + "updated_at": 1788116944772, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mtg14vn5e0de4dc8", + "board_id": null, + "title": "Wish A G4: rehome lifecycle lease + atomic-fs primitives", + "status": "done", + "claimed_by": "engineer-g4", + "claimed_at": 1788110157612, + "wish": "skills-everywhere", + "group_name": "group-4", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788107663921, + "updated_at": 1788111941077, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mtg14vsi44350a36", + "board_id": null, + "title": "Wish A G5: release and CI smokes", + "status": "done", + "claimed_by": "engineer-g5", + "claimed_at": 1788113433057, + "wish": "skills-everywhere", + "group_name": "group-5", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788107664114, + "updated_at": 1788117468064, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mtg14vxr224f5565", + "board_id": null, + "title": "Wish A G6: supersession notes + install docs section", + "status": "done", + "claimed_by": "engineer-g6", + "claimed_at": 1788110175133, + "wish": "skills-everywhere", + "group_name": "group-6", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788107664303, + "updated_at": 1788113202418, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mtg14w35c3ea69d2", + "board_id": null, + "title": "Wish A G7: real-host dogfood (C3)", + "status": "done", + "claimed_by": null, + "claimed_at": null, + "wish": "skills-everywhere", + "group_name": "group-7", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788107664497, + "updated_at": 1788120549088, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mthed21ac8fc9045", + "board_id": null, + "title": "Wish B G1: honest, genuinely pinned skills install", + "status": "done", + "claimed_by": "ultracode-g1", + "claimed_at": 1788200088877, + "wish": "skills-everywhere-b", + "group_name": "group-1", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788190346639, + "updated_at": 1788203946197, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mthed27g8f0541d9", + "board_id": null, + "title": "Wish B G2: rehomes and retirement hardening", + "status": "done", + "claimed_by": "ultracode-g2", + "claimed_at": 1788200089113, + "wish": "skills-everywhere-b", + "group_name": "group-2", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788190346860, + "updated_at": 1788203946415, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mthed2dm1199bb26", + "board_id": null, + "title": "Wish B G3: delete the Codex plugin subsystem; rewrite doctor and uninstall", + "status": "done", + "claimed_by": "ultracode-g3", + "claimed_at": 1788203946620, + "wish": "skills-everywhere-b", + "group_name": "group-3", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788190347082, + "updated_at": 1788211897150, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mthed2jhd2bf3239", + "board_id": null, + "title": "Wish B G4: delete the hook runtime and the Claude/Kimi plugin; remove six `check` gates", + "status": "done", + "claimed_by": "ultracode-g4", + "claimed_at": 1788211897371, + "wish": "skills-everywhere-b", + "group_name": "group-4", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788190347293, + "updated_at": 1788216237068, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mthed2p7ae1bdd6f", + "board_id": null, + "title": "Wish B G5: delete Hermes, pi and `agent-sync.ts`; slim `runtime-integrations.ts`", + "status": "done", + "claimed_by": "ultracode-g5", + "claimed_at": 1788216237278, + "wish": "skills-everywhere-b", + "group_name": "group-5", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788190347499, + "updated_at": 1788221761403, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mthed2v47f8e9b80", + "board_id": null, + "title": "Wish B G6: build/release toolchain and the release workflow (C11)", + "status": "done", + "claimed_by": "ultracode-g6", + "claimed_at": 1788221761631, + "wish": "skills-everywhere-b", + "group_name": "group-6", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788190347712, + "updated_at": 1788223248026, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mthed317ecced05c", + "board_id": null, + "title": "Wish B G7: proof", + "status": "done", + "claimed_by": "ultracode-g7", + "claimed_at": 1788223248243, + "wish": "skills-everywhere-b", + "group_name": "group-7", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788190347931, + "updated_at": 1788226216867, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mthed3786bb695ea", + "board_id": null, + "title": "Wish C G1: skills-lint rules + skill-content corrections", + "status": "done", + "claimed_by": "ultracode-c1", + "claimed_at": 1788229827970, + "wish": "skills-everywhere-c", + "group_name": "group-1", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788190348148, + "updated_at": 1788232509762, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mthed3ddbcfe52f0", + "board_id": null, + "title": "Wish C G2: CLAUDE.md + AGENTS.md rewrite and drift guard", + "status": "done", + "claimed_by": "ultracode-c2", + "claimed_at": 1788229828189, + "wish": "skills-everywhere-c", + "group_name": "group-2", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788190348369, + "updated_at": 1788232509975, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mthed3izf9e001fa", + "board_id": null, + "title": "Wish C G3: public docs rewrite + release-notes page (.docs-vendor submodule)", + "status": "done", + "claimed_by": "ultracode-c3", + "claimed_at": 1788229828413, + "wish": "skills-everywhere-c", + "group_name": "group-3", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788190348571, + "updated_at": 1788236668018, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mthed3occ26b9f86", + "board_id": null, + "title": "Wish C G4: docs-lint coverage + final gate", + "status": "done", + "claimed_by": "ultracode-c4", + "claimed_at": 1788232510190, + "wish": "skills-everywhere-c", + "group_name": "group-4", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788190348764, + "updated_at": 1788234612692, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mtlkd9ad80ce9781", + "board_id": null, + "title": "Freeze structured CLI contract for DSH board", + "status": "done", + "claimed_by": "engineer-standard", + "claimed_at": 1788467460493, + "wish": "dsh-genie-board", + "group_name": "structured-cli", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788442298437, + "updated_at": 1788808522143, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mtlkd9g2aae24594", + "board_id": null, + "title": "Build DSH Host adapter and kanban", + "status": "done", + "claimed_by": "codex-g2", + "claimed_at": 1788808569102, + "wish": "dsh-genie-board", + "group_name": "dsh-plugin", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788442298642, + "updated_at": 1788810253292, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mtlkd9ln2f533934", + "board_id": null, + "title": "Package, smoke, and publish DSH discoverability", + "status": "in_progress", + "claimed_by": "g3_implementation", + "claimed_at": 1788810299062, + "wish": "dsh-genie-board", + "group_name": "release", + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788442298843, + "updated_at": 1788810299062, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mtroeuxf15d926b2", + "board_id": null, + "title": "Daily01: recover roadmap key-order sync fix", + "status": "done", + "claimed_by": "roadmap_implementation", + "claimed_at": 1788811924987, + "wish": null, + "group_name": null, + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788811888659, + "updated_at": 1788812675725, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mtrovqgrfa3f8cd3", + "board_id": null, + "title": "Daily01: apply approved fix-loop budget guidance", + "status": "done", + "claimed_by": "a1_implementation", + "claimed_at": 1788812722547, + "wish": null, + "group_name": null, + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788812676027, + "updated_at": 1788813047619, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mtrp3pjybe0a523f", + "board_id": null, + "title": "Daily01: fix hireAgent return under concurrent unhire", + "status": "done", + "claimed_by": "b1_implementation", + "claimed_at": 1788813069094, + "wish": null, + "group_name": null, + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788813048094, + "updated_at": 1788814099607, + "lane": null, + "agent_kind": null, + "heartbeat_at": 1788813959927, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mtrpq96zc09a9618", + "board_id": null, + "title": "Daily01: verify determinism and port approved fixture permissions", + "status": "done", + "claimed_by": "b3_implementation", + "claimed_at": 1788814161543, + "wish": null, + "group_name": null, + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788814099980, + "updated_at": 1788814467138, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mtrpy4u2ad2bb02a", + "board_id": null, + "title": "Daily01: review and integrate project Brain scaffold", + "status": "done", + "claimed_by": "brain_scaffold_implementation", + "claimed_at": 1788814502156, + "wish": null, + "group_name": null, + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788814467579, + "updated_at": 1788814870115, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mtrr50zm1be98f68", + "board_id": null, + "title": "Daily01: reject Unicode controls in DSH board input", + "status": "done", + "claimed_by": "unicode_controls_implementation", + "claimed_at": 1788816507937, + "wish": null, + "group_name": null, + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788816468802, + "updated_at": 1788817997403, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + }, + { + "id": "t_mtrrebyk5f21272f", + "board_id": null, + "title": "Daily01: close remaining promotion review contract gaps", + "status": "done", + "claimed_by": "promotion_review_implementation", + "claimed_at": 1788816934672, + "wish": null, + "group_name": null, + "assigned_agent": null, + "assigned_reason": null, + "created_at": 1788816902924, + "updated_at": 1788817997948, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null, + "block_kind": null + } + ], + "task_dependencies": [], + "stage_log": [], + "task_events": [ + { + "id": 1, + "task_id": "t_mrxqzo6s8fbb27ec", + "kind": "block", + "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", + "author_kind": "claude-code", + "author": null, + "created_at": 1784827974073 + }, + { + "id": 2, + "task_id": "t_mrxr9wzrda10d35b", + "kind": "block", + "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", + "author_kind": "claude-code", + "author": null, + "created_at": 1784827974160 + }, + { + "id": 3, + "task_id": "t_mrxr9x1pbce3a16b", + "kind": "block", + "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", + "author_kind": "claude-code", + "author": null, + "created_at": 1784827974238 + }, + { + "id": 4, + "task_id": "t_mrxr9x49eb19d0ad", + "kind": "block", + "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", + "author_kind": "claude-code", + "author": null, + "created_at": 1784827974322 + }, + { + "id": 5, + "task_id": "t_mrxr9x6sb3dfb668", + "kind": "block", + "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", + "author_kind": "claude-code", + "author": null, + "created_at": 1784827974396 + }, + { + "id": 6, + "task_id": "t_mrxr9x8t711ce43e", + "kind": "block", + "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", + "author_kind": "claude-code", + "author": null, + "created_at": 1784827974462 + }, + { + "id": 7, + "task_id": "t_mrxr9xbb010c4f81", + "kind": "block", + "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", + "author_kind": "claude-code", + "author": null, + "created_at": 1784827974557 + }, + { + "id": 38, + "task_id": "t_mscebl889535b59e", + "kind": "claim", + "note": "claimed by engineer-group1", + "author_kind": "claude-code", + "author": null, + "created_at": 1785712701025 + }, + { + "id": 39, + "task_id": "t_mscebl889535b59e", + "kind": "release", + "note": "completed", + "author_kind": "claude-code", + "author": null, + "created_at": 1785714161658 + }, + { + "id": 40, + "task_id": "t_mscebla86e45ab77", + "kind": "claim", + "note": "claimed by engineer-group2", + "author_kind": "claude-code", + "author": null, + "created_at": 1785714210396 + }, + { + "id": 41, + "task_id": "t_mscebla86e45ab77", + "kind": "release", + "note": "completed", + "author_kind": "claude-code", + "author": null, + "created_at": 1785716639548 + }, + { + "id": 42, + "task_id": "t_msi18po2a63fe11f", + "kind": "claim", + "note": "claimed by g1-engineer", + "author_kind": "human", + "author": null, + "created_at": 1786056837591 + }, + { + "id": 43, + "task_id": "t_msi18pv9b29465c2", + "kind": "claim", + "note": "claimed by g3-engineer", + "author_kind": "human", + "author": null, + "created_at": 1786057120256 + }, + { + "id": 44, + "task_id": "t_msi18prp35883ce7", + "kind": "claim", + "note": "claimed by g2-engineer", + "author_kind": "human", + "author": null, + "created_at": 1786057120710 + }, + { + "id": 45, + "task_id": "t_msiyp3ua4e038e63", + "kind": "claim", + "note": "claimed by block-model-eng", + "author_kind": "claude-code", + "author": null, + "created_at": 1786118248010 + }, + { + "id": 46, + "task_id": "t_msiyp3x10655eab9", + "kind": "claim", + "note": "claimed by index-lane-eng", + "author_kind": "claude-code", + "author": null, + "created_at": 1786118261193 + }, + { + "id": 47, + "task_id": "t_msiyp3ua4e038e63", + "kind": "release", + "note": "completed", + "author_kind": "claude-code", + "author": null, + "created_at": 1786120597564 + }, + { + "id": 48, + "task_id": "t_msiyp3x10655eab9", + "kind": "release", + "note": "completed", + "author_kind": "claude-code", + "author": null, + "created_at": 1786121037960 + }, + { + "id": 49, + "task_id": "t_msiyp3zr6f65f0fc", + "kind": "claim", + "note": "claimed by set-wish-eng", + "author_kind": "claude-code", + "author": null, + "created_at": 1786121077747 + }, + { + "id": 50, + "task_id": "t_msiyp3zr6f65f0fc", + "kind": "report", + "note": "set-wish verb shipped as b5178937a: setTaskWish + formatWishRef in task-state.ts, task set-wish CLI, 16 new tests, TAXONOMY updated. Focused suites 139 pass / 0 fail; check:fast exit 0.", + "author_kind": "claude-code", + "author": "set-wish-eng", + "created_at": 1786121536059 + }, + { + "id": 51, + "task_id": "t_msiyp42ib277f60a", + "kind": "claim", + "note": "claimed by task-delete-eng", + "author_kind": "claude-code", + "author": null, + "created_at": 1786121655149 + }, + { + "id": 52, + "task_id": "t_msiyp3zr6f65f0fc", + "kind": "release", + "note": "completed", + "author_kind": "claude-code", + "author": null, + "created_at": 1786122063091 + }, + { + "id": 53, + "task_id": "t_msiyp42ib277f60a", + "kind": "release", + "note": "completed", + "author_kind": "claude-code", + "author": null, + "created_at": 1786122699030 + }, + { + "id": 54, + "task_id": "t_msi18po2a63fe11f", + "kind": "release", + "note": "completed", + "author_kind": "human", + "author": "g1-engineer", + "created_at": 1786129435207 + }, + { + "id": 55, + "task_id": "t_msi18prp35883ce7", + "kind": "release", + "note": "completed", + "author_kind": "human", + "author": "g2-engineer", + "created_at": 1786129435338 + }, + { + "id": 56, + "task_id": "t_msi18pv9b29465c2", + "kind": "release", + "note": "completed", + "author_kind": "human", + "author": "g3-engineer", + "created_at": 1786129435459 + }, + { + "id": 65, + "task_id": "t_msnx6kz8c52d4762", + "kind": "claim", + "note": "claimed by engineer-a1", + "author_kind": "human", + "author": "cli", + "created_at": 1786408694769 + }, + { + "id": 66, + "task_id": "t_msnx6kz8c52d4762", + "kind": "release", + "note": "completed", + "author_kind": "human", + "author": "orchestrator-prime", + "created_at": 1786411453387 + }, + { + "id": 67, + "task_id": "t_msnwyz7136c34cb2", + "kind": "claim", + "note": "claimed by engineer-c", + "author_kind": "human", + "author": "cli", + "created_at": 1786411499996 + }, + { + "id": 68, + "task_id": "t_msnwyz4qff08ad29", + "kind": "claim", + "note": "claimed by engineer-b", + "author_kind": "human", + "author": "cli", + "created_at": 1786411501613 + }, + { + "id": 69, + "task_id": "t_msnx6l1k7cddfcc0", + "kind": "claim", + "note": "claimed by engineer-a2", + "author_kind": "human", + "author": "cli", + "created_at": 1786411502192 + }, + { + "id": 70, + "task_id": "t_msnx6l1k7cddfcc0", + "kind": "release", + "note": "completed", + "author_kind": "human", + "author": "orchestrator-prime", + "created_at": 1786412198104 + }, + { + "id": 71, + "task_id": "t_msnwyz4qff08ad29", + "kind": "release", + "note": "completed", + "author_kind": "human", + "author": "orchestrator-prime", + "created_at": 1786412348073 + }, + { + "id": 72, + "task_id": "t_msnwyz7136c34cb2", + "kind": "release", + "note": "completed", + "author_kind": "human", + "author": "orchestrator-prime", + "created_at": 1786412796523 + }, + { + "id": 73, + "task_id": "t_msnw1wva78ca40b7", + "kind": "release", + "note": "completed", + "author_kind": "human", + "author": "orchestrator-prime", + "created_at": 1786413384911 + }, + { + "id": 74, + "task_id": "t_msoqsd8sdb8df48b", + "kind": "claim", + "note": "claimed by g1-server-core", + "author_kind": "human", + "author": "cli", + "created_at": 1786460670279 + }, + { + "id": 75, + "task_id": "t_msoqsd8sdb8df48b", + "kind": "release", + "note": "completed", + "author_kind": "human", + "author": "cli", + "created_at": 1786462438505 + }, + { + "id": 76, + "task_id": "t_msp4fp6ce714c8be", + "kind": "wish", + "note": "(none)→delegate-bridge", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1786480645159 + }, + { + "id": 77, + "task_id": "t_msp4fp6ce714c8be", + "kind": "block", + "note": "blocked on remotty headless-turn-open (cross-repo; design M2 mechanism)", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1786480645240 + }, + { + "id": 78, + "task_id": "t_msp6a1qlaaa8acdc", + "kind": "claim", + "note": "claimed by engineer-trivial-a", + "author_kind": "human", + "author": "cli", + "created_at": 1786554116904 + }, + { + "id": 79, + "task_id": "t_msp6a1qlaaa8acdc", + "kind": "release", + "note": "completed", + "author_kind": "human", + "author": "cli", + "created_at": 1786554590852 + }, + { + "id": 80, + "task_id": "t_msoqsd8sdb8df48b", + "kind": "release", + "note": "completed", + "author_kind": "human", + "author": "cli", + "created_at": 1786558209313 + }, + { + "id": 81, + "task_id": "t_msoqsdb5429d06e8", + "kind": "claim", + "note": "claimed by g2-write-tools", + "author_kind": "human", + "author": "cli", + "created_at": 1786558267434 + }, + { + "id": 82, + "task_id": "t_msoqsdb5429d06e8", + "kind": "release", + "note": "completed", + "author_kind": "human", + "author": "cli", + "created_at": 1786568148733 + }, + { + "id": 83, + "task_id": "t_msoqsdde40b31ee9", + "kind": "claim", + "note": "claimed by g3-e2e-sweep", + "author_kind": "human", + "author": "cli", + "created_at": 1786568193562 + }, + { + "id": 84, + "task_id": "t_msoqsdde40b31ee9", + "kind": "release", + "note": "completed", + "author_kind": "human", + "author": "cli", + "created_at": 1786628093499 + }, + { + "id": 85, + "task_id": "t_msp4fp6ce714c8be", + "kind": "block", + "note": "re-targeted by spawn-context-contract group reversals (2026-08-13): gating moves from headless-turn-open to herdr-swap (0.3) + engine-turn-verbs (remotty board card t_msqkkjt221792465); S-F turn transport re-bases from one-shot headless launches to live herdr sessions (agent start -> prompt --wait --until -> read -> report/release)", + "author_kind": "prime-agent", + "author": "dream-scc-reversals", + "created_at": 1786657262650 + }, + { + "id": 86, + "task_id": "t_msp4fp6ce714c8be", + "kind": "comment", + "note": "Board-edge re-target recorded (spawn-context-contract group reversals, 2026-08-13): the enforced block on this card was re-placed via task block, never unblocked, so the live gating now reads herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) instead of headless-turn-open. Groups S-F re-base their turn transport to live herdr sessions; group A (done, W1 scope) is unaffected.", + "author_kind": "prime-agent", + "author": "dream-scc-reversals", + "created_at": 1786657272200 + }, + { + "id": 87, + "task_id": "t_msp6a1sz195f0ea9", + "kind": "comment", + "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): the seam spike now proves the live-herdr turn seam (agent start / prompt --wait --until / read) instead of the one-shot headless-launch seam. Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", + "author_kind": "prime-agent", + "author": "dream-scc-reversals", + "created_at": 1786657272357 + }, + { + "id": 88, + "task_id": "t_msp6a1vd3c103d09", + "kind": "comment", + "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): adapter cards + brief writer target live herdr sessions (id-free continue form via herdr agent verbs) instead of turn-per-launch headless argv. Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", + "author_kind": "prime-agent", + "author": "dream-scc-reversals", + "created_at": 1786657272489 + }, + { + "id": 89, + "task_id": "t_msp6a1xzca62d364", + "kind": "comment", + "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): bridge core drives live herdr sessions per turn (agent prompt --wait --until + read hand-back) instead of one-shot headless turn launches. Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", + "author_kind": "prime-agent", + "author": "dream-scc-reversals", + "created_at": 1786657272621 + }, + { + "id": 90, + "task_id": "t_msp6a20k0a1df8a8", + "kind": "comment", + "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): degradation paths now cover herdr session failures and release (not detached-launch refusals). Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", + "author_kind": "prime-agent", + "author": "dream-scc-reversals", + "created_at": 1786657272747 + }, + { + "id": 91, + "task_id": "t_msp6a23b3c681369", + "kind": "comment", + "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): task fan + ratification transport re-bases to live herdr sessions (per-turn prompt --wait --until / read) instead of one-shot headless launches. Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", + "author_kind": "prime-agent", + "author": "dream-scc-reversals", + "created_at": 1786657272871 + }, + { + "id": 92, + "task_id": "t_msp6a25sef5b7e40", + "kind": "comment", + "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): the adapter smoke matrix exercises live-herdr-session turns instead of one-shot headless launches. Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", + "author_kind": "prime-agent", + "author": "dream-scc-reversals", + "created_at": 1786657273022 + }, + { + "id": 93, + "task_id": "t_mtdns9r623748cfd", + "kind": "report", + "note": "Implemented bundled skills/quick plus pm cadence. RED observed on missing skill; focused contract 1/1, release/docs 43/43, mirror/activation 24/24, fresh-install 18/18, skills lint, typecheck, wishes/complexity/council/hook/plugin gates passed. Full check remains red on pre-existing uuid dead-code and unrelated concurrent historical-fixture/time-budget failures. No commit, push, PR, deploy, homolog, or production mutation.", + "author_kind": "hermes", + "author": "cli", + "created_at": 1787965628968 + }, + { + "id": 94, + "task_id": "t_mtdns9r623748cfd", + "kind": "release", + "note": "completed", + "author_kind": "hermes", + "author": "cli", + "created_at": 1787965629294 + }, + { + "id": 95, + "task_id": "t_mtfzsu4ee1390c48", + "kind": "wish", + "note": "(none)→skills-everywhere", "author_kind": "claude-code", - "author": null, - "created_at": 1784827974322 + "author": "cli", + "created_at": 1788108622794 }, { - "id": 5, - "task_id": "t_mrxr9x6sb3dfb668", - "kind": "block", - "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", + "id": 96, + "task_id": "t_mtg14v70f2fb54ba", + "kind": "wish", + "note": "codex-skill-installer#group-1→skills-everywhere", "author_kind": "claude-code", - "author": null, - "created_at": 1784827974396 + "author": "cli", + "created_at": 1788108622988 }, { - "id": 6, - "task_id": "t_mrxr9x8t711ce43e", - "kind": "block", - "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", + "id": 97, + "task_id": "t_mtg14vc4e18dcc51", + "kind": "wish", + "note": "codex-skill-installer#group-2→skills-everywhere", "author_kind": "claude-code", - "author": null, - "created_at": 1784827974462 + "author": "cli", + "created_at": 1788108623184 }, { - "id": 7, - "task_id": "t_mrxr9xbb010c4f81", - "kind": "block", - "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", + "id": 98, + "task_id": "t_mtg14vhpa6d08b44", + "kind": "wish", + "note": "codex-skill-installer#group-3→skills-everywhere", "author_kind": "claude-code", - "author": null, - "created_at": 1784827974557 + "author": "cli", + "created_at": 1788108623380 }, { - "id": 38, - "task_id": "t_mscebl889535b59e", + "id": 99, + "task_id": "t_mtg14vn5e0de4dc8", + "kind": "wish", + "note": "codex-skill-installer#group-4→skills-everywhere", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788108623614 + }, + { + "id": 100, + "task_id": "t_mtg14vsi44350a36", + "kind": "wish", + "note": "codex-skill-installer#group-5→skills-everywhere", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788108623808 + }, + { + "id": 101, + "task_id": "t_mtg14vxr224f5565", + "kind": "wish", + "note": "codex-skill-installer#group-6→skills-everywhere", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788108624005 + }, + { + "id": 102, + "task_id": "t_mtg14w35c3ea69d2", + "kind": "wish", + "note": "codex-skill-installer#group-7→skills-everywhere", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788108624202 + }, + { + "id": 103, + "task_id": "t_mtg14v70f2fb54ba", + "kind": "wish", + "note": "skills-everywhere→skills-everywhere#group-1", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788108651576 + }, + { + "id": 104, + "task_id": "t_mtg14vc4e18dcc51", + "kind": "wish", + "note": "skills-everywhere→skills-everywhere#group-2", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788108651778 + }, + { + "id": 105, + "task_id": "t_mtg14vhpa6d08b44", + "kind": "wish", + "note": "skills-everywhere→skills-everywhere#group-3", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788108651977 + }, + { + "id": 106, + "task_id": "t_mtg14vn5e0de4dc8", + "kind": "wish", + "note": "skills-everywhere→skills-everywhere#group-4", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788108652172 + }, + { + "id": 107, + "task_id": "t_mtg14vsi44350a36", + "kind": "wish", + "note": "skills-everywhere→skills-everywhere#group-5", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788108652368 + }, + { + "id": 108, + "task_id": "t_mtg14vxr224f5565", + "kind": "wish", + "note": "skills-everywhere→skills-everywhere#group-6", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788108652576 + }, + { + "id": 109, + "task_id": "t_mtg14w35c3ea69d2", + "kind": "wish", + "note": "skills-everywhere→skills-everywhere#group-7", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788108652779 + }, + { + "id": 110, + "task_id": "t_mtg14vn5e0de4dc8", "kind": "claim", - "note": "claimed by engineer-group1", + "note": "claimed by engineer-g4", "author_kind": "claude-code", - "author": null, - "created_at": 1785712701025 + "author": "cli", + "created_at": 1788110157613 }, { - "id": 39, - "task_id": "t_mscebl889535b59e", + "id": 111, + "task_id": "t_mtg14vxr224f5565", + "kind": "claim", + "note": "claimed by engineer-g6", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788110175134 + }, + { + "id": 112, + "task_id": "t_mtg14v70f2fb54ba", + "kind": "claim", + "note": "claimed by engineer-g1", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788111374069 + }, + { + "id": 113, + "task_id": "t_mtg14vn5e0de4dc8", "kind": "release", "note": "completed", "author_kind": "claude-code", - "author": null, - "created_at": 1785714161658 + "author": "cli", + "created_at": 1788111941078 }, { - "id": 40, - "task_id": "t_mscebla86e45ab77", - "kind": "claim", - "note": "claimed by engineer-group2", + "id": 114, + "task_id": "t_mtg14vxr224f5565", + "kind": "release", + "note": "completed", "author_kind": "claude-code", - "author": null, - "created_at": 1785714210396 + "author": "cli", + "created_at": 1788113202419 }, { - "id": 41, - "task_id": "t_mscebla86e45ab77", + "id": 115, + "task_id": "t_mtg14v70f2fb54ba", "kind": "release", "note": "completed", "author_kind": "claude-code", - "author": null, - "created_at": 1785716639548 + "author": "cli", + "created_at": 1788113339913 }, { - "id": 42, - "task_id": "t_msi18po2a63fe11f", + "id": 116, + "task_id": "t_mtg14vc4e18dcc51", "kind": "claim", - "note": "claimed by g1-engineer", - "author_kind": "human", - "author": null, - "created_at": 1786056837591 + "note": "claimed by engineer-g2", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788113384175 }, { - "id": 43, - "task_id": "t_msi18pv9b29465c2", + "id": 117, + "task_id": "t_mtg14vhpa6d08b44", "kind": "claim", - "note": "claimed by g3-engineer", - "author_kind": "human", - "author": null, - "created_at": 1786057120256 + "note": "claimed by engineer-g3", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788113405614 }, { - "id": 44, - "task_id": "t_msi18prp35883ce7", + "id": 118, + "task_id": "t_mtg14vsi44350a36", "kind": "claim", - "note": "claimed by g2-engineer", - "author_kind": "human", - "author": null, - "created_at": 1786057120710 + "note": "claimed by engineer-g5", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788113433058 }, { - "id": 45, - "task_id": "t_msiyp3ua4e038e63", + "id": 119, + "task_id": "t_mtg14vhpa6d08b44", + "kind": "release", + "note": "completed", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788116944773 + }, + { + "id": 120, + "task_id": "t_mtg14vsi44350a36", + "kind": "release", + "note": "completed", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788117468065 + }, + { + "id": 121, + "task_id": "t_mtg14vc4e18dcc51", + "kind": "release", + "note": "completed", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788118209074 + }, + { + "id": 122, + "task_id": "t_mtg14w35c3ea69d2", + "kind": "release", + "note": "completed", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788120549089 + }, + { + "id": 123, + "task_id": "t_mthed21ac8fc9045", "kind": "claim", - "note": "claimed by block-model-eng", + "note": "claimed by ultracode-g1", "author_kind": "claude-code", - "author": null, - "created_at": 1786118248010 + "author": "cli", + "created_at": 1788200088878 }, { - "id": 46, - "task_id": "t_msiyp3x10655eab9", + "id": 124, + "task_id": "t_mthed27g8f0541d9", "kind": "claim", - "note": "claimed by index-lane-eng", + "note": "claimed by ultracode-g2", "author_kind": "claude-code", - "author": null, - "created_at": 1786118261193 + "author": "cli", + "created_at": 1788200089114 }, { - "id": 47, - "task_id": "t_msiyp3ua4e038e63", + "id": 125, + "task_id": "t_mthed21ac8fc9045", "kind": "release", "note": "completed", "author_kind": "claude-code", - "author": null, - "created_at": 1786120597564 + "author": "cli", + "created_at": 1788203946198 }, { - "id": 48, - "task_id": "t_msiyp3x10655eab9", + "id": 126, + "task_id": "t_mthed27g8f0541d9", "kind": "release", "note": "completed", "author_kind": "claude-code", - "author": null, - "created_at": 1786121037960 + "author": "cli", + "created_at": 1788203946415 }, { - "id": 49, - "task_id": "t_msiyp3zr6f65f0fc", + "id": 127, + "task_id": "t_mthed2dm1199bb26", "kind": "claim", - "note": "claimed by set-wish-eng", + "note": "claimed by ultracode-g3", "author_kind": "claude-code", - "author": null, - "created_at": 1786121077747 + "author": "cli", + "created_at": 1788203946620 }, { - "id": 50, - "task_id": "t_msiyp3zr6f65f0fc", - "kind": "report", - "note": "set-wish verb shipped as b5178937a: setTaskWish + formatWishRef in task-state.ts, task set-wish CLI, 16 new tests, TAXONOMY updated. Focused suites 139 pass / 0 fail; check:fast exit 0.", + "id": 128, + "task_id": "t_mthed2dm1199bb26", + "kind": "release", + "note": "completed", "author_kind": "claude-code", - "author": "set-wish-eng", - "created_at": 1786121536059 + "author": "cli", + "created_at": 1788211897151 }, - { - "id": 51, - "task_id": "t_msiyp42ib277f60a", + { + "id": 129, + "task_id": "t_mthed2jhd2bf3239", "kind": "claim", - "note": "claimed by task-delete-eng", + "note": "claimed by ultracode-g4", "author_kind": "claude-code", - "author": null, - "created_at": 1786121655149 + "author": "cli", + "created_at": 1788211897373 }, { - "id": 52, - "task_id": "t_msiyp3zr6f65f0fc", + "id": 130, + "task_id": "t_mthed2jhd2bf3239", "kind": "release", "note": "completed", "author_kind": "claude-code", - "author": null, - "created_at": 1786122063091 + "author": "cli", + "created_at": 1788216237069 }, { - "id": 53, - "task_id": "t_msiyp42ib277f60a", - "kind": "release", - "note": "completed", + "id": 131, + "task_id": "t_mthed2p7ae1bdd6f", + "kind": "claim", + "note": "claimed by ultracode-g5", "author_kind": "claude-code", - "author": null, - "created_at": 1786122699030 + "author": "cli", + "created_at": 1788216237279 }, { - "id": 54, - "task_id": "t_msi18po2a63fe11f", + "id": 132, + "task_id": "t_mthed2p7ae1bdd6f", "kind": "release", "note": "completed", - "author_kind": "human", - "author": "g1-engineer", - "created_at": 1786129435207 + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788221761404 }, { - "id": 55, - "task_id": "t_msi18prp35883ce7", - "kind": "release", - "note": "completed", - "author_kind": "human", - "author": "g2-engineer", - "created_at": 1786129435338 + "id": 133, + "task_id": "t_mthed2v47f8e9b80", + "kind": "claim", + "note": "claimed by ultracode-g6", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788221761632 }, { - "id": 56, - "task_id": "t_msi18pv9b29465c2", + "id": 134, + "task_id": "t_mthed2v47f8e9b80", "kind": "release", "note": "completed", - "author_kind": "human", - "author": "g3-engineer", - "created_at": 1786129435459 + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788223248027 }, { - "id": 65, - "task_id": "t_msnx6kz8c52d4762", + "id": 135, + "task_id": "t_mthed317ecced05c", "kind": "claim", - "note": "claimed by engineer-a1", - "author_kind": "human", + "note": "claimed by ultracode-g7", + "author_kind": "claude-code", "author": "cli", - "created_at": 1786408694769 + "created_at": 1788223248244 }, { - "id": 66, - "task_id": "t_msnx6kz8c52d4762", + "id": 136, + "task_id": "t_mthed317ecced05c", "kind": "release", "note": "completed", - "author_kind": "human", - "author": "orchestrator-prime", - "created_at": 1786411453387 + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788226216868 }, { - "id": 67, - "task_id": "t_msnwyz7136c34cb2", + "id": 137, + "task_id": "t_mthed3786bb695ea", "kind": "claim", - "note": "claimed by engineer-c", - "author_kind": "human", + "note": "claimed by ultracode-c1", + "author_kind": "claude-code", "author": "cli", - "created_at": 1786411499996 + "created_at": 1788229827970 }, { - "id": 68, - "task_id": "t_msnwyz4qff08ad29", + "id": 138, + "task_id": "t_mthed3ddbcfe52f0", "kind": "claim", - "note": "claimed by engineer-b", - "author_kind": "human", + "note": "claimed by ultracode-c2", + "author_kind": "claude-code", "author": "cli", - "created_at": 1786411501613 + "created_at": 1788229828190 }, { - "id": 69, - "task_id": "t_msnx6l1k7cddfcc0", + "id": 139, + "task_id": "t_mthed3izf9e001fa", "kind": "claim", - "note": "claimed by engineer-a2", - "author_kind": "human", + "note": "claimed by ultracode-c3", + "author_kind": "claude-code", "author": "cli", - "created_at": 1786411502192 + "created_at": 1788229828414 }, { - "id": 70, - "task_id": "t_msnx6l1k7cddfcc0", + "id": 140, + "task_id": "t_mthed3786bb695ea", "kind": "release", "note": "completed", - "author_kind": "human", - "author": "orchestrator-prime", - "created_at": 1786412198104 + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788232509763 }, { - "id": 71, - "task_id": "t_msnwyz4qff08ad29", + "id": 141, + "task_id": "t_mthed3ddbcfe52f0", "kind": "release", "note": "completed", - "author_kind": "human", - "author": "orchestrator-prime", - "created_at": 1786412348073 + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788232509976 }, { - "id": 72, - "task_id": "t_msnwyz7136c34cb2", + "id": 142, + "task_id": "t_mthed3occ26b9f86", + "kind": "claim", + "note": "claimed by ultracode-c4", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788232510191 + }, + { + "id": 143, + "task_id": "t_mthed3occ26b9f86", "kind": "release", "note": "completed", - "author_kind": "human", - "author": "orchestrator-prime", - "created_at": 1786412796523 + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788234612693 }, { - "id": 73, - "task_id": "t_msnw1wva78ca40b7", + "id": 144, + "task_id": "t_mthed3izf9e001fa", "kind": "release", "note": "completed", - "author_kind": "human", - "author": "orchestrator-prime", - "created_at": 1786413384911 + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788236668019 }, { - "id": 74, - "task_id": "t_msoqsd8sdb8df48b", + "id": 145, + "task_id": "t_mtlkd9ad80ce9781", "kind": "claim", - "note": "claimed by g1-server-core", - "author_kind": "human", + "note": "claimed by engineer-standard", + "author_kind": "codex", "author": "cli", - "created_at": 1786460670279 + "created_at": 1788467460494 }, { - "id": 75, - "task_id": "t_msoqsd8sdb8df48b", + "id": 146, + "task_id": "t_mtlkd9ad80ce9781", "kind": "release", "note": "completed", - "author_kind": "human", + "author_kind": "codex", "author": "cli", - "created_at": 1786462438505 + "created_at": 1788808522145 }, { - "id": 76, - "task_id": "t_msp4fp6ce714c8be", - "kind": "wish", - "note": "(none)→delegate-bridge", - "author_kind": "claude-code", + "id": 147, + "task_id": "t_mtlkd9g2aae24594", + "kind": "claim", + "note": "claimed by codex-g2", + "author_kind": "codex", "author": "cli", - "created_at": 1786480645159 + "created_at": 1788808569103 }, { - "id": 77, - "task_id": "t_msp4fp6ce714c8be", - "kind": "block", - "note": "blocked on remotty headless-turn-open (cross-repo; design M2 mechanism)", - "author_kind": "claude-code", + "id": 148, + "task_id": "t_mtlkd9g2aae24594", + "kind": "release", + "note": "completed", + "author_kind": "codex", "author": "cli", - "created_at": 1786480645240 + "created_at": 1788810253296 }, { - "id": 78, - "task_id": "t_msp6a1qlaaa8acdc", + "id": 149, + "task_id": "t_mtlkd9ln2f533934", "kind": "claim", - "note": "claimed by engineer-trivial-a", - "author_kind": "human", + "note": "claimed by g3_implementation", + "author_kind": "codex", "author": "cli", - "created_at": 1786554116904 + "created_at": 1788810299063 }, { - "id": 79, - "task_id": "t_msp6a1qlaaa8acdc", + "id": 150, + "task_id": "t_mtlkd9ln2f533934", + "kind": "comment", + "note": "G3 implementation accepted for dev-to-main PR: independent SHIP, parent full gate 2024 pass / 1 skip / 0 fail, four-platform local artifact and extracted runtime-floor proof. Actual protected stable publication, published verification and topic remain pending after human main/release approval. Keep release task in progress.", + "author_kind": "codex", + "author": "cli", + "created_at": 1788811870438 + }, + { + "id": 151, + "task_id": "t_mtroeuxf15d926b2", + "kind": "claim", + "note": "claimed by roadmap_implementation", + "author_kind": "codex", + "author": "cli", + "created_at": 1788811924988 + }, + { + "id": 152, + "task_id": "t_mtroeuxf15d926b2", "kind": "release", "note": "completed", - "author_kind": "human", + "author_kind": "codex", "author": "cli", - "created_at": 1786554590852 + "created_at": 1788812675726 }, { - "id": 80, - "task_id": "t_msoqsd8sdb8df48b", + "id": 153, + "task_id": "t_mtrovqgrfa3f8cd3", + "kind": "claim", + "note": "claimed by a1_implementation", + "author_kind": "codex", + "author": "cli", + "created_at": 1788812722548 + }, + { + "id": 154, + "task_id": "t_mtrovqgrfa3f8cd3", "kind": "release", "note": "completed", - "author_kind": "human", + "author_kind": "codex", "author": "cli", - "created_at": 1786558209313 + "created_at": 1788813047621 }, { - "id": 81, - "task_id": "t_msoqsdb5429d06e8", + "id": 155, + "task_id": "t_mtrp3pjybe0a523f", "kind": "claim", - "note": "claimed by g2-write-tools", - "author_kind": "human", + "note": "claimed by b1_implementation", + "author_kind": "codex", "author": "cli", - "created_at": 1786558267434 + "created_at": 1788813069096 }, { - "id": 82, - "task_id": "t_msoqsdb5429d06e8", + "id": 156, + "task_id": "t_mtrp3pjybe0a523f", + "kind": "comment", + "note": "Accepted ce6fd7acc: independent SHIP, parent fullgate 2031 pass / 1 skip / 0 fail; real SQLite API and compiled CLI backup/import dogfood passed. Evidence: b1-acceptance.md. Internal retained API only; no new hire CLI.", + "author_kind": "codex", + "author": "cli", + "created_at": 1788814099274 + }, + { + "id": 157, + "task_id": "t_mtrp3pjybe0a523f", "kind": "release", "note": "completed", - "author_kind": "human", + "author_kind": "codex", "author": "cli", - "created_at": 1786568148733 + "created_at": 1788814099609 }, { - "id": 83, - "task_id": "t_msoqsdde40b31ee9", + "id": 158, + "task_id": "t_mtrpq96zc09a9618", "kind": "claim", - "note": "claimed by g3-e2e-sweep", - "author_kind": "human", + "note": "claimed by b3_implementation", + "author_kind": "codex", "author": "cli", - "created_at": 1786568193562 + "created_at": 1788814161544 }, { - "id": 84, - "task_id": "t_msoqsdde40b31ee9", + "id": 159, + "task_id": "t_mtrpq96zc09a9618", + "kind": "comment", + "note": "B2 independently already-fixed on main. B3 accepted a1574d13e, independent SHIP; 12 tests/73 assertions pass at default0022 and027, exact surviving source-hunk dispositions recorded. Evidence b3-acceptance.md.", + "author_kind": "codex", + "author": "cli", + "created_at": 1788814466701 + }, + { + "id": 160, + "task_id": "t_mtrpq96zc09a9618", "kind": "release", "note": "completed", - "author_kind": "human", + "author_kind": "codex", "author": "cli", - "created_at": 1786628093499 + "created_at": 1788814467142 }, { - "id": 85, - "task_id": "t_msp4fp6ce714c8be", - "kind": "block", - "note": "re-targeted by spawn-context-contract group reversals (2026-08-13): gating moves from headless-turn-open to herdr-swap (0.3) + engine-turn-verbs (remotty board card t_msqkkjt221792465); S-F turn transport re-bases from one-shot headless launches to live herdr sessions (agent start -> prompt --wait --until -> read -> report/release)", - "author_kind": "prime-agent", - "author": "dream-scc-reversals", - "created_at": 1786657262650 + "id": 161, + "task_id": "t_mtrpy4u2ad2bb02a", + "kind": "claim", + "note": "claimed by brain_scaffold_implementation", + "author_kind": "codex", + "author": "cli", + "created_at": 1788814502158 }, { - "id": 86, - "task_id": "t_msp4fp6ce714c8be", + "id": 162, + "task_id": "t_mtrpy4u2ad2bb02a", "kind": "comment", - "note": "Board-edge re-target recorded (spawn-context-contract group reversals, 2026-08-13): the enforced block on this card was re-placed via task block, never unblocked, so the live gating now reads herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) instead of headless-turn-open. Groups S-F re-base their turn transport to live herdr sessions; group A (done, W1 scope) is unaffected.", - "author_kind": "prime-agent", - "author": "dream-scc-reversals", - "created_at": 1786657272200 + "note": "Accepted c06f05869: preserves original PR2893 commit, independent SHIP all13files; corrected note routing and template dates; static JSON/frontmatter/rendering/identity/reference checks and Biome pass. Static scaffold only; no live Brain integration claim. PR disposition reconciled after dev integration.", + "author_kind": "codex", + "author": "cli", + "created_at": 1788814869856 }, { - "id": 87, - "task_id": "t_msp6a1sz195f0ea9", - "kind": "comment", - "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): the seam spike now proves the live-herdr turn seam (agent start / prompt --wait --until / read) instead of the one-shot headless-launch seam. Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", - "author_kind": "prime-agent", - "author": "dream-scc-reversals", - "created_at": 1786657272357 + "id": 163, + "task_id": "t_mtrpy4u2ad2bb02a", + "kind": "release", + "note": "completed", + "author_kind": "codex", + "author": "cli", + "created_at": 1788814870117 }, { - "id": 88, - "task_id": "t_msp6a1vd3c103d09", - "kind": "comment", - "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): adapter cards + brief writer target live herdr sessions (id-free continue form via herdr agent verbs) instead of turn-per-launch headless argv. Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", - "author_kind": "prime-agent", - "author": "dream-scc-reversals", - "created_at": 1786657272489 + "id": 164, + "task_id": "t_mtrr50zm1be98f68", + "kind": "claim", + "note": "claimed by unicode_controls_implementation", + "author_kind": "codex", + "author": "cli", + "created_at": 1788816507940 }, { - "id": 89, - "task_id": "t_msp6a1xzca62d364", + "id": 165, + "task_id": "t_mtrr50zm1be98f68", "kind": "comment", - "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): bridge core drives live herdr sessions per turn (agent prompt --wait --until + read hand-back) instead of one-shot headless turn launches. Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", - "author_kind": "prime-agent", - "author": "dream-scc-reversals", - "created_at": 1786657272621 + "note": "Unicode code correction committed5f97e6629 and independently SHIP:28tests461assertions,81 no-CLI rejection vectors. Final aggregate/dogfood acceptance remains pending until remaining validated promotion-review corrections are complete.", + "author_kind": "codex", + "author": "cli", + "created_at": 1788816902603 }, { - "id": 90, - "task_id": "t_msp6a20k0a1df8a8", - "kind": "comment", - "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): degradation paths now cover herdr session failures and release (not detached-launch refusals). Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", - "author_kind": "prime-agent", - "author": "dream-scc-reversals", - "created_at": 1786657272747 + "id": 166, + "task_id": "t_mtrrebyk5f21272f", + "kind": "claim", + "note": "claimed by promotion_review_implementation", + "author_kind": "codex", + "author": "cli", + "created_at": 1788816934673 }, { - "id": 91, - "task_id": "t_msp6a23b3c681369", + "id": 167, + "task_id": "t_mtrr50zm1be98f68", "kind": "comment", - "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): task fan + ratification transport re-bases to live herdr sessions (per-turn prompt --wait --until / read) instead of one-shot headless launches. Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", - "author_kind": "prime-agent", - "author": "dream-scc-reversals", - "created_at": 1786657272871 + "note": "Accepted: Unicode fix 5f97e6629 independently SHIP; aggregate eeafaeace passed 2042 tests, 1 skip, 0 failures. Four .101 artifacts verified and native CLI plus real DSH dogfood passed. Included in correction PR #2897 to dev.", + "author_kind": "codex", + "author": "cli", + "created_at": 1788817997180 }, { - "id": 92, - "task_id": "t_msp6a25sef5b7e40", + "id": 168, + "task_id": "t_mtrr50zm1be98f68", + "kind": "release", + "note": "completed", + "author_kind": "codex", + "author": "cli", + "created_at": 1788817997405 + }, + { + "id": 169, + "task_id": "t_mtrrebyk5f21272f", "kind": "comment", - "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): the adapter smoke matrix exercises live-herdr-session turns instead of one-shot headless launches. Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", - "author_kind": "prime-agent", - "author": "dream-scc-reversals", - "created_at": 1786657273022 + "note": "Accepted: five validated remote review gaps corrected in eeafaeace; independent SHIP, 2042 pass/1 skip/0 fail aggregate, four .101 artifact verification and native/DSH dogfood passed. Correction PR #2897 targets dev; promotion #2896 and post-main stable gates remain open.", + "author_kind": "codex", + "author": "cli", + "created_at": 1788817997619 + }, + { + "id": 170, + "task_id": "t_mtrrebyk5f21272f", + "kind": "release", + "note": "completed", + "author_kind": "codex", + "author": "cli", + "created_at": 1788817997949 } ], "wish_groups": [], From 6f857410a6d37f48cd6b4ad54dd7adbd3c4411b5 Mon Sep 17 00:00:00 2001 From: Felipe Rosa <felipe@namastex.ai> Date: Tue, 8 Sep 2026 19:44:39 +0000 Subject: [PATCH 23/24] chore(roadmap): retire stale planning cards Remove 21 ready cards whose own records say the plan is dead, and mark the shipped skills-everywhere umbrella card done. Done cards stay as history. - khal-native-theme (7): RELOCATED 2026-07-23 to khal-os/genie-desktop; the cards were blocked "do not checkout here" and the WISH no longer lives here - genie-ui-bridge design card (1): wish RETIRED 2026-08-30 (#2834) - live-dev-loop umbrella card (1): executed on the desktop fork 2026-07-22; the Genie UI track is retired - genie-official-roadmap (4): July jar-triage plan never executed (no archive dir, no Shipped section); superseded by the Orca/DSH/daily-release direction - roadmap-truth post-release oracle card (1): wish SHIPPED 2026-08-07, the follow-up was never run and is not tracked in INDEX.md - delegate-bridge (7): genie-side agent-turn bridge, blocked on remotty since 2026-08-13; superseded by the v6 direction where Orca owns dispatch The previous revision of .genie/roadmap.json holds every removed card; restore with `git show <prev>:.genie/roadmap.json > .genie/roadmap.json` followed by `genie task import --replace`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012RU32tvczV84Ys9kHVtEQD --- .genie/roadmap.json | 586 +------------------------------------------- 1 file changed, 11 insertions(+), 575 deletions(-) diff --git a/.genie/roadmap.json b/.genie/roadmap.json index d93ce1950..751122801 100644 --- a/.genie/roadmap.json +++ b/.genie/roadmap.json @@ -200,26 +200,6 @@ "blocked_reason": null, "block_kind": null }, - { - "id": "t_mruzpyww3e2a8b8b", - "board_id": null, - "title": "genie-ui-bridge — stdio bridge design (Ready)", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": null, - "group_name": null, - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1784658716672, - "updated_at": 1784658716672, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null - }, { "id": "t_mrv087jca37acdd9", "board_id": null, @@ -260,26 +240,6 @@ "blocked_reason": null, "block_kind": null }, - { - "id": "t_mrveq6oe95cb9141", - "board_id": null, - "title": "live-dev-loop — dual-channel iterate environment (Ready)", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": null, - "group_name": null, - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1784683920978, - "updated_at": 1784683920978, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null - }, { "id": "t_mrvf2p7za43e792d", "board_id": null, @@ -340,226 +300,6 @@ "blocked_reason": null, "block_kind": null }, - { - "id": "t_mrxqzo6s8fbb27ec", - "board_id": null, - "title": "khal-native-theme — KHAL NATIVE re-skin brainstorm", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": null, - "group_name": null, - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1784825451316, - "updated_at": 1784827974072, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": "claude-code", - "blocked_reason": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "block_kind": null - }, - { - "id": "t_mrxr9wzrda10d35b", - "board_id": null, - "title": "Registry + @khal-os/ui dependency + import smoke test", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "khal-native-theme", - "group_name": "group-1", - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1784825929289, - "updated_at": 1784827974160, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": "claude-code", - "blocked_reason": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "block_kind": null - }, - { - "id": "t_mrxr9x1pbce3a16b", - "board_id": null, - "title": "Token bridge + dark-only consolidation", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "khal-native-theme", - "group_name": "group-2", - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1784825929357, - "updated_at": 1784827974238, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": "claude-code", - "blocked_reason": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "block_kind": null - }, - { - "id": "t_mrxr9x49eb19d0ad", - "board_id": null, - "title": "Typography (bundled Geist) + motion system", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "khal-native-theme", - "group_name": "group-3", - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1784825929449, - "updated_at": 1784827974322, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": "claude-code", - "blocked_reason": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "block_kind": null - }, - { - "id": "t_mrxr9x6sb3dfb668", - "board_id": null, - "title": "Element restyle sweep + status colors", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "khal-native-theme", - "group_name": "group-4", - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1784825929540, - "updated_at": 1784827974396, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": "claude-code", - "blocked_reason": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "block_kind": null - }, - { - "id": "t_mrxr9x8t711ce43e", - "board_id": null, - "title": "K dot-matrix loader + KHAL terminal palette", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "khal-native-theme", - "group_name": "group-5", - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1784825929613, - "updated_at": 1784827974462, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": "claude-code", - "blocked_reason": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "block_kind": null - }, - { - "id": "t_mrxr9xbb010c4f81", - "board_id": null, - "title": "QA evidence pack + Felipe live QA handoff", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "khal-native-theme", - "group_name": "group-6", - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1784825929703, - "updated_at": 1784827974556, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": "claude-code", - "blocked_reason": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "block_kind": null - }, - { - "id": "t_ms58w0yqa195fbf8", - "board_id": null, - "title": "genie-official-roadmap — triage 36 wishes into the canonical board", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "genie-official-roadmap", - "group_name": null, - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1785278777572, - "updated_at": 1785278777572, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null - }, - { - "id": "t_ms58y58z0b45759f", - "board_id": null, - "title": "Archive move + link rewrites", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "genie-official-roadmap", - "group_name": "group-1", - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1785278876435, - "updated_at": 1785278876435, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null - }, - { - "id": "t_ms58y5bc8853df95", - "board_id": null, - "title": "Board seed — 19 lifecycle cards", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "genie-official-roadmap", - "group_name": "group-2", - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1785278876520, - "updated_at": 1785278876520, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null - }, - { - "id": "t_ms58y5e19f6e9757", - "board_id": null, - "title": "INDEX rewrite + polish brainstorm + final gates", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "genie-official-roadmap", - "group_name": "group-3", - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1785278876617, - "updated_at": 1785278876617, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null - }, { "id": "t_mscebl889535b59e", "board_id": null, @@ -660,26 +400,6 @@ "blocked_reason": null, "block_kind": null }, - { - "id": "t_msi2nv2gcf9c97af", - "board_id": null, - "title": "roadmap-truth post-release: run Group 1 live oracles (lane-divergence comparison + zz-sync-probe walk) against the shipped binary", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "roadmap-truth", - "group_name": null, - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1786054379272, - "updated_at": 1786054379272, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null - }, { "id": "t_msiyp3ua4e038e63", "board_id": null, @@ -920,26 +640,6 @@ "blocked_reason": null, "block_kind": null }, - { - "id": "t_msp4fp6ce714c8be", - "board_id": null, - "title": "Delegate bridge — routed cards become agent turns (W2+W3)", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "delegate-bridge", - "group_name": null, - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1786480620852, - "updated_at": 1786657262650, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": "dream-scc-reversals", - "blocked_reason": "re-targeted by spawn-context-contract group reversals (2026-08-13): gating moves from headless-turn-open to herdr-swap (0.3) + engine-turn-verbs (remotty board card t_msqkkjt221792465); S-F turn transport re-bases from one-shot headless launches to live herdr sessions (agent start -> prompt --wait --until -> read -> report/release)", - "block_kind": "work" - }, { "id": "t_msp6a1qlaaa8acdc", "board_id": null, @@ -960,126 +660,6 @@ "blocked_reason": null, "block_kind": null }, - { - "id": "t_msp6a1sz195f0ea9", - "board_id": null, - "title": "delegate-bridge S: seam spike (gate for B-F)", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "delegate-bridge", - "group_name": "group-s", - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1786483716515, - "updated_at": 1786483716515, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null - }, - { - "id": "t_msp6a1vd3c103d09", - "board_id": null, - "title": "delegate-bridge B: adapter cards + brief writer", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "delegate-bridge", - "group_name": "group-b", - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1786483716601, - "updated_at": 1786483716601, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null - }, - { - "id": "t_msp6a1xzca62d364", - "board_id": null, - "title": "delegate-bridge C: bridge core — task delegate", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "delegate-bridge", - "group_name": "group-c", - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1786483716695, - "updated_at": 1786483716695, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null - }, - { - "id": "t_msp6a20k0a1df8a8", - "board_id": null, - "title": "delegate-bridge D: degradation + doctor orphan check", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "delegate-bridge", - "group_name": "group-d", - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1786483716788, - "updated_at": 1786483716788, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null - }, - { - "id": "t_msp6a23b3c681369", - "board_id": null, - "title": "delegate-bridge E: task fan + ratification", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "delegate-bridge", - "group_name": "group-e", - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1786483716887, - "updated_at": 1786483716887, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null - }, - { - "id": "t_msp6a25sef5b7e40", - "board_id": null, - "title": "delegate-bridge F: adapter smoke matrix ×5", - "status": "ready", - "claimed_by": null, - "claimed_at": null, - "wish": "delegate-bridge", - "group_name": "group-f", - "assigned_agent": null, - "assigned_reason": null, - "created_at": 1786483716976, - "updated_at": 1786483716976, - "lane": null, - "agent_kind": null, - "heartbeat_at": null, - "blocked_by": null, - "blocked_reason": null, - "block_kind": null - }, { "id": "t_mtdb642cdb6f9f10", "board_id": null, @@ -1124,7 +704,7 @@ "id": "t_mtfzsu4ee1390c48", "board_id": null, "title": "codex-skill-installer: skills via skills.sh, retire Codex+Claude plugin delivery", - "status": "ready", + "status": "done", "claimed_by": null, "claimed_at": null, "wish": "skills-everywhere", @@ -1132,7 +712,7 @@ "assigned_agent": null, "assigned_reason": null, "created_at": 1788105422462, - "updated_at": 1788108622793, + "updated_at": 1788896649130, "lane": null, "agent_kind": null, "heartbeat_at": null, @@ -1704,69 +1284,6 @@ "task_dependencies": [], "stage_log": [], "task_events": [ - { - "id": 1, - "task_id": "t_mrxqzo6s8fbb27ec", - "kind": "block", - "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "author_kind": "claude-code", - "author": null, - "created_at": 1784827974073 - }, - { - "id": 2, - "task_id": "t_mrxr9wzrda10d35b", - "kind": "block", - "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "author_kind": "claude-code", - "author": null, - "created_at": 1784827974160 - }, - { - "id": 3, - "task_id": "t_mrxr9x1pbce3a16b", - "kind": "block", - "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "author_kind": "claude-code", - "author": null, - "created_at": 1784827974238 - }, - { - "id": 4, - "task_id": "t_mrxr9x49eb19d0ad", - "kind": "block", - "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "author_kind": "claude-code", - "author": null, - "created_at": 1784827974322 - }, - { - "id": 5, - "task_id": "t_mrxr9x6sb3dfb668", - "kind": "block", - "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "author_kind": "claude-code", - "author": null, - "created_at": 1784827974396 - }, - { - "id": 6, - "task_id": "t_mrxr9x8t711ce43e", - "kind": "block", - "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "author_kind": "claude-code", - "author": null, - "created_at": 1784827974462 - }, - { - "id": 7, - "task_id": "t_mrxr9xbb010c4f81", - "kind": "block", - "note": "RELOCATED 2026-07-23: khal-native-theme ledger + tasks live in khal-os/genie-desktop .genie (Felipe: desktop context in the desktop repo). Do not checkout here.", - "author_kind": "claude-code", - "author": null, - "created_at": 1784827974557 - }, { "id": 38, "task_id": "t_mscebl889535b59e", @@ -2037,24 +1554,6 @@ "author": "cli", "created_at": 1786462438505 }, - { - "id": 76, - "task_id": "t_msp4fp6ce714c8be", - "kind": "wish", - "note": "(none)→delegate-bridge", - "author_kind": "claude-code", - "author": "cli", - "created_at": 1786480645159 - }, - { - "id": 77, - "task_id": "t_msp4fp6ce714c8be", - "kind": "block", - "note": "blocked on remotty headless-turn-open (cross-repo; design M2 mechanism)", - "author_kind": "claude-code", - "author": "cli", - "created_at": 1786480645240 - }, { "id": 78, "task_id": "t_msp6a1qlaaa8acdc", @@ -2118,78 +1617,6 @@ "author": "cli", "created_at": 1786628093499 }, - { - "id": 85, - "task_id": "t_msp4fp6ce714c8be", - "kind": "block", - "note": "re-targeted by spawn-context-contract group reversals (2026-08-13): gating moves from headless-turn-open to herdr-swap (0.3) + engine-turn-verbs (remotty board card t_msqkkjt221792465); S-F turn transport re-bases from one-shot headless launches to live herdr sessions (agent start -> prompt --wait --until -> read -> report/release)", - "author_kind": "prime-agent", - "author": "dream-scc-reversals", - "created_at": 1786657262650 - }, - { - "id": 86, - "task_id": "t_msp4fp6ce714c8be", - "kind": "comment", - "note": "Board-edge re-target recorded (spawn-context-contract group reversals, 2026-08-13): the enforced block on this card was re-placed via task block, never unblocked, so the live gating now reads herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) instead of headless-turn-open. Groups S-F re-base their turn transport to live herdr sessions; group A (done, W1 scope) is unaffected.", - "author_kind": "prime-agent", - "author": "dream-scc-reversals", - "created_at": 1786657272200 - }, - { - "id": 87, - "task_id": "t_msp6a1sz195f0ea9", - "kind": "comment", - "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): the seam spike now proves the live-herdr turn seam (agent start / prompt --wait --until / read) instead of the one-shot headless-launch seam. Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", - "author_kind": "prime-agent", - "author": "dream-scc-reversals", - "created_at": 1786657272357 - }, - { - "id": 88, - "task_id": "t_msp6a1vd3c103d09", - "kind": "comment", - "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): adapter cards + brief writer target live herdr sessions (id-free continue form via herdr agent verbs) instead of turn-per-launch headless argv. Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", - "author_kind": "prime-agent", - "author": "dream-scc-reversals", - "created_at": 1786657272489 - }, - { - "id": 89, - "task_id": "t_msp6a1xzca62d364", - "kind": "comment", - "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): bridge core drives live herdr sessions per turn (agent prompt --wait --until + read hand-back) instead of one-shot headless turn launches. Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", - "author_kind": "prime-agent", - "author": "dream-scc-reversals", - "created_at": 1786657272621 - }, - { - "id": 90, - "task_id": "t_msp6a20k0a1df8a8", - "kind": "comment", - "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): degradation paths now cover herdr session failures and release (not detached-launch refusals). Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", - "author_kind": "prime-agent", - "author": "dream-scc-reversals", - "created_at": 1786657272747 - }, - { - "id": 91, - "task_id": "t_msp6a23b3c681369", - "kind": "comment", - "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): task fan + ratification transport re-bases to live herdr sessions (per-turn prompt --wait --until / read) instead of one-shot headless launches. Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", - "author_kind": "prime-agent", - "author": "dream-scc-reversals", - "created_at": 1786657272871 - }, - { - "id": 92, - "task_id": "t_msp6a25sef5b7e40", - "kind": "comment", - "note": "Gating re-target recorded (spawn-context-contract group reversals, 2026-08-13): the adapter smoke matrix exercises live-herdr-session turns instead of one-shot headless launches. Gating: herdr-swap (0.3) + engine-turn-verbs (remotty card t_msqkkjt221792465) - formerly headless-turn-open.", - "author_kind": "prime-agent", - "author": "dream-scc-reversals", - "created_at": 1786657273022 - }, { "id": 93, "task_id": "t_mtdns9r623748cfd", @@ -2891,6 +2318,15 @@ "author_kind": "codex", "author": "cli", "created_at": 1788817997949 + }, + { + "id": 171, + "task_id": "t_mtfzsu4ee1390c48", + "kind": "release", + "note": "completed", + "author_kind": "claude-code", + "author": "cli", + "created_at": 1788896649131 } ], "wish_groups": [], From 2175769b018e3ace8efa76b03001fd2feca438cf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 20:11:53 +0000 Subject: [PATCH 24/24] chore(version): bump to 5.260909.1 [auto-version] --- package.json | 2 +- plugins/genie/orca-plugin.json | 2 +- plugins/genie/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 5236bebe3..b16966104 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@automagik/genie", - "version": "5.260907.2", + "version": "5.260909.1", "description": "Collaborative terminal toolkit for human + AI workflows. NOTE: npm distribution discontinued 2026-05-09 \u2014 install via `curl -fsSL https://raw.githubusercontent.com/automagik-dev/genie/main/install.sh | bash` (cosign + SLSA verified). See https://automagik.dev/genie/release-process", "license": "MIT", "type": "module", diff --git a/plugins/genie/orca-plugin.json b/plugins/genie/orca-plugin.json index 71f939a4c..f77e4d17c 100644 --- a/plugins/genie/orca-plugin.json +++ b/plugins/genie/orca-plugin.json @@ -3,7 +3,7 @@ "id": "genie", "publisher": "automagik", "name": "Genie", - "version": "5.260907.2", + "version": "5.260909.1", "description": "Genie workflows backed by Orca as the sole lifecycle authority.", "author": { "name": "Namastex Labs", diff --git a/plugins/genie/package.json b/plugins/genie/package.json index 379cd851c..6f0d073a9 100644 --- a/plugins/genie/package.json +++ b/plugins/genie/package.json @@ -1,6 +1,6 @@ { "name": "genie-plugin", - "version": "5.260907.2", + "version": "5.260909.1", "private": true, "description": "Runtime dependencies for genie bundled CLIs", "license": "MIT",