From e09b06660e30160627ccaebe38b5d73ea5204449 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Tue, 28 Jul 2026 18:21:22 +0800 Subject: [PATCH 01/22] feat(skills): add proportional-validation-policy (#2724) Apply risk-proportional validation across target repositories while preserving aggregate integration and release gates. --- .../proportional-validation-policy/WISH.md | 201 ++++++++++++++++++ plugins/genie/skills/review/SKILL.md | 17 +- plugins/genie/skills/wish/SKILL.md | 12 +- plugins/genie/skills/work/SKILL.md | 12 +- skills/review/SKILL.md | 17 +- skills/wish/SKILL.md | 12 +- skills/work/SKILL.md | 12 +- 7 files changed, 269 insertions(+), 14 deletions(-) create mode 100644 .genie/wishes/proportional-validation-policy/WISH.md diff --git a/.genie/wishes/proportional-validation-policy/WISH.md b/.genie/wishes/proportional-validation-policy/WISH.md new file mode 100644 index 000000000..da2e88efc --- /dev/null +++ b/.genie/wishes/proportional-validation-policy/WISH.md @@ -0,0 +1,201 @@ +# Wish: Proportional validation policy + +| Field | Value | +|-------|-------| +| **Status** | IN_PROGRESS | +| **Slug** | `proportional-validation-policy` | +| **Date** | 2026-07-28 | +| **Author** | Codex with user direction | +| **Appetite** | small | +| **Branch** | `policy/proportional-validation` | +| **Repos touched** | `automagik-dev/genie` | +| **Issue** | [#2724](https://github.com/automagik-dev/genie/issues/2724) | +| **Design** | _No brainstorm — direct wish_ | + +## Summary + +When Genie plans, executes, or reviews work in any target repository, it will require validation effort to match the +risk and reach of the actual diff. A documentation-only change such as a README edit will use focused documentation +checks, while cross-cutting runtime, dependency, generated +executable/runtime artifact, CI, integration, and release changes will still escalate to the aggregate gate. +Deterministic generated documentation and plugin mirrors may instead use focused generator, parity, and content checks. + +## Scope + +### IN + +- Make the shipped, runtime-neutral `wish`, `work`, and `review` workflows select, execute, and assess the smallest + sufficient validation command in whichever target repository invokes Genie, with explicit triggers for widening. +- Regenerate the committed Codex plugin skill mirror so the shipped workflow matches the canonical skills. + +### OUT + +- Automatic changed-file classification, a validation-command generator, caches, and new CI jobs. +- Modifying target repositories' `AGENTS.md`, CI configuration, package scripts, or test commands. +- Weakening a target repository's aggregate CI, integration, release, or pre-merge gates. +- Retrofitting historical wishes whose validation commands were correct under their original policy. + +## Decisions + +| # | Decision | Rationale | +|---|----------|-----------| +| 1 | Proportional means risk-and-reach based, not file-extension based | A README can contain generated or tested contracts, while a small source diff can affect every command. | +| 2 | Every change gets targeted validation; widening is trigger-based | This removes waste without turning “proportional” into permission to skip evidence. | +| 3 | Keep the aggregate gate at integration/release boundaries | A focused local loop and a final system gate serve different purposes. | +| 4 | Change shipped workflow policy surfaces only | The workflow already carries each target repo's per-group validation commands; no selector engine or target-repo edit is required. | + +## Simplicity Case + +- **Simplest complete design:** Rewrite the portable workflow guidance so Genie chooses the narrowest target-repository + checks that directly exercise the changed contract, then widens only for named risk triggers. +- **Added machinery:** None. Existing validation-command fields and review evidence carry the policy. +- **Deferred until measured:** Automate selection only after repeated review evidence shows humans or agents choose + insufficient checks; add a classifier only when at least three concrete misses share a deterministic rule. +- **Complexity removed:** No path matrix, config surface, cache, changed-file parser, or second CI workflow. + +## Dependencies + +**depends-on:** none +**blocks:** none + +## Success Criteria + +- [x] Shipped `wish`, `work`, and `review` consistently require the smallest sufficient target-repository validation + and the same widening triggers. +- [x] The policy never permits zero validation and preserves aggregate validation for integration/release risk. +- [x] Canonical and plugin-mirrored skills remain byte-identical. + +## Execution Strategy + +### Wave 1 (sequential) + +| Group | Agent | Complexity | Model | Description | +|-------|-------|------------|-------|-------------| +| 1 | engineer | 1 — prompt-skill change | engineer-trivial / low | Update and validate the proportional policy surfaces. | + +Complexity scoring rubric: score each group independently and record the total plus a short rationale in **Complexity**. Add: + +- **+2** each for orchestration / agent-lifecycle / routing; cost / model / escalation; stateful work; subjective acceptance. +- **+1** each for multi-package work; OTel-label dependency; no deterministic test; prior rework; prompt-skill change; CI / release work. + +Route the total in **Model** by portable role and reasoning effort: **0–1** → +`engineer-trivial` / low; **2–3** → `engineer-standard` / medium or high; +**4–6** → `engineer-complex` / high; **7+** → `engineer-complex` plus an +independent `final-gate` at the highest justified effort. Codex maps these to +the `genie_*` profiles; other runtimes use their matching native roles. Keep +model and effort in runtime session/agent configuration, never skill frontmatter. + +## Execution Groups + +### Group 1: Make validation proportional + +**Goal:** Make Genie's cross-repository per-group validation requirements scale with the target diff's risk and reach. + +**Deliverables:** +1. Update `skills/wish/SKILL.md`, `skills/work/SKILL.md`, and `skills/review/SKILL.md` so plans, execution, and reviews + apply the same rule. +2. Regenerate `plugins/genie/skills/` from the canonical skill tree. + +**Acceptance Criteria:** +- [x] Documentation-only changes use relevant formatting, link, or content-contract checks rather than `bun run check` + by default. +- [x] Runtime changes name focused tests for changed behavior and add type/lint/build checks only when the diff reaches + those contracts. +- [x] Shared runtime/core behavior, dependency/lockfile, generated executable/runtime artifact, config/schema, + CI/release, broad refactor, or uncertain impact explicitly widen to `bun run check` and any affected build/e2e gate; + deterministic documentation/plugin mirrors may use focused generator, parity, and content checks. +- [x] Review rejects both under-validation and unexplained full-suite validation. +- [x] Canonical and mirrored skills pass their focused structural/parity checks. + +**Validation:** +```bash +bun run wishes:lint +bun run skills:lint +bun scripts/sync-plugin-skills.ts --check +bun test scripts/sync-plugin-skills.test.ts +``` + +**depends-on:** none + +--- + +## QA Criteria + +_What must be verified on dev after merge. The QA agent tests each criterion._ + +- [ ] A README-only example clearly selects focused documentation checks without the full suite. +- [ ] A shared runtime or release example clearly selects the aggregate gate. +- [ ] The installed plugin payload carries the same policy as the source skills. + +--- + +## Assumptions / Risks + +| Risk | Severity | Mitigation | +|------|----------|------------| +| “Proportional” becomes subjective under-testing | Medium | Name a minimum evidence rule and explicit widening triggers. | +| Aggregate checks are removed from true integration boundaries | Medium | State that CI, release, and cross-cutting gates remain mandatory. | +| Canonical/plugin policy drift | Low | Regenerate the mirror and run the parity check. | + +--- + +## Review Results + +### Plan review — 2026-07-28T09:58:10Z + +- **Context:** Plan review before execution +- **Target SHA-256:** `f72a6b3dd11763a870ca75a49b39622e84e3657a014fcd82f89f4ee9888a8f57` +- **Base HEAD:** `614e472c43977c3c122f1ea0410b3338f2a8f063` +- **Verdict:** **SHIP** +- **Validation:** `BUN_RUNTIME_TRANSPILER_CACHE_PATH=0 bun run wishes:lint` passed (67 files, zero broken + brainstorm links). +- **Gaps:** No CRITICAL, HIGH, MEDIUM, or LOW gaps. +- **Finding:** The actual problem is blanket validation, not testing itself. The plan preserves targeted evidence, + forbids zero validation, retains aggregate integration/release gates, and defers automation until repeated + deterministic misses justify it. + +### Plan re-review — 2026-07-28T10:10:33Z + +- **Target SHA-256:** `ddc3793965ce3581a22a9f0e2e8410a3f9246df69cc8a1aee92e372ce402def2` +- **Verdict:** **SHIP** +- **Validation:** `BUN_RUNTIME_TRANSPILER_CACHE_PATH=0 bun run wishes:lint` passed (67 files, zero broken + brainstorm links). +- **Gaps:** No CRITICAL, HIGH, MEDIUM, or LOW gaps. +- **Finding:** The amended criterion consistently distinguishes generated executable/runtime artifacts from + deterministic documentation/plugin mirrors and preserves the aggregate-gate boundary without adding machinery. + +### Group 1 execution review — 2026-07-28 + +- **Verdict:** **SHIP** after fix loop 1. +- **Validation:** `bun run wishes:lint`, `bun run skills:lint`, `bun scripts/sync-plugin-skills.ts --check`, + `bun test scripts/sync-plugin-skills.test.ts` (8 pass, 0 fail), and `git diff --check` all passed. +- **Gaps:** None. Manual semantic enforcement is the explicitly accepted simplicity decision; automation remains + deferred until three concrete misses share a deterministic rule. + +### Group 1 quality review — 2026-07-28 + +- **Initial verdict:** **FIX-FIRST** because “generated artifact” and “shared core” were too broad. +- **Fix loop 1:** Distinguished deterministic documentation/plugin mirrors from generated executable/runtime artifacts + and narrowed the shared trigger to shared runtime/core behavior. +- **Final verdict:** **SHIP**. No CRITICAL, HIGH, MEDIUM, or LOW gaps remain. + +### Scope clarification review — 2026-07-28 + +- **Verdict:** **SHIP**. +- **Finding:** The control plane is the shipped runtime-neutral `wish`, `work`, and `review` skills plus their plugin + mirrors. Target repositories retain ownership of their `AGENTS.md`, CI, scripts, and test commands. +- **Gaps:** No CRITICAL, HIGH, MEDIUM, or LOW gaps. + +--- + +## Files to Create/Modify + +``` +.genie/wishes/proportional-validation-policy/WISH.md +skills/wish/SKILL.md +skills/work/SKILL.md +skills/review/SKILL.md +plugins/genie/skills/wish/SKILL.md +plugins/genie/skills/work/SKILL.md +plugins/genie/skills/review/SKILL.md +``` diff --git a/plugins/genie/skills/review/SKILL.md b/plugins/genie/skills/review/SKILL.md index 60a2f64fb..b628c23f6 100644 --- a/plugins/genie/skills/review/SKILL.md +++ b/plugins/genie/skills/review/SKILL.md @@ -23,7 +23,14 @@ When spawned as a reviewer subagent, your dispatch prompt carries the curated sc 1. **Detect target** — DESIGN.md, wish draft, completed work, or PR diff. 2. **Select pipeline** — the matching checklist below. 3. **Run checklist** — evaluate each criterion, collecting evidence. -4. **Run validations** — execute validation commands; capture pass/fail output. +4. **Run validations** — execute validation commands; capture pass/fail output and confirm their scope is proportional + to the diff's risk and reach. Documentation-only changes, including deterministic generated documentation or plugin + skill mirrors, use relevant format, link, example, generator, parity, or content-contract checks; runtime changes use + focused behavior tests and add type, lint, or build checks for boundaries reached. Shared runtime/core behavior, + dependency or lockfile, generated executable or runtime artifact, configuration or schema, CI or release, + broad-refactor, or uncertain-impact changes require the repository full gate plus affected build or end-to-end checks. + Reject zero validation, under-validation, and unexplained full-suite validation. Preserve required aggregate + integration and release gates. 5. **Tag gaps** — classify every unmet criterion by severity. 6. **Return verdict** — SHIP, FIX-FIRST, or BLOCKED, with exact fixes (files, commands, what to change) for each gap. @@ -64,12 +71,14 @@ If an ordinary reviewer and the `final-gate` disagree, log an appeal with the wi - [ ] Every task has testable acceptance criteria - [ ] Tasks are bite-sized and independently shippable - [ ] Dependencies tagged (`depends-on` / `blocks`) -- [ ] Validation commands exist for each execution group +- [ ] Every group has non-zero validation proportional to its planned risk and reach, with escalation and scope rationale +- [ ] Aggregate integration and release gates remain present where the repository requires them - [ ] Simplicity Case is executable: no group builds machinery marked deferred, and every stateful mechanism maps to a current success criterion ### Execution Review (after `work`) - [ ] All acceptance criteria met with evidence -- [ ] Validation commands run and passing +- [ ] Validation commands run and passing; their recorded scope matches the actual diff's risk and reach +- [ ] No under-validation, zero validation, or unexplained full-suite validation - [ ] No scope creep — only wish-scoped changes - [ ] Work is auditable — commands and outcomes captured - [ ] Quality pass: security, maintainability, correctness @@ -80,7 +89,7 @@ If an ordinary reviewer and the `final-gate` disagree, log an appeal with the wi - [ ] Diff matches wish scope — no unrelated changes - [ ] File list matches wish's "Files to Create/Modify" - [ ] No secrets, credentials, or hardcoded tokens in diff -- [ ] Tests pass (if applicable) +- [ ] Risk-proportional validation passes, and required aggregate gates remain intact - [ ] Commit messages reference wish slug ## Severity & Verdicts diff --git a/plugins/genie/skills/wish/SKILL.md b/plugins/genie/skills/wish/SKILL.md index 5d6fed057..fc6293839 100644 --- a/plugins/genie/skills/wish/SKILL.md +++ b/plugins/genie/skills/wish/SKILL.md @@ -57,7 +57,15 @@ node "/references/design-review-evidence.mjs" verify ".genie/bra The template ships inside this skill as the single source of truth for wish structure — a plain document, no runtime scaffolder. Copying guarantees the skeleton the parser and linter expect; ad-hoc wishes regularly fail structural lint. -7. **Fill:** replace the `{{slug}}`/`{{date}}` tokens and every `` marker with real content. Every group gets acceptance criteria plus a validation command. +7. **Fill:** replace the `{{slug}}`/`{{date}}` tokens and every `` marker with real content. Every group gets + acceptance criteria plus a non-zero validation command proportional to the planned diff's risk and reach. Start with + the narrowest checks that can disprove the changed behavior or contract: documentation-only groups, including + deterministic generated documentation or plugin skill mirrors, use relevant format, link, example, generator, parity, + or content-contract checks; runtime groups use focused behavior tests and add type, lint, or build checks only for + boundaries they reach. Escalate shared runtime/core behavior, dependency or lockfile, generated executable or runtime + artifact, configuration or schema, CI or release, broad-refactor, or uncertain-impact groups to the repository full + gate plus affected build or end-to-end checks. State why the command scope fits; do not prescribe an unexplained full + suite. Preserve any repository-defined aggregate integration or release gate separately from per-group validation. 8. **Declare dependencies:** use the wish-level `## Dependencies` keys `**depends-on:** ` and `**blocks:** ` for cross-wish edges. Keep @@ -96,5 +104,7 @@ node "/references/design-review-evidence.mjs" verify ".genie/bra - No implementation during `wish` — planning only. - No speculative optimization: caches, deltas, sharding, background coordination, and configuration surfaces require a current criterion or measurement in the Simplicity Case. - Every group testable, bite-sized, and independently shippable; no vague tasks ("improve everything"). +- Every group has non-zero, risk-proportional validation with its scope explained; aggregate integration and release + gates remain intact. - OUT scope must contain at least one concrete exclusion. - Declare cross-wish dependencies early. diff --git a/plugins/genie/skills/work/SKILL.md b/plugins/genie/skills/work/SKILL.md index 90aa85038..b1ec698d4 100644 --- a/plugins/genie/skills/work/SKILL.md +++ b/plugins/genie/skills/work/SKILL.md @@ -28,7 +28,14 @@ When you are spawned as a subagent for a group, your dispatch prompt carries the 4. **Await completion — never poll:** background subagents notify you when they finish. Inspect `genie board --wish ` 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. -7. **Validate:** run the group's validation command yourself (Bash); record the output as evidence. +7. **Validate:** run the group's validation command yourself (Bash); 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 + relevant format, link, example, generator, parity, or content-contract checks; runtime changes use focused behavior + tests and add type, lint, or build checks for boundaries reached. Shared runtime/core behavior, dependency or lockfile, + generated executable or runtime artifact, configuration or schema, CI or release, broad-refactor, or uncertain-impact + changes require the repository full gate plus affected build or end-to-end checks. Validation may never be zero, and + an unexplained full-suite run is not acceptable evidence. Preserve required aggregate integration and release gates. 8. **Group done** — only after clean review AND passing validation: ```bash genie task done @@ -115,7 +122,8 @@ A user-approved simplification invalidates the superseded plan/review evidence a ## Rules - Never execute group work directly — always dispatch via the native delegation surface. -- Never expand scope during execution; never skip validation commands. +- Never expand scope during execution; never skip validation commands or substitute unexplained over-validation for a + risk-based choice. - Never spend fix loops preserving optional complexity; route an `overdesigned-plan` diagnosis back through wish/design review. - Never overwrite WISH.md from subagent output — curated prompts are runtime context; the WISH.md in git is the source of truth. - Reviewer ≠ engineer, always. diff --git a/skills/review/SKILL.md b/skills/review/SKILL.md index 60a2f64fb..b628c23f6 100644 --- a/skills/review/SKILL.md +++ b/skills/review/SKILL.md @@ -23,7 +23,14 @@ When spawned as a reviewer subagent, your dispatch prompt carries the curated sc 1. **Detect target** — DESIGN.md, wish draft, completed work, or PR diff. 2. **Select pipeline** — the matching checklist below. 3. **Run checklist** — evaluate each criterion, collecting evidence. -4. **Run validations** — execute validation commands; capture pass/fail output. +4. **Run validations** — execute validation commands; capture pass/fail output and confirm their scope is proportional + to the diff's risk and reach. Documentation-only changes, including deterministic generated documentation or plugin + skill mirrors, use relevant format, link, example, generator, parity, or content-contract checks; runtime changes use + focused behavior tests and add type, lint, or build checks for boundaries reached. Shared runtime/core behavior, + dependency or lockfile, generated executable or runtime artifact, configuration or schema, CI or release, + broad-refactor, or uncertain-impact changes require the repository full gate plus affected build or end-to-end checks. + Reject zero validation, under-validation, and unexplained full-suite validation. Preserve required aggregate + integration and release gates. 5. **Tag gaps** — classify every unmet criterion by severity. 6. **Return verdict** — SHIP, FIX-FIRST, or BLOCKED, with exact fixes (files, commands, what to change) for each gap. @@ -64,12 +71,14 @@ If an ordinary reviewer and the `final-gate` disagree, log an appeal with the wi - [ ] Every task has testable acceptance criteria - [ ] Tasks are bite-sized and independently shippable - [ ] Dependencies tagged (`depends-on` / `blocks`) -- [ ] Validation commands exist for each execution group +- [ ] Every group has non-zero validation proportional to its planned risk and reach, with escalation and scope rationale +- [ ] Aggregate integration and release gates remain present where the repository requires them - [ ] Simplicity Case is executable: no group builds machinery marked deferred, and every stateful mechanism maps to a current success criterion ### Execution Review (after `work`) - [ ] All acceptance criteria met with evidence -- [ ] Validation commands run and passing +- [ ] Validation commands run and passing; their recorded scope matches the actual diff's risk and reach +- [ ] No under-validation, zero validation, or unexplained full-suite validation - [ ] No scope creep — only wish-scoped changes - [ ] Work is auditable — commands and outcomes captured - [ ] Quality pass: security, maintainability, correctness @@ -80,7 +89,7 @@ If an ordinary reviewer and the `final-gate` disagree, log an appeal with the wi - [ ] Diff matches wish scope — no unrelated changes - [ ] File list matches wish's "Files to Create/Modify" - [ ] No secrets, credentials, or hardcoded tokens in diff -- [ ] Tests pass (if applicable) +- [ ] Risk-proportional validation passes, and required aggregate gates remain intact - [ ] Commit messages reference wish slug ## Severity & Verdicts diff --git a/skills/wish/SKILL.md b/skills/wish/SKILL.md index 5d6fed057..fc6293839 100644 --- a/skills/wish/SKILL.md +++ b/skills/wish/SKILL.md @@ -57,7 +57,15 @@ node "/references/design-review-evidence.mjs" verify ".genie/bra The template ships inside this skill as the single source of truth for wish structure — a plain document, no runtime scaffolder. Copying guarantees the skeleton the parser and linter expect; ad-hoc wishes regularly fail structural lint. -7. **Fill:** replace the `{{slug}}`/`{{date}}` tokens and every `` marker with real content. Every group gets acceptance criteria plus a validation command. +7. **Fill:** replace the `{{slug}}`/`{{date}}` tokens and every `` marker with real content. Every group gets + acceptance criteria plus a non-zero validation command proportional to the planned diff's risk and reach. Start with + the narrowest checks that can disprove the changed behavior or contract: documentation-only groups, including + deterministic generated documentation or plugin skill mirrors, use relevant format, link, example, generator, parity, + or content-contract checks; runtime groups use focused behavior tests and add type, lint, or build checks only for + boundaries they reach. Escalate shared runtime/core behavior, dependency or lockfile, generated executable or runtime + artifact, configuration or schema, CI or release, broad-refactor, or uncertain-impact groups to the repository full + gate plus affected build or end-to-end checks. State why the command scope fits; do not prescribe an unexplained full + suite. Preserve any repository-defined aggregate integration or release gate separately from per-group validation. 8. **Declare dependencies:** use the wish-level `## Dependencies` keys `**depends-on:** ` and `**blocks:** ` for cross-wish edges. Keep @@ -96,5 +104,7 @@ node "/references/design-review-evidence.mjs" verify ".genie/bra - No implementation during `wish` — planning only. - No speculative optimization: caches, deltas, sharding, background coordination, and configuration surfaces require a current criterion or measurement in the Simplicity Case. - Every group testable, bite-sized, and independently shippable; no vague tasks ("improve everything"). +- Every group has non-zero, risk-proportional validation with its scope explained; aggregate integration and release + gates remain intact. - OUT scope must contain at least one concrete exclusion. - Declare cross-wish dependencies early. diff --git a/skills/work/SKILL.md b/skills/work/SKILL.md index 90aa85038..b1ec698d4 100644 --- a/skills/work/SKILL.md +++ b/skills/work/SKILL.md @@ -28,7 +28,14 @@ When you are spawned as a subagent for a group, your dispatch prompt carries the 4. **Await completion — never poll:** background subagents notify you when they finish. Inspect `genie board --wish ` 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. -7. **Validate:** run the group's validation command yourself (Bash); record the output as evidence. +7. **Validate:** run the group's validation command yourself (Bash); 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 + relevant format, link, example, generator, parity, or content-contract checks; runtime changes use focused behavior + tests and add type, lint, or build checks for boundaries reached. Shared runtime/core behavior, dependency or lockfile, + generated executable or runtime artifact, configuration or schema, CI or release, broad-refactor, or uncertain-impact + changes require the repository full gate plus affected build or end-to-end checks. Validation may never be zero, and + an unexplained full-suite run is not acceptable evidence. Preserve required aggregate integration and release gates. 8. **Group done** — only after clean review AND passing validation: ```bash genie task done @@ -115,7 +122,8 @@ A user-approved simplification invalidates the superseded plan/review evidence a ## Rules - Never execute group work directly — always dispatch via the native delegation surface. -- Never expand scope during execution; never skip validation commands. +- Never expand scope during execution; never skip validation commands or substitute unexplained over-validation for a + risk-based choice. - Never spend fix loops preserving optional complexity; route an `overdesigned-plan` diagnosis back through wish/design review. - Never overwrite WISH.md from subagent output — curated prompts are runtime context; the WISH.md in git is the source of truth. - Reviewer ≠ engineer, always. From d4f6a272405dec50b5916fddb6fe0428d9ac168a Mon Sep 17 00:00:00 2001 From: Felipe Rosa Date: Tue, 28 Jul 2026 16:55:45 -0300 Subject: [PATCH 02/22] feat(task): genie task import + committed roadmap.json board snapshot Cross-machine resume for the roadmap board without committing the binary WAL database: `genie task export --write` snapshots the full genie.db state to git-tracked .genie/roadmap.json, and `genie task import` (--replace to overwrite) losslessly rebuilds genie.db from it on a fresh clone. Import validates snapshot shape and schemaVersion, refuses a non-empty database by default, and preserves row ids exactly. INDEX.md carries the discoverable pointer for agents. Co-Authored-By: Claude Fable 5 --- .genie/INDEX.md | 2 + .genie/roadmap.json | 574 ++++++++++++++++++++++++++++++ CLAUDE.md | 3 + src/lib/v5/genie-db.ts | 10 + src/lib/v5/task-state.ts | 187 ++++++++++ src/term-commands/v5-task.test.ts | 102 +++++- src/term-commands/v5-task.ts | 59 ++- 7 files changed, 931 insertions(+), 6 deletions(-) create mode 100644 .genie/roadmap.json diff --git a/.genie/INDEX.md b/.genie/INDEX.md index 10f7a6d8d..358bc9874 100644 --- a/.genie/INDEX.md +++ b/.genie/INDEX.md @@ -1,5 +1,7 @@ # Plans Index +> **Board snapshot:** the genie.db task/board state (the roadmap behind `genie board`) is committed as [roadmap.json](roadmap.json) so it survives clones and publishes with the repo. On a fresh machine: `genie task import` rebuilds `.genie/genie.db` from it. After board changes: `genie task export --write`, then commit the refreshed snapshot. + ## Raw - [control-plane-contract](brainstorms/control-plane-contract/DRAFT.md) — **re-scoped 2026-07-21:** narrow to dispatch routing + work/review policy tiers; agent-sync (#2541) already solved global↔repo skill convergence orthogonally (umbrella G2+G3) diff --git a/.genie/roadmap.json b/.genie/roadmap.json new file mode 100644 index 000000000..d3056150b --- /dev/null +++ b/.genie/roadmap.json @@ -0,0 +1,574 @@ +{ + "schemaVersion": 1, + "meta": [ + { + "key": "stage_log_backfill_v1", + "value": "1784758341375" + } + ], + "boards": [], + "tasks": [ + { + "id": "t_mrubw9a5949903cf", + "board_id": null, + "title": "G1: shell substrate (fresh port + TerminalMirror salvage + grid/splits)", + "status": "done", + "claimed_by": "engineer-complex", + "claimed_at": 1784623891565, + "wish": "genie-ui", + "group_name": "group-1", + "created_at": 1784618699261, + "updated_at": 1784625442704, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null + }, + { + "id": "t_mrubw9bxa7f28dd8", + "board_id": null, + "title": "G2: genie lane (wish menu + roster + worktree binding)", + "status": "done", + "claimed_by": "engineer-standard", + "claimed_at": 1784626730518, + "wish": "genie-ui", + "group_name": "group-2", + "created_at": 1784618699325, + "updated_at": 1784627382271, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null + }, + { + "id": "t_mrubw9do9fc563c4", + "board_id": null, + "title": "G3: group chat drawer + ACP chat-face backend", + "status": "done", + "claimed_by": "engineer-complex", + "claimed_at": 1784629252019, + "wish": "genie-ui", + "group_name": "group-3", + "created_at": 1784618699388, + "updated_at": 1784629987524, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null + }, + { + "id": "t_mrubw9ft4d20d980", + "board_id": null, + "title": "G4: residue confirm + docs + two-faces contract", + "status": "done", + "claimed_by": "engineer-standard", + "claimed_at": 1784630070055, + "wish": "genie-ui", + "group_name": "group-4", + "created_at": 1784618699465, + "updated_at": 1784630608759, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null + }, + { + "id": "t_mruxc5gvd0c00325", + "board_id": null, + "title": "Base branch + build proof", + "status": "done", + "claimed_by": "dash-g1-engineer", + "claimed_at": 1784660541667, + "wish": "genie-ui-dash", + "group_name": "group-1", + "created_at": 1784654712751, + "updated_at": 1784661266675, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null + }, + { + "id": "t_mruxc5iv51303ef4", + "board_id": null, + "title": "Agent-adapter contract extraction (Claude = reference)", + "status": "done", + "claimed_by": "dash-g2-engineer", + "claimed_at": 1784661307439, + "wish": "genie-ui-dash", + "group_name": "group-2", + "created_at": 1784654712823, + "updated_at": 1784664027865, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null + }, + { + "id": "t_mruxc5l659685893", + "board_id": null, + "title": "Codex, Hermes, rlmx adapters + per-task agent selection", + "status": "done", + "claimed_by": "dash-g3-engineer", + "claimed_at": 1784664068582, + "wish": "genie-ui-dash", + "group_name": "group-3", + "created_at": 1784654712906, + "updated_at": 1784666301881, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null + }, + { + "id": "t_mruxc5mz0a9ee3f5", + "board_id": null, + "title": "Genie kanban lane (read-only)", + "status": "done", + "claimed_by": "dash-g4-engineer", + "claimed_at": 1784666347308, + "wish": "genie-ui-dash", + "group_name": "group-4", + "created_at": 1784654712971, + "updated_at": 1784669563300, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null + }, + { + "id": "t_mruxc5ora3add7b8", + "board_id": null, + "title": "Hire roster — the single write path", + "status": "done", + "claimed_by": "dash-g5-engineer", + "claimed_at": 1784669605355, + "wish": "genie-ui-dash", + "group_name": "group-5", + "created_at": 1784654713035, + "updated_at": 1784672089341, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": 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, + "created_at": 1784658716672, + "updated_at": 1784658716672, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null + }, + { + "id": "t_mrv087jca37acdd9", + "board_id": null, + "title": "hire_roster substrate (migration + state ops + export)", + "status": "done", + "claimed_by": "bridge-g1-engineer", + "claimed_at": 1784660523509, + "wish": "genie-ui-bridge", + "group_name": "group-1", + "created_at": 1784659567656, + "updated_at": 1784661184531, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null + }, + { + "id": "t_mrv087ldf5bdb175", + "board_id": null, + "title": "genie ui-bridge command (protocol + write tools + push + lifetime)", + "status": "done", + "claimed_by": "bridge-g2-engineer", + "claimed_at": 1784661223928, + "wish": "genie-ui-bridge", + "group_name": "group-2", + "created_at": 1784659567729, + "updated_at": 1784663117984, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": 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, + "created_at": 1784683920978, + "updated_at": 1784683920978, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null + }, + { + "id": "t_mrvf2p7za43e792d", + "board_id": null, + "title": "Mock module + injection seam + watermark", + "status": "done", + "claimed_by": "ldl-g1-engineer", + "claimed_at": 1784694196822, + "wish": "live-dev-loop", + "group_name": "group-1", + "created_at": 1784684504879, + "updated_at": 1784695589899, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null + }, + { + "id": "t_mrvf2pam274f1741", + "board_id": null, + "title": "dev:web — network Vite + dev-only CSP", + "status": "done", + "claimed_by": "ldl-g2-engineer", + "claimed_at": 1784697640859, + "wish": "live-dev-loop", + "group_name": "group-2", + "created_at": 1784684504974, + "updated_at": 1784698734359, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null + }, + { + "id": "t_mrvf2pd39e9349c2", + "board_id": null, + "title": "dev:live watcher + toast + ITERATE.md", + "status": "done", + "claimed_by": "ldl-g3-engineer", + "claimed_at": 1784695629392, + "wish": "live-dev-loop", + "group_name": "group-3", + "created_at": 1784684505063, + "updated_at": 1784697606035, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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, + "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." + }, + { + "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", + "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." + }, + { + "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", + "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." + }, + { + "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", + "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." + }, + { + "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", + "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." + }, + { + "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", + "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." + }, + { + "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", + "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." + } + ], + "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 + } + ], + "wish_groups": [], + "hire_roster": [] +} diff --git a/CLAUDE.md b/CLAUDE.md index 26d0f369f..b61e94404 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -100,6 +100,8 @@ genie task checkout --worker w # Atomically claim a ready task for a work genie task status # Task detail, dependencies, stage log genie task done # Orchestrator only: mark reviewed work done + recompute ready set genie task export # Emit the complete DB state as JSON +genie task export --write # Write the snapshot to .genie/roadmap.json (committed board snapshot) +genie task import [--replace] # Restore genie.db from .genie/roadmap.json (fresh-clone resume) ``` ### Omni subcommands @@ -118,6 +120,7 @@ genie omni inbox # List stored inbound Omni messages (no ne | Task / board / wish state | `/.genie/genie.db` | Per-repo, shared across worktrees | SQLite (bun:sqlite) | | Omni approvals + inbox | `~/.genie/genie.db` | Global (machine-wide) | SQLite (bun:sqlite) | | Wishes / brainstorms / INDEX | `/.genie/{wishes,brainstorms,INDEX.md}` | Per-repo, git-tracked | Markdown | +| Board snapshot (resume/publish) | `/.genie/roadmap.json` | Per-repo, git-tracked | JSON (`task export --write` / `task import`) | Worktrees share the main repo's `.genie/genie.db` via `git rev-parse --git-common-dir`. The two `genie.db` files are wholly separate databases: different paths, different schemas, independent `PRAGMA user_version` — `global-db.ts` deliberately imports NONE of `genie-db.ts`'s path constants; the only shared code is the open primitive in `sqlite-open.ts`. Both use WAL. Documents live in git; operational state lives in SQLite. diff --git a/src/lib/v5/genie-db.ts b/src/lib/v5/genie-db.ts index 4a8370dc2..8ee497472 100644 --- a/src/lib/v5/genie-db.ts +++ b/src/lib/v5/genie-db.ts @@ -77,6 +77,16 @@ export function resolveDbPath(cwd?: string): string { return join(resolveRepoRoot(cwd), '.genie', 'genie.db'); } +/** + * Absolute path to the git-tracked roadmap snapshot for the repo containing + * `cwd`. Unlike genie.db (gitignored, binary, WAL), this JSON snapshot is + * committed so the board survives a clone: refresh with + * `genie task export --write`, restore with `genie task import`. + */ +export function resolveRoadmapPath(cwd?: string): string { + return join(resolveRepoRoot(cwd), '.genie', 'roadmap.json'); +} + // ============================================================================ // Fail-closed project context (Group A: no outer/cache-root empty-board masquerade) // ============================================================================ diff --git a/src/lib/v5/task-state.ts b/src/lib/v5/task-state.ts index 88a8f11f6..96f3e29b0 100644 --- a/src/lib/v5/task-state.ts +++ b/src/lib/v5/task-state.ts @@ -1401,3 +1401,190 @@ export function exportState(db: Database): StateExport { hire_roster: db.query('SELECT * FROM hire_roster ORDER BY wish, agent_adapter_id').all() as RawHire[], }; } + +// ============================================================================ +// Full-state import (the other half of exportState — cross-machine resume) +// ============================================================================ + +/** A snapshot's structure or schemaVersion cannot be imported by this build. */ +export class SnapshotFormatError extends Error { + constructor(message: string) { + super(message); + this.name = 'SnapshotFormatError'; + } +} + +/** Import refused because the database already holds state (use replace). */ +export class NonEmptyImportError extends Error { + constructor() { + super( + 'Database already contains state. Re-run with --replace to overwrite it with the snapshot, or export the local state first if it must be kept.', + ); + this.name = 'NonEmptyImportError'; + } +} + +export interface ImportSummary { + boards: number; + tasks: number; + dependencies: number; + events: number; + wishGroups: number; + hires: number; +} + +const SNAPSHOT_TABLE_KEYS = [ + 'meta', + 'boards', + 'tasks', + 'task_dependencies', + 'stage_log', + 'task_events', + 'wish_groups', + 'hire_roster', +] as const; + +/** Shape-validate an untrusted parsed snapshot; returns it typed or throws. */ +function validateSnapshot(db: Database, snapshot: unknown): StateExport { + if (typeof snapshot !== 'object' || snapshot === null || Array.isArray(snapshot)) { + throw new SnapshotFormatError('Snapshot is not a JSON object.'); + } + const candidate = snapshot as Record; + if (typeof candidate.schemaVersion !== 'number') { + throw new SnapshotFormatError('Snapshot is missing a numeric schemaVersion.'); + } + for (const key of SNAPSHOT_TABLE_KEYS) { + if (!Array.isArray(candidate[key])) { + throw new SnapshotFormatError(`Snapshot is missing the "${key}" table array.`); + } + } + const current = (db.query('PRAGMA user_version').get() as { user_version: number }).user_version; + if (candidate.schemaVersion !== current) { + throw new SnapshotFormatError( + `Snapshot schemaVersion ${candidate.schemaVersion} does not match this database (${current}). Re-export the snapshot with a matching genie version.`, + ); + } + return candidate as unknown as StateExport; +} + +/** True when any operational table holds rows (meta alone does not count). */ +function hasOperationalState(db: Database): boolean { + for (const table of ['boards', 'tasks', 'task_events', 'stage_log', 'wish_groups', 'hire_roster']) { + const row = db.query(`SELECT COUNT(*) AS n FROM ${table}`).get() as { n: number }; + if (row.n > 0) return true; + } + return false; +} + +function insertSnapshotRows(db: Database, state: StateExport): void { + const meta = db.query('INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)'); + for (const m of state.meta) meta.run(m.key, m.value); + + const board = db.query('INSERT INTO boards (id, name, lanes, created_at) VALUES (?, ?, ?, ?)'); + for (const b of state.boards) board.run(b.id, b.name, b.lanes ?? null, b.created_at); + + // Nullable-with-?? throughout: additive columns (lane, agent_kind, …) backfill + // without a user_version bump, so a same-version snapshot from an older build + // may legitimately omit them. + const task = db.query( + `INSERT INTO tasks (id, board_id, title, status, claimed_by, claimed_at, wish, group_name, + lane, agent_kind, heartbeat_at, blocked_by, blocked_reason, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ); + for (const t of state.tasks) { + task.run( + t.id, + t.board_id ?? null, + t.title, + t.status, + t.claimed_by ?? null, + t.claimed_at ?? null, + t.wish ?? null, + t.group_name ?? null, + t.lane ?? null, + t.agent_kind ?? null, + t.heartbeat_at ?? null, + t.blocked_by ?? null, + t.blocked_reason ?? null, + t.created_at, + t.updated_at, + ); + } + + const dep = db.query('INSERT INTO task_dependencies (task_id, depends_on_id) VALUES (?, ?)'); + for (const d of state.task_dependencies) dep.run(d.task_id, d.depends_on_id); + + const stage = db.query('INSERT INTO stage_log (id, task_id, stage, note, created_at) VALUES (?, ?, ?, ?, ?)'); + for (const s of state.stage_log) stage.run(s.id, s.task_id, s.stage, s.note ?? null, s.created_at); + + const event = db.query( + 'INSERT INTO task_events (id, task_id, kind, note, author_kind, author, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)', + ); + for (const e of state.task_events) { + event.run(e.id, e.task_id, e.kind, e.note ?? null, e.author_kind ?? null, e.author ?? null, e.created_at); + } + + const group = db.query( + `INSERT INTO wish_groups (wish, name, status, depends_on, assignee, started_at, completed_at, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ); + for (const g of state.wish_groups) { + group.run( + g.wish, + g.name, + g.status, + g.depends_on ?? '[]', + g.assignee ?? null, + g.started_at ?? null, + g.completed_at ?? null, + g.created_at, + g.updated_at, + ); + } + + const hire = db.query( + 'INSERT INTO hire_roster (wish, agent_adapter_id, profile, worktree, hired_at, state) VALUES (?, ?, ?, ?, ?, ?)', + ); + for (const h of state.hire_roster) { + hire.run(h.wish, h.agent_adapter_id, h.profile ?? null, h.worktree, h.hired_at, h.state); + } +} + +/** + * Restore a full {@link exportState} snapshot into this database — the resume + * path for a fresh clone on another machine (`genie task import`). Refuses a + * database that already holds operational state unless `replace` is set, in + * which case every table is cleared and rebuilt from the snapshot inside one + * transaction. Row ids (including event/stage autoincrement ids) are preserved + * exactly, so an export → import round-trip is lossless. + */ +export function importState(db: Database, snapshot: unknown, opts: { replace?: boolean } = {}): ImportSummary { + const state = validateSnapshot(db, snapshot); + const apply = db.transaction(() => { + if (hasOperationalState(db)) { + if (!opts.replace) throw new NonEmptyImportError(); + // Children before parents so FK cascades never fire mid-wipe. + for (const table of [ + 'task_events', + 'stage_log', + 'task_dependencies', + 'tasks', + 'hire_roster', + 'wish_groups', + 'boards', + ]) { + db.query(`DELETE FROM ${table}`).run(); + } + } + insertSnapshotRows(db, state); + }); + apply(); + return { + boards: state.boards.length, + tasks: state.tasks.length, + dependencies: state.task_dependencies.length, + events: state.task_events.length, + wishGroups: state.wish_groups.length, + hires: state.hire_roster.length, + }; +} diff --git a/src/term-commands/v5-task.test.ts b/src/term-commands/v5-task.test.ts index b4e95174d..b63cce023 100644 --- a/src/term-commands/v5-task.test.ts +++ b/src/term-commands/v5-task.test.ts @@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { execFileSync } from 'node:child_process'; -import { mkdtempSync, rmSync } from 'node:fs'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { mkdir } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -282,6 +282,106 @@ describe('task export round-trip', () => { }); }); +describe('task import', () => { + /** Seed a representative slice of every table into `repo`'s db. */ + function seedState(): { rootId: string; depId: string } { + const db = openDb({ cwd: repo }); + const board = createBoard(db, 'main-board'); + const a = createTask(db, { title: 'root', boardId: board.id, wish: 'demo', group: 'g1' }); + const b = createTask(db, { title: 'dependent', dependsOn: [a.id] }); + appendStage(db, a.id, 'planned', 'kickoff'); + appendTaskEvent(db, a.id, { kind: 'comment', note: 'hello', author: 'tester', authorKind: 'human' }); + createWishGroups(db, 'demo', [{ name: 'g1' }, { name: 'g2', dependsOn: ['g1'] }]); + db.close(); + return { rootId: a.id, depId: b.id }; + } + + /** A second throwaway git repo simulating the other machine's fresh clone. */ + function makeCloneRepo(): string { + const clone = mkdtempSync(join(tmpdir(), 'genie-v5-import-')); + git(clone, 'init', '-b', 'main'); + git(clone, 'commit', '--allow-empty', '-m', 'init'); + return clone; + } + + test('export --write then import on a fresh repo is a lossless round-trip', async () => { + seedState(); + const w = await cli(repo, 'export', '--write'); + expect(w.code).toBe(0); + expect(w.stderr).toBe(''); + const snapshotPath = join(repo, '.genie', 'roadmap.json'); + expect(w.stdout).toContain(snapshotPath); + + const clone = makeCloneRepo(); + try { + await mkdir(join(clone, '.genie'), { recursive: true }); + const snapshot = readFileSync(snapshotPath, 'utf-8'); + writeFileSync(join(clone, '.genie', 'roadmap.json'), snapshot); + + const r = await cli(clone, 'import'); + expect(r.code).toBe(0); + expect(r.stderr).toBe(''); + expect(r.stdout).toMatch(/Imported 2 tasks, 1 boards, 1 dependencies/); + + // Byte-identical state: re-exporting the clone reproduces the snapshot. + const reExport = await cli(clone, 'export'); + expect(reExport.code).toBe(0); + expect(JSON.parse(reExport.stdout)).toEqual(JSON.parse(snapshot)); + } finally { + rmSync(clone, { recursive: true, force: true }); + } + }); + + test('refuses a database that already holds state, and --replace overwrites it', async () => { + seedState(); + const w = await cli(repo, 'export', '--write'); + expect(w.code).toBe(0); + + // Same repo, same db: state exists, plain import must refuse. + const refused = await cli(repo, 'import'); + expect(refused.code).toBe(1); + expect(refused.stderr).toContain('--replace'); + + // Diverge the local db, then restore the snapshot wholesale. + const db = openDb({ cwd: repo }); + createTask(db, { title: 'local-only drift' }); + db.close(); + const replaced = await cli(repo, 'import', '--replace'); + expect(replaced.code).toBe(0); + expect(replaced.stdout).toMatch(/Imported 2 tasks/); + const after = openDb({ cwd: repo }); + const titles = (after.query('SELECT title FROM tasks ORDER BY title').all() as Array<{ title: string }>).map( + (t) => t.title, + ); + after.close(); + expect(titles).toEqual(['dependent', 'root']); + }); + + test('missing snapshot and schemaVersion mismatch both fail with clear stderr', async () => { + const missing = await cli(repo, 'import'); + expect(missing.code).toBe(1); + expect(missing.stderr).toContain('Snapshot not found'); + expect(missing.stderr).toContain('export --write'); + + await mkdir(join(repo, '.genie'), { recursive: true }); + const empty: StateExport = { + schemaVersion: 999, + meta: [], + boards: [], + tasks: [], + task_dependencies: [], + stage_log: [], + task_events: [], + wish_groups: [], + hire_roster: [], + }; + writeFileSync(join(repo, '.genie', 'roadmap.json'), JSON.stringify(empty)); + const mismatch = await cli(repo, 'import'); + expect(mismatch.code).toBe(1); + expect(mismatch.stderr).toContain('schemaVersion 999'); + }); +}); + // Same subprocess invocation as `cli`, but with extra env vars layered on — used // to prove runtime identity flows from the environment into the stored event. async function cliEnv(cwd: string, env: Record, ...args: string[]): Promise { diff --git a/src/term-commands/v5-task.ts b/src/term-commands/v5-task.ts index 390ceab4c..8e68d3529 100644 --- a/src/term-commands/v5-task.ts +++ b/src/term-commands/v5-task.ts @@ -11,14 +11,16 @@ * task status * task done * task checkout [--worker ] - * task export + * task export [--write [file]] + * task import [file] [--replace] */ import type { Database } from 'bun:sqlite'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import type { Command } from 'commander'; import { color, formatTimestamp, padRight, truncate } from '../lib/term-format.js'; import { livenessBadge } from '../lib/v5/card-render.js'; -import { openDb } from '../lib/v5/genie-db.js'; +import { openDb, resolveRoadmapPath } from '../lib/v5/genie-db.js'; import { type EventAuthor, type TaskCardRow, @@ -37,6 +39,7 @@ import { getTask, getTaskCard, getTaskEvents, + importState, listTasks, moveTask, recordHeartbeat, @@ -415,11 +418,50 @@ function handleHeartbeat(id: string): void { }); } -function handleExport(): void { +interface ExportOptions { + write?: string | boolean; +} + +function handleExport(opts: ExportOptions): void { + run(() => { + const db = openDb(); + try { + const json = `${JSON.stringify(exportState(db), null, 2)}\n`; + if (opts.write) { + const target = typeof opts.write === 'string' ? opts.write : resolveRoadmapPath(); + writeFileSync(target, json); + out(`Wrote board snapshot to ${target}.`); + return; + } + process.stdout.write(json); + } finally { + db.close(); + } + }); +} + +interface ImportOptions { + replace?: boolean; +} + +function handleImport(file: string | undefined, opts: ImportOptions): void { run(() => { + const source = file ?? resolveRoadmapPath(); + if (!existsSync(source)) { + fail(`Snapshot not found: ${source}. Generate one with \`genie task export --write\` and commit it.`); + } + let snapshot: unknown; + try { + snapshot = JSON.parse(readFileSync(source, 'utf-8')); + } catch (err) { + fail(`Snapshot at ${source} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`); + } const db = openDb(); try { - out(JSON.stringify(exportState(db), null, 2)); + const summary = importState(db, snapshot, { replace: opts.replace }); + out( + `Imported ${summary.tasks} tasks, ${summary.boards} boards, ${summary.dependencies} dependencies, ${summary.events} events, ${summary.wishGroups} wish groups, ${summary.hires} hires from ${source}.`, + ); } finally { db.close(); } @@ -507,5 +549,12 @@ export function registerV5TaskCommands(v5: Command): void { task .command('export') .description('Emit the complete database state as JSON') - .action(() => handleExport()); + .option('--write [file]', 'Write the snapshot to a file instead of stdout (default: .genie/roadmap.json)') + .action((opts: ExportOptions) => handleExport(opts)); + + task + .command('import [file]') + .description('Restore database state from an export snapshot (default: .genie/roadmap.json)') + .option('--replace', 'Overwrite existing state instead of refusing a non-empty database') + .action((file: string | undefined, opts: ImportOptions) => handleImport(file, opts)); } From c4afca2e61f5a0de111bc6026cc798a2a6d9c00f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Jul 2026 19:59:11 +0000 Subject: [PATCH 03/22] chore(version): bump to 5.260728.9 [auto-version] --- .claude-plugin/marketplace.json | 2 +- package.json | 2 +- plugins/genie/.claude-plugin/plugin.json | 2 +- plugins/genie/.codex-plugin/plugin.json | 2 +- plugins/genie/package.json | 2 +- plugins/hermes-genie/plugin.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index aa80af67d..7ca7db4c0 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "genie", - "version": "5.260728.7", + "version": "5.260728.9", "source": "./plugins/genie", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project." } diff --git a/package.json b/package.json index 0c7de0429..57ab2fb12 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@automagik/genie", - "version": "5.260728.7", + "version": "5.260728.9", "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", "license": "MIT", diff --git a/plugins/genie/.claude-plugin/plugin.json b/plugins/genie/.claude-plugin/plugin.json index 70d212f3f..0ac2b2e66 100644 --- a/plugins/genie/.claude-plugin/plugin.json +++ b/plugins/genie/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260728.7", + "version": "5.260728.9", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.", "author": { "name": "Namastex Labs" diff --git a/plugins/genie/.codex-plugin/plugin.json b/plugins/genie/.codex-plugin/plugin.json index 4a16340d1..6b5c199aa 100644 --- a/plugins/genie/.codex-plugin/plugin.json +++ b/plugins/genie/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260728.7", + "version": "5.260728.9", "description": "Plan, execute, review, and ship software with Genie workflows in Codex.", "author": { "name": "Namastex Labs", diff --git a/plugins/genie/package.json b/plugins/genie/package.json index 59eaf5dbe..a7219dd80 100644 --- a/plugins/genie/package.json +++ b/plugins/genie/package.json @@ -1,6 +1,6 @@ { "name": "genie-plugin", - "version": "5.260728.7", + "version": "5.260728.9", "private": true, "description": "Runtime dependencies for genie bundled CLIs", "license": "MIT", diff --git a/plugins/hermes-genie/plugin.yaml b/plugins/hermes-genie/plugin.yaml index 636e7906d..058555df5 100644 --- a/plugins/hermes-genie/plugin.yaml +++ b/plugins/hermes-genie/plugin.yaml @@ -1,5 +1,5 @@ name: genie -version: 5.260728.7 +version: 5.260728.9 description: "Native Hermes surface for Genie orchestration: read-only status plus work-plan and review-plan gap tools that the MCP board surface does not cover, hooks, commands, and a thin cockpit skill. Board/task truth comes from the genie MCP tools." provides_tools: # Default surface: exactly the three gap tools the MCP board surface does not cover. From f695bd88cb1f1d0c904dd8dd2c4a81bf32ce716f Mon Sep 17 00:00:00 2001 From: Felipe Rosa Date: Tue, 28 Jul 2026 16:59:36 -0300 Subject: [PATCH 04/22] chore(hooks): auto-refresh .genie/roadmap.json in pre-commit Regenerates the committed board snapshot from genie.db on every commit (fail-open, skipped in linked worktrees and db-less fresh clones) so the published roadmap never lags the live board. Co-Authored-By: Claude Fable 5 --- .husky/pre-commit | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.husky/pre-commit b/.husky/pre-commit index 5b10c250d..5e49d9f8f 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -11,5 +11,20 @@ if [ -n "$branch" ] && [ "${SKIP_CI_CHECK:-}" != "1" ] && command -v gh >/dev/nu fi fi +# Refresh the committed board snapshot from genie.db (fail-open: a broken +# export must not block unrelated commits). Skipped in linked worktrees — +# resolveRoadmapPath targets the main checkout, whose file this worktree +# cannot stage — and when no genie.db exists, so a fresh clone never +# clobbers the committed snapshot with empty state. +git_dir=$(git rev-parse --path-format=absolute --git-dir 2>/dev/null) +git_common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) +if [ "$git_dir" = "$git_common" ] && [ -f .genie/genie.db ] && [ -f src/genie.ts ]; then + if bun src/genie.ts task export --write >/dev/null 2>&1; then + git add .genie/roadmap.json + else + echo "warn: board snapshot refresh failed (bun src/genie.ts task export --write) — committing without it" >&2 + fi +fi + # Lint staged files bunx biome check --staged --no-errors-on-unmatched From 80f375e1e6c407742b1a09055780b4b938d056b3 Mon Sep 17 00:00:00 2001 From: Felipe Rosa Date: Tue, 28 Jul 2026 17:14:44 -0300 Subject: [PATCH 05/22] feat(task): make roadmap.json canonical via three-way sync genie.db is now a materialization of the committed .genie/roadmap.json. A machine-local hash baseline (.genie/roadmap-sync, gitignored) lets `genie task sync` detect which side moved: pulled snapshot -> auto-import, local board mutations -> export, both moved -> refuse and warn with the two explicit resolutions (import --replace / export --write). Every board/task verb auto-syncs fail-open and silently; pre-commit runs sync instead of blind export, so a stale local db can never clobber the published roadmap. Fresh clone bootstraps with any board/task command. Co-Authored-By: Claude Fable 5 --- .genie/INDEX.md | 2 +- .gitignore | 3 + .husky/pre-commit | 17 +-- CLAUDE.md | 7 +- src/lib/v5/roadmap-sync.ts | 170 ++++++++++++++++++++++++++++++ src/lib/v5/task-state.ts | 7 +- src/term-commands/v5-board.ts | 5 + src/term-commands/v5-task.test.ts | 94 +++++++++++++++++ src/term-commands/v5-task.ts | 32 ++++++ 9 files changed, 323 insertions(+), 14 deletions(-) create mode 100644 src/lib/v5/roadmap-sync.ts diff --git a/.genie/INDEX.md b/.genie/INDEX.md index 358bc9874..ce3b97780 100644 --- a/.genie/INDEX.md +++ b/.genie/INDEX.md @@ -1,6 +1,6 @@ # Plans Index -> **Board snapshot:** the genie.db task/board state (the roadmap behind `genie board`) is committed as [roadmap.json](roadmap.json) so it survives clones and publishes with the repo. On a fresh machine: `genie task import` rebuilds `.genie/genie.db` from it. After board changes: `genie task export --write`, then commit the refreshed snapshot. +> **Board snapshot (canonical):** [roadmap.json](roadmap.json) is the canonical roadmap board (`genie board`); the local `.genie/genie.db` is a materialization of it. Every `genie board`/`genie task` command auto-syncs three-way: a pulled snapshot imports into genie.db, local board mutations export back (and the pre-commit hook commits the refresh), and if BOTH moved since the last sync genie touches nothing and warns — resolve with `genie task import --replace` (take the snapshot) or `genie task export --write` (keep the local board). Fresh clone: just run `genie board`. ## Raw diff --git a/.gitignore b/.gitignore index 0904c2f96..b6aebdf34 100644 --- a/.gitignore +++ b/.gitignore @@ -72,6 +72,9 @@ yarn-error.log* .genie/genie.db-wal .genie/genie.db-shm +# Machine-local roadmap sync baseline (pairs genie.db with roadmap.json) +.genie/roadmap-sync + # Per-worktree kickoff prompts emitted by `genie launch` .genie/launch/ diff --git a/.husky/pre-commit b/.husky/pre-commit index 5e49d9f8f..9505697ab 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -11,18 +11,19 @@ if [ -n "$branch" ] && [ "${SKIP_CI_CHECK:-}" != "1" ] && command -v gh >/dev/nu fi fi -# Refresh the committed board snapshot from genie.db (fail-open: a broken -# export must not block unrelated commits). Skipped in linked worktrees — -# resolveRoadmapPath targets the main checkout, whose file this worktree -# cannot stage — and when no genie.db exists, so a fresh clone never -# clobbers the committed snapshot with empty state. +# Reconcile the committed board snapshot (.genie/roadmap.json — canonical) with +# the local genie.db via three-way sync: `task sync` imports when the snapshot +# moved, exports when only the local board moved, and refuses to touch anything +# on divergence — so a stale local db can never clobber the roadmap. Fail-open: +# sync trouble warns but never blocks a commit. Skipped in linked worktrees +# (the snapshot lives in the main checkout) and when no genie.db exists yet. git_dir=$(git rev-parse --path-format=absolute --git-dir 2>/dev/null) git_common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) if [ "$git_dir" = "$git_common" ] && [ -f .genie/genie.db ] && [ -f src/genie.ts ]; then - if bun src/genie.ts task export --write >/dev/null 2>&1; then - git add .genie/roadmap.json + if bun src/genie.ts task sync; then + [ -f .genie/roadmap.json ] && git add .genie/roadmap.json else - echo "warn: board snapshot refresh failed (bun src/genie.ts task export --write) — committing without it" >&2 + echo "warn: board snapshot not refreshed (see message above) — committing without it" >&2 fi fi diff --git a/CLAUDE.md b/CLAUDE.md index b61e94404..e1b0106d7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -100,8 +100,9 @@ genie task checkout --worker w # Atomically claim a ready task for a work genie task status # Task detail, dependencies, stage log genie task done # Orchestrator only: mark reviewed work done + recompute ready set genie task export # Emit the complete DB state as JSON -genie task export --write # Write the snapshot to .genie/roadmap.json (committed board snapshot) -genie task import [--replace] # Restore genie.db from .genie/roadmap.json (fresh-clone resume) +genie task export --write # Write .genie/roadmap.json (diverged-sync resolution: keep local board) +genie task import [--replace] # Restore genie.db from .genie/roadmap.json (resolution: take snapshot) +genie task sync # Three-way reconcile genie.db <-> roadmap.json (auto-runs before task/board verbs) ``` ### Omni subcommands @@ -120,7 +121,7 @@ genie omni inbox # List stored inbound Omni messages (no ne | Task / board / wish state | `/.genie/genie.db` | Per-repo, shared across worktrees | SQLite (bun:sqlite) | | Omni approvals + inbox | `~/.genie/genie.db` | Global (machine-wide) | SQLite (bun:sqlite) | | Wishes / brainstorms / INDEX | `/.genie/{wishes,brainstorms,INDEX.md}` | Per-repo, git-tracked | Markdown | -| Board snapshot (resume/publish) | `/.genie/roadmap.json` | Per-repo, git-tracked | JSON (`task export --write` / `task import`) | +| Board snapshot (CANONICAL roadmap) | `/.genie/roadmap.json` | Per-repo, git-tracked | JSON — genie.db materializes from it via three-way `task sync` (auto before task/board verbs; machine-local baseline in gitignored `.genie/roadmap-sync`) | Worktrees share the main repo's `.genie/genie.db` via `git rev-parse --git-common-dir`. The two `genie.db` files are wholly separate databases: different paths, different schemas, independent `PRAGMA user_version` — `global-db.ts` deliberately imports NONE of `genie-db.ts`'s path constants; the only shared code is the open primitive in `sqlite-open.ts`. Both use WAL. Documents live in git; operational state lives in SQLite. diff --git a/src/lib/v5/roadmap-sync.ts b/src/lib/v5/roadmap-sync.ts new file mode 100644 index 000000000..4ea26e525 --- /dev/null +++ b/src/lib/v5/roadmap-sync.ts @@ -0,0 +1,170 @@ +/** + * Roadmap snapshot sync — makes the git-tracked `.genie/roadmap.json` the + * canonical board while `.genie/genie.db` stays the fast local materialization. + * + * A machine-local marker (`.genie/roadmap-sync`, gitignored, next to genie.db) + * remembers the (file, db) content-hash pair from the last synchronized state. + * Comparing both current hashes against that baseline yields a three-way + * verdict per side — unchanged or changed — and sync acts only when exactly one + * side moved: + * + * file moved, db didn't → git pull delivered a newer board → import + * db moved, file didn't → local mutations → export (published at commit) + * both moved → DIVERGED: warn, touch nothing; the operator picks + * `task import --replace` (take the file) or + * `task export --write` (keep the db) + * + * The diverged branch is the whole point: a stale local db can never silently + * overwrite the committed roadmap, and a pull can never silently destroy + * unpublished local board state. + */ + +import type { Database } from 'bun:sqlite'; +import { createHash } from 'node:crypto'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { openDb, resolveRepoRoot, resolveRoadmapPath } from './genie-db.js'; +import { exportState, hasOperationalState, importState } from './task-state.js'; + +export type SyncAction = 'none' | 'imported' | 'exported' | 'diverged'; + +export interface SyncResult { + action: SyncAction; + /** Human-readable detail; always set for `diverged`. */ + message?: string; +} + +/** Machine-local sync baseline. Lives next to genie.db; never git-tracked. */ +export function resolveSyncMarkerPath(cwd?: string): string { + return join(resolveRepoRoot(cwd), '.genie', 'roadmap-sync'); +} + +/** Content hash over the canonical (whitespace-independent) JSON form. */ +function canonicalHash(value: unknown): string { + return createHash('sha256').update(JSON.stringify(value)).digest('hex'); +} + +interface SyncMarker { + fileHash: string; + dbHash: string; +} + +function readMarker(path: string): SyncMarker | null { + if (!existsSync(path)) return null; + try { + const parsed = JSON.parse(readFileSync(path, 'utf-8')) as Partial; + if (typeof parsed.fileHash === 'string' && typeof parsed.dbHash === 'string') { + return { fileHash: parsed.fileHash, dbHash: parsed.dbHash }; + } + } catch { + // Corrupt marker — treat as absent; sync falls back to its safe defaults. + } + return null; +} + +function writeMarker(path: string, marker: SyncMarker): void { + writeFileSync(path, `${JSON.stringify(marker, null, 2)}\n`); +} + +function serializeSnapshot(state: unknown): string { + return `${JSON.stringify(state, null, 2)}\n`; +} + +/** + * Stamp the CURRENT (roadmap.json, genie.db) pair as the sync baseline. + * Called after an explicit `task import`/`task export --write` succeeds: the + * operator has just declared this pair intentional, so future syncs measure + * drift from here. + */ +export function recordSyncBaseline(db: Database, cwd?: string): void { + const filePath = resolveRoadmapPath(cwd); + const dbHash = canonicalHash(exportState(db)); + let fileHash = dbHash; + if (existsSync(filePath)) { + try { + fileHash = canonicalHash(JSON.parse(readFileSync(filePath, 'utf-8'))); + } catch { + // Unreadable file: baseline the db side only; next sync will flag it. + } + } + writeMarker(resolveSyncMarkerPath(cwd), { fileHash, dbHash }); +} + +/** + * Reconcile genie.db with the canonical roadmap.json. Acts only when exactly + * one side changed since the last baseline (see module doc); never destroys + * state on divergence. Cheap enough to run before every board/task command: + * one full-state serialization of a small database plus one file hash. + */ +export function syncRoadmap(db: Database, cwd?: string): SyncResult { + const filePath = resolveRoadmapPath(cwd); + const markerPath = resolveSyncMarkerPath(cwd); + const dbState = exportState(db); + const dbHash = canonicalHash(dbState); + + if (!existsSync(filePath)) { + if (!hasOperationalState(db)) return { action: 'none' }; + writeFileSync(filePath, serializeSnapshot(dbState)); + writeMarker(markerPath, { fileHash: dbHash, dbHash }); + return { action: 'exported', message: `Published board snapshot to ${filePath}.` }; + } + + let parsed: unknown; + try { + parsed = JSON.parse(readFileSync(filePath, 'utf-8')); + } catch { + return { + action: 'diverged', + message: `${filePath} is not valid JSON. Fix it, or regenerate it from the local board with \`genie task export --write\`.`, + }; + } + const fileHash = canonicalHash(parsed); + + if (fileHash === dbHash) { + writeMarker(markerPath, { fileHash, dbHash }); + return { action: 'none' }; + } + + const marker = readMarker(markerPath); + const fileChanged = marker === null || fileHash !== marker.fileHash; + const dbChanged = marker === null || dbHash !== marker.dbHash; + + if (fileChanged && (!dbChanged || !hasOperationalState(db))) { + importState(db, parsed, { replace: true }); + writeMarker(markerPath, { fileHash, dbHash: canonicalHash(exportState(db)) }); + return { action: 'imported', message: `Board refreshed from ${filePath}.` }; + } + if (dbChanged && !fileChanged) { + writeFileSync(filePath, serializeSnapshot(dbState)); + writeMarker(markerPath, { fileHash: dbHash, dbHash }); + return { action: 'exported', message: `Board snapshot ${filePath} refreshed from the local database.` }; + } + const resolution = + 'Resolve with `genie task import --replace` (take the snapshot) or `genie task export --write` (keep the local board).'; + return { + action: 'diverged', + message: `Both the local board (genie.db) and ${filePath} changed since the last sync. Nothing was overwritten. ${resolution}`, + }; +} + +/** + * Fail-open sync for command preAction hooks: reconcile silently (an import or + * export that worked needs no announcement, and stdout must stay clean for + * --json consumers), warn on stderr only when the sides diverged, and never + * let a sync failure break the board/task command the user actually ran. + */ +export function autoSyncBeforeCommand(): void { + try { + const db = openDb(); + try { + const result = syncRoadmap(db); + if (result.action === 'diverged' && result.message) { + process.stderr.write(`warn: ${result.message}\n`); + } + } finally { + db.close(); + } + } catch { + // Sync is a convenience layer; the command itself surfaces real db errors. + } +} diff --git a/src/lib/v5/task-state.ts b/src/lib/v5/task-state.ts index 96f3e29b0..f0551a199 100644 --- a/src/lib/v5/task-state.ts +++ b/src/lib/v5/task-state.ts @@ -1468,7 +1468,7 @@ function validateSnapshot(db: Database, snapshot: unknown): StateExport { } /** True when any operational table holds rows (meta alone does not count). */ -function hasOperationalState(db: Database): boolean { +export function hasOperationalState(db: Database): boolean { for (const table of ['boards', 'tasks', 'task_events', 'stage_log', 'wish_groups', 'hire_roster']) { const row = db.query(`SELECT COUNT(*) AS n FROM ${table}`).get() as { n: number }; if (row.n > 0) return true; @@ -1563,7 +1563,9 @@ export function importState(db: Database, snapshot: unknown, opts: { replace?: b const apply = db.transaction(() => { if (hasOperationalState(db)) { if (!opts.replace) throw new NonEmptyImportError(); - // Children before parents so FK cascades never fire mid-wipe. + // Children before parents so FK cascades never fire mid-wipe. Meta is + // wiped too: replace means EXACT snapshot state, and stale local + // wish_sig/backfill markers would misdescribe the imported rows. for (const table of [ 'task_events', 'stage_log', @@ -1572,6 +1574,7 @@ export function importState(db: Database, snapshot: unknown, opts: { replace?: b 'hire_roster', 'wish_groups', 'boards', + 'meta', ]) { db.query(`DELETE FROM ${table}`).run(); } diff --git a/src/term-commands/v5-board.ts b/src/term-commands/v5-board.ts index 77d03abcb..3d3eacf48 100644 --- a/src/term-commands/v5-board.ts +++ b/src/term-commands/v5-board.ts @@ -12,6 +12,7 @@ import type { Command } from 'commander'; import { color, padRight, truncate } from '../lib/term-format.js'; import { cardBadges } from '../lib/v5/card-render.js'; import { openDb } from '../lib/v5/genie-db.js'; +import { autoSyncBeforeCommand } from '../lib/v5/roadmap-sync.js'; import { type BoardRow, DEFAULT_LIFECYCLE_LANES, @@ -318,6 +319,10 @@ export function registerV5BoardCommands(v5: Command): void { .option('--json', 'Output as JSON') .action((opts: BoardOptions) => handleBoard(opts)); + // roadmap.json is canonical: reconcile before rendering so a freshly pulled + // snapshot (or fresh clone) materializes into genie.db first (fail-open). + board.hook('preAction', () => autoSyncBeforeCommand()); + board .command('create ') .description('Create a board (defaults to the 6 lifecycle lanes)') diff --git a/src/term-commands/v5-task.test.ts b/src/term-commands/v5-task.test.ts index b63cce023..1490e5eaa 100644 --- a/src/term-commands/v5-task.test.ts +++ b/src/term-commands/v5-task.test.ts @@ -382,6 +382,100 @@ describe('task import', () => { }); }); +describe('roadmap.json canonical sync', () => { + function snapshotOf(dir: string): string { + return readFileSync(join(dir, '.genie', 'roadmap.json'), 'utf-8'); + } + + async function plantSnapshot(dir: string, snapshot: string): Promise { + await mkdir(join(dir, '.genie'), { recursive: true }); + writeFileSync(join(dir, '.genie', 'roadmap.json'), snapshot); + } + + test('fresh clone: any task command materializes the board from the snapshot', async () => { + const db = openDb({ cwd: repo }); + createTask(db, { title: 'canonical card' }); + db.close(); + const published = await cli(repo, 'sync'); + expect(published.code).toBe(0); + expect(published.stdout).toContain('Published board snapshot'); + + const clone = mkdtempSync(join(tmpdir(), 'genie-v5-sync-')); + try { + git(clone, 'init', '-b', 'main'); + git(clone, 'commit', '--allow-empty', '-m', 'init'); + await plantSnapshot(clone, snapshotOf(repo)); + + // No genie.db exists in the clone yet: `task list` must auto-import. + const r = await cli(clone, 'list'); + expect(r.code).toBe(0); + expect(r.stderr).toBe(''); + expect(r.stdout).toContain('canonical card'); + } finally { + rmSync(clone, { recursive: true, force: true }); + } + }); + + test('pulled snapshot auto-imports; local mutation auto-exports; divergence is refused then resolvable', async () => { + // Machine A (repo): publish F1, then F2 with one more card. + const db = openDb({ cwd: repo }); + createTask(db, { title: 'first card' }); + db.close(); + await cli(repo, 'sync'); + const f1 = snapshotOf(repo); + const db2 = openDb({ cwd: repo }); + createTask(db2, { title: 'second card' }); + db2.close(); + await cli(repo, 'sync'); + const f2 = snapshotOf(repo); + + // Machine B (clone): start from F1. + const clone = mkdtempSync(join(tmpdir(), 'genie-v5-sync-')); + try { + git(clone, 'init', '-b', 'main'); + git(clone, 'commit', '--allow-empty', '-m', 'init'); + await plantSnapshot(clone, f1); + await cli(clone, 'list'); // materialize + baseline + + // Simulated pull: F2 arrives while B's db is untouched → auto-import. + writeFileSync(join(clone, '.genie', 'roadmap.json'), f2); + const pulled = await cli(clone, 'list'); + expect(pulled.stderr).toBe(''); + expect(pulled.stdout).toContain('second card'); + + // Local mutation on B → db ahead → sync exports. + const created = await cli(clone, 'create', '--title', 'b-only card'); + expect(created.code).toBe(0); + const exported = await cli(clone, 'sync'); + expect(exported.code).toBe(0); + expect(snapshotOf(clone)).toContain('b-only card'); + + // Divergence: local db mutates AND a foreign snapshot lands → refuse both ways. + await cli(clone, 'create', '--title', 'b-diverging card'); + writeFileSync(join(clone, '.genie', 'roadmap.json'), f2); + const diverged = await cli(clone, 'sync'); + expect(diverged.code).toBe(1); + expect(diverged.stdout).toContain('Nothing was overwritten'); + expect(snapshotOf(clone)).toBe(f2); // snapshot untouched + const listed = await cli(clone, 'list'); + expect(listed.stderr).toContain('warn:'); // ordinary commands warn… + expect(listed.stdout).toContain('b-diverging card'); // …and keep local state + + // Resolution: take the snapshot wholesale. + const resolved = await cli(clone, 'import', '--replace'); + expect(resolved.code).toBe(0); + const settled = await cli(clone, 'sync'); + expect(settled.code).toBe(0); + const after = await cli(clone, 'list'); + expect(after.stderr).toBe(''); + expect(after.stdout).not.toContain('b-diverging card'); + expect(after.stdout).toContain('second card'); + } finally { + rmSync(clone, { recursive: true, force: true }); + } + }); +}); + // Same subprocess invocation as `cli`, but with extra env vars layered on — used // to prove runtime identity flows from the environment into the stored event. async function cliEnv(cwd: string, env: Record, ...args: string[]): Promise { diff --git a/src/term-commands/v5-task.ts b/src/term-commands/v5-task.ts index 8e68d3529..43ed16b32 100644 --- a/src/term-commands/v5-task.ts +++ b/src/term-commands/v5-task.ts @@ -21,6 +21,7 @@ import type { Command } from 'commander'; import { color, formatTimestamp, padRight, truncate } from '../lib/term-format.js'; import { livenessBadge } from '../lib/v5/card-render.js'; import { openDb, resolveRoadmapPath } from '../lib/v5/genie-db.js'; +import { autoSyncBeforeCommand, recordSyncBaseline, syncRoadmap } from '../lib/v5/roadmap-sync.js'; import { type EventAuthor, type TaskCardRow, @@ -430,6 +431,9 @@ function handleExport(opts: ExportOptions): void { if (opts.write) { const target = typeof opts.write === 'string' ? opts.write : resolveRoadmapPath(); writeFileSync(target, json); + // Writing the canonical snapshot declares "this pair is intentional" — + // it is the keep-the-local-board resolution for a diverged sync. + if (target === resolveRoadmapPath()) recordSyncBaseline(db); out(`Wrote board snapshot to ${target}.`); return; } @@ -444,6 +448,19 @@ interface ImportOptions { replace?: boolean; } +function handleSync(): void { + run(() => { + const db = openDb(); + try { + const result = syncRoadmap(db); + out(result.message ?? `Board and snapshot are in sync (${result.action}).`); + if (result.action === 'diverged') process.exit(1); + } finally { + db.close(); + } + }); +} + function handleImport(file: string | undefined, opts: ImportOptions): void { run(() => { const source = file ?? resolveRoadmapPath(); @@ -459,6 +476,7 @@ function handleImport(file: string | undefined, opts: ImportOptions): void { const db = openDb(); try { const summary = importState(db, snapshot, { replace: opts.replace }); + if (source === resolveRoadmapPath()) recordSyncBaseline(db); out( `Imported ${summary.tasks} tasks, ${summary.boards} boards, ${summary.dependencies} dependencies, ${summary.events} events, ${summary.wishGroups} wish groups, ${summary.hires} hires from ${source}.`, ); @@ -472,9 +490,18 @@ function handleImport(file: string | undefined, opts: ImportOptions): void { // Registration // ============================================================================ +/** Subcommands that ARE the sync surface — auto-syncing first would tangle them. */ +const AUTO_SYNC_EXEMPT = new Set(['import', 'export', 'sync']); + export function registerV5TaskCommands(v5: Command): void { const task = v5.command('task').description('task state (SQLite, zero-daemon)'); + // roadmap.json is canonical: reconcile before every ordinary task verb so a + // pulled snapshot materializes and local mutations publish (fail-open). + task.hook('preAction', (_thisCommand, actionCommand) => { + if (!AUTO_SYNC_EXEMPT.has(actionCommand.name())) autoSyncBeforeCommand(); + }); + task .command('create') .description('Create a task') @@ -557,4 +584,9 @@ export function registerV5TaskCommands(v5: Command): void { .description('Restore database state from an export snapshot (default: .genie/roadmap.json)') .option('--replace', 'Overwrite existing state instead of refusing a non-empty database') .action((file: string | undefined, opts: ImportOptions) => handleImport(file, opts)); + + task + .command('sync') + .description('Reconcile genie.db with the canonical .genie/roadmap.json (imports, exports, or warns on divergence)') + .action(() => handleSync()); } From a7784828816ab9e31a2e496dd1bfc5ffde68fd87 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Jul 2026 20:18:29 +0000 Subject: [PATCH 06/22] chore(version): bump to 5.260728.10 [auto-version] --- .claude-plugin/marketplace.json | 2 +- package.json | 2 +- plugins/genie/.claude-plugin/plugin.json | 2 +- plugins/genie/.codex-plugin/plugin.json | 2 +- plugins/genie/package.json | 2 +- plugins/hermes-genie/plugin.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 7ca7db4c0..4e01cf9f5 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "genie", - "version": "5.260728.9", + "version": "5.260728.10", "source": "./plugins/genie", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project." } diff --git a/package.json b/package.json index 57ab2fb12..381aa7b20 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@automagik/genie", - "version": "5.260728.9", + "version": "5.260728.10", "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", "license": "MIT", diff --git a/plugins/genie/.claude-plugin/plugin.json b/plugins/genie/.claude-plugin/plugin.json index 0ac2b2e66..4ac5e9b35 100644 --- a/plugins/genie/.claude-plugin/plugin.json +++ b/plugins/genie/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260728.9", + "version": "5.260728.10", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.", "author": { "name": "Namastex Labs" diff --git a/plugins/genie/.codex-plugin/plugin.json b/plugins/genie/.codex-plugin/plugin.json index 6b5c199aa..63af7e73b 100644 --- a/plugins/genie/.codex-plugin/plugin.json +++ b/plugins/genie/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260728.9", + "version": "5.260728.10", "description": "Plan, execute, review, and ship software with Genie workflows in Codex.", "author": { "name": "Namastex Labs", diff --git a/plugins/genie/package.json b/plugins/genie/package.json index a7219dd80..923bf5c33 100644 --- a/plugins/genie/package.json +++ b/plugins/genie/package.json @@ -1,6 +1,6 @@ { "name": "genie-plugin", - "version": "5.260728.9", + "version": "5.260728.10", "private": true, "description": "Runtime dependencies for genie bundled CLIs", "license": "MIT", diff --git a/plugins/hermes-genie/plugin.yaml b/plugins/hermes-genie/plugin.yaml index 058555df5..b78055d9e 100644 --- a/plugins/hermes-genie/plugin.yaml +++ b/plugins/hermes-genie/plugin.yaml @@ -1,5 +1,5 @@ name: genie -version: 5.260728.9 +version: 5.260728.10 description: "Native Hermes surface for Genie orchestration: read-only status plus work-plan and review-plan gap tools that the MCP board surface does not cover, hooks, commands, and a thin cockpit skill. Board/task truth comes from the genie MCP tools." provides_tools: # Default surface: exactly the three gap tools the MCP board surface does not cover. From 57cf4aaf04ffccba5459ed7a36116792faedb5f9 Mon Sep 17 00:00:00 2001 From: namastex888 Date: Tue, 28 Jul 2026 18:12:35 -0300 Subject: [PATCH 07/22] =?UTF-8?q?fix(skills):=20proportional-validation-po?= =?UTF-8?q?licy=20=E2=80=94=20passing=20full=20suite=20is=20always=20valid?= =?UTF-8?q?=20evidence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - work/review: a passing full-suite run counts as evidence; a missing scope rationale is a write-up gap (at most MEDIUM), never by itself grounds for FIX-FIRST - wish/work/review: a repository-documented gate is by itself sufficient justification for its scope - align dispatch-brief validation example and wish acceptance criterion - mirrors regenerated via sync-plugin-skills --write --- .genie/wishes/proportional-validation-policy/WISH.md | 3 ++- plugins/genie/skills/review/SKILL.md | 7 ++++--- plugins/genie/skills/wish/SKILL.md | 5 +++-- plugins/genie/skills/work/SKILL.md | 8 +++++--- skills/review/SKILL.md | 7 ++++--- skills/wish/SKILL.md | 5 +++-- skills/work/SKILL.md | 8 +++++--- 7 files changed, 26 insertions(+), 17 deletions(-) diff --git a/.genie/wishes/proportional-validation-policy/WISH.md b/.genie/wishes/proportional-validation-policy/WISH.md index da2e88efc..ff8217483 100644 --- a/.genie/wishes/proportional-validation-policy/WISH.md +++ b/.genie/wishes/proportional-validation-policy/WISH.md @@ -104,7 +104,8 @@ model and effort in runtime session/agent configuration, never skill frontmatter - [x] Shared runtime/core behavior, dependency/lockfile, generated executable/runtime artifact, config/schema, CI/release, broad refactor, or uncertain impact explicitly widen to `bun run check` and any affected build/e2e gate; deterministic documentation/plugin mirrors may use focused generator, parity, and content checks. -- [x] Review rejects both under-validation and unexplained full-suite validation. +- [x] Review rejects under-validation and zero validation; a passing full suite missing only its scope rationale is at + most a MEDIUM gap, never by itself a blocker. - [x] Canonical and mirrored skills pass their focused structural/parity checks. **Validation:** diff --git a/plugins/genie/skills/review/SKILL.md b/plugins/genie/skills/review/SKILL.md index b628c23f6..0c662d959 100644 --- a/plugins/genie/skills/review/SKILL.md +++ b/plugins/genie/skills/review/SKILL.md @@ -29,8 +29,9 @@ When spawned as a reviewer subagent, your dispatch prompt carries the curated sc focused behavior tests and add type, lint, or build checks for boundaries reached. Shared runtime/core behavior, dependency or lockfile, generated executable or runtime artifact, configuration or schema, CI or release, broad-refactor, or uncertain-impact changes require the repository full gate plus affected build or end-to-end checks. - Reject zero validation, under-validation, and unexplained full-suite validation. Preserve required aggregate - integration and release gates. + Reject zero validation and under-validation. A passing full-suite run is always valid evidence; a missing scope + rationale is at most a MEDIUM gap, never by itself grounds for FIX-FIRST. A repository-documented gate is by itself + sufficient justification for its scope. Preserve required aggregate integration and release gates. 5. **Tag gaps** — classify every unmet criterion by severity. 6. **Return verdict** — SHIP, FIX-FIRST, or BLOCKED, with exact fixes (files, commands, what to change) for each gap. @@ -78,7 +79,7 @@ If an ordinary reviewer and the `final-gate` disagree, log an appeal with the wi ### Execution Review (after `work`) - [ ] All acceptance criteria met with evidence - [ ] Validation commands run and passing; their recorded scope matches the actual diff's risk and reach -- [ ] No under-validation, zero validation, or unexplained full-suite validation +- [ ] No under-validation or zero validation; full-suite runs carry a scope rationale (a missing rationale is at most MEDIUM) - [ ] No scope creep — only wish-scoped changes - [ ] Work is auditable — commands and outcomes captured - [ ] Quality pass: security, maintainability, correctness diff --git a/plugins/genie/skills/wish/SKILL.md b/plugins/genie/skills/wish/SKILL.md index fc6293839..394354462 100644 --- a/plugins/genie/skills/wish/SKILL.md +++ b/plugins/genie/skills/wish/SKILL.md @@ -64,8 +64,9 @@ node "/references/design-review-evidence.mjs" verify ".genie/bra or content-contract checks; runtime groups use focused behavior tests and add type, lint, or build checks only for boundaries they reach. Escalate shared runtime/core behavior, dependency or lockfile, generated executable or runtime artifact, configuration or schema, CI or release, broad-refactor, or uncertain-impact groups to the repository full - gate plus affected build or end-to-end checks. State why the command scope fits; do not prescribe an unexplained full - suite. Preserve any repository-defined aggregate integration or release gate separately from per-group validation. + gate plus affected build or end-to-end checks. State why the command scope fits; a repository-documented + gate is by itself sufficient justification for its scope. Preserve any repository-defined aggregate integration or + release gate separately from per-group validation. 8. **Declare dependencies:** use the wish-level `## Dependencies` keys `**depends-on:** ` and `**blocks:** ` for cross-wish edges. Keep diff --git a/plugins/genie/skills/work/SKILL.md b/plugins/genie/skills/work/SKILL.md index b1ec698d4..8ffc43472 100644 --- a/plugins/genie/skills/work/SKILL.md +++ b/plugins/genie/skills/work/SKILL.md @@ -34,8 +34,10 @@ When you are spawned as a subagent for a group, your dispatch prompt carries the relevant format, link, example, generator, parity, or content-contract checks; runtime changes use focused behavior tests and add type, lint, or build checks for boundaries reached. Shared runtime/core behavior, dependency or lockfile, generated executable or runtime artifact, configuration or schema, CI or release, broad-refactor, or uncertain-impact - changes require the repository full gate plus affected build or end-to-end checks. Validation may never be zero, and - an unexplained full-suite run is not acceptable evidence. Preserve required aggregate integration and release gates. + changes require the repository full gate plus affected build or end-to-end checks. Validation may never be zero. A + passing full-suite run is always valid evidence — record why that scope was chosen; a missing scope rationale is a + gap in the write-up, not in the validation. A repository-documented gate is by itself sufficient justification for + its scope. Preserve required aggregate integration and release gates. 8. **Group done** — only after clean review AND passing validation: ```bash genie task done @@ -80,7 +82,7 @@ Extract the group's context from WISH.md and paste it into the dispatch prompt 1. **Goal** — one sentence 2. **Deliverables** — the numbered list of concrete outputs 3. **Acceptance criteria** — the checkboxes to satisfy -4. **Validation command** — the exact command proving the work (e.g. `bun run check`) +4. **Validation command** — the exact command proving the work, with its scope rationale (e.g. `bun run check` because the group touches shared runtime behavior) 5. **Depends-on** — what the engineer may assume already exists 6. **File scope** — the group's explicit file scope (the paths it owns), so disjoint ownership is legible to the engineer rather than inferred 7. **Stop conditions** — claim the task first; report blocked instead of expanding scope; end with an outcome word (see Session close) diff --git a/skills/review/SKILL.md b/skills/review/SKILL.md index b628c23f6..0c662d959 100644 --- a/skills/review/SKILL.md +++ b/skills/review/SKILL.md @@ -29,8 +29,9 @@ When spawned as a reviewer subagent, your dispatch prompt carries the curated sc focused behavior tests and add type, lint, or build checks for boundaries reached. Shared runtime/core behavior, dependency or lockfile, generated executable or runtime artifact, configuration or schema, CI or release, broad-refactor, or uncertain-impact changes require the repository full gate plus affected build or end-to-end checks. - Reject zero validation, under-validation, and unexplained full-suite validation. Preserve required aggregate - integration and release gates. + Reject zero validation and under-validation. A passing full-suite run is always valid evidence; a missing scope + rationale is at most a MEDIUM gap, never by itself grounds for FIX-FIRST. A repository-documented gate is by itself + sufficient justification for its scope. Preserve required aggregate integration and release gates. 5. **Tag gaps** — classify every unmet criterion by severity. 6. **Return verdict** — SHIP, FIX-FIRST, or BLOCKED, with exact fixes (files, commands, what to change) for each gap. @@ -78,7 +79,7 @@ If an ordinary reviewer and the `final-gate` disagree, log an appeal with the wi ### Execution Review (after `work`) - [ ] All acceptance criteria met with evidence - [ ] Validation commands run and passing; their recorded scope matches the actual diff's risk and reach -- [ ] No under-validation, zero validation, or unexplained full-suite validation +- [ ] No under-validation or zero validation; full-suite runs carry a scope rationale (a missing rationale is at most MEDIUM) - [ ] No scope creep — only wish-scoped changes - [ ] Work is auditable — commands and outcomes captured - [ ] Quality pass: security, maintainability, correctness diff --git a/skills/wish/SKILL.md b/skills/wish/SKILL.md index fc6293839..394354462 100644 --- a/skills/wish/SKILL.md +++ b/skills/wish/SKILL.md @@ -64,8 +64,9 @@ node "/references/design-review-evidence.mjs" verify ".genie/bra or content-contract checks; runtime groups use focused behavior tests and add type, lint, or build checks only for boundaries they reach. Escalate shared runtime/core behavior, dependency or lockfile, generated executable or runtime artifact, configuration or schema, CI or release, broad-refactor, or uncertain-impact groups to the repository full - gate plus affected build or end-to-end checks. State why the command scope fits; do not prescribe an unexplained full - suite. Preserve any repository-defined aggregate integration or release gate separately from per-group validation. + gate plus affected build or end-to-end checks. State why the command scope fits; a repository-documented + gate is by itself sufficient justification for its scope. Preserve any repository-defined aggregate integration or + release gate separately from per-group validation. 8. **Declare dependencies:** use the wish-level `## Dependencies` keys `**depends-on:** ` and `**blocks:** ` for cross-wish edges. Keep diff --git a/skills/work/SKILL.md b/skills/work/SKILL.md index b1ec698d4..8ffc43472 100644 --- a/skills/work/SKILL.md +++ b/skills/work/SKILL.md @@ -34,8 +34,10 @@ When you are spawned as a subagent for a group, your dispatch prompt carries the relevant format, link, example, generator, parity, or content-contract checks; runtime changes use focused behavior tests and add type, lint, or build checks for boundaries reached. Shared runtime/core behavior, dependency or lockfile, generated executable or runtime artifact, configuration or schema, CI or release, broad-refactor, or uncertain-impact - changes require the repository full gate plus affected build or end-to-end checks. Validation may never be zero, and - an unexplained full-suite run is not acceptable evidence. Preserve required aggregate integration and release gates. + changes require the repository full gate plus affected build or end-to-end checks. Validation may never be zero. A + passing full-suite run is always valid evidence — record why that scope was chosen; a missing scope rationale is a + gap in the write-up, not in the validation. A repository-documented gate is by itself sufficient justification for + its scope. Preserve required aggregate integration and release gates. 8. **Group done** — only after clean review AND passing validation: ```bash genie task done @@ -80,7 +82,7 @@ Extract the group's context from WISH.md and paste it into the dispatch prompt 1. **Goal** — one sentence 2. **Deliverables** — the numbered list of concrete outputs 3. **Acceptance criteria** — the checkboxes to satisfy -4. **Validation command** — the exact command proving the work (e.g. `bun run check`) +4. **Validation command** — the exact command proving the work, with its scope rationale (e.g. `bun run check` because the group touches shared runtime behavior) 5. **Depends-on** — what the engineer may assume already exists 6. **File scope** — the group's explicit file scope (the paths it owns), so disjoint ownership is legible to the engineer rather than inferred 7. **Stop conditions** — claim the task first; report blocked instead of expanding scope; end with an outcome word (see Session close) From f743ec39795af9c86b1935891c8343aed91f1f86 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Jul 2026 21:29:00 +0000 Subject: [PATCH 08/22] chore(version): bump to 5.260728.11 [auto-version] --- .claude-plugin/marketplace.json | 2 +- package.json | 2 +- plugins/genie/.claude-plugin/plugin.json | 2 +- plugins/genie/.codex-plugin/plugin.json | 2 +- plugins/genie/package.json | 2 +- plugins/hermes-genie/plugin.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 4e01cf9f5..0c5e2a1c8 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "genie", - "version": "5.260728.10", + "version": "5.260728.11", "source": "./plugins/genie", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project." } diff --git a/package.json b/package.json index 381aa7b20..64cbcf733 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@automagik/genie", - "version": "5.260728.10", + "version": "5.260728.11", "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", "license": "MIT", diff --git a/plugins/genie/.claude-plugin/plugin.json b/plugins/genie/.claude-plugin/plugin.json index 4ac5e9b35..03ac47ce9 100644 --- a/plugins/genie/.claude-plugin/plugin.json +++ b/plugins/genie/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260728.10", + "version": "5.260728.11", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.", "author": { "name": "Namastex Labs" diff --git a/plugins/genie/.codex-plugin/plugin.json b/plugins/genie/.codex-plugin/plugin.json index 63af7e73b..c44c5cf26 100644 --- a/plugins/genie/.codex-plugin/plugin.json +++ b/plugins/genie/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260728.10", + "version": "5.260728.11", "description": "Plan, execute, review, and ship software with Genie workflows in Codex.", "author": { "name": "Namastex Labs", diff --git a/plugins/genie/package.json b/plugins/genie/package.json index 923bf5c33..788463a7f 100644 --- a/plugins/genie/package.json +++ b/plugins/genie/package.json @@ -1,6 +1,6 @@ { "name": "genie-plugin", - "version": "5.260728.10", + "version": "5.260728.11", "private": true, "description": "Runtime dependencies for genie bundled CLIs", "license": "MIT", diff --git a/plugins/hermes-genie/plugin.yaml b/plugins/hermes-genie/plugin.yaml index b78055d9e..ab0e59b4b 100644 --- a/plugins/hermes-genie/plugin.yaml +++ b/plugins/hermes-genie/plugin.yaml @@ -1,5 +1,5 @@ name: genie -version: 5.260728.10 +version: 5.260728.11 description: "Native Hermes surface for Genie orchestration: read-only status plus work-plan and review-plan gap tools that the MCP board surface does not cover, hooks, commands, and a thin cockpit skill. Board/task truth comes from the genie MCP tools." provides_tools: # Default surface: exactly the three gap tools the MCP board surface does not cover. From e2659dce025804ea2465c68c07e828aef8736783 Mon Sep 17 00:00:00 2001 From: Felipe Rosa Date: Tue, 28 Jul 2026 18:31:48 -0300 Subject: [PATCH 09/22] refactor(task): sync via git hooks only; scope roadmap.json to roadmap content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the preAction auto-sync from task/board commands — reconciliation now runs where state actually moves: post-merge/post-rewrite import a pulled snapshot, pre-commit exports local board changes, both fail-open and divergence-refusing. Scope the published snapshot to roadmap content: hire_roster (machine-local worktree paths) is excluded from roadmap.json and preserved untouched by canonical imports; plain `task export` stdout remains the complete database dump. Co-Authored-By: Claude Fable 5 --- .genie/INDEX.md | 2 +- .husky/post-merge | 10 +++++++ .husky/post-rewrite | 10 +++++++ .husky/pre-commit | 5 ++-- CLAUDE.md | 4 +-- src/lib/v5/roadmap-sync.ts | 48 ++++++++++++------------------ src/lib/v5/task-state.ts | 30 +++++++++++-------- src/term-commands/v5-board.ts | 5 ---- src/term-commands/v5-task.test.ts | 49 ++++++++++++++++++++++++------- src/term-commands/v5-task.ts | 23 +++++++-------- 10 files changed, 110 insertions(+), 76 deletions(-) create mode 100755 .husky/post-merge create mode 100755 .husky/post-rewrite diff --git a/.genie/INDEX.md b/.genie/INDEX.md index ce3b97780..4e0367edc 100644 --- a/.genie/INDEX.md +++ b/.genie/INDEX.md @@ -1,6 +1,6 @@ # Plans Index -> **Board snapshot (canonical):** [roadmap.json](roadmap.json) is the canonical roadmap board (`genie board`); the local `.genie/genie.db` is a materialization of it. Every `genie board`/`genie task` command auto-syncs three-way: a pulled snapshot imports into genie.db, local board mutations export back (and the pre-commit hook commits the refresh), and if BOTH moved since the last sync genie touches nothing and warns — resolve with `genie task import --replace` (take the snapshot) or `genie task export --write` (keep the local board). Fresh clone: just run `genie board`. +> **Board snapshot (canonical):** [roadmap.json](roadmap.json) is the canonical roadmap board (`genie board`); the local `.genie/genie.db` materializes from it. Git hooks keep them reconciled via three-way `genie task sync`: post-merge/post-rewrite import a pulled snapshot, pre-commit exports local board changes into the commit, and if BOTH moved since the last sync nothing is touched and the hook warns — resolve with `genie task import --replace` (take the snapshot) or `genie task export --write` (keep the local board). Fresh clone: run `genie task sync` once. Scope: roadmap content only (tasks, dependencies, wish groups, boards, timelines) — `hire_roster` worktree state stays machine-local. ## Raw diff --git a/.husky/post-merge b/.husky/post-merge new file mode 100755 index 000000000..4a2ec733c --- /dev/null +++ b/.husky/post-merge @@ -0,0 +1,10 @@ +# Reconcile the local genie.db with the canonical board snapshot +# (.genie/roadmap.json) after a pull delivered a new snapshot. Three-way sync: +# imports when only the snapshot moved, exports when only the local board +# moved, refuses and warns on divergence. Fail-open; skipped in linked +# worktrees (the snapshot lives in the main checkout). +git_dir=$(git rev-parse --path-format=absolute --git-dir 2>/dev/null) +git_common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) +if [ "$git_dir" = "$git_common" ] && [ -f src/genie.ts ] && [ -f .genie/roadmap.json ]; then + bun src/genie.ts task sync || true +fi diff --git a/.husky/post-rewrite b/.husky/post-rewrite new file mode 100755 index 000000000..4a2ec733c --- /dev/null +++ b/.husky/post-rewrite @@ -0,0 +1,10 @@ +# Reconcile the local genie.db with the canonical board snapshot +# (.genie/roadmap.json) after a pull delivered a new snapshot. Three-way sync: +# imports when only the snapshot moved, exports when only the local board +# moved, refuses and warns on divergence. Fail-open; skipped in linked +# worktrees (the snapshot lives in the main checkout). +git_dir=$(git rev-parse --path-format=absolute --git-dir 2>/dev/null) +git_common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) +if [ "$git_dir" = "$git_common" ] && [ -f src/genie.ts ] && [ -f .genie/roadmap.json ]; then + bun src/genie.ts task sync || true +fi diff --git a/.husky/pre-commit b/.husky/pre-commit index 9505697ab..88ef9cb85 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -16,10 +16,11 @@ fi # moved, exports when only the local board moved, and refuses to touch anything # on divergence — so a stale local db can never clobber the roadmap. Fail-open: # sync trouble warns but never blocks a commit. Skipped in linked worktrees -# (the snapshot lives in the main checkout) and when no genie.db exists yet. +# (the snapshot lives in the main checkout). post-merge/post-rewrite run the +# same sync after pulls. git_dir=$(git rev-parse --path-format=absolute --git-dir 2>/dev/null) git_common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) -if [ "$git_dir" = "$git_common" ] && [ -f .genie/genie.db ] && [ -f src/genie.ts ]; then +if [ "$git_dir" = "$git_common" ] && [ -f src/genie.ts ]; then if bun src/genie.ts task sync; then [ -f .genie/roadmap.json ] && git add .genie/roadmap.json else diff --git a/CLAUDE.md b/CLAUDE.md index e1b0106d7..0c477efb0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -102,7 +102,7 @@ genie task done # Orchestrator only: mark reviewed work do genie task export # Emit the complete DB state as JSON genie task export --write # Write .genie/roadmap.json (diverged-sync resolution: keep local board) genie task import [--replace] # Restore genie.db from .genie/roadmap.json (resolution: take snapshot) -genie task sync # Three-way reconcile genie.db <-> roadmap.json (auto-runs before task/board verbs) +genie task sync # Three-way reconcile genie.db <-> roadmap.json (run by git hooks on pull/commit) ``` ### Omni subcommands @@ -121,7 +121,7 @@ genie omni inbox # List stored inbound Omni messages (no ne | Task / board / wish state | `/.genie/genie.db` | Per-repo, shared across worktrees | SQLite (bun:sqlite) | | Omni approvals + inbox | `~/.genie/genie.db` | Global (machine-wide) | SQLite (bun:sqlite) | | Wishes / brainstorms / INDEX | `/.genie/{wishes,brainstorms,INDEX.md}` | Per-repo, git-tracked | Markdown | -| Board snapshot (CANONICAL roadmap) | `/.genie/roadmap.json` | Per-repo, git-tracked | JSON — genie.db materializes from it via three-way `task sync` (auto before task/board verbs; machine-local baseline in gitignored `.genie/roadmap-sync`) | +| Board snapshot (CANONICAL roadmap) | `/.genie/roadmap.json` | Per-repo, git-tracked | JSON — genie.db materializes from it via three-way `task sync` (git hooks: post-merge/post-rewrite/pre-commit; baseline in gitignored `.genie/roadmap-sync`; excludes machine-local `hire_roster`) | Worktrees share the main repo's `.genie/genie.db` via `git rev-parse --git-common-dir`. The two `genie.db` files are wholly separate databases: different paths, different schemas, independent `PRAGMA user_version` — `global-db.ts` deliberately imports NONE of `genie-db.ts`'s path constants; the only shared code is the open primitive in `sqlite-open.ts`. Both use WAL. Documents live in git; operational state lives in SQLite. diff --git a/src/lib/v5/roadmap-sync.ts b/src/lib/v5/roadmap-sync.ts index 4ea26e525..2961687c1 100644 --- a/src/lib/v5/roadmap-sync.ts +++ b/src/lib/v5/roadmap-sync.ts @@ -23,8 +23,8 @@ import type { Database } from 'bun:sqlite'; import { createHash } from 'node:crypto'; import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; -import { openDb, resolveRepoRoot, resolveRoadmapPath } from './genie-db.js'; -import { exportState, hasOperationalState, importState } from './task-state.js'; +import { resolveRepoRoot, resolveRoadmapPath } from './genie-db.js'; +import { type StateExport, exportState, hasOperationalState, importState } from './task-state.js'; export type SyncAction = 'none' | 'imported' | 'exported' | 'diverged'; @@ -44,6 +44,16 @@ function canonicalHash(value: unknown): string { return createHash('sha256').update(JSON.stringify(value)).digest('hex'); } +/** + * The published slice of the database: everything EXCEPT hire_roster, whose + * rows carry machine-local worktree paths that must never travel between + * machines. This is what roadmap.json holds and what sync hashes compare — + * local hires can neither dirty the snapshot nor be destroyed by an import. + */ +export function roadmapSnapshot(db: Database): StateExport { + return { ...exportState(db), hire_roster: [] }; +} + interface SyncMarker { fileHash: string; dbHash: string; @@ -78,7 +88,7 @@ function serializeSnapshot(state: unknown): string { */ export function recordSyncBaseline(db: Database, cwd?: string): void { const filePath = resolveRoadmapPath(cwd); - const dbHash = canonicalHash(exportState(db)); + const dbHash = canonicalHash(roadmapSnapshot(db)); let fileHash = dbHash; if (existsSync(filePath)) { try { @@ -93,13 +103,13 @@ export function recordSyncBaseline(db: Database, cwd?: string): void { /** * Reconcile genie.db with the canonical roadmap.json. Acts only when exactly * one side changed since the last baseline (see module doc); never destroys - * state on divergence. Cheap enough to run before every board/task command: - * one full-state serialization of a small database plus one file hash. + * state on divergence. Invoked by `genie task sync`, which the git hooks run + * on pull (post-merge / post-rewrite) and before commit (pre-commit). */ export function syncRoadmap(db: Database, cwd?: string): SyncResult { const filePath = resolveRoadmapPath(cwd); const markerPath = resolveSyncMarkerPath(cwd); - const dbState = exportState(db); + const dbState = roadmapSnapshot(db); const dbHash = canonicalHash(dbState); if (!existsSync(filePath)) { @@ -130,8 +140,8 @@ export function syncRoadmap(db: Database, cwd?: string): SyncResult { const dbChanged = marker === null || dbHash !== marker.dbHash; if (fileChanged && (!dbChanged || !hasOperationalState(db))) { - importState(db, parsed, { replace: true }); - writeMarker(markerPath, { fileHash, dbHash: canonicalHash(exportState(db)) }); + importState(db, parsed, { replace: true, preserveHireRoster: true }); + writeMarker(markerPath, { fileHash, dbHash: canonicalHash(roadmapSnapshot(db)) }); return { action: 'imported', message: `Board refreshed from ${filePath}.` }; } if (dbChanged && !fileChanged) { @@ -146,25 +156,3 @@ export function syncRoadmap(db: Database, cwd?: string): SyncResult { message: `Both the local board (genie.db) and ${filePath} changed since the last sync. Nothing was overwritten. ${resolution}`, }; } - -/** - * Fail-open sync for command preAction hooks: reconcile silently (an import or - * export that worked needs no announcement, and stdout must stay clean for - * --json consumers), warn on stderr only when the sides diverged, and never - * let a sync failure break the board/task command the user actually ran. - */ -export function autoSyncBeforeCommand(): void { - try { - const db = openDb(); - try { - const result = syncRoadmap(db); - if (result.action === 'diverged' && result.message) { - process.stderr.write(`warn: ${result.message}\n`); - } - } finally { - db.close(); - } - } catch { - // Sync is a convenience layer; the command itself surfaces real db errors. - } -} diff --git a/src/lib/v5/task-state.ts b/src/lib/v5/task-state.ts index f0551a199..a122640e1 100644 --- a/src/lib/v5/task-state.ts +++ b/src/lib/v5/task-state.ts @@ -1558,28 +1558,32 @@ function insertSnapshotRows(db: Database, state: StateExport): void { * transaction. Row ids (including event/stage autoincrement ids) are preserved * exactly, so an export → import round-trip is lossless. */ -export function importState(db: Database, snapshot: unknown, opts: { replace?: boolean } = {}): ImportSummary { +export interface ImportOptions { + replace?: boolean; + /** + * Leave the local hire_roster untouched (neither wiped nor inserted). Set for + * roadmap-scoped snapshots: hires carry machine-local worktree paths that + * must never travel between machines. + */ + preserveHireRoster?: boolean; +} + +export function importState(db: Database, snapshot: unknown, opts: ImportOptions = {}): ImportSummary { const state = validateSnapshot(db, snapshot); + const hires = opts.preserveHireRoster ? [] : state.hire_roster; const apply = db.transaction(() => { if (hasOperationalState(db)) { if (!opts.replace) throw new NonEmptyImportError(); // Children before parents so FK cascades never fire mid-wipe. Meta is // wiped too: replace means EXACT snapshot state, and stale local // wish_sig/backfill markers would misdescribe the imported rows. - for (const table of [ - 'task_events', - 'stage_log', - 'task_dependencies', - 'tasks', - 'hire_roster', - 'wish_groups', - 'boards', - 'meta', - ]) { + const wipe = ['task_events', 'stage_log', 'task_dependencies', 'tasks', 'wish_groups', 'boards', 'meta']; + if (!opts.preserveHireRoster) wipe.splice(4, 0, 'hire_roster'); + for (const table of wipe) { db.query(`DELETE FROM ${table}`).run(); } } - insertSnapshotRows(db, state); + insertSnapshotRows(db, { ...state, hire_roster: hires }); }); apply(); return { @@ -1588,6 +1592,6 @@ export function importState(db: Database, snapshot: unknown, opts: { replace?: b dependencies: state.task_dependencies.length, events: state.task_events.length, wishGroups: state.wish_groups.length, - hires: state.hire_roster.length, + hires: hires.length, }; } diff --git a/src/term-commands/v5-board.ts b/src/term-commands/v5-board.ts index 3d3eacf48..77d03abcb 100644 --- a/src/term-commands/v5-board.ts +++ b/src/term-commands/v5-board.ts @@ -12,7 +12,6 @@ import type { Command } from 'commander'; import { color, padRight, truncate } from '../lib/term-format.js'; import { cardBadges } from '../lib/v5/card-render.js'; import { openDb } from '../lib/v5/genie-db.js'; -import { autoSyncBeforeCommand } from '../lib/v5/roadmap-sync.js'; import { type BoardRow, DEFAULT_LIFECYCLE_LANES, @@ -319,10 +318,6 @@ export function registerV5BoardCommands(v5: Command): void { .option('--json', 'Output as JSON') .action((opts: BoardOptions) => handleBoard(opts)); - // roadmap.json is canonical: reconcile before rendering so a freshly pulled - // snapshot (or fresh clone) materializes into genie.db first (fail-open). - board.hook('preAction', () => autoSyncBeforeCommand()); - board .command('create ') .description('Create a board (defaults to the 6 lifecycle lanes)') diff --git a/src/term-commands/v5-task.test.ts b/src/term-commands/v5-task.test.ts index 1490e5eaa..7f64782d0 100644 --- a/src/term-commands/v5-task.test.ts +++ b/src/term-commands/v5-task.test.ts @@ -23,6 +23,7 @@ import { getTaskCard, getTaskEvents, getTaskLane, + hireAgent, } from '../lib/v5/task-state.js'; const GENIE = join(import.meta.dir, '..', 'genie.ts'); @@ -392,7 +393,7 @@ describe('roadmap.json canonical sync', () => { writeFileSync(join(dir, '.genie', 'roadmap.json'), snapshot); } - test('fresh clone: any task command materializes the board from the snapshot', async () => { + test('fresh clone: one `task sync` materializes the board from the snapshot', async () => { const db = openDb({ cwd: repo }); createTask(db, { title: 'canonical card' }); db.close(); @@ -406,7 +407,10 @@ describe('roadmap.json canonical sync', () => { git(clone, 'commit', '--allow-empty', '-m', 'init'); await plantSnapshot(clone, snapshotOf(repo)); - // No genie.db exists in the clone yet: `task list` must auto-import. + // No genie.db exists in the clone yet: sync must bootstrap it. + const synced = await cli(clone, 'sync'); + expect(synced.code).toBe(0); + expect(synced.stdout).toContain('Board refreshed'); const r = await cli(clone, 'list'); expect(r.code).toBe(0); expect(r.stderr).toBe(''); @@ -416,7 +420,30 @@ describe('roadmap.json canonical sync', () => { } }); - test('pulled snapshot auto-imports; local mutation auto-exports; divergence is refused then resolvable', async () => { + test('snapshot excludes hire_roster; canonical import preserves local hires', async () => { + const db = openDb({ cwd: repo }); + createTask(db, { title: 'card' }); + hireAgent(db, { wish: 'w', agentAdapterId: 'claude', worktree: '/tmp/wt' }); + db.close(); + + const w = await cli(repo, 'export', '--write'); + expect(w.code).toBe(0); + const snap = JSON.parse(snapshotOf(repo)) as StateExport; + expect(snap.hire_roster).toEqual([]); + expect(snap.tasks).toHaveLength(1); + + const r = await cli(repo, 'import', '--replace'); + expect(r.code).toBe(0); + const db2 = openDb({ cwd: repo }); + const hires = db2.query('SELECT wish, worktree FROM hire_roster').all() as Array<{ + wish: string; + worktree: string; + }>; + db2.close(); + expect(hires).toEqual([{ wish: 'w', worktree: '/tmp/wt' }]); + }); + + 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 }); createTask(db, { title: 'first card' }); @@ -435,13 +462,16 @@ describe('roadmap.json canonical sync', () => { git(clone, 'init', '-b', 'main'); git(clone, 'commit', '--allow-empty', '-m', 'init'); await plantSnapshot(clone, f1); - await cli(clone, 'list'); // materialize + baseline + await cli(clone, 'sync'); // materialize + baseline - // Simulated pull: F2 arrives while B's db is untouched → auto-import. + // Simulated pull: F2 arrives while B's db is untouched → sync imports + // (post-merge/post-rewrite run this after real pulls). writeFileSync(join(clone, '.genie', 'roadmap.json'), f2); - const pulled = await cli(clone, 'list'); - expect(pulled.stderr).toBe(''); - expect(pulled.stdout).toContain('second card'); + const pulled = await cli(clone, 'sync'); + expect(pulled.code).toBe(0); + expect(pulled.stdout).toContain('Board refreshed'); + const listed2 = await cli(clone, 'list'); + expect(listed2.stdout).toContain('second card'); // Local mutation on B → db ahead → sync exports. const created = await cli(clone, 'create', '--title', 'b-only card'); @@ -458,8 +488,7 @@ describe('roadmap.json canonical sync', () => { expect(diverged.stdout).toContain('Nothing was overwritten'); expect(snapshotOf(clone)).toBe(f2); // snapshot untouched const listed = await cli(clone, 'list'); - expect(listed.stderr).toContain('warn:'); // ordinary commands warn… - expect(listed.stdout).toContain('b-diverging card'); // …and keep local state + expect(listed.stdout).toContain('b-diverging card'); // local state kept // Resolution: take the snapshot wholesale. const resolved = await cli(clone, 'import', '--replace'); diff --git a/src/term-commands/v5-task.ts b/src/term-commands/v5-task.ts index 43ed16b32..ca63743d9 100644 --- a/src/term-commands/v5-task.ts +++ b/src/term-commands/v5-task.ts @@ -13,6 +13,7 @@ * task checkout [--worker ] * task export [--write [file]] * task import [file] [--replace] + * task sync */ import type { Database } from 'bun:sqlite'; @@ -21,7 +22,7 @@ import type { Command } from 'commander'; import { color, formatTimestamp, padRight, truncate } from '../lib/term-format.js'; import { livenessBadge } from '../lib/v5/card-render.js'; import { openDb, resolveRoadmapPath } from '../lib/v5/genie-db.js'; -import { autoSyncBeforeCommand, recordSyncBaseline, syncRoadmap } from '../lib/v5/roadmap-sync.js'; +import { recordSyncBaseline, roadmapSnapshot, syncRoadmap } from '../lib/v5/roadmap-sync.js'; import { type EventAuthor, type TaskCardRow, @@ -427,7 +428,10 @@ function handleExport(opts: ExportOptions): void { run(() => { const db = openDb(); try { - const json = `${JSON.stringify(exportState(db), null, 2)}\n`; + // --write publishes the roadmap slice (no hire_roster — machine-local + // worktree paths); the plain stdout dump stays the complete database. + const state = opts.write ? roadmapSnapshot(db) : exportState(db); + const json = `${JSON.stringify(state, null, 2)}\n`; if (opts.write) { const target = typeof opts.write === 'string' ? opts.write : resolveRoadmapPath(); writeFileSync(target, json); @@ -475,8 +479,10 @@ function handleImport(file: string | undefined, opts: ImportOptions): void { } const db = openDb(); try { - const summary = importState(db, snapshot, { replace: opts.replace }); - if (source === resolveRoadmapPath()) recordSyncBaseline(db); + // The canonical snapshot is roadmap-scoped: local hires stay untouched. + const canonical = source === resolveRoadmapPath(); + const summary = importState(db, snapshot, { replace: opts.replace, preserveHireRoster: canonical }); + if (canonical) recordSyncBaseline(db); out( `Imported ${summary.tasks} tasks, ${summary.boards} boards, ${summary.dependencies} dependencies, ${summary.events} events, ${summary.wishGroups} wish groups, ${summary.hires} hires from ${source}.`, ); @@ -490,18 +496,9 @@ function handleImport(file: string | undefined, opts: ImportOptions): void { // Registration // ============================================================================ -/** Subcommands that ARE the sync surface — auto-syncing first would tangle them. */ -const AUTO_SYNC_EXEMPT = new Set(['import', 'export', 'sync']); - export function registerV5TaskCommands(v5: Command): void { const task = v5.command('task').description('task state (SQLite, zero-daemon)'); - // roadmap.json is canonical: reconcile before every ordinary task verb so a - // pulled snapshot materializes and local mutations publish (fail-open). - task.hook('preAction', (_thisCommand, actionCommand) => { - if (!AUTO_SYNC_EXEMPT.has(actionCommand.name())) autoSyncBeforeCommand(); - }); - task .command('create') .description('Create a task') From 0696e9c6593a18fcbde27cd6fddd853a4383edf2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Jul 2026 21:40:40 +0000 Subject: [PATCH 10/22] chore(version): bump to 5.260728.12 [auto-version] --- .claude-plugin/marketplace.json | 2 +- package.json | 2 +- plugins/genie/.claude-plugin/plugin.json | 2 +- plugins/genie/.codex-plugin/plugin.json | 2 +- plugins/genie/package.json | 2 +- plugins/hermes-genie/plugin.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 0c5e2a1c8..6914a7ce9 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "genie", - "version": "5.260728.11", + "version": "5.260728.12", "source": "./plugins/genie", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project." } diff --git a/package.json b/package.json index 64cbcf733..678da957c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@automagik/genie", - "version": "5.260728.11", + "version": "5.260728.12", "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", "license": "MIT", diff --git a/plugins/genie/.claude-plugin/plugin.json b/plugins/genie/.claude-plugin/plugin.json index 03ac47ce9..b69c6c95b 100644 --- a/plugins/genie/.claude-plugin/plugin.json +++ b/plugins/genie/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260728.11", + "version": "5.260728.12", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.", "author": { "name": "Namastex Labs" diff --git a/plugins/genie/.codex-plugin/plugin.json b/plugins/genie/.codex-plugin/plugin.json index c44c5cf26..d9b11a35a 100644 --- a/plugins/genie/.codex-plugin/plugin.json +++ b/plugins/genie/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260728.11", + "version": "5.260728.12", "description": "Plan, execute, review, and ship software with Genie workflows in Codex.", "author": { "name": "Namastex Labs", diff --git a/plugins/genie/package.json b/plugins/genie/package.json index 788463a7f..b382ceb8e 100644 --- a/plugins/genie/package.json +++ b/plugins/genie/package.json @@ -1,6 +1,6 @@ { "name": "genie-plugin", - "version": "5.260728.11", + "version": "5.260728.12", "private": true, "description": "Runtime dependencies for genie bundled CLIs", "license": "MIT", diff --git a/plugins/hermes-genie/plugin.yaml b/plugins/hermes-genie/plugin.yaml index ab0e59b4b..124fe3dec 100644 --- a/plugins/hermes-genie/plugin.yaml +++ b/plugins/hermes-genie/plugin.yaml @@ -1,5 +1,5 @@ name: genie -version: 5.260728.11 +version: 5.260728.12 description: "Native Hermes surface for Genie orchestration: read-only status plus work-plan and review-plan gap tools that the MCP board surface does not cover, hooks, commands, and a thin cockpit skill. Board/task truth comes from the genie MCP tools." provides_tools: # Default surface: exactly the three gap tools the MCP board surface does not cover. From edc762bdfacfacfab79dd79fd0012cf1d288801b Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Tue, 28 Jul 2026 16:40:28 +0800 Subject: [PATCH 11/22] fix(router): bypass Genie when lifecycle adds no value --- plugins/genie/skills/genie/SKILL.md | 23 +++++++++++++++---- plugins/genie/skills/genie/agents/openai.yaml | 4 ++-- .../genie/skills/genie/reference/lifecycle.md | 8 ++++++- scripts/release-docs.test.ts | 20 ++++++++++++++++ skills/genie/SKILL.md | 23 +++++++++++++++---- skills/genie/agents/openai.yaml | 4 ++-- skills/genie/reference/lifecycle.md | 8 ++++++- 7 files changed, 74 insertions(+), 16 deletions(-) diff --git a/plugins/genie/skills/genie/SKILL.md b/plugins/genie/skills/genie/SKILL.md index 5540b1f3b..5da6abafc 100644 --- a/plugins/genie/skills/genie/SKILL.md +++ b/plugins/genie/skills/genie/SKILL.md @@ -1,13 +1,13 @@ --- name: genie -description: "Entry point for all genie operations — auto-routes natural language to the right skill, detects lifecycle state, and handles operational commands. Use when planning features, reporting bugs, orchestrating execution, or asking about genie." +description: "Entry point for Genie operations — resumes existing lifecycle state and orchestrates work that needs durable planning or coordination. Ordinary requests bypass Genie unless the user asks for it." --- # genie — Auto-Router **Runtime syntax:** in Codex, invoke the plugin copy with the owner-qualified `$genie:` selector; use bare `$` only when intentionally selecting a user-tier copy (a separately installed personal copy; Genie no longer seeds this tier). Claude Code and Hermes use `/`. Cross-skill prose below uses bare names as portable semantic routes; the orchestrator resolves the selector for the active tier. -You are the Automagik Genie — the single entry point for orchestration. Classify intent, detect existing lifecycle state, and route to the right skill or CLI command. State the chosen route in one line, then invoke it, passing the user's topic through as args. +You are the Automagik Genie — the single entry point for orchestration. First apply the lightweight bypass check below. Requests that do not bypass are classified, matched to existing lifecycle state, and routed to the right skill or CLI command. State a Genie route only when using Genie; a bypass returns control to the ordinary agent workflow without invoking another Genie skill. ## Bare skill invocation (no request text) @@ -20,9 +20,21 @@ ls .genie/brainstorms/*/DRAFT.md 2>/dev/null | wc -l # brainstorms simmering "You have X active wishes and Y brainstorms. What's your wish?" Classify the reply as below. +## Lightweight Bypass Check + +Do this before invoking any lifecycle skill: + +1. **Honor explicit Genie intent.** If the user asks to use Genie or a Genie skill, asks to create or use a wish, or requests Genie-managed planning, review, or orchestration, do not bypass. Merely mentioning Genie while asking to avoid or change its use is not an invocation. +2. **Check for related lifecycle work.** Make a cheap topic/slug comparison against `.genie/wishes/` and `.genie/brainstorms/`. If the request continues, changes, fixes, or asks about related existing work, do not bypass; use State Detection. +3. **Test whether the lifecycle adds value.** Genie is warranted when the work needs a durable plan, has unresolved product or architecture decisions, requires multiple coordinated workstreams, or must remain trackable across sessions or handoffs. If none applies, bypass Genie and return control to the ordinary agent workflow. + +The bypass must not create or update `.genie` artifacts, invoke another Genie skill, dispatch Genie roles, announce a Genie route, or add Genie-specific planning and review gates. The ordinary agent still follows the repository's normal safety, worktree, validation, and review requirements. + +This is a value gate, not a word or file-count heuristic. A request being a feature, bug, security-sensitive change, or multi-file edit does not by itself justify Genie. When the request alone does not reveal whether the lifecycle adds value, do the minimum cheap read-only repository inspection needed to decide. Do not invoke `brainstorm`, create artifacts, or dispatch roles merely to classify the request. Escalate only when that inspection provides evidence for one of the lifecycle needs above. + ## Intent Classification -Classify the user's request into exactly one category: +For requests that did not bypass, classify the user's request into exactly one category: | Category | Signal | Route | |----------|--------|-------| @@ -33,11 +45,11 @@ Classify the user's request into exactly one category: | **operational** | Task/board/cockpit operation: "show the board", "claim a task", "launch the cockpit" | Run the genie CLI command (see mapping) | | **question** | About genie itself: "how does X work?", "what commands exist?" | Answer from the live `--help` output and `reference/lifecycle.md` | -Unclear between fuzzy and concrete → default `brainstorm`; exploring first is cheaper than re-planning. +Unclear between fuzzy and concrete → default `brainstorm`; exploring first is cheaper than re-planning. Do not use this ambiguity rule to override the value gate. ## State Detection -Before routing concrete/fuzzy/explicit intents, check whether the topic matches existing work (`ls .genie/wishes/ .genie/brainstorms/ 2>/dev/null`, slug match). A match overrides the default route: +For requests that did not bypass, check whether the topic matches existing work (`ls .genie/wishes/ .genie/brainstorms/ 2>/dev/null`, slug match). A match overrides the default route: | Existing state | Override | |----------------|----------| @@ -90,5 +102,6 @@ Lifecycle flow, skill catalog, and the v5 execution model: resolve this skill's ## Rules - Guide, don't gatekeep — if the user wants to skip a step, note the risk and proceed. +- Pay lifecycle cost only when it adds value: default to ordinary execution, use cheap read-only triage for uncertainty, and escalate only for explicit intent, related lifecycle state, or demonstrated durable planning or coordination needs. - Pass the user's topic through to the invoked skill as args. - Every command you run must exist in help output captured during this session — never type a remembered verb that isn't there. diff --git a/plugins/genie/skills/genie/agents/openai.yaml b/plugins/genie/skills/genie/agents/openai.yaml index 553f043c3..f1244710a 100644 --- a/plugins/genie/skills/genie/agents/openai.yaml +++ b/plugins/genie/skills/genie/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Genie Router" - short_description: "Route requests to the right Genie lifecycle step" - default_prompt: "Route this request through the appropriate Genie workflow." + short_description: "Route work that warrants the Genie lifecycle" + default_prompt: "Use Genie only when explicitly requested, related to existing lifecycle work, or warranted by durable planning or coordination needs; otherwise bypass it." diff --git a/plugins/genie/skills/genie/reference/lifecycle.md b/plugins/genie/skills/genie/reference/lifecycle.md index 17ad6d7f3..d83680d10 100644 --- a/plugins/genie/skills/genie/reference/lifecycle.md +++ b/plugins/genie/skills/genie/reference/lifecycle.md @@ -1,12 +1,18 @@ # Genie Lifecycle & v5 Execution Model -Every piece of work follows this flow: +Genie-managed work follows this flow: ``` Idea → brainstorm → design review → wish → plan review → work → implementation review → PR → Ship (explore) (design gate) (plan) (plan gate) (build) (verify) ``` +Ordinary requests unrelated to an existing wish or brainstorm bypass this lifecycle unless the user explicitly asks for +Genie or the work demonstrably needs durable planning, unresolved product or architecture decisions, multiple coordinated +workstreams, or tracking across sessions. When uncertain, use cheap read-only inspection and escalate only when it finds +one of those needs. The bypass adds no Genie artifacts, roles, or gates; ordinary repository safety and validation rules +still apply. Related existing work always resumes through its persisted state. + The gates review different artifacts. For non-trivial work, `brainstorm` automatically routes the completed DESIGN.md through read-only design review before `wish` may consume it. The resulting WISH.md must then pass plan review and persist `APPROVED` before `work` starts. After execution, a different reviewer validates the implementation. Passing one gate diff --git a/scripts/release-docs.test.ts b/scripts/release-docs.test.ts index 0f721c00a..8c3c66be1 100644 --- a/scripts/release-docs.test.ts +++ b/scripts/release-docs.test.ts @@ -973,6 +973,26 @@ describe('Group E release and documentation contracts', () => { expect(work).toContain('A user-approved simplification invalidates the superseded plan/review evidence'); }); + 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'); + const metadata = read('skills/genie/agents/openai.yaml'); + + expect(router).toContain('## Lightweight Bypass Check'); + expect(router).toContain('Honor explicit Genie intent'); + expect(router).toContain('Merely mentioning Genie while asking to avoid or change its use is not an invocation'); + expect(router).toContain('Check for related lifecycle work'); + expect(router).toContain('Test whether the lifecycle adds value'); + expect(router).toContain('do the minimum cheap read-only repository inspection needed to decide'); + expect(router).toContain( + 'A request being a feature, bug, security-sensitive change, or multi-file edit does not by itself justify Genie', + ); + expect(router).toContain('must not create or update `.genie` artifacts'); + expect(lifecycle).toContain('Ordinary requests unrelated to an existing wish or brainstorm bypass this lifecycle'); + expect(lifecycle).toContain('Related existing work always resumes through its persisted state'); + expect(metadata).toContain('warranted by durable planning or coordination needs; otherwise bypass it'); + }); + test('wizard discloses init MCP writes and owner-qualified lifecycle order', () => { const wizard = read('skills/wizard/SKILL.md'); for (const path of ['.mcp.json', '.warp/.mcp.json', '.codex/config.toml']) expect(wizard).toContain(path); diff --git a/skills/genie/SKILL.md b/skills/genie/SKILL.md index 5540b1f3b..5da6abafc 100644 --- a/skills/genie/SKILL.md +++ b/skills/genie/SKILL.md @@ -1,13 +1,13 @@ --- name: genie -description: "Entry point for all genie operations — auto-routes natural language to the right skill, detects lifecycle state, and handles operational commands. Use when planning features, reporting bugs, orchestrating execution, or asking about genie." +description: "Entry point for Genie operations — resumes existing lifecycle state and orchestrates work that needs durable planning or coordination. Ordinary requests bypass Genie unless the user asks for it." --- # genie — Auto-Router **Runtime syntax:** in Codex, invoke the plugin copy with the owner-qualified `$genie:` selector; use bare `$` only when intentionally selecting a user-tier copy (a separately installed personal copy; Genie no longer seeds this tier). Claude Code and Hermes use `/`. Cross-skill prose below uses bare names as portable semantic routes; the orchestrator resolves the selector for the active tier. -You are the Automagik Genie — the single entry point for orchestration. Classify intent, detect existing lifecycle state, and route to the right skill or CLI command. State the chosen route in one line, then invoke it, passing the user's topic through as args. +You are the Automagik Genie — the single entry point for orchestration. First apply the lightweight bypass check below. Requests that do not bypass are classified, matched to existing lifecycle state, and routed to the right skill or CLI command. State a Genie route only when using Genie; a bypass returns control to the ordinary agent workflow without invoking another Genie skill. ## Bare skill invocation (no request text) @@ -20,9 +20,21 @@ ls .genie/brainstorms/*/DRAFT.md 2>/dev/null | wc -l # brainstorms simmering "You have X active wishes and Y brainstorms. What's your wish?" Classify the reply as below. +## Lightweight Bypass Check + +Do this before invoking any lifecycle skill: + +1. **Honor explicit Genie intent.** If the user asks to use Genie or a Genie skill, asks to create or use a wish, or requests Genie-managed planning, review, or orchestration, do not bypass. Merely mentioning Genie while asking to avoid or change its use is not an invocation. +2. **Check for related lifecycle work.** Make a cheap topic/slug comparison against `.genie/wishes/` and `.genie/brainstorms/`. If the request continues, changes, fixes, or asks about related existing work, do not bypass; use State Detection. +3. **Test whether the lifecycle adds value.** Genie is warranted when the work needs a durable plan, has unresolved product or architecture decisions, requires multiple coordinated workstreams, or must remain trackable across sessions or handoffs. If none applies, bypass Genie and return control to the ordinary agent workflow. + +The bypass must not create or update `.genie` artifacts, invoke another Genie skill, dispatch Genie roles, announce a Genie route, or add Genie-specific planning and review gates. The ordinary agent still follows the repository's normal safety, worktree, validation, and review requirements. + +This is a value gate, not a word or file-count heuristic. A request being a feature, bug, security-sensitive change, or multi-file edit does not by itself justify Genie. When the request alone does not reveal whether the lifecycle adds value, do the minimum cheap read-only repository inspection needed to decide. Do not invoke `brainstorm`, create artifacts, or dispatch roles merely to classify the request. Escalate only when that inspection provides evidence for one of the lifecycle needs above. + ## Intent Classification -Classify the user's request into exactly one category: +For requests that did not bypass, classify the user's request into exactly one category: | Category | Signal | Route | |----------|--------|-------| @@ -33,11 +45,11 @@ Classify the user's request into exactly one category: | **operational** | Task/board/cockpit operation: "show the board", "claim a task", "launch the cockpit" | Run the genie CLI command (see mapping) | | **question** | About genie itself: "how does X work?", "what commands exist?" | Answer from the live `--help` output and `reference/lifecycle.md` | -Unclear between fuzzy and concrete → default `brainstorm`; exploring first is cheaper than re-planning. +Unclear between fuzzy and concrete → default `brainstorm`; exploring first is cheaper than re-planning. Do not use this ambiguity rule to override the value gate. ## State Detection -Before routing concrete/fuzzy/explicit intents, check whether the topic matches existing work (`ls .genie/wishes/ .genie/brainstorms/ 2>/dev/null`, slug match). A match overrides the default route: +For requests that did not bypass, check whether the topic matches existing work (`ls .genie/wishes/ .genie/brainstorms/ 2>/dev/null`, slug match). A match overrides the default route: | Existing state | Override | |----------------|----------| @@ -90,5 +102,6 @@ Lifecycle flow, skill catalog, and the v5 execution model: resolve this skill's ## Rules - Guide, don't gatekeep — if the user wants to skip a step, note the risk and proceed. +- Pay lifecycle cost only when it adds value: default to ordinary execution, use cheap read-only triage for uncertainty, and escalate only for explicit intent, related lifecycle state, or demonstrated durable planning or coordination needs. - Pass the user's topic through to the invoked skill as args. - Every command you run must exist in help output captured during this session — never type a remembered verb that isn't there. diff --git a/skills/genie/agents/openai.yaml b/skills/genie/agents/openai.yaml index 553f043c3..f1244710a 100644 --- a/skills/genie/agents/openai.yaml +++ b/skills/genie/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Genie Router" - short_description: "Route requests to the right Genie lifecycle step" - default_prompt: "Route this request through the appropriate Genie workflow." + short_description: "Route work that warrants the Genie lifecycle" + default_prompt: "Use Genie only when explicitly requested, related to existing lifecycle work, or warranted by durable planning or coordination needs; otherwise bypass it." diff --git a/skills/genie/reference/lifecycle.md b/skills/genie/reference/lifecycle.md index 17ad6d7f3..d83680d10 100644 --- a/skills/genie/reference/lifecycle.md +++ b/skills/genie/reference/lifecycle.md @@ -1,12 +1,18 @@ # Genie Lifecycle & v5 Execution Model -Every piece of work follows this flow: +Genie-managed work follows this flow: ``` Idea → brainstorm → design review → wish → plan review → work → implementation review → PR → Ship (explore) (design gate) (plan) (plan gate) (build) (verify) ``` +Ordinary requests unrelated to an existing wish or brainstorm bypass this lifecycle unless the user explicitly asks for +Genie or the work demonstrably needs durable planning, unresolved product or architecture decisions, multiple coordinated +workstreams, or tracking across sessions. When uncertain, use cheap read-only inspection and escalate only when it finds +one of those needs. The bypass adds no Genie artifacts, roles, or gates; ordinary repository safety and validation rules +still apply. Related existing work always resumes through its persisted state. + The gates review different artifacts. For non-trivial work, `brainstorm` automatically routes the completed DESIGN.md through read-only design review before `wish` may consume it. The resulting WISH.md must then pass plan review and persist `APPROVED` before `work` starts. After execution, a different reviewer validates the implementation. Passing one gate From 836837f37dada20b5f472b036c844f02771d97e3 Mon Sep 17 00:00:00 2001 From: namastex888 Date: Tue, 28 Jul 2026 18:53:24 -0300 Subject: [PATCH 12/22] fix(router): announced bypass, security-gated by default, cheap routes exempt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer revision of the bypass policy, rebased onto dev: - every bypass is announced in one line with a 'use genie' override - security-sensitive changes keep lifecycle review gates by default; only an explicit user choice skips them - bug reports (report), operational commands, and Genie questions are cheap and never bypassed — resolves the contradiction with the intent-classification table and skill catalog - value-gate criteria get a worked example each; uncertainty inspection bounded to the two ls commands plus files the request names - skill description keeps bug/question routing discoverable - content-contract test pins policy invariants instead of full sentences - mirrors regenerated via sync-plugin-skills --write --- plugins/genie/skills/genie/SKILL.md | 13 +++++++------ plugins/genie/skills/genie/agents/openai.yaml | 2 +- plugins/genie/skills/genie/reference/lifecycle.md | 6 ++++-- scripts/release-docs.test.ts | 11 +++++------ skills/genie/SKILL.md | 13 +++++++------ skills/genie/agents/openai.yaml | 2 +- skills/genie/reference/lifecycle.md | 6 ++++-- 7 files changed, 29 insertions(+), 24 deletions(-) diff --git a/plugins/genie/skills/genie/SKILL.md b/plugins/genie/skills/genie/SKILL.md index 5da6abafc..8f534d752 100644 --- a/plugins/genie/skills/genie/SKILL.md +++ b/plugins/genie/skills/genie/SKILL.md @@ -1,13 +1,13 @@ --- name: genie -description: "Entry point for Genie operations — resumes existing lifecycle state and orchestrates work that needs durable planning or coordination. Ordinary requests bypass Genie unless the user asks for it." +description: "Entry point for Genie operations — routes bug reports, questions, and operational commands, resumes existing lifecycle state, and orchestrates work that needs durable planning or coordination. Other ordinary requests bypass the lifecycle with a one-line notice unless the user asks for Genie." --- # genie — Auto-Router **Runtime syntax:** in Codex, invoke the plugin copy with the owner-qualified `$genie:` selector; use bare `$` only when intentionally selecting a user-tier copy (a separately installed personal copy; Genie no longer seeds this tier). Claude Code and Hermes use `/`. Cross-skill prose below uses bare names as portable semantic routes; the orchestrator resolves the selector for the active tier. -You are the Automagik Genie — the single entry point for orchestration. First apply the lightweight bypass check below. Requests that do not bypass are classified, matched to existing lifecycle state, and routed to the right skill or CLI command. State a Genie route only when using Genie; a bypass returns control to the ordinary agent workflow without invoking another Genie skill. +You are the Automagik Genie — the single entry point for orchestration. First apply the lightweight bypass check below. Requests that do not bypass are classified, matched to existing lifecycle state, and routed to the right skill or CLI command. State a Genie route when using Genie and a one-line bypass notice otherwise; a bypass returns control to the ordinary agent workflow without invoking another Genie skill. ## Bare skill invocation (no request text) @@ -25,12 +25,13 @@ ls .genie/brainstorms/*/DRAFT.md 2>/dev/null | wc -l # brainstorms simmering Do this before invoking any lifecycle skill: 1. **Honor explicit Genie intent.** If the user asks to use Genie or a Genie skill, asks to create or use a wish, or requests Genie-managed planning, review, or orchestration, do not bypass. Merely mentioning Genie while asking to avoid or change its use is not an invocation. -2. **Check for related lifecycle work.** Make a cheap topic/slug comparison against `.genie/wishes/` and `.genie/brainstorms/`. If the request continues, changes, fixes, or asks about related existing work, do not bypass; use State Detection. -3. **Test whether the lifecycle adds value.** Genie is warranted when the work needs a durable plan, has unresolved product or architecture decisions, requires multiple coordinated workstreams, or must remain trackable across sessions or handoffs. If none applies, bypass Genie and return control to the ordinary agent workflow. +2. **Route cheap categories normally.** Bug reports (`report`), operational commands, and questions about Genie cost little and are never bypassed — classify and route them per the table below. +3. **Check for related lifecycle work.** Make a cheap topic/slug comparison against `.genie/wishes/` and `.genie/brainstorms/`. If the request continues, changes, fixes, or asks about related existing work, do not bypass; use State Detection. +4. **Test whether the lifecycle adds value.** Genie is warranted when the work needs a durable plan ("track the catalog effort across this week's sessions"), has unresolved product or architecture decisions ("should packs install via git or npm?"), requires multiple coordinated workstreams ("rename the NATS subjects across core and every pack"), or must remain trackable across sessions or handoffs. If none applies, bypass Genie and return control to the ordinary agent workflow. -The bypass must not create or update `.genie` artifacts, invoke another Genie skill, dispatch Genie roles, announce a Genie route, or add Genie-specific planning and review gates. The ordinary agent still follows the repository's normal safety, worktree, validation, and review requirements. +Announce the bypass in one line — "No lifecycle needed — handling this directly; say 'use genie' to override." — then proceed. The bypass must not create or update `.genie` artifacts, invoke another Genie skill, dispatch Genie roles, claim a Genie route, or add Genie-specific planning and review gates. The ordinary agent still follows the repository's normal safety, worktree, validation, and review requirements. -This is a value gate, not a word or file-count heuristic. A request being a feature, bug, security-sensitive change, or multi-file edit does not by itself justify Genie. When the request alone does not reveal whether the lifecycle adds value, do the minimum cheap read-only repository inspection needed to decide. Do not invoke `brainstorm`, create artifacts, or dispatch roles merely to classify the request. Escalate only when that inspection provides evidence for one of the lifecycle needs above. +This is a value gate, not a word or file-count heuristic. A request being a feature or a multi-file edit does not by itself justify Genie. Security-sensitive changes do not bypass by default — auth, secrets, permissions, and injection surfaces keep the lifecycle's review gates unless the user explicitly chooses to skip them (note the risk and proceed, per Rules). When the request alone does not reveal whether the lifecycle adds value, inspect cheaply and read-only — at most the two `ls` commands above plus a brief look at files the request names. Do not invoke `brainstorm`, create artifacts, or dispatch roles merely to classify the request. Escalate only when that inspection provides evidence for one of the lifecycle needs above. ## Intent Classification diff --git a/plugins/genie/skills/genie/agents/openai.yaml b/plugins/genie/skills/genie/agents/openai.yaml index f1244710a..e1df2e917 100644 --- a/plugins/genie/skills/genie/agents/openai.yaml +++ b/plugins/genie/skills/genie/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Genie Router" short_description: "Route work that warrants the Genie lifecycle" - default_prompt: "Use Genie only when explicitly requested, related to existing lifecycle work, or warranted by durable planning or coordination needs; otherwise bypass it." + default_prompt: "Use Genie when explicitly requested, related to existing lifecycle work, or warranted by durable planning or coordination needs; otherwise bypass it with a one-line notice. Security-sensitive work does not bypass by default." diff --git a/plugins/genie/skills/genie/reference/lifecycle.md b/plugins/genie/skills/genie/reference/lifecycle.md index d83680d10..33ec4a5a4 100644 --- a/plugins/genie/skills/genie/reference/lifecycle.md +++ b/plugins/genie/skills/genie/reference/lifecycle.md @@ -10,8 +10,10 @@ Genie-managed work follows this flow: Ordinary requests unrelated to an existing wish or brainstorm bypass this lifecycle unless the user explicitly asks for Genie or the work demonstrably needs durable planning, unresolved product or architecture decisions, multiple coordinated workstreams, or tracking across sessions. When uncertain, use cheap read-only inspection and escalate only when it finds -one of those needs. The bypass adds no Genie artifacts, roles, or gates; ordinary repository safety and validation rules -still apply. Related existing work always resumes through its persisted state. +one of those needs. A bypass is announced in one line and adds no Genie artifacts, roles, or gates; ordinary repository +safety and validation rules still apply. Security-sensitive changes do not bypass by default — only an explicit user +choice skips their review gates. Bug triage (`report`), operational commands, and Genie questions are cheap and always +route normally. Related existing work always resumes through its persisted state. The gates review different artifacts. For non-trivial work, `brainstorm` automatically routes the completed DESIGN.md through read-only design review before `wish` may consume it. The resulting WISH.md must then pass plan review and persist diff --git a/scripts/release-docs.test.ts b/scripts/release-docs.test.ts index 8c3c66be1..a98fcc649 100644 --- a/scripts/release-docs.test.ts +++ b/scripts/release-docs.test.ts @@ -980,17 +980,16 @@ describe('Group E release and documentation contracts', () => { expect(router).toContain('## Lightweight Bypass Check'); expect(router).toContain('Honor explicit Genie intent'); - expect(router).toContain('Merely mentioning Genie while asking to avoid or change its use is not an invocation'); + expect(router).toContain('Route cheap categories normally'); expect(router).toContain('Check for related lifecycle work'); expect(router).toContain('Test whether the lifecycle adds value'); - expect(router).toContain('do the minimum cheap read-only repository inspection needed to decide'); - expect(router).toContain( - 'A request being a feature, bug, security-sensitive change, or multi-file edit does not by itself justify Genie', - ); + expect(router).toContain('Announce the bypass in one line'); + expect(router).toContain('Security-sensitive changes do not bypass by default'); expect(router).toContain('must not create or update `.genie` artifacts'); expect(lifecycle).toContain('Ordinary requests unrelated to an existing wish or brainstorm bypass this lifecycle'); + expect(lifecycle).toContain('Security-sensitive changes do not bypass by default'); expect(lifecycle).toContain('Related existing work always resumes through its persisted state'); - expect(metadata).toContain('warranted by durable planning or coordination needs; otherwise bypass it'); + expect(metadata).toContain('otherwise bypass it with a one-line notice'); }); test('wizard discloses init MCP writes and owner-qualified lifecycle order', () => { diff --git a/skills/genie/SKILL.md b/skills/genie/SKILL.md index 5da6abafc..8f534d752 100644 --- a/skills/genie/SKILL.md +++ b/skills/genie/SKILL.md @@ -1,13 +1,13 @@ --- name: genie -description: "Entry point for Genie operations — resumes existing lifecycle state and orchestrates work that needs durable planning or coordination. Ordinary requests bypass Genie unless the user asks for it." +description: "Entry point for Genie operations — routes bug reports, questions, and operational commands, resumes existing lifecycle state, and orchestrates work that needs durable planning or coordination. Other ordinary requests bypass the lifecycle with a one-line notice unless the user asks for Genie." --- # genie — Auto-Router **Runtime syntax:** in Codex, invoke the plugin copy with the owner-qualified `$genie:` selector; use bare `$` only when intentionally selecting a user-tier copy (a separately installed personal copy; Genie no longer seeds this tier). Claude Code and Hermes use `/`. Cross-skill prose below uses bare names as portable semantic routes; the orchestrator resolves the selector for the active tier. -You are the Automagik Genie — the single entry point for orchestration. First apply the lightweight bypass check below. Requests that do not bypass are classified, matched to existing lifecycle state, and routed to the right skill or CLI command. State a Genie route only when using Genie; a bypass returns control to the ordinary agent workflow without invoking another Genie skill. +You are the Automagik Genie — the single entry point for orchestration. First apply the lightweight bypass check below. Requests that do not bypass are classified, matched to existing lifecycle state, and routed to the right skill or CLI command. State a Genie route when using Genie and a one-line bypass notice otherwise; a bypass returns control to the ordinary agent workflow without invoking another Genie skill. ## Bare skill invocation (no request text) @@ -25,12 +25,13 @@ ls .genie/brainstorms/*/DRAFT.md 2>/dev/null | wc -l # brainstorms simmering Do this before invoking any lifecycle skill: 1. **Honor explicit Genie intent.** If the user asks to use Genie or a Genie skill, asks to create or use a wish, or requests Genie-managed planning, review, or orchestration, do not bypass. Merely mentioning Genie while asking to avoid or change its use is not an invocation. -2. **Check for related lifecycle work.** Make a cheap topic/slug comparison against `.genie/wishes/` and `.genie/brainstorms/`. If the request continues, changes, fixes, or asks about related existing work, do not bypass; use State Detection. -3. **Test whether the lifecycle adds value.** Genie is warranted when the work needs a durable plan, has unresolved product or architecture decisions, requires multiple coordinated workstreams, or must remain trackable across sessions or handoffs. If none applies, bypass Genie and return control to the ordinary agent workflow. +2. **Route cheap categories normally.** Bug reports (`report`), operational commands, and questions about Genie cost little and are never bypassed — classify and route them per the table below. +3. **Check for related lifecycle work.** Make a cheap topic/slug comparison against `.genie/wishes/` and `.genie/brainstorms/`. If the request continues, changes, fixes, or asks about related existing work, do not bypass; use State Detection. +4. **Test whether the lifecycle adds value.** Genie is warranted when the work needs a durable plan ("track the catalog effort across this week's sessions"), has unresolved product or architecture decisions ("should packs install via git or npm?"), requires multiple coordinated workstreams ("rename the NATS subjects across core and every pack"), or must remain trackable across sessions or handoffs. If none applies, bypass Genie and return control to the ordinary agent workflow. -The bypass must not create or update `.genie` artifacts, invoke another Genie skill, dispatch Genie roles, announce a Genie route, or add Genie-specific planning and review gates. The ordinary agent still follows the repository's normal safety, worktree, validation, and review requirements. +Announce the bypass in one line — "No lifecycle needed — handling this directly; say 'use genie' to override." — then proceed. The bypass must not create or update `.genie` artifacts, invoke another Genie skill, dispatch Genie roles, claim a Genie route, or add Genie-specific planning and review gates. The ordinary agent still follows the repository's normal safety, worktree, validation, and review requirements. -This is a value gate, not a word or file-count heuristic. A request being a feature, bug, security-sensitive change, or multi-file edit does not by itself justify Genie. When the request alone does not reveal whether the lifecycle adds value, do the minimum cheap read-only repository inspection needed to decide. Do not invoke `brainstorm`, create artifacts, or dispatch roles merely to classify the request. Escalate only when that inspection provides evidence for one of the lifecycle needs above. +This is a value gate, not a word or file-count heuristic. A request being a feature or a multi-file edit does not by itself justify Genie. Security-sensitive changes do not bypass by default — auth, secrets, permissions, and injection surfaces keep the lifecycle's review gates unless the user explicitly chooses to skip them (note the risk and proceed, per Rules). When the request alone does not reveal whether the lifecycle adds value, inspect cheaply and read-only — at most the two `ls` commands above plus a brief look at files the request names. Do not invoke `brainstorm`, create artifacts, or dispatch roles merely to classify the request. Escalate only when that inspection provides evidence for one of the lifecycle needs above. ## Intent Classification diff --git a/skills/genie/agents/openai.yaml b/skills/genie/agents/openai.yaml index f1244710a..e1df2e917 100644 --- a/skills/genie/agents/openai.yaml +++ b/skills/genie/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Genie Router" short_description: "Route work that warrants the Genie lifecycle" - default_prompt: "Use Genie only when explicitly requested, related to existing lifecycle work, or warranted by durable planning or coordination needs; otherwise bypass it." + default_prompt: "Use Genie when explicitly requested, related to existing lifecycle work, or warranted by durable planning or coordination needs; otherwise bypass it with a one-line notice. Security-sensitive work does not bypass by default." diff --git a/skills/genie/reference/lifecycle.md b/skills/genie/reference/lifecycle.md index d83680d10..33ec4a5a4 100644 --- a/skills/genie/reference/lifecycle.md +++ b/skills/genie/reference/lifecycle.md @@ -10,8 +10,10 @@ Genie-managed work follows this flow: Ordinary requests unrelated to an existing wish or brainstorm bypass this lifecycle unless the user explicitly asks for Genie or the work demonstrably needs durable planning, unresolved product or architecture decisions, multiple coordinated workstreams, or tracking across sessions. When uncertain, use cheap read-only inspection and escalate only when it finds -one of those needs. The bypass adds no Genie artifacts, roles, or gates; ordinary repository safety and validation rules -still apply. Related existing work always resumes through its persisted state. +one of those needs. A bypass is announced in one line and adds no Genie artifacts, roles, or gates; ordinary repository +safety and validation rules still apply. Security-sensitive changes do not bypass by default — only an explicit user +choice skips their review gates. Bug triage (`report`), operational commands, and Genie questions are cheap and always +route normally. Related existing work always resumes through its persisted state. The gates review different artifacts. For non-trivial work, `brainstorm` automatically routes the completed DESIGN.md through read-only design review before `wish` may consume it. The resulting WISH.md must then pass plan review and persist From 7146f1cd1325c6cef4c5da67c9c5dce2495fadbd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Jul 2026 22:26:21 +0000 Subject: [PATCH 13/22] chore(version): bump to 5.260728.13 [auto-version] --- .claude-plugin/marketplace.json | 2 +- package.json | 2 +- plugins/genie/.claude-plugin/plugin.json | 2 +- plugins/genie/.codex-plugin/plugin.json | 2 +- plugins/genie/package.json | 2 +- plugins/hermes-genie/plugin.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 6914a7ce9..af4b3c379 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "genie", - "version": "5.260728.12", + "version": "5.260728.13", "source": "./plugins/genie", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project." } diff --git a/package.json b/package.json index 678da957c..1dcc64fef 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@automagik/genie", - "version": "5.260728.12", + "version": "5.260728.13", "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", "license": "MIT", diff --git a/plugins/genie/.claude-plugin/plugin.json b/plugins/genie/.claude-plugin/plugin.json index b69c6c95b..8f575c97b 100644 --- a/plugins/genie/.claude-plugin/plugin.json +++ b/plugins/genie/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260728.12", + "version": "5.260728.13", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.", "author": { "name": "Namastex Labs" diff --git a/plugins/genie/.codex-plugin/plugin.json b/plugins/genie/.codex-plugin/plugin.json index d9b11a35a..50fb05fe2 100644 --- a/plugins/genie/.codex-plugin/plugin.json +++ b/plugins/genie/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260728.12", + "version": "5.260728.13", "description": "Plan, execute, review, and ship software with Genie workflows in Codex.", "author": { "name": "Namastex Labs", diff --git a/plugins/genie/package.json b/plugins/genie/package.json index b382ceb8e..4c249f006 100644 --- a/plugins/genie/package.json +++ b/plugins/genie/package.json @@ -1,6 +1,6 @@ { "name": "genie-plugin", - "version": "5.260728.12", + "version": "5.260728.13", "private": true, "description": "Runtime dependencies for genie bundled CLIs", "license": "MIT", diff --git a/plugins/hermes-genie/plugin.yaml b/plugins/hermes-genie/plugin.yaml index 124fe3dec..e4358328e 100644 --- a/plugins/hermes-genie/plugin.yaml +++ b/plugins/hermes-genie/plugin.yaml @@ -1,5 +1,5 @@ name: genie -version: 5.260728.12 +version: 5.260728.13 description: "Native Hermes surface for Genie orchestration: read-only status plus work-plan and review-plan gap tools that the MCP board surface does not cover, hooks, commands, and a thin cockpit skill. Board/task truth comes from the genie MCP tools." provides_tools: # Default surface: exactly the three gap tools the MCP board surface does not cover. From 3f9545070a3a078053691463a3fd1e5ce1cdfc76 Mon Sep 17 00:00:00 2001 From: Felipe Rosa Date: Tue, 28 Jul 2026 17:26:32 -0300 Subject: [PATCH 14/22] chore: add repository-local Hermes profile --- .gitignore | 8 ++++++++ .hermes/SOUL.md | 37 +++++++++++++++++++++++++++++++++++++ .hermes/config.yaml | 11 +++++++++++ .hermes/profile.yaml | 2 ++ biome.json | 2 ++ 5 files changed, 60 insertions(+) create mode 100644 .hermes/SOUL.md create mode 100644 .hermes/config.yaml create mode 100644 .hermes/profile.yaml diff --git a/.gitignore b/.gitignore index b6aebdf34..a8abb00e4 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,14 @@ yarn-error.log* # Misc .cache/ .temp/ +.dev/ + +# Repository-local Hermes profile: keep the portable contract in git and +# exclude plans, sessions, memories, credentials, logs, and other runtime state. +.hermes/* +!.hermes/config.yaml +!.hermes/profile.yaml +!.hermes/SOUL.md # Legacy worktrees (now default to ~/.genie/worktrees/) .worktrees/ diff --git a/.hermes/SOUL.md b/.hermes/SOUL.md new file mode 100644 index 000000000..9bdf85691 --- /dev/null +++ b/.hermes/SOUL.md @@ -0,0 +1,37 @@ +# Genie + +You are the repository-local Hermes profile for Genie v5. Turn intent into verified outcomes with the smallest +responsible scope. + +## Workflow + +Use `brainstorm` to clarify uncertain ideas, `wish` to define approved scope and acceptance criteria, `work` to +execute, `review` to judge evidence independently, and `learn` to capture concise reusable knowledge for the next run. + +Genie v5 is task-backed and zero-daemon by default: + +- `genie board` shows current work. +- `genie task` creates, claims, inspects, and completes task state. +- `genie launch ` opens isolated execution lanes when requested. +- `genie omni` handles the optional Omni bridge and approval inbox. + +Check `genie --help` or the relevant subcommand help before relying on remembered syntax. Never use obsolete +`serve`, `ls`, `task board`, `spawn`, or `--no-tui` surfaces, and never assume the retired +`/home/genie/workspace/agents` path. + +## Operating law + +Read repository instructions, preserve user changes, keep credentials and runtime state out of Git, and support +claims with files, commands, tests, or other direct evidence. Ask for explicit approval before: + +- accessing, revealing, or changing secrets or authentication; +- changing security policy or trust boundaries; +- destructive or difficult-to-reverse actions; +- production, release, deployment, or other live-environment changes; +- external communication or actions affecting people outside the workspace; +- spending budget, changing providers, or creating subscriptions; +- broadening product, architecture, or task scope; +- starting recurring services, schedulers, or background processes. + +Omni inspection and local planning are allowed when requested. Live Omni wiring—including handshake, key changes, +enabling approvals, starting the bridge, or sending a real test—requires a separate, immediate approval. diff --git a/.hermes/config.yaml b/.hermes/config.yaml new file mode 100644 index 000000000..11c5482d1 --- /dev/null +++ b/.hermes/config.yaml @@ -0,0 +1,11 @@ +model: + default: gpt-5.6-sol + provider: openai-codex + base_url: https://chatgpt.com/backend-api/codex + +terminal: + backend: local + timeout: 300 + +security: + redact_secrets: true diff --git a/.hermes/profile.yaml b/.hermes/profile.yaml new file mode 100644 index 000000000..b1cf7d8d7 --- /dev/null +++ b/.hermes/profile.yaml @@ -0,0 +1,2 @@ +description: Repository-local Hermes profile for Genie v5 coding and orchestration. Repository work is the default; live Omni wiring is a separate, explicitly approved mode. +description_auto: false diff --git a/biome.json b/biome.json index d6cf04236..ec388a166 100644 --- a/biome.json +++ b/biome.json @@ -48,6 +48,8 @@ "node_modules", "dist", "*.min.js", + ".dev", + ".hermes", ".genie", ".worktrees", ".claude", From 1850332ccc22b53d5b56453e1d81bf27ca7e7486 Mon Sep 17 00:00:00 2001 From: Felipe Rosa Date: Tue, 28 Jul 2026 19:40:12 -0300 Subject: [PATCH 15/22] chore: enable repository-local Genie plugin --- .hermes/config.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.hermes/config.yaml b/.hermes/config.yaml index 11c5482d1..0d9975088 100644 --- a/.hermes/config.yaml +++ b/.hermes/config.yaml @@ -9,3 +9,8 @@ terminal: security: redact_secrets: true + +plugins: + enabled: + - genie + disabled: [] From 5a74123915c6047362d9de5863042ac1e450f40f Mon Sep 17 00:00:00 2001 From: Felipe Rosa Date: Tue, 28 Jul 2026 19:59:53 -0300 Subject: [PATCH 16/22] =?UTF-8?q?docs(wish):=20genie-official-roadmap=20AP?= =?UTF-8?q?PROVED=20=E2=80=94=2036-wish=20triage=20into=20canonical=20boar?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design SHIP (1 fix loop, digest 36097b0d…), plan SHIP (2 fix loops, all findings execution-verified). 3-scout audit: 29 wishes DONE-verified on dev, 5 in progress, 2 open. Plan: archive 29 with link-rewrite inventory, cold-seed roadmap board with 19 lifecycle cards (Felipe-ratified work order), rewrite INDEX with Shipped section, draft public-roadmap-polish. Co-Authored-By: Claude Fable 5 --- .genie/INDEX.md | 2 + .../genie-official-roadmap/DESIGN.md | 95 ++++++++ .../genie-official-roadmap/DRAFT.md | 36 ++++ .genie/roadmap.json | 68 ++++++ .genie/wishes/genie-official-roadmap/WISH.md | 204 ++++++++++++++++++ 5 files changed, 405 insertions(+) create mode 100644 .genie/brainstorms/genie-official-roadmap/DESIGN.md create mode 100644 .genie/brainstorms/genie-official-roadmap/DRAFT.md create mode 100644 .genie/wishes/genie-official-roadmap/WISH.md diff --git a/.genie/INDEX.md b/.genie/INDEX.md index 4e0367edc..d0f96cbe6 100644 --- a/.genie/INDEX.md +++ b/.genie/INDEX.md @@ -35,6 +35,8 @@ ## Poured +- [WISH: genie-official-roadmap](wishes/genie-official-roadmap/WISH.md) — **APPROVED 2026-07-28** ([DESIGN](brainstorms/genie-official-roadmap/DESIGN.md) SHIP after 1 fix loop, digest `36097b0d…`; plan SHIP after 2 fix loops, all findings execution-verified by the reviewer): triage of all 36 wishes (3-scout evidence audit) into the official roadmap — archive 29 DONE-verified wishes with full link-rewrite inventory, cold-seed the `roadmap` board with 19 lifecycle cards (Work order: dogfood-remediation → v4-residue → release-ops → proportional-validation, Felipe-ratified), rewrite INDEX with a Shipped section, draft `public-roadmap-polish`. 3 groups: archive+links ∥ board-seed → INDEX rewrite; single commit in G3. + - khal-native-theme — **APPROVED 2026-07-23, RELOCATED same day** to `khal-os/genie-desktop:.genie/` (branch `wish/khal-native-theme`, commit `484c243`; Felipe: desktop context lives in the desktop repo — the fork's own genie.db carries the 6 seeded group tasks and its WISH adds the live-visual-checkpoint protocol CP-1..6). Design SHIP digest `bdd54325…`, plan SHIP after 1 fix loop, 3-lens ledger verify 0 must-fix. This repo's 7 stale task rows are hard-blocked with a relocation reason. First of Theme→Identity→SSH - [WISH: codex-plugin-dogfood-remediation](wishes/codex-plugin-dogfood-remediation/WISH.md) — **IN_PROGRESS** (2026-07-22, large appetite; 21/24 criteria as of 2026-07-23): exact task CWD + trustworthy Codex delivery evidence; the three open criteria concern MCP marker ownership and the evidence validator (added to INDEX 2026-07-26 — the most active wish in the repo had no INDEX entry, invisible to the jar↔INDEX drift lint) - khal-native-theme — **APPROVED 2026-07-23, RELOCATED same day** to `khal-os/genie-desktop:.genie/` (branch `wish/khal-native-theme`, commit `484c243`; Felipe: desktop context lives in the desktop repo — the fork's own genie.db carries the 6 seeded group tasks and its WISH adds the live-visual-checkpoint protocol CP-1..6). Design SHIP digest `bdd54325…`, plan SHIP after 1 fix loop, 3-lens ledger verify 0 must-fix. This repo's 7 stale task rows are hard-blocked with a relocation reason. First of Theme→Identity→SSH diff --git a/.genie/brainstorms/genie-official-roadmap/DESIGN.md b/.genie/brainstorms/genie-official-roadmap/DESIGN.md new file mode 100644 index 000000000..a17ccf588 --- /dev/null +++ b/.genie/brainstorms/genie-official-roadmap/DESIGN.md @@ -0,0 +1,95 @@ +# Design: Genie Official Roadmap — full-jar triage into the canonical board + +| Field | Value | +|-------|-------| +| **Slug** | `genie-official-roadmap` | +| **Date** | 2026-07-28 | +| **WRS** | 100/100 | + +## Problem + +36 wish dirs and 24 brainstorm dirs have accumulated with statuses drifting between WISH.md headers, INDEX.md prose, and dev reality; there is no single official roadmap. This blocks Felipe's #1 axis (public-facing polish): the project cannot present what is done, in flight, or next — even though the machinery (roadmap board, lanes, canonical `roadmap.json`) now exists. + +## Scope + +### IN +- **Archive move:** `git mv` the 29 DONE-verified wishes to `.genie/wishes/archive//`, WITH the link rewrites the depth change requires: + - Intra-wish design links: `../../brainstorms/…` → `../../../brainstorms/…` (17 links across ~15 archived WISH.md files; wishes-lint walks `archive/` recursively, so these are gate-enforced). + - Cross-wish links whose endpoints split: `genie-ui/WISH.md` → `../genie-ui-dash/WISH.md` becomes `../../genie-ui-dash/WISH.md` (target survives outside archive). Links where both endpoints move together (agent-sync↔pr-2545 ×3, routing-delivery-fix↔routing-matrix, pr-2545↔stable-release-security-gate) stay as-is. + - Repo-escaping links in pr-2545's REVIEW-DISPOSITION.md / FINALIZATION-VERIFICATION.md: `../../../.claude/…` → `../../../../.claude/…`. + - Verification: `bun run wishes:lint` (covers brainstorm links incl. inside archive) PLUS an explicit link-resolution sweep — every markdown relative link in moved files and in INDEX.md resolved against the filesystem (one-off script or grep+test loop); INDEX.md is NOT covered by any existing lint and must be swept manually. +- **Board seed** (cold seed — baseline recorded below): create the `roadmap` board (6 lifecycle lanes) and one lifecycle card per surviving initiative. Every card is created then explicitly `task move`d to its lane (CLI `task create` cannot set lanes, and NULL-lane cards render deceptively) — a card without a non-null lane in `genie task export` is a seeding failure. Full enumeration (19 cards): + - **Work (4):** codex-plugin-dogfood-remediation, v4-home-residue-doctor, release-ops-hardening, proportional-validation-policy — priority order per D4, encoded in INDEX prose (DAG stays doc-only). + - **Review (3):** genie-ui-dash (fork QA), live-dev-loop (fork QA), ritual-QA checklist (bundled: council live run, genie-mcp Warp QA, warp pane checklist, agent-sync convergence, update-handoff homolog dogfood, taxonomy PATH export). + - **Wish (1):** khal-rebrand (approved, unexecuted, fork). + - **Brainstorm (5):** genie-boards-ui, intent-to-wish-compiler, brainstorm-domain-map, cross-agent-delegate, genie-official-roadmap (this initiative; its card moves to Done when this wish ships). + - **Idea (6):** control-plane-contract, skill-absorbs, always-on-genie, genie-spend, dream-replatform, public-roadmap-polish (new). + - **Excluded:** khal-app-kit-identity + genie-remote-ssh (relocated to fork — INDEX line only, no card); khal-native-theme (same). +- **INDEX.md rewrite:** new **Shipped** section (one line per archived wish, links pointing into `archive/`); lifecycle sections (Raw/Simmering/Ready/Poured) reconciled to the card set above. Named repairs: remove the duplicated khal-native-theme RELOCATED bullet (conflict residue, lines 39+41); correct the codex-plugin-update-handoff entry ("Groups C–E not started" contradicts the wish's SHIPPED header — PR #2617 merged 2026-07-22); stable-release fact stated precisely as "first full stable v5.260727.5 (2026-07-27); current stable pinned at execution time" (v5.260728.8 as of review — verify fresh). Constraint: no lifecycle-section entry may use a `wishes/archive/…` path as its FIRST link (the drift lint's slug regex would capture `archive`); Shipped-section entries are outside the drift lint by design (documented property, not an accident). +- **Existing boardless rows:** the 29 group-task rows (incl. 6 hard-blocked khal-native-theme tombstones and 4 done genie-ui rows) stay untouched as execution history — cards and group tasks are different layers (D3). +- **New brainstorm:** `public-roadmap-polish` DRAFT + INDEX Raw entry (publish roadmap outward + docs/onboarding sweep) — drafted, not executed. +- **Publish:** pre-commit sync lands the seeded board in `roadmap.json`; criterion measured against the recorded baseline (`boards: []`, 29 boardless tasks, zero cards). + +### OUT +- No execution of any triaged wish (dogfood criteria, doctor checks, release hardening stay their own wishes). +- No fork (khal-os/genie-desktop) changes; its wishes are represented as cards only. +- No changes to sync/board/lint machinery (any INDEX-link lint gap closure is future work — this wish uses a one-off sweep). +- No brainstorm-dir moves or deletion; brainstorm cleanup is a later pass. +- The board's Done lane starts empty on purpose: done-ness is presented by the INDEX Shipped section this round; surfacing shipped work on the published board is scoped to `public-roadmap-polish` (D6). + +## Approach + +Single documentation-and-state pass over the audited inventory (3-scout evidence audit, 2026-07-28, per-wish PR/commit verification recorded in DRAFT.md). Alternatives considered: (a) INDEX-only cleanup without board seeding — rejected: leaves the roadmap unpublished, failing the #1 axis; (b) full re-review of each DONE wish before archiving — rejected: the scout audit already verified merge evidence; re-review adds cost without new information. Chosen: archive + link-rewrite + seed + rewrite in one wish, gated by wishes-lint, a full link-resolution sweep, doctor drift lint, and a lane-non-null export assertion. + +## Simplicity Case + +- **Simplest complete design:** git mv 29 dirs + ~25 link rewrites, 19 `task create` + 19 `task move` calls, one INDEX rewrite, one new brainstorm DRAFT. +- **Added machinery:** none persistent — one throwaway link-resolution sweep during execution (a lint gap exists; closing it durably is deferred). +- **Deferred until measured:** durable INDEX/markdown link lint; brainstorm-dir archiving; auto-lane-sync from WISH status (only if manual drift recurs). +- **Complexity removed:** no new schema, no dependency rows, no INDEX generator — prose stays human-authored per repo contract. + +## Decisions + +| # | Decision | Rationale | +|---|----------|-----------| +| 1 | Priority axes = public polish · dogfood pain · stable-release track (desktop parked) | Felipe, 2026-07-28 | +| 2 | DONE wishes → `.genie/wishes/archive//` | History preserved, active dir lean; Felipe picked over stay/delete | +| 3 | Roadmap board = canonical tracker; boardless group tasks stay the execution layer, untouched by this wish | Felipe; lanes stay non-spammy (one card per initiative) | +| 4 | Work-lane order: dogfood-remediation → v4-residue → release-ops → proportional-validation | Felipe ratified proposed order | +| 5 | 6 ritual-QA items = one bundled Review-lane card; their wishes archive now | Felipe; code evidence verified, rituals tracked without holding dirs open | +| 6 | Public-polish axis gets a NEW brainstorm (`public-roadmap-polish`), drafted not executed; it owns surfacing Done/Shipped outward | Gap: axis #1 had no wish; Done-lane emptiness this round is deliberate | +| 7 | Card enumeration is closed (the 19 listed) — anything else is an INDEX line, not a card | Reviewer finding: "no initiative missing" must be decidable | + +## Risks & Assumptions + +| # | Risk | Severity | Mitigation | +|---|------|----------|------------| +| 1 | Archive move breaks relative links | High if unmitigated | Explicit link inventory in Scope; wishes-lint gates brainstorm links (recursively, incl. archive); one-off resolution sweep covers INDEX.md and non-brainstorm links, which NO existing lint checks | +| 2 | Scout audit overclaims a DONE | Low | Scouts cited merged PR/commit per wish; a wrong card is reversible (git mv back + lane move) | +| 3 | INDEX↔lane drift after seeding | Low | doctor index-lane drift lint is the standing gate; archive paths forbidden as first links in lifecycle sections | +| 4 | roadmap.json churn/conflicts from seeding | Low | Cold seed against recorded baseline; single writer; three-way sync refuses divergence | +| 5 | Stable-release fact goes stale between design and execution | Low | INDEX states first-stable as fixed history; current-stable pinned by `gh release list` at execution | + +## Success Criteria + +- [ ] 29 audited-DONE wish dirs live under `.genie/wishes/archive/`; `bun run check` green (incl. wishes-lint over the recursive tree) +- [ ] Link-resolution sweep passes: every relative markdown link in moved files AND in `.genie/INDEX.md` resolves to an existing file (sweep output recorded as evidence) +- [ ] `roadmap` board exists; `genie task export` shows exactly 19 cards on it, each with a non-null lane matching the Scope enumeration; `genie board --board roadmap` renders them +- [ ] Ritual-QA card in Review lane enumerates all 6 items in its description/timeline +- [ ] `genie doctor` reports zero index-lane `drift` entries, and no lifecycle-section INDEX entry links `wishes/archive/…` as its first link +- [ ] `.genie/INDEX.md` Shipped section lists all 29 archived wishes; duplicated khal bullet removed; update-handoff entry corrected; stable-release facts stated per Risk 5 +- [ ] `roadmap.json` committed reflecting the seeded board (diff vs recorded baseline shows the board + 19 cards); `genie task sync` reports in-sync post-commit +- [ ] `public-roadmap-polish` brainstorm DRAFT exists with an INDEX Raw entry (and its Idea card per enumeration) + +## 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:** SHIP +- **Reviewed content SHA-256:** `36097b0def248af7f42aa467142b5979cb75382ff5414e3a201d4036cb955a1d` +- **Reviewer:** reviewer-subagent-fable5 +- **Reviewed at:** 2026-07-28T22:45:19.000Z + diff --git a/.genie/brainstorms/genie-official-roadmap/DRAFT.md b/.genie/brainstorms/genie-official-roadmap/DRAFT.md new file mode 100644 index 000000000..9093826d9 --- /dev/null +++ b/.genie/brainstorms/genie-official-roadmap/DRAFT.md @@ -0,0 +1,36 @@ +# genie-official-roadmap — DRAFT + +**Started:** 2026-07-28 · **Mode:** triage brainstorm (Felipe + orchestrator) + +## Problem + +`.genie/` carries 36 wish dirs + 24 brainstorm dirs; statuses drift between WISH.md headers, INDEX.md, and dev reality. Goal: one official genie roadmap — every wish triaged (done-on-dev / still-relevant / priority), published through the roadmap board (`roadmap.json`). + +## WRS + +``` +WRS: ██████░░░░ 60/100 + Problem ✅ | Scope ✅ | Decisions ◐ | Risks ✅ | Criteria ░ +``` + +## Felipe decisions (2026-07-28) + +- **Priority axes (ranked):** public-facing polish · dogfood pain · first-stable-release track. Desktop/Khal explicitly NOT a top axis. +- **DONE wishes** → move to `.genie/wishes/archive//`, one INDEX line each. +- **Roadmap board** becomes canonical: seed `roadmap` board, one lifecycle card per surviving initiative, published via roadmap.json. + +## Audit result (3 scouts, evidence-first, 2026-07-28) + +- **29 DONE-verified** (merge/PR/file evidence checked): agent-sync, agent-sync-hardening, boards-first-class, codex-plugin-update-handoff, council-workflow, dispatch-inproc-default, genie-mcp, genie-ui (superseded-by-design), genie-ui-bridge, hermes-homogeneous-integration, hermes-khaw-native-surface, hook-injection-hardening, omni-approval-ux, omni-branch-drift-sync, omni-runner-port, plugin-resource-shipping, pr-2545-ultra-release-gate, rolling-pr-auth-hardening, routing-delivery-fix, routing-matrix, skills-fable5-revamp, stable-release-security-gate (v5.260727.5 SHIPPED 2026-07-27!), taxonomy-rehoming, v5-completion, v5-demolition, v5-foundation, v5-housekeeping, warp-integration, worktree-isolation-hardening. +- **5 IN_PROGRESS:** codex-plugin-dogfood-remediation (21/24, 3 criteria open), proportional-validation-policy (mirror regen pending), genie-ui-dash (fork, awaits Felipe GUI QA), live-dev-loop (fork, awaits Felipe QA), khal-rebrand (fork, approved not executed). +- **2 OPEN:** v4-home-residue-doctor (DRAFT, 0/8 — carries live bug #2450), release-ops-hardening (DRAFT — its "after first stable" gate has now PASSED, so it is actionable). +- **Felipe-ritual backlog** riding on DONE wishes: council live ritual, genie-mcp Warp QA, warp-integration pane checklist, agent-sync live convergence, codex-plugin-update-handoff homolog dogfood, taxonomy PATH export. + +## Open + +- Final priority ordering of open items; whether ritual QA items become one bundled card; public-polish axis has no existing wish (gap — the roadmap publication itself may be its first deliverable). + +## Risks + +- WISH headers can overclaim → mitigated: every DONE bucket verified against merged PRs/files by scouts. +- Archive move must not break INDEX/wish-lint links (wishes-lint checks brainstorm links). diff --git a/.genie/roadmap.json b/.genie/roadmap.json index d3056150b..3e4dd51ba 100644 --- a/.genie/roadmap.json +++ b/.genie/roadmap.json @@ -500,6 +500,74 @@ "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." + }, + { + "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, + "created_at": 1785278777572, + "updated_at": 1785278777572, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": 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", + "created_at": 1785278876435, + "updated_at": 1785278876435, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": 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", + "created_at": 1785278876520, + "updated_at": 1785278876520, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": 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", + "created_at": 1785278876617, + "updated_at": 1785278876617, + "lane": null, + "agent_kind": null, + "heartbeat_at": null, + "blocked_by": null, + "blocked_reason": null } ], "task_dependencies": [], diff --git a/.genie/wishes/genie-official-roadmap/WISH.md b/.genie/wishes/genie-official-roadmap/WISH.md new file mode 100644 index 000000000..8794e0576 --- /dev/null +++ b/.genie/wishes/genie-official-roadmap/WISH.md @@ -0,0 +1,204 @@ +# Wish: Genie Official Roadmap — full-jar triage into the canonical board + +| Field | Value | +|-------|-------| +| **Status** | APPROVED | +| **Slug** | `genie-official-roadmap` | +| **Date** | 2026-07-28 | +| **Author** | Felipe + orchestrator (Fable 5) | +| **Appetite** | medium | +| **Branch** | `wish/genie-official-roadmap` | +| **Repos touched** | automagik-dev/genie only | +| **Design** | [DESIGN.md](../../brainstorms/genie-official-roadmap/DESIGN.md) | + +## Summary + +Turn the audited wish jar (36 wishes; 3-scout evidence audit 2026-07-28) into the official genie roadmap: archive the 29 DONE-verified wishes with the link rewrites the move requires, seed the canonical `roadmap` board with the closed set of 19 lifecycle cards, and rewrite INDEX.md to match reality. Publishes the roadmap through `roadmap.json` — the prerequisite for Felipe's #1 axis (public-facing polish). + +## Scope + +### IN + +- `git mv` 29 DONE-verified wishes to `.genie/wishes/archive//` with the full link-rewrite inventory from DESIGN Scope (17 intra-wish design links `../../`→`../../../`; genie-ui→genie-ui-dash split-endpoint link; pr-2545 repo-escaping `.claude` links). +- Cold-seed the `roadmap` board (6 lifecycle lanes) with exactly 19 cards, each create+move to its enumerated lane (Work 4 / Review 3 / Wish 1 / Brainstorm 5 / Idea 6 per DESIGN D7). +- Rewrite `.genie/INDEX.md`: new Shipped section (29 lines, archive links), lifecycle sections reconciled to the card set — including the reviewer-mandated relocations: `genie-official-roadmap` Raw→Simmering, `v4-home-residue-doctor` + `release-ops-hardening` Ready→Poured; remove duplicated khal-native-theme bullet; correct codex-plugin-update-handoff entry; stable-release facts (first stable v5.260727.5 fixed history; current stable pinned fresh at execution). +- Draft `public-roadmap-polish` brainstorm (DRAFT.md + INDEX Raw entry + Idea card). +- One-off link-resolution sweep over moved files and INDEX.md, output recorded as evidence. + +### OUT + +- No execution of any triaged wish (dogfood criteria, doctor checks, release hardening remain their own wishes). +- No fork (khal-os/genie-desktop) changes; fork wishes get cards only. +- No sync/board/lint machinery changes; the INDEX-link lint gap gets a one-off sweep, durable lint deferred. +- No brainstorm-dir moves or deletions. +- Done lane starts empty by design (D6); this wish's own card moves to Done only at ship, outside the seed-time criteria (reviewer LOW-B). + +## Decisions + +| # | Decision | Rationale | +|---|----------|-----------| +| 1 | Inherit DESIGN D1–D7 unchanged (axes, archive dir, canonical board, work order, ritual bundle, polish brainstorm, closed 19-card set) | Felipe-ratified 2026-07-28; design review SHIP after 1 fix loop | +| 2 | Three execution groups: archive+links, board seed, INDEX+brainstorm — INDEX last | INDEX links target archive paths (needs G1) and drift lint compares sections to lanes (needs G2) | +| 3 | Link sweep is a throwaway script run in G1 and re-run in G3, evidence pasted into this wish dir | No existing lint covers INDEX.md/non-brainstorm links (reviewer HIGH-2) | + +## Simplicity Case + +- **Simplest complete design:** git mv + ~25 targeted link edits, 19 create + 19 move CLI calls, one INDEX rewrite, one DRAFT.md. +- **Added machinery:** none persistent — one throwaway link-resolution sweep. +- **Deferred until measured:** durable markdown-link lint; brainstorm-dir archiving; auto lane-sync. +- **Complexity removed:** no schema changes, no dependency rows, no INDEX generator. + +## Dependencies + +**depends-on:** none +**blocks:** none + +(public-roadmap-polish is downstream but exists only as a planned brainstorm; declare the edge there when its wish is poured.) + +## Success Criteria + +- [ ] 29 audited-DONE wish dirs under `.genie/wishes/archive/`; `bun run check` green (wishes-lint scans the recursive tree, 0 broken links) +- [ ] Link sweep passes: every relative markdown link in moved files AND `.genie/INDEX.md` resolves; sweep output stored at `.genie/wishes/genie-official-roadmap/qa/link-sweep.txt` +- [ ] `genie task export` shows the `roadmap` board with exactly 19 cards, each non-null lane matching DESIGN's enumeration; `genie board --board roadmap` renders them +- [ ] Ritual-QA card in Review lane enumerates all 6 items (timeline comment) +- [ ] `genie doctor` reports zero index-lane `drift` entries; no lifecycle-section INDEX entry has `wishes/archive/…` as first link +- [ ] INDEX.md: Shipped section lists all 29; duplicate khal bullet gone; update-handoff entry corrected; stable-release facts per DESIGN Risk 5 +- [ ] `roadmap.json` committed; diff vs execution-start baseline (`boards: []`, 33 boardless rows, 0 cards) shows board + 19 cards; the 29 HISTORICAL boardless rows (pre-2026-07-28, excl. this wish's pointer + 3 group tasks, which legitimately transition during execution) unchanged in count/ids; `genie task sync` in-sync post-commit +- [ ] `public-roadmap-polish` DRAFT exists with INDEX Raw entry and Idea card + +## Execution Strategy + +### Wave 1 (parallel) + +| Group | Agent | Complexity | Model | Description | +|-------|-------|------------|-------|-------------| +| 1 | engineer | 3 — multi-file mechanical move with exact link inventory (+1 no deterministic test for INDEX links, +1 prior rework risk from lint, +1 multi-dir) | engineer-standard / medium | Archive 29 wishes + link rewrites + sweep | +| 2 | engineer | 2 — stateful board writes via CLI (+2 stateful) | engineer-standard / medium | Seed roadmap board: 19 cards create+move, ritual card comment | + +**Commit discipline:** G1 and G2 do NOT commit — the pre-commit hook syncs `roadmap.json` on every commit, so a G1 commit during G2's mid-seed would capture a partially-seeded board. The single commit is G3 deliverable 4. + +### Wave 2 (after G1 AND G2) + +| Group | Agent | Complexity | Model | Description | +|-------|-------|------------|-------|-------------| +| 3 | engineer | 3 — subjective prose reconciliation against two truth sources (+2 subjective acceptance, +1 no deterministic test) | engineer-standard / high | INDEX.md rewrite + public-roadmap-polish draft + final sweeps | + +## Execution Groups + +### Group 1: Archive move + link rewrites + +**Goal:** Relocate the 29 DONE-verified wishes into `.genie/wishes/archive/` without a single dangling link. + +**Deliverables:** +1. `git mv .genie/wishes/ .genie/wishes/archive/` for all 29 slugs listed in DESIGN (agent-sync, agent-sync-hardening, boards-first-class, codex-plugin-update-handoff, council-workflow, dispatch-inproc-default, genie-mcp, genie-ui, genie-ui-bridge, hermes-homogeneous-integration, hermes-khaw-native-surface, hook-injection-hardening, omni-approval-ux, omni-branch-drift-sync, omni-runner-port, plugin-resource-shipping, pr-2545-ultra-release-gate, rolling-pr-auth-hardening, routing-delivery-fix, routing-matrix, skills-fable5-revamp, stable-release-security-gate, taxonomy-rehoming, v5-completion, v5-demolition, v5-foundation, v5-housekeeping, warp-integration, worktree-isolation-hardening) +2. Link rewrites per DESIGN inventory: 17 `../../brainstorms/`→`../../../brainstorms/` (15 files; routing-matrix:12 and plugin-resource-shipping:12 carry two each); `archive/genie-ui/WISH.md` link to genie-ui-dash → `../../genie-ui-dash/WISH.md`; pr-2545 REVIEW-DISPOSITION.md + FINALIZATION-VERIFICATION.md `../../../.claude/`→`../../../../.claude/`; both-endpoints-moved links untouched +3. Link-resolution sweep script output (moved + surviving wishes; INDEX.md expected to fail until G3 — record both states) at `qa/link-sweep.txt` in this wish dir + +**Acceptance Criteria:** +- [ ] All 29 dirs under archive/, none remaining at top level; the 7 surviving wish dirs untouched +- [ ] `bun run wishes:lint` → 0 broken links +- [ ] Sweep: 0 unresolved links in moved and surviving wish files + +**Validation:** +```bash +bun run wishes:lint && ls .genie/wishes/archive | wc -l | grep -qx 29 && ls .genie/wishes | grep -vx archive | wc -l | grep -qx 8 && mkdir -p .genie/wishes/genie-official-roadmap/qa && bun -e 'import{readFileSync,existsSync,readdirSync,statSync}from"node:fs";import{dirname,resolve}from"node:path";const files=[];const walk=d=>{for(const e of readdirSync(d)){const p=`${d}/${e}`;statSync(p).isDirectory()?walk(p):e.endsWith(".md")&&files.push(p)}};walk(".genie/wishes");let bad=0;for(const f of files){for(const m of readFileSync(f,"utf8").matchAll(/\]\(([^)#\s]+)\)/g)){const t=m[1].split("#")[0];if(!t||/^[a-z]+:/.test(t))continue;if(!existsSync(resolve(dirname(f),t))){console.log(`${f}: ${t}`);bad++}}}console.log(`unresolved: ${bad}`);process.exit(bad?1:0)' > .genie/wishes/genie-official-roadmap/qa/link-sweep.txt; rc=$?; cat .genie/wishes/genie-official-roadmap/qa/link-sweep.txt; exit $rc +``` +(8 = 7 surviving wishes + this wish's own dir. The inline resolver is the concrete sweep for the classes wishes-lint does not check; G1 records wish-tree state — INDEX.md is swept in G3.) + +**depends-on:** none + +--- + +### Group 2: Board seed + +**Goal:** Create the canonical `roadmap` board with exactly the 19 enumerated lifecycle cards, every card in its correct lane. + +**Deliverables:** +1. `genie board create roadmap` (default 6 lifecycle lanes) +2. 19 × `genie task create --board roadmap --title …` (with `--wish ` where a slug exists) followed by `genie task move --to ` per DESIGN enumeration — Work: codex-plugin-dogfood-remediation, v4-home-residue-doctor, release-ops-hardening, proportional-validation-policy · Review: genie-ui-dash, live-dev-loop, ritual-QA checklist · Wish: khal-rebrand · Brainstorm: genie-boards-ui, intent-to-wish-compiler, brainstorm-domain-map, cross-agent-delegate, genie-official-roadmap · Idea: control-plane-contract, skill-absorbs, always-on-genie, genie-spend, dream-replatform, public-roadmap-polish +3. `genie task comment` on the ritual-QA card enumerating all 6 rituals (council live run, genie-mcp Warp QA, warp pane checklist, agent-sync convergence, update-handoff homolog dogfood, taxonomy PATH export) +4. Create a FRESH Brainstorm-lane card for genie-official-roadmap, then `genie task block t_ms58w0yqa195fbf8 --reason "superseded by roadmap-board card"` — the boardless pointer cannot be adopted (no code path mutates `board_id` after creation; `task move` throws LaneError on boardless tasks) + +**Acceptance Criteria:** +- [ ] `genie task export`: board `roadmap` exists; exactly 19 tasks with `board_id` = roadmap board; every one `lane` non-null and matching the enumeration +- [ ] Ritual card timeline contains the 6-item comment +- [ ] The 29 HISTORICAL boardless group-task rows untouched (same count/ids in export); exempt: this wish's own pointer + 3 group tasks (they transition via checkout/done/block during execution) + +**Validation:** +```bash +genie board --board roadmap && genie task export | bun -e 'const s=JSON.parse(await Bun.stdin.text());const b=s.boards.find(x=>x.name==="roadmap");if(!b)process.exit(1);const c=s.tasks.filter(t=>t.board_id===b.id);if(c.length!==19||c.some(t=>!t.lane))process.exit(1)' +``` + +**depends-on:** none + +--- + +### Group 3: INDEX rewrite + polish brainstorm + final gates + +**Goal:** Reconcile INDEX.md with the archive and the seeded board, and draft the public-roadmap-polish brainstorm. + +**Deliverables:** +1. INDEX.md rewrite: Shipped section (29 entries, `wishes/archive//WISH.md` links); lifecycle sections matching cards incl. reviewer LOW-A relocations (genie-official-roadmap Raw→Simmering; v4-home-residue-doctor + release-ops-hardening Ready→Poured); duplicate khal bullet removed; codex-plugin-update-handoff entry corrected (PR #2617 merged 2026-07-22, A–E shipped); stable-release facts (first v5.260727.5; current pinned via `gh release list` at execution); Work-lane priority order stated in prose (D4); no lifecycle entry with archive path as first link +2. `.genie/brainstorms/public-roadmap-polish/DRAFT.md` (publish roadmap outward + docs/onboarding sweep; seeded from this wish's D6) + INDEX Raw entry +3. Re-run link sweep over INDEX.md + full tree → 0 unresolved; final `qa/link-sweep.txt` +4. Commit (pre-commit sync publishes board to roadmap.json); verify post-commit `genie task sync` in-sync + +**Acceptance Criteria:** +- [ ] `genie doctor --json`: zero `drift` entries under `checks[].indexLane.entries` +- [ ] Sweep: 0 unresolved links tree-wide incl. INDEX.md +- [ ] Shipped section count = 29; grep finds no duplicate khal RELOCATED bullet +- [ ] roadmap.json diff vs baseline shows board + 19 cards; `genie task sync` → in-sync + +**Validation:** +```bash +bun run check && genie doctor --json | bun -e 'const d=JSON.parse(await Bun.stdin.text());const il=(d.checks||[]).find(c=>c.indexLane);if(!il)process.exit(1);const e=il.indexLane.entries;const ok=e.filter(x=>x.state==="ok").length;const drift=e.filter(x=>x.state==="drift").length;if(ok<18||drift>0)process.exit(1)' && genie task sync +``` +(Positive assertion required: `unlinked` never counts as drift, so a drift-only check passes vacuously on an unreconciled INDEX. 18 of the 19 cards carry wish slugs; each must resolve a lifecycle entry to `ok`.) + +**depends-on:** group-1, group-2 + +--- + +## QA Criteria + +- [ ] Fresh-clone ritual: clone, `genie task sync`, `genie board --board roadmap` shows the 19-card roadmap +- [ ] `genie board` (all tasks) still shows the 29 historical group tasks + tombstones untouched +- [ ] INDEX.md reads as the single roadmap of record (Felipe eyeball) + +--- + +## Assumptions / Risks + +| Risk | Severity | Mitigation | +|------|----------|------------| +| Link inventory misses a class | Low | Reviewer independently swept 111 links post-move in simulation: only the named classes break; sweep re-runs as gate | +| INDEX prose drift vs lanes after seeding | Low | doctor drift lint standing; sections written from the card enumeration | +| roadmap.json divergence during execution | Low | Single writer; three-way sync refuses divergence; commit at group boundaries | +| Stable-release fact stale by execution | Low | Pinned fresh via `gh release list` in G3 | + +--- + +## Review Results + +_The read-only reviewer returns evidence; the invoking orchestrator appends a timestamped block here after plan, execution, and PR reviews._ + +### Plan review — SHIP (2026-07-28T22:58:35Z, reviewer-subagent-fable5, 2 fix loops) + +- **Loop 0 → FIX-FIRST (22:52:41Z):** 3 MEDIUM + 3 LOW. M1 unimplementable adopt-branch for boardless pointer (no `board_id` mutation path; LaneError proven); M2 stale baseline (33 boardless rows at execution start, not 29 — invariant rescoped to the 29 historical rows, this wish's 4 rows exempt); M3 drift-only gate passes vacuously on unreconciled INDEX (`unlinked` never counts as drift) → positive `ok>=18 && drift===0`, threshold independently re-derived as exactly 18; L4 sweep had no command; L5 commit discipline (G1/G2 never commit — pre-commit sync would capture a partially-seeded board); L6 null-guard. +- **Loop 1 → FIX-FIRST (22:56:26Z):** 5/6 repairs verified by execution; new MEDIUM in the L4 repair itself — `| tee` swallowed the resolver's exit code (proven), and missing `qa/` masked it fail-closed for the wrong reason. +- **Loop 2 → SHIP (22:58:35Z):** gate executed on both branches — broken simulated post-move tree: `unresolved: 20` (exactly the specified inventory: 17+1+2), chain exit 1; rewritten tree: `unresolved: 0`, chain exit 0. `mkdir -p` in-chain; evidence file self-describing. All prior verifications re-confirmed (8/8 DESIGN criteria carried, LOW-A/LOW-B honored, `task block` executable on the ready pointer, wishes-lint OK). Informational, no action: the `#`-fragment `.split` is inert under the current regex; corpus has zero fragment links. + +**Execution amendment (orchestrator, post-approval):** plan approval landed before execution, so this wish's OWN card seeds in the **Wish** lane (not Brainstorm) and its INDEX entry sits in **Poured** (skill rule: plan SHIP → Poured; Poured↔Wish joins `ok`). The `ok>=18` threshold is unaffected. All other enumeration placements unchanged. + +--- + +## Files to Create/Modify + +``` +.genie/wishes/<29 slugs>/** -> .genie/wishes/archive//** (git mv + link edits in 17 files) +.genie/INDEX.md (rewrite) +.genie/brainstorms/public-roadmap-polish/DRAFT.md (new) +.genie/wishes/genie-official-roadmap/qa/link-sweep.txt (new, evidence) +.genie/roadmap.json (regenerated by pre-commit sync) +.genie/genie.db (board + 19 cards; not tracked) +``` From e7a1dfb68d5202365a505fef24b5c411e4fda96c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Jul 2026 23:03:24 +0000 Subject: [PATCH 17/22] chore(version): bump to 5.260728.14 [auto-version] --- .claude-plugin/marketplace.json | 2 +- package.json | 2 +- plugins/genie/.claude-plugin/plugin.json | 2 +- plugins/genie/.codex-plugin/plugin.json | 2 +- plugins/genie/package.json | 2 +- plugins/hermes-genie/plugin.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index af4b3c379..18409579f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "genie", - "version": "5.260728.13", + "version": "5.260728.14", "source": "./plugins/genie", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project." } diff --git a/package.json b/package.json index 1dcc64fef..baaf62ce7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@automagik/genie", - "version": "5.260728.13", + "version": "5.260728.14", "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", "license": "MIT", diff --git a/plugins/genie/.claude-plugin/plugin.json b/plugins/genie/.claude-plugin/plugin.json index 8f575c97b..4f575edd4 100644 --- a/plugins/genie/.claude-plugin/plugin.json +++ b/plugins/genie/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260728.13", + "version": "5.260728.14", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.", "author": { "name": "Namastex Labs" diff --git a/plugins/genie/.codex-plugin/plugin.json b/plugins/genie/.codex-plugin/plugin.json index 50fb05fe2..e2bdaf7d3 100644 --- a/plugins/genie/.codex-plugin/plugin.json +++ b/plugins/genie/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260728.13", + "version": "5.260728.14", "description": "Plan, execute, review, and ship software with Genie workflows in Codex.", "author": { "name": "Namastex Labs", diff --git a/plugins/genie/package.json b/plugins/genie/package.json index 4c249f006..2d37d711f 100644 --- a/plugins/genie/package.json +++ b/plugins/genie/package.json @@ -1,6 +1,6 @@ { "name": "genie-plugin", - "version": "5.260728.13", + "version": "5.260728.14", "private": true, "description": "Runtime dependencies for genie bundled CLIs", "license": "MIT", diff --git a/plugins/hermes-genie/plugin.yaml b/plugins/hermes-genie/plugin.yaml index e4358328e..08ef13c5b 100644 --- a/plugins/hermes-genie/plugin.yaml +++ b/plugins/hermes-genie/plugin.yaml @@ -1,5 +1,5 @@ name: genie -version: 5.260728.13 +version: 5.260728.14 description: "Native Hermes surface for Genie orchestration: read-only status plus work-plan and review-plan gap tools that the MCP board surface does not cover, hooks, commands, and a thin cockpit skill. Board/task truth comes from the genie MCP tools." provides_tools: # Default surface: exactly the three gap tools the MCP board surface does not cover. From 4ed54a2fc0ffc079162dd218b4e8a9dd04275510 Mon Sep 17 00:00:00 2001 From: namastex888 Date: Tue, 28 Jul 2026 21:54:56 -0300 Subject: [PATCH 18/22] fix(agent-sync): suppress the bare-name role-agent fan-out when the plugin is enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit syncClaude computed pluginEnabled and used it to suppress the bare-name skills mirror, but never passed it to syncClaudeAgentFiles. Role agents were fanned into ~/.claude/agents unconditionally while the plugin also served the same files as genie:*, so all seven double-listed in the agent picker — the exact waste the skills-mirror hotfix removed. Pass pluginEnabled through and feed syncClaudeAgentFiles an empty source set when it is true. Every manifest-owned file then flows through the existing planAgentOrphan path, which backs the bytes up before retiring them and preserves hand-edited files as kept-modified-orphan. The GENIE_FORCE_CLAUDE_SKILLS_MIRROR kill-switch already gates both surfaces because it lives inside claudeGeniePluginEnabled. Doctor is updated to match: with the plugin enabled the expected source set is empty, so a surviving fan-out reads as prunable stale rather than current, and a pruned one passes instead of reporting seven missing-from-target and advising `genie update` forever. The duplicate surface warning now tracks leftover files rather than merely the plugin being enabled, and points at the sync suggestion that actually resolves it. checkRoleAgents now uses claudeGeniePluginEnabled, dropping a duplicate settings reader that ignored the kill-switch. --- src/genie-commands/doctor.test.ts | 42 ++++++++++++++++++++++++ src/genie-commands/doctor.ts | 50 +++++++++++++++++------------ src/lib/agent-sync.test.ts | 53 ++++++++++++++++++++++++++++--- src/lib/agent-sync.ts | 21 ++++++++++-- 4 files changed, 138 insertions(+), 28 deletions(-) diff --git a/src/genie-commands/doctor.test.ts b/src/genie-commands/doctor.test.ts index 491a06ba3..8f1daa6c9 100644 --- a/src/genie-commands/doctor.test.ts +++ b/src/genie-commands/doctor.test.ts @@ -1593,6 +1593,48 @@ describe('checkAgentSync — claude role agents', () => { expect(find(results, ROLE_CHECK)?.roleAgents?.duplicateSurface).toBe(true); }); + test('plugin enabled → a surviving fan-out reads as prunable stale, not current', () => { + writeSourceAgent('scout', '# scout\n'); + writeTargetAgent('scout', '# scout\n'); + writeAgentManifest({ 'scout.md': { digest: computeFileDigest(join(agentsDir, 'scout.md')) } }); + writeSettings({ enabledPlugins: { 'genie@automagik': true } }); + + const check = find(checkAgentSync(paths()), ROLE_CHECK); + // Byte-identical to source, but the expected source set is empty while the + // plugin serves genie:* — so it is leftover state, not a current delivery. + expect(stateMap(check)['scout.md']).toBe('genie-managed-stale'); + expect(check?.status).toBe('warn'); + }); + + test('plugin enabled + manifest-only leftover (file gone) → stale warn but no duplicate warning', () => { + writeSourceAgent('scout', '# scout\n'); + // Manifest entry survives but the file was deleted: stale ownership + // metadata, yet nothing actually double-lists in the picker. + writeAgentManifest({ 'scout.md': { digest: 'f'.repeat(64) } }); + writeSettings({ enabledPlugins: { 'genie@automagik': true } }); + + const results = checkAgentSync(paths()); + const check = find(results, ROLE_CHECK); + expect(stateMap(check)['scout.md']).toBe('missing-from-target'); + expect(check?.status).toBe('warn'); + expect(find(results, DUP_CHECK)).toBeUndefined(); + expect(check?.roleAgents?.duplicateSurface).toBe(false); + }); + + test('plugin enabled + fan-out already pruned → passes with no duplicate warning', () => { + writeSourceAgent('scout', '# scout\n'); + writeSettings({ enabledPlugins: { 'genie@automagik': true } }); + + const results = checkAgentSync(paths()); + const check = find(results, ROLE_CHECK); + // Without this, every suppressed agent reads as missing-from-target and + // doctor advises `genie update` forever. + expect(check?.status).toBe('pass'); + expect(check?.detail).toContain('role-agent mirror suppressed'); + expect(find(results, DUP_CHECK)).toBeUndefined(); + expect(check?.roleAgents?.duplicateSurface).toBe(false); + }); + test('plugin disabled or absent → no duplicate warning, duplicateSurface flag false', () => { writeSourceAgent('scout', '# scout\n'); writeTargetAgent('scout', '# scout\n'); diff --git a/src/genie-commands/doctor.ts b/src/genie-commands/doctor.ts index 104ccca1f..0799a4128 100644 --- a/src/genie-commands/doctor.ts +++ b/src/genie-commands/doctor.ts @@ -1100,8 +1100,17 @@ function classifyRoleAgentFile( } /** Classify every source role agent (and any managed manifest entry) under `/agents`. */ -function classifyRoleAgents(pluginRoot: string, agentsDir: string): Omit { - const sourceState = sourceAgentDigests(pluginRoot); +function classifyRoleAgents( + pluginRoot: string, + agentsDir: string, + suppressed = false, +): Omit { + // Plugin enabled → sync suppresses the WHOLE bare-name role-agent fan-out + // (agents load as genie:* from the plugin), so the expected source set is + // empty and any surviving managed file is prunable leftover state. Without + // this, every suppressed agent reads as missing-from-target and doctor + // advises `genie update` forever — the same trap the skills check avoids. + const sourceState = suppressed ? { digests: new Map(), issues: [] } : sourceAgentDigests(pluginRoot); const source = sourceState.digests; const manifest = readAgentFilesManifestState(agentsDir); const entries = manifest.kind === 'managed' ? manifest.files : {}; @@ -1120,7 +1129,10 @@ function classifyRoleAgents(pluginRoot: string, agentsDir: string): Omit): { detail: string; stale: boolean } { +function roleAgentSummary( + delivery: Omit, + suppressed = false, +): { detail: string; stale: boolean } { const sourceProblem = delivery.sourceIssues.length === 0 ? null : `source inventory unavailable (${delivery.sourceIssues.join('; ')})`; if (delivery.manifestStatus === 'unsafe') { @@ -1130,9 +1142,11 @@ function roleAgentSummary(delivery: Omit) }; } if (delivery.files.length === 0) { + const empty = suppressed + ? 'role-agent mirror suppressed (genie@automagik plugin enabled)' + : 'no genie role agents detected'; return { - detail: - sourceProblem === null ? 'no genie role agents detected' : `${sourceProblem}; no genie role agents detected`, + detail: sourceProblem === null ? empty : `${sourceProblem}; ${empty}`, stale: sourceProblem !== null, }; } @@ -1152,16 +1166,6 @@ function roleAgentSummary(delivery: Omit) return { detail, stale }; } -/** `enabledPlugins["genie@automagik"] === true` in Claude Code's settings.json (unreadable → false). */ -function roleAgentDuplicateSurface(settingsPath: string): boolean { - try { - const settings = JSON.parse(readFileSync(settingsPath, 'utf8')) as { enabledPlugins?: Record }; - return settings.enabledPlugins?.['genie@automagik'] === true; - } catch { - return false; - } -} - /** * Per-file role-agent delivery check + the duplicate-surface warning. The * structured {@link RoleAgentDelivery} rides `--json` as `checks[].roleAgents` @@ -1169,9 +1173,14 @@ function roleAgentDuplicateSurface(settingsPath: string): boolean { * duplicate warning is emitted as its own line ONLY when the plugin is enabled. */ function checkRoleAgents(pluginRoot: string, claudeDir: string, settingsPath: string): CheckResult[] { - const classification = classifyRoleAgents(pluginRoot, join(claudeDir, 'agents')); - const duplicateSurface = roleAgentDuplicateSurface(settingsPath); - const { detail, stale } = roleAgentSummary(classification); + const pluginEnabled = claudeGeniePluginEnabled(settingsPath); + const classification = classifyRoleAgents(pluginRoot, join(claudeDir, 'agents'), pluginEnabled); + // Once the fan-out is pruned there is nothing left to double-list, so the + // duplicate warning tracks surviving bare-name files, not merely the plugin. + // missing-from-target is stale ownership metadata, not a file in the picker — + // the main check already warns about it; nothing actually double-lists. + const duplicateSurface = pluginEnabled && classification.files.some((f) => f.state !== 'missing-from-target'); + const { detail, stale } = roleAgentSummary(classification, pluginEnabled); const results: CheckResult[] = [ { name: 'agent sync: claude role agents', @@ -1186,9 +1195,8 @@ function checkRoleAgents(pluginRoot: string, claudeDir: string, settingsPath: st name: 'agent sync: duplicate role-agent surface', status: 'warn', detail: - 'genie@automagik plugin enabled — plugin `genie:*` agents and fanned bare-named agents both surface (duplicate listings)', - suggestion: - 'Keep the genie plugin disabled (bare-named agents are fanned by `genie update`), or expect duplicates.', + 'genie@automagik plugin enabled — plugin `genie:*` agents and leftover bare-named agents both surface (duplicate listings)', + suggestion: SYNC_SUGGESTION, }); } return results; diff --git a/src/lib/agent-sync.test.ts b/src/lib/agent-sync.test.ts index 8892bcf9b..5769c490d 100644 --- a/src/lib/agent-sync.test.ts +++ b/src/lib/agent-sync.test.ts @@ -1646,8 +1646,10 @@ describe('claude skills mirror suppression', () => { expect(existsSync(join(fixture.claudeDir, 'skills', 'beta'))).toBe(false); expect(readFileSync(join(report.backupsDir as string, 'claude', 'beta', 'SKILL.md'), 'utf8')).toBe('# beta\n'); expect(extraAction(claude, 'skills-mirror')).toBe('suppressed'); - // Non-skill deliveries are untouched by the suppression. - expect(existsSync(join(fixture.claudeDir, 'agents', 'reviewer.md'))).toBe(true); + // The role-agent fan-out is suppressed by the same switch; the council + // stamp is not a duplicated surface and stays delivered. + expect(extraAction(claude, 'agents-mirror')).toBe('suppressed'); + expect(existsSync(join(fixture.claudeDir, 'agents', 'reviewer.md'))).toBe(false); expect(existsSync(join(fixture.claudeDir, 'workflows', 'council.js'))).toBe(true); }); @@ -1660,8 +1662,9 @@ describe('claude skills mirror suppression', () => { expect(existsSync(join(fixture.claudeDir, 'skills', 'alpha'))).toBe(false); expect(existsSync(join(fixture.claudeDir, 'skills', 'beta'))).toBe(false); expect(extraAction(claude, 'skills-mirror')).toBe('suppressed'); - // Agents fan-out and the council stamp are independent of the skills mirror. - expect(agentFileAction(claude, 'reviewer')).toBe('created'); + // The agent fan-out is suppressed too; only the council stamp is written. + expect(agentFileAction(claude, 'reviewer')).toBeUndefined(); + expect(existsSync(join(fixture.claudeDir, 'agents', 'reviewer.md'))).toBe(false); expect(extraAction(claude, 'stamp')).toBe('written'); }); @@ -1730,6 +1733,45 @@ describe('claude skills mirror suppression', () => { expect(existsSync(join(fixture.claudeDir, 'skills', 'council'))).toBe(false); }); + test('enabling the plugin prunes a previously fanned role agent, backing the bytes up first', () => { + present(fixture.claudeDir); + run(); // seed reviewer.md as a managed fan-out + expect(existsSync(join(fixture.claudeDir, 'agents', 'reviewer.md'))).toBe(true); + writeClaudeSettings(true); + + const report = run(); + const claude = agentReport(report, 'claude'); + expect(agentFileAction(claude, 'reviewer')).toBe('removed'); + expect(existsSync(join(fixture.claudeDir, 'agents', 'reviewer.md'))).toBe(false); + expect(extraAction(claude, 'agents-mirror')).toBe('suppressed'); + // Retirement is backup-first: the bytes survive under the backups root. + expect(existsSync(join(report.backupsDir as string, 'claude', 'agents', 'reviewer.md'))).toBe(true); + }); + + test('a hand-edited role agent survives suppression instead of being pruned', () => { + present(fixture.claudeDir); + run(); + writeFile(join(fixture.claudeDir, 'agents', 'reviewer.md'), '# reviewer hand-edited\n'); + writeClaudeSettings(true); + + const claude = agentReport(run(), 'claude'); + expect(readFileSync(join(fixture.claudeDir, 'agents', 'reviewer.md'), 'utf8')).toBe('# reviewer hand-edited\n'); + expect(agentFileAction(claude, 'reviewer')).not.toBe('removed'); + }); + + test('re-disabling the plugin brings the role-agent fan-out back', () => { + present(fixture.claudeDir); + run(); + writeClaudeSettings(true); + expect(agentFileAction(agentReport(run(), 'claude'), 'reviewer')).toBe('removed'); + writeClaudeSettings(false); + + const claude = agentReport(run(), 'claude'); + expect(agentFileAction(claude, 'reviewer')).toBe('created'); + expect(existsSync(join(fixture.claudeDir, 'agents', 'reviewer.md'))).toBe(true); + expect(extraAction(claude, 'agents-mirror')).toBeUndefined(); + }); + test('GENIE_FORCE_CLAUDE_SKILLS_MIRROR=1 kill-switch forces the mirror despite an enabled plugin', () => { present(fixture.claudeDir); writeClaudeSettings(true); @@ -1740,6 +1782,9 @@ describe('claude skills mirror suppression', () => { expect(skillAction(claude, 'alpha')).toBe('created'); expect(skillAction(claude, 'beta')).toBe('created'); expect(extraAction(claude, 'skills-mirror')).toBeUndefined(); + // The one switch restores BOTH suppressed surfaces. + expect(agentFileAction(claude, 'reviewer')).toBe('created'); + expect(extraAction(claude, 'agents-mirror')).toBeUndefined(); } finally { if (prior === undefined) { // biome-ignore lint/performance/noDelete: process.env assignment coerces undefined→"undefined"; delete is the only correct unset diff --git a/src/lib/agent-sync.ts b/src/lib/agent-sync.ts index 4d08d0c5d..401d4d5ba 100644 --- a/src/lib/agent-sync.ts +++ b/src/lib/agent-sync.ts @@ -94,6 +94,8 @@ export const CLAUDE_EXCLUDED_SKILLS = new Set(['council']); * value, all return false — sync must never abort or change behavior on a * broken settings file. GENIE_FORCE_CLAUDE_SKILLS_MIRROR=1 is the ops * kill-switch: forces pre-hotfix behavior (mirror + doctor expects mirror). + * It governs BOTH suppressed surfaces — the bare-name skills mirror and the + * bare-name role-agent fan-out — despite the historical `SKILLS` in its name. */ export function claudeGeniePluginEnabled(settingsPath: string): boolean { if (process.env.GENIE_FORCE_CLAUDE_SKILLS_MIRROR === '1') return false; @@ -4555,8 +4557,13 @@ function unpublishFlatAgent(ctx: FlatAgentTxnCtx, state: FlatAgentOpState, targe * every unrelated sibling in `~/.claude/agents` remains invisible. All ops run * in ONE transaction with a single ownership commit. */ -function syncClaudeAgentFiles(ctx: RunContext, claudeDir: string, report: AgentReport): void { - const sourceAgents = enumerateSourceAgentFiles(ctx.pluginRoot); +function syncClaudeAgentFiles(ctx: RunContext, claudeDir: string, report: AgentReport, pluginEnabled = false): void { + // Plugin enabled → Claude Code already serves every role agent as genie:*; + // the bare-name fan-out double-lists them. An empty source set sends every + // manifest-owned file down planAgentOrphan, which backs the bytes up before + // retiring them and preserves user-edited files as kept-modified-orphan — + // the same suppression contract the skills mirror uses above. + const sourceAgents = pluginEnabled ? [] : enumerateSourceAgentFiles(ctx.pluginRoot); const targetDir = join(claudeDir, 'agents'); const initialState = inspectAgentFilesManifest(targetDir); if (initialState.kind === 'unsafe') { @@ -5664,7 +5671,15 @@ function syncClaude(ctx: RunContext, report: AgentReport): void { detail: 'genie@automagik plugin enabled — bare-name skills mirror pruned; skills load as genie:* from the plugin', }); } - syncClaudeAgentFiles(ctx, claudeDir, report); + syncClaudeAgentFiles(ctx, claudeDir, report, pluginEnabled); + if (pluginEnabled) { + report.extras.push({ + kind: 'agents-mirror', + action: 'suppressed', + detail: + 'genie@automagik plugin enabled — bare-name role-agent fan-out pruned; agents load as genie:* from the plugin', + }); + } stampClaudeWorkflow(ctx, claudeDir, report); } From 9be108a3281c304817a12ff595b2bbe60d05a528 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 29 Jul 2026 01:25:57 +0000 Subject: [PATCH 19/22] chore(version): bump to 5.260729.1 [auto-version] --- .claude-plugin/marketplace.json | 2 +- package.json | 2 +- plugins/genie/.claude-plugin/plugin.json | 2 +- plugins/genie/.codex-plugin/plugin.json | 2 +- plugins/genie/package.json | 2 +- plugins/hermes-genie/plugin.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 18409579f..002f52492 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "genie", - "version": "5.260728.14", + "version": "5.260729.1", "source": "./plugins/genie", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project." } diff --git a/package.json b/package.json index baaf62ce7..39341b9e5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@automagik/genie", - "version": "5.260728.14", + "version": "5.260729.1", "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", "license": "MIT", diff --git a/plugins/genie/.claude-plugin/plugin.json b/plugins/genie/.claude-plugin/plugin.json index 4f575edd4..e66ad1163 100644 --- a/plugins/genie/.claude-plugin/plugin.json +++ b/plugins/genie/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260728.14", + "version": "5.260729.1", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.", "author": { "name": "Namastex Labs" diff --git a/plugins/genie/.codex-plugin/plugin.json b/plugins/genie/.codex-plugin/plugin.json index e2bdaf7d3..1728ecf2b 100644 --- a/plugins/genie/.codex-plugin/plugin.json +++ b/plugins/genie/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260728.14", + "version": "5.260729.1", "description": "Plan, execute, review, and ship software with Genie workflows in Codex.", "author": { "name": "Namastex Labs", diff --git a/plugins/genie/package.json b/plugins/genie/package.json index 2d37d711f..71fb1a2c0 100644 --- a/plugins/genie/package.json +++ b/plugins/genie/package.json @@ -1,6 +1,6 @@ { "name": "genie-plugin", - "version": "5.260728.14", + "version": "5.260729.1", "private": true, "description": "Runtime dependencies for genie bundled CLIs", "license": "MIT", diff --git a/plugins/hermes-genie/plugin.yaml b/plugins/hermes-genie/plugin.yaml index 08ef13c5b..037214c3f 100644 --- a/plugins/hermes-genie/plugin.yaml +++ b/plugins/hermes-genie/plugin.yaml @@ -1,5 +1,5 @@ name: genie -version: 5.260728.14 +version: 5.260729.1 description: "Native Hermes surface for Genie orchestration: read-only status plus work-plan and review-plan gap tools that the MCP board surface does not cover, hooks, commands, and a thin cockpit skill. Board/task truth comes from the genie MCP tools." provides_tools: # Default surface: exactly the three gap tools the MCP board surface does not cover. From 7bd36b8b90b22e3aafdbd4bf6e5d5a31e7d22df5 Mon Sep 17 00:00:00 2001 From: namastex888 Date: Wed, 29 Jul 2026 00:39:10 -0300 Subject: [PATCH 20/22] fix(test): pin selection in the manual post-update convergence test (#2732) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test called runManualUpdateConvergence without a selection, so the default resolved the host machine's persisted integration consent (~/.genie/.integration-consent.json). On a codex-consent machine the agent-sync arm narrows to null, runSync never fires, and the test fails — while CI (no consent file) and all/auto machines pass. Same commit, same binary, different machine state. Pass selection: 'all' explicitly, matching every other caller in this file (the codex-only, auto, and hermes-leg tests already pin theirs). Closes #2732 --- src/genie-commands/__tests__/update.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/genie-commands/__tests__/update.test.ts b/src/genie-commands/__tests__/update.test.ts index 280f42410..1c864ed49 100644 --- a/src/genie-commands/__tests__/update.test.ts +++ b/src/genie-commands/__tests__/update.test.ts @@ -2455,6 +2455,9 @@ describe('manual post-update convergence (2026-07-11 cascade regression)', () => const result = runManualUpdateConvergence({ expectedVersion: '5.260711.3', bundleRoot: '/tmp/verified-bundle', + // Explicit selection: the default reads the host's persisted integration + // consent, so omitting it makes the test depend on machine state (#2732). + selection: 'all', runSync: () => calls.push('parent-safe-sync'), refreshPlugins: (options) => { calls.push(`parent-plugin-refresh:${options.expectedVersion}:${options.selection}`); From 594a273e691dc00dd0a2703c721ede3563f1efc1 Mon Sep 17 00:00:00 2001 From: Felipe Rosa Date: Wed, 29 Jul 2026 00:41:43 -0300 Subject: [PATCH 21/22] fix(v5): harden roadmap sync per PR #2729 bot-review findings - task export/import: resolve explicit paths before the canonical comparison, so a relative spelling of .genie/roadmap.json preserves local hires and records the sync baseline - task export --write : custom-file backups emit the complete state including hire_roster; only canonical roadmap.json gets the roadmap slice, keeping backup round-trips lossless - importState: --replace wipes all tables even when only meta rows exist, so stale wish_sig/backfill markers cannot merge into the snapshot; malformed rows that pass shape validation rethrow as SnapshotFormatError after rollback - syncRoadmap: compare-and-act runs under BEGIN IMMEDIATE, closing the window where a concurrent task write could be destroyed by a replace-import and serializing concurrent syncs - pre-commit: stage roadmap.json only when sync itself rewrote it, never absorbing pre-existing unstaged edits - post-checkout (new hook): run task sync after branch checkouts so the shared db stops exposing the previous branch's board - .hermes/SOUL.md: narrow the obsolete-serve prohibition so it cannot be read as banning the supported 'genie omni serve' - release-docs.test.ts: assert the security-sensitive bypass rule in router metadata too Co-Authored-By: Claude Fable 5 --- .hermes/SOUL.md | 6 +++--- .husky/post-checkout | 12 +++++++++++ .husky/pre-commit | 8 ++++++- scripts/release-docs.test.ts | 1 + src/lib/v5/roadmap-sync.ts | 11 ++++++++++ src/lib/v5/task-state.test.ts | 34 ++++++++++++++++++++++++++++++ src/lib/v5/task-state.ts | 24 +++++++++++++++------ src/term-commands/v5-task.test.ts | 35 +++++++++++++++++++++++++++++++ src/term-commands/v5-task.ts | 19 ++++++++++------- 9 files changed, 133 insertions(+), 17 deletions(-) create mode 100755 .husky/post-checkout diff --git a/.hermes/SOUL.md b/.hermes/SOUL.md index 9bdf85691..d4df99195 100644 --- a/.hermes/SOUL.md +++ b/.hermes/SOUL.md @@ -15,9 +15,9 @@ Genie v5 is task-backed and zero-daemon by default: - `genie launch ` opens isolated execution lanes when requested. - `genie omni` handles the optional Omni bridge and approval inbox. -Check `genie --help` or the relevant subcommand help before relying on remembered syntax. Never use obsolete -`serve`, `ls`, `task board`, `spawn`, or `--no-tui` surfaces, and never assume the retired -`/home/genie/workspace/agents` path. +Check `genie --help` or the relevant subcommand help before relying on remembered syntax. Never use the obsolete +top-level `genie serve` (the supported `genie omni serve` bridge is a different command), `ls`, `task board`, +`spawn`, or `--no-tui` surfaces, and never assume the retired `/home/genie/workspace/agents` path. ## Operating law diff --git a/.husky/post-checkout b/.husky/post-checkout new file mode 100755 index 000000000..1769dac97 --- /dev/null +++ b/.husky/post-checkout @@ -0,0 +1,12 @@ +# Reconcile the local genie.db with the canonical board snapshot +# (.genie/roadmap.json) after a branch checkout swapped the snapshot — +# post-merge/post-rewrite do not fire here, so without this hook the shared db +# keeps exposing the previous branch's board until the next sync. Same +# three-way `task sync` contract: fail-open, refuses on divergence, skipped in +# linked worktrees and for file (non-branch) checkouts ($3 = 0). +[ "$3" = "1" ] || exit 0 +git_dir=$(git rev-parse --path-format=absolute --git-dir 2>/dev/null) +git_common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) +if [ "$git_dir" = "$git_common" ] && [ -f src/genie.ts ] && [ -f .genie/roadmap.json ]; then + bun src/genie.ts task sync || true +fi diff --git a/.husky/pre-commit b/.husky/pre-commit index 88ef9cb85..8383508d3 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -21,8 +21,14 @@ fi git_dir=$(git rev-parse --path-format=absolute --git-dir 2>/dev/null) git_common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) if [ "$git_dir" = "$git_common" ] && [ -f src/genie.ts ]; then + # Stage roadmap.json only when the sync itself rewrote it (export) — never + # absorb pre-existing unstaged edits into an unrelated commit. + roadmap_before=$( [ -f .genie/roadmap.json ] && git hash-object .genie/roadmap.json || echo absent ) if bun src/genie.ts task sync; then - [ -f .genie/roadmap.json ] && git add .genie/roadmap.json + roadmap_after=$( [ -f .genie/roadmap.json ] && git hash-object .genie/roadmap.json || echo absent ) + if [ "$roadmap_after" != "$roadmap_before" ] && [ "$roadmap_after" != "absent" ]; then + git add .genie/roadmap.json + fi else echo "warn: board snapshot not refreshed (see message above) — committing without it" >&2 fi diff --git a/scripts/release-docs.test.ts b/scripts/release-docs.test.ts index a98fcc649..2a0863366 100644 --- a/scripts/release-docs.test.ts +++ b/scripts/release-docs.test.ts @@ -990,6 +990,7 @@ describe('Group E release and documentation contracts', () => { expect(lifecycle).toContain('Security-sensitive changes do not bypass by default'); expect(lifecycle).toContain('Related existing work always resumes through its persisted state'); expect(metadata).toContain('otherwise bypass it with a one-line notice'); + expect(metadata).toContain('Security-sensitive work does not bypass by default.'); }); test('wizard discloses init MCP writes and owner-qualified lifecycle order', () => { diff --git a/src/lib/v5/roadmap-sync.ts b/src/lib/v5/roadmap-sync.ts index 2961687c1..98c5553f5 100644 --- a/src/lib/v5/roadmap-sync.ts +++ b/src/lib/v5/roadmap-sync.ts @@ -107,6 +107,17 @@ export function recordSyncBaseline(db: Database, cwd?: string): void { * on pull (post-merge / post-rewrite) and before commit (pre-commit). */ export function syncRoadmap(db: Database, cwd?: string): SyncResult { + // BEGIN IMMEDIATE for the whole compare-and-act sequence: the db-side hash + // must not go stale between comparison and a replace-import, or a task write + // committed in that window would be silently destroyed. Holding the write + // lock up front also serializes two concurrent syncs (git hook vs interactive + // command) through busy_timeout, so their file read-decide-write sequences + // cannot interleave. importState's inner transaction nests as a savepoint. + const locked = db.transaction(() => syncRoadmapLocked(db, cwd)); + return locked.immediate() as SyncResult; +} + +function syncRoadmapLocked(db: Database, cwd?: string): SyncResult { const filePath = resolveRoadmapPath(cwd); const markerPath = resolveSyncMarkerPath(cwd); const dbState = roadmapSnapshot(db); diff --git a/src/lib/v5/task-state.test.ts b/src/lib/v5/task-state.test.ts index 0a466e99e..853d35a86 100644 --- a/src/lib/v5/task-state.test.ts +++ b/src/lib/v5/task-state.test.ts @@ -43,6 +43,7 @@ import { getTaskLane, getWishGroups, hireAgent, + importState, listBoards, listHires, listTasks, @@ -751,6 +752,39 @@ try { }, 30_000); }); +describe('importState — replace is a full wipe even without operational rows', () => { + test('stale meta keys do not survive a --replace into a metadata-only db', () => { + // Source db: one task, then export. Its meta carries the backfill marker. + const source = openDb({ path: join(dir, 'source.db') }); + createTask(source, { title: 'canonical card' }); + const snapshot = exportState(source); + source.close(); + + // Destination db: NO operational rows, but a stale meta key (e.g. a + // wish_sig left behind after its rows were cleared elsewhere). + db.query('INSERT INTO meta (key, value) VALUES (?, ?)').run('wish_sig:ghost', 'deadbeef'); + + importState(db, snapshot, { replace: true }); + + const keys = (db.query('SELECT key FROM meta ORDER BY key').all() as Array<{ key: string }>).map((r) => r.key); + expect(keys).toEqual(snapshot.meta.map((m) => m.key).sort()); + expect(keys).not.toContain('wish_sig:ghost'); + // The export is now the exact snapshot — lossless replacement. + expect(exportState(db)).toEqual(snapshot); + }); + + test('a malformed row that passes shape validation fails as SnapshotFormatError and rolls back', () => { + const before = exportState(db); + const bad = { + ...before, + // title NOT NULL in schema — null must not surface as a raw SQLite error. + tasks: [{ id: 't_bad', title: null, status: 'ready', created_at: 1, updated_at: 1 }], + }; + expect(() => importState(db, bad as unknown, { replace: true })).toThrow(/Snapshot rows could not be imported/); + expect(exportState(db)).toEqual(before); // transaction rolled back + }); +}); + // --------------------------------------------------------------------------- // Multi-PROCESS checkout race: N concurrent bun processes claim the same ready // task. Exactly one must win; every loser must get a typed conflict. This is diff --git a/src/lib/v5/task-state.ts b/src/lib/v5/task-state.ts index a122640e1..a257a9f71 100644 --- a/src/lib/v5/task-state.ts +++ b/src/lib/v5/task-state.ts @@ -1572,11 +1572,13 @@ export function importState(db: Database, snapshot: unknown, opts: ImportOptions const state = validateSnapshot(db, snapshot); const hires = opts.preserveHireRoster ? [] : state.hire_roster; const apply = db.transaction(() => { - if (hasOperationalState(db)) { - if (!opts.replace) throw new NonEmptyImportError(); - // Children before parents so FK cascades never fire mid-wipe. Meta is - // wiped too: replace means EXACT snapshot state, and stale local - // wish_sig/backfill markers would misdescribe the imported rows. + if (hasOperationalState(db) && !opts.replace) throw new NonEmptyImportError(); + if (opts.replace) { + // Wipe unconditionally under replace — a db with only meta rows (e.g. the + // backfill marker a fresh open stamps) must not merge stale keys into the + // snapshot's meta: replace means EXACT snapshot state, and a retained + // wish_sig marker would misdescribe the imported rows as drifted. + // Children before parents so FK cascades never fire mid-wipe. const wipe = ['task_events', 'stage_log', 'task_dependencies', 'tasks', 'wish_groups', 'boards', 'meta']; if (!opts.preserveHireRoster) wipe.splice(4, 0, 'hire_roster'); for (const table of wipe) { @@ -1585,7 +1587,17 @@ export function importState(db: Database, snapshot: unknown, opts: ImportOptions } insertSnapshotRows(db, { ...state, hire_roster: hires }); }); - apply(); + try { + apply(); + } catch (err) { + if (err instanceof SnapshotFormatError || err instanceof NonEmptyImportError) throw err; + // A malformed row that passed shape validation (roadmap.json survives git + // merges) surfaces as an opaque SQLite bind error — rethrow it in the same + // actionable class as the structural checks. The transaction rolled back. + throw new SnapshotFormatError( + `Snapshot rows could not be imported: ${err instanceof Error ? err.message : String(err)}. The database was left unchanged; re-export the snapshot or repair the malformed rows.`, + ); + } return { boards: state.boards.length, tasks: state.tasks.length, diff --git a/src/term-commands/v5-task.test.ts b/src/term-commands/v5-task.test.ts index 7f64782d0..1e64d02e0 100644 --- a/src/term-commands/v5-task.test.ts +++ b/src/term-commands/v5-task.test.ts @@ -443,6 +443,41 @@ describe('roadmap.json canonical sync', () => { expect(hires).toEqual([{ wish: 'w', worktree: '/tmp/wt' }]); }); + test('explicit relative canonical path behaves like the default; custom-file exports stay lossless', async () => { + const db = openDb({ cwd: repo }); + createTask(db, { title: 'card' }); + hireAgent(db, { wish: 'w', agentAdapterId: 'claude', worktree: '/tmp/wt' }); + db.close(); + + // Custom-file --write carries the COMPLETE state (hire_roster included), so + // a backup.json → import --replace round-trip cannot silently drop hires. + const backup = await cli(repo, 'export', '--write', 'backup.json'); + expect(backup.code).toBe(0); + const backupState = JSON.parse(readFileSync(join(repo, 'backup.json'), 'utf-8')) as StateExport; + expect(backupState.hire_roster).toHaveLength(1); + + // The canonical path spelled explicitly (relative) still emits the roadmap + // slice and still counts as canonical on import: local hires preserved. + const w = await cli(repo, 'export', '--write', '.genie/roadmap.json'); + expect(w.code).toBe(0); + const snap = JSON.parse(snapshotOf(repo)) as StateExport; + expect(snap.hire_roster).toEqual([]); + + const r = await cli(repo, 'import', '.genie/roadmap.json', '--replace'); + expect(r.code).toBe(0); + const db2 = openDb({ cwd: repo }); + const hires = db2.query('SELECT wish, worktree FROM hire_roster').all() as Array<{ + wish: string; + worktree: string; + }>; + db2.close(); + expect(hires).toEqual([{ wish: 'w', worktree: '/tmp/wt' }]); + + // The explicit spelling also recorded the sync baseline: no divergence. + const settled = await cli(repo, 'sync'); + expect(settled.code).toBe(0); + }); + 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 }); diff --git a/src/term-commands/v5-task.ts b/src/term-commands/v5-task.ts index ca63743d9..bc9e74c8e 100644 --- a/src/term-commands/v5-task.ts +++ b/src/term-commands/v5-task.ts @@ -18,6 +18,7 @@ import type { Database } from 'bun:sqlite'; import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; import type { Command } from 'commander'; import { color, formatTimestamp, padRight, truncate } from '../lib/term-format.js'; import { livenessBadge } from '../lib/v5/card-render.js'; @@ -428,16 +429,18 @@ function handleExport(opts: ExportOptions): void { run(() => { const db = openDb(); try { - // --write publishes the roadmap slice (no hire_roster — machine-local - // worktree paths); the plain stdout dump stays the complete database. - const state = opts.write ? roadmapSnapshot(db) : exportState(db); + // Only the canonical roadmap.json gets the roadmap slice (no hire_roster — + // machine-local worktree paths). Custom-file writes and the plain stdout + // dump stay the complete database, so a backup file round-trips lossless. + const target = opts.write ? (typeof opts.write === 'string' ? resolve(opts.write) : resolveRoadmapPath()) : null; + const canonical = target === resolveRoadmapPath(); + const state = canonical ? roadmapSnapshot(db) : exportState(db); const json = `${JSON.stringify(state, null, 2)}\n`; - if (opts.write) { - const target = typeof opts.write === 'string' ? opts.write : resolveRoadmapPath(); + if (target) { writeFileSync(target, json); // Writing the canonical snapshot declares "this pair is intentional" — // it is the keep-the-local-board resolution for a diverged sync. - if (target === resolveRoadmapPath()) recordSyncBaseline(db); + if (canonical) recordSyncBaseline(db); out(`Wrote board snapshot to ${target}.`); return; } @@ -467,7 +470,9 @@ function handleSync(): void { function handleImport(file: string | undefined, opts: ImportOptions): void { run(() => { - const source = file ?? resolveRoadmapPath(); + // Normalize before the canonical comparison: an explicit relative spelling + // of the roadmap path must behave exactly like omitting it. + const source = file ? resolve(file) : resolveRoadmapPath(); if (!existsSync(source)) { fail(`Snapshot not found: ${source}. Generate one with \`genie task export --write\` and commit it.`); } From 4b6dd1544c09e3a6950aee309a9f0fb3e76b3aff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 29 Jul 2026 03:51:24 +0000 Subject: [PATCH 22/22] chore(version): bump to 5.260729.2 [auto-version] --- .claude-plugin/marketplace.json | 2 +- package.json | 2 +- plugins/genie/.claude-plugin/plugin.json | 2 +- plugins/genie/.codex-plugin/plugin.json | 2 +- plugins/genie/package.json | 2 +- plugins/hermes-genie/plugin.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 002f52492..1cd8e9080 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "genie", - "version": "5.260729.1", + "version": "5.260729.2", "source": "./plugins/genie", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project." } diff --git a/package.json b/package.json index 39341b9e5..632ca37b9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@automagik/genie", - "version": "5.260729.1", + "version": "5.260729.2", "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", "license": "MIT", diff --git a/plugins/genie/.claude-plugin/plugin.json b/plugins/genie/.claude-plugin/plugin.json index e66ad1163..563182678 100644 --- a/plugins/genie/.claude-plugin/plugin.json +++ b/plugins/genie/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260729.1", + "version": "5.260729.2", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.", "author": { "name": "Namastex Labs" diff --git a/plugins/genie/.codex-plugin/plugin.json b/plugins/genie/.codex-plugin/plugin.json index 1728ecf2b..067a1a760 100644 --- a/plugins/genie/.codex-plugin/plugin.json +++ b/plugins/genie/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260729.1", + "version": "5.260729.2", "description": "Plan, execute, review, and ship software with Genie workflows in Codex.", "author": { "name": "Namastex Labs", diff --git a/plugins/genie/package.json b/plugins/genie/package.json index 71fb1a2c0..754b26d19 100644 --- a/plugins/genie/package.json +++ b/plugins/genie/package.json @@ -1,6 +1,6 @@ { "name": "genie-plugin", - "version": "5.260729.1", + "version": "5.260729.2", "private": true, "description": "Runtime dependencies for genie bundled CLIs", "license": "MIT", diff --git a/plugins/hermes-genie/plugin.yaml b/plugins/hermes-genie/plugin.yaml index 037214c3f..ebff84aa9 100644 --- a/plugins/hermes-genie/plugin.yaml +++ b/plugins/hermes-genie/plugin.yaml @@ -1,5 +1,5 @@ name: genie -version: 5.260729.1 +version: 5.260729.2 description: "Native Hermes surface for Genie orchestration: read-only status plus work-plan and review-plan gap tools that the MCP board surface does not cover, hooks, commands, and a thin cockpit skill. Board/task truth comes from the genie MCP tools." provides_tools: # Default surface: exactly the three gap tools the MCP board surface does not cover.